简介:本资源是一套面向计算机专业本科生的毕业设计实战项目,聚焦中医舌诊数字化场景,基于深度学习实现舌苔图像的识别、检测与分类鉴定,配套完整GUI交互界面,适用于毕设开题、中期答辩及终期交付全流程。资源共109个文件,包含26个核心Python源码(含模型训练、推理、UI逻辑)、6个预训练.pth模型文件、7张典型舌苔样本图、2个Qt Designer生成的.ui界面文件、2个Word格式论文文档(含摘要、方法、实验与答辩PPT提纲),以及日志文件和字体资源等,整体压缩包大小为105.43MB。已有196人下载学习,项目经导师指导并获99分高分评价,代码结构清晰、注释完整、环境配置简易,小白可直接运行调试,特别适合缺乏项目经验的应届生快速上手,亦可用于课程设计或AI视觉方向的实践拓展。
1. 这不是又一个“AI看舌头”的Demo:它真能跑通从数据采集→模型训练→GUI部署的完整闭环,且已通过高校毕设答辩(99分)
你搜“舌苔识别 毕业设计”,十有八九点开的是PPT截图+几行Keras代码+一张舌图标注框——运行不了、改不了、交不了。而这份资源,是某双非院校计算机专业大四学生真实交付的毕设项目,导师签字页、答辩记录表、查重报告、99分评分表全在压缩包里。它不玩概念,不做“伪端到端”:原始舌象图来自公开中医舌诊数据集(含正常/薄白苔/黄腻苔/灰黑苔四类),用ResNet50微调完成分类,TensorFlow 2.6 + Keras实现,训练日志(events.out.tfevents.*)全部保留可回溯;最关键的是——它带一个真正能双击运行、支持拖拽图片/实时摄像头识别、结果带置信度+中医术语解释的PyQt5 GUI,不是Jupyter Notebook里敲两行predict()就完事。适合两类人:一是正卡在毕设开题/中期/答辩前两周、急需可复现、可讲解、可演示的完整项目;二是想用真实医疗影像小场景练手深度学习工程落地的新手——它把数据清洗、类别平衡、模型导出、GUI集成、打包exe这些“玄学环节”全摊开给你看,连requirements.txt里每个包的版本都锁死了(比如tensorflow==2.6.0而非2.8+,避开了TF 2.7+对Windows GPU驱动的坑)。这不是玩具,是踩过所有坑后焊死的流水线。
2. 从舌象图到分类模型:数据准备、网络选型与训练脚本实操
2.1 数据集结构与预处理逻辑:为什么必须重采样+CLAHE增强?
项目采用自建+公开数据混合集,共1247张舌象图(非公开商用,仅限学术用途),按./data/train/normal/、./data/train/yellow_greasy/等子目录组织。但原始图存在严重问题:光照不均(手机拍摄反光)、分辨率混乱(320×240到1920×1080)、舌体占比差异大(有的占图80%,有的仅30%)。直接喂给CNN会崩。所以预处理脚本preprocess.py做了三件事:
- 统一尺寸+中心裁切:先缩放至1024×768,再用OpenCV找舌体最大连通域,以该区域中心为锚点裁出512×512正方形;
- CLAHE对比度受限自适应直方图均衡化:针对舌苔纹理弱、色差小的特点,参数设为
clipLimit=2.0, tileGridSize=(8,8),比全局均衡更保细节; - 类别重采样:
normal类321张,gray_black仅187张,用SMOTE生成合成样本(注意:仅对训练集做,验证集保持原分布)。
# preprocess.py 关键片段 import cv2 import numpy as np from sklearn.preprocessing import StandardScaler from imblearn.over_sampling import SMOTE def clahe_enhance(img): clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) ycrcb[:,:,0] = clahe.apply(ycrcb[:,:,0]) return cv2.cvtColor(ycrcb, cv2.COLOR_YCrCb2BGR) # 注意:SMOTE只作用于训练集特征矩阵X_train,标签y_train # X_train.shape = (n_samples, 512*512*3) → 向量化后SMOTE smote = SMOTE(random_state=42, sampling_strategy='not majority') X_train_res, y_train_res = smote.fit_resample(X_train.reshape(-1, 512*512*3), y_train) X_train_res = X_train_res.reshape(-1, 512, 512, 3) # 恢复图像维度提示:CLAHE参数
clipLimit不能设太高(>3.0),否则舌苔边缘会过曝成白边;tileGridSize太小(如2×2)会导致局部噪声放大。我一般先用cv2.imshow()预览10张图调参,再批量处理。
2.2 为什么选ResNet50微调而非YOLO或ViT?医疗小样本下的务实选择
项目没跟风ViT或Swin Transformer,原因很实在:你的GPU是GTX 1660(6GB显存),数据量不到1500张,ViT-base需要至少12GB显存+2000+样本才能收敛。ResNet50是平衡点——它在ImageNet上预训练的底层卷积核已学会提取边缘、纹理、颜色块,对舌苔这种强纹理特征天然友好;且include_top=False后接GlobalAveragePooling2D,参数量比全连接层少87%,训练快、不易过拟合。关键改动有三处:
- 输入尺寸从224×224改为512×512(适配舌象图高分辨率需求);
- 冻结前100层(
base_model.trainable = True后手动设base_model.layers[i].trainable = False for i in range(100)),只微调顶层; - 分类头用
Dense(128, activation='relu') → Dropout(0.5) → Dense(4, activation='softmax'),4对应四类舌苔。
# model_builder.py from tensorflow.keras.applications import ResNet50 from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout from tensorflow.keras.models import Model base_model = ResNet50( weights='imagenet', include_top=False, input_shape=(512, 512, 3) # 关键!改输入尺寸 ) # 冻结前100层 for layer in base_model.layers[:100]: layer.trainable = False x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(128, activation='relu')(x) x = Dropout(0.5)(x) predictions = Dense(4, activation='softmax')(x) # 四分类 model = Model(inputs=base_model.input, outputs=predictions) model.compile( optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'] )参数说明:
Dropout(0.5)是血泪经验——不加的话,验证集准确率虚高(92%),但测试新舌图时抖动极大(置信度标准差>0.3);加了之后训练慢15%,但泛化稳定(测试集准确率86.3%,标准差<0.08)。
2.3 训练日志解析:如何用TensorBoard定位过拟合与学习率陷阱?
压缩包里的events.out.tfevents.*文件就是TensorBoard日志。别删!它们能告诉你模型到底哪里不对。启动命令很简单:
tensorboard --logdir=./logs --bind_all --port=6006然后浏览器打开http://localhost:6006。重点看三块:
- SCALARS → accuracy & val_accuracy:如果
val_accuracy在第30轮后停滞甚至下降,而accuracy继续涨,就是过拟合(项目中出现过,解决方法是早停+增加Dropout); - IMAGES → sample_images:随机抽16张训练图,确认预处理后的舌图是否清晰、无裁切错误(曾发现23张图因CLAHE过度导致舌体发白,手动剔除);
- GRAPHS → model_1:检查网络结构是否和代码一致(尤其确认
ResNet50的input_shape确实是(512,512,3),不是默认的(224,224,3))。
注意:TensorBoard日志文件名中的时间戳(如
1652188470)对应Unix时间戳,可用在线工具转为北京时间,对照训练脚本里的datetime.now().strftime("%Y%m%d_%H%M%S"),确保日志和代码版本匹配。
3. GUI界面开发:PyQt5如何承载深度学习模型并保证响应不卡顿?
3.1 主窗口架构:QMainWindow + 多线程推理,避免GUI冻结
PyQt5主线程负责UI渲染,模型推理若在主线程跑,点击“识别”按钮后整个界面会假死3~5秒(ResNet50在CPU上推理单图约2.8s)。解决方案是QThread+信号槽机制:
InferenceWorker类继承QObject,封装model.predict()调用;- 主窗口创建
QThread实例,将InferenceWorkermoveToThread; - 点击按钮触发
worker.start(),推理完成 emitresult_signal,主窗口槽函数接收并更新UI。
# gui/main_window.py from PyQt5.QtCore import QThread, QObject, pyqtSignal from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton class InferenceWorker(QObject): result_signal = pyqtSignal(dict) # 发送 {'label': 'yellow_greasy', 'confidence': 0.92} def __init__(self, model, img_array): super().__init__() self.model = model self.img_array = img_array def run(self): # 预处理:归一化+expand_dims img_norm = self.img_array.astype('float32') / 255.0 img_batch = np.expand_dims(img_norm, axis=0) pred = self.model.predict(img_batch)[0] label_idx = np.argmax(pred) labels = ['normal', 'thin_white', 'yellow_greasy', 'gray_black'] self.result_signal.emit({ 'label': labels[label_idx], 'confidence': float(pred[label_idx]) }) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.model = load_model('./models/best_model.h5') # 加载已训练模型 self.thread = QThread() self.worker = None def on_recognize_clicked(self): # 从QLabel获取当前显示的舌图(已转为numpy array) img_array = self.get_current_image_as_array() self.worker = InferenceWorker(self.model, img_array) self.worker.moveToThread(self.thread) self.worker.result_signal.connect(self.update_result_display) self.thread.started.connect(self.worker.run) self.thread.start()关键细节:
img_array.astype('float32') / 255.0必须在子线程内做,不能在主线程预处理好再传——因为PyQt5的QPixmap对象不能跨线程传递,必须传原始numpy数组。
3.2 中医术语解释模块:硬编码规则表 vs 轻量级知识图谱
GUI右侧的“中医解读”区域不是简单打印'yellow_greasy',而是映射到临床意义。项目采用轻量级规则表(非大模型),因为毕设不允许外网调用API。diagnosis_rules.py定义:
DIAGNOSIS_MAP = { 'normal': { 'term': '正常舌象', 'description': '舌质淡红,舌苔薄白,干湿适中,颗粒均匀。', 'clinical_significance': '脏腑功能协调,气血充盈,胃气充足。' }, 'yellow_greasy': { 'term': '黄腻苔', 'description': '舌苔色黄而黏腻,颗粒细腻致密,刮之难去。', 'clinical_significance': '湿热内蕴,多见于脾胃湿热、肝胆湿热证。' }, # ... 其他两类 }UI中调用:
# 在update_result_display槽函数里 rule = DIAGNOSIS_MAP.get(result['label'], {}) self.label_term.setText(rule.get('term', '未知')) self.text_desc.setPlainText(rule.get('description', '')) self.text_significance.setPlainText(rule.get('clinical_significance', ''))为什么不用JSON或数据库?因为毕设答辩要求“零依赖、一键运行”。硬编码Python字典,打包进exe后无需额外文件,且修改术语只需改
.py文件,比改JSON更易调试。
3.3 打包为独立exe:PyInstaller的隐藏坑与必备参数
用pyinstaller --onefile --windowed --add-data "models;models" --add-data "data;data" main.py打包后,exe双击闪退?常见原因有三:
- TensorFlow DLL缺失:PyInstaller无法自动抓取TF的C++动态库,需手动指定路径:
pyinstaller --onefile --windowed ^ --add-binary "C:\Users\XXX\AppData\Roaming\Python\Python38\site-packages\tensorflow\libtensorflow_cc.dll;." ^ --add-binary "C:\Users\XXX\AppData\Roaming\Python\Python38\site-packages\tensorflow\libtensorflow_framework.dll;." ^ --add-data "models;models" --add-data "data;data" main.py - PyQt5插件路径错误:添加
--add-binary指向PyQt5的plugins目录(如C:\Python38\Lib\site-packages\PyQt5\plugins); - 模型加载路径硬编码:GUI代码里写
load_model('./models/best_model.h5'),但exe解压后工作目录是临时文件夹,应改用:import sys import os def resource_path(relative_path): if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) model = load_model(resource_path('models/best_model.h5'))
血泪经验:打包前务必用
--debug参数生成log,闪退时看log末尾报错——90%是DLL找不到或路径不对。我第一次打包失败,log里ImportError: DLL load failed while importing _pywrap_tensorflow_internal,折腾3小时才找到正确的DLL路径。
4. 避坑指南:99分毕设背后踩过的7个真实雷区
4.1 现象:训练时loss降得很快,但val_accuracy卡在60%不上升
原因:数据集划分时未按类别分层(stratify),导致验证集里gray_black类只有5张图,而训练集有180+张,模型根本没见过足够灰黑苔样本。
解决:用sklearn.model_selection.train_test_split时加stratify=y参数,确保各类在训练/验证集中比例一致。
4.2 现象:GUI中摄像头实时识别帧率极低(<2fps)
原因:OpenCV的cv2.VideoCapture(0)默认使用V4L2后端,在Windows上性能差;且每帧都做CLAHE+resize,CPU扛不住。
解决:① 改用DirectShow后端:cap = cv2.VideoCapture(0, cv2.CAP_DSHOW);② 将CLAHE移至模型输入预处理(即只对送入模型的单帧做),摄像头预览用原始图;③ 降低预览分辨率:cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640); cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)。
4.3 现象:打包exe后,点击“识别”按钮无反应,控制台无报错
原因:PyInstaller未打包h5py依赖的zlib.dll和hdf5.dll,导致load_model()静默失败。
解决:在spec文件中添加binaries=[('path/to/zlib.dll', '.'), ('path/to/hdf5.dll', '.')],或用--add-binary命令行参数显式包含。
4.4 现象:论文里写的“准确率92.3%”,但自己跑测试集只有84.1%
原因:原始论文用的是model.evaluate()在训练集上测的(错误!),正确做法是用独立测试集(test_data)且关闭dropout(model.evaluate(test_data, verbose=0))。项目已修正,测试集准确率86.3%±0.7%(5次随机划分均值)。
解决:严格区分train/val/test三集合,测试集绝对不可参与任何训练过程(包括数据增强参数调整)。
4.5 现象:舌图拖入GUI后显示变形(拉伸成宽屏)
原因:QLabel.setPixmap()未设置Qt.KeepAspectRatio,图片被强制填满控件。
解决:在设置pixmap后加label.setScaledContents(False),并用pixmap.scaled(width, height, Qt.KeepAspectRatio)预缩放。
5. 模型导出与跨平台部署:如何让导师电脑上也顺利运行?
5.1 从HDF5到SavedModel:兼容性升级的关键一步
项目原始模型保存为best_model.h5(Keras原生格式),但它在不同TensorFlow版本间兼容性差(TF 2.6保存的h5在TF 2.9可能load失败)。毕设答辩时导师电脑装的是TF 2.8,为保万无一失,必须导出为SavedModel格式:
# export_model.py import tensorflow as tf from model_builder import build_model # 重建模型结构 model = build_model() # 注意:不加载权重,只建结构 model.load_weights('./models/best_model.h5') # 单独加载权重 # 导出为SavedModel tf.saved_model.save( model, './models/saved_model', # 输出目录 signatures={ 'serving_default': model.call.get_concrete_function( tf.TensorSpec(shape=[None, 512, 512, 3], dtype=tf.float32, name='input_image') ) } )导出后,./models/saved_model目录下会有assets/、variables/、saved_model.pb三个部分。GUI代码中加载方式改为:
# 替换原来的 load_model() model = tf.keras.models.load_model('./models/saved_model')优势:SavedModel是TensorFlow官方推荐格式,跨版本兼容性极强;且支持TensorRT加速(虽毕设未用,但留了接口)。
5.2 环境隔离方案:conda vs venv,为什么最终选venv?
项目最初用conda创建环境(conda create -n tongue python=3.8),但打包时发现conda环境路径硬编码在.pth文件里,PyInstaller无法正确解析。改用venv后问题消失:
# 推荐流程 python -m venv env_tongue env_tongue\Scripts\activate.bat # Windows pip install -r requirements.txt # 确保requirements.txt里明确写出版本号 tensorflow==2.6.0 keras==2.6.0 pyqt5==5.15.6 opencv-python==4.5.5.64requirements.txt内容必须锁定版本,尤其tensorflow和pyqt5——pyqt5>=5.15在某些Windows系统上会安装6.x,导致QThread信号槽失效(PyQt6 API变更)。
5.3 导师电脑无GPU?CPU推理优化三板斧
导师电脑大概率是核显(Intel UHD Graphics),连CUDA都不装。此时CPU推理速度是瓶颈。项目已内置优化:
| 优化项 | 实现方式 | 效果 |
|---|---|---|
| 模型量化 | 使用tf.lite.TFLiteConverter转为int8模型 | 推理速度提升2.3倍,精度损失<0.5%(测试集85.8%→85.4%) |
| OpenMP线程数限制 | os.environ['TF_NUM_INTEROP_THREADS'] = '2'os.environ['TF_NUM_INTRAOP_THREADS'] = '4' | 防止多线程争抢,CPU占用率从100%降至65%,风扇不狂转 |
| 预热推理 | GUI启动时自动执行一次model.predict(np.zeros((1,512,512,3))) | 首次识别延迟从3.2s降至1.1s(避免JIT编译耗时) |
# gui/app.py 开头 import os os.environ['TF_NUM_INTEROP_THREADS'] = '2' os.environ['TF_NUM_INTRAOP_THREADS'] = '4' # 启动时预热 dummy_input = np.zeros((1, 512, 512, 3), dtype='float32') _ = model.predict(dummy_input) # 丢弃结果,只为触发编译从那以后我每次给导师演示前,都强制走一遍“预热→拍舌图→识别→看中医解读”全流程,确保答辩现场不翻车。哪怕多花10秒,也比答辩时卡住30秒强——那30秒,够你重写整个毕设了。希望帮到你。
本文还有配套的精品资源,点击获取