比较保边去噪与纹理去噪的质量取舍

图像去噪并不只是把画面抹得更平滑。平坦区域的噪点、物体边缘和细密纹理往往需要不同的处理策略。本文合并整理 scikit-image 官方的 Denoising a picture 与 Non-local means denoising for preserving textures:先比较总变分、双边和小波去噪,再用非局部均值观察纹理保留与参数选择的关系。

原文维护与发布方:scikit-image team;原页未列出可确认的个人作者。本文依据 0.26.0 文档及 v0.26.0 官方示例源码翻译、合并并补充静态审查说明,核对日期为 2026-10-05。正文中的对比图和示例输出来自原站,本次没有安装或运行去噪代码。

先看三种不同的平滑方式

总变分(TV)去噪希望在尽量接近输入图像的同时,降低图像的总变分。原文将总变分解释为图像梯度的 L1 范数。它倾向于生成较平坦的区域,同时保留区域间较清楚的边界。示例调用 denoise_tv_chambolle,分别使用 weight=0.1 与 weight=0.2,展示平滑程度变化后的结果。

双边滤波同时考虑像素之间的空间距离和颜色或强度相似性,再进行加权平均。因此,相邻但颜色差异很大的像素不一定互相强烈影响。原例将 sigma_spatial 固定为 15,把 sigma_color 从 0.05 调到 0.1,观察允许更大颜色差异参与平均后的变化。

总变分和双边滤波都可能让图像出现原文所说的“色调分层”外观:大片区域趋于平坦,区域间保留明显的边缘。调参数是在去除噪声和忠实保留原始细节之间取舍;更平滑的结果不自动等于更准确。

小波去噪先将图像表示为小波系数,再处理被视作噪声的小幅系数。原文用“把小系数置零”说明其基本思路。彩色图像如果分别处理各个颜色通道,有时会留下更显眼的颜色噪声,因此示例还比较了 YCbCr 色彩空间中的去噪结果。两次调用均保留 rescale_sigma=True,第二次另加 convert2ycbcr=True。小波功能需要 PyWavelets;它不是可以忽略的可选依赖。

实验一:给 Chelsea 的局部照片添加高斯噪声

原例从 data.chelsea() 取出行区间 100:250、列区间 50:300,并用 img_as_float 转换成浮点图像。设定的噪声标准差是 sigma=0.155,传给 random_noise 的参数却是方差,因此使用 var=sigma**2。将标准差直接填入 var 会改变实验条件。

estimate_sigma(noisy, channel_axis=-1, average_sigmas=True) 对颜色通道的噪声估计求平均;channel_axis=-1 表示最后一维是颜色通道。由于 random_noise 默认把结果裁剪到允许范围,估计值可能比设置值略小。原站本次文档构建显示估计值约为 0.14930,这不是必须复现到小数点后若干位的断言。

Chelsea 猫照片局部的八格官方对比图:噪声图、原图、两个强度的总变分和双边滤波,以及普通和 YCbCr 小波去噪。
scikit-image 0.26.0 官方示例原图,未经重新计算或修改。基础照片由 Stefan van der Walt 拍摄并以 CC0 提供;示例编排与程序由 scikit-image team 发布。图中输出属于原站示例,不是本次执行结果。

下面保留第一个示例的完整可执行代码结构。注释、参数、绘图布局和展示逻辑均来自原文,未加装依赖,也未在本次审查中执行。

import matplotlib.pyplot as plt

from skimage.restoration import (
    denoise_tv_chambolle,
    denoise_bilateral,
    denoise_wavelet,
    estimate_sigma,
)
from skimage import data, img_as_float
from skimage.util import random_noise


original = img_as_float(data.chelsea()[100:250, 50:300])

sigma = 0.155
noisy = random_noise(original, var=sigma**2)

fig, ax = plt.subplots(nrows=2, ncols=4, figsize=(8, 5), sharex=True, sharey=True)

plt.gray()

# Estimate the average noise standard deviation across color channels.
sigma_est = estimate_sigma(noisy, channel_axis=-1, average_sigmas=True)
# Due to clipping in random_noise, the estimate will be a bit smaller than the
# specified sigma.
print(f'Estimated Gaussian noise standard deviation = {sigma_est}')

ax[0, 0].imshow(noisy)
ax[0, 0].axis('off')
ax[0, 0].set_title('Noisy')
ax[0, 1].imshow(denoise_tv_chambolle(noisy, weight=0.1, channel_axis=-1))
ax[0, 1].axis('off')
ax[0, 1].set_title('TV')
ax[0, 2].imshow(
    denoise_bilateral(noisy, sigma_color=0.05, sigma_spatial=15, channel_axis=-1)
)
ax[0, 2].axis('off')
ax[0, 2].set_title('Bilateral')
ax[0, 3].imshow(denoise_wavelet(noisy, channel_axis=-1, rescale_sigma=True))
ax[0, 3].axis('off')
ax[0, 3].set_title('Wavelet denoising')

ax[1, 1].imshow(denoise_tv_chambolle(noisy, weight=0.2, channel_axis=-1))
ax[1, 1].axis('off')
ax[1, 1].set_title('(more) TV')
ax[1, 2].imshow(
    denoise_bilateral(noisy, sigma_color=0.1, sigma_spatial=15, channel_axis=-1)
)
ax[1, 2].axis('off')
ax[1, 2].set_title('(more) Bilateral')
ax[1, 3].imshow(
    denoise_wavelet(noisy, channel_axis=-1, convert2ycbcr=True, rescale_sigma=True)
)
ax[1, 3].axis('off')
ax[1, 3].set_title('Wavelet denoising\nin YCbCr colorspace')
ax[1, 0].imshow(original)
ax[1, 0].axis('off')
ax[1, 0].set_title('Original')

fig.tight_layout()

plt.show()

原站还显示一条 Matplotlib 提示:某个 RGB 浮点结果的最小值约为 -0.01696,显示时被裁剪到 [0, 1]。这说明“能显示”并不等于数值已落在预期范围。若后续要保存、量化或计算指标,应先检查算法输出的范围和数据类型,并说明任何裁剪步骤;不要悄悄修改结果再声称与原实验相同。

实验二:让非局部均值寻找相似纹理块

非局部均值(Non-local means,NLM)不是只平均当前位置周围的一圈像素。它把目标像素周围的小图像块,与其他像素周围的小块比较,再按块的相似程度对那些位置的像素值加权平均。这样,相似纹理即使不紧邻,也能提供去噪依据;一些容易被普通平滑抹掉的纹理有机会被保留。

当 fast_mode=False 时,计算块之间的距离使用空间高斯权重;当它为 True 时,则使用更快的、块内空间权重均匀的算法。这不只是同一运算的“速度开关”:权重方式也发生了变化,因此需要比较输出,而不能预设两种结果完全相同。

无论采用哪种模式,如果提供噪声标准差 sigma,算法在计算块距离时会减去预期噪声方差的贡献。原文指出这可能带来适度的质量改善。estimate_sigma 可作为设定 h 和可选 sigma 的起点;h 控制块权重随块距离衰减的程度。较大的 h 会允许不那么相似的块参与更强的平滑。

原作者明确说明,下面四种设置中的 h 是手动调整过的,用于接近各个变体在这个例子里的最佳表现。它们不是自动调参结果,更不是一套适合所有图像的默认值。

固定输入和搜索窗口,比较四个 NLM 变体

第二个例子使用 data.astronaut(),取出 [30:180, 150:300] 的局部区域,加入标准差 0.08 的高斯噪声。噪声估计使用 np.mean(estimate_sigma(noisy, channel_axis=-1))。共同参数 patch_size=5 对应 5×5 小块;patch_distance=6 对应 13×13 的搜索区域,颜色通道仍在最后一维。

原例四种 NLM 配置
变体 fast_mode h 显式提供 sigma
慢速 False 1.15 × sigma_est 否
慢速,利用噪声估计 False 0.8 × sigma_est sigma_est
快速 True 0.8 × sigma_est 否
快速,利用噪声估计 True 0.6 × sigma_est sigma_est
宇航员照片局部的六格官方对比图:加噪输入、无噪原图、慢速与快速非局部均值,以及两种模式显式提供噪声标准差的结果。
scikit-image 0.26.0 官方 NLM 示例原图,保留原图标题。基础照片为 NASA 的 Eileen Collins 宇航员照片;scikit-image 数据说明标为公共领域、无已知版权限制。示例编排与程序:scikit-image team。

第二个示例完整代码如下。它同时给出结果图和 PSNR 计算,保留原文的四组参数及无噪参照图。

import numpy as np
import matplotlib.pyplot as plt

from skimage import data, img_as_float
from skimage.restoration import denoise_nl_means, estimate_sigma
from skimage.metrics import peak_signal_noise_ratio
from skimage.util import random_noise


astro = img_as_float(data.astronaut())
astro = astro[30:180, 150:300]

sigma = 0.08
noisy = random_noise(astro, var=sigma**2)

# estimate the noise standard deviation from the noisy image
sigma_est = np.mean(estimate_sigma(noisy, channel_axis=-1))
print(f'estimated noise standard deviation = {sigma_est}')

patch_kw = dict(
    patch_size=5,  # 5x5 patches
    patch_distance=6,  # 13x13 search area
    channel_axis=-1,
)

# slow algorithm
denoise = denoise_nl_means(noisy, h=1.15 * sigma_est, fast_mode=False, **patch_kw)

# slow algorithm, sigma provided
denoise2 = denoise_nl_means(
    noisy, h=0.8 * sigma_est, sigma=sigma_est, fast_mode=False, **patch_kw
)

# fast algorithm
denoise_fast = denoise_nl_means(noisy, h=0.8 * sigma_est, fast_mode=True, **patch_kw)

# fast algorithm, sigma provided
denoise2_fast = denoise_nl_means(
    noisy, h=0.6 * sigma_est, sigma=sigma_est, fast_mode=True, **patch_kw
)

fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(8, 6), sharex=True, sharey=True)

ax[0, 0].imshow(noisy)
ax[0, 0].axis('off')
ax[0, 0].set_title('noisy')
ax[0, 1].imshow(denoise)
ax[0, 1].axis('off')
ax[0, 1].set_title('non-local means\n(slow)')
ax[0, 2].imshow(denoise2)
ax[0, 2].axis('off')
ax[0, 2].set_title('non-local means\n(slow, using $\\sigma_{est}$)')
ax[1, 0].imshow(astro)
ax[1, 0].axis('off')
ax[1, 0].set_title('original\n(noise free)')
ax[1, 1].imshow(denoise_fast)
ax[1, 1].axis('off')
ax[1, 1].set_title('non-local means\n(fast)')
ax[1, 2].imshow(denoise2_fast)
ax[1, 2].axis('off')
ax[1, 2].set_title('non-local means\n(fast, using $\\sigma_{est}$)')

fig.tight_layout()

# print PSNR metric for each case
psnr_noisy = peak_signal_noise_ratio(astro, noisy)
psnr = peak_signal_noise_ratio(astro, denoise)
psnr2 = peak_signal_noise_ratio(astro, denoise2)
psnr_fast = peak_signal_noise_ratio(astro, denoise_fast)
psnr2_fast = peak_signal_noise_ratio(astro, denoise2_fast)

print(f'PSNR (noisy) = {psnr_noisy:0.2f}')
print(f'PSNR (slow) = {psnr:0.2f}')
print(f'PSNR (slow, using sigma) = {psnr2:0.2f}')
print(f'PSNR (fast) = {psnr_fast:0.2f}')
print(f'PSNR (fast, using sigma) = {psnr2_fast:0.2f}')

plt.show()

怎样理解原文的 PSNR 输出

peak_signal_noise_ratio(astro, result) 以未加噪的 astro 为参照计算 PSNR。在该原站构建中,估计噪声标准差约为 0.07919,输出记录如下。这些数字来自下载到的原站正文,并非本次复现实测:

图像或配置 原站示例 PSNR / dB
加噪图 22.22
慢速 NLM 29.43
慢速 NLM,提供 sigma 29.85
快速 NLM 29.05
快速 NLM,提供 sigma 29.36

在这个输入和手调参数下,显式提供噪声标准差的两组结果分别高于对应未提供的设置。但这不能证明任何一组算法普遍优胜。原代码没有固定随机种子,每次噪声样本都可能变化;未知真实噪声也未必服从这里假设的高斯模型。真实受噪照片往往没有可用的无噪参照,不能把这里的全参考 PSNR 评价直接照搬过去。评估时应同时检查边缘、纹理和颜色,避免只看一个分数。

复现与静态安全审查说明

两个示例主要读取 scikit-image 示例数据,在内存里进行数组运算,再用 Matplotlib 显示。本文未发现所展示代码中存在命令注入、动态代码执行、硬编码凭证或删除文件的逻辑;这只是所读示例的静态检查结论,不等于依赖库无漏洞,也不覆盖读者自行添加的图片上传、网络服务和文件写入逻辑。

复现候选基线是 CPython 3.11、scikit-image 0.26.0、NumPy 2.2.6、SciPy 1.15.3、Matplotlib 3.10.3,并补充 PyWavelets 1.8.0。该组合仅作为候选配置记录保留,本次没有解析完整依赖锁、安装 wheel、检测平台二进制兼容性或运行算法。不要把“存在这些包”理解成环境已经验证可运行。

如需更稳定地比较参数,可在原文的 random_noise 调用中显式设置该版本支持的随机数生成器或种子,并记录具体版本、输入裁剪区间、随机状态、参数和数据范围。此处属于编辑补充建议,没有修改上面的原例代码,也没有生成新的实验分数。处理外部图片时还应限制尺寸与计算预算;非局部均值的搜索范围和输入大小会影响资源消耗。

来源、图片与许可

本稿合并的两篇官方文章均已核读全文,原例代码与 v0.26.0 tag 逐项比对。源页版权页脚为“© Copyright 2013-2025, the scikit-image team”;项目 v0.26.0 许可文件对一般文件声明“Copyright: 2009-2022 the scikit-image team”,适用 BSD-3-Clause,并列有其他文件的单独例外。本文所用示例不在所列例外中。代码的再分发须保留版权、许可条件及免责声明;完整适用声明随稿提供于 下列完整许可文本。

图片的基础素材权利另行核对:Chelsea 为摄影者 Stefan van der Walt 的 CC0 照片;astronaut 为 NASA 的 Eileen Collins 照片,数据说明称其已进入公共领域、无已知版权限制。两张配图均为获授权的官方完整对比图,本稿未移除署名或修改图像。

完整许可声明

Copyright: 2009-2022 the scikit-image team
License: BSD-3-Clause

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. Neither the name of the University nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Source: https://raw.githubusercontent.com/scikit-image/scikit-image/v0.26.0/LICENSE.txt
Applies to the two example programs reused in this article. The upstream
LICENSE.txt also lists separate exceptions for other files not reused here.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容