6.2 Python-OpenCV基礎(chǔ)
6.2.1 圖像的表示
前面章節(jié)已經(jīng)提到過了單通道的灰度圖像在計(jì)算機(jī)中的表示,就是一個(gè)8位無符號(hào)整形的矩陣。在OpenCV的C++代碼中,表示圖像有個(gè)專門的結(jié)構(gòu)叫做cv::Mat,不過在Python-OpenCV中,因?yàn)橐呀?jīng)有了numpy這種強(qiáng)大的基礎(chǔ)工具,所以這個(gè)矩陣就用numpy的array表示。如果是多通道情況,最常見的就是紅綠藍(lán)(RGB)三通道,則第一個(gè)維度是高度,第二個(gè)維度是高度,第三個(gè)維度是通道,比如圖6-1a是一幅3×3圖像在計(jì)算機(jī)中表示的例子:
圖6-1 RGB圖像在計(jì)算機(jī)中表示的例子
圖6-1中,右上角的矩陣?yán)锩總€(gè)元素都是一個(gè)3維數(shù)組,分別代表這個(gè)像素上的三個(gè)通道的值。最常見的RGB通道中,第一個(gè)元素就是紅色(Red)的值,第二個(gè)元素是綠色(Green)的值,第三個(gè)元素是藍(lán)色(Blue),最終得到的圖像如6-1a所示。RGB是最常見的情況,然而在OpenCV中,默認(rèn)的圖像的表示確實(shí)反過來的,也就是BGR,得到的圖像是6-1b??梢钥吹?,前兩行的顏色順序都交換了,最后一行是三個(gè)通道等值的灰度圖,所以沒有影響。至于OpenCV為什么不是人民群眾喜聞樂見的RGB,這是歷史遺留問題,在OpenCV剛開始研發(fā)的年代,BGR是相機(jī)設(shè)備廠商的主流表示方法,雖然后來RGB成了主流和默認(rèn),但是這個(gè)底層的順序卻保留下來了,事實(shí)上Windows下的最常見格式之一bmp,底層字節(jié)的存儲(chǔ)順序還是BGR。OpenCV的這個(gè)特殊之處還是需要注意的,比如在Python中,圖像都是用numpy的array表示,但是同樣的array在OpenCV中的顯示效果和matplotlib中的顯示效果就會(huì)不一樣。下面的簡(jiǎn)單代碼就可以生成兩種表示方式下,圖6-1中矩陣的對(duì)應(yīng)的圖像,生成圖像后,放大看就能體會(huì)到區(qū)別:
import numpy as np
import cv2
import matplotlib.pyplot as plt
# 圖6-1中的矩陣
img = np.array([
[[255, 0, 0], [0, 255, 0], [0, 0, 255]],
[[255, 255, 0], [255, 0, 255], [0, 255, 255]],
[[255, 255, 255], [128, 128, 128], [0, 0, 0]],
], dtype=np.uint8)
# 用matplotlib存儲(chǔ)
plt.imsave('img_pyplot.jpg', img)
# 用OpenCV存儲(chǔ)
cv2.imwrite('img_cv2.jpg', img)
不管是RGB還是BGR,都是高度×寬度×通道數(shù),H×W×C的表達(dá)方式,而在深度學(xué)習(xí)中,因?yàn)橐獙?duì)不同通道應(yīng)用卷積,所以用的是另一種方式:C×H×W,就是把每個(gè)通道都單獨(dú)表達(dá)成一個(gè)二維矩陣,如圖6-1c所示。
6.2.2 基本圖像處理
存取圖像
讀圖像用cv2.imread(),可以按照不同模式讀取,一般最常用到的是讀取單通道灰度圖,或者直接默認(rèn)讀取多通道。存圖像用cv2.imwrite(),注意存的時(shí)候是沒有單通道這一說的,根據(jù)保存文件名的后綴和當(dāng)前的array維度,OpenCV自動(dòng)判斷存的通道,另外壓縮格式還可以指定存儲(chǔ)質(zhì)量,來看代碼例子:
import cv2
# 讀取一張400x600分辨率的圖像
color_img = cv2.imread('test_400x600.jpg')
print(color_img.shape)
# 直接讀取單通道
gray_img = cv2.imread('test_400x600.jpg', cv2.IMREAD_GRAYSCALE)
print(gray_img.shape)
# 把單通道圖片保存后,再讀取,仍然是3通道,相當(dāng)于把單通道值復(fù)制到3個(gè)通道保存
cv2.imwrite('test_grayscale.jpg', gray_img)
reload_grayscale = cv2.imread('test_grayscale.jpg')
print(reload_grayscale.shape)
# cv2.IMWRITE_JPEG_QUALITY指定jpg質(zhì)量,范圍0到100,默認(rèn)95,越高畫質(zhì)越好,文件越大
cv2.imwrite('test_imwrite.jpg', color_img, (cv2.IMWRITE_JPEG_QUALITY, 80))
# cv2.IMWRITE_PNG_COMPRESSION指定png質(zhì)量,范圍0到9,默認(rèn)3,越高文件越小,畫質(zhì)越差
cv2.imwrite('test_imwrite.png', color_img, (cv2.IMWRITE_PNG_COMPRESSION, 5))
縮放,裁剪和補(bǔ)邊
縮放通過cv2.resize()實(shí)現(xiàn),裁剪則是利用array自身的下標(biāo)截取實(shí)現(xiàn),此外OpenCV還可以給圖像補(bǔ)邊,這樣能對(duì)一幅圖像的形狀和感興趣區(qū)域?qū)崿F(xiàn)各種操作。下面的例子中讀取一幅400×600分辨率的圖片,并執(zhí)行一些基礎(chǔ)的操作:
import cv2
# 讀取一張四川大錄古藏寨的照片
img = cv2.imread('tiger_tibet_village.jpg')
# 縮放成200x200的方形圖像
img_200x200 = cv2.resize(img, (200, 200))
# 不直接指定縮放后大小,通過fx和fy指定縮放比例,0.5則長(zhǎng)寬都為原來一半
# 等效于img_200x300 = cv2.resize(img, (300, 200)),注意指定大小的格式是(寬度,高度)
# 插值方法默認(rèn)是cv2.INTER_LINEAR,這里指定為最近鄰插值
img_200x300 = cv2.resize(img, (0, 0), fx=0.5, fy=0.5,
interpolation=cv2.INTER_NEAREST)
# 在上張圖片的基礎(chǔ)上,上下各貼50像素的黑邊,生成300x300的圖像
img_300x300 = cv2.copyMakeBorder(img, 50, 50, 0, 0,
cv2.BORDER_CONSTANT,
value=(0, 0, 0))
# 對(duì)照片中樹的部分進(jìn)行剪裁
patch_tree = img[20:150, -180:-50]
cv2.imwrite('cropped_tree.jpg', patch_tree)
cv2.imwrite('resized_200x200.jpg', img_200x200)
cv2.imwrite('resized_200x300.jpg', img_200x300)
cv2.imwrite('bordered_300x300.jpg', img_300x300)
這些處理的效果見圖6-2。
色調(diào),明暗,直方圖和Gamma曲線
除了區(qū)域,圖像本身的屬性操作也非常多,比如可以通過HSV空間對(duì)色調(diào)和明暗進(jìn)行調(diào)節(jié)。HSV空間是由美國(guó)的圖形學(xué)專家A. R. Smith提出的一種顏色空間,HSV分別是色調(diào)(Hue),飽和度(Saturation)和明度(Value)。在HSV空間中進(jìn)行調(diào)節(jié)就避免了直接在RGB空間中調(diào)節(jié)是還需要考慮三個(gè)通道的相關(guān)性。OpenCV中H的取值是[0, 180),其他兩個(gè)通道的取值都是[0, 256),下面例子接著上面例子代碼,通過HSV空間對(duì)圖像進(jìn)行調(diào)整:
# 通過cv2.cvtColor把圖像從BGR轉(zhuǎn)換到HSV
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# H空間中,綠色比黃色的值高一點(diǎn),所以給每個(gè)像素+15,黃色的樹葉就會(huì)變綠
turn_green_hsv = img_hsv.copy()
turn_green_hsv[:, :, 0] = (turn_green_hsv[:, :, 0]+15) % 180
turn_green_img = cv2.cvtColor(turn_green_hsv, cv2.COLOR_HSV2BGR)
cv2.imwrite('turn_green.jpg', turn_green_img)
# 減小飽和度會(huì)讓圖像損失鮮艷,變得更灰
colorless_hsv = img_hsv.copy()
colorless_hsv[:, :, 1] = 0.5 * colorless_hsv[:, :, 1]
colorless_img = cv2.cvtColor(colorless_hsv, cv2.COLOR_HSV2BGR)
cv2.imwrite('colorless.jpg', colorless_img)
# 減小明度為原來一半
darker_hsv = img_hsv.copy()
darker_hsv[:, :, 2] = 0.5 * darker_hsv[:, :, 2]
darker_img = cv2.cvtColor(darker_hsv, cv2.COLOR_HSV2BGR)
cv2.imwrite('darker.jpg', darker_img)
無論是HSV還是RGB,我們都較難一眼就對(duì)像素中值的分布有細(xì)致的了解,這時(shí)候就需要直方圖。如果直方圖中的成分過于靠近0或者255,可能就出現(xiàn)了暗部細(xì)節(jié)不足或者亮部細(xì)節(jié)丟失的情況。比如圖6-2中,背景里的暗部細(xì)節(jié)是非常弱的。這個(gè)時(shí)候,一個(gè)常用方法是考慮用Gamma變換來提升暗部細(xì)節(jié)。Gamma變換是矯正相機(jī)直接成像和人眼感受圖像差別的一種常用手段,簡(jiǎn)單來說就是通過非線性變換讓圖像從對(duì)曝光強(qiáng)度的線性響應(yīng)變得更接近人眼感受到的響應(yīng)。具體的定義和實(shí)現(xiàn),還是接著上面代碼中讀取的圖片,執(zhí)行計(jì)算直方圖和Gamma變換的代碼如下:
import numpy as np
# 分通道計(jì)算每個(gè)通道的直方圖
hist_b = cv2.calcHist([img], [0], None, [256], [0, 256])
hist_g = cv2.calcHist([img], [1], None, [256], [0, 256])
hist_r = cv2.calcHist([img], [2], None, [256], [0, 256])
# 定義Gamma矯正的函數(shù)
def gamma_trans(img, gamma):
# 具體做法是先歸一化到1,然后gamma作為指數(shù)值求出新的像素值再還原
gamma_table = [np.power(x/255.0, gamma)*255.0 for x in range(256)]
gamma_table = np.round(np.array(gamma_table)).astype(np.uint8)
# 實(shí)現(xiàn)這個(gè)映射用的是OpenCV的查表函數(shù)
return cv2.LUT(img, gamma_table)
# 執(zhí)行Gamma矯正,小于1的值讓暗部細(xì)節(jié)大量提升,同時(shí)亮部細(xì)節(jié)少量提升
img_corrected = gamma_trans(img, 0.5)
cv2.imwrite('gamma_corrected.jpg', img_corrected)
# 分通道計(jì)算Gamma矯正后的直方圖
hist_b_corrected = cv2.calcHist([img_corrected], [0], None, [256], [0, 256])
hist_g_corrected = cv2.calcHist([img_corrected], [1], None, [256], [0, 256])
hist_r_corrected = cv2.calcHist([img_corrected], [2], None, [256], [0, 256])
# 將直方圖進(jìn)行可視化
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
pix_hists = [
[hist_b, hist_g, hist_r],
[hist_b_corrected, hist_g_corrected, hist_r_corrected]
]
pix_vals = range(256)
for sub_plt, pix_hist in zip([121, 122], pix_hists):
ax = fig.add_subplot(sub_plt, projection='3d')
for c, z, channel_hist in zip(['b', 'g', 'r'], [20, 10, 0], pix_hist):
cs = [c] * 256
ax.bar(pix_vals, channel_hist, zs=z, zdir='y', color=cs, alpha=0.618, edgecolor='none', lw=0)
ax.set_xlabel('Pixel Values')
ax.set_xlim([0, 256])
ax.set_ylabel('Counts')
ax.set_zlabel('Channels')
plt.show()
上面三段代碼的結(jié)果統(tǒng)一放在下圖中:
可以看到,Gamma變換后的暗部細(xì)節(jié)比起原圖清楚了很多,并且從直方圖來看,像素值也從集中在0附近變得散開了一些。
評(píng)論
查看更多