基于Python的舌苔图像深度学习识别系统源码及GUI实现
发布时间:2026/10/2 2:46:22来源:尧图网络
简介这是一套面向高校计算机相关专业毕业设计与人工智能入门实践的舌苔图像深度学习识别系统源码包围绕医学图像分类任务提供从数据到界面的完整实现路径。包内共131个文件以26个Python源码、20个备份文件、10个编译缓存、6个PyTorch模型权重、7张示例图片及2个UI界面文件为主另含2份docx论文文档与json配置压缩包约129.97MB模块划分清晰便于按训练、推理、界面三条线检索。系统集成卷积神经网络特征提取、数据增强、梯度下降参数调优与实时图像采集交互界面支持多格式输入与结果可视化并附完整模型训练与验证方案。已有60人学习下载适合作为机器学习课程实践案例或毕业设计参考读者可据此复现舌象分类流程、理解GUI与模型对接方式并在此基础上做功能扩展与性能优化。1. 舌苔图像识别到底难在哪从一张手机照片到可用的分类结果舌苔图像深度学习识别说白了就是让模型看一眼舌头照片判断苔色是白、黄还是灰黑苔质是薄、厚、腻还是剥落。这件事在中医四诊客观化里属于看起来简单、做起来全是坑的典型手机拍出来的舌头光照忽明忽暗舌头伸出的长度每次不一样牙齿、嘴唇、反光、口水都会混进画面。我见过太多人拿公开数据集训一个 ResNet 就宣称 95% 准确率换一批自己拍的图直接掉到 60% 以下。这套基于 Python 的舌苔图像深度学习识别系统源码及 GUI 实现要解决的正是从原始照片到可交互界面的完整链路——数据怎么清洗、模型怎么选、GUI 怎么把推理结果稳定地呈现出来。适合有 Python 基础、想做一个能真正跑起来而不是只跑通 demo 的从业者也适合中医信息化方向做课题的学生。下面按我实际落地的顺序把每一步的参数和翻车点讲清楚。2. 数据准备与预处理舌体分割比分类模型更决定上限2.1 为什么先做舌体分割而不是直接喂原图直接拿整张照片训练分类网络模型会偷懒去学背景特征——比如某批数据都在同一张桌子上拍的模型记住桌布颜色就能猜对标签。这是血泪经验我第一版模型在自建集上 92%一换拍摄环境就崩。正确做法是先把舌体从背景里抠出来只保留舌头区域再送进分类网络。舌体分割常见两条路传统方法用 HSV 颜色空间阈值加形态学操作快但泛化差深度学习方法用 U-Net 或轻量分割网络标注几十张就能有明显提升。我一般先用传统方法快速生成粗标注再人工修正省一半标注时间。import cv2 import numpy as np def segment_tongue_hsv(img_bgr): # 转 HSV舌体在 H 通道偏红S 通道饱和度较高 hsv cv2.cvtColor(img_bgr, 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 cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations2) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations3) # 取最大连通域排除嘴唇等干扰 cnts, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not cnts: return None c max(cnts, keycv2.contourArea) clean np.zeros_like(mask) cv2.drawContours(clean, [c], -1, 255, -1) return clean这段逻辑的关键参数是 HSV 的 S 下限 40 和 V 下限 50。S 太低会把灰白舌苔也滤掉V 太低在暗光下整张图都判成背景。形态学核用椭圆 (7,7) 是因为舌体边缘是弧形方形核会把轮廓切出直角。最大连通域这一步不能省否则嘴唇和舌头连在一起时分割结果会多出一块。2.2 统一尺寸、色彩与增强策略分割完的舌体要统一到模型输入尺寸。分类网络常用 224×224 或 299×299分割网络常用 256×256。缩放时保持长宽比、短边补齐比直接拉伸更保形——舌头被拉扁会改变苔质纹理的视觉特征。色彩归一化我推荐用 CLAHE限制对比度自适应直方图均衡在 LAB 空间的 L 通道上做而不是全局直方图均衡。全局均衡会把不同光照的图强行拉到同一亮度分布反而丢失真实的苔色深浅信息。CLAHE 的 clipLimit 设 2.0、tileGridSize 设 (8,8) 是我试出来比较稳的组合。数据增强要克制。水平翻转、±15 度旋转、亮度 ±10% 是安全的垂直翻转不要用舌头上下颠倒不符合解剖先验模型会学到错误特征。颜色抖动幅度要小因为苔色本身就是分类依据大幅调色等于改标签。import albumentations as A train_tf A.Compose([ A.Resize(256, 256), A.HorizontalFlip(p0.5), A.Rotate(limit15, p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.5), A.CLAHE(clip_limit2.0, tile_grid_size(8, 8), p1.0), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ])Normalize 用的均值方差是 ImageNet 统计值只要用预训练权重就必须对齐否则迁移学习效果打折。CLAHE 放在 Normalize 之前顺序反了归一化就白做。2.3 标签体系与数据集划分的坑舌苔分类通常分两个维度苔色白、黄、灰黑和苔质薄、厚、腻、剥。建议做成两个独立分类头而不是一个多标签 softmax 硬凑。因为苔色和苔质是正交的白苔可以是薄也可以是厚合成一个标签空间会导致类别爆炸且样本极不均衡。划分数据集时按人划分而不是按图划分。同一个人拍了 10 张舌头如果随机分到训练和测试集模型等于在测试集上见到了训练时的人准确率虚高。正确做法是同一受试者的所有图只进一个集合。这个细节决定了你报出来的指标是不是真的。3. 模型选型与训练从 ResNet 到轻量化部署的取舍3.1 骨干网络怎么选精度、速度与数据量的三角数据量在几千张级别时从零训练 CNN 基本没戏必须用预训练权重做迁移学习。骨干网络我按数据规模给建议少于 2000 张用 ResNet18 或 MobileNetV32000 到 1 万张用 ResNet50 或 EfficientNet-B0超过 1 万张再考虑 EfficientNet-B3 以上。舌苔数据集通常不大ResNet18 加好的预处理往往打得过没调好的 ResNet50。EfficientNet 系列在舌苔任务上有个隐患它的复合缩放对纹理细节敏感而舌苔的腻、厚这类特征恰恰是细粒度纹理。如果发现 EfficientNet 不如 ResNet别急着怀疑代码先检查输入分辨率是不是被压得太低。import torch import torch.nn as nn from torchvision import models def build_model(num_classes, backboneresnet18, pretrainedTrue): if backbone resnet18: net models.resnet18(weightsIMAGENET1K_V1 if pretrained else None) # 替换最后的全连接层适配舌苔类别数 in_f net.fc.in_features net.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_f, num_classes) ) elif backbone mobilenetv3: net models.mobilenet_v3_small(weightsIMAGENET1K_V1 if pretrained else None) in_f net.classifier[-1].in_features net.classifier[-1] nn.Linear(in_f, num_classes) return netDropout 加在分类头前是防止小数据集过拟合的常规操作0.3 是我在几千张规模下比较稳的值数据越少可以调到 0.5。替换分类头后新加的层学习率要设大一些骨干层设小一些这就是差分学习率。3.2 训练参数学习率、批大小与早停差分学习率是迁移学习的关键。骨干层用 1e-4新分类头用 1e-3这样既不会破坏预训练特征又能让新层快速收敛。优化器用 AdamW 比 SGD 在小数据集上更省心weight_decay 设 1e-4。批大小受显存限制8 或 16 都行但要注意 BatchNorm 层在批太小时统计量不准。如果只能用批大小 4 以下建议把骨干里的 BN 冻住或者换 GroupNorm。早停监控验证集的宏平均 F1 而不是准确率。舌苔类别天然不均衡白苔样本远多于灰黑苔只看准确率会让模型偏向多数类。宏平均 F1 对每个类一视同仁更能反映真实能力。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR # 差分学习率骨干小分类头大 backbone_params [p for n, p in model.named_parameters() if fc not in n and classifier not in n] head_params [p for n, p in model.named_parameters() if fc in n or classifier in n] optimizer AdamW([ {params: backbone_params, lr: 1e-4}, {params: head_params, lr: 1e-3}, ], weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30, eta_min1e-6)CosineAnnealingLR 的 T_max 设成总 epoch 数让学习率平滑降到接近 0。eta_min 不要设 0留 1e-6 给后期微调。如果验证损失连续 5 个 epoch 不降就停这是早停的 patience。3.3 类别不均衡的处理顺序先试重采样再试损失加权最后才上 Focal Loss。很多人一上来就 Focal Loss结果超参难调还掉点。正确顺序是先看每类样本数差 3 倍以内用 WeightedRandomSampler 就够了差 10 倍以上再考虑类别权重极端不均衡某类只有几十张才用 Focal Loss 配合强增强。类别权重按样本数倒数算但别直接用开个平方根缓和一下。比如白苔 1000 张、灰黑苔 100 张权重比 10:1 太激进开根号后约 3.16:1 更稳。注意测试集绝对不能做任何重采样必须保持真实分布否则评估指标没有意义。4. GUI 实现把推理封装成能交付的桌面工具4.1 技术栈选择PyQt 还是 Tkinter做舌苔识别 GUI我强烈建议 PyQt5/PySide6不要用 Tkinter。原因很实际Tkinter 显示图片要绕 PIL 转换缩放和拖拽体验差而且没有现成的进度条和线程信号机制。PyQt 的 QThread 加信号槽能把推理放到后台线程界面不卡死这对加载模型这种耗时操作是刚需。GUI 的核心功能就四块选图/拍照、预览、点按钮推理、显示结果和置信度。别贪多先把这四块做扎实。from PySide6.QtCore import QThread, Signal from PySide6.QtWidgets import QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget from PySide6.QtGui import QPixmap import torch class InferWorker(QThread): # 推理结果通过信号传回主线程避免跨线程操作 UI finished Signal(str, float) def __init__(self, model, img_tensor, classes): super().__init__() self.model model self.img_tensor img_tensor self.classes classes def run(self): with torch.no_grad(): logits self.model(self.img_tensor) prob torch.softmax(logits, dim1) conf, idx prob.max(dim1) self.finished.emit(self.classes[idx.item()], conf.item()) class MainWindow(QMainWindow): def __init__(self, model, classes): super().__init__() self.model model self.classes classes self.setWindowTitle(舌苔识别) self.label QLabel(请选择舌象图片) self.btn QPushButton(开始识别) self.btn.clicked.connect(self.on_infer) layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.btn) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def on_infer(self): # 实际项目里这里从文件对话框取图并预处理 img_tensor torch.randn(1, 3, 256, 256) self.worker InferWorker(self.model, img_tensor, self.classes) self.worker.finished.connect(self.show_result) self.worker.start() def show_result(self, cls_name, conf): self.label.setText(f结果{cls_name} 置信度{conf:.2%})QThread 的 run 里不能碰任何 UI 对象只能通过 Signal 把数据发回主线程这是 PyQt 的铁律违反会随机崩溃。模型加载放在主窗口初始化时做一次不要每次推理都重新加载。4.2 模型加载与推理预处理的一致性GUI 里的预处理必须和训练时完全一致这是最容易翻车的地方。训练用了 CLAHE 加 ImageNet 归一化推理时少做一步结果就飘。我的做法是把预处理写成一个独立函数训练和推理都调它从源头保证一致。def preprocess_for_infer(img_bgr, size256): # 与训练保持完全一致的预处理链路 img cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img cv2.resize(img, (size, size)) lab cv2.cvtColor(img, cv2.COLOR_RGB2LAB) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) lab[:, :, 0] clahe.apply(lab[:, :, 0]) img cv2.cvtColor(lab, cv2.COLOR_LAB2RGB) img img.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img (img - mean) / std # HWC 转 CHW 再补 batch 维 tensor torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).float() return tensor注意 OpenCV 读图是 BGRPyTorch 和 PIL 习惯 RGB转换顺序错了颜色通道就反了白苔可能被识别成别的类。这个 bug 我踩过不止一次排查半天才发现是通道顺序。4.3 打包成可执行文件用 PyInstaller 打包时模型权重文件要作为数据文件带上不能只打包 py 脚本。命令里加--add-data把 .pth 和类别映射 json 一起打进去。打包后第一次运行慢是正常的因为要解压到临时目录。pyinstaller --noconfirm --windowed --name TongueApp \ --add-data model/best.pth;model \ --add-data model/classes.json;model \ main.py--windowed去掉控制台窗口--add-data的格式是源路径;目标路径Windows 用分号Linux 和 macOS 用冒号。打包体积大是 PyInstaller 的通病可以用--exclude-module排掉 matplotlib、scipy 这些没用到的库。5. 避坑与排查那些让准确率虚高和 GUI 崩溃的细节5.1 验证集准确率很高但实际用起来很差现象训练时验证集 95%拿新拍的舌头照片测试只有 60% 多。原因通常是数据泄漏——同一受试者的图同时进了训练和验证集模型记住了人而不是苔象特征。解决按受试者 ID 划分数据集确保一个人的所有图只出现在一个集合里。如果数据集没记录受试者信息至少按拍摄批次划分。5.2 GUI 点击识别后界面卡死现象点按钮后窗口无响应几秒后才出结果。原因是推理在主线程里跑阻塞了事件循环。解决把推理放进 QThread通过 Signal 回传结果。注意模型加载也要考虑如果模型很大加载那一下也会卡可以在启动时显示一个加载提示。5.3 打包后运行报找不到模型文件现象源码运行正常PyInstaller 打包后报 FileNotFoundError。原因是打包后文件路径变了__file__指向临时解压目录。解决用sys._MEIPASS判断是否在打包环境动态拼路径。import sys, os def resource_path(relative): # 打包后资源在 _MEIPASS 临时目录源码运行时在当前目录 base getattr(sys, _MEIPASS, os.path.dirname(os.path.abspath(__file__))) return os.path.join(base, relative)5.4 置信度普遍偏低且分布集中现象所有图片的最高置信度都在 40% 到 60% 之间模型像在犹豫。原因可能是标签噪声——同一张图不同标注者给了不同标签模型学不到清晰边界。解决做标注一致性检查让两个人独立标同一批图不一致的挑出来重新裁定。另外检查 softmax 前的 logits 尺度温度过高会让分布变平。5.5 训练损失正常但验证损失震荡现象训练 loss 稳定下降验证 loss 上下跳。原因通常是批大小太小导致 BN 统计量不稳或者学习率偏高。解决先把学习率降一半试试如果还震荡检查验证集是不是太小少于 200 张就容易震荡可以增大验证集或做交叉验证。6. 让模型真正可用的两个进阶技巧第一个技巧是测试时增强TTA。推理时对同一张图做原图、水平翻转、轻微旋转三个版本分别推理后把 softmax 概率平均。这个操作几乎零成本在舌苔任务上通常能涨 1 到 3 个点。代价是推理时间变三倍如果 GUI 对响应速度要求高可以只做原图和翻转两个版本。def infer_with_tta(model, img_tensor): model.eval() probs [] with torch.no_grad(): # 原图 probs.append(torch.softmax(model(img_tensor), dim1)) # 水平翻转 probs.append(torch.softmax(model(torch.flip(img_tensor, dims[3])), dim1)) # 平均后取最大 avg torch.stack(probs).mean(dim0) conf, idx avg.max(dim1) return idx.item(), conf.item()第二个技巧是给 GUI 加一个不确定兜底。当最高置信度低于某个阈值我一般设 0.6不显示具体类别而是提示建议重新拍摄或人工复核。这比强行给一个错答案要负责任得多尤其在辅助诊断场景里。阈值不要拍脑袋定在验证集上画一下置信度和准确率的关系曲线找到准确率开始明显下降的拐点。我自己的习惯是每做完一版模型先拿 20 张完全没参与训练的野生照片跑一遍看看真实表现再决定要不要继续调。数据集上的指标再好看也代替不了这一步。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网