首先

  在这篇文章中,我们将介绍如何使用OpenCV执行基于特征的图像对齐。我们将使用的技术通常被称为“基于特征图像对齐”,因为在该技术中,在一个图像中检测稀疏的特征集并且在另一图像中进行特征匹配。然后基于这些匹配特征将原图像映射到另一个图像,实现图像对齐。   在计算机视觉的许多应用中,时常会在两个不同的图片中共同出现一个相同或类似的事物,而两张图片中的相似事物的区别就在于可能它们所拍摄的角度、场景不同,或者物体细节上有所不同。   对于图像对齐的作用可能很难以言语来简单概括,此处我举个简单的例子来予以说明。比如我在某个场合拍了一张A4纸,在上面写了点字后从一个别的角度又对它拍了一张;在这个场景下,因为我在纸上写了些字导致两张图片中的纸并非完全相同的物体,但它们之间含有共同的特征:纸共有四个角。两张图片中纸张的四个角在各自的图片中位于不同的坐标处,不同图片中纸张的同一个角的坐标之间存在着一种对应关系(即第一张图片中的纸的左上角在图片中的坐标与第二张图片中的纸的左上角的坐标之间存在着一种对应关系),这种对应关系在计算机视觉中以矩阵的方式存在。   而图像对齐所做的事情就是找到两张图中的相似物体的这个对应关系(矩阵),然后将第二张图片与矩阵进行计算,计算出的结果就是将第二张图片的拍摄视角转换成第一张图片的拍摄视角后拍摄得到的图片。 下面会以上面例子为例来进行更加具体的说明。  

单应性变换

  图像对齐的实现其实是一个比较简单的3×3矩阵的运用,称为单应性变换(homography)。 举个简单的例子,现在有一个物体,从两个不同的角度去拍摄,得到分别如图1和图2所示的同一个物体的两个视角的图片。  
图1
图2
  将第一视角中物体的左上角的坐标记为(x1,y1),该点在第二视角中对应的坐标记为(x2,y2),并称第二个点为第一个点的对应点。   在这个例子中,单应性变换就是一个将第一视角图中的点映射到第二视角图上的对应点(物理坐标相同的点)的3×3的变换矩阵。该单应性变换矩阵H如下所示。  
  现在来考虑该例子中标记的两个点:(x1,y1)和(x2,y2),可以通过单应性变换矩阵H两者联系起来,如下图所示。  

单应性变换的局限性

  很显然,如果使用的两张图像都只是简单的二维图像,即位于现实世界中的同一平面上,那么上式对于图像中的所有对应点都是成立的。换句话说,将单应性变换矩阵应用于第一张图像,可实现第一张图像中的纸张与第二张图像中的纸张对齐。   但是如果是三维空间(即二维平面的纵向叠加)上的点,就没法再应用单应性变换来找到与之相对应点了。 需要注意的是,这里的维度取决于物理坐标而非图像,对于一般的图像而言,其始终都处于一种二维的状态。  

如何寻找对应点

  在计算机视觉的许多应用领域中,我们时常需要在目标图像中挑选出相对稳定的点。这些点称为关键点或者特征点。在OpenCV中有几个内置的关键点检测器,常用的有SIFT、SURF、ORB等,这里使用的是ORB。 ORB代表的是两个英文单词的融合,分别是:Oriented FAST和Rotated BRIEF,通常称为FAST和BRIEF。   首先我们需要知道的是,每个特征点检测器都由两个部分构成,分别是:定位器和特征描述子。 所谓的定位器是指识别的图像中,在图像变换时稳定不变的点,常见的图像变换有平移变换、缩放变换、旋转变换等。然后定位器可以一直找到这些点在图像中的x坐标和y坐标。而ORB检测器使用的定位器称为FAST。   但是在上述步骤中,特征定位器只能告诉我们机器感兴趣的点在图像中的位置,并不能对图像进行直接的描述,因此就有了特征检测器的第二部分:特征描述子。 特征描述子的作用就是对点的一些特征、外观进行了某种编码,以便我们可以通过这些编码来分辨不同的特征点。在特征点评估的时候,特征描述只会是一个数字数组。   在理想情况下,两张图像中的相同物理点会具有相同的特征描述(因为只是角度的变化而非类似于光的强度的变化)。在ORB检测器中,其使用的特征描述子就是BRIEF。 因此,只要知道了两个不同图像中的对应特征的关系,就能计算出与两张图像相关的映射矩阵。为了得到特征与特征间的关系,这里可以使用匹配算法来对第一张图像中的一些特征(如选用的纸张的4个角)与第二张图像中的对应特征之间进行匹配。   为此,我们需要将第一张图像中的每个特征点的描述子与第二张图像中的每个特征点的描述子进行比较、匹配,从而找到一种良好的匹配关系。换而言之,我们可以通过描述子来找到要匹配的特征点,然后再根据这些已经匹配好的特征点,计算两张图像相关的映射矩阵,从而实现图像映射,即特殊到一般的过程。

基于特征的图像对齐步骤

  特征对齐一般有以下几个步骤。   (1)获取图像 读入作为参考的图像,以及想要与此模板对齐的图像,所以需要读入两张图像。   (2)寻找特征点 读入图像后,检测两张图像中的ORB特征。这里只需要4个特征来计算单应性就可以了(特殊性),但事实上通常可以在两张图像中检测到数百个特征,这里可以通过设置一个名为MAX-FEATURES的参数来调节选择到的特征的数量。   (3)对特征点进行匹配 在上一步中我们得到了两张图像中匹配的特征,这时按匹配的评分(类似于相似度)对选取的特征进行排序,并保留其中一小部分的原始匹配。这里对两个特征描述符之间相似性的度量使用的标准为汉明距离(hamming distance)。 汉明距离表示的是两个相同长度的字对应位不同的总数量。举个简单的例子,例如有两个数10111和10100,如果要把前者变为后者的话,需要把最后两位从1变为0,即第一个数与第二个数在位数相同的前提下,有两位不同,所以这里的汉明距离为2。再举一个字母的例子,如果现在有haar和hand两个字,如果要把haar转化为hand的话,需要修改的是也是后两位,所以汉明距离为2。 (注意:得到的很多特征点中可能会有一些不正确的匹配,这些特征点是不应该选取的。)   (4)计算单应性变换 对于计算映射矩阵,我们需要在两张图像中至少得到4个对应点来计算单应性变换。因为上面说过,自动的匹配可能会包含一些不正确的匹配,所以自动的功能匹配并不能稳定地产生100%准确的匹配。原因在于我们选取到了不正确的匹配关系。为了处理这种情况,OpenCV提供了内置函数cv2.findHomography,其利用随机抽样一致性算法(RANSAC)的精准估计技术,使得在存在大量错误匹配的情况下也能得到相对正确的匹配结果。   (5)图像映射 计算出两张图像中的单应性变换之后,就可以对第一张图像中的所有像素点,通过乘以单应性变换的方式将其映射到对齐以后的图像,这里矩阵乘法使用的是之前介绍过的cv2.warpPerspective函数。  

图像对齐

  首先需要使用一张作为标准的基准图片(im1),如图3所示,另一张图片就是需要进行图片对齐的图片(im2),如图4所示。  
图3
图4
两张图片对齐的示例代码如下。  
from __future__ import print_function
import cv2
import numpy as np
MAX_MATCHES=500
GOOD_MATCH_PERCENT=0.15
def alignImages(im1,im2):
    # 将图像转换为灰度图
    im1Gray = cv2.cvtColor(im1,cv2.COLOR_BGR2GRAY)
    im2Gray = cv2.cvtColor(im2,cv2.COLOR_BGR2GRAY)
    # 寻找ORB特征
    orb=cv2.ORB_create(MAX_MATCHES)
    keypoints1,descriptors1=orb.detectAndCompute(im1Gray,None)
    keypoints2,descriptors2=orb.detectAndCompute(im2Gray,None)
    # 匹配特征
matcher=cv2.DescriptorMatcher_create(cv2.DESCRIPTOR_MATCHER_BRUTEFORCE_HAMMING)
    matches=matcher.match(descriptors1, descriptors2,None)
    # 对特征进行排序
    matches.sort(key=lambda x:x.distance,reverse=False)
    # 移除不良特征
    numGoodMatches=int(len(matches)*GOOD_MATCH_PERCENT)
    matches=matches[:numGoodMatches]
    # 选取正确的特征
    imMatches=cv2.drawMatches(im1,keypoints1,im2,keypoints2,matches,None)
    cv2.imwrite("matches.jpg", imMatches)
    # 对特征进行定位
    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
    # 寻找矩阵
    h,mask=cv2.findHomography(points1,points2,cv2.RANSAC)
    # 使用矩阵
    height,width,channels =im2.shape
    im1Reg=cv2.warpPerspective(im1,h,(width, height))
    return im1Reg, h
if __name__ == '__main__':
    # 读取标准图像
    refFilename="1.jpg"
    imReference=cv2.imread(refFilename)
    # 读取待对齐的图像
    imFilename="2.jpg"
    im=cv2.imread(imFilename)
    print("正在对齐")
    imReg,h=alignImages(im,imReference)
    (x,y,z)=imReg.shape
    cv2.namedWindow('img',cv2.WINDOW_NORMAL)
    cv2.resizeWindow('img',int(y/2),int(x/2))
    cv2.imshow('img',imReg)
    cv2.waitKey(0)
    # 对齐后输出到本地 
    outFilename="aligned.jpg"
    print("将图片保存为: ",outFilename)
    cv2.imwrite(outFilename,imReg)
    # 输出映射矩阵
    print("映射矩阵为: \n",h)
cv2.destroyAllWindows()
  运行上述代码,待对齐图经过基准图校准后,得到如图5的运行结果。有的时候可能会显示出错,这是图片属性的问题,代码中如果不能正常显示的话,可以去本地文件夹中查看导出的aligned.jpg图片。此外还有一张显示两张图片映射关系的图片,如图6所示。  
图5
图6
可以看到的是,虽然作为基准的图与待对齐的图是有差别的,但是依旧能够进行对齐。这里选用的两张图片中的物体相似度很高,但这并不是必须的,相似度高的好处在于它可以提供更多的对应点以供选取。   如果基准图使用的只是一张白纸,其也可以进行图像的对齐,但是在这种情况下我们只能得到较少的对应点(例如一张白纸的4个角点)。此时,如果仅有的几个匹配关系出现了错误,那么只能得到一个错误的单应性变换,从而得不到想要的结果。这里输出的映射矩阵如下图所示。  
(注意:代码中搜索的对应点的最大数量被设置成了500个,其实我们使用的图中可能找不到这么多的对应点。以A4纯白纸为例,用它作为基准图的话,可能就只有4个角点能用来当作对应点)