4. 动态物体检测与占据栅格更新

任务目标

在已有占据栅格地图的基础上,通过比较当前激光雷达扫描与地图的差异,检测动态物体(如行人、车辆),并仅对动态物体所在的栅格区域执行局部更新,避免静态地图被错误覆盖。

核心原理

将当前扫描点投影到栅格坐标系,若某栅格在地图中被标记为占据(occupied),但当前扫描显示为空闲(free),则该栅格很可能被动态物体遮挡或物体已移开,标记为动态(dynamic)。反之,若地图为空闲而当前扫描为占据,则可能是新出现的动态物体。仅对动态栅格执行概率更新,静态区域保持不变。

关键完整代码

import numpy as np
from dataclasses import dataclass

@dataclass
class GridCell:
    """栅格单元状态"""
    log_odds: float = 0.0          # 对数几率值
    state: str = 'unknown'         # 'free' / 'occupied' / 'dynamic'
    
class OccupancyGrid:
    """占据栅格地图(简化版)"""
    def __init__(self, width, height, resolution):
        self.width = width
        self.height = height
        self.resolution = resolution  # 米/格
        self.cells = [[GridCell() for _ in range(width)] for _ in range(height)]
        self.origin_x = -width * resolution / 2
        self.origin_y = -height * resolution / 2
    
    def world_to_grid(self, x, y):
        """世界坐标转栅格坐标"""
        gx = int((x - self.origin_x) / self.resolution)
        gy = int((y - self.origin_y) / self.resolution)
        if 0 <= gx < self.width and 0 <= gy < self.height:
            return gx, gy
        return None
    
    def update_cell(self, gx, gy, occupied_prob):
        """更新栅格对数几率"""
        if occupied_prob > 0.5:
            self.cells[gy][gx].log_odds += 0.4   # 占据增量
        else:
            self.cells[gy][gx].log_odds -= 0.4   # 空闲增量
        # 限制范围
        self.cells[gy][gx].log_odds = np.clip(self.cells[gy][gx].log_odds, -3.0, 3.0)
        # 更新状态
        if self.cells[gy][gx].log_odds > 0.5:
            self.cells[gy][gx].state = 'occupied'
        elif self.cells[gy][gx].log_odds < -0.5:
            self.cells[gy][gx].state = 'free'
        else:
            self.cells[gy][gx].state = 'unknown'

def detect_and_update(grid, scan_points, robot_pose):
    """
    检测动态物体并局部更新栅格
    scan_points: 当前激光扫描点 [(x, y), ...] 世界坐标
    robot_pose: 机器人位姿 (x, y, theta)
    """
    dynamic_cells = []
    
    # 1. 将当前扫描点投影到栅格
    current_hits = set()
    for (px, py) in scan_points:
        cell = grid.world_to_grid(px, py)
        if cell:
            current_hits.add(cell)
    
    # 2. 遍历当前扫描覆盖的栅格,检测动态
    for (gx, gy) in current_hits:
        cell = grid.cells[gy][gx]
        # 地图中为占据,但当前扫描显示空闲 → 动态(物体移开)
        if cell.state == 'occupied':
            # 检查该栅格是否被当前扫描射线穿过(即应为free)
            # 简化处理:若扫描点在该栅格附近但栅格本身不是hit,视为free
            # 此处用射线模拟:从机器人位置到扫描点连线上的栅格应为free
            ray_cells = bresenham_line(grid, robot_pose[0], robot_pose[1], 
                                       grid.origin_x + gx*grid.resolution, 
                                       grid.origin_y + gy*grid.resolution)
            for (rgx, rgy) in ray_cells:
                if grid.cells[rgy][rgx].state == 'occupied' and (rgx, rgy) not in current_hits:
                    grid.cells[rgy][rgx].state = 'dynamic'
                    dynamic_cells.append((rgx, rgy))
    
    # 3. 对动态栅格执行局部更新(重置为free)
    for (gx, gy) in dynamic_cells:
        grid.cells[gy][gx].log_odds = -2.0
        grid.cells[gy][gx].state = 'free'
    
    # 4. 对当前扫描命中的栅格执行正常占据更新
    for (gx, gy) in current_hits:
        grid.update_cell(gx, gy, occupied_prob=0.9)
    
    return dynamic_cells

def bresenham_line(grid, x0, y0, x1, y1):
    """Bresenham射线追踪,返回射线经过的栅格坐标列表"""
    cells = []
    gx0, gy0 = grid.world_to_grid(x0, y0)
    gx1, gy1 = grid.world_to_grid(x1, y1)
    if gx0 is None or gx1 is None:
        return cells
    
    dx = abs(gx1 - gx0)
    dy = abs(gy1 - gy0)
    sx = 1 if gx0 < gx1 else -1
    sy = 1 if gy0 < gy1 else -1
    err = dx - dy
    
    while True:
        cells.append((gx0, gy0))
        if gx0 == gx1 and gy0 == gy1:
            break
        e2 = 2 * err
        if e2 > -dy:
            err -= dy
            gx0 += sx
        if e2 < dx:
            err += dx
            gy0 += sy
    return cells

# ========== 最小验证示例 ==========
if __name__ == '__main__':
    # 创建10x10栅格,分辨率0.5m
    grid = OccupancyGrid(10, 10, 0.5)
    
    # 初始化静态地图:在(1.0, 0.0)处放置一个静态障碍物
    static_cell = grid.world_to_grid(1.0, 0.0)
    grid.cells[static_cell[1]][static_cell[0]].log_odds = 2.0
    grid.cells[static_cell[1]][static_cell[0]].state = 'occupied'
    
    # 模拟第一次扫描:机器人位于原点,看到静态障碍物
    robot_pose = (0.0, 0.0, 0.0)
    scan1 = [(1.0, 0.0)]  # 扫描到静态障碍物
    dyn1 = detect_and_update(grid, scan1, robot_pose)
    print(f"第一次扫描动态栅格数: {len(dyn1)}")  # 预期0
    
    # 模拟第二次扫描:障碍物移开,扫描到更远的点(2.0, 0.0)
    scan2 = [(2.0, 0.0)]  # 原位置变为free
    dyn2 = detect_and_update(grid, scan2, robot_pose)
    print(f"第二次扫描动态栅格数: {len(dyn2)}")  # 预期包含(1.0,0.0)所在栅格
    
    # 验证:原静态栅格应被标记为free
    cell = grid.cells[static_cell[1]][static_cell[0]]
    print(f"原障碍物栅格状态: {cell.state}, log_odds: {cell.log_odds:.1f}")

运行与验证

环境依赖:Python 3.8+,NumPy库(pip install numpy)。

运行方式:将代码保存为 dynamic_detection.py,在终端执行:

python dynamic_detection.py

预期输出:

第一次扫描动态栅格数: 0
第二次扫描动态栅格数: 1
原障碍物栅格状态: free, log_odds: -2.0

通过条件:第二次扫描检测到1个动态栅格,且原静态障碍物栅格被正确重置为free状态,说明动态物体检测与局部更新逻辑生效。

关键说明

  • 代码中bresenham_line用于模拟激光射线,判断被射线穿过的栅格应为free,从而识别被遮挡后暴露的栅格。
  • 实际应用中,激光扫描点通常包含数百个点,且需结合里程计位姿进行坐标变换;本案例聚焦核心检测逻辑,使用简化输入。
  • 动态栅格被重置为free后,下一帧若同一位置再次出现占据,会通过正常更新流程重新标记为occupied,实现持续跟踪。