简介:面向智慧城市市容巡检场景的街道涂鸦、垃圾、故障路灯等目标检测数据集,已按VOC与YOLO两种格式整理,适合训练目标检测模型。资源涵盖19263张图片对应的标注,共11个类别,覆盖涂鸦、垃圾堆放、故障路灯等常见市容问题,可用于街道环境卫生监测、市容问题自动发现等应用。需特别留意的是,数据集中超过一半为增强图片(由4张拼接而成),下载前请先查看预览图确认符合需求。压缩包共2000个文件,以XML标注文件为主,含1999个XML和1个使用说明TXT,其中XML对应Pascal VOC格式、TXT对应YOLO格式,整体约978.91MB。目前已有225人学习下载。资源内标注组织清晰,可直接用于相应检测框架训练,免去格式转换时间,适合从事城市管理、AI巡检算法开发的工程师与学生使用。
1. 智慧城市街道涂鸦垃圾故障路灯市容检测数据集:为什么19263张双格式数据值得动手
市政巡检员每天处理大量巡街照片,问题对象很杂:墙上涂鸦、街边成堆垃圾、故障路灯、破损井盖……每一项都单独建模型,成本高且难以维护。智慧城市街道涂鸦垃圾故障路灯市容检测数据集这类方案,就是把常见市容问题收进一个11类别、19263张的监督训练集合,同时给出Pascal VOC和YOLO两种标注格式。它解决的是智慧城市视觉落地里最现实的问题:有没有现成、干净、数量够的数据让你把检测模型先跑起来。适合谁用?做市政AI的算法工程师、搞智慧城市视觉终端的开发者、做城市管理课题的学生,都能拿这份数据直接练手,不用自己在街景图上慢慢画框。全文围绕“怎么打开它、怎么转格式、怎么避坑、怎么训练验证”展开,中间会有可直接复制的脚本和相关参数说明。
2. VOC与YOLO双格式到底存了什么:目录结构、XML字段与归一化坐标换算
拿到这类数据集压缩包,很多人的第一反应是直接解压扔进训练脚本。但市容检测数据里,Pascal VOC和YOLO格式的标注互不相同,训练框架的读取方式也不一样,先花十分钟把两种格式看清楚,后面能省半天排错时间。
2.1 两种格式并存的取舍:Pascal VOC的老牌标注与YOLO的现代训练
Pascal VOC是目标检测领域的老牌标注协议。它用一个XML文件描述每张图片里的对象:文件名、图片来源、图像宽高,以及每个目标的类别和边界框坐标(左上角xmin、ymin,右下角xmax、ymax)。公开数据集像早期的VOC2007、VOC2012,都是这套结构,很多老工具链也默认读它。YOLO格式则完全不同,每个标注是一行文本“类别id x_center y_center width height”,坐标全部对图片宽高做了归一化,取值在0到1之间。YOLO系列训练脚本用的是这种紧凑格式。
双格式并存的现实意义在于兼容两种工作流:你习惯了LabelImg的VOC交互,就用XML做二次修正;你要用YOLOv5、YOLOv8直接训练,就拿TXT格式省掉转换。另外,YOLO格式损失了目标属性信息,VOC格式则便于人工阅读和审核。对市容场景来说,涂鸦位置是否标全、垃圾袋是否漏标,用XML肉眼复查更直观,所以发布到开源社区的数据集往往保留VOC为“母版”,再生成YOLO副产物。
2.2 解压后先看目录结构:确认图片与标注的对应关系
拿到“.7z”压缩包,第一步是解压并列出目录树。Windows下用7-Zip,Linux下用p7zip,命令很简单。解压后应能看到类似下面的组织方式:
7z x 智慧城市街道涂鸦垃圾故障路灯市容检测数据集VOC+YOLO格式19263张11类别.7z -o./street_dataset cd ./street_dataset tree -L 2典型数据布局会是三个平级区域:JPEGImages或images放图片,Annotations或VOC/Annotations放VOC的XML文件,YOLOLabels或labels放YOLO的TXT文件。再往下可能还有ImageSets/Main存训练验证划分的txt,索引里每行写不带扩展名的图片名。看到这个结构后请先做一个动作:统计三种文件数量是否对得上。
这里有个常见问题:有些压缩包只给图片和标注,不给划分文件,需要自己按比例随机分出训练集、验证集。我一般不建议直接拿全部数据训练,因为智慧城市场景里同一街道多次拍摄会产生相似样本,不划分就评估,mAP会虚高,后面做部署对比时容易被质疑。
2.3 YOLO标注归一化坐标换算:从XML里的像素到txt里的比例
VOC和YOLO之间最关键的技术点,是坐标从“像素绝对坐标”换算成“归一化相对坐标”。比如一张1920×1080的街景图,XML里记录一个涂鸦框是“xmin=520, ymin=300, xmax=980, ymax=760”,对应YOLO的四个数必须这样算:x_center=(520+980)/2/1920≈0.3906,y_center=(300+760)/2/1080≈0.4907,width=(980-520)/1920≈0.2396,height=(760-300)/1080≈0.4259。
很多人在自己写的转换脚本里翻车,就是忽略了宽高必须除以原始图片尺寸,而不是除以缩放后的尺寸。市容照片来自不同设备,有可能是4032×3024的手机原图,也有可能是1920×1080的监控截图,若脚本不读每张图的真实宽高,而统一用某个固定值,生成的TXT全部错位。所以下一步的转换脚本里,第一步永远是打开XML,读<size>节点的width和height,而不是对图片文件做假设。
import xml.etree.ElementTree as ET xml_path = "Annotations/street_001.xml" tree = ET.parse(xml_path) root = tree.getroot() # 从XML的size节点获取真实宽高,这是归一化的分母 img_width = int(root.find("size/width").text) img_height = int(root.find("size/height").text) for obj in root.findall("object"): class_name = obj.find("name").text 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 box_width = (xmax - xmin) / img_width box_height = (ymax - ymin) / img_height print(class_name, round(x_center, 6), round(y_center, 6), round(box_width, 6), round(box_height, 6))这段代码验证了VOC到YOLO的换算过程。注意所有除法都用了浮点数,Python 3里单个斜杠就是真除法,不会取整。如果是在Python 2环境或不小心用了整数除法,1/2会得到0,坐标全部失常,这是踩坑重灾区。
另外要说明的是:归一化后的坐标允许略超出0到1范围。当目标被图片边缘截断时,xmin为负或xmax超出图片宽度,计算后会出现负值或大于1的值。YOLO训练脚本通常能容忍轻微越界,但当框严重越出画面(比如目标只有5%在图像内),我一般建议直接过滤掉,因为这类残缺样本会让模型学到奇怪的纹理,对市容场景的涂鸦检测收益很低。
3. 从VOC转YOLO:转换脚本与四个边界坑
很多框架只吃YOLO格式,所以拿到VOC标注后,最常做的就是批量转格式。这个过程看起来是把上面的单条逻辑包一层循环,但实际落地时至少有四个边界坑会让人折腾一晚上。这一章直接给出我常用的转换脚本和参数细节。
3.1 转换前先体检:统计11个类别的框数量与图片数量
写转换脚本之前,先对数据集整体做一次体检。市容检测数据集的难点常在类别极端不均衡:有些类别可能有一万多个框,有的类别只有一两百个框。不做统计直接转换,训练后稀有类别几乎不可用。
import os import xml.etree.ElementTree as ET from collections import Counter anno_dir = "Annotations" counter = Counter() image_names = [] for xml_file in os.listdir(anno_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(anno_dir, xml_file)) root = tree.getroot() image_names.append(root.find("filename").text) for obj in root.findall("object"): counter[obj.find("name").text] += 1 print("统计到图片数:", len(image_names)) print("各类别框数:", counter.most_common())统计结果出来后做两件事:第一,确认类别名是否和你的训练配置一致。比如某个类叫“Litter_Trash”,另一个叫“litter_trash”,在OCR里看着正常,但YOLO会把它们当成两个类,id错位,训练指标直接失真。市容数据因为采集周期长、标注人员不同,大小写不一致、下划线和空格混用的情况很常见,建议转换时统一做一次类别映射。第二,观察类别分布。若发现严重长尾分布,后面训练阶段就要考虑样本均衡策略,这一点我会在第四章展开。
3.2 VOC XML转YOLO TXT的核心脚本
体检通过之后,按下述脚本批量转换。脚本设计成一个函数,输入VOC目录、YOLO输出目录和类别映射表,输出每张图片对应的TXT文件。
import os import cv2 import xml.etree.ElementTree as ET # 类别映射表:按训练需求固定id顺序,不要依赖字母序 CLASS_MAP = { "Graffiti": 0, "Litter": 1, "TrashBag": 2, "GarbageBinFull": 3, "FaultyStreetlight": 4, "DamagedManholeCover": 5, "IllegalPoster": 6, "RoadSpillage": 7, "OccupiedSidewalk": 8, "OverflowingDustbin": 9, "VendorStall": 10, } def voc_to_yolo(anno_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(anno_dir): if not xml_file.endswith(".xml"): continue xml_path = os.path.join(anno_dir, xml_file) tree = ET.parse(xml_path) root = tree.getroot() img_path = os.path.join(img_dir, root.find("filename").text) # 用OpenCV读取图片尺寸,XML里的宽高如果缺失时可以兜底 h, w = cv2.imread(img_path).shape[:2] lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASS_MAP: print(f"跳过未映射类别: {name} in {xml_file}") continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 过滤严重越界框:完全在画面外或宽高为负 if xmax <= xmin or ymax <= ymin: continue cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.splitext(xml_file)[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) voc_to_yolo("Annotations", "JPEGImages", "YOLOLabels")这套脚本有几个关键点。第一,类别映射表是手工写的,不是自动从XML里收集的,目的是保证id顺序稳定。YOLO训练时类别id从0开始连续编号,class文件里第几行就对应id几。如果你用自动收集然后排序,下次换一台机器或者数据集更新类,id可能全变,模型就得重新训练。第二,用OpenCV读图片尺寸,比直接读XML里的size字段更可靠,因为部分标注工具生成的XML里宽高是错的,甚至缺失。第三,过滤了宽高异常的框。市容数据里,特别远的垃圾袋经常被标成几个像素的小框,这种框既没意义又干扰损失函数。
转换完成后,务必抽检。随机打开几个TXT文件,用可视化脚本把框画回图上检查,不要只看数值。标注框偏移一到两个像素肉眼看不出来,但归一化到小分辨率训练时,偏移可能被放大成明显的错位。
3.3 脚本参数说明:类别id、图片尺寸与输出目录
上述脚本有四个参数需要重点关注。
- 类别映射表CLASS_MAP:必须与训练用data.yaml的顺序完全一致。如果data.yaml写“Graffiti”在第二个位置,而映射表把Graffiti编为0,训练出来的类别就全部错位。
- 图片尺寸读取方式:脚本用了OpenCV的imread,它会按原图分辨率读取。但注意,如果图片有EXIF旋转信息,OpenCV默认不会自动矫正方向,某些手机拍摄的街景图会被倒置,导致框错位。解决方式是用
cv2.imread(..., cv2.IMREAD_IGNORE_ORIENTATION)强制不应用旋转,并在标注阶段就确认方向一致。 - 输出目录out_dir:建议转换到独立的
labels目录,不要直接在原始目录上改动。保留VOC原文件,出现问题有“后悔药”可吃,这也是我多年做数据集的基本习惯。 - 文本精度:
{:.6f}保留了6位小数。对1920宽的图片,6位小数对应约0.002像素精度,完全够用。有人习惯保留8位,没必要,反而让TXT文件变大,训练读取时字符串解析更慢。
3.4 四个边界坑:空标注、截断目标、非矩形框、中文路径
转换不是跑通就完事,边界情况才是数据质量的分水岭。
第一个边界坑是空标注。市容巡检中经常出现首尾帧图片里没有任何问题目标,但XML文件依然存在,只是没有<object>节点。转换脚本会产生一个空TXT文件,YOLO训练时遇空标签默认跳过该图。问题在于,若验证集里空图过多,评估时会引入偏差,建议单独把它们挑出来,要么从训练集剔除,要么标注成背景类别。
第二个边界坑是截断目标。街景图片里涂鸦经常被电线杆、车辆遮挡,标注框会延伸到画面外。处理方式有两种:一是将越界框裁剪回图像边界,二是直接过滤。我一直用过滤加保留混合策略:保留越界比例低于10%的框,并且让脚本把坐标clip到[0,1]区间,保证训练不崩。
第三个边界坑是非矩形框。涂鸦形状不规则,标注工具若输出polygon格式,很多转换脚本会直接报错,或强行取外接矩形。取外接矩形会引入大量背景,涂鸦本来就细长,背景一多模型容易误检。做法是把多边形顶点放进XML的<polygon>节点,转换时按最小外接矩形处理,同时记住该框是复杂目标,后续训练可配合高IOU阈值。
第四个边界坑是中文路径。开源数据集的标注文件里嵌着中文文件名,Windows下用脚本遍历时,编码如果不对,cv2.imread会返回None,紧接着取shape时就抛异常。解决办法是在脚本开头设置os.environ["PYTHONIOENCODING"] = "utf-8",或者统一先重命名文件为拼音加编号。这条属于经典“玄学Bug”,排查半小时后才意识到是编码问题。
4. 11类别数据怎么用:类别体系、样本均衡与训练集划分
市容检测数据集和通用目标检测数据集有个显著区别:类别语义高度重叠。垃圾桶满溢和路边垃圾袋,在视觉上边界模糊;涂鸦和非法小广告在颜色、纹理上也很接近。这些重叠会让模型在验证集上表现尚可,一上真实街道就露馅。所以动手训练前必须明确类别体系,并做好样本均衡处理。
4.1 类别体系设计:涂鸦、垃圾、故障路灯与市容杂项
先按市容问题类型把11个类别归组。第一类是涂鸦类,包含墙体喷绘涂鸦,特点是颜色鲜艳、边缘锐利、形状不规则;第二类是垃圾类,包含散落垃圾、袋装垃圾、垃圾桶满溢,特点是纹理杂乱、尺度跨度大,远看一个像素点,近看一大片;第三类是设施故障类,包含故障路灯、破损井盖,特点是暗光环境下对比度低;第四类是市容杂项类,包含违规小广告、占道经营、道路遗撒等,特点是类别间视觉特征极易混淆。
我在实操时第一步不是急着写训练脚本,而是把每张图片的类别标签打印出来,逐类做语义边界定义。比如“Litter”(散落垃圾)和“RoadSpillage”(道路遗撒),前者强调人为丢弃的废弃物,后者强调车辆运输中掉落的物品。边界定义好之后写进一个CLASS.md文件,将来新增标注数据或换标注外包时,这份文件就是标准答案。
4.2 训练/验证集划分:随机划分的脚本与种子固定
没有现成划分文件时,按8:2随机划分训练集和验证集。但必须固定随机种子,否则每次运行结果不同,同一张图忽而在训练集忽而在验证集,没法对比实验。
import os import random from sklearn.model_selection import train_test_split random.seed(42) image_files = [f for f in os.listdir("JPEGImages") if f.endswith(".jpg")] train_files, val_files = train_test_split(image_files, test_size=0.2, random_state=42) with open("train.txt", "w") as f: for name in train_files: f.write(os.path.join("JPEGImages", name) + "\n") with open("val.txt", "w") as f: for name in val_files: f.write(os.path.join("JPEGImages", name) + "\n") print(f"训练集 {len(train_files)} 张,验证集 {len(val_files)} 张")划分之后还要做一次类别分布校验:分别统计训练集和验证集里各类别框的数量,尽量让比例接近。如果验证集里“故障路灯”只有两个框,验证mAP的置信区间就会过宽,偶然性太大。发现比例不对时不要硬改随机种子,直接把稀有类别的样本手动放进训练集,再从其他类别里随机补齐验证集。
4.3 样本均衡策略:欠采样与数据增强的边界
长尾分布是市容数据集的常态。涂鸦、垃圾袋这类高频类别动辄几千框,破损井盖可能只有一两百框。直接训练会让模型倾向于把未知目标预测成高频类别,这属于目标检测里最常见的“翻车”模式。
常用做法分两步。第一步是图像的欠采样,控制高频类别的图片数量,让它不超过稀有类别图片数量的五倍。第二步是稀有类别的针对性增强,包括随机旋转、亮度扰动、Mosaic拼接。但注意,市容场景有强先验方向,涂鸦和垃圾不被倒置,旋转增强的角度范围应控制在正负30度以内,全角度旋转会违背“街道方向固定”的物理常识,反而降低真实场景精度。
如果评估下来模型对稀有类别还是无感,可以降低损失函数里正样本权重,或者在损失函数设计上给稀有类别更高的loss权重。YOLOv8的默认配置里没有直接的类别权重参数,但可以修改loss.py或者用采样器调整图片权重。这条要根据框架版本灵活处理,不要照抄网上的教条。
5. 避坑清单:训练这个数据集最容易翻车的5个检查点
把19263张数据真正跑起来之后,回看整个过程,坑其实不在模型结构,而在数据集本身的细节。下面五条是我做市容检测时实打实踩过的坑,按“现象→原因→解决”列出来,希望能帮你少走弯路。
现象一:训练loss正常下降,但验证集mAP始终在0.3附近徘徊。原因:类别映射表顺序与data.yaml不一致,导致验证时预测框类别id对不上,计算mAP全是错配。解决:训练前打印前10张图片的标签TXT内容,和data.yaml里class的顺序逐行校对。
现象二:训练卡死在某个epoch,报错提示“Image not found”。原因:XML里filename字段写了相对路径,但实际图片文件名带有中文或空格,Windows下路径拼接失败。解决:统一重命名图片为纯数字编号,同时更新XML里的filename节点。
现象三:某些类别检测精度极高,某些类别完全检不出。原因:训练时没做类别均衡,稀有类别样本数不足。解决:参考第4.3节,做欠采样加定向增强,并检查数据增强是否破坏图像语义。
现象四:验证集mAP很高,但部署到现场摄像头效果很差。原因:训练集和验证集来自同一批次街道照片,时间接近、光线相似,模型学的是场景匹配而不是目标特征。解决:用不同时间段、不同街道的照片做外部验证集,把内部验证集mAP当作参考值,不当作上线指标。
现象五:转换后的YOLO标签大量出现“nan”。原因:图片尺寸读取异常,cv2.imread返回None,宽高变成None,除法产生nan。解决:在转换脚本里加一个检查,读不到图片就直接打印文件名,单独排查,不要通过批量逻辑掩盖。
这五条几乎覆盖了从数据预处理到模型评估的完整链路。很多时候不是模型调参不够,而是数据管线的某个细节在捣鬼。保持怀疑态度,遇到底层指标异常,优先怀疑数据链路。
6. 跑通收尾:YOLOv8最小训练命令与mAP验证技巧
最后一步,用YOLOv8把转换好的数据集跑起来。这里给出最小可行的训练方案,并对关键参数做说明,适合快速验证数据集质量。
pip install ultralytics yolo detect train \ data=street.yaml \ model=yolov8s.pt \ epochs=50 \ imgsz=640 \ batch=16 \ lr0=0.01 \ seed=42其中street.yaml需要指向你的图片目录和标签目录:
train: ./train.txt val: ./val.txt nc: 11 names: [Graffiti, Litter, TrashBag, GarbageBinFull, FaultyStreetlight, DamagedManholeCover, IllegalPoster, RoadSpillage, OccupiedSidewalk, OverflowingDustbin, VendorStall]跑完50个epoch后,保存最优权重,用验证集评估。命令行会输出mAP50和mAP50-95,这两个数字不是终点。我更习惯翻出验证集的混淆矩阵,看看哪些类别在互相“打架”。如果涂鸦和非法小广告高度混淆,说明类别定义过细或标注本身有分歧,这时候改模型结构没用,回去统一标准才是正道。
最后一个血泪经验:训练完在测试集上可视化预测结果时,不要只看检测出目标的图,多翻翻漏检图。YOLO在明亮光照下的涂鸦识别效果通常不错,但晚上昏暗街道上的故障路灯、被树叶遮挡的垃圾,是模型最容易漏掉的两个场景。如果这两类在你的业务里重要,我建议对训练集做亮度扰动增强,并单独收集夜间数据进行微调。
这个方向的“验证”也不该停在mAP数字上。把模型接上GStreamer管道推流测试,在真实街道视频上的稳定性和误报率才是最终标准。项目做到这里,你会深刻体会到数据集质量对最终效果的决定作用——这也是我做智慧城市项目最深的教训,数据标注的歧义和类别边界模糊,比模型结构带来的影响大得多。希望这些经验对你有用,按这个路径走一遍,你也能在这个方向上少踩几个坑。
本文还有配套的精品资源,点击获取