简介基于深度学习的舌苔检测完整项目面向深度学习与计算机视觉方向的学习者、课程设计与毕业设计开发者以实时目标检测算法实现舌苔图像的自动定位与特征识别适合作为医学AI课题的参考工程。整个压缩包含110个文件大小约105.37MB结构上涵盖Python源码、PyTorch权重、TensorBoard训练日志、样本图像与标注JSON、界面脚本、Word及Markdown说明文档等既可直接运行测试也能对照代码理解数据准备、模型构建、训练验证及结果可视化的各个环节。已有133人学习下载。资料中保留了训练过程的完整记录与实验设计文档能帮助读者把握YOLO在舌苔检测场景中的调优思路同时为毕业论文或答辩演示提供可复现的素材。1. 舌苔检测不是玄学深度学习方法到底解决了什么问题「基于深度学习的舌苔检测」这几年在中医数字化、健康管理类App里反复被提及目标很直接给舌头拍一张照模型自动判断舌苔的薄厚、颜色和质地输出一条可复用的结构化记录。表面上是目标检测加细粒度分类的常规组合真正落地时会卡在三个地方舌苔图像的光照敏感极高同一舌体在暖光和冷光下颜色判若两样类间差异极小薄白苔与白厚苔的边界在专业医师里都存在主观分歧数据获取又绕不开隐私和标注一致性公开可用的数据集非常有限。这类项目的源码包往往把模型和训练脚本搭好了缺的是数据、标注规范和对场景的理解。这篇文章面向健康管理App开发者、医疗辅助记录工具的研发工程师讲清楚从源码包到可运行模型的完整路径、参数设置与坑位。2. 舌苔检测用分类还是检测模型两种路线的选型依据先说结论不要一上来就套分类模型。很多第一次接触这个标题的人会把舌苔图直接喂进ResNet之类的分类网络这个思路能跑通的前提是你手里已经有裁剪好的舌头区域图。真实产品里用户随手拍的是一张口腔周围照片舌头只占画面的一小块你必须先回答「舌头在哪」才能回答「舌苔是什么」。所以第一步选型不是选网络结构而是选整体路线直接用检测模型端到端输出「舌体框 苔质类别」还是先检测舌体、再裁剪出来做二级分类。2.1 舌苔图像与通用目标的三点本质差异舌苔检测和通用目标检测面对的数据差异很大这三条直接影响模型设计和超参选择。第一颜色就是语义。苔色白、黄、灰是分类主特征但颜色受光源色温和白平衡影响极大。同一个舌体在办公室冷光灯下偏蓝、在卧室暖光灯下偏黄模型如果只学到颜色分布换个环境直接就翻车。这决定了数据增强里颜色扰动必须做、但幅度又必须很小后面第3章会专门讲。第二类间差异小到特征图几乎重叠。薄白苔和白厚苔之间不是「有」和「没有」的区别而是苔体厚度带来的模糊程度差异纹理和颜色都很接近。通用检测模型在COCO、VOC上表现好是因为类别之间有清晰的轮廓和语义边界舌苔分类更像细粒度识别比的是对微纹理的敏感度。第三边界是模糊的。舌体与口腔黏膜、牙龈之间没有硬边检测框的 ground truth 本身就带主观性。两个标注员标同一个舌头边框差10个像素很正常。这意味着你不能指望模型的框收敛到「完美」模型能稳定复现标注规范的中位数就已经算训练到位。2.2 分类路线与检测路线的取舍直接分类适合相机引导固定位置的拍照流程比如App里画一个舌头轮廓提示框用户按提示把舌头放进框里这时你拿到的基本是已裁剪图用轻量分类网络就能做检测模型则适合「拿起手机就拍」的体验自动定位舌体并输出苔质类别。实际项目里还有一种折中方案第一级用检测模型定位舌体并裁剪第二级用细粒度分类器判断苔质。这样做的精度上限更高因为两级模型各干各的活检测模型不需要同时兼顾「框得准」和「分得细」代价是流程长、部署时跑两个模型延迟翻倍端侧资源比较吃紧。路线适用场景优势代价直接分类已有稳定裁剪图、部署资源紧张模型小、训练快依赖外部定位能力用户乱拍就崩单阶段检测原图端到端识别一步到位体验自然框与细粒度类别同时学收敛慢检测分类级联类别差异极小、精度优先两级解耦上限更高双模型部署复杂端侧延迟翻倍我的习惯是第一版先做单阶段检测拿到真实用户数据看混淆矩阵如果两类苔质确实分不开再拆出二级细粒度分类器。不要一开始就把链路搞复杂。2.3 一套可落地的检测模型配置示例以YOLO系单阶段检测为例类别先按苔质分四类薄白苔、白厚苔、黄腻苔、无苔。这里不写死某个具体实现你拿到源码包时看它继承的是哪个框架参数思路是通用的。# 舌苔检测模型配置以 YOLO 系结构为参考具体参数以你手里的源码包为准 task: detect nc: 4 names: 0: thin_white 1: thick_white 2: yellow_greasy 3: no_coating # 训练超参初值 imgsz: 640 batch: 16 epochs: 150 patience: 30 lr0: 0.005为什么是四分类而不是「检测舌体 多个属性多输出」第一版做单标签最简单标注一致性容易控制颜色、薄厚、腻腐各打一套标签标注员自己都会打架模型学到的就是噪声。先把单标签做到可用再考虑多任务。imgsz 取 640 是因为苔质判断依赖纹理细节降到 320 会明显丢失苔面的颗粒纹理batch 16 在 8~16G 显存的单卡上比较稳显存不足就降 batch 并同步降学习率靠底层API去自适应。patience 30 表示验证指标连续 30 轮不改善就早停舌苔数据量通常不大训练集小的时候 patience 可以放宽到 60。3. 舌苔数据集的采集与标注比模型更关键的环节很多拿到源码包的人第一件事就是装环境、跑预训练权重跑完发现效果稀烂然后怀疑模型不对。实际上舌苔检测这类细粒度任务数据质量对结果的影响远大于模型结构。第2章选型定完后80%的精力应该花在数据采集、标注规范和增强策略上这一章讲透。3.1 采集环境、隐私脱敏与标注规范采集设备用手机后置主摄就够了前置摄像头色彩还原差一截不建议用。拍摄距离固定在 15~25 厘米让受试者自然伸出舌头不要用力伸用力伸会导致舌体充血颜色明显偏红和放松状态完全不是一个分布。光源上优先用白色LED补光灯暖色台灯会让所有苔质偏黄黄腻苔的标注直接失真。视频抽帧采集效率高但要注意不能把连续帧都放进数据集。同一段视频里相邻帧的舌苔几乎一样都放进去只会让模型记住这一个人而不是记住这一类苔质。我一般每隔 10~15 帧抽一张面部识别区域裁掉后再入库。隐私脱敏不是可选项。训练数据里只保留口腔区域眼睛和面部轮廓一律裁掉按当地医疗数据管理要求做影像匿名化处理并走知情同意流程。这个环节不合规后面上线阶段全是雷。标注规范要写成文档并配案例图。检测框紧贴舌体轮廓不包嘴唇建议用多边形标注工具先描一圈舌体再把多边形转成矩形框。类别定义里最重要的一项是「薄白苔和白厚苔的分界线画在哪」这个必须找专业医师给出示例图让所有标注员对着同一套标准标。我当时会让两个标注员独立标同一批图分歧大的样本单独抽出来复核这样能把标注噪声控制在可接受范围。3.2 标注格式转换JSON 转 YOLO 的脚本与坐标审查标注工具导出的JSON里shapes 数组每个元素包含 label 和 points 点集points 是多边形顶点。YOLO 系需要的是归一化的 xywh 中心点格式转换脚本里最容易犯的错是把图像尺寸写死。手机机型一变分辨率都不同坐标全部错位。正确做法是从原图读取宽高。import json import os from PIL import Image def annotation_json_to_yolo(json_path, image_path, out_dir, class_map): 把标注工具导出的JSON多边形点集转成YOLO格式TXT。 class_map: 类别名 - 类别ID例如 {thin_white: 0, thick_white: 1} with open(json_path, r, encodingutf-8) as f: data json.load(f) # 从原图读真实尺寸不要写死分辨率 with Image.open(image_path) as im: img_w, img_h im.size lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue points shape[points] # 多边形顶点坐标 [[x, y], ...] xs [pt[0] for pt in points] ys [pt[1] for pt in points] min_x min(xs) max_x max(xs) min_y min(ys) max_y max(ys) # YOLO格式归一化的中心点x、中心点y、宽度、高度 x_center ((min_x max_x) / 2) / img_w y_center ((min_y max_y) / 2) / img_h box_w (max_x - min_x) / img_w box_h (max_y - min_y) / img_h lines.append(f{class_map[label]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) if not lines: return out_path os.path.join(out_dir, os.path.splitext(os.path.basename(json_path))[0] .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines))脚本逻辑本身不难两个关键点一是图像宽高必须从原图读二是在转换后要做一轮坐标审查。多边形转矩形框有个固有问题——矩形一定会把舌头两侧的唇部或牙齿包进来。标注时已经尽量往舌体边缘贴但转完后还是要把标注可视化出来逐张检查矩形框内唇色占比超过一定比例的样本要重新标。这个审查步骤省不得第5章第一个踩坑记录就是没做审查的后果。3.3 舌苔专用的数据增强配置数据增强是舌苔检测里最容易被用错的环节。通用检测里好用的增强直接搬过来往往把苔质语义破坏掉。以下是一套针对舌苔调过的增强配置。import albumentations as A train_transform A.Compose([ # 亮度/对比度扰动模拟不同环境光但幅度过大会把白苔洗成无苔 A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.6), # 色相扰动必须小黄腻苔和白厚苔的色相差本来就小 A.HueSaturationValue(hue_shift_limit5, sat_shift_limit25, val_shift_limit10, p0.5), # 旋转限制在±15度90度旋转会让模型把舌根阴影当特征 A.Rotate(limit15, border_mode0, p0.5), A.HorizontalFlip(p0.5), # 轻微缩放模拟不同拍摄距离 A.RandomScale(scale_limit0.15, p0.4), ])各项参数为什么这么定色相扰动给到 5 已经是上限黄腻苔和白厚苔的色相差异本来就在几个像素级别转多了类别语义直接崩掉。旋转限制在 ±15 度是因为舌体有方向性舌根阴影和舌尖半透明区域的相对位置是模型判断苔质分布的重要线索转到 90 度这个线索就废了。随机缩放 15% 用来模拟手机拍摄距离的抖动但缩太多会丢失苔面纹理细粒度模型对分辨率很敏感。另外Mosaic 增强在舌苔细粒度任务上要谨慎开。Mosaic 把四张图拼在一起采样到的舌体区域变小对检测大目标有帮助但对「认清苔面纹理」反而帮倒忙。我一般第一版直接关掉 Mosaic等模型过拟合了再开起来当正则用。4. 训练一个舌苔检测模型命令、超参与日志排查数据备齐后训练本身的坑相对少但参数得按这个任务的特性来调不能照搬通用检测的默认配置。这一章用一个最小可复现的流程从目录组织、训练命令到日志排查讲清楚每一步。4.1 数据集目录组织与数据描述文件拿到源码包后先看数据集目录YOLO 系实现通常需要 images 和 labels 两个平级目录。这里有个容易被忽略的细节train 和 val 的划分要按人分组不能按文件随机切理由在第5章展开这里先把目录结构搭对。datasets/tongue_data/ ├── images/ │ ├── train/ │ │ ├── patient_001/ │ │ │ ├── 0001.jpg │ │ │ └── 0002.jpg │ │ ├── patient_002/ │ │ └── ... │ └── val/ │ ├── patient_010/ │ └── ... └── labels/ ├── train/ └── val/数据描述文件就是给训练器读的配置路径、类别名都要对上你实际标注的标签# tongue_data.yaml path: ./datasets/tongue_data train: images/train val: images/val nc: 4 names: 0: thin_white 1: thick_white 2: yellow_greasy 3: no_coating注意 labels 目录结构和 images 保持一致YOLO 系会按相对路径自动匹配同名 txt。如果源码包自带的划分脚本是纯随机切文件我会直接改掉按病人ID分组重新划分这一步对验证集的可信度影响很大。4.2 训练启动命令与关键超参表数据组织好后训练命令本身不长以我常用的 YOLO 系 CLI 为例yolo train modelyolov8n.pt datatongue_data.yaml \ imgsz640 batch16 epochs150 \ patience30 device0 lr00.005model 参数指向一个公开预训练权重具体版本以你源码包里提供的为准。有几个参数要按这个任务的特性单独解释参数初值调整方向说明imgsz640不建议降到320苔质纹理依赖分辨率320会模糊类间边界batch16显存不足降到8且lr0同步减半降batch不降学习率会导致梯度噪声变大lr00.005不收敛试试0.001预训练权重下初始学习率一般够用patience30数据集小就加到60早停轮数数据少loss波动大太激进会提前掐断如果训练开始时 loss 直接不降先检查是不是 lr0 太高尤其是 batch 从 16 降到 8 之后。batch 变小梯度估计噪声变大同样学习率下更容易震荡减半是最省事的办法。4.3 训练日志与验证结果是好是坏怎么判断训练过程会输出 loss 曲线、每轮的指标和预测样例图看到这些文件别只盯着最终数字按下面这三点逐项排查train loss 一路下降、val loss 在几十轮后开始反弹这是过拟合的典型特征。正确做法是回退到 val loss 回升前那一轮的权重然后加大数据增强强度而不是继续加 epoch。数据量小的舌苔任务过拟合很常见硬训只会得到一份「训练集上的完美模型」换个环境立刻崩。loss 一开始就出现 NaN大概率是标注文件里出现了宽或高为 0 的框或者某些图像解码失败。去 labels 目录里找宽度异常小的行用脚本把这类样本清掉。验证结束会输出 mAP0.5 和 mAP0.5:0.95 两个指标。如果 mAP0.5 不错但 mAP0.5:0.95 偏低说明框定位精度不行根源基本是标注框贴合度差、标注噪声大回过头去修标注比换模型有效。注意训练完一定要展开查看那些带预测框的验证图。如果预测框把舌头包住后还带出一圈暗色背景说明模型学得更倾向于「保守的大框」此时调高置信度阈值往往比继续训练更能改善观感。5. 舌苔检测高频翻车点五个踩坑记录与解法这一章是实战里反复踩过的坑每一条都按「现象→原因→解决」写清楚。前几条在数据环节就能规避后两条主要出现在上线阶段。5.1 模型把嘴唇当成了舌苔现象推理时嘴唇、口腔黏膜被大面积标成舌苔甚至一张闭嘴的照片也能框出「舌体」模型变成了红色区域检测器。原因标注阶段矩形框把嘴唇大量包进去了。舌体边界模糊标注员一偷懒框就往外扩了半个嘴唇而唇色和舌色在多数照片里确实接近模型捡到了这个捷径学习到「框内偏红就是舌苔」。解决把多边形转矩形后的样本逐张可视化凡是矩形框内唇色占比过高的重新标注框紧贴舌体边界训练完成后用验证集统计「预测框中心点落点分布」如果大量落在画面偏侧位置检查是否在学唇部特征。必要时在推理后处理里加一个位置先验滤掉中心点落在预期舌区外的检测框。5.2 阴影比舌苔纹理更「显眼」现象暗光照片里模型框出舌根到咽部的阴影区域并给出较高的置信度同一张图在补光后模型反而犹豫。原因采集时光线不均匀舌面是曲面单侧打光会在舌根形成固定阴影。模型没学会苔质纹理学的是「舌根附近有一块暗区就是舌苔」这种位置模式。解决数据增强里加入低照度模拟对随机区域做 gamma 变换或局部压暗同时采集规范里明确打光角度用双光源避免单侧强阴影。这类问题数据增强能缓解但最干净的解法还是采集端标准化。5.3 训练验证集按图片切分导致指标虚高现象训练时验证集 mAP 到 0.9做内部自测感觉很不错一到真实用户手机上一测直接掉到 0.6。原因数据划分是按文件随机切的同一个人的几十帧被分到训练集和验证集两侧。模型见过的「这个人」不在训练数据里但「这个人的舌面特征」在验证集和训练集高度相关指标自然虚高。解决按人分组划分。代码如下import os from collections import defaultdict from sklearn.model_selection import train_test_split # 假设目录结构为 images/person_id/filename.jpg all_images [os.path.join(root, f) for root, _, files in os.walk(images) for f in files if f.endswith(.jpg)] person_to_images defaultdict(list) for img in all_images: person_id img.split(os.sep)[-2] person_to_images[person_id].append(img) persons list(person_to_images.keys()) train_persons, val_persons train_test_split( persons, test_size0.2, random_state42) train_files [p for pid in train_persons for p in person_to_images[pid]] val_files [p for pid in val_persons for p in person_to_images[pid]]按人分组后验证指标掉 0.1~0.2 是正常的那才是这个模型在新用户身上的真实水平。如果源码包里自带的数据划分脚本没有按人分组果断重写。5.4 类别不均衡稀有苔质永远被忽略现象训练集里薄白苔占 70%、白厚苔 20%、黄腻苔 8%、无苔 2%训练后模型几乎把所有样本都预测成薄白苔混淆矩阵第一列接近满的。原因交叉熵损失在类样本量差距大的时候天然偏向多数类稀有类别的梯度贡献被淹没在多数类里。解决先统计每类样本数给稀有类别设置更高权重或者直接对稀有类别做简单过采样。如果源码包支持 Focal Loss这类细粒度小样本任务用它能明显改善稀有类的召回率。加权重后训练 loss 变大是正常现象不要急着调回去观察验证集上每个类别的 recall 而不是整体 mAP。提示类别权重和过采样都是手段本质问题是稀有类别的样本多样性不足。如果加了权重后黄腻苔的 recall 还是不涨优先去补采集而不是把权重加到极限然后过拟合。5.5 端侧量化后颜色漂移现象FP16 模型在服务端测试正常转成 INT8 部署到手机后黄腻苔全部被识别成白厚苔其他类别倒是没怎么变。原因INT8 量化用 KL 散度校准参数对颜色分布里的微小数值差异非常敏感。舌苔类别之间的特征差异本来就小量化噪声直接把黄腻苔和厚白苔的决策边界抹掉了。解决不要一上来就追 INT8 量化移动端 GPU 上 FP16 通常就有两到三倍加速且基本不掉点如果必须 INT8校准集要用 100~200 张真实舌苔图覆盖所有类别不要用通用场景的图片量化后按类别看准确率矩阵如果只有某一对类别互相串考虑保留这两类的 FP16 分支或对量化后模型线下微调最后一层。6. 验证模型真实水平用混淆矩阵与误检分组定位短板模型训练完不要只看 mAP 一个数字。mAP 是加权平均它掩盖了「哪两类在互相混淆」这个关键信息。我习惯先出一张混淆矩阵再对误检样本按拍摄环境分组统计用这两步定位模型的真实短板。6.1 混淆矩阵指向类别间的真实混淆验证集推理完成后把每张图的预测类别和真实类别存下来用一段简单脚本出混淆矩阵import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix class_names [thin_white, thick_white, yellow_greasy, no_coating] cm confusion_matrix(y_true, y_pred, labelsrange(len(class_names))) fig, ax plt.subplots(figsize(6, 6)) im ax.imshow(cm, cmapBlues) ax.set_xticks(range(len(class_names)), class_names, rotation45) ax.set_yticks(range(len(class_names)), class_names) ax.set_xlabel(Predicted) ax.set_ylabel(True) for i in range(len(class_names)): for j in range(len(class_names)): ax.text(j, i, cm[i, j], hacenter, vacenter) plt.tight_layout() plt.savefig(confusion_matrix.png)读这张表时重点看对角线旁边的高频格子。比如 thick_white 经常被预测成 yellow_greasy说明模型在「厚度纹理」和「颜色」之间做了错误权衡。这种一对一混淆往往不是网络问题而是标注阶段这两类的案例定义本来就分不清先去复查标注指南比盲目调损失函数有效。6.2 误检样本按环境分组统计把验证集里所有误检图按「正常光、偏暗、偏黄光源、室内闪光灯」四组归类分别统计每组准确率。如果暗光组的准确率比整体低 20 个百分点问题在光照分布覆盖不足优先补数据增强或补采集而不是换更大的backbone——舌苔检测的瓶颈通常在数据分布不在模型容量。我在第一个舌苔检测版本跑出漂亮指标时开心了一阵真实用户一拍就崩后来发现是 5.3 的数据泄漏和 5.1 的唇部误检同时发作验证集里全是「熟人」模型压根没见过真正的新环境。从那以后我养成了个习惯验证集里必须留一部分「脏照片」——设备差、光线怪、构图乱先让模型在脏数据上及格再谈漂亮指标。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?