资讯详情

资讯详情

舌苔识别系统实战:PyQt5+ResNet-18构建可部署中医舌诊工具

简介本资源是一套面向高校计算机与医学信息交叉方向本科生的毕业设计级舌苔智能识别系统聚焦中医舌诊数字化落地场景提供从算法建模到GUI交互的完整技术闭环。压缩包共110个文件含26个Python核心源码含PyQt5界面、训练/推理脚本、6个预训练.pth模型、47个数据标注与日志文件含TensorBoard events日志、10个JSON配置及UI界面文件辅以毕业论文docx、舌象增强图像jpg/png及字体资源ttc整体容量104.93MB。已有536人学习下载资源结构清晰涵盖数据集构建含DCGAN生成对抗扩充、CNN舌苔检测网络设计、体质辨识逻辑实现及完整GUI工程部署论文章节详实覆盖需求分析、理论基础与实验验证可直接用于课程设计复现、毕设答辩支撑或AI中医方向入门实践。1. 舌苔识别不是“拍张照就出结果”一个毕业级系统为什么必须同时搞定模型、界面、数据闭环和医学可解释性你手头有一份标着“基于深度学习的舌苔识别检测鉴定系统”的压缩包解压后看到model/ui/dataset/report/四个文件夹——这不像一个玩具Demo而是一个能跑通“拍照→预处理→舌体分割→苔色/厚薄/润燥分类→中医证型映射→PDF报告生成”全链路的工程实体。它用的是 PyTorch不是 TensorFlow主干是轻量级 ResNet-18 改进版不是 ViT界面用 PyQt5 实现本地双窗口左窗实时摄像头流右窗显示舌苔区域热力图证型置信度条文字建议。最关键是它没用 ImageNet 预训练权重直接迁移而是用 327 张临床采集的舌象图含舌体掩膜、苔色标注、厚薄分级、润燥标签做了四阶段微调先做舌体粗定位U-Net再做舌面精细分割Mask R-CNN 微调接着在分割区域内提取 ROI 做多任务分类苔色/厚薄/润燥三头并行最后用规则引擎把分类结果映射到《中医诊断学》标准证型如“舌红苔黄厚腻”→“痰热壅肺证”。这不是纯算法竞赛题而是要让实习医生能在门诊电脑上点开.exe就用——所以 PyQt5 界面必须屏蔽掉所有命令行依赖、模型加载失败时自动降级为灰度直方图分析、导出报告带医院抬头和医师电子签名栏。如果你正卡在“模型准确率 92% 但部署后识别错乱”“PyQt5 打包后摄像头打不开”“舌苔标注不一致导致训练震荡”这篇笔记就是为你写的血泪复现指南。2. 从原始舌象图到可训练数据集标注规范、增强策略与四阶段数据流设计舌苔识别的数据质量直接决定模型上线后会不会把“淡红舌薄白苔”误判成“淡白舌薄白苔”——差一个证型用药方向就全反。我见过太多同学用手机随便拍几十张舌图就开始训练结果模型学到的不是苔质特征而是“手机闪光灯角度”和“口腔镜反光斑点”。下面这套数据准备流程是我带三届毕设学生踩坑后固化下来的最小可行方案。2.1 标注必须遵循《中医舌诊图像采集规范试行》三级标准不要用 LabelMe 或 CVAT 直接画多边形舌体分割、苔色区域、厚薄分区、润燥标记必须分层独立标注且每张图强制要求四个 JSON 文件文件名标注内容格式要求医学意义xxx_mask.json舌体完整轮廓含齿痕、裂纹COCO 格式 segmentation 字段polygon 点数 ≥ 120排除嘴唇、牙齿干扰保证 ROI 纯净xxx_color.json苔色主区域避开舌乳头、瘀点多边形框 HSV 色相均值区间[H_min, H_max]区分“淡黄”H30±5与“深黄”H20±3xxx_thickness.json厚薄分区线舌中线两侧各3段LineString 坐标序列标注“薄”“中”“厚”三类“厚苔”需覆盖舌面 ≥2/3“薄苔”仅限舌尖1/3xxx_moisture.json润燥标记点舌面随机采样9点Point 坐标 dry/moist/slippery标签避免主观判断用像素梯度变化率辅助校验提示xxx_mask.json必须用专业舌诊仪拍摄图生成非手机图我们用的是某三甲医院合作提供的 327 张原始图已剔除反光过强、唾液覆盖、光照不均样本。若你自采请严格按“自然光白平衡校准卡固定距离15cm无滤镜”执行否则标注再准也白搭。2.2 数据增强不是加高斯噪声那么简单针对舌象特性的四维扰动普通图像增强会破坏舌苔的病理纹理。我们只保留以下四种可控扰动并写死参数范围代码里不能调# data_augmentation.py import albumentations as A train_transform A.Compose([ # 维度1光照鲁棒性模拟诊室不同光源 A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.7), # 维度2舌体形变应对患者伸舌角度差异 A.ElasticTransform(alpha12, sigma4, alpha_affine6, p0.5), # 维度3苔质保真禁止模糊/锐化破坏苔颗粒感 A.OneOf([ A.MotionBlur(blur_limit3, p0.5), A.MedianBlur(blur_limit3, p0.5) ], p0.3), # 维度4色彩恒常性对抗白平衡偏差 A.HueSaturationValue(hue_shift_limit10, sat_shift_limit15, val_shift_limit10, p0.8), ], keypoint_paramsA.KeypointParams(formatxy, remove_invisibleFalse))关键参数说明ElasticTransform的alpha12是经验值小于10无法模拟舌体卷曲大于15会导致苔色区域扭曲失真HueSaturationValue的hue_shift_limit10严格限制在中医舌色谱系内HSV 色相环中白苔 H≈0°淡黄 H≈30°焦黄 H≈20°灰黑 H≈250°所有增强必须作用于mask和keypoints同步否则分割与分类任务对不上——这是后续模型崩塌的根源之一。2.3 四阶段数据流为什么必须拆成舌体→舌面→苔区→证型四级 pipeline直接端到端训练“舌图→证型”会因任务耦合导致梯度爆炸。我们强制拆解为阶段输入输出损失函数典型错误Stage1: 舌体定位原图舌体二值掩膜Dice Loss把嘴唇当舌体需加边缘约束 lossStage2: 舌面分割原图Stage1掩膜精细舌面掩膜含齿痕Focal Loss Boundary Loss齿痕断裂Boundary Loss 权重设为0.3Stage3: 苔质分类Stage2掩膜裁剪图[苔色, 厚薄, 润燥] 三元组Weighted CE厚薄类别权重 1.2“厚苔”样本少导致欠拟合SMOTE 过采样Stage4: 证型映射Stage3输出规则库中医证型概率分布KL Divergence对齐专家标注规则冲突如“舌红苔黄厚”应属“实热”但模型输出“虚热”这个设计让每个阶段可单独调试Stage1 准确率 95% 就停训Stage2 的 Boundary Loss 0.15 就检查标注质量Stage3 的厚薄分类 F1 0.82 就重采样——避免一锅炖导致问题溯源困难。3. 模型结构不是堆叠层数轻量级 ResNet-18 改进版如何兼顾精度与部署速度毕业设计不是 Kaggle 竞赛你的模型要跑在 i5-8250U GTX1050 笔记本上还要保证 PyQt5 界面不卡顿。我们放弃 ViT、EfficientNet 等大模型选择 ResNet-18 作为基座但做了三个关键手术替换 stem 卷积、插入通道注意力、解耦分类头。最终在 327 张图上达到 89.3% 平均 F1厚薄任务 91.2%润燥任务 87.5%苔色任务 88.7%推理耗时 42ms/帧RTX3060。3.1 Stem 卷积改造用 3×3 替代 7×7解决舌象高频细节丢失原始 ResNet-18 的第一层是7×7, stride2卷积会抹平舌苔的丝状纹理。我们改成# model/resnet_tongue.py class TongueResNet18(nn.Module): def __init__(self, num_classes_color4, num_classes_thickness3, num_classes_moisture3): super().__init__() # 替换原始 stem7x7 → 3x3 BatchNorm ReLU self.conv1 nn.Conv2d(3, 64, kernel_size3, stride1, padding1, biasFalse) # 关键padding1 保尺寸 self.bn1 nn.BatchNorm2d(64) self.relu nn.ReLU(inplaceTrue) self.maxpool nn.MaxPool2d(kernel_size3, stride2, padding1) # 保持感受野等效 # 后续 layer 完全复用官方 ResNet-18 结构layer1-layer4 self.layer1 self._make_layer(BasicBlock, 64, 2) self.layer2 self._make_layer(BasicBlock, 128, 2, stride2) self.layer3 self._make_layer(BasicBlock, 256, 2, stride2) self.layer4 self._make_layer(BasicBlock, 512, 2, stride2) # 插入 CBAM 注意力模块仅在 layer4 后 self.cbam CBAM(512, reduction_ratio16) # 解耦分类头避免任务干扰 self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc_color nn.Sequential( nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes_color) ) self.fc_thickness nn.Sequential( nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes_thickness) ) self.fc_moisture nn.Sequential( nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes_moisture) )为什么这样改kernel_size3, stride1, padding1让第一层不丢分辨率舌乳头纹理在 feature map 上清晰可见CBAM模块Convolutional Block Attention Module比 SE-Net 更适合舌象它同时建模通道重要性和空间位置能自动聚焦苔色区域而非舌根阴影三个独立 FC 头避免“厚薄预测被苔色梯度带偏”实测多任务联合训练时各任务 F1 波动 1.5%而共享头波动达 ±4.7%。3.2 训练策略冻结 backbone 分阶段 unfreeze 的实操节奏别一上来就model.train()我们采用三阶段训练法每阶段监控验证集 loss plateau阶段冻结层学习率Epochs监控指标早停条件Phase1layer4 及之后全部冻结1e-330Stage3 总 loss连续5轮 loss 不降Phase2解冻 layer3-layer45e-420厚薄任务 F1F1 提升 0.5%Phase3全网络微调1e-415证型映射 KL 散度KL 0.12# train.py 关键片段 def train_phase(model, dataloader, optimizer, phase): if phase 1: for param in model.layer3.parameters(): param.requires_grad False for param in model.layer4.parameters(): param.requires_grad False for param in model.cbam.parameters(): param.requires_grad False for param in model.fc_*.parameters(): param.requires_grad True # * 表示 color/thickness/moisture elif phase 2: for param in model.layer3.parameters(): param.requires_grad True for param in model.layer4.parameters(): param.requires_grad True # cbam 仍冻结防止过拟合小数据 # ... 其他逻辑血泪经验Phase1 必须用torch.optim.AdamW不是 SGD因为小数据下 AdamW 的 weight decay 能抑制过拟合Phase3 的 KL 散度目标值 0.12 是反复测试得出的——低于此值证型输出过于平滑所有证型概率接近 0.33高于此值则规则引擎无法收敛。3.3 模型导出为 TorchScript为什么 ONNX 在 PyQt5 里会崩溃很多同学导出 ONNX 后在 PyQt5 里ort.InferenceSession报错InvalidArgument: Input is not a vector根本原因是 ONNX 对动态 batch size 支持差而 PyQt5 摄像头 feed 是单帧。我们强制用 TorchScript# export_model.py model.eval() # 构造 dummy input1x3x224x224符合训练时 resize 尺寸 dummy_input torch.randn(1, 3, 224, 224) # 导出为 TorchScript非 trace用 script 保留 control flow traced_script_module torch.jit.script(model) traced_script_module.save(model/tongue_resnet18.pt) # 在 PyQt5 中加载注意 device self.model torch.jit.load(model/tongue_resnet18.pt) self.model.to(cuda if torch.cuda.is_available() else cpu) self.model.eval()关键点必须用torch.jit.script不是trace因为我们的模型里有if判断如 CBAM 的 channel/space attention switchdummy_input尺寸必须与训练时transforms.Resize(224)一致否则 runtime 报错PyQt5 加载时显式指定 device否则默认 CPU 推理慢 8 倍实测 320ms vs 42ms。4. PyQt5 界面不是拖控件完事实时摄像头流、热力图叠加与 PDF 报告生成的硬核实现PyQt5 界面崩溃八成发生在“摄像头启动”和“模型推理线程切换”两个环节。我们不用QTimer做帧循环会卡 UI也不用QThread信号跨线程易丢而是用QRunnable QThreadPool实现零卡顿流水线。整个界面核心就三个对象CameraWorker采集、InferenceWorker推理、ReportGenerator生成全部通过QMetaObject.invokeMethod串行通信。4.1 摄像头零延迟采集用 OpenCV VideoCapture QTimer.singleShot 替代 QCameraQCamera在 Windows 下兼容性极差且无法控制曝光/白平衡。我们直接用 OpenCV# ui/camera_worker.py class CameraWorker(QRunnable): def __init__(self, camera_id0): super().__init__() self.cap cv2.VideoCapture(camera_id) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 关闭自动曝光 self.cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 固定曝光值实测-6最佳 self.signal SignalEmitter() # 自定义信号类 def run(self): while True: ret, frame self.cap.read() if not ret: continue # BGR → RGB → QImage → QPixmap供 QLabel 显示 rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_frame.shape bytes_per_line ch * w qt_image QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888) pixmap QPixmap.fromImage(qt_image) # 发送 pixmap 到主线程非 blocking QMetaObject.invokeMethod(self.signal, frame_ready, Qt.QueuedConnection, Q_ARG(QPixmap, pixmap)) # 控制帧率30fps QThread.msleep(33)为什么这么写cap.set(...)显式关闭自动曝光否则舌面反光时模型误判“润燥”QMetaObject.invokeMethod比Signal.emit()更可靠避免跨线程信号丢失QThread.msleep(33)精确控制帧率比QTimer的timeout更稳定实测抖动 2ms。4.2 热力图叠加用 OpenCV 透明融合替代 QPainter避免 PyQt5 渲染撕裂PyQt5 的QPainter在高速刷新时会出现“半帧撕裂”。我们把热力图生成和叠加全交给 OpenCV# ui/inference_worker.py def overlay_heatmap(frame_rgb, heatmap, alpha0.6): frame_rgb: numpy array (H,W,3), uint8 heatmap: torch tensor (1,H,W), float32, 0~1 # 转 numpy resize 到 frame 尺寸 h, w frame_rgb.shape[:2] heatmap_np F.interpolate(heatmap.unsqueeze(0), size(h, w), modebilinear).squeeze().cpu().numpy() # 生成 jet colormap 热力图 heatmap_colored cv2.applyColorMap((heatmap_np * 255).astype(np.uint8), cv2.COLORMAP_JET) # 透明融合frame * (1-alpha) heatmap * alpha blended cv2.addWeighted(frame_rgb, 1-alpha, heatmap_colored, alpha, 0) return blended # 在 inference_worker.run() 中调用 blended_frame overlay_heatmap(rgb_frame, heat_tensor) # 转 QImage 显示 qt_img QImage(blended_frame.data, w, h, w*3, QImage.Format_RGB888)参数说明alpha0.6是平衡点小于 0.5 热力图太淡看不清大于 0.7 原图细节丢失F.interpolate用bilinear模式不是nearest避免热力图块状锯齿cv2.addWeighted比QPainter.drawPixmap快 3.2 倍实测 12ms vs 39ms。4.3 PDF 报告生成用 ReportLab 替代 PyQt5 打印支持医院抬头与电子签名PyQt5 的QPrinter无法嵌入矢量签名和医院 logo。我们用 ReportLab 生成 PDF# report/generator.py from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 from reportlab.platypus import Table, TableStyle, Paragraph from reportlab.lib.styles import getSampleStyleSheet def generate_report(patient_info, analysis_result, output_path): c canvas.Canvas(output_path, pagesizeA4) width, height A4 # 添加医院抬头矢量 logo c.drawImage(logo/hospital_logo.pdf, 50, height-100, width150, height60) # 标题 c.setFont(Helvetica-Bold, 16) c.drawString(50, height-180, 中医舌诊智能分析报告) # 患者信息表 data [ [姓名, patient_info[name]], [性别, patient_info[gender]], [年龄, str(patient_info[age]) 岁], [就诊日期, patient_info[date]] ] t Table(data, colWidths[80, 200]) t.setStyle(TableStyle([(BACKGROUND, (0, 0), (-1, 0), #f0f0f0), (GRID, (0, 0), (-1, -1), 1, #cccccc)])) t.wrapOn(c, width, height) t.drawOn(c, 50, height-250) # 分析结果热力图嵌入 c.drawImage(temp/heatmap.png, 50, height-450, width300, height200) c.setFont(Helvetica, 12) c.drawString(50, height-470, f舌苔证型{analysis_result[syndrome]}) c.drawString(50, height-490, f置信度{analysis_result[confidence]:.2%}) # 电子签名栏预留手写签名区域 c.line(50, 150, 300, 150) c.drawString(50, 135, 中医师签名__________________) c.save()落地细节c.drawImage支持 PDF 格式 logo保证打印不失真TableStyle设置网格线和背景色符合医疗文书规范签名栏用c.line()画横线留白供医生手写——这是医院审核硬性要求。5. 避坑指南PyQt5 深度学习部署的 5 个真实翻车现场与后悔药别等答辩前夜才发现问题。这 5 个坑是我帮 17 个毕设学生填过的每个都附带“现象→原因→解决”三连击照着做能省 3 天 debug 时间。5.1 现象PyQt5 打包后摄像头打不开报错cv2.error: OpenCV(4.5.5) ... error: (-215:Assertion failed) !ssize.empty() in function cv::resize原因PyInstaller 打包时未包含 OpenCV 的 DLLopencv_videoio_ffmpeg455_64.dll导致VideoCapture初始化失败后续cap.read()返回空帧cv2.resize输入为空。解决手动复制C:\Python39\Lib\site-packages\cv2\opencv_videoio_ffmpeg455_64.dll到打包目录dist/your_app/或在 spec 文件中添加binaries[(path/to/opencv_videoio_ffmpeg455_64.dll, cv2)]验证方法打包后运行your_app.exe --debug看是否输出FFmpeg backend loaded。5.2 现象模型在训练时准确率 92%但 PyQt5 里推理结果全错pred.argmax()总是 0原因训练时用了transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])但 PyQt5 推理时忘了对摄像头帧做同样归一化输入是 0~255 的 uint8模型当成了 0~1 的 float。解决在InferenceWorker.run()中加入归一化# frame_rgb 是 uint8 (0-255) frame_tensor torch.from_numpy(frame_rgb).permute(2,0,1).float() / 255.0 # 先转 float 再除 frame_norm transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])(frame_tensor)血泪教训归一化必须在torch.from_numpy之后、unsqueeze(0)之前否则permute顺序错乱。5.3 现象舌体分割掩膜边缘毛刺严重导致苔区 ROI 不准厚薄分类 F1 仅 72%原因U-Net 输出是 sigmoid 概率图直接0.5二值化会丢失亚像素精度且未加 CRF 后处理。解决用skimage.morphology.binary_closing填充小孔from skimage import morphology mask_binary (mask_prob 0.5).astype(np.uint8) mask_clean morphology.binary_closing(mask_binary, morphology.disk(2))或更优用 CRFConditional Random Field优化边界import pydensecrf.densecrf as dcrf # CRF 参数compat10颜色相似性权重sigma3空间距离衰减验证用cv2.contourArea(contour)检查舌体面积波动合格标准同一张图多次推理面积差 5%。5.4 现象PyQt5 界面点击“开始分析”后卡死 10 秒任务管理器显示 Python 占用 100% CPU原因模型推理在主线程执行阻塞了 UI 事件循环。QThread未正确 moveToThread或QRunnable未用QThreadPool.globalInstance()。解决确保InferenceWorker实例提交给全局线程池self.thread_pool QThreadPool.globalInstance() self.thread_pool.start(inference_worker) # 不是 worker.run()在InferenceWorker.run()结尾加QThread.currentThread().quit()排查技巧在run()开头加print(fThread ID: {QThread.currentThreadId()})确认不是主线程。5.5 现象导出 PDF 报告时中文乱码显示为方框原因ReportLab 默认字体不支持中文Paragraph使用Helvetica西文字体。解决下载思源黑体SourceHanSansSC-Regular.otf放入项目fonts/目录注册字体并设置样式from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont pdfmetrics.registerFont(TTFont(SimSun, fonts/SourceHanSansSC-Regular.otf)) styles getSampleStyleSheet() styles[Normal].fontName SimSun styles[Heading1].fontName SimSun注意.otf文件路径必须是绝对路径相对路径在 PyInstaller 打包后失效。6. 毕业答辩前的终极验证用三张图测出系统是否真正 ready别只跑通 demo 就交差。答辩老师一定会问“如果患者伸舌角度歪了 30 度或者灯光偏黄你的系统还准吗” 我让学生用这三张图做压力测试通不过的当场返工。6.1 测试图 1低照度舌图模拟诊室关灯查体来源把标准图用A.RandomBrightnessContrast(brightness_limit0.4, p1.0)生成合格线舌体分割 IoU ≥ 0.82苔色分类准确率 ≥ 85%玄学技巧在CameraWorker中加自动增益控制AGC# 动态调整 cap.set(cv2.CAP_PROP_GAIN, gain_value) # gain_value 10 (128 - np.mean(frame)) * 0.1 # 基于画面平均亮度6.2 测试图 2强反光舌图模拟患者唾液过多来源在标准图上叠加高斯斑点cv2.GaussianBlurcv2.addWeighted合格线润燥分类 F1 ≥ 0.80反光易误判“滑润”避坑点Stage3 模型必须在训练时加入A.RandomShadow增强否则泛化为 0。6.3 测试图 3多舌体舌图模拟患者未伸直舌来源用两张舌图拼接一张居中一张偏右 30%合格线模型必须只输出主舌体结果置信度 0.9忽略副舌体解决方案在 Stage1 U-Net 后加 NMSNon-Maximum Suppression过滤小区域IoU 阈值设为 0.3。最后说句实在话这个系统真正的价值不在 92% 的数字而在于它让中医舌诊第一次有了可复现、可追溯、可验证的数字化路径。我带的第一届学生现在在社区医院用这套系统做慢病随访把“舌苔由薄白转黄厚”的变化节点精准对应到血压药调整时间——这才是技术该有的温度。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →