OpenCV Photo 模块实战:Non-Local Means 图像去噪(cv.fastNlMeansDenoising 系列)
【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv
本文基于 OpenCV 官方 Python 教程中“Non-Local Means Denoising”一章,系统讲解 Non-Local Means(非局部均值)去噪算法的原理、cv.fastNlMeansDenoising/cv.fastNlMeansDenoisingColored/cv.fastNlMeansDenoisingMulti/cv.fastNlMeansDenoisingColoredMulti四个 API 的完整参数说明与可运行示例,并结合 photo 模块 C++ 源码(modules/photo/src/denoising.cpp)与测试用例,剖析 CIELAB 分量分离、窗口奇数约束、normType深度限制等底层实现细节,帮助你在去噪强度参数h与细节保留之间做出可验证的权衡。
从局部平滑到 Non-Local Means:算法原理
为什么“相似补丁平均”能去噪
高斯模糊、中值模糊等传统平滑技术本质上是局部操作:围绕每个像素取一个小邻域,做加权平均或取中值。这类方法对少量噪声有效,但邻域内不同纹理会互相“污染”,噪声较多时细节很快被抹平。
Non-Local Means 利用了噪声的两个统计性质:
- 噪声通常被视为零均值随机变量。对含噪像素可写为
p = p0 + n(p0为真实值,n为噪声)。如果从多张相同场景中取N个同一位置的像素求平均,由于噪声均值为零,结果理想情况下会收敛到真实值p0; - 自然图像具有大量自重复结构:取图中一个 5x5 小块,图像其他位置(往往就在附近)大概率存在与之相似的小块。
于是算法思路非常自然:以某个像素为中心取模板窗口,在整个图像(甚至整个视频片段)中搜索与之相似的窗口,对这些相似窗口做加权平均,用平均结果替换中心像素。相比局部模糊,它利用的是“非局部”的相似区域,因此去噪效果显著更好,代价是计算时间明显更长。
教程中给出一个便于直观验证的实验:让相机静止拍摄几秒得到大量同场景帧,求所有帧的平均图像,与原帧对比即可看到噪声下降;但该实验对相机和场景运动不稳健,且现实中常常只有一张含噪图像——这正是 Non-Local Means 用“图像内相似块”替代“多帧平均”的动机。
OpenCV 提供的四个 fastNlMeansDenoising 变体
OpenCV 在cv::photo模块中实现了带多项计算优化的 Non-Local Means 去噪(针对高斯白噪声),Python 侧共暴露四个函数:
| 函数 | 适用输入 |
|---|---|
cv.fastNlMeansDenoising() | 单幅灰度图像 |
cv.fastNlMeansDenoisingColored() | 单幅彩色图像 |
cv.fastNlMeansDenoisingMulti() | 短时间内连续采集的灰度图像序列 |
cv.fastNlMeansDenoisingColoredMulti() | 同上,但为彩色图像序列 |
四个函数的公共参数(对应 C++ 头文件 photo.hpp 中的声明):
h:调节滤波强度的核心参数。h越大去噪越彻底,但图像细节丢失越多;h越小保留细节,但也会保留部分噪声。C++ 默认值为 3,教程建议彩色图一般取 10 左右即可;hForColorComponents(hColor):仅彩色版本使用,作用同h,但只调节 L/AB 中颜色分量的强度,通常与h相同或接近(多数图像取 10 足以去除彩色噪声且不偏色);templateWindowSize:模板窗口尺寸(像素),必须为奇数,推荐 7;searchWindowSize:搜索窗口尺寸(像素),必须为奇数,推荐 21。搜索窗口与去噪耗时线性相关——窗口越大搜索耗时越长;normType(进阶重载支持):权重计算使用的范数,取cv.NORM_L2或cv.NORM_L1,默认NORM_L2。
Multi 版本还有两个序列专用参数:
imgToDenoiseIndex:指定序列中需要去噪的目标帧下标;temporalWindowSize:参与去噪的邻近帧总数,必须为奇数。算法会取目标帧前后共temporalWindowSize帧(目标帧居中)一起计算。例如传入 5 帧、imgToDenoiseIndex = 2、temporalWindowSize = 3,则第 0、2、4 帧(即第 2 帧及前后各 1 帧)参与对第 2 帧的去噪。
从源码结构看,C++ 侧对h还提供更细粒度的重载:fastNlMeansDenoising与fastNlMeansDenoisingMulti均接受std::vector<float> h,可以为每个通道单独设置强度(h数组长度为 1 时对所有通道生效,长度等于通道数时按通道生效),这在 denoising.cpp 的CV_Assert(hn == 1 || hn == cn)中得到确认。
实战 1:cv.fastNlMeansDenoisingColored()彩色图像去噪
对一张含高斯噪声的彩色图(教程示例中输入噪声标准差sigma = 25)去噪,注意dst参数传None表示由 OpenCV 自动创建输出:
import numpy as np import cv2 as cv from matplotlib import pyplot as plt img = cv.imread('die.png') # 含高斯噪声 (sigma=25) 的彩色骰子图,需自备 dst = cv.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21) # 参数依次为: # h=10 —— 亮度分量滤波强度 # hColor=10 —— 颜色分量滤波强度 # templateWindowSize=7 —— 模板窗口(奇数) # searchWindowSize=21 —— 搜索窗口(奇数) plt.subplot(121), plt.imshow(img) plt.subplot(122), plt.imshow(dst) plt.show()对结果局部放大可以看到:噪点被明显压制,而骰子边缘与纹理基本保留(见文首nlm_result1对比图)。
为什么彩色版本要先转 CIELAB
教程提到“彩色图像会先转换到 CIELAB 色彩空间,再分别对 L 和 AB 分量去噪”。这一点可以直接在 denoising.cpp 的cv::fastNlMeansDenoisingColored实现中验证:
- 输入类型被严格校验为
CV_8UC3或CV_8UC4,否则抛出Type of input image should be CV_8UC3 or CV_8UC4!; cvtColor(src, src_lab, COLOR_LBGR2Lab)完成色彩空间转换;- 用
mixChannels把 Lab 拆成单通道的l和双通道的ab两张图; - 分别调用
fastNlMeansDenoising(l, l, h, ...)和fastNlMeansDenoising(ab, ab, hForColorComponents, ...)——这就是h与hColor各自独立的原因; - 合并后用
COLOR_Lab2LBGR转回 BGR。
这种“亮度与色度分开、各自定强度”的设计避免了直接在 BGR 上做相似性度量时,颜色差异干扰结构匹配的问题,也解释了为何h与hColor可以分开调节。
实战 2:cv.fastNlMeansDenoisingMulti()视频序列去噪
多帧版本利用时间维度的相似帧做非局部平均,适合短时间内拍摄的静止或缓动视频。注意该函数对输入有明确约束:所有帧必须同类型、同尺寸(灰度场景下先自行转灰度);temporalWindowSize必须为奇数;imgToDenoiseIndex前后必须留得下半个时间窗,否则报错。这些校验对应 denoising.cpp 中的fastNlMeansDenoisingMultiCheckPreconditions:
if (temporalWindowSize % 2 == 0 || searchWindowSize % 2 == 0 || templateWindowSize % 2 == 0) { CV_Error(Error::StsBadArg, "All windows sizes should be odd!"); } if (imgToDenoiseIndex - temporalWindowHalfSize < 0 || imgToDenoiseIndex + temporalWindowHalfSize >= src_imgs_size) { CV_Error(Error::StsBadArg, "imgToDenoiseIndex and temporalWindowSize " "should be chosen corresponding srcImgs size!"); }完整示例:读取vtest.avi前 5 帧,加上方差 25 的高斯噪声,再对第 3 帧(下标 2)利用全部 5 帧去噪(vtest.avi需自备):
import numpy as np import cv2 as cv from matplotlib import pyplot as plt cap = cv.VideoCapture('vtest.avi') # create a list of first 5 frames img = [cap.read()[1] for i in range(5)] # convert all to grayscale gray = [cv.cvtColor(i, cv.COLOR_BGR2GRAY) for i in img] # convert all to float64 gray = [np.float64(i) for i in gray] # create a noise of variance 25 noise = np.random.randn(*gray[1].shape) * 10 # Add this noise to images noisy = [i + noise for i in gray] # Convert back to uint8 noisy = [np.uint8(np.clip(i, 0, 255)) for i in noisy] # Denoise 3rd frame considering all the 5 frames dst = cv.fastNlMeansDenoisingMulti(noisy, 2, 5, None, 4, 7, 35) # 参数依次为: # imgToDenoiseIndex=2 —— 去噪下标 2 的帧 # temporalWindowSize=5 —— 前后共 5 帧参与(奇数) # dst=None —— 自动创建输出 # h=4 —— 滤波强度 # templateWindowSize=7 # searchWindowSize=35 —— 比单帧示例更大, # 因为搜索范围扩展到多帧 plt.subplot(131), plt.imshow(gray[2], 'gray') plt.subplot(132), plt.imshow(noisy[2], 'gray') plt.subplot(133), plt.imshow(dst, 'gray') plt.show()结果三张图依次为:原始帧、加噪帧、去噪帧(见上文nlm_multi图)。教程特别强调:该计算耗时可观(It takes considerable amount of time for computation)。从源码结构看,搜索窗口越大、时间窗越大,参与比对的补丁数量呈乘积式增长;虽然实现已用parallel_for_按行并行(见 denoising.cpp 中fastNlMeansDenoisingMulti_),时间复杂度本身无法靠并行摊薄,实际使用时应尽量控制searchWindowSize与temporalWindowSize。
源码级细节:类型约束、OpenCL 与测试验证
normType 与位深约束
denoising.cpp 中对normType的分发逻辑给出了明确的输入位深边界:
NORM_L2(默认):仅支持CV_8U输入;NORM_L1:支持CV_8U与CV_16U;- 其他取值直接报
Unsupported norm type! Only NORM_L2 and NORM_L1 are supported。
也就是说,若你要用 16 位图像做 Non-Local Means 去噪,必须选用NORM_L1重载。对应的回归测试位于 test_denoising.cpp,其中包含 8U/16U 双位深NORM_L1对比测试(如fastNlMeansDenoisingMulti(..., 7, 21, NORM_L1)),以及单帧/多帧/彩色各变体的常规用例(fastNlMeansDenoising(original, result, 10)等)。
OpenCL 后端
fastNlMeansDenoising与fastNlMeansDenoisingColored入口均带有CV_OCL_RUN分发:当输入输出任一为 UMat 且尺寸大于 5x5 时走 OpenCL 实现(小图精度偏低故跳过),内核为 nlmeans.cl,分发与封装在 fast_nlmeans_denoising_opencl.hpp。CPU 核心的行级并行模板位于 fast_nlmeans_denoising_invoker.hpp 与 fast_nlmeans_multi_denoising_invoker.hpp。
参数取值速查与调参建议
综合头文件默认值与教程推荐值:
| 参数 | 默认值(C++ 声明) | 教程推荐 | 约束 |
|---|---|---|---|
h | 3.0 | 10(灰度单帧可试 4~15) | 越大去噪越强、细节丢失越多 |
hForColorComponents | 3.0 | 10 | 同上,仅作用于 L/AB 颜色分量 |
templateWindowSize | 7 | 7 | 必须为奇数 |
searchWindowSize | 21 | 21(多帧示例用 35) | 必须为奇数;耗时随其线性增长 |
temporalWindowSize(Multi) | — | 按序列长度定 | 必须为奇数;目标帧前后须留足半窗 |
normType(进阶重载) | NORM_L2 | — | NORM_L1才支持 16 位输入 |
算法背景可参考 Buades 等人的经典论文Non-local image and movie denoising(2005),OpenCV 头文件中对该方法的引用即指向此文献;教程原文推荐的 IPol 在线演示站点不再于本文列出外部链接,读者可按上述文献名自行检索。
小结
- Non-Local Means 用“图像内(或视频内)相似补丁的加权平均”替代局部平滑,以更高计算成本换取更干净且保真的去噪结果,适用于高斯白噪声场景;
- 单帧灰度用
cv.fastNlMeansDenoising,单帧彩色用cv.fastNlMeansDenoisingColored(内部转 CIELAB、L 与 AB 分量分别按h/hColor去噪),视频序列则用对应的Multi变体,通过imgToDenoiseIndex与奇数temporalWindowSize圈定参与帧; - 两个窗口参数必须为奇数,
searchWindowSize直接决定耗时;进阶重载下 16 位图像须搭配NORM_L1; - 需要 GPU 加速时可传入 UMat 触发 OpenCL 路径(内核见
modules/photo/src/opencl/nlmeans.cl),行为一致性有modules/photo/test/test_denoising.cpp与 OpenCL 测试modules/photo/test/ocl/test_denoising.cpp覆盖。
【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考