废话不多说,直接进入正题。做图像配准和目标跟踪的朋友,大概率都跟OpenCV的仿射变换估计打过交道。平时我们用到最多的可能是estimateAffine2D,但在某些场景下,比如只涉及旋转、平移和等比缩放的刚体变换,还有一个更精准、更稳定的函数——estimateAffinePartial2D。很多人第一次看到这个函数名会有点懵,这个"Partial"到底偏在哪?跟estimateAffine2D有什么区别?什么场景该用它?它的坑又在哪里?
我自己第一次用这个函数,是在做一个视频稳像的小项目,当时被全景抖动搞得焦头烂额,换了好几种特征匹配方案都不理想。后来查资料发现,像视频稳像、医疗图像对齐这类只涉及小角度旋转和缩放的应用,其实用estimateAffinePartial2D才是更合理的选择。这篇文章就把我对这个函数的理解、实际踩过的坑、以及完整的实操流程整理出来,希望能帮到正在被这个"未知函数"困扰的读者。
1. 核心思路拆解:estimateAffinePartial2D到底在干什么
1.1 从名字理解"Partial"的含义
estimateAffinePartial2D从名字上看,它的意思是"部分仿射变换估计"。这里的"Partical"并不是说这个函数只能处理部分图像区域,而是指它估计的仿射变换模型是"部分"的、受限的。
完整的2D仿射变换通常用2x3矩阵表示,它有6个自由度:
code复制| a b c |
| d e f |
其中a、b、d、e这四个参数可以组合出旋转、缩放、剪切(shear)等各种线性变换,c和f代表平移。完全自由的仿射变换可以描述平行四边形的一切形变,包括图像被"斜着拉伸"的效果。
而estimateAffinePartial2D估计的变换矩阵形式是:
code复制| h0 h1 h2 |
| -h1 h0 h3 |
注意看,这个矩阵的左上角2x2部分是有约束的:主对角线元素相等(h0),副对角线元素互为相反数(h1和-h1)。这意味着它只能表示旋转、平移和等比缩放(uniform scaling),不允许出现剪切和不均匀缩放。这个模型也叫"相似变换"(similarity transformation),自由度从6降到了4。
1.2 为什么要单独搞一个"受限"的版本
这是很多人的第一个疑问:既然estimateAffine2D能估计完整的仿射变换,为什么还要弄一个受限制的版本?
背后的逻辑其实很实际。对于完全自由的仿射变换,算法在寻找最优解时,搜索空间更大,也就更容易受到噪声和误匹配点的影响,导致结果出现不自然的形变。而在很多真实应用场景中,物体的运动本质上就是刚体运动——比如无人机拍摄的视频画面变化,主要是相机旋转、平移和镜头缩放,并不会有画面本身被"剪切"的情况。
这时候如果用完整仿射变换去拟合,反而会过拟合。举个例子,我给你两组特征点,第一组是正确匹配的,第二组里面有少量误匹配,它们把人脸区域朝着错误的方向拉。完整的仿射变换可能会因为这几个误匹配点,把整个变换扭曲成一个非常奇怪的形状;但estimateAffinePartial2D只允许球面缩放和旋转,它对误匹配点的容忍度就更高,结果更接近物体的真实运动。
打个生活化的比方:你要在纸上描一个正方形,如果允许你任意变形,你可能为了迁就三个描歪的点,把正方形画成一个奇形怪状的四边形;但如果只允许你旋转、平移和放大缩小,那这个正方形始终是正方形,只是位置、方向和大小变了。
1.3 与estimateAffine2D的核心区别一览
这两个函数在OpenCV里非常容易混淆,我把它们的核心区别整理成表格,方便大家对照查看:
| 对比项 | estimateAffine2D | estimateAffinePartial2D |
|---|---|---|
| 变换自由度 | 6 | 4 |
| 支持旋转 | 支持 | 支持 |
| 支持平移 | 支持 | 支持 |
| 支持等比缩放 | 支持 | 支持 |
| 支持剪切变形 | 支持 | 不支持 |
| 支持非等比缩放 | 支持 | 不支持 |
| 适用场景 | 平面物体任意形变、文档矫正 | 视频稳像、相机运动估计、刚体匹配 |
| 抗误匹配能力 | 一般 | 更强 |
| 输出矩阵结构 | 2x3,所有参数独立 | 2x3,h0和h1被约束 |
这里要特别强调一点:estimateAffinePartial2D输出的虽然也是2x3矩阵,但你直接拿这个矩阵去乘以像素坐标,效果等效于"旋转+平移+缩放"。如果你硬要用它去拟合一个存在剪切形变的匹配点集,结果会不太理想,因为它的模型根本描述不了剪切。这不是函数的缺陷,而是模型假设的边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 参数细节与实操要点:从函数签名到返回值
2.1 函数签名逐个参数拆解
OpenCV中estimateAffinePartial2D的函数签名如下(以C++版本为例):
cpp复制cv::Mat cv::estimateAffinePartial2D(
InputArray from,
InputArray to,
OutputArray inliers = noArray(),
int method = cv::RANSAC,
double ransacReprojThreshold = 3.0,
size_t maxIters = 2000,
double confidence = 0.99,
size_t naps = 1000
);
Python版本对应:
python复制retval, inliers = cv2.estimateAffinePartial2D(
from,
to,
method=cv2.RANSAC,
ransacReprojThreshold=3.0,
maxIters=2000,
confidence=0.99,
naps=1000
)
下面逐个说参数:
from:源点集,一个N行2列的数组,每一行是一个特征点的坐标,数据类型通常是float32。to:目标点集,格式与from一致,与from中的点一一对应。inliers:内点索引输出。这是一个可选的输出参数,如果传了,函数会返回哪些点被判定为内点(1)和野点(0),这在调试误匹配时非常有用。method:估计方法。常见选项有cv2.RANSAC、cv2.LMEDS和cv2.RHO。默认是RANSAC,我在实际项目中绝大多数情况也用RANSAC。ransacReprojThreshold:RANSAC内点判定阈值,单位为像素。一个匹配对的重投影误差如果小于这个阈值,就被认为是内点。默认值3.0。maxIters:RANSAC最大迭代次数,默认2000。confidence:置信度,表示算法最终找到的模型有多大的概率是"真实模型",默认0.99。naps:RANSAC每次迭代随机选取的子集点数,默认是1000。注意这个参数在不同OpenCV版本中名字和含义有细微差别,有的版本里它表示每次抽取的样本数量上限,我通常保持默认。
2.2 返回值与矩阵结构的深入理解
函数返回一个2x3的仿射变换矩阵M。前面提到过,这个矩阵的结构是受限的:
code复制M = | h0 h1 h2 |
| -h1 h0 h3 |
从矩阵中可以直接解读出几何信息:
- 缩放因子:
scale = sqrt(h0^2 + h1^2) - 旋转角度:
theta = atan2(h1, h0) - 平移量:
h2和h3
这个特性很重要。在视频稳像中,我们经常需要把这个矩阵分解成旋转角度、缩放比例和平移距离,estimateAffinePartial2D让这个分解变得极其简单。要是用estimateAffine2D,分解出来的角度和缩放就会混入剪切的信息,变得很复杂。
在你拿到变换矩阵后,如果想对图像进行实际变换,可以用cv2.warpAffine:
python复制import cv2
import numpy as np
# 假设M是estimateAffinePartial2D算出来的矩阵
# M是2x3的float矩阵
h, w = img.shape[:2]
warped = cv2.warpAffine(img, M, (w, h))
注意warpAffine生成的图像大小需要自己指定,如果你想把整个变换后的图像都装进去,得根据矩阵对四个角点做投影,再计算包围盒尺寸。我一开始直接用了原图的宽高,结果图像边缘被裁掉了一部分,后来又补了包围盒的计算。
2.3 从矩阵反推旋转平移缩放:一个实用脚本
这里给一个我从矩阵中提取旋转、平移、缩放的实用脚本,视频稳像和目标跟踪里可以直接用:
python复制def decompose_similarity(M):
"""
将estimateAffinePartial2D输出的2x3矩阵分解为缩放、旋转和平移
"""
# M是2x3矩阵
a = M[0, 0]
b = M[0, 1]
c = M[1, 0] # 理论上等于 -b
scale = np.sqrt(a*a + b*b)
theta = np.arctan2(b, a) # 弧度
tx = M[0, 2]
ty = M[1, 2]
return scale, theta, tx, ty
这里注意一点:由于浮点计算误差,M[1,0]未必严格等于-M[0,1],所以提取缩放时只用第一行的a和b计算,这样更稳妥。
3. 完整实操:从特征匹配到图像配准的落地流程
3.1 准备特征点集:这一步决定了结果的天花板
estimateAffinePartial2D本身不负责找特征点,它只负责根据已有的匹配点对来估计变换矩阵。所以第一步就是准备特征点。
我常用的特征点获取方式,是根据项目需求来选的:
- 如果只是做离线图像配准,用
SIFT或ORB检测特征并做暴力匹配或者FLANN匹配。 - 如果是视频稳像,通常用
goodFeaturesToTrack或光流跟踪,得到相邻帧的对应点。 - 如果是已知的标定板角点,可以用
findChessboardCorners。
不管用哪种,最终都要拿到两个形状一致的矩阵:src_pts和dst_pts,都是N行2列。
这里分享一个用SIFT提取特征点的示例:
python复制import cv2
import numpy as np
def compute_matched_points(img1, img2):
# 初始化SIFT检测器
sift = cv2.SIFT_create()
# 检测关键点和计算描述子
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# 使用FLANN匹配器
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
flann = cv2.FlannBasedMatcher(index_params, search_params)
matches = flann.knnMatch(des1, des2, k=2)
# 使用Lowe's ratio test筛选好的匹配点
good_matches = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
# 提取匹配点的坐标
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 2)
return src_pts, dst_pts, good_matches
这里有个细节:src_pts和dst_pts必须是float32类型,如果你拿到的坐标是整数或者float64,建议先转换一下。数据类型的坑我踩过,OpenCV很多函数对类型敏感,类型不对轻则报错,重则计算结果完全错误。
3.2 调用estimateAffinePartial2D进行矩阵估计
拿到点集后,调用函数本身其实很简单:
python复制# 调用estimateAffinePartial2D
M, inliers = cv2.estimateAffinePartial2D(
src_pts,
dst_pts,
method=cv2.RANSAC,
ransacReprojThreshold=3.0
)
如果RANSAC找到了足够的支持点,M就是所求的变换矩阵,inliers是一个N行1列的数组,值为1代表内点,0代表野点。如果找不到满足条件的变换,M会是一个空矩阵。
这里要特别提醒:调用之后一定要检查M是否为空。我见过不少新手直接拿M去warpAffine,结果程序崩溃,或者在视频稳像中画面突然变成一团黑。
python复制if M is None:
print("变换估计失败,匹配点不足以拟合模型")
return None
3.3 图像warp与拼接:让变换矩阵真正发挥作用
估计出M之后,最典型的应用就是把一张图像变换到另一张图像的坐标系下,实现配准或拼接。以“将图2对齐到图1”为例:
python复制# 将图2按照变换矩阵M进行warp,得到对齐到图1的图
h, w = img1.shape[:2]
warped_img2 = cv2.warpAffine(img2, M, (w, h))
操作为什么要这样写?因为M的定义是"把src_pts映射到dst_pts",也就是从图2的点映射到图1的点,所以要把图2放进warpAffine的输入,目标尺寸用图1的尺寸。
如果你的两幅图像有较大平移,warpAffine默认的尺寸会裁掉移出去的部分。这时候可以用我自己常用的方式:
python复制def warp_with_full_size(img, M):
h, w = img.shape[:2]
# 对图像的四个角点做变换,计算新图像的包围盒
corners = np.float32([[0, 0], [w-1, 0], [0, h-1], [w-1, h-1]]).reshape(-1, 1, 2)
transformed_corners = cv2.perspectiveTransform(
corners.reshape(-1, 1, 2).astype(np.float32),
np.vstack([M, [0, 0, 1]])
)
# 这里用perspectiveTransform需要4x2矩阵,实际应用中可以直接用透视变换矩阵
其实更简单的做法是手动计算变换后的坐标范围,动态调整warpAffine的输出尺寸。这一步我在处理全景拼接时花了些时间调,一开始图省事直接输出原尺寸,结果拼接出来的图边缘缺了一块,后来加上包围盒计算就正常了。
3.4 一个完整可运行的配准Demo
把上面的步骤串起来,就是最简单的图像配准Demo:
python复制import cv2
import numpy as np
def align_images(img1, img2):
src_pts, dst_pts, _ = compute_matched_points(img1, img2)
if len(src_pts) < 4:
print("匹配点太少,无法估计变换")
return None
M, inliers = cv2.estimateAffinePartial2D(
src_pts, dst_pts,
method=cv2.RANSAC,
ransacReprojThreshold=3.0
)
if M is None:
print("估计失败")
return None
h, w = img1.shape[:2]
aligned = cv2.warpAffine(img2, M, (w, h))
return aligned
img1 = cv2.imread('reference.jpg')
img2 = cv2.imread('target.jpg')
result = align_images(img1, img2)
if result is not None:
cv2.imshow('Aligned', result)
cv2.waitKey(0)
这个Demo在两张图像内容有足够重叠、角度差不大、缩放比在合理范围内的情况下,直接跑通是没问题的。但如果你发现结果明显错位,大概率是特征匹配阶段出了问题,这个后面在排查章节详细说。
4. 不同方法选择与应用场景的匹配
4.1 RANSAC、LMEDS、RHO怎么选
estimateAffinePartial2D支持三种估计方法,很多人直接默认RANSAC,其实方法选择也是有讲究的。
先看RANSAC。它是随机抽样一致性算法,思路是:从所有匹配点中随机抽取小部分点,拟合一个候选变换,然后统计其他点到这个变换的误差,把误差小的点记为内点,内点数量多的候选变换胜出。RANSAC容错率高,对误匹配点比例较高的场景非常有效,代价是迭代耗时。
再看LMEDS(最小中值法)。它试图找到一种变换,使得所有点到变换的重投影误差的"中值"最小。如果误匹配点数量没有超过一半,LMEDS给出的结果比较稳健。但它在误匹配特别多的情况下效果不如RANSAC,而且计算量也不小。
最后是RHO,这是RANSAC的一种变体,采用了更聪明的抽样策略和预检验机制,在匹配质量普遍较好的场景下速度更快。
我自己的经验是:当误匹配率比较高(比如光线变化剧烈、重复纹理多)时,优先RANSAC;当匹配质量尚可、但希望结果不要受少数极端值影响时,可以试试RHO。LMEDS我用的最少。
4.2 阈值、置信度与迭代次数的调参心得
ransacReprojThreshold是影响结果最直接的参数。它的含义是:对于一对匹配点,按当前候选变换映射到目标坐标系后,与目标点的距离如果小于这个值,就算内点。单位是像素。
默认3.0像素意味着,在分辨率不高的图像中,3个像素以内的重投影误差可以接受。如果你的图像分辨率很高(比如4K),或者特征点本身有一定的定位噪声,可以适当加大到5甚至8。但如果你把阈值调得太大,RANSAC会把很多误匹配点也当成内点,导致拟合出的变换不够精准。
maxIters和confidence是一对搭配参数。迭代次数越多,找到最优模型的可能性越大,但耗时也越长。默认2000次迭代配合0.99置信度,在多数场景下已经足够。我做视频稳像的时候,为了追求实时性,会手动把maxIters降到1000,实测在普通笔记本上,配1280x720的视频,单帧耗时能降下来一小截,效果几乎没差别。
4.3 精度提升技巧:两阶段估计
这里分享一个我从实际项目中总结出来的两阶段估计技巧。第一次调用estimateAffinePartial2D后,拿到inliers,把野点剔除,只用内点再次调用一次估计函数。这样做的道理是:第一次RANSAC的模型可能受野点干扰,虽然整体方向对了,但精度不够;剔除野点后,用干净的内点重新拟合,结果会精准很多。
代码很简单:
python复制M, inliers = cv2.estimateAffinePartial2D(
src_pts, dst_pts,
method=cv2.RANSAC,
ransacReprojThreshold=3.0
)
# 两阶段精修:只保留内点重新估计
if inliers is not None and np.sum(inliers) > 4:
src_inliers = src_pts[inliers.ravel() == 1]
dst_inliers = dst_pts[inliers.ravel() == 1]
M_refined, _ = cv2.estimateAffinePartial2D(
src_inliers, dst_inliers,
method=cv2.RANSAC,
ransacReprojThreshold=1.0
)
if M_refined is not None:
M = M_refined
实测下来,在特征定位噪声较小的情况下,两阶段估计能把旋转角度误差降低三分之一左右。当然,如果匹配点质量本来就很高,这个技巧带来的提升就有限,甚至可能因为重新估计引入新的数值误差。
5. 常见问题与排查技巧实录
5.1 调用后返回空矩阵
这是所有问题里最常见的。estimateAffinePartial2D返回空矩阵,意味着RANSAC没能从当前匹配点中找出足够内点的模型。原因通常是:
- 匹配点太少,少于模型求解的最小需求,或者有效内点数量不足。
- 匹配点质量太差,大量误匹配把正确模型淹没了。
ransacReprojThreshold设得太小,导致正确匹配的重投影误差也超过阈值。
排查方法:先打印src_pts和dst_pts的长度,以及matches的分布。如果匹配点数量低于20,就需要从特征提取阶段找问题。如果特征点数量挺多但就是估计不出来,可以把阈值从3调大到8试试,看能不能有结果,如果调大后有结果,说明原始匹配噪声偏大,需要优化特征匹配而不是继续调参数。
5.2 估计出的变换矩阵明显错误
矩阵不空,但结果明显不对,比如旋转方向反了、缩放比例不对。这个多半是src_pts和dst_pts顺序反了。
我自己就犯过这个错误:把src_pts当作图1的点,把dst_pts当作图2的点,但函数参数字面量上都是from和to,很容易混淆语义。你要知道,这里from是输入点,to是映射目标点。estimateAffinePartial2D(from, to)求的是把from映射到to的变换。如果你做反了,出来的矩阵就是逆变换的结果,图像warp出来方向、角度、位置全都对不上。
解决方式很直接:在调用前打印前几个点的坐标,在图像上可视化对应关系,确认匹配方向是一致的。
5.3 处理速度太慢,实时性不足
如果estimateAffinePartial2D调用的是高分辨率图像提取出来的数千个特征点,RANSAC的迭代成本会明显上升。在视频处理里,单帧处理时间如果超过50毫秒,就会明显感觉到卡顿。
优化思路有几个方向:
- 降低
maxIters迭代次数,从2000降到500,配合confidence适当降低。 - 先用网格筛选特征点,让特征点在图像上分布更均匀,数量控制在500以内。
- 对于视频相邻帧之间的变换,上一帧的变换矩阵可以作为先验,缩小搜索范围。
naps参数默认是1000,如果匹配点数量本身就不到1000,这个参数不会影响什么,但如果匹配点数量远超1000,可以适度降低来限制每次抽样的规模。
5.4 多视角自检清单
把这个函数做成标准模块后,我每次遇到新数据集,都会按照固定清单自查:
- 图像内容是否有足够的纹理特征,特征点数量是否足够(少于15个基本别想靠RANSAC)。
from和to的顺序语义是否理解正确。- 输入点坐标类型是否为
float32。 ransacReprojThreshold是否与图像分辨率匹配(超高分辨率建议5到8)。- 调用时是否检查返回的
M为空的情况。 - 结果可视化时,是否把内点与外点分别画出来,确认RANSAC的筛选质量。
这套清单帮我避开了好多隐蔽的坑,尤其是那种"偶尔出错但很难复现"的问题,多半是匹配质量波动加阈值设得太死导致的。
5.5 一个小技巧:直接可视化内点分布
很多人不看inliers就直接用结果,其实这里的输出信息量非常大。把内点和野点分别画在原图上,你就能一眼看出RANSAC认为哪些匹配是可靠的。
python复制import matplotlib.pyplot as plt
def plot_inliers(img1, img2, src_pts, dst_pts, inliers):
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
for ax, img, pts, color in [(axes[0], img1, src_pts, 'red'),
(axes[1], img2, dst_pts, 'blue')]:
ax.imshow(img, cmap='gray')
for idx, pt in enumerate(pts):
if inliers[idx] == 1:
ax.plot(pt[0], pt[1], 'o', color=color, markersize=3)
else:
ax.plot(pt[0], pt[1], 'x', color='yellow', markersize=3)
plt.show()
如果你发现内点虽然数量多但分布集中在一小片区域,比如全挤在图像左下角,那估计出的变换可能会对这片区域过拟合,对图像其他区域的对齐效果很差。这时候需要在特征提取阶段加入均匀化策略。
6. 从函数到应用:几个典型的落地场景
6.1 视频稳像
视频稳像是我最早使用estimateAffinePartial2D的场景。思路是:对视频相邻帧提取特征点,用光流跟踪,再把上一帧到当前帧的特征点对应关系交给这个函数,估算出帧间运动。把每一帧相对于参考帧的累积变换算出来后,逆向补偿,就能得到稳定的视频。
由于真实相机的运动绝大多数是旋转、平移外加少量缩放,用部分仿射变换去建模非常合适。如果用完整仿射变换,反而会把画面偶尔的微小剪切变形当成实际运动来补偿,输出结果会呈现不太自然的"呼吸感"。
6.2 多视角图像拼接
做全景拼接时,需要对多张有重叠区域的图像做对齐。传统的拼接算法用完整仿射变换或单应矩阵,但如果拍摄时相机主要做了旋转运动,使用相似变换模型能够减少拼接接缝处的畸变。
我之前用手机围绕一个雕塑拍了一圈,用完整仿射变换拼接出的全景,雕塑主体的线条稍微有点扭曲,换用estimateAffinePartial2D后,整体形状保持得更好,接缝附近的伪影也少了。当然,如果拍摄时存在明显的透视变化,单应矩阵仍然是更合适的模型,这个要根据场景灵活选择。
6.3 医学影像对齐与目标跟踪初始化
在医学影像中,比如同一个患者在不同时间的CT影像,或者在超声波图像中追踪探头移动,运动模型也以平移、旋转和缩放为主,很少会出现真实的剪切变形。用estimateAffinePartial2D做初步对齐,再用更精细的非刚性配准去微调,是一种很常见的流水线设计。
在目标跟踪领域,当跟踪目标发生明显的尺度变化和旋转时,可以用这个函数估计一个简单的相似变换来更新目标框。我之前见过有人直接用完整仿射变换去更新目标框,结果目标框很快被背景区域带偏,换成部分仿射变换后稳健了不少,这就是模型先验的力量。
6.4 与单应矩阵的界限
最后特别提醒一句:estimateAffinePartial2D处理的是平面上的相似变换,它跟findHomography的单应矩阵是两码事。单应矩阵适用于平面场景在不同视角下的投影关系,它有8个自由度,可以表达透视变形。如果你的两幅图像之间存在明显的视角变化(比如拍摄角度不同),单靠4自由度的相似变换是搞不定的。
选择模型的核心依据是"你期望物体在图像中发生什么样的运动"。如果运动被物理约束为刚体,用相似变换;如果物体是平面结构,但拍摄视角在变,用单应;如果物体本身在三维空间自由运动,透视模型都未必够,可能需要基础矩阵或本质矩阵。
7. 最后的实际操作体会与建议
说回我自己。estimateAffinePartial2D这个函数看起来很简单,但真正用好它,需要理解背后的模型假设、参数含义和适用边界。我跟它打了这么久交道,最深刻的体会就是:不要图省事、什么场景都用estimateAffine2D,也不要什么场景都硬套estimateAffinePartial2D。先想清楚你的运动模型到底是什么,再选函数,这样出来的结果才经得起推敲。
还有一个细节想重点强调:每次拿到变换矩阵后,建议顺手把它分解成缩放、旋转角和平移量,可视化对比一下。比如在视频稳像中,如果发现相邻帧之间的缩放量超过了2%,那基本可以确定特征点提取或匹配出了异常,因为真实相机在几十毫秒内的缩放不可能这么大。这个"合理性检查"能帮你提前暴露问题,省去后面一大串排查时间。
最后再分享一个搭配建议:如果你在实际项目中被迫用了estimateAffinePartial2D却发现效果不如预期,先别急着换函数。把特征提取阶段的对比度归一化做好,把误匹配率压下来,比更换任何估计函数都有效。RANSAC再强大,也架不住输入里一半以上都是野点。
希望这篇文章能把estimateAffinePartial2D从"未知函数"变成你工具箱里一个顺手好用的工具。下次再遇到需要估计旋转平移缩放的场景,不妨直接想到它,并且记得:先确认运动模型,再动代码。这样能省下大量不必要的调试时间。
