美颜磨皮算法之保边(双边&引导)滤波器原理及 Python 实现

保边滤波是对图像操作后,不会模糊边缘的部分(如下图所示),属于非线性的滤波方法,常见的保边滤波有双边滤波和引导滤波,典型应用场景是去噪,磨皮,扣图

本文介绍两种保边滤波器,分别是双边滤波器和导向滤波器。并且提供对应的 python 实现源码。关于公式推导,尾部有参考链接,这里只会给出结论和大致原理。

双边滤波


先来看看双边滤波器的计算公式

B i l a t e r a l F i l t e r ( i , j ) = ∑ k , l f ( k , l ) ∗ w ( i , j , k , l ) ∑ k , l w ( i , j , k , l ) BilateralFilter(i,j)=\frac{\sum_{k,l}f(k,l)*w(i,j,k,l)}{\sum_{k,l}w(i,j,k,l)} BilateralFilter(i,j)=k,lw(i,j,k,l)k,lf(k,l)w(i,j,k,l)

其中,f 表示像素在某一点处的像素值,而 w 则是该像素点需要乘于的权重,关于分母的求和是对权重的归一化。(k, l) 为模板窗口中心坐标,(i, j) 为模板窗口其他系数的坐标。

双边滤波能够保边的原因在于 w 权重的计算方式,他由两部分组成

w ( i , j , k , l ) = d ( i , j , k , l ) ∗ r ( i , j , k , l ) w(i,j,k,l)=d(i,j,k,l)*r(i,j,k,l) w(i,j,k,l)=d(i,j,k,l)r(i,j,k,l)

  • d 计算空间距离,离模板窗口中心越远,权重越小,反之不然
  • r 计算颜色的相似度,颜色相差越大,则越可能为边缘,权重越小,反之不然

美颜磨皮算法之保边(双边&引导)滤波器原理及 Python 实现_第1张图片

d 是高斯核(Gaussian Kernel),两个像素物理距离越大,则权值越小,距离越小则权值越大,这里的 σ 代表像素距离的标准差值,设置的越大,越多像素则就会被划分为距离不大,从而获得较大的权重,边缘就会被保留,反之不然。输入的图像如果是归一化的,取值范围为 0~1。

d ( k , l , i , j ) = e x p ( − ( i − k ) 2 + ( j − l ) 2 2 σ 2 ) d(k,l,i,j)=exp(-\frac{(i-k)^2+(j-l)^2}{2\sigma^2}) d(k,l,i,j)=exp(2σ2(ik)2+(jl)2)

r 值则是由像素之间的差值决定的(Range Kernel),如果两个像素相差越小则越不可能是边缘,权值越大,反之不然

r ( k , l , i , j ) = e x p ( − ∣ ∣ f ( k , l ) − f ( i , j ) ∣ ∣ 2 2 σ 2 ) r(k,l,i,j)=exp(-\frac{||f(k,l)-f(i,j)||^2}{2\sigma^2}) r(k,l,i,j)=exp(2σ2f(k,l)f(i,j)2)

其中,这两个式子中,(i,j) 表示当前处理像素点的坐标,而(k,l)则是模板中心点坐标。这里的 σ 代表像素相似性的标准差值,设置的越大,越多像素值就会被划分为相似,从而边缘就会被模糊掉,反之不然。输入图像如果是归一化的,取值范围为 0~1。

Python 实现

由于每个像素点都要计算权重模板,所以比较耗时,有一些优化方法,后续文章再展开讲

import os, cv2
import math
import numpy as np

def gaussian_kernel(winsize, gsigma):
    r = winsize // 2
    c = r
    denominator = 2 * gsigma * gsigma
    kernel = np.zeros((winsize, winsize))
    for i in range(-r, r + 1):
        for j in range(-c, c + 1):
            kernel[i + r][j + c] = math.exp(-(i * i + j * j) / denominator)
    return kernel

def bilateral_filter(img, winsize, gsigma, ssigma):
    """
    :param img: 输入图像(归一化)
    :param winsize:
    :param gsigma: 
    :param ssigma:
    :return: 处理后的图像(归一化)
    """
    gkernel = gaussian_kernel(winsize, gsigma)
    radius = winsize // 2
    # denominator = 2 * ssigma * ssigma
    img = np.pad(img, ((radius, radius), (radius, radius), (0, 0)), mode="constant", constant_values=0)
    h, w, c = img.shape
    result = np.zeros((h, w, c))
    for i in range(c):
        iimg = img[:, :, i]
        for ri in range(radius, h - radius):
            for ci in range(radius, w - radius):
                # 根据下标获得该处理模板区域内的像素值
                start_x, end_x = ci - radius, ci + radius
                start_y, end_y = ri - radius, ri + radius
                region = iimg[start_y:end_y + 1, start_x:end_x + 1]
                # 计算像素相似度
                similarity_weight = np.exp(-0.5 * np.power(region - iimg[ri, ci], 2.0) / math.pow(ssigma, 2))
                # 最终核模板值
                weight = similarity_weight * gkernel
                # 归一化
                weight = weight / np.sum(weight)
                result[ri, ci, i] = np.sum(region * weight)
    return result**

引导滤波


引导滤波是由何凯明大佬设计的一个新的保边滤波器,该滤波接收一张原图和引导图,处理后的图片与原图相似,但是在纹理部分则与引导图相似,简单来说引导图是原图的梯度图,他在原图边缘处梯度值大,平滑处梯度值小。他与双边滤波相似,但是可以优化成一个与半径无关的算法,因此计算效率上更加高。此外,引导滤波能够解决双边滤波中只保留边缘不保留梯度(渐变图)的问题。

引导滤波是如何保边的?

首先,导向滤波的一个重要的假设是在滤波窗口上存在线性的关系,如下式子,I 是导向图

q i = a k I i + b k q_{i} = a_{k}I_{i}+b_{k} qi=akIi+bk

且我们认为输入图像 p 是由输出图像 q 加上噪音 n 所构成

p i = q i + n i p_{i}=q_{i}+n_{i} pi=qi+ni

最终我们希望输出的图像 q 和输入图像 p 之间差距尽可能小,以此来求解相应的 ak 和 bk。

a r g m i n ( q i − p i ) 2 = a r g m i n ( a k I k + b k − p i ) 2 argmin(q_{i}-p_{i})^2=argmin(a_{k}I_{k}+b_{k}-p_{i})^2 argmin(qipi)2=argmin(akIk+bkpi)2

这个方程式最后的解的推到可以看尾部的参考链接,这里不展开讲。

上述方程的解可以作为如下表示:

a k = I k p k − u k p k σ k 2 + ϵ a_{k}=\frac{I_{k}p_{k}-u_{k}p_{k}}{\sigma_{k}^{2}+\epsilon} ak=σk2+ϵIkpkukpk

b k = ( 1 − a k ) p k b_{k}=(1-a_{k})p_{k} bk=(1ak)pk

这里,Pk 是原图,ϵ 用来界定平滑和边缘区域的阈值

考虑以下两种情况:

Case 1: 平坦区域,方差会远小于 ϵ,从而 a 小于 ϵ,相当于对该区域作均值滤波器

Case 2: 高方差区域,也就是边缘区域,方差很大,a 会趋向于 1,相当于在区域保持原有的梯度

直观点,可视化 ak 和 bk 的图如下:
美颜磨皮算法之保边(双边&引导)滤波器原理及 Python 实现_第2张图片

对于原图边缘部分,ak 对应的像素点趋向于白色,也就是值为 1,而 bk 对应的颜色则趋向于 0,反之不然

关于最后导向滤波公式是如何推到的可以看文章尾部的参考博文,里面每一步都有详细的推到

Python 实现

# eps 可以取 0.01,0.001 等,具体的自己测试一下吧
def guideFilter(I, p, winSize, eps, s):
    
    #输入图像的高、宽
    h, w = I.shape[:2]
    
    #缩小图像
    size = (int(round(w*s)), int(round(h*s)))
    
    small_I = cv2.resize(I, size, interpolation=cv2.INTER_CUBIC)
    small_p = cv2.resize(I, size, interpolation=cv2.INTER_CUBIC)
    
    #缩小滑动窗口
    X = winSize[0]
    small_winSize = (int(round(X*s)), int(round(X*s)))
    
    #I的均值平滑
    mean_small_I = cv2.blur(small_I, small_winSize)
    
    #p的均值平滑
    mean_small_p = cv2.blur(small_p, small_winSize)
    
    #I*I和I*p的均值平滑
    mean_small_II = cv2.blur(small_I*small_I, small_winSize)
    
    mean_small_Ip = cv2.blur(small_I*small_p, small_winSize)
    
    #方差
    var_small_I = mean_small_II - mean_small_I * mean_small_I #方差公式
    
    #协方差
    cov_small_Ip = mean_small_Ip - mean_small_I * mean_small_p
   
    small_a = cov_small_Ip / (var_small_I + eps)
    small_b = mean_small_p - small_a*mean_small_I
    
    #对a、b进行均值平滑
    mean_small_a = cv2.blur(small_a, small_winSize)
    mean_small_b = cv2.blur(small_b, small_winSize)
    
    #放大
    size1 = (w, h)
    mean_a = cv2.resize(mean_small_a, size1, interpolation=cv2.INTER_LINEAR)
    mean_b = cv2.resize(mean_small_b, size1, interpolation=cv2.INTER_LINEAR)
    
    q = mean_a*I + mean_b
    
    return q

参考链接

双边滤波原理

https://blog.csdn.net/qq_42261630/article/details/109538270

导向滤波原理

https://blog.csdn.net/edogawachia/article/details/78872932

https://blog.csdn.net/pi9nc/article/details/26592377

https://blog.csdn.net/qq_40755643/article/details/83831071

导向滤波公式推导

https://blog.csdn.net/weixin_43194305/article/details/88959183

你可能感兴趣的:(图形图像算法,python,深度学习,机器学习,图像处理)