第2章:点云数据基础
各位同学,欢迎来到点云数据基础这一章。说实话,很多做雷达感知的新人,一上来就急着调算法、跑模型,结果连点云数据长什么样都没搞清楚。我当年也犯过这个错——拿到一个Velodyne的bag文件,直接丢进聚类算法,出来的结果惨不忍睹。后来才明白,基础不牢,地动山摇。
这一章,我们就来把点云数据的基础打扎实。我会从定义、坐标系、数据结构三个维度展开。嗯,都是干货。
2.1 点云定义与特性
点云是什么?说白了,就是一堆三维空间中的点。每个点通常包含(x, y, z)坐标,有时候还带上强度、时间戳、甚至颜色信息。
对于毫米波雷达来说,点云有几个关键特性:
- 稀疏性:毫米波雷达的点云比激光雷达稀疏得多。一个激光雷达一帧可能有十几万个点,毫米波雷达可能只有几十到几百个点。我刚开始做融合时,总想用激光雷达那套方法处理毫米波点云,结果发现根本行不通——太稀疏了,聚类都聚不起来。
- 噪声大:毫米波雷达受多径反射、杂波干扰影响很大。你经常能看到一些“飞点”,明明前方是空旷路面,却突然冒出一个速度100km/h的目标。嗯,这就是噪声。
- 多普勒信息:这是毫米波雷达的独门武器。每个点除了位置,还带有径向速度信息。这个信息在目标检测中非常有用——静止目标和运动目标,一看速度就分出来了。
- 反射强度:不同材质对毫米波的反射强度不同。金属强,塑料弱,行人更弱。我在做路侧感知项目时,就利用这个特性区分过护栏和行人。
核心要点:毫米波雷达点云不是激光雷达的“低配版”,它有自己独特的物理特性。处理时一定要尊重这些特性,而不是盲目套用激光雷达的算法。
2.2 坐标系与坐标变换
坐标系这个问题,我吃过不少亏。有一次做多传感器标定,雷达和相机对不上,查了两天,最后发现是坐标系定义搞反了——雷达用的是右手系,相机用的是左手系。
常见的坐标系有:
| 坐标系 | 定义 | 典型应用 |
|---|---|---|
| 雷达坐标系 | 原点在雷达中心,x向前,y向左,z向上(右手系) | 原始点云数据 |
| 车体坐标系 | 原点在车辆后轴中心,x向前,y向左,z向上 | 车辆运动规划 |
| 世界坐标系 | 原点固定在地面某点,通常使用UTM或经纬度 | 全局定位、地图构建 |
| 图像坐标系 | 原点在图像左上角,u向右,v向下 | 雷达-相机融合 |
坐标变换的核心就是旋转和平移。数学上用一个4x4的齐次变换矩阵表示:
// 齐次变换矩阵 T = [R | t; 0 | 1]
// 其中 R 是3x3旋转矩阵,t 是3x1平移向量
// 示例:将雷达坐标系下的点变换到车体坐标系
Eigen::Matrix4f T_radar_to_vehicle;
T_radar_to_vehicle <<
0.999, 0.017, -0.017, 0.5,
-0.017, 0.999, 0.017, -0.2,
0.017, -0.017, 0.999, 0.1,
0, 0, 0, 1;
Eigen::Vector4f pt_radar(1.0, 0.5, 0.2, 1.0);
Eigen::Vector4f pt_vehicle = T_radar_to_vehicle * pt_radar;
个人经验:我建议所有坐标变换都用齐次矩阵来做,不要手动拆成旋转和平移分开算。为什么?因为齐次矩阵可以串联多个变换,比如 T_world_to_vehicle = T_vehicle_to_radar * T_radar_to_world,写起来非常清晰,不容易出错。
2.3 点云数据结构
实际工程中,点云数据怎么组织?我主要用两种:PCL和ROS。这两种我都深度用过,各有千秋。
2.3.1 PCL(Point Cloud Library)
PCL是点云处理的老牌库。它的核心数据结构是 pcl::PointCloud<T>,其中T是点类型。
// 定义一个包含强度信息的点云
pcl::PointCloud<pcl::PointXYZI> cloud;
cloud.width = 100; // 点云宽度
cloud.height = 1; // 无序点云,height=1
cloud.points.resize(cloud.width * cloud.height);
// 填充数据
for (size_t i = 0; i < cloud.points.size(); ++i) {
cloud.points[i].x = i * 0.1;
cloud.points[i].y = 0.0;
cloud.points[i].z = 0.0;
cloud.points[i].intensity = 50.0; // 强度值
}
PCL支持的点类型很多:PointXYZ(只有坐标)、PointXYZI(坐标+强度)、PointXYZRGB(坐标+颜色)等。对于毫米波雷达,我常用的是自定义点类型,加上速度信息:
// 自定义毫米波雷达点类型
struct PointRadar {
float x, y, z; // 位置
float intensity; // 反射强度
float velocity; // 径向速度
float snr; // 信噪比
uint64_t timestamp; // 时间戳
};
避坑指南:我曾经在PCL中使用 pcl::visualization 显示毫米波点云,发现默认的渲染方式会把所有点画成同样大小,根本看不出强度差异。后来我手动设置了点的大小和颜色映射,才把强度信息可视化出来。记住:可视化不是小事,它能帮你快速发现数据问题。
2.3.2 ROS(Robot Operating System)
ROS是机器人领域的标准中间件。在ROS中,点云数据通过 sensor_msgs::PointCloud2 消息传递。
// ROS中发布点云
sensor_msgs::PointCloud2 cloud_msg;
cloud_msg.header.frame_id = "radar_link";
cloud_msg.header.stamp = ros::Time::now();
cloud_msg.width = 100;
cloud_msg.height = 1;
cloud_msg.is_dense = true;
// 设置点云字段
sensor_msgs::PointCloud2Modifier modifier(cloud_msg);
modifier.setPointCloud2FieldsByString(2, "xyz", "intensity");
modifier.resize(100);
// 填充数据
sensor_msgs::PointCloud2Iterator<float> iter_x(cloud_msg, "x");
sensor_msgs::PointCloud2Iterator<float> iter_y(cloud_msg, "y");
sensor_msgs::PointCloud2Iterator<float> iter_z(cloud_msg, "z");
sensor_msgs::PointCloud2Iterator<float> iter_intensity(cloud_msg, "intensity");
for (int i = 0; i < 100; ++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity) {
*iter_x = i * 0.1;
*iter_y = 0.0;
*iter_z = 0.0;
*iter_intensity = 50.0;
}
// 发布
pub.publish(cloud_msg);
ROS的 PointCloud2 比PCL更灵活——它用字段描述符来定义每个点的数据布局。你可以任意添加字段,比如速度、SNR等,不需要预先定义点类型。但代价是访问速度稍慢,因为每次都要通过字符串查找字段索引。
2.3.3 PCL与ROS的互转
实际项目中,我经常需要在PCL和ROS之间转换数据。ROS提供了现成的转换函数:
// ROS -> PCL
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud_pcl(new pcl::PointCloud<pcl::PointXYZI>());
pcl::fromROSMsg(cloud_msg, *cloud_pcl);
// PCL -> ROS
sensor_msgs::PointCloud2 cloud_msg_out;
pcl::toROSMsg(*cloud_pcl, cloud_msg_out);
cloud_msg_out.header.frame_id = "radar_link";
我的建议:如果你在做离线数据处理,用PCL更顺手,API丰富,处理效率高。如果你在做在线系统(比如自动驾驶),用ROS更合适,因为它天然支持多节点通信和时间同步。我个人习惯是:离线用PCL,在线用ROS,中间用转换函数衔接。
2.4 本章小结
这一章我们讲了点云的定义、坐标系变换、以及两种主流数据结构。你想想看,这些内容看起来基础,但实际工程中80%的bug都出在这些地方——坐标系搞反了、字段类型不对、时间戳没对齐。嗯,基础的东西往往最容易被忽视。
下一章,我们会进入点云预处理,包括滤波、降采样、地面去除等。到时候我会分享一些实际项目中踩过的坑,比如怎么处理多径反射产生的鬼影点。敬请期待。