Skip to content

特征点检测与匹配

特征点检测的基本概念

  • 什么是特征
    • 图像特征就是指有意义的图像区域
    • 具有独特性、易于识别性,比如角点、斑点以及高密度区
  • 角点
    • 在特征中最重要的是角点
    • 灰度梯度的最大值对应的像素
    • 两条线的交点
    • 极值点(一阶导数最大值,但二阶导数为0)
  • OpenCV特征的场景
    1. 图像搜索,如以图搜图
    2. 拼图游戏
    3. 图像拼接,将两张有关联的图拼接到一起
  • 拼图方法
    1. 寻找特征
    2. 特征是唯一的
    3. 可追踪的
    4. 能比较的

Harris角点检测

  • 光滑地区,无论向哪里移动,衡量系数不变
  • 边缘地址,垂直边缘移动时,衡量系统变化剧烈
  • 在交点处,往哪个方向移动,衡量系统都变化剧烈
    Image title
  • Harris角点检测API:cornerHarris(img, dst, blockSize, ksize, k)

    1. img: 源
    2. dst: 输出结果
    3. blockSize: 窗口的大小
    4. ksize: kernel size Sobel的卷积核
    5. k: 权重系数,经验值,一般取0.02 ~ 0.04之间

    import cv2
    
    blockSize = 4
    ksize = 3
    k = 0.04
    
    img = cv2.imread('./chess.png')
    
    # 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # Harris角点检测
    dst = cv2.cornerHarris(gray, blockSize, ksize, k)
    
    # Harris角点的展示(dst 大于dst中最大值的0.01 标记为红色)
    img[dst > 0.01 * dst.max()] = [0, 0, 255]
    
    cv2.imshow('harris', img)
    cv2.waitKey(0)
    
    Image title

Shi-Tomasi角点检测

  • Shi-Tomasi是Harris角点检测的改进
  • Harris角点检测算的稳定性和k有关,而k是个经验值,不好设定最佳值
  • Shi-Tomasi的API:goodFeaturesToTrack(img, maxCorners, qualityLevel, minDistance, mask, blockSize, useHarrisDetector, k)

    1. img: 图像源
    2. maxCorners: 角点的最大数,值为0表示无限制
    3. qualityLevel: 角点的质量(小于1.0的正数,一般在0.01-0.1之间)
    4. minDistance: 角之间最小欧式距离,忽略小于此距离的点
    5. mask: 指定检测的区域,不设定则会检测整幅图
    6. blockSize: 检测窗口大小
    7. useHarrisDetector: 是否使用Harris算法(既可以使用Harris检测也可以用于Tomasi检测,如果写true就会使用原始的Harris算法,写false才会使用Shi-Tomasi的交点检测方法)
    8. k: 默认是0.04

    import cv2
    import numpy as np
    
    #Shi-Tomasi
    maxCorners = 1000
    ql = 0.01
    minDistance = 10
    
    img = cv2.imread('./chess.png')
    
    # 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # Shi-Tomasi
    corners = cv2.goodFeaturesToTrack(gray, maxCorners, ql, minDistance)
    corners = np.int0(corners)
    
    #Shi-Tomasi绘制角点
    for i in corners:
        x,y = i.ravel()
        cv2.circle(img, (x,y), 3, (255,0,0), -1)
    
    cv2.imshow('harris', img)
    cv2.waitKey(0)
    
    Image title

SIFT关键点检测

  • Scale-Invariant Feature Transform:与缩放无关的特征转换(有版权问题)
  • SIFT出现的原因
    • Harris角点具有旋转不变的特性,但缩放后,原来的角点有可能就不是角点了
      Image title
      如上图,按固定大小窗口进行检测,左侧原本是角但是放大到右侧窗口不变的情况下就检测不到是角了,而是边缘,这就是Harris角点检测存在的巨大问题(当原来的视角放大之后原本是角就不是角了)
    • SIFT就是要解决Harris的这个痛点,原本检测是角,放大之后检测依旧是角。
  • 使用SIFT的步骤
    1. 创建SIFT对象
    2. 进行检测,kp=sift.detect(img, ...) 获取特征点
    3. 绘制关键点,drawKeypoints(gray, kp, img)
      import cv2
      import numpy as np
      
      # 读取图片
      img = cv2.imread('chess.png')
      
      # 灰度化
      gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
      
      # 创建sift对象
      sift = cv2.xfeatures2d.SIFT_create()
      
      # 进行检测
      kp = sift.detect(gray, None)
      
      # 绘制keypoints
      cv2.drawKeypoints(gray, kp, img)
      
      cv2.imshow('img', img)
      cv2.waitKey(0)
      
      Image title

SIFT计算描述子

  • 关键点和描述子
    • 关键点:位置、大小和方向
    • 关键点描述子:记录了关键点周围对其有贡献的像素点的一组向量值,其不受仿射变换、光照变换等影响
  • 优点:特征检测的点特别准确,描述子也描述的非常详细
  • 缺点:速度慢,有版权问题
  • 计算描述子API:kp, des = sift.compute(img, kp)
    • 入参
      1. img: 图像源
      2. kp: 关键点
    • 返回值
      1. kp: 关键点
      2. des: 描述子:作用就是进行特征匹配
  • 同时计算关键点和描述子API:kp, des = sift.detectAndCompute(img, mask)

    • 入参
      1. img: 图像源
      2. mask: 指明对img中哪个区域进行计算
    • 返回值
      1. kp: 关键点
      2. des: 描述子
    import cv2
    import numpy as np
    
    # 读取图片
    img = cv2.imread('chess.png')
    
    # 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 创建sift对象
    sift = cv2.xfeatures2d.SIFT_create()
    
    # 进行检测 None时对整幅图像进行检测
    kp, des= sift.detectAndCompute(gray, None)
    print(des)
    '''
    [[  0.   0.   0. ...   0.   0.   0.]
    [  7.   0.   0. ...   0.   0.   0.]
    [  0.   0.   0. ...   0.   0.   0.]
    ...
    [  1.   0.   0. ...  49. 135.   0.]
    [  0.   0.   0. ...   0.   0.   0.]
    [  1.   0.   0. ...  50. 135.   0.]]
    '''
    print(des[0])
    '''
    [  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
    10.  12.   0.   0.   0.   0.   0.   0.  50.  11.   0.   0.   0.   0.
    0.   0.  68.  11.   3.   0.   0.   0.   0.   0.   0.   8.  88.   1.
    0.   0.   0.   0.  93. 171.  19.   0.   0.   0.   0.   0. 171. 151.
    0.   0.   0.   0.   0.   0. 171.  62.   7.   1.   0.   0.   0.   0.
    0.   1. 171.  36.   0.   0.   0.   0.   7.  56. 145.  15.   0.   0.
    0.   0.  63.  46.   0.   0.   0.   0.   0.   0.  51.   8.   1.   0.
    0.   0.   0.   0.   0.   0. 171.  44.   0.   0.   0.   0.   0.   0.
    169.  26.   0.   0.   0.   0.   0.   0.   1.   0.   0.   0.   0.   0.
    0.   0.]
    '''
    # 绘制keypoints
    cv2.drawKeypoints(gray, kp, img)
    
    cv2.imshow('img', img)
    cv2.waitKey(0)
    

SURF特征检测

  • Speeded-Up Robust Features:加速的鲁棒性特征检测(有版权问题)
  • 优点:速度快,保留了SIFT的优点(特征检测的点特别准确,描述子也描述的非常详细)
  • 缺点:有版权问题
  • 使用SURF的步骤

    1. 创建SURF对象,surf = cv2.xfeatures2d.SURF_create()
    2. 进行检测,kp, des = surf.detectAndCompute(gray, mask) 获取特征点与描述子
    3. 绘制关键点,drawKeypoints(gray, kp, img)

    import cv2
    import numpy as np
    
    # 读取图片
    img = cv2.imread('chess.png')
    
    # 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 创建sift对象
    sift = cv2.xfeatures2d.SURF_create()
    
    # SURF
    kp, des = surf.detectAndCompute(gray, None)
    
    # 绘制keypoints
    cv2.drawKeypoints(gray, kp, img)
    
    cv2.imshow('img', img)
    cv2.waitKey(0)
    
    Image title

OBR特征检测

  • Oriented FAST and Rotated BRIEF:特征点检测和描述子计算
  • 优点:ORB可以做到实时检测,无版权问题
  • FAST可以做到特征点的实时检测,但是没有方向,所以增加了方向这个属性OrientedBRIEF是对已检测到的特征点进行描述,它加快了特征描述符建立的速度,同时也极大的降低了特征匹配的时间,BRIEF对图像的旋转处理的不太好,所以又增加了Rotated属性。
  • 使用OBR的步骤

    1. 创建OBR对象,orb = cv2.ORB_create()
    2. 进行检测,kp, des = surf.detectAndCompute(gray, mask) 获取特征点与描述子
    3. 绘制关键点,drawKeypoints(gray, kp, img)

    import cv2
    import numpy as np
    
    # 读取图片
    img = cv2.imread('chess.png')
    
    # 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 创建ORB对象
    orb = cv2.ORB_create()
    # orb进行检测
    kp, des = orb.detectAndCompute(gray, None)
    
    # 绘制keypoints
    cv2.drawKeypoints(gray, kp, img)
    
    cv2.imshow('img', img)
    cv2.waitKey(0)
    
    Image title

暴力特征匹配

  • BF(Brute-Force),暴力特征匹配方法(使用枚举的方式进行匹配)
  • 暴力特征匹配原理:它使用第一组中每个特征的描述子,它与第二组中的所有特征描述子进行匹配(第一幅图与第二幅图),计算它们之间的差距,然后将最接近一个区配返回。
  • OpenCV特征匹配步骤

    • 创建匹配器:BFMatcher(normType, crossCheck)
      1. normType: 匹配类型,默认使用NORM_L2
        • NORM_L1: 使用SIFT描述子计算的,取描述子的绝对值,然后进行加法运算
        • NORM_L2: 使用SURF的描述子计算的,取描述子的平方和求开方,也就是欧氏距离
        • HAMMING1: 使用OBR描述子计算的,查看二进制位在第几位发生了变化
        • ...
      2. crossCheck: 交叉检查(第一幅图与第二幅匹配,第二幅与第一幅匹配,提升准确度),默认为false
    • 进行特征匹配:bf.match(des1, des2) 参数为SIFT、SURF、OBR等计算的描述子,对两幅图的描述子进行计算
      1. des1: 描述子
      2. des2: 描述子
    • 绘制匹配点:cv2.drawMatches(img1, kp1, img2, kp2 ...)
      1. img1, kp1: 搜索的图和特征点
      2. img2, kp2: 匹配的图和特征点
      3. match() 方法返回的匹配结果

    import cv2
    import numpy as np
    
    # 读取图片
    img1 = cv2.imread('a.png')
    img2 = cv2.imread('b.png')
    
    # 灰度化
    g1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
    g2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
    
    # 创建sift对象
    sift = cv2.xfeatures2d.SIFT_create()
    
    #进行检测
    kp1, des1 = sift.detectAndCompute(g1, None)
    kp2, des2 = sift.detectAndCompute(g2, None)
    
    bf = cv2.BFMatcher(cv2.NORM_L1)
    match = bf.match(des1, des2)
    
    img3 = cv2.drawMatches(img1, kp1, img2, kp2, match, None)
    
    cv2.imshow('show', img3)
    cv2.waitKey(0)
    
    Image title

FLANN特征匹配与图像查找

  • FLANN
    • FLANN 最快邻近区特征匹配方法
    • 优点:在进行批量特征匹配时,FLANN速度更快
    • 缺点:由于它使用的是临近近似值,所以精度较差,如果我们要做的是图像的精确匹配,就需要使用暴力特征匹配
    • 使用FLANN特征匹配的步骤
      1. 创建FLANN匹配器:FlannBasedMatcher(...)
        • index_params字典:匹配算法 KDTREE、LSH,一般KDTREE设置成5,search_params设置成50
          • KDTREE的设置方式
            • index_params = dict(algorithm=FLANN_INDEX_KDTREE, tree=5)
        • 如果上面参数匹配的是KDTREE则需要第二个字段-> search_params字典:指定KDTREE算法中遍历树的次数
          • search_params设置方式
            • search_params = dict(checks=50)
      2. 进行特征匹配:flann.match/knnMatch(...)
        • knnMatch方法
          • 参数为SIFTSURFORB等计算的描述子,k,表示取欧氏距离最近的前k个关键点
          • 返回的是匹配的结果DMatch对象
            • DMatch 的内容
              • distance: 描述子之间的距离,值越低越好
              • queryIdx: 第一个图像的描述子索引值
              • trainIdx: 第二个图的描述子索引值
              • imgIdx: 第二个图的索引值
      3. 绘制匹配点:cv2.drawMatches/drawMatchesKnn(...)
        • drawMatchesKnn 只能与 knnMatch 搭配才能绘制
          • 搜索img, kp
          • 匹配图img, kp
          • match() 方法返回的匹配结果(knn结果)
  • 图像查找

    • 图像查找:特征匹配 + 单应性矩阵
    • 什么是单应性矩阵
      Image title
      比如现在有两个相机,需要对平面上的一个物体进行拍照,两个相机同时拍平面X点时,在左侧图像中成像与x点,右侧图像中成像与x'点,由此可见对于同一个物体 不同视角的相机成像的位置是不一样的。单应性矩阵就是获取到一个矩阵,与图像1进行运算,就可以得到图像2的位置,图象1/2经过运算可以得到物体原始的位置,其中的H就是单应性矩阵。
    • 单应性的作用(一)图像转正
      Image title
    • 单应性的作用(二)换图像中的广告
      Image title

    import cv2
    import numpy as np
    
    #打开两个文件
    img1 = cv2.imread('e.png')
    img2 = cv2.imread('f.png')
    
    #灰度化
    g1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
    g2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
    
    #他建SIFT特征检测器
    sift = cv2.xfeatures2d.SIFT_create()
    
    #计算描述子与特征点
    kp1, des1 = sift.detectAndCompute(g1, None)
    kp2, des2 = sift.detectAndCompute(g2, None)
    
    #创建匹配器
    index_params = dict(algorithm = 1, trees = 5)
    search_params = dict(checks = 50)
    flann = cv2.FlannBasedMatcher(index_params, search_params)
    
    #对描述子进行匹配计算
    matchs = flann.knnMatch(des1, des2, k=2)
    
    good = []
    for i, (m, n) in enumerate(matchs):
        if m.distance < 0.7 * n.distance:
            good.append(m)
    
    # 图像查找 开始
    if len(good) >= 4:
        srcPts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
        dstPts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
    
        H, _ = cv2.findHomography(srcPts, dstPts, cv2.RANSAC, 5.0)
    
        h, w = img1.shape[:2]
        pts = np.float32([[0,0], [0, h-1], [w-1, h-1], [w-1, 0]]).reshape(-1, 1, 2)
        dst = cv2.perspectiveTransform(pts, H)
    
        cv2.polylines(img2, [np.int32(dst)], True, (0, 0, 255))
    else:
        print('the number of good is less than 4.')
        exit()
    # 图像查找 结束
    
    ret = cv2.drawMatchesKnn(img1, kp1, img2, kp2, [good], None)
    cv2.imshow('result', ret)
    cv2.waitKey()
    
    Image title

图像拼接

  • 图像拼接合并的步骤

    1. 读文件并重置尺寸
    2. 根据特征点和计算描述子,得到单应性矩阵
    3. 图像变换
    4. 图像拼接并输出图像
  • 图像拼接前

    Image title
    图像A
    Image title
    图像B
    Image title
    拼接结果

import cv2
import numpy  as np

def stitch_image(img1, img2, H):
    # 1. 获得每张图片的四个角点
    # 2. 对图片进行变换(单应性矩阵使图进行旋转,平移)
    # 3. 创建一张大图,将两张图拼接到一起
    # 4. 将结果输出

    #获得原始图的高/宽
    h1, w1 = img1.shape[:2]
    h2, w2 = img2.shape[:2]

    img1_dims = np.float32([[0,0], [0, h1], [w1, h1], [w1, 0]]).reshape(-1, 1, 2)
    img2_dims = np.float32([[0,0], [0, h2], [w2, h2], [w2, 0]]).reshape(-1, 1, 2)

    img1_transform = cv2.perspectiveTransform(img1_dims, H)

    # print(img1_dims)
    # print(img2_dims)
    # print(img1_transform)

    result_dims = np.concatenate((img2_dims, img1_transform), axis=0)
    #print(result_dims)

    [x_min, y_min] = np.int32(result_dims.min(axis=0).ravel()-0.5)
    [x_max, y_max ] = np.int32(result_dims.max(axis=0).ravel()+0.5)

    #平移的距离
    transform_dist = [-x_min, -y_min]

    #[1, 0, dx]
    #[0, 1, dy]         
    #[0, 0, 1 ]
    transform_array = np.array([[1, 0, transform_dist[0]],
                                [0, 1, transform_dist[1]],
                                [0, 0, 1]])

    result_img = cv2.warpPerspective(img1, transform_array.dot(H), (x_max-x_min, y_max-y_min))

    result_img[transform_dist[1]:transform_dist[1]+h2, 
                transform_dist[0]:transform_dist[0]+w2] = img2

    return result_img

def get_homo(img1, img2):
    #1. 创建特征转换对象
    #2. 通过特征转换对象获得特征点和描述子
    #3. 创建特征匹配器
    #4. 进行特征匹配
    #5. 过滤特征,找出有效的特征匹配点

    sift = cv2.xfeatures2d.SIFT_create()

    k1, d1 = sift.detectAndCompute(img1, None)
    k2, d2 = sift.detectAndCompute(img2, None)

    #创建特征匹配器
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(d1, d2, k=2)

    #过滤特征,找出有效的特征匹配点
    verify_ratio = 0.8
    verify_matches = []
    for m1, m2 in matches:
        if m1.distance < 0.8 * m2.distance:
            verify_matches.append(m1)

    min_matches = 8
    if len(verify_matches) > min_matches:

        img1_pts = []
        img2_pts = []

        for m in verify_matches:
            img1_pts.append(k1[m.queryIdx].pt)
            img2_pts.append(k2[m.trainIdx].pt)
        #[(x1, y1), (x2, y2), ...]
        #[[x1, y1], [x2, y2], ...]

        img1_pts = np.float32(img1_pts).reshape(-1, 1, 2)
        img2_pts = np.float32(img2_pts).reshape(-1, 1, 2)
        H, mask = cv2.findHomography(img1_pts, img2_pts, cv2.RANSAC, 5.0)
        return H

    else:
        print('err: Not enough matches!')
        exit()


#第一步,读取文件,将图片设置成一样大小640x480
#第二步,找特征点,描述子,计算单应性矩阵
#第三步,根据单应性矩阵对图像进行变换,然后平移
#第四步,拼接并输出最终结果

#读取两张图片
img1 = cv2.imread('g.png')
img2 = cv2.imread('h.png')

#将两张图片设置成同样大小
img1 = cv2.resize(img1, (640, 480))
img2 = cv2.resize(img2, (640, 480))

inputs = np.hstack((img1, img2))

#获得单应性矩阵
H = get_homo(img1, img2)

#进行图像拼接
result_image = stitch_image(img1, img2, H)

cv2.imshow('output-img', result_image)
cv2.waitKey()