本文是不完全转载(原文在此)。关于OpenCV的应用,网上能搜到的资料大都是用Python实现的,难得发现一篇是C++的,而且内容结构清晰、易懂,跟着练习了一遍,功能都跑通了,很满足!想把原文保存起来,以作留念(亦是致谢)。部分代码有所优化,手势识别算法略有增强。

前言

本文将使用OpenCV C++ 实现手势识别效果。本案例主要可以分为以下几个步骤:

1、手部关键点检测

2、手势识别

3、效果显示

接下来就来看看本案例具体是怎么实现的吧!!!

一. 手部关键点检测

如图所示,为我们的手部关键点所在位置。第一步,我们需要检测手部21个关键点。我们使用深度神经网络DNN模块来完成这件事。通过使用DNN模块可以检测出手部21个关键点作为结果输出,具体请看源码。

 

(图片来源:Google MediaPipe

1.1 功能源码

bool HandKeypoints_Detect(Mat& src, std::vector<Point>& keypoints)
{
	// 计算模型尺寸
	int width = src.cols;
	int height = src.rows;
	float ratio = width / (float)height;
	int modelHeight = 368;
	int modelWidth = int(ratio * modelHeight);

	// 加载DNN模型
	// 可以执行openpose开源项目 models目录下的getModels.bat 获取模型文件
	static const char szModelFile[] = ".\\assets\\pose_iter_102000.caffemodel"; // 主要用于存储训练好的神经网络模型的权重和偏置等参数
	static const char szProtoFile[] = ".\\assets\\pose_deploy.prototxt"; // 用于以文本格式定义神经网络的结构
	if (!std::filesystem::exists(szModelFile)) {
		std::cout << "Model file not found!" << std::endl;
		return false;
	}

	cv::dnn::Net net;
	try {
		net = cv::dnn::readNetFromCaffe(szProtoFile, szModelFile);
		//net = cv::dnn::readNet(szModelFile, szProtoFile);
	}
	catch (const std::exception& e) {
		std::cout << "Error: " << e.what() << std::endl;
		return false;
	}
	
	Mat blob = cv::dnn::blobFromImage(src, 1.0/255, Size(modelWidth, modelHeight), Scalar(0, 0, 0));
	net.setInput(blob, "image");

	// 结果输出
	Mat output = net.forward();
	int H = output.size[2];
	int W = output.size[3];
	int ptCount = keypoints.size();
	for (int i = 0; i < ptCount; i++) {
		Mat probMap(H, W, CV_32F, output.ptr(0, i)); // 第0行的第i列元素
		resize(probMap, probMap, Size(width, height));

		Point kp; // 最大可能性手部关键点位置
		double classProb;
		minMaxLoc(probMap, NULL, &classProb, NULL, &kp);
		keypoints[i] = kp;
	}

	return true;
}

1.2 功能效果

如图所示,我们已经通过DNN检测出21个手部关键点所在位置。接下来,我们需要使用这些关键点进行简单的手势识别。

二. 手势识别

2.1 算法原理

本案例实现手势识别是通过比较关键点位置确定的。首先拿出每个手指尖关键点索引(即4、8、12、16、20)。接下来,对比每个手指其它关键点与其指尖所在位置。

例如我们想确定大拇指现在的状态是张开的还是闭合的。如下图所示,由于OpenCV是以左上角为起点建立坐标系的。当大拇指处于张开状态时(掌心向内),我们可以发现,对比关键点4、关键点3所在位置。当4的x坐标大于3的x坐标时,拇指处于张开状态;当4的x坐标小于3的x坐标时,拇指处于闭合状态。

同理,其余四个手指,以食指为例。当关键点8的y坐标小于关键点6的y坐标时,此时食指处于张开状态;当关键点8的y坐标大于关键点6的y坐标时,此时食指处于闭合状态。

当手指处于张开状态时,我们计数1。通过统计手指的张开数达到手势识别的目的。具体请看源码。【注】在某些情况下,DNN识别的关键点有误,导致上述算法不够健壮。我对算法进行了局部增强,原理是:手指伸直时,从指尖往下的三个点须近似在同一条直线上。我们可以通过计算这三个点构成的三角形面积来判断:如果三点共线,面积为0;否则,面积超过一定阈值就认为不共线了。此部分实现代码已发布在GitHub上。

2.2 功能源码

bool HandPose_Recognition(std::vector<Point>& keypoints, int& count)
{
	static const int tipIds[] = { 4, 8, 12, 16, 20 };
	
	count = 0;
	// 大拇指
	if (keypoints[tipIds[0]].x > keypoints[tipIds[0] - 1].x) {
		// 如果关键点'4'的x坐标大于关键点'3'的x坐标,则说明大拇指是张开的。计数1
		count++;
	}
	// 其他4个手指
	for (int i = 1; i < 5; i++) {
		// 比如:如果关键点'8'的y坐标小于关键点'6'的y坐标,则说明食指是张开的。计数1
		if (keypoints[tipIds[i]].y < keypoints[tipIds[i] - 2].y) {
			count++;
		}
	}

	return true;
}

三. 结果显示

通过以上步骤,我们已经有了手部关键点所在坐标位置以及对应的手势结果,接下来就进行效果展示。

在这里,为了逼格高一点,我们将下面的手势模板图像作为输出结果放进我们的测试图中。具体操作请看源码。

3.1 功能源码

bool HandPose_ShowResult(Mat& src, std::vector<Point>& keypoints, int& count)
{
	// 画出关键点所在位置
	int ptCount = keypoints.size();
	for (int i = 0; i < ptCount; i++) {
		cv::circle(src, keypoints[i], 3, cv::Scalar(0, 0, 255), -1);
		cv::putText(src, std::to_string(i), keypoints[i], FONT_HERSHEY_COMPLEX, 0.8, cv::Scalar(0, 255, 0), 2);
	}

	// 为了显示骚操作,读取模板图片,作为识别结果
	char szTempFile[100];
	snprintf(szTempFile, sizeof(szTempFile), "./assets/gesture/%02d.png", count);
	Mat temp = imread(szTempFile);
	resize(temp, temp, Size(100, 100), 1, 1, INTER_AREA);
	// 叠加到原图的左下角
	temp.copyTo(src(Rect(0, src.rows - temp.rows, temp.cols, temp.rows)));
	putText(src, std::to_string(count), Point(20, 60), FONT_HERSHEY_COMPLEX, 2, Scalar(0, 0, 128), 3);

	return true;
}

3.2 效果显示

更多的手势识别效果如下图:

总结

本文使用OpenCV C++实现一些简单的手势识别,在这里仅为了提供一个算法思想,理解了算法思想自己想实现什么功能都会很简单。主要操作有以下几点。

1、使用DNN模块实现手部关键点检测

2、利用各关键点所在位置来判定手指的张合状态。

3、效果显示(仅为了实现效果演示,可以省略)

完整的代码可以从这里下载:https://github.com/luqiming666/OpenCVMisc

Logo

更多推荐