如何找到两个同心轮廓之间的距离,对于不同的角度?

时间:2016-04-08 15:53:44

标签: python opencv image-processing

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

这是一个示例图片:

enter image description here

谢谢!

3 个答案:

答案 0 :(得分:3)

采用两组两个形状的图像:

enter image description here

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

  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 参数,您将能够提取一张只有绿色形状的图像和一张只有蓝色形状的图像:

enter image description here

  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

传入蒙版图像会给你

enter image description here

  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 中的一个点,即相对于给定点的给定角度 angptnp.where 函数中的 get_pt_at_angle 用于将起始角度 0 移动到正 x 轴,因为它默认位于正 y 轴。

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

对每个参数的简要说明:

  • img,图像数组
  • cnt1,第一个形状
  • cnt2,第二个形状
  • center,距离计算的原点
  • step,每个值要跳跃的度数
  1. 定义一个字典来存储距离,以角度为键,距离为值:
    angles = dict()
  1. 循环遍历每个要检索两个形状边缘距离的角度,并找到两个轮廓的坐标,即迭代的ct角angle,相对于原点, center,使用我们之前定义的 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

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

enter image description here

enter image description here

  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}

enter image description here

注意:对于某些形状,字典中可能缺少某些角度。这可能是由 process 函数引起的;如果您调低某些值,例如模糊 sigma

,您将获得更准确的结果

答案 1 :(得分:2)

在下面的代码中,我刚刚给出了垂直线的示例,其余的可以通过旋转线获得。结果如下所示,而不是绘图,您可以使用坐标进行距离计算。

enter image description here

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()  

答案 2 :(得分:2)

我借用了 Shapely 的总体思路和 tfv'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()

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

Output

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

[[  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
----------------------------------------