ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Python机器学习的舌苔检测系统:从图像分类到UI部署

基于Python机器学习的舌苔检测系统:从图像分类到UI部署 简介这份资源是一套基于Python机器学习的舌苔检测系统完整项目包面向计算机、人工智能、通信工程、自动化等专业的高校学生、教师及科研从业者可用于毕业设计、课程设计、项目立项演示或自学进阶。项目代码完整、资料齐全包含设计文档与可运行源码适合在现有基础上二次开发或直接借鉴。压缩包共109个文件约105.04MB其中26个py源码文件承载核心算法与界面逻辑6个pth模型权重与10个pyc编译文件支撑训练与推理另有2个ui界面文件、7张jpg与2张png图像素材、5个json配置及docx设计文档、md说明、ttc字体等结构清晰便于按模块查阅。目前已有66人学习关注。读者可获得从数据预处理、模型训练到UI交互的完整实现路径并借助设计文档与配置说明快速理解项目架构、复现实验流程遇到环境配置或运行问题还可与作者远程交流适合作为机器学习落地舌诊场景的实践参考。1. 舌苔检测系统到底在做什么从一张手机照片到一份体质报告很多人第一次听到「基于 Python 机器学习的舌苔检测系统」脑子里浮现的是中医馆里老大夫眯着眼看舌头的画面。但真正落地时它其实是一条完整的图像分类流水线用户用手机拍一张舌头照片系统先做舌体分割把舌头从嘴唇、牙齿、背景里抠出来再提取颜色、纹理、厚薄、腐腻这些特征最后用一个分类模型判断它属于白苔、黄苔、灰黑苔还是正常薄白苔并给出对应的健康提示。这套东西适合两类人一类是想把机器学习真正跑通一个完整项目的学生和转行者另一类是手里有舌象数据、想快速验证一个中医辅助筛查原型的开发者。它不替代医生诊断但能把「看一眼舌头」这件事变成可复现、可量化、可批量处理的工程流程。论文部分负责讲清特征工程和模型选型的依据源码负责把训练和推理串起来UI 界面负责让非技术用户也能点一下按钮就出结果——三者缺一不可而最容易被低估的恰恰是中间那层特征提取它直接决定模型是学到真东西还是在拟合噪声。2. 数据从哪来、怎么标舌象数据集的采集与预处理2.1 舌象数据的三个来源与标注口径做舌苔检测第一道坎不是模型是数据。常见做法有三条路一是公开的中医舌象数据集数量有限且类别不均衡二是自己用手机在自然光下采集成本低但光照和角度极不稳定三是和中医门诊合作由医师现场标注质量最高但周期长。我一般会建议先用公开数据跑通流程再补采自己的数据做微调。标注口径必须提前定死是按舌苔颜色分白、黄、灰黑还是按苔质分薄、厚、腻、腐还是两者组合。如果标注时一会儿按颜色一会儿按质地模型学出来的决策边界会非常混乱这是血泪经验。采集时要注意三点拍摄距离固定在 15 到 20 厘米避免闪光灯直射造成高光过曝尽量在上午自然光下拍。每类至少 200 张起步否则类别不均衡会让模型偏向多数类。标注文件建议用 CSV字段包括图片路径、颜色标签、苔质标签、采集设备、光照条件方便后续做分层抽样和误差分析。2.2 用 OpenCV 做舌体分割与颜色校正的最小脚本原始照片里舌头只占一部分直接送进模型会被背景干扰。下面这段代码做两件事用 HSV 阈值粗分割舌体区域再用白平衡做一次颜色校正减少不同手机色温带来的偏差。import cv2 import numpy as np def segment_tongue(img_path): img cv2.imread(img_path) # 转 HSV舌体在 H 通道大致落在 0-20 和 160-180 两个区间 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) lower1 np.array([0, 40, 60]) upper1 np.array([20, 255, 255]) lower2 np.array([160, 40, 60]) upper2 np.array([180, 255, 255]) mask cv2.inRange(hsv, lower1, upper1) | cv2.inRange(hsv, lower2, upper2) # 形态学去噪闭运算填补舌面小孔 kernel np.ones((7, 7), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations1) # 取最大连通域排除嘴唇等干扰 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None largest max(contours, keycv2.contourArea) clean_mask np.zeros_like(mask) cv2.drawContours(clean_mask, [largest], -1, 255, -1) tongue cv2.bitwise_and(img, img, maskclean_mask) return tongue, clean_mask def white_balance(img): # 灰度世界假设做简单白平衡 result img.copy().astype(np.float32) b, g, r result[:, :, 0], result[:, :, 1], result[:, :, 2] gray (b g r) / 3 result[:, :, 0] np.clip(b * (gray.mean() / (b.mean() 1e-6)), 0, 255) result[:, :, 1] np.clip(g * (gray.mean() / (g.mean() 1e-6)), 0, 255) result[:, :, 2] np.clip(r * (gray.mean() / (r.mean() 1e-6)), 0, 255) return result.astype(np.uint8)逻辑说明HSV 双区间是为了覆盖红色到橙红色这一段舌体色域单区间容易漏掉偏紫的舌象。闭运算迭代两次是为了填补舌面反光造成的小孔洞开运算一次去掉边缘毛刺。取最大连通域是关键一步因为嘴唇和舌头颜色接近不排除的话分割结果会连成一片。白平衡用灰度世界假设虽然简单但对手机拍摄的色温漂移有肉眼可见的改善。参数说明lower1/upper1的 S 下限 40 是经验值低于 40 会把偏白的舌苔也排除掉V 下限 60 是为了滤掉阴影区域。形态学核大小 7×7 适合 1080P 以上的图如果图片分辨率低核要相应缩小到 3×3 或 5×5否则会把舌体边缘腐蚀掉。2.3 特征工程颜色矩、灰度共生矩阵与 LBP 怎么选分割完之后不能直接把像素丢给模型样本量不够时容易过拟合。常见做法是提取手工特征颜色矩一阶均值、二阶方差、三阶偏度描述苔色灰度共生矩阵GLCM的对比度、能量、熵描述苔质纹理LBP 直方图补充局部微纹理。这三组特征拼起来大概 50 到 80 维配合 SVM 或随机森林在小样本上表现稳定。如果数据量能到几千张也可以直接用 CNN 端到端训练但要注意舌象数据往往类别不均衡需要加类别权重或做重采样。我一般会两条路都跑一遍用手工特征 传统模型做基线再用 CNN 看能提升多少如果提升不明显说明数据量还不够支撑深度模型不如把精力花在数据增强和标注清洗上。3. 模型训练与评估从 SVM 基线到 CNN 微调3.1 用 scikit-learn 搭一个可复现的 SVM 基线在数据量不大、特征维度可控的情况下SVM 是最稳的起点。下面这段代码把颜色矩和 GLCM 特征拼起来做标准化后用网格搜索找最优参数。import numpy as np from skimage.feature import graycomatrix, graycoprops from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV, StratifiedKFold def color_moments(img): # 输入 BGR转 HSV 后取 H 和 S 通道的一二三阶矩 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) feats [] for ch in [hsv[:, :, 0], hsv[:, :, 1]]: ch ch.astype(np.float32) feats.extend([ch.mean(), ch.std(), float(np.mean((ch - ch.mean()) ** 3))]) return np.array(feats) def glcm_features(gray): # 量化到 16 级减少计算量同时保留纹理信息 q (gray / 16).astype(np.uint8) glcm graycomatrix(q, distances[1, 3], angles[0, np.pi/4, np.pi/2], levels16, symmetricTrue, normedTrue) feats [] for prop in [contrast, energy, homogeneity, entropy]: if prop entropy: vals -np.sum(glcm * np.log2(glcm 1e-10), axis(0, 1)) else: vals graycoprops(glcm, prop).flatten() feats.extend(vals) return np.array(feats) # 假设 X 是拼接好的特征矩阵y 是标签 pipe Pipeline([ (scaler, StandardScaler()), (svm, SVC(probabilityTrue, class_weightbalanced)) ]) param_grid { svm__C: [0.1, 1, 10, 100], svm__gamma: [scale, 0.01, 0.001], svm__kernel: [rbf] } cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) grid GridSearchCV(pipe, param_grid, cvcv, scoringf1_macro, n_jobs-1) grid.fit(X, y) print(grid.best_params_, grid.best_score_)逻辑说明颜色矩只取 H 和 S 通道因为 V 通道受光照影响太大放进去反而引入噪声。GLCM 量化到 16 级是权衡256 级计算量大且稀疏16 级在舌苔纹理上足够区分薄厚腻腐。class_weightbalanced是必须的舌象数据里正常薄白苔往往占一半以上不加权重模型会偷懒全预测多数类。评分用f1_macro而不是 accuracy因为我们要看的是每一类都别太差而不是整体数字好看。参数说明C控制惩罚力度舌象特征噪声大时 C 取小一点0.1 到 1更稳gamma用scale通常够用如果欠拟合再手动调小。distances[1,3]是像素对距离1 捕捉细纹理3 捕捉粗纹理两个一起用能同时覆盖薄苔和厚苔。3.2 迁移学习用 ResNet 做舌苔分类的微调策略当数据量到 2000 张以上可以上 CNN。不要从零训练用 ImageNet 预训练的 ResNet18 或 MobileNetV3把最后一层换成自己的类别数先冻结主干训分类头再解冻最后两个 stage 做小学习率微调。import torch import torch.nn as nn from torchvision import models, transforms def build_model(num_classes4): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结主干 for param in model.parameters(): param.requires_grad False # 替换分类头 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(model.fc.in_features, num_classes) ) return model train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])逻辑说明冻结主干是为了在小数据上不让预训练权重被破坏只训分类头通常就能到不错的精度。解冻最后两个 stage 时学习率要降到 1e-4 甚至 1e-5否则容易把学到的通用特征冲掉。数据增强里 ColorJitter 的幅度不能太大舌苔颜色本身就是判别依据饱和度调过头会把白苔变成黄苔这是翻车点。参数说明RandomRotation(15)是舌象拍摄角度偏差的经验上限再大就会把舌头转出画面。Dropout(0.3)在分类头前加一层小数据上能明显抑制过拟合。归一化用的 ImageNet 均值方差如果自己的数据整体偏暗可以换成自己数据集的统计值。3.3 评估指标为什么不能只看准确率舌苔检测的评估必须看混淆矩阵和每类 F1。正常薄白苔样本多模型全预测正常也能有 60% 以上的准确率但黄苔和灰黑苔的召回率会惨不忍睹。我一般会打印 classification_report重点看灰黑苔这一类因为它样本最少但临床意义最大。如果灰黑苔 F1 低于 0.6要么补数据要么在损失函数里给它更高权重。另外建议做一次按采集设备分组的交叉验证看看模型是不是在拟合某台手机的色偏而不是真正的舌苔特征。4. UI 界面与推理服务让非技术用户也能点一下出结果4.1 用 Gradio 快速搭一个可交互的舌苔检测界面论文和源码之外UI 界面是让这套系统能被真实使用的关键。不要一上来就写 PyQt用 Gradio 几十行就能出一个网页界面支持上传图片、显示分割结果和预测概率。import gradio as gr import numpy as np from PIL import Image def predict(img): # img 是 PIL 格式转成模型需要的输入 img_np np.array(img) tongue, mask segment_tongue_from_array(img_np) if tongue is None: return None, 未检测到舌体请重新拍摄 tongue white_balance(tongue) feats np.concatenate([color_moments(tongue), glcm_features(cv2.cvtColor(tongue, cv2.COLOR_BGR2GRAY))]) proba grid.predict_proba([feats])[0] labels [薄白苔, 白厚苔, 黄苔, 灰黑苔] result {labels[i]: float(proba[i]) for i in range(len(labels))} return tongue, result demo gr.Interface( fnpredict, inputsgr.Image(typepil, label上传舌象照片), outputs[gr.Image(label分割结果), gr.Label(label分类概率)], title舌苔检测系统, description上传自然光下拍摄的舌头照片系统会分割舌体并给出苔色分类概率。 ) demo.launch(server_name0.0.0.0, server_port7860)逻辑说明Gradio 的Image(typepil)直接给 PIL 对象省去文件读写。分割失败时返回提示而不是硬跑模型避免把背景当舌头预测出荒谬结果。输出用gr.Label显示概率条比单纯返回一个类别更让用户信服也能看出模型是不是在犹豫。参数说明server_name0.0.0.0让局域网内其他设备也能访问方便演示。server_port默认 7860如果被占用换一个即可。如果要做成桌面应用可以用 PyInstaller 把 Gradio 打包但更稳的做法是前端用 Vue 写页面后端用 FastAPI 暴露/predict接口Gradio 只用于内部调试。4.2 推理服务的性能与并发注意点如果只是个人用Gradio 足够。但如果要放到科室里多人同时用就要考虑并发。SVM 推理很快单张图特征提取加预测在 CPU 上大概 50 到 100 毫秒瓶颈在图像分割的形态学操作。常见做法是把分割和特征提取放到一个进程池里用 FastAPI 的run_in_executor避免阻塞事件循环。CNN 模型则建议用 ONNX Runtime 或 TorchScript 导出后推理比直接跑 PyTorch 快 2 到 3 倍显存占用也更低。另外要注意上传图片的大小限制手机直出照片可能 4000×3000直接送进分割会慢得离谱。在接口层先做一次长边缩放到 1024再送后续流程精度损失可以忽略速度提升明显。5. 避坑与排查舌苔检测系统最常见的 5 个翻车点5.1 现象模型在测试集上准确率 90%实际用全是薄白苔原因数据集中薄白苔占比过高且测试集和训练集来自同一批采集没有按设备或光照分组。模型学到的是「大多数样本长这样」而不是舌苔本身的判别特征。解决按采集设备和光照条件做分组划分确保测试集里有不同手机、不同时段的样本。同时打印混淆矩阵如果某一类召回率低于 0.5先补数据再调模型不要靠调参硬拉。5.2 现象分割出来的舌头边缘总是缺一块尤其是舌尖原因舌尖区域颜色偏红偏亮HSV 阈值上限卡太死或者形态学开运算核太大把舌尖腐蚀掉了。解决把 H 通道上限从 20 放宽到 25S 下限从 40 降到 30同时把开运算核从 7×7 降到 3×3。如果还不行改用 GrabCut 做二次分割以 HSV 粗分割结果作为初始矩形效果会稳很多。5.3 现象换一台手机拍预测结果全变原因不同手机的白平衡和色彩风格差异巨大模型对色偏敏感。灰度世界白平衡只能解决整体色温解决不了厂商的色调曲线。解决在训练时加入强颜色增强模拟不同色温下的舌象或者在推理前用一张标准色卡做颜色校正。更彻底的做法是把颜色特征从 RGB/HSV 转到与设备无关的 Lab 空间只取 a、b 通道做颜色矩对色偏的鲁棒性会好很多。5.4 现象UI 界面上传图片后一直转圈没有结果原因Gradio 默认超时时间较短而分割加特征提取在低配机器上可能超过 10 秒。或者图片太大内存爆了但没报错。解决在predict函数开头先缩放图片长边不超过 1024。Gradio 的Interface可以设allow_flaggingnever减少额外开销。如果还慢把分割逻辑用 OpenCV 的UMat走 GPU或者直接换更快的阈值分割方案。5.5 现象论文里写的特征和源码里对不上原因论文写的时候用的是早期版本的特征集后来源码迭代加了 LBP 或改了 GLCM 参数但论文没同步更新。这是论文加源码类项目最容易被答辩老师抓的点。解决在源码里加一个feature_config.yaml把所有特征开关和参数写进去论文里的特征表直接引用这个配置文件。每次改特征先改配置再同步论文避免两边打架。6. 把系统跑稳之后我还会做的一件事用置信度做拒识模型跑通、界面能点、论文能写很多人到这里就停了。但真正拿到实际场景里用你会发现有一类图片模型永远预测不准舌体没拍全、光线过暗、或者舌苔本身处于白黄交界的过渡态。这时候硬给一个分类结果不如让系统说「这张图我拿不准请重拍」。具体做法是在 SVM 的predict_proba或 CNN 的 softmax 输出上设一个置信度阈值。我一般会取 0.6 作为拒识线低于这个值就返回「无法确定建议在自然光下重拍」。这个阈值不是拍脑袋定的而是在验证集上画一条置信度-准确率曲线找到准确率开始明显下降的拐点。下面这段代码演示怎么在推理时加拒识逻辑。def predict_with_reject(feats, model, threshold0.6): proba model.predict_proba([feats])[0] max_prob np.max(proba) if max_prob threshold: return None, max_prob, 置信度不足请重拍 pred np.argmax(proba) return pred, max_prob, ok逻辑说明threshold是拒识线低于它就不给分类结果。返回三个值是为了让 UI 层能区分「预测成功」「置信度低」「其他错误」分别展示不同提示。这个逻辑对 SVM 和 CNN 都适用CNN 把predict_proba换成 softmax 即可。参数说明threshold0.6是舌苔四分类的经验值类别越多阈值可以适当降低二分类可以提到 0.7 以上。如果发现拒识率太高超过 20%说明模型本身判别力不够应该回去补数据或换模型而不是把阈值调低硬给结果。还有一个进阶玩法把拒识样本单独存下来定期人工标注后加入训练集形成主动学习的闭环。我自己的习惯是每周看一次拒识样本如果某一类拒识特别多优先补那一类的数据。这样跑几个月模型的实际可用率会明显上升比一次性堆数据有效得多。最后说一个我踩过的坑不要为了追求论文里的高准确率去调测试集。测试集一旦被用来调参它就不再是测试集了。我一般会留一份完全没碰过的 hold-out 集只在最后验收时跑一次那个数字才是真正能写进论文、也能在实际部署中站得住的。希望帮到你。本文还有配套的精品资源点击获取
返回列表