尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Pascal VOC吸烟数据集目标检测实战:从XML解析到YOLO训练

Pascal VOC吸烟数据集目标检测实战:从XML解析到YOLO训练 简介在智慧安防与安全生产领域吸烟行为自动检测是计算机视觉的热门落地场景。这份吸烟数据集围绕该任务构建面向目标检测学习者、算法工程师及科研人员可直接用于训练吸烟检测模型或验证检测算法。资料包包含991张原始图片及对应的991个Pascal VOC格式XML标注文件共1982个文件压缩包大小约44.92MBJPG与XML一一对应标注了人物与香烟区域方便进行数据划分与增强。根据作者说明模型平均识别率可达88.3%说明数据标注较为规范可作为真实场景中的基准数据集。目前已有262人学习下载适合从零搭建吸烟检测项目、扩充训练样本或参加算法竞赛时参考使用。文件中保留原始图片尺寸配合VOC标注可无缝接入YOLO、Faster R-CNN等主流框架整体开箱即用。1. 这组991张的吸烟数据集能干什么先看懂标注格式再谈识别率手头这组吸烟数据集991张原始图片平均识别率88.3%标注格式是Pascal VOC XML属于典型的小规模单类目标检测资源。它解决的是监控场景里“有没有人点烟”的自动判定工厂厂区、加油站、禁烟区都用得上。一个类别尺寸不大格外适合刚想跑通Faster R-CNN、SSD或YOLO流程的人但991张的体量想真正落地光“能跑通”远远不够XML标注结构、数据划分和验证口径任何一个偷懒都会让88.3%变成纸面数字。这篇按我实操的顺序从解开XML、跑通训练到排掉标注脏数据把这991张用透。2. 看懂Pascal VOC XML标注结构、解析脚本与编辑方式拿到数据集第一步不是急着训练是先把XML打开看一遍。Pascal VOC的标注是每张图片对应一个同名XML文件图里的每个目标对应一个object节点框的位置写在一组bndbox坐标里。下面三个小节分别解决“结构长什么样”“怎么用脚本批量读”“不写代码怎么改”。2.1 Pascal VOC标注文件结构annotation根节点下五个必读字段一张smoking_001.jpg对应的XML大致长这样?xml version1.0 encodingutf-8? annotation folderJPEGImages/folder filenamesmoking_001.jpg/filename source databasesmoking_dataset/database annotationPascal VOC/annotation /source size width1920/width height1080/height depth3/depth /size object namesmoking/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin680/xmin ymin320/ymin xmax810/xmax ymax460/ymax /bndbox /object /annotation五个必读字段我按经验排序第一是filename它决定XML能不能和图片对上很多脏数据就脏在这里。第二是size里的宽高后边做坐标归一化全靠它。第三是object/name类别名单类数据集里基本全是smoking但偶尔会有难例被标成别的名字。第四是bndbox的四个整型坐标注意VOC的坐标是像素绝对值不是归一化值。第五是difficult标记这个目标是不是难例训练时通常直接跳过。这个XML本身并不难懂难点在批量处理。991张图你不可能一张张开必须用脚本把所有XML的标签统计、坐标校验、文件名比对一次性做完。多数人拿到数据后先去看模型结构结果训练时反复报“no labels found”根源就是没先做这一步。2.2 用脚本批量解析XML校验991张标注一致性的最小命令解析XML我建议用Python标准库xml.etree.ElementTree不需要引入lxml也足够。下面的脚本会遍历Annotations目录检查每张XML的filename对应的图片是否存在、每个bndbox坐标是否合法同时统计类别数量import os import xml.etree.ElementTree as ET voc_root ./VOCdevkit jpeg_dir os.path.join(voc_root, JPEGImages) xml_dir os.path.join(voc_root, Annotations) xml_files [f for f in os.listdir(xml_dir) if f.endswith(.xml)] stat { total_image: 0, total_object: 0, class_cnt: {}, error: [] } for xml_f in sorted(xml_files): tree ET.parse(os.path.join(xml_dir, xml_f)) root tree.getroot() filename root.findtext(filename, ).strip() if not os.path.exists(os.path.join(jpeg_dir, filename)): stat[error].append(fmissing image: {xml_f} - {filename}) continue stat[total_image] 1 for obj in root.iter(object): name obj.findtext(name, ).strip() stat[class_cnt][name] stat[class_cnt].get(name, 0) 1 stat[total_object] 1 bnd obj.find(bndbox) if bnd is None: stat[error].append(fno bndbox: {xml_f}) continue xmin int(bnd.findtext(xmin)); ymin int(bnd.findtext(ymin)) xmax int(bnd.findtext(xmax)); ymax int(bnd.findtext(ymax)) if xmin 0 or ymin 0 or xmax xmin or ymax ymin: stat[error].append(fbad box: {xml_f} - ({xmin}, {ymin}, {xmax}, {ymax})) print(stat)逻辑说明先用ET.parse把XML解析成树对象再通过getroot拿到根节点findtext(filename)只取第一个匹配的filename节点内容strip()去掉编辑器可能带出的空格和换行。root.iter(object)会在整个树里遍历所有名为object的节点用obj.find(bndbox)定位框坐标。坐标校验用的是“xmax必须大于xminymax必须大于ymin且不能有负数”这套规则能挡住大多数手抖标注。参数说明voc_root改成你数据集的根目录jpeg_dir和xml_dir按实际文件夹名调整。运行完看stat里的class_cnt和error如果error不是空列表先把它清干净再谈训练。这个脚本是0成本投入但它能拦下后面至少半天排错时间算是我拿到任何VOC数据集后第一个跑的脚本。2.3 XML文件怎么打开和编辑三种现场做法与编码红线很多新手拿到XML第一反应是双击用浏览器打开能看但不好改。XML文件怎么打开和编辑我按场景分别说。只看结构用VS Code加XML插件左侧折叠树能一眼看清嵌套关系改少量框用XML Notepad这类树状编辑器不会写错闭合标签要批量改坐标或者给991张追加新目标别手改直接写Python脚本操作DOM再写回文件。这三条路径覆盖了从“读”到“改”的全部需求实测效率最高的还是最后一种。手工编辑XML有个编码红线必须保存成“UTF-8无BOM”。很多Windows编辑器默认存成UTF-8 BOM格式xml.etree.ElementTree解析时会在第一行报ParseError: not well-formed。这个问题我在标注补录时踩过当时用记事本另存了一下全部训练脚本直接挂掉。解决很简单改用VS Code另存右下角编码选“UTF-8 with signature”取消勾选或者写解析脚本时用utf-8-sig打开文件先吃掉BOM再交给解析器。日常维护标注文件时我建议统一定义一个小函数来读写XML编码永远走utf-8-sig少一事。另外编辑XML时不要动annotation之外的内容更不要往bndbox里写浮点数。VOC标准要求整数坐标训练框架和转换脚本几乎都按int()去读你写个680.5脚本直接抛ValueError。看上去是个小问题在991张的批量里就是一场灾难。3. 数据划分与训练参数让88.3%的平均识别率可复现991张原始图片、单类别、XML标注已经通过校验下一步是把标注喂进训练管线。这里有两个绕不开的步骤先把数据划分成训练/验证/测试三份再把VOC XML转换成目标检测框架真正消费的格式。最后一个问题才是训练参数。3.1 991张怎么分才不浪费按场景分层划分训练集与验证集991张说多不多说少也凑合划分不对会让之后所有指标失真。常见教训是直接random.shuffle后按比例切结果同一场景的连拍照片同时掉进训练集和验证集验证识别率虚高到九成以上。我现在的做法是先按场景分组再在组内随机切。import os import random random.seed(42) # 固定种子保证每次划分结果一致 image_dir JPEGImages images [f[:-4] for f in os.listdir(image_dir) if f.lower().endswith(.jpg)] random.shuffle(images) n_total len(images) # 991 n_val int(n_total * 0.1) # 99 n_test int(n_total * 0.1) # 99 train_txt open(train.txt, w, encodingutf-8) val_txt open(val.txt, w, encodingutf-8) test_txt open(test.txt, w, encodingutf-8) for i, stem in enumerate(images): line os.path.join(image_dir, stem .jpg) \n if i n_val: val_txt.write(line) elif i n_val n_test: test_txt.write(line) else: train_txt.write(line) train_txt.close() val_txt.close() test_txt.close()逻辑说明脚本从JPEGImages目录取出所有不带扩展名的文件名随机打乱后按8:1:1落到三个txt文件里。train.txt/val.txt/test.txt里每行是图片路径这是Pascal VOC训练最传统的“ImageSets”写法后续转换脚本或训练器可以直接消费。重点在random.seed(42)它让每次运行得到的划分完全一致复现别人88.3%的第一步就是划分一致。参数说明n_val和n_test用int()取整991张各分99张训练集793张。如果你发现同一场景图片高度相似需要在random.shuffle之前先按目录或文件名前缀分组把整个组分到同一个集合防止连拍照片串集。对于991这种规模test集只有99张别指望它代表全部真实场景它只用于和别人的结果横向对比。划分完我还建议再核一次把train.txt和val.txt两边的类别数量分布打出来如果某一类在验证集里一个都没有就得手动调整抽样。单类问题不太明显多类扩展时这一步是硬要求。3.2 VOC转YOLO格式归一化坐标转换脚本与class_id映射用YOLO系训练VOC XML不是直接输入的格式需要先转成YOLO txt每行一个目标“类别ID 中心点x 中心点y 宽 高”四个坐标值都除以图片宽高做归一化。转换脚本基本是固定模板import os import glob import xml.etree.ElementTree as ET class_names [smoking] # 顺序即class_id改这里要同步改data.yaml labels_dir labels os.makedirs(labels_dir, exist_okTrue) for xml_path in glob.glob(Annotations/*.xml): tree ET.parse(xml_path) root tree.getroot() width int(root.find(size).findtext(width)) height int(root.find(size).findtext(height)) out_lines [] for obj in root.iter(object): name obj.findtext(name, ).strip() if name not in class_names: continue # 跳过不认识的目标 class_id class_names.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)) x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height # clamp到[0,1]防止人工标注越界拖垮损失 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) box_w min(max(box_w, 0.0), 1.0) box_h min(max(box_h, 0.0), 1.0) out_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) stem os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(labels_dir, stem .txt), w, encodingutf-8) as f: f.write(\n.join(out_lines))逻辑说明脚本逐个读取XML从size节点拿原始宽高计算每个bndbox的中心点与宽高后除以宽高完成归一化。最后写到labels目录下同名txt。class_names.index(name)把类别名翻译成从0开始的整数ID这就是后续训练中打印的class_id。参数说明class_names的顺序即类别ID这是最容易出错的环节。如果class_names [smoking]那所有目标的class_id都是0将来你加了一个[person, smoking]旧的标注文件第一列就会从0变成1而不自知。glob.glob(Annotations/*.xml)只匹配该目录下XML如果目录里混入隐藏文件要先过滤。输出坐标保留6位小数已经足够再长的小数只会增大文件体积对训练精度毫无帮助。转换完打开任意一个txt看一眼别直接训练。一个合法样本长这样0 0.354167 0.425926 0.085417 0.129630。如果看到坐标有负数或大于1说明原始XML的bndbox越界了回第2章跑一遍校验脚本。3.3 训练参数基线让平均识别率88.3%稳定复现的做法要在991张上复现88.3%这个级别的平均识别率训练参数不能照搬大模型的默认值。小数据集、单类别、图像尺寸比较杂我常用的基线参数如下参数建议值说明image size640保留小目标细节过大容易过拟合batch size8~16991张别开32显存不足还容易振荡epochs150~200小数据集通常150轮内收敛optimizerSGDmomentum0.937或 AdamWYOLO系默认SGDAdamW收敛快但后期略抖initial lr0.0005~0.001单类数据集用0.01起步很容易发散label smoothing0.0~0.1单类时可以先关掉防止边界模糊val period10每10轮跑一次验证及时看曲线以YOLO系工具为例训练命令类似yolo detect train datasmoking.yaml modelyolov8n.pt \ imgsz640 batch16 epochs200 lr00.0005 patience50smoking.yaml里的names必须是[smoking]和转换脚本里的class_names顺序完全一致。这里我想单独说一下“平均识别率在88.3%”这个口径。目标检测里“识别率”在不同博主嘴里可能是accuracy、precision或mAP0.5三者计算方式差很多。我复现时会先在验证集上跑一次完整的mAP0.5和F1-score把阈值调到约0.25看验证集mAP能不能稳定到0.88附近。如果只看loss下降不单独统计验证集的召回率得到88.3%也没什么意义。小数据集最容易出的问题是在验证集上“贴地飞行”训练loss降到0.1以下但验证集mAP只有0.7这通常是过拟合不是参数没调好。噪音比较大的思路是先把数据增强里的hsv_h、fliplr打开然后配合early stopping别让模型把991张原图背下来。4. 高频踩坑清单XML解析、编码与训练不收敛的五个现场这组991张数据我在多个环境下跑过踩出来的坑集中在下面5个现场。每条按现象、原因、解决展开属于能直接照做的排错手册。4.1 XML解析报错UTF-8 BOM和编辑器污染现象脚本读XML时报xml.etree.ElementTree.ParseError: not well-formed (invalid token)或者UnicodeDecodeError: utf-8 codec cant decode byte 0xef。原因XML被Windows记事本保存成了带BOM的UTF-8文件头多了三个字节标准解析器不认。解决用VS Code打开后“另存为”编码选“UTF-8 without BOM”或者改脚本容忍BOMwith open(xml_path, r, encodingutf-8-sig) as f: tree ET.parse(f)这个坑在991张里只要混进一个文件整个目录的读取就停摆所以我的习惯是所有XML统一用utf-8-sig打开宁可多兼容一点也别让一个隐形字符卡掉整批数据。手工补标注时尤其容易触发因为标注工具保存规范与手动编辑不一致。4.2 filename与图片对不上991张里常见的脏数据现象校验脚本打印一大串missing image: smoking_100.xml - smoking_0100.jpg训练时又报找不到图片。原因数据在整理时xml名称和jpg名称不一致可能是序号补零规则不同也可能是folder字段写错。解决写一个批量重命名脚本以图片文件为准把XML改名对齐不要反过来改名图片因为图片名可能被其他数据引用。对齐规则很简单取xml的filename节点如果同名图片不存在就按当前xml文件名在JPEGImages里找唯一匹配的jpg然后把xml里的filename字段改成实际图片名并同步给xml文件改名。这个操作我在VOC数据集上做过多次核心原则是“一切以图片实际存在为准XML是附属品”。改完后再跑一遍2.2的校验脚本看到missing image清零再进入训练。4.3 bndbox坐标越界坐标大于图片宽高、xmin大于xmax现象训练初期loss异常大或者预测框大量落到图片边界外。原因人工标注时框拖出了画布边界标注工具生成了xmax1925而图宽只有1920也有少数情况是xmin和xmax填反了。解决在转换脚本里对坐标做clamp把xmin夹在[0, width]、xmax夹在[xmin1, width]对ymin/ymax同理同时把xminxmax的标注单独挑出来人工复核。更严格的校验规则是0 xmin xmax width 0 ymin ymax height width 0, height 0这条规则建议写进2.2的校验脚本而不是等训练崩了才查。991张里有三五张坐标越界很正常但在YOLO训练里这些越界框会变成极大值的正样本直接把损失拉爆浪费一个下午。4.4 class_id从0开始与类别顺序错位现象训练正常但预测结果全被当作错误的类别显示。现象data.yaml里的names顺序和转换脚本里的class_names不一致。比如转换脚本用[smoking]输出类别0但data.yaml里写的是[smoking, person]模型把0当成了person。解决统一维护一个类别清单文件转换脚本和训练配置都从它读取names: 0: smoking全项目只允许这一处定义names任何脚本不写死类别顺序。这个坑特别容易在“先转一批再补一批”时出现第二次转换时改了class_names的顺序旧的txt没重新生成模型就拿着新旧混杂的标注在训练mAP再漂亮也是错位结果。4.5 88.3%的复现陷阱验证集泄漏与随机种子现象自己训练的结果很漂亮验证集mAP达到0.88甚至更高一换到新场景视频立刻降到0.6。原因随机划分时同一场景的相似帧同时进了训练集和验证集模型见过“答案”了或者验证集本身只描述了训练分布内的情况。解决按视频或场景分组划分保证同一个场景帧只在训练集或只在验证集。如果数据集没有现成场景字段可以按文件名前缀分组前缀相同的归到同一边。设置random.seed只能保证你的划分过程可复现保证不了划分合理。991张的规模下我宁可把验证集压到80张也要确保它在场景上独立。5. 用不完的991张数据增强、错检分析与二次清洗991张看着少如果清理顺序对足够训出一个能进测试场的吸烟检测模型顺序反了就只是把噪音放大了。5.1 清洗先后顺序先剔错标再做增强有一个反复被翻车的做法一上来就开一大堆数据增强试图“弥补数据量不足”。这等于把漏标、错标、越界框一起放大模型学到的错误也被放大。我一般的顺序是先用2.2脚本把missing image和bad box清掉再用正则或关键词过滤掉difficult1的难例最后才开始水平翻转、HSV扰动、马赛克等增强。991张经过清洗往往只剩930~960张没关系干净的930张比带脏的991张训出来的mAP更稳。数据增强的开关值也保守一点hsv_h0.015左右就够了不要为了“充分使用数据”把色彩变换开过头否则夜晚样本会被染成奇怪的颜色。5.2 错检可视化把误报热点找出来单类检测最容易高发的是把打火机、烟盒、手指弯曲的动作误识别成吸烟。光看mAP看不出这种系统性误报我的做法是把验证集里所有预测框画出来按“假阳性”归类看它们集中在什么背景。import cv2 import glob import os from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) os.makedirs(vis_err, exist_okTrue) for img_path in glob.glob(val_images/*.jpg): img cv2.imread(img_path) results model.predict(img_path, conf0.25) for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) conf float(box.conf[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, fsmoking {conf:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(os.path.join(vis_err, os.path.basename(img_path)), img)画完图盯一遍把误检图按“小目标”“暗光”“遮挡”“高相似物体”四个标签归类。如果误报有一半集中在远处模糊小人上说明输入尺寸640不够可以单测一下调到960如果集中在白色圆柱体上那就要去采集这类硬负样本而不是继续调参数。这个可视化环节花不了半小时但它决定了991张数据到底是“够用”还是“白训”。我的习惯是每次训练完都把验证集前20个错的图打出来连夜看一遍确认错在数据还是错在模型再决定下一步。这样做能少走很多弯路。991张不是终点它是一个让你把标注校验、划分、转换、训练、错检分析整条链路跑透的起点把这些工序固化下来后面接更大的数据集也会顺手很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表