圖像拼接比較經典的是SIFT、SURF、ORB等算法。其中SURF是SIFT的更新版,在實時性上要優于後者。本次先實作圖檔級的融合、拼接。
SURF的建構流程是:建構Hessian矩陣、H矩陣判别式、建構尺度空間、精确定位特征點、主方向确定、特征點描述子生成、誤比對點剔除、融合圖像、優化連接配接處的圖像。
//zjy 2021.7.19 周五 SURF圖像融合
#include <iostream>
#include <stdio.h>
#include "opencv2/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/core/ocl.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/calib3d.hpp"
#include "opencv2/imgproc.hpp"
#include"opencv2/xfeatures2d.hpp"
#include"opencv2/ml.hpp"
using namespace cv;
using namespace std;
using namespace cv::xfeatures2d;
using namespace cv::ml;
void OptimizeSeam(Mat& img1, Mat& trans, Mat& dst);
typedef struct //結構體定義,126行-134行,four_corners_t是一個變量
{
Point2f left_top;
Point2f left_bottom; //point2f代表2維,需要X,Y軸來确定
Point2f right_top;
Point2f right_bottom;
}four_corners_t;
four_corners_t corners;
void CalcCorners(const Mat& H, const Mat& src) //const保護資料的值不被修改
{
double v2[] = { 0, 0, 1 };//左上角
double v1[3];//變換後的坐标值
Mat V2 = Mat(3, 1, CV_64FC1, v2); //列向量,CV_64FC164位浮點數,通道為1
Mat V1 = Mat(3, 1, CV_64FC1, v1); //列向量
V1 = H * V2;
//左上角(0,0,1)
cout << "V2: " << V2 << endl;
cout << "V1: " << V1 << endl;
corners.left_top.x = v1[0] / v1[2];
corners.left_top.y = v1[1] / v1[2];
//左下角(0,src.rows,1)
v2[0] = 0;
v2[1] = src.rows;
v2[2] = 1;
V2 = Mat(3, 1, CV_64FC1, v2); //列向量
V1 = Mat(3, 1, CV_64FC1, v1); //列向量
V1 = H * V2;
corners.left_bottom.x = v1[0] / v1[2];
corners.left_bottom.y = v1[1] / v1[2];
//右上角(src.cols,0,1)
v2[0] = src.cols;
v2[1] = 0;
v2[2] = 1;
V2 = Mat(3, 1, CV_64FC1, v2); //列向量
V1 = Mat(3, 1, CV_64FC1, v1); //列向量
V1 = H * V2;
corners.right_top.x = v1[0] / v1[2];
corners.right_top.y = v1[1] / v1[2];
//右下角(src.cols,src.rows,1)
v2[0] = src.cols;
v2[1] = src.rows;
v2[2] = 1;
V2 = Mat(3, 1, CV_64FC1, v2); //列向量
V1 = Mat(3, 1, CV_64FC1, v1); //列向量
V1 = H * V2;
corners.right_bottom.x = v1[0] / v1[2];
corners.right_bottom.y = v1[1] / v1[2];
}
int main()
{
Mat a = imread("C:\\Users\\dell\\Desktop\\OpenCV project\\SURF\\surf_1.jpg", 1);//右圖
Mat b = imread("C:\\Users\\dell\\Desktop\\OpenCV project\\SURF\\surf_2.jpg", 1);//左圖
Ptr<SURF> surf; //建立方式和OpenCV2中的不一樣,并且要加上命名空間xfreatures2d
//否則即使配置好了還是顯示SURF為未聲明的辨別符
surf = SURF::create(800);
BFMatcher matcher; //執行個體化一個暴力比對器
Mat c, d;
vector<KeyPoint> key1, key2;
vector<DMatch> matches; //DMatch是用來描述比對好的一對特征點的類,包含這兩個點之間的相關資訊
//比如左圖有個特征m,它和右圖的特征點n最比對,這個DMatch就記錄它倆最比對,并且還記錄m和n的
//特征向量的距離和其他資訊,這個距離在後面用來做篩選
surf->detectAndCompute(a, Mat(), key1, c);//輸入圖像,輸入掩碼用于屏蔽源圖像中的特定區域,輸入特征點矢量數組 ,存放所有特征點的描述向量
surf->detectAndCompute(b, Mat(), key2, d);//這個Mat行數為特征點的個數,列數為每個特征向量的尺寸,SURF是64(維)
matcher.match(d, c, matches); //比對,資料來源是特征向量,結果存放在DMatch類型裡面
//sort函數對資料進行升序排列
sort(matches.begin(), matches.end()); //篩選比對點,根據match裡面特征對的距離從小到大排序
vector<DMatch> good_matches; //保留好的特征點,剔除誤比對點
int ptsPairs = std::min(50, (int)(matches.size() * 0.15));
cout << ptsPairs << endl;
for (int i = 0; i < ptsPairs; i++)
{
good_matches.push_back(matches[i]);//距離最小的50個壓入新的DMatch
}
Mat outimg; //drawMatches這個函數直接畫出擺在一起的圖
drawMatches(b, key2, a, key1, good_matches, outimg, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS); //繪制比對點
imshow("combine", outimg);
//計算圖像配準點
vector<Point2f> imagePoints1, imagePoints2;
for (int i = 0; i < good_matches.size(); i++)
{
imagePoints2.push_back(key2[good_matches[i].queryIdx].pt);
imagePoints1.push_back(key1[good_matches[i].trainIdx].pt);
}
//擷取圖像1到圖像2的投影映射矩陣 尺寸為3*3,剔除誤配點
Mat homo = findHomography(imagePoints1, imagePoints2, RANSAC);
//也可以使用getPerspectiveTransform方法獲得透視變換矩陣,不過要求隻能有4個點,效果稍差
//Mat homo=getPerspectiveTransform(imagePoints1,imagePoints2);
cout << "變換矩陣為:\n" << homo << endl << endl; //輸出映射矩陣
//計算配準圖的四個頂點坐标
CalcCorners(homo, a);
cout << "left_top:" << corners.left_top << endl;
cout << "left_bottom:" << corners.left_bottom << endl;
cout << "right_top:" << corners.right_top << endl;
cout << "right_bottom:" << corners.right_bottom << endl;
//圖像配準
Mat imageTransform1, imageTransform2;
warpPerspective(a, imageTransform1, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), b.rows));
//warpPerspective(a, imageTransform2, adjustMat*homo, Size(b.cols*1.3, b.rows*1.8));
imshow("orb_trans", imageTransform1);
imwrite("orb_trans.jpg", imageTransform1);
//建立拼接後的圖,需提前計算圖的大小
int dst_width = imageTransform1.cols; //取最右點的長度為拼接圖的長度
int dst_height = b.rows;
Mat dst(dst_height, dst_width, CV_8UC3);
dst.setTo(0);
imageTransform1.copyTo(dst(Rect(0, 0, imageTransform1.cols, imageTransform1.rows)));
b.copyTo(dst(Rect(0, 0, b.cols, b.rows)));
imshow("surf_result", dst);
OptimizeSeam(b, imageTransform1, dst);
imshow("opm_surf_result", dst);
imwrite("opm_surf_result.jpg", dst);
waitKey();
return 0;
}
//優化兩圖的連接配接處,使得拼接自然
void OptimizeSeam(Mat& img1, Mat& trans, Mat& dst)
{
int start = MIN(corners.left_top.x, corners.left_bottom.x);//開始位置,即重疊區域的左邊界
double processWidth = img1.cols - start;//重疊區域的寬度
int rows = dst.rows;
int cols = img1.cols; //注意,是列數*通道數
double alpha = 1; //img1中像素的權重
for (int i = 0; i < rows; i++)
{
uchar* p = img1.ptr<uchar>(i); //擷取第i行的首位址
uchar* t = trans.ptr<uchar>(i);
uchar* d = dst.ptr<uchar>(i);
for (int j = start; j < cols; j++)
{
//如果遇到圖像trans中無像素的黑點,則完全拷貝img1中的資料
if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
{
alpha = 1;
}
else
{
//img1中像素的權重,與目前處理點距重疊區域左邊界的距離成正比,實驗證明,這種方法确實好
alpha = (processWidth - (j - start)) / processWidth;
}
d[j * 3] = p[j * 3] * alpha + t[j * 3] * (1 - alpha);
d[j * 3 + 1] = p[j * 3 + 1] * alpha + t[j * 3 + 1] * (1 - alpha);
d[j * 3 + 2] = p[j * 3 + 2] * alpha + t[j * 3 + 2] * (1 - alpha);
}
}
}

處理後的圖檔如上,圖檔是用别人的樣本圖檔做的,自己采集時注意調整分辨率。做圖像拼接的時候突然想到可能要往雙目實時全景拼接上去做…增加難度。移植怎麼搞???想到用定時采樣的辦法,也覺得不穩妥。
代碼解析:
1、four_corners_t 這個結構:
這個結構是用來在後面進行圖像拼接之前,實作圖像的變換的時候使用的用來存放變換之後的圖像的四個角的坐标。後面會有。
2、SURF特征檢測和比對的使用:
Ptr<SURF> surf; //建立方式和OpenCV2中的不一樣,并且要加上命名空間xfreatures2d
//否則即使配置好了還是顯示SURF為未聲明的辨別符
surf = SURF::create(800);
BFMatcher matcher; //執行個體化一個暴力比對器
Mat c, d; //特征點描述矩陣
vector<KeyPoint> key1, key2; //特征點
vector<DMatch> matches; //DMatch是用來描述比對好的一對特征點的類,包含這兩個點之間的相關資訊
//比如左圖有個特征m,它和右圖的特征點n最比對,這個DMatch就記錄它倆最比對,并且還記錄m和n的
//特征向量的距離和其他資訊,這個距離在後面用來做篩選
//檢測和計算圖像的關鍵點和描述
surf->detectAndCompute(a, Mat(), key1, c);//輸入圖像,輸入掩碼用于屏蔽源圖像中的特定區域,輸入特征點矢量數組 ,存放所有特征點的描述向量
surf->detectAndCompute(b, Mat(), key2, d);//這個Mat行數為特征點的個數,列數為每個特征向量的尺寸,SURF是64(維)
定義一個surf,參數為門限值,調整這個可以調整檢測精度,越大越高,不過相應的速度也會慢;detectAndCompute函數實作了檢測特征點并計算特征描述矩陣存儲到c, d中。
3、接下來的比對類DMatch類:這個類存儲了圖像特征之間的比對的資訊:
CV_PROP_RW int queryIdx; // query descriptor index 查詢Index
CV_PROP_RW int trainIdx; // train descriptor index 訓練Index
CV_PROP_RW int imgIdx; // train image index label?
CV_PROP_RW float distance; //特征點之間的歐氏距離
對這個類還不是很了解,不過他這三個變量 queryIdx trainIdx distance還是比較重要的。distance不用說,兩個特征點之間的距離,trainIdx應該是在訓練分類器時輸入訓練的點的Index;queryIdx是在利用分類器做回歸的時候對應的Index (兩個不同的圖檔,一個用來訓練,一個用來做測試,訓練與測試(分類)的對應的特征點應該是對應的)。
根據歐氏距離選擇比對良好的點。
4、畫出比對結果
Mat outimg; //drawMatches這個函數直接畫出擺在一起的圖
drawMatches(b, key2, a, key1, good_matches, outimg, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS); //繪制比對點
imshow("combine", outimg);
5、提取到特征之後對2圖像進行變換,投影到圖像1下
首先在得到變換矩陣之前,先得到特征點的Point2f類型的坐标:
vector<Point2f> imagePoints1, imagePoints2;
for (int i = 0; i < good_matches.size(); i++)
{
imagePoints2.push_back(key2[good_matches[i].queryIdx].pt);
imagePoints1.push_back(key1[good_matches[i].trainIdx].pt);
}
6、
Mat homo = findHomography(imagePoints1, imagePoints2, RANSAC);//擷取圖像2到圖像1的投影映射矩陣 3*3
//也可以使用getPerspectiveTransform方法獲得透視變換矩陣,不過要求隻能有4個點,效果稍差
//Mat homo=getPerspectiveTransform(imagePoints1,imagePoints2);
cout << "變換矩陣為:\n" << homo << endl << endl; //輸出映射矩陣
//計算配準圖的四個頂點坐标并輸出
CalcCorners(homo, a);
cout << "left_top:" << corners.left_top << endl;
cout << "left_bottom:" << corners.left_bottom << endl;
cout << "right_top:" << corners.right_top << endl;
cout << "right_bottom:" << corners.right_bottom << endl;
//圖像配準
Mat imageTransform1, imageTransform2;
warpPerspective(a, imageTransform1, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), b.rows));
//圖像配準 warpPerspective 對圖像進行透視變換 變換後矩陣的寬高都變化
//warpPerspective(a, imageTransform2, adjustMat*homo, Size(b.cols*1.3, b.rows*1.8));
imshow("orb_trans", imageTransform1);
imwrite("orb_trans.jpg", imageTransform1);
其中,calCorners利用的是齊次坐标系計算坐标面比較友善。warpPerspective是OpenCV自帶的透視變換函數。
7、變換之後進行圖像的複制,構成新的圖檔:
//建立拼接後的圖,需提前計算圖的大小
int dst_width = imageTransform1.cols; //取最右點的長度為拼接圖的長度
int dst_height = b.rows;
Mat dst(dst_height, dst_width, CV_8UC3);
dst.setTo(0);
//構成圖檔
//複制img2到dist的右半部分 先複制transform2的圖檔(因為這個尺寸比較大,後來的圖檔可以覆寫到他)
imageTransform1.copyTo(dst(Rect(0, 0, imageTransform1.cols, imageTransform1.rows)));
b.copyTo(dst(Rect(0, 0, b.cols, b.rows)));
copyTo函數在不使用Mask參數時複制的話,将圖檔黑色部分忽略,僅複制有顔色的部分。也就是黑色會被替換掉。是以要先複制imgTransform2,這個裡面會因為變換而産生許多黑色的部分,然後再複制img1(也就是在左邊 ,沒有變換的圖像)過去覆寫掉黑色。反過來的話黑色會把它覆寫掉。
8、優化連接配接處
optimizeSeam函數來優化拼接,思想大概是alpha參數根據2圖檔(變化的圖檔,右側的圖檔)與1重疊的位置來設定值,在重疊部分的值是由兩個圖檔的像素值α權重得到的。注意下面的*3,因為RGB。
//優化兩圖的連接配接處,使得拼接自然
void OptimizeSeam(Mat& img1, Mat& trans, Mat& dst)
{
int start = MIN(corners.left_top.x, corners.left_bottom.x);//開始位置,即重疊區域的左邊界
double processWidth = img1.cols - start;//重疊區域的寬度
int rows = dst.rows;
int cols = img1.cols; //注意,是列數*通道數
double alpha = 1; //img1中像素的權重
for (int i = 0; i < rows; i++)
{
uchar* p = img1.ptr<uchar>(i); //擷取第i行的首位址
uchar* t = trans.ptr<uchar>(i);
uchar* d = dst.ptr<uchar>(i);
for (int j = start; j < cols; j++)
{
//如果遇到圖像trans中無像素的黑點,則完全拷貝img1中的資料
if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
{
alpha = 1;
}
else
{
//img1中像素的權重,與目前處理點距重疊區域左邊界的距離成正比,實驗證明,這種方法确實好
alpha = (processWidth - (j - start)) / processWidth;
}
d[j * 3] = p[j * 3] * alpha + t[j * 3] * (1 - alpha);
d[j * 3 + 1] = p[j * 3 + 1] * alpha + t[j * 3 + 1] * (1 - alpha);
d[j * 3 + 2] = p[j * 3 + 2] * alpha + t[j * 3 + 2] * (1 - alpha);
}
}
}
參考下面的博文比較多,要消化掉。
參考:https://blog.csdn.net/Architet_Yang/article/details/81274571