OpenCV簡單粗糙的指尖檢測方法(FingerTips Detection)
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
在人機互動領域,如果可以比較好的檢測指尖,對於互動的豐富度、靈活性來說是有很大提升的。目前指尖檢測的方法也很多,我這裡稍微嘗試了下簡單了兩種。這兩種方法都藉助了手的幾何特徵,簡單但比較粗糙,魯棒性不夠。
方法一:重心距離法
見下圖,紅色點是手的重心,那麼手的邊緣的所有點與重心點的距離按順時針方向或者逆時針方向遍歷,就會出現五個峰值,分別是五個手指,這樣我們就可以簡單找到了。如果你是隻伸出一兩個手指,那麼就只有一兩個峰值了。
簡單的程式碼如下:
1、對影象做高斯模糊;
2、膚色分割(背景不要有類膚色,如果有,就需要加其他資訊來排除干擾);
3、找到手輪廓;
4、計算矩,即重心;
5、尋找指尖。
// Simple FingerTips Detection // Author : Zouxy // Date : 2013-3-23 // HomePage : http://blog.csdn.net/zouxy09 // Email : [email protected] #include "opencv2/opencv.hpp" using namespace cv; using namespace std; void skinExtract(const Mat &frame, Mat &skinArea) ; int main(int argc, char* argv[]) { Mat frame, skinArea; VideoCapture capture; capture.open(0); if (!capture.isOpened()) { cout<<"No camera!\n"<<endl; return -1; } while (1) { capture >> frame; //Mat frame = imread("fingertips(1).jpg"); if (frame.empty()) break; skinArea.create(frame.rows, frame.cols, CV_8UC1); skinExtract(frame, skinArea); Mat show_img; frame.copyTo(show_img, skinArea); vector<vector<Point> > contours; vector<Vec4i> hierarchy; //尋找輪廓 findContours(skinArea, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE); // 找到最大的輪廓 int index; double area, maxArea(0); for (int i=0; i < contours.size(); i++) { area = contourArea(Mat(contours[i])); if (area > maxArea) { maxArea = area; index = i; } } //drawContours(frame, contours, index, Scalar(0, 0, 255), 2, 8, hierarchy ); Moments moment = moments(skinArea, true); Point center(moment.m10/moment.m00, moment.m01/moment.m00); circle(show_img, center, 8 ,Scalar(0, 0, 255), CV_FILLED); // 尋找指尖 vector<Point> couPoint = contours[index]; vector<Point> fingerTips; Point tmp; int max(0), count(0), notice(0); for (int i = 0; i < couPoint.size(); i++) { tmp = couPoint[i]; int dist = (tmp.x - center.x) * (tmp.x - center.x) + (tmp.y - center.y) * (tmp.y - center.y); if (dist > max) { max = dist; notice = i; } // 計算最大值保持的點數,如果大於40(這個值需要設定,本來想根據max值來設定, // 但是不成功,不知道為何),那麼就認為這個是指尖 if (dist != max) { count++; if (count > 40) { count = 0; max = 0; bool flag = false; // 低於手心的點不算 if (center.y < couPoint[notice].y ) continue; // 離得太近的不算 for (int j = 0; j < fingerTips.size(); j++) { if (abs(couPoint[notice].x - fingerTips[j].x) < 20) { flag = true; break; } } if (flag) continue; fingerTips.push_back(couPoint[notice]); circle(show_img, couPoint[notice], 6 ,Scalar(0, 255, 0), CV_FILLED); line(show_img, center, couPoint[notice], Scalar(255, 0, 0), 2); } } } imshow("show_img", show_img); if ( cvWaitKey(20) == 'q' ) break; } return 0; } //膚色提取,skinArea為二值化膚色影象 void skinExtract(const Mat &frame, Mat &skinArea) { Mat YCbCr; vector<Mat> planes; //轉換為YCrCb顏色空間 cvtColor(frame, YCbCr, CV_RGB2YCrCb); //將多通道影象分離為多個單通道影象 split(YCbCr, planes); //運用迭代器訪問矩陣元素 MatIterator_<uchar> it_Cb = planes[1].begin<uchar>(), it_Cb_end = planes[1].end<uchar>(); MatIterator_<uchar> it_Cr = planes[2].begin<uchar>(); MatIterator_<uchar> it_skin = skinArea.begin<uchar>(); //人的面板顏色在YCbCr色度空間的分佈範圍:100<=Cb<=127, 138<=Cr<=170 for( ; it_Cb != it_Cb_end; ++it_Cr, ++it_Cb, ++it_skin) { if (138 <= *it_Cr && *it_Cr <= 170 && 100 <= *it_Cb && *it_Cb <= 127) *it_skin = 255; else *it_skin = 0; } //膨脹和腐蝕,膨脹可以填補凹洞(將裂縫橋接),腐蝕可以消除細的凸起(“斑點”噪聲) dilate(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1)); erode(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1)); }
方法二:曲率分析法
見下圖,可以看到,在指尖和兩指之間的凹槽的曲率是最大的。假設手的輪廓是通過一系列點{Pi}來儲存的,那麼我們可以通過 [Pi, Pi-k] 和 [Pi, Pi+k] 兩個向量的內積(點乘)來尋找高曲率的點,如下圖的Φ和β的計算結果,因為他們的兩個向量的夾角趨向於90度,也就是它們的內積趨向於0,所以內積結果越小,曲率越大,我們只需要設定一個閾值,就可以把手的指尖和兩指之間的凹槽都可以找到。
那麼如何分辨指尖和兩指之間的凹槽呢?我們可以通過兩個向量的叉乘來計算,我們想象下手處於3D空間中,那麼就還有一個z方向,β中兩個向量的叉乘是大於0的(叉乘方向垂直紙面朝向你,z軸正方向),而Φ處的叉乘是小於0的(叉乘方向垂直紙面遠離你,z軸負方向)。
綜上,通過在每個點上構造兩個向量,比較他們的點乘和叉乘就可以確定指尖了。
簡單的程式碼如下:
1、對影象做高斯模糊;
2、膚色分割(背景不要有類膚色,如果有,就需要加其他資訊來排除干擾);
3、找到手輪廓;
4、尋找指尖。
// Simple FingerTips Detection Using Curvature Determination // Author : Zouxy // Date : 2013-3-23 // HomePage : http://blog.csdn.net/zouxy09 // Email : [email protected] #include "opencv2/opencv.hpp" using namespace cv; using namespace std; void skinExtract(const Mat &frame, Mat &skinArea); int main(int argc, char* argv[]) { Mat frame, skinArea; VideoCapture capture; capture.open(0); if (!capture.isOpened()) { cout<<"No camera!\n"<<endl; return -1; } while (1) { capture >> frame; //Mat frame = imread("fingertips(1).jpg"); if (frame.empty()) break; GaussianBlur(frame, frame, Size(3, 3), 0); skinArea.create(frame.rows, frame.cols, CV_8UC1); skinExtract(frame, skinArea); Mat show_img; frame.copyTo(show_img, skinArea); vector<vector<Point> > contours; vector<Vec4i> hierarchy; //尋找輪廓 findContours(skinArea, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE); // 找到最大的輪廓 int index; double area, maxArea(0); for (int i=0; i < contours.size(); i++) { area = contourArea(Mat(contours[i])); if (area > maxArea) { maxArea = area; index = i; } } //drawContours(frame, contours, index, Scalar(0, 0, 255), 2, 8, hierarchy ); Moments moment = moments(skinArea, true); Point center(moment.m10/moment.m00, moment.m01/moment.m00); circle(show_img, center, 8 ,Scalar(0, 0, 255), CV_FILLED); // 尋找指尖 vector<Point> couPoint = contours[index]; int max(0), count(0), notice(0); vector<Point> fingerTips; Point p, q, r; for (int i = 5; (i < (couPoint.size() - 5)) && couPoint.size(); i++) { q = couPoint[i - 5]; p = couPoint[i]; r = couPoint[i + 5]; int dot = (q.x - p.x ) * (q.y - p.y) + (r.x - p.x ) * (r.y - p.y); if (dot < 20 && dot > -20) { int cross = (q.x - p.x ) * (r.y - p.y) - (r.x - p.x ) * (q.y - p.y); if (cross > 0) { fingerTips.push_back(p); circle(show_img, p, 5 ,Scalar(255, 0, 0), CV_FILLED); line(show_img, center, p, Scalar(255, 0, 0), 2); } } } imshow("show_img", show_img); if ( cvWaitKey(20) == 'q' ) break; } return 0; } //膚色提取,skinArea為二值化膚色影象 void skinExtract(const Mat &frame, Mat &skinArea) { Mat YCbCr; vector<Mat> planes; //轉換為YCrCb顏色空間 cvtColor(frame, YCbCr, CV_RGB2YCrCb); //將多通道影象分離為多個單通道影象 split(YCbCr, planes); //運用迭代器訪問矩陣元素 MatIterator_<uchar> it_Cb = planes[1].begin<uchar>(), it_Cb_end = planes[1].end<uchar>(); MatIterator_<uchar> it_Cr = planes[2].begin<uchar>(); MatIterator_<uchar> it_skin = skinArea.begin<uchar>(); //人的面板顏色在YCbCr色度空間的分佈範圍:100<=Cb<=127, 138<=Cr<=170 for( ; it_Cb != it_Cb_end; ++it_Cr, ++it_Cb, ++it_skin) { if (138 <= *it_Cr && *it_Cr <= 170 && 100 <= *it_Cb && *it_Cb <= 127) *it_skin = 255; else *it_skin = 0; } //膨脹和腐蝕,膨脹可以填補凹洞(將裂縫橋接),腐蝕可以消除細的凸起(“斑點”噪聲) dilate(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1)); erode(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1)); }
效果: