十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VOC与YOLO标注格式转换实战:黄鼠狼数据集制作与训练前检查

VOC与YOLO标注格式转换实战:黄鼠狼数据集制作与训练前检查 简介黄鼠狼目标检测数据集面向目标检测研究者与算法初学者提供一批经过精细标注的真实图像资源。数据集共收录427张黄鼠狼jpg图片对应427个xml标注文件与427个txt标注文件同时支持VOC和YOLO两种主流格式可直接衔接YOLO系列、Faster R-CNN等常见检测框架免去自行整理与格式转换的麻烦。所有标注类别统一为weasel由labelImg工具完成并遵循边界贴合、目标不遗漏及一致性校验等标注规范适合用于模型训练、验证集构建与算法调参练习该标注模式与主流检测框架的输入要求高度兼容能帮助使用者省去大量重复性标注时间。压缩包为rar格式整体约32.34MB内含1282个文件解压后分为jpg图片、xml标注、txt标注三个文件夹无需密码即可使用目录结构清晰便于分类加载。目前已有148人浏览学习适合需要快速获取小型目标检测数据集开展实验的开发者。1. 427张黄鼠狼数据集的定位为什么同时备好VOC和YOLO两份标注在做生态监测或养殖场防护时经常要识别黄鼠狼这种中小型食肉动物。手头攒了427张左右图片下一步是目标标注。但标注格式不是随便选的Pascal VOC的XML能记录对象名、难易、遮挡等结构化信息编辑也方便YOLO训练时只认txt每行一个类别 中心点x 中心点y 宽 高全部归一化到0到1。这两份格式不是竞争关系而是上下游VOC当母版YOLO当训练输入。我一般会先把所有图标成VOC再写脚本转成YOLO。这个顺序能减少重复劳动也让后续换算法时不至于推倒重来。适合刚拿到图像、准备做检测训练的同学。2. 制作VOC格式标注目录结构、标注工具与XML生成在动手标图之前先把VOC的目录骨架搭好。不是非要严格照搬VOC2007那种结构但如果后面要和公开数据集混用按惯例来能省掉很多路径错误。我常用的布局如下2.1 VOC目录骨架与XML字段weasel_dataset/ ├── JPEGImages/ # 427张jpg/png原图 ├── Annotations/ # 同名的xml标注 └── ImageSets/ └── Main/ # train.txt/val.txt/test.txt放图片名XML内部要包含图像尺寸和每个目标的包围框。一个标准的标注文件长这样annotation folderJPEGImages/folder filenameIMG_001.jpg/filename size width1280/width height720/height depth3/depth /size object nameweasel/name truncated0/truncated occluded0/occluded difficult0/difficult bndbox xmin345/xmin ymin128/ymin xmax612/xmax ymax390/ymax /bndbox /object /annotationXML里的folder在转YOLO时常常被忽略但保持正确的filename和size很重要。bndbox四值中xmin/ymin是左上角xmax/ymax是右下角必须是像素整数并且满足xmin xmax、ymin ymax。如果一张图里有多个黄鼠狼就会出现多个object块。下面是字段用途速查表字段含义转YOLO时的用途filename图片文件名拼接图片路径size/width/height原图宽高归一化坐标的分母object/name类别名映射成类别IDbndbox像素坐标计算中心点与宽高difficult难例标记转YOLO可忽略或过滤组织图片时要注意命名不要带空格和中文因为YOLO读取路径时对空格敏感。比如把IMG 001.jpg改成IMG_001.jpg否则后续生成txt列表后框架按空格切分会把文件名截断。2.2 用LabelImg产出XML的流程与参数最常见的做法是装一个LabelImg它直接输出PascalVOC格式的XML省去一次导出。安装和启动的命令是pip install labelImg labelImg JPEGImages/ Annotations/第一个参数是图片目录第二个参数是标注输出目录。如果是在Linux桌面环境用conda装可能需要先装pyqt5再执行否则窗口起不来。打开后先把顶部按键切到PascalVOC模式再设置自动保存。画框快捷键是W切上一张/下一张是A/D注意这个D在部分版本里是删除已选框删除键是Del。保存时会自动生成同名XML。标注427张图不是一次性能做完的事建议每标30张就抽查一次。常见问题是框住了树影或远处的半截身体。黄鼠狼体形细长尾巴占体积明显框的边界我会把尾巴尖也包进去如果只标躯干训练时学习到的目标范围会偏紧推理时往往只给出半截框。2.3 批量核查XML字段完整性的脚本手工标完先别急着转格式。写个Python脚本一次性检查所有XML里的size和box是否合理。下面这段能跑在python3无第三方依赖import xml.etree.ElementTree as ET import glob, os xml_list sorted(glob.glob(Annotations/*.xml)) for xml_path in xml_list: root ET.parse(xml_path).getroot() # 取文件名和size结点 filename root.findtext(filename) w int(root.findtext(size/width)) h int(root.findtext(size/height)) for obj in root.findall(object): name obj.findtext(name) if name not in (weasel,): print(f{xml_path}: unknown class {name}) bnd obj.find(bndbox) xmin int(bnd.findtext(xmin)); ymin int(bnd.findtext(ymin)) xmax int(bnd.findtext(xmax)); ymax int(bnd.findtext(ymax)) # 检查坐标顺序和图片边界 if not (0 xmin xmax w and 0 ymin ymax h): print(f{filename}: bad box ({xmin},{ymin},{xmax},{ymax}) in {w}x{h})这段代码用findtext的路径语法直接从XML取值省去一层层find。输出里只要出现bad box就说明边界写错了。最常见的错误是xmin和ymin填了0——LabelImg里从左上角角落开始拖框会得到0这本身合法但如果xmin等于xmax框就成了线转YOLO后会算出宽度为0训练时loss变成nan。核查时把这类极窄框也标出来用xmax - xmin 5再加一条打印。后面我在转换脚本里会做一步clip但这里的检查能更早发现问题。3. 把VOC标注转成YOLO脚本、归一化坐标与数据集划分YOLO系列v5/v8/v11都是训练时需要的标注是纯文本txt每张图片一个同名txt。与VOC不同YOLO的坐标是相对图片宽高归一化后的浮点数所以转换的关键就是计算中心点和宽高再除以图像尺寸。3.1 为什么YOLO格式只需要txtVOC的XML信息多但训练框架不直接消费XML。YOLO为了读取快把每个目标压缩成一行class_id x_center y_center width height。其中class_id从0开始对应data.yaml里的类别顺序。x_center和y_center是目标包围框中心点在图片上的相对位置width和height是框的相对宽度和高度。全部在[0,1]区间内。由于黄鼠狼数据集的图片宽度和高度各不相同可能来自不同相机不归一化就无法在一个batch里训练。从bndbox到YOLO四值的公式是x_center ((xmin xmax) / 2) / img_width y_center ((ymin ymax) / 2) / img_height box_w (xmax - xmin) / img_width box_h (ymax - ymin) / img_height先用整型的像素框计算中心再除以宽高得到浮点。有的同学会问为什么不用左上角坐标因为YOLO预测的就是中心点加宽高学习目标必须和预测目标对齐。对应关系可以看下面这个表VOC字段YOLO字段转换操作bndbox/xmin, xmaxx_center(xminxmax)/2/wbndbox/ymin, ymaxy_center(yminymax)/2/hxmax - xminwidth除以wymax - yminheight除以h3.2 从XML到txt的转换脚本下面这个脚本遍历Annotations目录输出到labels目录。它同时生成classes.txt并划分train/val。我用的是python xml.etree不需要额外库import xml.etree.ElementTree as ET import glob, os, random CLASSES [weasel] # 类别名列表顺序就是class_id os.makedirs(labels, exist_okTrue) xml_list sorted(glob.glob(Annotations/*.xml)) random.seed(42) random.shuffle(xml_list) # 先打乱再做划分 train_txt open(ImageSets/Main/train.txt, w) val_txt open(ImageSets/Main/val.txt, w) for idx, xml_path in enumerate(xml_list): root ET.parse(xml_path).getroot() filename root.findtext(filename) w int(root.findtext(size/width)) h int(root.findtext(size/height)) base os.path.splitext(filename)[0] label_lines [] for obj in root.findall(object): name obj.findtext(name) if name not in CLASSES: continue # 忽略未定义类别 class_id CLASSES.index(name) bnd obj.find(bndbox) xmin int(bnd.findtext(xmin)); ymin int(bnd.findtext(ymin)) xmax int(bnd.findtext(xmax)); ymax int(bnd.findtext(ymax)) # 坐标越界就clip到图片范围内 xmin max(0, xmin); ymin max(0, ymin) xmax min(w, xmax); ymax min(h, ymax) if xmax xmin or ymax ymin: continue # 非法框直接丢弃 x_center ((xmin xmax) / 2) / w y_center ((ymin ymax) / 2) / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h label_lines.append(f{class_id} {x_center:.5f} {y_center:.5f} {box_w:.5f} {box_h:.5f}) with open(flabels/{base}.txt, w) as f: f.write(\n.join(label_lines)) # 前90%进train后10%进val if idx int(len(xml_list) * 0.9): train_txt.write(f{base}\n) else: val_txt.write(f{base}\n) train_txt.close(); val_txt.close()脚本的逻辑先按类别表把VOC的name转成整数IDclip操作负责处理边界框越界label_lines用5位小数保留训练时完全够用。划分比例按9:1硬切如果你更在意验证集样本可改成8:2。需要注意这里是先打乱再切如果不shuffle前90%可能都是同一拍摄时段的图像场景不独立验证指标会虚高。random.seed(42)保证每次跑出来的划分一致方便复现。注意转换脚本里做clip后如果一张图中目标一半在画面外clip会保留画面内部分但这样框不完整。严格做法是直接用原坐标参与归一化让模型学习到越界框而不是手动clip。上面脚本为了保险还是做了clip实际项目里可以加个开关控制。3.3 数据集划分与文件路径约定YOLO系训练时不是直接读这些txt找图片而是通过train.txt/val.txt里的图片名或路径找到JPEGImages下的原图再读同名txt。所以这里的train.txt内容是IMG_001这种不带扩展名的base名。实际使用时ultralytics框架还支持直接给一个.txt文件每行是图片的绝对路径。为了在不同机器上都能跑我会把train.txt改成绝对路径用一条命令即可awk {print /absolute/path/weasel_dataset/JPEGImages/$0.jpg} ImageSets/Main/train.txt ImageSets/Main/train_path.txt注意如果图片是png需要把.jpg替换成.png。绝对路径方式在迁移服务器时容易失效我一般更推荐把数据集目录固定放在某个路径下再在data.yaml里用相对路径。427张图的数据量不大划分后train大约384张val约43张。对小数据集来说val集里的场景应该覆盖不同角度和光线否则验证时loss波动会很大。4. 训练前的标注体检类别映射、空标注与坐标越界的排查转完YOLO格式别急着开train。先做一轮体检。这一步能避开训练中途爆loss或mAP为0的问题。4.1 类别映射表VOC的name与data.yaml的通道一致性YOLOv8的data.yaml里categories顺序必须和转换脚本里的CLASSES顺序完全一致。比如我们classes.txt只有一行weasel那么data.yaml里就是path: /path/to/weasel_dataset train: ImageSets/Main/train.txt val: ImageSets/Main/val.txt nc: 1 names: [weasel]如果后面加了background或者其他动物转换脚本的CLASSES和data.yaml的names必须同时加且顺序相同。这里最容易出的错是在VOC XML里有的类别叫weasel有的叫weaseled一个多了字母转换时会落到class_id0或直接被跳过。建议在转换脚本跑完后统计一遍labels目录里出现的class_id分布。下面的检查表可以直接对照检查项方法判定标准类别ID一致性对比classes.txt与data.yaml两者顺序完全一致空标注ls -l labels/*.txt检查0字节文件空文件数低于图片总数5%坐标越界统计txt中浮点数范围0 值 1tiny box按原图宽高换算像素尺寸小于10px需复核4.2 统计每张图实例数与类别分布可以用一个Python脚本读取所有txt输出直方图信息。下面的命令直接统计每个类别id出现次数和空标注文件数量for f in labels/*.txt; do if [ ! -s $f ]; then echo empty: $f; fi done awk {count[$1]} END {for (id in count) print id, count[id]} labels/*.txt第一段循环用-s判断文件是否为空空文件会输出到终端。第二段awk按第一列类别ID计数。如果427张图里有30张空标注说明这些图要么是背景图要么漏标了。建议把空标注对应的图片挑出来再补标因为训练时背景图不是完全没用但如果比例太高模型会倾向输出极小的框。对黄鼠狼这种单类别数据集还可以数一下每张图平均实例数如果平均不到1.2说明很多图只有单目标模型容易学到图里只有一个目标的偏置。4.3 坐标越界与宽高为0的处理方式转换脚本虽然做了clip但clip不能解决标错位置的问题。比如一张720p的图XML里写ymax760clip后变成720框被拉到图像下边缘如果原本目标没到边缘标注就等于偏了。更隐蔽的是退化为线xmax-xmin1这种clip后仍然有宽度但视觉上就是竖线训练时让模型学到一个超扁平的框。我一般再跑一次复核把宽或高小于10像素的框挑出来看原图import os for txt in os.listdir(labels): with open(os.path.join(labels, txt)) as f: lines [l.split() for l in f.read().splitlines()] for line in lines: w float(line[3]); h float(line[4]) # 按参考分辨率估算实际像素这里以1280x720为例 if w * 1280 10 or h * 720 10: print(txt, tiny box, w, h)这里的1280和720是按常见分辨率估算的比例严格做法是从JPEGImages里读实际宽高。把这类tiny box对应原图挑出来修正VOC的XML后再重新跑一次转换而不是直接改txt。原因前面说过VOC是源头txt是派生。如果发现大量越界还要检查是不是图片本身被裁剪过比如从视频抽帧后分辨率变了但XML里还是旧尺寸这种时候要批量更新size字段而不是单个改框。5. 双格式在YOLOv8训练中的验证技巧到了这一步数据已经准备好。最后分享一个我常用的验证闭环不直接train先用YOLOv8做一次干跑确认data.yaml能被框架解析再用预测结果把标注画回图上检查。5.1 用ultralytics快速验证data.yaml与数据集安装ultralytics后运行yolo detect train dataweasel.yaml modelyolov8s.pt epochs1 batch8训练1个epoch不会产出有效模型但会在启动阶段检查数据集的路径、标签是否存在、类别数是否匹配。如果labels路径不对框架会报Dataset not found或Unable to read labels。这里有个关键参数data.yaml中的train指向的是图片路径列表文件如果不写val框架会采样一部分train做val注意这种行为在验证指标上会产生偏差。427张图的规模下建议显式把val指向一个单独的列表。5.2 可视化标注叠加与常见误区干跑通过后随机抽10张图把label画在图上肉眼检查。这个脚本直接用OpenCV画矩形import cv2, os img_dir JPEGImages; label_dir labels for img_name in os.listdir(img_dir): img cv2.imread(os.path.join(img_dir, img_name)) base os.path.splitext(img_name)[0] txt os.path.join(label_dir, base .txt) if not os.path.exists(txt): continue h, w img.shape[:2] for line in open(txt): cid, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw/2) * w); y1 int((yc - bh/2) * h) x2 int((xc bw/2) * w); y2 int((yc bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, weasel, (x1, max(0, y1-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(visual/ img_name, img)画框时注意要把归一化坐标乘回原图宽高用int()截断可能产生1像素误差但不影响检查。最常见的误区是有人直接在高分辨率原图上画然后看到框偏小以为是标注错了其实模型输入会resize成640x640框在原始尺寸下本来就偏小。真正常见的错误是坐标出现负值或者框的中心点明显偏移那就要回VOC里改XML改完重新转格式。一个实用的双格式同步技巧把转换脚本保存为voc2yolo.py每次改完标注只需python voc2yolo.py重跑一次同时用git管理Annotations/和labels/。在commit信息里写明改了哪张图这样当训练指标异常时能快速回溯到某一轮标注改动而不是翻整个数据集。本文还有配套的精品资源点击获取
返回列表