牙齿病变分割数据集解析:labelme六类标注与格式转换实践

发布时间:2026/10/29 23:11:01
牙齿病变分割数据集解析:labelme六类标注与格式转换实践

简介:面向牙齿影像分割与口腔病变检测方向的数据集说明文档,适合计算机视觉学习者、医学图像算法开发者及需要构建牙齿病变检测流程的研究人员参考。文档围绕2616张图像的labelme标注数据展开,交代Tooth、Caries、Cavity、Crack、calculus、inflamation六类标签的标注数量、多边形标注规则及labelme 5.5.0工具信息,并提示部分牙齿未标注不影响病变检测这一使用前提。压缩包共1个文件,为docx文档,大小约2.22MB,主要承载数据集结构、类别统计与格式转换说明,便于读者先评估数据规模再决定是否用于语义分割或实例分割任务。文档还说明json需自行转换为mask、YOLO或COCO格式,且发布方不对训练模型精度作保证。已有122人学习,可作为牙齿病变分割数据选型与标注核查的参考材料。

1. 从一张牙片说起:牙齿分割数据集到底是什么

牙疼去看牙,医生让你拍张片子,指着屏幕上一个发暗的小点说「这儿龋了」。你盯着那张灰度图看了半天,只觉得是一团模糊的黑白影子。牙齿分割和牙齿病变分割要干的事,就是把医生这一眼「看懂了」的过程拆成机器能学的样子:哪里是牙体,哪里有洞,哪里裂了,哪里有结石堆着。而「图像分割数据集」这个词,说的就是一批已经被人工一笔一笔描好边界的图像素材。这份牙齿分割数据集里有 2616 张牙科图像,每张配一份 labelme 标注文件,覆盖 6 个类别,从牙体轮廓到龋洞、裂纹、牙结石、炎症都画了进去。听起来像个「标注好的图片包」,真正有意思的地方却藏在标注规则里——尤其是那句「部分牙齿没有标注」。

2. 为什么病变检测敢「漏标牙齿」:标注设计的取舍

第一次翻这份数据集的说明,绝大多数人会在「部分牙齿没有标注」这句话上停住。按通用数据集的常识,标注不全约等于数据脏了,要么是标注的人偷懒,要么是流程出了问题。但把这句话放回它的任务目标里看——这份数据集的用途是检查病变——它立刻就变成了一个清晰的、经过计算的决定。理解这个决定,比记住六个类别的英文名重要得多,因为它决定了你后面所有预处理代码该怎么写。

2.1 病灶是主角,牙体轮廓是背景板

分割任务里,每一个标签代表一种模型要学着区分的像素。牙体轮廓这类标签的价值,在于给病灶提供一个解剖参照系:知道这颗牙大致在画面哪个位置、边界到哪里,才好判断「这颗牙的某个面上有一块龋坏」。它不承担「数清楚画面里有几颗牙」的职责,所以标不标全,对病变检测的损失函数几乎没影响。

打个比方,检查一栋楼里的火灾隐患,你不需要把每块砖都登记在册,只要标出冒烟的位置和楼体的大致轮廓就够了。漏登几块砖,不影响你判断哪里有火。牙体标注在这份数据里扮演的正是「楼体轮廓」的角色,病灶才是那几个冒烟点。

成本账也算得过来。一张全景牙片里往往有二十多颗牙,每颗都描一遍多边形,就是几十个形状;而病灶通常只有一两个小区域,面积小、边界刁钻,画起来更费神。标注预算固定的情况下,把时间压在病灶边界上,比压在牙缝轮廓上划算得多。这就是为什么 2616 张图里,Tooth 类只出现了 1709 个多边形——平均下来每张图还不到一个。

注意:漏标牙齿是标注规则的一部分,不是标注事故。你拿到数据后不必「补全」牙体,补全反而会引入不属于原始标注规则的形状。

2.2 2616 张图配 2616 个 json,一一对应意味着什么

图片数量 2616,标注文件数量也是 2616,两个数字相等这件事,信息量比看上去大。它说明这个文件夹是「全量覆盖」的:每张图都至少被标注员打开看过一遍,不存在「先标一部分试试水」的半成品状态。

这对切分数据集直接有用。你可以按文件名把图片和 json 一一配对,随手切成训练集、验证集、测试集,不用担心某张图有图无标导致训练时报错,也不用写复杂的对齐逻辑去反查。少了这层麻烦,第一次跑通训练脚本的时间能省下不少。

但一一对应不等于「每个 json 里都有形状」。有些图片里可能确实没有病变,标注员打开看完,保存了一个 shapes 为空列表的 json。这在病变检测数据集里非常正常,甚至是好事:阴性样本能压住模型的误报率,全是病灶的样本集训练出来的模型,见到健康牙片也会硬说有问题。所以拿到手先做一遍统计,别假设「有 json 就有标注」。

下面这两行命令是最省事的核对方式,几十秒就能确认数量对不对得上:

ls images/*.jpg | wc -l # 应该输出 2616 ls annotations/*.json | wc -l # 也应该输出 2616

如果第一个数字是 2616 而第二个不是,先别急着往下走,多半是解压时丢了文件或者目录名不对。再进一步,用一段脚本把每个 json 的类别分布统计出来,顺便找出空标注的样本:

import json, glob from collections import Counter counter = Counter() empty, total = 0, 0 for path in glob.glob("annotations/*.json"): with open(path, "r", encoding="utf-8") as f: data = json.load(f) shapes = data.get("shapes", []) total += 1 if not shapes: empty += 1 # 记录没有任何形状的样本 for s in shapes: counter[s["label"]] += 1 # 按类别名累加多边形个数 print("json 总数:", total) print("空标注数:", empty) for name, n in counter.most_common(): print(f"{name}: {n}")

这段代码没有依赖第三方库,只做一件事:把每个 json 的 shapes 列表读出来,按 label 字段计数。跑完你手上会有一份和官方说明完全对得上的分布表;如果对不上,比如某个类别的名字拼成了别的写法,那就说明文件被改动过,这时候停下来查清楚,比带病训练强。特别提一句,炎症那个类别在原始标注里拼作inflamation,比标准英文少一个 m,你做类别映射时必须原样照抄这个字符串,自己「顺手改正」会让一批标注直接变成未知类别。

2.3 六类标签的分工:谁在定位,谁在定性

六个类别看着平行,其实分两层。Tooth 是解剖结构类,负责告诉模型「牙在哪」;Caries、Cavity、Crack、calculus、inflamation 五个是病理类,负责告诉模型「哪里不对、不对成什么样」。理解这个分层,很多困惑会自动消失——比如为什么 Tooth 的数量远少于图片数,因为它是配角;为什么 Caries 有 2913 个,比图片总数还多,因为一张牙片上常常同时有好几个龋坏区域,配角可以缺,主角不能少。

把每个类别的多边形数除以图片总数,能得到一个「平均每张图出现几次」的粗略指标,这个指标比绝对数量更能说明问题:

类别名中文含义多边形数平均每张图出现次数
Tooth牙齿1709约 0.65
Caries龋坏2913约 1.11
Cavity龋洞1099约 0.42
Crack裂纹139约 0.05
calculus牙结石1207约 0.46
inflamation炎症620约 0.24

六类加起来一共 7687 个多边形,平均到 2616 张图上,每张约 2.94 个。这个数字说明整份数据不算「空」,大多数图里至少有两三处值得标注的东西。同时它也暴露了一个后面必须处理的问题:最少的 Crack 只有 139 个,最多的 Caries 有 2913 个,两者差了二十倍还多,这不是一个可以随便拉个模型就开训的分布。

3. labelme 格式到底存了什么:多边形、标签和图片路径

很多人对「labelme 格式」的理解停在「用 labelme 打开就能看」这一层,真到写转换脚本时才发现自己对里面存了什么一无所知。这一层其实很简单:一份 json 描述一张图,图里每个标注对象是一条带标签的点序列。搞清这三样东西——点序列、标签、图片尺寸——转换代码基本就成型了。

3.1 一个 json 文件的骨架长什么样

抛开冗余字段,一份典型的标注文件结构如下(坐标和尺寸是为了说明而写的示例值,不是真实样本):

{ "version": "5.5.0", "flags": {}, "shapes": [ { "label": "Caries", "points": [[412.5, 233.0], [430.2, 240.8], [441.0, 262.4], [418.7, 258.1]], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "sample_0001.jpg", "imageData": null, "imageHeight": 1024, "imageWidth": 1280 }

shapes是核心,一个列表装下这张图里所有标注对象。每条的label是类别名,必须和六个类别字符串严格对齐。points是多边形顶点的像素坐标,浮点数,原点是图片左上角,x 向右、y 向下,不需要归一化。shape_type标明这是多边形还是矩形、点、线;这份数据集全部按多边形画,所以正常应该是polygon。imagePath是图片的相对文件名,imageData通常是null,因为 labelme 默认不把图片以 base64 内嵌进 json,只在需要打包分发时才开这个选项。最后imageHeight和imageWidth决定了你转换时画布开多大,这两个值不可省略,取错了整张掩码会错位。

一条很常见的翻车经历是:脚本里自己用 PIL 读图片拿尺寸,结果某张图被旋转过、EXIF 方向没处理,读出来的宽高和 json 里记的正好对调,画出来的掩码整个转九十度,模型训练时指标一片玄学。稳妥的做法是优先信 json 里的宽高,再用图片实际尺寸做一次比对,不一致就打印出来单独看。

3.2 polygon、bbox、mask:三种表达方式的取舍

同一个病灶,可以有三种描述方式。外接矩形 bbox 只记左上角和右下角,信息量最小,好处是快、好算 IoU,目标检测用得多。多边形 polygon 记一圈边界点,形状贴合,体积小,还能随时拖动顶点修改,labelme 存的就是这个。掩码 mask 是逐像素的标签图,模型真正吃进去的东西,表达最完整,代价是占空间、不好手改。

三者的关系是一条单向流水线:polygon 栅格化成 mask,polygon 取极值压成 bbox。反过来,从 mask 反推 polygon 要靠轮廓提取,边缘会带锯齿;从 bbox 反推形状则完全不可能,信息已经丢了。

这也解释了为什么这份数据集只给 json 不给 mask。多边形是可编辑的原始资产,标注员画错一个角,双击拖一下就好;换成 mask,改一笔就得重画一片。发布方把最上游的形态给你,下游想要什么形状自己转,主动权在你手上。代价是转换这一步得自己写,而且要写对。

3.3 从 json 到 mask、YOLO、COCO 的转换账

语义分割要的是单通道标签图,每个像素存一个类别编号,0 留给背景。核心逻辑就是拿多边形去填色,后画的覆盖先画的:

import json import numpy as np from PIL import Image, ImageDraw # 类别顺序一旦定下就不要改,训练、推理、评估必须共用同一份映射 CLASSES = ["Tooth", "Caries", "Cavity", "Crack", "calculus", "inflamation"] NAME2ID = {name: i + 1 for i, name in enumerate(CLASSES)} # 0 保留给背景 with open("annotations/sample_0001.json", "r", encoding="utf-8") as f: data = json.load(f) h, w = data["imageHeight"], data["imageWidth"] mask = np.zeros((h, w), dtype=np.uint8) # 单通道标签图 for shape in data["shapes"]: label = shape["label"] if label not in NAME2ID: print("未知类别,已跳过:", label) # 防御拼写不一致的脏标签 continue if shape["shape_type"] != "polygon": continue # 只处理多边形,其他类型先不碰 pts = [(float(x), float(y)) for x, y in shape["points"]] layer = Image.new("L", (w, h), 0) ImageDraw.Draw(layer).polygon(pts, fill=NAME2ID[label], outline=NAME2ID[label]) layer = np.array(layer) # 已占用的像素不覆盖,避免大类吃掉小病灶 mask = np.where((layer > 0) & (mask == 0), layer, mask) Image.fromarray(mask).save("masks/sample_0001.png")

几个参数值得说明。fill和outline都设成类别编号,是为了让多边形边缘不留空洞,只设 fill 时细长裂纹容易出现断点。np.where那行的条件(layer > 0) & (mask == 0)是「先到先得」策略,如果你更希望后画的覆盖先画的,改成直接赋值即可,两种策略在类别重叠处的行为不一样,前者保护小目标,后者尊重标注顺序。真实数据里一颗龋坏牙上的多边形可能互相压边,选哪种要看你的评估方式,但一旦选定就别中途换。

要做实例分割,思路反过来:每个 shape 单独导出一张二值图,文件名带上类别和序号,一套图就是一个实例。要做 YOLO 格式,就把多边形压成外接矩形再归一化:

xs = [p[0] for p in shape["points"]] ys = [p[1] for p in shape["points"]] x1, x2 = max(0, min(xs)), min(w, max(xs)) # 裁到图内,防越界 y1, y2 = max(0, min(ys)), min(h, max(ys)) cx, cy = (x1 + x2) / 2 / w, (y1 + y2) / 2 / h bw, bh = (x2 - x1) / w, (y2 - y1) / h cls_id = CLASSES.index(shape["label"]) # YOLO 的类别从 0 开始 line = f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}"

这里最容易出问题的是类别编号起点。语义分割那套把 0 留给背景,YOLO 的 0 是第一个真实类别,两套映射混用会让 Tooth 和背景互相冒充,训练损失看着在降,验证时一片混乱。所以工程上最好只维护一份「类别名到编号」的字典,所有格式转换都从它派生,别每写一个脚本就抄一遍。

4. 六个类别怎么读:牙体、龋坏、龋洞、裂纹、结石、炎症

六个英文标签摆在一起,非医学背景的人第一眼很难分清 Caries 和 Cavity 到底差在哪,也不明白 Crack 和 Cavity 是不是一回事。这一章不解决医学定义之争,只讲一件事:在一份已经标好的数据集里,这些标签各自代表什么样的视觉区域,以及数量分布透露了什么。

4.1 标注数量分布里藏着三件事

把 7687 个多边形摊开看,第一件事是类别极不平衡。Caries 2913 个,Crack 只有 139 个,二十倍的差距意味着模型哪怕把裂纹全预测错,整体像素准确率照样能上九十九。这不是模型的错,是评估指标选错了。看这份数据必须看每一类的 IoU 或 Dice,别只看一个总数。

第二件事是 Tooth 的覆盖率。1709 个多边形对 2616 张图,覆盖率约 65%。这个数字和说明里「部分牙齿没有标注」完全吻合,它不是一个估计,而是能从文件里数出来的事实。任何需要「每颗牙都被框出来」的下游任务,用这份数据都会缺一块。

第三件事是病灶密度。平均每张图近三个多边形,说明数据不是「一张图一个小点」的稀疏样本,多数图像里有多个区域需要判断,模型得学会在同一张图上同时定位好几处不同性质的病变。这对训练是好事,对显存是坏事——高分辨率牙片加多实例,切图策略要提前想好。

4.2 Caries 和 Cavity 到底差在哪:病变类之间的界线

通俗地说,Caries 描述的是「龋坏这件事」,一个还在发展中的脱矿、变色过程;Cavity 描述的是「已经成了洞」,牙体上出现了实质缺损。前者面积可能更大、边界更模糊,后者往往是一个轮廓明确的暗区。二者在同一颗牙上可以并存,也可以只有其一,所以你会看到某些图里两个类别的多边形挨着甚至部分重叠。

Crack 是线状裂纹,细长、走向明确,和多边形的长宽比特征完全不同,视觉上不太会和块状的 Cavity 混。calculus 是附着在牙面或牙缝的沉积物,它不属于牙体本身,标注位置常在牙龈线附近。inflamation 针对的是软组织区域,画在牙龈那一圈,颜色和质地与牙体差异明显。

这套区分在理论上很清楚,落到人工标注上一定有分歧。同一处边缘模糊的暗区,两个标注员一个标 Caries 一个标 Cavity,完全可能。这类噪声是牙科影像数据集的常态,训练时不要抱着「标签绝对干净」的期待去设计损失函数,适当容忍软边界,比反复清洗数据更实际。

4.3 Crack 只有 139 个:长尾类别怎么处理

139 个多边形,散在 2616 张图里,平均二十张图才有一个。这种长尾类别在训练中几乎必然被淹没。直接开训的结果是模型学会忽略它,因为预测成背景的损失更小。

几种可行的处理方式,按改动成本从低到高排:给类别加权,把 Crack 的损失权重提到其他类的数倍;做重采样,让含裂纹的图在每个 batch 里多出现几次;换 focal loss 之类的机制,让难样本的梯度更突出;或者最省事的做法,先把六个类合并成「牙体」和「病变」两类跑通流程,再逐步拆细。最后这条路常被嫌弃不够「完整」,但对第一次接触这份数据的人,先用两类验证整条流水线能不能跑通,比一上来就六类全开、然后在长尾上反复调参要高效得多。

评估阶段也要相应调整。去掉整体准确率这个指标,改成打印每个类别的 IoU 和召回,哪怕最后 Crack 的召回只有零点几,你至少知道它差在哪,而不是被一个漂亮的总分骗过去。

5. 四个最容易搞混的概念:别把这份数据集用错地方

围绕这份数据集的困惑,大半不是出在技术上,而是出在概念上。下面四条是初学者最常踩的坑,每一条都能让脚本跑通、指标好看、结论全错。

5.1 语义分割与实例分割:同一张牙片,两种问法

易混点:这两个词都指向「给每个像素分类」,名字又像,很多人以为是一回事。

为什么会混:任务描述里常常只写「分割」,而 labelme 的多边形既可以是语义的原料,也可以是实例的原料,同一份 json 支持两种解读,不写清楚就分不清。

怎么区分:问一句「相邻的两处同类病灶,要不要分开数」。语义分割只关心像素属于哪一类,两颗牙上的龋坏挨在一起,输出图里连成一片也不影响结果。实例分割还要回答「这是第几个」,每个多边形必须是独立对象。落到这份数据集上,语义分割把所有多边形烧进一张单通道图,实例分割每个 shape 单独导出一张二值图并编号。选错的结果是掩码结构不匹配,模型结构再对也训不出东西。

5.2 labelme 的 json 与 mask 文件:不是少发了东西

易混点:下载完发现压缩包里全是 jpg 和 json,没有一张 png 掩码,第一反应是「资源不完整」。

为什么会混:大量分割教程直接从「images 文件夹 + masks 文件夹」讲起,看多了就以为掩码是数据集的标准配置。实际上那只是别人转换后的产物,不是标注的原始形态。

怎么区分:json 是标注工具的原生输出,掩码是下游格式。这份数据集明确说了只提供 jpg 和 json,需要 mask、YOLO、COCO 得自己转。换个角度想,拿到多边形反而更自由——想转几种格式就转几种,想要 bbox 就取极值,想要轮廓就填色。真给了掩码,你反而失去了调整边界的能力。转换代码在第 3 章已经给过,照着改路径就能跑。

5.3 「没标注的牙齿」与「标错的数据」:漏标不等于脏数据

易混点:看到某张图里有三颗牙,json 里只有一个 Tooth 多边形,判定为标注遗漏,动手补标或直接删样本。

为什么会混:通用数据集的质量常识是「标注要完整覆盖目标」,这个直觉在检测任务里成立,在病变检测任务里会误伤。

怎么区分:看任务目标和标注规则。这份数据集写明用途是检查病变,牙体只是参照物,漏标不影响病灶的定位与分类。真正该警惕的是另一类问题:多边形明显画到了牙体外、类别名拼错、坐标越界。前者是设计,后者才是错误。区分办法很朴素,随机抽十几张图把多边形叠上去看一眼,画歪的一眼就能看出来,漏标的看过说明就能放下。

5.4 病变检测与牙齿计数:同一份标注,两种目标

易混点:看到有 Tooth 这个类,顺手拿它去训练牙齿计数或牙位识别模型。

为什么会混:类别名太直白,「牙齿」听起来就该对应「所有牙齿」,直觉上 Coverage 应该是百分之百。

怎么区分:算一下覆盖率。1709 个 Tooth 多边形对应 2616 张图,平均每张不到一个,说明大量牙齿根本没标。拿这个子集去计数,结果系统性偏少,而且偏少的方式没有规律。要用这份数据做计数,得先补标牙体,那已经是另一个标注工程了。它擅长的场景只有一个方向:在已知牙体位置的前提下,判断这块区域有没有病变、是哪一种病变。

6. 什么时候该用它,什么时候别碰

判断一份数据集合不合用,不需要读完所有文档,看两件事就够:它的标注规则服务于什么目标,以及你的任务是否共享这个目标。这份 2616 张、6 类、labelme 格式的牙齿分割数据,适用边界大致如下。

你的场景建议原因
牙科病变检测的算法验证合适五类病理标注齐全,阴性样本也有
多类别分割的练手项目合适类别不平衡、长尾、多边形转换,坑都很典型
标注格式转换流水线调试合适只给 json,正好逼你把转换写对
课堂教学或演示合适多边形可编辑,改一个点就能讲清标注规则
牙齿计数、牙位识别不合适Tooth 覆盖率约 65%,计数必然偏少
临床辅助诊断不合适发布方明确不对精度作保证,不能用于真实诊断
需要时序对比的随访研究不合适单张静态图像,没有同一患者的时间序列

举一个具体的例子。假设你要做一个六类语义分割的基线模型,第一步不是搭网络,而是把数据摸清。先数文件确认 2616 对 2616,再跑一遍类别统计,看每个类的多边形数是否和说明对得上,然后把十张图的多边形叠回原图,肉眼确认裂纹那种细长形状没有被填色算法吃掉。这三步做完,你才知道后面该不该给 Crack 加权、该不该把大图切块。跳过这一步直接开训,最常见的结果是训练损失平稳下降,验证时裂纹类 IoU 接近零,你还以为是网络结构不行,换了两三个骨干网络,最后发现是类别权重从来没设对,这种时间浪费最不值。

什么时候别碰它也要说清楚。如果你的目标里包含「数清楚有几颗牙」,这份数据帮不上忙,补标的工作量比重新找一份还大。如果你要交付的是能进诊室的东西,那任何一份不带精度承诺的公开数据都只能当预训练或原型验证的素材,别拿它的指标去谈临床价值。如果你需要同一患者不同时期的对比,静态单图的结构从根上就不支持。

从那以后我每次拿到 labelme 格式的数据集,都强制走一遍「数文件、统类别、叠十张图」这三步,确认没问题才写训练脚本。这份牙齿分割数据集的价值不在于它多完美,而在于它把六类病变的边界都用可编辑的多边形留了下来,你想转成掩码、YOLO 还是 COCO,都还有回头路可走。资料就放在资源发布页里,下载完先跑一遍上面那几行统计代码,再动手改模型,会省下不少返工的夜晚。希望帮到你。

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