简介:基于Python实现的人脸识别图像超分辨率重建项目,面向计算机、人工智能、数据科学等专业的毕业设计、课程设计及期末大作业场景,可用于解决低分辨率人脸图像恢复清晰细节的实际问题。代码已通过功能验证,包含完整源码与详细注释,适合入门进阶、二次开发或立项演示。压缩包共两千个文件、大小约一百一十二MB,以一千九百五十五张jpg人脸图像作为数据集,配二十个py核心算法脚本、十个html可视化展示页面,以及xml、txt、json、yaml等配置说明文件,目录结构清晰,便于按模块调用。目前已有二百九十四人学习下载。通过该项目可完整走通人脸检测、特征提取、超分辨率重建的实现流程,并附有可交互的HTML对比页面和项目说明文档,帮助理解算法细节、复现实验效果;注释详实,可灵活改造以适配不同分辨率提升需求。
1. 人脸识别图像超分辨率重建:一份能直接跑通毕设的 Python 源码
晚上九点的园区门口,监控拍下来的人脸只有 80×80 像素,放大到 160 之后五官轮廓发虚,人脸识别模型置信度直接掉到 60% 以下。这不是摄像头不行,而是图像分辨率撑不住识别需求。基于 Python 的人脸识别图像超分辨率重建源码,解决的就是这个:从一张低分辨率人脸图重建出边缘清晰的高分辨率图,再喂给人脸识别模块。这份资源把“人脸检测 → 裁剪 → 超分重建 → 识别对比”串成完整链路,代码按模块拆分,带详细注释,适合做毕业设计、课程设计、期末大作业。计算机、人工智能、数据科学、通信、物联网方向的学生,拿到手可以直接当项目骨架。
2. 超分辨率重建到底在做什么:模型选型与链路设计
2.1 超分重建问题的本质:低分辨率里没有的信息,模型只能“猜”
图像超分辨率重建(Super-Resolution, SR)的任务是给定一张低分辨率图(Low Resolution, LR),恢复出对应的高分辨率图(High Resolution, HR)。从数学上看,退化过程可以写成:
y = (x ⊗ k) ↓_s + n
x 是原始高分辨率图,k 是模糊核(镜头散焦、运动模糊等),↓_s 是下采样倍数,n 是噪声。超分重建就是求这个过程的逆运算。问题是逆运算不唯一:一张 80×80 的图可以对应无数张 160×160 的图,模型本质上是在学一个“视觉先验”——什么区域该有纹理、哪里该是光滑皮肤。
人脸恰好是强先验目标。两只眼睛基本在一条水平线,鼻尖落在中轴线上,嘴巴在鼻子下方固定位置。这种结构化先验让网络在重建人脸时比重建风景、动物容易得多。这也是人脸超分经常单独做人脸对齐再送入网络的原因:对齐后五官位置固定,网络不需要自己“找”眼睛,只需要“画”眼睛。理解这一点,再看源码里的人脸检测和裁剪逻辑就不会觉得多余。
2.2 模型选型:为什么课设场景优先考虑 SRCNN
这套源码的核心模型是经典 SRCNN 结构。SRCNN 的思路非常直白:先把低分辨率图用 Bicubic 插值放大到目标尺寸,再通过三层卷积网络学习从“模糊的放大图”到“清晰原图”的映射。
第一层卷积做特征提取,输出 64 通道特征图;第二层做非线性映射;第三层还原成 RGB 三通道输出。整个网络参数量不大,在 CPU 上也能跑,训练时间以小时计,特别适合课程设计和毕业设计这种交付周期紧的项目。
对比一下几种常见实现:
| 模型 | 结构复杂度 | 推理速度 | 纹理效果 | 训练稳定性 | 课设友好度 |
|---|---|---|---|---|---|
| SRCNN | 低,三层卷积 | 快 | 边缘清晰但纹理一般 | 稳定 | 最高 |
| ESPCN | 低,亚像素卷积上采样 | 最快 | 边缘清晰 | 稳定 | 高 |
| SRGAN | 高,生成器+判别器 | 慢 | 纹理自然 | 不稳定,需调参 | 低 |
| SwinIR | 高,Transformer | 慢 | 效果好 | 依赖预训练权重 | 低 |
我一般不建议课设一上来就上 SRGAN。生成对抗网络训练时要同时调生成器损失、判别器损失、感知损失,跑十几个 epoch 发现图像纹理扭曲,很难定位是哪个环节出了问题。SRCNN 的调参空间小,主线逻辑清楚,答辩时能解释清楚每一层在做什么。
2.3 人脸识别与超分重建如何串成一条流水线
人脸识别图像超分辨率重建,拆开是两个任务:先找到人脸,再对人脸区域做超分。完整流水线如下:
import cv2 from models.srcnn import SRCNN from detector.face_detector import FaceDetector # 参数设置 SCALE = 2 # 超分倍数 MIN_FACE_SIZE = 40 # 小于40x40的人脸不处理 MODEL_PATH = "checkpoints/srcnn_scale2.pth" # 初始化模型 sr_model = SRCNN(scale=SCALE) sr_model.load_weights(MODEL_PATH) detector = FaceDetector(min_face_size=MIN_FACE_SIZE) # 读取输入 img = cv2.imread("input/lr_face.jpg") # 第一步:人脸检测 faces = detector.detect(img) # 返回 [(x, y, w, h), ...] # 第二步:对每个检测到的人脸区域做超分重建 for (x, y, w, h) in faces: face_roi = img[y:y+h, x:x+w] # 裁剪人脸 hr_roi = sr_model.predict(face_roi) # 超分重建 img[y:y+h, x:x+w] = cv2.resize(hr_roi, (w, h)) # 贴回原图代码里几个参数值得注意。MIN_FACE_SIZE控制最小人脸尺寸,设置太小会引入大量误检,设置太大会漏掉真正的目标;SCALE是重建倍数,SRCNN 对 2 倍重建效果好,4 倍会出现明显的光滑感缺失。之所以先检测再超分,而不是整图直接超分,好处有两个:一是只重建人脸区域,省显存,批量处理时速度更快;二是背景区域不会产生高频伪影,人脸识别模块看到的背景仍然是原始成像,不引入额外误差。
3. 数据准备与预处理:先把数据集改造成模型能吃的样子
3.1 数据集目录结构与训练/验证划分
超分重建是监督学习,需要成对的 HR 和 LR 图像。源码里默认的目录结构如下:
data/ train/ hr/ # 高分辨率原图 lr/ # 对应低分辨率图 val/ hr/ lr/ checkpoints/ # 模型权重保存位置 logs/ # 训练日志原始数据集(比如 CelebA、LFW,或者自己收集的人脸图片)只有 HR,需要先写脚本划分数据集并生成 LR。划分比例我一般取 8:2,数据量少的场景取 9:1,验证集至少留 200 张图,否则评估指标波动太大。划分脚本用random.shuffle后按比例切片即可,注意固定随机种子,保证每次运行划分结果一致。
import os import random import shutil random.seed(42) # 固定种子,保证可复现 all_images = [f for f in os.listdir("raw_hr") if f.endswith(".jpg")] random.shuffle(all_images) split_idx = int(len(all_images) * 0.8) for i, img in enumerate(all_images): split = "train" if i < split_idx else "val" src = os.path.join("raw_hr", img) dst = os.path.join("data", split, "hr", img) shutil.copy(src, dst)random.seed(42)是关键。如果不固定种子,每次运行进入训练集的图都不一样,模型效果和日志里的 PSNR 记录无法复现,答辩时被问到“这个指标怎么验证”会很被动。HR 文件准备好之后,下一步生成 LR。
3.2 Bicubic 降采样:生成低分辨率样本的常见做法
SRCNN 原文用的插值方式是 Bicubic(双三次插值),后续工作也大多沿用这一设定。生成 LR 样本时,先下采样到 SCALE 分之一,再上采样回原尺寸,这样模型输入输出的空间尺寸一致,不需要额外处理。
import cv2 import os SCALE = 2 for split in ["train", "val"]: hr_dir = f"data/{split}/hr" lr_dir = f"data/{split}/lr" os.makedirs(lr_dir, exist_ok=True) for name in os.listdir(hr_dir): hr = cv2.imread(os.path.join(hr_dir, name)) h, w = hr.shape[:2] # 降到 SCALE 分之一,再放大回原尺寸 lr_small = cv2.resize(hr, (w // SCALE, h // SCALE), interpolation=cv2.INTER_CUBIC) lr = cv2.resize(lr_small, (w, h), interpolation=cv2.INTER_CUBIC) cv2.imwrite(os.path.join(lr_dir, name), lr)注意两个细节。第一,训练和测试必须用同一种插值方式,我全程用cv2.INTER_CUBIC。如果训练集用 Bicubic、测试集用 Lanczos,退化模型不一致,PSNR 会下降 1~2 dB,肉眼可见地变糊。第二,w // SCALE可能会造成尺寸差 1 像素,建议先用w - w % SCALE把宽高规整成 SCALE 的整数倍,再开始降采样。
3.3 数据增强与 Dataset 封装
人脸超分的一个坑是模型容易记住训练集的人脸位置。如果输入总是全图,验证集 loss 会很好看,但换一张真实监控截图就崩。我在读数据时加了随机裁剪和水平翻转,让同一个人的脸出现在图像不同位置:
import torch from torch.utils.data import Dataset import cv2 import glob import random class FaceSRDataset(Dataset): def __init__(self, hr_dir, lr_dir, patch_size=96): self.hr_paths = sorted(glob.glob(hr_dir + "/*.jpg")) self.lr_paths = sorted(glob.glob(lr_dir + "/*.jpg")) self.patch_size = patch_size assert len(self.hr_paths) == len(self.lr_paths), "HR/LR 图片数量不一致" def __getitem__(self, idx): hr = cv2.imread(self.hr_paths[idx]) lr = cv2.imread(self.lr_paths[idx]) # 随机裁剪:LR 和 HR 在相同位置裁剪 h, w = lr.shape[:2] x = random.randint(0, w - self.patch_size) y = random.randint(0, h - self.patch_size) hr_patch = hr[y:y+self.patch_size, x:x+self.patch_size] lr_patch = lr[y:y+self.patch_size, x:x+self.patch_size] # 随机水平翻转 if random.random() > 0.5: hr_patch = cv2.flip(hr_patch, 1) lr_patch = cv2.flip(lr_patch, 1) # HWC -> CHW,并归一化到 [-1, 1] hr_tensor = torch.from_numpy(hr_patch.transpose(2, 0, 1)).float() / 127.5 - 1 lr_tensor = torch.from_numpy(lr_patch.transpose(2, 0, 1)).float() / 127.5 - 1 return lr_tensor, hr_tensor def __len__(self): return len(self.hr_paths)patch_size=96是经验值,覆盖人脸五官核心区域,同时保证 batch size 16 时显存占用可控。归一化到[-1, 1]而不是[0, 1],是因为模型最后的激活函数通常不设限制,输出范围与输入范围保持一致,loss 更容易下降。裁剪时 LR 和 HR 必须用同一个x, y,这个细节很多人会忽略——如果 LR 裁左上角、HR 裁中心,模型学到的映射关系就是错的。
4. 训练与评估:把训练循环跑起来,别只调网络
4.1 训练脚本结构与关键参数
SRCNN 训练本身不复杂,核心就三层卷积加一个均方误差或 L1 损失。源码里的训练脚本按 epoch 循环组织,结构如下:
import torch import torch.nn as nn from torch.utils.data import DataLoader from models.srcnn import SRCNN from dataset import FaceSRDataset # 训练参数 EPOCHS = 60 BATCH_SIZE = 16 LEARNING_RATE = 1e-4 NUM_WORKERS = 4 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SRCNN(scale=2).to(device) criterion = nn.L1Loss() optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) train_loader = DataLoader( FaceSRDataset("data/train/hr", "data/train/lr"), batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS ) for epoch in range(EPOCHS): model.train() total_loss = 0 for lr, hr in train_loader: lr, hr = lr.to(device), hr.to(device) pred = model(lr) loss = criterion(pred, hr) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() print(f"epoch {epoch+1}, loss = {total_loss / len(train_loader):.6f}")几个参数值得说明。LEARNING_RATE=1e-4对 SRCNN 来说是比较稳的起点,高于5e-4时 loss 前几个 epoch 会出现明显震荡;step_size=20, gamma=0.5表示每 20 个 epoch 学习率减半,让后期接近收敛时参数更新更精细;NUM_WORKERS=4在 Windows 上如果数据量小,改为 0 或 2 更稳,避免 multiprocessing 报错。
4.2 损失函数:L1 还是 L2
超分重建最常用的两个回归损失是 L1 和 MSE(L2)。代码里默认用nn.L1Loss(),这是有依据的。L2 损失对离群值极度敏感,训练时某个像素出现大的预测误差,梯度会被这个像素主导,导致边缘区域过度平滑。L1 是绝对误差,对离群点更鲁棒,重建出来的人脸边缘更锐利。
另一个常见选择是感知损失(Perceptual Loss),用 VGG 中间层的特征图做距离度量。SRGAN 那套方案里感知损失几乎是标配,但它需要额外加载 VGG 预训练权重,显存占用更大,调参也更麻烦。课设和毕设场景我建议先用 L1,如果成果需要锦上添花,再叠加感知损失。
4.3 评估指标:PSNR 与 SSIM 的计算方法
评估超分模型效果,PSNR(峰值信噪比)和 SSIM(结构相似性)是默认组合。PSNR 反映像素级误差,SSIM 反映亮度、对比度、结构三个维度的相似度,两者配合使用。计算代码:
import math import numpy as np def psnr(img1, img2): """img1, img2: 0-255 的 uint8 图像""" mse = np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2) if mse == 0: return float("inf") return 20 * math.log10(255 / math.sqrt(mse)) def ssim(img1, img2): from skimage.metrics import structural_similarity return structural_similarity(img1, img2, channel_axis=2)channel_axis=2对应 HWC 布局的彩色图像。PSNR 高于 30 dB 时,人眼主观上觉得“还可以”;超过 40 dB 说明两图几乎不可分辨。但 PSNR 有个经典陷阱:稍微模糊一点的图像反而可能获得更高 PSNR,因为平滑图像在像素级误差上不容易剧烈波动。所以评估时必须连同 SSIM 一起看,训练时每个 epoch 保存一次验证集的可视化对比图,比单纯盯数字靠谱得多。
5. 避坑:超分重建最容易翻车的五个场景
5.1 训练阶段的三个高频翻车点
翻车一:显存溢出(OOM)
现象:训练第 3~5 个 epoch,突然报CUDA out of memory,kill 掉进程后重跑还是崩。
原因:数据集的原始高分辨率图尺寸过大,随机裁剪后虽然变小,但 batch 内图像尺寸不统一,PyTorch 会按最大尺寸补齐,浪费大量显存。
解决:FaceSRDataset里强制patch_size=96,不要偷懒跳过去;batch size 从 16 减到 8 或 4,观察显存占用稳定后再加回去。
翻车二:训练 loss 怎么都不降
现象:loss 稳定在某个值,训练到第 30 个 epoch 依然没有下降趋势。
原因:数据没有对齐。最常见的有两种:HR 和 LR 文件名排序不一致,sorted(glob(...))在一个线程里排的是列表 A,另一个排的是列表 B,两边顺序对不上;或者 LR 做了归一化,HR 没有,网络学到的恒等映射本身就是错的。
解决:在__init__里加断言检查len(hr_paths) == len(lr_paths),再打印前 5 个文件对比;统一用/ 127.5 - 1归一化,验证集和测试集用完全相同的预处理函数。
翻车三:PSNR 涨到 31 dB,肉眼看依然糊
现象:训练日志里 PSNR 一路向上,保存的对比图却依然像蒙了一层纱。
原因:PSNR 偏向像素级误差,平滑但位置准确的图像往往得分不低,而人眼更在意边缘锐利度和纹理细节。
解决:每个 epoch 保存一次“原图-LR-重建图”三张拼接的可视化结果到logs/目录,答辩展示时直接放对比图,比报数字有说服力。
5.2 数据与评估阶段的两个隐蔽坑
隐蔽坑四:重建结果颜色发蓝或发青
现象:训练 loss 正常,重建出的脸轮廓清晰,但整张图颜色严重偏蓝。
原因:OpenCV 的imread返回 BGR 通道顺序,PyTorch 的预训练权重是按 RGB 训练的。BGR 直接送进模型,模型学到的是错位通道映射。
解决:在数据加载统一转通道,cv2.cvtColor(img, cv2.COLOR_BGR2RGB),推理脚本里也要保持一致,代码两端只要一端是 BGR 就全乱。
隐蔽坑五:训练用 Bicubic,测试用 Lanczos
现象:训练集 PSNR 刷到了 33 dB,换一张真实图片测,效果惨不忍睹。
原因:真实低分辨率图的退化方式未知,如果测试时用cv2.INTER_LANCZOS4缩放而训练时是全用INTER_CUBIC,模型学到的映射关系就不适用于测试数据。
解决:把所有处理流程统一封装成一个函数preprocess(img, scale, interpolation=cv2.INTER_CUBIC),训练、验证、推理共用;真实场景测试时宁可保持与训练完全一致的预处理,也不要临时换“看起来更高级”的插值算法。
6. 推理与可视化:把重建结果接到展示页面
6.1 单张图片推理与对比图生成
模型训练完之后,交付时通常要跑通“输入一张低分辨率图 → 输出高分辨率图”的完整推理。推理脚本和训练脚本不同点在于:模型切到eval()模式,关闭梯度计算,输出结果需要逆归一化回 0~255 再保存。
import cv2 import torch import numpy as np from models.srcnn import SRCNN device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SRCNN(scale=2).to(device) model.load_state_dict(torch.load("checkpoints/srcnn_scale2.pth", map_location=device)) model.eval() img = cv2.imread("input/test_lr.jpg") img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor = torch.from_numpy(img_rgb.transpose(2, 0, 1)).float().unsqueeze(0) / 127.5 - 1 with torch.no_grad(): output = model(tensor.to(device))[0].cpu() out_rgb = ((output.numpy().transpose(1, 2, 0) + 1) * 127.5).astype(np.uint8) out_bgr = cv2.cvtColor(out_rgb, cv2.COLOR_RGB2BGR) # 拼接对比图,方便直接展示 compare = np.hstack([img, out_bgr]) cv2.imwrite("output/compare.jpg", compare)torch.load的map_location=device是为了防止原来在 GPU 上训练的权重直接加载到 CPU 时报错;unsqueeze(0)增加 batch 维度,因为模型期望输入是[B, C, H, W]。
6.2 HTML 展示页与推理结果的对接方式
源码自带的index.html、face.html、original.html、show_2.html等页面,就是用来做过程展示的。一次完整的演示流程是:index.html作为任务首页,face.html展示人脸检测框,original.html展示原始低分辨率图像,show_2.html展示超分重建后的结果对比。
用 Flask 把这些串起来是最常见的做法:
from flask import Flask, render_template app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/face") def face(): # 显示人脸检测结果页面 return render_template("face.html", face_image="output/face.jpg") @app.route("/compare") def compare(): # 显示重建结果对比页面 return render_template("show_2.html", result_image="output/compare.jpg")render_template先把超分重建输出的对比图路径传给模板,模板里直接<img src="{{ result_image }}">渲染。注意存放路经最好用相对路径,避免部署时因绝对路径失效导致图片加载不出来。学生答辩时只要在浏览器里依次点击“人脸检测”和“重建对比”两个页面,效果比放一张训练曲线图直观得多。
我后来每次交付这类项目,都强制自己走一遍端到端验证:从一张真实手机拍摄的低分辨率照片开始,跑完检测、裁剪、重建、页面展示全流程,而不是只盯着 PSNR 数字。这套源码里带注释的model.py、dataset.py、train.py和那批 HTML 展示页,我已经照这个节奏拆过不止一次,希望帮到你。
本文还有配套的精品资源,点击获取