2D激光雷达数据处理——扫描数据解析和基本应用
上篇我们聊了激光雷达的三种测距原理——ToF、三角测距和相位法。但拿到原始数据之后怎么处理?很多候选人能说出原理,一上手就懵了。
之前面试一家做AGV的公司,面试官给了我一组2D激光雷达的原始扫描数据,让我现场写个滤波和障碍物提取的代码。我当时的处理逻辑有问题,虽然最后勉强写出来了,但过程很狼狈。
今天就把2D激光雷达数据处理这件事从头到尾讲清楚。从数据结构、坐标转换、滤波去噪,到障碍物提取和特征识别,一次讲透。
2D激光雷达的数据结构
2D激光雷达(比如经典的SICK LMS系列、Hokayo URG系列、思岚RPLIDAR系列)输出的数据本质上就是一个极坐标序列。
每一帧扫描包含一组测量值,每个测量值有三个关键信息:角度θ、距离r、反射强度intensity。
用ROS2的话题来描述,就是sensor_msgs/LaserScan消息:
# LaserScan消息的核心字段 angle_min # 起始角度(弧度) angle_max # 终止角度(弧度) angle_increment # 角度分辨率(相邻两个点的角度间隔) time_increment # 两个点之间的时间间隔 scan_time # 一帧扫描的时间 range_min # 最小有效距离 range_max # 最大有效距离 ranges[] # 距离数组,每个元素对应一个角度 intensities[] # 反射强度数组一个典型的2D雷达,比如SICK LMS511,扫描范围270度,角度分辨率0.25度,一帧就有1081个测距点。扫描频率25Hz,每秒25帧。
不同型号的参数差异很大。思岚RPLIDAR A1,360度扫描,角度分辨率约0.9度,一帧大约400个点,扫描频率5-10Hz。Hokuyo URG-04LX,扫描范围240度,角度分辨率0.25度,一帧961个点,扫描频率10Hz。选型的时候这几个参数直接决定了你后续算法的计算量和实时性要求。
面试时候有个坑要注意:ranges数组里的值可能是inf或nan。inf表示距离超出量程,nan表示这次测量无效(比如反射太弱)。处理数据之前必须先过滤掉这些无效值,不然后面计算会炸。
还有一个容易忽略的细节:angle_increment是固定的,但实际每个点的角度可能有微小偏差。这是因为电机的转速不是绝对均匀的。对于精度要求不高的应用(比如避障),可以忽略这个偏差,直接用理论角度。但做SLAM的时候,这个偏差会影响建图质量,需要用时间戳做插值补偿。
import numpy as np def filter_valid_ranges(scan): """过滤无效的测距值""" ranges = np.array(scan.ranges) valid_mask = (ranges > scan.range_min) & \ (ranges < scan.range_max) & \ ~np.isnan(ranges) & \ ~np.isinf(ranges) return ranges[valid_mask]讲真,很多初学者拿到LaserScan数据直接就用,从来不检查有效值。结果程序跑着跑着突然崩溃,一查发现是某个nan值传进了距离计算。养成先过滤再处理的习惯,能省很多debug的时间。
极坐标到笛卡尔坐标的转换
2D雷达的数据是极坐标形式的,但实际处理(做SLAM、做避障)都需要笛卡尔坐标。这个转换是基础中的基础。
def scan_to_cartesian(scan): """将LaserScan转换为笛卡尔坐标点集""" angles = np.arange(scan.angle_min, scan.angle_max + scan.angle_increment, scan.angle_increment) ranges = np.array(scan.ranges) # 过滤无效值 valid = (ranges > scan.range_min) & \ (ranges < scan.range_max) & \ ~np.isnan(ranges) angles = angles[valid] ranges = ranges[valid] # 极坐标转笛卡尔 x = ranges * np.cos(angles) y = ranges * np.sin(angles) return np.column_stack([x, y])这里有个容易忽略的点:上面算出来的坐标是雷达坐标系下的。如果你的雷达安装在机器人上,雷达坐标系和机器人基坐标系(base_link)之间有个固定的TF变换。你需要把这个变换也考虑进去。
# 雷达安装在机器人上的位置和朝向 lidar_x = 0.1 # 相对base_link向前10cm lidar_y = 0.0 # 居中 lidar_yaw = 0.0 # 没有旋转 # 坐标变换 cos_yaw = np.cos(lidar_yaw) sin_yaw = np.sin(lidar_yaw) x_robot = x * cos_yaw - y * sin_yaw + lidar_x y_robot = x * sin_yaw + y * cos_yaw + lidar_y实际项目中用TF2库来做这个变换就行,不需要手动算。但面试时候问你原理,你得知道底层就是这样一个旋转加平移。
有个坑我踩过:ROS2里的TF变换是有时间戳的。如果你用lookup_transform的时候传了当前时间,但雷达数据的时间戳是上一帧的,可能会查不到变换(因为TF缓存还没更新)。解决方案是用tf2_ros::Buffer的get_latest_common_time方法,或者直接用Time(0)表示"最新的可用变换"。
另外,如果你的机器人有运动(比如差速底盘在转弯),雷达扫描一帧需要时间(比如40毫秒),在这40毫秒内机器人已经移动了一小段距离。对于高速运动的机器人,这个"运动畸变"会影响建图质量。解决方案是用里程计数据做运动补偿——把每个点根据它的时间戳变换到同一时刻的坐标系下。这个在SLAM部分会详细讲。
面试中怎么聊
面试官问2D激光雷达数据处理,你可以按这个顺序回答:先说数据结构(LaserScan消息的字段含义),再说坐标转换(极坐标到笛卡尔,再到机器人坐标系),然后讲滤波去噪(飞点检测、中值滤波),接着讲障碍物提取(距离聚类、特征计算),最后结合你做过的项目说应用场景。
如果面试官继续深入,可能会问你:聚类算法的复杂度是多少?怎么优化?动态障碍物怎么跟踪?多雷达数据怎么融合?
多雷达融合是个高频问题。如果你的机器人前后各装了一个2D雷达,两个雷达的扫描数据需要统一到同一个坐标系下。这里有两个难点:一是时间同步,两个雷达的扫描频率可能不一样(比如前面10Hz后面25Hz),需要用时间戳做插值对齐;二是空间标定,两个雷达之间的相对位姿必须精确测量,否则融合出来的点云会出现"重影"。标定通常用标定板配合优化算法来做,这个在传感器标定专题里会展开讲。
再补充一个实际项目中遇到的corner case。当时我们做仓储AGV,用的思岚RPLIDAR A1,在仓库里运行时发现一个问题:货架是金属的,激光打上去反射很强,经常出现一个货架被检测成多个分离的簇。原因是金属表面的镜面反射导致激光在不同位置产生了多个回波。解决办法是结合反射强度(intensity)来辅助判断——镜面反射的intensity通常很高,而正常漫反射的intensity适中。我们设了一个intensity阈值,超过的直接标记为可疑点,在聚类时降低它们的权重。另外,仓库里还有一种情况是激光穿过货架缝隙打到后面的墙上,导致两个货架之间出现虚假的障碍物。这种点的距离值通常比货架面更远,可以用距离跳变检测来过滤——如果相邻两个点的距离差超过0.5米,中间的过渡区域大概率是无效数据。这些实战中的细节,书本上不会教你,但面试时候讲出来会让面试官觉得你真的做过项目。
这些问题留到后面的文章详细讲。
下一篇我们聊3D激光雷达的数据处理。从2D到3D,数据量暴增,处理方式也有很多不同。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列上一篇:第146篇 激光雷达测距原理——ToF/三角测距/相位法的区别 下一篇预告:第148篇 3D激光雷达数据处理——点云结构、格式和处理流程
有任何问题欢迎评论区留言,我会尽量回复。
