机械臂摆「8周年快乐」,字全给我放倒了——排查到最后是一个符号反了
起因
有台 Piper 六轴机械臂,我给它布置了个任务:把桌面上散落的五个正方形字块——「8」「周」「年」「快」「乐」——一个个抓起来,按顺序摆成一排,凑出「8周年快乐」四个字加一个数字。要求也不高:每个字放下之后,字头得朝上、正着朝向你,别倒着、别横着。
听起来简单,抓起来摆个积木嘛。结果它干出来的活儿:字是抓起来了,排也排成一排了,方向却全乱了——有的偏左 90°、有的偏右 90°,个别直接 180° 倒过来,而且每次跑出来的偏向都不一样。更气人的是,我看了一眼它识别的中间结果,方向明明判的是对的。
这篇就复盘一下:这东西到底怎么搭的、怎么认方向的,以及最后那个「方向全乱」是怎么查出来——根因不是一个复杂的算法问题,而是一个符号写反了。
代码和调试记录都开源了:nbsheep/roboarm-placement。
系统长啥样
先说硬件和底座,一句话概括:Piper 六轴机械臂(USB-CAN 连接)+ Orbbec Gemini 336L 深度相机(USB),跑在一个 VMware 的 Ubuntu 虚拟机里,软件栈是 LeRobot 那套框架,二次开发基于 cloud666666666/AIIT-Roboarm。
自己搭的时候有几个纯折腾人的坑,都记在部署指南里了:CAN 得每次开机手动激活(can0、1Mbps),Orbbec 相机要先装 udev 权限规则、VMware 的 USB 控制器还得从 2.0 改 3.1,不然相机直接 openUsbDevice failed。这些跟今天要讲的主题无关,跳过。
识别链路:检测 → 摆平 → 判方向 → 放下
要让机械臂把字摆正,整条视觉链路是这样的:
1 | OBB 旋转框检测 (YOLO11x-obb) |
两层模型各有分工:
- 检测用 YOLO11x-obb,旋转框(OBB)能直接给出带角度的框,比普通水平框更适合旋转摆放的字符。训练时有个关键点——禁止左右/上下翻转,因为中文字符镜像之后就是无效字,翻转增强只会把模型教坏。
- 方向分类用自己写的小网络 OrientCNN,输入是透视摆平后的灰度图,输出 4 类的 softmax,分别对应「这个字块还需要逆时针转 0/90/180/270° 才正立」。
检测负责「哪个字在哪儿、框是啥角度」,分类负责「这个字的头朝哪边」,两者接力,理论上就能把每个字摆得端端正正。
出问题:方向认对了,摆出来还是歪
链子搭好后,一跑就发现不对:字能抓起、能摆成排,方向却乱套。而且那个乱法很典型——干干净净地偏 90°,或者 180°,不是随便偏个 17°、23° 这种。
干净的 90° 本身就给了第一条线索:能让结果精确落在 90° 倍数上的,只能是「以 90° 为单位的量」——比如 4 分类判出来的 k,或者正方形「哪条算长边」的判定。连续量(标定误差、关节偏差)不会给出这么整齐的 90°。所以我一开始就盯着这两个 90° 源怀疑。
弯路一:先怀疑分类器
第一反应是 OrientCNN 认错了方向。尤其「周」「快」这种字,正着倒着都有点像,4 分类的 k 判错一个,方向就翻 90°。
于是加了多帧软投票(跨帧累加 softmax)+ 置信度门限,想把 k 稳住。
结果被自己打脸:单独跑了个自查脚本 check_orient.py,把每个字「摆正之后」的图拼出来看——所有字都是正的,置信度 0.86~0.99。识别没问题。
这里栽了个认知坑:check_orient 只能证明「图像里认得对」,不代表「摆得对」。从图像认对,到机械臂真正摆对,中间还隔着图像→机械臂的坐标转换、实际夹取、逆运动学三个环节,这些自查脚本一个都不碰。
弯路二:再怀疑正方形
第二个怀疑对象是「正方形」本身。积木 w≈h,gripper_angle_by_longer 判「哪条是长边」时会随机翻,抓取角跟着跳 90°,字块进夹爪时就被物理带转 90°。
于是改成「沿两条边轴取 4 个候选腕姿,选放置角最靠近 0° 的」。结果更糟了:候选让抓取角直接跑出了 ±90°(出现了 164°、-154° 这种离谱值),而下爪的趋近偏移 offset×cos(抓取角) 方向随之翻转,夹爪从物体反面下爪,把字块推歪——不光转 90°,位置也歪七扭八、排都不成排了。
只能 git restore 回退到当天最早版本,把工作版备份起来。教训:抓取趋近偏移必须用 ∈[-90°,90°] 的原始几何角,不能让它超范围。
转折:写个只读脚本,一次把整条链量出来
连着猜错两次之后,我决定不猜了,也不拿真机试错了——试错一次太贵。改为写一个只读、不动机械臂的诊断脚本 diag_place_angle.py:
- 不连 CAN、不发任何运动命令;
- 用一个纯运动学助手直接从 URDF 建运动学链,读关节限位和手眼标定矩阵,纯做正/逆运动学数值计算;
- 每个字打印一整串量:
angle_up、几何抓取角、逐帧极差、两种放置策略的净转角、干 IK 的「下发 yaw vs 实际可达 yaw」(查腕关节 J6 有没有被限位截断)、以及符号两种假设下的预测字头。
一次把整条放置链拆开量,三个假设当场毙掉两个:
| 假设 | 判据 | 结论 |
|---|---|---|
| 识别不稳 | angle_up 逐帧极差 <1° |
❌ 排除,识别很稳 |
| 正方形几何角乱跳 | 抓取角逐帧极差 0.4~0.8° | ❌ 排除,抓取角很稳 |
| 腕关节 J6(±120°) 限位截断 | 放置位姿干 IK 偏差 ≈0° | ❌ 排除,够得着 |
| 旋转符号反了 | 见下 | ✅ 命中 |
锁定:一个符号反了
问题出在「图像坐标系 → 机械臂坐标系」的旋转符号上。代码里有个 place_rotate_sign(±1),决定放置补转角是顺着转还是反着转。它一直是 -1,而正确的值应该是 +1。
符号问题有三条独立证据:
- 手眼标定的 homography 矩阵线性部分 det < 0——这是个镜像变换,意味着「图像里逆时针 = 机械臂顺时针」,两边旋向本来就是反的;
- 默认放置姿态工具朝下(RY=180),实测「机械臂 rot 每 +1° → 末端 yaw 反而是 -1°」,又叠了一重翻转;
- 之前 sign=-1 开分类器跑,误差正好散成
2×angle_up——这正是「符号反了」的数学特征。
放置角公式本来是这样的:
1 | delta_deg = place_rotate_sign × (place_text_img_deg − angle_up) |
最终字头朝向可以推成一个很干净的关系:
1 | 字头_最终 = angle_up × (1 − s_img × place_rotate_sign) |
s_img 是「机械臂转 1° 对应图像里字头转几度」的符号,由相机安装决定,代码里看不出来,只能靠标定矩阵和实测去推。算出来:
- 若
s_img × place_rotate_sign = +1→ 字头 = 0°(摆正) - 若
= −1→ 字头 =2×angle_up(东倒西歪)
答案一目了然。改 config.yaml 里 place_rotate_sign: -1 → 1,上真机跑一遍,五个字全部摆正。
经验
这次排查花了点时间,但换回来几条现在回头看很值钱的经验:
- 「认得对」≠「摆得对」。识别正确只是第一环,坐标转换、夹取、逆运动学,每一环都是独立的故障点,自查脚本只管得着第一环。
- 干净的 90° 只来自「以 90° 量化的量」。先找量化源(4 分类的 k、长短边判定);符号、标定、限位截断这些连续量,给不出整齐的 90°。
- 真机试错太贵,先做离线只读诊断。
diag_place_angle.py免相机、不动臂,把整条链拆开一次性量完,一次定位,省掉无数次空跑。 - 手眼矩阵可能是镜像(det<0)。图像系和机械臂系的旋向不一定一致,凡涉及旋转,务必先核对符号。
- 区分「符号错」和「标定偏置」。符号错 → 各字误差散成
2×angle_up(东倒西歪);标定偏置 → 各字统一偏一个固定角。后者调place_text_img_deg,别去动符号。
代码
- 成果仓(只含我自己在框架上的二次开发部分):nbsheep/roboarm-placement
stage4_infer.py端到端推理、orient_cnn.pt方向分类器权重classification/方向分类与放置脚本(含只读诊断diag_place_angle.py)object_detect/YOLO OBB 检测与训练docs/完整的放置方向调试记录 + 环境部署指南
- 上游框架:cloud666666666/AIIT-Roboarm
