1. 写这篇文档的目的
很多人在刚接触 6D pose、BOP、LINEMOD、PVN3D 这类数据时,最容易混淆的点不是代码,而是下面这个问题:
一个
.ply模型文件里明明只有几何形状,看起来既没有相机,也没有图像,更没有"参考世界"。那它到底是怎么和
RGB图像、depth图像、mask分割图联系起来的?
如果这个问题没有彻底想清楚,后面会连续出现这些误区:
- 以为
.ply"没有坐标系" - 以为
cam_R_m2c/cam_t_m2c是"相机在世界里的位姿" - 以为分割图是直接从
.ply里长出来的 - 以为 RGB 对齐了就代表 pose 正确
- 以为深度图只是辅助可视化,不影响几何关系
- 以为模型单位、深度单位、平移单位可以随便混
本文专门把这条链路完整讲清楚。
2. 先给结论
.ply 文件虽然通常只保存物体形状,但它里面的每个顶点坐标其实天然就定义在一个"模型坐标系"里。
也就是说:
.ply不是"没有坐标系".ply是"没有世界坐标系和相机坐标系"- 但它一定有自己的局部坐标系,也就是
model frame / object frame
这个模型坐标系中的点,通过一个 model -> camera 的刚体变换:
text
X_cam = R * X_obj + t
就可以被放到当前帧相机坐标系中。
然后再通过相机内参 K 投影到图像平面:
text
[u, v, 1]^T ~ K * X_cam
一旦完成这两步:
.ply模型和当前帧相机建立了三维关系- 模型和 RGB 图建立了二维投影关系
- 模型和深度图建立了三维深度一致性关系
- 模型和分割图建立了像素占据区域关系
所以,真正把它们联系在一起的核心不是 .ply 本身,而是:
- 模型坐标系定义
- 相机内参
K cam_R_m2ccam_t_m2c- 深度尺度定义
3. 你必须先分清的 4 个东西
3.1 .ply 模型文件
.ply 在这里最重要的信息通常是:
- 顶点坐标
x, y, z - 可能有面片
face - 可能有法向、颜色等附加信息
但在 pose 问题里,最本质的是:
- 每个顶点坐标都是在模型自己的局部坐标系里定义的
例如模型中有一个点:
text
X_obj = [10, 20, 30]^T
这不是图像坐标,不是世界坐标,也不是相机坐标。
它表示:
- 这个点相对于模型坐标系原点,位于
(10, 20, 30)
3.2 RGB 图像
RGB 图像是相机拍到的二维颜色图。
它本身只告诉你:
- 每个像素位置
(u, v)有什么颜色
RGB 图像并不直接告诉你:
- 这个像素对应的三维点在哪
- 物体的三维姿态是什么
RGB 和模型发生联系,是因为模型投影到了 RGB 平面上。
3.3 深度图
深度图是每个像素对应的深度测量值。
它通常表示:
- 从相机出发,沿该像素视线方向,到物体表面的距离
但要注意:
- 有的深度图单位是毫米
- 有的是米
- 有的是原始整数值,需要再乘
depth_scale
深度图本身仍然在"图像像素索引"上组织,但它比 RGB 多了一层三维信息。
3.4 分割图 mask
分割图通常是一个二维图像,告诉你:
- 哪些像素属于某个物体
- 哪些像素不属于它
分割图的本质是"像素集合",不是三维几何本体。
它可以来自:
- 人工标注
- 自动分割模型
- 根据已知 pose 把
.ply渲染到图像上得到可见区域
所以分割图不是从 .ply 直接"天然附带"的,而是通过图像空间中的映射关系得到的。
4. .ply 为什么说"有坐标系"
很多人说"PLY 没有参考系",这句话一半对,一半不对。
4.1 对的部分
.ply 文件通常不会告诉你:
- 这个模型在世界里的位置
- 这个模型面对哪台相机
- 这个模型相对某一帧图像的姿态
也就是说,它通常不包含:
- 世界坐标系定义
- 当前帧相机位姿
- 当前帧
R, t
4.2 不对的部分
只要 .ply 里有顶点坐标,它就一定隐含一个坐标系。
因为顶点必须相对于某个原点和某组轴来表达。
比如:
text
vertex 1: (0, 0, 0)
vertex 2: (100, 0, 0)
vertex 3: (0, 50, 0)
这里已经隐含了:
- 原点在哪里
- x 轴正方向是哪边
- y 轴正方向是哪边
- z 轴正方向是哪边
- 长度单位是什么
所以更准确的话应该是:
.ply有模型局部坐标系,但通常没有和某一帧图像绑定的相机外参。`
5. 这条链路里真正存在的几个坐标系
5.1 模型坐标系 object/model frame
这是 .ply 顶点所在的坐标系。
特点:
- 原点由建模或重建流程定义
- 轴方向由建模软件或数据集约定定义
- 所有模型顶点都在这个坐标系里
- 关键点、中心点、包围盒也通常在这个坐标系中定义
记作:
text
X_obj
5.2 相机坐标系 camera frame
这是当前一帧 RGB-D 图像对应的相机局部坐标系。
在常见视觉约定中:
- 原点在相机光心
+Z指向镜头前方X, Y的方向由数据集约定决定
记作:
text
X_cam
5.3 图像像素坐标系 image plane
这是二维图像坐标系。
记作:
text
(u, v)
它和三维坐标系不同。
它只表示:
- 像素列号
- 像素行号
5.4 世界坐标系 world frame
有些采集系统里会有世界坐标系,比如:
- 转台坐标系
- 标定板坐标系
- 机械臂基坐标系
但在单帧 PVN3D / BOP / LINEMOD 的最终监督里,世界坐标系不是必须的。
最终真正需要落盘的是:
text
model -> camera
6. .ply、RGB、depth、mask 是如何被"绑"到一起的
真正的联系分三层。
6.1 第一层:模型到相机
给定:
- 模型点
X_obj - 旋转矩阵
R - 平移向量
t
就有:
text
X_cam = R * X_obj + t
在 BOP/LINEMOD 里:
R = cam_R_m2ct = cam_t_m2cm2c = model to camera
这一步的意思是:
- 把模型坐标系中的几何点,搬到当前相机坐标系下
这一步之后,模型已经和当前图像帧建立了三维关系。
6.2 第二层:相机到 RGB 图像
相机坐标中的点 X_cam = [x, y, z]^T 通过相机内参 K 投影到图像:
text
u = fx * x / z + cx
v = fy * y / z + cy
矩阵形式:
text
[u', v', w']^T = K * [x, y, z]^T
u = u' / w'
v = v' / w'
这样就能把模型上的每个三维点映射到 RGB 图上。
于是你可以:
- 画轮廓
- 画投影点
- 画网格边
- 画半透明实体叠加
这就是 pose_tuner 为什么能够把 .ply 叠加到 RGB 上。
6.3 第三层:相机到深度图
如果某个像素 (u, v) 上的深度图值是 d,那么结合相机内参可反投影为相机坐标系中的三维点:
text
x = (u - cx) * d / fx
y = (v - cy) * d / fy
z = d
于是:
- 深度图可以变成相机坐标系点云
- 渲染后的模型深度也可以变成一个"理论深度图"
然后比较:
- 实测深度
- 模型渲染深度
就能检查 tz 是否正确、表面是否贴合、遮挡是否合理。
6.4 第四层:模型到分割图
一旦模型已经通过 R, t, K 投影到图像上,你就能知道:
- 模型在图像里占据哪些像素
于是分割图就可以由这些像素生成。
也就是说,分割图来自"模型投影后的像素区域",而不是来自 .ply 自己。
7. 为什么说分割图和 pose 有强关系
分割图可以有两种来源。
7.1 手工或网络分割得到的 mask
这种情况下:
- mask 先存在
- pose 需要去解释这个 mask 对应的是哪个三维姿态
7.2 已知 pose 后由模型渲染得到的 mask
这种情况下:
- 先有
.ply + R + t + K - 再把模型投影/渲染到图像平面
- 得到 silhouette / visible mask
所以在标准数据集里,mask 常常不是独立真理,而是和 pose 紧密耦合的产物。
换句话说:
.ply决定物体长什么样R, t决定它在相机前怎么摆K决定它落到图像的哪里- mask 是这个结果在二维像素上的占据集合
8. 一张图像帧里,这四类数据分别扮演什么角色
设当前帧有如下数据:
model.plyrgb/000123.pngdepth/000123.pngmask/000123_000000.pngscene_camera.jsonscene_gt.json
那么它们的角色是:
8.1 model.ply
定义:
- 物体在模型坐标系中的几何形状
8.2 scene_gt.json
定义:
- 当前帧这个物体实例从
model -> camera的外参 - 即
cam_R_m2c、cam_t_m2c
8.3 scene_camera.json
定义:
- 当前相机内参
cam_K - 可能还包含深度尺度信息
8.4 rgb
提供:
- 二维颜色观测
8.5 depth
提供:
- 每个像素的几何距离观测
8.6 mask
提供:
- 哪些像素属于该物体
它们一起构成一条完整链路:
text
PLY 顶点 --(R,t)--> 相机坐标点 --(K)--> RGB 像素
\
\--> 渲染深度 / 占据区域 / mask
9. 一个完整的数值例子
假设 .ply 中某个点为:
text
X_obj = [10, 0, 0]^T (单位: mm)
当前帧标注为:
text
R = I
t = [0, 0, 700]^T (单位: mm)
则该点在相机坐标系中:
text
X_cam = [10, 0, 700]^T
如果相机内参为:
text
fx = 572.4
fy = 573.6
cx = 325.3
cy = 242.0
则投影像素约为:
text
u = 572.4 * 10 / 700 + 325.3 ≈ 333.48
v = 573.6 * 0 / 700 + 242.0 = 242.0
这说明:
- 模型上的这个点会落到 RGB 图上的
(333.48, 242.0)附近
如果该位置的深度图读数也是接近 700 mm,说明:
- 几何关系是自洽的
如果这个点落在 mask 内部,也说明:
- 该点投影确实处于物体像素区域
10. 为什么 .ply 只有形状也足够做 6D pose
因为 6D pose 问题的本质不是"模型自己会不会说话",而是:
已知一个物体的三维几何形状,求它在当前相机坐标系中的刚体变换。
.ply 提供了三维形状后,剩下的全部联系都来自:
- 相机成像模型
- 位姿标注或估计
- 深度或颜色观测
所以在很多 pose 数据集里,.ply 只需要提供:
- 顶点
- 面片
- 单位
- 模型坐标系定义
就足够了。
11. "分割模型文件"和 .ply 的关系
你提到"分割模型文件",这个词在实际工程里可能有两种意思。
11.1 指每个物体一个独立的 .ply
例如:
obj_000001.plyobj_000002.ply
这种情况下,"分割"只是指:
- 每个对象的几何模型单独存储
它和图像分割不是一回事。
11.2 指分割结果文件 mask
例如:
mask_visib/000000_000000.pngmask/000000_000000.png
这种情况下,它是图像空间中的物体像素区域,不是 3D 模型本体。
所以一定要把这两类"分割"分开:
- 3D 模型文件分对象存放
- 2D 图像分割 mask 表示像素归属
12. 为什么 RGB 对了,pose 仍然可能不对
这是非常常见的坑。
12.1 只看轮廓,忽略深度
如果只看 RGB 叠加,很多时候:
tx, ty看起来差不多tz实际偏了很多
因为:
- 小幅度前后移动,二维轮廓变化可能不大
- 但深度图和三维位置会明显不一致
12.2 对称物体
对于对称物体:
- 不同旋转可能在 RGB 上看起来很像
- 但在坐标系定义上是不同 pose
12.3 单位不一致
如果:
.ply是毫米cam_t_m2c你当成米
那模型会整体缩放错 1000 倍,投影和深度都会出问题。
13. 为什么深度图特别重要
深度图是把二维图像和三维几何强行绑在一起的关键中介。
RGB 只能给你:
- 外观
- 纹理
- 轮廓
深度还能给你:
- 点到相机的距离
- 表面是否贴合
- 遮挡关系是否合理
tz是否错误
所以在 pose_tuner 这种工具里,深度窗口的意义不是"好看",而是:
- 辅助验证 pose 与几何是否真实一致
14. BOP / LINEMOD / PVN3D 里常见的真实关系
在这类项目里,通常会看到:
14.1 模型
text
lm_models/models/obj_000001.ply
表示:
- 物体 1 的三维模型
14.2 当前帧相机参数
json
{
"cam_K": [fx, 0, cx, 0, fy, cy, 0, 0, 1]
}
表示:
- 当前帧的内参
14.3 当前帧 pose
json
{
"cam_R_m2c": [...],
"cam_t_m2c": [...]
}
表示:
- 模型如何放到当前相机坐标系
14.4 当前帧 RGB / depth / mask
表示:
- 当前这帧真实观测到的像素、距离、分割区域
它们不是彼此孤立的文件,而是一组共同描述"同一时刻同一物体实例"的观测和几何关系。
15. 常见错误理解
15.1 错误一:PLY 没坐标系
正确说法:
- PLY 有模型局部坐标系
- 只是通常没有写明和当前相机的外参关系
15.2 错误二:cam_R_m2c 是相机姿态
正确说法:
- 它是模型到相机的变换
- 不是"相机在世界里怎么放"
15.3 错误三:mask 和 pose 无关
正确说法:
- 很多 mask 恰恰是由 pose 渲染或约束得到的
15.4 错误四:RGB 对齐就够了
正确说法:
- 还要检查深度一致性和单位一致性
15.5 错误五:深度图和 cam_t_m2c 单位可以不一致
正确说法:
- 只要单位不一致,反投影和渲染深度就会系统性错位
16. 在工程里应当如何自检
拿到一套新数据时,建议按下面顺序检查。
16.1 检查模型坐标系
确认:
- 原点定义
- 轴方向
- 长度单位
16.2 检查 pose 定义
确认:
- 是
model -> camera - 不是
camera -> model
16.3 检查内参
确认:
fx, fy, cx, cy对应当前分辨率
16.4 检查深度尺度
确认:
- 深度图原始值单位
- 是否需要乘
depth_scale
16.5 检查投影效果
确认:
- 模型轮廓是否落在物体上
- 深度是否与真实表面贴合
- mask 是否覆盖合理
17. 用一句最简洁的话重新概括
如果只记一句话,就记这个:
.ply提供的是"物体在模型坐标系中的三维形状";
cam_R_m2c和cam_t_m2c把这个形状放到当前相机坐标系;
K把它投影到 RGB 图像;深度图验证它在三维上的距离是否正确;
分割图表示它在二维像素上占据哪些区域。`
18. 最后给一个统一公式链
设模型点为:
text
X_obj
先变换到相机坐标系:
text
X_cam = R * X_obj + t
再投影到图像平面:
text
[u', v', w']^T = K * X_cam
u = u' / w'
v = v' / w'
如果同时有深度值 d(u, v),可以反投影:
text
x = (u - cx) * d / fx
y = (v - cy) * d / fy
z = d
因此这四类数据的关系是:
text
PLY(模型形状)
-> 通过 R,t 进入相机坐标系
-> 通过 K 落到 RGB 像素
-> 通过深度比较验证三维距离
-> 通过渲染占据区域得到 mask / segmentation
19. 结合当前仓库应如何理解
对当前仓库来说,最重要的工程约定是:
.ply提供模型几何cam_R_m2c/cam_t_m2c表示model -> camera- RGB 用来看投影对齐
- 深度用来看几何贴合
- mask 用来限定物体像素区域
因此后续无论你在做:
pose_tuner- BOP 数据转换
- PVN3D 训练数据整理
- 真实采集数据标注
都必须守住同一个原则:
不要把"模型本身的局部坐标系"与"当前帧相机下的姿态"混为一谈。
这两者一旦混了,后续所有投影、深度、mask、训练监督都会一起错。