7、融合
7.1 原理
在上一步中,虽然我们已经得到了接缝线,但如果只是简单的对接缝线的两侧选取不同的图像,那么对于重叠区域,在接缝线处的过度会出现不连贯的现象,在视觉上会显得有些突兀。因此我们还需要在接缝线两侧,对不同图像进行融合处理来克服上述不足之处。应用于图像拼接的融合算法有两种常用的方法:羽化和多频段融合。
羽化的原理是对边界进行平滑虚化,通过渐变的方法达到自然衔接的效果。在应用于图像拼接时,羽化是只对接缝线两侧的区域进行处理,它的公式为:
(92)
式中,R表示由n幅图像重叠后经过羽化处理后而得到的新图像,Ii表示第i幅图像在接缝线两侧区域内的部分,wi表示Ii的权值,它是当前像素到达第i幅图像最近边界的距离。从式92可以看出,羽化算法本质上就是加权平均的过程。另外羽化处理还可以通过设置锐度参数,来调整羽化平滑处理的虚化程度和羽化面积。
羽化算法虽然简单,但当重叠部分有细微的不重合的时候,图像的高频部分会出现较为明显的模糊情况。
为了能够保留图像的高频成分(即图像的细节部分),则需要应用多频段融合方法,它通过建立拉普拉斯(带通滤波器)金字塔,使各个频段上的信息都保留并融合在一起。
我们下面给出多频段融合方法的具体执行步骤:首先分别建立各个图像的拉普拉斯金字塔,然后针对重叠区域,把它们的金字塔的相同层应用式92进行合并,最后对该合并后的金字塔进行逆拉普拉斯变换,从而得到最终的融合图像。
拉普拉斯金字塔是通过高斯金字塔得到。高斯金字塔的上一层图像是对下一层图像进行高斯模糊(卷积高斯内核)再降采样(隔点采样)得到的。而拉普拉斯金字塔的各层图像是由高斯金字塔的相同层减去它的上一层的扩展(即先升采样,再卷积高斯内核)得到的,即
(93)
式中,L和G分别表示拉普拉斯和高斯金字塔,拉普拉斯金字塔的顶层图像就是高斯金字塔的顶层图像,下标n表示的是金字塔的层数,底层为0,并且G0为图像原图,expand表示扩展运算。拉普拉斯金字塔是由底层向顶层逐层构建得到的。图14示意了拉普拉斯金字塔的建立方法。
图14 拉普拉斯金字塔
当得到了不同图像的拉普拉斯金字塔后,我们仍然可以应用式92对不同的区域的不同层进行合并,同样也得到了一个金字塔,我们称为合并金字塔。其中式92的权值,在这里就是掩码,而各层的掩码也是通过建立金字塔得到,也就是需要为掩码建立一个高斯金字塔,金字塔的底层就是该图的掩码。
逆拉普拉斯变换的计算公式为:
(94)
式中,R为由式92得到的合并金字塔,S为融合金字塔,其中,S的顶层为R的顶层,S是从顶层向底层计算得到的,最终得到的融合金字塔的底层图像就是我们想要的融合图像。图15和图16分别表示了合并金字塔和融合金字塔建立过程。
图15 合成金字塔创建示意图
图16融合金字塔创建示意图
7.2 源码
图像融合的基类Blender为:
class CV_EXPORTS Blender
{
public:
virtual ~Blender() {}
enum { NO, FEATHER, MULTI_BAND }; //表示融合算法的类别
//该函数的主要作用是根据不同的算法类别type,实例化并得到不同的子类
static Ptr<Blender> createDefault(int type, bool try_gpu = false);
//prepare函数表示事先得到全景图像的Mat变量,就是为了像素赋值,先准备好全景图像的区域、尺寸
void prepare(const std::vector<Point> &corners, const std::vector<Size> &sizes);
virtual void prepare(Rect dst_roi);
virtual void feed(const Mat &img, const Mat &mask, Point tl); //预处理图像
virtual void blend(Mat &dst, Mat &dst_mask); //执行融合算法
protected:
//表示最终得到的全景图像和它的掩码
Mat dst_, dst_mask_;
Rect dst_roi_; //表示最终得到的全景图像的矩形变量
};
第一个prepare函数:
void Blender::prepare(const vector<Point> &corners, const vector<Size> &sizes)
//corners表示待拼接图像在全景图像中的左上角坐标
//sizes表示映射变换后待拼接图像的尺寸
{
//利用resultRoi函数得到最终的全景图像的尺寸
//调用另一个prepare函数,该函数的主要作用是初始化为dst_,dst_mask_和dst_roi_
prepare(resultRoi(corners, sizes));
}
为dst_和dst_mask_在img图像的区域内赋值,父类Blender类本质上没有进行任何融合,所以该类的feed函数就是简单赋值:
void Blender::feed(const Mat &img, const Mat &mask, Point tl)
//img表示待拼接的图像
//mask表示该图像的掩码
//tl表示该图像在全景图像的左上角坐标
{
CV_Assert(img.type() == CV_16SC3); //确保img类型正确
CV_Assert(mask.type() == CV_8U); //确保mask类型正确
int dx = tl.x - dst_roi_.x; //表示该图像在最终的全景图像的左上角的横坐标
int dy = tl.y - dst_roi_.y; //表示该图像在最终的全景图像的左上角的纵坐标
for (int y = 0; y < img.rows; ++y) //遍历图像的行
{
//得到各个变量的行首地址指针
const Point3_<short> *src_row = img.ptr<Point3_<short> >(y);
Point3_<short> *dst_row = dst_.ptr<Point3_<short> >(dy + y);
const uchar *mask_row = mask.ptr<uchar>(y);
uchar *dst_mask_row = dst_mask_.ptr<uchar>(dy + y);
for (int x = 0; x < img.cols; ++x) //遍历当前行的每个像素
{
if (mask_row[x]) //当前像素没有被掩码掉
dst_row[dx + x] = src_row[x]; //赋值
dst_mask_row[dx + x] |= mask_row[x]; //赋值
}
}
}
基类的blend并没有执行任何融合算法,只是简单的赋值,当融合类别为NO时,其实也就是调用的该基类:
void Blender::blend(Mat &dst, Mat &dst_mask)
//dst和dst_mask表示最终得到的全景图像和掩码
{
dst_.setTo(Scalar::all(0), dst_mask_ == 0); //为掩码部分赋0值
dst = dst_; //赋值
dst_mask = dst_mask_; //赋值
dst_.release(); //释放内存
dst_mask_.release(); //释放内容
}
下面介绍羽化融合算法FeatherBlender类的相关函数,它的父类为Blender类:
void FeatherBlender::prepare(Rect dst_roi)
{
Blender::prepare(dst_roi); //调用Blender::prepare函数
//全局变量dst_weight_map_表示最终得到的全景图像的权值映射图像,在这里初始化该变量
dst_weight_map_.create(dst_roi.size(), CV_32F);
dst_weight_map_.setTo(0);
}
void FeatherBlender::feed(const Mat &img, const Mat &mask, Point tl)
{
CV_Assert(img.type() == CV_16SC3); //确保img类型正确
CV_Assert(mask.type() == CV_8U); //确保mas