如何检测圆形腐蚀/膨胀

Posted

技术标签:

【中文标题】如何检测圆形腐蚀/膨胀【英文标题】:How to detect circular erosion/dilation 【发布时间】:2019-05-04 07:57:35 【问题描述】:

我想检测一条线上的圆形腐蚀和膨胀。对于膨胀,我尝试递归腐蚀图像,并且在每次递归时,我检查宽度/高度纵横比。如果比率小于 4,我假设它的轮廓是圆形的,对于每个这样的轮廓,我从力矩和面积计算圆心和半径。这是检测循环膨胀的函数:

def detect_circular_dilations(img, contours):
    contours_current, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    if len(contours_current) == 0:
        return get_circles_from_contours(contours)
    for c in contours_current:
        x, y, w, h = cv2.boundingRect(c)
        if w > h:
            aspect_ratio = float(w) / h
        else:
            aspect_ratio = float(h) / w
        if aspect_ratio < 4 and w < 20 and h < 20 and w > 5 and h > 5:
            contours.append(c)
    return detect_circular_dilations(cv2.erode(img, None, iterations=1), contours)

我想检测的循环膨胀示例如下:

我还没有解决的另一个问题是圆腐蚀的检测。循环腐蚀的一个例子如下:

在这里,我用红色矩形标记了我想检测的圆形侵蚀。可能有一些较小的圆形图案(左侧)不应被视为实际的圆形侵蚀。

有谁知道检测这种圆形的最佳方法是什么?对于循环膨胀,我将不胜感激任何评论/建议,以便有可能使检测更加稳健。

谢谢!

【问题讨论】:

不是一个答案,只是一个想法。您可能想查看potrace 及其长曲线优化的一些想法。见potrace.sourceforge.net和这里算法的技术描述potrace.sourceforge.net/potrace.pdf A又不是答案... :-) 在圆形扩张的情况下,您可以通过细化到骨架(1 像素厚)来估计白色物体(骨头?)的厚度然后将原始图像的平均强度除以骨架图像的平均强度,以了解骨骼的宽度/厚度。然后,您可以确定厚度变化(增加)的位置。或者,您可以导出顶边和底边的切线,通常它们会平行,但会在扩张开始时发散,然后在超出它之后再次平行。 【参考方案1】:

我会尝试用cv2.Canny() 找到线的两条边并搜索轮廓。如果您按边界框的宽度对轮廓进行排序,则前两个轮廓将是您的线条边缘。之后,您可以计算一个边缘中每个点到另一边缘的最小距离。然后您可以计算距离的中位数,并说如果一个点的距离大于或小于中位数(+- 容差),那么该点就是线的膨胀或腐蚀,并将其附加到列表中。如果需要,您可以通过遍历列表来整理噪音,如果它们不连续(在 x 轴上),则删除它们。

这是一个简单的例子:

import cv2
import numpy as np
from scipy import spatial

def detect_dilation(median, mindist, tolerance):
    count = 0
    for i in mindist:
        if i > median + tolerance:
            dilate.append((reshape_e1[count][0], reshape_e1[count][1]))
        elif i < median - tolerance:
            erode.append((reshape_e1[count][0], reshape_e1[count][1]))
        else:
            pass
        count+=1

def other_axis(dilate, cnt):
    temp = []
    for i in dilate:
        temp.append(i[0])
    for i in cnt:
        if i[0] in temp:
            dilate.append((i[0],i[1]))

img = cv2.imread('1.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray,100,200)
_, contours, hierarchy = cv2.findContours(edges,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
contours.sort(key= lambda cnt :cv2.boundingRect(cnt)[3])
edge_1 = contours[0]
edge_2 = contours[1]
reshape_e1 = np.reshape(edge_1, (-1,2))
reshape_e2 =np.reshape(edge_2, (-1,2))
tree = spatial.cKDTree(reshape_e2)
mindist, minid = tree.query(reshape_e1)
median = np.median(mindist)
dilate = []
erode = []
detect_dilation(median,mindist,5)
other_axis(dilate, reshape_e2)
other_axis(erode, reshape_e2)

dilate = np.array(dilate).reshape((-1,1,2)).astype(np.int32)
erode = np.array(erode).reshape((-1,1,2)).astype(np.int32)
x,y,w,h = cv2.boundingRect(dilate)
cv2.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2)
x,y,w,h = cv2.boundingRect(erode)
cv2.rectangle(img,(x,y),(x+w,y+h),(0,0,255),2)

cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

结果:

编辑:

如果图片有一条断线(这意味着更多的轮廓),您必须将每个轮廓视为单独的线。您可以通过在cv2.boundingRect() 的帮助下创建感兴趣的区域来实现此目的。但是当我尝试使用新上传的图片时,该过程不是很稳健,因为您必须更改容差才能获得所需的结果。由于我不知道其他图像是什么样的,您可能需要一种更好的方法来获取平均距离和容差因子。这里的任何方式都是我所描述的示例(15 表示容差):

import cv2
import numpy as np
from scipy import spatial

def detect_dilation(median, mindist, tolerance):
    count = 0
    for i in mindist:
        if i > median + tolerance:
            dilate.append((reshape_e1[count][0], reshape_e1[count][1]))
        elif i < median - tolerance:
            erode.append((reshape_e1[count][0], reshape_e1[count][1]))
        else:
            pass
        count+=1

def other_axis(dilate, cnt):
    temp = []
    for i in dilate:
        temp.append(i[0])
    for i in cnt:
        if i[0] in temp:
            dilate.append((i[0],i[1]))

img = cv2.imread('2.jpg')
gray_original = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh_original = cv2.threshold(gray_original, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)

# Filling holes
_, contours, hierarchy = cv2.findContours(thresh_original,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
    cv2.drawContours(thresh_original,[cnt],0,255,-1)
_, contours, hierarchy = cv2.findContours(thresh_original,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)

for cnt in contours:
    x2,y,w2,h = cv2.boundingRect(cnt)
    thresh = thresh_original[0:img.shape[:2][1], x2+20:x2+w2-20] # Region of interest for every "line"
    edges = cv2.Canny(thresh,100,200)
    _, contours, hierarchy = cv2.findContours(edges,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
    contours.sort(key= lambda cnt: cv2.boundingRect(cnt)[3])
    edge_1 = contours[0]
    edge_2 = contours[1]
    reshape_e1 = np.reshape(edge_1, (-1,2))
    reshape_e2 =np.reshape(edge_2, (-1,2))
    tree = spatial.cKDTree(reshape_e2)
    mindist, minid = tree.query(reshape_e1)
    median = np.median(mindist)
    dilate = []
    erode = []
    detect_dilation(median,mindist,15)
    other_axis(dilate, reshape_e2)
    other_axis(erode, reshape_e2)
    dilate = np.array(dilate).reshape((-1,1,2)).astype(np.int32)
    erode = np.array(erode).reshape((-1,1,2)).astype(np.int32)
    x,y,w,h = cv2.boundingRect(dilate)
    if len(dilate) > 0:
        cv2.rectangle(img[0:img.shape[:2][1], x2+20:x2+w2-20],(x,y),(x+w,y+h),(255,0,0),2)
    x,y,w,h = cv2.boundingRect(erode)
    if len(erode) > 0:
        cv2.rectangle(img[0:img.shape[:2][1], x2+20:x2+w2-20],(x,y),(x+w,y+h),(0,0,255),2)

cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

结果:

【讨论】:

这是一个合理的建议。但我不明白你为什么要在这里使用 Canny。这是一种在灰度值图像中寻找有意义边缘的算法。这是一个二值图像,边缘很容易找到:任何具有背景邻居的对象像素都是边缘像素。您可以通过获取图像与其 1 像素侵蚀之间的差异来获得这些。 你是对的。我使用 Canny 是因为它似乎是获取单行坐标的最简单方法……但是是的,您可以通过其他任何方式获取这些坐标。该解决方案的本质是两个物体之间的最小距离差异,可用于检测侵蚀和扩张线。 谢谢@kavko。我试图在一个简单的图像上运行它并且它有效。不幸的是,如果检测到两个以上的轮廓,就像新添加的膨胀示例图片一样,我还没有设法对边缘进行分组。 @NikoGamulin:请参阅编辑【参考方案2】:

此类问题通常使用距离变换和中轴变换来解决。这些在某种程度上是相关的,因为中轴沿着距离变换的脊延伸。总体思路是:

    计算图像的距离变换(对于每个前景像素,返回到最近的背景像素的距离;有些库以另一种方式实现,在这种情况下,您需要计算倒置图像的距离变换)。

    计算中轴(或骨架)。

    沿中轴的距离变换值是相关值,我们忽略所有其他像素。在这里,我们看到了线的局部半径。

    局部最大值是膨胀的质心。使用阈值来确定哪些是重要的膨胀,哪些不是(嘈杂的轮廓会导致许多局部最大值)。

    局部最小值是侵蚀的质心。

例如,我使用下面的 MATLAB 代码得到了以下输出。

这是我使用的代码。它使用带有DIPimage 3 的MATLAB,作为原理的快速证明。使用您喜欢使用的任何图像处理库,这应该可以直接转换为 Python。

% Read in image and remove the red markup:
img = readim('https://i.stack.imgur.com/bNOTn.jpg');
img = img3>100;
img = closing(img,5);

% This is the algorithm described above:
img = fillholes(img);               % Get rid of holes
radius = dt(img);                   % Distance transform
m = bskeleton(img);                 % Medial axis
radius(~m) = 0;                     % Ignore all pixels outside the medial axis
detection = dilation(radius,25)==radius & radius>25; % Local maxima with radius > 25
pos = findcoord(detection);         % Coordinates of detections
radius = double(radius(detection)); % Radii of detections

% This is just to make the markup:
detection = newim(img,'bin');
for ii=1:numel(radius)
   detection = drawshape(detection,2*radius(ii),pos(ii,:),'disk');
end
overlay(img,detection)

【讨论】:

我猜这只能检测膨胀而不是腐蚀。 @Niko:你需要找到沿线的局部最小值才能找到侵蚀。

以上是关于如何检测圆形腐蚀/膨胀的主要内容,如果未能解决你的问题,请参考以下文章

形态学操作-腐蚀与膨胀

二值形态学——开闭运算

android - 自定义圆形 imageView 得到错误膨胀

识别圆形手势以放置注释,如何检测圆形?

OpenCV学习笔记 008基于形态学运算的图像变换

OpenCV—python 形态学处理(腐蚀膨胀开闭运算边缘检测)