特征匹配与目标检测

欢迎访问我的博客首页


  目标检测应用广泛,但至今尚无通用的目标检测算法。因此我们可以针对特定场景设计满足需求的目标检测算法。

1. 透视变换与目标检测


1.1 透视变换


  从透视变换的名字解释透视变换:使一块印有图像的玻璃任意运动,则该图像在某个平面上的任意两个投影之间的关系就是透视变换。从实际场景解释透视变换:从不同视角看一幅平面图像,在任意两个视角看到的两个图像之间的关系就是透视变换。

  透视变换是两个视图或图像之间的关系,可以使用单应矩阵表示。即:如果两个视图或图像是透视变换关系,则存在一个单应矩阵,该单应矩阵可以用一幅视图或图像变换得到另一幅视图或图像。

  相机拍摄的图像是现实场景的缩小,相机内参 f x f_x fx f y f_y fy 分别表示横竖方向的缩放系数。只要两个相机的这两个缩放系数比相等,即 f x 1 f y 1 = f x 2 f y 2 \frac{f^1_x}{f^1_y} = \frac{f^2_x}{f^2_y} fy1fx1=fy2fx2,这两个相机拍摄同一个二维图案得到的图像就是透视变换关系。一般情况下,相机对横竖方向的缩放是相等的,即 f x = f y f_x = f_y fx=fy,所以不同相机按上述方法拍摄得到的图像也是透视变换关系。

1.2 目标检测


  图 1 左侧是目标图像,右侧是待检测图像。已知左图与右图中的对应区域是透视变换关系。我们要从右图上找出左图的部分,即,定位文字区域。

请添加图片描述

图 1 目标图像与待检测图像

  根据透视变换关系,我们可以设计匹配检测算法:先检测特征点并进行特征匹配,然后计算单应矩阵,再根据单应矩阵求目标在待检测图像上的位置。

# encoding=utf-8
import cv2
import numpy as np


def match_detect(path_obj, path_src, threshold_distance=0.65, threshold_matches=10):
    # 1. 目标图像与待检测图像。
    img_obj = cv2.imread(path_obj)
    img_src = cv2.imread(path_src)
    # 2. 特征检测。
    sift = cv2.xfeatures2d.SIFT_create()
    kp_obj, des_obj = sift.detectAndCompute(img_obj, None)
    kp_src, des_src = sift.detectAndCompute(img_src, None)
    # 3. 特征匹配。
    flann = cv2.FlannBasedMatcher(dict(algorithm=1, tree=5), dict(checks=50))
    matches = flann.knnMatch(des_obj, des_src, k=2)
    # 4. 根据特征的欧式距离筛选匹配。
    mask_matches = list()
    good_matches = list()
    for m, n in matches:
        if m.distance < threshold_distance * n.distance:
            mask_matches.append([1, 0])
            good_matches.append(m)
        else:
            mask_matches.append([0, 0])
    if len(good_matches) < threshold_matches:
        return None, None
    # 5. 画出匹配点。
    draw_params1 = dict(matchColor=(0, 255, 0), singlePointColor=(255, 0, 0), matchesMask=mask_matches, flags=0)
    img_dst1 = cv2.drawMatchesKnn(img_obj, kp_obj, img_src, kp_src, matches, None, **draw_params1)
    # img_dst1 = cv2.resize(img_dst1, (img_dst1.shape[1] // 4, img_dst1.shape[0] // 4), interpolation=cv2.INTER_AREA)
    cv2.imwrite(r'D:/res_matches1.jpg', img_dst1)
    # 6. 计算单应矩阵并获取掩码,掩码为 0 的匹配点在计算单应矩阵的过程中被筛选掉。
    pts_obj = np.expand_dims([kp_obj[m.queryIdx].pt for m in good_matches], axis=1).astype(np.float32)
    pts_src = np.expand_dims([kp_src[m.trainIdx].pt for m in good_matches], axis=1).astype(np.float32)
    matrix_homography, mask_homography = cv2.findHomography(pts_obj, pts_src, cv2.RANSAC, 5.0)
    # 7. 画出匹配点。坐标顺序(左上、左下、右下、右上)。
    draw_params2 = dict(matchColor=None, singlePointColor=None, matchesMask=mask_homography.ravel().tolist(), flags=0)
    img_dst2 = cv2.drawMatches(img_obj, kp_obj, img_src, kp_src, good_matches, None, **draw_params2)
    # img_dst2 = cv2.resize(img_dst2, (img_dst2.shape[1] // 4, img_dst2.shape[0] // 4), interpolation=cv2.INTER_AREA)
    cv2.imwrite(r'D:/res_matches2.jpg', img_dst2)
    # 8. 在 src 上用四边形画出 obj。
    h, w = img_obj.shape[:2]
    corner_obj = np.expand_dims([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]], axis=1).astype(np.float32)
    corner_roi = cv2.perspectiveTransform(corner_obj, matrix_homography)
    img_roi = cv2.polylines(img_src, [np.int32(corner_roi)], True, (0, 0, 255), 1, cv2.LINE_AA)
    # img_roi = cv2.resize(img_roi, (img_roi.shape[1] // 4, img_roi.shape[0] // 4), interpolation=cv2.INTER_AREA)
    return img_roi, np.squeeze(corner_roi).astype(np.int32)


if __name__ == '__main__':
    path2obj = r'D:/1.jpeg'
    path2src = r'D:/2.jpeg'
    roi, cor = match_detect(path2obj, path2src)
    if roi is None:
        print('Failed to find enough matches!')
        exit(0)
    print(str(cor).replace('\n', ','))
    cv2.imwrite(r'D:/res_roi.jpg', roi)

  该代码在 opencv-python==4.7.0.68 和 opencv-contrib-python==4.7.0.68 上测试通过。代码的第 5 部分与第 7 部分仅用于画出匹配点,与目标检测无关,可以删除。

  图 2 是代码输出,分为 3 行。第 1 行左侧是输入的目标图像 obj.jpg,右侧是在输入图像 src.jpg 上框出检测到的目标后输出的图像 res_roi.jpg。第 2 行是代码第 5 部分根据特征距离筛选匹配点之后的匹配情况。第 3 行是代码第 7 部分根据 OpeenCV 计算单应矩阵时产生的掩码筛选匹配点之后的匹配情况。

请添加图片描述

图 2 目标图像上的目标与待检测图像上的目标是透视变换关系

  从图 2 第 3 行可以看出,在透视变换的两幅图像上,使用单应矩阵掩码可以很好地筛选掉误匹配。

  OpenCV 计算的描述子是多维向量,并以树的形式存储。特征匹配时,树形数据结构查找速度快。两个描述子的欧氏距离称为特征距离。只有特征距离小于某个值时,才认为这两个特征是匹配的。代码中的阈值 threshold_distance 就用于指定这个值。因此,阈值越小,对能成为匹配点的一对描述子的相似性要求就越高,越能筛选掉误匹配,获得的匹配点也就越少。

  在已知是透视变换的情况下,OpenCV 使用 RANSAC 算法根据匹配点计算单应矩阵。计算单应矩阵至少需要 4 对匹配点。OpenCV 每次从 n 对匹配点中随机选出 m 对匹配点计算一个单应矩阵( 4 ≤ m ≤ n 4 \leq m \leq n 4mn),并计算此时的代价(误差)。代价越小,对应的单应矩阵和 m 对匹配点就越可靠。决定最终单应矩阵的匹配点对应的掩码为 1,在 RANSAC 算法中称为内点(inliers);其它掩码为 0,在 RANSAC 算法中称为外点(outliers)。

  计算单应矩阵前,可以把特征匹配的阈值设置大一些,以便为单应矩阵的计算提供更多数据。计算单应矩阵产生的掩码,可以有效筛选掉误匹配。

  从图 2 第 1 行可以看出,虽然目标图像和待检测图像尺寸不同,且目标图像上的目标与待检测图像上的目标形态也不同,但检测结果非常好。然而,单应矩阵的使用是有条件的。正如代码第 8 部分,目标图像上的目标与待检测图像上的目标必须能通过透视变换 perspectiveTransform 相互变换,否则这种检测效果就不好,如图 3。

请添加图片描述

图 3 目标图像上的目标与待检测图像上的目标不是透视变换关系

  从图 3 第 1 行可以看出,目标图像与待检测图像不是透视变换关系,检测结果很不理想。

2. 参考


  1. 模板匹配,博客园,2022。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值