[英]Finding the end points of a hand drawn line with opencv
我試圖找到手繪線的兩個端點我寫了這個找到輪廓的片段,但端點不正確:
img = cv2.imread("my_img.jpeg")
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Binary Threshold:
_, thr_img = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
cv2.imshow(winname="after threshold", mat=thr_img)
cv2.waitKey(0)
contours, _ = cv2.findContours(image=thr_img, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_SIMPLE)
for idx, cnt in enumerate(contours):
print("Contour #", idx)
cv2.drawContours(image=img, contours=[cnt], contourIdx=0, color=(255, 0, 0), thickness=3)
cv2.circle(img, tuple(cnt[0][0]), 5, (255, 255, 0), 5) # Result in wrong result
cv2.circle(img, tuple(cnt[-1][0]), 5, (0, 0, 255), 5) # Result in wrong result
cv2.imshow(winname="contour" + str(idx), mat=img)
cv2.waitKey(0)
原圖:
我也試過cornerHarris
但它給了我一些額外的分數,
有人可以建議一種准確且更好的方法嗎?
此解決方案使用 Python 實現此方法。 這個想法是用一個特殊的 kernel 對圖像進行卷積,它可以識別一條線的起點/終點。 這些是步驟:
現在,這將是所提出算法的第一次迭代。 但是,根據輸入圖像,可能存在重復的端點 - 彼此太近並且可以連接的單個點。 所以,讓我們結合一些額外的處理來擺脫這些重復的點。
這些最后的步驟太籠統了,當我們到達那一步時,讓我進一步詳細說明消除重復項背后的想法。 讓我們看看第一部分的代碼:
# imports:
import cv2
import numpy as np
# image path
path = "D://opencvImages//"
fileName = "hJVBX.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Resize image:
scalePercent = 50 # percent of original size
width = int(inputImage.shape[1] * scalePercent / 100)
height = int(inputImage.shape[0] * scalePercent / 100)
# New dimensions:
dim = (width, height)
# resize image
resizedImage = cv2.resize(inputImage, dim, interpolation=cv2.INTER_AREA)
# Color conversion
grayscaleImage = cv2.cvtColor(resizedImage, cv2.COLOR_BGR2GRAY)
grayscaleImage = 255 - grayscaleImage
到目前為止,我已經調整了圖像的大小(到原始比例的0.5
)並將其轉換為灰度(實際上是一個倒置的二進制圖像)。 現在,檢測端點的第一步是將width
標准化為1 pixel
。 這是通過計算skeleton
來實現的,可以使用OpenCV 的擴展圖像處理模塊來實現:
# Compute the skeleton:
skeleton = cv2.ximgproc.thinning(grayscaleImage, None, 1)
這是骨架:
現在,讓我們運行端點檢測部分:
# 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)
查看原始鏈接以獲取有關此方法的信息,但一般要點是 kernel 使得與線中的端點的卷積將產生110
的值,這是鄰域求和的結果。 涉及float
操作,因此必須小心數據類型和轉換。 該過程的結果可以在這里觀察到:
這些是端點,但是請注意,如果它們太靠近,可以連接一些點。 現在是重復消除步驟。 讓我們首先定義檢查點是否重復的標准。 如果這些點太接近,我們將加入它們。 讓我們提出一種基於形態學的點接近度方法。 我將使用大小為3
和3
次迭代的rectangular kernel
來擴展端點掩碼。 如果兩個或多個點太接近,它們的膨脹將產生一個大的、獨特的 blob:
# RGB copy of this:
rgbMask = endPointsMask.copy()
rgbMask = cv2.cvtColor(rgbMask, cv2.COLOR_GRAY2BGR)
# Create a copy of the mask for points processing:
groupsMask = endPointsMask.copy()
# Set kernel (structuring element) size:
kernelSize = 3
# Set operation iterations:
opIterations = 3
# Get the structuring element:
maxKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
# Perform dilate:
groupsMask = cv2.morphologyEx(groupsMask, cv2.MORPH_DILATE, maxKernel, None, None, opIterations, cv2.BORDER_REFLECT101)
這是膨脹的結果。 我將此圖像稱為groupsMask
:
注意一些點現在是如何共享鄰接的。 我將使用這個掩碼作為生成最終質心的指南。 算法是這樣的:遍歷endPointsMask
,對於每個點,產生一個 label。 使用dictionary
,存儲 label 和共享 label 的所有質心 - 使用groupsMask
通過flood-filling
在不同點之間傳播 label。 在dictionary
中,我們將存儲質心簇 label、質心總和的累積以及累積多少質心的計數,因此我們可以生成最終平均值。 像這樣:
# Set the centroids Dictionary:
centroidsDictionary = {}
# Get centroids on the end points mask:
totalComponents, output, stats, centroids = cv2.connectedComponentsWithStats(endPointsMask, connectivity=8)
# Count the blob labels with this:
labelCounter = 1
# Loop through the centroids, skipping the background (0):
for c in range(1, len(centroids), 1):
# Get the current centroids:
cx = int(centroids[c][0])
cy = int(centroids[c][1])
# Get the pixel value on the groups mask:
pixelValue = groupsMask[cy, cx]
# If new value (255) there's no entry in the dictionary
# Process a new key and value:
if pixelValue == 255:
# New key and values-> Centroid and Point Count:
centroidsDictionary[labelCounter] = (cx, cy, 1)
# Flood fill at centroid:
cv2.floodFill(groupsMask, mask=None, seedPoint=(cx, cy), newVal=labelCounter)
labelCounter += 1
# Else, the label already exists and we must accumulate the
# centroid and its count:
else:
# Get Value:
(accumCx, accumCy, blobCount) = centroidsDictionary[pixelValue]
# Accumulate value:
accumCx = accumCx + cx
accumCy = accumCy + cy
blobCount += 1
# Update dictionary entry:
centroidsDictionary[pixelValue] = (accumCx, accumCy, blobCount)
這是該過程的一些動畫,首先,質心被一個一個地處理。 我們正在嘗試加入那些似乎彼此接近的點:
組掩碼被新標簽淹沒。 將共享 label 的點加在一起以產生最終平均點。 有點難以看到,因為我的 label 從1
開始,但您幾乎看不到正在填充的標簽:
現在,剩下的就是產生最后的點了。 遍歷字典並檢查質心及其計數。 如果計數大於1
,則質心表示累積,並且必須按其計數進行潛水以產生最終點:
# Loop trough the dictionary and get the final centroid values:
for k in centroidsDictionary:
# Get the value of the current key:
(cx, cy, count) = centroidsDictionary[k]
# Process combined points:
if count != 1:
cx = int(cx/count)
cy = int(cy/count)
# Draw circle at the centroid
cv2.circle(resizedImage, (cx, cy), 5, (0, 0, 255), -1)
cv2.imshow("Final Centroids", resizedImage)
cv2.waitKey(0)
這是最終圖像,顯示了線條的終點/起點:
現在,端點檢測方法,或者更確切地說,卷積步驟,正在曲線上產生一個明顯的額外點,這可能是因為線上的一段與其鄰域分離得太遠 - 將曲線分成兩部分。 也許在卷積之前應用一點形態學可以解決這個問題。
我想建議一種更簡單、更有效的方法,更重要的是,它不會產生錯誤的端點:
這個想法很簡單,細化后, if neighbours count equals 1 --> the point is an end point
代碼是不言自明的:
def get_end_pnts(pnts, img):
extremes = []
for p in pnts:
x = p[0]
y = p[1]
n = 0
n += img[y - 1,x]
n += img[y - 1,x - 1]
n += img[y - 1,x + 1]
n += img[y,x - 1]
n += img[y,x + 1]
n += img[y + 1,x]
n += img[y + 1,x - 1]
n += img[y + 1,x + 1]
n /= 255
if n == 1:
extremes.append(p)
return extremes
主要的:
img = cv2.imread(p, cv2.IMREAD_GRAYSCALE)
img = cv2.threshold(img, 128, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)
img = cv2.ximgproc.thinning(img)
pnts = cv2.findNonZero(img)
pnts = np.squeeze(pnts)
ext = get_end_pnts(pnts, img)
for p in ext:
cv2.circle(img, (p[0], p[1]), 5, 128)
編輯:您可能有興趣訪問我對這個類似問題的回答。 它有一些額外的功能,它也檢測端點和連接器點。
我想提出另一種解決方案,涉及Hit-or-Miss Transform 。 此轉換有助於識別二進制圖像中的某些模式。 流行的腐蝕和膨脹操作構成了這種變換的基礎。 有關詳細信息、公式、插圖和代碼,請參閱此頁面
我對二值圖像執行的操作之一是骨架化,它將任何形狀的寬度降低到只有一個像素。 使用命中或未命中的變換查找端點非常容易,也就是說,如果您知道如何找到它們以及需要使用什么內核來發現它們。
變化:
這些端點可以位於八個不同的位置。 結束像素是下面每個 kernel 中心的像素:
- 可能發生的一種變化是當結束像素位於其他 2 個像素之間時,如下所示:
1 --> 前景像素(白色像素)
0 --> 背景像素(黑色像素)
|1 1 1|
|0 1 0|
|0 0 0|
|0 0 1|
|0 1 1|
|0 0 1|
|0 0 0|
|0 1 0|
|1 1 1|
|1 0 0|
|1 1 0|
|1 0 0|
- 另一種變化是當結束像素位於對角線的末端時; 像下面這樣:
|0 0 0|
|0 1 0|
|1 0 0|
|1 0 0|
|0 1 0|
|0 0 0|
|0 0 1|
|0 1 0|
|0 0 0|
|0 0 0|
|0 1 0|
|0 0 1|
代碼:
讀取圖像,將其反二值化並獲得其骨架:
img = cv2.imread(r'C:\Users\524316\Desktop\Stack\Code\Find_endpoints\lines.jpg')
img2 = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
th = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
skeleton = cv2.ximgproc.thinning(th, None, 1)
根據 OpenCV 中的文檔:
我們在下面從k1
到k4
構建了 4 個不同的變體 1 內核:
k1 = np.array(([ 0, 0, 0],
[-1, 1, -1],
[-1,-1, -1]), dtype="int")
k2 = np.array(([0, -1, -1],
[0, 1, -1],
[0, -1, -1]), dtype="int")
k3 = np.array(([-1, -1, 0],
[-1, 1, 0],
[-1, -1, 0]), dtype="int")
k4 = np.array(([-1, -1, -1],
[-1, 1, -1],
[ 0, 0, 0]), dtype="int")
使用上述所有內核執行命中或未命中變換並對圖像求和:
o1 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k1)
o2 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k2)
o3 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k3)
o4 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k4)
out1 = o1 + o2 + o3 + o4
我們在下面構建從k5
到k8
的變體 2 的下 4 個內核:
k5 = np.array(([-1, -1, -1],
[-1, 1, -1],
[ 0, -1, -1]), dtype="int")
k6 = np.array(([-1, -1, -1],
[-1, 1, -1],
[-1, -1, 0]), dtype="int")
k7 = np.array(([-1, -1, 0],
[-1, 1, -1],
[-1, -1, -1]), dtype="int")
k8 = np.array(([ 0, -1, -1],
[-1, 1, -1],
[-1, -1, -1]), dtype="int")
o5 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k5)
o6 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k6)
o7 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k7)
o8 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k8)
out2 = o5 + o6 + o7 + o8
添加兩個結果並繪制端點:
out = cv2.add(out1, out2)
pts = np.argwhere(out == 255)
for pt in pts:
img2 = cv2.circle(img2, (pt[1], pt[0]), 15, (0,255,0), -1)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.