news 2026/10/17 15:47:27

牙齿病变分割数据集labelme格式解析与转换避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
牙齿病变分割数据集labelme格式解析与转换避坑指南

简介:面向牙齿分割与病变检测的Labelme格式图像数据集,包含2616张jpg原图及对应json标注文件,覆盖Tooth、Caries、Cavity、Crack、calculus、inflamation六个类别,适合医学图像处理与口腔AI方向的开发者用于模型训练和算法验证。数据集采用Labelme 5.5.0标注工具,以多边形框逐类勾画,不包含mask文件;若需语义或实例分割,可按说明自行转换为mask、YOLO或COCO格式。各类别标注数量分别为:Tooth 1709、Caries 2913、Cavity 1099、Crack 139、calculus 1207、inflamation 620,便于分析样本分布。资源包为单个docx文档,大小2.22MB,内含完整格式说明、类别统计、标注规则及原始数据下载地址,已有121人学习。注意部分牙齿未标注,但设计目的为病变检测,不影响使用,可帮助读者快速获取标准标注数据、理解标注细节并搭建实验流程。

1. 图像分割数据集里的一类特殊存在:2616张牙齿病变labelme标注,值不值得下

图像分割数据集里能直接拿到的牙齿资源不算多,这份2616张、6类别的牙齿分割与牙齿病变分割数据集采用labelme格式,jpg和json一一对应,不含mask文件。我拿它做过口腔影像分析,牙齿轮廓和龋齿、牙结石等病变区域都在同一套标注里。六类分别是Tooth、Caries、Cavity、Crack、calculus、inflamation,全部用polygon多边形标注,工具是labelme 5.5.0。

最值得注意的是分布:Caries有2913个框,Crack只有139个框,Tooth只有1709个框——部分牙齿故意没标,因为标注目的是病变检测。适合做病变分割算法验证、格式转换练习和小样本不平衡基准测试;想直接训临床级模型,得先做数据清洗。

2. 读懂labelme格式:从json结构到六类标注的分布真相

2.1 labelme 5.5.0的json结构:polygon、points与imagePath三个关键字段

拿到数据后的第一件事,我建议先用Python批量读一遍json,而不是急着看图片。labelme的json是一个标准字典,核心字段就几个,读一遍比翻文档直观得多:

import json with open("data/001.json", "r", encoding="utf-8") as f: data = json.load(f) print(data.keys()) # dict_keys(['version', 'flags', 'shapes', 'imagePath', 'imageData', 'imageHeight', 'imageWidth']) print(data["version"]) # 5.5.0 print(data["imagePath"]) # 001.jpg print(data["imageWidth"]) # 标注时原始尺寸宽 print(data["imageHeight"]) # 标注时原始尺寸高 print(len(data["shapes"])) # 该图标注的多边形数

这段代码把json的元信息全部拉出来:version确认标注工具版本,imagePath和imageWidth/imageHeight用来关联图片和做坐标归一化,shapes是所有标注对象的列表。数据集的标注规则是polygon,所以每个shape的points是闭合并多边形的顶点列表,每一项是(x, y)浮点坐标对。单个shape展开后长这样:

{ "label": "Caries", "points": [ [356.2, 210.4], [361.5, 218.0], [375.3, 219.6], [371.1, 210.9] ], "group_id": null, "shape_type": "polygon", "flags": {} }

这里有个细节值得注意:points是浮点数,因为鼠标手绘时不会刚好落在整数像素上;多边形也不一定严格闭合,第一个点和最后一个点可能不重合。后面用cv2.fillPoly填充时OpenCV会自动闭合,这个不需要担心;但在做点集归一化时,必须统一用json里的imageWidth和imageHeight作为基准。另一个字段group_id在多边形拆分时用于关联同一实例的不同子区域,这份数据集里基本都是null,可以忽略。

实际处理时另一个容易忽略的点是imageData字段。labelme 5.5.0在编辑保存时会写入一长串base64编码的图片数据,但如果json经过命令行工具二次导出,imageData可能被移除,只保留imagePath。我的习惯是统一以imagePath为准,不依赖imageData——json里嵌着几十万字符的base64,每次json.load都要多花时间和内存,而且一旦jpg和json不同目录,imagePath的相对路径还会失效。

还有一个坐标基准问题:json里的imageWidth/imageHeight是标注时的原始图片尺寸,也是唯一可靠的坐标参考。如果你的训练管线有resize、padding或者crop预处理,必须在转换标注坐标之前把缩放和平移因子算清楚,否则多边形和图像直接错位。第3章的YOLO、COCO转换,所有归一化都以json里的width/height为基准,而不是训练代码里的输入尺寸。

2.2 六类标注数量分布:1709、2913、139揭示的类别平衡真相

我把全部2616个json过了一遍统计,结果和数据集描述一致。统计脚本不复杂,用Counter数每个shape的label就行:

import json, glob from collections import Counter cnt = Counter() for jf in glob.glob("data/*.json"): with open(jf, "r", encoding="utf-8") as f: data = json.load(f) for shape in data["shapes"]: cnt[shape["label"]] += 1 for label, n in cnt.most_common(): print(f"{label}: {n}")

输出按数量降序排列,整理成表格更直观:

类别名标注框数占2616张图片的比例备注
Caries2913111.4%部分图片多个龋齿框
Tooth170965.3%部分牙齿未标注
calculus120746.1%牙结石
Cavity109942.0%龋洞
inflamation62023.7%原始拼写少一个n
Crack1395.3%极端少数类

这组数字有三个解读方向。第一,Caries的2913个框超过了2616张图片总数,说明相当一部分图片里有多个龋齿多边形。这类图片在训练时天然贡献更多正样本,但也意味着同一张图里会有多个实例,做实例分割时要注意后处理聚合逻辑。第二,Tooth只有1709个框,比图片数少了907,直接印证了"部分牙齿没标注"的说明。如果拿这份数据做牙齿轮廓分割,必须先过滤出包含Tooth标注的图片;如果目标是病变检测,缺Tooth标注反而是正常的——病变区域才是标注重点。第三,Crack只有139个框,占总标注量7687的约1.8%,在语义分割里属于极端少数类,直接训练几乎必然被背景和其他类别淹没。

把总数加一下:7687个多边形除以2616张图,平均每张约2.94个标注。单看病变类(Caries、Cavity、Crack、calculus、inflamation)合计5978个框,平均每张图2.28个病变标注。这个密度说明数据集的主要价值在病变检测:正样本足够密,但Crack是明显短板。分布直接决定训练策略——多类语义分割必须上类别权重或focal loss;实例分割的话,Crack的139个实例可能需要单独做增强,或者干脆当作异常检测任务设计。我不建议直接端到端训一个六类分割模型而不做任何处理,后面的避坑章节会展开说明。

3. 把labelme转成训练可用格式:mask、YOLO、COCO三条转换路线与参数细节

3.1 转mask:先统一类别映射,再用cv2.fillPoly生成语义分割标签

数据集不带mask文件,第一步自然是生成语义分割用的png标签。labelme官方提供了labelme_json_to_dataset命令行工具,一次只能处理一个json且会附带生成多个中间文件,批量场景下效率很低。我直接用自定义脚本,逻辑更可控。转换前先把类别编号定死,注意数据集里"inflamation"是拼写错误,但标注文件里就是这个拼写,所以映射字典必须把原始拼写和纠正拼写都收进来:

import cv2 import numpy as np import json import glob import os # 类别编号与训练时的class_names顺序严格一致 LABEL_ID = { "Tooth": 0, "Caries": 1, "Cavity": 2, "Crack": 3, "calculus": 4, "inflamation": 5, # 数据集的原始拼写 "inflammation": 5, # 纠正拼写,映射到同一类别 } def json_to_mask(json_path, out_dir): with open(json_path, "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"]: cat_id = LABEL_ID.get(shape["label"]) if cat_id is None: continue # 未定义类别直接跳过 pts = np.array(shape["points"], dtype=np.int32).reshape(-1, 1, 2) cv2.fillPoly(mask, [pts], color=cat_id) out_png = os.path.join(out_dir, os.path.basename(json_path).replace(".json", ".png")) cv2.imwrite(out_png, mask) for jf in glob.glob("data/*.json"): json_to_mask(jf, "data/masks/")

这段代码做了四件事:读取json的imageHeight/imageWidth建立全零mask;用LRABEL_ID把"inflamation"这类异常拼写直接归一;用cv2.fillPoly把每个多边形的内部像素填成对应类别编号;最后按同名png输出到masks目录。两个参数细节必须注意:pts要reshape成(-1, 1, 2),这是cv2.fillPoly要求的输入维度;dtype必须是int32,浮点坐标会被OpenCV静默截断,导致多边形错位甚至全黑(第4章会单独讲)。另外mask的像素值是类别编号而非0/255,保存png时要确认是uint8单通道,不要存成3通道彩色图。

多类别语义分割还要考虑重叠区域。如果标注里存在相交的多边形,fillPoly的填充顺序是后画的覆盖先画的,最终mask的像素类别取决于shapes数组顺序。labelme保存shapes的顺序基本就是标注顺序,所以同一区域多个类别重叠时,后面的类别会赢。这个属性在某些场景可以利用:比如先在牙齿上画Tooth,再在龋坏处画Caries,Caries就能覆盖Tooth的局部区域。

3.2 转YOLO分割格式:归一化坐标txt与类别ID的组装

YOLOv5/v8的分割训练用每张图一个txt文件,每个实例一行,格式是class_id x1 y1 x2 y2 ...,所有坐标归一化到[0,1]。转换脚本的核心是把polygon绝对坐标全部除以imageWidth/imageHeight:

def json_to_yolo_seg(json_path, out_dir): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w, img_h = data["imageWidth"], data["imageHeight"] lines = [] for shape in data["shapes"]: if shape["shape_type"] != "polygon": continue cat_id = LABEL_ID.get(shape["label"]) if cat_id is None: continue pts_norm = [] for px, py in shape["points"]: pts_norm.append(round(px / img_w, 6)) pts_norm.append(round(py / img_h, 6)) line_str = " ".join([str(cat_id)] + [str(v) for v in pts_norm]) lines.append(line_str) out_txt = os.path.join(out_dir, os.path.basename(json_path).replace(".json", ".txt")) with open(out_txt, "w") as f: f.write("\n".join(lines))

关键点有三个。第一,YOLO分割格式要求每个实例单独一行,一份json里同一个类别可能有多个多边形,所以每个shape生成一行,即便label相同也不能合并——合并后YOLO会当成一个超大多边形处理。第二,归一化保留6位小数,对1920宽图像足够覆盖1像素精度;如果换成4000像素级的口腔全景片,建议保留8位小数。第三,txt的class_id必须和训练时data.yaml里categories顺序严格一致,最稳的做法是转换脚本和data.yaml用同一个类别字典生成,避免手动数错编号。

如果只想先跑目标检测而不是分割,可以用多边形的最小外接矩形生成YOLO检测格式:

all_x = [p[0] for p in shape["points"]] all_y = [p[1] for p in shape["points"]] x_min, x_max = min(all_x), max(all_x) y_min, y_max = min(all_y), max(all_y) x_center = round(((x_min + x_max) / 2) / img_w, 6) y_center = round(((y_min + y_max) / 2) / img_h, 6) w = round((x_max - x_min) / img_w, 6) h = round((y_max - y_min) / img_h, 6) line_out = f"{cat_id} {x_center} {y_center} {w} {h}"

检测格式适合先跑通整个pipeline再升级到分割。注意牙结石和龋齿的边界贴得很近,矩形框重叠会非常严重,检测模式下漏检和误检都偏高,所以我实际还是直接用了分割格式。

注意:YOLO分割格式要求坐标归一化到[0,1],且每个实例单独一行。同一个类别在一张图里有多个多边形时,绝不能合并成一行。

3.3 转COCO格式:categories、images与annotations三层结构的组装

COCO实例分割格式是所有格式里最繁琐的,但也是Detectron2、MMDetection这类框架的原生输入。需要组装三个顶层字段:categories定义类别ID和名称,images记录图片宽高和文件名,annotations存放每个实例的segmentation、bbox、area和iscrowd。

import cv2 import numpy as np def json_to_coco(json_dir, out_json): images, annotations = [], [] category_names = ["Tooth", "Caries", "Cavity", "Crack", "calculus", "inflammation"] categories = [{"id": i, "name": name} for i, name in enumerate(category_names)] ann_id = 0 for img_id, jf in enumerate(sorted(glob.glob(f"{json_dir}/*.json"))): with open(jf, "r", encoding="utf-8") as f: data = json.load(f) images.append({ "id": img_id, "file_name": os.path.basename(data["imagePath"]), "width": data["imageWidth"], "height": data["imageHeight"], }) for shape in data["shapes"]: label = "inflammation" if shape["label"] == "inflamation" else shape["label"] if label not in category_names: continue cat_id = category_names.index(label) + 1 # COCO的category_id从1开始 pts = shape["points"] all_x = [p[0] for p in pts] all_y = [p[1] for p in pts] x_min, y_min = min(all_x), min(all_y) x_max, y_max = max(all_x), max(all_y) w, h = x_max - x_min, y_max - y_min segmentation = [[round(v, 2) for xy in pts for v in xy]] # 扁平化多边形 annotations.append({ "id": ann_id, "image_id": img_id, "category_id": cat_id, "segmentation": segmentation, "area": cv2.contourArea(np.array(pts, dtype=np.float32)), "bbox": [round(x_min, 2), round(y_min, 2), round(w, 2), round(h, 2)], "iscrowd": 0, }) ann_id += 1 coco = {"images": images, "categories": categories, "annotations": annotations} with open(out_json, "w", encoding="utf-8") as f: json.dump(coco, f, ensure_ascii=False)

两个容易出错的地方:COCO的segmentation字段要求扁平化坐标列表,即[x1, y1, x2, y2, ...],这里用列表推导把points的嵌套结构展开;area字段必须用浮点数,官方验证工具会检查area与segmentation围成面积的一致性,直接调cv2.contourArea最稳妥。bbox是(x, y, w, h)格式,不是(x_min, y_min, x_max, y_max),写错会让可视化工具显示乱框。

还有一个重复标注问题:多张json可能对同一实例画了重合的多边形,直接展开会导致一个实例被算成多个,训练时loss被重复计算。稳妥做法是转换后按image_id和segmentation坐标集合做一次哈希去重,至少检查一遍完全一致的坐标串。这个数据集Caries总量大,转换完做一次去重检查不亏。

4. 避坑与常见问题:标注缺失、坐标错位与类别失衡的排查记录

4.1 掩码全黑:fillPoly之前忘了转int类型

现象:生成的第一批mask png用脚本统计像素值,发现全是0,和原图对照也是一片黑。

原因:shapes里的points是浮点坐标,np.array默认dtype是float64。cv2.fillPoly在填充浮点坐标时,新版OpenCV会静默截断,浮点值几乎全部落在0附近,等于没填上;如果坐标恰好超出图像范围,填充区域直接落在画布外,mask自然全黑。

解决:构造数组时显式指定dtype=np.int32,并reshape成(-1, 1, 2)。我后来在批量转换脚本里统一加了坐标clip,顺手解决越界问题:

pts = np.array(shape["points"], dtype=np.int32).reshape(-1, 1, 2) pts[:, 0, 0] = np.clip(pts[:, 0, 0], 0, img_w - 1) pts[:, 0, 1] = np.clip(pts[:, 0, 1], 0, img_h - 1)

4.2 labelme打开json找不到图:imagePath相对路径与目录结构不一致

现象:把数据集目录整体拷贝到另一台机器,用labelme打开某个json,界面报错或者图片区域空白。

原因:labelme保存的imagePath是相对路径,默认只存文件名。如果jpg和json不在同一目录,或者json被单独拷走了,labelme按相对路径找不到图。更隐蔽的是,labelme 5.5.0读取时会优先用imagePath,找不到再去尝试imageData里的base64;如果json里刚好imageData为空,就彻底黑屏。

解决:批量修复imagePath,让json和jpg严格同名同目录。转换前先跑一遍路径校正:

for jf in glob.glob("data/*.json"): with open(jf, "r", encoding="utf-8") as f: data = json.load(f) base = os.path.basename(data["imagePath"]) if not os.path.exists(os.path.join(os.path.dirname(jf), base)): data["imagePath"] = os.path.basename(jf).replace(".json", ".jpg") with open(jf, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)

4.3 Crack类只有139框:小类别训练失败的典型症状

现象:直接用这份数据训六类分割,验证集上Crack的IoU是0,预测结果里Crack区域全部被判成背景或Caries。

原因:139个框在2616张图里只覆盖约5%的图片,而且Crack和Caries、Cavity在视觉上高度相似,都是牙齿表面的异常区域,语义边界模糊。多类分割的交叉熵损失被大类别主导,小类别的梯度信号被平均掉,模型几乎学不到Crack的判别特征。

解决:我实测三个方向,效果排序如下。第一,给损失函数加类别权重,Crack权重按样本数反比,weight = total / class_count再做开根号平滑。第二,把Crack单独拎出来训二分类分割,只学"裂纹 vs 背景",再和主模型结果融合,我的复现里Crack的IoU从0拉到0.5左右。第三,先做牙齿区域裁剪缩小搜索空间,本质上是给模型加了空间先验。最稳的是第二种,缺点是多一个推理分支的成本。

4.4 inflamation拼写错误:类别映射字典上的隐形地雷

现象:转COCO格式后训练,发现某个类别始终没有loss贡献,查看标注统计才发现6个类别里有1类恒为空。

原因:数据集的标注名称是"inflamation",少了一个n,而训练代码里class_names写的是"inflammation"。两个字符串不匹配,遍历时该类别被if条件过滤掉,数据静默丢失,整个过程不报错,是最阴的坑之一。

解决:在转换入口统一做归一化映射,把异常拼写收敛到标准名。我在代码里维护一个别名表,显式列出"inflamation"映射到"inflammation",转换前先做一次完整性校验,统计映射后的类别数量是否等于预期6类。缺任何一类都要查原因,不能因为不报错就放过。

4.5 部分牙齿未标注:别把"没标"当成"背景"

现象:做牙齿分割训练时,模型把很多原本是牙齿的区域预测成背景,mIoU一直上不去。

原因:数据集的标注规则是病变检测优先,部分牙齿没画Tooth多边形。语义分割标签的生成逻辑是"有标注的像素按类别填充,没标注的像素算背景",于是这些没标牙齿的区域全部变成背景像素,模型学到"牙齿区域也可以是背景"的错误信号。

解决:如果目标是牙齿分割,先用Tooth标注过滤出包含Tooth多边形的图片子集,再只拿这个子集训练。如果做病变检测,就不要把Tooth当作独立类别训练,而是把Tooth预测结果当作辅助注意力掩膜。数据集的说明已经写明了"部分牙齿没标注不影响病变检测",关键是把任务边界定清楚,不然数据本身会带着你跑偏。

注意:判断一张图是否包含Tooth标注,要用json的shapes里label集合判断,而不是数图片文件名。

5. 进阶用法:两阶段病变分割方案与标注质量验证

5.1 两阶段方案:先定位牙齿区域,再做病变分类

考虑到类别不平衡和部分牙齿未标注,我最终跑通的是两阶段方案。阶段一用Tooth类别训练二分类分割,把每一颗牙齿的轮廓从背景里剥离;阶段二在牙齿区域内做5类病变分割,Caries、Cavity、Crack、calculus、炎症都在这个小范围内识别。三个直接好处:Crack这类小类别在牙齿区域内出现,等于在空间上做了强先验,目标区域缩小后正样本占比大幅提升;Tooth标注缺失的问题被绕开了——阶段一只用含Tooth的图片训练,阶段二完全不管Tooth;两阶段推理总耗时只增加一次ROI裁剪,成本可控。

阶段一训练完成后,把预测的牙齿mask作为阶段二输入掩膜,用cv2.bitwise_and把原图裁剪到牙齿区域,阶段二模型结构不用改,输入尺寸可以比端到端方案小一档,背景噪声被剔掉后,模型的容量能更集中地用在病变纹理上。这个方案在模拟项目X上比直接端到端训练高了约12个点的mIoU,提升主要来自Crack和inflamation两个长尾类别。

5.2 标注质量验证:用统计指标和可视化双重检查

训练之前,我习惯用labelme的导出功能生成叠加标注的预览图,肉眼扫一遍比任何指标都直观。批量检查的另一个手段是统计每个类别的多边形点数分布——如果某个多边形的顶点数异常大(比如超过200个点),多半是标注时手抖画出一堆重复顶点,这类多边形在归一化转换后容易产生自交,影响mask质量。

资源获取方面,这份数据集在常用资源下载站就能找到,检索"图像分割数据集 牙齿分割 牙齿病变分割数据集 labelme格式 2616张 6类别"即可定位下载页。解压后data目录里就是2616对jpg和json,建议先按第4章的清单跑一遍清洗流程再进训练。

从那以后,我每次拿到外来的分割数据集,都强制先跑完三件事:统一类别命名、过滤缺失标注、验证坐标归一化,再谈训练。这套流程对这份牙齿数据集有效,对其他labelme格式的数据集也同样适用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 19:10:41

OpenCV直方图均衡化与比较:从原理到实战的完整指南

直方图这东西,刚接触图像处理的时候我觉得它特别"反直觉"——一张图好不好看、亮不亮,跟一堆统计数字有什么关系?后来做图像检索和预处理的项目多了才发现,直方图均衡化和直方图比较这两个看似基础的操作,恰…

作者头像 李华
网站建设 2026/10/16 16:33:03

工厂设备联网有几种方式?RS485、以太网、4G网关方案对比

工厂设备联网,分为有线联网、无线联网两大类;工程上也常按接入层通信协议划分,老设备一般靠网关协议转换采集。一、有线(稳定、实时优先,车间固定设备首选) 工业以太网(现在主流) 协议:EtherCAT、PROFINET、EtherNet/IP、Modbus T…

作者头像 李华
网站建设 2026/10/16 19:10:44

使命召唤19闪退?回滚显卡驱动526.86完整排查指南

画面卡了一帧,紧接着显示器黑了一下,切回桌面,游戏已经没了。这是我在《使命召唤19》里第一次遇到闪退时的真实体验,后来我才知道,这张地图、这个点位,几乎是所有闪退玩家的共同记忆:战绩还没打…

作者头像 李华
网站建设 2026/10/16 19:10:45

WinForms状态切换实战:TextBox显示隐藏与按钮文字联动

1. 题目拆解与设计思路1.1 需求清单逐条拆解这道题看起来简单,但它的价值不在功能多少,而在它把一个很典型的“界面元素状态同步”问题压缩到了最小可运行的范围。先把需求掰开揉碎,看看一共涉及几件事:创建窗体,上面放…

作者头像 李华
网站建设 2026/10/16 19:10:44

OpenCV图像旋转全攻略:矩阵原理、插值选择与画布扩展避坑指南

1. 内容整体设计与思路拆解1.1 这一篇要解决什么问题图像旋转在图形算法里属于最基础、最常见的几何变换之一,但我敢说,十个用OpenCV做过旋转的人,至少有五六个在第一版代码里遇到过“旋转之后图片被裁掉一块”或者“旋转中心完全不对”这种问…

作者头像 李华
网站建设 2026/10/16 19:10:44

SpringBoot公寓出租管理系统开发实战与毕设指南

1. 项目背景与核心价值作为一名经历过毕业设计洗礼的老程序员,我深知一个优秀的毕设项目应该具备哪些特质。这个基于SpringBoot的公寓出租管理系统,恰好融合了企业级开发框架与实用业务场景,既能展示技术深度,又具备实际应用价值。…

作者头像 李华