LabelMe标注转YOLOv8分割数据集:完整Python转换链路与避坑指南
发布时间:2026/10/1 14:33:07来源:尧图网络
简介基于Python的Labelme标注转YoloV8语义分割数据集工具包面向计算机视觉与人工智能方向的开发者、学生及科研人员主要解决Labelme JSON格式标注与YoloV8语义分割数据集之间的转换问题并支持一键自动划分训练集和验证集有效提升数据准备效率。压缩包共14个文件包含两个Python脚本分别负责批量格式转换与训练流程演示、多份JSON标注样本、配套示例图片以及Markdown格式的使用说明文档整体仅1.95MB结构清晰、轻量易用。已有70人学习下载适合用于课程设计、毕业设计或项目初期立项演示也方便初学者快速上手进阶。代码经严格测试可正常运行借助示例数据能直观看到转换前后的差异说明文档详细讲解了环境配置、脚本参数与常见问题可帮助读者少走弯路。在此基础上还可二次开发扩展其他数据格式支持或自定义增强功能是一份兼顾学习与实用的工具源码。1. 从LabelMe到YOLOv8语义分割这条转换链路从头搭最省心用LabelMe标注完一批图兴冲冲想丢给YOLOv8做分割训练结果发现YOLOv8根本不认LabelMe的JSON。LabelMe存的是带绝对像素坐标的多边形YOLOv8分割任务要的是归一化坐标的txt标注中间这层转换绕不开手动逐个换算坐标根本不现实。网上能找到的现成转换脚本多半只处理目标检测的矩形框遇到分割多边形的却很少而且动不动就少了「自动划分训练集和验证集」这一步。我自己的数据处理经历里做语义分割数据集时格式转换和数据集划分翻车的次数比调模型参数还多。这篇文章把基于Python的LabelMe到YOLOv8语义分割数据集转换完整讲一遍JSON字段分析、转换脚本、训练集和验证集自动划分、验证手段、高频踩坑点都按能复现的方式写。适合正在做语义分割数据集制作、想用YOLOv8训练自己的数据集、又不愿意在格式转换上反复折腾的Python开发者。2. 先看清两种格式的底细LabelMe JSON与YOLOv8分割标注的字段差异2.1 LabelMe一个JSON里到底存了什么LabelMe标注完成后每张图会生成一个同名JSON文件。很多人的第一反应是拿json.load直接读结果面对一堆嵌套结构就开始逐个字段猜。实际上这个JSON的结构很固定把关键字段拆开看一遍后面写转换脚本就不需要反复试探。一个典型的LabelMe JSON长这样截取关键字段{ version: 5.3.1, flags: {}, shapes: [ { label: building, points: [[312.0, 148.0], [328.0, 168.0], [300.0, 186.0]], group_id: null, shape_type: polygon, flags: {} }, { label: road, points: [[100.0, 220.0], [200.0, 220.0], [200.0, 260.0], [100.0, 260.0]], group_id: null, shape_type: polygon, flags: {} } ], imagePath: image_001.jpg, imageData: , imageHeight: 720, imageWidth: 1280 }重点是shapes数组里面每个元素对应一个标注对象。label是类别名字符串后续要映射成数字IDpoints是多边形顶点格式是[[x, y], [x, y], ...]单位是像素顺序就是标注时鼠标点击的顺序shape_type标记标注类型分割数据集只需要polygonrectangle、circle、line都要过滤或特殊处理。imageWidth和imageHeight是整张图的宽高坐标归一化全靠这两个值。imageData是Base64编码的图像数据这里有个容易踩的坑图片比较大时LabelMe默认不嵌入图像数据这个字段是空字符串。所以转换脚本不能依赖imageData取尺寸要优先读imageWidth/imageHeight这两个字段在LabelMe 5.x版本里一定会写入。只有老版本没有这两个字段时才回退去读原始图像文件拿尺寸。group_id用于把多个shape归为同一个对象比如一辆面包车拆成车身和车窗两部分标注group_id就相同。做分割训练时同一类的多个实例各自作为独立多边形传入就好group_id可以先忽略。flags字段绝大多数时候是空字典除非你用到了LabelMe的实例标记功能转换时直接跳过。还要说一句为什么不用LabelMe自带的导出脚本。LabelMe源码里带了labelme2voc.py和labelme2coco.py前者导出VOC格式的PNG mask后者导出COCO JSON。YOLOv8的segment训练既不认VOC的mask目录也不直接吃COCO JSON它要的是每张图一个txt的多边形坐标。所以自写转换脚本是标准做法绕不开。2.2 YOLOv8分割标注的txt组织方式YOLOv8的segment任务用的是每张图一个同名txt的格式训练集和验证集分目录放。每个txt里一行表示一个分割实例。txt一行的结构是class_id x1 y1 x2 y2 x3 y3 ...class_id从0开始对应dataset.yaml里names列表的下标坐标全部是归一化值用像素坐标除以图宽图高得到。注意这里存的是多边形顶点不是mask位图YOLOv8内部会把多边形转换成mask参与损失计算。举例一张1280x720的图标注了一个类别ID为2、顶点像素坐标分别是(312,148)、(328,168)、(300,186)的多边形那么txt里这行就是2 0.243750 0.205556 0.256250 0.233333 0.234375 0.258333计算过程312/12800.24375148/7200.20556以此类推。坐标保留到6位小数已经足够保留更多位只会让txt膨胀保留3到4位在多边形边界上会出现肉眼可见的锯齿不建议。数据集目录结构是固定的YOLOv8训练时按dataset.yaml里的相对路径找图和标注dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── dataset.yaml图和txt按基名匹配images/train/a.jpg对应labels/train/a.txt扩展名无所谓基名必须一致。dataset.yaml里写path、train、val、nc、names五个字段后面3.4节给完整生成代码。两种格式的差异可以浓缩成一张表对比项LabelMe JSONYOLOv8分割txt文件组织每图一个JSON每图一个txt坐标单位绝对像素归一化到[0,1]顶点写法[[x,y],[x,y]...]嵌套数组class_id x1 y1 x2 y2...类别表示字符串label数字ID数据划分不涉及训练集和验证集分目录关于「语义分割」这个词有个容易误解的点YOLOv8的segment模式严格说是实例分割每个对象单独一个实例同一类的两辆车就是两行标注。如果你的下游任务真的需要一类别一张mask的语义分割结果用这份txt数据训练没问题推理时把同类的实例输出按类别合并就行。从业者常说的「语义分割数据集制作」在这个工具链语境下指的就是这种按实例多边形标注、按类别评估的训练流程。2.3 环境准备Python版本与依赖安装顺序转换脚本本身不依赖YOLO但我希望你验证环节也能跑通所以环境按「标注→转换→训练」一条链装齐。我自己常用的Python版本是3.9LabelMe在3.8到3.10上表现稳定3.11之后偶尔会有PyQt兼容问题。先装LabelMe做标注。安装命令很简单pip install labelme下载慢就换国内镜像清华的PyPI镜像常年可用pip install labelme -i https://pypi.tuna.tsinghua.edu.cn/simpleLabelMe依赖PyQt5做界面最容易翻车的是pyqt5-sip版本冲突现象是装完执行labelme直接抛ImportError。我的固定组合是下面这组Windows和Ubuntu 20.04上都验证过pip uninstall pyqt5 pyqt5-sip pyqt5-qt5 -y pip install pyqt5-sip12.11.0 pyqt55.15.7装完执行labelme能弹出标注界面就算成功。Linux服务器如果报Qt platform plugin相关错误缺的是图形库补系统包sudo apt install libgl1 libglib2.0-0 libegl1转换和可视化环节还需要三个库YOLOv8环境里一般都有没有就单独补pip install numpy opencv-python pyyamlnumpy处理坐标数组opencv-python做可视化验证pyyaml生成dataset.yaml。Ultralytics包本身也顺手装掉训练验证一条命令的事pip install ultralytics装完做一个快速自检确认版本都起来了labelme --version python -c import ultralytics, cv2, numpy; print(ultralytics.__version__, cv2.__version__, numpy.__version__)机器有GPU就顺手nvidia-smi看一眼驱动和显存。没有GPU也没关系转换是纯CPU操作训练用devicecpu能跑慢一点而已。Ubuntu 20.04上搭CPU版本的YOLOv8环境前面这些步骤完全够用不需要额外编译。3. 转换脚本的实现从多边形坐标到归一化分割标签3.1 整体思路与目录扫描转换脚本要做四件事扫描所有JSON、解析shapes、按训练集和验证集划分、写txt和复制图像。我习惯把脚本写成单文件命令行传参这样在服务器上批量跑也方便。先写目录扫描import json import os import random import shutil from pathlib import Path def load_json_files(labelme_dir): 扫描labelme_dir下所有.json文件返回路径列表。 注意过滤掉标注过程中产生的临时副本。 json_files [p for p in Path(labelme_dir).glob(*.json) if not p.name.startswith(._)] if len(json_files) 0: raise ValueError(f{labelme_dir} 下没有找到任何.json文件) return json_files逻辑很简单但有个现实问题标注过程中经常产生临时文件或系统隐藏文件比如macOS的._开头副本。如果混入转换流程后面会生成脏数据。所以扫描时过滤掉以._开头的文件这是我被坑过一次之后养成的习惯。接着写解析单个JSON的函数def parse_labelme_json(json_path): 解析单个labelme JSON返回图像名、宽高和shapes列表。 with open(json_path, r, encodingutf-8) as f: data json.load(f) # 优先用JSON里记录的长宽避免依赖图像文件 img_w data.get(imageWidth, 0) img_h data.get(imageHeight, 0) if img_w 0 or img_h 0: # 老版本labelme可能没写回退到读图像文件 import cv2 img_path os.path.join(os.path.dirname(json_path), data[imagePath]) img cv2.imread(img_path) img_h, img_w img.shape[:2] shapes [s for s in data[shapes] if s[shape_type] polygon] return data[imagePath], img_w, img_h, shapes这里我过滤了shape_type只保留polygon。矩形标注可以转成4个顶点的polygon但需要额外逻辑避坑章节再讲。用encodingutf-8读JSON是为了处理中文类别名Windows上默认编码可能是gbk不指定会抛UnicodeDecodeError。3.2 坐标归一化与类别ID映射核心转换逻辑把像素坐标除以宽高得到归一化坐标再拼成YOLOv8要求的行格式。def shape_to_yolo_line(shape, cls_id, img_w, img_h): 把一个shape转成YOLOv8分割txt的一行。 cls_id: 类别映射后的数字ID 返回字符串格式: class_id x1 y1 x2 y2 ... pts shape[points] # 过滤掉少于3个顶点的多边形YOLOv8无法处理 if len(pts) 3: return None norm_pts [] for x, y in pts: # 归一化并限制在[0,1]内防止标注时鼠标拖出边界 nx min(max(x / img_w, 0.0), 1.0) ny min(max(y / img_h, 0.0), 1.0) norm_pts.append(f{nx:.6f}) norm_pts.append(f{ny:.6f}) return f{cls_id} .join(norm_pts) \n这里有两个关键参数要说明。一个是归一化后的坐标裁剪标注时如果多边形顶点点到了图片边缘外侧像素坐标会超过图宽图高直接除完会得到大于1的归一化值YOLOv8训练时对越界坐标会告警甚至丢弃该实例。我用min/max把它夹到[0,1]区间这是稳妥做法。另一个是小数位数保留6位对应到1280宽度上约0.002像素的误差完全可以忽略。有同事保留8位txt大一圈但没有实际精度收益。类别映射需要一份类别表我建议用配置文件维护# class_names.txt 每行一个类别名行号从0开始和dataset.yaml的names顺序一致 class_names [] with open(class_names.txt, r, encodingutf-8) as f: for line in f: line line.strip() if line: class_names.append(line) cls_map {name: idx for idx, name in enumerate(class_names)} print(类别映射表:, cls_map)类别名必须和LabelMe标注时的label字段完全一致包括大小写和空格。如果LabelMe里标了Building映射表里写building这个shape会被直接跳过训练时这张图就少了一个对象。3.3 自动划分训练集和验证集随机种子与比例划分逻辑单独拆一个函数不跟写txt混在一起想换划分策略时只改一个地方def split_train_val(json_files, train_ratio0.8, seed42): 按比例划分训练集和验证集。 用固定随机种子保证可复现同一个数据集每次划分结果一致。 random.seed(seed) shuffled json_files.copy() random.shuffle(shuffled) n_train int(len(shuffled) * train_ratio) train_files shuffled[:n_train] val_files shuffled[n_train:] return train_files, val_files用random.seed而不是numpy的seed是因为这个脚本可能跑在不同环境里numpy版本不同随机数序列会有差异。random模块的Mersenne Twister实现非常稳定固定seed之后划分结果可复现。train_ratio我一般设0.8即80%训练20%验证。小数据集少于200张建议提到0.85到0.9因为验证集太少时训练过程验证指标波动大loss曲线抖得没法判断拟合情况。这里有个容易忽略的点划分是以「图」为单位不是以「标注实例」为单位。同一张图不会拆开分到两边但同一个场景连续拍摄的多张图可能全落在训练集或验证集导致验证集分布偏移。如果数据有明显场景分组最好按场景分组划分进阶篇再展开。3.4 输出目录结构与dataset.yaml的生成最后把划分结果落盘生成YOLOv8要求的目录结构和标注文件def convert_and_split(labelme_dir, output_dir, train_ratio0.8, seed42): json_files load_json_files(labelme_dir) train_files, val_files split_train_val(json_files, train_ratio, seed) for split, files in [(train, train_files), (val, val_files)]: img_out Path(output_dir) / images / split lbl_out Path(output_dir) / labels / split img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) for json_path in files: img_name, img_w, img_h, shapes parse_labelme_json(json_path) # 复制图像到images/split目录 src_img Path(json_path).parent / img_name if src_img.exists(): shutil.copy(src_img, img_out / img_name) else: print(f警告: 图像文件 {src_img} 不存在跳过 {json_path.name}) continue # 写分割标注txt txt_path lbl_out / (Path(img_name).stem .txt) lines [] for shape in shapes: cls_name shape[label] if cls_name not in cls_map: print(f跳过未映射类别 {cls_name} {json_path.name}) continue line shape_to_yolo_line(shape, cls_map[cls_name], img_w, img_h) if line: lines.append(line) txt_path.write_text(.join(lines), encodingutf-8) print(f转换完成: train {len(train_files)} 张, val {len(val_files)} 张)参数说明labelme_dir是原始标注目录output_dir是转换输出目录train_ratio是训练集比例seed是随机种子。函数把图像和标注一起复制到新目录YOLOv8训练时直接指向output_dir即可。注意如果output_dir已经存在先删掉重建旧文件残留会导致图标注不匹配。提示重复跑转换脚本前先删掉旧的output目录否则残留的旧标注文件会和新文件混在一起训练时图标注对不上排查起来很痛苦。dataset.yaml最后单独生成dataset_cfg { path: str(output_dir.resolve()), train: images/train, val: images/val, nc: len(class_names), names: class_names } with open(Path(output_dir) / dataset.yaml, w, encodingutf-8) as f: yaml.dump(dataset_cfg, f, allow_unicodeTrue, sort_keysFalse)allow_unicodeTrue保证中文类别名写成可读中文而不是\uXXXX转义sort_keysFalse让字段顺序贴近YOLOv8官方示例调试时对照方便。到这里一个带训练集和验证集划分的完整转换脚本就齐了。4. 转换完先别急着训练三类验证手段4.1 统计标签文件的基本信息转换完别急着开训练先跑一个统计脚本检查txt数量和内容是否合理from pathlib import Path def check_labels(labels_dir): 统计labels目录下所有txt的行数分布和坐标范围。 txt_files list(Path(labels_dir).glob(*.txt)) total_instances 0 empty_files 0 bad_ranges 0 for txt in txt_files: lines txt.read_text(encodingutf-8).strip().split(\n) if not lines or lines []: empty_files 1 continue total_instances len(lines) for line in lines: parts line.split() # 第一项是类别ID后面是成对的坐标 coords [float(v) for v in parts[1:]] if any(c 0 or c 1 for c in coords): bad_ranges 1 print(f标签文件数: {len(txt_files)}) print(f总实例数: {total_instances}) print(f空文件数: {empty_files}) print(f越界坐标行数: {bad_ranges})这个脚本主要看三个指标空文件数量如果很大说明很多标注图没有有效polygon去检查过滤条件越界坐标行数应该是0有就回到归一化那一步查裁剪逻辑平均每图实例数异常低说明有类别映射丢失或者大量shape被过滤了。还要单独看一眼类别分布尤其是那些稀有类别def check_class_distribution(labels_dir, class_names): 按类别统计实例数稀有类别如果在验证集里分不到样本后面训练会很头疼。 counts {name: 0 for name in class_names} for txt in Path(labels_dir).glob(*.txt): for line in txt.read_text(encodingutf-8).strip().split(\n): if line: cls_id int(line.split()[0]) if cls_id len(class_names): counts[class_names[cls_id]] 1 print(counts)打印出来的counts如果出现某个类别个位数的情况先记下来。这类稀有类别在随机划分里很容易全部掉进训练集或者全部掉进验证集后面进阶篇的分层抽样就是专门解决这个问题的。4.2 把分割标注可视化回原图统计只能发现数值问题看不出几何问题。多边形顶点的顺序错了、凹多边形标注反了、标注跟物体对不上都得可视化才能发现。我一般把验证图输出到一个check目录import cv2 import numpy as np import random from pathlib import Path def visualize_annotations(image_dir, label_dir, out_dir, class_names, max_count20): 随机挑max_count张图把多边形画回原图用于人工检查。 images sorted(Path(image_dir).glob(*.jpg)) images sorted(Path(image_dir).glob(*.png)) out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) random.seed(0) sample random.sample(images, min(max_count, len(images))) for img_path in sample: img cv2.imread(str(img_path)) txt_path Path(label_dir) / (img_path.stem .txt) if not txt_path.exists(): continue for line in txt_path.read_text(encodingutf-8).strip().split(\n): parts line.split() cls_id int(parts[0]) coords [float(v) for v in parts[1:]] pts [(int(coords[i] * img.shape[1]), int(coords[i 1] * img.shape[0])) for i in range(0, len(coords), 2)] # 把首点追加到末尾cv2.polylines才会闭合多边形 pts_closed pts [pts[0]] cv2.polylines(img, [np.array(pts_closed)], True, (0, 255, 0), 2) label_name class_names[cls_id] if cls_id len(class_names) else str(cls_id) cv2.putText(img, label_name, pts[0], cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(str(out_dir / img_path.name), img) print(f可视化结果已保存到 {out_dir})这里把归一化坐标乘回图像宽高还原像素位置。注意cv2.polylines不会自动闭合多边形必须手动把首点追加到顶点列表末尾否则画出来是个开口形状很容易误判标注有问题。print里的类别名和框位置对不上时先怀疑归一化时宽高顺序是不是写反了这个错我犯过两次。4.3 用YOLOv8最小命令跑通训练链路目录结构和yaml都齐了训练验证只需要一条命令。前提是2.3节装好了ultralyticsyolo tasksegment modetrain \ data/path/to/output/dataset.yaml \ modelyolov8n-seg.pt \ epochs10 \ imgsz640 \ batch8 \ device0参数说明tasksegment告诉YOLOv8走分割任务modelyolov8n-seg.pt会自动下载nano尺寸的预训练权重没外网环境就手动下载放到当前目录imgsz640是训练输入尺寸如果你的图都是大图且目标偏小这个值可以提到960batch大小根据显存调8G显存跑nano模型batch8没问题6G显存的GTX 1660 Ti建议降到batch4device0用GPU没GPU就改成devicecpu。第一次跑如果终端打印出数据集类别数和图像数说明dataset.yaml和目录结构都对上了。观察loss曲线前10个epoch的seg_loss如果能稳定下降说明模型学到了东西。如果val/box_loss一直是nan基本可以断定标注文件里出现了YOLOv8解析不了的行回到4.1的统计脚本去查。还有两个常见的起步失败提示dataset.yaml找不到多半是path字段写成了相对路径或者路径里有中文提示found 0 images检查images/train目录是否真的存在以及yaml里train字段写的是相对路径而不是绝对路径。5. 转换链路中的高频坑排查与避坑记录5.1 labelme装不上pyqt5启动直接报错现象pip install labelme之后命令行执行labelme报ImportError: cannot import name sip from PyQt5或者崩溃在Qt platform plugin相关错误上。原因pyqt5-sip版本和pyqt5版本不匹配。PyQt5 5.15.x对sip版本有要求pip自动解析偶尔会装上不兼容的组合。Qt platform plugin错误则常见于Linux服务器缺少libGL等图形库。解决Windows上按2.3节的固定版本组合重装。Linux服务器上除了pip重装还要装系统库sudo apt install libgl1 libglib2.0-0 libegl1装完再执行labelme --version确认能起来。这个问题在labelme安装教程里被问得最多本质就是PyQt5生态的版本管理问题。5.2 多边形坐标越界训练时实例被丢弃现象转换完统计数据一切正常训练日志里出现坐标clip或resegment的警告某个类别的mAP明显比其他类别低。原因标注时鼠标在画布边缘拖拽多边形顶点落到了图像外像素坐标超过了图宽图高。YOLOv8对归一化坐标要求严格落在[0,1]越界会触发警告并可能裁剪掉部分区域。解决在shape_to_yolo_line里用min/max做裁剪3.2节的代码已经包含。同时要在可视化验证时注意边界上有没有被压扁的多边形——裁剪会把图像外的部分直接切掉如果是边缘物体被切掉一半回标注软件修一下更稳妥别指望裁剪能兜住所有情况。5.3 shape_type混入了rectangle转换出来少东西现象某个类别总实例数明显少于标注时的印象检查JSON发现一部分shape的shape_type是rectangle。原因LabelMe工具栏里矩形工具和创建多边形工具是分开的标注员容易误点成矩形。rectangle只有两个对角点不是多边形直接过滤就会丢标注。解决转换脚本里把rectangle转成4点polygondef rectangle_to_polygon(points): 把两个对角点展开成4个角点。 x1, y1 points[0] x2, y2 points[1] return [points[0], [x2, y1], points[1], [x1, y2]]在解析shapes时shape_type为rectangle的先转成polygon再走统一流程。不过建议转之前先统计有多少rectangle混入如果数量很多说明标注员习惯用矩形工具最好回标注阶段统一口径而不是在转换脚本里补。5.4 训练集和验证集划分的随机性问题现象多次运行转换脚本两次训练集内容不一样换一台机器跑结果又不同。调参对比时验证集上的涨点说不清是模型改出来的还是数据划分变了。原因random.seed(42)虽然固定了但如果你没用seed或者脚本里在seed之前有其他random调用随机数序列就变了。另一个隐藏坑是set的遍历顺序不稳定如果划分逻辑基于set不同Python进程顺序不同结果就不同。解决确保划分时先random.seed再shuffle列表列表用sorted()排序保证初始顺序一致。更稳妥的做法是把划分结果落盘每次转换先查清单没有才新划分split_manifest Path(output_dir) / split.txt if split_manifest.exists(): # 读已有的划分结果保持旧图划分不变 train_files read_list(split_manifest, train) val_files read_list(split_manifest, val) else: train_files, val_files split_train_val(json_files, train_ratio, seed) write_list(split_manifest, train, train_files) write_list(split_manifest, val, val_files)这样增量标注时新图走新划分旧图划分保持不变模型横向对比才有意义。没有这个机制你前一轮实验和后一轮实验的数据分布可能完全不一样结论全是错的。5.5 中文路径和中文类别名的编码折腾现象Windows上JSON里是中文类别名脚本一跑就抛UnicodeDecodeError或者训练时dataset.yaml里的中文名变成乱码。原因LabelMe保存JSON用UTF-8Windows控制台默认GBKopen()不带encoding参数会按系统默认编码读直接报错或读成乱码。yaml.dump默认会把非ASCII转成\uXXXX转义转义后YOLOv8读取没问题但人眼排查很痛苦。解决读文件全部显式指定encodingutf-8写yaml时加allow_unicodeTrue。另外强烈建议路径全程用英文图像文件名、目录名不要带中文和空格。Ultralytics在Windows上对中文路径有历史遗留问题能避免就避免这不是玄学是踩过之后的血泪经验。6. 批量转换的进阶技巧增量标注、类别重映射与划分稳定6.1 增量标注与类别重映射5.4节落盘的split.txt在这里会发挥大作用。新标注了一批图重新跑转换脚本已有训练集和验证集划分不动新图按全局比例划分或补进样本不足的一边。核心是只转换清单外的文件converted set() for split in [train, val]: for line in (Path(output_dir) / f{split}.txt).read_text().splitlines(): converted.add(line) new_files [p for p in json_files if p.stem not in converted]增量转换最大的价值是保持验证集纯净。如果每次都重划分验证集里混入标注风格差异大的新数据指标会来回抖你以为是模型问题其实是数据问题。类别重映射则是为「标注用中文、训练用英文」或「漏标类别需要调整ID顺序」准备的。不要改原始JSON维护一份old_name→new_name映射文件转换时先映射再写txt。原始标注永远可追溯改映射只是重跑一次转换的事。6.2 按类别实例数分层抽样让验证集别再缺类别最后说一个影响很大的细节也是我踩得最深的一个坑。随机按图像数量划分训练集和验证集看着只是20%的小事实际上对分割模型影响巨大。某个类别在标注里只有30个实例随机划分后验证集只分到2个这个类在验证集上loss忽高忽低你怎么调学习率都没用。后来我把划分改成按类别实例数分层抽样保证每个类别在验证集都有一定占比。实现不复杂几十行import collections import random def stratified_split(json_files, train_ratio0.8, seed42): 按类别做分层抽样避免某个类别在验证集里缺席。 random.seed(seed) # 统计每个JSON里出现的类别 file_cls {jf.name: set(s[label] for s in parse_labelme_json(jf)[3]) for jf in json_files} # 按类别分组组内各自按比例抽验证样本 val_names set() for cls in set(c for cs in file_cls.values() for c in cs): names [n for n, cs in file_cls.items() if cls in cs] random.shuffle(names) n_val max(1, int(len(names) * (1 - train_ratio))) val_names.update(names[:n_val]) val_files [f for f in json_files if f.name in val_names] train_files [f for f in json_files if f.name not in val_names] return train_files, val_files这个函数的代价是一个文件包含多个类别时可能被多个类别重复抽进验证集最后set去重后训练集和验证集比例会略微偏离train_ratio可接受。好处是验证集上每个类别的IoU曲线稳定了很多调参终于不用靠猜。以后你遇到训练集和验证集划分后指标异常波动先看一眼是不是某个类别在验证集里样本太少这比改模型结构优先级高。我现在拿到任何新数据集第一件事就是跑一遍类别分布统计再决定用简单划分还是分层抽样。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网