天天看點

【OpenCV】Chapter9.邊緣檢測與圖像分割

邊緣檢測

Roberts算子/Prewitt算子/Sobel算子/Laplacian算子

邊緣檢測的原理和matlab實作在我之前這兩篇博文中提到過,這裡不再贅述。

​​​【計算機視覺】基礎圖像知識點整理​​​​【計算機視覺】數字圖像處理基礎知識題​​ 此次來看OpenCV的實作方式。

OpenCV并沒有直接提供相應的函數接口,是以通過自定義卷積核可以實作各種邊緣檢測算子。

示例程式:

"""
邊緣檢測(Roberts算子, Prewitt算子, Sobel算子, Laplacian算子)
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np

img = cv2.imread("../img/lena.jpg", flags=0)

# 自定義卷積核
# Roberts 邊緣算子
kernel_Roberts_x = np.array([[1, 0], [0, -1]])
kernel_Roberts_y = np.array([[0, -1], [1, 0]])
# Prewitt 邊緣算子
kernel_Prewitt_x = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]])
kernel_Prewitt_y = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]])
# Sobel 邊緣算子
kernel_Sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
kernel_Sobel_y = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]])
# Laplacian 邊緣算子
kernel_Laplacian_K1 = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]])
kernel_Laplacian_K2 = np.array([[1, 1, 1], [1, -8, 1], [1, 1, 1]])

# 卷積運算
imgBlur = cv2.blur(img, (3, 3))  # Blur 平滑後再做 Laplacian 變換
imgLaplacian_K1 = cv2.filter2D(imgBlur, -1, kernel_Laplacian_K1)
imgLaplacian_K2 = cv2.filter2D(imgBlur, -1, kernel_Laplacian_K2)
imgRoberts_x = cv2.filter2D(img, -1, kernel_Roberts_x)
imgRoberts_y = cv2.filter2D(img, -1, kernel_Roberts_y)
imgRoberts = np.uint8(cv2.normalize(abs(imgRoberts_x) + abs(imgRoberts_y), None, 0, 255, cv2.NORM_MINMAX))
imgPrewitt_x = cv2.filter2D(img, -1, kernel_Prewitt_x)
imgPrewitt_y = cv2.filter2D(img, -1, kernel_Prewitt_y)
imgPrewitt = np.uint8(cv2.normalize(abs(imgPrewitt_x) + abs(imgPrewitt_y), None, 0, 255, cv2.NORM_MINMAX))
imgSobel_x = cv2.filter2D(img, -1, kernel_Sobel_x)
imgSobel_y = cv2.filter2D(img, -1, kernel_Sobel_y)
imgSobel = np.uint8(cv2.normalize(abs(imgSobel_x) + abs(imgSobel_y), None, 0, 255, cv2.NORM_MINMAX))

plt.figure(figsize=(12, 8))
plt.subplot(341), plt.title('Origin'), plt.imshow(img, cmap='gray'), plt.axis('off')
plt.subplot(345), plt.title('Laplacian_K1'), plt.imshow(imgLaplacian_K1, cmap='gray'), plt.axis('off')
plt.subplot(349), plt.title('Laplacian_K2'), plt.imshow(imgLaplacian_K2, cmap='gray'), plt.axis('off')
plt.subplot(342), plt.title('Roberts'), plt.imshow(imgRoberts, cmap='gray'), plt.axis('off')
plt.subplot(346), plt.title('Roberts_X'), plt.imshow(imgRoberts_x, cmap='gray'), plt.axis('off')
plt.subplot(3, 4, 10), plt.title('Roberts_Y'), plt.imshow(imgRoberts_y, cmap='gray'), plt.axis('off')
plt.subplot(343), plt.title('Prewitt'), plt.imshow(imgPrewitt, cmap='gray'), plt.axis('off')
plt.subplot(347), plt.title('Prewitt_X'), plt.imshow(imgPrewitt_x, cmap='gray'), plt.axis('off')
plt.subplot(3, 4, 11), plt.title('Prewitt_Y'), plt.imshow(imgPrewitt_y, cmap='gray'), plt.axis('off')
plt.subplot(344), plt.title('Sobel'), plt.imshow(imgSobel, cmap='gray'), plt.axis('off')
plt.subplot(348), plt.title('Sobel_X'), plt.imshow(imgSobel_x, cmap='gray'), plt.axis('off')
plt.subplot(3, 4, 12), plt.title('Sobel_Y'), plt.imshow(imgSobel_y, cmap='gray'), plt.axis('off')
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

LoG算子(Marr-Hildreth 算法)

Marr-Hildreth 算法是改進的邊緣檢測算子,是平滑算子與 Laplace 算子的結合,因而兼具平滑和二階微分的作用,其定位精度高,邊緣連續性好,計算速度快。

示例程式:

"""
LoG邊緣檢測算子
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np
from scipy import signal

img = cv2.imread("../img/lena.jpg", flags=0)


def ZeroDetect(img):  # 判斷零交叉點
    h, w = img.shape[0], img.shape[1]
    zeroCrossing = np.zeros_like(img, np.uint8)
    for x in range(0, w - 1):
        for y in range(0, h - 1):
            if img[y][x] < 0:
                if (img[y][x - 1] > 0) or (img[y][x + 1] > 0) \
                        or (img[y - 1][x] > 0) or (img[y + 1][x] > 0):
                    zeroCrossing[y][x] = 255
    return zeroCrossing

imgBlur = cv2.blur(img, (3, 3))  # Blur 平滑後再做 Laplacian 變換

# 近似的 Marr-Hildreth 卷積核 (5*5)
kernel_MH5 = np.array([
    [0, 0, -1, 0, 0],
    [0, -1, -2, -1, 0],
    [-1, -2, 16, -2, -1],
    [0, -1, -2, -1, 0],
    [0, 0, -1, 0, 0]])
imgMH5 = signal.convolve2d(imgBlur, kernel_MH5, boundary='symm', mode='same')  # 卷積計算
zeroMH5 = ZeroDetect(imgMH5)  # 判斷零交叉點

# 由 Gauss 标準差計算 Marr-Hildreth 卷積核
sigma = 3  # Gauss 标準差,輸入參數
size = int(2 * round(3 * sigma)) + 1  # 根據标準差确定視窗大小,3*sigma 占比 99.7%
print("sigma={:d}, size={}".format(sigma, size))
x, y = np.meshgrid(np.arange(-size / 2 + 1, size / 2 + 1), np.arange(-size / 2 + 1, size / 2 + 1))  # 生成網格
norm2 = np.power(x, 2) + np.power(y, 2)
sigma2, sigma4 = np.power(sigma, 2), np.power(sigma, 4)
kernelLoG = ((norm2 - (2.0 * sigma2)) / sigma4) * np.exp(- norm2 / (2.0 * sigma2))  # 計算 LoG 卷積核
# Marr-Hildreth 卷積運算
imgLoG = signal.convolve2d(imgBlur, kernelLoG, boundary='symm', mode='same')  # 卷積計算
# 判斷零交叉點
zeroCrossing = ZeroDetect(imgLoG)

plt.figure(figsize=(10, 7))
plt.subplot(221), plt.title("Marr-Hildreth (sigma=0.5)"), plt.imshow(imgMH5, cmap='gray'), plt.axis('off')
plt.subplot(222), plt.title("Marr-Hildreth (sigma=3)"), plt.imshow(imgLoG, cmap='gray'), plt.axis('off')
plt.subplot(223), plt.title("Zero crossing (size=5)"), plt.imshow(zeroMH5, cmap='gray'), plt.axis('off')
plt.subplot(224), plt.title("Zero crossing (size=19)"), plt.imshow(zeroCrossing, cmap='gray'), plt.axis('off')
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

DoG算子

DoG算子是對LoG算子的簡化。

示例程式:

"""
DoG邊緣檢測算子
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np
from scipy import signal

img = cv2.imread("../img/lena.jpg", flags=0)


# 高斯核低通濾波器,sigmaY 預設時 sigmaY=sigmaX
kSize = (5, 5)
imgGaussBlur1 = cv2.GaussianBlur(img, (5, 5), sigmaX=1.0)  # sigma=1.0
imgGaussBlur2 = cv2.GaussianBlur(img, (5, 5), sigmaX=2.0)  # sigma=2.0
imgGaussBlur3 = cv2.GaussianBlur(img, (5, 5), sigmaX=4.0)  # sigma=4.0
imgGaussBlur4 = cv2.GaussianBlur(img, (5, 5), sigmaX=16.0)  # sigma=16.0

# 高斯差分算子 (Difference of Gaussian)
imgDoG1 = imgGaussBlur2 - imgGaussBlur1  # sigma=1.0,2.0
imgDoG2 = imgGaussBlur3 - imgGaussBlur2  # sigma=2.0,4.0
imgDoG3 = imgGaussBlur4 - imgGaussBlur3  # sigma=4.0,16.0

plt.figure(figsize=(10, 6))
plt.subplot(231), plt.title("GaussBlur (sigma=2.0)"), plt.imshow(imgGaussBlur2, cmap='gray'), plt.axis('off')
plt.subplot(232), plt.title("GaussBlur (sigma=4.0)"), plt.imshow(imgGaussBlur3, cmap='gray'), plt.axis('off')
plt.subplot(233), plt.title("GaussBlur (sigma=16.)"), plt.imshow(imgGaussBlur4, cmap='gray'), plt.axis('off')
plt.subplot(234), plt.title("DoG (sigma=1.0,2.0)"), plt.imshow(imgDoG1, cmap='gray'), plt.axis('off')
plt.subplot(235), plt.title("DoG (sigma=2.0,4.0)"), plt.imshow(imgDoG2, cmap='gray'), plt.axis('off')
plt.subplot(236), plt.title("DoG (sigma=4.0,16.)"), plt.imshow(imgDoG3, cmap='gray'), plt.axis('off')
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

Canny算子

Canny算子執行的基本步驟為:

(1)使用高斯濾波對圖像進行平滑;

(2)用一階有限差分計算梯度幅值和方向;

(3)對梯度幅值進行非極大值抑制(NMS);

(4)用雙門檻值處理和連通性分析來檢測和連接配接邊緣

OpenCV提供了函數​

​cv.Canny​

​實作Canny邊緣檢測算子。

cv.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) → edges

參數說明:

  • image:輸入圖像,8-bit 灰階圖像,不适用彩色圖像
  • edges:輸出邊緣圖像,8-bit 單通道圖像,大小與輸入圖像相同
  • threshold1:第一門檻值 TL
  • threshold2:第二門檻值 TH
  • apertureSize:Sobel 卷積核的孔徑,可選項,預設值 3
  • L2gradient: 計算圖像梯度幅值 标志符,預設值為 True 表示 L2 法,False 表示 L1 法

示例程式:

"""
Canny邊緣檢測算子
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np

img = cv2.imread("../img/lena.jpg", flags=0)

# 高斯核低通濾波器,sigmaY 預設時 sigmaY=sigmaX
kSize = (5, 5)
imgGauss1 = cv2.GaussianBlur(img, kSize, sigmaX=1.0)  # sigma=1.0
imgGauss2 = cv2.GaussianBlur(img, kSize, sigmaX=10.0)  # sigma=2.0

# 高斯差分算子 (Difference of Gaussian)
imgDoG = imgGauss2 - imgGauss1  # sigma=1.0, 10.0

# Canny 邊緣檢測, kSize 為高斯核大小,t1,t2為門檻值大小
t1, t2 = 50, 150
imgCanny = cv2.Canny(imgGauss1, t1, t2)

plt.figure(figsize=(10, 6))
plt.subplot(131), plt.title("Origin"), plt.imshow(img, cmap='gray'), plt.axis('off')
plt.subplot(132), plt.title("DoG"), plt.imshow(imgDoG, cmap='gray'), plt.axis('off')
plt.subplot(133), plt.title("Canny"), plt.imshow(imgCanny, cmap='gray'), plt.axis('off')
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

圖像分割

區域生長

區域生長方法将具有相似性質的像素或子區域組合為更大區域。

區域增長方法的步驟:

(1)對圖像自上而下、從左向右掃描,找到第 1 個還沒有通路過的像素,将該像素作為種子 (x0, y0);

(2)以 (x0, y0) 為中心, 考慮其 4 鄰域或 8 鄰域像素 (x, y),如果其鄰域滿足生長準則 則将 (x, y) 與 (x0, y0) 合并到同一區域,同時将 (x, y) 壓入堆棧;

(3)從堆棧中取出一個像素,作為種子 (x0, y0) 繼續步驟(2);

(4)當堆棧為空時傳回步驟(1);

(5)重複步驟(1)-(4),直到圖像中的每個點都被通路過,算法結束

示例程式:

"""
圖像分割之區域生長
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np


def getGrayDiff(image, currentPoint, tmpPoint):  # 求兩個像素的距離
    return abs(int(image[currentPoint[0], currentPoint[1]]) - int(image[tmpPoint[0], tmpPoint[1]]))


# 區域生長算法
def regional_growth(img, seeds, thresh=5):
    height, weight = img.shape
    seedMark = np.zeros(img.shape)
    seedList = []
    for seed in seeds:
        if (0 < seed[0] < height and 0 < seed[1] < weight): seedList.append(seed)
    label = 1  # 種子位置标記
    connects = [(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)]  # 8 鄰接連通
    while (len(seedList) > 0):  # 如果清單裡還存在點
        currentPoint = seedList.pop(0)  # 将最前面的那個抛出
        seedMark[currentPoint[0], currentPoint[1]] = label  # 将對應位置的點标記為 1
        for i in range(8):  # 對這個點周圍的8個點一次進行相似性判斷
            tmpX = currentPoint[0] + connects[i][0]
            tmpY = currentPoint[1] + connects[i][1]
            if tmpX < 0 or tmpY < 0 or tmpX >= height or tmpY >= weight:  # 是否超出限定門檻值
                continue
            grayDiff = getGrayDiff(img, currentPoint, (tmpX, tmpY))  # 計算灰階差
            if grayDiff < thresh and seedMark[tmpX, tmpY] == 0:
                seedMark[tmpX, tmpY] = label
                seedList.append((tmpX, tmpY))
    return seedMark


img = cv2.imread("../img/lena.jpg", flags=0)
# histCV = cv2.calcHist([img], [0], None, [256], [0, 256])  # 灰階直方圖
# OTSU 全局門檻值處理
ret, imgOtsu = cv2.threshold(img, 127, 255, cv2.THRESH_OTSU)  # 門檻值分割, thresh=T
# 自适應局部門檻值處理
binaryMean = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 3)
# 區域生長圖像分割
# seeds = [(10, 10), (82, 150), (20, 300)]  # 直接給定 種子點
imgBlur = cv2.blur(img, (3, 3))  # cv2.blur 方法
_, imgTop = cv2.threshold(imgBlur, 250, 255, cv2.THRESH_BINARY)  # 高百分位門檻值産生種子區域
nseeds, labels, stats, centroids = cv2.connectedComponentsWithStats(imgTop)  # 過濾連通域,獲得質心點 (x,y)
seeds = centroids.astype(int)  # 獲得質心像素作為種子點
imgGrowth = regional_growth(img, seeds, 8)

plt.figure(figsize=(8, 6))
plt.subplot(221), plt.axis('off'), plt.title("Origin")
plt.imshow(img, 'gray')
plt.subplot(222), plt.axis('off'), plt.title("OTSU(T={})".format(ret))
plt.imshow(imgOtsu, 'gray')
plt.subplot(223), plt.axis('off'), plt.title("Adaptive threshold")
plt.imshow(binaryMean, 'gray')
plt.subplot(224), plt.axis('off'), plt.title("Region grow")
plt.imshow(255 - imgGrowth, 'gray')
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

區域分離

區域分離的判據是使用者選擇的謂詞邏輯Q,通常是目标區域特征一緻性的測度,例如灰階均值和方差。

分離過程先判斷目前區域是否滿足目标的特征測度,如果不滿足則将目前區域分離為多個子區域進行判斷;不斷重複判斷、分離,直到拆分到最小區域為止。

示例程式:

"""
圖像分割之區域分離
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np


def SplitMerge(src, dst, h, w, h0, w0, maxMean, minVar, cell=4):
    win = src[h0: h0 + h, w0: w0 + w]
    mean = np.mean(win)  # 視窗區域的均值
    var = np.std(win, ddof=1)  # 視窗區域的标準差,無偏樣本标準差

    if (mean < maxMean) and (var > minVar) and (h < 2 * cell) and (w < 2 * cell):
        # 該區域滿足謂詞邏輯條件,判為目标區域,設為白色
        dst[h0:h0 + h, w0:w0 + w] = 255  # 白色
        # print("h0={}, w0={}, h={}, w={}, mean={:.2f}, var={:.2f}".
        #       format(h0, w0, h, w, mean, var))
    else:  # 該區域不滿足謂詞邏輯條件
        if (h > cell) and (w > cell):  # 區域能否繼續分拆?繼續拆
            SplitMerge(src, dst, (h + 1) // 2, (w + 1) // 2, h0, w0, maxMean, minVar, cell)
            SplitMerge(src, dst, (h + 1) // 2, (w + 1) // 2, h0, w0 + (w + 1) // 2, maxMean, minVar, cell)
            SplitMerge(src, dst, (h + 1) // 2, (w + 1) // 2, h0 + (h + 1) // 2, w0, maxMean, minVar, cell)
            SplitMerge(src, dst, (h + 1) // 2, (w + 1) // 2, h0 + (h + 1) // 2, w0 + (w + 1) // 2, maxMean, minVar,
                       cell)
        # else:  # 不能再分拆,判為非目标區域,設為黑色
        #     src[h0:h0+h, w0:w0+w] = 0  # 黑色


img = cv2.imread("../img/lena.jpg", flags=0)
hImg, wImg = img.shape
mean = np.mean(img)  # 視窗區域的均值
var = np.std(img, ddof=1)  # 視窗區域的标準差,無偏樣本标準差
print("h={}, w={}, mean={:.2f}, var={:.2f}".format(hImg, wImg, mean, var))

maxMean = 80  # 均值上界
minVar = 10  # 标準差下界
src = img.copy()
dst1 = np.zeros_like(img)
dst2 = np.zeros_like(img)
dst3 = np.zeros_like(img)
SplitMerge(src, dst1, hImg, wImg, 0, 0, maxMean, minVar, cell=32)  # 最小分割區域 cell=32
SplitMerge(src, dst2, hImg, wImg, 0, 0, maxMean, minVar, cell=16)  # 最小分割區域 cell=16
SplitMerge(src, dst3, hImg, wImg, 0, 0, maxMean, minVar, cell=8)  # 最小分割區域 cell=8

plt.figure(figsize=(9, 7))
plt.subplot(221), plt.axis('off'), plt.title("Origin")
plt.imshow(img, 'gray')
plt.subplot(222), plt.axis('off'), plt.title("Region split (c=32)")
plt.imshow(dst1, 'gray')
plt.subplot(223), plt.axis('off'), plt.title("Region split (c=16)")
plt.imshow(dst2, 'gray')
plt.subplot(224), plt.axis('off'), plt.title("Region split (c=8)")
plt.imshow(dst3, 'gray')
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

K均值聚類

OpenCV 提供了函數​

​cv.kmeans​

​來實作 k-means 聚類算法。

cv.kmeans(data, K, bestLabels, criteria, attempts, flags[, centers]) → compactness, labels, centersdst

參數說明:

  • data:用于聚類的資料,N 維數組,類型為 CV_32F、CV_32FC2
  • K:設定的聚類數量
  • bestLabels:整數數組,分類标簽,每個樣本的所屬聚類的序号
  • criteria:元組 (type, max_iter, epsilon),算法結束标準,最大疊代次數或聚類中心位置精度
  • cv2.TERM_CRITERIA_EPS:如果達到指定的精度 epsilon,則停止算法疊代
  • cv2.TERM_CRITERIA_MAX_ITER:在指定的疊代次數max_iter之後停止算法
  • cv2.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER:當滿足上述任何條件時停止疊代
  • attempts:标志,指定使用不同聚類中心初值執行算法的次數
  • flags:像素鄰域的尺寸,用于計算鄰域的門檻值,通常取 3,5,7
  • cv2. KMEANS_RANDOM_CENTERS:随機産生聚類中心的初值
  • cv2. KMEANS_PP_CENTERS:Kmeans++ 中心初始化方法
  • cv2. KMEANS_USE_INITIAL_LABELS:第一次計算時使用使用者指定的聚類初值,之後的計算則使用随機的或半随機的聚類中心初值
  • centers:聚類中心數組,每個聚類中心為一行,可選項
  • labels:整數數組,分類标簽,每個樣本的所屬聚類的序号
  • centersdst:聚類中心數組

示例程式:

"""
圖像分割之k均值聚類
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np

img = cv2.imread("../img/img.jpg", flags=1)

dataPixel = np.float32(img.reshape((-1, 3)))
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, 0.1)  # 終止條件
flags = cv2.KMEANS_RANDOM_CENTERS  # 起始的中心選擇

K = 2  # 設定聚類數
_, labels, center = cv2.kmeans(dataPixel, K, None, criteria, 10, flags)
centerUint = np.uint8(center)
classify = centerUint[labels.flatten()]  # 将像素标記為聚類中心顔色
imgKmean3 = classify.reshape((img.shape))  # 恢複為二維圖像

K = 3  # 設定聚類數
_, labels, center = cv2.kmeans(dataPixel, K, None, criteria, 10, flags)
centerUint = np.uint8(center)
classify = centerUint[labels.flatten()]  # 将像素标記為聚類中心顔色
imgKmean4 = classify.reshape((img.shape))  # 恢複為二維圖像

K = 5  # 設定聚類數
_, labels, center = cv2.kmeans(dataPixel, K, None, criteria, 10, flags)
centerUint = np.uint8(center)
classify = centerUint[labels.flatten()]  # 将像素标記為聚類中心顔色
imgKmean5 = classify.reshape((img.shape))  # 恢複為二維圖像

plt.figure(figsize=(9, 7))
plt.subplot(221), plt.axis('off'), plt.title("Origin")
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))  # 顯示 img1(RGB)
plt.subplot(222), plt.axis('off'), plt.title("K-mean (k=2)")
plt.imshow(cv2.cvtColor(imgKmean3, cv2.COLOR_BGR2RGB))
plt.subplot(223), plt.axis('off'), plt.title("K-mean (k=3)")
plt.imshow(cv2.cvtColor(imgKmean4, cv2.COLOR_BGR2RGB))
plt.subplot(224), plt.axis('off'), plt.title("K-mean (k=5)")
plt.imshow(cv2.cvtColor(imgKmean5, cv2.COLOR_BGR2RGB))
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

尋找輪廓

OpenCV提供函數​

​cv.findContours()​

​​從二值圖像中尋找輪廓,函數​

​cv2.drawContours()​

​繪制輪廓。

cv.findContours(image, mode, method[, contours[, hierarchy[, offset]]]) → contours, hierarchy

參數說明:

  • image:原始圖像,8 位單通道二值圖像
  • mode: 輪廓檢索模式
  • cv.RETR_EXTERNAL:隻檢索最外層輪廓
  • cv.RETR_LIST:檢索所有輪廓,不建立任何層次關系
  • cv.RETR_CCOMP:檢索所有輪廓,并将其組織為兩層, 頂層是各部分的外部輪廓,次層是内層輪廓
  • cv.RETR_TREE:檢索所有輪廓,并重建嵌套輪廓的完整層次結構
  • cv.RETR_FLOODFILL:漫水填充法(泛洪填充)
  • method: 輪廓近似方法
  • cv.CHAIN_APPROX_NONE:輸出輪廓的每個像素點
  • cv.CHAIN_APPROX_SIMPLE:壓縮水準、垂直和斜線,僅保留這些線段的端點
  • cv.CHAIN_APPROX_TC89_L1:應用 Teh-Chin 鍊近似算法 L1
  • cv.CHAIN_APPROX_TC89_KCOS:應用 Teh-Chin 鍊近似算法 KCOS
  • contours:檢測到的所有輪廓,清單格式,每個輪廓存儲為包含邊界點坐标 (x,y) 的點向量
  • 清單(LIST)長度為 L,對應于找到的 L 個輪廓,按 0,…L-1 順序排列
  • 清單中的第 i 個元素是一個形如 (k,1,2) 的 Numpy 數組,表示第 i 個輪廓,k 是第 i 個輪廓的邊界點的數量
  • 數組 contours[i] 是構成第 i 個輪廓的各邊界點坐标 (x,y) 的點向量
  • 注意邊界點的坐标表達形式是 (x,y),而不是 OpenCV 中常用的像素坐标表達形式 (y,x)。
  • hierarchy:輪廓的層次結構和拓撲資訊,是一個形如 (1,k,4) 的 Numpy 數組
  • k 對應于找到的輪廓數量
  • hierarchy[0][i] 表示第 i 個輪廓的層次結構,是包含 4個值的數組 [Next, Previous, First Child, Parent],分别代表第 i 個輪廓的同層的後一個輪廓、同層的前一個輪廓、第一個子輪廓、父輪廓的編号
  • offset:每個輪廓點的偏移量

示例程式:

"""
圖像分割之繪制輪廓
"""
import cv2
import matplotlib.pyplot as plt
import numpy as np

img = cv2.imread("../img/img.jpg", flags=1)

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 灰階圖像
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)
plt.figure(figsize=(9, 6))
plt.subplot(131), plt.axis('off'), plt.title("Origin")
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.subplot(132), plt.axis('off'), plt.title("BinaryInv")
plt.imshow(binary, 'gray')

# 尋找二值化圖中的輪廓
binary, contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  # OpenCV3
# contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # OpenCV4~
# # 繪制輪廓
contourPic = img.copy()  # OpenCV3.2 之前的早期版本,查找輪廓函數會修改原始圖像
contourPic = cv2.drawContours(contourPic, contours, -1, (0, 0, 255), 2)  # OpenCV3
# contourPic = cv.drawContours(img, contours, -1, (0, 0, 255), thickness=cv.FILLED,maxLevel=1)

print("len(contours) = ", len(contours))  # 所有輪廓的清單
for i in range(len(contours)):
    print("i=", i, contours[i].shape)  # 第 i 個輪廓的邊界點
print("hierarchy.shape : ", hierarchy.shape)  # 層次結構
print(hierarchy)

plt.subplot(133), plt.axis('off'), plt.title("External contour")
plt.imshow(cv2.cvtColor(contourPic, cv2.COLOR_BGR2RGB))
plt.tight_layout()
plt.show()      
【OpenCV】Chapter9.邊緣檢測與圖像分割

趣味應用

下面展示兩個趣味應用,原理就不細述了,可以根據閱讀代碼了解。

GraphCuts圖割法

GraphCuts圖割法作者為​​youcans​​,利用OpenCV實作的互動性應用。通過用滑鼠左鍵标記前景,滑鼠右鍵标記背景,然後實作分割。

代碼:

'''
GraphCuts 互動式圖割分割算法

說明:
  (1) 用滑鼠左鍵标記前景,滑鼠右鍵标記背景;
  (2) 可以重複标記,不斷優化;
  (3) 按 Esc 鍵退出,完成分割。
'''

import cv2
import numpy as np
from matplotlib import pyplot as plt

drawing = False
mode = False


class GraphCutXupt:
    def __init__(self, t_img):
        self.img = t_img
        self.img_raw = img.copy()
        self.img_width = img.shape[0]
        self.img_height = img.shape[1]
        self.scale_size = 640 * self.img_width // self.img_height
        if self.img_width > 640:
            self.img = cv2.resize(self.img, (640, self.scale_size), interpolation=cv2.INTER_AREA)
        self.img_show = self.img.copy()
        self.img_gc = self.img.copy()
        self.img_gc = cv2.GaussianBlur(self.img_gc, (3, 3), 0)
        self.lb_up = False
        self.rb_up = False
        self.lb_down = False
        self.rb_down = False
        self.mask = np.full(self.img.shape[:2], 2, dtype=np.uint8)
        self.firt_choose = True


# 滑鼠的回調函數
def mouse_event2(event, x, y, flags, param):
    global drawing, last_point, start_point
    # 左鍵按下:開始畫圖
    if event == cv2.EVENT_LBUTTONDOWN:
        drawing = True
        last_point = (x, y)
        start_point = last_point
        param.lb_down = True
        print('mouse lb down')
    elif event == cv2.EVENT_RBUTTONDOWN:
        drawing = True
        last_point = (x, y)
        start_point = last_point
        param.rb_down = True
        print('mouse rb down')
    # 滑鼠移動,畫圖
    elif event == cv2.EVENT_MOUSEMOVE:
        if drawing:
            if param.lb_down:
                cv2.line(param.img_show, last_point, (x, y), (0, 0, 255), 2, -1)
                cv2.rectangle(param.mask, last_point, (x, y), 1, -1, 4)
            else:
                cv2.line(param.img_show, last_point, (x, y), (255, 0, 0), 2, -1)
                cv2.rectangle(param.mask, last_point, (x, y), 0, -1, 4)
            last_point = (x, y)
    # 左鍵釋放:結束畫圖
    elif event == cv2.EVENT_LBUTTONUP:
        drawing = False
        param.lb_up = True
        param.lb_down = False
        cv2.line(param.img_show, last_point, (x, y), (0, 0, 255), 2, -1)
        if param.firt_choose:
            param.firt_choose = False
        cv2.rectangle(param.mask, last_point, (x, y), 1, -1, 4)
        # print('mouse lb up')
    elif event == cv2.EVENT_RBUTTONUP:
        drawing = False
        param.rb_up = True
        param.rb_down = False
        cv2.line(param.img_show, last_point, (x, y), (255, 0, 0), 2, -1)
        if param.firt_choose:
            param.firt_choose = False
            param.mask = np.full(param.img.shape[:2], 3, dtype=np.uint8)
        cv2.rectangle(param.mask, last_point, (x, y), 0, -1, 4)
        # print('mouse rb up')


if __name__ == '__main__':
    img = cv2.imread("../img/img.jpg", flags=1)  # 讀取彩色圖像(Youcans)
    g_img = GraphCutXupt(img)

    cv2.namedWindow('image')
    # 定義滑鼠的回調函數
    cv2.setMouseCallback('image', mouse_event2, g_img)
    while (True):
        cv2.imshow('image', g_img.img_show)
        if g_img.lb_up or g_img.rb_up:
            g_img.lb_up = False
            g_img.rb_up = False
            bgdModel = np.zeros((1, 65), np.float64)
            fgdModel = np.zeros((1, 65), np.float64)
            rect = (1, 1, g_img.img.shape[1], g_img.img.shape[0])
            # print(g_img.mask)
            mask = g_img.mask
            g_img.img_gc = g_img.img.copy()
            cv2.grabCut(g_img.img_gc, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_MASK)
            mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')  # 0和2做背景
            g_img.img_gc = g_img.img_gc * mask2[:, :, np.newaxis]  # 使用蒙闆來擷取前景區域
            cv2.imshow('result', g_img.img_gc)
        # 按下ESC鍵退出
        if cv2.waitKey(20) == 27:
            break

    plt.figure(figsize=(10, 7))
    plt.subplot(221), plt.axis('off'), plt.title("xupt")
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))  # 顯示 img(RGB)
    plt.subplot(222), plt.axis('off'), plt.title("mask")
    plt.imshow(mask, 'gray')
    plt.subplot(223), plt.axis('off'), plt.title("mask2")
    plt.imshow(mask2, 'gray')
    plt.subplot(224), plt.axis('off'), plt.title("Grab Cut")
    plt.imshow(cv2.cvtColor(g_img.img_gc, cv2.COLOR_BGR2RGB))
    plt.tight_layout()
    plt.show()      

有點類似于剪輯軟體中的摳圖筆刷。

【OpenCV】Chapter9.邊緣檢測與圖像分割

舊影浮光——色彩保留濾鏡

該demo的作者是​​ZouKeh​​,通過一個互動性界面,可以在原圖灰階圖上繪制矩形,進而框選出有色彩的部分。

"""
Title:舊影浮光——基于Opencv的色彩保留濾鏡
Author:ZouKeh
Link:https://www.bilibili.com/video/BV1BG4y1r7WP
"""

import numpy as np
import cv2


def color_choose():
    # 選擇需要保留的顔色
    color = int(input("choose color(1.red 2.yellow 3.blue 4.green 5.white):"))
    lower = np.array([0, 0, 0])
    upper = np.array([0, 0, 0])
    if color == 1:
        lower = np.array([156, 60, 60])
        upper = np.array([180, 255, 255])
        # lower = np.array([0, 60, 60])
        # upper = np.array([10, 255, 255])
    elif color == 2:
        lower = np.array([26, 43, 46])
        upper = np.array([34, 255, 255])
    elif color == 3:
        lower = np.array([100, 43, 46])
        upper = np.array([124, 255, 255])
    elif color == 4:
        lower = np.array([35, 43, 46])
        upper = np.array([77, 255, 255])
    elif color == 5:
        lower = np.array([0, 0, 221])
        upper = np.array([180, 30, 255])
    return lower, upper, color


def choose_range(img):
    # 在圖檔上畫區域 選擇需要保留顔色的區域
    a = []
    b = []

    ##滑鼠事件 左鍵單擊
    def on_EVENT_LBUTTONDOWN(event, x, y, flags, param):
        global num  # 界面上點的個數 0開始 偶數為起點 奇數為終點
        # global begin_xy
        if event == cv2.EVENT_LBUTTONDOWN and num % 2 == 0:  # 畫選框的左上角 紅色
            xy = "%d,%d" % (x, y)
            a.append(x)
            b.append(y)
            cv2.circle(img, (x, y), 2, (0, 0, 255), thickness=-1)
            cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN,
                        1.0, (0, 0, 0), thickness=1)
            cv2.imshow("image", img)
            print(x, y)
            num += 1
            begin_xy = (x, y)
        elif event == cv2.EVENT_LBUTTONDOWN and num % 2 == 1:  # 畫選框的右下角 綠色
            xy = "%d,%d" % (x, y)
            a.append(x)
            b.append(y)
            cv2.circle(img, (x, y), 2, (0, 255, 0), thickness=-1)
            cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN,
                        1.0, (0, 0, 0), thickness=1)
            # cv2.arrowedLine(img, begin_xy, (x, y), (0, 0, 255), 2, 0, 0, 0.1)  # 畫完終點後畫箭頭
            cv2.imshow("image", img)
            print(x, y)
            num += 1

    cv2.namedWindow('image', cv2.WINDOW_NORMAL)
    cv2.setMouseCallback("image", on_EVENT_LBUTTONDOWN)

    while True:
        cv2.imshow('image', img)
        key = cv2.waitKey(1)
        if key == ord('q'):  # 在鍵盤上按Q鍵退出畫圖
            break
    if num % 2 == 1:  # 如果num為奇數說明有一個起點多餘了 去掉
        a = a[:-1]
        b = b[:-1]
    print(a, b)
    return a, b


# 将坐标點清單a,b 轉換為corner_list(坐标點必須為(x,y)形式)
def get_corner_list(a, b):
    corner_list = []
    for i in range(int(len(a) / 2)):
        corner_list.append([a[2 * i], b[2 * i], a[2 * i + 1], b[2 * i + 1]])
    # print(corner_list)
    return corner_list


# 将在選區外的掩膜去除
# 判斷點是否在選擇區域内
def in_box(i, j, corner_list):
    # if_inbox = False
    for k in corner_list:
        if i >= k[0] and i <= k[2] and j >= k[1] and j <= k[3]:
            return True
        else:
            continue
    return False


def cut(mask_r, corner_list):
    for i in range(mask_r.shape[0]):
        for j in range(mask_r.shape[1]):
            if mask_r[i, j] == 255 and not in_box(j, i, corner_list):
                mask_r[i, j] = 0
            else:
                continue
    return mask_r


# 主函數
def main(corner_list, img_path, lower, upper, color):
    # 轉為hsv顔色模式
    img = cv2.imread(img_path)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, lower, upper)
    if color == 1:
        lower_red2 = np.array([0, 60, 60])
        upper_red2 = np.array([10, 255, 255])  # thers is two ranges of red
        mask_2 = cv2.inRange(hsv, lower_red2, upper_red2)
        mask = mask + mask_2
    mask = cut(mask, corner_list)
    gray_image = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    gray = cv2.merge([gray_image, gray_image, gray_image])
    # 将mask于原視訊幀進行按位與操作,則會把mask中的白色用真實的圖像替換:
    res = cv2.bitwise_and(img, img, mask=mask)
    mask_bg = cv2.bitwise_not(mask)
    gray = cv2.bitwise_and(gray, gray, mask=mask_bg)
    result = res + gray
    cv2.namedWindow('Result', 0)
    cv2.imshow('Result', result)
    cv2.imwrite('result.jpg', result)
    cv2.waitKey(0)


if __name__ == '__main__':
    img_path = '../img/img.jpg'
    lower, upper, color = color_choose()
    img = cv2.imread(img_path)
    num = 0
    a, b = choose_range(img)
    cv2.destroyAllWindows()
    corner_list = get_corner_list(a, b)
    main(corner_list, img_path, lower, upper, color)      

繼續閱讀