【问题标题】:How to detect different types of arrows in image?如何检测图像中不同类型的箭头?
【发布时间】:2021-06-17 11:16:04
【问题描述】:

是否有轮廓方法来检测 Python CV 中的箭头?可以使用轮廓、形状和顶点。

# find contours in the thresholded image and initialize the shape detector
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
perimeterValue = cv2.arcLength(cnts , True)
vertices = cv2.approxPolyDP(cnts , 0.04 * perimeterValue, True)

也许我们可以查看轮廓的尖端,也可以检测三角形?

希望它可以检测不同对象之间、正方形、矩形和圆形之间的箭头。 (否则,将不得不使用机器学习)。 如果可能的话也很高兴得到这三个结果(箭头长度,粗细,方向角度)

本题推荐模板匹配,不指定任何代码库。寻找可以通过代码创建的可行的东西

how to detect arrows using open cv python?

如果 PythonOpenCV 没有能力,请打开以使用另一个库。

【问题讨论】:

  • 您到底需要什么?您是否希望将每个箭头归入自己的类别?例如,给定一个箭头图像,分类器是否应该输出“属于第 1 类 - 实心箭头”?
  • 嗨@eldesgraciado 我刚刚更新了问题

标签: python python-3.x opencv image-processing computer-vision


【解决方案1】:

您要求的解决方案过于复杂,无法通过一个函数或特定算法来解决。事实上,这个问题可以分解成更小的步骤,每个步骤都有自己的算法和解决方案。我不会为您提供免费的、完整的、复制粘贴的解决方案,而是为您提供问题的大致轮廓并发布我设计的解决方案的一部分。这些是我建议的步骤:

  1. 识别提取图像中的所有箭头斑点,并一一处理。

  2. 尝试找到箭头的端点。那是终点和起点(或“尾”和“尖端”)

  3. 撤消旋转,这样您就可以始终拉直箭头,无论它们的角度如何。

  4. 此后,箭头将始终指向一个方向。这种规范化让我们自己轻松进行分类

处理后,您可以将图像传递给 Knn 分类器、支持向量机甚至(如果您愿意在这方面称其为“大炮”)问题)一个 CNN(在这种情况下,您可能不需要撤消旋转 - 只要您有足够的训练样本)。您甚至不必计算特征,因为将原始图像传递给 SVM 可能就足够了。但是,对于每个箭头类,您需要不止一个训练样本。

好吧,让我们看看。首先,让我们从输入中提取每个箭头。这是使用cv2.findCountours 完成的,这部分非常简单:

# Imports:
import cv2
import math
import numpy as np

# image path
path = "D://opencvImages//"
fileName = "arrows.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# Grayscale conversion:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
grayscaleImage = 255 - grayscaleImage

# Find the big contours/blobs on the binary image:
contours, hierarchy = cv2.findContours(grayscaleImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)

现在,让我们查看contours一一处理。让我们计算箭头的(非旋转)bounding box 和该子图像的crop。现在,请注意可能会出现一些噪音。在这种情况下,我们不会处理那个 blob。我应用区域过滤器来绕过小区域的斑点。像这样:

# Process each contour 1-1:
for i, c in enumerate(contours):

    # Approximate the contour to a polygon:
    contoursPoly = cv2.approxPolyDP(c, 3, True)

    # Convert the polygon to a bounding rectangle:
    boundRect = cv2.boundingRect(contoursPoly)

    # Get the bounding rect's data:
    rectX = boundRect[0]
    rectY = boundRect[1]
    rectWidth = boundRect[2]
    rectHeight = boundRect[3]

    # Get the rect's area:
    rectArea = rectWidth * rectHeight

    minBlobArea = 100

我们设置minBlobArea 并处理该轮廓。 Crop 轮廓高于该区域阈值时的图像:

        # Check if blob is above min area:
        if rectArea > minBlobArea:

            # Crop the roi:
            croppedImg = grayscaleImage[rectY:rectY + rectHeight, rectX:rectX + rectWidth]

            # Extend the borders for the skeleton:
            borderSize = 5        
            croppedImg = cv2.copyMakeBorder(croppedImg, borderSize, borderSize, borderSize, borderSize, cv2.BORDER_CONSTANT)

            # Store a deep copy of the crop for results:
            grayscaleImageCopy = cv2.cvtColor(croppedImg, cv2.COLOR_GRAY2BGR)

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

这里发生了一些事情。在我crop 当前箭头的ROI 之后,我扩展了该图像的边框。我存储此图像的深层副本以供进一步处理,最后,我计算skeleton。在骨架化之前完成边界扩展,因为如果轮廓太接近图像限制,算法会产生伪影。在各个方向填充图像可以防止这些伪影。 skeleton 是我寻找箭头终点和起点的方式所必需的。更多的是后者,这是第一个裁剪和填充的箭头:

这是skeleton

请注意,轮廓的“厚度”被归一化为 1 个像素。这很酷,因为这是我在接下来的处理步骤中所需要的:寻找起点/终点。这是通过应用convolutionkernel 来完成的,该kernel 旨在识别二值图像上的一个像素宽的端点。详情请参阅this post。我们将准备kernel 并使用cv2.filter2d 得到卷积:

            # 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:
            binaryImage = np.where(imgFiltered == 110, 255, 0)
            # The above operation converted the image to 32-bit float,
            # convert back to 8-bit uint
            binaryImage = binaryImage.astype(np.uint8)

卷积后,所有端点的值为110。将这些像素设置为255,而将其余像素设置为黑色,则生成以下图像(经过适当的转换):

那些微小的像素对应于箭头的“尾部”和“尖端”。请注意,每个“箭头部分”不止一个点。这是因为箭头的端点不能完美地以一个像素结束。例如,在尖端的情况下,端点将比尾部更多。这是我们稍后将利用的特性。现在,注意这一点。有多个终点,但我们只需要一个起点和一个终点。我将使用K-Means 将点分组到两个集群中。

使用K-means 还可以让我确定哪些端点属于尾部,哪些属于尖端,因此我将始终知道箭头的方向。滚吧:

            # Find the X, Y location of all the end-points
            # pixels:
            Y, X = binaryImage.nonzero()

            # Check if I got points on my arrays:
            if len(X) > 0 or len(Y) > 0:

                # Reshape the arrays for K-means
                Y = Y.reshape(-1,1)
                X = X.reshape(-1,1)
                Z = np.hstack((X, Y))

                # K-means operates on 32-bit float data:
                floatPoints = np.float32(Z)

                # Set the convergence criteria and call K-means:
                criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
                _, label, center = cv2.kmeans(floatPoints, 2, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)

注意数据类型。如果我打印标签和中心矩阵,我会得到这个(第一个箭头):

Center:
[[  6.  102. ]
 [104.   20.5]]

Labels:
[[1]
 [1]
 [0]]

center 告诉我每个集群的中心(x,y)——这就是我最初寻找的两个点。 label 告诉我原始数据属于哪个 cluster。如您所见,最初有 3 个点。其中2个点(属于箭头尖端的点)区域分配给cluster 1,而剩余的端点(箭头尾部)分配给cluster 0。在centers 矩阵中,中心按簇号排序。也就是说——第一个中心是cluster 0 的中心,而第二个集群是cluster 1 的中心。使用此信息,我可以轻松查找对大多数点进行分组的集群 - 这将是箭头的尖端,而其余的将是尾部:

                # Set the cluster count, find the points belonging
                # to cluster 0 and cluster 1:
                cluster1Count = np.count_nonzero(label)
                cluster0Count = np.shape(label)[0] - cluster1Count

                # Look for the cluster of max number of points
                # That cluster will be the tip of the arrow:
                maxCluster = 0
                if cluster1Count > cluster0Count:
                    maxCluster = 1

                # Check out the centers of each cluster:
                matRows, matCols = center.shape
                # We need at least 2 points for this operation:
                if matCols >= 2:
                    # Store the ordered end-points here:
                    orderedPoints = [None] * 2
                    # Let's identify and draw the two end-points
                    # of the arrow:
                    for b in range(matRows):
                        # Get cluster center:
                        pointX = int(center[b][0])
                        pointY = int(center[b][1])
                        # Get the "tip"
                        if b == maxCluster:
                            color = (0, 0, 255)
                            orderedPoints[0] = (pointX, pointY)
                        # Get the "tail"
                        else:
                            color = (255, 0, 0)
                            orderedPoints[1] = (pointX, pointY)
                        # Draw it:
                        cv2.circle(grayscaleImageCopy, (pointX, pointY), 3, color, -1)
                        cv2.imshow("End Points", grayscaleImageCopy)
                        cv2.waitKey(0)

这是结果;箭头终点的尖端始终为红色,尾部的终点始终为蓝色:

现在,我们知道了箭头的方向,让我们计算一下角度。我将从0360 测量这个角度。角度始终是水平线和尖端之间的角度。所以,我们手动计算角度:

                        # Store the tip and tail points:
                        p0x = orderedPoints[1][0]
                        p0y = orderedPoints[1][1]
                        p1x = orderedPoints[0][0]
                        p1y = orderedPoints[0][1]
                        # Compute the sides of the triangle:
                        adjacentSide = p1x - p0x
                        oppositeSide = p0y - p1y
                        # Compute the angle alpha:
                        alpha = math.degrees(math.atan(oppositeSide / adjacentSide))

                        # Adjust angle to be in [0,360]:
                        if adjacentSide < 0 < oppositeSide:
                            alpha = 180 + alpha
                        else:
                            if adjacentSide < 0 and oppositeSide < 0:
                                alpha = 270 + alpha
                            else:
                                if adjacentSide > 0 > oppositeSide:
                                    alpha = 360 + alpha

现在您有了角度,并且始终在相同的参考之间测量该角度。很酷,我们可以像下面这样撤消原始图像的旋转:

                        # Deep copy for rotation (if needed):
                        rotatedImg = croppedImg.copy()
                        # Undo rotation while padding output image:
                        rotatedImg = rotateBound(rotatedImg, alpha)
                        cv2. imshow("rotatedImg", rotatedImg)
                        cv2.waitKey(0)

                else:
                    print( "K-Means did not return enough points, skipping..." )
            else:
                 print( "Did not find enough end points on image, skipping..." )

这会产生以下结果:

无论其原始角度如何,箭头始终指向右上角。如果您想在自己的类中对每个箭头进行分类,请将其用作一批训练图像的归一化。 现在,您注意到我使用了一个函数来旋转图像:rotateBound。这个函数是taken from here。此函数在旋转后正确填充图像,因此您最终不会得到错误裁剪的旋转图像。

这是rotateBound的定义和实现:

def rotateBound(image, angle):
    # grab the dimensions of the image and then determine the
    # center
    (h, w) = image.shape[:2]
    (cX, cY) = (w // 2, h // 2)
    # grab the rotation matrix (applying the negative of the
    # angle to rotate clockwise), then grab the sine and cosine
    # (i.e., the rotation components of the matrix)
    M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
    cos = np.abs(M[0, 0])
    sin = np.abs(M[0, 1])
    # compute the new bounding dimensions of the image
    nW = int((h * sin) + (w * cos))
    nH = int((h * cos) + (w * sin))
    # adjust the rotation matrix to take into account translation
    M[0, 2] += (nW / 2) - cX
    M[1, 2] += (nH / 2) - cY
    # perform the actual rotation and return the image
    return cv2.warpAffine(image, M, (nW, nH))

这些是其余箭头的结果。尖端(始终为红色)、尾部(始终为蓝色)及其“投影归一化” - 始终指向右侧:


剩下的就是收集样本你的不同箭头类,设置一个分类器训练 使用您的样本并使用来自我们检查的最后一个处理块的拉直图像对其进行 测试

一些评论:一些箭头,比如没有填充的箭头,没有通过端点识别部分,因此没有产生足够的点进行聚类。该箭头被算法绕过。问题比最初更难,对吧?我建议对该主题进行一些研究,因为无论任务看起来多么“简单”,最终,它都会由自动化的“智能”系统执行。而且这些系统在一天结束时并不是真的那么聪明。

【讨论】:

    【解决方案2】:

    以下是我为完成这项工作而制定的工作流程:

    1. 导入必要的库:
    import cv2
    import numpy as np
    
    1. 定义一个接收图像的函数,并将其处理成可以让python更容易找到每个形状的必要轮廓的东西。可以调整这些值以更好地满足您的需求:
    def preprocess(img):
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        img_blur = cv2.GaussianBlur(img_gray, (5, 5), 1)
        img_canny = cv2.Canny(img_blur, 50, 50)
        kernel = np.ones((3, 3))
        img_dilate = cv2.dilate(img_canny, kernel, iterations=2)
        img_erode = cv2.erode(img_dilate, kernel, iterations=1)
        return img_erode
    
    1. 定义一个接收两个列表的函数;形状的近似轮廓,points,以及该轮廓的凸包的索引,convex_hull。对于下面的函数,在调用函数之前,您必须确保points 列表的长度恰好比convex_hull 列表的长度大2 个单位。推理是,在最佳情况下,箭头应该正好多出 2 个点,而这些点不存在于箭头的凸包中。
    def find_tip(points, convex_hull):
    
    
    1. find_tip 函数中,定义points 数组的索引列表,其中的值不存在于convex_hull 数组中:
        length = len(points)
        indices = np.setdiff1d(range(length), convex_hull)
    
    1. 为了找到箭头的尖端,给定箭头的大致轮廓为points,以及向箭头的两个点的索引indices ,我们可以通过从indices 列表的第一个索引中减去2 或通过将2 添加到indices 列表的第一个索引来找到提示。请参阅以下示例以供参考:

    为了知道您是应该从indices 列表的第一个元素中减去2,还是添加2,您需要执行与第二个(即最后一个)元素完全相反的操作indices 列表中的;如果生成的两个索引从points 列表返回相同的值,那么您找到了箭头的尖端。我使用了一个for 循环,该循环遍历数字01。第一次迭代会将2 添加到indices 列表的第二个元素:j = indices[i] + 2,并从indices 列表的第一个元素中减去2indices[i - 1] - 2

        for i in range(2):
            j = indices[i] + 2
            if j > length - 1:
                j = length - j
            if np.all(points[j] == points[indices[i - 1] - 2]):
                return tuple(points[j])
    

    这部分:

            if j > length - 1:
                j = length - j
    

    是否有这样的情况:

    如果您尝试将2 添加到索引5,您将获得IndexError。因此,如果说jj = indices[i] + 2 变为7,则上述条件会将j 转换为len(points) - j

    1. 读取图像并获取其轮廓,利用之前定义的preprocess 函数,然后将其传递给cv2.findContours 方法:
    img = cv2.imread("arrows.png")
    
    contours, hierarchy = cv2.findContours(preprocess(img), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    
    1. 遍历轮廓,找到每个形状的近似轮廓和凸包:
    for cnt in contours:
        peri = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.025 * peri, True)
        hull = cv2.convexHull(approx, returnPoints=False)
        sides = len(hull)
    
    1. 如果凸包的边数是45(箭头平底的情况下的额外边),并且如果箭头的形状正好有两个不存在的点在凸包中,找到箭头的尖端:
        if 6 > sides > 3 and sides + 2 == len(approx):
            arrow_tip = find_tip(approx[:,0,:], hull.squeeze())
    
    1. 如果确实有提示,那么恭喜!你找到了一个不错的箭头!现在可以突出显示箭头,并且可以在箭头尖端的位置画一个圆圈:
            if arrow_tip:
                cv2.drawContours(img, [cnt], -1, (0, 255, 0), 3)
                cv2.circle(img, arrow_tip, 3, (0, 0, 255), cv2.FILLED)
    
    1. 最后,显示图片:
    cv2.imshow("Image", img)
    cv2.waitKey(0)
    

    总共:

    import cv2
    import numpy as np
    
    def preprocess(img):
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        img_blur = cv2.GaussianBlur(img_gray, (5, 5), 1)
        img_canny = cv2.Canny(img_blur, 50, 50)
        kernel = np.ones((3, 3))
        img_dilate = cv2.dilate(img_canny, kernel, iterations=2)
        img_erode = cv2.erode(img_dilate, kernel, iterations=1)
        return img_erode
    
    def find_tip(points, convex_hull):
        length = len(points)
        indices = np.setdiff1d(range(length), convex_hull)
    
        for i in range(2):
            j = indices[i] + 2
            if j > length - 1:
                j = length - j
            if np.all(points[j] == points[indices[i - 1] - 2]):
                return tuple(points[j])
    
    img = cv2.imread("arrows.png")
    
    contours, hierarchy = cv2.findContours(preprocess(img), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    
    for cnt in contours:
        peri = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.025 * peri, True)
        hull = cv2.convexHull(approx, returnPoints=False)
        sides = len(hull)
    
        if 6 > sides > 3 and sides + 2 == len(approx):
            arrow_tip = find_tip(approx[:,0,:], hull.squeeze())
            if arrow_tip:
                cv2.drawContours(img, [cnt], -1, (0, 255, 0), 3)
                cv2.circle(img, arrow_tip, 3, (0, 0, 255), cv2.FILLED)
    
    cv2.imshow("Image", img)
    cv2.waitKey(0)
    

    原图:

    Python 程序输出:

    【讨论】:

    【解决方案3】:

    这是cv2.connectedComponentsWithStats 的一种方法。分别提取每个箭头后,我得到了箭头上最远的点。这些点之间的距离(或多或少)给了我箭头的长度。另外,我正在使用这两个点计算箭头的角度,即两点之间的斜率。最后,为了找到厚度,我在这些点之间画了一条直线。而且,我正在计算箭头的每个像素到线的最短距离。最重复的距离值应该给我箭头的粗细。

    算法并不完美。特别是,如果箭头倾斜。但是,我觉得这是一个很好的起点,你可以改进它。

    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
    from scipy.spatial import distance
    import math
    
    
    
    img = cv2.imread('arrows.png',0)
    
    _,img = cv2.threshold(img,10,255,cv2.THRESH_BINARY_INV)
    
    labels, stats = cv2.connectedComponentsWithStats(img, 8)[1:3]
    
    
    
    for label in np.unique(labels)[1:]:
    
        arrow = labels==label
    
        indices = np.transpose(np.nonzero(arrow)) #y,x
    
        dist = distance.cdist(indices, indices, 'euclidean')
    
    
        far_points_index = np.unravel_index(np.argmax(dist), dist.shape) #y,x
    
    
        far_point_1 = indices[far_points_index[0],:] # y,x
        far_point_2 = indices[far_points_index[1],:] # y,x
    
    
        ### Slope
        arrow_slope = (far_point_2[0]-far_point_1[0])/(far_point_2[1]-far_point_1[1])  
        arrow_angle = math.degrees(math.atan(arrow_slope))
    
        ### Length
        arrow_length = distance.cdist(far_point_1.reshape(1,2), far_point_2.reshape(1,2), 'euclidean')[0][0]
    
    
        ### Thickness
        x = np.linspace(far_point_1[1], far_point_2[1], 20)
        y = np.linspace(far_point_1[0], far_point_2[0], 20)
        line = np.array([[yy,xx] for yy,xx in zip(y,x)])
        thickness_dist = np.amin(distance.cdist(line, indices, 'euclidean'),axis=0).flatten()
    
        n, bins, patches = plt.hist(thickness_dist,bins=150)
    
        thickness = 2*bins[np.argmax(n)]
    
        print(f"Thickness: {thickness}")
        print(f"Angle: {arrow_angle}")
        print(f"Length: {arrow_length}\n")
        plt.figure()
        plt.imshow(arrow,cmap='gray')
        plt.scatter(far_point_1[1],far_point_1[0],c='r',s=10)
        plt.scatter(far_point_2[1],far_point_2[0],c='r',s=10)
        plt.scatter(line[:,1],line[:,0],c='b',s=10)
        plt.show()
    

    • 厚度:4.309328382835436
    • 角度:58.94059117029002
    • 长度:102.7277956543408

    • 厚度:7.851144897915465
    • 角度:-3.366460663429801
    • 长度:187.32325002519042

    • 厚度:2.246710258748367
    • 角度:55.51004336926862
    • 长度:158.93709447451215

    • 厚度:25.060450615293227
    • 角度:-37.184706453233126
    • 长度:145.60219778561037

    【讨论】:

    • 有趣,这很棒,现在这真的会检测不同对象之间的箭头吗?如果箭头在正方形、矩形和圆形之间,它会看到箭头吗?那太好了,无论如何这都很好
    • 不。一种算法不能只做所有事情:) 当我得到它时,图像中只有箭头。您可以使用一些特征(角、边缘的数量等)在其他对象中提取箭头。然后就可以用我的方法来获取箭头的属性了。
    • 这其实很漂亮!
    【解决方案4】:

    您主要关心的是过滤掉不同形状的箭头。我已经使用convexityDefects 实现了一个方法。你可以阅读更多关于凸面缺陷here

    此外,我还在其他形状内添加了更多箭头,以展示该方法的稳健性。

    更新图片


    使用凸面缺陷从图像中过滤箭头的方法。

    def get_filter_arrow_image(threslold_image):
        blank_image = np.zeros_like(threslold_image)
    
        # dilate image to remove self-intersections error
        kernel_dilate = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
        threslold_image = cv2.dilate(threslold_image, kernel_dilate, iterations=1)
    
        contours, hierarchy = cv2.findContours(threslold_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
        if hierarchy is not None:
    
            threshold_distnace = 1000
    
            for cnt in contours:
                hull = cv2.convexHull(cnt, returnPoints=False)
                defects = cv2.convexityDefects(cnt, hull)
    
                if defects is not None:
                    for i in range(defects.shape[0]):
                        start_index, end_index, farthest_index, distance = defects[i, 0]
    
                        # you can add more filteration based on this start, end and far point
                        # start = tuple(cnt[start_index][0])
                        # end = tuple(cnt[end_index][0])
                        # far = tuple(cnt[farthest_index][0])
    
                        if distance > threshold_distnace:
                            cv2.drawContours(blank_image, [cnt], -1, 255, -1)
    
            return blank_image
        else:
            return None
    

    过滤箭头图像


    我已经为箭头的角度和长度添加了方法,如果这还不够好,请告诉我;基于3个坐标点的角度检测还有更复杂的方法。
    def get_max_distace_point(cnt):
        max_distance = 0
        max_points = None
        for [[x1, y1]] in cnt:
            for [[x2, y2]] in cnt:
                distance = get_length((x1, y1), (x2, y2))
    
                if distance > max_distance:
                    max_distance = distance
                    max_points = [(x1, y1), (x2, y2)]
    
        return max_points
    
    
    def angle_beween_points(a, b):
        arrow_slope = (a[0] - b[0]) / (a[1] - b[1])
        arrow_angle = math.degrees(math.atan(arrow_slope))
        return arrow_angle
    
    def get_arrow_info(arrow_image):
        arrow_info_image = cv2.cvtColor(arrow_image.copy(), cv2.COLOR_GRAY2BGR)
        contours, hierarchy = cv2.findContours(arrow_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        arrow_info = []
        if hierarchy is not None:
    
            for cnt in contours:
                # draw single arrow on blank image
                blank_image = np.zeros_like(arrow_image)
                cv2.drawContours(blank_image, [cnt], -1, 255, -1)
    
                point1, point2 = get_max_distace_point(cnt)
    
                angle = angle_beween_points(point1, point2)
                lenght = get_length(point1, point2)
    
                cv2.line(arrow_info_image, point1, point2, (0, 255, 255), 1)
    
                cv2.circle(arrow_info_image, point1, 2, (255, 0, 0), 3)
                cv2.circle(arrow_info_image, point2, 2, (255, 0, 0), 3)
    
                cv2.putText(arrow_info_image, "angle : {0:0.2f}".format(angle),
                            point2, cv2.FONT_HERSHEY_PLAIN, 0.8, (0, 0, 255), 1)
                cv2.putText(arrow_info_image, "lenght : {0:0.2f}".format(lenght),
                            (point2[0], point2[1]+20), cv2.FONT_HERSHEY_PLAIN, 0.8, (0, 0, 255), 1)
    
            return arrow_info_image, arrow_info
        else:
            return None, None
    
    

    角度和长度图片

    代码

    import math
    import cv2
    import numpy as np
    
    
    def get_filter_arrow_image(threslold_image):
        blank_image = np.zeros_like(threslold_image)
    
        # dilate image to remove self-intersections error
        kernel_dilate = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
        threslold_image = cv2.dilate(threslold_image, kernel_dilate, iterations=1)
    
        contours, hierarchy = cv2.findContours(threslold_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
        if hierarchy is not None:
    
            threshold_distnace = 1000
    
            for cnt in contours:
                hull = cv2.convexHull(cnt, returnPoints=False)
                defects = cv2.convexityDefects(cnt, hull)
    
                if defects is not None:
                    for i in range(defects.shape[0]):
                        start_index, end_index, farthest_index, distance = defects[i, 0]
    
                        # you can add more filteration based on this start, end and far point
                        # start = tuple(cnt[start_index][0])
                        # end = tuple(cnt[end_index][0])
                        # far = tuple(cnt[farthest_index][0])
    
                        if distance > threshold_distnace:
                            cv2.drawContours(blank_image, [cnt], -1, 255, -1)
    
            return blank_image
        else:
            return None
    
    
    def get_length(p1, p2):
        line_length = ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
        return line_length
    
    
    def get_max_distace_point(cnt):
        max_distance = 0
        max_points = None
        for [[x1, y1]] in cnt:
            for [[x2, y2]] in cnt:
                distance = get_length((x1, y1), (x2, y2))
    
                if distance > max_distance:
                    max_distance = distance
                    max_points = [(x1, y1), (x2, y2)]
    
        return max_points
    
    
    def angle_beween_points(a, b):
        arrow_slope = (a[0] - b[0]) / (a[1] - b[1])
        arrow_angle = math.degrees(math.atan(arrow_slope))
        return arrow_angle
    
    
    def get_arrow_info(arrow_image):
        arrow_info_image = cv2.cvtColor(arrow_image.copy(), cv2.COLOR_GRAY2BGR)
        contours, hierarchy = cv2.findContours(arrow_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        arrow_info = []
        if hierarchy is not None:
    
            for cnt in contours:
                # draw single arrow on blank image
                blank_image = np.zeros_like(arrow_image)
                cv2.drawContours(blank_image, [cnt], -1, 255, -1)
    
                point1, point2 = get_max_distace_point(cnt)
    
                angle = angle_beween_points(point1, point2)
                lenght = get_length(point1, point2)
    
                cv2.line(arrow_info_image, point1, point2, (0, 255, 255), 1)
    
                cv2.circle(arrow_info_image, point1, 2, (255, 0, 0), 3)
                cv2.circle(arrow_info_image, point2, 2, (255, 0, 0), 3)
    
                cv2.putText(arrow_info_image, "angle : {0:0.2f}".format(angle),
                            point2, cv2.FONT_HERSHEY_PLAIN, 0.8, (0, 0, 255), 1)
                cv2.putText(arrow_info_image, "lenght : {0:0.2f}".format(lenght),
                            (point2[0], point2[1] + 20), cv2.FONT_HERSHEY_PLAIN, 0.8, (0, 0, 255), 1)
    
            return arrow_info_image, arrow_info
        else:
            return None, None
    
    
    if __name__ == "__main__":
        image = cv2.imread("image2.png")
    
        gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        _, thresh_image = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY_INV)
        cv2.imshow("thresh_image", thresh_image)
    
        arrow_image = get_filter_arrow_image(thresh_image)
        if arrow_image is not None:
            cv2.imshow("arrow_image", arrow_image)
            cv2.imwrite("arrow_image.png", arrow_image)
    
            arrow_info_image, arrow_info = get_arrow_info(arrow_image)
            cv2.imshow("arrow_info_image", arrow_info_image)
            cv2.imwrite("arrow_info_image.png", arrow_info_image)
    
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    

    细箭头上的凸缺陷。

    • 蓝点 - 缺陷的起点
    • 绿点 - 远点(如果有缺陷)
    • 红点 - 缺陷终点
    • 黄线 = 从起点到终点的缺陷线。
    image defect-1 defect-2 and so on...
    ..

    【讨论】:

    • 这是一个很好的快速问题,我在粗箭头上看到凸面缺陷,它们位于细箭头上的什么位置?图片在这里ibb.co/Bqg8bTk随时在回答中画画
    • 还有什么是阈值距离1000?是不是任何箭头的过滤器都必须大于 1000?
    • 我已经更新了答案。 threshold_distnace = 1000建议黄线(缺陷线-起点到终点)到绿点(远点)的距离。
    • 这很好,问题是,如果图片中还有其他凸物体,它会不起作用吗?然而,在规则形状、矩形、三角形中,它会起作用,这是一个很好的起点,我可以与机器学习一起实现答案,除非你知道如何区分箭头和其他凸/凸缺陷,欣赏它!
    • 如果您可以从数据中提供一些凸形示例,则构建方法会更容易。另外,如果你有足够的注释数据,我建议你使用机器学习。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-09
    • 2011-05-30
    • 2019-10-07
    • 2011-04-30
    • 2017-02-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多