ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenCV简易手势识别:HSV肤色分割到凸包缺陷的完整实现

OpenCV简易手势识别:HSV肤色分割到凸包缺陷的完整实现 简介这是一份面向Python初学者与课程大作业需求的OpenCV手势识别入门资源解决从图像处理到手势分类的完整实现问题。压缩包共2个文件其中主程序为带逐行注释的简易手势识别.py代码从视频帧读取、灰度转换、Canny边缘检测、连通组件分析、轮廓提取到基于形状特征的识别逻辑逐段拆解另一份报告.docx为半成品课题论文已搭建研究背景、相关工作、方法设计与实验思路框架可用于毕设或课程报告扩展。整体仅446KB轻量化不占空间已有1508人学习下载说明其注释细致度和拿来即用的完成度受到认可。通过学习可掌握OpenCV常见预处理流程、特征提取思路以及如何用Python将视觉算法组织为可运行程序结合论文框架能快速补齐项目文档适合需要代码报告双交付的开发者。1. 为什么简易手势识别值得自己动手做一遍而不是直接抄模型大作业季最常搜到的题目之一就是基于OpenCV的简易手势识别。这个标题的背后是一条非常成熟的传统图像处理路线用摄像头采集手部画面做肤色分割把手从背景中请出来再用轮廓与凸包缺陷定位指尖最后把指尖个数翻译成0到5的数字。它不依赖深度学习不需要GPU也不存在训练集从哪来的问题一个普通笔记本加USB摄像头就能跑通。适合课程设计、开放实验和大作业交差也适合第一次接触OpenCV的人把整条图像处理流水线亲手摸一遍。题目里的逐行分析、超级精细的注释不是包装话术每一行代码背后都对应一个参数和一个可能翻车的点。把这一层搞清楚答辩时才不会被问倒。2. 把手从画面里请出来HSV颜色空间与肤色分割的最小流程2.1 为什么用HSV做肤色分割而不是对着BGR三通道硬设范围OpenCV默认读进来的图片是BGR三通道。BGR的三通道都跟着光照强度一起变摄像头角度稍微一动三个数值就整体偏移直接在BGR里设范围过滤要么把皮肤大片漏掉要么把背景一起圈进来。HSV把颜色拆成色调、饱和度、明度三个维度其中色调对光照变化相对不敏感肤色在色调轴上集中在一个不大且稳定的区间。这个特性决定了肤色分割在HSV空间里做通常比在BGR空间里直接设阈值省心得多。另外HSV也优于背景差分和深度学习。背景差分需要摄像头固定且背景干净大作业现场不可控深度学习手势识别模型要准备数据集、清洗标注调参成本也高和简易两个字差得很远。HSV肤色分割虽然朴素但整条链路都能在答辩时用手一笔一画讲清楚这是这个方案最大的性价比。OpenCV里的HSV范围需要注意8位图像中H的范围是0到179S和V的范围是0到255很多教材里用的0到360、0到100在这里并不直接适用。常见肤色的H大致落在0到20以及接近170到179的红偏黄区间S在48以上V在50以上。这不是一组万能值不同摄像头白平衡不同实际范围要现场微调。2.2 最小可运行代码打开摄像头、转HSV、用inRange抽出手我习惯先把最简流程跑通再往上加轮廓。这个流程只需要四件事打开摄像头、翻转画面、转HSV、做inRange。import cv2 import numpy as np # 打开摄像头0表示默认摄像头外接摄像头打不开就试1、2 cap cv2.VideoCapture(0) # 定义肤色范围注意OpenCV的H最大是179而不是360 lower np.array([0, 48, 50], dtypenp.uint8) upper np.array([20, 170, 255], dtypenp.uint8) while True: # ret是布尔值frame是这一帧的BGR图像 ret, frame cap.read() if not ret: print(读取失败请检查摄像头) break # 水平翻转让画面像镜子一样操作更顺手 frame cv2.flip(frame, 1) # BGR转HSV参数写错成COLOR_RGB2HSV会让画面颜色错乱 hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 落在lower和upper之间的像素置为255其余置为0 mask cv2.inRange(hsv, lower, upper) cv2.imshow(mask, mask) # 等待按键q退出waitKey里的1表示只等1毫秒 if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逐行看VideoCapture(0)打开默认摄像头USB摄像头型号多有时是VideoCapture(1)最好在程序里先cap.isOpened()判断一下。cap.read()返回两个值摄像头拔出或驱动卡顿时ret变False直接break退出不是好习惯后面第5章会说怎么处理。flip的原因是你面对屏幕移动右手画面里的手如果没翻转会向左动翻转后动作方向一致方便后面的交互演示。cv2.cvtColor这里最容易翻车。OpenCV读入的是BGR但cv2.COLOR_RGB2HSV这种写法在视觉上不会报错只会让色相整体错位然后你调半天HSV参数都调不对。新手阶段请记住来自cap.read()的图一定是BGR转HSV就用COLOR_BGR2HSV。inRange生成的是二值掩码白色区域代表被判定为皮肤的部分。这里我只用了0到20的H区间如果你的手偏红或者室内灯光偏暖可以在循环外再构造一个高H区间把两个mask用cv2.bitwise_or合并能明显减少漏检。提示HSV范围不是一次调对的。先用这段代码跑起来再在cv2.createTrackbar里挂上lower和upper的6个分量边拖边看mask比盲调快十倍。2.3 形态学操作为什么开运算和闭运算一个都不能省裸的inRange结果通常布满噪点。墙上一个斑点、袖口边缘、头发丝都会变成白色小区域手本身又可能因为反光在内部出现黑窟窿。如果不处理直接找轮廓碎块会让最大轮廓选错黑窟窿会让凸包缺陷误报指尖。形态学操作专门处理这两个问题。kernel_open np.ones((5, 5), np.uint8) kernel_close np.ones((8, 8), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_open) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_close)开运算是先腐蚀再膨胀作用是把独立小噪点腐蚀掉闭运算是先膨胀再腐蚀作用是把手部区域内部的小孔补上同时让相邻的皮肤碎块连成片。这两个操作的顺序有讲究先开运算避免闭运算先把噪点放大成一片再闭运算把开运算后留下的边缘毛刺和孔洞填平。核大小是第一个要调的参数。5x5和8x8在640x480分辨率下够用分辨率升高后核要成比例放大分辨率降低则缩小。核太小噪点去不掉核太大会把张开的手指缝直接焊死后面凸包缺陷就找不到指尖了。出现指间粘连时第一反应不是调convexityDefects而是回头把闭运算的核改小。形态学处理完mask就是相对干净的白色连通区域下一步就该让OpenCV告诉我们手在哪里。这也是从像素处理进入几何处理的分界线。3. 轮廓与凸包缺陷识别手势的核心几何逐行拆解3.1 findContours的正确打开方式两值返回和三值返回的坑在OpenCV图像处理项目里找轮廓这个函数是版本差异的重灾区。OpenCV 3的cv2.findContours返回三个值OpenCV 4返回两个值网上教程新旧混杂很多新手直接把旧代码粘进新环境第一行就报not enough values to unpack。如果不想被这个问题折腾先用一个兼容写法contours cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2][-2]取返回值倒数第二个元素。在OpenCV 4里返回(contours, hierarchy)倒数第二个是contours在OpenCV 3里返回(image, contours, hierarchy)倒数第二个还是contours。这样一段代码在两个版本下都不会炸。模式参数我固定用RETR_EXTERNAL它只取最外层轮廓。手势识别不需要手心的纹理轮廓内部轮廓会干扰哪块是手的判断用RETR_EXTERNAL最省事。轮廓近似用CHAIN_APPROX_SIMPLE它把一条直线上的点压缩成两个端点保留转角点即可轮廓点数少一半以上后续凸包和缺陷计算都会更快。3.2 凸包与凸包缺陷指尖藏在哪个几何量里拿到手部轮廓后要回答哪里是指尖。最朴素的想法是找轮廓上曲率最大的点但轮廓点噪声大同一个指尖附近可能有几十个候选点曲率阈值稍微一变结果就乱跳。更稳的做法是先把轮廓的凸包算出来想象拿一根橡皮筋绷住轮廓最外面的凸出点橡皮筋形成的多边形就是凸包。五指张开时指尖是凸出的顶点手指之间的谷底则陷在凸包边里面。这些凹陷就是凸包缺陷。OpenCV的cv2.convexityDefects用来找凹陷。调用它之前先要用cv2.convexHull(contour, returnPointsFalse)拿到轮廓点的索引序列注意这里必须把returnPoints设成False。默认的True返回的是凸包顶点坐标传进convexityDefects会直接报错因为缺陷函数拿索引去轮廓数组里取点。这个参数是教程最容易写漏的地方。convexityDefects返回一个数组每个缺陷有四个值起点索引、终点索引、最远点索引、深度。前三个点画出来就是指间谷底和两侧指尖深度是从谷底到凸包边的距离。五指张开时每两个手指之间有一个明显的缺陷四个缺陷对应五根手指。深度值不是像素距离OpenCV用定点数保存要除以256才算还原成像素。这个细节不处理后面阈值全乱。3.3 完整可运行的指尖统计代码把前面的逻辑收拢成一个函数输入mask输出轮廓和手指数。这版代码可以直接接在第2章的HSV循环后面。import cv2 import numpy as np def count_fingers(mask): # 先形态学顺序固定去噪、补洞 kernel_open np.ones((5, 5), np.uint8) kernel_close np.ones((8, 8), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_open) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_close) # 找外轮廓[-2]写法兼容OpenCV 3和4 contours cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] if not contours: return None, 0 # 所有轮廓中面积最大的优先认为是手 hand max(contours, keycv2.contourArea) area cv2.contourArea(hand) # 面积太小说明画面里只有一小块噪声 if area 1000: return None, 0 # returnPointsFalse只要轮廓点索引 hull cv2.convexHull(hand, returnPointsFalse) # 每个缺陷起点、终点、最远点、深度 defects cv2.convexityDefects(hand, hull) finger_count 0 if defects is not None: for i in range(defects.shape[0]): s, e, f, d defects[i, 0] depth d / 256.0 # 定点数转回像素距离 if depth 6000: finger_count 1 return hand, finger_count 1逐行拆max(contours, keycv2.contourArea)取面积最大的轮廓。如果画面里同时出现手和脸脸的面积经常比手更大所以这个函数在单人场景下要配合ROI使用否则会把脸当手。面积阈值1000是640x480下的经验值上一章说过分辨率高时按比例上调。cv2.convexHull(hand, returnPointsFalse)返回的hull是一维索引数组convexityDefects拿到hull后会在内部把这些索引还原成轮廓点再做向量叉积判断凹陷。defects[i, 0]解出四个字段。深度的单位必须除以256把代码里的d换成d / 256.0而不是整数除法否则多数深度都会被截成0。缺陷数加1等于手指数因为五根手指张开时只有四个指间谷。如果手指并拢到没有缝隙缺陷数会少这正是下一章要处理的情况不能只依赖深度阈值还要加角度约束把并拢的指间谷判断出来。4. 让识别结果从能跑到能用四个必调参数与两种误用4.1 面积阈值与ROI分辨率一变就要跟着调的数值第3章里的area 1000只是经验值。摄像头分辨率从640x480切到1280x720像素总数变成3倍手稍微离远一点轮廓面积就会跌到阈值以下识别结果从有手变没手。所以第一个要调的参数就是面积阈值调法不是靠感觉而是看实际运行时打印出来的面积分布。我一般会在循环里临时加一行cv2.contourArea(hand)输出把近景、远景、半握拳各抓几帧观察不同手势的面积范围然后把阈值定在最小有效面积的一半。同时可以用ROI降低干扰。手势识别只关心画面中央的手把固定区域之外全部屏蔽计算量小背景里的木色家具、远处的肤色海报也会少很多h, w frame.shape[:2] frame frame[h // 3:, :] # 只保留下半部分ROI切掉上半部分之后人脸和脖子大概率不在范围内最大轮廓的误判率明显下降。但注意ROI也会带来边界断层手伸到画面顶部会被一刀切掉mask变成半只手掌凸包缺陷会被切边制造出假缺陷。所以在选ROI时要么让手全程待在画面中央要么把ROI框得足够大。4.2 defects深度阈值张手和握拳的判决线凸包缺陷的深度反映的是指间谷底到凸包边的距离。五指张开时这个距离很大在640x480分辨率下经常超过8000握拳时指尖与指根之间的高度差变小缺陷深度明显下降。用6000作判决线能把大部分握拳的干扰排除。问题在于这个6000不是普适值。分辨率升高深度也随之放大需要按比例上调分辨率降低则下调。要确定自己的判决线不要在理论上纠结直接把每个缺陷的depth打印出来连续采集张开、握拳、比2、比5四组动作各30帧看两组数据的分布分界在哪里。这里我最常用的调试办法是把depth显示在画面上用cv2.putText每一帧写出来动作切换时一目了然。如果张开和握拳的深度分布有重叠说明把关的不是深度阈值而是形态学。指间缝隙噪声太大或闭运算核太粗会让本应很深的凹陷被填浅。回到第2章调核大小比在convexityDefects里抠阈值更有效。4.3 手指间隙误判用三点角度约束把手腕边缘关在门外深度阈值只能排除浅缺陷但手腕和小臂连接处有时会在内翻时形成一个深度不低的凹陷看起来像手指间隙。要拦住它需要第三个参数角度。用凸包缺陷返回的起点、终点和最远点构造三角形。起点和终点分别位于谷底两侧最远点就是谷底本身。在这个三角形里最远点处的夹角在手指张开时通常处于30度到120度之间而手腕连接处的夹角要钝得多常常超过140度。把角度判断加进去误报会少很多。p_s hand[s][0] p_e hand[e][0] p_f hand[f][0] a np.linalg.norm(p_s - p_f) b np.linalg.norm(p_e - p_f) c np.linalg.norm(p_s - p_e) cos_angle (a*a b*b - c*c) / (2*a*b) angle np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0))) if depth 6000 and 30 angle 120: finger_count 1np.clip(cos_angle, -1.0, 1.0)这一行不能省。浮点误差会让三角形余弦值略微超出[-1,1]arccos会返回NaNNaN参与比较永远为False整个缺陷就被吞掉。这个坑在numpy和数学库混用时候很常见这里同样适用。加了角度约束以后手腕区域被过滤掉剩下的缺陷基本都是指间谷。这个方案不算高深但你在答辩时能现场讲清楚为什么钝角不算手指比黑盒深度学习好解释得多。4.4 两种常见误用approxPolyDP数顶点、convexHull返回点坐标第一种误用是拿cv2.approxPolyDP去拟合轮廓然后数顶点数量当手指数。这个思路看起来有理但手部边缘有大量圆弧epsilon调小会出现十个以上伪顶点调大又会把真实指尖削平识别结果极不稳。凸包缺陷天生就是为凹陷几何准备的不要绕。第二种误用是把cv2.convexHull(hand, returnPointsTrue)的返回值直接传给cv2.convexityDefects。这个错误在OpenCV的报错信息里写的是凸包点数必须大于等于3之类的模糊描述容易让人跑去检查轮廓是不是有问题实际上是传错了数据。记住convexHull喂给convexityDefects时必须用returnPointsFalse拿到索引。除了这两种误用还有一个误用直接在mask上数白色连通域当手。如果闭运算没做手缝处可能被断成几块数出来2个连通域手指数跟着乱。这个误用不是语法错误而是流程顺序错误形态学的闭运算在找轮廓之前必须完成。调参顺序我建议固定先定ROI再调HSV范围然后调形态学核最后才动convexityDefects的深度和角度。一次只动一个参数观察连续30帧不要一边调三个变量一边喊玄学大多数识别不稳定是自己调出来的。5. 避坑OpenCV版本、光照、肤色误检的5条实战记录5.1 findContours返回值不一致现象从教程里复制的手势识别代码在自己电脑上运行第一行就报not enough values to unpack (expected 3, got 2)。原因教程基于OpenCV 3写的而你的环境是OpenCV 4findContours的返回约定变了。这不是逻辑错误而是API变化导致的兼容问题。解决统一写成contours cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]或者先打印cv2.__version__确认版本后再改。如果是C环境报contourArea未定义标识符则多半是没有包含opencv2/imgproc.hpp那是另一个分支的问题。5.2 背景肤色误检最大轮廓不是手现象手明明在画面里识别框却框住脸、脖子或者背景里的木色家具。原因HSV肤色范围不是只圈中了手凡是颜色相近的区域都会被inRange放进来最大轮廓自然不一定是手。解决先切ROI把画面中央或下半部分固定为手部活动区然后加上轮廓外接矩形的宽高比约束。正常手的长宽比范围大致在0.5到2.0之间超出这个范围的多半不是完整手掌直接丢掉x, y, w, h cv2.boundingRect(hand) if 0.5 h / w 2.0: # 通过验证才继续算手指背景干扰是肤色方案的天性不要指望一个阈值全解决。允许的范围内ROI加宽高比加中心点位置三重约束比盲目调HSV更有效。5.3 手一离镜头近就误判现象手靠近摄像头时mask大面积过曝手指之间的缝隙忽然消失识别跳动。原因近景皮肤反光强V通道飙高S通道下降固定的HSV范围覆盖不了这种变化。同时像素距离变近让指间缝在图像里变窄闭运算核把缝隙焊死。解决把闭运算核从8x8下调到5x5V通道上限从255下调到230让过亮区域漏出白色。更关键的是控制使用距离手离摄像头30到50厘米时指尖尺寸和指间缝宽度最稳定。大作业演示时最好加一句手请放在这个区域这不算功能缺陷而是交互设计的一部分。5.4 摄像头拉流中断现象程序跑几分钟后画面黑屏cap.read()一直返回False按键失灵。原因笔记本USB口供电不稳或摄像头驱动出现丢流。这在OpenCV实时项目里很常见不一定是代码问题。解决不要一读到False就break加一个连续失败计数超过5帧再重连fail 0 while True: ret, frame cap.read() if not ret: fail 1 if fail 5: cap.open(0) fail 0 continue fail 0 # 正常处理另外cap.open(0)重新打开后摄像头可能需要一瞬间重新曝光要等下一帧成功。这里不做等待直接处理会出现画面偏暗一两帧可以在重连后sleep(0.2)再继续。5.5 光照一变就翻车现象代码白天在窗边识别正常晚上开日光灯后整条识别链路乱掉。原因色温改变让HSV空间里的H和S整体漂移固定的lower和upper不再贴合当前肤色。解决不要把方案变成对光照的赌博。正确做法是先把演示环境固定用白光LED侧前方打光避免冷暖光源混在一起。其次要做一个离线验收基准录一段包含张开、握拳、比2、比5的手部视频把参数定死后反复跑这段视频统计稳定帧数。这个统计结果既是调试工具也是答辩数据比现场对着摄像头反复调参更能说明工作量。如果非实时不可就在PPT里承认肤色分割对光照敏感并干净利落说明你的补偿措施。6. 验证与进阶录制回放做回归测试再把相同画面送给骨架提取做对照6.1 先录一段固定视频把实时翻车变成离线回归实时摄像头最大的问题是你没法复现上一次翻车的那一秒。我的习惯是先跑一段录制程序把30秒动作存成一个avi文件再用第3章的count_fingers逐帧读取import cv2 def record_test_video(out_filegesture_test.avi, seconds30): cap cv2.VideoCapture(0) fourcc cv2.VideoWriter_fourcc(*MJPG) out cv2.VideoWriter(out_file, fourcc, 25, (640, 480)) frame_count 0 total 25 * seconds while frame_count total: ret, frame cap.read() if not ret: continue out.write(frame) frame_count 1 cv2.imshow(recording, frame) if cv2.waitKey(1) 0xFF ord(q): break out.release() cap.release()fourcc cv2.VideoWriter_fourcc(*MJPG)指定编码格式25是目标帧率(640, 480)必须和cap.read()的真实分辨率一致否则写出来的视频会一帧都不进去。录完以后回放时把每帧识别结果打印出来人手记下每一帧预期的数字看到结果后按帧比对。这个方法能抓住两种问题一是某个特定角度下指尖突然消失二是握拳动作被误判成2或3。离线回归虽然原始但能让你在答辩前拿到一串可复现的准确率而不是刚才在电脑上明明好的。6.2 骨架提取做交叉验证两条传统路线互相兜底如果想让方案更结实可以在同一帧上再跑一条参考路线cv2.ximgproc.thinning把二值mask细化成单像素骨架。骨架提取后手指会变成从掌心伸出的细线数骨架端点和分叉点可以推算出手指数。这个算法不在opencv-python主包里需要安装opencv-contrib-python但仍然是OpenCV家族不引入深度学习。我一般会把凸包缺陷的结果和骨架端点结果都打印出来两个方法在大多数帧上一致时就提交不一致的那几帧正好是参数需要微调的地方。这就是传统图像处理方案最舒服的体验每一步都能解释、能调试、能交叉验证最后过答辩不用靠运气。我自己的习惯是任何手势识别程序交出去之前都要留一份固定视频和测试代码。遇到你参数为什么这么设的追问直接打开这段回放把深度和角度两个数字调给他看。比背20页原理管用得多。这个习惯后来帮我在好几个类似的小项目里省掉了返工希望帮到你。本文还有配套的精品资源点击获取
返回列表