
简介这是一份面向计算机相关专业学生与初学者的Python舌象检测项目源码聚焦于判断输入图像中是否包含面部、是否伸出舌头以及是否翘起舌头等状态可应用于中医舌诊辅助分析、人机交互实验或课程设计场景。压缩包共36个文件约21.48MB包含8个Python脚本、18张jpg与jpeg测试图片、2个xml级联分类器文件、2个md说明文档及json配置等脚本覆盖人脸检测、嘴部定位、HSV舌体分割与分水岭区域分割等模块图片则用于验证不同姿态下的检测效果。已有164人学习下载。项目代码经过完整测试附带README与图片检测说明读者可据此理解从人脸到嘴部再到舌体的多级检测流程掌握OpenCV级联分类器与颜色空间分割的配合方式并可在现有结构上修改以扩展其他功能适合作为毕设、课设或入门练手参考。1. 舌象检测到底在检测什么从一张自拍判断伸舌与翘舌很多人第一次听到「舌象检测」脑子里浮现的是中医馆里老大夫看舌苔的画面觉得这东西离工程很远。但真落到代码层面它其实是一个很具体的图像分类问题给一张用户自拍判断画面里有没有脸、有没有伸出舌头、舌头是平伸还是上翘。这三个判断串起来才构成一个能用的入口。我在做健康类小程序时踩过的第一个坑就是用户举着手机对着镜子拍脸是歪的、舌头只露一点点模型直接懵了。所以标题里「面部/伸舌头/翘舌头」这三类不是随便写的它们对应三个递进的判定层级缺一个后面全废。这套方案适合谁做健康管理 App 的、做中医辅助工具的、想拿 Python 练手图像分类的都能直接抄。核心链路是人脸检测先框出嘴部区域再在嘴部 ROI 上做舌头状态分类。用 Python 加 OpenCV 和轻量级分类模型就能跑通不需要 GPU 也能出结果。热搜里「python入门」「python安装教程」这些词说明很多人卡在环境上所以我会把环境配置和最小可跑命令写清楚让你从装 Python 到看见分类结果不超过半小时。舌象检测不是玄学它是一套可拆解、可复现的工程流程下面按「先立原理、再动手、最后避坑」的顺序讲透。2. 面部与嘴部定位为什么先做人脸检测再谈舌头2.1 人脸检测选型Haar、DNN 还是 MediaPipe做舌象检测的第一步不是分类舌头而是把脸找出来。常见做法有三种OpenCV 自带的 Haar 级联、OpenCV DNN 模块加载 Caffe 模型、以及 MediaPipe Face Detection。我一般会优先用 MediaPipe原因是它在侧脸和遮挡场景下比 Haar 稳太多。Haar 的误检率在自拍场景里高得离谱背景有类似人脸的纹理就会框出来后面嘴部 ROI 全乱。DNN 方案精度够但模型文件要额外下载对新手不友好。MediaPipe 装完就能用返回的人脸框和关键点直接可用。选型理由说清楚舌象检测的输入是用户自拍光照和角度不可控Haar 对光照敏感DNN 部署重MediaPipe 在精度和易用性之间平衡最好。如果你要做的是嵌入式端那另说但 Python 桌面或服务端场景MediaPipe 是首选。2.2 用 MediaPipe 在本地跑通人脸与嘴部关键点先装环境。Python 版本建议 3.8 到 3.10太新的版本有些包还没跟上。命令如下pip install opencv-python mediapipe numpy这三行装完写一个最小脚本把图片里的人脸框和嘴部关键点打出来import cv2 import mediapipe as mp # 初始化人脸检测和面部关键点模块 mp_face_detection mp.solutions.face_detection mp_face_mesh mp.solutions.face_mesh # 读取输入图像 image cv2.imread(test_face.jpg) h, w image.shape[:2] rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 人脸检测min_detection_confidence 控制检出阈值 with mp_face_detection.FaceDetection(model_selection0, min_detection_confidence0.5) as face_det: results face_det.process(rgb) if results.detections: for det in results.detections: bbox det.location_data.relative_bounding_box x1 int(bbox.xmin * w) y1 int(bbox.ymin * h) x2 int((bbox.xmin bbox.width) * w) y2 int((bbox.ymin bbox.height) * h) cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2) print(f人脸框: ({x1},{y1}) - ({x2},{y2})) # 面部关键点用 FaceMesh 拿到嘴部轮廓点 with mp_face_mesh.FaceMesh(static_image_modeTrue, max_num_faces1, refine_landmarksTrue) as face_mesh: mesh_results face_mesh.process(rgb) if mesh_results.multi_face_landmarks: landmarks mesh_results.multi_face_landmarks[0] # 嘴部关键点索引上唇 13下唇 14左嘴角 61右嘴角 291 for idx in [13, 14, 61, 291]: lm landmarks.landmark[idx] cx, cy int(lm.x * w), int(lm.y * h) cv2.circle(image, (cx, cy), 3, (0, 0, 255), -1) print(f嘴部关键点 {idx}: ({cx},{cy})) cv2.imwrite(output_face.jpg, image)逻辑说明先用人脸检测拿到大致人脸框再用 FaceMesh 拿 468 个面部关键点其中嘴部相关的几个点用来定位嘴部 ROI。参数方面model_selection0适合近距离人脸min_detection_confidence0.5是默认值如果误检多可以提到 0.7。refine_landmarksTrue会额外输出虹膜和嘴唇精细点对舌象检测有用。2.3 从关键点裁剪嘴部 ROI 的坐标计算拿到嘴部关键点后不能直接把整张脸丢给分类模型那样舌头区域太小特征被稀释。我一般会以左右嘴角和上下唇为基准向外扩 1.5 倍裁剪一个矩形区域。具体做法是取 61 和 291 的 x 坐标作为左右边界取 13 和 14 的 y 坐标作为上下边界然后按比例外扩。这个 ROI 就是后续舌头分类的输入。这里有个参数要调外扩系数。太小会切掉舌头边缘太大会引入下巴和鼻子干扰。我实测下来 1.4 到 1.6 之间比较稳具体看你的数据集。裁剪完统一 resize 到 224x224方便后面接分类网络。这一步做完你就有了一个干净的嘴部区域图接下来才是判断舌头状态。3. 伸舌与翘舌分类从 ROI 到三分类模型3.1 三分类标签定义与数据采集的坑标签定义直接决定模型能不能用。我一般定义三类no_tongue闭嘴或普通张嘴没伸舌、tongue_flat平伸舌头、tongue_up翘舌舌尖向上卷。注意「翘舌」和「卷舌」不是一回事翘舌是舌尖上抬卷舌是舌体卷曲标注时要统一标准否则模型学出来是乱的。数据采集是血泪经验最多的地方。用户自拍的光照差异极大有人背光拍出来脸是黑的有人开美颜把舌头磨没了。我建议采集时至少覆盖室内白光、室内暖光、室外自然光、侧光四种光照每种光照下三类各采 200 张以上。另外要专门采一批「只露一点点舌头」的边界样本这类样本在线上最容易翻车。标注时用文件夹分目录目录名就是标签名后面训练直接读目录。3.2 用 MobileNetV3 做迁移学习的最小训练脚本数据准备好后用 PyTorch 加预训练 MobileNetV3 做迁移学习。MobileNetV3 参数量小CPU 推理也能到实时适合舌象检测这种轻量场景。先装依赖pip install torch torchvision pillow训练脚本如下import torch import torch.nn as nn from torchvision import datasets, transforms, models from torch.utils.data import DataLoader # 数据增强训练集用随机翻转和颜色抖动验证集只做 resize train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 目录结构data/train/no_tongue、data/train/tongue_flat 等 train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers2) # 加载预训练 MobileNetV3替换最后一层为三分类 model models.mobilenet_v3_small(pretrainedTrue) model.classifier[3] nn.Linear(model.classifier[3].in_features, 3) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() # 只训练分类头时学习率可以大一点微调整个网络时用 1e-4 optimizer torch.optim.Adam(model.parameters(), lr1e-4) for epoch in range(15): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() # 验证阶段 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) pred model(imgs).argmax(1) correct (pred labels).sum().item() total labels.size(0) print(fEpoch {epoch1}, Val Acc: {correct/total:.4f}) torch.save(model.state_dict(), tongue_cls.pth)逻辑说明ImageFolder按目录名自动映射标签省去手写 Dataset。ColorJitter模拟不同光照这是舌象检测泛化的关键。学习率 1e-4 是微调整网的常用值如果你只想训分类头可以把前面层冻结学习率提到 1e-3。batch_size32在 8G 显存下够用CPU 训练就降到 8。3.3 推理阶段把分类模型接回嘴部 ROI训练完保存权重推理时把第 2 章的嘴部 ROI 裁剪出来resize 到 224送进模型import torch from torchvision import transforms from PIL import Image model models.mobilenet_v3_small(pretrainedFalse) model.classifier[3] nn.Linear(model.classifier[3].in_features, 3) model.load_state_dict(torch.load(tongue_cls.pth, map_locationcpu)) model.eval() infer_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) roi Image.open(mouth_roi.jpg) tensor infer_tf(roi).unsqueeze(0) with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1) pred prob.argmax(1).item() labels [no_tongue, tongue_flat, tongue_up] print(f预测: {labels[pred]}, 置信度: {prob[0][pred]:.4f})参数说明map_locationcpu保证没有 GPU 也能加载。置信度低于 0.6 时建议返回「无法判断」让用户重拍这比硬给一个错结果体验好。整个链路跑通后从输入自拍到输出舌头状态CPU 上大概 200 毫秒以内。4. 避坑与排查舌象检测线上翻车的五个真实记录4.1 现象用户伸舌但模型判成 no_tongue原因训练集里「伸舌」样本舌头伸出长度普遍偏长线上用户只伸一点点ROI 里舌头占比太小模型没学到短伸舌特征。解决补采短伸舌样本或者在 ROI 裁剪时把外扩系数从 1.5 降到 1.2让舌头在 ROI 里占比更大。我一般会两个都做效果最稳。4.2 现象翘舌和平伸混淆严重原因标注时翘舌标准不统一有人把舌尖轻微上抬标成翘舌有人只标明显上卷的。解决重新定义标注规范翘舌要求舌尖与下唇夹角大于 30 度标注时用关键点辅助判断。另外可以在分类头前加一个角度回归分支辅助区分。4.3 现象MediaPipe 在某些手机上检测不到人脸原因部分手机前置摄像头默认输出镜像图或者图像方向 EXIF 没处理MediaPipe 拿到的是旋转 90 度的图。解决读图后用cv2.rotate按 EXIF 方向校正或者统一转成 RGB 后再送检测。这个坑在安卓端特别常见后悔药就是提前做方向归一化。4.4 现象模型在验证集准确率 95%线上只有 70%原因验证集和训练集同分布都是实验室采集的线上用户光照和角度差异大。解决验证集要单独采一批「野生样本」不参与训练只做评估。另外训练时加强 ColorJitter 和随机旋转提升泛化。4.5 现象推理速度慢CPU 上超过 1 秒原因每次推理都重新加载模型或者输入分辨率没降。解决模型只加载一次常驻内存输入从 224 降到 192 或 160精度掉一点但速度翻倍。MobileNetV3-small 在 160 输入下 CPU 推理可以到 50 毫秒以内。5. 进阶技巧用关键点几何约束提升翘舌判定准确率分类模型有个天然短板它看的是整体纹理对「舌尖角度」这种几何信息不敏感。我在实际项目里发现纯分类模型在翘舌判定上容易把「舌头平伸但舌尖微抬」误判成翘舌。后来加了一个几何约束分支准确率提了 8 个点。具体做法是用 FaceMesh 拿到上唇 13、下唇 14、舌尖点需要额外检测可以用舌头分割模型取最上沿点计算舌尖与下唇连线和水平线的夹角。夹角大于阈值就强判翘舌分类模型只负责区分有没有舌头。这个思路的本质是「几何管角度纹理管有无」两个信号互补。实现上你可以先用一个轻量分割模型把舌头区域抠出来取区域最上方的点作为舌尖近似。分割模型用 U-Net 小版本就行训练数据用标注好的舌头 mask。如果不想训分割也可以用颜色阈值在 ROI 里粗提舌头区域HSV 空间里舌头偏红阈值调好也能用只是稳定性差一些。验证方法上我习惯做一个混淆矩阵看三类之间的误判方向。如果翘舌大量被误判成平伸就加几何约束如果 no_tongue 被误判成有舌头就补负样本。另外建议做一个「置信度拒绝」机制低于 0.6 的样本不返回结果而是提示重拍这样线上投诉会少很多。最后说个我自己的习惯每次模型更新前一定拿同一批 200 张野生样本跑一遍前后对比不看绝对准确率只看误判方向有没有变。舌象检测这东西玄学的地方在于光照和角度工程能做的就是把这些变量尽量框住。希望帮到你。本文还有配套的精品资源点击获取