之前已经写了基于SURF的图片融合,但是在实际项目中,更多的是实时视频/双摄像头的融合。这里采用柱面投影+模板匹配+渐入渐出融合的方法,分辨率是816×612。经测试加入柱面投影的效果不如未加入的。
//2021.9.15
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include<iostream>
using namespace cv;
using namespace std;
//柱面投影
Mat cylinder(Mat imgIn, int f);
//计算两幅图像之间的平移量(模板匹配,归一化相关性匹配法)
Point2i getOffset(Mat img, Mat img1);
//线性融合(渐入渐出融合法)
Mat linearFusion(Mat img, Mat img1, Point2i a);
int main()
{
string path1 = "C:/Users/dell/Desktop/SURF/video_1_1.mp4";//左图
string path2 = "C:/Users/dell/Desktop/SURF/video_3_1.mp4";//右图
VideoCapture cap1(path1);
VideoCapture cap2(path2);
//摄像头启动
/* VideoCapture cap1(0);
VideoCapture cap2(1);*/
double rate = 60;
int delay = 1000 / rate;
bool stop(false);
Mat frame1;
Mat frame2;
Mat frame;
Point2i a;//存储偏移量
int k = 0;
namedWindow("cam1", WINDOW_AUTOSIZE); //opencv4.2.0将CV_WINDOW_AUTOSIZE改为WINDOW_AUTOSIZE
namedWindow("cam2", WINDOW_AUTOSIZE);
namedWindow("stitch",WINDOW_AUTOSIZE);
if (cap1.isOpened() && cap2.isOpened())
{
cout << "*** ***" << endl;
cout << "摄像头已启动!" << endl;
}
else
{
cout << "*** ***" << endl;
cout << "警告:摄像头打开不成功或者未检测到有两个摄像头!" << endl;
cout << "程序结束!" << endl << "*** ***" << endl;
return -1;
}
//cap1.set(CV_CAP_PROP_FRAME_WIDTH, 640);
//cap1.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
//cap2.set(CV_CAP_PROP_FRAME_WIDTH, 640);
//cap2.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
cap1.set(CAP_PROP_FOCUS, 0); //opencv4.2.0将CV_CAP_PROP_FOCUS改为CAP_PROP_FOCUS;定义摄像头参数cap.set
cap2.set(CAP_PROP_FOCUS, 0);
while (!stop)
{
if (cap1.read(frame1) && cap2.read(frame2))
{
imshow("cam1", frame1);
imshow("cam2", frame2);
//彩色帧转灰度
cvtColor(frame1, frame1, COLOR_RGB2GRAY); //opencv4.2.0将CV_RGB2GRAY改为COLOR_RGB2GRAY
cvtColor(frame2, frame2, COLOR_RGB2GRAY);
//柱面投影变换
//frame1 = cylinder(frame1, 1005);
//frame2 = cylinder(frame2, 1005);
//匹配和拼接
/*视频拼接通过while循环实现,下面这个判断的意思是,有两
*种情形才计算平移参数,一是程序启动时,前3个循环内;二
*是按下回车键时。这样在场景和摄像头相对固定时,避免了平
*移量的重复计算,提高了拼接的实时性
*/
if (k < 3 || waitKey(delay) == 13)//按回车键
{
cout << "正在匹配..." << endl;
a = getOffset(frame1, frame2);
}
frame = linearFusion(frame1, frame2, a);
imshow("stitch", frame);
k++;
}
else
{
cout << "----------------------" << endl;
cout << "waitting..." << endl;
}
//按下ESC键,退出循环,程序结束
if (waitKey(1) == 27)
{
stop = true;
cout << "程序结束!" << endl;
cout << "*** ***" << endl;
}
}
return 0;
}
//计算平移参数
Point2i getOffset(Mat img, Mat img1)
{
Mat templ(img1, Rect(0, 0.4 * img1.rows, 0.2 * img1.cols, 0.2 * img1.rows));
Mat result(img.cols - templ.cols + 1, img.rows - templ.rows + 1, CV_8UC1);//result存放匹配位置信息
matchTemplate(img, templ, result, TM_CCORR_NORMED); //opencv4.2.0将CV_TM_CCORR_NORMED改为TM_CCORR_NORMED
normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());
double minVal; double maxVal; Point minLoc; Point maxLoc; Point matchLoc;
minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat());
matchLoc = maxLoc;//获得最佳匹配位置
int dx = matchLoc.x;
int dy = matchLoc.y - 0.4 * img1.rows;//右图像相对左图像的位移
Point2i a(dx, dy);
return a;
}
//线性(渐入渐出)融合
Mat linearFusion(Mat img, Mat img1, Point2i a)
{
int d = img.cols - a.x;//过渡区宽度
int ms = img.rows - abs(a.y);//拼接图行数
int ns = img.cols + a.x;//拼接图列数
Mat stitch = Mat::zeros(ms, ns, CV_8UC1);
//拼接
Mat_<uchar> ims(stitch);
Mat_<uchar> im(img);
Mat_<uchar> im1(img1);
if (a.y >= 0)
{
Mat roi1(stitch, Rect(0, 0, a.x, ms));
img(Range(a.y, img.rows), Range(0, a.x)).copyTo(roi1);
Mat roi2(stitch, Rect(img.cols, 0, a.x, ms));
img1(Range(0, ms), Range(d, img1.cols)).copyTo(roi2);
for (int i = 0; i < ms; i++)
for (int j = a.x; j < img.cols; j++)
ims(i, j) = uchar((img.cols - j) / float(d) * im(i + a.y, j) + (j - a.x) / float(d) * im1(i, j - a.x));
}
else
{
Mat roi1(stitch, Rect(0, 0, a.x, ms));
img(Range(0, ms), Range(0, a.x)).copyTo(roi1);
Mat roi2(stitch, Rect(img.cols, 0, a.x, ms));
img1(Range(-a.y, img.rows), Range(d, img1.cols)).copyTo(roi2);
for (int i = 0; i < ms; i++)
for (int j = a.x; j < img.cols; j++)
ims(i, j) = uchar((img.cols - j) / float(d) * im(i, j) + (j - a.x) / float(d) * im1(i + abs(a.y), j - a.x));
}
return stitch;
}
//柱面投影校正
Mat cylinder(Mat imgIn, int f)
{
int colNum, rowNum;
colNum = 2 * f * atan(0.5 * imgIn.cols / f);//柱面图像宽
rowNum = 0.5 * imgIn.rows * f / sqrt(pow(f, 2)) + 0.5 * imgIn.rows;//柱面图像高
Mat imgOut = Mat::zeros(rowNum, colNum, CV_8UC1);
Mat_<uchar> im1(imgIn);
Mat_<uchar> im2(imgOut);
//正向插值
int x1(0), y1(0);
for (int i = 0; i < imgIn.rows; i++)
for (int j = 0; j < imgIn.cols; j++)
{
x1 = f * atan((j - 0.5 * imgIn.cols) / f) + f * atan(0.5 * imgIn.cols / f);
y1 = f * (i - 0.5 * imgIn.rows) / sqrt(pow(j - 0.5 * imgIn.cols, 2) + pow(f, 2)) + 0.5 * imgIn.rows;
if (x1 >= 0 && x1 < colNum && y1 >= 0 && y1 < rowNum)
{
im2(y1, x1) = im1(i, j);
}
}
return imgOut;
}
可以针对视频中的目标进行融合,也可以驱动摄像头就行融合拼接,效果如下图所示。由于视频分辨率被我处理过的原因导致物体有畸变,可以进行修改完善,融合后的图片无明显拼接痕迹,而且速度较快实时性很好,能够在板子上运行。如果需要对图像进一步操作,在cvtcolor后可以进行修改。
与拼接软件Kolor Autopano Video Pro 2.0得到的图像进行比较。两者差异不大(在同组样本视频下)。
图为 Kolor Autopano Video Pro 得到的全景拼接图像
代码块解析:
主要有三个函数
柱面投影cylinder;计算两幅图像之间的平移量getOffset;线性融合linearFusion。
1、getOffset//计算平移参数
Point2i getOffset(Mat img, Mat img1)
{
Mat templ(img1, Rect(0, 0.4 * img1.rows, 0.2 * img1.cols, 0.2 * img1.rows));
Mat result(img.cols - templ.cols + 1, img.rows - templ.rows + 1, CV_8UC1);//result存放匹配位置信息
matchTemplate(img, templ, result, TM_CCORR_NORMED); //opencv4.2.0将CV_TM_CCORR_NORMED改为TM_CCORR_NORMED
normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());
double minVal; double maxVal; Point minLoc; Point maxLoc; Point matchLoc;
minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat());
matchLoc = maxLoc;//获得最佳匹配位置
int dx = matchLoc.x;
int dy = matchLoc.y - 0.4 * img1.rows;//右图像相对左图像的位移
Point2i a(dx, dy);
return a;
}
①matchTemplate为采用的模板匹配算法。此处采用的是TM_CCORR_NORMED法。常见的匹配算法公式如下:
其中,TM_CCORR_NORMED是标准相关性匹配。采用模板和图像间的乘法操作,数越大表示匹配程度较高, 0表示最坏的匹配效果。
opencv的目标匹配函数为matchTemplate,函数原型为:matchTemplate(image, templ, method[, result[, mask]]) -> result
image参数表示待搜索源图像,必须是8位整数或32位浮点。
templ参数表示模板图像,必须不大于源图像并具有相同的数据类型。
method参数表示计算匹配程度的方法。
result参数表示匹配结果图像,必须是单通道32位浮点。如果image的尺寸为W x H,templ的尺寸为w x h,则result的尺寸为(W-w+1)x(H-h+1)。
其中,其中result是模板图像去匹配的区域位置图像。
②normalize,归一化数据。
void cv::normalize(InputArry src,InputOutputArray dst,double alpha=1,double beta=0,int norm_type=NORM_L2,int dtype=-1,InputArray mark=noArry())
参数说明:
src 输入数组;
dst 输出数组,数组的大小和原数组一致;
alpha 用来规范值或者规范范围,并且是下限;
beta 只用来规范范围并且是上限,因此只在NORM_MINMAX中起作用;
norm_type 归一化选择的数学公式类型;
dtype 当为负,输出在大小深度通道数都等于输入,当为正,输出只在深度与输如不同,不同 的地方由dtype决定;
mark 掩码。选择感兴趣区域,选定后只能对该区域进行操作。
③minMaxLoc,在给定的矩阵中寻找最大和最小值,并给出它们的位置。
该功能不适用于多通道阵列。 如果您需要在所有通道中查找最小或最大元素,要先将阵列重新解释为单通道。
函数minMaxLoc原型为:minMaxLoc(src[, mask]) -> minVal, maxVal, minLoc, maxLoc
src参数表示输入单通道图像。
mask参数表示用于选择子数组的可选掩码。
minVal参数表示返回的最小值,如果不需要,则使用NULL。
maxVal参数表示返回的最大值,如果不需要,则使用NULL。
minLoc参数表示返回的最小位置的指针(在2D情况下); 如果不需要,则使用NULL。
maxLoc参数表示返回的最大位置的指针(在2D情况下); 如果不需要,则使用NULL。
2、linearFusion采用渐入渐出融合,其实就是在重叠区域,对两幅图像的像素,线性地分配权值。公式:img=d∗img1+(1−d)∗img2img=d∗img1+(1−d)∗img2;其中img为融合后的图像,img1和img2为待拼接的两幅图像。d为重叠区域中某个像素点到边界的距离。
Mat linearFusion(Mat img, Mat img1, Point2i a)
{
int d = img.cols - a.x;//过渡区宽度
int ms = img.rows - abs(a.y);//拼接图行数
int ns = img.cols + a.x;//拼接图列数
Mat stitch = Mat::zeros(ms, ns, CV_8UC1);
//拼接
Mat_<uchar> ims(stitch);
Mat_<uchar> im(img);
Mat_<uchar> im1(img1);
if (a.y >= 0)
{
Mat roi1(stitch, Rect(0, 0, a.x, ms));
img(Range(a.y, img.rows), Range(0, a.x)).copyTo(roi1);
Mat roi2(stitch, Rect(img.cols, 0, a.x, ms));
img1(Range(0, ms), Range(d, img1.cols)).copyTo(roi2);
for (int i = 0; i < ms; i++)
for (int j = a.x; j < img.cols; j++)
ims(i, j) = uchar((img.cols - j) / float(d) * im(i + a.y, j) + (j - a.x) / float(d) * im1(i, j - a.x));
}
else
{
Mat roi1(stitch, Rect(0, 0, a.x, ms));
img(Range(0, ms), Range(0, a.x)).copyTo(roi1);
Mat roi2(stitch, Rect(img.cols, 0, a.x, ms));
img1(Range(-a.y, img.rows), Range(d, img1.cols)).copyTo(roi2);
for (int i = 0; i < ms; i++)
for (int j = a.x; j < img.cols; j++)
ims(i, j) = uchar((img.cols - j) / float(d) * im(i, j) + (j - a.x) / float(d) * im1(i + abs(a.y), j - a.x));
}
return stitch;
}