颜色识别:不用 AI 也能「认出」颜色
这是整门课的第一个实验,而且特意选了最简单的:一行 AI 代码都没有。只有一条数学规则 —— 把颜色换算成一个角度,落在范围内就要,不在就丢。先在这个「看得见摸得着」的规则里建立起手感,再往下才碰神经网络。
01目标:红/绿/蓝色块被圈出,打印颜色名
- 说出 HSV 三通道各管什么,并解释为什么它比 RGB 更抗光照变化
- 用
cv2.inRange对指定颜色建掩膜,跑通「相机 → 圈出目标」全流程 - 用
open_kernel/min_area消掉噪点,调出稳定不抖的颜色框
02问题:机器怎么找到红色,和人一样吗
👆 点击画面,切换「像素格」视图 —— 每一个小方格就是一个像素
同一个画面,人和机器「找到红色」的方式完全不同:
| 对比 | 人 | 机器 |
|---|---|---|
| 靠什么 | 眼睛看 | 摄像头采集的 RGB 彩色画面 |
| 怎么判断「这是红的」 | 一眼看过去,大脑直接给出结论 | ? —— 画面切成一个个像素后,每格都带一组 RGB 数字,怎么把红色挑出来 |
| 怎么给位置 | 说出大体位置:「大概在画面中间偏左」 | ? —— 能给到多精,够不够直接拿去用 |
机器这边,问题一个接一个(后面几节逐一给答案):
① 怎么找。人靠眼睛,机器手里只有摄像头采回来的一幅 RGB 彩色画面 —— 上图点一下就能看到,它其实由密密麻麻的小方格拼成,每格叫一个像素、各自带一组 RGB 数字。怎么从这些像素里把红色挑出来?
② 画面暗了还找得到吗。光线一暗,红色就不那么明显,每个像素的 RGB 数字全都跟着变 —— 方法会不会一暗就失灵?
③ 找到了,输出的又是什么。会像人一样只给一个「大概在中间」的模糊位置吗?这样的位置,能用于后续的工业检测、或者机械臂的精准抓取吗?
先看个对比,你会发现这个方法为什么这么有用:
| 方案 | 要不要训练 | 速度 | 适用场景 |
|---|---|---|---|
| 深度学习检测 | 要(几小时 GPU) | 十几 FPS | 形状复杂、需要分出类别的东西 |
| 颜色阈值 | 不要,只要调几个数 | 约 33 FPS | 目标颜色鲜明、背景不冲突的场合 |
只要你要找的东西是「一块颜色」,比如:红色的小球、蓝色的积木、绿色的巡线胶带 —— 那就别上神经网络,一个阈值又快又省事,而且你能完全解释它为什么这么判。
03现象:红积木套上绿圈,灰纸片没有任何圈
你会看到:镜头前摆着一块红色积木和一块蓝色积木 —— 红积木被绿圈稳稳套住,蓝色积木被标成 BLUE,两块各认各的颜色、互不冒认;放块灰色纸片进来 —— 没有任何圈。
三步跑出这个画面(先照做,每步为什么下一节拆讲):
① 终端 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 就认得出(四步动画)
把每个像素的色相(H)拿来和范围比一比,落在区间内就留下,否则变黑。
打个比方:就像戴一副只透红光的滤色镜 —— 红的照样红,别的全变黑。电脑干的事一模一样,只是它逐像素判断,一秒能判 30 万个。
整个过程只有四个动作:换颜色尺子 → 比范围 → 去噪点 → 画圈。读完就能理解全部代码。
四步 ↔ 代码:图上每一步,对应哪个函数
把图上的每一格对回代码 —— 知道哪一步是哪个函数、配置里哪个参数在管它、参数一动画面哪块会跟着变:
| 步 | 图上的效果 | 函数 | 关键参数 | 调整对画面的影响 |
|---|---|---|---|---|
| ② | 彩图换成 HSV 尺子 | cv2.cvtColor(…, COLOR_BGR2HSV) | 无(固定转换) | 只是换坐标系,没有可调项 |
| ③ | 黑白掩膜:白 = 符合、黑 = 丢弃(含小白点噪声) | cv2.inRange(lower, upper) | h / h2、s、v | 收窄 → 目标缺块、甚至整块变黑;放宽 → 背景杂物也变白(橙棕桌面被圈进来就是它太宽) |
| ④-1 | 小白点被清掉 | cv2.morphologyEx(开/闭运算) | open_kernel、close_kernel | 调大 → 噪点清得干净,但目标边缘被啃、小目标整块消失;调小 → 白点残留,框跟着抖 |
| ④-2 | 白块变稳定的绿圈 | cv2.findContours → cv2.minEnclosingCircle | min_area、min_radius~max_radius | min_area 调大 → 目标也会被滤掉、没框;调小 → 噪点也画圈 |
这些参数都写在 config/lab_config.yaml;参数动一格画面怎么变,下方面试台可以拨着看,到第 05 节还能用 --tune 实时拖滑杆亲手试。
静态图看完了,这里放了一台可以把四步拨着玩的实验台:点顶部步骤切换,拖下面两根滑杆挪动圆和方,看标签、掩膜和最终读数怎么跟着变。
为什么不能直接用 RGB?
这是初学者最常问的问题,也是最关键的一点:
亮处的红:(255,30,30)
暗处的红:(120,15,15)
你没法用一组固定的范围同时框住它们。因为 RGB 把「亮不亮」和「什么颜色」混在一起了。
亮处的红 ≈ H=0, S=230, V=255
暗处的红 ≈ H=2, S=225, V=120
H 几乎不变!只比 H 就能认出来。
| 通道 | 含义 | 取值范围 | 一句话 |
|---|---|---|---|
| H 色相 | 是什么颜色 | 0 ~ 179(注意不是 360) | 红色 0 附近,绿色 60 附近,蓝色 120 附近 |
| S 饱和度 | 颜色有多纯 | 0 ~ 255 | 0 = 灰白,255 = 纯色 |
| V 明度 | 有多亮 | 0 ~ 255 | 0 = 全黑,255 = 最亮 |
第 ④ 步到底在做什么:从掩膜到「一个圆」
上面四步图里第④步只写了「去噪点 + 画圈」,但代码其实做了三件事,缺一件结果就不对。这一步是初学者最容易「看懂了图、读不懂代码」的地方:
| 顺序 | 函数 | 干什么 | 少了它会怎样 |
|---|---|---|---|
| 1 | cv2.findContours | 把白色区域连成一圈圈边界,得到「轮廓点集」的列表 | 你只有一堆像素,不知道自己有几块目标 |
| 2 | cv2.minEnclosingCircle | 对每个轮廓求最小外接圆,把上千个边界点压成 (圆心, 半径) 两个数 | 数据量下不来,也没法算「球心在哪」 |
| 3 | 按面积/半径过滤,取最大的那个 | 滤掉小噪点、过滤过大的误检,只留每种颜色一个目标 | 一个颜色会框出十几个小圈,画面乱跳 |
为什么用最小外接圆而不是外接矩形?因为本实验的目标是球形(红/蓝/绿小球)。外接矩形会随球的旋转方向变化,面积和中心都会抖;外接圆的圆心和半径对旋转完全不变,而且圆心天然就在球心上 —— 这正是后面要做抓取定位时需要的。
min_area 500 是「小于 500 个像素的区域当噪点丢掉」;min_radius 10 / max_radius 400 是「半径小于 10 或大于 400 的圆都不要」。三个数都要按实际画面标定 —— 球拿远了面积变小,拿近了面积变大,阈值太紧就会时有时无。
先认识 4 个词
描述颜色的「坐标系」。同一个颜色可以用 RGB 描述,也可以用 HSV 描述,两者可以互相换算。
一张和原图一样大的黑白图。白色 = 保留这个像素,黑色 = 丢掉。所有「挑出某类东西」的操作都靠它。
对黑白图做的小整理。开运算去掉孤立小白点,闭运算填掉黑色小洞。
把白色区域连成一条边界。有了边界就能算面积、中心、画圆 —— 从「图」变成「数据」。
开运算和闭运算到底在干什么?选读 40 秒
两个都是用一个小方格(叫「核」,比如 5×5)在图上滑动:
开运算(先腐蚀再膨胀):让白色区域先瘦一圈再胖回来。瘦的过程中,小到撑不住的白点就消失了,大块区域恢复原样。用来去噪点。
闭运算(先膨胀再腐蚀):反过来,先胖一圈再瘦回来。小的黑洞就被填满了。用来补目标内部的空洞。
核必须是奇数(3、5、7、9),因为要有个唯一中心点。偶数核的锚点位置不明确,OpenCV 会报错。配置里 open_kernel: 5 就是 5×5 的椭圆核。
为什么本实验用椭圆核而不是方块核?因为目标是圆的。方块核在对角方向会引入「方角」效应,把圆形轮廓啃出棱角;椭圆核各个方向的作用距离一致,对圆形目标更友好。代码里就是 cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))。
它吃什么、吐什么
范围写在 config/lab_config.yaml 里,红/绿/蓝各一组。
左栏:原图 + 绿色轮廓 + 黄色外接框 + 红色中心十字 + 颜色名。
右栏:黑白掩膜,能直接看到「它认为哪里是这个颜色」。
配置里红色那一段有个特殊写法,一定要理解:
colors: - name: 红色 h: [0, 15] # 第一段:0~15 s: [100, 255] v: [80, 255] h2: [160, 180] # ← 第二段!红色跨了 0 的边界
[160, 15] 在程序里是「下限比上限大」,一个都选不出来。所以必须拆成 [0,15] 和 [160,180] 两段,分别算完再按位或合并。只有红色(以及最靠近红的品红/粉)需要这样处理 —— 其他颜色的色相区间不跨 0 点,一段就够。范围是闭区间:
h2: [160, 180] 把 H=180 也包含在内。虽然 OpenCV 实际取值到 179,但写 180 更贴近真实物理边界(色相环的 360° 处),语义更清楚。h 上限从 15 提到 20 左右。05实操:三条命令:实时 / 双栏 / 滑块调参
-
进目录,读一眼阈值配置约 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。 -
开相机,看双栏效果约 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,拿个红色的东西放到镜头前。 -
用滑块现场调阈值(最有意思的一步)约 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 只打印、不写回。要自己抄过去。
你该看到的画面
左:原图,目标上叠着绿色轮廓、黄色外接框、红色中心十字,左下角打印颜色名。
右:黑白掩膜,白的就是它认定的目标区域。
目标的形状清晰可见,周围基本全黑。有零星小白点说明噪点多,需要加大开运算核或提高 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 |
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 范围太窄,边缘的颜色被排除了 | 把 H 上下限各放宽 5~10 |
| 大片背景都变白 | S 下限太低,灰白色也被当成「有颜色」 | 把 s 下限从 100 提高到 130+ |
| 阴影处认不出来 | V 下限太高,暗处像素的明度不够 | 把 v 下限从 80 降到 50 |
07验证:三道小测
回页首对照「实验目标」逐条自检:① 能讲清 HSV 抗亮暗的原理 → 试 Q1;② 能说清 inRange 与红色双段阈值怎么写 → 试 Q2;③ 掌握噪点去除的手段 → 试 Q3。答不上的条目,回到对应小节补完再交卷 —— 三条全 ✓,这一课就完成了。
h: [0,15] 和 h2: [160,180] 两段,为什么?[160,15] 程序会认为无解,一个都选不出来。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,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 的任务。
inRange 的两段范围、形态学的奇核、和轮廓那三个过滤数。