ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

BlenderProc渲染6D位姿数据集:坐标转换、域随机化与标注自检

BlenderProc渲染6D位姿数据集:坐标转换、域随机化与标注自检

做 6D 位姿估计的同学大多经历过这个阶段:公开能下到的位姿数据集翻来覆去就那么几个,背景干净、物体摆得规规矩矩,模型在这种数据上能刷到九十多,一换到真实产线采的图就掉到四十出头。第一反应通常是“网络不够强”,于是回头改 backbone、加注意力、上对比学习,折腾两个月发现数字还是那个数。真正的瓶颈在数据侧——不是数量不够,而是姿态分布、遮挡关系、光照变化跟实际场景差得太远。用 BlenderProc 渲染 6D 位姿估计数据集,就是解决这件事最常规也最划算的路径:拿 CAD 模型,用一段 Python 脚本在 Blender 里搭场景、撒物体、采相机,一次渲染同时吐出 RGB、深度、实例分割掩码、相机内参和每个物体的 model-to-camera 位姿矩阵。这些标注是算出来的,不是标出来的,精度天然在浮点误差量级,不存在人工标注那种“对不对全看标的人心情”的问题。

但这套流程的坑,远比“装个库跑个 demo”多。它难的地方不在渲染本身,而在你能不能把坐标系的转换关系算对,这是一个需要仔细推导的问题;在于渲染出来的场景是否真的贴近真实相机。写错一个符号,损失函数收敛到一个恒等变换上,训练曲线好看,测试一塌糊涂,还很难查到原因。下面把这些东西按实际动手顺序摊开讲。

1. 靠加数据量解决不了位姿数据集的根本问题

1.1 人工标注位姿在工程上是不成立的

先说清楚为什么必须转向渲染。6D 位姿的标注不是画框,是给每个物体一个 3×3 的旋转矩阵加一个 3×1 的平移向量,一共 6 个自由度。人工要标出这 6 个数,常见做法是对着一张图目测物体的朝向,再估一个距离,误差动辄十几度、几厘米。这种精度喂给网络,网络学到的就是噪声。用辅助设备采集(比如带跟踪的机械臂、标记点阵列)精度能上来,但成本和场景受限立刻爆炸——你不能为了标注一个料箱里的零件,先给它贴一圈标记点。

另一个现实问题是不均衡。真实采集中,绝大多数样本里物体都是正对着相机、摆在桌面中间、光照均匀,而恰恰是那些侧视、被遮挡、逆光的样本才是模型真正需要学的。人工采集时这些样本既难采又难标,最后数据集里它们的占比低得可怜。渲染侧完全没这个约束:你可以指定相机只在某个仰角区间采样,可以强制 30% 的帧带有部分遮挡,可以让物体以任意姿态出现。这种分布可控性,是渲染数据相对真实数据最大的、也经常被低估的价值。

1.2 BlenderProc 替你做的其实是四件事

很多人把它理解成一个“渲染器封装”,其实它的核心价值在流程编排。第一件是场景生命周期管理:bproc.init()之后,场景里的物体、材质、灯光、相机都可以通过 Python 对象操作,不需要去点 Blender 界面。第二件是采样原语:物体怎么摆在平面上、相机怎么绕着物体转、物体之间怎么避免穿模,这些都有现成函数,省掉大量重复代码。第三件是标注的自动收集:渲染时开启 segmentation 输出,它会按你给每个物体设置的category_id生成掩码图,同时把每个物体的世界位姿、相机位姿、内参一起打包。第四件是格式导出:COCO、HDF5、BOP 风格这几种常见格式都有对应 writer,直接对接训练管线。

这四件事串起来,意味着你改一行采样范围就能出一版新数据,而不用重新组织整个工程。我见过有团队用纯 Blender Python API 手搓这套东西,光是“渲染时同步记录位姿”这一块就写了八百行,还得自己处理多帧、多相机的对应关系。没必要重复造这个轮子。

1.3 什么时候不该用它

必须说清楚边界。渲染数据补的是“姿态分布”和“标注精度”,补不了“真实传感器特性”。如果你的真实场景有强烈的运动模糊、卷帘快门畸变、镜头脏污、特殊波段成像,渲染出来的干净图是学不到这些的,这时候该做的是真实数据采集加半自动标注,或者渲染图上叠一层针对性的退化合成。

还有一个更隐蔽的边界:材质。工业件里的金属反光、透明件、织物绒毛,在渲染里要还原得非常接近真实,代价极高——需要准确的 BRDF 参数、环境光贴图、甚至次表面散射。如果你的任务强依赖这些外观线索(比如区分两个几何完全相同、只有表面处理不同的零件),纯渲染数据的域差距会很大。这种情况我的经验是渲染数据打底、真实数据微调,比例大概 8:2 到 9:1,具体看外观差异有多关键。

2. CAD 模型进场景之前必须处理的几何问题

2.1 单位、原点、朝向:STL 给你的是毫米和一个随手放的原点

这是新手踩得最多、也最浪费时间的一个问题。工程上拿到的 CAD 导出件,绝大多数 STL 单位是毫米,而 Blender 的世界单位默认是米。一个 80mm 的零件直接加载进来,会变成一个 80 米大的巨物,相机放在“合理”位置时画面里只有一片白。解决办法是在加载后立刻缩放:

import blenderproc as bproc import numpy as np bproc.init() objs = bproc.loader.load_obj("part.stl") obj = objs[0] # CAD 单位为 mm,Blender 世界单位为 m obj.set_scale([0.001, 0.001, 0.001]) bproc.object.snap_to_surface # 注意:缩放后需要更新矩阵 obj.set_location([0, 0, 0])

比单位更麻烦的是原点和朝向。CAD 里的模型原点可能在装配体的某个基准上,导出单个零件后这个原点可能飘在模型外几米远的地方。位姿标注的平移分量是相对于物体原点定义的,原点飘了,标注出来的 t 就没法用。稳妥的做法是加载后统一把原点归到几何中心(或者你训练时会用到的那个规范点,比如抓取点),并且把主轴朝向对齐到约定方向。这一步不做,后面所有位姿数据都是错的,而且错得很隐蔽——因为渲染图看起来完全正常。

朝向对齐的约定要提前定死。我的习惯是让物体的“正面”朝 +Z、“上方”朝 +Y,这样和多数位姿定义里的 model 坐标系一致。整个数据集统一,训练侧就不用每个模型单独记一套旋转补偿。

2.2 网格清理不是洁癖,是稳定性的前提

CAD 导出的网格经常带非流形边、重复顶点、内部翻面。渲染时这些问题可能表现为:某个面莫名其妙变黑(法线反了)、深度图在某些像素上出现尖刺(自相交)、碰撞检测失效(物体互相穿模但检测说没碰)。前两个影响图像质量,第三个直接影响采样成功率。

清理的优先级:先合并重复顶点(Blender 的 remove doubles,容差 1e-4 量级),再重算法线,再检查非流形边。面数也要控制——一个五十万面的零件,单帧渲染时间可能是五千面版本的十倍,而视觉上几乎没有区别。位姿估计任务里网络看到的是整体轮廓和纹理分布,不是微观倒角,减面到保留外形特征就够了。我一般控制在单物体两万面以内。

减面之后要重新检查一遍法线,减面算法有时会把原本正确的法线搞乱。这一步花不了多少时间,但能省掉后面“为什么某些角度渲染出来一片黑”的排查。

2.3 没有纹理的模型,网络学不到任何视觉线索

纯色、无纹理的 CAD 模型渲染出来,如果再用均匀光照,得到的图像几乎是不可区分的——物体转 90 度和转 180 度,画面里都是一块一样的灰。网络在这种情况下只能靠轮廓学,泛化能力极差。

三个层次的补救。最低成本是给一个带噪声的程序化材质:基础色加一点非均匀的粗糙度和颜色扰动,让表面有细微的视觉变化。往上一档是贴一张程序化生成的无缝纹理(噪声、条纹、斑块都可以),配合正确的 UV 展开。再往上是用真实的材质扫描贴图,但要考虑贴图和模型 UV 的匹配问题。

mat = bproc.material.create("part_mat") mat.set_principled_shader_value("Base Color", np.random.uniform(0.2, 0.85, 3).tolist() + [1.0]) mat.set_principled_shader_value("Roughness", np.random.uniform(0.2, 0.9)) mat.set_principled_shader_value("Metallic", np.random.choice([0.0, 0.0, 1.0])) obj.replace_materials(mat)

金属度这一项要谨慎。设成 1.0 会得到强反射,在环境光贴图不够真实的情况下反而制造出奇怪的亮斑。我一般 70% 的样本设为 0(绝缘体),30% 设为 1,给网络一点材质多样性但不极端。

3. 位姿标注的坐标转换:从 Blender 世界系到 OpenCV 相机系

3.1 推导那一次变换

这是整条链路里唯一必须靠精确推导的地方,也是最容易出错的地方。位姿标注的标准定义是:给一个模型坐标系下的点 $X_m$,经过旋转 $R$ 和平移 $t$,得到相机坐标系下的坐标 $X_c = R X_m + t$。注意方向是 model → camera,不是反过来。很多人写成 world → camera 就直接用了,结果标注的旋转是“物体在世界里的朝向”,而不是“物体相对于相机的朝向”,网络学出来自然是错的。

正确的计算链路是:从 Blender 拿到物体到世界的变换 $T_{m \to w}$ 和相机到世界的变换 $T_{c \to w}$,那么物体到相机的变换是

$$T_{m \to c} = T_{c \to w}^{-1} \cdot T_{m \to w}$$

对应的代码:

# 物体到世界的 4x4 obj2world = obj.get_local2world_mat() # 相机到世界的 4x4 cam2world = bproc.camera.get_camera_pose() world2cam = np.linalg.inv(cam2world) obj2cam = world2cam @ obj2world R_m2c = obj2cam[:3, :3] t_m2c = obj2cam[:3, 3]

这段本身没难度,难的是下一步。

3.2 Z 轴朝上和 Y 轴朝下的根本冲突

Blender 的世界坐标系是 Z-up,相机局部坐标系是 X 向右、Y 向上、Z 轴指向相机背后(也就是相机看向 -Z 方向)。而 OpenCV / 多数深度学习框架用的相机坐标系是 X 向右、Y 向下、Z 轴指向前方。两者差了一个绕 X 轴 180 度的翻转:

$$R_{cv} = R_{blender} \cdot \text{diag}(1, -1, -1)$$

$$t_{cv} = \text{diag}(1, -1, -1) \cdot t_{blender}$$

代码上就是:

flip = np.diag([1.0, -1.0, -1.0]) R_cv = R_m2c @ flip t_cv = flip @ t_m2c

如果你的流程用的是 BlenderProc 自带的write_bop之类的 writer,它内部已经处理了这一步,你不需要手动翻。但只要你打算自己写导出、自己拼标注文件、或者做任何自定义的验证,这个翻转必须自己做,而且做完一定要验。我见过不止一次,因为漏了这一步,所有旋转矩阵的行列式符号正确、正交性也正确,唯独 Y 和 Z 反了,训出来的模型在上下颠倒的物体上表现崩坏,查了一个多星期。

3.3 用反向投影检验自己的矩阵是否正确

写一个最小验证脚本,不要等到跑了几万张才发现错。

import numpy as np, cv2 def project(K, R, t, pts3d): """pts3d: (N,3) 模型系坐标 -> 像素坐标""" pts_cam = (R @ pts3d.T + t.reshape(3, 1)).T uv = (K @ pts_cam.T).T return uv[:, :2] / uv[:, 2:3], pts_cam[:, 2] # 取模型的 8 个包围盒角点,投影到图像上 uv, depth = project(K, R_cv, t_cv, bbox_corners)

检查三件事:投影出来的 8 个点是否落在物体的实例掩码区域内(可以算凸包和掩码的 IoU,正常应该在 0.9 以上);所有点的相机系深度是否为正;掩码的重心像素与投影点重心的偏差是否在几个像素内。这三项都过,坐标系基本就是对的。

一个额外的交叉验证思路:跑一遍cv2.solvePnP,用掩码轮廓上提取的点和模型的 3D 轮廓点求解,比较解出来的 R、t 和标注值。两者应该在 1 度、几毫米以内吻合。如果差得很远,说明轮廓提取或者点对应有问题,但至少能定位问题在标注侧还是提取侧。

4. 位姿分布决定了模型的实际上限

4.1 相机绕物体旋转球面分布

相机采样的核心问题是要不要覆盖物体的所有视角。如果训练时物体只被从上方 30 度到 60 度俯视观察过,推理时出现一个从底部看的视角,模型基本废掉。所以采样要覆盖尽量完整的球面,同时模拟真实相机的可达区域。

具体做法是球坐标采样。给定物体的世界坐标中心 $c$、采样半径 $r$、方位角 $\phi$ 和仰角 $\theta$ 的范围:

def sample_cam(obj_center, radius, theta_range, phi_range): theta = np.random.uniform(*theta_range) phi = np.random.uniform(*phi_range) loc = np.array([ radius * np.cos(theta) * np.cos(phi), radius * np.cos(theta) * np.sin(phi), radius * np.sin(theta), ]) + obj_center forward = obj_center - loc rot = bproc.camera.rotation_from_forward_vec(forward, up_axis='Y') return bproc.math.build_transformation_mat(loc, rot)

需要注意的是up_axis='Y'——这里的 Y 指相机局部坐标的 Y 轴向上,不是世界坐标的 Y。这个参数写错会导致所有图像绕视线轴随机滚转,看起来像相机歪了,其实是位姿计算没问题、只是 up 定义搞混了。

仰角范围的选择要看场景。桌面场景里相机不可能从桌面下方拍,theta就应该限制在 5 度到 85 度之间(0 度是水平,90 度是正上方)。如果是手持扫描或者机械臂抓取,范围可以更大。范围设对了,数据分布的合理性立刻上一个台阶。

4.2 物体的摆法要符合物理常识

物体随机飘在空中是最省事的做法,但这样渲染出来的图里没有接触阴影、没有互相遮挡,网络学不到“物体和支撑面的关系”这个极强的先验。正确的做法是让物体落在平面上,允许一定的堆叠和邻近。

BlenderProc 提供了在表面上采样的接口:

plane = bproc.object.create_primitive("PLANE") plane.set_scale([2, 2, 1]) plane.hide_render = True # 不参与成像,但仍然作为碰撞/射线检测对象 bproc.object.sample_poses_on_surface( objects_to_sample=[obj], surface=plane, sample_pose_func=sample_pose_on_plane, min_distance=0.002, max_distance=0.05, max_tries=300, )

min_distance是物体相对表面的最小抬升,给 0 会导致部分顶点嵌进平面里,接触区域出现难看的穿插。给个 2mm 左右的余量比较自然。max_distance控制“悬浮”的容忍度,太大就会出现物体明显悬空的诡异画面。

多物体场景还要配bproc.object.make_collision_objects让它们互斥。碰撞检测的开销和物体面数正相关,减面在这里也会回报你——同样数量的采样尝试,减面后的成功率能高一截,整体耗时反而下降。

4.3 对称物体的位姿歧义必须提前处理

一个圆柱形零件,绕自身对称轴旋转任意角度,外观完全一样。渲染出来的数据里,同一个外观对应无数个“正确”的位姿标签,网络学的时候会被这些矛盾的监督信号拉扯。这不是渲染的错,是任务定义的问题。

处理办法有两种。第一种是在标注里给出对称性信息:告诉训练侧这个物体绕某个轴是 N 重对称的,或者完全连续对称,损失函数里用对称感知的位姿距离。第二种更粗暴但有效:在采样时把对称轴方向的旋转约束住,只采一个代表性角度。第二种做法省事,但会让网络在这个自由度上没被训练到,推理时输出不稳定。

我的建议是两步都做:采样时仍然放开对称轴旋转(保证外观多样性),同时在标注文件里带一个symmetry字段,训练侧据此选用对应的损失。很多开源位姿框架对这件事支持得不错,只要标注格式给对了就能用上。

5. 渲染参数里影响标注质量的关键设置

5.1 采样数和降噪的权衡

Cycles 是路径追踪渲染器,采样数(samples per pixel)直接决定噪声水平。64 采样时暗部还有明显噪点,128 采样基本干净,256 以上耗时翻倍但肉眼几乎看不出差别。这里的关键认知是:位姿估计任务对图像噪声的容忍度其实不低,网络看的是轮廓和局部纹理,轻微的渲染噪声甚至能起到一点数据增强的作用。所以不必追求无噪,64 到 128 之间是性价比最好的区间。

自适应采样更省时间:设一个噪声阈值,渲染器在噪点达标的像素上提前停止。对于大面积纯色背景的场景,这个优化能省掉 30% 到 50% 的时间。

bproc.renderer.set_max_amount_of_samples(128) bproc.renderer.set_noise_threshold(0.02) bproc.renderer.set_denoiser("INTEL")

降噪器要谨慎开。某些降噪算法会在物体边缘做平滑,轻微糊掉轮廓,同时——更麻烦的——降噪后的图像和 segmentation 掩码可能在边缘处不再严格对齐。如果你的训练用到边缘敏感的损失(比如轮廓一致性),这个错位会有影响。我的做法是:RGB 开降噪,但在导出前用掩码把物体边缘膨胀一两个像素,容许这一点误差。

5.2 深度图和分割掩码与抗锯齿的矛盾

深度输出和分割输出都有抗锯齿选项,默认是开的。开了之后,物体边缘的像素会得到混合值:深度图上边缘像素是前后景深度的加权平均(一个物理上没意义的中间值),分割图上边缘像素被归到某一边但边界不确定。对位姿估计来说,边缘像素的混合深度是很脏的监督信号。

bproc.renderer.enable_depth_output(activate_antialiasing=False) bproc.renderer.enable_segmentation_output( map_by=["category_id"], default_values={"category_id": 0}, )

关掉抗锯齿还有个副作用——图像边缘会显得有锯齿。但对训练影响不大,反而让掩码边界和深度边界严格一致,后面做点云重建或者 2D-3D 对应时省心得多。

分割图一定要显式设置default_values,否则背景区域的category_id可能是未定义值,导出后训练侧读到一堆 NaN 或者异常大的 ID,损失爆炸。这个问题排查起来极其痛苦,因为图像看起来完全正常。

5.3 内参和分辨率之间的绑定关系

相机内参必须和实际渲染分辨率匹配,这是死规矩。常见错误是先算了一组 K(比如按 640×480 算的),后来为了加快渲染改成 320×240,但忘了同步改 K,结果标注的投影全部错位。

bproc.camera.set_intrinsics_from_K_matrix(K, width, height)里 width 和 height 是显式传的,不一致时会静默按传入的值调整,不报错。稳妥的做法是把分辨率和 K 绑定成一个函数,改分辨率时就重算 K:

def make_K(fx, fy, cx, cy): return np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) W, H = 640, 480 fov_x_deg = 60.0 fx = W / (2 * np.tan(np.deg2rad(fov_x_deg) / 2)) # ≈ 554.3 fy = fx # 方形像素,focal 相同 K = make_K(fx, fy, W / 2, H / 2) bproc.camera.set_resolution(W, H) bproc.camera.set_intrinsics_from_K_matrix(K, W, H)

60 度水平视场对应 640 宽度下 fx 约 554 像素,这个数值可以作为合理性参考——如果你的 fx 算出来是几十或者几千,多半有单位或者公式搞错了。主点默认放在图像中心(W/2, H/2),除非你知道真实相机有偏移,否则不要动。

6. 域随机化的适度原则:过度会降低效果

6.1 哪些维度值得放开随机

域随机化的本质是让模型见到足够多的“无关变化”,从而只关注真正相关的特征。位姿估计里值得随机的维度,按重要性排序:

随机维度建议范围理由
光照强度与方向强随机真实场景光照差异极大,是最主要的域差距来源
材质基础色中随机真实物体颜色多变,但同类零件颜色通常接近
材质粗糙度/金属度中随机影响高光和反射,对轮廓提取有益
相机内参弱到中模拟不同焦距/距离,但不要极端
背景类型强随机纯色和复杂背景都应该出现
物体数量与遮挡中随机遮挡样本是模型真正的痛点
图像噪声/模糊弱模拟传感器特性,过量会破坏轮廓

背景这一项特别提一下。全用纯色背景训练出来的模型,一遇到复杂背景就废。我的做法是按比例混合:40% 纯色/渐变背景,40% 随机贴图背景(用无版权纹理库),20% 用真实场景的无物体照片做背景。第三类效果最好,但获取成本也最高。

6.2 随机过头会带来的负面影响

有几种随机化我试过之后去掉了。一是极端的相机俯仰——超过 ±60 度的倾斜,渲染出来的图在真实桌面场景里几乎不可能出现,白白稀释了有效样本。二是极端的颜色饱和度——把物体染成高饱和的荧光色,确实增加了视觉多样性,但训练出来的模型对颜色过于敏感,在真实灰度图上表现变差。三是过量的运动模糊——渲染的运动模糊和真实相机的运动模糊在频域特性上差异很大,加了反而引入伪影。

判断某个随机项该不该留,有个简单的经验法则:问自己“真实场景里这个变化出现的概率有多大”。如果低于 5%,就把它去掉或者降到很低的权重。

6.3 用后期合成补上渲染不擅长的部分

渲染器不擅长模拟的东西(传感器噪声、JPEG 压缩伪影、镜头渐晕),用后期合成补比在渲染器里硬做要高效得多。一个简单的做法是在渲染完成后,对每张图按随机概率施加:

def degrade(img): if np.random.rand() < 0.5: img = cv2.GaussianBlur(img, (3, 3), np.random.uniform(0, 0.8)) noise = np.random.normal(0, np.random.uniform(0, 6), img.shape) img = np.clip(img + noise, 0, 255).astype(np.uint8) if np.random.rand() < 0.3: q = np.random.randint(60, 95) img = cv2.imdecode(cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, q])[1], 1) return img

关键是这种退化只作用于 RGB,不能动掩码和深度。所以退化步骤要放在标注导出之后,或者严格区分路径。这个顺序搞反了,掩码和图像对不上,整个数据集作废。

7. 导出与自检:让标注自己证明自己

7.1 文件组织要与训练侧的解码逻辑对齐

导出的目录结构建议按标准的位姿数据集风格来:每张图一个 ID,RGB 在rgb/,深度在depth/,掩码在mask/,位姿在scene_gt.json这类标注文件里按场景聚合。位姿要存成行优先展开的 12 个数(3×4 矩阵),或者分开存 R 和 t,看你的训练框架习惯。

bproc.writer.write_bop( os.path.join(OUTPUT_DIR, "bop_data"), target_objects=[obj], dataset="custom", depths=data["depth"], colors=data["colors"], color_file_format="JPEG", ignore_dist_thres=10.0, m2mm=True, )

m2mm这个参数别忽略。深度图默认是米,而多数位姿格式约定用毫米,不转的话训练侧读到的深度会小三个数量级,位姿的平移分量也跟着错。不同版本的write_bop参数名有调整,跑之前先看一眼本地版本的签名,别直接抄网上的代码。

自己写导出也很常见,尤其是需要加自定义字段的时候。核心就是把每帧的obj2cam矩阵、相机 K、以及物体 ID 一起写进 JSON,注意 JSON 里不能有纳秒级的浮点抖动用不了,转成 Python float 再序列化。

7.2 三种成本最低的自检

第一种,前面说过的投影检验,抽样 100 帧跑一遍,凸包 IoU 低于 0.85 的帧全部标红人工看一眼。第二种,深度一致性检验:取掩码区域的深度中位数,和标注平移向量的模长做比较,两者的差异应该在一个合理的物体尺寸范围内。差异巨大的帧说明位姿或者深度有一个不对。第三种,连续性检验——如果你是按固定轨迹采相机,相邻帧之间的位姿变化应该平滑,突变超过阈值的帧意味着采样出了问题。

这三项加起来写不到两百行代码,但能把 90% 以上的低级错误挡在训练之前。数据集做错最难受的地方是它不会立刻报错,只会让模型表现莫名其妙地差,然后你花两周去调网络。

8. 大批量渲染的工程化:分片、容错与加速

8.1 单帧耗时的拆解和规模预估

一张 640×480、128 采样、单物体、简单光照的图,在中端 GPU 上大约 0.3 到 0.8 秒。加上场景重置、采样、标注写入的开销,端到端按 1 秒一帧估比较稳妥。十万张图就是一天多一点的纯渲染时间。

这个数字看起来还行,但有几个放大因素:物体面数、光源数量、反射和折射(如果开了玻璃材质)、分辨率。每翻倍分辨率,时间是四倍。所以先用 320×240 跑 200 帧测一下实际速度,再按真实分辨率线性外推,别凭感觉估算——我见过把两天的任务估成八小时的情况。

8.2 分片跑和断点续跑

一次性跑十万帧几乎必然在中途出问题:显存泄漏、驱动超时、某个极端场景导致采样死循环。所以一定要分片,每片 2000 到 5000 帧,脚本接受起始 ID 和数量两个参数,每片独立跑、独立写盘。

# 分片执行,每片 3000 帧 for i in $(seq 0 33); do START=$((i * 3000)) blenderproc run render.py --start_id $START --count 3000 --seed $((i + 1000)) done

每片一个独立随机种子,保证可复现。已经跑完的片如果出问题,删掉重跑就行,不用从头来。同时跑多片的话,注意 GPU 显存要够,通常一块卡跑两个进程就是极限了,再多反而因为显存换入换出变慢。

8.3 常见的崩溃原因和对应处理

采样死循环是最常见的。物体在平面上怎么放都碰撞,sample_poses_on_surface会一直重试直到超时。处理办法是给max_tries设一个合理值,失败时记录下来跳到下一帧,而不是让整个任务挂住。跑完之后统计失败率,如果超过 5%,说明场景参数设置有问题(物体太大、平面太小、距离范围不合理),要回头调参数而不是硬扛。

显存泄漏通常表现为跑到几千帧后速度骤降甚至 OOM。原因是每一帧新建的材质、贴图没有释放。BlenderProc 里有资源清理接口,但更简单的办法是分片——每片重启进程,泄漏累积不到出问题的程度。

还有一个很隐蔽的问题:渲染出来的图在某些帧上突然全黑。大概率是相机被放到了物体内部,或者相机在平面下方。加入相机位置的合法性检查(相机到物体的距离在合理范围、相机 z 坐标高于平面)能挡掉绝大多数。

9. 一点个人经验

这套流程我从最早的纯 Blender 脚本一路用到 BlenderProc,最大的感受是:真正把时间耗掉的从来不是渲染,而是坐标系和标注格式。这两件事上花一天时间做严格的验证,后面能省掉两周的调试。所以我的建议是,第一次搭环境时不要急着跑批量,先用单个物体、固定相机位置渲染 5 帧,把投影自检跑通,确认每一帧的包围盒都精确框住了物体,再去改采样范围。这个顺序看起来慢,其实最快。

另一个感受是关于数据规模的。很多人以为渲染数据是越多越好,实际上五万张精心设计的采样,效果往往好过五十万张随机撒点的。采样范围、遮挡比例、光照变化的分布设计,比数量重要得多。我现在的习惯是先出一版一万张的小数据集,训一个基线,看失败样本集中在什么位姿区间,再针对性地调采样分布重出一版。这个迭代两三轮,比一次性堆量有效率。

最后提一个容易忽略的细节:物体的category_id一旦在数据集里分配好,就不要中途改动。很多框架的类别映射是硬编码在配置里的,改了 ID 而没同步,训练不会报错,只会让某一类的精度莫名其妙变成零。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进