《Practical Python and OpenCV》學習記錄 -2

書本中接下來介紹了一些基本更加高級概念和操作: 直方圖,歸一化,平滑化,模糊化,邊緣檢測等.

4.Histogram(直方圖):

 直方圖:描述了在圖像中像素值大小的分佈狀態.

 計算函數:cv2.calcHist(images,channels,mask,histSize,ranges)

 channels:是一個列表,表示通道索引.如要計算灰度圖的直方圖,則該參數爲[0],如果要計算三個通道的則爲[0,1,2]

  mask:若給定了mask,則計算在mask中的直方圖,若爲None則計算全圖的

 histSize:也是一個列表,表示每一個通道我們需要使用的區間大小,比如灰度圖的,可以使用[32],則以32以及倍數劃分了256.

 ranges:像素值大小的範圍,如RGB的爲[0,255]

     灰度圖像的直方圖

image = cv2.imread(args["image"])
image_gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
hist = cv2.calcHist([image], [0], None, [256], [0,255])
plt.figure()
plt.title("the histogram of gray image")
plt.xlabel("the bins")
plt.ylabel("number of Pixels")
plt.xlim([0,256])
plt.plot(hist)
plt.show()

 

  顏色圖的直方圖


channels = cv2.split(image)
colors = ("b", "g", "r")
for (channel, color) in zip(channels, colors):
    hist = cv2.calcHist([channel], [0], None, [256], [0, 256])
    plt.plot(hist, color = color)
    plt.xlim([0, 256])
plt.show()

同時計算多通道的直方圖,舉個例子說明一下它的意義,我們需要知道一張圖裏面有多少個像素點是B=20,G=30,這個時候就需要計算B和G通道的直方圖.


channels = cv2.split(image)
fig = plt.figure()
ax = fig.add_subplot(131)
hist = cv2.calcHist((channels[1], channels[0]), (0, 1), None,
    [32, 32], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation = "nearest")
ax.set_title("2D Color Histogram for G and B")
plt.colorbar(p)

ax = fig.add_subplot(132)
hist = cv2.calcHist((channels[1], channels[2]), [0, 1], None,
    [32, 32], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation = "nearest")
ax.set_title("2D Color Histogram for G and R")
plt.colorbar(p)

ax = fig.add_subplot(133)
hist = cv2.calcHist((channels[0], channels[2]), [0, 1], None,
    [32, 32], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation = "nearest")
ax.set_title("2D Color Histogram for B and R")
plt.colorbar(p)

   對於計算三通道的直方圖也是類似的,意義也是類似於計算二通道的.

直方圖均衡化:提高全圖的對比度,直方圖均衡化用於在灰度圖之中,

  實現該過程使用cv2.equalizeHist(image)

image = cv2.imread(args["image"])
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

eq = cv2.equalizeHist(image)

cv2.imshow("Histogram Equalization", np.hstack([image, eq]))

可以看出均衡化之後,亮的更加亮了,暗的更加暗了.

直方圖加掩模:計算在感興趣的mask之中的直方圖

#define a function to draw the histogram graph
def plot_histogram(image, title, mask = None):
    chans = cv2.split(image)
    colors = ("b", "g", "r")
    plt.figure()
    plt.title(title)
    plt.xlabel("Bins")
    plt.ylabel("# of Pixels")

    for (chan, color) in zip(chans, colors):
        hist = cv2.calcHist([chan], [0], mask, [256], [0, 256])
        plt.plot(hist, color = color)
        plt.xlim([0, 256])

mask = np.zeros(image.shape[0:2], dtype = "uint8")
cv2.rectangle(mask, (15, 15), (130, 100), 255, -1)
cv2.imshow("Mask", mask)

masked = cv2.bitwise_and(image, image, mask = mask)
cv2.imshow("Applying the Mask", masked)
cv2.waitKey(0)
plot_histogram(image, "Histogram for Masked Image", mask = mask)

         這是天空部分的三個通道的直方圖

5.平滑化和模糊化

模糊化(blurring):可以用來去噪等.

average blur:採用函數cv2.blur(image, (3, 3)),第一個參數爲需要操作的圖像,第二個參數爲卷積核,即要取平均的區域.

blurred = np.hstack([
   cv2.blur(image, (3, 3)),
   cv2.blur(image, (5, 5)),
   cv2.blur(image, (7, 7))])
cv2.imshow("Averaged", blurred)
cv2.waitKey(0)

可以看出,隨着卷積核的增大,圖片越來越模糊了.

Gaussian blur:採用函數cv2.GaussianBlur(image, (3, 3), 0), 第二個參數爲卷積核大小,第三個參數爲高斯分佈的標準差.高斯模糊化給每個卷積核中的值附加了一個權重,來計算需要替代的值.

blurred = np.hstack([
   cv2.GaussianBlur(image, (3, 3), 0),
   cv2.GaussianBlur(image, (5, 5), 0),
   cv2.GaussianBlur(image, (7, 7), 0)])
cv2.imshow("Gaussian", blurred)
cv2.waitKey(0)

高斯模糊比均值模糊看起來更加的自然一些.

median blur:採用函數cv2.medianBlur(image, 3),第二個參數是卷積核的大小.該方法用來去除椒鹽噪聲,讓圖片失去細節.

bilateral blur:採用函數cv2.bilateralFilter(image, 5, 21, 21),第二參數是卷積核的大小;第三個參數爲第一個高斯分佈的標準差,用來確定空間的;第四個參數爲第二個高斯分佈的標準差,用於與(x,y)強度相近似的點確定像素點的強度。兩者的結合既能夠保存了圖像的邊緣,也能夠去除噪聲。

blurred = np.hstack([
   cv2.bilateralFilter(image, 5, 21, 21),
   cv2.bilateralFilter(image, 7, 31, 31),
   cv2.bilateralFilter(image, 9, 41, 41)])
cv2.imshow("Bilateral", blurred)

6.門限操作(thresholding)

simple thresholding(簡單的門限操作):讓灰度圖像變成二值化的圖像。

利用函數(T, thresh) = cv2.threshold(blurred, 155, 255, cv2.THRESH_BINARY),第一個參數是需要二值化的圖像;第二個參數是門限;第三個參數是超過門限之後賦予的新值;第四個參數是代表正常二值化還是需要反轉二值化,cv2.THRESH_BINARY表示正常二值化,即小於門限則輸出的爲0,而cv2.THRESH_BINARY_INV表示反轉的二值化,即小於門限則輸出爲1。返回的參數T爲使用的門限,thresh爲返回處理後的圖像。

image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#before thresholding, blurring the image by GaussianBlur
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Image", image)

(T, thresh) = cv2.threshold(blurred, 155, 255, cv2.THRESH_BINARY)
cv2.imshow("Threshold Binary", thresh)

(T, threshInv) = cv2.threshold(blurred, 155, 255, cv2.THRESH_BINARY_INV)
cv2.imshow("Threshold Binary Inv", threshInv)

cv2.imshow("Coins", cv2.bitwise_and(image, image, mask = 
    threshInv))

 

右上角爲正常二值化,左下角爲反轉二值化,右下角爲採用反轉二值化作爲mask作用在原圖上來畫出硬幣。

adaptive thresholding(自適應門限操作):自動挑選門限值讓灰度圖像變成二值化的圖像,相比於簡單的自己挑選門限而言,它的靈活性更加強;自適應的門限是根據像素點周圍的幾個值來來確定操作該像素的門限,所以在一張圖處理中可以存在很多的門限值;可以很好的解決圖像像素值差異太大,單個門限很難操作的問題。

使用函數,thresh = cv2.adaptiveThreshold(blurred, 255,cv2.ADAPTIVE_THRESH_MEAN_C,

cv2.THRESH_BINARY_INV, 11, 4)

第二個參數爲:超過門限後賦予的新值

第三個參數爲:採用的自適應的門限計算方式,cv2.ADAPTIVE_THRESH_MEAN_C代表取均值操作,cv2.ADAPTIVE_THRESH_GAUSSIAN_C代表高斯操作,與blur中說到的高斯操作類似。

第四個參數爲:正常二值化,還是反轉二值化,與simple threshold中的用法一樣。

第五個參數爲:卷積核的大小

第六個參數爲:用來微調(fine_tune)計算出來的門限值,減去這個常數C得到新的門限值。

image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#before adaptive thresholding, blurring by Gaussian
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Image", image)

thresh = cv2.adaptiveThreshold(blurred, 255, 
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 4)
cv2.imshow("Mean Thresh", thresh)

thresh = cv2.adaptiveThreshold(blurred, 255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 3)
cv2.imshow("Gaussian Thresh", thresh)

在實際的操作中,可以多調節調節卷積核的大小和用來微調的那個常數C,有些時候改變一些會得到意想不到的結果。

otsu and riddler-calvard:這也是一個自動計算門限值的方法,他們的原理是假設圖像的灰度直方圖中有兩個峯值,而生成的這個門限用來區分出這兩個峯值。

 採用的函數是:mahotas庫中的函數,T = mahotas.thresholding.otsu(blurred)返回的值也是計算出的門限;還有另外一個方法T = mahotas.thresholding.rc(blurred),返回的值也是計算出的門限。

image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Image", image)

T = mahotas.thresholding.otsu(blurred)
print("Otsu's threshold: {}".format(T))
thresh = image.copy()
thresh[thresh > T] = 255
thresh[thresh < T] = 0
thresh =cv2.bitwise_not(thresh)
cv2.imshow("Otsu", thresh)

T = mahotas.thresholding.rc(blurred)
print("Riddler-Calvard: {}".format(T))
thresh = image.copy()
thresh[thresh > T] = 255
thresh[thresh < T] = 0
thresh =cv2.bitwise_not(thresh)
cv2.imshow("Riddler - Calvard", thresh)

因爲mahotas計算出來的都是門限值,所以需要自己操作一下圖片的二值化,在示例中兩個函數計算出來的門限都是137。

7.梯度和邊緣檢測(gradient and edge detection)

梯度計算:利用Laplacian和Sober算子來計算梯度。

  Laplacian算子計算使用的函數:lap = cv2.Laplacian(image, cv2.CV_64F),第二個參數是計算後輸出數據的類型,注意採用的是CV_64F浮點類型,而不是使用UINT8,是因爲在opencv中uint8不存在負數,當計算到的梯度強度爲負數時,它是自動取爲零,這不是我們想看到的結果。利用浮點類型計算完之後,再取絕對值轉化爲uint8:lap = np.uint8(np.absolute(lap))。

sobel算子計算使用的函數:cv2.Sobel(image, cv2.CV_64F, 1, 0),第三個和第四個參數取值可以是:(1,0)和(0,1),其中(1,0)代表計算x軸上的梯度,即垂直梯度;(0,1)代表計算y軸上的梯度,即水平梯度。數據類型也和Laplacian算子一樣。

image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imshow("Image", image)
##需要把計算梯度的數據類型改爲64F,因爲uint8在cv中會截取,
##在numpy中會取模運算
lap = cv2.Laplacian(image, cv2.CV_64F)
lap = np.uint8(np.absolute(lap))
cv2.imshow("Laplacian", lap)
cv2.waitKey(0)

sobelX = cv2.Sobel(image, cv2.CV_64F, 1, 0)
sobleY = cv2.Sobel(image, cv2.CV_64F, 0, 1)

sobelX = np.uint8(np.absolute(sobelX))
sobelY = np.uint8(np.absolute(sobleY))

sobelCombined = cv2.bitwise_or(sobelX, sobelY)

cv2.imshow("soble X", sobelX)
cv2.imshow("sobel Y", sobelY)
cv2.imshow("sobel combined", sobelCombined)

在使用Sobel算子時,分別計算了x軸和y軸上的梯度之後,再利用cv2.bitwise_or()函數進行融合。

Canny edge detector:用來檢測邊緣的檢測器,是一個多步驟的檢測器,包括了,去噪、計算Sobel算子梯度、抑制邊緣(極大值抑制)、後置的一個門限。

採用的函數:cv2.Canny(image, 30, 150),第二三個參數分別爲兩個門限值,小於門限值一則認爲不是邊緣,大於門限值二則認爲是一個邊緣,介於兩者之中的值根據強度的某種聯繫來確定。

image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Blurrde", blurred)

canny = cv2.Canny(image, 30, 150)
cv2.imshow("Canny", canny)

 

可以看出檢測出了硬幣的邊緣。

8.contour(輪廓)

 找出在圖像中的輪廓,一般就是用來找出圖像中的物體。

使用的函數:(_, cnts, _) = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

第一個參數:需要尋找出輪廓的圖像,這是一個已經檢測出邊緣的圖像,最好進行復制使用,因爲該函數會破壞原圖

第二個參數:需要尋找的輪廓類型,cv2.RETR_EXTERNAL代表最外層的輪廓,cv2.RETR_LIST代表所有的輪廓

第三個參數: cv2.CHAIN_APPROX_SIMPLE一般採用來減小資源的使用。

返回的第一個值:處理完之後的圖片,被損壞的很嚴重

返回的第二個值:返回檢測到的輪廓

返回的第三個值:輪廓的層次

我們只需要用到返回檢測到的輪廓即可。

檢測到輪廓cnts之後,我們可以把它畫在原圖之上,採用的函數是cv2.drawContours(coins, cnts, -1, (0, 255, 0), 2),

第一個參數:原圖

第二個參數:檢測到的輪廓數組

第三個參數:想要畫出輪廓對於的索引,若爲-1,則畫出所有的輪廓

第四個參數:畫輪廓的顏色

第五個參數:畫輪廓線的粗細

image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(image, (11, 11), 0)
cv2.imshow("image", image)

edged = cv2.Canny(blurred, 40, 275)
cv2.imshow("Edges", edged)
(_, cnts, _) = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE)

print("I count {} coin in this image". format(len(cnts)))

coins = image.copy()
cv2.drawContours(coins, cnts, -1, (0, 0, 255), 2)
cv2.imshow("Coins", coins)

 控制檯輸出了:I count 9 coin in this image

相比於之前採用的Candy邊緣檢測器而言,這個邊緣檢測出來的效果更加的好,是因爲在用GaussianBlur時使用了更大的卷積核,丟失了硬幣的更多細節,從而只剩下了外層的邊緣。並且畫出了這些硬幣。

 

爲了把每個硬幣都切出來,可以遍歷cnts這個列表,並且利用cv2.boundingRect(c)找出包含輪廓最小的矩形的左上角座標以及長寬,從而可以利用切片找出硬幣,接着利用cnts找出可以包圍它的最小圓,找出對應大小的mask,得到最終的硬幣圖。

for (i, c) in enumerate(cnts):
    (x, y, w, h) = cv2.boundingRect(c)

    print("Coin #{}".format(i +1 ))
    coin = image[y:y + h, x:x + w]
    cv2.imshow("coin", coin)

    mask = np.zeros(image.shape[0:2], dtype = "uint8")
    ((centerX, centerY), radius) = cv2.minEnclosingCircle(c)
    cv2.circle(mask, (int(centerX), int(centerY)), int(radius),
        255, -1)
    mask = mask[y:y + h, x:x + w]
    cv2.imshow("Masked Coin", cv2.bitwise_and(coin, coin, mask = 
        mask))
    cv2.waitKey(0)

  最終找出了所有的coin。

 

9 conclusion

 以上就是在《Practical Python and OpenCV》的所有內容了,確實都是一些簡單的入門操作,也是作爲一個記錄吧,方便之後對這些基本函數的一些查詢和記憶的加深,之後再對case study進行補充。

ps:一個正躺在牀上睡覺的舍友,看着我和另外一個舍友都在學習,竟然跑下來複習今天學習的功課了。。。。。爲了舍友能早點睡,去睡了去睡了。。。。。。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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