C++视觉开发 四.手势识别

本章记录传统手势识别,在表示0-5六个数值时的识别问题。例如识别剪刀石头布,手势,以及其表示的动作。在识别时将手势中的凹陷区域称为凸缺陷,其个数作为识别的重要依据。

需要注意,在凸缺陷个数为0时,无法识别个数,需要引入凸包的概念,后面会讲到。实际过程中,算法获取凸缺陷时,会获取到细小的凸缺陷,需要将细小的凸缺陷屏蔽。

一.理论基础

1.凸包

凸包(Convex Hull)是(物体最外层)给定点集的最小凸多边形或多面体,包含所有点,并确保多边形内部任意两点的连线也在内部。换句话说,凸包指的是完全包含原有轮廓,并且仅由轮廓上的点构成的多边形。在凸包内可以想象成用橡皮筋围住一组钉在板上的钉子,橡皮筋绷紧后包围的区域即为凸包,任意三个点构成的面向内部的角的角度都小于180°。凸包在计算几何、计算机视觉、图形学等领域有广泛应用。

凸包示意图 

重要函数:

(1)cv::convexHull 计算凸包

功能:计算二维点集的凸包,该函数通常用于图像处理和计算几何中,以确定形状的边界。

函数语法:

void cv::convexHull( InputArray points, OutputArray hull,bool clockwise=false, bool returnPoints=true 
);
参数含义
points输入的二维点集,可以是 std::vector<cv::Point>cv::Mat 类型。
hull

输出的凸包结果,类型与输入点集相同

如果 returnPoints 为 true,则返回凸包上的点;

如果为 false,则返回凸包点集的索引。

clockwise

指定输出的凸包点的顺序。

如果为 true,则按顺时针方向排序;

否则按逆时针方向排序。默认值为 false

returnPoints

指定输出结果的类型。

如果为 true,则返回凸包上的点;

如果为 false,则返回凸包点集的索引。默认值为 true

示例代码:

    vector<vector<cv::Point>> contours;cv::findContours(binary, contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);vector<cv::Point> hull;cv::convexHull(contours[0], hull);

(2)cv::polylines 绘制多边形

功能:可以用来绘制一组点连接形成的多边形轮廓。

函数语法:

void cv::polylines( 
InputOutputArray image, 
InputArrayOfArrays hull, 
bool isClosed, 
const Scalar& color, 
int thickness=1, 
int lineType=LINE_8, 
int shift=0 
);
参数含义
image输入输出参数,表示要绘制的图像。
pts输入参数,表示一个或多个点集的数组,每个点集表示一条多边形线。
isClosed

输入参数,表示多边形是否封闭

如果为 true,则绘制一个封闭的多边形;否则绘制一条开放的多边形线。

color输入参数,表示线条的颜色。
thickness(可选)输入参数,表示线条的粗细,默认为 1。
lineType(可选)输入参数,表示线条的类型 LINE_8, LINE_4等。
shift(可选)输入参数,表示点坐标的小数点位数,默认为 0。

(3)应用示例:绘制图像的凸包

#include <opencv2/opencv.hpp>
#include <iostream>int main() {// 读取并绘制原始图像cv::Mat o = cv::imread("hand.bmp");if(o.empty()) {std::cerr << "Could not open or find the image!" << std::endl;return -1;}cv::imshow("original", o);// 提取轮廓cv::Mat gray;cv::cvtColor(o, gray, cv::COLOR_BGR2GRAY);cv::Mat binary;cv::threshold(gray, binary, 127, 255, cv::THRESH_BINARY);std::vector<std::vector<cv::Point>> contours;std::vector<cv::Vec4i> hierarchy;cv::findContours(binary, contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);// 寻找凸包,得到凸包的角点std::vector<cv::Point> hull;cv::convexHull(contours[0], hull);// 绘制凸包cv::polylines(o, hull, true, cv::Scalar(0, 255, 0), 2);// 输出凸包的角点for (const auto& point : hull) {std::cout << point << std::endl;}// 显示凸包cv::imshow("result", o);cv::waitKey(0);cv::destroyAllWindows();return 0;
}

2.凸缺陷

概念:凸缺陷(Convexity Defects)是指形状的凸包与形状之间的区域。这些缺陷表示在形状的边界上向内凹陷的部分。具体来说,凸缺陷是形状的轮廓与其凸包之间的点,这些点与凸包形成的线段是形状的凹陷部分。

通常情况下,使用如下四个特征值来表示凸缺陷:
1.起点:该特征值用于说明当前凸缺陷的起点位置。需要注意的是,起点值用轮廓索引表示。也就是说,起点一定是轮廓中的一个点,并且用其在轮廓中的序号来表示。例如,点A是凸缺陷1的起点。
2.终点:该特征值用于说明当前凸缺陷的终点位置。该值也是使用轮廓索引表示的。例如,图中的点B是凸缺陷1的终点。
3.轮廓上距离凸包最远的点:例如,点C是凸缺陷1中的轮廓上距离凸包最远的点。
4.最远点到凸包的近似距离:例如,距离D是凸缺陷1中的最远点到凸包的近似距离。


(1)cv::convexityDefects 计算凸缺陷

功能:计算输入轮廓与其凸包之间的凸缺陷,返回每个凸缺陷的起点、终点、最远点和深度

函数语法:

void cv::convexityDefects(InputArray contour, InputArray convexhull, OutputArray convexityDefects);
参数含义
contour输入的轮廓点集。
convexhull输入的凸包点的索引。
convexityDefects输出的凸缺陷。

示例代码:

// 计算凸缺陷
std::vector<cv::Vec4i> defects;
cv::convexityDefects(contours[0], hull, defects);

(2)cv::line 绘制线条

功能:在图像 img 上绘制一条从 pt1pt2 的线条,线条的颜色、粗细和类型可以由相应的参数控制。

函数语法:

void cv::line(Mat& img, Point pt1, Point pt2, const Scalar& color, int thickness=1, int lineType=LINE_8, int shift=0
);
参数含义
img输入/输出图像,在图像上绘制线条。
pt1线条的起点,类型为 cv::Point
pt2线条的终点,类型为 cv::Point
color

线条的颜色,类型为 cv::Scalar。

例如,cv::Scalar(0, 0, 255) 表示红色。

thickness(可选)线条的粗细,默认为 1。
lineType(可选)线条的类型。
shift点坐标的小数位数,默认为 0。

(3)cv::circle 绘制圆

功能:在图像 img 上绘制一个以 center 为圆心、半径为 radius 的圆,圆的颜色、线条粗细和类型可以由相应的参数控制。

函数语法:

void cv::circle(Mat& img, Point center, int radius, const Scalar& color, int thickness=1,int lineType=LINE_8, int shift=0
);

参数含义
img输入输出图像,在图像上绘制圆。
center圆心的坐标,类型为 cv::Point
radius圆的半径,类型为 int
color

圆的颜色,类型为 cv::Scalar

例如,cv::Scalar(255, 0, 0) 表示蓝色。

thickness圆的线条粗细。如果为负值,如 -1,则绘制填充的圆。
lineType线条的类型。
shift点坐标的小数位数,默认为 0。

(4)应用示例1:一个图像里有单独轮廓

#include <opencv2/opencv.hpp>
#include <iostream>int main() {// 读取图像cv::Mat img = cv::imread("hand.bmp");if (img.empty()) {std::cerr << "Could not open or find the image!" << std::endl;return -1;}cv::imshow("original", img);// 转换为灰度图像cv::Mat gray;cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);// 二值化cv::Mat binary;cv::threshold(gray, binary, 127, 255, cv::THRESH_BINARY);// 查找轮廓std::vector<std::vector<cv::Point>> contours;std::vector<cv::Vec4i> hierarchy;cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);// 计算凸包std::vector<int> hull;cv::convexHull(contours[0], hull, false);// 计算凸缺陷std::vector<cv::Vec4i> defects;cv::convexityDefects(contours[0], hull, defects);std::cout << "defects=\n";for (const auto& defect : defects) {std::cout << defect << std::endl;}// 绘制凸缺陷for (size_t i = 0; i < defects.size(); i++) {int s = defects[i][0]; // 起点int e = defects[i][1]; // 终点int f = defects[i][2]; // 远点// int d = defects[i][3]; // 距离(这里未使用)cv::Point start = contours[0][s];cv::Point end = contours[0][e];cv::Point far = contours[0][f];cv::line(img, start, end, cv::Scalar(0, 0, 255), 2);cv::circle(img, far, 5, cv::Scalar(255, 0, 0), -1);}// 显示结果cv::imshow("result", img);cv::waitKey(0);cv::destroyAllWindows();return 0;
}

 结果如图:

(5)应用示例2: 一个图像里有很多轮廓

需要对每一个轮廓分别计算凸包和凸缺陷,并将结果绘制在同一张图像上。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>void processImage(cv::Mat& img) {// 转换为灰度图像cv::Mat gray;cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);// 二值化cv::Mat binary;cv::threshold(gray, binary, 127, 255, cv::THRESH_BINARY);// 查找轮廓std::vector<std::vector<cv::Point>> contours;std::vector<cv::Vec4i> hierarchy;cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);// 遍历每个轮廓for (size_t i = 0; i < contours.size(); i++) {// 计算凸包std::vector<int> hull;cv::convexHull(contours[i], hull, false);// 计算凸缺陷std::vector<cv::Vec4i> defects;cv::convexityDefects(contours[i], hull, defects);// 绘制凸包std::vector<cv::Point> hullPoints;for (size_t j = 0; j < hull.size(); j++) {hullPoints.push_back(contours[i][hull[j]]);}cv::polylines(img, hullPoints, true, cv::Scalar(0, 255, 0), 2);// 绘制凸缺陷for (size_t j = 0; j < defects.size(); j++) {int s = defects[j][0]; // 起点int e = defects[j][1]; // 终点int f = defects[j][2]; // 远点cv::Point start = contours[i][s];cv::Point end = contours[i][e];cv::Point far = contours[i][f];cv::line(img, start, end, cv::Scalar(0, 0, 255), 2);cv::circle(img, far, 5, cv::Scalar(255, 0, 0), -1);}}
}int main() {// 读取图像cv::Mat img = cv::imread("hand.bmp");if (img.empty()) {std::cerr << "Could not open or find the image!" << std::endl;return -1;}cv::imshow("original", img);// 处理图像processImage(img);// 显示结果cv::imshow("result", img);cv::waitKey(0);cv::destroyAllWindows();return 0;
}

3.凸缺陷与凸包面积比

当有0个凸缺陷时,手势既可能是1也可能是0,所以我们做以下判断:

凸包面积=凸缺陷面积+轮廓面积。

数值0的手势:轮廓/凸包面积> 0.9。

数值1的手势:轮廓/凸包面积≤ 0.9。

应用示例:

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;void reg(cv::Mat& img) {cv::Mat gray;cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);cv::Mat binary;cv::threshold(gray, binary, 127, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);vector < vector<cv::Point>> contours;cv::findContours(binary, contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);//找到最大轮廓/*auto max_contour = std::max_element(contours.begin(), contours.end(), [](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {return cv::contourArea(a) < cv::contourArea(b);});*/double max_area = 0;vector<cv::Point> max_contours;for (int i = 0; i < contours.size(); ++i) {double area = cv::contourArea(contours[i]);if (area > max_area) {max_area = area;max_contours = contours[i];}}cout << "Max contour area:" << max_area << endl;vector<cv::Point> hull;cv::convexHull(max_contours, hull);double areahull = cv::contourArea(hull);// 通常情况下,手势0,轮廓和凸包大致相等,该值大于0.9.// 手势1,轮廓要比凸包小一些,该值小于等于0.9double arearatio = max_area / areahull;string result;if (arearatio > 0.9) {result = "fist:0";}else {result = "finger:1";}cv::Point org(0, 80);int font = cv::FONT_HERSHEY_SIMPLEX;double fontScale = 2;cv::Scalar color(0, 0, 255);int thickness = 3;cv::putText(img, result, org, font, fontScale, color, thickness);};
int main() {cv::Mat img1 = cv::imread("zero.jpg");cv::Mat img2 = cv::imread("one.jpg");if (img1.empty() || img2.empty()) {cerr << "error" << endl;return -1;}reg(img1);reg(img2);cv::imshow("zero", img1);cv::imshow("one", img2);cv::waitKey();cv::destroyAllWindows();return 0;
}

二.识别过程

1.识别流程

上面为基本流程图,下面介绍具体步骤。

2.具体步骤

(1)获取图像

读取摄像头,划定识别区域,仅在区域里识别手势。

     cv::Mat frame;cap.read(frame);if (frame.empty()) break;cv::flip(frame, frame, 1);// 设定一个固定区域作为识别区域cv::Rect roi_rect(400, 10, 200, 200);cv::Mat roi = frame(roi_rect);cv::rectangle(frame, roi_rect, cv::Scalar(0, 0, 255), 0);

(2)识别皮肤

本步骤的主要任务是色彩空间转换。将图像从BGR转化为HSV,以进行皮肤检测。

HSV 色彩空间的稳定性

色调(Hue, H):表示颜色的类型,例如红色、绿色等。

饱和度(Saturation, S):表示颜色的纯度。

明度(Value, V):表示颜色的亮度。

皮肤颜色在HSV色彩空间中的色调范围相对稳定,通常集中在一定的色调范围内。在HSV色彩空间中,色调(Hue)对于光照变化和阴影的影响较小,这使得在不同光照条件下,颜色的检测更加稳定。尽管皮肤颜色的亮度和饱和度可能会有所不同,但色调(Hue)变化较小,这使得使用HSV空间可以更有效地检测皮肤。

        // 在hsv色彩空间内检测出皮肤cv::Mat hsv;cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV);cv::Scalar lower_skin(0, 28, 70);cv::Scalar upper_skin(20, 255, 255);cv::Mat mask;cv::inRange(hsv, lower_skin, upper_skin, mask);

(3)图像预处理

去除噪声,高斯滤波

        // 预处理cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2));cv::dilate(mask, mask, kernel, cv::Point(-1, -1), 4);cv::GaussianBlur(mask, mask, cv::Size(5, 5), 100);

(4)获取轮廓

        // 找出轮廓vector<vector<cv::Point>> contours;vector<cv::Vec4i> hierarchy;cv::findContours(mask, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);double max_area = 0;vector<cv::Point> max_contour;for (int i = 0; i < contours.size(); ++i) {double area = cv::contourArea(contours[i]);if (area > max_area) {max_area = area;max_contour = contours[i];}}

(5)获取凸包

        vector<cv::Point> hull;cv::convexHull(max_contour, hull);double areahull = cv::contourArea(hull);

(6)轮廓与凸包面积比

        double arearatio = max_area / areahull;

(7)获取凸缺陷

        // 获取凸缺陷vector<int> hull_indices;cv::convexHull(max_contour, hull_indices, false);vector<cv::Vec4i> defects;cv::convexityDefects(max_contour, hull_indices, defects);

(8)计算并绘制有效凸缺陷

        int n = 0; // 凹凸点个数初始值为0// 遍历凸缺陷,判断是否为指间凸缺陷for (size_t i = 0; i < defects.size(); i++) {int s = defects[i][0]; // 起点int e = defects[i][1]; // 终点int f = defects[i][2]; // 远点cv::Point start = (max_contour)[s];cv::Point end = (max_contour)[e];cv::Point far = (max_contour)[f];double a = cv::norm(end - start);double b = cv::norm(far - start);double c = cv::norm(end - far);// 计算手指之间的角度double angle = acos((b * b + c * c - a * a) / (2 * b * c)) * 57;if (angle <= 90 && defects[i][3] > 20) {n++;cv::circle(roi, far, 3, cv::Scalar(255, 0, 0), -1); // 用蓝色绘制最远点}// 绘制手势的凸包cv::line(roi, start, end, cv::Scalar(0, 255, 0), 2);}

(9)使用凸缺陷识别手势

        // 通过凸缺陷个数及面积比判断识别结果string result;if (n == 0) {if (arearatio > 0.9) {result = "0";}else {result = "1";}}else if (n == 1) {result = "2";}else if (n == 2) {result = "3";}else if (n == 3) {result = "4";}else if (n == 4) {result = "5";}

(10)显示结果

        // 显示识别结果cv::putText(frame, result, cv::Point(400, 80), cv::FONT_HERSHEY_SIMPLEX, 2, cv::Scalar(0, 0, 255), 3);cv::imshow("frame", frame);if (cv::waitKey(25) == 27) { // 键盘Esc键退出break;}}cv::destroyAllWindows();cap.release();

完整代码:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
#include <vector>using namespace std;int main() {cv::VideoCapture cap(0, cv::CAP_DSHOW);if (!cap.isOpened()) {cerr << "Error opening video stream" << endl;return -1;}while (cap.isOpened()) {cv::Mat frame;cap.read(frame);if (frame.empty()) break;cv::flip(frame, frame, 1);// 设定一个固定区域作为识别区域cv::Rect roi_rect(400, 10, 200, 200);cv::Mat roi = frame(roi_rect);cv::rectangle(frame, roi_rect, cv::Scalar(0, 0, 255), 0);// 在hsv色彩空间内检测出皮肤cv::Mat hsv;cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV);cv::Scalar lower_skin(0, 28, 70);cv::Scalar upper_skin(20, 255, 255);cv::Mat mask;cv::inRange(hsv, lower_skin, upper_skin, mask);// 预处理cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2));cv::dilate(mask, mask, kernel, cv::Point(-1, -1), 4);cv::GaussianBlur(mask, mask, cv::Size(5, 5), 100);// 找出轮廓vector<vector<cv::Point>> contours;vector<cv::Vec4i> hierarchy;cv::findContours(mask, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);// 找到最大轮廓/*auto max_contour = max_element(contours.begin(), contours.end(), [](const vector<cv::Point>& a, const vector<cv::Point>& b) {return cv::contourArea(a) < cv::contourArea(b);});double areacnt = cv::contourArea(*max_contour);*/double max_area = 0;vector<cv::Point> max_contour;for (int i = 0; i < contours.size(); ++i) {double area = cv::contourArea(contours[i]);if (area > max_area) {max_area = area;max_contour = contours[i];}}// 获取轮廓的凸包vector<cv::Point> hull;cv::convexHull(max_contour, hull);double areahull = cv::contourArea(hull);// 获取轮廓面积、凸包的面积比double arearatio = max_area / areahull;// 获取凸缺陷vector<int> hull_indices;cv::convexHull(max_contour, hull_indices, false);vector<cv::Vec4i> defects;cv::convexityDefects(max_contour, hull_indices, defects);int n = 0; // 凹凸点个数初始值为0// 遍历凸缺陷,判断是否为指间凸缺陷for (size_t i = 0; i < defects.size(); i++) {int s = defects[i][0]; // 起点int e = defects[i][1]; // 终点int f = defects[i][2]; // 远点cv::Point start = (max_contour)[s];cv::Point end = (max_contour)[e];cv::Point far = (max_contour)[f];double a = cv::norm(end - start);double b = cv::norm(far - start);double c = cv::norm(end - far);// 计算手指之间的角度double angle = acos((b * b + c * c - a * a) / (2 * b * c)) * 57;if (angle <= 90 && defects[i][3] > 20) {n++;cv::circle(roi, far, 3, cv::Scalar(255, 0, 0), -1); // 用蓝色绘制最远点}// 绘制手势的凸包cv::line(roi, start, end, cv::Scalar(0, 255, 0), 2);}// 通过凸缺陷个数及面积比判断识别结果string result;if (n == 0) {if (arearatio > 0.9) {result = "0";}else {result = "1";}}else if (n == 1) {result = "2";}else if (n == 2) {result = "3";}else if (n == 3) {result = "4";}else if (n == 4) {result = "5";}// 显示识别结果cv::putText(frame, result, cv::Point(400, 80), cv::FONT_HERSHEY_SIMPLEX, 2, cv::Scalar(0, 0, 255), 3);cv::imshow("frame", frame);if (cv::waitKey(25) == 27) { // 键盘Esc键退出break;}}cv::destroyAllWindows();cap.release();return 0;
}

结果如图:

三.剪刀石头布

1.cv::matchShapes 形状匹配

功能:用于比较两个形状相似度的函数。它通过计算两个对象的Hu矩来测量相似度。两个对象可以是轮廓,也可以是灰度图。

函数语法:

参数含义
contour1第一个轮廓或灰度图像
contour2第二个轮廓或灰度图像
method

int类型,用于计算相似度的比较方法。常见的方法有:

cv::CONTOURS_MATCH_I1: I1 (Hu)距离。可直接写为1

cv::CONTOURS_MATCH_I2: I2 (Hu)距离。可直接写为2

cv::CONTOURS_MATCH_I3: I3 (Hu)距离。可直接写为3

parameter不使用时传入 0

代码示例: 

#include <opencv2/opencv.hpp>
#include <iostream>using namespace std;int main() {// 读取图像cv::Mat o1 = cv::imread("o1.jpg");cv::Mat o2 = cv::imread("o2.jpg");cv::Mat o3 = cv::imread("o3.jpg");if (o1.empty() || o2.empty() || o3.empty()) {cerr << "Could not open or find the images!" << endl;return -1;}// 转换为灰度图像cv::Mat gray1, gray2, gray3;cv::cvtColor(o1, gray1, cv::COLOR_BGR2GRAY);cv::cvtColor(o2, gray2, cv::COLOR_BGR2GRAY);cv::cvtColor(o3, gray3, cv::COLOR_BGR2GRAY);// 二值化cv::Mat binary1, binary2, binary3;cv::threshold(gray1, binary1, 127, 255, cv::THRESH_BINARY);cv::threshold(gray2, binary2, 127, 255, cv::THRESH_BINARY);cv::threshold(gray3, binary3, 127, 255, cv::THRESH_BINARY);// 查找轮廓vector<vector<cv::Point>> contours1, contours2, contours3;vector<cv::Vec4i> hierarchy;cv::findContours(binary1, contours1, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);cv::findContours(binary2, contours2, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);cv::findContours(binary3, contours3, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);if (contours1.empty() || contours2.empty() || contours3.empty()) {cerr << "Could not find contours in the images!" << endl;return -1;}// 获取第一个轮廓vector<cv::Point> cnt1 = contours1[0];vector<cv::Point> cnt2 = contours2[0];vector<cv::Point> cnt3 = contours3[0];// 形状匹配double ret0 = cv::matchShapes(cnt1, cnt1, 1, 0.0);double ret1 = cv::matchShapes(cnt1, cnt2, 1, 0.0);double ret2 = cv::matchShapes(cnt1, cnt3, 1, 0.0);// 输出结果cout << "o1.shape = " << o1.size() << endl;cout << "o2.shape = " << o2.size() << endl;cout << "o3.shape = " << o3.size() << endl;cout << "相同图像(cnt1,cnt1)的matchShape = " << ret0 << endl;cout << "相似图像(cnt1,cnt2)的matchShape = " << ret1 << endl;cout << "不相似图像(cnt1,cnt3)的matchShape = " << ret2 << endl;// 显示图像cv::imshow("original1", o1);cv::imshow("original2", o2);cv::imshow("original3", o3);cv::waitKey(0);cv::destroyAllWindows();return 0;
}

运行结果:
 

需要注意,选取轮廓作参数时,仅从原始图像中选取了部分轮廓参与匹配。

而使用灰度图作为参数时,函数使用了更多特征参与匹配,所以结果不一样。

2.剪刀石头布识别

实现程序:图片的剪刀石头布识别

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;string reg(const cv::Mat& x) {cv::Mat o1 = cv::imread("paper.jpg");cv::Mat o2 = cv::imread("rock.jpg");cv::Mat o3 = cv::imread("scissors.jpg");if (o1.empty() || o2.empty() || o3.empty()) {cerr << "Could not open or find the images!" << endl;return "";}cv::Mat gray1, gray2, gray3, xgray;cv::cvtColor(o1, gray1, cv::COLOR_BGR2GRAY);cv::cvtColor(o2, gray2, cv::COLOR_BGR2GRAY);cv::cvtColor(o3, gray3, cv::COLOR_BGR2GRAY);cv::cvtColor(x, xgray, cv::COLOR_BGR2GRAY);cv::Mat binary1, binary2, binary3, xbinary;cv::threshold(gray1, binary1, 127, 255, cv::THRESH_BINARY);cv::threshold(gray2, binary2, 127, 255, cv::THRESH_BINARY);cv::threshold(gray3, binary3, 127, 255, cv::THRESH_BINARY);cv::threshold(xgray, xbinary, 127, 255, cv::THRESH_BINARY);vector<vector<cv::Point>> contours1, contours2, contours3, xcontours;vector<cv::Vec4i> hierarchy;cv::findContours(binary1, contours1, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);cv::findContours(binary2, contours2, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);cv::findContours(binary3, contours3, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);cv::findContours(xbinary, xcontours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);if (contours1.empty() || contours2.empty() || contours3.empty() || xcontours.empty()) {cerr << "Could not find contours in one or more images!" << endl;return "";}vector<cv::Point> cnt1 = contours1[0];vector<cv::Point> cnt2 = contours2[0];vector<cv::Point> cnt3 = contours3[0];vector<cv::Point> cntx = xcontours[0];vector<double> ret;ret.push_back(cv::matchShapes(cntx, cnt1, cv::CONTOURS_MATCH_I1, 0.0));ret.push_back(cv::matchShapes(cntx, cnt2, cv::CONTOURS_MATCH_I1, 0.0));ret.push_back(cv::matchShapes(cntx, cnt3, cv::CONTOURS_MATCH_I1, 0.0));int max_index = min_element(ret.begin(), ret.end()) - ret.begin();string result;if (max_index == 0) {result = "paper";} else if (max_index == 1) {result = "rock";} else {result = "scissors";}return result;
}int main() {cv::Mat t1 = cv::imread("test1.jpg");cv::Mat t2 = cv::imread("test2.jpg");cv::Mat t3 = cv::imread("test3.jpg");if (t1.empty() || t2.empty() || t3.empty()) {cerr << "Could not open or find the test images!" << endl;return -1;}// 输出识别结果cout << reg(t1) << endl;cout << reg(t2) << endl;cout << reg(t3) << endl;// 显示处理结果cv::Point org(0, 60);int font = cv::FONT_HERSHEY_SIMPLEX;double fontScale = 2;cv::Scalar color(255, 255, 255);int thickness = 3;cv::putText(t1, reg(t1), org, font, fontScale, color, thickness);cv::putText(t2, reg(t2), org, font, fontScale, color, thickness);cv::putText(t3, reg(t3), org, font, fontScale, color, thickness);cv::imshow("test1", t1);cv::imshow("test2", t2);cv::imshow("test3", t3);cv::waitKey(0);cv::destroyAllWindows();return 0;
}

拓展:摄像头使用凸缺陷识别剪刀石头布

#include <opencv2/opencv.hpp>
#include <iostream>
#include <cmath>int main() {cv::VideoCapture cap(0, cv::CAP_DSHOW);if (!cap.isOpened()) {std::cerr << "Error: Could not open camera." << std::endl;return -1;}while (true) {cv::Mat frame;cap >> frame;if (frame.empty()) {std::cerr << "Error: Could not read frame." << std::endl;break;}cv::flip(frame, frame, 1);cv::Rect roi_rect(400, 10, 200, 200);cv::Mat roi = frame(roi_rect);cv::rectangle(frame, roi_rect, cv::Scalar(0, 0, 255), 2);cv::Mat hsv;cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV);cv::Scalar lower_skin(0, 28, 70);cv::Scalar upper_skin(20, 255, 255);cv::Mat mask;cv::inRange(hsv, lower_skin, upper_skin, mask);cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2));cv::dilate(mask, mask, kernel, cv::Point(-1, -1), 4);cv::GaussianBlur(mask, mask, cv::Size(5, 5), 100);std::vector<std::vector<cv::Point>> contours;cv::findContours(mask, contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);if (contours.empty()) continue;auto cnt = *std::max_element(contours.begin(), contours.end(), [](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {return cv::contourArea(a) < cv::contourArea(b);});double areacnt = cv::contourArea(cnt);std::vector<cv::Point> hull;cv::convexHull(cnt, hull);double areahull = cv::contourArea(hull);double arearatio = areacnt / areahull;std::vector<int> hull_indices;cv::convexHull(cnt, hull_indices, false);std::vector<cv::Vec4i> defects;cv::convexityDefects(cnt, hull_indices, defects);int n = 0;for (const auto& defect : defects) {cv::Point start = cnt[defect[0]];cv::Point end = cnt[defect[1]];cv::Point far = cnt[defect[2]];double a = cv::norm(end - start);double b = cv::norm(far - start);double c = cv::norm(end - far);double angle = std::acos((b * b + c * c - a * a) / (2 * b * c)) * 57;if (angle <= 90 && defect[3] > 20) {n++;cv::circle(roi, far, 3, cv::Scalar(255, 0, 0), -1);}cv::line(roi, start, end, cv::Scalar(0, 255, 0), 2);}std::string result;if (n == 0) {result = (arearatio > 0.9) ? "Rock" : "Invalid";} else if (n == 1 || n == 2) {result = "Scissors";} else if (n == 4) {result = "Paper";} else {result = "Invalid";}cv::putText(frame, result, cv::Point(400, 80), cv::FONT_HERSHEY_SIMPLEX, 2, cv::Scalar(0, 0, 255), 3);cv::imshow("frame", frame);if (cv::waitKey(25) == 27) break;  // Exit on ESC key}cv::destroyAllWindows();cap.release();return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/866025.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

S272钡铼技术4G无线RTU支持多路DIN输入和模拟量转换至4G网络

钡铼第四代RTU S272是一款先进的工业级4G远程遥测终端&#xff0c;为各种远程工业数据采集和控制系统提供了高效解决方案。结合了现代通信技术和多功能的输入输出接口&#xff0c;S272不仅支持多路数字量和模拟量输入&#xff0c;还具备灵活的扩展性和强大的控制功能&#xff0…

WEB攻防-XSS跨站反射型存储型DOM型标签闭合输入输出JS代码解析

文章目录 XSS跨站-输入输出-原理&分类&闭合XSS跨站-分类测试-反射&存储&DOM反射型XSS存储型XSSDOM-base型XSS XSS跨站-输入输出-原理&分类&闭合 漏洞原理&#xff1a;接受输入数据&#xff0c;输出显示数据后解析执行 基础类型&#xff1a;反射(非持续…

典型案例 | 基于全数字实时仿真的嵌入式DevOps解决方案

为丰富浙江省信息技术应用创新&#xff08;以下简称“信创”&#xff09;产业生态&#xff0c;在全社会各领域形成示范效应&#xff0c;浙江省经信厅联合省密码管理局开展2023年浙江省深化信创典型案例评选工作。 经过征集申报、专家评选、名单公示等程序&#xff0c;确定36个…

实现前端项目自动构建和部署(Gitee Go)

前言 相信所有的前端开发者都希望将自己的代码部署在服务器上让所有人都能访问到&#xff0c;但是却不知道如何进行部署。其实要是实现代码上线非常简单&#xff0c;我们只需要将build之后的代码上传到服务器&#xff0c;然后通过Nginx起一个服务指向我们build后的代码就可以了…

Cocos 7.2~7.4

这几天没更新CSDN&#xff0c;跑去玩Cocos了。自从知道我的粉丝百分之十之八九都是假人&#xff0c;更新确实没什么动力了。主要还是把这边当成一个日记本吧。 选择cocos的原因也很简单。会点js&#xff0c;技术栈比较接近&#xff0c;上手估计也快。简单记录下这几天的内容 主…

@amap/amap-jsapi-loader 实现高德地图中添加多边围栏,并可编辑,编辑后获得围栏各个点的经纬度

先上一张效果图 看看是不是大家想要的效果&#xff5e; ❤️ 希望其中的小点能帮助大家&#xff0c;主要看怎么绘制在地图上的代码即可 1.第一步要加入项目package.json中或者直接yarn install它都可以 想必大家应该都会 "amap/amap-jsapi-loader": "0.0.7&qu…

C语言作业笔记

1. 要找俩个数使其相加等于一个数&#xff0c;那么俩个数从头尾出发&#xff0c;先动一边&#xff0c;假设是尾先动&#xff0c;一开始俩个数相加大于sum&#xff08;小于的话就动头&#xff09;&#xff0c;那么总有一时刻俩数相加小于sum&#xff0c;则就在那一刻停下来&…

关于5G和卫星

手机&#xff0c;已经串联起了我们生活中的一切环节。我们随时随地拿出手机&#xff0c;都能畅快地上网。 这一切是如此地理所当然&#xff0c;以至于我们甚至想不到这样不可思议的问题&#xff1a; 移动通信网络真的无处不在吗&#xff1f; 我们都知道&#xff0c;地球虽叫…

毕业论文初稿写作方法与过程

毕业论文初稿写作方法与过程 毕业论文是大学生在学业结束前必须完成的一项重要任务&#xff0c;它不仅是对学生所学知识的综合运用&#xff0c;也是对学生研究能力和写作能力的检验。写好毕业论文初稿是完成高质量毕业论文的关键一步。下面将具体阐述毕业论文初稿的写作方法和过…

Redis 7.x 系列【18】事务

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 概述2. 命令2.1 MULTI2.2 EXEC2.3 DISCARD2.4 WATCH2.5 UNWATCH 3. 事务中的错误4.…

无法识别为 cmdlet、函数、脚本文件或可运行程序的名称

一、遇到问题 PS D:\software\nacos\nacos-server-2.3.1\bin> startup.cmd -m standalone startup.cmd : 无法将“startup.cmd”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&#xff0c; 请确保路径正确&#xff0c;然后…

Ubuntu su命令输入密码后提示“su: 认证失败”

在Ubuntu系统中&#xff0c;使用su命令切换到root用户时提示“su: 认证失败”通常是因为root账户默认情况下是被锁定的&#xff0c;没有设置密码。以下是一些解决这个问题的方法&#xff1a; 使用sudo命令&#xff1a;Ubuntu推荐使用sudo命令代替直接使用root用户。sudo命令允许…

Aavegotchi的Gotchiverse新地图: 沉睡的野兽即将苏醒!

Gotchi 守护者们&#xff0c;准备好了&#xff0c;因为我们要大开杀戒了&#xff01; 加入我们吧&#xff08;后果自负&#xff01;&#xff09;&#xff0c;我们将深入Gotchiverse&#xff0c;前往奥姆夫山--我们虚拟世界中所有 FOMO 的炽热源头。 请继续阅读&#xff0c;了解…

AI 绘画的常用技巧和操作方法

随着人工智能技术的飞速发展&#xff0c;AI 绘画已经成为设计和艺术领域的一股新兴力量。无论是设计师、艺术家&#xff0c;还是普通的科技爱好者&#xff0c;都能通过 AI 绘画工具创造出令人惊叹的作品。 AI 绘画的基本原理 AI 绘画的核心在于机器学习算法。通过训练大量的图像…

餐饮界的新传奇:沃可趣员工社区,让品牌关怀在指尖流淌

咖啡师与顾客发生肢体冲突、员工用咖啡粉泼顾客……某精品咖啡一天爆出两个大瓜&#xff01; 很快有网友指出咖啡店员工长期遭受重压&#xff0c;与品牌之间存在根本矛盾。 同样做餐饮的老牌快餐&#xff0c;门店密度与之不相上下&#xff0c;却很少发生这样的暴雷。 不仅因…

算法力扣刷题 三十一【150. 逆波兰表达式求值】

前言 栈和队列篇。 记录 三十一【150. 逆波兰表达式求值】 一、题目阅读 给你一个字符串数组 tokens &#xff0c;表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式。返回一个表示表达式值的整数。 注意&#xff1a; 有效的算符为 、-、* 和 / 。 每个操作…

Django创建项目(1)

运行 注意 在本次创建Django项目时&#xff0c;出现了一点小问题&#xff0c;由于我之前pip换源过&#xff0c;换源用的是http&#xff0c;结果在创建时&#xff0c;pip只支持https&#xff0c;所以如果出现创建项目失败的问题&#xff0c;那么有可能是因为换源的问题&#xf…

(三十一)Flask之wtforms库【剖析源码下篇】

每篇前言&#xff1a; &#x1f3c6;&#x1f3c6;作者介绍&#xff1a;【孤寒者】—CSDN全栈领域优质创作者、HDZ核心组成员、华为云享专家Python全栈领域博主、CSDN原力计划作者 &#x1f525;&#x1f525;本文已收录于Flask框架从入门到实战专栏&#xff1a;《Flask框架从入…

AlphaGo 背后的人工智能:机器学习和神经网络

文章目录 一、说明二、背景三、围棋游戏四、AlphaGo 算法五、神经网络六、AlphaGo 的未来七、人工智能的未来八、结论 一、说明 棋盘游戏围棋被视为人工智能最具挑战性的任务之一&#xff0c;因为它“复杂、基于模式且难以编程”。计算机程序 AlphaGo 战胜李世石成为人工智能和…

Redis 五大数据类型底层原理

0、前言 本文涉及的主题&#xff1a; redis 对象存储 底层数据结构&#xff1a;int、embstr、raw、ziplist、listpack、quicklist、skiplist、intset、hashtable redis 数据类型&#xff1a;string、list、set、zset、hash 1、对象存储、底层编码、数据类型 1.1 对象存储…