opencv-圖像的空間域處理之點運算
在給定圖像的像素上直接進行運算的方法稱之爲圖像空間域的處理;而根據所操作的像素的多少和類型分爲:
- 單像素操作(點運算):即對單個像素點進行處理
- 鄰域操作:即對某一像素點的操作與該點周圍的其他點相關
- 幾何變換:對整張圖片進行全局性的操作
- 形態學操作:對特定圖像形狀(邊界、凸殼等)的處理或操作
本文介紹空間域處理的點運算,其主要有如下幾種常見操作:圖像加法、圖像閾值、直方圖均衡及圖像的位運算
四、圖像加法
直接相加
在opencv中,可以使用函數cv2.add()
將某些圖像的像素進行加法運算,當然也可以直接使用 Numpy,res=img1+img2
。但需要注意的是:
-
相加的兩幅圖像尺寸、類型必須一致,或者可以直接加一個標量值
-
OpenCV的加法是一種飽和操作(相加超過255則等於255),而Numpy的加法是一種模操作(相加超過255之後則對255取模求餘數)
x = np.uint8([250])
y = np.uint8([10])
print(cv2.add(x,y))
>> 255
print(x+y)
>> 4
圖像混合
相比於加法,圖像混合操作可能更使用一些,其本質也是加法,但卻對圖片作了不同的加權,給人一種混合或者透明的感覺,其計算公式如下:
在opencv中,可以函數cv2.addWeighted(α, img1, β, img2, γ)
可以按下面的公式對圖片進行混合操作,其具有更靈活的定義:
如下案例即把兩幅圖混合在一起,第一幅圖的權重是 0.4,第二幅圖的權重是 0.6,γ取值爲 0:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img1 = cv2.imread('image/A.jpg')
img2 = cv2.imread('image/B.jpeg')
plt.figure(figsize=(20,8))
plt.subplot(131), plt.imshow(img1[:,:,::-1]), plt.xticks([]), plt.yticks([])
plt.subplot(132), plt.imshow(img2[:,:,::-1]), plt.xticks([]), plt.yticks([])
dst = cv2.addWeighted(img1, 0.4, img2, 0.6, 0)
plt.subplot(133), plt.imshow(dst[:,:,::-1]), plt.xticks([]), plt.yticks([])
plt.show()
五、圖像閾值
全局閾值
簡單來說,閾值操作就是當某一像素值高於設定的閾值時,我們給這個像素賦予一個值,否則我們給它賦予另外一個值
閾值操作往往被用於圖像分割、噪聲修復等過程中
在opencv中,閾值操作的函數是cv2.threshhold()
,該函數包含四個參數:
- 第一個參數是原圖像,原圖像應該是灰度圖
- 第二個參數就是用來對像素值進行分類的閾值
- 第三個參數就是當像素值高於(有時是小於)閾值時應該被賦予的新的像素值
- 第四個參數決定OpenCV中的不同閾值方法,這些方法包括:
cv2.THRESH_BINARY
:二值化操作,將大於閾值的部分設置爲黑色(255),小於閾值的部分設置爲白色(0)cv2.THRESH_BINARY_INV
:反向二值化,將大於閾值的部分設置爲白色(0),小於閾值的部分設置爲黑色(255)cv2.THRESH_TRUNC
: 大於閾值部分設爲閾值,否則不變cv2.THRESH_TOZERO
: 大於閾值部分不變,小於閾值的部分設置爲白色(0)cv2.THRESH_TOZERO_INV
:大於閾值部分設置爲白色(0),小於閾值的部分不變
該函數具有兩個返回值:
- 第一個爲 retVal,爲最終處理的閾值(當使用cv2.THRESH_OTSU時設定閾值會自適應變化)
- 第二個即是閾值化之後的圖像結果
下面即是一個用不同閾值處理方法得到的圖像:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('image/03.png',0)
ret,thresh1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV)
ret,thresh3 = cv2.threshold(img,127,255,cv2.THRESH_TRUNC)
ret,thresh4 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO)
ret,thresh5 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
fig = plt.figure(figsize=(14,8), dpi=80)
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
Otsu’s 二值化
Otsu是一種全局上自動選擇閾值的方法,相比於人工地根據效果不斷嘗試,Otsu可以更快捷地根據圖像的直方圖給出相對較好的結果;
這裏用到到的函數還是cv2.threshold()
,但是需要多傳入一個參數(flag):cv2.THRESH_OTSU
- 這時要把閾值設爲0,然後算法會找到最優閾值,這個最優閾值就是返回值
retVal
- Ostu的算法在解決雙峯圖像的分割時有奇效!
下面的案例中,輸入圖像是較暗,在使用127爲全局閾值時效果並不好,可以通過Ostu的方法直接找到合適的閾值:
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('image/01.jpeg',0)
ret1,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
images = [img, 0, th1,
img, 0, th2]
titles = ['Original Image','Histogram','Global Thresholding (v=127)',
'Original Image','Histogram',"Otsu's Thresholding(v={})".format(ret2)]
fig = plt.figure(figsize=(15,6), dpi=80)
for i in range(2):
plt.subplot(2,3,i*3+1),plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
plt.subplot(2,3,i*3+2),plt.hist(images[i*3].ravel(),256)
plt.title(titles[i*3+1])
plt.subplot(2,3,i*3+3),plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
plt.show()
Ostu的具體算法爲:
-
在某灰度圖片上共有M*N個像素點,其平均灰度值爲μ
-
根據某閾值T可將其分爲兩類,分別統計出大於/小於等於該閾值的像素點佔總像素的百分比 ω1和 ω2,每一類的平均灰度值爲μ1和μ2
-
根據平均數的定義,有
-
在此基礎上求得類間方差,爲:
-
對於每一個閾值T,都可以計算出其間類方差g(T),Ostu即是找到了使得方差最小的閾值T
自適應閾值
當同一幅圖像上的不同部分的具有不同亮度時,全局閾值並不能得到很好的效果,我們可能會希望根據具體情況對圖片的不同位置使用不同的閾值,該操作稱爲自適應閾值
(嚴格來說,自適應閾值參考了領域的像素值,不能算是點運算,但爲了保持連貫性就與閾值操作一同放在了點運算的這部分)
opencv提供cv2.adaptiveThreshold()
函數完成自適應閾值的操作,其包含如下參數:
-
src爲需要操作的圖片
-
maxValue爲設定的新像素值
-
adaptiveMethod是指定計算閾值的方法,通常爲如下兩種:
cv2.ADPTIVE_THRESH_MEAN_C
:閾值取自相鄰區域的平均值cv2.ADPTIVE_THRESH_GAUSSIAN_C
:閾值取值相鄰區域的加權和,權重爲一個二維高斯核
-
thresholdType:即閾值處理的方式,只有兩種可供選擇
-
cv2.THRESH_BINARY
:二值化,與全局閾值時含義相同,即
-
cv2.THRESH_BINARY_INV
:反向二值化
-
Block Size:用於設置鄰域的大小
-
C:一個常數C,閾值等於前面的輸出結果減去這個常數
我們使用下面的代碼來展示簡單閾值與自適應閾值的差別:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('image/02.jpg',0)
img = cv2.medianBlur(img,5)
ret,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
th2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,2)
th3 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,11,2)
titles = ['Original Image', 'Global Thresholding (v = 127)',
'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]
fig = plt.figure(figsize=(18,10), dpi=80)
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()
七、直方圖均衡
統計直方圖
直方圖是對整張圖片灰度值的頻率統計直方圖,通過直方圖我們可以對圖像的對比度,亮度,灰度分佈等有一個直觀的認識。OpenCV和Numpy都有內置函數做這件事:
方法一:使用opencv的內置函數
函數cv2.calcHist(images,channels,mask,histSize,ranges[,hist[,accumulate]])
幫助我們統計一幅圖像灰度值的頻數,返回結果爲包含統計結果的多維數組,其參數理解如下:
- images爲需要分析的圖片,需要以數組格式傳入,即[img]
- channel爲需要分析的通道,仍以數組格式傳入,對於灰度圖爲[0];對於彩色圖,可以是 [0],[1],[2],它們分別對應着通道 B,G,R
- mask,圖像掩模(相當於選框),當要統計整幅圖像的直方圖就把它設爲 None
- histSize,統計組的數目Bins,以數組格式傳入,因爲灰度值爲[0, 255]的離散數據,其範圍並不大,可以直接分爲255組,即[256]
- ranges,像素值範圍,通常爲 [0, 256]
import cv2
img = cv2.imread('image/01.jpg',0)
hist = cv2.calcHist([img],[0],None,[256],[0,256])
hist
>> array([[ 52.], [ 14.],..., [3470.]], dtype=float32)
方法二:使用Numpy的統計函數
使用Numpy中統計直方圖的函數np.histogram()
或np.bincount()
可以完成這項任務,
np.histogram()
通常使用前三個參數,返回一個包含統計結果的數組hist和分組的範圍bins:
- a爲原始數據,要求爲一維數組的格式,可以對圖片使用
img.ravel()
或img.flatten()
將其轉換爲一維數組格式 - bins爲灰度值統計時的組數
- range是所需統計數據的範圍,對整張圖統計時可以直接使用[0, 256],也可以使用更小的範圍進行更精細的觀察
如下案例是將灰度值分爲32組進行統計:
import cv2
import numpy as np
img = cv2.imread('image/01.jpg',0)
hist,bins = np.histogram(img.ravel(), 32, [0,256])
hist
>> array([ 379, 2322, 3149, 3102, 1718, 3379, 3738, 8726, 8409,
11809, 8753, 3094, 2084, 1580, 1620, 2406, 1886, 2399,
3793, 5047, 8013, 11870, 15268, 10583, 12241, 12061, 18225,
9705, 1744, 1517, 3285, 4095], dtype=int64)
bins
>> array([ 0., 8., 16., 24., 32., 40., 48., 56., 64., 72., 80.,
88., 96., 104., 112., 120., 128., 136., 144., 152., 160., 168.,
176., 184., 192., 200., 208., 216., 224., 232., 240., 248., 256.])
np.bincount()
有3個參數,返回一個包含統計結果的數組:
- x爲需要統計的數據,以數組形式傳入,因而爲
img.ravel()
- weight爲與x同形狀的權重值,用於調整統計的比例,在這裏我們不需用
- minlength爲最少的統計組數,這也可以不填
import cv2
import numpy as np
img = cv2.imread('image/01.jpg',0)
hist=np.bincount(img.ravel(), minlength)
hist
>> array([ 52, 14, 18, 37, ..., 76, 77, 3470], dtype=int64)
提示:通常,np.bincount()
比np.histogram
快10倍,opencv的內置方法cv2.calcHist()
方法比np.histogram()
快40倍,因而推薦使用opencv的內置方法進行直方圖的統計
直方圖的繪製
使用Matplotlib中有直方圖繪製函數:matplotlib.pyplot.hist()
它可以直接統計並繪製直方圖。
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('image/01.jpg',0)
plt.figure(figsize=(15,4))
plt.subplot(121), plt.imshow(img, 'gray')
plt.subplot(122), plt.hist(img.ravel(),256,[0,256])
plt.show()
當然,也可以配合cv2.calcHist()
方法來繪製更復雜的直方圖,如:
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('image/01.jpg')
color = ('b','g','r')
plt.figure(figsize=(16,5))
plt.subplot(121), plt.imshow(img[:,:,::-1])
plt.subplot(122)
for i,col in enumerate(color):
histr = cv2.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color = col)
plt.xlim([0,256])
plt.show()
提示:enumerate()
是一種既可以遍歷索引又可以遍歷元素的方法,其可將數組或列表組成一個索引序列方便使用
直方圖均衡化HE
如果拿到了一副整體過亮或過暗的圖片,其像素值在直方圖上分佈很集中,會影響視覺觀察;我們可以通過直方圖均衡化,將其做一個橫向拉伸使其分佈在直方圖的各個位置上
通常情況下這種操作會很大程度上地改善圖像的對比度
OpenCV中的直方圖均衡化函數爲cv2.equalizeHist()
,該函數的輸入圖片僅僅是一副灰度圖像,輸出結果是直方圖均衡化之後的圖像,下邊的代碼是進行直方圖均衡化的案例:
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('image/08.jpg',0)
equ = cv2.equalizeHist(img)
res = np.hstack((img,equ))
plt.figure(figsize=(10,6))
plt.imshow(res, 'gray'), plt.show()
下面簡單推導、分析直方圖均衡化是經歷了一個什麼樣的處理過程,並用Numpy來實現:
-
若原圖灰度值r經過HE變換T後,變爲s,則有:,變換後的函數值應該仍在灰度取值範圍[0, L-1]內,且s應該關於r單調遞增(即原本的亮暗不能顛倒)
-
因而,其概率密度有,用微分可以表示爲:
(該式可以直觀地理解爲變換前後的概率密度函數下的面積相等)
- 我們假設變換後的s服從均勻分佈,即有
- 因而,綜合以上3式,可以解得
-
將其轉換爲離散形式,即爲(L-1)乘以r的累積分佈函數的值
-
對於灰度圖,(L-1)即爲255,而原圖的累積分佈函數可以用頻率代替概率進行統計
如下爲Numpy代碼對上述過程的實現:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('image/08.jpg',0)
plt.figure(figsize=(15,7))
plt.subplot(221), plt.imshow(img,'gray')
# 計算累積分佈圖
hist,bins = np.histogram(img.flatten(),256,[0,256])
cdf = hist.cumsum()
cdf_normalized = cdf * hist.max()/ cdf.max()
plt.subplot(222)
plt.plot(cdf_normalized, color = 'b')
plt.hist(img.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.legend(('cdf','histogram'), loc = 'upper left')
# 構建Numpy掩模數組,cdf爲原數組,當數組元素爲0時,掩蓋(計算時被忽略)
cdf_m = np.ma.masked_equal(cdf,0)
cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
cdf = np.ma.filled(cdf_m,0).astype('uint8')
img2 = cdf[img]
plt.subplot(223), plt.imshow(img2,'gray')
hist,bins = np.histogram(img2.flatten(),256,[0,256])
cdf = hist.cumsum()
cdf_normalized = cdf * hist.max()/ cdf.max()
plt.subplot(224)
plt.plot(cdf_normalized, color = 'b')
plt.hist(img.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.legend(('cdf','histogram'), loc = 'upper left')
plt.show()
五、圖像位運算
在圖像的操作中,往往也會用到邏輯運算(按位運算),包括有:AND
,OR
,NOT
,XOR
等,它們主要針對於二值圖像,常常用來規劃選區(構建掩膜),Opencv提供瞭如下函數來完成這一操作:
- 與運算
cv2.btwise_and(img1, img2, mask)
- 或運算
cv2.bitwise_or(img1, img2, mask)
- 非運算
cv2.bitwise_not(img1, mask)
- 異或運算(相同爲1,不同爲0)
cv2.bitwise_xor(img1, img2, mask)
下面的案例綜合運用了圖像的按位運算,實現了將白色背景的Logo摳圖並添加到背景圖上的效果:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img1 = cv2.imread('image/B.jpeg')
img2 = cv2.imread('image/logo0.jpg')
rows,cols,channels = img2.shape
roi = img1[0:rows, 0:cols]
# 通過閾值二值化操作濾去白色的背景,得到Logo的掩膜
img2gray = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(img2gray, 200, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)
# 通過位運算得到不同的圖像效果
img1_bg = cv2.bitwise_and(roi,roi,mask = mask)
img2_fg = cv2.bitwise_and(img2,img2,mask = mask_inv)
dst = cv2.add(img1_bg, img2_fg)
img1[0:rows, 0:cols ] = dst
plt.imshow(img1[:,:,::-1])
plt.show()
2020年2月25日 本性之初