经常看到有人拿着同一份数据训练 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 + 回车 | 确认当前选中框(视版本而定) |
我的操作习惯是:右手负责画框和调整,左手负责切换图片和保存。整个流程就是“画框 -> 填类别 -> Ctrl+S -> D翻页”。不要标一张存一张,这样太碎,但也不要攒 100 张才存一次,中间软件崩了眼泪都流不出来。我一般每标完 10 张就 Ctrl+S 一次。
画框的时候还有一个细节: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 就是 0,car 的 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 0,names[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_ok=True) os.makedirs(os.path.join(base, "labels", split), exist_ok=True) 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 是否存在、是否为空。这一步很有价值。现实中经常出现标注过程中漏按 Ctrl+S,导致一张图没有任何标注文件。这种图如果被混进训练集,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_ok=True) 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", encoding="utf-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", encoding="utf-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 天时间好长,但真正开始训练后因为数据问题反反复复试错、返工、重新标注,花的时间通常只多不少。
现在我的习惯是每次标注完成、划分完成后,强制自己在项目根目录生成一份数据说明文件,内容包括:图片总数、每个集合的图片数量、类别清单、随机种子、标注日期、标注人和备注。这份文件也许一个月后我自己都不看,但它是让别人快速接手项目的一把钥匙,也是半年后复盘数据集问题时的重要依据。
文章到这里已经覆盖了环境安装、标注实操、格式细节、自动划分脚本、数据校验这一整条链路。把这条链路跑通,你手里就有了一个可以反复复用的标准流程。下次换任何新项目、新数据,套上这个流程,就能少走很多弯路。