分享一个大二课程设计的自选课题“手势控制播放器”,。
利用OpenCV跨平台计算机视觉库模拟计算机视觉,实时检测用户的手势动作并翻译为计算机指令并且执行,本次实验以网易云音乐播放器为实验软件(酷狗、酷我、QQ等音乐播放器也可用)实现用户手势操作播放/暂停、上一首、下一首简单操作。

工具:

c++、Opencv、 网易云音乐播放器 、当然还有你的hand!

  • 系统结构分析

  • 流程图

  • 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
#include "stdafx.h"
#include "CVObject.h"

#include <cmath>
#include <queue>
#include <algorithm>

using namespace std;

void ThresholdBidirection(Mat & img, int lower, int upper) {
if (img.channels() != 1)
throw exception("图像通道数目不为1");

for (int h = 0; h < img.rows; h++) {
for (int w = 0; w < img.cols; w++) {
uchar * data = &img.at<uchar>(w, h);
if (*data <= upper && *data >= lower) {
*data = 255;
}
else {
*data = 0;
}
}
}
}

void FindTargets(const Mat & img, const int erea_threshold, Rect & rect, Mat & filtered) {
if (img.channels() != 1)
throw exception("FindTargets : 通道数必须为 1");
vector<vector<Point>> contours; // contours被定义成二维浮点型向量,这里面将来会存储找到的边界的(x,y)坐标。
findContours(img, contours, CV_RETR_EXTERNAL, CHAIN_APPROX_NONE);
vector<Point> max_contour; //寻找面积最大的轮廓画矩形框架
double max_area = 0;
for (auto item : contours) {
auto area = contourArea(item);
if (area > max_area && area > erea_threshold) {
max_area = area;
max_contour = item;
}
}

rect = Rect();
if (max_contour.size() > 0)
rect = boundingRect(max_contour);

filtered = Mat::zeros(img.rows, img.cols, CV_8UC3); //指定数组的形状的整数数组
drawContours(filtered, vector<vector<Point>>{max_contour}, -1, Scalar(255, 255, 255), CV_FILLED); //轮廓绘制函数指定范围
rectangle(filtered, rect, Scalar(0, 255, 255), 3);

#ifdef DEBUG
imshow("contours", filtered);
#endif
}

bool Locus::InRegionLeftOfTurn(Point p, Size winSize) {
if (p.x < winSize.width / 3) {
return true;
}
return false;
}

bool Locus::InRegionRightOfTurn(Point p, Size winSize) {
if (p.x>winSize.width * 2 / 3) {
return true;
}
return false;
} //轨迹添加中心点
void Locus::addPoint(Point p, Size winSize) {
tracks.push_back(p);
newPoint = p;
}

Gesture Locus::analyseLocus() {
Gesture g = NOOPERATION;

Point first = tracks.front();
if (InRegionRightOfTurn(first, winSize) && InRegionLeftOfTurn(newPoint, winSize)) {
g = PREVIOUS;
tracks.clear();
tracks.push_back(newPoint);
}
else if (InRegionLeftOfTurn(first, winSize) && InRegionRightOfTurn(newPoint, winSize)) {
g = NEXT;
tracks.clear();
tracks.push_back(newPoint);
}
else if (InRegionTop(first, winSize) && InRegionBottom(newPoint, winSize)) {
g = PAUSEPLAY;
tracks.clear();
tracks.push_back(newPoint);
}
else if (InRegionBottom(first, winSize) && InRegionTop(newPoint, winSize)) {
g = PAUSEPLAY;
tracks.clear();
tracks.push_back(newPoint);
}

return g;
}
//取中心区域的顶部
bool Locus::InRegionTop(Point p, Size winSize) {
if (p.y < winSize.height / 3) {
return true;
}
return false;
}
//取中心区域的底部
bool Locus::InRegionBottom(Point p, Size winSize) {
if (p.y > winSize.height * 2 / 3) {
return true;
}
return false;
}






#include "stdafx.h"
#include "CVObject.h"
#include <exception>


void sendHotKey(BYTE key) { //创建热键
keybd_event(VK_CONTROL, 0, 0, 0);
keybd_event(VK_MENU, 0, 0, 0);
keybd_event(key, 0, 0, 0);
keybd_event(key, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
}
//发送内部命令
void postCommand(Gesture g) {
switch (g) {
case NOOPERATION:
return;
case PREVIOUS:
sendHotKey(VK_RIGHT);
cout << "previous" << endl; //触发上一首
break;
case NEXT:
sendHotKey(VK_LEFT);
cout << "next" << endl; //触发下一首
break;
case PAUSEPLAY:
sendHotKey('X');
cout << "pause/play" << endl; //触发暂停/播放
break;
default:
return;
}
}

bool PreSet(int & threshold_lower, int & threshold_higher) {
namedWindow("skin", CV_WINDOW_NORMAL);
namedWindow("video", CV_WINDOW_NORMAL);
//为皮肤窗口创建轨迹条
createTrackbar("Lower Threshold", "skin", &threshold_lower, 255, NULL);
createTrackbar("Higher Threshold2", "skin", &threshold_higher, 255, NULL);

moveWindow("skin", 0, 320);
moveWindow("video", 0, 0);

return true;
}

VideoCapture openCamera(int cameraIndex) {
VideoCapture cap(cameraIndex);
if (!cap.isOpened())
throw exception("初始化相机失败");

return cap;
}

Mat getFrame(VideoCapture & cap) {
Mat img;
bool ret = cap.read(img);
if (!ret)
throw exception("读取相机图像失败");

return img;
}
//框架句柄
void FramePreHandle(Mat ImgFrame, int & threshold_lower, int & threshold_higher, Mat & cb, Mat & ImgFrameSmall) {
Mat ImgSkin;
vector<Mat> channels; //用来存放三个通道的像素值
resize(ImgFrame, ImgFrameSmall, winSize);
cvtColor(ImgFrameSmall, ImgSkin, CV_BGR2YCrCb); //转YCrCb
split(ImgSkin, channels);
ThresholdBidirection(channels[2], threshold_lower, threshold_higher);

Mat element = getStructuringElement(MORPH_RECT, Size(5, 5));
morphologyEx(channels[2], channels[2], MORPH_OPEN, element);
cb = channels[2];

#ifdef DEBUG
imshow("cb", cb); //显示cd窗口
#endif
}

bool SetControlUI(const Size & winSize, Mat & ImgSkin) {
ImgSkin = Mat::zeros(winSize.height, winSize.width, CV_8UC3);

rectangle(ImgSkin, Point(0, 0), Point(winSize.width / 6, winSize.height), Scalar(0, 255, 255), CV_FILLED);
rectangle(ImgSkin, Point(winSize.width * 5 / 6, 0), Point(winSize.width, winSize.height), Scalar(0, 255, 255), CV_FILLED);
//指定的位置和大小初始化 rectangle 类的新实例。
return true;
}

int main(int argc, char**argv) {
try {
int cameraIndex = 0;
if (argc == 2) {
cameraIndex = argv[1][0] - '0';
}

int threshold_lower = 0, threshold_higher = 122;
PreSet(threshold_lower, threshold_higher);
VideoCapture cap = openCamera(cameraIndex);
Sleep(1000);
Mat ImgFrame;

Locus locusAnalyser; //轨迹分析

while (true) {
ImgFrame = getFrame(cap);
if (ImgFrame.empty())
break;

Mat cb, ImgSkin, ImgFrameSmall, ImgSkin_s;
FramePreHandle(ImgFrame, threshold_lower, threshold_higher, cb, ImgFrameSmall);
SetControlUI(winSize, ImgSkin);
Rect target_rect;
FindTargets(cb, 1000, target_rect, ImgSkin_s);
// findContours会改变原图
if (target_rect.width > 0) {
rectangle(ImgFrameSmall, target_rect, Scalar(255, 0, 0), 3);
Point center(target_rect.x + target_rect.width / 2, target_rect.y + target_rect.height / 2);
ImgSkin += ImgSkin_s;

//在目标中心画十字
line(ImgSkin, Point(center.x - 5, center.y - 5), Point(center.x + 5, center.y + 5), Scalar(0, 0, 255), 3);
line(ImgSkin, Point(center.x + 5, center.y - 5), Point(center.x - 5, center.y + 5), Scalar(0, 0, 255), 3);

// 轨迹
locusAnalyser.addPoint(center, winSize);
Gesture g = locusAnalyser.analyseLocus();

// 手势姿态
postCommand(g);
}
else {
locusAnalyser.reset();
}


imshow("skin", ImgSkin);
imshow("video", ImgFrameSmall);

if (waitKey(3) == 27)
break;
}

return 0;
}
catch (exception & e) {
cerr << e.what() << endl;
return -1;
}
}
  • YCrCb空间的肤色提取

YCrCb即YUV色彩空间,Y是亮度的分量,而肤色侦测是对亮度比较敏感的,由摄像头拍摄的BGR图像转化为YCrCb空间的话可以去除亮度对肤色的侦测影响。

例如:

原图

处理后

  • 项目演示截图

截图一,调整阈值到最佳

左侧移动到右侧

瞎玩

这个。。。也是瞎玩