1. 程式人生 > 實用技巧 >OpenCV計算機視覺學習(2)——影象算術運算 &影象閾值(數值計算,掩膜mask操作,邊界填充,二值化)

OpenCV計算機視覺學習(2)——影象算術運算 &影象閾值(數值計算,掩膜mask操作,邊界填充,二值化)

人工智慧學習離不開實踐的驗證,推薦大家可以多在FlyAI-AI競賽服務平臺多參加訓練和競賽,以此來提升自己的能力。FlyAI是為AI開發者提供資料競賽並支援GPU離線訓練的一站式服務平臺。每週免費提供專案開源演算法樣例,支援演算法能力變現以及快速的迭代演算法模型。

如果需要處理的原圖及程式碼,請移步小編的GitHub地址

  傳送門:請點選我

  如果點選有誤:https://github.com/LeBron-Jian/ComputerVisionPractice

  在OpenCV中我們經常會遇到一個名字:Mask(掩膜)。很多函式都使用到它,那麼這個Mask到底是什麼呢,下面我們從影象基本運算開始,一步一步學習掩膜。

1,影象算術運算

  影象的算術運算有很多種,比如兩幅影象可以相加,相減,相乘,相除,位運算,平方根,對數,絕對值等;影象也可以放大,縮小,旋轉,還可以擷取其中的一部分作為ROI(感興趣區域)進行操作,各個顏色通道還可以分別提取對各個顏色通道進行各種運算操作。總之,對影象可以進行的算術運算非常的多。這裡先學習圖片間的數學運算,影象混合,按位運算。

1.1 圖片加法

  要疊加兩張圖片,可以用 cv2.add() 函式,相加兩幅圖片的形狀(高度/寬度/通道數)必須相同, numpy中可以用 res = img1 + img2 相加,但這兩者的結果並不相同。

1

2

3

4

x = np.uint8([250])

y = np.uint8([10])

print(cv2.add(x, y)) # 250+10 = 260 => 255

print(x + y) # 250+10 = 260 % 256 = 4

  如果是二值化圖片(只有0和255),兩者結果是一樣的(用 numpy的方式更簡便一些)。

  這裡我們代入影象中看一下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

#encoding:utf-8

import cv2

import numpyasnp

import matplotlib.pyplotasplt

# 舉一個極端的例子,真的只是運氣好,遇到了。。。。

img = cv2.imread('lena.jpg')

img_add = img + 10

img_add2 = cv2.add(img, img_add)

print(img[0:4, :, 0])

print(img_add[0:4, :, 0])

print(img_add2[0:4, :, 0])

'''

這個是 logo1.jpg 的效果

[[246 246 246 ... 246 246 246]

[246 246 246 ... 246 246 246]

[246 246 246 ... 246 246 246]

[246 246 246 ... 246 246 246]]

[[0 0 0 ... 0 0 0]

[0 0 0 ... 0 0 0]

[0 0 0 ... 0 0 0]

[0 0 0 ... 0 0 0]]

[[246 246 246 ... 246 246 246]

[246 246 246 ... 246 246 246]

[246 246 246 ... 246 246 246]

[246 246 246 ... 246 246 246]]

這個是 lena.jpg 的效果

[[126 125 124 ... 128 120 90]

[127 126 124 ... 135 131 96]

[124 123 121 ... 144 138 96]

[116 119 116 ... 73 56 35]]

[[136 135 134 ... 138 130 100]

[137 136 134 ... 145 141 106]

[134 133 131 ... 154 148 106]

[126 129 126 ... 83 66 45]]

[[255 255 255 ... 255 250 190]

[255 255 255 ... 255 255 202]

[255 255 252 ... 255 255 202]

[242 248 242 ... 156 122 80]]

# 我們發現 使用numpy庫的加法,則運算結果取模

使用opencv的add()函式,則運算結果當大於255,則取255

'''

  注意:OpenCV中的加法與Numpy的加法是有所不同的,OpenCV的加法是一種飽和操作,而Numpy的加法是一種模操作。

Numpy庫的加法

  其運算方法是:目標影象 = 影象1 + 影象2,運算結果進行取模運算

  • 當畫素值 小於等於 255 時,結果為:“影象1 + 影象2”,例如:120+48=168
  • 當畫素值 大於255 時,結果為:對255取模的結果,例如:(255 + 64) % 255 = 64

OpenCV的加法

  其運算方法是:目標影象 = cv2.add(影象1, 影象2)

  • 當畫素值 小於等於 255 時,結果為:“影象1 + 影象2”,例如:120+48=168
  • 當畫素值 大於255 時,結果為:255,例如:255 + 64 = 255

  兩種方法對應的程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

# encoding:utf-8

import cv2

import numpyasnp

import matplotlib.pyplotasplt

# 讀取圖片

img = cv2.imread('logo1.jpg')

test = img

# 方法一:Numpy加法運算

result1 = img + test

# 方法二:OpenCV加法運算

result2 = cv2.add(img, test)

all_pic = np.column_stack((img, result1, result2))

# 顯示影象

cv2.imshow('img result1 result2', all_pic)

# cv2.imshow("original", img)

# cv2.imshow("result1", result1)

# cv2.imshow("result2", result2)

# 等待顯示

cv2.waitKey(0)

cv2.destroyAllWindows()

  原圖及其效果圖如下:

  其中,result1為Numpy的方法,result2為OpenCV的方法。

1.2 影象混合

  影象融合通常是指將2張或者兩張以上的影象資訊融合到1張影象上,融合的影象含有更多的資訊,能夠更方便人們觀察或計算機處理。

  影象融合是在影象加法的基礎上增加了係數和亮度調節量。

  影象融合:目標影象 = 影象1*係數1 + 影象2*係數2 + 亮度調節量

  影象混合 cv2.addWeighted() 也是一種圖片相加的操作,只不過兩幅圖片的權重不一樣, y 相當於一個修正值:

dst = α*img1 + β*img2 + γ

  PS:當 alpha 和 beta 都等於1,則相當於圖片相加。

  程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

import cv2

import numpyasnp

img1 = cv2.imread('lena_small.jpg')

img2 = cv2.imread('opencv_logo_white.jpg')

# print(img1.shape, img2.shape) # (187, 186, 3) (184, 193, 3)

img2 = cv2.resize(img2, (186, 187))

# print(img1.shape, img2.shape)

res = cv2.addWeighted(img1, 0.6, img2, 0.4, 0)

cv2.imshow("res", res)

cv2.waitKey(0)

cv2.destroyAllWindows()

  注意這裡,兩張圖片的尺寸必須一致。原圖和結果圖如下:

1.3 影象矩陣減法

  影象矩陣減法與加法其實類似,我們這不多做說明,只貼函式:

1

2

3

4

5

6

7

8

9

10

11

函式原型:cv2.subtract(src1, src2, dst=None, mask=None, dtype=None)

src1:影象矩陣1

src1:影象矩陣2

dst:預設選項

mask:預設選項

dtype:預設選項

  

1.4 按位運算

  按位操作有:AND ,OR, NOT,XOR 等。cv2.bitwise_and(), cv2.bitwise_not(), cv2.bitwise_or(), cv2.bitwise_xor()分別執行按位與/或/非/異或運算。下面我們貼一下opencv中的函式

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

bitwise_or—影象或運算

函式原型:cv2.bitwise_or(src1, src2, dst=None, mask=None)

src1:影象矩陣1

src1:影象矩陣2

dst:預設選項

mask:預設選項

bitwise_xor—影象異或運算

函式原型:bitwise_xor(src1, src2, dst=None, mask=None)

src1:影象矩陣1

src1:影象矩陣2

dst:預設選項

mask:預設選項

bitwise_not—影象非運算

函式原型:bitwise_not(src1, src2, dst=None, mask=None)

src1:影象矩陣1

src1:影象矩陣2

dst:預設選項

mask:預設選項

  掩膜就是用來對圖片進行全域性或區域性的遮擋,當我們提取影象的一部分,選擇非矩陣ROI時這些操作會很有用,常用於Logo投射。

  通過 threshold 函式將圖片固定閾值二值化(影象二值化定義:將影象上的畫素點的灰度值設定為0或255,也就是將整個影象呈現出明顯的黑和白的視覺效果)

  一幅影象包括目標物體,背景還有噪聲,要想從多值的數字影象中直接提取出目標物體,常用的方法就是設定一個閾值T,用 T 將影象的資料分為兩部分:大於 T 的畫素群和小於 T 的畫素群。這是研究灰度變換的最特殊的方法,稱為影象二值化(Binarization)

  下面做一個例子,關於Logo投射。(下面首先展示兩張照片,一張原圖,一張logo圖,目的是投射logo到原圖上)

  思路如下:我們的目的是把 logo 放在左邊,所以我們只關心這一塊區域,下面我們的目的是建立掩碼(這是在Logo圖上),並且保留除了logo以外的背景(這是在原圖),然後進行融合(這是在原圖),最後融合放在原圖。

  程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

# _*_coding:utf-8_*_

import cv2

import numpyasnp

img_photo = cv2.imread('james.jpg')

img_logo = cv2.imread('logo1.jpg')

print(img_logo.shape, img_photo.shape)

# (615, 327, 3) (640, 1024, 3)

rows, cols, channels = img_logo.shape

photo_roi = img_photo[0:rows, 0:cols]

gray_logo = cv2.cvtColor(img_logo, cv2.COLOR_BGR2GRAY)

# 中值濾波

midian_logo = cv2.medianBlur(gray_logo, 5)

# mask_bin 是黑白掩膜

ret, mask_bin = cv2.threshold(gray_logo, 127, 255, cv2.THRESH_BINARY)

# mask_inv 是反色黑白掩膜

mask_inv = cv2.bitwise_not(mask_bin)

# 黑白掩膜 和 大圖切割區域 去取和

img_photo_bg_mask = cv2.bitwise_and(photo_roi, photo_roi, mask=mask_bin)

# 反色黑白掩膜 和 logo 取和

img2_photo_fg_mask = cv2.bitwise_and(img_logo, img_logo, mask=mask_inv)

dst = cv2.add(img_photo_bg_mask, img2_photo_fg_mask)

img_photo[0:rows, 0:cols] = dst

cv2.imshow("mask_bin", mask_bin)

cv2.imshow("mask_inv", mask_inv)

cv2.imshow("img_photo_bg_mask", img_photo_bg_mask)

cv2.imshow("img2_photo_fg_mask", img2_photo_fg_mask)

cv2.imshow("img_photo", img_photo)

cv2.waitKey(0)

cv2.destroyAllWindows()

  圖示過程如下:

  下面第一張是黑色是因為 背景圖中 ,左邊就是黑色,所以這裡不顯示而已。

  最終形態如下:

2,掩膜(mask)

  在有些影象處理的函式中有的引數裡面會有 mask 引數,即此函式支援掩膜操作。

  首先我們要理解什麼是掩膜?,其次掩膜有什麼作用呢?

2.1 掩膜(mask)的概念

  簡單來說:掩膜是用一副二值化圖片對另外一幅圖片進行區域性的遮擋。

  首先我們從物理的角度來看看 mask 到底是什麼過程。

  數字影象處理中的掩膜的概念是借鑑於 PCB 製版的過程,在半導體制作中,許多晶片工藝步驟採用光刻技術,用於這些步驟的圖形”底片”稱為掩膜(也稱為“掩模”),其作用是:在矽片上選定的區域中對一個不透明的圖形模板遮蓋,繼而下面的腐蝕或擴散將隻影響選定的區域意外的區域。

  圖形掩膜(Image mask)與其類似,用選定的圖形,圖形或物體,對處理的影象(全部或區域性)進行遮擋,來控制影象處理的區域或處理過程。用於覆蓋的特點影象或物體稱為掩膜或模板。光學影象處理中,掩膜可以足膠片,濾光片等。掩膜是由0和1組成的一個二進位制影象。當在某一功能中應用掩膜時,1值區域被處理,被遮蔽的0值區域不被包括在計算中。通過制定的資料值,資料範圍,有限或無限值,感興趣區和註釋檔案來定義影象掩膜,也可以應用上述選項的任意組合作為輸入來建立掩膜。

2.2 掩膜的作用

  數字影象處理中,掩膜為二維矩陣陣列,有時也用多值影象,影象掩膜主要用於:

  • 1,提取感興趣區,用預先製作的感興趣區掩膜與待處理影象相乘,得到感興趣區影象,感興趣區內影象值保持不變,而區外影象值都為零。
  • 2,遮蔽作用,用掩膜對影象上某些區域做遮蔽,使其不參加處理或不參加處理引數的計算,或僅對遮蔽區做處理或統計。
  • 3,結構特徵提取,用相似性變數或影象匹配方法檢測和提取影象中與掩膜相似的結構特徵。
  • 4,特殊性質影象的製作

  掩膜是一種影象濾鏡的模板,試用掩膜經常處理的是遙感影象。當提取道路或者河流,或者房屋時,通過一個 N*N 的矩陣來對影象進行畫素過濾,然後將我們需要的地物或者標誌突出顯示出來,這個矩陣就是一種掩膜。在OpenCV中,掩膜操作時相對簡單的。大致的意思是,通過一個掩膜矩陣,重新計算影象中的每一個畫素值。掩膜矩陣控制了舊影象當前位置以及周圍位置畫素對新影象當前位置畫素值的影響力度。用數學術語將,即我們自定義一個權重表。

  在所有影象基本運算的操作函式中,凡是帶有掩膜(mask)的處理函式,其掩膜都參與運算(輸入影象運算完之後再與掩膜影象或矩陣運算)。

2.3 通過掩膜操作實現影象對比圖的改變

  矩陣的掩膜操作非常簡單,根據掩膜來重新計算每個畫素的畫素值,掩膜(mask)也被稱為核心。

什麼是圖和掩膜的與運算呢?

  其實就是原圖中的每個畫素和掩膜中的每個對應畫素進行與運算。比如1 & 1 = 1;1 & 0 = 0;

  比如一個 3*3 的影象與 3*3 的掩膜進行運算,得到的結果影象就是:

  說白了,mask就是點陣圖,來選擇哪個畫素允許拷貝,哪個畫素不允許拷貝,如果mask畫素的值時非0的,我們就拷貝它,否則不拷貝。

2.4 mask小結

  1,影象中,各種位運算,比如與,或,非運算與普通的位運算類似。

  2,如果用一句話總結,掩膜就是兩幅影象之間進行的各種位運算操作。

程式碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

#_*_coding:utf-8_*_

import cv2

import numpyasnp

def mask_processing(path):

image = cv2.imread(path) # 讀圖

# cv2.imshow("Oringinal", image) #顯示原圖

print(image.shape[:2]) # (613, 440)

# 輸入影象是RGB影象,故構造一個三維陣列,四個二維陣列是mask四個點的座標,

site = np.array([[[300, 280], [150, 280], [150, 50], [300, 50]]], dtype=np.int32)

im = np.zeros(image.shape[:2], dtype="uint8") # 生成image大小的全白圖

cv2.polylines(im, site, 1, 255) # 在im上畫site大小的線,1表示線段閉合,255表示線段顏色

cv2.fillPoly(im, site, 255) # 在im的site區域,填充顏色為255

mask = im

cv2.namedWindow('Mask', cv2.WINDOW_NORMAL) # 可調整視窗大小,不加這句不可調整

cv2.imshow("Mask", mask)

masked = cv2.bitwise_and(image, image, mask=mask) # 在模板mask上,將image和image做“與”操作

cv2.namedWindow('Mask to Image', cv2.WINDOW_NORMAL) # 同上

cv2.imshow("Mask to Image", masked)

cv2.waitKey(0) # 影象一直顯示,鍵盤按任意鍵即可關閉視窗

cv2.destroyAllWindows()

if__name__ =='__main__':

path ='irving.jpg'

mask_processing(path)

  程式碼說明:

1,考慮到當影象尺寸太大,所以我們用 cv2.namedWindow() 函式可以指定視窗是否可以調整大小。在預設情況下,標誌為 cv2.WINDOW_AUTOSIZE。但是,如果指定標誌為 cv2.WINDOW_Normal,則可以調整視窗的大小,這些操作可以讓我們的工作更方便一些。

2,對座標軸的理解,上面程式碼中的四個座標從第一個到最後一個分別對應下圖中的 x1 x2 x4 x3。(我實際實驗是這樣的,如果有不同想法,可以交流)。

  原圖如下:

  mask與處理後圖的結果如下:

3,邊界填充

  在做深度學習的時候,難免遇到需要填充邊界。邊緣填充是什麼呢?

  因為對於影象的卷積操作,最邊緣的畫素一般無法處理,所以卷積核中心倒不了最邊緣畫素。這就需要先將影象的邊界填充,再根據不同的填充演算法進行卷積操作,得到的新影象就是填充後的影象。

  如果你想在影象周圍建立一個邊,就像相框一樣,你可以使用 cv2.copyMakeBorder() 函式,這經常在卷積運算或 0 填充時被用到,這個函式如下:

1

def copyMakeBorder(src, top, bottom, left, right, borderType, dst=None, value=None):

  引數解釋:

  • src:輸入影象
  • top,buttom,left,right 對應邊界的畫素數目(分別為影象上面, 下面, 左面,右面填充邊界的長度)
  • borderType 要新增哪種型別的邊界,型別如下:

    ——cv2.BORDER_CONSTANT 新增有顏色的常數值邊界,還需要下一個引數(value)

    ——cv2.BORDER_REFLECT 邊界元素的映象,反射法,即以最邊緣的畫素為對稱軸。比如: fedcba|abcdefgh|hgfedcb

    ——cv2.BORDER_REFLECT_101 or cv2.BORDER_DEFAULT跟BORDER_REFLECT類似,但是由區別。例如: gfedcb|abcdefgh|gfedcba

    ——cv2.BORDER_REPLICATE 複製法,重複最後一個元素。例如: aaaaaa|abcdefgh|hhhhhhh

    ——cv2.BORDER_WRAP 不知道怎麼說了, 就像這樣: cdefgh|abcdefgh|abcdefg

  • value 邊界顏色,通常用於常量法填充中,即邊界的型別是 cv2.BORDER_CONSTANT,

  為了更好的理解這幾種型別,請看下面程式碼演示:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

import cv2

import numpyasnp

import matplotlib.pyplotasplt

# 讀取圖片

img = cv2.imread('kd1.jpg') # (221, 405, 3)

# img = cv2.imread('lena.jpg') # (263, 263, 3)

# print(img.shape)

# 各個邊界需要填充的值, 為了展示效果,這裡填充的大一些

top_size, bottom_size, left_size, right_size = (50, 50, 50, 50)

# 複製法 重複邊界,填充 即複製最邊緣畫素

replicate = cv2.copyMakeBorder(img, top_size, bottom_size,

left_size, right_size,

borderType=cv2.BORDER_REPLICATE)

# 反射法 反射邊界,填充 即對感興趣的影象中的畫素在兩邊進行復制,

# 例如 fedcba|abcdefgh|hgfedcb

reflect = cv2.copyMakeBorder(img, top_size, bottom_size,

left_size, right_size,

borderType=cv2.BORDER_REFLECT)

# 反射101邊界法 反射101邊界,填充 這個是以最邊緣為軸,對稱 ,

# 例如 gfedcb|abcdefg|gfedcba

reflect101 = cv2.copyMakeBorder(img, top_size, bottom_size,

left_size, right_size,

borderType=cv2.BORDER_REFLECT_101)

# 外包裝法 填充

# 例如 cdefgh|abcdefgh|abcdegf

wrap = cv2.copyMakeBorder(img, top_size, bottom_size,

left_size, right_size,

borderType=cv2.BORDER_WRAP)

# 常量法,常數值填充 ,常量值可以自己設定 value=0

constant = cv2.copyMakeBorder(img, top_size, bottom_size,

left_size, right_size,

borderType=cv2.BORDER_CONSTANT,

value=(0, 255, 0))

plt.subplot(231)

plt.imshow(img,'gray')

plt.title('origin')

plt.subplot(232)

plt.imshow(replicate,'gray')

plt.title('replicate')

plt.subplot(233)

plt.imshow(reflect,'gray')

plt.title('reflect')

plt.subplot(234)

plt.imshow(reflect101,'gray')

plt.title('reflect101')

plt.subplot(235)

plt.imshow(wrap,'gray')

plt.title('wrap')

plt.subplot(236)

plt.imshow(constant,'gray')

plt.title('constant')

plt.show()

  原圖1如下:

  處理的效果圖如下:

  效果2如下:

注意:plt.imshow() 顯示圖片色差問題

  我們都知道 cv2.imshow() 顯示的原始圖片是BGR格式,即原圖如下所示:

  那通過opencv將BGR格式轉換為RGB格式,圖顯示如下:

  這就解釋了為什麼plt.imshow()顯示圖片色差問題,原因就是讀取圖片的通道不同。

3.1 細節函式

  為了能快速對比出各個方法得出的影象的區別,可以使用np.vstack()或者np.hstack()對比,將影象放在同一個視窗。

1

2

rec=np.hstack((replicate,reflect))

cv_show("replicate_reflect",rec)

  注意:使用np.vstack()或者np.hstack()函式時,影象的大小必須一致,不然會報錯。

     使用np.vstack()或者np.hstack()函式時,可能會出現影象顯示不完全情況

4,影象閾值(二值化)

4.1 影象二值化原理

  二值化核心思想,設閾值,大於閾值的為0(黑色)或 255(白色),使影象稱為黑白圖。

  閾值可固定,也可以自適應閾值。

  自適應閾值一般為一點畫素與這點為中序的區域畫素平均值或者高斯分佈加權和的比較,其中可以設定一個差值也可以不設定。

  影象的閾值化旨在提取影象中的目標物體,將背景以及噪聲區分開來。通常會設定一個閾值T,通過T將影象的畫素分為兩類:大於T的畫素群和小於T的畫素群。

  灰度轉換處理後的影象中,每個畫素都只有一個灰度值,其大小表示明暗程度。所謂影象的二值化 ,就是將影象上的畫素點的灰度值設定為0或255,也就是將整個影象呈現出明顯的只有黑和白的視覺效果。一幅影象包括目標物體、背景還有噪聲,要想從多值的數字影象中直接提取出目標物體。

  常用的二值化演算法下所示:

  當灰度Gray小於閾值T的時候,其畫素設定為0,表示黑色;當灰度Gray大於或等於閾值T時,其Y值為255,表示白色。

  全域性閾值就是一幅影象包括目標物體、背景還有噪聲,要想從多值的數字影象中直接提取出目標物體;常用的方法就是設定一個閾值T,用T將影象的資料分成兩部分:大於T的畫素群和小於T的畫素群。這是研究灰度變換的最特殊的方法,稱為影象的二值化(Binarization)。

  區域性閾值就是當同一幅影象上的不同部分的具有不同亮度時。這種情況下我們需要採用自適應閾值。此時的閾值是根據影象上的每一個小區域計算與其對應的閾值。因此在同一幅影象上的不同區域採用的是不同的閾值,從而使我們能在亮度不同的情況下得到更好的結果。

  二值化處理廣泛應用於各行各業,比如生物學中的細胞圖分割,交通領域的車牌設計等。在文化應用領域中,通過二值化處理將所需民族文物影象轉換為黑白兩色圖,從而為後面的影象識別提供更好的支撐作用。

4.2 簡單閾值處理(全域性閾值)

  Python-OpenCV中提供了閾值(threshold)函式:

1

threshold(src, thresh, maxval, type, dst=None)

  變數的作用:

  • 第一個引數 src 指原影象,原影象應該是灰度圖,只能輸入單通道影象
  • 第二個引數 thresh 指用來對畫素值進行分類的閾值
  • 第三個引數 maxval 指當畫素值高於(有時是小於,根據 type 來決定)閾值時應該被賦予的新的畫素值,在二元閾值THRESH_BINARY和逆二元閾值THRESH_BINARY_INV中使用的最大值
  • 第四個引數 dst 指不同的不同的閾值方法,這些方法包括以下五種型別:

    cv2.THRESH_BINARY 超過閾值部分取 maxval(最大值),否則取 0

    cv2.THRESH_BINARY_INV THRESH_BINARY 的反轉

    cv2.THRESH_TRUNC     大於閾值部分設為閾值,否則不變

    cv2.THRESH_TOZERO 大於閾值部分不改變,否則設為零

    cv2.THRESH_TOZERO_INV THRESH_TOZERO 的反轉

  (盜圖來自:https://blog.csdn.net/whl970831/article/details/98231314 https://blog.csdn.net/Eastmount/article/details/83548652)

  詳細解析如下:

  用函式表示如下:

  對應OpenCV提供的五張圖如下,第一張為原圖,後面依次為:二進位制閾值化,反二進位制閾值化,截斷閾值化,反閾值化為0,閾值化為0.

  程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

# _*_coding:utf-8_*_

import cv2

import numpyasnp

frommatplotlib import pyplotasplt

def parse_thresh(path):

img = cv2.imread(path) # 讀取原始照片

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

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 = ['Origin Image','gray','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV'] # 標題

images = [img, gray, thresh1, thresh2, thresh3, thresh4, thresh5] # 對應的圖

foriinrange(7): # 畫7次圖

plt.subplot(2, 4, i + 1), plt.imshow(images[i],'gray')

plt.title(titles[i])

plt.xticks([]), plt.yticks([])

plt.show()

if__name__ =='__main__':

path ='durant.jpg'

parse_thresh(path)

  結果如下:

4.3 自適應閾值處理(區域性閾值)  

  Python-OpenCV提供了自適應閾值函式:

1

cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C, dst=None)

  引數意義:

  • 第一個引數 src 指原影象,原影象應該是灰度圖。
  • 第二個引數 x 指當畫素值高於(有時是小於)閾值時應該被賦予的新的畫素值
  • 第三個引數 adaptive_method 引數為:

    CV_ADAPTIVE_THRESH_MEAN_C
    CV_ADAPTIVE_THRESH_GAUSSIAN_C

  • 第四個引數 threshold_type 指取閾值型別:必須是下者之一

    CV_THRESH_BINARY
    CV_THRESH_BINARY_INV

  • 第五個引數 block_size 指用來計算閾值的象素鄰域大小: 3, 5, 7, …
  • 第六個引數 param1 指與方法有關的引數。

    對方法CV_ADAPTIVE_THRESH_MEAN_C 和 CV_ADAPTIVE_THRESH_GAUSSIAN_C, 它是一個從均值或加權均值提取的常數, 儘管它可以是負數。
    對方法CV_ADAPTIVE_THRESH_MEAN_C,先求出塊中的均值,再減掉param1。
    對方法 CV_ADAPTIVE_THRESH_GAUSSIAN_C ,先求出塊中的加權和(gaussian), 再減掉param1。

  程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import cv2

frommatplotlib import pyplotasplt

#詳細說明參考上方例子

img = cv2.imread('sss.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]

foriinrange(4):

plt.subplot(2,2,i+1),plt.imshow(images[i],'gray')

plt.title(titles[i])

plt.xticks([]),plt.yticks([])

plt.show()

  綜合程式碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

import cv2ascv

#全域性閾值

def threshold_demo(image):

gray = cv.cvtColor(image, cv.COLOR_RGB2GRAY) #把輸入影象灰度化

ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_TRIANGLE) #直接閾值化是對輸入的單通道矩陣逐畫素進行閾值分割。

#print("threshold value %s"%ret)

cv.namedWindow("threshold", cv.WINDOW_NORMAL)

cv.imshow("threshold", binary)

#區域性閾值

def local_threshold(image):

gray = cv.cvtColor(image, cv.COLOR_RGB2GRAY) #把輸入影象灰度化

binary = cv.adaptiveThreshold(gray, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY, 25, 10) #自適應閾值化能夠根據影象不同區域亮度分佈,改變閾值

cv.namedWindow("adaptiveThreshold", cv.WINDOW_NORMAL)

cv.imshow("adaptiveThreshold", binary)

src = cv.imread('sss.jpg')

cv.namedWindow('input_image', cv.WINDOW_NORMAL) #設定為WINDOW_NORMAL可以任意縮放

cv.imshow('input_image', src) #源圖

threshold_demo(src) #全域性

local_threshold(src) #區域性

cv.waitKey(0)

cv.destroyAllWindows()

  

5, Otsu 二值化

  在使用全域性閾值時,我們就是隨便給了一個數來做閾值,那我們怎麼知道我們選取的這個數的好壞呢?答案就是不停的嘗試。如果是一副雙峰影象(簡單來說雙峰影象是指影象直方圖中存在兩個峰)呢?我們豈不是應該在兩個峰之間的峰谷選一個值作為閾值?這就是 Otsu 二值化要做的。簡單來說就是對一副雙峰影象自動根據其直方圖計算出一個閾值。(對於非雙峰影象,這種方法得到的結果可能會不理想)。
  這裡用到到的函式還是 cv2.threshold(),但是需要多傳入一個引數(flag):cv2.THRESH_OTSU。
  這時要把閾值設為 0。然後演算法會找到最優閾值,這個最優閾值就是返回值 retVal。如果不使用 Otsu 二值化,返回的retVal 值與設定的閾值相等。
  下面的例子中,輸入影象是一副帶有噪聲的影象。第一種方法,設127 為全域性閾值。第二種方法,直接使用 Otsu 二值化。第三種方法,先使用一個 5x5 的高斯核除去噪音,然後再使用 Otsu 二值化。

  程式碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

import cv2

frommatplotlib import pyplotasplt

img = cv2.imread('sss.jpg',0)

# 設127 為全域性閾值

ret1,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)

# 直接使用 Otsu 二值化

ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

# 先使用一個 5x5 的高斯核除去噪音,然後再使用 Otsu 二值化

blur = cv2.GaussianBlur(img,(5,5),0)

ret3,th3 = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

images = [img, 0, th1,

img, 0, th2,

blur, 0, th3]

titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',

'Original Noisy Image','Histogram',"Otsu's Thresholding",

'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]

foriinrange(3):

plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')

plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])

plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)

plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])

plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')

plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])

plt.show()

  

5.1 Otsu 最大類間方差法原理

  OTSU 演算法是由日本學者 OTSU 於 1979 年提出的一種對影象進行二值化的高效演算法。OTSU演算法又叫大津演算法,其本質是最大類間方差法。

  它的原理是利用閾值將原影象分為前景,背景兩個影象。

  前景:用 n1,csum,m1 來表示在當前閾值下的前景的點數,質量距,平均灰度。

  背景:用n2,sum-csum,m2 來表示在當前閾值下的背景的點數,質量距,平均灰度。

  當取最佳閾值時,背景應該與前景差別最大,關鍵在於如何選擇衡量差別的標準,而在otsu演算法中這個衡量差別的標準就是最大類間方差。

5.2 Otsu 最大類間方差法的效能

  類間方差法對噪音和目標大小十分敏感,它僅對類間方差為單峰的影象產生較好的分割效果。

  當目標與背景的大小比例懸殊時,類間方差準則可能呈現雙峰或多峰,此時效果不好,但是類間方差法是用時最少的。

5.3 Otsu 最大類間方差法的公式推導

  記 t 為前景與背景的分割閾值,前景點數佔影象比例為 w0,平均灰度為 u0;背景點數佔影象比例為 w1,平均灰度為 u1.

  則影象的總平均灰度為: u = w0 * u0 + w1 * u1

  前景和背景影象的方差:g = w0 * (u0 - u) * (u0 - u) + w1 * (u1 - u) * (u1 - u) = w0 * w1 * (u0 - u1) * (u0 - u1)

  當方差 g 最大時,可以認為此時前景和背景差異最大,此時的灰度 t 是最佳閾值 sb = w0 * w1*(u1 - u0)*(u0 - u1)

  程式碼實現:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

# _*_coding:utf-8_*_

import cv2

import numpyasnp

def max_class_threshold_variance(origin_photo):

img = cv2.imread(origin_photo, -1)

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

retval, dst = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU)

cv2.imshow("src", img)

cv2.imshow("gray", gray)

cv2.imshow("dst", dst)

cv2.waitKey(0)

  

5.4 Otsu’ ’s 二值化是如何工作的?

  在這一部分我們會演示怎樣使用 Python 來實現 Otsu 二值化演算法,從而告訴大家它是如何工作的。如果你不感興趣的話可以跳過這一節。因為是雙峰圖,Otsu 演算法就是要找到一個閾值(t), 使得同一類加權方差最小,需要滿足下列關係式:

  其中:

  其實就是在兩個峰之間找到一個閾值 t,將這兩個峰分開,並且使每一個峰內的方差最小。實現這個演算法的 Python 程式碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

img = cv2.imread('noisy2.png',0)

blur = cv2.GaussianBlur(img,(5,5),0)

# find normalized_histogram, and its cumulative distribution function

hist = cv2.calcHist([blur],[0],None,[256],[0,256])

hist_norm = hist.ravel()/hist.max()

Q = hist_norm.cumsum()

bins = np.arange(256)

fn_min = np.inf

thresh = -1

foriinxrange(1,256):

p1,p2 = np.hsplit(hist_norm,[i]) # probabilities

q1,q2 = Q[i],Q[255]-Q[i] # cum sum of classes

b1,b2 = np.hsplit(bins,[i]) # weights

# finding means and variances

m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2

v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2

# calculates the minimization function

fn = v1*q1 + v2*q2

iffn < fn_min:

fn_min = fn

thresh = i

# find otsu's threshold value with OpenCV function

ret, otsu = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

prin(thresh,ret)

  

參考文獻:https://blog.csdn.net/weixin_42338058/article/details/88568704

按位運算參考:https://blog.51cto.com/devops2016/2088574

https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_core/py_image_arithmetics/py_image_arithmetics.html


更多精彩內容請訪問FlyAI-AI競賽服務平臺;為AI開發者提供資料競賽並支援GPU離線訓練的一站式服務平臺;每週免費提供專案開源演算法樣例,支援演算法能力變現以及快速的迭代演算法模型。

挑戰者,都在FlyAI!!!