图像处理:将扫描图像映射到具有许多相同特征的模板图像上
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 提供的说明,我尝试仅使用扫描图像找到所有交叉的心形。我将不得不在更多图像上尝试该算法以检查这种方法是否会泛化。
将扫描图像二值化。
gray_image = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray_image, 180, 255, cv2.THRESH_BINARY_INV)
- 执行 dilation 以闭合心形轮廓和代表十字的曲线中的小间隙。注意 - 结构元素
np.ones((1,2), np.uint8
可以通过 运行 算法通过多个图像并找到最合适的结构元素来更改。
closing_original = cv2.morphologyEx(original_binary, cv2.MORPH_DILATE, np.ones((1,2), np.uint8)).
- 找到图像中的所有轮廓。轮廓包括所有心形和底部的三角形。我们通过对轮廓的高度和宽度施加限制来过滤它们,从而消除其他轮廓,如点。此外,我们还使用 contour hierachies 来消除十字心的内部轮廓。
contours_original, hierarchy_original = cv2.findContours(closing_original, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
- 我们遍历每个过滤后的轮廓。
正常心脏轮廓 -
带十字心的轮廓 -
让我们观察一下这两种心的区别。如果我们查看正常心脏内从 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)
可以在更多图像上测试此方法以确定其准确性。
问题描述
我们正在尝试将扫描图像与模板图像相匹配:
- 扫描图像示例:
- 模板图片示例:
模板图像包含一组不同大小和轮廓属性(闭合、左开和右开)的心形。模板中的每颗心都是一个我们知道其位置、大小和轮廓类型的感兴趣区域。我们的目标是将扫描图像匹配到模板上,以便我们可以在扫描图像中提取这些 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 提供的说明,我尝试仅使用扫描图像找到所有交叉的心形。我将不得不在更多图像上尝试该算法以检查这种方法是否会泛化。
将扫描图像二值化。
gray_image = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(gray_image, 180, 255, cv2.THRESH_BINARY_INV)
- 执行 dilation 以闭合心形轮廓和代表十字的曲线中的小间隙。注意 - 结构元素
np.ones((1,2), np.uint8
可以通过 运行 算法通过多个图像并找到最合适的结构元素来更改。
closing_original = cv2.morphologyEx(original_binary, cv2.MORPH_DILATE, np.ones((1,2), np.uint8)).
- 找到图像中的所有轮廓。轮廓包括所有心形和底部的三角形。我们通过对轮廓的高度和宽度施加限制来过滤它们,从而消除其他轮廓,如点。此外,我们还使用 contour hierachies 来消除十字心的内部轮廓。
contours_original, hierarchy_original = cv2.findContours(closing_original, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
- 我们遍历每个过滤后的轮廓。
正常心脏轮廓 -
带十字心的轮廓 -
让我们观察一下这两种心的区别。如果我们查看正常心脏内从 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)
可以在更多图像上测试此方法以确定其准确性。