使用 OpenCV 检测图像中已知形状/对象的方法

Posted

技术标签:

【中文标题】使用 OpenCV 检测图像中已知形状/对象的方法【英文标题】:Methods for detecting a known shape/object in an image using OpenCV 【发布时间】:2020-07-07 02:36:02 【问题描述】:

我的任务是使用 OpenCV 检测给定图像中的对象(我不在乎它是 Python 还是 C++ 实现)。该对象(在下面的三个示例中显示)是一个黑色矩形,其中有五个白色矩形。所有维度都是已知的。

但是,图像的旋转、比例、距离、视角、照明条件、相机焦距/镜头和背景未知。黑色矩形的边缘不能保证完全可见,但是五个白色矩形前面永远不会有任何东西——它们总是完全可见的。最终目标是能够检测图像中该对象的存在,并旋转、缩放和裁剪以显示删除了透视的对象。鉴于它的四个角,我相当有信心可以调整图像以裁剪到对象。但是,我不太有信心能可靠地找到这四个角。在模棱两可的情况下,最好不要找到对象,而不是将图像的某些其他特征误识别为对象。

使用 OpenCV,我想出了以下方法,但是我觉得我可能遗漏了一些明显的东西。是否有更多可用的方法,或者其中一种是最佳解决方案?

基于边缘的轮廓

第一个想法是寻找物体的外边缘。

使用 Canny 边缘检测(在缩放到已知尺寸、灰度和高斯模糊之后),找到与对象的外部形状最匹配的轮廓。 这处理透视、颜色、尺寸问题,但在例如背景复杂或图像中其他地方存在与对象形状相似的东西时会失败。也许这可以通过一套更好的规则来找到正确的轮廓来改进——可能涉及五个白色矩形以及外边缘。

特征检测

下一个想法是使用特征检测来匹配已知模板。

使用 ORB 特征检测、描述符匹配和单应性 (from this tutorial) 失败,我相信是因为它检测到的特征与对象内的其他特征非常相似(许多核心器恰好是四分之一白色和三四分之一黑色)。但是,我确实喜欢匹配已知模板的想法——这个想法对我来说很有意义。我想虽然因为对象在几何上非常基本,所以在特征匹配步骤中可能会发现很多误报。

平行线

使用 Houghlines 或 HoughLinesP,寻找均匀分布的平行线。刚刚开始走这条路,所以需要研究阈值处理等的最佳方法。虽然对于具有复杂背景的图像来说它看起来很乱,但我认为它可能工作得很好,因为我可以依靠黑色对象中的白色矩形应该始终具有高对比度,可以很好地指示线条的位置。

'条形码扫描'

我的最终想法是逐行扫描图像,寻找白色到黑色的图案。

我还没有开始这个方法,但想法是取一条图像(以某个角度),转换为 HSV 颜色空间,并在图像中寻找顺序出现五次的常规黑白图案值列。这个想法听起来很有希望,因为我认为它应该忽略许多未知变量。

想法

我查看了许多 OpenCV 教程,以及诸如 this one 之类的 SO 问题,但是因为我的对象在几何上非常简单,所以我在实现给出的想法时遇到了问题。

我觉得这是一个可以完成的任务,但我的斗争是知道进一步追求哪种方法。我已经对前两个想法进行了相当多的实验,虽然我没有取得任何非常可靠的结果,但也许我缺少一些东西。是否有一种我没有想到的标准方法来完成这项任务,或者我建议的方法之一是最明智的?

编辑:一旦使用上述方法之一(或其他方法)找到角点,我正在考虑使用 Hu Moments 或 OpenCV 的 matchShapes() 函数来消除任何误报。

EDIT2:根据@Timo 的要求添加了更多输入图像示例


Orig1 Orig2 Orig3 Extra image 1 Extra image 2 Extra image 3 Extra image 4

【问题讨论】:

条形码方法看起来很有趣。您是否尝试过结合不同的方法?我会使用多种算法并将它们与多数投票或最大值搜索结合起来。 @Timo 我确实考虑过将它们结合起来,然后尝试评估每种算法的某种置信度值。我最初对此想法的担忧是,花时间微调一个算法已经足够长了,更不用说多个了!但我确实认为这可能对健壮性有意义,谢谢! 集成算法的另一个优点是,您不必对每个单独的算法进行精确微调,因为它与其他算法相比较,因此本身没有那么大的影响。使用更轻量级和简单的算法可能比使用一些复杂和微调的算法更有效。如果您将这种方法推向极端(非常简单,但有很多算法或其实例),您会注意到这是许多机器学习算法的运行方式(忽略学习部分)。 我还有另一个想法可能会很好,但我没有时间解释它。您能否再添加一些输入图像(不带任何标记)? @Timo 这很有意义:多种算法。我已将图像添加到帖子中。还需要注意的是,对象上的三个 + 标记旨在用作旋转/镜像保护,以便在所选算法识别对象的四个角并裁剪后使用。 【参考方案1】:

查看问题陈述后,我能够计算出相当不错的结果。

在这里,我们使用了 KAZE 特征,这是一种在非线性尺度空间中的新型多尺度 2D 特征检测和描述算法。以前的方法通过构建或逼近图像的高斯尺度空间来检测和描述不同尺度级别的特征。

但是,高斯模糊不尊重对象的自然边界,并且在相同程度上平滑了细节和噪声,从而降低了定位精度和独特性。相反,我们通过非线性扩散滤波在非线性尺度空间中检测和描述二维特征。通过这种方式,我们可以使模糊局部适应图像数据,减少噪声但保留对象边界,获得优越的定位精度和独特性。

非线性尺度空间是使用高效的加性算子分裂 (AOS) 技术和可变电导扩散构建的。我们对基准数据集进行了广泛的评估,并在可变形表面上进行了实际匹配应用。尽管由于非线性尺度空间的构建,我们的特征在计算上比 SURF 更昂贵,但与 SIFT 相比,我们的结果显示,与以前的最先进方法相比,在检测和描述方面的性能都向前迈进了一步.

您可以在研究论文here中找到更多参考资料。


import os, cv2, random
import numpy as np
import matplotlib.pyplot as plt


#show image
def displayImage(input_img, display_title=None):
    im_shape = input_img.shape
    c = 3
    if len(im_shape) >= 3:
        c = im_shape[2]
    if len(im_shape) == 2:
        c = 1

    if c == 3:
        rgb_img = cv2.cvtColor(input_img.copy(), cv2.COLOR_BGR2RGB)
        plt.imshow(rgb_img)
    if c == 1:
        plt.imshow(input_img,cmap='gray')

    plt.axis('off')
    plt.grid(False)
    if not display_title is None:
        plt.title(display_title)
    plt.show()


def featureExtractor(image, fd):
    kpts, desc = fd.detectAndCompute(image, None)
    return kpts, desc


def featureMatching(kpts1, desc1, kpts2, desc2, fd='kaze'):

    if desc1 is None and desc_2 is None:
        print('Empty descriptor')
        return

    if fd == 'akaze':
        # create BFMatcher object
        bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
        # Match descriptors.
        matches = bf.match(desc1, desc2)
        # Sort them in the order of their distance.
        matches = sorted(matches, key = lambda x:x.distance)
        # good_matches = matches[:10]
        return matches
    else:
        # Matching descriptor vectors with a FLANN based matcher
        matcher = cv2.DescriptorMatcher_create(cv2.DescriptorMatcher_FLANNBASED)
        knn_matches = matcher.knnMatch(desc1, desc2, 2)
        # Filter matches using the Lowe's ratio test
        good_matches = []
        for m,n in knn_matches:
            ratio_thresh = 0.7
            if m.distance < ratio_thresh * n.distance:
                good_matches.append(m)
        return good_matches


def reprojectionError(matches, kpts1, kpts2, M):
    ptsA = np.float32([ kpts1[m.queryIdx].pt for m in good_matches ])
    ptsA_ = ptsA.reshape(-1,1,2)
    ptsB = np.float32([ kpts2[m.trainIdx].pt for m in good_matches ])

    ptsB_ = cv2.perspectiveTransform(ptsA_, M)
    ptsB_ = ptsB_.reshape(ptsB.shape)
    reproj_err = 0.

    for i in range(len(ptsB)):
        delx = ptsB[i][0] - ptsB_[i][0]
        delx *= delx

        dely = ptsB[i][1] - ptsB_[i][1]
        dely *= dely

        reproj_err += delx + dely
        reproj_err = np.sqrt(reproj_err)
        #   print 'reprojection error:', reproj_err
    reproj_err /= float(len(ptsB))
    return reproj_err


def drawMatches(img1, img2, good_matches, kpts1, desc1, kpts2, desc2):
    src_pts = np.float32([ kpts1[m.queryIdx].pt for m in good_matches ]).reshape(-1,1,2)
    dst_pts = np.float32([ kpts2[m.trainIdx].pt for m in good_matches ]).reshape(-1,1,2)

    M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC,5.0)
    if M is not None:
        matchesMask = mask.ravel().tolist()
        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,M)
        dst += (w, 0)  # adding offset

        draw_params = dict(matchColor = (0,0,255), # draw matches in green color
                        singlePointColor = None,
                        matchesMask = matchesMask, # draw only inliers
                        flags = 2)

        result = cv2.drawMatches(img1, kpts1, img2, kpts2, good_matches, None,**draw_params)
        # Draw bounding box in Red
        cv2.polylines(result, [np.int32(dst)], True, (0,0,255),3, cv2.LINE_AA)
        displayImage(result, 'result')
        return M


fd = 
    'kaze': cv2.KAZE_create(),
    'akaze': cv2.AKAZE_create()

key = 'akaze'

detect = 'path/to/cropped/template/of/the/object/to/be/detected'
target = 'path/to/target/image/where/the/object/to/be/detected'

template = cv2.imread(detect)
scene = cv2.imread(target)

# extract features form the template image
kpts1, desc1  = featureExtractor(template, fd[key])
# extract features form the scene image
kpts2, desc2 = featureExtractor(scene, fd[key])

good_matches = featureMatching(kpts1, desc1, kpts2, desc2, key)

if good_matches is not None:
    M = drawMatches(scene, template, good_matches, kpts1, desc1, kpts2, desc2)
    reprojection_error = reprojectionError(good_matches, kpts1, kpts2, M)
    print(f'Reprojection error: reprojection_error')

基于良好匹配的关键点,您可以在图像中找到对象的边界多边形。使用直方图均衡等图像预处理可以进一步增强结果。

结果:

【讨论】:

【参考方案2】:

我花了一些时间研究这个问题并编写了一个小 Python 脚本。我正在检测您的形状内的白色矩形。将代码粘贴到 .py 文件中,并将所有输入图像复制到 input 子文件夹中。图像的最终结果只是一个虚拟 atm,脚本尚未完成。我会在接下来的几天里继续尝试。该脚本将创建一个 debug 子文件夹,其中将保存一些显示当前检测状态的图像。

import numpy as np
import cv2
import os

INPUT_DIR = 'input'
DEBUG_DIR = 'debug'
OUTPUT_DIR = 'output'
IMG_TARGET_SIZE = 1000

# each algorithm must return a rotated rect and a confidence value [0..1]: (((x, y), (w, h), angle), confidence)

def main():
    # a list of all used algorithms
    algorithms = [rectangle_detection] 

    # load and prepare images
    files = list(os.listdir(INPUT_DIR))
    images = [cv2.imread(os.path.join(INPUT_DIR, f), cv2.IMREAD_GRAYSCALE) for f in files]
    images = [scale_image(img) for img in images]

    for img, filename in zip(images, files):
        results = [alg(img, filename) for alg in algorithms]
        roi, confidence = merge_results(results)

        display = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
        display = cv2.drawContours(display, [cv2.boxPoints(roi).astype('int32')], -1, (0, 230, 0))            
        cv2.imshow('img', display)
        cv2.waitKey()


def merge_results(results):
    '''Merges all results into a single result.'''
    return max(results, key=lambda x: x[1]) 

def scale_image(img):    
    '''Scales the image so that the biggest side is IMG_TARGET_SIZE.'''
    scale = IMG_TARGET_SIZE / np.max(img.shape)
    return cv2.resize(img, (0,0), fx=scale, fy=scale)     


def rectangle_detection(img, filename):    
    debug_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    _, binarized = cv2.threshold(img, 50, 255, cv2.THRESH_BINARY)    
    contours, _ = cv2.findContours(binarized, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    # detect all rectangles
    rois = []
    for contour in contours:
        if len(contour) < 4:
            continue
        cont_area = cv2.contourArea(contour)
        if not 1000 < cont_area < 15000: # roughly filter by the volume of the detected rectangles
            continue
        cont_perimeter = cv2.arcLength(contour, True)
        (x, y), (w, h), angle = rect = cv2.minAreaRect(contour)
        rect_area = w * h
        if cont_area / rect_area < 0.8: # check the 'rectangularity'
            continue        
        rois.append(rect)

    # save intermediate results in the debug folder
    rois_img = cv2.drawContours(debug_img, contours, -1, (0, 0, 230))
    rois_img = cv2.drawContours(rois_img, [cv2.boxPoints(rect).astype('int32') for rect in rois], -1, (0, 230, 0))
    save_dbg_img(rois_img, 'rectangle_detection', filename, 1)

    # todo: detect pattern

    return rois[0], 1.0 # dummy values


def save_dbg_img(img, folder, filename, index=0):
    '''Writes the given image to DEBUG_DIR/folder/filename_index.png.'''
    folder = os.path.join(DEBUG_DIR, folder)
    if not os.path.exists(folder):
        os.makedirs(folder)
    cv2.imwrite(os.path.join(folder, '_:02.png'.format(os.path.splitext(filename)[0], index)), img)


if __name__ == "__main__":
    main()

这是当前 WIP 的示例图像

下一步是检测多个矩形之间的模式/关系。当我取得进展时,我会更新这个答案。

【讨论】:

哇,太棒了!谢谢你付出了这么多工作。我已经开始写条码扫描方法的代码,会考虑如何确定一个置信度值。但是您发送的内容已经运行良好,所以我现在对这项任务更有信心

以上是关于使用 OpenCV 检测图像中已知形状/对象的方法的主要内容,如果未能解决你的问题,请参考以下文章

教你如何使用 OpenCV检测图像中的轮廓

教你如何使用 OpenCV检测图像中的轮廓

使用 OpenCV 进行三角形检测

OpenCV - 检测矩形或五边形

Python+OpenCV图像处理之直线检测

Python+OpenCV图像处理(十四)—— 直线检测