2. 基于特征点匹配的全局运动估计

任务说明

本案例实现运动相机视频稳像的第一级核心:通过ORB特征点检测与匹配,估计相邻帧间的全局运动(仿射变换),为后续的帧间平滑与裁剪补偿提供运动参数。输入为一段含轻微抖动的视频帧序列,输出为每相邻两帧间的仿射变换矩阵(2×3),并可视化特征匹配结果以验证估计正确性。

实现方案

采用OpenCV的ORB检测器提取相邻帧特征点,使用BFMatcher(Hamming距离)进行暴力匹配,经比率测试与RANSAC筛选后,通过estimateAffinePartial2D估计6自由度仿射变换。该变换可描述平移、旋转与各向同性缩放,足以覆盖运动相机常见的抖动模式(高频平移+小幅旋转)。

帧间运动估计流程 Frame t ORB特征提取 Frame t+1 ORB特征提取 BFMatcher匹配 比率测试(0.75) RANSAC筛选 仿射矩阵 M (2×3) [a b tx; c d ty] 用于后续帧间平滑 输入: 视频帧序列 (灰度图) 输出: 相邻帧间仿射矩阵序列 M[0..N-2] 验证: 特征匹配可视化 + 仿射误差阈值 EIS Pipeline Stage 1/3

核心代码

import cv2
import numpy as np

def estimate_affine_flow(frame1, frame2, max_features=1000):
    """
    估计相邻帧间的仿射变换矩阵
    参数:
        frame1, frame2: 灰度图 (H, W) uint8
        max_features: ORB最大特征点数
    返回:
        M: 2x3 仿射矩阵 (成功) 或 None (失败)
        good_matches: 筛选后的匹配对列表
        keypoints1, keypoints2: 对应关键点
    """
    # 1. ORB特征检测
    orb = cv2.ORB_create(nfeatures=max_features, scaleFactor=1.2, nlevels=8)
    kp1, des1 = orb.detectAndCompute(frame1, None)
    kp2, des2 = orb.detectAndCompute(frame2, None)
    
    if des1 is None or des2 is None or len(kp1) < 10 or len(kp2) < 10:
        return None, [], kp1, kp2
    
    # 2. 暴力匹配 (Hamming距离)
    bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
    matches = bf.knnMatch(des1, des2, k=2)
    
    # 3. Lowe比率测试筛选
    good = []
    for m, n in matches:
        if m.distance < 0.75 * n.distance:
            good.append(m)
    
    if len(good) < 8:
        return None, [], kp1, kp2
    
    # 4. 提取匹配点坐标
    src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
    dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
    
    # 5. RANSAC估计仿射变换 (平移+旋转+各向同性缩放)
    M, inlier_mask = cv2.estimateAffinePartial2D(
        src_pts, dst_pts, method=cv2.RANSAC, ransacReprojThreshold=3.0
    )
    
    if M is None:
        return None, [], kp1, kp2
    
    # 6. 保留内点匹配
    inlier_matches = [g for g, inl in zip(good, inlier_mask.ravel()) if inl]
    return M, inlier_matches, kp1, kp2

def visualize_matches(frame1, frame2, kp1, kp2, matches, max_draw=50):
    """绘制特征匹配连线图"""
    vis = cv2.drawMatches(
        frame1, kp1, frame2, kp2, matches[:max_draw], None,
        flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
        matchColor=(0, 255, 0), singlePointColor=(0, 0, 255)
    )
    return vis

# ============ 示例调用与验证 ============
if __name__ == "__main__":
    # 生成模拟抖动帧对 (仅用于验证代码逻辑)
    h, w = 240, 320
    rng = np.random.default_rng(42)
    
    # 创建带纹理的背景
    base = rng.integers(0, 255, (h, w), dtype=np.uint8)
    base = cv2.GaussianBlur(base, (5, 5), 0)
    
    # 帧1: 原始视角
    frame1 = base.copy()
    
    # 帧2: 模拟相机运动 (平移+旋转)
    theta = np.deg2rad(1.5)  # 1.5度旋转
    tx, ty = 3.0, -2.0       # 平移量
    M_true = np.array([
        [np.cos(theta), -np.sin(theta), tx],
        [np.sin(theta),  np.cos(theta), ty]
    ], dtype=np.float32)
    frame2 = cv2.warpAffine(frame1, M_true, (w, h), borderMode=cv2.BORDER_REPLICATE)
    
    # 估计仿射变换
    M_est, inliers, kp1, kp2 = estimate_affine_flow(frame1, frame2)
    
    if M_est is not None:
        print("估计的仿射矩阵 M_est:")
        print(np.round(M_est, 4))
        print(f"\n内点匹配数: {len(inliers)}")
        
        # 计算估计误差 (与真实变换对比)
        error = np.linalg.norm(M_est - M_true)
        print(f"与真实变换的Frobenius误差: {error:.4f}")
        print(f"误差 < 0.5 判定通过: {error < 0.5}")
        
        # 保存可视化结果
        vis = visualize_matches(frame1, frame2, kp1, kp2, inliers)
        cv2.imwrite("match_visualization.jpg", vis)
        print("匹配可视化已保存: match_visualization.jpg")
    else:
        print("特征匹配失败,尝试调整参数或检查输入帧")

落地与验证

运行方式:安装OpenCV(pip install opencv-python numpy),直接执行上述脚本。脚本会生成一对模拟抖动帧(1.5°旋转+平移),估计其仿射变换并输出误差。

验证方法:观察终端输出的内点匹配数(应大于20)和误差值(应小于0.5)。打开match_visualization.jpg,应看到大量绿色连线均匀分布在画面各处,且连线方向一致,表明匹配正确。

判定标准:估计矩阵与真实矩阵的Frobenius范数误差小于0.5,且可视化中无明显错误匹配(交叉连线)。对于真实视频,可将相邻帧传入estimate_affine_flow,输出矩阵序列即为后续平滑模块的输入。