资讯详情

labelme格式牙齿分割数据集如何高效转COCO并训练语义分割模型

📅 2026/10/11 18:10:42 | 华诺云谱 👁 阅读
labelme格式牙齿分割数据集如何高效转COCO并训练语义分割模型
简介针对牙齿分割与病变检测的图像分割数据集说明文档适用于医学影像分析、目标检测与分割等方向的开发者也可作为口腔疾病辅助诊断项目的参考。文档基于labelme 5.5.0标注规范介绍了一套包含2616张jpg图像及对应json标注的数据集不包含mask文件标注类别覆盖Tooth牙齿、Caries龋齿、Cavity龋洞、Crack裂纹、calculus牙结石、inflamation炎症六类并给出每类框数Tooth 1709、Caries 2913、Cavity 1099、Crack 139、calculus 1207、inflamation 620。文档特别说明部分牙齿未标注属正常情况因主要目的是病变检测不影响模型训练若需进行语义分割或实例分割可借助labelme打开编辑再将json自行转换为mask、YOLO或COCO格式。这份docx说明文档共1个文件压缩包约2.22MB能帮助你快速掌握数据集的类别分布、标注规则和转换思路。已有116人学习建议在开展牙齿图像分割实验前先通读。1. 2616张labelme格式牙齿分割数据集先别急着训练把三件事想清楚再动手牙齿分割和牙齿病变分割是口腔AI落地的两个地基任务前者告诉系统“牙在哪”后者告诉系统“哪颗牙有问题”。标题里这套组合——labelme格式、2616张、6类别是个分量相当足的开局2616张全景牙片在医学影像数据集里算得上中大规模6类别说明标注不只勾了牙齿轮廓还把病变区域单独拎了出来这让下游能做“病变位置定位”而不是只能做“牙齿计数”。适合谁做全景片、CBCT影像分析的个人开发者或者想做牙科辅助诊断系统的团队。拿到数据集第一反应往往是解压后直接开训但更该做的是三件事验货、转格式、核对标签顺序这三步走错任何一步后面几十个小时训练基本白费。2. 验货环节labelme的JSON结构里有什么直接决定你的转换脚本怎么写2.1 labelme标注文件的每一段字段背后都是一个处理决策labelme是标注工具不是训练框架它产出的每个json文件就是一张图的全部标注信息。牙齿分割数据集如果是labelme格式图片旁边通常躺着一个同名.json。先看一个典型的例子再逐段解释。{ version: 5.0.1, flags: {}, shapes: [ { label: tooth, points: [[112.3, 85.6], [118.0, 92.1], [130.5, 90.0]], group_id: null, shape_type: polygon, flags: {} } ], imagePath: IMG_0001.png, imageData: null, imageHeight: 1536, imageWidth: 2048 }这段结构在labelme导出的文件里非常典型字段含义如下字段含义对后续处理的直接作用shapes标注对象数组每个元素一个目标牙齿和病变都在里面label当前对象的类别名6类标签的字符串藏在这里大小写中英文都可能出现points多边形顶点坐标数组是浮点数不是整数转COCO时千万别提前取整shape_type形状类型牙科影像几乎全是polygon偶尔有rectangleimagePath图像相对路径json移动后最容易失效的字段imageData内嵌图像数据若不为null则是base64编码json会膨胀到几十兆imageHeight/Width标注时的图像尺寸与实际图像尺寸不一致时说明图被换过这里最值得记的一点labelme的坐标是float不是int。标注者画牙冠时不会精确到像素整数系统记录的是浮点坐标。很多团队转格式时图省事直接int()等mask出来发现牙冠边缘全是锯齿这是最常见的第一类翻车现场。另一个常见坑是flags。labelme里flags通常是空对象但有些工作流会用flags记录“这张图有没有病变”“是否难例”。处理时要保留但不参与类别判断。我一般会先写一个只读脚本把所有json的flags值拉一份防止有标注者在里面塞了额外语义而不自知。2.2 六个类别到底叫什么让脚本统计一遍别信文档猜拿到数据集后第一个动作不是读docx说明而是跑标签统计。标题自称“6类别”但实际标签名可能五花八门英文小写、首字母大写、中文、甚至拼写错误。“牙齿”“龋齿”“根尖病变”在多人标注时很容易出现同义不同名。import json, glob, os, collections def collect_all_labels(json_dir): label_counter collections.Counter() shape_type_counter collections.Counter() for jf in glob.glob(os.path.join(json_dir, *.json)): with open(jf, r, encodingutf-8) as f: data json.load(f) for shape in data[shapes]: lab shape[label].strip() label_counter[lab] 1 shape_type_counter[(lab, shape[shape_type])] 1 return label_counter, shape_type_counter label_counter, shape_counter collect_all_labels(annotations) print(label 统计) for label, cnt in label_counter.most_common(): print(f {label:20s} {cnt}) print(label shape_type 组合) for key, cnt in shape_counter.most_common(): print(f {key[0]:20s} {key[1]:10s} {cnt})这段脚本按label分组统计所有标注对象的出现次数同时统计label与shape_type的组合为的是确认6个类别里有没有混进矩形标注或多边形标注。输出后建议人工核对一遍正常牙齿数据集里牙齿主类别占比最高病变类占比往往只有个位数百分比。如果统计出来的类别不止6个看是不是大小写混用或中英文混用比如tooth和Tooth被统计成两个类别这在多人标注的数据集里很常见。另一个要留意的是空标注如果统计发现某个json的shapes列表为空也就是这张图没有任何标注。后续转COCO时它的annotations会是空的训练脚本如果默认每张图都有mask就会直接报错。2.3 配对与一致性检查图片、json、尺寸三方对不齐训练时全炸labelme的imagePath记录的是标注时的相对路径。数据集经过下载、解压、重命名后这个字段经常失效。还有一类问题是图像被重新导出过分辨率变了但json里的imageHeight/Width还是旧的。跑一次三方对账import os, json from PIL import Image IMG_DIR images JSON_DIR annotations missing_img [] # json 引用了不存在的图 size_mismatch [] # json 里的尺寸和实际图不一致 for fn in os.listdir(JSON_DIR): if not fn.endswith(.json): continue with open(os.path.join(JSON_DIR, fn), r, encodingutf-8) as f: data json.load(f) img_path os.path.join(IMG_DIR, data[imagePath]) if not os.path.exists(img_path): missing_img.append(fn) continue with Image.open(img_path) as im: real_w, real_h im.size if (real_w, real_h) ! (data[imageWidth], data[imageHeight]): size_mismatch.append((fn, data[imageWidth], data[imageHeight], real_w, real_h)) print(缺图 json, len(missing_img)) for f in missing_img[:10]: print( , f) print(尺寸不一致, len(size_mismatch)) for f, jw, jh, rw, rh in size_mismatch[:10]: print(f {f}: json{jw}x{jh} 实际{rw}x{rw})逻辑说明脚本对每个json先确认对应图片存在再比对标注时记录的尺寸和实际尺寸。缺图比尺寸不一致更危险——缺图样本在训练时会让DataLoader崩掉尺寸不一致则会造成mask和图像对不上训练不报错但指标永远上不去。参数说明这里用PIL读图而不是OpenCV是因为有些牙科影像可能是16bit灰度PNGOpenCV默认按8bit读会静默截断。PIL至少能忠实地返回尺寸。如果发现大量尺寸不一致后续统一缩放到固定输入尺寸时要按实际图像尺寸缩放标注坐标而不是按json里记录的值缩放。3. 把labelme转成COCO多点归一化、坐标精度与可视验证3.1 为什么宁可多写一次转换也不直接用labelme json训练labelme是标注环节的工具不是训练环节的数据接口。每张图一个json没有统一的类别表、没有图像清单、没有默认的数据集划分而训练框架里无论是检测还是分割都期望数据能一次性加载、按索引访问。COCO格式恰好补上这些单个json聚合了images、annotations、categories三张主表生态里的评估脚本、可视化工具、数据增强库都能直接消费。做牙齿分割时如果后面要切到实例分割每颗牙单独一个实例COCO的polygon和RLE都有现成支持如果只是语义分割COCO也能在训前一次转成mask目录。还有一个实际理由团队协作。标注工作是多人分批做的labelme的元数据字段version、group_id不可控COCO经过一次标准转换后类别映射、坐标格式、字段顺序全都固定了后续谁接手都不用再猜。我一般会保留两套中间产物原始labelme json作为标注底稿转换后的COCO作为训练唯一入口两边用脚本保证同步绝不手工改COCO。3.2 labelme2coco转换脚本鞋带公式算面积坐标保持float转换的核心工作是三件事把多边形的二维点阵打成一维浮点数组按类别映射表填category_id为每个标注补上bbox和area。下面是一个可直接落地的版本。import json, os, glob import numpy as np def polygon_area(points): 鞋带公式计算多边形面积返回平方像素数 pts np.asarray(points, dtypenp.float64) if pts.shape[0] 3: return 0.0 x pts[:, 0] y pts[:, 1] return 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))) def flatten_points(points): COCO 的 polygon 需要一维扁平列表 [x1,y1,x2,y2,...] return np.asarray(points, dtypenp.float64).flatten().tolist() def labelme_to_coco(ann_dir, img_dir, category_names, out_path): coco { images: [], annotations: [], categories: [{id: idx, name: name} for idx, name in enumerate(category_names, start1)], } ann_id 1 for img_id, json_file in enumerate( sorted(glob.glob(os.path.join(ann_dir, *.json)))): with open(json_file, r, encodingutf-8) as f: data json.load(f) coco[images].append({ id: img_id, file_name: os.path.basename(data[imagePath]), width: data[imageWidth], height: data[imageHeight], }) for shape in data[shapes]: label shape[label].strip() if label not in category_names: continue # 未知标签先跳过避免把噪音带进训练 cat_id category_names.index(label) 1 pts shape[points] if len(pts) 3: continue # 少于3个点无法构成多边形 xs [p[0] for p in pts] ys [p[1] for p in pts] bbox [min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)] area polygon_area(pts) coco[annotations].append({ id: ann_id, image_id: img_id, category_id: cat_id, segmentation: [flatten_points(pts)], area: area, bbox: bbox, iscrowd: 0, }) ann_id 1 with open(out_path, w, encodingutf-8) as f: json.dump(coco, f, ensure_asciiFalse) # 顺序即 category_id跑之前先按第2章统计确认类别名 labelme_to_coco(annotations, images, [tooth, caries, gingiva, root, pulp, periapical_lesion], train_coco.json)脚本逻辑分四层外层遍历json并建立image表内层遍历每个shapes生成annotation未知标签直接跳过最后整体落盘。area用鞋带公式算真实多边形面积而不是bbox面积COCO评估工具在算mask AP时会按这个字段做归一化填错会拿到一个自己骗自己的高AP。参数说明里有一个必须强调的坑category_names的顺序就是最终category_id。同一个数据集如果有人把顺序从[tooth,caries]改成[caries,tooth]训练结果会完全错位。我的习惯是先用第2章统计脚本拿到全部label集合人工确认一次语序再把这个列表写进转换脚本任何新增或改名都要重新走统计流程不直接在脚本里加一行。另一个值得注意的点bbox用min/max直接计算COCO要求格式为[x, y, width, height]左上角坐标加宽高别把右下角坐标填进去。3.3 转完必须可视化没赋值的多边形是看不出来的转换脚本不报错不等于结果正确。牙科影像的多边形数量多、形态细碎一颗牙一个轮廓病变区可能只有几十个像素最有效的验证方式是把mask叠加到原图上人工扫一遍。import json, cv2, numpy as np, os def render_masks(coco_path, img_dir, out_dir, colorsNone): with open(coco_path, r, encodingutf-8) as f: coco json.load(f) for img_info in coco[images][:20]: # 先抽查前20张 img cv2.imread(os.path.join(img_dir, img_info[file_name])) anns [a for a in coco[annotations] if a[image_id] img_info[id]] for ann in anns: poly np.array(ann[segmentation][0], dtypenp.float64) poly poly.reshape(-1, 2).astype(np.int32) color colors.get(ann[category_id], (0, 255, 0)) cv2.fillPoly(img, [poly], colorcolor) out os.path.join(out_dir, img_info[file_name]) cv2.imwrite(out, img) print(written:, out) render_masks(train_coco.json, images, overlay_check, {1: (0, 255, 0), 2: (0, 0, 255), 3: (255, 200, 0)})脚本抽查前20张遍历每张图的annotation把多边形填色画到原图上再写回磁盘。检查重点不是整体效果而是三类细节牙冠轮廓是否贴合牙釉质边界病变类别是否独立成块而不是糊成一片相邻牙的重叠区域是否被重复标注。一条经验阈值如果抽查里看到超过三处轮廓明显漂移说明原始json在标注时就有系统性偏移先回到标注端修数据不要硬调增强参数去掩盖。提示可视化时不要把GT和预测画在同一张图里那样会影响对原始标注质量的判断。这一步只画GT不叠加任何模型输出。4. 训练设置与参数选择牙齿主体分割和病变分割是两套难度4.1 模型选型2616张的体量UNet家族依然是首选牙齿分割数据和自然图像不一样全景片结构相对固定牙齿在图像里位置规律、边界清晰用不着非常深的网络。我一般首选UNet或UNet配一个带预训练权重的encoder比如resnet34或efficientnet-b0。为什么不直接上SegFormer或Swin这类结构它们参数多在2616张规模下容易过拟合牙科影像没有ImageNet那么强的结构先验预训练收益打折扣。DeepLabV3可以做第二梯队尝试它的空洞卷积对牙冠大边界有优势但对病变小目标不友好。关键区分是任务牙齿主体分割是大目标、边界清晰常规Dice Loss就能收敛病变分割龋齿、根尖周病变目标小、占比低更像小目标检测网络需要保留较高分辨率特征UNet的跳跃连接在这里比深层全局注意力更管用。4.2 划分数据集时按影像来源分组不能随机切牙科场景里一个患者往往有多个视角或多次拍摄的片子同一患者的多张影像高度相似。如果随机打乱并按比例划分训练集里出现某位患者的部分片子、验证集里出现这位患者的另一张片子模型等于提前“见过”了验证数据验证指标虚高得离谱上线后会原形毕露。常见做法是按文件名前缀推断患者ID或者按数据集文档里带的患者编号字段分组。import os, random def group_split(ann_dir, val_ratio0.15, test_ratio0.1, seed42): json_files [f for f in os.listdir(ann_dir) if f.endswith(.json)] groups {} # 常见命名规则: patient_001_left.png - patient_001 for f in json_files: pid _.join(f.split(_)[:2]) groups.setdefault(pid, []).append(f) pids list(groups.keys()) rng random.Random(seed) rng.shuffle(pids) n_val max(1, int(len(pids) * val_ratio)) n_test max(1, int(len(pids) * test_ratio)) val_pids set(pids[:n_val]) test_pids set(pids[n_val:n_val n_test]) train_pids set(pids[n_val n_test:]) split {train: [], val: [], test: []} for pid, files in groups.items(): if pid in train_pids: split[train].extend(files) elif pid in val_pids: split[val].extend(files) else: split[test].extend(files) for k, v in split.items(): print(k, len(v)) return split group_split(annotations)分组脚本先把文件按患者ID聚合再以患者为单位划分训练、验证、测试。关键参数是文件名切分规则如果命名是IMG_0001.png这种没有患者信息的说明数据集没预留分组字段只能退回随机划分但在报告里要写明这一局限。val_ratio和test_ratio按0.15/0.1是全景片数据集的合理默认值测试集只做最终评估不参与调参。提示检查分组合理性时打印每个患者下的样本数与影像视角数。如果某个患者有20张以上而数据集平均每人只有2张优先怀疑文件名前缀规则没切准。4.3 训练参数类别权重、损失函数、增强三板斧牙科数据集的特殊性在于类别不平衡牙齿主体占面积大病变区域可能只占整张图的0.5%甚至更少模型会用“把所有像素预测为背景”来拿高分。缓解手段有两个给loss加类别权重或改用Dice/Tversky系列损失。我常用的组合是Dice Loss加Cross Entropy权重按各类别面积统计的倒数设置。config { model: unet, encoder: resnet34, pretrained: True, image_size: (512, 512), batch_size: 8, epochs: 80, optimizer: adamw, lr: 1e-4, weight_decay: 1e-4, loss: { name: dice_ce, dice_weight: 0.7, ce_weight: 0.3, class_weights: [1.0, 2.5, 4.0, 3.0, 4.5, 5.0] }, augmentation: { random_rotate: 30, hflip: True, scale: [0.8, 1.2], brightness_contrast: 0.2, clahe: True, elastic: False }, metrics: [dice, iou] }逐项说明为什么这么调。image_size 512是全景片的折中原始尺寸常为15003000像素直接训练显存不够缩到512能保留牙冠边界和病变区的相对细节再往下到256根尖病变这种小区域会被抹成几个像素。batch_size 8在12GB显存附近是UNet加resnet34的合理值显存小就降到4并同步把epochs从80提到120。lr 1e-4配adamw是稳定起点配合cosine衰减比step衰减省心。dice_weight和ce_weight的比值决定网络早期收敛速度CE提供稳定梯度方向Dice保证前景区域被压实。class_weights要从第2章统计的面积占比回推——面积占比最小的类别权重最大权重总和归一化到1即可。clahe增强对口腔影像价值很大因为不同医院设备输出的对比度差异明显clahe能抹平一部分这种差异elastic弹性形变在牙科解剖结构上要谨慎牙的位置有解剖约束过度形变会让模型学到不可能的几何形态。验证时不能只看整体Dice。我的习惯是分两个维度看牙齿主体类别的Dice是否稳定在0.88以上病变类别单独统计Dice和Recall因为漏检一颗病变更致命。如果病变类Dice上不去但Recall还行通常是边界分割过细导致的评估波动如果Recall也不行问题多半在标注或类别权重而不是模型结构。5. 牙齿分割数据集避坑指南5个高频踩坑点与解决方案5.1 json和图片数量对不上先做双向核对现象数据集说明写着2616张解压后json有2616个但images目录里只有2590张图或者反过来多出30个json。原因标注阶段有图被误删、同步工具没传全、重命名时文件被覆盖。这类问题不影响转换脚本但会在训练时变成DataLoader的随机报错而且报错信息经常指向一个毫不相关的文件极难排查。解决进入任何后续流程前先跑一次双向文件清单比对把只在images和只在json里的文件都列出来。常见处理是缺json的图直接排除缺图的json也排除——除非能找到原始文件否则不要手工补json补出来的标注质量无法验证反而污染训练。import os img_dir images ann_dir annotations img_names {f for f in os.listdir(img_dir) if os.path.splitext(f)[1].lower() in {.png, .jpg, .jpeg, .bmp}} json_names {f.replace(.json, ) for f in os.listdir(ann_dir) if f.endswith(.json)} only_img img_names - json_names only_json json_names - img_names print(只有图没有json, len(only_img)) for f in sorted(only_img)[:10]: print( , f) print(只有json没有图, len(only_json)) for f in sorted(only_json)[:10]: print( , f)脚本用集合差集做比对一步得出两类缺失清单。注意它对比的是去掉扩展名后的文件名因为labelme有时把imagePath写错扩展名比如图是.PNG大写、json里记录.png小写在Linux上就是两个文件。5.2 Mask边缘锯齿严重坐标精度被int吃掉了现象可视化检查时牙齿轮廓呈明显阶梯状像马赛克拼出来的。原因转换脚本里把points先做了int()或者有人为了省存储把坐标量化到整数。牙冠轮廓本身是平滑曲线量化后一个像素的误差在牙釉质高对比边界上会放大成肉眼可见的锯齿。解决从labelme到COCO全程保持浮点坐标只允许在“画mask后输出为uint8图像”这一步才做类型转换。另一个隐含点训练时对mask做resize用最近邻插值对原图做resize用双线性插值。两件事分开做不要图省事把原图和mask合并成一条数据流resize。5.3 类别错位label名不统一训练集里出现第七个类别现象统计出来的label集合里有tooth、Tooth、牙还有拼写错误carie。文档里说6类实际统计出7、8种字符串。原因多人协同标注时没有统一标签字典标注工具下拉框可手动输入标错一天也没人发现。解决在转换脚本前加一道“标签归一化”。先收集全量label字符串人工确认哪些是同义词建映射表{Tooth: tooth, 牙: tooth, carie: caries}然后再进转换。归一化脚本要独立保存这样重新训练时能复现同一个映射。更彻底的做法是跑一次期望类别清单校验shapes里出现期望列表之外的字符串就中断并打印文件名而不是默默跳过——静默跳过会把漏标伪装成正常。5.4 图像尺寸不统一导致shape mismatch和标注漂移现象训练时DataLoader偶发shape mismatch或者可视化时发现mask和牙冠错开几个像素。原因牙科设备品牌多全景片输出尺寸跨度大从1500x2000到3000x1500都有加上json里的imageHeight/Width可能是标注时旧图的尺寸换图后没同步更新。解决训练管线统一到固定尺寸512或1024但缩放逻辑必须正确先把labelme的浮点坐标乘上scale因子再画mask不要先画mask再整体resize mask。如果json里记录尺寸和实际图像不一致以实际图像为准计算scale因子。这类问题排第四是因为它不报错、不影响启动只影响精度最容易骗过人眼。5.5 重复标注与自相交多边形面积算负值、Dice高不起来现象某张图里同一颗牙被标了两遍转换时面积计算出现异常值或COCO评估时某个类别的AP低得反常。原因标注者画到一半发现画错了没删旧多边形直接画了第二个或多边形最后闭合点处理不规范首尾交叉形成自相交。解决转换脚本对每个label的多个polygon做合并判断同label且中心点距离小于阈值时只保留面积大的那个自相交多边形用鞋带公式算出的面积可能偏离真实值处理方式是做一次几何修复或者直接把这类样本挑出来人工复查。面积字段在COCO评估里是归一化权重填错不会报错但会拿到自我欺骗的指标。6. 训练前再加一道工序用mask质量报告揪出最后10%漏网问题训练前把每张图的标注质量量化成一张报告是我每次拿到新数据集必做的一步。具体做法把掩码按类别统计像素占比、连通域数量输出离群样本的缩略图人只需要扫离群样本不用看完全部2616张。一张全景片里牙齿主体通常是几个大片连通域病变区域是零星小斑块如果一个类别在某些图里突然出现几十个细小连通域多半是标注时多点了一堆锚点或者在噪点上画了形状。import json, numpy as np, cv2, os def quality_report(coco_path, img_dir, out_dir): with open(coco_path, r, encodingutf-8) as f: coco json.load(f) records [] for img in coco[images]: anns [a for a in coco[annotations] if a[image_id] img[id]] img_arr cv2.imread(os.path.join(img_dir, img[file_name])) h, w img_arr.shape[:2] for ann in anns: mask np.zeros((h, w), dtypenp.uint8) poly np.array(ann[segmentation][0]).reshape(-1, 2).astype(np.int32) cv2.fillPoly(mask, [poly], 1) area int(mask.sum()) n_components cv2.connectedComponents(mask)[0] - 1 records.append({ image: img[file_name], category_id: ann[category_id], area_px: area, components: n_components, }) for cat_id in range(1, 7): cat_recs [r for r in records if r[category_id] cat_id] if not cat_recs: continue areas sorted(r[area_px] for r in cat_recs) q1, q3 np.percentile(areas, [25, 75]) iqr q3 - q1 outliers [r for r in cat_recs if r[area_px] q1 - 1.5 * iqr or r[area_px] q3 1.5 * iqr] print(f类 {cat_id}: 中位面积 {np.median(areas):.0f}, 离群样本 {len(outliers)})这段脚本按类别输出面积分布和四分位离群样本把“我觉得哪里有问题”变成“算法告诉我哪里有问题”。参数q1/q3用的是经典箱线图离群阈值在标注质量检查里够用如果离群比例超过10%说明该类别标注风格差异大先看标注规范而不是直接删样本。我现在的流程已经固化成解压后先按第2章脚本验货再按第3章脚本转COCO用第5章检查单逐条过五类已知坑最后跑一遍质量报告再进训练。这一圈下来训练时遇到的意外会少很多省下的时间远超过检查花掉的一小时。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑