简介面向智慧城市市容巡检与目标检测模型训练这份资源包含街道涂鸦、垃圾堆放、故障路灯等11类市容问题的完整检测数据集。共19263张jpg图片每张均配有Pascal VOC格式xml与YOLO格式txt标注可直接用于YOLO系列、MMDetection等主流框架训练其中超过一半为由4张图片拼接而成的增强样本可帮助提升模型在复杂街景多目标场景下的泛化能力。压缩包为7z格式约978.91MB内含1999个xml标注文件与1个使用说明txt文件图片与标注一一对应目录组织清晰便于按批次划分训练集和验证集。当前已有228人学习浏览适合刚接触目标检测的初学者作为市容方向练手数据也适合算法工程师在数据增强、模型调优与迁移验证时参考使用。1. 智慧城市市容检测数据集19263张图解决巡查人力不够的痛点做智慧城市市容管理的甲方项目时最常见的一个诉求是街道上乱贴的涂鸦、散落的垃圾、坏掉的路灯靠网格员人工巡查拍照上报一天走两万步也拍不全漏报率高还不及时。这个标题里的数据集就是冲着这个场景来的——19263张真实街道图像覆盖涂鸦、垃圾、故障路灯等11个市容问题类别同时给了VOC和YOLO两种标注格式解压就能进训练流程。对算法工程师来说这意味着不用从零标注、不用纠结格式转换直接把这批数据喂给检测框架就能起跑。对项目负责人来说市容检测的模型底子可以从这份数据上长出来后续再接增量数据做持续迭代。这篇文章我会从数据集内部结构拆起讲到类别难点、训练配置、高频踩坑最后落到评估和部署的实用技巧让你拿到这份压缩包后能少走弯路。2. 数据集结构拆解VOC与YOLO双格式的目录布局与选型理由2.1 JPEGImages、Annotations、ImageSetsVOC三件套怎么配合拿到这份.7z压缩包第一步不是急着解压训练而是先把目录结构看清楚。VOC格式的组织方式非常固定解压后你会看到典型的VOC布局JPEGImages存原始图片Annotations存对应的XML标注文件ImageSets/Main存训练集和验证集的划分索引。这种布局在目标检测领域沿用了十几年PyTorch的TorchVision、老版Detectron、以及各种基于VOC的评测脚本都能直接读。先解压并确认结构# 先列出压缩包内容确认没有异常路径或多余嵌套目录 7z l 智慧城市街道涂鸦垃圾故障路灯市容检测数据集VOCYOLO格式19263张11类别.7z | head -40 # 解压到工作目录 mkdir -p /data/smart_city_det 7z x 智慧城市街道涂鸦垃圾故障路灯市容检测数据集VOCYOLO格式19263张11类别.7z -o/data/smart_city_det解压后看一眼顶层布局cd /data/smart_city_det find . -maxdepth 2 -type d | sort正常情况下会看到JPEGImages、Annotations、ImageSets以及YOLO格式的images和labels目录。这里有个细节值得留意有些双格式数据集会把YOLO的labels直接放在根目录有些会再套一层yolo子目录。我建议你解压后先跑一遍目录树不要凭经验假设路径。VOC标注文件是XML格式一个文件对应一张图里面记录了每个目标的类别名和边界框坐标。用一段Python脚本快速检查标注完整性import xml.etree.ElementTree as ET import os ann_dir Annotations xml_files os.listdir(ann_dir) print(fXML标注文件总数: {len(xml_files)}) # 抽查第一个标注文件的结构 tree ET.parse(os.path.join(ann_dir, xml_files[0])) root tree.getroot() for obj in root.findall(object): name obj.find(name).text bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) print(f类别: {name}, 框坐标: ({xmin}, {ymin}) - ({xmax}, {ymax}))这段脚本的核心作用是验证标注文件能否被正确解析。坐标是绝对像素值单位是图片原始分辨率这一点和YOLO格式有本质区别——YOLO的坐标是归一化到0~1之间的相对值混用必翻车。另外还要确认Annotations目录下的XML数量和JPEGImages里的图片数量一致如果不一致说明数据集有缺失训练前必须补齐或剔除对应样本。2.2 labels目录下的txtYOLO归一化坐标与类别编号YOLO格式的核心是每个图片对应一个同名.txt文件文件里每一行描述一个目标格式为类别编号 x_center y_center width height四个坐标值全部归一化到0~1。这是YOLO系列和Ultralytics框架的通用输入格式和VOC的XML完全不同。用一个labels目录下的文件示例来说明# 查看一个YOLO标注文件的内容 cat labels/street_000001.txt # 输出示例: # 1 0.5234 0.6812 0.1542 0.0871 # 0 0.1120 0.4456 0.0788 0.0532第一列是类别编号从0开始计数对应数据集的类别清单。后面四列分别是归一化后的中心点x、中心点y、框宽、框高。注意YOLO格式没有坐标范围校验如果值大于1或者出现负数说明标注有问题训练时loss会异常跳动。我自己处理数据集时习惯先用Python批量扫一遍所有txt检查坐标是否合法import os label_dir labels bad_files [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: bad_files.append((fname, 列数不对)) continue cls_id int(parts[0]) coords [float(x) for x in parts[1:]] if any(v 0 or v 1 for v in coords): bad_files.append((fname, 坐标越界)) break if coords[2] 0 or coords[3] 0: bad_files.append((fname, 宽高为负)) break print(f异常文件数: {len(bad_files)}) for fname, reason in bad_files[:10]: print(f{fname}: {reason})坐标越界和宽高为负是YOLO标注最常见的两类数据错误原因通常是VOC转YOLO时的系数算错或者源标注本身就有问题。这份数据集的制作者如果标注质量靠谱这批检查应该全过但你仍然值得跑一遍——因为训练到一半发现loss炸了再回头查数据浪费的时间远多于这几分钟。2.3 双格式同时提供的真实价值框架兼容与二次迁移为什么数据集要同时给VOC和YOLO两种格式我理解这是为了覆盖两拨使用者。第一拨人用的是老牌检测框架比如TorchVision的Faster R-CNN、SSD这些框架的官方数据加载器原生支持VOC的XML标注给VOC格式就能直接跑。第二拨人用的是YOLO系从YOLOv5到YOLOv8、v9、v11Ultralytics生态统一吃的就是YOLO格式的txt标注给YOLO格式就能直接训练。双格式省掉了转换这一步也省掉了转换过程中的坐标错位风险。但双格式的价值不止于此。我在实际项目里经常需要做标注格式的二次迁移比如把这份数据转成COCO格式去做实例分割或者转成特定的接口格式给边缘设备推理用。有VOC的XML作为中间桥梁转换逻辑非常清晰先解析XML拿到绝对坐标和类别名再按目标格式组织输出。如果是靠YOLO的txt反推因为坐标已经归一化还得重新乘以图片宽高中间多一步不说图片尺寸如果和原始不一致反推出的绝对坐标就错了。所以我的建议是日常训练用YOLO格式做格式迁移和交叉验证时回VOC的XML去找源头信息。3. 11个类别的检测难点涂鸦、垃圾、故障路灯各自的坑3.1 类别清单与难度分级先确认class定义再看训练目标标题里点名了涂鸦、垃圾、故障路灯这三个类别但完整的11个类别清单在解压后的类别定义文件里。以我的经验这类市容数据集的类别定义一般在classes.txt、data.yaml或者某个说明文档里。拿到数据集的第一件事就是把这个清单读出来因为后续所有训练配置、类别映射、结果解读都依赖它。# 常见的位置YOLO目录下的classes.txt或data.yaml find /data/smart_city_det -name classes.txt -o -name *.yaml | head -5 # 查看类别清单 cat /data/smart_city_det/classes.txt市容检测的类别有一个共性不是所有类别难度都一样。以我接触过的同类项目经验涂鸦的形态变化极大墙面上的喷漆、贴纸、刻痕在视觉特征上完全不同垃圾有塑料袋、纸箱、饮料瓶、建筑废料尺寸跨度从几厘米到一两米故障路灯虽然目标结构清晰但夜间低光照和灯杆遮挡会严重影响检测效果。第一轮训练前建议把11个类别分成简单、中等、困难三档简单档包括那些形态稳定、尺寸适中的类别困难档就是涂鸦、小件垃圾这一类。这个分级决定了后续数据增强和损失函数调优的重点方向。3.2 小目标与密集场景垃圾和涂鸦为什么天然难检市容检测的难点和通用目标检测不一样。像行人检测、车辆检测目标通常占据画面较大面积形态也相对规整。但街道上的垃圾和涂鸦往往是小目标或极端宽高比目标。一块巴掌大的纸屑在1920×1080的巡检图像里可能只有20×20像素一只涂鸦喷漆可能是一米宽但只有10厘米高的窄长条。这两种情况分别对应小目标检测和极端长宽比目标恰好是检测模型最不擅长的两个方向。小目标问题出在下采样上。YOLOv8的检测头分别在8倍、16倍、32倍下采样特征图上做预测一个20×20像素的目标在32倍下采样后只剩下不到1个像素的信息模型根本认不出来。这也是为什么训练市容检测模型时输入分辨率的选择会直接决定小目标召回率。这部分的处理策略我在第4章第3节和第5章第4节会展开讲这里先记住一个结论不要用默认的640×640直接训先统计一下标注框的尺寸分布再决定。密集场景是另一个让人头疼的问题。垃圾在街道上经常是聚集出现的一处垃圾堆里可能有七八个小目标互相紧挨甚至重叠。这时候NMS的阈值设置很关键阈值设高了会合并掉相邻的独立目标设低了同一目标可能出现多个重复框。如果用YOLO系列iou0.4左右是我在密集目标场景下的起步值具体还要看评估结果微调。3.3 类别不均衡数样本分布再决定要不要调损失函数市容数据集的类别不均衡问题通常非常严重。故障路灯和大型垃圾堆这类目标一张图里可能就出现一两次标注样本总量相对少而小型垃圾、涂鸦这类目标单张图里可能密集出现几十个样本总量大得多。如果不做任何处理直接训练模型会倾向于学到大类别的特征小类别几乎不召回。训练前先统计一下每个类别的实例数量import os from collections import Counter label_dir labels class_counter Counter() image_count Counter() for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: cls_id int(line.split()[0]) class_counter[cls_id] 1 image_count[cls_id] 1 print(类别实例分布:) for cls_id, cnt in sorted(class_counter.items()): print(f类别 {cls_id}: {cnt} 个实例, 出现在 {image_count[cls_id]} 张图中)统计完你会看到差距常见的情况是最大类别和最小类别相差10倍以上。处理方式有三个层次最简单的是在损失函数里加类别权重让模型对少数类的误判付出更高代价YOLO可用的方法包括在损失权重参数上调高对应类别的权重进阶做法是对少数类做过采样训练时多抽几次包含这些类别的图片再往上就是针对性数据增强比如对包含故障路灯的图片做随机裁剪放大让模型更多看到这类目标的细节。先看分布再选手段而不是一上来就套复杂方案这是我在多个数据集上验证过的路径。4. 用YOLOv8跑通市容检测训练最小复现命令与参数配置4.1 解压校验与数据集划分训练前必须做的三件事训练开始前有三件事必须做校验数据完整性、确认图片和标注对得上、按比例划分训练集和验证集。数据完整性我在第2.1节已经写了脚本这里重点说划分。有些双格式数据集会直接提供ImageSets/Main/train.txt和val.txt如果有直接用如果没有自己做随机划分。用一组Python脚本完成划分import os import random from shutil import copy2 random.seed(42) image_dir JPEGImages # VOC原始图片目录 label_dir labels # YOLO标注目录 output_base yolo_split # 读取所有图片文件名假设jpg格式 images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(images) # 按 8:2 划分训练集和验证集 split_idx int(len(images) * 0.8) train_images images[:split_idx] val_images images[split_idx:] def organize(file_list, split_name): img_out os.path.join(output_base, split_name, images) lbl_out os.path.join(output_base, split_name, labels) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for fname in file_list: stem os.path.splitext(fname)[0] copy2(os.path.join(image_dir, fname), os.path.join(img_out, fname)) lbl_src os.path.join(label_dir, stem .txt) if os.path.exists(lbl_src): copy2(lbl_src, os.path.join(lbl_out, stem .txt)) else: print(f警告: {stem} 缺少YOLO标注文件) organize(train_images, train) organize(val_images, val) print(f训练集: {len(train_images)} 张, 验证集: {len(val_images)} 张)这段脚本做了三件事随机打乱图片顺序、按8:2比例切分、把图片和标注复制到YOLO训练框架约定的目录结构。random.seed(42)固定了随机种子保证每次划分结果一致方便后续复现实验。还有一个值得注意的点划分后要检查验证集里是否覆盖了全部11个类别如果某些类别只出现在训练集而在验证集里没有样本评估时那一类的AP会直接为0看起来像模型没学好实际是数据划分的问题。4.2 数据配置yaml路径、类别名、类别数一个都不能错YOLOv8Ultralytics训练的第一步是准备好数据配置文件。这个yaml文件定义了数据集路径、类别数量和类别名称列表是模型训练的地图。一个典型配置如下# smart_city.yaml # 训练和验证数据的根目录 path: /data/smart_city_det/yolo_split # 训练集和验证集的图片目录相对path train: train/images val: val/images # 类别数量与类别名列表按数据集classes.txt的顺序填写 # 下面以3个类别为例实际需要填全11个类别 names: 0: graffiti 1: garbage 2: broken_streetlight这里最大的坑是类别名顺序必须和classes.txt里的定义完全一致。YOLO格式的txt标注里存的是类别编号yaml里的names映射决定编号对应的类别名。如果映射错位比如把graffiti和garbage的编号对调了训练不会报错loss也会正常下降但输出的检测结果会张冠李戴——模型明明框住了垃圾却说是涂鸦。这种错误最隐蔽因为训练指标看起来一切正常只有到实际推理时才发现类别全乱了。我一般会在写完yaml后做一个快速验证随机挑3张训练图片加载模型做一次前向推理看预测的类别名和图片内容是否吻合。这一步只要几分钟但能避免训练几十个epoch后才发现映射错了的悲剧。4.3 启动训练与日志观察从loss曲线判断模型有没有学进去配置就绪后启动训练命令行如下# 使用YOLOv8 nano模型适合第一批验证 yolo detect train \ data/data/smart_city_det/yolo_split/smart_city.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ projectruns/smart_city \ nameexp1参数说明modelyolov8n.pt是用COCO预训练权重做迁移学习初始化比从零训练收敛快得多也更容易学到通用特征imgsz640是输入分辨率对市容小目标场景建议后续试试896或1280虽然训练速度慢一些但对小目标的召回提升明显batch16取决于GPU显存显存不够就降到8device0指定第一块GPU。训练启动后观察两个东西一是loss曲线是否稳定下降前10个epoch的box_loss和cls_loss如果出现剧烈震荡或者不降反升大概率是学习率太高或数据有问题二是验证集指标的变化趋势正常的mAP曲线会在20个epoch内有一个明显爬升如果到30个epoch还在低位徘徊就该停下去查数据了。训练日志在runs/smart_city/exp1/目录下用TensorBoard或直接看终端输出都行。# 训练结束后查看结果指标 yolo detect val \ modelruns/smart_city/exp1/weights/best.pt \ data/data/smart_city_det/yolo_split/smart_city.yaml验证命令会输出每个类别的AP、mAP50和mAP50-95。这里不要只盯着mAP50看市容检测的小目标类别在mAP50-95上通常会掉得很惨如果某一类别AP50正常但AP50-95很低说明模型对目标定位不够精确需要更大的输入分辨率或者更细致的特征融合。这组指标是后续调优的方向标做一轮基础训练后先花时间读一遍结果比自己乱改参数有效得多。5. 市容检测训练避坑清单5个高频翻车点与排查方法5.1 坐标格式混用VOC的xml当YOLO的txt用框全乱现象训练能启动loss也在降但训练完验证时mAP几乎为0可视化预测框全部错位——有的框画到了图片外面有的框大小只有几个像素。原因VOC的XML存的是绝对像素坐标YOLO的txt存的是归一化相对坐标。有人图省事直接把XML里的xmin、ymin、xmax、ymax当YOLO格式用没有除图片宽高做归一化。1920×1080的图片里一个xmin500的框在YOLO格式里应该是500/1920≈0.26直接用500的话模型看到的是远超1的坐标值训练直接乱套。解决写一个VOC转YOLO的脚本核心就是读取XML的目标框和图片宽高做归一化换算注意宽度 (xmax - xmin) / img_width高度 (ymax - ymin) / img_height。转换后按第2.2节的检查脚本重新扫一遍确认所有坐标都在0~1之间再开训。这一类问题我在不同项目里至少踩过三次每次都是省了转换脚本的时间结果花更多时间排查。5.2 类别编号与类别名错位训练不报错但结果全错现象训练过程一切正常loss收敛漂亮mAP看着也不错但推理时模型把涂鸦识别成垃圾把故障路灯识别成别的类。原因yaml文件里的names列表顺序和数据集classes.txt不一致。数据集制作者定义0graffiti, 1garbage你写的yaml里0garbage, 1graffiti模型学到的0号特征对应涂鸦但你告诉它0是垃圾。所有指标都基于编号计算内部没有报错机制能检测这种语义错位。解决训练前直接读取数据集的类别定义文件classes.txt或yaml用脚本解析后和你的训练配置做自动比对而不是肉眼确认。再把训练集里某张图的标注类别和图片内容快速目检一下。这套检查流程应该写进你的数据集准备清单里每次换新数据集都跑一遍。5.3 样本不均衡导致漏检少数类几乎不召回现象验证集上大类别比如普通垃圾的AP能到0.9但某个少数类比如故障路灯的AP只有0.2甚至更低检测结果里几乎找不到这类目标。原因标注实例数量差距过大模型在训练中没见过足够多的少数类正样本特征学不出来。我在第3.3节的统计脚本里见过最极端的情况某类别只有几百个实例而最大类别有十几万实例差了三个数量级。解决先按第3.3节统计分布如果少数类和多数类差距超过20倍考虑三个手段叠加一是给少数类加更高的损失权重二是对包含少数类的图片做复制增强让它在每个epoch里出现更多次三是对少数类目标做局部裁剪放大把小目标放大后再拼回图像。这三招按顺序试每加一招重新训练看AP变化不要一次全上否则分不清哪个手段有效。5.4 图片分辨率差异大resize策略影响小目标召回现象训练时imgsz设为640验证时某些小目标类别的AP50只有0.3左右怎么调损失函数都上不去。原因市容数据集里的图片来源可能很杂手机拍摄的、监控截屏的、无人机航拍的都有原始分辨率从720p到4K差异巨大。统一resize到640×640时高分辨率图片里的小目标会被压缩到几乎看不见低分辨率图片里本来就不清晰的目标resize放大后糊成一团。解决先统计数据集的原始尺寸分布确定一个能覆盖大多数图片的目标分辨率。1920×1080的源图我一般建议imgsz至少设到896或1280。显存有限的场景考虑用rectTrue做矩形推理让模型按原始宽高比批量训练而不是强行压成正方形。另外可以尝试把高分辨率和低分辨率的图片分开成两个数据子集分别训练或做多尺度训练这个方案工程量大一些但对混合源数据集确实有效。5.5 7z压缩包解压异常先查完整性再谈训练现象解压到一半报CRC Error或者Unexpected end of archive有些文件解出来是0字节标注文件内容截断。原因.7z文件在传输或存储过程中损坏或者下载不完整。这类数据集动辄几个GB传输中断、网盘同步冲突都可能导致压缩包不完整。解决解压前先用7z t命令测试压缩包完整性# 测试压缩包是否有损坏 7z t 智慧城市街道涂鸦垃圾故障路灯市容检测数据集VOCYOLO格式19263张11类别.7z如果输出里出现警告或错误说明压缩包有问题重新下载或者找数据源补文件。解压完成后还要做一轮文件级校验统计JPEGImages和Annotations的文件数量是否一致再按第2.1节的脚本抽查几个XML能否正常解析。这一步虽然基础但能在训练前把数据坏了这个嫌疑从排障清单里划掉后面出问题就不用回头怀疑数据本身了。6. 从mAP到能上线的市容检测评估、调优与部署的一个实用技巧模型训练完验证集mAP达标只是万里长征走了一半。市容检测项目上线前最关键的一步是按实际场景的置信度阈值调优。通用目标检测默认的置信度阈值是0.25但市容场景的漏检和误检代价完全不同漏掉一个故障路灯可能导致安全隐患误报一个垃圾堆顶多是保洁人员白跑一趟。这种不对称的代价决定了你上线后的置信度阈值不应该用默认值。调阈值的做法是在验证集上扫描不同置信度下的表现找漏检率和误检率的平衡点# 扫描不同conf阈值下的指标变化 for conf in 0.05 0.10 0.15 0.20 0.25 0.30 0.40 0.50 do yolo detect val \ modelruns/smart_city/exp1/weights/best.pt \ data/data/smart_city_det/yolo_split/smart_city.yaml \ conf$conf done扫描完成后把每个阈值下mAP50和各类别AP拉成一张表你会看到阈值从0.25放到0.05涂鸦和垃圾这类难检类别的召回明显提升但误检数量也上来了阈值收到0.4以上误检少了但故障路灯这类少数类召回又掉了。我一般会针对不同类别设置不同阈值——比如垃圾检测用较低的阈值保证召回故障路灯用中等阈值平衡精度这个能力在YOLO的推理接口里可以通过逐类设置conf阈值实现值得好好利用。部署到边缘设备时有一个我保留至今的习惯先导成ONNX格式做一次全流程测试而不是直接用PyTorch模型上设备。from ultralytics import YOLO # 加载训练好的模型 model YOLO(runs/smart_city/exp1/weights/best.pt) # 导出ONNX格式用于边缘设备推理 model.export(formatonnx, imgsz896, halfTrue) # 导出的onnx文件在相同目录下 # 用onnxruntime做一次推理对比 import onnxruntime as ort import numpy as np import cv2 session ort.InferenceSession(runs/smart_city/exp1/weights/best.onnx) img cv2.imread(test_street.jpg) img_resized cv2.resize(img, (896, 896)) input_data img_resized.astype(np.float32) / 255.0 input_data np.transpose(input_data, (2, 0, 1))[None, :, :, :] outputs session.run(None, {session.get_inputs()[0].name: input_data})这段代码的核心价值在于验证导出链路没有精度损失。ONNX导出后用同一个测试图片对比PyTorch推理和ONNX推理的检测结果如果框的坐标和类别一致说明部署链路是可信的可以继续做量化、TensorRT加速等后续优化。这一步在每一个项目里都是必须做的它能保证训练环境和部署环境之间的模型行为不发生意外变化——毕竟部署前的惊悚体验没有人愿意经历第二次。这个流程走完你就有了一个能从数据、训练、评估到部署闭环的市容检测方案。我自己每次拿到新数据集都会按这个顺序走一遍数数据、查标注、跑基线、扫阈值、导出部署五个环节一环扣一环省掉了大量来回排查的折返时间。这份数据集的双格式设计确实省了格式转换的功夫但数据质量校验和训练配置的细心程度决定了它在你手里能不能跑出该有的效果。希望这些踩过的坑和验证习惯能帮到你。本文还有配套的精品资源点击获取