如何检测图像中不同类型的箭头?

How to detect different types of arrows in image?

提问人:mattsmith5 提问时间:3/20/2021 最后编辑:desertnautmattsmith5 更新时间:1/31/2022 访问量:6674

问:

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

enter image description here

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

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

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

此问题建议模板匹配,并且不指定任何代码库。寻找可以创建代码的可行内容

如何使用 Open CV Python 检测箭头?

如果 PythonOpenCV 没有功能,请开放使用另一个库。

python-3.x opencv 图像处理 计算机视觉

评论

0赞 stateMachine 3/20/2021
你到底需要什么?您是否希望将每个箭头归入其自己的类别中?例如,给定箭头图像,分类器是否应输出“属于类 1 - 实心箭头”?
0赞 mattsmith5 3/20/2021
嗨@eldesgraciado我刚刚更新了问题
0赞 granular bastard 11/28/2023
所有示例图像都包含与其他对象隔离的箭头?哪种算法可以检测接触或部分重叠其他物体的箭头?

答:

3赞 Prefect 3/26/2021 #1

下面是使用 .在单独提取每个箭头后,我得到了箭头上最远的点。这些点之间的距离给了我(或多或少)箭头的长度。另外,我正在使用这两个点来计算箭头的角度,即两点之间的坡度。最后,为了找到厚度,我在这些点之间画了一条直线。而且,我正在计算箭头的每个像素到线的最短距离。重复次数最多的距离值应该给我箭头的粗细。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()

arrow_1

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

arrow_2

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

arrow_3

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

arrow_4

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

评论

0赞 mattsmith5 3/27/2021
有趣的是,这很棒,现在这真的能检测到不同物体之间的箭头吗?如果箭头在正方形、矩形和圆形之间,它会看到箭头吗?那太好了,反正这很好
0赞 Prefect 3/27/2021
不。一种算法不能只做所有事情:)当我拿到它时,图像中只有箭头。您可以使用某些特征(角数、边数等)来提取其他对象中的箭头。然后,您可以使用我的方法获取箭头的属性。
0赞 Red 4/1/2021
这其实很整洁!
5赞 stateMachine 3/27/2021 #2

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

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

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

  3. 撤消旋转,因此无论箭头的角度如何,您始终可以拉直箭头。

  4. 在此之后,箭头将始终指向一个方向。这种规范化可以很容易地进行分类

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

现在,让我们一检查并处理它们。让我们计算箭头和该子图像的(非旋转)。现在,请注意可能会出现一些噪音。在这种情况下,我们不会处理该 Blob。我应用一个区域过滤器来绕过小面积的斑点。喜欢这个:contoursbounding boxcrop

# 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

我们设定了一个轮廓并处理该轮廓。 如果等值线高于该区域阈值,则为图像:minBlobAreaCrop

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

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

这是:skeleton

请注意,轮廓的“厚度”归一化为 1 像素。这很酷,因为这就是我在以下处理步骤中所需要的:查找起点/终点。这是通过应用一个 设计用于识别二进制图像上一个像素宽的端点来完成的。有关具体信息,请参阅此帖子。我们将准备 并用于获取卷积:convolutionkernelkernelcv2.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)

卷积后,所有端点的值均为 。将这些像素设置为 ,而其余像素设置为黑色,将生成以下图像(正确转换后):110255

这些微小的像素对应于箭头的“尾部”和“尖端”。请注意,每个“箭头部分”都有多个点。这是因为箭头的端点并不完全以一个像素结束。例如,在尖端的情况下,端点会比尾部多。这是我们将在后面利用的一个特征。现在,请注意这一点。有多个终点,但我们只需要一个起点和一个终点。我将用于将点分组为两个聚类。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告诉我每个集群的中心——这是我最初寻找的两个点。 告诉我原始数据落在哪个位置。如您所见,最初有 3 个点。其中 2 个点(属于箭头尖端的点)区域分配给 ,而其余端点(箭头尾部)则分配给 。在矩阵中,中心按聚类编号排序。也就是说,第一个中心是 ,而第二个簇是 的中心。使用此信息,我可以很容易地查找将大多数点分组的聚类 - 这将是箭头的尖端,而其余的将是尾部:(x,y)labelclustercluster 1cluster 0centerscluster 0cluster 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

这是定义和实现: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))

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


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

一些备注:有些箭头,如未填充的箭头,未能通过端点识别部分,因此没有产生足够的点进行聚类。该箭头被算法绕过。不过,这个问题比最初更棘手,对吧?我建议对这个话题做一些研究,因为无论任务看起来多么“容易”,最终都会由自动化的“智能”系统执行。归根结底,这些系统并不是那么智能。

3赞 Vatsal Parsaniya 3/27/2021 #3

因为您主要关心的是过滤掉不同形状的箭头。我已经使用.您可以在此处阅读有关凸性缺陷的更多信息。convexityDefects

此外,我还在其他形状中添加了更多箭头,以演示该方法的鲁棒性。

更新的图像

Updated Image


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

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

过滤箭头图像

Filterd Image


我添加了箭头角度和长度的方法,如果这还不够好,请告诉我;基于 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

角度和长度图像

angle and length image

法典

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

细箭头上的凸性缺陷。

  • 蓝点 - 缺陷的起点
  • 绿点 - 缺陷的远点
  • 红点 - 缺陷终点
  • 黄线 = 从起点到终点的缺陷线。
图像 缺陷-1 缺陷-2 等等......
enter image description here enter image description here enter image description here ..

评论

0赞 mattsmith5 3/27/2021
这是一个很好的快速问题,我在粗箭头上看到了凸性缺陷,它们在细箭头上的位置在哪里?图片在这里 ibb.co/Bqg8bTk 随意画出答案
0赞 mattsmith5 3/27/2021
还有什么是阈值距离 1000?这只是任何箭头必须大于 1000 的过滤器吗?
0赞 Vatsal Parsaniya 3/27/2021
我已经更新了答案。 建议从黄线(缺陷线 - 起点到终点)到绿点(远点)的距离。threshold_distnace = 1000
0赞 mattsmith5 3/28/2021
这太好了,问题是,如果图片中还有其他凸起的物体,它就不行了吗?但是在常规形状、矩形、三角形中,它会起作用,这是一个很好的起点,我可能会与机器学习一起实现答案,除非您知道如何区分其他凸/和凸缺陷中的箭头,请欣赏它!
0赞 Vatsal Parsaniya 3/28/2021
如果可以从数据中提供一些凸形状的示例,则构建方法会更容易。此外,如果你有足够的注释数据,我建议你使用机器学习。
7赞 Red 3/29/2021 #4

以下是我整理的工作流程,可以完成这项工作:

  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. 定义一个将接受两个列表的函数;形状的近似轮廓 ,以及该轮廓的凸包的索引。对于以下函数,在调用该函数之前,必须确保列表的长度正好比列表的长度大 2 个单位。理由是,理想情况下,箭头应该有 2 个点,而这些点在箭头的凸包中不存在。pointsconvex_hullpointsconvex_hull
def find_tip(points, convex_hull):

  1. 在函数中,定义数组中不存在值的数组索引列表:find_tippointsconvex_hull
    length = len(points)
    indices = np.setdiff1d(range(length), convex_hull)
  1. 为了找到箭头的尖端,给定箭头的近似轮廓和与箭头凹陷的两个点的索引,我们可以通过从列表中的第一个索引中减去或添加到列表的第一个索引来找到尖端。请参阅以下示例以供参考:pointsindices2indices2indices

enter image description here

为了知道您是应该从列表的第一个元素中减去,还是添加 ,您需要执行与列表的第二个(最后一个)元素完全相反的操作;如果生成的两个索引从列表中返回相同的值,则您找到了箭头的尖端。我使用了一个循环来循环数字和 .第一次迭代将添加到列表的第二个元素: ,并从列表的第一个元素中减去: :2indices2indicespointsfor012indicesj = indices[i] + 22indicesindices[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

有这样的情况:

enter image description here

其中,如果您尝试添加到索引中,您将得到一个 .因此,如果 从 中变为 ,则上述条件将转换为 。25IndexErrorj7j = indices[i] + 2jlen(points) - j

  1. 读取图像并获取其轮廓,在将图像传递到方法之前,使用前面定义的函数:preprocesscv2.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)

原始图像:

enter image description here

Python 程序输出:

enter image description here

评论

0赞 mattsmith5 4/7/2021
嗨,安,我们在这里留下了另一个赏金问题,谢谢 stackoverflow.com/questions/66946804/......