OpenCV之全景图像拼接

1.实现流程

通过提取图像中的特征点并匹配这些特征点来找到图像间的对应关系,再通过透视变换(Homography)将两幅图像拼接成一幅完整的图像,大致流程:

  1. 特征点检测与描述
    • 使用 SIFT 算法检测两张图像中的关键点,并计算特征描述子(局部特征向量)
  2. 特征匹配
    • 采用 KNN 近邻匹配(k=2)找到两张图像间的最佳匹配点对
    • 使用 比值测试(ratio test) 过滤掉低质量的匹配点,减少误匹配
  3. 单应性矩阵计算
    • 通过 RANSAC 算法 计算 单应性矩阵 (Homography, H),剔除误匹配点
    • 单应性矩阵用于 透视变换,使一张图像对齐另一张图像
  4. 图像变换与拼接
    • 利用 cv2.warpPerspective() 对图像A进行透视变换,使其尽可能与图像B对齐
    • 图像B放入最终结果中,合成拼接图像
  5. 可视化匹配结果
    • 在两张图像上绘制匹配点连线,帮助观察特征匹配的效果

2.相关算法

1.SIFT(尺度不变特征变换)

SIFT (Scale-Invariant Feature Transform) 是一种 关键点检测和描述算法,能够在 不同尺度、旋转、光照变化 下仍然保持稳定的特征匹配。

主要步骤

  1. 尺度空间构造
    • 采用 高斯模糊(Gaussian Blur) 生成不同尺度的图像(高斯金字塔)
    • 计算 DoG(Difference of Gaussian, 差分高斯) 以找到特征点
  2. 关键点检测与精细定位
    • 通过 极值检测 在不同尺度下找到潜在特征点
    • 过滤掉低对比度点和边缘响应点,提高鲁棒性
  3. 方向分配
    • 计算特征点周围的 梯度方向直方图,分配主要方向,使其具备旋转不变性
  4. 特征描述
    • 计算每个关键点周围的 梯度直方图,形成 128 维特征向量(局部描述子)
    • 这些特征向量用于匹配不同图像中的关键点

特点

  • 尺度、旋转、光照不变性:可用于不同大小、旋转角度和亮度变化的图像匹配
  • 抗噪性强:在复杂背景下仍能准确提取关键点
  • 计算复杂度高:比其他方法(如 ORB)慢,但匹配效果更稳定

2.KNN 近邻匹配

KNN (K-Nearest Neighbors) 是一种用于 特征点匹配 的方法,它基于欧几里得距离计算两个图像之间的相似特征点

工作原理

  1. 计算欧几里得距离
    • 计算图像A中的每个特征点与图像B中所有特征点的欧几里得距离,找到最相似的 K 个点(通常 K=2)
  2. 比值测试(Ratio Test)
    • 计算最近邻 D1 和次近邻 D2 之间的距离比 D1/D2
    • 如果 D1/D2 < 设定阈值(通常 0.75),则认为匹配有效,否则丢弃该匹配点
    • 这样可以过滤掉误匹配点,提高匹配精度

特点

  • 简单高效:直接基于欧几里得距离计算匹配点
  • 容易受噪声干扰:误匹配点较多,因此需要 比值测试RANSAC 进行筛选

3.RANSAC(随机抽样一致性算法)

RANSAC (RANdom SAmple Consensus, 随机抽样一致算法) 是一种 鲁棒的误匹配去除方法,用于筛选正确的匹配点,提高拼接精度

工作原理

  1. 随机采样
    • 在匹配点集中 随机选取 4 对匹配点,计算一个变换模型(如单应性矩阵 H)。
  2. 计算内点数
    • 通过该变换模型,将所有匹配点进行投影,并计算它们与目标点的误差。
    • 误差小于阈值(如 4 像素)的点称为 内点(Inliers)
  3. 重复迭代
    • 反复进行 随机抽样+计算内点数 的过程,找到拥有最多内点的模型。
    • 最终使用这些内点 重新计算一个更精确的变换矩阵

特点

  • 能够过滤掉错误匹配点,提高拼接的精度。
  • 适用于特征匹配、物体识别、立体视觉等任务。
  • 计算复杂度较高,但效果很好。

4.透视变换(Homography)

透视变换 (Homography Transformation) 主要用于将一张图像变换到与另一张图像对齐的视角,从而实现图像拼接。

数学模型
透视变换矩阵 H 是一个 3×3 矩阵,用于描述两张图像之间的映射关系:

$\begin{bmatrix} x{\prime} \ y{\prime} \ w{\prime} \end{bmatrix} = H \cdot \begin{bmatrix} x \ y \ 1 \end{bmatrix}$

其中:

  • $(x, y)$是原始图像中的坐标。
  • $(x{\prime}, y{\prime})$ 是变换后图像的坐标。
  • cv2.findHomography() 通过匹配点计算 H,并用 cv2.warpPerspective() 对图像进行变换。

特点

  • 适用于拼接、增强现实(AR)、目标跟踪 等任务。
  • 需要 至少 4 对匹配点 才能计算出 H 矩阵。
  • 如果匹配点不够准确,会导致变换失败。

3.实现过程

1.获取特征点

检测图像中的特征点并计算这些特征点的描述子,使用 SIFT 算法来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def _detect_describe(image):  
# 创建SIFT特征检测器
descriptor = cv2.SIFT_create()

# 寻找图像中的特征点,计算这些关键点的特征描述子,用于后续的匹配
# kps 包含图像中所有关键点的信息(例如位置、尺度等)
# features 每个关键点的描述子,它用于描述关键点的外观特征
(kps, features) = descriptor.detectAndCompute(image, None)

# 关键点的坐标(x, y)
num_kps = np.float32([kp.pt for kp in kps])

# 关键点的坐标,关键点的描述子,关键点对象
return num_kps, features, kps

2.匹配特征点

用于匹配两幅图像的特征点,使用暴力匹配器(BFMatcher)和 k 最近邻(KNN)算法来找到两幅图像之间的特征点对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def _match_key_points(kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh):  
# 创建暴力匹配器,基于欧几里得距离来进行特征点匹配
matcher = cv2.BFMatcher()

# 为每个特征点找到两个最接近的匹配点(最近邻和次最近邻)
raw_matches = matcher.knnMatch(featuresA, featuresB, 2)

matches = []
for m in raw_matches:
# 检查最近的匹配点与次近的匹配点的距离比是否满足ratio阈值
# 如果是,则认为这两个特征点匹配成功
if len(m) == 2 and m[0].distance < m[1].distance * ratio:
# 存储两个点在featuresA, featuresB中的索引值
matches.append((m[0].trainIdx, m[0].queryIdx))

# 当筛选后的匹配对大于4时,计算视角变换矩阵
if len(matches) > 4:
# 获取匹配对的点坐标
pts_a = np.float32([kpsA[i] for (_, i) in matches])
pts_b = np.float32([kpsB[i] for (i, _) in matches])

# 通过RANSAC筛选出更准确的匹配点对,减少误匹配的影响
(H, status) = cv2.findHomography(pts_a, pts_b, cv2.RANSAC, reprojThresh)

# 返回结果
return matches, H, status

# 如果匹配对小于4时,返回None
return None

3.图像拼接

这里将两张图片横向拼接,并在匹配点之间绘制红色直线,帮助可视化匹配效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def _draw_matches(imageA, imageB, kpsA, kpsB, matches, status):  

# 初始化可视化图片,将A、B图左右连接到一起
(hA, wA) = imageA.shape[:2]
(hB, wB) = imageB.shape[:2]
vis = np.zeros((max(hA, hB), wA + wB, 3), dtype="uint8")
vis[0:hA, 0:wA] = imageA
vis[0:hB, wA:] = imageB

# 联合遍历,画出匹配对
for ((trainIdx, queryIdx), s) in zip(matches, status):
# 当点对匹配成功时,画到可视化图上
if s == 1:
pt_a = (int(kpsA[queryIdx].pt[0]), int(kpsA[queryIdx].pt[1]))
pt_b = (int(kpsB[trainIdx].pt[0]) + wA, int(kpsB[trainIdx].pt[1]))
# 在两幅图像之间的匹配特征点之间绘制红色线段,便于查看哪些点在两幅图像中是匹配的
cv2.line(vis, pt_a, pt_b, (0, 0, 255), 1)
return vis

4.调用

原图

最终效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def stitch(imageB, imageA, ratio=0.75, reprojThresh=4.0):  
# 提取SIFT特征:对两张图像分别检测特征点和计算描述子
(num_kpsA, featuresA, kpsA) = _detect_describe(imageA)
(num_kpsB, featuresB, kpsB) = _detect_describe(imageB)

# 找到匹配点对,并计算 单应性矩阵 H
m = _match_key_points(num_kpsA, num_kpsB, featuresA, featuresB, ratio, reprojThresh)

# 如果返回结果为空,没有匹配成功的特征点,退出算法
if m is None:
return None

# H是3x3视角变换矩阵
(matches, H, status) = m
# 透视变换,使两张图像对齐
result = cv2.warpPerspective(imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
utils.cv_show('result imageA', result) # 如下图
# 将图片B传入result图片最左端,最终形成一张拼接后的大图
result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB
utils.cv_show('result imageB', result) # 如下图
# 生成匹配图片
vis = _draw_matches(imageA, imageB, kpsA, kpsB, matches, status)
utils.cv_show('draw matches', vis) # 如下图
return result

4.总结

关键步骤包括:

  1. 检测并描述图像中的特征点
  2. 匹配特征点并计算单应性矩阵
  3. 通过透视变换将图像对齐,并拼接成一张新图像

5.备注

环境:

  • mac: 15.2
  • python: 3.12.4
  • numpy: 1.26.4
  • opencv-python: 4.11.0.86

资源和代码:

https://github.com/keychankc/dl_code_for_blog/tree/main/007_opencv_image_stitching