使用OpenCV寻找包含另一张图像的最相似图像

7
如果标题不够清晰,让我们假设我有一个图像列表(10k+),我正在寻找一个目标图像。
以下是目标图像的示例: enter image description here 以下是我将要搜索以找到“相似”的图像的示例(ex1、ex2和ex3): enter image description here enter image description here enter image description here 这里是我所做的匹配(我使用KAZE)。
from matplotlib import pyplot as plt
import numpy as np
import cv2
from typing import List
import os
import imutils


def calculate_matches(des1: List[cv2.KeyPoint], des2: List[cv2.KeyPoint]):
    """
    does a matching algorithm to match if keypoints 1 and 2 are similar
    @param des1: a numpy array of floats that are the descriptors of the keypoints
    @param des2: a numpy array of floats that are the descriptors of the keypoints
    @return:
    """
    # bf matcher with default params
    bf = cv2.BFMatcher(cv2.NORM_L2)
    matches = bf.knnMatch(des1, des2, k=2)
    topResults = []
    for m, n in matches:
        if m.distance < 0.7 * n.distance:
            topResults.append([m])

    return topResults


def compare_images_kaze():
    cwd = os.getcwd()
    target = os.path.join(cwd, 'opencv_target', 'target.png')
    images_list = os.listdir('opencv_images')
    for image in images_list:
        # get my 2 images
        img2 = cv2.imread(target)
        img1 = cv2.imread(os.path.join(cwd, 'opencv_images', image))
        for i in range(0, 360, int(360 / 8)):
            # rotate my image by i
            img_target_rotation = imutils.rotate_bound(img2, i)

            # Initiate KAZE object with default values
            kaze = cv2.KAZE_create()
            kp1, des1 = kaze.detectAndCompute(img1, None)
            kp2, des2 = kaze.detectAndCompute(img2, None)
            matches = calculate_matches(des1, des2)

            try:
                score = 100 * (len(matches) / min(len(kp1), len(kp2)))
            except ZeroDivisionError:
                score = 0
            print(image, score)
            img3 = cv2.drawMatchesKnn(img1, kp1, img_target_rotation, kp2, matches,
                                      None, flags=2)
            img3 = cv2.cvtColor(img3, cv2.COLOR_BGR2RGB)
            plt.imshow(img3)
            plt.show()
            plt.clf()


if __name__ == '__main__':
    compare_images_kaze()

这是我的代码的输出结果:

ex1.png 21.052631578947366
ex2.png 0.0
ex3.png 42.10526315789473

enter image description here enter image description here enter image description here

它的表现还不错!它能够判断ex1是相似的,而ex2不相似。但是它表示ex3是相似的(比ex1更相似)。是否有额外的预处理或后处理(可能使用机器学习,假设机器学习确实有用)或者只需更改我的方法即可仅保留ex1作为相似项而不是ex3?

(请注意,我创建的这个分数是我在网上找到的。不确定它是否是一种准确的方法)

添加了更多示例如下:

另一组示例:

这就是我要搜索的内容

enter image description here

我希望上面的图像与中间和底部的图像相似(注意:我将目标图像旋转45度并将其与下面的图像进行比较)

特征匹配(如下面的答案所述)对于找到第二个图像的相似性非常有用,但是对于第三个图像却没有用(即使将其正确旋转)

enter image description here

enter image description here

enter image description here

4个回答

7

检测最相似的图像

代码

您可以使用模板匹配来检测其他图像中是否存在您想要检测的图像。其中,作为模板的是您希望检测的图像。我将该小图像保存在template.png中,而其他三个图像则分别是img1.pngimg2.pngimg3.png

我定义了一个函数,利用cv2.matchTemplate计算模板在图像中是否存在的置信度量。通过对每个图像应用该函数,置信度最高的图像即包含模板:

import cv2

template = cv2.imread("template.png", 0)
files = ["img1.png", "img2.png", "img3.png"]

for name in files:
    img = cv2.imread(name, 0)
    print(f"Confidence for {name}:")
    print(cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED).max())

输出结果:

Confidence for img1.png:
0.8906427
Confidence for img2.png:
0.4427919
Confidence for img3.png:
0.5933967

解释:

  1. 导入opencv模块,并通过将cv2.imread方法的第二个参数设置为0,将模板图像读取为灰度图像:
import cv2

template = cv2.imread("template.png", 0)
  1. 定义您要确定哪个图像包含模板的图像列表:
files = ["img1.png", "img2.png", "img3.png"]
  • 循环遍历文件名并将每个文件作为灰度图像读入:
  • for name in files:
        img = cv2.imread(name, 0)
    
    1. 最后,您可以使用cv2.matchTemplate在每个图像中检测模板。有许多检测方法可供选择,但是我决定使用cv2.TM_CCOEFF_NORMED方法:
        print(f"Confidence for {name}:")
        print(cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED).max())
    

    该函数的输出范围介于01之间,可以看到,它成功检测到第一张图像最有可能包含模板图像(具有最高置信度)


    可视化

    代码

    如果仅仅检测哪个图像包含模板还不够,您还可以尝试下面的代码以获得可视化效果:

    import cv2
    import numpy as np
    
    def confidence(img, template):
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
        res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
        conf = res.max()
        return np.where(res == conf), conf
    
    files = ["img1.png", "img2.png", "img3.png"]
    
    template = cv2.imread("template.png")
    h, w, _ = template.shape
    
    for name in files:
        img = cv2.imread(name)
        ([y], [x]), conf = confidence(img, template)
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
        text = f'Confidence: {round(float(conf), 2)}'
        cv2.putText(img, text, (x, y), 1, cv2.FONT_HERSHEY_PLAIN, (0, 0, 0), 2)
        cv2.imshow(name, img)
        
    cv2.imshow('Template', template)
    cv2.waitKey(0)
    

    输出:

    在此输入图片描述

    说明:

    1. 导入必要的库:
    import cv2
    import numpy as np
    

    定义一个函数,该函数将接受一张完整图像和一张模板图像。由于 `cv2.matchTemplate` 方法需要灰度图像,因此将这两个图像转换为灰度图像:
    def confidence(img, template):
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
    

    使用cv2.matchTemplate方法在图像中检测模板,并返回置信度最高的点的位置以及最高置信度。
        res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
        conf = res.max()
        return np.where(res == conf), conf
    

    定义您要确定哪个图像包含模板的图像列表,并读取模板图像:
    files = ["img1.png", "img2.png", "img3.png"]
    template = cv2.imread("template.png")
    
    1. 获取模板图像的大小,以便稍后在图像上绘制矩形:
    h, w, _ = template.shape
    
    1. 循环遍历文件名并读取每张图像。使用我们之前定义的confidence函数,获取检测到的模板左上角的x y位置以及置信度:
    for name in files:
        img = cv2.imread(name)
        ([y], [x]), conf = confidence(img, template)
    

    在图像的角落画一个矩形,将文本放置在图像上。最后,展示图像。
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
        text = f'Confidence: {round(float(conf), 2)}'
        cv2.putText(img, text, (x, y), 1, cv2.FONT_HERSHEY_PLAIN, (0, 0, 0), 2)
        cv2.imshow(name, img)
    

    另外,展示一下用于对比的模板:
    cv2.imshow('Template', template)
    cv2.waitKey(0)
    

    谢谢您的建议。我尝试使用模板匹配,对于这种情况它起作用了,但它并不能完全适用于其他情况(如果您想要,我可以发布示例)。您有其他的建议吗? - mike_gundy123
    @mike_gundy123 我可能有其他建议,但我需要先看看其他的例子。 - Ann Zen
    好的,如果有帮助的话,我添加了另一组示例! - mike_gundy123
    @mike_gundy123 谢谢,我添加了另一个答案。 - Ann Zen

    3
    我不确定给出的图像是否类似于您实际的任务或数据,但对于这种类型的图像,您可以尝试简单的模板匹配,参见这个OpenCV教程
    基本上,我只是按照教程进行了一些修改:
    import cv2
    import matplotlib.pyplot as plt
    
    # Read images
    examples = [cv2.imread(img) for img in ['ex1.png', 'ex2.png', 'ex3.png']]
    target = cv2.imread('target.png')
    h, w = target.shape[:2]
    
    # Iterate examples
    for i, img in enumerate(examples):
    
        # Template matching
        # cf. https://docs.opencv.org/4.5.2/d4/dc6/tutorial_py_template_matching.html
        res = cv2.matchTemplate(img, target, cv2.TM_CCOEFF_NORMED)
    
        # Get location of maximum
        _, max_val, _, top_left = cv2.minMaxLoc(res)
    
        # Set up threshold for decision target found or not
        thr = 0.7
        if max_val > thr:
    
            # Show found target in example
            bottom_right = (top_left[0] + w, top_left[1] + h)
            cv2.rectangle(img, top_left, bottom_right, (0, 255, 0), 2)
    
        # Visualization
        plt.figure(i, figsize=(10, 5))
        plt.subplot(1, 2, 1), plt.imshow(img[..., [2, 1, 0]]), plt.title('Example')
        plt.subplot(1, 2, 2), plt.imshow(res, vmin=0, vmax=1, cmap='gray')
        plt.title('Matching result'), plt.colorbar(), plt.tight_layout()
    
    plt.show()
    

    这是结果:

    Example 1

    Example 2

    Example 3

    ----------------------------------------
    System information
    ----------------------------------------
    Platform:      Windows-10-10.0.16299-SP0
    Python:        3.9.1
    PyCharm:       2021.1.1
    Matplotlib:    3.4.1
    OpenCV:        4.5.1
    ----------------------------------------
    

    编辑:为了强调来自不同颜色的信息,可以使用HSV颜色空间中的色调通道进行模板匹配:
    import cv2
    import matplotlib.pyplot as plt
    
    # Read images
    examples = [
        [cv2.imread(img) for img in ['ex1.png', 'ex2.png', 'ex3.png']],
        [cv2.imread(img) for img in ['ex12.png', 'ex22.png', 'ex32.png']]
    ]
    targets = [
        cv2.imread('target.png'),
        cv2.imread('target2.png')
    ]
    
    # Iterate examples and targets
    for i, (ex, target) in enumerate(zip(examples, targets)):
        for j, img in enumerate(ex):
    
            # Rotate last image from second data set
            if (i == 1) and (j == 2):
                img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
    
            h, w = target.shape[:2]
    
            # Get hue channel from HSV color space
            target_h = cv2.cvtColor(target, cv2.COLOR_BGR2HSV)[..., 0]
            img_h = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)[..., 0]
    
            # Template matching
            # cf. https://docs.opencv.org/4.5.2/d4/dc6/tutorial_py_template_matching.html
            res = cv2.matchTemplate(img_h, target_h, cv2.TM_CCOEFF_NORMED)
    
            # Get location of maximum
            _, max_val, _, top_left = cv2.minMaxLoc(res)
    
            # Set up threshold for decision target found or not
            thr = 0.6
            if max_val > thr:
    
                # Show found target in example
                bottom_right = (top_left[0] + w, top_left[1] + h)
                cv2.rectangle(img, top_left, bottom_right, (0, 255, 0), 2)
    
            # Visualization
            plt.figure(i * 10 + j, figsize=(10, 5))
            plt.subplot(1, 2, 1), plt.imshow(img[..., [2, 1, 0]]), plt.title('Example')
            plt.subplot(1, 2, 2), plt.imshow(res, vmin=0, vmax=1, cmap='gray')
            plt.title('Matching result'), plt.colorbar(), plt.tight_layout()
            plt.savefig('{}.png'.format(i * 10 + j))
    
    plt.show()
    

    新结果:

    enter image description here

    enter image description here

    enter image description here

    enter image description here

    enter image description here

    enter image description here


    嗯,我看到了,但是不太确定它能否很好地工作。你觉得有没有可能将这两种方法(Kaze和模板匹配)结合起来,以实现更强大的解决方案? - mike_gundy123
    更新了更多的示例,顺便说一下(感谢您在模板匹配方面的帮助!) - mike_gundy123
    模板匹配不具有旋转和/或缩放不变性。新目标中的两条线靠得很近,而在三个新示例中,它们更分散。这就是为什么即使正确旋转后,模板匹配也会失败的原因。 - HansHirse
    那真是不幸 :/。我还有什么其他解决这个问题的推荐路线吗? - mike_gundy123
    我添加了一个想法,结合了HSV颜色空间中的色调通道,但仍无法保证在任意输入上正常工作。 - HansHirse
    显示剩余4条评论

    2

    概念

    我们可以使用cv2.matchTemplate方法来检测一张图片在另一张图片中的位置,但对于你的第二组图像,它们具有旋转。此外,我们需要考虑颜色。

    cv2.matchTemplate将输入一张图像、一个模板(另一张图片)和一个模板检测方法,并返回一个灰度数组,其中灰度数组中最亮的点是具有最高置信度的模板所在点。

    我们可以使用4个不同角度的模板,并使用结果置信度最高的那个。当我们检测到可能匹配模板的点时,我们使用一个函数(我们自己定义)检查模板中最频繁的颜色是否存在于我们检测到的图像块中。如果不存在,则忽略该块,无论返回的置信度值如何。

    代码

    import cv2
    import numpy as np
    
    def frequent_colors(img, vals=3):
        colors, count = np.unique(np.vstack(img), return_counts=True, axis=0)
        sorted_by_freq = colors[np.argsort(count)]
        return sorted_by_freq[-vals:]
    
    def get_templates(img):
        template = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        for i in range(3):
            yield cv2.rotate(template, i)
            
    def detect(img, template, min_conf=0.45):
        colors = frequent_colors(template)
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        conf_max = min_conf
        shape = 0, 0, 0, 0
        for tmp in get_templates(template):
            h, w = tmp.shape
            res = cv2.matchTemplate(img_gray, tmp, cv2.TM_CCOEFF_NORMED)
            for y, x in zip(*np.where(res > conf_max)):
                conf = res[y, x]
                if conf > conf_max:
                    seg = img[y:y + h, x:x + w]
                    if all(np.any(np.all(seg == color, -1)) for color in colors):
                        conf_max = conf
                        shape = x, y, w, h
        return shape
    
    files = ["img1_2.png", "img2_2.png", "img3_2.png"]
    template = cv2.imread("template2.png")
    
    for name in files:
        img = cv2.imread(name)
        x, y, w, h = detect(img, template)
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
        cv2.imshow(name, img)
    
    cv2.imshow('Template', template)
    cv2.waitKey(0)
    

    输出

    enter image description here

    说明

    1. 导入必要的库:
    import cv2
    import numpy as np
    

    定义一个函数frequent_colors,它将接受一张图片并返回图片中最常见的颜色。可选参数val表示要返回多少种颜色;如果val3,则会返回最常见的三种颜色:
    def frequent_colors(img, vals=3):
        colors, count = np.unique(np.vstack(img), return_counts=True, axis=0)
        sorted_by_freq = colors[np.argsort(count)]
        return sorted_by_freq[-vals:]
    
    1. 定义一个名为get_templates的函数,该函数将接受一张图片,并以4个不同的角度生成该图片的灰度版本 - 原始、顺时针90度、180度和逆时针90度:
    def get_templates(img):
        template = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        for i in range(3):
            yield cv2.rotate(template, i)
    

    4. 定义一个名为 detect 的函数,该函数将接收一张图片和一个模板图片,并返回检测到的模板在图片上的边界框的 x、y、w、h 值。对于此函数,我们将使用之前定义的 frequent_colorsget_templates 函数。参数 min_conf 是将检测结果分类为实际检测所需的最小置信度值。
    def detect(img, template, min_conf=0.45):
    

    检测模板中出现频率最高的三种颜色,并将它们存储在变量colors中。同时,定义主图像的灰度版本:
        colors = frequent_colors(template)
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    

    定义检测到的最大置信度的初始值,以及检测到的补丁的初始值:
        conf_max = min_conf
        shape = 0, 0, 0, 0
    

    循环遍历4个角度的灰度模板,获取灰度模板的形状(因为旋转会改变形状),并使用cv2.matchTemplate方法在图像上获取检测到的模板的灰度数组:
        for tmp in get_templates(template):
            h, w = tmp.shape
            res = cv2.matchTemplate(img_gray, tmp, cv2.TM_CCOEFF_NORMED)
    

    循环遍历检测到的模板的x,y坐标,其中置信度大于conf_min,并将置信度存储在变量conf中。如果conf大于初始最大置信度变量(conf_max),则继续检测模板中出现的三种最常见颜色是否存在于图像的补丁中:
            for y, x in zip(*np.where(res > conf_max)):
                conf = res[y, x]
                if conf > conf_max:
                    seg = img[y:y + h, x:x + w]
                    if all(np.any(np.all(seg == color, -1)) for color in colors):
                        conf_max = conf
                        shape = x, y, w, h
    

    在最后,我们可以返回形状。如果图像中没有检测到模板,则该形状将是为其定义的初始值,0, 0, 0, 0
        return shape
    
    1. 最后,循环遍历每个图像并使用我们定义的detect函数获取边界框的x、y、w、h。使用cv2.rectangle方法将边界框绘制到图像上:
    files = ["img1_2.png", "img2_2.png", "img3_2.png"]
    template = cv2.imread("template2.png")
    
    for name in files:
        img = cv2.imread(name)
        x, y, w, h = detect(img, template)
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
        cv2.imshow(name, img)
    
    cv2.imshow('Template', template)
    cv2.waitKey(0)
    

    -2

    首先,数据以图形的形式呈现,您不能从它们的数值数据中获取重叠值吗?

    您是否尝试过对白蓝色变化进行边缘检测,然后对这些边缘拟合一些圆,并检查它们是否重叠?

    由于输入数据相当受控制(没有有机摄影或视频),也许您不必走机器学习的路线。


    抱歉,您说的第一部分是什么意思?我可以尝试使用这个来进行边缘检测。我的主要问题是如何将边缘检测融入某种“相似度”评分中。 - mike_gundy123
    1
    我曾评论过数据是以带有坐标轴的图形呈现的(0:300,0:400)。那些数据被以某种方式绘制出来了。为什么要使用cv2来确定重叠部分,而不是处理用于生成图形的基础数值数据呢? - Shawn Ramirez
    是的,那些原始数据(关键点和描述符)被用于calculate_matches()函数中,而图表的作用是以视觉化的方式展示KAZE算法认为的“相似”,如果这样说更容易理解。 - mike_gundy123

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接