医学图像分割实战:Labelme牙齿数据集从清洗到训练全流程

医学图像分割实战:Labelme牙齿数据集从清洗到训练全流程 简介这份图像分割数据集面向牙齿病变检测与医学影像分析共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.txt2616张图说大不大说小不小。放在深度学习里如果做分类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文件可能会一头雾水其实它结构并不复杂。核心的几个字段是shapes、imagePath、imageWidth、imageHeight、imageData。shapes是一个列表每个元素代表一个多边形标注对象包含以下信息label当前对象的类别名比如tooth或caries。points多边形的顶点坐标列表形式是[[x1, y1], [x2, y2], [x3, y3], ...]坐标单位是像素。shape_type标注类型常见的是polygon有些也会用rectangle或circle医学图像里多边形最常用因为边界不规则。imagePath是原始图像的文件名标注工具靠它来关联图像和标注。imageWidth和imageHeight是图像尺寸单位是像素。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, encodingutf-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), dtypenp.uint8) # 遍历所有标注 for shape in label_data[shapes]: label shape[label] points np.array(shape[points], dtypenp.int32) # 如果类别不在映射表里跳过或报错 if label not in class_id_map: print(fWarning: label {label} not in class_id_map, skip) continue class_id class_id_map[label] cv2.fillPoly(mask, [points], colorclass_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编码器输入尺寸设成512x512Batch 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_weight0.5, dice_weight0.5): super().__init__() self.ce_weight ce_weight self.dice_weight dice_weight self.ce_loss nn.CrossEntropyLoss(ignore_index255) self.dice_loss DiceLoss(modemulticlass) 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_index255是留作忽略特定像素的手段。比如有些数据集的掩码图里边缘区域标注不确定可以在预处理时将其设为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不下降或者直接NaNLoss是NaN最常见的原因是输入图像里有异常像素值。比如图像是16位的TIFF或DICOM像素值范围不是0-255直接喂给模型后梯度爆炸了就NaN。解决方案是统一预处理把所有图像读成float32再归一化到[0, 1]区间或者用ImageNet的均值和标准差做标准化。如果图像里有全黑的坏图像或全白的过曝图像建议做一次清洗剔除或标记这些异常样本。5.3 模型总把病变区域预测成健康牙齿这是典型的类别不均衡问题。牙齿病变区域在图像中通常是小面积区域和健康牙齿的面积差距可能是10倍以上。除了用组合Loss之外我还会做一个操作把病变区域的mask在原图上画出来统计一下各类别的像素占比如果发现占比差距太大可以考虑用缓存与数据重采样策略让模型每个epoch能看到更均衡的类别分布。另一种有效手段是把病变区域裁剪出来单独训练用小Patch训练增强模型对小区域的感知能力。实际项目里这个方案往往能带来明显的精度提升。5.4 怎么判断数据集本身质量好不好训练之前先把数据质量关把好。我会做一轮快速质量体检有多少张图的标注文件为空shapes为空列表这些图要么删掉要么标注。有多少张图的标注是异常的比如点到图外面、多边形自相交可以用脚本粗略过滤。各类别样本数量分布如何统计后画一个简单的柱状图一目了然。所有标注是不是同一个人做的如果是多人标注不同标注员的边界画法可能差异很大这个会让模型无所适从。这一轮体检做完基本上对数据集的信任度心里就有数了。如果有问题尽早发现别等到训练跑完一轮才发现数据本身有硬伤那个时间成本太高了。6. 口腔影像分割的落地思路不只是刷竞赛分数模型训练完之后牙齿分割能应用到哪些场景这个问题也值得展开聊聊。当前比较热的落地场景包括口腔CBCT图像里的牙齿自动定位、内窥镜图像下的龋齿筛查、牙周炎评估辅助、种植手术规划中的牙槽骨和神经管分割等。这份2616张的数据集如果能结合口腔X光或内窥镜图像使用可以考虑做成一个端侧辅助筛查工具用户拍一张口腔照片模型自动标出可疑病变区域这就是一个很实际的产品原型。延伸一下这套数据还能用于教学演示和科研对比。医学图像分割是深度学习在医疗影像里最经典的任务之一拿2616张的标注数据做不同模型结构的对比实验——U-Net、U-Net、DeepLabv3、SegFormer——跑一遍精度对比作为课程设计或论文实验部分都很有价值。我个人在实际操作中体会到一份数据集拿到手最耗时间的不是训练模型而是数据的前期准备和检查。2616张图听起来多但真正静下心来做格式转换、质量校验、类别统计、划分数据、上手训练两天时间足够跑通全流程。关键是每一步都稳扎稳打别图快跳步。模型结构反而不是最高优先级的变量先把数据管线彻底搞明白训练和调参都是水到渠成的事。最后再分享一个小技巧如果你在同一份数据上要跑多个模型的对比实验建议把预处理、数据加载、评估指标、数据划分全都固化成公共模块复制代码的时候永远用同一套数据处理逻辑这样才能保证不同模型的比较结果只受模型结构影响而不被数据处理差异干扰。本文还有配套的精品资源点击获取