
简介这是一套面向高校计算机相关专业毕业设计与人工智能初学者的舌苔图像深度学习识别系统源码包围绕医学图像分类任务提供从界面到模型的完整实现。资源共131个文件以Python源码、模型权重、界面文件、训练日志与论文文档为主压缩包约129.97MB其中py文件承载核心逻辑pth保存训练好的网络参数ui与pyc支撑图形界面运行docx与md记录论文与说明。系统集成卷积神经网络特征提取、实时图像采集分析、舌象分类识别及模型训练验证等模块并采用数据增强与梯度下降优化提升泛化能力界面支持多种图像格式输入与结果可视化。已有60人学习下载适合作为机器学习课程实践案例或毕业设计参考模块化代码便于功能扩展与性能调优。1. 舌苔图像识别到底难在哪从一张手机照片到可用的分类结果舌苔图像深度学习识别系统说白了就是让模型看一张舌头照片判断舌苔属于哪一类——薄白、厚白、黄腻、少苔这些常见分型。很多同行第一次听到这个方向直觉反应是「不就是个图像分类吗ResNet 套上去就完了」。真动手才发现翻车点根本不在模型结构上而在数据舌苔图像的类间差异极其细微同一类在不同光照、不同手机白平衡下能差出十万八千里而不同类之间可能只差一层薄薄的苔色。这就是为什么一个在 ImageNet 上表现良好的骨干网络直接迁移过来准确率可能只有六成出头。这套系统真正要解决的问题有三个层次。第一层是数据层舌象采集没有标准设备手机、相机、环形灯、自然光混在一起必须先做颜色归一化和舌体区域裁剪否则模型学到的全是背景和光照。第二层是模型层分类头怎么设计、要不要做多标签舌质和舌苔往往同时判断、类别不均衡怎么处理。第三层是工程层训练完的模型怎么封装成一个能点按钮、能拖图片、能出结果的 GUI而不是停在 Jupyter Notebook 里。适合读这篇的人有三类手里有舌象数据集想做分类的算法同学、需要交付一个带界面的演示系统给非技术用户看的工程同学、以及想拿这个方向做课程设计或论文但不知道坑在哪的学生。下面按「数据怎么准备 → 模型怎么训 → GUI 怎么接 → 坑在哪」的顺序讲每一步都给能直接抄的命令和参数。2. 数据准备与预处理舌体分割、颜色归一化和数据集划分2.1 为什么不能直接把原图丢给模型舌苔识别的第一道坎是舌体分割。原始照片里舌头只占中间一块四周是嘴唇、牙齿、下巴皮肤这些区域的纹理和颜色对分类毫无帮助反而会干扰模型。常见做法是先做舌体区域检测再裁剪出 ROI 送进分类网络。分割方案有两种传统 CV 用 HSV 阈值加形态学深度学习用 U-Net 或轻量分割网络。如果数据量不大几百到几千张我一般先用 HSV 阈值快速跑一版看分割效果再决定要不要上分割网络。颜色归一化是第二个关键点。同一张舌头冷光下偏蓝、暖光下偏黄模型很容易把光照当成分类依据。工程上常用灰度世界假设或白平衡校正把图像拉到统一色温。这一步不做后面调参调到怀疑人生也上不去。import cv2 import numpy as np def segment_tongue(img_bgr): HSV 阈值 形态学做舌体粗分割返回裁剪后的 ROI hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) # 舌体偏红H 在 0-15 和 160-180 两段 lower1 np.array([0, 40, 50]) upper1 np.array([15, 255, 255]) lower2 np.array([160, 40, 50]) upper2 np.array([180, 255, 255]) mask cv2.inRange(hsv, lower1, upper1) | cv2.inRange(hsv, lower2, upper2) # 开运算去噪闭运算补洞 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 取最大连通域 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None c max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(c) return img_bgr[y:yh, x:xw] def gray_world_white_balance(img_bgr): 灰度世界假设做白平衡缓解色温差异 result img_bgr.astype(np.float32) avg result.reshape(-1, 3).mean(axis0) gray avg.mean() scale gray / (avg 1e-6) result * scale return np.clip(result, 0, 255).astype(np.uint8)segment_tongue里 HSV 的 H 阈值范围要根据你的数据集实测调整40 和 50 是饱和度和亮度的下限太低会把嘴唇也框进来。gray_world_white_balance的1e-6是防止除零scale三个通道分别缩放让整体均值趋近灰色。这两步跑完建议人工抽检 50 张确认舌体没被裁掉、颜色没偏得离谱再往下走。2.2 数据集划分与类别不均衡处理舌苔数据集的天然问题是类别不均衡薄白苔样本一大堆黄腻苔可能只有几十张。直接按 8:1:1 随机划分验证集里黄腻苔可能就几张指标波动极大。我一般用分层抽样stratified split保证每个类别在训练/验证/测试里的比例一致同时对少数类做增强随机旋转 ±15 度、水平翻转、亮度扰动 ±10%、轻微弹性形变。from sklearn.model_selection import train_test_split import os def split_dataset(root, test_size0.15, val_size0.15, seed42): paths, labels [], [] for cls in sorted(os.listdir(root)): cls_dir os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for f in os.listdir(cls_dir): paths.append(os.path.join(cls_dir, f)) labels.append(cls) # 先分训练验证 和 测试 X_tv, X_test, y_tv, y_test train_test_split( paths, labels, test_sizetest_size, stratifylabels, random_stateseed) # 再从训练验证里分验证 val_ratio val_size / (1 - test_size) X_train, X_val, y_train, y_val train_test_split( X_tv, y_tv, test_sizeval_ratio, stratifyy_tv, random_stateseed) return (X_train, y_train), (X_val, y_val), (X_test, y_test)stratifylabels是分层抽样的关键保证每个类别按比例分配。seed固定住方便复现。划分完打印一下每个集合的类别分布如果某个类在验证集里少于 5 张要么补数据要么在评估时用宏平均 F1 而不是准确率。提示舌象数据涉及个人健康信息采集和使用时要做好脱敏去掉人脸、姓名等可识别信息只保留舌体区域。3. 模型选型与训练从 ResNet 迁移到舌苔五分类3.1 骨干网络怎么选为什么不用自己搭舌苔分类的数据量通常在几千张量级从零训一个 CNN 基本没戏迁移学习是唯一现实路径。骨干网络常见选择ResNet50、EfficientNet-B0、MobileNetV3。如果部署在服务器或 PC 上ResNet50 稳如果要塞进移动端或低配设备MobileNetV3 更合适。EfficientNet-B0 是精度和参数的折中我个人在舌苔任务上更偏好它因为输入分辨率 224 时对细纹理的捕捉比 ResNet 略好。分类头设计上舌苔识别有个特殊点舌质和舌苔往往需要同时判断如果只做单标签五分类可以把「舌质舌苔」组合成一个复合类别但类别数会膨胀。更干净的做法是做多标签两个分支各自输出。不过如果论文或课程设计只要求单标签分类那就先做单标签把流程跑通再扩展。import torch import torch.nn as nn from torchvision import models def build_model(num_classes5, backboneefficientnet_b0, pretrainedTrue): if backbone resnet50: model models.resnet50(weightsmodels.ResNet50_Weights.DEFAULT if pretrained else None) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.4), nn.Linear(in_features, num_classes) ) elif backbone efficientnet_b0: model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.DEFAULT if pretrained else None) in_features model.classifier[1].in_features model.classifier nn.Sequential( nn.Dropout(0.4), nn.Linear(in_features, num_classes) ) return modelDropout(0.4)是防止小数据集过拟合的常规手段如果训练集超过一万张可以降到 0.2。pretrainedTrue加载 ImageNet 预训练权重这是迁移学习的基础。替换分类头时注意不同骨干的层名不一样ResNet 是fcEfficientNet 是classifier写错会直接报错。3.2 训练循环与关键超参训练部分用 PyTorch 标准流程但有几个参数对舌苔任务特别敏感。学习率微调时主干用 1e-4分类头用 1e-3分组学习率能明显加快收敛。优化器AdamW 比 SGD 在小数据集上更稳权重衰减设 1e-4。批次大小受显存限制一般 16 或 32。数据增强用 torchvision 的 transforms 组合。from torch.utils.data import DataLoader from torchvision import transforms from torchvision.datasets import ImageFolder import torch.optim as optim train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.1, contrast0.1, saturation0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_ds ImageFolder(data/train, transformtrain_tf) val_ds ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classeslen(train_ds.classes)).to(device) # 分组学习率主干小分类头大 backbone_params [p for n, p in model.named_parameters() if classifier not in n and fc not in n] head_params [p for n, p in model.named_parameters() if classifier in n or fc in n] optimizer optim.AdamW([ {params: backbone_params, lr: 1e-4}, {params: head_params, lr: 1e-3} ], weight_decay1e-4) criterion nn.CrossEntropyLoss(label_smoothing0.1) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)label_smoothing0.1对舌苔这种边界模糊的分类很有用能缓解模型对某一类的过度自信。CosineAnnealingLR的T_max设成总 epoch 数让学习率平滑降到接近零。训练时每个 epoch 后在验证集上算宏平均 F1不要只看准确率否则少数类被忽略了你都不知道。3.3 训练过程要看哪些指标除了 loss 和 accuracy舌苔任务要重点盯混淆矩阵。薄白苔和少苔容易混黄腻苔和厚白苔容易混这两组是重灾区。如果发现某两类互相误判严重先回去看这两类的样本在颜色归一化后是不是还是很像如果是数据本身的问题调模型没用。另一个指标是每类的召回率少数类召回率低于 0.6 就说明模型没学到要么加数据要么加类别权重。from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, loader, device, class_names): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesclass_names, digits4)) print(confusion_matrix(all_labels, all_preds))classification_report会输出每类的 precision、recall、f1confusion_matrix看具体混在哪。这两个输出建议每个 epoch 存一份训练完对比看趋势。4. GUI 实现用 PyQt5 把模型封装成可点击的桌面应用4.1 界面结构怎么设计GUI 的目标用户是不懂代码的医生或学生所以交互要极简一个「选择图片」按钮、一个预览区、一个「开始识别」按钮、一个结果显示区。技术选型上PyQt5 是 Python 桌面 GUI 里最成熟的方案跨平台、控件全、和 PyTorch 集成没有额外依赖。Tkinter 更轻但控件丑做演示系统不够体面。Web 方案Flask 前端适合多用户但部署麻烦单机演示用 PyQt5 最省事。界面布局用 QVBoxLayout 垂直堆叠顶部是标题中间左右分栏左边原图预览右边结果显示底部是按钮区。结果显示用 QLabel 加大字号把类别名和置信度都显示出来置信度低于阈值时给个提示。import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog) from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt class TongueGUI(QMainWindow): def __init__(self, predictor): super().__init__() self.predictor predictor self.setWindowTitle(舌苔图像识别系统) self.resize(900, 600) central QWidget() self.setCentralWidget(central) layout QVBoxLayout(central) # 图片预览区 self.img_label QLabel(请选择一张舌象图片) self.img_label.setAlignment(Qt.AlignCenter) self.img_label.setMinimumSize(400, 400) self.img_label.setStyleSheet(border: 1px solid #ccc;) layout.addWidget(self.img_label) # 结果区 self.result_label QLabel(等待识别...) self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setStyleSheet(font-size: 20px; color: #333;) layout.addWidget(self.result_label) # 按钮区 btn_layout QHBoxLayout() self.btn_open QPushButton(选择图片) self.btn_predict QPushButton(开始识别) self.btn_predict.setEnabled(False) btn_layout.addWidget(self.btn_open) btn_layout.addWidget(self.btn_predict) layout.addLayout(btn_layout) self.btn_open.clicked.connect(self.open_image) self.btn_predict.clicked.connect(self.run_predict) self.current_path None def open_image(self): path, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.png *.jpg *.jpeg)) if path: self.current_path path pix QPixmap(path).scaled(400, 400, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.img_label.setPixmap(pix) self.btn_predict.setEnabled(True) self.result_label.setText(已加载点击开始识别) def run_predict(self): if not self.current_path: return cls_name, conf self.predictor(self.current_path) self.result_label.setText(f识别结果{cls_name} 置信度{conf:.2%})predictor是一个外部传入的函数接收图片路径返回类别名和置信度这样 GUI 和模型解耦换模型不用改界面。btn_predict初始禁用选了图片才启用避免用户空点。QPixmap.scaled保持宽高比防止图片变形。4.2 模型推理与 GUI 的对接推理函数要做三件事读图、预处理和训练时一致、前向计算。预处理必须和验证集完全一致否则精度会掉。把模型加载和推理封装成一个类GUI 初始化时加载一次避免每次点击都重新加载权重。import torch from PIL import Image from torchvision import transforms class TonguePredictor: def __init__(self, ckpt_path, class_names, devicecpu): self.device torch.device(device) self.class_names class_names self.model build_model(num_classeslen(class_names), pretrainedFalse) self.model.load_state_dict(torch.load(ckpt_path, map_locationself.device)) self.model.to(self.device).eval() self.tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def __call__(self, img_path): img Image.open(img_path).convert(RGB) tensor self.tf(img).unsqueeze(0).to(self.device) with torch.no_grad(): logits self.model(tensor) probs torch.softmax(logits, dim1)[0] conf, idx probs.max(dim0) return self.class_names[idx.item()], conf.item()pretrainedFalse是因为要加载自己的权重map_location保证在 CPU 上也能加载 GPU 训的模型。unsqueeze(0)加 batch 维度。softmax后取最大值和索引。这个类可以直接传给 GUI也可以单独在命令行测试。注意如果推理时用的预处理和训练时不一致比如少了 Normalize 或 Resize 尺寸不同精度会莫名其妙掉一大截这是最常见的翻车点之一。5. 避坑与排查舌苔识别系统落地时最容易踩的五个坑5.1 现象训练准确率 95%实际用手机拍一张就错原因训练集和实际使用场景的域差异太大。训练集可能是固定设备、固定光照采集的手机拍的光照、角度、白平衡全变了。模型学到的是训练集的域特征不是舌苔本身的特征。解决训练时就要做强的颜色和几何增强把 ColorJitter 的 brightness、contrast、saturation 幅度调大0.2~0.3加随机裁剪和旋转。如果条件允许采集一批手机拍摄的样本混进训练集。推理时先做白平衡校正把输入拉到和训练集接近的色温。5.2 现象验证集 F1 很高测试集一塌糊涂原因数据划分时没有分层或者验证集和测试集有同一患者的多次拍摄导致信息泄漏。舌象数据往往一个人拍多张如果按图片随机划分同一个人可能同时出现在训练和测试里模型等于见过答案。解决按患者 ID 划分同一个人只出现在一个集合里。如果数据里没有患者 ID至少按拍摄批次或日期划分。划分完检查一下有没有高度相似的图片跨集合。5.3 现象GUI 点识别按钮卡死几秒原因推理在主线程里跑PyTorch 前向计算阻塞了 Qt 的事件循环。图片大、模型大时尤其明显。解决把推理放到 QThread 里通过信号槽把结果传回主线程更新界面。或者至少把模型加载放在启动时不要每次点击都加载。如果模型在 GPU 上第一次推理还有 CUDA 初始化开销可以在启动时跑一次空推理预热。5.4 现象少数类召回率极低模型几乎不预测黄腻苔原因类别不均衡交叉熵损失被多数类主导。模型发现全预测薄白苔就能拿到高准确率于是躺平。解决用带类别权重的 CrossEntropyLoss权重按类别频率的倒数设置。或者用 Focal Loss 降低易分类样本的权重。再配合少数类的过采样和增强。评估时看宏平均 F1不要被准确率骗了。5.5 现象换了一台机器模型加载报错或结果不对原因PyTorch 版本不一致、CUDA 版本不匹配、或者保存模型时用了torch.save(model)整个对象而不是state_dict。整个对象保存依赖类定义路径换环境容易崩。解决统一用torch.save(model.state_dict(), path)保存加载时先实例化模型再load_state_dict。requirements 里锁死 torch 和 torchvision 版本。如果跨设备用map_location显式指定。6. 进阶技巧用 Grad-CAM 验证模型到底在看哪里模型训完、GUI 跑通之后还有一个问题没回答模型到底是看舌苔判断的还是看背景、看光照、看某个无关的伪影这个问题不解决系统上线就是黑匣子。我一般用 Grad-CAM 做可视化验证把模型关注的区域热力图叠在原图上如果高亮区域集中在舌体中部说明模型学到了合理特征如果高亮在边缘或背景说明数据有问题得回去重新处理。import torch import numpy as np import cv2 from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image def visualize_cam(model, img_path, target_layer, devicecpu): model.eval().to(device) img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized cv2.resize(img, (224, 224)) input_tensor torch.from_numpy(img_resized / 255.0).permute(2, 0, 1).unsqueeze(0).float().to(device) # 用 ImageNet 均值方差归一化 mean torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1).to(device) std torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1).to(device) input_tensor (input_tensor - mean) / std cam GradCAM(modelmodel, target_layers[target_layer]) grayscale_cam cam(input_tensorinput_tensor)[0] visualization show_cam_on_image(img_resized / 255.0, grayscale_cam, use_rgbTrue) return visualizationtarget_layer一般选最后一个卷积阶段比如 ResNet 的layer4、EfficientNet 的features[-1]。show_cam_on_image把热力图叠在原图上红色区域是模型关注度最高的地方。跑一批测试图人工看热力图分布如果大部分图的高亮都在舌体上说明模型可信如果高亮乱跑先别急着调模型回去查数据。除了 Grad-CAM还有两个验证习惯我一直在用。第一做一个「打乱标签」实验把训练集的标签随机打乱再训一遍如果模型还能在验证集上拿到高准确率说明数据泄漏了。正常情况打乱标签后准确率应该掉到随机水平。第二留一个「外部测试集」从不同设备、不同时间采集的样本里留一批不参与任何训练和调参只在最后测一次。这个数字才是系统真实可用的精度。最后说个血泪经验舌苔识别系统的精度上限由数据决定不由模型决定。我见过太多人花两周调网络结构准确率从 82% 调到 83%但花两天重新做颜色归一化和舌体分割准确率直接到 89%。先把数据管线做扎实再谈模型。GUI 部分不要追求花哨能稳定跑、不卡死、结果清晰就够用了。希望帮到你。本文还有配套的精品资源点击获取