传感器融合与感知系统

智能无人车核心课程 · 高中进阶篇

第一章:多传感器系统概述

1.1 为什么需要多传感器融合?

单一传感器无法应对所有场景。想象一个自动驾驶车辆进入隧道:

传感器隧道内表现局限性
摄像头光线骤变,图像过曝或过暗依赖光照条件
激光雷达正常工作,但缺少语义信息无法识别交通标志颜色
GPS信号丢失,无法定位室内/隧道/高楼区失效
IMU短期可用,但累积误差长时间漂移严重
毫米波雷达正常工作角分辨率低,无法识别行人姿态

核心洞察:每种传感器都有"能力边界",融合的目的就是让系统在任何场景下都能找到"可靠的信息源"。

1.2 传感器特性深度对比

摄像头(Camera)

工作原理:光子 → 镜头聚焦 → CMOS 感光阵列 → 电信号 → ISP 处理 → 数字图像

关键参数:
├── 分辨率:1280×720 / 1920×1080 / 2880×1860(Tesla 使用 120 万像素摄像头)
├── 帧率:30fps / 60fps(高速场景需更高帧率)
├── 动态范围:HDR 可达 120dB(人眼约 90dB)
├── 视场角(FOV):30°(长焦)~ 120°(鱼眼)
└── 探测距离:200m~500m(取决于焦距)

实际应用中的挑战:

  • 逆光场景:太阳直射镜头时,图像出现光晕。解决方案:HDR 多帧合成 + 偏振滤镜
  • 雨雾天气:水滴附着镜头。解决方案:疏水涂层 + 摄像头清洗系统 + 算法去雾
  • 夜间场景:光照不足。解决方案:大光圈镜头 + 高感光度 CMOS + 红外补光

激光雷达(LiDAR)

工作原理:激光发射 → 遇到物体反射 → 接收器接收 → 计算飞行时间(ToF)→ 生成点云

关键参数:
├── 线数:16线 / 32线 / 64线 / 128线(Velodyne VLS-128)
├── 测距精度:±2cm(典型值)
├── 角分辨率:0.1°~0.4°
├── 探测距离:100m~300m
├── 点云密度:每秒 100 万~260 万个点
└── 转速:600~1200 RPM(旋转式)

点云数据结构:

# 一个典型的 LiDAR 点云数据结构
class PointCloud:
    def __init__(self):
        self.points = []  # N×3 数组:(x, y, z)
        self.intensity = []  # N×1 数组:反射强度
        self.timestamp = []  # N×1 数组:每个点的时间戳
        
    def to_bev_image(self, resolution=0.1):
        """将 3D 点云转换为鸟瞰图(BEV)"""
        x_min, x_max = -50, 50
        y_min, y_max = -50, 50
        width = int((x_max - x_min) / resolution)
        height = int((y_max - y_min) / resolution)
        bev = np.zeros((height, width), dtype=np.uint8)
        
        for point in self.points:
            x, y, z = point
            if x_min <= x <= x_max and y_min <= y <= y_max:
                col = int((x - x_min) / resolution)
                row = int((y - y_min) / resolution)
                bev[row, col] = min(255, int(z * 10 + 128))
        
        return bev

毫米波雷达(Radar)

工作原理:电磁波发射 → 遇到目标反射 → 接收天线接收 → 多普勒频移计算 → 目标距离与速度

关键参数:
├── 频段:77GHz(主流)/ 79GHz(新一代)
├── 探测距离:前方 200m+,侧方 80m+
├── 距离分辨率:3.75cm(79GHz 4GHz 带宽)
├── 速度分辨率:0.1m/s
├── 角分辨率:10°~15°(4D 成像雷达可达 1°)
└── 优势:全天候工作,直接测量径向速度

1.3 传感器标定

多传感器融合的前提是精确标定——知道每个传感器在车辆坐标系中的精确位置和姿态。

外参标定流程:

步骤 1:安装标定
├── 使用高精度全站仪测量各传感器的安装位置
├── 获得初始的平移向量 T 和旋转矩阵 R
└── 精度:±1cm 平移,±0.5° 旋转

步骤 2:在线自标定
├── 利用行驶中的自然特征(车道线、路沿、其他车辆)
├── 通过优化算法不断微调外参
├── 典型方法:基于互信息的 LiDAR-Camera 标定
└── 精度:±0.5cm 平移,±0.1° 旋转

第二章:点云处理与 3D 目标检测

2.1 点云预处理

原始点云包含大量噪声,需要预处理:

def preprocess_point_cloud(cloud):
    """点云预处理管线"""
    
    # 1. 去除地面点
    # 使用 RANSAC 拟合地面平面方程 ax + by + cz + d = 0
    ground_model = RANSAC(max_iterations=1000, distance_threshold=0.2)
    ground_model.fit(cloud.points)
    non_ground = cloud.remove_points(ground_model.inliers)
    
    # 2. 统计滤波去除离群点
    # 计算每个点到其 k 近邻的平均距离,剔除距离过大的点
    filtered = statistical_outlier_removal(non_ground, k=20, std_threshold=2.0)
    
    # 3. 体素化降采样
    # 将空间划分为 0.1m×0.1m×0.1m 的体素,每个体素保留一个代表点
    downsampled = voxel_grid_filter(filtered, voxel_size=0.1)
    
    return downsampled

2.2 PointPillars:实时 3D 目标检测

PointPillars 是工业界广泛使用的点云检测算法,兼顾精度与速度。

核心思想:将 3D 点云转换为 2D 伪图像,然后用成熟的 2D 卷积网络检测。

处理流程:

原始点云 (N × 4)  [x, y, z, intensity]
       ↓
[Feature Encoder]
├── 空间划分:将 XY 平面划分为 pillars(柱体)
├── 每个 pillar 内:
│   ├── 最多保留 P 个点(如 100 个)
│   ├── 对每个点计算扩展特征:
│   │   ├── (x, y, z) - 点坐标
│   │   ├── intensity - 反射强度
│   │   ├── (x_c, y_c, z_c) - 与 pillar 中心的偏移
│   │   └── (x_p, y_p) - 与 pillar 几何中心的偏移
│   └── 通过 PointNet 提取 pillar 特征 → 1 个特征向量
├── 输出:D × H × W 的伪图像
│   ├── D = 特征维度(如 64)
│   ├── H = Y 方向的 pillar 数量
│   └── W = X 方向的 pillar 数量
       ↓
[2D Backbone Network]
├── 多层 2D 卷积 + BatchNorm + ReLU
├── 多尺度特征融合(FPN 结构)
└── 输出:多通道特征图
       ↓
[Detection Head]
├── 分类分支:每个位置预测属于各类别的概率
├── 回归分支:预测 (x, y, w, l, θ, z, h) 
│   ├── (x, y) - 中心点偏移
│   ├── (w, l) - 宽高
│   ├── θ - 朝向角
│   └── (z, h) - 高度信息
└── 方向分类:消除朝向角的 180° 歧义

2.3 BEVFusion:多模态融合

BEVFusion 是当前最先进的多传感器融合框架之一。

融合策略:在鸟瞰图(BEV)空间进行特征融合

摄像头图像 (6 个视角)          LiDAR 点云
       ↓                           ↓
[Image Backbone]             [Point Encoder]
├── ResNet/Swin Transformer   ├── VoxelNet/PointPillars
├── 输出:多尺度特征图         └── 输出:BEV 特征
       ↓                           ↓
[View Transform]             [BEV Project]
├── LSS / BEVFormer          ├── 直接投影到 BEV
├── 将图像特征转换到 BEV 空间  └── 输出:BEV 特征
       ↓                           ↓
       └───────────┬───────────────┘
                   ↓
           [BEV Feature Fusion]
           ├── 拼接(Concat)或逐元素相加
           ├── Transformer 注意力融合
           └── 输出:融合后的 BEV 特征
                   ↓
           [Prediction Head]
           ├── 3D 目标检测
           ├── BEV 语义分割(车道线、可行驶区域)
           └── 运动预测(速度、航向变化率)

第三章:SLAM 与定位

3.1 为什么定位如此重要?

自动驾驶需要厘米级定位精度。GPS 的精度(3~5 米)远远不够——想象车辆在高速公路上,3 米的误差可能意味着不知道自己在哪条车道。

3.2 多源融合定位系统

定位系统架构:

┌─────────────────────────────────────────────────────────┐
│                    融合定位引擎                          │
│  ┌─────────────────────────────────────────────────┐   │
│  │  扩展卡尔曼滤波器(EKF)                         │   │
│  │  状态向量:[x, y, z, roll, pitch, yaw, vx, vy, vz] │
│  └─────────────────────────────────────────────────┘   │
│         ↑          ↑          ↑          ↑              │
│    ┌────┴───┐ ┌───┴────┐ ┌──┴───┐ ┌───┴────┐         │
│    │GNSS/RTK│ │  IMU   │ │ LiDAR│ │ 轮速计 │         │
│    │ 定位   │ │ 积分   │ │SLAM  │ │ 里程计 │         │
│    │±2cm    │ │短期精确│ │±5cm  │ │±0.5%   │         │
│    │但会跳变│ │但会漂移│ │全局一致│ │长期稳定│         │
│    └────────┘ └────────┘ └──────┘ └────────┘         │
└─────────────────────────────────────────────────────────┘

3.3 LiDAR SLAM 原理

核心思想:通过匹配连续帧的点云,估计车辆的运动轨迹,同时构建环境地图。

LiDAR SLAM 工作流程:

帧 N 的点云 P_n          帧 N+1 的点云 P_{n+1}
         ↓                        ↓
   [特征提取]               [特征提取]
   ├── 边缘特征             ├── 边缘特征
   │   (曲率大的点)        │
   └── 平面特征             └── 平面特征
       (曲率小的点)           (曲率小的点)
         ↓                        ↓
   [点云匹配] ←──────────── [初始位姿预测]
   ├── 边缘特征 ↔ 边缘线        (来自 IMU/里程计)
   ├── 平面特征 ↔ 平面
   └── ICP / NDT 优化
         ↓
   [位姿更新] → 输出 T_n(车辆在世界坐标系中的位姿)
         ↓
   [地图更新] → 将 P_n 转换到世界坐标系,加入全局地图

NDT(正态分布变换)匹配:

def ndt_matching(source_cloud, target_map, initial_pose):
    """
    NDT 匹配算法
    
    核心思想:将目标点云空间划分为体素,
    每个体素内用多元高斯分布 N(μ, Σ) 建模。
    优化源点云的变换,使变换后的点落在高斯分布概率最大的位置。
    """
    # 1. 构建目标地图的 NDT 网格
    voxel_size = 1.0  # 1m × 1m × 1m
    ndt_map = NDTMap(voxel_size)
    for voxel in target_map:
        points = voxel.get_points()
        mean = np.mean(points, axis=0)      # 均值 μ
        cov = np.cov(points.T)              # 协方差 Σ
        ndt_map.add_voxel(mean, cov)
    
    # 2. 优化变换参数
    pose = initial_pose.copy()
    for iteration in range(30):
        score = 0
        H = np.zeros((6, 6))  # Hessian 矩阵
        g = np.zeros(6)       # 梯度向量
        
        for point in source_cloud:
            # 将点变换到目标坐标系
            transformed = transform_point(point, pose)
            voxel = ndt_map.get_voxel(transformed)
            
            if voxel is None:
                continue
            
            # 计算概率密度
            diff = transformed - voxel.mean
            prob = np.exp(-0.5 * diff.T @ np.linalg.inv(voxel.cov) @ diff)
            
            # 累积梯度和 Hessian
            J = compute_jacobian(point, pose)  # 6×3 雅可比矩阵
            score += -np.log(prob + 1e-10)
            g += J.T @ (voxel.cov_inv @ diff) * prob
            H += J.T @ voxel.cov_inv @ J * prob
        
        # 牛顿法更新
        delta = -np.linalg.solve(H, g)
        pose = apply_delta(pose, delta)
        
        if np.linalg.norm(delta) < 1e-6:
            break
    
    return pose

第四章:行为规划与决策

4.1 分层规划架构

高层决策(Mission Planner)
├── 输入:起点、终点、路网拓扑
├── 任务:全局路径规划
├── 算法:A* / Dijkstra / 路网搜索
└── 输出:全局参考路径(由一系列路段组成)

中层规划(Behavior Planner)
├── 输入:全局路径 + 感知结果 + 交通规则
├── 任务:行为决策
│   ├── 跟车(Car Following)
│   ├── 变道(Lane Change)
│   ├── 超车(Overtaking)
│   ├── 路口通行(Intersection)
│   ├── 让行(Yielding)
│   └── 紧急避让(Emergency Avoidance)
├── 算法:有限状态机(FSM)/ 决策树 / 强化学习
└── 输出:行为指令(如"向左变道"、"减速让行")

底层规划(Motion Planner)
├── 输入:行为指令 + 障碍物位置 + 车辆运动学约束
├── 任务:生成可执行的轨迹
├── 算法:
│   ├── 基于采样的方法(Lattice Planner)
│   ├── 基于优化的方法(QP / NLP)
│   └── 基于搜索的方法(Hybrid A*)
└── 输出:时空轨迹 (x(t), y(t), θ(t), v(t), a(t))

4.2 有限状态机(FSM)实现

class AutonomousDrivingFSM:
    """自动驾驶行为决策有限状态机"""
    
    def __init__(self):
        self.current_state = "LANE_KEEPING"
        self.transitions = {
            "LANE_KEEPING": {
                "前方有慢车": "FOLLOWING",
                "左侧快车道空出": "CHANGE_LANE_LEFT",
                "右侧慢车道空出": "CHANGE_LANE_RIGHT",
                "前方路口": "APPROACHING_INTERSECTION",
                "行人横穿": "EMERGENCY_STOP",
            },
            "FOLLOWING": {
                "前车加速/变道离开": "LANE_KEEPING",
                "跟车距离足够,左侧空出": "CHANGE_LANE_LEFT",
                "前车急刹": "EMERGENCY_STOP",
            },
            "CHANGE_LANE_LEFT": {
                "变道完成": "LANE_KEEPING",
                "左侧突然出现车辆": "ABORT_LANE_CHANGE",
            },
            "APPROACHING_INTERSECTION": {
                "绿灯,无冲突": "CLEAR_INTERSECTION",
                "红灯": "STOP_AT_LIGHT",
                "有横向来车": "YIELD_AT_INTERSECTION",
            },
            # ... 更多状态
        }
    
    def update(self, perception, planning_context):
        """每个规划周期调用"""
        # 评估当前状态下的所有可能转移
        for condition, next_state in self.transitions[self.current_state].items():
            if self.evaluate_condition(condition, perception, planning_context):
                print(f"状态转移:{self.current_state} → {next_state}")
                self.current_state = next_state
                return next_state
        return self.current_state

第五章:实战项目

项目 1:点云可视化与分析

使用 Python + Open3D 处理真实 LiDAR 数据:

import open3d as o3d

# 加载点云
pcd = o3d.io.read_point_cloud("kitti_sample.pcd")

# 地面分割
_, inliers = pcd.segment_plane(distance_threshold=0.2, ransac_n=3, num_iterations=100)
ground = pcd.select_by_index(inliers)
objects = pcd.select_by_index(inliers, invert=True)

# 聚类
labels = np.array(objects.cluster_dbscan(eps=0.3, min_points=10))
colors = plt.cm.tab20(labels.astype(float) / (labels.max() + 1))
objects.colors = o3d.utility.Vector3dVector(colors[:, :3])

# 可视化
o3d.visualization.draw_geometries([ground, objects])

项目 2:简单的 PID 横向控制

class LateralPIDController:
    """横向控制:让车辆保持在车道中心"""
    
    def __init__(self, kp=1.0, ki=0.01, kd=0.1):
        self.kp = kp  # 比例系数
        self.ki = ki  # 积分系数
        self.kd = kd  # 微分系数
        self.integral = 0
        self.prev_error = 0
    
    def compute_steering(self, lateral_error, dt):
        """
        lateral_error: 车辆中心与车道中心的横向偏差(米)
                       正值表示偏右,负值表示偏左
        """
        self.integral += lateral_error * dt
        derivative = (lateral_error - self.prev_error) / dt
        self.prev_error = lateral_error
        
        steering_angle = (
            self.kp * lateral_error +
            self.ki * self.integral +
            self.kd * derivative
        )
        
        # 限制转向角度
        return np.clip(steering_angle, -0.5, 0.5)  # ±0.5 弧度

课后思考

  • 为什么特斯拉选择"纯视觉"方案(只用摄像头),而大多数自动驾驶公司选择"激光雷达+摄像头"融合方案?各自的优劣是什么?
  • 在暴雨天气下,哪些传感器会受到影响?如何设计冗余策略?
  • 如果 GPS 信号完全丢失(如长隧道),车辆如何维持定位精度?
  • PointPillars 为什么选择将 3D 点云转为 2D 伪图像,而不是直接在 3D 空间处理?

参考资源:
- Apollo 自动驾驶平台文档
- KITTI Vision Benchmark Suite
- nuScenes 数据集
- PointPillars 论文:Lang et al., "PointPillars: Fast Encoders for Object Detection from Point Clouds", CVPR 2019
- BEVFusion 论文:Liu et al., "BEVFusion: Multi-Task Multi-Sensor Fusion with Unified Bird's-Eye View Representation", ICRA 2023