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)
也许我们可以查看轮廓的尖端,并检测三角形?
希望它能够检测不同对象之间、正方形、矩形和圆形之间的箭头。(否则,将不得不使用机器学习)。如果可能的话,也很高兴得到这三个结果(箭头长度、厚度、方向角度)
这个问题建议模板匹配,并且不指定任何代码库。寻找可以用代码创建的可行的东西
如果 PythonOpenCV 没有能力,请开放使用另一个库。
这是我整理的工作流程,可以使这项工作成功:
import cv2
import numpy as np
Run Code Online (Sandbox Code Playgroud)
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)
points以及该轮廓的凸包索引convex_hull。对于下面的函数,在调用该函数之前,必须确保points列表的长度比列表的长度正好大 2 个单位。convex_hull原因是,最佳情况下,箭头应该恰好还有 2 个箭头凸包中不存在的点。def find_tip(points, convex_hull):
Run Code Online (Sandbox Code Playgroud)
find_tip函数中,定义数组索引列表points,其中值不存在于数组中convex_hull: length = len(points)
indices = np.setdiff1d(range(length), convex_hull)
Run Code Online (Sandbox Code Playgroud)
points的两个点的索引,我们可以通过从列表中的第一个索引中减去来找到尖端,或添加到列表的第一个索引。请参阅以下示例以供参考:indices2indices2indices为了知道是否应该2从列表的第一个元素中减去indices或添加2,您需要对列表的第二个(即最后一个)元素执行完全相反的操作indices;如果结果两个索引从points列表中返回相同的值,那么您找到了箭头的尖端。我使用了一个for循环遍历数字0和的循环1。第一次迭代将添加2到列表的第二个元素indices:j = 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变为,7则j = indices[i] + 2上述条件将转换j为len(points) - j。
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)
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)
4或5(如果箭头具有平底,则为额外的边),并且如果箭头的形状恰好还有凸包中不存在的两个点,则找到箭头: if 6 > sides > 3 and sides + 2 == len(approx):
arrow_tip = find_tip(approx[:,0,:], hull.squeeze())
Run Code Online (Sandbox Code Playgroud)
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)
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程序输出:
您要求的解决方案太复杂,无法通过一种函数或特定算法来解决。事实上,问题可以分解为更小的步骤,每个步骤都有自己的算法和解决方案。我不会为您提供免费、完整的复制粘贴解决方案,而是为您提供问题的总体概述并发布我设计的解决方案的一部分。这些是我建议的步骤:
\n识别并提取图像中的所有箭头斑点,并对它们进行一一处理。
\n尝试寻找终点箭头的即终点和起点(或“尾部”和“尖端”)
\n撤消旋转,这样无论角度如何,箭头始终都是直的。
\n此后,箭头将始终指向一个方向。这种标准化让我们可以轻松地进行分类。
\n处理后,您可以将图像传递给Knn分类器、支持向量机,甚至(如果您愿意在这个问题上称其为“大佬”)CNN (在这种情况下,您可能不需要撤消旋转 - 只要您有足够的训练样本)。您甚至不必计算特征,因为将原始图像传递给 aSVM可能就足够了。但是,每个箭头类别都需要多个训练样本。
好吧,让我们看看。首先,让我们从输入中提取每个箭头。这是使用 完成的cv2.findCountours,这部分非常简单:
# 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)\nRun Code Online (Sandbox Code Playgroud)\n现在,我们来一一查看contours并处理。让我们计算箭头和该子图像的(未旋转的)。现在请注意,可能会出现一些噪音。在这种情况下,我们将不会处理该 blob。我应用区域过滤器来绕过小区域的斑点。像这样:bounding boxcrop
# 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\nRun Code Online (Sandbox Code Playgroud)\n我们设置minBlobArea并处理该轮廓。Crop如果轮廓高于该区域阈值,则图像:
# 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)\nRun Code Online (Sandbox Code Playgroud)\n这里发生了一些事情。在当前箭头之后crop,ROI我扩展了该图像的边框。我存储该图像的深层副本以供进一步处理,最后,我计算skeleton. 边界扩展是在骨架化之前完成的,因为如果轮廓太接近图像限制,算法会产生伪影。在各个方向上填充图像可以防止这些伪影。这skeleton是我寻找箭头终点和起点的方式所需要的。更多的是后者,这是裁剪和填充的第一个箭头:
\n这是skeleton:
\n请注意,轮廓的“厚度”被标准化为 1 像素。这很酷,因为这就是我以下处理步骤所需的:查找起点/终点。这是通过应用convolution旨在kernel识别二值图像上的一像素宽端点的 来完成的。具体可以参考这篇文章。我们将准备kernel并使用cv2.filter2d来获得卷积:
# 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)\nRun Code Online (Sandbox Code Playgroud)\n卷积后,所有端点的值为110。将这些像素设置为255,而其余像素设置为黑色,会产生以下图像(经过正确转换后):
\n这些微小的像素对应于箭头的“尾部”和“尖端”。请注意,每个“箭头部分”有多个点。这是因为箭头的端点并不完美地以一个像素结束。例如,在尖端的情况下,将比尾部有更多的端点。这是我们稍后将利用的一个特性。现在,请注意这一点。有多个终点,但我们只需要一个起点和一个终点。我要使用K-Means将点分组为两个簇。
使用K-means还可以让我识别哪些端点属于尾部,哪些端点属于尖端,因此我将始终知道箭头的方向。来吧:
# 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)\nRun Code Online (Sandbox Code Playgroud)\n请注意数据类型。如果我打印标签和中心矩阵,我会得到这个(对于第一个箭头):
\nCenter:\n[[ 6. 102. ]\n [104. 20.5]]\n\nLabels:\n[[1]\n [1]\n [0]]\nRun Code Online (Sandbox Code Playgroud)\ncenter告诉我每个簇的中心(x,y)\xe2\x80\x93 这是我最初寻找的两个点。label告诉我cluster原始数据属于哪一个。如您所见,最初有 3 个点。其中 2 个点(属于箭头尖端的点)区域分配给cluster 1,而剩余的端点(箭头尾部)分配给cluster 0。在centers矩阵中,中心按簇编号排序。即 \xe2\x80\x93 第一个中心是 的一个cluster 0,而第二个簇是 的中心cluster 1。使用此信息,我可以轻松地查找将大多数点分组的簇 - 这将是箭头的尖端,而其余的将是尾部:
# 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)\nRun Code Online (Sandbox Code Playgroud)\n这就是结果;箭头终点的尖端始终为红色,尾部的终点始终为蓝色:
\n
\n现在,我们知道了箭头的方向,让我们计算角度。0我将从到测量这个角度360。该角度始终是地平线和尖端之间的角度。因此,我们手动计算角度:
# 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\nRun 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..." )\nRun Code Online (Sandbox Code Playgroud)\n这会产生以下结果:
\n
\n无论其原始角度如何,箭头始终指向右上角。如果您想将每个箭头分类到其自己的类中,请使用它作为一批训练图像的归一化。\n现在,您注意到我使用了一个函数来旋转图像:rotateBound。这个函数取自这里。此功能可以在旋转后正确填充图像,这样您就不会得到被错误裁剪的旋转图像。
这是以下的定义和实现rotateBound:
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))\nRun Code Online (Sandbox Code Playgroud)\n这些是其余箭头的结果。尖端(始终为红色)、尾部(始终为蓝色)及其“投影归一化” - 始终指向右侧:
\n
\n
\n
\n
\n剩下的就是收集不同箭头类别的样本,设置分类器,用样本对其进行训练并进行测试。,并使用来自我们检查的最后一个处理块的拉直图像对其
\n一些备注:某些箭头(例如未填充的箭头)未能通过端点识别部分,因此无法产生足够的点用于聚类。算法绕过了该箭头。但问题比最初更难,对吧?我建议对这个主题进行一些研究,因为无论任务看起来多么“容易”,最终都会由自动化的“智能”系统执行。归根结底,这些系统并不是真的那么聪明。
\n| 归档时间: |
|
| 查看次数: |
7534 次 |
| 最近记录: |