
简介这份资源是面向计算机相关专业毕业生与项目实战学习者的深度学习舌苔识别检测鉴定系统完整方案源自个人大四毕业设计经导师指导并获评审99分认可。系统以Python实现配套GUI界面可用于毕业设计、课程设计或期末大作业帮助读者快速搭建一个可运行的中医舌诊智能识别项目。压缩包共109个文件约105.43MB包含26个py源码文件、6个pth模型权重、2个ui界面文件、2个docx论文资料及json配置、jpg/png示例图、ttc字体等覆盖训练、推理与界面展示全流程。目前已有196人学习下载。读者可获得完整可运行的源码、训练好的模型权重、论文文档与图形界面便于理解数据预处理、模型训练、结果可视化及界面交互等关键环节也适合作为深度学习入门与项目排错参考。1. 舌苔识别系统到底难在哪从一张手机照片到可答辩的毕业设计舌苔识别这件事听起来像是把一张舌头照片丢进模型里出个分类结果真动手做才发现坑全在细节里。手机拍出来的舌头色温一会儿偏黄一会儿偏蓝伸舌角度、光线强弱、口腔开合程度全都不一致同一根舌头在不同照片里能呈现出完全不同的颜色分布。而中医舌诊里「薄白苔」「黄腻苔」「灰黑苔」这些分类恰恰高度依赖颜色和纹理的细微差异。所以一个能拿得出手的毕业设计核心不是堆多深的网络而是把「图像预处理 → 舌体分割 → 苔质分类 → GUI 展示」这条链路打通让评委看到你理解了问题本身。这套方案适合计算机、人工智能、生物医学工程方向的本科或硕士毕业生也适合想快速搭一个可演示深度学习项目的开发者。Python 做主力语言深度学习框架选 PyTorch 或 TensorFlow 都行GUI 用 PyQt5 或 Tkinter 落地论文资料围绕数据集构建、模型对比、消融实验来写整套东西是自洽的。2. 数据从哪来、怎么标舌苔数据集的构建与预处理2.1 公开数据不够用时的三条采集路径舌苔识别最大的现实问题是公开数据集极少。常见做法是三条路并行一是去中医舌诊相关的公开图像库找基础样本数量通常只有几百张类别还不均衡二是和本地中医院或中医诊所合作在征得同意的前提下拍摄临床舌象这是最靠谱的来源三是用网络爬虫收集舌象图片做补充但质量参差必须人工筛。我一般会建议先凑够每类 200 张起步五类就是 1000 张左右这个量级用迁移学习足够跑出能看的结果。采集时统一用后置摄像头、固定光源环形补光灯最好、距离 15 到 20 厘米让被拍者自然伸舌别用力过度导致舌体变形。标注环节建议用 LabelImg 或 CVAT 做两类标注一类是舌体区域的边界框或分割掩码用于训练分割模型另一类是整张图的苔质类别标签用于分类。标注一致性很关键最好两个人独立标一遍再交叉核对分歧大的样本直接丢弃。别小看这一步我见过太多毕设因为标签噪声太大模型准确率卡在 60% 上不去最后归因到「数据不行」但已经来不及重标了。2.2 用 OpenCV 做舌体分割与颜色校正的最小流程原始照片直接喂给分类网络效果很差因为背景嘴唇、牙齿、皮肤会干扰。常见做法是先分割出舌体区域再做颜色归一化。下面是一段可复现的预处理代码import cv2 import numpy as np def preprocess_tongue(img_path, target_size(224, 224)): # 读取图像并统一尺寸 img cv2.imread(img_path) img cv2.resize(img, target_size) # 转 HSV 空间舌体在 H 通道有较集中的分布 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 舌体颜色大致落在红色到橙红色区间经验阈值 lower np.array([0, 40, 60]) upper np.array([25, 255, 255]) mask cv2.inRange(hsv, lower, upper) # 形态学操作去掉噪点保留最大连通区域 kernel np.ones((5, 5), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 只保留最大轮廓排除零散色块 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: largest max(contours, keycv2.contourArea) clean_mask np.zeros_like(mask) cv2.drawContours(clean_mask, [largest], -1, 255, -1) else: clean_mask mask # 用掩码提取舌体 tongue cv2.bitwise_and(img, img, maskclean_mask) # 灰度世界假设做简单白平衡缓解色温偏差 result cv2.cvtColor(tongue, cv2.COLOR_BGR2LAB) l, a, b cv2.split(result) l cv2.equalizeHist(l) # 只对亮度通道做均衡避免色偏 result cv2.merge((l, a, b)) result cv2.cvtColor(result, cv2.COLOR_LAB2BGR) return result, clean_mask # 调用示例 tongue_img, tongue_mask preprocess_tongue(sample.jpg) cv2.imwrite(tongue_processed.jpg, tongue_img)这段代码的逻辑是先统一尺寸保证输入一致再用 HSV 阈值粗分割舌体形态学操作填补空洞并去掉小噪点取最大轮廓确保只留舌体。LAB 空间只对 L 通道做直方图均衡是为了提升对比度但不破坏 a、b 色度通道避免把黄苔调成白苔。阈值[0, 40, 60]到[25, 255, 255]是经验值实际用的时候要拿几十张样本调不同拍摄设备下这个范围会漂移。如果分割结果把嘴唇也框进来了把 H 上限从 25 降到 15 试试。提示预处理阶段建议把中间结果都存下来答辩时展示「原图 → 分割 → 校正」的对比图比只放一个准确率数字有说服力得多。3. 模型选型与训练从 ResNet 迁移学习到苔质分类3.1 为什么舌苔分类优先用迁移学习而不是从零训练舌苔数据集规模通常在一千到几千张这个量级从零训练一个 CNN 基本等于自杀过拟合会非常严重。迁移学习是标准解法用 ImageNet 上预训练的 ResNet50 或 EfficientNet-B0冻结前面的卷积层只训练最后的全连接分类头等 loss 稳定后再解冻部分层做微调。我一般分两阶段第一阶段冻结 backbone学习率设 1e-3训 20 个 epoch第二阶段解冻最后两个 stage学习率降到 1e-4再训 30 个 epoch。这样既利用了预训练特征又让模型适配舌苔这个特定域。选 ResNet50 还是 EfficientNet看你的算力。ResNet50 参数量约 25M单张 1080Ti 或 3060 上 batch size 32 能跑EfficientNet-B0 只有 5.3M 参数精度接近但更快适合笔记本显卡。如果要做模型对比实验写进论文建议至少跑 ResNet18、ResNet50、EfficientNet-B0 三个用同一套数据划分对比准确率、F1 和推理时间这样论文的实验章节才有内容。3.2 训练脚本的关键参数与数据增强配置下面是一个基于 PyTorch 的训练骨架重点看数据增强和优化器配置import torch import torch.nn as nn from torchvision import models, transforms, datasets from torch.utils.data import DataLoader # 数据增强舌苔对翻转敏感度低但颜色变换要克制 train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), # 小角度旋转模拟伸舌角度差异 transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.02), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 加载数据集目录结构为 data/train/类别名/图片 train_set datasets.ImageFolder(data/train, transformtrain_transform) val_set datasets.ImageFolder(data/val, transformval_transform) train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers4) # 构建模型替换分类头 model models.resnet50(pretrainedTrue) num_classes len(train_set.classes) model.fc nn.Linear(model.fc.in_features, num_classes) model model.cuda() # 第一阶段只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) def train_one_epoch(epoch): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() correct (outputs.argmax(1) labels).sum().item() total labels.size(0) return total_loss / len(train_loader), correct / total for epoch in range(20): loss, acc train_one_epoch(epoch) scheduler.step() print(fEpoch {epoch1}, Loss: {loss:.4f}, Acc: {acc:.4f})数据增强里ColorJitter的 hue 参数只给了 0.02这是刻意的——舌苔分类高度依赖颜色hue 调大了等于把黄苔变成白苔模型学不到正确特征。RandomRotation给 15 度也是保守值伸舌角度差异没那么夸张。优化器用 Adam 而不是 SGD是因为迁移学习阶段参数少Adam 收敛更快等第二阶段解冻后再换 SGD 加 momentum 会更稳。StepLR每 10 个 epoch 降一半学习率防止后期震荡。第二阶段微调的代码结构一样只是把requires_grad打开最后两个 layer4 的 block学习率改成 1e-4。训练完记得保存state_dict后面 GUI 加载要用。3.3 类别不均衡时用加权损失和分层采样舌苔数据里「薄白苔」样本通常最多「灰黑苔」最少直接训练模型会偏向多数类。两个处理手段一是给CrossEntropyLoss传weight参数按类别频率的倒数计算二是用WeightedRandomSampler做分层采样让每个 batch 里各类别比例接近。代码上from torch.utils.data import WeightedRandomSampler import numpy as np # 统计每类样本数 targets [s[1] for s in train_set.samples] class_counts np.bincount(targets) class_weights 1.0 / class_counts sample_weights class_weights[targets] sampler WeightedRandomSampler(weightssample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_set, batch_size32, samplersampler, num_workers4) # 损失函数加权 weights_tensor torch.FloatTensor(class_weights / class_weights.sum()).cuda() criterion nn.CrossEntropyLoss(weightweights_tensor)这两个手段叠加用效果最好但要注意采样后每个 epoch 看到的样本分布变了验证集不能这么干验证集必须保持原始分布才能反映真实性能。4. GUI 界面与系统集成把模型变成能演示的桌面程序4.1 PyQt5 主界面布局与模型加载毕业设计答辩时评委不会看你敲命令行跑推理他们要的是一个能点按钮、能选图片、能出结果的界面。PyQt5 是首选组件丰富、文档多。主界面一般分三块左边显示原图右边显示预处理后的舌体区域和分类结果底部放「选择图片」「开始识别」「保存报告」三个按钮。模型在程序启动时加载一次别每次点识别都重新加载否则等十几秒体验极差。import sys import cv2 import torch from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog from PyQt5.QtGui import QPixmap, QImage from torchvision import transforms, models import torch.nn as nn class TongueApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(舌苔识别检测系统) self.setGeometry(100, 100, 900, 600) # 加载模型只加载一次 self.model models.resnet50(pretrainedFalse) self.model.fc nn.Linear(self.model.fc.in_features, 5) # 5 个类别 self.model.load_state_dict(torch.load(tongue_model.pth, map_locationcpu)) self.model.eval() self.class_names [薄白苔, 白腻苔, 黄腻苔, 灰黑苔, 剥落苔] self.transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) self.init_ui() def init_ui(self): central QWidget() self.setCentralWidget(central) main_layout QHBoxLayout() self.original_label QLabel(原图区域) self.original_label.setFixedSize(400, 400) self.original_label.setStyleSheet(border: 1px solid gray;) self.result_label QLabel(识别结果区域) self.result_label.setFixedSize(400, 400) self.result_label.setStyleSheet(border: 1px solid gray;) main_layout.addWidget(self.original_label) main_layout.addWidget(self.result_label) btn_layout QHBoxLayout() self.btn_select QPushButton(选择图片) self.btn_predict QPushButton(开始识别) self.btn_select.clicked.connect(self.select_image) self.btn_predict.clicked.connect(self.predict) btn_layout.addWidget(self.btn_select) btn_layout.addWidget(self.btn_predict) container QVBoxLayout() container.addLayout(main_layout) container.addLayout(btn_layout) central.setLayout(container) def select_image(self): path, _ QFileDialog.getOpenFileName(self, 选择舌象图片, , Images (*.jpg *.png)) if path: self.img_path path pixmap QPixmap(path).scaled(400, 400) self.original_label.setPixmap(pixmap) def predict(self): if not hasattr(self, img_path): return img cv2.imread(self.img_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor self.transform(img_rgb).unsqueeze(0) with torch.no_grad(): output self.model(tensor) prob torch.softmax(output, dim1) conf, pred torch.max(prob, 1) result_text f识别结果{self.class_names[pred.item()]}\n置信度{conf.item():.2%} self.result_label.setText(result_text) if __name__ __main__: app QApplication(sys.argv) window TongueApp() window.show() sys.exit(app.exec_())这段代码里模型加载放在__init__里只执行一次map_locationcpu保证没显卡的电脑也能跑。predict方法里先转 RGB 再走 transform因为 OpenCV 读进来是 BGR顺序错了颜色就反了分类结果会离谱。置信度用softmax算出来展示给评委看比只给一个类别名专业。4.2 推理结果可视化与报告导出光显示一个类别名太单薄建议在结果区域叠加显示分类类别、置信度、以及用 Grad-CAM 生成的热力图标出模型关注舌体的哪个区域。Grad-CAM 实现不复杂网上有成熟代码挂到你的模型最后一个卷积层上就行。报告导出用reportlab或直接生成 HTML 再转 PDF把原图、预处理图、热力图、分类结果、时间戳都放进去答辩时打印出来当附件印象分直接拉满。注意GUI 里所有耗时操作模型推理、图像预处理如果超过 0.5 秒建议放到QThread里跑否则界面会卡死评委点一下没反应就尴尬了。5. 避坑与排查舌苔识别系统最常见的五个翻车点现象一验证集准确率 90%换自己手机拍的照片全错。原因是训练数据和你手机拍的图像分布不一致色温、分辨率、背景都不同。解决办法是在预处理阶段加白平衡和直方图均衡训练时加大 ColorJitter 的 brightness 和 contrast 范围另外收集几十张自己设备拍的图做微调。现象二模型把所有样本都预测成样本最多的那一类。这是典型的类别不均衡问题。先打印混淆矩阵确认然后用加权损失加分层采样再不行就上 Focal Loss。别急着换模型数据层面的问题换网络解决不了。现象三GUI 点识别按钮程序直接崩。九成是图像通道顺序或尺寸不对。OpenCV 读的是 BGRPyTorch 的预训练模型要 RGB另外 transform 里 Resize 的尺寸必须和训练时一致。在predict里加 try-except 把异常打出来别让程序静默退出。现象四训练 loss 震荡不下降。先检查学习率是不是太大迁移学习第一阶段 1e-3 够了第二阶段必须降到 1e-4。再检查数据标签有没有错位ImageFolder是按文件夹名排序映射类别的你建文件夹时名字顺序和后面class_names列表对不上就全乱了。现象五论文里实验数据对不上代码跑出来的结果。这是血泪经验——每次跑实验必须固定随机种子torch.manual_seed(42)、np.random.seed(42)、random.seed(42)三件套都加上DataLoader 的worker_init_fn也要设。否则你论文写的结果和答辩现场跑出来的对不上解释起来非常被动。6. 让系统更耐打的三个进阶技巧第一个技巧是用 Test Time Augmentation 提升推理稳定性。单张图推理时做多次增强水平翻转、小角度旋转、不同裁剪把多次预测的概率平均后取最大值。代码上就是在predict里循环几次 transform把 softmax 输出累加再除。这个技巧不增加训练成本但通常能把准确率再提 1 到 2 个百分点答辩时演示效果更稳。第二个技巧是给模型加一个「拒识」机制。当最高置信度低于某个阈值比如 0.6时不输出具体类别而是提示「图像质量不足请重新拍摄」。这能避免模型在垃圾输入上强行给一个错误答案实际使用中比硬分类更可靠。阈值怎么定拿验证集跑一遍看错误样本的置信度分布取一个能滤掉大部分错误又不误杀正确样本的值。第三个技巧是模型量化。如果答辩用的电脑没有独立显卡用torch.quantization把模型转成 int8推理速度能快 2 到 3 倍精度损失通常在 1% 以内。操作上先做动态量化一行代码torch.quantization.quantize_dynamic(model, {nn.Linear}, dtypetorch.qint8)就能搞定对 ResNet 这种以卷积为主的网络效果有限但对全连接层多的模型提升明显。技巧适用场景预期收益实现成本TTA 推理答辩演示、追求稳定准确率 1~2%低改推理代码拒识机制实际部署、交互系统减少误判低加阈值判断动态量化无显卡环境速度 2~3 倍低一行代码我自己做这类系统最大的教训是别在模型结构上反复折腾把时间花在数据清洗和预处理上回报率高得多。我见过用 ResNet18 加干净数据跑出 92% 的也见过用 EfficientNet 加脏数据卡在 70% 的。先把数据管线做扎实再谈模型选型。希望帮到你。本文还有配套的精品资源点击获取