1. 程式人生 > >使用OpenCV和Python拼接影象

使用OpenCV和Python拼接影象

寫在前面

首先這是一篇英文部落格的翻譯,先放上鍊接:https://www.pyimagesearch.com/2018/12/17/image-stitching-with-opencv-and-python/
翻譯是靠谷歌翻譯和自己的理解,個別地方翻譯有點問題,請對照原文,大神可以直接閱讀原文。
知道Adrian Rosebrock有一段時間了,是一位高質量、高產的大神,寫的部落格有很多幹貨。
翻譯的目的,一是對影象拼接感興趣,二是可以加深理解和記憶,後續可能還會翻譯一些部落格,向大佬學習!
這篇部落格主要是利用OpenCV內建的庫實現的影象拼接,沒有涉及太多理論知識,更多內容可以閱讀博主之前的部落格(下面有介紹),以及搜尋相關資料。

Introduction

在這裡插入圖片描述
在本教程中,您將學習如何使用Python,OpenCV以及cv2.createStitchercv2.Stitcher_create函式執行影象拼接。使用今天的程式碼,您將能夠將多個影象拼接在一起,建立拼接影象的全景圖。

就在不到兩年前,我釋出了兩個關於影象拼接和全景構造的指南:

這兩個教程都涵蓋了典型影象拼接演算法的基礎知識,它至少需要四個關鍵步驟:
1.檢測關鍵點(DoG,Harris等)並從兩個輸入影象中提取區域性不變描述子(SIFT,SURF等)
2.匹配影象之間的描述子
3.使用RANSAC演算法使用我們匹配的特徵向量估計單應矩陣
4.使用從步驟#3獲得的單應矩陣應用warping變換

但是,我原來實現的最大問題是它們無法處理兩個以上的輸入影象。

在今天的教程中,我們將重新審視使​​用OpenCV的影象拼接,包括如何將兩個以上的影象拼接成全景影象。

使用OpenCV和Python拼接影象

在今天教程的第一部分中,我們將簡要回顧OpenCV的影象拼接演算法,該演算法通過cv2.createStitchercv2.Stitcher_create函式加入到OpenCV庫中。
從那裡我們將審查我們的專案結構並實現可用於影象拼接的Python指令碼。
我們將檢視第一個指令碼的結果,注意其侷限性,然後實現第二個Python指令碼,該指令碼可用於更美觀的影象拼接結果。
最後,我們將審查第二個指令碼的結果,並再次注意任何限制或缺點。

OpenCV的影象拼接演算法

在這裡插入圖片描述
我們今天在這裡使用的演算法類似於Brown和Lowe在其2017年論文“Automatic Panoramic Image Stitching with Invariant Features”中提出的方法。
與以前對輸入影象的排序敏感的影象拼接演算法不同,Brown和Lowe方法更加魯棒,使其對以下內容不敏感:

  • 影象的順序
  • 影象的方向
  • 光照變化
  • 實際上不是全景圖的一部分的噪聲影象

此外,它們的影象拼接方法能夠通過使用增益補償和影象混合產生更美觀的輸出全景影象。
對該演算法的完整,詳細的審查超出了本文的範圍,因此如果您有興趣瞭解更多資訊,請參閱原始出版物

專案結構

讓我們看看如何使用tree命令組織此專案:

$ tree --dirsfirst
.
├── images
│   └── scottsdale
│       ├── IMG_1786-2.jpg
│       ├── IMG_1787-2.jpg
│       └── IMG_1788-2.jpg
├── image_stitching.py
├── image_stitching_simple.py
└── output.png
 
2 directories, 6 files

輸入影象在images/資料夾。我選擇為我的scottsdale/影象集製作一個子資料夾,以防我想在以後新增其他子資料夾。
今天我們將回顧兩個Python指令碼:

  • image_stitching_simple.py:我們的簡單版影象拼接可以在不到50行的Python程式碼中完成!
  • image_stitching.py:這個指令碼包括我的hack來提取拼接影象的ROI以獲得美觀​​的結果。

最後一個檔案output.png是生成的拼接影象的名稱。使用命令列引數,您可以輕鬆更改輸出影象的檔名+路徑。

cv2.createStitcher和cv2.Stitcher_create函式

在這裡插入圖片描述
OpenCV已經通過cv2.createStitcher(OpenCV 3.x)和cv2.Stitcher_create(OpenCV 4)函式實現了類似於Brown和Lowe的論文的方法。
假設您已正確配置和安裝OpenCV,您將能夠查詢OpenCV 3.x的cv2.createStitcher的函式簽名:

createStitcher(...)
    createStitcher([, try_use_gpu]) -> retval

請注意,此函式只有一個引數try_gpu,可用於改善整個影象拼接pipeline。OpenCV的GPU支援是有限的,我從來沒有能夠使這個引數工作,所以我建議總是把它保留為False

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變數指示影象拼接是否成功,並且可以是以下四個變數之一:

  • OK = 0:影象拼接成功。
  • ERR_NEED_MORE_IMGS = 1:如果您收到此狀態程式碼,則需要更多輸入影象來構建全景圖。通常,如果輸入影象中檢測不到足夠的關鍵點,則會發生此錯誤。
  • ERR_HOMOGRAPHY_EST_FAIL = 2:當RANSAC單應性估計失敗時,會發生此錯誤。同樣,您可能需要更多影象,或者您的影象沒有足夠的區別,獨特的紋理/物件,以便準確匹配關鍵點。
  • ERR_CAMERA_PARAMS_ADJUST_FAIL = 3:我之前從未遇到過這個錯誤,所以我對它沒有多少了解,但要點是它與未能從輸入影象中正確估計相機內參/外參有關。如果遇到此錯誤,您可能需要參考OpenCV文件,甚至可以深入瞭解OpenCV C ++程式碼。

現在我們已經回顧了cv2.createStitchercv2.Stitcher_create.stitch方法,讓我們繼續實際使用OpenCV和Python實現影象拼接。

用Python實現影象拼接

開啟image_stitching_simple.py檔案並插入以下程式碼:

# import the necessary packages
from imutils import paths
import numpy as np
import argparse
import imutils
import cv2
 
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--images", type=str, required=True,
	help="path to input directory of images to stitch")
ap.add_argument("-o", "--output", type=str, required=True,
	help="path to the output image")
args = vars(ap.parse_args())

我們所需的包由第2-6行語句匯入。值得注意的是,我們將使用OpenCV和imutils。如果您還沒有,請繼續安裝它們:

  • 要安裝OpenCV,只需按照我的OpenCV安裝指南之一操作即可。
  • 可以使用pip:pip install --upgrade imutils安裝/更新imutils包。請務必升級它,因為通常會新增新功能。

如果您不熟悉argparse和命令列引數的概念,請閱讀此部落格文章
讓我們載入輸入影象:

# grab the paths to the input images and initialize our images list
print("[INFO] loading images...")
imagePaths = sorted(list(paths.list_images(args["images"])))
images = []
 
# loop over the image paths, load each one, and add them to our images to stitch list
for imagePath in imagePaths:
	image = cv2.imread(imagePath)
	images.append(image)

在這裡我們獲取我們的imagePaths(第2行)。
然後,對於每個imagePath,我們將載入image並將其新增到images(第3-8行)。
現在,images在記憶體中,讓我們繼續使用OpenCV的內建功能將它們拼接成全景圖:

# initialize OpenCV's image stitcher object and then perform the image stitching
print("[INFO] stitching images...")
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
(status, stitched) = stitcher.stitch(images)

在第3行建立了stitcher物件。請注意,根據您使用的是OpenCV 3還是4,將呼叫不同的建構函式。
隨後,我們可以將影象傳遞給.stitch方法(第4行)。對.stitch的呼叫返回statusstitched影象(假設拼接成功)。
最後,我們將(1)將拼接影象寫入磁碟並(2)在螢幕上顯示:

# if the status is '0', then OpenCV successfully performed image stitching
if status == 0:
	# write the output stitched image to disk
	cv2.imwrite(args["output"], stitched)
 
	# display the output stitched image to our screen
	cv2.imshow("Stitched", stitched)
	cv2.waitKey(0)
 
# otherwise the stitching failed, likely due to not enough keypoints) being detected
else:
	print("[INFO] image stitching failed ({})".format(status))

假設我們的狀態標誌指示成功(第2行),我們將stitched影象寫入磁碟(第4行)並顯示它直到按下一個鍵(第7和8行)。
否則,我們只會列印一條失敗訊息(第11和12行)。

基本影象拼接結果

要嘗試使用影象拼接指令碼,請確保使用教程的“Downloads”部分下載原始碼和示例影象。
images/scottsdale/目錄中,你會發現我在亞利桑那州斯科茨代爾參觀Frank Lloyd Wright著名的Taliesin West房子時拍攝的三張照片:
在這裡插入圖片描述
我們的目標是將這三幅影象拼接成一幅全景影象。要執行拼接,請開啟終端,導航到下載程式碼和影象的位置,然後執行以下命令:

$ python image_stitching_simple.py --images images/scottsdale --output output.png
[INFO] loading images...
[INFO] stitching images...

在這裡插入圖片描述
請注意我們已經成功執行影象拼接!
但那些全景周圍的黑色區域呢?那些是什麼?
這些區域來自執行構建全景圖所需的視角warps。
有一種方法可以擺脫它們……但我們需要在下一節中實現一些額外的邏輯。

使用OpenCV和Python的更好的影象拼接器

在這裡插入圖片描述
我們的第一個影象拼接指令碼是一個良好的開端,但圍繞全景本身的那些黑色區域不是我們稱之為“美學上令人愉悅”的東西。
更重要的是,你不會在iOS,Android等內建流行的影象拼接應用程式中看到這樣的輸出影象。
因此,我們將稍微hack我們的指令碼幷包含一些額外的邏輯來建立更美觀的全景圖。
我將再次重申,這種方法是一種黑客(hack)行為。
我們將審查基本的影象處理操作,包括閾值,輪廓提取,形態學操作等,以獲得我們想要的結果。
據我所知,OpenCV的Python繫結並沒有為我們提供手動提取全景圖的最大內部矩形區域所需的資訊。如果OpenCV可以做到,請在評論中告訴我,我很想知道。
讓我們繼續並開始 - 開啟image_stitching.py指令碼並插入以下程式碼:

# import the necessary packages
from imutils import paths
import numpy as np
import argparse
import imutils
import cv2

# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--images", type=str, required=True,
	help="path to input directory of images to stitch")
ap.add_argument("-o", "--output", type=str, required=True,
	help="path to the output image")
ap.add_argument("-c", "--crop", type=int, default=0,
	help="whether to crop out largest rectangular region")
args = vars(ap.parse_args())

# grab the paths to the input images and initialize our images list
print("[INFO] loading images...")
imagePaths = sorted(list(paths.list_images(args["images"])))
images = []

# loop over the image paths, load each one, and add them to our images to stich list
for imagePath in imagePaths:
	image = cv2.imread(imagePath)
	images.append(image)

# initialize OpenCV's image sticher object and then perform the image stitching
print("[INFO] stitching images...")
stitcher = cv2.createStitcher() if imutils.is_cv3() else cv2.Stitcher_create()
(status, stitched) = stitcher.stitch(images)

所有這些程式碼都與我們之前的指令碼相同,只有一個例外。
添加了--crop命令列引數。當在終端中為此引數提供1時,我們將繼續執行我們的裁剪hack。
下一步我們開始實現其他功能:

# if the status is '0', then OpenCV successfully performed image stitching
if status == 0:
	# check to see if we supposed to crop out the largest rectangular region from the stitched image
	if args["crop"] > 0:
		# create a 10 pixel border surrounding the stitched image
		print("[INFO] cropping...")
		stitched = cv2.copyMakeBorder(stitched, 10, 10, 10, 10,
			cv2.BORDER_CONSTANT, (0, 0, 0))

		# convert the stitched image to grayscale and threshold it
		# such that all pixels greater than zero are set to 255
		# (foreground) while all others remain 0 (background)
		gray = cv2.cvtColor(stitched, cv2.COLOR_BGR2GRAY)
		thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)[1]

注意我是如何在第4行設定--crop標誌時建立一個新塊的。讓我們開始討論這個塊:

  • 首先,我們將在拼接影象的所有邊上新增10畫素邊框(第7和8行),確保我們能夠在本節後面找到完整全景輪廓的輪廓。
  • 然後我們將建立一個灰度版本的拼接影象(第13行)。
  • 從那裡我們對灰度影象進行閾值處理(第14行)。

以下是這三個步驟的結果(閾值):
在這裡插入圖片描述
我們現在有一個全景圖的二進位制影象,其中白色畫素(255)是前景,黑色畫素(0)是背景。
給定我們的閾值影象,我們可以應用輪廓提取,計算最大輪廓的邊界框(即全景輪廓本身的輪廓),並繪製邊界框:

		# find all external contours in the threshold image then find
		# the *largest* contour which will be the contour/outline of
		# the stitched image
		cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
			cv2.CHAIN_APPROX_SIMPLE)
		cnts = imutils.grab_contours(cnts)
		c = max(cnts, key=cv2.contourArea)

		# allocate memory for the mask which will contain the
		# rectangular bounding box of the stitched image region
		mask = np.zeros(thresh.shape, dtype="uint8")
		(x, y, w, h) = cv2.boundingRect(c)
		cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)

在第4-6行上提取和解析輪廓。然後,第7行抓取具有最大區域的輪廓(即拼接影象本身的輪廓)。
注意:imutils.grab_contours函式是imutils == 0.5.2中的新功能,以適應OpenCV 2.4,OpenCV 3和OpenCV 4以及它們對cv2.findContours的不同返回簽名。
第11行為我們的新矩形mask分配記憶體。然後,第12行計算出最大輪廓的邊界框。使用邊界矩形資訊,在第13行,我們在mask上繪製一個純白色矩形。
上面程式碼塊的輸出如下所示:
在這裡插入圖片描述
此邊界框是整個全景圖可以容納的最小矩形區域。
現在,這裡出現了我為部落格文章撰寫的最大的hack之一:

		# create two copies of the mask: one to serve as our actual
		# minimum rectangular region and another to serve as a counter
		# for how many pixels need to be removed to form the minimum
		# rectangular region
		minRect = mask.copy()
		sub = mask.copy()

		# keep looping until there are no non-zero pixels left in the
		# subtracted image
		while cv2.countNonZero(sub) > 0:
			# erode the minimum rectangular mask and then subtract
			# the thresholded image from the minimum rectangular mask
			# so we can count if there are any non-zero pixels left
			minRect = cv2.erode(minRect, None)
			sub = cv2.subtract(minRect, thresh)

在第5和6行,我們建立了兩個掩模影象副本:

  • minMask,第一個mask,將逐漸縮小,直到它可以放入全景內部。
  • sub,第二個mask,將用於確定是否需要繼續減小minMask的大小。

第10行開始一個while迴圈,它將繼續迴圈,直到sub中沒有更多的前景畫素。
第14行執行侵蝕形態學操作以減小minRect的大小。
第15行然後從minRect中減去thresh ,一旦minRect中沒有更多的前景畫素,我們就可以從迴圈中斷開。
我在下面做了一個動畫:
在這裡插入圖片描述
上面是sub影象,下面是minRect影象。
注意minRect的大小是如何逐漸減小的,直到sub中沒有剩下前景畫素為止,此時我們知道我們已經找到了可以適合全景最大矩形區域的最小矩形掩模。
給定最小內部矩形,我們可以再次找到輪廓並計算邊界框,但這次我們只需從拼接影象中提取ROI:

		# find contours in the minimum rectangular mask and then
		# extract the bounding box (x, y)-coordinates
		cnts = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL,
			cv2.CHAIN_APPROX_SIMPLE)
		cnts = imutils.grab_contours(cnts)
		c = max(cnts, key=cv2.contourArea)
		(x, y, w, h) = cv2.boundingRect(c)

		# use the bounding box coordinates to extract the our final
		# stitched image
		stitched = stitched[y:y + h, x:x + w]

這裡:

  • minRect中找到輪廓(第3和4行)。
  • 為多個OpenCV版本處理解析輪廓(第5行)。您需要imutils>= 0.5.2才能使用此功能。
  • 抓住最大的輪廓(第6行)。
  • 計算最大輪廓的邊界框(第7行)。
  • 使用邊界框資訊從我們的拼接影象中提取ROI(第11行)。

最終拼接的影象可以顯示在我們的螢幕上,然後儲存到磁碟:

	# write the output stitched image to disk
	cv2.imwrite(args["output"], stitched)

	# display the output stitched image to our screen
	cv2.imshow("Stitched", stitched)
	cv2.waitKey(0)

# otherwise the stitching failed, likely due to not enough keypoints)
# being detected
else:
	print("[INFO] image stitching failed ({})".format(status))

改進的影象拼接效果

開啟一個終端並執行以下命令:

$ python image_stitching.py --images images/scottsdale --output output.png \
	--crop 1
[INFO] loading images...
[INFO] stitching images...
[INFO] cropping...

在這裡插入圖片描述
注意這次我們如何通過應用上面詳述的hack從輸出拼接影象中去除黑色區域(由warping變換引起)。

限制和缺點

在之前的教程中,我演示瞭如何構建實時全景影象拼接演算法 ,這個教程取決於我們手動執行關鍵點檢測,特徵提取和關鍵點匹配這一事實,使我們能夠訪問使用的單應矩陣將我們的兩個輸入影象變成全景圖。
雖然OpenCV的內建cv2.createStitchercv2.Stitcher_create函式當然能夠構建準確,美觀的全景圖,但該方法的主要缺點之一是它抽象出對單應矩陣的任何訪問。
實時全景構造的假設之一是場景本身在內容方面沒有太大變化。
一旦我們計算出初始單應性估計,我們只需要偶爾重新計算矩陣。
無需執行完整的關鍵點匹配和RANSAC估計,在構建全景圖時可以大大提高速度,因此無需訪問原始單應矩陣,採用OpenCV的內建影象拼接演算法並將其轉換為實時具有挑戰性。

使用OpenCV執行影象拼接時遇到錯誤?

嘗試使用cv2.createStitcher函式或cv2.Stitcher_create函式時,可能會遇到錯誤。
我看到人們遇到的兩個“易於解決”的錯誤就是忘記了他們正在使用的OpenCV版本。
例如,如果您使用OpenCV 4但嘗試呼叫cv2.createSticher,您將遇到以下錯誤訊息:

cv2.createStitcher
Traceback (most recent call last):
File “”, line 1, in
AttributeError: module ‘cv2’ has no attribute ‘createStitcher’

您應該使用cv2.Stitcher_create函式。
同樣,如果您使用OpenCV 3並嘗試呼叫cv2.Sticher_create,您將收到此錯誤:

cv2.Stitcher_create
Traceback (most recent call last):
File “”, line 1, in
AttributeError: module ‘cv2’ has no attribute ‘Stitcher_create’

而是使用cv2.createSticher函式。
如果您不確定您使用的是哪個OpenCV版本,可以使用cv2 .__ version__進行檢查:

>>> cv2.__version__
'4.0.0'

在這裡你可以看到我正在使用OpenCV 4.0.0。
您可以對系統執行相同的檢查。
您可能遇到的最終錯誤,可以說是最常見的錯誤,與OpenCV(1)沒有貢獻支援和(2)在未啟用OPENCV_ENABLE_NONFREE = ON選項的情況下編譯有關。
要解決此錯誤,必須安裝opencv_contrib模組並將OPENCV_ENABLE_NONFREE選項設定為ON
如果您遇到與OpenCV的none-freecontrib模組相關的錯誤,請確保參考我的OpenCV安裝指南以確保您完全安裝了OpenCV。
注意:如果您未遵循我的某個安裝指南,我將無法除錯您自己安裝的OpenCV,因此請確保在配置系統時使用我的OpenCV安裝指南。

總結

在今天的教程中,您學習瞭如何使用OpenCV和Python執行多個影象拼接。
使用OpenCV和Python,我們能夠將多個影象拼接在一起並建立全景影象。
我們的輸出全景影象不僅精確的拼接位置,而且美觀也令人愉悅。
然而,使用OpenCV的內建影象拼接類的一個最大缺點是它抽象了大部分內部計算,包括產生的單應矩陣本身。
如果您嘗試執行實時影象拼接,就像我們在上一篇文章中所做的那樣,您可能會發現快取單應矩陣並且偶爾執行關鍵點檢測,特徵提取和特徵匹配是有益的。
跳過這些步驟並使用快取矩陣執行透視變形可以減少管道的計算負擔並最終加速實時影象拼接演算法,但不幸的是,OpenCV的cv2.createStitcher Python繫結不能讓我們訪問原始矩陣。
如果您有興趣瞭解有關實時全景構建的更多資訊,請參閱我之前的帖子
我希望你喜歡今天關於影象拼接的教程!