资讯动态

Labelme转YOLOv8语义分割数据集完整实战指南

发布时间:2026/10/1 23:10:34 来源:尧图企业网站定制
简介面向计算机视觉方向开发者这套Python工具源码可将LabelMe标注格式自动转换为YoloV8语义分割数据集并自动划分训练集与验证集大幅减少手工整理标注文件的时间也方便后续直接用于模型训练。资源共14个文件压缩包大小约1.95MB主要包括两个Python脚本用于批量格式转换与训练流程示例多个JSON标注源文件及配套JPEG/JPG图片便于对照验证转换效果另附Markdown格式的使用说明清晰交代运行方式与参数含义方便二次开发。代码经过严格测试功能完整适合人工智能、电子信息、物联网、自动化等专业的高校学生作为课程设计或毕业设计参考也可供科研人员、行业从业者借鉴使用。当前已有70人学习浏览项目内源码、示例数据与说明文档齐全读者可在现有逻辑上修改构建自有数据集的YoloV8语义分割训练集与验证集。1. 语义分割数据集转换先让labelme的json变成YOLOv8能吃懂的txt在你的目标检测和分割项目里从labelme标注格式转换到YOLOv8语义分割数据集是很多人绕不过去的一步。labelme保存的是JSON文件里面是每张图的对象标注、多边形坐标点和标签名而YOLOv8分割模型训练时要的是每行一个实例的归一化txt——类别id加一串多边形顶点同时还要把train和val拆成两个目录。手工去转小数据集还能忍几十张图以上就容易翻车点看漏了、坐标忘了归一化、类别对不上号哪一环出问题训练出来的模型都会症状百出。这套基于python的转换工具从读JSON、写归一化txt到自动按比例划分训练集和验证集一条命令走完适合那些已经在用labelme做语义分割数据集制作、准备跑yolov8训练自己的数据集又不想在格式转换上多花时间的人。2. LabelMe标注格式拆解JSON字段、坐标体系与归一化原理2.1 一份labelme JSON到底存了什么用labelme标注完一张图默认会生成一个和图片同名的.json文件。大多数人第一次打开这个文件时都会懵一下字段不算少真正关心就那几个。我一般把labelme的json理解为三块头部是imagePath和imageData中间是shapes数组尾部是imageWidth和imageHeight。imagePath存储的是原图文件名labelme默认会把它写成相对路径比如example1.jpeg如果你的图在子目录里会带路径。imageData是一长串base64编码的位图数据正常情况下体积很大如果你用labelme直接保存json这一串会占据整个json百分之九十以上的体积。转换脚本基本用不到它转成YOLOv8格式时可以直接忽略不要被它的长度吓到。shapes是整个json的核心它是一个数组长度等于这张图上的标注对象数量。每个元素是一个字典至少要关注三个子字段label是对象标签名比如road、carshape_type表示标注方式polygon、rectangle、circle、line都出现过points是坐标点列表polygon类型就是一个包含多个[x, y]对的数组注意顺序是x在前y在后。下面这个例子简化掉了imageData字段{ version: 5.3.1, imagePath: example1.jpeg, imageData: null, shapes: [ { label: road, shape_type: polygon, points: [[45, 62], [100, 67], [120, 150], [50, 158]] } ], imageWidth: 512, imageHeight: 384 }这个结构里最关键的信息是第一label字段不会自动排序它的写入顺序取决于你标注时点击的先后所以类别id不能靠JSON里的出现顺序直接定死第二points是图像像素坐标单位是像素必须经过归一化才能给YOLO用。shape_type字段也要额外留意因为rectangle的points只有两个点左上右下对角线circle有三个点语义完全不同混在一起用同一套逻辑解析转换结果会直接损坏。2.2 坐标归一化的关键为什么必须读原图尺寸YOLOv8语义分割训练时polygon坐标必须归一化到[0,1]区间公式很简单归一化x 像素x / 图像宽度归一化y 像素y / 图像高度。但这个公式成立的前提是你拿到的图像宽高是原图真实的宽高。这里有一个很多人踩过的坑json末尾的imageWidth和imageHeight并不总等于原图尺寸。为什么不等因为labelme在加载图时如果图太大它可能不会主动刷新尺寸信息更常见的是你标注完成之后在外部工具里把图像resize了但json没重新生成里边的尺寸还是旧值。所以我的做法是转换时不信任json里的任何尺寸字段直接用PIL打开imagePath对应的原图实时读它当前的宽高from PIL import Image def get_image_size(image_path): with Image.open(image_path) as img: w, h img.size return w, hImage.open返回的对象自带size属性元组第一个值是宽度第二个是高度和原图的像素宽高严格一致。为什么不直接信json里的imageWidth因为那只是labelme写json那一刻的快照不保证和当前文件一致。归一化基准错了txt里的坐标就是错的而这类错误在训练时不会报错只会让mask区域全部错位属于最难排查的问题之一。顺带一提如果imagePath是相对路径且工作目录不对PIL会直接抛FileNotFoundError这时用os.path.abspath拼一下路径就能定位问题。2.3 YOLOv8语义分割的txt与目录约定YOLOv8的分割数据集标签文件通常和图片同名但后缀是.txt放在labels目录下图片放images目录下。每个txt文件里一行对应一个实例格式是第一个数字是类别id后面是x1 y1 x2 y2 … 的坐标序列坐标全部是归一化后的浮点数顶点顺序保持和labelme里一致即可。0 0.087891 0.161458 0.195312 0.174479 0.234375 0.390625 0.097656 0.411458上面这一行表示类别0后面8个数是4个顶点的x/y坐标。注意YOLO格式里不管是检测还是分割坐标一概是归一化值。检测的txt里是xywh四个点分割的txt里是多边形顶点序列顶点数没有硬性上限但至少要有3个点才能构成多边形少于3个点在yolov8训练时会直接报错。目录结构一般按下面这样组织ultralytics框架的标准读取方式是img/train、img/val、labels/train、labels/val四个目录对应成对yolo_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml用来告诉训练脚本三件事train和val两个分支的图片路径以及nc和names类别信息。理解了这个目录约定后面再看convert_folder.py的参数和输出就不会被脚本内部的路径拼接绕晕。很多从检测转分割的人在这里最容易弄混分割txt的顶点数不是固定的不同对象点数可以不同所以不能用检测那套固定列数的读取逻辑去读。3. convert_folder.py实操扫描目录、解析JSON、写归一化txt3.1 脚本入口与参数input_dir和output_dir怎么传资源包里convert_folder.py是转换主程序建议先把它放到一个单独的目录里比如项目根目录然后在同级建两个文件夹一个放labelme标注的所有json和图片另一个让脚本输出yolo格式数据集。包里的使用说明.md把参数表写得很清楚但实际跑起来的问题还是得看转换脚本本身怎么处理边界情况。命令行参数按下面的方式传python convert_folder.py \ --input_dir ./labelme_annotations \ --output_dir ./yolo_dataset \ --val_ratio 0.2 \ --seed 42--input_dir是labelme标注目录要求图片和同名json都放在这个目录下--output_dir是输出目录不存在时脚本会帮你创建--val_ratio是验证集比例0.2表示五分之一的数据划分到val--seed是随机种子固定成42能保证每次划分结果一致。参数设计这个细节我个人很看重seed。因为如果你的数据集是动态更新的每次生成时如果不固定seedtrain和val的成员每次都变前后两轮训练的结果就没有可比性。初学者常见的问题是参数名记混--input_dir写成了--input报错unrecognized arguments时多半就是这里对不上。资源包里自带了examples目录里面有example1.json、example1.jpeg这类对应好的标注样例你可以先复制一个测试目录跑一遍这个命令确认输出结构正常再上全量数据。3.2 单个JSON的转换核心从points到归一化顶点序列转换的核心逻辑是遍历目录下所有json逐个解析再写出对应的txt。我把它拆成单个json的转换函数来写方便你单独调试某一张图import json import os from PIL import Image def convert_one(json_path, img_dir, label_dir, class_list): with open(json_path, encodingutf-8-sig) as f: data json.load(f) img_name data[imagePath] if os.path.isabs(img_name): img_path img_name else: img_path os.path.join(img_dir, img_name) with Image.open(img_path) as img: w, h img.size txt_name os.path.splitext(img_name)[0] .txt txt_path os.path.join(label_dir, txt_name) lines [] for shape in data[shapes]: label shape[label] if label not in class_list: class_list.append(label) cls_id class_list.index(label) points shape[points] norm_coords [] for x, y in points: nx min(max(x / w, 0.0), 1.0) ny min(max(y / h, 0.0), 1.0) norm_coords.append(f{nx:.6f}) norm_coords.append(f{ny:.6f}) lines.append(f{cls_id} .join(norm_coords)) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) return txt_path这段代码我强调三个点。第一是打开json时的编码用了utf-8-sig而不是utf-8原因后面排查章节详细说这里先记住。第二是读取原图尺寸时如果imagePath是绝对路径就直接用相对路径要和图片目录拼接否则会因找不到图而报FileNotFoundError。第三是坐标裁剪到[0,1]区间用min和max把越界点拉回来这个处理能拦住一些labelme标注时手误拖出画布的顶点。还有个细节是f{nx:.6f}保留6位小数。YOLO格式对小数位没有强制要求但保留6位已经足够支撑常见的1920x1080分辨率再更多位数对训练结果几乎没有影响反而让txt体积变大。你如果处理4K以上图像这个精度也够用因为归一化后坐标值本身就落在0到1之间误差被缩放过了。3.3 类别映射持久化classes.txt和data.yaml同步写上面convert_one函数里class_list是一个可变列表每遇到一个新标签就追加进去类别id就是它在列表里的下标。这个设计有一个隐含约束同一批数据跑完所有标签都在class_list里必须把它持久化下来否则第二次转换类别顺序可能变化。def write_classes(class_list, output_dir): cls_path os.path.join(output_dir, classes.txt) with open(cls_path, w, encodingutf-8) as f: for name in class_list: f.write(name \n) return cls_path一般我会在脚本最后把class_list写入一个classes.txt一行一个类名顺序就是id顺序。data.yaml里的names字段也是从这份列表生成。这样即使后续补充标注数据重新转换只要沿用同样的class_list顺序旧txt里的类别id就不会错位。这个点很关键因为如果你先转了100张图用到5个类别后来又加了第6个类别而新一次转换是重新从头扫新的列表顺序可能和之前不同模型训练时类别含义就对不上了。转换脚本跑完之后你要习惯看一眼classes.txt的内容确认类别顺序符合预期。我见过不止一次label里混入了一个多余的空字符串或者大小写不一致的写法导致class_list里出现空位。最稳妥的做法是转换完成后先打印一遍完整的类别映射再进入自动划分阶段。4. 自动划分训练集与验证集随机种子、目录规范与data.yaml4.1 按文件粒度shuffle固定seed保证可复现转换之后是自动划分。划分的单位是文件不是实例。一个图片对应一个json、一个txt所以按文件名列表来shuffle是合理的import random import os import shutil def split_dataset(img_dir, label_dir, train_img_dir, val_img_dir, train_label_dir, val_label_dir, val_ratio0.2, seed42): img_names sorted(os.listdir(img_dir)) random.seed(seed) random.shuffle(img_names) val_count int(len(img_names) * val_ratio) val_names set(img_names[:val_count]) train_names img_names[val_count:] for n in img_names: src_img os.path.join(img_dir, n) src_txt os.path.join(label_dir, os.path.splitext(n)[0] .txt) if n in val_names: dst_img_dir val_img_dir dst_label_dir val_label_dir else: dst_img_dir train_img_dir dst_label_dir train_label_dir shutil.copy(src_img, dst_img_dir) if os.path.exists(src_txt): shutil.copy(src_txt, dst_label_dir) return len(train_names), len(val_names)逻辑很简单先读全部图片文件列表按seed打乱取前val_ratio的比例作为验证集其余是训练集。这里用copy而不是move因为move会把原目录里的文件搬走之后还想对源目录做检查原始文件就不在了。copy的代价是磁盘占用翻倍但数据集本身就是图片多占一份空间换来原数据安全是划算的。划分完成后返回值里一个是训练集数量一个是验证集数量你可以打印出来看一眼比例是否接近预期。val_count取整时可能和真实val_ratio差一张这是正常的属于整数除法带来的误差不需要纠结。如果数据集很小比如只有20张图val_ratio反而是小事先保证每个类别在训练集里都出现过。4.2 目录结构与data.yaml生成ultralytics框架读取数据集时通过data.yaml里的path、train、val字段找图片labels目录会自动拼接出来。生成的data.yaml大致长这样path: /abs/path/to/yolo_dataset train: images/train val: images/val nc: 2 names: 0: road 1: car需要注意path写绝对路径还是相对路径。ultralytics文档里的建议是都可以写但训练脚本执行时的工作目录不同相对路径经常解析到别的地方然后报错train dataset not found。我的习惯是写成绝对路径或者用os.path.abspath动态生成保证在vscode里跑和命令行跑结果一致。names字段的顺序必须和前面class_list保持一致否则类别id错位模型训练出来语义就乱了。如果数据集目录结构和上面不一致比如你习惯把label目录叫maskultralytics也支持自定义但需要在data.yaml里写清楚不建议用默认推断。还有一个容易被忽略的点labelme里标注空白区域当作背景时如果你的业务里背景不需要建模就不要把它当一个类别标进shapes里。这会增加类别数还会让背景在分割mask里占据大面积权重影响前景类别的收敛。4.3 train_example.py的用法与关键参数资源包里的train_example.py是训练入口的参考实现它的作用不是把所有训练细节包圆而是给你展示一份可以跑通的最小配置。核心训练代码一般是这样的from ultralytics import YOLO model YOLO(yolov8n-seg.pt) results model.train( datayolo_dataset/data.yaml, epochs100, imgsz640, batch8, device0, projectruns/seg, nameexp1 )model用yolov8n-seg.pt这是官方预训练权重初次训练会去下载网络不好时可能卡住不动。epochs是训练轮数先跑100轮看loss曲线是否收敛batch的大小取决于显存8G显存跑n级模型用8比较稳imgsz是训练时resize到640如果你的原图长宽比差异很大可以考虑768device0表示用第一张GPU没有GPU就改成cpu。关键参数对照如下方便你根据自己的机器调整参数建议值说明epochs100先跑100轮观察收敛不收敛再加imgsz640分割任务里别超过原图分辨率batch88G显存起步大小OOM就降一半device0 / cpu无GPU用cpu速度慢但能跑这里我特别提醒一个容易踩的点训练脚本对标签格式极敏感txt里哪怕只有一行坐标出问题训练进程一般不会立刻中断而是等到计算loss时才抛异常浪费半天时间。所以训练前至少要确认txt非空、每行第一个数字是合法类别id、坐标数量是偶数且大于等于6也就是3个顶点。train_example.py里其实也做了基础检查但只检查文件数量检查不了内容这也是下一篇要讲可视化验证的原因。5. 常见问题与排查记录5条从转换到训练的真实踩坑5.1 JSON解析失败BOM头与中文路径现象运行convert_one打开某个json时直接抛json.decoder.JSONDecodeError提示Expecting property name enclosed in double quotes但用labelme重新打开文件却完全正常。原因labelme在Windows上保存json时部分版本会用带BOM的UTF-8编码文件头多了三个不可见字节标准json.load以utf-8读取时把这个BOM当成非法内容。另一个常见来源是中文目录json里的imagePath如果包含中文或反斜杠分割符也会在路径拼接时报错但错误信息通常是FileNotFoundError而不是解码错误。解决读取json统一用encodingutf-8-sig它能自动吃掉BOM头。imagePath如果是Windows下的反斜杠路径先做一次replace(\, /)再交给os.path.join去拼接。这两条改完之后Windows上从labelme导出的数据基本不会再出解码问题。5.2 归一化坐标越界手滑拖出画布的顶点现象转换完成txt里出现负数或者大于1的坐标值训练时偶尔报错检查时发现某些多边形顶点明显飘在图像外面。原因labelme标注时用鼠标左键添加顶点如果边界点拖出了画布范围points里就会记录超出图像宽高的坐标值。特别是带鱼眼镜头或长图缩放的标注任务中这类情况很容易混进去。归一化之后这些越界点的数值就落在[0,1]之外。解决写入txt前做一次裁剪x和y分别用min(max(v, 0), 1)限制到[0,1]区间。如果一个多边形所有顶点全部越界这种shape建议直接跳过并打印warning因为你很难知道它原本想标哪里。之前写convert_one时已经在归一化时顺手做了裁剪属于低成本高收益的防御。5.3 类别id错位二次转换时标签顺序变化现象第一次转换跑完类别0是road数据更新后再转一次类别0变成了car训练时模型预测的类别和你的业务命名对不上。原因class_list是边转换边收集的第一次遇到的标签顺序影响id分配。源目录里文件顺序变了比如新增了不同类别的图新一次转换得到的id顺序就和旧的不一致。labelme的shapes写入顺序又取决于标注时的点击顺序没法保证稳定。解决先全量扫描一遍所有json把标签集合收集齐再按固定顺序排序或者直接读取已有的classes.txt作为初始class_list。我的习惯是每次转换前先检查output目录里有没有classes.txt有就先加载没有才从本次数据里构建。这个做法能让新旧两批数据融合时类别id保持连续一致。5.4 图片与标注文件不成对多出来或缺失的json现象转换完成统计数量images目录260张labels目录只有251个txt训练时很多图找不到标签文件而报错或者反过来标签文件多出来。原因labelme标注中常出现标注到一半的json或者图片复制过来但标注忘了带。convert_folder.py如果对缺失情况静默跳过数量就始终对不上。训练时ultralytics遇到无标签的图通常只是跳过但你的有效训练样本数比想象中少一层。解决转换结束后做一次完整性校验遍历图片列表逐个检查同名txt是否存在缺失的图片统一移到一个待复查目录并打印具体文件名。图片多txt少的问题能靠这个拦下来txt多图少的情况在转换脚本遍历json时会报KeyError因为json里imagePath对应的图不存在。5.5 训练时报polygon点数不足shapes里混着非polygon类型现象用yolov8跑训练中途抛出Invalid vertex count or bad polygon指向某个txt文件手动打开看坐标数量也没看出问题。原因labelme标注时可能用了rectangle或line模式画目标这些模式下的points数量不是多边形顶点可能是2个点或3个点。转换脚本如果不管shape_type统一当作polygon处理就会出现只有2个顶点、1个点的非法txt。yolov8需要至少3个点构成面少于这个数直接报错。解决转换时判断shape[shape_type]非polygon的shape单独处理。rectangle的两个对角点可以补齐成4个点再转换circle建议直接跳过因为转换为多边形后点位数量不稳定。在标注规范里统一用polygon模式能省掉后面所有这类问题。如果数据已经标乱了可以加一段修正逻辑for shape in data[shapes]: if shape[shape_type] ! polygon: print(f{img_name}: skip {shape[label]}, type{shape[shape_type]}) continue这段代码的作用是过滤掉非polygon类型的shape同时打印哪张图的哪个标签被跳过了。跑完转换再去看输出目录里有没有大面积缺失的txt就能回头找标注规范的问题。6. 转换结果可视化验证把txt画回原图的最后一关6.1 quick_check可视化脚本转换脚本能跑通不代表坐标就是对的。我每次训练前都会做一个叠加可视化检查把txt里的归一化坐标乘回原图宽高然后再把多边形画到原图上。这一步能发现所有肉眼可见的错位问题import cv2 import numpy as np def check_one(img_path, txt_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r, encodingutf-8) as f: lines f.readlines() for line in lines: parts line.strip().split() cls_id int(parts[0]) pts np.array(parts[1:], dtypenp.float32).reshape(-1, 2) pts[:, 0] * w pts[:, 1] * h pts pts.astype(np.int32) cv2.polylines(img, [pts], True, (0, 0, 255), 2) cv2.putText(img, class_names[cls_id], (pts[0][0], pts[0][1] - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(check, img) cv2.waitKey(0)关键一行就是pts[:, 0] * w把归一化坐标还原成像素坐标。如果多边形和原图上的目标边缘完全贴合说明转换正确如果偏到一边说明归一化基准或者类别id写错了。6.2 训练前的检查清单我现在每次转完数据集都会按这套清单走一遍全程不到十分钟先确认train和val的txt数量等于图片数量再随机抽10张图跑quick_check然后确认data.yaml里names的顺序和classes.txt一致最后看一眼txt里的坐标值是否都在0到1之间。这套流程看着简单但能拦住九成以上的低级错误。可视化检查有效的前提是class_names传的是正确的类别列表。你可以手动传入也可以从训练时用的data.yaml里解析出来我一般直接读classes.txt保证和转换脚本写出的id顺序一致。从那以后我每批新转换的数据集在丢进ultralytics之前都强制走一遍叠加可视化花五分钟检查省下来的是几小时的无效训练。这一招从labelme转到YOLOv8的场景里救过我太多次了希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑