简介:这是一套基于Python与PyQt5实现的图像去噪算法合集,面向图像处理初学者、算法研究人员及需要快速对比去噪效果的技术人员,可一站式体验多种主流去噪算法。资源围绕高斯滤波、中值滤波、FFT频域去噪、双边滤波、非局部均值与深度学习去噪等常见方法,提供可直接运行的Python代码,并配有简洁的PyQt5图形界面,无需编写代码即可选用算法、调节参数并预览保存结果。压缩包共6个文件,包含5个Python脚本和1个Qt界面文件(ui),整体仅6KB;脚本分别承担主程序入口、算法实现与图形界面逻辑,结构清晰、便于二次修改学习,也适合直接嵌入实际图像处理流程。目前已有6212人学习,适合在毕设、课设或实际项目中作为图像预处理模块参考,也能帮助快速理解不同去噪算法的适用场景与差异。
1. 图像去噪算法集合:别再把去噪当成一行 cv2 调用
“图像处理之图像去噪的各种算法实现”这个项目,第一眼很像课程作业,真正跑起来才发现它是一套能反复对比效果的 PyQt5 工具箱。之前我给一张带椒盐噪声的工业图片做预处理,直接cv2.GaussianBlur一行代码下去,噪声没去掉,边缘倒糊了一半。后来拿到这套源码才发现,去噪的坑大多不在算法本身,而在“有没有一个能随时调参数、立刻看对比的环境”。这个项目把高斯滤波、中值滤波、FFT 频域去噪、双边滤波、非局部均值,以及深度学习去噪的扩展接口,全部收进了同一个 GUI 界面,适合想快速跑通各种去噪算法的初学者,也适合需要对比滤波器效果、做二次开发的工程人员。非编程人员也能用滑块和下拉框操作,不用看代码就能知道不同去噪算法在同样噪声下差别有多大。
2. 项目结构与技术选型:六个文件怎么组织才能不写成代码屎山
2.1 解压后的文件清单:每个文件都不白给
chengxu.zip解压后会看到一个chengxu文件夹,里面的文件不算多,但命名有些随意,光看文件名容易懵。我第一次打开时也愣了一下,后来逐个看了一遍才理清楚。
| 文件 | 角色 | 一句话说明 |
|---|---|---|
main.py | 程序入口 | 启动 QApplication,加载主窗口 |
untitled.ui | Qt Designer 界面文件 | 按钮、滑块、图像显示区域都在这里定义 |
untitled.py | 界面生成类 | 由untitled.ui自动生成的 Python 类,main.py直接引用 |
color_image_denosing.py | 算法核心 | 彩色图像去噪相关算法的封装,集中管理参数 |
gao_si_lvbo.py | 高斯滤波独立模块 | 高斯核生成和卷积演示,也可以单独运行验证 |
double_edge.py | 边缘保持对比工具 | 生成去噪前后的边缘叠加图,用来验证边缘是否被保留 |
看到gao_si_lvbo这个拼音文件名,基本可以确定作者是随手起的,不是规范工程命名。double_edge.py直译是“双边缘”,在这类去噪项目里,常见做法是把图像转成灰度图,分别对噪声图和去噪图做 Sobel 或 Canny 边缘提取,再把两个边缘叠加到同一张画布上,用来直观判断“边缘有没有被滤波器抹掉”。如果打开看里面是cv2.Canny、cv2.Sobel之类的调用,不用意外。
这个文件清单还暴露了一个问题:中值滤波、FFT、非局部均值这些算法并没有单独拆成文件,大概率都集中在color_image_denosing.py里。也就是说,这个项目把“算法”和“展示”做了两级拆分:算法核心一个文件,界面逻辑一个文件,入口一个文件。对于一个小型 GUI 项目来说,这个分层足够清晰,后续加新算法不需要动 UI 文件。
2.2 技术栈分工:OpenCV 做算法、NumPy 做计算、PyQt5 做壳
这个项目没有用很重的框架,核心依赖就是三个:OpenCV、NumPy、PyQt5。OpenCV 负责GaussianBlur、medianBlur、bilateralFilter、fastNlMeansDenoisingColored这类成熟实现;NumPy 负责 FFT 变换、频域掩码、数组归一化;PyQt5 负责窗口、按钮、滑块和图像显示。
如果全是自己写,高斯卷积、中值排序、傅里叶变换这些代码量会非常大,而且数值稳定性不一定有保证。用 OpenCV 的好处是它内置了高频优化过的 C++ 实现,Python 调用只是薄薄一层封装,跑图的速度比纯 Python 手写循环快几十倍。
PyQt5 这一层也有讲究。项目里同时存在untitled.ui和untitled.py,说明界面是用 Qt Designer 画的,然后通过工具生成 Python 代码。常见的工作流是:先用 Qt Designer 打开untitled.ui,拖好按钮、滑块、QLabel 图像框,保存后执行pyuic5 untitled.ui -o untitled.py生成对应的 Python 类。之后不要手工去改untitled.py,因为只要.ui一变,重新生成就会覆盖掉手改的内容。界面布局的改动应该回到.ui文件里做。
2.3 界面与算法解耦:main.py 里没有一长串图像处理代码
很多人写图像处理 GUI,会把算法全塞进按钮的点击回调里,结果就是main.py越来越长,后面想换参数得翻几百行代码。这个项目没有这么做。main.py只负责创建窗口对象,真正的逻辑在untitled.py的槽函数里,算法在color_image_denosing.py里。入口部分大致是下面这个样子。
# main.py import sys from PyQt5.QtWidgets import QApplication from untitled import Ui_MainWindow class DenoiseWindow(Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) # 把 untitled.ui 里的所有控件组装进来 if __name__ == "__main__": app = QApplication(sys.argv) win = DenoiseWindow() win.show() sys.exit(app.exec_())注意,这里DenoiseWindow继承的是Ui_MainWindow,而不是单个控件类。setupUi是 Qt Designer 生成类里的标准方法,它会创建所有子控件、设置布局、连接信号。如果你看到这段代码,说明作者选择了“继承生成类”而不是“实例化生成类”的写法,这种方法比较省事,但要求你对 Qt 的对象树有一定理解。
真正的按钮逻辑一般写在untitled.py里,或者通过装饰器在运行时绑定。常见做法是:
self.openButton.clicked.connect(self.on_open_image) self.applyButton.clicked.connect(self.on_apply)这样做的核心价值是解耦:算法怎么算,不关心按钮叫什么;界面怎么摆,不关心滤波器核大小是怎么算出来的。以后想加一个“批量处理”入口,只需要复制一个按钮的处理逻辑,复用同一个算法函数即可。
2.4 数据流:从打开图片到显示结果,中间经历了什么
一次完整的去噪操作,在代码层面会走这样一条链路:
- 点击“打开图像”,
QFileDialog.getOpenFileName返回文件路径。 - 用
cv2.imread读取图像,此时颜色通道是 BGR。 - 把原始图像保存到窗口对象的
self.image属性里,方便后续算法函数访问。 - 用户在下拉框里选择算法,拖动滑块设置参数。
- 点击“应用”,调用
color_image_denosing.py里的对应函数,传入self.image和参数。 - 算法函数返回处理结果,再经过 BGR 到 RGB 转换,包装成
QImage显示到界面上。
如果你在代码里看到颜色不对,十有八九是第 6 步忘了换通道顺序。OpenCV 的imread读进来是 BGR,Qt 的QImage认 RGB,直接cv2.cvtColor(image, cv2.COLOR_BGR2RGB)转一下再显示就正常了。这个细节我会在后面的避坑章节专门展开。
3. 核心算法拆解:六种去噪实现从原理到代码
3.1 高斯滤波:gao_si_lvbo.py 里的卷积核生成
高斯滤波是所有去噪算法里最容易上手的,它用高斯函数生成一个二维卷积核,对图像做加权平均。离中心越近的像素权重越大,离中心越远的像素权重越小。gao_si_lvbo.py里如果只看关键实现,通常会包含一个自定义的高斯核生成函数,而不是直接调用cv2.GaussianBlur完事。
# gao_si_lvbo.py import cv2 import numpy as np def gaussian_kernel(size=5, sigma=1.0): """生成二维高斯核,size 必须是正奇数""" k = size // 2 x, y = np.mgrid[-k:k+1, -k:k+1] kernel = np.exp(-(x**2 + y**2) / (2 * sigma**2)) kernel /= kernel.sum() # 归一化,让平坦区域亮度不改变 return kernel def gaussian_filter(image, size=5, sigma=1.0): kernel = gaussian_kernel(size, sigma) return cv2.filter2D(image, -1, kernel)这里np.mgrid[-k:k+1, -k:k+1]生成的是以中心像素为原点的网格坐标,比如 size=3 时会得到[-1, 0, 1]的坐标组合。高斯公式里的sigma控制核的形状,sigma 越小,中心权重越突出,平滑效果越弱;sigma 越大,核越扁平,图像越模糊。
我一般会提醒自己:cv2.filter2D的第二个参数传-1,表示输出图像的深度和输入一致。如果你传了其他数值,比如cv2.CV_64F,得到的是浮点图像,后面直接转uint8会丢失精度甚至出现奇怪的颜色。相比之下,直接调cv2.GaussianBlur(image, (size, size), sigma)更方便,但自己写一遍核生成,更容易理解高斯滤波的本质。
3.2 中值滤波与双边滤波:去椒盐噪声和保持边缘的取舍
中值滤波的原理是窗口内像素排序后取中间值,所以它对椒盐噪声特别有效。椒盐噪声是图像上随机出现的纯白或纯黑像素,这些异常值在排序后会被挤到两端,中间值大概率还是正常灰度。虽然 OpenCV 提供了现成函数,但在工程实现里,关键是搞清楚它的参数限制。
def median_denoise(image, ksize=3): # ksize 必须为奇数,且大于 1 return cv2.medianBlur(image, ksize) def bilateral_denoise(image, d=9, sigma_color=75, sigma_space=75): # d 是邻域直径,sigma_color 是灰度差阈值,sigma_space 是空间距离阈值 return cv2.bilateralFilter(image, d, sigma_color, sigma_space)中值滤波的ksize如果设成偶数,OpenCV 会直接抛异常,这点在前端 UI 里很容易踩坑。它还有个隐性缺点:核越大,图像越“塑料化”,尤其是 5x5 以上会明显丢失纹理。双边滤波就不一样了,它同时参考空间距离和像素灰度差异。左边像素和中心像素灰度差很大时,即使空间距离很近,权重也会很低,所以边缘不会被平均掉。
sigma_color和sigma_space这两个参数比较难凭感觉定。如果sigma_color设得过大,被判为“相似”的像素变多,双边滤波就退化成高斯滤波;sigma_space设得过大,计算量会暴涨。一个常见起点是d=9, sigma_color=75, sigma_space=75,在高分辨率图上这个参数组合已经比较慢了。
3.3 FFT 频域去噪:把特定频率的噪声从频谱上抠掉
FFT 去噪和前面几种完全不同,它先把图像从空间域转到频率域。图像里的平缓区域对应低频,细节和噪声对应高频。通过构造一个低通掩码,乘到频谱上,把高频部分削弱,再反变换回空间域,就能达到平滑效果。
def fft_denoise(image, radius=30, filter_type="gaussian"): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image f = np.fft.fft2(gray.astype(np.float32)) fshift = np.fft.fftshift(f) # 把零频移到频谱中心 rows, cols = gray.shape crow, ccol = rows // 2, cols // 2 if filter_type == "ideal": # 理想低通:半径外直接置零,过渡带生硬,容易振铃 mask = np.zeros((rows, cols), np.float32) mask[crow-radius:crow+radius, ccol-radius:ccol+radius] = 1 else: # 高斯低通:距离越远衰减越平滑,振铃弱很多 y, x = np.ogrid[:rows, :cols] dist = np.sqrt((x - ccol)**2 + (y - crow)**2) mask = np.exp(-(dist**2) / (2 * radius**2)) fshift *= mask f_ishift = np.fft.ifftshift(fshift) img_back = np.fft.ifft2(f_ishift) img_back = np.abs(img_back) # 频域计算结果是复数,取模才是图像强度 return np.uint8(cv2.normalize(img_back, None, 0, 255, cv2.NORM_MINMAX))这里fftshift的作用是把零频分量移到中心。因为二维 FFT 的结果里,四个角是低频,中心是高频,不 shift 的话掩码不好设计。radius是低通半径,单位是像素,半径越小保留的频率越少,图像越模糊。
我常看到有人把理想低通和傅里叶逆变换直接拼在一起,结果图像边缘出现一圈一圈的条纹,这就是所谓的振铃效应。原因是理想低通在频域上有一个矩形截断,对应回空间域会产生振荡。所以项目里如果让我选,我默认用高斯低通,至少视觉上更温和。这个坑我会在后面的避坑章节细讲。
3.4 非局部均值与深度学习:高质量去噪的进阶路线
非局部均值(Non-Local Means)的核心思想是:图像里很多小块结构是重复出现的,比如纹理、边缘片段,找到这些相似块后加权平均,能比局部滤波器更好地保留细节。OpenCV 给彩色图提供了现成接口。
def nlmeans_denoise(image, h=10): # h 是滤波强度,太大细节会损失;后面 7 和 21 是模板窗口和搜索窗口 return cv2.fastNlMeansDenoisingColored(image, None, h, h, 7, 21)fastNlMeansDenoisingColored的参数顺序是:输入图、输出图、亮度滤波强度、颜色滤波强度、模板窗口大小、搜索窗口大小。模板窗口越大,参与计算的块越大,但纹理细节也更难保留;搜索窗口越大,能搜到更多相似块,但耗时成倍增长。这个函数在 1080p 图片上跑一次可能要几秒钟,所以 UI 里最好加一个进度提示,否则用户会以为程序卡死了。
深度学习去噪是另一条路线,像 DnCNN、FFDNet 这类模型需要预训练权重。这套源码大概率不会内置权重文件,但会预留一个接口位置。常见做法是写一个加载 PyTorch 模型的函数,文件不存在时抛出友好错误。
# color_image_denosing.py 中的预留接口 def denoise_dl(image, model_path="weights/dncnn.pth"): try: import torch model = torch.load(model_path) model.eval() # 图像从 HWC 转 CHW,并归一化到 [0, 1] tensor = torch.from_numpy(image.transpose(2, 0, 1)).float().unsqueeze(0) / 255.0 with torch.no_grad(): out = model(tensor) out = out.squeeze(0).permute(1, 2, 0).numpy() * 255.0 return np.clip(out, 0, 255).astype(np.uint8) except ImportError: raise RuntimeError("需要先安装 PyTorch 才能使用深度学习去噪")这里transpose(2, 0, 1)是把 HxWxC 的高度、宽度、通道顺序调成 CxHxW,这是大多数卷积神经网络的输入格式。unsqueeze(0)增加 batch 维度。如果torch.load加载出来的模型是旧版 PyTorch 保存的,可能会遇到兼容性问题,所以真正开发时我一般用torch.jit.load或者onnxruntime来减少依赖。
3.5 界面里的参数映射:滑块值如何变成算法参数
项目做成了 PyQt5 界面,算法和 UI 之间一定会有一个“翻译”层。用户在界面上拖的是滑块,但中值滤波、高斯滤波要求核大小是奇数。滑块如果给的是 2、4、6,直接传给函数就会报错。所以槽函数里要做一次取整转换。
def on_apply_clicked(self): method = self.comboBox.currentText() val = self.horizontalSlider.value() if val % 2 == 0: val += 1 # 强制转成奇数 if method == "高斯滤波": result = gaussian_filter(self.image, size=val, sigma=1.0) elif method == "中值滤波": result = cv2.medianBlur(self.image, val) elif method == "双边滤波": result = cv2.bilateralFilter(self.image, val, 75, 75) elif method == "FFT 去噪": result = fft_denoise(self.image, radius=val) self.show_result(result)这一段代码看着简单,却是整个 GUI 是否好用的关键。如果滑块范围是 1 到 31,那奇数只有 16 个值,用户拖起来会有“卡顿”感,所以更好的做法是让滑块本身按 2 的倍数步进,然后在读取时加一。比如滑块范围 0 到 15,实际核大小就是2 * value + 1,这样每个位置都有意义。这个细节在很多新手项目里没人注意,但直接影响用户体验。
不同算法对参数的语义差别很大,核大小去模糊,radius去频率,h去噪声强度。界面里最好把滑块标签动态换成当前算法的参数名,否则用户看着同一个滑块,选不同算法时完全不知道怎么调。
4. 跑通项目:从环境准备到界面联调
4.1 环境准备:依赖装不对,后面全是泪
拿到源码后第一步不是直接跑python main.py,而是确认依赖。这套项目最少需要numpy、opencv-python、PyQt5三个包。我一般会先建一个干净虚拟环境,避免污染系统 Python。
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install numpy opencv-python PyQt5opencv-python和opencv-contrib-python不能同时装,否则会有冲突。fastNlMeansDenoisingColored在主模块里就包含,不需要额外装 contrib。如果你看到导入torch的报错,那说明你选了深度学习去噪又没有装 PyTorch,项目本身没有把 torch 写进基础依赖,这是合理的,毕竟深度学习的 GPU 版本体积太大。
4.2 运行 main.py:从命令行到窗口弹出的完整过程
依赖装好之后,在项目根目录执行:
python main.py正常情况下会弹出一个 PyQt5 窗口,里面有打开图像的按钮、算法下拉框、滑块和图像显示区。如果窗口弹出时报错说找不到untitled模块,多半是untitled.py和main.py不在同一个目录。检查一下main.py里的import untitled前的路径处理。
import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))这段代码的作用是把当前文件所在目录加进模块搜索路径,防止你在其他路径下启动脚本时找不到同目录文件。
如果项目里的untitled.py是用pyuic5生成的,它会依赖PyQt5.QtWidgets里的一些类。生成文件的头部通常写着# Form implementation generated from reading ui file 'untitled.ui',这说明它只是界面定义,不能直接运行。必须通过main.py把它实例化并显示。
4.3 参数配置与效果预览:到底该动哪个滑块
窗口打开后,导入一张带噪声的图,选择算法,调整参数,点击应用。为了让你不盲目乱试,我整理了一个参数参考表,这也是我在实际调试时常用的起点。
| 算法 | 界面参数 | 推荐起点 | 调参方向 |
|---|---|---|---|
| 高斯滤波 | 核大小 | 5 | 噪声密则加大,但边缘会糊 |
| 中值滤波 | 核大小 | 3 | 椒盐噪声严重时加到 5 |
| 双边滤波 | 邻域直径 | 9 | 加大 sigma_color 会让边缘变弱 |
| FFT 去噪 | 低通半径 | 30 | 画面有振铃时加大半径,效果不够时减小 |
| 非局部均值 | 滤波强度 h | 10 | 噪声强则加到 15,细节重要则降到 5 |
| 深度学习 | 无 | 无 | 需要额外模型文件 |
预览结果之后,如果觉得效果可以,“保存图像”按钮一般会调用cv2.imwrite。注意保存路径里不能有中文,否则在某些 Windows 版本下 OpenCV 会写入失败。保存前也要记得把显示用的 RGB 图像转回 BGR,不然存出来的图颜色会偏蓝偏红。
4.4 命令行单独运行算法模块:不看界面也能验证
除了 GUI,作者保留了gao_si_lvbo.py这样的独立模块,这意味着可以在不启动界面的情况下直接测试算法。比如:
python gao_si_lvbo.py如果这个文件里有if __name__ == "__main__":的测试代码,它会自己读一张测试图、做一次高斯滤波并把结果打印出来。这种写法的好处是,算法在接入 GUI 之前已经经过一轮基本验证。我做算法调试时也习惯用命令行脚本先跑通数据流,再封装给界面,能少踩很多 GUI 线程的坑。
当你要排查“是算法错了,还是界面显示错了”时,直接写一个三行脚本,用同一张图调用color_image_denosing.py里的函数,用cv2.imwrite固化结果。如果命令行输出正常,问题大概率出在 Qt 图像转换上。
5. 常见问题与避坑:为什么我的去噪结果越搞越糊
5.1 高斯滤波和中值滤波报错:核大小必须是正奇数
现象:程序在调用cv2.GaussianBlur时抛出Assertion failed (ksize.width > 0 && ksize.height > 0 && (ksize.width % 2 == 1)),中值滤波也会报类似错误。
原因:OpenCV 要求核宽度、高度都是正奇数,因为核必须有一个明确的中心像素。滑块给出的数值是 2、4、6 这类偶数时,算法找不到中心点,直接拒绝执行。
解决:在 UI 层读取滑块值后强制转换。常见做法是if val % 2 == 0: val += 1,更好的做法是让滑块步长为 2,实际核大小用2 * slider_value + 1换算。这样不仅不会报错,滑块的每个刻度都有意义。以后只要涉及“尺寸”类的参数,都要先检查奇偶。
5.2 FFT 去噪后图像边缘出现一圈一圈的振铃
现象:用理想低通滤波后,图像边缘出现规则的明暗条纹,像涟漪一样,画面看起来“发脏”。
原因:理想低通在频域上把半径外所有频率直接置零,这个掩码在空间域对应一个带旁瓣的滤波器。频谱截断越陡,空间域的振荡越明显,叠加到图像边缘上就是振铃。
解决:把理想低通换成高斯低通或巴特沃斯低通。高斯掩码没有硬边界,过渡平缓,振铃要弱得多。如果必须用理想低通,可以加一个边缘羽化:mask在半径附近做一个渐变带,而不是从 1 直接跳到 0。项目代码里如果已经默认了filter_type="gaussian",就不要再手动切到 ideal。
5.3 双边滤波参数调大后界面直接卡死
现象:在 UI 里把双边滤波的邻域直径调到 15 以上,点击应用后窗口失去响应,几秒钟后才弹回来,CPU 占用接近 100%。
原因:双边滤波对每个像素都要计算空间权重和灰度权重,复杂度跟邻域大小成正比。d=15表示每个像素要遍历周围 225 个像素,高分辨率图上这个计算量非常可观,而且它是单线程实现,界面线程被它阻塞后自然就卡了。
解决:在界面层做两层限制。第一层是滑块上限,d最多给到 15,sigma_space不要超过 100。第二层是用QThread把算法丢到子线程执行,执行过程中禁用按钮,完成后通过信号把结果传回主线程。如果项目里没有线程封装,最简单的保底办法是先缩图处理再放大,用户体验至少不会死到没法点。
5.4 Qt 界面上显示的颜色偏蓝偏暗
现象:用cv2.imread读入图片,算法处理完后用QPixmap显示,原本红色的物体变成了蓝色,画面整体色调很奇怪。
原因:OpenCV 读图得到的是 BGR 顺序,而QImage构造时默认按 RGB 解释。如果不做通道转换,颜色分量交换,看起来就是红蓝颠倒。
解决:显示前先转换一次通道,然后构造 QImage。
rgb_image = cv2.cvtColor(result, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w qimg = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)注意bytes_per_line必须等于ch * w,如果图像经过某些处理变成了非连续内存,要先用np.ascontiguousarray把数据在内存中排布好,否则 QImage 会读取到错乱的数据,出现花屏。
5.5 深度学习去噪点击后提示找不到模型文件
现象:界面里的算法列表有“深度学习去噪”,但是选完点击应用,直接弹窗报错说路径不存在,或者ModuleNotFoundError: No module named 'torch'。
原因:项目只预留了深度学习接口,没有自带预训练权重。PyTorch 也不在常见的基础依赖里,环境里没装自然导入失败。
解决:第一选择,去噪需求不强烈时直接在界面里禁用这个算法项,避免用户误点。第二选择,用 ONNX Runtime 加载模型,onnxruntime比 PyTorch 轻很多,推理速度也快,但需要先把.pth转成.onnx格式。第三选择,如果只是想学习调用流程,可以先用一个随机的卷积层网络代替,把数据流跑通再说。这里我特别提醒一点:不要随便在网上下载来路不明的.pth文件,模型里如果带了恶意序列化代码,torch.load执行时会直接中招。宁可先用简化模型验证流程,再去正规渠道拉权重。
6. 二次开发:把自定义滤波器接进 UI 并批量验证去噪效果
6.1 自定义滤波器的接入方式
这套项目最常见的改造需求是“我自己的滤波算法也想放进界面里对比”。color_image_denosing.py已经把所有算法封装成了“输入图像,输出图像”的统一函数签名,所以自定义算法只要保持同样的输入输出,就能被 UI 调用。
比如我想加入一个自己设计的掩膜锐化去噪算法,只需要写:
def my_sharpen_denoise(image, strength=1.0): kernel = np.array([[0, -1, 0], [-1, 4 + strength, -1], [0, -1, 0]]) return cv2.filter2D(image, -1, kernel)然后在untitled.py的算法选择下拉框里加一项“自定义锐化去噪”,在on_apply_clicked里加一个分支。整个过程不需要改 UI 布局,只要下拉框的addItem一项就能把新算法暴露给用户。这就是前面“界面与算法解耦”带来的好处,如果是把所有代码揉在一个文件里的写法,这一步会痛苦得多。
如果新算法要暴露更多参数,一个做法是把color_image_denosing.py里的函数改成接受一个字典参数,界面把滑块键值对统一传进去:
def custom_denoise(image, params): alpha = params.get("alpha", 1.0) beta = params.get("beta", 0.5) ...6.2 批量去噪:从一个文件夹处理到另一个文件夹
有了算法核心模块后,批量处理其实比界面还简单。写一个独立脚本,遍历文件夹,复用color_image_denosing.py里的函数,输出到目标目录。
import os import cv2 from color_image_denosing import fft_denoise # 按需导入 def batch_denoise(input_dir, output_dir, radius=30): os.makedirs(output_dir, exist_ok=True) for name in os.listdir(input_dir): if not name.lower().endswith((".png", ".jpg", ".bmp")): continue image = cv2.imread(os.path.join(input_dir, name)) result = fft_denoise(image, radius=radius) cv2.imwrite(os.path.join(output_dir, name), result) print(f"processed: {name}")这个脚本对初学者有两个容易忽略的点。一是cv2.imread读不到文件时不会报错,只返回None,所以要先检查image is not None,否则后面所有函数都会挂。二是输出目录不存在时 OpenCV 不会自动创建,要在循环前用os.makedirs确保目录存在。
6.3 量化验证:PSNR 和 SSIM 比肉眼更可靠
在二次开发时,只靠眼睛看效果很容易被误导。同一张图,不同参数下视觉差别不大,但算法质量可能已经变了。给项目补一个量化评价模块,是收益最高的改造。
def psnr(original, processed, peak=255.0): mse = np.mean((original.astype(np.float32) - processed.astype(np.float32)) ** 2) if mse == 0: return float("inf") return 10 * np.log10(peak**2 / mse) def ssim(original, processed): from skimage.metrics import structural_similarity return structural_similarity(original, processed, multichannel=True)PSNR 只要输入两张尺寸相同的图,返回单位是 dB,数值越大表示失真越小。通常 30dB 以上目视质量就不错,低于 20dB 基本没法看。SSIM 更接近人的主观感受,范围在 0 到 1 之间,越接近 1 越好。注意这里original和processed必须是同类型的数组,如果一张是uint8一张是float32,算出来的结果没有意义。
我自己的教训是:一开始给这个项目加新算法时,只测了视觉观感,觉得某滤波“看起来挺干净”,结果跑了一遍 PSNR,发现数值反而比原图还低。原因是加了很强的平滑,噪声确实少了,但图像本身的结构也丢了。后来我给自己定了个规矩,任何去噪算法在进 UI 之前,必须先在一组带噪声的标准图上跑 PSNR/SSIM 基准,记录参数范围,再谈界面接入。从那以后,我每次新加滤波器都会强制走一遍这个验证流程,再放进界面里给用户调。希望帮到你。
本文还有配套的精品资源,点击获取