资讯动态

基于MediaPipe与VL的中医舌象重构引擎:从黑盒到白盒计算

发布时间:2026/10/14 14:40:08 来源:尧图企业网站定制
中医舌诊这件事过去很长一段时间里都停留在老师傅带徒弟的口传心授阶段。舌头伸出来淡红、薄白、齿痕、裂纹这些词听起来简单但真要量化十个人能给你十种描述。我做了几年计算机视觉相关的项目一直想碰这个方向但每次都被同一个问题卡住舌象的特征太软了颜色、纹理、形态、润燥每一样都不是单一指标能说清楚的。直到我把MediaPipe和视觉语言模型VL这两套东西拼到一起才算真正把这件事从黑盒盲猜推进到了白盒计算。这篇文章想聊的就是这套基于MediaPipe VL的中医舌象重构引擎。所谓重构引擎不是简单拍张舌头照片丢给模型出个结论而是把舌体分割、关键点定位、区域划分、特征量化、语义描述这几步串成一条可解释的流水线。它解决的核心问题是让舌象分析从我觉得你有点脾虚这种模糊判断变成舌体区域HSV均值落在某区间、裂纹像素占比X%、舌苔覆盖率Y%这种可复现、可追溯的计算结果。适合谁看做中医数字化、健康类App、AI辅助诊断的开发者以及想了解视觉语言模型怎么落地到具体垂直场景的工程师。哪怕你只是好奇AI看舌头到底靠不靠谱看完也能有个清晰的判断。1. 为什么舌象分析需要重构而不是识别1.1 传统舌诊数字化的三个死结先说清楚为什么直接上分类模型这条路走不通。我早期试过最朴素的做法收集几千张舌头照片打上脾虚湿热气虚这类标签然后训一个ResNet做分类。结果惨不忍睹准确率在测试集上勉强六成换个手机拍同一批人直接掉到四成以下。问题出在三个地方。第一个死结是标签的主观性。同一个舌头A医生说是淡白舌B医生可能觉得是淡红偏淡这种标签噪声对分类模型是致命的。模型学到的不是舌象本身的规律而是标注者的个人偏好。第二个死结是成像条件不可控。手机白平衡、环境光色温、拍摄距离、伸舌力度任何一个变量变了像素值就全变了。分类模型对颜色极度敏感你没法保证用户每次都在同一个灯下拍。第三个死结是缺乏中间表征。分类模型是个黑盒它直接给你一个结论但你不知道它凭什么这么判。中医师不认用户也不信监管更过不去。所以重构这个词是关键。我们要做的不是让模型直接输出诊断而是重建出一套结构化的、可解释的舌象中间表征让后续的判断建立在这些可验证的数据之上。1.2 白盒计算的核心思路先结构化再语义化整套引擎的设计哲学就一句话几何和颜色的事交给传统CV语义和推理的事交给VL模型。这两者分工明确各干各擅长的事。MediaPipe负责的是白盒部分——它把舌头从一张杂乱的自拍里抠出来定位关键点划分出舌尖、舌中、舌边、舌根这些区域然后在这些区域上做像素级的统计。这部分是纯数学输入确定、输出确定可复现。VL模型负责的是语义部分——它拿到结构化数据后结合图像本身输出符合中医术语体系的描述比如舌质偏淡舌体略胖边有齿痕苔薄白。这部分是概率性的但它建立在前面确定性的数据之上所以结论有据可查。提示这个分工不是拍脑袋定的。我试过让VL模型直接从原图出结论结果它对颜色和比例的判断极不稳定同一张图问两次能给两个答案。把量化工作前置到CV环节VL只做它擅长的语义归纳稳定性提升非常明显。1.3 这套架构适合什么场景不适合什么场景得把边界说清楚免得有人拿去做医疗诊断然后出事。适合的场景健康管理类App的舌象记录与趋势追踪、中医教学中的舌象标准化演示、科研中的舌象数据采集与量化分析、养生内容平台的个性化建议生成。不适合的场景任何形式的临床诊断替代、需要承担医疗责任的判断、对精度要求达到医疗器械级别的应用。这套引擎输出的是特征描述和倾向性参考不是诊断结论。这一点在产品和合规上必须拎清楚。2. 核心模块拆解MediaPipe到底做了什么2.1 舌体分割为什么不用现成的分割模型很多人第一反应是上SAM或者U-Net做舌体分割。我试过效果不是不好而是太重。SAM跑一张图在普通设备上要好几秒而且它对舌头的边界理解并不比专门调过的方案好。舌头的分割有个特殊性它和嘴唇、口腔内壁、牙齿的颜色接近边界模糊通用分割模型经常把嘴唇一起框进去。我最后采用的是MediaPipe Face Mesh 自定义区域约束的组合。Face Mesh能给出468个面部关键点其中嘴唇内外轮廓的点位非常精确。舌头必然在嘴唇轮廓内部这就给了一个强约束。在此基础上我用嘴唇内轮廓作为初始ROI再结合颜色阈值舌头通常是偏红偏粉的色调饱和度高于口腔暗部做精细分割。具体做法是先取Face Mesh的嘴唇内轮廓点索引大致在78、80、81、82、84、87、88、91、95、146、178、181、185、191、308、310、311、312、317、318、321、324、375、402、405、415这些位置附近连成多边形作为mask。然后在这个mask内部做HSV空间的阈值分割把舌头和口腔暗部分开。import mediapipe as mp import cv2 import numpy as np mp_face_mesh mp.solutions.face_mesh face_mesh mp_face_mesh.FaceMesh( static_image_modeTrue, max_num_faces1, refine_landmarksTrue, min_detection_confidence0.5 ) # 嘴唇内轮廓关键点索引 INNER_LIP_IDX [78, 80, 81, 82, 84, 87, 88, 91, 95, 146, 178, 181, 185, 191, 308, 310, 311, 312, 317, 318, 321, 324, 375, 402, 405, 415] def extract_tongue_roi(image_bgr): h, w image_bgr.shape[:2] rgb cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) result face_mesh.process(rgb) if not result.multi_face_landmarks: return None, None landmarks result.multi_face_landmarks[0] pts [] for idx in INNER_LIP_IDX: lm landmarks.landmark[idx] pts.append([int(lm.x * w), int(lm.y * h)]) pts np.array(pts, dtypenp.int32) mask np.zeros((h, w), dtypenp.uint8) cv2.fillPoly(mask, [pts], 255) # HSV阈值进一步分离舌体 hsv cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV) tongue_mask cv2.inRange(hsv, (0, 40, 60), (25, 255, 255)) tongue_mask cv2.bitwise_and(tongue_mask, mask) return tongue_mask, pts这段代码是整个流水线的入口。实测下来只要用户正脸对着镜头、舌头自然伸出ROI提取的成功率在九成以上。失败的情况主要是侧脸、遮挡、或者舌头伸得太少。2.2 关键点定位与舌体区域划分拿到舌体mask之后下一步是把它划分成有中医意义的区域。传统舌诊把舌面分为**舌尖心肺、舌中脾胃、舌边肝胆、舌根肾**四个功能区。这个划分不是随便切的它对应着不同的脏腑映射。我的做法是用舌体的外接矩形和主轴方向做几何划分。先对mask做PCA求主轴把舌头摆正然后按比例切分舌尖占前1/3舌中占中间1/3舌根占后1/3舌边则是左右两侧各15%宽度的条带。def divide_tongue_regions(mask): ys, xs np.where(mask 0) if len(xs) 100: return None pts np.column_stack([xs, ys]).astype(np.float32) mean, eigenvectors cv2.PCACompute(pts, meanNone) # 投影到主轴 proj (pts - mean) eigenvectors.T main_axis proj[:, 0] cross_axis proj[:, 1] # 按主轴分三段 t1 np.percentile(main_axis, 33) t2 np.percentile(main_axis, 66) regions {} regions[tip] pts[main_axis t1] regions[middle] pts[(main_axis t1) (main_axis t2)] regions[root] pts[main_axis t2] # 舌边横向投影的两端 edge_thresh np.percentile(np.abs(cross_axis), 85) regions[edge_left] pts[cross_axis -edge_thresh] regions[edge_right] pts[cross_axis edge_thresh] return regions这里有个坑要提醒PCA主轴方向可能翻转导致舌尖和舌根搞反。解决办法是结合Face Mesh的鼻子朝向做校正——舌尖一定离鼻子更远。这个校正逻辑不加后面所有区域特征都是错的。2.3 特征量化把望诊变成数字区域划分好之后就是逐区域提取特征。我目前用的特征集分四大类每一类都对应中医望诊里的具体观察项。颜色特征在HSV和Lab两个空间里算均值和标准差。舌质的颜色主要看H通道和a通道Lab里的红绿轴舌苔的颜色看亮度和饱和度。为什么要两个空间都算因为HSV对光照变化更敏感但直观Lab更接近人眼感知且对光照相对鲁棒两者结合能互相校验。纹理特征用LBP局部二值模式和灰度共生矩阵算粗糙度、对比度、熵。这些指标对应的是舌面的老嫩胖瘦和裂纹的多少。形态特征舌体的长宽比、面积占比、边缘的凹凸度。齿痕的检测就是看边缘的周期性凹陷——对边缘轮廓做傅里叶变换如果高频分量有规律性的峰值大概率是齿痕。苔质特征舌苔覆盖率的计算稍微复杂一点。我的做法是在舌体区域内用颜色阈值区分苔和质——苔通常偏白偏黄、亮度高、饱和度低质偏红、饱和度高。两者的像素比例就是苔质比。def extract_features(image_bgr, mask, regions): hsv cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV) lab cv2.cvtColor(image_bgr, cv2.COLOR_BGR2LAB) feats {} for name, pts in regions.items(): if len(pts) 50: continue region_mask np.zeros(mask.shape, dtypenp.uint8) region_mask[pts[:, 1], pts[:, 0]] 255 h_vals hsv[:, :, 0][region_mask 0] s_vals hsv[:, :, 1][region_mask 0] v_vals hsv[:, :, 2][region_mask 0] a_vals lab[:, :, 1][region_mask 0] feats[f{name}_h_mean] float(np.mean(h_vals)) feats[f{name}_s_mean] float(np.mean(s_vals)) feats[f{name}_v_mean] float(np.mean(v_vals)) feats[f{name}_a_mean] float(np.mean(a_vals)) feats[f{name}_v_std] float(np.std(v_vals)) return feats这套特征提取下来一张舌头能产出上百个数值。这些数值就是白盒的底气——每一个结论都能追溯到具体的数字。3. VL模型接入让数字说人话3.1 VL模型在这套引擎里的角色定位前面说了VL模型不做量化它做的是语义归纳和术语映射。具体来说它接收两样东西原始舌象图像以及MediaPipe产出的结构化特征JSON。然后输出一段符合中医术语体系的描述文本。为什么不让VL直接看图说话因为VL模型对颜色和比例的判断是印象派的。你问它这个舌头红不红它可能说偏红但偏红到底是RGB多少它给不出。而中医舌诊恰恰需要这种精确性。所以我的设计是VL模型负责把数字翻译成术语而不是自己产生数字。举个例子特征JSON里可能有tip_a_mean: 145.2Lab a通道值越高越红VL模型看到这个值结合它对中医舌诊的知识输出舌尖偏红提示心肺有热倾向。这个145.2是MediaPipe算的偏红和心肺有热是VL模型基于知识做的映射。两者结合既有数据支撑又有语义解释。3.2 提示词工程怎么让VL模型说中医话VL模型的输出质量八成取决于提示词。我踩了很多坑才摸出一套相对稳定的模板。核心原则是给足上下文、限定输出格式、提供术语表、要求引用数据。提示词的结构大致是这样的你是一名中医舌诊辅助分析助手。请基于提供的舌象图像和结构化特征数据 输出一段舌象描述。 【结构化特征数据】 {feature_json} 【术语参考】 - 舌色淡白、淡红、红、绛、紫 - 舌形胖大、瘦薄、老嫩、齿痕、裂纹、点刺 - 苔色白、黄、灰黑 - 苔质薄、厚、腻、腐、剥落、润、燥 【输出要求】 1. 按舌质-舌形-舌苔-综合倾向四段输出 2. 每个判断必须引用至少一个具体特征值 3. 不确定的地方标注参考不要强行下结论 4. 不输出任何诊断性结论只做特征描述和倾向提示这个模板里**每个判断必须引用至少一个具体特征值**是最关键的一条。它逼着VL模型不能凭空发挥必须锚定在数据上。实测下来加了这条约束之后输出的可解释性提升了一个档次。3.3 结构化输出与后处理VL模型的原始输出是自然语言但下游应用往往需要结构化数据。所以我在VL输出之后加了一层解析把舌质-舌形-舌苔-综合倾向四段拆成JSON字段。import json import re def parse_vl_output(text): sections {tongue_body: , tongue_shape: , tongue_coating: , tendency: } patterns { tongue_body: r舌质[:]\s*(.?)(?舌形|$), tongue_shape: r舌形[:]\s*(.?)(?舌苔|$), tongue_coating: r舌苔[:]\s*(.?)(?综合|$), tendency: r综合倾向[:]\s*(.) } for key, pat in patterns.items(): m re.search(pat, text, re.S) if m: sections[key] m.group(1).strip() return sections后处理还要做一层术语归一化。VL模型有时候会说舌体稍胖有时候说舌略胖大这些得映射到统一的术语表里否则下游没法做统计分析。4. 完整实操流程从一张自拍到一份舌象报告4.1 环境准备与依赖安装先把环境搭起来。MediaPipe的安装是第一个坎不同平台差异挺大。# Python环境建议3.9-3.11 pip install mediapipe opencv-python numpy # 如果要用GPU加速的VL模型按所选框架装 pip install torch transformers注意MediaPipe对Python版本比较挑3.12有时候装不上建议用3.10或3.11。另外在ARM架构的设备上比如某些开发板需要从源码编译会麻烦一些。VL模型的选择上如果追求本地部署可以选参数量在7B左右的视觉语言模型如果追求效果用API调用大模型更省事。我两种都试过本地部署的好处是数据不出本地适合对隐私敏感的场景API的好处是省算力、效果稳定。4.2 拍摄规范这一步做不好后面全白搭我必须强调拍摄规范的重要性。前面所有的算法优化都抵不过一张拍废的照片。经过大量测试我总结了几条硬性要求光线自然光或接近自然光的白光避免暖黄灯和彩色光。色温偏差会直接污染颜色特征。距离舌头占画面高度的1/3到1/2太远细节丢失太近畸变严重。角度正脸平视镜头与舌面基本平行避免俯拍或仰拍。伸舌自然伸出不要过度用力用力会让舌色变深舌尖略向下。时长伸舌不要超过10秒时间长了舌色会因充血变化。我在App里加了一个拍摄引导界面实时检测人脸位置和光照条件不达标就不让拍。这个引导把有效样本率从不到五成提到了八成以上。4.3 完整流水线代码把前面的模块串起来就是完整的处理流程def analyze_tongue(image_path): image cv2.imread(image_path) if image is None: return {error: 图像读取失败} # 1. 舌体ROI提取 tongue_mask, lip_pts extract_tongue_roi(image) if tongue_mask is None: return {error: 未检测到有效舌体} # 2. 区域划分 regions divide_tongue_regions(tongue_mask) if regions is None: return {error: 舌体区域过小} # 3. 特征提取 features extract_features(image, tongue_mask, regions) # 4. 质量校验 quality check_quality(image, tongue_mask, features) if not quality[passed]: return {error: 图像质量不达标, detail: quality} # 5. VL语义生成 vl_input build_vl_prompt(image, features) vl_output call_vl_model(vl_input) # 6. 结构化解析 structured parse_vl_output(vl_output) return { features: features, description: structured, quality: quality }这个流程跑通之后一张图从输入到输出大概2到5秒取决于VL模型是本地还是API。4.4 质量校验把废片挡在门外质量校验这一步很多人会忽略但它直接决定了整套系统的可信度。我设了三个校验维度清晰度用拉普拉斯方差判断低于阈值说明模糊直接拒绝。光照均匀性把舌体区域分成九宫格算各格亮度均值如果极差超过阈值说明有阴影或高光拒绝。舌体完整度舌体mask面积占嘴唇内轮廓面积的比例低于60%说明舌头没伸够或者被遮挡拒绝。def check_quality(image, mask, features): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) lap_var cv2.Laplacian(gray, cv2.CV_64F).var() ys, xs np.where(mask 0) region gray[ys.min():ys.max(), xs.min():xs.max()] h, w region.shape grid_means [] for i in range(3): for j in range(3): cell region[i*h//3:(i1)*h//3, j*w//3:(j1)*w//3] if cell.size 0: grid_means.append(cell.mean()) light_range max(grid_means) - min(grid_means) if grid_means else 999 return { passed: lap_var 100 and light_range 60, sharpness: float(lap_var), light_range: float(light_range) }阈值不是拍脑袋定的是我拿几百张标注过合格/不合格的图跑出来的。清晰度阈值100、光照极差60这两个数在测试集上的分类准确率能到九成。5. 踩坑实录与常见问题排查5.1 颜色漂移最头疼的问题颜色漂移是这套系统最大的敌人。同一根舌头在窗边拍和在室内灯下拍H通道均值能差20以上直接导致淡红变红。我试过几种方案方案一白平衡校正。用灰卡或者画面里的白色参照物做校正。问题是用户拍摄时不一定有参照物。方案二基于人脸肤色做归一化。人脸肤色相对稳定可以用脸颊区域的Lab值作为基准把舌体颜色归一化到标准光照下。这个方案我最终采用了效果不错但前提是脸要拍全。方案三直接让VL模型忽略绝对颜色只看相对关系。比如舌尖比舌中更红这种相对描述对光照不敏感。我在提示词里加了一条要求VL模型优先输出相对比较。实际用下来方案二加方案三的组合最稳。归一化处理掉大部分光照影响相对描述兜底剩下的偏差。5.2 齿痕检测的误报齿痕是舌边有牙齿压出的凹陷但很多人舌边本来就不规则容易误报。我一开始用边缘凹凸度做判断误报率很高。后来改成傅里叶变换看周期性——齿痕是牙齿压出来的凹陷有规律频率相对固定而自然的不规则边缘没有这种周期性。def detect_tooth_marks(contour): # contour是舌体边缘轮廓点 if len(contour) 20: return False # 转成极坐标下的半径序列 center contour.mean(axis0) radii np.linalg.norm(contour - center, axis1) # 傅里叶变换 fft np.abs(np.fft.rfft(radii - radii.mean())) # 看中频段是否有明显峰值 mid_band fft[3:10] if len(mid_band) 0: return False peak_ratio mid_band.max() / (fft[1:].mean() 1e-6) return peak_ratio 2.5这个阈值2.5也是调出来的配合边缘平滑预处理误报率降到了可接受范围。5.3 VL模型输出不稳定VL模型有个通病同样的输入两次调用可能给不同的输出。这在需要一致性的场景里很要命。我的应对策略有三个降低温度参数。把temperature调到0.1以下输出会稳定很多代价是多样性下降但舌象描述本来就不需要多样性。固定随机种子。如果模型支持设一个固定seed。多次采样取众数。对关键字段比如舌色调用三次取出现最多的那个。这个方案成本高但用在关键判断上值得。5.4 常见问题速查表问题现象可能原因排查方向解决方案检测不到舌体人脸未正对镜头检查Face Mesh是否检出引导用户正脸、光线充足舌体mask包含嘴唇嘴唇内轮廓点不准可视化mask叠加收紧HSV阈值加形态学腐蚀颜色特征波动大光照不一致对比不同光照下同图启用肤色归一化齿痕误报边缘不规则看傅里叶峰值提高峰值阈值加平滑VL输出跑题提示词约束不足检查输出是否引用数据强化必须引用特征值约束舌尖舌根颠倒PCA主轴翻转检查主轴方向用鼻子朝向校正处理速度慢VL模型太大计时各环节换小模型或改API调用5.5 几个容易被忽略的细节伸舌力度影响舌色。用力伸舌会让舌体充血变红这个变化能到10个H通道值以上。所以拍摄引导里一定要强调自然伸出。舌苔的润燥很难量化。润燥本质是舌面反光程度但反光受光源位置影响极大。我目前的做法是用高光像素占比做粗略估计但可靠性一般所以在输出里对润燥只做参考标注不下强结论。不同人群的基准不同。老年人的舌色普遍偏暗儿童的偏红这些是生理差异不是病理。如果做长期追踪最好用个人基线做对比而不是跟人群均值比。数据隐私。舌象照片属于敏感生物特征数据存储和传输必须加密最好本地处理不上传。这一点在产品设计初期就要考虑后期补很麻烦。6. 效果验证与迭代方向6.1 怎么验证这套引擎靠不靠谱验证分两个层面。技术层面看特征提取的重复性——同一张图多次处理特征值应该完全一致确定性部分同一根舌头不同时间拍特征值应该在合理范围内波动。我测下来归一化之后同一舌头的H通道均值波动能控制在5以内。业务层面看VL输出和中医师判断的一致性。我找了几位中医师对一批样本做盲评然后跟引擎输出对比。在舌色和苔色这两个维度上一致率能到八成以上在舌形上稍低七成左右综合倾向因为本身主观性就强一致率六成多。这个结果不算完美但作为辅助工具已经够用。6.2 后续可以怎么扩展这套引擎的架构是开放的几个方向可以继续做。加入时序追踪把用户多次拍摄的数据连起来看趋势比单次判断有价值得多。接入更多模态比如结合问诊信息睡眠、饮食、情绪做多模态融合。个性化基线为每个用户建立自己的舌象基线用偏离度代替绝对值做判断。我个人最看好时序追踪这个方向。舌象是动态的单次快照信息量有限但如果能连续记录一个月的变化曲线那价值就完全不一样了。技术上也不难就是把每次的特征存下来做时间序列分析。最后分享一个我在实操中体会很深的点这套系统的价值不在于替代中医师而在于把说不清的东西变得说得清。以前用户问我舌头怎么样只能得到一句模糊的评价现在能给出具体的特征数据和趋势变化。这种从模糊到清晰的转变才是数字化真正的意义所在。至于判断准不准那是下一步的事先把数据采集和表征做扎实后面的路才走得稳。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑