TinyPerson数据集COCO转YOLO格式完整教程与训练避坑指南 简介该数据集面向目标检测、人群检测及小目标检测方向的开发者与研究者由TinyPerson图像整理而成共包含1532张带标注样本标注格式覆盖VOC xml与YOLO txt两种主流类型其中YOLO txt已划分好训练、验证、测试集可直接用于YOLO系列模型训练省去自行标注和格式转换的环节。压缩包总计2000个文件以1532个txt标签文件为主体另有467个xml标注文件与1个yaml配置文件整体大小78.54MB目录结构清晰便于直接放入YOLO项目使用。当前已有2915人学习下载资源适合正在开展目标检测课程设计、毕业设计或算法对比实验的深度学习学习者与进阶者借助现成标注数据快速验证模型效果并为后续小目标检测优化与调参提供可靠基础。1. 为什么大家都在找“YOLO格式的TinyPerson数据集”做了一段时间目标检测的人尤其是专注小目标识别方向的朋友大概率都听过TinyPerson数据集。这个数据集在远距离行人检测、海上搜救、岸线监控这一类场景里地位很特殊因为是少有的专门针对“极小目标行人”设计的公开数据集。它的原始标注是COCO JSON格式但YOLO系列训练需要的是txt标签而且网上能直接下载到的“YOLO格式成品”质量参差不齐有的标签类别映射是乱的有的坐标没归一化好拿过来直接跑训练出来的模型大概率是废的。这篇文章我就把“把TinyPerson数据集从COCO格式转换成YOLO格式”这件事掰开揉碎讲清楚。内容包括格式转换的核心逻辑、完整脚本、转换后如何自查、以及训练时最容易踩的坑。适合正在折腾YOLOv5/YOLOv8/YOLOv11的检测玩家也适合准备自己做数据集、又不太清楚YOLO标签规范的新手。看完这篇文章你不仅能拿到一份能直接跑的YOLO格式TinyPerson还能理解背后的转换原理以后遇到其他COCO格式数据集比如VisDrone、SODA也能顺手处理。1.1 TinyPerson到底是什么TinyPerson数据集的核心定位一句话概括就是专门为“远距离小目标行人检测”设计的高难度数据集。它采集的大多是海上、岸边、航拍视角下的行人画面背景复杂、目标极小很多目标在整张图里只有几个像素甚至更小人眼都需要仔细凑近了才能分辨。官方公布的数据规模大概在500张图左右标注实例大约1600个。这个体量放在今天的大模型时代不算大但它的难度恰恰是“目标尺寸”这一点。数据集中目标平均尺寸通常在20像素以下有的极端场景连5像素都不到。对于YOLO这类基于锚框或中心点的检测器来说这种目标属于“看不见就真的看不见”的级别特征极度匮乏非常考验数据标注质量和训练策略。标注类别上有两个sea_person海上的行人/类人目标和earth_person陆地/岸边的行人/类人目标。这两个类别名挺有意思它不是简单的“人”分类而是按场景区分在做海上搜救这类任务时这样的细粒度标注反而有用——你可以知道目标是在水中还是岸上这对救援决策很有价值。1.2 “YOLO格式”和COCO格式的差异很多刚接触数据集转换的人容易犯迷糊同样是标注框为什么非要费劲转格式直接用COCO JSON不行吗这里要理解YOLO系列训练脚本的工作方式。YOLO系列v5/v8/v11默认的标签系统是每张图片对应一个同名txt文件txt文件里每一行代表一个目标格式是class_id x_center y_center width height注意这五个值全部是归一化后的浮点数。x_center y_center width height表示的是目标中心点坐标和宽高相对于图片宽高的比例取值范围0到1。而COCO JSON里记录的bbox是绝对像素坐标格式是[x, y, width, height]其中x, y是目标框左上角的像素坐标。从COCO转到YOLO核心操作就是两步把左上角坐标转成中心点坐标然后除以图片宽高做归一化。对比项COCO JSONYOLO TXT存储方式单个JSON文件集中管理每张图一个txt文件坐标含义像素坐标绝对位置归一化坐标比例值框表示左上角 x, y, 宽, 高中心点 x, y, 宽, 高类别表示类别ID对应JSON里的categories从0开始的整数ID可读性人可读但训练脚本解析麻烦机器友好训练时直接读取2. 动手转换前先搞清楚这5个关键点如果你只是随便找个转换脚本跑一下运气好也许能跑通但只要数据稍有变化脚本就最容易翻车。转换这个动作虽然简单背后的5个细节没想清楚后面训练时才会有各种奇怪问题冒出来。2.1 标注坐标系与归一化第一个关键点是坐标系的转换公式。COCO的bbox是[x, y, width, height]左上角坐标加宽高。YOLO需要的是目标中心点的位置和宽高比例。转换公式如下x_center (x width / 2) / image_width y_center (y height / 2) / image_height norm_width width / image_width norm_height height / image_height这里有个容易忽略的细节image_width和image_height必须来自图片的真实尺寸而不是JSON里某个字段。TinyPerson原始JSON里images字段会记录width和height按理说可以直接用但有些第三方整理的版本在标注时对图片做过缩放、裁剪甚至补边JSON里的尺寸可能和实际图片对不上。最稳妥的做法是转换时用cv2.imread或者PIL.Image.open读一遍真实图片尺寸再去做归一化。虽然慢一点但绝对不会错。2.2 类别映射sea_person / earth_personTinyPerson原始JSON里的类别ID我记得raw COCO文件里categories的id不是从0开始的具体可能是1和2也可能有偏移而YOLO训练要求类别ID必须严格从0开始连续编号没有空洞。所以转换时不能直接把COCO的category_id写进txt必须先建立一个映射表categories_map { 1: 0, # sea_person - 0 2: 1 # earth_person - 1 }如果你有特殊需求比如只关心“有没有人”不关心是在海上还是岸上可以把两个类别合成一个person都在txt里写成0。这个选择和你的实际任务强相关没有绝对的对错但决定了模型最后的输出粒度。2.3 训练集/验证集划分策略TinyPerson官方其实提供了划分好的train/val列表但网上流传的版本不一定带这层信息。如果你拿到的JSON里没有划分建议自己做。划分时要注意一定要按图片维度划分而不是按标注实例划分。也就是同一张图的所有目标必须全部进训练集或全部进验证集不能一张图的一半个框在训练集、另外半个框在验证集。后者会导致数据泄漏验证集loss会异常低但实际泛化能力一塌糊涂。推荐比例训练集80%验证集20%。TinyPerson总共500多张图验证集100多张量级合理。3. 一步步把COCO转成YOLO格式说了一堆原理现在上实操。我直接给一份我用过很多次的转换脚本它不光是给TinyPerson用的任何COCO格式的数据集都能用同一个逻辑处理。3.1 完整转换脚本import json import os import random from collections import defaultdict def convert_coco_json_to_yolo( coco_json_path, output_dir, image_dirNone, train_ratio0.8 ): COCO JSON - YOLO txt coco_json_path: 原始COCO标注文件 output_dir: 输出目录会在里面生成 images/train, images/val, labels/train, labels/val image_dir: 图片目录如果不传则默认使用JSON里的file_name字段 train_ratio: 训练集比例 with open(coco_json_path, r, encodingutf-8) as f: coco json.load(f) # 建立COCO类别ID - YOLO类别ID的映射 cat_id_to_yolo_id {} categories sorted(coco[categories], keylambda x: x[id]) for yolo_id, cat in enumerate(categories): cat_id_to_yolo_id[cat[id]] yolo_id print(类别映射:, {cat[name]: cat_id_to_yolo_id[cat[id]] for cat in categories}) # 建立 image_id - 图片信息 的索引 images_info {} for img in coco[images]: images_info[img[id]] img # 建立 image_id - 标注列表 的索引 annotations_by_image defaultdict(list) for ann in coco[annotations]: annotations_by_image[ann[image_id]].append(ann) # 打乱图片顺序后划分train/val确保一张图的所有标注进同一集合 image_ids list(images_info.keys()) random.shuffle(image_ids) split_idx int(len(image_ids) * train_ratio) train_ids set(image_ids[:split_idx]) val_ids set(image_ids[split_idx:]) # 创建目录结构 for split in [train, val]: os.makedirs(os.path.join(output_dir, images, split), exist_okTrue) os.makedirs(os.path.join(output_dir, labels, split), exist_okTrue) train_count, val_count 0, 0 issue_count 0 for img_id, img in images_info.items(): # 判断当前图片属于哪个集合 if img_id in train_ids: split train train_count 1 else: split val val_count 1 # 图片文件名优先用JSON里的file_name没有的话用image_dirid if file_name in img: file_name os.path.basename(img[file_name]) else: file_name f{img[id]:012d}.jpg src_path os.path.join(image_dir, file_name) if image_dir else file_name dst_img_dir os.path.join(output_dir, images, split, file_name) # 优先用真实图片尺寸避免JSON虚标 try: from PIL import Image with Image.open(src_path) as im: img_w, img_h im.size except Exception: img_w, img_h img.get(width), img.get(height) if not img_w or not img_h: print(f[跳过] 无法获取图片尺寸: {file_name}) issue_count 1 continue # 复制图片到目标目录 import shutil if os.path.exists(src_path): shutil.copy2(src_path, dst_img_dir) else: print(f[警告] 图片不存在: {src_path}) label_lines [] for ann in annotations_by_image.get(img_id, []): # 过滤无效标注 if bbox not in ann: continue x, y, w, h ann[bbox] if w 0 or h 0: issue_count 1 continue # 归一化转换 x_center (x w / 2) / img_w y_center (y h / 2) / img_h norm_w w / img_w norm_h h / img_h # 类别ID映射 yolo_id cat_id_to_yolo_id.get(ann[category_id]) if yolo_id is None: print(f[跳过] 未知类别ID: {ann[category_id]}) issue_count 1 continue label_lines.append(f{yolo_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}) # 写入txt标签 txt_path os.path.join(output_dir, labels, split, os.path.splitext(file_name)[0] .txt) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(label_lines)) print(f转换完成: train {train_count} 张, val {val_count} 张, 问题标注 {issue_count} 条) print(f输出目录: {output_dir}) if __name__ __main__: convert_coco_json_to_yolo( coco_json_pathtiny_person_coco.json, output_dirtiny_yolo, image_dirtiny_person_images, train_ratio0.8 )3.2 脚本怎么用先把官方标注文件命名为tiny_person_coco.json图片放在tiny_person_images目录下然后直接运行脚本。跑完会在tiny_yolo下生成完整目录tiny_yolo/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/代码里有一点我刻意加了尝试用PIL读取真实图片尺寸。跑TinyPerson这类数据时图片是原始尺寸还好说但如果有些版本图片被预处理过这一步就是保命用的。宁可多花几秒钟读图也不要为了省事直接信JSON里的尺寸字段。3.3 生成Dataset配置文件转换完目录还要给YOLO训练写一个data.yaml。拿YOLOv8举例在tiny_yolo目录下新建data.yamlpath: ./tiny_yolo train: images/train val: images/val names: 0: sea_person 1: earth_person如果你的类别顺序和我不一样一定先看labels里第一列的数字对应什么名字再改names。这一步错位模型训练完输出全是乱的。4. 转换后的自查与可视化转换脚本跑完不代表万事大吉。我自己第一次转完直接拿去训练结果模型完全学不到东西回头检查才发现小问题一堆。转换完必须做一轮自查这步省不了。4.1 数据分布检查我先说几个自查的指标标注数量分布统计每个txt文件的行数看是否有全部为空的标签文件。TinyPerson里有些极端难例目标实在太小标注员可能漏标但也不能出现大量空标签把训练带偏。空标签文件如果超过总样本的10%建议检查JSON原始文件是不是解析出问题了。目标面积分布YOLO训练时如果目标归一化宽度小于0.01即目标宽度不到图片宽度的1%损失函数里目标框的权重会非常小模型很容易忽略这类目标。记录一下转换后目标宽高的min/median/max做到心里有数。import os import numpy as np label_dir tiny_yolo/labels/train sizes [] empty_count 0 for txt_file in os.listdir(label_dir): with open(os.path.join(label_dir, txt_file), r) as f: lines f.readlines() if len(lines) 0: empty_count 1 for line in lines: parts line.strip().split() if len(parts) 5: w, h float(parts[3]), float(parts[4]) sizes.append((w, h)) sizes np.array(sizes) print(f空标签文件数量: {empty_count}) print(f目标数量: {len(sizes)}) print(f归一化宽度: min{sizes[:,0].min():.4f}, median{np.median(sizes[:,0]):.4f}, max{sizes[:,0].max():.4f}) print(f归一化高度: min{sizes[:,1].min():.4f}, median{np.median(sizes[:,1]):.4f}, max{sizes[:,1].max():.4f})如果发现中位数已经到0.01以下说明这数据集确实够“小目标”训练时要针对性地调整策略。4.2 可视化验证脚本跑完必须抽几张贴框图看看。用OpenCV把归一化坐标换算回像素坐标画在图上肉眼确认框的位置是否贴合目标。import cv2 import os def draw_yolo_boxes(image_path, label_path): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f.readlines(): parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_c, y_c, box_w, box_h map(float, parts[1:]) x1 int((x_c - box_w / 2) * w) y1 int((y_c - box_h / 2) * h) x2 int((x_c box_w / 2) * w) y2 int((y_c box_h / 2) * h) color (0, 255, 0) if cls_id 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 1) return img sample tiny_yolo/images/train/000000.jpg sample_label tiny_yolo/labels/train/000000.txt result draw_yolo_boxes(sample, sample_label) cv2.imwrite(check_visual.jpg, result)画出来的图如果框整体偏移、框大小明显不对多半是归一化时图片尺寸和真实尺寸不一致。如果框偏移只出现在特定图片检查那张图是不是分辨率和其他图不一样或者JSON里的尺寸字段写错了。5. 训练时最常见的坑与排查转换完数据喂给YOLO训练又会遇到新一轮问题。这一节我把训练TinyPerson时最常见的问题和排查思路整理成表格基本都是我实际踩过的。5.1 常见问题速查表现象可能原因排查方式loss直接飞NaN标签里有inf或nan值写脚本扫描txt检查是否出现非数字字符mAP一直在0附近标签类别ID与data.yaml不一致打印一个txt文件看第一列数字核对names一张图只检测出一个框标签文件被覆盖/部分丢失确认图片和txt文件一一对应无重名覆盖训练正常但小目标完全检不出输入分辨率太小或anchors不合适调高输入尺寸如640到1280关闭mosaic验证时框位置整体偏移归一化时用了错误图片尺寸重新读取真实图片尺寸再次转换训练集loss很低验证集mAP为零数据泄漏同一张图被分到train和val检查是否有重叠文件重新划分数据集5.2 类别ID错位的典型案例这个坑我印象太深了。早期转完TinyPerson后没仔细看直接拿YOLOv5默认的COCO预训练权重训练发现模型输出一直在“预测人”但精确定位全错。排查了半天最后发现是转换脚本里把类别映射写反了sea_person对应1earth_person对应0而data.yaml里写的是反过来。模型不是学不会是学了一套类别错位的标签。处理这类问题核心原则就一条转完先抽查5-10个标签文件确认第一列数字对应的类别和图片里实际目标一致再进训练流程。哪怕检查花20分钟也比训练一晚上后发现白跑强。5.3 小目标训练的参数调整建议TinyPerson数据集的特殊性在于目标极小用YOLO默认参数训练效果很一般。我在实验里试过几个参数调整实测有效开启SAHI切片推理训练时用原图或切图训练推理时用SAHI这类切片工具把大图切成若干小图分别检测再合并结果。TinyPerson原图里目标太小整图推理容易漏检切图后小目标的相对尺寸变大检测率提升明显。调高输入分辨率YOLO默认输入640x640但TinyPerson的原始图片分辨率通常在1024x1024甚至更高直接resize到640把目标缩得更小。有几个方案可以考虑训练时用1280x1280输入或者保留原分辨率只在训练时随机裁剪局部区域相当于放大目标。显存不够的话用切图训练更实际。关闭或降低mosaic增强mosaic对常规目标检测很有用但对极小目标可能起反作用——4张图拼在一起小目标被进一步缩小而且拼接边界容易把目标截断导致标注失真。实测下来对TinyPerson这类数据关闭mosaic改用简单的平移、翻转、色彩抖动训练稳定性反而更好。6. 除了转换这个数据集还能怎么用最后顺便聊一下拿到YOLO格式TinyPerson之后除了直接训练还能做点什么。这个数据集的真正价值不只是当训练集它还适合用来做数据增强验证、小目标检测器对比评测以及配合其他数据集做联合训练。TinyPerson最大的问题是数据量不大、场景单一海上为主单独训练出来的模型换到城市街道、园区监控这些场景泛化能力会比较有限。我建议把它和常规行人数据集混合使用比如在通用行人数据集上先预训练再用TinyPerson微调这样能兼顾通用场景和小目标专门场景。另一个思路是类别合并。如果你最终任务只是“检测人”不太关心人在海上还是岸上训练时把sea_person和earth_person合并成单一person类别会更好。类别多意味着每个类别的正样本更少对只有1600个实例的小数据集来说合并类别能让模型学到更鲁棒的行人特征而不是纠结于场景区分。我自己在实际使用中的体会是TinyPerson是个非常适合用来检验小目标检测能力的“压力测试”数据集模型在它上面的表现基本能反映真实小目标场景中的下限水平。如果你做的项目涉及高空监控、无人机视角、远距离行人识别这些方向花一个下午把数据格式搞定后面能少走很多弯路。最后再分享一个小技巧转换脚本和检查脚本别删整理成一个数据预处理的小工具库下次碰到COCO格式的数据集改个路径直接就能复用。本文还有配套的精品资源点击获取