一、Mat生成圖片
面的簡單代碼就可以生成兩種表示方式下,圖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,就是把每個通道都單獨表達成一個二維矩陣,如圖所示。
二、基本圖像處理
1、存取圖像
讀圖像用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))
2、縮放,裁剪和補邊
縮放通過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)
注意:
//裁剪的時候,x,y是反的,正確的是rect=image[y0:yo+h0,x0:x0+w0]
這些處理的效果見圖6-2。
3、色調,明暗,直方圖和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('Channels') ax.set_zlabel('Counts') plt.show()
上面三段代碼的結果統一放在下圖中:
可以看到,Gamma變換后的暗部細節比起原圖清楚了很多,並且從直方圖來看,像素值也從集中在0附近變得散開了一些。
三、 圖像的仿射變換
圖像的仿射變換涉及到圖像的形狀位置角度的變化,是深度學習預處理中常到的功能,在此簡單回顧一下。仿射變換具體到圖像中的應用,主要是對圖像的縮放,旋轉,剪切,翻轉和平移的組合。在OpenCV中,仿射變換的矩陣是一個2×3的矩陣,其中左邊的2×2子矩陣是線性變換矩陣,右邊的2×1的兩項是平移項:
對於圖像上的任一位置(x,y),仿射變換執行的是如下的操作:
需要注意的是,對於圖像而言,寬度方向是x,高度方向是y,坐標的順序和圖像像素對應下標一致。所以原點的位置不是左下角而是右上角,y的方向也不是向上,而是向下。在OpenCV中實現仿射變換是通過仿射變換矩陣和cv2.warpAffine()這個函數,還是通過代碼來理解一下,例子中圖片的分辨率為600×400:
代碼實現的操作示意在下圖中:
import cv2 import numpy as np # 讀取一張斯里蘭卡拍攝的大象照片 img = cv2.imread('lanka_safari.jpg') # 沿着橫縱軸放大1.6倍,然后平移(-150,-240),最后沿原圖大小截取,等效於裁剪並放大 M_crop_elephant = np.array([ [1.6, 0, -150], [0, 1.6, -240] ], dtype=np.float32) img_elephant = cv2.warpAffine(img, M_crop_elephant, (400, 600)) cv2.imwrite('lanka_elephant.jpg', img_elephant) # 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, (400, 600)) cv2.imwrite('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, (400, 600)) cv2.imwrite('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, (400, 600)) cv2.imwrite('lanka_safari_transformed.jpg', img_transformed)
四、 基本繪圖
OpenCV提供了各種繪圖的函數,可以在畫面上繪制線段,圓,矩形和多邊形等,還可以在圖像上指定位置打印文字,比如下面例子:
import numpy as np import cv2 # 定義一塊寬600,高400的畫布,初始化為白色 canvas = np.zeros((400, 600, 3), dtype=np.uint8) + 255 # 畫一條縱向的正中央的黑色分界線 cv2.line(canvas, (300, 0), (300, 399), (0, 0, 0), 2) # 畫一條右半部份畫面以150為界的橫向分界線 cv2.line(canvas, (300, 149), (599, 149), (0, 0, 0), 2) # 左半部分的右下角畫個紅色的圓 cv2.circle(canvas, (200, 300), 75, (0, 0, 255), 5) # 左半部分的左下角畫個藍色的矩形 cv2.rectangle(canvas, (20, 240), (100, 360), (255, 0, 0), thickness=3) # 定義兩個三角形,並執行內部綠色填充 triangles = np.array([ [(200, 240), (145, 333), (255, 333)], [(60, 180), (20, 237), (100, 237)]]) cv2.fillPoly(canvas, triangles, (0, 255, 0)) # 畫一個黃色五角星 # 第一步通過旋轉角度的辦法求出五個頂點 phi = 4 * np.pi / 5 rotations = [[[np.cos(i * phi), -np.sin(i * phi)], [i * np.sin(phi), np.cos(i * phi)]] for i in range(1, 5)] pentagram = np.array([[[[0, -1]] + [np.dot(m, (0, -1)) for m in rotations]]], dtype=np.float) # 定義縮放倍數和平移向量把五角星畫在左半部分畫面的上方 pentagram = np.round(pentagram * 80 + np.array([160, 120])).astype(np.int) # 將5個頂點作為多邊形頂點連線,得到五角星 cv2.polylines(canvas, pentagram, True, (0, 255, 255), 9) # 按像素為間隔從左至右在畫面右半部份的上方畫出HSV空間的色調連續變化 for x in range(302, 600): color_pixel = np.array([[[round(180*float(x-302)/298), 255, 255]]], dtype=np.uint8) line_color = [int(c) for c in cv2.cvtColor(color_pixel, cv2.COLOR_HSV2BGR)[0][0]] cv2.line(canvas, (x, 0), (x, 147), line_color) # 如果定義圓的線寬大於半斤,則等效於畫圓點,隨機在畫面右下角的框內生成坐標 np.random.seed(42) n_pts = 30 pts_x = np.random.randint(310, 590, n_pts) pts_y = np.random.randint(160, 390, n_pts) pts = zip(pts_x, pts_y) # 畫出每個點,顏色隨機 for pt in pts: pt_color = [int(c) for c in np.random.randint(0, 255, 3)] cv2.circle(canvas, pt, 3, pt_color, 5) # 在左半部分最上方打印文字 cv2.putText(canvas, 'Python-OpenCV Drawing Example', (5, 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) cv2.imshow('Example of basic drawing functions', canvas) cv2.waitKey()