参考引用
- Point Cloud Library
- 黑马机器人 | PCL-3D点云
- PCL深度图像
1. 引言
1.1 深度图像的获取与研究方向
-
获取方法
- 激光雷达深度成像法,计算机立体视觉成像,坐标测量机法,莫尔条纹法,结构光法
-
研究方向
- 深度图像的分割技术
- 深度图像的边缘检测技术
- 基于不同视点的多幅深度图像的配准技术
- 基于深度数据的三维重建技术
- 基于三维深度图像的三维目标识别技术
- 深度图像的多分辨率建模和几何压缩技术
1.2 深度图像定义
-
深度图像(Depth Images)也被称为距离影像(Range Image),是指:将从图像采集器到场景中各点的距离值作为像素值的图像,它直接反应了景物可见表面的几何形状(来自传感器一个特定角度拍摄的一个三维场景获取的有规则的有焦距等基本信息的深度图)
-
从数学模型上看,深度图像可以看做是标量函数 j : I 2 → R j:I^2\to R j:I2→R 在几何 K K K 上的离散采样,得到 r i = j ( u i ) r_i=j(u_i) ri=j(ui),其中 u i ∈ I 2 u_i\in I^2 ui∈I2 为二维网格(矩阵)的索引, r i ∈ R , i = 1 , … , k r_i\in R,i=1,\ldots,k ri∈R,i=1,…,k,以下为从不同视角获得的深度图像过程示意
-
点云:当一束激光照射到物体表面时,所反射的激光会携带方位、距离和强度等信息。若将激光束按照某种轨迹进行扫描,便会边扫描边记录到反射的激光点信息,由于扫描极为精细,则能够得到大量的激光点,因而就可形成激光点云。点云格式有 .las、.pcd、.txt 等
在 PCL 中深度图像与点云最主要的区别在于:近邻的检索方式不同,并且可以互相转换,深度图像经过点云变换可以计算为点云数据,有规则及有必要信息的点云数据可以反算为深度图像数据
2. 从点云创建深度图
2.1 pcl::RangeImage
- RangeImage 类继承于 PointCloud,主要功能是:实现一个特定视点得到一个三维场景的深度图像,其继承关系如下
2.2 核心函数及参数说明
- 函数
createFromPointCloud(pointCloud, angularResolution, maxAngleWidth, maxAngleHeight, sensorPose, coordinate_frame, noiseLevel, minRange, borderSize)
- 参数
- pointCloud:被检测点云
- angularResolution = 1:邻近的像素点所对应的每个光束之间相差 1°
- maxAngleWidth = 360:进行模拟的距离传感器对周围的环境拥有一个完整的 360° 视角,无论任何数据集都推荐使用此设置,因为最终获取的深度图像将被裁剪到有空间物体存在的区域范围
- maxAngleHeight = 180:当传感器后面没有可以观测的点时,设置一个水平视角为 180° 的激光扫描仪即可,因为只需要观察距离传感器前面就可以了
- sensorPose:定义了模拟深度图像获取传感器的 6 自由度位置,其原始值为横滚角 roll、俯仰角 pitch、偏航角 yaw 都为 0,设置的模拟传感器的位姿是一个仿射变换矩阵,默认为 4*4 的单位矩阵变换
- coordinate_frame:设置为 CAMERA_FRAME 说明系统的 X 轴是向右的、Y 轴是向下的、Z 轴是向前的,另外参数值是 LASER_FRAME,其 X 轴向前、Y 轴向左、Z 轴向上
- noiseLevel = 0:是指使用一个归一化的 Z 缓存区来创建深度图像,如果想让邻近点集都落在同一个像素单元,可以设置一个较高的值,例如 noiseLevel = 0.05 可以理解为深度距离值是通过查询点半径为 5cm 的圆内包含的点用来平均计算而得到的
- minRange = 0:设置最小的获取距离,如果设置 > 0 则所有模拟器所在位置半径 minRange 内的邻近点都将被忽略,即为盲区
- borderSize = 1:设置获取深度图像边缘的宽度,默认为 0。如果设置 > 0 ,在裁剪图像时,将在图像周围留下当前视点不可见点的边界
2.3 代码实现(C++)
#include <pcl/range_image/range_image.h>
#include <pcl/io/pcd_io.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/visualization/range_image_visualizer.h>
int main(int argc, char **argv) {
pcl::PointCloud<pcl::PointXYZ>::Ptr pointCloudPtr(new pcl::PointCloud<pcl::PointXYZ>);
// 智能指针 pointCloudPtr 指向堆内存中的一个点云对象,而传递对象时通常有两种方式:通过值进行传递或通过引用进行传递
// 1. 值传递方式,要将整个对象从一个地方复制到另一个地方,会产生大量开销,尤其当对象较大时程序运行缓慢
// 2. 引用的方式,只是将指针所指向的对象暴露出来,不会进行拷贝操作,可提高程序效率
pcl::PointCloud<pcl::PointXYZ> &pointCloud = *pointCloudPtr; // 使用引用的形式赋值是为了避免拷贝操作,可提高效率
// 创建一个矩形形状的点云
// 1.遍历一个立方体的所有点,并将每个点添加到点云对象中。这个立方体的长、宽、高为 1,中心点坐标为(1, 0, 0)
// 2.最终生成了一个包含 100000 个点的点云对象,其中每个点在以(1, 0, 0)为中心
// 的 1x1x1 的立方体内,并且在 y-z 平面内均匀分布
for (float y = -0.5f; y <= 0.5f; y += 0.01f) {
for (float z = -0.5f; z <= 0.5f; z += 0.01f) {
pcl::PointXYZ point;
point.x = 2.0f - y;
point.y = y;
point.z = z;
pointCloud.points.push_back(point); // 循环添加点数据到点云对象
}
}
pointCloud.width = (uint32_t) pointCloud.points.size();
pointCloud.height = 1;
// pcl::io::loadPCDFile("./data/table_scene_lms400.pcd", pointCloud);
// 根据前面得到的点云图,通过 1deg 的分辨率生成深度图
// 模拟深度传感器的角度分辨率,即深度图像中一个像素对应的角度大小(都转为弧度)
float angularResolution = (float)(1.0f * (M_PI / 180.0f)); // 弧度 1°
// 模拟深度传感器的水平最大采样角度(都转为弧度)
float maxAngleWidth = (float) (360.0f * (M_PI / 180.0f)); // 弧度 360°
// 模拟传感器的垂直方向最大采样角度(都转为弧度)
float maxAngleHeight = (float) (180.0f * (M_PI / 180.0f)); // 弧度 180°
Eigen::Affine3f sensorPose = (Eigen::Affine3f) Eigen::Translation3f(0.0f, 0.0f, 0.0f); // 传感器采集位置
pcl::RangeImage::CoordinateFrame coordinate_frame = pcl::RangeImage::CAMERA_FRAME; // 相机坐标系
float noiseLevel = 0.00; // 获取深度图像深度时,近邻点对查询点距离值的影响水平
float minRange = 0.0f; // 设置最小的获取距离,小于最小获取距离的位置为传感器的盲区
int borderSize = 1; // 获得深度图像的边缘的宽度
// 使用智能指针 boost::shared_ptr 创建一个名为 range_image_ptr 的指向 pcl::RangeImage 对象的指针
// 同时动态分配内存,该对象可被多个指针共享,直到没有指针引用它时才会自动释放
boost::shared_ptr<pcl::RangeImage> range_image_ptr(new pcl::RangeImage);
pcl::RangeImage &rangeImage = *range_image_ptr;
rangeImage.createFromPointCloud(pointCloud, angularResolution, maxAngleWidth, maxAngleHeight,
sensorPose, coordinate_frame, noiseLevel, minRange, borderSize);
std::cout << rangeImage << "\n";
// 3D 可视化操作
pcl::visualization::PCLVisualizer viewer ("3D Viewer");
viewer.setBackgroundColor (1, 1, 1);
// 添加深度图点云
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointWithRange> range_image_color_handler (range_image_ptr, 0, 0, 0);
viewer.addPointCloud (range_image_ptr, range_image_color_handler, "range image");
viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 4, "range image");
// 添加原始点云
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> org_image_color_handler (pointCloudPtr, 255, 100, 0);
viewer.addPointCloud (pointCloudPtr, org_image_color_handler, "orginal image");
viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "orginal image");
viewer.initCameraParameters();
viewer.addCoordinateSystem(1.0);
// 在可视化窗口中实时显示点云数据,并能够对其进行交互操作。当用户关闭窗口时,循环结束,程序自动退出
while (!viewer.wasStopped ()) {
viewer.spinOnce(); // 检查并处理可视化窗口的所有事件(如:鼠标点击、键盘输入等),然后更新可视化窗口的渲染结果
pcl_sleep(0.01); // 让程序暂停 0.01 秒,这是为了控制循环的速度,防止过快更新可视化窗口导致程序卡死
}
return (0);
}
-
配置文件 CMakeLists.txt
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(range_image_creation) find_package(PCL 1.2 REQUIRED) include_directories(${PCL_INCLUDE_DIRS}) link_directories(${PCL_LIBRARY_DIRS}) add_definitions(${PCL_DEFINITIONS}) add_executable (range_image_creation range_image_creation.cpp) target_link_libraries (range_image_creation ${PCL_LIBRARIES})
-
编译并执行
$ mkdir build $ cd build $ cmake .. $ make $ ./range_image_creation
// 输出结果 header: seq: 0 stamp: 0 frame_id: points[]: 1360 width: 40 height: 34 sensor_origin_: 0 0 0 sensor_orientation_: 0 0 0 1 is_dense: 0 angular resolution: 1deg/pixel in x and 1deg/pixel in y.
3. 从深度图提取边界
3.1 引言
-
对三种不同类型的点感兴趣
- obstacle border:对象边界(属于对象的最外面的可见点)
- shadow border:阴影边界(在背景中与遮挡物相邻的点)
- veil points:面纱点集(对象边界与阴影边界之间的内插点)
-
以下是一个典型的激光雷达获得的 3D 数据对应的点云图
3.2 代码实现(C++)
注意事项
- 要提取边界信息,重点区分未观察到的图像点和应该观察到但超出传感器范围的点。后者通常用来标记边界,而未观察到的点通常不标记边界。因此,最好可以提供这些测量信息。如果无法提供超出这些应该观察到的传感器范围的点,则可以使用 setUnseenToMaxRange 函数,将那些点设置为最大深度(本例添加 -m 参数)
- range_image_border_extraction.cpp
#include <iostream>
#include <boost/thread/thread.hpp>
#include <pcl/range_image/range_image.h>
#include <pcl/io/pcd_io.h>
#include <pcl/visualization/range_image_visualizer.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/features/range_image_border_extractor.h>
#include <pcl/console/parse.h>
#include <pcl/common/file_io.h> // for getFilenameWithoutExtension
typedef pcl::PointXYZ PointType;
float angular_resolution = 0.5f;
pcl::RangeImage::CoordinateFrame coordinate_frame = pcl::RangeImage::LASER_FRAME;
// 若为 true,则:将 应该观察到 但超出传感器范围 的点设置为最大深度
bool setUnseenToMaxRange = false;
// 命令帮助提示
void printUsage (const char* progName) {
std::cout << "\n\nUsage: "<< progName <<" [options] <scene.pcd>\n\n"
<< "Options:\n"
<< "-------------------------------------------\n"
<< "-r <float> angular resolution in degrees (default "<< angular_resolution << ")\n" // 指定角度分辨率的浮点数值,默认值为 angular_resolution
<< "-c <int> coordinate frame (default "<< (int)coordinate_frame << ")\n" // 指定坐标系的整数值,默认值为 coordinate_frame
<< "-m Treat all unseen points to max range\n" // 将所有未被观察到的点视为最大距离处理
<< "-h this help\n" // 输出帮助信息
<< "\n\n";
}
int main (int argc, char** argv) {
// 解析命令行参数(执行时在 ./range_image_border_extraction.cpp 后添加)
if (pcl::console::find_argument (argc, argv, "-h") >= 0) {
printUsage (argv[0]);
return 0;
}
if (pcl::console::find_argument (argc, argv, "-m") >= 0) {
setUnseenToMaxRange = true;
cout << "Setting unseen values in range image to maximum range readings.\n";
}
int tmp_coordinate_frame;
if (pcl::console::parse (argc, argv, "-c", tmp_coordinate_frame) >= 0) {
coordinate_frame = pcl::RangeImage::CoordinateFrame (tmp_coordinate_frame);
cout << "Using coordinate frame "<< (int)coordinate_frame <<".\n";
}
if (pcl::console::parse (argc, argv, "-r", angular_resolution) >= 0)
cout << "Setting angular resolution to " << angular_resolution << "deg.\n";
// 将一个以度为单位表示的 角度分辨率值 转换为弧度制的值
angular_resolution = pcl::deg2rad (angular_resolution);
// 读取点云 PCD 文件;如果没有输入 PCD 文件就生成一个点云
pcl::PointCloud<PointType>::Ptr point_cloud_ptr(new pcl::PointCloud<PointType>);
pcl::PointCloud<PointType> &point_cloud = *point_cloud_ptr;
pcl::PointCloud<pcl::PointWithViewpoint> far_ranges; // 定义点云中每个点的位置和视角信息
Eigen::Affine3f scene_sensor_pose(Eigen::Affine3f::Identity ()); // 申明传感器的位置是一个 4*4 的仿射变换
// 解析命令行参数,并返回所有 .pcd 扩展名的文件的索引列表
std::vector<int> pcd_filename_indices = pcl::console::parse_file_extension_argument(argc, argv, "pcd");
if (!pcd_filename_indices.empty()) {
std::string filename = argv[pcd_filename_indices[0]]; // 保存从命令行参数获取到的第一个 PCD 文件名
if (pcl::io::loadPCDFile(filename, point_cloud) == -1) {
cout << "Was not able to open file \"" << filename << "\".\n";
printUsage(argv[0]);
return 0;
}
// 给传感器的位姿赋值,就是获取点云的传感器的 平移与旋转的向量
// scene_sensor_pose:相机在场景中的位姿(位置和朝向)
// Eigen::Affine3f:可以表示平移、旋转等多种变换,并且支持矩阵乘法来组合多个变换
scene_sensor_pose = Eigen::Affine3f (Eigen::Translation3f (point_cloud.sensor_origin_[0],
point_cloud.sensor_origin_[1],
point_cloud.sensor_origin_[2])) *
Eigen::Affine3f (point_cloud.sensor_orientation_);
// 去除文件扩展名,并添加 "_far_ranges.pcd" 来创建输出文件名
std::string far_ranges_filename = pcl::getFilenameWithoutExtension(filename) + "_far_ranges.pcd";
if (pcl::io::loadPCDFile(far_ranges_filename.c_str(), far_ranges) == -1)
std::cout << "Far ranges file \"" << far_ranges_filename << "\" does not exists.\n";
} else { // 如果没有读取点云,则要自己生成点云
cout << "\nNo *.pcd file given => Generating example point cloud.\n\n";
for (float x = -0.5f; x <= 0.5f; x += 0.01f) {
for (float y = -0.5f; y <= 0.5f; y += 0.01f) {
PointType point;
point.x = x;
point.y = y;
point.z = 2.0f - y;
point_cloud.points.push_back(point);
}
}
point_cloud.width = (int) point_cloud.points.size();
point_cloud.height = 1;
}
// 从创建的原始点云中获取深度图
float noise_level = 0.0;
float min_range = 0.0f;
int border_size = 1;
// 使用智能指针 boost::shared_ptr 管理内存,确保在不需要该对象时自动释放其占用的空间
boost::shared_ptr<pcl::RangeImage> range_image_ptr(new pcl::RangeImage);
pcl::RangeImage &range_image = *range_image_ptr;
range_image.createFromPointCloud (point_cloud, angular_resolution, pcl::deg2rad(360.0f), pcl::deg2rad(180.0f),
scene_sensor_pose, coordinate_frame, noise_level, min_range, border_size);
range_image.integrateFarRanges(far_ranges); // 将远处的距离数据合并到深度图像中对应的位置上,以提高远处物体的检测效果
// 将所有未被观测到的点的值设置为距离图像中已知距离的最大值
// 避免在后续的处理或分析过程中出现未知的距离值所带来的问题
if (setUnseenToMaxRange)
range_image.setUnseenToMaxRange();
// 设置点云可视化
pcl::visualization::PCLVisualizer viewer("3D Viewer");
viewer.setBackgroundColor(1, 1, 1);
viewer.addCoordinateSystem(1.0f, "global");
// 创建一个自定义的点云颜色处理器,用于将点云数据中的每个点赋予相同的黑色颜色
pcl::visualization::PointCloudColorHandlerCustom<PointType> point_cloud_color_handler(point_cloud_ptr, 0, 0, 0);
// 将点云数据加入到场景中,并使用上述颜色处理器使其呈现黑色
viewer.addPointCloud(point_cloud_ptr, point_cloud_color_handler, "original point cloud");
//PointCloudColorHandlerCustom<pcl::PointWithRange> range_image_color_handler(range_image_ptr, 150, 150, 150);
//viewer.addPointCloud(range_image_ptr, range_image_color_handler, "range image");
//viewer.setPointCloudRenderingProperties(PCL_VISUALIZER_POINT_SIZE, 2, "range image");
// 提取深度图边界
pcl::RangeImageBorderExtractor border_extractor(&range_image); // 从深度图像或距离图像中提取边界信息
pcl::PointCloud<pcl::BorderDescription> border_descriptions; // 用于存储每个点是否是 边界点及其边界类型 的信息
border_extractor.compute(border_descriptions);
// 在 3D viewer 中显示点云
// pcl::PointWithRange 存储了每个点的三维坐标 xyz 和距离信息 range
pcl::PointCloud<pcl::PointWithRange>::Ptr border_points_ptr(new pcl::PointCloud<pcl::PointWithRange>),
veil_points_ptr(new pcl::PointCloud<pcl::PointWithRange>),
shadow_points_ptr(new pcl::PointCloud<pcl::PointWithRange>);
pcl::PointCloud<pcl::PointWithRange> &border_points = *border_points_ptr,
&veil_points = * veil_points_ptr,
&shadow_points = *shadow_points_ptr;
// 从给定的 border_descriptions 中提取特定属性的点,并将这些点分别存储到三个不同的 PointCloud 中
// 使用双重循环遍历整个 range_image,在每个像素位置 (x, y) 处检查该位置的点是否具有特定的边界属性
for (int y = 0; y < (int)range_image.height; ++y) {
for (int x = 0; x < (int)range_image.width; ++x) {
/*
首先,通过 border_descriptions.points[y * range_image.width + x] 访问像素点(x, y)对应的边界特征
然后,使用 pcl::BORDER_TRAIT__OBSTACLE_BORDER 常量进行比较,判断该像素点是否具有障碍物边界特征
如果是,则通过 range_image.points[y * range_image.width + x] 获取该像素点对应的三维点,并添加到 border_points 容器中
1. border_descriptions:表示边界描述的容器,其中包含了每个像素点的边界特征
2. range_image:表示一个基于深度图的三维点云数据结构,其中每个像素点对应于一个三维空间中的点
3. x 和 y:表示像素点的坐标
4. pcl::BORDER_TRAIT__OBSTACLE_BORDER:表示障碍物边界特征的枚举常量
*/
if (border_descriptions.points[y * range_image.width + x].traits[pcl::BORDER_TRAIT__OBSTACLE_BORDER])
border_points.points.push_back(range_image.points[y * range_image.width + x]);
if (border_descriptions.points[y * range_image.width + x].traits[pcl::BORDER_TRAIT__VEIL_POINT])
veil_points.points.push_back(range_image.points[y * range_image.width + x]);
if (border_descriptions.points[y * range_image.width + x].traits[pcl::BORDER_TRAIT__SHADOW_BORDER])
shadow_points.points.push_back(range_image.points[y * range_image.width + x]);
}
}
// 分别设置三种类型点云可视化
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointWithRange> border_points_color_handler (border_points_ptr, 0, 255, 0);
viewer.addPointCloud<pcl::PointWithRange> (border_points_ptr, border_points_color_handler, "border points");
viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 7, "border points");
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointWithRange> veil_points_color_handler (veil_points_ptr, 255, 0, 0);
viewer.addPointCloud<pcl::PointWithRange> (veil_points_ptr, veil_points_color_handler, "veil points");
viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 7, "veil points");
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointWithRange> shadow_points_color_handler (shadow_points_ptr, 0, 255, 255);
viewer.addPointCloud<pcl::PointWithRange> (shadow_points_ptr, shadow_points_color_handler, "shadow points");
viewer.setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 7, "shadow points");
// 在深度图中显示点云
pcl::visualization::RangeImageVisualizer *range_image_borders_widget = NULL;
// 要显示的距离图像,显示范围的最小值,显示范围的最大值(返回 float 类型的正无穷大)
// 是否应该在边框上显示描述信息,可选的边框描述信息,显示窗口的名称
range_image_borders_widget = pcl::visualization::RangeImageVisualizer::getRangeImageBordersWidget
(range_image, -std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(), false,
border_descriptions, "Range image with borders");
while (!viewer.wasStopped ()) {
range_image_borders_widget->spinOnce();
viewer.spinOnce ();
pcl_sleep(0.01);
}
}
-
配置文件 CMakeLists.txt
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(range_image_border_extraction) find_package(PCL 1.3 REQUIRED) include_directories(${PCL_INCLUDE_DIRS}) link_directories(${PCL_LIBRARY_DIRS}) add_definitions(${PCL_DEFINITIONS}) add_executable (range_image_border_extraction range_image_border_extraction.cpp) target_link_libraries (range_image_border_extraction ${PCL_LIBRARIES})
-
编译并执行
$ mkdir build $ cd build $ cmake .. $ make $ ./range_image_border_extraction -m table_scene_lms400.pcd # 使用 -m 设置传感器看不见的位置