【问题标题】:How to find the distance between two concentric contours, for different angles?如何找到不同角度的两个同心轮廓之间的距离?
【发布时间】:2021-04-06 05:45:50
【问题描述】:

我有一个带有两个轮廓的图像,其中一个轮廓总是在另一个轮廓“内部”。我想找到 90 个不同角度的两个轮廓之间的距离(意思是每 4 度的距离)。我该怎么做?

这是一个示例图像:

谢谢!

【问题讨论】:

  • 看看身材匀称的包装和这个答案:stackoverflow.com/questions/36314240/…
  • 1.) 你取内轮廓并计算质心。 2.) 对于每个角度,您创建一条线。 3.) 对于每条线,您计算两个轮廓的交点,如另一个示例所示。我错过了什么吗?
  • 您可以使用OpenCV findContours函数获取您的轮廓坐标:docs.opencv.org/2.4/modules/imgproc/doc/…
  • CV_CHAIN_APPROX_NONE 不会返回所有坐标。 Opencv 中绝对没有返回所有坐标的本机函数。 docs.opencv.org/3.1.0/d4/d73/…
  • 如果你使用 cv2.approxPolyDP,你会得到所有可以用于形状的多边形。下面的答案中给出的示例。

标签: python opencv image-processing


【解决方案1】:

在下面的代码中,我只是给你垂直线的例子,其余的可以通过旋转线来获得。结果是这样的,不用画图,可以用坐标来计算距离。

import shapely.geometry as shapgeo
import numpy as np
import cv2


img = cv2.imread('image.jpg', 0)
ret, img =cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)

#Fit the ellipses
_, contours0, hierarchy = cv2.findContours( img.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
outer_ellipse = [cv2.approxPolyDP(contours0[0], 0.1, True)]
inner_ellipse = [cv2.approxPolyDP(contours0[2], 0.1, True)]

h, w = img.shape[:2]
vis = np.zeros((h, w, 3), np.uint8)
cv2.drawContours( vis, outer_ellipse, -1, (255,0,0), 1)
cv2.drawContours( vis, inner_ellipse, -1, (0,0,255), 1)

##Extract contour of ellipses
cnt_outer = np.vstack(outer_ellipse).squeeze()
cnt_inner = np.vstack(inner_ellipse).squeeze()

#Determine centroid
M = cv2.moments(cnt_inner)
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
print cx, cy

#Draw full segment lines 
cv2.line(vis,(cx,0),(cx,w),(150,0,0),1)

# Calculate intersections using Shapely
# http://toblerity.org/shapely/manual.html
PolygonEllipse_outer= shapgeo.asLineString(cnt_outer)
PolygonEllipse_inner= shapgeo.asLineString(cnt_inner)
PolygonVerticalLine=shapgeo.LineString([(cx,0),(cx,w)])


insecouter= np.array(PolygonEllipse_outer.intersection(PolygonVerticalLine)).astype(np.int)
insecinner= np.array(PolygonEllipse_inner.intersection(PolygonVerticalLine)).astype(np.int)
cv2.line(vis,(insecouter[0,0], insecinner[1,1]),(insecouter[1,0], insecouter[1,1]),(0,255,0),2)
cv2.line(vis,(insecouter[0,0], insecinner[0,1]),(insecouter[1,0], insecouter[0,1]),(0,255,0),2)

cv2.imshow('contours', vis)

0xFF & cv2.waitKey()
cv2.destroyAllWindows()  

【讨论】:

  • 如果我们可以扩展不同的轮廓,例如三角形、矩形,这将是一种很好的,它们偶尔可能会交织/重叠,我认为你上面的回答也可以像这样的图片,只是想扩展每 4 度一次,等等ibb.co/6XBJn7q
  • 也是性能方面的明智之举,想知道是否有更快的方法来测量距离而无需物理绘制线交点等(如果可能)
  • @mattsmith5 我现在的时间有点短,似乎其他一些人已经提供了适当的答案(这是 SO 的工作方式),所以我会继续一。但是感谢您的赞美!
【解决方案2】:

拍摄这两组两个形状的图像:

我们想要找到每组形状的边缘之间的距离,包括边缘重叠的位置。

  1. 首先,我们导入必要的模块:
import cv2
import numpy as np
  1. 为此,我们首先需要检索图像中的每个形状作为轮廓列表。在上面的特定示例中,需要检测 4 个形状。要检索每个形状,我们需要使用遮罩来遮盖除感兴趣形状颜色之外的所有颜色:
def get_masked(img, lower, upper):
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(img_hsv, np.array(lower), np.array(upper))
    img_mask = cv2.bitwise_and(img, img, mask=mask)
    return img_mask

lowerupper 参数将确定不会从图像中屏蔽掉的最小 HVS 值和最大 HSV 值。给定正确的lowerupper 参数,您将能够提取一张只有绿色形状的图像和一张只有蓝色形状的图像:

  1. 使用蒙版图像,您可以继续将它们处理成更清晰的轮廓。这是preprocess 函数,其值可以在必要时进行调整:
def get_processed(img):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    img_blur = cv2.GaussianBlur(img_gray, (7, 7), 7)
    img_canny = cv2.Canny(img_blur, 50, 50)
    kernel = np.ones((7, 7))
    img_dilate = cv2.dilate(img_canny, kernel, iterations=2)
    img_erode = cv2.erode(img_dilate, kernel, iterations=2)
    return img_erode

传入蒙版图像会给你

  1. 图像经过遮罩和处理后,即可供 opencv 检测其轮廓:
def get_contours(img):
    contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    return [cnt for cnt in contours if cv2.contourArea(cnt) > 500]

return 语句中的列表推导通过指定每个轮廓的面积必须大于 500 来过滤噪声。

  1. 现在,我们将定义一些稍后将使用的基本函数:
def get_centeroid(cnt):
    length = len(cnt)
    sum_x = np.sum(cnt[..., 0])
    sum_y = np.sum(cnt[..., 1])
    return int(sum_x / length), int(sum_y / length)

def get_pt_at_angle(pts, pt, ang):
    angles = np.rad2deg(np.arctan2(*(pt - pts).T))
    angles = np.where(angles < -90, angles + 450, angles + 90)
    found= np.rint(angles) == ang
    if np.any(found):
        return pts[found][0]

函数的名称是不言自明的;第一个返回轮廓的中心点,第二个返回给定点数组pts 中的一个点,即相对于给定点pt 的给定角度angget_pt_at_angle 函数中的 np.where 用于将起始角度 0 移动到正 x 轴,因为它默认位于正 y 轴。

  1. 是时候定义返回距离的函数了。首先定义一下,让这五个参数可以传入:
def get_distances(img, cnt1, cnt2, center, step):

每个参数的简要说明:

  • img,图片数组
  • cnt1,第一个造型
  • cnt2,第二个造型
  • center,距离计算的起点
  • step,每个值要跳转的度数
  1. 定义一个字典来存储距离,以角度为键,距离为值:
    angles = dict()
  1. 循环遍历你想检索两个形状边缘的距离的每个角度,并找到两个轮廓的坐标,即迭代的 ct 角度,angle,相对于原点,@987654358 @,使用我们之前定义的get_pt_at_angle 函数。
    for angle in range(0, 360, step):
        pt1 = get_pt_at_angle(cnt1, center, angle)
        pt2 = get_pt_at_angle(cnt2, center, angle)
  1. 检查两个轮廓中是否存在与原点成特定角度的点:
        if np.any(pt1) and np.any(pt2):
  1. 您可以使用np.linalg.norm 方法获取两点之间的距离。我还让它绘制了文本和连接线以进行可视化。不要忘记将角度和值添加到angles 字典中,然后您可以跳出内部for 循环。在函数的最后,返回上面绘制了文本和线条的图像:
            d = round(np.linalg.norm(pt1 - pt2))
            cv2.putText(img, str(d), tuple(pt1), cv2.FONT_HERSHEY_PLAIN, 0.8, (0, 0, 0))
            cv2.drawContours(img, np.array([[center, pt1]]), -1, (255, 0, 255), 1)
            angles[angle] = d

    return img, angles
  1. 最后,您可以使用在图像上定义的函数:
img = cv2.imread("shapes1.png")

img_green = get_masked(img, [10, 0, 0], [70, 255, 255])
img_blue = get_masked(img, [70, 0, 0], [179, 255, 255])

img_green_processed = get_processed(img_green)
img_blue_processed = get_processed(img_blue)

img_green_contours = get_contours(img_green_processed)
img_blue_contours = get_contours(img_blue_processed)

使用四个形状的图像,您可以看出img_green_contoursimg_blue_contours 将分别包含两个轮廓。但是您可能想知道:我是如何选择最小和最大 HSV 值的?好吧,我使用了跟踪条码。您可以运行以下代码,使用轨迹栏调整 HSV 值,直到找到图像中的所有内容都被遮盖(黑色)的范围,除了您要检索的形状:

import cv2
import numpy as np

def empty(a):
    pass
    
cv2.namedWindow("TrackBars")
cv2.createTrackbar("Hue Min", "TrackBars", 0, 179, empty)
cv2.createTrackbar("Hue Max", "TrackBars", 179, 179, empty)
cv2.createTrackbar("Sat Min", "TrackBars", 0, 255, empty)
cv2.createTrackbar("Sat Max", "TrackBars", 255, 255, empty)
cv2.createTrackbar("Val Min", "TrackBars", 0, 255, empty)
cv2.createTrackbar("Val Max", "TrackBars", 255, 255, empty)

img = cv2.imread("shapes0.png")

while True:
    h_min = cv2.getTrackbarPos("Hue Min", "TrackBars")
    h_max = cv2.getTrackbarPos("Hue Max", "TrackBars")
    s_min = cv2.getTrackbarPos("Sat Min", "TrackBars")
    s_max = cv2.getTrackbarPos("Sat Max", "TrackBars")
    v_min = cv2.getTrackbarPos("Val Min", "TrackBars")
    v_max = cv2.getTrackbarPos("Val Max", "TrackBars")
    
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    lower = np.array([h_min, s_min, v_min])
    upper = np.array([h_max, s_max, v_max])
    
    mask = cv2.inRange(img_hsv, lower, upper)
    img_masked = cv2.bitwise_and(img, img, mask=mask)

    cv2.imshow("Image", img_masked)
    if cv2.waitKey(1) & 0xFF == ord("q"): # If you press the q key
        break

根据我选择的值,我得到:

  1. 平行循环通过蓝色形状轮廓和绿色形状轮廓,根据您希望原点位于哪个颜色形状的中心,您可以将该颜色轮廓传递给我们之前定义的get_centeroid函数:
for cnt_blue, cnt_green in zip(img_blue_contours, img_green_contours[::-1]):
    center = get_centeroid(cnt_blue)
    img, angles = get_distances(img, cnt_green.squeeze(), cnt_blue.squeeze(), center, 30)
    print(angles)

请注意,我使用30 作为步骤;这个数字可以改成4,我用30这样可视化会更清晰。

  1. 最后,我们可以显示图像了:
cv2.imshow("Image", img)
cv2.waitKey(0)

总共:

import cv2
import numpy as np

def get_masked(img, lower, upper):
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(img_hsv, np.array(lower), np.array(upper))
    img_mask = cv2.bitwise_and(img, img, mask=mask)
    return img_mask

def get_processed(img):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    img_blur = cv2.GaussianBlur(img_gray, (7, 7), 7)
    img_canny = cv2.Canny(img_blur, 50, 50)
    kernel = np.ones((7, 7))
    img_dilate = cv2.dilate(img_canny, kernel, iterations=2)
    img_erode = cv2.erode(img_dilate, kernel, iterations=2)
    return img_erode

def get_contours(img):
    contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    return [cnt for cnt in contours if cv2.contourArea(cnt) > 500]

def get_centeroid(cnt):
    length = len(cnt)
    sum_x = np.sum(cnt[..., 0])
    sum_y = np.sum(cnt[..., 1])
    return int(sum_x / length), int(sum_y / length)

def get_pt_at_angle(pts, pt, ang):
    angles = np.rad2deg(np.arctan2(*(pt - pts).T))
    angles = np.where(angles < -90, angles + 450, angles + 90)
    found= np.rint(angles) == ang
    if np.any(found):
        return pts[found][0]
        
def get_distances(img, cnt1, cnt2, center, step):
    angles = dict()
    for angle in range(0, 360, step):
        pt1 = get_pt_at_angle(cnt1, center, angle)
        pt2 = get_pt_at_angle(cnt2, center, angle)
        if np.any(pt1) and np.any(pt2):
            d = round(np.linalg.norm(pt1 - pt2))
            cv2.putText(img, str(d), tuple(pt1), cv2.FONT_HERSHEY_PLAIN, 0.8, (0, 0, 0))
            cv2.drawContours(img, np.array([[center, pt1]]), -1, (255, 0, 255), 1)
            angles[angle] = d
            
    return img, angles

img = cv2.imread("shapes1.png")

img_green = get_masked(img, [10, 0, 0], [70, 255, 255])
img_blue = get_masked(img, [70, 0, 0], [179, 255, 255])

img_green_processed = get_processed(img_green)
img_blue_processed = get_processed(img_blue)

img_green_contours = get_contours(img_green_processed)
img_blue_contours = get_contours(img_blue_processed)

for cnt_blue, cnt_green in zip(img_blue_contours, img_green_contours[::-1]):
    center = get_centeroid(cnt_blue)
    img, angles = get_distances(img, cnt_green.squeeze(), cnt_blue.squeeze(), center, 30)
    print(angles)

cv2.imshow("Image", img)
cv2.waitKey(0)

输出:

{0: 5, 30: 4, 60: 29, 90: 25, 120: 31, 150: 8, 180: 5, 210: 7, 240: 14, 270: 12, 300: 14, 330: 21}
{0: 10, 30: 9, 60: 6, 90: 0, 120: 11, 150: 7, 180: 5, 210: 6, 240: 6, 270: 4, 300: 0, 330: 16}

注意:对于某些形状,字典中可能不存在某些角度。这将是由process 函数引起的;如果你调低一些值,你会得到更准确的结果,比如模糊 sigma

【讨论】:

    【解决方案3】:

    我使用Shapelytfv's answer 的基本代码借用了总体思路。然而,迭代所需的角度、计算与形状相交的正确线所需的端点、计算和存储距离等都丢失了,所以我添加了所有这些。

    这就是我的完整代码:

    import cv2
    import numpy as np
    import shapely.geometry as shapgeo
    
    # Read image, and binarize
    img = cv2.imread('G48xu.jpg', cv2.IMREAD_GRAYSCALE)
    img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)[1]
    
    # Find (approximated) contours of inner and outer shape
    cnts, hier = cv2.findContours(img.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
    outer = [cv2.approxPolyDP(cnts[0], 0.1, True)]
    inner = [cv2.approxPolyDP(cnts[2], 0.1, True)]
    
    # Just for visualization purposes: Draw contours of inner and outer shape
    h, w = img.shape[:2]
    vis = np.zeros((h, w, 3), np.uint8)
    cv2.drawContours(vis, outer, -1, (255, 0, 0), 1)
    cv2.drawContours(vis, inner, -1, (0, 0, 255), 1)
    
    # Squeeze contours for further processing
    outer = np.vstack(outer).squeeze()
    inner = np.vstack(inner).squeeze()
    
    # Calculate centroid of inner contour
    M = cv2.moments(inner)
    cx = int(M['m10'] / M['m00'])
    cy = int(M['m01'] / M['m00'])
    
    # Calculate maximum needed radius for later line intersections
    r_max = np.min([cx, w - cx, cy, h - cy])
    
    # Set up angles (in degrees)
    angles = np.arange(0, 360, 4)
    
    # Initialize distances
    dists = np.zeros_like(angles)
    
    # Prepare calculating the intersections using Shapely
    poly_outer = shapgeo.asLineString(outer)
    poly_inner = shapgeo.asLineString(inner)
    
    # Iterate angles and calculate distances between inner and outer shape
    for i, angle in enumerate(angles):
    
        # Convert angle from degrees to radians
        angle = angle / 180 * np.pi
    
        # Calculate end points of line from centroid in angle's direction
        x = np.cos(angle) * r_max + cx
        y = np.sin(angle) * r_max + cy
        points = [(cx, cy), (x, y)]
    
        # Calculate intersections using Shapely
        poly_line = shapgeo.LineString(points)
        insec_outer = np.array(poly_outer.intersection(poly_line))
        insec_inner = np.array(poly_inner.intersection(poly_line))
    
        # Calculate distance between intersections using L2 norm
        dists[i] = np.linalg.norm(insec_outer - insec_inner)
    
        # Just for visualization purposes: Draw lines for some examples
        if (i == 10) or (i == 40) or (i == 75):
    
            # Line from centroid to end points
            cv2.line(vis, (cx, cy), (int(x), int(y)), (128, 128, 128), 1)
    
            # Line between both shapes
            cv2.line(vis,
                     (int(insec_inner[0]), int(insec_inner[1])),
                     (int(insec_outer[0]), int(insec_outer[1])), (0, 255, 0), 2)
    
            # Distance
            cv2.putText(vis, str(dists[i]), (int(x), int(y)),
                        cv2.FONT_HERSHEY_COMPLEX, 0.75, (0, 255, 0), 2)
    
    # Output angles and distances
    print(np.vstack([angles, dists]).T)
    
    # Just for visualization purposes: Output image
    cv2.imshow('Output', vis)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    我为可视化目的生成了一些示例输出:

    还有,这是输出的摘录,显示了角度和相应的距离:

    [[  0  70]
     [  4  71]
     [  8  73]
     [ 12  76]
     [ 16  77]
     ...
     [340  56]
     [344  59]
     [348  62]
     [352  65]
     [356  67]]
    

    希望代码是不言自明的。如果没有,请不要犹豫,提出问题。我很乐意提供更多信息。

    ----------------------------------------
    System information
    ----------------------------------------
    Platform:      Windows-10-10.0.16299-SP0
    Python:        3.9.1
    NumPy:         1.20.2
    OpenCV:        4.5.1
    Shapely:       1.7.1
    ----------------------------------------
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-03-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-09
      • 2021-05-14
      • 1970-01-01
      相关资源
      最近更新 更多