圖像、視頻加載與顯示
創建顯示窗口
import cv2 if __name__ == "__main__": # 創建窗口 cv2.namedWindow('new', cv2.WINDOW_NORMAL) # 調整窗口大小 cv2.resizeWindow('new', 640, 480) # 顯示窗口 cv2.imshow('new', 0) # 顯示時長 key = cv2.waitKey(0) if key == 'q': exit() # 銷燬窗口 cv2.destroyWindow()
運行結果
載入圖片
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/WechatIMG12.jpeg") cv2.imshow('img', img) while True: key = cv2.waitKey(0) if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
保存文件
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/WechatIMG12.jpeg") cv2.imshow('img', img) while True: key = cv2.waitKey(0) if key & 0xFF == ord('q'): break elif key & 0xFF == ord('s'): cv2.imwrite("/Users/admin/Documents/帥照.png", img) cv2.destroyAllWindows()
當我們點擊鍵盤"s"鍵的時候,運行結果
進入/Users/admin/Documents文件夾
(base) -bash-3.2$ ls | grep 帥照
帥照.png
攝像頭視頻採集
import cv2 if __name__ == "__main__": # 創建窗口 cv2.namedWindow('video', cv2.WINDOW_NORMAL) cv2.resizeWindow('video', 640, 480) # 獲取視頻設備 cap = cv2.VideoCapture(0) while True: # 從攝像頭讀視頻楨 ret, frame = cap.read() # 將視頻幀在窗口中顯示 cv2.imshow('video', frame) key = cv2.waitKey(1) if key & 0xFF == ord('q'): break # 釋放資源 cap.release() cv2.destroyAllWindows()
運行結果
這裏可以看到攝像頭已經打開,並開始採集視頻。
讀取視頻文件
我們這裏使用一段鸚鵡的視頻,使用命令ffplay查看每秒播放幀數
./ffplay cockatoo.mp4
ffplay version N-104454-gd92fdc7144-tessus https://evermeet.cx/ffmpeg/ Copyright (c) 2003-2021 the FFmpeg developers
built with Apple clang version 11.0.0 (clang-1100.0.33.17)
configuration: --cc=/usr/bin/clang --prefix=/opt/ffmpeg --extra-version=tessus --enable-avisynth --enable-fontconfig --enable-gpl --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libfreetype --enable-libgsm --enable-libmodplug --enable-libmp3lame --enable-libmysofa --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-version3 --pkg-config-flags=--static --enable-librtmp --enable-ffplay --enable-sdl2 --disable-ffmpeg --disable-ffprobe
libavutil 57. 7.100 / 57. 7.100
libavcodec 59. 12.100 / 59. 12.100
libavformat 59. 8.100 / 59. 8.100
libavdevice 59. 0.101 / 59. 0.101
libavfilter 8. 16.100 / 8. 16.100
libswscale 6. 1.100 / 6. 1.100
libswresample 4. 0.100 / 4. 0.100
libpostproc 56. 0.100 / 56. 0.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'cockatoo.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf56.4.101
Duration: 00:00:14.00, start: 0.000000, bitrate: 416 kb/s
Stream #0:0[0x1](und): Video: h264 (High 4:4:4 Predictive) (avc1 / 0x31637661), yuv444p(progressive), 1280x720, 387 kb/s, 20 fps, 20 tbr, 10240 tbn (default)
Metadata:
handler_name : VideoHandler
vendor_id : [0][0][0][0]
Stream #0:1[0x2](und): Audio: mp3 (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 24 kb/s (default)
Metadata:
handler_name : SoundHandler
vendor_id : [0][0][0][0]
13.63 A-V: -0.031 fd= 0 aq= 0KB vq= 0KB sq= 0B f=0/0
我們可以看到有這麼一段
Stream #0:0[0x1](und): Video: h264 (High 4:4:4 Predictive) (avc1 / 0x31637661), yuv444p(progressive), 1280x720, 387 kb/s, 20 fps, 20 tbr, 10240 tbn (default)
這裏有一個20 fps,說明該視頻是每秒播放20楨
import cv2 if __name__ == "__main__": # 創建窗口 cv2.namedWindow('video', cv2.WINDOW_NORMAL) cv2.resizeWindow('video', 640, 480) # 獲取視頻文件 cap = cv2.VideoCapture("/Users/admin/Downloads/cockatoo.mp4") while True: # 從文件讀視頻楨 ret, frame = cap.read() # 將視頻幀在窗口中顯示 cv2.imshow('video', frame) # 此處不能設爲1,否則會過快,可以設的比播放視頻每秒幀數長一點 key = cv2.waitKey(40) if key & 0xFF == ord('q'): break # 釋放資源 cap.release() cv2.destroyAllWindows()
運行結果
攝像頭採集數據輸出爲媒體文件
import cv2 if __name__ == "__main__": fourcc = cv2.VideoWriter_fourcc(*'MJPG') # 25爲幀率,(1280, 720)爲分辨率,該分辨率必須與設備攝像頭分辨率保持一致 vw = cv2.VideoWriter("/Users/admin/Documents/out.mp4", fourcc, 25, (1280, 720)) # 創建窗口 cv2.namedWindow('video', cv2.WINDOW_NORMAL) cv2.resizeWindow('video', 640, 480) # 獲取攝像頭資源 cap = cv2.VideoCapture(0) # 判斷攝像頭是否打開 while cap.isOpened(): # 從攝像頭讀視頻楨 ret, frame = cap.read() if ret: # 將視頻幀在窗口中顯示 cv2.imshow('video', frame) # 寫數據到多媒體文件 vw.write(frame) key = cv2.waitKey(1) if key & 0xFF == ord('q'): break else: break # 釋放資源 cap.release() vw.release() cv2.destroyAllWindows()
運行結果
(base) -bash-3.2$ ls | grep out
out.mp4
控制鼠標
import cv2 import numpy as np def mouse_callback(event, x, y, flags, userdata): print(event, x, y, flags, userdata) if __name__ == "__main__": cv2.namedWindow('mouse', cv2.WINDOW_NORMAL) cv2.resizeWindow('mouse', 640, 360) # 設置鼠標回調 cv2.setMouseCallback('mouse', mouse_callback, '123') # 設置背景爲黑色 img = np.zeros((360, 640, 3), np.uint8) while True: cv2.imshow('mouse', img) key = cv2.waitKey(1) if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
當鼠標在黑色區域滑動的時候,控制檯會將鼠標的座標給打印出來
0 272 156 0 123
0 272 155 0 123
0 272 155 0 123
0 271 155 0 123
0 271 155 0 123
TrackBar的使用
TrackBar就是一種滑動條,滑動到不同的位置,獲取相應的值做不同的處理。
import cv2 import numpy as np def callback(): pass if __name__ == "__main__": cv2.namedWindow('trackbar', cv2.WINDOW_NORMAL) # 創建trackbar,R是trackbar的名字,0是默認當前值,255是最大值 cv2.createTrackbar('R', 'trackbar', 0, 255, callback) cv2.createTrackbar('G', 'trackbar', 0, 255, callback) cv2.createTrackbar('B', 'trackbar', 0, 255, callback) # 純黑色背景 img = np.zeros((480, 640, 3), np.uint8) while True: cv2.imshow('trackbar', img) r = cv2.getTrackbarPos('R', 'trackbar') g = cv2.getTrackbarPos('G', 'trackbar') b = cv2.getTrackbarPos('B', 'trackbar') img[:] = [b, g, r] key = cv2.waitKey(10) if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
trackbar取不同的值會有不同的背景色
OpenCV的色彩空間
RGB人眼的色彩空間
每一個像素有三種顏色——紅色、綠色和藍色。通過不同光源的組合,形成真彩色,有暗的,有明亮的。
上圖中每一個方格都代表一個像素。
OpenCV默認使用的是BGR,BGR跟RGB的區別就是排列順序的不同。電腦上一般的排列順序都是RGB。
HSV/HSB/HSL
HSV代表的是色相、飽和度、明亮度。HSB和HSV是一個意思。
- Hue:色相,即色彩,如紅色、藍色
- Saturation:飽和度,顏色的純度,值越大,純度越高,最開始的時候是灰的,逐漸增大就純度越高,如果是紅色就是純紅,藍色就是純藍
- Value:明度,代表更暗一些還是更亮一些,當更暗的時候,黑色的程度越高;更亮一些就黑色成分少一些。
該圖中旋轉一圈的過程中代表了不同的顏色。對於飽和度來說,以中心點爲基礎,底下是黑色,上面是白色,中間是黑與白之間的灰。越靠近於圓柱邊緣的地方,顏色的純度越高。而對於縱軸,底下是黑色的,越往上越來越亮,這個就是明亮度。
對於OpenCV來說更喜歡使用HSV,使用HSV在背景判斷上要好過RGB,因爲在一個背景中可能有各種綠色,使用HSV就可以統一將背景判斷爲綠色,而使用RGB就不太好判斷,每一種成分都有。
判斷背景是通過色度來進行判斷的,上圖中0度就是純紅,60度就是黃色,120度爲綠色,180度爲青色,240度爲藍色,300度爲粉紅。這裏是不考慮從圓心到邊緣的漸變的一些因素的。
HSL
- Hue:色相,即色彩,如紅色、藍色
- Saturation:飽和度
- Ligthness:亮度
HSL與HSV看起來差不多,但存在着不同。
這裏左圖是HSL的,右圖是HSV的,對於HSL到最頂成的時候就是純白,無論色相是什麼,飽和度是什麼。而HSV就沒有這麼誇張。我們基本上使用的都是HSV,HSL幾乎是不使用的。
YUV
YUV主要用在視頻領域。Y代表的是灰色圖像,UV代表的是顏色。YUV來自於電視節目,以前的電視只有黑白電視,就只有這個Y,後來有了彩色電視,但是要兼容黑白電視劇,當彩色電視機播放黑白電視劇的時候就只播放這個Y。一般的YUV包含YUV4:2:0、YUV4:2:2、YUV4:4:4。
YUV4:2:0
上圖中,4個Y對應2個U或者V。不同的間隔,U或者V都是不一定的。
色彩空間轉換
import cv2 def callback(): pass if __name__ == "__main__": cv2.namedWindow('color', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") colorspaces = [cv2.COLOR_BGR2RGBA, cv2.COLOR_BGR2BGRA, cv2.COLOR_BGR2GRAY, cv2.COLOR_BGR2HSV_FULL, cv2.COLOR_BGR2YUV] cv2.createTrackbar('curcolor', 'color', 0, len(colorspaces) - 1, callback) while True: index = cv2.getTrackbarPos('curcolor', 'color') # 顏色空間轉換 cvt_img = cv2.cvtColor(img, colorspaces[index]) cv2.imshow('color', cvt_img) key = cv2.waitKey(10) if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
trackbar取值爲0的時候
trackbar取值爲1的時候
trackbar取值爲2的時候
trackbar取值爲3的時候
trackbar取值爲4的時候
ROI(Region of Image)
roi的意思是對圖像的一個區域進行提取
import cv2 if __name__ == "__main__": cv2.namedWindow('roi', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") print(img.shape) roi = img[0:550, 750:1350] while True: # 顏色空間轉換 cv2.imshow('roi', roi) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
(1080, 1920, 3)
我們這裏需要注意的是img是一個numpy三維矩陣,它的第一個維度是圖像的高,第二個維度是圖像的寬,第三個維度是圖像的通道數。
Mat
Mat就是矩陣,它的結構如下
對於Header頭部來說,存放的是一些屬性,包括維度、行數,列數。而Data是存放數據的地方,就是圖像中的實際像素。總體如下
字段 | 說明 | 字段 | 說明 |
---|---|---|---|
dims | 維度 | channels | 通道數 RGB是3 |
rows | 行數 | size | 矩陣大小 |
cols | 列數 | type | dep+dt+chs CV_8UC3 |
depth | 像素的位深 | data | 存放數據 |
Mat拷貝
這裏Mat A是第一個創建的Mat,Mat B是拷貝Mat A,這裏我們可以看到Mat A和Mat B的Header是兩部分,而Data是它們公用的,也就是說Mat A和Mat B的Header的指針指向的是同一塊內存空間。所以當我們用Mat B來拷貝Mat A的時候,默認情況下屬於淺拷貝。有關深淺拷貝的概念請參考淺析克隆
import cv2 if __name__ == "__main__": cv2.namedWindow('img1', cv2.WINDOW_NORMAL) cv2.namedWindow('img2', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") # 淺拷貝 img2 = img1 # 將圖像左上角變成紅色小方塊 img1[10:100, 10:100] = [0, 0, 255] while True: cv2.imshow('img1', img1) cv2.imshow('img2', img2) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
上面的兩張圖是兩個不同的窗口,它們的左上角都有一小塊紅色的方塊,說明,這種拷貝方式屬於淺拷貝。
import cv2 if __name__ == "__main__": cv2.namedWindow('img1', cv2.WINDOW_NORMAL) cv2.namedWindow('img2', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") # 深拷貝 img2 = img1.copy() # 將圖像左上角變成紅色小方塊 img1[10:100, 10:100] = [0, 0, 255] while True: cv2.imshow('img1', img1) cv2.imshow('img2', img2) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們可以看到img2的左上角是沒有紅色小方塊的,說明這是深拷貝。
Mat的屬性
import cv2 if __name__ == "__main__": img = cv2.imread("/Users/admin/Documents/111.jpeg") print(img.shape) # 圖像佔用內存空間數,高度*寬度*通道數 print(img.size) # 圖像中每個元素的位深 print(img.dtype)
運行結果
(1080, 1920, 3)
6220800
uint8
這裏我們可以看到每個元素的類型是uint8,說明它是一個無符號8位整型,是從0~255的範圍。
通道的分割與合併
這裏我們需要明白一個概念,任何的單通道圖像都是灰色的,而任何彩色圖像都必須是三通道的。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 分割圖像的三個通道 b, g, r = cv2.split(img) while True: cv2.imshow('img', b) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
那麼我們顯示的是藍色通道,爲什麼是黑白的呢?其實要顯示藍色通道的圖像依然要合併另外兩個通道,即紅色通道和綠色通道,只不過這兩個通道我們需要設置成純黑。
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 分割圖像的三個通道 b, g, r = cv2.split(img) filt = np.zeros((1080, 1920), np.uint8) # 合併三個通道,但只保留藍色通道信息 imgnew = cv2.merge((b, filt, filt)) while True: cv2.imshow('img', imgnew) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
如果我們要一張純藍色的圖片呢?
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) filt = np.zeros((1080, 1920), np.uint8) b = np.full((1080, 1920), 255, np.uint8) # 合併三個通道,但只保留藍色通道信息 imgnew = cv2.merge((b, filt, filt)) while True: cv2.imshow('img', imgnew) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
對比兩張圖片,我們可以看到,在純藍圖片中,藍色通道中的所有像素值都是255,而從111.jpeg中藍色通道的矩陣應該就是各不相同的像素大小最終顯示出來的效果。
OpenCV圖形繪製
畫線
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,5爲線寬 cv2.line(img, (750, 550), (1350, 550), (255, 255, 255), 5) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們來畫一條斜線,並增加鋸齒感
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
在視頻中畫線
import cv2 if __name__ == "__main__": cv2.namedWindow('video', cv2.WINDOW_NORMAL) cv2.resizeWindow('video', 640, 480) # 獲取視頻文件 cap = cv2.VideoCapture("/Users/admin/Downloads/cockatoo.mp4") while True: # 從文件讀視頻楨 ret, frame = cap.read() if ret: cv2.line(frame, (0, 600), (1280, 600), (0, 0, 255), 5) # 將視頻幀在窗口中顯示 cv2.imshow('video', frame) # 此處不能設爲1,否則會過快,可以設的比播放視頻每秒幀數長一點 key = cv2.waitKey(40) if key & 0xFF == ord('q'): break else: key = cv2.waitKey(40) if key & 0xFF == ord('q'): break # 釋放資源 cap.release() cv2.destroyAllWindows()
運行結果
畫矩形
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), 8) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
畫實心矩形
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
畫圓
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,5爲線寬,16的鋸齒度數 cv2.circle(img, (1050, 275), 275, (0, 0, 255), 5, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
畫實心圓
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
畫橢圓
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 360是橢圓的畫線部分的度數 cv2.ellipse(img, (1050, 275), (500, 275), 0, 0, 360, (0, 0, 255), 5, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
如果我們要繪製橢圓的下半部分
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 cv2.ellipse(img, (1050, 275), (500, 275), 0, 0, 180, (0, 0, 255), 5, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
如果我們要繪製橢圓的上半部分
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 cv2.ellipse(img, (1050, 275), (500, 275), 180, 0, 180, (0, 0, 255), 5, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
如果我們要繪製一些斜的橢圓
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 cv2.ellipse(img, (1050, 275), (500, 275), 60, 0, 360, (0, 0, 255), 5, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
如果我們要繪製一個與第一個橢圓垂直的橢圓
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 cv2.ellipse(img, (1050, 275), (500, 275), 90, 0, 360, (0, 0, 255), 5, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
如果我們要畫一個實體扇形
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
畫多邊形
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 # cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16) pts = np.array([(750, 550), (1350, 550), (1050, 0)], np.int32) # 畫一個多邊形(三角形),pts是三個點的座標 cv2.polylines(img, [pts], True, (0, 0, 255), 8, 16) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
畫實心多邊形
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 # cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16) pts = np.array([(750, 550), (1350, 550), (1050, 0)], np.int32) # 畫一個多邊形(三角形),pts是三個點的座標 # cv2.polylines(img, [pts], True, (0, 0, 255), 8, 16) # 畫一個實心多邊形(三角形),pts是三個點的座標 cv2.fillPoly(img, [pts], (0, 0, 255)) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏需要注意的是,畫實心多邊形和普通多邊形是兩個不同的API。
繪製文本
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 畫一條白線(750, 550)爲起始點座標,(1350, 550)爲終止點座標 # (255, 255, 255)爲顏色,用三通道表示,8爲線寬, # 1爲鋸齒度,越小鋸齒感越強,越大越平滑 # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1) # 畫一個紅框(750, 0)爲起始點座標,(1350, 550)爲終止點座標 # -1表示線寬無限大,即爲實心 # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1) # 畫一個紅色的圓,(1050, 275)爲圓心座標,275爲半徑,-1爲線寬無限大,即爲實心,16的鋸齒度數 # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16) # 畫一個紅色的橢圓,(1050, 275)爲中心點座標,(500, 275)爲長寬的一半 # 第一個0爲長方體角度起始值,第二個0爲長方體角度終止值 # 180是橢圓的畫線部分的度數 # cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16) # pts = np.array([(750, 550), (1350, 550), (1050, 0)], np.int32) # 畫一個多邊形(三角形),pts是三個點的座標 # cv2.polylines(img, [pts], True, (0, 0, 255), 8, 16) # 畫一個實心多邊形(三角形),pts是三個點的座標 # cv2.fillPoly(img, [pts], (0, 0, 255)) # 繪製一段文本,(750, 275)爲起始座標,cv2.FONT_ITALIC爲字體,8爲字體大小,(0, 0, 0)爲黑色,10爲字體粗細 cv2.putText(img, "Beauty", (750, 275), cv2.FONT_ITALIC, 8, (0, 0, 0), 10) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
鼠標繪製圖形
import cv2 import math if __name__ == "__main__": curshape = 0 startpos = (0, 0) def mouse_callback(event, x, y, flags, userdata): global startpos if event & cv2.EVENT_LBUTTONDOWN == cv2.EVENT_LBUTTONDOWN: startpos = (x, y) elif event & cv2.EVENT_LBUTTONUP == cv2.EVENT_LBUTTONUP: if curshape == 0: cv2.line(img, startpos, (x, y), (0, 0, 255), 8, 16) elif curshape == 1: cv2.rectangle(img, startpos, (x, y), (0, 0, 255), 8, 16) elif curshape == 2: a = x - startpos[0] b = y - startpos[1] r = int(math.sqrt(a**2 + b**2)) cv2.circle(img, startpos, r, (0, 0, 255), 8, 16) else: print("無效類型") cv2.namedWindow('drawshape', cv2.WINDOW_NORMAL) cv2.setMouseCallback('drawshape', mouse_callback, "123") img = cv2.imread("/Users/admin/Documents/111.jpeg") while True: cv2.imshow('drawshape', img) key = cv2.waitKey(1) & 0xFF if key == ord('q'): break elif key == ord('l'): # 畫線 curshape = 0 elif key == ord('r'): # 畫矩形 curshape = 1 elif key == ord('c'): # 畫圓 curshape = 2 cv2.destroyAllWindows()
運行結果
這上面的形狀都是用鼠標繪製出來的。
OpenCV的算術與位運算
圖像加法運算
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgadd', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = np.full((1080, 1920, 3), 50, np.uint8) result = cv2.add(img1, img2) while True: cv2.imshow('imgadd', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們可以看到圖像好像是增加了曝光。
圖像減法運算
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgsub', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = np.full((1080, 1920, 3), 100, np.uint8) result = cv2.subtract(img1, img2) while True: cv2.imshow('imgsub', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏需要注意的是,圖像的減法運算,兩個參數的位置不可調換,但是加法就沒有這個要求。上圖中就像是原圖得到了銳化。
圖像乘法運算
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgmul', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = np.full((1080, 1920, 3), 2, np.uint8) result = cv2.multiply(img1, img2) while True: cv2.imshow('imgmul', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
圖像的乘法運算如果乘的像素點太大,基本上整個圖像就看不清楚了。
圖像除法運算
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgdiv', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = np.full((1080, 1920, 3), 3, np.uint8) result = cv2.divide(img1, img2) while True: cv2.imshow('imgdiv', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
圖像融合
我們這裏有一張新的圖片
現在我們要將這張圖片與之前的美女的圖片進行融合。
import cv2 if __name__ == "__main__": cv2.namedWindow('imgadd', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = cv2.imread("/Users/admin/Documents/222.jpeg") print(img1.shape) print(img2.shape) # 融合兩張圖片,0.3和0.7分別表示兩張圖片融合的權重,0表示融合 # 後的圖片的所有元素都加0,表示靜態權重 result = cv2.addWeighted(img1, 0.3, img2, 0.7, 0) while True: cv2.imshow('imgadd', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
(1080, 1920, 3)
(1080, 1920, 3)
這裏需要注意的是,要融合的兩張圖片的高和寬以及通道數必須相等,纔可以進行融合。這裏兩張圖片都是1080*1920*3的圖像。
圖像位運算
- 非運算
非運算就是將圖像中的像素進行255-x操作(x爲原像素值)
import cv2 if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") new_img = cv2.bitwise_not(img) while True: cv2.imshow('imgbit', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這個有點像彩色照片的底片
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = np.full((1080, 1920, 3), 255, np.uint8) new_img = cv2.subtract(img2, img1) while True: cv2.imshow('imgbit', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
使用255-原圖像像素值與非運算效果一樣
- 與運算
與運算就是將圖像各個通道像素點值轉成二進制數按位與進行運算。
import cv2 if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = cv2.imread("/Users/admin/Documents/222.jpeg") new_img = cv2.bitwise_and(img1, img2) while True: cv2.imshow('imgbit', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
上面的代碼等同與
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = cv2.imread("/Users/admin/Documents/222.jpeg") result = np.bitwise_and(img1, img2) while True: cv2.imshow('imgbit', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
- 或運算
或運算就是將圖像各個通道像素點值轉成二進制數按位或進行運算。
import cv2 if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = cv2.imread("/Users/admin/Documents/222.jpeg") new_img = cv2.bitwise_or(img1, img2) while True: cv2.imshow('imgbit', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
上面的代碼等同於
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = cv2.imread("/Users/admin/Documents/222.jpeg") result = np.bitwise_or(img1, img2) while True: cv2.imshow('imgbit', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
- 異或運算
異或運算就是將圖像各個通道像素點值轉成二進制數按位異或進行運算。
import cv2 if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = cv2.imread("/Users/admin/Documents/222.jpeg") new_img = cv2.bitwise_xor(img1, img2) while True: cv2.imshow('imgbit', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
上面的代碼等同於
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") img2 = cv2.imread("/Users/admin/Documents/222.jpeg") result = np.bitwise_xor(img1, img2) while True: cv2.imshow('imgbit', result) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
添加水印
我們先把logo給畫出來,看看是什麼樣子的
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imlog', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/111.jpeg") logo = np.zeros((200, 200, 3), np.uint8) mask = np.zeros((200, 200), np.uint8) logo[20:120, 20:120] = [0, 0, 255] logo[80:180, 80:180] = [0, 255, 0] while True: cv2.imshow('imlog', logo) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們要將該logo放入到圖片裏面去
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('imlog', cv2.WINDOW_NORMAL) # 導入圖片 img = cv2.imread("/Users/admin/Documents/111.jpeg") # 創建logo和mask logo = np.zeros((200, 200, 3), np.uint8) mask = np.full((200, 200), 255, np.uint8) # 繪製logo logo[20:120, 20:120] = [0, 0, 255] # 該區域爲紅色 logo[80:180, 80:180] = [0, 255, 0] # 該區域爲綠色 mask[20:120, 20:120] = 0 # 該區域爲黑色 mask[80:180, 80:180] = 0 # 該區域爲黑色 # 選擇圖像添加logo的位置,並提取該部分的圖像像素(3通道) roi = img[0:200, 0:200] # 對mask進行位與操作(0與任何值位與都是0,即爲黑色,而任何值與255(二進制11111111)) # 進行位與操作都是該值本身 # 這裏roi是三通道,而mask爲單通道, # 則這裏爲roi的每個通道都與mask進行位與操作 # 最終roi的位置與mask爲0的部分變爲0,其他部分保留roi其像素本身 tmp = cv2.bitwise_and(roi, roi, mask=mask) # 0加任何數等於任何數,所以這裏roi爲0的位置變成logo的像素 # 而logo爲0的部分保留roi的像素值 dst = cv2.add(tmp, logo) # 將得到的圖像放入原始圖像中 img[0:200, 0:200] = dst while True: cv2.imshow('imlog', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
圖像基本變換
圖像放大與縮小
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") new_img = cv2.resize(img, (200, 200)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們可以看到在進行縮小的過程中,圖像是有失真的。我們也可以直接使用比例縮放
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") new_img = cv2.resize(img, None, fx=0.3, fy=0.3) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們來看一下一張小圖的放大會是什麼效果,原圖如下
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_AUTOSIZE) img = cv2.imread("/Users/admin/Documents/333.jpeg") # 圖像縮放,cv2.INTER_AREA表示效果最好 # cv2.INTER_NEAREST表示臨近插值 new_img = cv2.resize(img, None, fx=3, fy=3, interpolation=cv2.INTER_AREA) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
圖像翻轉
上下翻轉
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 上下翻轉 new_img = cv2.flip(img, 0) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
左右翻轉
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 0上下翻轉,大於0左右翻轉 new_img = cv2.flip(img, 1) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
上下+左右翻轉
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 0上下翻轉,大於0左右翻轉,小於0上下+左右翻轉 new_img = cv2.flip(img, -1) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
圖像旋轉
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 0上下翻轉,大於0左右翻轉,小於0上下+左右翻轉 # new_img = cv2.flip(img, -1) # 順時針旋轉90度 new_img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 0上下翻轉,大於0左右翻轉,小於0上下+左右翻轉 # new_img = cv2.flip(img, -1) # 順時針旋轉180度 new_img = cv2.rotate(img, cv2.ROTATE_180) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 0上下翻轉,大於0左右翻轉,小於0上下+左右翻轉 # new_img = cv2.flip(img, -1) # 逆時針旋轉90度 new_img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏需要注意的是,cv2.rotate無法旋轉任意一個角度,只有這麼三個角度可以旋轉。
圖像的仿射變換
仿射變換是圖像旋轉、縮放、平移的總稱。
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") h, w, ch = img.shape # 變換矩陣之右平移100像素,下平移100像素 M = np.float32([[1, 0, 100], [0, 1, 100]]) # 圖像的仿射變換,M是變換矩陣,(w, h)是圖像大小 new_img = cv2.warpAffine(img, M, (w, h)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏需要說明的是,變換矩陣是線性代數中的基礎,可以參考線性代數整理 中的圖形變換矩陣
之前我們在看圖形的旋轉中,只能旋轉3個角度,無法任意旋轉,現在我們就自己定義旋轉的變換矩陣來讓圖片旋轉任意角度
這個是使圖形旋轉的變換矩陣公式,現在我們來讓圖形逆時針旋轉15度(圍繞(0,0)旋轉)
import cv2 import numpy as np import math if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") h, w, ch = img.shape # 變換矩陣之右平移100像素,下平移100像素 # M = np.float32([[1, 0, 100], [0, 1, 100]]) theta = math.pi / 12 # 將圖像逆時針旋轉15度 M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]]) # 圖像的仿射變換,M是變換矩陣,(w, h)是圖像大小 new_img = cv2.warpAffine(img, M, (w, h)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
又比如是將圖像進行拉伸的變換矩陣
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") h, w, ch = img.shape # 變換矩陣之右平移100像素,下平移100像素 # M = np.float32([[1, 0, 100], [0, 1, 100]]) # theta = math.pi / 12 # 將圖像逆時針旋轉15度 # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]]) # 將圖像進行拉伸 M = np.float32([[1, 0.3, 0], [0, 1, 0]]) # 圖像的仿射變換,M是變換矩陣,(w, h)是圖像大小 new_img = cv2.warpAffine(img, M, (w, h)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
獲取變換矩陣
由於上述變換矩陣需要特定的變換矩陣公式,OpenCV提供了一種獲取該變換矩陣的方法
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") h, w, ch = img.shape # 變換矩陣之右平移100像素,下平移100像素 # M = np.float32([[1, 0, 100], [0, 1, 100]]) # theta = math.pi / 12 # 將圖像逆時針旋轉15度 # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]]) # 將圖像進行拉伸 # M = np.float32([[1, 0.3, 0], [0, 1, 0]]) # 獲取變換矩陣,(w / 2, h / 2)爲圖像中心點,逆時針旋轉15度,1.0表示不縮放 M = cv2.getRotationMatrix2D((w / 2, h / 2), 15, 1.0) # 圖像的仿射變換,M是變換矩陣,(w, h)是圖像大小 new_img = cv2.warpAffine(img, M, (w, h)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們再將圖像旋轉後進行縮放
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") h, w, ch = img.shape # 變換矩陣之右平移100像素,下平移100像素 # M = np.float32([[1, 0, 100], [0, 1, 100]]) # theta = math.pi / 12 # 將圖像逆時針旋轉15度 # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]]) # 將圖像進行拉伸 # M = np.float32([[1, 0.3, 0], [0, 1, 0]]) # 獲取變換矩陣,(w / 2, h / 2)爲圖像中心點,逆時針旋轉15度,0。6表示縮小爲60% M = cv2.getRotationMatrix2D((w / 2, h / 2), 15, 0.6) # 圖像的仿射變換,M是變換矩陣,(w, h)是圖像大小 new_img = cv2.warpAffine(img, M, (w, h)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
變換矩陣二
在上圖中由三個綠點來獲取變換矩陣。注意,這裏左右兩個圖的三個綠點都要給出,左圖的三個綠點叫做src,右圖的三個綠點叫做dst。
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") h, w, ch = img.shape # 變換矩陣之右平移100像素,下平移100像素 # M = np.float32([[1, 0, 100], [0, 1, 100]]) # theta = math.pi / 12 # 將圖像逆時針旋轉15度 # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]]) # 將圖像進行拉伸 # M = np.float32([[1, 0.3, 0], [0, 1, 0]]) # 獲取變換矩陣,(w / 2, h / 2)爲圖像中心點,逆時針旋轉15度,0。6表示縮小爲60% # M = cv2.getRotationMatrix2D((w / 2, h / 2), 15, 0.6) src = np.float32([[400, 300], [800, 300], [400, 1000]]) dst = np.float32([[200, 400], [600, 500], [150, 1100]]) M = cv2.getAffineTransform(src, dst) # 圖像的仿射變換,M是變換矩陣,(w, h)是圖像大小 new_img = cv2.warpAffine(img, M, (w, h)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
透視變換
透視變換是將拍攝的比較傾斜的圖轉換成比較方正的圖,這裏我們使用一張非常傾斜的圖
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/444.jpeg") print(img.shape) # 源數據圖取大概一個梯形形狀 src = np.float32([[474, 100], [1659, 100], [0, 1200], [1896, 1200]]) dst = np.float32([[0, 0], [2300, 0], [0, 2400], [2300, 2400]]) # 獲取轉換矩陣 M = cv2.getPerspectiveTransform(src, dst) # 進行透視變換 new_img = cv2.warpPerspective(img, M, (2300, 2400)) while True: cv2.imshow('img', new_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
OpenCV中的濾波器
圖像濾波
一副圖像通過濾波器得到另一幅圖像。其中濾波器又稱爲卷積核,濾波的過程稱爲卷積。有關卷積核的內容請參考Tensorflow深度學習算法整理 ,這裏不再贅述。
低通濾波與高通濾波
- 低通濾波可以去除噪音或平滑圖像。比方說這樣一個卷積核(一般可以用於美顏)
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 5*5的卷積核 kernal = np.ones((5, 5), np.float32) / 25 # 低通濾波處理(卷積操作),-1表示不改變卷積後的圖像大小 dst = cv2.filter2D(img, -1, kernal) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這張圖可能看起來不是特別明顯,不過沒關係。
- 高通濾波可以幫助查找圖像的邊緣。(一般可以用於摳圖)
方盒濾波與均值濾波
對於卷積核,如果我們自己去手工創建卷積核可能比較困難,OpenCV爲我們提供了一些常用的卷積核作爲濾波器。
- 方盒濾波
參數a的作用:
- normalize=True,a = 1/(W*H),這裏W、H是濾波器的寬和高,這是一個均值濾波
- normalize=False,a = 1
當normalize==True的時候,方盒濾波==均值濾波,一般情況下,我們使用的都是均值濾波。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 5*5的卷積核 # kernal = np.ones((5, 5), np.float32) / 25 # 低通濾波處理(卷積操作),-1表示不改變卷積後的圖像大小 # dst = cv2.filter2D(img, -1, kernal) # 均值濾波,效果跟上面相同 dst = cv2.blur(img, (5, 5)) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
我們再來看一下方盒濾波
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 5*5的卷積核 # kernal = np.ones((5, 5), np.float32) / 25 # 低通濾波處理(卷積操作),-1表示不改變卷積後的圖像大小 # dst = cv2.filter2D(img, -1, kernal) # 方盒濾波 dst = cv2.boxFilter(img, -1, (5, 5), normalize=False) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
高斯濾波
高斯濾波是一種呈現正態分佈的濾波器,當圖像中的噪音呈正態分佈的時候,可以使用高斯濾波來矯正。
它的原理就是在我們的卷積核中,中心點不是最大的,但是比重是最大的,而周圍的點可能比較大,但比重比較低。總之就是越靠邊上,比重越低;越靠近中心,比重越高。
- 高斯權重
在上圖中,我們可以看到,中心點25的權重爲14.7%,最高,靠近中心點的上下左右的權重爲11.83%,而四個角的權重只有9.47%,最低。
我們先來給美女圖像添加高斯噪聲。
import cv2 import numpy as np if __name__ == "__main__": def gasuss_noise(image, mean=0, var=0.001): ''' 添加高斯噪聲 mean : 均值 var : 方差 ''' image = np.array(image / 255, dtype=np.float) noise = np.random.normal(mean, var ** 0.5, image.shape) out = image + noise if out.min() < 0: low_clip = -1. else: low_clip = 0. out = np.clip(out, low_clip, 1.0) out = np.uint8(out * 255) return out cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") img_out = gasuss_noise(img, mean=0, var=0.001) # 5*5的卷積核 # kernal = np.ones((5, 5), np.float32) / 25 # 低通濾波處理(卷積操作),-1表示不改變卷積後的圖像大小 # dst = cv2.filter2D(img, -1, kernal) # 方盒濾波 # dst = cv2.boxFilter(img, -1, (5, 5), normalize=False) while True: cv2.imshow('img', img_out) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們再使用高斯濾波器來進行處理
import cv2 import numpy as np if __name__ == "__main__": def gasuss_noise(image, mean=0, var=0.001): ''' 添加高斯噪聲 mean : 均值 var : 方差 ''' image = np.array(image / 255, dtype=np.float) noise = np.random.normal(mean, var ** 0.5, image.shape) out = image + noise if out.min() < 0: low_clip = -1. else: low_clip = 0. out = np.clip(out, low_clip, 1.0) out = np.uint8(out * 255) return out cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") img_out = gasuss_noise(img, mean=0, var=0.001) # 5*5的卷積核 # kernal = np.ones((5, 5), np.float32) / 25 # 低通濾波處理(卷積操作),-1表示不改變卷積後的圖像大小 # dst = cv2.filter2D(img, -1, kernal) # 方盒濾波 # dst = cv2.boxFilter(img, -1, (5, 5), normalize=False) # 高斯濾波,sigmaX和sigmaY是輻射半徑 dst = cv2.GaussianBlur(img_out, (33, 33), sigmaX=50, sigmaY=50) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
中值濾波
假設我們使用3*3的卷積核去遍歷圖像,每取得一個區域的像素的時候,就將該區域的圖像像素進行排序,比如[1,5,5,5,6,7,8,9,11],取中間值作爲卷積後的結果值。這裏就是6.
中值濾波的優點是對胡椒噪音(在圖像中分佈隨機的噪音點)效果明顯。
我們先來對美女圖像生成胡椒噪音
import cv2 import numpy as np import random if __name__ == "__main__": def sp_noise(image, prob): ''' 添加椒鹽噪聲 prob:噪聲比例 ''' output = np.zeros(image.shape, np.uint8) thres = 1 - prob for i in range(image.shape[0]): for j in range(image.shape[1]): rdn = random.random() if rdn < prob: output[i][j] = 0 elif rdn > thres: output[i][j] = 255 else: output[i][j] = image[i][j] return output cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") out_img = sp_noise(img, 0.02) while True: cv2.imshow('img', out_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們再使用中值濾波器來進行處理
import cv2 import numpy as np import random if __name__ == "__main__": def sp_noise(image, prob): ''' 添加椒鹽噪聲 prob:噪聲比例 ''' output = np.zeros(image.shape, np.uint8) thres = 1 - prob for i in range(image.shape[0]): for j in range(image.shape[1]): rdn = random.random() if rdn < prob: output[i][j] = 0 elif rdn > thres: output[i][j] = 255 else: output[i][j] = image[i][j] return output cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") out_img = sp_noise(img, 0.02) # 中值濾波 dst = cv2.medianBlur(out_img, 5) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們再來看另外一張圖
import cv2 import numpy as np import random if __name__ == "__main__": def sp_noise(image, prob): ''' 添加椒鹽噪聲 prob:噪聲比例 ''' output = np.zeros(image.shape, np.uint8) thres = 1 - prob for i in range(image.shape[0]): for j in range(image.shape[1]): rdn = random.random() if rdn < prob: output[i][j] = 0 elif rdn > thres: output[i][j] = 255 else: output[i][j] = image[i][j] return output cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/555.jpeg") # out_img = sp_noise(img, 0.02) # 中值濾波 dst = cv2.medianBlur(img, 15) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
雙邊濾波
雙邊濾波的優點:可以保留邊緣,同時可以對邊緣內的區域進行平滑處理。他的主要作用是進行美顏。
- 雙邊濾波的原理
在上圖中的a輸入中,有一個高度,這個高度代表顏色的邊緣,顏色的落差特別大。上下兩塊代表邊緣的兩部分。對於雙邊濾波來說,對於邊緣不會做處理,但是對於邊緣的兩部分的突起的部分給抹平。最終輸出的就是d圖。對於b空域核和c值域核,它們影響的是不同的地點的。b空域核影響的是顏色的落差邊緣,c值域核影響的是邊緣之外的平滑效果的。
現在依然來對美女圖片進行雙邊濾波,先看一下原圖,頭髮和臉的顏色都略顯暗淡。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 雙邊濾波,7代表邊緣直徑,20表示空域核的值,意思爲顏色的變化範圍 # 在這個範圍認爲都是相同的顏色 # 50爲值域核的值,意思爲在平面上進行平滑處理的範圍 dst = cv2.bilateralFilter(img, 7, 20, 50) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
經過雙邊濾波後,我們可以看到,頭髮和臉都變得更加紅潤有光澤,眼睛也更加明亮。
高通濾波——索貝爾算子
前面的都是低通濾波,主要作用是消除噪音,平滑圖像。OpenCV也提供了很多高通濾波。
常見的高通濾波:
- Sobel(索貝爾),對噪音適應性很強,在內部實現中使用了高斯濾波,對噪音首先進行了過濾,之後通過一階導數求得圖像的邊緣。有很多算法都是使用索貝爾爲基礎的。
- Scharr(沙爾),卷積核不可改變的,尺寸是固定的,3*3大小的一個卷積核,如果索貝爾的size設成-1,則自動使用的是沙爾濾波算法,所以一般情況下使用的是索貝爾,很少使用沙爾。對於3*3的卷積核來說,索貝爾的效果是沒有沙爾好的,因爲沙爾可以檢測出更細的邊緣線。而索貝爾就比較粗糙一些,但它可以改變卷積核大小。對於索貝爾和沙爾來說,在計算邊緣的時候,只能求一個方向的,要麼是橫軸要麼是縱軸。最終的結果我們需要將橫軸檢測的邊緣與縱軸檢測的邊緣加在一起,做一次加法運算才能得出最終的結果。
- Laplacian(拉普拉斯),不用單獨求橫軸或者縱軸邊緣,它一下就能將橫軸和縱軸的邊緣全部檢測出來。但是對於噪音比較敏感,在內部沒有降噪的功能,所以在使用拉普拉斯之前還需要手工降噪。這樣才能更好的體驗出拉普拉斯的效果。
Sobel算子:先向x方向求導,然後在y方向求導,最終結果:|G|=|Gx|+|Gy|
我們先來看一下只求一個方向導數的結果,這裏先向y方向求導。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5) while True: cv2.imshow('img', d1) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們再來看一下只向x方向求導
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5) # 向x方向求導 d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5) while True: cv2.imshow('img', d2) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
最終我們將兩個方向求導的結果加起來
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # 向x方向求導 d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) dst = cv2.add(d1, d2) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們再來看一張這個圖,效果會更加明顯
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/888.jpg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # 向x方向求導 d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) dst = cv2.add(d1, d2) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
沙爾算子
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # 向x方向求導 # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) # 沙爾濾波,它的卷積核大小固定爲3*3,求y方向導數 d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0) # dst = cv2.add(d1, d2) while True: cv2.imshow('img', d1) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # 向x方向求導 # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) # 沙爾濾波,它的卷積核大小固定爲3*3,求y方向導數 d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0) # 求x方向導數 d2 = cv2.Scharr(img, cv2.CV_64F, 0, 1) # dst = cv2.add(d1, d2) while True: cv2.imshow('img', d2) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # 向x方向求導 # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) # 沙爾濾波,它的卷積核大小固定爲3*3,求y方向導數 d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0) # 求x方向導數 d2 = cv2.Scharr(img, cv2.CV_64F, 0, 1) dst = cv2.add(d1, d2) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
拉普拉斯算子
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 索貝爾濾波,cv2.CV_64F是位深,即數據類型,這裏是64位float類型,1表示向y方向求導 # 0表示不向x方向求導,ksize爲卷積核大小 # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # 向x方向求導 # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) # 沙爾濾波,它的卷積核大小固定爲3*3,求y方向導數 # d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0) # 求x方向導數 # d2 = cv2.Scharr(img, cv2.CV_64F, 0, 1) # dst = cv2.add(d1, d2) # 拉普拉斯濾波 dst = cv2.Laplacian(img, cv2.CV_64F, ksize=5) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
邊緣檢測Canny
Canny是圖像邊緣檢測的終極大法,由於之前的三個算子有着這樣那樣的問題,我們來看看Canny的優勢
- 使用5*5高斯濾波消除噪音
- 計算圖像梯度的方向(0°/45°/90°/135°)
- 取局部極大值
- 閾值計算
如果超過來maxVal最大值,肯定是邊緣;如果低於minVal最小值,肯定不是邊緣。如果在最大值和最小值之間,則要看所確定的值與之前是否是連貫的,比如說A是一個邊緣,C與A在連線上,所以C也是一個邊緣。對於B來說,也在最大值和最小值之間,由於它不在邊緣的一條線上,所以B不是邊緣。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # Canny濾波,50爲最小值,160爲最大值 dst = cv2.Canny(img, 50, 160) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏隨着最小值和最大值的調整,它檢測出來的邊緣是不一樣的,比如我調小最大值
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # Canny濾波,50爲最小值,100爲最大值 dst = cv2.Canny(img, 50, 100) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們可以看到它會把更多地方視爲邊緣。在後面進行目標檢測的時候就會使用Canny進行輪廓的查找。
OpenCV中的形態學
形態學概述
- 什麼是形態學處理:
- 基於圖像形態進行處理的一些基本方法。比如說圖片中有一個杯子,形態學可以幫我們找到這個杯子在哪,它並不關心我們要找的是什麼。
- 這些處理方法基本是對二進制圖像進行處理(即黑白圖像)。如果我們拿到的是一個彩色圖像,則需要先進行轉換成灰色圖像。
- 卷積核決定着圖像處理後的效果
- 形態學圖像處理方式:
- 腐蝕與膨脹。腐蝕的意思就是將一個區域變小。膨脹的意思就是將一個區域變大。
- 開運算,就是先做腐蝕再做膨脹。
- 閉運算,就是先做膨脹再做腐蝕。
- 頂帽
- 黑帽
頂帽和黑帽都是一些插值處理
圖像全局二值化
什麼是二值化:
- 將圖像的每個像素變成兩種值,如0,255。對於灰色圖像,它是有灰色程度的,它是一層一層由黑到白(即非0和255的中間值),有梯度的。我們在進行形態處理的時候,如果有梯度處理起來就比較麻煩。
- 全局二值化,我們選出一個閾值,所有的像素都和這個閾值做對比,如果低於這個閾值,就變成0;如果高於這個閾值,就變成255。
- 局部二值化,將圖形分成很多的域,很多的小塊,在每一個小塊裏再做二值化。這樣就可以對一些光線的圖像,特別暗的部分處理起來就會有特別的效果。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 將彩色圖像轉成灰色圖像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,180爲閾值,255是超過閾值的轉化值 # cv2.THRESH_BINARY是類型,還有一個反向的cv2.THRESH_BINARY_INV # 低於閾值的變成255,高於閾值的變成0 ret, dst = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們來看一下相反值的情況
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 將彩色圖像轉成灰色圖像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,180爲閾值,255是超過閾值的轉化值 # cv2.THRESH_BINARY是類型,還有一個反向的cv2.THRESH_BINARY_INV # 低於閾值的變成255,高於閾值的變成0 ret, dst = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY_INV) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們也可以通過改變閾值來看一下變化
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") # 將彩色圖像轉成灰色圖像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,180爲閾值,255是超過閾值的轉化值 # cv2.THRESH_BINARY是類型,還有一個反向的cv2.THRESH_BINARY_INV # 低於閾值的變成255,高於閾值的變成0 ret, dst = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
閾值類型
前面我們只介紹了兩種類型的閾值類型,就是THRESH_BINARY以及THRESH_BINARY_INV,也就是上圖中的第二項和第三項。而第一項是一個原始圖,表示像素點的不同值,它可能有峯值和谷值。這裏是以中間的線爲閾值的,所以對於THRESH_BINARY來說就是其原始圖像素轉化圖。而對於THRESH_BINARY_INV來說與THRESH_BINARY來說剛好相反。
對於第四項TRUNCATE來說,實際就是進行了削峯操作,大於閾值的值都變成了閾值本身,小於閾值的值保留原值。對於第五項THRESH_TO_ZERO_INV和第六項THRESH_TO_ZERO來說也是一個相反的過程,THRESH_TO_ZERO是將小於閾值的值變成0,保留大於閾值的原值。THRESH_TO_ZERO_INV是將大於閾值的值變成0,保留小於閾值的原值。TRUNCATE、THRESH_TO_ZERO、THRESH_TO_ZERO_INV是用的比較少的類型,我們一般使用的是THRESH_BINARY和THRESH_BINARY_INV。
自適應閾值二值化
由於光照不均勻以及陰影的存在,只有一個閾值會使得在陰影處的白色被二值化成黑色。
我們現在用一張兒童思維導圖來看一下
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/999.jpeg") # 將彩色圖像轉成灰色圖像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,180爲閾值,255是超過閾值的轉化值 # cv2.THRESH_BINARY是類型,還有一個反向的cv2.THRESH_BINARY_INV # 低於閾值的變成255,高於閾值的變成0 ret, dst = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
對比兩張圖,我們可以看見雖然二值圖保留了原圖的絕大部分信息,但還是有很多的信息丟失了,現在我們使用自適應的二值化來處理
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/999.jpeg") # 將彩色圖像轉成灰色圖像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,180爲閾值,255是超過閾值的轉化值 # cv2.THRESH_BINARY是類型,還有一個反向的cv2.THRESH_BINARY_INV # 低於閾值的變成255,高於閾值的變成0 # ret, dst = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV) # 自適應二值化,255爲最大值,cv2.ADAPTIVE_THRESH_GAUSSIAN_C爲高斯窗口加權平均值 # 即越在中心點權值越重,越靠近邊緣權值越低, # 還有一種爲cv2.ADAPTIVE_THRESH_MEAN_C,表示計算臨近區域的平均值 # 11爲鄰近區域的大小,0爲從計算的平均值或加權平均值重減去的值 dst = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 0) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這樣我們可以看到所有該保留的信息的紋路。我們也可以將顏色給反過來
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/999.jpeg") # 將彩色圖像轉成灰色圖像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,180爲閾值,255是超過閾值的轉化值 # cv2.THRESH_BINARY是類型,還有一個反向的cv2.THRESH_BINARY_INV # 低於閾值的變成255,高於閾值的變成0 # ret, dst = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV) # 自適應二值化,255爲最大值,cv2.ADAPTIVE_THRESH_GAUSSIAN_C爲高斯窗口加權平均值 # 即越在中心點權值越重,越靠近邊緣權值越低, # 還有一種爲cv2.ADAPTIVE_THRESH_MEAN_C,表示計算臨近區域的平均值 # 11爲鄰近區域的大小,0爲從計算的平均值或加權平均值重減去的值 dst = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 0) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
無論是哪種方式,我們都希望除了紋路之外,其他部分都是純白色或者純黑色的,而不是有這麼多的噪點,這個可以後面來處理。
腐蝕
腐蝕的原理
上圖的背景是一個黑底,中間有白色區域的原始圖像。左上角是一個5*5的卷積核,其中的每一項都是1。經過這個卷積核掃描過的圖像部分,如果圖像部分都是黑色(像素都是0),那麼圖像的部分不會發生改變,因爲0乘以任何數依然爲0。當卷積核經過白色區域進行掃描的時候,我們知道卷積操作會將一個輸入圖像變成一個輸出圖像,而5*5=25個像素會變成一個像素,該像素會保留在卷積核中心點的位置上,這個值就是255,卷積操作不會修改原圖,它會將這個255放到一個新圖中,位置就在原圖的卷積核中心的位置。當然在這個卷積操作過程中會採用padding技術,所以新圖和原圖會保持尺寸相等。新圖的初始化狀態我們可以認爲是一個全0的二維數組,即全黑的。在卷積核不斷對原圖進行卷積操作的過程中,將卷積的結果像素值不斷填充到新圖的過程。這裏需要注意的是圖中的外圍虛線部分的白色區域在原圖中是不存在的,只是使用了padding技術對原圖的外圍進行了擴充,全部填充了0罷了。
一般腐蝕的卷積核大小有3*3、5*5、7*7,它的重點是卷積核內的值都是1.
它的腐蝕過程如上圖所示,A的區域爲全白的區域,經過B這個3*3的腐蝕卷積核腐蝕後,就變成了右邊的的區域。
我們來看一個例子,原圖如下
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1212.jpg") # 3*3的卷積核 kernel = np.ones((3, 3), np.uint8) # 腐蝕操作,iterations = 8爲卷積次數 dst = cv2.erode(img, kernel, iterations=8) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們可以看到這個i瘦成了一道閃電。
卷積核的類型
之前我們用numpy自己創建了一個卷積核,但其實OpenCV爲我們提供了幾種卷積核。
- MORPH_RECT,這種卷積核就是跟我們自己創建的卷積核是一樣的,正方形的卷積核,大小自己設定。
- MORPH_ELLIPSE,這種卷積核是橢圓形的卷積核,它的四個角都是0.
- MORPH_CROSS,這種卷積核是一種交叉的卷積核
但我們經常用的還是MORPH_RECT
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1212.jpg") # 3*3的正方形卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 腐蝕操作,iterations = 8爲卷積次數 dst = cv2.erode(img, kernel, iterations=8) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果與之前相同。現在我們來看一下幾種不同的卷積核的樣子
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) print(kernel)
運行結果
[[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]]
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) print(kernel)
運行結果
[[0 0 0 1 0 0 0]
[0 1 1 1 1 1 0]
[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]
[1 1 1 1 1 1 1]
[0 1 1 1 1 1 0]
[0 0 0 1 0 0 0]]
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (7, 7)) print(kernel)
運行結果
[[0 0 0 1 0 0 0]
[0 0 0 1 0 0 0]
[0 0 0 1 0 0 0]
[1 1 1 1 1 1 1]
[0 0 0 1 0 0 0]
[0 0 0 1 0 0 0]
[0 0 0 1 0 0 0]]
膨脹
膨脹剛好與腐蝕相反
上圖中中間的虛線部分是原圖A,A經過卷積核B卷積之後,變成了實線部分,進行了擴張。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1212.jpg") # 3*3的正方形卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3)) # 膨脹操作 dst = cv2.dilate(img, kernel, iterations=12) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們可以看到這個i膨脹成了一道閃電。
開運算
開運算= 腐蝕 + 膨脹
開運算可以去除上面這張圖中的黑色區域的白色噪點。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1313.jpg") # 13*13的正方形卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13)) # 開運算,以cv2.MORPH_OPEN標識 dst = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們需要注意的是,由於開運算只是做一次腐蝕和一次膨脹,所以我們的卷積核的尺寸需要設的大一點,如果只是3*3的是無法達到效果消除噪點的。當然如果我們單獨使用腐蝕和膨脹的話,可以進行多次卷積來消除噪點,這樣就可以使用3*3的卷積核。
閉運算
閉運算 = 膨脹 + 腐蝕
閉運算可以消除上圖中白色區域的黑色噪點。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1414.jpg") # 13*13的正方形卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13)) # 閉運算,以cv2.MORPH_CLOSE標識 dst = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
形態學梯度
梯度 = 原圖 - 腐蝕
它主要是用來計算白色區域的邊緣的。以此來獲取圖像邊緣。
我們還是以這張圖爲例
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1212.jpg") # 13*13的正方形卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13)) # 梯度,以cv2.MORPH_GRADIENT標識 dst = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
頂帽運算
頂帽 = 原圖 - 開運算
頂帽可以去掉大的白色區域,留下小的白色區域
我們以這張圖爲例
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1515.jpg") # 33*33的正方形卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (33, 33)) # 頂帽,以cv2.MORPH_TOPHAT標識 dst = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們可以看到,爲了在腐蝕過程中徹底消除小的白色塊,我們需要把卷積核設的大一點,否則頂帽後就是全部都是黑色的。
黑帽運算
黑帽 = 原圖 - 閉運算
黑帽可以取出圖中的黑色噪點,我們還是以這張圖爲例
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1414.jpg") # 13*13的正方形卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13)) # 黑帽,以cv2.MORPH_BLACKHAT標識 dst = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
目標識別
圖像輪廓
圖像輪廓指的是具有相同顏色或強度的連續點的曲線。
這也是需要進行二值化的原因,因爲對於一個灰色圖像來說,可能一個圖像輪廓的邊緣它們像素點的灰度值是不一致的,所以爲了更好辨認,需要它們具有相同的強度。
在上面這張圖中,我們就識別出了三個輪廓,它們都具有相同的顏色或強度的連續點。
- 圖像輪廓的作用
- 可以用於圖形分析,提取ROI
- 物體的識別和檢測
- 注意點
- 爲了檢測的準確性,需要先對圖像進行二值化或Canny操作。檢測輪廓是目的,二值化或Canny操作是技術手段。
- 畫輪廓時會修改輸入的圖像。如果不想改變原始圖像,需要先進行深拷貝。一般我們在進行二值化的時候需要把圖像變成黑底白線,這樣更加有利於輪廓的查找。
我們來看一下如何查找這個圖形的輪廓,這裏需要注意的是,該圖並不是完全的黑白二色圖,而是具有一定灰度的。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/2121.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_EXTERNAL查找最外圍的輪廓 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 繪製輪廓,-1表示繪製所有輪廓 cv2.drawContours(img, contours, -1, (0, 0, 255), 5) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們看到,它就把圖像外圍的輪廓給查找出來了。由於輪廓查找函數的參數比較多,我們來一個個看
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/2121.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_TREE查找所有的輪廓,並按照樹形結構存儲 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 繪製輪廓,-1表示繪製所有輪廓 cv2.drawContours(img, contours, -1, (0, 0, 255), 5) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們可以看到它查找到了3個輪廓,並且都給繪製了出來。
除了上面這兩種查找輪廓的類型還有
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
這裏cv2.RETR_LIST表示檢測的輪廓不建立等級關係,它們都是平級的。
contours, hierarchy = cv2.findContours(binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
這裏的cv2.RETR_CCOMP表示檢測的輪廓建立層級關係,但每層最多兩級。
這些類型的不同主要體現在返回值contours, hierarchy,它們有不同的數據結構,我們來看一下這些返回數據
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) print(contours) print(hierarchy)
運行結果
[array([[[ 0, 0]],
[[ 0, 787]],
[[697, 787]],
[[697, 0]]], dtype=int32)]
[[[-1 -1 -1 -1]]]
這說明返回的是四個繪製節點的座標值,並且沒有層級。
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) print(contours) print(hierarchy)
運行結果
[array([[[ 0, 0]],
[[ 0, 787]],
[[697, 787]],
[[697, 0]]], dtype=int32), array([[[ 72, 235]],
[[ 73, 234]],
[[624, 234]],
[[626, 236]],
[[626, 726]],
[[624, 728]],
[[622, 728]],
[[621, 727]],
[[620, 728]],
[[619, 728]],
[[618, 727]],
[[ 78, 727]],
[[ 77, 728]],
[[ 76, 727]],
[[ 72, 727]],
[[ 71, 726]],
[[ 71, 583]],
[[ 72, 582]],
[[ 72, 580]],
[[ 71, 579]],
[[ 71, 560]],
[[ 72, 559]],
[[ 72, 556]],
[[ 71, 555]],
[[ 71, 488]],
[[ 72, 487]],
[[ 71, 486]],
[[ 71, 481]],
[[ 72, 480]],
[[ 71, 479]],
[[ 71, 251]],
[[ 72, 250]],
[[ 72, 248]],
[[ 71, 247]],
[[ 72, 246]]], dtype=int32), array([[[ 72, 61]],
[[ 73, 60]],
[[108, 60]],
[[109, 61]],
[[110, 61]],
[[111, 60]],
[[112, 61]],
[[583, 61]],
[[584, 60]],
[[585, 61]],
[[586, 61]],
[[587, 60]],
[[588, 60]],
[[589, 61]],
[[590, 60]],
[[621, 60]],
[[623, 62]],
[[623, 69]],
[[622, 70]],
[[ 73, 70]],
[[ 71, 68]],
[[ 72, 67]],
[[ 71, 66]],
[[ 72, 65]],
[[ 72, 64]],
[[ 71, 63]],
[[ 72, 62]]], dtype=int32)]
[[[-1 -1 1 -1]
[ 2 -1 -1 0]
[-1 1 -1 0]]]
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) print(contours) print(hierarchy)
運行結果
[array([[[ 72, 235]],
[[ 73, 234]],
[[624, 234]],
[[626, 236]],
[[626, 726]],
[[624, 728]],
[[622, 728]],
[[621, 727]],
[[620, 728]],
[[619, 728]],
[[618, 727]],
[[ 78, 727]],
[[ 77, 728]],
[[ 76, 727]],
[[ 72, 727]],
[[ 71, 726]],
[[ 71, 583]],
[[ 72, 582]],
[[ 72, 580]],
[[ 71, 579]],
[[ 71, 560]],
[[ 72, 559]],
[[ 72, 556]],
[[ 71, 555]],
[[ 71, 488]],
[[ 72, 487]],
[[ 71, 486]],
[[ 71, 481]],
[[ 72, 480]],
[[ 71, 479]],
[[ 71, 251]],
[[ 72, 250]],
[[ 72, 248]],
[[ 71, 247]],
[[ 72, 246]]], dtype=int32), array([[[ 72, 61]],
[[ 73, 60]],
[[108, 60]],
[[109, 61]],
[[110, 61]],
[[111, 60]],
[[112, 61]],
[[583, 61]],
[[584, 60]],
[[585, 61]],
[[586, 61]],
[[587, 60]],
[[588, 60]],
[[589, 61]],
[[590, 60]],
[[621, 60]],
[[623, 62]],
[[623, 69]],
[[622, 70]],
[[ 73, 70]],
[[ 71, 68]],
[[ 72, 67]],
[[ 71, 66]],
[[ 72, 65]],
[[ 72, 64]],
[[ 71, 63]],
[[ 72, 62]]], dtype=int32), array([[[ 0, 0]],
[[ 0, 787]],
[[697, 787]],
[[697, 0]]], dtype=int32)]
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[-1 1 -1 -1]]]
contours, hierarchy = cv2.findContours(binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) print(contours) print(hierarchy)
運行結果
[array([[[ 0, 0]],
[[ 0, 787]],
[[697, 787]],
[[697, 0]]], dtype=int32), array([[[ 72, 235]],
[[ 73, 234]],
[[624, 234]],
[[626, 236]],
[[626, 726]],
[[624, 728]],
[[622, 728]],
[[621, 727]],
[[620, 728]],
[[619, 728]],
[[618, 727]],
[[ 78, 727]],
[[ 77, 728]],
[[ 76, 727]],
[[ 72, 727]],
[[ 71, 726]],
[[ 71, 583]],
[[ 72, 582]],
[[ 72, 580]],
[[ 71, 579]],
[[ 71, 560]],
[[ 72, 559]],
[[ 72, 556]],
[[ 71, 555]],
[[ 71, 488]],
[[ 72, 487]],
[[ 71, 486]],
[[ 71, 481]],
[[ 72, 480]],
[[ 71, 479]],
[[ 71, 251]],
[[ 72, 250]],
[[ 72, 248]],
[[ 71, 247]],
[[ 72, 246]]], dtype=int32), array([[[ 72, 61]],
[[ 73, 60]],
[[108, 60]],
[[109, 61]],
[[110, 61]],
[[111, 60]],
[[112, 61]],
[[583, 61]],
[[584, 60]],
[[585, 61]],
[[586, 61]],
[[587, 60]],
[[588, 60]],
[[589, 61]],
[[590, 60]],
[[621, 60]],
[[623, 62]],
[[623, 69]],
[[622, 70]],
[[ 73, 70]],
[[ 71, 68]],
[[ 72, 67]],
[[ 71, 66]],
[[ 72, 65]],
[[ 72, 64]],
[[ 71, 63]],
[[ 72, 62]]], dtype=int32)]
[[[-1 -1 1 -1]
[ 2 -1 -1 0]
[-1 1 -1 0]]]
然後我們來看一下對美女圖片進行輪廓查找的樣子
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_TREE查找所有的輪廓,並按照樹形結構存儲 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 繪製輪廓,-1表示繪製所有輪廓 cv2.drawContours(img, contours, -1, (0, 0, 255), 5) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
輪廓的面積與周長
在我們查找到輪廓之後有很多碎小的輪廓,這些輪廓可能不是我們的計算範圍之內,我們需要過濾掉它們。這個時候就可以通過面積的大小,來過濾獲取我們真正需要的那個輪廓。又比如我們在一張圖片中,我們知道一個物體的實際面積是多大,那麼計算這個面積就可以通過實際面積計算出一個比例值來,通過這個比例值我們就可以知道這個圖片中其他物體的大小。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/2121.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_TREE查找所有的輪廓,並按照樹形結構存儲 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 計算面積 area = cv2.contourArea(contours[0]) print(area) # 計算周長,True表示輪廓爲閉合的,False表示輪廓是打開的 len = cv2.arcLength(contours[0], True) print(len) # 繪製輪廓,-1表示繪製所有輪廓 cv2.drawContours(img, contours, -1, (0, 0, 255), 5) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
548539.0
2968.0
這樣我們就得到了最外層的輪廓的面積和周長。
多邊形逼近與凸包
在上圖中有兩個手型的圖案,左邊的輪廓就叫做多邊形逼近,而右邊的輪廓就叫做凸包。對於多邊形逼近來說,它的手型是可以調整的。正常來說,當我們在查找輪廓的時候,對於這個手型輪廓是嚴格按照像素邊緣點來進行繪製,但是這樣會帶來一個問題,當圖形比較複雜的時候,它的輪廓邊緣線會非常多,數據量會非常大。對於有些應用其實沒必要存這麼多數據,只需要存一些特徵點,將手型會描述出來就可以了。比如說左邊的圖形中,我們只需要存一些關鍵點就可以將這個手型給描述出來。這就是多邊形逼近的作用,減少了存儲的數據量,同時通過特徵點把這個手型給描述出來。這個特徵點的選取是可以調整的,比如在小拇指到左下端中可以增加特徵點來描繪左邊手型的形狀。但如果我們不想要這麼嚴格,就可以把精度調低一些,這樣存儲的數據就會減少。
而凸包就是描述一個輪廓,而且是凸出的,不會往下凹。我們就用這個手型爲例來看看這兩個操作
我們來看一下,當我們不使用多邊形逼近和凸包時的效果
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/hand.png") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_TREE查找所有的輪廓,並按照樹形結構存儲 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 計算面積 # area = cv2.contourArea(contours[0]) # print(area) # 計算周長,True表示輪廓爲閉合的,False表示輪廓是打開的 # len = cv2.arcLength(contours[0], True) # print(len) # 繪製輪廓,-1表示繪製所有輪廓,3表示繪製框的粗細 cv2.drawContours(img, contours, -1, (0, 255, 0), 3) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們來增加多邊形逼近
import cv2 if __name__ == "__main__": def drawShape(src, points): i = 0 while i < len(points): if i == len(points) - 1: x, y = points[i][0] x1, y1 = points[0][0] cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3) else: x, y = points[i][0] x1, y1 = points[i + 1][0] cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3) i = i + 1 cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/hand.png") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_TREE查找所有的輪廓,並按照樹形結構存儲 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 計算面積 # area = cv2.contourArea(contours[0]) # print(area) # 計算周長,True表示輪廓爲閉合的,False表示輪廓是打開的 # len = cv2.arcLength(contours[0], True) # print(len) # 繪製輪廓,-1表示繪製所有輪廓,3表示繪製框的粗細 cv2.drawContours(img, contours, -1, (0, 255, 0), 3) e = 20 # 獲取多邊形逼近的特徵點,e爲精度,True表示雖否閉合 approx = cv2.approxPolyDP(contours[0], e, True) # 將多邊形逼近的特徵點給連接起來 drawShape(img, approx) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在來增加凸包
import cv2 if __name__ == "__main__": def drawShape(src, points): i = 0 while i < len(points): if i == len(points) - 1: x, y = points[i][0] x1, y1 = points[0][0] cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3) else: x, y = points[i][0] x1, y1 = points[i + 1][0] cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3) i = i + 1 cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/hand.png") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_TREE查找所有的輪廓,並按照樹形結構存儲 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 計算面積 # area = cv2.contourArea(contours[0]) # print(area) # 計算周長,True表示輪廓爲閉合的,False表示輪廓是打開的 # len = cv2.arcLength(contours[0], True) # print(len) # 繪製輪廓,-1表示繪製所有輪廓,3表示繪製框的粗細 cv2.drawContours(img, contours, -1, (0, 255, 0), 3) e = 20 # 獲取多邊形逼近的特徵點,e爲精度,True表示雖否閉合 # approx = cv2.approxPolyDP(contours[0], e, True) # 將多邊形逼近的特徵點給連接起來 # drawShape(img, approx) # 獲取凸包的特徵點 hull = cv2.convexHull(contours[0]) # 將凸包的特徵點給連接起來 drawShape(img, hull) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
我們也可以查找美女圖片的最大面積的輪廓,來進行多邊形逼近
import cv2 if __name__ == "__main__": def drawShape(src, points): i = 0 while i < len(points): if i == len(points) - 1: x, y = points[i][0] x1, y1 = points[0][0] cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3) else: x, y = points[i][0] x1, y1 = points[i + 1][0] cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3) i = i + 1 cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 # cv2.RETR_TREE查找所有的輪廓,並按照樹形結構存儲 # cv2.CHAIN_APPROX_SIMPLE輪廓記錄方式,這裏是壓縮記錄 # contours, hierarchy是兩個返回值,contours查到的所有的輪廓列表 # hierarchy表示輪廓的層級 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 計算面積 area = 0 find = 0 for i in range(len(contours)): areatemp = cv2.contourArea(contours[i]) if areatemp > area: area = areatemp find = i # print(area) # print(find) # area = cv2.contourArea(contours[0]) # print(area) # 計算周長,True表示輪廓爲閉合的,False表示輪廓是打開的 # len = cv2.arcLength(contours[0], True) # print(len) # 繪製輪廓,-1表示繪製所有輪廓,3表示繪製框的粗細 cv2.drawContours(img, contours, -1, (0, 255, 0), 3) e = 30 # 獲取多邊形逼近的特徵點,e爲精度,True表示雖否閉合 approx = cv2.approxPolyDP(contours[find], e, True) # 將多邊形逼近的特徵點給連接起來 drawShape(img, approx) # 獲取凸包的特徵點 # hull = cv2.convexHull(contours[0]) # 將凸包的特徵點給連接起來 # drawShape(img, hull) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
外接矩形
這個功能是輪廓中一個更爲重要的功能。它包括兩種類型
- 最小外接矩形
- 最大外接矩形
在上圖中,對於這個白色的閃電來說,紅色框是最小外接矩形,綠色框是最大外接矩形。
我們以這張圖爲例,來看一下最小外接矩形和最大外接矩形
我們也一樣,先來看一下該圖像的輪廓
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1010.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(img, contours, -1, (0, 255, 0), 3) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們來看一下中間這個框的最小外接矩形
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1010.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 獲取中間Hello框的最小外接矩形 r = cv2.minAreaRect(contours[1]) # 獲取矩形四個頂點(浮點型),並轉化爲整形 box = np.int0(cv2.boxPoints(r)) # 繪製輪廓 cv2.drawContours(img, [box], 0, (0, 0, 255), 5) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們來對中間這個框繪製最大外接矩形
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/1010.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY) # 輪廓查找 contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 獲取中間Hello框的最小外接矩形 r = cv2.minAreaRect(contours[1]) # 獲取矩形四個頂點(浮點型),並轉化爲整形 box = np.int0(cv2.boxPoints(r)) # 繪製最小外接矩形 cv2.drawContours(img, [box], 0, (0, 0, 255), 5) # 獲取中間Hello框的最大外接矩形 x, y, w, h = cv2.boundingRect(contours[1]) # 繪製最大外接矩形 cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 5) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
車輛統計
這裏我們需要對前面的內容進行一個全面的整理,原視頻如下
我們來統計來往的車輛總數
import cv2 if __name__ == "__main__": # 最小寬高 min_w = 90 min_h = 90 # 畫線高度 line_high = 550 # 偏移量 off_set = 7 def center(x, y, w, h): # 計算車輛中心點 x1 = int(w / 2) y1 = int(h / 2) cx = x + x1 cy = y + y1 return cx, cy # 創建窗口 cv2.namedWindow('video', cv2.WINDOW_NORMAL) # 獲取視頻文件 cap = cv2.VideoCapture("/Users/admin/Documents/video.mp4") # 去背景 bgsubmog = cv2.createBackgroundSubtractorMOG2() # 腐蝕卷積核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) # 膨脹卷積核 kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 通過的車輛數 car_num = 0 cars = [] while True: # 從文件讀視頻楨 ret, frame = cap.read() if ret: # 將每一幀圖像轉成灰度圖像 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 高斯濾波器去除高斯噪音 gaussian = cv2.GaussianBlur(gray, (3, 3), sigmaX=25, sigmaY=25) # 去背景應用 mask = bgsubmog.apply(gaussian) # 腐蝕去除高斯濾波器剩下的噪點 erode = cv2.erode(mask, kernel) # 進行三次膨脹抵消腐蝕縮小的圖形 dilate = cv2.dilate(erode, kernel1, iterations=3) # 進行兩次閉運算,消除車輛內部的噪點 close = cv2.morphologyEx(dilate, cv2.MORPH_CLOSE, kernel) close = cv2.morphologyEx(close, cv2.MORPH_CLOSE, kernel) # 尋找每一幀的所有輪廓 contours, hierarchy = cv2.findContours(close, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 在圖像底部畫一條紅線 cv2.line(frame, (0, line_high), (1280, line_high), (0, 0, 255), 5) # 遍歷所有的輪廓 for i, c in enumerate(contours): # 獲取該輪廓的最大外接矩形 x, y, w, h = cv2.boundingRect(c) # 如果該外接矩形的寬和高小於最小寬高,視爲噪音,過濾掉 noValid = (w < min_w) and (h < min_h) if noValid: continue # 畫出目標輪廓的最大外接矩形 cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 5) # 獲取該外接矩形的中心點,並放入列表中 cpoint = center(x, y, w, h) cars.append(cpoint) for x, y in cars: if (y > line_high - off_set) and (y < line_high + off_set): # 如果該矩形中心點在紅線偏移量範圍內,車輛記數+1 # 並在列表中移除該中心點 car_num += 1 cars.remove((x, y)) # 在圖像上打印車輛統計數量 cv2.putText(frame, "pass cars count:" + str(car_num), (500, 60), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 8) # 將視頻幀在窗口中顯示 cv2.imshow('video', frame) # 此處不能設爲1,否則會過快,可以設的比播放視頻每秒幀數長一點 key = cv2.waitKey(20) if key & 0xFF == ord('q'): break # 釋放資源 cap.release() cv2.destroyAllWindows()
運行結果
特徵點檢測與匹配
特徵檢測的基本概念
- OpenCV特徵的場景
- 圖像搜索,如以圖搜圖。如果我們要在互聯網上搜索一張圖片,如果以全像素來搜索的話,那麼這個計算量是難以估算的。實際上我們會把圖的一些特徵點給提取出來,可能就是很小的字節。這麼少的數據再去進行搜索的時候就非常方便。比如對於Google來說,他每天從全世界獲取到大量的圖片,會搜索提取圖像的特徵點,把這些圖片的主要的特徵點給提取出來之後存儲到數據庫中,當用戶進行搜索的時候,也會對用戶提交的圖像進行特徵檢測。檢測出特徵點後再去特徵庫中進行搜索。這時候就能找到多匹配的圖片了。
- 拼圖遊戲,我們將人物照切成很多小的方塊,在每個方塊裏都有一些特徵值。只要我們看到了這些特徵值就可以把它們拼接到一起。比方說一個頭部的圖像分成了四塊,當我們找到了其中的一塊的時候,另一塊其實非常容易找到。比如說它是以鼻子分界的,左半邊鼻子和右半邊鼻子看到以後就可以把它們拼接到一起。這個就是通過特徵查找。當我們組裝頭部圖片的時候就會去找頭部信息就不會去找手部信息,當我們組裝手部的時候就會去找手部信息。對於我們人類來說也是通過特徵來進行拼圖的。
- 圖像拼接,將兩張有關聯的圖拼接到一起。當我們去看一番景色的時候,人眼看到的範圍要比鏡頭看到的景色要廣闊。那我們如何能讓拍到的景象與人眼看到的景象一致呢?其實就用到了全景圖像,就是用相機連續的拍照,在不同的角度上進行拍照,比如180度,360度。拍完了之後將這些圖像拼接到一起,這樣就形成了一個全景的圖像。它與我們人眼看到的景色是一致的。有的全景圖像甚至超過了人眼,它可以360度。它其實也是通過OpenCV的特徵點檢測。當相機拍照的每一幅圖像,它們中間都是有一定重合度的,那麼在重合的這一塊是有特徵值的,如果將兩張圖片的特徵值都找到之後,將它們重合點拼接到一起,這樣就形成了一個全景圖像。
- 尋找特徵
在上面的這張圖中取出了6塊,這6塊可以分成3組,A、B是一組,C、D是一組,E、F是一組。我們通過第一組特徵,是否能知道它在哪呢?從目前來看,我們很難區分它在哪,因爲很多地方是有這個信息的。對於這種平坦的圖像不是唯一的,我們很難進行識別。對於第二組來說是邊緣,雖然我們大概能知道它在哪裏,但是我們依然無法確切知道它的具體位置。第三組是兩個角,這時候我們就很容易判別出來。通過這張圖,我們就知道,對於角來說它的特徵足夠明顯,邊次之,平坦就無法區分了。
- 什麼是特徵
圖像特徵就是指有意義的圖像區域,具有獨特性、易於識別性,比如角點、斑點以及高密度區。
在特徵中最重要的是角點,角點就是
- 灰度梯度的最大值對應的像素。
- 兩條線的交點
- 極值點(一階導數最大值,但二階導數爲0)
這些條件對於計算機來說是可識別的,但對於人來說是不會去這麼計算的。
哈里斯(Harris)角點檢測
在上圖中,粉紅色的小塊是一個卷積核。這三張圖分別代表平坦、邊緣和角點的區域檢測。在第一張圖中,如果在卷積核的移動範圍內(上下左右各個方向移動),卷積核提取的像素沒有任何的變化,說明這是一個平坦的圖片。在第二張圖中,如果卷積核沿着邊緣平行移動的時候,提取的像素是沒有變化的;如果卷積核沿着邊緣垂直移動的時候,那麼提取的像素就會有劇烈的變化,這個時候harris會認爲這是一條邊緣。在第三張圖中,無論卷積核朝任何一個方向移動的時候,提取的像素都會產生變化,這個時候就檢測到了一個角點。總結如下
- 光滑地區,無論卷積核向哪裏移動,衡量係數不變。
- 邊緣地區,卷積核垂直邊緣移動時,衡量係數變化劇烈。
- 在交點處,無論卷積核朝哪個方向移動,衡量係數都變化劇烈。
我們以這張圖爲例來進行角點檢測說明
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/888.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Harris角點檢測,2爲檢測窗口大小,3爲索貝爾卷積核大小, # 0.04爲權重係數,經驗值,一般取0.02~0.04之間 dst = cv2.cornerHarris(gray, 2, 3, 0.04) # Harris角點的展示 img[dst > 0.01 * dst.max()] = [0, 0, 255] while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們可以看到在每個交叉的地方都有紅色的小點,一些數字拐角的地方也有這些紅色的小點。
Shi-Tomasi角點檢測
Shi-Tomasi是Harris角點檢測的改進,前面我們看到Harris角點檢測有一個權重係數經驗值,這個值對不同的圖片的角點檢測可能會不同,在0.02~0.04之間。而Tomasi就不用設置這個權重係數了。
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/888.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Harris角點檢測,2爲檢測窗口大小,3爲索貝爾卷積核大小, # 0.04爲權重係數,經驗值,一般取0.02~0.04之間 # dst = cv2.cornerHarris(gray, 2, 3, 0.04) # Harris角點的展示 # img[dst > 0.01 * dst.max()] = [0, 255, 0] # Tomasi角點檢測,1000爲角點的最大數,值爲0表示無限制 # 0.01這個參數一般取0.01~1之間的數 # 10表示角之間的最小歐式距離,忽略小於此距離的點 corners = cv2.goodFeaturesToTrack(gray, 1000, 0.01, 10) corners = np.int0(corners) # Tomasi繪製角點 for i in corners: x, y = i.ravel() cv2.circle(img, (x, y), 3, (255, 0, 0), -1) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
SIFT關鍵點檢測
- SIFT出現的原因
- Harris角點具有旋轉不變的特性,無論圖像如何旋轉,角點是不會發生變化的。
- 縮放後,原來的角點有可能就不是角點了。比如說圖放大後
最左邊的區域在我們用卷積核檢測時發現這裏是一個角,但當該圖放大後,我們再使用卷積核去檢測的時候,會發現它變成了一個邊緣。這就是Harris角點檢測時存在的一個巨大的問題。
- 關鍵點與描述子
- 關鍵點:位置、大小和方向。如果有的關鍵點特別小,實際我們是需要給它過濾掉,減少計算量。因爲這些小的關鍵點對於特徵的查找和匹配意義不大。
- 關鍵點描述子:記錄了關鍵點周圍對其有貢獻的像素點的一組向量,其不受仿射變換、光照變換等影響。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/888.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 實例化SIFT對象 sift = cv2.SIFT_create() # 獲取灰度圖像中的關鍵點,返回值kp爲關鍵點 # 關鍵點包含角點,一階導數極值點 kp = sift.detect(gray, None) # 計算描述子des kp, des = sift.compute(img, kp) # 上面兩步可以合併成一步 # kp, des = sift.detectAndCompute(gray, None) # 打印第0個描述子 print(des[0]) # 繪出關鍵點 cv2.drawKeypoints(gray, kp, img) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
[ 0. 12. 156. 2. 0. 0. 0. 0. 10. 87. 87. 0. 0. 0.
0. 0. 7. 18. 2. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 18. 42. 156. 13. 13. 17. 5. 1. 156. 156.
79. 1. 1. 0. 0. 5. 97. 26. 0. 0. 0. 0. 0. 1.
2. 0. 0. 0. 0. 0. 0. 0. 38. 5. 5. 35. 88. 35.
9. 78. 156. 14. 1. 0. 2. 1. 1. 156. 105. 1. 0. 0.
0. 0. 0. 24. 1. 0. 0. 0. 0. 0. 0. 1. 9. 0.
0. 30. 13. 0. 3. 156. 45. 0. 0. 0. 0. 0. 0. 156.
14. 0. 0. 0. 0. 0. 0. 20. 1. 0. 0. 0. 0. 0.
0. 0.]
通過打印出來的描述子我們可以看到,它是某個關鍵點周圍提供貢獻的像素值,在進行匹配的時候才能根據這些描述子來進行匹配。
SURF(Speeded-Up Robust Features)特徵檢測
SIFT進行特徵檢測的時候非常準確,描述子也描述的非常詳細。但是SIFT最大的問題是速度慢,因此纔有SURF。如果我們想對一系列的圖片快速的進行特徵檢測獲取描述子的話,使用SIFT會非常的慢。SURF保留了SIFT的優點,而且檢測速度也比較快。但是由於專利原因,暫不可用。
ORB特徵檢測
ORB可以做到實時監測,它對SIFT和SURF做了兩點改進,一塊是對特徵點檢測做了改進,另一塊是對描述子的計算做了改進。
ORB = Oriented FAST + Rotated BRIEF
ORB有優點,也有缺點。要做到實時檢測,實際上就是對數據量的縮減,在區域劃分的時候,就要拋棄一些沒有必要的點。對描述子的計算也是拋棄了大量的數據。雖然ORB快了,但是它的準確率就不如SIFT和SURF。所以對檢測數量不多的圖片,且對準確率要求較高時,我們應該使用SIFT;而要檢測的圖片的數量非常龐大的時候,就應該要使用ORB了。
- FAST:可以做到特徵點的實時檢測。它進行檢測的時候,檢測出的特徵點是不帶方向的。所以又給它增加了方向的屬性,所以成了Oriented FAST
- BRIEF:是對已經檢測到的特徵點進行描述。它加快了特徵描述符建立的速度,同時也極大的降低了特徵匹配的時間。實際是對特徵點描述子的計算。對於BRIEF來說對圖像的旋轉處理的不太好,所以增加了Rotated BRIEF,對它進行了改進,對旋轉具有魯棒性。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/888.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 實例化ORB對象 orb = cv2.ORB_create() # 獲取灰度圖像中的關鍵點和描述子,返回值kp爲關鍵點 # 關鍵點包含角點,一階導數極值點,des爲描述子 kp, des = orb.detectAndCompute(gray, None) # 打印第0個描述子 print(des[0]) # 繪出關鍵點 cv2.drawKeypoints(gray, kp, img) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
[130 205 38 216 21 133 172 103 148 18 80 18 168 8 52 65 77 35
19 129 242 172 22 255 33 26 222 192 21 12 241 136]
對比ORB和SIFT的結果,我們會發現無論是描述子還是畫出的關鍵點,ORB的數量都相對較少。
暴力特徵匹配
- 特徵匹配方法
- BF(Brute-Force),暴力特徵匹配方法
- FLANN,最快近鄰區特徵匹配方法
- 暴力特徵匹配原理
它使用第一組中的每個特徵的描述子,與第二組中的所有特徵描述子進行匹配,計算它們之間的差距(相似度),然後將最接近一個匹配返回。
這裏我們要進行匹配的這樣兩幅圖
以及
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/opencv_search.png") img2 = cv2.imread("/Users/admin/Documents/opencv_orig.png") g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR) g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR) sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(g1, None) kp2, des2 = sift.detectAndCompute(g2, None) # 創建匹配器,cv2.NORM_L1爲匹配類型,對描述子取絕對值進行加法運算,還有 # cv2.NORM_L2對描述子取平方和開方,這兩種都是爲SIFT和SURF提供的類型 # 還有cv2.NORM_HAMMING判斷二進制位在第幾位開始不同,如果相同的越多,則速度越高 # 和cv2.NORM_HAMMING2,是爲ORB提供的類型 # 還有一個參數爲crossCheck,默認False,表示是否進行交叉匹配,即兩幅圖互相查找特徵 bf = cv2.BFMatcher(cv2.NORM_L1) # 進行特徵匹配 match = bf.match(des1, des2) # 繪製匹配點 img3 = cv2.drawMatches(img1, kp1, img2, kp2, match, None) while True: cv2.imshow('img', img3) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這裏我們可以看到,它會把具有相似的特徵點通過直線給串聯起來直觀的展示出來。但是也會有匹配有誤的地方。
FLANN特徵匹配
相比暴力匹配,FLANN在進行批量特徵匹配時,速度更快。但由於FLANN使用的是鄰近近似值,所以精度較差。如果我們要進行圖像的精度匹配,我們應該使用暴力特徵匹配。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/opencv_search.png") img2 = cv2.imread("/Users/admin/Documents/opencv_orig.png") g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR) g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR) sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(g1, None) kp2, des2 = sift.detectAndCompute(g2, None) index_params = dict(algorithm=1, trees=5) search_params = dict(checks=50) # 創建匹配器,index_params爲一個字典,它有兩種算法KDTREE和LSH # KDTREE是給SIFT和SURF使用的,LSH是給ORB使用的,如果調換會報錯 # search_params爲一個字典,指定KDTREE算法中遍歷樹的次數 flann = cv2.FlannBasedMatcher(index_params, search_params) # 對描述子進行特徵匹配,k表示歐式距離最近的前k個關鍵點 # 返回的是DMatch對象,它包含了distance,描述子之間的距離(近似度),值越低越好 # 以及queryIdx,第一個圖像的描述子索引值,trainIdx,第二個圖像的描述子索引 # imgIdx,第二個圖的索引值 matchs = flann.knnMatch(des1, des2, k=2) # 創建一個最好距離的列表 good = [] # 遍歷返回結果的所有距離 for i, (m, n) in enumerate(matchs): if m.distance < 0.7 * n.distance: good.append(m) # 繪製匹配點 img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, [good], None) while True: cv2.imshow('img', img3) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
相比於暴力匹配,我們會發現,它的匹配結果比較少。也就是說它的匹配效果不如暴力匹配。
圖像查找
對於批量圖像查找來說,使用FLANN更好一些,當我們獲取特徵匹配之後,把它們輸入單應性矩陣,拿到單應性矩陣再經過透視變換就可以獲取到最終的圖像了。即特徵匹配+單應性矩陣+透視變換。
- 單應性矩陣
在上圖所示,有兩個相機對底層的相同物品進行拍照,但是這兩個相機的角度不同,這樣第一個相機拍到的照片就是image1,第二個相機拍到的照片就是image2。對於物品的同一個點X,image1上對應的是x,image2上對應的是x'。單應性矩陣與image1進行運算,此時的x點就可以得到image2中x'的位置。同樣道理,單應性矩陣與image2進行運算可以得到原始物體的X的位置;單應性矩陣與image1進行運算也可以得到原始物體的X的位置。它其實是一種線性變換,有關線性變換的內容可以參考線性代數整理(二)
- 單應性的應用
比方說,我們從一個角度拍攝的銀行卡就可以通過單應性矩陣給轉成一張正面照。
這裏左邊的是一張原始圖,它其中有一副巨大的廣告牌,此時我們可以使用單應性矩陣將該廣告牌給替換成右圖中我們自己的廣告牌,而無需去手動摳圖,修改。
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/666.jpg") img2 = cv2.imread("/Users/admin/Documents/8888.jpg") g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR) g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR) sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(g1, None) kp2, des2 = sift.detectAndCompute(g2, None) # 創建匹配器,cv2.NORM_L1爲匹配類型,對描述子取絕對值進行加法運算,還有 # cv2.NORM_L2對描述子取平方和開方,這兩種都是爲SIFT和SURF提供的類型 # 還有cv2.NORM_HAMMING判斷二進制位在第幾位開始不同,如果相同的越多,則速度越高 # 和cv2.NORM_HAMMING2,是爲ORB提供的類型 # 還有一個參數爲crossCheck,默認False,表示是否進行交叉匹配,即兩幅圖互相查找特徵 bf = cv2.BFMatcher(cv2.NORM_L2) # 進行特徵匹配 matchs = bf.knnMatch(des1, des2, k=2) # 創建一個最好距離的列表 good = [] # 遍歷返回結果的所有距離 for i, (m, n) in enumerate(matchs): if m.distance < 0.7 * n.distance: good.append(m) if len(good) >= 4: src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) # 獲取單應性矩陣H,src_pts爲原圖的關鍵點座標,dst_pts爲查找圖的關鍵點座標 # cv2.RANSAC表示對錯誤的匹配點做一個過濾,具體含義爲隨機抽樣抑制算法 # 5.0是一個閾值,是一個經驗值,在1~10之間 # 單應性矩陣即爲查找圖的區域座標 H, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) # 獲取原圖的高和寬 h, w = img1.shape[:2] # 獲取原圖區域 pts = np.float32([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2) # 進行透視變換,得到變換後的圖像範圍 dst = np.int32(cv2.perspectiveTransform(pts, H)) # 在查找圖中畫出查找範圍的矩形 cv2.polylines(img2, [dst], True, (0, 0, 255), 5) else: print("好的數據點太少,必須不少於4") exit() # 繪製匹配點 img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, matchs, None) while True: cv2.imshow('img', img3) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
圖像拼接
這裏我們會對前面的內容做一個整理,我們要拼接的兩張圖片如下
以及
這裏我們可以看到,這兩張圖像是具有非常多的相同的特徵點的。我們先將這兩張圖原始拼接在一起。
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/map1.png") img2 = cv2.imread("/Users/admin/Documents/map2.png") img1 = cv2.resize(img1, (640, 480)) img2 = cv2.resize(img2, (640, 480)) img3 = np.hstack((img1, img2)) while True: cv2.imshow('img', img3) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在來根據尋找到的兩張圖匹配的特徵點完成圖像的拼接
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/map1.png") img2 = cv2.imread("/Users/admin/Documents/map2.png") img1 = cv2.resize(img1, (640, 480)) img2 = cv2.resize(img2, (640, 480)) img3 = np.hstack((img1, img2)) def get_homo(img1, img2): ''' 獲取單應性矩陣 :param img1: :param img2: :return: ''' # 獲取特徵轉換對象 sift = cv2.SIFT_create() g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR) g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR) # 獲取關鍵點和描述子 kp1, des1 = sift.detectAndCompute(g1, None) kp2, des2 = sift.detectAndCompute(g2, None) # 創建暴力匹配器 bf = cv2.BFMatcher(cv2.NORM_L2) # 進行特徵匹配 matchs = bf.knnMatch(des1, des2, k=2) # 設置特徵比例 verify_ratio = 0.8 # 有效特徵點 verify_matches = [] # 過濾無效匹配點,獲取有效匹配點 for m1, m2 in matchs: if m1.distance < verify_ratio * m2.distance: verify_matches.append(m1) # 設置最小應匹配的個數 min_matches = 8 # 有效匹配點的數量必須達到最小值 if len(verify_matches) > min_matches: img1_pts = [] img2_pts = [] # 獲取圖1、圖2的匹配的特徵點座標 for m in verify_matches: img1_pts.append(kp1[m.queryIdx].pt) img2_pts.append(kp2[m.trainIdx].pt) img1_pts = np.float32(img1_pts).reshape(-1, 1, 2) img2_pts = np.float32(img2_pts).reshape(-1, 1, 2) # 根據兩幅圖匹配的特徵點座標獲取單應性矩陣 H, _ = cv2.findHomography(img1_pts, img2_pts, cv2.RANSAC, 5.0) return H else: print("好的匹配點太少,必須不少於8") exit() def stitch_image(img1, img2, H): ''' 拼接圖像 :param img1: :param img2: :param H: :return: ''' # 獲取兩張圖像的高和寬 h1, w1 = img1.shape[:2] h2, w2 = img2.shape[:2] # 獲取兩張圖片的四個頂點 img1_dims = np.float32([[0, 0], [0, h1], [w1, h1], [w1, 0]]).reshape(-1, 1, 2) img2_dims = np.float32([[0, 0], [0, h2], [w2, h2], [w2, 0]]).reshape(-1, 1, 2) # 獲取第一張圖的透視變換後的圖像範圍 img1_transform = cv2.perspectiveTransform(img1_dims, H) # 將第一張圖像變換後的圖像範圍與第二張圖像的範圍給拼接起來 result_dims = np.concatenate((img1_transform, img2_dims), axis=0) # 獲取聯合範圍的最小值座標和最大值座標 [x_min, y_min] = np.int32(result_dims.min(axis=0).ravel() - 0.5) [x_max, y_max] = np.int32(result_dims.max(axis=0).ravel() + 0.5) # 由於第一張圖透視變換後的圖像範圍超出邊界, # 則聯合範圍的最小值的x、y座標都是負數 # 現在將其轉成正數 transform_dist = [-x_min, -y_min] # 設置平移矩陣,可以將超出邊框的圖像移動到邊框內 transform_array = np.array([[1, 0, transform_dist[0]], [0, 1, transform_dist[1]], [0, 0, 1]]) # 對圖一進行透視變換,轉換矩陣爲平移矩陣與單應性矩陣的點積表示圖一不僅進行了單應性轉換還進行了平移 result_img = cv2.warpPerspective(img1, transform_array.dot(H), (x_max - x_min, y_max - y_min)) # 將圖二放入對接的區域中 result_img[transform_dist[1]:transform_dist[1] + h2, transform_dist[0]:transform_dist[0] + w2] = img2 return result_img H = get_homo(img1, img2) result_img = stitch_image(img1, img2, H) while True: cv2.imshow('img', result_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
現在我們來看一下兩幅圖的匹配的特徵點以及拼接以後的圖像查找的區域
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) cv2.namedWindow('img3', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/map1.png") img2 = cv2.imread("/Users/admin/Documents/map2.png") img1 = cv2.resize(img1, (640, 480)) img2 = cv2.resize(img2, (640, 480)) def get_homo(img1, img2): ''' 獲取單應性矩陣 :param img1: :param img2: :return: ''' # 獲取特徵轉換對象 sift = cv2.SIFT_create() g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR) g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR) # 獲取關鍵點和描述子 kp1, des1 = sift.detectAndCompute(g1, None) kp2, des2 = sift.detectAndCompute(g2, None) # 創建暴力匹配器 bf = cv2.BFMatcher(cv2.NORM_L2) # 進行特徵匹配 matchs = bf.knnMatch(des1, des2, k=2) # 設置特徵比例 verify_ratio = 0.4 # 有效特徵點 verify_matches = [] matchs_new = [] # 過濾無效匹配點,獲取有效匹配點 for i, (m1, m2) in enumerate(matchs): if m1.distance < verify_ratio * m2.distance: verify_matches.append(m1) matchs_new.append(matchs[i]) # 設置最小應匹配的個數 min_matches = 8 # 有效匹配點的數量必須達到最小值 if len(verify_matches) > min_matches: img1_pts = [] img2_pts = [] # 獲取圖1、圖2的匹配的特徵點座標 for m in verify_matches: img1_pts.append(kp1[m.queryIdx].pt) img2_pts.append(kp2[m.trainIdx].pt) img1_pts = np.float32(img1_pts).reshape(-1, 1, 2) img2_pts = np.float32(img2_pts).reshape(-1, 1, 2) # 根據兩幅圖匹配的特徵點座標獲取單應性矩陣 H, _ = cv2.findHomography(img1_pts, img2_pts, cv2.RANSAC, 5.0) H1, _ = cv2.findHomography(img2_pts, img1_pts, cv2.RANSAC, 5.0) img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, matchs_new, None) h1, w1 = img1.shape[:2] h2, w2 = img2.shape[:2] # 獲取原圖區域 pts1 = np.float32([[0, 0], [0, h1 - 1], [w1 - 1, h1 - 1], [w1 - 1, 0]]).reshape(-1, 1, 2) pts2 = np.float32([[0, 0], [0, h2 - 1], [w2 - 1, h2 - 1], [w2 - 1, 0]]).reshape(-1, 1, 2) # 獲取透視變換的轉換範圍 dst1 = np.int32(cv2.perspectiveTransform(pts1, H)) dst2 = np.int32(cv2.perspectiveTransform(pts2, H1)) # 在查找圖中畫出查找範圍的矩形 cv2.polylines(img2, [dst1], True, (0, 0, 255), 5) cv2.polylines(img1, [dst2], True, (0, 0, 255), 5) return H, img3 else: print("好的匹配點太少,必須不少於8") exit() def stitch_image(img1, img2, H): ''' 拼接圖像 :param img1: :param img2: :param H: :return: ''' # 獲取兩張圖像的高和寬 h1, w1 = img1.shape[:2] h2, w2 = img2.shape[:2] # 獲取兩張圖片的四個頂點 img1_dims = np.float32([[0, 0], [0, h1], [w1, h1], [w1, 0]]).reshape(-1, 1, 2) img2_dims = np.float32([[0, 0], [0, h2], [w2, h2], [w2, 0]]).reshape(-1, 1, 2) # 獲取第一張圖的透視變換後的圖像範圍 img1_transform = cv2.perspectiveTransform(img1_dims, H) # 將第一張圖像變換後的圖像範圍與第二張圖像的範圍給拼接起來 result_dims = np.concatenate((img1_transform, img2_dims), axis=0) # 獲取聯合範圍的最小值座標和最大值座標 [x_min, y_min] = np.int32(result_dims.min(axis=0).ravel() - 0.5) [x_max, y_max] = np.int32(result_dims.max(axis=0).ravel() + 0.5) # 由於第一張圖透視變換後的圖像範圍超出邊界, # 則聯合範圍的最小值的x、y座標都是負數 # 現在將其轉成正數 transform_dist = [-x_min, -y_min] # 設置平移矩陣,可以將超出邊框的圖像移動到邊框內 transform_array = np.array([[1, 0, transform_dist[0]], [0, 1, transform_dist[1]], [0, 0, 1]]) # 對圖一進行透視變換,轉換矩陣爲平移矩陣與單應性矩陣的點積表示圖一不僅進行了單應性轉換還進行了平移 result_img = cv2.warpPerspective(img1, transform_array.dot(H), (x_max - x_min, y_max - y_min)) # 將圖二放入對接的區域中 result_img[transform_dist[1]:transform_dist[1] + h2, transform_dist[0]:transform_dist[0] + w2] = img2 return result_img H, img3 = get_homo(img1, img2) result_img = stitch_image(img1, img2, H) while True: cv2.imshow('img', result_img) cv2.imshow('img3', img3) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
不過OpenCV有自帶的圖像拼接器
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img1 = cv2.imread("/Users/admin/Documents/map1.png") img2 = cv2.imread("/Users/admin/Documents/map2.png") img1 = cv2.resize(img1, (640, 480)) img2 = cv2.resize(img2, (640, 480)) # 創建一個圖像拼接器對象 stitcher = cv2.Stitcher.create() # 進行圖像拼接,返回的結果是一個元組,包含了索引和拼接後的圖像 result_img = stitcher.stitch([img1, img2]) while True: cv2.imshow('img', result_img[1]) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果