先写了人手的检测程序,下一步基于检测程序再用camshift算法做人手的跟踪。
目前完成的程序在我的笔记本上运行大概是一帧80-100ms,直接用检测算法来做跟踪算法其实也马马虎虎可以用了。
开发环境如下:
系统:Windows 10
IDE:Visual Studio 2013
语言:C++
算法库:OpenCV
程序思路如下
1)获取视频帧
2)将视频帧转换到YCrCb颜色空间,并分割通道
3)基于Cr和Cb两个通道做肤色区域的分割,得到肤色区域二值图像
4)将二值图像分别做膨胀和腐蚀处理,得到前景和背景的标记(marker)图像,应用分水岭算法,得到大块肤色区域的边缘轮廓
5)对4)中得到的边缘轮廓用8向种子算法处理,对不同的肤色区域做了标记,并返回了不同肤色区域的边界范围,这些肤色区域作为人手区域的候选区域
6)将5)中得到的候选区域与准备好的人手模板(Cr通道)进行模板匹配,匹配前先将候选区域缩放到与模板相同的大小;使用的方法是平方差匹配法,得到每个候选区域的匹配值(越小越接近)
7)对5)中得到的候选区域中肤色像素的比例进行统计
8)根据6)与7)中得到的结果对候选区域进行筛选,认为匹配值 <0.02(0为最匹配,1为最不匹配),且肤色区域比例<0.65的区域为人手区域(因为人脸区域一般肤色占比比较高)
9)在输出帧中对确定的人手区域画长方形框做标记
讨论:
1)之所以使用肤色区域比例的筛选方法,是因为基于肤色的情况下,人脸和人手非常容易混淆,经过尝试发现增进模板的数量效果并不好,因此采用了这种方法,但这种方法带来的问题就是人手必须张开(从而降低肤色在候选框中的比例)才能稳定被找到。
2)其实不做模板匹配只统计肤色比例应该也是可以的,但是稳定性会比较差。
3)分水岭算法的介绍见以下链接:
http://www.xuebuyuan.com/1014698.html
效果图如下:
- #include "stdafx.h"
- #include <iostream>
- #include <vector>
- #include <string>
- #include <list>
- #include <map>
- #include <stack>
- #include <opencv2/core/core.hpp>
- #include <opencv2/features2d/features2d.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include <opencv2/imgproc/imgproc.hpp>
- #include <opencv2/calib3d/calib3d.hpp>
- using namespace std;
- using namespace cv;
- //8邻接种子算法,并返回每块区域的边缘框
- void Seed_Filling(const cv::Mat& binImg, cv::Mat& labelImg, int& labelNum, int (&ymin)[20], int(&ymax)[20], int(&xmin)[20], int(&xmax)[20]) //种子填充法
- {
- if (binImg.empty() ||
- binImg.type() != CV_8UC1)
- {
- return;
- }
- labelImg.release();
- binImg.convertTo(labelImg, CV_32SC1);
- int label = 1;
- int rows = binImg.rows - 1;
- int cols = binImg.cols - 1;
- for (int i = 1; i < rows - 1; i++)
- {
- int* data = labelImg.ptr<int>(i);
- for (int j = 1; j < cols - 1; j++)
- {
- if (data[j] == 1)
- {
- std::stack<std::pair<int, int>> neighborPixels;
- neighborPixels.push(std::pair<int, int>(j, i)); // 像素位置: <j,i>
- ++label; // 没有重复的团,开始新的标签
- ymin[label] = i;
- ymax[label] = i;
- xmin[label] = j;
- xmax[label] = j;
- while (!neighborPixels.empty())
- {
- std::pair<int, int> curPixel = neighborPixels.top(); //如果与上一行中一个团有重合区域,则将上一行的那个团的标号赋给它
- int curX = curPixel.first;
- int curY = curPixel.second;
- labelImg.at<int>(curY,curX) = label;
- neighborPixels.pop();
- if ((curX>0)&&(curY>0)&&(curX<(cols-1))&&(curY<(rows-1)))
- {
- if (labelImg.at<int>(curY - 1,curX) == 1) //上
- {
- neighborPixels.push(std::pair<int, int>(curX, curY - 1));
- //ymin[label] = curY - 1;
- }
- if (labelImg.at<int>( curY + 1,curX) == 1) //下
- {
- neighborPixels.push(std::pair<int, int>(curX, curY + 1));
- if ((curY+1)>ymax[label])
- ymax[label] = curY + 1;
- }
- if (labelImg.at<int>(curY,curX - 1) == 1) //左
- {
- neighborPixels.push(std::pair<int, int>(curX - 1, curY));
- if ((curX - 1)<xmin[label])
- xmin[label] = curX - 1;
- }
- if (labelImg.at<int>(curY,curX + 1) == 1) //右
- {
- neighborPixels.push(std::pair<int, int>(curX + 1, curY));
- if ((curX + 1)>xmax[label])
- xmax[label] = curX + 1;
- }
- if (labelImg.at<int>(curY - 1,curX-1) == 1) //左上
- {
- neighborPixels.push(std::pair<int, int>(curX - 1, curY - 1));
- //ymin[label] = curY - 1;
- if ((curX - 1)<xmin[label])
- xmin[label] = curX - 1;
- }
- if (labelImg.at<int>(curY + 1,curX+1) == 1) //右下
- {
- neighborPixels.push(std::pair<int, int>(curX+1, curY + 1));
- if ((curY + 1)>ymax[label])
- ymax[label] = curY + 1;
- if ((curX + 1)>xmax[label])
- xmax[label] = curX + 1;
- }
- if (labelImg.at<int>( curY + 1,curX - 1) == 1) //左下
- {
- neighborPixels.push(std::pair<int, int>(curX - 1, curY+1));
- if ((curY + 1)>ymax[label])
- ymax[label] = curY + 1;
- if ((curX - 1)<xmin[label])
- xmin[label] = curX - 1;
- }
- if (labelImg.at<int>( curY - 1,curX + 1) == 1) //右上
- {
- neighborPixels.push(std::pair<int, int>(curX + 1, curY-1));
- //ymin[label] = curY - 1;
- if ((curX + 1)>xmax[label])
- xmax[label] = curX + 1;
- }
- }
- }
- }
- }
- }
- labelNum = label-1;
- }
- class WatershedSegmenter {
- private:
- cv::Mat markers;
- public:
- void setMarkers(const cv::Mat& markerImage) {
- // Convert to image of ints
- markerImage.convertTo(markers, CV_32S);
- }
- cv::Mat process(const cv::Mat &image) {
- // Apply watershed
- cv::watershed(image, markers);
- return markers;
- }
- // Return result in the form of an image
- cv::Mat getSegmentation() {
- cv::Mat tmp;
- // all segment with label higher than 255
- // will be assigned value 255
- markers.convertTo(tmp, CV_8U);
- return tmp;
- }
- // Return watershed in the form of an image
- cv::Mat getWatersheds() {
- cv::Mat tmp;
- markers.convertTo(tmp, CV_8U,255, 255);
- return tmp;
- }
- };
- int main()
- {
- //设置视频读入,括号里面的数字是摄像头的选择,一般自带的是0
- cv::VideoCapture cap(0);
- if (!cap.isOpened())
- {
- return -1;
- }
- Mat frame;
- Mat binImage,tmp;
- Mat Y, Cr, Cb;
- vector<Mat> channels;
- //模板图片,是Cr颜色通道的人手图像截图
- Mat tmpl = imread("bwz.jpg",CV_8UC1);
- bool stop = false;
- while (!stop)
- {
- //读入视频帧,转换颜色空间,并分割通道
- cap >> frame;
- cvtColor(frame, binImage, CV_BGR2GRAY);
- frame.copyTo(tmp);
- cvtColor(tmp, tmp, CV_BGR2YCrCb);
- split(tmp, channels);
- Cr = channels.at(1);
- Cb = channels.at(2);
- //肤色检测,输出二值图像
- for (int j = 1; j < Cr.rows - 1; j++)
- {
- uchar* currentCr = Cr.ptr< uchar>(j);
- uchar* currentCb = Cb.ptr< uchar>(j);
- uchar* current = binImage.ptr< uchar>(j);
- for (int i = 1; i < Cb.cols - 1; i++)
- {
- if ((currentCr[i] > 140) && (currentCr[i] < 170) &&(currentCb[i] > 77) && (currentCb[i] < 123))
- current[i] = 255;
- else
- current[i] = 0;
- }
- }
- //形态学处理
- //dilate(binImage, binImage, Mat());
- dilate(binImage, binImage, Mat());
- //分水岭算法
- cv::Mat fg;
- cv::erode(binImage, fg, cv::Mat(), cv::Point(-1, -1), 6);
- // Identify image pixels without objects
- cv::Mat bg;
- cv::dilate(binImage, bg, cv::Mat(), cv::Point(-1, -1), 6);
- cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
- // Show markers image
- cv::Mat markers(binImage.size(), CV_8U, cv::Scalar(0));
- markers = fg + bg;
- // Create watershed segmentation object
- WatershedSegmenter segmenter;
- segmenter.setMarkers(markers);
- segmenter.process(frame);
- Mat waterShed;
- waterShed = segmenter.getWatersheds();
- //imshow("watershed", waterShed);
- //获得区域边框
- threshold(waterShed, waterShed, 1, 1, THRESH_BINARY_INV);
- //8向种子算法,给边框做标记
- Mat labelImg;
- int label, ymin[20], ymax[20], xmin[20], xmax[20];
- Seed_Filling(waterShed, labelImg, label, ymin, ymax, xmin, xmax);
- //根据标记,对每块候选区就行缩放,并与模板比较
- Size dsize = Size(tmpl.cols, tmpl.rows);
- float simi[20];
- for (int i = 0; i < label; i++)
- {
- simi[i] = 1;
- if (((xmax[2 + i] - xmin[2 + i])>50) && ((ymax[2 + i] - ymin[2 + i]) > 50))
- {
- //rectangle(frame, Point(xmin[2 + i], ymin[2 + i]), Point(xmax[2 + i], ymax[2 + i]), Scalar::all(255), 2, 8, 0);
- Mat rROI = Mat(dsize, CV_8UC1);
- if (rROI.cols==0)
continue; - resize(Cr(Rect(xmin[2 + i], ymin[2 + i], xmax[2 + i] - xmin[2 + i], ymax[2 + i] - ymin[2 + i])), rROI, dsize);
- Mat result;
- matchTemplate(rROI, tmpl, result, CV_TM_SQDIFF_NORMED);
- simi[i] = result.ptr<float>(0)[0];
- //cout << simi[i] << endl;
- }
- }
- //统计一下区域中的肤色区域比例
- float fuseratio[20];
- for (int k = 0; k < label; k++)
- {
- fuseratio[k] = 1;
- if (((xmax[2 + k] - xmin[2 + k])>50) && ((ymax[2 + k] - ymin[2 + k]) > 50))
- {
- int fusepoint=0;
- for (int j = ymin[2+k]; j < ymax[2+k]; j++)
- {
- uchar* current = binImage.ptr< uchar>(j);
- for (int i = xmin[2+k]; i < xmax[2+k]; i++)
- {
- if (current[i] == 255)
- fusepoint += 1;
- }
- }
- fuseratio[k] = float(fusepoint) / ((xmax[2 + k] - xmin[2 + k])*(ymax[2 + k] - ymin[2 + k]));
- //cout << fuseratio[k] << endl;
- }
- }
- //给符合阈值条件的位置画框
- for (int i = 0; i < label; i++)
- {
- if ((simi[i]<0.02)&&(fuseratio[i]<0.65))
- rectangle(frame, Point(xmin[2 + i], ymin[2 + i]), Point(xmax[2 + i], ymax[2 + i]), Scalar::all(255), 2, 8, 0);
- }
- imshow("frame", frame);
- //processor.writeNextFrame(frame);
- imshow("test", binImage);
- if (waitKey(1) >= 0)
- stop = true;
- }
- cout << "ss" << endl;
- //cv::waitKey();
- return 0;
- }
目前还存在诸多不如意之处,还待继续改进。若有表达不清或者有错误之处,烦请看到此文的朋友提出或指正~