- 基于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邻域)
};
**
实现效果
**
**
注意模板的手势图片一定要是这样的黑白图片
**
这是我自己补充的流程图