
简介这份资源是面向计算机、人工智能、电子信息等相关专业学生与初级开发者的本科毕业设计项目包主题为基于Python深度学习的模糊人脸图像增强系统可用于毕设答辩、课程设计、大作业或项目立项演示帮助解决模糊人脸图像清晰化这一典型视觉任务。压缩包共20个文件约359KB以9个py源码文件为核心辅以6个pyc编译文件、2张png效果图、1个xml配置、1个txt日志与1个md说明文档涵盖数据输入、模型定义、测试脚本与资源素材等模块目录结构清晰便于按功能定位代码。目前已有194人学习下载具备一定参考热度。读者可从中获得一套可运行的深度学习人脸增强实现思路包括网络模型搭建、训练与测试流程、日志记录方式及项目组织规范适合作为入门实战练习与二次开发的基础模板。1. 模糊人脸增强毕设从一张糊脸到可交付系统的完整路径拍毕业照那天手抖了一下回去放大一看人脸糊成一团五官只剩轮廓。这种场景你大概率遇到过——监控截图、老照片翻拍、手机抓拍运动物体模糊人脸几乎无处不在。本科毕业设计里选「基于 Python 深度学习的模糊人脸图像增强系统」这个题本质上要解决的就是给一张低质量、模糊、可能还带噪声的人脸图用深度学习模型把它恢复成清晰可辨的样子并且封装成一个能跑起来、能演示、能写进论文的系统。这个方向适合两类人一是想拿深度学习做毕设但不想碰太偏门任务的本科生二是已经会点 Python、想找一个端到端项目练手的人。它不需要你从零发明算法但需要你把数据、模型、训练、推理、界面这条链路完整走通。下面我按实际做项目的顺序把选型理由、代码、参数和踩过的坑一次讲清楚。2. 模糊人脸增强到底在做什么退化模型与方案选型2.1 先搞清楚「模糊」是怎么来的很多人一上来就找模型结果训练半天效果玄学根本原因是没定义清楚退化过程。模糊人脸图像的退化通常可以写成I_blur (I_sharp ⊛ k) ↓s n其中 k 是模糊核高斯核、运动模糊核、散焦核⊛ 是卷积↓s 是下采样n 是噪声。人脸增强任务要做的就是从 I_blur 反推 I_sharp。这里有个关键分叉如果只做去模糊deblur输入输出同分辨率如果同时做超分super-resolution输入是小图输出是大图。毕设里最常见的设定是「低分辨率 模糊」一起处理因为这样任务更有难度、论文更好写演示效果也更直观。理解退化模型的意义在于你合成训练数据时就是按这个公式去造配对样本。真实场景的模糊核未知所以学术界常用两种路线——盲去模糊blind deblurring和非盲去模糊。毕设建议走盲去模糊因为演示时你不可能知道用户上传图片的模糊核。2.2 为什么选深度学习而不是传统方法传统方法里维纳滤波、Richardson-Lucy 反卷积这些都需要估计模糊核核估错了结果直接崩。深度学习的优势是端到端学习退化到清晰的映射不需要显式估计核。对于人脸这种有强结构先验的对象网络还能学到「眼睛应该长什么样」「嘴巴边缘应该在哪」这类先验这是传统方法做不到的。具体到网络选型毕设常见的有三条路线方案代表结构优点缺点适合场景CNN 回归SRCNN / DnCNN / RRDB结构简单好训练容易过度平滑细节丢失入门、快速出结果GAN 对抗SRGAN / ESRGAN / GFPGAN细节锐利视觉效果好训练不稳定可能产生伪影追求演示效果TransformerRestormer / SwinIR长距离依赖建模强显存吃紧训练慢有显卡、想写创新点我的建议是主干用 RRDB 或残差密集块做回归再挂一个轻量判别器做对抗微调。这样既有稳定的训练过程又能拿到锐利的视觉效果。如果时间紧直接上 ESRGAN 的生成器结构把输入通道改成 3、输出通道改成 3去掉不必要的上采样层即可。2.3 数据集怎么准备合成配对样本的完整脚本公开的真实模糊-清晰人脸配对数据很少所以毕设里几乎都是自己合成。用 CelebA 或 FFHQ 的清晰人脸按退化模型造模糊图。下面这段代码是我实际用的合成脚本import cv2 import numpy as np import os import random def random_gaussian_kernel(kernel_size21, sigma_min0.5, sigma_max5.0): 生成随机高斯模糊核 sigma random.uniform(sigma_min, sigma_max) kernel cv2.getGaussianKernel(kernel_size, sigma) kernel kernel kernel.T return kernel def random_motion_kernel(kernel_size21): 生成随机运动模糊核 kernel np.zeros((kernel_size, kernel_size)) # 随机方向 angle random.uniform(0, np.pi) # 在核中心画一条线 center kernel_size // 2 for i in range(kernel_size): x int(center (i - center) * np.cos(angle)) y int(center (i - center) * np.sin(angle)) if 0 x kernel_size and 0 y kernel_size: kernel[y, x] 1 kernel kernel / kernel.sum() return kernel def degrade_image(img, scale4, noise_std5.0): 对清晰图做退化模糊 下采样 噪声 h, w img.shape[:2] # 随机选模糊类型 if random.random() 0.5: kernel random_gaussian_kernel() else: kernel random_motion_kernel() # 卷积模糊 blurred cv2.filter2D(img, -1, kernel) # 下采样 small cv2.resize(blurred, (w // scale, h // scale), interpolationcv2.INTER_CUBIC) # 加高斯噪声 noise np.random.normal(0, noise_std, small.shape).astype(np.float32) noisy np.clip(small.astype(np.float32) noise, 0, 255).astype(np.uint8) return noisy def build_dataset(sharp_dir, save_lr_dir, save_hr_dir, scale4): os.makedirs(save_lr_dir, exist_okTrue) os.makedirs(save_hr_dir, exist_okTrue) for name in os.listdir(sharp_dir): path os.path.join(sharp_dir, name) img cv2.imread(path) if img is None: continue # 统一裁剪到 256x256保证训练尺寸一致 img cv2.resize(img, (256, 256)) lr degrade_image(img, scalescale) cv2.imwrite(os.path.join(save_lr_dir, name), lr) cv2.imwrite(os.path.join(save_hr_dir, name), img) if __name__ __main__: build_dataset(./celeba_sharp, ./dataset/lr, ./dataset/hr, scale4)这段脚本的逻辑分三步先随机选高斯核或运动核做卷积模糊再按 scale 下采样最后加高斯噪声。参数上kernel_size 取 21 是经验值太小模糊不明显太大计算慢sigma 范围 0.5 到 5.0 覆盖了从轻微到严重的模糊noise_std 取 5.0 是模拟常见传感器噪声。scale4 是超分任务的标准倍率如果你想做 2 倍或 8 倍改这个参数即可但注意 8 倍时网络要加深否则恢复能力不够。提示合成数据时一定要保证 LR 和 HR 的文件名一一对应后面 DataLoader 配对读取全靠这个。我见过有人用随机数命名结果训练时图文不对应loss 死活降不下去。3. 模型搭建与训练从 RRDB 到可收敛的 loss 组合3.1 生成器结构为什么用 RRDB 而不是普通残差块RRDBResidual in Residual Dense Block是 ESRGAN 的核心组件它的思路是在残差块里再套残差并且用密集连接把每一层的特征都传给后面。这样做的好处是梯度流动更顺畅深层网络也能训得动。对于人脸增强深层特征很重要因为要恢复的是眼睛、嘴唇这种高频细节。下面是一个精简版 RRDB 生成器去掉了不必要的上采样层适合 4 倍超分import torch import torch.nn as nn class DenseBlock(nn.Module): def __init__(self, channels64, growth32): super().__init__() self.conv1 nn.Conv2d(channels, growth, 3, 1, 1) self.conv2 nn.Conv2d(channels growth, growth, 3, 1, 1) self.conv3 nn.Conv2d(channels 2 * growth, growth, 3, 1, 1) self.conv4 nn.Conv2d(channels 3 * growth, channels, 3, 1, 1) self.lrelu nn.LeakyReLU(0.2, inplaceTrue) def forward(self, x): x1 self.lrelu(self.conv1(x)) x2 self.lrelu(self.conv2(torch.cat([x, x1], 1))) x3 self.lrelu(self.conv3(torch.cat([x, x1, x2], 1))) x4 self.conv4(torch.cat([x, x1, x2, x3], 1)) return x4 * 0.2 x # 残差缩放稳定训练 class RRDB(nn.Module): def __init__(self, channels64): super().__init__() self.db1 DenseBlock(channels) self.db2 DenseBlock(channels) self.db3 DenseBlock(channels) def forward(self, x): out self.db1(x) out self.db2(out) out self.db3(out) return out * 0.2 x class Generator(nn.Module): def __init__(self, in_ch3, out_ch3, nf64, nb8, scale4): super().__init__() self.conv_first nn.Conv2d(in_ch, nf, 3, 1, 1) self.body nn.Sequential(*[RRDB(nf) for _ in range(nb)]) self.conv_body nn.Conv2d(nf, nf, 3, 1, 1) # 上采样4倍用两次PixelShuffle self.upsample nn.Sequential( nn.Conv2d(nf, nf * 4, 3, 1, 1), nn.PixelShuffle(2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(nf, nf * 4, 3, 1, 1), nn.PixelShuffle(2), nn.LeakyReLU(0.2, inplaceTrue), ) self.conv_last nn.Conv2d(nf, out_ch, 3, 1, 1) def forward(self, x): feat self.conv_first(x) body_out self.conv_body(self.body(feat)) feat feat body_out feat self.upsample(feat) return self.conv_last(feat)关键参数说明nf64 是特征通道数显存够可以加到 128效果会好但训练慢一倍nb8 是 RRDB 块数量毕设用 8 到 16 都合理残差缩放系数 0.2 是 ESRGAN 论文里的经验值不加这个训练容易震荡。PixelShuffle 上采样比直接反卷积更稳定不容易产生棋盘格伪影。3.2 loss 组合只用 L1 会糊只对抗会崩训练模糊人脸增强loss 设计是核心。我试过的组合是L_total L_pixel λ1 * L_perceptual λ2 * L_ganL_pixel 用 L1 loss保证像素级接近收敛稳定。L_perceptual 用 VGG 特征距离让恢复结果在语义上像人脸而不是像素平均。L_gan 用对抗 loss逼生成器产生锐利细节。λ1 取 1.0λ2 取 0.1 是比较稳的起点。如果发现结果太平滑把 λ2 加到 0.2如果出现明显伪影降到 0.05。import torch.nn.functional as F from torchvision.models import vgg19 class PerceptualLoss(nn.Module): def __init__(self): super().__init__() vgg vgg19(pretrainedTrue).features[:35].eval() for p in vgg.parameters(): p.requires_grad False self.vgg vgg def forward(self, sr, hr): # 归一化到 VGG 输入范围 sr_feat self.vgg((sr 1) / 2) hr_feat self.vgg((hr 1) / 2) return F.l1_loss(sr_feat, hr_feat) class GANLoss(nn.Module): def __init__(self): super().__init__() self.criterion nn.BCEWithLogitsLoss() def forward(self, pred, is_real): target torch.ones_like(pred) if is_real else torch.zeros_like(pred) return self.criterion(pred, target)训练循环里先更新判别器两次再更新生成器一次这是 WGAN 之外的常见做法。判别器用 PatchGAN输出一个特征图而不是单个标量对人脸局部细节更敏感。3.3 训练参数与显存控制参数建议值说明batch_size8~16256x256 patch8G 显存用 8lr_G1e-4生成器学习率lr_D1e-4判别器学习率optimizerAdambeta10.9, beta20.99epoch100~200看 loss 曲线决定patch_size128 或 192训练时随机裁剪省显存如果显存不够把 patch_size 降到 96或者用梯度累积模拟大 batch。我一般会先跑 10 个 epoch 看 L1 loss 是否稳定下降如果震荡就调小学习率。注意对抗训练开始前先只用 L1 perceptual 训 20 个 epoch让生成器有个像样的初始输出再开判别器。直接上对抗生成器容易被判别器带偏输出一堆彩色噪点。4. 推理、评估与系统封装让毕设能演示4.1 推理脚本与模型导出训练完要把模型跑起来输入一张模糊图输出增强图。推理时不需要判别器只要生成器。下面是最小推理脚本import torch import cv2 import numpy as np from model import Generator def enhance_image(model_path, input_path, output_path, scale4): device torch.device(cuda if torch.cuda.is_available() else cpu) model Generator(scalescale).to(device) state torch.load(model_path, map_locationdevice) model.load_state_dict(state) model.eval() img cv2.imread(input_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 归一化到 [-1, 1] inp img.astype(np.float32) / 127.5 - 1.0 inp torch.from_numpy(inp).permute(2, 0, 1).unsqueeze(0).to(device) with torch.no_grad(): out model(inp) out out.squeeze(0).permute(1, 2, 0).cpu().numpy() out ((out 1) * 127.5).clip(0, 255).astype(np.uint8) out cv2.cvtColor(out, cv2.COLOR_RGB2BGR) cv2.imwrite(output_path, out) if __name__ __main__: enhance_image(./checkpoints/g_best.pth, ./test/blur.png, ./test/enhanced.png)注意归一化范围要和训练时一致。训练用 [-1,1]推理也必须 [-1,1]否则输出会偏色。这是血泪经验我见过有人训练用 [0,1]推理忘了改结果整张图发绿。4.2 评估指标PSNR 和 SSIM 怎么算才不骗自己毕设论文里一般要报 PSNR 和 SSIM。但这两个指标有个坑它们是在 YCbCr 的 Y 通道上算的不是 RGB。用错通道数值会虚高。from skimage.metrics import peak_signal_noise_ratio as psnr from skimage.metrics import structural_similarity as ssim import cv2 def evaluate(sr, hr): # 转 YCbCr只取 Y 通道 sr_y cv2.cvtColor(sr, cv2.COLOR_BGR2YCrCb)[:, :, 0] hr_y cv2.cvtColor(hr, cv2.COLOR_BGR2YCrCb)[:, :, 0] p psnr(hr_y, sr_y, data_range255) s ssim(hr_y, sr_y, data_range255) return p, s另外PSNR 高不代表视觉好。GAN 训出来的结果 PSNR 往往比纯 L1 低但人眼看着更清楚。所以论文里最好同时放主观对比图别只堆指标。4.3 系统封装PyQt 还是 Gradio毕设演示系统常见两种做法PyQt5本地桌面程序适合答辩时离线演示不依赖网络。缺点是布局代码繁琐。Gradio几行代码起一个网页界面适合录屏演示。缺点是需要浏览器且首次加载慢。如果导师要求「系统」感强一点用 PyQt5 做一个上传按钮 显示区域 处理按钮。核心逻辑就是调上面的 enhance_image 函数。如果只想快速出效果Gradio 的 Interface 三行搞定。我一般建议两个都做PyQt 用于答辩现场Gradio 用于录演示视频。提示打包 PyQt 程序时用 PyInstaller注意把模型文件一起打包进去否则换台电脑就找不到权重。模型路径用相对路径别写绝对路径。5. 避坑与排查那些让我重训三次的问题5.1 现象训练 loss 正常下降但推理输出全灰原因训练时输入归一化用了 ImageNet 的 mean/std推理时只做了 /255。两者不一致网络看到的分布完全不同。解决把归一化参数写成一个配置字典训练和推理共用同一个函数。别在两处各写一遍。5.2 现象生成人脸出现棋盘格纹理原因用了转置卷积做上采样且 kernel_size 和 stride 不匹配。解决换成 PixelShuffle或者转置卷积的 kernel_size 设为 stride 的整数倍。PixelShuffle 基本不会出这个问题推荐优先用。5.3 现象对抗训练开始后PSNR 断崖式下跌原因判别器太强生成器被压着打输出越来越离谱。解决降低判别器学习率到生成器的 1/2或者给判别器加谱归一化spectral norm。另外对抗 loss 权重从 0.05 开始加别一上来就 0.1。5.4 现象显存溢出batch_size 降到 1 还是 OOM原因RRDB 的密集连接会缓存中间特征显存占用比普通残差块高不少。解决把训练 patch 从 256 降到 128或者用 torch.utils.checkpoint 做梯度检查点。检查点会牺牲 20% 速度但显存能省一半。5.5 现象合成数据上效果很好真实模糊照片一塌糊涂原因合成用的高斯核和真实模糊核分布不匹配网络过拟合到合成退化。解决在合成时加入更多退化类型比如散焦模糊、运动模糊、JPEG 压缩伪影。另外可以用 Real-ESRGAN 的退化管道它模拟了更复杂的真实退化。如果时间允许收集几十张真实模糊-清晰配对做微调效果提升很明显。6. 进阶技巧用感知损失权重调度把 PSNR 和视觉质量同时拉高前面说了 PSNR 和视觉质量往往打架但有一个技巧可以让两者都不太差感知损失权重调度。具体做法是训练前期 perceptual loss 权重设小一点比如 0.1让网络先学好像素级重建训练后期逐步加大到 1.0逼网络关注语义细节。这样最终模型既有不错的 PSNR又有锐利的边缘。实现上就是一个线性 warmupdef get_perceptual_weight(epoch, total_epochs, start0.1, end1.0): if epoch total_epochs * 0.3: return start progress (epoch - total_epochs * 0.3) / (total_epochs * 0.7) return start (end - start) * min(progress, 1.0)配合这个调度我一般还会在最后 20 个 epoch 把 L1 权重降到 0.5让感知和对抗 loss 占主导。这样出来的结果PSNR 可能只比纯 L1 低 0.3dB但肉眼看着清楚很多。另一个实用技巧是测试时增强TTA对输入做水平翻转、垂直翻转分别推理后再平均。这个操作不增加训练成本推理时多花 4 倍时间但 PSNR 通常能涨 0.1 到 0.2dB。答辩前如果指标差一点用这招补上。最后说一个我自己的习惯每次改完模型结构或 loss先跑一个 5 分钟的小实验——用 100 张图训 10 个 epoch看 loss 曲线和一张验证图的输出。如果小实验就不对别浪费时间跑全量。这个习惯帮我省了至少三次通宵重训。希望帮到你。本文还有配套的精品资源点击获取