☰
基于CV与深度学习的中医舌诊自动化分析系统实现
2026/10/2 9:38:44 网站建设 项目流程

简介:这套中医舌诊自动化智能分析系统以深度学习与计算机视觉技术为核心,面向从事中医信息化、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( source="user_upload.jpg", conf=0.5, iou=0.45, imgsz=640, retina_masks=True, # 输出高分辨率 mask 而不是下采样 mask ) mask = results[0].masks.data[0].cpu().numpy() # 单通道 mask,0/1 bbox = results[0].boxes.xyxy[0].cpu().numpy() # 舌体矩形框,用于裁剪

这里retina_masks=True是关键参数。默认情况下 YOLOv8-seg 的 mask 是在特征图尺度上生成的,直接取出来做像素统计会损失边缘细节;开启后 mask 会保持原图分辨率。舌苔和舌质的颜色差异往往就在几个像素的厚度上,mask 精度不够的话,后面提取特征时等于是在残次品上做分析。conf=0.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_score=100是初始健康分,特征命中后依次扣减。舌质红绛在中医理论里多主热证,心火亢盛最多见,所以心扣 8 分、肾扣 5 分、肝扣 3 分;舌质淡白是气血两虚的典型表现,脾胃和肾的扣分最多。权重的数值精度不需要太细,这个系统的价值是「结构化中医经验」,不是替代医生做精确诊断,所以分数输出后面一定带免责声明。

3. 从用户上传到五脏指数输出:完整流程与代码落地

3.1 预处理:光照校正与白平衡是决定成败的第一道关卡

用户上传的舌体照片五花八门:有的在暖光下拍,有的开了美颜滤镜,有的用后置闪光灯。如果不做任何预处理直接进分割模型和特征提取模块,出来的指数根本没有参考价值。这里的核心矛盾在于:人类中医师可以靠经验「脑补」校正色偏,但算法不行。

一个简单但有效的方案是用舌体周围的皮肤颜色做参考白平衡。正常面部皮肤色在一个较稳定的区间内(Lab 空间 a 通道值约 12-18,b 通道值约 10-20),如果皮肤区域的颜色统计偏离这个区间,就认为光照存在色偏,然后计算一个线性颜色变换矩阵把皮肤色拉回正常范围,这个矩阵同时作用于整张图和舌体区域。这比直接套用灰度世界算法要稳得多,因为舌体本身是红色调的,灰度世界算法会把红色误判成色偏。

import cv2 import numpy as np def correct_white_balance(image, skin_ref_l=135, skin_ref_a=145, skin_ref_b=160): """以面部皮肤为参考做颜色校正。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), iterations=2) 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_score=100): """根据提取的特征,迭代扣分计算五脏健康指数。""" 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_details

Sigmoid压缩这一步值得说明:如果不做压缩,大多数人的五脏健康指数会落在 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 用户上传的图片里有整张脸和多根手指

现象:分割模型输出了不止一个 mask,results[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_size=9, epsilon_ratio=0.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, key=cv2.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_size=9能抹掉 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」推进到「有人敢用、还敢把结果拿给医生看」的程度,也祝你少踩几个我当年踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询