Skip to content

DepthAI 点云查看器

介绍

本脚本允许用户通过兼容 DepthAI 的硬件捕获 3D 点云数据,并使用 Open3D 实现实时可视化。该脚本的一个重要功能是能够将捕获的点云以多种格式保存到磁盘上。

前提条件和依赖关系

确保你的系统满足以下要求:

  • 兼容 DepthAI 的硬件设备(例如 OAK-D 相机)
  • Python 3.x 环境
  • 必要的 Python 库:depthai, opencv-python, numpy, open3d

安装

在终端或命令提示符中运行以下命令以安装所需库:

pip install depthai opencv-python numpy open3d

源码

point_cloud_viewer.py
# coding=utf-8
import argparse
import time
from pathlib import Path

import cv2
import depthai as dai
import numpy as np
import open3d as o3d

root = Path(__file__).parent.resolve()

# 解析命令行参数
parser = argparse.ArgumentParser(description="DepthAI Point Cloud Viewer")
parser.add_argument(
    "-mres",
    "--mono_resolution",
    type=str,
    default="400p",
    choices={"480p", "400p", "720p", "800p", "1200p"},
    help="选择单目相机分辨率(高度)。 (默认:%(default)s)",
)
parser.add_argument(
    "-cres",
    "--color_resolution",
    default="1080p",
    choices={
        "720p",
        "800p",
        "1080p",
        "1200p",
        "4k",
        "5mp",
        "12mp",
        "13mp",
        "48mp",
        "1352X1012",
        "1440X1080",
        "2024X1520",
        "4000X3000",
        "5312X6000",
    },
    help="选择彩色相机分辨率/高度。 (默认:%(default)s)",
)
parser.add_argument(
    "-ds",
    "--isp_downscale",
    nargs=2,
    default=[1, 3],
    type=int,
    help="将 ISP 输出按此因子降低采样率(默认:%(default)s)",
)

parser.add_argument(
    "-f",
    "--fps",
    type=int,
    default=30,
    help="相机帧率(默认:%(default)s)",
)

parser.add_argument(
    "-e",
    "--extended_disparity",
    default=False,
    action="store_true",
    help="启用扩展视差,最小深度越近,视差范围加倍(从 95 到 190)(默认:%(default)s)",
)
parser.add_argument(
    "-ne",
    "--no_extended_disparity",
    default=False,
    action="store_true",
    help="禁用扩展视差,最小深度越近,视差范围加倍(从 95 到 190)(默认:%(default)s)",
)
parser.add_argument(
    "-sub",
    "--subpixel",
    default=True,
    action="store_true",
    help="使用亚像素插值(默认:%(default)s)",
)
parser.add_argument(
    "-nsub",
    "--no_subpixel",
    action="store_true",
    help="禁用亚像素插值(默认:%(default)s)",
)
parser.add_argument(
    "-l",
    "--lr_check",
    # default=True,
    action="store_false",
    help="左/右视差检查(默认:%(default)s)",
)
parser.add_argument(
    "-file",
    "--filename",
    type=str,
    default="point_cloud",
    help="输出 PLY 文件名(默认:%(default)s)",
)
parser.add_argument(
    "-s",
    "--sparse",
    action="store_true",
    help="启用或禁用稀疏点云计算(默认:%(default)s)",
)
parser.add_argument(
    "-suf",
    "--suffix",
    default="ply",
    choices={"ply", "pcd", "xyz", "xyzrgb"},
    help="保存点云文件后缀(默认:%(default)s)",
)
args = parser.parse_args()

# 定义单目和彩色相机的分辨率
mono_res_opts = {
    "400p": dai.MonoCameraProperties.SensorResolution.THE_400_P,
    "480p": dai.MonoCameraProperties.SensorResolution.THE_480_P,
    "720p": dai.MonoCameraProperties.SensorResolution.THE_720_P,
    "800p": dai.MonoCameraProperties.SensorResolution.THE_800_P,
    "1200p": dai.MonoCameraProperties.SensorResolution.THE_1200_P,
}

color_res_opts = {
    "720p": dai.ColorCameraProperties.SensorResolution.THE_720_P,
    "800p": dai.ColorCameraProperties.SensorResolution.THE_800_P,
    "1080p": dai.ColorCameraProperties.SensorResolution.THE_1080_P,
    "1200p": dai.ColorCameraProperties.SensorResolution.THE_1200_P,
    "4k": dai.ColorCameraProperties.SensorResolution.THE_4_K,
    "5mp": dai.ColorCameraProperties.SensorResolution.THE_5_MP,
    "12mp": dai.ColorCameraProperties.SensorResolution.THE_12_MP,
    "13mp": dai.ColorCameraProperties.SensorResolution.THE_13_MP,
    "48mp": dai.ColorCameraProperties.SensorResolution.THE_48_MP,
    "1352X1012": dai.ColorCameraProperties.SensorResolution.THE_1352X1012,
    "1440X1080": dai.ColorCameraProperties.SensorResolution.THE_1440X1080,
    "2024X1520": dai.ColorCameraProperties.SensorResolution.THE_2024X1520,
    "4000X3000": dai.ColorCameraProperties.SensorResolution.THE_4000X3000,
    "5312X6000": dai.ColorCameraProperties.SensorResolution.THE_5312X6000,
}

MONO_RES = mono_res_opts.get(args.mono_resolution)
COLOR_RES = color_res_opts.get(args.color_resolution)

FPS = args.fps
SPARSE = args.sparse
SUFFIX = args.suffix
FILENAME = args.filename
NUMERATOR, DENOMINATOR = args.isp_downscale
EXTENDED_DISPARITY = args.extended_disparity
if args.no_extended_disparity:
    EXTENDED_DISPARITY = False
SUBPIXEL = args.subpixel
if args.no_subpixel:
    SUBPIXEL = False

LR_CHECK = args.lr_check


class FPSCounter:
    def __init__(self):
        self.frameCount = 0
        self.fps = 0
        self.startTime = time.time()

    def tick(self):
        self.frameCount += 1
        if self.frameCount % 10 == 0:
            elapsedTime = time.time() - self.startTime
            self.fps = self.frameCount / elapsedTime
            self.frameCount = 0
            self.startTime = time.time()
        return self.fps


def create_pipeline():
    """创建 DepthAI 流水线"""
    pipeline = dai.Pipeline()
    # 创建相机节点
    camRgb = pipeline.create(dai.node.ColorCamera)
    monoLeft = pipeline.create(dai.node.MonoCamera)
    monoRight = pipeline.create(dai.node.MonoCamera)
    # 创建深度估计和点云节点
    depth = pipeline.create(dai.node.StereoDepth)
    pointcloud = pipeline.create(dai.node.PointCloud)
    # 创建同步节点和输出节点
    sync = pipeline.create(dai.node.Sync)
    xOut = pipeline.create(dai.node.XLinkOut)
    xOut.input.setBlocking(False)

    # 设置相机属性
    camRgb.setResolution(COLOR_RES)
    camRgb.setBoardSocket(dai.CameraBoardSocket.CAM_A)
    camRgb.setIspScale(NUMERATOR, DENOMINATOR)
    camRgb.setFps(FPS)

    monoLeft.setResolution(MONO_RES)
    monoLeft.setCamera("left")
    monoLeft.setFps(FPS)
    monoRight.setResolution(MONO_RES)
    monoRight.setCamera("right")
    monoRight.setFps(FPS)

    # 设置深度估计属性
    depth.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
    depth.initialConfig.setMedianFilter(dai.MedianFilter.KERNEL_7x7)
    depth.setLeftRightCheck(LR_CHECK)
    depth.setExtendedDisparity(EXTENDED_DISPARITY)
    depth.setSubpixel(SUBPIXEL)
    depth.setDepthAlign(dai.CameraBoardSocket.CAM_A)
    depth.setOutputSize(*camRgb.getIspSize())

    # 设置点云属性
    pointcloud.initialConfig.setSparse(SPARSE)

    # 将节点连接起来
    monoLeft.out.link(depth.left)
    monoRight.out.link(depth.right)
    depth.depth.link(pointcloud.inputDepth)
    camRgb.isp.link(sync.inputs["rgb"])
    pointcloud.outputPointCloud.link(sync.inputs["pcl"])
    sync.out.link(xOut.input)
    xOut.setStreamName("out")

    return pipeline


def main():  # noqa: PLR0914, PLR0915
    """主函数,运行 DepthAI 点云查看器"""
    pipeline = create_pipeline()
    with dai.Device(pipeline) as device:
        print("DepthAI 点云查看器已启动!")
        print("按 `q`键退出,按`s` 键保存点云数据。")

        isRunning = True

        def key_callback(vis, action, mods):
            global isRunning  # noqa: PLW0603
            if action == 0:
                isRunning = False

        q = device.getOutputQueue(name="out", maxSize=4, blocking=False)
        vis = o3d.visualization.VisualizerWithKeyCallback()
        vis.create_window()
        vis.register_key_action_callback(81, key_callback)
        pcd = o3d.geometry.PointCloud()
        coordinateFrame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=1000, origin=[0, 0, 0])
        vis.add_geometry(coordinateFrame)

        first = True
        fpsCounter = FPSCounter()
        while isRunning:
            inMessage = q.get()
            inColor = inMessage["rgb"]
            inPointCloud = inMessage["pcl"]
            if inColor is not None:
                cvColorFrame = inColor.getCvFrame()
                # 将帧转换为 RGB 格式
                cvRGBFrame = cv2.cvtColor(cvColorFrame, cv2.COLOR_BGR2RGB)
                fps = fpsCounter.tick()
                # 在帧上显示 FPS
                cv2.putText(
                    cvColorFrame,
                    f"FPS: {fps:.2f}",
                    (10, 30),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    1,
                    (0, 0, 255),
                    2,
                )
                cv2.imshow("color", cvColorFrame)

            if inPointCloud:
                time.time()
                points = inPointCloud.getPoints().astype(np.float64)
                pcd.points = o3d.utility.Vector3dVector(points)
                colors = (cvRGBFrame.reshape(-1, 3) / 255.0).astype(np.float64)
                pcd.colors = o3d.utility.Vector3dVector(colors)
                if first:
                    vis.add_geometry(pcd)
                    first = False
                else:
                    vis.update_geometry(pcd)
            vis.poll_events()
            vis.update_renderer()

            key = cv2.waitKey(1) & 0xFF
            if key == ord("q"):
                break
            if key == ord("s"):
                file_path = root.joinpath(f"{FILENAME}.{SUFFIX}")
                o3d.io.write_point_cloud(file_path.as_posix(), pcd)
                print(f"将点云保存为 {file_path}")

        cv2.destroyAllWindows()
        vis.destroy_window()


if __name__ == "__main__":
    main()

用法

运行脚本并通过命令行参数自定义设置,例如设置相机分辨率和帧率:

python point_cloud_viewer.py --mono_resolution 400p --color_resolution 1080p --fps 30

可用参数

  • -mres, --mono_resolution:单目相机分辨率(高度,单位:像素)
  • -cres, --color_resolution:彩色相机分辨率
  • -f, --fps:相机帧率
  • -f, --filename:输出 PLY 文件名 (不含后缀)
  • -suf--suffix:输出 PLY 后缀名
  • ... [包括所有其他参数及其解释]

保存点云

脚本提供了保存捕获的点云数据的功能。用户可以通过按 's' 键来触发保存过程,并可以通过 --output 参数自定义保存的文件名和 --pointcloud 参数来指定文件格式。

支持的点云格式有:

  • PLY(默认)
  • PCD
  • XYZ
  • XYZRGB

示例

启动查看器并将点云数据保存为默认的 PLY 格式:

python point_cloud_viewer.py --output my_point_cloud

启动查看器并将点云数据保存为 PCD 格式:

python point_cloud_viewer.py --output my_point_cloud -p pcd
当你按下 's' 键时,点云将保存到脚本文件所在目录下,文件名为 my_point_cloud.pcd。

常见问题解答

问:如果收到关于缺少 Open3D 的错误怎么办?

答:确保您已经使用给出的安装命令安装了所有依赖项。