点云教程

介绍

本教程介绍了如何使用 Zivid SDK 进行 点云 数据的相关操作。

小技巧

如果您更喜欢观看视频教程,我们的网络研讨会 Getting your point cloud ready for your application (为您的应用准备好点云)涵盖了点云教程。

先决条件

帧(Frame)

Frame 包含了点云和彩色图像(存储在计算设备内存中)以及捕获设置和相机信息。

Frame 包含了点云和彩色图像(存储在计算设备内存中)以及捕获设置和相机信息。

zivid.Frame 包含了点云和彩色图像(存储在计算设备内存中)以及捕获设置和相机信息。

捕获(Capture)

使用 Zivid 进行捕获时,您会得到一个 frame 作为返回数据。点云存储在该 frame 中,而该 frame 又存储在 GPU 内存中。捕获的图像可以选择包含或者不包含颜色信息,具体取决于您调用的方法。更多信息请参阅 包含不同捕获模式的表格 。

带颜色数据的捕获

如果您想要捕获带颜色的点云,可以使用 Camera::capture2D3D() 方法。

跳转到源码

源码

const auto frame = camera.capture2D3D(settings);
跳转到源码

源码

using (var frame = camera.Capture2D3D(settings))
跳转到源码

源码


frame = camera.capture_2d_3d(settings)

不带颜色数据的捕获

如果您想要捕获不带颜色的点云,可以使用 Camera::capture3D() 方法。

跳转到源码

source

const auto frame3D = camera.capture3D(settings);
跳转到源码

源码

using (var frame3D = camera.Capture3D(settings))
跳转到源码

源码

frame_3d = camera.capture_3d(settings)

查看 捕获教程 了解有关如何捕获图像的详细说明。

加载(Load)

可以通过 ZDF 文件加载图像帧。

跳转到源码

源码

const auto dataFile = std::string(ZIVID_SAMPLE_DATA_DIR) + "/Zivid3D.zdf";
std::cout << "Reading ZDF frame from file: " << dataFile << std::endl;
const auto frame = Zivid::Frame(dataFile);
跳转到源码

源码

var dataFile =
    Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "/Zivid/Zivid3D.zdf";
Console.WriteLine("Reading ZDF frame from file: " + dataFile);

using (var frame = new Zivid.NET.Frame(dataFile))
{
跳转到源码

源码

data_file = get_sample_data_path() / "Zivid3D.zdf"
print(f"Reading point cloud from file: {data_file}")

frame = zivid.Frame(data_file)

点云

从 Frame 获取句柄

您现在可以从 GPU 上的点云数据获取句柄。

跳转到源码

源码

const auto pointCloud = frame.pointCloud();
跳转到源码

源码

var pointCloud = frame.PointCloud;
跳转到源码

源码

point_cloud = frame.point_cloud()

点云包含了 XYZ、RGB 和 SNR 数据,分布在 2D 网格上。

如需了解更多信息,请查看 点云结构。

方法 Frame::pointCloud() 不从 GPU 内存执行任何复制。

备注

Camera::capture2D3D() 和 Camera::capture3D() 方法会在相机完成原始图像采集后的某个时刻返回。 Frame::pointCloud() 句柄可立即使用。但是,实际的点云数据只有在 GPU 处理完成后才可用。任何对数据复制函数(见下文)的调用都会被阻塞,并等待处理完成后再继续执行请求的复制操作。

请查看 点云捕获过程 了解更多细节。

获取属性 Frame.PointCloud 不会从 GPU 内存执行任何复制。

备注

Camera.Capture2D3D() 和 Camera.Capture3D() 方法会在相机完成原始图像采集后的某个时刻返回。 Frame.PointCloud 句柄可立即使用。然而,实际的点云数据只有在 GPU 处理完成后才可用。任何对数据复制方法(见下文)的调用都会被阻塞,并等待处理完成后再继续执行请求的复制操作。

请查看 点云捕获过程 了解更多细节。

功能 zivid.frame.point_cloud() 不从 GPU 内存执行任何复制。

备注

zivid.camera.capture_2d_3d() 和 zivid.camera.capture_3d() 方法会在相机完成原始图像采集后的某个时刻返回。 zivid.frame.point_cloud() 句柄可立即使用。但是,实际的点云数据只有在 GPU 处理完成后才可用。任何对数据复制函数(见下文)的调用都会被阻塞,并等待处理完成后再继续执行请求的复制操作。

请查看 点云捕获过程 了解更多细节。

无序点云

可以将有序点云转换为无序点云。转换过程中,所有 NaN 值都会被移除,点云会被展平为一维数组。

跳转到源码

source

const auto unorganizedPointCloud = frame.pointCloud().toUnorganizedPointCloud();
跳转到源码

源码

unorganized_point_cloud = frame.point_cloud().to_unorganized_point_cloud()

组合多个无序点云

无序点云可以通过附加的无序点云进行扩展。

跳转到源码

源码

stitchedPointCloud.extend(currentPointCloud.transform(transformationMatrixZivid));
跳转到源码

源码

stitched_point_cloud.extend(current_point_cloud.transform(transformation_matrix))

将数据从 GPU 复制到 CPU 内存

您现在可以根据需要有选择地复制数据。下面的列表列出了输出数据格式以及如何从 GPU 复制它们。

小技巧

如果您想直接在 GPU 上处理数据而不复制到 CPU,请参阅 GPU 访问教程。

这些 API 中的大多数也适用于无序点云。

返回类型

复制功能

单像素数据量

总数据量

Zivid::Array2D<Zivid::PointXYZ>

PointCloud::copyPointsXYZ() 或 PointCloud::copyData<Zivid::PointXYZ>()

12 bytes

28 MB

Zivid::Array2D<Zivid::PointXYZW>

PointCloud::copyPointsXYZW() 或 PointCloud::copyData<Zivid::PointXYZW>()

16 bytes

37 MB

Zivid::Array2D<Zivid::PointZ>

PointCloud::copyPointsZ() 或 PointCloud::copyData<Zivid::PointZ>()

4 bytes

9 MB

Zivid::Array2D<Zivid::ColorRGBA>

PointCloud::copyColorsRGBA() 或 PointCloud::copyData<Zivid::ColorRGBA>()

4 bytes

9 MB

Zivid::Array2D<Zivid::SNR>

PointCloud::copySNRs() 或 PointCloud::copyData<Zivid::SNR>()

4 bytes

9 MB

Zivid::Array2D<Zivid::PointXYZColorRGBA>

PointCloud::copyData<PointXYZColorRGBA>()

16 bytes

37 MB

Zivid::Array2D<Zivid::PointXYZColorBGRA>

PointCloud::copyPointsXYZColorsBGRA() 或 PointCloud::copyData<PointXYZColorBGRA>()

16 bytes

37 MB

Zivid::Image<Zivid::ColorRGBA>

PointCloud::copyImageRGBA()

4 bytes

9 MB

Zivid::Image<Zivid::ColorBGRA>

PointCloud::copyImageBGRA()

4 bytes

9 MB

Zivid::Image<Zivid::ColorsRGB>

PointCloud::copyImagesRGB()

4 bytes

9 MB

返回类型

复制方法

单像素数据量

总数据量

float[height,width,3]

PointCloud.CopyPointsXYZ()

12 bytes

28 MB

float[height,width,4]

PointCloud.CopyPointsXYZW()

16 bytes

37 MB

float[height,width,1]

PointCloud.CopyPointsZ()

4 bytes

9 MB

byte[height,width,4]

PointCloud.CopyColorsRGBA()

4 bytes

9 MB

float[height,width]

PointCloud.CopySNRs()

4 bytes

9 MB

Zivid.NET.PointXYZColorRGBA[height, width]

PointCloud.CopyPointsXYZColorsRGBA()

16 bytes

37 MB

Zivid.NET.PointXYZColorBGRA[height, width]

PointCloud.CopyPointsXYZColorsBGRA()

16 bytes

37 MB

Zivid.NET.ImageRGBA

PointCloud.CopyImageRGBA()

4 bytes

9 MB

Zivid.NET.ImageBGRA

PointCloud.CopyImageBGRA()

4 bytes

9 MB

Zivid.NET.ImageSRGB

PointCloud.CopyImageSRGB()

4 bytes

9 MB

返回类型

复制功能

单像素数据量

总数据量

numpy.ndarray([height,width,3], dtype=float32)

PointCloud.copy_data("xyz")

12 bytes

28 MB

numpy.ndarray([height,width,3], dtype=float32)

PointCloud.copy_data("xyzw")

16 bytes

37 MB

numpy.ndarray([height,width], dtype=float32)

PointCloud.copy_data("z")

4 bytes

9 MB

numpy.ndarray([height,width,4], dtype=uint8)

PointCloud.copy_data("rgba")

4 bytes

9 MB

numpy.ndarray([height,width,4], dtype=uint8)

PointCloud.copy_data("bgra")

4 bytes

9 MB

numpy.ndarray([height,width,4], dtype=uint8)

PointCloud.copy_data("srgb")

4 bytes

9 MB

numpy.ndarray([height,width], dtype=float32)

PointCloud.copy_data("snr")

4 bytes

9 MB

numpy.ndarray([height,width], dtype=[('x', '<f4'), ('y', '<f4'), ('z', '<f4'), ('r', 'u1'), ('g', 'u1'), ('b', 'u1'), ('a', 'u1')])

PointCloud.copy_data("xyzrgba")

16 bytes

37 MB

以下是如何复制数据的示例。

跳转到源码

源码

const auto data = pointCloud.copyData<Zivid::PointXYZColorRGBA_SRGB>();
跳转到源码

源码

var pointCloudData = pointCloud.CopyPointsXYZColorsRGBA_SRGB();
跳转到源码

源码

xyz = point_cloud.copy_data("xyz")
rgba = point_cloud.copy_data("rgba_srgb")

内存分配选项

在内存分配方面,复制数据有两种方式:

  • Zivid SDK 可以分配内存缓冲区并将数据复制到其中。

  • 用户可以将指针传递给预分配的内存缓冲区,Zivid SDK 会将数据复制到预分配的内存缓冲区。

我们将展示两个使用了 OpenCV 的内存分配的示例。

将选定数据从 GPU 复制到 CPU 内存(Zivid 分配)

如果您只关心例如点云的 RGB 颜色数据,您可以仅将该数据复制到 CPU 内存。

跳转到源码

源码

std::cout << "Capturing frame" << std::endl;
frame = camera.capture2D3D(settings);

const auto frame2D = frame.frame2D();
if(!frame2D.has_value())
{
    throw std::runtime_error("Captured frame does not contain a 2D image.");
}
std::cout << "Copying colors with Zivid API from GPU to CPU" << std::endl;
auto colors = frame2D->imageBGRA_SRGB();

std::cout << "Casting the data pointer as a void*, since this is what the OpenCV matrix constructor requires."
          << std::endl;
auto *dataPtrZividAllocated = const_cast<void *>(static_cast<const void *>(colors.data()));

std::cout << "Wrapping this block of data in an OpenCV matrix. This is possible since the layout of \n"
          << "Zivid::ColorBGRA_SRGB exactly matches the layout of CV_8UC4. No copying occurs in this step."
          << std::endl;
const cv::Mat bgraZividAllocated(colors.height(), colors.width(), CV_8UC4, dataPtrZividAllocated);

std::cout << "Displaying image" << std::endl;
cv::imshow("BGRA image Zivid Allocated", bgraZividAllocated);
cv::waitKey(CI_WAITKEY_TIMEOUT_IN_MS);

将选定数据从 GPU 复制到 CPU 内存(用户分配)

在上面的示例中,数据的所有权由返回的 Zivid::Array2D<> 对象拥有。或者,您也可以提供一个预先分配的内存缓冲区给 Zivid::PointCloud::copyData(dataPtr)。 dataPtr 的类型定义了要复制的数据(PointXYZ、ColorRGBA 等)。

现在让我们看一下与上面完全相同的用例。但是,这一次我们允许 OpenCV 来分配必要的存储空间。然后我们让 Zivid API 将数据直接从 GPU 复制到这个内存位置。

跳转到源码

源码

std::cout << "Allocating the necessary storage with OpenCV API based on resolution info before any capturing"
          << std::endl;
auto bgraUserAllocated = cv::Mat(resolution.height(), resolution.width(), CV_8UC4);

std::cout << "Capturing frame" << std::endl;
auto frame = camera.capture2D3D(settings);
auto pointCloud = frame.pointCloud();

std::cout << "Copying data with Zivid API from the GPU into the memory location allocated by OpenCV"
          << std::endl;
pointCloud.copyData(&(*bgraUserAllocated.begin<Zivid::ColorBGRA_SRGB>()));

std::cout << "Displaying image" << std::endl;
cv::imshow("BGRA image User Allocated", bgraUserAllocated);
cv::waitKey(CI_WAITKEY_TIMEOUT_IN_MS);

参见

复制速度 对将每种数据类型从 GPU 复制到系统内存所需的时间进行了基准测试。

转换

你可能想要 转换 点云,将其原点从相机坐标系转换到机器人基坐标系,或者比如, 通过将点云从mm转换为m来缩放点云。

跳转到源码

源码

pointCloud.transform(baseToCameraTransform);
跳转到源码

源码

pointCloud.Transform(transformBaseToCamera);
跳转到源码

源码

point_cloud.transform(base_to_camera_transform)

转换可以就地完成:

  • PointCloud::transform()

  • UnorganizedPointCloud::transform()

或者通过创建一个新实例:

  • PointCloud::transformed()

  • UnorganizedPointCloud::transformed()

以下示例展示了如何创建一个应用了变换的 UnorganizedPointCloud 新实例。请注意,在此示例中并不需要创建新实例,因为未变换的点云在变换之后不会被使用。

跳转到源码

source

const auto transformedUnorganizedPointCloud = unorganizedPointCloud.transformed(transformationMatrix);
跳转到源码

源码

transformed_unorganized_point_cloud = unorganized_point_cloud.transformed(transformation_matrix)

甚至就地 API 也会返回转换后的点云,因此您可以直接使用它,如下例所示。

跳转到源码

源码

stitchedPointCloud.extend(currentPointCloud.transform(transformationMatrixZivid));
跳转到源码

源码

stitched_point_cloud.extend(current_point_cloud.transform(transformation_matrix))

如果感兴趣的对象是经过 ROI 裁剪的点云的子集,您可以通过应用二值掩膜进一步细化它。典型的工作流程是捕获一张 2D 图像,运行分割算法识别对象,然后将得到的掩膜应用到 3D 点云上。这会使分割区域之外的所有点失效。

有关 API 用法和代码示例,请参阅 点云教程中的掩膜部分。

掩膜

You can mask the point cloud to keep only a specific region of interest. A typical use case is to capture a 2D image, run it through a segmentation algorithm to produce a binary mask, and apply it to the 3D point cloud. Non-zero values in the mask invalidate the corresponding points (set to NaN), while zero values preserve them. The mask can be applied using PointCloud::mask() (in-place) or PointCloud::masked() (returns a new copy), on both PointCloud and Frame.

跳转到源码

源码

// Create a ones-filled mask
Zivid::Mask mask(resolution);

// Calculate rectangle bounds
const int height = static_cast<int>(resolution.height());
const int width = static_cast<int>(resolution.width());
const int heightMin = (height - pixelsToDisplay) / 2;
const int heightMax = (height + pixelsToDisplay) / 2;
const int widthMin = (width - pixelsToDisplay) / 2;
const int widthMax = (width + pixelsToDisplay) / 2;

// Create OpenCV Mat wrapper for the mask data
cv::Mat maskMat(height, width, CV_8UC1, mask.data());

// Draw filled rectangle on the mask to unmask the central region
cv::rectangle(
    maskMat, cv::Point(widthMin, heightMin), cv::Point(widthMax, heightMax), cv::Scalar(0), cv::FILLED);

return mask;
auto maskedPointCloud = pointCloud.masked(mask);
跳转到源码

源码

// Create a ones-filled mask
var mask = new Zivid.NET.Mask(resolution);

// Calculate rectangle bounds
int height = (int)resolution.Height;
int width = (int)resolution.Width;
int heightMin = (height - pixelsToDisplay) / 2;
int heightMax = (height + pixelsToDisplay) / 2;
int widthMin = (width - pixelsToDisplay) / 2;
int widthMax = (width + pixelsToDisplay) / 2;

// Set pixels inside the rectangle to zero
for (int y = heightMin; y < heightMax; ++y)
{
    for (int x = widthMin; x < widthMax; ++x)
    {
        mask[x, y] = 0;
    }
}

return mask;
    var maskedPointCloud = pointCloud.Masked(mask);
跳转到源码

源码

pixels_to_display = 300
print(f"Generating binary mask of central {pixels_to_display} x {pixels_to_display} pixels")
height = frame.point_cloud().height
width = frame.point_cloud().width
mask = np.ones((height, width), bool)

h_min = (height - pixels_to_display) // 2
h_max = (height + pixels_to_display) // 2
w_min = (width - pixels_to_display) // 2
w_max = (width + pixels_to_display) // 2
mask[h_min:h_max, w_min:w_max] = 0
print("Masking point cloud")
point_cloud.mask(mask)

掩膜也可以由第三方数据构建,例如 OpenCV 图像。

跳转到源码

源码

// Create a circular mask in OpenCV
const int centerX = static_cast<int>(resolution.width()) / 2;
const int centerY = static_cast<int>(resolution.height()) / 2;
const int radius = pixelsToDisplay;
cv::circle(opencvMask, cv::Point(centerX, centerY), radius, cv::Scalar(0), cv::FILLED);

// Convert OpenCV mask to Zivid::Mask
const Zivid::Mask circularMask(resolution, opencvMask.datastart, opencvMask.dataend);
跳转到源码

源码

opencv_mask = np.ones((height, width), dtype=np.uint8)
center_x = width // 2
center_y = height // 2
radius = pixels_to_display
cv2.circle(opencv_mask, (center_x, center_y), radius, 0, thickness=cv2.FILLED)

point_cloud.mask(opencv_mask.astype(bool))

备注

掩膜支持隐式重采样:如果掩膜的分辨率与点云的分辨率相差一个整数倍(2、3 或 4),系统会自动对其进行重采样。这在从与 3D 捕获分辨率不同的 2D 捕获创建掩膜时非常有用。

下采样

有时您可能不需要相机输出的高空间分辨率(高空间分辨率意味着更多的细节和更短的点之间的距离)点云。那么您对点云进行 下采样 。

备注

Sampling(采样) - 3D 描述了一种基于硬件的子/下采样方法,它可以降低捕获过程中点云的分辨率,同时也减少采集和捕获时间。

备注

UnorganizedPointCloud 不支持下采样,但它支持体素下采样,请参阅 体素下采样。

可以就地进行下采样,从而修改当前的点云。

跳转到源码

源码

pointCloud.downsample(Zivid::PointCloud::Downsampling::by2x2);
跳转到源码

源码

pointCloud.Downsample(Zivid.NET.PointCloud.Downsampling.By2x2);
跳转到源码

源码

point_cloud.downsample(zivid.PointCloud.Downsampling.by2x2)

也可以将下采样后的点云作为一个新的点云实例,它不会改变现有的点云。

跳转到源码

源码

auto downsampledPointCloud = pointCloud.downsampled(Zivid::PointCloud::Downsampling::by2x2);
跳转到源码

源码

var downsampledPointCloud = pointCloud.Downsampled(Zivid.NET.PointCloud.Downsampling.By2x2);
跳转到源码

源码

downsampled_point_cloud = point_cloud.downsampled(zivid.PointCloud.Downsampling.by2x2)

Zivid SDK 支持以下下采样率: by2x2, by3x3, 和 by4x4, 可以进行多次下采样。

体素下采样

UnorganizedPointCloud 支持体素下采样。该 API 接受两个参数:

  1. voxelSize - 体素的大小(以毫米为单位)。

  2. minPointsPerVoxel - 每个体素保留的最小点数。

体素下采样将三维空间细分为具有给定大小的立方体素网格。如果给定体素包含的点数等于或超过给定限制,则所有这些源点都将被替换为具有以下属性的单个点:

  • 位置(XYZ)是源点位置的 SNR 加权平均值,即高置信度的源点对最终位置的影响将大于低置信度的源点。

  • 颜色(RGBA)是源点颜色的平均值。

  • 信噪比 (SNR) 是源点 SNR 值的 sqrt(sum(SNR^2)),即新点的 SNR 将随着用于计算其位置的源点的数量和置信度的增加而增加。

使用 minPointsPerVoxel > 1 特别适用于去除由不同角度捕获的点云组合而成的无序点云中的噪点和伪影。这是因为某个伪影很可能只出现在其中一次捕获中,而 minPointsPerVoxel 可用于仅填充两次捕获 "一致" 的体素。

跳转到源码

source

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

源码

final_point_cloud = stitched_point_cloud.voxel_downsampled(0.5, 1)

法线

一些应用需要计算点云的 法线 数据。

跳转到源码

源码

std::cout << "Computing normals and copying them to CPU memory" << std::endl;
const auto normals = pointCloud.copyData<Zivid::NormalXYZ>();
跳转到源码

源码

Console.WriteLine("Computing normals and copying them to CPU memory");
var normals = pointCloud.CopyNormalsXYZ();
跳转到源码

源码

print("Computing normals and copying them to CPU memory")
normals = point_cloud.copy_data("normals")

法线 API 将计算点云中每个点的法线,并将法线从 GPU 内存复制到 CPU 内存。其结果是一个法向量矩阵,每条法线对应输入点云的每个点。法线的大小和输入点云的大小相等。

可视化

您可以通过帧(frame)可视化点云。

跳转到源码

源码

std::cout << "Setting up visualization" << std::endl;
Zivid::Visualization::Visualizer visualizer;

std::cout << "Visualizing point cloud" << std::endl;
visualizer.showMaximized();
visualizer.show(frame);
visualizer.resetToFit();

std::cout << "Running visualizer. Blocking until window closes." << std::endl;
visualizer.run();
跳转到源码

源码

Console.WriteLine("Setting up visualization");
using (var visualizer = new Zivid.NET.Visualization.Visualizer())
{
    Console.WriteLine("Visualizing point cloud");
    visualizer.Show(frame);
    visualizer.ShowMaximized();
    visualizer.ResetToFit();

    Console.WriteLine("Running visualizer. Blocking until window closes.");
    visualizer.Run();
}
跳转到源码

source

print("Visualizing point cloud")
with zivid.visualization.Visualizer() as visualizer:
    visualizer.set_window_title("Zivid Point Cloud Visualizer")
    visualizer.colors_enabled = True
    visualizer.axis_indicator_enabled = True
    visualizer.show(frame)
    visualizer.reset_to_fit()
    visualizer.run()

您也可以从点云对象来可视化点云。

跳转到源码

源码

std::cout << "Getting point cloud from frame" << std::endl;
auto pointCloud = frame.pointCloud();

std::cout << "Setting up visualization" << std::endl;
Zivid::Visualization::Visualizer visualizer;

std::cout << "Visualizing point cloud" << std::endl;
visualizer.showMaximized();
visualizer.show(pointCloud);
visualizer.resetToFit();

std::cout << "Running visualizer. Blocking until window closes." << std::endl;
visualizer.run();
跳转到源码

源码

Console.WriteLine("Getting point cloud from frame");
var pointCloud = frame.PointCloud;

Console.WriteLine("Setting up visualization");
var visualizer = new Zivid.NET.Visualization.Visualizer();

Console.WriteLine("Visualizing point cloud");
visualizer.Show(pointCloud);
visualizer.ShowMaximized();
visualizer.ResetToFit();

Console.WriteLine("Running visualizer. Blocking until window closes.");
visualizer.Run();
跳转到源码

source

    point_cloud = frame.point_cloud()

with zivid.visualization.Visualizer() as visualizer:
    visualizer.set_window_title("Zivid Point Cloud Visualizer")
    visualizer.colors_enabled = True
    visualizer.axis_indicator_enabled = True
    visualizer.show(point_cloud)
    visualizer.reset_to_fit()
    visualizer.run()

如需了解更多信息,请查看 可视化教程,里面包含了如何使用第三方库实现点云、彩色图像、深度图和法线的可视化。

结论

本教程展示了如何使用 Zivid SDK 来提取、操作、转换和可视化点云。

有关相应的 API,请参阅 基础应用。

版本历史

SDK

变更

2.18.0

Added Zivid::Mask class and PointCloud::mask() / PointCloud::masked() methods on PointCloud and Frame for post-capture binary masking.

2.16.0

Added support for UnorganizedPointCloud. PointCloud::transformed() is added as a function (also available as UnorganizedPointCloud::transformed()).

2.11.0

添加了对 SRGB 色彩空间(SRGB color space)的支持。

2.10.0

Monochrome Capture(单色捕获) 引入了比 下采样 更快的替代方案。