opencv学习笔记(二):基于肤色的人手检测(跟踪)程序

最近在做毕业设计,其中一个部分要实现对视频序列中人手位置的跟踪。因此先写了人手的检测程序,下一步基于检测程序再用camshift算法做人手的跟踪。

目前完成的程序在我的笔记本上运行大概是一帧80-100ms,直接用检测算法来做跟踪算法其实也马马虎虎可以用了。

 

开发环境如下:

系统:Windows 10

IDEVisual Studio 2013

语言:C++

算法库:OpenCV

 

程序思路如下

 

1)获取视频帧

2)将视频帧转换到YCrCb颜色空间,并分割通道

3)基于CrCb两个通道做肤色区域的分割,得到肤色区域二值图像

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

 

 

效果图如下:


opencv学习笔记(二):基于肤色的人手检测(跟踪)程序_第1张图片


opencv学习笔记(二):基于肤色的人手检测(跟踪)程序_第2张图片


opencv学习笔记(二):基于肤色的人手检测(跟踪)程序_第3张图片


#include "stdafx.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


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(i);
		for (int j = 1; j < cols - 1; j++)
		{
			if (data[j] == 1)
			{
				std::stack> neighborPixels;
				neighborPixels.push(std::pair(j, i));     // 像素位置:   
				++label;  // 没有重复的团,开始新的标签 
				ymin[label] = i;
				ymax[label] = i;
				xmin[label] = j;
				xmax[label] = j;
				while (!neighborPixels.empty())
				{
					std::pair curPixel = neighborPixels.top(); //如果与上一行中一个团有重合区域,则将上一行的那个团的标号赋给它  
					int curX = curPixel.first;
					int curY = curPixel.second;
					labelImg.at(curY,curX) = label;

					neighborPixels.pop();

					if ((curX>0)&&(curY>0)&&(curX<(cols-1))&&(curY<(rows-1)))
					{ 
						if (labelImg.at(curY - 1,curX) == 1)						//上
						{
							neighborPixels.push(std::pair(curX, curY - 1));
							//ymin[label] = curY - 1;
						}
						if (labelImg.at( curY + 1,curX) == 1)						//下
						{
							neighborPixels.push(std::pair(curX, curY + 1));
							if ((curY+1)>ymax[label])
								ymax[label] = curY + 1;
						}
						if (labelImg.at(curY,curX - 1) == 1)						//左
						{
							neighborPixels.push(std::pair(curX - 1, curY));
							if ((curX - 1)(curY,curX + 1) == 1)						//右
						{
							neighborPixels.push(std::pair(curX + 1, curY));
							if ((curX + 1)>xmax[label])
								xmax[label] = curX + 1;
						}
						if (labelImg.at(curY - 1,curX-1) == 1)					//左上
						{  
							neighborPixels.push(std::pair(curX - 1, curY - 1));
							//ymin[label] = curY - 1;
							if ((curX - 1)(curY + 1,curX+1) == 1)					//右下
						{  
							neighborPixels.push(std::pair(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( curY + 1,curX - 1) == 1)					//左下
						{ 
							neighborPixels.push(std::pair(curX - 1, curY+1));
							if ((curY + 1)>ymax[label])
								ymax[label] = curY + 1;
							if ((curX - 1)( curY - 1,curX + 1) == 1)					//右上
						{
							neighborPixels.push(std::pair(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 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(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;
}


其中的模板图片为

opencv学习笔记(二):基于肤色的人手检测(跟踪)程序_第4张图片

目前还存在诸多不如意之处,还待继续改进。若有表达不清或者有错误之处,烦请看到此文的朋友提出或指正~


你可能感兴趣的:(opencv,计算机视觉)