☰
Python实战舌象检测:人脸检测与伸舌翘舌分类
2026/10/1 5:45:35 网站建设 项目流程

简介:这是一份面向计算机相关专业学生与初学者的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_selection=0, min_detection_confidence=0.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_mode=True, max_num_faces=1, refine_landmarks=True) 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_selection=0适合近距离人脸,min_detection_confidence=0.5是默认值,如果误检多可以提到 0.7。refine_landmarks=True会额外输出虹膜和嘴唇精细点,对舌象检测有用。

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(brightness=0.3, contrast=0.3, saturation=0.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", transform=train_tf) val_ds = datasets.ImageFolder("data/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=2) # 加载预训练 MobileNetV3,替换最后一层为三分类 model = models.mobilenet_v3_small(pretrained=True) 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(), lr=1e-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(f"Epoch {epoch+1}, Val Acc: {correct/total:.4f}") torch.save(model.state_dict(), "tongue_cls.pth")

逻辑说明:ImageFolder按目录名自动映射标签,省去手写 Dataset。ColorJitter模拟不同光照,这是舌象检测泛化的关键。学习率 1e-4 是微调整网的常用值,如果你只想训分类头,可以把前面层冻结,学习率提到 1e-3。batch_size=32在 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(pretrained=False) model.classifier[3] = nn.Linear(model.classifier[3].in_features, 3) model.load_state_dict(torch.load("tongue_cls.pth", map_location="cpu")) 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, dim=1) pred = prob.argmax(1).item() labels = ["no_tongue", "tongue_flat", "tongue_up"] print(f"预测: {labels[pred]}, 置信度: {prob[0][pred]:.4f}")

参数说明:map_location="cpu"保证没有 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 张野生样本跑一遍前后对比,不看绝对准确率,只看误判方向有没有变。舌象检测这东西,玄学的地方在于光照和角度,工程能做的就是把这些变量尽量框住。希望帮到你。

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

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

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

立即咨询