三、OpenCV-python 之 圖像處理(Ⅰ)

1、空間轉換

OpenCV的圖像顏色空間很多,常見的有BGR、HSV等。

cv.cvtColor(input_image, flag)		# 空間轉換函數,flag參數多達150多種,常用的cv.COLOR_BGR2GRAY、cv.COLOR_BGR2HSV

(1)HSV空間中,色調範圍Hue range【0,179】,飽和度Saturation range【0,255】,顏色明亮程度Value range【0,255】;
(2)HSV空間中更加容易表示顏色,所以通常將BGR圖像轉換爲HSV,然後進行提取。

cv.inRange(		# 該函數返回一個mask,對每一個通道的顏色值檢測是否位於lowerb~upperb之間
    src,		# 輸入圖像
    lowerb,		# 下界,它是一個數組,形式跟輸入圖像的單個像素點的形式一致
    upperb,		# 上界
    dst
)

cv.inRange()函數通常用於提取特定顏色、特定目標物體,但是需要先將圖像轉換爲HSV格式,HSV對顏色表示更容易,提取更方便。

應用舉例:

import cv2 as cv
import numpy as np

cap = cv.VideoCapture(0)
while(1):
    # Take each frame
    _, frame = cap.read()							# 第一個參數給出是否獲取幀成功
    
    # Convert BGR to HSV
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
    
    # Define range of blue color in HSV
    lower_blue = np.array([110,50,50])				# 注意,這是在HSV空間下的
    upper_blue = np.array([130,255,255])
    
    # Threshold the HSV image to get only blue colors
    mask = cv.inRange(hsv, lower_blue, upper_blue)
    
    # Bitwise-AND mask and original image
    res = cv.bitwise_and(frame,frame, mask= 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()

效果圖:

在這裏插入圖片描述

2、圖像幾何變換

(1)縮放

cv.resize(src, dst, dsize, fx, fy, interpolation)	# dsize是輸出圖像的尺寸(w,h),fx-fy是縮放的比例參數,dsize和後面的 fx,fy 二選一即可,最後一個是插值類型,有下面幾種

cv.INTER_NEAREST            # 最近鄰插值,放大效果很模糊
cv.INTER_LINEAR             # 雙線性插值,放大效果比最近鄰稍好
cv.INTER_CUBIC              # 雙三次插值
cv.INTER_AREA               # 像素區域重採樣,放大效果跟最近鄰差不多
cv.INTER_LANCZOS4           # Lanczos插值

OpenCV中但凡涉及到圖像尺寸的數據,一般都是按照 (w, h, c) 的形式來操作,比如上面縮放操作傳入的尺寸;但是通過 image.shape 獲取圖像尺寸時卻是按照 (h, w, c) 返回的,因爲 image 是 numpy 數據,shape 是 numpy 的方法。另外,OpenCV的縮放操作採用插值的數學計算方式,這和後面將要看到的採樣不太一樣,在高斯金字塔中上採樣通過插入0行、0列然後卷積的方式,下采樣是直接捨棄一半的行、列。
(2)平移

cv.warpAffine(
    src, 
    dst, 
    M,				# 平移矩陣,其size是2x3
    dsize,			# 輸出圖像的尺寸,它的形式是(width,height)
    flags,			# 插值方式
    borderMode,		# 邊界的模式,有可能是直接置爲0,有可能是置爲邊緣像素值
    borderValue		# 邊界的填充值
)

這裏用到的 cv.warpAffine() 函數是一個仿射變換函數,核心在於變換矩陣 M。

仿射變換更多的是改變像素的位置,而不改變像素值的大小,所以矩陣 M 是作用在像素位置上的,這跟卷積核的操作不一樣,卷積核的目的是改變圖像的色彩呈現,所以它是和圖像中的每一個像素做計算。

(3)旋轉
獲取旋轉參數,得到的是一個參數矩陣M,然後直接送入上面的 cv.warpAffine()。要注意的是,如果旋轉的前後圖像尺寸不一致,可能會發生意料之外的剪裁。

cv.getRotationMatrix2D(
    center,		# 旋轉中心,一般是圖像中心
    angle,		# 旋轉角度
    scale		# 縮放比例
)

// 例如:
matRotate = cv.getRotationMatrix2D((h/2, w/2), -180, 1.0)	# 順時針旋轉180度
mat_rotate[0, 2] += (w_new - w)/2							# 適應旋轉後的尺寸,不剪裁圖片
mat_rotate[1, 2] += (h_new - h)/2

frame = cv.warpAffine(frame, mat_rotate, (w_new, h_new))	# 根據旋轉矩陣參數來實現旋轉變換

(4)仿射變換
平行線在變換後仍然平行(就是矩形變成了平行四邊形),需要做的就是給定變換前後圖像中的三個點的位置,然後計算變換矩陣 M,然後同樣送入 cv.warpAffine()。

cv.getAffineTransform(
    src,		# 原始圖像中的三個點的位置,是一個3x2矩陣
    dst			# 變換之後,這三個點在圖像中的位置
)

(5)透視變換
直線在變換之後仍然是直線(不保證平行),需要做的是給定變換前後的四個點的位置,這四個點中的任意三個不共線,計算變換矩陣 M,然後送入 cv.warpPerspective()。

M = cv.getPerspectiveTransform(src, dst, solveMethod = DECOMP_LU)
cv.warpPerspective(
    src, 
    dst, 
    M,			# 注意:這個變換矩陣和仿射變換的略微不同,它是一個3x3的
    dsize,
    flags = INTER_LINEAR,
    borderMode = BORDER_CONSTANT,
    borderValue = Scalar()
)

3、閾值

(1)全局閾值
閾值操作可以用來二值化圖像,閾值相當於一個分界線,將像素值劃分爲兩個集合,分別進行不同的操作。全局閾值中的閾值人爲設定,對於每一個像素點來說是相同、不變的。

cv.threshold(
    src,		# 輸入圖像
    dst,		# 輸出圖像
    thresh,		# 閾值
    maxval,		# 最大值,就是將滿足條件的像素值置爲該值
    type		# 閾值操作類型,小於置0大於置maxval,大於置0小於置maxval,小於不變大於置maxval,小於不變大於置0,小於置0大於不變
)

(2)自適應閾值
這種閾值操作一般用在圖像光照不均勻的情況下,像素的閾值由其附近的領域像素值決定。

cv::adaptiveThreshold(
    src,
    dst,
    maxValue,        // 最大值
    adaptiveMethod,  // 自適應方式有兩種,cv.ADAPTIVE_THRESH_MEAN_C領域面積減去常數C後的均值,cv.ADAPTIVE_THRESH_GAUSSIAN_C鄰域值減去常數C的高斯加權和
    thresholdType,   // 閾值操作類型,這跟上面的全局閾值是一致的
    blockSize,       // 鄰域大小
    C                // 常數值
)

(3)Otsu’s:直方圖閾值
它的閾值是通過直方圖來確定的,適用於雙模態圖像;因爲雙模態圖像的直方圖一般只有兩個波峯,閾值就取波峯的均值。

cv.threshold(
    src,
    0,
    255,
    cv.THRESH_BINARY+cv.THRESH_OTSU		# 使用方式是在閾值操作類型的後面加上一個cv.THRESH_OTSU參數
)

(1)直方圖閾值對於圖像去噪很好用,可以先用高斯濾波去掉一部分噪點,然後用直方圖閾值,得到的圖像基本沒有噪點,但是僅限於雙模態圖像
(2)官方網站給出了直方圖閾值的計算方法,這裏省略該過程

4、圖像濾波

(1)基本概念

  • 高頻:圖像中顏色變化劇烈的部分,它展示了圖像中的細節,例如邊緣、噪點等位置
  • 低頻:圖像中顏色變化不大的部分,多是一些平坦光滑的大部分區域
  • 高通濾波:過濾掉低頻部分,只保留那些變化劇烈的邊緣、噪點(波峯波谷不見了,但是抖動仍然在),常用於圖像的邊緣檢測
  • 低通濾波:過濾掉高頻部分,保留的是平坦的大部分區域(波峯波谷還在,過濾掉細節,圖像的波線更加平滑),效果就是去掉噪點、使圖像變得模糊(因爲邊緣特徵的高頻變成了低頻)

(圖像與濾波可以參考這裏

(2)均值濾波
均值濾波的卷積核是一個 3x3 矩陣,每一個元素的值都相同,取加權求和之後的值作爲濾波後的像素值;根據卷積核的元素值不同,低通濾波有兩種。

cv.blur(
    src,
    dst,
    ksize,							# 卷積核的尺寸,卷積核的每一個元素值都是相同的,且和爲1,該參數必須是一個tuple
    anchor = Point(-1,-1),			# 錨點位置,默認爲(-1,-1)就是指卷積核的中心位置
    borderType = BORDER_DEFAULT		# 邊緣的填補方式
)

cv.boxFilter(
    src,
    dst,
    ddepth,							# 輸出圖像的深度,默認爲-1,表示與輸入圖像一致
    ksize,							# 卷積核的尺寸,卷積核的元素值受到該尺寸和下面的normalize參數的影響,該參數必須是一個tuple
    anchor = Point(-1,-1),			# 錨點
    normalize = true,				# 是否進行歸一化,如果是那就跟上面的cv.blur沒有區別(取均值),如果不是那就代表是求和(也就是卷積核每一個元素的值是1)
    borderType = BORDER_DEFAULT		# 邊緣的填補方式
)

(3)高斯濾波
在去除高斯噪點(例如毛邊)的時候很有用,它的計算方式跟均值濾波沒有區別,不同之處在於卷積核;低通濾波會使得圖像整體變模糊,高斯濾波也不例外。

cv.GaussianBlur(
    src,
    dst,
    ksize,						# 卷積核大小,width、height可以不同,但是必須是正奇數;卷積核的元素值服從高斯分佈,中心元素替換爲鄰域內的高斯加權和,該參數必須是一個tuple
    sigmaX,						# 卷積核在 X 方向的標準差
    sigmaY = 0,					# 卷積核在 Y 方向的標準差,如果沒設置那就跟 sigmaX 相同;如果sigmaX、sigmaY都是0,那就從卷積核來計算
    borderType = BORDER_DEFAULT	# 邊緣填充方式
)

(4)中值濾波
上面的均值濾波、高斯濾波都是通過計算(可能是區域中存在的,也可能不存在)產生一個值替代中間元素,而中值則是直接使用區域內的中間值來完成替代。因爲椒鹽噪聲的像素值要麼是0要麼是255,不可能是區域內的中值,所以中值濾波對於椒鹽去噪很有效。

椒鹽噪聲:椒–黑,鹽–白,所以它指的是黑白色噪點,通常出現在灰度圖中。
高斯噪聲:服從高斯分佈的一類噪聲,通常是因爲不良照明和高溫引起的傳感器噪聲,通常在RGB圖像中顯現比較明顯。

cv.medianBlur(
    src,
    dst,
    ksize			# 整形參數,表示卷積核的大小,它只是起到一個指定區域的作用,必須是正奇數(1,3,5,...)
)

(5)雙邊濾波
它能夠在有效去除噪聲的同時保留邊緣信息,但是它的速度比較慢;它使用的是雙高斯濾波,空間高斯函數確保只考慮附近像素的模糊,而強度差高斯函數(這是一個像素差的函數)確保只考慮與中心像素強度相似的像素的模糊,所以邊緣就會被保留。

cv::bilateralFilter(
    src,
    dst,
    d,								# 整形參數,卷積核大小,d>5就會很慢,一般令 d=5 來保證實時
    sigmaColor,						# 顏色空間值,如果<10,該濾波器基本不會有效果;如果>150,該濾波器的濾波將會很強,將圖像變成卡通風格;值越大表示與中心像素差距越大的像素將被考慮進來
    sigmaSpace,						# 座標空間值,它通常與顏色空間值保持一致,特性也是一樣的;值越大表示越遠的像素值將被考慮進來
    borderType = BORDER_DEFAULT		# 邊緣填充方式
)

以上的四種濾波方式均屬於低通濾波,可以對比看一下它們各自的效果:
在這裏插入圖片描述

5、形態學操作

(1)腐蝕Erosion
形態學操作針對的是二值圖像,而且是對白色區域(255)而言的,腐蝕就是把白色區域變小。具體來說,如果卷積核區域存在0,那麼中心就被置爲0,除非卷積核區域全爲1

cv::erode(
    src,
    dst,
    kernel,							# 卷積核的大小,一般是一個3x3矩陣,比如使用 np.ones((3,3),np.uint8)
    anchor = Point(-1,-1),			# 中心位置
    iterations = 1,					# 腐蝕的操作次數,默認爲1次
    borderType = BORDER_CONSTANT,	# 邊緣填充類型
    borderValue = morphologyDefaultBorderValue()	# 邊緣填充值
)

腐蝕對於去掉二值圖像中的一些白色噪點很有用,注意是白色噪點

(2)膨脹Dilation
與腐蝕相反,如果卷積核區域包含1那就把中心區域置爲1,除非卷積核區域全爲0,這會把白色區域變大,它可以方便的去掉二值圖像中的黑色噪點

cv.dilate(src, dst, kernel, anchor, iterations, borderType, borderValue)		# 參數跟腐蝕一模一樣

(3)開操作
先腐蝕後膨脹,可以去掉白色噪點且保持剩餘圖像尺寸不變,白色的拐角區域、細瘦的白色區域也會消失

cv.morphologyEx(
    src,
    dst,
    op,						# 形態學操作類型,cv.MORPH_CLOSE
    kernel,
    anchor = Point(-1,-1),
    iterations = 1,			# 開操作次數
    borderType = BORDER_CONSTANT,
    borderValue = morphologyDefaultBorderValue()
)

(4)閉操作
先膨脹後腐蝕,可以去掉黑色噪點且保持剩餘圖像尺寸不變,黑色拐角區域、細瘦黑色區域也會消失

cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)

(5)形態學梯度
先進行膨脹、腐蝕操作,然後膨脹結果減去腐蝕結果。

cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)

(6)頂帽操作
先進行開運算,然後原圖像減去開運算結果。開運算會讓一些細小的白色區域(比如毛邊輪廓、噪點)被過濾掉,減法操作就會讓這些區域顯現出來。

cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)

(7)黑帽操作
先進行閉運算操作,然後將閉運算結果減去原圖像。

cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)

(1)這些形態學操作雖然有各自的特性與適用範圍,最終選擇哪一個還是要是具體情況、最終效果而定;
(2)卷積核的形狀並不一定非要是矩形的,它也可以是橢圓、十字形的,cv.getStructuringElement可以快速的得到這些卷積核。

6、圖像梯度

對應了前面的低通濾波,這裏是高通濾波;低通濾波是去噪,高通濾波則用來檢測邊緣。
(1)sobel
它是高斯平滑與微分處理的結合體,因此對噪聲具有更加強的抵抗力;它有垂直、水平兩個方向的邊緣檢測。

在這裏插入圖片描述

cv.Sobel(
    src,
    dst,
    ddepth,			# 輸出圖像的深度
    dx,				# 對 X 方向的導數(係數),1 表示對 X 方向求導,0 表示不求導;垂直方向
    dy,				# 對 Y 方向的導數(係數),1 表示對 Y 方向求導,0 表示不求導;水平方向
    ksize = 3,		# sobel 核的大小,一般這個是固定的,具體的值見下圖
    scale = 1,		# 計算導數的比例因子???默認情況下不進行縮放
    delta = 0,		# 在將計算結果存儲到dst之前添加的delta值,一般不用
    borderType		# 邊界填充類型
)

(1)cv.Scharr() 是同 sobel 相類似的邊緣檢測算子,區別是卷積核的值不同,scharr 是[-3,-10,-3],它的精度會比 sobel 更好;它解決了 sobel 算子內核由於求導近似產生的誤差;
(2)sobel 算子也被稱爲一階微分算子,它只進行一階求導,這與下面的拉普拉斯算子是有區別的;
(3)算法步驟:高斯平滑(去噪),轉灰度,求梯度(微分),合併近似梯度。

(2)Laplacian
拉普拉斯求的是二階導數,這使得拉普拉斯算子具有各向同性、旋轉不變性。

cv.Laplacian(
    src,
    dst,
    ddepth,
    ksize = 1,	# 拉普拉斯的核的大小和之前的卷積核大小的意義有些不同,因爲它是二階的,所以當ksize=1時實際上卷積核的尺寸是3
    scale = 1,	# 縮放比例
    delta = 0,	# 在存儲結果之前添加的delta值
    borderType	# 邊緣填充方式
)

(1)拉普拉斯算子對圖像中的孤立線、孤立點、線端點特別敏感,它能使得原來的亮點更亮
(2)同sobel算子一樣,拉普拉斯算子也會增強圖像中的噪點,所以進行該高通濾波之前往往需要先用低通濾波去掉噪點
(3)拉普拉斯算子求邊緣效果比sobel更強

(3)關於目標圖像的深度
一般我們設置參數ddepth=-1,讓目標圖像深度與輸入圖像一致,但是這裏有個問題,如果目標圖像的類型是CV_8U或者np.uint8,在求梯度的時候 OpenCV 按照從左到右、從上到下的順序進行,這樣如果是 white-black 邊界的話梯度是正值,可以保留下來,但是如果是 black-white 邊界的話梯度將是負值,當我們將數據轉換爲CV_8U或者uint8的時候,這些負值就變成了0,這時邊界就消失了。
怎麼解決呢?可以將ddepth設置爲CV_16S、CV_64F等,反正不要用CV_8U就行。

CV_8U/CV_16S/CV_64F的區別?
在OpenCV中,ddepth類型的結構定義爲:CV_<bit_depth>(S|U|F)C<number_of_channels>
----bit_depth:表示一個像素點所佔用的空間大小(單位是bite)
----U表示無符號整型,S表示有符號整型,F表示單精度浮點型
----number_of_channels表示通道數
例如:CV_8UC1就表示一個像素點佔用8bite,像素值類型是無符號整型(也就是0~255),單通道

7、Canny邊緣檢測

算法步驟:

  1. 去噪:邊緣和噪點都屬於高頻特徵,噪點會影響邊緣檢測的準確性,所以需要先用高斯濾波進行去噪
  2. 梯度計算:利用sobel算子檢測水平/垂直方向上的梯度G_x/G_y(一階微分),然後計算邊緣上的梯度
  3. NMS:非極大值抑制,檢查每一個像素在鄰域內的梯度是否爲最大值,如果不是就將梯度置爲0
  4. 閾值確定:這裏需要兩個閾值maxval、minval,梯度值在maxval之上的被確定爲邊界,在minval之下的被丟棄,位於二者之間的如果和邊界有連接就被認爲是邊界,否則也將被丟棄
cv::Canny(
    image,                # 輸入圖像
    edges,                # 輸出邊緣圖像
    threshold1,           # 閾值1,注意並不一定是maxval,該函數會將兩個閾值進行比較,之後決定maxval、minval
    threshold2,           # 閾值2,閾值範圍越大邊緣細節越多
    apertureSize = 3,     # sobel算子的卷積核大小
    L2gradient = false    # 像素梯度的計算方法:true表示採用平方和開方的方式,false表示採用絕對值相加的方式
)

我們將三種邊緣檢測算子做一個對比:

img_sobel = cv.Sobel(img, cv.CV_8U, 1, 1, ksize=3)
img_lapla = cv.Laplacian(img, cv.CV_8U, ksize=1)
img_canny = cv.Canny(img, 100, 200, 3)

要注意的是有幾個參數會影響輸出的結果:輸出圖像的深度ddepth,卷積核大小。

在這裏插入圖片描述

8、圖像金字塔

(1)高斯金字塔
下采樣:該操作會丟失一些信息,即便再通過上採樣還原尺寸也無法恢復;實現方式是先用卷積核進行加權求和,然後捨棄一半的行和列,這樣尺寸就減半,達到模糊效果。

cv::pyrDown(                        
    src,                            # 原始圖像
    dst,                            # 輸出圖像
    dstsize = Size(),               # 輸出圖像的尺寸,一般是原始圖像寬高的1/2
    borderType = BORDER_DEFAULT     # 邊界填充方式
)

上採樣:它的卷積核跟下采樣是相同的,區別在於採樣的方式;實現方式是先往原始圖像中添加0元素行、列(上採樣),圖像尺寸增加一倍,然後用4倍於下采樣卷積核的卷積核進行加權求和。

cv::pyrUp(                          
    src,
    dst,
    dstsize = Size(),
    borderType = BORDER_DEFAULT
)

採樣卷積核:

在這裏插入圖片描述

(2)Laplacian金字塔
前面看到laplacian算子是一種高通濾波,用來獲取邊緣信息;這裏的laplacian金字塔也是針對邊緣圖像的,它是從高斯金字塔中獲得的,常用來做圖像壓縮(沒有專門的函數)
實現方式:先對原圖像使用下采樣得到1/2原圖像,然後對下采樣圖像進行上採樣,再將原圖像和上採樣圖像做減法(得到圖像的細節信息,例如邊緣、紋理等)。

在這裏插入圖片描述

laplacian金字塔呈現的是黑白圖,它包含的是邊緣輪廓、紋理等細節特徵,很有可能已經看不出來原始圖像是哪一個

(3)圖像融合
直接拼接的方式會在邊界留下痕跡,我們考慮用圖像金字塔的方式實現。
先下采樣得到高斯金字塔,這會丟失一些細節信息,比如紋理;然後利用高斯金字塔上採樣、做減法得到拉普拉斯金字塔,這就可以得到圖像的細節信息;然後在拉普拉斯金字塔層面上做拼湊(它的最高層是保留了大部分圖像信息的),最後做圖像上採樣、求和,得到拼湊的結果。

import cv2 as cv
import numpy as np,sys
A = cv.imread('apple.jpg')
B = cv.imread('orange.jpg')
# generate Gaussian pyramid for A
G = A.copy()
gpA = [G]
for i in xrange(6):
    G = cv.pyrDown(G)
    gpA.append(G)
# generate Gaussian pyramid for B
G = B.copy()
gpB = [G]
for i in xrange(6):
    G = cv.pyrDown(G)
    gpB.append(G)
# generate Laplacian Pyramid for A
lpA = [gpA[5]]
for i in xrange(5,0,-1):
    GE = cv.pyrUp(gpA[i])
    L = cv.subtract(gpA[i-1],GE)
    lpA.append(L)
# generate Laplacian Pyramid for B
lpB = [gpB[5]]
for i in xrange(5,0,-1):
    GE = cv.pyrUp(gpB[i])
    L = cv.subtract(gpB[i-1],GE)
    lpB.append(L)
# Now add left and right halves of images in each level
LS = []
for la,lb in zip(lpA,lpB):
    rows,cols,dpt = la.shape
    ls = np.hstack((la[:,0:cols/2], lb[:,cols/2:]))
    LS.append(ls)
# now reconstruct
ls_ = LS[0]
for i in xrange(1,6):
    ls_ = cv.pyrUp(ls_)
    ls_ = cv.add(ls_, LS[i])
# image with direct connecting each half
real = np.hstack((A[:,:cols/2],B[:,cols/2:]))
cv.imwrite('Pyramid_blending2.jpg',ls_)
cv.imwrite('Direct_blending.jpg',real)

最終的效果:

在這裏插入圖片描述

更多圖像融合相關參考這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章