ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenCV+Python指尖检测:背景减除、凸包与键盘模拟

OpenCV+Python指尖检测:背景减除、凸包与键盘模拟 简介面向Python与OpenCV开发者的手势识别实战资料基于GitHub开源项目改进后支持手指指尖检测并能在Windows平台通过手指数目模拟键盘按键实现简单的免触控交互。整个资源为1个PDF文件仅233KB内容以完整源码为主干配有逐段中文注释并给出环境说明Python3.6OpenCV3.4.0与核心参数的含义及可调范围代码覆盖背景减除、高斯模糊、二值化、轮廓提取、凸包绘制、指尖定位与计数、模拟按键等关键环节从摄像头读取到输出控制一应俱全很适合初次接触手势识别或希望快速跑通Demo的学习者。已有1992人学习阅读时既可以按代码顺序跟踪处理流程也可以把指尖检测部分移植到自己的项目中二次开发或在此基础上调整阈值、修改按键映射以适配不同场景。1. 手势识别这可能是你最快跑通的指尖检测方案用 Python 做手势识别很多人第一反应是上 MediaPipe 或深度学习模型但如果你只需要在 Windows 下通过手指数目触发键盘操作OpenCV 的经典方案反而更直接不需要训练、不需要 GPU、单个 Python 文件就能跑。本文要拆的这个项目来自 GitHub 上的 Fingers-Detection-using-OpenCV-and-Python原作者实现了基于背景减除的指尖检测我在此基础上补充了指尖坐标过滤逻辑和 win32api 键盘模拟让整个程序可以在 Windows 下用 2 根手指、3 根手指去触发按键。这个方案的核心思路不是靠肤色检测而是靠背景减除先让摄像头记住一帧没有手的背景然后把每一帧画面和背景做差分前景就是你的手。整个过程只依赖 OpenCV 的图像处理函数和 numpy 运算原理透明、参数可调非常适合作为手势识别入门项目来复现。适合的人群很明确想理解 OpenCV 轮廓分析和凸包原理的人、需要在本地用摄像头做交互控制的人、以及不想引入大模型依赖的嵌入式或桌面端开发者。2. 从背景建模到二值化先把「手」从画面里抠出来2.1 为什么选 MOG2 而不是肤色检测这个项目最关键的一步是cv2.createBackgroundSubtractorMOG2。我去查了原项目的设计意图作者选它而不是肤色检测是因为肤色模型对光线和白平衡极其敏感同一个人的手在暖光灯和 LED 灯下 YCrCb 范围的分布完全不同肤色阈值调一次换一个环境就失效。而背景减除不关心你手是什么颜色只关心画面里什么东西动了、什么东西没动。MOG2 的全称是 Mixture of Gaussians v2OpenCV 内置的自适应混合高斯背景建模。它的工作方式是对每个像素建立多个高斯分布一个像素如果长期保持相近的灰度值就被归入背景模型一旦某个像素的灰度值和背景模型差异超过阈值就被判定为前景。createBackgroundSubtractorMOG2(history, varThreshold, detectShadows)的三个参数中history 决定用多少帧来训练背景模型varThreshold 决定判定前景的方差阈值detectShadows 用于是否检测阴影。bgModel cv2.createBackgroundSubtractorMOG2(0, bgSubThreshold)这段代码里 history 传了 0表示使用默认的 200 帧bgSubThreshold的值是 50表示像素值和背景模型的马氏距离超过 50 就认为是前景。注意 MOG2 的默认参数对阴影检测是开启的原项目没有单独处理阴影这在实际运行中会导致手部边缘出现灰黑色的噪点块。我一般会把 detectShadows 显式设为 False减少后续轮廓提取的干扰bgModel cv2.createBackgroundSubtractorMOG2(history500, varThresholdbgSubThreshold, detectShadowsFalse)history 从 0 改成 500目的是让背景模型在窗口移动、窗帘飘动时更稳定。如果你的摄像头固定不动这个值可以更大如果摄像头是手持的建议降到 100 以下否则背景模型更新太慢手的残影会残留在前景里。2.2 removeBG 里的腐蚀操作到底在干什么removeBG函数是整条处理链的入口它做的事情不止是前景提取还加了一次腐蚀def removeBG(frame): fgmask bgModel.apply(frame, learningRatelearningRate) kernel np.ones((3, 3), np.uint8) fgmask cv2.erode(fgmask, kernel, iterations1) res cv2.bitwise_and(frame, frame, maskfgmask) return resbgModel.apply返回的是一张单通道掩膜白色区域代表前景黑色代表背景。直接用这张掩膜去截原图你会看到手的边缘有一圈毛刺——这是摄像头噪声和背景模型边缘像素误判造成的。腐蚀的作用就是把这些毛刺削掉kernel 是 3×3 的全 1 矩阵iterations1 表示执行一次腐蚀效果是让白色区域的边界向内收缩 1 个像素。代价是手指尖也会变细一圈对于指尖检测来说这反而有利于减少后续轮廓的锯齿。learningRate传的是 0这个参数另有含义。MOG2 的 apply 方法里learningRate 为负数默认 -1表示自动更新背景模型传 0 表示这一帧完全不更新背景。原项目里学习率恒为 0意味着背景模型只在按 b 键那一刻被初始化之后永远不会学习新的背景变化。这在固定场景下是优点——手不会慢慢被融入背景但你如果中途移开摄像头或者背景里多了个物体前景就会一直存在直到按 r 重置。2.3 双边滤波和高斯模糊的分工一个保边一个降噪预处理阶段有两个容易搞混的滤波开头对原始帧做的是cv2.bilateralFilter对前景掩膜做的是cv2.GaussianBlur。前者是双边滤波它的特点是同时考虑像素的空间距离和灰度差异所以在平滑噪声的同时能保留边缘。用在原始帧上是为了让背景建模时像素的灰度分布更稳定避免传感器噪点导致背景模型误判。frame cv2.bilateralFilter(frame, 5, 50, 100)参数依次是 d5、sigmaColor50、sigmaSpace100。d 是滤波窗口直径5 意味着考虑每个像素周围 5×5 的邻域sigmaColor 越大灰度差异大的像素越容易被一起平滑边缘保留能力越弱sigmaSpace 越大远处像素对当前像素的影响越大。我实测下来这三个参数在 640×480 的摄像头画面上表现不错但如果你用的是 1080p 画面d 要适当加大到 7否则平滑效果不足。高斯模糊则用在灰度图上目的是消除二值化时产生的椒盐噪声blur cv2.GaussianBlur(gray, (blurValue, blurValue), 0)blurValue设为 41这是一个偏大的核。41×41 的高斯核意味着每个输出像素是周围 41×41 范围内像素的加权平均对轮廓的平滑作用很明显代价是丢失细小的凸起。如果手指很细或者摄像头距离手较远可以考虑把 blurValue 降到 21否则指尖可能被高斯模糊抹平导致后面距离法找不准最远点。2.4 阈值滑块的正确姿势先调亮度再调阈值程序启动时会创建一个名为 trackbar 的窗口里面放了一个 threshold 滑条范围 0 到 100。这里的阈值是二值化的分界线灰度图上像素值大于 threshold 的变成 255白小于的变成 0黑。cv2.createTrackbar(threshold, trackbar, threshold, 100, printThreshold)中初始值 threshold 是全局变量 60回调函数 printThreshold 只是把当前阈值打印出来。实际操作时我建议按这个顺序校准先按 b 捕获背景然后对手放在摄像头前观察 binary 窗口如果手是黑色、背景是白色说明阈值反了需要把阈值调低如果手上有大片空洞说明阈值太高背景的灰度值也被算进了前景。有个细节值得注意cap_region_x_begin 0.5和cap_region_y_end 0.8限定了感兴趣区域。程序只在画面右半部分、从上往下 80% 高度的区域内做手势检测画面上用红色矩形标出了这个区域。这个设计是有意的——大多数人的操作习惯是右手放在摄像头右侧左手操作键盘所以只检测右侧区域能减少误判。如果你习惯左手手势把cap_region_x_begin改成 0.0 并调整矩形框绘制逻辑就行。3. 指尖检测轮廓、凸包和距离法的三角关系3.1 从二值图到轮廓findContours 的坑从版本开始二值化之后thresh是一张黑白图白色区域就是手。接下来要找到手的轮廓OpenCV 提供了findContours。这里有一个版本差异的坑在文末的补充里明确写了OpenCV 3.x 时代返回三个值OpenCV 4.0 之后返回两个值。thresh1 copy.deepcopy(thresh) _, contours, hierarchy cv2.findContours(thresh1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)第一版代码针对的是 OpenCV 3.4.0所以用了三个返回值。copy.deepcopy(thresh)很关键——findContours会直接修改输入图像如果不拷贝后续想再显示二值化结果就会拿到一张被破坏的图。cv2.RETR_TREE表示建立轮廓的层级树cv2.CHAIN_APPROX_SIMPLE则用最少的点来编码轮廓只保留端点。如果你装的是 OpenCV 4.x直接跑这段会报错说解包的值太多解决方式很简单contours, hierarchy cv2.findContours(thresh1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)我自己的经验是与其改代码不如在同一条处理链里做版本兼容cnt_info cv2.findContours(thresh1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) if len(cnt_info) 3: _, contours, hierarchy cnt_info else: contours, hierarchy cnt_info这样无论队友用的是 3.4 还是 4.5代码都不会炸。3.2 为什么只取最大轮廓凸包检测的数学基础轮廓可能有多个比如手旁边放了杯水、或者背景减除没做干净留下了一块噪点。项目里用面积法筛掉小轮廓length len(contours) maxArea -1 if length 0: for i in range(length): temp contours[i] area cv2.contourArea(temp) if area maxArea: maxArea area ci i res contours[ci]cv2.contourArea计算的是轮廓包围的面积不是像素点数因为轮廓是折线直接数点数会低估面积。取最大轮廓后下一步是计算凸包hull cv2.convexHull(res)凸包是包含轮廓所有点的最小凸多边形。指尖检测依赖凸包的原因是手指伸出来时指尖是轮廓上离手心最远的点同时也是凸包的顶点。非凸的凹点比如指缝不可能是凸包顶点所以凸包可以直接过滤掉指缝形成的干扰点。cv2.drawContours(drawing, [hull], 0, (0, 0, 255), 3)用红色画凸包绿色画原始轮廓调试时一眼就能看出凸包是否贴合手指外形。3.3 重心距离法找指尖为什么不用凸包缺陷算出凸包之后主流的指尖检测思路有两种一种是基于凸包缺陷convexity defects通过寻找轮廓上凹陷最深处的点来推断指缝位置再反推指尖另一种是本文采用的距离法直接找轮廓上离重心最远的点。原作者选择了后者代码里先算轮廓的矩moments cv2.moments(res) center (int(moments[m10] / moments[m00]), int(moments[m01] / moments[m00]))这里用零阶矩 m00 归一化一阶矩 m10 和 m01得到的是轮廓质心。在手势识别中质心和重心的差异可以忽略用矩计算是最稳的因为它对轮廓形状不敏感。指尖检测的核心逻辑是遍历轮廓上的每个点计算它到重心的距离平方fingerRes [] max 0; count 0; notice 0; cnt 0 for i in range(len(res)): temp res[i] dist (temp[0][0] - center[0])**2 (temp[0][1] - center[1])**2 if dist max: max dist notice i if dist ! max: count count 1 if count 40: count 0 max 0这段代码初看有点绕。它不是在找全局最远点而是在找「局部最远点」每当 dist 超过当前最大值就更新 notice当 dist 不再是最大值且持续了 40 个点以上说明当前这个最远点是一个独立的凸起把它记下来然后重置 max继续找下一个凸起。count 40 是防止噪声造成的假峰值被当作指尖——如果只是少数几个点突然变远count 还没到 40 就被重置了。为什么用距离平方而不是实际距离因为math.sqrt是浮点运算在 Python 的循环里对每个轮廓点都开根号一帧几十毫秒的延迟就是从这里来的。距离平方的单调性和距离一致不影响最值比较但省掉了大量开方运算。这是原项目里很实惠的优化。3.4 指尖过滤的两道闸门低于手心不算、靠太近不算找到局部最远点之后还有两道过滤这是原项目里容易被人忽略但实战价值最高的部分。第一道是高度过滤if center[1] res[notice][0][1]: continuecenter[1]是重心的 y 坐标res[notice][0][1]是候选点的 y 坐标。图像坐标系里 y 向下增大所以center[1] 候选点 y意味着候选点比重心更低。如果它比手心还低那大概率是小臂或者手腕的边缘凸起不是手指。这个假设在手掌朝下、手指向前伸的手势下成立但如果你做的是手掌朝上的手势指尖会比重心低这一条会把所有指尖都过滤掉。用这个项目时手势固定成掌心朝下会更稳。第二道是距离过滤for j in range(len(fingerRes)): if abs(res[notice][0][0] - fingerRes[j][0]) 20: flag True break这是检查候选点和已确认的指尖在 x 坐标上是否太近。x 坐标差小于 20 像素视为同一个手指的重复检测。注意这里只比了 x 坐标没比 y 坐标——因为手指竖直伸出时同一个手指的轮廓点分布在同一 x 区间内的不同高度x 是最有区分度的维度。如果摄像头是横着放的这个判断会失效你需要改成同时比较 x 和 y或者干脆算两点欧氏距离。20 这个阈值对应的是 640×480 画面下正常手指宽度的像素量。摄像头分辨率更高时这个值要等比放大比如 1280×720 画面下建议调到 40。4. 从指尖到手指数键盘模拟与控制逻辑4.1 win32api 模拟按键空格键的按下与释放指尖检测完成后cnt 就是当前帧识别到的手指数。原项目把它用来控制空格键win32api.keybd_event(32, 0, 0, 0) win32api.keybd_event(32, 0, win32con.KEYEVENTF_KEYUP, 0)keybd_event的第一个参数是虚拟键码32 对应空格第二个参数是扫描码0 表示不指定第三个参数是标志位0 表示按下win32con.KEYEVENTF_KEYUP表示释放。这里有个容易踩的坑按下和释放之间没有延时很多程序检测不到这么快的击键。我一般会在两次调用之间加一个time.sleep(0.05)模拟真实按键的持续时间import time win32api.keybd_event(32, 0, 0, 0) time.sleep(0.05) win32api.keybd_event(32, 0, win32con.KEYEVENTF_KEYUP, 0)还有一个问题是连续触发。只要手势保持 3 根手指每帧都会触发一次按键一秒钟 30 帧就是 30 次空格。原项目没有做防抖。实际使用时我会引入一个冷却时间last_trigger_time 0 if triggerSwitch is True and cnt 3: current_time time.time() if current_time - last_trigger_time 0.5: win32api.keybd_event(32, 0, 0, 0) time.sleep(0.05) win32api.keybd_event(32, 0, win32con.KEYEVENTF_KEYUP, 0) last_trigger_time current_time这样做的好处是想触发翻页就快速比个 3然后放下不用一直举着手。0.5 秒的冷却时间不会让操作变得迟钝但能挡住 90% 的误触。4.2 三种模式的状态机b 捕获、r 重置、n 开启程序的交互逻辑通过键盘事件切换状态核心是一个布尔变量isBgCaptured和triggerSwitch。初始状态下isBgCaptured为 0程序只做画面显示不做任何手势检测按下 b 键背景模型建立isBgCaptured变为 1开始检测指尖按下 r 键背景模型置空回到初始状态按下 n 键triggerSwitch变为 True此时检测到的手指数目才会触发键盘事件。k cv2.waitKey(10) if k 27: break elif k ord(b): bgModel cv2.createBackgroundSubtractorMOG2(0, bgSubThreshold) isBgCaptured 1 elif k ord(r): bgModel None triggerSwitch False isBgCaptured 0 elif k ord(n): triggerSwitch True注意cv2.waitKey(10)的返回值。它每 10 毫秒读取一次键盘输入返回的是按键的 ASCII 码。ord(b)取 b 的 ASCII 码这样比较的是数字而非字符避免编码问题。还有一个隐藏细节cv2.waitKey只有在当前窗口获得焦点时才能收到按键所以你必须先点击 OpenCV 弹出的任一窗口再按这些快捷键。这套状态机的设计思路值得借鉴把「建模」和「触发」分成两个开关避免摄像头刚启动背景还没建模就误触发键盘。如果你想加一个暂停功能可以在 n 与 r 之间加一个 p 键把triggerSwitch设回 False这样就不用重置背景了。4.3 打印的真实作用它是指尖检测的调试工具代码里在每帧检测完指尖后有一行print(cnt)很多人以为只是单纯的手指数输出。在实际调试中这行打印的意义远不止于此当你把程序连接到其他软件比如游戏、PPT之前先通过终端观察 cnt 是否稳定可以快速判断是检测算法的问题还是键盘模拟的问题。我会把它扩展成更完整的调试输出print(fframe: {frame_id}, fingers: {cnt}, centers: {center})帧号和重心坐标一起输出能帮你判断指尖检测是不是发生了跳变。跳变的特征是相邻两帧手指数从 2 变成 4 又变成 2而你的手根本没动——这种情况八成是距离法的 count 40 阈值太小或者高斯模糊核不够大。不要开着摄像头去猜终端输出的数字是最诚实的反馈。5. 避坑指南OpenCV 版本、光照和误触这三座大山5.1 坑一OpenCV 4.0 后 findContours 返回值变化现象运行程序直接报错提示not enough values to unpack (expected 3, got 2)。原因OpenCV 4.0 调整了findContours的接口旧版本返回(image, contours, hierarchy)三个值新版本只返回(contours, hierarchy)两个值。项目源代码按 OpenCV 3.4.0 写的三个返回值在 4.x 下必然崩。解决两个方案任选。方案一是降级 OpenCVpip install opencv-python3.4.0.14但要注意 3.4.0 与 Python 3.9 以上版本不兼容如果你是 Python 3.10 以上的环境这条路走不通。方案二更推荐把解包改成自动适配。cnt_result cv2.findContours(thresh1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) contours cnt_result[0] if len(cnt_result) 2 else cnt_result[1] hierarchy cnt_result[1] if len(cnt_result) 2 else cnt_result[2]这段逻辑的含义是返回值长度是 2直接取第 0 个作为轮廓长度是 3说明是老版本轮廓在第 1 个位置。这样写的好处是你的代码在 3.4 到 4.8 的所有版本都能跑。5.2 坑二光照一换手直接融进背景现象白天在窗边跑得好好的晚上把灯一关手在 binary 窗口里大面积变黑或者背景里出现大片白色噪点指尖数量乱跳。原因这个项目的背景模型是在按下 b 键那一刻固定的后续完全不更新。白天和晚上的光线色温不同MOG2 建的模型是基于白天的灰度分布晚上灯光一变所有像素的灰度值整体偏移背景就和手一起变成了前景。光照突变时整个画面的灰度分布都变了背景模型必然失配。解决三条路。第一固定你的环境拉窗帘、固定灯光让背景灰度在运行期间尽量不变。第二把学习率从 0 改成-1让 MOG2 自动更新背景模型——但这样手静止不动几秒手也被融入背景指尖会消失需要你用动手机制来避免。第三启动后重新按一下 r 再按 b重新建模。这个方法最直接但治标不治本。我实际项目里是加了一个简单的光照补偿取画面左上角固定区域的灰度均值如果与初始背景的灰度均值偏差超过阈值自动触发一次重新建模。roi frame[0:50, 0:50] current_brightness np.mean(cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)) if abs(current_brightness - initial_brightness) 15: bgModel cv2.createBackgroundSubtractorMOG2(0, bgSubThreshold) isBgCaptured 1这里 initial_brightness 是按下 b 键时从同一个位置采的灰度均值15 的阈值能容忍正常的缓慢光线漂移又能及时响应灯光切换。5.3 坑三静止手势触发键盘连发现象只比了 3 根手指没有动空格键被不停触发翻页根本停不下来。原因代码没有防抖。triggerSwitch为 True 后只要 cnt 3每帧约 33 毫秒都执行一次keybd_event每秒触发 30 次按键。这在操作系统层面是合法的但目标程序会认为你按住空格没松手。解决加入冷却时间我自己用的是 0.5 秒阈值在前文有过代码。这里再补充一点如果你希望手势保持时只触发一次还可以加状态锁prev_cnt 0 if cnt ! prev_cnt and cnt 3: # 触发按键 prev_cnt cnt逻辑是只有手指数发生变化时才触发手势保持不动则不产生新事件。这个方案适合「比 3 翻页、比 2 返回」的场景因为每次换手势才需要一次按键。如果同一个手势要连续触发多次比如比 3 连续翻多页冷却时间方案更合适。5.4 坑四mask 窗口全是黑的现象按 b 键之后mask 窗口没有任何白色区域binary 窗口里也看不到手。原因多半是背景建模时手已经在画面里。MOG2 建模时如果手是静止的它会被当成背景的一部分你把手拿走再比手势背景模型里那个位置是手的灰度值而新进来的手也是那个灰度值差分结果为零。另一个常见原因是cap_region_x_begin0.5你的手在画面左侧压根不在检测区域内。解决第一步按 b 之前把手从摄像头画面里移开等 1 秒再按第二步确认手在画面右侧的红色矩形框内第三步如果还是黑的按 r 重置再重新按 b多试两次。还不行就把 threshold 滑条往左拖降低到 30 左右让更多的灰度差异被识别为前景。5.5 坑五同事的电脑上跑不起来现象代码在自己的机器上一切正常换一台电脑报错ModuleNotFoundError: No module named win32api或者摄像头一直黑屏。原因win32api 是 pywin32 库提供的不是 Python 标准库也不是 OpenCV 自带的。pip install pywin32不装这个程序在 Windows 上就跑不了。摄像头黑屏则可能是电脑自带摄像头被占用比如 Zoom、微信等软件正在使用摄像头VideoCapture(0)抢不到资源。解决在项目文件头部加一个依赖检查try: import win32api import win32con except ImportError: raise SystemExit(Missing pywin32, run: pip install pywin32)摄像头被占用的问题关掉所有可能占用摄像头的软件然后重启 Python 进程。如果你用的是外接摄像头把VideoCapture(0)改成VideoCapture(1)设备索引从 0 开始0 通常是内置摄像头1 是第一个外接摄像头。设备忙时 OpenCV 不会报错只是ret为 False可以在camera.read()后面加个判断ret, frame camera.read() if not ret: print(Failed to read from camera, check if it is occupied) continue6. 让指尖检测更准的验证方法单步回放与边界数据整个程序最让我头疼的不是算法本身而是「感觉不对但说不清哪里不对」。后来我养成一个习惯不管项目多小都要给自己留一条单步验证的路。具体做法是把摄像头输入换成本地视频或图片序列这样才能复现同一组手势。做法很简单把camera cv2.VideoCapture(0)换成camera cv2.VideoCapture(test.avi)代码其他部分不用改。本地视频不受光线、手的位置影响同一帧跑到那里检测结果应该完全相同。如果两次跑同一帧结果不一样那一定是程序里有隐式状态泄漏。验证手指数目有个更系统的办法录制 5 段短视频每段固定比 1 到 5 根手指每段 10 秒。跑完程序后统计每帧输出的 cnt 分布看这 10 秒内 dominant出现最多的手指数是不是和手势一致。比如比 3 的手指理想输出是大部分帧 cnt 等于 3偶尔跳到 4 或 2 属于正常如果超过一半的帧都不在 3 上就要回头调 threshold 或者 blurValue。我一般会用一个小脚本统计from collections import Counter counts Counter(all_cnts) total_frames len(all_cnts) accuracy counts[expected_fingers] / total_frames print(faccuracy: {accuracy:.2%})把准确率跟 80% 这个基准比高于 80%这个手势可以实际使用低于 80%先别急着上键盘模拟回去调参。影响准确率的因素按权重排序第一是光照环境一致的条件下能显著提精度第二是 blurValue核太大会把相邻手指的边缘糊在一起第三是 threshold它决定手指边缘的完整度第四才是距离法的 count 参数。另一个简洁有效的验证法是画轮廓点轨迹。把每一帧的指尖坐标存进列表跑完一组手势后一次性画出来你会直观地看到指尖检测的稳定性。指尖应该是几个清晰的簇而不是一大片散点import matplotlib.pyplot as plt xs [p[0] for p in tip_history] ys [p[1] for p in tip_history] plt.scatter(xs, ys, s1) plt.show()指尖坐标如果散得像噪点说明轮廓不稳定优先检查高斯模糊核和 MOG2 的前景阈值而不是去调指尖检测逻辑。如果簇很清晰但位置漂移那多半是摄像头自动白平衡在起作用可以考虑用camera.set(cv2.CAP_PROP_WHITE_BALANCE_BLUE_U, 0)禁用自动白平衡。从那以后我每次拿到这类视觉检测项目都会强制自己先录一段固定视频再跑不把时间浪费在「这次手放的位置跟上次不一样」这种不可控变量上。这个方法帮我在这个项目里至少省出了半天调试时间希望你也能用它少走弯路。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进