简介这份资源面向计算机视觉入门与进阶学习者聚焦视频场景下的小球目标检测与颜色分类任务适合希望理解传统图像处理流程、又不想从零搭建环境的开发者。包内共3个文件包含1个Python主程序、1张效果预览图和1段测试视频压缩包约320KB体量轻巧简单配置OpenCV环境即可直接运行。代码由作者独立编写并亲测调试通过核心运用形状轮廓检测与色彩模型完成小球的定位与颜色判别输出效果类似YOLO算法检测后的标注框便于直观对比传统方法与深度学习方案的差异。目前已有1871人学习下载说明该案例在入门实践中具有一定参考价值。读者可借此掌握视频逐帧读取、轮廓筛选、颜色空间转换与结果可视化等关键环节并基于现有脚本快速替换素材、调整阈值参数形成自己的检测小项目。1. 用 opencv-python 做视频小球及颜色检测从能跑到跑稳的那条线很多人第一次用 opencv-python 做视频小球及颜色检测代码不到五十行就能在屏幕上画出一个框于是觉得这事已经结束了。真正拿到现场去用才发现问题全在后面换个灯光颜色就飘球滚快一点框就丢背景里出现一件相近颜色的衣服检测结果直接跑偏。这个标题要解决的不是「能不能检测到」而是「在真实视频流里能不能稳定地检测到并且把颜色判对」。它适合两类人一类是刚学完 OpenCV 基础、想找一个完整小玩意练手的开发者另一类是已经写过一版、但被误检和抖动折磨过、想把方案做扎实的工程师。整条链路其实就四步读视频帧、转到合适的颜色空间、按颜色阈值生成掩膜、在掩膜上找轮廓并判定小球。听起来简单但每一步都有参数和边界下面按能复现的顺序拆开讲。2. 颜色空间与阈值为什么 HSV 比 RGB 更适合做小球检测2.1 RGB 做阈值检测的三个硬伤用 RGB 直接卡阈值最直观的问题是三个通道耦合太紧。一个红色小球在强光下R 通道可能到 255G 和 B 也被环境光抬起来在阴影里R 掉到 120G 和 B 跟着掉。你没法用一组固定的 R、G、B 范围同时覆盖亮部和暗部因为亮度变化会同时改变三个通道的值而不是只改一个。第二个硬伤是光照色温。同一颗球白炽灯下偏黄日光灯下偏青RGB 三个通道的比例整体漂移阈值区间要跟着重调。第三个硬伤是阈值调试没有直觉。你看到球是红色但红色对应的 R 下限该设 180 还是 200G 上限该设 80 还是 100全靠试试出来的参数换一个场景就废。HSV 把颜色信息H 色调和亮度信息V 明度拆开了。色调描述「这是什么颜色」明度描述「有多亮」饱和度描述「颜色有多纯」。调阈值时你可以先固定 H 的范围把颜色框住再用 S 和 V 的范围把过暗、过曝、发灰的像素排除掉。这个拆分让参数有了物理含义也让同一套 H 范围在不同光照下更容易复用。2.2 用 opencv-python 把 BGR 帧转成 HSV 并生成掩膜下面这段代码是整条链路的核心读一帧、转 HSV、按阈值生成二值掩膜。注意 OpenCV 读进来的默认是 BGR 顺序不是 RGB转换时用cv2.COLOR_BGR2HSV。import cv2 import numpy as np cap cv2.VideoCapture(0) # 0 表示默认摄像头也可换成视频文件路径 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 红色在 HSV 里跨了 0 和 180 两端所以要分两段 lower_red1 np.array([0, 120, 70]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([170, 120, 70]) upper_red2 np.array([180, 255, 255]) while True: ret, frame cap.read() if not ret: break # 高斯模糊压噪点核必须是奇数 blurred cv2.GaussianBlur(frame, (5, 5), 0) hsv cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) mask1 cv2.inRange(hsv, lower_red1, upper_red1) mask2 cv2.inRange(hsv, lower_red2, upper_red2) mask cv2.bitwise_or(mask1, mask2) # 形态学开运算去小白点闭运算补内部空洞 kernel np.ones((5, 5), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) cv2.imshow(mask, mask) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明先模糊再转 HSV模糊放在前面是因为在 BGR 上做高斯模糊计算量比在 HSV 上小而且能减少传感器噪点对色调的干扰。inRange对每个像素做区间判断落在区间内输出 255否则输出 0得到的就是二值掩膜。红色分两段是因为 HSV 的 H 通道在 OpenCV 里范围是 0 到 180红色绕了一圈0 到 10 和 170 到 180 都是红。参数说明lower_red1里的 120 是饱和度下限低于它的像素被认为是发灰的不算红色70 是明度下限低于它的像素太暗也不参与判定。这两个值不是固定的光照弱的时候明度下限要往下调比如调到 50光照强、反光严重的时候饱和度下限要往上提比如提到 150否则白色高光会被误判成红色。高斯核(5, 5)适合 640x480 的分辨率分辨率更高时可以用(7, 7)但核越大边缘越糊小球边界会变钝。2.3 阈值到底怎么调一套可复用的标定流程不要凭感觉拖滑块。我一般用下面这个流程把阈值定下来。第一步把小球放在实际使用场景里截一帧保存成图片。第二步用取色工具在球体上取五个点记录 H、S、V 值取 H 的最小值和最大值作为 H 范围的初值S 和 V 取五个点里的最小值再往下留 20 的余量。第三步把背景里最接近球色的区域也取五个点看它们的 H 值是否落在你定的范围内如果落进去了说明 H 范围太宽要收窄。第四步把阈值代进上面的代码看掩膜里球体是不是一个完整的实心块背景是不是全黑。第五步在视频流里跑一遍观察球从亮处滚到暗处时掩膜有没有断裂断了就把 V 下限往下调。提示调阈值时把掩膜窗口和原图窗口并排看比只看检测结果框要快得多因为你能直接看到哪些背景像素被误判了。3. 从掩膜到小球轮廓筛选与圆心定位的工程做法3.1 轮廓提取与面积筛选掩膜出来之后下一步是找轮廓。cv2.findContours在 OpenCV 4.x 里返回两个值轮廓列表和层级信息。拿到轮廓后不能直接用因为掩膜里可能有噪点形成的小轮廓也可能有球体反光造成的断裂轮廓。筛选逻辑是先按面积过滤掉太小的再按圆度过滤掉形状不对的。contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area cv2.contourArea(cnt) if area 300: # 面积太小大概率是噪点 continue perimeter cv2.arcLength(cnt, True) if perimeter 0: continue circularity 4 * np.pi * area / (perimeter * perimeter) if circularity 0.6: # 圆度太低不是球 continue (x, y), radius cv2.minEnclosingCircle(cnt) center (int(x), int(y)) radius int(radius) cv2.circle(frame, center, radius, (0, 255, 0), 2) cv2.circle(frame, center, 3, (255, 0, 0), -1)逻辑说明RETR_EXTERNAL只取最外层轮廓避免球体内部纹理产生嵌套轮廓。CHAIN_APPROX_SIMPLE压缩轮廓点省内存。圆度公式4πA/P²在完美圆形时等于 1越接近 1 越圆。minEnclosingCircle返回能包住轮廓的最小圆的圆心和半径比boundingRect更适合圆形目标因为球在画面里旋转时外接矩形会变但最小外接圆基本不变。参数说明面积下限 300 是针对 640x480 分辨率、球直径大约 30 像素的情况。如果你的球更小比如直径 15 像素面积下限要降到 80 左右球更大就往上提。圆度下限 0.6 是一个偏宽松的值因为运动模糊和掩膜边缘锯齿会把圆度拉低设到 0.8 以上容易丢检。如果场景里没有其他圆形干扰物可以降到 0.5 提高召回。3.2 多帧稳定用轨迹连续性压掉单帧误检单帧检测再准视频里也会偶尔冒出一帧误检。解决办法是利用帧间连续性小球在相邻帧之间的位置不会跳变太大。我一般维护一个简单的轨迹缓存记录最近几帧的圆心如果当前帧检测到的圆心和上一帧的距离超过阈值就认为这一帧不可信用上一帧的位置代替或者直接跳过。prev_center None max_jump 80 # 相邻帧圆心最大允许位移单位像素 # 在检测循环内部 if prev_center is not None: dx center[0] - prev_center[0] dy center[1] - prev_center[1] if (dx * dx dy * dy) ** 0.5 max_jump: center prev_center # 跳变太大沿用上一帧 prev_center center逻辑说明max_jump的取值和帧率、球的实际运动速度有关。30 帧每秒、球在画面里每秒移动不超过 200 像素时相邻帧位移大约 7 像素设 80 已经很宽松能容忍偶尔的检测延迟。如果球速很快这个值要相应放大但放得太大就失去了过滤误检的作用。参数说明这个方案假设画面里只有一个球。如果有多球需要先做轮廓匹配再各自维护轨迹复杂度会上一个台阶。单球场景下这个几行的缓存就能把误检率压下去一大截属于性价比很高的做法。3.3 颜色判定从掩膜反推小球颜色检测到小球位置之后颜色判定有两种做法。第一种是直接用你设定的阈值颜色作为结果因为掩膜就是按那个颜色生成的这适合已知球色的场景。第二种是从原图里取圆心周围一小块区域算 HSV 均值再映射到颜色名称这适合球色会变的场景。hsv_roi hsv[max(0, center[1]-10):center[1]10, max(0, center[0]-10):center[0]10] h_mean np.mean(hsv_roi[:, :, 0]) s_mean np.mean(hsv_roi[:, :, 1]) v_mean np.mean(hsv_roi[:, :, 2]) if s_mean 50 or v_mean 40: color_name unknown elif h_mean 10 or h_mean 170: color_name red elif h_mean 35: color_name yellow elif h_mean 85: color_name green elif h_mean 130: color_name blue else: color_name purple逻辑说明取圆心周围 20x20 的区域做均值比取单像素稳因为单像素可能落在高光或阴影上。先判断饱和度和明度太低就归为 unknown避免把灰色或黑色误判成某种颜色。H 的分段是按 OpenCV 的 0 到 180 范围划的红色跨两端所以单独处理。参数说明ROI 大小 20x20 适合半径 15 像素以上的球球更小就缩小到 10x10否则 ROI 会超出球体范围把背景颜色算进来。饱和度下限 50 和明度下限 40 是经验值实际用的时候对着灰色物体跑一遍看会不会被误判会就往上提。4. 避坑与排查小球检测翻车的五个真实原因4.1 掩膜里球体中间有空洞现象掩膜显示球体是一个环中间是黑的导致轮廓面积偏小甚至断裂。原因球体表面有高光反光高光区域饱和度骤降被饱和度下限过滤掉了。解决把饱和度下限从 120 降到 80 左右或者对掩膜先做闭运算再找轮廓闭运算的核可以设大一点比如(7, 7)。如果高光特别严重考虑在镜头前加偏振片这是硬件层面的解法。4.2 背景里相近颜色物体被误检现象画面里出现一件红色衣服或红色包装盒检测框同时出现在球和背景物体上。原因H 范围设得太宽把相近色调都包进来了。解决收窄 H 范围同时提高饱和度下限。另一个办法是加面积上限球在画面里的面积不会超过某个值超过就排除。还可以用位置约束如果球只在画面中间区域活动就把边缘区域的轮廓直接忽略。4.3 球快速滚动时检测框闪烁或丢失现象球慢慢滚的时候检测稳定一加速就丢帧。原因运动模糊导致球体边缘糊掉掩膜面积缩小低于面积下限被过滤。解决降低面积下限同时把高斯模糊的核从(5, 5)降到(3, 3)减少额外的模糊。如果相机支持提高快门速度比调参数更有效。另外前面提到的轨迹缓存能在这时候兜底丢一两帧不影响整体。4.4 换一个房间就要重调阈值现象在实验室调好的参数搬到走廊就失效。原因色温变了HSV 里的 H 值整体偏移。解决不要指望一套参数打天下。我的做法是准备两组参数一组偏暖光一组偏冷光启动时用画面里已知的白色区域做一次白平衡校正或者干脆在代码里加一个按键切换参数组的逻辑。更彻底的做法是用自适应阈值但这会引入额外复杂度小球检测这个场景不太值得。4.5 帧率越跑越低现象刚开始跑很流畅跑几分钟后画面卡顿。原因每帧都在做全图inRange和形态学操作分辨率高的时候计算量不小另外imshow开太多窗口也会拖慢。解决把分辨率降到 640x480 或更低检测够用就行。形态学操作的核不要超过(7, 7)。如果只是调试把掩膜窗口关掉只显示原图和检测结果。还有一个容易忽略的点cap.read()返回的帧如果不做处理直接丢弃缓冲区会堆积可以设置cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)减少缓冲。5. 进阶技巧用颜色检测做多球追踪与性能压榨单球跑通之后往上走有两个方向。一个是多球追踪一个是性能优化。多球追踪的难点不在检测而在匹配这一帧检测到三个球上一帧也有三个球怎么知道哪个是哪个。简单做法是按圆心距离做最近邻匹配给每个球维护一个 ID新出现的球分配新 ID连续几帧没匹配上的 ID 回收。这个逻辑用字典就能实现不需要引入复杂的追踪库。tracks {} # id - center next_id 0 def match_tracks(detections, tracks, max_dist60): global next_id used set() for det in detections: best_id, best_dist None, max_dist for tid, tcenter in tracks.items(): if tid in used: continue d ((det[0]-tcenter[0])**2 (det[1]-tcenter[1])**2) ** 0.5 if d best_dist: best_dist, best_id d, tid if best_id is not None: tracks[best_id] det used.add(best_id) else: tracks[next_id] det next_id 1 return tracks逻辑说明对每个检测结果在已有轨迹里找距离最近且没被占用的匹配上就更新位置匹配不上就新建轨迹。max_dist控制匹配半径太小会导致 ID 频繁切换太大会把两个靠近的球合并成一条轨迹。实际用的时候球之间最近距离要大于max_dist才不会串。性能优化方面最有效的一招是降低检测频率。不需要每帧都做颜色检测可以每三帧检测一次中间帧用轨迹预测补位。预测用简单的匀速模型就够当前位置加上上一帧的位移。这样 CPU 占用能降一半以上对树莓派这类设备很关键。另一招是把 ROI 限制在上一帧检测框周围的一小块区域只在这个区域里做inRange而不是全图。球运动连续的时候这个做法能把计算量再压一个数量级。注意ROI 限制的前提是球不会突然消失再出现在远处。如果场景里有遮挡导致球消失几帧ROI 会跟丢这时候要退回到全图检测重新捕获。最后说一个我自己的习惯每次调完参数把当前场景截一帧存下来参数写在文件名里。下次换场景翻车的时候回头看看之前哪些参数在什么光照下work过比重新试一遍快得多。这个方案从代码量看是个小玩意但参数和边界的坑一个不少把上面这些跑一遍基本就能从「能跑」走到「跑稳」了。希望帮到你。本文还有配套的精品资源点击获取