中医舌诊这件事,过去很长一段时间里都停留在“老师傅带徒弟”的口传心授阶段。舌头伸出来,是薄白苔还是厚腻苔,是齿痕舌还是裂纹舌,不同的人看可能给出不同的判断。我做了几年中医数字化相关的项目,最深的感受就是:舌象这块的数据,太依赖主观经验了,同一个患者在不同时间、不同光线下拍出来的舌头照片,连同一个医生都可能有不同的描述。这几年我一直在琢磨,能不能把这件事从“黑盒盲猜”变成“白盒计算”——也就是说,让机器不只是给一个结论,而是能把舌象的每一个特征维度都拆解出来,用可量化、可复现的方式重新构建一遍。
这个项目就是在这个背景下折腾出来的。核心思路是用 MediaPipe 做舌体的关键点检测和区域分割,再结合 VL 模型(视觉语言模型)做舌象特征的语义理解和结构化输出,最终形成一个“舌象重构引擎”。说人话就是:你给一张舌头照片,它能告诉你舌体的轮廓在哪、舌质什么颜色、舌苔什么厚度、有没有齿痕裂纹,而且每一步都有中间结果可以追溯,不是直接甩一个“你脾虚”就完事。这套东西适合谁看?如果你在做中医数字化、健康管理类应用,或者单纯对 MediaPipe 和 VL 模型的落地感兴趣,那这篇内容应该能给你不少可以直接抄作业的东西。
1. 整体架构设计与技术选型思路
1.1 为什么是 MediaPipe 加 VL 模型的组合
先说选型这件事。市面上做舌象分析的方案我大致归为三类:纯传统图像处理、纯深度学习分类、以及多模态组合。纯传统图像处理(比如基于 HSV 阈值分割舌体)的问题是鲁棒性太差,换个光源、换个手机拍,阈值就得重新调,根本没法产品化。纯深度学习分类(比如直接训一个 ResNet 做舌象分类)的问题是黑盒,你给它一张图,它输出一个类别,但为什么是这个类别、舌体的哪个区域起了决定作用,你完全不知道,这在医疗健康场景里是致命的。
MediaPipe 的价值在于它提供了非常稳定的关键点检测和分割能力。Face Mesh 那套 468 个关键点的模型,其实已经覆盖了嘴唇周围和部分口腔区域,舌体虽然不在标准关键点里,但我们可以基于嘴唇和下巴的关键点做区域推断,再配合自训练的分割模型做精细化的舌体 mask 提取。MediaPipe 的好处是轻量、跨平台、CPU 上就能跑,这对后续部署到移动端或者边缘设备非常关键。
VL 模型(视觉语言模型)则是解决“语义理解”这一层。舌象的特征描述本质上是语言性的——“舌质淡红”“苔薄白”“边有齿痕”,这些是结构化的自然语言描述。VL 模型可以直接把图像和这些描述对齐,我们不需要为每个特征单独训一个分类器,而是用 prompt 的方式让模型输出结构化的舌象描述。这样一来,整个系统的可扩展性就上来了:想加一个新特征,改 prompt 就行,不用重新标注数据训模型。
提示:MediaPipe 和 VL 模型的分工要明确。MediaPipe 负责“在哪里”(定位和分割),VL 模型负责“是什么”(特征识别和语义描述)。两者通过舌体 mask 裁剪后的图像做衔接,不要让 VL 模型去处理整张原始照片,那样噪声太大。
1.2 舌象重构引擎的核心模块拆解
整个引擎我拆成了四个核心模块,每个模块的输入输出都定义清楚,方便单独调试和替换。
第一个模块是图像预处理与质量校验。这一步很多人会忽略,但实际上舌象照片的质量参差不齐,有的曝光过度,有的偏色严重,有的舌头根本没伸出来。我加了一个质量校验环节,用简单的图像统计指标(亮度均值、色彩偏差、清晰度)先做一轮筛选,不合格的直接打回让用户重拍,避免后面所有模块都在垃圾数据上跑。
第二个模块是舌体定位与分割。用 MediaPipe 的人脸关键点先定位嘴部区域,然后在这个 ROI 里跑舌体分割。分割我用的是轻量级的 U-Net 变体,输入是嘴部 ROI,输出是舌体的二值 mask。这里有个细节:舌体和嘴唇、牙齿的颜色有时候很接近,单纯靠颜色分割会出问题,所以我在训练数据里特意加了很多“舌头贴着嘴唇”“舌头被牙齿挡住一部分”的样本,让模型学会区分。
第三个模块是舌象特征提取与结构化。拿到舌体 mask 之后,裁剪出纯净的舌体图像,然后分两路走:一路做传统的颜色和纹理特征计算(比如舌质的 LAB 均值、舌苔区域的纹理熵),另一路送给 VL 模型做语义描述生成。两路结果做交叉验证,比如传统方法算出舌质偏淡,VL 模型也说“舌质淡白”,那这个结论的可信度就高。
第四个模块是重构与可视化输出。这一步是把前面所有中间结果整合起来,生成一个“舌象重构报告”。报告里不只是文字结论,还包括舌体轮廓叠加图、舌质舌苔分区图、各特征维度的量化数值。这样医生或者用户能看到完整的推理链条,而不是一个黑盒结论。
1.3 数据流与接口设计的关键考量
数据流的设计我踩过不少坑。最开始我是把整张照片直接丢给 VL 模型,结果模型经常被背景里的东西干扰,比如患者穿了红色衣服,模型就倾向于说“舌质红”。后来改成先分割再送 VL,准确率明显上来了。
接口设计上,我坚持每个模块都输出结构化的 JSON,而不是图片或者纯文本。比如舌体分割模块输出的是 mask 的 RLE 编码加边界框坐标,特征提取模块输出的是各特征的数值和置信度。这样做的好处是整条链路可追溯,哪个环节出了问题,看中间输出就能定位。
还有一个关键点是坐标系的一致性。MediaPipe 输出的关键点是归一化坐标,分割模型输出的是像素坐标,VL 模型处理的是裁剪后的图像。这三个坐标系之间的转换必须严格对齐,否则最后可视化的时候轮廓会飘。我的做法是定义一个统一的“舌象坐标系”,所有模块的输出都转换到这个坐标系下再做融合。
2. MediaPipe 舌体定位的实操细节
2.1 环境搭建与依赖安装的避坑指南
MediaPipe 的安装看起来简单,但实际踩坑不少。我用的环境是 Python 3.9 加 MediaPipe 0.10.x 版本,这个组合在我这边实测最稳。太新的 Python 版本有时候会遇到 wheel 包不兼容的问题,太旧的 MediaPipe 版本又缺少一些关键 API。
安装命令本身不复杂:
pip install mediapipe==0.10.9 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3但有几个坑要提前说。第一,MediaPipe 对 numpy 版本有要求,如果你环境里已经有其他包依赖了更高版本的 numpy,可能会冲突。我的建议是单独建一个虚拟环境,别跟其他项目混在一起。第二,OpenCV 的版本也要注意,有些版本的cv2.imshow在无头服务器上会报错,如果你是在服务器上跑,记得用opencv-python-headless。第三,MediaPipe 的 GPU 支持在部分平台上配置起来很麻烦,我实测下来 CPU 版本对于舌体定位这个任务已经足够了,没必要折腾 GPU。
注意:如果你在 macOS 上安装 MediaPipe 遇到问题,先检查一下是不是 Apple Silicon 芯片。早期版本的 MediaPipe 对 M 系列芯片的支持不太好,需要升级到较新的版本。另外,安装完之后跑一个最简单的 Face Mesh 示例验证一下,别等到集成到项目里才发现环境有问题。
2.2 人脸关键点检测与嘴部 ROI 提取
MediaPipe Face Mesh 会输出 468 个面部关键点,其中跟嘴部相关的关键点索引大概在 61 到 291 这个范围。我实际用到的几个关键索引是:61(左嘴角)、291(右嘴角)、13(上唇内侧)、14(下唇内侧)、17(下唇下缘)、0(上唇上缘)。这几个点连起来大致能框出嘴部区域。
但这里有个问题:舌体伸出的时候,嘴部区域的实际范围会比静止状态大不少。如果直接用静止状态的关键点做 ROI,舌头尖部经常会被裁掉。我的做法是在关键点基础上做一个动态扩展,扩展系数根据上下唇关键点的距离来算。具体来说,如果上下唇距离是 d,那 ROI 的高度就取 2.5d,宽度取左右嘴角距离的 1.3 倍,中心点取上下唇中点的位置。
import mediapipe as mp import cv2 import numpy as np mp_face_mesh = mp.solutions.face_mesh def extract_mouth_roi(image, expand_ratio=2.5): with mp_face_mesh.FaceMesh( static_image_mode=True, max_num_faces=1, refine_landmarks=True, min_detection_confidence=0.5 ) as face_mesh: results = face_mesh.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) if not results.multi_face_landmarks: return None landmarks = results.multi_face_landmarks[0] h, w = image.shape[:2] # 关键点索引 left_mouth = landmarks.landmark[61] right_mouth = landmarks.landmark[291] upper_lip = landmarks.landmark[13] lower_lip = landmarks.landmark[14] # 转换为像素坐标 lx, ly = int(left_mouth.x * w), int(left_mouth.y * h) rx, ry = int(right_mouth.x * w), int(right_mouth.y * h) ux, uy = int(upper_lip.x * w), int(upper_lip.y * h) dx, dy = int(lower_lip.x * w), int(lower_lip.y * h) # 计算 ROI center_x = (lx + rx) // 2 center_y = (uy + dy) // 2 mouth_width = abs(rx - lx) mouth_height = abs(dy - uy) roi_w = int(mouth_width * 1.3) roi_h = int(mouth_height * expand_ratio) x1 = max(0, center_x - roi_w // 2) y1 = max(0, center_y - roi_h // 2) x2 = min(w, center_x + roi_w // 2) y2 = min(h, center_y + roi_h // 2) return image[y1:y2, x1:x2], (x1, y1, x2, y2)这段代码里expand_ratio这个参数是可以调的。我实测下来 2.5 对于大多数伸舌照片够用,但如果用户伸舌幅度特别大,可能需要调到 3.0。反过来如果只是微微伸舌,2.0 就够了。这个参数我建议做成可配置的,根据实际数据分布来定。
2.3 舌体分割模型的训练与调优
MediaPipe 本身不提供舌体分割,所以这一步需要自己训一个分割模型。我用的是轻量级 U-Net,输入是 256x256 的嘴部 ROI,输出是同尺寸的二值 mask。模型结构不复杂,编码器用 MobileNetV3 的前几层,解码器用转置卷积逐步上采样,最后接一个 sigmoid 输出。
训练数据这块是最花时间的。我前后标注了大概 2000 张舌象照片,标注的标准是:舌体可见区域全部标为前景,被牙齿遮挡的部分标为背景,舌苔和舌质不做区分(那是后面特征提取的事)。标注工具用的是 LabelMe,导出成 COCO 格式再转成 mask。
训练时的损失函数我用的是 Dice Loss 加 BCE Loss 的组合,Dice 负责整体区域的重合度,BCE 负责像素级的分类精度。学习率用余弦退火,初始 1e-3,训练 100 个 epoch。数据增强方面,我加了随机亮度调整、随机对比度调整、随机旋转(正负 15 度)、随机缩放(0.9 到 1.1)。这里特别说一下,不要加水平翻转,因为舌象的左右特征(比如齿痕的位置)是有临床意义的,翻转会破坏这个信息。
训练完之后,在验证集上 IoU 大概能到 0.89 左右,对于舌体分割这个任务来说够用了。推理的时候,把嘴部 ROI 缩放到 256x256 送进模型,输出的 mask 再缩放回原始 ROI 尺寸,最后映射回整张照片的坐标系。
实操心得:舌体分割最容易出错的情况是“舌头和嘴唇颜色接近”。我的解决办法是在训练数据里刻意增加这类难样本的权重,同时在推理后处理阶段加一个形态学操作,把分割结果中面积过小的连通域去掉,避免把嘴唇误判成舌体。
3. VL 模型驱动的舌象特征结构化
3.1 VL 模型的选型与本地化部署
VL 模型这块,我试过好几个方案。闭源 API 的好处是效果好、接入快,但问题是数据隐私和成本。舌象照片属于健康数据,传到第三方服务上总归不太放心,而且调用量大了成本也不低。所以后来我转向了开源 VL 模型的本地化部署。
目前我在用的是基于 LLaVA 架构的轻量级 VL 模型,参数量在 7B 左右,量化到 4bit 之后显存占用大概 6GB,一张消费级显卡就能跑起来。这个模型对中文的支持还不错,而且可以针对舌象领域做微调。如果你不想自己部署,也有一些在线的免费 VL 模型可以试用,但长期来看还是本地部署更可控。
部署方式我用的是 FastAPI 加 vLLM 的组合。vLLM 负责模型推理的批处理和显存管理,FastAPI 负责对外提供 HTTP 接口。这样舌象重构引擎的其他模块可以通过 HTTP 请求的方式调用 VL 模型,解耦做得比较干净。
from fastapi import FastAPI, UploadFile from PIL import Image import io app = FastAPI() @app.post("/analyze_tongue") async def analyze_tongue(file: UploadFile): image_bytes = await file.read() image = Image.open(io.BytesIO(image_bytes)) # 构建 prompt prompt = """请分析这张舌象图片,按以下JSON格式输出: { "tongue_color": "舌质颜色描述", "coating_color": "舌苔颜色描述", "coating_thickness": "舌苔厚薄描述", "tongue_shape": "舌形描述", "teeth_marks": "是否有齿痕", "cracks": "是否有裂纹" } 只输出JSON,不要其他内容。""" # 调用 VL 模型(这里省略具体推理代码) result = vl_model.generate(image, prompt) return result3.2 Prompt 工程与舌象特征的结构化输出
Prompt 的设计直接决定了 VL 模型输出的质量。我前后改了十几版 prompt,总结下来有几个关键点。
第一,输出格式必须严格约束。VL 模型有时候会自由发挥,加一些“根据图片来看”“总的来说”之类的废话。我在 prompt 里明确写了“只输出JSON,不要其他内容”,并且在解析的时候加了容错处理,如果 JSON 解析失败就重试一次。
第二,特征维度要定义清楚。中医舌象的特征维度其实挺多的,但不是什么都要让 VL 模型来做。我筛选了六个核心维度:舌质颜色、舌苔颜色、舌苔厚薄、舌形、齿痕、裂纹。这六个维度是临床最常用的,也是 VL 模型比较擅长的。更细的特征比如“舌苔的腐腻程度”这种,我暂时还是交给传统图像特征来做。
第三,要给模型提供参考标准。我在 prompt 里会附上每个维度的可选值范围,比如舌质颜色限定在“淡白、淡红、红、绛红、青紫”这几个选项里。这样模型的输出会更规范,后续做结构化解析也更容易。
TONGUE_ANALYSIS_PROMPT = """你是一位中医舌诊专家。请分析这张舌象图片,严格按照以下要求输出。 舌质颜色:从[淡白, 淡红, 红, 绛红, 青紫]中选择 舌苔颜色:从[白, 黄, 灰, 黑]中选择 舌苔厚薄:从[薄, 厚, 无苔]中选择 舌形:从[正常, 胖大, 瘦小, 老嫩]中选择 齿痕:从[有, 无]中选择 裂纹:从[有, 无]中选择 输出格式为JSON: { "tongue_color": "...", "coating_color": "...", "coating_thickness": "...", "tongue_shape": "...", "teeth_marks": "...", "cracks": "..." } 只输出JSON,不要任何解释。"""3.3 传统图像特征与 VL 语义的交叉验证
单靠 VL 模型有一个问题:它的输出有时候不稳定。同一张图片,稍微改一下 prompt 的措辞,输出可能就不一样。为了解决这个问题,我加了一路传统图像特征计算,用来跟 VL 模型的输出做交叉验证。
传统特征这块,舌质颜色我用的是舌体 mask 中心区域的 LAB 色彩空间的均值和标准差。为什么用 LAB 而不是 RGB?因为 LAB 的色彩感知更接近人眼,而且 L 通道和 AB 通道分离,光照变化主要影响 L 通道,对 AB 通道影响小,这样颜色判断更稳。舌苔厚薄我用的是舌体区域的纹理熵,苔厚的地方纹理更粗糙,熵值更高。
交叉验证的逻辑是这样的:如果传统特征算出舌质偏淡(L 值高,A 值低),而 VL 模型也说“淡白”,那这个结论的置信度就高。如果两者矛盾,比如传统特征说偏红但 VL 说淡白,那就标记为“低置信度”,在最终报告里提示需要人工复核。
def cross_validate(traditional_features, vl_result): confidence = {} # 舌质颜色交叉验证 lab_mean = traditional_features['lab_mean'] vl_color = vl_result['tongue_color'] if lab_mean['a'] < 128 and vl_color in ['淡白', '淡红']: confidence['tongue_color'] = 'high' elif lab_mean['a'] > 140 and vl_color in ['红', '绛红']: confidence['tongue_color'] = 'high' else: confidence['tongue_color'] = 'low' # 其他维度的交叉验证逻辑类似 return confidence提示:交叉验证的阈值不是拍脑袋定的,我是拿了一批有专家标注的舌象数据,统计了传统特征和专家判断的对应关系之后才确定的。如果你自己的数据分布不一样,这些阈值需要重新标定。
4. 舌象重构引擎的完整实操流程
4.1 从原始照片到结构化报告的端到端流程
把前面所有模块串起来,整个流程大概是这样的。用户上传一张舌象照片,系统先做质量校验,检查亮度、清晰度、色彩偏差是否在可接受范围内。如果照片不合格,直接返回提示让用户重拍,并给出具体的改进建议(比如“光线太暗,请在自然光下拍摄”)。
质量校验通过之后,进入舌体定位环节。MediaPipe 检测人脸关键点,提取嘴部 ROI,然后舌体分割模型输出舌体 mask。如果 mask 面积占比太小(比如小于 ROI 的 15%),说明舌头可能没伸出来或者被遮挡太多,同样打回重拍。
拿到舌体 mask 之后,裁剪出纯净的舌体图像,同时做两件事:一是计算传统图像特征,二是调用 VL 模型做语义分析。两路结果汇总之后做交叉验证,生成置信度评分。最后把所有信息整合成一个结构化的 JSON 报告,包括舌体轮廓图、特征数值、语义描述、置信度。
整个流程在 CPU 上跑大概需要 2 到 3 秒,其中 VL 模型推理占了大头。如果对实时性要求高,可以把 VL 模型换成更小的版本,或者用批处理的方式做异步推理。
4.2 关键参数的计算与调优记录
这里记录几个我在调优过程中反复调整的关键参数,供你参考。
舌体分割的置信度阈值:模型输出的 mask 是概率图,需要设一个阈值来二值化。我试过 0.5、0.6、0.7 三个值,最后定在 0.6。0.5 的话边缘会多出一些噪声区域,0.7 的话舌体边缘会被侵蚀掉一圈。0.6 是比较好的平衡点。
嘴部 ROI 的扩展系数:前面提到过,我最终用的是 2.5。这个值是在 500 张测试图上统计出来的,能覆盖 95% 以上的伸舌情况。如果你的人群伸舌习惯不一样,可以重新统计。
VL 模型的 temperature 参数:这个参数控制输出的随机性。舌象分析需要的是稳定、可复现的结果,所以我把 temperature 设得很低,0.1。这样同一张图片多次推理的结果基本一致。
交叉验证的置信度阈值:传统特征和 VL 输出一致时标为高置信度,不一致时标为低置信度。但“一致”的判断标准需要量化。我的做法是给每个特征维度定义一个数值区间,传统特征落在对应区间内就算一致。
| 参数名称 | 取值范围 | 最终取值 | 调整依据 |
|---|---|---|---|
| 分割置信度阈值 | 0.5-0.7 | 0.6 | 边缘噪声与侵蚀的平衡 |
| ROI 扩展系数 | 2.0-3.0 | 2.5 | 覆盖 95% 伸舌情况 |
| VL temperature | 0.1-1.0 | 0.1 | 保证输出稳定性 |
| 最小舌体面积占比 | 10%-20% | 15% | 过滤无效照片 |
4.3 可视化输出的设计与实现
可视化这块我花了不少心思,因为这是让用户信任系统的关键。如果只给一个文字结论,用户会觉得是黑盒。但如果能看到舌体的轮廓线、舌质舌苔的分区着色、每个特征的数值,信任感就完全不一样了。
我的可视化输出包含三层。第一层是原始照片叠加舌体轮廓,让用户确认系统确实找到了舌头。第二层是舌体区域的放大图,上面用不同颜色标注舌质区和舌苔区,颜色深浅对应特征强度。第三层是特征雷达图,六个维度的数值一目了然。
实现上,轮廓叠加用 OpenCV 的findContours加drawContours就行。分区着色稍微麻烦一点,需要根据舌苔的纹理特征做一个软分割,然后用addWeighted做半透明叠加。雷达图我用的是 matplotlib,生成图片之后转成 base64 嵌入到报告里。
import cv2 import numpy as np import matplotlib.pyplot as plt def visualize_tongue_analysis(image, mask, features): # 第一层:轮廓叠加 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) overlay = image.copy() cv2.drawContours(overlay, contours, -1, (0, 255, 0), 2) # 第二层:舌体区域放大 x, y, w, h = cv2.boundingRect(contours[0]) tongue_crop = image[y:y+h, x:x+w] tongue_crop = cv2.resize(tongue_crop, (300, 300)) # 第三层:雷达图 categories = ['舌质颜色', '舌苔颜色', '舌苔厚薄', '舌形', '齿痕', '裂纹'] values = [features[c] for c in categories] angles = np.linspace(0, 2 * np.pi, len(categories), endpoint=False).tolist() values += values[:1] angles += angles[:1] fig, ax = plt.subplots(figsize=(4, 4), subplot_kw=dict(polar=True)) ax.plot(angles, values, 'o-', linewidth=2) ax.fill(angles, values, alpha=0.25) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories) return overlay, tongue_crop, fig5. 常见问题排查与避坑经验
5.1 舌体分割失败的典型场景与修复
舌体分割失败是我遇到最多的问题,大概有这么几种典型场景。
第一种是舌头和背景颜色接近。比如用户穿了白色衣服,舌头又偏淡,分割模型就容易把衣服的一部分也框进来。解决办法是在 ROI 提取阶段就把范围收紧,别让背景进来太多。另外可以在分割后处理阶段加一个约束:舌体 mask 必须跟嘴部 ROI 的中心区域有足够大的重叠,否则判定为分割失败。
第二种是舌头伸出幅度太小。有些用户只是微微张嘴,舌头几乎没伸出来,这种情况下分割模型只能看到一点点舌体,mask 面积很小。我的处理是设一个最小面积阈值,低于这个阈值就直接返回“请重新拍摄,舌头尽量伸出”。
第三种是光线导致的颜色失真。强光下舌头会过曝,暗光下又会偏色。我在预处理阶段加了一个简单的自动白平衡,用灰度世界算法做初步校正。如果校正后的色彩偏差还是很大,就提示用户换光线重拍。
实操心得:分割失败的照片不要直接丢弃,可以存下来做难样本挖掘。我定期会把分割失败的照片拿出来重新标注,加入训练集,这样模型的鲁棒性会逐步提升。
5.2 VL 模型输出不稳定的应对策略
VL 模型输出不稳定主要表现在两个方面:一是同一张图片多次推理结果不一致,二是输出格式不符合要求。
对于第一个问题,降低 temperature 是最直接的办法。我把 temperature 从默认的 0.7 降到了 0.1,输出的一致性明显改善。另外可以加一个“多次推理投票”的机制,同一张图片跑三次,取多数结果作为最终输出。这样虽然会增加推理时间,但稳定性提升很明显。
对于第二个问题,prompt 的约束要足够强。我在 prompt 里不仅指定了 JSON 格式,还给了具体的字段名和可选值。如果模型还是输出不符合格式的内容,我会在解析失败时自动重试,重试时在 prompt 里加上“上次输出格式错误,请严格按照JSON格式输出”的提示。
还有一个技巧是给模型提供 few-shot 示例。在 prompt 里放一两个输入输出的例子,模型会更容易理解你想要什么格式。我实测下来,加了 few-shot 之后,格式错误的概率从大概 15% 降到了 3% 以下。
5.3 性能优化与部署注意事项
性能这块,瓶颈主要在 VL 模型推理。7B 的模型量化到 4bit 之后,单次推理大概 1.5 到 2 秒。如果并发量上来了,这个延迟是扛不住的。我的优化方案是用 vLLM 做批处理,把多个请求攒在一起推理,吞吐量能提升 3 到 5 倍。
MediaPipe 和分割模型这块性能压力不大,CPU 上跑一次大概 200 到 300 毫秒。如果部署在移动端,MediaPipe 有专门的移动端 SDK,分割模型也可以转成 TFLite 格式,进一步压缩体积和加速。
部署架构上,我建议把 VL 模型单独部署成一个服务,跟其他模块解耦。这样 VL 模型可以独立扩缩容,其他模块也可以用更轻量的方式部署。接口用 HTTP 或者 gRPC 都行,看你的技术栈。
| 问题类型 | 典型表现 | 排查思路 | 解决方案 |
|---|---|---|---|
| 分割失败 | mask 面积过小或包含背景 | 检查 ROI 范围和分割阈值 | 收紧 ROI,调整阈值,加后处理约束 |
| VL 输出不稳定 | 多次推理结果不一致 | 检查 temperature 和 prompt | 降低 temperature,加 few-shot,多次投票 |
| 颜色判断偏差 | 舌质颜色与肉眼不符 | 检查白平衡和色彩空间 | 加自动白平衡,用 LAB 空间做判断 |
| 推理速度慢 | 端到端超过 5 秒 | 定位瓶颈模块 | VL 模型批处理,分割模型量化 |
6. 后续扩展方向与个人实践体会
这套引擎目前跑通的是单张静态照片的分析,但舌象其实是一个动态变化的东西。同一个人早上和晚上的舌象可能就不一样,吃了不同东西之后舌苔也会变。所以下一步我想做的是时序舌象分析,让用户定期拍摄,系统追踪舌象特征的变化趋势。这个对慢性病管理和健康监测挺有价值的。
另一个方向是多模态融合。舌象只是中医望诊的一部分,如果能把面诊、脉诊(通过可穿戴设备采集)的数据也融合进来,整个健康评估的维度会更完整。技术上就是把不同模态的特征在 VL 模型的 embedding 空间里做对齐,这个我还在探索阶段。
最后说一点个人体会。做中医数字化这件事,最大的挑战不是技术,而是怎么把中医的经验性描述转化成可计算的特征。我一开始总想着用纯深度学习端到端地做,后来发现不行,中医的很多概念是有明确内涵的,你得把这些内涵拆解成机器能理解的特征,再用模型去学。MediaPipe 和 VL 模型的组合之所以有效,就是因为它们分别解决了“定位”和“语义”这两个关键问题,而不是试图用一个模型解决所有问题。这个思路我觉得可以迁移到很多类似的场景里。