做检测项目的人,十有八九不是在调模型,而是在跟数据搏斗。我刚带完一个巡检项目,前后折腾下来深深体会到:数据集的质量直接决定模型上限,而“标注完不会转格式”这件事,几乎每个入行的人都要踩一遍。所谓一篇博文讲清楚检测数据集制作全流程,其实就是回答三个问题:数据从哪来、标注怎么做、VOC/COCO/YOLO怎么转。这三个环节搞顺了,后面训YOLO、调TensorRT、部署到边缘设备都是顺水推舟的事。
这篇内容适合三类人看:一是刚接触目标检测、手里有一堆图片不知道从哪下手的初学者;二是已经跑通模型但被数据格式和标注质量反复折磨的工程同学;三是需要给团队定数据规范和标注流程的项目负责人。我尽量把收集、标注、格式互转、训练前的自检闭环这几块讲透,中间会穿插我实测过的工具、踩过的坑和可以直接抄的转换脚本。
1. 先想清楚再动手:数据规划这件事怎么设计
很多人一上来就开标注工具画框,画了两天才发现类别定义不统一、图片尺寸混乱、缺了一堆背景负样本,最后只能推倒重来。数据集的制作最忌讳“先干后想”,真正高效的流程是先用半天把下面两件事定死。
1.1 检测目标决定标注策略
同样是“目标检测”,不同场景对标注的要求天差地别。我做过一个鸟类目标检测的生态监测项目,无人机航拍画面里鸟群密集、目标只有十几个像素,这时候标注必须放大到200%以上逐只画框,而且需要画“点”辅助对齐,不然框稍微偏两个像素,模型学到的边界就是歪的。而开关闭合检测那种工业场景,目标大且固定,难的是“闭合”和“未闭合”的类别定义——缝隙小于多少算闭合?这个阈值不写清楚,两个标注员会给出完全不同的结果。
所以我建议项目开工前,先输出一份标注规范文档,把五件事写死:
- 类别体系:有哪些类、各类的层级关系,是“人+车+狗”平铺,还是“车辆-小轿车-SUV”嵌套
- 正负样本定义:什么算目标、什么算背景、模糊目标如何处理、严重遮挡是否标注
- 边界画法:框要贴合目标最外缘,还是包含必要的上下文背景
- 特殊规则:截断目标、多实例重叠、目标部分出画如何处理
- 容差标准:框与真实边界的最大偏差,比如工业场景我习惯要求IoU偏差不超过2%
这套规范看起来繁琐,但它就是数据集质量的第一道防线。你不需要把它写成几十页,A4纸一页能写完就够,关键是要让每个标注者和验证者都按同一套标准工作。
1.2 数据从哪来:开源、自采还是“自制传感器”
数据来源大体分三路。
第一路是复用公开数据集。比如车辆检测方向常见到BDD100K这类驾驶场景数据集,里面有十万张图的规模,类别也相对齐整。用它跑预训练、做冷启动非常好用。要注意的是,公开数据集各有各的任务定义和标注口径,直接用可能跟你的业务场景对不上,而且公开数据普遍存在类别分布倾斜,需要按需筛选再补充自采数据。
第二路是自采。这又分两种情况:固定摄像头的监控场景,你可以直接通过RTSP拉流,按帧间隔抽帧保存;移动场景(无人机、车载、手持设备),则需要规划采集路线和时段,保证覆盖不同角度、不同光照、不同遮挡程度。自采时有个我吃过亏的细节:不要连续帧全存。25帧的视频里相邻两帧几乎一样,全存会导致训练集和验证集之间存在极强的“时间相关性”,模型验证指标虚高,部署后立刻露馅。我现在的做法是按照“每N帧取一帧,且跨不同时段、不同场景抽帧”来筛选。
第三路是从互联网收集公开图片。这条路我必须提醒:图片版权和数据授权问题越来越严格,不要直接爬来就用。稳妥的方式是确认数据来源的授权条款,尽量使用开放许可的图像资源,或者自己采购数据服务。
数据收集还有一个容易被忽略的点:多样性检查。收集完成后先把图片按亮度、色温、模糊度、目标尺寸四类维度做统计分布,确认你的数据不是“某一个晴天下午”的数据,而是覆盖了项目真实环境中的数据分布。航拍场景还要额外注意季节变化,同一片区域,春天和秋天的植被特征差别会直接影响模型识别。
2. 标注是数据集质量的真正分水岭
数据图片有了,接下来进入标注环节。很多人觉得标注就是“画框嘛,有手就行”,实际上标注质量对模型的影响远比想象中大。YOLO系列损失函数在边界框回归分支上用IoU相关的度量,框贴不贴、中心点准不准、边界留多少余量,都会变成梯度直接反馈到模型参数里。你标注偏差5个像素,模型学出来就是偏差5个像素的“标准”。
2.1 标注工具选型:从CVAT到LabelImg怎么挑
我这么多年用下来,标注工具选择完全取决于团队规模和项目类型。给你做个参考:
| 工具 | 适用场景 | 关键优点 | 主要缺点 |
|---|---|---|---|
| CVAT | 中大型团队、视频标注、需要模型辅助预标注 | 支持在线协作、视频抽帧标注、可导入自训练模型做预标注 | 部署稍复杂,需要维护服务端 |
| LabelImg | 个人学习、几十张图的小项目 | 安装简单、打开就能用 | 停更多年,功能老旧,无协作能力 |
| Label Studio | 多模态任务、关系标注、文本/图像混合项目 | 类型丰富,适合做综合数据集 | 较重,框标注效率不如CVAT |
| AnyLabeling | 遥感大图、不规则目标 | 支持多边形、自动分割辅助 | 生态和文档不如CVAT |
| Roboflow | 云端快速打标签、想顺手做增强 | 浏览器直接用、导出格式全 | 数据在云端,敏感项目要慎重 |
| QGIS/ArcGIS | GIS遥感影像出图和坐标标注 | 能直接处理地理坐标、分式制图标注 | 做检测数据集需要额外导出转换,流程绕 |
我个人的主力推荐是CVAT。它自托管部署以后,团队成员通过浏览器就能标注,支持多人同时协作,而且最关键的是:它可以加载你自己训练出来的一版模型做预标注,标注员只需要修改模型画错的框,而不是从零开始画。这在几千张图的项目里能省下一大半人力。我实测过,一个熟练标注员用纯手画框每小时能标大概80到120个目标,用上模型预标注后这个数字能翻一到两倍。
如果你只是自己学习YOLO,暂时只有一两百张图,那LabelImg也不是不能用,但别在这个工具上花太多时间,早点切换到CVAT或者AnyLabeling更划算。遥感图像那边我多说一句:GIS软件里常见的“分式标注”是制图出图用的符号化功能,做目标检测数据集时不要直接在QGIS里拿这个当标签,正确做法是把地物矢量导出成GeoJSON或者shapefile,再转换成VOC或COCO的坐标体系,否则后面坐标对齐会非常痛苦。
2.2 标注规范与质检:让标注员不返工、让模型不背锅
工具定了,真正拉开差距的是流程和管理。我给自己的项目立了三条规矩。
第一条规矩是先标后审分两轮。第一轮由标注员完成初标,第二轮由技术负责人或资深标注员抽检复核,抽检比例我习惯定在20%~30%。抽检不是随便看看,要看四件事:类别是否标错、框是否贴合目标、有没有漏标严重遮挡目标、有没有把背景误标成目标。复检中发现同一类问题反复出现,就把问题截图整理成“返工案例”发到群里,这比写十页规范文档有用得多。
第二条规矩是每个目标都必须“所见即所得”。严重遮挡的目标按可见部分画框,不要脑补完整轮廓;目标只有一小部分出画时,保留出画部分在图像内的框;目标完全出画则不标注。这些规则的背后是模型训练逻辑:你让模型学“完整的目标长什么样”,它只会从你的标注里学“可见部分长什么样”,所以标注要忠实反映图像中实际可见的物理范围。
第三条规矩是用统计数字说话。每周对标注进度做一次分布统计,包括每类目标的数量、每张图的平均目标数、标注框面积的直方图、图片分辨率的分布。这些数字能提前暴露问题。比如某类别的框数只有其他类别的十分之一,那你不用等训练完就知道这个类别会欠拟合;比如框面积直方图在极小值处出现异常堆积,那可能是标注员漏标了中大型目标,只挑容易画的小目标标。
这里也想提醒一句工业场景的朋友:开关闭合检测、传送带异物检测这类项目,正负样本极度不平衡是常态。你花两周标注了一千张“正常无目标”的背景图,模型才能学会不误报。一定不要只标正样本而不收集背景负样本,这类数据在后续误检率控制上起着决定性作用。
3. 三大数据格式的底层逻辑:VOC、COCO、YOLO
标注完成之后,你手里会有一堆XML或者JSON格式的标注文件。这时候新手最容易原地蒙圈:VOC里面是XML,COCO里面是JSON,YOLO里面是txt,这三个格式到底什么关系?为什么要绕来绕去?理解这三个格式的本质,就是你“一次搞对”的前提。
3.1 VOC:XML不是随便记四个角点
VOC格式源自Pascal VOC挑战赛,标注文件是一个XML,跟图片同名。核心结构大概长这样:
<annotation> <folder>images</folder> <filename>000001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>person</name> <bndbox> <xmin>100</xmin> <ymin>80</ymin> <xmax>320</xmax> <ymax>480</ymax> </bndbox> </object> </annotation>关键点是:VOC的四个坐标是绝对像素坐标,xmin/ymin代表框左上角,xmax/ymax代表框右下角,而且它们是闭区间。这意味着xmax减去xmin是框的宽度(在连续值情况下还要考虑像素边界问题,但检测任务一般按像素坐标处理即可)。VOC格式朴实,缺点也很明显:一个文件一坨标签,图片和标注文件分离存放,没有统一的索引结构,大量图片时文件数量膨胀,管理非常难受。但它作为“中间格式”很好用,因为人人都会解析XML。
3.2 COCO:一份JSON看懂目标检测的“标准容器”
COCO格式把整个数据集的标注信息集中到一个大的JSON文件里。它最核心的有五个字段,分别是info、images、annotations、categories、licenses。其中images是图片元信息列表,每项包含id、file_name、width、height;annotations是标注列表,每条标注包含id、image_id、category_id、bbox、area、iscrowd;categories是类别列表,从id到名称的映射。
COCO的bbox是一个四元组[x, y, width, height],注意跟VOC的[xmin, ymin, xmax, ymax]不一样,而且COCO的坐标是绝对像素值,x、y是框左上角。这是格式互转时最容易出错的地方,没有之一。我见过有人直接把COCO的x、y当成xmin、ymin,把width、height当成xmax、ymax用,结果所有框都画错了位置。
COCO的优势是结构统一、生态庞大,尤其做多任务或多数据集合并时,一份JSON就能管理全部信息。它的缺点是手写和人工检查都很难受,JSON文件动辄几百MB,肉眼排查不现实。所以我的习惯是:COCO作为内部存储格式和交换格式,但不作为人工标注的最终产品格式。
3.3 YOLO:每行五个数字背后的秘密
YOLO格式是三者里最“精简”的,训练时每个图片对应一个同名txt文件,每一行代表一个目标:
cls_id x_center y_center width height具体到数字,比如一行:
0 0.532100 0.441200 0.124500 0.256300它的含义是:类别索引0(对应类别列表里的第一个类),目标中心点的x坐标为图像宽度的0.5321倍,中心点y为图像高度的0.4412倍,框宽为图像宽度的0.1245倍,框高为图像高度的0.2563倍。看到没有,YOLO格式里所有坐标都是相对图像尺寸归一化到0~1之后的小数。
这个格式最大的坑在于:类别索引完全依赖你定义的类别顺序。你在转换脚本里把“person”排在第几位,训练配置文件data.yaml里就必须把“person”排在同一位,否则模型学到的东西全乱套。我经常看到有人从开源库下载了权重,拿着自己的数据集去训,最后发现类别全部错位——因为在他们的转换脚本里,类别顺序跟预训练模型的类别顺序不一致。这个错位在训练时不会报错,只会默默降低模型性能,属于最隐蔽的错误之一。
另一个点:YOLO坐标是归一化后的浮点数,转换时保留的小数位数会影响精度。我一般用6位小数,足够在1920x1080的图像上保持亚像素级的精度,但如果是遥感超大图,建议保留到8位甚至用更高精度。别用2位小数的格式,那是给自己埋雷。
4. 格式互转一次搞对:核心实现与避坑指南
理解了三种格式,接下来就是转换环节。这个环节特别容易出问题的地方不是代码本身,而是坐标语义、类别索引、文件夹结构这三件事。我觉得最好的办法是给自己定一个“内部统一格式”,再写一套转换脚本,别每次都临时改。
4.1 统一中间格式:转换传递链路怎么设计
我的个人建议是:内部管理用COCO,训练喂给模型用YOLO,跟外部团队交换用VOC或COCO。为什么要这样设计?因为COCO结构化程度高,一份JSON能承载所有信息,适合放在版本管理里做diff和统计;YOLO是训练框架直接消费的格式,不需要训练时再解析XML或JSON;而VOC虽然格式老,但解析简单,跟一些老旧工具链对接时反而是最稳妥的。
具体到目录组织,我推荐一个稳定模板:
dataset/ images/ train/ val/ labels/ train/ val/ annotations/ instances_train.json instances_val.json classes.txtimages放图片,labels放YOLO的txt标注,annotations放COCO格式的JSON。classes.txt是类别列表,每行一个类名,顺序就是YOLO格式的类别索引顺序。这个模板结构清晰,任何脚本都能基于它做校验,我建议团队项目从第一天就按这个模板创建。
4.2 VOC到YOLO的转换代码
核心逻辑是:从XML里读出绝对像素坐标,除以图像宽高完成归一化,再把类别名映射成类别索引。这里的图像宽高不要自己去猜,一定从XML的 节点读,因为XML里记录的就是这张图的原始分辨率。直接看代码:
import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_file: Path, class_names: list[str], out_dir: Path) -> None: tree = ET.parse(xml_file) root = tree.getroot() size = root.find("size") width = int(size.find("width").text) height = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: continue cls_id = class_names.index(name) box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) x_center = (x1 + x2) / 2 / width y_center = (y1 + y2) / 2 / height w = (x2 - x1) / width h = (y2 - y1) / height lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_dir.mkdir(parents=True, exist_ok=True) out_file = out_dir / (xml_file.stem + ".txt") out_file.write_text("\n".join(lines) + "\n", encoding="utf-8")注意代码里对“类别不在class_names里”的情况做了continue跳过。这个不是多余的,因为标注过程中难免出现脏数据,一个类名拼写错误就会导致该目标被无声吞掉。我建议转换完输出一行统计日志,比如“文件名 成功N个 跳过M个”,方便肉眼检查。
4.3 COCO转YOLO的代码与反向转换
COCO转YOLO是先读JSON,再根据image_id和category_id做映射。有一个需要留意的点:COCO的类别id通常从1开始,而YOLO的类别索引从0开始,所以转换时要构建一个从COCO category_id到YOLO class_id的字典,而不是直接用category_id当class_id。
import json from pathlib import Path def coco_to_yolo(ann_json: Path, out_dir: Path) -> None: data = json.loads(ann_json.read_text(encoding="utf-8")) cat_id_to_class_id = { cat["id"]: i for i, cat in enumerate(data["categories"]) } for img in data["images"]: img_id = img["id"] width = img["width"] height = img["height"] lines = [] for ann in data["annotations"]: if ann["image_id"] != img_id: continue x, y, w, h = ann["bbox"] x_center = (x + w / 2) / width y_center = (y + h / 2) / height w_norm = w / width h_norm = h / height lines.append( f"{cat_id_to_class_id[ann['category_id']]} " f"{x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}" ) out_dir.mkdir(parents=True, exist_ok=True) out_file = out_dir / (Path(img["file_name"]).stem + ".txt") out_file.write_text("\n".join(lines) + "\n", encoding="utf-8")反向转换YOLO转VOC,思路就是把归一化坐标乘回宽高,得到xmin/ymin/xmax/ymax,再构造XML节点。这里我提醒一句:YOLO到VOC如果丢失了原始图像尺寸信息,就得从图片文件本身去读。写代码时不要假设所有图片都是同一个分辨率,尤其自采数据,经常出现混合分辨率的情况。稳妥做法是转换前用PIL或OpenCV读取每张图片的真实宽高。
from PIL import Image from pathlib import Path def yolo_to_voc(txt_file: Path, image_file: Path, class_names: list[str]) -> list[dict]: with Image.open(image_file) as im: width, height = im.size objects = [] for line in txt_file.read_text(encoding="utf-8").strip().splitlines(): parts = line.strip().split() if len(parts) < 5: continue cls_id = int(parts[0]) x_center, y_center, w, h = map(float, parts[1:]) x1 = (x_center - w / 2) * width y1 = (y_center - h / 2) * height x2 = (x_center + w / 2) * width y2 = (y_center + h / 2) * height objects.append({ "name": class_names[cls_id], "xmin": int(round(x1)), "ymin": int(round(y1)), "xmax": int(round(x2)), "ymax": int(round(y2)), }) return objectsVOC转COCO的代码也不复杂,把XML里的xmin、ymin、xmax、ymax换算成[x, y, width, height],再填上image_id和category_id即可。需要说明的是,COCO的categories里的id我习惯从1开始,因为pycocotools在计算mAP时会用category_id做索引,从1开始更符合业界惯例。如果你跳过了这一步,用id=0作为第一个类别,部分评估工具会把第0类当成背景,mAP计算直接出错。
4.4 转换时最容易踩的五个坑
把这三四年里我在格式互转上踩过的坑集中列一遍,每一个都是真金白银换来的教训:
COCO的bbox是[x, y, width, height],不是[x_min, y_min, x_max, y_max]。很多人拿COCO转YOLO时直接用x+w当xmax,结果框整体偏移一个宽度。正确做法是x_center = x + w/2。
类别顺序错位是隐形的。同一份数据,类别列表顺序不同,生成txt的cls_id就不同。预训练模型、data.yaml、转换脚本这三处的类别顺序必须完全一致。我现在的做法是把classes.txt纳入版本控制,转换脚本自动读取它,禁止任何人手动写“class 0 = person”。
坐标越界和负数不检查。目标在图像边缘时,标注框偶尔会超出图像边界,归一化后出现大于1或小于0的值。YOLO训练时会直接忽略这些样本或报异常。转换后要加越界检查,把越界框clamp到0~1,但不要粗暴删除,除非它是完全在图像外的噪声标注。
空标注文件不要直接删。一张图没有任何目标时,YOLO格式对应一个空txt文件,这个文件必须保留。训练框架看到空文件就知道这张图是背景负样本。删掉空文件会导致一张背景图被当成“没有标注数据”而跳过,负样本失效,误检率会上升。
图像尺寸信息必须从原始来源读取。VOC的XML里有size节点,COCO的JSON里也有width、height,但当两种格式混用、或者从YOLO反向转换时,这些元信息很容易丢失。永远不要假设尺寸等于“训练时resize后的尺寸”,YOLO训练会在加载时做letterbox缩放,但存储格式里的坐标是原始图像尺寸下的坐标。
5. 训练前的数据校验与反馈闭环
格式转完,很多人迫不及待开始训练。先缓一缓。数据从“能训练”到“训练得好”,还差一轮系统校验。这个环节看似只是几个脚本的事,但能帮你省出整整一周的排查时间。
5.1 划分训练集和验证集:时间、场景、类别三重隔离
数据划分直接影响评估可信度。我见过太多人图省事,直接random.sample按文件名单词随机分,结果同一段监控视频的连续帧一部分进了train、一部分进了val,验证mAP跑到0.9,部署到现场直接崩到0.5以下。这种问题在视频帧数据里几乎必然发生,因为相邻帧几乎一模一样。
我的划分原则是三条:
- 时间隔离:采集时间在前80%的数据作为train,最后20%作为val,模拟“用历史数据预测未来数据”的真实场景
- 场景隔离:确保同一个摄像头、同一地点、同一条航线拍的数据不要同时出现在train和val
- 类别隔离:划分后统计train和val里每个类别的框数比例,重点类别在验证集里至少要有几十个样本,否则验证指标波动巨大
如果你做的是小样本数据,比如只有几百张图,还有一个补充手段:K折验证。把数据切成5份,轮流拿其中1份做验证。这样虽然训练成本翻了5倍,但评估结果稳定得多,尤其适合像开关闭合检测这样类别少、单类数量少的工业场景。
5.2 训练阶段反馈来的数据问题怎么修
训练过程中出现的很多诡异情况,根子都在数据上。我把高频问题整理成一张排查表:
| 训练现象 | 常见数据原因 | 处理建议 |
|---|---|---|
| loss直接变成NaN | 标注坐标越界、标签文件里有非数字字符、图像文件损坏 | 先跑数据校验脚本,检查txt内容是否合法 |
| 验证AP极低,训练loss正常 | 数据划分泄漏(train/val太像)或验证集太小 | 重新按时间/场景划分,或加大验证集 |
| 小目标完全不收敛 | 标注框面积普遍太小、小目标占比过低 | 补充小目标样本,或改用NWD等小目标友好损失 |
| 某个类别AP为0 | 该类别训练样本太少,存在类别不均衡 | 做类别过采样,或用复制粘贴增强 |
| BN层数值崩溃 | 数据里出现极端的高亮度/低亮度噪声,或学习率过大 | 检查图像质量、剔除坏图、降低学习率或冻结前几层BN |
| 误检满天飞 | 背景负样本严重不足 | 补充背景图,把空标注文件保留在数据集里 |
训练中“BN崩溃”这个词,很多新手第一次听到会懵。其实它表现为训练到一半loss突然飞升,报错里出现跟BatchNorm相关的nan值。数据层面的常见诱因是某些图像存在极端像素分布,比如传感器故障产生的全白画面,或标注文件里混入了非法坐标。我建议在训练前就做一次全量数据清洗:逐张图片检查能否被OpenCV解码、尺寸是否为正数、像素分布是否有异常尖峰。这个检查脚本跑一次也就几分钟,但能拦住一大批训练崩溃。
小目标问题也要单独说。遥感图像标注、航拍鸟类监测这类场景下,目标常常只有十几个像素。我实测下来,YOLO模型在这种数据上收敛慢、漏检高,除了补充更多高分辨率样本外,损失函数层面也可以做改进,比如引入基于Normalized Wasserstein Distance的NWD变体,对小目标的定位误差更不敏感。不过这是模型层面的优化,数据层面的前提是:小目标框必须画得极其准确,偏差1到2个像素在归一化坐标里差别就很明显。
5.3 从数据集到部署:顺带聊聊推理路数怎么估
数据做完、模型训好,很多人下一步是接到实时视频流上做推理部署。有个问题我经常在社区里被问到:像T4这种显卡,处理1080p 25帧的视频流,用TensorRT跑YOLO,640分辨率能带几路?
我的回答是:别只看GPU推理能力,解码和前后处理才是瓶颈。T4上用TensorRT FP16跑YOLOv8s、640输入,单帧GPU纯推理时间大概3到5毫秒,看起来单卡能跑两三百帧每秒对吧?但实际链路里每一帧要先从RTSP拉流、硬解码、缩放、归一化、推理、后处理、绘制结果,这些环节都要吃CPU和GPU显存带宽。1080p的H.264解码即使在GPU硬解,也要占一部分资源,CPU侧还要做RTSP拉流和帧解析。
给你一个我实测过的估算口径:单路25帧视频流下,含解码、预处理、推理、后处理全链路的单帧耗时通常在20到40毫秒。按这个算,一张T4带8到12路是比较安全的区间。你要是只做隔帧检测(比如每3帧检一次),可以把这个数字往上提到15到20路。这里我给不出一个精确的“标准答案”,因为模型大小、输入分辨率、后处理阈值、画面复杂度都会影响。但记住一句话:先从数据侧控制输入规模,比盲目堆显卡更划算。你把输入分辨率从640降到512、或者裁剪出感兴趣区域再送检,路数立刻就能翻倍。
聊到这里,回顾整个数据链路,我最想强调的还是那句老话:“一次搞对”。怎么才能一次搞对?不靠运气,靠的是流程前置、格式统一、脚本复用、校验兜底。我现在的个人习惯是:所有转换脚本都放在项目仓库里版本化管理,每次转换前后都跑一遍数据校验脚本,classes.txt永远是一份受控文件,原始标注永远备份不覆盖。踩过几次坑之后你会明白,数据集制作真正的成本不是画框那几周,而是返工、错位和排查那几周不可见的时间黑洞。把格式转换这件小事一次搞清楚,后面的训练和部署才能真正顺利起来。
最后分享一个小技巧:如果你手里已经有一套训好的模型,在CVAT里做新项目的预标注时,一定要先做一次“预标注质量抽样”。随机抽50张图让模型跑一遍,人工检查框的置信度和贴合度,再决定完全接受、部分修改或推翻重新标注。这能帮你在标注阶段就预判训练阶段的很多问题,比事后返工高效得多。