☰
labelme格式皮肤伤口分割数据集:从解压到YOLOv8-seg训练全流程指南
2026/10/11 20:51:16 网站建设 项目流程

简介:这份皮肤伤口分割数据集面向医学图像分析、深度学习语义分割/实例分割方向的开发者与研究者,提供经过人工标注的皮肤创伤样本,可用于模型训练、验证与算法调优。资源包含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 解压 .7z:Windows 与 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是标注形状(伤口分割场景下基本都是polygon),group_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, encoding="utf-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 与 COCO

3.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, encoding="utf-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", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") json_dir = "wound_data/json" out_dir = "wound_data/labels" os.makedirs(out_dir, exist_ok=True) 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[i+1] - xs[i+1] * 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, encoding="utf-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"], dtype=np.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", encoding="utf-8") as f: json.dump(coco, f, ensure_ascii=False)

这段代码里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.yaml:

path: /absolute/path/to/wound_data train: images/train val: images/val names: 0: necrotic_tissue 1: granulation 2: redness 3: normal_skin 4: suture

path建议写绝对路径,相对路径在训练时经常因为工作目录不同而找不到数据。这里还涉及一个数据划分策略:248 张图按 8:2 划分训练集和验证集,即大约 198 张训练、50 张验证。划分时要按“图”为单位,不要把同一张图的不同标注拆到两个集合里。如果你是按脚本划分,我一般会用随机种子固定划分结果,保证每次跑实验的验证集一致。

4.2 训练命令与关键参数:imgsz、epochs、batch 怎么设

数据准备好了就可以开训。YOLOv8-seg 的训练命令非常简洁:

yolo segment train \ data=wound_data/data.yaml \ model=yolov8n-seg.pt \ epochs=150 \ imgsz=640 \ batch=8 \ patience=20 \ project=wound_seg \ name=exp1

model=yolov8n-seg.pt是 nano 版本,显存占用小、训练快,对于 248 张的小数据集更适合先跑通再升级。epochs=150配合patience=20做早停:如果连续 20 轮验证集 mAP 不涨,训练自动终止,这个组合对小型数据集非常友好。imgsz默认 640,但如果伤口区域在整图中占比很小,建议提高到 1024 或 1280。分辨率翻倍显存占用大约翻四倍,16GB 显存跑 1024 基本到顶。batch默认 16,显存不够就是 8 或 4,另外可以加amp=True开启混合精度,这个默认就是开的。

参数不是越大越好。imgsz调大能提升小目标分割精度,但代价是训练时间拉长、显存压力增大;epochs在小数据集上 100 轮已经能收敛,跑 300 轮反而可能过拟合,让验证集 mAP 下滑。如果你的标注框很小,可以试试optimizer=AdamW替代默认的 SGD,收敛更稳定,但需要把lr0从默认的 0.01 降到 0.001,这是我在小数据集上调参时比较常用的一组组合。

4.3 类别不平衡与样本少的处理:从增强到损失权重

5 个类别的实例数量往往不均衡,比如normal_skin这类背景性区域会远多于suture这类小目标。YOLOv8-seg 没有直接的类别权重参数,我常用的三种处理思路:

第一种是过采样小类别。把包含小类别标注的图片复制几份放进训练集,让模型每个 epoch 看到更多次。注意复制时要随机改变图像增强参数,否则只是机械重复,容易过拟合。第二种是关闭或降低对医疗图像有害的数据增强。YOLO 默认开启hsv_h、hsv_s、hsv_v颜色增强,但伤口颜色本身是诊断信息,比如坏死组织偏黑褐色、肉芽组织偏鲜红,颜色抖动太强会让模型学不到颜色特征。可以在训练命令里加hsv_h=0.0 hsv_s=0.2 hsv_v=0.2把这些增强压到很低。第三种是拼图增强多样化,mosaic=0.5而不是默认的 1.0,因为 mosaic 会把四张图拼在一起裁掉大量上下文,而伤口边界判断非常依赖周围皮肤纹理。

这里要泼一盆冷水:248 张图无论怎么增强,模型的泛化能力都有限。你的目标应该是“在相同采集条件下稳定分割”,而不是“对任何肤色、任何部位的伤口都有效”。如果产品场景跨度过大,这个数据集的正确用法是作为预训练底料,再用少量现场数据微调。这算是我做医疗图像项目的一条血泪经验。

5. 伤口数据集翻车现场:五个高频问题与排查清单

5.1 类别顺序错乱:训练 mAP 为 0 的第一嫌疑

现象:训练 loss 正常下降,验证 loss 也正常,但 mAP 一直是 0,预测出来的 mask 颜色和真实类别完全对不上。

原因:labelme 的类别是字符串,YOLO 的类别是整数 id。转换脚本里的label2id字典顺序和data.yaml里的names顺序有一处不一致,模型学到的“id=0”是 A 类别,但评估时 id=0 被解释成 B 类别,整个混乱。

解决:训练前写一个 5 行的校验脚本,读一个 JSON 的 label 顺序,和data.yaml的 names 打印出来左右对比。我最早在这里翻过车,后来把这段校验写成了固定的启动脚本,每次训练前自动跑一遍,对不上直接报错退出。

import yaml import json with open("wound_data/data.yaml", encoding="utf-8") as f: cfg = yaml.safe_load(f) with open("wound_data/json/001.json", encoding="utf-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。这套流程走下来你应该能跑通一个可靠的分割模型,剩下的就是在真实数据上持续补充标注了。希望帮到你。

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

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

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

立即咨询