基于OpenCv的简单手势识别(课程设计)

  • 基于OpenCv视觉库实现识别手势1-5,IDE采用的是Visual Studio 2015。
  • 图像可实现动态采集,通过修改代码可以调用移动设备的摄像头。
  • 原理是提前把手势1-5的图像存放在工程文件中,再把实时采集到的手势图像与之对比,利用Hu不变矩这一几何特征得出相似度最高的图像。

环境配置:Visual Studio 2015+OpenCV-3.1.0

代码和思路主要参考了这位作者(https://blog.csdn.net/luoyouren/article/details/65633170)
本人在此之上为代码添加了更完整易懂的注释,以及提供了提高识别率的建议,详细的软件安装及环境配置不细说了,直接上代码和实现效果

**

代码

**

/*此程序对背景有要求,背景色最好是纯色并且容易与手的颜色区分开,否则识别效果很差,建议用白色背景,需离摄像头30-50cm

* 摄像头读取-->
* HSV颜色空间转换-->
* HSV通道分离-->
* 中值滤波-->
* 肤色分割-->
* 形态学运算-->
* 轮廓检测及过滤-->
* 轮廓形状匹配

*/
/*--------https://blog.csdn.net/luoyouren/article/details/65633170--------*/

#include <iostream>
#include <string>
#include <opencv2\opencv.hpp>
using namespace cv;
using namespace std;

#define MAXVALUE (80)
#define KERNEL_SIZE (5)
#define TEMPLATE_NUMS   (5)				//括号内为模板图片个数

int minVal = 3, maxVal = 20;//白天建议3 14
int match_number = -1;

Mat frame;                  //原始图像帧,//Mat可以理解为一个存储数据的容器,关于Mat类的详解和用法,https://blog.csdn.net/guyuealian/article/details/70159660
vector <Mat> channels;      //HSV通道分离
Mat frameH;                 //H通道
Mat result;                 //最终结果
Mat resultRGB;              //将结果显示在原图
vector< vector<Point> > mContoursTemp;  //轮廓模板集,vector容器里面放了一个vector容器,子容器里放点
vector< vector<Point> > mContoursProc;  //待处理轮廓集

//函数声明
void trackBarMin(int pos, void* userdata) {}	//分割H通道时的最小值
void trackBarMax(int pos, void* userdata) {}	//分割H通道时的最大值
void init_hand_template(void);					//载入模板的轮廓
void hand_contours(Mat &srcImage);				// 对肤色分割、滤波去噪、开运算后图像进行轮廓提取并过滤
void hand_template_match(void);					// 将目标轮廓与模板轮廓进行匹配
void number_draw(Mat &img, int num);			// 在图片的左上角标注数字
void setMatInt(Mat & input_image, uchar val);	// 将Mat中的每个元素设置为某个数值
char *tmp_names[TEMPLATE_NUMS] = { "1.bmp", "2.bmp","3.bmp","4.bmp","5.bmp" };	// , "6.bmp", "7.bmp", "8.bmp", "9.bmp", "10.bmp"};
const char *num_char[] = { "1", "2", "3","4", "5"  };							// ,"6", "7", "8", "9", "10" };//在图片的左上角标注数字


/***********************************************************主函数******************************************************/
int main()
{
	// 载入模板的轮廓
	init_hand_template();

	//----------------------------肤色分割调参窗口---------------------//
	namedWindow("TrackBar", CV_WINDOW_AUTOSIZE);
	createTrackbar("minVal", "TrackBar", &minVal, MAXVALUE, trackBarMin);
	createTrackbar("maxVal", "TrackBar", &maxVal, MAXVALUE, trackBarMax);
	//----------------------------摄像头读取----------------------------//
	VideoCapture capture;
	capture.open(0);//括号里是0则调用电脑摄像头,若是"http://admin:admin@192.168.1.195:8081"则调用IP摄像头,如果是"Video 3.wmv"则调用视频文件

	if (false == capture.isOpened())
	{
		cout << "camera open failed!" << endl;
		return -1;
	}

	Mat kernel = getStructuringElement(MORPH_RECT, Size(KERNEL_SIZE, KERNEL_SIZE)); //函数返回指定形状和尺寸的结构元素
	//Mat getStructuringElement(int shape, Size esize, Point anchor = Point(-1, -1));
	//shape:表示内核的形状,有三种形状可以选择,矩形:MORPH_RECT;交叉形:MORPH_CORSS;椭圆形:MORPH_ELLIPSE
	//Size:内核的尺寸
	//Point anchor:锚点的位置

	while (true)
	{
		// 获取图片帧
		capture >> frame;
		if (true == frame.empty())
		{
			cout << "get no frame" << endl;
			break;
		}
		namedWindow("1.原始图片", CV_WINDOW_NORMAL);
		imshow("1.原始图片", frame);// 显示原始图片

		resultRGB = frame.clone();


		//--------------------------转换HSV颜色通道----------------------------------------//
		cvtColor(frame, frame, CV_BGR2HSV);//在opencv中,其默认的颜色制式排列是BGR而非RGB
		//void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0 );
		//InputArray src:	输入图像即要进行颜色空间变换的原图像,可以是Mat类 
		//OutputArray dst : 输出图像即进行颜色空间变换后存储图像,也可以Mat类
		//int code :		转换的代码或标识,即在此确定将什么制式的图片转换成什么制式的图片
		//int dstCn = 0 :	目标图像通道数,如果取值为0,则由src和code决定


		//----------------------------HSV色调饱和度亮度通道分离----------------------------//
		split(frame, channels);//分离后, channels[0]对应H, channels[1]对应S, channels[2]对应
		//void split(const Mat& src,Mat *mvBegin);
		//const Mat& src:	要进行分离的图像矩阵
		//channels:			可以是Mat数组的首地址,或者一个vector<Mat>对象
		frameH = channels[0];
		namedWindow("2.H通道图片", CV_WINDOW_NORMAL);
		imshow("2.H通道图片", frameH);//显示H通道图片


		//--------------------------------------滤波平滑-----------------------------------//  
		medianBlur(frameH, frameH, 11);	// 中值滤波,可以很好的去除椒盐噪声,而且ksize越大效果越好。
		//void medianBlur(InputArray src, OutputArray dst, int ksize)
		//int ksize:		滤波模板的尺寸大小,必须是大于1的奇数,如3、5、7……
		namedWindow("3.滤波平滑之后的图片", CV_WINDOW_NORMAL);
		imshow("3.滤波平滑之后的图片", frameH);// 显示滤波平滑之后的图片


		//-----------------------------------肤色分割, 二值化-----------------------------//
		inRange(frameH, Scalar(minVal), Scalar(maxVal), result);
		//void inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst)
		//src:				输入图像
		//lowerb:			lower boundary下限,scalar类型的像素值,单通道scalar取一个值就行,彩图3通道scalar三个值;
		//upperb:			上限,类型与lowerb同理
		//dst:				输出图像,尺寸与src一致,类型是CV_8U,但没有指定通道数
		namedWindow("4.肤色分割之后的图片", CV_WINDOW_NORMAL);
		imshow("4.肤色分割之后的图片", result);// 显示肤色分割之后的图片


		//----------------------------------------形态学运算-----------------------------//

	
		morphologyEx(result, result, MORPH_OPEN, kernel);//函数利用基本的膨胀和腐蚀技术,来执行更加高级形态学变换
		//morphologyEx(result, result, MORPH_CLOSE, kernel);

		//void morphologyEx(InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1, -1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue();
		//src:				输入图像,图像位深应该为以下五种之一:CV_8U, CV_16U,CV_16S, CV_32F 或CV_64F
		//dst:				输出图像,需和源图片保持一样的尺寸和类型。 				
		//int op:						表示形态学运算的类型:MORPH_ERODE	 腐蚀
		//													  MORPH_DILATE	 膨胀
		//													  MORPH_OPEN	 开运算     先腐蚀,再膨胀,可清除一些小东西(亮的),放大局部低亮度的区域
		//													  MORPH_CLOSE	 闭运算		先膨胀,再腐蚀,可清除小黑点
		//													  MORPH_GRADIENT 形态学梯度 膨胀图与腐蚀图之差,提取物体边缘
		//													  MORPH_TOPHAT	 顶帽		原图像-开运算图,突出原图像中比周围亮的区域
		//													  MORPH_BLACKHAT 黑帽		闭运算图-原图像,突出原图像中比周围暗的区域								
		//kernel:			形态学运算的内核,为NULL,使用参考点位于中心3x3的核。一般使用函数getStructuringElement配合这个参数的使用,本程序在第93行有
		//anchor:			锚的位置,其有默认值( - 1, - 1),表示锚位于中心。
		namedWindow("5.形态学运算之后的图片", CV_WINDOW_NORMAL);
		imshow("5.形态学运算之后的图片", result);// 显示形态学运算之后的图片
		

		//---------------对肤色分割、滤波去噪、开运算后图像进行轮廓提取并过滤------------//
		hand_contours(result);


		//-------------------将目标轮廓与模板轮廓进行匹配-------------------------------//
		hand_template_match();


		number_draw(resultRGB, match_number);//将匹配结果显示到图片的左上角
		namedWindow("8.识别结果", CV_WINDOW_NORMAL);
		imshow("8.识别结果", resultRGB);

		char key = (char)waitKey(10);
		if (27 == key)
		{
			break;
		}
	}
	return 0;
}

/*************************************载入模板轮廓(提前存储在工程文件夹的)********************************************/
void init_hand_template(void)
{
	Mat srcImage;
	Mat dstImage;
	vector< vector<Point> > mContours;
	vector< Vec4i > mHierarchy;

	for (int i = 0; i < TEMPLATE_NUMS; i++)
	{
		srcImage = imread(tmp_names[i], IMREAD_GRAYSCALE);//读取文件
		//imshow("srcimage ", srcImage);

		if (true == srcImage.empty())
		{
			cout << "Failed to load image: " << tmp_names << endl;
			continue;
		}

		dstImage = Mat::zeros(srcImage.rows, srcImage.cols, CV_8UC3);//Mat 初始化为0
		//表示定义了一个srcImage的行和srcImage的列的矩阵,矩阵的每个单元的由三个(C3:3 Channel)8位无符号整形(U Unsigned U8 8位)构成。

		// 寻找轮廓
		findContours(srcImage, mContours, mHierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point(0, 0));
		//void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point())
		//image:	输入图像,图像必须为8-bit单通道图像,图像中的非零像素将被视为1,0像素保留其像素值,故加载图像后会自动转换为二值图像。
		//contours:	检测到的轮廓,每个轮廓都是以点向量的形式进行存储即使用point类型的vector表示。
		//hierarchy:可选的输出向量(std::vector),包含了图像的拓扑信息,作为轮廓数量的表示hierarchy包含了很多元素,
		//			每个轮廓contours[i]对应hierarchy中hierarchy[i][0]~hierarchy[i][3],分别表示后一个轮廓,前一个轮廓,父轮廓,
		//			内嵌轮廓的索引,如果没有对应项,则相应的hierarchy[i]设置为负数。 
		//mode:		轮廓检索模式,RETR_EXTERNAL:	  表示只检测最外层轮廓,对所有轮廓设置hierarchy[i][2]=hierarchy[i][3]=-1 
		//						 RETR_LIST:		  提取所有轮廓,并放置在list中,检测的轮廓不建立等级关系
		//						 RETR_CCOMP:		  提取所有轮廓,并将轮廓组织成双层结构(two-level hierarchy),顶层为连通域的外围边界,次层位内层边界 
		//						 RETR_TREE:		  提取所有轮廓,并重新建立网状轮廓结构 
		//method:	轮廓近似方法,CHAIN_APPROX_NONE:  从获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1 
		//						 CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息 
		//offset:	轮廓点可选偏移量,有默认值Point()

		if (mContours.size() > 0)
		{
			drawContours(dstImage, mContours, -1, Scalar(0, 0, 255), 1, 8, mHierarchy);
			//namedWindow("dstImage", CV_WINDOW_NORMAL);
			//imshow(tmp_names[i], dstImage);
			mContoursTemp.insert(mContoursTemp.end(), mContours.begin(), mContours.end());
		}
	}
	cout << "mContoursTemp size = " << mContoursTemp.size() << endl;
}

/******************对(目标轮廓)集进行肤色分割、滤波去噪、开运算后图像进行轮廓提取并过滤(视频采集的)*****************/
void hand_contours(Mat &srcImage)
{
	Mat imageProc = srcImage.clone();
	Size sz = srcImage.size();//尺寸
	Mat draw = Mat::zeros(sz, CV_8UC3);
	vector< vector<Point> > mContours;
	vector< Vec4i > mHierarchy;

	findContours(imageProc, mContours, mHierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point(0, 0));//只查找最外层轮廓

	//cout << "mContours.size = " << mContours.size() << endl;

	mContoursProc.clear();//清空上次图像处理的轮廓

	if (mContours.size() > 0)
	{
		drawContours(draw, mContours, -1, Scalar(0, 0, 255), 1, 8 , mHierarchy);// 绘制所有轮廓
		//void drawContours(InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar& color, int thickness=1, int lineType=8, InputArray hierarchy=noArray(), int maxLevel=INT_MAX, Point offset=Point() )
		//contours:表示输入的轮廓组,每一组轮廓由点vector构成,
		//contourIdx:指明画第几个轮廓,如果该参数为负值,则画全部轮廓,
		//Scalar& color:轮廓的颜色,
		//thickness:轮廓的线宽,如果为负值或CV_FILLED表示填充轮廓内部,
		//lineType:线型,轮廓线的邻域模式('4'邻域 或 '8'邻域),
		//InputArray hierarchy=noArray():轮廓结构信息,可选 (从 findContours得到),
		//maxLevel:轮廓中的最大下降,
		//Point offset=Point():所有点的偏移(可选)

		namedWindow("6.所有轮廓", CV_WINDOW_NORMAL);
		imshow("6.所有轮廓", draw);//显示所有轮廓

		double contArea = 0;
		double imageArea = sz.width * sz.height;
		const int SIZE = mContours.size(); 
		Rect bound; //Rect矩形类,矩形界限

		for (int i = 0; i < SIZE; i++)
		{
			contArea = contourArea(mContours[i]);

			if (contArea / imageArea < 0.015)// 过滤小面积的轮廓,原函数是0.015
			{
				continue;
			}

			bound = boundingRect(mContours[i]);// 如果轮廓边界与窗口贴近或者相连,则排除
			if (bound.x < 2
				|| bound.y < 2
				|| (bound.x + bound.width + 2) > sz.width
				|| (bound.y + bound.height + 2) > sz.height)// ||是逻辑或运算符
			{
				continue;
			}
			mContoursProc.push_back(mContours[i]);//剩下的轮廓就是基本符合条件的轮廓,保存起来
		}

		//cout << "mContoursProc.size = " << mContoursProc.size() << endl;
      
		draw = Scalar::all(0); //将矩阵所有元素赋值为某个值
		drawContours(draw, mContoursProc, -1, Scalar(0, 0, 255), 1, 8);
		namedWindow("7.过滤后的轮廓", CV_WINDOW_NORMAL);
		imshow("7.过滤后的轮廓", draw); //显示过滤后的轮廓
	}
}

/*********************************************将目标轮廓与模板轮廓进行匹配**********************************************/
void hand_template_match(void)
{
	if ((mContoursProc.size() == 0) || (mContoursTemp.size() == 0))//如果目标轮廓的尺寸=0或模板轮廓的尺寸=0则返回,||是逻辑或运算符
	{
		//cout << "There are no contours to match" << endl;
		return;
	}

	double hu = 1;	//hu = 1.0
	double huTmp = 0.0;	//huTmp = 0.0
	const int SIZE = mContoursProc.size();
	int m = -1;
	int n = -1;

	for (int i = 0; i < TEMPLATE_NUMS; i++)	//TEMPLATE_NUMS=5
	{
		for (int j = 0; j < SIZE; j++)
		{
			huTmp = matchShapes(mContoursTemp[i], mContoursProc[j], CV_CONTOURS_MATCH_I1, 0);//根据计算比较两张图像Hu不变距的函数,函数返回值代表相似度大小,完全相同的图像返回值是0,返回值最大是1
			//MatchShapes(const void* object1, const void* object2, int method ,double parameter=0);
			//object1:待匹配的物体1
			//object2:待匹配的物体2
			//method: CV_CONTOURS_MATCH_I1:
			//		  CV_CONTOURS_MATCH_I2:
			//		  CV_CONTOURS_MATCH_I3:

			if (huTmp < hu)//hu矩越小,匹配度越高
			{
				hu = huTmp;//保存好,是哪个轮廓和哪个模板匹配上了
				m = i;
				n = j;
			}
		}
	}
	cout << "m = " << (m + 1) << "; n = " << n << "; hu = " << hu << endl;
	match_number = m + 1;// 匹配到的数字

}

/**********************************************在图片的左上角标注数字***************************************************/
void number_draw(Mat &img, int num)
{
	if (num < 1)//如果未识别到任何数字则返回
	{
		return;
	}
	string text = num_char[num - 1];
	putText(img, text, Point(5, 100), FONT_HERSHEY_SIMPLEX, 4, Scalar(255, 0, 255), 8); //在图像上绘制文字
	// putText(Mat& img, const string& text, Point origin, int fontFace, double fontScale, Scalar color, int thickness, int lineType, bool bottomLeftOrigin)
	// Mat& img:	待绘制的图像
	// string& text:待绘制的文字
	// Point origin:文本框的左下角
	// fontFace:	字体 (如FONT_HERSHEY_PLAIN)
	// fontScale:	尺寸因子,值越大文字越大
	// color:		线条的颜色(RGB)
	// thickness:	线条宽度
	// lineType:	线型(4邻域或8邻域,默认8邻域)
};

**

实现效果

**
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
**

注意模板的手势图片一定要是这样的黑白图片

**
在这里插入图片描述

这是我自己补充的流程图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 30
    点赞
  • 226
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值