4. 亚像素匹配理论:抛物线拟合、高斯拟合、重心法、插值法、相位相关法

好,咱们进入正题。上一章我们聊了整像素匹配,说白了就是找到像素级别的对应点。但实际项目中,你会发现整像素精度根本不够用。我做过一个双目测距的项目,目标距离50米,整像素误差能导致测距偏差超过2米——这谁受得了?

所以,我们需要亚像素精度。也就是把匹配精度推到0.1像素、0.01像素甚至更高。今天我就把这几种主流方法掰开揉碎讲清楚。

核心思想:整像素匹配找到的是离散的整数坐标,亚像素匹配就是在这些整数点周围,用连续函数去拟合代价曲线,然后找到真正的极值点位置。

亚像素匹配方法 基于拟合的方法 基于插值的方法 频域方法 抛物线拟合 高斯拟合 重心法 双线性插值 双三次插值 相位相关法 精度:抛物线 < 高斯 ≈ 重心 < 插值 < 相位相关

4.1 抛物线拟合——最简单,也最容易翻车

抛物线拟合的原理很直观。你想想看,整像素匹配后,我们得到了一个代价曲线(比如SAD、NCC的响应值)。这个曲线在最优匹配点附近,形状通常像个倒扣的碗。那我们就用二次函数去拟合这个碗底。

具体做法是:取最优匹配点及其左右各一个点,共三个点,拟合一条抛物线。抛物线的顶点就是亚像素位置。

// 一维抛物线拟合,C++实现
double parabolicFit(double c0, double c1, double c2) {
    // c0: 左邻点代价, c1: 中心点代价, c2: 右邻点代价
    double denominator = c0 - 2 * c1 + c2;
    if (fabs(denominator) < 1e-10) return 0.0;  // 防止除零
    double delta = (c0 - c2) / (2.0 * denominator);
    return delta;  // 亚像素偏移量,相对于中心点
}

我的经验:抛物线拟合在代价曲线接近理想抛物线时效果不错。但我在一个纹理稀疏的项目中吃过亏——代价曲线太平坦,拟合出来的顶点位置飘忽不定。后来我加了一个判断:如果分母太小,就放弃亚像素修正,直接用整像素结果。

注意:抛物线拟合假设代价曲线是严格的二次函数。实际上,SAD、SSD的曲线更接近线性,NCC的曲线更接近高斯。所以抛物线拟合只适合精度要求不高(0.2~0.3像素)的场景。

4.2 高斯拟合——更符合实际情况

为什么说高斯拟合更合理?因为很多匹配代价(尤其是NCC和互信息)的响应曲线,在最优解附近确实服从高斯分布。我做过实验,用高斯拟合比抛物线拟合的精度平均提升0.05~0.1像素。

高斯拟合需要三个点,但公式稍微复杂一点。我们对高斯函数取对数,就变成了二次函数,然后套用抛物线拟合的思路。

// 一维高斯拟合,Python实现
import numpy as np

def gaussian_fit(c0, c1, c2):
    """
    c0, c1, c2: 三个点的代价(取对数前)
    返回亚像素偏移量
    """
    # 取对数,转换为二次形式
    # 注意:代价必须为正数
    if c0 <= 0 or c1 <= 0 or c2 <= 0:
        return 0.0
    
    ln_c0 = np.log(c0)
    ln_c1 = np.log(c1)
    ln_c2 = np.log(c2)
    
    denominator = ln_c0 - 2 * ln_c1 + ln_c2
    if abs(denominator) < 1e-10:
        return 0.0
    
    delta = (ln_c0 - ln_c2) / (2.0 * denominator)
    return delta

关键点:高斯拟合要求代价值为正数。如果你用的是SAD或SSD(值越小越好),需要先取倒数或取负,再套用高斯拟合。我习惯用 exp(-SAD / sigma) 做转换,sigma根据图像噪声水平调节。

4.3 重心法——简单粗暴但有效

重心法,也叫质心法。它的思路特别朴素:把代价曲线看作一个概率分布,然后求这个分布的"重心"位置。

公式很简单:

// 重心法,C++实现
double centroidFit(const std::vector<double>& costs, int center_idx) {
    double numerator = 0.0;
    double denominator = 0.0;
    
    for (int i = -1; i <= 1; i++) {
        int idx = center_idx + i;
        if (idx < 0 || idx >= costs.size()) continue;
        double w = costs[idx];
        numerator += i * w;
        denominator += w;
    }
    
    if (fabs(denominator) < 1e-10) return 0.0;
    return numerator / denominator;
}

重心法有个好处:它对噪声不敏感。因为它是加权平均,个别点的异常值会被平均掉。我在做工业检测时,经常用重心法做快速亚像素定位,速度比拟合方法快不少。

避坑指南:我曾经在低对比度图像上用过重心法,结果偏移量总是偏向图像中心。后来发现是背景噪声导致的偏差。解决办法:先对代价曲线做一个阈值处理,把低于噪声底限的值截断。

4.4 插值法——双线性与双三次

前面几种方法都是在代价空间上做拟合。插值法不一样,它直接在图像空间上操作。说白了,就是把图像变"密",然后做整像素匹配。

双线性插值:用周围4个像素,做线性加权。计算量小,但会引入平滑效应,边缘会模糊。

双三次插值:用周围16个像素,用三次多项式拟合。效果更好,但计算量是双线性的4~5倍。

// 双三次插值核函数,C++实现
double bicubicKernel(double x) {
    double a = -0.5;  // 常用参数
    x = fabs(x);
    
    if (x <= 1.0) {
        return (a + 2.0) * x * x * x - (a + 3.0) * x * x + 1.0;
    } else if (x < 2.0) {
        return a * x * x * x - 5.0 * a * x * x + 8.0 * a * x - 4.0 * a;
    }
    return 0.0;
}

我的建议:如果你做的是实时系统,用双线性插值就够了。但如果你做的是高精度三维重建,比如文物数字化,那必须上双三次。我做过对比,在纹理丰富的区域,双三次比双线性的亚像素精度高0.05像素左右。

4.5 相位相关法——频域里的"降维打击"

相位相关法跟前面几种方法完全不是一个路子。它不是在空间域做拟合,而是把图像变换到频域,利用傅里叶变换的平移性质。

核心原理:两幅图像如果存在平移,它们的傅里叶变换的幅度谱相同,只有相位谱有差异。这个相位差正好对应平移量。

// 相位相关法核心步骤,Python实现
import numpy as np
import cv2

def phase_correlation(img1, img2):
    # 1. 计算傅里叶变换
    f1 = np.fft.fft2(img1)
    f2 = np.fft.fft2(img2)
    
    # 2. 计算互功率谱
    cross_power = f1 * np.conj(f2)
    cross_power /= np.abs(cross_power) + 1e-10  # 归一化
    
    # 3. 逆变换得到相关峰
    correlation = np.fft.ifft2(cross_power)
    
    # 4. 找到峰值位置
    h, w = correlation.shape
    peak = np.unravel_index(np.argmax(np.abs(correlation)), (h, w))
    
    # 5. 亚像素峰值定位(用高斯拟合)
    # ... 省略具体实现
    
    return peak

注意:相位相关法对旋转和缩放敏感。如果两幅图像之间有旋转,直接做相位相关会失败。我遇到过这种情况,后来先用SIFT做粗匹配,再用相位相关做精匹配,效果就好了。

4.6 方法对比与选型建议

方法 精度 计算量 抗噪性 适用场景
抛物线拟合 0.2~0.3 px 极低 一般 快速粗定位
高斯拟合 0.1~0.2 px 较好 NCC匹配后精修
重心法 0.15~0.25 px 极低 工业检测、实时系统
双线性插值 0.1~0.2 px 中等 一般 实时系统
双三次插值 0.05~0.1 px 较高 较好 高精度重建
相位相关法 0.01~0.05 px 极好 卫星图像、医学图像

总结一下我的经验:没有银弹。我一般这样选——如果项目周期紧、精度要求不高,用抛物线拟合或重心法。如果精度要求高,先做整像素匹配,然后用双三次插值重采样,再做一次匹配。如果图像噪声大、需要亚0.1像素精度,那就上相位相关法。

嗯,这几种方法各有各的脾气。你可以在自己的数据集上跑一遍对比实验,看看哪种最适合你的场景。下一章我们聊聊亚像素匹配的工程实现细节,包括如何优化速度、如何处理边界效应——这些都是实际项目中绕不开的坑。


专注资料整理