ARTICLE DETAIL

资讯详情

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

YOLO txt标注全解析:从坐标换算到抽烟检测数据集实战

YOLO txt标注全解析:从坐标换算到抽烟检测数据集实战 简介YOLO格式的抽烟检测数据集面向计算机视觉目标检测学习者与开发者可直接用于模型训练与验证省去数据清洗、格式转换等繁琐环节。资源包共1569个文件以txt标注与jpg图片为主体并另附可视化Python脚本和示例PNG图数据已明确划分为训练集627张、测试集156张每张图片对应同名txt标签严格遵循YOLO坐标归一化格式另含检测类别字典文件结构清晰无需额外处理即可接入YOLOv5/v8等主流框架。压缩包仅31.94MB下载与实验都很轻量无需额外配置复杂环境适合个人学习、课程作业或快速原型验证。目前已有106人学习拿到资源后直接运行可视化脚本即可随机读取一张图片并自动绘制、保存边界框便于直观检查标签质量随后便可衔接模型训练与效果评估。1. 抽烟检测数据集YOLO txt格式背后是一个完整的系统工程加油站、化工园区、建筑工地这种严禁明火的场所靠人盯监控盯不住全天候画面自动识别抽烟成了目标检测里一个真实的刚性需求。抽烟检测数据集解决的就是这个问题给模型一批标注好的图片告诉它烟头、拿烟的手在画面里的位置训练出能自动画框的模型。这个数据集最常用的是YOLO标注格式的txt文件——一张图对应一个同名txt每行五个数字简洁到没有多余信息。很多人拿到数据集第一反应是去跑训练脚本结果报错说标签读不出来或者训练完mAP低得离谱问题往往不在模型而在txt的格式、类别顺序和归一化坐标上。这里不聊模型调优而是把贴标签、转格式、校验、划分、配置、排错这一整条链路讲透适合刚入门目标检测的人也适合做私有数据集的工程师照着一一步步落地。2. 先读懂YOLO标注txt五个数字背后的坐标换算与坐标系约定2.1 一行标注是什么含义YOLO txt格式的核心行是0 0.518229 0.455729 0.218750 0.260417。从左到右分别是类别ID、归一化的中心点x、中心点y、归一化宽度、归一化高度。五个字段全部是浮点数不需要引号不需要分号行尾不要有多余空格。类别ID从0开始计数如果数据集只有一个类别那所有行的第一个数字都是0。这跟Pascal VOC的xml、COCO的json都不一样。xml里存的是绝对像素坐标xmin/ymin/xmax/ymaxCOCO json存的是像素坐标加宽高而YOLO选择了相对坐标。相对坐标的好处在于图像从1280x720缩放到640x360txt文件完全不用改模型训练时内部再怎么resize标注和输入图片始终处于同一个坐标系里。2.2 双向换算像素坐标和归一化坐标怎么互相转换先看从归一化坐标还原出像素框的代码这也是后面做可视化校验的基础img_w, img_h 640, 480 # 原图宽高来自训练或推理输入 x_c, y_c, w, h 0.518229, 0.455729, 0.218750, 0.260417 x1 int((x_c - w / 2) * img_w) # 左上角x像素坐标 y1 int((y_c - h / 2) * img_h) # 左上角y x2 int((x_c w / 2) * img_w) # 右下角x y2 int((y_c h / 2) * img_h) # 右下角y print(x1, y1, x2, y2)逻辑说明txt里的x_c是框中心点x占全图宽的比例乘以图像宽就得到像素位置w是框宽占全图宽的比例乘img_w得到像素宽。先让中心点向左、向上各退半个宽高得到左上角再向右、向下各进半个宽高得到右下角。注意全程用浮点算完最后一次性取整不要先取整再乘那样会丢精度。反向换算同样常见比如从标注平台拿到像素级框要转成YOLO格式时用这个逻辑x1, y1, x2, y2 156, 98, 296, 223 # 像素坐标人工标注或来自其他数据集 img_w, img_h 640, 480 x_c ((x1 x2) / 2) / img_w # 中心点x归一化 y_c ((y1 y2) / 2) / img_h # 中心点y归一化 w (x2 - x1) / img_w # 宽度归一化 h (y2 - y1) / img_h # 高度归一化 print(f0 {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f})参数说明x1、y1是左上角x2、y2是右下角四个值必须来自同一张图、同一单位。如果数据来自COCO这种直接给x、y、width、height的格式中心点要换成x_c (x width / 2) / img_w。最容易踩的坑是拿x1直接除以图宽当成中心点这样框整体往左上偏半个身位模型学出来的框全是偏的。把所有字段的含义和换算方式整理成一张表转换时对照着写不容易错字段位置含义取值范围计算方式第1列类别ID0到nc-1由classes.txt的顺序决定第2列中心点x0到1(xminxmax)/2再除以图宽第3列中心点y0到1(yminymax)/2再除以图高第4列框宽0到1(xmax-xmin)除以图宽第5列框高0到1(ymax-ymin)除以图高2.3 为什么这个格式成了事实标准计算机视觉里的目标检测算法换了好几轮yolo从v3一路迭代到v8模型结构、损失函数改了很多次txt标注格式却几乎没变过。相比三维目标检测里点云加立方体那种复杂标注二维检测的txt确实简单到没什么学习成本和yolo实例分割比分割要在每一行后面追加一串多边形顶点抽烟检测这种框级任务根本用不到。但格式简单也意味着表达能力有限它不区分一个框里是烟头还是拿烟的手不记录遮挡和截断信息。所以在真实项目里很多人会把抽烟拆成多个类比如手拿烟、烟头、烟雾让模型分别学推理时再做融合。这个取舍必须在数据准备阶段想清楚因为txt里类别ID一旦定死后面改类别顺序等于把所有标注重新生成一遍。3. 从标注工具到格式转换拿到合法YOLO txt的两条常用路径3.1 用LabelImg直接产出YOLO txt不自己写标注工具的话最常用的就是LabelImg。安装和启动命令pip install labelImg labelImg /data/smoking/images /data/smoking/classes.txt参数说明第一个参数是图片目录第二个参数是预定义类别文件每行一个类名行的顺序就是txt里类别ID的映射。启动后界面右侧有个PascalVOC/YOLO切换按钮点成YOLO模式再开始画框保存时就会输出同名txt到图片目录。如果保持默认的Pascal VOC模式保存出来的是一堆xml后面还得转格式。LabelImg两种模式的区别可以这样记模式保存产物每张图的文件名适合场景PascalVOCxml单文件同名.xml后续要转COCO或做复杂后处理YOLOtxt单文件同名.txt直接交给yolov5/yolov8训练使用上有个容易被忽视的点classes.txt的顺序一旦定了就不要改。第0行写smoking那txt里所有0都代表烟如果哪天把classes.txt重排成0: smoking_with_hand, 1: smoking老数据的0就从烟变成了手拿烟跑完训练评估全部错位只能重新标注。手工标注适合几百张图的规模一张图抠细节可能要几分钟。要标注几万帧监控画面更常见的做法是先用公开数据训练一版粗糙模型对未标注视频做批量预测人工只修正高置信度的伪标签。这种半自动流程能省掉大量画框时间代价是有可能把模型的偏差复制进伪标签里修正时要格外留意边界情况。3.2 把Pascal VOC的xml批量转成YOLO txtLabelImg的Pascal VOC模式、很多老数据集以及部分标注平台导出的结果提供的都是xml或json。xml转txt的脚本几乎每个项目都要写一遍核心逻辑如下import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, class_names): root ET.parse(xml_path).getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue # 跳过不关心的类别 cls_id class_names.index(name) # 类名映射为数字ID 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_c ((x1 x2) / 2) / img_w # 中心点归一化 y_c ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}) Path(out_dir).mkdir(parentsTrue, exist_okTrue) out_file Path(out_dir) / (Path(xml_path).stem .txt) out_file.write_text(\n.join(lines) \n, encodingutf-8) # 批量调用 for xml_file in Path(/data/pascal_voc/annots).glob(*.xml): voc_to_yolo(xml_file, /data/yolo/labels, [smoking])逻辑说明每次转换先读size节点里的宽高保证归一化分母是原图尺寸而不是模型resize之后的尺寸。然后遍历每个object用类名表映射ID再把bndbox里的四个像素值换算成中心点加宽高的比例。输出txt与xml同名写在同一个函数里批量调用时传目录即可。参数说明class_names如果包含多个类顺序就是最终的类别顺序。如果xml里的xmax小于xminw会变成负数yolo读到后框是坏的最好在转换时加一个if x2 x1: raise ValueError做拦截。还有一个细节有的xml里width和height节点不在size下而在annotation下不同数据集结构不完全一致转换前先打印一下root的tag层级。3.3 从COCO的json批量转txtCOCO格式的数据集同样常见网上不少目标检测数据集以COCO json发布。json转txt的思路和xml一致只是解析方式不同import json from pathlib import Path coco json.load(open(/data/coco/annotations.json, encodingutf-8)) img_map {img[id]: img for img in coco[images]} cat_map {cat[id]: cat[name] for cat in coco[categories]} new_id {cat_id: idx for idx, cat_id in enumerate(cat_map.keys())} # 全局ID重映射为连续ID out_dir Path(/data/yolo/labels) out_dir.mkdir(exist_okTrue) for ann in coco[annotations]: img img_map[ann[image_id]] x, y, w, h ann[bbox] # COCO的bbox是左上角宽高 img_w, img_h img[width], img[height] x_c (x w / 2) / img_w y_c (y h / 2) / img_h cls_id new_id[ann[category_id]] line f{cls_id} {x_c:.6f} {y_c:.6f} {w / img_w:.6f} {h / img_h:.6f} out_file out_dir / (Path(img[file_name]).stem .txt) with open(out_file, a) as fp: fp.write(line \n)这段代码里最容易踩的坑是COCO的category_id是全局编号中间可能跳号也可能不从0开始不能直接当成YOLO的类别ID必须先做一次重新映射。另外一张图对应多条标注所以要追加写入也就是用open(..., a)如果写成w每处理一条新标注就把文件覆盖一次最后文件里只剩最后一条。重复跑这段脚本前先清空labels目录否则会追加出重复行。3.4 网络数据集下载后的三件套检查不管数据集是怎么拿到的解压后先做三件事第一找类别清单确认是classes.txt、labels.txt还是只能从txt首列数字反推类别数第二随机打开三五张图把txt对应上去看框是否贴着目标而不是贴在背景上第三统计每个类别的框数量单类少于500框的训练效果通常很难看。统计脚本很短from collections import Counter from pathlib import Path counter Counter() for txt in Path(/data/smoking/labels).glob(*.txt): cls_counts Counter(line.split()[0] for line in txt.read_text().splitlines()) counter.update(cls_counts) print(counter)这段脚本把数据集里所有txt的第一列数字汇总输出类似Counter({0: 3821, 1: 104})的结果。如果某个类只有一百多框要么补数据要么在训练配置里给这个类加权重否则模型大概率把稀少类当成背景忽略掉。抽样检查时优先挑有遮挡、光线暗、目标小的图这种图最容易暴露标注质量参差。4. 喂给模型前先校验YOLO txt标注的四类常见异常与可视化4.1 先约定目录划分前保证images和labels成对存在标注质量直接决定训练上限模型结构再新也救不了坏标注。数据集拿到手第一步不是急着划分先把所有图片放images/所有标注放labels/只按文件名对应。划分脚本能正确运行的前提就是这种平铺结构。训练框架最终要求的目录结构如下这也是第5章划分脚本要生产的目标目录内容images/train/训练图片images/val/验证图片labels/train/训练标注txt文件名与images/train一一对应labels/val/验证标注txt划分前先在平铺目录上跑一遍缺失检查避免把缺标注的图混进训练集for f in images/*.jpg images/*.png; do base${f##*/} base${base%.*} if [ ! -f labels/${base}.txt ]; then echo missing: ${f} fi done这段bash脚本先把路径里的目录部分去掉只留文件名再去掉扩展名然后拼接出预期的txt路径。如果输出为空说明图片和标注成对出现。注意图片格式混合时要同时扫jpg和png只写*.jpg会把png全部漏掉训练时又会因为找不到这些png的标注而出问题。4.2 标注正文最常见的四类异常异常现象训练时的表现常见原因坐标越界loss异常升高或预测框飞出去归一化时除以了resize后的尺寸txt文件为空该图被当成纯背景训练漏标或转换脚本跳过了所有目标类别号越界训练报class index错误classes.txt顺序和原数据集不一致字段数不是5标签读取时直接报错手工改坏或混入其他格式标注给一个能一次扫完整个数据集的校验脚本from pathlib import Path def validate_dataset(img_dir: Path, label_dir: Path, nc: int): errors [] for img in list(img_dir.glob(*.jpg)) list(img_dir.glob(*.png)): txt label_dir / (img.stem .txt) if not txt.exists(): errors.append(f{img.name}: 缺少txt) continue for line_no, line in enumerate(txt.read_text().splitlines(), 1): parts line.split() if len(parts) ! 5: errors.append(f{img.name}:{line_no} 字段数应为5实际{len(parts)}) continue cls, x_c, y_c, w, h parts vals [float(v) for v in parts[1:]] if int(cls) 0 or int(cls) nc: errors.append(f{img.name}:{line_no} 类别号越界) if not all(0 v 1 for v in vals): errors.append(f{img.name}:{line_no} 坐标或宽高不在(0,1]) return errors errs validate_dataset(Path(images), Path(labels), nc2) print(\n.join(errs) if errs else OK)逻辑说明遍历图片目录下的所有jpg和png对每张图先找同名txt不存在就记缺失存在则逐行拆分先检查字段数再检查类别ID是否落在[0, nc-1]最后检查坐标和宽高是否都在0到1之间。nc换成你的实际类别数代码里的0 v 1是开区间因为中心点和宽高理论上不可能等于0出现0说明转换时对应像素坐标算错了。注意校验脚本里的nc必须和最终训练时data.yaml里的nc一致否则类别号越界这一类错误根本发现不了。跑完后errors为空不代表数据没问题只能说明格式合法。真正决定训练效果的还有框是否贴得准、类别是否标得对这部分要继续做人工抽查。4.3 可视化抽查把txt画回原图看框贴不贴脚本校验抓不到框标偏了、标在了烟盒上而不是烟头上这类语义错误。这种问题只能靠肉眼import cv2 img cv2.imread(images/0001.jpg) h, w img.shape[:2] for line in open(labels/0001.txt, encodingutf-8): cls, x_c, y_c, bw, bh line.split() x_c, y_c, bw, bh map(float, (x_c, y_c, bw, bh)) x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(check_0001.jpg, img)代码里的宽高直接取img.shape[:2]也就是原图分辨率再按归一化坐标乘回来画出来的框贴着目标说明换算正确。抽查时要刻意选目标占比较小的图。抽烟检测里烟头可能只有二十几个像素这类图标注偏差会被成倍放大在监控画面里尤其明显。一次挑几十张均匀分布在不同光线条件下的图基本能判断整个数据集能不能用。5. 划分train/val并编写data.yamlyolov5和yolov8训练自己的数据集5.1 用脚本划分数据集并保持双目录同步数据校验通过后按9:1划分训练集和验证集固定随机种子保证可复现import random from pathlib import Path random.seed(42) imgs list(Path(images).glob(*.jpg)) random.shuffle(imgs) split int(len(imgs) * 0.9) train_imgs, val_imgs imgs[:split], imgs[split:] for split_name, subset in [(train, train_imgs), (val, val_imgs)]: Path(fimages/{split_name}).mkdir(exist_okTrue) Path(flabels/{split_name}).mkdir(exist_okTrue) for img in subset: img.rename(Path(fimages/{split_name}) / img.name) txt Path(labels) / (img.stem .txt) txt.rename(Path(flabels/{split_name}) / txt.name)这段脚本的关键在于每次移动图片后立即移动同名txt两步之间不插任何逻辑保证两者永远落在相同的训练或验证分支里。随机种子固定为42后每次执行都得到同样的划分复现训练结果时不用重新划分。如果图片和标注原来就在同一个目录里先把txt移动成labels/、图片移动成images/再跑这段划分。如果原始数据是按文件夹分类的比如smoking/和not_smoking/两个目录要先写一个合并脚本把两类文件名统一编号再进入上面的流程。不然图片重名会在rename时互相覆盖静默丢数据。5.2 data.yaml的字段与拼路径规则yolov5和yolov8都能直接读这份配置path: /data/smoking train: images/train val: images/val nc: 2 names: 0: smoking 1: smoking_handpath是数据集根目录train和val按path / train的方式拼接所以这里写相对路径。names列表的下标就是txt里的类别ID数量必须严格等于nc。如果只有单个类别写names: [smoking]也可以yaml里等价于0: smoking。一个高频报错是nc填了1但txt里出现了1这个类别号训练一开始就报class index out of range。这种情况不用猜是哪张图的错直接把第4章的校验脚本跑一遍传入nc1几秒钟就能定位到具体文件名和行号。类别数不一致的问题80%出在又一次改了classes.txt的顺序另20%出在下载的数据集本身类别编号就不是你想的那样。5.3 启动训练前的自检与训练命令目录和yaml都就位后先跑一行快速校验再正式训练python train.py --img 640 --batch 16 --epochs 100 --data data.yaml --weights yolov5s.pt启动后前几行日志会打印类别列表、图片数量和标注数量这三项要逐一对上。如果训练到一半mAP0.5一直是0优先怀疑标注有没有被模型读到而不是模型结构问题。用OpenCV或txt阅读器单独解析一张标注确认坐标没有系统性偏移再回头看训练日志里每个epoch的box_loss是不是在正常下降。yolov8的对应命令写法是yolo detect train datadata.yaml modelyolov8s.pt epochs100 imgsz640 batch16两种框架对同一份txt和data.yaml都能直接使用损失函数、数据增强各自实现但标注格式的约定完全一致。这也是为什么现在做目标检测数据集时优先把标注统一成YOLO txt以后换框架成本最低。训练时如果开了多卡batch要按卡数翻倍学习率也要相应调整否则batch变大后loss曲线会抖得厉害。6. 抽烟检测的实战边界小目标、遮挡与误检的落地技巧6.1 小目标检测切图比换大模型更直接抽烟检测在1080p监控画面里最大的难点是小目标。烟头往往只占整张图的不到1%直接缩到640x640后目标被压到十几个像素yolov5s和yolov8s都很难学。常见做法是切图按512x512窗口、256步长把大图切成patch目标在patch里被放大好几倍。推理时把patch预测框加上偏移量映射回原图重叠区域的重复框用NMS合并。这一套操作对mAP的提升通常比把模型从s换成m或l还要明显因为小目标检测的本质问题是特征像素太少放大是直接补特征。6.2 压误检不要只动conf阈值监控场景里笔、口红、手指交错经常被误检成烟。很多人把conf从0.25调到0.5误检少了漏检也上来了。我一般会走两级过滤第一级用很低的conf0.05把所有候选框全捞回来第二级按形状和尺寸过滤。烟头宽高比大致在0.3到1.5之间面积基本不超过全图的千分之一长宽比超过1.5的大概率是笔类物体直接按类目过滤掉。这种策略比单纯抬阈值稳因为它保留了对低质量小目标的召回能力误检交给后置规则处理。6.3 框级验证比看视频更高效判断模型好坏不能只在视频里肉眼扫把所有预测结果和真值txt逐对算IoU才是量化标准。烟头这种小目标IoU阈值建议放宽到0.30.5的阈值对小框太苛刻容易得出模型全坏了的结论。一个通用的IoU函数是这样def compute_iou(a, b): x1, y1 max(a[0], b[0]), max(a[1], b[1]) x2, y2 min(a[2], b[2]), min(a[3], b[3]) inter max(0, x2 - x1) * max(0, y2 - y1) area_a (a[2] - a[0]) * (a[3] - a[1]) area_b (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a area_b - inter 1e-6)把真值和预测都转成[x1, y1, x2, y2]的像素坐标后逐一对账高于阈值的算命中。这个数比训练日志里的loss曲线直观得多也是向业务方交付时能直接拿出去的指标在哪个threshold、哪个IoU标准下抽烟告警的召回率和误报率分别是多少。本文还有配套的精品资源点击获取
返回列表