在前一篇文章中,我们完成了对手指肌肤颜色的检测。检测到肌肤颜色之后,接下来就是要确定手掌的范围。
在这个过程中,将要使用到的核心函数是Opencv中的convexityDefects。
如上图所示,黑色的轮廓线为convexity hull, 而convexity hull与手掌之间的部分为convexity defects. 每个convexity defect区域有四个特征量:起始点(startPoint),结束点(endPoint),距离convexity hull最远点(farPoint),最远点到convexity hull的距离(depth)。
void convexityDefects(InputArray contour, InputArray convexhull, OutputArrayconvexityDefects)
参数:
coutour: 输入参数,检测到的轮廓,可以调用findContours函数得到;
convexhull: 输入参数,检测到的凸包,可以调用convexHull函数得到。注意,convexHull函数可以得到vector<vector<Point>>和vector<vector<int>>两种类型结果,这里的convexhull应该为vector<vector<int>>类型,否则通不过ASSERT检查;
convexityDefects:输出参数,检测到的最终结果,应为vector<vector<Vec4i>>类型,Vec4i存储了起始点(startPoint),结束点(endPoint),距离convexity hull最远点(farPoint)以及最远点到convexity hull的距离(depth)
在这张例图中,蓝色的是convexity defects的起始点和结束点,红色的是最远点。通过观察可以发现,我们需要定位的是蓝色的点。
所以在编写代码时,需要进行以下几步的处理:
(1)进行高斯滤波,对图像的Mat进行处理,方便后面的提取轮廓。
//高斯滤波,将rgbaMat用高斯滤波处理后,重新输出到rgbaMat中去 Imgproc.GaussianBlur (rgbaMat, rgbaMat, new OpenCVForUnity.Size (5, 5), 1, 1);
(2)提取图像中选定颜色的轮廓,将其存在一个点阵中。这里就用到了我们在上一篇文章中提到过的HSV处理。对其进行了膨胀和轮廓提取。
List<MatOfPoint> contours = detector.GetContours (); detector.Process (rgbaMat);
(3)寻找最小的包围盒,提取屏幕中占比最大和次大的rect。之所以要寻找两个rect,是因为想要实现双手的识别。但是在实际操作的时候发现,双手的识别的情况容易因为手部的移动显得很不稳定(两只手占比差不多的情况下,最大和次大经常会跳变。)所以暂时应用的是单手。
//寻找最小包围矩形 RotatedRect rect = Imgproc.minAreaRect (new MatOfPoint2f (contours [0].toArray ())); double boundWidth = rect.size.width; double boundHeight = rect.size.height; int boundPos1 = 0; // int boundPos2 = 0; //取最大的rect和次大的rect(双手) for (int i = 0; i < contours.Count; i++) { rect = Imgproc.minAreaRect (new MatOfPoint2f (contours [i].toArray ())); if (rect.size.width * rect.size.height > boundWidth * boundHeight) { boundWidth = rect.size.width; boundHeight = rect.size.height; boundPos1 = i; } } // for (int i = 0; i < contours.Count; i++) { // if (i != boundPos1) { // rect = Imgproc.minAreaRect (new MatOfPoint2f (contours [i].toArray ())); // if (rect.size.width * rect.size.height > boundWidth * boundHeight) { // boundWidth = rect.size.width; // boundHeight = rect.size.height; // boundPos2 = i; // } // } // }
(4)获得凸包,并且利用凸包和之前得到的轮廓,计算起始点和结束点的位置。
MatOfInt hull1 = new MatOfInt (); // MatOfInt hull2 = new MatOfInt (); MatOfInt4 convexDefect1 = new MatOfInt4 (); // MatOfInt4 convexDefect2 = new MatOfInt4 (); Imgproc.convexHull (new MatOfPoint (contour1.toArray ()), hull1); // Imgproc.convexHull (new MatOfPoint (contour2.toArray ()), hull2); // if (hull2.toArray ().Length < 3) // return; if (hull1.toArray ().Length < 3) return; Imgproc.convexityDefects (new MatOfPoint (contour1.toArray ()), hull1, convexDefect1); // Imgproc.convexityDefects (new MatOfPoint (contour2.toArray ()), hull2, convexDefect2);
List<MatOfPoint> hullPoints1 = new List<MatOfPoint> (); List<Point> listPo1 = new List<Point> (); for (int j = 0; j < hull1.toList().Count; j++) { listPo1.Add (contour1.toList () [hull1.toList () [j]]); } MatOfPoint e1 = new MatOfPoint (); e1.fromList (listPo1); hullPoints1.Add (e1); // // List<MatOfPoint> hullPoints2 = new List<MatOfPoint> (); // List<Point> listPo2 = new List<Point> (); // for (int j = 0; j < hull2.toList().Count; j++) { // listPo2.Add (contour2.toList () [hull2.toList () [j]]); // } // MatOfPoint e2 = new MatOfPoint (); // e2.fromList (listPo2); // hullPoints2.Add (e2); //List<Point> listPoDefect = new List<Point> (); listPoDefect1 = new List<Point>(); int count1 = 0; if (convexDefect1.rows () > 0) { List<int> convexDefectList1 = convexDefect1.toList (); List<Point> contourList1 = contour1.toList (); for (int j = 0; j < convexDefectList1.Count; j = j + 4) { //最远点 Point farPoint = contourList1 [convexDefectList1 [j + 2]]; //最远点到convexity hull的距离(depth) int depth = convexDefectList1 [j + 3]; if (depth > threasholdSlider.value&&farPoint.y<a1) { listPoDefect1.Add (contourList1 [convexDefectList1 [j]]); count1++; } // Debug.Log ("convexDefectList [" + j + "] " + convexDefectList [j + 3]); } }
所有的点的信息就被存储在名为listPoDefect1的点的list中。在VR项目中实际应用的时候,只需要利用cs代码获取对应脚本中的参数,并且进行赋值就可以了。