
简介这套基于深度学习的舌象诊断系统是一款面向计算机类毕业设计与课程设计的完整源码包目标场景是借助图像识别技术辅助舌象判读适合具备一定Python与深度学习基础、正在筹备完整项目或论文的学生。压缩包共184个文件42.65MB以54个Python源码、40个pyc缓存及61张JPG舌象样本为主另含JSON配置、UI界面、TXT/Markdown说明和《学习路线》文档覆盖模型搭建、数据组织、交互界面到项目文档的全链路已有93人浏览学习。项目目录结构清晰代码可直接运行示例图像与文档配套完整便于复现舌象识别流程也能为毕业答辩、课程报告和二次开发提供可落地的工程基线。1. 基于深度学习的舌象诊断系统毕设课设都能用的完整闭环毕业设计里“舌象诊断”这种题看着专业做起来却特别容易烂尾。数据集要自己清理模型要能分清舌头和嘴唇最后还要当着导师的面跑通一整套流程——既有Python训练又有C推理才撑得起“系统”两个字。这套基于深度学习的舌象诊断系统其实是一份把整条链路补全的工程包从舌象图像预处理到多标签分类模型训练再到LibTorch导出和C端调用每一步都有可运行的代码和调参记录。适合用PyTorch做毕设或课程设计的在校生也适合想快速迁移到口腔图像识别、中医客观化场景的工程师。与其自己从头攒环境、踩数据坑不如拿这套代码当骨架按自己的数据换一换类别和路径就能交差。2. 舌象诊断的技术底座先搞清楚模型到底在学什么2.1 舌象特征映射到视觉任务颜色、纹理与形态中医舌诊的维度归纳起来基本是四类舌色淡白、淡红、红、绛红、苔色白、黄、灰黑、舌形胖大、瘦薄、齿痕和裂纹。这四个维度映射到图像任务上本质不是同一个问题。舌色是全局颜色统计舌苔是局部纹理分布舌形是轮廓形状判断裂纹则是更细的边缘与纹理组合。所以这套诊断系统做的其实是多标签分类同一张舌图可能同时输出“舌红”“苔黄”“有齿痕”三个结论而不是简单地从几个类别里挑一个。很多人拿到题目的第一反应是上目标检测拿YOLO去框舌头这个方向其实偏了。舌象诊断更接近细粒度图像分类类间差异非常小薄白苔和白腻苔在RGB直方图里几乎重合模型必须学到局部细节才能区分。因此输入侧我会统一先把图片resize到224×224然后用ImageNet预训练权重初始化网络再在自建舌象数据集上做微调。迁移学习在这里的价值很明显舌象数据和ImageNet的分布差异虽然不小但底层的边缘、纹理、颜色渐变卷积核都是可复用的能极大缓解小数据集训不动的尴尬。细粒度分类还有个容易被忽略的点注意力。模型需要自己学会把有效权重放到舌面上而不是背景的桌子或嘴唇上。可如果输入图里背景太乱模型就有概率走偏。与其让模型硬学不如在预处理阶段就把非舌头区域去掉。这也是后面要单独做舌象分割的原因先抠图再训练比直接扔原图进去省心得多。2.2 为什么不选复杂网络backbone选型对比本科毕设或课程作业能拿到的舌象数据通常只有几百到几千张甚至有些是从公开数据集里拼出来的。在这个数据量级下选网络的第一原则不是越深越好而是“容量够用且不容易过拟合”。ResNet系列是我在这个代码包里默认采用的backbone原因很朴素结构成熟、PyTorch和LibTorch对它的算子支持全面、导出部署不会出幺蛾子。模型参数量3000张图单epoch参考耗时适用场景ResNet18约11M约150秒起步验证、CPU部署、快速跑通流程ResNet34约21M约200秒默认选项精度与速度平衡ResNet50约25M约260秒数据量够大时追求上限MobileNetV3约4M约120秒边缘设备或实时性要求高我一般建议从ResNet18开始原因很实际在几百张舌象数据上ResNet18和ResNet50的最终准确率差距通常不超过两个点但ResNet18对标注噪声更钝感。模型容量一旦过大反而容易把个别标错的样本“背”下来导致验证集曲线忽高忽低也就是大家常说的翻车。如果换到上千张图、类别又多到十几个再把backbone切到ResNet50。选型完成后同样值得注意的是舌象特有的数据陷阱比如同一个病人拍了十几张高度相似的图片如果随机划分训练集和验证集模型会靠“记忆”拿高分换到新病人立刻崩。这个问题代码包的数据划分脚本里已经做了按病人ID分组的处理但换用自己的数据时很多人会忘了这一步后面避坑章节再专门说。3. 数据准备与预处理从原始舌图到干净的训练集3.1 舌象分割用HSV颜色空间把舌头从脸上抠出来自采的舌象照片背景通常很杂有嘴唇、牙齿、下巴甚至手机边框。直接拿去训练模型会花大量参数学习背景的冗余特征。所以第一步是分割最常见的做法是基于HSV颜色空间的阈值分割。RGB下肤色和舌色的差异不好描述但HSV把色调、饱和度、明度拆开之后舌头在色调维度上有相对稳定的区间。import cv2 import numpy as np def extract_tongue_mask(image, vizFalse): # 转HSV色调H独立于亮度比RGB更适合肤色与舌色的区分 hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 舌体色相大致落在0~10和170~180两段 # 饱和度S取30以上明度V取60以上先把暗部和皮肤背景滤掉 lower1 np.array([0, 30, 60]) upper1 np.array([10, 255, 255]) lower2 np.array([170, 30, 60]) 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_CLOSE, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) if viz: overlay image.copy() overlay[mask 0] (0, 0, 0) cv2.imshow(tongue_mask, overlay) cv2.waitKey(0) return mask这段代码的核心逻辑是先找出“偏红”的像素区域再用形态学操作把结果整理成连续的舌体mask。两个色相区间分别对应标准红色区间和红色-紫色交界区间因为舌头在不同光照下色相会有轻微漂移单区间容易切碎。饱和度下限30和明度下限60是为了排除皮肤和嘴唇的暗部这两个阈值在室内正常光照下表现稳定。这套分割在暗光或偏色场景下会失效典型症状是mask中间出现大量空洞或者把嘴唇一起框进来。资源包里给出的参数是基于室内自然光拍摄的舌图换成手机补光拍摄通常需要把明度下限往下调。分割的目的不是做到像素级完美只要舌体区域保留八成以上、背景去掉七成以上对分类模型的帮助就非常明显。3.2 数据增强与类别不均衡小样本下的保命手段舌象数据集常见的毛病是类别严重不均衡“健康/淡红舌”可能占了快一半黄苔、灰黑苔这类样本只有几十张。如果直接按原始分布训练模型会学出一个偷懒策略全部预测成类别多的那一类因为整体准确率依然很高。from torchvision import transforms # 训练阶段的增强组合颜色扰动模拟不同光照旋转和裁剪模拟拍摄角度差异 train_transform transforms.Compose([ transforms.RandomResizedCrop(size(224, 224), scale(0.8, 1.0)), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.02), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])增强配置里ColorJitter是舌象任务的关键。不同手机拍出来的舌头颜色偏差很大如果不做颜色抖动模型会把“偏冷的红”和“偏暖的红”学成两类。hue抖动我控制在0.02以内因为舌色诊断本身依赖颜色信息色相扰动太猛会把真实类别信息破坏掉。类别不均衡的解决手段我推荐用加权采样。代码包里的做法是给每个类别统计样本数然后反比设置采样权重让每个batch里不同类别的图片尽量均衡。配合多标签场景下的BCEWithLogitsLoss模型输出层的每个节点独立判断一个标签是否存在而不是像单标签分类那样做softmax互斥。这点容易被忽略用CrossEntropyLoss跑多标签任务会直接出问题因为一张图既要“舌红”又要“苔黄”时softmax会强行分出一个主类。4. 训练与部署核心代码、参数配置与C落地4.1 基于PyTorch的训练主流程训练主流程没有太花哨的设计就是标准的PyTorch训练循环但有几个细节直接关系到舌象任务能不能收敛。数据加载用Dataset类封装每个样本读的是分割后已经去背景的图片标签是multi-hot向量。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import models from dataset import TongueDataset model models.resnet34(pretrainedTrue) # 全连接层改为多标签输出num_classes对应标签维度 model.fc nn.Linear(model.fc.in_features, num_classes) model model.cuda() train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) criterion nn.BCEWithLogitsLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) for epoch in range(total_epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels.float()) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) print(fepoch {epoch:03d} loss {running_loss/len(train_set):.4f})代码里的关键点在最后两层。model.fc替换成输出维度等于标签数量的线性层配合BCEWithLogitsLoss逻辑上等价于每个标签做一个独立的逻辑回归。输出层没有接sigmoid因为BCEWithLogitsLoss内部已经做了数值稳定的sigmoid计算直接在forward里接sigmoid反而会让梯度变得不稳定。4.2 参数配置学习率、优化器与checkpoint策略这套系统里推荐的训练超参我见过很多复现失败都是因为照搬了ImageNet分类的参数。舌象数据量小学习率必须压得比常规分类任务低。参数推荐值说明优化器AdamW权重衰减更稳定配合预训练模型效果好初始学习率1e-4微调阶段别超过这个值训练轮数30~50数据几百张时40轮以内足够batch size16~32取决于GPU显存小于16时注意BN统计学习率策略CosineAnnealingLR比固定学习率稳定避免后期震荡损失函数BCELossWithLogits多标签场景专用训练有个容易被忽略的点注意观察训练loss和验证loss的差距。舌象诊断的类别相似度过高模型很容易在训练集上把细微颜色差异“背”下来验证loss在第15个epoch就开始反弹这时候不是继续加大epoch就能解决的而是要么增强数据扰动要么提前在验证集F1最高的位置保存checkpoint。checkpoint建议每5个epoch保存一次同时额外保存一份验证集指标最优的权重。代码包里对最好权重做了单独复制路径一般是checkpoints/best_f1.pth后续导出和部署都用它避免手忙脚乱去翻历史权重。4.3 部署环节LibTorch导出与C推理很多毕设做到训练完就停了但项目名带“系统”两个字演示时通常要求有一个能脱离Python环境运行的接口。常见做法是用torch.jit把模型导出再在C端用LibTorch加载。导出这一步有一些细节不注意就会出问题。import torch from torchvision import models model models.resnet34(pretrainedFalse) model.fc torch.nn.Linear(model.fc.in_features, num_classes) checkpoint torch.load(checkpoints/best_f1.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model.eval() # 用固定shape的示例输入做trace导出后续C端输入必须严格匹配 example torch.randn(1, 3, 224, 224) traced torch.jit.trace(model, example) traced.save(tongue_resnet34.pt)使用torch.jit.trace时示例输入的维度要和实际部署时的输入一致这里是1×3×224×224。如果C端用了1×3×128×128的输入虽然大概率也能跑但输出结果完全不可控。如果想导出更灵活的动态维度版本需要用torch.jit.script重写forward但舌象分类场景输入通常是固定尺寸trace足够了。C端加载模型的前处理最容易踩坑。训练时用的是Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225])C端必须原样复刻包括通道顺序。#include torch/script.h #include opencv2/opencv.hpp torch::Tensor preprocess(const cv::Mat bgr) { cv::Mat rgb, resized; cv::cvtColor(bgr, rgb, cv::COLOR_BGR2RGB); cv::resize(rgb, resized, cv::Size(224, 224)); resized resized.clone(); // 保证内存连续from_blob的前提 torch::Tensor tensor torch::from_blob( resized.data, {1, 224, 224, 3}, torch::kUInt8); tensor tensor.permute({0, 3, 1, 2}).to(torch::kFloat).div(255.0); // 每通道标准化mean和std必须与训练侧完全一致 auto mean torch::tensor({0.485, 0.456, 0.406}).view({1, 3, 1, 1}); auto std torch::tensor({0.229, 0.224, 0.225}).view({1, 3, 1, 1}); return (tensor - mean) / std; }这段代码里最容易翻车的是from_blob它指向的是resized.data的内存地址如果resized在后续操作中被释放或内容被修改tensor的行为就不确定了所以提前clone()保证独立内存。另一个细节是通道顺序OpenCV默认是BGR模型训练时用的是RGB这里必须先转再送入网络顺序反了的表现是训练时准确率正常部署后准确率掉到接近随机。C主程序加载模型后把预处理tensor包装成std::vectortorch::jit::IValue传进去对输出做sigmoid并在0.5阈值处切分类别就完成了整个推理闭环。这套链路跑通之后系统的完整性会明显上一个档次答辩或课设展示时也更有说服力。5. 舌象诊断系统的避坑指南五条血泪经验5.1 训练过程loss持续震荡模型永远预测“健康”现象训练loss前三轮快速下降之后一直横在0.6附近验证集输出几乎全是“健康”这个类别。原因数据集中健康类占比超过七成模型发现全猜健康也能拿高准确率梯度在多数样本上确认了这个方向难样本对损失函数的贡献被稀释。解决改用WeightedRandomSampler按类别反比加权采样让每个batch中不同类别的数量趋于均衡同时把评价指标从Acc换成F1或AUC训练中只按验证集F1保存最优权重。5.2 验证集准确率89%换一批新舌图直接崩现象自己的验证集上表现很好到导师拿来的新采集照片上预测结果明显异常。原因划分数据时按图片随机切分同一个病人拍了十几张高度相似的图同时落在训练集和验证集等于模型“见过”验证集的一部分。这是舌象数据集里最隐蔽的数据泄露。解决按病人ID或拍摄会话划分数据集保证同一个人的所有图片只在训练集或验证集中出现。代码包里的划分脚本已经按目录名做了这种处理换成自己的数据时这一步不能省。5.3 Python推理正常C端输出全是同一个值现象Python端加载同一个权重文件预测良好C端编译运行后输出几乎归一所有标签概率相同。原因前处理没对齐常见是忘了做标准化或者用了BGR直接送入模型。模型训练时学的特征分布是一套mean/std归一化后的空间输入分布一变整个输出就垮了。解决把Python训练时的预处理封装成一个函数导出前手工验证C端先用cv::cvtColor转RGB再逐通道执行标准化。更稳的做法是导出一个包含预处理节点的graph但那样需要把resize和标准化写进torch.jit脚本里复杂度高一点。5.4 用YOLO框舌头框出来总带上嘴唇现象目标检测方案跑下来预测框把舌头和嘴唇整个包进去切出来的图没法做分类。原因舌象不是目标检测任务舌头和嘴唇的视觉边界在框级别无法区分。YOLO学习的是目标中心与框范围它没有像素级的边缘敏感度框到嘴唇是结构性的必然。解决改用分割后再分类的两段式流程。先用HSV阈值分割或轻量分割网络获取舌体mask按mask的裁剪框做resize再送进分类网络。这套系统代码包里默认就是分割分类结构。5.5 数据标注口径混乱训练集里同一类舌头长得完全不一样现象训练集里“苔白”这一类中有偏黄的、有偏灰的模型训练完成后对黄苔的置信度始终不高。原因舌象标注个人主观性很强不同来源的数据集对“薄白苔”和“白腻苔”的判读标准不一致混在一起训练会污染边界样本。解决训练前先做一次标签清洗把置信度低的样本抽出来人工复核更快的做法是先用现有模型跑一遍预测把预测概率低于0.7的样本挑出来看大部分是标注或分割的问题。这块脏活躲不掉但做完之后模型稳定性是质的提升。6. 进阶把准确率从85%提到90%的几个土办法基础链路跑通之后卡在85%左右上不去是常态。我试过几条收益最明显的路线都不需要换大模型适合在课设和毕设里快速见效。第一个是加注意力模块。在ResNet倒数第二层特征图后拼接一个轻量CBAM代码量不到十行训练时间大约增加一成对舌色和纹理的判断更聚焦。CBAM的通道注意力能自动增强“颜色相关”特征空间注意力让模型把有效位置放到舌体中央比单纯加模型深度划算得多。第二个是换损失函数。BCEWithLogitsLoss在难样本上容易被“带偏”改成Focal Loss之后让模型更关注那些预测概率在0.4~0.6之间的模糊样本黄苔和灰黑苔这类少数类的F1会有明显上升。唯一要注意的是gamma参数从2开始调太大会让训练早期梯度几乎消失。第三个是我个人最推荐的做一次Grad-CAM可视化。把激活热力图叠到原图上你能清楚看到模型判断“舌红”时看的是舌面还是嘴唇边缘。我见过一个案例模型在背景上学会了一个暗色纹理特征准确率看着不错热力图一出来就现了原形——它根本没在学舌头。从那以后我每次做图像分类项目都强制走一遍热力图验证哪怕只是放在答辩PPT里截图展示都比只贴一张准确率曲线更能说服人。这套舌象诊断系统的分割、训练、导出和C部署链路都已经按这个流程打通拿到代码包后按顺序跑大概率能少走我当初踩过的这些弯路希望帮到你。本文还有配套的精品资源点击获取