Posted on

計算兩個點之間的直線距離

使用math.hypot

math.hypot 是 Python 內置的數學模塊 math 中的函數。它接受兩個參數,分別代表兩點的 x 和 y 坐標差值,然後返回它們的歐幾里德距離(即直線距離)。

import math

x1, y1 = 1, 2
x2, y2 = 3, 4

distance = math.hypot(x2 - x1, y2 - y1)
print(distance)

使用np.sqrt

np.sqrt 是 NumPy 庫中的函數,用於計算給定數值的平方根。要使用 np.sqrt 計算兩點之間的距離,你需要首先計算兩點在 x 和 y 坐標軸上的差值的平方和,然後將它們相加,再使用 np.sqrt 對結果進行平方根運算。

import numpy as np

x1, y1 = 1, 2
x2, y2 = 3, 4

distance = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
print(distance)

Posted on

對輪廓的點做旋轉計算

使用角度的正弦和餘弦函數,將長方形的寬度和高度乘以正確的係數,以獲得旋轉後的角點座標。

下面為一個將一個長方形的四個點作45度旋轉的簡單範例:

import cv2
import numpy as np
import math

base_x = 100
base_y = 100
width = 100
height = 50
angle = 45

# 計算長方形中心點座標
center_x = base_x + (width // 2)
center_y = base_y + (height // 2)

# 將角度轉換為弧度
angle_rad = math.radians(angle)

# 計算長方形四個角的相對座標
cos_val = math.cos(angle_rad)
sin_val = math.sin(angle_rad)
x = width / 2
y = height / 2

# 計算四個角點座標
point1 = (int(center_x - x * cos_val + y * sin_val), int(center_y - x * sin_val - y * cos_val))
point2 = (int(center_x + x * cos_val + y * sin_val), int(center_y + x * sin_val - y * cos_val))
point3 = (int(center_x + x * cos_val - y * sin_val), int(center_y + x * sin_val + y * cos_val))
point4 = (int(center_x - x * cos_val - y * sin_val), int(center_y - x * sin_val + y * cos_val))

print("Point 1:", point1)
print("Point 2:", point2)
print("Point 3:", point3)
print("Point 4:", point4)

# 創建空白影像
image = np.zeros((500, 500, 3), dtype=np.uint8)

# 轉換座標為Numpy陣列
pts = np.array([point1, point2, point3, point4], dtype=np.int32)

# 繪製多邊形
cv2.polylines(image, [pts], True, (0, 255, 0), thickness=2)

# 顯示結果
cv2.imshow('Rectangle', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

顯示結果如下

Posted on

點到多邊形的最短距離

cv2.pointPolygonTest是OpenCV中的一個函數,用於計算點到多邊形的最短距離或點是否在多邊形內。

函數的語法如下:

_, intersection = cv2.pointPolygonTest(rect3, tuple(line1_midpoint), measureDist=False)
  • contour:多邊形的輪廓,可以是Numpy陣列或OpenCV的輪廓物件。
  • point:要計算距離的點,通常是一個(x, y)座標元組。
  • measureDist:指定是否計算點到多邊形的最短距離。如果為True,則返回距離值;如果為False,則返回一個整數值表示點的位置關係:正數表示點在多邊形內部、負數表示點在多邊形外部、0表示點在多邊形邊界上。

相關函數請參考: cv2.distanceTransform

另外要畫出多邊形可使用cv2.polylines,如以下範例

import cv2
import numpy as np

# 定義長方形的四個角點
rect1 = np.array([[100, 100], [300, 100], [300, 200], [100, 200]])

# 創建空白影像
image = np.zeros((500, 500, 3), dtype=np.uint8)

# 繪製長方形
cv2.polylines(image, [rect1], True, (0, 255, 0), thickness=2)

# 顯示結果
cv2.imshow('Image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

Posted on

使用OpenCV將圖形轉正

旋轉圖片的方法

若是單純只是要把圖片做角度的旋轉,可以直接使用OpenCV 的 cv2.rotate() 函数。可按指定的方向旋轉圖像。如下:

import cv2

# 讀取圖像
image = cv2.imread('your_image.jpg')

# 將圖像旋轉90度
rotated_image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)

# 顯示旋轉後的圖像
cv2.imshow('Rotated Image', rotated_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

翻轉圖片的方法

cv2.flip() 是 OpenCV 中用於圖像翻轉的函數。它可以在水平、垂直或兩個方向上翻轉圖像。該函數接受三個參數:輸入圖像、翻轉的模式和輸出圖像的可選參數。

dst = cv2.flip(src, flipCode[, dst])

flipCode:翻轉的模式。可以是以下值之一:

  • 0:水平翻轉(沿著垂直軸翻轉)。
  • 1:垂直翻轉(沿著水平軸翻轉)。
  • -1:同時在水平和垂直方向上翻轉。

cv2.flip() 函數和 cv2.rotate() 函數都可以用於實現圖像的旋轉和翻轉,但它們的效果是不同的。

cv2.flip() 函數可以在水平和垂直方向上翻轉圖像,包括水平翻轉、垂直翻轉和同時在水平和垂直方向上翻轉。例如,使用 cv2.flip(image, -1) 可以同時在水平和垂直方向上翻轉圖像。

cv2.rotate() 函數用於對圖像進行旋轉。通過指定旋轉的角度和旋轉中心點,可以實現不同角度的旋轉。例如,使用 cv2.rotate(image, cv2.ROTATE_180_CLOCKWISE) 可以將圖像順時針旋轉180度。

雖然cv2.flip(image, -1)cv2.rotate(image, cv2.ROTATE_180_CLOCKWISE) 可以實現類似的效果,將圖像翻轉或旋轉180度,但它們的內部操作是不同的。 cv2.flip() 是基於軸對稱翻轉實現的,而 cv2.rotate() 是基於旋轉變換實現的。

針對形狀做角度校正

在許多圖像偵測的狀況,我們仍然會需要針對物件去做旋轉,首先我們一定是先用cv2.findContours取得輪廓,然後取得該物件輪廓的角度。這邊很重要的,就是要取得物件輪廓的角度,要取得角度,首先就要先去做輪廓擬合(請參考: OpenCV裡面形狀擬合的幾種方法)。

這邊我大推使用橢圓去做輪廓擬合並且取得軸心的角度,為什麼呢? 雖然cv2.minAreaRect() 可計算最小擬合矩形,但是這個矩形會非常容易受到輪廓的些微影響而改變擬合的方式,例如以下圖為例,就有可能有黑框、紅色框兩種的最小擬合矩形(會視當下輪廓取得的細微變化而改變)。也因此所取得的角度會非常多變,後續的辨識也會更困難

但是使用最小擬合橢圓,對於像上面這種左右、上下為對稱,但是長寬不同的形狀來說,非常適合使用最小擬合橢圓cv2.fitEllipse(),使用範例如下

import cv2

image = cv2.imread('./333_2023-06-08_19-57-30.jpg')
canny = cv2.Canny(image , 50, 250)
cnts, hier = cv2.findContours(canny , cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 執行最小橢圓擬合
ellipse = cv2.fitEllipse(cnts[0])
(center, axes, angle) = ellipse
cv2.ellipse(image, ellipse, (0, 255, 0), 2)
# 顯示結果
cv2.imshow('image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

上面可以看到(center, axes, angle) = ellipse這邊的angle就是所偵測到的輪廓的角度,接著可以用cv2.warpAffine方法將圖像轉正

def rotatedDice(image, cnt):
    # 取得最小擬合橢圓並對圖像做翻轉
    ellipse = cv2.fitEllipse(cnt)
    (center, axes, angle) = ellipse
    angle = angle + 90
    rotation_matrix = cv2.getRotationMatrix2D(tuple(center), angle, 1)
    image = cv2.warpAffine(image, rotation_matrix,(image.shape[1], image.shape[0]))
    # 計算裁切位置
    mark = np.zeros_like(image)
    cv2.drawContours(mark, [cnt], 0, (255, 255, 255), -1)
    mark = cv2.warpAffine(mark, rotation_matrix,(mark.shape[1], mark.shape[0]))
    mark = cv2.cvtColor(mark, cv2.COLOR_RGB2GRAY)
    cnts, hier = cv2.findContours(mark, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    x, y, w, h = cv2.boundingRect(cnts[0])
    matting_result = image[y:y+h,x:x+w,:]
    return matting_result

從上面,我們使用cv2.warpAffine來做圖片角度的校正,warpAffine裡面有要輸入一個旋轉的矩陣的參數,在上面的範例,我們使用cv2.getRotationMatrix2D,這個參數是單純做形狀旋轉,但是在真實的世界當中,大部分3D的角度轉換也會帶有著深度的轉換,如下圖

這時候就會需要使用cv2.getAffineTransform來取得這個旋轉矩陣

import cv2
import numpy as np

# 定義三個點的坐標
point1 = (106, 92)
point2 = (28, 91)
point3 = (154, 33)

# 定義旋轉角度
rotation_angle = -45

# 創建一個空白圖像
image = np.zeros((500, 500), dtype=np.uint8)

# 在圖像上繪製三角形
cv2.drawContours(image, [np.array([point1, point2, point3])], 0, (255), thickness=2)

# 計算旋轉中心
center = np.mean([point1, point2, point3], axis=0)

# 構建旋轉矩陣
rotation_matrix = cv2.getRotationMatrix2D(tuple(center), rotation_angle, 1)

# 對整個圖像進行旋轉
rotated_image = cv2.warpAffine(image, rotation_matrix, (image.shape[1], image.shape[0]))

# 顯示旋轉後的圖像
cv2.imshow("Rotated Image", rotated_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Posted on

OpenCV魔術棒填充顏色

函數介紹

cv2.floodFill() 函數可以用來對圖像進行泛洪填充。泛洪填充是指將圖像中指定的像素點及其相連的像素點填充成指定的顏色。它通常用於圖像的背景去除、圖像分割等應用中。常用的場景如下:

  1. 圖像分割:可以使用泛洪填充來將圖像分割成不同的區域,例如可以從圖像中自動分離出前景和背景。
  2. 圖像去噪:可以使用泛洪填充來去除圖像中的噪聲,例如在二值化圖像中可以填充噪點附近的像素,使其與周圍的像素保持一致。
  3. 圖像修復:可以使用泛洪填充來修復圖像中的缺陷,例如在圖像中填充缺陷周圍的像素,使其與周圍的像素保持一致。
  4. 圖像標記:可以使用泛洪填充來對圖像進行標記,例如對圖像中的區域進行標記,或者在圖像中添加文字等。

總之,floodFill是一種非常實用的圖像處理技術,可以在很多場合下使用,並且可以通過調整填充的參數來達到不同的效果。

參數介紹

cv2.floodFill() 函數的常用參數如下:

cv2.floodFill(image, mask, seedPoint, newVal[, rect[, loDiff[, upDiff[, flags]]]]) -> retval, image, mask, rect
  • image:要填充的圖像,必須為8位、單通道或三通道影像。如果是三通道影像,則只有當 flags 參數中包含 cv2.FLOODFILL_FIXED_RANGE 時,填充才會基於每個像素的三通道值。
  • mask:用於指定填充區域的填充標記,必須為單通道、8位或32位浮點數影像,大小應比 image 多2個像素。如果填充標記中對應位置的值為0,則該像素將不會被填充。如果該參數為 None,則會自動創建一個和 image 大小相同的標記。
  • seedPoint:種子點的位置,是一個二元數組 (x, y)
  • newVal:填充的新值,可以是一個標量或一個三元數組 (B, G, R)
  • rect:可選的輸出參數,用於返回填充區域的最小矩形。
  • loDiff:可選的最小差值,如果當前像素和種子點之間的差值小於 loDiff,則這個像素將被填充。默認值為0。
  • upDiff:可選的最大差值,如果當前像素和種子點之間的差值大於 upDiff,則這個像素不會被填充。默認值為0。
  • flags:可選的填充標誌,可以是以下幾種取值之一或者它們的組合:
    • cv2.FLOODFILL_FIXED_RANGE:基於每個像素的三通道值來填充,默認基於灰度值。
      • cv2.FLOODFILL_MASK_ONLY:僅修改填充標記,不修改圖像。
        • cv2.FLOODFILL_MULTISCALE:使用多個尺度進行填充。
          • cv2.FLOODFILL_POINT:表示 seedPoint 參數為像素的坐標,而不是像素值。

使用範例

import cv2
import numpy as np

# 讀入圖像,轉為灰度
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 找到種子點
seed_point = (100, 100)

# 設置填充顏色和填充標記
fill_color = (0, 0, 255)
fill_mask = np.zeros((gray.shape[0]+2, gray.shape[1]+2), dtype=np.uint8)

# 泛洪填充
cv2.floodFill(img, fill_mask, seed_point, fill_color)

# 顯示圖像
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Posted on

OpenCV裡面形狀擬合的幾種方法

取得輪廓的矩形邊界框

cv2.boundingRect() 函數可以用來計算一個輪廓的矩形邊界框(bounding box),即最小矩形框,這個矩形框可以完全包圍輪廓的所有點。這個函數的返回值是一個元組 (x,y,w,h),其中 (x,y) 是矩形框左上角的座標,wh 是矩形框的寬度和高度。

下面是一個使用 cv2.boundingRect() 函數找到最小矩形框的範例程式碼:

import cv2

# 讀入圖像,轉為灰度
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化,尋找輪廓
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 畫出輪廓
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 尋找最小矩形框
x, y, w, h = cv2.boundingRect(contours[0])

# 畫出矩形框
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2)

# 顯示圖像
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

最小擬合矩形

cv2.minAreaRect() 可計算最小擬合矩形,這個函數會將給定的輪廓點集擬合成一個矩形,這個矩形具有最小面積,可以包圍住所有的輪廓點。

下面是一個使用 cv2.minAreaRect() 函數找到最小擬合矩形的範例程式碼:

import cv2

# 讀入圖像,轉為灰度
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化,尋找輪廓
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 畫出輪廓
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 尋找最小擬合矩形
rect = cv2.minAreaRect(contours[0])
box = cv2.boxPoints(rect)
box = np.int0(box)

# 畫出矩形
cv2.drawContours(img, [box], 0, (0, 0, 255), 2)

# 顯示圖像
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

最小擬合矩形所得到的結果rect其實是會有三個值,包括中心點座標、寬和高的數組、矩形的角度,我們可以用下面的程式產生自己定義的rect(因為rect本身無法修改,要修改就要自己建一個)

# 定義旋轉矩形的中心點、寬度、高度和角度
center = (250, 250)
width = 200
height = 100
angle = 45

# 計算旋轉矩形的四個角點
rect = ((center[0], center[1]), (width, height), angle)
box = cv2.boxPoints(rect)
box = np.int0(box)

取得最小包圍橢圓

若需要找到一個能夠包圍所有點的橢圓,可以使用 cv2.minEnclosingEllipse() 函數。這個函數會將給定的點集包圍在一個最小面積橢圓內。

下面是使用 cv2.minEnclosingEllipse() 函數找到最小包圍橢圓的範例程式碼:

import cv2

# 讀入圖像,轉為灰度
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化,尋找輪廓
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 畫出輪廓
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 尋找最小包圍橢圓
ellipse = cv2.fitEllipse(contours[0])
cv2.ellipse(img, ellipse, (0, 0, 255), 2)

# 尋找最小面積包圍橢圓
ellipse = cv2.minEnclosingEllipse(contours[0])
cv2.ellipse(img, (int(ellipse[0][0]), int(ellipse[0][1])),
            (int(ellipse[1][0] / 2), int(ellipse[1][1] / 2)),
            ellipse[2], 0, 360, (255, 0, 0), 2)

# 顯示圖像
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

最佳擬合橢圓

cv2.fitEllipse() 函數找到的是能夠最好擬合給定點集的橢圓,並不一定能夠包圍住所有點。

這個函數會將輸入的輪廓點集擬合成一個橢圓,返回橢圓的中心座標、軸長、旋轉角度等相關信息。

下面是一個簡單的範例程式碼,展示如何使用 cv2.fitEllipse() 找到最小包圍橢圓:

import cv2

# 讀入圖像,轉為灰度
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化,尋找輪廓
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 畫出輪廓
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 尋找最小包圍橢圓
ellipse = cv2.fitEllipse(contours[0])
cv2.ellipse(img, ellipse, (0, 0, 255), 2)

# 顯示圖像
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

最小包圍圓

要找到一個能夠最小外接圓包圍給定的點集,可以使用 cv2.minEnclosingCircle() 函數。這個函數會將給定的點集包圍在一個最小面積圓內。

下面是一個使用 cv2.minEnclosingCircle() 函數找到最小外接圓的範例程式碼:

import cv2
import numpy as np

# 讀入圖像,轉為灰度
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化,尋找輪廓
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 畫出輪廓
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 尋找最小外接圓
(x, y), radius = cv2.minEnclosingCircle(contours[0])
center = (int(x), int(y))
radius = int(radius)

# 畫出圓形
cv2.circle(img, center, radius, (0, 0, 255), 2)

# 顯示圖像
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

最適擬合直線

要找到一個能夠最好擬合給定點集的直線,可以使用 cv2.fitLine() 函數。這個函數會將給定的點集擬合成一條直線,返回的是一個向量 (vx,vy,x0,y0),其中 (vx,vy) 是直線的方向向量,(x0,y0) 是直線上的一點。

下面是一個使用 cv2.fitLine() 函數找到最適擬和直線的範例程式碼:

import cv2
import numpy as np

# 讀入圖像,轉為灰度
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值化,尋找輪廓
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 畫出輪廓
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 尋找最適擬和直線
rows, cols = img.shape[:2]
[vx, vy, x, y] = cv2.fitLine(contours[0], cv2.DIST_L2, 0, 0.01, 0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)

# 畫出直線
cv2.line(img, (cols-1, righty), (0, lefty), (0, 0, 255), 2)

# 顯示圖像
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Posted on

用OPENCV繪製垂直線

繪製出穿過這兩個點(10,20)和(50,30)的中心點,並與和這兩點相連的直線垂直的線。

import cv2
import numpy as np

# 創建一個空的圖像,並設置其大小為 100x100 像素,並設置其通道數為 3 (RGB)
img = np.zeros((100, 100, 3), np.uint8)

# 定義點的座標
point1 = (10, 20)
point2 = (50, 30)

# 計算兩個點的中心點座標
center_point = ((point1[0] + point2[0]) // 2, (point1[1] + point2[1]) // 2)

# 繪製出兩個點以及中心點
cv2.circle(img, point1, 2, (0, 0, 255), -1)
cv2.circle(img, point2, 2, (0, 0, 255), -1)
cv2.circle(img, center_point, 2, (0, 255, 0), -1)

# 計算與這兩點相連的直線垂直的線的端點座標
if point1[0] == point2[0]:
    # 如果這兩個點的 x 座標相等,則直接在中心點上下各畫一個點,這兩個點就是線的端點
    line_point1 = (center_point[0], 0)
    line_point2 = (center_point[0], 100)
else:
    # 否則,計算這兩個點之間的斜率
    k = (point2[1] - point1[1]) / (point2[0] - point1[0])
    # 計算垂直於這條線的斜率
    vk = -1 / k
    # 計算線的端點座標
    line_point1 = (center_point[0] - 50, int(center_point[1] - vk * 50))
    line_point2 = (center_point[0] + 50, int(center_point[1] + vk * 50))

# 繪製出垂直線
cv2.line(img, line_point1, line_point2, (255, 0, 0), 1)

# 顯示圖像
cv2.imshow("image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Posted on

取得輪廓的中心點

以下為範例程式:

import cv2

# 讀取圖像,並將其轉換為灰度圖像
img = cv2.imread("image.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 進行閾值處理,以便更好地找到輪廓
ret, thresh = cv2.threshold(gray, 127, 255, 0)

# 查找輪廓
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 繪製輪廓,以便在圖像上進行視覺化
cv2.drawContours(img, contours, -1, (0, 255, 0), 3)

# 計算輪廓的中心點
if len(contours) > 0:
    # 取最大的輪廓
    c = max(contours, key=cv2.contourArea)
    # 計算輪廓的矩
    M = cv2.moments(c)
    # 計算中心點座標
    center_x = int(M["m10"] / M["m00"])
    center_y = int(M["m01"] / M["m00"])
    # 繪製中心點,以便在圖像上進行視覺化
    cv2.circle(img, (center_x, center_y), 5, (255, 0, 0), -1)

# 顯示圖像
cv2.imshow("image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Posted on

如何分割黏在一起的撲克牌

範例說明

本文為參考下面的文章:
Image Segmentation with Distance Transform and Watershed Algorithm

這篇文章是OpenCV官方網站上的一篇教程,介紹了如何使用distanceTransform函數進行圖像分割。在這篇教程中,作者首先介紹了distanceTransform函數的基本概念和用法,然後通過一個實例演示了如何使用distanceTransform函數對圖像進行分割。

範例程式碼

以下為程式範例

from __future__ import print_function
import cv2 as cv
import numpy as np
import argparse
import random as rng
rng.seed(12345)
parser = argparse.ArgumentParser(description='Code for Image Segmentation with Distance Transform and Watershed Algorithm.\
    Sample code showing how to segment overlapping objects using Laplacian filtering, \
    in addition to Watershed and Distance Transformation')
parser.add_argument('--input', help='Path to input image.', default='cards.png')
args = parser.parse_args()
src = cv.imread(cv.samples.findFile(args.input))
if src is None:
    print('Could not open or find the image:', args.input)
    exit(0)
# Show source image
cv.imshow('Source Image', src)
src[np.all(src == 255, axis=2)] = 0
# Show output image
cv.imshow('Black Background Image', src)
kernel = np.array([[1, 1, 1], [1, -8, 1], [1, 1, 1]], dtype=np.float32)
# do the laplacian filtering as it is
# well, we need to convert everything in something more deeper then CV_8U
# because the kernel has some negative values,
# and we can expect in general to have a Laplacian image with negative values
# BUT a 8bits unsigned int (the one we are working with) can contain values from 0 to 255
# so the possible negative number will be truncated
imgLaplacian = cv.filter2D(src, cv.CV_32F, kernel)
sharp = np.float32(src)
imgResult = sharp - imgLaplacian
# convert back to 8bits gray scale
imgResult = np.clip(imgResult, 0, 255)
imgResult = imgResult.astype('uint8')
imgLaplacian = np.clip(imgLaplacian, 0, 255)
imgLaplacian = np.uint8(imgLaplacian)
#cv.imshow('Laplace Filtered Image', imgLaplacian)
cv.imshow('New Sharped Image', imgResult)
bw = cv.cvtColor(imgResult, cv.COLOR_BGR2GRAY)
_, bw = cv.threshold(bw, 40, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
cv.imshow('Binary Image', bw)
dist = cv.distanceTransform(bw, cv.DIST_L2, 3)
# Normalize the distance image for range = {0.0, 1.0}
# so we can visualize and threshold it
cv.normalize(dist, dist, 0, 1.0, cv.NORM_MINMAX)
cv.imshow('Distance Transform Image', dist)
_, dist = cv.threshold(dist, 0.4, 1.0, cv.THRESH_BINARY)
# Dilate a bit the dist image
kernel1 = np.ones((3,3), dtype=np.uint8)
dist = cv.dilate(dist, kernel1)
cv.imshow('Peaks', dist)
dist_8u = dist.astype('uint8')
# Find total markers
_, contours, _ = cv.findContours(dist_8u, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# Create the marker image for the watershed algorithm
markers = np.zeros(dist.shape, dtype=np.int32)
# Draw the foreground markers
for i in range(len(contours)):
    cv.drawContours(markers, contours, i, (i+1), -1)
# Draw the background marker
cv.circle(markers, (5,5), 3, (255,255,255), -1)
markers_8u = (markers * 10).astype('uint8')
cv.imshow('Markers', markers_8u)
cv.watershed(imgResult, markers)
#mark = np.zeros(markers.shape, dtype=np.uint8)
mark = markers.astype('uint8')
mark = cv.bitwise_not(mark)
# uncomment this if you want to see how the mark
# image looks like at that point
#cv.imshow('Markers_v2', mark)
# Generate random colors
colors = []
for contour in contours:
    colors.append((rng.randint(0,256), rng.randint(0,256), rng.randint(0,256)))
# Create the result image
dst = np.zeros((markers.shape[0], markers.shape[1], 3), dtype=np.uint8)
# Fill labeled objects with random colors
for i in range(markers.shape[0]):
    for j in range(markers.shape[1]):
        index = markers[i,j]
        if index > 0 and index <= len(contours):
            dst[i,j,:] = colors[index-1]
# Visualize the final image
cv.imshow('Final Result', dst)
cv.waitKey()

distanceTransform

distanceTransform函數是OpenCV中的一個函數,用於計算圖像中每個非零點到最近背景像素的距離。distanceTransform函數的第二個Mat矩陣參數dst保存了每個點與最近的零點的距離信息,圖像上越亮的點,代表了離零點的距離越遠。在這篇文章中,作者通過一個實例演示了如何使用distanceTransform函數對圖像進行分割。

在這個實例中,作者首先讀取了一張灰度圖像,然後使用threshold函數對圖像進行二值化處理。接著,作者使用distanceTransform函數計算了圖像中每個非零點到最近背景像素的距離,並將結果保存在了一個Mat矩陣中。最後,作者使用threshold函數對Mat矩陣進行二值化處理,得到了一張分割後的圖像。

需要注意的是,在使用distanceTransform函數時,需要先將圖像進行二值化處理。此外,在計算距離時,可以選擇歐氏距離、L1距離或L-infinity距離等不同的計算方式。

處理的過程圖片





其他參考資料

OpenCV C++/Obj-C: Advanced square detection

Posted on

拆分相黏的正方形

原始圖片

在寫純OpenCV的圖像辨識上,最困難的是當要找尋的目標的邊界因為模糊或相黏,而無法抓出正確的邊界的狀況
因為一般使用OpenCV做圖像辨識,我們都會需要先抓到例如說畫面中的某種色塊、再找尋某種符合某條件的形狀之類的
例如以修圖軟體而言,可能會需要先抓取膚色,然後轉換膚色的灰度階層取得面部的高低起伏,再根據灰度階層去抓取符合某種形狀的高低(如鼻子)或顏色差(如嘴巴、眼睛)

也因此,抓取正確的形狀在純粹的圖像辨識(沒有機器學習)的狀況下非常重要,而拆分相黏的形狀(如手放在臉前面),仍然要正確的辨識目標物件,也成了圖像辨識的一大挑戰
關於這一系列的其他文章,請見:

以下為這次我們要挑戰的目標,就是將這兩個黏在一起的正方形拆分為兩個正方形

解決問題的思考方向

首先我先參考官網的分水嶺演算法介紹:Image Segmentation with Watershed Algorithm
這邊的狀況和我們的需求很類似,都是將相黏的物件拆分開來

下面是在stackoverflow裡一位大大分享的他使用分水嶺演算法的範例程式:

from scipy.ndimage import label
import cv2
import numpy
def segment_on_dt(img):
    dt = cv2.distanceTransform(img, cv2.DIST_L2, 3) # L2 norm, 3x3 mask
    dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
    dt = cv2.threshold(dt, 100, 255, cv2.THRESH_BINARY)[1]
    lbl, ncc = label(dt)

    lbl[img == 0] = lbl.max() + 1
    lbl = lbl.astype(numpy.int32)
    cv2.watershed(cv2.cvtColor(img, cv2.COLOR_GRAY2BGR), lbl)
    lbl[lbl == -1] = 0

在這篇文章裡有幾個函數需要我們去理解:

cv.distanceTransform

OpenCV的distanceTransform是一個圖像處理功能,可以計算圖像中每個像素到最近的零值像素之間的歐幾里德距離。distanceTransform功能可以在圖像分割、形狀檢測、物體識別等應用中使用。
在OpenCV中,distanceTransform有三種不同的實現方式:cv2.DIST_L1、cv2.DIST_L2和cv2.DIST_C。cv2.DIST_L1使用曼哈頓距離,cv2.DIST_L2使用歐幾里德距離,而cv2.DIST_C使用切比雪夫距離。
使用distanceTransform功能需要先將圖像二值化,然後計算圖像中每個像素到最近的零值像素之間的距離。distanceTransform返回的結果是一個浮點型的圖像,每個像素值表示該像素到最近的零值像素之間的距離。

以下是distanceTransform的Python程式碼示例:

import cv2
import numpy as np

img = cv2.imread('image.jpg', 0)
ret,thresh = cv2.threshold(img,127,255,0)
dist_transform = cv2.distanceTransform(thresh,cv2.DIST_L2,5)

以下為對上圖做cv.distanceTransform的結果

cv.threshold

將距離變換圖像dt進行歸一化處理,將其轉換成0-255之間的整數型圖像,並使用threshold函數將其二值化,生成一個二值化圖像dt。這個二值化圖像dt中的像素值只有0和255兩種,用來表示圖像中物體和背景之間的分界線。
在這邊我使用了0.9*dist_transform.max()來做為閥值,確認兩個方形之間可以不相連

ret, sure_fg = cv.threshold(dist_transform,0.9*dist_transform.max(),255,0)

結果如下:

cv2.connectedComponents

在圖像處理和計算機視覺中,常常需要將圖像分割成多個不同的區域,然後對每個區域進行不同的分析和處理。圖像分割的第一步就是對圖像進行連通區域標記,將相連的像素點標記為同一個區域,以便後續處理。

程式碼使用OpenCV中的label函數對二值化圖像dt進行連通區域標記,生成一個標記圖像lbl和連通區域數量ncc。標記圖像lbl中的每個像素點都標記了其所屬的連通區域編號。

OpenCV中提供了幾個函數可以實現連通區域標記,其中最常用的是cv2.connectedComponents和cv2.connectedComponentsWithStats函數,這些函數會將每個連通區域分配一個唯一的標籤(編號),並返回每個區域的一些統計信息,如面積、重心等。

scipy.ndimage.labelcv2.connectedComponents都是對二值化圖像中的連通區域進行標記的函數,但在實現和用法上有所不同。

scipy.ndimage.label是Scipy中的一個函數,用於對二值化圖像進行連通區域標記,它的使用方式如下:

from scipy.ndimage import label
labels, num_features = label(binary_image)

其中,binary_image是二值化的圖像,labels是與原始圖像大小相同的數組,其中每個像素點都標記了其所屬的連通區域編號,num_features是圖像中連通區域的數量。

cv2.connectedComponents是OpenCV中的一個函數,用於對二值化圖像進行連通區域標記,它的使用方式如下:

num_labels, labels = cv2.connectedComponents(binary_image)

binary_image是二值化的圖像,num_labels是圖像中連通區域的數量,labels是與原始圖像大小相同的數組,其中每個像素點都標記了其所屬的連通區域編號。

兩個函數的返回值不同,scipy.ndimage.label返回的是每個像素點所屬的連通區域編號,而cv2.connectedComponents返回的是每個連通區域的編號。另外,cv2.connectedComponents還可以通過修改設置參數來指定標記的種類,例如指定為4或8連通等。

cv2.watershed

程式碼將標記圖像lbl轉換成整數型數組,並使用OpenCV中的watershed函數進行分水嶺分割,生成一個分割圖像lbl。分割圖像lbl中的像素點表示原始圖像中的每個像素點所屬的區域編號。

cv2.watershed() 是 OpenCV 中的一種分割算法,通常用於分割圖像中的目標物體。

在使用該函數時,需要先對輸入的圖像進行預處理,以便生成一組用於分割的初始標記。這通常可以通過在原始圖像中標記前景和背景像素來實現。然後,可以將這些標記傳遞給 cv2.watershed() 函數進行分割。該函數會根據標記和圖像的梯度信息來確定目標物體的邊界,將其分割為不同的區域。

函數的語法如下:

markers = cv2.watershed(img, markers)

img 是輸入圖像,markers 是與輸入圖像相同大小的標記矩陣。在函數執行完畢後,markers 矩陣中每個像素的值將被設置為其所屬的分割區域的標記值。
需要注意的是,cv2.watershed() 函數是一個原地操作,即它會修改傳遞給它的標記矩陣,而不是返回一個新的矩陣。因此,在調用該函數之前,最好複製一份原始標記矩陣以備份。
cv2.watershed() 函數是一種基於圖像分水嶺的分割算法,它可以對灰度圖像進行分割,將圖像中的前景和背景分開。該算法的分割結果是基於圖像梯度的變化來進行分割的,因此不能直接實現直線分割。

# 應用watershed算法進行圖像分割
    markers = cv2.watershed(cv2.cvtColor(img, cv2.COLOR_GRAY2BGR), markers)
    # 根據標記將圖像分成不同的部分
    img[markers == -1] = 0

結果如下:

完整用法範例

import cv2
import numpy as np

# 創建一個黑白圖像
img = np.zeros((500, 500), dtype=np.uint8)
cv2.rectangle(img, (100, 100), (200, 200), 255, -1)
cv2.rectangle(img, (150, 150), (250, 250), 255, -1)

# 求出距離變換圖像
dt = cv2.distanceTransform(img, cv2.DIST_L2, 3)
dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(np.uint8)
# 閾值分割得到前景和背景
ret, thresh = cv2.threshold(dt, 0.8*dt.max(),255,0)
thresh = cv2.dilate(thresh, None, iterations=4)
unknown = cv2.subtract(img,thresh)
# 得到標記圖像,每個區域用不同的正整數標記
ret, markers = cv2.connectedComponents(thresh)
markers = markers+1
markers[unknown==255] = 0
# 應用watershed算法進行圖像分割
markers = cv2.watershed(cv2.cvtColor(img, cv2.COLOR_GRAY2BGR), markers)
# 根據標記將圖像分成不同的部分
img[markers == -1] = 0

# 顯示結果
cv2.imshow("img", img)
cv2.waitKey(0)
cv2.destroyAllWindows()