Python 带遮罩的灰度图像上的 openCV matchTemplate

Python openCV matchTemplate on grayscale image with masking

我有一个项目,我想在图像中找到一堆箭头,如下所示:ibb.co/dSCAYQ 使用以下模板:ibb.co/jpRUtQ

我在 Python 中使用 cv2 的模板匹配功能。我的算法是将模板旋转 360 度并匹配每次旋转。我得到以下结果:ibb.co/kDFB7k

如您所见,除了两个非常靠近的箭头外,它运行良好,例如另一个箭头位于模板的黑色区域中。

我正在尝试使用掩码,但似乎 cv2 根本没有应用我的掩码,即无论掩码数组具有什么值,匹配都是相同的。已经尝试了两天,但 cv2 的有限文档没有帮助。

这是我的代码:

import numpy as np
import cv2
import os
from scipy import misc, ndimage

STRIPPED_DIR = #Image dir
TMPL_DIR = #Template dir
MATCH_THRESH = 0.9
MATCH_RES = 1  #specifies degree-interval at which to match

def make_templates():
    base = misc.imread(os.path.join(TMPL_DIR,'base.jpg')) # The templ that I rotate to make 360 templates
    for deg in range(360):
        print('making template: ' + str(deg))
        tmpl = ndimage.rotate(base, deg)
        misc.imsave(os.path.join(TMPL_DIR, 'tmp' + str(deg) + '.jpg'), tmpl)

def make_masks():
    for deg in range(360):
        tmpl = cv2.imread(os.path.join(TMPL_DIR, 'tmp' + str(deg) + '.jpg'), 0)
        ret2, mask = cv2.threshold(tmpl, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
        cv2.imwrite(os.path.join(TMPL_DIR, 'mask' + str(deg) + '.jpg'), mask)

def match(img_name):
    img_rgb = cv2.imread(os.path.join(STRIPPED_DIR, img_name))
    img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)

    for deg in range(0, 360, MATCH_RES):
        tmpl = cv2.imread(os.path.join(TMPL_DIR, 'tmp' + str(deg) + '.jpg'), 0)
        mask = cv2.imread(os.path.join(TMPL_DIR, 'mask' + str(deg) + '.jpg'), 0)
        w, h = tmpl.shape[::-1]
        res = cv2.matchTemplate(img_gray, tmpl, cv2.TM_CCORR_NORMED, mask=mask)
        loc = np.where( res >= MATCH_THRESH)

        for pt in zip(*loc[::-1]):
            cv2.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
        cv2.imwrite('res.png',img_rgb)

一些我认为可能是错误但不确定如何解决的事情:

  1. mask/tmpl/img 应具有的频道数。我尝试了一个带有彩色 4 通道 png 的示例 ,但不确定它如何转换为灰度或 3 通道 jpeg。
  2. 掩码数组的值。例如屏蔽掉的像素应该是 1 还是 255?

非常感谢任何帮助。

更新 我修复了代码中的一个小错误; matchTemplate() 的参数中必须使用 mask=mask。这与使用 255 的掩码值相结合产生了差异。但是,现在我得到了大量的误报: http://ibb.co/esfTnk 请注意,假阳性比真阳性的相关性更强。 关于如何修复我的面具以解决此问题的任何指示?现在我只是简单地使用我的模板的黑白转换。

您已经解决了第一个问题,但我将对它们进行一些扩展:

对于二进制掩码,它应该是 uint8 类型,其中值只是零或非零。零位置将被忽略,如果它们不为零,则包含在掩码中。您可以传递 float32 而不是作为掩码,在这种情况下,它可以让您 weight 像素;所以 0 的值是忽略,1 是包含,.5 是包含但只给它一半的权重作为另一个像素。请注意,只有 TM_SQDIFFTM_CCORR_NORMED 支持掩码,但这没关系,因为您使用的是后者。 matchTemplate 的掩码仅为单通道。正如您发现的那样,mask 不是位置参数,因此必须使用参数中的键 mask=your_mask 调用它。所有这些在 this page on the OpenCV docs.

中都非常明确

现在开始新一期:

这与您使用的方法以及您使用 jpgs 的事实有关。看看 formulas for the normed methods。在图像完全为零的地方,您将得到错误的结果,因为您将除以零。但这不是确切的问题---因为 returns nannp.nan > value 总是 returns 错误,所以你永远不会从 nan 中画出一个正方形值。

相反,问题出在边缘情况下,您会得到非零值的提示;并且因为您使用的是 jpg 图像,所以并非所有黑色值都恰好为 0;事实上,很多都不是。请注意公式中的平均值,当图像中有 1、2、5 等值时,平均值将非常小 window,因此它会破坏相关值.您应该改用 TM_SQDIFF (因为它是唯一允许掩码的其他方法)。此外,因为您使用的是 jpg,所以您的大部分掩码都毫无价值,因为任何非零值(甚至 1)都算作包含。您应该使用 pngs 作为掩码。只要模板有合适的掩码,模板使用 jpg 还是 png 都没有关系。

对于 TM_SQDIFF,您不是在寻找最大值,而是在寻找最小值——您希望模板和图像补丁之间的差异最小。您知道差异应该非常小——像素完美匹配恰好为 0,您可能不会得到。您可以稍微调整一下阈值。请注意,您总是会为每次旋转获得非常接近的值,因为模板的性质——小箭头条几乎不会添加那么多正值,并且不一定保证一个度数的离散化是完全正确的(除非你那样制作图像)。但是,即使箭头指向完全错误的方向,也仍然会非常接近,因为有很多重叠;并且朝向正确方向的箭头将真正接近正确方向的值。

在您 运行 代码时预览平方差的结果是什么:

res = cv2.matchTemplate(img_gray, tmpl, cv2.TM_SQDIFF, mask=mask)
cv2.imshow("result", res.astype(np.uint8))
if cv2.waitKey(0) & 0xFF == ord('q'):
    break

可以看到模板的各个方向基本上都匹配的很好

不管怎么说,门槛 8 似乎就够了:

我在你的代码中唯一修改的是将所有图像更改为 pngs,切换到 TM_SQDIFF,确保 loc 查找值 less than 阈值而不是大于,并使用 8 的 MATCH_THRESH。至少我认为这就是我改变的全部。看看以防万一:

import numpy as np
import cv2
import os
from scipy import misc, ndimage

STRIPPED_DIR = ...
TMPL_DIR = ...
MATCH_THRESH = 8
MATCH_RES = 1  #specifies degree-interval at which to match

def make_templates():
    base = misc.imread(os.path.join(TMPL_DIR,'base.jpg')) # The templ that I rotate to make 360 templates
    for deg in range(360):
        print('making template: ' + str(deg))
        tmpl = ndimage.rotate(base, deg)
        misc.imsave(os.path.join(TMPL_DIR, 'tmp' + str(deg) + '.png'), tmpl)

def make_masks():
    for deg in range(360):
        tmpl = cv2.imread(os.path.join(TMPL_DIR, 'tmp' + str(deg) + '.png'), 0)
        ret2, mask = cv2.threshold(tmpl, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
        cv2.imwrite(os.path.join(TMPL_DIR, 'mask' + str(deg) + '.png'), mask)

def match(img_name):
    img_rgb = cv2.imread(os.path.join(STRIPPED_DIR, img_name))
    img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)

    for deg in range(0, 360, MATCH_RES):
        tmpl = cv2.imread(os.path.join(TMPL_DIR, 'tmp' + str(deg) + '.png'), 0)
        mask = cv2.imread(os.path.join(TMPL_DIR, 'mask' + str(deg) + '.png'), 0)
        w, h = tmpl.shape[::-1]
        res = cv2.matchTemplate(img_gray, tmpl, cv2.TM_SQDIFF, mask=mask)

        loc = np.where(res < MATCH_THRESH)
        for pt in zip(*loc[::-1]):
            cv2.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
        cv2.imwrite('res.png',img_rgb)