
目錄:
- 函數介紹
- 圖像拼接算法實作
- 圖像拼接算法改進
Image Stitching with OpenCV and Python - PyImageSearchwww.pyimagesearch.com
本文參考上面這個連結,實作多張圖像的拼接,建構一張全景圖。
根據多個圖像建立全景圖的步驟為:
- 檢測兩張圖像的關鍵點特征(DoG、Harris等)
- 計算不變特征描述符(SIFT、SURF或ORB等)
- 根據關鍵點特征和描述符,對兩張圖像進行比對,得到若幹比對點對,并移除錯誤比對;
- 使用Ransac算法和比對的特征來估計單應矩陣(homography matrix);
- 通過單應矩陣來對圖像進行仿射變換;
- 兩圖像拼接,重疊部分融合;
- 裁剪以獲得美觀的最終圖像。
原理比較複雜,本文先不講解,OpenCV中已經實作了全景圖拼接的算法,它們是
cv2.createStitcher
(OpenCV 3.x) 和
cv2.Stitcher_create
(OpenCV 4) 。
該算法對以下條件具有較好的魯棒性:
- 輸入圖像的順序
- 圖像的方向
- 光照變化
- 圖像噪聲
一、函數介紹
OpenCV 3.x 的 cv2.createStitcher 函數原型為:
createStitcher(...)
createStitcher([, try_use_gpu]) -> retval
這個函數有一個參數
try_use_gpu
,它可以用來提升圖像拼接整個過程的速度。
OpenCV 4 的 cv2.Stitcher_create 函數原型為:
Stitcher_create(...)
Stitcher_create([, mode]) -> retval
. @brief Creates a Stitcher configured in one of the stitching
. modes.
.
. @param mode Scenario for stitcher operation. This is usually
. determined by source of images to stitch and their transformation.
. Default parameters will be chosen for operation in given scenario.
. @return Stitcher class instance.
要執行實際的圖像拼接,我們需要調用
.stitch
方法:
OpenCV 3.x:
stitch(...) method of cv2.Stitcher instance
stitch(images[, pano]) -> retval, pano
OpenCV 4.x:
stitch(...) method of cv2.Stitcher instance
stitch(images, masks[, pano]) -> retval, pano
. @brief These functions try to stitch the given images.
.
. @param images Input images.
. @param masks Masks for each input image specifying where to
. look for keypoints (optional).
. @param pano Final pano.
. @return Status code.
該方法接收一個圖像清單,然後嘗試将它們拼接成全景圖像,并進行傳回。
變量
status=0
表示圖像拼接是否成功。
二、圖像拼接算法實作
先把三張圖檔讀取出來存放到清單裡:
img_dir = 'pictures/stitching'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
圖檔順序沒有影響,我試了一下,不同的圖檔順序,輸出全景圖都相同。
然後構造圖像拼接對象
stitcher
, 要注意的是,OpenCV 3 和 4 的構造器是不同的。
import imutils
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
再把圖像清單傳入
.stitch
函數,該函數會傳回狀态和拼接好的全景圖(如果沒有錯誤):
status, stitched = stitcher.stitch(images)
完整代碼如下:
import os
import cv2
import imutils
img_dir = 'pictures/stitching'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
status, stitched = stitcher.stitch(images)
if status==0:
cv2.imwrite('pictures/stitch.jpg', stitched)
OpenCV真的很強大,這短短幾行,就實作了拼接全景圖。
全景圖如下:
呃,全景圖是實作了,但是周圍出現了一些黑色區域。
這是因為建構全景時會做透視變換,透視變換時會産生這些黑色區域。
是以需要做進一步處理,裁剪出全景圖的最大内部矩形區域,也就是隻保留下圖中紅色虛線邊框内的全景區域。
三、圖像拼接算法改進
擷取圖像清單并得到初步全景圖,這兩步還是相同的:
img_dir = 'pictures/stitching'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
status, stitched = stitcher.stitch(images)
在全景圖四周各添加10像素寬的黑色邊框,以確定能夠找到全景圖的完整輪廓:
stitched = cv2.copyMakeBorder(stitched, 10, 10, 10, 10,
cv2.BORDER_CONSTANT, (0, 0, 0))
再将全景圖轉換為灰階圖,并将不為0的像素全置為255,作為前景,其他像素灰階值為0,作為背景。
gray = cv2.cvtColor(stitched, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
現在有了全景圖的二值圖,再應用輪廓檢測,找到最大輪廓的邊界框,
注:和輪廓相關的詳細講解可以檢視 這篇文章。
cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea) # 擷取最大輪廓
mask = np.zeros(thresh.shape, dtype="uint8")
x, y, w, h = cv2.boundingRect(cnt)
# 繪制最大外接矩形框(内部填充)
cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)
這個白色矩形框是整個全景圖可以容納下的最小矩形區域。
接下來就是最難,也是最巧妙的部分了,先建立mask的兩個副本:
-
,這個mask的白色區域會慢慢縮小,直到它剛好可以完全放入全景圖内部。minRect
-
,這個mask用于确定sub
是否需要繼續減小,以得到滿足要求的矩形區域。minRect
minRect = mask.copy()
sub = mask.copy()
# 開始while循環,直到sub中不再有前景像素
while cv2.countNonZero(sub) > 0:
minRect = cv2.erode(minRect, None)
sub = cv2.subtract(minRect, thresh)
不斷地對
minRect
進行腐蝕操作,然後用
minRect
減去之前得到的門檻值圖像,得到
sub
,
再判斷
sub
中是否存在非零像素,如果不存在,則此時的
minRect
就是我們最終想要的全景圖内部最大矩形區域。
sub
和
minRect
在while循環中的變化情況如下動圖所示:
因為OpenCV中灰階圖像素值範圍0-255,如果兩個數相減得到負數的話,會直接将其置為0;如果兩個數相加,結果超過了255的話,則直接置為255。
比如下面這個圖,左圖中白色矩形可以完全包含在全景圖中,但不是全景圖的最大内接矩形,用它減去右邊的門檻值圖,
因為黑色像素減白色像素,會得到黑色像素,是以其結果圖為全黑的圖。
是以上面那個while循環最終得到的
minRect
就是減去門檻值圖得到全黑圖的面積最大的矩形區域。
好了,我們已經得到全景圖的内置最大矩形框了,接下來就是找到這個矩形框的輪廓,并擷取其坐标:
cnts, hierarchy = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
# 計算最大輪廓的邊界框
(x, y, w, h) = cv2.boundingRect(cnt)
# 使用邊界框坐标提取最終的全景圖
stitched = stitched[y:y + h, x:x + w]
得到最終結果圖如下:
完整代碼如下:
import os
import cv2
import imutils
import numpy as np
img_dir = '/images'
names = os.listdir(img_dir)
images = []
for name in names:
img_path = os.path.join(img_dir, name)
image = cv2.imread(img_path)
images.append(image)
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
status, stitched = stitcher.stitch(images)
# 四周填充黑色像素,再得到門檻值圖
stitched = cv2.copyMakeBorder(stitched, 10, 10, 10, 10, cv2.BORDER_CONSTANT, (0, 0, 0))
gray = cv2.cvtColor(stitched, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
mask = np.zeros(thresh.shape, dtype="uint8")
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)
minRect = mask.copy()
sub = mask.copy()
# 開始while循環,直到sub中不再有前景像素
while cv2.countNonZero(sub) > 0:
minRect = cv2.erode(minRect, None)
sub = cv2.subtract(minRect, thresh)
cnts, hierarchy = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
# 使用邊界框坐标提取最終的全景圖
stitched = stitched[y:y + h, x:x + w]
cv2.imwrite('final.jpg', stitched)
如果覺得有用,就點個贊吧(ง •̀_•́)ง。