1.?繪制圖像輪廓
1.1 什么是輪廓
????????輪廓是一系列相連的點組成的曲線,代表了物體的基本外形。相對于邊緣,輪廓是連續的,邊緣不一定連續,如下圖所示。其實邊緣主要是作為圖像的特征使用,比如可以用邊緣特征可以區分臉和手,而輪廓主要用來分析物體的形態,比如物體的周長和面積等,可以說邊緣包括輪廓。
1.2?尋找輪廓
????????在OpenCV中,使用cv2.findContours()來進行尋找輪廓,其原理過于復雜,這里只進行一個簡單的介紹,具體的實現原理可參考:https://zhuanlan.zhihu.com/p/107257870
????????尋找輪廓需要將圖像做一個二值化處理,并且根據圖像的不同選擇不同的二值化方法來將圖像中要繪制輪廓的部分置為白色,其余部分置為黑色。也就是說,我們需要對原始的圖像進行灰度化、二值化的處理,令目標區域顯示為白色,其他區域顯示為黑色,如下圖所示。
????????之后,對圖像中的像素進行遍歷,當一個白色像素相鄰(上下左右及兩條對角線)位置有黑色像素存在或者一個黑色像素相鄰(上下左右及兩條對角線)位置有白色像素存在時,那么該像素點就會被認定為邊界像素點,輪廓就是有無數個這樣的邊界點組成的。
????????下面具體介紹一下cv2.findContours()函數,其函數原型為:
????????contours,hierarchy = cv2.findContours(image,mode,method)
-
contours:表示獲取到的輪廓點的列表。檢測到有多少個輪廓,該列表就有多少子列表,每一個子列表都代表了一個輪廓中所有點的坐標。
-
hierarchy:表示輪廓之間的關系。對于第i條輪廓,hierarchy[i][0], hierarchy[i][1] , hierarchy[i][2] , hierarchy[i][3]分別表示其后一條輪廓、前一條輪廓、(同層次的第一個)子輪廓、父輪廓的索引(如果沒有相應的輪廓,則對應位置為-1)。該參數的使用情況會比較少。
-
image:表示輸入的二值化圖像。
-
mode:表示輪廓的檢索模式。
-
method:輪廓的表示方法。
1.2.1?mode參數
????????mode參數共有四個選項分別為:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。
????????RETR_LIST:表示列出所有的輪廓。并且在hierarchy里的輪廓關系中,每一個輪廓只有前一條輪廓與后一條輪廓的索引,而沒有父輪廓與子輪廓的索引。
????????RETR_EXTERNAL:表示只列出最外層的輪廓。并且在hierarchy里的輪廓關系中,每一個輪廓只有前一條輪廓與后一條輪廓的索引,而沒有父輪廓與子輪廓的索引。
????????RETR_CCOMP:表示列出所有的輪廓。并且在hierarchy里的輪廓關系中,輪廓會按照成對的方式顯示。
????????RETR_TREE:表示列出所有的輪廓。并且在hierarchy里的輪廓關系中,輪廓會按照樹的方式顯示,其中最外層的輪廓作為樹根,其子輪廓是一個個的樹枝。
1.2.2 method參數
????????輪廓近似方法。決定如何簡化輪廓點的數量。
????????method參數有三個選項:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。
????????CHAIN_APPROX_NONE表示將所有的輪廓點都進行存儲;
????????CHAIN_APPROX_SIMPLE表示只存儲有用的點,比如直線只存儲起點和終點,四邊形只存儲四個頂點,默認使用這個方法;
????????CHAIN_APPROX_TC89_L1表示使用Teh-Chin鏈逼近算法進行輪廓逼近。這種方法使用的是Teh-Chin鏈碼,它是一種邊緣檢測算法,可以對輪廓進行逼近,減少輪廓中的冗余點,從而更加準確地表示輪廓的形狀。CHAIN_APPROX_TC89_L1是一種較為精確的輪廓逼近方法,適用于需要較高精度的輪廓表示的情況。
????????對于mode和method這兩個參數來說,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE這兩個選項。
1.2.3 繪制輪廓
????????輪廓找出來后,其實返回的是一個輪廓點坐標的列表,因此我們需要根據這些坐標將輪廓畫出來,因此就用到了繪制輪廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
-
image:原始圖像,一般為單通道或三通道的 numpy 數組。
-
contours:包含多個輪廓的列表,每個輪廓本身也是一個由點坐標構成的二維數組(numpy數組)。
-
contourIdx:要繪制的輪廓索引。如果設為
-1
,則會繪制所有輪廓。 -
color:繪制輪廓的顏色,可以是 BGR 值或者是灰度值(對于灰度圖像)。
-
thickness:輪廓線的寬度,如果是正數,則畫實線;如果是負數,則填充輪廓內的區域。
? ? ? ? 示例:
import cv2 as cvimg = cv.imread('./images/num.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 二值化:OTSU+反閾值法
_, img_binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
# 查找輪廓:cv.findContours(img,mode,method)
contours, h = cv.findContours(img_binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# 繪制輪廓:cv.drawContours(img.contours,-1,(b,g,r),2,thickness)
cv.drawContours(img, contours, -1, (0, 0, 255), 2)
cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()
2.?凸包特征檢測
????????在進行凸包特征檢測之前,首先要了解什么是凸包。通俗的講,凸包其實就是將一張圖片中物體的最外層的點連接起來構成的凸多邊形,它能包含物體中所有的內容。
????????我們以點集來舉例,假如有這么一些點,其分布如下圖所示:
????????那么經過凸包檢測并繪制之后,其結果應該如下圖所示:
????????可以看到,原圖像在經過凸包檢測之后,會將最外圍的幾個點進行連接,剩余的點都在這些點的包圍圈之內。那么凸包檢測到底是怎么檢測出哪些點是最外圍的點呢?
????????我們還是以上面的點集為例,假設我們知道這些點的坐標,那么我們就可以找出處于最左邊和最右邊的點,如下圖所示:
????????接著將這兩個點連接,并將點集分為上半區和下半區,我們以上半區為例:
????????找到上面這些點離直線最遠的點,其中,這條直線由于有兩個點的坐標,所以其表示的直線方程是已知的,并且上面的點的坐標也是已知的,那么我們就可以根據點到直線的距離公式來進行計算哪個點到直線的距離最遠,假設直線的方程為:A x+B y+C=0,那么點到直線的距離公式為:
????????然后我們就可以得到距離這條線最遠的點,將其與左右兩點連起來,并分別命名為y1和y2,如下圖所示:
????????然后分別根據點的坐標求出y1和y2的直線方程,之后將上半區的每個點的坐標帶入下面公式中:
????????當d=0時,表明該點在直線上;當d>0時,表明點在直線的上方,在這里就代表該點在上圖所圍成的三角形的外面,也就意味著該三角形并沒有完全包圍住上半區的所有點,需要重新尋找凸包點;當d<0時,表明點在直線的下方,在這里就代表該點在上圖所圍成的三角形的里面,也就代表著這類點就不用管了。
????????當出現d>0時,我們需要將出現這種結果的兩個計算對象:某點和y1或y2這條線標記,并在最后重新計算出現這種現象的點集到y1或y2的距離來獲取新的凸包點的坐標。在本例子中,也就是如下圖所示的點和y2這條直線:
????????由于本例子中只有這一個點在這個三角形之外,所以毫無疑問的它就是一個凸包點,因此直接將它與y2直線的兩個端點相連即可。當有很多點在y2直線外時,就需要計算每個點到y2的距離,然后找到離得最遠的點與y2構建三角形,并重新計算是否還有點在該三角形之外,如果沒有,那么這個點就是新的凸包點,如果有,那就需要重復上面的步驟,直到所有的點都能被包圍住,那么構建直線的點就是凸包點。這是上半區尋找凸包點的過程,下半區尋找凸包點的思路與此一模一樣,只不過是需要篩選d<0(也就是點在直線的下方)的點,并重新構建三角形,尋找新的凸包點。
????????上面的過程都是基于我們知道點的坐標進行的,實際上,對于未經處理的圖像,我們無法直接獲取點的坐標。特別是對于彩色圖像,我們需要將其轉換為二值圖像,并使用輪廓檢測技術來獲取輪廓邊界的點的坐標。然后,我們才能進行上述尋找凸包點的過程。因此,在處理圖像時,我們需要將彩色圖像轉換為二值圖像,并通過輪廓檢測技術來獲取輪廓邊界的點的坐標,然后才能進行凸包點的尋找過程。
2.1 獲取凸包點
????????cv2.convexHull(points, hull=None, clockwise=False, returnPoints=True)
-
points
:輸入參數,圖像的輪廓 -
hull
(可選):輸出參數,用于存儲計算得到的凸包頂點序列,如果不指定,則會創建一個新的數組。 -
clockwise
(可選):布爾值,如果為True,則計算順時針方向的凸包,否則默認計算逆時針方向的凸包。 -
returnPoints
(可選):布爾值,如果為True(默認),則函數返回的是原始點集中的點構成的凸包頂點序列;如果為False,則返回的是凸包頂點對應的邊界框內的索引。
2.2 繪制凸包
????????cv2.polylines(image, pts, isClosed, color, thickness=1)
-
image
:要繪制線條的目標圖像,它應該是一個OpenCV格式的二維圖像數組(如numpy數組)。 -
pts
:一個二維 numpy 數組,每個元素是一維數組,代表一個多邊形的一系列頂點坐標。 -
isClosed
:布爾值,表示是否閉合多邊形,如果為 True,會在最后一個頂點和第一個頂點間自動添加一條線段,形成封閉的多邊形。 -
color
:線條顏色,可以是一個三元組或四元組,分別對應BGR或BGRA通道的顏色值,或者是灰度圖像的一個整數值。 -
thickness
(可選):線條寬度,默認值為1。
? ? ? ? 示例:
import cv2 as cvimg = cv.imread('./images/tu.png')
# 取輪廓點
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
_, img_binary = cv.threshold(gray, 127, 255, cv.THRESH_OTSU)
contours, h = cv.findContours(img_binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
contours1, h1 = cv.findContours(img_binary, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
print(contours1)
# 獲取凸包點:cv2.convexHull(points, hull=None, clockwise=False, returnPoints=True)
hull = cv.convexHull(contours[0])
# print(hull)
# 繪制凸包
cv.polylines(img, [hull], True, (0, 0, 255), 2)
cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()
3.?圖像輪廓特征查找
3.1 外接矩形
????????形狀的外接矩形有兩種,如下圖,綠色的叫外接矩形,表示不考慮旋轉并且能包含整個輪廓的矩形。其中,外接矩形可根據獲得到的輪廓坐標中最上、最下、最左、最右的點的坐標來繪制外接矩形,也就是下圖中的綠色矩形。
? ? ? ? 示例:
import cv2 as cvimg = cv.imread('./images/num.png')
# 讀取灰度圖
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 二值化處理
_, img_binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
# cv.imshow('img_binary', img_binary)# 查找輪廓
contours, h = cv.findContours(img_binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)# 繪制輪廓
cv.drawContours(img, contours, -1, (0, 255, 0), 3)# 遍歷輪廓(該圖有三個輪廓分別對應圖中的三個數字)
for cont in contours:x, y, w, h = cv.boundingRect(cont)# print(x, y, w, h)cv.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()
3.2 最小外接矩形
????????最小外接矩形就是上圖所示的藍色矩形,尋找最小外接矩形使用的算法叫做旋轉卡殼法。下面簡單說明一下旋轉卡殼法的思路:
在上一章節中,我們了解到了凸包的概念,凸包就是一個點集的凸多邊形,它是這個點集所有點的凸殼,點集中所有的點都處于凸包里,構成凸包的點我們叫做凸包點。而旋轉卡殼法就是基于凸包點進行的,
旋轉卡殼法有一個很重要的前提條件:對于多邊形P的一個外接矩形存在一條邊與原多邊形的邊共線。假設某凸包圖如下所示:
????????根據前提條件,上面的凸多邊形的最小外接矩形與凸多邊形的某條邊是共線的。因此我們只需要以其中的一條邊為起始邊,然后按照逆時針方向計算每個凸包點與起始邊的距離,并將距離最大的點記錄下來。
????????如上圖所示,我們首先以a、b兩點為起始邊,并計算出e點離起始邊最遠,那么e到起始邊的距離就是一個矩形的高度,因此我們只需要再找出矩形的寬度即可。對于矩形的最右邊,以向量為基準,然后分別計算凸包點在向量
上的投影的長度,投影最長的凸包點所在的垂直于起始邊的直線就是矩形最右邊所在的直線。
????????如上圖所示,d點就是在向量上投影最長的凸包點,那么通過d點垂直于直線ab的直線就是矩形的右邊界所在的直線。矩形的左邊界的也是這么計算的,不同的是使用的向量不是\
而是
。
????????如上圖所示,h點垂直于ab的直線就是以ab為起始邊所計算出來的矩形所在的左邊界所在的直線。其中矩形的高就是e點到直線ab的距離,矩形的寬是h點在向量上的投影加上d點在向量
上的投影減去ab的長度,即:
????????于是我們就有了以ab為起始邊所構成的外接矩形的寬和高,這樣就可以得到該矩形的面積。然后再以bc為起始邊,并計算其外接矩形的面積。也就是說凸多邊形有幾個邊,就要構建幾次外接矩形,然后找到其中面積最小的矩形作為該凸多邊形的最小外接矩形。
????????在OpenCV中,可以直接使用cv2.minAreaRect()來獲取最小外接矩形,該函數只需要輸入一個參數,就是凸包點的坐標,然后會返回最小外接矩形的中心點坐標、寬高以及旋轉角度。通過返回的內容信息,即可繪制凸多邊形的的最小外接矩形。
????????需要使用到的API說明:
????????contours, hierarchy = cv2.findContours(image_np_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE),contours為二值圖像上查找所有的外部輪廓
????????rect = cv2.minAreaRect(cnt),傳入的cnt參數為contours中的輪廓,可以遍歷contours中的所有輪廓,然后計算出每個輪廓的小面積外接矩形,rect 是計算輪廓最小面積外接矩形:rect 結構通常包含中心點坐標 (x, y)
、寬度 width
、高度 height
和旋轉角度 angle
????????cv2.boxPoints(rect).astype(int)cv2.boxPoints(rect)返回 是一個形狀為 4行2列的數組,每一行代表一個點的坐標(x, y),順序按照逆時針或順時針方向排列,將最小外接矩形轉換為邊界框的四個角點,并轉換為整數坐標。
????????cv2.drawContours(image, contours, contourIdx, color, thickness)
-
image:原圖像,一般為 numpy 數組,通常為灰度或彩色圖像。
-
contours:一個包含多個輪廓的列表,可以用上一個api得到的 [box]
-
contourIdx:要繪制的輪廓索引。如果設置為
-1
,則繪制所有輪廓。 -
color:輪廓的顏色,可以是 BGR 顏色格式的三元組,例如
(0, 0, 255)
表示紅色。 -
thickness:輪廓線的粗細,如果是正數,則繪制實線;如果是 0,則繪制輪廓點;如果是負數,則填充輪廓內部區域。
????????示例:
import cv2 as cv
import numpy as npimg = cv.imread('./images/num.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 二值化:OTSU+反閾值法
_, img_binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)# 查找輪廓:找最外層+最有用的
contours, h = cv.findContours(img_binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# 循環,遍歷每一個輪廓,查找最小外接矩陣
for con in contours:# 返回最小外接矩陣中心點坐標、寬高、旋轉角度rect = cv.minAreaRect(con) # rect中包含了centerpoint,w、h、angle# 返回最小外接矩形的四個頂點坐標points = cv.boxPoints(rect).astype(np.int32)# 繪制最小外接矩形cv.drawContours(img, [points], -1, (0, 255, 0), 2)print(rect)cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()
3.3 最小外接圓
????????尋找最小外接圓使用的算法是Welzl算法。Welzl算法基于一個定理:希爾伯特圓定理表明,對于平面上的任意三個不在同一直線上的點,存在一個唯一的圓同時通過這三個點,且該圓是最小面積的圓(即包含這三個點的圓中半徑最小的圓,也稱為最小覆蓋圓)。
????????進一步推廣到任意 n 個不在同一圓上的點,總存在一個唯一的最小覆蓋圓包含這 n 個點。
????????若已經存在平面上互不共線(或共圓)的 n 個點,并確定了它們的最小覆蓋圓,那么添加第 n+1 個點,并且要求這個點不在原來的最小覆蓋圓內(即在圓外),為了使新的包含 n+1 個點的最小覆蓋圓的半徑增大,新加入的點必須位于由原 n 個點確定的最小覆蓋圓的邊界上(即圓周上)。這是因為,如果新點在原最小覆蓋圓的內部,顯然不會影響最小覆蓋圓;如果新點在原最小覆蓋圓之外但不在圓周上,那么通過新點和至少兩個原有圓上的點可以構造出一個更大的圓,這個圓必然比原最小覆蓋圓更大,因此不是包含所有 n+1 個點的最小覆蓋圓。所以,按照這一邏輯,當第 n+1 個點在原 n 個點的最小覆蓋圓外時,確實這個點會位于包含所有 n+1 個點的新最小覆蓋圓的圓周上。
????????有了這個定理,就可以先取3個點建立一個圓(不共線的三個點即可確定一個圓,如果共線就取距離最遠的兩個點作為直徑建立圓),然后遍歷剩下的所有點,對于遍歷到的點P來說:如果該點在圓內,那么最小覆蓋圓不變。
????????如果該點在圓外,根據上述定理,該點一定在想要求得的最小覆蓋圓的圓周上,又因為三個點才能確定一個圓,所以需要枚舉P點之前的點來找其余的兩個點。當找到與P點組成的圓能夠將所有點都包含在圓內或圓上,該圓就是這些點的最小外接圓。
????????在OpenCV中,可以直接使用cv2.minEnclosingCircle()來獲取最小外接圓,該函數只需要輸入一個參數,就是要繪制最小外接圓的點集的坐標,然后會返回最小外接圓的圓心坐標與半徑。通過該函數返回的內容信息即可繪制某點集的最小外接圓。如下圖所示:
????????需要使用的API說明
????????cv2.minEnclosingCircle(points) -> (center, radius)
????????參數說明:
-
points
:輸入參數圖片輪廓數據
????????返回值:
-
center
:一個包含圓心坐標的二元組(x, y)
。 -
radius
:浮點數類型,表示計算得到的最小覆蓋圓的半徑。
????????cv2.circle(img, center, radius, color, thickness)
-
img
:輸入圖像,通常是一個numpy數組,代表要繪制圓形的圖像。 -
center
:一個二元組(x, y)
,表示圓心的坐標位置。 -
radius
:整型或浮點型數值,表示圓的半徑長度。 -
color
:顏色標識,可以是BGR格式的三元組(B, G, R)
,例如(255, 0, 0)
表示紅色。 -
thickness
:整數,表示圓邊框的寬度。如果設置為-1
,則會填充整個圓。
示例:
import cv2 as cv
import numpy as npimg = cv.imread('./images/num.png')
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 二值化
_, binary = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)# 獲取輪廓值
contours, h = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)for cont in contours:(x, y), r = cv.minEnclosingCircle(cont)x, y, r = np.int_(x), np.int_(y), np.int_(r)cv.circle(img, (x, y), r, (0, 255, 0), 2, cv.LINE_AA)cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()
4.?直方圖均衡化
4.1 什么是直方圖
直方圖是對數據進行統計的一種方法,并且將統計值組織到一系列實現定義好的 bin 當中。其中, bin 為直方圖中經常用到的一個概念,可以譯為 “直條” 或 “組距”,其數值是從數據中計算出的特征統計量,這些數據可以是諸如梯度、方向、色彩或任何其他特征。
4.2 繪制直方圖
????????就是以像素值為橫坐標,像素值的個數為縱坐標繪制一個統計圖。
????????hist=cv2.calcHist(images, channels, mask, histSize, ranges)
-
images
:輸入圖像列表,可以是一幅或多幅圖像(通常是灰度圖像或者彩色圖像的各個通道)。 -
channels
:一個包含整數的列表,指示在每個圖像上計算直方圖的通道編號。如果輸入圖像是灰度圖,它的值就是 [0];如果是彩色圖像的話,傳入的參數可以是 [0],[1],[2] 它們分別對應著通道 B,G,R。 -
mask
(可選):一個與輸入圖像尺寸相同的二值掩模圖像,其中非零元素標記了參與直方圖計算的區域,None為全部計算。 -
histSize
:一個整數列表,也就是直方圖的區間個數(BIN 的數目)。用中括號括起來,例如:[256]。 -
ranges
:每維數據的取值范圍,它是一個二維列表,每一維對應一個通道的最小值和最大值,例如對灰度圖像可能是[0, 256]
。
????????返回值hist 是一個長度為255的數組,數組中的每個值表示圖像中對應灰度等級的像素計數
????????minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(hist),獲取直方圖的最小值、最大值及其對應最小值的位置索引、最大值的位置索引
????????cv2.line(img, pt1, pt2, color, thickness)
-
img:原始圖像,即要在上面畫線的numpy數組(一般為uint8類型)。
-
pt1 和 pt2:分別為線段的起點和終點坐標,它們都是元組類型,例如
(x1, y1)
和(x2, y2)
分別代表線段兩端的橫縱坐標。 -
color:線段的顏色,通常是一個包含三個元素的元組
(B, G, R)
表示BGR色彩空間的像素值,也可以是灰度圖像的一個整數值。 -
thickness:線段的寬度,默認值是1,如果設置為負數,則線寬會被填充。
示例:
import cv2 as cv
import numpy as npimg = cv.imread('./images/lvbo.png')
# cv.imshow('img', img)# 統計彩色圖中藍色通道中的所有像素中0-255出現的頻率
hist = cv.calcHist(img, [0], None, [256], [0, 256])
# print(hist, hist.shape)
minval, maxval, mini, maxi = cv.minMaxLoc(hist)
print(maxval)
# print(minval, maxval, mini, maxi)
# 初始化一個圖像,用于繪制直方圖
backg = np.zeros([256, 256, 3], np.uint8)
# 指定直方圖最大高度
hmax = int(0.9 * 256)
# 遍歷像素個數
for h in range(256):# 高度歸一化hmax的高度上h_g = int(hist[h].item() * hmax / maxval)cv.line(backg, (h, 256), (h, 256 - h_g), (255, 0, 0), 1)cv.imshow('hist', backg)
cv.waitKey(0)
cv.destroyAllWindows()
4.3 直方圖均衡化
????????一副效果好的圖像通常在直方圖上的分布比較均勻,直方圖均衡化就是用來改善圖像的全局亮度和對比度。
????????如果一幅圖像整體很亮,那所有的像素值的取值個數應該都會很高,所以應該把它的直方圖做一個橫向拉伸,就可以擴大圖像像素值的分布范圍,提高圖像的對比度
????????通俗的講,就是遍歷圖像的像素統計出灰度值的個數、比例與累計比例,并重新映射到0-255范圍(也可以是其他范圍)內,其實從觀感上就可以發現,下面兩幅圖中前面那幅圖對比度不高,偏灰白。
????????可以看到均衡化后圖片的亮度和對比度效果明顯好于原圖。
4.3.1?自適應直方圖均衡化
????????自適應直方圖均衡化(Adaptive Histogram Equalization, AHE),通過調整圖像像素值的分布,使得圖像的對比度和亮度得到改善。具體過程如下所示:
????????假設有一個3*3的圖像,其灰度圖的像素值如上圖所示,現在我們要對其進行直方圖均衡化,首先就是統計其每個像素值的個數、比例以及其累計比例。如下圖所示。
????????接下來我們就要進行計算,就是將要縮放的范圍(通常是縮放到0-255,所以就是255-0)乘以累計比例,得到新的像素值,并將新的像素值放到對應的位置上,比如像素值為50的像素點,將其累計比例乘以255,也就是0.33乘以255得到84.15,取整后得到84,并將84放在原圖像中像素值為50的地方,像素值為100、210、255的計算過程類似,最終會得到如下圖所示的結果,這樣就完成了最基本的直方圖均衡化的過程。
????????dst = cv.equalizeHist(imgGray):imgGray為需要直方圖均衡化的灰度圖返回值為處理后的圖像。
? ? ? ? 示例:
import cv2 as cvimg = cv.imread('images/zhifang.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 均衡化
img_n = cv.equalizeHist(gray)cv.imshow('gray', gray)
cv.imshow('img_n', img_n)
cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(0)
????????該方法適用于圖像的灰度分布不均勻,且灰度分布集中在更窄的范圍,圖像的細節不夠清晰且對比度較低的情況,然而,傳統的直方圖均衡化方法會引入噪聲,并導致圖像中出現過度增強的區域。這是因為直方圖均衡化方法沒有考慮到圖像的局部特征和全局對比度的差異。
4.3.2 對比度受限的自適應直方圖均衡化
????????很明顯,因為全局調整亮度和對比度的原因,臉部太亮,大部分細節都丟失了。自適應均衡化就是用來解決這一問題的:它在每一個小區域內(默認8×8)進行直方圖均衡化。當然,如果有噪點的話,噪點會被放大,需要對小區域內的對比度進行了限制,所以這個算法全稱叫:對比度受限的自適應直方圖均衡化(Contrast Limited Adaptive Histogram Equalization, CLAHE)。
????????其主要步驟為:
-
圖像分塊(Tiling):圖像首先被劃分為多個不重疊的小塊(tiles)。這樣做的目的是因為在全局直方圖均衡化中,單一的直方圖無法反映圖像各個局部區域的差異性。通過局部處理,AHE能夠更好地適應圖像內部的不同光照和對比度特性。(tiles 的 大小默認是 8x8)
-
計算子區域直方圖:對于每個小塊,獨立計算其內部像素的灰度直方圖。直方圖反映了該區域內像素值的分布情況。
-
子區域直方圖均衡化:對每個小塊的直方圖執行直方圖均衡化操作。這涉及重新分配像素值,以便在整個小塊內更均勻地分布。均衡化過程會增加低頻像素的數量,減少高頻像素的數量,從而提高整個小塊的對比度。
-
對比度限制(Contrast Limiting):如果有噪聲的話,噪聲會被放大。為了防止過大的對比度增強導致噪聲放大,出現了限制對比度自適應直方圖均衡化(CLAHE)。CLAHE會在直方圖均衡化過程中引入一個對比度限制參數。當某一小塊的直方圖在均衡化后出現極端值時,會對直方圖進行平滑處理(使用線性或非線性的鉗制函數),確保對比度增強在一個合理的范圍內。
-
重采樣和鄰域像素融合:由于小塊之間是不重疊的,直接拼接經過均衡化處理的小塊會產生明顯的邊界效應。因此,在CLAHE中通常采用重采樣技術來消除這種效應,比如通過雙線性插值將相鄰小塊的均衡化結果進行平滑過渡,使最終圖像看起來更為自然和平滑。
-
合成輸出圖像:將所有小塊均衡化后的結果整合在一起,得到最終的自適應直方圖均衡化后的圖像。
????????clahe = cv2.createCLAHE(clipLimit=None, tileGridSize=None)
-
clipLimit(可選):對比度限制參數,用于控制直方圖均衡化過程中對比度增強的程度。如果設置一個大于1的值(如2.0或4.0),CLAHE會限制對比度增強的最大程度,避免過度放大噪聲。如果不設置,OpenCV會使用一個默認值。
-
tileGridSize(可選):圖像分塊的大小,通常是一個包含兩個整數的元組,如
(8, 8)
,表示將圖像劃分成8x8的小塊進行獨立的直方圖均衡化處理。分塊大小的選擇會影響到CLAHE的效果以及處理速度。
創建CLAHE對象后,可以使用 .apply()
方法對圖像進行CLAHE處理:
img=clahe.apply(image)
-
image:要均衡化的圖像。
-
img均衡后的圖像
示例:
import cv2 as cvimg = cv.imread('images/zhifang.png', cv.IMREAD_GRAYSCALE)
cv.imshow('img', img)img_e = cv.equalizeHist(img) # 自適應,全局亮度調整
cv.imshow('img_e', img_e)clahe = cv.createCLAHE(2.0, (8, 8)) # 受限的,局部:8x8
# 調用apply方法,返回受限的自適應直方圖均衡化的圖像
img_l = clahe.apply(img)
cv.imshow('img_l', img_l)cv.waitKey(0)
cv.destroyAllWindows()