天天看點

opencv3圖像拼接_拼接圖像得到全景圖

opencv3圖像拼接_拼接圖像得到全景圖

目錄:

  1. 函數介紹
  2. 圖像拼接算法實作
  3. 圖像拼接算法改進

Image Stitching with OpenCV and Python - PyImageSearch​www.pyimagesearch.com

本文參考上面這個連結,實作多張圖像的拼接,建構一張全景圖。

根據多個圖像建立全景圖的步驟為:

  1. 檢測兩張圖像的關鍵點特征(DoG、Harris等)
  2. 計算不變特征描述符(SIFT、SURF或ORB等)
  3. 根據關鍵點特征和描述符,對兩張圖像進行比對,得到若幹比對點對,并移除錯誤比對;
  4. 使用Ransac算法和比對的特征來估計單應矩陣(homography matrix);
  5. 通過單應矩陣來對圖像進行仿射變換;
  6. 兩圖像拼接,重疊部分融合;
  7. 裁剪以獲得美觀的最終圖像。

原理比較複雜,本文先不講解,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真的很強大,這短短幾行,就實作了拼接全景圖。

全景圖如下:

opencv3圖像拼接_拼接圖像得到全景圖

呃,全景圖是實作了,但是周圍出現了一些黑色區域。

這是因為建構全景時會做透視變換,透視變換時會産生這些黑色區域。

是以需要做進一步處理,裁剪出全景圖的最大内部矩形區域,也就是隻保留下圖中紅色虛線邊框内的全景區域。

opencv3圖像拼接_拼接圖像得到全景圖

三、圖像拼接算法改進

擷取圖像清單并得到初步全景圖,這兩步還是相同的:

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))
           
opencv3圖像拼接_拼接圖像得到全景圖

再将全景圖轉換為灰階圖,并将不為0的像素全置為255,作為前景,其他像素灰階值為0,作為背景。

gray = cv2.cvtColor(stitched, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
           
opencv3圖像拼接_拼接圖像得到全景圖

現在有了全景圖的二值圖,再應用輪廓檢測,找到最大輪廓的邊界框,

注:和輪廓相關的詳細講解可以檢視 這篇文章。

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)
           
opencv3圖像拼接_拼接圖像得到全景圖

這個白色矩形框是整個全景圖可以容納下的最小矩形區域。

接下來就是最難,也是最巧妙的部分了,先建立mask的兩個副本:

  • minRect

    ,這個mask的白色區域會慢慢縮小,直到它剛好可以完全放入全景圖内部。
  • sub

    ,這個mask用于确定

    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循環中的變化情況如下動圖所示:

opencv3圖像拼接_拼接圖像得到全景圖

因為OpenCV中灰階圖像素值範圍0-255,如果兩個數相減得到負數的話,會直接将其置為0;如果兩個數相加,結果超過了255的話,則直接置為255。

比如下面這個圖,左圖中白色矩形可以完全包含在全景圖中,但不是全景圖的最大内接矩形,用它減去右邊的門檻值圖,

因為黑色像素減白色像素,會得到黑色像素,是以其結果圖為全黑的圖。

opencv3圖像拼接_拼接圖像得到全景圖

是以上面那個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]
           

得到最終結果圖如下:

opencv3圖像拼接_拼接圖像得到全景圖

完整代碼如下:

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)
           

如果覺得有用,就點個贊吧(ง •̀_•́)ง。