拼接
重要
Download the example stitching Sample Data(示例数据) dataset and reference files to follow along or run the samples.
介绍
本文是关于如何拼接 Zivid 的无序点云的深入教程。
为了理解真正的问题,我们先来简单了解一下这个应用。与任何相机或人眼一样,Zivid 相机无法透视或绕过物体,它一次只能观察一侧。因此,要获得物体的完整 3D 模型,必须通过旋转物体或重新定位相机来从多个视角捕获它。
问题
Zivid SDK 返回的点云以 相机坐标系 表示,其中原点位于相机内部。从多个视角捕获时,每个点云都相对于捕获时相机的坐标系进行定义。如果只是简单地组合这些点云而不进行坐标系变换,它们将无法在空间上对齐,在 3D 空间中会显得脱节且不连贯。换句话说,这些点云并不共享与被扫描对象关联的通用坐标系。
解决方案
要将多个点云合并为对象的单个、空间对齐的 3D 表征,您需要:
将所有点云转换为附加在被扫描物体上的通用坐标系。
优化对齐以补偿位姿估计中的任何噪音或小错误。
从每个相机坐标系到通用坐标系的变换可能来自多种来源:机器人运动学、运动跟踪系统或手动测量。然而,这些变换很少是完美的,通常会引入微小的对齐误差。要纠正这些误差,您可以使用 Experimental::Toolbox::localPointCloudRegistration()。此 API 工具通过计算一个能最佳地将一个点云配准到另一个点云的变换矩阵,来优化两个重叠点云之间的对齐。
在开始拼接点云之前,务必确保捕获的点云具有足够多的重叠特征点。此外,我们还必须丢弃可能导致拼接错误的数据,例如距离相机太远的点,或不属于我们正在拼接的物体/场景的点。我们建议在拼接前使用 Zivid 的 Region of Interest(感兴趣区域)。对于相机静止的捕获,可以定义一次感兴趣区域,并将其用于所有捕获。而使用机械臂围绕物体移动相机时,则可能需要为每个点云配置不同的感兴趣区域。或者,您也可以在机器人基坐标系中定义一个单一的工作空间 ROI 框,并在将每个点云变换到基坐标系后 将其作为后处理应用。这样,无论视角如何,同一个 ROI 体积都会对每次捕获进行过滤。
备注
Zivid 样本数据中所有需要拼接的点云均已经过 ROI 过滤。
无需预对齐即可拼接点云
本节演示了在不进行预对齐的情况下拼接点云。关键要求是,物体与相机之间从一个点云到另一个点云的相对运动足够小,以确保捕获之间有明显的重叠。在此示例中,物体保持静止,而相机在每次捕获之间略微移动,并使用 Experimental::Toolbox::localPointCloudRegistration() 对点云进行对齐。我们首先从一个简单示例开始,该示例在没有先验位姿信息的情况下拼接两个点云,然后介绍一个更高级的案例,使用旋转台扫描整个物体。
拼接两个点云
这个简单示例演示了如何在没有预对齐的情况下拼接两个点云。要使用此方法进行拼接,我们需要:
加载两个 ZDF 文件。
将点云转换为无序格式。
应用体素下采样。
估计对齐的变换。
将点云拼接到一起。
对最终的拼接结果进行下采样。
下面演示了如何加载 ZDF 文件,并使用 PointCloud::toUnorganizedPointCloud() 将点云转换为无序格式。这些无序点云使用 UnorganizedPointCloud::extend() 合并为一个 UnorganizedPointCloud 对象,并显示出来。
// Ensure the dataset is extracted to the correct location depending on the operating system:
// - Windows: %ProgramData%/Zivid/StitchingPointClouds/
// - Linux: /usr/share/Zivid/data/StitchingPointClouds/
// StitchingPointClouds/
// └── BlueObject/
std::cout << "Reading point clouds from ZDF files" << std::endl;
const auto directory = std::filesystem::path(ZIVID_SAMPLE_DATA_DIR) / "StitchingPointClouds" / "BlueObject";
if(!std::filesystem::exists(directory))
{
std::ostringstream oss;
oss << "Missing dataset folders.\n"
<< "Make sure 'StitchingPointClouds/BlueObject/' exist at " << ZIVID_SAMPLE_DATA_DIR << ".\n\n"
<< "You can download the dataset (StitchingPointClouds.zip) from:\n"
<< "https://support.zivid.com/en/latest/api-reference/samples/sample-data.html";
throw std::runtime_error(oss.str());
}
const auto frame1 = Zivid::Frame((directory / "BlueObject.zdf").string());
const auto frame2 = Zivid::Frame((directory / "BlueObjectSlightlyMoved.zdf").string());
std::cout << "Converting organized point clouds to unorganized point clouds and voxel downsampling"
<< std::endl;
const auto unorganizedPointCloud1 = frame1.pointCloud().toUnorganizedPointCloud();
const auto unorganizedPointCloud2 = frame2.pointCloud().toUnorganizedPointCloud();
std::cout << "Displaying point clouds before stitching" << std::endl;
Zivid::UnorganizedPointCloud unorganizedNotStitchedPointCloud;
unorganizedNotStitchedPointCloud.extend(unorganizedPointCloud1);
unorganizedNotStitchedPointCloud.extend(unorganizedPointCloud2);
visualizePointCloud(unorganizedNotStitchedPointCloud);
// Ensure the dataset is extracted to the correct location depending on the operating system:
// - Windows: %ProgramData%/Zivid/StitchingPointClouds/
// - Linux: /usr/share/Zivid/data/StitchingPointClouds/
// StitchingPointClouds/
// └── BlueObject/
var sampleDataDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "/Zivid";
var directory = Path.Combine(sampleDataDir, "StitchingPointClouds", "BlueObject");
Console.WriteLine("Reading point clouds from ZDF files");
if (!Directory.Exists(directory))
{
Console.WriteLine("Missing dataset folders.");
Console.WriteLine("Make sure 'StitchingPointClouds/BlueObject/' exists at " + sampleDataDir);
Console.WriteLine("You can download the dataset (StitchingPointClouds.zip) from:");
Console.WriteLine("https://support.zivid.com/en/latest/api-reference/samples/sample-data.html");
return 1;
}
var frame1 = new Zivid.NET.Frame(Path.Combine(directory, "BlueObject.zdf"));
var frame2 = new Zivid.NET.Frame(Path.Combine(directory, "BlueObjectSlightlyMoved.zdf"));
Console.WriteLine("Converting organized point clouds to unorganized point clouds and voxel downsampling");
var unorganizedPointCloud1 = frame1.PointCloud.ToUnorganizedPointCloud();
var unorganizedPointCloud2 = frame2.PointCloud.ToUnorganizedPointCloud();
Console.WriteLine("Displaying point clouds before stitching");
var unorganizedNotStitchedPointCloud = new Zivid.NET.UnorganizedPointCloud();
unorganizedNotStitchedPointCloud.Extend(unorganizedPointCloud1);
unorganizedNotStitchedPointCloud.Extend(unorganizedPointCloud2);
VisualizePointCloud(unorganizedNotStitchedPointCloud);
# Ensure the dataset is extracted to the correct location depending on the operating system:
# • Windows: %ProgramData%\\Zivid\\StitchingPointClouds\\
# • Linux: /usr/share/Zivid/data/StitchingPointClouds/
# The folder must contain:
# StitchingPointClouds/
# └── BlueObject/
print("Reading point clouds from files")
directory = get_sample_data_path() / "StitchingPointClouds" / "BlueObject"
if not directory.exists() or not directory.exists():
raise FileNotFoundError(
f"Missing dataset folders.\n"
f"Make sure 'StitchingPointClouds/BlueObject' exist at {get_sample_data_path()}.\n\n"
f"You can download the dataset (StitchingPointClouds.zip) from:\n"
f"https://support.zivid.com/en/latest/api-reference/samples/sample-data.html"
)
frame_1 = zivid.Frame(directory / "BlueObject.zdf")
frame_2 = zivid.Frame(directory / "BlueObjectSlightlyMoved.zdf")
print("Converting organized point clouds to unorganized point clouds and voxel downsampling")
unorganized_point_cloud_1 = frame_1.point_cloud().to_unorganized_point_cloud()
unorganized_point_cloud_2 = frame_2.point_cloud().to_unorganized_point_cloud()
print("Displaying point clouds before stitching")
unorganized_not_stitched_point_cloud = zivid.UnorganizedPointCloud()
unorganized_not_stitched_point_cloud.extend(unorganized_point_cloud_1)
unorganized_not_stitched_point_cloud.extend(unorganized_point_cloud_2)
display_pointcloud(unorganized_not_stitched_point_cloud)
应用体素下采样以减少点数,从而加快配准速度。然后,我们使用 Experimental::Toolbox::localPointCloudRegistration() 计算两个点云之间的变换矩阵。使用得到的变换对第二个点云进行变换,并将其添加到最终点云中(包含第一个点云的 UnorganizedPointCloud 对象)。为了完成拼接,我们对拼接后的点云应用体素下采样,并显示结果。
std::cout << "Estimating transformation between point clouds" << std::endl;
const auto unorganizedPointCloud1LPCR = unorganizedPointCloud1.voxelDownsampled(1.0, 3);
const auto unorganizedPointCloud2LPCR = unorganizedPointCloud2.voxelDownsampled(1.0, 3);
const auto registrationParams = Zivid::Experimental::LocalPointCloudRegistrationParameters{};
const auto localPointCloudRegistrationResult = Zivid::Experimental::Toolbox::localPointCloudRegistration(
unorganizedPointCloud1LPCR, unorganizedPointCloud2LPCR, registrationParams);
if(!localPointCloudRegistrationResult.converged())
{
throw std::runtime_error("Registration did not converge...");
}
const auto pointCloud1ToPointCloud2Transform = localPointCloudRegistrationResult.transform();
const auto unorganizedPointCloud2Transformed =
unorganizedPointCloud2.transformed(pointCloud1ToPointCloud2Transform.toMatrix());
std::cout << "Stitching and displaying painted point clouds to evaluate stitching quality" << std::endl;
Zivid::UnorganizedPointCloud finalPointCloud;
finalPointCloud.extend(unorganizedPointCloud1);
finalPointCloud.extend(unorganizedPointCloud2Transformed);
Zivid::UnorganizedPointCloud paintedFinalPointCloud;
paintedFinalPointCloud.extend(unorganizedPointCloud1.paintedUniformColor(Zivid::ColorRGBA{ 255, 0, 0, 255 }));
paintedFinalPointCloud.extend(
unorganizedPointCloud2Transformed.paintedUniformColor(Zivid::ColorRGBA{ 0, 255, 0, 255 }));
visualizePointCloud(paintedFinalPointCloud);
std::cout << "Voxel-downsampling the stitched point cloud" << std::endl;
finalPointCloud = finalPointCloud.voxelDownsampled(2.0, 1);
std::cout << "Visualize the overlapped point clouds" << std::endl;
visualizePointCloud(finalPointCloud);
Console.WriteLine("Estimating transformation between point clouds");
var unorganizedPointCloud1LPCR = unorganizedPointCloud1.VoxelDownsampled(1.0f, 3);
var unorganizedPointCloud2LPCR = unorganizedPointCloud2.VoxelDownsampled(1.0f, 3);
var registrationParams = new Zivid.NET.Experimental.LocalPointCloudRegistrationParameters();
var identityPose = new Pose(Zivid.NET.Matrix4x4.Identity());
var localPointCloudRegistrationResult = PointCloudRegistration.LocalPointCloudRegistration(
unorganizedPointCloud1LPCR,
unorganizedPointCloud2LPCR,
registrationParams,
identityPose);
if (!localPointCloudRegistrationResult.Converged)
{
throw new Exception("Registration did not converge...");
}
var pointCloud1ToPointCloud2Transform = localPointCloudRegistrationResult.Transform;
var unorganizedPointCloud2Transformed =
unorganizedPointCloud2.Transform(pointCloud1ToPointCloud2Transform.ToMatrix());
Console.WriteLine("Stitching and displaying painted point clouds to evaluate stitching quality");
var finalPointCloud = new Zivid.NET.UnorganizedPointCloud();
finalPointCloud.Extend(unorganizedPointCloud1);
finalPointCloud.Extend(unorganizedPointCloud2Transformed);
var paintedFinalPointCloud = new Zivid.NET.UnorganizedPointCloud();
paintedFinalPointCloud.Extend(unorganizedPointCloud1.PaintedUniformColor(new Zivid.NET.ColorRGBA { r = 255, g = 0, b = 0, a = 255 }));
paintedFinalPointCloud.Extend(unorganizedPointCloud2Transformed.PaintedUniformColor(new Zivid.NET.ColorRGBA { r = 0, g = 255, b = 0, a = 255 }));
VisualizePointCloud(paintedFinalPointCloud);
Console.WriteLine("Voxel-downsampling the stitched point cloud");
finalPointCloud = finalPointCloud.VoxelDownsampled(2.0f, 1);
Console.WriteLine("Visualize the overlapped point clouds");
VisualizePointCloud(finalPointCloud);
print("Estimating transformation between point clouds")
unorganized_point_cloud_1_lpcr = unorganized_point_cloud_1.voxel_downsampled(voxel_size=1.0, min_points_per_voxel=3)
unorganized_point_cloud_2_lpcr = unorganized_point_cloud_2.voxel_downsampled(voxel_size=1.0, min_points_per_voxel=3)
registration_params = LocalPointCloudRegistrationParameters()
local_point_cloud_registration_result = local_point_cloud_registration(
target=unorganized_point_cloud_1_lpcr, source=unorganized_point_cloud_2_lpcr, parameters=registration_params
)
assert local_point_cloud_registration_result.converged(), "Registration did not converge..."
point_cloud_1_to_point_cloud_2_transform = local_point_cloud_registration_result.transform()
unorganized_point_cloud_2_transformed = unorganized_point_cloud_2.transformed(
point_cloud_1_to_point_cloud_2_transform.to_matrix()
)
print("Stitching and displaying painted point clouds to evaluate stitching quality")
final_point_cloud = zivid.UnorganizedPointCloud()
final_point_cloud.extend(unorganized_point_cloud_1)
final_point_cloud.extend(unorganized_point_cloud_2_transformed)
painted_final_point_cloud = zivid.UnorganizedPointCloud()
painted_final_point_cloud.extend(unorganized_point_cloud_1.painted_uniform_color([255, 0, 0, 255]))
painted_final_point_cloud.extend(unorganized_point_cloud_2_transformed.painted_uniform_color([0, 255, 0, 255]))
display_pointcloud(painted_final_point_cloud)
print("Voxel-downsampling the stitched point cloud")
final_point_cloud = final_point_cloud.voxel_downsampled(voxel_size=2.0, min_points_per_voxel=1)
display_pointcloud(final_point_cloud)
可以在拼接前为各个点云赋予统一的颜色,以直观地可视化对齐质量。
使用旋转台进行拼接
此示例演示了如何拼接放置在旋转台上的物体的点云。要使用此方法进行拼接,我们需要:
连接到 Zivid 相机。
加载使用了感兴趣区域 (ROI) 的相机设置。
当物体旋转时捕获多个点云。
将每次捕获转换为无序的点云并进行下采样。
将新的 frame 注册到不断增长的模型中。
将不断增长的模型拼接到新的 frame 中。
进行下采样、显示结果并将最终结果导出为多边形文件格式 (.ply)。
备注
在此示例中,旋转台在约 25 秒内完成 360 度旋转。由于每个周期耗时超过 250 毫秒(包括捕获、传输、处理和拼接),我们在每次捕获之间添加了 1 秒的延迟。这样一来,我们便可以从物体周围均匀分布的角度进行约 20 次捕获。
We begin by connecting to the camera and loading a capture setting with a defined ROI. The ROI is defined to include the object on the rotary table. It's ideal to avoid capturing points of the table itself, but if the object is large enough, including some table points is generally acceptable.
std::cout << "Connecting to camera" << std::endl;
auto camera = app.connectCamera();
std::cout << "Loading settings from file: " << settingsPath << std::endl;
const auto settings = Zivid::Settings(settingsPath);
接下来,我们需要捕获旋转物体的点云,并应用局部点云配准技术进行对齐。每个新捕捉到的点云都将成为参考 frame,之前拼接的模型将转换到该 frame 中。
为什么要转换拼接模型而不是最后一次捕获呢?因为将最后一次捕获的模型转换为固定的世界坐标系需要应用所有之前的转换,从而导致误差累积。通过始终将现有模型转换为最新的 frame,误差可以保持在局部范围内,更易于控制。
Zivid::UnorganizedPointCloud unorganizedStitchedPointCloud;
auto registrationParams = Zivid::Experimental::LocalPointCloudRegistrationParameters{};
auto previousToCurrentPointCloudTransform = Zivid::Matrix4x4::identity();
for(int numberOfCaptures = 0; numberOfCaptures < 20; ++numberOfCaptures)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
auto frame = camera.capture2D3D(settings);
const auto unorganizedPointCloud = frame.pointCloud().toUnorganizedPointCloud().voxelDownsampled(1.0, 2);
if(numberOfCaptures != 0)
{
if(unorganizedStitchedPointCloud.size() < 4 || unorganizedPointCloud.size() < 4)
{
std::cout << "Not enough points for registration, skipping stitching..." << std::endl;
continue;
}
const auto registrationResult = Zivid::Experimental::Toolbox::localPointCloudRegistration(
unorganizedStitchedPointCloud,
unorganizedPointCloud,
registrationParams,
previousToCurrentPointCloudTransform);
if(!registrationResult.converged())
{
std::cout << "Registration did not converge..." << std::endl;
continue;
}
previousToCurrentPointCloudTransform = registrationResult.transform().toMatrix();
unorganizedStitchedPointCloud.transform(previousToCurrentPointCloudTransform.inverse());
}
unorganizedStitchedPointCloud.extend(unorganizedPointCloud);
std::cout << "Captures done: " << numberOfCaptures << std::endl;
}
std::cout << "Voxel-downsampling the stitched point cloud" << std::endl;
unorganizedStitchedPointCloud = unorganizedStitchedPointCloud.voxelDownsampled(0.75, 2);
visualizePointCloud(unorganizedStitchedPointCloud);
var unorganizedStitchedPointCloud = new Zivid.NET.UnorganizedPointCloud();
var registrationParams = new Zivid.NET.Experimental.LocalPointCloudRegistrationParameters();
var previousToCurrentPointCloudTransform = new Pose(Zivid.NET.Matrix4x4.Identity());
var captureCount = 20;
for (int numberOfCaptures = 0; numberOfCaptures < captureCount; ++numberOfCaptures)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
using (var frame = camera.Capture2D3D(settings))
{
var unorganizedPointCloud = frame.PointCloud.ToUnorganizedPointCloud().VoxelDownsampled(1.0f, 2);
if (numberOfCaptures != 0)
{
if (unorganizedStitchedPointCloud.Size < 4 || unorganizedPointCloud.Size < 4)
{
Console.WriteLine("Not enough points for registration, skipping stitching...");
continue;
}
var registrationResult = PointCloudRegistration.LocalPointCloudRegistration(
unorganizedStitchedPointCloud,
unorganizedPointCloud,
registrationParams,
previousToCurrentPointCloudTransform);
if (!registrationResult.Converged)
{
Console.WriteLine("Registration did not converge, skipping this frame...");
continue;
}
previousToCurrentPointCloudTransform = registrationResult.Transform;
var previousToCurrentMatrix = new Zivid.NET.Matrix4x4(previousToCurrentPointCloudTransform.ToMatrix());
unorganizedStitchedPointCloud.Transform(previousToCurrentMatrix.Inverse());
}
unorganizedStitchedPointCloud.Extend(unorganizedPointCloud);
Console.WriteLine("Captures done: " + (numberOfCaptures + 1));
}
}
Console.WriteLine("Voxel-downsampling the stitched point cloud");
unorganizedStitchedPointCloud = unorganizedStitchedPointCloud.VoxelDownsampled(0.75f, 2);
VisualizePointCloud(unorganizedStitchedPointCloud);
previous_to_current_point_cloud_transform = np.eye(4)
unorganized_stitched_point_cloud = zivid.UnorganizedPointCloud()
registration_params = LocalPointCloudRegistrationParameters()
for number_of_captures in range(20):
time.sleep(1)
frame = camera.capture_2d_3d(settings)
unorganized_point_cloud = (
frame.point_cloud().to_unorganized_point_cloud().voxel_downsampled(voxel_size=1.0, min_points_per_voxel=2)
)
if number_of_captures != 0:
local_point_cloud_registration_result = local_point_cloud_registration(
target=unorganized_stitched_point_cloud,
source=unorganized_point_cloud,
parameters=registration_params,
initial_transform=previous_to_current_point_cloud_transform,
)
if not local_point_cloud_registration_result.converged():
print("Registration did not converge...")
continue
previous_to_current_point_cloud_transform = local_point_cloud_registration_result.transform().to_matrix()
unorganized_stitched_point_cloud.transform(np.linalg.inv(previous_to_current_point_cloud_transform))
unorganized_stitched_point_cloud.extend(unorganized_point_cloud)
print(f"Captures done: {number_of_captures}")
print("Voxel-downsampling the stitched point cloud")
unorganized_stitched_point_cloud = unorganized_stitched_point_cloud.voxel_downsampled(
voxel_size=0.75, min_points_per_voxel=2
)
display_pointcloud(unorganized_stitched_point_cloud)
备注
由于旋转台以恒定的角速度旋转,连续点云之间的变换应该相似。因此,可以将前一次的变换作为下一次配准的初始猜测。
最终,拼接结果会进一步进行体素下采样(voxel-downsampled)并显示。请参见下方的拼接点云结果。
使用预对齐拼接点云
当您能够提前估算捕获之间的相对位姿时,可以将该预对齐与局部点云配准结合使用以优化结果。预对齐的变换可以来自不同的来源,具体选择取决于您的设置:
使用机械臂(手眼标定):相机与物体之间的相对运动由机器人提供。可能是相机安装在机器人上并围绕静止物体移动(Eye-in-Hand),也可能是相机静止不动、机器人在相机前移动物体(Eye-to-Hand)。
使用多相机标定(多台静止相机):多台静止相机从不同角度观察同一场景。
使用从其他来源获知的变换(例如转台位姿、Zivid 标定板或 ArUco 标记):有关如何在拼接前应用此类变换,请参阅 按变换拼接。
使用机械臂(手眼标定)
本节概述了如何使用手眼标定数据和局部点云配准,拼接从相机与物体之间不同相对位姿获取的点云。这种方法同样适用于两种手眼配置:
Eye-in-Hand:相机安装在机器人上,并围绕静止物体移动。
Eye-to-Hand:相机静止不动,机器人握持并移动物体,使其位于相机前方。
本节其余部分以 Eye-in-Hand 配置为示例进行说明。该工作流程和数学原理对 Eye-to-Hand 同样适用,只是此时机器人位姿描述的是物体的位姿,而非相机的位姿。
概述和要求
这种拼接方法可通过机械臂搭载相机围绕物体移动,从而生成该物体完整的三维点云。它特别适用于以下场景:
物体比相机的视野范围更大。
需要从多个角度对物体进行捕获,以用于检测、建模或操作。
要使用此方法,您需要一个有效的 手眼标定 结果。该结果通常以 hand_eye_transform.yaml 文件形式提供,该文件定义了机器人末端执行器坐标系与相机坐标系之间的转换关系。利用这一变换,可以将不同相机位姿下捕获的点云转换到同一公共坐标系中,从而实现多视角点云的精确对齐。
您需要:
一组已捕获的 Zivid 点云(
capture_*.zdf)一组对应的机器人位姿文件(
robot_pose_*.yaml)——每次捕获对应一个位姿文件从成功的手眼标定过程中获得的
hand_eye_transform.yaml
这些元素结合起来,可以让您将各个捕获的数据在统一的坐标系中进行预对齐。
拼接工作流程
拼接过程主要包括以下几个步骤:
加载从不同机器人位姿捕获的多个点云。
加载每个位姿(
robot_pose_*.yaml)和手眼标定结果(hand_eye_transform.yaml)。使用手眼变换将每个机器人位姿转换到相机位姿。
将点云从相机坐标系转换到机器人基坐标系,对点云进行预对齐。
在基坐标系中应用工作空间 ROI 框,以从每个视角中剔除背景。
对每个点云(体素网格)进行下采样,以加快处理速度并减少噪声。
使用局部点云配准(Local Point Cloud Registration)来优化不断增加数据的已拼接点云与每个新加入点云之间的对齐精度。
(可选)重新加载原始的全分辨率点云,并应用估计的转换关系,以全分辨率进行拼接。
将最终结果导出为拼接后的 .ply 点云文件。
[Robot Arm (Pose A)] -> capture_1.zdf
[Robot Arm (Pose B)] -> capture_2.zdf
...
[Robot Arm (Pose N)] -> capture_N.zdf
Each capture is paired with:
- robot_pose_*.yaml
- hand_eye_transform.yaml
Apply:
- Pre-alignment using robot pose and hand-eye calibration
- Workspace ROI box filtering in robot base frame
- Local point cloud registration for refinement
Output:
- Combined and visualized point cloud
- Optionally reload original captures for full-resolution stitching
Export:
- Final stitched point cloud as a PLY file
工作空间 ROI 框在机器人基坐标系中定义,并在每个点云变换到基坐标系后作为后处理应用。这样可以从每个视角中过滤掉背景,而无需为每次捕获单独配置 ROI:
const auto smallWorkspaceRoiBox = Zivid::Settings::RegionOfInterest::Box{
Zivid::Settings::RegionOfInterest::Box::Enabled::yes,
Zivid::Settings::RegionOfInterest::Box::PointO{ -150, 300, 50 },
Zivid::Settings::RegionOfInterest::Box::PointA{ -150, 600, 50 },
Zivid::Settings::RegionOfInterest::Box::PointB{ 150, 300, 50 },
Zivid::Settings::RegionOfInterest::Box::Extents{ -75, 40 },
};
auto pointCloud = frame.pointCloud();
pointCloud.transform(baseToCameraTransform);
pointCloud.maskByRegionOfInterest(workspaceRoiBox);
var smallWorkspaceRoiBox = new Zivid.NET.Settings.RegionOfInterestGroup.BoxGroup
{
Enabled = true,
PointO = new Zivid.NET.PointXYZ { x = -150, y = 300, z = 50 },
PointA = new Zivid.NET.PointXYZ { x = -150, y = 600, z = 50 },
PointB = new Zivid.NET.PointXYZ { x = 150, y = 300, z = 50 },
};
smallWorkspaceRoiBox.Extents = new Zivid.NET.Range<double>(-75, 40);
var pointCloud = frame.PointCloud;
pointCloud.Transform(baseToCameraTransform);
pointCloud.MaskByRegionOfInterest(workspaceRoiBox);
small_workspace_roi_box = zivid.Settings.RegionOfInterest.Box(
enabled=True,
point_o=[-150, 300, 50],
point_a=[-150, 600, 50],
point_b=[150, 300, 50],
extents=(-75, 40),
)
point_cloud = frame.point_cloud()
point_cloud.transform(base_to_camera_transform)
point_cloud.mask_by_region_of_interest(workspace_roi_box)
局部配准(Local Registration)的工作原理
在利用机器人位姿和手眼标定结果完成初始预对齐后,仍需进一步优化,以补偿微小的位姿误差。局部点云配准通过将每个下采样后的点云与不断增长的拼接点云(即先前已拼接的点云)进行对齐,从而提高整体精度。
在进行配准之前,所有点云都会进行体素下采样。这一步骤对于拼接过程至关重要,因为它:
降低传感器噪声,从而实现更可靠的对齐
有助于移除离群点,避免其对配准过程造成干扰
通过在不损失结构细节的前提下减少点的数量,来提升处理速度
用户在流程开始前选择拼接策略,但实际的拼接操作仅在所有转换关系估计完成之后才执行:
低分辨率拼接:使用在对齐过程中准备好的下采样点云。
高分辨率拼接:重新加载原始的全分辨率点云,并应用已估计的变换,以生成细节更丰富的最终点云。
备注
不使用体素下采样( --full-resolution )的拼接可生成更好细节的点云,但需要更多的内存和计算时间。
备注
在进行局部配准时,点云必须已在预对齐步骤中达到相对较好的初始对齐状态。如果初始错位过大,算法可能无法收敛,或产生不准确的结果。
参数 max_correspondence_distance 定义了在源点云和目标点云之间搜索对应点的匹配半径。该参数应满足以下要求:
大于典型的点间距(以确保能够找到正确的匹配点)
但也不能过大,以免包含错误的对应点,从而降低对齐质量
auto registrationParameters = Zivid::Experimental::LocalPointCloudRegistrationParameters{
Zivid::Experimental::LocalPointCloudRegistrationParameters::MaxCorrespondenceDistance{ 2 }
};
下图展示了某个因尺寸超出相机视场而需要多次捕获的物体的拼接点云结果。
Zivid SDK 示例
探索使用机器人搭载相机进行点云拼接的完整示例代码:
机械臂搭载相机拼接点云的准确度
机械臂搭载相机拼接点云的准确度取决于每次单独静态捕获中的误差,以及机器人和拼接流程引入的任何误差。主要影响因素包括:
相机尺寸正确度误差 — 单次静态捕获的系统性缩放误差,针对每种相机型号进行了规定。有关定义以及如何解读数据表数值,请参阅 Zivid 规格术语。
手眼标定误差 — 相机在机器人法兰上的位姿相对于机器人末端执行器的已知准确程度,换言之,即相机坐标系到机器人法兰坐标系的变换的已知准确程度。该误差由 手眼标定 确定,并对安装刚性较为敏感(请参阅 手臂安装方案的机械注意事项)。
机器人零位校准误差 — 机器人自身对其关节零位和连杆几何结构的了解准确程度。这是机器人本身的一项属性,因制造商、型号和个体设备而异。
机器人重复精度和绝对准确度 — 指令位姿与实际机器人位姿之间的差异。重复精度通常由机器人制造商公布;绝对准确度通常比重复精度更差,且很少公布。
拼接算法误差 — 在通过机器人位姿完成预对齐并通过局部点云配准进行优化后,仍残留的错位误差。该误差取决于重叠程度、场景纹理以及配准参数。
备注
由于非相机因素在很大程度上取决于制造商、安装方式和使用场景,Zivid 并未针对机械臂搭载相机拼接点云发布统一的准确度规格。
要最小化非相机因素的影响:
使用刚性强、低振动的安装方式,以确保相机到法兰的变换在负载和运动过程中保持恒定。刚性材料以及短而紧凑的支架都能减少形变,有助于保持该变换的恒定性。请参阅 手臂安装方案的机械注意事项。
仔细执行 预热、Infield Correction 和 手眼标定,并在任何机械变动后重新验证这些结果。
静态捕获(曝光期间机器人保持静止),而非在运动过程中捕获。
确保相邻捕获之间有足够的重叠,以便局部点云配准能够纠正残余的错位。
使用多相机标定(多台静止相机)
当多台 Zivid 相机固定安装在场景周围的不同位置时,您可以先对它们进行一次相互标定,然后使用得到的变换矩阵拼接此后的每一次捕获。以下情况适合使用此方法:
相机相对于场景保持静止。
您希望同时(或快速连续)捕获所有视角,而不是移动单个相机。
该工作流程包含两个部分:
版本历史记录
SDK |
变更 |
|---|---|
2.17.0 |
为 C# 添加了对本地点云配准的支持。 |