- traditional CV pipeline referencing jlu_vision_26: HSV mask -> contour LightBar -> PCA corner refine -> pairing gates - big/small armor classification (bar distance / bar length ratio) - detect_test with synthetic selftest, 6/6 passed - measured armor size 131.56x55mm, solver NaN guard + ITERATIVE fallback
259 lines
11 KiB
Markdown
259 lines
11 KiB
Markdown
# 27RM
|
||
|
||
RoboMaster 视觉工程(MindVision 相机 + OpenCV)。
|
||
|
||
## 构建
|
||
|
||
```bash
|
||
xmake # 产出四个程序(见下)
|
||
xmake run 27RM # 跑主程序
|
||
```
|
||
|
||
### VSCode + clangd
|
||
|
||
`compile_commands.json` 含本机绝对路径,**不入库**,克隆后自己生成一次:
|
||
|
||
```bash
|
||
xmake project -k compile_commands .vscode
|
||
```
|
||
|
||
(`.vscode/settings.json` 里已配置 `clangd.arguments: ["--compile-commands-dir=.vscode"]`)
|
||
|
||
## 程序清单
|
||
|
||
| 程序 | 用途 |
|
||
|---|---|
|
||
| `27RM` | 自瞄主程序 |
|
||
| `capture_calib` | **相机标定图采集工具** |
|
||
| `calibrate` | **标定计算工具**(照片 → 内参/畸变 → `configs/camera.yaml`) |
|
||
| `pose_test` | **几何链路验证工具**(4 角点 → 距离/yaw/pitch,含合成自检) |
|
||
| `detect_test` | **灯条检测验证工具**(检测 → PnP 一条龙,含合成自检) |
|
||
|
||
## 配置
|
||
|
||
```
|
||
configs/
|
||
├── camera.yaml # 相机内参 + 畸变 + 标定元信息(由 calibrate 生成,需入库)
|
||
├── camera_runtime.yaml # 运行时参数:曝光/增益/白平衡/帧率(手工维护,需入库)
|
||
└── light_bar.yaml # 灯条检测参数:HSV 范围/配对门限/PCA(手工维护,需入库)
|
||
```
|
||
|
||
代码里这样用(`inc/camera_calib.hpp`):
|
||
|
||
```cpp
|
||
#include "camera_calib.hpp"
|
||
|
||
CameraCalib calib;
|
||
if (!calib.load("configs/camera.yaml")) return -1; // 启动时加载一次
|
||
|
||
cv::Mat undist;
|
||
calib.undistort(frame, undist); // 每帧去畸变
|
||
|
||
// 装甲板四角点(左下、左上、右上、右下)→ 位姿
|
||
std::array<cv::Point2f, 4> corners{lb, lt, rt, rb};
|
||
if (auto pose = calib.solvePose(corners, ArmorSize::small())) {
|
||
double yaw, pitch;
|
||
CameraCalib::poseToYawPitch(pose->tvec, yaw, pitch);
|
||
// pose->distance_mm / pose->reproj_error_px 可用于过滤坏帧
|
||
}
|
||
```
|
||
|
||
`solvePose` 内部已处理:去畸变 → `solvePnPGeneric`(IPPE) 取回全部候选解 →
|
||
筛掉「相机后方 (z<=0)」「重投影 > 8px」→ 取最优。详情见源码注释。
|
||
|
||
相机参数设置(`inc/camera_set.hpp`,`initializeCamera` 之后调用):
|
||
|
||
```cpp
|
||
cam.setAeState(false); // 手动曝光(比赛必须,否则画面亮度漂移)
|
||
cam.setExposureTime(3000.0); // 曝光时间,微秒
|
||
cam.setAnalogGainTimes(2.0); // 模拟增益,倍率(内部按 fAnalogGainStep 换算)
|
||
cam.setWbMode(false); // 关自动白平衡(颜色阈值才稳定)
|
||
cam.setFrameSpeed(2); // 帧率档位 0/1/2
|
||
```
|
||
|
||
也可以直接由配置驱动:`applyRuntimeParams` 的示例见 `src/pose_test.cpp`。
|
||
|
||
> 程序必须在工程根目录运行(`configs/` 是相对路径),或自行传绝对路径。
|
||
|
||
## pose_test:几何链路验证
|
||
|
||
写检测器之前,先用它验证「标定 + 坐标约定 + PnP 解算」:
|
||
拿真的装甲板(或打印 131.56×55mm 矩形)摆在相机前,鼠标点 4 个角,
|
||
输出的距离/yaw/pitch 与卷尺、量角器核对。之后接检测器出问题必是检测的锅。
|
||
|
||
```bash
|
||
./build/linux/x86_64/debug/pose_test # 相机 + 鼠标点角
|
||
./build/linux/x86_64/debug/pose_test --image 照片.jpg # 图片模式
|
||
./build/linux/x86_64/debug/pose_test --selftest # 合成自检(无需相机/显示器)
|
||
# 脚本/无窗口:直接给 4 个角点(左下 左上 右上 右下),--size 自定义矩形(毫米)
|
||
./build/linux/x86_64/debug/pose_test --image 照片.jpg --size 180x120 \
|
||
--corners x1,y1 x2,y2 x3,y3 x4,y4
|
||
```
|
||
|
||
- 相机模式按键:`r` 清除重选 | `b` 切换大小装甲 | `u` 切换去畸变显示 | `s` 截图 | `q` 退出
|
||
- `--selftest` 自检已通过:正前方/右偏/上仰/复合/板内滚转均精确恢复,
|
||
交叉乱序被重投影门限正确拒绝
|
||
- 真实照片验证:6 张标定图(棋盘格外圈 180×120mm)距离 0.44–0.69m、
|
||
重投影 0.3–2.5px,与卷尺真值一致
|
||
|
||
### 两个踩过的坑(写检测器时别重蹈)
|
||
|
||
1. **OpenCV 4.x 的 IPPE 有两个数值雷区**(实测):
|
||
①「畸变 + 板内旋转」→ 解出垃圾解;②「完美轴对称矩形」→ 返回 NaN。
|
||
解法:IPPE 全候选逐个筛(有限值/z>0/重投影),**全被筛掉就自动退回
|
||
`SOLVEPNP_ITERATIVE`**——两条退化路径都稳(`solvePose` 已内置)。
|
||
2. **4 个共面点的顺序错误查不出来**:循环/镜像顺序 = 同一个四边形 =
|
||
同一个解(无害);交叉乱序会被重投影门限拦下。真正需要 roll 门限的是
|
||
「检测器给出假四边形」场景,等写检测器时照搬 jlu 的 `roll_thres 15°`。
|
||
|
||
### 装甲板尺寸(2026-09-16 实测)
|
||
|
||
| 板型 | W 灯条中心距 | L 灯条长度 | 来源 |
|
||
|---|---|---|---|
|
||
| 小装甲(2/3/4/5 号、哨兵) | **131.56 mm** | **55.0 mm** | 实测 3 号板,与官方规格 131.5mm 一致 |
|
||
| 大装甲(英雄 1 号) | 230 mm(名义值) | 55.0 mm | 待实测校正 W |
|
||
|
||
⚠️ 尺寸误差 1% = 距离误差 1%;拿 135mm 模型解 230mm 大板,距离会差 0.59 倍。
|
||
检测器将来必须同时输出「4 角点 + 大小装甲分类」。
|
||
|
||
## detect_test:灯条检测 + PnP 一条龙
|
||
|
||
传统 CV 管线(借鉴 jlu_vision_26):HSV 颜色掩膜 → 轮廓 → LightBar
|
||
(minAreaRect+质心+端点)→ PCA 角点修正(灰度矩对称轴 + 亮度梯度找端点)
|
||
→ 左右配对五道门 → 大小装甲分类 → 4 角点 → `solvePose`。
|
||
|
||
```bash
|
||
./build/linux/x86_64/debug/detect_test # 相机实时
|
||
./build/linux/x86_64/debug/detect_test --image 照片.jpg # 静态图
|
||
./build/linux/x86_64/debug/detect_test --selftest # 合成自检(无需相机)
|
||
```
|
||
|
||
- 按键:`q` 退出 | `s` 截图 | `c` 切换敌方颜色(红方打蓝,默认蓝) | `r` 重载参数
|
||
- 参数都在 `configs/light_bar.yaml`,现场按 `r` 热重载调参
|
||
- 敌方颜色默认 `blue`(主人是红方);拿自己红板测试时改 `red` 或现场按 `c`
|
||
- `--selftest` 自检已通过 6/6:正前方/右偏/上仰/复合/板内滚转/大装甲,
|
||
距离误差 ≤3.1%、yaw/pitch 误差 ≤0.05°,大小分类全部正确
|
||
|
||
### 调参时踩过的三个坑(已在默认参数里避开)
|
||
|
||
1. **形态学闭运算别开 2 次**:远景灯条只有 2~3px 宽,腐蚀两轮整个抹掉
|
||
2. **长宽比门限别设太高**(默认 1.5):远景灯条模糊+阈值后掩膜被撑到
|
||
10px 宽,实测比只剩 1.7;防误检主要靠配对五道门
|
||
3. **角点顺序的镜像坑**:板子朝向相机时,板的左条出现在画面右侧
|
||
(面对面镜像),输出角点已按模型约定「板左下→板左上→板右上→板右下」处理
|
||
|
||
### 渲染自检的教训(写自检时别重蹈)
|
||
|
||
圆管式画灯条会让端弧超出模型端点 4px+,距离直接偏 17%;用填充四边形、
|
||
端点严格对齐模型点。
|
||
|
||
## 相机标定流程
|
||
|
||
> **本工程标定板规格**:10 × 7 内角点、**方格 20.0 mm**(卡尺实测值,不是打印标称值)
|
||
> **相机**:MV-SUA131GC(1/3" CMOS,3.75μm 像元)+ MV-LD-M12-2.8MM-C 镜头,输出 1280×1024
|
||
> ⚠️ M12 镜头**调焦会改变有效焦距**,标定和使用时必须保持对焦环位置一致
|
||
|
||
### 1. 采集标定图
|
||
|
||
```bash
|
||
# 用 MindVision 相机(机器人上那台)
|
||
./build/linux/x86_64/debug/capture_calib --square 20.0
|
||
|
||
# 或改用 USB 摄像头
|
||
./build/linux/x86_64/debug/capture_calib --usb 0 --square 20.0
|
||
```
|
||
|
||
按键:
|
||
|
||
| 键 | 功能 |
|
||
|---|---|
|
||
| 空格 | 保存当前帧(默认要求检测到棋盘格) |
|
||
| `f` | 强制保存(未检测到也存) |
|
||
| `d` | 删除上一张 |
|
||
| `c` | 开关实时角点检测显示 |
|
||
| `g` | 灰度预览(排查过曝/反光) |
|
||
| `q` / ESC | 退出 |
|
||
|
||
拍摄要求:
|
||
|
||
- 标定板出现在画面**中心、四角、边缘**(2.8mm 广角镜头边缘畸变最大)
|
||
- 姿态多样:正对、倾斜、旋转、俯仰
|
||
- 清晰不糊、光照均匀、无反光、板子贴平硬板
|
||
- **20~30 张**,保存于 `calib_images/`(已加入 .gitignore,不会入库)
|
||
|
||
### 2. 计算内参
|
||
|
||
```bash
|
||
./build/linux/x86_64/debug/calibrate --dir calib_images --square 20.0 # 只算顶层
|
||
./build/linux/x86_64/debug/calibrate --dir calib_all --square 20.0 # 全部 31 张(推荐)
|
||
./build/linux/x86_64/debug/calibrate --square 20.0 --show # 逐张查看角点与重投影
|
||
```
|
||
|
||
工具会输出重投影误差 RMS、内参 K、畸变系数、每张图误差排行,并写入 `camera_calib.yml`。
|
||
|
||
| 重投影误差 | 评价 |
|
||
|---|---|
|
||
| < 0.3 px | 很好 |
|
||
| 0.3 ~ 0.5 px | 可接受 |
|
||
| > 0.5 px | 建议重拍 |
|
||
|
||
> **当前标定结果**(`calib_all/` 31 张,20mm 方格):
|
||
> fx=657.47, fy=658.25, cx=625.64, cy=509.00,RMS **0.483 px**
|
||
> **距离验证**:卷尺 700mm vs 程序 687~697mm,误差 **< 2%** ✓
|
||
|
||
### ⚠️ 标定图必须同时具备「三种多样性」
|
||
|
||
| 维度 | 作用 | 不满足的后果 |
|
||
|---|---|---|
|
||
| **画面位置**(九宫格覆盖) | 约束**畸变系数** | 边缘畸变解不出来,RMS 偏高 |
|
||
| **倾角**(正对/上下倾/左右倾/大角度) | 约束**焦距与主点** | 参数耦合,精度下降 |
|
||
| **距离**(近/中/远,板子占画面 40%/20%/8%) | 约束**尺度** ⭐ | **fx 无法确定**:所有图同距离时,"焦距×距离"可整体缩放而图像不变,优化器会落到错误值,**且 RMS 依然很低**(最阴险的坑) |
|
||
|
||
> 实测教训:一套 19 张、覆盖 9/9 但**距离相近**的图,标出的 fx=883(实际 659,偏差 34%),距离误差直接 +32%;
|
||
> 与旧的 12 张(距离 24~65cm)合并后 fx 回到 657,误差 <2%。**距离多样性是 fx 的锚点,不可省。**
|
||
|
||
### 3. 使用内参
|
||
|
||
推荐直接用封装好的 `CameraCalib`(见上方「配置」一节),它已经处理了配置路径、去畸变缓存和位姿门限。
|
||
若要手动读:
|
||
|
||
```cpp
|
||
cv::Mat K, D;
|
||
cv::FileStorage fs("configs/camera.yaml", cv::FileStorage::READ);
|
||
fs["camera_matrix"] >> K;
|
||
fs["dist_coeffs"] >> D;
|
||
```
|
||
|
||
之后用 `cv::undistort` / `cv::solvePnP` 做去畸变与位姿解算。
|
||
|
||
### 4. 距离验证(改完参数一定要做)
|
||
|
||
```bash
|
||
~/yolo-env/bin/python ~/rm_vision/verify_distance.py 图片.jpg --square 20.0 --distance 647
|
||
```
|
||
|
||
用卷尺量出真实距离,与程序算出的距离对比:误差 < 2% 说明标定可用。
|
||
|
||
## 提交规则(.gitignore)
|
||
|
||
| 内容 | 是否入库 | 原因 |
|
||
|---|---|---|
|
||
| `src/` `inc/` `xmake.lua` `README.md` | ✅ 入库 | 源码与构建脚本 |
|
||
| `configs/camera.yaml` | ✅ **必须入库** | 相机内参,全队共用 |
|
||
| `calib_images/` `calib_all/` | ❌ 不入库 | 标定照片,20MB+ 且随时重拍 |
|
||
| `*.jpg` `*.png` `*.bmp` `*.avi` `*.mp4` | ❌ 不入库 | 图片视频一律不入库 |
|
||
| `build/` `.xmake/` `Camera/` `*.log` | ❌ 不入库 | 编译产物、SDK 运行时日志 |
|
||
| `.vscode/compile_commands.json` | ❌ 不入库 | xmake 生成,含本机绝对路径 |
|
||
|
||
确有少量配图(例如 README 插图)要提交时,用强制添加:
|
||
|
||
```bash
|
||
git add -f docs/xxx.png
|
||
```
|
||
|
||
### ⚠️ 标定图请自己留备份
|
||
|
||
照片不入库 = 只有本机一份。重装系统前记得把 `calib_images/` 拷到网盘或 U 盘,
|
||
否则将来想重新标定(换镜头、调焦后)就没有原始数据了。
|
||
标定**结果**在 `configs/camera.yaml` 里,但结果无法替代原始照片。
|