十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

COCO/VOC/LabelMe转YOLO格式:标注转换与数据检查全攻略

COCO/VOC/LabelMe转YOLO格式:标注转换与数据检查全攻略 1. 先搞清楚三种格式到底差在哪标注格式的底层逻辑很多刚开始接触 YOLO 训练的朋友最容易犯的一个错误就是一上来就拿起标注工具干活标完了之后对着json、xml文件发懵这玩意儿怎么变成 YOLO 能用的数据要搞清楚这个问题得分清楚一个最基本的事实——YOLO 训练真正需要的不是某一种标注格式而是txt文件 归一化坐标。这个绕不开的前提是后面所有转换工作的起点。1.1 COCO一套严谨的 JSON 标准COCO 数据集格式是目前学术界和工业界用得最广泛的标注格式之一它的底层是一个庞大的 JSON 文件里面包含了三个核心字段images、annotations、categories。images里存放的是每张图片的 id、文件名、宽度、高度等信息annotations里是每个目标的详细标注记录比如属于哪张图image_id、属于哪个类别category_id、检测框位置bbox、以及是否被截断iscrowdcategories则是类名和类 id 的映射表。需要特别注意的是COCO 的bbox字段格式是[x, y, width, height]这里的 x 和 y 是边界框左上角的像素坐标而不是中心点。很多人第一次转格式就在这里翻车——直接把 COCO 的 bbox 套到 YOLO 的坐标体系里训练出来的模型预测框偏得离谱。COCO 的多边形分割信息segmentation也是一个关键点。当我们在做目标检测任务时模型只需要边界框但如果只是单纯地把segmentation转换成外接矩形有时候会丢失一些精度。这个后面在 LabelMe 转换的部分还会细说。1.2 VOCXML 文件承载的经典方案VOC 格式Pascal VOC是另一个非常经典的标注格式它不像 COCO 那样用一个巨大的 JSON 文件搞定所有东西而是每张图片对应一个 XML 文件。这里面最核心的字段是size图片宽高和通道数和object每个目标的名字和边界框bndbox。VOC 的bndbox给出的是xmin、ymin、xmax、ymax也是像素坐标但是表达方式更直观——直接就是左上角和右下角两个点。这种格式对人力检查很友好打开 XML 一看就知道标注框落在哪里所以在很多老牌标注工具里依然是标配。VOC 转 YOLO 其实是非常机械的操作因为字段信息对应关系非常清晰。但你需要注意一个坑VOC 支持一张图中存在多个object但某些老旧工具导出的 XML 里同一个目标的多个框会被合并成一个或者是bndbox和polygon同时存在解析时容易取错字段。1.3 LabelMe散装 JSON 与图像同名的约定LabelMe 是很多人首选的标注工具因为它部署简单、界面友好一个labelme命令就能启动。LabelMe 导出的是一个和图片同名的.json文件里面包含imagePath图片名、imageWidth、imageHeight、shapes数组等。shapes里的每个元素有label类别名、points坐标点数组和shape_type标注类型。LabelMe 的一个潜在问题在于它有两套坐标约定。老的版本在保存时是像素坐标但最新版本特别是 5.x 以后的默认配置中如果标注的图片在保存时被压缩points里可能是归一化坐标。也就是说x 和 y 的范围是 0 到 1直接拿这些点做像素换算的话你会得到一堆飞到图片外面的框。转换前必须先确认这一点。1.4 YOLO 训练真正要的不是原格式而是 txt 归一化坐标无论你从 COCO、VOC 还是 LabelMe 哪条路过来最终都要落到 YOLO 自己的格式上。这个格式非常简单一张图片对应一个.txt文件文件名和图片名完全一致后缀不同每一行描述一个目标格式是class_id x_center y_center width height这里的x_center、y_center、width、height全部经过归一化到 0~1 之间计算公式是x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height为什么要用归一化坐标因为 YOLO 在训练时会对输入图片做 resize如果坐标是像素值一旦换分辨率就全乱了。归一化后不管原始图片是 640x640 还是 1920x1080坐标都是相对值模型在不同的输入尺度下都能正确映射。提示YOLO 格式的class_id是从 0 开始的整数不是字符串也不是 1 开始的编号。转换时类别映射表比如{cat: 0, dog: 1}一定要单独保存下来不然后面推理的时候你根本不知道模型输出的0到底代表什么。三种格式和 YOLO 格式的对比我整理在下面这张表里每次转换前看一眼可以少踩很多坑格式存储方式bbox 表达方式坐标性质与 YOLO 的关系COCO单个大 JSON 文件[x, y, width, height]像素坐标需转换构建 id 映射表VOC每图一个 XML[xmin, ymin, xmax, ymax]像素坐标需转换直接对应LabelMe每图一个 JSON点数组多形状像素或归一化需先取外接矩形再转换YOLO每图一个 txt[x_center, y_center, w, h]0~1 归一化训练直接使用的格式2. 三个方向的转换我踩过的那些坑格式本身不复杂复杂的是转换过程中那些细枝末节的坑。这一章我把三个方向的转换代码都贴出来同时把我在实测中踩过的坑一并讲清楚。所有脚本都是 Python 写的依赖os、json、xml.etree.ElementTree、cv2这些常用库。2.1 VOC XML 转 YOLO txt核心还是坐标变换VOC 转 YOLO 是我觉得最顺的一条路因为 XML 里所有信息都直白地摆在那里。核心代码如下import os import xml.etree.ElementTree as ET def voc_xml_to_yolo_txt(xml_path, output_dir, class_dict): tree ET.parse(xml_path) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) yolo_lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_dict: continue class_id class_dict[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(output_dir, txt_name), w) as f: f.write(\n.join(yolo_lines))这段代码里面有几个容易被忽略的点第一width和height只能从size字段里取不能自己去读图片。因为标注时的图片尺寸和当前磁盘上的图片尺寸可能不一致一旦标注软件做过缩放你直接读图片算出的宽高就和 XML 里记录的对不上归一化坐标直接偏差。第二class_dict的构建要非常小心。我自己习惯在项目根目录放一个classes.txt一行一个类别名顺序就是类别 id 的顺序。这样的好处是后续不管是转换、训练还是推理用的都是同一个映射表不会出现训练时猫是0推理时猫变成1这种莫名其妙的错位。第三坐标值建议保留 6 位小数。太多不必要的精度没有意义文件还会变大太少的话框的边界会产生像素级的偏移对于小目标场景影响会被放大。2.2 COCO JSON 转 YOLO txt注意 categories 的顺序问题COCO 转 YOLO 相比 VOC 要绕一些因为 COCO 是一个大 JSON你需要先建立image_id到file_name的映射。但在这一步之前必须先解决categories的顺序问题——COCO 的类别 id 一般不是从 0 开始的连续整数而且顺序是按字母排序还是按标注顺序直接决定转换后的输出。import json import os def coco_json_to_yolo_txt(json_path, output_dir): with open(json_path, r) as f: coco_data json.load(f) categories sorted(coco_data[categories], keylambda x: x[id]) class_dict {cat[id]: idx for idx, cat in enumerate(categories)} img_id_to_info {img[id]: img for img in coco_data[images]} ann_by_img {} for ann in coco_data[annotations]: if ann.get(iscrowd, 0) 1: continue img_id ann[image_id] if img_id not in ann_by_img: ann_by_img[img_id] [] ann_by_img[img_id].append(ann) for img_id, anns in ann_by_img.items(): img_info img_id_to_info[img_id] img_width img_info[width] img_height img_info[height] yolo_lines [] for ann in anns: category_id ann[category_id] if category_id not in class_dict: continue class_id class_dict[category_id] bbox ann[bbox] x, y, w, h bbox x_center (x w / 2.0) / img_width y_center (y h / 2.0) / img_height width w / img_width height h / img_height yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) base_name os.path.splitext(os.path.basename(img_info[file_name]))[0] txt_path os.path.join(output_dir, base_name .txt) with open(txt_path, w) as f: f.write(\n.join(yolo_lines))这段代码里我做了几个防御性处理iscrowd这个字段一定要跳过。COCO 官方数据里iscrowd1的目标是密集人群或物体堆叠场景根本无法给出清晰的边界框硬转过来只会给训练引入大量噪声。categories排序时用的是sorted(..., keylambda x: x[id])不是按name排序。这样能保证在不同版本的 COCO 子集中同一个类别的数字 id 相对稳定。如果你从不同的 COCO 子集拼接数据这一步尤其重要。关于 bbox 为负数或宽高为零的异常标注我在第三节检查项里会专门写过滤代码这里先不展开。2.3 LabelMe JSON 转 YOLO txtshape_type 不只是 polygonLabelMe 转换是我见过翻车率最高的转换因为很多人把 LabelMe 和 COCO 的 JSON 结构混淆了以为points就是 bbox 的四个角点。实际上LabelMe 的shapes里points是一个数组根据shape_type不同含义完全不同当shape_type是rectangle时points有两个元素分别是左上角和右下角的坐标。当shape_type是polygon时points是任意数量多边形的顶点坐标需要自己求外接矩形。当shape_type是circle时points只有两个元素圆心和圆上一点。import json import os def labelme_json_to_yolo_txt(json_path, output_dir, class_dict): with open(json_path, r) as f: labelme_data json.load(f) img_width labelme_data[imageWidth] img_height labelme_data[imageHeight] yolo_lines [] for shape in labelme_data[shapes]: label shape[label] if label not in class_dict: continue class_id class_dict[label] shape_type shape.get(shape_type, polygon) points shape[points] if shape_type rectangle: xmin min(points[0][0], points[1][0]) ymin min(points[0][1], points[1][1]) xmax max(points[0][0], points[1][0]) ymax max(points[0][1], points[1][1]) elif shape_type polygon: xs [p[0] for p in points] ys [p[1] for p in points] xmin, xmax min(xs), max(xs) ymin, ymax min(ys), max(ys) elif shape_type circle: center_x, center_y points[0] radius ((points[1][0] - center_x) ** 2 (points[1][1] - center_y) ** 2) ** 0.5 xmin, ymin center_x - radius, center_y - radius xmax, ymax center_x radius, center_y radius else: continue x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) txt_name os.path.splitext(os.path.basename(labelme_data[imagePath]))[0] .txt with open(os.path.join(output_dir, txt_name), w) as f: f.write(\n.join(yolo_lines))在这里有几个必须说清楚的坑多边形转矩形会引入背景噪声。如果你标注的是一个细长物体比如电线、枝条外接矩形会把大量背景包进来。在目标检测任务里这通常是可以接受的因为模型本身就是在学习框住物体而不是精确分割物体。但对于密集小目标场景多边形外接矩形之间的重叠会非常多此时我更建议直接用 LabelMe 标注矩形或者是标注完多边形后在训练前做一次 NMS 合并。检查 points 是否经过归一化。LabelMe 新版本在打开图片时如果做了缩放预览某些情况下导出的 points 是归一化的。最简单的判断方法是看imageWidth和imageHeight的值然后比对 points 中坐标的数值范围。如果 points 最大值小于等于 1而 imageWidth 是几千那一定被归一化了需要先乘以对应尺寸。3. 训练前那套不得不做的检查项转换完格式之后很多人直接就开训了然后训练到一半发现 loss 曲线异常、mAP 几乎为零回头排查才发现是数据本身就有问题。这类问题通常不是格式问题而是标注质量、文件配对、类别分布的问题。所以训练前我强烈建议跑一遍下面这套检查脚本成本极低但能省下好几天排查时间。3.1 标注框合法性检查从坐标越界到零宽高标注框的合法性检查看起来很简单但实际执行时你会发现标注工具偶尔会导出一些非常离谱的框——坐标是负数、宽高为零、甚至坐标全都在图片外面。这些脏数据如果不清理轻则影响 loss 收敛重则导致训练过程中直接报错。import os def check_yolo_labels(labels_dir, image_dir): issues [] for txt_name in os.listdir(labels_dir): if not txt_name.endswith(.txt): continue txt_path os.path.join(labels_dir, txt_name) img_name os.path.splitext(txt_name)[0] .jpg if not os.path.exists(os.path.join(image_dir, img_name)): issues.append(fMissing image for label: {txt_name}) with open(txt_path, r) as f: lines f.readlines() if not lines: issues.append(fEmpty label file: {txt_name}) continue for idx, line in enumerate(lines): parts line.strip().split() if len(parts) ! 5: issues.append(fBad line {idx 1} in {txt_name}: {line.strip()}) continue cls_id, x_center, y_center, w, h parts cls_id, x_center, y_center, w, h int(cls_id), float(x_center), float(y_center), float(w), float(h) if cls_id 0: issues.append(fNegative class id in {txt_name}: {cls_id}) if not (0 x_center 1 and 0 y_center 1): issues.append(fCenter out of range in {txt_name}: ({x_center}, {y_center})) if w 0 or h 0: issues.append(fZero/negative size in {txt_name}: w{w}, h{h}) if w 1 or h 1: issues.append(fBBox larger than image in {txt_name}: w{w}, h{h}) return issues这段脚本主要做五件事检查 txt 文件是否有对应图片、检查是否为空文件、检查每一行格式是否标准、检查坐标是否在 0~1 范围内、检查宽高是否大于 0 且不超过 1。其中w 1或h 1是一个很容易被忽略的异常。当某个标注框的宽度超过图片宽度时说明转换逻辑算错了——可能是把图片宽高写反了也可能是拿了一个临时的 class id 做了错误替换。这种数据一旦进入训练会造成非常大的梯度波动。3.2 图片与标签配对检查两类问题必须分开看配对问题分两类一类是label 存在但图片缺失另一类是图片存在但 label 缺失。第一类问题往往出现在你手动复制了某个文件夹时只拷了labels忘了拷images或者在数据清洗时误删了图片文件。第二类问题则更隐蔽——某些图片因为标注时质量问题被标注工具跳过了或者是在处理时格式转换失败。如果你直接拿这样的数据集去训练YOLO 默认会跳过没有 label 的图片但这会导致你实际训练的图片数比你预期的少很多尤其是当你用验证集指标评估时看到的 mAP 可能虚高。一个最简单的检查方式就是对比两个文件夹的文件名集合def compare_images_and_labels(image_dir, labels_dir): image_files set(os.path.splitext(f)[0] for f in os.listdir(image_dir) if f.endswith((.jpg, .jpeg, .png))) label_files set(os.path.splitext(f)[0] for f in os.listdir(labels_dir) if f.endswith(.txt)) no_label image_files - label_files no_image label_files - image_files print(fImages without labels: {len(no_label)}) print(fLabels without images: {len(no_image)}) if no_label: print(Examples:, list(no_label)[:5]) if no_image: print(Examples:, list(no_image)[:5])从我的实际经验来看超过 2000 张图片的数据集里出现几个no_label的图片非常正常——特别是你用了多个标注工具协作时某个人标注完忘记保存、或者导出时漏了文件都很常见。而no_image则多半是图片被去重脚本处理掉了但 label 还在。3.3 类别分布统计小类别问题在训练中会被无情放大最后一项检查是类别分布统计。很多人在整理 YOLO 数据集时只关注了总图片数没有关注类别分布然后训练出来模型对某些类别完全无感——因为它太稀少了。from collections import Counter import os def analyze_class_distribution(labels_dir, class_names): counter Counter() for txt_name in os.listdir(labels_dir): if not txt_name.endswith(.txt): continue with open(os.path.join(labels_dir, txt_name), r) as f: for line in f: cls_id int(line.strip().split()[0]) counter[cls_id] 1 for cls_id in range(len(class_names)): count counter.get(cls_id, 0) print(fClass {cls_id} ({class_names[cls_id]}): {count})类别极端不均衡时我的处理策略分几个等级如果某类别样本量少于总样本量的 1%我会考虑先加训练轮次或者做重采样如果少于 0.1%基本可以判断这个类别的检测效果不会好与其硬训练不如先补充数据。重采样不是简单地复制图片——可以把包含该类别的小图片做裁剪增强或者用马赛克增强的方式增加目标出现次数这些在后期调优时很有用。3.4 损坏图片检测不要只依赖标注工具的预览最后一个容易被忽略的检查是图片文件本身是否损坏。标注工具里能打开不代表训练时一定能读取因为标注工具的图片加载库往往比较宽容而 PyTorch 的Image.open()稍微遇到截断的 JPEG 就会报错。from PIL import Image import os def check_images_valid(image_dir): bad_images [] for img_name in os.listdir(image_dir): if not img_name.endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(image_dir, img_name) try: with Image.open(img_path) as img: img.verify() except Exception as e: bad_images.append((img_name, str(e))) return bad_images注意这里我用的是verify()而不是load()因为verify()只检查文件头信息。如果一张图片头部信息完好但数据区域损坏verify()可能检测不出来。更彻底的做法是在verify()之后重新打开一次并load()整个图片数组。虽然会慢一些但对于要进入训练的数据这个耗时是值得的。4. 数据划分不是随便 split目录结构、随机种子与防止泄漏数据划分看着是个简单活——train_test_split调一行代码就完事。但在实际项目中划分方式直接决定了模型评估的可信度。我在第三章里提到过如果验证集里恰好没有某个类别你会得到一个虚高的 mAP反过来如果训练集和验证集里有相近图片你又会得到一个虚高的 mAP。所以划分这一步不能偷懒。4.1 一份能让 YOLO 直接开训的目录结构YOLO 系列特别是 YOLOv5/YOLOv8对数据集目录结构有默认约定虽然不是强制但遵循这个约定会让训练命令简洁很多。推荐结构如下dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml这里有几个关键点images和labels两个目录的划分必须完全一致——即images/train下的每张图片在labels/train下都有同名 txt 文件images/val和labels/val同样如此。test目录不是必须的但如果你要发布模型我建议还是留一份独立的 test 集做最终无偏评估。data.yaml是 YOLO 训练时的数据配置文件内容大致如下path: /path/to/dataset train: images/train val: images/val test: images/test names: 0: person 1: car 2: bicycle注意names列表的顺序必须和你的class_dict转换顺序完全一致。我之前见过有人转换时用字典序训练时用另一个顺序导致类别标签全部错位模型训练得再好也是废的。4.2 划分脚本的细节同源图片不重叠写划分脚本最容易犯的错误是直接用random.shuffle后按比例截断然后复制文件。这样做本身没问题但当你多次执行脚本后每次划分结果都不一样一旦哪天你忘了保存随机种子之前实验的可复现性就没了。我建议用下面这种写法import os import random import shutil def split_dataset(image_dir, output_dir, val_ratio0.1, test_ratio0.1, seed42): random.seed(seed) images [f for f in os.listdir(image_dir) if f.endswith((.jpg, .jpeg, .png))] random.shuffle(images) total len(images) val_count int(total * val_ratio) test_count int(total * test_ratio) val_images set(images[:val_count]) test_images set(images[val_count:val_count test_count]) train_images set(images[val_count test_count:]) for split_name, split_images in [ (train, train_images), (val, val_images), (test, test_images), ]: os.makedirs(os.path.join(output_dir, images, split_name), exist_okTrue) os.makedirs(os.path.join(output_dir, labels, split_name), exist_okTrue) for img_name in split_images: img_src os.path.join(image_dir, img_name) img_dst os.path.join(output_dir, images, split_name, img_name) shutil.copy2(img_src, img_dst) label_name os.path.splitext(img_name)[0] .txt label_src os.path.join(labels_dir, label_name) if os.path.exists(label_src): shutil.copy2(label_src, os.path.join(output_dir, labels, split_name, label_name))这段代码里有几个细节我特别想强调随机种子一定要固定。你可以把 seed 设计成命令行参数后续做对照实验时只要 seed 不变划分结果就是唯一的。使用set做交集检查。划分完三个集合后最好加一行断言assert len(val_images train_images) 0 assert len(val_images test_images) 0 assert len(train_images test_images) 0这样可以防止代码逻辑改动后出现交叉污染。注意连续帧 / 相似场景的按组划分问题。如果你在做视频抽帧数据集连续帧的相似度极高直接随机划分会导致训练集和验证集出现几乎一模一样的图片模型在验证集上的表现会变得异常的好但在真实场景里一测就露馅。这种情况下正确的做法是按视频片段分组把同一个视频的所有帧全部放进同一个集合。4.3 平衡划分的思路别让某个类别只在训练集里闪现平衡划分的目标是保证每个集合中各类别的大致比例接近全集。实现方法不复杂——按类别做分层采样。from collections import defaultdict def stratified_split_by_label(labels_dir, val_ratio0.1, seed42): random.seed(seed) label_to_images defaultdict(list) for txt_name in os.listdir(labels_dir): if not txt_name.endswith(.txt): continue with open(os.path.join(labels_dir, txt_name), r) as f: classes set(int(line.split()[0]) for line in f) for cls_id in classes: label_to_images[cls_id].append(txt_name) val_images set() train_images set() for cls_id, imgs in label_to_images.items(): random.shuffle(imgs) val_count int(len(imgs) * val_ratio) val_images.update(imgs[:val_count]) train_images.update(imgs[val_count:]) return train_images, val_images这种按类别比例划分的做法能最大程度避免小类别只在训练集或只在验证集出现。但要注意一张图片可能同时包含多个类别所以在划分时用set去重是关键。如果一个图片同时是 A 类别的验证集和 B 类别的训练集需要你定义优先级——通常情况下应该把这张图归入验证集因为在训练时见过这张图会让验证指标失真。5. 一些我觉得值得拿出来的经验5.1 中文路径和文件名的问题这个真的不是玄学。虽然现代深度学习框架大部分都支持 Unicode 路径但在实际部署时尤其是你在 Docker 容器里训练或者在 Windows 和 Linux 之间切换目录中文路径很容易触发编码问题。更麻烦的是有些图像处理库特别是 C 底层实现在读取中文路径时会直接失败。我的建议是从一开始就用纯英文命名所有图片和文件夹。图片名建议采用日期_序号.jpg或场景_序号.png这种不带空格的格式。文件名中的空格也是个隐形坑——有些工具会把空格当作分隔符导致数据读取时路径被截断。标注完成后如果发现文件名有中文或空格批处理重命名同时同步修改 label 文件名。别只改图片名不改 label 名这个错误我在之前的项目里犯过排查了一整天才发现。5.2 小类别要不要单独处理如果你的数据集里包含大量小目标比如 32x32 像素以下的物体直接训练 YOLO 会非常吃力。YOLO 结构本身对小目标不友好因为低层特征图虽然分辨率高但语义信息不足。在数据整理阶段你可以先统计一下所有标注框的尺寸分布看看小目标占比。做法很简单解析所有 txt 文件的 width 和 height再结合图片实际尺寸算出像素面积。如果小目标占比过高我建议你在训练配置里开启马赛克增强mosaic augmentation这是目前缓解小目标问题最有效的手段之一。如果还不行就得考虑切图策略——把大图切成多个小图分别标注和训练。不过要注意切图会对边界框产生新的挑战如果一个目标恰好横跨两个切图需要目标检测的切图工具自动把框裁剪成两个子框。这块代码比较繁琐一般项目里如果不是特别必要我不会优先推荐切图方案。5.3 标注工具本身也有坑LabelMe 虽然好用但有几个版本问题是没法通过代码绕开的导出路径和图片路径不一致。LabelMe 保存的imagePath有时是绝对路径有时是相对路径取决于你的图片是通过命令行参数传入还是直接拖拽进去。转换脚本里如果直接读取imagePath拼接路径很可能会文件找不到。我的做法是完全不信任imagePath字段直接用 json 文件名作为图片名。同一张图片被多次保存。多人协作标注时LabelMe 默认会对同名图片自动加后缀导致你拿到一堆image_1.json、image_1_copy.json这样的文件。整理时如果不去重训练集和验证集里会出现几乎相同的数据。好在这个问题可以通过计算图片内容的哈希值来去重。LabelMe 的自动保存间隔。如果你用 LabelMe 标注特别多的点软件有时会在保存大文件时卡顿甚至丢失最后几个点的数据。我在处理上千张图的标注任务时一般每标完 50 张图就重启一次标注工具让内存彻底释放减少卡死概率。5.4 关于显卡的一点额外提醒整理完数据集后你会发现训练才是新一轮折腾的开始。前几年总有朋友问我 AMD 显卡能不能跑 YOLO——这个问题现在依然存在。如果你是学习用途CPU 也能跑就是慢如果是实用场景NVIDIA 显卡的 CUDA 生态确实省心太多。入门做实验的话6GB 显存起步够跑 YOLOv8n 和 YOLOv8s再大一点的模型就会比较吃力。不过这个问题和数据整理无关了属于训练环境层面的事。6. 完整流程串联从零到一个能跑起来的 YOLO 数据集前面章节里把格式、检查、划分都拆开讲了这里我给你一个完整的操作顺序照着走就不会乱。6.1 一条可执行的整理链路我一般按照下面这个流程整理数据集每一步都可以用脚本半自动完成拿到原始标注数据不管是 COCO、VOC 还是 LabelMe。统一转成 YOLO 格式的txt文件转换时同时生成并保存classes.txt。跑一遍合法性检查坐标范围、宽高、配对、损坏图片把异常项修掉或剔除。统计类别分布确认没有极端不均衡的类别。按 8:1:1 或 9:1 做分层划分固定随机种子生成images/train、images/val、images/test和对应的labels目录。写好data.yaml确认names顺序和classes.txt完全一致。小批量试跑 5~10 个 epoch先确认训练不报错、loss 在下降。检查训练日志里的类别分布和 loss确认和统计结果一致后再启动完整训练。6.2 一张图表把全过程串起来阶段输入输出关键工具 / 方法容易踩的坑格式转换COCO/VOC/LabelMe 原始标注YOLO txt 文件Python 转换脚本坐标体系搞混、类别 id 错位数据处理原始图片 txt干净图片 干净 txt合法性检查脚本中文路径、损坏图片、空 txt数据划分全量数据train/val/test 目录分层划分脚本随机种子未固定、类别分布不均衡配置确认三个目录 classes.txtdata.yaml手动比对names 顺序和训练配置不一致试跑验证数据集 模型配置训练日志 权重文件YOLO 官方训练命令显存不够、库版本冲突上表中每一步如果遇到问题逆着链路往前查。绝大多数情况下训练异常都是数据问题而不是模型问题。6.3 关于数据集发布规范的一点补充如果你打算把自己整理的数据集分享给别人或者用于论文复现我建议你在发布时附上三样东西README.txt数据来源、标注规范、类别定义、classes.txt类别映射、data.yaml训练配置。这看起来只是附加的小文件但能帮别人省下大量的一一比对时间。我在接手别人的数据集时最怕的就是只给一个 images 文件夹和一个 labels 文件夹没有任何说明——类别含义需要猜、标注边界需要猜、划分方式需要猜这种数据集用起来就是在消耗信任。整理数据集这件事本身没有太多黑科技但每一个细节都可能影响最终模型的性能。我自己在做项目的过程中把上面这些检查和转换脚本做成了一个小工具库每次拿到新数据都先跑一遍已经形成肌肉记忆了。如果你也经常跟 YOLO 数据集打交道建议你尽早把这套流程固化成脚本别每次都手动操作——手动操作多了总会漏掉某个步骤。
返回列表