尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于深度学习的舌苔识别系统:Python源码+GUI界面+模型+论文报告全解析

基于深度学习的舌苔识别系统:Python源码+GUI界面+模型+论文报告全解析 简介这份资源是面向深度学习入门者与计算机相关专业毕业设计学生的舌苔识别检测鉴定系统完整项目包围绕中医舌象智能分析场景解决舌苔分类与体质辨识的算法落地问题。包内共110个文件以Python源码、模型权重文件、UI界面文件、论文报告文档及运行截图为主另含少量配置与说明文件压缩包约105MB目录结构清晰便于按模块查阅。项目涵盖数据集构建与图像增强、生成对抗网络扩充样本、卷积神经网络建模与训练、舌苔检测及体质辨识功能实现等环节并配有GUI界面与训练日志可帮助读者理解从数据标注到模型部署的完整流程。目前已有182人学习下载适合需要参考完整毕设方案、复现实验或撰写论文报告的学习者。1. 舌苔识别系统到底在做什么从一张手机照片到一份体质报告很多人第一次听到「基于深度学习的舌苔识别检测鉴定系统」脑子里浮现的是中医馆里老大夫眯着眼看舌头的画面。其实这套系统的核心任务非常具体输入一张舌头照片输出舌苔的颜色、厚薄、腐腻、润燥等特征分类再往上叠一层体质倾向判断。它解决的是中医舌诊主观性强、不同医生结论不一致的问题把「望诊」里最依赖经验的部分变成可复现的推理流程。这套东西适合谁一是做中医信息化、健康管理类产品的开发者需要一个能塞进 App 或小程序里的舌象分析模块二是高校做毕业设计或课程项目的同学需要一个带 GUI、有模型、有论文报告的完整闭环三是想入门深度学习图像分类的工程师舌苔数据集类别清晰、图像差异明显比猫狗分类更贴近真实业务。标题里提到的 python 源码、GUI 界面、模型、论文报告、运行截图本质上是一套「能跑起来、能演示、能写进文档」的交付物。下面我按实际落地的顺序把数据、模型、界面、训练、排错一条线讲透。2. 数据从哪来、怎么标舌苔数据集的构建与预处理2.1 舌苔识别的类别体系怎么定做任何图像分类第一步不是写模型是定标签。舌苔识别常见的分类维度有两套一套按苔色分「白苔、黄苔、灰黑苔」一套按苔质分「薄苔、厚苔、腻苔、腐苔、剥苔」。实际项目里如果只做单标签分类建议先收敛到 4 到 6 类比如白薄、白厚、黄薄、黄厚、腻苔、剥苔。类别太多会导致类间差异变小模型学不动标注成本也翻倍。我一般会先跟业务方确认一件事最终报告要输出什么。如果报告只写「舌苔偏厚」那厚薄就是主任务颜色作为辅助属性。如果报告要写「黄厚腻」那就是多标签或者组合标签。多标签的实现方式是把最后一层 sigmoid 输出改成多个二分类头损失用 binary cross entropy而不是 softmax。这个选择在动手写代码前必须定死否则后面改起来要重做数据集。标注工具用 labelImg 或者自己写个简单的 tkinter 标注界面都行关键是每张图要记录拍摄条件光源是自然光还是环形补光、舌头有没有伸到位、有没有被牙齿遮挡。这些元信息在排查模型误判时非常有用。2.2 图像预处理的四个必做步骤舌苔图像和普通自然图像最大的区别是舌头在画面里的位置、大小、角度都不固定而且口腔内部光照极不均匀。直接 resize 送进网络模型会去学背景而不是舌苔。所以预处理必须包含舌体分割或至少舌体区域裁剪。常见做法是用一个轻量分割模型比如 U-Net 或者 MobileNet 做 backbone 的 DeepLab先把舌体抠出来再送分类网络。如果项目周期紧可以退而求其次用颜色阈值加轮廓检测做粗裁剪但泛化性会差一些。下面是一段可复现的预处理脚本包含读取、裁剪、归一化和数据增强。import cv2 import numpy as np from torchvision import transforms # 舌体粗定位基于 HSV 颜色空间提取偏红区域 def crop_tongue(img_path, target_size(224, 224)): img cv2.imread(img_path) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 舌体在 HSV 中大致落在红色到橙红色区间 lower np.array([0, 40, 60]) upper np.array([25, 255, 255]) mask cv2.inRange(hsv, lower, upper) # 形态学闭运算填补口腔内高光造成的空洞 kernel np.ones((7, 7), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return cv2.resize(img, target_size) # 取面积最大的轮廓作为舌体 c max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(c) # 外扩 10% 避免裁掉舌边缘 pad_w, pad_h int(w * 0.1), int(h * 0.1) x1 max(0, x - pad_w) y1 max(0, y - pad_h) x2 min(img.shape[1], x w pad_w) y2 min(img.shape[0], y h pad_h) crop img[y1:y2, x1:x2] return cv2.resize(crop, target_size) # 训练集增强舌苔图像对颜色敏感慎用色相抖动 train_tf transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), 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]) ])这段代码的逻辑分两层。第一层是舌体粗定位用 HSV 阈值加最大轮廓筛选目的是把背景和嘴唇尽量排除。参数里 lower 和 upper 的色相范围要根据你的数据集实际光源调环形补光灯下舌头偏橙红自然光下偏暗红范围太窄会漏检太宽会把嘴唇也框进来。第二层是增强策略注意 hue 抖动只给了 0.02因为舌苔颜色本身就是分类依据色相大幅抖动等于把标签改掉这是很多人翻车的地方。提示如果你的数据集里舌头位置已经很规整可以跳过粗定位但归一化必须做且 mean/std 建议用你自己数据集的统计值重新算一遍不要直接套 ImageNet 的。2.3 数据集划分与类别不均衡处理舌苔数据天然不均衡白薄苔的样本量往往是灰黑苔的十几倍。直接按 8:1:1 随机划分测试集里稀有类可能只有几张图指标波动极大。我一般用分层抽样保证每个类别在训练、验证、测试里的比例一致。代码上 sklearn 的 train_test_split 加 stratify 参数就能搞定。对于不均衡本身优先用加权采样WeightedRandomSampler让稀有类在每个 batch 里被抽到的概率提高。损失函数层面可以叠加类别权重但不要同时把采样权重和损失权重都调得很激进容易过拟合稀有类。经验值是采样权重按类别频率的倒数开平方损失权重按倒数开四次方这样既缓解不均衡又不至于把模型带偏。3. 模型选型与训练从 ResNet 到轻量级部署的取舍3.1 为什么舌苔分类首选迁移学习而不是从零训练舌苔数据集规模通常在几千到一两万张之间这个量级从零训练一个 CNN 基本没戏模型会严重过拟合验证集准确率卡在 60% 上下不动。迁移学习是标准答案用 ImageNet 预训练的 ResNet50、EfficientNet-B0 或者 MobileNetV3把最后一层全连接改成你的类别数先冻结 backbone 训练分类头再解冻后几层做微调。选哪个 backbone 取决于部署环境。如果最终要跑在服务器或 PC 的 GUI 程序里ResNet50 或 EfficientNet-B0 都行准确率优先。如果后续要往移动端或边缘设备移植MobileNetV3 或 ShuffleNet 更合适参数量小、推理快准确率损失通常在 2 到 3 个百分点。标题里带 GUI 界面说明至少有一个桌面端演示程序那 ResNet50 完全够用单张图推理在普通 CPU 上也就几百毫秒。下面是一段基于 PyTorch 的训练脚本骨架包含冻结策略和分层学习率。import torch import torch.nn as nn from torchvision import models from torch.optim import Adam NUM_CLASSES 6 EPOCHS 40 device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载预训练 ResNet50替换分类头 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) for param in model.parameters(): param.requires_grad False # 先冻结全部 backbone model.fc nn.Sequential( nn.Dropout(0.4), nn.Linear(model.fc.in_features, NUM_CLASSES) ) model model.to(device) # 第一阶段只训练分类头学习率可以大一点 criterion nn.CrossEntropyLoss(weightclass_weights.to(device)) optimizer Adam(model.fc.parameters(), lr1e-3) # 第二阶段解冻 layer4 和 fc用分层学习率微调 def unfreeze_and_finetune(model): for name, param in model.named_parameters(): if layer4 in name or fc in name: param.requires_grad True return Adam([ {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 5e-4} ])逻辑说明第一阶段冻结 backbone 只训分类头是因为随机初始化的分类头梯度很大如果直接解冻全部网络大梯度会破坏预训练好的底层特征。等分类头收敛到验证集准确率不再明显上升再进入第二阶段解冻 layer4。分层学习率的用意是越靠近输入的层特征越通用越不需要大改所以给更小的学习率分类头是全新的给大一点。参数上Dropout 给 0.4 是因为舌苔数据集普遍偏小全连接层容易记住训练样本。class_weights 用前面说的类别频率倒数开四次方。EPOCHS 设 40 是经验值实际用早停EarlyStopping监控验证集 loss连续 8 个 epoch 不下降就停。3.2 训练过程中必须盯住的三个指标第一个是训练集和验证集的准确率差值。如果训练集到 99% 而验证集只有 70%说明过拟合优先加数据增强、加 Dropout、减小模型容量。第二个是混淆矩阵重点看哪些类别互相混淆。舌苔识别里白厚苔和腻苔经常混因为视觉上都偏白偏厚这时候要考虑是不是标签定义本身有重叠或者需要引入纹理特征。第三个是每类的召回率稀有类召回率低于 60% 就要回头检查采样和权重。我一般会在训练脚本里每 5 个 epoch 存一次 checkpoint同时把验证集预测结果和置信度写进日志。这样后面做 GUI 演示时可以直接拿置信度低的样本做案例分析说明模型在哪些边界情况下不可靠。3.3 模型导出与推理封装训练完的模型要能被 GUI 调用最稳的方式是导出成 TorchScript 或者 ONNX。TorchScript 对 PyTorch 生态最友好加载时不需要原始模型定义代码。导出命令很简单model.eval() example torch.randn(1, 3, 224, 224).to(device) traced torch.jit.trace(model, example) traced.save(tongue_model.pt)推理封装时注意两件事一是输入预处理必须和训练时完全一致包括 resize 尺寸、归一化参数、通道顺序二是输出要过 softmax 转成概率GUI 上显示「白厚苔 82%」比显示 logits 直观得多。如果要做多标签softmax 换成 sigmoid阈值一般设 0.5但可以根据业务调比如宁可漏报不可误报时把阈值提到 0.6。4. GUI 界面怎么做tkinter 与 PyQt 的落地对比4.1 界面框架选型够用比好看重要标题里明确带 GUI 界面说明交付物要能双击运行、点按钮出结果。Python 做桌面 GUI 主流就两个选择tkinter 和 PyQt5/PySide6。tkinter 是标准库不用额外装依赖打包成 exe 体积小缺点是控件丑、布局麻烦。PyQt 控件丰富、样式好看但打包后体积大商用还有授权问题。如果是毕业设计或内部演示我建议 tkinter因为环境配置简单老师或同事拿到源码直接 python 运行就能出界面不会卡在装 PyQt 上。如果要做成产品原型给客户看PyQt 更体面。下面给一个 tkinter 的最小可用界面包含选图、推理、结果显示三个区域。import tkinter as tk from tkinter import filedialog, Label, Button from PIL import Image, ImageTk import torch import cv2 import numpy as np class TongueApp: def __init__(self, root, model_path): self.root root self.root.title(舌苔识别检测系统) self.root.geometry(600x500) self.model torch.jit.load(model_path) self.model.eval() self.class_names [白薄苔, 白厚苔, 黄薄苔, 黄厚苔, 腻苔, 剥苔] self.btn Button(root, text选择舌象图片, commandself.load_image) self.btn.pack(pady10) self.img_label Label(root) self.img_label.pack() self.result_label Label(root, text等待检测..., font(微软雅黑, 14)) self.result_label.pack(pady20) def load_image(self): path filedialog.askopenfilename(filetypes[(Image, *.jpg *.png *.jpeg)]) if not path: return img Image.open(path).resize((300, 300)) self.tk_img ImageTk.PhotoImage(img) self.img_label.config(imageself.tk_img) self.predict(path) def predict(self, path): # 预处理必须与训练一致 img cv2.imread(path) img cv2.resize(img, (224, 224)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor torch.from_numpy(img).float().permute(2, 0, 1) / 255.0 mean torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1) std torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1) tensor (tensor - mean) / std tensor tensor.unsqueeze(0) with torch.no_grad(): logits self.model(tensor) probs torch.softmax(logits, dim1)[0] idx torch.argmax(probs).item() conf probs[idx].item() self.result_label.config( textf识别结果{self.class_names[idx]} 置信度{conf:.2%} ) if __name__ __main__: root tk.Tk() app TongueApp(root, tongue_model.pt) root.mainloop()这段代码的关键点在 predict 方法里resize 到 224、BGR 转 RGB、除以 255、按 ImageNet 的 mean/std 归一化顺序一步都不能错。我见过有人把归一化写在 ToTensor 之前或者忘了 BGR 转 RGB结果模型输出全是同一类排查半天以为是模型坏了其实是预处理翻车。GUI 本身不复杂难的是让界面里的推理链路和训练时严格对齐。4.2 界面交互里容易忽略的体验细节第一图片显示区域要和推理输入分开。界面上显示 300x300 是为了好看但送模型前必须重新按 224 处理不能直接拿显示用的图去推理。第二置信度低于某个阈值时给提示比如低于 60% 显示「建议重新拍摄」这比硬给一个错误结论负责任。第三加一个「批量检测」入口实际使用中医生往往一次拍好几张单张点选效率太低。如果要做成 exe 分发用 PyInstaller 打包注意把模型文件和可能的字体文件一起打进去用 --add-data 参数。打包后在没装 Python 的机器上测试一遍重点看模型加载路径是不是写死了绝对路径。5. 避坑与排查舌苔识别项目里最常见的五个翻车点5.1 现象训练准确率很高演示时结果全错原因训练时的预处理和 GUI 推理时的预处理不一致最常见的是归一化参数不同、通道顺序不同、或者 resize 的插值方式不同。训练用 PIL 的 bilinear推理用 cv2 的默认双线性数值上会有细微差异累积到深层网络就会被放大。解决把预处理写成一个独立函数训练和推理都调用同一个函数。如果训练用 Dataset 类推理时也复用这个类的 transform不要手写一套。5.2 现象模型对某几类完全分不开混淆矩阵里全挤在一起原因标签定义本身有歧义。比如「厚苔」和「腻苔」在视觉上高度重叠标注时不同人理解不同导致同一张图在不同标注者手里标签不一样。数据层面就已经矛盾了模型自然学不会。解决回头做一轮标注一致性检查让两个人独立标同一批图算 Kappa 系数。低于 0.7 就重新定义类别边界或者把易混类合并。宁可少分几类也不要带着矛盾标签硬训。5.3 现象GUI 程序在开发机上正常换台电脑就报错原因模型文件路径写成了绝对路径或者依赖库版本不一致。TorchScript 模型对 PyTorch 版本有一定要求高版本导出的模型在低版本上可能加载失败。解决路径统一用 os.path.join 加相对路径模型文件和脚本放同一目录。打包时锁定 PyTorch 版本在目标机器上用相同版本测试。如果必须跨版本导出 ONNX 用 onnxruntime 推理兼容性更好。5.4 现象稀有类召回率极低模型几乎不预测稀有类原因类别不均衡没处理好或者虽然加了采样权重但损失函数没加类别权重模型倾向于预测多数类就能拿到不错的整体准确率。解决先看混淆矩阵确认稀有类是不是被完全忽略。如果是检查 WeightedRandomSampler 有没有真正生效打印几个 batch 的标签分布看看。然后叠加类别权重损失但权重不要超过频率倒数开四次方太大会导致多数类性能崩掉。5.5 现象推理速度慢点一下按钮要等好几秒原因模型在 CPU 上跑且没有做推理优化。ResNet50 在 CPU 上单张图可能确实要一两秒如果 GUI 主线程直接跑推理界面会卡死。解决把推理放到子线程里主线程只负责界面刷新。如果还是慢换 MobileNetV3 或者对模型做量化torch.quantizationINT8 量化后 CPU 推理速度通常能提升 2 到 3 倍准确率损失在 1 个百分点以内。6. 进阶技巧用 Grad-CAM 让舌苔识别结果可解释模型给出「黄厚苔 85%」之后用户下一个问题往往是「你凭什么这么判断」。在医疗健康类应用里不可解释的输出很难建立信任。Grad-CAM 是最容易落地的可解释性方法它把模型最后卷积层的梯度加权回传到特征图上生成一张热力图标出模型最关注的区域。如果热力图落在舌苔上说明模型学对了如果落在嘴唇或背景上说明数据或训练有问题。实现上用 pytorch-grad-cam 库最省事也可以自己写钩子。核心步骤是注册前向钩子拿特征图注册反向钩子拿梯度对梯度做全局平均得到权重加权求和特征图ReLU 后归一化再叠加到原图上。下面是一段自包含的实现。import torch import cv2 import numpy as np class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, input_tensor, class_idxNone): output self.model(input_tensor) if class_idx is None: class_idx output.argmax(dim1).item() self.model.zero_grad() output[0, class_idx].backward() # 梯度全局平均得到每个通道的权重 weights self.gradients.mean(dim(2, 3), keepdimTrue) cam (weights * self.activations).sum(dim1, keepdimTrue) cam torch.relu(cam) cam cam - cam.min() cam cam / (cam.max() 1e-8) return cam.squeeze().cpu().numpy(), class_idx # 使用示例叠加到原图 def overlay_cam(img_path, cam, alpha0.4): img cv2.imread(img_path) img cv2.resize(img, (224, 224)) heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) return cv2.addWeighted(img, 1 - alpha, heatmap, alpha, 0)参数说明target_layer 一般选 ResNet 的 layer4 最后一个 Bottleneck太浅的层感受野小热力图会碎太深的层分辨率低定位不准。alpha 控制热力图透明度0.4 左右既能看清原图又能看到关注区域。class_idx 不指定时取预测概率最大的类也可以指定某个类看模型对它的关注区域。实际用的时候我会在 GUI 里加一个「查看关注区域」按钮点一下弹出热力图。这不仅是给用户看的也是我自己排查模型的工具。如果发现热力图长期落在舌头边缘或牙齿上说明舌体分割没做好或者数据增强把舌头裁掉了。这个习惯帮我提前发现过好几次数据管道的问题比只看准确率曲线有用得多。最后说个我自己的教训做这类项目别一上来就追求高准确率先把数据管道和预处理对齐做扎实把 GUI 推理链路跑通再回头调模型。我早期有次花了两周调网络结构最后发现是验证集里混进了训练集的图指标虚高白忙一场。希望帮到你。本文还有配套的精品资源点击获取
返回列表