在双摄像头相对平行固定,所拍摄图像视差很小,可使用平移运动模型的情形下,我们提到了“柱面投影+模板匹配+渐入渐出拼接”的解决方案。不考虑多线程,参见图像拼接(四):双摄像头实时视频拼接(平移模型)
可以看出,图像或视频拼接其实是一项”流水线”技术,包括:源图像数据获取(两幅源图像)、柱面投影、模板匹配、图像融合、输出或者是显示拼接后的图像。这个过程中每个步骤有先后,可以说是串行的,怎么能使用多线程处理呢?但是想象一下工厂里的流水线,每个工序在进行的过程中,上一个工序产生的物料都已经准备足,当前工序做完之后,也会立刻传递给下一个工序。所以,不必等待最后的步骤做完,才从头开始。通过这样分析,整个流程的完成时间取决于最慢的那个单步,所以整个效率会有很大提高。
按上面所分,整个过程包括5个单步,所以可以分为5个线程,如果每步耗时比较平均的话,时间效率可以提高5倍!可是,怎么完成“流水线”式的多线程呢?怎么控制每步之间的同步呢?还有“图像融合”这一步需要“源图像数据获取”和“模板匹配(计算平移量)”这两步的同步数据。之前找了一些资料,加上多线程学习的不是很充分,这个问题没有得到很好的解决。
为了简化问题,将线程分为2个,这个问题就容易的多了。第一个线程就叫做“数据准备线程”,它包括帧图像读取,通过模板匹配计算平移量(由于我设计的实验中,柱面投影对结果影响不大,故忽略);第二个线程叫做“数据处理线程”,它包括渐入渐出拼接和拼接后的图像显示。
废话不多说,上代码:
#include"opencv2/core/core.hpp"
#include"opencv2/highgui/highgui.hpp"
#include"opencv2/imgproc/imgproc.hpp"
#include<iostream>
#include<queue>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<time.h>
using namespace std;
using namespace cv;
Mat cylinder(Mat imgIn, int f);
Point2i getOffset(Mat img, Mat img1);
Mat linearStitch(Mat img, Mat img1, Point2i a);
mutex mut;
queue<Point2i> data_queue;
condition_variable data_cond;
VideoCapture cap1(0);
VideoCapture cap2(1);
Mat frame1;
Mat frame2;
Mat frame;
Point2i a;//存储偏移量
bool run = true;
double t = (double)getTickCount();//线程2时间
double t1 = (double)getTickCount();//线程1时间
void data_preparation_thread()
{
while (run)
{
if (cap1.read(frame1) && cap2.read(frame2))
{
waitKey(1);
if (waitKey(20) >= 0)
{
run = false;
}
imshow("cam1", frame1);
imshow("cam2", frame2);
//彩色帧转灰度
cvtColor(frame1, frame1, CV_RGB2GRAY);
cvtColor(frame2, frame2, CV_RGB2GRAY);
//匹配
//cout << "正在匹配..." << endl;
t1 = ((double)getTickCount() - t1) / getTickFrequency();
cout <<"匹配时间:"<< t1 << endl;
t1 = (double)getTickCount();
a = getOffset(frame1, frame2);
}
lock_guard<mutex> lk(mut);
data_queue.push(a);
data_cond.notify_one();
}
}
void data_processing_thread()
{
while (run)
{
unique_lock<mutex> lk(mut);
data_cond.wait(lk, []{return !data_queue.empty(); });
Point2i a = data_queue.front();
data_queue.pop();
lk.unlock();
//cout << "正在拼接"<<endl;
frame = linearStitch(frame1, frame2, a);
waitKey(20);
if (waitKey(20) >= 0)
{
run = false;
}
t = ((double)getTickCount() - t) / getTickFrequency();
cout <<"拼接时间:"<< t << endl;
imshow("stitch", frame);
t = (double)getTickCount();
}
}
int main()
{
//初始化
if (cap1.isOpened() && cap2.isOpened())
{
cout << "*** ***" << endl;
cout << "摄像头已启动!" << endl;
}
else
{
cout << "*** ***" << endl;
cout << "警告:请检查摄像头(数量2)是否安装好!" << endl;
cout << "程序结束!" << endl << "*** ***" << endl;
return -1;
}
cap1.set(CV_CAP_PROP_FOCUS, 0);
cap2.set(CV_CAP_PROP_FOCUS, 0);
//匹配对齐线程
thread t1(data_preparation_thread);
//拼接线程
thread t2(data_processing_thread);
t1.join();
t2.join();
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, CV_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 linearStitch(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;
}
对代码做些解释说明:
关于C++的多线程,linux和windows上都有相关的库,我查找了windows编程的书,里面的有些多线程函数过于古老,感觉杂乱。后又了解到C++11标准库里有多线程库,这个比较新,又是语言层面的,通用性和移植性更高,所以采用了C++11的多线程库。可以看到,有关多线程头文件如下:
#include<thread>
#include<mutex>
#include<condition_variable>
代码中使用了多线程中互斥量、锁、条件变量的概念。如果对这些概念感到茫然,可以学习下C++11线程库的教程。稍有学习后你会发现上述代码很简单。
为了帮助理解,画了张简图,(图中while的意思仅代表每个线程内部都不停在循环处理)
通俗的理解,互斥量是一块有限的资源,每次只能有一个线程占据它。占据它的方式就是用一个锁,将它锁定。
//定义一个互斥量
mutex mut;
//定义一个锁,锁定互斥量
lock_guard<mutex> lk(mut);
还可以用另一种锁的类型定义锁,这种类型更加灵活
//定义一个锁,锁定互斥量
unique_lock<mutex> lk(mut);
//...
//解锁
lk.unlock();
定义和创建一个线程,要定义它的线程处理函数。运行它,使用join()函数。
//匹配对齐线程
thread t1(data_preparation_thread);
//拼接线程
thread t2(data_processing_thread);
t1.join();
t2.join();
所以整个代码,大部分的工作是完成两个线程处理函数。
线程1(数据准备):执行读帧和计算平移量的工作,工作完成后,要求锁定互斥量,将结果压入队列,并通过条件变量发出通知。
线程2(数据处理):锁定互斥量,通过条件变量等待消息,仅当消息传来,拿到数据后才释放(有些霸道哦)。。拿到数据后,解锁互斥量,然后就可以愉快地执行任务了:图像融合和拼接结果的显示。
注:我现在分析代码,发现仅仅将平移量结果压入队列,而当前帧数据是通过全局变量实现更新的,这样的做法可以work,但有些草率,可以再定义两个Mat类型的队列的.
最后分析实验结果的时间效率,发现符合预期,拼接时间降低大约
12
。
关于C++11多线程条件变量的详解,参考资料: