使用 Open3D 批量渲染并导出固定视角点云截图

一、前言

在三维点云处理与可视化中,固定视角批量生成点云渲染截图是一个常见的需求。例如,想要将同一系列的点云(PCD 文件)在同样的视角下生成序列图片,以便后续合成为视频或进行其他可视化演示。本文将介绍如何使用 Python + Open3D 实现批量加载 PCD 文件、设置统一的相机视角,并导出渲染截图。

二、环境准备

  1. Python 环境:建议使用 Python 3.7+。
  2. Open3D 库:本文使用的是 open3dopen3d.visualization.gui。安装方式如下:
    pip install open3d
    
  3. 其他依赖库
    • numpy
    • pickle
    • glob
    • time
    • os
      如果缺少对应的库,使用 pip install 库名 即可。

三、代码解析

下面的代码分为几个主要部分:

  1. 相机矩阵转换:将 Open3D 的模型矩阵转换为外参矩阵。
  2. 相机内参生成:根据视口大小与视场角(FOV)生成相机内参。
  3. 保存与加载相机视角:使用 pickle 将当前相机的内外参持久化保存,方便在下次使用时快速恢复相机位置。
  4. 批量处理函数:遍历指定文件夹下所有 .pcd 文件,加载点云、应用相机视角、并自动保存渲染截图。
  5. 主函数:在 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.height
        intrinsic = 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}")
        return

    print(f"Found {len(pcd_files)} PCD files")

    # 初始化GUI
    gui.Application.instance.initialize()
    vis = o3d.visualization.O3DVisualizer("PCD Batch Renderer", 1920, 1080)
    gui.Application.instance.add_window(vis)

    # 设置渲染参数
    vis.point_size = 4
    vis.show_axes = False
    vis.show_skybox(False)

    def process_next(idx):
        if idx >= len(pcd_files):
            print("Batch processing completed!")
            gui.Application.instance.quit()
            return

        pcd_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}")
                
                # 判断是否是最后一个文件
                if idx == len(pcd_files) - 1:
                    # 如果是最后一个文件,延迟一段时间后再退出程序
                    def quit_delayed():
                        print("All files processed. Exiting...")
                        gui.Application.instance.quit()
                    
                    # 延迟2秒后退出,确保最后的截图已经保存
                    time.sleep(1) 
                    gui.Application.instance.post_to_main_thread(vis, quit_delayed)
                else:
                    # 否则继续处理下一个文件
                    time.sleep(1)
                    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
    1. 读取点云数据 pcd = o3d.io.read_point_cloud(...)
    2. 加载之前保存的视角参数 load_view(vis, view_file)
    3. 设置几何体到渲染窗口
    4. 通过 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. 视角设置的两种方式

  1. 先在单个点云上用脚本交互式设置并保存
    • 先运行一个类似的脚本,只加载一个点云,不做批处理。
    • 在界面中使用鼠标旋转/平移点云至理想位置,然后调用 save_view(vis)
  2. 直接修改代码中的相机参数:如果你对内参、外参很熟悉,也可以直接硬编码想要的矩阵。

四、使用说明

  1. 准备 PCD 文件:将所有需要处理的 .pcd 文件放在同一个文件夹中。
  2. 保存视角(可选):
    • 若你已知道要使用的视角参数,可以跳过这一步;否则先写个简单脚本,加载一两个 PCD 文件后,通过交互操作找到满意的视角,执行 save_view(vis)
    • 此时会生成一个 saved_view.pkl 文件,里面记录了相机的内外参。
  3. 运行批处理
    • 修改 batch_process() 中的 input_folderoutput_folder 为你的输入、输出路径。
    • 运行脚本后,Open3D 窗口会依次加载每个 .pcd,应用保存好的视角,然后自动截图并存储到 output_folder 中。

在所有点云都处理完后,你就能在输出文件夹下看到对应的 .png 文件序列。

五、结果展示

下面是一张示例截图,展示了点云在固定视角下的渲染效果(仅做示意,非实际数据):
在这里插入图片描述

六、后续扩展

  1. 生成视频:如果想将渲染好的序列图片合成为视频,可使用 ffmpeg,示例命令如下:
    ffmpeg -framerate 10 -i labeled_sync_frame_%03d.png -c:v libx264 -pix_fmt yuv420p output.mp4
    
    其中 -framerate 10 表示每秒 10 帧,可根据需要调整。
  2. 更多可视化选项:如改变 point_size、背景颜色、或添加坐标轴等,可参考 Open3D 文档或修改 O3DVisualizer 的属性。
  3. 其他文件格式:如果想批量处理 .ply.xyz,只需要在代码中修改对应的读取方式,以及 glob.glob 匹配模式即可。

七、总结

通过上述方法,可以轻松地在同一视角下对多份点云进行批量渲染和截图,适用于制作点云动画、对比分析等场景。核心思想是事先保存好相机参数,并在批处理过程中为每个点云恢复相同的内外参,保证输出图像的视角一致。希望对你的三维可视化工作有所帮助,欢迎交流讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值