转换拼接教程

本教程介绍如何使用多台相机相互校准的结果。多相机转换的结果是一个变换矩阵,可用于将一个点云转换到另一个点云的坐标系中。这是拼接点云的第一步。

先决条件

在学习本教程之前,您应该已经完成了 多相机标定教程

加载相关变换矩阵并映射到点云

要应用正确的变换矩阵,我们必须将其映射到相应的 frame 。如果直接从相机采集,我们可以使用其序列号;如果从文件加载,我们可以使用 ZDF frame 中的序列号。当我们使用相机序列号或 ZDF 文件中的序列号时,我们希望在 YAML 文件名上找到完全匹配的序列号。

我们将变换矩阵映射到点云的方式与直接从相机捕获以及从文件加载的方式非常相似。主要区别在于,当使用相机捕获时,我们会访问序列号并搜索以该名称命名的 YAML 文件。然后,我们将变换矩阵映射到相机。

转至源代码

source

const auto transformsMappedToCameras =
    getTransformationMatricesFromYAML(transformationMatricesfileList, connectedCameras);
转至源代码

源码

transforms_mapped_to_cameras = get_transformation_matrices_from_yaml(args.yaml_files, connected_cameras)

另一方面,在从 ZDF 文件加载数据时,我们需要从文件列表中找到一个 ZDF 文件,并从 frame 中提取其序列号。然后,我们在同一文件列表中查找使用该序列号作为文件名的 YAML 文件。接着,我们将变换矩阵与对应的 frame 进行映射。在 getTransformedPointClouds 函数中,我们立即对点云应用该变换矩阵,并扩展无序的点云数据。

转至源代码

源码

Zivid::UnorganizedPointCloud getTransformedPointClouds(
    Zivid::UnorganizedPointCloud stitchedPointCloud;
                Zivid::Matrix4x4 transformationMatrixZivid(yamlFileName);
                Zivid::UnorganizedPointCloud currentPointCloud = frame.pointCloud().toUnorganizedPointCloud();
                stitchedPointCloud.extend(currentPointCloud.transform(transformationMatrixZivid));
转至源代码

源码

def get_transformed_point_clouds(
    stitched_point_cloud = zivid.UnorganizedPointCloud()
        transformation_matrix = zivid.Matrix4x4(yaml_file)
        current_point_cloud = frame.point_cloud().to_unorganized_point_cloud()
        stitched_point_cloud.extend(current_point_cloud.transform(transformation_matrix))

应用变换矩阵并将变换后的点云与之前的点云拼接

我们已经用变换矩阵映射了捕获内容。现在,我们可以进行变换和拼接了。

我们可以从 transform 中捕获并提取校正 transformsMappedToCameras 。每次捕获时,我们都会转换并扩展无序点云。

转至源代码

source

for(auto &camera : connectedCameras)
{
    const auto settingsPath = std::string(ZIVID_SAMPLE_DATA_DIR) + "/Settings/" + sanitizedModelName(camera)
                              + "_ManufacturingSpecular.yml";
    std::cout << "Imaging from camera: " << camera.info().serialNumber() << std::endl;
    const auto frame = camera.capture2D3D(Zivid::Settings(settingsPath));
    const auto unorganizedPointCloud = frame.pointCloud().toUnorganizedPointCloud();
    const auto transformationMatrix = transformsMappedToCameras.at(camera.info().serialNumber().toString());
    const auto transformedUnorganizedPointCloud = unorganizedPointCloud.transformed(transformationMatrix);
    stitchedPointCloud.extend(transformedUnorganizedPointCloud);
}
转至源代码

源码

for camera in connected_cameras:
    settings_path = (
        get_sample_data_path()
        / "Settings"
        / f"{camera.info.model_name.replace('2+', 'Two_Plus').replace('2', 'Two').replace(' ', '_')}_ManufacturingSpecular.yml"
    )
    print(f"Imaging from camera: {camera.info.serial_number}")
    frame = camera.capture(zivid.Settings.load(settings_path))
    unorganized_point_cloud = frame.point_cloud().to_unorganized_point_cloud()
    transformation_matrix = transforms_mapped_to_cameras[camera.info.serial_number]
    transformed_unorganized_point_cloud = unorganized_point_cloud.transformed(transformation_matrix)
    stitched_point_cloud.extend(transformed_unorganized_point_cloud)

体素下采样

在可视化之前清理拼接的点云:

转至源代码

source

const auto finalPointCloud = stitchedPointCloud.voxelDownsampled(0.5, 1);
转至源代码

源码

final_point_cloud = stitched_point_cloud.voxel_downsampled(0.5, 1)

这里我们要求 voxel (体素)大小为 0.5 毫米。如果涉及超过 2 个相机,第二个参数会很有用。这样它就可以用于在像素内进行多数投票。更多信息请参阅 体素下采样

可视化

我们使用 Open3D 来可视化和设置视点,以便将其放置在主相机内。

复制数据

我们可以将点直接从 GPU 复制到 Open3D 张量。颜色处理稍微复杂一些,因为 Zivid 将颜色管理为 3 个 8 位通道,而 Open3D 使用 3 个浮点数。因此,我们必须先将它们复制到 CPU,然后在进行规范化时将它们赋值给 Open3D 张量。

转至源代码

source

open3d::t::geometry::PointCloud copyToOpen3D(const Zivid::UnorganizedPointCloud &pointCloud)
{
    using namespace open3d::core;
    auto device = Device("CPU:0");
    auto xyzTensor = Tensor({ static_cast<int64_t>(pointCloud.size()), 3 }, Dtype::Float32, device);
    auto rgbTensor = Tensor({ static_cast<int64_t>(pointCloud.size()), 3 }, Dtype::Float32, device);

    pointCloud.copyData(reinterpret_cast<Zivid::PointXYZ *>(xyzTensor.GetDataPtr<float>()));

    // Open3D does not store colors in 8-bit
    const auto rgbaColors = pointCloud.copyColorsRGBA_SRGB();
    for(size_t i = 0; i < pointCloud.size(); ++i)
    {
        const auto r = static_cast<float>(rgbaColors(i).r) / 255.0f;
        const auto g = static_cast<float>(rgbaColors(i).g) / 255.0f;
        const auto b = static_cast<float>(rgbaColors(i).b) / 255.0f;
        rgbTensor.SetItem(TensorKey::Index(i), Tensor::Init({ r, g, b }));
    }

    open3d::t::geometry::PointCloud cloud(device);
    cloud.SetPointPositions(xyzTensor);
    cloud.SetPointColors(rgbTensor);
    return cloud;
}

转至源代码

源码

def copy_to_open3d_point_cloud(
    xyz: np.ndarray, rgb: np.ndarray, normals: Optional[np.ndarray] = None
) -> o3d.geometry.PointCloud:
    """Copy point cloud data to Open3D PointCloud object.

    Args:
        rgb: RGB image
        xyz: A numpy array of X, Y and Z point cloud coordinates
        normals: Ordered array of normal vectors, mapped to xyz

    Returns:
        An Open3D PointCloud
    """
    if len(np.shape(xyz)) == 3:
        xyz = np.nan_to_num(xyz).reshape(-1, 3)
    if normals is not None:
        normals = np.nan_to_num(normals).reshape(-1, 3)
    if len(np.shape(rgb)) == 3:
        rgb = rgb.reshape(-1, rgb.shape[-1])[:, :3]

    open3d_point_cloud = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(xyz))
    open3d_point_cloud.colors = o3d.utility.Vector3dVector(rgb / 255)
    if normals is not None:
        open3d_point_cloud.normals = o3d.utility.Vector3dVector(normals)

    return open3d_point_cloud

保存拼接后的点云

最后我们可以选择保存点云:

转至源代码

source

const std::string &fileName = stitchedPointCloudFileName;
std::cout << "Saving " << finalPointCloud.size() << " data points to " << fileName << std::endl;

using PLY = Zivid::Experimental::PointCloudExport::FileFormat::PLY;
const auto colorSpace = Zivid::Experimental::PointCloudExport::ColorSpace::sRGB;
Zivid::Experimental::PointCloudExport::exportUnorganizedPointCloud(
    finalPointCloud, PLY{ fileName, PLY::Layout::unordered, colorSpace });
转至源代码

源码

print(f"Saving {len(final_point_cloud.size())} data points to {args.output_file}")
export_unorganized_point_cloud(
    final_point_cloud, PLY(args.output_file, layout=PLY.Layout.unordered, color_space=ColorSpace.srgb)
)