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()

取得最小包圍橢圓

若需要找到一個能夠包圍所有點的橢圓,可以使用 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()
Posted on 1 Comment

如何檢測畫面中可能的正方形

最簡單-使用findContours

OpenCV 中有一個名為 findContours 的函數,可以用來查找圖像中的輪廓。一旦你找到了圖像中的輪廓,你可以使用 approxPolyDP 函數來近似地計算輪廓的形狀。如果你要查找正方形,你可以在這些形狀中尋找具有 4 個頂點的多邊形,這些多邊形應該有相近的邊長和角度。如果你要檢測的正方形不是水平的或垂直的,你可能需要使用角度信息來確定它的方向。

import cv2
# 读入图像
img = cv2.imread('square.jpg')

# 将图像转为灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 使用 Canny 边缘检测
edges = cv2.Canny(gray, 50, 150)

# 寻找轮廓
contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 遍历轮廓
for contour in contours:
    # 近似计算轮廓
    approx = cv2.approxPolyDP(contour, 0.01 * cv2.arcLength(contour, True), True)
    # 如果是 4 个顶点的多边形,且边长和角度相近,则认为是正方形
    if len(approx) == 4 and cv2.isContourConvex(app

當我們可以取得相黏物件邊緣時 – 分水嶺演算法

分水嶺演算法-偵測相連區域形狀

當形狀邊緣不清楚時-使用霍夫找線

使用霍夫變換檢測直線。因為正方形的四條邊是直線,因此可以通過檢測這四條直線來確定正方形。

具體來說,可以使用 OpenCV 中的 HoughLinesP 函數來檢測直線。該函數會返回一組檢測到的線段,這些線段可以是任意方向和長度的線段,因此我們需要對這些線段進行篩選,只保留長度、方向和相對位置都符合要求的線段。接著,我們可以將這些線段按照一定的規則組合成四條邊,從而確定正方形。

以下是一個示例代碼,演示如何使用霍夫變換檢測正方形:

import cv2
import numpy as np

# 读入图像
img = cv2.imread('square.jpg')

# 将图像转为灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 使用 Canny 边缘检测
edges = cv2.Canny(gray, 50, 150)

# 使用霍夫变换检测直线
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 100, minLineLength=100, maxLineGap=10)

# 筛选直线
filtered_lines = []
for line in lines:
    x1, y1, x2, y2 = line[0]
    length = np.sqrt((x2-x1)**2 + (y2-y1)**2)
    angle = np.arctan2(y2-y1, x2-x1)
    # 保留长度在一定范围内的直线
    if 100 < length < 200:
        filtered_lines.append((length, angle, line))

# 组合直线
sides = []
for i, (length1, angle1, line1) in enumerate(filtered_lines):
    for j, (length2, angle2, line2) in enumerate(filtered_lines[i+1:], i+1):
        angle_diff = np.abs(angle1 - angle2)
        # 保留角度相近的直线
        if angle_diff < np.pi/4:
            # 计算两条直线的中心点
            x1, y1, x2, y2 = line1
            cx1, cy1 = (x1+x2)/2, (y1+y2)/2
            x1, y1, x2, y2 = line2
            cx2, cy2 = (x1+x2)/2, (y1+y2)/2
            center_diff = np.sqrt((cx2-cx1)**2 + (cy2-cy1)**2)
            # 保留中心点距离相近的直线
            if center_diff < 20:
                # 将两条直线组合成一条边
                sides.append((length1+length2, angle1, angle2, line1, line2))

# 按照长度排序,取前四条直线作为正方形的四条边
sides = sorted(sides
取前四条直线作为正方形的四条边
sides = sorted(sides, reverse=True)[:4]

计算正方形的四个顶点
corners = []
for i, (length1, angle1, angle2, line1, line2) in enumerate(sides):
for j, (length3, angle3, angle4, line3, line4) in enumerate(sides[i+1:], i+1):
# 计算两条直线的交点
x1, y1, x2, y2 = line1
cx1, cy1 = (x1+x2)/2, (y1+y2)/2
x1, y1, x2, y2 = line3
cx2, cy2 = (x1+x2)/2, (y1+y2)/2
k1 = np.tan(angle1)
k2 = np.tan(angle3)
x = (cy1 - cy2 + k2cx2 - k1cx1) / (k2 - k1)
y = k1*(x - cx1) + cy1
corners.append((x, y))

绘制正方形
for i in range(4):
x1, y1 = corners[i]
x2, y2 = corners[(i+1)%4]
cv2.line(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 3)

显示结果
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

基於角點的角點檢測

哈里斯角點檢測器簡介

Harris Corner Detector 是一種角點檢測算子,常用於計算機視覺算法中以提取角點並推斷圖像的特徵。它由 Chris Harris 和 Mike Stephens 於 1988 年在 Moravec 的角檢測器改進後首次提出。Harris 角點檢測器相比之前的角點檢測器,直接考慮了角點分數的差異,直接參考方向,而不是每 45 度角使用 shifting patches,並被證明在區分邊緣和邊緣方面更準確。角落。從那以後,它被改進並被許多算法採用,為後續應用預處理圖像。

Posted on

使用OpenCV判別圖像清晰度

3種清晰度評價方法

  • Tenengrad梯度方法: Tenengrad梯度方法利用Sobel算子分別計算水平和垂直方向的梯度,同一場景下梯度值越高,圖像越清晰。以下是具體實現,這裡衡量的指標是經過Sobel算子處理後的圖像的平均灰度值,值越大,代表圖像越清晰。
  • Laplacian梯度方法: Laplacian()變換不需要區分圖像的x和y方向計算梯度,從上圖的2種kernel也可以看到其x和y方向是對稱的。
  • 方差方法: 方差是概率論中用來考察一組離散數據和其期望(即數據的均值)之間的離散(偏離)成都的度量方法。方差較大,表示這一組數據之間的偏差就較大,組內的數據有的較大,有的較小,分佈不均衡;方差較小,表示這一組數據之間的偏差較小,組內的數據之間分佈平均,大小相近。

圖像清晰度識別之Laplacian算子

Laplacce算子是一種各向同性算子,二階微分算子,在只關心邊緣的位置而不考慮其周圍的像素灰度差值時比較合適。Laplace算子對孤立像素的響應要比對邊緣或線的響應要更強烈,因此只適用於無噪聲圖像。存在噪聲情況下,使用Laplacian算子檢測邊緣之前需要先進行低通濾波。所以,通常的分割算法都是把Laplacian算子和平滑算子結合起來生成一個新的模板。

從模板形式容易看出,如果在圖像中一個較暗的區域中出現了一個亮點,那麼用拉普拉斯運算就會使這個亮點變得更亮。因為圖像中的邊緣就是那些灰度發生跳變的區域,所以拉普拉斯銳化模板在邊緣檢測中很有用。一般增強技術對於陡峭的邊緣和緩慢變化的邊緣很難確定其邊緣線的位置。但此算子卻可用二次微分正峰和負峰之間的過零點來確定,對孤立點或端點更為敏感,因此特別適用於以突出圖像中的孤立點、孤立線或線端點為目的的場合。

在圖像處理中,圖像的清晰程度可以被表示為圖像的邊緣和顏色變化的強度。圖像的清晰度越強,邊緣和顏色變化的強度就越高。因此,通過評估圖像的清晰度,可以檢測圖像是否模糊。

使用範例和結果

import numpy as np
import cv2
from os import listdir
import re
files = [f for f in listdir('./wrong2/') if re.match(r'.*\.jpg', f)]
for i in range(len(files)):
    image = cv2.imread("./wrong2/"+files[i])
    image = cv2.resize(image, (100, 120))
    image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    # Calculate the Laplacian of the image
    laplacian = cv2.Laplacian(image, cv2.CV_64F)
    score = np.var(laplacian)
    print(files[i], score)
cv2.waitKey(0)

下面這張圖片的分數為1099.5216466388888

而這張為2966.9266674375

所以可以知道,由於拉普拉斯是在求邊緣,而模糊偵測就會是一種比較級的狀況,也就是說,如果一個動態的影片,前一偵的邊緣多,後一偵突然變少,有可能就是因為正在移動而造成的模糊導致邊緣變少

Posted on

讓OpenCV支持GPU

OpenCV CUDA

https://opencv.org/platforms/cuda/

現代 GPU 加速器已經變得強大且功能強大,足以執行通用計算 (GPGPU)。這是一個發展非常迅速的領域,引起了開發計算密集型應用程序的科學家、研究人員和工程師的極大興趣。儘管在 GPU 上重新實現算法存在困難,但許多人這樣做是為了檢查它們的速度。為了支持這些努力,許多高級語言和工具已經可用,例如 CUDA、OpenCL、C++ AMP、調試器、分析器等。

計算機視覺的重要組成部分是圖像處理,這是圖形加速器最初設計的領域。其他部分也假定大規模並行計算並且通常自然映射到 GPU 架構。因此,實現所有這些優勢並在圖形處理器上加速 OpenCV 具有挑戰性,但非常有益。

目標

  • 在 GPU 上為開發者提供方便的計算機視覺框架,與當前 CPU 功能保持概念上的一致性。
  • 使用 GPU 實現最佳性能(針對現代架構調整的高效內核、優化的數據流,如異步執行、複製重疊、零複製)
  • 完整性(盡可能多地實施,即使加速不是很好;這樣可以完全在 GPU 上運行算法並節省應對開銷)

表現

使用範例

這兩個都是載入圖片的方法,上面的是載至CPU,而下方則是載至GPU
img_Mat = cv2.imread(‘test.jpg’) # 16-bit float, OpenCV GPU版本安裝教學

請見此篇: https://cloud.tencent.com/developer/article/1722771

所需套件如下:
1. nVidia 驅動程式和cuDNN
2. Cmake:cmake-3.20.0-rc3-windows-x86_64.msi(用來重新編譯支援GPU的OpenCV)
3. OpenCV 4.51:opencv-4.5.1.tar.gz
4. OpenCV_contrib 4.5.1:opencv_contrib-4.5.1.tar.gz

Posted on

使用GrabCut抓取前景

理論

GrabCut 算法由英國劍橋微軟研究院的 Carsten Rother、Vladimir Kolmogorov 和 Andrew Blake 設計。在他們的論文“GrabCut”:使用迭代圖切割的交互式前景提取中。需要一種算法來以最少的用戶交互進行前景提取,結果就是 GrabCut。

從用戶的角度來看它是如何工作的?最初用戶在前景區域周圍繪製一個矩形(前景區域應該完全在矩形內)。然後算法迭代地對其進行分段以獲得最佳結果。完畢。但在某些情況下,分割效果不佳,例如,它可能將一些前景區域標記為背景,反之亦然。在這種情況下,用戶需要進行精細的修飾。只需在有錯誤結果的圖像上畫一些筆劃即可。Strokes 基本上說 *“嘿,這個區域應該是前景,你將它標記為背景,在下一次迭代中更正它”* 或者它的反面是背景。然後在下一次迭代中,你會得到更好的結果。

請參見下圖。第一個球員和足球被包圍在一個藍色矩形中。然後進行一些帶有白色筆觸(表示前景)和黑色筆觸(表示背景)的最終潤色。我們得到了一個不錯的結果。

函數介紹

grabCut()

下面介紹幾個常用的參數:

  • img – 輸入圖像
  • mask – 這是一個蒙版圖像,我們在其中指定哪些區域是背景、前景或可能的背景/前景等。它由以下標誌完成,cv.GC_BGD、cv.GC_FGD、cv.GC_PR_BGD、cv.GC_PR_FGD,或者簡單地通過0,1,2,3 到圖像。
  • rect – 它是包含格式為 (x,y,w,h) 的前景對象的矩形的坐標
  • bdgModel、fgdModel – 這些是算法內部使用的數組。您只需創建兩個大小為 (1,65) 的 np.float64 類型零數組。
  • iterCount – 算法應該運行的迭代次數。
  • model- 它應該是cv.GC_INIT_WITH_RECT或cv.GC_INIT_WITH_MASK或組合決定我們是繪製矩形還是最終的修飾筆劃。

簡單範例

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('image.jpg')
mask = np.zeros(img.shape[:2],np.uint8)
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
rect = (50,50,450,290)
cv.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv.GC_INIT_WITH_RECT)
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
# newmask是我手動標註的mask圖片
newmask = cv.imread('newmask.png',0)
# 標記為白色的地方(當然是前景),更改 mask=1
# 標記為黑色的地方(確定背景),更改 mask=0
mask[newmask == 0] = 0
mask[newmask == 255] = 1
mask, bgdModel, fgdModel = cv.grabCut(img,mask,None,bgdModel,fgdModel,5,cv.GC_INIT_WITH_MASK)
mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask[:,:,np.newaxis]
cv.imshow("img",img)
plt.imshow(img),plt.colorbar(),plt.show()

結果