简介:本资源是一套面向本科生毕业设计与中医智能化实践的舌苔识别检测鉴定系统完整实现,聚焦传统中医舌诊数字化落地,适用于深度学习初学者、医学AI方向课程设计及毕业课题开发。压缩包共110个文件,含26个核心Python源码(含GUI界面构建、模型训练与推理脚本)、6个预训练.pth模型、7张舌象样本图与2个.ui界面文件,辅以毕业论文(docx)、数据增强配置(json)及字体资源(ttc),整体容量105.47MB,结构清晰,开箱即用。已有266人学习下载,涵盖从数据集构建(含DCGAN舌象生成)、ResNet/CNN模型设计、图像增强策略到体质辨识逻辑实现的全流程技术细节,论文章节完整覆盖需求分析、网络设计、实验验证与可行性论证,特别适合需要可运行代码+理论支撑+规范文档三位一体方案的学习者。
1. 舌苔识别不是“拍张照就出结果”:一个毕业设计级系统为什么必须同时搞定数据、模型、GUI和论文逻辑链
你手头有一份标着“基于深度学习的舌苔识别检测鉴定系统python源码(带GUI界面)+模型+毕业论文.zip”的压缩包,点开发现:有train.py、predict.py、ui_main.py、model_best.pth、一份Word格式的毕业论文,还有个叫dataset/的文件夹——但里面只有23张图,且命名混乱(tongue_001.jpg、舌苔_2.jpg、IMG_20230512_142233.jpg混在一起)。这不是一个能直接部署的工业系统,而是一个典型高校毕业设计闭环产物:它用真实临床逻辑包装技术实现,靠可复现的最小路径覆盖“数据采集→标注→训练→推理→可视化→论文论证”全链条。真正卡住90%同学的,从来不是模型结构本身,而是舌苔图像特有的低对比度、强反光、舌体形变、背景干扰,以及GUI里按钮一按就报错、论文里“准确率98.7%”却没写清测试集构成、混淆矩阵也没贴出来这些细节。本文不讲ResNet怎么改,只带你从这个zip包出发,把“舌苔识别”这件事在毕业设计语境下真正跑通、讲清、防翻车——适合医学影像方向本科生、中医信息学初学者,以及需要快速交付答辩系统的指导教师。
2. 数据准备:舌苔图像不是通用数据集,必须自己动手清洗、标注、增强
舌苔识别的数据瓶颈比模型选择更致命。公开数据集如TCM-Tongue-2020(2020年发布)仅含1276张图,且未公开原始标注协议;而多数学生直接用手机拍摄,导致光照不均、舌体偏移、背景杂乱。本系统所附dataset/目录实为典型“现场采集原始素材”,需重构为标准训练流程可用格式。
2.1 图像清洗:先解决“拍得不像舌苔”的问题
舌苔图像常见三类失真:
- 反光斑块:舌面唾液或拍摄角度导致局部过曝,像素值饱和(R/G/B > 245连续区域);
- 背景污染:手机拍摄时牙龈、嘴唇、手指边缘混入,占用>15%画面面积;
- 舌体偏移:舌体未居中,有效区域<画面60%。
我一般用OpenCV做批量预处理,核心是先定位舌体再裁切,而非简单缩放:
import cv2 import numpy as np import os def preprocess_tongue_image(img_path, output_dir): img = cv2.imread(img_path) if img is None: print(f"跳过损坏图像: {img_path}") return # 步骤1:转HSV空间,利用舌体在H通道的集中性(红-橙色系) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) h_channel = hsv[:,:,0] # 步骤2:自适应阈值分割舌体粗轮廓(避开反光干扰) # 使用Otsu法但限定在H通道中值附近±30范围内 h_median = np.median(h_channel[h_channel > 0]) lower_h = max(0, h_median - 30) upper_h = min(180, h_median + 30) mask_h = cv2.inRange(hsv, (lower_h, 30, 30), (upper_h, 255, 255)) # 步骤3:形态学闭运算填充空洞,再找最大连通域(即舌体) kernel = np.ones((5,5), np.uint8) mask_clean = cv2.morphologyEx(mask_h, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(mask_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: print(f"未检出舌体: {img_path}") return largest_contour = max(contours, key=cv2.contourArea) # 步骤4:用最小外接矩形裁切,保证舌体完整且留10%边距 x, y, w, h = cv2.boundingRect(largest_contour) x_pad, y_pad = int(w*0.1), int(h*0.1) x_start = max(0, x - x_pad) y_start = max(0, y - y_pad) x_end = min(img.shape[1], x + w + x_pad) y_end = min(img.shape[0], y + h + y_pad) cropped = img[y_start:y_end, x_start:x_end] # 步骤5:统一尺寸并保存 resized = cv2.resize(cropped, (224, 224)) filename = os.path.basename(img_path).split('.')[0] + "_clean.jpg" cv2.imwrite(os.path.join(output_dir, filename), resized) # 批量处理示例 input_dir = "raw_dataset/" output_dir = "cleaned_dataset/" os.makedirs(output_dir, exist_ok=True) for img_file in os.listdir(input_dir): if img_file.lower().endswith(('.jpg', '.jpeg', '.png')): preprocess_tongue_image(os.path.join(input_dir, img_file), output_dir)参数说明:
h_median ±30是关键容差值,舌苔H值通常在5–25(暗红)或15–35(淡黄),过大会引入背景噪声,过小会漏切舌体;x_pad/y_pad=10%是经验安全边距,避免裁切时丢失舌缘苔质特征;224×224是适配主流CNN输入尺寸的最小公倍数,非强制,但低于192易丢失苔质纹理细节。
2.2 标注规范:舌苔分类不是“有/无”,而是四层临床维度
本系统论文中提到的“舌苔识别”实际包含四个正交维度:
| 维度 | 类别 | 判定依据(临床标准) | 标注方式 |
|---|---|---|---|
| 苔色 | 白、黄、灰、黑 | 舌苔基底颜色,排除光照色偏 | 单标签(0-3) |
| 苔质 | 薄、厚、腻、腐 | 苔颗粒致密程度与附着牢固度 | 单标签(0-3) |
| 润燥 | 润、滑、燥、糙 | 表面津液状态,反光强度与均匀度 | 单标签(0-3) |
| 分布 | 均匀、前部、根部、剥落 | 苔覆盖位置及完整性 | 单标签(0-3) |
注意:不要用ImageNet式单标签分类思维!一张舌象图需生成4个独立标签。例如:
tongue_001.jpg→[0, 1, 0, 0](白苔、厚苔、润苔、均匀分布)。标注工具推荐LabelImg,但需自定义4个独立多选框,而非单个下拉菜单。
2.3 数据增强:针对舌苔特性的物理仿真增强
通用增强(旋转、翻转)对舌苔无效——舌体天然左右不对称,上下翻转会扭曲解剖结构。必须采用病理驱动增强:
- 模拟不同光源:用
cv2.addWeighted()叠加高斯噪声模拟闪光灯直射(反光增强)或环形灯漫射(阴影减弱); - 模拟湿度变化:用
cv2.GaussianBlur()对局部区域模糊模拟“滑苔”(津液过多),或用cv2.Laplacian()锐化边缘模拟“糙苔”(津液亏虚); - 模拟拍摄距离:双三次插值缩放至150%再缩回,模拟近距离拍摄的纹理放大效应。
增强后数据集应满足:每类组合(如“白+厚+润+均匀”)至少30张,否则模型会偏向高频组合(如“白+薄+润+均匀”占70%)。
3. 模型构建:不用从零搭网络,用迁移学习+舌苔先验微调才是毕业设计最优解
直接训练ResNet50在200张图上会过拟合,而全连接层替换又丢失空间特征。本系统采用双路径微调策略:主干用ImageNet预训练权重冻结前4个stage,新增4个并行分支分别预测四维标签,每个分支含2层FC+Dropout(p=0.3)。这样既复用通用特征,又避免维度间干扰。
3.1 模型结构定义:PyTorch实现四输出头
import torch import torch.nn as nn from torchvision import models class TongueClassifier(nn.Module): def __init__(self, num_classes_per_task=4): super().__init__() # 加载预训练ResNet18,冻结前4个stage(共4个Sequential模块) backbone = models.resnet18(pretrained=True) for param in backbone.parameters(): param.requires_grad = False # 替换最后的fc层为全局平均池化+自定义头 self.backbone = nn.Sequential(*list(backbone.children())[:-2]) # 去掉avgpool和fc self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 四个独立任务头(苔色/苔质/润燥/分布) self.heads = nn.ModuleList([ nn.Sequential( nn.Linear(512, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes_per_task) ) for _ in range(4) ]) def forward(self, x): x = self.backbone(x) # [B, 512, H, W] x = self.avgpool(x) # [B, 512, 1, 1] x = torch.flatten(x, 1) # [B, 512] outputs = [head(x) for head in self.heads] return outputs # list of 4 tensors, each [B, 4] # 初始化模型 model = TongueClassifier() # 查看可训练参数量(应仅含heads部分) trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) print(f"可训练参数量: {trainable_params:,}") # 约 4×(512×128 + 128×4) ≈ 268,000逻辑说明:冻结backbone前4个stage(conv1→layer3)保留底层纹理提取能力,仅微调layer4和heads;
AdaptiveAvgPool2d((1,1))替代全连接层,避免因输入尺寸变化导致维度错配;4个独立head确保各维度预测无耦合,符合中医舌诊“四维一体”理论。
3.2 损失函数设计:多任务学习必须加权平衡
四维标签重要性不同:苔色诊断价值最高(决定寒热属性),分布次之(提示病位深浅),润燥与苔质相对次要。直接用nn.CrossEntropyLoss()平均加权会导致模型专注预测苔色而忽略其他。采用动态加权损失:
class MultiTaskLoss(nn.Module): def __init__(self, weights=[1.0, 0.7, 0.6, 0.8]): # 权重按苔色/苔质/润燥/分布顺序 super().__init__() self.weights = torch.tensor(weights) self.criterion = nn.CrossEntropyLoss() def forward(self, preds, targets): # preds: list of 4 tensors [B,4], targets: [B,4] long tensor loss = 0 for i, (pred, weight) in enumerate(zip(preds, self.weights)): loss += weight * self.criterion(pred, targets[:, i]) return loss # 训练时使用 criterion = MultiTaskLoss(weights=[1.0, 0.7, 0.6, 0.8]) optimizer = torch.optim.AdamW(model.heads.parameters(), lr=1e-3)参数说明:权重
[1.0, 0.7, 0.6, 0.8]来自临床专家访谈——苔色权重设为1.0基准,分布(反映病位)权重0.8因与预后强相关,润燥(津液状态)权重0.6因主观性强,苔质(邪气性质)权重0.7居中。若测试发现某维度准确率持续<60%,可临时提升其权重0.1再训10轮。
3.3 训练策略:小数据集必须用早停+学习率预热
200张图训练易震荡,需严格控制:
from torch.optim.lr_scheduler import CosineAnnealingLR # 预热阶段:前5轮线性增大学习率至峰值 scheduler_warmup = torch.optim.lr_scheduler.LinearLR( optimizer, start_factor=0.01, end_factor=1.0, total_iters=5 ) # 主训练阶段:余弦退火,最低学习率1e-6 scheduler_main = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6) # 早停机制:监控验证集加权F1(四维平均) best_f1 = 0.0 patience_counter = 0 patience = 15 # 连续15轮无提升则停止 for epoch in range(100): train_loss = train_one_epoch(model, dataloader_train, criterion, optimizer) # 验证阶段计算四维F1 val_f1 = validate(model, dataloader_val) if val_f1 > best_f1: best_f1 = val_f1 torch.save(model.state_dict(), "best_model.pth") patience_counter = 0 else: patience_counter += 1 if patience_counter >= patience: print(f"早停触发,最佳F1: {best_f1:.4f}") break # 学习率调度 if epoch < 5: scheduler_warmup.step() else: scheduler_main.step()血泪经验:不加预热会导致初始梯度爆炸(loss瞬间飙到inf),余弦退火比StepLR收敛更稳;早停
patience=15是200图规模的黄金值,小于10易欠拟合,大于20过拟合风险陡增。
4. GUI开发:不是套模板,而是让中医师愿意点开用的交互逻辑
系统GUI不是炫技,核心目标是降低中医师操作门槛:上传图片→自动裁切→显示四维结果→导出PDF报告。本系统用PyQt5实现,但关键在控件逻辑而非UI美化。
4.1 主界面布局:用QTabWidget分隔“操作区”与“结果区”
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QFileDialog, QTextEdit, QGroupBox, QFormLayout) from PyQt5.QtGui import QPixmap, QImage import sys class TongueApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("舌苔智能识别系统 v1.0") self.setGeometry(100, 100, 1000, 700) # 主容器 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # Tab页:操作区 & 结果区 tabs = QTabWidget() main_layout.addWidget(tabs) # Tab1:图像上传与处理 tab1 = QWidget() tab1_layout = QVBoxLayout(tab1) # 上传按钮组 upload_group = QGroupBox("图像上传") upload_layout = QHBoxLayout() self.btn_upload = QPushButton("选择舌象图片") self.btn_upload.clicked.connect(self.load_image) self.btn_capture = QPushButton("实时拍摄(需USB摄像头)") self.btn_capture.clicked.connect(self.capture_image) upload_layout.addWidget(self.btn_upload) upload_layout.addWidget(self.btn_capture) upload_group.setLayout(upload_layout) tab1_layout.addWidget(upload_group) # 预览区域 preview_group = QGroupBox("舌体预览") preview_layout = QVBoxLayout() self.lbl_preview = QLabel("等待上传...") self.lbl_preview.setAlignment(Qt.AlignCenter) preview_layout.addWidget(self.lbl_preview) preview_group.setLayout(preview_layout) tab1_layout.addWidget(preview_group) # 处理按钮 self.btn_process = QPushButton("开始识别") self.btn_process.clicked.connect(self.run_inference) self.btn_process.setEnabled(False) # 初始禁用 tab1_layout.addWidget(self.btn_process) tabs.addTab(tab1, "图像处理") # Tab2:结果展示 tab2 = QWidget() tab2_layout = QVBoxLayout(tab2) result_group = QGroupBox("识别结果") result_layout = QFormLayout() self.lbl_color = QLabel("苔色:待识别") self.lbl_texture = QLabel("苔质:待识别") self.lbl_moisture = QLabel("润燥:待识别") self.lbl_distribution = QLabel("分布:待识别") result_layout.addRow("苔色", self.lbl_color) result_layout.addRow("苔质", self.lbl_texture) result_layout.addRow("润燥", self.lbl_moisture) result_layout.addRow("分布", self.lbl_distribution) result_group.setLayout(result_layout) tab2_layout.addWidget(result_group) # 报告导出 self.btn_export = QPushButton("导出PDF报告") self.btn_export.clicked.connect(self.export_report) self.btn_export.setEnabled(False) tab2_layout.addWidget(self.btn_export) tabs.addTab(tab2, "结果分析") def load_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择舌象图片", "", "Images (*.png *.jpg *.jpeg)" ) if file_path: self.current_image_path = file_path self.display_image(file_path) self.btn_process.setEnabled(True) def display_image(self, path): pixmap = QPixmap(path).scaled(400, 300, Qt.KeepAspectRatio) self.lbl_preview.setPixmap(pixmap)逻辑说明:
QTabWidget强制分离操作流与结果流,避免新手误点“导出”却无结果;btn_process.setEnabled(False)初始禁用,防止未上传就点击;QFormLayout对齐四维标签,符合中医师阅读习惯(从左到右依次为苔色→苔质→润燥→分布)。
4.2 推理集成:模型加载与结果映射必须零延迟
GUI中调用模型不能阻塞主线程,否则界面冻结。用QThread异步执行:
from PyQt5.QtCore import QThread, pyqtSignal class InferenceThread(QThread): finished = pyqtSignal(list) # 发送四维结果列表 def __init__(self, model_path, image_path): super().__init__() self.model_path = model_path self.image_path = image_path def run(self): # 加载模型(此处简化,实际需加载权重+预处理) model = TongueClassifier() model.load_state_dict(torch.load(self.model_path)) model.eval() # 预处理图像(同训练时一致) img = cv2.imread(self.image_path) img = preprocess_tongue_image_for_inference(img) # 同2.1节清洗逻辑 # 推理 with torch.no_grad(): pred = model(torch.from_numpy(img).unsqueeze(0).float()) # 将logits转为类别索引 results = [torch.argmax(p, dim=1).item() for p in pred] self.finished.emit(results) # 在主窗口中启动线程 def run_inference(self): self.btn_process.setEnabled(False) self.thread = InferenceThread("best_model.pth", self.current_image_path) self.thread.finished.connect(self.show_results) self.thread.start() def show_results(self, results): # 映射索引到中文标签 color_map = ["白苔", "黄苔", "灰苔", "黑苔"] texture_map = ["薄苔", "厚苔", "腻苔", "腐苔"] moisture_map = ["润苔", "滑苔", "燥苔", "糙苔"] dist_map = ["均匀分布", "前部分布", "根部分布", "剥落分布"] self.lbl_color.setText(f"苔色:{color_map[results[0]]}") self.lbl_texture.setText(f"苔质:{texture_map[results[1]]}") self.lbl_moisture.setText(f"润燥:{moisture_map[results[2]]}") self.lbl_distribution.setText(f"分布:{dist_map[results[3]]}") self.btn_export.setEnabled(True) self.btn_process.setEnabled(True)避坑重点:
QThread必须继承并重写run(),不能直接在主线程调用model();preprocess_tongue_image_for_inference()需与训练时完全一致(包括HSV范围、裁切逻辑),否则出现“训练准、GUI不准”玄学问题。
5. 避坑指南:90%的毕业答辩翻车都发生在这5个环节
舌苔识别系统看似简单,但临床数据特性导致大量隐性坑。以下是我带过12届毕设学生总结的高频翻车点,按出现概率排序:
5.1 现象:GUI点击“开始识别”后无响应,日志显示CUDA out of memory
原因:模型加载时默认用GPU,但学生电脑无NVIDIA显卡或CUDA版本不匹配(如代码用torch 1.13,但本地装的是1.12)。更隐蔽的是,PyQt5的QPixmap加载大图(>4K)会吃光显存。
解决:强制CPU推理 + 图像预缩放。在InferenceThread.run()开头加:
device = torch.device("cpu") # 强制CPU model.to(device) # 加载图像后立即缩放 img = cv2.resize(img, (224, 224)) # 避免QPixmap加载原图5.2 现象:训练时loss下降但验证集准确率停滞在50%左右
原因:标注不一致。例如同一张“黄厚苔”图,在不同标注者手下被标成[1,1,0,0](黄+厚)和[1,1,1,0](黄+厚+滑),导致模型学习到噪声。
解决:实施双盲标注校验。让两位标注者独立标注全部数据,用Cohen's Kappa系数评估一致性(κ>0.8才合格)。若κ<0.6,必须组织标注者对照《中医舌诊图谱》重新培训。
5.3 现象:论文里写“测试集准确率92.3%”,但答辩时老师用新图测试只有65%
原因:测试集泄露。学生把所有图放在dataset/下,用sklearn.model_selection.train_test_split随机划分,但未按“拍摄日期”或“拍摄者”分层,导致测试集集中于某天光线好的样本。
解决:按采集批次划分。例如:第1-3天图做训练,第4天做验证,第5天做测试。代码中用glob.glob("20230501_*.jpg")等按日期筛选,而非随机打乱。
5.4 现象:GUI导出PDF报告后文字乱码(显示□□□)
原因:PyQt5默认字体不支持中文,QTextDocument生成PDF时用英文字体渲染中文。
解决:指定中文字体。在export_report()中:
from PyQt5.QtGui import QFont doc = QTextDocument() font = QFont("SimHei", 10) # 微软雅黑或宋体 doc.setDefaultFont(font) # 后续添加文本...5.5 现象:模型在训练集上准确率99%,但实际舌象图识别全错
原因:数据清洗过度。用HSV阈值时h_median ±30设为±50,把黄苔(H≈20)和灰苔(H≈100)全滤掉了,只剩白苔(H≈0)样本,模型学会“永远输出白苔”。
解决:可视化HSV分布。在清洗前加:
plt.hist(h_channel.flatten(), bins=180, alpha=0.7) plt.xlabel("Hue Value") plt.ylabel("Pixel Count") plt.title("Hue Distribution of Tongue Images") plt.savefig("hue_distribution.png") # 检查是否双峰(白/黄)、多峰(白/黄/灰)根据直方图调整阈值,确保覆盖所有临床常见苔色。
6. 论文写作与答辩技巧:让评审专家一眼看出你真懂舌诊,不是调包侠
毕业论文不是代码说明书,而是临床问题→技术解法→验证闭环的叙事。我带的学生常犯的错是:花20页写ResNet结构,却用1页带过“为什么选四维分类而非端到端回归”。以下是我的硬核建议:
6.1 论文结构黄金比例:临床逻辑占60%,技术实现占30%,实验分析占10%
| 章节 | 内容要点 | 字数建议 | 避免雷区 |
|---|---|---|---|
| 第2章 舌诊临床基础 | 引用《中医诊断学》教材,说明苔色/苔质/润燥/分布的辨证意义;附真实舌象图(带标注箭头) | 2500字 | 不要抄百度百科,必须引用教材页码(如“《中医诊断学》第3版P78”) |
| 第3章 系统设计 | 用UML活动图展示“上传→清洗→裁切→推理→报告”全流程;表格对比传统人工判读 vs 本系统耗时/一致性 | 3000字 | 不要贴PyQt5控件截图,要画数据流图(DFD) |
| 第4章 实验验证 | 仅列3组关键数据:①四维各自准确率(非总准确率)②与3位主治医师结果的一致性(Kappa值)③单图平均处理时间 | 1500字 | 不要写“本系统准确率92.3%”,必须写“苔色识别准确率95.1%,分布识别准确率86.4%” |
6.2 图表制作:让评审专家3秒看懂你的工作价值
- Figure 3:舌苔四维标注示意图
用Visio画一张舌体图,分区标注:舌尖(前部)、舌中(根部)、舌面(均匀)、舌缘(剥落),旁边配4个小图分别展示白/黄/灰/黑苔的HSV直方图。 - Table 2:与临床医师一致性对比
评价者 苔色Kappa 苔质Kappa 润燥Kappa 分布Kappa 平均 医师A vs 系统 0.82 0.75 0.68 0.79 0.76 医师B vs 系统 0.85 0.71 0.72 0.81 0.77 医师C vs 系统 0.79 0.69 0.65 0.76 0.72
6.3 答辩话术:把技术细节转化成临床语言
当被问“为什么用ResNet18不用ViT?”
❌ 错误答法:“ViT参数太多,我的电脑跑不动。”
✅ 正确答法:“ViT擅长全局建模,但舌苔诊断依赖局部纹理(如腻苔的颗粒感、腐苔的疏松感),ResNet18的卷积感受野更匹配这种微观特征提取,这与《舌诊图谱》强调‘观苔察微’的原则一致。”
当被问“准确率92%是否足够临床应用?”
❌ 错误答法:“已经很高了!”
✅ 正确答法:“92%是四维平均值,其中苔色识别达95.1%,已超过初级医师85%的平均水平(引自《中医舌诊一致性研究》2022);但分布识别86.4%尚需提升,我们计划下一阶段接入舌体分割模型,这是当前临床痛点。”
最后说句实在话:我见过太多学生把毕设做成“调参游戏”,模型跑通就万事大吉。但真正的价值在于——当你把一张模糊的舌象图拖进GUI,3秒后屏幕显示“黄厚苔、燥苔、根部分布”,老中医盯着结果点头说“这人该用黄连温胆汤”,那一刻你才真正理解什么叫“AI赋能中医”。希望这篇笔记帮你绕过那些我踩过的坑,把力气花在刀刃上。希望帮到你。
本文还有配套的精品资源,点击获取