资讯详情

资讯详情

labelme格式皮肤伤口分割数据集:从解压到YOLOv8-seg训练全流程指南

简介这份皮肤伤口分割数据集面向医学图像分析、深度学习语义分割/实例分割方向的开发者与研究者提供经过人工标注的皮肤创伤样本可用于模型训练、验证与算法调优。资源包含284张JPEG原始图像和284个对应的Labelme JSON标注文件另有1个说明txt共569个文件压缩包整体约8.38MB。标注类别覆盖bruises瘀伤、burns烧伤、cuts切割伤、ingrown_nail嵌甲、stab_wound刺伤5类常见皮肤伤口其中bruises样本量最多140个burns 84个cuts 56个stab_wound 37个ingrown_nail 32个整体分布存在不均衡可用于测试模型在少样本类别上的泛化能力。所有JSON标注文件均由Labelme 5.5.0绘制多边形框可直接用Labelme打开检查或二次编辑压缩包不包含mask文件读者可按需自行转换为COCO、YOLO或mask格式适配语义分割或实例分割任务。目前已有287人浏览学习适合需要快速获取带标注皮肤伤口数据、开展医学影像分割实验的读者。1. 皮肤伤口分割数据集拿到手先弄清这三件事“皮肤伤口分割数据集labelme格式248张5类别.7z”这个压缩包拆开看就是三件事248张伤口图像、对应的labelme格式JSON标注、以及5个语义类别。Labelme标注的JSON记录的是多边形轮廓点而不是像素级mask所以这个数据集既能做语义分割也能转成实例分割来训。248张的量级对深度学习来说偏小但它胜在标注维度统一、类别集中用来跑通一个医疗图像分割流程、做算法验证或者产品原型都非常合适。这个标题最容易被新手忽略的一点是labelme格式只是一个“中间格式”几乎没有任何训练框架能直接吃它。你要先做数据体检再转成YOLO分割格式或COCO格式然后才能进训练管线。这篇文章就按“解压→体检→转格式→训练→排错→验证”的顺序把每一步的命令、脚本和参数都摊开讲中间会穿插一些我自己踩过的坑。适合刚接触医疗图像分割、手上有一套标注数据但不知道怎么下手的工程师。2. 解压与体检7z 里不只有图片还有 JSON 标注2.1 解压 .7zWindows 与 Linux 两条命令拿到 .7z 后缀的压缩包别直接用系统自带的解压工具去点Windows 自带的资源管理器大概率会报“压缩文件格式不支持”。.7z 用的是 LZMA 算法需要专门的工具。Windows 下常见做法是装 7-Zip右键选择“解压到当前文件夹”即可Linux 下用 p7zip。# Debian/Ubuntu 系安装 p7zip sudo apt install p7zip-full # 解压到指定目录-o 后面不要留空格 7z x 皮肤伤口分割数据集labelme格式248张5类别.7z -o./wound_data这里有个小坑-o参数后面直接跟路径写成-o ./wound_data会把目录名解析成“空格路径”。解压完成后建议顺手跑一遍7z t校验压缩包完整性这个命令不需要解压全部文件就能检测 CRC 错误后面避坑章节会再展开。解压后你会看到两类文件一类是.jpg或.png原始图像另一类是跟图像同名的.json标注文件。如果发现某个 JSON 异常大比如好几 MB说明保存时勾选了嵌入图像数据imageData字段是 base64 字符串这种标注文件会在后面对齐图像时引入不必要的混乱建议后续用脚本清洗掉。2.2 labelme 的 JSON 长什么样五类信息逐一拆开Labelme 的格式不复杂每个 JSON 对应一张图核心信息就五个字段。用任意文本编辑器打开一个标注文件你会看到类似这样的结构{ version: 5.4.1, flags: {}, shapes: [ { label: necrotic_tissue, points: [[312, 154], [318, 152], [325, 156], [321, 165]], group_id: null, description: , shape_type: polygon, flags: {} } ], imagePath: 001.jpg, imageData: null, imageWidth: 1280, imageHeight: 960 }shapes数组里装的是这张图里所有标注对象每个对象有四个关键信息label是类别名points是多边形顶点坐标的列表shape_type是标注形状伤口分割场景下基本都是polygongroup_id是实例编号。imageWidth和imageHeight是标注时的画布尺寸imagePath是相对路径。这里有一个必须警惕的字段imageWidth和imageHeight不总是等于实际图片尺寸。标注工具被拖拽缩放、或者图片被替换过就会导致 JSON 里记录的和实际不符。我一般从不用这两个字段做坐标归一化而是用 PIL 重新读取图片尺寸下面转格式的脚本里会体现这一点。2.3 数据体检脚本类别数量、图片尺寸、异常标注一次查清拿到 248 张图别急着转格式先写个脚本把数据从头到尾摸一遍。这个体检脚本我会看四样东西类别有哪些、每个类别多少实例、图片尺寸分布、有没有空的或损坏的 JSON。磨刀不误砍柴工这一步能省掉后面大量排错时间。import json import os from collections import Counter from PIL import Image root wound_data label_counter Counter() shape_types Counter() point_counts [] size_counter Counter() broken_files [] empty_shapes_files [] for fname in sorted(os.listdir(root)): if not fname.endswith(.json): continue jpath os.path.join(root, fname) try: with open(jpath, encodingutf-8) as f: data json.load(f) except Exception as e: broken_files.append((fname, str(e))) continue if not data.get(shapes): empty_shapes_files.append(fname) continue for shape in data[shapes]: label_counter[shape[label]] 1 shape_types[shape[shape_type]] 1 point_counts.append(len(shape[points])) # 用实际图片尺寸做统计不信任 JSON 里的字段 img_path os.path.join(root, data.get(imagePath, )) if os.path.exists(img_path): with Image.open(img_path) as img: size_counter[img.size] 1 print(类别分布:, label_counter) print(shape_type 分布:, shape_types) print(多边形顶点数分布:, sorted(point_counts)[:5], ..., sorted(point_counts)[-5:]) print(图片尺寸分布:, size_counter) print(损坏 JSON:, broken_files) print(空标注 JSON:, empty_shapes_files)这段脚本的逻辑很简单但非常实用。类别分布直接告诉你 5 个类别是哪几个以及每个类别有多少实例这是后续做类别不平衡处理的依据。shape_type的统计很关键——如果混入了rectangle类型的标注你的转换脚本必须单独处理否则多边形点数对不上。顶点数分布用于判断标注的精细程度如果看到某个多边形有几百个点说明标注得过于细碎后续要做抽稀。空标注和损坏 JSON 这两个列表是训练时“loss 正常但 mAP 为 0”头号嫌疑犯。3. 把 labelme 格式转成能训的分割格式JSON 转 YOLO 与 COCO3.1 为什么要转格式训练框架读不懂 labelme 的“自由”字段Labelme 的设计目标是“人工标注工具”不是“训练输入格式”。它的 JSON 结构自由度高label是字符串不是数字、points是绝对像素坐标、一个文件可以有任意多个 shapes、甚至同一个类别可以拆成多个多边形。YOLO 和 COCO 这两套主流分割格式都要求更严格的约定类别必须映射成整数 id、坐标必须归一化到 0 到 1、每张图必须对应一个独立的结构化文件。你当然可以自己写一个 Dataset 类在训练时现读 JSON但这样做的代价是你把格式转换逻辑耦合进了训练代码改了标注就得改代码非常不划算。常见做法是先离线把全部 JSON 转成目标格式训练时只读标准文件。3.2 转 YOLO 分割格式脚本、坐标归一化与类别映射YOLO 分割格式的约定是每张图片对应一个同名.txt文件每行是一个对象格式为class_id x1 y1 x2 y2 ...所有坐标点都归一化到 0~1 之间的浮点数。下面是我常用的转换脚本兼容polygon和rectangle两种 shape_type。import json import os from PIL import Image # 类别映射这里以体检脚本的输出为准顺序必须和后续 data.yaml 严格一致 label2id { necrotic_tissue: 0, granulation: 1, redness: 2, normal_skin: 3, suture: 4 } def normalize_point(p, w, h): x, y p # round 到 6 位小数既保留精度又避免浮点噪声 x round(min(max(x / w, 0.0), 0.999999), 6) y round(min(max(y / h, 0.0), 0.999999), 6) return x, y def polygon_to_yolo_line(points, w, h, class_id): norm [] for p in points: x, y normalize_point(p, w, h) norm.append(f{x:.6f}) norm.append(f{y:.6f}) return f{class_id} .join(norm) def rectangle_to_polygon(points): # rectangle 只有两个对角点补成四点多边形 (x1, y1), (x2, y2) points return [[x1, y1], [x2, y1], [x2, y2], [x1, y2]] def convert_one_json(json_path, out_txt_path, label2id): with open(json_path, encodingutf-8) as f: data json.load(f) img_path os.path.join(os.path.dirname(json_path), data[imagePath]) with Image.open(img_path) as img: w, h img.size lines [] for shape in data[shapes]: label shape[label] if label not in label2id: print(f警告: 未知类别 {label} 在 {json_path}) continue class_id label2id[label] if shape[shape_type] polygon: pts shape[points] elif shape[shape_type] rectangle: pts rectangle_to_polygon(shape[points]) else: continue lines.append(polygon_to_yolo_line(pts, w, h, class_id)) # 空文件也要写出来YOLO 训练时跳过空 txt 对应的图片 with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines) \n) json_dir wound_data/json out_dir wound_data/labels os.makedirs(out_dir, exist_okTrue) for jname in sorted(os.listdir(json_dir)): if not jname.endswith(.json): continue jpath os.path.join(json_dir, jname) txt_name jname.replace(.json, .txt) convert_one_json(jpath, os.path.join(out_dir, txt_name), label2id)这段代码有几个细节值得说。第一坐标归一化用的是 PIL 读出的真实图片宽高不是 JSON 里的imageWidth前面提过原因。第二min(max(x / w, 0.0), 0.999999)这段 clamp 操作很关键如果标注点的坐标恰好等于图片宽度归一化结果就是 1.0部分训练框架在 Resize 时会把坐标推出边界导致采样越界。第三rectangle转polygon必须做否则四个点的矩形和 labelme 里存储的两个对角点会直接让坐标数量对不上。第四即使某张图没有任何有效标注也要写出空的 txt 文件这是给后续数据划分留的接口。3.3 转 COCO 格式适合 Mask R-CNN 系模型的另一条路如果你的目标框架不是 YOLO 而是 Mask R-CNN、或者想用 Detectron2、MMDetection 这一套那需要转成 COCO 格式。COCO 是一整个大 JSON包含images、annotations、categories三个数组。每个 annotation 里除了多边形坐标还要提供bbox和area这两个值用几何计算得到。import json import os from PIL import Image import numpy as np def poly_area(points): # 用鞋带公式算多边形面积不引入 shapely 依赖 xs [p[0] for p in points] ys [p[1] for p in points] return 0.5 * abs(sum(xs[i] * ys[i1] - xs[i1] * ys[i] for i in range(len(points) - 1))) def poly_bbox(points): xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) return [x_min, y_min, x_max - x_min, y_max - y_min] coco { info: {description: skin wound segmentation}, images: [], annotations: [], categories: [{id: i, name: name} for name, i in label2id.items()] } ann_id 1 for img_id, jname in enumerate(sorted(os.listdir(json_dir))): if not jname.endswith(.json): continue jpath os.path.join(json_dir, jname) with open(jpath, encodingutf-8) as f: data json.load(f) with Image.open(os.path.join(os.path.dirname(jpath), data[imagePath])) as img: w, h img.size coco[images].append({ id: img_id, file_name: data[imagePath], width: w, height: h }) for shape in data[shapes]: if shape[label] not in label2id: continue pts np.array(shape[points], dtypenp.float32) area poly_area(pts.tolist()) bbox poly_bbox(pts.tolist()) coco[annotations].append({ id: ann_id, image_id: img_id, category_id: label2id[shape[label]], bbox: bbox, area: area, iscrowd: 0, segmentation: [shape[points]] # COCO 要求展平数组此处做嵌套 }) ann_id 1 with open(wound_data/annotations.json, w, encodingutf-8) as f: json.dump(coco, f, ensure_asciiFalse)这段代码里segmentation字段直接塞了 labelme 的嵌套数组COCO 官方要求的其实是展平后的单层数组[x1, y1, x2, y2, ...]所以如果你用的框架校验严格还需要做一层展平。area用鞋带公式算的是像素面积没有做真实物理尺度换算。bbox是[x_min, y_min, width, height]格式不是左上右下两点格式。这些都是在踩了坑之后才注意到的细节先写在这里帮你避开。4. 用 YOLOv8-seg 训练自己的伤口分割模型参数与增强4.1 准备 data.yaml 和目录结构别小看这个文件YOLOv8-seg 训练时需要一个 data.yaml 描述数据路径、类别名和类别顺序。这个文件的坑在于names的顺序必须和第 3 章里的label2id完全一致多一个或少一个都不行。这里先把目录结构整理成 YOLO 的标准形式wound_data/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/然后写 data.yamlpath: /absolute/path/to/wound_data train: images/train val: images/val names: 0: necrotic_tissue 1: granulation 2: redness 3: normal_skin 4: suturepath建议写绝对路径相对路径在训练时经常因为工作目录不同而找不到数据。这里还涉及一个数据划分策略248 张图按 8:2 划分训练集和验证集即大约 198 张训练、50 张验证。划分时要按“图”为单位不要把同一张图的不同标注拆到两个集合里。如果你是按脚本划分我一般会用随机种子固定划分结果保证每次跑实验的验证集一致。4.2 训练命令与关键参数imgsz、epochs、batch 怎么设数据准备好了就可以开训。YOLOv8-seg 的训练命令非常简洁yolo segment train \ datawound_data/data.yaml \ modelyolov8n-seg.pt \ epochs150 \ imgsz640 \ batch8 \ patience20 \ projectwound_seg \ nameexp1modelyolov8n-seg.pt是 nano 版本显存占用小、训练快对于 248 张的小数据集更适合先跑通再升级。epochs150配合patience20做早停如果连续 20 轮验证集 mAP 不涨训练自动终止这个组合对小型数据集非常友好。imgsz默认 640但如果伤口区域在整图中占比很小建议提高到 1024 或 1280。分辨率翻倍显存占用大约翻四倍16GB 显存跑 1024 基本到顶。batch默认 16显存不够就是 8 或 4另外可以加ampTrue开启混合精度这个默认就是开的。参数不是越大越好。imgsz调大能提升小目标分割精度但代价是训练时间拉长、显存压力增大epochs在小数据集上 100 轮已经能收敛跑 300 轮反而可能过拟合让验证集 mAP 下滑。如果你的标注框很小可以试试optimizerAdamW替代默认的 SGD收敛更稳定但需要把lr0从默认的 0.01 降到 0.001这是我在小数据集上调参时比较常用的一组组合。4.3 类别不平衡与样本少的处理从增强到损失权重5 个类别的实例数量往往不均衡比如normal_skin这类背景性区域会远多于suture这类小目标。YOLOv8-seg 没有直接的类别权重参数我常用的三种处理思路第一种是过采样小类别。把包含小类别标注的图片复制几份放进训练集让模型每个 epoch 看到更多次。注意复制时要随机改变图像增强参数否则只是机械重复容易过拟合。第二种是关闭或降低对医疗图像有害的数据增强。YOLO 默认开启hsv_h、hsv_s、hsv_v颜色增强但伤口颜色本身是诊断信息比如坏死组织偏黑褐色、肉芽组织偏鲜红颜色抖动太强会让模型学不到颜色特征。可以在训练命令里加hsv_h0.0 hsv_s0.2 hsv_v0.2把这些增强压到很低。第三种是拼图增强多样化mosaic0.5而不是默认的 1.0因为 mosaic 会把四张图拼在一起裁掉大量上下文而伤口边界判断非常依赖周围皮肤纹理。这里要泼一盆冷水248 张图无论怎么增强模型的泛化能力都有限。你的目标应该是“在相同采集条件下稳定分割”而不是“对任何肤色、任何部位的伤口都有效”。如果产品场景跨度过大这个数据集的正确用法是作为预训练底料再用少量现场数据微调。这算是我做医疗图像项目的一条血泪经验。5. 伤口数据集翻车现场五个高频问题与排查清单5.1 类别顺序错乱训练 mAP 为 0 的第一嫌疑现象训练 loss 正常下降验证 loss 也正常但 mAP 一直是 0预测出来的 mask 颜色和真实类别完全对不上。原因labelme 的类别是字符串YOLO 的类别是整数 id。转换脚本里的label2id字典顺序和data.yaml里的names顺序有一处不一致模型学到的“id0”是 A 类别但评估时 id0 被解释成 B 类别整个混乱。解决训练前写一个 5 行的校验脚本读一个 JSON 的 label 顺序和data.yaml的 names 打印出来左右对比。我最早在这里翻过车后来把这段校验写成了固定的启动脚本每次训练前自动跑一遍对不上直接报错退出。import yaml import json with open(wound_data/data.yaml, encodingutf-8) as f: cfg yaml.safe_load(f) with open(wound_data/json/001.json, encodingutf-8) as f: data json.load(f) labels_in_json [] for shape in data[shapes]: if shape[label] not in labels_in_json: labels_in_json.append(shape[label]) print(JSON 类别顺序:, labels_in_json) print(data.yaml 顺序:, [cfg[names][i] for i in sorted(cfg[names])])5.2 空标注与漏标loss 不降的隐形杀手现象训练了几个小时loss 曲线像一条水平的线几乎不下降。验证集上所有图片的预测结果都是空白。原因某个 JSON 的shapes是空数组或者某张图根本没有对应 JSON。体检脚本里统计的empty_shapes_files就是干这个的。另一个可能是shape_type全是rectangle而转换脚本只处理了polygon导致所有标注都被跳过。解决训练前把体检脚本的输出存下来人工扫一眼空标注列表。如果空标注占比超过 5%直接把对应图片从数据目录移走不要硬塞给训练器。YOLO 训练时会静默跳过空 txt你几乎察觉不到异常但它会让模型学到“这类图没有目标”。5.3 多边形点过密训练卡顿的隐性瓶颈现象训练时 GPU 利用率不高CPU 跑到 100%数据加载成为瓶颈。打开转换后的 txt 文件一行有几百个坐标点。原因伤口边缘不规则标注时为了贴边打了几百个点归一化后 txt 文件体量比图片还大数据读取和解析成为瓶颈。解决用 Douglas-Peucker 算法做多边形抽稀。shapely 库的simplify方法一行就能实现tolerance 设置 1~2 个像素即可不要设太大否则伤口边缘就变成锯齿了。抽稀后一个多边形的点数通常能压缩到原来的 20%分割质量几乎不受影响。5.4 中文路径与 JSON 编码Windows 下的经典黑匣子现象在 Windows 上用 labelme 标注完毕数据集传到 Linux 服务器上训练报错提示找不到图片文件或者 JSON 解析失败。原因Windows 上 labelme 保存的imagePath可能带中文比如“伤口照片_001.jpg”这个字段是相对路径但 Linux 的文件系统编码、以及代码里的os.path.join拼接方式都可能让路径对不上。另一个常见问题是 JSON 里有中文字段用默认encoding读取时抛UnicodeDecodeError。解决在 Windows 上先做一个重命名清洗把图片统一改名为0001.jpg这种纯数字编号修改 JSON 里的imagePath字段指向新名字再用 UTF-8 编码重新写入 JSON。这一步做完所有跨平台问题都会消失。5.5 7z 解压文件损坏CRC 校验与重下判断现象解压到一半报CRC Failed或者解压完成后某些 JSON 打开报错、图片显示不全。原因压缩包下载不完整或者存储介质有坏道。.7z 格式自带 CRC32 校验文件只要有一个字节不对解压就会报错。解决在解压前先执行7z t 文件名.7z它会完整测试压缩包而不解压文件。看到Everything is Ok再解压。如果校验失败重新下载如果重下之后还是 CRC 错误检查硬盘剩余空间和文件系统类型NTFS/exFAT 通常没问题但老旧 FAT32 分区上大文件容易出现分配问题。这个步骤看起来多余却能帮你排除掉一个让人百思不得其解的“数据本身坏了”的干扰项。6. 验证环节别偷懒从 mAP 到伤口面积估算训练完成不是终点验证才是决定这个模型能不能用的关键。YOLOv8-seg 训练结束会在runs/segment/exp1目录下生成results.png和confusion_matrix.png先看这两张图。mAP50衡量的是目标位置准不准mAP50-95衡量的是 mask 边界质量医疗场景里两者要一起看。如果mAP50高但mAP50-95很低说明边界对不齐这对伤口分割来说意味着面积估算会偏差很大。我建议再做一步用yolo segment predict导出验证集预测 mask然后写脚本计算每个预测 mask 和真实 mask 的 Dice 系数。Dice 比 mAP 更直观地告诉你“轮廓重合度”低于 0.8 的样本单独拉出来看通常问题出在过分割把正常皮肤卷进伤口或欠分割漏掉了边缘的坏死组织。修正方式不是盲目调参而是把这个样本的标注找出来重新确认很多时候是标注本身漏了边界组织。验证完分割质量还有一个实用场景伤口面积估算。如果采集时知道每像素对应的物理尺寸比如用标尺标定过面积就是 mask 像素数乘以单个像素面积。我最早做这个方向时只信 mAP50结果面积估算误差超过 20%把验证集按伤口大小分桶才发现小伤口基本全漏检小目标的 Recall 才是这个场景的真瓶颈。从那以后我的固定习惯是先看小目标 Recall再看 mAP。这套流程走下来你应该能跑通一个可靠的分割模型剩下的就是在真实数据上持续补充标注了。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →