1. 项目整体设计与技术选型
1.1 这个项目解决的是什么问题
京剧脸谱识别系统,本质上是一个图像分类任务。说白了,就是让电脑看一眼京剧脸谱的图片,然后告诉我们这是哪个人物、属于什么谱式,或者对应什么颜色类型。很多人在拿到这个题目的时候会误以为它只是个简单的“图片分类Demo”,但实际上要真正把它做成一个能演示、能答辩、能拿出手的毕设,背后牵扯的东西并不少:数据集怎么来、模型怎么训、界面怎么做、整个系统怎么串起来,每一步都有讲究。
我最初接到这个课题时,第一反应是“这不就是个CNN分类吗”,但做到中期才发现,真正花时间的不是模型训练,而是数据整理、标签体系设计和界面交互打磨。这个项目对本科毕设来说,处在“深度适中、工作量明确、展示效果好”的位置,选它作为毕设题目是比较稳妥的,既能体现深度学习基本功,又能通过PyQt界面展示完整的系统能力,答辩的时候也容易把故事讲完整。
1.2 为什么选择PyQt + 深度学习这套方案
先说结论:这个组合是毕设场景下最“稳”的方案之一。
模型端用深度学习做图像分类,几乎是目前的标准答案。传统图像处理方案(比如基于颜色直方图、纹理特征加SVM分类)虽然能跑,但泛化能力弱,面对不同光照、不同画风的脸谱图片时准确率波动很大。尤其是京剧脸谱这种“颜色鲜明、纹样复杂”的目标,深度卷积网络对纹理特征的提取能力明显优于手工特征设计。
界面端选PyQt而不是Web前端,主要是考虑到三点:
第一,PyQt是桌面应用方案,展示效果直接,答辩时演示不需要依赖浏览器和服务器,打开就能跑,环境问题少一半。第二,PyQt的信号槽机制和Python语言的开发效率结合得很好,一个做深度学习的学生不需要额外学一套前端框架,就能把界面做得比较完整。第三,PyQt在中文环境下的编码支持和控件丰富度都不错,文件选择、图片预览、结果展示这些功能都有现成组件可用。
1.3 项目技术栈全览
我最终使用的技术组合如下:
- 深度学习框架:PyTorch,版本1.10以上,配套torchvision
- 模型结构:ResNet18作为骨干网络,替换最后的全连接层,输出类别数为自定义分类数量
- 界面框架:PyQt5,配合qdarkstyle做了深色主题,视觉上比较像专业软件
- 数据处理:Pillow + OpenCV,负责图片读取、尺寸归一化、数据增强
- 环境管理:conda虚拟环境,Python版本3.8
- 推理部署:直接加载训练好的.pth权重文件,使用PyTorch的CPU推理模式
注意:如果电脑没有NVIDIA显卡,也完全不影响这个项目的完成度。脸谱图片分辨率不高、类别数量有限,CPU推理速度完全够用,单张图片识别在0.5秒以内,不会出现卡顿感影响了答辩演示。
2. 数据集构建与预处理——这个项目的“隐形工作量”
2.1 京剧脸谱数据的来源与采集策略
数据是图像分类项目的命脉。我一开始以为找脸谱图片很简单,毕竟京剧脸谱的图片网上一搜一大把,但真正清洗下来才发现问题很多:图片分辨率参差不齐、有水印的、有漫画风格的、有实物面具拍摄的、还有舞台剧照的,混在一起直接训练,模型会学出各种各样的奇怪特征。
我的处理方式是分来源收集,保证数据多样性:
- 来源一:公开图像数据集平台上的京剧脸谱子集,这部分图片质量相对干净,作为基础数据
- 来源二:从公开的戏曲文化图库中整理出的经典脸谱图,包含不同流派绘制的差异
- 来源三:手工拍摄的实体脸谱面具照片,给模型增加真实场景下的噪声
这里要特别提醒一点:图片采集过程中一定要记录来源和授权情况。毕设虽然不商用,但答辩时如果被问到数据来源,你得能说清楚。另外,最终成果展示页面上要标注图片来源,这也是学术规范的一部分。
2.2 数据清洗与标注流程
拿到原始图片后,清洗是第一优先级。我踩过的坑包括:有些图片虽然叫“关羽脸谱”,但实际内容是关公的全身剧照,脸只占画面的一小部分;还有的图片是多个脸谱的合影,根本没法直接用于单分类训练。
我的清洗规则是这样定的:
- 分辨率小于256×256的图片直接剔除,因为放大后细节丢失严重,模型学不到有效的纹样特征
- 面部占比过小的图片剔除,这个通过肉眼检查,花了大概两个晚上分批看完
- 重复图片去重,用图片哈希算法算感知哈希值,直接删掉重复项
- 水印过于严重的图片剔除,水印区域会影响卷积层提取特征
标注方面,我采用“文件夹即标签”的方式,每个类别的图片放在对应目录下,用脚本自动生成CSV格式的标注文件,包含图片路径和类别ID两列。这样既简单又不容易出错,比手动写标注文件效率高一个量级。
经验:标注是枯燥活,但千万别交给脚本自动猜。脸谱类别之间的差异很细微,比如同样以黑色为主色调的脸谱,可能一个是张飞一个是项羽,最稳妥的办法是分类目录建立后,每个目录随机抽查20张图确认没有混入错误样本。
2.3 数据增强与样本均衡
京剧脸谱数据集的类别不均衡问题非常明显。像关羽这种知名度极高的角色,图片数量可能有200多张;但一些偏门的谱式角色,全部能搜到的干净图片可能就30~40张。如果不做处理,模型会对高频类别过拟合,低频类别几乎不学习。
我的处理方案是组合拳:
- 对低频类别做过采样,即重复加载样本,同时配合数据增强做差异化变换
- 使用torchvision的transforms做随机水平翻转、随机旋转±10度、随机亮度饱和度调整、随机裁剪缩放
- 增加一个小技巧:把图片转换为灰度图后和原图做一定比例的融合,模拟不同年代老照片的色调差异
最终数据规模大约在两千多张图,单类最少的也有60张以上。对于毕设级别的人脸/脸谱分类,这个规模已经够用。如果低于50张每类,模型基本就很难收敛到理想效果,这个时候就要考虑用迁移学习来弥补数据量不足的问题。
3. 深度学习模型设计与训练
3.1 模型选型:为什么用ResNet18而不是自研CNN
第二个高频问题就是“你为什么不自己设计一个网络结构”。我在答辩时被问到过,这个问题的回答思路很重要。
我的回答逻辑是:毕设的核心目标是构建一个完整可用的识别系统,模型结构应该优先选择已有充分验证的基础架构。自研CNN当然可以,但如果没有足够的实验验证,很难说明你设计的网络为什么有效。选择ResNet18的理由集中在两点:
- 残差结构解决了深层网络梯度消失问题,在数据量不算非常大的情况下也能训得动
- 模型参数量约1100万左右,CPU推理仍然可以接受,不会像ResNet50那样在答辩演示时明显卡顿
如果你想展示自己的工作增量,可以在ResNet18的基础上做两点调整:一个是在每个残差块的输出后增加一个轻量级的注意力模块,另一个是把最后的全局平均池化改成注意力池化。这样做既能保留主干网络的稳定性,又能讲出“我做了改进”的故事。
3.2 训练参数设置与调优过程
下面是我实际使用的训练配置,不一定是最优解,但整个过程比较顺:
import torch from torchvision import models, transforms from torch.utils.data import DataLoader, Dataset device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后的全连接层,假设有 num_classes 个类别 model.fc = torch.nn.Linear(model.fc.in_features, num_classes) model = model.to(device)优化器我用了SGD,初始学习率0.001,动量0.9,权重衰减1e-4。很多人习惯直接用Adam,但迁移学习场景下SGD配合较小的学习率往往收敛更稳,这是我反复实验后的体感。学习率调度用的StepLR,每10个epoch衰减为原来的0.1倍,总共训练50轮。
训练时设置了早停机制,监控验证集准确率,连续8个epoch不上升就停止。实际训练中大约在第30轮左右达到稳定状态,验证集准确率在92%到95%之间浮动,具体数值取决于测试集的划分比例。
3.3 模型评估与效果对比
评估部分不能只给一个准确率就算完事,我做了三层的评估体系:
- 混淆矩阵:重点看哪些类别之间容易互相混淆,比如红脸类和紫红脸类的边界处于模糊地带,这在纹理上确实有相似性
- 单类别精确率和召回率:找出模型表现最差的3个类别,分析原因,通常是因为这些类别的图像风格差异过大,模型没有学到统一的特征
- 推理耗时统计:分别统计CPU和GPU环境下单张图片的平均耗时,便于答辩时展示系统的实用性
为了体现对比分析,我还加了两个对照实验:一个是不做任何数据增强直接训练,验证集准确率约81%,明显低于增强后的95%;另一个是用浅层三层卷积网络训练,准确率约76%。这些对比数据比任何口头描述都有说服力,答辩老师看PPT看到这个对比表基本就会点头。
4. PyQt界面开发与系统集成
4.1 界面框架设计与模块划分
界面部分不能做成“一个窗口加一个按钮”这种敷衍形态。我在动手之前先画了简单的模块图,把系统拆成四个区域:
- 左侧:图片选择区,支持单张选择、批量选择、以及摄像头截图三种输入方式
- 中间:图片显示区,展示原始图片和预处理后的图片,方便对比观察
- 右侧:识别结果区,显示预测类别、置信度得分,以及该类别的文字介绍
- 底部:历史记录区,保存每一次识别记录,支持导出CSV
这四个区域通过PyQt的布局管理器排列,窗口大小调整时控件能自适应缩放。整体风格用QSS样式表做了一套深色主题,配色上参考了传统戏曲脸谱的红黑金元素,让UI风格贴合项目主题。
4.2 核心功能实现:图片识别与实时预览
识别流程是:用户点击选择图片按钮 -> QFileDialog打开文件浏览器 -> 选定图片后通过QPixmap显示原图 -> 点击识别按钮 -> 图片经过预处理 -> 模型推理 -> 结果展示。
这里有一个容易被忽略的细节:QPixmap显示的是原始任意尺寸的图片,但模型输入要求固定尺寸(我是224×224)。两者之间需要一个统一的转换层,把待识别图片缩放到224×224的RGB输入张量,同时不要破坏原始显示图片的宽高比。
如果你直接对原始图片做resize然后显示,脸谱会被拉变形,看起来非常不专业。我的做法是:原图用Qt的setScaledContents配合等比例缩放显示,推理时单独走一份预处理管线。
from PyQt5.QtWidgets import QFileDialog, QLabel from PyQt5.QtGui import QPixmap import cv2 import numpy as np import torch from torchvision import transforms def on_select_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.jpg *.png *.jpeg *.bmp)") if not path: return self.current_path = path pixmap = QPixmap(path) # 等比例缩放到显示区域 scaled = pixmap.scaled( self.label_image.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.label_image.setPixmap(scaled) def on_predict(self): if not hasattr(self, 'current_path'): return img = cv2.imread(self.current_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (224, 224)) img = transforms.ToTensor()(img).unsqueeze(0) # 归一化,均值和方差与训练时保持一致 mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1) std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1) img = (img - mean) / std self.model.eval() with torch.no_grad(): outputs = self.model(img) probabilities = torch.nn.functional.softmax(outputs, dim=1) confidence, predicted = torch.max(probabilities, 1) # 更新界面显示 self.label_result.setText(f"预测类别:{self.classes[predicted.item()]}") self.label_confidence.setText(f"置信度:{confidence.item() * 100:.2f}%")4.3 模型加载与推理优化
模型加载放在界面初始化阶段完成,不要在用户点按钮时才加载,否则第一次点击会有明显的卡顿感。具体做法是在MainWindow的构造函数里调用load_model方法,把权重文件路径写死在配置文件里。
推理优化方面,有一个很实用的小技巧:如果模型比较大、CPU推理略慢,可以先转成TorchScript格式再加载。TorchScript在CPU上的推理速度通常有10%到20%的提升,而且摆脱了对模型定义代码的强依赖,部署更干净。转换代码很简单:
scripted_model = torch.jit.script(model) scripted_model.save("face_recognition.pt")加载时直接torch.jit.load即可。不过注意,如果你的模型里用了BatchNorm层,转TorchScript前要确保模型处于eval()模式,否则batch统计信息会被错误嵌入。
5. 毕业答辩准备与高频问题应对
5.1 答辩演示流程设计
答辩时间通常只有10分钟左右,演示流程必须提前设计好,不能现场手忙脚乱。我的演示顺序是:
- 用一句话介绍项目背景(京剧脸谱是传统文化符号,识别系统有文化科普价值)
- 展示系统界面全貌,让老师看到整体功能分区
- 演示单张识别,选一张有代表性的脸谱图,预期准确率高、置信度高
- 演示批量识别,让老师看到系统能处理多张图片
- 展示混淆矩阵和准确率曲线,说明模型训练过程
- 总结技术栈和未来改进方向
其中第3步选图是关键,建议提前准备三张标准测试图,都是模型表现很好的样本,确保演示效果稳定。千万别拿一张没见过的图现场赌模型识别率——虽然大概率没问题,但万一翻车很尴尬。
5.2 高频答辩问题与应答思路
答辩老师最爱问的问题就那几个,提前准备好答案,现场就不慌。
“你的数据从哪里来的?”——回答来源构成和清洗流程,强调数据多样性。
“准确率多少?怎么评估的?”——给出验证集准确率和混淆矩阵分析,如果老师再追问,就补充说单类别的精确率召回率也做了统计。
“为什么用ResNet18,不用更大模型?”——从数据规模、推理效率和效果三个角度回答,核心逻辑是:数据量有限时大模型容易过拟合,CPU推理时大模型有性能损耗,ResNet18的精度已经满足系统需求。
“你的系统和现有方案比有什么优势?”——不要踩已有系统,而是强调“轻量化和易部署”,在普通笔记本上就能跑,不依赖GPU服务器。
“未来怎么改进?”——可以提两个方向:一是引入更多类别的数据覆盖,二是换成检测+分类的两阶段模型以支持多人脸谱图,让系统从图像分类升级为图像理解。
5.3 项目亮点包装与改进方向
给项目的“亮点”排序是很有必要的。我做这个项目时总结出的三个亮点是:
第一,数据管线的完整性。从爬取清洗到增强划分,形成了一套可复用的数据处理流程,不只是一张训练集扔进模型完事。
第二,系统集成的完整度。界面、模型、历史记录管理、批量输出——形成了一个闭环系统,而不是一个孤立的模型脚本。
第三,实验对比的说服力。设计了三组对照实验,用数据证明了数据增强和模型结构选择的价值。
改进方向如果你有精力,可以加一个“实时视频识别”模块,调用摄像头或用预录视频做逐帧识别,这个功能答辩效果非常惊艳,但工作量不小,需要评估剩余时间是否充足。
6. 踩坑记录与实操经验
6.1 环境配置阶段踩过的坑
我在配置环境时遇到过一个很典型的问题:OpenCV在Python 3.9以上版本偶尔会出现import cv2报错的情况,原因是对应的wheel包不兼容。解决办法是用conda安装opencv-python-headless版本,或者降级Python版本到3.8。
另一个常见坑是torch和torchvision版本不匹配导致torchvision.models里某些预训练权重加载失败。我的建议是尽量保持torch和torchvision版本完全一致,使用pip install torch torchvision --index-url时看清楚配套关系。
6.2 模型训练阶段的两个教训
第一个教训是:过早停止训练会漏掉精度提升。我前期用的是固定50个epoch但没加早停,后来加了早停机制反而发现验证集准确率在35到40轮之间还会有一个小跳跃,这通常是因为学习率衰减后模型进入了更细致的参数空间。
第二个教训是:测试集划分比例不能太小。我最早用的8:2划分,测试集只有400多张图,置信区间波动大,每次评估结果都不稳定。改成7:2:1的train/val/test划分后,评估结果稳定很多,答辩时拿出的数字也更可信。
6.3 PyQt与深度学习框架集成的坑
PyQt加载模型推理时有个棘手问题:如果模型加载和推理都在主线程跑,界面会在推理期间“无响应”。虽然单张图片推理时间短看不出来,但批量识别的时候就很明显,窗口会卡住十几秒。
解决办法是用QThread把推理任务放到子线程,或者使用QApplication.processEvents()手动刷新事件循环。最简单的方案是QThread加信号槽:子线程推理完成后发射一个信号,主线程槽函数更新界面。
from PyQt5.QtCore import QThread, pyqtSignal class InferenceThread(QThread): result_ready = pyqtSignal(int, float) def __init__(self, model, image_tensor, classes, parent=None): super().__init__(parent) self.model = model self.image_tensor = image_tensor self.classes = classes def run(self): self.model.eval() with torch.no_grad(): outputs = self.model(self.image_tensor) probabilities = torch.nn.functional.softmax(outputs, dim=1) confidence, predicted = torch.max(probabilities, 1) self.result_ready.emit(predicted.item(), confidence.item())在界面那边连接result_ready信号到更新方法即可。这个改动之后,批量识别时窗口能保持流畅刷新,用户的体验完全不一样。
7. 一次完整的毕设收尾心得
最后说点实际的体会。这个项目做完之后我最深的感受是:毕设项目最重要的不是模型多先进,而是形成闭环。数据能说清楚、模型能训练、界面能用、演示能跑、论文能写——这五步连贯起来,项目就立住了。
我个人在实际操作中有一个建议:开发过程中随时截图和记录版本,包括每个阶段的界面效果、训练曲线、某个bug的修复前后对比。这些材料后期写论文和做答辩PPT时都是很好的素材,能省下大量重新回忆整理的时间。
另外,代码模块化一定要从第一天就坚持。我用的是简单但有效的分工:dataset.py管数据加载,train.py管训练流程,inference.py管推理封装,main_window.py管界面逻辑。答辩时如果老师要看代码,这个结构一眼就能看懂,比一堆混乱的函数强太多。
再分享一个小技巧:把整个项目的依赖导出成requirements.txt,并且在文档里写明conda环境的创建命令。答辩环境随时可能换电脑,提前把环境搭建说明写好,能避免现场漏装依赖的尴尬。
这个项目后期还可以扩展的方向不少,比如加入语音播报脸谱角色介绍、接入知识图谱做人物关系展示、或者把模型换成更轻量的MobileNet系列拿到嵌入式设备上运行。但作为毕业设计,先把当前这套系统打磨到无懈可击,再考虑延伸,才是正确的顺序。