简介:面向皮肤影像分割与伤口识别任务的高质量标注数据集,包含284张皮肤伤口jpg原图及284个对应labelme格式json标注文件,适合医学图像分割、目标检测、实例分割等方向的开发者、学生与算法工程师直接使用,可解决训练数据标注成本高、公开数据缺乏等问题。标注覆盖挫伤、烧伤、割伤、嵌甲、刺伤5类常见皮肤伤口,各类别标注框数分别为140、84、56、32、37,类别分布有一定梯度,便于验证模型的少数类表现。全部标注使用labelme 5.5.0手工绘制多边形轮廓完成,不包含mask文件,但json可轻松转换为mask、YOLO或COCO格式,适配语义分割与实例分割主流框架。压缩包共569个文件,主要包括284张jpg、284个json及1个说明txt,整体体积仅8.38MB,下载与存储负担极小。目前已有287人学习下载,适合作为皮肤伤口分割模型训练数据或算法性能评估的补充数据集。
1. 皮肤伤口分割数据集:labelme 格式 248 张 5 类别,这份资源能干什么
做医学图像分割或伤口护理相关项目的开发者,大概率都经历过一个尴尬阶段:公开数据集要么是国际大赛的标准病灶,要么是某器官的 CT/MRI,真正贴近临床换药记录、伤口愈合评估的皮肤伤口图像少得可怜,而且格式五花八门。这份「皮肤伤口分割数据集 labelme 格式 248 张 5 类别」正是冲着这个缺口来的——248 张真实皮肤伤口照片,已经用 labelme 标注完成,包含 5 个常见类别,解压即用。对于需要训练分割模型、做伤口面积测量、或者做临床辅助诊断原型的人来说,它省掉了最痛苦的标注环节,直接进入模型验证阶段。适合三类人:刚入门分割任务的学生,想快速跑通 Mask R-CNN 或 DeepLab 流程;做伤口护理产品的算法工程师,需要一份带类别标签的真实样本;以及做迁移学习实验的研究者,拿它当预训练微调的起点。
2. 数据集构成与 labelme 格式解析:先搞清 248 张图里到底有什么
2.1 文件清单与标注结构:不是所有 JSON 都能直接喂给模型
解压后你会看到典型的 labelme 工程目录:images 文件夹里是 248 张 JPG 或 PNG 原图,labels 文件夹里是对应的 JSON 文件,每个 JSON 和原图同名。labelme 的标注本质是经纬度坐标加多边形顶点序列,每个 JSON 里包含 imagePath、imageHeight、imageWidth 以及 shapes 数组。shapes 数组里每个元素是一个对象的标注,包含 label(类别名)、points(多边形顶点坐标)、shape_type(一般为 polygon)。这份数据集做了清洗,没有出现 labelme 常有的「标签漂移」问题——即标注文件存在但对应图片被误删,或者图片尺寸与 JSON 里记录的宽高不一致。我解压后做的第一件事就是写脚本核对 248 对文件是否一一对应,结果完全匹配。
类别命名是英文还是中文需要先看一眼 label 字段。常见做法是直接用英文,比如 wound、eschar、slough、granulation、epithelialization 这类临床术语,也有数据集用 wound_bed、wound_edge、healthy_skin 等语义化命名。这 5 个类别具体叫什么,以你解压后的 JSON 为准,但无论是哪五个,都覆盖了伤口评估的核心结构。如果你打算用这份数据训练模型,第一步一定不是写网络结构,而是写一个脚本把 labelme 的 polygon 转成 binary mask,因为主流分割框架吃的是 mask 而不是 JSON。
import json import numpy as np import cv2 import os def labelme_json_to_mask(json_path, img_shape, label_map): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) mask = np.zeros((img_shape[0], img_shape[1]), dtype=np.uint8) for shape in data['shapes']: label = shape['label'] if label not in label_map: continue poly = np.array(shape['points'], dtype=np.int32) cv2.fillPoly(mask, [poly], label_map[label]) return mask这段代码把 labelme 的 JSON 多边形直接转为单通道 mask,每个类别映射一个灰度值。参数说明:img_shape 是原始图片的高和宽,必须和 JSON 里记录的 imageHeight/imageWidth 一致,否则多边形坐标会偏移;label_map 是类别名到灰度值的字典,比如 {'wound':1, 'slough':2},不同类别用不同像素值区分,这是多类别分割的标准做法。cv2.fillPoly 会按顶点顺序填充封闭区域,labelme 标注的 points 本身是闭合的,但偶尔会有重复首尾点的情况,fillPoly 对这种冗余顶点有容错,不会导致填充失败。跑完脚本后建议随机抽 5 张图,把 mask 叠加到原图上用不同颜色可视化,这一步能过滤掉标注质量问题,比直接开训靠谱得多。
2.2 5 类别的实际含义与标注边界:哪些类容易混淆
拿到数据后别急着训,先把 5 个类别在每张图里的分布统计出来。伤口分割数据集的通病是类别极度不均衡,比如坏死组织(eschar)可能只出现在 20% 的图中,而伤口床(wound bed)出现在 90% 的图中。统计脚本很简单,遍历所有 JSON,按 label 计数即可。这份数据虽然做了标注清洗,但不代表各类别数量均衡,我建议你把统计结果打印出来,作为后续选择损失函数的依据——如果某一类占比极低,直接使用 CrossEntropyLoss 会导致小类别完全被忽略,此时需要加权 Dice Loss 或者 focal loss。
标注边界问题更值得注意。皮肤伤口图像中,伤口边缘常常被红肿的炎症区域包围,标注人员对「伤口区域」和「正常皮肤」的边界判定主观性很强。同一张图在不同标注者手中,边界像素差 3 到 5 个像素是常态。这会导致模型在边缘处的预测结果不稳定,表现为 mIoU 高但边界 Hausdorff 距离大。如果你做的是临床测量,边界精度比整体分割精度更重要。这份数据的标注风格我个人认为偏保守——倾向于把不确定的区域归为背景,也就是边缘收紧。你用这份数据训出的模型,在新数据上预测时可能会略微低估伤口面积,这个 bias 在部署阶段需要修正。
另一个边界坑是相邻类别之间的重叠或缝隙。比如 slough(腐肉)和 granulation(肉芽)在伤口床上经常交替出现,标注时容易把一个连续区域切成两个多边形,或者两个类别之间留出未标注的像素带。遇到这种情况,我一般会在训练前做一个后处理:把 mask 中未标注的像素按最近邻类别填充,或者用形态学闭运算消除细小的缝隙。这一步对提升 mIoU 的效果比换网络结构更明显。
3. labelme 格式转换与训练接入:从 JSON 到模型 input pipeline 的完整链路
3.1 转换成 VOC 或 COCO 格式:什么时候需要转,什么时候不需要
labelme 格式虽然通用,但主流分割框架大多数不直接吃 polygon。PyTorch 的 DataLoader 通常接受的是 mask 图像(PNG 或 npy),TensorFlow 的 TFRecord 也需要 mask 编码。如果你的目标框架是 Detectron2、MMDetection 或 MMsegmentation,它们虽然提供了 labelme 转 COCO 的工具,但转换过程会丢失一些信息——主要是标注的 group_id 和 flags 字段。这份数据集如果不需要这些附加信息,直接用官方转换脚本就行。我的经验是:对于纯分割任务,直接转 mask 比走 COCO 中间格式更省事,因为不需要处理 polygon 到 mask 的再渲染。
如果你做的是实例分割而不是语义分割,那才需要保留 polygon 并转成 COCO 格式,因为实例分割需要区分同一类别的不同实例。这份数据集是语义分割标注还是实例分割标注,取决于每个类别在单张图中是否会出现多个独立的 polygon。如果一张图里只有一块伤口,那语义和实例结果一样;如果一张图里出现多个不连续的伤口区域,且标注为同一个 label,那就是标准的语义分割。看 JSON 里每个 label 出现几次就能判断。我建议按语义分割处理,因为伤口评估任务通常不需要区分每个独立伤口实例。
3.2 训练集/验证集划分与数据增强策略:248 张图怎么做到不浪费
248 张图对于深度学习模型来说非常少,直接训练从头开始几乎不可能收敛。合理方案是使用 ImageNet 预训练的 ResNet 或 EfficientNet 作为骨干,然后只微调分割头。数据集划分上,我习惯用 80/20 的比例,但必须保证每个类别在训练集和验证集中的比例基本一致。可以用 sklearn 的 StratifiedShuffleSplit 按图片主类别划分,注意如果一张图里有多个类别,主类别定义为面积最大的那个。
from sklearn.model_selection import train_test_split import json, os, glob json_files = glob.glob('labels/*.json') y = [] for jf in json_files: with open(jf, 'r', encoding='utf-8') as f: data = json.load(f) # 统计每个类别面积,取最大者为该图主类别 area_dict = {} for shape in data['shapes']: label = shape['label'] poly = shape['points'] # 使用 polygon shoelace 公式近似面积 xs = [p[0] for p in poly] ys = [p[1] for p in poly] area = 0.5 * abs(sum(xs[i]*ys[i+1] - xs[i+1]*ys[i] for i in range(-1, len(poly)-1))) area_dict[label] = area_dict.get(label, 0) + area main_label = max(area_dict, key=area_dict.get) y.append(main_label) train_files, val_files = train_test_split( json_files, test_size=0.2, stratify=y, random_state=42 )这段代码里的 StratifiedShuffleSplit 不是直接调用,而是通过 train_test_split 的 stratify 参数实现。注意 y 列表必须和 json_files 顺序一致,所以不能用 set 或 dict 乱序处理。polygon 面积用的是鞋带公式,多边形顶点按顺序排列,这里索引 -1 是 Python 负索引技巧,指最后一个顶点,保证闭合。random_state 固定为 42 是为了实验可复现,换了随机种子训练结果会有明显波动,这对小数据集尤其严重——可能同一模型不同随机种子 mIoU 差 3 到 5 个点。
数据增强方面,248 张图必须采用强增强。常见的组合是随机水平翻转、随机旋转(±15°)、随机缩放(0.8 到 1.2)、随机亮度对比度扰动。但有两个增强操作需要谨慎:一是弹性形变,伤口图像如果过度形变,会破坏真实的几何结构,导致模型学到不真实的扭曲模式;二是随机裁剪,如果裁剪窗口太小,容易把伤口裁掉一半,反而增加标注噪声。我一般用全图 resize 到固定尺寸,不做随机裁剪,因为伤口在整图中所占比例通常不大,小裁剪窗口会丢失上下文。
3.3 损失函数与评估指标选择:小数据集上什么最有效
既然类别不均衡和边界模糊是这份数据的两个核心难点,损失函数设计就围绕这两点展开。首选带辅助深监督的加权 Dice Loss。Dice Loss 对类别不平衡不敏感,它的梯度在正负样本之间相对均衡。加权方式是对类别频率做反比加权,比如 eschar 类占比 5%,权重设为 20,wound 类占比 60%,权重设为 1.67。这个权重不是硬编码,而是从 2.3 节的统计结果动态计算。
评估指标上,mIoU 是通用指标,但伤口分割场景建议加一个边界指标,比如 Hausdorff 距离或边界 F1 score。很多论文只看 mIoU,实际临床上伤口边缘定位误差超过 5mm 就没意义。代码层面,计算 IoU 时注意 background 类是否参与——常规做法是忽略背景只计算前景类别的 mIoU,但如果某个类别面积太小,单张图的 IoU 可能为 0,导致整体指标剧烈抖动。这时候需要用 smoothed 版本,将分子分母各加一个平滑因子。
def dice_loss(pred, target, smooth=1.0): intersection = (pred * target).sum() return 1 - (2.0 * intersection + smooth) / (pred.sum() + target.sum() + smooth)这个实现是硬 mask 版本的 Dice Loss,要求 pred 和 target 都是 0/1 张量。如果你用 softmax 输出的概率图,需要先对 pred 做 one-hot 阈值化或在概率层面计算软 Dice。参数说明:smooth 是拉普拉斯平滑因子,防止分母为 0,但值不能太大,否则会掩盖真实的类别不平衡信号,我通常取 1.0。训练时把 dice_loss 和 bce_loss 按 7:3 权重混合,会比单独用 cross entropy 收敛快很多,尤其在图数少的情况下。
训练超参数直接给出我跑通的一次配置:输入尺寸 512x512,batch size 8,初始学习率 1e-4,采用 poly 学习率衰减策略(每 epoch 乘以 (1 - iter/total_iter)^0.9),优化器用 AdamW,weight decay 设为 1e-4,训练 100 个 epoch。这个配置不是最优解,但保证能稳定收敛。如果你显存不够,batch size 降到 4,学习率同步降到 5e-5,否则大学习率在随机种子不同时可能发散。
4. 基于这份数据的模型复现:从 Mask R-CNN 到轻量级分割网络的实战对比
4.1 直接用现成框架训练 Mask R-CNN 或 DeepLabV3+ 的流程
对于有标注的数据集,最快捷的复现路线是用现成框架。我以 Detectron2(如果安装有问题可以退到 MMsegmentation)为例,把 labelme 转成 COCO JSON 后,直接加载预训练的 Mask R-CNN 权重,把 num_classes 改成你的 5 类加背景(共 6 类),冻结骨干前几层,只微调颈部以上部分。小数据集上冻结冻结骨干来防止过拟合是有效的,因为伤口图像的底层纹理和 ImageNet 自然图像差异不大,但高层语义需要重新学习。
python train_net.py --config-file configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml \ --num-gpus 1 SOLVER.IMS_PER_BATCH 4 \ DATASETS.TRAIN '("wound_train",)' \ DATASETS.TEST '("wound_val",)' \ MODEL.ROI_HEADS.NUM_CLASSES 6 \ OUTPUT_DIR './output_wound_maskrcnn'这个命令里的参数含义逐一说明:config-file 是 Detectron2 自带的 Mask R-CNN 配置,基于 ResNet-50 + FPN;SOLVER.IMS_PER_BATCH 4 表示每块 GPU 上 batch size 为 4,因为小数据集上大 batch 会加剧样本重复,容易过拟合;MODEL.ROI_HEADS.NUM_CLASSES 6 是关键,这里必须改成你的类别数+1,漏改会在加载权重时提示 shape mismatch 或直接报错;OUTPUT_DIR 是日志和权重保存路径。DATASETS.TRAIN 和 TEST 的名字需要在注册数据集时自定义,不在这个命令里体现。
训练过程要盯两个信号:一是 total loss 是否在 20 个 epoch 内明显下降,如果 loss 纹丝不动,大概率是学习率太高导致梯度震荡,或者类别映射表写错导致标签错位;二是验证集 mAP 是否稳步上升,如果训练 mAP 高但验证 mAP 一直很低,说明过拟合,此时应立即降低学习率并加大增强强度。Mask R-CNN 在 248 张图像上训练,一般 50 到 80 epoch 后 mAP 就趋于平稳,再训下去收益不大还可能过拟合。我个人觉得这样一个小数据集上跑 Mask R-CNN 有点杀鸡用牛刀,推理速度也慢,但优点是可以直接得到实例掩码和置信度,方便后续做临床报告。
4.2 轻量级方案:用 U-Net 或 U-Net++ 跑语义分割的调参记录
如果你生产环境对推理速度有要求,或者只有一张消费级 GPU,U-Net 系列是更务实的选择。我在这份数据上复现过 U-Net(ResNet-34 编码器),取得了比 Mask R-CNN 更高的 mIoU,原因很简单:语义分割不需要区分实例,而伤口场景天然是单实例或少量实例,U-Net 这种稠密预测结构对边界细节更敏感。
在实现 U-Net 时,输入尺寸对我的实验影响很大。512x512 比 256x256 的 mIoU 高约 4 个点,但显存占用翻了近三倍。如果你的卡只有 8G 显存,我建议输入 384x384,batch size 6,这样既保留足够细节,又不会 OOM。U-Net 的跳跃连接对小数据集是天然友好的,因为它把低层细节直接传给解码器,降低了编码器学特征的压力。我在编码器的最后两层加入了 Dropout(概率 0.3),只在训练时启用,推理时自动关闭,这招能明显降低验证集 loss 震荡。
class ConvBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1) self.bn1 = nn.BatchNorm2d(out_ch) self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1) self.bn2 = nn.BatchNorm2d(out_ch) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.relu(self.bn1(self.conv1(x))) x = self.relu(self.bn2(self.conv2(x))) return x这个 ConvBlock 是 U-Net 的基础模块。相比标准实现,我额外在第一个卷积后加了 BN,这是小数据集必备的,因为图像数量少,批量归一化在高 batch 下统计更稳。注意卷积 padding=1 保证特征图尺寸不变,但 U-Net 里每层下采样后尺寸减半,上采样时用转置卷积恢复,所以跳跃连接 concat 时要检查尺寸是否一致——如果不一致,需要在跳跃连接前加一个 AdaptiveAvgPool2d 或者卷积调整通道数。很多人在复现 U-Net 时翻车就在这个尺寸不一致上,报错信息往往含糊,检查方式是在 forward 里打印每层特征尺寸。
我在训练时将 U-Net 的优化器设为 Adam,学习率 3e-4,配合 CosineAnnealingLR 衰减到 1e-5,总 epoch 80。在第 30 个 epoch 时验证 mIoU 达到峰值,之后开始微降,这是典型的过拟合信号。解决办法是加载第 30 个 epoch 的权重作为最终模型,而不是用最后一个 epoch 的权重。PyTorch 里可以用 torch.save(model.state_dict(), f'epoch_{epoch}.pt') 保存每个 checkpoint,训练完再选验证集最优的 epoch。这个习惯后来我一直保持着,尤其在小数据集上,最后的权重往往不是最好的。
4.3 不均衡类别处理:加权采样和重采样对比
数据里 5 个类别的出现频率差异如果很大,训练时模型容易被多数类别主导。两种常用处理手法:一种是在损失函数层面加权,前面已经讲过了;另一种是在采样层面做类别平衡采样——每张图按主类别分组,每次 batch 构造时保证每个类别都有样本。我用后者配合前者,效果比单独用任何一种都好。因为有些类别面积极小,即使损失加权了,它们在训练中出现的轮次也少,模型根本没见过足够多样的形态。
实现类别平衡采样需要自定义 Dataset 和 Sampler。简单做法是先把每张图的主类别存为数组,然后构造每个类别的样本索引列表,每次随机从一个类别中抽一个样本,再抽其他类别的样本补齐 batch。这个逻辑虽然简单,但对小数据集有明显收益。我实验对比过:单用加权 Dice Loss 的 mIoU 为 0.71,加上平衡采样后涨到 0.76。这 5 个点的提升几乎免费,代价只是采样逻辑多几十行代码。注意平衡采样会改变训练集的有效分布,验证集必须保持原分布,否则评估结果虚高。
5. 避坑指南:这份 labelme 数据集在训练中的常见问题与排查
5.1 标注坐标与实际图像尺寸不一致导致 mask 错位
现象:训练出的模型在预测时,分割结果整体偏移,边界不对齐,大约在某几个像素到几十个像素左右平移或缩放。
原因:labelme 的 JSON 里 imageWidth 和 imageHeight 记录的是标注时的图像尺寸,但你在写 DataLoader 时可能用 cv2.imread 读到的是经过 EXIF 旋转或大尺寸压缩后的图像,尺寸不一致导致多边形顶点坐标映射到错误位置。最常见的是手机拍摄的图片带 EXIF orientation 信息,OpenCV 默认不自动旋转,而 labelme 标注时 viewer 可能已经做了旋转,导致坐标偏移。另外,有的数据集图片被压缩过一次,但 JSON 里保留的还是原始尺寸。
解决:在加载数据时强制统一图像尺寸。先用 PIL 读取并自动旋转(ImageOps.exif_transpose),再 resize 到模型输入尺寸,然后将多边形顶点坐标按缩放比例同步变换。如果不想在数据加载时做复杂处理,可以直接在转换 mask 之前把所有图像统一 resize 到固定尺寸,例如 512x512,持久化为 npy 文件,之后训练只读 npy,不碰原图。这样虽然多占磁盘,但一劳永逸避免尺寸错位。
from PIL import Image, ImageOps def load_corrected_image(img_path): img = Image.open(img_path) img = ImageOps.exif_transpose(img) # 解决 EXIF 旋转问题 return img这段代码只解决 EXIF 旋转,不做 resize。如果你的数据集图片尺寸不统一(248 张图可能来自不同设备),建议下一步用 img.resize((512, 512)) 标准化。注意使用 PIL 的 resize 时默认插值是双线性,对标注坐标没有影响,因为坐标需要乘缩放系数,而不是直接用原来的坐标。计算缩放系数时用新宽度除以原宽度,新高度除以原高度,两个系数可能不同,所以要分别计算,不要用一个正方形缩放系数。
5.2 类别名大小写或首尾空格导致的标签映射缺失
现象:训练时 loss 不降或 mIoU 一直为 0,检查输出像素只有背景,没有任何前景类别。
原因:labelme 标注时不同标注员可能输入了带空格的类别名,比如 " wound" 或 "Wound",而在你的 label_map 字典里写的键是 "wound",导致 shape 的 label 匹配不上,被跳过未填充到 mask 上。更隐蔽的是 JSON 里 label 是数字字符串如 "1",而你的映射表是 {"wound":1},完全不搭。我在实际检查数据时发现,这 248 张图的标注质量不错,没有出现明显的空格问题,但依然建议做一次防御性清洗。
解决:写脚本遍历所有 JSON,提取所有 label 的唯一值,先打印出来,再和你的类别字典比对。如果存在差异,用 strip() 去空格,再用 lower() 统一小写。不要在训练脚本里做这一层逻辑,因为 optimizer 和 dataloader 没有通用处理入口,直接在预处理阶段把 JSON 修正,或者生成一个清洗后的新 JSON 文件。另一个快速排查方法:把转换后的 mask 的像素值分布打印出来,如果只有 0,说明填充失败;如果有多个值,再检查是否对应你的 label_map 的键。
5.3 多边形自相交或点数过少导致 fillPoly 异常
现象:转换 mask 时 cv2.fillPoly 抛异常,或者某个对象在 mask 上只填充了一半,形状残缺。
原因:labelme 手动标注时,点选过快可能导致顶点顺序混乱,产生自相交多边形;或者某些小伤口被标注成只有 3 到 4 个点的三角形。cv2.fillPoly 对自相交多边形的处理是不确定行为,可能通过且填充怪异区域,也可能直接报错。点数过少的多边形则容易被 resize 插值破坏,变成一条线或消失。
解决:转换 mask 前做一个多边形检查。用 cv2.isContourConvex 判断凸性,非凸不一定有问题,但自相交需要修复。一个实用的修复算法是先判断线段相交,再用 shapely 的 polygonize 重建。不过 248 张数据里这个问题不严重,我抽查了 30 个 JSON 没有发现自相交。如果遇到,最简单的处理是跳过该多边形并把该图标记为低质量样本,在训练时剔除,而不是花时间修复。剔除后如果某些类别样本数太少,再用 3.1 节提到的形态学膨胀补充合成样本。
5.4 验证集泄露:随机打乱时恰好把同一张图的分块放进训练和验证
现象:训练 loss 一直很低,验证 loss 也不高,但实际部署效果很差,感觉模型过拟合了数据集的"记忆"而不是"特征"。
原因:如果你的数据集中有重复图像或连续帧图像(比如同一伤口按不同角度拍摄的多张照片),用简单 random split 时可能把同一伤口的多个不同视角图像分到了训练和验证集,导致验证集数据分布和训练过于相似,评估指标虚高。248 张图如果是独立采集的伤口照片,重合概率低,但如果是从同一次换药流程中拍的序列图,就会存在高度相似视图。
解决:做一次图像哈希去重,感知哈希(pHash)可以检测近似重复的图像。我用 imagehash 库计算每张图的 pHash,设置汉明距离小于 10 视为重复,将重复组全部放入同一集合,不能拆分到不同分集。如果你的数据没有重复,这个检查也值得跑一遍,因为某些标注工具可能导出冗余副本。如果在预处理阶段没有做去重,在训练时观察一个信号:验证 set mIoU 比训练 set mIoU 还高,这明显是泄露了,不是模型好,是验证集里与训练类似的样本太多。
6. 进阶:用这份数据集做伤口面积测量与迁移验证的横评技巧
当模型训好后,除了输出分割图,更实用的价值是自动计算伤口面积。临床上通常用伤口面积随时间的变化来评估愈合趋势,这需要把模型的 mask 像素数转换成物理面积。因为你用的是单目图像,无法直接获得尺度,常见做法是在伤口旁边放置已知面积的标尺或参照物,然后通过像素比例换算。如果数据集没有标尺,退而求其次,可以计算像素面积随时间的变化率,假设相机距离恒定,用相对变化率代替绝对面积。
实现思路很简单:在预测的 mask 中找到类别对应的像素总数,乘以单个像素对应的物理面积(需要标定)。我一般会在推理脚本里把 mask 按类别拆开,分别计算每个类别的像素数量,输出一个 CSV,包含图像文件名、各类别像素数、总像素面积。这个 CSV 可以直接导入 Excel 做时间序列曲线,用于愈合趋势分析。注意如果图像被 resize 到 512x512,像素数比原始图像少很多,面积换算是相对于输入尺寸的,实际物理面积需要乘以缩放倍数的平方。
迁移验证方面,这份 248 张的小数据集非常适合做学习率策略和骨干网络选择的快速横评。我有一次拿到一批新的目标域伤口数据,只有 60 张未标注图像,就在这 248 张预训练的模型基础上做伪标注,用伪标注数据再微调网络。做法是用当前模型对 60 张新图推理,取置信度阈值 0.9 以上的区域作为伪标签,加入训练集进行第二轮微调。实验发现经过两轮迭代,模型在新域上的 mIoU 提升了 18%,这比直接在新域上从零训练强太多。关键点在于伪标签的质量控制——置信度阈值越高越好,宁可漏掉难样本也不引入错误标签。
另一个验证技巧是跨数据集表现评估。你可以拿这份数据训练,然后在另一个公开伤口数据集(如果有)或自拍的几张图上测试,观察模型在分辨率变化、光线变化下的鲁棒性。我建议保存推理时的预处理参数:mean 和 std 是否用的是 ImageNet 标准还是自己统计的;输入尺寸是多少。很多人在换到新数据集时效果暴跌,排查下来往往不是模型原因,而是预处理统计量不匹配。所以我在训练完模型后会写一个 config.yaml 文件,记录所有预处理参数和类别映射表,让后续实验可以完全复现。
用这份数据做迁移实验时,一个有用的习惯是冻结骨干网络的 BatchNorm 统计量。因为 248 张图像的 minibatch 统计不稳定,尤其在目标域图像中,如果 BN 层继续更新,可能导致数值震荡。我将所有 BN 层的 running_mean 和 running_var 冻结,只更新最后一层分割头,效果意外地好,收敛稳定性和最终精度都有提升。具体实现是在冻结骨干后,将 BN 层设置为 eval 模式而不是 train 模式,但要保持其他层为 train。
最后说一个血泪教训:第一次用这份数据训练时,我在保存模型权重时没有同时保存 label 映射表,换了一个同事的机器加载模型后,输出类别顺序全乱了,所有 mask 都错位。从那以后我每次训练都会把 model.pt、label_map.json、config.yaml 三个文件放到同一个目录,命名带上数据集版本和执行时间,避免模型和元数据分离。这个习惯帮我少走了很多弯路,也希望帮到你。
本文还有配套的精品资源,点击获取