6.2 Python-OpenCV基礎
6.2.1 圖像的表示
前面章節已經提到過了單通道的灰度圖像在計算機中的表示,就是一個8位無符號整形的矩陣。在OpenCV的C++代碼中,表示圖像有個專門的結構叫做cv::Mat,不過在Python-OpenCV中,因為已經有了numpy這種強大的基礎工具,所以這個矩陣就用numpy的array表示。如果是多通道情況,最常見的就是紅綠藍(RGB)三通道,則第一個維度是高度,第二個維度是高度,第三個維度是通道,比如圖6-1a是一幅3×3圖像在計算機中表示的例子:
圖6-1 RGB圖像在計算機中表示的例子
圖6-1中,右上角的矩陣里每個元素都是一個3維數組,分別代表這個像素上的三個通道的值。最常見的RGB通道中,第一個元素就是紅色(Red)的值,第二個元素是綠色(Green)的值,第三個元素是藍色(Blue),最終得到的圖像如6-1a所示。RGB是最常見的情況,然而在OpenCV中,默認的圖像的表示確實反過來的,也就是BGR,得到的圖像是6-1b。可以看到,前兩行的顏色順序都交換了,最后一行是三個通道等值的灰度圖,所以沒有影響。至于OpenCV為什么不是人民群眾喜聞樂見的RGB,這是歷史遺留問題,在OpenCV剛開始研發的年代,BGR是相機設備廠商的主流表示方法,雖然后來RGB成了主流和默認,但是這個底層的順序卻保留下來了,事實上Windows下的最常見格式之一bmp,底層字節的存儲順序還是BGR。OpenCV的這個特殊之處還是需要注意的,比如在Python中,圖像都是用numpy的array表示,但是同樣的array在OpenCV中的顯示效果和matplotlib中的顯示效果就會不一樣。下面的簡單代碼就可以生成兩種表示方式下,圖6-1中矩陣的對應的圖像,生成圖像后,放大看就能體會到區別:
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存儲
plt.imsave('img_pyplot.jpg', img)
# 用OpenCV存儲
cv2.imwrite('img_cv2.jpg', img)
不管是RGB還是BGR,都是高度×寬度×通道數,H×W×C的表達方式,而在深度學習中,因為要對不同通道應用卷積,所以用的是另一種方式:C×H×W,就是把每個通道都單獨表達成一個二維矩陣,如圖6-1c所示。
6.2.2 基本圖像處理
存取圖像
讀圖像用cv2.imread(),可以按照不同模式讀取,一般最常用到的是讀取單通道灰度圖,或者直接默認讀取多通道。存圖像用cv2.imwrite(),注意存的時候是沒有單通道這一說的,根據保存文件名的后綴和當前的array維度,OpenCV自動判斷存的通道,另外壓縮格式還可以指定存儲質量,來看代碼例子:
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通道,相當于把單通道值復制到3個通道保存
cv2.imwrite('test_grayscale.jpg', gray_img)
reload_grayscale = cv2.imread('test_grayscale.jpg')
print(reload_grayscale.shape)
# cv2.IMWRITE_JPEG_QUALITY指定jpg質量,范圍0到100,默認95,越高畫質越好,文件越大
cv2.imwrite('test_imwrite.jpg', color_img, (cv2.IMWRITE_JPEG_QUALITY, 80))
# cv2.IMWRITE_PNG_COMPRESSION指定png質量,范圍0到9,默認3,越高文件越小,畫質越差
cv2.imwrite('test_imwrite.png', color_img, (cv2.IMWRITE_PNG_COMPRESSION, 5))
縮放,裁剪和補邊
縮放通過cv2.resize()實現,裁剪則是利用array自身的下標截取實現,此外OpenCV還可以給圖像補邊,這樣能對一幅圖像的形狀和感興趣區域實現各種操作。下面的例子中讀取一幅400×600分辨率的圖片,并執行一些基礎的操作:
import cv2
# 讀取一張四川大錄古藏寨的照片
img = cv2.imread('tiger_tibet_village.jpg')
# 縮放成200x200的方形圖像
img_200x200 = cv2.resize(img, (200, 200))
# 不直接指定縮放后大小,通過fx和fy指定縮放比例,0.5則長寬都為原來一半
# 等效于img_200x300 = cv2.resize(img, (300, 200)),注意指定大小的格式是(寬度,高度)
# 插值方法默認是cv2.INTER_LINEAR,這里指定為最近鄰插值
img_200x300 = cv2.resize(img, (0, 0), fx=0.5, fy=0.5,
interpolation=cv2.INTER_NEAREST)
# 在上張圖片的基礎上,上下各貼50像素的黑邊,生成300x300的圖像
img_300x300 = cv2.copyMakeBorder(img, 50, 50, 0, 0,
cv2.BORDER_CONSTANT,
value=(0, 0, 0))
# 對照片中樹的部分進行剪裁
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。
色調,明暗,直方圖和Gamma曲線
除了區域,圖像本身的屬性操作也非常多,比如可以通過HSV空間對色調和明暗進行調節。HSV空間是由美國的圖形學專家A. R. Smith提出的一種顏色空間,HSV分別是色調(Hue),飽和度(Saturation)和明度(Value)。在HSV空間中進行調節就避免了直接在RGB空間中調節是還需要考慮三個通道的相關性。OpenCV中H的取值是[0, 180),其他兩個通道的取值都是[0, 256),下面例子接著上面例子代碼,通過HSV空間對圖像進行調整:
# 通過cv2.cvtColor把圖像從BGR轉換到HSV
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# H空間中,綠色比黃色的值高一點,所以給每個像素+15,黃色的樹葉就會變綠
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)
# 減小飽和度會讓圖像損失鮮艷,變得更灰
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,我們都較難一眼就對像素中值的分布有細致的了解,這時候就需要直方圖。如果直方圖中的成分過于靠近0或者255,可能就出現了暗部細節不足或者亮部細節丟失的情況。比如圖6-2中,背景里的暗部細節是非常弱的。這個時候,一個常用方法是考慮用Gamma變換來提升暗部細節。Gamma變換是矯正相機直接成像和人眼感受圖像差別的一種常用手段,簡單來說就是通過非線性變換讓圖像從對曝光強度的線性響應變得更接近人眼感受到的響應。具體的定義和實現,還是接著上面代碼中讀取的圖片,執行計算直方圖和Gamma變換的代碼如下:
import numpy as np
# 分通道計算每個通道的直方圖
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矯正的函數
def gamma_trans(img, gamma):
# 具體做法是先歸一化到1,然后gamma作為指數值求出新的像素值再還原
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)
# 實現這個映射用的是OpenCV的查表函數
return cv2.LUT(img, gamma_table)
# 執行Gamma矯正,小于1的值讓暗部細節大量提升,同時亮部細節少量提升
img_corrected = gamma_trans(img, 0.5)
cv2.imwrite('gamma_corrected.jpg', img_corrected)
# 分通道計算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])
# 將直方圖進行可視化
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()
上面三段代碼的結果統一放在下圖中:
可以看到,Gamma變換后的暗部細節比起原圖清楚了很多,并且從直方圖來看,像素值也從集中在0附近變得散開了一些。
評論
查看更多