前言
这篇文章是本人玩kinect时做的一个小实验,即不采用机器学习等类似AI的方法来做简单的手势数字识别,当然了,该识别的前提是基于本人前面已提取出手部的博文的基础上进行的。由于是纯数学形状上来判别手势,所以只是做了个简单的0~5的数字识别系统,其手势的分割部分效果还不错(因为其核心代码是由OpenNI提供的),手势数字识别时容易受干扰,效果一般般,毕竟这只是个简单的实验。
实验基础
首先来看下本系统的流程图,如下所示:
其中轮廓的提取,多边形拟合曲线的求法,凸包集和凹陷集的求法都是采用opencv中自带的函数。手势数字的识别是利用凸包点以及凹陷点和手部中心点的几何关系,简单的做了下逻辑判别了(可以肯定的是这种方法很烂),具体的做法是先在手部定位出2个中心点坐标,这2个中心点坐标之间的距离阈值由程序设定,其中一个中心点就是利用OpenNI跟踪得到的手部位置。有了这2个中心点的坐标,在程序中就可以分别计算出在这2个中心点坐标上的凸凹点的个数。当然了,这样做的前提是用人在做手势表示数字的同时应该是将手指的方向朝上(因为没有像机器学习那样通过样本来训练,所以使用时条件要苛刻很多)。利用上面求出的4种点的个数(另外程序中还设置了2个辅助计算点的个数,具体见代码部分)和简单的逻辑判断就可以识别出数字0~5了。其它的数字可以依照具体的逻辑去设计(还可以设计出多位数字的识别),只是数字越多设计起来越复杂,因为要考虑到它们之间的干扰性,且这种不通用的设计方法也没有太多的实际意义。
OpenCV知识点总结
void convexityDefects(InputArray contour, InputArray convexhull, OutputArray convexityDefects)
这个在函数在前面的博文中已经介绍过,当时是这么解释的:
该函数的作用是对输入的轮廓contour,凸包集合来检测其轮廓的凸型缺陷,一个凸型缺陷结构体包括4个元素,缺陷起点坐标,缺陷终点坐标,缺陷中离凸包线距离最远的点的坐标,以及此时最远的距离。参数3即其输出的凸型缺陷结构体向量。
其凸型缺陷的示意图如下所示:
不过这里需要重新对这3个参数做个详细的说明:
第1个参数虽然写的是contour,字面意思是轮廓,但是本人实验过很多次,发现如果该参数为目标通过轮廓检测得到的原始轮廓的话,则程序运行到onvexityDefects()函数时会报内存错误。因此本程序中采用的不是物体原始的轮廓,而是经过多项式曲线拟合后的轮廓,即多项式曲线,这样程序就会顺利地运行得很好。另外由于在手势识别过程中可能某一帧检测出来的轮廓非常小(由于某种原因),以致于少到只有1个点,这时候如果程序运行到onvexityDefects()函数时就会报如下的错误:
查看opencv源码中对应错误提示的位置,其源码如下:
表示在1969行的位置出错,也就是CV_Assert( ptnum > 3 );出错,说明出错时此处的ptnum <=3;看上面一行代码ptnum = points.checkVector(2, CV_32S);所以我们需要了解checkVector()函数的功能,进入opencv中关于checkVector的源码,如下:
int Mat::checkVector(int _elemChannels, int _depth, bool _requireContinuous) const{ return (depth() == _depth || _depth <= 0) && (isContinuous() || !_requireContinuous) && ((dims == 2 && (((rows == 1 || cols == 1) && channels() == _elemChannels) || (cols == _elemChannels))) || (dims == 3 && channels() == 1 && size.p[2] == _elemChannels && (size.p[0] == 1 || size.p[1] == 1) && (isContinuous() || step.p[1] == step.p[2]*size.p[2]))) ? (int)(total()*channels()/_elemChannels) : -1;}
该函数源码大概意思就是说对应的Mat矩阵如果其深度,连续性,通道数,行列式满足一定条件的话就返回Mat元素的个数和其通道数的乘积,否则返回-1;而本文是要求其返回值大于3,有得知此处输入多边形曲线(即参数1)的通道数为2,所以还需要求其元素的个数大于1.5,即大于2才满足ptnum > 3。简单的说就是用convexityDefects()函数来对多边形曲线进行凹陷检测时,必须要求参数1曲线本身至少有2个点(也不知道这样分析对不对)。因此本人在本次程序convexityDefects()函数前加入了if(Mat(approx_poly_curve).checkVector(2, CV_32S) > 3)来判断,只有满足该if条件,才会进行后面的凹陷检测。这样程序就不会再出现类似的bug了。
第2个参数一般是由opencv中的函数convexHull()获得的,一般情况下该参数里面存的是凸包集合中的点在多项式曲线点中的位置索引,且该参数以vector的形式存在,因此参数convexhull中其元素的类型为unsigned int。在本次凹陷点检测函数convexityDefects()里面根据文档,要求该参数为Mat型。因此在使用convexityDefects()的参数2时,一般将vector直接转换Mat型。
参数3是一个含有4个元素的结构体的集合,如果在c++的版本中,该参数可以直接用vector<Vec4i>来代替,Vec4i中的4个元素分别表示凹陷曲线段的起始坐标索引,终点坐标索引,离凸包集曲线最远点的坐标索引以及此时的最远距离值,这4个值都是整数。在c版本的opencv中一般不是保存的索引,而是坐标值,如下所示:
struct CvConvexityDefect{ CvPoint* start; // point of the contour where the defect begins CvPoint* end; // point of the contour where the defect ends CvPoint* depth_point; // the farthest from the convex hull point within the defect float depth; // distance between the farthest point and the convex hull};
C/c++知识点总结:
std::abs()函数中的参数不能为整型,否则编译的时候会报错参数不匹配,因此一般情况下可以传入long型,这样就不会报错了。
实验结果:
这里显示的是手势分割后的效果以及其对应的数字识别结果。
数字“0”的识别结果:
数字“1”的识别结果:
数字“2”的识别结果:
数字“3”的识别结果:
数字“4”的识别结果:
数字“5”的识别结果:
实验主要部分代码及注释:
本次实验程序实现过程和上面的系统流程图类似,大概过程如下:
1. 求出手部的掩膜
2. 求出掩膜的轮廓
3. 求出轮廓的多变形拟合曲线
4. 求出多边形拟合曲线的凸包集,找出凸点
5. 求出多变形拟合曲线的凹陷集,找出凹点
6. 利用上面的凸凹点和手部中心点的几何关系来做简单的数字手势识别
copenni类采用前面博文设计的,这里给出主函数代码部分。
main.cpp:
#include#include "opencv2/highgui/highgui.hpp"#include "opencv2/imgproc/imgproc.hpp"#include #include "copenni.cpp"#include #define DEPTH_SCALE_FACTOR 255./4096.#define ROI_HAND_WIDTH 140#define ROI_HAND_HEIGHT 140#define MEDIAN_BLUR_K 5#define XRES 640#define YRES 480#define DEPTH_SEGMENT_THRESH 5#define MAX_HANDS_COLOR 10#define MAX_HANDS_NUMBER 10#define HAND_LIKELY_AREA 2000#define DELTA_POINT_DISTENCE 25 //手部中心点1和中心点2距离的阈值#define SEGMENT_POINT1_DISTANCE 27 //凸点与手部中心点1远近距离的阈值#define SEGMENT_POINT2_DISTANCE 30 //凸点与手部中心点2远近距离的阈值using namespace cv;using namespace xn;using namespace std;int main (int argc, char **argv){ unsigned int convex_number_above_point1 = 0; unsigned int concave_number_above_point1 = 0; unsigned int convex_number_above_point2 = 0; unsigned int concave_number_above_point2 = 0; unsigned int convex_assist_above_point1 = 0; unsigned int convex_assist_above_point2 = 0; unsigned int point_y1 = 0; unsigned int point_y2 = 0; int number_result = -1; bool recognition_flag = false; //开始手部数字识别的标志 vector color_array;//采用默认的10种颜色 { color_array.push_back(Scalar(255, 0, 0)); color_array.push_back(Scalar(0, 255, 0)); color_array.push_back(Scalar(0, 0, 255)); color_array.push_back(Scalar(255, 0, 255)); color_array.push_back(Scalar(255, 255, 0)); color_array.push_back(Scalar(0, 255, 255)); color_array.push_back(Scalar(128, 255, 0)); color_array.push_back(Scalar(0, 128, 255)); color_array.push_back(Scalar(255, 0, 128)); color_array.push_back(Scalar(255, 128, 255)); } vector hand_depth(MAX_HANDS_NUMBER, 0); vector hands_roi(MAX_HANDS_NUMBER, Rect(XRES/2, YRES/2, ROI_HAND_WIDTH, ROI_HAND_HEIGHT)); namedWindow("color image", CV_WINDOW_AUTOSIZE); namedWindow("depth image", CV_WINDOW_AUTOSIZE); namedWindow("hand_segment", CV_WINDOW_AUTOSIZE); //显示分割出来的手的区域 namedWindow("handrecognition", CV_WINDOW_AUTOSIZE); //显示0~5数字识别的图像 COpenNI openni; if(!openni.Initial()) return 1; if(!openni.Start()) return 1; while(1) { if(!openni.UpdateData()) { return 1; } /*获取并显示色彩图像*/ Mat color_image_src(openni.image_metadata_.YRes(), openni.image_metadata_.XRes(), CV_8UC3, (char *)openni.image_metadata_.Data()); Mat color_image; cvtColor(color_image_src, color_image, CV_RGB2BGR); Mat hand_segment_mask(color_image.size(), CV_8UC1, Scalar::all(0)); for(auto itUser = openni.hand_points_.cbegin(); itUser != openni.hand_points_.cend(); ++itUser) { point_y1 = itUser->second.Y; point_y2 = itUser->second.Y + DELTA_POINT_DISTENCE; circle(color_image, Point(itUser->second.X, itUser->second.Y), 5, color_array.at(itUser->first % color_array.size()), 3, 8); /*设置不同手部的深度*/ hand_depth.at(itUser->first % MAX_HANDS_COLOR) = (unsigned int)(itUser->second.Z* DEPTH_SCALE_FACTOR);//itUser->first会导致程序出现bug /*设置不同手部的不同感兴趣区域*/ hands_roi.at(itUser->first % MAX_HANDS_NUMBER) = Rect(itUser->second.X - ROI_HAND_WIDTH/2, itUser->second.Y - ROI_HAND_HEIGHT/2, ROI_HAND_WIDTH, ROI_HAND_HEIGHT); hands_roi.at(itUser->first % MAX_HANDS_NUMBER).x = itUser->second.X - ROI_HAND_WIDTH/2; hands_roi.at(itUser->first % MAX_HANDS_NUMBER).y = itUser->second.Y - ROI_HAND_HEIGHT/2; hands_roi.at(itUser->first % MAX_HANDS_NUMBER).width = ROI_HAND_WIDTH; hands_roi.at(itUser->first % MAX_HANDS_NUMBER).height = ROI_HAND_HEIGHT; if(hands_roi.at(itUser->first % MAX_HANDS_NUMBER).x <= 0) hands_roi.at(itUser->first % MAX_HANDS_NUMBER).x = 0; if(hands_roi.at(itUser->first % MAX_HANDS_NUMBER).x > XRES) hands_roi.at(itUser->first % MAX_HANDS_NUMBER).x = XRES; if(hands_roi.at(itUser->first % MAX_HANDS_NUMBER).y <= 0) hands_roi.at(itUser->first % MAX_HANDS_NUMBER).y = 0; if(hands_roi.at(itUser->first % MAX_HANDS_NUMBER).y > YRES) hands_roi.at(itUser->first % MAX_HANDS_NUMBER).y = YRES; } imshow("color image", color_image); /*获取并显示深度图像*/ Mat depth_image_src(openni.depth_metadata_.YRes(), openni.depth_metadata_.XRes(), CV_16UC1, (char *)openni.depth_metadata_.Data());//因为kinect获取到的深度图像实际上是无符号的16位数据 Mat depth_image; depth_image_src.convertTo(depth_image, CV_8U, DEPTH_SCALE_FACTOR); imshow("depth image", depth_image); //取出手的mask部分 //不管原图像时多少通道的,mask矩阵声明为单通道就ok for(auto itUser = openni.hand_points_.cbegin(); itUser != openni.hand_points_.cend(); ++itUser) { for(int i = hands_roi.at(itUser->first % MAX_HANDS_NUMBER).x; i < std::min(hands_roi.at(itUser->first % MAX_HANDS_NUMBER).x+hands_roi.at(itUser->first % MAX_HANDS_NUMBER).width, XRES); i++) for(int j = hands_roi.at(itUser->first % MAX_HANDS_NUMBER).y; j < std::min(hands_roi.at(itUser->first % MAX_HANDS_NUMBER).y+hands_roi.at(itUser->first % MAX_HANDS_NUMBER).height, YRES); j++) { hand_segment_mask.at (j, i) = ((hand_depth.at(itUser->first % MAX_HANDS_NUMBER)-DEPTH_SEGMENT_THRESH) < depth_image.at (j, i)) & ((hand_depth.at(itUser->first % MAX_HANDS_NUMBER)+DEPTH_SEGMENT_THRESH) > depth_image.at (j,i)); } } medianBlur(hand_segment_mask, hand_segment_mask, MEDIAN_BLUR_K); Mat hand_segment(color_image.size(), CV_8UC3); color_image.copyTo(hand_segment, hand_segment_mask); /*对mask图像进行轮廓提取,并在手势识别图像中画出来*/ std::vector< std::vector > contours; findContours(hand_segment_mask, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);//找出mask图像的轮廓 Mat hand_recognition_image = Mat::zeros(color_image.rows, color_image.cols, CV_8UC3); for(int i = 0; i < contours.size(); i++) { //只有在检测到轮廓时才会去求它的多边形,凸包集,凹陷集 recognition_flag = true; /*找出轮廓图像多边形拟合曲线*/ Mat contour_mat = Mat(contours[i]); if(contourArea(contour_mat) > HAND_LIKELY_AREA) { //比较有可能像手的区域 std::vector approx_poly_curve; approxPolyDP(contour_mat, approx_poly_curve, 10, true);//找出轮廓的多边形拟合曲线 std::vector< std::vector > approx_poly_curve_debug; approx_poly_curve_debug.push_back(approx_poly_curve); drawContours(hand_recognition_image, contours, i, Scalar(255, 0, 0), 1, 8); //画出轮廓 // drawContours(hand_recognition_image, approx_poly_curve_debug, 0, Scalar(256, 128, 128), 1, 8); //画出多边形拟合曲线 /*对求出的多边形拟合曲线求出其凸包集*/ vector hull; convexHull(Mat(approx_poly_curve), hull, true); for(int i = 0; i < hull.size(); i++) { circle(hand_recognition_image, approx_poly_curve[hull[i]], 2, Scalar(0, 255, 0), 2, 8); /*统计在中心点1以上凸点的个数*/ if(approx_poly_curve[hull[i]].y <= point_y1) { /*统计凸点与中心点1的y轴距离*/ long dis_point1 = abs(long(point_y1 - approx_poly_curve[hull[i]].y)); int dis1 = point_y1 - approx_poly_curve[hull[i]].y; if(dis_point1 > SEGMENT_POINT1_DISTANCE && dis1 >= 0) { convex_assist_above_point1++; } convex_number_above_point1++; } /*统计在中心点2以上凸点的个数*/ if(approx_poly_curve[hull[i]].y <= point_y2) { /*统计凸点与中心点1的y轴距离*/ long dis_point2 = abs(long(point_y2 - approx_poly_curve[hull[i]].y)); int dis2 = point_y2 - approx_poly_curve[hull[i]].y; if(dis_point2 > SEGMENT_POINT2_DISTANCE && dis2 >= 0) { convex_assist_above_point2++; } convex_number_above_point2++; } } // /*对求出的多边形拟合曲线求出凹陷集*/ std::vector convexity_defects; if(Mat(approx_poly_curve).checkVector(2, CV_32S) > 3) convexityDefects(approx_poly_curve, Mat(hull), convexity_defects); for(int i = 0; i < convexity_defects.size(); i++) { circle(hand_recognition_image, approx_poly_curve[convexity_defects[i][2]] , 2, Scalar(0, 0, 255), 2, 8); /*统计在中心点1以上凹陷点的个数*/ if(approx_poly_curve[convexity_defects[i][2]].y <= point_y1) concave_number_above_point1++; /*统计在中心点2以上凹陷点的个数*/ if(approx_poly_curve[convexity_defects[i][2]].y <= point_y2) concave_number_above_point2++; } } } /**画出手势的中心点**/ for(auto itUser = openni.hand_points_.cbegin(); itUser != openni.hand_points_.cend(); ++itUser) { circle(hand_recognition_image, Point(itUser->second.X, itUser->second.Y), 3, Scalar(0, 255, 255), 3, 8); circle(hand_recognition_image, Point(itUser->second.X, itUser->second.Y + 25), 3, Scalar(255, 0, 255), 3, 8); } /*手势数字0~5的识别*/ //"0"的识别 if((convex_assist_above_point1 ==0 && convex_number_above_point2 >= 2 && convex_number_above_point2 <= 3 && concave_number_above_point2 <= 1 && concave_number_above_point1 <= 1) || (concave_number_above_point1 ==0 || concave_number_above_point2 == 0) && recognition_flag == true) number_result = 0; //"1"的识别 if(convex_assist_above_point1 ==1 && convex_number_above_point1 >=1 && convex_number_above_point1 <=2 && convex_number_above_point2 >=2 && convex_assist_above_point2 == 1) number_result = 1; //"2"的识别 if(convex_number_above_point1 == 2 && concave_number_above_point1 == 1 && convex_assist_above_point2 == 2 /*convex_assist_above_point1 <=1*/ && concave_number_above_point2 == 1) number_result = 2; //"3"的识别 if(convex_number_above_point1 == 3 && concave_number_above_point1 <= 3 && concave_number_above_point1 >=1 && convex_number_above_point2 >= 3 && convex_number_above_point2 <= 4 && convex_assist_above_point2 == 3) number_result = 3; //"4"的识别 if(convex_number_above_point1 == 4 && concave_number_above_point1 <=3 && concave_number_above_point1 >=2 && convex_number_above_point2 == 4) number_result = 4; //"5"的识别 if(convex_number_above_point1 >=4 && convex_number_above_point2 == 5 && concave_number_above_point2 >= 3 && convex_number_above_point2 >= 4) number_result = 5; if(number_result !=0 && number_result != 1 && number_result != 2 && number_result != 3 && number_result != 4 && number_result != 5) number_result == -1; /*在手势识别图上显示匹配的数字*/ std::stringstream number_str; number_str << number_result; putText(hand_recognition_image, "Match: ", Point(0, 60), 4, 1, Scalar(0, 255, 0), 2, 0 ); if(number_result == -1) putText(hand_recognition_image, " ", Point(120, 60), 4, 2, Scalar(255, 0 ,0), 2, 0); else putText(hand_recognition_image, number_str.str(), Point(150, 60), 4, 2, Scalar(255, 0 ,0), 2, 0); imshow("handrecognition", hand_recognition_image); imshow("hand_segment", hand_segment); /*一个循环中对有些变量进行初始化操作*/ convex_number_above_point1 = 0; convex_number_above_point2 = 0; concave_number_above_point1 = 0; concave_number_above_point2 = 0; convex_assist_above_point1 = 0; convex_assist_above_point2 = 0; number_result = -1; recognition_flag = false; number_str.clear(); waitKey(20); }}
copenni.h:
#ifndef COPENNI_H#define COPENNI_H#include#include #include #include #include #include
copenni.cpp:
#include "copenni.h"#include#include #include
实验总结:
由本次实验的操作过程可以看出,识别效果抗干扰能力比较差差。因此后续的工作是建立一个手势识别的数据库,寻找一个好的手部特征向量,和一个好的分类器。这有可能将是本人研究生毕业论文的研究方向,加油!
参考文献: