海康 VisionMaster 十二点标定・C++ 工程技术文档
九点平移仿射 + 旋转圆心归一化 + 角度映射,含单点绝对 / 相对抓取引导、旋转拍照补偿、多拍照工位与纯平移模式。
矩阵运算全部基于 Eigen | 结果 JSON 导入导出 | 眼在手外 (Fixed) / 眼在手上 (Moving) / 纯 9 点平移
| 环境 | 说明 |
|---|---|
| 开发环境 | VS2017 (v141) · C++17 · x64 |
| 依赖 | Eigen 3.3.9(仅头文件) |
| Moving | 已与 VM 真实数据逐位对齐 |
| 纯平移 | 双模式闭环 0 误差 |
目录
-
功能概述与模式选择
-
坐标与角度约定
-
十二点标定原理与公式
-
单点抓取引导(Moving 为已验证主线)
-
眼在手外 Fixed 的差异
-
旋转拍照使能与多拍照工位
-
纯 9 点平移模式
-
工具与旋转轴心不一致的处理
-
工程结构与 API
-
JSON 标定结果字段
-
使用步骤与代码示例
-
验证记录与精度
-
注意事项与 FAQ(踩坑记录)
1. 功能概述与模式选择
本工程复现并扩展了海康 VisionMaster「平移旋转标定(十二点标定)」与「单点抓取(绝对抓取)」的完整算法链路:用一组像素点对 ↔ 机器人法兰点对拟合出 像素坐标 → 物理坐标 的映射,生产时把相机识别到的工件特征点与方向,换算成机器人末端的抓取位姿(绝对坐标 + 相对示教点的偏移)。
| 模式 | 相机安装 | 采点方式 | 引导是否需要拍照位 | 本工程状态 |
|---|---|---|---|---|
| Moving 眼在手上 | 相机装在法兰上,随机器人运动 | 移动相机去拍固定在工作台的标定板 / 标定针 | 需要,每次传本次拍照法兰位 runSnap |
已与 VM 真实数据逐位对齐 |
| Fixed 眼在手外 | 相机固定在机架 / 上方,相对世界静止 | 机器人抓着标定板走 9 个平移位、原地转 3 次 | 不需要,拍照位恒为标定第 5 点 | 已实现,12 点旋转 / 9 点平移均数值仿真闭环(现场数据待标定) |
| TransOnly 纯平移 | 两种安装均可 | 只采 9 个平移点,不采旋转点(或运行时强制 9 点) | Fixed 不需要 / Moving 多工位仍需要 | 已实现,双模式闭环 0 误差 |
这是两个互相正交的选择,不要混在一起:
①
相机装哪
(Moving / Fixed)------ 决定要不要补偿拍照位平移
snapOffset和相机自转 Δφ;
②
做 9 点还是 12 点标定
------ 决定
能不能处理工件在盘面内的旋转
。
** 能否抓 "带旋转的工件" 只取决于 ②,与 ① 无关:** 眼在手外 (Fixed) 只要采了 12 点,
同样用 M12 处理旋转工件
;眼在手上 (Moving) 若只采 9 点(或强制 9 点),也一样只平移不转。
选型一句话:
相机跟着法兰动 → Moving;相机固定不动 → Fixed;工件方向被机械保证不变、只做 XY 纠偏 → 9 点(不采旋转点,或引导时置
transOnlyMode=true);工件会在盘面内自转 → 采十二点(Moving/Fixed 均可)。
2. 坐标与角度约定
-
像素坐标 p=(u,v):u 向右、v 向下(图像原点在左上角);图像角度 r 单位度,顺时针为正(视觉匹配工具输出)。
-
物理坐标 P=(X,Y):机器人世界 / 用户坐标系,X、Y 为平面两轴;法兰角度 R 单位度,逆时针为正(机器人控制器口径)。
-
齐次表示:像素
p~ = [u,v,1]ᵀ,平面仿射统一写成 3×3 齐次矩阵。 -
所有角度差在合成前都用
wrap180()规整到 (−180°, 180°];−180° 与 +180° 是同一方向。 -
长度单位:像素 px、物理 mm;角度:度(deg)。
3. 十二点标定原理与公式
3.1 采点规范(至少 12 点)
-
前 9 个为平面平移点(九宫格):法兰姿态 R 保持不变,只改变 X、Y;每个点记录像素 (u,v,r) 与法兰 (X,Y,R)。
-
后 n(n≥3)个为旋转点 :以第 5 点(索引 4)为旋转轴,法兰 X、Y 不变、只改变 R,让标记在图像中绕圆心画圆弧。
-
标准 12 点中第 11 点(索引 10)与第 5 点重合(旋转序列的中间姿态);旋转点物理 X、Y 与第 5 点一致,仅 R 不同。
-
旋转点越多、角度跨度越大,圆心与角度映射越稳(见 §13 病态说明)。
3.2 第一步:九点平移仿射 M9(最小二乘)
对每个平移点,把像素齐次 p~_i 拟合到 "相对第 5 点 S5 的物理向量" q_i:
q_i = M9 · p~_i
Fixed(相机静止):q_i = S_i − S5
Moving(相机动):q_i = S5 − S_i (相机位移与像素位移反向,故取负号)
把每行写成 [u v 1]·P = q,堆叠成超定方程组 L·P = B,用 SVD 最小二乘求解(对超定、轻微病态最稳健):
L(9×3,第 i 行 [u_i, v_i, 1])· P(3×2) = B(9×2) ⇒ P = L⁺B
⎡ a b tx ⎤
M9 = ⎢ c d ty ⎥ 像素齐次 → 相对第5点物理向量
⎣ 0 0 1 ⎦
3.3 第二步:像方旋转圆心 c
旋转点在图像中位于同一圆周上,拟合其圆心 c=(c_u,c_v) 与半径 ρ:
-
恰好 3 点 :三点确定唯一外接圆(精确解,由圆方程
x²+y²+Dx+Ey+F=0消元)。 -
多于 3 点 :先用 Pratt 代数拟合 (广义特征值,约束
B²+C²−4AD=1,对 "大半径圆只采到一小段弧" 的病态远比 Kasa 稳健),再做高斯 - 牛顿几何精化 (最小化点到圆几何距离|p_i−c|−ρ)。拟合前对坐标做质心中心化以改善数值条件。
3.4 第三步:物方圆心与原点归一化 M12
把像方圆心经 M9 映射,得到 "相对第 5 点口径" 的物方圆心:
c_rel = M9 · c~
再做一次平移,把仿射原点从第 5 点搬到旋转中心,得到 VM 直接输出的十二点矩阵:
M12 = T(−c_rel) · M9
物方圆心有两个账面口径(同一物理点,原点不同):
| 字段 | 公式 | 含义 |
|---|---|---|
cWorldRelP5 |
M9 · c~ = c_rel |
相对第 5 点口径,仅用于 M12 推导 / 诊断 |
cWorld |
L·c = −M12 平移项 |
VM 界面口径,对应 XML 的 RotCenterWorldPoint |
L 为 M12 的 2×2 线性部分;M12 平移项恒为 −L・c。两口径相差仿射平移项 −L・p5。
3.5 第四步:角度映射 R = k・r + b
用旋转点的图像角 r 与法兰角 R(两侧先做角度解缠绕 unwrap,避免跨 ±180°)做一维最小二乘:
k = Σ(r−r̄)(R−R̄) / Σ(r−r̄)² , b = R̄ − k·r̄
-
斜率 k 的理论幅值为 1,符号即手性 (图像角与法兰角是同向还是反向)。符号由相机安装的镜像 / 坐标系手性 决定,并不按 Moving/Fixed 固定 ------ 同一种安装方式、相机正反装不同,k 都可能为 +1 或 −1,一律以实际拟合符号 s=sign (k) 为准。
-
截距 b 为图像零位与法兰零位的偏置角。
-
关键:引导时只取 sign (k)=±1,不用拟合幅值。 拟合幅值仅用于精度诊断(VM 引导内部即按 ±1 计算,输出为整数角度)。
-
rotDirectionState(对标 VM RotDirectionState):法兰 R 增大时旋转点在图像中绕圆心顺时针 → −1,逆时针 → +1,仅作采集方向诊断,不参与解算。
3.6 精度指标(残差 RMS)
| 指标 | 含义 | 单位 |
|---|---|---|
| translate_physical_rmse | 平移点 q_i 与 M9・p~_i 的物理残差 | mm |
| translate_pixel_rmse | 用 M9 逆映射回像素后的残差 | px |
| rotate_circle_rmse | 旋转点到拟合圆的半径残差 | px |
| angle_map_rmse | R−(k・r+b) 的角度残差(按最短角) | deg |
4. 单点抓取引导(Moving 为已验证主线)
4.1 三个必须分清的输入
| 概念 | 字段 | 说明 |
|---|---|---|
| 基准(像方) | markPx [u,v,r] |
工作台上摆一个基准工件,相机识别其特征点像素与方向,整批不变 |
| 示教(抓取位) | teach [X,Y,R] |
机器人实际抓到该基准工件时的法兰位姿(要发给机器人的基准) |
| 基准拍照物理点 | calibSnap [X,Y,R] |
建立基准时的相机拍照法兰位,Moving 下取标定第 5 点 |
| 运行像方点 | runPx [u,v,r] |
当前工件被识别到的特征点像素与方向(每个工件变化) |
| 本次拍照物理点 | runSnap [X,Y,R] |
拍当前工件时的法兰位;Moving 必填,Fixed 内部忽略 |
基准拍照物理点的三个作用:
① 把局部坐标还原成世界绝对坐标;② 作为旋转补偿的参考中心;③ 与本次拍照位做差,补偿移动拍摄 / 多工位带来的机位偏差。Fixed 模式下三者退化为常数第 5 点。
4.2 为什么角度要分 "姿态通道" 和 "位置公转通道"
本套数据中标定矩阵线性部分满足 det(L) < 0(像素 u 右 v 下 → 机器人 XY 是镜像映射),图像中的角运动映射到机器人 XY 平面后方向被翻转一次。经 VM 真实数据验证,必须把角度拆成两个符号可能相反的通道:
-
法兰姿态角 (夹爪自身朝哪)与图像角差反号;
-
抓取点绕特征点的公转 (抓取位置)与图像角差同号。
4.3 完整公式链(12 点;Moving 为已验证主线,Fixed 同样适用)
记 C = 基准拍照位.XY,S = 本次拍照位.XY,T=teach.XY,σ=rotateSign(默认 +1,现场方向反了改 −1)。Moving:C=calibSnap、S=runSnap;Fixed:C=S=baseSnap(第 5 点 TCP)。
① 选矩阵与局部映射(9 点用 M9,12 点用 M12,与相机在手上/手外无关):
m = M · p~_m , u0 = M · p~_run (M = transOnly ? M9 : M12)
② 相机自转补偿(旋转拍照使能,见 §6;关闭时 Δφ=0):
u = R(Δφ) · u0
③ 还原世界特征点:
P_m = m + C , P_r = u + S
④ 双通道角度(Δr = r_run − r_m,s = sign(k)):
Moving:kPose = −s,kPos = +s(姿态/位置异号)
Fixed :kPose = +s,kPos = +s(姿态/位置同号),且 Δφ≡0
drPose = wrap180( kPose·Δr + Δφ ) 法兰 R 增量
drPos = wrap180( kPos·Δr − Δφ ) XY 平面公转角(Fixed 时 Δφ=0)
⑤ 三项位移:
moveOffset = u − m (工件平移)
snapOffset = S − C (拍照位变化,Fixed 恒 0)
w = T − P_m (示教抓取点相对基准特征点的偏距)
⑥ 抓取点绕特征点公转:
rotated = P_m + R(σ·drPos) · w
⑦ 合成绝对抓取位姿:
T_new = rotated + moveOffset + snapOffset
R_new = wrap180( T.R + drPose )
⑧ 相对抓取位姿(对标 VM"相对坐标"):
relative = T_new − T (R 分量按最短角)
直观理解:
示教时记住 "抓取点相对特征点的偏距 w"。工件平移多少,抓取点跟着平移多少(moveOffset/snapOffset);工件转了一个角度,抓取点就绕着当前特征点把偏距 w 反向公转同一角度(rotated 项),夹爪自身姿态再叠加角度增量。
5. 眼在手外 Fixed 的差异(Fixed 同样支持 12 点旋转)
先纠正一个关键认知: Fixed(相机固定)不是 "只能用 M9、不能处理旋转"。选用 M9 还是 M12 由采 9 点还是 12 点决定,与相机装哪无关。Fixed 采了 12 点,引导一样用 M12、一样能抓盘面内自转的工件;Fixed 与 Moving 的差别只在 "相机动不动" 带来的拍照位补偿项。
| 环节 | Moving(眼在手上) | Fixed(眼在手外) |
|---|---|---|
| 局部映射矩阵 | 12 点 M12 / 9 点 M9 | 同样 12 点 M12 / 9 点 M9 |
| 世界特征点 | P = M·p + S |
P = M12·p + baseSnap.XY(9 点用 M9) |
| C、S | calibSnap / runSnap | C = S = baseSnap(第 5 点 TCP) |
| snapOffset | = S − C(多工位要补) | 恒 0(相机不动,无机位差) |
| 旋转拍照 Δφ | 按需补偿(相机可能自转) | 强制 0(相机不会转) |
| 角度系数 | 姿态 −s / 位置 +s(异号) | 姿态 +s / 位置 +s(同号) |
| 工件盘面内旋转 | 12 点支持 | 12 点同样支持;9 点不支持 |
5.1 为什么 M12 在 Fixed 下严格成立(推导)
Fixed 采点时,标定 mark 装在法兰上、相对 TCP 有固定装夹偏置 h0。九点仿射拟合的是 "mark 像素 → 法兰 TCP 世界坐标",因此对同一成像平面上的任意像素 p,M9 都带着同一个常数偏置:
M9·p~ = P_world(p) − R0·h0
旋转点绕 TCP 转,拟合出的像方圆心 c_img 正是 TCP 的成像位置,其 M9 口径物方圆心:
c_rel = M9·c~_img = TCP5 − R0·h0
归一化 M12 = T(−c_rel)·M9 后,装夹偏置常数被消掉:
M12·p~ = P_world(p) − TCP5
引导时再加回绝对化中心 C = TCP5 = baseSnap.XY,即严格还原世界点:
P = M12·p~ + baseSnap.XY = P_world(p)
抓取点公转 rotated = P_m + R(β)·(T−P_m) 绕的是识别到的工件特征点 P_m,与标定圆心是否偏心无关,所以不存在 (R−I)・c 之类的系统误差。数值仿真(含 mark 偏置、镜像相机、工件平移 + 旋转 25°)位置误差约 2e-13 mm、角度 0。
关于此前版本的说明(已废弃):
早期版本曾让 Fixed 一律用 M9,并记录过 "Fixed 用 M12 会有 (R−I)・c_rel 偏心误差"。那是当时把绝对化中心 C 的口径取错(没有正确加回 TCP5)造成的假象,
结论是错的
。现已统一为:
12 点(Moving/Fixed)都用 M12,9 点才用 M9
;Fixed 只是 snapOffset=0、Δφ=0、角度同号。
6. 旋转拍照使能与多拍照工位
6.1 旋转拍照使能(仅 Moving 有意义)
若生产时相机相对标定时刻绕光轴发生了姿态变化,开启 rotSnapEnabled 并给出 Δφ:
-
订阅实时角度 (
subscribeSnapAngle=true):Δφ = wrap180(本次拍照 R − 基准拍照 R),由外部通信(机器人 / PLC)实时下发; -
固定偏差 (
subscribeSnapAngle=false):直接填deltaPhiDeg固定角。
若 25 个拍照点相机姿态始终与基准示教拍照位一致(只平移不转),
不需要
开这个开关;只有相机姿态真的转了才需要。Δφ 同时进入姿态通道与位置通道(Moving 位置通道取反,见 4.3 ④)。
6.2 多拍照工位(如托盘 5×5=25 点,逐点拍摄)
同一高度、相机在 25 个不同平移机位逐一拍摄时,对每个工位:
-
该工位识别到的 runPx 与该工位拍照法兰位 runSnap 一起传入
guide(); -
snapOffset = S − C自动抵消机位移动,输出与机位无关,始终换算到同一世界坐标系; -
相机姿态若全程不变,旋转拍照使能保持关闭;若某工位相机姿态偏转,则开启并订阅该工位实时 R。
数值验证:相机挪到 (30,20)、(−25,15) 等不同工位,纯平移引导输出与机位无关、误差为 0(见 §12)。批量工位可用
loadGuideBatch / saveGuideResults一次导入导出。
7. 纯 9 点平移模式
进入纯 9 点平移有两种途径(眼在手上、眼在手外都适用):
-
标定级: 旋转点少于 3(含 0)时,
calibrate12()自动退化为纯平移标定,置cal.transOnly=true。 -
运行级: 即使做了 12 点标定,若当前工位工件方向被治具保证不变、只想做 XY 纠偏,可在引导输入置
in.transOnlyMode = true,本次引导强制走 9 点平移(忽略图像角差,不转角度、不公转)。求解时取transOnly = cal.transOnly || in.transOnlyMode。
| 项 | 十二点模式 | 纯 9 点平移 |
|---|---|---|
| 触发 | 旋转点 ≥ 3 且未强制 9 点 | 旋转点 < 3,或 in.transOnlyMode=true |
| 拟合 | M9 + 圆心 + 角度 k/b | 仅 M9,M12=M9 |
| 圆心 / 半径 / 方向标志 | 计算 | 不求(circleMethod=−1,置 0) |
| 引导 XY | 平移 + 旋转公转 | 仅平移 |
| 抓取角度 | 按图像角差换算 | 恒定 = 示教 R(drPose=drPos=0) |
| 旋转拍照 Δφ | 生效 | 忽略(无旋转中心) |
| Moving 多工位拍照位补偿 | 生效 | 仍生效 |
| JSON 标志 | "trans_only": false |
"trans_only": true |
纯平移最终位置: T_new = T_示教 + moveOffset + snapOffset , R_new = T_示教.R
适用场景: 料盘 / 托盘等间距阵列取放、定位治具、工装预定位、平面吸盘 / 磁吸盲抓 ------ 工件只有 XY 偏移、方向由料道 / 挡销 / 夹具保证不变。
红线:
工件只要会在盘面内自转,纯平移模式无法把抓取点绕特征点公转、也没有角度手性 / 零位,必然抓不准,那种场合必须采十二点。
8. 工具与旋转轴心不一致的处理
"工具和旋转轴心不一致" 要区分三种完全不同的偏心:
| 情形 | 含义 | 本工程是否处理 / 如何处理 |
|---|---|---|
| (a) 抓取点与视觉特征点 / 工件旋转中心不重合 | 吸盘 / 夹爪接触点 ≠ 相机识别的特征点,工件转动时抓取点绕特征点画圆 | 十二点模式处理 ,正是 rotated = P_m+R(drPos)·w 这段:示教记录偏距 w,工件转 β 后抓取点按 R (β)・w 公转,偏心多大都能算。纯平移模式不能(前提是工件不转)。 |
| (b) 法兰旋转轴与工具 TCP 偏心 | 夹爪 / 吸盘相对 J6 法兰轴偏轴安装 | 属机器人 TCP 标定范畴,视觉标定不直接解;只要在机器人控制器里把 TCP 设准、示教位 T 用该 TCP 下的位姿,公式照样成立(偏心被 TCP 吸收)。 |
| © 相机与法兰不同轴(眼在手上) | 相机光轴不经过法兰旋转中心 | 由 M12 光轴中心归一化 + 拍照位 /Δφ 补偿覆盖,已处理。 |
9. 工程结构与 API
Calib2D_12/
├─ json_mini.hpp 自包含 header-only JSON 解析/生成(无第三方依赖)
├─ vm12_calib.hpp 纯算法接口:数据结构 + 标定/引导函数声明
├─ vm12_calib.cpp 纯算法实现:仿射/圆拟合/角度映射/标定/引导(Eigen)
├─ vm12_io.hpp JSON/文件读写接口声明
├─ vm12_io.cpp JSON/文件读写实现
├─ main.cpp 手动调试入口(你在此填写点对与引导输入)
└─ Calib2D_12.vcxproj VS2017 工程(v141, C++17, x64, /utf-8)
9.1 命名空间与核心类型(vm12_calib.hpp)
namespace vm12;
enum class CameraMode { Fixed, Moving }; // 眼在手外 / 眼在手上
struct CalibPoint { double u,v,r; double X,Y,R; }; // 像素(u,v,r) + 法兰(X,Y,R)
struct CalibResult { // 标定结果
CameraMode mode;
Matrix3d M9, M12; // 九点仿射 / 十二点归一化
Vector2d cImg, cWorld, cWorldRelP5;
double circleRadiusPx; int circleMethod, rotDirectionState;
double k, b; // 物理R = k*图像r + b
Vector3d baseSnap; // 第5点 [X,Y,R]
CalibResiduals res;
bool valid, transOnly;
};
struct GuideInput { // 引导输入(见 §4.1)
Vector3d markPx, teach, calibSnap, runPx, runSnap;
bool rotSnapEnabled, subscribeSnapAngle;
double deltaPhiDeg, rotateSign;
bool transOnlyMode = false; // true=强制纯9点平移(即便做了12点标定),
// 忽略工件旋转,只补XY,Moving/Fixed 均适用
};
struct GuideOutput { // 引导输出
Vector2d mLocal,uLocal,Pmark,Prun;
Vector2d moveOffset,snapOffset,rotateOffset;
Vector3d target; // 绝对抓取 [X,Y,R]
Vector3d relative; // 相对抓取 = target - teach
double drPoseDeg, drPosDeg;
};
9.2 核心函数
Matrix3d fitAffine(transPts, mode, s5); // 九点仿射(最小二乘)
Vector2d fitCircle(rotPts, radius&, method&); // 三点外接圆 / Pratt+GN
double fitAngleMap(rotPts, b&, rmse&); // R=k*r+b
CalibResult calibrate12(transPts, rotPts, mode); // 完整标定(rotPts<3 自动纯平移)
Vector2d pixelToLocal(cal, u, v);
GuideOutput guide(cal, in); // 单点抓取引导
9.3 IO 函数(vm12_io.hpp)
bool loadCalibData(path, mode&, transPts&, rotPts&, err&); // 读原始点(9/12、纯平移均可)
std::string exportCalibData(mode, transPts, rotPts, indent=2); // 原始点 -> JSON 字符串
bool saveCalibData(path, mode, transPts, rotPts, err&); // 原始点导出 JSON
bool saveCalibResult(path, cal, err&);
bool saveCalibResultWithData(path, cal, transPts, rotPts, err&);// 结果+原始点合并归档
bool loadCalibResult(path, cal&, err&);
std::string exportResult(cal, indent=2);
bool loadGuideBatch(path, base&, records&, err&); // 25 工位批量
bool saveGuideResults(path, vector<pair<name,GuideOutput>>, err&);
10. JSON 标定结果字段
| JSON 键 | 对应字段 | 说明 |
|---|---|---|
camera_mode |
mode | "fixed_eye_to_hand" / "moving_eye_in_hand" |
valid |
valid | 标定是否成功 |
trans_only |
transOnly | 是否纯 9 点平移 |
M9_affine.matrix_3x3 |
M9 | 九点仿射 3×3(行优先) |
M12_normalized.matrix_3x3 |
M12 | 十二点归一化 3×3 |
rotation_center.image_center_px |
cImg | 像方圆心 u,v |
rotation_center.world_center_mm |
cWorld | VM 口径物方圆心 |
rotation_center.world_center_rel_p5_mm |
cWorldRelP5 | 相对第 5 点口径 |
rotation_center.circle_radius_px |
circleRadiusPx | 像方圆半径 |
rotation_center.circle_fit_method |
circleMethod | three_point_circumcircle / pratt_plus_gauss_newton |
rotation_center.rot_direction_state |
rotDirectionState | −1/1,仅诊断 |
angle_map.k_consistency |
k | 角度斜率(引导只用其符号) |
angle_map.b_offset_deg |
b | 角度零位偏置 |
base_snap_point_5th.X_mm/Y_mm/R_deg |
baseSnap | 第 5 点法兰位姿 |
residuals_rmse.* |
res | 四项 RMS 精度指标 |
10.1 原始标定数据 JSON(12 点的像方 / 物方原始点,可读回复标)
saveCalibData 导出、loadCalibData 读回;saveCalibResultWithData 把结果与原始点写进同一文件(既能读结果,也能读回原始点重新标定)。
{
"camera_mode": "moving",
"translate_count": 9,
"rotate_count": 3,
"trans_only": false,
"translate_points": [
{ "index":0, "u":1964.91, "v":304.52, "img_angle_deg":90.96,
"X":-577.31, "Y":209.66, "R_deg":179.995 }
],
"rotate_points": [ ]
}
-
camera_mode:"moving"/"fixed"(读回兼容长名 moving_eye_in_hand);rotate_count纯平移为 0。 -
点字段:像方
u / v / img_angle_deg(像素、度),物方X / Y / R_deg(mm、度)。 -
读回仅要求平移点 ≥3,旋转点 ❤️ 自动按纯平移处理。
-
已验证:导出→读回→重新标定,M12 与原标定逐位一致(误差 0)。
11. 使用步骤与代码示例
11.1 标定(Moving 十二点)
std::vector<vm12::CalibPoint> transPts, rotPts;
// 9 个平移点:像素(u,v,r) + 法兰(X,Y,R)
transPts.push_back({1964.91,304.52,90.96, -577.306,209.658,179.995});
// ... 共 9 个
// 3 个(或更多)旋转点:物理 XY 与第5点一致,仅 R 不同
rotPts.push_back({879.35,1037.91,85.37, -557.306,189.658,174.995});
rotPts.push_back({1223.48,1022.42,90.43, -557.306,189.658,179.995});
rotPts.push_back({1567.83,1036.81,95.59, -557.306,189.658,184.995});
vm12::CalibResult cal =
vm12::calibrate12(transPts, rotPts, vm12::CameraMode::Moving);
if (!cal.valid) { /* 点数不足:平移至少 3 */ }
std::string err;
vm12::saveCalibResult("calib_result.json", cal, err); // 导出 JSON
11.2 单点引导(绝对 + 相对)
vm12::GuideInput in;
in.markPx << 640, 512, 30; // 基准像方点 [u,v,r]
in.teach << -557.306, 150.123, 10; // 示教抓取法兰位
in.calibSnap << -557.306, 189.658, 179.995;// 基准拍照物理点(第5点)
in.runPx << 655, 502, 22; // 运行像方点
in.runSnap << -557.306, 189.658, 179.995;// 本次拍照物理点(Fixed忽略)
in.rotSnapEnabled = false; // 相机姿态不变则关闭
in.subscribeSnapAngle = false; // true=订阅实时拍照R
in.deltaPhiDeg = 0.0; // 固定姿态偏差
in.rotateSign = 1.0; // 旋转方向反了改 -1
// in.transOnlyMode = true; // 取消注释=强制9点平移(忽略工件旋转),Moving/Fixed 均可
vm12::GuideOutput o = vm12::guide(cal, in);
// 绝对:o.target (X,Y,R) → 发给机器人
// 相对:o.relative(dX,dY,dR) → 对标 VM"相对坐标"
11.3 纯 9 点平移(两种途径,眼内 / 眼外都适用)
// 途径一:标定级------只采 9 个平移点,rotPts 留空
std::vector<vm12::CalibPoint> transPts, rotPts; // rotPts 留空
// ... 只填 9 个平移点 ...
auto cal = vm12::calibrate12(transPts, rotPts, vm12::CameraMode::Fixed);
// cal.transOnly == true;guide() 调用方式不变,R 自动保持示教角
// 途径二:运行级------已做 12 点标定,但某工位工件方向恒定、只想 XY 纠偏
vm12::GuideInput q; // 其余字段照常填
q.transOnlyMode = true; // 本次引导强制 9 点:不转角度、抓取点不公转
auto o2 = vm12::guide(cal12, q); // Moving 多工位的拍照位平移补偿仍生效
11.4 现场验证顺序建议
-
先纯平移核对 XY:工件不转、只移动,确认 X、Y 偏差正确;
-
再纯旋转核对 R:工件只转、不移,确认角度方向与数值;
-
方向若整体反了,把
rotateSign改为 −1; -
最后做平移 + 旋转复合与多工位验证。
12. 验证记录与精度
12.1 Moving 与 VM「单点抓取」逐位对比(真实标定数据)
输入:mark (640,512,30),teach (−557.306,150.123,R10),calibSnap/runSnap = 第 5 点 (−557.306,189.658,179.995),run (655,502,22),旋转拍照关闭。
| 输出 | 本工程 | VM | 差值 |
|---|---|---|---|
| 绝对 X (mm) | −563.8918 | −563.8935 | 0.002 |
| 绝对 Y (mm) | 167.3354 | 167.2851 | 0.050 |
| 绝对 R (°) | 18.0000 | 18 | 0 |
| 相对 X (mm) | −6.5857 | −6.5874 | 0.002 |
| 相对 Y (mm) | 17.2124 | 17.1621 | 0.050 |
| 相对 R (°) | 8.0000 | 8 | 0 |
残余约 0.05 mm 的 Y 差来自三点圆病态:VM 像方圆心与本工程纯外接圆相差约 12 px(VM 用内部亚像素 / 联合优化,并非对这 3 点做纯外接圆),传导到 M12 平移项后约 0.05 mm。多采旋转点后会进一步缩小。
12.2 纯平移双模式 / 多工位闭环(数值仿真)
| 场景 | 结果误差 |
|---|---|
| Fixed 纯平移 | 0, 0 |
| Moving 本工位 (0,0) | 0, 0 |
| Moving 工位 (30,20) | 0, 0 |
| Moving 工位 (−25,15) | 0, 0 |
12.3 Fixed 12 点旋转 / 9 点平移闭环(数值仿真,已按统一矩阵口径复算)
合成眼在手外几何:镜像相机(u=kX+c_u,v=−kY+c_v)、mark 相对 TCP 装夹偏置 h0=(5,−8) mm,9 平移点 + 3 旋转点(±10°);工件再叠加平移 d=(15,−9) mm、自转 β=25°。
| 场景 | 位置 / 角度误差 | 结论 |
|---|---|---|
| Fixed 12 点,工件平移 + 旋转 25° | (2.2e−13, 4.6e−13) mm,角度 0 | M12 + C=baseSnap 严格命中,Fixed 能处理旋转 |
| Fixed 强制 9 点(transOnlyMode),只平移 | ≈1e−15 mm,角度恒定 | 正确忽略旋转、只补 XY |
| Moving 12 点(VM 金标准回归) | 与 VM 差 ≤0.00005 mm、角度 0 | 统一矩阵后 Moving 无回归 |
注:早期文档此处曾写 "Fixed 必须用 M9、用 M12 会有 (R−I)・c_rel 偏心误差",那是绝对化中心 C 口径取错造成的误判,已废弃,正确结论见 §5.1。
12.4 VM 标定数据复算结论(Moving)
-
VM 矩阵线性 L 乘点 0 相对点 5 的像素差 = S5−S0,与 Moving 约定 B=S5−S 一致,模式选 Moving 正确;
-
det (L)=−7.53e−4 < 0(镜像手性),RotDirectionState=−1,与 VM 一致;
-
平移物理 RMS≈0.020 mm、像素 RMS≈0.74 px、角度 RMS≈0.025°。
13. 注意事项与 FAQ(踩坑记录)
13.1 三个旋转点极度病态时圆心会偏
注意:
该组数据像方圆半径约 3961 px,却只采到张角约 10° 的一小段弧(三点 Y 最大差仅约 16 px)。三点外接圆虽是数学唯一解,但对噪声极敏感,VM 内部并非纯外接圆,导致圆心账面差约 12 px。
建议:
旋转点采 5~9 个、角度跨度 ±30° 左右(在机构允许范围内尽量大),程序会自动切换 Pratt + 高斯 - 牛顿几何拟合,圆心与角度映射明显更稳。
13.2 Eigen 的 auto 表达式悬空(曾导致结果 NaN)
注意:
lambda / 函数中用
auto直接返回
(M*h).head<2>()这类 Eigen 表达式时,返回的是持有局部变量引用的
未求值表达式
,离开作用域后引用悬空,结果可能变成 0 或 NaN。
正确写法
显式指定返回值类型强制当场求值:
auto toLocal = [&](double u,double v) -> Eigen::Vector2d {
Eigen::Vector3d h(u,v,1.0);
return (Mloc*h).head<2>(); // 立即求值,勿用 auto 返回
};
13.3 角度显示 −180 还是 +180
角度规整用 atan2 (sin,cos),恰好在 ±180° 边界时受浮点影响可能显示 −180。−180° 与 +180° 是同一法兰方向,机器人等价识别,不影响抓取。若现场必须统一为 +180,可在输出层加一行边界归一化(不建议改 wrap180,以免影响角度解缠绕)。
13.4 角度斜率 k 只用于定符号
拟合出的 k 幅值(如 0.979)只用于精度诊断;引导一律按理论 ±1 计算,否则会出现 VM 输出整数 18°、程序输出 17.83° 的偏差。
13.5 rotateSign 是最终人工兜底
位置旋转补偿方向默认 rotateSign=+1。若实抓发现工件旋转后抓取点绕的方向整体反了(极少数机器人 XY 角度正向特殊),改为 −1 即可;先用小角度转动试验确认。
13.6 VS2017 编译:编码与语言标准
-
源文件含中文注释,统一保存为 UTF-8 带 BOM ,并在 vcxproj 四个配置加
/utf-8,否则 MSVC 按 GBK 解析会报大量 C3861/C2039/C2143 之类的假错误; -
语言标准 C++17、平台 x64;Pratt 拟合需要
#include <Eigen/Eigenvalues>(GeneralizedSelfAdjointEigenSolver); -
Eigen 仅需头文件路径,无链接依赖。
13.7 常见疑问速查
| 现象 / 疑问 | 结论 |
|---|---|
| VM 编辑标定点表里没有像素角度列 | 像素角度 r 由 "图像角度" 端子绑定匹配工具在采样时自动采集,表中唯一 "角度" 列是物理 R;平移点不用角度,旋转点两者都用 |
| 只采 9 点能用吗 | 能,自动纯平移,只补 XY、R 不变;Moving/Fixed 均可。工件会转则必须 12 点 |
| 固定式相机 (Fixed) 能抓旋转工件吗 | 能。 只要采了 12 点,Fixed 一样用 M12 做旋转公转;用 M9 还是 M12 只取决于 9/12 点,与相机装哪无关 |
| 做了 12 点但某工位不想转 | 引导时置 in.transOnlyMode=true 即可强制 9 点平移,不必重新标定 |
| Fixed 要不要传拍照位 / 开旋转拍照 | 都不需要;内部 C=S = 第 5 点、snapOffset=0、Δφ=0,但 12 点的旋转补偿照常生效 |
| 25 个拍照位怎么处理 | 每个工位传 runSnap,snapOffset 自动抵消;姿态变了再开旋转拍照并订阅实时 R |
| 夹爪中心和特征点不重合 | 十二点模式的旋转公转项就是为此而设,可直接兼容 |
本文档随工程代码维护,公式与 vm12_calib.hpp/vm12_calib.cpp/vm12_io. 实现一一对应。Moving 链路已用 VisionMaster 真实标定数据逐位验证。*
上述文本时直接AI生成的,源码我感觉把这个文档贴进去,代码应该就出来差不多了
数据直接使用例程xml里面的数据
xml
<?xml version="1.0" encoding="UTF-8"?>
<CalibInfo>
<CalibInputParam>
<CalibParam ParamName="CreateCalibTime" DataType="string">
<ParamValue>2023-02-22 14:06:19</ParamValue>
</CalibParam>
<CalibParam ParamName="CalibType" DataType="string">
<ParamValue>NPointCalib</ParamValue>
</CalibParam>
<CalibParam ParamName="TransNum" DataType="int">
<ParamValue>9</ParamValue>
</CalibParam>
<CalibParam ParamName="RotNum" DataType="int">
<ParamValue>3</ParamValue>
</CalibParam>
<CalibParam ParamName="CalibErrStatus" DataType="int">
<ParamValue>0</ParamValue>
</CalibParam>
<CalibParam ParamName="TransError" DataType="float">
<ParamValue>0.61060435</ParamValue>
</CalibParam>
<CalibParam ParamName="RotError" DataType="float">
<ParamValue>0.063306116</ParamValue>
</CalibParam>
<CalibParam ParamName="TransWorldError" DataType="float">
<ParamValue>0.016752763</ParamValue>
</CalibParam>
<CalibParam ParamName="RotWorldError" DataType="float">
<ParamValue>0.0017368897</ParamValue>
</CalibParam>
<CalibParam ParamName="PixelPrecisionX" DataType="float">
<ParamValue>0.027476067</ParamValue>
</CalibParam>
<CalibParam ParamName="PixelPrecisionY" DataType="float">
<ParamValue>0.027396675</ParamValue>
</CalibParam>
<CalibParam ParamName="PixelPrecision" DataType="float">
<ParamValue>0.027436364</ParamValue>
</CalibParam>
<CalibPointFListParam ParamName="ImagePointLst" DataType="CalibPointList">
<PointF>
<X>1964.9121</X>
<Y>304.52209</Y>
<R>90.960564</R>
</PointF>
<PointF>
<X>1952.2449</X>
<Y>1034.9376</Y>
<R>90.810684</R>
</PointF>
<PointF>
<X>1938.5756</X>
<Y>1764.2733</Y>
<R>91.159958</R>
</PointF>
<PointF>
<X>1211.0314</X>
<Y>1751.4301</Y>
<R>90.752579</R>
</PointF>
<PointF>
<X>1223.4573</X>
<Y>1022.4023</Y>
<R>90.440575</R>
</PointF>
<PointF>
<X>1236.803</X>
<Y>291.47818</Y>
<R>90.699593</R>
</PointF>
<PointF>
<X>509.33368</X>
<Y>278.73111</Y>
<R>90.595367</R>
</PointF>
<PointF>
<X>497.3317</X>
<Y>1008.9418</Y>
<R>90.335953</R>
</PointF>
<PointF>
<X>484.5603</X>
<Y>1736.9171</Y>
<R>90.639099</R>
</PointF>
<PointF>
<X>879.34839</X>
<Y>1037.9055</Y>
<R>85.374908</R>
</PointF>
<PointF>
<X>1223.479</X>
<Y>1022.4151</Y>
<R>90.426163</R>
</PointF>
<PointF>
<X>1567.8267</X>
<Y>1036.8088</Y>
<R>95.586868</R>
</PointF>
</CalibPointFListParam>
<CalibPointFListParam ParamName="WorldPointLst" DataType="CalibPointList">
<PointF>
<X>-577.30603</X>
<Y>209.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-557.30603</X>
<Y>209.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-537.30603</X>
<Y>209.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-537.30603</X>
<Y>189.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-557.30603</X>
<Y>189.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-577.30603</X>
<Y>189.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-577.30603</X>
<Y>169.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-557.30603</X>
<Y>169.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-537.30603</X>
<Y>169.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-557.30603</X>
<Y>189.658</Y>
<R>174.995</R>
</PointF>
<PointF>
<X>-557.30603</X>
<Y>189.658</Y>
<R>179.995</R>
</PointF>
<PointF>
<X>-557.30603</X>
<Y>189.658</Y>
<R>184.995</R>
</PointF>
</CalibPointFListParam>
</CalibInputParam>
<CalibOutputParam>
<CalibParam ParamName="RotDirectionState" DataType="int">
<ParamValue>-1</ParamValue>
</CalibParam>
<CalibParam ParamName="IsRightCoorA" DataType="int">
<ParamValue>1</ParamValue>
</CalibParam>
<PointF ParamName="RotCenterImagePoint" DataType="CalibPointF">
<RotCenterImagePointX>1228.5143</RotCenterImagePointX>
<RotCenterImagePointY>4983.2388</RotCenterImagePointY>
<RotCenterImageR>-999</RotCenterImageR>
</PointF>
<PointF ParamName="RotCenterWorldPoint" DataType="CalibPointF">
<RotCenterWorldPointX>-135.90598</RotCenterWorldPointX>
<RotCenterWorldPointY>-36.196297</RotCenterWorldPointY>
<RotCenterWorldR>-999</RotCenterWorldR>
</PointF>
<CalibFloatListParam ParamName="CalibMatrix" DataType="FloatList">
<ParamValue>0.00048535553</ParamValue>
<ParamValue>-0.027392274</ParamValue>
<ParamValue>135.90598</ParamValue>
<ParamValue>-0.027471779</ParamValue>
<ParamValue>-0.00049101119</ParamValue>
<ParamValue>36.196297</ParamValue>
<ParamValue>0</ParamValue>
<ParamValue>0</ParamValue>
<ParamValue>1</ParamValue>
</CalibFloatListParam>
</CalibOutputParam>
</CalibInfo>
代码拟合的
xml
{
"M12_normalized": {
"matrix_3x3": [
[
0.0004968749426612299,
-0.027401719774454183,
136.26567436383823
],
[
-0.027485744087265174,
-0.00048269480127579797,
36.215651618223042
],
[
0,
0,
1
]
],
"note": "pixel_homogeneous -> local physical coord (rotation center as origin)"
},
"M9_affine": {
"matrix_3x3": [
[
0.0004968749426612299,
-0.027401719774454183,
27.382964304570251
],
[
-0.027485744087265174,
-0.00048269480127579797,
34.142502041521666
],
[
0,
0,
1
]
]
},
"angle_map": {
"b_offset_deg": 91.413393141189019,
"k_consistency": 0.97920645094118541,
"note": "physical_R_deg = k * image_angle_deg + b; fixed k=-1, moving k=+1 (standard convention)"
},
"base_snap_point_5th": {
"R_deg": 179.995,
"X_mm": -557.30602999999996,
"Y_mm": 189.65799999999999
},
"camera_mode": "moving_eye_in_hand",
"residuals_rmse": {
"angle_map_rmse_deg": 0.025261672109529405,
"rotate_circle_rmse_px": 2.6254850544756925e-13,
"translate_physical_rmse_mm": 0.020356486273858922,
"translate_pixel_rmse_px": 0.74173516292919139
},
"rotation_center": {
"circle_fit_method": "three_point_circumcircle",
"circle_radius_px": 3972.7793585314125,
"image_center_px": [
1229.8921074480224,
4995.1892823096514
],
"rot_direction_state": -1,
"world_center_mm": [
-136.26567436383823,
-36.215651618223042
],
"world_center_rel_p5_mm": [
-108.88271005926798,
-2.0731495767013755
]
},
"trans_only": false,
"valid": true
}
误差还是有的,主要误差来源是旋转中心拟合的有偏差,这个我看度数只有左右5度左右,误差会比较大,实际使用要调整大一点,几个AI工具算下来和他官方这个稍微差点,不清楚啥原因

抓取:
VM结果:


代码结果,绝对抓取位,我看都低于0.1mm以下:
