RoboMaster 视觉招新考核全流程拆解:从蓝色灯条到 CV1 串口 B. 一条视觉链路的诞生:灯条检测 → 相机标定 → AprilTag 位姿 → 串

25 阅读15分钟

RoboMaster 视觉招新考核全流程拆解:从蓝色灯条到 CV1 串口

HUANGZHENXUAN123/vision-recruitment: RoboMaster 视觉组招新考核

一句话概括:用 OpenCV 打通「视频里找灯条 → 相机标定 → AprilTag 三维位姿 → 按 CV1 协议通过串口上报」这条完整链路,并让每一步都能被验证。

适合读者:准备 RoboMaster 视觉组考核的同学、刚开始接触 OpenCV + 相机几何的开发者。 阅读收益:一条可直接复用的流水线结构、每个环节的参数取舍理由、以及真实踩过的坑

目录

    1. 项目全景:一条链路,三个任务
    1. 环境与工程组织
    1. 任务一:视频蓝色灯条检测
    1. 任务二:相机标定与 AprilTag 位姿估计
    1. 任务三:CV1 串口通信
    1. 怎么证明它真的能跑
    1. 踩坑记录与已知问题
    1. 小结与下一篇预告
  • 附录

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(任务三)
Python3.10.12
OpenCV5.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

【两张位姿演示图(画出坐标轴)pose_result.jpg

tag_test_pose.jpg】 为什么距离越远、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起始标识 + 协议版本接收端可以靠 $ 快速对齐帧头,避免半包错位
seq32 位无符号递增序号,从 0 开始让接收端能独立发现丢包,而不依赖时间戳
t_ms程序启动后的毫秒数便于事后对齐"哪一帧的位姿在什么时候发出"
valid1 有效 / 0 无效显式表达状态,而不是靠字段缺省或空值
idAprilTag ID,无效时为 -1让接收端区分"没 Tag"与"不是我的 Tag"
x_mm, y_mm, z_mm单位毫米,保留 1 位小数电控侧直接可用,不需要再做单位换算
rx, ry, rzRodrigues 旋转向量(弧度),保留 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

【

image.png】


5. 怎么证明它真的能跑

把"验收"从口头描述变成可复现的清单:

任务验证方式结果
任务一逐帧处理输出视频,人工抽查代表帧【抽查 N 帧,漏检/误检情况】
任务二重投影误差 + 不同距离/角度对比17/18 有效,平均误差 1.0518 px
任务二无 Tag 异常测试【程序表现】
任务三虚拟串口对 + 串口助手接收日志有效/无效报文均正确,恢复正常

【可选加分项:写一个 <30 行的校验脚本,对 serial_receive.log 逐行重算 XOR 并统计 seq 是否连续。哪怕考核不要求,这类"自证材料"在评阅时非常加分。】


6. 踩坑记录与已知问题

踩坑记录与已知问题

  1. WSL 下摄像头与 COM 口不可用

    • 现象:在 WSL 中运行整合程序时,可能无法访问 Windows 摄像头或虚拟串口 COM11。
    • 排查:检查 /dev/video*、/dev/ttyUSB* 等设备节点,并分别测试摄像头和串口。
    • 原因:WSL 与 Windows 硬件设备之间存在映射限制,Windows 虚拟串口不一定能直接作为 Linux 串口使用。
    • 解决:在 WSL 中完成代码开发、协议测试和离线图片测试;如果实时设备无法访问,则使用 Windows Python 运行最终的摄像头与串口整合程序。两种环境共用同一套代码和配置逻辑。
  2. 形态学核大小与灯条断裂的权衡

    • 现象:形态学核过小时,灯条掩膜可能存在断裂和空洞;核过大时,相邻目标容易粘连,细小灯条也可能被破坏。
    • 排查:分别比较不同大小卷积核下的开运算和闭运算结果,观察灯条完整性、噪声数量和目标粘连情况。
    • 原因:形态学操作会改变白色目标区域的边界,卷积核越大,处理作用越明显。
    • 解决:先使用较小卷积核去除孤立噪声,再根据视频实际效果调整闭运算参数;最终参数以灯条保持完整、噪声较少且不发生明显粘连为准。
  3. Tag 边长填写错误导致 tvec 整体缩放

    • 现象:AprilTag 能够正常检测,但计算得到的 tvec 和距离与实际测量距离存在明显比例误差。
    • 排查:检查位姿估计函数中的 tag_size 参数,并重新测量 Tag 黑色外框的实际边长。
    • 原因:程序初始使用了错误的 Tag 边长 0.08 m,而实际黑色外框边长为 0.163 m。位姿估计中的平移量会随目标实际尺寸成比例变化。
    • 解决:将配置中的 tag_size_m 修改为 0.163,并统一使用黑色外框边长进行位姿估计。修改后重新验证不同距离下的 tvec 和直线距离。
  4. 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. 参考链接

C. 项目地址