最近在做毕业设计,其中一个部分要实现对视频序列中人手位置的跟踪。因此先写了人手的检测程序,下一步基于检测程序再用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); 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; }
目前还存在诸多不如意之处,还待继续改进。若有表达不清或者有错误之处,烦请看到此文的朋友提出或指正~