本人初学OpenCV做的项目,这个项目初始是用于手影识别,即使用摄像头拍摄,双手在摄像头前做出相关手影动作,程序使用匹配算法识别出用户正在模仿的动物,并触发对应事件。其实也可以识别并匹配其他内容(由模板文件决定,最好是简单图形)。

①读取模板文件:

首先制作黑色背景白色图像的模板文件,统一格式命名后放入一文件夹内,用该函数读入文件夹目录,即可将文件夹内各个文件路径读入vector中。

void getFiles( string path, vector<string>& files) {  
	long hFile = 0;												//文件句柄
	struct _finddata_t fileinfo;										//文件信息                         
	string p;  
	if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) != -1) {  
		do {    
//----------1.1-将目录继续迭代----------
			if((fileinfo.attrib & _A_SUBDIR))						
			{  
				if(strcmp(fileinfo.name,".") != 0 && strcmp(fileinfo.name,"..") != 0)  
					getFiles( p.assign(path).append("\\").append(fileinfo.name), files );  
			}														
//----------1.2-将文件加入列表----------  
			else {  
				files.push_back(p.assign(path).append("\\").append(fileinfo.name) );  
			}  
		}   
		while(_findnext(hFile, &fileinfo) == 0);_findclose(hFile);  
	}  
}  

②模板旋转处理:

因为摄像头拍摄到的内容(如手影)并不一定是以预期角度拍摄的,可能会导致拍摄到的画面与模板图像内容一致却因角度不同而导致匹配失败,所以需将模板文件旋转不同角度后存入结构体中并与摄像头拍摄到的画面逐一匹配。

Mat revolve(Mat img, Point2f center, double ang, double scale)
{
	Mat rotatemat= getRotationMatrix2D(center,ang,scale);
	Mat imgR;
	warpAffine(img,imgR,rotatemat,img.size());
	return imgR;
}
//

③图像预处理:

对摄像头拍摄到的画面进行简单的预处理

void pretreat (Mat &img)
{
	cvtColor(img,img,CV_BGR2GRAY);								//转为灰度图
	threshold(img,img,100,255,THRESH_BINARY);						//二值化处理
	medianBlur(img,img,3);									//中值滤波
}

④图像识别轮廓并填充裁剪:

因为摄像头拍摄到的内容(如手影)并不一定是以预期距离拍摄的,可能会导致拍摄到的画面与模板图像内容一致却因比例不同而导致匹配失败,所以需将摄像头拍摄到的画面识别轮廓后进行填充或者裁剪以达到与模板图像同样的比例。(比例为轮廓中的内容占整张图片面积的比例)

bool padding (Mat &img)
{
	Cflag = false;
	AreaMax=0;
	xMin=10000;
	yMin=10000;
	xMax=0;
	yMax=0;
//----------4.1-寻找轮廓------------
	vector<vector<Point>> contours;				 			        //定义二维向量,用于存轮廓坐标
	vector<Vec4i> hierarchy;								//定义一组向量,每个元素包含4个int型
	findContours(img,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point());		
	imgFil=Mat::zeros(img.size(),CV_8UC1);							//创建与img尺寸相同的单通道灰度图,初始化为全黑
//----------4.2-筛选轮廓------------
	for(i=0;i<contours.size();i++)							        //i为轮廓序号
	{   
		Area = contourArea(contours[i], true);						//求轮廓所包含图形面积
		if ((Area > 1000)&&(Area > AreaMax))						//保留面积最大的轮廓(且轮廓面积必须大于1000)
		{
			Cflag = true;
			AreaMax = Area;
			k = i;
		} 
	}  
	if (!Cflag)									        //未找到符合要求的轮廓
	{
		return Cflag;
	}
	else
	{
//----------4.3-填充轮廓------------
		xMax = 0;
		yMax = 0;
		xMin = 10000;
		yMin = 10000;
		drawContours(imgFil,contours,k,Scalar(255),-1,8,hierarchy,CV_RETR_LIST);	//绘制轮廓
		for(int j = 0;j < contours[k].size();j++)   
		{  
			x = contours[k][j].x;							//轮廓点横坐标
			y = contours[k][j].y;							//轮廓点纵坐标
			if (x > xMax)
			{
				xMax = x;							//右下角横坐标
			}
			if (y > yMax)
			{
				yMax = y;							//右下角纵坐标
			}	
			if (x < xMin)
			{
				xMin = x;							//左下角横坐标
			}
			if (y < yMin)
			{
				yMin = y;							//左下角纵坐标
			}
		}
//----------4.4-裁剪图像------------
		if ((xMax-xMin) > (yMax-yMin))						        //求最大图边长
		{
			LenPicS = xMax-xMin;
		} 
		else
		{
			LenPicS = yMax-yMin;
		}
		xCen = (xMin + xMax) / 2;							//计算图像中点横坐标
		yCen = (yMin + yMax) / 2;							//计算图像中点纵坐标
		xMin = xCen- LenPicS/2;								//更改左上角横坐标
		yMin = yCen- LenPicS/2;								//更改左上角纵坐标
		Size szTmp1(imgFil.rows+2*LenPicS,imgFil.rows+2*LenPicS);
		imgTmp = Mat::zeros(szTmp1,CV_8UC1);	
		imgTmpRoi = imgTmp(Rect(LenPicS,LenPicS,imgFil.rows,imgFil.rows));			
		imgFil.copyTo(imgTmpRoi);						//扩大图片(防止溢出)
		imgMax = imgTmp(Rect(xMin+LenPicS,yMin+LenPicS,LenPicS,LenPicS));	//截取填充部分(最大图)
//----------4.5-比例套边------------
		LenPicD = sqrt(AreaMax / prop);						//根据面积计算比例图边长
		if (LenPicS < LenPicD)
		{
			Size szTmp2(LenPicD,LenPicD);
			imgTmp = Mat::zeros(szTmp2,CV_8UC1);
			imgTmpRoi = imgTmp(Rect((LenPicD-LenPicS)/2,(LenPicD-LenPicS)/2,LenPicS,LenPicS));  
			imgMax.copyTo(imgTmpRoi);					//为填充区域套上边框
			img = imgTmp; 
		}
		else
		{
			img = imgMax;
		}
	}
	return Cflag;
}

⑤图像匹配算法:

使用感知哈希算法进行图像匹配。

int mapping (Mat cmpSrc, Mat cmptem)
{
	iavg1 = 0;
	iavg2 = 0;
	for (int i = 0; i < nL; i++)									//遍历边
	{
		uchar* data1 = cmpSrc.ptr<uchar>(i);						//以指针访问像素
		uchar* data2 = cmptem.ptr<uchar>(i);

		tmp = i * nL;
		for (int j = 0; j < nL; j++) 
		{
			int tmp1 = tmp + j;
			arr1[tmp1] = data1[j] / 4 * 4;
			arr2[tmp1] = data2[j] / 4 * 4;
			iavg1 += arr1[tmp1];
			iavg2 += arr2[tmp1];
		}
	}
	iavg1 /= nS;													//求平均值
	iavg2 /= nS;
	for (int i = 0; i < nS; i++)									//遍历图
	{
		arr1[i] = (arr1[i] >= iavg1) ? 1 : 0;						//大于平均值置1,否则置0
		arr2[i] = (arr2[i] >= iavg2) ? 1 : 0;
	}
	diff = 0;
	for (int i=0;i<nS;i++)
	{
		if(arr1[i] != arr2[i])
		{
			++diff;													//不一样的像素个数
		}
	}
	return diff;
}

整个程序代码如下:

//---------------------------------------------------0.声明定义赋值--------------------------------------------------------
//----------0.1-头文件------------
#include <iostream>
#include <string>
#include <math.h>
#include <vector>
#include <io.h>
#include <fstream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>

//----------0.2-命名空间----------
using namespace std;
using namespace cv;

//----------0.3-常量定义----------
const double prop = 0.25;											//填充所占图像比例【函数padding中使用】
const double angle = 30;											//旋转角度范围(-angle° ~ angle°)【main函数中使用】
const int nL = 30;													//匹配图像边长度【全局使用】
const int nS = nL * nL;												//匹配图像像素数【全局使用】
const string folder = "手影动作模板";									//放置模板图片的文件夹【main函数中使用】

//----------0.4-变量定义----------
bool Cflag;															//填充成功标志【全局使用】

int i, j, k, n, tmp;												//循环、临时变量【全局使用】
int diff, diffMin, num;												//不同的像素个数、最少的不同像素个数、最少像素个数对应的模板编号【全局使用】
int LenPicS, LenPicD;												//最大图边长、比例图边长【函数padding中使用】
int arr1[nS], arr2[nS], iavg1, iavg2;								//原图像素值、模板像素值、原图平均像素值、模板平均像素值【函数mapping中使用】

double Area, AreaMax;												//轮廓面积、最大轮廓面积【函数padding中使用】
double x, y, xMin, yMin;											//轮廓点横坐标、轮廓点纵坐标、左上角横坐标、左上角纵坐标【函数padding中使用】
double xMax, yMax, xCen, yCen;										//右下角横坐标、右下角纵坐标、中心点横坐标、中心点纵坐标	【函数padding中使用】
double ang;															//旋转角度【main函数中使用】
double samePer;														//相同像素个数在总像素中所占的百分比【main函数中使用】

string tplName;														//模板图片名称【main函数中使用】

Mat imgTem, imgTemR,frame, imgSrc;									//模板图片、旋转后的模板图片、视频流、视频帧【main函数中使用】											
Mat imgTmp, imgTmpRoi, imgFil, imgMax;								//填充图、最大图、临时MAT、临时ROI【函数padding中使用】
Mat	rotatemat, imgR;												//旋转矩阵、旋转图【函数revolve中使用】
Mat cmptem;															//对比模板【函数mapping中使用】

//----------0.5-结构体定义--------
struct templatePic													//模板图片结构体

{
	string templateName;											//模板名称
	string templatePath;											//模板路径
	Mat templateImg;												//模板图像
};

//----------0.6-函数定义----------
void getFiles( string path, vector<string>& files);					//查找图片路径
Mat revolve(Mat img, Point2f center, double ang, double scale);		//模板旋转处理
void pretreat (Mat &img);											//图像预先处理
bool padding (Mat &img);											//图像填充裁剪
int mapping (Mat cmpSrc, Mat cmptem);								//图像匹配算法
//


//---------------------------------------------------1.查找图片路径--------------------------------------------------------
void getFiles( string path, vector<string>& files) {  
	long hFile = 0;														//文件句柄
	struct _finddata_t fileinfo;										//文件信息                         
	string p;  
	if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) != -1) {  
		do {    
//----------1.1-将目录继续迭代----------
			if((fileinfo.attrib & _A_SUBDIR))						
			{  
				if(strcmp(fileinfo.name,".") != 0 && strcmp(fileinfo.name,"..") != 0)  
					getFiles( p.assign(path).append("\\").append(fileinfo.name), files );  
			}														
//----------1.2-将文件加入列表----------  
			else {  
				files.push_back(p.assign(path).append("\\").append(fileinfo.name) );  
			}  
		}   
		while(_findnext(hFile, &fileinfo) == 0);_findclose(hFile);  
	}  
}  
//


//---------------------------------------------------2.模板旋转处理--------------------------------------------------------
Mat revolve(Mat img, Point2f center, double ang, double scale)
{
	Mat rotatemat= getRotationMatrix2D(center,ang,scale);
	Mat imgR;
	warpAffine(img,imgR,rotatemat,img.size());
	return imgR;
}
//


//---------------------------------------------------3.图像预先处理--------------------------------------------------------
void pretreat (Mat &img)
{
	cvtColor(img,img,CV_BGR2GRAY);									//转为灰度图
	threshold(img,img,100,255,THRESH_BINARY);						//二值化处理
	medianBlur(img,img,3);											//中值滤波
}
//


//---------------------------------------------------4.图像填充裁剪--------------------------------------------------------
bool padding (Mat &img)
{
	Cflag = false;
	AreaMax=0;
	xMin=10000;
	yMin=10000;
	xMax=0;
	yMax=0;
//----------4.1-寻找轮廓------------
	vector<vector<Point>> contours;				 					//定义二维向量,用于存轮廓坐标
	vector<Vec4i> hierarchy;										//定义一组向量,每个元素包含4个int型
	findContours(img,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point());		
	imgFil=Mat::zeros(img.size(),CV_8UC1);							//创建与img尺寸相同的单通道灰度图,初始化为全黑
//----------4.2-筛选轮廓------------
	for(i=0;i<contours.size();i++)									//i为轮廓序号
	{   
		Area = contourArea(contours[i], true);						//求轮廓所包含图形面积
		if ((Area > 1000)&&(Area > AreaMax))						//保留面积最大的轮廓(且轮廓面积必须大于1000)
		{
			Cflag = true;
			AreaMax = Area;
			k = i;
		} 
	}  
	if (!Cflag)														//未找到符合要求的轮廓
	{
		return Cflag;
	}
	else
	{
//----------4.3-填充轮廓------------
		xMax = 0;
		yMax = 0;
		xMin = 10000;
		yMin = 10000;
		drawContours(imgFil,contours,k,Scalar(255),-1,8,hierarchy,CV_RETR_LIST);		//绘制轮廓
		for(int j = 0;j < contours[k].size();j++)   
		{  
			x = contours[k][j].x;									//轮廓点横坐标
			y = contours[k][j].y;									//轮廓点纵坐标
			if (x > xMax)
			{
				xMax = x;											//右下角横坐标
			}
			if (y > yMax)
			{
				yMax = y;											//右下角纵坐标
			}	
			if (x < xMin)
			{
				xMin = x;											//左下角横坐标
			}
			if (y < yMin)
			{
				yMin = y;											//左下角纵坐标
			}
		}
//----------4.4-裁剪图像------------
		if ((xMax-xMin) > (yMax-yMin))								//求最大图边长
		{
			LenPicS = xMax-xMin;
		} 
		else
		{
			LenPicS = yMax-yMin;
		}
		xCen = (xMin + xMax) / 2;									//计算图像中点横坐标
		yCen = (yMin + yMax) / 2;									//计算图像中点纵坐标
		xMin = xCen- LenPicS/2;										//更改左上角横坐标
		yMin = yCen- LenPicS/2;										//更改左上角纵坐标
		Size szTmp1(imgFil.rows+2*LenPicS,imgFil.rows+2*LenPicS);
		imgTmp = Mat::zeros(szTmp1,CV_8UC1);	
		imgTmpRoi = imgTmp(Rect(LenPicS,LenPicS,imgFil.rows,imgFil.rows));			
		imgFil.copyTo(imgTmpRoi);									//扩大图片(防止溢出)
		imgMax = imgTmp(Rect(xMin+LenPicS,yMin+LenPicS,LenPicS,LenPicS));	//截取填充部分(最大图)
//----------4.5-比例套边------------
		LenPicD = sqrt(AreaMax / prop);								//根据面积计算比例图边长
		if (LenPicS < LenPicD)
		{
			Size szTmp2(LenPicD,LenPicD);
			imgTmp = Mat::zeros(szTmp2,CV_8UC1);
			imgTmpRoi = imgTmp(Rect((LenPicD-LenPicS)/2,(LenPicD-LenPicS)/2,LenPicS,LenPicS));  
			imgMax.copyTo(imgTmpRoi);								//为填充区域套上边框
			img = imgTmp; 
		}
		else
		{
			img = imgMax;
		}
	}
	return Cflag;
}
//


//---------------------------------------------------5.图像匹配算法--------------------------------------------------------
int mapping (Mat cmpSrc, Mat cmptem)
{
	iavg1 = 0;
	iavg2 = 0;
	for (int i = 0; i < nL; i++)									//遍历边
	{
		uchar* data1 = cmpSrc.ptr<uchar>(i);						//以指针访问像素
		uchar* data2 = cmptem.ptr<uchar>(i);

		tmp = i * nL;
		for (int j = 0; j < nL; j++) 
		{
			int tmp1 = tmp + j;
			arr1[tmp1] = data1[j] / 4 * 4;
			arr2[tmp1] = data2[j] / 4 * 4;
			iavg1 += arr1[tmp1];
			iavg2 += arr2[tmp1];
		}
	}
	iavg1 /= nS;													//求平均值
	iavg2 /= nS;
	for (int i = 0; i < nS; i++)									//遍历图
	{
		arr1[i] = (arr1[i] >= iavg1) ? 1 : 0;						//大于平均值置1,否则置0
		arr2[i] = (arr2[i] >= iavg2) ? 1 : 0;
	}
	diff = 0;
	for (int i=0;i<nS;i++)
	{
		if(arr1[i] != arr2[i])
		{
			++diff;													//不一样的像素个数
		}
	}
	return diff;
}
//


//------------------------------------------------------6.主函数-----------------------------------------------------------
int main()
{
	struct templatePic templ;											//用于存储模板信息的结构体
	vector<string> path;												//用于记录文件路径的向量
	vector<templatePic> temVec;											//用于记录模板信息的向量
//----------6.1-获取文件路径------------
	getFiles(folder, path);												//读取文件夹下所有文件
	for(i = 0; i < path.size(); i++ ) 
	{  
		imgTem = imread(path[i],0);										//根据文件路径读取图像
		if (path[i].length() == 23)										//路径名长度
		{
			tplName = path[i].substr(13,2);								//记录其名称(模板文件名长度1)
		}
		else
		{
			tplName = path[i].substr(13,4);								//记录其名称(模板文件名长度2)
		}
		resize(imgTem, imgTem, Size(nL, nL), 0, 0, INTER_CUBIC);		//压缩图片
		Point2f cen = Point2f(imgTem.cols/2,imgTem.rows/2);				//旋转中心
//----------6.2-旋转循环遍历------------
		for (ang=-angle;ang<= angle;ang+= 5)							//从-angle逐渐加至angle							
		{
			imgTemR = revolve(imgTem, cen, ang, 1.0);					//旋转图像
//----------6.3-使用向量归纳------------
			templ.templateName = tplName;								//存储模板名称
			templ.templatePath = path[i];								//存储模板路径
			templ.templateImg = imgTemR;								//存储模板图像
			temVec.push_back(templ);									//存入向量
		}
	}  			
//----------6.4-调用摄像头--------------
	VideoCapture cap(0);												//使用0号摄像头
	int w = static_cast<int>(cap.get(CV_CAP_PROP_FRAME_WIDTH));			//获取视频宽度
	int h = static_cast<int>(cap.get(CV_CAP_PROP_FRAME_HEIGHT));		//获取视频高度
	Size sz(w,h);														//视频尺寸
	while (cap.isOpened())												//摄像头打开,保持循环
	{  
//----------6.5-保存视频流--------------
		cap >> frame;													//从摄像头将视频流读出至Mat容器
		imgSrc = frame(Rect((w-h)/2,0,h,h)); 
		imshow("视频图像原画", imgSrc);
		if (waitKey(10) == 27)											//waitKey()参数为刷新速率,若参数为r,则每1000/r秒刷新一次				
		{																//27为ESC键值,按下ESC键则结束运行
			break;
		}
//----------6.6-图像前期处理------------
		pretreat(imgSrc);												//图像预处理
		Cflag = padding(imgSrc);										//图像填充轮廓并裁剪
		if (!Cflag)
		{
			continue;
		}
		imshow("视频图像轮廓", imgSrc);
		resize(imgSrc, imgSrc, Size(nL, nL), 0, 0, INTER_CUBIC);		//转化为nL位,减少信息
//----------6.7-图像模板比对------------
		diffMin=200;
		for (n=0;n<temVec.size();n++)									//循环遍历向量
		{
			diff = 0;
			diff = mapping(imgSrc, temVec[n].templateImg);				//视频帧鱼模板图逐图对比
			if(diff<diffMin)											
			{
				diffMin = diff;											//记录最少的不同像素的个数
				num = n;												//记录不同像素最少时的序号
			}
		}
		samePer = (double)(nS - diffMin) / (double)nS;					//计算相同的像素个数在总像素中所占的比例
		if (samePer<0.8)
		{
			cout<<"未能匹配到"<<endl;									//相似度太低则判断未能识别
		}
		else
		{																//输出相似的动物和相似度
			cout<<"匹配成功,这是"<<temVec[num].templateName<<"!   相似度为:"<<samePer*100<<"%"<<endl;
		}
		cout<<"--------------------------------------------------------------------------------"<<endl;
	}  
	return 0;
}
//





更多推荐

基于OpenCV的视频轮廓识别与图像匹配【手影识别】