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

mat*_*th5 6 python opencv image-processing computer-vision python-3.x

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)
Run Code Online (Sandbox Code Playgroud)

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

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

这个问题建议模板匹配,并且不指定任何代码库。寻找可以用代码创建的可行的东西

如何使用 open cv python 检测箭头?

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

Ann*_*Zen 8

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

  1. 导入必要的库:
import cv2
import numpy as np
Run Code Online (Sandbox Code Playgroud)
  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
Run Code Online (Sandbox Code Playgroud)
  1. 定义一个接受两个列表的函数;形状 的近似轮廓points以及该轮廓的凸包索引convex_hull。对于下面的函数,在调用该函数之前,必须确保points列表的长度比列表的长度正好大 2 个单位。convex_hull原因是,最佳情况下,箭头应该恰好还有 2 个箭头凸包中不存在的点。
def find_tip(points, convex_hull):

Run Code Online (Sandbox Code Playgroud)
  1. find_tip函数中,定义数组索引列表points,其中值不存在于数组中convex_hull
    length = len(points)
    indices = np.setdiff1d(range(length), convex_hull)
Run Code Online (Sandbox Code Playgroud)
  1. 为了找到箭头的尖端,假设我们有箭头的近似轮廓和向箭头points的两个点的索引,我们可以通过从列表中的第一个索引中减去来找到尖端,或添加到列表的第一个索引。请参阅以下示例以供参考:indices2indices2indices

在此输入图像描述

为了知道是否应该2从列表的第一个元素中减去indices或添加2,您需要对列表的第二个(即最后一个)元素执行完全相反的操作indices;如果结果两个索引从points列表中返回相同的值,那么您找到了箭头的尖端。我使用了一个for循环遍历数字0和的循环1。第一次迭代将添加2到列表的第二个元素indicesj = indices[i] + 2,并从列表2的第一个元素中减去: :indicesindices[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])
Run Code Online (Sandbox Code Playgroud)

这部分:

        if j > length - 1:
            j = length - j
Run Code Online (Sandbox Code Playgroud)

有没有这样的情况:

在此输入图像描述

如果您尝试添加2到索引5,您将得到一个IndexError. 因此,如果 说j变为,7j = indices[i] + 2上述条件将转换jlen(points) - j

  1. preprocess在将图像传递给方法之前,利用之前定义的函数读取图像并获取其轮廓cv2.findContours
img = cv2.imread("arrows.png")

contours, hierarchy = cv2.findContours(preprocess(img), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
Run Code Online (Sandbox Code Playgroud)
  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)
Run Code Online (Sandbox Code Playgroud)
  1. 如果凸包的边数为45(如果箭头具有平底,则为额外的边),并且如果箭头的形状恰好还有凸包中不存在的两个点,则找到箭头:
    if 6 > sides > 3 and sides + 2 == len(approx):
        arrow_tip = find_tip(approx[:,0,:], hull.squeeze())
Run Code Online (Sandbox Code Playgroud)
  1. 如果确实有提示,那么恭喜你!你找到了一支不错的箭头!现在可以突出显示箭头,并可以在箭头尖端的位置绘制一个圆:
        if arrow_tip:
            cv2.drawContours(img, [cnt], -1, (0, 255, 0), 3)
            cv2.circle(img, arrow_tip, 3, (0, 0, 255), cv2.FILLED)
Run Code Online (Sandbox Code Playgroud)
  1. 最后展示一下图片:
cv2.imshow("Image", img)
cv2.waitKey(0)
Run Code Online (Sandbox Code Playgroud)

共:

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)
Run Code Online (Sandbox Code Playgroud)

原图:

在此输入图像描述

Python程序输出:

在此输入图像描述


sta*_*ine 6

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

\n
    \n
  1. 识别提取图像中的所有箭头斑点,并对它们进行一一处理。

    \n
  2. \n
  3. 尝试寻找终点箭头的即终点和起点(或“尾部”和“尖端”)

    \n
  4. \n
  5. 撤消旋转,这样无论角度如何,箭头始终都是直的。

    \n
  6. \n
  7. 此后,箭头将始终指向一个方向。这种标准化让我们可以轻松地进行分类

    \n
  8. \n
\n

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

\n

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

\n
# Imports:\nimport cv2\nimport math\nimport numpy as np\n\n# image path\npath = "D://opencvImages//"\nfileName = "arrows.png"\n\n# Reading an image in default mode:\ninputImage = cv2.imread(path + fileName)\n\n# Grayscale conversion:\ngrayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)\ngrayscaleImage = 255 - grayscaleImage\n\n# Find the big contours/blobs on the binary image:\ncontours, hierarchy = cv2.findContours(grayscaleImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
# Process each contour 1-1:\nfor i, c in enumerate(contours):\n\n    # Approximate the contour to a polygon:\n    contoursPoly = cv2.approxPolyDP(c, 3, True)\n\n    # Convert the polygon to a bounding rectangle:\n    boundRect = cv2.boundingRect(contoursPoly)\n\n    # Get the bounding rect\'s data:\n    rectX = boundRect[0]\n    rectY = boundRect[1]\n    rectWidth = boundRect[2]\n    rectHeight = boundRect[3]\n\n    # Get the rect\'s area:\n    rectArea = rectWidth * rectHeight\n\n    minBlobArea = 100\n
Run Code Online (Sandbox Code Playgroud)\n

我们设置minBlobArea并处理该轮廓。Crop如果轮廓高于该区域阈值,则图像:

\n
        # Check if blob is above min area:\n        if rectArea > minBlobArea:\n\n            # Crop the roi:\n            croppedImg = grayscaleImage[rectY:rectY + rectHeight, rectX:rectX + rectWidth]\n\n            # Extend the borders for the skeleton:\n            borderSize = 5        \n            croppedImg = cv2.copyMakeBorder(croppedImg, borderSize, borderSize, borderSize, borderSize, cv2.BORDER_CONSTANT)\n\n            # Store a deep copy of the crop for results:\n            grayscaleImageCopy = cv2.cvtColor(croppedImg, cv2.COLOR_GRAY2BGR)\n\n            # Compute the skeleton:\n            skeleton = cv2.ximgproc.thinning(croppedImg, None, 1)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n\n

这是skeleton

\n\n

请注意,轮廓的“厚度”被标准化为 1 像素。这很酷,因为这就是我以下处理步骤所需的:查找起点/终点。这是通过应用convolution旨在kernel识别二值图像上的一像素宽端点的 来完成的。具体可以参考这篇文章。我们将准备kernel并使用cv2.filter2d来获得卷积:

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

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

\n\n

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

\n

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

\n
            # Find the X, Y location of all the end-points\n            # pixels:\n            Y, X = binaryImage.nonzero()\n\n            # Check if I got points on my arrays:\n            if len(X) > 0 or len(Y) > 0:\n\n                # Reshape the arrays for K-means\n                Y = Y.reshape(-1,1)\n                X = X.reshape(-1,1)\n                Z = np.hstack((X, Y))\n\n                # K-means operates on 32-bit float data:\n                floatPoints = np.float32(Z)\n\n                # Set the convergence criteria and call K-means:\n                criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)\n                _, label, center = cv2.kmeans(floatPoints, 2, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
Center:\n[[  6.  102. ]\n [104.   20.5]]\n\nLabels:\n[[1]\n [1]\n [0]]\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
                # Set the cluster count, find the points belonging\n                # to cluster 0 and cluster 1:\n                cluster1Count = np.count_nonzero(label)\n                cluster0Count = np.shape(label)[0] - cluster1Count\n\n                # Look for the cluster of max number of points\n                # That cluster will be the tip of the arrow:\n                maxCluster = 0\n                if cluster1Count > cluster0Count:\n                    maxCluster = 1\n\n                # Check out the centers of each cluster:\n                matRows, matCols = center.shape\n                # We need at least 2 points for this operation:\n                if matCols >= 2:\n                    # Store the ordered end-points here:\n                    orderedPoints = [None] * 2\n                    # Let\'s identify and draw the two end-points\n                    # of the arrow:\n                    for b in range(matRows):\n                        # Get cluster center:\n                        pointX = int(center[b][0])\n                        pointY = int(center[b][1])\n                        # Get the "tip"\n                        if b == maxCluster:\n                            color = (0, 0, 255)\n                            orderedPoints[0] = (pointX, pointY)\n                        # Get the "tail"\n                        else:\n                            color = (255, 0, 0)\n                            orderedPoints[1] = (pointX, pointY)\n                        # Draw it:\n                        cv2.circle(grayscaleImageCopy, (pointX, pointY), 3, color, -1)\n                        cv2.imshow("End Points", grayscaleImageCopy)\n                        cv2.waitKey(0)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n\n

现在,我们知道了箭头的方向,让我们计算角度0我将从到测量这个角度360。该角度始终是地平线和尖端之间的角度。因此,我们手动计算角度:

\n
                        # Store the tip and tail points:\n                        p0x = orderedPoints[1][0]\n                        p0y = orderedPoints[1][1]\n                        p1x = orderedPoints[0][0]\n                        p1y = orderedPoints[0][1]\n                        # Compute the sides of the triangle:\n                        adjacentSide = p1x - p0x\n                        oppositeSide = p0y - p1y\n                        # Compute the angle alpha:\n                        alpha = math.degrees(math.atan(oppositeSide / adjacentSide))\n\n                        # Adjust angle to be in [0,360]:\n                        if adjacentSide < 0 < oppositeSide:\n                            alpha = 180 + alpha\n                        else:\n                            if adjacentSide < 0 and oppositeSide < 0:\n                                alpha = 270 + alpha\n                            else:\n                                if adjacentSide > 0 > oppositeSide:\n                                    alpha = 360 + alpha\n
Run Code Online (Sandbox Code Playgroud)\n

现在你有了角度,并且这个角度总是在相同的参考点之间测量的。太酷了,我们可以像下面这样撤消原始图像的旋转:

\n
                        # Deep copy for rotation (if needed):\n                        rotatedImg = croppedImg.copy()\n                        # Undo rotation while padding output image:\n                        rotatedImg = rotateBound(rotatedImg, alpha)\n                        cv2. imshow("rotatedImg", rotatedImg)\n                        cv2.waitKey(0)\n\n                else:\n                    print( "K-Means did not return enough points, skipping..." )\n            else:\n                 print( "Did not find enough end points on image, skipping..." )\n
Run Code Online (Sandbox Code Playgroud)\n

这会产生以下结果:

\n\n

无论其原始角度如何,箭头始终指向右上角。如果您想将每个箭头分类到其自己的类中,请使用它作为一批训练图像的归一化。\n现在,您注意到我使用了一个函数来旋转图像:rotateBound。这个函数取自这里。此功能可以在旋转后正确填充图像,这样您就不会得到被错误裁剪的旋转图像。

\n

这是以下的定义和实现rotateBound

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

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

\n\n\n
\n\n\n

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

\n

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

\n