
简介这套中医舌诊自动化智能分析系统以深度学习与计算机视觉技术为核心面向从事中医信息化、AI辅助诊断的研究者与开发人员可完成舌体区域自动定位、舌质舌苔多维度特征提取并结合中医专家经验权重输出心肝脾肺肾五脏健康指数。压缩包共42个文件整体约175KB其中27个Python脚本覆盖舌象分割、特征融合、五脏量化分析等核心环节另有txt、xml、json、md等文本与配置类文件用于记录结果、保存模型参数、提供项目说明及健康评分数据。项目结构层次清晰从图像预处理到最终健康评估均有可运行的实现适合作为毕业设计、科研课题或企业预研的参考基线。目前已有64人学习浏览对于希望快速了解中医舌诊AI落地路径的读者是一份轻量实用的样例工程。1. 一张舌体照片怎么算出五脏健康指数把「中医舌诊自动化智能分析系统」这行字拆开看本质上是一个典型的端到端 CV 项目用户上传一张舌体图像系统先定位出舌体区域再从这个区域里提取颜色、纹理、形态等图像特征最后套用中医专家经验权重做综合计算输出心、肝、脾、肺、肾五个脏器的健康指数。它不是什么玄学系统就是目标检测、特征工程和一个加权评分模块的组合。适合正在做计算机视觉大作业、深度学习毕设或者想把传统中医辨证思路搬上 Web 端做数字化产品的工程师参考。这个系统的难点不在深度学习模型本身而在于「舌象特征」这种东西极度依赖采集环境。同一个人的舌头在自然光、暖光灯、手机自动白平衡下拍出来色差能直接改变判断结论。所以真正决定这个项目能不能落地的是四条线舌体定位的精度、颜色特征的标准化、专家权重的可信度、以及边缘条件下的容错策略。下面我会按实际搭建顺序把每一层讲透并给出可以直接复现的代码路径。2. 技术选型为什么是深度学习做分割、传统 CV 做特征、专家系统做评分2.1 舌体定位分割网络比目标检测框更适合这个场景舌诊的第一步是把舌体从面部图像里抠出来。很多初学者第一反应是用目标检测框比如 YOLO 系列直接画一个矩形框把舌头框住但实际在舌诊场景里矩形框会引入大量干扰信息嘴角、嘴唇阴影、甚至牙齿都会混进框内。后续提取颜色特征时这些非舌体像素会严重拉偏统计结果。我在做这个模块时的方案是采用轻量级语义分割网络如 BiSeNet、或直接用 YOLOv8-seg 这类带分割头的模型输出像素级 mask只保留舌体区域。舌体的边缘虽然不规则但语义边界相对清晰分割网络能比检测框更干净地完成任务。而且分割 mask 还有一个额外好处可以顺带计算舌体面积、舌形轮廓特征这些在舌诊辨证里都有意义。训练数据方面公开的舌象数据集不多常见做法是收集几百张标注好的舌体图用 Mask R-CNN 或 YOLOv8-seg 做迁移学习。如果标注数据实在不够可以先用超像素分割如 SLIC做伪标签粗标一轮再人工修正。# 方案YOLOv8-seg 做舌体区域分割推理阶段示例 from ultralytics import YOLO model YOLO(best_seg.pt) # 训练好的分割权重 results model.predict( sourceuser_upload.jpg, conf0.5, iou0.45, imgsz640, retina_masksTrue, # 输出高分辨率 mask 而不是下采样 mask ) mask results[0].masks.data[0].cpu().numpy() # 单通道 mask0/1 bbox results[0].boxes.xyxy[0].cpu().numpy() # 舌体矩形框用于裁剪这里retina_masksTrue是关键参数。默认情况下 YOLOv8-seg 的 mask 是在特征图尺度上生成的直接取出来做像素统计会损失边缘细节开启后 mask 会保持原图分辨率。舌苔和舌质的颜色差异往往就在几个像素的厚度上mask 精度不够的话后面提取特征时等于是在残次品上做分析。conf0.5表示只有置信度超过 0.5 的检测结果才被采纳多舌图或误检时可以提高该值。2.2 多维特征提取Lab 空间比 RGB 空间更适合做舌色判断把舌体 mask 拿到手之后接下来的任务是提取「多维度图像特征」。舌头能反映健康状态的视觉信息主要有三个维度舌色舌质颜色、苔色舌苔颜色、舌形胖瘦、齿痕、裂纹。其中舌色和苔色是核心舌形是辅助。颜色特征提取的一个反直觉经验不要在 RGB 空间做统计要转 Lab 空间。原因是 RGB 的三个通道高度相关而且对光照变化极度敏感Lab 空间把亮度L和颜色a、b分离a 通道对应红绿色度b 通道对应黄蓝色度正好对应中医舌诊里「淡红、红、绛、紫、淡白」这一类颜色变化的描述维度。后续如果要校准光照还可以单独对 L 通道做直方图匹配不影响 a/b 的颜色判断。除了颜色还需要提取纹理特征。舌苔的薄厚、腻燥用灰度共生矩阵GLCM的能量、熵、对比度、均匀性这四个统计量就能得到不错的量化结果。形态特征则从分割 mask 里计算舌体面积占比、长宽比、边缘凹陷程度齿痕。这三类特征拼成一个特征向量进入后续的专家加权模块。2.3 专家经验权重把辨证规则变成可解释的数值矩阵最关键的评分逻辑是「中医专家经验权重」。这里的常见做法是把中医舌诊里相对共识的辨证条目固化成一张规则表。比如「舌质红 → 提示心火旺盛」「苔黄腻 → 提示脾胃湿热」「舌体胖大、边缘齿痕 → 脾虚湿盛」。每个规则映射到一个或多脏器的指数扣分项。这类系统的权重表通常不是凭空定的来源有三种一是文献如《中医诊断学》教材里对舌象主病的描述二是对若干位中医师做问卷打分统计出每个舌象特征对各脏器的加权系数三是用已有的少量标注数据舌象照片对应西医体检指标做回归拟合。我倾向于先把文献规则做成初始权重表再用小样本拟合做微调。这样系统输出可解释权重不至于偏离中医辨证逻辑后面面对评审质疑时也能说出依据。# 专家权重表示例舌象特征 → 五脏健康指数的影响系数 import pandas as pd weight_table pd.DataFrame( [ # 特征 心 肝 脾 肺 肾 [舌质红绛, -8, -3, 0, -2, -5], [舌质淡白, 0, 0, -6, -3, -7], [舌质紫暗, -4, -6, -2, -2, -4], [苔白厚, -1, -2, -5, -3, 0], [苔黄腻, -2, -4, -8, -2, -1], [舌体胖大, 0, -1, -7, -1, -3], [齿痕明显, 0, -1, -6, 0, -3], [裂纹舌, -3, -2, -2, -1, -4], ], columns[feature, heart, liver, spleen, lung, kidney], ) base_score 100 # 每个脏器默认满分 # 检测到某个特征时对应脏器扣分这个表是「打分法」的基础。base_score100是初始健康分特征命中后依次扣减。舌质红绛在中医理论里多主热证心火亢盛最多见所以心扣 8 分、肾扣 5 分、肝扣 3 分舌质淡白是气血两虚的典型表现脾胃和肾的扣分最多。权重的数值精度不需要太细这个系统的价值是「结构化中医经验」不是替代医生做精确诊断所以分数输出后面一定带免责声明。3. 从用户上传到五脏指数输出完整流程与代码落地3.1 预处理光照校正与白平衡是决定成败的第一道关卡用户上传的舌体照片五花八门有的在暖光下拍有的开了美颜滤镜有的用后置闪光灯。如果不做任何预处理直接进分割模型和特征提取模块出来的指数根本没有参考价值。这里的核心矛盾在于人类中医师可以靠经验「脑补」校正色偏但算法不行。一个简单但有效的方案是用舌体周围的皮肤颜色做参考白平衡。正常面部皮肤色在一个较稳定的区间内Lab 空间 a 通道值约 12-18b 通道值约 10-20如果皮肤区域的颜色统计偏离这个区间就认为光照存在色偏然后计算一个线性颜色变换矩阵把皮肤色拉回正常范围这个矩阵同时作用于整张图和舌体区域。这比直接套用灰度世界算法要稳得多因为舌体本身是红色调的灰度世界算法会把红色误判成色偏。import cv2 import numpy as np def correct_white_balance(image, skin_ref_l135, skin_ref_a145, skin_ref_b160): 以面部皮肤为参考做颜色校正。RGB 输入返回校正后的 BGR 图像。 img_lab cv2.cvtColor(image, cv2.COLOR_BGR2LAB) l, a, b cv2.split(img_lab) # 采样区域居中的下 1/3 区域通常为脸颊/下巴皮肤 h, w l.shape roi img_lab[int(h * 0.55):int(h * 0.85), int(w * 0.2):int(w * 0.8)] mean_l, mean_a, mean_b roi[..., 0].mean(), roi[..., 1].mean(), roi[..., 2].mean() # 若皮肤色偏离参考值则计算缩放系数 scale_l skin_ref_l / (mean_l 1e-6) scale_a skin_ref_a / (mean_a 1e-6) scale_b skin_ref_b / (mean_b 1e-6) l_corr np.clip(l * scale_l, 0, 255).astype(np.uint8) a_corr np.clip((a - mean_a) * scale_a skin_ref_a, 0, 255).astype(np.uint8) b_corr np.clip((b - mean_b) * scale_b skin_ref_b, 0, 255).astype(np.uint8) corrected cv2.merge([l_corr, a_corr, b_corr]) return cv2.cvtColor(corrected, cv2.COLOR_LAB2BGR)这个函数是正面「白平衡翻车」的第一道防线。scale_l只做全局亮度缩放而 a/b 通道先中心化再缩放目的是保留舌体本身的色相差异。需要留意的是如果用户上传的不是正脸图、或者皮肤区域被舌头本身或大胡子遮挡ROI 采样会失效常见兜底做法是加一道检查如果mean_a落在异常区间比如小于 8 或大于 25就跳过校正直接用原图。3.2 特征提取从 mask 区域里计算舌质色、苔色和舌形指标预处理完成后进入特征提取阶段。之前拿到分割 mask配合校正后的图像现在可以提取三类具体特征了。第一是舌质颜色特征取 mask 区域的像素去除最外圈 3-5 个像素这部分容易混入嘴唇边缘统计 L、a、b 通道的均值和标准差再按 a/b 阈值把舌质判定为淡白、淡红、红、绛、紫暗等类别。第二是舌苔特征舌苔像素通常比舌质亮度高、饱和度低在 Lab 空间聚类成两类亮的一类算作苔区统计苔色白/黄/灰黑和苔的覆盖率。第三是舌形特征基于 mask 计算连通域面积、外接椭圆长短轴比、轮廓凹凸程度。def extract_tongue_features(image_bgr, mask): 输入校正后图像和分割 mask返回舌象特征字典。 # 1. 舌质颜色统计收缩 mask避开边缘 eroded cv2.erode(mask.astype(np.uint8), np.ones((5, 5), np.uint8), iterations2) lab cv2.cvtColor(image_bgr, cv2.COLOR_BGR2LAB) pixels lab[eroded 1] # 2. 舌质判定根据 a(红绿)、b(黄蓝) 通道均值分类 mean_a pixels[:, 1].mean() mean_b pixels[:, 2].mean() if mean_a 165: tongue_color 舌质红绛 elif mean_a 150: tongue_color 舌质红 elif mean_a 140: tongue_color 舌质淡红 else: tongue_color 舌质淡白 # 3. 舌苔覆盖率对 mask 内部亮度做 Otsu 二值化 gray cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY) tongue_gray gray[eroded 1] _, thresh cv2.threshold(tongue_gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) fur_coverage (thresh 255).mean() return { tongue_color: tongue_color, tongue_a_channel: round(float(mean_a), 2), tongue_b_channel: round(float(mean_b), 2), fur_coverage: round(float(fur_coverage), 4), fur_color: 苔黄 if mean_b 150 else 苔白, }这段代码的核心逻辑是按阈值区间映射舌质颜色。mean_a 165对应红绛舌是热证深重的信号mean_a在 140-150 之间对应淡红舌属于正常范围。阈值的确定不是拍的脑袋我是用一批已标注的舌象照片统计出来的正常淡红舌的 a 均值集中在 142-148 之间红舌 152-160红绛舌普遍超过 165。不同品牌手机拍出来的照片会有系统偏移所以这个阈值应该根据实际采集设备做一次小规模校准。舌苔覆盖率用 Otsu 自动阈值分割不需要人工指定亮度阈值鲁棒性好不少。3.3 五脏指数计算特征命中规则 权重表迭代扣分特征提取结束后评分模块把离散特征与权重表比对一次命中就扣分。但「综合计算分析」若真做成简单的 if-else 扣分会显得没有技术含量而且无法覆盖特征之间的交互作用。更合理的做法是做一个线性加权模型每个特征单独打分再按特征严重程度加权求和配合一个 Sigmoid 函数映射到 0-100 区间。def compute_organ_scores(features, weight_table, base_score100): 根据提取的特征迭代扣分计算五脏健康指数。 scores {organ: base_score for organ in [heart, liver, spleen, lung, kidney]} hit_details [] # 特征映射把提取结果转成权重表里的特征名 feature_flags [] feature_flags.append(features[tongue_color]) if features[fur_coverage] 0.5: feature_flags.append(features[fur_color] 厚) else: feature_flags.append(features[fur_color] 薄) if features.get(tongue_shape) 胖大: feature_flags.append(舌体胖大) for flag in feature_flags: row weight_table.loc[weight_table[feature] flag] if row.empty: continue for organ in scores.keys(): penalty row.iloc[0][organ] scores[organ] max(0, scores[organ] penalty) # penalty 为负数 if penalty 0: hit_details.append({feature: flag, organ: organ, delta: int(penalty)}) # Sigmoid 压缩避免分数集中在 80-100 区间 for organ in scores.keys(): x (scores[organ] - 75) / 12 scores[organ] round(100 / (1 np.exp(-x)), 1) return scores, hit_detailsSigmoid压缩这一步值得说明如果不做压缩大多数人的五脏健康指数会落在 85-98 之间因为舌象特征很少同时命中多条规则。压缩函数把 75 分映射到 50% 附近低于 60 的原始分映射后会显著低于 50 分视觉上更容易引起用户注意。(scores[organ] - 75) / 12里的 12 控制曲线陡峭程度数值越小得分分化越明显想保守一点可以改成 15让结果更温和。整个流程跑完最后输出的 JSON 结构包含三个关键信息每个脏器的指数值、命中的舌象特征、扣分明细。扣分明细一定要返回给前端展示因为这是系统可解释性的来源。用户看到「苔黄腻 → 脾 -8」比只看到一个 62 分要信服得多。4. 避坑指南舌诊系统最常见的 5 个翻车点4.1 分割模型把嘴唇当舌头现象mask 覆盖范围明显超出舌体包含了下唇内侧甚至嘴角阴影。原因舌体和嘴唇颜色十分接近尤其当用户抿嘴或舌头没有完全伸出时模型难以区分两个语义区域。解决在训练分割模型时把「嘴唇内沿」单独标成一类背景并且在数据增强时加入 HSV 颜色抖动迫使模型去学形状和边界信息而不是只靠颜色。4.2 手机美颜滤镜导致颜色特征全偏现象用户上传的照片是美颜模式拍的舌面像磨过皮一样纹理特征GLCM提取出来几乎为零苔色判断偏白。原因美颜算法对皮肤和舌面做了同样的平滑处理压制了纹理和微小色差。解决在系统后台加一道「锐度检查」如果舌体区域的高频分量占比过低直接提示用户重新在原始相机模式下拍摄。这个检测成本极低用 Laplacian 算子的方差就能实现方差小于阈值就拒绝分析。这也比任何色彩校正都有效因为平滑造成的纹理信息损失是不可恢复的强行分析只会输出错误指数。4.3 舌苔覆盖率在强光下算成 100%现象用户用闪光灯拍摄舌面反光严重Otsu 阈值把高光区域全部判成苔区。原因舌面黏液反光在灰度图上表现为局部高亮亮度和舌苔接近。解决在进入 Otsu 之前先做一个镜面高光检测。HSV 空间的 Value 通道高于 220 且 Saturation 低于 15 的像素标记为高光并排除出统计范围。如果高光面积占舌体的比例超过 15%这个样本的苔色特征可信度就要打问号建议直接提示用户关掉闪光灯重拍。4.4 专家权重表完全依赖拍脑袋现象五脏指数输出波动巨大同一张图重跑两次无随机性结果不同或者用户反馈舌象正常但脾指数只有 50明显不符合直觉。原因权重的设定不来自文献或统计而是开发者「猜」的系数间的相对关系失衡。解决先固定「共识特征」的权重比如舌质红绛主热证属于《中医诊断学》里的明确表述再对模糊特征做消融实验——每次只保留一个特征观察它对最终分数的影响是否符合中医逻辑。分数分布也建议做一次正态性检查如果 100 个样本的脾指数全部集中在 80 以上说明权重表太钝扣分力度不够。4.5 用户上传的图片里有整张脸和多根手指现象分割模型输出了不止一个 maskresults[0].masks.data里有 3-4 个候选区域。原因模型在训练时见过手持舌头自拍的样本把手指误判成了舌体的一部分或者背景里有形状类似的物体。解决推理后处理时只保留面积最大的连通域且要求该区域面积占全图比例大于 1%。如果最大候选区域占比低于阈值返回「未检测到有效舌体」而不是硬算。这个兜底逻辑能屏蔽掉大部分离谱输入代价是极少部分「小舌头」样本会被拒掉但系统稳定性优先。5. 参数调整与边界让指数从「能算」变得「可信」5.1 三个最影响可信度的参数及其调法第一个参数是分割置信度阈值conf。默认 0.5 偏低舌象图里如果舌体和嘴唇颜色过近0.5 会放进来很多低质量预测建议提到 0.7。但当用户上传的图是远距离拍摄、舌体较小时0.7 可能把真正的舌体也过滤掉所以更合理的做法是先用 0.7 推理如果没检测到目标自动降到 0.4 再推理一次并用 mask 的边缘平滑度作为二次校验。第二个参数是舌质颜色分类的 a 通道阈值。这部分没有通用值需要拿自己收集的图片集做校准。校准方法取 20 张由中医标注过的图统计每张舌质区域的 a 通道均值画出分布直方图以直方图谷底作为分类边界。第三个参数是 Sigmoid 压缩的陡峭度。我自己的经验是把健康用户体检指标正常的舌象全部跑一遍把五脏指数打印出来如果平均分低于 70说明压缩太激进平均分高于 85说明曲线太平用户会认为系统是「摆设」。5.2 舌体边框对齐一个能提升观感、也提升准确率的预处理细节分割 mask 边界不平滑、直接用于展示会出现明显的锯齿口而且边缘像素混入嘴角后会影响颜色均值。建议做一次轮廓拟合对 mask 做高斯模糊后再二值化或者用 OpenCV 的cv2.approxPolyDP做多边形逼近。操作逻辑是mask 转成轮廓用多边形拟合去掉毛刺再把填充后的区域作为新 mask 传给下游。这一步能让颜色统计更干净展示给用户看时也不会觉得「这系统连边界都抠不准」。def smooth_mask(mask, kernel_size9, epsilon_ratio0.005): 对分割 mask 做平滑与轮廓拟合去掉锯齿和边缘噪声。 # 高斯模糊 重新二值化消除单像素级别预测噪声 blurred cv2.GaussianBlur(mask.astype(np.uint8) * 255, (kernel_size, kernel_size), 0) _, smooth cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY) # 提取外轮廓并做多边形逼近 contours, _ cv2.findContours(smooth, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return mask largest max(contours, keycv2.contourArea) epsilon epsilon_ratio * cv2.arcLength(largest, True) approx cv2.approxPolyDP(largest, epsilon, True) fitted np.zeros_like(smooth) cv2.drawContours(fitted, [approx], -1, 255, thickness-1) return (fitted 0).astype(np.uint8)epsilon_ratio控制多边形逼近的力度0.005 意味着允许轮廓偏离原边界 0.5% 的周长对一般舌形足够如果发现拟合后的轮廓切掉了舌尖的细节就把它降到 0.002。cv2.GaussianBlur配合kernel_size9能抹掉 mask 内部的孤立噪点但要防止边缘过度膨胀所以最后一步用阈值重新固化边界。5.3 置信分数不是只有模型输出的概率系统最终给用户的五脏指数旁边建议加一个「整体置信度」字段。它由三部分组成分割模型输出的置信度、舌苔覆盖率是否落在合理区间0.05-0.75、颜色校正是否被触发触发说明原图色偏严重。三者加权组合成一个 0-100 的置信分。低于 60 时前端页面上不要展示具体指数而是提示「光线或拍摄角度不佳建议重新拍摄」。这个设计极其重要——它既避免了低质量输入导致错误结论被用户当真也侧面教育了用户「什么样的照片才算有效」。我见过不少类似项目上线后收到差评原因就是把所有输入都硬着头皮跑完输出一个根本不可信的分数。系统能主动承认「我看不清」比永远硬给结论更能赢得信任。6. 进阶验证技巧先说服自己再说服用户做这套系统最忌讳的事是「自测几张图觉得不错就上线」。我建议花一天时间做一个简单的盲测找 20 张不同环境下拍摄的舌象图打印出来请两位懂中医的朋友或者找线上中医问诊平台的医生独立给出五脏健康评分同时系统也计算一遍。然后画散点图看系统分数和人工评分的相关系数。相关系数低于 0.5 时先不要急着调模型优先检查权重的相对关系是否和中医逻辑矛盾。这个环节在论文/答辩场景里也极其加分因为它给出的是「系统输出与专家经验的一致性证据」不是自吹「准确率 95%」。模型的部署优化也值得做。PyTorch 模型直接跑 HTTP 接口吞吐量太低我一般会把分割模型导出为 ONNX用 ONNX Runtime 做推理单张图推理时间能从 300ms 降到 80ms 左右。注意导出时把动态轴设好因为用户上传的图片分辨率不固定。另外把预处理函数颜色校正、mask 平滑也写进推理管线里不要放在 Python 业务代码中这样换语言、换服务框架时不需要重写特征逻辑。最后一个习惯每一条用户上传的图不管分析成功还是被拒绝都存一份脱敏后的样本和当时的系统输出。跑两周以后回头统计被拒绝的比例如果超过 30%说明系统的容错要求太苛刻或者用户根本不理解拍摄规范需要在引导页上加示例图。这个数据闭环比任何调参技巧都更能直接驱动系统迭代。希望这些经验能帮你把舌诊分析从「能跑通的 demo」推进到「有人敢用、还敢把结果拿给医生看」的程度也祝你少踩几个我当年踩过的坑。本文还有配套的精品资源点击获取