【问题标题】:How to obtain the extreme edges from an identified contour?如何从识别的轮廓中获取极端边缘?
【发布时间】:2021-09-16 14:38:06
【问题描述】:

我需要知道如何在识别轮廓后捕获一条线的极端边缘(起点和终点坐标)。目前,我正在识别下图中形状(不同类型的线条)的轮廓,并将它们绘制回新图像。我已经尝试从轮廓数组中获取最顶部、最底部、最左侧和最右侧的坐标,但它们不会精确到具有如下曲线的线。那么有什么方法可以从轮廓数组中捕获这些起点和终点吗?

源代码

import cv2
import numpy as np

# Let's load a simple image with 3 black squares
image = cv2.imread("C:/Users/Hasindu/3D Objects/edge-test-188.jpg")
cv2.waitKey(0)

# Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Find Canny edges
edged = cv2.Canny(gray, 30, 200)
cv2.waitKey(0)

# Finding Contours
# Use a copy of the image e.g. edged.copy()
# since findContours alters the image
contours, hierarchy = cv2.findContours(edged,
    cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cv2.imshow('Canny Edges After Contouring', edged)
cv2.waitKey(0)

print("Number of Contours found = " + str(len(contours)))
print(contours)
topmost = tuple(contours[0][contours[0][:,:,1].argmin()][0]);
bottommost = tuple(contours[0][contours[0][:,:,1].argmax()][0]);

print(topmost);
print(bottommost);



# Draw all contours
# -1 signifies drawing all contours
cv2.drawContours(image, contours, -1, (0, 255, 0), 3)


cv2.imshow('Contours', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

输入 1

输出 1

编辑:

我遵循了 stateMachine 建议的解决方案,但我的所有输入都不是 100% 准确。您可以清楚地看到第二个输入图像上的一些端点未被解决方案检测到。

输入 2

输出 2

【问题讨论】:

    标签: python numpy opencv image-processing opencv-contour


    【解决方案1】:

    找到轮廓后,我们对轮廓的每个点应用一个特殊的过滤器。它应用以每个像素为中心的遮罩,然后在遮罩区域中找到轮廓(或连接的组件或斑点)。理想情况下,对于端点,该区域中只有一个斑点,而对于其他点,则不止一个。我们获取每个轮廓的候选端点,然后将它们聚类成两个簇,因为由于过滤器宽度和线条粗细,会有两个以上的候选。如果聚类输出两个点,就是处理后轮廓的端点。

    一个例子如下所示。

    mask:
    1 1 1 1 1
    1 0 0 0 1
    1 0 0 0 1
    1 0 0 0 1
    1 1 1 1 1
    
    image:
    0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0
    0 0 0 1 1 1 1 1 0 0 0
    0 0 0 0 1 1 1 1 0 0 0
    0 0 0 0 0 0 0 1 0 0 0
    0 0 0 0 0 0 0 1 0 0 0
    0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0
    
    response for end point:
    1 1 1 1 1     0 0 0 0 0        0 0 0 0 0
    1 0 0 0 1     0 0 0 0 0        0 0 0 0 0
    1 0 0 0 1  &  0 0 1 1 1    =   0 0 0 0 1 
    1 0 0 0 1     0 0 0 1 1        0 0 0 0 1
    1 1 1 1 1     0 0 0 0 0        0 0 0 0 0
    
    response for corner point:
    1 1 1 1 1     0 0 0 0 0        0 0 0 0 0
    1 0 0 0 1     0 0 0 0 0        0 0 0 0 0
    1 0 0 0 1  &  1 1 1 0 0    =   1 0 0 0 0 
    1 0 0 0 1     1 1 1 0 0        1 0 0 0 0
    1 1 1 1 1     0 0 1 0 0        0 0 1 0 0
    

    如果图像太嘈杂或您的输入是 jpeg 并且阈值处理不好,这将无法正常工作,因为它可能会在遮罩区域中引入一些杂散分量,从而将该点算作不适合一个终点。

    如果输入图像(或阈值图像)中的线条宽度超过 2 个像素,则可以更改过滤器半径(代码中的 r)。

    如果线间距小于两个像素,那么您将再次遇到当前过滤器或任何更大的过滤器的问题。但是在这种情况下,您可以在单独的图像中绘制每个轮廓,然后应用过滤器,但为了简单起见,我没有在代码中这样做。

    在这里,我们使用 CHAIN_APPROX_SIMPLE 来减少轮廓像素数和 Otsu 阈值。为简单起见,代码不处理轮廓点落在图像边界的情况。

    import cv2 as cv
    import numpy as np
        
    im = cv.imread('dclSa.jpg')
    gray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
    # apply Otsu threshold
    th, bw = cv.threshold(gray, 0, 1, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)
    # find contours
    contours, _ = cv.findContours(bw, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    
    # create filter
    r = 2
    mask = np.ones((2*r+1, 2*r+1), dtype=np.uint8)
    mask[1:2*r, 1:2*r] = 0
    #print mask
    
    criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 5, 1.0)
    
    for contour in contours:
        all_x = []
        all_y = []
        for point in contour:
            x = point[0][0]
            y = point[0][1]
            # extract the region centered around the contour pixel
            roi = bw[y-r:y+r+1, x-r:x+r+1]
            # find the blobs in masked region
            n, _ = cv.findContours(roi & mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    
            # if the blob count is 1, this pixel is an end point candidate
            # if you use cv.connectedComponents to find blobs, then check 2 == n as it counts background
            if 1 == len(n):
                all_x.append(x)
                all_y.append(y)
    
        # if there are no candidate points, check next contour
        if not all_x:
            continue
    
        # we are done with a contour. cluster the end point candidates into two clusters
        points = np.vstack((all_x, all_y))
        _, _, endpoints = cv.kmeans(np.float32(points.transpose()), 2, None, criteria, 5, cv.KMEANS_RANDOM_CENTERS)
        
        # if the clustering goes well, we'll have the two end points of the contour
        if 2 == len(endpoints) and 2 == len(endpoints[0]) and 2 == len(endpoints[1]):
            im = cv.circle(im, (int(endpoints[0][0]), int(endpoints[0][1])), 3, (255, 0, 0), -1)
            im = cv.circle(im, (int(endpoints[1][0]), int(endpoints[1][1])), 3, (0, 255, 0), -1)
    

    【讨论】:

    • 这是一个很好的答案。您使用cv.findContours() 来确定二进制图像中的连接组件,在我看来这有点小题大做。我建议改用cv.connectedComponents(),特别是retval, labels = cv.connectedComponents(bw, labels=np.zeros_like(bw), connectivity=8)
    • @BartvanOtterdijk 谢谢。如果有人想使用它,请在评论中提到 cv.connectedComponents() 的用法。
    • @dhanushka 不幸的是我尝试了你的解决方案,它给了我一个错误。
    • 46 # 如果聚类顺利,我们将得到轮廓 47 的两个端点 if 2 == len(endpoints) and 2 == len(endpoints[0]) and 2 = = len(endpoints[1]): ---> 48 im = cv.circle(im, (endpoints[0][0], endpoints[0][1]), 3, (255, 0, 0), -1) 49 im = cv.circle(im, (endpoints[1][0], endpoints[1][1]), 3, (0, 255, 0), -1) 错误:OpenCV(4.5.2 ) :-1: error: (-5:Bad argument) in function 'circle' > 重载解析失败:> - 无法解析 'center'。索引为 0 的序列项类型错误 > - 无法解析“中心”。索引为 0 的序列项类型错误
    • @HasinduDahanayake 在绘图函数中将端点坐标转换为 int。我更新了答案。
    【解决方案2】:

    一种可能的解决方案涉及在this 帖子中应用该方法。它涉及将输入图像与用于识别端点的特殊内核进行卷积。这些是步骤:

    1. 将图像转换为灰度
    2. 通过对灰度图像应用 Otsu 阈值 来获取二值图像
    3. 应用一点形态学,以确保我们有连续和闭合的曲线
    4. 计算图像的骨架
    5. 卷积骨架与端点内核
    6. 在原始图像上绘制端点

    让我们看看代码:

    # Imports:
    import cv2
    import numpy as np
    
    # Reading an image in default mode:
    inputImage = cv2.imread(path + fileName)
    # Prepare a deep copy of the input for results:
    inputImageCopy = inputImage.copy()
    
    # Grayscale conversion:
    grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
    
    # Threshold via Otsu:
    _, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    

    第一位非常简单。只需使用 Otsu 的阈值获得二值图像。结果如下:

    阈值可能会错过曲线内的一些像素,从而导致“间隙”。我们不希望这样,因为我们正在尝试识别端点,这些端点本质上是曲线上的间隙。让我们使用一点形态学来填补可能的空白 - closing 将有助于填补这些较小的空白:

    # Set morph operation iterations:
    opIterations = 2
    # Get the structuring element:
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    # Perform Closing:
    binaryImage = cv2.morphologyEx(binaryImage, cv2.MORPH_CLOSE, kernel, None, None, opIterations, cv2.BORDER_REFLECT101)
    

    这是现在的结果:

    好的,接下来是获取二进制图像的skeleton骨架 是二进制图像的一个版本,其中线条已被标准化为宽度为1 pixel。这很有用,因为我们可以将图像与3 x 3 内核进行卷积,并寻找特定的像素模式——那些识别端点的模式。让我们使用 OpenCV 的扩展图像处理模块来计算骨架:

    # Compute the skeleton:
    skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)
    

    没什么特别的,只需一行代码即可完成。结果是这样的:

    这张图片很微妙,但是曲线现在有1 px的宽度,所以我们可以应用卷积。这种方法的主要思想是卷积产生一个非常具体的值,其中在输入图像中发现了黑白像素的模式。我们要找的值是110,但是我们需要在实际卷积之前执行一些操作。详情请参阅original post。这些是操作:

    # Threshold the image so that white pixels get a value of 0 and
    # black pixels a value of 10:
    _, binaryImage = cv2.threshold(skeleton, 128, 10, cv2.THRESH_BINARY)
    
    # Set the end-points kernel:
    h = np.array([[1, 1, 1],
                  [1, 10, 1],
                  [1, 1, 1]])
    
    # Convolve the image with the kernel:
    imgFiltered = cv2.filter2D(binaryImage, -1, h)
    
    # Extract only the end-points pixels, those with
    # an intensity value of 110:
    endPointsMask = np.where(imgFiltered == 110, 255, 0)
    
    # The above operation converted the image to 32-bit float,
    # convert back to 8-bit uint
    endPointsMask = endPointsMask.astype(np.uint8)
    

    如果我们imshowendPointsMask,我们会得到这样的结果:

    在上图中,您可以看到已识别端点的位置。让我们获取这些白色像素的坐标:

    # Get the coordinates of the end-points:
    (Y, X) = np.where(endPointsMask == 255)
    

    最后,让我们在这些位置画圈:

    # Draw the end-points:
    for i in range(len(X)):
        # Get coordinates:
        x = X[i]
        y = Y[i]
        # Set circle color:
        color = (0, 0, 255)
        # Draw Circle
        cv2.circle(inputImageCopy, (x, y), 3, color, -1)
    
        cv2.imshow("Points", inputImageCopy)
        cv2.waitKey(0)
    

    这是最终结果:


    编辑:识别哪个 blob 产生每组点

    由于您还需要知道哪个斑点/轮廓/曲线产生了每组端点,因此您可以使用其他一些函数重新编写下面的代码来实现这一点。在这里,我将主要依靠我之前编写的用于检测图像中最大的斑点的函数。两条曲线中的一条总是比另一条更大(即面积更大)。如果你提取这条曲线,处理它,然后迭代地从原始图像中减去它,你可以逐条曲线处理,每次你可以知道哪条曲线(当前最大的一条)产生了当前的端点。让我们修改代码来实现这些想法:

    # Imports:
    import cv2
    import numpy as np
    
    # image path
    path = "D://opencvImages//"
    fileName = "w97nr.jpg"
    
    # Reading an image in default mode:
    inputImage = cv2.imread(path + fileName)
    
    # Deep copy for results:
    inputImageCopy = inputImage.copy()
    
    # Grayscale conversion:
    grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
    
    # Threshold via Otsu:
    _, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    
    # Set morph operation iterations:
    opIterations = 2
    
    # Get the structuring element:
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    
    # Perform Closing:
    binaryImage = cv2.morphologyEx(binaryImage, cv2.MORPH_CLOSE, kernel, None, None, opIterations, cv2.BORDER_REFLECT101)
    
    # Compute the skeleton:
    skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)
    

    直到骨架计算,一切都是一样的。现在,我们将提取当前最大的 blob 并对其进行处理以获得其端点,我们将继续提取当前最大的 blob,直到没有更多曲线可以提取。因此,我们只需修改先前的代码来管理这个想法的迭代性质。另外,让我们将端点存储在列表中。此列表的每一行将表示一条新曲线:

    # Processing flag:
    processBlobs = True
    
    # Shallow copy for processing loop:
    blobsImage = skeleton
    
    # Store points per blob here:
    blobPoints = []
    
    # Count the number of processed blobs:
    blobCounter = 0
    
    # Start processing blobs:
    while processBlobs:
    
        # Find biggest blob on image:
        biggestBlob = findBiggestBlob(blobsImage)
    
        # Prepare image for next iteration, remove
        # currrently processed blob:
        blobsImage = cv2.bitwise_xor(blobsImage, biggestBlob)
    
        # Count number of white pixels:
        whitePixelsCount = cv2.countNonZero(blobsImage)
        # If the image is completely black (no white pixels)
        # there are no more curves to process:
        if whitePixelsCount == 0:
            processBlobs = False
    
        # Threshold the image so that white pixels get a value of 0 and
        # black pixels a value of 10:
        _, binaryImage = cv2.threshold(biggestBlob, 128, 10, cv2.THRESH_BINARY)
    
        # Set the end-points kernel:
        h = np.array([[1, 1, 1],
                      [1, 10, 1],
                      [1, 1, 1]])
    
        # Convolve the image with the kernel:
        imgFiltered = cv2.filter2D(binaryImage, -1, h)
    
        # Extract only the end-points pixels, those with
        # an intensity value of 110:
        endPointsMask = np.where(imgFiltered == 110, 255, 0)
    
        # The above operation converted the image to 32-bit float,
        # convert back to 8-bit uint
        endPointsMask = endPointsMask.astype(np.uint8)
    
        # Get the coordinates of the end-points:
        (Y, X) = np.where(endPointsMask == 255)
    
        # Prepare random color:
        color = (np.random.randint(low=0, high=256), np.random.randint(low=0, high=256), np.random.randint(low=0, high=256))
    
        # Prepare id string:
        string = "Blob: "+str(blobCounter)
        font = cv2.FONT_HERSHEY_COMPLEX
        tx = 10
        ty = 10 + 10 * blobCounter
        cv2.putText(inputImageCopy, string, (tx, ty), font, 0.3, color, 1)
    
        # Store these points in list:
        blobPoints.append((X,Y, blobCounter))
        blobCounter = blobCounter + 1
    
        # Draw the end-points:
        for i in range(len(X)):
            x = X[i]
            y = Y[i]
    
            cv2.circle(inputImageCopy, (x, y), 3, color, -1)
    
            cv2.imshow("Points", inputImageCopy)
            cv2.waitKey(0)
    

    这个循环提取最大的 blob 并对其进行处理,就像在帖子的第一部分中一样 - 我们将图像与端点内核进行卷积并定位匹配点。对于原始输入,结果如下:

    如您所见,每组点都使用一种唯一颜色(随机生成)绘制。还有当前的blob“ID”(只是一个升序计数)以与每组点相同的颜色绘制在文本中,因此您知道哪个blob产生了每组端点。该信息存储在blobPoints 列表中,我们可以打印它的值,如下所示:

    # How many blobs where found:
    blobCount = len(blobPoints)
    print("Found: "+str(blobCount)+" blobs.")
    
    # Let's check out each blob and their end-points:
    for b in range(blobCount):
        # Fetch data:
        p1 = blobPoints[b][0]
        p2 = blobPoints[b][1]
        id = blobPoints[b][2]
        # Print data  for each blob:
        print("Blob: "+str(b)+" p1: "+str(p1)+" p2: "+str(p2)+" id: "+str(id))
    

    哪些打印:

    Found: 2 blobs.
    Blob: 0 p1: [39 66] p2: [ 42 104] id: 0
    Blob: 1 p1: [129 119] p2: [25 49] id: 1
    

    这是findBiggestBlob 函数的实现,它只使用其面积计算图像上最大的斑点。它返回一个孤立的最大 blob 的图像,这来自 C++ implementation 我写的相同想法:

    def findBiggestBlob(inputImage):
        # Store a copy of the input image:
        biggestBlob = inputImage.copy()
        # Set initial values for the
        # largest contour:
        largestArea = 0
        largestContourIndex = 0
    
        # Find the contours on the binary image:
        contours, hierarchy = cv2.findContours(inputImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
    
        # Get the largest contour in the contours list:
        for i, cc in enumerate(contours):
            # Find the area of the contour:
            area = cv2.contourArea(cc)
            # Store the index of the largest contour:
            if area > largestArea:
                largestArea = area
                largestContourIndex = i
    
        # Once we get the biggest blob, paint it black:
        tempMat = inputImage.copy()
        cv2.drawContours(tempMat, contours, largestContourIndex, (0, 0, 0), -1, 8, hierarchy)
        # Erase smaller blobs:
        biggestBlob = biggestBlob - tempMat
    
        return biggestBlob
    

    【讨论】:

    • 非常感谢您的精彩解释。我测试了您的解决方案,效果非常好。但是我需要通过知道它属于哪条线来捕获这些坐标。如果我进一步解释我的要求,我需要特别知道识别线的(开始,结束)坐标。但是我们在下面的语句之后得到的两个数组包含所有与起点和终点相关的坐标(x和y坐标),我无法一次提取与特定线相关的起点和终点坐标。声明:(Y, X) = np.where(endPointsMask == 255)
    • 如果您能告诉我如何从这个解决方案中一次获取与特定行相关的起点和终点,那就太好了。
    • 我遵循了您的代码。但它不准确。请参阅更新后的帖子。
    • 更改内核大小:kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) 或不对该图像进行形态处理:kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 1 ))
    • @AlexAlex 非常感谢您的帮助。更改内核大小对于检测先前解决方案中丢失的端点非常有效,但在某些情况下,唯一的问题是输出中将多个端点检测为一个blob 。我的意思是输出应该只包含与一条线相关的起点和终点坐标。但在某些情况下,它会检测不同线的终点和起点坐标以像这样收集和输出它,29 p1:[545 562 562] p2:[ 193 281 283] id: 29 。有时像这样是空的。Blob: 41 p1: [] p2: [] id: 41``` .
    猜你喜欢
    • 2013-08-07
    • 2017-07-21
    • 2013-04-02
    • 2015-09-26
    • 1970-01-01
    • 2013-06-10
    • 1970-01-01
    • 2012-07-12
    • 2020-09-22
    相关资源
    最近更新 更多