最初我看特征点匹配的东西源于三维重建,由于特征点匹配的不准确,导致两幅图像之间的位置关系计算不准确,从而使得最后生成的三维点云中有很多的噪声。看特征点匹配大概看了一个半月,把已有的除了最新的基于深度学习特征点匹配的方法都看了一个遍。后来三维重建没有再继续做下去,跟老师要了两周的时间想用特征点匹配试试图像拼接。
由于两周时间有点短,而且中间我的电脑还崩过三四天,最终也没有把图像拼接完全做通,我先把当前做好的记录下来,其余的再留个以后慢慢改善吧。
已做到的:多张有序图像之间的拼接、两幅图像交界处渐入渐出融合、图像的柱形投影;
未做到的:不同色调图像之间的拼接效果不理想、拼接处有虚影、最终图像的拉直。
好了,下面来简单介绍一下图像拼接的原理。
图像拼接在图像处理领域是一个比较成熟的方向了,目前都没有关于图像拼接的新论文出现了,他的主要组成分为两部分:(1)特征点匹配,确定两幅图像之间的位置关系;(2)把所有图像投影变换到同一坐标系,并完成对接与融合。下面来分别做简单的介绍。
特征点匹配我就不多讲了,我的博文中有一个分类是讲比较经典的特征点匹配的方法的:特征点匹配 - lhanchao的博客
特征点匹配后,我们得到了两幅图像中相互匹配的特征点对,以及每个特征点对应的特征点描述符。然而我们得到特征点对中会有一部分是误匹配点,因此我们需要进行匹配点对的消除,一般我们使用的RANSAC去除误匹配点对,同样有博文介绍特征点匹配——使用基础矩阵、单应性矩阵的RANSAC算法去除误匹配点对,简单的来说,就是通过不断优化两幅图像之间的位置关系来验证特征点匹配点对是否正确。
从上面的描述中,我们可以发现一个很好的东西,就是我们用来去除误匹配的特征点对的矩阵,就是我们最终需要计算的两幅图像之间的位置关系,一石两鸟有没有~
注意,这里的矩阵指的是RANSAC博文中的单应性矩阵。我们在这里有个假设,即两幅图像之间是符合透视变换的,可以用如下方式表达:
得到透视矩阵以后,就可以把一副图像投影到另一幅图像的坐标系之中了,但是如果把原图投影过去的话,我看过实验结果,一般会产生比较大的形变,效果不是很好。这里再介绍一种方法,先对两幅原图像进行柱形投影变换的预处理,然后再计算透视矩阵进行拼接。对于柱形投影的介绍,可以看这个人的博客柱面投影解析。
进行拼接其实非常简单,主要把两幅图像变换到同一坐标系中,然后直接把图像贴上去即可。但是麻烦的是图像的融合,因为不同的图片之间,往往亮度信息不一致,如果只是进行拼接的话就会出现一条很明显的拼接缝隙,一般去除这条缝隙的方法是最大值法、渐进渐出法。
最大值法是比较两个像素点的值,哪个大最终拼接的图像就是哪个,简单粗暴;渐进渐出法是越靠近拼接边缘时,待拼接图像像素点的权值越大,拼接图像的像素值得权值越小,最终结果取加权和。
这里简单介绍一下我在实现的过程中遇到的一些坑,希望以后有人不再跳。
1、我在进行特征点匹配时使用的是ORB算法,具有比常用的SIFT速度更快的优点关于ORB算法的介绍见博文特征点匹配——ORB算法介绍。ORB算法使用的特征点检测方法是FAST算法,有一个缺点就是得到的特征点往往聚集成一团,这样即使要求检测很多特征点,往往也全部集中在同一个区域,而如果该区域正好不是两幅图像中共有的区域,那么匹配结果就惨不忍睹。
如下图:
SIFT就不会这样,得到的特征点很平均的分布,效果很好。由于这个原因,我在检测特征点时用到了一个先验知识,即我所检测的区域是在离另一幅图像比较近的区域,如图1和图2拼接时只检测图1的右2/3区域,图2的左2/3区域,这样可以避免特征点检测在两幅图像不共有的区域的情况。
这就用到了OpenCV中mask,例子如下:
void test()
{
Mat img1 = imread("1.jpg");
//set mask
Mat mask = Mat::zeros(Size(img1.size()), CV_8UC1);
mask(Rect(0, 0, mask.cols / 2, mask.rows / 2)).setTo(255);
//detect 500 keypoints
ORB orb(500);
vector kp;
orb.detect(img1, kp, mask);
//draw keypoints in img1
for (size_t i = 0; i < kp.size(); i++)
{
circle(img1, Point(kp[i].pt.x, kp[i].pt.y), 2, Scalar(0, 0, 255), 2);
}
imwrite("1_1.jpg", img1);
}
这样,就只在图像的左侧进行了特征点检测。
2、OpenCV中的findHomgrophy函数中得到的透视矩阵是img1到img2的投影矩阵,即findHomography(image1Points, image2Points, CV_RANSAC, 2.5f, inlier_mask);
得到的是图像1到图像2的变换矩阵,即是以图像2的坐标系为参考坐标系的,不要弄混了。另外如果是这样,得到的透视矩阵中水平平移的量,即前文中的 m2 一般情况下都是负的,也就是说图像1中的部分区域不会出现在拼接图像中,解决这个问题不能强制把 m2 变成0或者变成正值,这样会造成图像1的进一步形变。我解决这个问题的办法是先把图像2向左移动一定距离,从而使得透视矩阵的 m2 变大。
3、在进行拼接时,如果全部是从左到右进行拼接的话,后面的图像会有较大的形变,因此比较可靠的方法是从中间向两边扩散的方法,这样形变量会减少。
目前就想到了这么多,虽然讲起来很简单,但是实现的过程中总是或多或少的出现问题,细节上的处理比较麻烦,如果只是想要一个差不多的结果,可以向我这样试试。代码就不贴了,有兴趣的话可以联系我。
最后放一个结果图
——————————————2017-01-10更新——————————————
好多人私信和评论跟我要代码,之前觉得代码写的很不规范,所以也一直没有贴出来,最近有点忙(主要是懒),也一直没有整理,我这里先把之前写的核心代码放出来吧,代码和注释都不太规范,等以后有空了再把所有的都贴出来,这里我用的opencv版本是Opencv2.4.12,应当Opencv2以上的版本都可以,Opencv3.0可能会有一些问题,因为Opencv3中对features2d好像有改动。
//ORBAlgorithm.h文件,主要是一些函数的声明
#pragma once
#include
#include
#include
#include
#include
class ORBAlgorithm
{
public:
ORBAlgorithm();
~ORBAlgorithm();
//detect feature points
void detectPoints(const cv::Mat &img1, const cv::Mat& mask1, const cv::Mat &img2, const cv::Mat& mask2,
std::vector & kp1, std::vector & kp2, cv::Mat& des1, cv::Mat& des2);
//match feature points and return the match points pairs
void matchPoints(const std::vector & kp1, const std::vector & kp2,
const cv::Mat& des1, const cv::Mat& des2,std::vector & matchPairs);
//use RANSAC to get the homography mat
void getHomographyMat(const std::vector & kp1, const std::vector & kp2,
std::vector & good_matches, cv::Mat& homography);
void getHomographyMat(const cv::Mat& img1, const cv::Mat& img2, const std::vector & kp1,
const std::vector & kp2, std::vector & good_matches, cv::Mat& homography);
static bool sortByDistance(const cv::DMatch& match1, const cv::DMatch& match2)
{
return match1.distance < match2.distance;
}
};
//ORBAlgorithm.cpp文件,对ORBAlgorithm.h声明的函数的实现
#include "ORBAlgorithm.h"
using namespace cv;
using std::vector;
ORBAlgorithm::ORBAlgorithm()
{
}
ORBAlgorithm::~ORBAlgorithm()
{
}
//detect featrue points from two images
//img1 and img2 are two images to match
//kp1 and kp2 are two points list storing the feature points
//des1 and des2 are two mats storing the descriptors of the feature points
void ORBAlgorithm::detectPoints(const Mat &img1, const Mat& mask1, const Mat &img2, const Mat& mask2,
vector & kp1, vector & kp2, Mat& des1, Mat& des2)
{
//detect 4000 keypoints, the first image scale is 1.2 and detect 2 scale images
ORB orb(4000,1.2);
//SIFT sift;
int64 time1 = getTickCount(), time2 = 0, time3 = 0, time4 = 0;
orb(img1, mask1, kp1, des1);
//sift(img1, Mat(), kp1, des1);
time2 = getTickCount();
double cost1 = 1000.0 * (time2 - time1) / getTickFrequency();
time3 = getTickCount();
orb(img2, mask2, kp2, des2);
//sift(img2, Mat(), kp2, des2);
time4 = getTickCount();
double cost2 = 1000.0*(time4 - time3) / getTickFrequency();
}
//match feature points using descriptors and return the match point pairs
void ORBAlgorithm::matchPoints(const vector & kp1, const vector & kp2,
const Mat& des1, const Mat& des2,vector & goodMatches)
{
Ptr matcher = DescriptorMatcher::create("BruteForce-Hamming");
//Ptr matcher = DescriptorMatcher::create("BruteForce");
vector matches1to2;
vector matches2to1;
vector twoDirectionMatch;
//match
matcher->match(des1, des2, matches1to2);
matcher->match(des2, des1, matches2to1);
//get the intersection of match image1 to image2 and match image2 to image1
int *flag = new int[des2.rows];
memset(flag, -1, sizeof(int)*des2.rows);
for (size_t i = 0; i < des2.rows; i++)
{
flag[matches2to1[i].queryIdx] = matches2to1[i].trainIdx;
}
for (size_t i = 0; i < matches1to2.size(); i++)
{
if (flag[matches1to2[i].trainIdx] == matches1to2[i].queryIdx)
twoDirectionMatch.push_back(matches1to2[i]);
}
//get the best 100 matches
sort(twoDirectionMatch.begin(), twoDirectionMatch.end(), sortByDistance);
size_t loop_time = twoDirectionMatch.size();
if (loop_time >= 500)
loop_time = 500;
for (size_t i = 0; i < loop_time; i++)
{
goodMatches.push_back(twoDirectionMatch[i]);
}
}
//get homography mat using RANSAC
void ORBAlgorithm::getHomographyMat(const vector & kp1, const vector & kp2,
vector & goodMatches, Mat& homography)
{
vector image1Points;
vector image2Points;
for (size_t i = 0; i < goodMatches.size(); i++)
{
image1Points.push_back(kp1[goodMatches[i].queryIdx].pt);
image2Points.push_back(kp2[goodMatches[i].trainIdx].pt);
}
Mat inlier_mask;
homography = findHomography(image2Points, image1Points, CV_RANSAC, 2.5f, inlier_mask);
std::cout << homography << std::endl;
int i = 0;
for (vector ::iterator iter = goodMatches.begin(); iter != goodMatches.end();)
{
if (!inlier_mask.at(i))
{
iter = goodMatches.erase(iter);
}
else
iter++;
i++;
}
}
void ORBAlgorithm::getHomographyMat(const Mat& img1, const Mat& img2, const vector & kp1,
const vector & kp2, vector & goodMatches, Mat& homography)
{
vector image1Points;
vector image2Points;
for (size_t i = 0; i < goodMatches.size(); i++)
{
image1Points.push_back(kp1[goodMatches[i].queryIdx].pt);
image2Points.push_back(kp2[goodMatches[i].trainIdx].pt);
}
Mat inlier_mask;
homography = findHomography(image2Points, image1Points, CV_RANSAC, 2.5f, inlier_mask);
std::cout << homography << std::endl;
int i = 0;
for (vector ::iterator iter = goodMatches.begin(); iter != goodMatches.end();)
{
if (!inlier_mask.at(i))
{
iter = goodMatches.erase(iter);
}
else
iter++;
i++;
}
std::cout << "goodMatche num: " << goodMatches.size() << std::endl;
Mat matchImage;
drawMatches(img1, kp1, img2, kp2, goodMatches, matchImage, Scalar(0, 255, 0),Scalar(0,0,255));
imwrite("picture\\matchImage.jpg", matchImage);
}
//图像拼接核心函数代码
//caclulate the homography mat of 2 images
void ImageStitching::getHomomgraphy(const Mat& src1, const Mat& mask1, const Mat& src2, const Mat& mask2, Mat& homography)
{
//自己定义的类,实现ORB特征点提取和匹配算法,以及计算图像变换矩阵
ORBAlgorithm test;
//keypoints
vector kp1, kp2;
//descriptors
Mat des1, des2;
//points matches
vector matchPairs;
std::cout << "detecting keypoints..." << std::endl;
test.detectPoints(src1, mask1, src2, mask2, kp1, kp2, des1, des2);
std::cout << "matching images..." << std::endl;
test.matchPoints(kp1, kp2, des1, des2, matchPairs);
std::cout << "computing the homography mat..." << std::endl;
test.getHomographyMat(src1, src2, kp1, kp2, matchPairs, homography);
}
//image cylinder projection
void ImageStitching::projection(const Mat& src, Mat& dst)
{
int width = src.cols;
int height = src.rows;
int centerX = width / 2;
int centerY = height / 2;
dst = src.clone();
double f = width / (2 * tan(PI / 5 / 2));
double theta, pointX, pointY;
for (int i = 0; i < height; i++)
{
uchar* ptr = dst.ptr(i);
int k = 0;
for (int j = 0; j < width; j++)
{
theta = asin((j - centerX) / f);
pointY = f*tan((j - centerX) / f) + centerX;
pointX = (i - centerY) / cos(theta) + centerY;
if (pointX >= 0 && pointX <= height && pointY >= 0 && pointY <= width)
{
const uchar *tmp = src.ptr(pointX);
ptr[k] = tmp[(int)pointY * 3];
ptr[k + 1] = tmp[(int)pointY * 3 + 1];
ptr[k + 2] = tmp[(int)pointY * 3 + 2];
}
else
{
ptr[k] = 0;
ptr[k + 1] = 0;
ptr[k + 2] = 0;
}
k += 3;
}
}
}
//stitch images from center. srcs are original images, dst is the result
//s_num is the given start num, if s_num is 0 then start from the left
void ImageStitching::stitchImagesFromCenter(vector & srcs, Mat& dst, int s_num)
{
if (s_num == 0)
{
stitchImages(srcs, dst);
return;
}
//start image after cylinder projection
Mat p_src1;
projection(srcs[s_num], p_src1);
//stitch from right to left
for (int i = s_num - 1; i >= 0; i--)
{
int extraCol = p_src1.cols/2;
Mat tp_src1 = Mat::zeros(p_src1.rows, p_src1.cols + extraCol, p_src1.type());
Mat tROI = tp_src1(Rect(extraCol, 0, p_src1.cols, p_src1.rows));
p_src1.convertTo(tROI, tROI.type());
//the left image after cylinder projection
//projection this image to the right image's cordinate using warp perspective
Mat p_src2;
projection(srcs[i], p_src2);
//homography mat
Mat H;
if (i == s_num - 1)
{
Mat mask = Mat::zeros(tp_src1.size(), CV_8UC1);
mask(Rect(extraCol, 0, p_src1.cols, tp_src1.rows)).setTo(255);
getHomomgraphy(tp_src1, mask, p_src2, Mat(), H);
}
else
{
Mat mask1 = Mat::zeros(tp_src1.size(), CV_8UC1);
mask1(Rect(extraCol, 0, p_src1.cols*(1.0 / (s_num - i)), p_src1.rows)).setTo(255);
Mat mask2 = Mat::zeros(p_src2.size(), CV_8UC1);
mask2(Rect(Point2f(p_src2.cols / 3, 0), Point2f(p_src2.cols - 1, p_src2.rows - 1))).setTo(255);
getHomomgraphy(tp_src1, mask1, p_src2, mask2, H);
}
Mat tmp;
warpPerspective(p_src2, tmp, H, Size(tp_src1.cols, tp_src1.rows));
for (int m = 0; m < tmp.rows; m++)
{
uchar* p_tp_src1 = tp_src1.ptr(m);
uchar* p_tmp = tmp.ptr(m);
for (int n = 0; n < tmp.cols * 3; n += 3)
{
if (p_tp_src1[n] || p_tp_src1[n + 1] || p_tp_src1[n + 2])
{
if (p_tmp[n] || p_tmp[n+1] || p_tmp[n+2])
{
int dis = n / 3 - extraCol;
if (dis < 500 && dis > 0)
{
double weight = 1.0 * dis / 500;
p_tmp[n] = p_tp_src1[n] * weight + p_tmp[n] * (1 - weight);
p_tmp[n + 1] = p_tp_src1[n + 1] * weight + p_tmp[n + 1] * (1 - weight);
p_tmp[n + 2] = p_tp_src1[n + 2] * weight + p_tmp[n + 2] * (1 - weight);
}
else if (dis >= 0)
{
p_tmp[n] = p_tp_src1[n];
p_tmp[n + 1] = p_tp_src1[n + 1];
p_tmp[n + 2] = p_tp_src1[n + 2];
}
}
else
{
p_tmp[n] = p_tp_src1[n];
p_tmp[n + 1] = p_tp_src1[n + 1];
p_tmp[n + 2] = p_tp_src1[n + 2];
}
}
}
}
int maxNotZero = 0;
uchar* p_start = tmp.ptr(0);
uchar* p_middle = tmp.ptr(tmp.rows / 2);
uchar* p_bottom = tmp.ptr(tmp.rows - 1);
for (int m = 0; m < tmp.cols*3; m+=3)
{
bool b_start = p_start[m] || p_start[m + 1] || p_start[m + 2];
bool b_middle = p_middle[m] || p_middle[m + 1] || p_middle[m + 2];
bool b_bottom = p_bottom[m] || p_bottom[m + 1] || p_bottom[m + 2];
if (b_start && b_middle && b_bottom)
{
maxNotZero = m/3;
break;
}
}
Mat tmpROI = tmp(Rect(Point2f(maxNotZero, 0), Point2f(tmp.cols - 1, tmp.rows - 1)));
p_src1 = tmpROI;
}
for (size_t i = s_num + 1; i < srcs.size(); i++)
{
Mat p_src2;
projection(srcs[i], p_src2);
//去右方黑边
size_t m = p_src1.cols - 1;
Mat roiMat = p_src1(Rect(Point2f(0, 0), Point2f(m, p_src1.rows - 1)));
Mat H;
if (i != 1)
{
Mat mask1 = Mat::zeros(roiMat.size(), CV_8UC1);
mask1(Rect(Point2f(roiMat.cols*(1 - 1.0 / i), 0), Point2f(roiMat.cols - 1, roiMat.rows - 1))).setTo(255);
Mat mask2 = Mat::zeros(p_src2.size(), CV_8UC1);
mask2(Rect(0, 0, p_src2.cols * 2 / 3, p_src2.rows)).setTo(255);
getHomomgraphy(roiMat, mask1, p_src2, mask2, H);
}
else
{
getHomomgraphy(roiMat, Mat(), p_src2, Mat(), H);
}
Mat tmp;
warpPerspective(p_src2, tmp, H, Size(H.at<double>(0, 2) + p_src2.cols, p_src2.rows));
Mat p_src1ROI = tmp(Rect(0, 0, roiMat.cols, roiMat.rows));
for (size_t i = 0; i < p_src1ROI.rows; i++)
{
uchar* p_roiMat = roiMat.ptr(i);
uchar* p_tmp = tmp.ptr(i);
//double dis = roiMat.cols - H.at(0, 2);
for (size_t j = 0; j < p_src1ROI.cols * 3; j += 3)
{
if (p_roiMat[j] || p_roiMat[j + 1] || p_roiMat[j + 2])
{
if (p_tmp[j] || p_tmp[j + 1] || p_tmp[j + 2])
{
int dis = roiMat.cols - j / 3;
if (dis < 200 && dis > 0)
{
double weight = 1.0 * dis / 200;
p_tmp[j] = p_roiMat[j] * weight + p_tmp[j] * (1 - weight);
p_tmp[j + 1] = p_roiMat[j + 1] * weight + p_tmp[j + 1] * (1 - weight);
p_tmp[j + 2] = p_roiMat[j + 2] * weight + p_tmp[j + 2] * (1 - weight);
}
else if (dis > 0)
{
p_tmp[j] = p_roiMat[j];
p_tmp[j + 1] = p_roiMat[j + 1];
p_tmp[j + 2] = p_roiMat[j + 2];
}
}
else
{
p_tmp[j] = p_roiMat[j];
p_tmp[j + 1] = p_roiMat[j + 1];
p_tmp[j + 2] = p_roiMat[j + 2];
}
}
}
}
p_src1 = tmp;
}
dst = p_src1;
imwrite("dst.jpg", dst);
}
因为代码写的不太好,如果有问题欢迎评论里帮我指正,谢谢