Python使用openCV在灰度图像上进行带掩模的matchTemplate

7
我有一个项目,我想在类似于以下图片的图像中定位一堆箭头: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. 掩码/模板/图像应该具有的通道数。我已经尝试了一个使用彩色4通道png文件的例子stackoverflow eg.,但不确定它如何转换为灰度或3通道jpeg文件。
  2. 掩码数组的值。例如,掩盖的像素应该是1还是255?

非常感谢任何帮助。

更新 我在代码中修复了一个微不足道的错误;必须在matchTemplate()的参数中使用mask=mask。这与使用mask值为255一起使得结果有所不同。然而,现在我得到了大量的误报,如下所示: http://ibb.co/esfTnk请注意,误报比真正的匹配更强烈。 有什么方法可以改进我的掩码以解决这个问题吗?目前,我只是使用我的模板的黑白转换。


尝试使用仅包含0255强度值的单通道掩码。 - ZdaR
@ZdaR 我尝试了,但它不起作用。似乎即使使用零矩阵或255矩阵的掩码也对匹配没有影响。 - TonyZ
结果证明这个方法起作用了,我犯了一个微不足道的错误。我需要在matchTemplate()中使用参数mask=mask。 然而还有进一步的复杂情况。我已经更新了问题。谢谢。 - TonyZ
1个回答

9
你已经解决了第一个问题,但我会稍微扩展一下:
对于二进制掩码,它应该是 uint8 类型,其中的值只有零或非零。值为零的位置将被忽略,如果它们是非零的,则包含在掩码中。你可以传递一个 float32 作为掩码,这样可以让你加权像素;因此,值为0时会被忽略,1时会被包含,0.5时会被包含,但只会给另一个像素的一半权重。请注意,掩码仅支持 TM_SQDIFFTM_CCORR_NORMED,但这没关系,因为你正在使用后者。 matchTemplate 的掩码仅支持单通道。正如你发现的那样,mask 不是一个位置参数,所以必须在参数中使用键来调用它,例如 mask=your_mask。所有这些都在 OpenCV 文档的这个页面中明确说明。
现在是新问题:
它与你使用的方法和使用 jpg 有关。查看规范化方法的公式。当图像完全为零时,你将得到错误的结果,因为你将除以零。但这不是确切的问题——因为它返回 nan,而且 np.nan > value 总是返回 false,所以你永远不会从 nan 值中绘制正方形。
相反,问题出在边缘情况,即你会得到一些非零值的提示;因为你使用的是 jpg 图像,不是所有的黑色值都恰好为 0;事实上,许多值不是。请注意,在公式中,你正在除以平均值,并且当你的图像窗口中有像 1、2、5 等值时,平均值将非常小,因此它会使相关值爆炸。你应该使用 TM_SQDIFF(因为它是唯一允许掩码的其他方法)。此外,由于你使用的是 jpg,大多数掩码都是无用的,因为任何非零值(甚至是 1)都计入包含范围内。你应该使用 png 作为掩码。只要模板有适当的掩码,使用 jpgpng 都没关系。
使用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的阈值就可以解决问题:

使用正方形差异和8的阈值进行匹配定位

我在您的代码中修改的唯一内容是将所有图像更改为png,切换到TM_SQDIFF,确保loc查找小于阈值而不是大于阈值的值,并使用MATCH_THRESH为8。至少我认为这是我改变的全部内容。请检查一下以防万一:

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)

@TonyZ 看起来你的评论还没有写完,可以继续说一下。png 文件是未压缩的,而 jpg 是压缩的。对于 png,矩阵中的内容与写入文件的内容完全相同;而 jpg 则是压缩的,因此其值与你编写的矩阵不完全相同。 - alkasm
嗨!谢谢你的回答!我发现使用TM_SQDIFF可以解决问题,但不知道可以使用png作为掩模。我必须使用16的阈值和jpeg掩模,但8更好。虽然匹配每个旋转角度有点笨重,但我需要恢复箭头的方向以供我的程序使用。再次感谢你!现在我对底层发生的事情有了更好的理解。 - TonyZ
1
这并不笨重,这是非常清晰的代码(这也是我回答如此详细的原因之一,所以谢谢你),考虑到正在进行的操作,它运行起来并不需要很长时间(部分原因是因为您的模板足够小)。 - alkasm
1
@AlexanderReynolds 这是一条非常有价值的回答 - 谢谢你 - PNG提示非常宝贵。 - jtlz2

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