图像融合(一)Poisson Blending

泊松融合(Poisson Blending)又作 Seamless clone,用于将两幅图像“无缝”的融合起来,基本原理就是最优化一个方程,尽量在和base边界处保持相关的亮度,同时保留剪切过来图像的梯度,这样看起来两张图像就“无缝”拼合在一起了。因为该工具在opencv中已经存在,所以我们可以直接使用opencv体验其效果。这篇博简单介绍原理,以及感受效果。

一、泊松方程与拉普拉斯方程

我们从微积分中这样几个概念开始:

  • 梯度:一个向量场在某点的变化速率。对于图像来说,边缘的梯度大,平摊区域的梯度小,这个很好理解。
  • 通量:一个向量场流过一个截面的量。我们把向量场想象成水流,那么通量就代表了单位时间通过该截面的水量。
  • 散度:散度流出通量减去流入通量。对于空间内的无源场,散度是0,因为流入等于流出,对于有源场,散度不为0。
  • 旋度:旋度和散度类似,只是将向量分解到切向来积分(而散度是在发向上积分的),旋度的方向符合右手定则。

回到图像上来,那么如何才能保证拼接是无缝的呢?

泊松方程计算得到散度:

左侧算子被称为Laplace算子,如果式子右侧为0,即区域散度为0,则该区域的亮度和剪辑之前是一致的。在Laplace算子作用下为0的函数也称为调和函数(可以理解为区域内具有相同和)。

那么这样就满足了无缝的一个条件,把粘贴过来的图像的亮度调整到和原始图像一致。

那么第二个问题来了,粘贴过来的图像怎样保持和原来相近的纹理呢?一个很直接的思想是,把梯度copy过来就可以。那我们就有了算法的基本流程:

上面三幅图分别对应于 前景、背景、结果。区域的边界 omega 上的散度是已知的,我们保持区域内散度不变,即为第一个约束,我们定义一个差值函数f,在边界条件约束下,在omega上最小化 f-v 就可以了。

我们在一维上来理解下:

左边是原图,相当于前景,红色直方图是引导向量v,右边是背景图,想把左边红色部分移过去,但是又要变化最小。

那么就是要计算如下最小化问题,边界条件由背景定义

因为都是正数,所以方程直接相加:

分开求偏导数

偏导为0时最小,则转化为:

手算,解得:

这样就在融合过程中保证接缝处的亮度是衔接的,同时内容由是另一张图像的。

对于二维图像来说,上述过程可以用图像的拉普拉斯算子进行简化,使得最终的计算量没有特别大。

二、Normal Clone

OpenCV中有seamless clone的实现,我们直接参考demo来介绍其中几种不同方法参数配置的效果:(博主是在jupyter中运行的,imshow这里根据自己的情况修改即可)

import cv2 as cv
import sys
import numpy as np
import matplotlib.pyplot as plt

cv.samples.addSamplesDataSearchPath("D:/data/opencv_extra-master/testdata/cv/cloning")

def imshow(im, title=''):
    im = cv.cvtColor(im, cv.COLOR_BGR2RGB)
    h,w = im.shape[:2]
    print(title, im.shape)
    plt.imshow(im,cmap='gray')
    plt.title(title)
    plt.show()

src = cv.imread(cv.samples.findFile("Normal_Cloning/source1.png"))
dst = cv.imread(cv.samples.findFile("Normal_Cloning/destination1.png"))
mask = cv.imread(cv.samples.findFile("Normal_Cloning/mask.png"))

imshow(src, 'src')
imshow(dst, 'dst')
imshow(mask, 'mask')

output = cv.seamlessClone(src, dst, mask, (400,100), cv.NORMAL_CLONE)
imshow(output, 'output')

我们首先使用的是NORMAL_CLONE方法,该方法就是正常的标准实现,可以看到无缝拼接的效果了

然后我们改变一节拼接处的位置,我们会发现下图NORMAL_CLONE给出的结果图像中出了鬼影,数的纹理都消失不见,那么如果我们像保留背景中的纹理应该怎么办呢?

OpenCV给我们提供了另一种方法: MIXED_CLONE,这种方法会将前景和背景的纹理也做融合,效果如下图:

OpenCV还为我们提供了第三种方法: MONOCHROME_TRANSFER  , 这种方法只会对前景的luma做融合,而chroma是沿用背景的,这样其颜色和背景就是一样的,效果如下图:

三、localColorChange

OpenCV的Demo中还提供了几项应用,其中就有改变局部颜色的例子,就是在Possion Blending的基础上把区域颜色进行调整。

实现也非常简单,忍不住贴了opencv的源码,localColorChange的源码就比正常实现的源码多了两行,把对应贴过来图的patch的r、g、b各自乘以一个指定的增益,这样在解泊松方程之前就可以改变区域的颜色了。

void Cloning::localColorChange(Mat &I, Mat &mask, Mat &wmask, Mat &cloned, float red_mul=1.0,
                                 float green_mul=1.0, float blue_mul=1.0)
{
    computeDerivatives(I,mask,wmask);

    arrayProduct(patchGradientX,binaryMaskFloat, patchGradientX);
    arrayProduct(patchGradientY,binaryMaskFloat, patchGradientY);
    scalarProduct(patchGradientX,red_mul,green_mul,blue_mul);
    scalarProduct(patchGradientY,red_mul,green_mul,blue_mul);

    evaluate(I,wmask,cloned);
}

四、textureFlatten

在Possion Blending的基础上,用Can你也算子处理mask,得到边缘,然后在Possion重建时抹平引导图的梯度,这样重构输出图像的纹理就消失不见了。

函数输入参数的阈值就是Canny边缘检测的阈值。

void Cloning::textureFlatten(Mat &I, Mat &mask, Mat &wmask, float low_threshold,
        float high_threshold, int kernel_size, Mat &cloned)
{
    computeDerivatives(I,mask,wmask);

    Mat out;
    Canny(mask,out,low_threshold,high_threshold,kernel_size);

    Mat zeros = Mat::zeros(patchGradientX.size(), CV_32FC3);
    Mat zerosMask = (out != 255);
    zeros.copyTo(patchGradientX, zerosMask);
    zeros.copyTo(patchGradientY, zerosMask);

    arrayProduct(patchGradientX,binaryMaskFloat, patchGradientX);
    arrayProduct(patchGradientY,binaryMaskFloat, patchGradientY);

    evaluate(I,wmask,cloned);
}

五、illuminationChange

在Possion Blending的基础上,操作区域内patch的梯度,使得重构时,高光不会被引导重建。

OK, See You Next Chapter!

 

发表评论