简介:面向城市管理与目标检测算法学习场景,街道乱放广告牌检测数据集提供114张真实街景图片,同时给出VOC与YOLO两种格式的标注框,类别统一为广告牌,共有165个矩形标注,适合用于违规广告牌识别模型的训练与验证。资源包共344个文件,包含114张jpg原图、114个xml标注文件及116个txt文件(其中114个为YOLO格式标注),整体仅8.12MB,目录结构清晰,便于快速加载与二次处理。目前已有312人学习下载,数据量适中、格式规范,既可作为目标检测入门练习数据,也适合做数据增强或迁移学习实验;使用labelImg工具标注,矩形框规则明确,有助于理解VOC与YOLO两种常用标注格式的对应关系。需要提醒的是,数据集只保证标注准确合理,不对模型精度作承诺,使用者可按需划分训练集和测试集。
1. 街道乱放广告牌检测数据集:114张VOC+YOLO双格式,单类目标检测怎么落地
城市管理巡检里,街道乱放广告牌检测是一个比较典型的单类目标检测任务,看着简单,真做起来却有两道坎:一是正样本要现场采集,违规广告牌形态五花八门;二是标注格式不统一,换训练框架就得重新转格式。我最近拆了一份街道乱放广告牌检测数据集,VOC+YOLO双格式直接打包,解压出来是114张jpg、114个xml、114个txt,标注类别只有guangguang一个,累计165个矩形框,全部用labelImg人工画框完成。对想快速验证YOLOv5/YOLOv8训练流程的从业者,这份数据省掉了爬图、清洗、标注、转格式的重复劳动;对做城管告警、门店违规识别项目的人来说,可以作为冷启动阶段的预训练样本。下面从解压、校验到训练、避坑,把整个流程过一遍。
2. 拆开压缩包看标注细节:VOC与YOLO两种格式的目录结构、xml字段与归一化换算
2.1 双格式目录结构:jpg/xml/txt三者怎么对应
拿到压缩包先不要急着解压训练,先把目录结构摸清楚。这份数据集的文件名前缀是firc_jd,后面跟编号,我拆开之后看到的实际对应关系是:每张图片有一个同名xml和一个同名txt,三者散落在同一层目录里,并不像很多开源数据集那样强制分成JPEGImages、Annotations、labels三个子目录。这对训练来说其实更自由,你自己按需归类就行。
| 文件类型 | 数量 | 内容说明 |
|---|---|---|
| jpg | 114 | 现场采集的街道场景原图 |
| xml | 114 | Pascal VOC格式标注,labelImg默认产物 |
| txt | 114 | YOLO格式标注,每行一个目标框 |
| 标注类别 | 1 | guanggao(广告牌) |
| 总框数 | 165 | 全部为目标框,平均每张约1.4个框 |
为什么我强调“双格式”这件事?因为labelImg保存时默认写VOC的xml,而YOLO系列训练读的是txt。很多数据集只给其中一种,你拿到后要么装labelImg重新导出,要么到处找转换脚本。这份直接把两种都给齐了,理论上解压后可以直接喂给YOLO训练。另外注意摘要里特别提了一句“不包含分割路径的txt文件”,意思是这份txt不是YOLO分割格式的多边形polygon坐标,而是每行一条检测记录,别当成分割标注喂给yolov8-seg,否则会直接报维度错误。
2.2 VOC的xml标签字段:object、size、bndbox逐一拆解
VOC格式的好处是结构直观,一个目标对应一个<object>节点。我拿其中一张图的结构做个示意,字段含义如下:
<annotation> <folder>JPEGImages</folder> <filename>firc_jd_13.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>guanggao</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>100</xmin> <ymin>80</ymin> <xmax>340</xmax> <ymax>260</ymax> </bndbox> </object> </annotation>这里的<size>是原图宽高和通道数,<bndbox>是矩形框的左上角xmin/ymin和右下角xmax/ymax。<truncated>和<difficult>在labelImg里默认是0,表示目标没有被截断、不算难例,训练时这两个字段YOLO不读,但保留着不影响转换。需要注意一点:xml里的<filename>和实际jpg文件名必须完全一致,包括字母大小写,否则后面做数据划分时很容易出现“图片走了、标注没跟上”的情况。
2.3 YOLO txt的归一化坐标:从xml到txt的换算与类别映射
YOLO格式的txt每一行是class_id x_center y_center width height,坐标全部归一化到0到1之间。归一化公式我在实际项目里一般这样写:
def xml_to_yolo(xml_path, img_width, img_height): import xml.etree.ElementTree as ET tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): class_id = 0 # 本数据集只有guanggao,索引固定为0 bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return "\n".join(lines)这段脚本的核心逻辑是:先解析xml拿到bndbox四个坐标,再除以图片宽高做归一化,最后按class_id cx cy w h拼成一行。因为这份数据只有一个类别,class_id固定写0;如果你的训练data.yaml里把类别顺序改了,这个数字也要跟着改。归一化后w和h是相对值,理论上永远小于1,如果跑出来某行w或h大于1,说明xml里的bndbox越界了,后面避坑章节会专门讲这个场景。
3. 从7z到可训练数据集:解压校验、框数核对与train/val划分脚本
3.1 解压7z:Windows与Linux两种方式及文件完整性检查
压缩包是7z格式,和zip不太一样,Windows系统自带的资源管理器解压不了,得装第三方工具。我一般在Windows上用7-Zip,右键“解压到当前文件夹”就能看到jpg/xml/txt三组文件。Linux环境下用命令行:
# Ubuntu/Debian 先装 p7zip sudo apt install p7zip-full # 解压到目标目录 7z x 街道乱放广告牌检测数据集VOC+YOLO格式114张1类别.7z -o./street_sign_dataset解压参数说明:x表示保留完整目录结构解压,-o后面跟输出目录,注意-o和目录路径之间不要加空格,加了会被识别成两个参数。解压完第一件事不是看图片,而是数文件数量。我的习惯是跑一条ls | wc -l分别统计jpg、xml、txt的数量,必须都是114。这种标注数据一旦中间缺一个txt,训练时那张图就会被跳过,而且你不会立刻发现,等loss曲线异常才回头排查,非常浪费时间。
3.2 校验脚本:xml与txt框数、文件名一对一核对
解压正常不代表标注没毛病。我拆数据集的固定动作是先跑一个校验脚本,确认三件事:文件名一一对应、每个txt行数不为0、总框数等于165。
import os from pathlib import Path img_dir = Path("./street_sign_dataset") # 图片目录 xml_dir = Path("./street_sign_dataset") # xml目录 txt_dir = Path("./street_sign_dataset") # txt目录 imgs = {p.stem for p in img_dir.glob("*.jpg")} xmls = {p.stem for p in xml_dir.glob("*.xml")} txts = {p.stem for p in txt_dir.glob("*.txt")} # 1. 三集合必须完全一致 print("jpg数量:", len(imgs), "xml数量:", len(xmls), "txt数量:", len(txts)) print("缺xml:", imgs - xmls, "缺txt:", imgs - txts) # 2. 每个txt不能是空文件,且框数总和应为165 total_boxes = 0 for t in sorted(txt_dir.glob("*.txt")): lines = [line.strip() for line in t.read_text(encoding="utf-8").splitlines() if line.strip()] total_boxes += len(lines) if len(lines) == 0: print("空标注:", t.name) print("总框数:", total_boxes) assert total_boxes == 165, "框数不对,检查标注文件"这个脚本的逻辑不复杂,但很实用:先用Path.stem取文件名不带后缀的部分,做差集判断;再逐行读取txt统计框数。注意txt的编码要按UTF-8读,如果解压工具在Windows上把文件名搞成乱码,这里集合差集会直接暴露问题。实际跑这份数据时,框数出来的结果就应该是165,多一行少一行都要停下来查。
3.3 按比例切分train/val并生成data.yaml
校验通过后做数据划分。114张图不算多,我一般按9:1切,也就是train约103张、val约11张。如果是做正式项目,这个比例还会再调整,但对小样本来说val太多反而让训练集更稀疏。
import random import shutil from pathlib import Path src = Path("./street_sign_dataset") train_dir = Path("./street_sign_dataset/images/train") val_dir = Path("./street_sign_dataset/images/val") train_lbl = Path("./street_sign_dataset/labels/train") val_lbl = Path("./street_sign_dataset/labels/val") for d in [train_dir, val_dir, train_lbl, val_lbl]: d.mkdir(parents=True, exist_ok=True) jpg_files = sorted(src.glob("*.jpg")) random.seed(42) random.shuffle(jpg_files) val_cnt = int(len(jpg_files) * 0.1) val_files = jpg_files[:val_cnt] train_files = jpg_files[val_cnt:] def move_pair(file_list, img_dst, lbl_dst): for jpg in file_list: xml = jpg.with_suffix(".xml") txt = jpg.with_suffix(".txt") shutil.copy(jpg, img_dst / jpg.name) shutil.copy(xml, lbl_dst / xml.name) shutil.copy(txt, lbl_dst / txt.name) move_pair(train_files, train_dir, train_lbl) move_pair(val_files, val_dir, val_lbl) print("train:", len(train_files), "val:", len(val_files))这里我用的copy而不是move,是给自己留后悔药——万一划分不均或者想重新切,原目录还在。随机种子固定为42,保证每次跑出来的划分结果一致,方便复现。划分之后YOLO训练的data.yaml就能这样写:
path: /absolute/path/to/street_sign_dataset train: images/train val: images/val names: 0: guanggao提示:path字段尽量写绝对路径。相对路径在YOLOv8里有时会因为启动目录不同而找不到图片,报
dataset not found,排查起来很绕。
4. 用YOLOv8跑通单类小样本训练:dataset.yaml、增强参数与训练日志解读
4.1 模型选型与dataset.yaml:为什么单类114张优先选n或s
处理数据集用于YOLOv8训练时,第一关是选模型尺寸。很多人上来就选yolov8m甚至yolov8x,觉得大模型精度上限高。但对114张、单类别、165框的数据集,大模型几乎必然过拟合。我的经验是:n和s之间选一个,先跑通流程看loss和mAP,如果欠拟合再往上加。
| 模型 | 参数量 | 对114张小样本的适用性 |
|---|---|---|
| yolov8n | 约3.2M | 首选,训练快,过拟合风险低 |
| yolov8s | 约9.4M | 可试,augmentation拉满时也稳 |
| yolov8m | 约25.9M | 不推荐,数据量撑不起 |
| yolov8l/x | 43.7M/68.2M | 这个数据量基本别碰 |
这个选择逻辑不复杂:目标检测模型容量越大,需要的数据和训练轮次越多。广告牌检测不是猫狗分类那种纹理极度丰富的任务,n模型的特征提取能力已经足够覆盖“柜台上立一块牌子”这种中粒度目标。训练入口文件data.yaml也很简单,类别guanggao对应索引0,和txt里的class_id保持一致即可。
4.2 训练命令与增强参数:epochs、close_mosaic、batch怎么配合
数据量小,训练参数就得往“稳”字上靠。我实际跑这份数据的命令是:
yolo detect train \ data=street_sign_dataset/data.yaml \ model=yolov8n.pt \ epochs=200 \ imgsz=640 \ batch=8 \ patience=30 \ close_mosaic=10 \ lr0=0.005 \ cache=ram参数说明:model=yolov8n.pt会自动下载COCO预训练权重,不用手动找yolo预训练模型下载地址;epochs=200对小样本足够,多了反而记住噪声;patience=30表示30轮验证指标不涨就早停;close_mosaic=10很关键——mosaic增强在训练最后10轮自动关闭,这是YOLOv8官方推荐的策略,避免最后阶段还在拼图造成优化震荡;lr0从默认0.01降到0.005,小样本下初始学习率过高容易让BN层崩溃;cache=ram把图片缓存进内存,114张小图完全放得下,训练速度提升明显。
增强参数如果默认效果不好,我会在命令里追加:
| 参数 | 默认值 | 小样本建议 | 作用 |
|---|---|---|---|
| hsv_h | 0.015 | 0.02 | 色相抖动,模拟不同光照 |
| hsv_s | 0.7 | 0.8 | 饱和度抖动,应对黄昏/阴天 |
| degrees | 0.0 | 10.0 | 轻微旋转,广告牌不总是水平的 |
| translate | 0.1 | 0.2 | 平移,让目标出现在不同位置 |
| fliplr | 0.5 | 0.3 | 左右翻转,注意文字类目标慎用 |
| scale | 0.5 | 0.5 | 缩放模拟远近变化 |
fliplr对广告牌检测要谨慎:如果路边广告牌本身带文字,翻转后文字是反的,但检测任务只看矩形框位置,不看文字方向,所以影响不大。真正要留意的是degrees别开太大,广告牌在真实场景里很少大角度倾斜,转太多反而制造伪样本。
4.3 训练日志与混淆矩阵:loss、P/R、mAP50的读法与常见误读
训练完成后,YOLOv8会在runs/detect/train/下生成results.csv,里面每一列对应一个指标。我看这份数据时重点盯三个:train/box_loss是否持续下降且无尖峰、metrics/mAP50(B)在val上是否稳定、metrics/precision(B)和metrics/recall(B)是否出现严重失衡。小样本单类任务常见的现象是recall很高、precision偏低,因为模型把相似的招牌背景都当成广告牌。
还有一个容易被误读的地方:混淆矩阵。YOLOv8画出的混淆矩阵纵轴是真实类别占比,横轴是预测类别占比,格子加起来不等于100%很正常,它表达的是“召回率视角”的分布,不是联合分布。看到矩阵合计不是1别急着怀疑代码 bug,先看是不是没做归一化。这属于yolo混淆矩阵里最常被问的坑。
5. 避坑手册:114张小样本广告牌检测的五个常见坑
5.1 解压后文件名乱码或文件缺失
现象:用某些国产解压软件打开7z,解压出的jpg文件名变成乱码,或者xml/txt三件套少了一件。原因:压缩包内文件名按UTF-8编码,老版本解压工具按ANSI解码,中文文件名和前缀符号被转义破坏。解决:Windows一律用7-Zip 21以上版本,Linux用p7zip解压;解压完先跑一遍上文的集合差集脚本,发现缺文件就从原包重新解压,不要手工改名凑数。
5.2 txt里出现全0坐标或宽高越界
现象:训练时日志提示某行标注框无效,或者val的mAP一开始就是0。原因:xml里的bndbox如果出现xmax等于xmin,归一化后宽为0;如果标注框超出了图片边界,归一化后数值可能大于1,模型训练时算loss直接算不下去。解决:在校验脚本里加一条判断,凡是w或h小于等于0、大于1的行单独打印,定位到具体txt后人工回看图,用labelImg修正对应xml再重新生成txt。这种问题通常在数据生产阶段就存在,属于标注质量筛查,不是训练配置能救的。
5.3 val的mAP很高,实拍视频却漏检
现象:验证集mAP50到0.9以上,但拿现场手机拍的视频一测,远处广告牌完全不识别。原因:数据集里的原图拍摄距离偏近,目标框占整图比例较大;验证集和训练集来自同一采集批次,分布高度相似,评估结果虚高。实拍场景里目标小、亮度杂、背景乱,分布漂移直接击穿模型。解决:训练时可以用imgsz=768提升小目标分辨率,推理时对视频帧做ROI裁剪,只检测街道两侧的固定区域;同时加大实际场景数据的补充比例,把每天巡检拍的新图按周回流训练集。
5.4 把guanggao改成中文类别名后训练报错
现象:觉得类别名用拼音不够专业,把data.yaml里的guanggao改成“广告牌”,一跑训练就报错,要么KeyError要么标签读取为空。原因:Windows下记事本另存为时默认编码是ANSI(GBK),YAML按UTF-8读取时中文变乱码;即使强迫保存为UTF-8,cmd终端和Python解释器的编码环境不一致也会出问题。解决:类别名保持英文或拼音最稳妥,这是我在多个项目里的血泪经验;实在要显示中文,只改可视化标签映射,不动训练文件里的names字段。
5.5 训练中BN崩溃或loss出现NaN
现象:训练到一半train/box_loss突然变成nan,之后的epoch全部无效。原因:小样本下初始学习率偏高,加上最后阶段mosaic增强拼接了大量不自然样本,BN层的running_mean被带偏。解决:把lr0降到0.003到0.005之间,close_mosaic开到15到20轮,batch固定不要忽大忽小;如果还崩,用yolo detect train ... freeze=10把前10层backbone冻结,只训练neck和head,让训练更稳。这个毛病在yolo训练里不算罕见,看到nan先查学习率,别急着换模型。
6. 验证与业务落地:从mAP到置信度阈值与告警逻辑的联动
6.1 导出ONNX并在业务侧做一次快速验证
训练完的best.pt不能直接丢给线上服务,建议先导成ONNX,再用onnxruntime快速验证一张图:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640导出后用一段Python接口验证推理结果,核心参数是置信度阈值和NMS的IoU阈值:
import cv2 import onnxruntime as ort import numpy as np sess = ort.InferenceSession("best.onnx") input_name = sess.get_inputs()[0].name img = cv2.imread("street_scene.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) resized = cv2.resize(img, (640, 640)) blob = resized.astype(np.float32) / 255.0 blob = np.transpose(blob, (2, 0, 1))[None] out = sess.run(None, {input_name: blob})[0] # out形状为 [1, 5+nc, 8400],取第一维做后处理参数说明:ONNX导出的输出是[1, 6, 8400]形状,第一维batch,第二维是cx cy w h obj_conf class_conf,第三维是anchor解耦后的候选框数量。实际项目里我不会把后处理写死,而是用ultralytics库里的YOLO直接加载onnx做推理,减少重复造轮子。
6.2 置信度阈值与连续帧告警:业务侧怎么用
模型输出的是概率,业务侧要把它转成动作。我做城管告警类需求时,置信度阈值一般按场景分三档:
| 阈值 | 适用场景 | 行为 |
|---|---|---|
| 0.25 | 初次检测、巡检抽查 | 标记待确认,不直接告警 |
| 0.45 | 正式业务告警 | 触发工单 |
| 0.60 | 夜间或雨天 | 严控误报,只认高置信度 |
低阈值带来的误报,我用连续帧确认来过滤,而不是直接调高置信度:
class AlarmWindow: def __init__(self, min_frames=3, max_gap=5): self.min_frames = min_frames self.max_gap = max_gap self.countdown = 0 def update(self, det_conf): hit = det_conf >= 0.45 if hit: self.countdown += 1 else: self.countdown = max(0, self.countdown - 1) return self.countdown >= self.min_frames这个逻辑很简单:同一路视频流里,连续3帧都检测到广告牌才触发告警,中间漏一帧可以通过countdown缓冲,不会立刻清零。相比单帧硬判,它能挡住大部分行人路过、车辆遮挡造成的闪烁误报。从那以后我每次拿到一个标注数据集,都强制先走一遍解压校验、划分、小参数训练、导出验证的流程,再谈业务指标,这份114张的广告牌数据集也是这样跑通的。希望帮到你。
本文还有配套的精品资源,点击获取