问题描述
目标图像检测是指在输入图像中寻找给定目标图像并框选目标图像所在区域的过程。
一个典型的目标图像检测的例子如下:
图像匹配
关于图像匹配很自然的一个想法是逐点比较像素的RBG值。但是几何变换前后的图像我们通常认为是同一幅图像,考虑这一点,这种初级的想法是行不通的。对于图像的匹配我们一般使用特征点来进行。
特征点
参考人对相同图像进行匹配的过程,我们可以发现人眼对不同视角、不同光照下的图像有着同样的感觉。即不同视角下的图像在某种程度上对人是等价的
仿照这种现象,我们试图使用相似的思路来进行计算机对图像的匹配。一方面图像在各种变换下像素的变化是很大的,我们需要提取其中的一些变换相对不大的点来进行比较。这些点我们称为关键点。一般使用角点(Conor point)作为关键点。
对图像提取关键点如图
另一方面我们希望关键点在图像变换前后有着某种不变性,以便于跟踪。这种不变性是单纯的灰度值或者RGB值无法满足的。因此我们使用人工设计的方法来生成一个向量对关键点进行描述。我们把这个向量称为描述子。
综上,对一幅图像进行特征提取包括关键点提取和关键点的描述子计算两种。常用的特征有SIFT特征、ORB特征、SURF特征等。
特征匹配
对两幅图像提取了特征点。接下来我们要对特征点进行匹配。衡量两幅图像中两个特征点是否对应的方法有很多,最简单的是计算两点描述向量的距离。假设目标图像A中有一点 x 1 x_1 x1,其描述量为 d 1 d_1 d1。输入图像B有一点 x 2 x_2 x2,其描述量 d 2 d_2 d2。 d 1 d_1 d1与 d 2 d_2 d2之间的距离 d 12 = ∣ ∣ d 1 − d 2 ∣ ∣ d_{12} = ||d_1-d_2|| d12=∣∣d1−d2∣∣ 。||x||表示x的范数。对于不同的描述符,我们使用不同类型的距离。
对
x
1
x_1
x1与B中的所有特征点计算它们的描述量的距离,选取距离最小的一个点作为与
x
1
x_1
x1的一对匹配,我们就得到了两幅图像间的特征匹配情况。如下图:
可以看到图像中有许多误匹配。对此我们可以进行过滤,常用过滤方法有交叉验证和比率测试。
交叉验证:假设得到了A中的x1与B中的x2描述量距离最小,测试x2是否也与A中的x1描述量距离最小。当满足时确认这是一对正确的匹配。
比率测试:假设A中的x1与B中的x2描述量距离最小,距离为d12;与B中的x3距离次小,距离为d13。只有当d12/d13小于某值时确认这是一对正确的匹配。
图像对齐
在特征匹配的过程中我们得到了目标图像A与输入图像B中对应的特征点对集合 M = { ( x 1 , x 2 ) ∣ x 1 ∈ A , x 2 ∈ B , x 1 m a t c h e s x 2 } M = \{(x_1,x_2)|x_1\in A,x_2 \in B,x_1~matches~x_2\} M={(x1,x2)∣x1∈A,x2∈B,x1 matches x2}使用这些特征点对我们可以计算出从目标到输入图像的单应性矩阵H,从而计算出图像中标记的顶点。
计算单应性矩阵
在前面的文章中讲到,单应性矩阵描述了变换前后两幅图像平面的对应关系。对变换前后的对应点
x
,
x
′
x,x'
x,x′。有
x
′
=
H
x
x'=Hx
x′=Hx这是一个线性方程,之前讲过H的自由度最多为8 。而每个特征点对提供了两个约束,故只需要4个特征点对就可以求出H。
一般来说我们会得到多于4个特征点对,此时上面的方程变成一个超定方程。可以解出它的最小二乘解。另一方面,即使使用了ratio test还是会有很多错误匹配,因此在计算单应矩阵时我们使用RANSAC算法,消除误匹配的影响。
使用单应性矩阵计算标记顶点
求出单应性矩阵H,我们可以轻易得到图像中的标记顶点。
假设目标图像的顶点齐次坐标为为:
A
(
0
,
0
,
1
)
,
B
(
0
,
h
e
i
g
h
t
,
1
)
,
C
(
w
i
d
t
h
,
0
,
1
)
,
D
(
w
i
d
t
h
,
h
e
i
g
h
t
,
1
)
A(0,0,1),B(0,height,1),C(width,0,1),D(width, height,1)
A(0,0,1),B(0,height,1),C(width,0,1),D(width,height,1)
分别使用 x ′ = H x x'=Hx x′=Hx算出 A ′ 、 B ′ 、 C ′ 、 D ′ A'、B'、C'、D' A′、B′、C′、D′就得到了图像中标记的四个顶点(对多于四个顶点的情况亦然。由顶点就可以画出标记的边界。
最后得到的效果如图。
图片恢复
同样的求出单应性矩阵之后我们可以轻易的恢复图像中的目标。
具体操作是对图像中的每个点使用 x = H − 1 x ′ x=H^{-1}x' x=H−1x′进行变换,得到新的图像,剔除变换后跑出边界的点。就得到了恢复出目标图像。
最后得到的效果如图。
附录:用到的代码
#include <opencv2\opencv.hpp>
#include <vector>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
//read image
auto target= imread("../../target.jpg");
vector<KeyPoint> kpts_m;
Mat desp_m;
auto img = imread("../../img.jpg");
vector<KeyPoint> kpts_i;
Mat desp_i;
//extract feature
auto orbDetector = BRISK::create();
orbDetector->detectAndCompute(target, noArray(), kpts_m, desp_m);
orbDetector->detectAndCompute(img, noArray(), kpts_i, desp_i);
Mat out1;
drawKeypoints(target, kpts_m, out1);
imshow("kpts", out1);
//match keypoints
auto matcher = BFMatcher::create(4, true);
vector<DMatch> matches;
matcher->match(desp_m, desp_i, matches);
Mat out2;
drawMatches(target, kpts_m, img, kpts_i, matches, out2);
imshow("matches", out2);
//calculate H
vector<Point2f> m_p;
vector<Point2f> f_p;
for (auto correspondence : matches)
{
m_p.push_back(kpts_m[correspondence.queryIdx].pt);
f_p.push_back(kpts_i[correspondence.trainIdx].pt);
}
Mat H_f_m = findHomography(m_p, f_p, noArray(), RANSAC);
//-- Get the corners from the image_1 ( the object to be "detected" )
std::vector<Point2f> obj_corners(4);
obj_corners[0] = cvPoint(0, 0); obj_corners[1] = cvPoint(img.cols, 0);
obj_corners[2] = cvPoint(img.cols, img.rows); obj_corners[3] = cvPoint(0, img.rows);
std::vector<Point2f> scene_corners(4);
perspectiveTransform(obj_corners, scene_corners, H_f_m);
Mat out3 = img.clone();
//-- Draw lines between the corners (the mapped object in the scene - image_2 )
line(out3, scene_corners[0], scene_corners[1], Scalar(0, 255, 0), 4);
line(out3, scene_corners[1], scene_corners[2], Scalar(0, 255, 0), 4);
line(out3, scene_corners[2], scene_corners[3], Scalar(0, 255, 0), 4);
line(out3, scene_corners[3], scene_corners[0], Scalar(0, 255, 0), 4);
imshow("detection", out3);
//recover
Mat out4;
warpPerspective(img, out4, H_f_m.inv(), { img.cols ,img.rows});
imshow("recover", out4);
waitKey(0);
}