简介:这份资源面向从事医学图像分割、牙齿病变检测的算法工程师与研究者,提供一套labelme格式的牙齿及牙齿病变标注数据集,可用于语义分割或实例分割模型的训练与验证。压缩包内共1个docx文件,约2.22MB,主要承载数据集说明与下载指引,实际数据为2616张jpg图片及对应的2616个json标注文件,不含mask文件。数据集涵盖Tooth、Caries、Cavity、Crack、calculus、inflamation共6个类别,各类别标注数量分别为1709、2913、1099、139、1207和620,使用labelme 5.5.0以多边形框方式标注。需注意部分牙齿未标注,因分割目的以病变检测为主,不影响病变识别;json文件需自行转换为mask、YOLO或COCO格式。目前已有122人学习,适合需要牙齿病变检测数据、希望快速开展分割实验的读者参考使用。
1. 牙齿病变分割数据集:2616 张 labelme 标注,6 类病变怎么落地
牙科影像的 AI 辅助诊断这两年从论文走向了椅旁,但真正动手做分割的人都会卡在同一个地方:找不到带精细多边形标注的牙齿病变数据。公开数据里要么只有全景片分类标签,要么只标了牙齿轮廓、没标病变。这份 2616 张的 labelme 格式牙齿分割数据集,恰好补的就是这个缺口——它把 Tooth、Caries、Cavity、Crack、calculus、inflamation 六类病变都用 polygon 画了出来,图片和 json 一一对应,2616 对,不多不少。
它适合谁?做口腔影像分割的算法同学、想验证病变检测 pipeline 的工程师、需要一份可编辑标注做二次加工的研究者。注意它的定位:分割目的是查病变,所以部分正常牙齿没标注,这不是标注事故,是设计取舍。下面从格式、转换、训练、排错一路拆到能跑。
2. labelme 格式拆解:json 里到底存了什么,怎么读
2.1 一份 json 的字段结构与六类标签
labelme 5.5.0 导出的 json 是标准结构,核心就三块:imagePath、imageData(可选)、shapes。每个 shape 里有label、points、shape_type、flags。这份数据集用的是 polygon,所以shape_type全是polygon,points是一串[x, y]顶点,不是矩形框。六类标签名严格区分大小写:Tooth、Caries、Cavity、Crack、calculus、inflamation。注意inflamation是数据集里的拼写,转换脚本里必须原样匹配,写成inflammation会直接漏掉 620 个标注。
先写个脚本把标注分布和图片尺寸摸清楚,别急着转格式。很多人一上来就转 mask,结果类别对不上、坐标越界,回头查半天。
import json, os, glob from collections import Counter json_dir = "annotations" # json 所在目录 img_dir = "images" # jpg 所在目录 label_counter = Counter() size_set = set() bad_files = [] for jp in glob.glob(os.path.join(json_dir, "*.json")): with open(jp, "r", encoding="utf-8") as f: data = json.load(f) # 记录图片尺寸,用于后续校验坐标是否越界 h, w = data.get("imageHeight"), data.get("imageWidth") size_set.add((w, h)) for shape in data["shapes"]: label_counter[shape["label"]] += 1 # 检查多边形顶点是否落在图像范围内 for x, y in shape["points"]: if x < 0 or y < 0 or x > w or y > h: bad_files.append(jp) break print("类别分布:", dict(label_counter)) print("图片尺寸集合:", size_set) print("坐标越界文件数:", len(set(bad_files)))逻辑说明:imageHeight/imageWidth是 labelme 写入的原图尺寸,用它做坐标边界校验最直接。label_counter统计出来的数量应该和简介里的 Tooth 1709、Caries 2913、Cavity 1099、Crack 139、calculus 1207、inflamation 620 对得上,对不上说明有 json 损坏或标签拼写被改过。size_set如果出现多个尺寸,说明图片不是统一分辨率,转 mask 时不能写死宽高。
参数说明:json_dir和img_dir按你解压后的实际目录改;如果 json 里带了imageData(base64 内嵌图),文件会很大,读取时可以用json.load正常处理,但转格式时建议忽略imageData,直接用同名 jpg。
2.2 为什么这份数据不能直接喂给分割网络
labelme 的 json 是标注中间态,不是训练格式。语义分割要的是单通道 mask(每像素一个类别 id),实例分割要的是 COCO 的 polygon 或 RLE,YOLO 分割要的是归一化的class x1 y1 x2 y2 ...文本。这份数据集只给 jpg + json,mask 要自己转,这是它最容易被低估的一步。
另一个坑是「部分牙齿没标注」。如果你按语义分割做,未标注区域在 mask 里是背景(0),模型会把正常牙齿也学成背景,导致推理时牙齿区域被吞。常见做法是:要么只把六类病变当作前景、Tooth 单独处理,要么在 loss 里对未标注像素做 ignore。我一般会在转 mask 时额外生成一张ignore_mask,把「有牙齿但没标注」的区域标出来,训练时权重置零。这一步不做,mIoU 看着还行,实际病变召回会很难看。
3. json 转 mask / YOLO / COCO:三套脚本与参数
3.1 转语义分割 mask:类别映射与 ignore 区域
语义分割 mask 是一张和原图同尺寸的灰度图,像素值就是类别 id。映射关系自己定,但一旦定了就不能改,否则权重白训。
import json, os, numpy as np from PIL import Image, ImageDraw # 类别到 id 的映射,0 留给背景 CLASS_MAP = { "Tooth": 1, "Caries": 2, "Cavity": 3, "Crack": 4, "calculus": 5, "inflamation": 6 } def json_to_mask(json_path, out_path): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) w, h = data["imageWidth"], data["imageHeight"] mask = Image.new("L", (w, h), 0) # 全背景 draw = ImageDraw.Draw(mask) for shape in data["shapes"]: label = shape["label"] if label not in CLASS_MAP: continue # 未知标签跳过,避免污染 pts = [tuple(p) for p in shape["points"]] draw.polygon(pts, fill=CLASS_MAP[label]) mask.save(out_path) for jp in os.listdir("annotations"): if jp.endswith(".json"): json_to_mask(os.path.join("annotations", jp), os.path.join("masks", jp.replace(".json", ".png")))逻辑说明:Image.new("L", ...)建单通道图,draw.polygon按顶点填充类别 id。多边形重叠时后画的覆盖先画的,这份数据六类病变基本不重叠,但 Tooth 和病变可能叠,建议把 Tooth 放最后画或单独处理。CLASS_MAP里没列的标签直接跳过,防止有人手动加了新类导致 id 错位。
参数说明:fill=CLASS_MAP[label]的值就是训练时num_classes的依据,这里最大是 6,所以num_classes=7(含背景)。如果你想把 Tooth 当背景只留五类病变,把Tooth从映射里删掉即可,但记得同步改num_classes。
3.2 转 YOLO 分割格式:归一化与类别索引
YOLO 分割的标签是 txt,每行class_index x1 y1 x2 y2 ...,坐标全部归一化到 0~1。类别索引从 0 开始,和CLASS_MAP的 id 要错开一位。
import json, os CLASS_LIST = ["Tooth", "Caries", "Cavity", "Crack", "calculus", "inflamation"] def json_to_yolo(json_path, out_txt): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) w, h = data["imageWidth"], data["imageHeight"] lines = [] for shape in data["shapes"]: if shape["label"] not in CLASS_LIST: continue cls_id = CLASS_LIST.index(shape["label"]) # 归一化并保留 6 位小数,减少文件体积 coords = [] for x, y in shape["points"]: coords.append(f"{x / w:.6f}") coords.append(f"{y / h:.6f}") lines.append(f"{cls_id} " + " ".join(coords)) with open(out_txt, "w") as f: f.write("\n".join(lines))逻辑说明:CLASS_LIST.index保证类别顺序固定,训练时的data.yaml里names必须和这个列表完全一致。归一化用x/w、y/h,注意是除以宽和高,别写反。保留 6 位小数是经验值,再多没必要,再少可能丢精度。
参数说明:YOLOv8-seg 的data.yaml里nc: 6,names按CLASS_LIST顺序写。如果某个类别样本极少(比如 Crack 只有 139 个),训练时容易欠拟合,可以在data.yaml里不做特殊处理,而是在 loss 里用cls_pw加权,或者对含 Crack 的图做 oversampling。
3.3 转 COCO 格式:实例分割的 category_id 与 segmentation
COCO 的实例分割用 polygon 的segmentation字段,格式是[[x1,y1,x2,y2,...]],category_id从 1 开始。这份数据转 COCO 最省事,因为 labelme 的 polygon 几乎可以直接搬。
import json, os from pycocotools import mask as mask_util # 可选,用于 RLE coco = {"images": [], "annotations": [], "categories": []} CLASS_LIST = ["Tooth", "Caries", "Cavity", "Crack", "calculus", "inflamation"] for i, name in enumerate(CLASS_LIST, start=1): coco["categories"].append({"id": i, "name": name, "supercategory": "lesion"}) ann_id = 1 for img_id, jp in enumerate(os.listdir("annotations"), start=1): if not jp.endswith(".json"): continue with open(os.path.join("annotations", jp), "r", encoding="utf-8") as f: data = json.load(f) w, h = data["imageWidth"], data["imageHeight"] coco["images"].append({ "id": img_id, "file_name": jp.replace(".json", ".jpg"), "width": w, "height": h }) for shape in data["shapes"]: if shape["label"] not in CLASS_LIST: continue seg = [[c for p in shape["points"] for c in p]] # 展平 xs = [p[0] for p in shape["points"]] ys = [p[1] for p in shape["points"]] coco["annotations"].append({ "id": ann_id, "image_id": img_id, "category_id": CLASS_LIST.index(shape["label"]) + 1, "segmentation": seg, "bbox": [min(xs), min(ys), max(xs)-min(xs), max(ys)-min(ys)], "area": (max(xs)-min(xs)) * (max(ys)-min(ys)), "iscrowd": 0 }) ann_id += 1 with open("instances.json", "w") as f: json.dump(coco, f)逻辑说明:segmentation用展平的顶点列表,COCO 接受这种 polygon 格式。bbox用顶点极值算,area用外接框面积近似,严格来说应该用多边形面积,但多数框架对area不敏感。iscrowd=0表示单实例。
参数说明:category_id从 1 开始,和categories里的id对应。如果后续用 Detectron2 或 MMDetection,instances.json直接注册即可。注意这份数据没有 mask 文件,COCO 的segmentation是 polygon,不是 RLE,别混。
4. 避坑与排查:标注、转换、训练里最容易翻车的五件事
4.1 现象:转完 mask 全是黑图,一个白点都没有
原因:json 里的points是相对坐标(0~1)而不是像素坐标,或者imageWidth/imageHeight读成了字符串。labelme 默认存像素坐标,但有人手动改过 json 就会出问题。
解决:先跑 2.1 的校验脚本,打印size_set和几个points的极值。如果points最大值小于 1,说明是归一化坐标,转 mask 时要乘回宽高。如果imageWidth是字符串,int()转一下。
4.2 现象:训练时 loss 正常下降,但验证集 mIoU 一直 0.2 以下
原因:未标注的牙齿区域被当成背景,模型把大量正常牙齿学成了背景类,病变区域被稀释。这份数据「部分牙齿没标注」是设计如此,但直接训就会踩这个坑。
解决:生成 ignore mask,把「有牙齿但无标注」的区域在 loss 里忽略。简单做法是用一个牙齿检测模型先框出牙齿区域,再和标注区域做差集。或者退一步,只把六类病变当正类,Tooth 不参与 loss。
4.3 现象:Crack 类别的 AP 几乎为 0
原因:Crack 只有 139 个标注,是六类里最少的,且裂纹在图像上细长、对比度低,polygon 面积小,模型很难学到。
解决:对含 Crack 的图片做 oversampling,或者在 dataloader 里对 Crack 类别做 copy-paste 增强。另外检查一下 Crack 的 polygon 是不是太细导致转 mask 后只剩一两个像素,如果是,转 mask 时对 Crack 做形态学膨胀。
4.4 现象:YOLO 训练报「label out of bounds」
原因:归一化后坐标出现负数或大于 1,通常是 json 里有多边形顶点超出图像边界,或者imageWidth/imageHeight和实际 jpg 尺寸不一致。
解决:转 YOLO 前先 clamp,x = min(max(x, 0), w),y同理。同时用 PIL 读一下 jpg 的实际尺寸,和 json 里的imageWidth对比,不一致的样本直接剔除。
4.5 现象:labelme 打开 json 报错或图片不显示
原因:json 里的imagePath是绝对路径,换机器后路径失效;或者 json 里带了imageData但 base64 损坏。
解决:用 labelme 打开时选「Open Dir」而不是「Open File」,让它按目录匹配。如果imageData损坏,用脚本把imageData字段删掉,只留imagePath,labelme 会去同目录找同名 jpg。
5. 从标注到推理:一份可复现的验证流程与我的固定习惯
转完格式别急着上大模型,先用一份小样本把整条链路跑通。我一般会抽 50 张图,按 3.1 转 mask,用最简单的 U-Net 训 20 个 epoch,看六类的像素准确率和召回。这一步不是为了刷指标,是为了确认「json → mask → dataloader → loss → 推理可视化」没有断点。断点往往不在模型,而在标注和转换。
验证流程可以固定成四步。第一步,跑 2.1 的统计脚本,确认六类数量和简介一致,坐标无越界。第二步,抽一张图,把 json 的 polygon 用 PIL 画出来,和原图叠在一起看,确认标注位置对得上。第三步,转 mask 后把 mask 和原图并排看,确认病变区域被正确填充、Tooth 和病变没有互相覆盖。第四步,用转好的 YOLO 或 COCO 格式训一个轻量模型,只看 loss 曲线和验证集可视化,不看绝对精度。
这里有个我踩过的坑:早期我直接拿这份数据训语义分割,num_classes设成 6,忘了背景,结果类别 id 整体错位,Tooth 被当成背景,Caries 被当成 Tooth,训了两天才发现。从那以后我每次转 mask 都强制走一遍「打印类别 id 映射 + 抽一张图可视化」的流程,不管多急都不跳过。
再给一个推理阶段的小技巧。这份数据的病变类别里,Caries 和 Cavity 在视觉上容易混,calculus 和 inflamation 也常同时出现。推理时如果发现某张图同时预测出 Caries 和 Cavity,别急着当误检,先看标注里这两类是不是本来就共存。我一般会在后处理里加一条:同一区域同时出现 Caries 和 Cavity 时,保留置信度高的那个,另一类降权。这条规则不是通用的,但在这份数据的分布下能压掉一部分重复框。
最后说数据本身。2616 张、6 类、labelme 5.5.0 标注、polygon 规则,这些参数决定了它适合做病变分割的原型验证和格式转换练习,不适合直接当 benchmark 刷 SOTA。发布方也明确说了不对模型精度作保证,这话实在。你要做的是把它当成一份可编辑的标注底料,按自己的任务转格式、补 ignore、调类别权重。需要这份数据的话,去 download.csdn.net/download/FL1623863129/88570705 拿,解压后先跑统计脚本,再动手转格式。希望帮到你。
本文还有配套的精品资源,点击获取