news 2026/10/14 12:56:19

目标检测数据集标注校验与类别不平衡处理:从VOC/YOLO格式到训练避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
目标检测数据集标注校验与类别不平衡处理:从VOC/YOLO格式到训练避坑

简介:面向目标检测任务的专业工具数据集,包含钳子、剪刀、螺丝刀三类目标,共3668张真实场景图片,并已匹配Pascal VOC与YOLO两种标注格式,可直接用于YOLO系列、Faster R-CNN等模型训练与评估。压缩包共2000个文件,以VOC格式XML标注文件为主(1999个),另附说明文档,整体约83.44MB,目录结构清晰,便于导入训练脚本和格式转换。数据集标注总量3686个边界框,其中螺丝刀1712框、钳子1568框、剪刀406框,各类别分布较均衡,适合工业工具检测、智能仓储或安防场景的算法验证。目前已有413人学习下载,既适合目标检测初学者理解VOC/YOLO标注规范,也能为有经验的开发者提供现成的模型调优与性能对比样本。

1. 3668张钳子剪刀螺丝刀目标检测数据集:先看框数分布再决定怎么训练

目标检测里,工具类数据集的坑往往不在算法,而在类别分布上。这份3668张图片的钳子、剪刀、螺丝刀检测数据集覆盖pliers、scissors、screwdrivers三类手持工具,VOC与YOLO格式一次配齐,解压就能进训练管线。我先翻了一遍框数台账:screwdrivers有1712个框,pliers有1568个框,scissors只有406个框,比例接近4比4比1,剪刀类别明显偏少。做工地工具清点、维修工位漏检或相关教学实验的人,拿到数据第一件事不该是调学习率,而是正视样本不平衡:直接训练,很容易在剪刀那一类掉点。数据里没有额外的路径清单文件,主体就是jpg、xml、yolo的txt,结构干净,既适合拿来练手VOC与YOLO格式转换,也适合当迁移学习和数据增强的实验基底。

2. 解压后的两份标注:VOC的xml与YOLO的txt,逐字段拆开看

先把解压后的目录看明白。压缩包打开以后,主体是一批jpg、一批xml、一批txt。这三个后缀对应同一批图片:jpg是原始画面,xml是labelImg保存的VOC标注,txt是YOLO训练时吃的归一化标签。很多第一次用这类数据集的人会误以为txt是路径清单,打开一看是坐标行,然后愣住。这里把预期校正过来:三个后缀各3668个,按同名文件一一对应,txt里是YOLO格式的框坐标。

还需要注意,这个数据集没有额外提供train.txt、val.txt这类路径划分文件,也没有帮你按比例分好训练集和验证集。动手训练之前,要自己写个划分脚本,按7比2比1或8比2切。这个步骤不算难,但如果从解压到训练之间只做了数据复制、没做划分,整个训练过程都是在“全量数据”上自娱自乐,val指标会好看到失真。

2.1 文件构成与命名规律:jpg、xml、txt一一对应

文件命名沿用了同一套前缀规律,比如压缩包里可以看到xyxr_tools_617.xml、xyxr_tools_455.xml这样的文件,对应同目录下的xyxr_tools_617.jpg和xyxr_tools_617.txt。编号不连续,中间有跳号,这是采集和筛选过程中常见的结果,不影响使用;真正影响使用的是文件名对不上。

我把文件清单整理成一张表:

文件后缀数量内容说明
.jpg3668原始图像,尺寸不完全统一
.xml3668VOC标注,记录图像尺寸、类别名、矩形框像素坐标
.txt3668YOLO标注,每行一条记录,固定五个数字

校验文件完整性时,不要只看总数相等。我在交付检查里一般会写一段脚本,把三个后缀的基准名取出来求差集;如果出现xxx.jpg存在但xxx.xml缺失的情况,这张图会在训练时被静默跳过,导致你统计的样本数和实际参与训练的数不一致。这类问题用眼睛翻目录很难发现,脚本跑一遍最稳。

这份数据的类别一共三个,名字是pliers、scissors、screwdrivers。总框数为3686,图片总数是3668,两者只差18,说明大部分图片里只有一个目标。这个判断对后续策略影响很大:它意味着数据结构偏“单目标检测”,如果实际部署场景是工具箱内多件工具互相叠放,你的增强策略和anchor设计都要单独调整,而不能拿一套通用配置硬套。

训练目录的整理方式,我一般会这样建:在数据根目录下创建images/train、images/val、labels/train、labels/val四个子目录,jpg放进images下对应目录,txt放进labels下对应目录,xml作为原始标注归档单独存放。后续不管用哪个YOLO系列框架,目录结构基本都是这个套路。

2.2 xml字段拆解:folder、size、object、bndbox都是什么

VOC格式的标注,标准结构并不复杂。labelImg保存出来的xml大体长这样:

<annotation> <folder>xyxr_tools</folder> <filename>xyxr_tools_617.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>pliers</name> <bndbox> <xmin>120</xmin> <ymin>180</ymin> <xmax>912</xmax> <ymax>648</ymax> </bndbox> </object> </annotation>

注意这里只是结构示意,某张图的真实坐标和尺寸以文件里的实际值为准。把这套字段读熟,后面对接任何VOC转YOLO脚本都不虚。

逐个字段说:folder是labelImg保存时所在的文件夹名,不可靠,因为压缩包解压后目录名可能变了;filename是图片文件名,重命名图片时必须同步修改;size里的宽度、高度、通道数最关键,YOLO坐标归一化必须知道原图宽高;object可能有多段,代表一张图多个目标;name是类别字符串;bndbox里的xmin、ymin、xmax、ymax,是矩形左上角和右下角的像素坐标。

这里有个容易被忽略的细节:xml里的坐标是整数。labelImg标注时记录到像素级,看起来精确实在,但如果你后续对图像做过缩放或裁切,整数坐标不会自动跟着变。另一个细节是xmax、ymax到底包不包含边界像素,多数开源转换脚本直接拿(xmax-xmin)除以宽得到w,边界差1像素在训练里影响很小,不值得纠结。真正需要纠结的是xmin大于xmax这类明显错误,后面章节的检查脚本会专门拦。

2.3 yolo txt拆解:类别ID、中心坐标和归一化换算

YOLO格式的txt比xml更简练,每一行代表一个目标,固定5个数:

0 0.4750 0.5000 0.1400 0.2400

从左到右依次是类别ID、归一化中心点x、中心点y、归一化宽度w、归一化高度h。所有坐标都用原图宽高做了除法,所以数值都在0到1之间,跟你最终输入给网络的图片尺寸没有直接关系。上面这行可以理解为:类别0,中心点在图的47.5%宽度、50%高度处,目标宽占整张图的14%,高占24%。

从xml到yolo的换算是固定的公式,我习惯这样记:

  • cx = (xmin + xmax) / 2 / width
  • cy = (ymin + ymax) / 2 / height
  • w = (xmax - xmin) / width
  • h = (ymax - ymin) / height

类别ID则按类别表顺序来。这份数据的三类名称是pliers、scissors、screwdrivers,所以txt里的0、1、2对应这三个名字。实际配置yaml文件时,names顺序一定要和txt里的ID保持一致。只要这里错一位,训练出来的结果就是类别标签错乱,这种错误比不收敛还难排查,因为loss照样下降,推理时钳子会被标成螺丝刀,你还在那儿反复调学习率。

3. 写个Python体检脚本:把3668张图的标注全面查一遍

在把数据交给训练脚本之前,我会先做一次标注体检。这个习惯是从一次翻车经历里养出来的:某次我拿一套类似数据直接训练,跑了100轮才发现有几十张图的yolo txt坐标是负数,模型在那里埋头收敛,val指标却一直乱跳。后来把标注全量检查一遍,问题一目了然。这里给出三个脚本,分别对付xml、txt和可视化。

3.1 读xml清点账目:类别框数、图片尺寸、异常框一起输出

第一段脚本只依赖Python标准库,不需要额外安装依赖:

import os import glob import xml.etree.ElementTree as ET xml_dir = "/path/to/xyxr_tools" # 改成你解压后的xml所在目录 stats = {} size_list = [] bad_boxes = [] for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() w_img = int(root.findtext("size/width")) h_img = int(root.findtext("size/height")) size_list.append((w_img, h_img)) for obj in root.iter("object"): name = obj.findtext("name") box = obj.find("bndbox") xmin = int(box.findtext("xmin")) ymin = int(box.findtext("ymin")) xmax = int(box.findtext("xmax")) ymax = int(box.findtext("ymax")) stats[name] = stats.get(name, 0) + 1 if xmax <= xmin or ymax <= ymin: bad_boxes.append((os.path.basename(xml_path), name, "宽或高为0/负值")) if xmin < 0 or ymin < 0 or xmax > w_img or ymax > h_img: bad_boxes.append((os.path.basename(xml_path), name, "坐标越出图像边界")) print("各类别框数:", stats) print("图片总数:", len(size_list)) print("图片最大宽度/高度:", max(s[0] for s in size_list), max(s[1] for s in size_list)) print("异常框数量:", len(bad_boxes)) for item in bad_boxes[:50]: print(item)

脚本逻辑是遍历所有xml,取出尺寸和每个object的坐标,再分两类做异常判断。这里重点说两个参数:root.findtext用的xpath路径在标准VOC结构里都能命中,比一层层find再find利索很多;异常判断条件把“宽或高为0/负值”和“坐标越出图像边界”分开记录,是因为前者往往来自人工标注手滑,后者往往来自图像在标注后被无脑压缩或裁切。执行完要看两类输出:类别框数台账和异常框列表。如果bad_boxes超过几十条,先修这批再说训练的事。

3.2 读txt查格式:越界坐标、空文件、多余字段拦在训练之前

第二段脚本检查YOLO格式,代码也很短:

import os import glob label_dir = "/path/to/xyxr_tools_txt" # 改成你的yolo标签目录 issues = [] for txt_path in glob.glob(os.path.join(label_dir, "*.txt")): with open(txt_path, "r", encoding="utf-8") as f: lines = f.read().strip().splitlines() if not lines: issues.append((os.path.basename(txt_path), "空文件,无标注")) continue for line_no, line in enumerate(lines, 1): parts = line.split() if len(parts) != 5: issues.append((os.path.basename(txt_path), line_no, f"字段数={len(parts)}")) continue cls_id, cx, cy, w, h = parts[0], float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) if cls_id not in ("0", "1", "2"): issues.append((os.path.basename(txt_path), line_no, f"非法类别ID={cls_id}")) if not (0.0 <= cx <= 1.0 and 0.0 <= cy <= 1.0): issues.append((os.path.basename(txt_path), line_no, f"中心坐标越界{cx:.4f},{cy:.4f}")) if w <= 0.0 or h <= 0.0 or w > 1.0 or h > 1.0: issues.append((os.path.basename(txt_path), line_no, f"宽高异常w={w:.4f},h={h:.4f}")) print("发现问题条数:", len(issues)) for item in issues[:100]: print(item)

这段脚本的关键参数是类别ID白名单0、1、2,以及坐标有效区间0到1闭区间。实际转换时偶尔会有某行用逗号分隔坐标,split()默认按空白切分,逗号会让字段数变成1,同样能拦下来。空文件在这里单独报一条,是因为多数检测框架会把空txt当作背景图片处理,并不报错,容易漏。所有坐标是否越界,是YOLO训练前最该查的一项,回归头遇到越界标注轻则loss波动,重则一整轮训练白干。

3.3 可视化抽查:把框画回图片,看标注是否贴合工具轮廓

数值检查只能发现坐标层面的硬错误,标注是否贴合还得靠眼睛看。这段脚本抽取一小部分图片,把xml里的矩形框画回去:

import os import glob import random import cv2 import xml.etree.ElementTree as ET img_dir = "/path/to/xyxr_tools_jpg" # 图片目录 xml_dir = "/path/to/xyxr_tools_xml" # xml目录 out_dir = "./check_vis" os.makedirs(out_dir, exist_ok=True) color_map = { "pliers": (0, 0, 255), "scissors": (0, 255, 0), "screwdrivers": (255, 0, 0), } xml_list = glob.glob(os.path.join(xml_dir, "*.xml")) sample = random.sample(xml_list, min(30, len(xml_list))) for xml_path in sample: base = os.path.basename(xml_path).replace(".xml", "") img = cv2.imread(os.path.join(img_dir, base + ".jpg")) if img is None: print("图片缺失:", base) continue root = ET.parse(xml_path).getroot() for obj in root.iter("object"): name = obj.findtext("name") box = obj.find("bndbox") x1 = int(box.findtext("xmin")); y1 = int(box.findtext("ymin")) x2 = int(box.findtext("xmax")); y2 = int(box.findtext("ymax")) color = color_map.get(name, (0, 255, 255)) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, name, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(os.path.join(out_dir, base + "_check.jpg"), img) print("输出目录:", out_dir)

这里用random.sample从全部xml里抽30张,避免只看到前几张相似场景。OpenCV画框和写文字都很直白,参数里值得一提的只有max(0, y1-5):当矩形框贴着图像顶部时,文字不会跑到画面外面被截断。如果抽查发现多张图的框明显松松垮垮,把一大片背景包进目标里,就要考虑是标注时把工具连同远处的余料一起框了;这类数据喂给模型,学出来的边界会跟着歪,之后部署时框的位置会飘。

4. 避坑:从解压到训练之前最容易翻车的5个点

这部分把我在类似工具检测数据上踩过的坑直接列出来,每条按现象、原因、解决展开。读的时候可以对照自己手上的场景,命中哪条就优先处理哪条。

4.1 剪刀只有406框:类别不平衡直接拉低AP

现象:训练中期开始,pliers和screwdrivers的AP稳步上升,scissors的AP卡在低位,甚至随epoch增加出现轻微下降。

原因:数据里scissors框数只有406,占全部3686框的11%,而screwdrivers占46%、pliers占42.5%。模型每轮迭代看到的剪刀样本太少,梯度贡献被多数类淹没,等于一直在“少数服从多数”,剪刀边界始终学不扎实。

解决:先不谈换模型,最简单的做法是在loss里对三个类别按1/框数的比例做加权,让少数类的贡献不被稀释。在此基础上再做有倾向性的数据增强:把剪刀图片随机旋转、亮度抖动、镜像翻转,让同一个标注点以更多形态出现。再不够就用复制粘贴策略,把剪刀目标贴到背景或工具盒场景里,把数量补到千框量级后再看效果。

4.2 标签名里的复数:screwdrivers与screwdriver一字之差全盘错位

现象:训练时日志里显示的各类目标数量正常,loss下降也正常,但推理时螺丝刀总是被标成钳子,或者输出类别顺序全乱。

原因:类别名在原始xml里写的是screwdrivers,词尾有s。手写yaml时很容易习惯性写成screwdriver,类别表和txt里面的class id对不上。YOLO的names顺序一旦错位,class 2在模型输出里就被映射成错误的名称字符串。

解决:以数据原始xml中的name为准,不要靠记忆拼写。写个小脚本从全部xml里提取不重复的name:

import glob import xml.etree.ElementTree as ET names = set() for xml_path in glob.glob("/path/to/xyxr_tools/*.xml"): root = ET.parse(xml_path).getroot() for obj in root.iter("object"): names.add(obj.findtext("name")) print(sorted(names))

输出三个名字,直接复制粘贴到配置文件里,不手打。这个习惯能省掉大量排查时间,也顺带把单复数问题消掉。

4.3 金属反光和长柄形状:细长工具最容易漏检

现象:val集图片里,螺丝刀横放在桌面上,背景又有纹理和反光时,模型输出置信度很低,甚至完全没有框出来。

原因:这类长条形工具在图像里表现为大长宽比的矩形,背景占比高,金属反光边缘又和附近物体反光融在一起,检测头很难确定边界。矩形框本身对这种细长目标就不友好,框稍微紧一点,背景信息就压过了前景。

解决:训练时把输入分辨率提上去,从640提到1280往往有立竿见影的效果,长条目标的像素信息更完整。增强参数里把亮度、对比度抖动加大,模拟不同光源条件。推理阶段还可以做多尺度预测,对低置信度结果放宽一点NMS阈值,但先查分辨率,这个手段最直接。

4.4 钳子和螺丝刀交叉摆放:矩形框重叠之后的NMS难题

现象:一张照片里钳子和螺丝刀交叉,标注时画了两个互相重叠的大矩形,训练收敛后,后处理把两个框合并成一个,输出只剩一个目标。

原因:矩形框表达不了交叉目标的真实轮廓。重叠区域过大时,NMS按IoU阈值把另一个框当作重复框抑制掉;两个类别的回归头各自输出框,又都在同一区域激活,后处理一压就只剩一个。

解决:如果应用场景就是工具箱里的散放工具,建议把这类交叉图单独抽出来,改成旋转检测,或者先做实例分割生成mask,再转成旋转框。坚持用水平框的话,要针对这类场景扩大样本量,并使用MixUp增强,光靠通用参数是压不下去的。这份数据单目标图片占多数,交叉图不会太多,但只要有,就必须在val集里单独统计,否则漏检在评测时被平摊掉,你会误以为模型表现不错。

4.5 letterbox缩放后忘了重算坐标:检测框整体偏移

现象:训练和推理都使用640x640输入,发现预测框中心点整体向下或向右偏,图像边缘有灰边时尤其明显。

原因:letterbox缩放会先按长宽比等比缩小图像,再在较短的边上补灰条。目标在原始图片上的归一化坐标,和新画布上的坐标系已经不是一回事。如果直接从txt读原始坐标而不补偿偏移量,框在新画布上就整体错位了。

解决:不重复造轮子的做法,是使用框架内置的letterbox逻辑,它返回图像时会同时返回缩放比例和填充偏移,把标签同步平移缩放即可。自己写预处理的话,给标签也加一个同样的变换函数。检查这份数据时,可以先手工选一张宽图和一张高图,把letterbox前后的框位置对比一遍,确认脚本没有写错再喂进训练。

5. 落地上手:先算先验框,再写一份能直接跑的训练配置

数据校验通过以后,下一步不是立刻开训,而是先用标注统计结果把先验框算出来。这一步经常被跳过,尤其很多人直接沿用默认anchor,等到回归头学不动才开始怀疑数据。

5.1 从标注里统计bbox宽高,粗略估算先验框

用之前的xml体检脚本顺手把宽高收集起来,用numpy看分布:

import numpy as np import glob import xml.etree.ElementTree as ET ratios = [] for xml_path in glob.glob("/path/to/xyxr_tools/*.xml"): root = ET.parse(xml_path).getroot() w_img = int(root.findtext("size/width")) h_img = int(root.findtext("size/height")) for obj in root.iter("object"): box = obj.find("bndbox") w = (int(box.findtext("xmax")) - int(box.findtext("xmin"))) / w_img h = (int(box.findtext("ymax")) - int(box.findtext("ymin"))) / h_img ratios.append((w, h)) arr = np.array(ratios) print("框数:", len(arr)) print("宽高均值:", arr.mean(axis=0)) print("宽高中位数:", np.median(arr, axis=0)) print("宽高比均值:", (arr[:, 0] / arr[:, 1]).mean())

从工具检测的场景看,多数工具横放,框整体偏长条状。中位数和均值跑出来以后,再用YOLO系列框架内置的聚类脚本跑一遍,得到anchor会更贴合这个数据集。如果你的图片里螺丝刀立着放的比例高,宽高比均值会明显靠近1,那就以聚类结果为准,不要凭印象写死。

5.2 一份yaml参考配置与训练前检查顺序

数据切换成训练目录后,配置文件写成这样:

path: ./xyxr_tools train: images/train val: images/val nc: 3 names: 0: pliers 1: scissors 2: screwdrivers

path改成解压后的实际根目录,train和val先要把数据按比例划开。划分的时候坚持三个后缀一起移动或复制,只拷jpg不拷xml,之后再想转格式或者重新可视化就要返工。最后按固定顺序过一遍:先跑xml异常检查脚本,再跑txt校验,再做可视化抽查,最后算先验框。

从那以后,我每次拿到别人整理好的数据集,都强制先走一遍这三段体检脚本,再谈网络结构和超参。数据校验这个步骤看起来不起眼,却能省掉训练跑半天才发现标签错位、框越界的返工成本。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 12:55:58

机床侧测量工装的规划(上)位置选择与空间安排

机床侧测量工装的规划&#xff08;上&#xff09;&#xff1a;位置选择与空间安排 引言 机床上加装测头和对刀仪之后&#xff0c;很多人会把注意力集中在设备本体的选型、信号接入和宏程序调试上&#xff0c;而把工作区里那些“附属件”当作现场随手就位的零碎活儿。实际情况恰…

作者头像 李华
网站建设 2026/10/14 12:55:40

HAProxy+Keepalived高可用负载均衡集群架构设计与实战避坑

1. 高可用负载均衡集群的架构设计与选型思考1.1 为什么单台负载均衡器会成为整个系统的命门很多团队在搭建服务集群时&#xff0c;第一反应是买一台性能不错的服务器装上负载均衡软件&#xff0c;把流量分发到后端几台应用服务器上&#xff0c;觉得这就万事大吉了。我早期也这么…

作者头像 李华
网站建设 2026/10/14 12:55:02

线性回归实现PM2.5预测:从数据清洗到滚动预测全流程解析

简介&#xff1a;基于线性回归的PM2.5预测系统Python源码&#xff0c;面向环境数据分析、机器学习入门及课程设计人群&#xff0c;解决从原始监测数据到浓度预测的完整回归建模问题。压缩包共19个文件&#xff0c;以12个csv数据文件为主体&#xff0c;覆盖训练集、测试集及特征…

作者头像 李华
网站建设 2026/10/14 12:54:49

电力系统同步相量计算算法对比与工程实践

1. 电力系统同步相量计算的核心价值 电力系统同步相量测量是智能电网运行控制的基础技术。当我在某省级调度中心第一次见到相量测量单元(PMU)的实时数据刷新时&#xff0c;瞬间理解了这项技术的革命性意义——它让我们首次获得了电网动态过程的"显微镜"。 传统SCADA…

作者头像 李华
网站建设 2026/10/14 12:53:37

机器人空间感知进阶:地图中间件与自然语言查询实战

接到 lingbot-map 这个标题&#xff0c;我的第一反应是&#xff1a;这不只是一个起得挺随意的内部代号&#xff0c;而是一个很典型的“给智能助手补上空间感知能力”的项目。说白了&#xff0c;就是在机器人对话、任务调度的逻辑里&#xff0c;插入一层地图服务中间件&#xff…

作者头像 李华
网站建设 2026/10/14 12:53:36

运行时错误分析工具REA:日志归因与告警收敛实战

在线上服务兜兜转转做了几年&#xff0c;最怕的其实不是需求改来改去&#xff0c;而是深更半夜手机突然震动&#xff1a;线上某个服务报错率飙升&#xff0c;日志翻了几页也没定位到根因。这种问题多了以后&#xff0c;我决定写一个小工具&#xff0c;代号就叫 REA&#xff08;…

作者头像 李华