简介:基于Python实现的人脸识别图像超分辨率重建项目,面向计算机科学、人工智能、数据科学与大数据技术、通信、物联网等专业的在校学生、教师及企业开发者,主要解决低分辨率人脸图像重建为高清细节、辅助人脸识别的落地问题,同时可满足课程设计、期末大作业、毕业设计等应用场景。资源包共2000个文件,其中1955张jpg图像用于人脸数据集与重建效果对比,20个Python源码文件实现人脸检测、特征提取与超分重建在内的核心流程,并附带详细注释;另有HTML可视化页面、sh训练脚本、json/yaml/xml配置及说明文档,便于直接运行与二次开发。压缩包约112MB,目录结构清晰,适合从入门到进阶逐步学习。目前已有293人学习下载,借助源码注释与可视化对比页面,可迅速理解超分辨率重建的完整思路,也可在此基础上扩展功能,用于项目演示或论文实验。
1. 从低清人脸说起:超分辨率重建的任务与选型
在监控和门禁这类实际场景里,捕捉到的人脸往往只有几十乘几十像素,直接送给人脸识别模型,特征提取器很难稳定输出可用的向量。图像超分辨率重建不是简单放大插值,而是通过先验和学习从低分辨率图像恢复高频细节,让后续识别流程拿到质量更高的输入。基于Python实现这一流程,最常见的组合是OpenCV做预处理、PyTorch做超分网络、Flask包装成Web演示。这套源码正是按照这条链路组织起来:人脸检测、人脸裁剪、超分辨率重建、前后台展示,代码附带详细中文注释,可以当成毕业设计、课程设计或初期项目立项演示来用。阅读源码时,建议先跑通Web页面,再逐个模块读实现,这样能快速建立“输入一张低清图,输出一张高清图”的整体感觉。
2. 人脸检测与核心预处理:OpenCV与MTCNN的选型对比
超分重建之前必须把人脸区域定位出来。直接对整幅图超分,背景会浪费算力,而且人脸区域占比小,重建效果不聚焦。这套源码选用的是先做人脸检测,把低分辨率人脸裁剪出来,再送入超分网络。选型上有两条常见路线:OpenCV Haar Cascade和MTCNN。项目里两种都保留了,用注释标注了各自的使用边界,方便在不同场景下切换。
2.1 为什么先做检测而不是直接超分
从算力角度看,超分网络的计算量随输入尺寸平方增长。如果整张监控画面是1920×1080,人脸只有60×60像素,直接整图超分会浪费大量计算在墙壁和地面这些无意义区域。从效果角度看,训练超分模型时用的数据是人脸块,推理时输入也应当是人脸块,这样才和训练分布一致。如果把人脸从原图中裁剪出来,模型学到的纹理先验就能集中在人脸的皮肤、眼睛、边缘上。
另一个原因是人脸识别下游任务通常只需要人脸区域的标准尺寸。比如ArcFace识别模型要求输入112×112人脸,超分重建的目标尺寸可以直接对齐到识别模型要求,省去二次缩放。所以在很多实际工程里,超分模块其实是“检测+对齐+超分+特征提取”整条pipeline中的一环,而不是独立功能。讲到这一步,项目里用人脸检测到的人脸框直接作为超分输入,就是为了让整个流程串成一个完整且可解释的demo。
2.2 OpenCV Haar Cascade 的检测代码与参数
OpenCV内置的Haar Cascade是最轻量的人脸检测方案,不需要额外模型文件,也无需GPU。在项目中,用于快速定位正面人脸非常合适。下面是代码片段:
import cv2 cascade = cv2.CascadeClassifier("data/haarcascade_frontalface_default.xml") img = cv2.imread("low_face.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = cascade.detectMultiScale( gray, scaleFactor=1.1, # 每层缩放比例,越小越慢但越准 minNeighbors=5, # 目标周围至少5个候选框才保留 minSize=(30, 30) # 小于30x30的候选框直接丢弃 ) for (x, y, w, h) in faces: face = img[y:y+h, x:x+w]逻辑说明:detectMultiScale首先对图像构建金字塔,每次缩放scaleFactor,滑动窗口在每一层上检测哈尔特征,多个窗口会形成候选区域。minNeighbors用于抑制误检,值越大越好,但容易漏掉侧脸或部分遮挡的人脸。minSize针对监控场景很重要,因为远距离的小脸会被当成背景噪声。参数调优时,我对scaleFactor的体会是:1.05到1.1之间都比较稳,超过1.2会明显漏检;minNeighbors设在3到6之间,门禁场景建议5以上。如果你的摄像头俯仰角较大,比如装在门顶,Haar Cascade漏检率会明显上升,这个抉择不要等全部代码跑通再改,最好在预处理阶段就通过配置文件固定下来。
2.3 MTCNN 的引入与对齐裁剪
Haar Cascade只能给出矩形框,无法给出关键点。当人脸存在旋转或角度偏移时,超分网络学到的特征容易失真。MTCNN在检测同时输出双眼、鼻尖、嘴角共五个关键点,借助关键点可以做仿射变换,把人脸对齐到水平姿态。项目注释中提到,MTCNN虽有额外依赖,但对后续识别率提升明显。下面是对齐人脸的关键代码:
from mtcnn import MTCNN import cv2 import numpy as np detector = MTCNN() img = cv2.imread("low_face.jpg") rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results = detector.detect_faces(rgb_img) if results: keypoints = results[0]["keypoints"] left_eye = np.array(keypoints["left_eye"], dtype=np.float32) right_eye = np.array(keypoints["right_eye"], dtype=np.float32) # 计算眼睛连线与水平方向的夹角 dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) M = cv2.getRotationMatrix2D(tuple(left_eye), angle, scale=1.0) aligned = cv2.warpAffine(rgb_img, M, (rgb_img.shape[1], rgb_img.shape[0]))MTCNN返回的box是整数元组,但可能出现负数坐标或超出图像宽高的值。我一般会做一次边界裁剪,避免后续numpy切片越界。对齐后的人脸同样需要重新裁剪,再进入超分模型。另外,低分辨率人脸图像通常伴有压缩噪声,SRCNN输入前增加一个3x3的高斯滤波虽然会丢失一些边缘,但能避免模型把压缩伪影当作有效纹理去放大。我通常只在推理时使用轻度的medianBlur,训练时不加,以免改变数据分布。
2.4 预处理管线:归一化与边界问题
在把裁剪后的人脸送入超分网络前,需要统一尺寸。SRCNN这类CNN要求固定输入,项目里设为64×64或128×128,答辩时可以自行改参数。常用做法是先把裁剪块等比缩放到短边等于目标尺寸,再中心剪切到完全一致,而不是直接拉伸,否则会引入人脸的横向变形。随后进行归一化:
face_gray = cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY) face_resized = cv2.resize(face_gray, (64, 64), interpolation=cv2.INTER_CUBIC) face_norm = (face_resized.astype(np.float32) - 127.5) / 128.0这里的归一化采用类似“0.5均值、0.5标准差”的做法,把范围压到[-1, 1]。还可以改用减去数据集均值再除以方差,但后者依赖统计值,项目里为了通用性选了简单方案。注意训练时数据生成和推理时的预处理必须保持一致,很多复现跑不出效果,就是因为训练时用了一整套augmentation,推理时却只resize,导致分布漂移。
下表对比两种检测器的工程属性,帮助选型:
| 检测器 | 依赖包 | CPU推理耗时 | 关键点支持 | 适合场景 |
|---|---|---|---|---|
| Haar Cascade | opencv-python | 约15ms | 否 | 正面清晰人脸、实时预览 |
| MTCNN | mtcnn, tensorflow | 约180ms | 是 | 倾斜、遮挡、视频中的人脸 |
选择标准很直接:如果超分后的下游任务是人脸识别,优先MTCNN;如果只是演示超分效果、追求交互流畅,Haar Cascade足够了。项目源码里用一个配置文件切换检测后端,修改一处即可,这也是后面二次开发时最容易替换的部分。
3. 超分重建模型:SRCNN 结构与训练实现
拿到人脸图块后,超分网络负责学习从低分辨率到高分辨率的非线性映射。SRCNN是三卷积层结构的开山之作,参数不足一万,CPU上单帧推理只需要毫秒级,非常适合课程设计阶段完整读懂每一个参数。虽然它没有后来ESPCN、SRGAN那样激进,但作为入门和毕设基础,SRCNN的稳定性是最大优势。在GPU上训练时,batch_size可以调到64,学习率同步提高到2e-4;CPU训练则建议batch_size=16,并把线程数设置为4,训练时间会缩短一半。
3.1 SRCNN 原理:稀疏编码到深度学习的演进
传统插值算法如双三次插值、Lanczos只依赖局部邻域像素做加权平均,无法恢复高频纹理。SRCNN的核心是把“低分辨率插值放大”和“高频细节补偿”合成为一步学习。流程是:低分辨率图像先通过双三次插值放大到目标尺寸,然后交给卷积网络。第一个卷积层负责从放大图像中提取特征块,第二个卷积层把这些特征块做非线性映射,第三个卷积层把映射结果重建为最终图像。每一步都有清晰的意义,和后来端到端隐空间学习的网络相比,SRCNN更接近“手动设计+数据驱动”的过渡形态。
3.2 模型结构与参数量分析
模型在PyTorch中的定义如下:
import torch.nn as nn class SRCNN(nn.Module): def __init__(self, num_channels=3): super(SRCNN, self).__init__() self.conv1 = nn.Conv2d(num_channels, 64, kernel_size=9, padding=4) self.conv2 = nn.Conv2d(64, 32, kernel_size=1, padding=0) self.conv3 = nn.Conv2d(32, num_channels, kernel_size=5, padding=2) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.relu(self.conv1(x)) x = self.relu(self.conv2(x)) x = self.conv3(x) return x卷积层的padding选择依据是输出尺寸不变:对kernel=9,padding=4才能保持宽高;kernel=1时padding=0即可;kernel=5时padding=2。颜色通道设为3,直接处理RGB人脸块。如果训练数据是灰度人脸,可以改成1,但要注意加载权重时第一层和第三层的维度也要对应调整。
| 层 | 输入通道 | 输出通道 | 卷积核 | 参数数量 |
|---|---|---|---|---|
| conv1 | 3 | 64 | 9×9 | 15616 |
| conv2 | 64 | 32 | 1×1 | 2080 |
| conv3 | 32 | 3 | 5×5 | 2403 |
合计约2万个参数,对于超分任务来说非常轻量。这也是为什么这个项目在实际CPU部署时不用特别复杂的推理优化就能跑得起来。
3.3 训练数据准备:从公开数据集构建低分-高分对
训练SRCNN需要成对的HR原图和LR降质图。最直接的做法是准备一批高清人脸图,先做模糊下采样得到LR,再把LR放大回和HR相同的尺寸,最终让网络学习“放大后图像→HR”的差值。下面是一个实时数据对生成函数:
import cv2 import numpy as np def gen_pair(hr_img, scale=4): h, w = hr_img.shape[:2] lr_img = cv2.resize(hr_img, (w // scale, h // scale), interpolation=cv2.INTER_CUBIC) lr_up = cv2.resize(lr_img, (w, h), interpolation=cv2.INTER_CUBIC) return lr_up, hr_imgscale控制降采样倍数,一般取2、3、4。需要注意:生成LR时如果先高斯模糊再下采样,模拟的是真实镜头失焦;直接双三次下采样模拟的是数字缩放。监控场景建议加入模糊和少量噪声,避免模型只在理想缩放下有效。项目里将这批生成函数放在数据加载器中,每轮随机生成,相当于免费的数据增强。
3.4 训练代码与指标监控
训练采用MSE作为损失函数,选用Adam优化器,并在每个epoch结束后计算验证集PSNR。PSNR比Loss更直观,是超分领域最常用的指标。核心训练循环如下:
import torch import torch.optim as optim import numpy as np def psnr(img1, img2, max_val=1.0): mse = np.mean((img1 - img2) ** 2) if mse == 0: return float("inf") return 10 * np.log10(max_val**2 / mse) model = SRCNN(3) criterion = torch.nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) for epoch in range(30): model.train() for lr_up, hr in train_loader: optimizer.zero_grad() out = model(lr_up) loss = criterion(out, hr) loss.backward() optimizer.step() scheduler.step() model.eval() val_psnr = evaluate_psnr(model, val_loader) print(f"epoch {epoch}: psnr={val_psnr:.2f} dB")代码里有一个常见坑:当像素归一到[0,1]时,psnr函数的max_val应设为1.0;如果把像素输出保持在0-255,max_val才是255。项目里的统一做法是数据归一化到[0,1],所以PSNR计算时max_val=1.0。训练中如果PSNR不升,先检查数据的通道顺序和归一化一致性。
训练完成后,把state_dict保存为weights/srcnn.pth。加载时需要先创建模型实例,再load_state_dict,不能用torch.load直接给模型赋值。另一个容易出错的是自动求导,推理时记得model.eval()和with torch.no_grad(),否则显存会被不必要的计算图占住。训练步数根据经验:SRCNN在30个epoch内就有可感知的效果,但想要更细腻的纹理,需要多尺度训练,也就是说同一个HR块分别按2x、3x、4x生成三个LR对,放进同一批数据。这会让模型对输入尺寸不那么敏感。
4. 用 Flask 把模型包装成 Web 演示系统
源码包里的HTML文件并不是静态页面,而是Flask模板。index.html负责上传低清人脸图,show_2.html用左右对比方式展示原图和超分结果,face.html、original.html等则分别展示检测人脸和原图。把这些页面拼起来,就形成一个可在浏览器里点击体验的完整系统,比命令行演示更直观,也更容易在答辩中展示效果。
4.1 项目结构与前后端分工
项目目录按模块划分:
project/ ├── app.py ├── train.py ├── models/ │ └── srcnn.pth ├── utils/ │ ├── face_detector.py │ └── sr_model.py ├── templates/ │ ├── base.html │ ├── index.html │ ├── show_2.html │ ├── face.html │ └── original.html └── uploads/utils/face_detector.py封装了检测器初始化与检测逻辑,utils/sr_model.py负责加载权重和推理。app.py只关心HTTP请求与模板渲染,不直接操作模型细节。这种分层方式在注释里写得很清楚,二次开发时新增一个基于GAN的超分模型,不需要改动前端。
4.2 后端推理接口实现
app.py中最核心的是接收上传图片并进入预处理、超分、编码返回这条链路:
from flask import Flask, render_template, request, redirect, url_for import cv2 import numpy as np from utils.face_detector import detect_face from utils.sr_model import super_resolve app = Flask(__name__) model = load_srcnn() # 全局加载一次,避免每次请求都读盘 def process_image(path): img = cv2.imread(path) box = detect_face(img) # 返回(x, y, w, h) x, y, w, h = box face = img[y:y+h, x:x+w] sr_face = super_resolve(face, model) # 输出为0-255 RGB numpy return img, face, sr_face def encode_img(img_rgb): _, buffer = cv2.imencode(".jpg", cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)) import base64 return "data:image/jpeg;base64," + base64.b64encode(buffer).decode("utf-8") @app.route("/", methods=["GET", "POST"]) def index(): if request.method == "POST": file = request.files["image"] if not file or file.filename == "": return redirect(url_for("index")) path = "uploads/" + file.filename file.save(path) original, face, sr_face = process_image(path) return render_template("show_2.html", original=encode_img(original), face=encode_img(face), sr_face=encode_img(sr_face)) return render_template("index.html")代码逻辑说明:process_image返回的是BGR格式的numpy数组,但encode_img期望RGB,所以编码前先做一次通道转换。用base64嵌入HTML而非保存临时文件,可避免并发写文件时的文件名冲突。如果图像较大,base64字符串会变长,但演示场景下一般几百KB,不影响体验。
4.3 前端模板:表单提交与对比展示
index.html只需一个带enctype的文件上传表单。show_2.html通过Flask传入的base64字符串直接作为img src显示,不需要额外请求。为了直观展示重建效果,页面里可以并联显示低清脸和高清脸:
<div> <h3>低清人脸</h3> <img src="{{ face_path }}" style="width: 128px; height: 128px;"> </div> <div> <h3>超分重建结果</h3> <img src="{{ sr_face_path }}" style="width: 128px; height: 128px;"> </div>注意Flask模板变量名需要与render_template关键字保持一致。实际项目里所有页面可以继承base.html,把导航栏和页脚逻辑抽出来。show_2.html通过{% extends "base.html" %}复用公共头部,改动一次全局生效。源码包中的base.html正是这个作用,建议先读它理解模板层级。如果上传的图片本身就是高分辨率,检测到的face区域清晰,超分效果就不明显。建议在实验中先准备一张自己拍的模糊人脸图,或者用脚本先对高清图做下采样再上传。
4.4 启动与联调命令
确保依赖安装完成,项目启动只需要两条命令:
pip install flask opencv-python torch torchvision mtcnn python app.py浏览器打开 http://127.0.0.1:5000 访问。如果5000端口被占用,可以在app.run(host="0.0.0.0", port=8080)中指定其他端口。注意host=0.0.0.0可以让局域网内手机访问,方便演示。常见问题还有:uploads目录不存在导致保存失败;模型权重路径错误导致加载失败;MTCNN在CPU上首次加载较慢;cv2.imread读不到中文路径下的中文文件名。最后一个在Windows上很容易发生,一般会改成先读取bytes再用cv2.imdecode解码,而不是直接传路径给imread。
前端页面运行起来后,可以先上传一张含单人脸的图片,观察检测框是否正确。检测框错位时,超分重建的人脸可能包含大量背景,这时回到utils/face_detector.py调整minNeighbors参数。如果检测不到人脸,考虑降低minSize到20。这些调试步骤在项目注释中都有对应位置的标注,按图索骥即可。
5. 把模型导出为 ONNX:CPU 环境下的推理加速与精度验证
课程设计答辩现场通常没有GPU,PyTorch CPU推理虽然能跑,但每次加载权重、构建计算图都有额外开销。我一般会在项目交付前把SRCNN导出为ONNX,再用onnxruntime加载,这样部署机只需要安装onnxruntime和opencv,连PyTorch都不用装,同时推理速度还能提升10%-20%。
导出时需要注意固定输入尺寸:
import torch import onnxruntime as ort model = SRCNN(3).eval() dummy = torch.randn(1, 3, 64, 64) torch.onnx.export(model, dummy, "srcnn.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=11)dynamic_axes里我把batch维度设为动态,但保留宽高固定为64×64。SRCNN是全卷积网络,理论上支持任意尺寸输入,但固定尺寸后ONNX可以对卷积算子做更激进的优化,推理更稳定。导出后验证一下输出和PyTorch一致。
使用onnxruntime推理的代码是:
session = ort.InferenceSession("srcnn.onnx", providers=["CPUExecutionProvider"]) # lr_numpy 为 [N, C, H, W] 的 float32 数组 onnx_out = session.run(["output"], {"input": lr_numpy})[0]注意这里的输入是numpy数组,不需要predict模式。如果之前PyTorch推理时输入归一化到[-1,1],导出后的模型必须沿用相同归一化。我见过最频繁的报错就是导出后输入范围不匹配,导致重建图像整体偏亮或偏暗。
最后一个验证环节,建议从测试集中挑选20张低分辨率人脸,分别统计MSE和PSNR,再把重建前后人脸各送入一个现成的识别模型,记录特征向量的余弦相似度提升值。具体做法是在show_2.html页面下方增加一个段落,直接显示PSNR和推理耗时。这样答辩时一键截图即可说明:超分不仅让图像更好看,还让人脸识别准确率从78%提升到92%,这样的证据比单纯展示图像更有说服力。
这个项目在CPU上还有一个更激进的优化方向:使用torch.jit.script对模型进行trace,然后设置torch.set_num_threads(4),限制线程数能减少频繁切换的开销。对于这样的小模型,线程数过高反而变慢。
本文还有配套的精品资源,点击获取