简介面向牙齿影像分析与口腔病变检测方向的算法开发者与研究者这份资源提供了一套可直接用labelme打开编辑的牙齿分割标注数据集重点服务于龋齿、龋洞、裂纹、牙结石与炎症等病变区域的识别任务适合具备一定图像分割基础、需要自建训练数据的中高级学习者。压缩包为单个docx文档约2.22MB其中整理了2616张jpg图像与对应json标注文件的完整说明标注由labelme 5.5.0以多边形方式完成涵盖Tooth、Caries、Cavity、Crack、calculus、inflamation共6类各类标注数量从139到2913不等并明确提示部分牙齿未标注不影响病变检测。已有122人学习关注。读者可据此了解数据规模与类别分布自行将json转换为mask、YOLO或COCO格式用于语义分割或实例分割实验同时需注意发布方不对训练模型精度作保证。1. 牙科影像分割数据集2616 张 labelme 标注到底能干什么拿到一份牙齿病变分割数据集第一反应往往不是太好了有数据了而是这标注能不能直接喂给模型。这份资源给的是 2616 张 jpg 配 2616 个 labelme json6 个类别全是多边形标注没有现成 mask。它解决的是牙科影像分割里最耗时的环节——从零标注。适合做医学图像分割验证、牙科辅助诊断原型、labelme 转 mask 流程练手的人。不适合想要开箱即训、直接跑出高精度模型的人因为 json 到 mask 的转换、部分牙齿漏标的处理都得自己动手。下面按数据长什么样 → 怎么转 → 怎么训 → 坑在哪拆开讲。2. 先看清数据底子6 类标注的分布与选型判断2.1 类别分布决定了这份数据能训什么把 6 个类别的标注数拉出来看分布极不均衡类别英文名标注数占比特征牙齿Tooth1709主体结构但明显少于图片数龋齿Caries2913数量最多病变检测主力龋洞Cavity1099中等裂纹Crack139极稀疏牙结石calculus1207中等偏多炎症inflamation620偏少2616 张图里 Tooth 只有 1709 个标注说明约三分之一的图里牙齿没被完整标出。项目正文已经点明分割目的是查病变部分牙齿没标不影响病变检测。这句话是理解整份数据的关键——它不是一份完整牙齿实例分割数据集而是一份病变区域优先的数据集。如果你的任务是数牙齿、做全口牙位识别这份数据会让你翻车如果任务是龋齿、牙结石、炎症的病灶分割它是对口的。Crack 只有 139 个标注是典型的稀疏类别。直接按像素级交叉熵训模型大概率把这个类学成背景。血泪经验是稀疏类别要么在采样时过采样含 Crack 的图要么在 loss 里给类别权重别指望默认配置能救回来。2.2 为什么是 labelme 而不是现成 masklabelme 存的是 json里面记录每个多边形的顶点坐标和 label 名不存像素 mask。好处是标注可编辑、可回溯、顶点精度高代价是训练前必须自己栅格化成 mask。这份数据用 labelme 5.5.0 标注polygon 规则意味着每个标注是一串点而不是矩形框。对语义分割来说这是对的——牙齿和病变边界本来就不规则矩形框会引入大量背景像素。选型上要清楚语义分割每个像素归一类和实例分割区分同一类的不同个体对这份数据的用法不同。语义分割直接把 6 类栅格化成 6 通道或单通道索引图即可实例分割需要把每个 polygon 当独立实例同一张图里多个 Tooth 要分开但数据里部分牙齿漏标会让实例数不稳定做实例分割要谨慎。3. 把 json 转成可训练的 mask脚本与参数3.1 环境准备与目录约定先固定目录结构后面所有脚本都基于它dataset/ ├── images/ # 2616 张 jpg ├── jsons/ # 2616 个同名 json └── masks/ # 输出脚本生成装依赖labelme 自带转换工具但版本要对上pip install labelme5.5.0 pip install opencv-python numpy pillow注意 labelme 版本尽量和标注时一致5.5.0高版本 json 字段偶有差异低版本可能读不了新字段。转换脚本我一般自己写比labelme_json_to_dataset可控因为官方脚本对多类别、自定义颜色映射不够灵活。3.2 单张 json 转 mask 的核心逻辑import json import numpy as np import cv2 from pathlib import Path # 类别到索引的映射顺序固定训练和推理必须一致 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, encodingutf-8) as f: data json.load(f) h, w data[imageHeight], data[imageWidth] mask np.zeros((h, w), dtypenp.uint8) # 0 为背景 for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue pts np.array(shape[points], dtypenp.int32) # polygon 填充同一类别重叠时后画的覆盖先画的 cv2.fillPoly(mask, [pts], CLASS_MAP[label]) cv2.imwrite(str(out_path), mask) if __name__ __main__: img_dir Path(dataset/images) json_dir Path(dataset/jsons) mask_dir Path(dataset/masks) mask_dir.mkdir(exist_okTrue) for jp in json_dir.glob(*.json): json_to_mask(jp, mask_dir / (jp.stem .png))逻辑说明imageHeight/imageWidth从 json 里读保证 mask 和原图同尺寸fillPoly按顶点填充CLASS_MAP把字符串 label 映射成 0-6 的整数索引0 留给背景。参数上最关键的是CLASS_MAP的顺序——训练时的类别索引、推理时的颜色表、评估时的混淆矩阵都依赖它一旦改动必须全链路同步否则会出现训练是龋齿、推理显示牙结石的玄学问题。3.3 批量转换与完整性校验批量跑完后必须校验不能假设 2616 个都成功import os from pathlib import Path json_files list(Path(dataset/jsons).glob(*.json)) mask_files list(Path(dataset/masks).glob(*.png)) print(json:, len(json_files), mask:, len(mask_files)) # 检查空 mask全背景这类图要么是漏标要么是纯背景 empty [] for mp in mask_files: m cv2.imread(str(mp), cv2.IMREAD_GRAYSCALE) if m.max() 0: empty.append(mp.name) print(空 mask 数量:, len(empty))空 mask 是这份数据的高频现象因为部分牙齿没标、有些图可能只有背景。训练时全背景图对语义分割是有效负样本但如果比例过高会拉低正样本学习效率建议统计后决定是否剔除。校验这一步别省我见过直接开训、跑了半天发现 mask 全是黑的回头查是 json 里shapes为空。4. 训练配置类别不平衡与漏标怎么处理4.1 损失函数与采样策略6 类里 Crack 只有 139 个标注Tooth 1709、Caries 2913像素级不平衡更严重病变区域通常只占图的一小块。常见做法是组合损失import torch import torch.nn as nn import torch.nn.functional as F class CombinedLoss(nn.Module): def __init__(self, num_classes7, weightsNone): super().__init__() # weights 按类别频率的倒数设置Crack 给高权重 self.ce nn.CrossEntropyLoss(weightweights) def forward(self, logits, target): # logits: [B, C, H, W], target: [B, H, W] ce_loss self.ce(logits, target) # Dice 缓解前景背景不平衡 probs F.softmax(logits, dim1) dice 0.0 for c in range(1, logits.shape[1]): p probs[:, c] t (target c).float() inter (p * t).sum() dice (2 * inter 1e-6) / (p.sum() t.sum() 1e-6) dice_loss 1 - dice / (logits.shape[1] - 1) return ce_loss dice_loss参数说明weights建议按总像素 / (类别数 * 该类像素)估算Crack 的权重会明显高于其他类Dice 部分跳过背景类从 1 开始循环避免背景主导。这套组合在医学小目标分割里是常规操作比单用交叉熵稳。4.2 漏标牙齿的处理边界项目正文明确说部分牙齿没标注。训练时这会带来一个隐蔽问题模型把未标注的牙齿预测成 Toothloss 却按背景惩罚等于在教模型别认这颗牙。处理方式有两种一是把未标注区域当 ignore indexloss 里跳过二是接受噪声靠数据量摊平。我一般选前者在 mask 生成时把确定是牙齿但没标的区域标成 255CrossEntropyLoss(ignore_index255)跳过。但这份数据没提供哪些没标的信息所以实操上更现实的做法是训练目标聚焦病变类Tooth 类权重调低别让模型在牙齿完整性上死磕。提示如果你的下游任务是病灶检测而非全牙分割评估指标只看 Caries/Cavity/calculus/inflamation 的 DiceTooth 的漏标就不会污染结论。5. 避坑排查这份数据最容易翻车的五个地方5.1 现象转出来的 mask 和原图对不上原因json 里的imageWidth/imageHeight和实际 jpg 尺寸不一致常见于图片被二次压缩或裁剪过。解决转换前用cv2.imread读原图尺寸和 json 字段比对不一致的图单独列出来人工确认别直接resize硬转否则多边形坐标全错位。5.2 现象训练 loss 正常但验证 Dice 极低原因验证集里 Crack 类样本太少或者验证集划分时没做分层采样导致某折里根本没有 Crack。解决按类别出现情况分层划分 train/val保证每个类在验证集里有足够样本Crack 这种稀疏类可以单独统计召回别只看总体 Dice。5.3 现象模型把牙结石预测成龋齿原因calculus 和 Caries 在颜色、纹理上接近标注边界本身有主观性且两类像素数都不少。解决检查混淆矩阵确认是哪两类混淆在 loss 里对这两类加区分性权重或引入边界损失boundary loss强化边缘数据层面复查标注确认没有同一区域被标成两类。5.4 现象空 mask 图导致训练不稳定原因部分图只有背景或漏标严重全零 mask 在 Dice loss 里分母接近零梯度异常。解决转换后统计空 mask 比例超过阈值比如 5%就剔除或降采样Dice 计算里已经加了1e-6平滑但仍建议过滤纯空图。5.5 现象labelme 打开 json 报错或类别显示异常原因labelme 版本不匹配或 json 里 label 名有前后空格、大小写不一致比如Tooth和tooth。解决统一用 5.5.0 打开转换脚本里对 label 做strip()并建立大小写归一映射遇到未知 label 打印出来人工核对别静默跳过。6. 进阶验证用混淆矩阵反推标注质量数据到手别急着堆模型先花半小时做一次标注质量体检比调参收益大。做法是随机抽 100 张图把生成的 mask 叠加回原图用半透明颜色显示人工扫一遍。重点看三类问题——病变区域是否被完整包住、同一病变是否被拆成多个 polygon、边界是否明显超出病灶。这一步能提前发现系统性标注偏差避免模型学了一堆噪声还以为是数据不够。更量化的验证是跑一个轻量模型比如 UNet 小通道版训 10 个 epoch输出混淆矩阵from sklearn.metrics import confusion_matrix import numpy as np # pred, target 为展平后的像素级标签 cm confusion_matrix(target.flatten(), pred.flatten(), labelslist(range(7))) print(cm) # 重点看对角线外的非零项尤其是 Caries-calculus、Cavity-Caries如果 Caries 和 calculus 之间大量互错大概率是标注边界模糊而不是模型不行。这时候回头复查标注比换 backbone 有用。我一般会把这个矩阵存成图和 mask 叠加图放一起作为这份数据能不能用的判断依据。从那以后我每次拿到 labelme 格式的医学数据集都强制先走一遍转 mask → 空图统计 → 叠加目检 → 小模型混淆矩阵四步再决定要不要投入正式训练。这份牙齿病变数据集的价值不在2616 张这个数字而在于它把牙科病灶的 6 类边界摆在了你面前剩下的转换和清洗才是真正决定成败的部分。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?