3. 激光SLAM回环检测基础

激光SLAM回环检测基础:基于直方图描述子的回环候选生成 学习卡片

任务目标

实现一个基于点云直方图的全局描述子提取与匹配模块,用于激光SLAM中的回环检测。给定两帧点云,提取其直方图特征,通过特征相似度判断是否构成回环候选。

核心思路

将三维点云投影到球坐标系,按方位角、俯仰角划分网格,统计每个网格内点的分布(距离均值或点数),形成一维直方图向量作为全局描述子。匹配时计算两描述子的余弦相似度或卡方距离,超过阈值则判定为回环候选。

关键完整代码

import numpy as np
from scipy.spatial import KDTree

class PointCloudHistogramDescriptor:
    """基于球坐标网格统计的点云全局描述子"""
    def __init__(self, azim_bins=36, elev_bins=18, max_range=50.0):
        self.azim_bins = azim_bins      # 方位角分桶数 (360°/36=10°每桶)
        self.elev_bins = elev_bins      # 俯仰角分桶数
        self.max_range = max_range      # 有效距离上限(m)
        self.azim_step = 2 * np.pi / azim_bins
        self.elev_step = np.pi / elev_bins

    def compute(self, points):
        """
        输入: points (N,3) 点云坐标
        输出: desc (azim_bins*elev_bins,) 归一化直方图描述子
        """
        # 过滤距离过远的点
        dist = np.linalg.norm(points, axis=1)
        mask = (dist > 0.1) & (dist < self.max_range)
        pts = points[mask]
        if len(pts) == 0:
            return np.zeros(self.azim_bins * self.elev_bins)

        # 转球坐标
        x, y, z = pts[:, 0], pts[:, 1], pts[:, 2]
        r = np.linalg.norm(pts, axis=1)
        azim = np.arctan2(y, x) + np.pi          # [0, 2π)
        elev = np.arccos(np.clip(z / r, -1, 1))  # [0, π]

        # 分桶索引
        azim_idx = np.floor(azim / self.azim_step).astype(int)
        elev_idx = np.floor(elev / self.elev_step).astype(int)
        azim_idx = np.clip(azim_idx, 0, self.azim_bins - 1)
        elev_idx = np.clip(elev_idx, 0, self.elev_bins - 1)

        # 统计每桶内平均距离(比点数更鲁棒)
        flat_idx = azim_idx * self.elev_bins + elev_idx
        hist = np.zeros(self.azim_bins * self.elev_bins)
        for i in range(len(flat_idx)):
            hist[flat_idx[i]] += r[i]
        # 归一化
        norm = np.linalg.norm(hist)
        if norm > 0:
            hist /= norm
        return hist

    @staticmethod
    def match_score(desc1, desc2):
        """余弦相似度,范围[-1,1],越大越相似"""
        return float(np.dot(desc1, desc2))

# ---------- 最小可复现验证 ----------
def generate_test_cloud(seed=0, yaw_offset=0.0):
    """生成带随机噪声的环形点云,模拟同一位置不同视角"""
    rng = np.random.default_rng(seed)
    n = 2000
    # 环形结构 + 随机噪声
    theta = rng.uniform(0, 2*np.pi, n)
    radius = 5.0 + rng.normal(0, 0.3, n)
    z = rng.normal(0, 0.5, n)
    x = radius * np.cos(theta)
    y = radius * np.sin(theta)
    pts = np.stack([x, y, z], axis=1)
    # 绕Z轴旋转模拟视角变化
    c, s = np.cos(yaw_offset), np.sin(yaw_offset)
    R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
    return pts @ R.T

if __name__ == "__main__":
    # 1. 同一位置不同视角 → 应高相似度
    cloud_a = generate_test_cloud(seed=42, yaw_offset=0.0)
    cloud_b = generate_test_cloud(seed=42, yaw_offset=0.15)  # ~8.6°偏航

    # 2. 不同位置 → 应低相似度
    cloud_c = generate_test_cloud(seed=7, yaw_offset=0.0)

    desc_extractor = PointCloudHistogramDescriptor(azim_bins=36, elev_bins=18)

    desc_a = desc_extractor.compute(cloud_a)
    desc_b = desc_extractor.compute(cloud_b)
    desc_c = desc_extractor.compute(cloud_c)

    score_same = PointCloudHistogramDescriptor.match_score(desc_a, desc_b)
    score_diff = PointCloudHistogramDescriptor.match_score(desc_a, desc_c)

    print(f"同一位置(视角差8.6°) 相似度: {score_same:.4f}")
    print(f"不同位置              相似度: {score_diff:.4f}")
    print(f"判定阈值建议: 0.85")
    print(f"回环检测结果: 同一位置→{score_same > 0.85}, 不同位置→{score_diff > 0.85}")

运行与验证

依赖安装:

pip install numpy scipy

运行方式:

python histogram_descriptor.py

通过条件:程序输出中,score_same应明显高于score_diff(通常前者>0.9,后者<0.7),且回环检测结果两行均为True。若差异不明显,可增大azim_bins或减小噪声幅度。

关键说明

  • 描述子维度为 azim_bins × elev_bins(本例36×18=648维),对点云密度不敏感,但对视角旋转敏感——绕Z轴旋转会导致直方图循环移位,实际工程中可结合傅里叶变换或滑动匹配处理。
  • 本实现采用“桶内平均距离”作为统计量,相比纯点数统计对距离噪声更鲁棒。若场景为室内结构化环境,可改用桶内点数并配合归一化。
  • 回环判定阈值需根据传感器噪声和场景自适应性调整,建议在真实数据上统计同类/异类相似度分布后选取。