激光雷达建图最怕什么?怕动态点云去除没做好。扫地机器人走着走着,地图上多出一面“人形墙”;仓储AGV在通道里跑了几圈,点云地图里凭空浮现一堆“幽灵货架”;自动驾驶车辆在路口掉头,定位突然跳到马路牙子上——十有八九都是动态点云在捣乱。激光雷达虽然能给出一帧一帧高精度三维点云,但它并不会主动告诉你哪些点属于静止的建筑、哪些点属于正在走路的行人和开动的车辆。我做过好几轮机器人感知相关的落地项目,跟动态点云打了很长时间的交道,把常用的去动态方案基本都过了一遍,这篇就把自己的思路和实测经验整理出来。适合刚接触SLAM、正在被建图残影困扰的工程师参考,也适合做实时定位、需要在前端拦截动态点的朋友对照排查。
1. 先把问题说透:动态点云的成因和代价
想干掉动态点云,先得搞清楚它到底从哪来、会造成什么后果。很多人一上来就急着上算法,结果参数调来调去,静态墙面反而被削掉一大片,就是因为没理解问题的本质。
1.1 动态点云不光是“移动的物体”这么简单
字面意义上,动态点云指行人、车辆、机械臂、门板这类正在运动或状态会发生变化的物体反射回来的点。但在真实场景里,还有两类经常被忽略的“动态点”:一类是半静态物体,比如路边停了一会儿的车、被搬到另一个位置的托盘、打开了一半的防火门,它们在某段时间内是静止的,却在下一段时间内出现新位置、消失旧位置;另一类是传感器自身的运动补偿残差,比如车辆在颠簸路面震动,畸变校正做得不理想,原本静止的墙边缘会拖出一串“重影”。
也就是说,动态点云去除在处理的不只是“正在动的点”,而是“与静态地图模型不一致的点”。搞明白这个区别,后面很多参数设计才能想清楚。
1.2 “残影地图”是怎么一步步毁掉系统的
动态点云最直接的影响,是让建图结果出现残影和双重轮廓。我见过最夸张的一次,一条通道里有人来回走动了几趟,离线建出来的地图上,通道墙壁外多了一层约三十厘米厚的“雾状点云”,后续的路径规划硬是把这条通道判断成了障碍区。动态点云的危害往下延伸还有几个层次:
- 定位漂移:scan-to-map匹配时,动态点会被当作环境特征参与配准,相当于给观测方程塞进了大量错误观测值。尤其当动态物体占视野比例较高时,匹配残差会异常增大,定位输出会突然跳变。
- 回环检测失效:动态点会破坏场景描述子的稳定性。同一个地点,上午和下午扫出来的特征段落差异过大,回环候选就匹配不上,或者匹配出一个明显错误的回环。
- 地图膨胀与导航失败:占据栅格地图里,动态点会让栅格变成occupied,代价地图上形成“虚拟障碍”。移动机器人接近这些位置时会急停绕行,甚至误判无路可走。
- 计算资源浪费:每帧几万到几十万个点,如果动态点占比20%,就等于有20%的计算量在给错误的观测做服务,还顺带污染特征提取和地面分割。
拿生活里的事情打个比方,动态点云就像你在长曝光照片里来回走动的人影。单看某一帧,人影是清晰的;叠加到背景上,就成了半透明的拖影。机器人建图叠的帧数越多,拖影越明显,地图质量越差。
2. 五大去动态思路,我实测的适用场景和选型建议
网上关于动态点云去除的方案五花八门,我按原理把它们归纳成五类:残差一致性过滤、射线追踪验证、语义分割识别、帧间运动聚类、多帧深度一致性。每类我都跑过真机数据,这里把适用场景、优势和坑点一次说完。
2.1 残差一致性过滤:依赖一张足够好的静态底图
残差过滤的思路很直白:把当前帧变换到地图坐标系下,对每个点做最近邻搜索,算它和地图点之间的距离。距离超过阈值的点,说明地图上“找不到对应位置”,大概率是动态点,直接删掉。
这个方法的优点是实现简单、CPU开销低、对算力紧张的嵌入式平台友好。缺点也很致命:它的前提是已经有一张干净的静态底图。如果底图本身是脏的,里面包含大量动态残影,那残差过滤会把残影当作合法地图,反而把真实静态墙点删除。因此我一般只在两遍建图或定位阶段用这个方法,第一遍先建出粗略地图,第二遍用残差过滤清理动态点,得到干净地图后再做实时定位。
实际调参时,最近邻距离阈值通常取传感器噪声的2到3倍。机械式激光雷达在10米处噪声约2厘米,阈值设5厘米左右;但机器人本体振动明显时,建议放宽到8到10厘米,否则大量墙点会被误杀。
2.2 射线追踪验证:几何上“不该出现的点”基本是动态
射线追踪是另一种经典方案。激光雷达的每一次测量,本质是从传感器发射一束激光,打到物体表面再反射回来。如果这束激光在抵达某个点之前,途中应该经过一片空旷自由空间,可地图上显示该路径前方早就该被某个物体截住,那就说明这个点是“穿墙而过”的异常点,很可能就是动态物体。
工程上更常用的简化做法是维护一个占据栅格体素地图。对当前帧的每个点,在传感器原点和该点之间做射线遍历;如果路径上的体素状态是occupied,而实测点又出现在这个体素内部或者更远的位置,再配合残差判断,就能确认动态属性。
射线追踪的优点是误杀率低,因为它是从传感器物理模型出发,对“违反光学常识”的点进行判定;缺点是计算量大,每条射线都要做体素遍历,一帧几万条射线在CPU上跑会很吃力。实测下来,体素分辨率设20厘米时,单帧5万点大约需要15到30毫秒,GPU或指令集优化后能压到10毫秒以内。适合对静态地图质量要求高的场景,比如高精地图采集。
2.3 语义分割识别:直接把“人”和“车”删掉,简单粗暴但小心误伤
深度学习方案最近几年很流行。用语义分割网络对点云逐点分类,识别出person、bicycle、car这些动态物体类别,然后整类删除。工程上常用RangeNet++这类基于距离图像的分割网络,推理速度快;也有用PointPainting把图像语义投影到点云上的做法,准确率更高但多了一层传感器标定依赖。
这个方案的优势是逻辑直观,不需要维护静态底图,适合开放式场景。痛点在于三类问题:一是目标类别覆盖不全,比如叉车、手推车、宠物这些没学过的类别会漏掉;二是分割边缘不干净,行人边缘的“晕圈”会带入少量静景点,这些点往往位于扶手、栏杆等细长结构附近,容易误删;三是语义类别的运动状态不是绝对的——路边停着的车是静态的,建图时把它删掉反而会破坏停车场结构。所以我现在更倾向于把语义分割当成动态点候选区域的生成器,而不是最终过滤器,后面再接一层几何验证,用起来才放心。
2.4 帧间运动聚类:从运动本身出发,不依赖任何先验
帧间运动聚类的基本流程是:先把前后两帧点云做配准,找到每个点在相邻帧的对应点并计算位移矢量;然后把这些矢量做聚类,运动方向和速度一致且空间相邻的点,归为同一个运动物体,整块剔除。
这个方案不依赖底图,也不依赖训练数据,理论上什么动态物体都能发现,包括路上滚过的球、移动的托盘。但它对配准精度非常敏感,如果传感器本身有抖动,或者帧率太低导致帧间位移过大,很容易把静态背景误聚类为运动物体。实测中,我用10Hz机械式雷达在室内做实验,效果不错;但在室外高速行驶场景,帧间位移大、遮挡变化快,聚类极易出现断裂,需要引入IMU预积分做运动补偿才能稳住。
2.5 多帧深度一致性:固态激光雷达时代的潜力方案
如果用的是非重复扫描的固态激光雷达,每一帧覆盖的视角会慢慢变化,同一个静态物体会被多次扫到,而快速运动的物体往往只在极少数帧中出现。利用这个特性,可以统计一段时间内同一个体素被多次观察到的深度分布:深度分布稳定且多帧一致的体素判为静态,深度突变或只出现一两帧的体素判为动态。
这个方法在车载固态雷达上很有前景,因为非重复扫描天然提供了多帧观测样本。缺点是实时性不好,需要维护一段时间的观测历史;另外对极低速运动物体可能失效,一个五分钟才移动一厘米的物体很难和静态物区分开来。目前我只在数据集上验证过,还没上到真机实时管线。
2.6 五类方案横向对比与选型建议
| 方案 | 准确率 | 实时性 | 依赖条件 | 典型误杀场景 | 推荐场景 |
|---|---|---|---|---|---|
| 残差一致性 | 中高 | 高 | 静态底图质量高 | 墙边薄结构、动态阴影 | 两遍建图、定位过滤 |
| 射线追踪验证 | 高 | 中 | 传感器内参、体素地图 | 传感器抖动、标定误差 | 离线高精建图 |
| 语义分割识别 | 中 | 中高 | 训练数据、GPU | 静止的车辆、未学习类别 | 开放式道路、园区 |
| 帧间运动聚类 | 高 | 中 | 配准精度高、帧率足 | 静止被误判为低速运动 | 室内机器人、低速场景 |
| 多帧深度一致性 | 中高 | 低 | 非重复扫描雷达 | 极缓速物体 | 车载固态雷达建图 |
选型没有“最好”,只有“最适合”。我现在的默认组合是:离线建图阶段用射线追踪+残差过滤搭配,把动态点一次性清干净;实时定位阶段用轻量残差过滤+可选语义裁剪,因为地图已经干净了,残差过滤足够用,GPU预算宽裕再加语义。
3. 一套可落地的通用去动态管线:残差过滤加射线验证
这一节我把目前在用的动态点过滤管线完整展开,给出一份可以直接抄作业的工程方案。管线包含预处理、残差计算、射线验证、输出静态点云四个步骤,全部基于公开常见的算法框架实现,不依赖特定硬件,核心代码逻辑可以用伪代码表达。
3.1 预处理:先把点云和位姿收拾干净
去动态之前,必须先确认两个前提:点云已经完成运动畸变校正,当前帧位姿相对地图是准确的。否则残差计算和射线验证都会失真。
- 运动畸变校正:机械式激光雷达一帧内每个点的时间戳不同,如果机器人正在运动,点云会跟着拉伸扭曲。我用惯导或轮式里程计给每个点做线性插值,把所有点变换到这一帧结束时刻的传感器坐标系。
- 位姿初始化:如果是实时定位,直接用上一帧的最优位姿做初值;如果离线处理,先做一次基于静态假设的帧间配准,再用配准结果更新位姿。
预处理完成的标准很简单:把当前帧叠加到地图上,肉眼看到静止墙面轮廓清晰对齐,没有明显的拖影。这个检查步骤看似多余,却能省掉后面排查的大量时间。
3.2 残差过滤:核心参数的选择逻辑
残差过滤的输入是当前帧点云和静态地图,输出是候选动态点集合。实现分四步:
- 把静态地图点云体素化,建立体素索引,体素边长一般取10到20厘米。
- 用KD-Tree或直接查体素索引,为当前帧每个点搜索半径内的最近邻地图点。
- 计算每个点到最近邻的距离,得到残差序列。
- 残差超过阈值的点,标记为动态候选点。
伪代码大致长这样:
# 伪代码:残差一致性过滤 def residual_filter(frame_points, static_map, voxel_size, max_distance): voxel_map = build_voxel_index(static_map, voxel_size) dynamic_candidates = [] for point in frame_points: neighbors = voxel_map.search(point, radius=voxel_size * 1.5) if not neighbors: dynamic_candidates.append(point) continue nearest_dist = min(distance(point, n) for n in neighbors) if nearest_dist > max_distance: dynamic_candidates.append(point) return dynamic_candidates这里最关键的参数是max_distance。取值太小,静态点因为传感器噪声、位姿误差被大量误杀;取值太大,动态点漏检率飙升。我给一个经验公式:max_distance ≈ (2倍传感器噪声标准差 + 位姿误差估计值) × 2。室内机器人噪声小、位姿准,5到8厘米够用;室外车辆行驶速度快、振动大,建议10到15厘米。
另一个容易出错的地方是体素搜索半径。体素边长和半径设置不合理,会导致邻居搜索为空,孤立的静态点被当成动态点。我习惯把搜索半径设为体素边长的1.5倍,保证每个点至少能命中一个相邻体素。
3.3 射线验证:把误杀的墙点捞回来
残差过滤会把两类静态点误判为动态:一是地图本身稀疏、暂时没有覆盖到的区域;二是传感器噪声导致的大残差。射线验证能在这时候把“不该出现”的点捞回来。
射线验证的核心逻辑是:对候选动态点,从传感器原点向该点发一条射线,射线经过的路径如果大部分体素处于unknown或free状态,说明这个点是“凭空出现”的,极可能真是动态点;如果路径上有occupied体素且距离接近,说明它可能只是一个噪声点,应该保留。
# 伪代码:射线追踪验证 def verify_dynamic(candidate, sensor_origin, voxel_map): ray_cells = ray_traverse(sensor_origin, candidate.position) free_count = 0 blocked_count = 0 for cell in ray_cells: state = voxel_map.get_state(cell) if state == FREE: free_count += 1 elif state == OCCUPIED: blocked_count += 1 if free_count > len(ray_cells) * 0.3 and blocked_count < 3: return True # 路径上大量自由空间,该点为动态 return False射线验证能把“穿墙点”和“游离散点”分开处理,但代价是计算量。我在CPU上对候选点做并行化,每帧候选点通常只有三五千个,用OpenMP四线程跑,耗时能控制在5毫秒内;全点云做射线遍历的场景很少用。
3.4 整合管线与性能实测
把上面两个步骤串起来,就得到一套完整的动态点过滤管线:
- 运动补偿 + 位姿更新。
- 残差过滤,生成候选动态点集合。
- 射线验证,排除被误杀的静态点。
- 从原始帧中剔除确认的动态点,输出静态点云。
- 静态点云进入后端建图或定位模块。
我用一个室内楼道场景做测试:10Hz机械雷达,每帧约3万点,环境里有人来回走动。管线跑完,一帧处理时间约18到25毫秒,其中残差过滤4到6毫秒,射线验证12到18毫秒,剩余为预处理开销。动态点检出率约85%,静态结构保留率约98.5%。那漏掉的15%动态点,大多是行人距离探测器较远、点云稀疏、残差恰好低于阈值的情况。
这套管线最大的优势是通用性:不需要训练数据,不依赖语义类别,换个传感器、换个场景都能跑。缺点也很明显:必须有一张干净的底图,如果底图脏,效果会大打折扣。
4. 工程落地避坑:五个高频问题的排查记录
动态点云去除的难点从来不在算法原理,而在工程细节。下面五个问题是我在真实项目里遇到过的,每一项都踩过坑后才总结出对策。
4.1 动态物体静止时,要不要删?
路边停着的车、仓库里不动的托盘、库位上长时间放置的料箱,这类物体在某一时段是“静态”的,但它们随时可能动起来。如果你在建图时把它们删掉了,地图上对应的位置就是空洞,等它们下次挪走,空洞还在;如果保留它们,等到它们真的动了,残影又会出现。
我的处理原则是:建图阶段保留所有当前时刻静止的物体,但在最终交付地图前,用多时段数据做一次对比,统计哪些区域的点云频繁出现、消失,把那些“时隐时现”的区域标为动态区,统一清理。这样既不会把常用停靠位抹掉,也能把真正的动态残影清除。
4.2 参数过于激进,薄壁结构全被削平
体素半径太小、残差阈值太小,最容易出现的问题就是薄壁结构误删。一堵厚度10厘米的隔板墙,如果位姿误差只有2厘米,看起来没问题;但如果是车辆绕行时激光斜射,墙面的入射角很大,实际测距点会发生拉伸,残差轻松超过阈值。
排查这个问题,我习惯对过滤后的点云做可视化和统计对比:把同一条通道在动态过滤前后的点数量做差值图,误删严重的区域会出现雪花状空洞。遇到这种情况,优先放宽残差阈值,同时把射线验证中的free_count比例调高,给墙点更多“存活”机会。
4.3 动态点残留导致回环检测反复失败
动态点如果没清理干净,回环检测里的特征描述子计算会被污染。最典型的问题是同一个地点两次经过时,一次有行人遮挡、一次没有,提取出的场景特征差异巨大,导致候选回环匹配失败。
这个问题单靠去动态算法解决不了,得在回环检测模块同时做两道防线:一是计算描述子之前,先对局部窗口做一次残差过滤,把明显不一致的点剔除;二是回环匹配时,不仅比较当前帧和候选帧的描述子相似度,还要用几何一致性做二次验证。两条腿走路之后,回环检测的成功率明显提升。
4.4 高速运动目标导致单帧畸变残留
动态物体速度过快时,即便进行了运动畸变校正,物体本身的点云仍然会形成拉伸和拖尾。原因是畸变校正常常假设物体是静态的,只补偿了传感器自身的运动,没有补偿物体运动。结果就是动态目标轮廓被拉成一条“彗星尾巴”,残差过滤可能只删掉了头部,尾部残留到静态点云里。
实测里这类残留最麻烦,因为它们距离墙面往往不远,射线验证也不容易筛出来。我目前的方案是加一层帧间一致性检查:连续两帧都出现在同一体素附近、且位置与地图最近的静态结构点距离超过阈值的,才标注为动态。高速目标很难在连续多帧保持稳定位置,这个条件会筛掉大多数彗星尾。
4.5 建图与定位阶段的动态处理策略完全不同
很多人踩的坑是,把离线建图的参数直接用到实时定位上。两阶段的目标不同:建图阶段要求绝对干净,可以牺牲部分实时性,射线追踪、多帧验证、人工检查都可以轮番上;定位阶段要求实时响应,如果动态过滤耗时超过20毫秒,定位帧率就会掉,需要轻量化方案。
我现在的推荐配置是:建图时用“语义候选+射线追踪+残差过滤”三重验证,所有被删点记录成动态区域掩码;定位时只保留轻量残差过滤,同时把建图阶段生成的动态区域掩码加载进来,对这些区域直接降低权重或忽略,不用重复算射线。这样既保证地图干净,又让实时模块足够轻快。
下面是高频问题的速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 墙面出现大量空洞 | 残差阈值过小 | 放宽阈值,检查位姿精度 |
| 动态点一直漏检 | 体素搜索半径太小 | 加大搜索半径,检查体素覆盖 |
| 过滤后仍见彗星拖尾 | 运动补偿不够 | 增加帧间一致性检查 |
| 回环匹配总是失败 | 轻微残影仍在污染描述子 | 回环前增加二次几何验证 |
| 定位帧率明显下降 | 射线验证耗时过长 | 改为定位阶段轻量方案 |
| 行人和车辆边缘被保留 | 语义分割漏检 | 增加几何候选区域补充 |
4.6 一句实在话
说句实在话,动态点云去除做到最后,拼的不是哪个算法更新、哪个网络更强,而是对传感器误差、位姿精度、场景变化这几个底层的理解。我见过把语义分割调到99%准确率的团队,现场一跑还是翻车,因为没处理好停着的车和启动瞬间的半动态物体。也见过只用残差过滤加一套好参数的团队,稳稳定位一整天。先把自己的传感器和位姿摸透,再谈算法,这条经验值很多钱。
最后再分享一个小技巧:无论用什么方案,都把被过滤的点单独存一份日志。排障的时候回看日志,能看到哪些点是被误杀的、哪些是漏掉的,比反复调参数试错快得多。这个习惯我保留到现在,救过很多次急。