ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于深度学习的舌象诊断系统:Python实现与课设避坑指南

基于深度学习的舌象诊断系统:Python实现与课设避坑指南 简介这份资源是Python实现的基于深度学习的舌象诊断系统完整源代码面向计算机相关专业的毕业设计、期末大作业与课程设计需求者也适合希望入门深度学习图像分类的新手参考。项目围绕舌象图像识别与诊断展开功能完善、界面美观、操作简单下载后简单部署即可运行使用。压缩包共183个文件约42.7MB其中54个py源码文件承载模型训练与界面逻辑61个jpg与1个jpeg为舌象样本图片40个pyc为编译缓存另有14个txt、7个json、2个ui界面文件及ttf字体、docx说明文档等结构清晰便于按模块查阅。代码含详细注释新手也能看懂可帮助读者快速理解数据预处理、模型构建、训练评估到界面交互的完整链路并在此基础上完成二次开发或论文撰写。目前已有164人学习下载适合需要现成方案与排错思路的课设与毕设场景。1. 舌象诊断系统到底在做什么从一张舌头照片到一条证型结论很多同学第一次听到「Python实现基于深度学习的舌象诊断系统源代码」这个课设题目脑子里浮现的是「拍张舌头照片模型告诉我得了什么病」。真动手才发现难点根本不在模型有多深而在舌体怎么从一张自拍里干净地抠出来、颜色怎么校准、证型标签怎么定义。中医舌诊看的是舌质、舌苔、舌形、润燥这几类特征落到图像上就是颜色分布、纹理、边缘形态和反光区域。系统要做的是把这些视觉特征映射到「舌质红/淡白」「苔黄/苔白腻」这类可枚举的标签上再组合成一条证型描述。它适合计算机视觉课设、人工智能大作业、医学图像入门项目的同学也适合想拿一个完整「数据—训练—推理—界面」闭环练手的人。这一章先把边界划清楚我们做的是图像分类与特征识别不是临床诊断输出的是参考标签不是处方。2. 数据从哪来、标签怎么定舌象数据集构建与预处理2.1 舌象数据的三个来源与采集注意点课设阶段拿不到医院级数据常见做法有三条路。第一条是公开的中医舌象数据集网上能搜到一些高校整理的小规模舌图库通常几百到几千张分辨率参差。第二条是自己用手机在自然光下拍找同学当志愿者每人拍伸舌正面、侧面、舌下三张注意别用美颜和滤镜那会把舌色改得面目全非。第三条是从网络图片里筛但版权和标注质量都不好控只建议做补充。采集时最容易翻车的是光源。同一根舌头白炽灯下偏黄冷白灯下偏青模型学到的可能是灯而不是舌。我的习惯是固定一个时间段、固定窗边散射光、关掉顶灯手机开专业模式锁白平衡。样本量上如果只做三分类比如舌质淡白/红/绛每类至少 150 张起步做苔色加苔质的多标签每类 300 张以上才勉强能训。2.2 舌体分割把舌头从嘴唇和牙齿里抠出来直接拿整张自拍去分类背景和嘴唇会严重干扰。所以预处理第一步是舌体分割。课设里没必要上重型分割网络用 OpenCV 在 HSV 空间做阈值加形态学就够用快且可解释。import cv2 import numpy as np def extract_tongue_region(img_bgr): # 转 HSV舌体在 H 通道偏红S 通道饱和度较高 hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) # 红色在 HSV 里跨 0 和 180 两端分两段取再合并 lower1 np.array([0, 40, 60]) upper1 np.array([10, 255, 255]) lower2 np.array([170, 40, 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_OPEN, kernel, iterations2) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations3) # 取最大连通域排除嘴唇碎片 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None big max(contours, keycv2.contourArea) clean np.zeros_like(mask) cv2.drawContours(clean, [big], -1, 255, -1) tongue cv2.bitwise_and(img_bgr, img_bgr, maskclean) return tongue, clean这段逻辑的关键在 HSV 阈值。lower1/upper1管 0 到 10 的红色lower2/upper2管 170 到 180 的红色两段合并才能覆盖完整红色环。S下限设 40 是为了滤掉灰白背景V下限 60 滤掉暗部。开闭运算的核大小 7 是经验值图小就降到 5图大可以到 9。最后取最大连通域是因为嘴唇常被一起选中而舌体通常是画面里最大的红色块。如果分割结果把下巴也带进来说明V下限太低往上调到 80 试试。2.3 颜色校准与尺寸统一分割完还要做颜色校准否则不同设备拍的图没法一起训。简单可靠的办法是白平衡归一化在舌体区域外找一块接近白色的参考比如牙齿或背景纸算它的 RGB 均值再按比例缩放整幅图。没有参考物时退而求其次做灰度世界假设把三个通道均值拉到相近。尺寸统一到 224×224 或 256×256看你的骨干网。注意不要直接拉伸舌体长宽比会被破坏建议先按短边缩放再中心裁剪或者填充成正方形。标签文件用 CSV一行一张图列是文件名加各标签的 0/1方便后面多标签训练。3. 模型选型与训练从 ResNet 迁移到多标签舌象分类3.1 为什么课设首选迁移学习而不是从零训 CNN舌象数据量小从零训一个 CNN 基本必然过拟合验证集准确率卡在 60% 上下晃。迁移学习拿 ImageNet 预训练的 ResNet18 或 MobileNetV3把最后全连接层换成自己的类别数前几层冻结只训后面几十张图就能看到效果。MobileNetV3 更适合课设因为参数量小普通笔记本 CPU 也能跑推理答辩演示不慌。选 ResNet18 还是 MobileNetV3看两点如果要做多标签舌质、苔色、苔质同时输出两个都行改输出维度即可如果要在树莓派或手机上演示选 MobileNetV3。别一上来就上 ViT数据不够时它比 CNN 更难收敛调参时间会吃掉你大半工期。3.2 用 PyTorch 搭一个可跑通的训练脚本import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import transforms, models from PIL import Image import pandas as pd class TongueDataset(Dataset): def __init__(self, csv_path, img_dir, transformNone): self.df pd.read_csv(csv_path) self.img_dir img_dir self.transform transform # 假设 CSV 里 label_ 开头的列都是标签 self.label_cols [c for c in self.df.columns if c.startswith(label_)] def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(f{self.img_dir}/{row[filename]}).convert(RGB) if self.transform: img self.transform(img) labels torch.tensor(row[self.label_cols].values.astype(float32)) return img, labels # 训练集做增强验证集只做归一化 train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def build_model(num_labels): model models.mobilenet_v3_small(weightsmodels.MobileNet_V3_Small_Weights.DEFAULT) # 冻结特征提取部分 for p in model.features.parameters(): p.requires_grad False in_features model.classifier[3].in_features model.classifier[3] nn.Linear(in_features, num_labels) return model def train(csv_train, csv_val, img_dir, num_labels, epochs20): device torch.device(cuda if torch.cuda.is_available() else cpu) train_ds TongueDataset(csv_train, img_dir, train_tf) val_ds TongueDataset(csv_val, img_dir, val_tf) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size16, shuffleFalse) model build_model(num_labels).to(device) # 多标签用 BCEWithLogitsLoss单标签换成 CrossEntropyLoss criterion nn.BCEWithLogitsLoss() optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) for epoch in range(epochs): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() # 验证阶段只算平均损失方便观察是否过拟合 model.eval() val_loss 0.0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) val_loss criterion(model(imgs), labels).item() print(fepoch {epoch1}, val_loss {val_loss/len(val_loader):.4f}) return model逻辑上分三块。TongueDataset负责把 CSV 里的文件名和标签读进来标签列以label_开头这样加标签不用改代码。build_model冻结features只训分类头这是小数据防过拟合的核心操作。损失函数用BCEWithLogitsLoss是因为舌象标签天然是多标签——一张图可以同时是「舌质红」和「苔黄腻」用交叉熵会强行让它们互斥反而不对。参数上batch_size16是 8G 显存的安全值显存小就降到 8。lr1e-3配 AdamW 对冻结骨干的微调比较稳如果 loss 震荡就降到 5e-4。epochs20是起点看验证损失连续 3 轮不降就停。增强里ColorJitter的幅度别开大舌色是核心特征改狠了等于把标签改了。3.3 评估指标别只看准确率多标签任务里准确率会被大量负样本稀释。比如 10 个标签里 9 个是 0模型全预测 0 也能有 90% 准确率但一个阳性都抓不到。要看每个标签的 precision、recall 和 F1重点看阳性样本少的那些类。课设答辩时老师常问「你苔黄腻这一类召回多少」提前把sklearn.metrics.classification_report跑出来按标签列打印心里有数。4. 推理服务与界面把模型接成一个能演示的系统4.1 用 Flask 起一个最小推理接口from flask import Flask, request, jsonify import torch from torchvision import transforms from PIL import Image import io from model_def import build_model # 复用训练时的模型定义 app Flask(__name__) device torch.device(cpu) LABELS [舌质淡白, 舌质红, 舌质绛, 苔白, 苔黄, 苔腻, 苔少] model build_model(len(LABELS)) model.load_state_dict(torch.load(tongue_model.pth, map_locationdevice)) model.eval() infer_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) app.route(/predict, methods[POST]) def predict(): file request.files[image] img Image.open(io.BytesIO(file.read())).convert(RGB) x infer_tf(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(x) probs torch.sigmoid(logits)[0].cpu().numpy() # 阈值 0.5 以上算命中可按标签单独调 result {LABELS[i]: float(probs[i]) for i in range(len(LABELS)) if probs[i] 0.5} return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000)接口只做一件事收图、预处理、前向、sigmoid 转概率、按阈值筛标签。load_state_dict前一定要先build_model建同结构否则键对不上。阈值 0.5 是默认值实际用的时候阳性少的标签可以降到 0.3 提高召回代价是误报变多这个权衡在答辩时讲清楚反而是加分项。4.2 前端页面与结果呈现前端不用复杂一个 HTML 表单上传图片JS 用fetch发到/predict把返回的 JSON 渲染成标签加概率的列表就行。展示时建议按概率排序并给每个标签配一句简短说明比如「苔黄舌苔呈黄色多提示热象」。这样演示时不像一个黑匣子老师能看懂系统在输出什么。如果时间充裕可以在页面上并排显示原图和分割后的舌体图直观展示预处理效果。这一步对答辩帮助很大因为分割质量直接决定模型输入质量把中间结果亮出来说明你理解整条链路。5. 避坑与排查舌象系统课设里最容易翻车的五件事5.1 现象训练准确率 95%换一批图就崩原因几乎都是数据泄漏。同一根舌头拍的多张图被随机分到了训练集和验证集模型记住了这根舌头而不是特征。解决是按人划分数据集同一个人所有图只进训练或只进验证用GroupShuffleSplit按志愿者 ID 分组。5.2 现象分割结果把嘴唇和下巴一起框进来HSV 阈值里V下限太低暗红区域被误判。把V下限从 60 提到 80 到 100同时把S下限提到 60。如果还不行加一步「取轮廓后按面积和长宽比过滤」舌体长宽比通常在 1.2 到 2.0 之间太扁的轮廓丢掉。5.3 现象验证损失不降训练损失一直降典型过拟合。先确认骨干是否真的冻结了打印requires_grad为 True 的参数数量应该只有分类头。然后加增强、加 dropout、减小学习率。如果数据实在少把RandomCrop的尺寸从 224 降到 192让模型看到更多局部变化。5.4 现象推理时概率全在 0.5 附近没有明确标签模型没学好或者标签定义太模糊。检查标签是不是互斥的却被当成多标签训了比如「舌质淡白」和「舌质红」不该同时为 1。另外看训练轮数是不是不够冻结骨干时收敛会慢一些20 轮可能偏少加到 30 轮再看。5.5 现象Flask 接口在本地跑通换台机器就报错多半是模型保存和加载的设备不一致或者model_def.py没一起拷过去。保存时用torch.save(model.state_dict(), ...)加载时先实例化同结构模型。跨设备时map_location要写对CPU 推理就写cpu。依赖版本也容易出问题把torch、torchvision、flask的版本写进requirements.txt换机器先pip install -r。6. 让系统更可信置信度校准与一个可复现的验证技巧课设做到能跑通只是及格线想让老师觉得你懂行得在输出可信度上做点文章。多标签模型直接输出的 sigmoid 概率往往偏乐观明明没把握的标签也给到 0.6。一个便宜好用的校准方法是温度缩放在验证集上训一个标量温度 T把 logits 除以 T 再 sigmoid让概率更贴近真实命中率。import torch import torch.nn as nn import numpy as np def find_temperature(logits, labels): # logits/labels 都是验证集上的 numpy 数组 logits_t torch.tensor(logits, dtypetorch.float32) labels_t torch.tensor(labels, dtypetorch.float32) # 温度作为可学习参数用 LBFGS 优化 T nn.Parameter(torch.ones(1) * 1.5) optimizer torch.optim.LBFGS([T], lr0.01, max_iter50) criterion nn.BCEWithLogitsLoss() def closure(): optimizer.zero_grad() loss criterion(logits_t / T, labels_t) loss.backward() return loss optimizer.step(closure) return T.item()用法是先跑一遍验证集把模型输出的原始 logits 和真实标签存下来调find_temperature得到 T推理时把logits / T再 sigmoid。T 大于 1 说明模型原来过于自信除以它之后概率会被压平更接近实际。这个技巧代码量小但答辩时能讲出「我做了置信度校准」比只报一个准确率有说服力。验证校准效果可以画可靠性图把预测概率分成 10 个区间每个区间算平均预测概率和实际阳性率理想情况是对角线。校准前曲线通常低于对角线校准后会贴近。这个图用 matplotlib 十几行就能画放在报告里很直观。另一个我常用的验证习惯是留一志愿者交叉验证每次留一个人的所有图做验证其余人训练轮一遍取平均指标。虽然训练次数多但小数据集上比单次划分可靠得多能提前暴露「换个人就崩」的问题。课设时间紧的话至少做 3 折按人分组验证别只做一次随机划分就写结论。最后说个血泪经验先把整条链路用 20 张图跑通再扩数据。我见过太多同学卡在数据收集上收了两千张图才发现分割脚本有 bug全部重来。先用小样本把分割、训练、推理、界面四步走通每一步都有输出再往里灌数据工期可控得多。舌象诊断这个方向模型结构不是门槛数据质量和预处理才是真正拉开差距的地方。希望帮到你。本文还有配套的精品资源点击获取
返回列表