简介:这是一套面向高校计算机相关专业毕业设计、人工智能课程实践与机器学习入门者的舌苔图像深度学习识别系统源码包,围绕医学图像分类任务提供从数据到界面的完整实现。包内共131个文件,以Python源码、模型权重文件、界面配置、训练日志与论文文档为主,另含少量图片素材与依赖说明,压缩包约129.97MB,目录按功能模块划分,便于按需查阅与二次开发。系统集成卷积神经网络特征提取、数据增强、梯度下降参数调优与实时图像采集分析界面,支持多种图像格式输入及识别结果可视化,并附完整模型训练与验证方案。已有60人学习下载,适合作为毕业设计参考、课程作业模板或医学图像处理方向的实践案例,读者可据此理解舌象分类的完整流程、模型调参思路与GUI交互设计方法。
1. 舌苔图像识别到底难在哪:从一张手机照片到可用的分类结果
很多人第一次听到「基于 Python 的舌苔图像深度学习识别系统」,脑子里浮现的是拍张舌头照片、模型吐出一个「厚腻苔」标签这么简单。真动手做才发现,难点根本不在模型结构,而在数据本身:舌象图像受拍摄设备、光源色温、伸舌姿势、口腔反光影响极大,同一根舌头在诊室冷白光和手机暖光下能拍出两种颜色。再加上公开可用的舌苔标注数据集极少,绝大多数团队要自己从零采集、清洗、标注,这才是整个项目最耗时的部分。
这套系统要解决的核心问题,是把非结构化的舌面图像映射到中医舌诊里可解释的类别,比如苔色(白、黄、灰黑)和苔质(薄、厚、腻、剥)。它适合三类人:想拿一个完整深度学习项目练手的学生、需要给中医辅助诊断做原型的工程师、以及想把舌象采集做成硬件产品但缺算法验证的团队。GUI 在这里不是装饰,而是让非技术使用者(比如中医师)能直接上传图片、看到分类置信度和热力图,否则模型再准也落不了地。
2. 数据准备与预处理:舌苔图像为什么不能直接丢进网络
2.1 舌体分割是绕不开的第一步
直接把整张自拍丢进分类网络,背景里的脸、牙齿、嘴唇会严重干扰模型。舌体分割的目标是把舌头区域抠出来,只保留有效像素。常见做法有两种:一是用传统图像处理(HSV 阈值 + 形态学操作)快速出粗分割,二是训练一个轻量 U-Net 做精细分割。我一般先用传统方法做一版,验证数据质量,再决定要不要上分割网络。
import cv2 import numpy as np def segment_tongue(image_path): img = cv2.imread(image_path) # 转 HSV,舌头在色调上偏红,饱和度较高 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 红色在 HSV 里跨 0 和 180,需要两段阈值合并 lower1 = np.array([0, 40, 50]) upper1 = np.array([10, 255, 255]) lower2 = np.array([170, 40, 50]) upper2 = np.array([180, 255, 255]) mask = cv2.inRange(hsv, lower1, upper1) | cv2.inRange(hsv, lower2, upper2) # 开运算去噪,闭运算补空洞 kernel = np.ones((7, 7), np.uint8) 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 largest = max(contours, key=cv2.contourArea) result = np.zeros_like(mask) cv2.drawContours(result, [largest], -1, 255, -1) return cv2.bitwise_and(img, img, mask=result)这段代码的逻辑是:HSV 空间对光照变化比 RGB 更鲁棒,红色双区间阈值覆盖舌头的主要色域,形态学操作去掉噪点和空洞,最后用最大连通域排除嘴唇、脸颊等红色区域。参数上,lower1/upper1的饱和度下限 40 是关键,设太低会把嘴唇也框进来,设太高舌头边缘会被切掉。实际调参时建议先跑 20 张样本,把 mask 可视化出来看,比盲调数值快得多。
2.2 数据增强要针对舌象特点设计
通用增强(随机裁剪、翻转)对舌苔识别帮助有限,因为舌苔的纹理和颜色分布是有方向性和位置性的。更有效的增强包括:模拟不同色温的白平衡偏移、局部亮度扰动、以及轻微的弹性形变来模拟伸舌姿态差异。注意不要用大角度旋转,舌头上下颠倒的样本在真实场景里不存在,加了反而引入噪声。
import albumentations as A train_transform = A.Compose([ A.Resize(224, 224), # 模拟色温变化,舌苔颜色是核心特征,必须覆盖 A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.7), A.HueSaturationValue(hue_shift_limit=8, sat_shift_limit=15, val_shift_limit=10, p=0.5), # 弹性形变模拟伸舌姿态差异 A.ElasticTransform(alpha=30, sigma=5, p=0.3), A.HorizontalFlip(p=0.5), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ])HueSaturationValue的hue_shift_limit不要超过 10,否则白苔可能被增强成黄苔,标签就错了。ElasticTransform的 alpha 控制形变强度,30 左右比较温和,再大舌体轮廓会扭曲得不自然。归一化用 ImageNet 均值方差是因为后面要加载预训练权重,这一步不能省。
2.3 数据集划分与类别不平衡处理
舌苔数据天然不平衡:白苔样本远多于灰黑苔,薄苔远多于剥苔。直接训练会让模型偏向多数类。常见做法是加权采样加 Focal Loss 组合。加权采样让每个 batch 里各类别比例接近,Focal Loss 则降低易分样本的权重,逼模型关注难例。
from torch.utils.data import WeightedRandomSampler import torch # class_counts 是每个类别的样本数,比如 [800, 300, 120] class_counts = [800, 300, 120] weights = 1.0 / torch.tensor(class_counts, dtype=torch.float) # 给每个样本分配对应类别权重 sample_weights = [weights[label] for _, label in dataset] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True)replacement=True表示允许重复采样少数类,这是处理不平衡的标准手段。num_samples设成总样本数,保证一个 epoch 的步数和普通训练一致。如果少数类样本少于 50 张,光靠采样不够,得考虑数据合成或迁移学习。
3. 模型选型与训练:从 ResNet 到注意力机制的取舍
3.1 骨干网络怎么选才不浪费算力
舌苔分类不是 ImageNet 那种千类问题,通常只有 6 到 12 个类别,数据量也有限。用 ResNet50 起步是稳妥选择,但如果你只有几千张图,ResNet18 或 EfficientNet-B0 反而更容易收敛,过拟合风险更低。我一般会先跑一个 ResNet18 baseline,看验证集准确率能不能到 80% 以上,再决定要不要换更大的骨干。
import torch.nn as nn from torchvision import models def build_model(num_classes=6, backbone='resnet18', pretrained=True): if backbone == 'resnet18': model = models.resnet18(weights=models.ResNet18_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) ) return modelDropout(0.4)放在全连接前是为了抑制过拟合,舌苔数据集小的时候这个值可以加到 0.5。pretrained=True加载 ImageNet 权重能显著加快收敛,但如果你的舌象数据和自然图像差异极大(比如全是灰度图),预训练收益会打折,这时候可以考虑从头训练配合更强增强。
3.2 加入注意力模块提升关键区域响应
舌苔的判别信息集中在舌面中后部,舌边缘和舌尖的贡献较小。加一个轻量的通道注意力(SE Block)或空间注意力,能让模型把权重压到有效区域。SE Block 实现简单,插入到 ResNet 的残差块后面即可,参数量增加很少。
class SEBlock(nn.Module): def __init__(self, channels, reduction=16): super().__init__() self.pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(channels, channels // reduction), nn.ReLU(inplace=True), nn.Linear(channels // reduction, channels), nn.Sigmoid() ) def forward(self, x): b, c, _, _ = x.size() y = self.pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * y # 通道加权reduction=16是原论文的默认值,通道数少的时候(比如 64)可以改成 8,否则中间层维度太低会损失信息。这个模块的作用是让网络自动学习哪些通道对舌苔分类更重要,训练完后可以把通道权重可视化,验证模型是否真的关注了颜色和纹理通道。
3.3 训练策略与学习率调度
舌苔识别训练有几个关键设置:优化器用 AdamW 比 SGD 更容易调,初始学习率 1e-3 配合余弦退火,训练 30 到 50 个 epoch。早停 patience 设 8 到 10,因为验证集准确率波动可能比较大。
from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6) for epoch in range(50): model.train() for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() # 验证逻辑省略,记录 val_acc 用于早停weight_decay=1e-4是 AdamW 的常用值,比 SGD 时代的 5e-4 小,因为 AdamW 的解耦权重衰减机制不同。eta_min=1e-6保证学习率不会降到零,最后几个 epoch 还能微调。如果验证集准确率在 10 个 epoch 内不涨,直接停,别硬跑,舌苔数据量撑不起太长训练。
4. GUI 实现:让中医师也能用的桌面端界面
4.1 技术选型:PyQt5 还是 Tkinter
GUI 框架选择取决于你要什么。Tkinter 是 Python 自带,零依赖,但控件丑、布局难调,适合内部工具。PyQt5 功能强、控件丰富、支持样式表,打包后体积大一些但体验好得多。如果这个系统要给中医师日常用,我建议 PyQt5,因为图片拖拽、置信度进度条、热力图叠加这些交互 Tkinter 做起来很痛苦。
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog) from PyQt5.QtGui import QPixmap import sys class TongueApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("舌苔识别系统") self.resize(600, 500) central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) self.image_label = QLabel("请上传舌象图片") self.image_label.setFixedSize(400, 300) layout.addWidget(self.image_label) btn = QPushButton("选择图片") btn.clicked.connect(self.load_image) layout.addWidget(btn) self.result_label = QLabel("") layout.addWidget(self.result_label) def load_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.png *.jpg)") if path: pixmap = QPixmap(path).scaled(400, 300) self.image_label.setPixmap(pixmap) # 这里调用模型推理,返回类别和置信度 self.result_label.setText("识别结果:待接入模型") if __name__ == "__main__": app = QApplication(sys.argv) window = TongueApp() window.show() sys.exit(app.exec_())这段代码搭了一个最小可运行窗口:一个图片显示区、一个选择按钮、一个结果标签。load_image里预留了模型推理的接入点。实际项目中,推理要放到独立线程里,否则大模型加载会卡住界面,这是新手最容易翻车的地方。
4.2 把推理结果和热力图接进界面
光显示类别不够,中医师需要知道模型凭什么这么判。用 Grad-CAM 生成热力图,叠加在原图上,能直观看到模型关注的是舌面哪个区域。这一步在 GUI 里用 QThread 异步执行,避免阻塞主线程。
from PyQt5.QtCore import QThread, pyqtSignal import torch import cv2 import numpy as np class InferenceThread(QThread): finished = pyqtSignal(str, float, np.ndarray) def __init__(self, model, image_path, transform): super().__init__() self.model = model self.image_path = image_path self.transform = transform def run(self): img = cv2.imread(self.image_path) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor = self.transform(image=img_rgb)['image'].unsqueeze(0) with torch.no_grad(): output = self.model(tensor) prob = torch.softmax(output, dim=1) conf, pred = torch.max(prob, dim=1) # 热力图生成逻辑省略,返回叠加后的图像 heatmap = np.zeros_like(img_rgb) # 占位 self.finished.emit(str(pred.item()), conf.item(), heatmap)QThread的finished信号携带三个值:类别名、置信度、热力图数组。主线程收到信号后更新界面。注意torch.no_grad()必须加,否则推理时会建计算图,显存暴涨。热力图叠加时用cv2.addWeighted控制透明度,一般 0.4 到 0.5 之间比较清楚。
4.3 打包成 exe 的坑
用 PyInstaller 打包 PyQt5 + PyTorch 项目,体积轻松上 1GB,而且经常遇到找不到 DLL 的问题。常见解法是在 spec 文件里显式添加 torch 的 lib 路径,并用--onefile换成--onedir减少启动解压时间。如果目标机器没有 GPU,打包时要装 CPU 版 torch,否则运行时报 CUDA 相关错误。
5. 避坑与排查:舌苔识别项目里最容易翻车的五件事
5.1 现象:训练准确率 99%,上线一测全错
原因:数据集划分时把同一根舌头的多张照片分到了训练集和验证集,模型记住了舌头而不是舌苔特征。解决:按受试者 ID 划分,同一个人所有照片只出现在一个集合里。这个坑极其常见,血泪经验。
5.2 现象:模型把黄苔全预测成白苔
原因:白苔样本是黄苔的三倍以上,且黄苔样本里很多是浅黄,和偏白舌苔在 RGB 空间差异小。解决:除了加权采样,还要在损失函数里给黄苔更高权重,或者用颜色空间增强专门拉开黄白差异。另外检查标注,浅黄和白苔的边界标注是否一致。
5.3 现象:GUI 上传图片后界面卡死
原因:模型推理在主线程执行,PyTorch 加载模型和推理耗时几百毫秒到几秒,阻塞了 Qt 事件循环。解决:把推理放进 QThread,通过信号槽回传结果。如果还卡,检查是不是每次推理都重新加载了模型,模型应该只加载一次常驻内存。
5.4 现象:换一台电脑运行报错找不到 torch
原因:PyInstaller 打包时没有把 torch 的动态库打进去,或者目标机器缺少 VC++ 运行库。解决:用--collect-all torch参数重新打包,并在目标机器安装对应版本的 Visual C++ Redistributable。如果还不行,检查 Python 版本和 torch 版本是否匹配。
5.5 现象:热力图每次都不一样,无法复现
原因:推理时没有固定随机种子,或者模型处于 train 模式导致 Dropout 和 BatchNorm 行为不一致。解决:推理前调用model.eval(),并设置torch.manual_seed(42)。如果用了 CUDA,还要加torch.cuda.manual_seed_all(42)。这个坑在写论文复现实验时特别致命。
6. 进阶技巧:用测试时增强和置信度校准把准确率再提一档
模型训练完之后,别急着交付。有两个几乎零成本但效果明显的技巧值得试。第一个是测试时增强(TTA):对同一张测试图做多种变换(原图、水平翻转、轻微亮度调整),分别推理后取平均概率。舌苔识别里 TTA 通常能涨 1 到 3 个百分点,代价只是推理时间翻几倍,对 GUI 场景完全可接受。
def predict_with_tta(model, image, transform_list): model.eval() probs = [] with torch.no_grad(): for t in transform_list: tensor = t(image=image)['image'].unsqueeze(0) output = model(tensor) probs.append(torch.softmax(output, dim=1)) # 平均多个增强视图的概率 return torch.mean(torch.stack(probs), dim=0)transform_list里放 3 到 5 个增强管道,不要太多,否则推理延迟会让用户等得不耐烦。注意 TTA 用的增强要和训练增强同分布,训练时没用过的变换别加进来。
第二个是置信度校准。深度学习模型输出的 softmax 概率往往偏高,模型说 95% 置信,实际可能只有 70% 是对的。用温度缩放(Temperature Scaling)在验证集上拟合一个温度参数 T,推理时把 logits 除以 T 再 softmax,能让置信度更可靠。这对医疗辅助场景很重要,因为中医师需要根据置信度决定是否采纳建议。
class TemperatureScaler(nn.Module): def __init__(self): super().__init__() self.temperature = nn.Parameter(torch.ones(1) * 1.5) def forward(self, logits): return logits / self.temperature # 在验证集上优化 temperature 参数 scaler = TemperatureScaler() optimizer = torch.optim.LBFGS([scaler.temperature], lr=0.01, max_iter=50) # 优化目标是最小化 NLL 损失,具体训练循环省略温度参数的初始值设 1.5 是个经验起点,优化后通常在 1.2 到 2.5 之间。校准完之后,GUI 上显示的置信度才真正有参考价值。我自己的习惯是:任何要给人看的分类系统,交付前必须做置信度校准,否则那个百分比就是玄学。这两个技巧加上前面说的按受试者划分数据,基本能保证你的舌苔识别系统在真实场景里不会太离谱。希望帮到你。
本文还有配套的精品资源,点击获取