一、前言
在三维点云处理与可视化中,固定视角批量生成点云渲染截图是一个常见的需求。例如,想要将同一系列的点云(PCD 文件)在同样的视角下生成序列图片,以便后续合成为视频或进行其他可视化演示。本文将介绍如何使用 Python + Open3D 实现批量加载 PCD 文件、设置统一的相机视角,并导出渲染截图。
二、环境准备
- Python 环境:建议使用 Python 3.7+。
- Open3D 库:本文使用的是
open3d
和open3d.visualization.gui
。安装方式如下:pip install open3d
- 其他依赖库:
numpy
pickle
glob
time
os
如果缺少对应的库,使用pip install 库名
即可。
三、代码解析
下面的代码分为几个主要部分:
- 相机矩阵转换:将 Open3D 的模型矩阵转换为外参矩阵。
- 相机内参生成:根据视口大小与视场角(FOV)生成相机内参。
- 保存与加载相机视角:使用
pickle
将当前相机的内外参持久化保存,方便在下次使用时快速恢复相机位置。 - 批量处理函数:遍历指定文件夹下所有
.pcd
文件,加载点云、应用相机视角、并自动保存渲染截图。 - 主函数:在
if __name__ == "__main__":
中调用批处理函数,或先进行单独的相机视角设置保存操作。
完整代码如下(可直接复制使用):
import numpy as np
import open3d as o3d
import open3d.visualization.gui as gui
from pickle import load, dump
import os
import glob
import time# 用于将坐标系转换为OpenGL风格
ToGLCamera = np.array([[1, 0, 0, 0],[0, -1, 0, 0],[0, 0, -1, 0],[0, 0, 0, 1]
])
FromGLGamera = np.linalg.inv(ToGLCamera)def model_matrix_to_extrinsic_matrix(model_matrix):"""将Open3D的model_matrix转换为外参矩阵"""return np.linalg.inv(model_matrix @ FromGLGamera)def create_camera_intrinsic_from_size(width=1024, height=768, hfov=60.0, vfov=60.0):"""根据视口大小与水平/垂直FOV生成相机内参"""fx = (width / 2.0) / np.tan(np.radians(hfov) / 2)fy = (height / 2.0) / np.tan(np.radians(vfov) / 2)return np.array([[fx, 0, width / 2.0],[0, fy, height / 2.0],[0, 0, 1]])def save_view(vis, fname='saved_view.pkl'):"""保存当前可视化窗口的相机视角(内参、外参、图像尺寸)"""try:model_matrix = np.asarray(vis.scene.camera.get_model_matrix())extrinsic = model_matrix_to_extrinsic_matrix(model_matrix)width, height = vis.size.width, vis.size.heightintrinsic = create_camera_intrinsic_from_size(width, height)saved_view = dict(extrinsic=extrinsic, intrinsic=intrinsic, width=width, height=height)with open(fname, 'wb') as pickle_file:dump(saved_view, pickle_file)print(f"Camera view saved to {fname}")except Exception as e:print("Error saving view:", e)def load_view(vis, fname="saved_view.pkl"):"""加载已保存的相机视角(内参、外参、图像尺寸)"""try:with open(fname, 'rb') as pickle_file:saved_view = load(pickle_file)vis.setup_camera(saved_view['intrinsic'], saved_view['extrinsic'],saved_view['width'], saved_view['height'])print(f"Camera view loaded from {fname}")except Exception as e:print("Can't load view file:", e)def process_pcd_folder(input_folder, output_folder, view_file='saved_view.pkl'):"""批量处理文件夹中的所有 PCD 文件,应用指定视角并保存截图"""os.makedirs(output_folder, exist_ok=True)pcd_files = sorted(glob.glob(os.path.join(input_folder, "*.pcd")))if not pcd_files:print(f"No PCD files found in {input_folder}")returnprint(f"Found {len(pcd_files)} PCD files")# 初始化GUIgui.Application.instance.initialize()vis = o3d.visualization.O3DVisualizer("PCD Batch Renderer", 1920, 1080)gui.Application.instance.add_window(vis)# 设置渲染参数vis.point_size = 4vis.show_axes = Falsevis.show_skybox(False)def process_next(idx):if idx >= len(pcd_files):print("Batch processing completed!")gui.Application.instance.quit()returnpcd_file = pcd_files[idx]print(f"Processing {idx+1}/{len(pcd_files)}: {os.path.basename(pcd_file)}")try:# 加载点云文件pcd = o3d.io.read_point_cloud(pcd_file)geom_name = f"PointCloud_{idx}"# 清除之前所有几何体,确保内存资源不会累积if idx > 0:vis.remove_geometry(f"PointCloud_{idx-1}")vis.add_geometry(geom_name, pcd)# 加载预先保存的视角load_view(vis, view_file)# 构建输出路径base_name = os.path.splitext(os.path.basename(pcd_file))[0]output_path = os.path.join(output_folder, f"{base_name}.png")def take_screenshot():# 延迟1秒以确保视角和渲染完全加载time.sleep(1)vis.export_current_image(output_path)print(f"Screenshot saved to {output_path}")# 处理完当前文件后,处理下一个process_next(idx + 1)# 使用post_to_main_thread确保截图任务在GUI线程执行gui.Application.instance.post_to_main_thread(vis, take_screenshot)except Exception as e:print(f"Error processing {pcd_file}: {e}")# 出错时跳过当前文件,继续下一个process_next(idx + 1)# 开始处理第一个文件process_next(0)gui.Application.instance.run()def batch_process():"""主函数:指定输入、输出文件夹以及相机视角文件,然后进行批量处理"""input_folder = './input'output_folder = './screenshots'os.makedirs(output_folder, exist_ok=True)view_file = 'saved_view.pkl'process_pcd_folder(input_folder, output_folder, view_file)if __name__ == "__main__":# 若需要先设置视角,运行 save_view 所在的逻辑# 若已设置好视角,运行 batch_process()批量处理batch_process()
1. 代码主要流程
- 读取文件列表:通过
glob.glob
获取指定文件夹下的所有.pcd
文件并排序。 - 初始化 Open3D GUI:使用
O3DVisualizer
进行可视化。 - 循环处理每个 PCD:
- 读取点云数据
pcd = o3d.io.read_point_cloud(...)
- 加载之前保存的视角参数
load_view(vis, view_file)
- 设置几何体到渲染窗口
- 通过
vis.export_current_image(...)
将当前视图截图保存
- 读取点云数据
- 处理结束后退出:当全部
.pcd
文件处理完毕,自动退出 GUI。
2. 视角保存与加载
save_view(vis, fname='saved_view.pkl')
:从当前的vis.scene.camera
获取model_matrix
,然后计算外参矩阵、内参矩阵并存储到一个字典中,通过pickle
持久化到saved_view.pkl
文件。load_view(vis, fname='saved_view.pkl')
:从文件中读取上述字典,调用vis.setup_camera(...)
将相机恢复到保存时的视角。
这样做的好处是,我们可以先交互式地在 Open3D 中调整一个理想的点云视角,然后保存该视角。后续就可以用同样的参数去渲染其他点云,实现“统一视角”输出。
3. 视角设置的两种方式
- 先在单个点云上用脚本交互式设置并保存:
- 先运行一个类似的脚本,只加载一个点云,不做批处理。
- 在界面中使用鼠标旋转/平移点云至理想位置,然后调用
save_view(vis)
。
- 直接修改代码中的相机参数:如果你对内参、外参很熟悉,也可以直接硬编码想要的矩阵。
四、使用说明
- 准备 PCD 文件:将所有需要处理的
.pcd
文件放在同一个文件夹中。 - 保存视角(可选):
- 若你已知道要使用的视角参数,可以跳过这一步;否则先写个简单脚本,加载一两个 PCD 文件后,通过交互操作找到满意的视角,执行
save_view(vis)
。 - 此时会生成一个
saved_view.pkl
文件,里面记录了相机的内外参。
- 若你已知道要使用的视角参数,可以跳过这一步;否则先写个简单脚本,加载一两个 PCD 文件后,通过交互操作找到满意的视角,执行
- 运行批处理:
- 修改
batch_process()
中的input_folder
和output_folder
为你的输入、输出路径。 - 运行脚本后,Open3D 窗口会依次加载每个
.pcd
,应用保存好的视角,然后自动截图并存储到output_folder
中。
- 修改
在所有点云都处理完后,你就能在输出文件夹下看到对应的 .png
文件序列。
五、结果展示
下面是一张示例截图,展示了点云在固定视角下的渲染效果(仅做示意,非实际数据):
六、后续扩展
- 生成视频:如果想将渲染好的序列图片合成为视频,可使用
ffmpeg
,示例命令如下:
其中ffmpeg -framerate 10 -i labeled_sync_frame_%03d.png -c:v libx264 -pix_fmt yuv420p output.mp4
-framerate 10
表示每秒 10 帧,可根据需要调整。 - 更多可视化选项:如改变
point_size
、背景颜色、或添加坐标轴等,可参考 Open3D 文档或修改O3DVisualizer
的属性。 - 其他文件格式:如果想批量处理
.ply
或.xyz
,只需要在代码中修改对应的读取方式,以及glob.glob
匹配模式即可。
七、总结
通过上述方法,可以轻松地在同一视角下对多份点云进行批量渲染和截图,适用于制作点云动画、对比分析等场景。核心思想是事先保存好相机参数,并在批处理过程中为每个点云恢复相同的内外参,保证输出图像的视角一致。希望对你的三维可视化工作有所帮助,欢迎交流讨论!