☰
目标检测数据集制作全流程:从标注规范到VOC/COCO/YOLO格式转换
2026/10/1 18:02:23 网站建设 项目流程

做检测数据集最磨人的不是标得慢,而是格式来回倒腾。我接手过好几个项目,团队里有人用LabelImg导出的VOC XML,有人从CVAT拉下来的COCO JSON,训练那边又只认YOLO的txt,每次做新一轮数据都花一两天在写转换脚本、排查类别对不上的问题上。这篇就把检测数据集的全流程摊开讲:怎么收集、怎么标、VOC/COCO/YOLO三种格式底层是怎么回事、互转到底在转什么,给出可以直接复用的代码和规则。无论是刚入门目标检测的学生,还是被标注格式折磨的工程团队,这篇都能用。

1. 返工重灾区不在转换,而在收集和标注的地基

先说一个我观察了很久的结论:格式转换本身不难,难的是数据一开始就没收对、没标对,导致后面所有流程都在填坑。很多人以为买个好用的标注工具、写个转换脚本就万事大吉,结果训练时发现mAP上不去、某些类识别极差、模型对光照特别敏感,回头查了一大圈才发现是数据源的问题。

1.1 收集阶段就要想清楚的场景覆盖

数据收集最容易犯的错是"贪多图快"。我见过有人用爬虫抓了十万张图,看着数量很大,实际一分析全是同一类背景、同一种角度、同一个时间段拍的,训练出来模型换个环境就废。收集数据前先列一个覆盖矩阵,每个维度至少有三五档变化:

  • 光照变化:白天、傍晚、夜间补光、逆光、强反光
  • 尺度变化:近景大目标、远景小目标、同框大小混杂
  • 角度变化:俯拍、平视、仰拍、侧倾
  • 背景干扰:纯色背景、复杂纹理、目标与背景颜色相近
  • 遮挡程度:完整、半遮挡、密集遮挡
  • 运动状态:静止、低运动模糊、高运动模糊

真正好的检测数据集不看你存了多少G图片,而是看这些图片里目标出现的形态跨度有多大。以车辆检测为例,白天阳光下拍的车、夜晚车灯照亮一部分的车、雨雾天气模糊的车,三者的特征差异比"大众和丰田"的差异大得多。

1.2 视频抽帧不是越快越好

自己用视频抽帧做数据时,按固定间隔抽是最省事也最容易踩坑的方案。如果视频里一辆车静止停着占了十秒,按每秒一帧抽就有十张几乎完全一样的图,这些图进训练集会带来严重的过拟合。我常用的抽帧策略是"去相似帧":先做一遍快速感知哈希,过滤掉帧间相似度过高的候选帧,保证数据集的"有效信息密度"。

另一个实用技巧是按运动强度动态抽帧。用简单的帧间差分计算画面变化量,变化大的区间加密抽帧,变化小的区间稀疏抽帧。这样同一个视频能抽出覆盖面更广的样本,后期标注的ROI也更高。

1.3 公开数据集与自采数据的组合策略

完全从零收集太费劲,完全依赖公开数据集又和控制场景差距太大。我的习惯是分三步走:先找一批高质量公开数据集快速跑通基线,再针对自己的场景补充自采数据,最后用难例挖掘持续补充。比如做鸟类目标检测,公开的CUB-200数据集有图像但很多是特写,和你用远摄镜头在树冠里找鸟完全两回事,就得靠自采视频抽帧来补齐小目标样本。

公开数据集引用时还要注意许可协议和来源标注问题,特别是商用项目,很多公开数据集只允许科研用途。这块不把关好,后面部署上线可能踩法律风险。

2. 标注工具和标注规则:先立规矩再开工

数据收集完就进入标注环节。我给所有团队的建议是:换工具之前先立标注规范,规范不统一,换什么工具都一样出问题。

2.1 工具选型对比

标注工具折腾过不少,说说我用下来的真实感受。单机简单场景用LabelImg最顺,它是VOC格式原生的,界面朴素但速度不错,适合一个人标几百张图的小项目。团队协作、数据量大的项目,CVAT是首选,它原生支持Pascal VOC、COCO、YOLO三种格式的导入导出,在线的任务分配和审核流程对人效提升很大,一个任务分给几个人同时标还能做交叉复核。

最近两年X-AnyLabeling也值得关注,它集成了多种AI辅助标注(SAM模型交互分割、自动边框预测),敏感度和效率确实高,尤其适合目标轮廓复杂、形状不规则的场景。不过辅助标注跑偏的现象也有,后面标注质量检查环节必须跟上。

2.2 必须先定的四类标注规范

动标之前,团队必须把下面四件事写成文档,并且每个标注员都签字确认:

第一,类名表。类别名统一用英文小写加下划线,比如person、traffic_light,禁用带空格和大小写混拼的命名。YOLO格式是纯数字ID,换格式时ID和类名完全靠映射表,类名不一致会导致转换后类别彻底错乱。

第二,边界框定义。检测框是紧贴目标外轮廓,还是包含少量边缘间隙,这个必须统一。标准做法是紧贴可见轮廓,包括被遮挡但能判断边界的部分。框的大小直接影响YOLO的anchor匹配效果。

第三,遮挡与截断的标注规则。半遮挡目标(遮挡面积不超过50%)必须标框,框取可见部分的外接矩形;完全遮挡不标;目标超出图像边界时,框要严格截断在图像边缘内,不允许让xmax超出图片宽度。

第四,小目标的下限。一张1920x1080图里小于20x20像素的目标,建议在早期标注阶段直接放弃,否则大量小目标框会让训练loss剧烈震荡。如果领域本身就看重小目标(比如遥感、无人机视角),那就需要单独配置切片策略,把大幅影像切成有重叠的小图再标。

2.3 标注边界的一致性问题

CVAT的交互标注意味着标注员可以来回拖动边界框,这带来的一个问题是目标边界到底落在"语义边界"还是"视觉边界"。举个例子,同一辆车,一个人把框贴着车身画,另一个人把后视镜和保险杠凸出部分也包进去了,两个框可能差好几像素。对YOLO这种基于锚框的算法,几个像素的抖动对最终回归结果影响不小。

我的做法是在规范里写死"语义边界"策略:框边界落在目标语义属于它的最外层像素的外切边缘。也就是只要目标自身的部件(后视镜、天线、翘起的保险杠)能被观察到,就包含进框;背景里的物体(路牌、灯杆、灌木)即便和车产生粘连,也不能包含。标完后做一轮全量复核,两个人交叉看,把边界不一致的框统一。

3. 把三种格式当数据结构看:坐标体系是分水岭

VOC、COCO、YOLO这三种格式,很多人只记了个大概就去做转换,结果就是转得越多错得越离谱。我自己总结的方法是:把它们当成三种数据结构去理解,而不是三个文件名后缀。三种格式的差异核心就一句话——坐标体系完全不同。

3.1 VOC XML:数组思维的绝对坐标

VOC的标注文件是一个XML文档,里面每个<object>节点描述一个目标,边界框用的是绝对像素坐标,四个值是左上角(xmin, ymin)和右下角(xmax, ymax),就是x1y1x2y2格式。图片尺寸、通道数等信息也都写在XML的<size>节点里。

一个典型的XML长这样:

<annotation> <folder>JPEGImages</folder> <filename>001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>person</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>120</xmin> <ymin>300</ymin> <xmax>480</xmax> <ymax>870</ymax> </bndbox> </object> </annotation>

值得留意的细节是<truncated>(目标是否超出图像边界)和<difficult>(目标是否难以识别)两个字段,训练时需要根据任务决定是否跳过difficult=1的样本,转换格式时这两个字段经常被粗心丢掉。

3.2 COCO JSON:索引表思维的规范JSON

COCO是三个格式里结构最规整、也是维数最多的。它本质是三张表:images表存每张图的ID、文件名、宽高;annotations表存每个矩形框的具体信息,bbox是[x, y, width, height]——注意是左上角坐标加宽高,不是右下角坐标;categories表存类别ID和类别名的映射。

{ "images": [ {"id": 1, "file_name": "001.jpg", "width": 1920, "height": 1080} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [120, 300, 360, 570], "area": 205200, "iscrowd": 0} ], "categories": [ {"id": 1, "name": "person", "supercategory": "none"} ] }

COCO的area字段很多人换算时忽略,其实它就是width * height。另外COCO的iscrowd字段代表这个框内是一整群目标还是单目标,训练时处理方式不同,转换脚本里经常被默认写成0。这些细节不管,模型精度可能莫名其妙地差。

3.3 YOLO TXT:训练友好的归一化相对坐标

YOLO格式的每个图片对应一个txt文件,每行一个目标,格式是class cx cy w h。这里的坐标是归一化相对坐标,值的范围在0到1之间。中心点cx, cy是矩形框中心点的坐标除以图片宽高,宽w和高h也是用目标的宽高分别除以图片宽高。

0 0.156250 0.541667 0.187500 0.263889

这种设计的初衷是让训练时的网络输入和标签保持在同一个量纲下,避免图片尺寸变化导致标签失效。也正因为是相对坐标,YOLO格式的txt不携带图片尺寸信息,一旦和图片分离,就无法还原绝对像素坐标。

3.4 一张表看透三种格式的坐标系

格式文件形式框坐标表示坐标基准类别表示是否依赖图片尺寸
VOCXMLxmin, ymin, xmax, ymax绝对像素字符串类名是(size节点记录)
COCOJSONx, y, width, height绝对像素数字类别ID是(images表记录)
YOLOTXTcx, cy, w, h归一化相对值数字类别ID是(转换时需要知道尺寸)

所以格式互转的本质就是:把同一坐标系下的框描述换成另一种坐标系下的描述。绝对转绝对(VOC转COCO)最轻松,只要拆解和重组字段;绝对转归一化(VOC/COCO转YOLO)需要读出图片宽高做除法;归一化转绝对(YOLO转VOC/COCO)必须知道图片尺寸才能还原,这是回转换最易踩坑的点。

4. 格式互转的完整代码:从VOC到YOLO、从COCO到YOLO

说完了原理,下面直接给代码。我给的是经过多个项目验证的转换函数,不是网上那种只能跑通小数据集的玩具脚本,包含了边界值防错和明显脏数据的过滤。

4.1 转换前必须做的数据消毒

转换脚本执行前,先跑一遍数据消毒。我见过大量转换后训练报错、或者loss异常飙升的案例,最后定位下来几乎都是源标注文件里有"脏数据":xmax和xmin相等(目标退化成一个点)、坐标超出图片边界、一张图里类别名写错了、某个image_id在images表中找不到。这些在转换前过滤掉,能省下后面排查的非常多时间。

def sanitize_voc_annotation(root, img_w, img_h): """清洗单个VOC对象,返回合规的bbox或None""" xmin = float(root.find('bndbox/xmin').text) ymin = float(root.find('bndbox/ymin').text) xmax = float(root.find('bndbox/xmax').text) ymax = float(root.find('bndbox/ymax').text) xmin, xmax = sorted([xmin, xmax]) ymin, ymax = sorted([ymin, ymax]) if xmax - xmin <= 1 or ymax - ymin <= 1: return None xmin = max(0, min(xmin, img_w - 1)) xmax = max(0, min(xmax, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) ymax = max(0, min(ymax, img_h - 1)) return xmin, ymin, xmax, ymax

这套消毒策略的核心是:先判断目标框是否退化,再暴力截断边框到图像边界内。注意这里用img_w - 1而不是img_w,因为像素坐标是从0开始数的,最右一个像素的坐标是width - 1。

4.2 VOC转YOLO的完整实现

import os import cv2 import xml.etree.ElementTree as ET def voc_to_yolo(xml_dir, img_dir, out_dir, class_names): os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue xml_path = os.path.join(xml_dir, xml_file) tree = ET.parse(xml_path) root = tree.getroot() filename = root.find('filename').text img_path = os.path.join(img_dir, filename) img = cv2.imread(img_path) if img is None: print(f"[skip] 图片不存在或无法读取: {img_path}") continue img_h, img_w = img.shape[:2] txt_name = os.path.splitext(xml_file)[0] + '.txt' txt_path = os.path.join(out_dir, txt_name) lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in class_names: print(f"[warn] 未映射的类名 {name} 在 {xml_file} 中") continue cls_id = class_names.index(name) box = sanitize_voc_annotation(obj, img_w, img_h) if box is None: continue xmin, ymin, xmax, ymax = box cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(txt_path, 'w') as f: f.write('\n'.join(lines)) print(f"[done] VOC转YOLO完成,输出目录: {out_dir}")

这段代码里有几个决定"一次搞对"的细节:读图片用cv2.imread而不是直接从XML的size节点拿宽高,因为XML里的尺寸可能和实际图片不一致(resize过、被压缩软件改过都可能导致对不上);归一化时用img_w做分母,而不是img_w - 1,这是归一化坐标和整数像素坐标约定的差异,很多人在这里纠结半天,统一用img_w就能保持YOLO训练的一致性。

4.3 COCO转YOLO的要点与代码

COCO转YOLO比VOC转YOLO多一层嵌套结构,必须先建立image_id到file_name的映射,再遍历annotations去填充每个文件的行。

import json import os import cv2 def coco_to_yolo(coco_json_path, img_dir, out_dir, sort_by_name=True): os.makedirs(out_dir, exist_ok=True) with open(coco_json_path, 'r') as f: coco = json.load(f) img_id_to_file = {} img_id_to_size = {} for img in coco['images']: img_id = img['id'] img_id_to_file[img_id] = img['file_name'] img_id_to_size[img_id] = (img['height'], img['width']) cat_id_to_cls = {} for cat in coco['categories']: cat_id_to_cls[cat['id']] = cat['name'] img_anns = {} for ann in coco['annotations']: image_id = ann['image_id'] img_anns.setdefault(image_id, []).append(ann) for img_id, anns in img_anns.items(): file_name = img_id_to_file[img_id] img_path = os.path.join(img_dir, file_name) img = cv2.imread(img_path) if img is None: print(f"[skip] 图片不存在: {img_path}") continue actual_h, actual_w = img.shape[:2] exp_h, exp_w = img_id_to_size[img_id] if actual_w != exp_w or actual_h != exp_h: print(f"[warn] 尺寸不匹配 {file_name}: JSON记录({exp_w},{exp_h}) 实际({actual_w},{actual_h})") lines = [] for ann in anns: if ann.get('iscrowd', 0) == 1: continue cat_id = ann['category_id'] if cat_id not in cat_id_to_cls: continue cls_name = cat_id_to_cls[cat_id] # 用类别名做映射中间层 cls_id = sorted(cat_id_to_cls.values()).index(cls_name) x, y, w, h = ann['bbox'] cx = (x + w / 2) / actual_w cy = (y + h / 2) / actual_h nw = w / actual_w nh = h / actual_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") txt_name = os.path.splitext(file_name)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines)) print(f"[done] COCO转YOLO完成,输出目录: {out_dir}")

COCO转YOLO最容易搞错的点是category_id的映射。COCO的类别ID往往不是从0开始的连续整数(比如是1、3、5),而YOLO要求类别ID必须是从0开始连续索引。上面代码用sorted(cat_id_to_cls.values()).index(cls_name)把类别名排序后取索引,保证了无论COCO原始ID多么乱,输出YOLO的ID始终是从0开始按字母序排的。

4.4 YOLO转VOC / YOLO转COCO的回转换

YOLO转VOC或COCO,核心难点是:YOLO的txt不携带图片尺寸,必须先定位到对应的图片文件读取宽高。所以回转换之前必须确保JPEGImages(或images目录)里面的图片存在,否则转出的绝对坐标一定是错的。图片在可以由脚本自动对齐,图片丢了只能手动清理数据。

import os import cv2 def yolo_to_voc(txt_dir, img_dir, xml_out_dir, class_names): os.makedirs(xml_out_dir, exist_ok=True) for txt_file in os.listdir(txt_dir): if not txt_file.endswith('.txt'): continue base = os.path.splitext(txt_file)[0] img_path = None for ext in ['.jpg', '.jpeg', '.png', '.bmp', '.webp']: candidate = os.path.join(img_dir, base + ext) if os.path.exists(candidate): img_path = candidate break if img_path is None: print(f"[error] 找不到图片: {base}") continue img = cv2.imread(img_path) img_h, img_w = img.shape[:2] annotation = ET.Element('annotation') ET.SubElement(annotation, 'folder').text = 'JPEGImages' ET.SubElement(annotation, 'filename').text = os.path.basename(img_path) size = ET.SubElement(annotation, 'size') ET.SubElement(size, 'width').text = str(img_w) ET.SubElement(size, 'height').text = str(img_h) ET.SubElement(size, 'depth').text = '3' with open(os.path.join(txt_dir, txt_file), 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f"[warn] 异常行 {txt_file}: {line.strip()}") continue cls_id = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) if cls_id >= len(class_names): print(f"[error] 类别ID越界 {txt_file}: {cls_id}") continue xmin = int(round((cx - w / 2) * img_w)) ymin = int(round((cy - h / 2) * img_h)) xmax = int(round((cx + w / 2) * img_w)) ymax = int(round((cy + h / 2) * img_h)) xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) obj = ET.SubElement(annotation, 'object') ET.SubElement(obj, 'name').text = class_names[cls_id] ET.SubElement(obj, 'pose').text = 'Unspecified' ET.SubElement(obj, 'truncated').text = '0' ET.SubElement(obj, 'difficult').text = '0' bbox = ET.SubElement(obj, 'bndbox') ET.SubElement(bbox, 'xmin').text = str(xmin) ET.SubElement(bbox, 'ymin').text = str(ymin) ET.SubElement(bbox, 'xmax').text = str(xmax) ET.SubElement(bbox, 'ymax').text = str(ymax) xml_tree = ET.ElementTree(annotation) xml_path = os.path.join(xml_out_dir, base + '.xml') xml_tree.write(xml_path, encoding='utf-8', xml_declaration=True) print(f"[done] YOLO转VOC完成,输出目录: {xml_out_dir}")

注意回转换时用int(round(...)),不是直接int(...)截断。比如归一化坐标cx=0.5、图片宽1920、目标宽0.1时,(0.5 + 0.1/2) * 1920得到1056.0,看起来整除了,但更多时候结果是1056.000001这种浮点误差值,直接截断会少1个像素。系统性少1个像素对训练影响不大,但逼死强迫症也不值当,round到底。

4.5 类别文件管理

不管做哪个方向的转换,最后都要生成一份和标注文件配套的classes.txt。YOLO训练时这个文件的顺序就等于类别ID顺序,改一个顺序就相当于把所有标注的ID全改了,模型等于重训。我见过有人直接把网上的VOC 20类classes.txt套到自己数据集上,结果训练时类别标签全错,mAP直接掉到地平线。

我建议把类别顺序管理上升到项目根目录下的一个唯一配置文件里,所有转换脚本都从它读顺序。比如:

person bicycle car motorbike traffic_light

这个文件一旦定稿,就不要轻易变更顺序。如果非要加类别,只能往后追加;如果非要删类别,就重新生成全量标注文件,并且重新验证一轮。

5. 转换完不等于能用:可视化验证是最后一道防线

转换脚本跑完,文件都生成了,很多人的第一反应就是直接丢进训练脚本。这是最容易中招的时刻。转换过程中可能出现各种隐蔽问题,比如类别顺序乱了、归一化坐标错了一位、某张图的目标全被过滤掉变成空txt。不验证直接训练,轻则mAP低得很稳定,重则训练几轮就loss爆掉。

5.1 画框验证是成本最低的手段

最直观的验证方式是把转换后的标注画在原图上,人眼扫一遍就知道有没有问题。下面这段代码把YOLO的txt叠加到图片上:

import cv2 import os def draw_yolo_boxes(img_path, txt_path, class_names): img = cv2.imread(img_path) img_h, img_w = img.shape[:2] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, cx, cy, w, h = map(float, parts) x1 = int((cx - w / 2) * img_w) y1 = int((cy - h / 2) * img_h) x2 = int((cx + w / 2) * img_w) y2 = int((cy + h / 2) * img_h) color = (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) label = class_names[int(cls_id)] cv2.putText(img, label, (x1, max(20, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return img

跑一遍所有图片,随机抽几十张出来看,重点检查三件事:框是否紧贴目标轮廓、框是否越出图像边界、类别标签和框内目标是否一致。肉眼验证虽然土,但在数据集质量检查上是无可替代的一环。

5.2 反向转换验证法

一个我特别推荐的方法:正向转换后,反向转回去,再把反向转换后的坐标和原始标注比一次,算IoU。如果平均IoU低于0.98,说明某个环节有尺度过不去的误差。这个方法能自动化批量检测,适合大型数据集。

核心思路是以YOLO为中间桥梁:VOC转YOLO再转回VOC,理论上坐标应当几乎不变(只有归一化和反归一化的舍入误差)。下面这段就是比较的核心逻辑:

def box_iou(box_a, box_b): x1 = max(box_a[0], box_b[0]) y1 = max(box_a[1], box_b[1]) x2 = min(box_a[2], box_b[2]) y2 = min(box_a[3], box_b[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area_a = (box_a[2] - box_a[0]) * (box_a[3] - box_a[1]) area_b = (box_b[2] - box_b[0]) * (box_b[3] - box_b[1]) union = area_a + area_b - inter return inter / union if union > 0 else 0

比对时若某个目标的IoU低于0.95,就把它连同原图一起输出到排查目录。这个自动化检查能一次性揪出所有"转完悄悄变了"的问题。

5.3 容易漏的三类问题

第一类是重复类别名。VOC里同一个目标被标了两次,XML里有两个<object>节点用同一个名字,坐标完全一样。YOLO格式里看不出问题,但训练时这个目标被当成两个正样本,梯度被拉了两次,影响虽小却白白浪费显存。转换脚本里可以加一个按(name, xmin, ymin, xmax, ymax)去重的逻辑。

第二类是空白txt文件。如果VOC里有目标的图片转出来txt是空的,大概率是类名不在映射表里被脚本跳过了。这种情况图片还在,标签确实空的,训练时这张纯背景图会被当成负样本。少量负样本没问题,如果大量图片的txt全空,训练集阳性比例失衡,模型会倾向于输出空检测。转换脚本跑完统计一下有多少个txt文件是0字节,超过图片总数的5%就得查。

第三类是标注文件与图片文件名不对齐。VOC里<filename>节点写的是001.jpg,实际文件叫IMG_001.jpg,或大小写不一致,跨平台迁移时路径全断。上面代码里我直接拿filename去找图,就是在处理这类问题——找不到图宁可跳过,也不要生成标着错误路径的标注。

5.4 数据集划分的一个隐蔽坑

转完格式后要划分train/val,这时有一个特别隐蔽的坑:直接从视频抽帧得到的数据集,如果按随机比例划分训练集和验证集,同一场景连续帧的相似图片可能同时出现在两边,造成验证集信息泄漏,验证集mAP虚高。划分前需要先按视频片段ID做分组,同一片段的图片分到同一个集合,再在组级别做划分。这个思路也适用于有先后时间戳的连续监控数据。我见过一个项目,验证集mAP高达0.98,上线后实际场景只有0.6,查了半天就是这个问题。

划分完还要检查每个集合里各类别的分布是否均衡。用一个简单的统计脚本输出train和val各自的类别数量矩阵,如果某个类在验证集里样本只有个位数,就要考虑按类别补充或调整划分种子。

做数据集全流程这活儿,七分在前期规划,三分在工程执行。把收集覆盖度、标注规范、坐标体系三个地基打牢,格式互转就是一道机械的算术题,再也不会半夜被一句"格式不对"拉起来加班。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询