从原理到实现 | 如何通过球面投影将点云转换为Range图像

点击下方卡片,关注“自动驾驶之心”公众号

ADAS巨卷干货,即可获取

点击进入→自动驾驶之心技术交流群

后台回复【3D检测综述】获取最新基于点云/BEV/图像的3D检测综述!

前言

将3维激光点云通过球面投影(Spherical Projection)转换为2维距离图像(Range Images),是自动驾驶应用场景中一种非常常见的点云处理方式。点云转换为距离图像后,通常会被输入给一个2维卷积神经网络去实现目标检测、语义分割等任务。目前采用这种点云处理方式的典型目标检测算法有RangeDet,语义分割算法有SqueezeSegRangeNet++SalsaNext等。在这些文章中,都只给出一个通过球面投影转换到距离图像的最终公式,至于这个公式是怎么来的却没有详细的推导,初看论文的读者可能会比较困惑。本文将对这个投影公式做一定的推导,可能本人理解的也不是很对,欢迎大家批评指正。

d642789931358b90b93d2cc578bf632e.png

图片来源于RangeDet论文

球面投影推导过程

假设有一个m线的旋转扫描式激光雷达,它的垂直视场角FOV被分为上下两个部分:FOV_upFOV_down,通常以FOV_up的数值为正数而FOV_down数值为负数,所以FOV = FOV_up + abs(FOV_down)。激光雷达旋转扫描一周得到的点云相当于是以其自身为中心的空心圆柱体,如果把这个圆柱体展开的话,那么就可以把点云投影到一个图像平面中去,这个图像平面就是距离图像。

67d9ccec4f00ee78823549a73795a8cb.png

对于一个m线的激光雷达,在扫描的某一时刻会得到m个点,如果旋转一周扫描了n次,那么得到的点云就可以用一个的矩阵来表示。那么怎么把3维的点云投影到2维的距离图像平面呢?这就需要用到球面坐标。

1bee4cb88cc3988f9cec0711e469a77c.png

图片来源于RangeDet论文

球面坐标用3个参数来表示:距离,方位角(Azimuth),天顶角(Zenith)。通常使用的激光雷达点云中的每个由3维笛卡尔坐标表示的点实际上是从球面坐标系转换而来:

让我们再通过下图来理解一下3维笛卡尔坐标系和球面坐标系之间的关系。

9c2f9d342bf30a5a47eb5681132b7d21.png

假设3维笛卡尔坐标系下的点坐标为,那么用球面坐标系可以这样表示该点:

如果以x轴方向为前视图的方向把激光雷达旋转扫描一周得到的圆柱体展开后,可以得到一副这样的图像:坐标原点在图像的中心,图像中像素的纵坐标由pitch角投影得到(范围为[FOV_down,FOV_up]),横坐标由yaw角投影得到(范围为)。

ab20f4e89125733fddd6dd672e95bd67.png

由于图像坐标系是以左上角作为坐标原点,所以上面得到的前视图还需要做一下坐标转换,把坐标原点移到左上角去:

把3维点云投影为2维图像,这种降维操作必然会带来信息损失。为了尽可能减少投影带来的信息损失,我们需要选择合适大小的投影图像。对于一个64线的激光雷达,一般会设置投影图像的高为64,那么图像的宽该如何设置呢?假设激光雷达的水平分辨率为0.35度,那么旋转一周一个激光器最多产生的点数为。在卷积神经网络中,一般会对输入特征图做多次2倍下采样,所以图像的宽度需要设置为2的次幂,这里可设置为1024

由于不同类型激光雷达的视场角、水平分辨率不同,投影图像的尺寸也会根据需要设置为不同的值,为了适应这些变化,yawpitch还需要进行规范化:

规范化后,再乘以投影图像的宽高,就得到了这个点投影到距离图像的坐标:

上式中的第二步是将代入得到的。

代码实现

理解了原理后,我们再用代码来把这个投影过程实现一遍。在RangeNet++中,点云被转换为5个通道的距离图像,这5个通道分别代表点云的这5个属性。下面的代码将展示如何通过球面投影将点云转换为需要的距离图像,使用的点云数据来源于SemanticKITTI数据集。

#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>

#include <cmath>
#include <iostream>
#include <memory>
#include <opencv2/opencv.hpp>
#include <string>
#include <vector>

int main(int argc, char** argv) {
  if (argc < 2) {
    std::cout << "Usage: " << argv[0] << " <pcd_file>\n";
    return -1;
  }

  const std::string pcd_file(argv[1]);
  pcl::PointCloud<pcl::PointXYZI>::Ptr point_cloud(
      new pcl::PointCloud<pcl::PointXYZI>);

  if (pcl::io::loadPCDFile<pcl::PointXYZI>(pcd_file, *point_cloud) == -1) {
    std::cout << "Couldn't read pcd file!\n";
    return -1;
  }

  constexpr int width = 2048;
  constexpr int height = 64;
  constexpr float fov_up = 3 * M_PI / 180.0;
  constexpr float fov_down = -25 * M_PI / 180.0;
  constexpr float fov = std::abs(fov_up) + std::abs(fov_down);
  const std::vector<float> image_means{12.12, 10.88, 0.23, -1.04, 0.21};
  const std::vector<float> image_stds{12.32, 11.47, 6.91, 0.86, 0.16};
  float* range_images = new float[5 * width * height]();

  for (const auto& point : point_cloud->points) {
    const auto& x = point.x;
    const auto& y = point.y;
    const auto& z = point.z;
    const auto& intensity = point.intensity;
    const float range = std::sqrt(x * x + y * y + z * z);
    const float yaw = -std::atan2(y, x);
    const float pitch = std::asin(z / range);

    float proj_x = 0.5f * (yaw / M_PI + 1.0f) * width;
    float proj_y = (1.0f - (pitch + std::abs(fov_down)) / fov) * height;
    proj_x = std::floor(proj_x);
    proj_y = std::floor(proj_y);

    const int u = std::clamp<int>(static_cast<int>(proj_x), 0, width - 1);
    const int v = std::clamp<int>(static_cast<int>(proj_y), 0, height - 1);

    range_images[0 * width * height + v * width + u] =
        (range - image_means.at(0)) / image_stds.at(0);
    range_images[1 * width * height + v * width + u] =
        (x - image_means.at(1)) / image_stds.at(1);
    range_images[2 * width * height + v * width + u] =
        (y - image_means.at(2)) / image_stds.at(2);
    range_images[3 * width * height + v * width + u] =
        (z - image_means.at(3)) / image_stds.at(3);
    range_images[4 * width * height + v * width + u] =
        (intensity - image_means.at(4)) / image_stds.at(4);
  }

  // 对range通道进行可视化
  cv::Mat range =
      cv::Mat(height, width, CV_32FC1, static_cast<void*>(range_images));
  cv::Mat normalized_range, u8_range, color_map;
  cv::normalize(range, normalized_range, 255, 0, cv::NORM_MINMAX);
  normalized_range.convertTo(u8_range, CV_8UC1);
  cv::applyColorMap(u8_range, color_map, cv::COLORMAP_JET);
  cv::imwrite("range_color_map.jpg", color_map);
  cv::imshow("Range Image", color_map);
  cv::waitKey(0);

  delete[] range_images;

  return 0;
}

range通道可视化的结果如下图所示:

5071d91515b5c0cdb916c34b8036f358.jpeg

上面的代码有几个需要说明的地方:

  • fov_upfov_downimage_meansimage_stds这几个参数来源于RangeNet++预训练模型中的arch_cfg.yaml文件。

  • std::clamp需要c++17支持,编译的时候请使用-std=c++17编译选项。

  • 实际使用中width * height的值只需要计算一次,没必要在循环里面反复计算,这里这么写只是为了方便理解。

参考资料

  • RangeDet: In Defense of Range View for LiDAR-based 3D Object Detection

  • https://towardsdatascience.com/spherical-projection-for-point-clouds-56a2fc258e6c

d278195030a3c62642c53f6c50234a82.png

自动驾驶之心】全栈技术交流群

自动驾驶之心是首个自动驾驶开发者社区,聚焦目标检测、语义分割、全景分割、实例分割、关键点检测、车道线、目标跟踪、3D目标检测、BEV感知、多传感器融合、SLAM、光流估计、深度估计、轨迹预测、高精地图、规划控制、模型部署落地、自动驾驶仿真测试、硬件配置、AI求职交流等方向;

加入我们:自动驾驶之心技术交流群汇总!

自动驾驶之心【知识星球】

想要了解更多自动驾驶感知(分类、检测、分割、关键点、车道线、3D目标检测、多传感器融合、目标跟踪、光流估计、轨迹预测)、自动驾驶定位建图(SLAM、高精地图)、自动驾驶规划控制、领域技术方案、AI模型部署落地实战、行业动态、岗位发布,欢迎扫描下方二维码,加入自动驾驶之心知识星球(三天内无条件退款),日常分享论文+代码,这里汇聚行业和学术界大佬,前沿技术方向尽在掌握中,期待交流!

e4c956893917e22c9db5cad766dfb979.jpeg

<think>好的,我现在需要解决用户的问题,如何将Livox MID-70的点云数据转换为二维图像。首先,我需要了解Livox MIDX-70的基本特性,比如它是固态激光雷达,扫描模式可能与传统的机械式雷达不同,这可能会影响点云的分布和处理方式。 接下来,用户的需求是转换为二维图像,可能用于SLAM、目标检测或地图构建等应用。转换方法通常包括点云预处理、投影到二维平面、生成图像等步骤。常见的投影方式有俯视图(BEV)、前视图、球面投影等。Livox MID-70的非重复扫描模式可能导致点云密度随时间增加,所以需要考虑如何有效利用这种特性。 预处理方面,可能需要去除地面点或者使用体素滤波降采样。例如,使用PCL库的VoxelGrid滤波器。然后,投影到二维平面,比如将点云的X、Y坐标映射到图像像素,Z值作为强度或高度信息。这里可能需要确定分辨率,比如每个像素代表多少米,以及处理坐标范围的问题。 生成图像时,可能需要将点云数据转换为矩阵,并将高度或反射强度映射为灰度或颜色。OpenCV可以用来生成图像,并保存为PNG或JPG格式。此外,用户可能需要示例代码,所以得考虑使用Python还是C++,这里用户提到了Python的可能性,但点云处理常用PCL库,而Python中可能需要用到open3d或者numpy、matplotlib等库。 还需要考虑是否有现成的工具,比如ROS中的点云处理包,或者Livox SDK提供的工具。例如,Livox的官方文档可能有相关的转换示例。另外,引用中提到的ECO方法中的特征降维是否适用?虽然引用中提到的矩阵P用于特征降维,可能不直接相关,但点云图像转换也可能涉及特征简化,比如将三维点云投影到二维时,可能需要选择保留哪些信息(如高度、强度)。 关于性能,引用[2]提到LIO-SAM在点云处理上的效率,可能涉及到快速处理点云数据的方法,比如优化算法或简化计算步骤,这在处理大规模点云时很重要,特别是实时应用。 可能的挑战包括点云稀疏性问题,尤其是Livox的固态雷达在近距离可能点云较密,远处稀疏,投影到二维时如何填补空白区域。可能需要插值方法,或者使用栅格化处理,每个像素取最大高度或平均高度。 示例代码方面,可能需要使用Python的open3d库读取点云,numpy处理数据,matplotlib或OpenCV生成图像。例如,读取.pcd文件,提取x,y,z坐标,创建二维网格,将z值映射到像素值,然后生成图像。 最后,要确保代码示例的正确性,比如处理坐标变换、数据类型转换,以及图像的正确显示和保存。同时,需要提醒用户可能需要调整参数,如分辨率、投影平面等,以适应不同的应用场景。</think>要将Livox MID-70点云数据转换为二维图像,可通过以下步骤实现: --- ### **步骤1:点云数据预处理** 1. **数据读取** 使用Livox SDK或开源库(如`Open3D`、`PCL`)读取原始点云数据(`.lvx`或`.pcd`格式)。 ```python import open3d as o3d pcd = o3d.io.read_point_cloud("data.pcd") ``` 2. **滤波与降采样** 去除噪声点并使用体素滤波降低数据量: ```python pcd = pcd.voxel_down_sample(voxel_size=0.1) # 体素尺寸根据需求调整 ``` --- ### **步骤2:点云投影到二维平面** #### **方法1:俯视图(BEV,Bird's Eye View)** - 将三维点云投影到XY平面,保留高度(Z值)或反射强度作为像素值。 - **代码示例**: ```python import numpy as np import cv2 points = np.asarray(pcd.points) x_min, x_max = np.min(points[:,0]), np.max(points[:,0]) y_min, y_max = np.min(points[:,1]), np.max(points[:,1]) resolution = 0.1 # 每像素0.1米 width = int((x_max - x_min) / resolution) height = int((y_max - y_min) / resolution) image = np.zeros((height, width), dtype=np.uint8) for point in points: x, y, z = point px = int((x - x_min) / resolution) py = int((y - y_min) / resolution) if 0 <= px < width and 0 <= py < height: image[py, px] = int(z * 255 / (np.max(points[:,2]) - np.min(points[:,2]))) # 高度映射为灰度值 cv2.imwrite("bev_image.png", image) ``` #### **方法2:球面投影Range Image)** - 将点云映射到极坐标系,生成距离-角度图像(类似激光雷达的原始扫描格式)。 - **公式**: $$ \theta = \arctan\left(\frac{y}{x}\right), \quad \phi = \arcsin\left(\frac{z}{\sqrt{x^2 + y^2 + z^2}}\right) $$ 将$\theta$和$\phi$量化为图像的行列索引,距离或反射强度作为像素值[^1]。 --- ### **步骤3:工具与库推荐** 1. **开源工具** - **Open3D**:支持点云读写、滤波、投影及可视化。 - **PCL(Point Cloud Library)**:提供完整的点云处理流程(需C++编程)。 - **ROS(Robot Operating System)**:使用`pcl_ros`包实时处理点云并发布为图像话题。 2. **Livox官方资源** - Livox SDK提供API直接获取点云数据,支持自定义投影逻辑[^2]。 --- ### **示例效果** - **俯视图**:适用于地面分割、障碍物检测(如图1)。 - **前视图**:适合目标识别(如图2)。 - **球面投影**:可用于SLAM中的特征匹配(如图3)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值