本博客内容来源于网络以及其他书籍,结合自己学习的心得进行重编辑,因为看了很多文章不便一一标注引用,如图片文字等侵权,请告知删除。
之前几篇文章都介绍的关于视觉中的基础的算法概念,今天这篇文章我们来学习一下一个更有意思,能够实际应用的算法-- 全景拼接。可能文章中有一些方法,我们会从其他文章详解,所以没有进行详细描述。主要还是理解全景拼接的流程,对一些基础的概念有了解。所以我么开始。
全景拼接简介
全景拼接就是将多张图片进行缝合,生成一张视野更大的全景图。全景并不只是指环绕一周的图片,而是我们通过图像拼接技术创建更宽更高的场景。
现在像360度的全景相机已经很普遍了,价格也有很大的下降,比如insta360等。这一类的全景相机基本上都是由两个鱼眼相机拼接而成,也有一些是通过多个广角相机拼接而成的。大家每个人都应该用过手机上的全景拍摄,通过移动相机,完成拍摄一张视角更宽广的图片,这里也用到全景拼接技术。图像拼接已经有很多的落地场景,但是仍旧有一些问题还有很多学者在优化。
全景拼接一般流程
下面只是对基础的全景拼接整理的流程,还有很多算法有很多流程来优化提高最后的结果。
- 对图片畸变进行矫正。
- 对图片提取特征点,对特征进行匹配,得到输入图像之间的映射关系T。
- 图像拼接,根据映射关系T进行图像的Warp变换,对齐图像。
- 图像融合,利用颜色调整来消除图像间的色差等方式来消除拼缝,多张图片融合为一张
- 定义全景映射模型,常用的包括:球面、柱面、平面,其中球面映射应用最为广泛。
上面的流程,肯定有很多概念不知道什么意思,下面将对其进行解释。
全景拼接流程中关键技术点详解
-
图像矫正
一般的全景相机会使用两颗或多颗广角或者鱼眼相机来节省成本。广角或者鱼眼相机的畸变都比较大,所以需要通过提前标定好的参数,对图片进行去畸变矫正。如果是正常的畸变较小的小孔相机则基本不需要。
-
图片匹配
所谓图片匹配就是找到相邻图片中相对应的点。主要有两种方式:
- 与特征无关的匹配方式,一般都用于没有复杂变换的图像拼接情况下,常见的为灰度相关性匹配,这种方法计算简单,仅仅通过灰度模板匹配。
- 根于特征进行匹配,常见特征有特征曲线,特征轮廓,特征点,特征点使用较多。分别在图像中找到相应的特征描述点,比如sift,surf,orb等,然后根据描述子,通过描述子的相似性来匹配两种图像中的点,找到对应匹配的点对。针对特征匹配具体讲解会在其他文章中讲解。
-
warp变换
在上一步我们找到了许多对应的点对,通过这些点对来估算"单应矩阵",然后通过单应矩阵将待匹配的图片转换到原始图片的平面。
我们来慢慢解释上面的疑问,首先现在我们来看看什么是单应矩阵?
单应性矩阵为一3x3矩阵,描述了射影几何中平面到平面的映射关系,其自由度为8,由九个元素组成,通常令最后一个元素为1或者使其F范数为1,该矩阵可将无穷远点投射于有限处,即空间中平行线在图像上相交于有限处。
上图中描述将image2平面中的点投影到image1中的点的变换矩阵即单应矩阵。上图两个投影中心在一点,但单应性矩阵可以表达出image2平面投影到image1平面的旋转和平移。
现在我们知道单应矩阵是干什么用的,所以也可以将待匹配的图投影到原始图片的平面上了。现在需要知道的就是怎么来求单应矩阵?
为了提高单应矩阵的正确性,要提高匹配准确率,一般可以配合上RANSAC算法的方式进行估计,ransac主要帮助筛选出匹配准确性更高的点对。
所以我们至少需要四对点对就可以算出单应矩阵。
-
图像融合
在拼接完成后,如果两幅图像因拍摄环境的不同,则会产生明显的的过渡区域(可以观察下面的实验结果),此时需要利用图像融合,将重叠区域的像素按两幅图像权重相加,使其能形成缓慢的过度。
常用的方式有:基于金字塔的图像融合,根据距重叠区边缘的距离来设定对应的权重,或者是直接将两幅图的重叠区按某个比例权重相加。这是一种比较基础简单的方式,还有更多优秀的方式暂不介绍。
-
投影映射
对于360度的图片,可以说是一个全包围的图片,我们要把这个图片展开就需要投影映射,就像我们的世界地图一样,可以把平面的地图转换到到地球仪上进行观看。
映射模型可以看作是用于图像映射的载体,相当于二维图像映射到三维空间的一种变换。
全景拼接中的难点
- 当相邻照片重叠部分过少时,难以匹配
- 当图片中有多处相似部分,难以消除错误匹配
- 图片之间存在视差以及匹配误差,拼缝处有时难以达到光滑过度且不变形。
- 不同图片的亮度对比度等之间的差异,使图像融合不自然
- 图片在拼接处有动态的物体,无法剔除
OpenCV 全景拼接效果展示[代码]
代码较多,直接从主函数步骤比较简单来分析。
#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>
#include <boost/filesystem.hpp>
#include <opencv2/xfeatures2d.hpp>
std::vector<cv::Mat> process_input_images(std::vector<cv::Mat> input){
std::vector<cv::Mat> gray_images;
for(int i=0 ;i < input.size() ;i++){
cv::Mat gray_image;
cvtColor(input[i], gray_image, CV_RGB2GRAY);
gray_images.push_back(gray_image);
}
return gray_images;
}
std::vector<std::string> get_all_image_file(std::string image_folder_path){
boost::filesystem::path dirpath = image_folder_path;
boost::filesystem::directory_iterator end;
std::vector<std::string> files;
for (boost::filesystem::directory_iterator iter(dirpath); iter != end; iter++)
{
boost::filesystem::path p = *iter;
files.push_back(dirpath.string()+ "/"+ p.leaf().string());
}
std::sort(files.begin(),files.end());
return files;
}
// 从文件夹中读取图片
std::vector<cv::Mat> read_input_images(std::string image_folder_path)
{
std::vector<cv::Mat> images;
std::vector<std::string> image_files_path = get_all_image_file(image_folder_path);
for(int i=0; i< image_files_path.size() ;i++){
cv::Mat image;
image = cv::imread(image_files_path[i]);
images.push_back(image);
}
return images;
}
// 提取特征点
std::vector< std::vector<cv::KeyPoint> > extracte_features(std::vector<cv::Mat> images){
std::vector< std::vector<cv::KeyPoint> > keypoints;
for(int i=0 ;i < images.size() ;i ++){
std::vector<cv::KeyPoint> keypoint;
cv::Ptr<cv::Feature2D> f2d = cv::xfeatures2d::SIFT::create();
f2d->detect(images[i],keypoint);
cv::Mat image_with_kp;
cv::drawKeypoints(images[i], keypoint, image_with_kp, cv::Scalar::all(-1));
cv::imwrite("image_with_kp"+std::to_string(i)+".png",image_with_kp);
keypoints.push_back(keypoint);
}
return keypoints;
}
// 计算描述子
std::vector<cv::Mat> extracte_descriptors(std::vector<cv::Mat> images,std::vector< std::vector<cv::KeyPoint> >keypoints){
std::vector< cv::Mat > descriptors;
for(int i=0 ;i < images.size() ;i ++){
cv::Mat descriptor;
cv::Ptr<cv::Feature2D> f2d = cv::xfeatures2d::SIFT::create();
f2d->compute(images[i],keypoints[i],descriptor);
descriptors.push_back(descriptor);
}
return descriptors;
}
// 匹配计算出两张图片的变换矩阵
cv::Mat find_transfer(std::vector<cv::Mat> images,std::vector< std::vector<cv::KeyPoint> >keypoints,std::vector< cv::Mat > descriptors){
cv::FlannBasedMatcher matcher;
std::vector<cv::DMatch> matches, goodmatches;
matcher.match(descriptors[0], descriptors[1], matches);
cv::Mat firstmatches;
cv::drawMatches(images[0], keypoints[0], images[1], keypoints[1],
matches, firstmatches, cv::Scalar::all(-1), cv::Scalar::all(-1),
std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
cv::imwrite("firstmatches.png",firstmatches);
double max_dist = 0; double min_dist = 1000;
for (int i = 0; i < descriptors[0].rows; i++) {
if (matches[i].distance > max_dist) {
max_dist = matches[i].distance;
}
if (matches[i].distance < min_dist) {
min_dist = matches[i].distance;
}
}
for (int i = 0; i < descriptors[0].rows; i++) {
if (matches[i].distance < 1.5 * min_dist) {
goodmatches.push_back(matches[i]);
}
}
cv::Mat img_matches;
cv::drawMatches(images[0], keypoints[0], images[1], keypoints[1],
goodmatches, img_matches, cv::Scalar::all(-1), cv::Scalar::all(-1),
std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
cv::imwrite("img_matches.png",img_matches);
std::vector<cv::Point2f> keypoints1, keypoints2;
for (int i = 0; i < goodmatches.size(); i++) {
keypoints1.push_back(keypoints[0][goodmatches[i].queryIdx].pt);
keypoints2.push_back(keypoints[1][goodmatches[i].trainIdx].pt);
}
cv::Mat trans = findHomography(keypoints2, keypoints1, CV_RANSAC);
return trans;
}
// 拼接出融合优化
void OptimizeSeam(cv::Mat& img1, cv::Mat& trans, cv::Mat& dst,std::vector<cv::Point2f> corners)
{
int start = MIN(corners[0].x, corners[1].x);
double processWidth = img1.cols - start;
int rows = dst.rows;
int cols = img1.cols;
double alpha = 1;
for (int i = 0; i < rows; i++)
{
uchar* p = img1.ptr<uchar>(i);
uchar* t = trans.ptr<uchar>(i);
uchar* d = dst.ptr<uchar>(i);
for (int j = start; j < cols; j++)
{
if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
{
alpha = 1;
}
else
{
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);
}
}
}
// 拼接融合图片
cv::Mat warp_image(std::vector<cv::Mat> images,cv::Mat trans,std::vector<cv::Point2f> corners){
cv::Mat image_right_perspective ;
cv::warpPerspective(images[1], image_right_perspective, trans, cv::Size(MAX(corners[2].x, corners[3].x), images[0].rows));
cv::imwrite("image_right_perspective.png",image_right_perspective);
int dst_width = image_right_perspective.cols;
int dst_height = images[0].rows;
cv::Mat dst(dst_height, dst_width, CV_8UC3);
dst.setTo(0);
image_right_perspective.copyTo(dst(cv::Rect(0, 0, image_right_perspective.cols, image_right_perspective.rows)));
images[0].copyTo(dst(cv::Rect(0, 0, images[0].cols, images[0].rows)));
cv::imwrite("before_opt.png",dst);
OptimizeSeam(images[0],image_right_perspective,dst,corners);
cv::imwrite("reult.png",dst);
return dst;
}
// 就算变换后图片的四个角
std::vector<cv::Point2f> cal_corner(std::vector<cv::Mat> images,cv::Mat trans){
double v2[] = { 0, 0, 1 };
double v1[3];
std::vector<cv::Point2f> corners;
cv::Point2f left_top,left_bottom,right_top,right_bottom;
cv::Mat V2 = cv::Mat(3, 1, CV_64FC1, v2);
cv::Mat V1 = cv::Mat(3, 1, CV_64FC1, v1);
V1 = trans * V2;
left_top.x = v1[0] / v1[2];
left_top.y = v1[1] / v1[2];
v2[0] = 0;
v2[1] = images[1].rows;
v2[2] = 1;
V2 = cv::Mat(3, 1, CV_64FC1, v2);
V1 = cv::Mat(3, 1, CV_64FC1, v1);
V1 = trans * V2;
left_bottom.x = v1[0] / v1[2];
left_bottom.y = v1[1] / v1[2];
v2[0] = images[1].cols;
v2[1] = 0;
v2[2] = 1;
V2 = cv::Mat(3, 1, CV_64FC1, v2);
V1 = cv::Mat(3, 1, CV_64FC1, v1);
V1 = trans * V2;
right_top.x = v1[0] / v1[2];
right_top.y = v1[1] / v1[2];
v2[0] = images[1].cols;
v2[1] = images[1].rows;
v2[2] = 1;
V2 = cv::Mat(3, 1, CV_64FC1, v2);
V1 = cv::Mat(3, 1, CV_64FC1, v1);
V1 = trans * V2;
right_bottom.x = v1[0] / v1[2];
right_bottom.y = v1[1] / v1[2];
corners.push_back(left_top);
corners.push_back(left_bottom);
corners.push_back(right_top);
corners.push_back(right_bottom);
return corners;
}
int main(int argc, char *argv[])
{
std::vector<cv::Mat> input_images = read_input_images(argv[1]);
// 两张图片拼接按步演示
{
std::vector<cv::Mat> two_input_images;
two_input_images.push_back(input_images[0]);
two_input_images.push_back(input_images[1]);
std::vector<cv::Mat> gray_images = process_input_images(two_input_images);
std::vector< std::vector<cv::KeyPoint> > keypoints = extracte_features(gray_images);
std::vector<cv::Mat> descriptors = extracte_descriptors(gray_images,keypoints);
cv::Mat trans = find_transfer(two_input_images,keypoints,descriptors);
std::vector<cv::Point2f> corners =cal_corner(two_input_images,trans);
cv::Mat after_warp = warp_image(two_input_images,trans,corners);
}
// opencv自带函数对多张图片进行拼接
{
cv::Stitcher stitch_all = cv::Stitcher::createDefault(true);
cv::Mat pano;
cv::Stitcher::Status status = stitch_all.stitch(input_images, pano);
cv::imwrite("pano_all.png",pano);
}
}
实验数据就是我在屋子外面拍的几张照片(都上传的时候都做了resize)
图片1 | 图片2 | 图片3 |
---|---|---|
图片4 | 图片5 | -- |
两张拼接处理选择的第一张与第二张图片
图片1提取关键点 | 图片2提取关键点 |
---|---|
配准效果(貌似看不清)
初次匹配 | 过滤后 |
---|---|
总结
对于简单的版本的拼接算法,基本上还是免不了之前说的困难点,比如拼接处有重影,参数不好,容易匹配失误等。
重要的事情说三遍:
如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )
如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )
如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )
任何人或团体、机构全部转载或者部分转载、摘录,请保留本博客链接或标注来源。博客地址:开飞机的乔巴
作者简介:开飞机的乔巴(WeChat:zhangzheng-thu),现主要从事机器人抓取视觉系统以及三维重建等3D视觉相关方面,另外对slam以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。
网友评论