Python-OpenCV實戰二(影象的表示和處理)
影象的表示
在OpenCV的C++程式碼中,表示影象有個專門的結構叫做cv::Mat,不過在Python-OpenCV中,因為已經有了numpy這種強大的基礎工具,所以這個矩陣就用numpy的array表示。如果是多通道情況,最常見的就是紅綠藍(RGB)三通道,則第一個維度是高度,第二個維度是高度,第三個維度是通道,比如圖a是一幅3×3影象在計算機中表示的例子:
右上角的矩陣裡每個元素都是一個3維陣列,分別代表這個畫素上的三個通道的值。最常見的RGB通道中,第一個元素就是紅色(Red)的值,第二個元素是綠色(Green)的值,第三個元素是藍色(Blue),最終得到的影象如a所示。RGB是最常見的情況,然而在OpenCV中,預設的影象的表示確實反過來的,也就是BGR,得到的影象是b。可以看到,前兩行的顏色順序都交換了,最後一行是三個通道等值的灰度圖,所以沒有影響。至於OpenCV為什麼不是人民群眾喜聞樂見的RGB,這是歷史遺留問題,在OpenCV剛開始研發的年代,BGR是相機裝置廠商的主流表示方法,雖然後來RGB成了主流和預設,但是這個底層的順序卻保留下來了,事實上Windows下的最常見格式之一bmp,底層位元組的儲存順序還是BGR。OpenCV的這個特殊之處還是需要注意的,比如在Python中,影象都是用numpy的array表示,但是同樣的array在OpenCV中的顯示效果和matplotlib中的顯示效果就會不一樣。下面的簡單程式碼就可以生成兩種表示方式下,圖中矩陣的對應的影象,生成影象後,放大看就能體會到區別:
import numpy as np import cv2 import matplotlib.pyplot as plt 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,就是把每個通道都單獨表達成一個二維矩陣,如圖c所示。
基本影象處理
存取影象
讀影象用cv2.imread(),可以按照不同模式讀取,一般最常用到的是讀取單通道灰度圖,或者直接預設讀取多通道。存影象用cv2.imwrite(),注意存的時候是沒有單通道這一說的,根據儲存檔名的字尾和當前的array維度,OpenCV自動判斷存的通道,另外壓縮格式還可以指定儲存質量,來看程式碼例子:
import cv2 # 讀取一張640x480解析度的影象 color_img = cv2.imread('dog.jpg') color_img.shape
(480, 640, 3)
# 直接讀取單通道
gray_img = cv2.imread('dog.jpg',cv2.IMREAD_GRAYSCALE)
gray_img.shape
(480, 640)
# 把單通道圖片儲存後,再讀取,仍然是3通道,
# 相當於把單通道值複製到3個通道儲存
cv2.imwrite('test_grayscale.jpg',gray_img)
reload_grayscale = cv2.imread('test_grayscale.jpg')
print(reload_grayscale.shape) # (480, 640, 3)
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('data/moon.jpg')
# img.shape # (576, 1024, 3)
# 縮放成500x250的方形影象
img_500x250 = cv2.resize(img, (500, 250))
cv2.imwrite('resized_500x250.jpg', img_500x250)
不直接指定縮放後大小,通過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) # (288, 512, 3)
cv2.imwrite('resized_200x300.jpg', img_200x300)
# 在上張圖片的基礎上,上下各貼50畫素的黑邊,生成300x300的影象
img_300x300 = cv2.copyMakeBorder(img_200x300, 50, 50, 0, 0,
cv2.BORDER_CONSTANT,
value=(0, 0, 0))
print(img_300x300.shape) # (388, 512, 3)
cv2.imwrite('bordered_300x300.jpg', img_300x300)
對照片中樹的部分進行剪裁
patch_tree = img[371:660,335:575]
cv2.imwrite('cropped_tree.jpg', patch_tree)
色調,明暗,直方圖和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)
影象的仿射變換
影象的仿射變換涉及到影象的形狀位置角度的變化,是深度學習預處理中常到的功能, 在此簡單回顧一下。仿射變換具體到影象中的應用,主要是對影象的縮放,旋轉,剪下,翻轉和平移的組合
在OpenCV中,仿射變換的矩陣是一個2×3的矩陣,其中左邊的2×2子矩陣是線性變換矩陣,右邊的2×1的兩項是平移項:
需要注意的是,對於影象而言,寬度方向是x,高度方向是y,座標的順序和影象畫素對應下標一致。所以原點的位置不是左下角而是右上角,y的方向也不是向上,而是向下。在OpenCV中實現仿射變換是通過仿射變換矩陣和cv2.warpAffine()這個函式,還是通過程式碼來理解一下,例子中圖片的解析度為640×480:
import cv2
import numpy as np
img = cv2.imread('data/dog.jpg')
# 沿著橫縱軸放大1.6倍,然後平移(-150,-240),最後沿原圖大小擷取,
# 等效於裁剪並放大
M_crop_dog = np.array([
[1.6, 0, -150],
[0, 1.6, -240]
], dtype=np.float32)
img_dog = cv2.warpAffine(img,M_crop_dog,(640,480))
cv2.imwrite('data/lanka_dog.jpg',img_dog)
# x軸的剪下變換,角度15°
theta = 15 * np.pi / 180
M_shear = np.array([
[1, np.tan(theta), 0],
[0, 1, 0]
], dtype=np.float32)
img_sheared = cv2.warpAffine(img, M_shear, (640, 480))
cv2.imwrite('data/lanka_safari_sheared.jpg', img_sheared)
# 順時針旋轉,角度15°
M_rotate = np.array([
[np.cos(theta), -np.sin(theta), 0],
[np.sin(theta), np.cos(theta), 0]
], dtype=np.float32)
img_rotated = cv2.warpAffine(img, M_rotate, (640, 480))
cv2.imwrite('data/lanka_safari_rotated.jpg', img_rotated)
# 某種變換,具體旋轉+縮放+旋轉組合可以通過SVD分解理解
M = np.array([
[1, 1.5, -400],
[0.5, 2, -100]
], dtype=np.float32)
img_transformed = cv2.warpAffine(img, M, (640, 480))
cv2.imwrite('data/lanka_safari_transformed.jpg', img_transformed)