我们知道对于滤波器而言,在均值滤波中W时是1/n,n是w中所有像素个数。在高斯平滑中,W服从二维的高斯分布。
但是无论是均值滤波还是高斯滤波,他们都有一个共同的弱点,它们都属于各向同性滤波。
噪声的特点是以其为中心的各个方向上梯度都较大并且相差不多。图像边缘在相对于区域也会出现梯度的越变,但是边缘只有在其法向方向才会出现较大的梯度,在切向方向梯度较小。
因此才会出现各向同性滤波对待噪声和边缘信息采取一致的态度,从而导致噪声被磨皮的同时,图像中边缘,纹理和细节也被抹去了。
引导滤波(导向滤波)是一种图像滤波技术,通过一张引导图,对初始图象(输入图像)进行滤波处理,使得最后的输出图像大体上与初始图象相似,但是,纹理部分与引导图相似。其典型应用有两个:保边图像平滑,抠图。
引导滤波不仅能实现双边滤波的边缘平滑,而且在检测到边缘附件有很好的表现。可应用在图像增强,HDR压缩,图像抠图以及图像去霾等场景。
算法原理:
滤波器的数学公式如下:
其中I是引导图像(guided Image),P是输入的带滤波图像,Q是滤波后的输出图像,W是根据引导图像I来确定加权平均运算中所采取的权值。一般我们选择原彩色图的灰度图作为引导图 I 或者对灰度图进行一些保留边缘的滤波操作再作为引导图。
μk是窗口内像素点的均值,Ii和Ij指相邻两个像素点的值,σk代表窗口内像素点的方差,ε是一个惩罚值。自适应权重可以根据上式分析得到:Ii和Ij在边界两侧时,(Ii-μk)和(Ij-μk)异号,否则,则同号。而异号时的权重值将远远小于同号时的权重值,这样处于平坦区域的像素则会被加以较大的权重,平滑效果效果更明显,而处于边界两侧的像素则会被加以较小的权重,平滑效果较弱,能够起到保持边界的效果。
惩罚值ε对滤波效果影响也很大,当ε值很小时,滤波如前面所述;当ε值很大时,权重的计算公式将近似为一个均值滤波器,平滑效果会更明显。
导向滤波器示意图如下:
由于我们无法知道每一个具体的ni是多少,所以我们尝试通过引导图计算出想要的图。也就是说我们通过引导图I和滤波输出q的线性引导关系才能知道哪些是边缘,哪些是区域。其实就是再求梯度运算时保持了一致:▽q=a▽I
所以也可以从线性滤波的公式角度来看(推导可参考暗通道先验之Guided Filter 导向滤波):
当I=p时公式可以简化为:
此外,在计算每个窗口的线性系数时,我们可以发现一个像素会被多个窗口包含,也就是说,每个像素都由多个线性函数所描述。因此,如之前所说,要具体求某一点的输出值时,只需将所有包含该点的线性函数值平均即可(局部线性模型)如下
cv2.ximgproc.guidedFilter()可以实现这个功能:
cv2.guidedFilter(guide, src, dst, radius, eps[, dDepth])
guide: nparray
guided image (or array of images) with up to 3 channels,
if it have more then 3 channels then only first 3 channels will be used.
src: nparray
filtering image with any numbers of channels.
radius: int
radius of Guided Filter.
eps: float
regularization term of Guided Filter.
{eps}^2 is similar to the sigma in the color space into bilateralFilter().
dDepth: int
optional depth of the output image. 一般取-1
完整代码以及解释:
import argparse # 导包
import cv2
import matplotlib.pyplot as plt
import skimage
import numpy as np
ap=argparse.ArgumentParser() # 创建ArgumentParser对象
ap.add_argument("-i","--image",required=True,help="path to the image") # 通过add_argument()告ArgumentParser如何将命令行中的参数转化成所需要的对象
# 第三个参数什么意思呢?意思你在命令输入python load_display_save.py后面必须加上“-i”或者”–image”
# 第四个参数是帮助参数,就是解释你这个python load_display_save.py -i后面跟的参数是什么,可以看懂在这后面应该跟图片的路径
args=vars(ap.parse_args())
# 这里为什么要是用vars()呢?是为了能够像字典一样访问 ap.parse_args()的值,即现args[“image”]=ap.parse_args()
img=cv2.imread(args["image"]) # cv2.imread()接口读图像,读进来直接是BGR 格式数据格式在 0~255,通道格式为(W,H,C)
#img=img[:,:,::-1]
guide=cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) #用灰度图作为引导
dst1=cv2.ximgproc.guidedFilter(guide=guide,src=img,radius=8,eps=50,dDepth=-1)
dst2=cv2.ximgproc.guidedFilter(guide=guide,src=img,radius=8,eps=200,dDepth=-1)
dst3=cv2.ximgproc.guidedFilter(guide=guide,src=img,radius=8,eps=500,dDepth=-1)
if __name__=='__main__':
cv2.imshow("Img",img) #在窗口显示图像
cv2.imshow("dst1",dst1)
cv2.imshow("dst2",dst2)
cv2.imshow("dst3",dst3)
cv2.waitKey(0) #显示图像必备
cv2.destroyALLWindows() #释放窗口
import argparse
import cv2
import matplotlib.pyplot as plt
import numpy as np
def guidedFilter(I,P,Q,r,eps):
# 转换原图像信息,将输入扩展维64位浮点数
print(I.dtype)
I=I.astype(np.float64)
P=P.astype(np.float64)
assert(I.shape==P.shape) #输入的I和P的形状一定相同
# 各种均值计算
meanI=cv2.boxFilter(I,ksize=(2*r+1,2*r+1),ddepth=-1,normalize=True)
meanP=cv2.boxFilter(P,ksize=(2*r+1,2*r+1),ddepth=-1,normalize=True)
corrI=cv2.boxFilter(I*I,ksize=(2*r+1,2*r+1),ddepth=-1,normalize=True)
corrIP=cv2.boxFilter(I*P,ksize=(2*r+1,2*r+1),ddepth=-1,normalize=True)
#计算相关系数,计算IP的协方差cov和I的方差var
varI=corrI-meanI*meanI
covIP=corrIP-meanI*meanP
#计算系数参数a,b
a=covIP/(varI+eps)
b=meanP-a*meanI
#计算系数a,b的均值
meanA=cv2.boxFilter(a,ksize=(2*r+1,2*r+1),ddepth=-1,normalize=True)
meanB=cv2.boxFilter(b,ksize=(2*r+1,2*r+1),ddepth=-1,normalize=True)
#生成输出矩阵
q=meanA*I+meanB
#转换图片信息,使之便会unit8矩阵
q=q.astype(Q.dtype) # Q.dtype=unit8 我这样写是因为我的np.unit8会报错,所以换了一种方式
return q
ap=argparse.ArgumentParser() # 创建ArgumentParser对象
ap.add_argument("-i","--image",required=True,help="path to the image") # 通过add_argument()告诉ArgumentParser如何将命令行中的参数转化成所需要的对象
# 第三个参数什么意思呢?意思你在命令输入python load_display_save.py后面必须加上“-i”或者”–image”
# 第四个参数是帮助参数,就是解释你这个python load_display_save.py -i后面跟的参数是什么,可以看懂在这后面应该跟图片的路径
args=vars(ap.parse_args())
# 这里为什么要是用vars()呢?是为了能够像字典一样访问 ap.parse_args()的值,即现在 args[“image”]=ap.parse_args()
img=cv2.imread(args["image"]) # cv2.imread()接口读图像,读进来直接是BGR 格式数据格式在 0~255,通道格式为(W,H,C)
#img=img[:,:,::-1]
guide=cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) #用灰度图作为引导
dst1=cv2.ximgproc.guidedFilter(guide=guide,src=img,radius=8,eps=50,dDepth=-1)
dst4=guidedFilter(P=img,I=img,Q=img,r=8,eps=50)
dst2=cv2.ximgproc.guidedFilter(guide=guide,src=img,radius=8,eps=200,dDepth=-1)
dst5=guidedFilter(P=img,I=img,Q=img,r=8,eps=200)
dst3=cv2.ximgproc.guidedFilter(guide=guide,src=img,radius=8,eps=500,dDepth=-1)
dst6=guidedFilter(P=img,I=img,Q=img,r=8,eps=500)
if __name__=='__main__':
cv2.imshow("Salt",img) #在窗口显示图像
cv2.imshow("cv.eps50",dst1)
cv2.imshow("eps50",dst4)
cv2.imshow("cv.eps200",dst2)
cv2.imshow("eps200",dst5)
cv2.imshow("cv.eps500",dst3)
cv2.imshow("eps500",dst6)
cv2.waitKey(0) #显示图像必备
cv2.destroyALLWindows() #释放窗口
参考博客:
OpemCV-Python教程(16)
一文道尽传统去噪方法