泊松融合(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!