核心课程 / 视觉入门课 / EXP-01
EXP-01 · 第 1 阶段 · 传统视觉 · 验证性 · 4 学时 · 最简单的实验

颜色识别:不用 AI 也能「认出」颜色

这是整门课的第一个实验,而且特意选了最简单的:一行 AI 代码都没有。只有一条数学规则 —— 把颜色换算成一个角度,落在范围内就要,不在就丢。先在这个「看得见摸得着」的规则里建立起手感,再往下才碰神经网络。

难度 ⭐ · 4 学时 · 全课起点 HSV 色彩空间阈值掩膜 形态学开闭最小外接圆约 33 FPS
01 颜色02 轮廓03 二维码04 检测05 分类06 分割07 跟踪08 人脸09 骨骼10 3D11 体感12 手势

01目标:红/绿/蓝色块被圈出,打印颜色名

🎯 实验目标 · 学完你将能
  • 说出 HSV 三通道各管什么,并解释为什么它比 RGB 更抗光照变化
  • 用 cv2.inRange 对指定颜色建掩膜,跑通「相机 → 圈出目标」全流程
  • 用 open_kernel / min_area 消掉噪点,调出稳定不抖的颜色框
本实验的目标:相机对着桌面,红/绿/蓝三种色块被各自圈出来,左下角还打印出颜色名字。

02问题:机器怎么找到红色,和人一样吗

👆 点击画面,切换「像素格」视图 —— 每一个小方格就是一个像素

画面很简单:正中间一块红色方块 —— 人一眼就能指出它在哪,机器呢?

同一个画面,人和机器「找到红色」的方式完全不同:

对比人机器
靠什么眼睛看摄像头采集的 RGB 彩色画面
怎么判断「这是红的」一眼看过去,大脑直接给出结论? —— 画面切成一个个像素后,每格都带一组 RGB 数字,怎么把红色挑出来
怎么给位置说出大体位置:「大概在画面中间偏左」? —— 能给到多精,够不够直接拿去用

机器这边,问题一个接一个(后面几节逐一给答案):

① 怎么找。人靠眼睛,机器手里只有摄像头采回来的一幅 RGB 彩色画面 —— 上图点一下就能看到,它其实由密密麻麻的小方格拼成,每格叫一个像素、各自带一组 RGB 数字。怎么从这些像素里把红色挑出来?

② 画面暗了还找得到吗。光线一暗,红色就不那么明显,每个像素的 RGB 数字全都跟着变 —— 方法会不会一暗就失灵?

③ 找到了,输出的又是什么。会像人一样只给一个「大概在中间」的模糊位置吗?这样的位置,能用于后续的工业检测、或者机械臂的精准抓取吗?

先看个对比,你会发现这个方法为什么这么有用:

方案要不要训练速度适用场景
深度学习检测要(几小时 GPU)十几 FPS形状复杂、需要分出类别的东西
颜色阈值不要,只要调几个数约 33 FPS目标颜色鲜明、背景不冲突的场合

只要你要找的东西是「一块颜色」,比如:红色的小球、蓝色的积木、绿色的巡线胶带 —— 那就别上神经网络,一个阈值又快又省事,而且你能完全解释它为什么这么判。

03现象:红积木套上绿圈,灰纸片没有任何圈

🎬 现象速览 · 30 秒先看效果

你会看到:镜头前摆着一块红色积木和一块蓝色积木 —— 红积木被绿圈稳稳套住,蓝色积木被标成 BLUE,两块各认各的颜色、互不冒认;放块灰色纸片进来 —— 没有任何圈。

EXP-01 运行结果三联画面:RGB 轮廓叠加、识别信息、HSV 掩膜
实际运行结果(三联画面):左 = 原始摄像头画面,中 = 叠加绿色检测框与颜色名的识别结果,右 = HSV 掩膜 —— 白块即被判为目标颜色的像素,黑块被丢弃

三步跑出这个画面(先照做,每步为什么下一节拆讲):

① 终端 1 · 起相机。前两条 source 把 ROS2 系统环境和本工作区环境装进当前终端,缺任何一条,后面的命令都会提示找不到;export ROS_DOMAIN_ID=N 把节点通信频道拨到 N——N 是 0~101 里的任一个数,每台设备各选各的、别都设成同一个值(这样既避开默认频道上别人的节点,两台机器也不会撞号);最后一条 ros2 launch 拉起相机驱动。日志开始滚动、没有红字报错就是起来了。

source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash
export ROS_DOMAIN_ID=N
ros2 launch ascamera hp60c.launch.py

② 终端 2 · 跑识别。cd 进本实验的源码目录;python src/app.py 启动识别节点,--ros 表示以 ROS 模式收发图像,不带它就是单机模式。终端打印出 http://…:8080 开头的地址就算成功。

cd ~/ros2_ws/src/xflz_eai_car1/vision/03_lab_color_recognition
python src/app.py --ros

③ 浏览器 · 看流。把 <板IP> 换成实验板的实际 IP(上一步的启动日志里会打印局域网地址),8080 是流服务固定端口,lab_color.mjpg 是识别结果的视频流。打开后拿红色、绿色、蓝色积木逐个放到镜头前。

http://<板IP>:8080/lab_color.mjpg

🔍 看什么:绿圈是不是只套住目标颜色、绝不越界到别的颜色上?手在镜头前晃过,圈依然认「颜色」不认「形状」。把积木挪到阴影里,圈会发抖、变碎 —— 记住这个现象,下一节讲的 HSV 就是为它准备的解药。

这就是本实验最终要呈现的现象。此时照做、先看懂它就行;第 05 节会按代码框架 → 流程 → 步骤带你亲手跑出同一个画面 —— 跑出来、对得上,实操才算完成。

04原理:为什么换了 HSV 就认得出(四步动画)

CORE IDEA · 一句话原理

把每个像素的色相(H)拿来和范围比一比,落在区间内就留下,否则变黑。

打个比方:就像戴一副只透红光的滤色镜 —— 红的照样红,别的全变黑。电脑干的事一模一样,只是它逐像素判断,一秒能判 30 万个。

整个过程只有四个动作:换颜色尺子 → 比范围 → 去噪点 → 画圈。读完就能理解全部代码。

① 相机原图(BGR) 有红绿蓝,也有灰背景 ② 换成 HSV 尺子 H: 0 ──────── 179 S: 饱和度 0-255 V: 明度 0-255 色相变成「角度」了 这一步只是换了个坐标系 cv2.cvtColor() ③ 比范围 → 黑白掩膜 白 = 范围内像素 | 小白点 = 噪声 cv2.inRange() ④ 去噪点 + 画圈 形态学先清掉小白点 cv2.morphologyEx() cv2.findContours() 全程没有神经网络:只有换算、比较、去噪、画图 —— 所以能跑到 30+ FPS
图 3-1|四步走完。注意第③步:掩膜是一张黑白图,白色表示「这个像素的颜色符合要求」;边缘散落的小白点是噪声 —— 正是第④步形态学要清掉的对象。

四步 ↔ 代码:图上每一步,对应哪个函数

把图上的每一格对回代码 —— 知道哪一步是哪个函数、配置里哪个参数在管它、参数一动画面哪块会跟着变:

步图上的效果函数关键参数调整对画面的影响
②彩图换成 HSV 尺子cv2.cvtColor(…, COLOR_BGR2HSV)无(固定转换)只是换坐标系,没有可调项
③黑白掩膜:白 = 符合、黑 = 丢弃(含小白点噪声)cv2.inRange(lower, upper)h / h2、s、v收窄 → 目标缺块、甚至整块变黑;放宽 → 背景杂物也变白(橙棕桌面被圈进来就是它太宽)
④-1小白点被清掉cv2.morphologyEx(开/闭运算)open_kernel、close_kernel调大 → 噪点清得干净,但目标边缘被啃、小目标整块消失;调小 → 白点残留,框跟着抖
④-2白块变稳定的绿圈cv2.findContours → cv2.minEnclosingCirclemin_area、min_radius~max_radiusmin_area 调大 → 目标也会被滤掉、没框;调小 → 噪点也画圈

这些参数都写在 config/lab_config.yaml;参数动一格画面怎么变,下方面试台可以拨着看,到第 05 节还能用 --tune 实时拖滑杆亲手试。

静态图看完了,这里放了一台可以把四步拨着玩的实验台:点顶部步骤切换,拖下面两根滑杆挪动圆和方,看标签、掩膜和最终读数怎么跟着变。

为什么不能直接用 RGB?

这是初学者最常问的问题,也是最关键的一点:

用 RGB 的问题
同样是红色,数值差很多

亮处的红:(255,30,30)
暗处的红:(120,15,15)
你没法用一组固定的范围同时框住它们。因为 RGB 把「亮不亮」和「什么颜色」混在一起了。

→
HSV 的好处
颜色和亮度分开了

亮处的红 ≈ H=0, S=230, V=255
暗处的红 ≈ H=2, S=225, V=120
H 几乎不变!只比 H 就能认出来。

通道含义取值范围一句话
H 色相是什么颜色0 ~ 179(注意不是 360)红色 0 附近,绿色 60 附近,蓝色 120 附近
S 饱和度颜色有多纯0 ~ 2550 = 灰白,255 = 纯色
V 明度有多亮0 ~ 2550 = 全黑,255 = 最亮
⚠️ 为什么 H 最大是 179 不是 360?OpenCV 为了把 H 塞进 8 位整数(0-255),把 0-360 度除以了 2。所以你算出来的角度要 ÷2 才能用。这是 OpenCV 的历史包袱,几乎所有初学者都在这栽过。

第 ④ 步到底在做什么:从掩膜到「一个圆」

上面四步图里第④步只写了「去噪点 + 画圈」,但代码其实做了三件事,缺一件结果就不对。这一步是初学者最容易「看懂了图、读不懂代码」的地方:

顺序函数干什么少了它会怎样
1cv2.findContours把白色区域连成一圈圈边界,得到「轮廓点集」的列表你只有一堆像素,不知道自己有几块目标
2cv2.minEnclosingCircle对每个轮廓求最小外接圆,把上千个边界点压成 (圆心, 半径) 两个数数据量下不来,也没法算「球心在哪」
3按面积/半径过滤,取最大的那个滤掉小噪点、过滤过大的误检,只留每种颜色一个目标一个颜色会框出十几个小圈,画面乱跳

为什么用最小外接圆而不是外接矩形?因为本实验的目标是球形(红/蓝/绿小球)。外接矩形会随球的旋转方向变化,面积和中心都会抖;外接圆的圆心和半径对旋转完全不变,而且圆心天然就在球心上 —— 这正是后面要做抓取定位时需要的。

过滤参数怎么理解 · min_area 500 是「小于 500 个像素的区域当噪点丢掉」;min_radius 10 / max_radius 400 是「半径小于 10 或大于 400 的圆都不要」。三个数都要按实际画面标定 —— 球拿远了面积变小,拿近了面积变大,阈值太紧就会时有时无。
🔍 为什么每种颜色只留最大的一个?因为本实验的假设是「画面里每种颜色只有一个主目标」。这换来的是稳定的输出(不会有多余的分数)。如果需要数「红球有几个」,就要改成保留全部合格轮廓 —— 这正是 进阶 1 的任务。
核心记忆点:HSV = 「什么颜色 / 有多纯 / 有多亮」三个独立旋钮。而 RGB 是三个混在一起的旋钮,拧一个会连着影响另外两个。

先认识 4 个词

色彩空间

描述颜色的「坐标系」。同一个颜色可以用 RGB 描述,也可以用 HSV 描述,两者可以互相换算。

掩膜 mask

一张和原图一样大的黑白图。白色 = 保留这个像素,黑色 = 丢掉。所有「挑出某类东西」的操作都靠它。

形态学

对黑白图做的小整理。开运算去掉孤立小白点,闭运算填掉黑色小洞。

轮廓 contour

把白色区域连成一条边界。有了边界就能算面积、中心、画圆 —— 从「图」变成「数据」。

开运算和闭运算到底在干什么?选读 40 秒

两个都是用一个小方格(叫「核」,比如 5×5)在图上滑动:

开运算(先腐蚀再膨胀):让白色区域先瘦一圈再胖回来。瘦的过程中,小到撑不住的白点就消失了,大块区域恢复原样。用来去噪点。

闭运算(先膨胀再腐蚀):反过来,先胖一圈再瘦回来。小的黑洞就被填满了。用来补目标内部的空洞。

核必须是奇数(3、5、7、9),因为要有个唯一中心点。偶数核的锚点位置不明确,OpenCV 会报错。配置里 open_kernel: 5 就是 5×5 的椭圆核。

为什么本实验用椭圆核而不是方块核?因为目标是圆的。方块核在对角方向会引入「方角」效应,把圆形轮廓啃出棱角;椭圆核各个方向的作用距离一致,对圆形目标更友好。代码里就是 cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))。

它吃什么、吐什么

INPUT · 喂进去
相机画面 + 三组 HSV 范围

范围写在 config/lab_config.yaml 里,红/绿/蓝各一组。

→
OUTPUT · 吐出来
双栏画面 + 颜色名

左栏:原图 + 绿色轮廓 + 黄色外接框 + 红色中心十字 + 颜色名。
右栏:黑白掩膜,能直接看到「它认为哪里是这个颜色」。

配置里红色那一段有个特殊写法,一定要理解:

colors:
  - name: 红色
    h: [0, 15]        # 第一段:0~15
    s: [100, 255]
    v: [80, 255]
    h2: [160, 180]    # ← 第二段!红色跨了 0 的边界
🔍 为什么红色要写两段?因为色相是个圆环:0 度和 180 度其实是同一个位置(红色)。如果红色范围从 160 跨过 0 到 15,写成一个区间 [160, 15] 在程序里是「下限比上限大」,一个都选不出来。所以必须拆成 [0,15] 和 [160,180] 两段,分别算完再按位或合并。只有红色(以及最靠近红的品红/粉)需要这样处理 —— 其他颜色的色相区间不跨 0 点,一段就够。

范围是闭区间:h2: [160, 180] 把 H=180 也包含在内。虽然 OpenCV 实际取值到 179,但写 180 更贴近真实物理边界(色相环的 360° 处),语义更清楚。
⚠️ 两段之间有个「死区」H 在 16~159 之间的像素两段都不匹配,直接判为「不是红色」。这不是 bug —— 橙(H≈15)、黄(H≈25)、绿(H≈60)、青(H≈90)、蓝(H≈120)本来就不该算红色。如果你发现偏橙的红球认不出,就把 h 上限从 15 提到 20 左右。

05实操:三条命令:实时 / 双栏 / 滑块调参

  1. 进目录,读一眼阈值配置约 2 分钟
    cd ~/ros2_ws/src/xflz_eai_car1/vision/03_lab_color_recognition
    cat config/lab_config.yaml
    # 注意:目录名里的 "lab" 是「实验」的缩写 # 不是 Lab 色彩空间!这里用的是 HSV。 colors: - name: 红色 ... h: [0,15] h2: [160,180] - name: 绿色 ... h: [35,85] - name: 蓝色 ... h: [90,130] morph: open_kernel: 5 ← 去噪点的核大小 close_kernel: 9 ← 填孔洞的核大小 contour: min_area: 500 ← 小于这个面积当噪点丢掉 min_radius: 10 ← 圆太小不要 max_radius: 400 ← 圆太大不要

    这个实验没有模型文件,models/ 目录是空的 —— 因为全靠阈值,不需要 AI。

  2. 开相机,看双栏效果约 3 分钟

    终端 1 起相机(老三样:source + DOMAIN_ID + launch;N 取 0~101 任一整数,每台设备各选各的、别都设成同一个值):

    source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash
    export ROS_DOMAIN_ID=N
    ros2 launch ascamera hp60c.launch.py

    终端 2 跑颜色识别:

    cd ~/ros2_ws/src/xflz_eai_car1/vision/03_lab_color_recognition
    python src/app.py --ros
    Streams index: http://127.0.0.1:8080 http://127.0.0.1:8080/lab_color.mjpg ← 启动时会打印本机与局域网两个地址,用局域网那个 30帧平均帧率: 33.0 30帧平均帧率: 33.9 ← 稳定在 30~35 FPS 区间(纯 CPU,无 NPU)

    关于帧率:实测约 33 FPS。这个数比很多人预期的「传统视觉一定上百帧」低,原因是每帧要对三种颜色各做一次 inRange + 两次形态学 + 轮廓查找,而且 640×480 的 MJPEG 编码也要占时间。但 33 FPS 对视觉控制完全够用 —— 已经比实验4(13 FPS)和实验7(14 FPS)快一倍以上。

    浏览器打开 http://<板子IP>:8080/lab_color.mjpg,拿个红色的东西放到镜头前。

  3. 用滑块现场调阈值(最有意思的一步)约 5 分钟

    光照一变,原来的阈值可能就不准了。这个模式给你六个滑块(H/S/V 各上下限),边拖边看掩膜怎么跟着变:

    python src/app.py --tune 红色 --ros
    出现两个窗口: lab_tune ← 6 个滑块(H/S/V 的上下限) lab_view ← 左原图 | 右掩膜 Ctrl+C 退出时会打印当前阈值,方便抄进配置

    调参口诀:先拖 H 的上下限把目标圈住(其他颜色变黑),再调 S 下限把灰白背景筛掉(如果还需要),最后调 V 下限把太暗的阴影排掉。

    调好之后,把数值手工写回 config/lab_config.yaml 对应的颜色段落,再重跑 --ros 就是你的专属阈值了。

    ⚠️ 注意--tune 调出来的结果不会自动保存到配置文件里,Ctrl+C 只打印、不写回。要自己抄过去。
三个命令跑完,你就掌握了颜色识别的完整工作流:看效果 → 拖滑块 → 抄阈值 → 固化。这套动作在真实项目里天天用。
🔁 现象呼应这三步跑完出现的画面,就是第 03 节「🎬 现象速览」里的那个:红积木被绿圈圈住并打印颜色名。两处对上了,说明代码框架、流程、步骤你已完整走通。

你该看到的画面

窗口 · lab_view
左右两栏

左:原图,目标上叠着绿色轮廓、黄色外接框、红色中心十字,左下角打印颜色名。
右:黑白掩膜,白的就是它认定的目标区域。

掩膜正常
一块干净的白斑

目标的形状清晰可见,周围基本全黑。有零星小白点说明噪点多,需要加大开运算核或提高 min_area。

掩膜异常
目标破碎 / 大片全白

破碎 → H 范围太窄,或 V 下限太高把暗部切掉了;大片全白 → S 下限太低,把灰白背景也收进来了。

🔍 为什么板上用 --ros 而不是直接开摄像头?因为这块板子的 HP60C 相机走的是 ROS2 话题,不像普通 USB 摄像头能用 cv2.VideoCapture(0) 直接读(板载 OpenCV 未编译 libv4l 相关后端)。--camera 0 是给普通 USB 相机准备的回退方案 —— 在板子上打不开是正常的,不是环境坏了。

踩坑速查

现象原因怎么办
红色一个都选不出来红色跨 0 边界,只写了 h 没写 h2补上 h2: [160, 180],让两段分别算再合并
H 值算出来是 300 多用了 0-360 的角度制OpenCV 的 H 是 0-179,角度要除以 2
调参窗口拖了没反应板子上没有显示器,弹不出窗口--tune 需要显示器;SSH 远程时改用 --ros 看推流
调好的阈值重开又变回默认--tune 不自动写配置文件Ctrl+C 后把打印的值手工抄进 lab_config.yaml
换了光照就认不出阈值是「死」的,光照一强一弱像素值就飘现场重调;或用 auto_hsv_range 思路自动采样标定
--camera 0 打不开板上 HP60C 不走 UVC,读不了用 --ros;--camera 只对普通 USB 相机有效
形态学核报错核设成了偶数(比如 4)核必须是奇数:3 / 5 / 7 / 9
掩膜正常但左栏没有框轮廓被过滤掉了:面积或半径不在范围内调小 min_area / 放宽 min_radius~max_radius
💡 排查口诀先看右栏掩膜,再看左栏框。掩膜不对 = 阈值问题(调 H/S/V);掩膜对但没框 = 轮廓过滤问题(min_area 太大或 max_radius 太小)。这两类问题的处理方向完全不同,先分清能省一半时间。

进阶(可选)

进阶 1 · 加一种新颜色(比如黄色)约 20 分钟

在 config/lab_config.yaml 的 colors 列表末尾追加一段:

- name: 黄色
  h: [20, 35]
  s: [100, 255]
  v: [100, 255]

然后 python src/app.py --tune 黄色 --ros 微调范围。注意黄色和绿色的 H 区间相邻(黄约 20-35,绿约 35-85),如果分不清就把两边的边界错开一点。

进阶 2 · 把位置变成控制指令约 40 分钟

现在程序只知道「绿色在画面里」,不知道「绿色离画面中心偏多少」。但代码里已经算了中心点(那个红色十字)。

思路:算出中心点相对画面中心的横向偏差 dx = cx_ball − W/2,偏差为正就右转、为负就左转,|dx| 很小就直行。这就是最简单的视觉巡线。

# 概念示意
err  = (cx_ball - W/2) / (W/2)   # 归一化到 [-1, 1]
angular = -0.8 * err             # 目标在右 → 右转 → 角速度为负(ROS 约定逆时针为正)
linear  =  0.2 if abs(err) < 0.15 else 0.05   # 转得急就先慢一点

注意角速度符号:ROS 里 angular.z 正值是逆时针(左转)。目标在画面右侧时,车应该右转,所以要给负值 —— 这是初学者最容易搞反的一处。实验 11 会用 turtlesim 先验证方向再上真车。

下一个实验(EXP-02 多边形轮廓)会把这个思路完整走一遍。

进阶 3 · 为什么光照一变就失效,怎么救选读 1 分钟

根本原因:阈值是硬编码的常数。白天窗边光线和傍晚灯光下,同一个红球拍出来的像素值完全不同。

三个实用对策:

  • 固定光源:工业现场最常用的办法 —— 加个环形灯罩,光照恒定,阈值就永远有效。成本最低、最可靠。
  • 自动标定:开机时把一个已知颜色的参照物放到镜头前,程序自动算出当前阈值。适应能力好,但要多一步操作。
  • 改用深度学习:实验 4 之后那种模型天生对光照更鲁棒。代价是需要数据、要训练、跑得慢。值不值,看场景。

这三条没有对错,选哪条取决于你的现场条件。能固定光源就别上 AI —— 这是工程师的常识。

06互动:拖 H 范围,看掩膜怎么碎又怎么糊

📌 这一节是「掌握检查」:亲手改一个参数,看现象是否朝你预期的方向变,还要能说出为什么 —— 说得出,才算掌握。

下面这个实验台模拟一件事:H 的范围放宽或收窄,掩膜会怎么变。亲手拖一遍,胜过读十页文档。

⚡ 参数实验台 · H 范围的宽窄对掩膜的影响
35
85
上面一排是画面里存在的颜色(小标签写着它的 H 值) H ∈ [35, 85] → 保留 3 个 变亮 = 被掩膜保留,变暗 = 被丢弃

三种典型情况

现象原因怎么调
掩膜破碎,目标缺一块H 范围太窄,边缘的颜色被排除了把 H 上下限各放宽 5~10
大片背景都变白S 下限太低,灰白色也被当成「有颜色」把 s 下限从 100 提高到 130+
阴影处认不出来V 下限太高,暗处像素的明度不够把 v 下限从 80 降到 50
这就是传统视觉的本质:你告诉它「什么算红」,它就严格照办。它不会思考,但也绝不会骗你。这就是可解释性。

07验证:三道小测

✅ 目标对账 · 验证掌握得有多好

回页首对照「实验目标」逐条自检:① 能讲清 HSV 抗亮暗的原理 → 试 Q1;② 能说清 inRange 与红色双段阈值怎么写 → 试 Q2;③ 掌握噪点去除的手段 → 试 Q3。答不上的条目,回到对应小节补完再交卷 —— 三条全 ✓,这一课就完成了。

Q1同样是红色,亮处是 (255,30,30),暗处是 (120,15,15)。为什么用 HSV 就更好认?
因为 HSV 的数值范围更小
因为 H 通道把「是什么颜色」和「有多亮」分开了,两处的 H 几乎相同
因为 HSV 是深度学习专用的
对。RGB 三个通道混着亮度和颜色,亮暗一变三个数全变。HSV 里 H 只管「什么颜色」,亮暗主要影响 V —— 所以只比 H 就能认出同一个颜色。
Q2配置里红色写了 h: [0,15] 和 h2: [160,180] 两段,为什么?
为了跑得更快
红色横跨 0 度边界,一个区间无法表达,必须拆成两段
写错了,应该只写一段
对。色相是圆环,0 和 180 是同一个位置。真实的红色范围是 [160,180] ∪ [0,15],写成单个区间 [160,15] 程序会认为无解,一个都选不出来。
Q3掩膜右上角总有一堆零星小白点,框也一直跳。最有效的做法是?
把 H 范围调到只有 2 那么窄
加大开运算的核(open_kernel 从 5 改到 7~9),或提高 min_area
换一个模型
对。零星白点是噪点。开运算专门用来「去掉撑不住的小白点」,或直接用面积门槛把太小的轮廓滤掉。把 H 范围调窄会把真目标也一起弄丢。

08章节总结:HSV 换一把尺子,成败全在阈值和过滤

概念它解决了什么和别的关系
HSV 色彩空间
cv2.cvtColor
把「什么颜色 / 有多纯 / 有多亮」拆成三个独立旋钮,亮暗一变不再跟着改颜色读数上游是相机的 BGR 原图;换完尺子才轮到 inRange 比范围
阈值掩膜
cv2.inRange
逐像素比范围,白 = 保留、黑 = 丢掉,把「颜色」变成一张黑白图吃 h / h2 / s / v;吐出的掩膜喂给形态学
形态学开 / 闭
cv2.morphologyEx
开运算去掉撑不住的孤立小白点,闭运算填掉目标内部的黑色小洞吃掩膜、吐干净掩膜;open_kernel 5 / close_kernel 9 就是这一步的核
轮廓 → 最小外接圆
cv2.findContours → cv2.minEnclosingCircle
把上千个边界点压成「圆心 + 半径」两个数,才算得出球心在哪被 min_area 500 / min_radius 10 / max_radius 400 过滤;每种颜色只留最大的一个
绿圈 + 黄框 + 红十字 + 颜色名把「哪个像素是红的」变成「画面里这个球在哪、多大」下游:进阶 2 的 dx = cx_ball − W/2 直接拿圆心算转向;抓取定位拿圆心当球心
⚠ 反直觉的地方 ① 直觉是色相 H 是 0~360 的角度,实际是OpenCV 为了把它塞进 8 位整数除以了 2,H 只有 0~179,量出来的角度得 ÷2 才填得进配置; ② 直觉是红色就是一段区间,实际是色相是个圆环 —— 0 度和 180 度是同一个位置,红色必须拆成 h: [0,15] 和 h2: [160,180] 两段分别算再合并,写成 [160,15] 一个都选不出来; ③ 直觉是没框就是阈值没调好,实际是掩膜正常却没框,锅在轮廓过滤(min_area 太大或 max_radius 太小),跟 H 一点关系都没有 —— 所以口诀是「先看右栏掩膜,再看左栏框」。
💡 三条要注意 ① h2: [160, 180] 里的 180 是照色相环写的语义边界,OpenCV 实际取值只到 179;H 在 16~159 之间是两段都不匹配的「死区」,偏橙的红球认不出时把 h 上限从 15 提到 20 左右; ② 形态学核必须是奇数(3 / 5 / 7 / 9),写成 4 这种偶数直接报错;open_kernel 从 5 调到 7~9 清噪点更狠,代价是目标边缘被啃、小目标整块消失; ③ min_area 500 的单位是像素面积,min_radius 10 / max_radius 400 才是半径 —— 球拿远了面积变小、拿近了变大,三个数都要按实际画面标定,卡太紧就时有时无。

前置依赖 · 缺一样就跑不起来

类型具体项缺了会怎样 / 怎么补
环境source /opt/ros/humble/setup.bash + source ~/ros2_ws/install/setup.bash,再 export ROS_DOMAIN_ID=N缺任何一条 source,后面的命令都会提示找不到;N 取 0~101 任一个数,每台设备各选各的、别都设成同一个值
Python 库OpenCV(四步全靠 cv2:cvtColor / inRange / morphologyEx / findContours / minEnclosingCircle)缺了第一步就断。注意板载 OpenCV 未编译 libv4l 相关后端,所以 --camera 0 打不开是正常的,不是环境坏了 —— 用 --ros
模型 / 资源本实验不需要模型 —— models/ 目录是空的,全靠阈值—
配置config/lab_config.yaml:三组颜色范围(红 h: [0,15] + h2: [160,180]、绿 h: [35,85]、蓝 h: [90,130])与 morph / contour 两组参数红色少写 h2 就一个都选不出来;min_area 太大会「掩膜正常但左栏没框」
硬件 / 前置实验HP60C 相机 + ros2 launch ascamera hp60c.launch.py;--tune 那条命令要显示器 —— 板子上没显示器就弹不出窗口,SSH 远程改用 --ros 看推流相机没起 → 没有画面可认;没有前置实验,这是全课第一个实验

这套代码用在哪儿

  • 分拣线色块分选:相机对着传送带 → 每帧输出「颜色名 + 圆心 + 半径」,机械臂据此决定抓哪个、往哪条料道放 —— 外接圆的圆心天然就在球心上,正好是抓取定位要的那两个数。
  • 目标跟随 / 视觉巡线:拿圆心算 dx = cx_ball − W/2、归一化成 err,angular = -0.8 * err 直接当底盘角速度,|err| < 0.15 才给 0.2 的前进速度;这个思路 EXP-02 会完整走一遍。
  • 别拿它数个数:本代码每种颜色只留最大的一个 —— 要数「红球有几个」,得改成保留全部合格轮廓,这是进阶 1 的任务。
核心记忆点:HSV 只是换了一把尺子;真正决定成败的是 inRange 的两段范围、形态学的奇核、和轮廓那三个过滤数。