简介:面向计算机视觉学习与牙科图像识别研究,这套YOLOv8牙科解剖数据集由Roboflow标注完成,包含训练、验证、测试三部分共724幅牙齿图像及对应标签,其中训练505幅、验证112幅、测试107幅。数据集按标准机器学习流程组织目录,配套data.yaml定义类别与路径,可直接用于YOLOv8模型训练、评估与结果复现。压缩包共1797个文件,以jpg图像与txt标注文件为主体,另含pt模型权重、yaml配置、ipynb训练记录、csv指标、png样例图等辅助材料,便于核对标注效果与训练过程。包体约43.53MB,结构清晰,适合从入门到进阶的开发者作为牙科目标检测任务的学习与试验样本。当前已有91人学习下载,可用于理解商业级数据集的标注规范与YOLOv8训练流程。
1. 牙科图像检测为什么值得用YOLOv8:Roboflow标注数据的价值与边界
拿到一批Roboflow标注的牙科解剖图像和标签文件时,大多数人第一反应是“标好了,直接丢进YOLOv8就能训”。实际跑一遍才会发现,标签格式、类别ID顺序、数据划分和图像预处理,每一个环节都可能让训练翻车。这篇文章围绕YOLOv8与Roboflow标注的牙齿图像数据集,讲清楚从解压数据集到训练、验收的完整路径:怎么读标签文件、训练参数怎么设、牙齿小目标和类别不平衡带来的坑在哪里,以及如何验证模型是真的可用而不是只在mAP上好看。适合正在做医学影像检测、口腔AI项目或者用YOLOv8跑自己数据集的人,无论你是入门还是已经在踩坑阶段,都能在这里找到对应的一步。
2. 读懂Roboflow导出的牙齿数据集:标签格式、类别定义与最小目录结构
2.1 先从Roboflow的zip说起:配置文件与三套目录
常见做法是在Roboflow的版本导出界面里选择YOLOv8/PyTorch TXT格式,下载回来一个zip。解压后目录大概是这样的:
unzip dental_dataset.zip -d dental_dataset tree dental_dataset -L 2输出大致为:
dental_dataset ├── README.roboflow.txt ├── data.yaml ├── train │ ├── images │ └── labels ├── valid │ ├── images │ └── labels └── test ├── images └── labels注意有些导出选项里没有test目录,只有train和valid,这很正常。拿到目录后什么也别改,先做两个检查:图片和txt是否一一对应,有没有空标签文件。Roboflow在导出时偶尔会把没有标注的图像一并放进文件夹,而这些图在YOLO训练里会被当成背景负样本,是好东西,但如果它的名字没有对应txt,可能会在后续清洗时被误删。统计一下:
from pathlib import Path for split in ["train", "valid", "test"]: img_dir = Path(f"dental_dataset/{split}/images") lbl_dir = Path(f"dental_dataset/{split}/labels") imgs = {p.stem for p in img_dir.glob("*.*")} lbls = {p.stem for p in lbl_dir.glob("*.txt")} print(split, "图片数:", len(imgs), "标签数:", len(lbls)) print("有图无标签:", len(imgs - lbls), "有标签无图:", len(lbls - imgs))glob(".")是为了把jpg、png都收进来,YOLO对图片扩展名不敏感,但图片名与txt的主名必须完全一致;如果“有图无标签”数量偏多,先确认Roboflow导出时是否勾选了负样本选项,再决定是补标注还是保留为背景。
2.2 YOLO标签txt的每一行:类别ID与归一化中心点、宽高
随便打开一张图的同名txt,比如train/labels/pat001_01.txt,内容类似下面五行:
2 0.5231 0.4124 0.0832 0.0945 0 0.6111 0.4372 0.0790 0.1021每行五个字段:第一个是类别ID,整数,对应data.yaml里的names下标;后面四个是归一化后的目标框中心点x、中心点y、宽、高,范围都在0到1之间,计算方式是“像素坐标除以图片宽高”。这种格式与图像尺寸解耦,同一份标签可以配合不同imgsz使用,这也是YOLO系格式最方便的地方。
坑在这里埋着:如果Roboflow导出时选择了缩放预处理,比如把原图强制resize到640x640,导出的图像已经是缩放后的,标签也按缩放结果归一化;你再按原尺寸做一次resize,坐标就会整体错位。我拿到数据后的第一件事,是写脚本把每张图的实际尺寸打出来,确认和训练时的imgsz没有重复缩放。
快速验证脚本,把txt里的框画回原图,人工看三张就能发现坐标错位:
import cv2 img_path = "dental_dataset/train/images/pat001_01.jpg" txt_path = "dental_dataset/train/labels/pat001_01.txt" img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cls, x, y, bw, bh = map(float, line.split()) x1 = int((x - bw / 2) * w) y1 = int((y - bh / 2) * h) x2 = int((x + bw / 2) * w) y2 = int((y + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("check_boxes.jpg", img)这个脚本做的事很简单:把归一化坐标乘回图像宽高,画框保存。如果框都准确包在牙齿上,说明标签和图像匹配;如果框漂到牙缝或图像外面,就回到Roboflow预处理设置里检查是不是做了意外缩放。画框用整数坐标,OpenCV矩形不接受浮点,每次画框前都要乘回原图尺寸。
2.3 data.yaml是数据集契约:类别顺序错了,整个训练就废了
Roboflow导出包里的data.yaml长这样:
train: dental_dataset/train/images val: dental_dataset/valid/images nc: 5 names: 0: caries 1: crown 2: root 3: implant 4: molar两点要注意:第一,train和val路径是相对路径,我把zip解压到项目目录下再改的,如果你把数据集移到别处,这两行必须跟着改,否则训练时会报dataset not found;第二,names的列表顺序和txt里第一列的类别ID一一对应,不能只看名称对不对。
如果你在Roboflow里删过类别再加回来,ID很可能已经不是最初的顺序,旧标注全部错位。我碰到过一次:同事在Roboflow里把crown和root的顺序调换了一下,导出后所有txt里的ID都跟着变,训练出来的模型把牙冠全看成牙根。后来我固定了一个习惯:拿到数据先扫描全部txt,统计每个类别ID出现次数,和data.yaml里的names做一次对照,确认无误再开训练。
import os from collections import Counter label_root = "dental_dataset/train/labels" counter = Counter() for txt in os.listdir(label_root): with open(os.path.join(label_root, txt)) as f: for line in f: counter[int(line.split()[0])] += 1 print(counter)如果counter里的键范围不在0到nc-1,说明标签和names不匹配,需要回到Roboflow重新导出,或者写重映射脚本修正。这一步不花多少时间,但能挡住后面一整夜的无效训练。
3. 用YOLOv8在牙齿图像上跑通第一版:训练命令、模型选择与参数设定
3.1 装环境与最小训练命令:从COCO预训练的yolov8n.pt开始
用ultralytics包跑YOLOv8是最省事的路径。建议在干净的虚拟环境里装:
pip install ultralytics装完直接跑最小训练命令,不需要自己写训练循环:
yolo detect train \ data=dental_dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=30 \ cos_lr=True几个参数选择:model=yolov8n.pt,意思是加载在COCO2017上预训练好的Nano权重继续微调,而不是从yolov8n.yaml随机权重开始。牙科图像和自然图像相差很大,但backbone前面几层学到的边缘、纹理基础仍然有用,迁移学习在这个场景下几乎总是优于从零训练。
batch=16在6G显存的GTX 1660Ti上能跑动n模型,如果显存只有4G就降到8;patience=30是早停阈值,连续30轮验证指标不涨就自动停。第一次跑最重要的不是指标,而是确认数据加载正常、loss有下降趋势,所以我建议先跑20轮验证流程,再去跑完整训练。
3.2 模型尺寸与显存:牙科数据集该从哪一档开始
| 模型 | 单卡显存参考(imgsz=640, batch=16) | 适合场景 |
|---|---|---|
| yolov8n | 约4GB | 几百张小数据集、边缘部署、试跑流程 |
| yolov8s | 约6GB | 中等数据集的主力选项 |
| yolov8m | 约10GB | 数据量大且小目标多的场景 |
| yolov8l/x | 16GB以上 | 大图高精度,但小数据集极易过拟合 |
牙科标注数量通常只有几百到几千张,我一般从n或s起步。原因有两个:第一,小模型在数据量不足时反而更稳,不容易背题;第二,模型最终如果想部署到RK3588这类带NPU的边缘设备上,n和s级别才跑得动,m以上算力直接不够。别一上来就选l/x,数据量撑不住,训练时间也成倍增长。
3.3 imgsz、epochs与增强:牙齿小目标对分辨率极其敏感
默认imgsz=640能跑,但牙齿影像里真正的难点是小目标——细小的龋坏点、根尖阴影可能只有几十像素。640分辨率下这些目标经过网络多次下采样后,有效特征只剩几个像素,检测头几乎没法定位。这类数据集我建议准备两档配置:先用640跑一版建立baseline,然后把imgsz调到960甚至1280再看小目标AP有没有明显上涨。显存不够就减batch,例如:
yolo detect train \ data=dental_dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=960 \ batch=8 \ flipud=0.0 \ fliplr=0.5 \ mosaic=0.5这里flipud=0.0是默认值,但它必须保持为0:上牙和下牙在解剖上有明确的方向性,垂直翻转会让模型学到错误的上下颌关系。fliplr保留0.5是因为口腔左右大致对称,翻转后的图像依然合理,相当于把数据量翻倍。mosaic降到0.5是我在牙齿数据上的习惯,mosaic把四张图拼一起,牙齿轮廓会被切成奇怪形状,比例太高模型容易学到残缺牙列的伪影;保留0.5让模型见到的正常图更多。
epochs提到150是因为分辨率提高后收敛变慢,patience照旧,配合早停不会浪费太多时间。每轮训练结束后,ultralytics会自动生成完整的损失函数曲线图在runs/detect/train/results.png,里面有train_box_loss和val_box_loss两条线,如果val在某一轮后开始回升,说明过拟合,早停会接住。
4. 牙齿数据集训练避坑:五个现象、原因与解决方法
4.1 小目标漏检:龋坏点在640分辨率下直接消失
现象:验证集里牙冠、种植体这类大目标检得好,但细小的龋坏点几乎全部漏掉,precision还行,recall明显拉胯。
原因:牙齿影像的特点是目标密、目标小、背景结构复杂。默认YOLOv8检测头在P3/P4/P5层上做预测,stride分别为8/16/32,一个20像素的龋坏点在P3层只剩两三个像素的特征,检测头根本分不清它是病灶还是噪声。
解决:优先提高imgsz到960以上,让目标在输入里占更多像素;如果显存撑不住,可以做切片训练——把高分辨率原图切成512或640的patch,patch之间留10%到20%重叠,训练和推理都走切片。另一种做法是修改模型yaml,把stride=8的P2特征也接进检测头,也就是自定义一个小目标检测分支,但代价是整体训练变慢、显存占用上升,数据量小的场景收益有限。我在牙科项目里的顺序是:先提imgsz,再切patch,最后才动网络结构。
4.2 过拟合:训练loss一路向下,验证mAP在30轮就到顶
现象:train_box_loss降到0.2以下,但val指标在某个epoch到顶后不再涨,甚至往下跌。
原因:牙科标注集通常只有几百张,模型容量一大就把训练图背下来了。更糟糕的是,口腔照片的背景高度重复——同一台牙椅、同样的反光板、类似的拉钩,这些背景让模型更容易走捷径。
解决:把模型从yolov8m降回yolov8n或yolov8s;把mixup打开,ultralytics默认mixup=0.0,牙科数据开到0.2左右可以让模型不那么依赖局部纹理;epochs控制在100到150,配合patience早停。还有一个我常用的手段是K-Fold交叉验证:把所有train和valid合并,按患者分组做5折,对每一折单独训练并记录mAP,最后看均值和方差。数据量小的时候,交叉验证的结果比单次划分可信得多。
4.3 mAP虚高:同一患者的图像横跨训练和验证
现象:训练验证时mAP有0.85,换一批新患者片子直接掉到0.5以下,现场完全不敢用。
原因:这是医学影像数据里最典型的翻车场景。Roboflow导出时默认随机划分train/valid,如果你手里的数据是一个患者多张不同角度的片子,随机划分会把这些片子的兄弟分到两侧,模型等于提前见过验证集的一半内容。牙科临床数据尤其容易这样:一个患者可能拍了全景片、根尖片、术前术后多张照片,它们长得高度相似。
解决:划分必须按患者patient_id分组,而不是按图片分组。做法我在下一章给一个完整脚本。这里先记住结论——任何医学影像数据集,只要不按患者隔离,mAP高得再漂亮都不能信。
4.4 重复框压不住:同一颗牙被框了三次
现象:推理结果里同一颗牙上叠着三四个IoU很高的框,置信度还都不低。
原因:牙齿排列紧密、形态相似,模型给出的候选框密集。YOLOv8默认推理时nms_iou=0.7,这个阈值对密集目标太宽松,相邻牙齿和同一颗牙的候选框都很难被压掉。
解决:推理时把iou降到0.5左右、conf提到0.3以上:
yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=test_images/ \ conf=0.3 \ iou=0.5如果还是压不住,就把全景大图切成左右半口分别推理再合并结果。目标密度降一半,NMS的压力会小很多,这是我在全景片上的习惯做法。
4.5 损失曲线剧烈震荡:lr和batch互不匹配
现象:训练到30轮后,train_box_loss出现锯齿状抖动,val指标也跟着上下跳,看起来完全没有收敛。
原因:小batch配合默认lr0=0.01时,梯度估计的噪声很大;牙科数据类别又不均匀,个别类别的梯度频繁主导更新方向,loss曲线就抖得厉害。
解决:把lr0降到0.005或0.002,同时尽量把batch顶到显存上限。打开cos_lr=True让学习率在后半段平滑衰减,震荡会明显缓解。看训练状态的时候不要只看train loss,重点看results.png里val_box_loss和metrics/mAP50两条线,它们平稳才说明模型真的在收敛。
5. 让牙齿数据集真正可用:按患者拆分、类别重映射与增强策略
5.1 按患者拆分的脚本:同一颗牙不能既在训练集又在验证集
核心是把文件名的患者ID解析出来,用患者做单位划分。很多数据集文件名里带了患者ID,例如pat001_01.jpg表示患者1的第一张图。脚本先按patient_id做分组,再按组拆分:
import os import re import shutil import random from pathlib import Path from collections import defaultdict dataset_root = Path("dental_dataset") all_image_paths = list((dataset_root / "train/images").glob("*.*")) patient_images = defaultdict(list) for img_path in all_image_paths: # 从文件名 "pat001_01.jpg" 提取 "pat001" match = re.match(r"(pat\d+)", img_path.stem) if not match: print("无法解析患者ID:", img_path.name) continue patient_images[match.group(1)].append(img_path) patient_ids = list(patient_images.keys()) random.seed(42) random.shuffle(patient_ids) val_count = max(1, int(len(patient_ids) * 0.2)) val_patients = set(patient_ids[:val_count]) for pid, imgs in patient_images.items(): for img_path in imgs: label_path = dataset_root / "train/labels" / (img_path.stem + ".txt") dest_split = "valid" if pid in val_patients else "train" dest_img = dataset_root / dest_split / "images" / img_path.name dest_lbl = dataset_root / dest_split / "labels" / (img_path.stem + ".txt") shutil.move(str(img_path), str(dest_img)) if label_path.exists(): shutil.move(str(label_path), str(dest_lbl)) else: print("缺少标签:", label_path)逻辑:先统计所有患者,洗牌后取20%作为验证患者,然后逐个把图像和对应txt移动到valid或train目录。最重要的细节是洗牌前先固定random.seed(42),否则两次运行得到不同拆分,实验结果没法对比。脚本设计成只运行一次,重复运行前先把目录整理干净。如果文件名里没有患者ID,就得让数据提供方补一个患者字段的CSV,用CSV里的patient_id做group,这一步绕不开。
5.2 类别重映射与类别平衡:把细粒度标签合并成临床口径
Roboflow的原始标注可能很细,比如把龋齿分成浅龋、深龋、邻面龋,但你的临床目标只需要一个龋齿类。重映射就是把多个旧类别ID合并成一个新ID:
# 旧类别ID -> 新类别ID remap = { 0: 0, # shallow_caries -> caries 1: 0, # deep_caries -> caries 2: 1, # crown -> crown 3: 2, # root -> root 4: 2, # root_canal -> root } for split in ["train", "valid", "test"]: label_dir = Path(f"dental_dataset/{split}/labels") for txt in label_dir.glob("*.txt"): lines = txt.read_text().strip().splitlines() new_lines = [] for line in lines: parts = line.split() old_cls = int(parts[0]) new_cls = remap.get(old_cls) if new_cls is None: continue # 丢弃不需要的类 parts[0] = str(new_cls) new_lines.append(" ".join(parts)) txt.write_text("\n".join(new_lines) + "\n")写完之后记得同步改写data.yaml的names,让names顺序与重映射后的ID一致。这一步最容易出错,脚本不会提醒你data.yaml已经过期,训练时也不会报错,只会让类别混淆矩阵变得不可读。
重映射之后通常会暴露类别不平衡:龋齿可能有5000个框,种植体只有200个。YOLOv8的损失函数没有直接提供类别权重参数,常见做法是给少样本类过采样——把含种植体的图片在训练列表里多复制几份,每轮迭代期望见到的次数就上去了。这个操作在数据准备阶段做,比在训练阶段硬调loss要直观,我一般按“最少类框数接近最多类的一半”来控制复制倍数。
5.3 增强策略:牙科影像哪些增强开、哪些必须关
ultralytics的增强参数都可以在训练命令里直接覆盖。我给牙齿数据集的一版常用增强配置是:
yolo detect train \ data=dental_dataset/data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=960 \ batch=8 \ flipud=0.0 \ fliplr=0.5 \ mosaic=0.5 \ mixup=0.2 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4参数逐个说原因:flipud保持0.0,上牙翻到下牙会让解剖语义错乱;fliplr保留0.5,口腔左右对称,翻转后仍然符合临床形态,数据量等于翻倍;mosaic降到0.5,四图拼接容易产生不连续的牙列,保留一半mosaic一半原图可以让模型学到的背景更真实;mixup从默认0.0开到0.2,两张图线性混合可以缓解小数据集的过拟合,对医学图像这种纹理敏感任务效果不错;hsv三个参数只对彩色内窥镜照片有意义,如果数据是X光灰度图,可以把hsv_h降为0,避免给灰度图注入伪色彩。
另外要注意:Roboflow导出时会提供生成增强版本的选项,如果你已经在训练里开了增强,就不要再用Roboflow的增强副本,否则同一患者的多个增强变体会跨train/valid,又变成变相的数据泄露。
6. 训练完先别急着上线:用混淆矩阵、逐类AP与一张域外片子验收
6.1 验证命令与混淆矩阵
训练完成后,最容易被忽略的是runs目录下的confusion_matrix.png。用验证命令先确认最终指标:
yolo detect val model=runs/detect/train/weights/best.pt data=dental_dataset/data.yaml跑完打开confusion_matrix.png,重点看两类错误:正常牙齿被误判成龋齿,以及龋齿漏检成背景。这两者在临床上代价完全不同,假阳性顶多多拍一张片子,假阴性会漏掉真实病灶,单看mAP根本看不出这种差异。终端还会打印每一类的AP明细,拖后腿的类决定了下一步往哪补数据。
6.2 用一张域外片子做最终验收
指标再漂亮,都不如拿一张训练和验证里都没见过的新患者片子跑一次推理。把验证集里随机留出的新患者图丢进模型:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( "holdout/pat099_pre.jpg", conf=0.25, iou=0.5, save=True, )这里conf用0.25、iou用0.5,是牙科密集目标的常用组合。如果这张片子验证通过,再拿不同设备、不同角度甚至不同医院的数据试,直到能找到模型的边界——知道它在什么条件下可靠、什么条件下会乱。图像尺寸如果和训练时不一致,推理会自动resize,但框坐标会对应回原图,这一点ultralytics处理得比较好。
6.3 热力图:看模型到底在看哪里
如果你怀疑模型是凭病灶还是凭背景纹理做的判断,可以给backbone最后一层卷积做一个EigenCAM热力图。YOLOv8没有现成的CAM接口,常见做法是在推理时拿到model.model.model[-2]的卷积输出,用pytorch_grad_cam库反向传播一次,把热力图叠加回原图。注意Hook必须作用在backbone输出而不是Detect头上,否则梯度会被解码流程冲掉。热力图对调试某类误判特别有用,比如模型总是把牙冠边缘的高光当成龋齿时,热力图会显示它根本没在看病灶区域。但日常验收不是每次都要做,先用混淆矩阵和域外片子,怀疑具体类别出问题时再上也不迟。
我吃到过最深刻的教训是:mAP只是数据集的成绩单,不代表临床现场的表现。每一版模型验收时,我都会强制让自己先跑一张域外片子,再决定要不要让别人用。希望帮到你,少走我走过的弯路。
本文还有配套的精品资源,点击获取