医学图像分割实战:Labelme牙齿数据集从清洗到训练全流程
2026/9/8 10:14:29 网站建设 项目流程

简介:这份图像分割数据集面向牙齿病变检测与医学影像分析,共2616张JPG图片并配对2616个LabelMe格式JSON标注,覆盖Tooth、Caries、Cavity、Crack、calculus、inflamation六个类别,其中Caries、Tooth、calculus分别包含2913、1709、1207个多边形标注,适合用于实例分割、病变检测等模型训练与算法验证。注意:部分牙齿未标注并非遗漏,而是标注重点聚焦病变区域。资源包内共2000个文件,主体为1999个JSON标注文件和1个说明txt,压缩包37.92MB,可通过LabelMe直接打开编辑,JSON可自行转换为mask或YOLO格式。该资源已有390人学习,适合医学图像分割初学者及相关方向算法工程师参考使用。

1. 从0到1吃透一个牙齿分割数据集:2616张、6类别、Labelme格式,到底该怎么用

做医学图像分割的人,十有八九都经历过这样的尴尬:跑通了U-Net,模型在公开数据集上效果也还行,可一换到自己的数据上,各种崩。崩的原因常不在模型,而在数据本身——标注格式五花八门,类别定义模糊,数据集质量没把控,直接开训就翻车。

我最近拿到一份牙齿分割相关的数据集,标题写的是“图像分割数据集 牙齿分割 牙齿病变分割数据集 labelme格式 2616张 6类别.7z”。说实话,看到这个标题,我第一反应是:信息量够了,但离“能直接用”还差几步。它告诉你这是Labelme格式、有2616张图、6个类别,但你没下载之前,根本不知道里面长什么样,更不知道这6个类别到底怎么分的。这篇文章我就用这份数据集为引子,聊聊拿到一份医学图像分割数据集之后,怎么快速摸底、清洗校验、格式转换、训练配置、排查问题。这套流程不只适用于牙齿分割,任何拿到手的数据集都能套用,内容尽量写得实操一点,能少踩坑就少踩坑。

2. 数据集的整体拆解:2616张图和6个类别,意味着什么

2.1 先别急着训练,拿到压缩包先做这三件事

不管从哪里获取数据集,下载完先别急着解压开训。第一件事,校验文件完整性。.7z压缩包一般都有哈希值或文件大小,先确认下载的文件体积和发布方标注一致,压缩包是否损坏。第二件事,解压到项目目录下,用tree或者系统文件管理器把目录结构过一遍,整体有概念。第三件事,统计样本量、检查标注文件格式,这一步直接决定后面训练流程怎么设计。

拿到这份牙齿数据集后,我先解压看了一下目录结构。通常一份标准的分割数据集,应该是images/annotations/两个目录,一个放原始图像,一个放对应的标注文件。这份数据集的目录结构类似这样:

dataset/ ├── images/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── annotations/ │ ├── 0001.json │ ├── 0002.json │ └── ... └── classes.txt

2616张图说大不大,说小不小。放在深度学习里,如果做分类,2616张只算小规模;但做分割任务,尤其是医学图像分割,这个量级是完全可以起步的。很多公开的医学分割数据集的规模也就在几百到几千张之间,因为医学图像的标注成本实在太高,一张口腔内窥镜图像里要逐像素标出牙齿边界,有经验的人每张也要花几分钟到十几分钟。2616张如果是一个人标注的,算下来是不小的工作量,所以在训练时也要心疼一点用,别浪费。

2.2 6个类别到底怎么分,这决定你的模型头怎么设计

“6类别”是这份数据集最核心的信息之一。在开始训练之前,我建议先确认这个“6类”到底包含哪些内容。常见的一种分法是:1个背景类(background)加上5个目标类(比如健康牙齿、龋齿、牙结石、牙髓病变、根尖病变等)。在标注的时候,背景类通常是隐含的——Labelme的JSON文件里只标注有意义的对象,没标到的区域就是背景。但转换到训练格式时,背景类要显式地补出来。

还有一种是6个都是目标类,没有单独的目录或用特定标签表示背景。我建议在动手转换之前先看一眼classes.txt或者随便打开一个JSON文件,把类别列表确认清楚。如果数据源没提供类别列表,可以写一段脚本来遍历所有标注文件,统计出现过的标签名,这样最稳。

至于模型头怎么设计,其实不用太纠结。在U-Net类模型里,PyTorch的num_classes参数直接传类别数。如果是用segmentation_models_pytorch之类的库,它通常已经帮你把多分类问题处理好了,最后输出的张量维度是[batch, num_classes, H, W]。但有一个非常容易被新手踩的坑:如果你用了“背景+5类”的标注方式,那么num_classes到底写6还是写5,取决于你训练的Loss函数。如果用CrossEntropyLoss,背景也是类别之一,就要传6;但如果classes.txt里已经显式包含了background,那就按6处理,正常得很。

2.3 Labelme格式的核心逻辑:JSON里到底存了什么东西

Labelme标注工具的输出是JSON文件,这和很多语义分割任务里常用的单通道PNG掩码图不一样。不熟悉Labelme的人第一次打开JSON文件可能会一头雾水,其实它结构并不复杂。核心的几个字段是shapesimagePathimageWidthimageHeightimageData

shapes是一个列表,每个元素代表一个多边形标注对象,包含以下信息:

  • label:当前对象的类别名,比如toothcaries
  • points:多边形的顶点坐标列表,形式是[[x1, y1], [x2, y2], [x3, y3], ...],坐标单位是像素。
  • shape_type:标注类型,常见的是polygon,有些也会用rectanglecircle(医学图像里多边形最常用,因为边界不规则)。

imagePath是原始图像的文件名,标注工具靠它来关联图像和标注。imageWidthimageHeight是图像尺寸,单位是像素。imageData字段比较特殊,它保存的是原始图像Base64编码后的字符串,如果JSON文件里有这个字段,即使原图丢失也能从标注里提取出图像来。但从数据整理角度讲,这个字段会显著增大JSON文件体积,很多工具在保存时可以去掉这个字段。

理解了JSON结构,后面所有格式转换就好办了——不管转成COCO格式、VOC格式、还是直接生成PNG掩码图,本质上都是在解析JSON的shapes字段,然后根据多边形的顶点坐标去填充像素级掩码。

3. 从Labelme到训练就绪:格式转换和预处理是重头戏

3.1 明明有标注文件,为什么还不能直接开训

很多人拿到Labelme数据集后直接想跑训练,结果发现训练代码根本读不了。原因很简单:主流的语义分割训练框架,要么吃的是PNG/JPG格式的掩码图(比如U-Net的经典实现),要么吃的是COCO/VOC这种标准格式的标注(比如MMSegmentation的某些配置)。Labelme的JSON格式是一种标注过程格式,不是训练输入格式。

所以拿到2616张图的Labelme数据,第一步永远是格式转换。我个人更推荐把Labelme格式转成“单通道掩码PNG”的中间格式,原因有三个:

  • 通用性最强,后续想接哪个框架都可以,不用再跑一次转换脚本。
  • 掩码图可以直接可视化检查,一眼能看出标注有没有错、类别颜色对不对。
  • 存储体积小,一个PNG掩码图通常只有几KB到几十KB,比JSON小得多。

转换的时候,有一个原则必须守住:原图尺寸和对齐方式不能变。标注坐标是相对于原图的,如果转换时做了缩放、裁剪、旋转,坐标也跟着变,那就要在转换脚本里同步处理好,不然标注和图像就错位了。

3.2 手写一个Labelme转PNG掩码的通用脚本

我这里提供一个我常用的转换脚本逻辑,用Python实现,依赖labelme库和opencv-python。这个脚本的核心思路是:遍历所有JSON文件,读取shapes,用OpenCV的fillPoly函数把每个多边形填充成对应的类别ID,最终生成单通道掩码图。

import json import os import cv2 import numpy as np from labelme import utils def labelme_to_mask(json_path, output_dir, class_id_map): with open(json_path, 'r', encoding='utf-8') as f: label_data = json.load(f) # 读取图像尺寸 img_w = label_data['imageWidth'] img_h = label_data['imageHeight'] # 创建全零掩码,背景默认为0 mask = np.zeros((img_h, img_w), dtype=np.uint8) # 遍历所有标注 for shape in label_data['shapes']: label = shape['label'] points = np.array(shape['points'], dtype=np.int32) # 如果类别不在映射表里,跳过或报错 if label not in class_id_map: print(f'Warning: label "{label}" not in class_id_map, skip') continue class_id = class_id_map[label] cv2.fillPoly(mask, [points], color=class_id) # 保存掩码图 base_name = os.path.basename(json_path).replace('.json', '') cv2.imwrite(os.path.join(output_dir, f'{base_name}.png'), mask)

使用前要定义类别映射表,比如:

class_id_map = { 'background': 0, 'healthy_tooth': 1, 'caries': 2, 'calculus': 3, 'periapical_lesion': 4, 'gingivitis': 5 }

这里要特别提醒一点:类别映射表一旦定下来,整个项目周期里就不要改了。因为训练好的模型,输出类别ID是绑定的,如果中途改了映射表,已训练模型的评估结果就全部失效了。如果你不确定类别怎么命名,建议在转换之前把所有JSON里的标签统计一遍,统一归类,再定映射表。

转换完之后,强烈建议生成一张掩码覆盖的可视化图,把原图和掩码图叠加在一起,半透明显示,检查有没有错位、漏标、类别标错的情况。这一步很花时间,但比训练到一半发现问题再回炉要省得多。

3.3 数据集的划分策略:训练、验证、测试的比例别拍脑袋

数据集划分是另一个容易被忽视但影响很大的环节。常见的做法是按7:2:1或8:1:1比例划分成训练集、验证集、测试集。这里有两个经验:

第一,划分前先把所有文件的ID列表打乱随机化,不要直接用前70%做训练,后30%做测试。因为数据集的顺序可能有潜在规律,比如同一个病人的多张片子排在一起,如果不打乱,可能出现同类数据集中出现在某个集合的情况,影响模型泛化能力的评估。

第二,如果数据集中有同一个病例的多个视角图像,最好按病例划分,而不是按图像划分。换句话说,同一个病例的图像要全部放进训练集或者全部放进验证集,不能让同一个病例的数据同时出现在训练集和验证集里,否则会产生数据泄漏,模型评估结果虚高。

划分完数据集,记得把三个集合的文件列表保存下来,用.txt.csv都行。后面训练、评估、推理都要用这个划分结果,别每次跑都重新随机划分一次。

4. 开始训练之前,还有两个关键问题要想清楚

4.1 数据量够不够?要不要做数据增强

2616张原图对于医学分割来说不算特别充裕。我的建议是,第一轮训练先用原始数据做baseline,记录验证集上的mIoU、Dice系数等指标,再考虑逐步增加数据增强策略。

医学图像分割里,数据增强要做得谨慎一些。常规的自然图像增强方法,比如随机裁剪、水平翻转、颜色抖动、旋转等,大部分可以直接用,但有几种要注意:

  • 颜色抖动要控制幅度,特别是如果原始图像是内窥镜图或X光图,颜色本身有诊断意义,过度调色会造成错误的学习信号。
  • 随机旋转角度不要太大,一般±15度以内比较安全,旋转角度过大会导致原本的解剖结构变形失真。
  • 弹性形变(elastic deformation)在医学图像里效果好,因为牙齿、器官这类结构常有自然变形,不过参数需要实验调整,变形太大反而会生成不真实的样本。
  • 不要用垂直翻转!对于牙齿和内窥镜图像,上下翻转会完全改变解剖位置关系,模型学到错误的空间先验。

我常用的增强组合是:随机水平翻转+小角度旋转+轻微对比度调整+轻微高斯噪声,前两项是空间层面的,后两项是像素层面的。用albumentations库实现很方便,它和PyTorch的DataLoader配合得很好。

4.2 训练配置的初始设定:别一上来就调大学习率

我第一次用这份类型的口腔数据集训练时,用的是U-Net加ResNet34编码器,输入尺寸设成512x512,Batch Size是8,优化器选Adam,初始学习率是1e-4。这个配置对大部分医学分割任务都算比较稳的起点。

有一点值得特别注意:医学图像分割里,前后景类别通常极度不平衡。比如一张口腔内窥镜图,牙齿区域可能只占画面面积的20%到30%,背景占了大部分,如果病变区域再小一些,占比可能只有百分之几。如果直接用普通的CrossEntropyLoss,模型很容易偏向预测背景,Dice分数上不去。

解决办法有两个方向:一是用加权CrossEntropyLoss,给前景类别更高的权重;二是用Dice Loss或Focal Loss,这类损失函数天然对小目标更友好。组合损失(CrossEntropy + Dice)在实际项目中效果通常最好。我用segmentation_models_pytorch库的时候,习惯自己写一个组合损失类:

import torch.nn as nn import torch.nn.functional as F from segmentation_models_pytorch.losses import DiceLoss class ComboLoss(nn.Module): def __init__(self, ce_weight=0.5, dice_weight=0.5): super().__init__() self.ce_weight = ce_weight self.dice_weight = dice_weight self.ce_loss = nn.CrossEntropyLoss(ignore_index=255) self.dice_loss = DiceLoss(mode='multiclass') def forward(self, logits, targets): ce_loss = self.ce_loss(logits, targets) dice_loss = self.dice_loss(logits, targets) return self.ce_weight * ce_loss + self.dice_weight * dice_loss

这里ignore_index=255是留作忽略特定像素的手段。比如有些数据集的掩码图里边缘区域标注不确定,可以在预处理时将其设为255,训练时这些像素就不会参与Loss计算。

4.3 从多分类到二分类:某些任务里“病变分割”可能要单独做

还有一个值得讨论的问题。标题里同时提到了“牙齿分割”和“牙齿病变分割”,这是两个相关的但不同的任务。牙齿分割关注的是把每颗牙齿或全部牙齿区域从背景中分离出来;病变分割关注的是在牙齿区域内把龋齿、牙结石、根尖病变等区域标出来。

在实际应用中,这两个任务可以一起做,也就是“6类别的多分类分割”,模型直接输出每个像素的类别标签。但也可以拆成两个阶段:第一阶段先做牙齿区域分割(二分类:牙齿/背景),第二阶段只对牙齿区域内的像素做病变分类。拆分的做法在多分类数据集样本不够均衡的时候有它的价值,因为如果某些病变类别的样本量特别少,多分类模型的训练难度会高很多。

如果你发现某个类别的样本量特别少,我建议先统计一下各类别像素占比和样本数量。低于总样本数10%的类别,要么考虑把它并入“其他病变”,要么单独做一个二分类子任务。一切以数据实际情况为准,别为了凑“6类”而强行训练一个效果不佳的模型。

5. 常见问题与排查技巧实录

5.1 掩码图和原图对不上,位置错位了

这是我见过最多的问题,基本都出在图像尺寸上。Labelme的JSON里的坐标是以保存图像时的尺寸为基准的,如果你的原图被代码自动做了Resize或者Exif旋转,坐标就变了。

排查方法:可视化叠加。写一段代码,把原图读出来,再把PNG掩码读出来,用cv2.addWeighted叠加显示。如果发现轮廓和牙齿边缘错位,十有八九是尺寸或通道问题——注意检查原图是RGB还是灰度,如果原图有alpha通道,用cv2.imread默认读进去是3通道,可能已经自动丢弃了alpha,如果标注坐标里有坐标计算依赖了alpha通道信息也可能出错。

5.2 训练时Loss不下降或者直接NaN

Loss是NaN,最常见的原因是输入图像里有异常像素值。比如图像是16位的TIFF或DICOM,像素值范围不是0-255,直接喂给模型后,梯度爆炸了就NaN。

解决方案是统一预处理:把所有图像读成float32,再归一化到[0, 1]区间,或者用ImageNet的均值和标准差做标准化。如果图像里有全黑的坏图像或全白的过曝图像,建议做一次清洗,剔除或标记这些异常样本。

5.3 模型总把病变区域预测成健康牙齿

这是典型的类别不均衡问题。牙齿病变区域在图像中通常是小面积区域,和健康牙齿的面积差距可能是10倍以上。除了用组合Loss之外,我还会做一个操作:把病变区域的mask在原图上画出来,统计一下各类别的像素占比,如果发现占比差距太大,可以考虑用缓存与数据重采样策略,让模型每个epoch能看到更均衡的类别分布。

另一种有效手段是把病变区域裁剪出来单独训练,用小Patch训练增强模型对小区域的感知能力。实际项目里这个方案往往能带来明显的精度提升。

5.4 怎么判断数据集本身质量好不好

训练之前,先把数据质量关把好。我会做一轮快速质量体检:

  1. 有多少张图的标注文件为空(shapes为空列表)?这些图要么删掉,要么标注。
  2. 有多少张图的标注是异常的,比如点到图外面、多边形自相交?可以用脚本粗略过滤。
  3. 各类别样本数量分布如何?统计后画一个简单的柱状图,一目了然。
  4. 所有标注是不是同一个人做的?如果是多人标注,不同标注员的边界画法可能差异很大,这个会让模型无所适从。

这一轮体检做完,基本上对数据集的信任度心里就有数了。如果有问题,尽早发现,别等到训练跑完一轮才发现数据本身有硬伤,那个时间成本太高了。

6. 口腔影像分割的落地思路:不只是刷竞赛分数

模型训练完之后,牙齿分割能应用到哪些场景,这个问题也值得展开聊聊。当前比较热的落地场景包括:口腔CBCT图像里的牙齿自动定位、内窥镜图像下的龋齿筛查、牙周炎评估辅助、种植手术规划中的牙槽骨和神经管分割等。这份2616张的数据集如果能结合口腔X光或内窥镜图像使用,可以考虑做成一个端侧辅助筛查工具,用户拍一张口腔照片,模型自动标出可疑病变区域,这就是一个很实际的产品原型。

延伸一下,这套数据还能用于教学演示和科研对比。医学图像分割是深度学习在医疗影像里最经典的任务之一,拿2616张的标注数据做不同模型结构的对比实验——U-Net、U-Net++、DeepLabv3+、SegFormer——跑一遍精度对比,作为课程设计或论文实验部分都很有价值。

我个人在实际操作中体会到,一份数据集拿到手,最耗时间的不是训练模型,而是数据的前期准备和检查。2616张图听起来多,但真正静下心来做格式转换、质量校验、类别统计、划分数据、上手训练,两天时间足够跑通全流程。关键是每一步都稳扎稳打,别图快跳步。模型结构反而不是最高优先级的变量,先把数据管线彻底搞明白,训练和调参都是水到渠成的事。最后再分享一个小技巧:如果你在同一份数据上要跑多个模型的对比实验,建议把预处理、数据加载、评估指标、数据划分全都固化成公共模块,复制代码的时候永远用同一套数据处理逻辑,这样才能保证不同模型的比较结果只受模型结构影响,而不被数据处理差异干扰。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询