ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

激光点云动态障碍物去除:从标定到深度校验的数据链路实践

激光点云动态障碍物去除:从标定到深度校验的数据链路实践 激光点云地图里出现半透明的“鬼影墙”、路口柱子旁边叠着一层歪歪扭扭的假轮廓、停车场出口的边缘像被反复涂抹过……如果你在建图或定位时遇到过这类现象那说明动态障碍物已经把激光点污染了。这类问题靠常规的统计滤波很难根治我们的做法是用视觉语义分割先认人、认车、认非机动车再把图像上的动态区域映射回激光点在建图和定位之前就把这些点摘掉。这篇“数据篇”不聊网络结构怎么改专心讲清楚从传感器标定、时间同步、点云到像素的映射、深度校验到阈值筛选和数据验证这一整条数据链路怎么搭以及我在实际项目里踩过的坑。整套流程里最容易翻车的不是语义分割模型而是数据链路本身。我们实测下来网络只占了三成工作量剩下的七成全在处理数据对齐和误删问题。所以这篇内容对正在搞激光SLAM、点云建图、高精地图或者多传感器融合的工程师会比较有用哪怕你只是想把动态点从点云里赶干净也能直接照着这套思路落实。1. 动态点污染到底有多严重一张图看懂“必须做”的理由1.1 动态点在点云地图里是怎么变成“鬼影”的激光雷达的工作原理是发射光束、接收回波它天然不做“语义区分”。一辆车从你面前开过它反射的距离信息会被当成真实环境的一部分写进地图一个人从路边走过他的点云也会被当作静态结构融合进去。相当于一次曝光里混入了多张底片。最常见的结果就是一面干净的墙旁边悬浮着一层半透明的轮廓或者车道边缘出现拖尾状的点云残留。这种污染在视觉上好看只是第一层问题更要命的是它会影响下游所有环节。做scan-to-map匹配的时候动态点会让最近邻搜索产生错误对应关系把雷达姿态往偏的方向拉做栅格地图的时候动态物占据的栅格被反复标记最后形成一个“虽然没有实体但概率很高”的虚假障碍物规划模块看到这东西就可能踩出无意义的急刹或绕行。我见过最夸张的情况是园区里一群人密集路过建完图之后路口直接多了一堵墙。1.2 为什么靠传统激光方法不够非要引入视觉语义很多SLAM系统本身也带动态点处理比如基于距离连续性的离群点过滤、基于多帧差分的运动点检测。但这些方法的思路是“从几何上判断这个点有没有动”遇到运动缓慢、走走停停的行人或者短暂静止的车辆几何方法很容易失效——因为它们在几十帧里位移量很小容易被判成静态。此外纯几何方法还分不清“正在运动的物体”和“本身就是环境中一部分的物体”比如路边停着的车它没有动但它也不该进入建图。视觉语义分割的优点在于从根上给了你一个“先验身份”。模型看到一个人、一辆车、一辆自行车它不会去纠结这物体现在动没动它先把这类对象标记为“高概率动态候选”再由后续的深度校验和置信度阈值做进一步筛选。本质上这是用2D的语义先验给3D点云做一次笨而可靠的预分类把动态对象的召回率提上一个台阶剩下的精确率问题交给数据处理去扛。2. 地面之上的隐性地基联合标定和时钟同步是一切的开始2.1 相机与激光雷达联合标定为什么是这套方案的地基要从图像语义掩码反查激光点第一步就是把激光雷达坐标系下的三维点转换到相机坐标系下的二维像素坐标。这个过程依赖两个东西相机内参和相机-雷达外参。内参决定了三维点经过针孔模型投影后落在像素平面的位置外参则是相机坐标系和雷达坐标系之间的旋转矩阵和平移向量。任何一个参数不准语义mask和点云之间就会错位后面的深度校验、删点决策全都会连锁出错。实际标定中我有一个深刻的体会很多人标定完只看“标定板区域的投影误差”却忽略了不同距离下的表现。近距离误差在2个像素以内时你以为外参没问题但到了20到30米开外外参的几个毫弧度旋转误差会被放大成明显的横向偏移。动态物体往往正出现在中远距离上一旦偏移了几个像素语义轮廓和真实点云就会错位半辆车。所以我建议标定完成后至少要在5米、15米、30米三个距离上各取一次静态目标做投影叠加检查确认没有系统性偏移再用。具体标定可以用棋盘格靶标配合自动标定工具。流程大致是同时采集包含棋盘格的点云和图像分别在点云里提取标定板平面和角点在图像里提取棋盘格角点然后通过PNP求外参。标定现场要注意棋盘格要斜着摆、竖着摆、远近都摆尽可能覆盖视场和距离范围否则求出来的外参容易在某个方向上有退化。整个标定通常需要采集20到30帧取重投影误差最小、分布最均匀的一组作为最终结果。2.2 时间戳对齐视觉和点云不同步时动态目标直接“重影”外参解决的是“空间能不能对上”时间同步解决的是“同一瞬间能不能对上”。动态目标就算空间标定再准如果相机采集时刻和雷达采集时刻差了50毫秒车已经往前挪了一大截投影到图像上的位置自然就和语义掩码错位。尤其当车速或对象运动速度较快时这种错位会被进一步放大。时间同步有两种做法。硬件同步是最可靠的方式通常做法是给相机外部触发线接上雷达或工控机的PPS脉冲让相机曝光时刻和雷达扫描某个角度对齐所有传感器统一用GPS或者PTP时间戳。软件同步则是各自用独立时钟打时间戳然后在上层做最近帧匹配或插值。在项目里我倾向于优先用硬件同步因为软件同步一旦时钟漂移排查起来非常痛苦而且雷达每一帧本身有几十毫秒的扫描周期如果相机曝光时间和雷达扫描不在同一个相位动态物体的边缘轮廓天然就差一点。从数据侧来说一个很实用的中间做法是先统一所有传感器的时间源让所有节点都从同一个UTC或者PTP主钟读取时间戳然后做帧同步的时候取雷达帧的起始时间戳在图像序列里找时间差最小的一帧如果时间差超过了一个图像帧间隔的一半就丢帧。我们在园区场景里把时间同步误差控制在10毫秒以内之后动态目标边缘的重影问题几乎消失。3. 从像素掩码到激光点投影映射与深度校验的实现细节3.1 激光点如何投影到图像像素上拿到一张语义分割图之后常规思路是“哪个像素是人/车就把这个像素对应的点删掉”。问题来了图像是稠密二维网格点云是稀疏三维点它们之间没有天生的索引关系需要通过投影来建立对应。投影公式本身不复杂。设激光点在激光雷达坐标系下的坐标为 p_lidar [X, Y, Z]^T先通过外参 R, t 转到相机坐标系 p_cam R * p_lidar t然后用内参矩阵 K 投影到像素坐标 u fx * X_cam/Z_cam cxv fy * Y_cam/Z_cam cy。注意相机坐标系一般定义是X向右、Y向下、Z向前如果和你的外参定义方向相反投影出来的图会上下左右颠倒整个mask全部错位。这种低级错误在联调时经常出现排查方法是在点云投影结果上叠加显示分割图如果人的语义mask和人的点云轮廓偏移但方向一致多半是外参问题如果是镜像错乱先查坐标系方向定义。另外图像必须做去畸变处理。激光点投影用的是理想针孔模型而相机实际图像带有径向畸变和切向畸变如果不做去畸变靠近图像边缘的激光点投影位置会偏移好几个像素。我们习惯在图像预处理阶段直接对分割网络输入做去畸变这样整个pipeline只用一套无畸变的图像坐标系。3.2 深度校验为什么不能拿到动态mask就无脑删这是整套数据链路里最容易出错、也最值得讲透的一个环节。直接拿着语义分割输出的动态mask去删点会在两个典型场景下翻车。第一个场景是遮挡。假设一辆车停在前面激光打到了车表面也打到了车后面的墙上。车在图像上占一块区域车后面的墙在该区域里是被车挡住的但从激光视角看墙点依然会投影到车身的像素坐标内。如果直接按mask删除墙上的点也会被误删地图就缺了一块。第二个场景是透射。车窗玻璃、围栏这类物体会让激光穿透部分光束打到车内或更远的背景上这些穿透点同样投影在动态mask里一删就误伤了背景。要解决这个问题需要引入基于深度的遮挡判断。具体做法是先统计落到动态mask区域内的所有激光点对每个像素记录该像素上最近一个激光点的深度值生成一张稀疏深度图然后遍历同样落在动态mask内的其他激光点如果它的深度与对应像素的最近深度之差超过某个阈值比如大于0.3米就认为它大概率属于更远的背景或穿透点不做删除。这个思路类似图形学里的Z-Buffer用最近深度挡住后面的点防止“隔山打牛”。这个深度阈值不能设得太死。设小了车窗玻璃后面的点照样被删设大了动态物体腰部的边缘点又可能因为和最近点深度差较大而漏删。我们项目里通常取0.3米到0.5米之间具体数值要根据雷达角分辨率、场景里动态物尺寸和传感器安装高度来标定。给一个直观参考64线雷达在20米处相邻点间距大约0.1到0.2米0.3米的阈值足够区分“同一物体的点”和“背景穿透点”。3.3 Mask膨胀让动态物体边缘的漏检点无处可逃语义分割模型输出的掩码边缘天然是锯齿状的经常出现“车身主体被正确识别但轮毂边缘、后视镜、人的脚踝”这些细节被漏检。如果只按原始mask删点这些边缘点会残留在点云里建图后动态物体的轮廓就像被啃过一口。工程上的常规做法是在把mask用于删点之前先对动态mask做一次形态学膨胀也就是把mask边界向外扩张几个像素。这样做的意义不是多删一些无关点而是把分割模型在边缘处的不确定性吸收掉确保动态物体的完整轮廓点都能被覆盖到。膨胀尺寸一般取3x3或5x5像素就行太大容易覆盖到旁边静止物体。我习惯配合上一节的深度校验一起用先膨胀再按深度差做遮挡判断这样既不漏掉轮胎边缘也不会把墙体背景误删。顺序很重要先膨胀后校验可以先圈定更大的候选区再通过深度差把误入的背景点放回去。4. 删点不是二值问题动态置信度与阈值策略的取舍4.1 语义类别和置信度如何映射成删除决策语义分割网络输出的不是单一的“动态/静态”标签而是每个类别带一个概率值。工程上如果要直接用就得把多类别输出压成一个“动态候选置信度”。一般做法是维护一个动态类别清单比如行人、骑行的人、摩托车、小汽车、卡车、公交车分割模型对这些类别输出的概率取最大值作为该像素的动态置信度。但这里有一个很关键的项目取舍到底哪些类别算“动态”要不要把路边停着的车也算我们一开始把车辆类别全归为动态结果静态车辆在点云地图里也消失了后来发现停车场、小区道路这种场景里静态车辆其实是重要的结构参考。最后调整为两级策略行人、非机动车这类几乎只有运动时才会出现在路面的对象设为高动态优先级车辆类别则结合多帧移动判断如果同一个车辆实例在多帧之间的位姿变化超过阈值才把它归为动态。如果项目对建图实时性要求很高简化做法也可以先把车辆全删但要知道自己丢了什么。推荐的做法永远是在“动态召回率”和“静态点保留率”之间留一个可调参数。4.2 阈值到底怎么定先从验证集里找数字置信度阈值是最大的玄学参数。阈值设太高漏检严重动态点残留多阈值设太低误删率上升静态背景被削掉。我的建议是不要凭感觉拍脑袋而是先在离线数据上统计一条曲线。具体做法是选3到5段有代表性的数据长度各一分钟左右覆盖行人、车辆、非机动车混行的场景。人工标注一遍哪些点属于动态目标、哪些点属于静态背景然后跑一版完整的投影和深度校验统计动态点保留率和静态点误删率随阈值的变化。表格里大概长这样置信度阈值动态点漏删率静态点误删率适用场景0.3约2%约6%重动态区域优先保证地图干净0.5约4%约2%综合平衡推荐起步值0.7约10%约0.5%静态场景为主最大程度保护背景这个表只是举例真实数据一定不一样。但调参思路是通用的先确定你对误删的容忍度再看对应阈值下动态点漏删率能不能接受。我通常先以静态点误删率小于2%为硬约束再尽量压低动态漏删率。如果两者冲突就回到多帧一致性投票或者加强传感器同步层面去解决问题而不是一味调阈值。5. 怎么证明你真删干净了量化指标与可视化验证5.1 三个核心指标漏删率、误删率、残影指数删动态点最怕的是“看着干净了但实际没删干净”或者“地图干净了但被删的全是墙”。为了不被主观视觉误导我用三个指标来量化清洗效果。第一个是漏删率所有本应属于动态物体的点里有多少没被识别和清洗。第二个是误删率所有静态背景点里有多少被误删。这两个指标直接反映算法链路在“该删”和“不该删”之间的平衡。第三个是残影指数这个指标直接面向建图结果把清洗前后的点云分别建图在固定空间区域内统计栅格被重复占据的次数动态残留越多重复占据率越高。残影指数在项目里很有用因为它把“点云层面删得干不干净”转成了“地图层面到底有没有污染”信息更直观。要拿到前两个指标需要一份带标注的验证集。人力成本肯定有但对调参非常值。标注工具可以用开源的3D点云标注软件把每一帧里的行人、车辆、两轮车框出来或者刷出来再和程序生成的动态点求交集。如果嫌标注成本高可以用一个折中办法找一段没有动态物体的静态背景数据人为叠加动态物体的点云这样标注GT是精确合成的但复杂度也不低。5.2 可视化检查让错误一秒现形指标再漂亮也不能替代直观的可视化检查。我最常用的调试画面是四窗口方案左上显示原始图像和语义分割mask右上显示原始点云左下显示清洗后保留的点云右下显示被删除的点云并标成红色。一辆车经过时如果车的点云在右下窗口里是完整的而旁边的墙面在左下窗口里没有破洞基本可以判定这一帧处理正确。这种可视化调试方式在排查外参错位时尤其高效。投影如果偏了几个像素红色删除点和图像上车辆轮廓之间会有一条偏差带一眼就能看出来是横向偏移还是纵向偏移。还有一个建议把清洗后的点云做成bag回放连续看几十秒不要只看单帧。动态物在运动过程中边缘状态变化很快单帧正常不代表动态过程正常连续回放能发现不少偶发性的误删和漏删。5.3 端到端验证回到建图和定位去看真实收益点云清洗最终是为了服务建图和定位所以最考验效果的验证方式是端到端运行。把同一段原始bag分别用“不开动态清除”和“打开动态清除”跑一遍建图对比最终地图上的残影数量。正常情况下开了动态清除之后地图墙面厚度会变薄、边缘更锐利、道路边沿没有重影。这个验证手段不快但它最直接地回答了“删动态点这件事到底值不值得做”。定位端也有一个有效验证故意把车辆开到有动态物频繁经过的路段记录定位轨迹和真实轨迹的偏差。动态点没有被清除时局部匹配很容易被动态物带偏定位轨迹会出现抖动清除干净之后轨迹平滑度会明显改善。这比单纯看点云指标更有说服力因为最终系统关心的是定位稳不稳、地图准不准。6. 一条可落地的数据清洗流水线骨架模块划分和代码级串联6.1 流水线整体拆解从原始数据到干净点云把上面所有环节串起来整个数据清洗pipeline可以拆成六步。第一步是数据采集同时记录相机图像、激光点云、时间戳和标定参数第二步是传感器同步让每一帧点云都能找到对应的图像帧第三步是语义分割推理输出逐像素的类别置信度第四步是投影与深度校验把二维mask映射回三维点并做遮挡判断第五步是阈值筛选按动态置信度决定每个点是否删除第六步是结果输出吐出干净点云或者点级mask给下游SLAM模块。这六步里第三、四步是计算密集区其余步骤都是数据搬运。如果是在线运行建议把语义分割放到GPU推理线程点云映射放到CPU线程两者通过时间戳同步不要让分割推理阻塞整个pipeline。如果是离线处理反而不需要太在意实时性可以把整个过程做成一个批处理工具直接输入原始bag输出清洗后的bag。6.2 一个Python骨架示例投影、深度校验和删点主流程为了让大家直观看到代码怎么组织我写一个精简易读的Python骨架。真实工程要考虑数据结构和性能但核心逻辑是一样的import numpy as np import cv2 def filter_dynamic_points(points_lidar, seg_class, seg_prob, K, T_cam_lidar, dynamic_classes, conf_thres0.5, depth_thres0.3, dilate_size5): # 1. 从语义分割输出构建动态mask dynamic_mask np.zeros(seg_class.shape, dtypebool) for cls in dynamic_classes: dynamic_mask | (seg_class cls) (seg_prob conf_thres) # 2. 对mask做膨胀吸收边缘漏检 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dilate_size, dilate_size)) dynamic_mask cv2.dilate(dynamic_mask.astype(np.uint8), kernel).astype(bool) # 3. 将激光点转换到相机坐标系并投影到像素 ones np.ones((points_lidar.shape[0], 1)) points_homo np.hstack([points_lidar, ones]) points_cam (T_cam_lidar points_homo.T).T z_cam points_cam[:, 2] u np.round(K[0, 0] * points_cam[:, 0] / z_cam K[0, 2]).astype(int) v np.round(K[1, 1] * points_cam[:, 1] / z_cam K[1, 2]).astype(int) H, W dynamic_mask.shape valid (z_cam 0) (u 0) (u W) (v 0) (v H) keep np.ones(len(points_lidar), dtypebool) # 4. 在动态mask区域生成最近深度图 depth_map np.full((H, W), np.inf, dtypenp.float32) cand_idx valid dynamic_mask[v, u] for i in np.where(cand_idx)[0]: if z_cam[i] depth_map[v[i], u[i]]: depth_map[v[i], u[i]] z_cam[i] # 5. 深度校验深度远大于最近深度的点不删 for i in np.where(cand_idx)[0]: if z_cam[i] depth_map[v[i], u[i]] depth_thres: keep[i] False return points_lidar[keep], keep这个骨架里最核心的是第4步和第5步。第4步先给动态区域建立一个最近深度图第5步再逐个点比较深度差从而过滤掉同一个像素上属于背景的激光点。实际项目中还可以在这个基础上加多帧投票比如连续三帧里两帧判定为动态的点才删能显著降低单帧误删但代价是动态点判定延迟一帧。对于低速建图场景这很好用对于高速自动驾驶场景需要权衡延迟。6.3 在线与离线的模块复用我踩过的一个大坑是最初在线模块和离线脚本各写一套逻辑私有参数不互通最后两边跑出来的结果对不上。后来把所有核心逻辑抽成一个独立库语义分割模型路径、外参、内参、置信度阈值、深度阈值全部通过配置文件传入。在线节点只负责订阅数据、调用核心库、发布清洗后的点云离线工具负责读取bag、调用同一个库、输出结果。这样保证同一套参数在在线和离线环境下表现一致排查问题也只需要盯一份代码。另外强烈建议把清洗结果和原始点云同时发布出去比如干净点云发一个话题被删除的点云发一个话题。这样在调试时可以直接把删除点显示成红色和原始点云、干净点云做三方对比很多隐蔽问题会立刻暴露。7. 数据篇里踩过最深的几个坑7.1 外参差一点点远端误删一大片这个问题在前面提过但值得再强调一次因为它太常见了。我们的车在做完标定后跑了一段城市路结果发现路牌、路灯杆的顶部经常被削掉。查了半天最后发现是外参里俯仰角偏差了大约0.2度。在15米外0.2度就会带来约5厘米的偏移而路灯杆顶部正好处于行驶方向的边缘稍微一偏就被动态mask覆盖然后被删掉了。解决方法是重新做标定并把标定后的外参通过投影叠加图逐距离验证而不是只看标定板区域误差。7.2 车窗玻璃穿透深度校验救了一堵墙最开始我们没有做深度校验直接拿动态mask删点。跑了一个地下车库场景后发现车旁边的墙面上出现了很多“凹坑”。排查后发现是车窗玻璃透射导致的激光束穿透车窗玻璃打到了车外侧的墙但这些点投影到图像上正好落在车窗的像素位置而车窗属于车的动态mask于是一整排墙面点被误删。加上深度校验之后墙点深度明显大于车窗表面深度被成功保留下来。这个案例让我对“深度校验”的优先级排到了所有环节的前列。7.3 雨天和逆光下分割模型的输出质量明显下降语义分割模型不是万能的它一到雨天和逆光场景就容易漏检。雨滴会遮挡图像细节逆光会让车体轮廓和背景融为一体这时动态mask经常缺胳膊少腿动态点漏检率大幅上升。应对方案有两个方向。第一是采集更多恶劣天气的数据做finetune但这属于算法工作第二是数据策略上的兜底比如在雨天场景降低置信度阈值、增加mask膨胀尺寸宁可让误删率稍微抬升也要保证动态点不残留。建图任务对误删的容忍度其实比对漏删的容忍度高一些因为地图里少了几个点还能靠其他帧补回来而动态残留会让墙变厚、边线变脏。7.4 帧率不同步导致动态目标边缘毛刺如果摄像头是15帧、雷达是10帧每帧点云对上的图像实际上是不同时刻的那么动态目标的边缘在点云和图像之间天然存在错位。这种错位在视觉上表现为动态物体的点云边缘比图像mask大一圈或歪一截。我们在一个项目里遇到类似问题最后不是靠算法解决的而是换了一台支持外部触发的相机把曝光时刻和雷达扫描相位对齐错位立刻消失。所以如果项目对动态祛除效果要求高建议一开始就选支持硬件触发的相机而不是后期用软件插值硬凑。7.5 一个看似奇怪但实际的坑删除点占比过高时需要回头查数据源状态监控也很重要。我给数据清洗模块加了一个统计项输出每帧被删除点的比例。正常情况下动态点占比在不同场景里是稳定的比如开阔道路可能是1%到3%路口密集场景可能到10%左右。如果某一天这个值突然冲到30%大概率不是动态物变多而是外参配置加载错误、语义分割模型被错误切换、或者深度校验被人为关掉了。这个简单的数量监控可以帮你在问题发生早期就抓到异常而不是等建图结束后在地图上找鬼影。最后说一点个人体会。这类“数据篇”的工作不会像算法模型那样有漂亮的指标曲线但它决定了下游建图和定位效果的上限。之前有一段时间我总想把模型换大、把分割精度再提几个点后来发现很多问题根本不是模型精度不够而是数据链路上有错位或误删。先把数据链路理顺用可视化工具把每一帧清洗结果看清楚再去纠结模型优化这个顺序别搞反。只要你把标定、同步、投影、深度校验、可视化验证这几件事做扎实动态障碍物祛除这个任务就成功了一大半剩下的才轮到算法慢慢打磨。
返回列表