# 前言
在 2020 年 5 月 30 号那天,我介绍了 ORB_FAST 特征关键点检测和 BRIEF 特征描述子匹配。实际上,它们都可以归为基于特征的图像对齐技术。在这里进行一个总结。
# 概述
如上图所示,基于特征的图像对其方法,会把手机拍摄的表单和表单的模板对齐。
这里用到的技术被称为基于特征的图像对齐,因为在这种技术中,要在一个图像中检测到一组特征点,并与另一张图像中的特征点相匹配。然后根据这些匹配的特征点计算出一个转换规则,从而将一个图像映射到另一个图像上。
# 什么是图像对齐或图像配准
图像对齐(也称图像配准)可以扭曲旋转(其实是仿射变换)一张图使它和另一个图可以很完美的对齐。
# 图像对齐的应用场景
- 表单对齐
- 在一些医学应用中,可能需要把多次拍摄的照片拼接起来
- 最有意思的应用应该就是合成全景图片了
# 图像对齐:理论基础
图像对齐技术的核心是一种维数 3 X 3 的单应性矩阵(Homography )。参考目录里有篇资料单独讲了它的相关理论。下面是一些简略的介绍。
# 单应性矩阵
要想使用单应性矩阵把同一个场景的两张照片联系起来,需要两个条件:
- 两张照片中拍摄了同一个平面
- 这两幅照片是通过旋转照相机的光学轴来拍摄的。我们在生成全景图的同时就是这么做的。
单应性矩阵就是一个简单的 3 X 3 矩阵
假设 (x1,y1) 是第一张图片上的点,(x2,y2) 是第二张图片上的同一个物理点,那么使用单应性矩阵可以映射他们:
# 如何找到单应性矩阵
如果我们知道两张图片中 4 个或者更多对应的特征关键点,就可以使用 opencv 中的 findHomography 来得到正两张图的单应性矩阵。findHomography 内部其实是通过解一组线性方程组来找到单应性矩阵的。
# 如何找到特征关键点
很多机器视觉需要找到图片中一些神奇的具有一定稳定性的点。这些点被称为关键点或者特征点。OpenCV 中实现了好几种关键点的检测器(e.g. SIFT, SURF, and ORB)。由于 SIFT(所以非自己编译版本的 OpenCV4 就不能使用这个检测器了) 和 SURF 收费,而 ORB 又快又准又免费,所以 ORB 也是越来越流行了。
ORB 特征检测器相关知识可以看我之前的总结:ORB_FAST 特征关键点检测
这里也总结一下。一个特征点检测器由两部分组成:
- 定位器。这个模块要找到图片上具有旋转不变性、缩放不变性及仿射不变性的点。定位器找到这些点的 (x, y) 坐标。ORB 的定位器被称为 FAST。这种寻找特征点的算法一听就知道,很快嘛。
- 上一步只是找到特征点在哪,这一步要获得它们的外观编码来区分彼此。这样特征点就可以使用被称为描述子的一串数字来表示了。理想情况下,不同照片上对应的同一个物理点应该具有相同的描述子。ORB 的描述子是一种改进的 BRISK 描述子。
由于只有知道两张图片特征点对应关系的情况下才能计算两图的单应性矩阵,所以我们需要一个算法来自动匹配两图的特征点。这个算法将会把两图的特征点一一比较。
# OpenCV 图像对齐代码
# 基于特征的图像对齐步骤
- 读取图片到内存
- 检测特征点。为两张图检测 ORB 特征点。为了计算单应性矩阵 4 个就够了,但是一般会检测到成百上千的特征点。可以使用参数 MAX_FEATURES 来控制检测的特征点的数量。检测特征点并计算描述子的函数是 detectAndCompute。
- 特征匹配。找到两图中匹配的特征点,并按照匹配度排列,保留最匹配的一小部分。然后把匹配的特征点画出来并保存图片到磁盘。我们使用汉明距离来度量两个特征点描述子的相似度。
- 计算单应性矩阵。上一步产生的匹配的特征点不是 100% 正确的,就算只有 20% ~ 30% 的匹配是正确的也不罕见。findHomography 函数使用一种被称为随机抽样一致算法 (Random Sample Consensus) 的技术在大量匹配错误的情况下计算单应性矩阵。
- 扭转图片 (Warping image)。有了精确的单应性矩阵,就可以把一张图片的所有像素映射到另一个图片。warpPerspective 函数用来完成这个功能。
# 代码演示
下面的代码来自《Image Alignment (Feature Based) using OpenCV (C++/Python)》
from __future__ import print_function | |
import cv2 | |
import numpy as np | |
MAX_FEATURES = 500 | |
GOOD_MATCH_PERCENT = 0.15 | |
def alignImages(im1, im2): | |
# Convert images to grayscale | |
im1Gray = cv2.cvtColor(im1, cv2.COLOR_BGR2GRAY) | |
im2Gray = cv2.cvtColor(im2, cv2.COLOR_BGR2GRAY) | |
# Detect ORB features and compute descriptors. | |
orb = cv2.ORB_create(MAX_FEATURES) | |
keypoints1, descriptors1 = orb.detectAndCompute(im1Gray, None) | |
keypoints2, descriptors2 = orb.detectAndCompute(im2Gray, None) | |
# Match features. | |
matcher = cv2.DescriptorMatcher_create(cv2.DESCRIPTOR_MATCHER_BRUTEFORCE_HAMMING) | |
matches = matcher.match(descriptors1, descriptors2, None) | |
# Sort matches by score | |
matches.sort(key=lambda x: x.distance, reverse=False) | |
# Remove not so good matches | |
numGoodMatches = int(len(matches) * GOOD_MATCH_PERCENT) | |
matches = matches[:numGoodMatches] | |
# Draw top matches | |
imMatches = cv2.drawMatches(im1, keypoints1, im2, keypoints2, matches, None) | |
cv2.imwrite("matches.jpg", imMatches) | |
# Extract location of good matches | |
points1 = np.zeros((len(matches), 2), dtype=np.float32) | |
points2 = np.zeros((len(matches), 2), dtype=np.float32) | |
for i, match in enumerate(matches): | |
points1[i, :] = keypoints1[match.queryIdx].pt | |
points2[i, :] = keypoints2[match.trainIdx].pt | |
# Find homography | |
h, mask = cv2.findHomography(points1, points2, cv2.RANSAC) | |
# Use homography | |
height, width, channels = im2.shape | |
im1Reg = cv2.warpPerspective(im1, h, (width, height)) | |
return im1Reg, h | |
if __name__ == '__main__': | |
# Read reference image | |
refFilename = "form.jpg" | |
print("Reading reference image : ", refFilename) | |
imReference = cv2.imread(refFilename, cv2.IMREAD_COLOR) | |
# Read image to be aligned | |
imFilename = "scanned-form.jpg" | |
print("Reading image to align : ", imFilename); | |
im = cv2.imread(imFilename, cv2.IMREAD_COLOR) | |
print("Aligning images ...") | |
# Registered image will be resotred in imReg. | |
# The estimated homography will be stored in h. | |
imReg, h = alignImages(im, imReference) | |
# Write aligned image to disk. | |
outFilename = "aligned.jpg" | |
print("Saving aligned image : ", outFilename); | |
cv2.imwrite(outFilename, imReg) | |
# Print estimated homography | |
print("Estimated homography : \n", h) |
# 结果展示
这是没对齐之前的图片(scanned-form)以及模板图片(form) 👇
特征描述子匹配 👇
原图与对齐后的图片 👇
# 写在最后
接下来会考虑用目前所学的知识实现全景图拼接,也是项目需要,敬请期待吧~