

一、引导滤波简介
引导滤波(Guided Filter)是由何凯明(Kaiming He)、孙剑(Jian Sun)和唐晓鸥(Xiaoou Tang)于 2010 年提出的一种边缘保持平滑滤波方法,最早发表于 ECCV 2010 论文'Guided Image Filtering',并在 2013 年 IEEE TPAMI 上给出扩展版本。
引导滤波是一种基于局部线性模型的图像滤波技术,其核心思想是:在一个局部窗口内,假设输出图像是引导图像(可以是输入图像本身,也可以是另一幅图像)的线性变换。通过最小化重建误差并加入正则化项,引导滤波能够在有效去除噪声或平滑纹理的同时,严格保留显著边缘结构。与双边滤波相比,引导滤波不存在梯度反转(gradient reversal)问题,且其计算复杂度为线性时间 O(N),非常适合高分辨率图像和实时应用。由于其稳定性、效率和良好的边缘保持特性,引导滤波被广泛应用于图像去噪、HDR 压缩、图像增强、图像融合、语义分割后处理(如 CRF 替代)以及深度学习中的特征引导平滑等场景。
二、引导滤波原理
2.1 线性模型
引导滤波的本质是在每个滤波窗口内,假设输出图 q 与引导图 I 呈线性关系。当用图像自身作为引导图时(自引导滤波),I 和输入图 p 是同一张图,线性模型为:

参数解析:
ω_k 表示以像素 k 为中心的滤波窗口(如 3×3);
a_k:窗口内的斜率(控制边缘保留程度);
b_k:窗口内的截距(控制平滑程度);
目标:找到最优的 a_k、b_k,让输出图 q 尽可能接近输入图 p(误差最小)。
2.2 斜率和截距的计算
斜率 a_k 和截距 b_k 的计算方法,这里用一个 3×3 窗口的具体像素值来演示 (输入图像 p=I,8 位灰度图)。
假设窗口 ω_k 内的像素值如下 (中心像素 k 为 100):

2.2.1 计算窗口内的 3 个核心统计量
计算斜率 a_k 和截距 b_k,首先需要窗口内的均值和方差,这正是 boxFilter 要干的活。
窗口内像素均值 \bar{I}_k:所有像素的平均值:
\bar{I}_k = \frac{60 + 70 + 80 + 80 + 100 + 120 + 100 + 110 + 130}{9} = \frac{850}{9} ≈ 94.44
窗口内像素平方均值 \bar{I}_k^2:所有像素平方的平均值:
\bar{I}_k^2 = \frac{60^2 + 70^2 + 80^2 + 80^2 + 100^2 + 120^2 + 100^2 + 110^2 + 130^2}{9} = \frac{84700}{9} ≈ 9411.11
窗口内像素与均值的协方差 cov(I,p)_k:因为自引导时 I=p,协方差等于方差 var(I)_k:
var(I)_k = \bar{I}_k^2 - (\bar{I}_k)^2 ≈ 9411.11 - 8918.91 = 492.2
2.2.2 代入公式计算斜率和截距
引导滤波通过最小化误差推导得到 a_k 和 b_k 的计算公式:

参数解析:
自引导时 p=I,\bar{p}_k = \bar{I}_k;
ε 正则化参数 (防止分母为 0,比如取 10)。
代入数值计算:

2.2.3 线性系数的物理意义
斜率 a_k ≈ 0.98(接近 1):说明这个窗口内有明显边缘(像素值从 60 到 130 变化大),滤波时要保留边缘 → 输出 q_i ≈ I_i(几乎不滤波)。
若窗口是平坦区域(比如所有像素都是 100),则

2.3 斜率和截距问题
问题:通过前面步骤讲解,掌握了每个小窗口内的斜率和截距计算方法,那么 a_k ≈ 0.98 和 b_k ≈ 1.89 具体是哪个像素的斜率和截距?是中心像素的吗?还是 3×3 小窗口内全局所有像素的斜率和截距都是一致?
答:a_k ≈ 0.98 和 b_k ≈ 1.89 是以中心像素 100 为核心的 3×3 窗口 ω_k 的全局线性系数,而非仅属于中心像素,但最终会通过加权平均让每个像素获得专属的 a、b,完成整幅图的滤波。
关于此疑惑,详见下面讲解。
2.4 基于窗口系数,初步计算窗口内所有像素的 q_i
2.4.1 引导滤波的完整逻辑
每个像素会被多个相邻窗口覆盖(比如中心像素会是自身窗口的中心,也是周围 8 个窗口的成员),因此每个像素的最终 a、b 是所有包含该像素的窗口的 a_k 和 b_k 的均值,最终输出是:

2.4.2 完整引导滤波实现过程(基于 3×3 窗口 + 具体像素值)
用以下 3×3 窗口 (记为窗口 W_5,中心像素为第 5 个像素,坐标 (1,1)),如下:

2.4.2.1 计算窗口 W_5 的 a_k 和 b_k
前面已经讲清楚各参数计算的来源,这里直接获取值,如下:

2.4.2.2 基于窗口系数、初步计算窗口内所有像素的 q_i
窗口内每个像素 i 的初步输出为:

参数解析:上标 (5) 表示来自窗口 W_5。
逐个计算 9 个像素的初步值,各像素的滤波输出(基于像素 5 的窗口系数):
- 像素 1(60):q_1^(5) = 0.98 × 60 + 1.89 = 60.69
- 像素 2(70):q_2^(5) = 0.98 × 70 + 1.89 = 70.49
- 像素 3(80):q_3^(5) = 0.98 × 80 + 1.89 = 80.29
- 像素 4(80):q_4^(5) = 0.98 × 80 + 1.89 = 80.29
- 像素 5(100):q_5^(5) = 0.98 × 100 + 1.89 = 99.89
- 像素 6(120):q_6^(5) = 0.98 × 120 + 1.89 = 119.49
- 像素 7(100):q_7^(5) = 0.98 × 100 + 1.89 = 99.89
- 像素 8(110):q_8^(5) = 0.98 × 110 + 1.89 = 109.69
- 像素 9(130):q_9^(5) = 0.98 × 130 + 1.89 = 129.29
2.4.2.3 考虑多窗口覆盖 (每个像素被多个窗口包含)
引导滤波中,每个像素会被多个相邻窗口覆盖,因此需要对所有包含该像素的窗口的 a_k、b_k 取均值,得到该像素的最终 \bar{a}_i,\bar{b}_i。
以中心像素 5(100)为例:像素 5 是窗口 W5 的中心,同时也是窗口 W_2(以像素 2 为中心)、W_4(以像素 4 为中心)、W_6(以像素 6 为中心)、W_8(以像素 8 为中心)的成员。
假设我们计算出这 4 个相邻窗口的系数,各窗口的系数计算:
- W_2(中心 70):a_2 ≈ 0.95, b_2 ≈ 3.5
- W_4(中心 80):a_4 ≈ 0.96, b_4 ≈ 3.2
- W_6(中心 120):a_6 ≈ 0.98, b_6 ≈ 2.4
- W_8(中心 110):a_8 ≈ 0.97, b_8 ≈ 2.9
像素 5 的最终系数:
\bar{a}_5 = \frac{a_5 + a_2 + a_4 + a_6 + a_8}{5} = \frac{0.98 + 0.95 + 0.96 + 0.98 + 0.97}{5} = 0.968
\bar{b}_5 = \frac{b_5 + b_2 + b_4 + b_6 + b_8}{5} = \frac{1.89 + 3.5 + 3.2 + 2.4 + 2.9}{5} = 2.778
像素 5 最终的滤波输出为:
q_5 = \bar{a}_5 · I_5 + \bar{b}_5 = 0.968 × 100 + 2.778 = 99.578
三、实战代码
3.1 参数设置
学者在使用代码过程,需要修改的地方如下:

3.2 C++ 代码
下面是完整的 C++ 代码:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
Mat guidedFilterForDenoising(const Mat& I, const Mat& p, int r, float eps) {
CV_Assert(!I.empty() && !p.empty());
CV_Assert(I.size() == p.size());
CV_Assert(r >= 1);
CV_Assert(eps > 0);
int inputType = p.type();
Mat I32f, p32f;
if (I.depth() == CV_8U) {
I.convertTo(I32f, CV_32F, 1.0 / 255.0);
} else {
I32f = I.clone();
}
(p.() == CV_8U) {
p.(p32f, CV_32F, / );
} {
p32f = p.();
}
;
Mat mean_I, mean_p;
(I32f, mean_I, CV_32F, winSize, (, ), , BORDER_REPLICATE);
(p32f, mean_p, CV_32F, winSize, (, ), , BORDER_REPLICATE);
Mat mean_Ip, mean_II;
(I32f.(p32f), mean_Ip, CV_32F, winSize, (, ), , BORDER_REPLICATE);
(I32f.(I32f), mean_II, CV_32F, winSize, (, ), , BORDER_REPLICATE);
Mat cov_Ip = mean_Ip - mean_I.(mean_p);
Mat var_I = mean_II - mean_I.(mean_I);
Mat a = cov_Ip / (var_I + eps);
Mat b = mean_p - a.(mean_I);
Mat mean_a, mean_b;
(a, mean_a, CV_32F, winSize, (, ), , BORDER_REPLICATE);
(b, mean_b, CV_32F, winSize, (, ), , BORDER_REPLICATE);
Mat q32f = mean_a.(I32f) + mean_b;
Mat result;
(inputType == CV_8UC1) {
q32f.(result, CV_8UC1, , );
} (inputType == CV_8UC3) {
q32f.(result, CV_8UC3, , );
} (inputType == CV_32FC1) {
result = q32f.();
} (inputType == CV_32FC3) {
result = q32f.();
} {
cerr << << endl;
();
}
result;
}
{
(!src.());
(src.() == );
vector<Mat> channels;
(src, channels);
( c = ; c < ; ++c) {
channels[c] = (channels[c],
channels[c],
r, eps);
}
Mat dst;
(channels, dst);
dst;
}
{
Mat src = ();
(src.()) {
cerr << << endl;
;
}
r = ;
eps = ;
Mat denoised = (src, r, eps);
(, src);
(, denoised);
cout << << endl;
();
;
}
3.3 Python 代码
下面是完整的 Python 代码:
import cv2
import numpy as np
import sys
def guidedFilterForDenoising(I, p, r, eps):
"""
通用引导滤波(保边降噪专用,无耗时优化,精度优先)
Parameters:
I: 引导图(numpy 数组),可以是原图自身(自引导)或其他特征图(异引导)
p: 待滤波的输入图(numpy 数组),需要降噪的图像
r: 滤波窗口半径(降噪核心参数:r 越大,降噪越强,建议 3~15)
eps: 正则化参数(保边核心参数:eps 越小,保边越强;eps 越大,平滑越强,建议 0.001~10)
Returns:
result: 滤波后的降噪图像(numpy 数组),与输入图尺寸、类型一致
Notes:
1. 支持单通道灰度图(uint8/float32)、3 通道彩色图(uint8/float32)
2. 降噪场景建议使用自引导(I=p),既保留边缘又滤除噪声
3. 无任何耗时优化,全程高精度浮点计算,专注降噪效果
"""
if I is None or p is None:
raise ValueError("引导图或输入图不能为空!")
if I.shape != p.shape:
raise ValueError("引导图与输入图尺寸必须一致!")
if r < 1:
raise ValueError("滤波窗口半径 r 必须大于等于 1!")
if eps <= 0:
raise ValueError("正则化参数 eps 必须大于 0!")
input_dtype = p.dtype
h, w = p.shape[:2]
is_color = len(p.shape) == 3 and p.shape[2] == 3
if I.dtype == np.uint8:
I32f = I.astype(np.float32) / 255.0
else:
I32f = I.copy().astype(np.float32)
p.dtype == np.uint8:
p32f = p.astype(np.float32) /
:
p32f = p.copy().astype(np.float32)
win_size = ( * r + , * r + )
mean_I = cv2.boxFilter(I32f, cv2.CV_32F, win_size, anchor=(-, -), normalize=, borderType=cv2.BORDER_REPLICATE)
mean_p = cv2.boxFilter(p32f, cv2.CV_32F, win_size, anchor=(-, -), normalize=, borderType=cv2.BORDER_REPLICATE)
I_mul_p = I32f * p32f
I_mul_I = I32f * I32f
mean_Ip = cv2.boxFilter(I_mul_p, cv2.CV_32F, win_size, anchor=(-, -), normalize=, borderType=cv2.BORDER_REPLICATE)
mean_II = cv2.boxFilter(I_mul_I, cv2.CV_32F, win_size, anchor=(-, -), normalize=, borderType=cv2.BORDER_REPLICATE)
cov_Ip = mean_Ip - mean_I * mean_p
var_I = mean_II - mean_I * mean_I
a = cov_Ip / (var_I + eps)
b = mean_p - a * mean_I
mean_a = cv2.boxFilter(a, cv2.CV_32F, win_size, anchor=(-, -), normalize=, borderType=cv2.BORDER_REPLICATE)
mean_b = cv2.boxFilter(b, cv2.CV_32F, win_size, anchor=(-, -), normalize=, borderType=cv2.BORDER_REPLICATE)
q32f = mean_a * I32f + mean_b
input_dtype == np.uint8:
q32f = np.clip(q32f * , , )
result = q32f.astype(np.uint8)
:
result = q32f.astype(input_dtype)
result
():
src :
ValueError()
src.ndim != src.shape[] != :
ValueError()
channels = cv2.split(src)
denoised_channels = []
c channels:
denoised_c = guidedFilterForDenoising(c, c, r, eps)
denoised_channels.append(denoised_c)
dst = cv2.merge(denoised_channels)
dst
():
img_path =
src = cv2.imread(img_path)
src :
(, file=sys.stderr)
-
r =
eps =
denoised = guidedFilterRGBForDenoising(src, r, eps)
cv2.imshow(, src)
cv2.imshow(, denoised)
cv2.imwrite(, denoised)
()
cv2.waitKey()
cv2.destroyAllWindows()
__name__ == :
main()
四、去噪实例效果
使用本博文教程中代码去噪结果如下,左图为原图,右图为引导滤波去噪后结果:





五、总结
本文详细介绍了引导滤波(Guided Filter)的原理与实现方法。文章通过具体示例(3×3 窗口)逐步解析了斜率和截距的计算过程,包括均值、方差及协方差的计算,并演示了如何通过线性模型得到滤波结果。此外,还探讨了窗口系数的物理意义及像素级滤波输出的计算方法,展示了引导滤波在边缘保留和平滑效果上的优势。引导滤波具有线性计算复杂度(O(N)),适用于图像去噪、HDR 压缩、语义分割后处理等场景,是计算机视觉领域的重要滤波技术。