Labelimg标注YOLO数据集:从标注到划分校验的完整实战指南

发布时间:2026/10/2 6:41:56
Labelimg标注YOLO数据集:从标注到划分校验的完整实战指南 经常看到有人拿着同一份数据训练 YOLO出来的效果却是天壤之别。模型结构一样、超参数一样、训练轮数也一样唯一能拉开差距的就是数据。标注标得对不对、格式存得规不规范、训练集验证集分得合不合理这三点直接决定了你是跑出一套能用的检测模型还是白白烧掉几天显卡时间换来一堆“Loss 下降但 mAP 不见涨”的无效结果。这篇文章就围绕一条完整链路来写用 labelimg 给 YOLO 数据集做标注标完以后用工具把数据集自动划分成训练集、验证集和测试集最后再讲怎么做标注质量校验。适合刚开始做目标检测、准备训练自己的 YOLOv5/YOLOv8 模型的读者也适合那些已经被“训练集验证集重叠”“标签类别错乱”折磨过一轮的初学者。文章里没有花哨的东西全部是我自己实际操作过的流程和踩过的坑。1. 为什么选 labelimg 做目标检测标注而不是其他工具1.1 labelimg 和 labelme、CVAT、Roboflow 的定位差异做目标检测的数据标注市面上工具说多不多说少不少。很多人一上来就懵不知道选哪个。我给一个简单的判断标准你标的是矩形框还是多边形如果是做目标检测输出的是 (xmin, ymin, xmax, ymax) 这种矩形框labelimg 就是最省事的那个。它界面简单快捷键顺手不吃显卡老电脑也能跑单机就能干活数据和标注文件都在本地不用上传任何服务器。labelme 更擅长做实例分割和语义分割的标注虽然也能出矩形框但它的强项是画多边形用来标 YOLO 的检测框有点大材小用操作反而繁琐。CVAT 是网页版工具功能确实强支持多人协作、自动标注、跟踪标注但部署和维护成本高。个人做项目、小团队搞数据集为了标注去部署一套 CVAT 有点杀鸡用牛刀。Roboflow 是平台型工具能标注、能增强、能在线管理数据集但它强依赖云端数据要传上去。如果你的项目数据有保密要求或者网络条件不稳定本地工具永远是更稳妥的选择。所以结论很明确本地单机做检测框标注labelimg 是性价比最高的方案。它能直接导出 YOLO 格式的 txt 标注文件省掉格式转换这一道工序简洁高效。1.2 labelimg 的两个主要优势轻量起步 原生 YOLO 格式labelimg 最大的优点就是“轻”。它不是那种什么都能干但什么都重的平台工具它只解决一件事在图片上画框然后把框的坐标按你选定的格式存下来。第二个优点是它原生支持 YOLO 格式。打开 labelimg 时选择 YOLO 格式模式它会在保存目录自动生成一个 classes.txt同时把每个目标的标注写成一个与图片同名的 txt 文件内容就是归一化后的 class_id 和坐标信息。这样标完之后数据基本可以直接扔进 YOLO 训练流程里中间不用再写转换脚本。另外一个隐蔽的优点是它对中文字段的容错性。当然我这里不是推荐你用中文恰恰相反我建议图片名、文件夹路径全部用英文。但你如果接手过别人用其他工具标的数据里面夹着中文名图片很多工具会直接崩溃labelimg 相对抗造一些。2. 环境准备和目录规划动手前先把地基打好2.1 安装方式与版本说明labelimg 的安装方式有三种我按推荐程度排一下。第一种pip 直接装pip install labelimg装完在终端输入labelimg就能启动。这是最简单的方案适合大多数人。命令行窗口不要关关掉就等于把标注工具关掉了这是个非常容易踩的低级坑我在群里见人问过不止一次。第二种从源码启动git clone https://github.com/HumanSignal/labelImg.git cd labelImg pyrcc5 -o libs/resources.py resources.qrc python labelImg.py这种方式适合需要改代码、二次开发的人。比如你想加一个自动保存开关、想改界面字体源码版更方便。但对普通标注任务没必要走这条路。第三种Windows 上的 release 版本直接下载 exe 用。这个方案的问题在于版本老旧界面和功能细节跟最新代码有差异而且下载 source 经常因为网络问题失败。我不建议你用。安装过程中最常见的坑就是 pyqt5 依赖问题。报错信息通常是ModuleNotFoundError: No module named PyQt5解决办法很简单装一下就行pip install pyqt5另一个坑是 macOS 上首次打开提示“已损坏”或无权限到系统设置里面允许这个应用运行即可系统版本不同位置不同这里不展开。2.2 标注前的目录结构设计千万别拿到图片就开始标。我以前吃过这个亏图片散落在好几个文件夹标注文件默认跟图片放一起最后整理数据花了整整一下午。正确做法是开工前就把目录结构定下来。我的标准结构长这样dataset/ ├── images/ # 原始图片全部放这里 ├── labels/ # 标注输出的 txt/xml标注时生成的 ├── classes.txt # 类别清单由 labelimg 自动生成 └── backup/ # 手动备份目录可选等标注完成、划分数据集之后再扩展成 YOLO 训练用的标准结构dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt └── data.yaml注意一个细节标注阶段images和labels路径的对应关系决定了后面划分脚本写起来是简单还是痛苦。我建议图片和标注文件名保持严格同名图片叫frame_0001.jpg标注就叫frame_0001.txt这是 YOLO 生态的基本约定从标注第一天就养成这个习惯后面会省很多事。图片命名上不建议用中文、空格、特殊符号。用type_date_seq这种带描述性的格式也可以或者干脆用纯数字流水号比如000001.jpg。不要用“第1张.jpg”这种名字轻则脚本报编码错误重则训练时读不到文件。3. labelimg 实战标注格式选择、快捷操作、疑难样本3.1 PascalVOC 还是 YOLO 格式别选错启动 labelimg 后界面左侧有一排按钮。在标注之前先点击左侧的 “PascalVOC” 按钮它会切换成 “YOLO” 格式。这个操作建议放在第一步因为两种格式的存储逻辑完全不同PascalVOC 格式生成 XML 文件记录的是像素坐标 (xmin, ymin, xmax, ymax)。YOLO 格式生成 txt 文件记录的是归一化后的中心点坐标和宽高 (class_id, x_center, y_center, width, height)。如果你选 PascalVOC 标了一堆图后面转 YOLO 格式还得写脚本转换。虽然转换不复杂但多一步就多一个出错的机会。切换到 YOLO 格式后下次你设置保存目录时labelimg 会在目录下生成一个classes.txt。注意这个文件里的类别顺序就是你标注时写入 txt 的类别编号顺序错一个空格后面训练全乱。3.2 标注流程与高频快捷键打开图片目录Open Dir和保存目录Change Save Dir都设置好后就可以开标了。先记住这几个快捷键效率至少提升三倍操作快捷键说明画框W按下后拖拽鼠标画矩形框上一张/下一张A / D切换图片保存Ctrl S保存当前图片的标注删除标注框Del删除选中框放大/缩小Ctrl 加号 / Ctrl 减号放大后精细调整边界平移视图空格 拖拽放大图片后在画面内移动标注确认Ctrl 回车确认当前选中框视版本而定我的操作习惯是右手负责画框和调整左手负责切换图片和保存。整个流程就是“画框 - 填类别 - CtrlS - D翻页”。不要标一张存一张这样太碎但也不要攒 100 张才存一次中间软件崩了眼泪都流不出来。我一般每标完 10 张就 CtrlS 一次。画框的时候还有一个细节labelimg 的框可以手动微调。画完一个框之后框的四个角和四条边都有拖拽点放大图片把边界贴住目标的边缘。目标检测的标注框讲究“紧贴目标轮廓”不要预留额外的空白边不然模型训练时会把背景也学进去导致检测框偏大。3.3 遮挡目标、小目标、多目标的标注经验实际项目里最头疼的就是遮挡目标。比如你要检测路上行走的行人一个人被电线杆挡了一半这个框怎么画我的经验是只要目标的可见部分还能让你判断出它是什么类别的就按完整目标的大致范围画框让模型学习被遮挡状态下的目标特征。如果目标遮挡太严重比如一辆车只剩一个轮子露在外面就看你的业务需求了。如果遮挡目标也需要在推理时被检出那就标如果不需要宁可不标。这里的原则是“标注策略和推理场景要一致”这是很多人忽略的点。小目标的情况更典型。一张高清大图上远处几十个小目标一个个去画框很费眼。先把图片放大到 200%再在图片区域内平移着找。这样虽然慢但标注质量高。别嫌麻烦小目标本来就难检测标注再不准训练出来的模型基本没救。多目标重叠的图比如一张桌子上摆着好几个相同的杯子每个杯子都要单独画框不要只标一个就跳过。很多人偷懒只标部分目标训练时模型就会“学会”只检部分目标最终 mAP 虚高但实际漏检严重。4. 格式细节与类别一致性最容易出错的“隐含规则”4.1 YOLO 标注 txt 到底存了什么东西很多人盯着 txt 文件看半天看不懂。拿一张 1920x1080 的图举例标注一个目标后txt 里会有一行类似这样的数据2 0.5125 0.4444 0.2375 0.1852这五个数的含义分别是第 1 个数2类别 id对应 classes.txt 里第三种类别从 0 开始数第 2 个数0.5125目标的归一化中心点 x 坐标第 3 个数0.4444目标的归一化中心点 y 坐标第 4 个数0.2375目标归一化宽度第 5 个数0.1852目标归一化高度所谓归一化就是拿像素值除以图片宽度或高度x_center_norm ((xmin xmax) / 2) / image_width y_center_norm ((ymin ymax) / 2) / image_height width_norm (xmax - xmin) / image_width height_norm (ymax - ymin) / image_height为什么要归一化因为 YOLO 要处理不同分辨率的输入图像归一化之后同一目标在不同尺寸图上产生的坐标表示是一致的。训练时模型统一缩放到同一分辨率归一化坐标天然兼容这种做法不需要在数据加载阶段再做坐标变换。一个容易出错的点是labelimg 在 YOLO 模式下写入的坐标是相对原图尺寸做归一化。如果标注完成后你用脚本把图片改成别的尺寸而没有同步重新计算 txt 坐标那这一整套标注就废了。所以养成一个习惯做完任何图片尺寸变换必须同步做坐标重算。4.2 classes.txt 的顺序为什么可能毁掉一次训练这是标注阶段最隐蔽的坑。labelimg 在 YOLO 模式下每次创建新的标注保存目录时会生成一个空的classes.txt。你第一次画框填写类别名时它会把类别名写进去。关键问题来了如果你在 A 电脑上标注时先用的是“person”后是“car”classes.txt 内容是person car那么 person 的 id 就是 0car 的 id 就是 1。但如果换到 B 电脑你的标注目录被重建先填了“car”后填了“person”classes.txt 变成car person那同一个 txt 文件里的 0 就不再是 person而是 car。模型不会知道你曾经约定过什么它只知道 id 对表classes.txt的次序读取类别名。很多人训练时发现预测结果里“狗”识别成了“猫”先检查的就是这里。我的做法是团队里统一提供一个写死的 classes.txt标注前拷贝到保存目录整个项目期间谁都不许改。每个类别一行顺序一旦确定永久不变。如果中间要加新类别只能追加到末尾不能插到中间。这算是我被坑了两次之后才彻底改掉的陋习。另外一个相关细节是 classes.txt 内容必须和后续训练用的 data.yaml 中的类别列表保持一致。YOLOv5 和 YOLOv8 的 data.yaml 格式长这样train: dataset/images/train val: dataset/images/val nc: 2 names: [person, car]这里names的顺序必须逐位对应 classes.txt。names[0]对应代码里的 id 0names[1]对应 id 1。如果 data.yaml 里把 car 放前头、person 放后头那训练出来的模型预测出来的人会被认为是车。这类错误非常隐蔽报错不会出现但结果的每个框都错位。5. 自动划分数据集脚本把一份数据拆成三份的正确姿势5.1 为什么不能靠手动划分有些人标注完以后在文件夹里手动拖拽图片把一部分当训练集、一部分当验证集。短时间数据少的时候可能感觉没毛病但一旦数据量几百上千张手动划分的问题就全暴露了。第一手动划分无法保证随机性。你可能下意识地全部把前 80% 的图片划入训练集而前 80% 的图片恰好是同一个场景、同一光照条件拍出来的结果训练集和验证集的分布差距巨大训练出来的模型泛化能力拉胯。第二手动划分很容易产生重复。很多项目的图片序列是有相关性的比如视频抽帧出来的图frame_0001.jpg 和 frame_0002.jpg 几乎一模一样。如果你靠肉眼手动挑很容易把高度相似的图片同时放进训练集和验证集造成验证集“泄漏”mAP 看着好看实际一上真实场景就露馅。第三无法复现。训练跑完发现效果不错想记录一下当时用了哪些图片做验证手动划分根本还原不了。所以正确做法是用脚本加固定随机种子做自动划分。固定随机种子这一点极其重要它保证了每次运行脚本划分结果一致实验可复现。5.2 可复现的随机划分脚本下面这个脚本是我在实际项目里的常用版本支持按比例划分、自动检测标签缺失和空标签并且保证图片和标注文件一一对应。import os import random import shutil from collections import Counter # 配置区域 IMAGE_DIR dataset/images # 原始图片目录 LABEL_DIR dataset/labels # 原始标注 txt 目录 OUTPUT_DIR dataset_split # 输出根目录 TRAIN_RATIO 0.8 # 训练集比例 VAL_RATIO 0.1 # 验证集比例剩余为测试集 RANDOM_SEED 42 # 固定随机种子保证可复现 EXTENSIONS {.jpg, .jpeg, .png, .bmp} # random.seed(RANDOM_SEED) def find_images(image_dir): images [] for root, _, files in os.walk(image_dir): for f in files: ext os.path.splitext(f)[1].lower() if ext in EXTENSIONS: images.append(os.path.join(root, f)) images.sort() return images def ensure_dirs(base): for split in [train, val, test]: os.makedirs(os.path.join(base, images, split), exist_okTrue) os.makedirs(os.path.join(base, labels, split), exist_okTrue) def main(): images find_images(IMAGE_DIR) print(f共找到图片 {len(images)} 张) valid_images [] missing_label_count 0 empty_label_count 0 for img_path in images: stem os.path.splitext(os.path.basename(img_path))[0] label_path os.path.join(LABEL_DIR, stem .txt) if not os.path.exists(label_path): missing_label_count 1 continue if os.path.getsize(label_path) 0: empty_label_count 1 continue valid_images.append(img_path) print(f有标注且非空的图片: {len(valid_images)}) print(f缺失标注文件: {missing_label_count}) print(f标注文件为空: {empty_label_count}) random.shuffle(valid_images) n len(valid_images) n_train int(n * TRAIN_RATIO) n_val int(n * VAL_RATIO) assign_map {} for i, img_path in enumerate(valid_images): if i n_train: assign_map[img_path] train elif i n_train n_val: assign_map[img_path] val else: assign_map[img_path] test ensure_dirs(OUTPUT_DIR) for img_path, split in assign_map.items(): stem os.path.splitext(os.path.basename(img_path))[0] label_path os.path.join(LABEL_DIR, stem .txt) dst_img os.path.join(OUTPUT_DIR, images, split, os.path.basename(img_path)) dst_label os.path.join(OUTPUT_DIR, labels, split, stem .txt) shutil.copy2(img_path, dst_img) shutil.copy2(label_path, dst_label) for split in [train, val, test]: cnt sum(1 for v in assign_map.values() if v split) print(f{split}: {cnt} 张) if __name__ __main__: main()脚本做了什么我一行行解释清楚。用random.seed(RANDOM_SEED)固定随机种子。同样的种子不管运行多少次shuffle的结果都一致这就是可复现的关键。数据量小的时候可能感觉不到必要性等你要对比不同模型效果、或者调参后想用同一套划分做公平比较时你就知道这个 seed 有多值钱了。用os.walk递归找图片避免手动维护图片路径列表。只要图片在IMAGE_DIR下任意子目录都能找到。对每张图片检查同名 txt 是否存在、是否为空。这一步很有价值。现实中经常出现标注过程中漏按 CtrlS导致一张图没有任何标注文件。这种图如果被混进训练集YOLO 训练时会跳过它但验证时会因为图片和标签对不上而报错。更麻烦的是如果你用的增强策略里包含了 mosaic空标签图参与拼接还会把别的图片的标签弄混乱。所以划分前强制检查把残次品直接筛出来不要带病进训练。最后用copy2复制而非移动保证原始标注数据仍然完整地留在原目录。这是一种很稳的工作方式原数据永远不动导出/划分全是副本。这样即使划分脚本有 bug原始数据也还完好无损可以重新跑。5.3 分层抽样进一步保证类别分布均匀上面的随机划分对绝大多数项目够用了。但如果你遇到类别极度不平衡的情况比如 1000 张图里 900 张只有人、100 张才有车随机划分可能导致某个集合里一张车都没有训练集和验证集的类别分布差得很远模型对“车”的泛化能力完全没法评估。这时可以做分层抽样。简单说就是先找出每张图片里含哪些类别然后按类别分布在 train/val/test 中做加权分配尽量让每个集合里的类别比例接近全局比例。实现思路是给每张图片算一个“类别指纹”比如[person, car]然后对每个类别分别抽一部分进入验证集。不需要做到完美只要保证“包含稀有类别的图片”在每个集合里都有一定数量。我遇到过最典型的情况是在做一个工业零件检测时某个异常类样本全网就 200 张如果不加任何处理做随机划分200 张里可能只有 10 张进验证集而且全被归到测试集。后来改成按类别抽样训练集留 150 张验证集留 30 张测试集留 20 张模型的评估结果才真正可信了一点。对这个问题有兴趣的读者可以先从统计每张图片的类别分布着手再决定要不要上重采样或者数据增强来补弱势类别。5.4 轻量替代方案用现成脚本或命令行工具如果你不想自己维护脚本也有更省事的方案。Ultralytics 官方仓库的split_dataset.py脚本很多人在用。它的核心逻辑和我上面写的差不多但少了一些可定制项。如果你能接受默认的 train/val/test 比例是 80/10/10且不想改代码直接拿官方脚本跑就行。想快速验证、不想把时间花在写脚本上的人可以用这个方案。另外Roboflow 的划分功能也可以做但需要把数据传上去再导出对本地项目来说流程偏重我个人不太推荐。我更建议的做法是把上面那份脚本保存到自己常用的工具目录按项目需求改改路径就能跑。虽然官方有现成的但自己手里有一份完全可控的划分脚本很多边界情况比如空标签、类别不均衡、按文件名规则排除某些图片都能快速适配长期看是省时间的。6. 划分后的校验与踩坑排查6.1 可视化校验把标注框画回图片上看划分完数据集很多人的下一步是直接开训。我劝你忍一忍先花十分钟做一次可视化校验把标注文件里的数值“翻译”回图片上亲眼确认框的位置、大小是对的。用 OpenCV 写一个简单的画框脚本把 train 和 val 目录下的图片随机抽 50 张将标注框画到原图上并保存到check/目录。import os import cv2 import random IMAGE_DIR dataset_split/images/train LABEL_DIR dataset_split/labels/train CHECK_DIR check CLASSES [person, car] # 和 classes.txt 保持一致 os.makedirs(CHECK_DIR, exist_okTrue) colors [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)] images [] for root, _, files in os.walk(IMAGE_DIR): for f in files: if f.lower().endswith(.jpg) or f.lower().endswith(.png): images.append(os.path.join(root, f)) random.seed(1) sample random.sample(images, min(50, len(images))) for img_path in sample: stem os.path.splitext(os.path.basename(img_path))[0] label_path os.path.join(LABEL_DIR, stem .txt) if not os.path.exists(label_path): continue img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id int(parts[0]) x_center float(parts[1]) * w y_center float(parts[2]) * h box_w float(parts[3]) * w box_h float(parts[4]) * h x1 int(x_center - box_w / 2) y1 int(y_center - box_h / 2) x2 int(x_center box_w / 2) y2 int(y_center box_h / 2) label CLASSES[cls_id] if cls_id len(CLASSES) else str(cls_id) color colors[cls_id % len(colors)] cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, label, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) out_path os.path.join(CHECK_DIR, stem _check.jpg) cv2.imwrite(out_path, img) print(f已生成可视化校验图 {len(sample)} 张到 {CHECK_DIR})这个脚本的本质就是你把 yolo 格式的归一化坐标还原成像素坐标画框。校验时重点看三件事框是不是紧贴目标、类别标签和框内物体是否一致、有没有明显错位。我每次标注完都会抽 100 张左右看一遍。作弊一点的技巧是在同一目录放几张“难度极高”的图比如过曝的、动态模糊的、目标极小的专门检验这些边缘样本的标注有没有问题。6.2 训练前最后一道检查统计标签类别数量可视化抽查解决的是“标得准不准”的问题下面这个统计脚本解决的是“数据全不全”的问题。训练前跑一遍统计每个集合中各类别目标的总数确认类别分布符合预期。import os from collections import Counter for split in [train, val, test]: label_dir os.path.join(dataset_split, labels, split) counter Counter() empty_files 0 for root, _, files in os.walk(label_dir): for f in files: if not f.endswith(.txt): continue path os.path.join(root, f) if os.path.getsize(path) 0: empty_files 1 continue with open(path, r, encodingutf-8) as fh: for line in fh: if line.strip(): cls_id int(line.strip().split()[0]) counter[cls_id] 1 print(f{split}: 类别分布 {sorted(counter.items())}, 空标注文件 {empty_files} 个)看到输出里的空标注文件数不为 0我建议直接把对应的图片从数据集中剔除。YOLO 训练时对没有标注的图片不会报错它直接跳过。但验证时如果标签文件为空某些版本会输出警告更糟的是这类图片会让num_labels统计产生偏差影响你对数据集“到底有多少有效样本”的认知。还有个容易被忽略的点标注 class_id 越界。比如 classes.txt 里只有 3 个类但某个 txt 里出现了 class_id 为 5 的记录训练时 YOLO 会直接把这一行忽略或报错。用上面这个统计脚本就能发现如果 counter 里出现了大于等于类别总数的 id马上回去检查那张图。6.3 踩坑排查参考表问题现象可能原因处理方式训练提示All bounding boxes of dataset are empty标签目录没有挂上或标签文件全部为空检查 data.yaml 路径用 6.2 节脚本统计标签预测类别和实际目标对不上classes.txt 与 data.yaml 类别顺序不一致统一团队 classes.txt禁止中途改顺序验证集 mAP 虚高但实际检测很差验证集与训练集图片高度相似数据泄漏用视频抽帧图片时按时间间隔抽帧划分时避免连续帧跨集合某些类别完全没被检测到该类别目标过少或被错误打标可视化校验 类别分布统计必要时补数据标注框与目标贴合度差检测框偏大画框时留了太多空白边放大图片重新标框紧贴目标轮廓7. 最后分享一点个人经验我自己把“标注 - 划分 - 校验”这条流程跑了少说二三十个数据集之后最大的体会是数据准备阶段的时间永远不要省。很多人标了 2000 张图觉得 2 天时间好长但真正开始训练后因为数据问题反反复复试错、返工、重新标注花的时间通常只多不少。现在我的习惯是每次标注完成、划分完成后强制自己在项目根目录生成一份数据说明文件内容包括图片总数、每个集合的图片数量、类别清单、随机种子、标注日期、标注人和备注。这份文件也许一个月后我自己都不看但它是让别人快速接手项目的一把钥匙也是半年后复盘数据集问题时的重要依据。文章到这里已经覆盖了环境安装、标注实操、格式细节、自动划分脚本、数据校验这一整条链路。把这条链路跑通你手里就有了一个可以反复复用的标准流程。下次换任何新项目、新数据套上这个流程就能少走很多弯路。