拿到 LingBot-Depth 2.0 的深度图,并不等于机械臂能直接抓。深度图是像素级的中间结果,后面至少还缺三段自建工作:把深度反投影成有序点云、把点云从相机坐标系搬到机械臂能理解的工作坐标系、再从点云里筛出真正可抓取的候选面。这三段都依赖你自己的内参、手眼标定结果和抓取策略,厂商一般不替你做,也不该由深度模型负责。下面是可逐层落地的顺序和检查点,每一步都能用日志或叠加图验证,验证不过就不要进入下一步。
LingBot-Depth 2.0 输出的深度图只是中间结果,点云生成、坐标系对齐与抓取判定都要自己接。建议固定顺序:先按内参反投影出有序点云并打印坐标范围,再核对单位与轴向,然后用外参把点云搬到工作坐标系,最后按深度连续性筛候选面并用叠加图人工复核。每层都要有可验证检查点,标定或单位出错时不要往下游调参。
从深度图生成有序点云
先把二维深度变成可操作的三维点。这里的关键是像素坐标、深度值、单位三者一一对应:像素 (u, v) 是整数索引,深度值是该像素沿光轴的 Z 距离,内参 fx、fy、cx、cy 把像素偏移换算成物理长度。反投影时 x、y 的单位由深度单位决定——深度是米,点云就是米;深度是毫米,点云也是毫米。内参必须与深度图同分辨率,若深度图被缩放或裁剪过,内参要同步换算,否则点云会整体偏移。
import numpy as np
# depth: HxW float32,先按采集配置确认单位
# K: 3x3 内参,fx, fy, cx, cy
fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2]
H, W = depth.shape
u, v = np.meshgrid(np.arange(W), np.arange(H))
z = depth.astype(np.float32)
valid = z > 0
x = (u - cx) * z / fx
y = (v - cy) * z / fy
pts = np.stack([x, y, z], axis=-1) # HxWx3,有序列保持像素对应关系
print("x range", pts[valid, 0].min(), pts[valid, 0].max())
print("y range", pts[valid, 1].min(), pts[valid, 1].max())
print("z range", pts[valid, 2].min(), pts[valid, 2].max())
print("valid ratio", valid.mean())
打印坐标范围做初检:把相机正对桌面,z 范围应大致等于相机到桌面与到背景的距离;x、y 范围应与视场角和工作距离推算的数量级一致。有效点比例明显偏低,通常是深度值全零或单位被误当成米/毫米,先解决再往下走。
核对坐标轴方向与单位
点云能算出来不代表方向对。多数低级错位来自整体旋转或千倍缩放,而不是模型精度问题。需要确认三件事:一是深度单位是米还是毫米,两者差一千倍;二是相机光轴朝哪个方向,即 Z 是朝向物体还是背向物体;三是图像上下是否翻转,即 V 轴是否与物理 Y 轴同向。这三点在 OpenCV 约定、ROS 约定和部分深度 SDK 之间并不一致,建议以实际采集配置为准,不要凭经验假设。
验证步骤:在视场中放一个已知高度的物体(例如量过的方块),用点云量一下它上表面与桌面的 Z 差值。若相差约一千倍,就是单位问题;若出现负值且绝对值接近高度,是光轴方向反了;若物体在图像里偏上、点云却落在 Y 负方向,说明上下翻转。记录下这三项的结论并写进配置注释,避免每次重接时重新猜。
把相机坐标系变换到工作坐标系
抓取点必须在机械臂能理解的坐标系里表达。做法是把点云左乘一个 4x4 外参矩阵,把相机坐标系变换到工作坐标系或末端坐标系。这个矩阵来自手眼标定结果:眼在手上的方案通常包含末端到相机的变换,眼在外的方案是相机到基座的变换。接入位置一般放在标定流程的输出文件里,读取后与点云拼接,而不是在抓取代码里硬编码数值。
# T_cam_to_work: 4x4,相机坐标系 -> 工作坐标系
pts_h = np.concatenate([pts.reshape(-1, 3), np.ones((pts.shape[0]*pts.shape[1], 1))], axis=1)
pts_work = (T_cam_to_work @ pts_h.T).T[:, :3]
pts_work = pts_work.reshape(H, W, 3)
检查矩阵是否写反:选三个已知点,例如标定板角点或工装上量好的位置,分别用深度图和手工测量得到它们在相机坐标系与工作坐标系下的坐标,代进矩阵看是否吻合。若所有点都落在一个镜像位置,多半是外参与逆矩阵用反了;若只有旋转方向不对,检查旋转分量的欧拉角顺序。矩阵写反时下游抓取偏移会随位置放大,越远离标定原点越明显,建议在标定覆盖范围边缘也做一次核对。
按深度连续性筛掉不可靠区域
边缘和空洞常被误当成可抓取面,需要用深度连续性把它们排掉。基本思路是:先用有效值掩码去掉零值和超出工作距离的点,再比较每个像素与邻域的深度差,差值超过阈值说明该处是边缘或深度断裂,标记为不可靠。阈值需要结合物体厚度和相机噪声确认,通常从几毫米到一两厘米之间试。
d = depth.astype(np.float32)
valid = (d > z_min) & (d < z_max) # 依据工作距离设定
dx = np.abs(np.diff(d, axis=1, prepend=d[:, :1]))
dy = np.abs(np.diff(d, axis=0, prepend=d[:1, :]))
smooth = valid & (dx < edge_th) & (dy < edge_th)
from scipy import ndimage
labels, n = ndimage.label(smooth)
for i in range(1, n + 1):
m = labels == i
if m.sum() < min_area:
continue
print(i, "area", m.sum(), "mean_z", d[m].mean())
连通块统计给出的面积与平均深度是筛选依据:面积过小的块可能是噪声,平均深度异常的块可能与背景混在一起。建议把每个候选块的中心与法向也记下来,后面判定抓取姿态时会用到。这一步的阈值先按能稳定复现目标物体的标准设,宁可少给候选,也不要放进明显不可靠的面。
用叠加图人工复核候选区域
真机动作前先确认候选面是否落在目标物体上。做法是把候选区域的掩码投影回原图,用半透明色叠加,然后保存成图片或推到调试页面看。投影只需要按像素索引对应即可,因为有序点云与原图分辨率一致,不需要重投影。
overlay = color_img.copy()
overlay[smooth] = (0.5 * overlay[smooth] + 0.5 * np.array([0, 255, 0])).astype(np.uint8)
cv2.imwrite("candidates.png", overlay)
复核时要留意几类失败样例并单独归类记录:反光表面会给出跳变或缺失的深度;遮挡会让同一物体被切断成多个块;透明物体常常直接没有有效深度值。把这些样例按类型存成小图集,配上当时的深度图和参数,后续调阈值或换工位时可以直接对照,而不是每次从零复现。人工复核通过的候选区域,才交给抓取姿态计算和真机试抓。