簡體   English   中英

用 opencv 找到手繪線的終點

[英]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 對圖像進行卷積,它可以識別一條線的起點/終點。 這些是步驟:

  1. 稍微調整一下圖像的大小,因為它太大了。
  2. 將圖像轉換為灰度
  3. 獲取骨架
  4. 將骨架與端點kernel卷積
  5. 獲取端點坐標

現在,這將是所提出算法的第一次迭代。 但是,根據輸入圖像,可能存在重復的端點 - 彼此太近並且可以連接的單個點。 所以,讓我們結合一些額外的處理來擺脫這些重復的點。

  1. 識別可能的重復點
  2. 加入重復的點
  3. 計算最終點

這些最后的步驟太籠統了,當我們到達那一步時,讓我進一步詳細說明消除重復項背后的想法。 讓我們看看第一部分的代碼:

# 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操作,因此必須小心數據類型和轉換。 該過程的結果可以在這里觀察到:

這些是端點,但是請注意,如果它們太靠近,可以連接一些點。 現在是重復消除步驟。 讓我們首先定義檢查點是否重復的標准。 如果這些點太接近,我們將加入它們。 讓我們提出一種基於形態學的點接近度方法。 我將使用大小為33次迭代的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)

Output: 在此處輸入圖像描述

編輯:您可能有興趣訪問我對這個類似問題的回答。 它有一些額外的功能,它也檢測端點和連接器點。

我想提出另一種解決方案,涉及Hit-or-Miss Transform 此轉換有助於識別二進制圖像中的某些模式。 流行的腐蝕和膨脹操作構成了這種變換的基礎。 有關詳細信息、公式、插圖和代碼,請參閱此頁面

我對二值圖像執行的操作之一是骨架化,它將任何形狀的寬度降低到只有一個像素 使用命中或未命中的變換查找端點非常容易,也就是說,如果您知道如何找到它們以及需要使用什么內核來發現它們。

變化:

這些端點可以位於八個不同的位置。 結束像素是下面每個 kernel 中心的像素:

  1. 可能發生的一種變化是當結束像素位於其他 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|
  1. 另一種變化是當結束像素位於對角線的末端時; 像下面這樣:
|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 中的文檔

  • 1 -> 前景像素
  • -1 -> 背景像素
  • 0 -> 前景/背景像素

我們在下面從k1k4構建了 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

我們在下面構建從k5k8的變體 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM