ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CoppeliaSim仿真手眼标定:3D相机搭建与数据采集实战

CoppeliaSim仿真手眼标定:3D相机搭建与数据采集实战 手眼标定这四个字第一次听到的人多半会猜是某种医学操作。在机器人圈子里它指的是让机械臂知道相机看到的东西落在自己坐标系的什么位置——说白了就是求相机与机械臂末端或机械臂基座之间那个固定不变的变换矩阵。真机上做这件事成本高、复现难、翻车点多。我在三种不同构型的工位上折腾过手眼标定最深的体会是真正难的从来不是最后那几行求解代码而是数据采得干不干净、坐标系有没有约定清楚。这也是我把手眼标定的第一站放进 CoppeliaSim很多人更熟悉它改名前的老名字 v-REP的原因。在仿真里我能拿到干净的位姿真值能随时重置重来、随时换构型把整条链路先跑通等回到真机只需要专心对付现实误差这一件事。这篇是系列的第一部分主题是仿真环境下的机器人 3D 相机搭建、手眼标定的数学骨架梳理以及标定数据的采集与筛选。如果你手里有机械臂、有深度相机、正准备做视觉引导抓取或者实时视觉追踪这篇可以直接当施工图看。1. 为什么把手眼标定的第一站放在 CoppeliaSim1.1 真机标定的三个现实门槛先说清楚真机标定到底卡在哪里。手眼标定的输入是一组机械臂末端位姿 相机观测到的标定板位姿的配对数据理论上十几组数据就能解出结果。但实际做起来门槛集中在三个地方。第一是位姿精度不可控。机械臂末端位姿通常从控制器读取读数受关节零位误差、连杆参数误差、减速器回程间隙影响标称重复定位精度 0.02mm 的机器人在大臂展姿态下末端绝对误差可能到零点几毫米甚至更高。这些误差会原封不动地进入标定矩阵最后表现为标定结果看着收敛了实际抓取偏了两毫米。第二是数据采集效率低。每换一个姿态就要等机械臂停稳、等相机曝光、等算法检测角点。一个姿态三十秒二十个姿态就是十分钟中间任何一次检测失败都要重来。碰上机械臂运动范围受限、工作空间里到处是遮挡的产线工位凑够一组旋转角度足够分散的数据能耗掉半天。第三是失败成本。真机上撞一下就是真金白银标定板被机械臂蹭一下、相机线被拽一下都得停机排查。所以真机标定的合理姿势是先在仿真里把全流程跑顺把代码调通把参数定下来真机上只做验证和微调。1.2 仿真平台为什么选 CoppeliaSim机器人仿真平台不少Gazebo、Isaac Sim、MuJoCo、Webots 各有所长。做手眼标定这个具体任务我最终固定在 CoppeliaSim 上有几个很实际的理由。一是场景搭建快。CoppeliaSim 里拖一个视觉传感器就是一台相机改两个参数就换镜头不用写 URDF 也不用配 SJMS 插件。手眼标定要反复调整相机的安装位置和朝向做对比实验这种改一次要重新编译整个仿真包的工作流我实在受不了。二是接口干净。CoppeliaSim 的 ZMQ Remote API 让 Python 脚本可以直接读写仿真里任意对象的位姿、关节角、传感器数据采集一组标定数据的代码量大概几十行。对比之下很多平台取一次传感器数据要写一大堆桥接代码。三是它自带真值。仿真里我可以直接从场景图计算出相机相对末端的真实变换拿它和标定算法的输出做对比误差来源一眼可辨。这个能力在真机上是不存在的也是仿真最大的价值——你有一个可以拿来对答案的标准答案。注意CoppeliaSim 从 V4.0 开始更名网上大量教程还写着 v-REP两者的脚本 API 大方向一致但新版推荐用 ZMQ Remote API 而不是老的 legacy remote API。查资料时按手里的版本号去筛混用容易踩坑。1.3 仿真标定能验证什么、不能验证什么这一点必须提前讲清楚否则很容易产生错误预期。仿真能验证的坐标系的定义方向对不对这个坑特别多、手眼方程组的构造是否正确、求解算法的数值稳定性、采样姿态的分布是否合理、标定流程的代码是否跑得通、不同求解方法在同一批数据上的差异。这些是逻辑层的问题仿真能百分之百暴露。仿真不能验证的相机内参的实际畸变、镜头与法兰的装配偏差、机械臂的实际运动学误差、标定板加工精度、光照变化对检测的影响。这些是物理层的问题只能在真机上面对。我一般把仿真当标定算法的单元测试用。仿真里跑出 0.01mm 级别的重投影误差不稀奇别拿这个数字去对外宣称精度它的意义是证明你的代码逻辑没问题剩下的误差才轮到真机去贡献。2. 从零搭一个带 3D 相机的标定工位2.1 机械臂模型导入后必须先核对的三件事CoppeliaSim 导入机械臂模型有两条路一是用自带的模型浏览器里现成的机械臂二是从 URDF 导入。URDF 导入的路径更通用但导入后有三件事必须逐项核对我踩过不止一次。第一关节零位与旋转方向。导入的模型里关节零位往往是 URDF 作者定义的未必和你手里机器的零位一致。检查方法很简单把所有关节角设为零看末端姿态是不是你预期的姿态再单独动一个关节看旋转方向正负是否符合直觉。方向反了不会报错但会让后续所有采样姿态变得莫名其妙。第二关节是运动学模式还是动力学模式。手眼标定采集只需要纯运动学不需要动力学仿真把关节设为运动学模式Kinematic可以避免重力导致的关节下垂和抖动采集速度快一个量级。判断方式是在关节属性里看它有没有被动力学引擎接管——如果设了目标位置但关节慢慢飘过去还带振荡那就是动力学模式。第三基座坐标系。机械臂基座在世界坐标系下的位姿决定了 T_world_base这个矩阵后面会反复用到。导入后如果模型不是摆在世界原点一定要把这个变换记下来或者在场景树里把基座放到原点省掉一堆麻烦。-- 在 CoppeliaSim 的脚本里快速核对关节信息 local jointHandles {} for i 1, 6 do local h sim.getObject(./joint .. i) jointHandles[i] h -- 打印关节当前角度与类型 print(string.format(joint%d pos%.4f, i, sim.getJointPosition(h))) end提示如果模型里关节命名不规整别急着写死路径用sim.getObjectsInTree遍历场景树把所有关节句柄捞出来按名字排序后再索引。模型换了也不用改代码。2.2 3D 相机的两种建模方式图像式与真值式仿真里做视觉最重要的一个决策是你到底要不要渲染出真实图像。方式一是真实渲染。在场景里放一个视觉传感器Vision Sensor设置分辨率、视场角、近远裁剪面它可以输出 RGB 图像和深度图。然后再用 OpenCV 去检测标定板角点、做 PnP 求位姿。这条路最接近真机因为它把图像处理引入的位姿误差也一起模拟进去了。方式二是直接读真值。标定板在场景里是一个对象你可以直接读它的世界位姿相机也是对象也能读世界位姿两者一除就得到 T_cam_target。这条路完全跳过图像处理得到的是理想数据。我的建议是两条路都搭一遍然后对比结果。真值路径用来验证求解代码渲染路径用来评估图像处理环节引入了多少误差。如果两路结果的差距大得离谱说明问题在图像处理而不是标定算法。对于 3D 相机比如结构光、ToF、双目这类输出点云或深度图的设备仿真里的建模方式是视觉传感器负责出 RGB 和深度缓冲再按相机内参把深度图反投影成点云。CoppeliaSim 的视觉传感器可以直接取深度缓冲取值是 0 到 1 的归一化值需要按近远裁剪面换算成实际距离。# 深度值换算成实际米数 near sim.getObjectFloatParam(cam_handle, sim.visionfloatparam_near_clipping) far sim.getObjectFloatParam(cam_handle, sim.visionfloatparam_far_clipping) depth_m near * far / ((far - near) * depth_norm near) # 透视相机的非线性映射这里要注意一个容易忽略的点不同引擎、不同相机类型归一化深度到实际距离的换算公式不一致。正投影相机是线性的透视相机是上面这种非线性关系。公式写错的话点云会被整体拉伸但在视觉上又很难一眼看出来因为场景里恰好有一块平面的时候看起来还是平的。我的做法是放一个已知尺寸的立方体在固定距离上量一下点云里的边长对得上再往下做。2.3 手眼安装位姿与坐标系约定的第一原则相机怎么装直接决定后面标定方程的形式这个必须在动手前定死。Eye-in-Hand眼在手上相机固定在机械臂法兰或末端执行器上跟着机械臂一起动。优点是相机视野随机械臂移动可以近距离观察目标标定精度相对好。缺点是相机线缆要跟着动工作空间受限。这是绝大多数视觉引导抓取项目的选择。Eye-to-Hand眼在手外相机固定在支架或龙门架上位置不动俯视整个工作台。优点是视野固定、线缆稳定、能看到全局。缺点是视野内一旦遮挡就没辙而且标定精度对相机到工作台的距离敏感。两种构型对应的标定方程不一样。眼在手上是标准的 AXXB眼在手外是 AXZB 的形式。这篇先集中讲眼在手上它更通用也是后面实时视觉追踪的基础。坐标系约定这件事我要单独强调**在写下任何一行标定代码之前先把每个坐标系的名字、原点位置、三轴朝向写在纸上或注释里。**我见过太多标定失败的案例最后查出来是某个坐标系的 Y 轴和 Z 轴写反了。CoppeliaSim 默认是 Z 轴向上的右手系很多从 ROS 转过来的脚本默认 Z 轴向前两边一混标定结果能对上才有鬼。推荐一套命名坐标系记号含义世界系W仿真场景全局CoppeliaSim 原生基座系B机械臂基座法兰底面末端系T法兰中心通常取第六轴输出法兰面相机系C光心为原点Z 沿光轴向前标定板系G标定板左上角第一个角点或板中心有了这套记号眼在手上的所有变换关系就是一句话T_W_T 乘以 T_T_C 乘以 T_C_G 恒等于 T_W_G。其中 T_T_C 是你的待求量其他三个矩阵都能测到或读出来。3. 手眼标定到底在解什么方程3.1 从一次观测到 AXXB 的推导很多人背下了 AXXB 这个式子但说不清 A 和 B 到底是什么。我用人话推一遍。机械臂在两个不同位姿下观察同一个固定标定板。位姿 1 时末端系到世界系的变换记为 T1位姿 2 时记为 T2。相机装在末端上所以相机到末端的变换 X 是固定的、未知的。标定板相对于相机的位置在两次观测中是不同的分别记为 C1 和 C2。从世界系角度看标定板是静止的所以T1 · X · C1 T2 · X · C2把同侧项整理一下(T2⁻¹ · T1) · X X · (C2 · C1⁻¹)令 A T2⁻¹ · T1B C2 · C1⁻¹就得到 AX XB。A 完全由机械臂的正运动学给出是纯真值B 完全由相机观测给出带着图像处理的全部误差。整条链路里A 是干净的B 是脏的标定的质量上限基本由 B 决定。这也解释了一个常见困惑为什么换了好几组数据标定结果还是不收敛因为问题不在求解算法而在 B 的噪声太大。图像里角点检测偏一个像素在近距离下可能对应零点几毫米的位姿误差累积到十几组数据里算法就找不出一个自洽的 X 了。3.2 采样姿态怎么选才不会退化数据够不够不是看组数是看姿态分布的张角。假设你只让机械臂绕第六轴转末端位置基本不变只有姿态在变。这时候 A 的旋转轴始终几乎是同一根轴方程组在旋转部分会严重退化解出来的 X 对这根轴上的分量极不敏感表现为某几个方向上的标定误差特别大。这不是算法的问题是数据的问题神仙算法也救不回来。我用的采样策略是这样的让末端在相机视野内尽量覆盖更大的空间取 5 到 8 个不同的位置点均匀分布在相机视野和机械臂可达空间的交集里。在每个位置点上让末端姿态绕至少三根不同的轴各转 30 度以上。转太少意味着两次观测的旋转轴区分度不够。保持标定板始终完整出现在视野里、角点清晰。这一点在仿真是免费的在真机上要用光照和曝光去换。总组数落在 15 到 25 之间。低于 12 组基本不可信超过 25 组收益迅速递减还不如把时间花在提高单组数据质量上。一组好的数据长什么样任意两组的末端旋转角差大于 30 度旋转轴之间的夹角大于 30 度。你可以写个几十行的脚本对采集到的姿态集合算一下这个指标不达标就补采。这一步看起来麻烦但比标定完发现精度不够再回来重新采要省事得多。3.3 仿真里怎么拿到干净的标定数据在 CoppeliaSim 里采数据典型的流程是给一组关节角 - 关节运动到位 - 读末端位姿 - 读相机观测的标定板位姿 - 存成一对。前面说过有真值和渲染两条路这里展开讲真值路怎么走。标定板在场景树里是一个对象比如从模型库里拖进来的棋盘格模型。相机的世界位姿用sim.getObjectPose(camHandle, -1)拿到标定板的世界位姿用同样方式拿到。那么相机观测到标定板的位姿就是T_C_G inv(T_W_C) · T_W_G注意这里有个陷阱sim.getObjectPose返回的是对象参考系的位姿而标定板的板面未必和它的对象原点重合。如果标定板模型的原点在板中心那没问题如果原点在板的一角你需要在标定板系里再乘一个固定偏移才能得到棋盘格角点阵的位姿。这个偏移一旦搞错标定结果会有个恒定的平移偏差而且在所有姿态下表现一致很容易被误判成相机安装位置的偏差。import numpy as np def pose_to_T(pose): CoppeliaSim 的 pose 是 [x,y,z,qx,qy,qz,qw]转成 4x4 齐次矩阵 p np.array(pose[:3]) q np.array(pose[3:7]) x, y, z, w q R np.array([ [1-2*(y*yz*z), 2*(x*y-z*w), 2*(x*zy*w)], [2*(x*yz*w), 1-2*(x*xz*z), 2*(y*z-x*w)], [2*(x*z-y*w), 2*(y*zx*w), 1-2*(x*xy*y)], ]) T np.eye(4) T[:3, :3] R T[:3, 3] p return T3.4 手眼标定到底要哪些数据这是被问得最多的问题之一我直接列个清单。必须有的机械臂末端位姿序列每次观测时的 T_W_T、相机观测的标定板位姿序列T_C_G、相机内参矩阵和畸变系数、标定板的物理参数格点数、格边长。如果走渲染路径还需要原始图像或至少是检测出的角点像素坐标方便回溯排查。可选但很有用的每次观测的关节角用于检查正运动学是否一致、采集时的时间戳用于排查同步问题、渲染路径下的重投影误差用于剔除外点。有一类数据特别容易被漏掉标定板坐标系的定义方式。说白了就是你说的 T_C_G 里的 G 到底在哪一点、Z 轴朝哪边。不同库的约定不一样OpenCV 的solvePnP输出的板系通常把原点放在第一个角点、Z 轴垂直于板面向外但有些库把原点放在板中心。如果采数据用一个约定、求解用另一个约定最后得到的 X 里会混进去一个固定偏移看起来像是相机装歪了。实操心得我在每个项目的标定脚本开头都会写一段注释把五个坐标系的定义、原点位置、轴向、单位全部写清楚然后写两行自检代码——把已知量代回 T_W_T · X · T_C_G 看是否恒等于 T_W_G。自检不过就不往下走比标定完再回头查要省太多时间。4. 标定数据的采集、筛选与质量评估4.1 数据采集脚本的整体结构一套能用的采集脚本骨架大概是四段连接仿真、初始化、循环采集、落盘保存。连接部分用 ZMQ Remote API初始化部分把要用的对象句柄全取出来机械臂关节、末端、相机、标定板循环部分遍历预设的姿态列表落盘部分把每个姿态的末端位姿和相机观测存成结构化的文件。from coppeliasim_zmqremoteapi_client import RemoteAPIClient import numpy as np, json client RemoteAPIClient(localhost, 23000) client.setStepping(True) sim client.require(sim) joints [sim.getObject(f/UR5/joint{i}) for i in range(6)] tcp sim.getObject(/UR5/tip) cam sim.getObject(/UR5/tip/camera) board sim.getObject(/calibBoard) samples [] for q in preset_joint_configs: for i, h in enumerate(joints): sim.setJointPosition(h, q[i]) sim.step() # 推进一个仿真步让场景更新 client.step() # 等待服务端返回确保数据同步 T_W_T pose_to_T(sim.getObjectPose(tcp, -1)) T_W_C pose_to_T(sim.getObjectPose(cam, -1)) T_W_G pose_to_T(sim.getObjectPose(board, -1)) T_C_G np.linalg.inv(T_W_C) T_W_G samples.append({T_W_T: T_W_T.tolist(), T_C_G: T_C_G.tolist()}) with open(handeye_samples.json, w) as f: json.dump(samples, f, indent2)这里最关键的调用是sim.step()和client.step()的配合。前者推进仿真本身后者处理通信往返。只调其中一个会出现数据没更新或者脚本卡死的现象这是新手最常撞的墙。4.2 采样点的自动生成手写姿态列表既费时又难保证分布质量。我一般用自动生成第一步在相机视野和机械臂可达空间的交集里用拉丁超立方或简单的网格采样生成 6 到 8 个位置点。第二步在每个位置点上围绕三根近似正交的轴各生成若干旋转旋转角在 25 到 45 度之间随机取值。生成出来的候选姿态不能直接用因为可能出现机械臂自碰撞、末端碰到工作台、标定板跑出视野、关节超过限位。所以要加一层筛选——用逆运动学求解每个候选姿态的关节角解不出来或者解出来超限位就丢弃再把标定板的包围盒投影到相机像平面确认完整可见最后用碰撞检测排除自碰撞。这一步在仿真里是纯计算几百个候选姿态筛一遍也就几秒钟但能给后面的标定省下大量返工。真机上想做同样的筛选得实打实地动一遍机械臂成本天差地别。4.3 数据质量的三个量化指标采完数据不能直接就扔给求解器要先量化评估。**指标一旋转张角。**计算所有姿态对的相对旋转角取最小值。如果最小值小于 20 度说明存在过于接近的姿态对对求解贡献很小应该剔除。**指标二旋转轴分布。**把所有姿态对的相对旋转轴归一化后看它们的散布。理想情况下这些轴应该指向多个不同方向形成一个近似球面覆盖。如果它们挤在一小片区域说明采样集中在单一旋转模式上。**指标三标定板在视野中的位置分布。**如果所有观测里标定板都在图像正中央那标定结果在图像边缘区域的可靠性会打折扣。理想情况是板子在视野里有明显的位置变化覆盖中心、四角附近。这一点在仿真里可以精确控制在真机上主要靠机械臂位姿来间接实现。指标合格线不达标的表现最小相对旋转角≥ 20°存在近似重复姿态解不稳定旋转轴夹角最小值≥ 30°采样退化某方向误差大图像位置覆盖覆盖中心与四角边缘区域标定精度差有效姿态对数12 ~ 25太少欠定太多收益递减4.4 剔除外点的一个实用做法采集数据里总会有那么几组是异常的图像模糊、角点检测跳变、机械臂还没停稳就采了。这些外点对求解的影响远大于随机噪声必须剔掉。我用的做法是留一交叉验证先拿全部数据解一次 X然后把每组数据单独代回 T_W_T · X · T_C_G算出残差理论上应该恒等于 T_W_G所以残差就是偏差量。把所有残差排序剔除明显偏大的那几组再重新求解。重复一到两轮一般能把数据里的脏样本清理干净。残差的度量要旋转和平移分开看旋转残差取相对旋转矩阵对应的轴角平移残差取欧氏距离。因为这两者的量纲不同混在一起排序会掩盖旋转方向上的问题。仿真里如果用的是真值路径残差应该接近机器精度只要看到某个样本残差比其他大一个数量级基本可以确定这组数据有问题。5. ZMQ Remote API 实操把采集回路跑通5.1 环境准备与版本对齐CoppeliaSim 的 ZMQ Remote API 从 V4.3 开始提供客户端包是coppeliasim-zmqremoteapi-client。安装方式很简单但版本对齐这个坑必须先说。pip install coppeliasim-zmqremoteapi-client服务端默认监听 23000 端口这个可以在 CoppeliaSim 的远程 API 设置里改。客户端连接时如果只写主机名不写端口用的是默认值。多台仿真同时跑的时候端口会冲突一定要显式指定。还有一个特别隐蔽的问题**Python 的 numpy 版本和 CoppeliaSim 自带的 Python 版本不一致时位姿数据的精度会受影响。**从仿真端传过来的浮点数经过序列化再反序列化本身就有精度损失。如果拿这些数据做高精度标定建议把传输格式改成字符串再自己解析虽然麻烦但精度可控。注意老教程里的simxStart、simxGetObjectPosition那一套是 legacy remote API和 ZMQ Remote API 的写法完全不兼容。照着老教程抄代码会直接跑不起来报函数不存在的错。识别方法很简单函数名带simx前缀的是旧版用sim.getObjectPose这类的是新版。5.2 同步模式为什么必须开 Stepping这是采集脚本里最容易出错的地方我单独讲。CoppeliaSim 有两种运行模式连续运行和步进模式。连续模式下仿真自己按实时钟跑你的脚本发一条指令场景可能已经往前走了好几步你读到的位姿和你设的关节角对不上。步进模式下只有你调用一次sim.step()仿真才往前走一步什么时候读数据完全由你控制。采集标定数据必须用步进模式。client.setStepping(True) # 开启步进模式 for q in configs: for i, h in enumerate(joints): sim.setJointPosition(h, q[i]) sim.step() # 应用关节角更新场景 client.step() # 与服务端同步一次 # 此后读到的一切都是与当前关节角严格对应的调用顺序是先 sim.step 再 client.step反过来会读到上一帧的数据。这个坑我第一次写的时候踩了一整个下午症状是标定结果总有一个固定偏差像是相机被平移了一段距离。5.3 相机数据的读取与坐标系转换从视觉传感器取数据接口有两组图像数据用sim.getVisionSensorImg深度数据用sim.getVisionSensorDepth或直接取深度缓冲。图像数据返回的是字节串加分辨率需要按 BGR 顺序 reshape 成图像数组。深度缓冲返回的是浮点列表需要按前面讲的非线性公式换算成实际距离。img, res sim.getVisionSensorImg(cam_handle) img np.frombuffer(img, dtypenp.uint8).reshape(res[1], res[0], 3) img img[::-1] # CoppeliaSim 的图像是上下翻转的别忘了这行 depth sim.getVisionSensorDepth(cam_handle, 1, [0, 0], res) depth np.array(depth).reshape(res[1], res[0])上下翻转这行代码必须加。CoppeliaSim 输出的图像原点在左下角而 OpenCV 和绝大多数图像处理库的原点在左上角。忘了翻转的话检测出的角点会上下镜像PnP 出来的位姿在俯仰角上直接反号标定结果偏差巨大而且很难从数据上看出规律。深度图反投影成点云需要相机内参。仿真里没有内参矩阵这个直接的概念你得从视场角和分辨率推fov sim.getObjectFloatParam(cam_handle, sim.visionfloatparam_perspective_angle) fx res[0] / (2 * np.tan(fov / 2)) fy fx # 方形像素fx 和 fy 相同 cx, cy res[0] / 2, res[1] / 2注意这里推出来的 fx 和 fy 是理想值不含畸变。仿真里的虚拟相机默认是理想针孔模型所以在仿真中走渲染路径时用这组内参做 PnP 得到的位姿误差只来自角点检测不来自畸变。这也是前面说的仿真验证逻辑层的一个体现。5.4 落盘与复现数据采集完成后我把每一组样本存成一个独立的 JSON 文件加上一个全局的元信息文件记录采集时的配置、仿真版本、相机参数、标定板参数。这样做的好处是任何一次标定实验都可以完整复现包括当时用的什么参数。复现这件事在调试阶段价值极大。有一次我的标定结果怎么调都不对最后是靠回放三个月前的一份采集数据逐组对比残差才发现问题出在某一次改动中我不小心把标定板的尺寸参数从 30mm 改成了 25mm。如果没有完整的元信息记录这个 bug 可能要查上一周。文件结构建议这样组织calib_session_20240612/ ├── meta.json # 相机参数、标定板参数、仿真版本 ├── sample_000.json # 单组样本T_W_T / T_C_G / 关节角 ├── sample_001.json └── ...5.5 从采集到求解的衔接采集完的数据交给求解器之前还有一步预处理容易被忽略把求解器需要的矩阵格式对齐。不同的标定库要求的输入形式不一样。有的要求你传入相邻两次的 A 和 B也就是相对变换有的要求你传入全部绝对位姿由库内部自己算相对量。传错了不会报错但结果会明显不对。以常见的cv2.calibrateHandEye为例它要求传入的是所有姿态下的旋转矩阵和平移向量绝对量内部自己构造相对变换。而有些自己实现的 Tsai-Lenz 求解器要求你传入成对的相对变换。这两者的区别在接口文档里往往写得很简略只能靠维度对不对、结果合不合理来判断。我一般会在传参前打印一下 A 和 B 的维度以及它们的模长范围确认量级正常。如果 A 的平移部分全为零说明相对变换算错了比如把所有姿态都传成了同一个如果 B 的旋转部分全是单位矩阵说明标定板位置在多次观测中没变化数据本身有问题。这些小检查写起来几分钟能省掉大量猜谜时间。6. 常见问题与踩坑速查6.1 症状与成因对照表下面这张表是我这些年踩过的坑的浓缩版遇到问题可以直接对号入座。症状可能成因排查方法标定结果有一个固定平移偏差坐标系约定不一致或标定板原点定义不同打印各坐标系变换链验证 T_W_T·X·T_C_G 是否恒等结果总偏差一个角度相机 Z 轴约定不同朝前 vs 朝后检查相机系定义确认光轴方向求解不收敛采样姿态退化旋转轴集中算最小相对旋转角与轴夹角分布数据读出来全是上一帧的值同步顺序错了确保 sim.step 在 client.step 之前深度点云整体拉伸深度归一化公式用错线性 vs 非线性用已知尺寸物体验证点云尺度图像上下颠倒没做图像翻转加img[::-1]某几组样本残差异常大外点图像模糊或运动未停稳留一交叉验证剔除外点关节设了目标位置但不动关节是动力学模式或仿真未推进切运动学模式确认调用了 step6.2 几个只有实操过才知道的细节**关节运动到位不等于姿态稳定。**在动力学模式下关节到达目标位置后还会有残余振荡这时候读出来的末端位姿是抖的。解决办法是切运动学模式或者采数据前多推进几个仿真步让振荡衰减。我习惯在设置关节角之后连推三步再读数据。**标定板的物理尺寸必须和仿真里的模型尺寸完全一致。**仿真里拖进来的棋盘格模型它的格边长是模型作者随便定的。你得量一下模型里两个角点之间的实际距离把这个值作为标定板的物理参数传给求解器。用默认值或者目测值标定出的平移分量会整体缩放。**仿真速度和实时性无关。**步进模式下仿真会跑得飞快别拿仿真里的耗时去估算真机上的采集时间。真机上采集一组数据的时间通常是仿真的几十倍。**真值路径和渲染路径得到的 X 不该差太多。**如果差得多先查图像处理环节角点检测、PnP 参数、图像翻转而不是怀疑标定算法。标定算法在两组数据上都不收敛那才轮到怀疑算法或数据分布。实操心得我给自己定了一条规矩——每改一处坐标系的定义就在纸上画一遍变换链然后用两行代码验证一次。这个习惯看起来笨但它让我避开了至少三次调了三天发现是坐标系写反的惨剧。标定这件事80% 的时间花在坐标系的确认上都不算多。6.3 下一步的方向这篇把仿真环境的搭建、手眼方程的推导、数据采集与筛选这条链路走完了。接下来要面对的是求解Tsai-Lenz 的闭式解、Park-Martin 的改进、Daniilidis 的对偶四元数方法各自适合什么数据、怎么在 Python 里调 OpenCV 的calibrateHandEye、标定结果怎么验证、误差怎么分解到各个方向。再往后就是拿标定好的变换去做实时视觉追踪把相机看到的目标位置换算到机械臂坐标系里配合视觉伺服把末端送过去。我个人在仿真里做手眼标定最实在的收获不是标出了多准的矩阵而是终于能一眼看懂那些坐标系之间的变换关系。真机上的标定误差往往是物理装配和运动学误差的混合体查起来毫无头绪仿真里每一层误差都能被单独拎出来看调通之后再回真机你会发现很多曾经觉得玄学的问题其实早在坐标系定义那一步就埋下了伏笔。整套采集脚本我一般会留一版最小可运行版本不追求功能全只保证从连接仿真到落盘数据这条链路能跑通后面所有实验都从它上面长出来——这比一开始就写个功能齐全的大脚本要可靠得多。
返回列表