Opencv基于特征点的图像对齐

二维图像之间的单应变换

图像中的2D点(x,y)(x,y)可以被表示成3D向量的形式$(x_1,x_2,x_3)$,其中$x=\frac{x_1}{x_3}$,$y=\frac{x_2}{x_3}$。它被叫做点的齐次表达,位于投影平面$P^2$上。所谓单应就是发生在投影平面$P^2$上的点和线可逆的映射。其它叫法包括射影变换、投影变换和平面投影变换等。

单应变换矩阵是一个3*3的矩阵H。这个变换可以被任意乘上一个非零常数,而不改变变换本身。它虽然具有9个元素,但是具有8个自由度。这意味这它里面有8个未知参数待求。

典型地,可以通过图像之间的特征匹配来估计单应矩阵。

单应和齐次坐标

一个单应矩阵是大小为3*3的矩阵$H = \begin{bmatrix} h_{11} & h_{12} & h_{13}\ h_{21} & h_{22} & h_{23}\ h_{31} & h_{32} & h_{33}\end{bmatrix}$,满足给定一个点 $P_1 = \left[ \begin{matrix} x_1 \ y_1 \ w_1 \end{matrix} \right]$ ,矩阵H把点$P_1$变换成一个新的点$P_2 = \left[ \begin{matrix} x_2 \ y_2 \ w_2 \end{matrix} \right] = \begin{bmatrix} h_{11} & h_{12} & h_{13}\ h_{21} & h_{22} & h_{23}\ h_{31} & h_{32} & h_{33}\end{bmatrix}\cdot \left[ \begin{matrix} x_1 \ y_1 \ w_1 \end{matrix} \right]$ 。由于他们都是齐次坐标,对应在图像上的两个点分别是$\left[ \begin{matrix} \frac{x_1}{w_1} \ \frac{y_1}{w_1}\end{matrix} \right]$,$\left[ \begin{matrix} \frac{x_2}{w_2} \ \frac{y_2}{w_2}\end{matrix} \right]$ 。

单应的自由度

如果给定一个单位$H={h_{ij}}$,给H的每个元素乘上a,得到的单应$aH$和$H$作用相同,因为新的单应无非把齐次点$P_1$变成了齐次点$aP_2$,$aP_2$和$P_2$在图像上对应的点是相同的。所以一个单应中只有8个自由度,一般令$h_{33}=1$来归一化。

求解单应

8个未知数需要8个方程来求解,之所以4对点能求解,因为他们一个点能提供两个方程。

假设图像上有两个点$(x_1,y_1)$,$(x_2,y_2)$,他们的齐次坐标为$\left[ \begin{matrix} x_1 \ y_1 \ 1 \end{matrix} \right]$,$\left[ \begin{matrix} x_2 \ y_2 \ 1 \end{matrix} \right]$。

带入上述的推导可以得到:

$$
x_2=x_1h_{11}+y_1h_{12}+h_{13}
$$

$$
y_2=x_1h_{21}+y_1h_{22}+h_{23}
$$

$$
1=x_1h_{31}+y_1h_{32}+h_{33}
$$

一般令$h_{33}=1$来归一化,可得到:

$$
x_2=\frac{x_1h_{11}+y_1h_{12}+h_{13}}{x_1h_{31}+y_1h_{32}+1}
$$

$$
y_2=\frac{x_1h_{21}+y_1h_{22}+h_{23}}{x_1h_{31}+y_1h_{32}+1}
$$

把这两个式子重新组织一下,得到等价的矩阵形式:

$$
Au=v
$$

$$
A = \left[ \begin{matrix} x_1 \ y_1 \ 1 \ 0 \ 0 \ 0 \ -x_1x_2 \ -x_2y_1 \ 0 \ 0 \ 0 \ x_1 \ y_1 \ 1 \ -x_1y_2 \ -y_1y_2 \end{matrix} \right]
$$

$$
u=\left[ \begin{matrix} h_{11} \ h_{12} \ h_{13} \ h_{21} \ h_{22} \ h_{23} \ h_{31} \ h_{32} \ h_{33} \end{matrix} \right]^T
$$

$$
v=\left[ \begin{matrix} x_2 \ y_2 \end{matrix} \right]^T
$$

如果有四对不共线匹配点对,这个方程组就能够垒到8行,存在唯一解。

如果多于四对点,比如有n对点,方程就垒到2n行,用最小二乘法或SVD分解就可以求解$H$。

由于点对中可能存在不少错误匹配,一般使用RANSAC算法剔除错误匹配点对。

Opencv中单应性矩阵H的计算

如果在两幅对应的图像中已知4个映射点的坐标,就可以使用 findHomography函数如下:

**C++: ** Mat cv::findHomography(InputArray srcPoints,InputArray dstPoints,OutputArray mask,int method = 0,double ransacReprojThreshold = 3)
Python: retval,mask=cv.findHomography(srcPoints,dstPoints[,method[,ransacReprojThreshold[,mask[,maxIters[,confidence]]]]])

自动寻找对应点(corresponding points)

由上文知道知道两对对应点(4个映射点)的坐标即可求得单应性矩阵H。

我们可以使用在OpenCV中的几个关键点检测器(例如SIFT,SURF和ORB)。

本文将采用ORB关键点检测器,SIFT和SURF已经注册专利。

一个特征点检测器由两个部分组成:

定位器(Locator):它可以识别图像上在平移(移位),缩放(缩小增大/缩小)和旋转等图像变换下稳定的点。 定位器查找这些点的x,y坐标。 ORB检测器使用的定位器叫做FAST
描述符(Descriptor):上述步骤中的定位器仅告诉我们特征点在哪里。 特征检测器的第二部分是对点的外观进行编码的描述符,以便我们可以从另幅图中指出同一个特征点。 在特征点处评定的描述符只是一个数字数组。 理想情况下,两幅图像中的相同物理点应具有相同的描述符。 ORB使用的是BRISK特征描述符的修改版本。

总结:图像对齐思路

1、读取图像

2、特征点检测

3、匹配特征点

4、计算单应矩阵参数

5、矫正图像

C++ Code

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <opencv2/opencv.hpp>
#include "opencv2/xfeatures2d.hpp"
#include "opencv2/features2d.hpp"

using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;

const int MAX_FEATURES = 500;
const float GOOD_MATCH_PERCENT = 0.15f;


void alignImages(Mat &im1, Mat &im2, Mat &im1Reg, Mat &h)

{

// Convert images to grayscale
Mat im1Gray, im2Gray;
cvtColor(im1, im1Gray, CV_BGR2GRAY);
cvtColor(im2, im2Gray, CV_BGR2GRAY);

// Variables to store keypoints and descriptors
std::vector<KeyPoint> keypoints1, keypoints2;
Mat descriptors1, descriptors2;

// Detect ORB features and compute descriptors.
Ptr<Feature2D> orb = ORB::create(MAX_FEATURES);
orb->detectAndCompute(im1Gray, Mat(), keypoints1, descriptors1);
orb->detectAndCompute(im2Gray, Mat(), keypoints2, descriptors2);

// Match features.
std::vector<DMatch> matches;
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming");
matcher->match(descriptors1, descriptors2, matches, Mat());

// Sort matches by score
std::sort(matches.begin(), matches.end());

// Remove not so good matches
const int numGoodMatches = matches.size() * GOOD_MATCH_PERCENT;
matches.erase(matches.begin()+numGoodMatches, matches.end());


// Draw top matches
Mat imMatches;
drawMatches(im1, keypoints1, im2, keypoints2, matches, imMatches);
imwrite("matches.jpg", imMatches);


// Extract location of good matches
std::vector<Point2f> points1, points2;

for( size_t i = 0; i < matches.size(); i++ )
{
points1.push_back( keypoints1[ matches[i].queryIdx ].pt );
points2.push_back( keypoints2[ matches[i].trainIdx ].pt );
}

// Find homography
h = findHomography( points1, points2, RANSAC );

// Use homography to warp image
warpPerspective(im1, im1Reg, h, im2.size());

}


int main(int argc, char **argv)
{
// Read reference image
string refFilename("form.jpg");
cout << "Reading reference image : " << refFilename << endl;
Mat imReference = imread(refFilename);


// Read image to be aligned
string imFilename("scanned-form.jpg");
cout << "Reading image to align : " << imFilename << endl;
Mat im = imread(imFilename);


// Registered image will be resotred in imReg.
// The estimated homography will be stored in h.
Mat imReg, h;

// Align images
cout << "Aligning images ..." << endl;
alignImages(im, imReference, imReg, h);

// Write aligned image to disk.
string outFilename("aligned.jpg");
cout << "Saving aligned image : " << outFilename << endl;
imwrite(outFilename, imReg);

// Print estimated homography
cout << "Estimated homography : \n" << h << endl;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!