ARTICLE DETAIL

资讯详情

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

舌苔识别检测鉴定系统源码详解:从数据增强到CNN分类与GUI部署

舌苔识别检测鉴定系统源码详解:从数据增强到CNN分类与GUI部署 简介面向深度学习视觉方向毕业设计的舌苔识别检测鉴定系统完整工程包适合计算机视觉、医学图像处理等方向学生参考与二次开发。包内整合了可运行的Python源码、带GUI交互界面、训练完成的模型权重pth文件及配套毕业论文覆盖舌象数据集构建、图像增强、DCGAN生成舌象图片、卷积神经网络训练与舌苔检测等关键环节并附带实验训练记录。资源共一百一十个文件其中包含py脚本、pth模型、ui界面、json配置、jpg与png样本图、docx论文等主要类型压缩包大小约105.47MB目录结构清晰可对照论文与源码理解系统脉络便于按章节复现实验。该工程包已吸引266人浏览学习尤其适合需要完成舌苔识别类课题设计并希望快速搭建可演示系统的毕设学生。1. 舌苔识别检测鉴定源码拆包先认目录再谈复现第一次解压这套基于深度学习的舌苔识别检测鉴定系统源码我习惯先把整个目录结构扫一遍而不是急着双击一个大的 .py 文件。看到一堆events.out.tfevents.*文件时心里基本有底了训练走的是 TensorFlow 的正式日志流程不是随手写的玩具脚本再继续找 GUI 入口、模型权重和论文文档三轮下来这份材料的完整度就清楚了。这套源码解决的是一个很具体的落地问题——从一批标注好的舌象图片出发训练一个能区分舌苔状态的分类模型再把这个模型包进一个带图形界面的工具里让非技术背景的人也能选一张图、点一下按钮就看到结果。它适合正卡在毕设选题、需要一份能讲清楚原理又跑得起来的参考工程的同学也适合想把手头中医舌诊图片快速做成小系统的从业者。下面我按拆包顺序把里面最值得复用的几块逐一展开。2. 数据集构建从标注分类到 DCGAN 扩样整条流程一次走通舌象数据是这套系统真正的瓶颈。论文第 4 章的标题已经把过程串出来了先做图片标注分类再做常规图像增强最后用生成对抗网络补少数类。把这三层串起来的逻辑很简单原始舌象图片往往数量少、类别也不均衡直接拿去训卷积神经网络很容易过拟合所以先用传统增强把数据量撑起来再用 DCGAN 把数量不足的类别补到能训练的量级。下面按顺序讲每一步以及每一步里最容易出错的地方。2.1 标注分类先行目录名就是标签拿到原始舌象图片第一件事不是写网络而是把标签定清楚。常见做法是按类别分目录一个目录对应一种舌苔状态目录名就是类别名。这套源码的数据加载大概率走的是目录结构所以目录名一旦不一致后面训练时类别映射就会错位。import os import shutil from sklearn.model_selection import train_test_split src_dir data/raw_tongue # 原始图片按类别放在这里 dst_dir data/tongue_split # 切分后的输出目录 classes [white_coat, yellow_coat, gray_black_coat] for cls in classes: files [f for f in os.listdir(os.path.join(src_dir, cls)) if f.endswith((.jpg, .png))] train, val train_test_split(files, test_size0.2, random_state42) for subset, subset_files in [(train, train), (val, val)]: out_path os.path.join(dst_dir, subset, cls) os.makedirs(out_path, exist_okTrue) for f in subset_files: shutil.copy(os.path.join(src_dir, cls, f), out_path)这段脚本做两件事先按类别归档再用train_test_split按 8:2 分出训练集和验证集。random_state42保证每次切分结果完全一致这对复现训练曲线和调试问题都很重要。test_size0.2是中小数据集的常用比例数据总量只有一两百张时再往大切验证集就会挤压训练数据。这里有个容易被忽略的坑如果数据集中同一个来源的多张图片随机切分很容易把同源图片分到训练和验证两边后面训练出来的验证精度会虚高。遇到这种情况我会把切分依据从文件名换成样本来源编号按人分组切分。这个细节直接关系到模型真实泛化能力第 5 章里还会专门展开。2.2 图像增强扩充数据集旋转、翻转、亮度扰动怎么设舌象图片和普通物体识别不太一样舌头在画面里通常居中但拍摄角度和光照强度波动很大舌色和苔色对光源尤其敏感。所以增强策略要有针对性不能照搬 ImageNet 那套大的旋转和裁剪。常规做法是用 TensorFlow/Keras 里的ImageDataGenerator做在线增强训练时每轮看到的图都不完全一样等效扩充了数据集。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1.0 / 255.0, rotation_range10, width_shift_range0.08, height_shift_range0.08, horizontal_flipTrue, brightness_range(0.8, 1.2) ) val_datagen ImageDataGenerator(rescale1.0 / 255.0) train_generator train_datagen.flow_from_directory( data/tongue_split/train, target_size(224, 224), batch_size32, class_modecategorical ) val_generator val_datagen.flow_from_directory( data/tongue_split/val, target_size(224, 224), batch_size32, class_modecategorical )几个参数值得单独解释rotation_range10而不是 30因为舌象旋转太狠会破坏生理结构特征模型会学到不真实的形态horizontal_flipTrue是安全的舌头左右对称翻转不改变类别语义。brightness_range(0.8, 1.2)直接模拟不同采光条件下的舌色和苔色变化这个对舌苔检测尤其关键。flow_from_directory会自动把目录名解析成类别名并按字母序生成class_indices映射后面 GUI 推理时要保持这个顺序一致。验证集这里只用rescale不加任何随机变换否则验证结果会被增强噪声污染失去对真实精度的评估意义。2.3 DCGAN 生成舌象补少数类训练 trick 与筛选标准常规增强解决的是“同一张图翻出更多相似版本”的问题解决不了“某一类本身就只有几张图”的问题。论文第 4.3 节选了 DCGAN 做少数类补充思路是用生成对抗网络批量合成舌象图片把样本量补到一个能训练的量级。DCGAN 的本质是把原始 GAN 里的全连接结构替换成卷积结构生成器从随机噪声一路转置卷积放大到图片尺寸判别器再去判断输入是真图还是生成图。import tensorflow as tf def make_generator(): return tf.keras.Sequential([ tf.keras.layers.Dense(7 * 7 * 256, use_biasFalse, input_shape(100,)), tf.keras.layers.BatchNormalization(), tf.keras.layers.LeakyReLU(), tf.keras.layers.Reshape((7, 7, 256)), tf.keras.layers.Conv2DTranspose(128, 5, strides2, paddingsame, use_biasFalse), tf.keras.layers.BatchNormalization(), tf.keras.layers.LeakyReLU(), tf.keras.layers.Conv2DTranspose(64, 5, strides2, paddingsame, use_biasFalse), tf.keras.layers.BatchNormalization(), tf.keras.layers.LeakyReLU(), tf.keras.layers.Conv2DTranspose(3, 5, strides2, paddingsame, activationtanh) ])这个生成器结构沿用 DCGAN 的经典配置输入 100 维随机噪声先全连接扩到 7×7×256再用三层转置卷积逐步放大到 28×28×3。每层卷积后面接BatchNormalization和LeakyReLU输出层用tanh把像素压到 [-1, 1]训练时真图也要做同样的归一化。生成器和判别器要交替训练常见做法是判别器每训练两步、生成器训练一步防止某一方过早占据优势导致训练失效。生成出来的图片不能直接倒进训练集。我一般会先人工筛一遍有没有明显发糊、有没有形态畸变、风格和真实类别样本是否协调。通过筛选的图建议只占该类别训练集的 20%~30%占比太高会让模型学到生成器特有的伪影泛化能力反而下降。损失曲线要在训练过程中持续观察如果判别器 loss 一路归零、生成器 loss 持续飙升基本就是学习率设置出了问题具体修法在第 5 章的避坑记录里。3. 用卷积神经网络做分类器模型选型、训练脚本与参数落地数据准备完之后就进入训练阶段。论文第 2 章的机器学习理论已经铺垫到了卷积神经网络实际训练时绕不开这几件事网络怎么搭、输入怎么进、训练参数怎么调、过拟合怎么防。这几个问题的优先级应该按数据量来排而不是先纠结用 ResNet 还是 VGG。3.1 网络结构怎么选小而稳的 CNN 优先于大而全的模型舌苔分类这个任务的图像内容本身不算复杂类别数通常在四到五个左右区分的是舌色、苔色、苔质这些整体视觉特征并不需要像 ImageNet 那样用上百层去区分上千个细粒度类别。所以我的倾向是数据量足就从零训练一个紧凑的 CNN数据量少就微调预训练模型。两者各有适用场景但都绕不开一个共同点——输入尺寸和通道顺序必须和推理阶段保持一致。从事件文件events.out.tfevents.*可以判断训练端用的是 TensorFlow 2.x 的 Keras 流程下面按这个环境写模型结构。import tensorflow as tf def build_cnn(input_shape(224, 224, 3), num_classes4): inputs tf.keras.Input(shapeinput_shape) x tf.keras.layers.Conv2D(32, 3, activationrelu, paddingsame)(inputs) x tf.keras.layers.MaxPooling2D()(x) x tf.keras.layers.Conv2D(64, 3, activationrelu, paddingsame)(x) x tf.keras.layers.MaxPooling2D()(x) x tf.keras.layers.Conv2D(128, 3, activationrelu, paddingsame)(x) x tf.keras.layers.GlobalAveragePooling2D()(x) x tf.keras.layers.Dropout(0.5)(x) outputs tf.keras.layers.Dense(num_classes, activationsoftmax)(x) return tf.keras.Model(inputs, outputs)这里用GlobalAveragePooling2D替代Flatten 全连接是刻意为之它把最后一张特征图直接压成向量参数量小很多配合Dropout(0.5)能明显缓解过拟合。两层MaxPooling把特征图从 224×224 降到 56×56 再降到 28×28计算量可控用普通笔记本 CPU 也能一轮接一轮跑下去。如果你拿到的源码里网络更深不用慌只需要确认最后Dense层的节点数等于你的类别数输入层是不是 224×224 即可。3.2 训练脚本与关键参数让 loss 收敛而不是撞运气网络结构定下来之后训练参数的影响力比结构更大。我见过不少同学把网络层数换来换去loss 却始终在 1.0 附近横盘最后发现是学习率给得太激进。下面这段训练配置是中小数据集上比较稳的起点model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), losscategorical_crossentropy, metrics[accuracy] ) callbacks [ tf.keras.callbacks.ModelCheckpoint(model/tongue_best.h5, save_best_onlyTrue), tf.keras.callbacks.ReduceLROnPlateau(patience3, factor0.5), tf.keras.callbacks.EarlyStopping(patience10, restore_best_weightsTrue) ] history model.fit( train_generator, steps_per_epochtrain_generator.samples // 32, validation_dataval_generator, validation_stepsval_generator.samples // 32, epochs50, callbackscallbacks )参数选择的理由整理成一张表方便调整时对照参数建议值理由learning_rate1e-4比 Adam 默认的 1e-3 保守避免 loss 震荡发散batch_size32与flow_from_directory保持一致显存占用适中epochs50配 EarlyStopping到点自动停不空转ReduceLROnPlateau.patience3连续 3 轮验证 loss 不降就减半学习率EarlyStopping.patience10连续 10 轮无改善即停止训练save_best_onlyTrue只保留验证集上最好的权重省磁盘还有一个容易被忽略的匹配关系class_modecategorical时标签会做 one-hotloss 用categorical_crossentropy如果改用class_modesparseloss 就要换成sparse_categorical_crossentropy。两套体系不能混用否则训练时维度报错会让人摸不着头脑。训练完可以用 TensorBoard 看全过程tensorboard --logdir日志目录。事件文件就是这个流程的记录产物曲线里如果训练精度和验证精度差距越拉越大多半是增强不够或 Dropout 不够先回去补数据策略不要急着堆网络层数。4. 把模型装进 GUI从预处理对齐到 Tkinter 逐张检测模型训练完成只是这套源码的一半价值。带 GUI 是这个资源区别于普通分类 demo 的核心点用户打开窗口、选一张舌象图片、程序把识别结果显示出来。这里最容易出的问题不在界面本身而在推理阶段的输入处理和训练阶段不一致。4.1 推理前处理GUI 里最容易翻车的就是 resize 和归一化训练时flow_from_directory会自动把图片 resize 到 224×224再除以 255。到了 GUI 阶段如果用 PIL 读图直接塞给模型十有八九会报维度错误。原因很直接PIL 读出的形状是(width, height, channels)通道顺序是 RGB而模型期望的输入是(batch, 224, 224, 3)如果原图是 RGBA 四通道连通道数量都对不上。import numpy as np from PIL import Image def preprocess_image(image_path, target_size(224, 224)): img Image.open(image_path).convert(RGB) img img.resize(target_size) arr np.array(img, dtypenp.float32) / 255.0 arr np.expand_dims(arr, axis0) return arr这段代码做了四件事强制转 RGB 过滤掉透明通道、resize 到训练尺寸、归一化到 0-1、用expand_dims补上 batch 维。四个步骤缺一不可少一个model.predict都会抱怨输入不对。/ 255.0要和训练时的rescale保持一致如果训练时用的是其他归一化方式这里必须同步修改。如果拿 OpenCV 读图还要注意它默认是 BGR 通道顺序直接喂给按 RGB 训练的模型精度会肉眼可见地掉一截但不报错这种错位最坑人。4.2 Tkinter 主流程选图、预测、回显结果的完整结构GUI 框架我用 Tkinter 做例子因为它是 Python 自带的标准库不需要额外安装依赖这类源码里也最常被用作默认界面实现。界面主流程就三件事放一个按钮让用户选图、选完调用预处理再喂给模型、最后把类别名和置信度显示到界面上。import tkinter as tk from tkinter import filedialog from tensorflow.keras.models import load_model class TongueApp: def __init__(self, model_path): self.model load_model(model_path) self.class_names [白苔, 黄苔, 灰黑苔, 剥苔] self.win tk.Tk() self.win.title(舌苔识别检测鉴定系统) self.pick_btn tk.Button(self.win, text选择舌头图片, commandself.select_and_predict) self.pick_btn.pack(pady10) self.result_label tk.Label(self.win, text尚未识别, font(SimSun, 14)) self.result_label.pack(pady10) def select_and_predict(self): path filedialog.askopenfilename(filetypes[(图片, *.jpg *.png)]) if not path: return arr preprocess_image(path) prob self.model.predict(arr, verbose0)[0] idx int(np.argmax(prob)) self.result_label.config( textf{self.class_names[idx]}置信度 {prob[idx] * 100:.1f}%) def run(self): self.win.mainloop() if __name__ __main__: app TongueApp(model_pathmodel/tongue_best.h5) app.run()load_model会把训练时保存的结构和权重一起读进来所以tongue_best.h5的路径、以及训练时的输入尺寸和类别顺序都不能改。class_names的顺序必须和训练时的class_indices一致否则出现张冠李戴。你可以在训练后执行一次print(train_generator.class_indices)把映射关系打出来再照着写进 GUI。predict默认会刷进度条这里显式传verbose0让界面干净一些。提示选图按钮触发后如果图片较大预测会卡一两秒界面像没响应。稳妥的做法是先把按钮disabled预测完成后再恢复至少给用户一个“正在识别”的状态反馈。5. 常见复现避坑数据、训练和部署三层各自踩到的坑这套源码我前后跑通了不止一次帮别人排查时发现来来回回就是那么几个固定问题。下面五条按现象、原因、解决方式列出来对照自己的报错内容改就行。5.1 坑一测试集精度掺水数据分组不够严现象训练精度和验证精度都很高一拿“没见过”的真实舌象去试表现明显比验证时差一截。原因舌象数据集往往来自少数几个样本来源同一个人的多张图片被随机切分到了训练集和验证集。模型相当于在验证时见过“近似同款”的图片精度自然是虚的。最典型的表现是数据集里有一批连续编号的同源图片随机切分却把前后几张拆到了两边。解决改成按样本来源分组再切分。文件名的前缀能标识来源或病患就先按前缀聚合再执行切分保证同一来源的图片只落在一侧。这一步在数据量只有几百张时尤其关键直接决定论文里那个准确率数字到底真不真。5.2 坑二DCGAN 生成图全是噪点训练判据出了问题现象跑了几百个 batch 后生成器输出的还是一团噪点或者所有生成图都收敛成同一张模糊舌象。原因生成器和判别器的训练不平衡。判别器学得太快把生成器的梯度信号压没了或者学习率用了默认的 1e-3对 DCGAN 来说太激进训练直接震荡发散。解决按 DCGAN 论文经验把learning_rate设为 2e-4Adam 的beta_1从默认 0.9 调到 0.5。训练中不要追求判别器 loss 一路降到 0G 和 D 两条 loss 曲线交替波动才是健康信号。判断结果好不好不要只看数值定期把生成图批量保存下来人工看一眼比任何指标都直观。5.3 坑三GUI 报维度不一致resize 和通道顺序没对齐现象GUI 里model.predict直接抛Input 0 of layer ... is incompatible或者出现形状相关的异常。原因输入图片没转成(1, 224, 224, 3)或者原图带透明通道变成了 4 通道。还有一种是 OpenCV 读图默认 BGR喂给按 RGB 训练的模型颜色通道反了之后不报错但精度悄悄往下掉。解决前处理代码统一写好训练和 GUI 都从同一个函数取数不要各写各的。convert(RGB)、resize((224, 224))、expand_dims三步连在一起缺一步都补上。遇到奇怪的预测结果先检查通道顺序再怀疑其他。5.4 坑四loss 卡住不动学习率与类别权重没调好现象训练十几轮loss 挂在某个值附近不动精度也上不去换网络结构也没用。原因多数情况是类别不均衡模型一直在输出数量最多的那个类别少数情况是学习率不当太大导致震荡太小导致走不动。解决先打印每个类别的样本数给样本少的类别调高class_weight。然后用 1e-4 的学习率重训一轮配上ReduceLROnPlateau让它在验证指标停滞时自动降学习率。如果 loss 依然不动优先怀疑数据质量本身而不是网络结构。5.5 坑五tfevents 日志越攒越大磁盘被训练日志挤爆现象训练一切正常但工程目录里躺着十几个几十甚至几百 MB 的events.out.tfevents.*文件磁盘空间告急。原因每次运行训练都会追加 TensorBoard 事件文件日志清理没做好迭代了多少次就留下多少份历史记录。这个问题在长时间调试的过程中很容易爆发。解决训练脚本里按时间戳建日志目录比如logs/20250320_run只保留最近一两次运行的记录。已经训练完的工程删掉多余的events.out.tfevents.*不影响模型加载也不影响 GUI 运行只是历史曲线没法再回看了所以删除前先确认不再需要复现旧结果。6. 往深走一步从舌苔分类延伸到体质辨识并验证模型的真实泛化如果不想让毕设停在“分出一个类别”这个层面论文第 3 章提到的体质辨识就是个不错的延伸方向。舌苔状态和体质之间有一套经验性的映射关系常见做法是把模型输出的类别再映射成“阳虚倾向、湿热倾向”这类描述。技术上不用改模型改一个字典就行coat_to_constitution { 白苔: 寒湿或阳虚倾向, 黄苔: 湿热或实热倾向, 灰黑苔: 久病或重病倾向, 剥苔: 气阴两虚倾向 }这段映射放在 GUI 里就是模型预测之后的一步拿到class_names[idx]去字典里查对应描述拼到结果显示文本里。需要注意这类判断始终是辅助参考不是诊断结论展示时要突出“辅助决策”的定位。验证模型真实泛化能力我一般会额外做一件事单独收三五十张训练时没有见过的图片用脚本批量推理统计出混淆矩阵。from sklearn.metrics import confusion_matrix cm confusion_matrix(y_true, y_pred) print(cm)输出的矩阵对角线数值高说明每个类别都基本被正确识别如果某一列明显集中说明模型把多个类别都错判成了一个类回头去查数据均衡和预处理对齐。比起只报一个整体准确率这一步测出来的才是模型到实际环境里的底子。从那以后我每次拿到一份“模型加 GUI”的毕设源码都会先写一个三行脚本把训练入口里的预处理参数和推理入口里的预处理参数拉出来对一遍尺寸、归一化、通道顺序全对齐了再打开界面。这个习惯帮我在新项目上省掉了大量排查时间希望帮到你。本文还有配套的精品资源点击获取
返回列表