简介:面向毕业设计与课程实践的舌象智能诊断系统,基于YOLOv深度学习框架与Python语言构建,定位为可运行、可扩展的完整项目方案,兼顾医学教学演示、科研分析与基层辅助诊断场景。整套资源共221个文件,压缩包约42.76MB,主要包含54个Python源码文件、61张标注图片、配套模型权重备份(zbak)及训练配置文件(json/txt),另有界面ui、说明文档与图表,源码注释完整,便于初学者按模块拆解学习。系统覆盖图像采集、特征提取、模型训练与结果分析全流程,预处理环节加入光照补偿与色彩校正,模型可识别舌体轮廓与舌苔分布,并支持批量处理、可视化报告及多用户权限管理。当前已有67人学习,适合需要在真实数据上完成深度学习落地项目的读者。
1. 舌象智能诊断系统在做什么:从一张舌照片到一份结构化报告,中间隔着三道坎
中医望诊里,舌诊是最典型的那个“黑匣子”——同一张舌照片,不同医生看,结论经常差出级别。舌象智能诊断系统要干的事,就是用 YOLOv 深度学习目标检测加 Python 服务,把“看舌头”从主观经验变成可复现的量化流程:先自动定位舌头区域,再判断舌色、苔色、苔质,最后输出结构化报告。这套方案适合两类人:想拿医学图像做一个完整落地项目的开发者,以及中医信息化团队在正式立项前先搭的验证原型。标题里带完整源码与数据集,意味着不用从零攒数据,可以直接在已有基础上复现和二次改造。真做起来你会发现,卡点不在模型结构,而在数据采集规范和标注一致性,下面按数据、训练、服务、排错的顺序逐个说透。
2. 为什么舌象检测选YOLOv:任务特殊性、选型理由与数据集的三步准备
在一个中医影像项目立项前,很多人第一句话就问“用语义分割还是目标检测”。我的答案取决于输出需求:系统最终要把舌体区域交给后续诊断模块,矩形检测框已经足够,不需要像素级的轮廓。舌诊照片的来源是手机或诊室相机,画面里除了舌头还有面部、口腔、反光,检测模块要做的事就是“把舌头框出来”,而不是做一次精细的图像分割。
2.1 舌象目标检测任务与通用检测的区别
舌头不是刚性目标,伸舌力度不同,长度、宽窄、弯曲度都会变;舌体周边还跟着嘴唇、牙齿、硬腭,色域与皮肤高度重叠。通用检测里常见的“颜色即特征”在这里失效,模型必须学的是舌面纹理、舌尖形态、舌根与咽喉交接处的结构。这就决定了选型标准:第一,单阶段模型优先,因为业务最终要实时预览,给用户看取景框;第二,模型要能扛住小样本迁移,因为真实舌象集规模有限;第三,输出要有再工程化的余地,不能只给一个类别名。
在YOLOv系列里,我一般以YOLOv8作为默认起点。原因很具体:v8改成anchor-free之后,解除了anchor尺寸和长宽比这一组超参数,舌体形态变化大,手工调anchor本来就是玄学;其次ultralytics的接口把这部分统一了,训练、验证、导出ONNX都在一个Python API里完成,服务端工程化顺滑许多。对比Faster R-CNN,v8在同类硬件上推理速度能快一个量级,精度在舌象这种中尺度目标上差距很小。对比Mask R-CNN这类实例分割模型,舌象诊断的下一步是裁剪区域的颜色与纹理分析,矩形四角之外的像素对诊断没有增量价值,分割模型多出的标注成本、显存成本都不划算。要守住一个原则:先验证业务闭环,再往上堆复杂度。
2.2 数据集来源、隐私规范与按患者划分的脚本
舌象数据集的现状是:公开的学术舌象集数量不多,标注维度集中在舌色、苔色这类分类标签,检测框往往要自己补标;医院门诊自采数据质量好,但涉及个人健康信息,采集前要明确脱敏方式和使用边界。常见做法是公开集打底、自采集调优,因此数据整理流程必须从第一天就规范。
命名规范我固定用patientID_sessionID_shotID三段式,比如p003_s01_002.jpg,后面所有脚本都靠这个命名解析患者维度。划分训练集与验证集时,必须按患者分组,不能按图片随机打散——同一个人的几十张照片高度相似,按图随机划分会把验证集泄漏到训练集里,得出的mAP是个假象。
# scripts/split_dataset.py # 按患者维度划分训练/验证集,避免同一患者照片同时出现在两侧 import os import random from collections import defaultdict from pathlib import Path random.seed(2024) IMG_ROOT = Path("datasets/tongue/images/all") # 原始图片目录 SAVE_ROOT = Path("datasets/tongue/images") # 划分后输出目录 TRAIN_RATIO = 0.8 patient_map = defaultdict(list) for img_path in IMG_ROOT.glob("*.jpg"): # 文件名约定:p003_s01_002.jpg,第一段是患者id parts = img_path.stem.split("_") patient_map[parts[0]].append(img_path) patients = list(patient_map.keys()) random.shuffle(patients) train_patients = set(patients[:int(len(patients) * TRAIN_RATIO)]) for patient, img_list in patient_map.items(): dest = "train" if patient in train_patients else "val" (SAVE_ROOT / dest).mkdir(parents=True, exist_ok=True) for src in img_list: # 用软链接避免复制大体积jpg占双倍磁盘 os.symlink(src, SAVE_ROOT / dest / src.name)这段脚本的逻辑是把患者作为随机单元,而不是把图片作为随机单元。random.seed(2024)保证同一批数据在多次执行时划分结果一致,方便对比不同训练配置;用os.symlink而不是shutil.copy,是为了避免好几万张JPG在磁盘上重复占空间。划分完成后建议再跑一次重复检查,用感知哈希对比训练集验证集图像的相似度,把相似度超过阈值的样本标记出来人工确认,这一步能救回不少后续评估时的信誉。
2.3 标注格式转换与可视化质检
手工标注阶段用的工具基本还是LabelImg,它导出的默认格式是Pascal VOC XML,而YOLOv8训练读的是每张图一个txt的YOLO格式。下面这个脚本负责把XML统一转为txt:
# scripts/voc2yolo.py # 把LabelImg导出的VOC XML标注,转换为YOLO训练的txt标注 import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path: Path, out_dir: Path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") W = int(size.find("width").text) H = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name != "tongue": # 舌象检测只保留舌体这一个类 continue box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # YOLO格式:class cx cy w h,全部除以图宽高做归一化 cx = (x1 + x2) / 2 / W cy = (y1 + y2) / 2 / H w = (x2 - x1) / W h = (y2 - y1) / H lines.append(f"0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_dir.mkdir(parents=True, exist_ok=True) (out_dir / (xml_path.stem + ".txt")).write_text("\n".join(lines)) # 用法:遍历标注目录,逐个调用上方函数 for xml_file in Path("datasets/tongue/labels_xml").glob("*.xml"): voc_to_yolo(xml_file, Path("datasets/tongue/labels_yolo"))这里有几个容易翻车的点:VOC坐标是像素值,而YOLO需要归一化后的0到1浮点,直接用原值训练会出问题;XML里如果混入了LabelImg预置的其他类别而没有过滤,转换后会把背景当成目标。转换完成后还要随机抽二十张图,把检测框画回原图检查:
# scripts/vis_labels.py # 把txt标注画回原图,人工检查框是否贴合舌尖与舌根 import cv2 img = cv2.imread("datasets/tongue/images/train/p003_s01_002.jpg") h, w = img.shape[:2] for line in open("datasets/tongue/labels_yolo/p003_s01_002.txt"): _, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w); y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w); y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("vis_p003_s01_002.jpg", img)画框检查的重点不是“框得准不准”,而是有没有把下唇、牙龈包进来。舌体和嘴唇同色域,标框的人手一抖把下唇框进去,模型学到的模式里就混入了“唇部开口”特征,推理时自然就会连带框出别人的嘴唇。质检通过后再进入训练环节,否则训练两小时、排错一整天,都是这里省下的功夫在后面加倍补交。
3. 用YOLOv8训练自己的舌象数据集:环境、最小命令与增强参数调优
数据质检通过之后,代码部分就顺畅了。训练阶段的目标不是把模型调到最极致,而是让团队里每个成员都能用一套固定命令复现训练过程。下面先从环境与最小训练命令讲起,再展开针对舌象的增强参数定制。
3.1 环境搭建与一次训练的最小命令
环境按官方文档装即可:Python 3.10以上,准备好CUDA和对应驱动。安装依赖只用一行命令,但要装到专门的虚拟环境里,不要直接打入系统Python,否则后面的OpenCV和ultralytics版本冲突会让你怀疑人生。常见做法是conda建一个新环境,然后pip install ultralytics opencv-python。
训练入口是两个文件:一个描述数据集的YAML,一个Python脚本。YAML内容如下:
# datasets/tongue/tongue.yaml path: /home/yourname/datasets/tongue # 改成你机器上的绝对路径 train: images/train val: images/val names: 0: tongueYAML里唯一需要小心的是path这个字段,它要求是绝对路径。用相对路径时,换一台机器整个路径就断了,训练报错会很奇怪。train和val是相对path的子目录,这里直接指向划分好的两个图片目录。
训练脚本可以很短:
# scripts/train_tongue.py from ultralytics import YOLO # 用coco预训练权重初始化,再在自己的舌象数据上微调 model = YOLO("yolov8n.pt") model.train( data="datasets/tongue/tongue.yaml", epochs=120, # 数据量小,epoch设多些,配合早停 imgsz=640, # 训练分辨率,先跑小尺寸验证流程 batch=16, # 单卡常见配置,显存不够就降到8 device=0, # 使用GPU;没有GPU时写cpu,但速度慢一个量级 patience=20, # 验证指标连续20个epoch不涨就停 )这段代码的imgsz值得多说一句。手机原图经常是3000x4000级别,直接跑960分辨率会占用大量显存;先用640把流程跑通,再提高分辨率看收益,是稳妥路径。舌象的舌乳头等细纹理在640下会损失一部分,如果后续诊断对纹理敏感,再升到960。训练结束后,runs/detect/train/weights/下会同时出现best.pt和last.pt,前者是验证集指标最优的权重,后者是最后一个epoch的实时权重。
3.2 舌象训练的四个关键增强超参
第一次跑通之后,真正让模型在不同光源、不同手机下都能可用的,是数据增强参数。通用数据集上默认的一整套增强,在舌象上可能帮倒忙。下面这组配置是我在舌象项目上反复试出来的起点:
# scripts/train_tongue_advanced.py from ultralytics import YOLO model = YOLO("yolov8n.pt") model.train( data="datasets/tongue/tongue.yaml", epochs=150, imgsz=960, # 提升分辨率保留舌体纹理细节 batch=8, # 分辨率翻倍,batch要相应减半 mosaic=0.3, # 舌象拼接增强意义不大,降到0.3减少误学 close_mosaic=10, # 最后10个epoch关掉mosaic,让模型回归真实布局 hsv_h=0.03, # 色相扰动要小,过大把淡红舌增强成红舌 hsv_v=0.6, # 亮度扰动加大,提升对不同光源的适应力 fliplr=0.5, # 舌体左右基本对称,水平翻转是安全增强 scale=0.3, # 缩放幅度控制住,舌体宽窄比例不能学歪 optimizer="AdamW", lr0=1e-3, )逐项解释这些参数。mosaic是Ultralytics默认最强的一档增强,把四张图拼成一张,对目标小、背景杂的场景有效;但舌象样本本身就带结构性,四张舌头拼在一起会让模型学到“舌体可以以奇怪的方式拼接”的错误先验,降低到0.3即可。close_mosaic=10让最后十个epoch彻底关闭拼接,让模型在真实布局上收尾,这是缓解增强与真实分布不一致的常用手段。hsv_h只给0.03,舌色之间的界限本来就细,色相扰动过大等于人为制造标签噪声。hsv_v=0.6是这组参数里最重要的,舌象照片在不同光源下的亮度差异远大于色相差异,把亮度扰动拉高,模型才不会死记“舌头就应该是这个亮度”。如果不确定参数怎么调,先全部用默认跑一个baseline,再逐个参数改,不要让两个以上参数同时变,否则出了问题根本定位不到是哪个增强引入的。
3.3 评估指标怎么读,怎么导出部署格式
训练结束后的评估不要只看最终那张图。Ultralytics会在runs/detect/train/下生成results.csv,我一般直接打开看最后几十行的metrics/mAP50(B)和metrics/mAP50-95(B)。对舌象检测来说,mAP50到0.9以上不算难,因为舌头是画面里的大目标;真正能暴露问题的是mAP50-95,它对框的贴合程度非常敏感,这个值低,说明框要么偏大要么偏小,裁剪出来的区域会带着嘴唇或者切掉舌尖。业务上更实用的评估口径是“裁剪保留率”:对每张验证图,检查预测框内部有多少比例的舌体真值被覆盖,这个指标直接决定了后续诊断模块拿到的区域干不干净。
导出部署格式的代码是同一套API:
# scripts/export_onnx.py from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") model.export( format="onnx", dynamic=True, # 支持动态batch,服务端并发更灵活 imgsz=640, half=True, # FP16精度,GPU推理更快;CPU环境把half去掉 simplify=True, # 简化计算图,减少算子兼容问题 )导出完成后,强烈建议做一次“同一张图、两种格式”的对比:把predict结果里的框坐标分别用pt和onnx跑一遍,检查坐标差是否在1个像素以内。ONNX转换偶尔会丢算子或精度抖动,直接拿到生产环境推理,翻车时排查成本高得多。这一步属于十分钟能做完、能省一晚上排错的操作,值得写进团队的发布流程。
4. 用Python把模型变成舌象诊断服务:FastAPI接口与诊断规则管线
训练好的权重只是“能看到舌头”的检测模型,离诊断系统还差两层:一层是外部用户能调用的服务接口,另一层是把检测框转换成舌色、苔色诊断结论的业务逻辑。这一章把它做成最小可用的工程结构。
4.1 最小可用检测接口:一次加载、多请求复用
服务端用FastAPI,原因很朴素:它自带请求校验和OpenAPI文档,前端联调的时候直接看/docs页面就能试接口,省去手写接口说明。核心代码量不大:
# app/main.py import cv2 import numpy as np from pathlib import Path from fastapi import FastAPI, UploadFile from ultralytics import YOLO app = FastAPI() # 进程启动时加载一次,不要在请求处理函数里重复加载权重 model = YOLO("weights/best.pt") device = "cuda:0" if __import__("torch").cuda.is_available() else "cpu" @app.post("/detect") async def detect(file: UploadFile): content = await file.read() img_array = np.asarray(bytearray(content), dtype=np.uint8) # 内存解码,避免把临时文件写到磁盘再读 img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) if img is None: return {"code": 1, "msg": "image decode failed"} results = model.predict( source=img, imgsz=960, conf=0.5, # 舌象是中等目标,阈值太高容易漏检 iou=0.6, device=device, verbose=False, ) boxes = results[0].boxes.xyxy.cpu().numpy().tolist() confs = results[0].boxes.conf.cpu().numpy().tolist() return {"code": 0, "boxes": boxes, "confs": confs} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)两个细节解释一下。第一,model必须在模块加载时初始化,如果每次请求都重新加载,几百毫秒的加载耗时会让接口响应时间飙到秒级,并发一上来机器直接卡死。第二,imgsz=960要和训练时的分辨率保持一致,推理分辨率突然改变,检测框的质量都会下降。conf=0.5是一个折中值,舌象检测对漏检更敏感,宁可偶尔多一个误检框,也不能把真舌头丢了,业务侧可以通过置信度排序做二次过滤。
4.2 从检测框到诊断结论:先搭规则管线,再换分类模型
拿到检测框后,诊断模块的第一步是裁剪舌体区域。裁剪时我习惯向外扩10%,因为检测框通常贴合舌尖和舌根,直接裁剪可能把边缘部分切掉,外扩能保留下颌组织与舌根的过渡带:
# app/diagnose.py import cv2 import numpy as np def crop_tongue(img, box, expand=0.1): x1, y1, x2, y2 = [int(v) for v in box] w, h = x2 - x1, y2 - y1 x1 = max(0, int(x1 - w * expand)) y1 = max(0, int(y1 - h * expand)) x2 = min(img.shape[1], int(x2 + w * expand)) y2 = min(img.shape[0], int(y2 + h * expand)) return img[y1:y2, x1:x2] def tongue_color_rule(roi): """简单规则基线:用HSV空间像素占比近似判断舌色,正式项目会换成分类模型。""" hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) H, S, V = hsv[:, :, 0], hsv[:, :, 1], hsv[:, :, 2] # 淡白/淡红:整体高亮、低饱和 pale_ratio = float(np.mean((V > 180) & (S < 90))) # 红舌:红色调像素占比高 red_ratio = float(np.mean(((H < 10) | (H > 170)) & (S > 60) & (V > 60))) if pale_ratio > 0.5: return "淡白舌" if red_ratio > 0.6: return "红舌" return "淡红舌"这块必须把话说清楚:HSV阈值规则只是用来快速跑通闭环的基线,在受控光源下能把“淡红/红/淡白”分开,自然光下很容易翻车。正式版本用两级架构:YOLOv负责定位,裁剪后的舌体图送入一个三分类或四分类的轻量模型,标签由医生标注的舌色、苔色、苔质组成。规则管线的价值在于,它让系统的接口、数据流、报告结构先定型,后续替换分类模型时,/detect接口不需要动,只有tongue_color_rule内部实现被替换,这种隔离在工程上很值钱。
诊断输出建议直接返回结构化JSON,同时加一句免责说明:
{ "image_id": "p003_s01_002", "tongue_color": "淡红舌", "coating_color": "白苔", "coating_texture": "薄苔", "confidence": 0.87, "note": "仅供参考,不作为医疗诊断依据" }苔色和苔质如果暂时没有分类模型,可以用同一套规则管线先给出“白苔/黄苔”“薄苔/厚苔”的粗略判断,注意在文档里标注这些字段当前是规则输出、准确性有限,避免用户误读。
4.3 源码组织的几个习惯:训练、权重、数据三者分离
一个含完整源码与数据集的项目,最容易踩的坑是“所有文件堆在一个目录里”。我自己的目录结构固定如下:
tongue_diag/ ├── app/ │ ├── main.py # FastAPI服务入口 │ ├── detect.py # YOLOv8检测封装 │ └── diagnose.py # 诊断规则管线 ├── scripts/ # 数据集划分、格式转换、评估脚本 ├── weights/ # best.pt,小体积、不入git ├── datasets/tongue/ # 原始数据与划分后的数据 └── requirements.txt # 锁定关键依赖版本这里有几个配套习惯:weights和datasets不要提交到代码仓库,训练权重动辄几十上百MB,数据集又是敏感健康数据,仓库只保留能从头复现训练过程的代码和说明;requirements.txt要精确锁住ultralytics的版本,不同主版本之间的参数兼容性差异足以让同一份代码在另一台机器上报出完全不认识的关键字错误。写README时,把“跑训练”“跑服务”“重新生成数据划分”三条命令各写一行,后面接手的人不用翻代码也能把环境搭起来。
5. 舌象诊断系统的踩坑实录:从训练到演示,五条血泪经验
训练、服务都能跑通之后,真正的挑战才刚开始。下面是按真实频率排序的五条踩坑记录,每条都按现象、原因、解决写清楚。
5.1 换个房间,检测框还在,诊断结论却翻车了
现象:demo 时同一个受试者,在窗边自然光下判断为“淡红舌”,移到LED灯下变成“红舌”,再换到诊室灯管下又变成“淡白舌”,检测框每一张都框得准,但颜色判断来回跳。 原因:诊断规则管线直接消费BGR像素,没有做白平衡和亮度校正;训练图像的亮度分布太单一,模型把“某个固定亮度”当成了舌色特征的一部分。 解决:采集阶段固定光源或让标准色卡入镜;推理阶段用OpenCV的白平衡算法先校正一张图;训练阶段把hsv_v增强参数拉高到0.6左右,让模型不再依赖绝对亮度。这个分层修法执行到位之后,同类翻车基本不再出现。
5.2 训练指标mAP 0.93,部署到门诊手机漏检一半
现象:本地测试集指标很漂亮,验证集mAP50超过0.9,但把模型部署到门诊拍摄的手机图上,漏检率肉眼可见地高。 原因:数据集划分是按图片随机打散的,同一个患者的几十张高度相似照片同时出现在训练集和验证集,验证指标是被这个泄漏撑起来的假象,是个黑匣子。 解决:回到数据划分这一步,按患者分组划分;同时用感知哈希把训练集和验证集做重复度检查,把相似度超过阈值的样本挑出来人工复核。这两步做完,模型的真实水平才会原形毕露,后续调优才有意义。
5.3 训练一百个epoch,输出永远只有“薄白苔”
现象:不管输入什么舌头,报告里苔质永远是“薄苔”,苔色永远是“白苔”,分类准确率看着还挺高。 原因:训练集里薄白苔占了七成以上,分类模型学到的是“无脑预测多数类,期望损失最低”的最优解,宏观指标被样本分布灌水了。 解决:一是在数据层面按类别做重采样或给loss加权重;二是把舌色、苔色、苔质拆成独立的子模型分别训练,不要再塞进同一个多分类头;三是苔质这类细粒度属性,可以用检测框内纹理区域的统计特征辅助判断,降低模型对多数类的路径依赖。
5.4 训练到一半显存OOM,没有后悔药只能从头再来
现象:epoch跑到40多,CUDA out of memory,之前的训练进度全部作废;重启后要么调小batch重跑,要么干脆不知道从哪个权重继续。 原因:先是batch size设置得碰运气,其次是mosaic增强在epoch后段仍然开着,某几个batch的拼接尺寸特别大,内存峰值直接顶爆;还有就是不熟悉last.pt的作用。 解决:显存吃紧时先把batch减半,再考虑降低imgsz;在高版本ultralytics上使用close_mosaic=10,让最后10个epoch关掉拼接;养成每个实验跑完就把runs目录备份的习惯,续训时model.train(resume=True)直接接上last.pt,这个后悔药能省掉一整晚重训时间。
5.5 中文文件名和透明PNG让训练集悄悄缩水
现象:数据里有一批“舌象_白苔_003.jpg”,训练日志没有error只有warn,但训练过程结束后发现参与训练的样本数比预期少了几百张。 原因:OpenCV的imread函数对中文路径支持不完善,读不出来的图片返回None,但没有抛异常;另一个坑是RGBA的PNG被当成三通道读取,透明区域变成黑色背景,悄悄污染了样本分布。 解决:图片在进数据集之前统一重命名为英文加数字;读图后立即检查img is None,为空的直接raise让训练停止,而不是默默跳过;PNG用cv2.IMREAD_UNCHANGED读入再转RGB合并白底。这些检查写进数据质检脚本,就不会再发生“少了几百张图但没人知道”的事情。
6. 系统跑通后先别急着演示:混淆矩阵与色卡校准这两个验证技巧
模型训练完、服务也通了,先别急着向业务方演示。我有两个习惯性验证动作,能在十分钟内暴露系统最不靠谱的环节。
6.1 用混淆矩阵找出“最难分开的那一对标签”
舌色分类里最尴尬的不是谁对谁错,而是红舌和绛红舌、白苔和薄苔这种相邻标签之间反复纠缠。只盯着准确率根本看不到这种纠缠,要看混淆矩阵。用医生标注的标签和系统输出各拉一列,一行代码就能画出来:
# scripts/eval_confusion.py from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt y_true = [...] # 医生标注的舌色标签 y_pred = [...] # 系统输出的舌色标签 labels = ["淡白", "淡红", "红", "绛红"] disp = ConfusionMatrixDisplay( confusion_matrix=confusion_matrix(y_true, y_pred, labels=labels), display_labels=labels, ) disp.plot() plt.savefig("cm_tongue_color.png", dpi=150)看这个矩阵有一个固定顺序:先找对角线相邻的非零项,比如“红”被大量判成“绛红”,这说明数据里这两个类的样本边界本身模糊;再找跨区域的大数值,那基本是采集光源问题而不是模型问题。优先解决混淆最重的相邻对,比盲目增加训练轮数有用得多。
6.2 一套固定的色卡校准流程,替代“换手机就换结果”
不同手机拍照的白平衡策略差异很大,同一部手机在不同光源下也有偏移。为了解决这个问题,我在采集规范里强制要求画面边缘出现标准色卡,推理时先检测色卡区域,用它的白点做整图白平衡校正。OpenCV提供现成实现:
wb = cv2.xphoto.createSimpleWB() img_balanced = wb.balanceWhite(img)这行代码不复杂,但前提是画面里有可参考的中性色区域。没有色卡时,直接调用反而会把本就偏色的图像拉向另一个极端。固定色卡入镜、再校正、再送入诊断管线,这一整套流程跑下来,不同手机间的结果一致性才会有质的提升。我的习惯是每次拿到新设备,先拍一组带色卡的样张过一遍全流程,再谈调优。舌象诊断这类系统的难点不在模型,而在数据和评估口径有没有被钉死,把这两处校准做好,后面的调优才有意义,希望帮到你。
本文还有配套的精品资源,点击获取