ARTICLE DETAIL

资讯详情

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

基于深度学习的舌苔识别检测鉴定系统实战:从数据预处理到PyQt5部署

基于深度学习的舌苔识别检测鉴定系统实战:从数据预处理到PyQt5部署 简介面向计算机专业学生与毕业设计者的基于深度学习的舌苔识别检测鉴定系统包含完整源码与 PyQt5 图形界面适用于舌苔图像识别检测、课程设计、期末大作业等场景也适合初次接触深度学习项目的学习者参考项目由个人在导师指导下完成评审分 99 分代码完整可直接运行整体难度适中。压缩包共 109 个文件大小约 105.44MB核心内容包括 26 个 Python 源码文件、6 个模型权重文件、2 个界面设计文件以及多个 TensorFlow 训练事件记录与配置文件并附带 2 份毕业论文文档覆盖从模型训练、界面交互到论文撰写的完整链路。目前已吸引 150 人学习下载。对急需完成相关设计的学生而言这套资料既提供了可复现的识别检测流程也给出了工程化组织示例能有效缩短从环境搭建到功能验证的周期可作为毕业设计或课程设计的重要参考。1. 舌苔识别系统为什么最难的不是深度学习模型而是数据纪律把一张手机拍的舌头照片直接丢进 ResNet大概率会得到一堆离谱的结果——把嘴唇当舌体、把光线阴影当成黄苔、把齿痕当成裂纹。这些年接触过的舌象识别方向项目翻车点几乎全在数据纪律上而不是模型结构不够新。所谓基于深度学习的舌苔识别检测鉴定系统拆开就是三件事先用目标检测或分割的思路把舌体从面部照片里定位出来再用 CNN 对苔色、苔质给出分类判断最后用 PyQt5 把这套推理流程封装成能双击运行的桌面软件并配套一篇结构完整的毕业论文。这套系统在中医学数字化、健康管理自检领域有明确的实用场景也是计算机视觉方向毕业设计里性价比很高的选题。适合已经有 Python 基础、想完整走一遍“数据 → 训练 → 部署 → 论文产出”全流程的同学入手。2. 舌苔数据怎么准备从图像采集到舌体分割的预处理链路2.1 舌象数据的特殊性为什么不能随便拿网图凑数舌象识别和一般图像分类最大的区别在于色彩就是核心特征。中医望舌主要看舌质颜色淡白、淡红、红、绛、紫和舌苔状态白苔、黄苔、灰黑苔、厚薄、腻腐、剥苔这些属性高度依赖颜色还原度。随便从网上扒图不同相机的白平衡、滤镜和美颜处理会把色相完全带偏模型学到的可能是某种手机的渲染风格而不是真实的舌象规律。常见的数据来源有三条路公开的舌象图像数据集、与中医学院或医院合作采集、自采后请专业人士标注。公开数据集数量少且标注口径不统一合作采集周期长自采数据质量参差不齐。毕设阶段比较务实的组合是公开数据集打底自己补拍 100 到 200 张统一用手机在自然光或标准光源下拍摄拍摄时叮嘱受试者伸出舌头时保持自然放松、不要用力卷曲。数量上几百张能跑通全流程想要分类结果稳定至少需要每类 200 张以上。标注规范是另一个容易忽略的环节。舌象是典型的多标签数据同一张舌图可能同时是“黄苔”和“厚腻苔”。在做毕业设计时建议先不要追求完整的中医辨证标签体系而是把问题简化为主任务苔色分类淡白/白/黄/灰黑加一个厚薄二分类。把多标签问题拆成单标签任务训练稳定性和验证指标都好解释。这里有个血泪经验一开始我做了五个属性的多任务输出结果每个任务的样本都稀释到不到一百张验证集波动大得没法看论文里也没法讲清楚。数据项建议标注方式说明舌体位置矩形框检测任务或像素级掩膜分割任务毕设用矩形框够用分割更适合论文加分项苔色单标签淡白苔 / 白苔 / 黄苔 / 灰黑苔灰黑苔样本少时并入“其他”类避免不均衡厚薄二分类薄苔 / 厚苔以舌体中部区域为判断标准腻腐可选建议作为扩展任务样本不足时不要硬做2.2 白平衡与舌体粗定位预处理脚本怎么写拿到原始图片后第一步不是直接 resize 送进网络而是做白平衡校正。常见的做法是灰度世界假设认为一幅正常色彩图像的三通道均值应该趋于一致把偏色拉回来。舌象图片受环境光影响极大这一步能显著降低不同拍摄条件下的色差干扰。实用代码里还要加一个细节增益截断防止过曝或过暗的样本被拉爆。除了白平衡舌体定位也很关键。虽然标题里有“检测”但毕设阶段的检测不一定要上目标检测网络先用传统图像处理把舌头从面部区域里粗切割出来既快又稳还能给后面的分类模型省去大量背景噪声。以下脚本是常用的预处理组合。# preprocess.py -- 舌象预处理灰度世界白平衡 HSV 肤色分割定位舌体 import cv2 import numpy as np def gray_world_whitebalance(img): 灰度世界假设白平衡把RGB三通道均值拉平消除环境光偏色 b, g, r cv2.split(img) mean_b, mean_g, mean_r np.mean(b), np.mean(g), np.mean(r) gray (mean_b mean_g mean_r) / 3.0 scale_b, scale_g, scale_r gray / mean_b, gray / mean_g, gray / mean_r # 增益截断在0.5~2.0之间防止极端亮度样本颜色失真 for scale, ch in zip((scale_b, scale_g, scale_r), (b, g, r)): scale np.clip(scale, 0.5, 2.0) ch[:] np.clip(ch * scale, 0, 255) return cv2.merge([b, g, r]).astype(np.uint8) def locate_tongue(img): HSV肤色分割 最大连通域返回舌体区域裁剪结果 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 肤色在HSV中的H分量横跨0~25和170~180两段S下限51过滤低饱和背景 mask cv2.inRange(hsv, (0, 51, 60), (25, 255, 255)) \ cv2.inRange(hsv, (170, 51, 60), (180, 255, 255)) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 去掉细小噪点 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 补上舌体内部空洞 num, labels, stats, _ cv2.connectedComponentsWithStats(mask) if num 2: # 只有背景说明分割失败 return img, None # 取面积最大的连通域通常是舌头但也可能是嘴唇需后续人工检查 largest 1 np.argmax(stats[1:, cv2.CC_STAT_AREA]) x, y, w, h (stats[largest, cv2.CC_STAT_LEFT], stats[largest, cv2.CC_STAT_TOP], stats[largest, cv2.CC_STAT_WIDTH], stats[largest, cv2.CC_STAT_HEIGHT]) return img[y:yh, x:xw], (x, y, w, h)这段代码里最值得调的是 HSV 阈值。(0, 51, 60)中 V 下限定为 60是假设拍摄环境有一定亮度如果图片偏暗分割出来的连通域会很小甚至为空。S 下限 51 是为了过滤灰暗背景但遇到皮肤偏黄或被黄苔染色的舌面时也可能把舌面切成碎片。一个实用技巧是分割后对面积最大的连通域做一次面积占比检查如果框出的区域占了整张图 80% 以上大概率是把整张脸框进来了此时回退使用原图中心裁剪。另外要说明的是这套肤色分割假设舌体和面部肤色接近对淡红舌、正常唇色是有效的。遇到极深的灰黑苔或紫暗舌H 通道会明显偏移分割结果不可靠。保险做法是把分割后的裁剪框可视化保存一批人工抽检后再进训练集。2.3 训练集构建Dataset 类和标签编码的细节预处理脚本只是第一步真正决定模型上限的是 Dataset 的写法。舌象分类里常见的一个隐形坑是ColorJitter的饱和度抖动参数不能像做 ImageNet 分类那样随意设置。舌苔的黄色和白色本身就是靠色相与饱和度区分的饱和度抖动过大等于在训练时把两个类别搅浑。我自己试过saturation0.5训练集准确率很高验证集直接崩到三分之一这就是颜色语义被增强搞坏的典型案例。训练集路径管理上用一个label.txt文件统一管理比文件夹命名更灵活尤其后续要调类别比例时不用移动文件。以下是一个适配预处理链路的 Dataset 类写法。# dataset.py -- 舌象分类Dataset复用上面的白平衡与定位函数 import os import cv2 import torch from torch.utils.data import Dataset from torchvision import transforms class TongueDataset(Dataset): def __init__(self, root, label_file, modetrain): self.samples [] with open(label_file, r, encodingutf-8) as f: for line in f: path, label line.strip().split(,) self.samples.append((os.path.join(root, path), int(label))) tf [] if mode train: tf [ transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.15, saturation0.05, hue0.02), transforms.RandomRotation(10), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), ] else: tf [transforms.Resize(256), transforms.CenterCrop(224)] tf [transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])] self.tf transforms.Compose(tf) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img cv2.imread(path) if img is None: return self.__getitem__((idx 1) % len(self.samples)) img gray_world_whitebalance(img) img, box locate_tongue(img) if box is None: # 分割失败就退回中心区域不让样本白白流失 h, w img.shape[:2] img img[h//4:3*h//4, w//4:3*w//4] img cv2.resize(img, (224, 224)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) return self.tf(img), torch.tensor(label, dtypetorch.long)标签文件格式是每行图片路径,类别ID类别 ID 从 0 开始连续编码。归一化参数用的是 ImageNet 的统计值因为接下来要做迁移学习这样能在分布上对齐预训练模型的数据空间。RandomResizedCrop(224, scale(0.8, 1.0))的 scale 下限定为 0.8是防止裁剪区域太小把舌苔核心区切掉如果做目标检测这里还可以改成按 bounding box 做 jitter但毕设阶段没必要上这个复杂度。3. 模型选型与训练用 PyTorch 把舌苔分类做扎实3.1 选型思路检测在前、鉴定在后骨干网络用 ResNet18 就够了“舌苔识别检测鉴定系统”这个标题拆开看其实是两个任务检测负责回答“舌头在哪”鉴定负责回答“舌苔是什么状态”。检测部分如果上 YOLO 或者 SSD需要标注框的数据量小样本场景很容易训练不足。务实的选择是检测用传统图像处理上一章的肤色分割也就是“检测在前”的角色鉴定用深度学习分类网络也就是真正的核心算法。分类模型选择上ResNet18 是当前小样本医学图像任务里性价比最高的骨干网络。它比 VGG16 参数量小一个数量级推理速度快在 PyQt5 桌面端 CPU 上也能跑到几十毫秒一张。Swin Transformer 这类新架构在小数据集上很容易过拟合而且显存占用高部署时还依赖高版本 PyTorch对毕设性价比不高。MobileNetV3 可以作为备选如果你的机器没有 GPU或者你想强调“轻量化部署”这个论文卖点就用 MobileNetV3-Large。这里给出一段迁移学习构建模型的代码。# model.py -- 构建ResNet18替换分类头并支持冻结骨干 import torch.nn as nn from torchvision import models def build_model(num_classes4, freezeTrue, dropout0.3): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(dropout), nn.Linear(in_features, 128), nn.ReLU(), nn.Linear(128, num_classes), ) if freeze: # 冻结骨干网络只训练分类头适合数据量少的第一阶段 for p in model.parameters(): p.requires_grad False for p in model.fc.parameters(): p.requires_grad True return modelfreezeTrue是迁移学习的第一阶段骨干网络保留 ImageNet 上学到的纹理和边缘特征只训练分类头。这样做的原因是舌象图片虽然语义特殊但底层特征和自然图像是共享的冻结骨干能大幅减少过拟合风险。第二阶段再解冻骨干做全局微调对应论文里的“两阶段训练策略”这也正好是毕业论文里可以重点写的实验设计。3.2 训练循环分层学习率、早停和类别权重训练部分有一个初学者最容易忽略的点loss 函数的类别权重。舌象数据集天然不均衡正常淡红舌薄白苔占了大头灰黑苔、紫暗舌等病理样本稀少。如果直接用CrossEntropyLoss()模型学到的就是把所有样本都判成多数类准确率可能高达 80%但宏平均 F1 惨不忍睹。解决方法是统计每类样本数给 loss 加权重少数类错判的惩罚更大。早停是另一个必须的配置。舌象小数据集上训练 15 到 20 个 epoch 后验证集指标往往开始抖动继续训练只会过拟合到训练集的光线特征上。监控验证集 macro-F1连续 N 个 epoch 不改善就回滚最佳权重。下面是一段核心训练逻辑。# train.py -- 两阶段训练先用冻结骨干热身再解冻微调 import torch import torch.nn as nn import numpy as np from torch.utils.data import DataLoader, WeightedRandomSampler def build_sampler(dataset): 按类别频率构造采样权重少数类被抽中的概率更高 labels [s[1] for s in dataset.samples] class_counts np.bincount(labels) weights 1.0 / class_counts sample_weights [weights[label] for label in labels] return WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights)) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total images.size(0) return total_loss / total, correct / total def train_stage(model, dataset, stage_epochs, lr, device): sampler build_sampler(dataset) # 用重采样处理类别不均衡 loader DataLoader(dataset, batch_size16, samplersampler, num_workers2) criterion nn.CrossEntropyLoss() # 只更新requires_gradTrue的参数冻结的部分不受影响 optimizer torch.optim.AdamW( [p for p in model.parameters() if p.requires_grad], lrlr, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxstage_epochs) for epoch in range(stage_epochs): loss, acc train_one_epoch(model, loader, criterion, optimizer, device) scheduler.step() print(fepoch{epoch1} loss{loss:.4f} acc{acc:.4f})参数设置上第一阶段冻结骨干时lr1e-3给分类头用训练 15 个 epoch第二阶段解冻全部参数后lr降到1e-4再训练 20 个 epoch。batch_size16是综合考虑舌象图经过预处理后尺寸是 22416 张一批在 6GB 显存的显卡上刚好跑得动显存小就降到 8。weight_decay1e-4做 L2 正则医学小数据集上别省这一步。这一段代码里没有直接写早停逻辑实际使用时在每轮计算验证集 macro-F1 后做保存。# 验证与早停接上面的训练代码 def evaluate(model, dataset, device): model.eval() loader DataLoader(dataset, batch_size16, shuffleFalse) preds, trues [], [] with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) preds.extend(outputs.argmax(1).cpu().numpy()) trues.extend(labels.cpu().numpy()) return compute_macro_f1(trues, preds) # 自行实现或用sklearn.metrics.f1_score3.3 评估指标的陷阱准确率高不代表系统可用很多学生在答辩时说“模型准确率 92%”评委一问每类分别多少就露馅了。舌象数据的类别分布天然倾斜正常舌象样本多病理舌象样本少。准确率只反映整体判断正确的比例一个全判成“正常薄白苔”的模型也能拿到很高的准确率。论文和毕设答辩里真正有用的是三个指标的组合宏平均精确率、宏平均召回率、宏平均 F1。宏平均 F1 把每个类别视作同等重要少数类的表现好坏会直接反映出来。同时在报告里给出混淆矩阵用 matplotlib 画出来贴进论文这是答辩时最直观的展示材料。如果条件允许还可以按性别、年龄段分组统计准确率这种细粒度分析在医药类毕设中很受导师欢迎。“玄学”之处在于舌象的某些错误分类在医生看来是不可接受的。比如把灰黑苔误判成白苔可能掩盖了危重病情。因此在损失函数设计上除了类别权重还可以引入一个代价矩阵相邻类别判错代价小跨色系判错代价大。不过毕设阶段先把类别权重和重采样做好就足够支撑一篇合格论文了。4. PyQt5 界面整合把模型推理变成可交付的桌面应用4.1 主界面布局三大区域把用户操作路径理顺PyQt5 界面设计要解决的核心问题不是好看而是让用户的操作路径最短打开图片 → 看到结果 → 看到依据。我一般把主窗口分成三个区域左侧是原图显示与舌体定位框右侧是推理结果面板展示苔色分类、厚薄判断和置信度底部是操作栏放“打开图片”“开始识别”“保存报告”三个按钮。这样布局的好处是用户从左到右、从上到下自然浏览符合桌面软件操作习惯。界面实现上用QMainWindow做容器中间放一个QWidget整体用QHBoxLayout和QVBoxLayout嵌套。图片显示用QLabel结果展示用QTextBrowser它天然支持富文本后续想在结果里加上不同颜色的置信度文字很方便。有一个细节QLabel显示图片时要用setScaledContents(True)配合setMinimumSize否则图片或大或小在不同分辨率屏幕上很难看。PyQt5 在高分屏和 Linux 下容易出现字体模糊或控件错位老规矩是入口处声明高 DPI 缩放策略这写在main.py里。# main.py -- 程序入口先设高DPI策略再创建QApplication import sys from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication from main_window import MainWindow if __name__ __main__: # 高DPI缩放必须在QApplication创建之前设置否则不生效 QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec_())4.2 模型推理放进 QThread界面卡死问题从根上解决PyQt5 集成深度学习模型最常见的翻车现场是点击“开始识别”按钮界面直接白屏标题栏出现“未响应”过几十秒才恢复。原因很简单PyTorch 推理是同步阻塞的模型加载权重、预处理图像、前向计算这些操作全部在 GUI 主线程里执行用户界面自然卡死。解决办法是把推理逻辑丢进QThread在子线程里算完再通过信号把结果传回主线程更新界面。跨线程更新 UI 是 PyQt5 里最容易出错的地方记住一条铁律子线程里永远不要直接操作控件只能发信号。下面是一个经过验证的 Worker 写法。# infer_worker.py -- 模型推理线程与GUI解耦 from PyQt5.QtCore import QThread, pyqtSignal import torch import cv2 class InferWorker(QThread): result_ready pyqtSignal(dict) # 推理完成信号携带结果字典 error_occurred pyqtSignal(str) # 异常信号携带错误信息 def __init__(self, model, device, img_path, preprocess_fn, parentNone): super().__init__(parent) self.model model self.device device self.img_path img_path self.preprocess_fn preprocess_fn def run(self): try: self.model.eval() img cv2.imread(self.img_path) if img is None: self.error_occurred.emit(无法读取图片请检查文件格式) return tensor, roi self.preprocess_fn(img) # 复用第二章的预处理 with torch.no_grad(): logits self.model(tensor.unsqueeze(0).to(self.device)) probs torch.softmax(logits, dim1).squeeze(0) conf, idx torch.max(probs, dim0) self.result_ready.emit({ class_id: int(idx.item()), confidence: float(conf.item()), roi: roi, }) except Exception as e: self.error_occurred.emit(str(e))主窗口里使用这个 Worker 时先实例化、连接信号、调用start()启动。模型加载要放在窗口初始化时只做一次千万别放进run()里每张图都重新加载权重否则单张推理时间会从 50 毫秒变成 3 秒。模型文件用torch.jit.trace或torch.save导出后推理时直接加载即可。界面收到result_ready信号后更新右侧的QTextBrowser。置信度展示上做一些可视化处理高于 0.8 显示“可信度较高”0.5 到 0.8 显示“建议复核”低于 0.5 直接提示“样本质量差请重新拍摄”。这种设计在论文里可以写成“人机协同的置信度分级机制”比单纯报一个数好讲得多。4.3 打包成 exePyInstaller 的路径与资源坑PyQt5 应用打包用 PyInstaller 是事实标准但每次打包舌苔识别系统都能遇到新问题最常见的是两个模型文件路径找不到和 UI 资源加载失败。开发时模型路径写的是绝对路径打包后换一台电脑自然找不到。正确的做法是判断是否处于打包环境用sys._MEIPASS定位解压目录。另一个坑是 PyQt5 自带的插件目录platforms没被打进去。虽然 PyInstaller 通常会识别 PyQt5 的依赖但个别版本会漏掉 xcb 或 windows 平台插件导致双击 exe 提示 “could not load the Qt platform plugin”。常规处理是执行打包命令后打开 dist 目录确认存在PyQt5/Qt5/plugins的对应插件如果缺失就手动复制进_internal目录。以下是常用打包命令。# 打包命令注意--add-data把模型和标签文件一起打进去 pyinstaller --noconfirm \ --windowed \ --name TongueAnalysis \ --add-data models/best_model.pth;models \ --add-data labels.txt;. \ --hidden-import PyQt5.sip \ main.py--windowed表示不显示控制台窗口适合交付给非技术用户--add-data的分隔符在 Windows 上是分号、Linux 和 macOS 上是冒号跨平台打包时要留意。用--hidden-import PyQt5.sip是防止 PyQt5 的 sip 模块在打包时被漏掉这是历史遗留问题新版 PyInstaller 一般不需要但加上无伤大雅。打包后的体积通常在 150MB 到 300MB主要被 PyTorch 的 CUDA 库吃掉。如果只做 CPU 推理可以在安装 torch 时选择 CPU 版本打包体积能明显减半。另外要提前告诉使用者首次启动较慢因为 PyInstaller 解压资源需要时间这不是程序卡死是正常现象。5. 舌苔识别开发避坑与排查从训练翻车到界面卡死的 5 个实战案例5.1 案例一loss 在下降但验证集 F1 卡在 0.3还以为是模型不行现象训练集 loss 从 2.3 平滑降到 0.8训练准确率超过 90%但验证集宏平均 F1 始终在 0.3 左右徘徊。原因两层叠加的问题。第一层是类别不均衡正常舌象样本占比过高模型把稀缺类别全部判错。第二层是标签耦合黄苔和厚腻苔在真实数据里高度相关互斥的 one-hot 编码强迫模型在这两个属性间做非此即彼的决策。解决先看混淆矩阵确认少数类是否被整体吞掉然后给CrossEntropyLoss加类别权重或用WeightedRandomSampler重采样最后把苔色和厚薄拆成两个独立的分类输出各自算 loss 再加权求和减轻标签耦合带来的梯度冲突。5.2 案例二部署到界面上预测结果和训练时不一致同一张图每次结果还不同现象训练脚本里跑验证集结果正常同一个模型搬到 PyQt5 界面里预测结果对不上甚至同一张图片连续点两次识别预测标签不一样。原因推理时忘了把模型切到评估模式。模型里带 BatchNorm 和 Dropout默认处于训练模式BatchNorm 会用到当前 batch 的统计量单张推理时统计量剧烈抖动Dropout 随机丢弃神经元结果自然不稳定。解决在run()里、forward之前调用self.model.eval()并把这句放第一行然后用with torch.no_grad():包住前向计算。这属于开发流程规范问题不在数据处理层面写代码时养成“模型加载后立刻 eval”的习惯就能根治。5.3 案例三PyQt5 点击“开始识别”后窗口白屏、标题栏显示未响应现象按钮按下后界面冻结十几秒到几十秒后才恢复期间拖动窗口无响应。原因模型推理在 GUI 主线程执行。PyTorch 加载模型权重、预处理图片、前向计算都是阻塞操作主线程被占用窗口消息循环停止响应。解决把推理逻辑移入QThread见 4.2 的InferWorker推理结束后通过pyqtSignal把结果传回主线程。另一个隐蔽点预处理阶段如果用了较慢的cv2.connectedComponentsWithStats也要放进子线程否则图片大时同样卡界面。可以把信号设计成progress pyqtSignal(str)在预处理、推理、保存报告等阶段各发一次文本进度让用户知道程序在跑什么。5.4 案例四肤色分割把嘴唇和舌头一起框进来分类结果被唇色误导现象舌体定位框偏大把下嘴唇包含进来模型对唇色偏红的样本频繁判成红舌对涂了口红的样本直接崩溃。原因HSV 肤色分割把嘴唇和舌面识别为同一肤色连通域开口操作后仍连成一片另外训练样本里舌头大概率出现在画面中央模型学到的是“中间是舌头”的位置先验框一偏就误判。解决在分割后做几何判定——舌体连通域的宽高比通常在 0.6 到 1.6 之间超出范围说明可能混入嘴唇或下巴回退为原图中心区域裁剪训练阶段对舌体位置做随机平移增强最高平移幅度 10%迫使模型学习形状特征而非位置特征。更彻底的方法是用轻量分割网络如 U-Net 浅层版输出舌体 mask但需要像素级标注数据毕设阶段按需引入。5.5 案例五PyInstaller 打包后双击运行闪退或在别人电脑上报错提示找不到模型现象开发环境一切正常打包后用--windowed运行双击闪退在命令行手动运行时看到FileNotFoundError: models/best_model.pth。原因模型路径写死成了开发机的绝对路径--add-data只解决了文件打包没有解决运行时路径解析。PyInstaller 打包的程序运行时会把资源解压到临时目录这个目录路径由sys._MEIPASS提供开发环境里根本不存在。解决统一用工具函数解析资源路径打包前先打印sys._MEIPASS确认目录结构再决定代码里的相对路径写法。示例如下。# utils.py -- 兼容开发环境和PyInstaller打包环境的路径解析 import sys import os def resource_path(relative_path): 开发时返回相对路径打包后返回解压目录下的实际路径 base_path getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base_path, relative_path) # 使用方式加载模型和标签都走resource_path model_path resource_path(models/best_model.pth) label_file resource_path(labels.txt)6. 用 Grad-CAM 热力图给论文加分一行 hook 拿到可视化解释答辩时评委最常问的问题就是“你的模型凭什么这么判断”。对舌象识别这类医学影像任务光报准确率是不够的热力图是目前最直观、最好讲的可视化解释方案。Grad-CAM 的思路是把网络最后一个卷积层的输出特征图按梯度加权求和得到类别的空间注意力分布再上采样回输入尺寸就能看出模型到底盯着图片的哪个区域做决策。在舌苔识别场景中理想的热力图应该集中在舌体中后部与舌根区域的舌苔分布区而不是嘴唇或背景。实现 Grad-CAM 不需要魔改模型结构用 PyTorch 的 hook 机制就能拿到中间层的激活值和梯度。以下是一段兼容 ResNet18 的完整实现。# gradcam.py -- 基于forward hook与backward hook的Grad-CAM可视化 import cv2 import numpy as np import torch class GradCAM: def __init__(self, model, target_layer): self.model model self.activations {} self.gradients {} # forward hook获取目标卷积层的输出特征图 target_layer.register_forward_hook(self.save_activation) # backward hook获取该特征图的梯度 target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations[value] output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients[value] grad_output[0].detach() def generate(self, input_tensor, class_idx): output self.model(input_tensor) if class_idx is None: class_idx output.argmax(dim1).item() score output[0, class_idx] self.model.zero_grad() score.backward() activations self.activations[value] gradients self.gradients[value] # 对特征图每个通道求平均梯度作为该通道的注意力权重 weights torch.mean(gradients, dim(2, 3), keepdimTrue) cam torch.relu(activations * weights).sum(dim1, keepdimTrue) cam cam.squeeze().cpu().numpy() cam cv2.resize(cam, (input_tensor.size(3), input_tensor.size(2))) cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam staticmethod def overlay(cam, img_bgr, alpha0.5): heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) return cv2.addWeighted(img_bgr, 1 - alpha, heatmap, alpha, 0)使用这套工具时直接用model.layer4[-1]作为target_layerResNet18 的最后一层卷积输出是 7×7 空间分辨率上采样到 224×224 后热力图粒度还算细腻。class 不指定时就取模型预测的类别答辩时可以做成“预测哪一类就高亮哪一类对应的区域”的对照实验。写论文的时候把每一类苔色挑 2 到 3 张代表图原图、定位框图、热力图三列并排展示每张图下面写一句“模型关注区域主要位于舌中部与根部提示该决策依赖舌苔覆盖区域而非舌体边缘”。我习惯每次训练完先跑一遍全部验证集的热力图抽看 20 张再谈准确率。这个流程相当于一个快速体检成了改不掉的习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表