泊松图像融合(Seamless cloning)的原理 及 API实现

参考网址:泊松融合原理及C ++算法实现
注:本文大部分参考上面博客,加入了自己的理解和整理。感谢原博主的精辟讲解。

本文只讲“Poisson Image Editing”第一个功能,普通无缝融合功能。我将直接给出离散形式实现方法,算法流程。

图像合成是通过将源图像中一个物体或者一个区域嵌入到目标图像生成一个新的图像。

在对图像进行合成的过程中,为了使合成后的图像更自然,合成边界应当保持无缝。但如果原图像和目标图像有着明显不同的纹理特征,则直接合成后的图像会存在明显的边界。针对此问题,提出了一种利用构造泊松方程求解像素最优值的方法,在保留了源图像梯度信息的同时,可以很好的融合源图像与目标图像的背景。该方法根据用户指定的边界条件求解一个泊松方程,实现了梯度域上的连续,从而达到边界处的无缝融合。

泊松图像编辑的主要思想是,根据源图像的梯度信息以及目标图像的边界信息,利用插值的方法重新构建出合成区域内的图像像素。

开始这个算法前,我需要先讲解一个数学问题:


1.梯度计算

现在假设一幅3×3的单通道灰度图像:

泊松图像融合(Seamless cloning)的原理 及 API实现_第1张图片

通过拉普拉斯卷积核,进行卷积,就可以求解散度了。

 

拉普拉斯卷积核

假设每一点的像素值为V,V(1)表示像素点1的值,我们可以定义像素点5的散度的计算公式为:

DIV(5)= [V(2)+ V(4)+ V(6)+ V(8)] - 4 * V(5)

正规的散度求解过程应该是先求解像素点5的梯度值,然后再对梯度求导。


 2.泊松重建

如果给定一张图像,可以利用拉普拉卷积核,求解每个像素点的散度(注意:后面用于泊松方程求解散度的时候,应该先求梯度,然后再对梯度求导得到散度,不要直接用卷积核,否则会导致边界出现过渡不自然的现象。这里使用卷积核只是为了推导出下面的多元一次方程)。

现在反过来,如果给定每个像素点的散度,要求解每个像素点的值,要怎么求取?这便是泊松方程的灵魂了。现在假设图像的大小是4×4的16个像素点图片,如下:

 泊松图像融合(Seamless cloning)的原理 及 API实现_第2张图片

假设给你像素点6,7,10,11的散度值格(6),DIV(7),格(10),格(11),那么可以列出如下4个方程:

我们只有4个方程,可是里面有16个像素点(未知数)。(R(A)= R(A,x)= 4)<(n = 16),多元一次方程组有无穷解,所以单靠上面的4个方程来求解所有的像素值是不可能的。因此需要添加约束方程,这便是泊松重建方程的约束条件了。假设我们添加边界约束条件,如果知道已经了上面那副图像最一圈外围的每个像素点的值U,这样我们就可以得到12个约束方程即:

V(1)= u(1)V(2)= u(2)V(3)= u(3)

V(4)= u(4)V(5)= u(5)V(8)= u(8)

V(9)= u(9)V(12)= u(12)V(13)= u(13)

V(14)=u(14)             V(15)=u(15)        V(16)=u(16)

上面有12个方程,外加给定的散度4个方程,这样我们有16个方程。( R(A)=R(A,x)=16) = ( n= 16 ),多元一次方程组有唯一解,这样就能实现通过散度+边界约束条件,实现图像重建。这个便是泊松方程的主要过程。

不管图像多大,如果我们已经知道图像最外一圈的像素值(约束条件),以及其它像素点的散度值,我们就能把这个方程给列出来,构建泊松方程,重建图像。

因此泊松融合,说的再简单一点,就是构建方程组:Ax=b。然后通过求解这个方程组得到每个像素点的值。


 3.  泊松图像融合

我们知道,对于一个像素点的散度求解,其实就是拉普拉斯算子滤波的结果:

 

拉普拉斯算子

因为泊松重建,其实就是求解方程组:Ax=b

算法的整个过程在于求解系数稀疏矩阵A、及b。只要A、b求出来了,我们就可以求解方程组得到x,x就是融合结果的像素颜色值。

一个错误的做法是:直接用拉普拉斯卷积核对源图像的兴趣区域(ROI)进行卷积来得到散度b的值。这种思路没有错,然而这样会出现边界过渡不自然的现象。(我猜是因为本来是求二阶偏导,结果用拉普拉斯算子存在误差)。

正确的求解散度b的思路应该是:

(1)求解:源图像ROI的梯度场src_mask_grad,目标图像不被修改(去除源图像ROI的剩余区域)的像素区域的梯度场dst_grad。

(2)通过src_mask_grad+dst_grad得到整幅待重建图像的梯度场。

(3)最后才根据梯度场求解散度。


 4.  算法流程

(1)问题描述:

现在假设我们有源图像的感兴趣区域g,背景(目标)图片S:

                                   泊松图像融合(Seamless cloning)的原理 及 API实现_第3张图片

待克隆图像区域(ROI)                                   背景图片S

现在我们希望把图片g融合粘贴到S中,且实现自然融合的效果:

泊松图像融合(Seamless cloning)的原理 及 API实现_第4张图片 


 (2)算法实现:

步骤1:计算源图像的ROI区域src_mask的梯度场

通过差分的方法,可以求得图像的梯度场v:

 

ROI的梯度场

梯度场的求取用一阶算子卷积就可以得到(比如sobel算子)。在这里我贴一下opencv的泊松融合这一步的代码:

computeGradientX(src_mask, src_mask_gradX);//计算源图像ROI区域mask的x方向梯度  
computeGradientY(src_mask ,src_mask_gradY);//计算源图像ROI区域mask的y方向梯度 

计算梯度的函数为:

void Cloning::computeGradientX( const Mat &src_mask, Mat &src_mask_gradX)  
{  
    Mat kernel = Mat::zeros(1, 3, CV_8S);  
    kernel.at(0,2) = 1;  
    kernel.at(0,1) = -1;  
  
    if(src_mask.channels() == 3)  
    {  
        filter2D(src_mask, src_mask_gradX, CV_32F, kernel);  
    }  
    else if (src_mask.channels() == 1)  
    {  
        Mat tmp[3];  
        for(int channel = 0 ; channel < 3 ; ++channel)  
        {  
  		filter2D(src_mask, tmp[channel], CV_32F, kernel);  
        }  
        merge(tmp, 3, src_mask_gradX);  
    }  
}  
  
void Cloning::computeGradientX( const Mat &src_mask, Mat &src_mask_gradY)  
{  
    Mat kernel = Mat::zeros(1, 3, CV_8S);  
    kernel.at(2,0) = 1;  
    kernel.at(1,0) = -1;  
  
    if(src_mask.channels() == 3)  
    {  
        filter2D(src_mask, src_mask_gradY, CV_32F, kernel);  
    }  
    else if (src_mask.channels() == 1)  
    {  
        Mat tmp[3];  
        for(int channel = 0 ; channel < 3 ; ++channel)  
        {  
  	    filter2D(src_mask, tmp[channel], CV_32F, kernel);  
        }  
        merge(tmp, 3, src_mask_gradY);  
    }  
}  

这样我们就可以计算出感兴趣区域src_mask的梯度场Vsrc_mask(src_mask_gradX, src_mask_gradY)。(考虑直接用Sobel求出)


步骤2:计算背景图片dst的梯度场

如果你想简化计算,背景图片Ω区域的梯度场是不需要计算的,因为这一块迟早会被g的梯度场替换掉;所以可以对背景图像dst做mask操作,来将Ω区域的像素置0;mask操作可以参考步骤3。(参考博客的博主没有做去除背景图片dst的Ω区域的操作,而是直接对dst求梯度)

得到了背景图片的梯度场Vdst(dst_gradX, dst_gradY):

computeGradientX(dst, dst_gradX);//计算背景图像dst的x方向梯度  
computeGradientY(dst, dst_gradY);//计算背景图像dst的y方向梯度  

步骤3:计算融合图像的梯度场

参考博客的博主,没有对背景图像dst做去除Ω区域的操作,所以这里需要先对步骤2得到的dst_gradX dst_gradYmask操作,才能直接与步骤1得到src_mask的梯度相加。如果已经再步骤2做了去除背景图片dst的Ω区域的操作,就不需要这一步了。具体的mask操作如下: 

arrayProduct(dst_gradX, binaryMaskFloatInverted, dst_gradX);  
arrayProduct(dst_gradY, binaryMaskFloatInverted, dst_gradY); 
//矩阵点乘,将lhs与rhs点乘得到result,因为有三个通道,估计mat不能实现三通道的矩阵的一次性点乘,所以才有这个函数  
void Cloning::arrayProduct(const Mat & lhs, const Mat & rhs, Mat & result) const  
{  

    vector  lhs_channels;  
    vector  result_channels;  

    split(lhs, lhs_channels);//拆分成3个通道的矩阵  
    split(result, result_channels);  
 
    for(int chan = 0 ; chan < 3 ; ++chan)//三个矩阵进行分别相乘  
        multiply(lhs_channels[chan], rhs, result_channels[chan]); 
  merge(result_channels, result);//合成为一个  
}  

上面函数中binaryMaskFloatInverted是一个mask,即Ω区域的值为0,非Ω区域的值为1。

补充一点:我认为对步骤1得到的src_mask_ggrad_X和src_mask_ggrad_Y也要做mask操作(Ω区域的值为1,非Ω区域的值为0),来使src_mask_ggrad_X和src_mask_ggrad_Y矩阵的size与dst_gradX dst_gradY的size相同。这样覆盖时,才能刚好吻合。不知道这样的想法对不对,欢迎指正!

计算完成以后,我们就直接把ROI的梯度场覆盖到S(去除Ω区域)的梯度场上:

Mat gradX = Mat(dst.size(), CV_32FC3);  
Mat gradY = Mat(dst.size(),CV_32FC3);  
//求解整张融合图片的梯度场
gradX = dst_gradX + src_mask_gradX;  
gradY = dst_gradY + src_mask_gradY;  

 泊松图像融合(Seamless cloning)的原理 及 API实现_第5张图片

待重建图像的梯度场

步骤4:求解融合图像的散度

通过步骤3中,我们可以得到每个像素点的梯度值,也就是待重建图像的梯度场,因此接着我们需要对梯度求偏导,从而获得散度。

computeLaplacianX(gradX,laplacianX); //求解梯度的散度
computeLaplacianY(gradY,laplacianY);  

其相关调用函数:

void Cloning :: computeLaplacianX(const Mat&gradX,Mat&laplacianX)  

{  
    Mat kernel = Mat :: zeros(1,3,CV_8S);  
    kernel.at (0,0)= -1;  
    kernel.at (0,1)= 1;  
    filter2D(gradX,laplacianX,CV_32F,kernel);  
}  
其实:
computeLaplacianX(gradX,laplacianX); //求解梯度的散度
computeLaplacianY(gradY,laplacianY);  

这两句代码就是对梯度(gradX,gradY)在x和y方向上求偏导。因此最后散度的计算为:

lap = laplacianX + laplacianY; //散度

步骤5:求解系数矩阵

第4步我们已经把散度计算完毕,回顾一下前面的泊松重建方程,Ax = b的,B便是散度,因此接着我们需要只要构建系数矩阵,还有约束方程就确定了。

这里没有直接按我们的一般理解去求A,然后x=,因为如果直接用求解A,然后求A的逆矩阵,计算量特别大。opencv的源码用了泊松方程的快速求解方法,假如待重建图像的大小是1000*1000的,那么系数矩阵的大小就是(1000*1000)X(1000*1000)的方阵。虽然A最后是稀疏矩阵,但是这么庞大的矩阵,搞起来也要崩溃。

我还是得讲一下普通的解法,系数矩阵A到底是什么。

其实矩阵A,前面已经提到过了。矩阵A的对角线的元素为-4,然后每行有对应的其它4个非零元素,其值为1,因为我们拉普拉斯卷积核的时候,就是这样搞的。还有一点我们图像边界像素点的值应该为1。为了简单理解,回到博文最开始的部分:

 泊松图像融合(Seamless cloning)的原理 及 API实现_第6张图片

如果一幅图像,除了边界像素点之外,上面3*3图像的边界像素点为1、2、3、4、6、7、8、9(值均为1)。其它像素点的散度(上图中的像素5)我都已经知道了。那么我就可以列出泊松方程:

[V(2)+V(4)+V(6)+V(8)]-4*V(5)=div(5)

然后如果在把一幅图像的边界像素点的像素值告诉你,那么你就可以求解泊松方程了,假设约束点的值为u。以上面3*3的图像为例,最后系数矩阵A的构造为:

 泊松图像融合(Seamless cloning)的原理 及 API实现_第7张图片

然后最后列出Ax=b的结果为:

 泊松图像融合(Seamless cloning)的原理 及 API实现_第8张图片

这样分别求解三个通道的方程,我们就可以获得每个点的像素R,G,B值了。再啰嗦一遍上面系数矩阵A的特点,图像最外围一圈的边界的对角线元素值为1,因为这些点是约束方程,其它的非边界点就直接根据拉普拉斯的卷积核就可以了。opencv的源码如果你看不懂,建议看一下这个:http://eric-yuan.me/poisson-blending-2/   

(3)算法流程图

泊松图像融合(Seamless cloning)的原理 及 API实现_第9张图片


5.  算法的OpenCV API实现

#include 
#include

using namespace std;
using namespace cv;

/*******************************************************************/
/*                        泊松图像融合算法API实现                    */
/*******************************************************************/

int main()
{
	//载入源图像
	Mat src = imread("F:/test_photo/poisson1.jpg");
	//载入目标图像
	Mat dst = imread("F:/test_photo/poisson2.jpg");

	if (!src.data)
	{
		cout << "could not load source image..." << endl;
		return -1;
	}
	imshow("input image1", src);
	if (!dst.data)
	{
		cout << "could not load destination image..." << endl;
		return -1;
	}
	imshow("input image1", src);
	imshow("input image2", dst);

	//确定src的ROI区域:全部区域
	Mat mask = 255 * Mat::ones(src.rows, src.cols, src.depth());

	//确定src要在dst上摆放的位置p:center
	Point p(dst.cols / 2, dst.rows / 2);

	//创建与原图像等尺寸和类型的输出图像
	Mat blend;
	blend.create(src.size(), src.type()); 

	//泊松融合
	seamlessClone(src, dst, mask, p, blend, NORMAL_CLONE);
	imshow(“normal_clone输出图像”,混合);

	waitKey(0);

	返回0;

}

 输出结果:

泊松图像融合(Seamless cloning)的原理 及 API实现_第10张图片       泊松图像融合(Seamless cloning)的原理 及 API实现_第11张图片

泊松图像融合(Seamless cloning)的原理 及 API实现_第12张图片

你可能感兴趣的:(泊松图像融合(Seamless cloning)的原理 及 API实现)