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

资讯详情

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

基于深度学习的舌苔识别系统:从数据标注到GUI部署的完整实战

基于深度学习的舌苔识别系统:从数据标注到GUI部署的完整实战 简介这份资源面向深度学习方向的毕业设计学生与计算机视觉入门者提供一套完整的舌苔识别检测鉴定系统实现方案可用于中医舌象分类、体质辨识等课题的复现与二次开发。压缩包共110个文件约105.46MB包含26个Python源码文件、6个pth模型权重、2个ui界面文件、2个docx论文报告及若干json配置、jpg与png运行截图源码、模型、界面与文档配套齐全。资源围绕舌象数据集构建与扩充、图像增强、生成对抗网络、卷积神经网络训练等环节展开并附有需求分析、可行性论证与体质辨识功能设计目录结构清晰便于按模块查阅。已有183人学习下载适合需要完整赛题方案、可运行GUI演示与论文写作参考的读者能帮助快速理解从数据标注到模型部署的全流程并对照运行截图排查环境与依赖问题。1. 舌苔识别系统到底在做什么从一张手机照片到一份体质报告很多人第一次听到「舌苔识别」会以为是个噱头其实它解决的是一个很具体的痛点中医舌诊依赖经验同一个舌头不同医生看结论可能差出一大截。把这件事交给深度学习模型本质是做两件事——先在一张舌头照片里把舌体区域抠出来再对舌质颜色、舌苔厚薄、苔色、裂纹、齿痕这些特征做分类最后映射成一份可读的体质倾向报告。这套「python源码 GUI界面 模型 论文报告」的组合适合三类人想找一个完整深度学习落地项目练手的学生、需要给中医辅助诊断做原型的工程师、以及要交课程设计或毕设但不想从零搭环境的人。它不追求临床级精度追求的是链路完整、能跑通、能改。下面我按自己复现这类项目的顺序把数据、模型、GUI、踩坑一条条讲清楚。2. 数据从哪来、怎么标舌象数据集的构建与预处理2.1 舌体分割和舌苔分类是两套标签新手最容易翻车的地方是把「舌体分割」和「舌苔分类」当成一个任务。实际上这是两条独立的标注线。分割任务需要的是像素级掩码mask把舌头从嘴唇、牙齿、背景里分离出来分类任务需要的是整张舌图的属性标签比如苔色白/黄/灰黑、苔质薄/厚/腻/剥落、舌质淡红/红/绛/紫。常见做法是先用分割模型拿到舌体 ROI再在 ROI 上做分类。如果只有整图标签没有掩码可以用一个轻量分割模型先跑一遍伪标签人工修正一批再迭代。我一般会保留 10% 的图完全不参与训练专门用来验证分割边界是否稳定因为嘴唇和舌头颜色接近时模型特别容易把下唇算进舌体。标注工具用 labelme 就够导出 COCO 或 YOLO 格式都行。关键是标注规范要写死舌体边界以舌缘可见轮廓为准被牙齿遮挡的部分按平滑曲线补齐不要留锯齿。规范不统一后面模型学到的边界就是一团糊。2.2 预处理里的三个必调参数拿到原始照片后直接喂给网络效果通常很差因为手机拍摄的光照、色温、角度差异太大。预处理环节有三个参数必须调参数作用常见取值调不好会怎样输入尺寸统一分辨率224×224 或 448×448太小丢裂纹细节太大显存吃紧白平衡方式消除色温偏差灰度世界法 / 白 patch 法苔色分类直接偏色白苔判成黄苔舌体裁剪边距ROI 外扩比例5%~10%边距为 0 会切掉齿痕特征白平衡这块我要多说一句。舌苔分类对颜色极度敏感同一根舌头在暖光下拍是黄苔在冷光下拍可能就偏白。灰度世界法实现简单但对大面积单色背景会失效更稳的是在拍摄时放一张标准白卡预处理时用白 patch 做校正。如果数据集里没有白卡退而求其次用舌体区域自身的亮度均值做归一化虽然不完美但比不做强。import cv2 import numpy as np def preprocess_tongue(img_path, size448, margin0.08): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 灰度世界白平衡按三通道均值拉平 result img.astype(np.float32) avg result.reshape(-1, 3).mean(axis0) gray avg.mean() result * (gray / avg) result np.clip(result, 0, 255).astype(np.uint8) # 假设已有分割掩码 mask这里用占位 mask np.ones(img.shape[:2], dtypenp.uint8) * 255 ys, xs np.where(mask 0) y1, y2, x1, x2 ys.min(), ys.max(), xs.min(), xs.max() h, w y2 - y1, x2 - x1 # 按 margin 外扩再裁切并 resize y1 max(0, int(y1 - h * margin)) y2 min(img.shape[0], int(y2 h * margin)) x1 max(0, int(x1 - w * margin)) x2 min(img.shape[1], int(x2 w * margin)) crop result[y1:y2, x1:x2] crop cv2.resize(crop, (size, size)) return crop这段代码的逻辑是先做灰度世界白平衡把色温拉回中性再根据掩码算舌体外接框按比例外扩后裁切并统一尺寸。size决定网络输入分辨率做裂纹检测建议不低于 448margin控制外扩齿痕明显的样本可以调到 0.1避免边缘特征被切掉。注意白平衡要在裁切之前做否则背景颜色会干扰均值计算。2.3 数据增强别乱用翻转舌象数据增强有个坑左右翻转会改变齿痕的分布位置如果模型学到的是「左边有齿痕某类」翻转后就变成噪声。上下翻转更不能用舌头没有倒过来的。安全的增强是亮度扰动、轻微旋转±10°、随机裁剪和对比度调整。颜色抖动要慎用因为颜色本身就是分类依据抖动幅度大了等于在教模型忽略颜色。3. 模型选型与训练分割用 U-Net分类用轻量 CNN3.1 为什么分割和分类不共用一个 backbone有人图省事想用一个多任务网络同时出分割和分类。理论上可行但实操里两个任务的收敛速度差很多分割 loss 降得快分类 loss 还在震荡共享 backbone 容易被分割任务带偏。我一般拆成两个模型分割用 U-Net 或 DeepLabV3分类用 ResNet18 或 MobileNetV3。分割模型只负责抠图输出掩码分类模型只吃抠好的 ROI。选 U-Net 的理由是舌体分割属于小目标、边界要求高的任务U-Net 的跳跃连接能保留边缘细节。backbone 用 ResNet34 就够再深了在小数据集上过拟合严重。分类模型选 MobileNetV3 是因为要嵌到 GUI 里实时跑参数量小、推理快精度损失在可接受范围。import torch import torch.nn as nn from torchvision import models class TongueClassifier(nn.Module): def __init__(self, num_classes6, pretrainedTrue): super().__init__() # 用 MobileNetV3 做 backbone适合 GUI 端低延迟推理 self.backbone models.mobilenet_v3_small( weightsmodels.MobileNet_V3_Small_Weights.DEFAULT if pretrained else None ) in_features self.backbone.classifier[0].in_features # 替换分类头输出苔色/苔质等属性 self.backbone.classifier nn.Sequential( nn.Linear(in_features, 256), nn.Hardswish(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): return self.backbone(x) # 训练时冻结前几层只微调后半段小数据集更稳 model TongueClassifier(num_classes6) for name, param in model.backbone.features.named_parameters(): if int(name.split(.)[0]) 8: param.requires_grad False这段代码定义了一个基于 MobileNetV3 的分类器把原分类头换成两层全连接中间加 Dropout 防过拟合。num_classes按你的标签体系改如果苔色 3 类加苔质 3 类可以做成多标签输出把最后的 loss 换成 BCEWithLogitsLoss。冻结前 8 层是因为浅层学的是通用纹理小数据集上微调反而容易过拟合。训练时学习率设 1e-3冻结层用 1e-4batch size 16 起步显存不够就降到 8。3.2 训练参数怎么设才不玄学训练翻车最常见的原因是学习率和 batch size 不匹配。经验公式是学习率随 batch size 线性缩放batch 16 用 1e-3batch 8 就降到 5e-4。优化器用 AdamW 比 Adam 稳weight decay 设 1e-4。分割任务用 Dice Loss BCE 组合分类任务类别不均衡时用 Focal Loss 或给类别加权。早停策略要设patience 给 10 个 epoch。舌象数据集通常不大几百到几千张模型很容易在 30 个 epoch 内过拟合。验证集 loss 连续 10 轮不降就停保存验证集上指标最好的权重别用最后一轮的。提示训练前先把数据按 7:1.5:1.5 切分训练/验证/测试测试集全程不参与调参最后只跑一次。很多人把测试集当验证集用报出来的精度虚高。4. GUI 界面怎么做PyQt5 把模型包成能点的工具4.1 界面布局和推理线程分离GUI 最容易踩的坑是把推理放在主线程里点一下按钮界面卡死几秒用户以为程序崩了。正确做法是用 QThread 把推理放到子线程主线程只负责刷新界面。布局上分三块左边图片预览区右边结果显示区底部按钮区选择图片、开始识别、保存报告。from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog from PyQt5.QtGui import QPixmap from PyQt5.QtCore import QThread, pyqtSignal import sys class InferThread(QThread): finished pyqtSignal(dict) def __init__(self, img_path, model): super().__init__() self.img_path img_path self.model model def run(self): # 这里调用预处理 模型推理结果通过信号发回主线程 result {苔色: 白, 苔质: 薄, 舌质: 淡红} self.finished.emit(result) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(舌苔识别系统) self.label QLabel(请选择舌象图片) self.btn QPushButton(选择图片并识别) self.btn.clicked.connect(self.on_click) layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.btn) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def on_click(self): path, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.png *.jpg)) if not path: return self.label.setPixmap(QPixmap(path).scaled(400, 400)) # 启动子线程推理避免界面卡顿 self.thread InferThread(path, modelNone) self.thread.finished.connect(self.show_result) self.thread.start() def show_result(self, result): text \n.join([f{k}: {v} for k, v in result.items()]) self.label.setText(text) app QApplication(sys.argv) win MainWindow() win.show() sys.exit(app.exec_())这段代码搭了一个最小可用的 GUI 骨架。InferThread继承 QThread把推理逻辑放在run里通过pyqtSignal把结果传回主线程更新界面。on_click里先弹文件选择框选中后显示缩略图并启动线程。实际项目里run方法要接上第 2 章的预处理和第 3 章的模型推理把结果字典填完整。注意 PyQt5 的信号槽跨线程是安全的但不要在子线程里直接操作界面控件。4.2 打包成 exe 时模型文件怎么放用 PyInstaller 打包时模型权重文件不会自动打进去运行时找不到路径。解决办法是在代码里用sys._MEIPASS判断是否在打包环境把模型路径指向临时解压目录。spec 文件里用datas把 .pth 文件和标签映射 json 一起带上。打包命令加--add-data model.pth;.Windows 下分隔符是分号Linux 下是冒号。注意打包后的 exe 首次启动会解压到临时目录模型大的话启动会慢几秒别以为是卡死。可以在启动画面加个进度提示。5. 避坑与排查舌苔识别项目里最容易翻车的五件事5.1 现象训练精度 99%实际用一张新图就乱判原因数据集里同一根舌头拍了多张训练集和验证集里都有模型记住了这张图而不是学到特征。解决按「人」或「拍摄批次」切分数据集同一个人/同一批次的图只能出现在一个集合里。切分前先按文件名或元数据分组再组内切分。5.2 现象白苔全被判成黄苔原因白平衡没做或做错了暖光环境下白苔偏黄模型学到的是光照而不是苔色。解决预处理强制加白平衡训练时加亮度扰动增强让模型对色温不敏感。如果数据集本身色温混乱考虑在 HSV 空间做颜色归一化只保留色调和饱和度信息。5.3 现象GUI 点识别按钮没反应等几秒才出结果原因推理在主线程跑界面被阻塞。解决按 4.1 的方式把推理放进 QThread主线程只更新界面。如果推理确实慢可以在按钮点击后先禁用按钮并显示「识别中」完成后恢复。5.4 现象分割掩码边缘毛糙齿痕被切掉原因输入分辨率太低或者分割模型没做后处理。解决分割输入提到 512×512推理后对掩码做形态学闭运算填小洞再取最大连通域去掉零散噪点。齿痕区域可以在掩码外扩 5% 后再裁切。5.5 现象换一台电脑跑报 CUDA out of memory原因模型默认加载到 GPU但目标机器显存小或没 GPU。解决代码里加设备判断torch.cuda.is_available()为 False 时自动切 CPU。CPU 推理慢但能跑GUI 场景下 MobileNetV3 在 CPU 上单张图也就几百毫秒可以接受。6. 把精度再往上推一档多标签输出和置信度可视化单标签分类有个天然缺陷一根舌头可能同时有「薄白苔」和「齿痕」硬分成一类会丢信息。进阶做法是把输出改成多标签每个属性独立过一个 sigmoidloss 用 BCEWithLogitsLoss。这样模型能同时输出「苔色白0.92、苔质薄0.88、齿痕有0.76」信息量比单标签大得多。import torch.nn as nn class MultiLabelHead(nn.Module): def __init__(self, in_features, num_attrs): super().__init__() # 每个属性独立二分类互不竞争 self.fc nn.Linear(in_features, num_attrs) def forward(self, x): return torch.sigmoid(self.fc(x)) # 训练时用 BCEWithLogitsLoss注意输入是 logits 不是 sigmoid 后的值 criterion nn.BCEWithLogitsLoss(pos_weighttorch.tensor([2.0, 1.5, 1.0, 3.0]))pos_weight用来对付正样本少的属性比如「裂纹」可能只占 10%权重给 3.0 让模型更关注它。推理时把 sigmoid 输出按阈值 0.5 二值化但展示给用户时保留原始概率让医生自己判断置信度。置信度可视化也值得做。在 GUI 里用颜色条显示每个属性的概率低于 0.6 的标灰提示「不确定」。这样用户知道哪些结论可信、哪些只是参考。我自己的习惯是任何分类模型上线前先拿 20 张边界样本跑一遍看置信度分布如果大量样本卡在 0.5 附近说明特征区分度不够得回去查数据标注或加特征。最后说个血泪经验论文报告里的精度数字一定要用独立测试集跑别用验证集的最好一轮。我见过太多项目验证集 95%、测试集 70%问题就出在测试集被反复用来调参了。把测试集锁死跑完再打开数字难看也比虚高强。希望帮到你。本文还有配套的精品资源点击获取
返回列表