opencv圖像處理常用操作一


讀取顯示圖像

# 讀取並顯示圖像
import cv2

path_to_image = r'pby.jpg'
"""
第二個參數
1 讀取彩色,默認
0 讀取灰度圖
-1 加載圖像,包括alpha通道
"""
original_image = cv2.imread(path_to_image, 1)
cv2.imshow('original image', original_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

cv2.namedWindow('image', cv2.WINDOW_NORMAL)
cv2.imshow('image', original_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

保存圖像

# 保存圖像
import cv2

img = cv2.imread('pby.jpg', 0)
cv2.imshow('image', img)
k = cv2.waitKey(0)
if k == 27:  # 等待ESC退出
    cv2.destroyAllWindows()
elif k == ord('s'):  # 等待關鍵字,保存和退出
    cv2.imwrite('gray.png', img)
    cv2.destroyAllWindows()

使用matplotlib

本文章不深入講解matplotlib的使用,后續更新matplotlib的詳細使用教程

"""
OpenCV加載的彩色圖像處於BGR模式。但是Matplotlib以RGB模式顯示。
"""
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('pby.jpg', 0)
plt.imshow(img, cmap='gray', interpolation='bicubic')
plt.xticks([]), plt.yticks([])  # 隱藏 x 軸和 y 軸上的刻度值
plt.show()

訪問攝像頭

import cv2 as cv

cap = cv.VideoCapture(0) # 參數可以是0-3,0表示默認攝像頭
print("width is %s" % cap.get(cv.CAP_PROP_FRAME_WIDTH))
print("height is %s" % cap.get(cv.CAP_PROP_FRAME_HEIGHT))
if cap.set(cv.CAP_PROP_FRAME_WIDTH, 320):
    print('width now sets to 320')
if cap.set(cv.CAP_PROP_FRAME_HEIGHT, 240):
    print('height now sets to 240')

if not cap.isOpened():
    print("Cannot open camera")
    exit()
while True:
    # 逐幀捕獲
    ret, frame = cap.read()
    # 如果正確讀取幀,ret為True
    if not ret:
        print("Can't receive frame (stream end?). Exiting ...")
        break
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    # 顯示結果幀
    cv.imshow('frame', gray)
    if cv.waitKey(1) == ord('q'):
        break
# 完成所有操作后,釋放捕獲器
cap.release()
cv.destroyAllWindows()

讀取視頻文件

import cv2 as cv

cap = cv.VideoCapture('output.avi') # 傳入視頻路徑
while cap.isOpened():
    ret, frame = cap.read()
    # 如果正確讀取幀,ret為True
    if not ret:
        print("Can't receive frame (stream end?). Exiting ...")
        break
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    cv.imshow('frame', gray)
    if cv.waitKey(25) == ord('q'):
        break
cap.release()
cv.destroyAllWindows()

寫視頻文件

import cv2 as cv

cap = cv.VideoCapture(0)
# if cap.set(cv.CAP_PROP_FRAME_WIDTH, 320):
#     print('width now sets to 320')
# if cap.set(cv.CAP_PROP_FRAME_HEIGHT, 240):
#     print('height now sets to 240')

fourcc = cv.VideoWriter_fourcc(*'XVID')  # 定義編解碼器並創建VideoWriter對象
out = cv.VideoWriter('output.avi', fourcc, 20.0, (640, 480))
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        print("Can't receive frame (stream end?). Exiting ...")
        break
    # gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    out.write(frame)
    cv.imshow('frame', frame)
    if cv.waitKey(1) == ord('q'):
        break
# 完成工作后釋放所有內容
cap.release()
out.release()
cv.destroyAllWindows()

在圖像上畫形狀、文字

import cv2 as cv
import numpy as np

# 創建黑色的圖像
img = np.zeros((512, 512, 3), np.uint8)
# 繪制一條厚度為5的藍色對角線
cv.line(img, (0, 0), (511, 511), (255, 0, 0), 5)

# 畫矩形
cv.rectangle(img, (384, 0), (510, 128), (0, 255, 0), 3)

# 畫圓圈
cv.circle(img, (447, 63), 63, (0, 0, 255), -1)

# 畫橢圓
cv.ellipse(img, (256, 256), (100, 50), 0, 0, 180, 255, -1)

# 畫多邊形
pts = np.array([[10, 5], [20, 30], [70, 20], [50, 10]], np.int32)
pts = pts.reshape((-1, 1, 2))

pts2 = np.array([[255, 240], [198, 189], [99, 345]], np.int32)
pts2 = pts2.reshape(-1, 1, 2)

cv.polylines(img, [pts], True, (0, 255, 255))  # 閉合多邊形
cv.polylines(img, [pts2], False, (0, 255, 255))  # 只連接不閉合

# 向圖片中添加文本
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img, 'OpenCV', (10, 500), font, 4, (255, 255, 255), 2, cv.LINE_AA)

cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()

用鼠標畫

import cv2 as cv
import numpy as np
import random


def main():
    # 鼠標回調函數
    def draw_circle(event, x, y, flags, param):
        if event == cv.EVENT_LBUTTONDBLCLK:
            cv.circle(img, (x, y), random.randint(50, 200), (255, 0, 0), -1)

    # 創建一個黑色的圖像,一個窗口,並綁定到窗口的功能
    img = np.zeros((512, 512, 3), np.uint8)
    cv.namedWindow('image')
    cv.setMouseCallback('image', draw_circle)
    while True:
        cv.imshow('image', img)
        if cv.waitKey(20) & 0xFF == 27:  # 按esc退出
            break
    cv.destroyAllWindows()


if __name__ == '__main__':
    main()

軌跡欄的使用

import cv2 as cv
import numpy as np


def nothing(x):
    pass


# 創建一個黑色的圖像
img = np.zeros((300, 512, 3), np.uint8)
cv.namedWindow('image')
# 創建顏色變化的軌跡欄
cv.createTrackbar('R', 'image', 0, 255, nothing)
cv.createTrackbar('G', 'image', 0, 255, nothing)
cv.createTrackbar('B', 'image', 0, 255, nothing)
# 為 ON/OFF 功能創建開關
switch = 'OFF/ON'
cv.createTrackbar(switch, 'image', 0, 1, nothing)
while True:
    cv.imshow('image', img)
    k = cv.waitKey(1) & 0xFF
    if k == 27:
        break
    # 得到四條軌跡的當前位置
    r = cv.getTrackbarPos('R', 'image')
    g = cv.getTrackbarPos('G', 'image')
    b = cv.getTrackbarPos('B', 'image')
    s = cv.getTrackbarPos(switch, 'image')
    if s == 0:
        img[:] = 0
    else:
        img[:] = [b, g, r]
cv.destroyAllWindows()

訪問像素點

# 訪問像素點
def access_px():
    img = cv.imread('pby.jpg')  # 加載彩色圖像,默認是彩色圖像
    px = img[100, 100]  # 通過行和列坐標來訪問像素值
    print(px)  # [37 61 73]
    # 僅訪問藍色像素
    blue = img[100, 100, 0]  # opencv讀取圖片是BGR格式
    print(blue)  # 37
    img[100, 100] = [255, 255, 255]  # 修改該坐標對應的像素值
    print(img[100, 100])  # [255 255 255]
    # 上面的方法通常用於選擇數組的區域,例如前5行和后3列。對於單個像素訪問,Numpy數組方法array.item()和array.itemset())被認為更好,但是它們始終返回標量。
    # 如果要訪問所有B,G,R值,則需要分別調用所有的array.item()
    # 訪問 RED 值
    print(img.item(10, 10, 2))  # 60
    # 修改 RED 值
    img.itemset((10, 10, 2), 100)
    print(img.item(10, 10, 2))  # 100

訪問圖像屬性

# 訪問圖像屬性
def access_properties():
    img = cv.imread('pby.jpg')
    print(img.shape)  # 訪問圖片的形狀,返回行,列,通道數(如果讀取的是彩色圖片)如果圖像是灰度的,則返回的元組僅包含行數和列數
    print(img.size)  # 獲取圖片像素總數
    print(img.dtype)  # 獲取圖像數據類型

裁剪感興趣區域

# 圖像感興趣區域
def roi():
    img = cv.imread('pby.jpg')
    ball = img[280:340, 330:390]
    cv.imshow('ball', ball)
    cv.waitKey(3000)
    cv.destroyAllWindows()

拆分合並通道

# 拆分/合並 通道
def split_merge():
    img = cv.imread('pby.jpg')
    b, g, r = cv.split(img)  # 拆分圖像通道,此操作比較耗時
    b2 = img[:, :, 0]  # 前面兩個切片划定區域,第三個數取0,1,2 指定通道
    cv.imshow('b2', b2)
    img[:, :, 2] = 0  # 指定紅色通道,並修改所有像素值為0
    cv.imshow('changed every red px into 0', img)
    # cv.imshow('b', b)
    # cv.imshow('g', g)
    # cv.imshow('r', r)
    img_merge = cv.merge((b, g, r))  # 合並圖像通道
    cv.imshow('merged image', img_merge)
    cv.waitKey(0)
    cv.destroyAllWindows()

添加邊框

def board():
    BLUE = [255, 0, 0]
    img1 = cv.imread('opencv-logo.png')
    replicate = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_REPLICATE)
    reflect = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_REFLECT)
    reflect101 = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_REFLECT_101)
    wrap = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_WRAP)
    constant = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_CONSTANT, value=BLUE)
    plt.subplot(231), plt.imshow(img1, 'gray'), plt.title('ORIGINAL')
    plt.subplot(232), plt.imshow(replicate, 'gray'), plt.title('REPLICATE')
    plt.subplot(233), plt.imshow(reflect, 'gray'), plt.title('REFLECT')
    plt.subplot(234), plt.imshow(reflect101, 'gray'), plt.title('REFLECT_101')
    plt.subplot(235), plt.imshow(wrap, 'gray'), plt.title('WRAP')
    # 圖像由matplotlib顯示 因此紅色和藍色通道將互換
    plt.subplot(236), plt.imshow(constant, 'gray'), plt.title('CONSTANT')
    plt.show()

執行結果:

圖像加法&圖像融合

def add_func():
    x = np.uint8([250])
    y = np.uint8([10])
    print(x)  # [250]
    print(y)  # [10]
    print(cv.add(x, y))  # [[255]] # 250+10 = 260 => 255 OpenCV加法是飽和運算
    print(x + y)  # [4] Numpy加法是模運算

# 圖像融合 也是圖像加法,但是對圖像賦予不同的權重,以使其具有融合或透明的感覺。
def addWeighted_func():
    img1 = cv.imread('1.jpg')
    img2 = cv.imread('2.jpg')
    cv.imshow('img1', img1)
    cv.imshow('img2', img2)
    dst = cv.addWeighted(img1, 0.5, img2, 0.5, 0)
    cv.imshow('dst', dst)
    cv.waitKey(0)
    cv.destroyAllWindows()   

addWeighted_func的執行結果:

位操作&掩碼

def bit_operation():
    img1 = cv.imread('1.jpg')
    img2 = cv.imread('opencv-logo.png')
    rows, cols, channels = img2.shape
    roi = img1[0:rows, 0:cols]
    # 現在創建logo的掩碼,並同時創建其相反掩碼
    img2gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)
    ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
    cv.imshow('img2gray', img2gray)
    cv.imshow('mask', mask)
    mask_inv = cv.bitwise_not(mask)
    cv.imshow('mask_inv', mask_inv)
    # 現在將ROI中logo的區域塗黑
    img1_bg = cv.bitwise_and(roi, roi, mask=mask_inv)
    # 僅從logo圖像中提取logo區域
    img2_fg = cv.bitwise_and(img2, img2, mask=mask)
    # 將logo放入ROI並修改主圖像
    dst = cv.add(img1_bg, img2_fg)
    img1[0:rows, 0:cols] = dst
    cv.imshow('res', img1)
    cv.waitKey(0)
    cv.destroyAllWindows()

執行結果:

以下圖像依次為

灰度圖

掩碼一

掩碼二

圖像加法

使用opencv衡量代碼性能

import cv2 as cv

# 使用opencv衡量代碼性能
img1 = cv.imread('pby.jpg')
e1 = cv.getTickCount()
for i in range(5, 49, 2):
    img1 = cv.medianBlur(img1, i)
e2 = cv.getTickCount()
t = (e2 - e1) / cv.getTickFrequency()
print(t)  # 3.6950288

改變顏色空間

OpenCV中有超過150種顏色空間轉換方法。但是我們將研究只有兩個最廣泛使用的,
BGR ↔ 灰色 和 BGR↔HSV。
對於顏色轉換,我們使用cv函數。cvtColor(input_image, flag),其中flag決定轉換的類型。
對於BGR→灰度轉換,我們使用標志cv.COLOR_BGR2GRAY。
類似地,對於BGR→HSV,我們使用標志cv.COLOR_BGR2HSV。
要獲取其他標記,只需在Python終端中運行以下命令

import cv2 as cv
# OpenCV中有超過150種顏色空間轉換方法。但是我們將研究只有兩個最廣泛使用的,
# BGR ↔ 灰色 和 BGR↔HSV。
# 對於顏色轉換,我們使用cv函數。cvtColor(input_image, flag),其中flag決定轉換的類型。
# 對於BGR→灰度轉換,我們使用標志cv.COLOR_BGR2GRAY。
# 類似地,對於BGR→HSV,我們使用標志cv.COLOR_BGR2HSV。
# 要獲取其他標記,只需在Python終端中運行以下命令
flags = [i for i in dir(cv) if i.startswith('COLOR_')]
print(flags)  # ['COLOR_BAYER_BG2BGR', 'COLOR_BAYER_BG2BGRA', ....,'COLOR_YUV420sp2RGBA', 'COLOR_mRGBA2RGBA']

HSV的色相范圍為[0,179],飽和度范圍為[0,255],值范圍為[0,255]。不同的軟件使用不同的規模。因此,如果你要將OpenCV值和它們比較,你需要將這些范圍標准化。

HSV顏色模型

HSV(Hue, Saturation, Value)是根據顏色的直觀特性由A. R. Smith在1978年創建的一種顏色空間, 也稱六角錐體模型(Hexcone Model)。、這個模型中顏色的參數分別是:色調(H),飽和度(S),亮度(V)。

色調H:用角度度量,取值范圍為0°~360°,從紅色開始按逆時針方向計算,紅色為0°,綠色為120°,藍色為240°。它們的補色是:黃色為60°,青色為180°,品紅為300°;

飽和度S:取值范圍為0.0~1.0;

亮度V:取值范圍為0.0(黑色)~1.0(白色)。

RGB和CMY顏色模型都是面向硬件的,而HSV(Hue Saturation Value)顏色模型是面向用戶的。

HSV模型的三維表示從RGB立方體演化而來。設想從RGB沿立方體對角線的白色頂點向黑色頂點觀察,就可以看到立方體的六邊形外形。六邊形邊界表示色彩,水平軸表示純度,明度沿垂直軸測量。

HSV顏色分量范圍

一般對顏色空間的圖像進行有效處理都是在HSV空間進行的,然后對於基本色中對應的HSV分量需要給定一個嚴格的范圍,下面是通過實驗計算的模糊范圍(准確的范圍在網上都沒有給出)。

H: 0— 180

S: 0— 255

V: 0— 255

此處把部分紅色歸為紫色范圍:

HSV六棱錐

H參數表示色彩信息,即所處的光譜顏色的位置。該參數用一角度量來表示,紅、綠、藍分別純度S為一比例值,范圍從0到1,它表示成所選顏色的純度和該顏色最大的純度之間的比率。S=0時,只有灰度。相隔120度。互補色分別相差180度。

V表示色彩的明亮程度,范圍從0到1。有一點要注意:它和光強度之間並沒有直接的聯系。

HSV對用戶來說是一種直觀的顏色模型。我們可以從一種純色彩開始,即指定色彩角H,並讓V=S=1,然后我們可以通過向其中加入黑色和白色來得到我們需要的顏色。增加黑色可以減小V而S不變,同樣增加白色可以減小S而V不變。例如,要得到深藍色,V=0.4 S=1 H=240度。要得到淡藍色,V=1 S=0.4 H=240度。

一般說來,人眼最大能區分128種不同的色彩,130種色飽和度,23種明暗度。如果我們用16Bit表示HSV的話,可以用7位存放H,4位存放S,5位存放V,即745或者655就可以滿足我們的需要了。由於HSV是一種比較直觀的顏色模型,所以在許多圖像編輯工具中應用比較廣泛,如Photoshop(在Photoshop中叫HSB)等等,但這也決定了它不適合使用在光照模型中,許多光線混合運算、光強運算等都無法直接使用HSV來實現。

追蹤指定顏色的物體

def track_color():
    cap = cv.VideoCapture(0)
    while True:
        # 讀取幀
        _, frame = cap.read()
        # 轉換顏色空間 BGR 到 HSV
        hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
        # 定義HSV中藍色的范圍
        lower_blue = np.array([110, 50, 50])
        upper_blue = np.array([130, 255, 255])
        # 設置HSV的閾值使得只取藍色
        mask = cv.inRange(hsv, lower_blue, upper_blue)
        # 將掩膜和圖像逐像素相加
        res = cv.bitwise_and(frame, frame, mask=mask)
        cv.imshow('frame', frame)
        cv.imshow('mask', mask)
        cv.imshow('res', res)
        k = cv.waitKey(5) & 0xFF
        if k == 27:
            break
    cv.destroyAllWindows()

執行結果如下:

可以看到還是有不少噪點的,這里是用電腦的前置攝像頭拍攝,手機純藍色背景,受光照影響,顯示效果不好

找到指定顏色的HSV值

def get_hsv_value():
    # 綠色的圖片
    green = np.uint8([[[0, 255, 0]]])
    # 獲取綠色的hsv值
    hsv_green = cv.cvtColor(green, cv.COLOR_BGR2HSV)
    print(hsv_green)  # [[[60 255 255]]]

現在把 [H- 10,100,100] 和 [H+ 10,255, 255] 分別作為下界和上界即可追蹤指定顏色了

圖像幾何變換

1.縮放

# 縮放圖片
def resize():
    img = cv.imread('pby.jpg')
    res1 = cv.resize(img, None, fx=0.25, fy=0.25, interpolation=cv.INTER_CUBIC)
    cv.imshow('res1', res1)
    # 或者
    height, width = img.shape[:2]
    res2 = cv.resize(img, (int(0.25 * width), int(0.25 * height)), interpolation=cv.INTER_CUBIC)
    cv.imshow('res2', res2)
    cv.waitKey(0)
    cv.destroyAllWindows()

2.平移

# 平移
def translate():
    img = cv.imread('pby.jpg', 0)
    rows, cols = img.shape
    M = np.float32([[1, 0, 100], [0, 1, 50]])
    dst = cv.warpAffine(img, M, (cols, rows))
    cv.imshow('img', dst)
    cv.waitKey(0)
    cv.destroyAllWindows()

3.旋轉

# 旋轉
def rotate():
    img = cv.imread('pby.jpg', 0)
    rows, cols = img.shape
    # cols-1 和 rows-1 是坐標限制
    M = cv.getRotationMatrix2D(((cols - 1) / 2.0, (rows - 1) / 2.0), 90, 1)
    print(M)
    # [[ 6.12323400e-17  1.00000000e+00 -1.13686838e-13]
    #  [-1.00000000e+00  6.12323400e-17  1.07900000e+03]]
    dst = cv.warpAffine(img, M, (cols, rows))
    cv.imshow('img', dst)
    cv.waitKey(0)
    cv.destroyAllWindows()

4.仿射變換

# 仿射變換
# 在仿射變換中,原始圖像中的所有平行線在輸出圖像中仍將平行。為了找到變換矩陣,我們需要輸入圖像中的三個點及其在輸出圖像中的對應位置。
# 然后cv.getAffineTransform將創建一個2x3矩陣,該矩陣將傳遞給cv.warpAffine
def affine():
    img = cv.imread('drawing.png')
    rows, cols, ch = img.shape
    pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
    pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
    M = cv.getAffineTransform(pts1, pts2)
    print(M)
    dst = cv.warpAffine(img, M, (cols, rows))
    cv.imshow('input', img)
    cv.imshow('output', dst)
    cv.waitKey(0)
    cv.destroyAllWindows()

5.透視變換

# 透視變換
# 對於透視變換,您需要3x3變換矩陣。即使在轉換后,直線也將保持直線。要找到此變換矩陣,需要在輸入圖像上有4個點,在輸出圖像上需要相應的點。
# 在這四個點中,其中三個不應共線。然后通過函數cv.getPerspectiveTransform找到變換矩陣。然后將cv.warpPerspective應用於此3x3轉換矩陣
def perspective_transform():
    img = cv.imread('sudoku.png')
    # rows, cols, ch = img.shape
    pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
    pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
    M = cv.getPerspectiveTransform(pts1, pts2)
    dst = cv.warpPerspective(img, M, (300, 300))
    cv.imshow('img', img)
    cv.imshow('dst', dst)
    cv.waitKey(0)
    cv.destroyAllWindows()
    # plt.subplot(121), plt.imshow(img), plt.title('Input')
    # plt.subplot(122), plt.imshow(dst), plt.title('Output')
    # plt.show()

圖像閾值處理

簡單閾值處理

threshold函數用於應用閾值。其參數分別為:

1.源圖像(必須為灰度圖)

2.閾值

3.超出閾值時所分配的最大值

4.閾值類型

閾值類型主要有以下幾種:

import cv2 as cv
from matplotlib import pyplot as plt


# 簡單閾值,對於每個像素,應用相同的閾值。如果像素值小於閾值,則將其設置為0,否則將其設置為最大值。
def simple_threshold():
    img = cv.imread('pby.jpg')  # 讀取圖片
    img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)  # 轉成灰度圖
    # threshold函數 用於應用閾值。其參數分別為:1.源圖像(必須為灰度圖)2.閾值 3.超出閾值時所分配的最大值4.閾值類型
    ret, thresh1 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)
    ret, thresh2 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY_INV)
    ret, thresh3 = cv.threshold(img_gray, 127, 255, cv.THRESH_TRUNC)
    ret, thresh4 = cv.threshold(img_gray, 127, 255, cv.THRESH_TOZERO)
    ret, thresh5 = cv.threshold(img_gray, 127, 255, cv.THRESH_TOZERO_INV)
    titles = ['Original Image', 'gray image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
    img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
    images = [img_rgb, img_gray, thresh1, thresh2, thresh3, thresh4, thresh5]
    for i in range(7):
        plt.subplot(2, 4, i + 1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

運行結果如下:

自適應閾值處理

# 當同一幅圖像上的不同部分具有不同亮度時。這種情況下我們需要采用自適應閾值。此時的閾值是根據圖像上的每一個小區域計算與其對應的閾值。
# 因此在同一幅圖像上的不同區域采用的是不同的閾值,從而使我們能在亮度不同的情況下得到更好的結果。
def adaptive_threshold():
    img = cv.imread('pby.jpg')
    img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # 中值濾波
    img_gray = cv.medianBlur(img_gray, 5)
    ret, th1 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)
    # 11 為 Block size,即鄰域大小,用於計算閾值的窗口大小, 2 為 常數,可以理解為偏移量
    th2 = cv.adaptiveThreshold(img_gray, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2)
    th3 = cv.adaptiveThreshold(img_gray, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
    titles = ['Original Image', 'Global Thresholding (v = 127)', 'Adaptive Mean Thresholding',
              'Adaptive Gaussian Thresholding']
    img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
    images = [img_rgb, th1, th2, th3]
    for i in range(4):
        plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

運行結果:

中值濾波

這里介紹一下中值濾波的概念

無論是直接獲取的灰度圖像,還是由彩色圖像轉換得到的灰度圖像,里面都有噪聲的存在,噪聲對圖像質量有很大的影響。進行中值濾波不僅可以去除孤點噪聲,而且可以保持圖像的邊緣特性,不會使圖像產生顯著的模糊,比較適合於實驗中的人臉圖像。

中值濾波是一種非線性的信號處理方法,因此中值濾波器也就是一種非線性的濾波器。在一定條件下,其可以克服線性濾波器處理圖像細節模糊的問題,而且它對濾除脈沖干擾和圖像掃描噪聲非常有效,但是,對點、線、尖頂等細節較多的圖像,則會引起圖像信息的丟失。中值濾波器最先被應用於一維信號的處理中,后來被人們引用到二維圖像的處理中來。

中值濾波是對一個滑動窗口內的諸像素灰度值排序,用其中值代替窗口中心像素的原來灰度值,它是一種非線性的圖像平滑法,它對脈沖干擾級椒鹽噪聲的抑制效果好,在抑制隨機噪聲的同時能有效保護邊緣少受模糊。

中值濾波可以過濾尖峰脈沖。目的在於我們對於濾波后的數據更感興趣。濾波后的數據保留的原圖像的變化趨勢,同時去除了尖峰脈沖對分析造成的影響。

以一維信號的中值濾波舉例。對灰度序列80、120、90、200、100、110、70,如果按大小順序排列,其結果為70、80、90、10O、110、120、200,其中間位置上的灰度值為10O,則該灰度序列的中值即為100。一維信號中值濾波實際上就是用中值代替規定位置(一般指原始信號序列中心位置)的信號值。對前面所舉的序列而言,中值濾波的結果是用中值100替代序列80、120、90、200、100、110、70中的信號序列中心位置值200,得到的濾波序列就是80、120、90、100、100、110、70。如果在此序列中200是一個噪聲信號,則用此方法即可去除這個噪聲點。

二維中值濾波算法是對於一幅圖像的像素矩陣,取以目標像素為中心的一個子矩陣窗口,這個窗口可以是33 ,55 等根據需要選取,對窗口內的像素灰度排序,取中間一個值作為目標像素的新灰度值。窗口示例如ooooxoooo上面x為目標像素,和周圍o組成3*3矩陣Array,然后對這9個元素的灰度進行排序,以排序后的中間元素Array[4]為x的新灰度值,如此就完成對像素x的中值濾波,再迭代對其他需要的像素進行濾波即可。

中值濾波的基本思想是,把局部區域的像素按灰度等級進行排序,取該領域中灰度的中值作為當前像素的灰度值。

中值濾波的步驟為:

  • 將濾波模板(含有若干個點的滑動窗口)在圖像中漫游,並將模板中心與圖中某個像素位置重合;

  • 讀取模板中各對應像素的灰度值;

  • 將這些灰度值從小到大排列;

  • 取這一列數據的中間數據,將其賦給對應模板中心位置的像素。如果窗口中有奇數個元素,中值取元素按灰度值大小排序后的中間元素灰度值。如果窗口中有偶數個元素,中值取元素按灰度值大小排序后,中間兩個元素灰度的平均值。

因為圖像為二維信號,中值濾波的窗口形狀和尺寸對濾波器效果影響很大,不同圖像內容和不同應用要求往往選用不同的窗口形狀和尺寸。

由以上步驟,可以看出,中值濾波對孤立的噪聲像素即椒鹽噪聲、脈沖噪聲具有良好的濾波效果。由於其並不是簡單的取均值,所以,它產生的模糊也就相對比較少。

二值化

# otsu 二值化
# 在使用全局閾值時,我們就是隨便給了一個數來做閾值,那我們怎么知道我們選取的這個數的好壞呢?答案就是不停的嘗試。
# 如果是一副雙峰圖像(簡單來說雙峰圖像是指圖像直方圖中存在兩個峰)呢?我們豈不是應該在兩個峰之間的峰谷選一個值作為閾值?
# 這就是 Otsu 二值化要做的。簡單來說就是對一副雙峰圖像自動根據其直方圖計算出一個閾值。(對於非雙峰圖像,這種方法得到的結果可能會不理想)。
# 這里用到的函數還是 cv2.threshold(),但是需要多傳入一個參數(flag):cv2.THRESH_OTSU。這時要把閾值設為 0。然后算法會找到最優閾值,
# 這個最優閾值就是返回值 retVal。如果不使用 Otsu 二值化,返回的retVal 值與設定的閾值相等。
# 下面的例子中,輸入圖像是一副帶有噪聲的圖像。
# 第一種方法,我們設127 為全局閾值。
# 第二種方法,我們直接使用 Otsu 二值化。
# 第三種方法,我們首先使用一個 5x5 的高斯核除去噪音,然后再使用 Otsu 二值化。
def otsu():
    img = cv.imread('noisy.png')
    img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # global thresholding
    ret1, th1 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)
    # Otsu's thresholding
    ret2, th2 = cv.threshold(img_gray, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
    # Otsu's thresholding after Gaussian filtering
    # (5,5)為高斯核的大小,0 為標准差
    blur = cv.GaussianBlur(img_gray, (5, 5), 0)
    # 閾值一定要設為 0!
    ret3, th3 = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
    # plot all the images and their histograms
    images = [img_gray, 0, th1,
              img_gray, 0, th2,
              blur, 0, th3]
    titles = ['Original Noisy Image', 'Histogram', 'Global Thresholding (v=127)',
              'Original Noisy Image', 'Histogram', "Otsu's Thresholding",
              'Gaussian filtered Image', 'Histogram', "Otsu's Thresholding"]
    # 這里使用了 pyplot 中畫直方圖的方法,plt.hist, 要注意的是它的參數是一維數組
    # 所以這里使用了(numpy)ravel 方法,將多維數組轉換成一維,也可以使用 flatten 方法
    # ndarray.flat 1-D iterator over an array.
    # ndarray.flatten 1-D array copy of the elements of an array in row-major order.
    for i in range(3):
        plt.subplot(3, 3, i * 3 + 1), plt.imshow(images[i * 3], 'gray')
        plt.title(titles[i * 3]), plt.xticks([]), plt.yticks([])
        plt.subplot(3, 3, i * 3 + 2), plt.hist(images[i * 3].ravel(), 256)
        plt.title(titles[i * 3 + 1]), plt.xticks([]), plt.yticks([])
        plt.subplot(3, 3, i * 3 + 3), plt.imshow(images[i * 3 + 2], 'gray')
        plt.title(titles[i * 3 + 2]), plt.xticks([]), plt.yticks([])
    plt.show()

執行結果:

otsu二值化工作原理

# 手動計算閾值
def calculate_otsu():
    img = cv.imread('noisy.png')
    img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    blur = cv.GaussianBlur(img_gray, (5, 5), 0)
    # find normalized_histogram, and its cumulative distribution function
    # 計算歸一化直方圖
    # CalcHist(image, accumulate=0, mask=NULL)
    hist = cv.calcHist([blur], [0], None, [256], [0, 256])
    hist_norm = hist.ravel() / hist.max()
    Q = hist_norm.cumsum()
    bins = np.arange(256)
    fn_min = np.inf
    thresh = -1
    for i in range(1, 256):
        p1, p2 = np.hsplit(hist_norm, [i])  # probabilities
        q1, q2 = Q[i], Q[255] - Q[i]  # cum sum of classes
        b1, b2 = np.hsplit(bins, [i])  # weights
        # finding means and variances
        m1, m2 = np.sum(p1 * b1) / q1, np.sum(p2 * b2) / q2
        v1, v2 = np.sum(((b1 - m1) ** 2) * p1) / q1, np.sum(((b2 - m2) ** 2) * p2) / q2
        # calculates the minimization function
        fn = v1 * q1 + v2 * q2
        if fn < fn_min:
            fn_min = fn
            thresh = i
    # find otsu's threshold value with OpenCV function
    ret, otsu, = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
    print(thresh)
    print(ret)

圖像平滑處理

自定義濾波器進行卷積

# 在介紹四種模糊(平滑)濾波器之前,先使用自定義濾波器對圖像進行卷積處理
def custom_filter():
    img = cv2.imread('opencv-logo.png')
    kernel = np.ones((5, 5), np.float32) / 25
    print(kernel)
    # cv.Filter2D(src, dst, kernel, anchor=(-1, -1))
    # depth –desired depth of the destination image;
    # if it is negative, it will be the same as src.depth();
    # the following combinations of src.depth() and depth are supported:
    # src.depth() = CV_8U, depth = -1/CV_16S/CV_32F/CV_64F
    # src.depth() = CV_16U/CV_16S, depth = -1/CV_32F/CV_64F
    # src.depth() = CV_32F, depth = -1/CV_32F/CV_64F
    # src.depth() = CV_64F, depth = -1/CV_64F
    # when depth=-1, the output image will have the same depth as the source.
    dst = cv2.filter2D(img, -1, kernel)
    plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(dst), plt.title('Averaging'), plt.xticks([]), plt.yticks([])
    plt.show()

執行結果

打印輸出

[[0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]]

均值濾波器

# 用卷積框覆蓋區域所有像素的平均值來代替中心元素。
def average_filter():
    img = cv2.imread('opencv-logo.png')
    blur = cv2.blur(img, (5, 5))
    plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
    plt.show()

高斯濾波器

# 高斯核(簡單來說,方框不變,將原來每個方框的值是相等的,現在里面的值是符合高斯分布的,方框中心的值最大,其余方框根據
# 距離中心元素的距離遞減,構成一個高斯小山包。原來的求平均數現在變成求加權平均數,權就是方框里的值)
# 高斯濾波器是求中心點鄰近區域像素的高斯加權平均值。這種高斯濾波器只考慮像素之間的空間關系,而不會考慮像素值之間的關系(像素的相似度)。
# 所以這種方法不會考慮一個像素是否位於邊界。因此邊界也會模糊掉
def gaussian_blur():
    img = cv2.imread('opencv-logo.png')
    blur = cv2.GaussianBlur(img, (5, 5), 0, 0)
    plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
    plt.show()

中值濾波器

# 用與卷積框對應像素的中值來替代中心像素的值。這個濾波器經常用來去除椒鹽噪聲。前面的濾波器都是用計算得到的一個新值來取代中
# 心像素的值,而中值濾波是用中心像素周圍(也可以使他本身)的值來取代他。他能有效的去除噪聲。卷積核的大小也應該是一個奇數。
def median_blur():
    img = cv2.imread('opencv-logo.png')
    blur = cv2.medianBlur(img, (5, 5))
    plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
    plt.show()

雙邊濾波器

# 雙邊濾波在同時使用空間高斯權重和灰度值相似性高斯權重。空間高斯函數確保只有鄰近區域的像素對中心點有影響,灰度值相似性高斯函數確保只有
# 與中心像素灰度值相近的才會被用來做模糊運算。所以這種方法會確保邊界不會被模糊掉,因為邊界處的灰度值變化比較大。
# 通常用於處理圖片紋理,同時保留邊界
def bilateral_blur():
    img = cv2.imread('opencv-logo.png')
    # cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
    # d – Diameter of each pixel neighborhood that is used during filtering.
    # If it is non-positive, it is computed from sigmaSpace
    # 9 鄰域直徑,兩個 75 分別是空間高斯函數標准差,灰度值相似性高斯函數標准差
    blur = cv2.bilateralFilter(img, 9, 75, 75)
    plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
    plt.show()

形態學操作 腐蝕&膨脹&開運算&閉運算&形態學梯度&禮帽&黑帽

腐蝕和膨脹

def erode_dilate():
    img = cv2.imread('j.png', cv2.IMREAD_COLOR)
    kernel = np.ones((5, 5), np.uint8)
    erosion = cv2.erode(img, kernel, iterations=1) # 腐蝕
    dilation = cv2.dilate(img, kernel, iterations=1) # 膨脹
    titles = ['original image', 'eroded image', 'dilated image']
    images = [img, erosion, dilation]
    for i in range(3):
        plt.subplot(1, 3, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

執行結果:

開運算【先腐蝕再膨脹】

# 先進行腐蝕再進行膨脹就叫做開運算,常用來去除噪聲
def morphologyEx_open():
    img = cv2.imread('j with noise.png', cv2.IMREAD_COLOR)
    kernel = np.ones((5, 5), np.uint8)
    opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
    titles = ['original image', 'opening image']
    images = [img, opening]
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

運行結果

閉運算【先膨脹再腐蝕】

# 先膨脹再腐蝕。它經常被用來填充前景物體中的小洞,或者前景物體上的小黑點。
def morphologyEx_close():
    img = cv2.imread('j with noise2.png', cv2.IMREAD_COLOR)
    kernel = np.ones((5, 5), np.uint8)
    closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
    titles = ['original image', 'closing image']
    images = [img, closing]
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i]),
        plt.xticks([]), plt.yticks([])
    plt.show()

運行結果:

形態學梯度

# 一幅圖像膨脹與腐蝕的差,看上去就像是前景物體的輪廓
def morphologyEx_gradient():
    img = cv2.imread('j.png', cv2.IMREAD_COLOR)
    kernel = np.ones((5, 5), np.uint8)
    gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
    titles = ['original image', 'gradient image']
    images = [img, gradient]
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i]),
        plt.xticks([]), plt.yticks([])
    plt.show()

運行結果:

禮帽&黑帽

# 原始圖像與進行開運算之后得到的圖像的差。
# tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
# 進行閉運算之后得到的圖像與原始圖像的差
# blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
def morphologyEx_tophat_blackhat():
    img = cv2.imread('j.png', cv2.IMREAD_COLOR)
    kernel = np.ones((5, 5), np.uint8)
    tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)  # 禮帽
    blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)  # 黑帽
    titles = ['original image', 'tophat image', 'blackhat image']
    images = [img, tophat, blackhat]
    for i in range(3):
        plt.subplot(1, 3, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i]),
        plt.xticks([]), plt.yticks([])
    plt.show()

運行結果:

形態學操作之間的關系

  • opening:

    dst = open(src,element) = dilate(erode(src,element),element)

  • closing:

    dst = close(src,element) = erode(dilate(src,element),element)

  • morphological gradient:

    dst = morph_grad(src,element) = dilate(src,element) - erode(src,element)

  • tophat:

    dst = tophat(src,element) = src - open(src,element)

  • blackhat:

    dst = blackhat(src,element) = close(src,element) - src

結構化元素

在前面的例子中我們使用 Numpy 構建了結構化元素,它是正方形的。但有時我們需要構建一個橢圓形/圓形的核。為了實現這種要求,提供了 OpenCV 函數 cv2.getStructuringElement()。你只需要告訴他你需要的核的形狀 和大小。

# Rectangular Kernel
>>> cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
array(
[[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]], dtype=uint8)

# Elliptical Kernel
>>> cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
array([
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0]], dtype=uint8)

# Cross-shaped Kernel
>>> cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
array(
[[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]], dtype=uint8)

圖像梯度

梯度簡單來說就是求導。 OpenCV 提供了三種不同的梯度濾波器,或者說高通濾波器:Sobel, Scharr 和 Laplacian。Sobel,Scharr 其實就是求一階或二階導數。Scharr 是對 Sobel(使用小的卷積核求解梯度角度時)的優化。Laplacian 是求二階導數。

Sobel算子和Scharr算子

Sobel 算子是高斯平滑與微分操作的結合體,所以它的抗噪聲能力很好。 你可以設定求導的方向(xorder 或 yorder)。還可以設定使用的卷積核的大 小(ksize)。如果 ksize=-1,會使用 3x3 的 Scharr 濾波器,它的效果要 比 3x3 的 Sobel 濾波器好(而且速度相同,所以在使用 3x3 濾波器時應該盡量使用 Scharr 濾波器)。3x3 的 Scharr 濾波器卷積核如下

Laplacian 算子

拉普拉斯算子可以使用二階導數的形式定義,可假設其離散實現類似於二階 Sobel 導數,事實上,OpenCV 在計算拉普拉斯算子時直接調用 Sobel 算子。計算公式如下:

拉普拉斯濾波器使用的卷積核:

def sobel_laplacian():
    img = cv2.imread('dave.png', cv2.IMREAD_GRAYSCALE)
    # cv2.CV_64F 輸出圖像的深度(數據類型),可以使用-1, 與原圖像保持一致 np.uint8
    laplacian = cv2.Laplacian(img, cv2.CV_64F)
    # 參數 1,0 為只在 x 方向求一階導數,最大可以求 2 階導數。
    sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
    # 參數 0,1 為只在 y 方向求一階導數,最大可以求 2 階導數。
    sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)
    plt.subplot(2, 2, 1), plt.imshow(img, cmap='gray'), plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(2, 2, 2), plt.imshow(laplacian, cmap='gray'), plt.title('Laplacian'), plt.xticks([]), plt.yticks([])
    plt.subplot(2, 2, 3), plt.imshow(sobel_x, cmap='gray'), plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
    plt.subplot(2, 2, 4), plt.imshow(sobel_y, cmap='gray'), plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])
    plt.show()

運行結果:

在查看上面這個例子的注釋時不知道你有沒有注意到:當我們可以通過參數 -1 來設定輸出圖像的深度(數據類型)與原圖像保持一致,但是我們在代碼中使用的卻是 cv2.CV_64F。這是為什么呢?想象一下一個從黑到白的邊界 的導數是整數,而一個從白到黑的邊界點導數卻是負數。如果原圖像的深度是 np.int8 時,所有的負值都會被截斷變成 0,換句話說就是把邊界丟失掉。 所以如果這兩種邊界你都想檢測到,最好的辦法就是將輸出的數據類型設置的更高,比如 cv2.CV_16S,cv2.CV_64F 等。取絕對值然后再把它轉回到 cv2.CV_8U。下面的示例演示了輸出圖片的深度不同造成的不同效果。

def depth_of_output():
    img = cv2.imread('boxs.png', cv2.IMREAD_GRAYSCALE)
    # Output dtype = cv2.CV_8U
    sobel_x8u = cv2.Sobel(img, cv2.CV_8U, 1, 0, ksize=5)
    # 也可以將參數設為-1
    # sobel_x8u = cv2.Sobel(img,-1,1,0,ksize=5)
    # Output dtype = cv2.CV_64F. Then take its absolute and convert to cv2.CV_8U
    sobel_x64f = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
    abs_sobel64f = np.absolute(sobel_x64f)
    sobel_8u = np.uint8(abs_sobel64f)
    plt.subplot(1, 3, 1), plt.imshow(img, cmap='gray'), plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(1, 3, 2), plt.imshow(sobel_x8u, cmap='gray'), plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
    plt.subplot(1, 3, 3), plt.imshow(sobel_8u, cmap='gray'), plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
    plt.show()

運行結果:

Canny邊緣檢測

Canny 邊緣檢測是一種非常流行的邊緣檢測算法,是 John F.Canny 在 1986 年提出的。它是一個有很多步構成的算法。

1.噪聲去除

由於邊緣檢測很容易受到噪聲影響,所以第一步是使用 5x5 的高斯濾波器去除噪聲

2.計算圖像梯度

對平滑后的圖像使用 Sobel 算子計算水平方向和豎直方向的一階導數(圖像梯度)(Gx 和 Gy)。根據得到的這兩幅梯度圖(Gx 和 Gy)找到邊界的梯度和方向,公式如下

梯度的方向一般總是與邊界垂直。梯度方向被歸為四類:垂直,水平,和 兩個對角線

3.非極大值抑制

在獲得梯度的方向和大小之后,應該對整幅圖像做一個掃描,去除那些非邊界上的點。對每一個像素進行檢查,看這個點的梯度是不是周圍具有相同梯度方向的點中最大的。

現在你得到的是一個包含“窄邊界”的二值圖像

4.滯后閾值

現在要確定那些邊界才是真正的邊界。這時我們需要設置兩個閾值: minVal 和 maxVal。

當圖像的灰度梯度高於 maxVal 時被認為是真的邊界, 那些低於 minVal 的邊界會被拋棄

如果介於兩者之間的話,就要看這個點是否與某個被確定為真正的邊界點相連,如果是就認為它也是邊界點,如果不是就拋棄。

A 高於閾值 maxVal 所以是真正的邊界點,C 雖然低於 maxVal 但高於 minVal 並且與 A 相連,所以也被認為是真正的邊界點。而 B 就會被拋棄,因為他不僅低於 maxVal 而且不與真正的邊界點相連。所以選擇合適的 maxVal 和 minVal 對於能否得到好的結果非常重要。 在這一步,一些小的噪聲點也會被除去,因為我們假設邊界都是一些長的線段。

def canny():
    img = cv2.imread('tree.jpg', cv2.IMREAD_GRAYSCALE)
    edges = cv2.Canny(img, 80, 100)
    plt.subplot(121), plt.imshow(img, cmap='gray'), plt.title('Original Image'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(edges, cmap='gray'), plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
    plt.show()

運行結果:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM