精华内容
下载资源
问答
  • 基于opencv 手势识别-仅仅是手势识别
    2021-10-17 16:31:40
    import cv2
    import matplotlib.pyplot as plt
    import numpy as np
    import time
    import os
    import mediapipe as mp
    
    mphands = mp.solutions.hands
    mpdraw = mp.solutions.drawing_utils
    
    wcap , hcap = 640 , 480    #定义摄像头的宽  和   高
    cap = cv2.VideoCapture(0)
    cap.set(3,wcap)
    cap.set(4,hcap)
    pTime = 0
    while True:
        ret , img = cap.read()
        imgRGB = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)    #转换rgb为bgr  bgr mediapipe才可识别
        with mphands.Hands(min_detection_confidence=0.65,min_tracking_confidence=0.65) as hands:
            result = hands.process(imgRGB)
            # print(result.multi_hand_landmarks)
            if result.multi_hand_landmarks:
                for hand_landmarks in result.multi_hand_landmarks:
                    mpdraw.draw_landmarks(img,hand_landmarks,mphands.HAND_CONNECTIONS)
    
        cTime = time.time()
        fps = 1/(cTime - pTime)
        pTime = cTime
        cv2.putText(img,f'fps:{int(fps)}',(20,45),cv2.FONT_HERSHEY_COMPLEX,1,(255,0,0),2,8,0)
        cv2.imshow("image",img)
        if cv2.waitKey(1) == 27:
            break
    cap.release()        #释放摄像头
    cv2.destroyWindow()   #窗口关闭

    人丑不好意思哈哈哈哈 

    更多相关内容
  • 基于 Opencv手势识别系统分析 指导老师:鲁晓军 作者:毛晓洁 日期:2013.10.18 基于 Opencv手势识别系统分析 系统研究背景 手势识别流程分析 系统总体结构 系统的设计与实现 总结 1.系统研究背景及应用 人机交互在...
  • 我是用的opencv自己获取的自己手势的数据集、测试集。 第二部分-dataset自定义数据集以及训练: 采用tensorflow 的dataset模块创建自己数据集和tensorflow.keras api实现模型的构建以及训练. 第三部分-模型预测与应用
  • 基于 Opencv手势识别系统分析 指导老师:鲁晓军 作者:毛晓洁 日期:2013.10.18 基于 Opencv手势识别系统分析 系统研究背景 手势识别流程分析 系统总体结构 系统的设计与实现 总结 1.系统研究背景及应用 人机交互在...
  • 基于Opencv手势识别

    2018-04-27 16:30:38
    基于Opencv凸包检测的手势识别,使用训练好的XML文件,可以识别0,1,2,3,4,5,6,8等手势。
  • #资源达人分享计划#
  • 一个实现石头剪刀布的小程序,基于opencv,在vs2010和opencv2.4.4下可以完美运行,另一个是一个根据手势播放音频的,还有一个提供参考
  • opencv手势识别与匹配

    2015-04-26 19:40:16
    一种基于opencv模式识别的手势识别,能够识别手指个数,
  • 基于opencv手势识别

    2020-11-13 19:30:17
    基于opencv手势识别,可以在一个窗口上显示手势和识别到底是哪个手势。 用C语言实现的。 基于opencv手势识别,可以在一个窗口上显示手势和识别到底是哪个手势。 用C语言实现的。
  • 手势识别 基于OpenCV的数字手势识别算法
  • 基于Python的OpenCV手势识别程序代码+文档+演示视频,利用图像数据处理,用Python编写的手势识别小游戏
  • 基于OpenCV手势识别完整项目(Python3.7)

    万次阅读 多人点赞 2019-08-09 08:52:41
    这是我的本科毕设题目,刚开始接触机器学习这方面,感谢CSDN和GitHub上的大佬,...项目简介:基于Win10 + Python3.7的环境,利用Python的OpenCV、Sklearn和PyQt5等库搭建了一个较为完整的手势识别系统,用于识别日...

    这是我的本科毕设题目,刚开始接触机器学习这方面,感谢CSDN和GitHub上的大佬,网上类似项目很多,方法也有很多,自己顺带进行了整理,边做毕设边分享一下自己学习心得吧,也算是梳理一下所学知识,各大佬有什么好的建议还请指出,不吝赐教。

    项目简介:基于Win10 + Python3.7的环境,利用Python的OpenCV、Sklearn和PyQt5等库搭建了一个较为完整的手势识别系统,用于识别日常生活中1-10的静态手势。

    整个项目的资源:https://download.csdn.net/download/qq_41562704/11471042(包含手势库和已训练的模型,可以直接运行使用)

    环境:Win10 + Python3.7 + OpenCV3.4.5,各个库的安装就不多说了

    最终的效果图如图所示:

     

    整个项目分为四个部分,即预处理,特征提取,模型训练,界面设计

    预处理

    1.获取手势

    2.图像预处理

    2.1去噪

    2.2 肤色检测 + 二值化处理

    2.3 形态学处理

    2.4 轮廓提取

    特征提取

    3 傅里叶算子提取

    4 建立特征库

    4.1 数据增强

    4.2 计算手势库的特征

    模型训练

    5 训练SVM模型

    界面设计

    6 PyQt设计界面


    预处理

    这部分需要完成摄像头录制手势后,提取出手的轮廓线

    这部分参考资料:

    https://blog.csdn.net/ifruoxi/article/details/78091954(获取手势,基于Python)

    https://blog.csdn.net/qq_22527639/article/details/81501565(肤色检测:方法全,理论介绍的也很全面,基于C++)

    https://blog.csdn.net/shadow_guo/article/details/43602051(基于RGB空间肤色检测,基于Python)

    https://blog.csdn.net/weixin_40893939/article/details/84527037(基于HSV空间和YCrCb空间肤色检测,基于Python)

    https://blog.csdn.net/Eastmount/article/details/83581277(腐蚀膨胀理论介绍,基于Python)

    https://blog.csdn.net/dz4543/article/details/80655067(轮廓提取,基于Python)

    1.获取手势

    主要是调用OpenCV,创建main.py和 picture.py

    main.py 当前负责录像,picture负责处理图像

    main.py

    import cv2
    import picture as pic
    
    font = cv2.FONT_HERSHEY_SIMPLEX #设置字体
    size = 0.5 #设置大小
    
    width, height = 300, 300 #设置拍摄窗口大小
    x0,y0 = 300, 100 #设置选取位置
    
    cap = cv2.VideoCapture(0) #开摄像头
    
    if __name__ == "__main__":
    	while(1):
    		ret, frame = cap.read() #读取摄像头的内容
    		frame = cv2.flip(frame, 2)
    		roi = pic.binaryMask(frame, x0, y0, width, height) #取手势所在框图并进行处理
    		key = cv2.waitKey(1) & 0xFF#按键判断并进行一定的调整
    		#按'j''l''u''j'分别将选框左移,右移,上移,下移
    		#按'q'键退出录像
    		if key == ord('i'):
    			y0 += 5
    		elif key == ord('k'):
    			y0 -= 5
    		elif key == ord('l'):
    			x0 += 5
    		elif key == ord('j'):
    			x0 -= 5
    		if key == ord('q'):
    			break
    		cv2.imshow('frame', frame) #播放摄像头的内容
    	cap.release()
    	cv2.destroyAllWindows() #关闭所有窗口
    

    2.图像预处理

    预处理在picture.py中完成。

    预处理的主要步骤为:去噪 -> 肤色检测 -> 二值化 -> 形态学处理 -> 轮廓提取,其中最麻烦的两项为肤色检测和轮廓提取。

    2.1去噪

    即滤波,主要是为了实现对图像噪声的消除,增强图像的效果,其实个人感觉这里滤波的作用不是很明显,也可以选择不滤波,在肤色检测后会有二次滤波。

    #以3*3的模板进行均值滤波
    blur = cv2.blur(roi, (3,3))
    #以3*3的模板进行高斯滤波,最后一个参数表示x与y方向的标准差,给0的话,函数会自己运算
    blur = cv2.GaussianBlur(roi, (3,3), 0)
    #中值滤波
    blur = cv2.medianBlur(roi,5)
    #双边滤波,9为区域的直径,后面两个参数是空间高斯函数标准差和灰度值相似性高斯函数标准差
    blur = cv2.bilateralFilter(img,9,75,75)

    均值滤波器、高斯滤波器、中值滤波器、双边滤波器都可以进行使用。推荐使用双边滤波器,该滤波器考虑了图像的空间关系,也考虑图像的灰度关系。双边滤波同时使用了空间高斯权重和灰度相似性高斯权重,确保了边界不会被模糊掉。不过我在处理中直接省去了去噪这个过程。

    2.2 肤色检测 + 二值化处理

    picture.py

    方法一:基于RGB颜色空间

    判断条件:

    在均匀光照下,R>95 AND G>40 B>20 AND MAX(R,G,B)-MIN(R,G,B)>15 AND ABS(R-G)>15 AND R>G AND R>B;

    在侧光拍摄环境下,R>220 AND G>210 AND B>170 AND ABS(R-G)<=15 AND R>B AND G>B

    import cv2
    import numpy as np
    
    def binaryMask(frame, x0, y0, width, height):
    	cv2.rectangle(frame,(x0,y0),(x0+width, y0+height),(0,255,0)) #画出截取的手势框图
    	roi = frame[y0:y0+height, x0:x0+width] #获取手势框图
    	cv2.imshow("roi", roi) #显示手势框图
    	res = skinMask(roi) #进行肤色检测
    	cv2.imshow("res", res) #显示肤色检测后的图像
    	return res
    
    ##########方法一###################
    ##########BGR空间的手势识别#########
    def skinMask(roi):
    	rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) #转换到RGB空间
    	(R,G,B) = cv2.split(rgb) #获取图像每个像素点的RGB的值,即将一个二维矩阵拆成三个二维矩阵
    	skin = np.zeros(R.shape, dtype = np.uint8) #掩膜
    	(x,y) = R.shape #获取图像的像素点的坐标范围
    	for i in range(0, x):
    		for j in range(0, y):
    			#判断条件,不在肤色范围内则将掩膜设为黑色,即255
    			if (abs(R[i][j] - G[i][j]) > 15) and (R[i][j] > G[i][j]) and (R[i][j] > B[i][j]):
    				if (R[i][j] > 95) and (G[i][j] > 40) and (B[i][j] > 20) \
    						and (max(R[i][j],G[i][j],B[i][j]) - min(R[i][j],G[i][j],B[i][j]) > 15):
    					skin[i][j] = 255
    				elif (R[i][j] > 220) and (G[i][j] > 210) and (B[i][j] > 170):
    					skin[i][j] = 255
    	res = cv2.bitwise_and(roi,roi, mask = skin) #图像与运算
    	return res

    效果图:

    方法二:基于HSV颜色空间

    判断条件:0<=H<=20,S>=48,V>=50

    肤色检测的方式不同影响的是skinMask,之后的代码只是修改skinMask函数,picture.py中其他代码不需要改动。

    ##########方法二###################
    ########HSV颜色空间H范围筛选法######
    def skinMask(roi):
    	low = np.array([0, 48, 50]) #最低阈值
    	high = np.array([20, 255, 255]) #最高阈值
    	hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) #转换到HSV空间
    	mask = cv2.inRange(hsv,low,high) #掩膜,不在范围内的设为255
    	res = cv2.bitwise_and(roi,roi, mask = mask) #图像与运算
    	return res

    效果图:

    方法三:椭圆肤色检测模型

    在YCrCb空间,肤色像素点会聚集到一个椭圆区域。先定义一个椭圆模型,然后将每个RGB像素点转换到YCrCb空间比对是否在椭圆区域,是的话判断为皮肤。

    ##########方法三###################
    #########椭圆肤色检测模型##########
    def skinMask(roi):
    	skinCrCbHist = np.zeros((256,256), dtype= np.uint8)
    	cv2.ellipse(skinCrCbHist, (113,155),(23,25), 43, 0, 360, (255,255,255), -1) #绘制椭圆弧线
    	YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB) #转换至YCrCb空间
    	(y,Cr,Cb) = cv2.split(YCrCb) #拆分出Y,Cr,Cb值
    	skin = np.zeros(Cr.shape, dtype = np.uint8) #掩膜
    	(x,y) = Cr.shape
    	for i in range(0, x):
    		for j in range(0, y):
    			if skinCrCbHist [Cr[i][j], Cb[i][j]] > 0: #若不在椭圆区间中
    				skin[i][j] = 255
    	res = cv2.bitwise_and(roi,roi, mask = skin)
    	return res

    效果图:

     

    方法四:YCrCb颜色空间的Cr分量+Otsu法阈值分割算法

    针对YCrCb中Cr分量的处理,对CR通道单独进行Otsu处理,Otsu方法opencv里用threshold,Otsu算法是对图像的灰度级进行聚类。

    ################方法四####################
    ####YCrCb颜色空间的Cr分量+Otsu法阈值分割算法
    def skinMask(roi):
    	YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB) #转换至YCrCb空间
    	(y,cr,cb) = cv2.split(YCrCb) #拆分出Y,Cr,Cb值
    	cr1 = cv2.GaussianBlur(cr, (5,5), 0)
    	_, skin = cv2.threshold(cr1, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) #Ostu处理
    	res = cv2.bitwise_and(roi,roi, mask = skin)
    	return res

    效果图:

    方法五:Cr,Cb范围筛选法

    该方法与方法一、二类似,不同的只是颜色空间不相同

    判断条件:133<=Cr<=173 77<=Cb<=127

    ##########方法五###################
    ########Cr,Cb范围筛选法###########
    def skinMask(roi):
    	YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB) #转换至YCrCb空间
    	(y,cr,cb) = cv2.split(YCrCb) #拆分出Y,Cr,Cb值
    	skin = np.zeros(cr.shape, dtype = np.uint8)
    	(x,y) = cr.shape
    	for i in range(0, x):
    		for j in range(0, y):
    			#每个像素点进行判断
    			if(cr[i][j] > 130) and (cr[i][j] < 175) and (cb[i][j] > 77) and (cb[i][j] < 127):
    				skin[i][j] = 255
    	res = cv2.bitwise_and(roi,roi, mask = skin)
    	return res

    效果图:

    方法六:OpenCV自带AdaptiveSkinDetector

    关于该函数的使用可以参考http://www.cnblogs.com/tornadomeet/archive/2012/11/20/2778740.html

    最终方案选择:在几种方式中选择效果比较好的,RGB和HSV的效果一般,而且曝光的话,效果更差,YCrCb是一个单独把亮度分离开来的颜色模型,使用这个颜色模型的话,像肤色不会受到光线亮度而发生改变,方法三和四均可。

    2.3 形态学处理

    即便是比较好的肤色检测算法,分割出来的手势,也难免有黑点,或者背景有白点,这时候需要对分割出来的手势图进行进一步处理,主要是腐蚀膨胀两个操作。

    腐蚀和膨胀是针对白色部分(高亮部分而言)。从数学角度来说,膨胀或者腐蚀操作就是将图像(或图像的一部分区域,称之为A)与核(称之为B)进行卷积。 
    膨胀就是求局部最大值操作,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素,这样就会使图像中的高亮区域逐渐增长。 
    腐蚀就是求局部最小值操作,即计算核B覆盖的区域的像素点的最小值,并把这个最小值赋值给参考点指定的像素,这样就会使图像中的高亮区域逐渐减少。 

    开运算:先腐蚀后膨胀,去除孤立的小点,毛刺

    闭运算:先膨胀后腐蚀,填平小孔,弥合小裂缝

    在binaryMask函数中return前面添加以下代码,进行开运算

    kernel = np.ones((3,3), np.uint8) #设置卷积核
    erosion = cv2.erode(res, kernel) #腐蚀操作
    cv2.imshow("erosion",erosion)
    dilation = cv2.dilate(erosion, kernel)#膨胀操作
    cv2.imshow("dilation",dilation)

    效果如图:

    可以看到背景杂质点去掉了

    2.4 轮廓提取

    在binaryMask函数中return前面添加以下代码,对肤色检测后的图像提取手势区域

    binaryimg = cv2.Canny(res, 50, 200) #二值化,canny检测
    h = cv2.findContours(binaryimg,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE) #寻找轮廓
    contours = h[1] #提取轮廓
    ret = np.ones(res.shape, np.uint8) #创建黑色幕布
    cv2.drawContours(ret,contours,-1,(255,255,255),1) #绘制白色轮廓
    cv2.imshow("ret", ret)

    特征提取

    这部分主要任务是对第一部分提取的轮廓点坐标提取出他们的傅里叶描述子,建立手势特征库

    参考资料:

    https://github.com/timfeirg/Fourier-Descriptors(提取特征代码比较完整)

    https://github.com/alessandroferrari/elliptic-fourier-descriptors(椭圆傅里叶描述子的提取)

    https://www.cnblogs.com/edie0902/p/3658174.html(傅里叶算子的数学思想)

    3 傅里叶算子提取

    将picture.py中的提取轮廓点部分删去,添加

    import fourierDescriptor as fd
    ret, fourier_result = fd.fourierDesciptor(res)

    创建fourierDescriptor.py

    在这个文件中完成对轮廓点坐标的傅里叶描述子的提取,具体代码如下:

    import cv2
    import numpy as np
    
    MIN_DESCRIPTOR = 32  # surprisingly enough, 2 descriptors are already enough
    
    ##计算傅里叶描述子
    def fourierDesciptor(res):
        #Laplacian算子进行八邻域检测
        gray = cv2.cvtColor(res, cv2.COLOR_BGR2GRAY)
        dst = cv2.Laplacian(gray, cv2.CV_16S, ksize = 3)
        Laplacian = cv2.convertScaleAbs(dst)
        contour = find_contours(Laplacian)#提取轮廓点坐标
        contour_array = contour[0][:, 0, :]#注意这里只保留区域面积最大的轮廓点坐标
        ret_np = np.ones(dst.shape, np.uint8) #创建黑色幕布
        ret = cv2.drawContours(ret_np,contour[0],-1,(255,255,255),1) #绘制白色轮廓
        contours_complex = np.empty(contour_array.shape[:-1], dtype=complex)
        contours_complex.real = contour_array[:,0]#横坐标作为实数部分
        contours_complex.imag = contour_array[:,1]#纵坐标作为虚数部分
        fourier_result = np.fft.fft(contours_complex)#进行傅里叶变换
        #fourier_result = np.fft.fftshift(fourier_result)
        descirptor_in_use = truncate_descriptor(fourier_result)#截短傅里叶描述子
        #reconstruct(ret, descirptor_in_use)
        return ret, descirptor_in_use
    
    def find_contours(Laplacian):
        #binaryimg = cv2.Canny(res, 50, 200) #二值化,canny检测
        h = cv2.findContours(Laplacian,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE) #寻找轮廓
        contour = h[1]
        contour = sorted(contour, key = cv2.contourArea, reverse=True)#对一系列轮廓点坐标按它们围成的区域面积进行排序
        return contour
    
    #截短傅里叶描述子
    def truncate_descriptor(fourier_result):
        descriptors_in_use = np.fft.fftshift(fourier_result)
        
        #取中间的MIN_DESCRIPTOR项描述子
        center_index = int(len(descriptors_in_use) / 2)
        low, high = center_index - int(MIN_DESCRIPTOR / 2), center_index + int(MIN_DESCRIPTOR / 2)
        descriptors_in_use = descriptors_in_use[low:high]
        
        descriptors_in_use = np.fft.ifftshift(descriptors_in_use)
        return descriptors_in_use
    
    ##由傅里叶描述子重建轮廓图
    def reconstruct(img, descirptor_in_use):
        #descirptor_in_use = truncate_descriptor(fourier_result, degree)
        #descirptor_in_use = np.fft.ifftshift(fourier_result)
        #descirptor_in_use = truncate_descriptor(fourier_result)
        #print(descirptor_in_use)
        contour_reconstruct = np.fft.ifft(descirptor_in_use)
        contour_reconstruct = np.array([contour_reconstruct.real,
                                        contour_reconstruct.imag])
        contour_reconstruct = np.transpose(contour_reconstruct)
        contour_reconstruct = np.expand_dims(contour_reconstruct, axis = 1)
        if contour_reconstruct.min() < 0:
            contour_reconstruct -= contour_reconstruct.min()
        contour_reconstruct *= img.shape[0] / contour_reconstruct.max()
        contour_reconstruct = contour_reconstruct.astype(np.int32, copy = False)
    
        black_np = np.ones(img.shape, np.uint8) #创建黑色幕布
        black = cv2.drawContours(black_np,contour_reconstruct,-1,(255,255,255),1) #绘制白色轮廓
        cv2.imshow("contour_reconstruct", black)
        #cv2.imwrite('recover.png',black)
        return black

    这里需要注意:

    轮廓提取后进行了二次去噪,即只保留区域面积最大的曲线,效果如图

    其次关于利用傅里叶算子重建轮廓图,在实际使用过程中并不需要,仅仅为了在测试阶段检验效果

    取32项傅里叶算子的时候,重建效果如下,基本可以还原手势形状。

    4 建立特征库

    这个部分的任务是采集手势1-10,同时利用旋转平移等操作对得到的手势库进行扩充。然后对整个手势库中的每张照片中的手势轮廓线计算傅里叶描述子并保存。我采取的方案是每个手势采集20份,然后扩充为200份,这里的数目可以自己调节。保存格式为"x_i",表示手势_x的第i张图片

    4.1 数据增强

    在数据增强前,先采集手势,在项目文件夹中创建一个“image”文件夹保存样本库,"test_image"保存测试库(看需求,可以不用)

    创建data_augmention.py

    对测试库进行操作的时候仅仅需要修改一下path及相关的数字

    import random
    import cv2
    path = './' + 'image' + '/'
    
    #旋转
    def rotate(image, scale=0.9):
        angle = random.randrange(-90, 90)#随机角度
        w = image.shape[1]
        h = image.shape[0]
        #rotate matrix
        M = cv2.getRotationMatrix2D((w/2,h/2), angle, scale)
        #rotate
        image = cv2.warpAffine(image,M,(w,h))
        return image
    
    if __name__ == "__main__":
        for i in range(5, 6):
    
            cnt = 21#计数
            for j in range(1, 21):
                roi = cv2.imread(path + str(i) + '_' + str(j)+'.png')
                for k in range(12):
                    img_rotation = rotate(roi)#旋转
                    cv2.imwrite(path + str(i) + '_' + str(cnt)+ '.png',img_rotation)
                    cnt += 1
                    img_flip = cv2.flip(img_rotation,1)#翻转
                    cv2.imwrite(path + str(i) + '_' + str(cnt)+ '.png',img_flip)
                    cnt += 1
                print(i,'_',j,'完成')
    

    4.2 计算手势库的特征

    创建loadData.py文件

    import fourierDescriptor as fd
    import cv2
    import numpy as np
    
    path = './' + 'feature' + '/'
    path_img = './' + 'image' + '/'
    
    if __name__ == "__main__":
        for i in range(1, 11):
            for j in range(1, 201):
                roi = cv2.imread(path_img + str(i) + '_' + str(j) + '.png')
    
                descirptor_in_use = abs(fd.fourierDesciptor(roi))
    
                fd_name = path + str(i) + '_' + str(j) + '.txt'
                # fd_name = path + str(i) + '.txt'
                with open(fd_name, 'w', encoding='utf-8') as f:
                    temp = descirptor_in_use[1]
                    for k in range(1, len(descirptor_in_use)):
                        x_record = int(100 * descirptor_in_use[k] / temp)
                        f.write(str(x_record))
                        f.write(' ')
                    f.write('\n')
                print(i, '_', j, '完成')

    对手势库中10个手势的200份图片一一进行操作,保存的特征的格式与图片格式一致,用txt文件进行保存

    模型训练

    这个部分的主要任务是利用已有的样本库训练SVM模型并保存

    这部分参考资料:

    https://cuijiahua.com/blog/2017/11/ml_8_svm_1.html(SVM的原理)

    https://cuijiahua.com/blog/2017/11/ml_9_svm_2.html(sklearn的使用)

    https://blog.csdn.net/qysh123/article/details/80063447(SVM调参)

    5 训练SVM模型

    使用网格搜索法进行调参,利用joblib模块保存模型

    import numpy as np
    from os import listdir
    from sklearn.externals import joblib
    from functools import reduce
    from sklearn.svm import SVC
    from sklearn.model_selection import GridSearchCV
    import matplotlib.pyplot as plt
    
    path = './' + 'feature' + '/'
    model_path = "./model/"
    test_path = "./test_feature/"
    
    test_accuracy = []
    
    #读txt文件并将每个文件的描述子改为一维的矩阵存储
    def txtToVector(filename, N):
    	returnVec = np.zeros((1,N))
    	fr = open(filename)
    	lineStr = fr.readline()
    	lineStr = lineStr.split(' ')
    	for i in range(N):
    		returnVec[0, i] = int(lineStr[i])
    	return returnVec
    
    def tran_SVM(N):
    	svc = SVC()
    	parameters = {'kernel':('linear', 'rbf'),
    	              'C':[1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
    	              'gamma':[0.00001, 0.0001, 0.001, 0.1, 1, 10, 100, 1000]}#预设置一些参数值
    	hwLabels = []#存放类别标签
    	trainingFileList = listdir(path)
    	m = len(trainingFileList)
    	trainingMat = np.zeros((m,N))
    	for i in range(m):
    		fileNameStr = trainingFileList[i]
    		classNumber = int(fileNameStr.split('_')[0])
    		hwLabels.append(classNumber)
    		trainingMat[i,:] = txtToVector(path+fileNameStr,N)#将训练集改为矩阵格式
    	print("数据加载完成")
    	clf = GridSearchCV(svc, parameters, cv=5, n_jobs=8)#网格搜索法,设置5-折交叉验证
    	clf.fit(trainingMat,hwLabels)
    	print(clf.return_train_score)
    	print(clf.best_params_)#打印出最好的结果
    	best_model = clf.best_estimator_
    	print("SVM Model save...")
    	save_path = model_path + "svm_efd_" + "train_model.m"
    	joblib.dump(best_model,save_path)#保存最好的模型
    
    def test_SVM(clf,N):
    	testFileList = listdir(test_path)
    	errorCount = 0#记录错误个数
    	mTest = len(testFileList)
    	for i in range(mTest):
    		fileNameStr = testFileList[i]
    		classNum = int(fileNameStr.split('_')[0])
    		vectorTest = txtToVector(test_path+fileNameStr,N)
    		valTest = clf.predict(vectorTest)
    		#print("分类返回结果为%d\t真实结果为%d" % (valTest, classNum))
    		if valTest != classNum:
    			errorCount += 1
    	print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount/mTest * 100))
    
    
    ####训练 + 验证#####
    if __name__ == "__main__":
    	tran_SVM(31)
    	clf = joblib.load(model_path + "svm_efd_" + "train_model.m")
    	test_SVM(clf,31)
    

    训练结果如图:

    界面设计

    字面意思,这部分的任务就是设计一个界面可以实时调用已经训练好的模型预测手势

    小声嘀咕一句:原来Python有这么好用的写界面的库呀(。-ω-)zzz

    这个部分也不是必须的,只不过为了稍微好看那么一点,可以直接修改main.py,使得按p的时候就进行预测

    import classfier as cf
    ......
    		elif key == ord('p'):
    			descirptor_in_use = abs(fourier_result)
    			fd_test = np.zeros((1,31))
    			temp = descirptor_in_use[1]
    			for k in range(1,len(descirptor_in_use)):
    				fd_test[0,k-1] = int(100 * descirptor_in_use[k] / temp)
    			test_svm = cf.test_fd(fd_test)
    			print("test_svm =",test_svm)
    			test_svm_efd = cf.test_efd(efd_test)
    			print("test_svm_efd =",test_svm_efd)
    		cv2.imshow('frame', frame) #播放摄像头的内容

    这部分参考资料:

    https://blog.csdn.net/niuyongjie/article/details/81161559(PyQt安装,我安装的是5.11.3版本)

    http://code.py40.com/pyqt5/16.html(PyQt教程)

    6 PyQt设计界面

    创建myGUI.py文件

    第一次用PyQt这个库,所以尺寸方面的控制用来最古老的方式(数字控制)还请大家见谅(╥╯^╰╥)

    import sys
    from PyQt5.QtWidgets import QApplication, QWidget, QToolTip, \
         QPushButton,QMessageBox,QDesktopWidget, QLabel
    from PyQt5.QtGui import QFont,QIcon,QPixmap,QImage
    from PyQt5.QtCore import QTimer
    import cv2
    import picture as pic
    import classify as cf
    import numpy as np
    
    
    class myWindow(QWidget):
        def __init__(self,parent = None):
            super(myWindow,self).__init__(parent)
    
            self.timer_camera = QTimer()
            self.cap = cv2.VideoCapture()
            self.initUI()
            self.slot_init()
    
    
        def initUI(self):
    
            self.mylabel()
            self.myButton()
            self.myLabelPic()
    
            self.setFixedSize(670,520)
            self.center()
            self.setWindowIcon(QIcon('icon.jpg'))
            self.setWindowTitle('gesture recognition')
    
    
        def mylabel(self):
    
            label_roi = QLabel('原图',self)
            label_roi.setStyleSheet("QLabel{font-size:18px;}")
            label_roi.resize(60,30)
            label_roi.move(120,15)
    
            label_res = QLabel('轮廓线', self)
            label_res.setStyleSheet("QLabel{font-size:18px;}")
            label_res.resize(60, 30)
            label_res.move(480, 15)
    
            label_pre = QLabel('预测', self)
            label_pre.setStyleSheet("QLabel{font-size:20px;}")
            label_pre.resize(50,30)
            label_pre.move(400,400)
    
            label_result = QLabel('结果', self)
            label_result.setStyleSheet("QLabel{font-size:20px;}")
            label_result.resize(50, 30)
            label_result.move(400,430)
    
        def myLabelPic(self):
            self.label_show_roi = QLabel(self)
            self.label_show_roi.setFixedSize(301,301)
            self.label_show_roi.move(20,50)
            self.label_show_roi.setStyleSheet("QLabel{background:white;}")
            self.label_show_roi.setAutoFillBackground(True)
    
            self.label_show_ret = QLabel(self)
            self.label_show_ret.setFixedSize(301, 301)
            self.label_show_ret.move(350, 50)
            self.label_show_ret.setStyleSheet("QLabel{background:white;}")
            self.label_show_ret.setAutoFillBackground(True)
    
            self.label_show_recognition = QLabel('0',self)
            self.label_show_recognition.setStyleSheet("QLabel{background:white;}")
            self.label_show_recognition.setStyleSheet("QLabel{font-size:50px;}")
            self.label_show_recognition.setFixedSize(100,100)
            self.label_show_recognition.move(500, 380)
            self.label_show_recognition.setAutoFillBackground(True)
    
        def myButton(self):
            QToolTip.setFont(QFont('SansSerif', 10))
    
            self.button_open_camera = QPushButton('打开相机', self)
            self.button_open_camera.setToolTip('按i,k,j,l可以进行上下左右调整')
            self.button_open_camera.resize(100,30)
            self.button_open_camera.move(100, 400)
    
            self.butoon_recognition = QPushButton('开始预测', self)
            self.butoon_recognition.setFixedSize(100, 30)
            self.butoon_recognition.move(100, 450)
    
    
        def slot_init(self):
            self.button_open_camera.clicked.connect(self.button_open_camera_click)
            self.butoon_recognition.clicked.connect(self.button_recognition_click)
            self.timer_camera.timeout.connect(self.show_camera)
    
        def button_open_camera_click(self):
            if self.timer_camera.isActive() == False:
                self.cap.open(0)
                self.timer_camera.start(30)
                self.button_open_camera.setText(u'关闭相机')
            else:
                self.timer_camera.stop()
                self.cap.release()
                self.label_show_roi.clear()
                self.label_show_ret.clear()
                self.label_show_recognition.setText('0')
                self.button_open_camera.setText(u'打开相机')
    
        def button_recognition_click(self):
            descirptor_in_use = abs(self.fourier_result)
            fd_test = np.zeros((1, 31))
            temp = descirptor_in_use[1]
            for k in range(1, len(descirptor_in_use)):
                fd_test[0, k - 1] = int(100 * descirptor_in_use[k] / temp)
            efd_test = np.zeros((1, 15))
            for k in range(1, len(self.efd_result)):
                temp = np.sqrt(self.efd_result[k][0] ** 2 + self.efd_result[k][1] ** 2) + np.sqrt(
                    self.efd_result[k][2] ** 2 + self.efd_result[k][3] ** 2)
                efd_test[0, k - 1] = (int(1000 * temp))
            test_knn, test_svm = cf.test_fd(fd_test)
            print("test_knn =", test_knn)
            print("test_svm =", test_svm)
            test_knn_efd, test_svm_efd = cf.test_efd(efd_test)
            print("test_knn_efd =", test_knn_efd)
            print("test_svm_efd =", test_svm_efd)
            num = [0]*11
            num[test_knn[0]] += 1
            num[test_svm[0]] += 1
            num[test_knn_efd[0]] += 1
            num[test_svm_efd[0]] += 1
            res = 0
            for i in range(1, 11):
                if num[i] >= 2:
                    res = i
                    break
            print(res)
            self.label_show_recognition.setText(str(res))
    
    
        def show_camera(self):
            width, height = 300, 300  # 设置拍摄窗口大小
            x0, y0 = 300, 100  # 设置选取位置
            flag, frame = self.cap.read()
            roi, res, ret, self.fourier_result, self.efd_result = pic.binaryMask(frame, x0, y0, width, height)
            roi = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
            show_roi = QImage(roi.data, roi.shape[1], roi.shape[0], QImage.Format_RGB888)
            show_ret = QImage(ret.data, ret.shape[1], ret.shape[0], QImage.Format_Grayscale8)
            self.label_show_roi.setPixmap(QPixmap.fromImage(show_roi))
            self.label_show_ret.setPixmap(QPixmap.fromImage(show_ret))
    
    
        def closeEvent(self, QCloseEvent):
            reply = QMessageBox.question(self, 'Message',"Are you sure to quit?",
                                         QMessageBox.Yes |QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                if self.cap.isOpened():
                    self.cap.release()
                if self.timer_camera.isActive():
                    self.timer_camera.stop()
                QCloseEvent.accept()
            else:
                QCloseEvent.ignore()
    
        def center(self):
            qr = self.frameGeometry()
            cp = QDesktopWidget().availableGeometry().center()
            qr.moveCenter(cp)
            self.move(qr.topLeft())
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        win = myWindow()
        win.show()
        sys.exit(app.exec())

    后面几个部分由于时间关系,讲得不如前面的详细,向大家表示歉意,一些细节部分建议大家下载我的源码来看。有问题的地方欢迎讨论~源码中另外也用了椭圆傅里叶描述子作为特征。

    展开全文
  • 基于OpenCV实现的手势识别,可识别出剪刀石头布.资源中包含实现代码思路,源代码以及效果图.
  • 基于OpenCV的简易实时手势识别1.基本信息介绍1.1实验步骤1.2效果展示2.肤色检测+二值化+开运算+高斯模糊2.1 flip()函数原型2.2cvtColor()函数原型2.3split()函数原型2.4GaussianBlur()函数原型2.5Code3.连通空心部分...

    1.基本信息介绍

    这是我大一寒假时写着玩的,非常简陋。基于凸包检测,所以实际上是计算指尖数量判断1~5的手势。又为1 ~3手势赋了控制鼠标操作的功能(但不能移动鼠标,而且因为手势识别不太准确所以这个功能实现得很废/doge)。(才疏学浅,希望有生之年能写个更好的
    版本信息:Visual Studio2015 OpenCV4.1.1
    语言:C/C++
    (至于为什么不用python,现在当事人也很后悔

    1.1实验步骤

    (1)图像捕获
    直接调用笔记本内置摄像头,使图像绕y轴对称翻转,得到内置(前置)摄像头所拍摄的视频画面的镜像画面,从而得到以操作者为第一视角的正向画面。

    (2)肤色检测
    先将图像由RGB空间转换至YCrCb空间。
    然后将图像成Y(像素的亮度)、Cb(红色分量与亮度的信号差值)、Cr(蓝色分量与亮度的信号差值)三个单通道图像。
    再提取Cb、Cr两通道的图像,进行数值判断,满足Cr>133 && Cr<173 && Cb>77 && Cb<127的点即判断为肤色部分。
    最后将完成肤色切割的图像进行二值化。

    (3)图像预处理
    本实验先使用开运算(即先腐蚀后膨胀)对二值化后的手掌图像进行处理,去除图中的小孤立点,消除较小连通域,保留较大连通域,在不明显改变较大连通域面积的同时平滑连通域的边界,是手掌轮廓更明显,为之后的漫水填充做准备。
    然后进行高斯滤波,从而消除图像上的高斯噪声。
    再通过漫水填充算法,将手掌中因光线角度等因素在肤色检测中缺失的部分填充。
    最后图像腐蚀处理漫水填充后的图像,将细小的噪声去除的同时,将图像主要区域的面积缩小。为之后的多边形拟合曲线求得图像近似轮廓做准备。

    (4)指尖检测
    本实验先用多边形逼近手部轮廓,求得近似轮廓。
    再使用凸包检测函数对手部轮廓进一步进行多边形逼近,进而获得一个凸多边形。找到重心位置,通过比较凸包的顶点与重心的y轴坐标,去除纵坐标小于重心纵坐标的顶点,保留纵坐标大于重心的凸包的顶点,再规定凸点间距离范围以消除由同一个指尖产生的多个凸包顶点,得到指尖数量。

    (5)模拟鼠标
    最后通过得到的指尖数量,控制鼠标操作。
    当指尖数量=1时,在图像重心处显示“Left”,同时执行鼠标左键单击功能。
    当指尖数量=2时,在图像重心处显示“Double click”,同时执行鼠标左键双击功能。
    当指尖数量=3时,在图像重心处显示“Right”,同时执行鼠标右键单击功能。

    1.2效果展示

    请添加图片描述

    #include<opencv2\opencv.hpp>
    #include<iostream>
    #include<vector>
    #include<algorithm>
    #include<math.h>
    #include<Windows.h>
    
    using namespace std;
    using namespace cv;
    
    /*介绍基本信息*/
    void Introduce()
    {
    	cout << "\n----------------------------------------------------------------------------";
    	cout << "\n功能:以手势代替鼠标进行左右键点击";
    	cout << "\n版本信息:Visual Studio2015	OpenCV4.1.1";
    	cout << "\n-------------------------------------指令集---------------------------------";
    	cout << "\n手势1:单击鼠标左键Left";
    	cout << "\n手势2:双击鼠标左键Double click";
    	cout << "\n手势3:单击鼠标右键Right";
    	cout << "\n----------------------------------------------------------------------------\n";
    }
    

    2.肤色检测+二值化+开运算+高斯模糊

    2.1 flip()函数原型

    本实验通过使图像绕y轴对称翻转,得到内置(前置)摄像头所拍摄的视频画面的镜像画面,从而得到以操作者为第一视角的正向画面。

    flip()函数原型
    flip(	InputArray		src,
    OutputArray	dst,
    Int				flipCode
    )
    

    ①src:输入图像。
    ②dst:输出图像,与src具有相同的大小、数据类型及通道数。
    ③flipCode:翻转方式标志。数值大于0表示绕y轴翻转;数值等于0表示绕x轴翻转;数值小于0,表示绕两个轴翻转。

    2.2cvtColor()函数原型

    本实验中肤色检测步骤如下:
    ①通过颜色模型转换函数cvtColor()函数将图像由RGB空间转换至YCrCb空间。
    ②通过多通道分离函数split()将图像成Y(像素的亮度)、Cb(红色分量与亮度的信号差值)、Cr(蓝色分量与亮度的信号差值)三个单通道图像。
    ③提取Cb、Cr两通道的图像,进行数值判断,满足Cr>133 && Cr<173 && Cb>77 && Cb<127的点即判断为肤色部分。
    ④将完成肤色切割的图像进行二值化。

    cvtColor()函数原型
    cvtColor(	InputArray		src,
    OutputArray	dst,
    int		code,
    int		dstCn	=0
    )
    

    ①src:待转换颜色模型的原始图像。
    ②dst:转换颜色模型后的目标图像。
    ③code:颜色空间转换的标志。本实验使用的标志参数为。
    ④dstCn:目标图像中的通道数。若参数为0,则从src和代码中自动导出通道数。本实验中使用默认参数。

    2.3split()函数原型

    split()函数原型
    split(	const		Mat& src,
    Mat *		mvbegin
    )
    split(	InputArray				m,
    OutputArrayOfArrays	mv
    )
    

    ①src:待分离的多通道图像。
    ②mvbegin:分离后的单通道图像,为数组形式,数组大小需要与图像的通道数一致。
    ③m:待分离的多通道图像。
    ④mv:分离后的单通道图像,为向量(vector)形式。

    2.4GaussianBlur()函数原型

    在图像采集的众多过程中都容易引用高斯噪声。高斯滤波器考虑了像素滤波器中心距离的影响,以滤波器中心位置为高斯分布的均值,根据高斯分布公式和每个像素离中心位置的距离计算出滤波器内每个位置的数值,从而形成一个高斯滤波器。在将高斯滤波器与图像之间进行滤波操作,进而实现对图像的高斯滤波。
    本实验使用GaussianBlur()函数进行高斯滤波。

    GaussianBlur()函数原型
    GaussianBlur(	InputArray		src,
    OutputArray	dst,
    Size			ksize,
    double			sigmaX,
    double			sigmaY=0,
    int				borderType=BORDER_DEFAULT(默认参数)
    )
    

    ①src:待高斯滤波的图像,图像的数据类型必须为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F,通道数目任意。
    ②dst:输出图像,与src尺寸、通道数、数据类型都相同。
    ③ksize:高斯滤波器的尺寸。滤波器必须是政奇数。如果尺寸为0,则由标准偏差计算尺寸。
    ④sigmaX:X轴方向的高斯滤波器标准偏差。
    ⑤sigmaY:Y轴方向的高斯滤波器标准偏差。如果输入量为0,则将其设置为等于sigmaX;如果两个轴的标准差都为0,则根据输入的高斯滤波器尺寸计算标准偏差。
    ⑥borderType:像素外推法选择标志。(边界外推方法标志见下表)

    方法标志参数简记作用
    BORDER_CONSTANT0用特定值填充
    BORDER_REPLICATE1两端复制填充
    BORDER_REFLECT2倒序填充
    BORDER_WRAP3正序填充
    BORDER_REFLECT_1014不包含边界值的倒序填充
    BORDER_TRANSPARENT5随机填充
    BORDER_REFLECT1014同BORDER_REFLECT_101
    BORDER_DEFAULT4同BORDER_DEFAULLT
    BORDER_ISOLATED16不关心感兴趣区域之外的部

    2.5Code

    /*基于YCrCb空间的肤色检测+二值化+开运算+高斯模糊*/
    Mat skin(Mat&ImageIn)
    {
    		Mat Image_y;
    		flip(ImageIn, Image_y, 1);//将图像沿y轴翻转,即镜像
    		namedWindow("前置摄像头", WINDOW_NORMAL | WINDOW_KEEPRATIO);imshow("前置摄像头", Image_y);
    
    		Mat Image = Image_y.clone();//用clone()函数复制图像
    		Mat YCrCb_Image;
    		cvtColor(Image, YCrCb_Image, COLOR_BGR2YCrCb);
    		vector<Mat>Y_Cr_Cb;
    		split(YCrCb_Image, Y_Cr_Cb);
    		Mat CR = Y_Cr_Cb[1];
    		Mat CB = Y_Cr_Cb[2];
    		Mat ImageOut = Mat::zeros(Image.size(), CV_8UC1);//zeros():构建一个全为0的矩阵,即创建一个全黑的图片
    
    		//Cr>133 && Cr<173 && Cb>77 && Cb<127
    		for (int i = 0; i < Image.rows; i++)
    		{
    			for (int j = 0; j < Image.cols; j++)
    			{
    				if (CR.at<uchar>(i, j) >= 133 && CR.at<uchar>(i, j) <= 173 && CB.at<uchar>(i, j) >= 77 && CB.at<uchar>(i, j) <= 127)
    				{
    					ImageOut.at<uchar>(i, j) = 255;
    				}
    			}
    		}
    
    		Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));//结构元素 表示内核为一个3*3的矩形
    		morphologyEx(ImageOut, ImageOut, MORPH_OPEN, kernel);//使用morphologyEx()函数进行开运算
    		GaussianBlur(ImageOut, ImageOut, Size(3, 3), 5);
    		
    		return ImageOut;
    }
    
    

    3.连通空心部分+腐蚀

    3.1 floodFill()函数原型

    漫水填充法是根据像素灰度值之间的差值寻找相同区域以实现分割。本实验通过floodFill()函数,将手掌中因光线角度等因素在肤色检测中缺失的部分填充。
    漫水填充法主要步骤如下:
    ①选择种子点。
    ②以种子为中心,判断4-领域或者8-领域的像素值与中子点像素值的差值,将差值小于阈值的像素点添加进区域内。
    ③将新加入的像素点作为新的种子点,反复执行第二步,直到没有新的像素点被添加进该区域为止。

    floodFill()函数原型
    floodFill(	InputOutputArray		image,
    InputOutputArray		mask,
    Point					seedPoint,
    Scalar					newVal,
    Rect					*rect=0,
    Scalar					loDiff = Scalar(),
    Scalar					upDiff = Scalar(),
    int						flags = 4
    )
    

    ①image:输入及输出图像,可以为CV_8U或CV_32F数据类型的单通道或三通道图像。
    ②mask:掩码矩阵,尺寸比输入图像宽和高各大2的单通道图像,用于标记漫水填充的区域
    ③seedPoint:种子点,可以为图像范围内任意一点。
    ④newVal:归入种子点区域内像素点的新像素值,该值会直接作用在原图中。
    ⑤rect:种子点漫水填充区域的最小矩形边界,默认值为0,表示不输出边界。
    ⑥loDiff:添加进种子点区域条件的下界差值,当邻域某像素点的像素值域与种子点像素值的差值大于该值时,该像素点被添加进种子所在的区域。
    ⑦upDiff:添加进种子点区域条件的上界差值,当种子点像素值与邻域某像素点的像素值的差值小于该值时,该像素点被添加进种子点所在的区域。
    ⑧flags:漫水填充法的操作标志,由3部分构成,分别表示邻域种类、掩码矩阵中被填充像素点的像素值和填充算法的规则,填充算法可选的标志如下表

    操作标志参数简记含义
    FLOODFILL_FIXED_RANGE1<<16如果设置该参数,那么仅考虑当前像素值与初始种子点像素之间的差值,否则考虑新种子点像素值与当前像素值之间的差异,即范围是否浮动的标志
    FLOODFILL_MASK_ONLY1<<17如果设置,那么该函数不会更改原始图像,即忽略第四个参数newVal,只生成掩码矩阵

    3.2 morphologyEx()函数原型

    本实验中使用开运算处理肤色检测处理后的图像,去除图中的噪声,消除较小连通域,保留较大连通域,并且能够在不明显改变较大连通域面积的同时平滑连通域的边界,为之后的漫水填充做准备。
    本实验中还使用了图像腐蚀处理漫水填充后的图像,将细小的噪声去除的同时,将图像主要区域的面积缩小。为之后的多边形拟合曲线求得图像近似轮廓做准备。
    OpenCV4中提供了图像腐蚀和膨胀运算不同组合形式的morphologyEx()函数。

    morphologyEx()函数原型
    MorphologyEx(	InputArray		src,
    OutoutArray	dst,
    int				op,
    InputArray		kernel,
    Point	anchor = point(-1,-1),
    int		iterations = 1,
    Int		borderType = BORDER_CONSTANT,
    Const Scalar & borderValue = morphologyDefaultBorderValue()
    )
    

    src:输入图像
    dst:形态学操作后的输出图像
    op:形态学操作类型的标志,可选择的标志及其函数如下表所示
    kernel:结构元素,可以自己生成,也可以用getStructuringElement()函数生成
    anchor:中心点在结构元素中的位置,默认参数为结构元素的集合中心点。
    iterations:处理的次数
    borderType:像素外推法选择标志
    borderValue:使用边界不变外推法时的边界值。

    形态学操作类型标志参数简记含义
    MORPH_ERODE0图像腐蚀
    MORPH_DILATE1图像膨胀
    MORPH_OPEN2开运算
    MORPH_CLOSE3闭运算
    MORPH_GRANDIENT4形态学梯度
    MORPH_TOPHAT5顶帽运算
    MORPH_BLACKHAT6黑帽运算
    MORPH_HITMISS7击中击不中运算

    3.3Code

    /*连通空心部分+腐蚀*/
    Mat Floodfill(Mat&Img_src)
    {
    	Size f_size = Img_src.size();
    	Mat image = Mat::zeros(f_size.height + 2, f_size.width + 2, CV_8UC1);
    	Img_src.copyTo(image(Range(1, f_size.height + 1), Range(1, f_size.width + 1)));
    	floodFill(image, Point(0, 0), Scalar(255));
    	Mat cutImg, Img_dst;
    	image(Range(1, f_size.height + 1), Range(1, f_size.width + 1)).copyTo(cutImg);
    	Img_dst = Img_src | (~cutImg);
    
    	Mat kernel1 = getStructuringElement(MORPH_RECT, Size(10, 10));//结构元素 表示内核为一个10*10的矩形
    	morphologyEx(Img_dst,Img_dst, MORPH_ERODE, kernel1);//使用morphologyEx()函数进行腐蚀运算
    
    	return Img_dst;
    }
    

    4.多边形拟合曲线

    4.1approxPolyDP()函数原型

    本实验通过approxPolyDP()函数对图像进行处理,用多边形逼近手部轮廓,求得近似轮廓,为之后的凸包检测做准备。

    approxPolyDP()函数原型
    approxPolyDP(	InputArray		curve,
    OutputArray	approxCurve,
    double 		epsilon,
    bool			closed
    )
    

    ①curve:输入轮廓像素点。
    ②approxCurve:多边形逼近结果,以多边形顶点坐标的形式给出。
    ③epsilon:逼近的精度,即原始曲线和逼近曲线之间的最大距离。
    ④closed:逼近曲线是否为封闭曲线的标志,true表示封闭。

    4.2Code

    /*计算两点间距离*/
    double distance(Point a, Point b)
    {
    	double distance = sqrt(abs((a.x - b.x)*(a.x - a.x) + (a.y - b.y)*(a.y - b.y)));
    	return distance;
    }
    
    /*将坐标点连接成封闭图形*/
    void draw(Mat Img1, Mat Img2)
    {
    	for (int i = 0;i < Img1.rows;i++)
    	{
    		if (i == Img1.rows - 1)
    		{
    			Vec2i point1 = Img1.at<Vec2i>(i);
    			Vec2i point2 = Img1.at<Vec2i>(0);
    			line(Img2, point1, point2, Scalar(255, 255, 255), 2, 8, 0);
    			break;
    		}
    		Vec2i point1 = Img1.at<Vec2i>(i);
    		Vec2i point2 = Img1.at<Vec2i>(i + 1);
    		line(Img2, point1, point2, Scalar(255, 255, 255), 5, 8, 0);
    	}
    }
    
    /*多边形拟合曲线绘制近似轮廓*/
    Mat approx(Mat&Img_src)
    {
    	Mat Img_dst = Mat::zeros(Img_src.size(), CV_8UC1);
    
    	vector<vector<Point>>contours;
    	vector<Vec4i>hierarchy;
    	findContours(Img_src, contours, hierarchy, 0, 2, Point());
    
    	for (int t = 0;t < contours.size();t++)
    	{
    		Mat app;
    		approxPolyDP(contours[t], app, 15, true);
    		draw(app, Img_dst);
    	}
    	return Img_dst;
    }
    

    5.凸包检测+重心+ 鼠标操作

    5.1convexHull()函数原型

    在图形学中,将二维平面上的点集最外层的点连接起来构成的凸多边形称为凸包。
    本实验通过用于物体凸包检测的convexHull()函数,对手部轮廓进一步进行多边形逼近,进而获得一个凸多边形。找到重心位置,通过比较凸包的顶点与重心的y轴坐标,得到纵坐标大于重心的凸包的顶点,再规定凸点间距离范围,得到指尖数量。

    convexHull()函数原型
    convexHull(	InputArray		points,
    OutputArray	hull,
    bool			clockwise = false,
    bool			returnPoints = true

    ①points:输入的二维点集或轮廓坐标,数据类型为vector或者Mat。
    ②hull:输出的凸包的顶点的坐标或者索引,数据类型为vector或者vector。
    ③clockwise:方向标志。当参数取值为true时,凸包顺序为顺时针方向;当参数取值为false时,凸包顺序为逆时针方向。
    ④returnPoints:输出数据的类型标志。当参数取值为true时,第二个参数输出的结果是凸包顶点的坐标,数据类型为vector;当参数取值为false时,第二个参数输出的结果是凸包顶点的索引,数据类型为vector。

    5.2moments()函数原型

    moments()函数原型
    moments(	InputArray		array,
    bool			binaryImage = false
    )
    

    ①array:计算矩的区域二维像素坐标集合或者单通道的CV_8U图像。
    ②binaryImage:是否将所有非零像素值视为1的标志,该标志只在第一个参数设置为图像类型的数据时才会起作用。
    moments()函数会返回一个Moments类的变量。Moments类中含有几何矩、中心矩及归一化的几何矩的数值属性。

    5.3Mouse_event()函数原型

    本实验通过mouse_event()函数来代替鼠标操作。

    Mouse_event()函数原型
    mouse_event(	DWORD		dwFlags,
    DWORD		dx,
    DWORD		dy,
    DWORD		dwData,
    ULONG_PTR	dwExtraInfo
    )
    

    ①dwFlags:标志位集,指定点集按钮和鼠标动作。
    ②dx:指定鼠标沿x轴的绝对位置或者从上次鼠标事件产生以来移动的数量。
    ③dy:指定鼠标沿y轴的绝对位置或者从上次鼠标事件产生以来移动的数量。
    ④dwData:如果dwFlags为MOUSEEVENT_WHEEL,则dwData指定鼠标轮移动的数量。正值表示鼠标轮向前移动,即远离用户的方向;负值表示鼠标轮向后移动,即朝向用户。如果dwFlags不是MOUSEEVENT_WHEEL,则dwData应为0。
    ⑤dwExtrafo:指定与鼠标事件相关的附加32位值。

    鼠标动作标志参数含义
    MOUSEEVENT_ABSOLUTEdx和dy参数含有规范化的绝对坐标。如果不设置,这些参数含有相对数据:相对于上次位置的改动位置
    MOUSEEVENT_MOVE鼠标移动
    MOUSEEVENT_LEFTDOWN鼠标左键按下
    MOUSEEVENT_LEFTUP鼠标左键松开
    MOUSEEVENT_RIGHTDOWN鼠标右键按下
    MOUSEEVENT_RIGHTUP鼠标右键松开
    MOUSEEVENT_MIDLEDOWN鼠标中键按下
    MOUSEEVENT_MIDLEUP鼠标中键松开
    MOUSEEVENT_WHEEL鼠标轮被滚动,如果鼠标有一个滚轮,滚轮数量由dwData给出

    5.5 Code

    /*凸包检测+重心+ 鼠标操作 */
    Mat CH(Mat&Image_src)
    {
    	/*轮廓*/
    	Mat ImageOut = approx(Image_src);
    	vector<vector<Point>>contours;
    	vector<Vec4i>hierarchy;
    	findContours(ImageOut, contours, hierarchy, 0, 2, Point());
    
    	/*画重心*/
    	Moments moment = moments(ImageOut, true);
    	Point center(moment.m10 / moment.m00, moment.m01 / moment.m00);
    	circle(ImageOut, center, 8, Scalar(255, 255, 255), -1);
    
    	int dist;
    	int sum = 0;
    	for (int t = 0;t < contours.size();t++)
    	{
    		/*凸包检测*/
    		vector<Point>hull;
    		convexHull(contours[t], hull);
    
    		for (size_t i = 0;i < hull.size();i++)
    		{
    			int a = hull.size();
    
    			if (i != hull.size() - 1)
    				dist = distance(hull[i], hull[i + 1]);
    			int dist1 = distance(hull[i], center);
    			if (hull[i].y < center.y&&dist>20)
    			{
    				circle(ImageOut, hull[i], 15, Scalar(255, 255, 255), 2, 8, 0);
    				sum += 1;
    			}
    
    			if (i == hull.size() - 1)
    			{
    				line(ImageOut, hull[i], hull[0], Scalar(255, 255, 255), 5, 8, 0);
    				break;
    			}
    			line(ImageOut, hull[i], hull[i + 1], Scalar(255, 255, 255), 5, 8, 0);
    		}
    	}
    	cout << sum << endl;
    
    	if (sum == 1)
    	{
    		mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
    		string str3 = "Left";
    		putText(ImageOut, str3, center, 0, 2, Scalar(255, 255, 255), 4, 8);
    		waitKey(0);
    
    	}
    	if (sum == 2)
    	{
    		mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
    		mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
    		string str1 = "Double click";
    		putText(ImageOut, str1, center, 0, 2, Scalar(255, 255, 255), 4, 8);
    	}
    	if (sum == 3)
    	{
    		mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
    		string str2 = "Right";
    		putText(ImageOut, str2, center, 0, 2, Scalar(255, 255, 255), 4, 8);
    		waitKey(0);
    	}
    	return	ImageOut;
    }
    

    6.主函数 摄像头调用

    6.1摄像头调用

    OpenCV中为读取视频文件和调用摄像头而设计了VideoCapture类。视频文件由专门的视频读取函数进行视频读取,并将每一帧图像保存到Mat类矩阵中。
    本实验通过VideoCapture类直接调用笔记本内置摄像头。

    VideoCapture类调用摄像头构造函数
    VideoCapture( int		index,
    Int		apiPreference
    )
    

    ①index:需要打开的摄像头设备的ID。
    ②airPreference:读取数据时设置的属性。

    6.2 Code

    int main()
    {
    	Introduce();
    	/*调用摄像头*/
    	VideoCapture capture(0);
    	/*检查是否成功打开*/
    	if (!capture.isOpened())
    	{
    		cout << "摄像头打开失败T_T";
    		return -1;
    	}
    
    	while (1) {
    		Mat In;
    		capture >> In;
    
    		Mat A = skin(In);
    		Mat B = Floodfill(A);
    		Mat Out = CH(B);
    
    		namedWindow("Result", WINDOW_NORMAL | WINDOW_KEEPRATIO);imshow("Result", Out);
    		waitKey(2);
    	}
    	return 0;
    }
    

    7.代码中的其他API函数

    7.1 getStructuringElement()函数原型

    getStructuringElement(	int			shape,
    Size		ksize,
    Point		anchor = Point(-1-1)
    

    ①shape:生成结构元素的种类,可选参数及其含义如表所示。
    ②ksize:结构元素的尺寸。
    ③anchor:中心点的位置,默认为结构元素的几何中心。
    标志参数 简记 作用
    MORPH_RECT 0 矩形结构元素,所有元素都为1
    MORPH_CROSS 1 十字结构元素,中间的列和行元素为1
    MORPH_ELLIPSE 2 椭圆结构元素,矩形的内接椭圆元素为1

    7.2 findContours()函数原型

    FindCountours(	InputArray					image,
    OutputArrayOfArrays		contours,
    OutputArray				hierarchy,
    int							mode,
    int							method,
    Point						offset = Point()

    ①image:输入图像,数据类型为CV_8U的单通道灰度图或二值化图像。
    ②contours:存放检测到的轮廓,每个轮廓中放着像素的坐标。数据类型为vector<vector>。
    ③hierarchy:存放各个轮廓之间的结构信息,数据类型为vector。
    ④mode:轮廓检测模式标志。
    ⑤method:轮廓逼近方法标志。
    ⑥offset:每个轮廓点移动的可选偏移量。

    轮廓检测模式标志参数简记含义
    RETR_EXTERNAL0只检测最外层轮廓
    RETR_LIST1提取所有轮廓,并放在list中。检测的轮廓不建立等级关系
    RETR_CCOMP2提取所有轮廓,并且将其组织为双层结构。顶层为连通域的外围边界,次层为孔的内层边界
    RETR_TREE3提取所有轮廓,并重新建立网状的轮廓结构
    轮廓逼近方法标志参数简记含义
    CHAIN_APPROX_NONE1获取每个轮廓的每个像素,相邻两个点的像素位置相差1
    CHAIN_APPROX_SIMPLE2压缩水平方向、垂直方向和对角线方向的元素,只保留该方向的终点坐标

    7.3 circle()函数原型

    circle(	InputOutputArray  	img,
    Point					center,
    int						radius,
    const					Scalar &color,
    int						thickness = 1,
    int						lineType = LINE_8,
    int						shift = 0

    ①img:需要绘制圆形的图像。
    ②center:圆形的圆心位置坐标。
    ③radius:圆形的半径,单位为像素。
    ④color:圆形的颜色。
    ⑤thickness:轮廓的宽度,如果数值为负,则绘制一个实心圆。
    ⑥lineType:边界类型。
    ⑦shift:中心坐标和半径数值中的小数位数。

    7.4 line()函数原型

    line(	InputOutputArray		img,
    Point					pt1,
    Point					pt2,
    const					scalar & color,
    int						thickness = 1,
    int						lineType = LINE_8,
    int						shift = 0

    ①pt1:直线起点在图像中的坐标。
    ②pt2:直线终点在图像中的坐标。
    ③color:直线的颜色。

    7.5 namedWindow()函数

    namedWindow(	const	String & winname,
    int		flags = WINDOW_AUTOSIZE
    )
    

    ①winname:窗口名称,用作窗口的标识符。
    ②flags:窗口属性设置标志。在默认的情况之下,窗口所加载的标志参数为“WINDOW_AUTOSIZE|WINDOW_KEEPRATIO|WINDOW_GUI_EXPANDED”。

    窗口属性标志参数作用
    WINDOW_NORMAL显示图像后,允许用户随意调整窗口大小
    WINDOW_AUTOSIZE根据图像大小显示窗口,不允许用户调整大小
    WINDOW_OPENGL创建窗口的时候会支持OpenGL
    WINDOW_FULLSCREEN全屏显示窗口
    WINDOW_FREERATIO调整图像尺寸以充满窗口
    WINDOW_KEEPRATIO保持图像的比例
    WINDOW_GUI_EXPANDED创建的窗口允许添加工具栏和状态栏
    WINDOW_GUI_NORMAL创建没有状态栏和工具栏的窗口

    7.6 imshow()函数

    imshow(	const		String & winname,
    InputArray 	mat
    )
    

    ①winnam:要显示图像的窗口的名字,用字符串形式赋值。
    ②mat:要显示的图像矩阵。

    8.参考文献

    [1]贾建军.基于视觉的手势识別技术研究[D].哈尔滨工业大学.2008
    [2]孟国庆.基于OpenCV的手势识别技术研究[D].西安科技大学.2014
    [3]Gary Bradski Adrian Kaehler .学习OpenCV[M]. 于仕琪 刘瑞祯译. 北京:清华大学出版社.2014
    [4]王天庆. Python人脸识别从入门到工程实践[M]. 北京:机械工业出版社.2019.4
    [5]冯振 郭延宁 吕跃勇. OpenCV 4 快速入门[M].北京:人民邮电出版社.2020

    展开全文
  • OpenCV+python手势识别框架和简单例子 基于 静默虚空 大神文章的原码修改调通的。https://blog.csdn.net/linsk/article/details/76457955 
  • 当初选择opencv这个库是因为感觉它较skimage对新手比较友好,现在在学图像识别之前想把手势识别再看一遍,且思且记,以便日后复习。 好了废话不多说,上干货! 首先,我们要对手势识别的基本步骤做一个了解:打开...

    前言:在各类的智能识别中,手势识别是比较简单的一种了。本人大二,在大一下学期做了一个简单的树莓派摄像头手势识别的程序。当初选择opencv这个库是因为感觉它较skimage对新手比较友好,现在在学图像识别之前想把手势识别再看一遍,且思且记,以便日后复习。

    好了废话不多说,上干货!

    首先,我们要对手势识别的基本步骤做一个了解:打开摄像头 ——>截取图像手势——>图像处理——>手势模型匹配预测——>给出判断。再本篇博客中我重点介绍的是图像处理这个环节,因为这是最基本的,手势模型预测可能会涉及机器学习(还是有点难度的)。大家可以参考这一篇博客:https://cuijiahua.com/blog/2017/11/ml_8_svm_1.html

    第一步:去噪即滤波,因为我们截取的图像可能会包含很多无用的干扰信息,所以这一步是对图像进行预处理。代码块如下:

    blur = cv2.bilateralFilter(img,9,75,75)

    主要函数方法介绍:1.cv2的bilateral Filter方法含有四个参数,img是图像,9代表处理面积,75是空间高斯标准差和相似性高斯函数标准差(这边至于为啥是75应该是个数据我们不在深究)

    效果图:

    第二步:二值化黑白处理

    def binaryMask(frame,x0,y0,width,height):
        cv2.rectangle(frame,(x0,y0),(x0+width,y0+height),(0,255,0))
        roi = frame[y0:y0+height,x0:x0+width]
        cv2.imshow("roi",roi)#读取roi文件
        res = skinMask(roi)
        cv2.imshow("res",res)

    这边定义一个函数,有四个参数分别是:frame要处理的图像,x0和y0要处理图片的左上角的坐标,width和heignt是你要处理图片的长和宽。该函数的作用就是绘制一个手势框图来获取最初的手势图像。

    主要函数方法介绍:1.rectangle绘制一个矩形区域即绘制手势框图。

                                    2.fream绘制手势框图。

                                    3.imshow显示手势框图。

    第三步:肤色检测,所谓肤色检测顾名思义就是把人皮肤的颜色的区域保留,不是人皮肤颜色的区域掩膜成黑色。

    具体的代码如下:

    ​
    ​
    def skinMask(roi):
    	rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) #转换到RGB空间
    	(R,G,B) = cv2.split(rgb) #获取图像每个像素点的RGB的值,即将一个二维矩阵拆成三个二维矩阵
    	skin = np.zeros(R.shape, dtype = np.uint8) #掩膜
    	(x,y) = R.shape #获取图像的像素点的坐标范围
    	for i in range(0, x):
    		for j in range(0, y):
    			#判断条件,不在肤色范围内则将掩膜设为黑色,即255
    			if (abs(R[i][j] - G[i][j]) > 15) and (R[i][j] > G[i][j]) and (R[i][j] > B[i][j]):
    				if (R[i][j] > 95) and (G[i][j] > 40) and (B[i][j] > 20) \
    						and (max(R[i][j],G[i][j],B[i][j]) - min(R[i][j],G[i][j],B[i][j]) > 15):
    					skin[i][j] = 255
    				elif (R[i][j] > 220) and (G[i][j] > 210) and (B[i][j] > 170):
    					skin[i][j] = 255
    	res = cv2.bitwise_and(roi,roi, mask = skin) #图像与运算
    
        return res
    ​
    
    ​

     定义函数skinMask参数为roi,想用numpy中的zeros方法返回一个给定形态和类型的用0填充的数组,YCrCb则是返回的roi把它转换到RGB空间。因为imread读取的图片是以BGR的形式的。

    这边我提一句奥,那边if里面的条件判断语句是在一般的光线下,所有的的RGB特定数据,也就是说在强光或者是很暗的环境下手势识别的效果可能没有那么好。

    一般光照条件下的判断条件:R>95 AND G>40 B>20 AND MAX(R,G,B)-MIN(R,G,B)>15 AND ABS(R-G)>15 AND R>G AND R>B

    在侧光拍摄环境下,R>220 AND G>210 AND B>170 AND ABS(R-G)<=15 AND R>B AND G>B

    效果图:

     

     

    第四步:轮廓提取

      

    camera = cv2.VideoCapture(0)
    
    camera.set(10, 150)
    
    while camera.isOpened():
        ret, frame = camera.read()
        threshold = cv2.getTrackbarPos('trh1', 'trackbar')
        frame = cv2.bilateralFilter(frame, 5, 50, 100)  # smoothing filter
        frame = cv2.flip(frame, 1)  # flip the frame horizontally
        cv2.rectangle(frame, (int(cap_region_x_begin * frame.shape[1]), 0),
                      (frame.shape[1], int(cap_region_y_end * frame.shape[0])), (255, 0, 0), 2)
        cv2.imshow('original', frame)
    
        #  Main operation
        if isBgCaptured == 1:  # this part wont run until background captured
            img = removeBG(frame)
            img = img[0:int(cap_region_y_end * frame.shape[0]),
                  int(cap_region_x_begin * frame.shape[1]):frame.shape[1]]  # clip the ROI
            # cv2.imshow('mask', img)
    
            # convert the image into binary image
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            blur = cv2.GaussianBlur(gray, (blurValue, blurValue), 0)
            # cv2.imshow('blur', blur)
            ret, thresh = cv2.threshold(blur, threshold, 255, cv2.THRESH_BINARY)
            # cv2.imshow('ori', thresh)
    
            # get the coutours
            thresh1 = copy.deepcopy(thresh)
            contours, hierarchy = cv2.findContours(thresh1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
            length = len(contours)
            maxArea = -1
            if length > 0:
                for i in range(length):  # find the biggest contour (according to area)
                    temp = contours[i]
                    area = cv2.contourArea(temp)
                    if area > maxArea:
                        maxArea = area
                        ci = i
    
                res = contours[ci]
               
                hull = cv2.convexHull(res)
                drawing = np.zeros(img.shape, np.uint8)
               
                cv2.drawContours(drawing, [res], 0, (0, 255, 0), 2)
                cv2.drawContours(drawing, [hull], 0, (0, 0, 255), 3)
    
    
          cv2.imshow('output', drawing)

    效果图:

             

     

    总结:实现了简单的图像处理和轮廓绘制。为接下来的特征提取打下了基础 

    展开全文
  • 基于OPENCV手势识别技术

    万次阅读 多人点赞 2020-12-11 13:44:04
    基于OPENCV手势识别技术   本篇博客主要介绍基于OPENCV手势识别程序,代码为C++,OPENCV版本为OPENCV3会有较为详细的实现流程和源码,并且做到源码尽量简单,注释也自认为较为清晰,希望能帮助到大家。
  • 基于OpenCV+MediaPipe的手势识别(数字、石头剪刀布等手势识别) 可识别左右手,共定义了15种手势,可以自行增加 https://blog.csdn.net/weixin_53403301/article/details/123232435
  • 基于OPENCV手势识别

    2020-12-12 00:15:25
    基于OPENCV手势识别程序,代码为C++,OPENCV版本为OPENCV3会有较为详细的实现流程和源码,并且做到源码尽量简单,注释也自认为较为清晰,希望能帮助到大家。
  • 基于openCV手势识别

    千次阅读 2021-02-06 15:50:50
    主要运用的知识就是opencv,python基本语法,图像处理基础知识,下面看效果: 捕捉摄像头(获取视频) cap = cv2.VideoCapture("C:/Users/lenovo/Videos/wgs.mp4")#读取文件 #cap = cv2.VideoCapture(0)#读取摄像头 ...
  • 毕业设计 - 题目:基于机器视觉opencv的手势检测 手势识别 算法 - 深度学习 卷积神经网络 opencv python
  • 软件功能:识别摄像头所捕获的区域手指所指示的个数,并打印在画面上。 性能指标:能成功识别由单只手比划出的0-5数字并准确显示 话不多说直接上效果图吧 一、工具与环境 工具:Anaconda,...
  • 基于OpenCv的简单手势识别(课程设计)

    万次阅读 多人点赞 2019-02-26 14:07:05
    基于OpenCv视觉库实现识别手势1-5,IDE采用的是Visual Studio 2015。 图像可实现动态采集,通过修改代码可以调用移动设备的摄像头。 原理是提前把手势1-5的图像存放在工程文件中,再把实时采集到的手势图像与之对比...
  • 伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大...选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。
  • 【计算机视觉】基于Python—OpenCV手势识别详解(一)
  • opencv2.3.1做的静态手势检测。先滤波去噪-->转换到HSV空间-->根据皮肤在HSV空间的分布做出阈值判断,这里用到了inRange函数,然后进行一下形态学的操作,去除噪声干扰,是手的边界更加清晰平滑-->得到的...
  • 基于OpenCV3.0和vs2012软件的手势识别的石头剪刀布的游戏
  • 基于opencv3.0手势识别

    2016-06-15 22:56:03
    基于opencv3.0手势识别
  • 内含文件:超详细注释手势识别py程序+半成品可以套用课题论文一份

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,718
精华内容 1,087
关键字:

基于opencv的手势识别

友情链接: renshidabian.rar