图像处理:将扫描图像映射到具有许多相同特征的模板图像上

Image Processing: Mapping a scanned image on a template image with many identical features

问题描述

我们正在尝试将扫描图像与模板图像相匹配:

模板图像包含一组不同大小和轮廓属性(闭合、左开和右开)的心形。模板中的每颗心都是一个我们知道其位置、大小和轮廓类型的感兴趣区域。我们的目标是将扫描图像匹配到模板上,以便我们可以在扫描图像中提取这些 ROI。在扫描图像中,其中一些心是交叉的,它们将被呈现给分类器,分类器决定它们是否交叉。

我们的方法

根据 PyImageSearch 上的教程,我们尝试使用 ORB 来查找匹配的关键点(下面包含代码)。这应该允许我们计算将扫描图像映射到模板图像上的透视变换矩阵。

我们尝试了一些预处理步骤,例如阈值 and/or 模糊扫描图像。我们也尽可能地增加了特征的最大数量。

问题

该方法不适用于我们的图像集。这可以在下图中看到: 貌似很多关键点映射到模板图像的错误部分,所以变换矩阵计算不正确

ORB 是这里使用的正确技术吗,或者是否有可以微调算法的参数以提高性能?感觉好像我们错过了一些应该让它起作用的简单东西,但我们真的不知道如何继续使用这种方法:)。

我们正在尝试一种替代技术,我们将扫描与各个心形相互关联。这应该给出一个在心脏位置有峰值的图像。通过在这些峰周围绘制边界框,我们希望将该边界框映射到模板的边界框上(我可以根据要求详细说明)

非常感谢任何建议!

import cv2 as cv
import matplotlib.pyplot as plt
import numpy as np


# Preprocessing parameters
THRESHOLD = True
BLUR      = False

# ORB parameters
MAX_FEATURES = 4048
KEEP_PERCENT = .01
SHOW_DEBUG = True

# Convert both the input image and template to grayscale
scan_file = r'scan.jpg'
template_file = r'template.jpg'

scan     = cv.imread(scan_file)
template = cv.imread(template_file)

scan_gray     = cv.cvtColor(scan, cv.COLOR_BGR2GRAY)
template_gray = cv.cvtColor(template, cv.COLOR_BGR2GRAY)

if THRESHOLD:
    _,  scan_gray     = cv.threshold(scan_gray, 127, 255, cv.THRESH_BINARY)
    _, template_gray  = cv.threshold(template_gray, 127, 255, cv.THRESH_BINARY)
    
if BLUR:
    scan_gray = cv.blur(scan_gray, (5, 5))
    template_gray = cv.blur(template_gray, (5, 5))

# Use ORB to detect keypoints and extract (binary) local invariant features
orb = cv.ORB_create(MAX_FEATURES)

(kps_template, desc_template) = orb.detectAndCompute(template_gray, None)
(kps_scan, desc_scan)         = orb.detectAndCompute(scan_gray, None)

# Match the features
#method  = cv.DESCRIPTOR_MATCHER_BRUTEFORCE_HAMMING
#matcher = cv.DescriptorMatcher_create(method)
#matches = matcher.match(desc_scan, desc_template)
bf = cv.BFMatcher(cv.NORM_HAMMING)
matches = bf.match(desc_scan, desc_template)

# Sort the matches by their distances
matches = sorted(matches, key = lambda x : x.distance)

# Keep only the top matches
keep = int(len(matches) * KEEP_PERCENT)
matches = matches[:keep]


if SHOW_DEBUG:
    matched_visualization = cv.drawMatches(scan, kps_scan, template, kps_template, matches, None)
    plt.imshow(matched_visualization)

根据@it_guy 提供的说明,我尝试仅使用扫描图像找到所有交叉的心形。我将不得不在更多图像上尝试该算法以检查这种方法是否会泛化。

  1. 将扫描图像二值化。

    gray_image = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2GRAY)
    ret, thresh = cv2.threshold(gray_image, 180, 255, cv2.THRESH_BINARY_INV)
    

  1. 执行 dilation 以闭合心形轮廓和代表十字的曲线中的小间隙。注意 - 结构元素 np.ones((1,2), np.uint8 可以通过 运行 算法通过多个图像并找到最合适的结构元素来更改。
closing_original = cv2.morphologyEx(original_binary, cv2.MORPH_DILATE, np.ones((1,2), np.uint8)). 

  1. 找到图像中的所有轮廓。轮廓包括所有心形和底部的三角形。我们通过对轮廓的高度和宽度施加限制来过滤它们,从而消除其他轮廓,如点。此外,我们还使用 contour hierachies 来消除十字心的内部轮廓。
contours_original, hierarchy_original = cv2.findContours(closing_original, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)

  1. 我们遍历每个过滤后的轮廓。

正常心脏轮廓 -

带十字心的轮廓 -

让我们观察一下这两种心的区别。如果我们查看正常心脏内从 white-to-black 像素和 black-to-white 像素(从上到下)的过渡,我们会发现对于大多数图像列,此类过渡的数量为 4。(顶部边框 - 2 个过渡,底部边框 - 2 个过渡)

white-to-black 像素 - (255, 255, 0, 0, 0)

black-to-white 像素 - (0, 0, 255, 255, 255)

但是,在交叉心的情况下,大多数列的转换数必须为 6,因为交叉曲线/线在心脏内添加了两个转换(black-to-white 首先,然后white-to-black)。因此,在所有具有大于或等于 4 个此类转换的图像列中,如果超过 40% 的列具有 6 个转换,则给定轮廓表示交叉轮廓。结果-

代码-

import cv2
import numpy as np

def convert_to_binary(rgb_image):
    gray_image = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2GRAY)
    ret, thresh = cv2.threshold(gray_image, 180, 255, cv2.THRESH_BINARY_INV)
    return gray_image, thresh

original = cv2.imread('original.jpg')
height, width = original.shape[:2]
original_gray, original_binary = convert_to_binary(original) # Get binary image
cv2.imwrite("binary.jpg", original_binary)
closing_original = cv2.morphologyEx(original_binary, cv2.MORPH_DILATE, np.ones((1,2), np.uint8)) # Close small gaps in the binary image
cv2.imwrite("closed.jpg", closing_original)
contours_original, hierarchy_original = cv2.findContours(closing_original, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) # Get all the contours
bounding_rects_original = [cv2.boundingRect(c) for c in contours_original] # Get all contour bounding boxes
orig_boxes = list()

all_contour_image = original.copy()
for i, (x, y, w, h) in enumerate(bounding_rects_original):
    if h > height / 2 or w > width / 2:  # Eliminate extremely large contours
        continue
    if h < w / 2 or w < h / 2: # Eliminate vertical / horuzontal lines
        continue
    if w * h < 200: # Eliminate small area contours
        continue
    if hierarchy_original[0][i][3] != -1: # Eliminate contours created by heart crosses
        continue
    orig_boxes.append((x, y, w, h))
    cv2.rectangle(all_contour_image, (x,y), (x + w, y + h), (0, 255, 0), 3)
# cv2.imshow("warped", closing_original)
cv2.imwrite("all_contours.jpg", all_contour_image)

final_image = original.copy()
for x, y, w, h in orig_boxes:
    cropped_image = closing_original[y - 2 :y + h + 2, x: x + w] # Get the heart binary image

    col_pixel_diffs = np.abs(np.diff(cropped_image.T.astype(np.int16))/255) # Obtain all consecutive pixel differences in all the columns 

    column_sums = np.sum(col_pixel_diffs, axis=1) # Get the sum of each column's transitions. This results in an array of size equal 
    # to the number of columns, each element representing the number of black-white and white-black transitions. 

    percent_crosses = np.sum(column_sums >= 6)/ np.sum(column_sums >= 4) # Percentage of columns with 6 transitions among columns with 4 transitions
    if percent_crosses > 0.4: # Crossed heart criterion
        cv2.rectangle(final_image, (x,y), (x + w, y + h), (0, 255, 0), 3)
        cv2.imwrite("crossed_heart.jpg", cropped_image)
    else:
        cv2.imwrite("normal_heart.jpg", cropped_image)
cv2.imwrite("all_crossed_hearts.jpg", final_image)

可以在更多图像上测试此方法以确定其准确性。