本文為在iOS環(huán)境下利用OpenCV技術(shù)實(shí)現(xiàn)全景圖片合成并對(duì)合成圖片進(jìn)行剪裁的簡(jiǎn)單實(shí)現(xiàn)胎撤。
Image Stitching with OpenCV and Python
本文參考以上連接實(shí)現(xiàn)多張圖像的拼接囚戚,并通過(guò)C++和Objective-C代碼實(shí)現(xiàn)構(gòu)建一張全景圖婚温。
根據(jù)多個(gè)圖像創(chuàng)建全景圖的步驟為:
檢測(cè)兩張圖像的關(guān)鍵點(diǎn)特征(DoG某抓、Harris等)
計(jì)算不變特征描述符(SIFT、SURF或ORB等)
根據(jù)關(guān)鍵點(diǎn)特征和描述符坑质,對(duì)兩張圖像進(jìn)行匹配患雇,得到若干匹配點(diǎn)對(duì),并移除錯(cuò)誤匹配淮野;
使用Ransac算法和匹配的特征來(lái)估計(jì)單應(yīng)矩陣(homography matrix)捧书;
通過(guò)單應(yīng)矩陣來(lái)對(duì)圖像進(jìn)行仿射變換;
兩圖像拼接骤星,重疊部分融合经瓷;
裁剪以獲得美觀的最終圖像。
原理比較復(fù)雜洞难,本文先不講解舆吮,OpenCV中已經(jīng)實(shí)現(xiàn)了全景圖拼接的算法,cv::Stitcher::createDefault(try_use_gpu) (OpenCV 3.x) 和 cv::Stitcher::create()(OpenCV 4.x) 队贱。
該算法對(duì)以下條件具有較好的魯棒性:
輸入圖像的順序
圖像的方向
光照變化
圖像噪聲
OpenCV在Stitcher類中實(shí)現(xiàn)的拼接模塊流程(源)
一色冀、函數(shù)介紹
OpenCV 3.x 的 cv::Stitcher::createDefault() 函數(shù)原型為:
Stitcher stitcher = Stitcher::createDefault(try_use_gpu);
這個(gè)函數(shù)有一個(gè)參數(shù) try_use_gpu,它可以用來(lái)提升圖像拼接整個(gè)過(guò)程的速度柱嫌。
OpenCV 4 的 cv::Stitcher::create()函數(shù)原型為:
Ptr<Stitcher> create(Mode mode = Stitcher::PANORAMA);
要執(zhí)行實(shí)際的圖像拼接锋恬,我們需要調(diào)用 .stitch 方法:
OpenCV 3.x:
stitch(...) method of cv::Stitcher instance
Status cv::Stitcher::stitch(InputArrayOfArrays images,
const std::vector< std::vector< Rect > > & rois,
OutputArray pano )
OpenCV 4.x:
stitch(...) method of cv::Stitcher instance
Status stitch(InputArrayOfArrays images, OutputArray 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.
*/
該方法接收一個(gè)圖像列表,然后嘗試將它們拼接成全景圖像编丘,并進(jìn)行返回与学。
變量 status=0表示圖像拼接是否成功。
二嘉抓、圖像拼接算法實(shí)現(xiàn)
先將圖片讀取出來(lái)放入iOS原生數(shù)組內(nèi)
UIImage*image1 = [UIImageimageNamed:@"pano_01.jpg"];
UIImage*image2 = [UIImageimageNamed:@"pano_02.jpg"];
UIImage*image3 = [UIImageimageNamed:@"pano_03.jpg"];
NSArray*imageArray = @[image1, image2, image3];
[self.imgArr addObjectsFromArray:imageArray];
圖片順序沒(méi)有影響索守,不同的圖片順序,輸出全景圖都相同抑片。
調(diào)用CVWrapper內(nèi)圖像拼接方法
[CVWrapper processWithArray:self.imgArr stitchType:QKDefaultStitch];
該方法會(huì)將傳入的圖片調(diào)整方向后再轉(zhuǎn)成cv::Mat圖片矩陣蕾盯,再將矩陣加入cv::Mat泛型數(shù)組內(nèi),供OpenCV方法拼接圖像后,返回UIImage對(duì)象级遭。
//多張圖片合成處理
+ (UIImage*) processWithArray:(NSArray*)imageArray stitchType:(QKCVWrapperTypeCode)stitchType
{
if ([imageArray count]==0){
NSLog (@"imageArray is empty");
return 0;
}
std::vector<cv::Mat> matImages;
for (id image in imageArray) {
if ([image isKindOfClass: [UIImage class]]) {
/*
All images taken with the iPhone/iPa cameras are LANDSCAPE LEFT orientation.
The UIImage imageOrientation flag is an instruction to the OS to transform the image during display only.
When we feed images into openCV, they need to be the actual orientation that we expect
them to be for stitching. So we rotate the actual pixel matrix here if required.
*/
UIImage* rotatedImage = [image rotateToImageOrientation];
cv::Mat matImage = [rotatedImage CVMat3];
// matImage = testaaaa(matImage);
NSLog (@"matImage: %@",image);
matImages.push_back(matImage);
}
}
NSLog (@"stitching...");
cv::Mat stitchedMat;
switch (stitchType) {
case QKDefaultStitch:
stitchedMat = stitch(matImages);
break;
case QKFisheyeStitch:
stitchedMat = fisheyeStitch(matImages);
break;
case QKPlaneStitch:
stitchedMat = planeStitch(matImages);
break;
default:
break;
}
UIImage* result = [UIImage imageWithCVMat:stitchedMat];
return result;
}
stitch望拖、fisheyeStitch或者planeStitch 方法實(shí)現(xiàn)圖片拼
我們暫時(shí)提供了三種拼接方法,分別為默認(rèn)拼接方法挫鸽、魚(yú)眼相機(jī)拼接方法和環(huán)視(平面曲翹)拼接方法说敏。本文我們主要介紹默認(rèn)拼接方法,其他方法暫不介紹丢郊。
圖片轉(zhuǎn)換好后盔沫,我們將執(zhí)行圖像拼接
首先構(gòu)造拼接對(duì)象stitcher,要注意OpenCV 3和4的構(gòu)造方法是不同的枫匾。
Ptr<Stitcher> stitcher = Stitcher::create();//4.0
然后再把圖像列表傳入.stitch函數(shù)架诞,該函數(shù)會(huì)返回狀態(tài)和拼接好的全景圖(如果沒(méi)有錯(cuò)誤):
//拼接
Stitcher::Status status = stitcher->stitch(imgs, pano);
完整代碼如下
cv::Mat stitch (vector<Mat>& images)
{
imgs = images;
Mat pano;//拼接圖
/* 3.0
Stitcher stitcher = Stitcher::createDefault(try_use_gpu);
Stitcher::Status status = stitcher.stitch(imgs, pano);
*/
Ptr<Stitcher> stitcher = Stitcher::create();//4.0
//拼接
Stitcher::Status status = stitcher->stitch(imgs, pano);
if (status != Stitcher::OK)
{
cout << "Can't stitch images, error code = " << int(status) << endl;
}
return pano;
}
全景圖如下:
通過(guò)openCV基礎(chǔ)方法我們實(shí)現(xiàn)了全景圖,但是周圍出現(xiàn)了一些黑色區(qū)域干茉。
這是因?yàn)闃?gòu)建全景時(shí)會(huì)做透視變換谴忧,透視變換時(shí)會(huì)產(chǎn)生黑色區(qū)域。
所以我們需要進(jìn)一步處理角虫,剪裁出全景圖內(nèi)的最大內(nèi)部矩形區(qū)域沾谓,也就是留下圖中紅色虛線邊框內(nèi)的全景區(qū)域。
三戳鹅、圖像裁剪算法
在活的全景圖后均驶,我們需要對(duì)其進(jìn)行剪裁加工,以便我們得到更完美的圖片枫虏。
1.在全景圖四周各添加10像素寬的黑色邊框妇穴,以確保能夠找到全景圖的完整輪廓:
Mat stitched;//黑色邊框輪廓圖copyMakeBorder(inputMat, stitched,10,10,10,10, cv::BORDER_CONSTANT,true);
2.全景圖轉(zhuǎn)換灰度圖,并將所有大于0的像素全置為255隶债。
Matgray;cv::cvtColor(stitched,gray,cv::COLOR_BGR2GRAY);
3.中值濾波腾它,去除黑色邊際中可能含有的噪聲干擾。
int ksize: 濾波模板的尺寸大小燃异,必須是大于1的奇數(shù),如3继蜡、5回俐、7……
cv::medianBlur(gray, gray,7)
4.作為前景,其他像素灰度值為0稀并,作為背景仅颇。
Mat tresh;
threshold(gray, tresh, 0, 255, THRESH_BINARY);
經(jīng)過(guò)上述步驟操作,我們得到結(jié)果(tresh):
現(xiàn)在有了全景圖的二值圖碘举,其中白色像素(255)是前景忘瓦,黑色像素(0)是背景,通過(guò)我們的閾值圖像引颈,
我們可以應(yīng)用輪廓檢測(cè)耕皮,找到最大輪廓的邊界框(即全景圖本身的輪廓) 境蜕,并繪制邊框。
5.尋找最大輪廓
vector<vector<Point>> contours; //contours:包含圖像中所有輪廓的python列表(三維數(shù)組),每個(gè)輪廓是包含邊界所有坐標(biāo)點(diǎn)(x, y)的Numpy數(shù)組凌停。
vector<Vec4i> hierarchy = vector<cv::Vec4i>();//vec4i是一種用于表示具有4個(gè)維度的向量的結(jié)構(gòu)粱年,每個(gè)值都小于cc>
findContours(tresh.clone(), contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//傳入?yún)?shù)不一樣
//計(jì)算最大輪廓的邊界框
int index = getMaxContour(contours);
if (index == -1) {
return -1;
}
vector<Point> cnt = contours[index];
//使用邊界矩形信息,將輪廓內(nèi)填充成白色
drawContours(tresh, contours, index, Scalar(255,0,0));
//蒙板
Mat mask = Mat::zeros(tresh.rows, tresh.cols, CV_8UC1); // 0矩陣
//依賴輪廓罚拟,繪制最大外接矩形框(內(nèi)部填充)
Rect cntRect = cv::boundingRect(cnt);
rectangle(mask, cntRect, cv::Scalar(255, 0, 0), -1);
//循環(huán)最大的輪廓邊框
int getMaxContour(std::vector<vector<cv::Point>> contours){
double max_area = 0;
int index = -1;
for (int i = 0; i < contours.size(); i++) {
double tempArea = contourArea(contours[i]);
if (tempArea > max_area) {
max_area = tempArea;
index = i;
}
}
return index;
}
經(jīng)過(guò)上述操作我們得到下圖:
這個(gè)白色區(qū)域是整個(gè)全景圖可以容納下的最小的矩形區(qū)域台诗。
接下來(lái)我們將進(jìn)行微微關(guān)鍵和巧妙的部分。
首先我們創(chuàng)建一塊mask蒙板的兩個(gè)副本赐俗。
minRect拉队,這個(gè)mask的白色區(qū)域會(huì)慢慢縮小,直到它剛好可以完全放入全景圖內(nèi)部阻逮。
sub粱快,這個(gè)mask用于確定minRect是否需要繼續(xù)減小,以得到滿足要求的矩形區(qū)域夺鲜。
不斷地對(duì)minRect進(jìn)行腐蝕操作皆尔,然后用minRect減去之前得到的閾值圖像,得到sub币励,
再判斷sub中是否存在非零像素慷蠕,如果不存在,則此時(shí)的minRect就是我們最終想要的全景圖內(nèi)部最大矩形區(qū)域食呻。
sub和minRect在while循環(huán)中的變化情況如下動(dòng)圖所示:
因?yàn)镺penCV中灰度圖像素值范圍0-255流炕,如果兩個(gè)數(shù)相減得到負(fù)數(shù)的話,會(huì)直接將其置為0仅胞;如果兩個(gè)數(shù)相加每辟,結(jié)果超過(guò)了255的話,則直接置為255干旧。
比如下面這個(gè)圖渠欺,左圖中白色矩形可以完全包含在全景圖中,但不是全景圖的最大內(nèi)接矩形椎眯,用它減去右邊的閾值圖挠将,
因?yàn)楹谏袼販p白色像素,會(huì)得到黑色像素编整,所以其結(jié)果圖為全黑的圖舔稀。
既然我們已經(jīng)得到了全景圖內(nèi)的內(nèi)置最大矩形邊框,接下來(lái)就是找到這個(gè)矩形框的輪廓掌测,并獲取其坐標(biāo):
//第二次循環(huán)
cv::Mat minRectClone = minRect.clone();
cv::resize(minRectClone, minRectClone,
cv::Size(minRectClone.cols * scale, minRectClone.rows * scale),
(float)minRect.cols / 2, (float)minRect.rows / 2,INTER_LINEAR);
std::vector<std::vector<Point> > cnts;
vector<Vec4i> hierarchyA = vector<cv::Vec4i>();
findContours(minRectClone, cnts, hierarchyA, RETR_TREE, CHAIN_APPROX_SIMPLE);
int idx = getMaxContour(cnts);
if (idx == -1) {
return -1;
}
//最終矩形輪廓
Rect finalRect = cv::boundingRect(cnts[idx]);
最后我們通內(nèi)接矩形輪廓内贮,提取最終的全景圖
//提取最終全景圖
outputMat = Mat(stitched, finalRect).clone();
得到最終結(jié)果圖如下:
源代碼如下:
#include "stitching.h"
#include "algorithm"
#include <iostream>
#include <fstream>//openCV 2.4.x
//#include "opencv2/stitching/stitcher.hpp"
//openCV 3.x
#include "opencv2/stitching.hpp"
//cpenCV 4.x 以上調(diào)用混編,OC類需將引入的openCV頭文件放入s引入的最前方
using namespace std;
using namespace cv;
bool try_use_gpu = false;
vector<Mat> imgs;
string result_name = "result.jpg";
int thresh = 100;
int max_thresh = 255;
RNG rng(12345);
const int scale = 2;
void printUsage();
int parseCmdArgs(int argc, char** argv);
int getMaxContour(std::vector<vector<cv::Point>> contours);
cv::Mat stitch (vector<Mat>& images)
{
imgs = images;
Mat pano;//拼接圖
/* 3.0
Stitcher stitcher = Stitcher::createDefault(try_use_gpu);
Stitcher::Status status = stitcher.stitch(imgs, pano);
*/
Ptr<Stitcher> stitcher = Stitcher::create();//4.0
//拼接
Stitcher::Status status = stitcher->stitch(imgs, pano);
if (status != Stitcher::OK)
{
cout << "Can't stitch images, error code = " << int(status) << endl;
}
return pano;
}
int corpBoundingRect(cv::Mat &inputMat, cv::Mat &outputMat)
{
//在全景圖四周各添加10像素寬的黑色邊框,以確保能夠找到全景圖的完整輪廓:
Mat stitched;//黑色邊框輪廓圖
copyMakeBorder(inputMat, stitched, 10, 10, 10, 10, cv::BORDER_CONSTANT, true);
//全景圖轉(zhuǎn)換灰度圖夜郁,并將不為0的像素全置為255
//作為前景尖啡,其他像素灰度值為0尤辱,作為背景。
Mat gray;
cv::cvtColor(stitched, gray, cv::COLOR_BGR2GRAY);
//中值濾波,去除黑色邊際中可能含有的噪聲干擾
cv::medianBlur(gray, gray, 7);
//白色剪影與黑色背景
Mat tresh;
threshold(gray, tresh, 0, 255, THRESH_BINARY);
//resize 縮小一半處理
resize(tresh, tresh,
Size(tresh.cols / scale, tresh.rows / scale),
tresh.cols / 2,
tresh.rows / 2, INTER_LINEAR);
//現(xiàn)在有了全景圖的二值圖护昧,再應(yīng)用輪廓檢測(cè)滥玷,找到最大輪廓的邊界框两疚,
vector<vector<Point>> contours; //contours:包含圖像中所有輪廓的python列表(三維數(shù)組),每個(gè)輪廓是包含邊界所有坐標(biāo)點(diǎn)(x, y)的Numpy數(shù)組量窘。
vector<Vec4i> hierarchy = vector<cv::Vec4i>();//vec4i是一種用于表示具有4個(gè)維度的向量的結(jié)構(gòu),每個(gè)值都小于cc>
findContours(tresh.clone(), contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//傳入?yún)?shù)不一樣
//計(jì)算最大輪廓的邊界框
int index = getMaxContour(contours);
if (index == -1) {
return -1;
}
vector<Point> cnt = contours[index];
drawContours(tresh, contours, index, Scalar(255,0,0));
//蒙板
Mat mask = Mat::zeros(tresh.rows, tresh.cols, CV_8UC1); // 0矩陣
//依賴輪廓?jiǎng)?chuàng)建矩形
Rect cntRect = cv::boundingRect(cnt);
rectangle(mask, cntRect, cv::Scalar(255, 0, 0), -1);
Mat minRect = mask.clone();//minRect的白色區(qū)域會(huì)慢慢縮小赵颅,直到它剛好可以完全放入全景圖內(nèi)部虽另。
Mat sub = mask.clone();//sub用于確定minRect是否需要繼續(xù)減小,以得到滿足要求的矩形區(qū)域饺谬。
//開(kāi)始while循環(huán)捂刺,直到sub中不再有前景像素
while (cv::countNonZero(sub) > 0) {
// int zero = cv::countNonZero(sub);
// printf("剩余前景像素 %d \n",zero);
cv::erode(minRect, minRect, Mat());
cv::subtract(minRect, tresh, sub);
}
//第二次循環(huán)
cv::Mat minRectClone = minRect.clone();
cv::resize(minRectClone, minRectClone,
cv::Size(minRectClone.cols * scale, minRectClone.rows * scale),
(float)minRect.cols / 2, (float)minRect.rows / 2,INTER_LINEAR);
std::vector<std::vector<Point> > cnts;
vector<Vec4i> hierarchyA = vector<cv::Vec4i>();
findContours(minRectClone, cnts, hierarchyA, RETR_TREE, CHAIN_APPROX_SIMPLE);
int idx = getMaxContour(cnts);
if (idx == -1) {
return -1;
}
Rect finalRect = cv::boundingRect(cnts[idx]);
//
//// printf("finalRect {x = %d, y = %d, width = %d, height = %d \n", finalRect.x, finalRect.y, finalRect.width, finalRect.height);
outputMat = Mat(stitched, finalRect).clone();
return 0;
}
//魚(yú)眼拼接
cv::Mat fisheyeStitch (vector<Mat>& images) {
imgs = images;
Mat pano;
// Stitcher stitcher = Stitcher::createDefault(try_use_gpu);
Ptr<Stitcher> stitcher = Stitcher::create();//4.0
Ptr<FisheyeWarper> fisheye_warper = makePtr<cv::FisheyeWarper>();
// stitcher.setWarper(fisheye_warper);
stitcher->setWarper(fisheye_warper);
//拼接
// Stitcher::Status status = stitcher.stitch(imgs, pano);
Stitcher::Status status = stitcher->stitch(imgs, pano);
if (status != Stitcher::OK)
{
cout << "Can't stitch images, error code = " << int(status) << endl;
//return 0;
}
return pano;
}