opencv-圖像的空間域處理之點運算

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

圖像混合

相比於加法,圖像混合操作可能更使用一些,其本質也是加法,但卻對圖片作了不同的加權,給人一種混合或者透明的感覺,其計算公式如下:
g(x)=(1α)f0(x)+αf1(x),α[0,1] g(x)=(1− \alpha ) f_0(x)+ \alpha f_1(x) , \alpha \in [0,1]
在opencv中,可以函數cv2.addWeighted(α, img1, β, img2, γ) 可以按下面的公式對圖片進行混合操作,其具有更靈活的定義:
dst=αimg1+βimg2+γ dst = \alpha·img1 + \beta·img2+\gamma
如下案例即把兩幅圖混合在一起,第一幅圖的權重是 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

  • 根據平均數的定義,有μ=ω1μ1+ω2μ2\mu=\omega_1*\mu_1+\omega_2*\mu_2

  • 在此基礎上求得類間方差,爲:
    g(x)=ω1(μ1μ)2+ω2(μ2μ)2=ω1ω2(μ1μ2)2 \begin{aligned} g(x)&=\omega_1*(\mu_1-\mu)^2+\omega_2*(\mu_2-\mu)^2 \\ &=\omega_1\omega_2*(\mu_1-\mu_2)^2 \end{aligned}

  • 對於每一個閾值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:二值化,與全局閾值時含義相同,即
    dst(x,y)={maxValueif src(x,y)>T(x,y)0otherwise dst(x,y) = \begin{cases} maxValue & if \ src(x,y) > T(x,y) \\ 0 & otherwise \\ \end{cases}

  • cv2.THRESH_BINARY_INV:反向二值化
    dst(x,y)={0if src(x,y)>T(x,y)maxValueotherwise dst(x,y) = \begin{cases} 0 & if \ src(x,y) > T(x,y) \\ maxValue & otherwise \\ \end{cases}

  • 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,則有:s=T(r)s=T(r),變換後的函數值應該仍在灰度取值範圍[0, L-1]內,且s應該關於r單調遞增(即原本的亮暗不能顛倒)

  • 因而,其概率密度有P(s)=P(T(r))P(s)=P(T(r)),用微分可以表示爲:

P(s)=P(r)drdsr=T1(s) P(s)=P(r)\frac{dr}{ds} \Bigg| _{r=T^{-1}(s)}

(該式可以直觀地理解爲變換前後的概率密度函數下的面積相等)

  • 我們假設變換後的s服從均勻分佈,即有

P(s)=1L1 P(s)=\frac{1}{L-1}

  • 因而,綜合以上3式,可以解得

s=T(x)=(L1)0xp(r)dr s=T(x)=(L-1)\int_0^x p(r)dr

  • 將其轉換爲離散形式,即爲(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()

在這裏插入圖片描述

五、圖像位運算

在圖像的操作中,往往也會用到邏輯運算(按位運算),包括有:ANDORNOTXOR 等,它們主要針對於二值圖像,常常用來規劃選區(構建掩膜),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日 本性之初

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