VisionMaster 十二点标定+单点抓取

海康 VisionMaster 十二点标定・C++ 工程技术文档

九点平移仿射 + 旋转圆心归一化 + 角度映射,含单点绝对 / 相对抓取引导、旋转拍照补偿、多拍照工位与纯平移模式。

矩阵运算全部基于 Eigen | 结果 JSON 导入导出 | 眼在手外 (Fixed) / 眼在手上 (Moving) / 纯 9 点平移

环境 说明
开发环境 VS2017 (v141) · C++17 · x64
依赖 Eigen 3.3.9(仅头文件)
Moving 已与 VM 真实数据逐位对齐
纯平移 双模式闭环 0 误差

目录

  1. 功能概述与模式选择

  2. 坐标与角度约定

  3. 十二点标定原理与公式

  4. 单点抓取引导(Moving 为已验证主线)

  5. 眼在手外 Fixed 的差异

  6. 旋转拍照使能与多拍照工位

  7. 纯 9 点平移模式

  8. 工具与旋转轴心不一致的处理

  9. 工程结构与 API

  10. JSON 标定结果字段

  11. 使用步骤与代码示例

  12. 验证记录与精度

  13. 注意事项与 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 个不同平移机位逐一拍摄时,对每个工位:

  1. 该工位识别到的 runPx 与该工位拍照法兰位 runSnap 一起传入 guide();

  2. snapOffset = S − C 自动抵消机位移动,输出与机位无关,始终换算到同一世界坐标系;

  3. 相机姿态若全程不变,旋转拍照使能保持关闭;若某工位相机姿态偏转,则开启并订阅该工位实时 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 现场验证顺序建议

  1. 先纯平移核对 XY:工件不转、只移动,确认 X、Y 偏差正确;

  2. 再纯旋转核对 R:工件只转、不移,确认角度方向与数值;

  3. 方向若整体反了,把 rotateSign 改为 −1;

  4. 最后做平移 + 旋转复合与多工位验证。


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以下:

相关推荐
Lzg_na3 小时前
僵尸进程、孤儿进程、守护进程:含义、区别与生命周期详解
c++
zhanghaha13143 小时前
C++ 的类是什么(新手保姆讲解,配合 Qt 对照理解)
c++
aqiu1111113 小时前
【C++算法打怪专栏】LeetCode 402. 移掉 K 位数字(单调栈 + 贪心)
c++·算法·leetcode
(Charon)3 小时前
【C++面试】线程并发与异步:区别、实现原理及使用场景
开发语言·c++
JMchen4 小时前
实战项目一——实现一个高性能图片滤镜App
android·c++·性能优化
码匠许师傅4 小时前
【C++三方组件】libpqxx:PostgreSQL 的 C++ 客户端
开发语言·c++·postgresql
H.莓飛4 小时前
【C++】内存管理
linux·开发语言·jvm·c++·visual studio
longlongzihan5 小时前
从LeetCode 19. 删除链表的倒数第 N 个结点深入理解“哨兵节点”
c++·算法·leetcode·链表
爱吃提升5 小时前
Dev-C++ 5.11 完整版快捷键大全(信奥/初学适用)
c语言·c++