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