ORB-SLAM3视觉SLAM原理与ROS2实战

一、引言

SLAM(同时定位与建图)是机器人和AR的核心。ORB-SLAM3是视觉SLAM的SOTA方案,支持单目/双目/RGB-D+IMU融合,能在动态环境中鲁棒运行。本文将解析其三大线程并给出ROS2集成。

二、三线程架构

┌──────────────┐  帧  ┌──────────────┐  关键帧  ┌──────────────┐
│  Tracking    │─────→│ Local Mapping│────────→│ Loop Closing │
│  (实时跟踪)  │      │ (局部建图)   │         │ (回环检测)    │
│              │←─────│              │         │              │
│ ORB提取+匹配 │      │ BA优化       │         │ 词袋检测      │
│ 位姿估计     │      │ 新地图点生成 │         │ 全局BA        │
│ 重定位       │      │ 冗余帧剔除   │         │ 位姿图优化    │
└──────────────┘      └──────────────┘         └──────────────┘

三、ORB特征提取

import cv2
import numpy as np

class ORBExtractor:
    def __init__(self, n_features=1000, scale_factor=1.2, n_levels=8):
        self.orb = cv2.ORB_create(
            nfeatures=n_features,
            scaleFactor=scale_factor,
            nlevels=n_levels,
            edgeThreshold=31,
            patchSize=31,
            fastThreshold=20
        )
    
    def extract(self, image):
        """提取ORB特征点+描述子"""
        keypoints, descriptors = self.orb.detectAndCompute(image, None)
        
        # 四叉树均匀分布(ORB-SLAM独有,避免特征点聚集)
        keypoints = self._distribute_quadtree(keypoints, image.shape)
        
        return keypoints, descriptors
    
    def _distribute_quadtree(self, kps, shape, min_nodes=4):
        """四叉树均匀分布特征点"""
        h, w = shape[:2]
        nodes = [Node(0, 0, w, h, kps)]
        
        while len(nodes) < min_nodes:
            new_nodes = []
            for node in nodes:
                if len(node.kps) <= 1:
                    new_nodes.append(node)
                else:
                    n1, n2, n3, n4 = node.split()
                    new_nodes.extend([n1, n2, n3, n4])
            nodes = new_nodes
        
        # 每个节点保留最强响应点
        result = []
        for node in nodes:
            if node.kps:
                best = max(node.kps, key=lambda k: k.response)
                result.append(best)
        return result

四、PnP位姿估计

def estimate_pose_ransac(pts_3d, pts_2d, K):
    """EPnP + RANSAC: 3D-2D匹配估计相机位姿"""
    # 降级方案1: 用3D-2D PnP
    success, rvec, tvec, inliers = cv2.solvePnPRansac(
        pts_3d, pts_2d, K, None,
        iterationsCount=100,
        reprojectionError=4.0,
        confidence=0.99,
        flags=cv2.SOLVEPNP_EPNP
    )
    
    if success:
        R, _ = cv2.Rodrigues(rvec)
        T = np.eye(4); T[:3, :3] = R; T[:3, 3] = tvec.flatten()
        return T, len(inliers)
    
    # 降级方案2: 单应矩阵(平面场景)
    if len(pts_2d) >= 4:
        H, mask = cv2.findHomography(pts_3d[:, :2], pts_2d, cv2.RANSAC, 4.0)
        if H is not None:
            # 从H分解位姿
            solutions = cv2.decomposeHomographyMat(H, K)
            return solutions[0], mask.sum()
    
    return None, 0

五、局部BA优化

import g2o

class BundleAdjustment:
    def optimize(self, keyframes, mappoints, fixed_kf_id):
        optimizer = g2o.SparseOptimizer()
        solver = g2o.BlockSolverSE3(g2o.LinearSolverEigenSE3())
        optimizer.set_algorithm(g2o.OptimizationAlgorithmLevenberg(solver))
        
        # 添加相机顶点
        kf_vertices = {}
        for kf in keyframes:
            v = g2o.VertexSE3Expmap()
            v.set_id(kf.id)
            v.set_estimate(g2o.SE3Quat(kf.R, kf.t))
            v.set_fixed(kf.id == fixed_kf_id)
            optimizer.add_vertex(v)
            kf_vertices[kf.id] = v
        
        # 添加地图点顶点
        for mp in mappoints:
            v = g2o.VertexPointXYZ()
            v.set_id(mp.id + 10000)
            v.set_estimate(mp.position)
            v.set_marginalized(True)
            optimizer.add_vertex(v)
        
        # 添加重投影边
        for kf in keyframes:
            for mp_id, (kp, _) in kf.observations.items():
                if mp_id in mappoints:
                    edge = g2o.EdgeSE3ProjectXYZ()
                    edge.set_vertex(0, kf_vertices[kf.id])
                    edge.set_vertex(1, optimizer.vertex(mp_id + 10000))
                    edge.set_measurement(kp.pt)
                    edge.set_information(np.eye(2))
                    edge.set_parameter_id(0, K)  # 内参
                    optimizer.add_edge(edge)
        
        optimizer.initialize_optimization()
        optimizer.optimize(10)
        
        # 更新位姿和地图点
        for kf in keyframes:
            kf.set_pose(kf_vertices[kf.id].estimate())

六、回环检测(DBoW2词袋)

# 词袋模型:ORB描述子→视觉词汇→BoW向量
# 相似度>阈值 → 回环候选 → Sim3验证 → 位姿图优化

class LoopDetector:
    def __init__(self, vocabulary_path):
        self.vocab = cv2.BOWKMeansTrainer.load(vocabulary_path)
    
    def detect(self, current_frame, all_keyframes, min_score=0.3):
        # 编码当前帧
        current_bow = self._compute_bow(current_frame.descriptors)
        
        # 与所有历史关键帧比较
        candidates = []
        for kf in all_keyframes:
            score = self._bow_similarity(current_bow, kf.bow)
            if score > min_score:
                candidates.append((kf, score))
        
        # 组一致性检查(连续3帧检测到同组)
        groups = self._group_consistency(candidates)
        
        # 几何验证(Sim3)
        for group in groups:
            best_kf = max(group, key=lambda x: x[1])[0]
            # 3D-3D RANSAC验证
            if self._geometric_verification(current_frame, best_kf):
                return best_kf
        
        return None

七、ROS2集成

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from geometry_msgs.msg import PoseStamped

class SLAMNode(Node):
    def __init__(self):
        super().__init__('orb_slam3')
        self.slam = ORBSLAM3("/path/to/vocabulary.txt", "/path/to/config.yaml")
        
        self.create_subscription(Image, '/camera/color', self.rgb_callback, 10)
        self.create_subscription(Image, '/camera/depth', self.depth_callback, 10)
        self.pose_pub = self.create_publisher(PoseStamped, '/slam/pose', 10)
    
    def rgb_callback(self, msg):
        cv_image = self.bridge.imgmsg_to_cv2(msg, "bgr8")
        timestamp = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
        
        pose = self.slam.track_rgbd(cv_image, self.last_depth, timestamp)
        
        if pose is not None:
            pose_msg = PoseStamped()
            pose_msg.header = msg.header
            pose_msg.pose.position.x = pose[0, 3]
            pose_msg.pose.position.y = pose[1, 3]
            pose_msg.pose.position.z = pose[2, 3]
            self.pose_pub.publish(pose_msg)

八、总结

ORB-SLAM3三大创新:ORB特征提取+四叉树分布、三线程并行、DBoW2回环+全局BA。ROS2集成后可实现实时视觉定位建图。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐