RoboMaster 视觉招新考核全流程拆解:从蓝色灯条到 CV1 串口
HUANGZHENXUAN123/vision-recruitment: RoboMaster 视觉组招新考核
一句话概括:用 OpenCV 打通「视频里找灯条 → 相机标定 → AprilTag 三维位姿 → 按 CV1 协议通过串口上报」这条完整链路,并让每一步都能被验证。
适合读者:准备 RoboMaster 视觉组考核的同学、刚开始接触 OpenCV + 相机几何的开发者。 阅读收益:一条可直接复用的流水线结构、每个环节的参数取舍理由、以及真实踩过的坑
目录
-
- 项目全景:一条链路,三个任务
-
- 环境与工程组织
-
- 任务一:视频蓝色灯条检测
-
- 任务二:相机标定与 AprilTag 位姿估计
-
- 任务三:CV1 串口通信
-
- 怎么证明它真的能跑
-
- 踩坑记录与已知问题
-
- 小结与下一篇预告
- 附录
0. 项目全景:一条链路,三个任务
三个任务看似独立,其实是同一条数据流的三个阶段:先学会"看见颜色",再学会"知道自己在哪",最后学会"把结果说给队友听"
视频 / 摄像头
↓
任务一:BGR → HSV → 蓝色分割 → 形态学 → 轮廓 → 面积/长宽比筛选 → 旋转矩形
↓(同一套阈值与筛选思路复用到实车场景)
任务二:棋盘格标定 → AprilTag 检测 → solvePnP → R / t → 直线距离
↓(R / t 是任务三的唯一输入)
任务三:tvec / rvec → CV1 报文组帧 → XOR 校验 → 串口发送 → 串口助手
成果一览
| 任务 | 核心产出 | 关键指标 |
|---|---|---|
| 任务一 | 带标注的输出视频 + 逐帧统计 | 【代表帧数量、平均处理耗时 ms/帧】(该处不作具体数据) |
| 任务二 | camera_params.npz + 位姿结果 | 有效标定图m/n,平均重投影误差 xx p |
| 任务三 | serial_receive.log + 可复现联调 | 115200/8N1,XOR 校验通过,【连续发送帧数 / 丢帧情况】 |
1. 环境与工程组织
1.1 环境
| 项目 | 版本 |
|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS(任务一、二);Windows(任务三) |
| Python | 3.10.12 |
| OpenCV | 5.0.0 |
| 主要依赖 | numpy、opencv-python、pupil-apriltags、pyserial、PyYAML、matplotlib |
bash
# Ubuntu / WSL 侧
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
powershell
# Windows 侧(任务三需要访问摄像头与 COM 口)
python -m pip install -r requirements.txt
1.2 目录职责
代码块
vision-recruitment/
├── opencv_basics/ # API 练习:读图、通道分离、HSV、形态学、轮廓
├── task1_lightbar/ # 视频蓝色灯条检测
├── task2_apriltag/ # 相机标定、AprilTag 检测、位姿估计
└── task3_serial/ # CV1 协议、串口发送、联调记录
为什么先有 opencv_basics/? 因为任务一里 80% 的 bug 都不是"检测逻辑写错",而是对某个 API 的输入范围、返回结构理解错了。把这些 API 单独在一个小目录里跑通、留档,后面调参时才知道该怀疑哪一步。
2. 任务一:视频蓝色灯条检测
2.1 处理流水线
代码块
读取视频帧
↓ BGR → HSV
蓝色区域分割(inRange + B-R 差分辅助)
↓
形态学开运算 / 闭运算
↓
findContours 提取轮廓
↓
面积筛选 + 长宽比筛选
↓
minAreaRect → boxPoints 画旋转矩形
↓
叠加帧号 / 灯条数 / 耗时
↓
VideoWriter 写出标注视频
2.2 每一步"为什么这么做"
(1)为什么用 HSV 而不是直接对 BGR 设阈值? BGR 三个通道的数值同时受亮度影响,"蓝色"在暗处和亮处的 BGR 组合完全不同,阈值区间会碎成好几段。HSV 把色相(H)单独拆出来,光照变化主要影响 V,色相基本稳定,所以一个较宽的 H 区间就能覆盖大部分光照条件。
(2)为什么还要加一层 B-R 差分? 在偏白、偏灰的高光区域,H 值本身不稳定甚至随机跳变,光靠 H 阈值容易把白色高光误判成灯条。加上 B-R 差分阈值(当前取 40)作为"必须真的偏蓝"的二次确认,可以砍掉大部分这类误检。
(3)形态学在这里解决什么问题?
- 开运算:先腐蚀后膨胀,去掉零散噪点和小亮斑;
- 闭运算:先膨胀后腐蚀,把灯条内部因反光断开的区域连成一块。
当前流程使用了 1×1 卷积核——这其实是"名义上保留形态学步骤、实际几乎不做平滑"的配置。这是对比实验后的结论:在这个视频的噪声水平下,大核会顺手把细长灯条一起吃掉。
(4)筛选规则为什么是"面积 + 长宽比"这两个? 灯条在图像里的先天特征是细长:面积能过滤掉噪点和整块背景,长宽比能过滤掉方形/圆形的干扰物。当前参数:
| 参数 | 取值 | 作用 |
|---|---|---|
| HSV 下限 | (80, 120, 110) | 蓝色起点 |
| HSV 上限 | (135, 255, 255) | 蓝色终点 |
| B-R 差分阈值 | 40 | 排除白色高光 |
| 形态学核 | 1 × 1 | 保留原始形状 |
| 最小轮廓面积 | 3 | 保留远景小灯条 |
| 最大轮廓面积 | 100000 | 丢弃整块背景 |
| 长宽比范围 | 1.5 ~ 20 | 只要细长目标 |
(5)为什么用 minAreaRect 而不是 boundingRect? 灯条在画面里是斜的。boundingRect 给出的是轴对齐矩形,斜置灯条会被框成一个正方形,长宽比筛选直接失效;minAreaRect 给出最小面积旋转矩形,长宽比才反映真实的形状比例。
2.3 运行方式
bash
python task1_lightbar/detect_video.py \
task1_lightbar/input.mp4 \
task1_lightbar/output/lightbar_result.mp4
输出视频会在画面上叠加:当前帧号、当前帧灯条数、单帧处理耗时,以及每根候选灯条的旋转矩形、面积、长宽比。
【插图位 1:representative_frames/ 里挑 2-3 张代表帧,展示"检测成功 / 高光误检 / 灯条断开"三种情况对比。图注写清每张图想说明什么。】
2.4 调参心得
-
现象:远处灯条较小,部分目标出现漏检 → 改动:适当放宽目标区域的面积筛选条件,并结合形状特征进行判断 → 结果:远处灯条的保留情况改善,同时尽量避免引入过多小噪声。
-
现象:部分帧中出现零散噪声点,导致误检数量增加 → 改动:调整形态学开运算和闭运算的处理方式,先去除孤立噪声,再修复目标区域中的小缺口 → 结果:掩膜更加连贯,误检数量减少,灯条轮廓更加稳定。
-
现象:场地灯条或其他蓝色干扰区域被误认为目标 → 改动:结合轮廓的长宽比、面积、旋转角度和位置关系进行综合筛选,而不是只依赖颜色阈值 → 结果:对干扰区域的排除能力增强,目标灯条的框选更加准确。
3. 任务二:相机标定与 AprilTag 位姿估计
3.1 为什么任务二必须先标定
任务二要的不是"Tag 在图像里的像素坐标",而是"Tag 在相机坐标系下的真实位置"。像素 → 米 这一步需要一个映射关系,它由相机内参(焦距、主点)和畸变系数决定。不标定,solvePnP 出来的 tvec 只是一个数值,换台相机、换个分辨率就完全不可信。
3.2 标定流程
代码块
采集 18 张不同角度/距离的棋盘格图片
↓
findChessboardCorners 找内角点(9 × 6)
↓
cornerSubPix 亚像素精化
↓
calibrateCamera → 内参矩阵 + 畸变系数
↓
重投影误差评估
↓
保存 camera_params.npz
| 参数 | 数值 |
|---|---|
| 有效标定图片 | 17 / 18 |
| 棋盘格内角点 | 9 × 6 |
| 平均重投影误差 | 1.0518 px |
| 方格边长 | 见 calibration_info.txt |
为什么用重投影误差判断标定质量? 它是"把标定出的三维点重新投影回图像,与真实角点的像素距离"。1 px 左右说明内参已经够用;如果到了 3 px 以上,通常是图片角度太单一(比如全在同一个平面姿态)或角点检测不准,需要重拍而不是硬调代码。 【被判定无效(1/18) 那张为对照组。】
3.3 位姿估计流程
代码块
读取摄像头 / 图片
↓
AprilTag 检测(tag36h11)
↓
按 --target-id 筛选目标 ID
↓
已知 Tag 物理边长 0.163 m,构造物点
↓
solvePnP → rvec / tvec
↓
旋转矩阵 R + 平移向量 t + 直线距离
坐标系约定(最容易错的地方):
- X:图像向右
- Y:图像向下
- Z:镜头前方
代码块
p_camera = R × p_tag + t
R:Tag 坐标系 → 相机坐标系的旋转矩阵t:Tag 中心在相机坐标系中的平移向量z是"相机正前方的深度",不等于直线距离
代码块
distance = sqrt(x² + y² + z²)
为什么选 tag36h11? 它的码距较大,抗误检能力强,是 AprilTag 家族里最常用的一支;同时 Tag 物理边长必须与代码里填的边长一致,否则 tvec 会按比例整体缩放——这是最隐蔽的一类错误,因为画面看起来"完全正常"。
3.4 实测结果
| 图片 | tvec (m) | 直线距离 (m) |
|---|---|---|
| 图 1 | (-0.0147, -0.0510, 0.5604) | 0.5630 |
| 图 2 | (0.0469, -0.0316, 0.7054) | 0.7076 |
【两张位姿演示图(画出坐标轴)
】
为什么距离越远、z 与直线距离差得越多? 因为当 Tag 偏离光轴时,x、y 分量不可忽略;距离越近、越靠画面中心,z 与直线距离才越接近。这也是只看 z 判断"离得多远"会出错的原因。
4. 任务三:CV1 串口通信
4.1 数据流
代码块
USB 摄像头
↓
AprilTag 检测
↓
solvePnP 位姿估计
↓
tvec / rvec
↓
CV1 报文组帧(含 XOR 校验)
↓
COM11 ──虚拟串口对──> COM12
↓
串口助手(验证接收)
4.2 为什么任务三跑在 Windows 上
WSL 默认拿不到 Windows 宿主的摄像头设备和 COM 口,跨系统转发(usbipd 之类)对本阶段的收益很低。所以任务三直接在 Windows Python 下运行,用一对虚拟串口(COM11 → COM12)模拟"电控 ↔ 视觉"的真实链路,串口助手充当接收端。
| 项目 | 配置 |
|---|---|
| 摄像头编号 | 1 |
| 发送端 | COM11 |
| 接收端 | COM12 |
| 波特率 | 115200 |
| 数据位 / 校验 / 停止位 | 8 / 无 / 1 |
| 流控 | 无 |
| 发送频率 | 约 10 Hz |
4.3 报文格式与设计动机
代码块
$CV1,seq,t_ms,valid,id,x_mm,y_mm,z_mm,rx,ry,rz*HH\r\n
示例:
代码块
$CV1,42,12345,1,0,100.0,-50.0,800.0,0.000000,0.000000,0.000000*33\r\n
| 字段 | 含义 | 为什么这么设计 |
|---|---|---|
$CV1 | 起始标识 + 协议版本 | 接收端可以靠 $ 快速对齐帧头,避免半包错位 |
seq | 32 位无符号递增序号,从 0 开始 | 让接收端能独立发现丢包,而不依赖时间戳 |
t_ms | 程序启动后的毫秒数 | 便于事后对齐"哪一帧的位姿在什么时候发出" |
valid | 1 有效 / 0 无效 | 显式表达状态,而不是靠字段缺省或空值 |
id | AprilTag ID,无效时为 -1 | 让接收端区分"没 Tag"与"不是我的 Tag" |
x_mm, y_mm, z_mm | 单位毫米,保留 1 位小数 | 电控侧直接可用,不需要再做单位换算 |
rx, ry, rz | Rodrigues 旋转向量(弧度),保留 6 位小数 | 与 solvePnP 输出一致,避免欧拉角的多解与万向锁问题 |
HH | $ 与 * 之间所有 ASCII 字节的 XOR,两位大写十六进制 | 校验一行报文的完整性,防止串口噪声被当成数据 |
\r\n | 真实发送 0D 0A | 文本行协议的标准结束符,串口助手可直接换行显示 |
一个值得强调的取舍:为什么不转成欧拉角? 欧拉角直觉上更好读,但有三个问题:旋转顺序约定不统一、接近 ±90° 时存在万向锁、同一姿态可能有多组解。Rodrigues 向量与
solvePnP的输出一一对应,中间不引入额外误差。如果上位机需要欧拉角,由接收端按自己的旋转顺序转换更合适。
4.4 无效状态与恢复
以下情况发送无效报文(valid=0、id=-1、坐标与旋转向量全 0):
- 未检测到 AprilTag
- 检测到的 ID 与目标 ID 不匹配
- 位姿估计失败
- 摄像头读到空帧
目标重新出现后,程序自动恢复发送有效位姿。
为什么要主动发"无效"而不是干脆不发? 因为安静无法被区分。接收端如果没收到数据,它无法判断是"视觉没看到目标"还是"视觉程序崩了、串口断了"。持续发送无效帧,等于持续心跳:链路坏了才没有数据。
4.5 运行方式与验证
powershell
Set-Location "C:\Users\32716\Desktop\vision-recruitment-win"
python .\task3_serial\detect_apriltag_serial.py `
--camera 1 `
--target-id 0 `
--port COM11
【
】
5. 怎么证明它真的能跑
把"验收"从口头描述变成可复现的清单:
| 任务 | 验证方式 | 结果 |
|---|---|---|
| 任务一 | 逐帧处理输出视频,人工抽查代表帧 | 【抽查 N 帧,漏检/误检情况】 |
| 任务二 | 重投影误差 + 不同距离/角度对比 | 17/18 有效,平均误差 1.0518 px |
| 任务二 | 无 Tag 异常测试 | 【程序表现】 |
| 任务三 | 虚拟串口对 + 串口助手接收日志 | 有效/无效报文均正确,恢复正常 |
【可选加分项:写一个 <30 行的校验脚本,对 serial_receive.log 逐行重算 XOR 并统计 seq 是否连续。哪怕考核不要求,这类"自证材料"在评阅时非常加分。】
6. 踩坑记录与已知问题
踩坑记录与已知问题
-
WSL 下摄像头与 COM 口不可用
- 现象:在 WSL 中运行整合程序时,可能无法访问 Windows 摄像头或虚拟串口
COM11。 - 排查:检查
/dev/video*、/dev/ttyUSB*等设备节点,并分别测试摄像头和串口。 - 原因:WSL 与 Windows 硬件设备之间存在映射限制,Windows 虚拟串口不一定能直接作为 Linux 串口使用。
- 解决:在 WSL 中完成代码开发、协议测试和离线图片测试;如果实时设备无法访问,则使用 Windows Python 运行最终的摄像头与串口整合程序。两种环境共用同一套代码和配置逻辑。
- 现象:在 WSL 中运行整合程序时,可能无法访问 Windows 摄像头或虚拟串口
-
形态学核大小与灯条断裂的权衡
- 现象:形态学核过小时,灯条掩膜可能存在断裂和空洞;核过大时,相邻目标容易粘连,细小灯条也可能被破坏。
- 排查:分别比较不同大小卷积核下的开运算和闭运算结果,观察灯条完整性、噪声数量和目标粘连情况。
- 原因:形态学操作会改变白色目标区域的边界,卷积核越大,处理作用越明显。
- 解决:先使用较小卷积核去除孤立噪声,再根据视频实际效果调整闭运算参数;最终参数以灯条保持完整、噪声较少且不发生明显粘连为准。
-
Tag 边长填写错误导致
tvec整体缩放- 现象:AprilTag 能够正常检测,但计算得到的
tvec和距离与实际测量距离存在明显比例误差。 - 排查:检查位姿估计函数中的
tag_size参数,并重新测量 Tag 黑色外框的实际边长。 - 原因:程序初始使用了错误的 Tag 边长
0.08 m,而实际黑色外框边长为0.163 m。位姿估计中的平移量会随目标实际尺寸成比例变化。 - 解决:将配置中的
tag_size_m修改为0.163,并统一使用黑色外框边长进行位姿估计。修改后重新验证不同距离下的tvec和直线距离。
- 现象:AprilTag 能够正常检测,但计算得到的
-
OpenCV 5.0 与旧教程的 API 差异
- 现象:部分旧教程中的函数写法、返回值形式或示例参数与当前 OpenCV 5.0 环境不完全一致,直接照搬代码时可能出现报错或结果差异。
- 排查:根据报错信息检查当前 OpenCV 版本,并通过
help()、官方文档和实际函数返回值确认接口。 - 原因:教程可能基于旧版本 OpenCV,且不同版本的接口实现和推荐用法会有所变化。
- 解决:以当前环境实际运行结果和 OpenCV 官方文档为准;对关键函数进行最小化测试后再接入完整程序,并避免直接复制未经验证的旧版代码。
7. 小结与下一篇预告
【这次考核最大的收获是:掌握了从图像处理、AprilTag 位姿估计到串口通信的完整视觉流程,也提高了独立排查环境、参数和代码问题的能力。
如果重做一遍,我会先统一项目目录和运行环境,再搭建完整流程框架,最后逐步调试各个模块,这样可以减少文件分散和接口不一致带来的问题。】
下一篇我会逐文件拆解代码,包括:
blue_mask.py/morphology.py:从最小示例理解 HSV 阈值与形态学核detect_video.py:逐帧流水线的函数划分与耗时统计calibrate_camera.py:棋盘格标定与重投影误差的计算位置estimate_pose.py:solvePnP的输入构造与坐标系约定cv1_protocol.py:报文组帧、XOR 校验与浮点格式化陷阱detect_apriltag_serial.py:无效状态机与 10 Hz 主循环
附录
A. 参数速查
| 类别 | 参数 | 值 |
|---|---|---|
| 任务一 | HSV 范围 | (80,120,110) ~ (135,255,255) |
| 任务一 | B-R 差分阈值 | 40 |
| 任务一 | 面积 / 长宽比 | 3 ~ 100000 / 1.5 ~ 20 |
| 任务二 | Tag 家族 / 边长 | tag36h11 / 0.163 m |
| 任务二 | 棋盘格内角点 | 9 × 6 |
| 任务三 | 串口 | COM11 → COM12, 115200, 8N1 |
B. 参考链接
- OpenCV 官方文档
- OpenCV Camera Calibration
- AprilTag 官方项目
- pupil-apriltags
- pySerial 官方文档
- RoboMaster 视觉组招新考核手册(26.9)
C. 项目地址