《Poisson Image Editing》论文理解与复现
作者:互联网
1. 导读
本报告的主要内容是阅读《Poisson Image Editing》论文之后对原理进行理解并利用python复现论文中的每个功能。
2. 引言
图像融合是图像处理的一个基本问题,目的是将源图像中一个物体或者一个区域嵌入到目标图像生成一个新的图像。在对图像进行合成的过程中,为了使合成后的图像更自然,合成边界应当保持无缝。但如果源图像和目标图像有着明显不同的纹理特征,则直接合成后的图像会存在明显的边界。
针对这个问题,论文提出了一种利用构造泊松方程求解像素最优值的方法,在保留了源图像梯度信息的同时,融合源图像与目标图像。泊松图像编辑(Poisson Image Editing)的核心就是根据用户指定的边界条件求解一个泊松方程,实现了梯度域上的连续,从而达到边界处的无缝融合。
总的来说,将泊松方程引入图像融合后,接下来的操作很容易在梯度域中进行应用,并且可以通过局部的图像编辑得到全局融合的效果。
3. 技术贡献
利用基于求解泊松方程的通用插值机制,引入了各种新的图像区域无缝编辑工具。允许将不透明和透明的源图像区域无缝地输入到目标区域。并可以基于类似的数学思想,允许用户在一个选定的区域内无缝地修改图像的外观,具体为区域内对象的纹理、照明、颜色、边界无缝处理。
4. 方法介绍——导向插值求解泊松方程
总结来说,图像融合下面两点一定要满足:
(1)边缘过渡要平滑,就是说梯度要小;
(2)感兴趣区域内部的自身纹理要最大程度的保留。
4.1 导向插值原理
图中,\(g\)是源图像中要合成的部分,\(V\)是引导梯度场,\(S\)是合并后的图像,\(\Omega\)是合并后被覆盖的区域,\(\partial \Omega\)是边界,\(\Omega\)内的像素用\(f\)表示(\(f\)未知),\(\Omega\)外的像素用\(f^*\)表示(已知)。
-
合并后的图像看上去尽可能的平滑,没有明显的边界,所以\(\Omega\)内的梯度值要尽可能的小。因此,\(f\)的值由下面的公式来确定:
\[\underset {f}{min} \underset{\Omega}{\iint} || \nabla f||^2,f|_{\partial \Omega}=f^*|_{\partial \Omega} \]这个的解是以下具有狄利克雷边界条件的拉普拉斯方程的唯一解:\(\Delta f=0\) over \(\Omega\) with $ f|{\partial \Omega}=f^*|{\partial \Omega}$。
-
虽然最后的结果很平滑,但是太模糊了,缺失了纹理。因此引入了引导场的进一步约束来修正问题。
\[\underset {f}{min} \underset{\Omega}{\iint} || \nabla f-\nabla V||^2,f|_{\partial \Omega}=f^*|_{\partial \Omega} \]-
在这里,\(V\)可以是保守场也可以是非保守场。更好的理解就是\(V\)作保守场的时候,看成是源图像,即\(V=g\)。即让\(\Omega\)内的梯度尽可能与源图像相似。后面有优化出非保守场,在无缝融合那一块。
-
这样做的目的是,为了让融合结果中的\(\Omega\)内无限趋近源图像\(g\),这里需要有指引的梯度场\(V\),当融合后的图像内的\(f\)与\(g\)的梯度信息越相似,而边界处等于\(f^*\)的值,就说明原始纹理保持的越好,融合的效果越好。
-
其解是以下具有狄利克雷边界条件的泊松方程的唯一解:\(\Delta f= div(V)\) over \(\Omega\) with $ f|{\partial \Omega}=f^*|{\partial \Omega}$。
-
4.2 应用于图像中——离散化
由于图像可以看做是二元函数,其中 x,y 的值是离散的,最小变化量为 1。因此可以用差分代替微分的计算\(\Delta f\)。论文中的数学表达比较抽象,我通过查阅资料之后推导出更直观的表达。
如果\(f\)是关于x的函数,那么\(f\)在某个点的导数就是:
\[f^\prime(x_0)=lim_{\Delta x \rightarrow 1}\frac{f(x_0+\Delta x)-f(x_0)}{\Delta x} \]将\(\Delta x\)代入1,并用于一阶偏导函数,可得:
\[\frac{\partial f}{\partial x} = f(x+1,y)-f(x,y) \\ \frac{\partial f}{\partial y} = f(x,y+1)-f(x,y) \]在一阶偏导函数的基础上求二阶偏导函数。在求一阶偏导函数的时候是通过\(x+1\)的形式来近似求\(x\)点的偏导的,直观上来讲取值有点偏右了。那么在求二阶导函数的时候,让\(x = x - 1\)来近似\(x\)点,更为恰当。所以,二阶偏导函数为:
\[\frac{\partial^2 f}{\partial x^2}=f(x+1,y)-f(x,y)-(f((x-1)+1,y)-f(x-1,y))\\ = f(x+1,y)+f(x−1,y)−2f(x,y)\\ \frac{\partial^2 f}{\partial y^2}=f(x,y+1)−f(x,y)−(f(x,(y−1)+1)−f(x,y−1))\\=f(x,y+1)+f(x,y−1)−2f(x,y) \]由拉普拉斯算子的定义,两式求和可得:
\[\Delta f= f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)−4f(x,y) \]上面的式子,用在图像里可以看做上,下,左,右的点求和,减去四倍的中心点。
因此,许多资料都说可以用如下拉普拉斯卷积核对图像做卷积:
\[\left[ \begin{matrix} 0 & 1 & 0\\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{matrix} \right] \]5. 应用
5.1 无缝融合(SeamlessClone)
无缝融合是泊松方程求解合成图像像素值的最直观应用。即实现前景图像和背景图像的融合。无缝融合主要分为以下三种:
- 常规融合(Normal Cloning)
引导向量场为源图像梯度,因此源图像的纹理会保留在融合区域;并根据\(\Delta f = div(\mathbf v)\),总结可得如下公式
- 混合融合(Mixed Cloning)
融合区域的纹理(梯度)由源图像和目标图像共同决定。混合融合会选择源图像和目标图像之中的主要纹理(梯度),因此不会产生平滑区域。公式表达如下:
通过适当地将源图像的梯度与目标图像的梯度混合,也可以令人信服地添加透明对象。此外,具有复杂轮廓的物体,包括孔,可以自动添加,而不需要艰苦的切割。
- 单色迁移(Monochrome Transfer)
此时引导向量\(\mathbf v\)是源图像从彩色转成灰度之后的梯度,然后进行normal cloning。这样做的目的是保留源图像的纹理(梯度),丢弃其色彩信息,使得融合区域色彩与目标图像一致。
5.2 选区编辑(Selection editing)
无缝融合处理的问题是将源图像中的区域融合到目标图像中,引导场部分或完全取决于源图像\(g\)的梯度。而选区编辑处理的问题是在单一图像上取出一块区域,对这个区域进行处理,使用完全依赖于原始图像的引导场来定义选区图像的变换。主要形式包括:纹理扁平化、局部明亮变化、局部色彩变化以及无缝拼接。
纹理扁平化和局部明亮变化是通过对原始梯度场\(\nabla f^*\)进行非线性修改实现;背景或前景色修改和无缝拼接是通过就地(in-place)无缝融合实现,原始图片经域内(提供新的源图像)或域外(提供新的边界值)修改后作为源。
-
纹理扁平化(Texture flattening)
利用稀疏筛(sparse sieve)对图像梯度\(\nabla f^*\)进行处理,只保留最突出的特征,具体公式表达为:
\[\mathbf v(\mathbf x) = \mathbf M(\mathbf x) \nabla f^*(\mathbf x), \quad \forall \mathbf x \in \Omega \]其中,\(\mathbf M(\mathbf x)\)为二进制蒙板,用于开启稀疏筛区域。
当\(\mathbf M(\mathbf x)\)可以为边缘检测器时,通过仅保留边缘位置处的梯度,可以冲洗所选区域的纹理,使其内容具有平坦的外观。 在Opencv中,这里使用Canny边缘检测算法。
边缘检测器选取的边缘越少(选择性越强),边缘映射就越稀疏,扁平化效果就越明显。
-
局部明亮变化(Local illumination changes)
通过使用合适的狄利克雷边界条件,上述方法应用于校正选区内图像。引导场在对数域(log-domain)上定义为
\[\mathbf v = \alpha^\beta | \nabla f^*|^{-\beta}\nabla f^* \]其中,\(\alpha = 0.2|\nabla f^*|_{avg}\)(在\(\Omega\)域上\(f^*\)的平均梯度范数的0.2倍),\(\beta = 0.2\)
该功能主要是对区域内明亮变换明显的地方做修改;\(\beta\)越大,图片平滑越多,\(\alpha\)越大,越接近原图细节;对于加亮曝光不足的区域比较有效果。
-
局部色彩变化(Local color changes)
给定原始图像和选区($ \Omega$域),利用无缝融合修改原始图像的前景色和背景色:
(1)修改背景色:给定$ \Omega\(域外目标函数\)f*$,例下图中间:通过将选区外图像进行去色处理,得到的图像作为$f*$
(2)修改前景色:给定$ \Omega\(域内源函数\)g\(,例下图右侧:将选区内图像**各通道灰度值**分别乘以1.5、0.5、0.5,得到的图像作为\)g$(后面三个参数是r、g、b三个通道的乘数因子,在0.5-2.5之间。值越大起到锐化的作用)
-
无缝拼接(seamless tiling)
选区($ \Omega\(域)为矩形,原始图像选区内图像作为源图像\)g\(,进行泊松方程求解。其中边界条件由\)g$的边界值导出:
\[f^*_{north} = f^*_{south} = \frac{g_{north}+g_{south}}{2} \\ f^*_{west} = f^*_{east} = \frac{g_{west}+g_{east}}{2} \]
6. 代码复现——基于python的opencv实现
6.1 常规融合
1. 思路
-
计算源图像在mask图像的区域内的散度
-
求出mask图像区域的中心点,得到目标图像中融合的位置
-
逐通道求解出融合后的像素值。对于每一个通道,需要构造A系数矩阵和b向量,然后再利用\(A\mathbf x = b\)求解出该通道的\(\mathbf x\)。具体思路为:
-
找到要求解的像素个数N,并对每个像素\((x,y)\)建立\([0,N-1]\)个索引(因为A系数矩阵是根据索引求构造的)。
-
对于要求解的每个像素\((x,y)\)对应的索引\(i\),当前\(A[i,i] = -4\), \(b[i] =\) 该像素的二阶微分值
-
判断邻居上下左右四个位置的像素是否在mask内,如果在mask内,则令\(A[i,该邻居像素索引] = 1\),如果不在mask内,直接让\(b[i]\)减去该像素的目标图像像素值(因为如果该邻居像素不在mask内,说明是边界像素,像素值是已知的,不需要求系数,因此可以直接移到方程右边,即b[i])
-
根据构造的A和b进行矩阵运算,求解出\(\mathbf x\)
-
将\(\mathbf x\)复制到目标图像中的对应像素位置
-
-
合并三个通道,得到最后的图像
2. 代码实现
我学习了opencv中seamlessClone函数的源码,但是源码是基于C++的openGL实现的,我借鉴了一些实现方法,并结合我的思路用python的opencv实现seamlessClone函数
-
主函数
输入源图像、目标图像、mask图像、融合位置的中心点,调用seamlessClone函数,输出融合图像。
src = cv.imread(r"E:\CG\Code\possionImageEditing\normalCloning\1.jpg") dst = cv.imread(r"E:\CG\Code\possionImageEditing\normalCloning\2.jpg") mask = cv.imread(r"E:\CG\Code\possionImageEditing\normalCloning\mask.jpg") mask = mask[:,:,0] center = (180,190) result = Possion.seamlessClone(src,dst,mask,center,0) cv.imshow('result', result) cv.waitKey(0)
-
seamlessClone函数
封装在Poisson类,函数的参数是源图像、目标图像、mask图像、融合位置的中心点、融合类型(0表示常规融合)
def seamlessClone(src,dst,mask,center,flag): # 1. 求源图像的散度 laplacian = cv.Laplacian(np.float64(src),ddepth=-1,ksize=1) # 2. 计算mask区域的范围 loc = np.nonzero(mask) xbegin, xend, ybegin, yend = np.min(loc[0]) - 1, np.max(loc[0]) + 1, np.min(loc[1]) - 1, np.max(loc[1]) + 1 # 3. 根据mask区域范围截取出对应的mask、src、lap cutMask = mask[xbegin:xend, ybegin:yend] cutSrc = src[xbegin:xend, ybegin:yend] cutLap = laplacian[xbegin:xend, ybegin:yend] # 4. 为了方便后面计算复制的位置,将mask、laplacian、src变成和dst一样大 finalMask = np.zeros((dst.shape[0], dst.shape[1])) # 只复制长和宽,单通道 finalSrc = np.zeros_like(dst) finalLap = np.zeros_like(dst, dtype=np.float64) # 5. 根据中心点,得到融合的x、y范围 xmid = (np.max(loc[0]) - np.min(loc[0])) // 2 + 1 xbegin = center[0] - xmid xend = center[0] + cutMask.shape[0] - xmid ymid = (np.max(loc[1]) - np.min(loc[1])) // 2 + 1 ybegin = center[1] - ymid yend = center[1] + cutMask.shape[1] - ymid # 复制到对应位置 finalMask[xbegin:xend, ybegin:yend] = cutMask finalSrc[xbegin:xend, ybegin:yend] = cutSrc finalLap[xbegin:xend, ybegin:yend] = cutLap # 逐通道求解 result = [getX(a, finalMask, b) for a, b in zip(cv.split(dst), cv.split(finalLap))] # 合并三个通道 final = cv.merge(result) return final
-
getX函数
输入的src和dst为单通道,函数功能用于构造A和b,再利用scipy.sparse求解线性方程组,得出x
def getX(dst, mask, lap): # 1 计算求解的像素个数 loc = np.nonzero(mask) num = loc[0].shape[0] # 2. 有多少个像素个数则需要多少个方程,需要构造num*num大小的稀疏矩阵和num大小的b向量 # 3. A用lilmatrix,这种矩阵方便逐个添加 A = lil_matrix((num, num), dtype=np.float64) b = np.ndarray((num, ), dtype=np.float64) # 一维数组,长度只有1, 而且最外层的长度为num # 4. 要将每个像素映射到0~num-1的索引之中,因为A系数矩阵也是根据索引求构造的 hhash = {(x, y): i for i, (x, y) in enumerate(zip(loc[0], loc[1]))} # 用于找上下左右四个像素位置 dx = [1,0,-1,0] dy = [0,1,0,-1] # 5. 构造A系数矩阵和b向量 for i, (x, y) in enumerate(zip(loc[0],loc[1])): A[i, i] = -4 b[i] = lap[x, y] p = [(x + dx[j], y + dy[j]) for j in range(4)] # p为上下左右的像素位置 for j in range(4): if p[j] in hhash: # 像素位置在mask内 A[i, hhash[p[j]]] = 1 else: b[i] -= dst[p[j]] # 由于A是稀疏矩阵,可以将lilmatrix转成cscmatrix,方便矩阵运算 A = A.tocsc() # 6. 求解X X = linalg.splu(A).solve(b) # 7. 将X复制到对应位置 result = np.copy(dst) for i, (x, y) in enumerate(zip(loc[0],loc[1])): if X[i] < 0: X[i] = 0 if X[i] > 255: X[i] = 255 result[x, y] = X[i] return result
3. 实现结果
输入图像:
输出结果:
cv.seamlessClone函数结果:
6.2 混合融合
1. 思路
- 计算在mask图像的区域内的融合散度
- 在这里不能直接用laplacian算子求出散度然后再进行比较,因为公式里面是要求比较的是梯度,因此需要在梯度上进行融合
- 梯度有两个方向:x方向和y方向,每个方向有左、右两个方向的梯度。因此需要对四个方向的梯度进行比较
-
计算源图像、目标图像四个方向的梯度
-
对于每个方向,比较每个像素的源图像梯度和目标图像梯度,取绝对值大的一方作为最终的梯度
-
最后四个方向的梯度累计求和就可以得到最终的散度
-
求出mask图像区域的中心点,得到目标图像中融合的位置
-
逐通道求解出融合后的像素值。对于每一个通道,需要构造A系数矩阵和b向量,然后再利用\(A\mathbf x = b\)求解出该通道的\(\mathbf x\)。其思路同常规融合中的思路
-
合并三个通道,得到最终图像
2. 代码实现
-
主函数
同常规融合的主函数,只是调用seamlessClone函数时最后一个参数为1,表示融合类型为“混合融合”
src = cv.imread(r"E:\CG\Code\possionImageEditing\normalCloning\1.jpg") dst = cv.imread(r"E:\CG\Code\possionImageEditing\normalCloning\2.jpg") mask = cv.imread(r"E:\CG\Code\possionImageEditing\normalCloning\mask.jpg") mask = mask[:,:,0] center = (180,190) result = Possion.seamlessClone(src,dst,mask,center,1) cv.imshow('result', result) cv.waitKey(0)
-
seamlessClone函数
def seamlessClone(src,dst,mask,center,flag): # 1. 计算mask区域的外切矩形 loc = np.nonzero(mask) xbegin, xend, ybegin, yend = np.min(loc[0]) - 1, np.max(loc[0]) + 1, np.min(loc[1]) - 1, np.max(loc[1]) + 1 # 2. 根据mask区域范围截取出对应的mask、src cutMask = mask[xbegin:xend, ybegin:yend] cutSrc = src[xbegin:xend, ybegin:yend] # 3. 为了方便后面计算复制的位置,将mask、laplacian、src变成和dst一样大 finalMask = np.zeros((dst.shape[0], dst.shape[1])) # 只复制长和宽,单通道 finalSrc = np.zeros_like(dst) finalLap = np.zeros_like(dst, dtype=np.float64) # 4. 根据中心点,得到融合的x、y范围 xmid = (np.max(loc[0]) - np.min(loc[0])) // 2 + 1 xbegin = center[0] - xmid xend = center[0] + cutMask.shape[0] - xmid ymid = (np.max(loc[1]) - np.min(loc[1])) // 2 + 1 ybegin = center[1] - ymid yend = center[1] + cutMask.shape[1] - ymid # 5. 复制到对应位置 finalMask[xbegin:xend, ybegin:yend] = cutMask finalSrc[xbegin:xend, ybegin:yend] = cutSrc # 6. 求融合散度 if flag == 1: kernel = [np.array([[0, -1, 1]]),np.array([[1, -1, 0]]), np.array([[0], [-1], [1]]), np.array([[1], [-1], [0]])] # 四个方向梯度的过滤器,分别为x正方向、x反方向、y正方向、y反方向,散度就是在同一个方向进行两次梯度计算 # 求src图、dst梯度 grads = [] for i in range(4): srcGrad = cv.filter2D(np.float64(finalSrc), -1, kernel[i]) dstGrad = cv.filter2D(np.float64(dst), -1, kernel[i]) grads.append(np.where(np.abs(srcGrad) > np.abs(dstGrad), srcGrad, dstGrad)) # 哪个梯度大就取哪个 finalLap = np.sum(grads, axis=0) # 四个梯度求和,即两个散度求和,得到最终的散度 # 7. 逐通道求解 result = [getX(a, finalMask, b) for a, b in zip(cv.split(dst), cv.split(finalLap))] # 8. 合并三个通道 final = cv.merge(result) return final
3. 实现结果
输入图像:
输出结果:
- 可以看出,在这个融合例子上,混合融合的效果是更好的,因为混合融合可以保留目标图像的一些突出纹理
cv.seamlessClone函数实现结果:
- 对比之下cv自带的函数的融合结果略差于自己实现的函数融合结果
6.3 单色迁移
1. 思路
- 单色迁移的思路基本等同于常规融合,只是需要在第一步将原图像转换为灰度图,然后再进行常规融合
2. 代码实现
-
主函数
因为单色迁移的思路和无缝融合操作大致一致,因此像opencv也是把它放在同一个API中。这里调用seamlessClone函数时最后一个参数为2,表示“单色迁移”
src = cv.imread(r"E:\CG\Code\possionImageEditing\monochromeTransfer\1.jpg") dst = cv.imread(r"E:\CG\Code\possionImageEditing\monochromeTransfer\2.jpg") mask = cv.imread(r"E:\CG\Code\possionImageEditing\monochromeTransfer\mask.jpg") mask = mask[:,:,0] center = (166,117) result = Possion.seamlessClone(src,dst,mask,center,2) cv.imshow('result', result) cv.waitKey(0)
-
seamlessClone函数
def seamlessClone(src,dst,mask,center,flag): # 1. 将原图转为灰度图 if flag == 2: src = cv.cvtColor(src, cv.COLOR_BGR2GRAY) laplacian = cv.Laplacian(np.float64(src),ddepth=-1,ksize=1) loc = np.nonzero(mask) # 2. 取出mask的位置,一并修改src、lap只剩要修改的区域 xbegin, xend, ybegin, yend = np.min(loc[0]) - 1, np.max(loc[0]) + 1, np.min(loc[1]) - 1, np.max(loc[1]) + 1 cutMask = mask[xbegin:xend, ybegin:yend] cutSrc = src[xbegin:xend, ybegin:yend] cutLap = laplacian[xbegin:xend, ybegin:yend] # 3. 为了方便后面计算复制的位置,将mask、laplacian、src变成和dst一样大 finalMask = np.zeros((dst.shape[0], dst.shape[1])) # 只复制长和宽,单通道 # 因为src是灰度图,所以是单通道 if flag == 2: finalSrc = np.zeros((dst.shape[0], dst.shape[1])) finalLap = np.zeros((dst.shape[0], dst.shape[1]),dtype=np.float64) # 逐通道求解 result = [getX(a, finalMask, b) for a, b in zip(cv.split(dst), cv.split(finalLap))] final = cv.merge(result) return final
3. 实现结果
输入图:
输入图像:
输出结果:
- 蓝色区域就是单色迁移的结果,只保留了纹理,没有保留原图的色彩信息
cv.seamlessClone函数实现结果:
6.4 纹理扁平化
1. 思路
-
方案一:
- 利用canny算子检测边缘。边缘点值为255,非边缘点为0
- 计算原图像的散度
- 遍历整个canny边缘图像,当像素不是边缘点时,修改其对应像素的散度值为0
- 逐通道构建\(A\mathbf x = b\)求解像素值,目标图像也是原图像,该步骤思路同常规融合的求解思路
如果只是修改散度,则代码实现如下:
def textureFlattening(src, mask, low, high): # low、high分别是Canny边缘检测器低阈值和高阈值
# 1. 利用canny算子检测边缘。边缘点值为255,非边缘点为0
canny = cv.Canny(src, low, high)
# 2. 计算src的散度
laplacian = cv.Laplacian(np.float64(src),ddepth=-1,ksize=1)
# 3. 当像素不是边缘点时,修改其散度值为0
for i in range(canny.shape[0]):
for j in range(canny.shape[1]):
if canny[i][j] == 0:
laplacian[i][j] = 0
# 4. 逐通道求解,目标图像也是src
ret = [getX(d, mask, l) for d, l in zip(cv.split(src), cv.split(laplacian))]
retImg = cv.merge(ret)
return retImg
如上图为实现的结果。可以看出这个方法不能实现纹理扁平化,通过观察结果矩阵发现像素的变化很大,我猜测是直接利用边缘点则直接将散度赋为0导致的。边缘点的影响可能逐步影响到梯度,从而影响散度;某个方向梯度为0,不代表散度就为0。
-
方案二:
-
利用canny算子检测边缘。边缘点值为255,非边缘点为0
-
计算原图像四个方向的梯度
-
检测四个方向的边缘点
计算出来的canny没有方向性,因此需要像计算原图像梯度一样去计算四个方向的边缘点
这里用到的核是:
\[\left[ \begin{matrix} 0 & 1 & 0\\ 1 & 4 & 1 \\ 0 & 1 & 0 \end{matrix} \right] \]因为canny边缘图像中,边缘点为255,非边缘点为0。因此,某一像素的值与其方向上的像素值求和,就可以得出该像素是否为边缘点。此时如果该像素点为0,说明在该方向上项链像素都为0,其他情况(255,255)(0,255)(255,0)都可以统计出该点为边缘点
以下面的矩阵为例,255为边缘点,0为非边缘点。
计算向下方向的边缘点,可以得到下图的矩阵。
说明蓝色圈内在向下方向都属于边缘点。
-
遍历整个canny边缘图像,当像素在某一方向不是边缘点时,修改其梯度值为0
-
合并梯度,得到src的散度
-
逐通道构建\(A\mathbf x = b\)求解像素值,目标图像也是原图像,该步骤思路同常规融合的求解思路
-
2. 代码实现
-
主函数
输入源图像、目标图像、mask图像,调用textureFlattening函数,设置canny阈值范围阈值为[80,120],输出结果图像。
import cv2 as cv import PossionImageEditing.Possion as Possion src = cv.imread(r"E:\CG\Code\possionImageEditing\textureFlattening\1.jpg") mask = cv.imread(r"E:\CG\Code\possionImageEditing\textureFlattening\mask.jpg") result = Possion.textureFlattening(src,mask,80,120) cv.imshow('result', result) cv.waitKey(0)
-
textureFlattening函数
def textureFlattening(src, mask, low, high): # low、high分别是Canny边缘检测器低阈值和高阈值 # 用于计算src四个方向梯度的核 kernel = [np.array([[0, -1, 1]]), np.array([[1, -1, 0]]), np.array([[0], [-1], [1]]), np.array([[1], [-1], [0]])] # 用于检测四个方向边缘点的核 kernelEdge = [np.array([[0, 1, 1]]), np.array([[1, 1, 0]]), np.array([[0], [1], [1]]), np.array([[1], [1], [0]])] # 1. canny检测边缘 canny = cv.Canny(src, low, high) # 2. 检测四个方向边缘点 edges = [cv.filter2D(canny, -1, kernelEdge[i]) for i in range(4)] # 3. 计算src四个方向梯度 grads = [cv.filter2D(np.float64(src), -1, kernel[i]) for i in range(4)] # 4. 不是边缘点的将梯度修改为0 for i in range(4): for j in range(edges[i].shape[0]): for k in range(edges[i].shape[1]): if edges[i][j][k] == 0: grads[i][j][k] = 0 # 5. 计算src的散度 laplacian = np.sum(grads, axis=0) # 6. 逐通道求解 ret = [getX(d, mask, l) for d, l in zip(cv.split(src), cv.split(laplacian))] retImg = cv.merge(ret) return retImg
3. 实现结果
输入图像:
输入图像:
输出结果:
cv.textureFlattening函数实现结果:
6.5 局部明亮变化
1. 思路
- 计算原图像的散度
- 将选区外图像进行去色处理:只保留mask区域的散度,将非mask区域的散度修改为0
- 修改前景色的亮度:根据重新\(\mathbf v = \alpha^\beta | \nabla f^*|^{-\beta}\nabla f^*\)计算散度
- 逐通道利用\(A \mathbf x = b\)求解像素值
2. 代码实现
-
主函数
输入源图像、mask图像,调用illuminationChange函数,设置\(\alpha\)和\(\beta\)的值分别为0.1和0.2,输出结果图像。
import cv2 as cv import PossionImageEditing.Possion as Possion src = cv.imread(r"E:\CG\Code\possionImageEditing\illuminationChange\1.jpeg") mask = cv.imread(r"E:\CG\Code\possionImageEditing\illuminationChange\mask.jpg") mask = mask[:,:,0] result = Possion.illuminationChange(src, mask, 0.1, 0.2) cv.imshow('result', result) cv.waitKey(0)
-
illuminationChange函数
def illuminationChange(src, mask, alpha, beta): # 1. 计算src的散度 laplacian = cv.Laplacian(np.float64(src), -1, ksize = 1) # 2. 将选区外图像进行去色处理:只保留mask区域的散度,将非mask区域的散度修改为0 for i in range(mask.shape[0]): for j in range(mask.shape[1]): if mask[i][j] == 0: laplacian[i][j] = 0 # 3. 修改前景色的亮度:根据公式重新计算散度 laplacian = laplacian * (alpha ** beta * np.log(np.linalg.norm(laplacian)) ** (-beta)) # 4. 逐通道求解 result = [getX(a, mask, b) for a, b in zip(cv.split(src), cv.split(laplacian))] final = cv.merge(result) return final
3. 实现结果
输入图像:
输出结果:
cv.illuminationChange函数实现结果:
6.6 局部色彩变化
1. 思路
- 分离出原图像的r、g、b三个通道,并根据参数计算新的rgb的值得到新的图像
- 计算新图像的散度,此时该散度保留了新图像的色彩信息
- 根据新图像的散度,逐通道利用\(A\mathbf x = b\)求解mask区域内的像素值
2. 代码实现
-
主函数
输入源图像、mask图像,将源图像继续BGR到RGB的转换,调用colorChange函数,设置r、g、b的参数分别为1.5和0.5和0.5,得到的结果再转换回去BGR,输出最终图像。
import cv2 as cv import PossionImageEditing.Possion as Possion src = cv.imread(r"E:\CG\Code\possionImageEditing\colorChange\1.jpeg") mask = cv.imread(r"E:\CG\Code\possionImageEditing\colorChange\mask.jpg") mask = mask[:,:,0] retImg = Possion.colorChange(cv.cvtColor(src, cv.COLOR_BGR2RGB), mask, 1.5, 0.5, 0.5) retImg = cv.cvtColor(retImg, cv.COLOR_RGB2BGR) cv.imshow('result', retImg) cv.waitKey(0)
如果没有进行RGB的转换,即代码如下,会出现颜色变化不一致的结果
import cv2 as cv import PossionImageEditing.Possion as Possion src = cv.imread(r"E:\CG\Code\possionImageEditing\colorChange\1.jpeg") mask = cv.imread(r"E:\CG\Code\possionImageEditing\colorChange\mask.jpg") mask = mask[:,:,0] retImg = Possion.colorChange(src, mask, 1.5, 0.5, 0.5) cv.imshow('result', retImg) cv.waitKey(0)
-
colorChange函数
def colorChange(src, mask, red, green, blue): # 1. 分离出r、g、b三个通道 r, g, b = cv.split(src) # 2. 重新计算r、g、b的值,并合并成新的图像 newSrc = cv.merge((r * red, g * green, b * blue)) # 3. 计算新图像的散度 laplacian = cv.Laplacian(np.float64(newSrc), -1, ksize=1) # 4. 根据新图像的散度,逐通道求解mask区域内的像素值 result = [getX(a, mask, b) for a, b in zip(cv.split(src), cv.split(laplacian))] final = cv.merge(result) return final
3. 实现结果
输入图像:
输出结果:
cv.colorChange函数实现结果:
6.7 无缝拼接
1. 思路
-
根据公式计算边界的像素值,具体为:
新的边界值 = (原本的像素值 + 对称边界的像素值)/ 2
-
计算源图像的散度
-
构造mask图像,此时mask图像应是整个图像的矩形区域
-
逐通道求解像素值:遍历每个通道的每个像素
- 如果该像素是边界点,则\(b[i]\)为融合的边界值,\(A[i,i]=1\),因此相当于新的图像的边界的像素值为融合的边界值
- 如果像素不是边界点,则\(b[i]\)为该像素的散度值,\(A[i,i] = 4\),\(A[i,邻居] = 1\)
2. 代码实现
-
主函数
输入源图像,调用seamlessTiling函数,输出结果为边界进行了无缝处理的图像,再调用np.tile拼接图像。并对比没有进行边界无缝处理的图像拼接。
import numpy as np import cv2 as cv import PossionImageEditing.Possion as Possion src = cv.imread(r"E:\CG\Code\possionImageEditing\seamlessTiling\1.jpg") result = Possion.seamlessTiling(src.astype(np.float64)) result_img = np.tile(result, (2, 2, 1)) cv.imwrite("res.jpg",result_img) ori_img = np.tile(src, (2, 2, 1)) cv.imshow('orignal',ori_img) cv.waitKey(0)
-
seamlessTiling函数
def seamlessTiling(src): # 1. 融合的mask是整个图像矩形区域 mask = np.ones((src.shape[0], src.shape[1])) # 2. 求出边界的位置 boundary_mask = np.ones((src.shape[0], src.shape[1])) boundary_mask[np.ix_(np.arange(1, src.shape[0] - 1), np.arange(1, src.shape[1] - 1))] = 0 _boundary = np.nonzero(boundary_mask) boundary = {(x, y): i for i, (x, y) in enumerate(zip(_boundary[0], _boundary[1]))} loc = np.nonzero(mask) num = loc[0].shape[0] # 需要求解的像素个数 A = lil_matrix((num, num), dtype=np.float64) hhash = {(x, y): i for i, (x, y) in enumerate(zip(loc[0], loc[1]))} # 找上下左右四个像素位置 dx = [1, 0, -1, 0] dy = [0, 1, 0, -1] # 3. 逐通道求解A矩阵和b向量 result = [] for _src in cv.split(src): # 3.1 求解原图散度 _lap = compute_laplacian(_src) # 3.2 计算边界新的像素值 tmp = np.zeros_like(_src) tmp[0] = (_src[0] + _src[-1]) * 0.5 tmp[-1] = (_src[0] + _src[-1]) * 0.5 tmp[:, 0] = (_src[:, 0] + _src[:, -1]) * 0.5 tmp[:, -1] = (_src[:, 0] + _src[:, -1]) * 0.5 b = np.ndarray((num,), dtype=np.float64) # 3.3 遍历图像每个像素 for i, (x, y) in enumerate(zip(loc[0], loc[1])): # 如果像素是边界点,则b[i]为融合的边界值,A[i,i]=1,因此相当于新的图像的边界的像素值为融合的边界值 if (x, y) in boundary: b[i] = tmp[x, y] A[i, i] = 1 # 如果像素不是边界点,则b[i]为散度值,A[i,i] = 4,A[i,邻居] = 1 else: b[i] = _lap[x, y] A[i, i] = -4 p = [(x + dx[j], y + dy[j]) for j in range(4)] # p为上下左右的像素位置 for j in range(4): A[i, hhash[p[j]]] = 1 A = A.tocsr() # 3.4 求解像素值 X = linalg.spsolve(A, b) # 3.5 将X复制到对应位置 res = np.zeros_like(_src) for i, (x, y) in enumerate(zip(loc[0], loc[1])): if X[i] < 0: X[i] = 0 if X[i] > 255: X[i] = 255 res[x, y] = X[i] result.append(res) # 4. 合并三个通道 final = cv.merge(result) return final
3. 实现结果
输入图像:
输出结果:
直接拼接结果:
7. 思考的问题
-
为什么要用梯度?
图像梯度的最重要性质是梯度可以用来反映图像中亮度改变最明显的区域,也就是说可以用梯度来捕捉图像上的亮度变化,梯度的方向在图像灰度的最大变化率上,它恰好可以反映出图像边缘的灰度变化。
-
拉普拉斯算子\(\Delta f\)对图片的影响有什么?
-
拉普拉斯算子大多是用于检测边缘。边缘处的二阶导函数是过零点,即:左右符号相反的点。注意:单单是为零的点,不足以判断边缘。因为,平滑图像一阶,二阶倒数也可能为零。
-
对于高梯度的图片,由拉普拉斯算子提取的二阶变化在感知上是最显著的
-
\(f\)和\(f^*\)是什么函数?
是关于x和y的函数
-
为什么最小值问题的解是具有狄利克雷边界条件的拉普拉斯方程的唯一解?(以\(\underset {f}{min} \underset{\Omega}{\iint} || \nabla f||^2,f|_{\partial \Omega}=f^*|_{\partial \Omega}\)为例,其解是\(\Delta f=0\) over \(\Omega\) with $ f|{\partial \Omega}=f^*|{\partial \Omega}$的唯一解)
-
证明:由\(min\)式可得要求的函数是能量泛函(微分的范数平方再积分),因此可以用欧拉-拉格朗日方程求解能量泛函数的极值。
\(f\)是在被积函数中,此时被积函数是\(F=|| \nabla f||^2=f_x^2+f_y^2\),代入二维拉格朗日方程,可得:
化简得:
\[\frac{d}{dx}(\frac{\partial F}{\partial f_x})=\frac{d}{dx}(2f_x)=2\frac{\partial^2 f}{\partial x^2} \] 因此,
\[\frac{\partial^2 f}{\partial x^2}+\frac{\partial^2 f}{\partial y^2}=\Delta f=0 \]-
泊松方程与拉普拉斯方程
有点像热方程,就是钳住了边界,并且知道边界发生了什么,然后想让它扩散到中心的过程,并且使这些梯度的约束看起来尽可能好
- 泊松方程和拉普拉斯方程的区别在于右边是否为0
- 已知图像点二阶微分值(散度div(V)),求解各个图像点的像素值——泊松方程\(\Delta f = div(V)\)求解过程
-
假设有n个未知像素值,应用拉普拉斯卷积核后可得到x条方程式(都是关于完全在\(\Omega\)区域内建立的方程,即上下左右四个点都在\(\Omega\)区域内,所以x<n)但是要求出全部\(f\)中的未知数是不够的,要对边界值进行确定,此时利用的是狄利克雷边界条件,即给出边界处函数在边界处的实际值。
-
针对完全在Mask图像内的像素(上下左右都在mask内)
\[-4f(x,y)+f(x+1,y)+f(x-1,y)+f(x,y+1)+f(x,y-1) =div(V) \] -
针对部分在Mask图像内的像素(与边界相邻的像素)
\[-4f(x,y)+f(x+1,y)+S(x-1,y)+f(x,y+1)+S(x,y-1)=div(V) \] 其中,\(S(x,y)\)为目标图像的像素值
-
-
矩阵化表示此方程组之后,得到形式为\(Ax=b\)。\(A\)是构建的系数矩阵,\(b\)是散度值,x是融合图像的像素值,求解得到\(x\)
参考文献
-
cv2.Laplacian与cv2.filter2d -不同的结果 https://cloud.tencent.com/developer/ask/sof/904647
-
OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例 https://blog.csdn.net/LaoYuanPython/article/details/123333112
-
文献阅读 - Poisson Image Editing https://blog.csdn.net/zhaoyin214/article/details/88196575
-
图像处理(十二)图像融合(1)Seamless cloning泊松克隆-Siggraph 2004 https://blog.csdn.net/hjimce/article/details/45716603
-
拉普拉斯算子 https://xudeyu.github.io/2019/03/31/laplace-operator.html
-
opencv的API文档
https://docs.opencv.org/4.x/dc/d81/photo_8hpp.html
https://docs.opencv.org/4.x/df/da0/group__photo__clone.html#gad55df6aa53797365fa7cc23959a54004
- opencv seamlessClone源码 https://github.com/opencv
标签:src,Image,mask,像素,Poisson,图像,np,Editing,cv 来源: https://www.cnblogs.com/3-louise-wang/p/16671316.html