
简介智慧城市街道涂鸦垃圾故障路灯市容检测数据集提供19263张街道实景图片覆盖涂鸦、垃圾堆放、故障路灯等11类市容问题面向智慧城市巡检、算法竞赛和学术研究可直接用于目标检测模型的训练与评测。数据采用Pascal VOC与YOLO双格式每张jpg均配有同名xml和txt标注XML适合VOC系列标注工具解析TXT可直接被YOLO系列框架读取方便在不同深度学习环境中切换整包约978.91MBjpg、xml、txt三类文件各19263个合计超过5.7万个文件。需特别留意过半样本为四图拼接增强图片下载前务必查看预览图确认标注与画面一致性。数据可辅助城管部门、安防厂商和高校实验室开展市容异常检测算法验证支撑从标注解析到模型部署的完整链路。目前已有224人浏览学习适合需要真实街景异常样本的开发者快速构建市容智能检测方案。1. 这份市容检测数据集到底能帮你省多少事市容巡查拍回来的照片堆在硬盘里靠人工把涂鸦、垃圾堆、故障路灯一张张挑出来分门别类一天最多处理几百张漏检率还高。智慧城市街道涂鸦垃圾故障路灯市容检测数据集解决的就是这个需求19263张市容实拍图、11个目标类别同时给了VOC和YOLO两种标注格式压缩成.7z发布。适合正在做城市管理算法、市容巡检系统、或者想拿真实场景数据练YOLO系列模型的开发者和算法工程师。这份数据集的价值不在于“多”而在于“省”省掉从零采集、清洗、标注的时间也省掉了把VOC转成YOLO时踩坑的功夫。但拿到压缩包只是第一步解压、核对、转格式、划分、训练这一整条链路里还有不少细节下面按实际操作的顺序拆开讲。2. 从.7z到可训练的目录解压方式与VOC/YOLO双格式结构2.1 Linux和Windows下解压.7z命令、密码参数和校验方式把压缩包拿到手先别急着双击先确认一件事这个包是不是加密的。市容数据集经常带密码发布密码错了或者版本不对解压到一半就报CRC错误非常容易翻车。Linux下我一般用p7zip解压。Debian/Ubuntu系的安装命令sudo apt install p7zip-full装好后用标准命令解压。注意-o参数后面跟输出目录没有空格写错了7z会把目录名当成文件名的一部分7z x 智慧城市街道涂鸦垃圾故障路灯市容检测数据集VOCYOLO格式19263张11类别.7z -o/home/user/dataset -y如果压缩包带密码7z x 数据集.7z -o/home/user/dataset -p你的密码 -yWindows下装了7-Zip之后右键菜单里直接有“解压到当前文件夹”命令行操作方式一样7z.exe路径在C:\Program Files\7-Zip\。.7z不是普通zipWindows自带的资源管理器解出来中文文件名常常乱码所以建议只用7-Zip处理。解压完之后不要急着删压缩包先做校验。7z t会逐个文件校验CRC如果下载过程损坏或者密码不完整这一步直接暴露7z t 智慧城市街道涂鸦垃圾故障路灯市容检测数据集VOCYOLO格式19263张11类别.7z看到“Everything is Ok”才算通过。这一步是后悔药等训到一半发现图片打不开再排查就晚了。2.2 VOC和YOLO两种目录结构Annotations与labels的关系解压之后通常能看到两套结构并存。VOC系列的标注是XML文件YOLO系列的标注是TXT文件两者描述的是同一批图片的同一批框只是格式不同。VOC标准结构是三个目录加一个清单目录VOCdevkit/ ├── JPEGImages/ # 19263张图片 ├── Annotations/ # 19263个XML标注 └── ImageSets/ └── Main/ # train.txt / val.txt / test.txtYOLO结构有两种组织方式。一种是把图片分到images/train/、images/val/标注分到labels/train/、labels/val/另一种是图片和TXT平铺在两个目录靠train.txt清单里的路径去关联。这份数据集如果同时给了VOC和YOLO说明作者已经把两边整理齐了但train/val/test划分不一定两边都给了——经常出现VOC里有ImageSets/Main的划分文件而YOLO目录下只有平铺的images/和labels/。这种情况后面第四章再处理。先理解两种标注格式的对应关系。VOC的XML里存的是绝对像素坐标YOLO的TXT里存的是归一化的中心点加宽高两者转换需要图片实际宽高参与计算。这也是为什么要先核对JPEGImages里图片的尺寸而不是想当然按标称尺寸转。2.3 列文件清单先确认“图片数 XML数 TXT数”解压完第一件事数数。19263张图片那Annotations里应该有19263个XMLlabels里也应该有19263个TXT。用find数一遍find JPEGImages -name *.jpg | wc -l find Annotations -name *.xml | wc -l find labels -name *.txt | wc -l如果三个数字对不齐别急着转格式。先找出缺的是哪些# 找出有图片但没标注的文件名 ls JPEGImages | sed s/\.jpg$// img_list.txt ls Annotations | sed s/\.xml$// ann_list.txt comm -23 img_list.txt ann_list.txtcomm命令需要列表先排序没有排序的话用sort处理一下。缺标注的图片直接剔除训练集而不是让它带着空标注进入YOLO流程——空TXT在YOLO训练里会导致那一张图不参与损失计算系统不会报错但会让你的样本数悄悄缩水这就是典型的“黑匣子”问题。提示这个数据集的类别覆盖的是市容问题目标标题点名的涂鸦、垃圾、故障路灯是核心三个类别其余类别一般按城市管理常见要素补齐。先把classes.txt或names.txt打开看一遍确认类别顺序后面所有环节都依赖这个顺序。3. 构建类别映射表把VOC XML转成YOLO txt的完整脚本3.1 VOC的XML里到底存了什么一份VOC标注XML的典型结构是根节点annotation下面有folder、filename、size宽高和深度以及若干object节点。每个object节点包含name类名、pose、truncated、difficult和bndbox。bndbox里是xmin、ymin、xmax、ymax四个绝对像素坐标。这份数据集描述的是市容目标涂鸦、垃圾这类目标形状不规则标注框很多是紧贴目标外接矩形的但个别样本可能标得松。转换的时候遇到difficult节点取值1的框通常直接跳过——YOLO训练不需要“难例不参与损失”这种VOC风格逻辑。有个细节容易被忽略XML里的类名可能和classes.txt里的字符串不一致比如XML里叫garbage而清单里叫litter字符串匹配不上那这个框就被静默丢掉了。转换前把XML里的类名集合先抓一遍import xml.etree.ElementTree as ET from pathlib import Path names set() for xml_path in Path(Annotations).glob(*.xml): root ET.parse(xml_path).getroot() for obj in root.iter(object): names.add(obj.find(name).text.strip()) print(names)输出结果和classes.txt逐项比对差哪个改哪个。光这一步就能拦住很多转换完发现“少了很多框”的玄学问题。3.2 转换脚本XML解析、归一化、边界裁剪一次到位VOC转YOLO的完整脚本如下这是处理数据集用于YOLO训练最核心的一步# convert_voc2yolo.py VOC XML 转 YOLO TXT 用法: python convert_voc2yolo.py --xml_dir Annotations --out_dir labels --classes classes.txt import xml.etree.ElementTree as ET import argparse from pathlib import Path def convert_one(xml_path: Path, out_dir: Path, class2id: dict): root ET.parse(xml_path).getroot() size root.find(size) if size is None: print(跳过(无size):, xml_path.name) return width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in class2id: print(f未知类名 {name} 于 {xml_path.name}) continue # 跳过difficult1的框 diff obj.find(difficult) if diff is not None and int(diff.text) 1: continue bb obj.find(bndbox) x1 float(bb.find(xmin).text) y1 float(bb.find(ymin).text) x2 float(bb.find(xmax).text) y2 float(bb.find(ymax).text) # 归一化中心点与宽高都除以图片尺寸 x_c ((x1 x2) / 2) / width y_c ((y1 y2) / 2) / height bw (x2 - x1) / width bh (y2 - y1) / height # 边界裁剪坐标可能略微超界裁剪到[0,1] x_c max(0.0, min(x_c, 1.0)) y_c max(0.0, min(y_c, 1.0)) bw max(0.0, min(bw, 1.0)) bh max(0.0, min(bh, 1.0)) if bw 1e-6 or bh 1e-6: continue lines.append(f{class2id[name]} {x_c:.6f} {y_c:.6f} {bw:.6f} {bh:.6f}) if lines: out_dir.mkdir(parentsTrue, exist_okTrue) (out_dir / (xml_path.stem .txt)).write_text( \n.join(lines), encodingutf-8 ) def main(): parser argparse.ArgumentParser() parser.add_argument(--xml_dir, requiredTrue, helpVOC XML目录) parser.add_argument(--out_dir, requiredTrue, helpYOLO txt输出目录) parser.add_argument(--classes, requiredTrue, help类别清单一行一类行序即id) args parser.parse_args() lines Path(args.classes).read_text(encodingutf-8).splitlines() class2id {name.strip(): i for i, name in enumerate(lines) if name.strip()} for xml_path in Path(args.xml_dir).glob(*.xml): convert_one(xml_path, Path(args.out_dir), class2id) print(转换完成) if __name__ __main__: main()执行命令python convert_voc2yolo.py --xml_dir Annotations --out_dir labels --classes classes.txt脚本里几个关键点值得说清楚。class2id的构建用的是字典映射而非列表索引类别清单里有一行空行就会导致id错位所以构建时过滤了空行。size节点缺失时直接跳过而不是抛异常因为个别标注文件不完整跳过让整个批处理能一口气跑完最后统计跳过数量再决定要不要处理。边界裁剪用的是max/min组合而不是直接丢弃市容场景里目标贴近图像边缘时标注人员经常把框画出边界YOLO的标签要求数值必须在[0,1]区间不裁剪会让训练损失变成NaN。3.3 转完怎么验证把TXT画回图片上看框位转换完不是数一下TXT数量就完事坐标转错了根本看不出来。我习惯抽样20张图把TXT里的框画回去肉眼比一下框和目标的贴合程度# check_labels.py import cv2 from pathlib import Path classes Path(classes.txt).read_text(encodingutf-8).splitlines() classes [c.strip() for c in classes if c.strip()] img_dir Path(JPEGImages) label_dir Path(labels) out_dir Path(check) out_dir.mkdir(exist_okTrue) for img_path in sorted(img_dir.glob(*.jpg))[:20]: img cv2.imread(str(img_path)) if img is None: print(图片损坏:, img_path.name) continue h, w img.shape[:2] txt_path label_dir / (img_path.stem .txt) if not txt_path.exists(): print(缺失标签:, img_path.name) continue for line in txt_path.read_text(encodingutf-8).splitlines(): parts line.split() if len(parts) ! 5: print(格式异常:, txt_path.name, line) continue cid, x_c, y_c, bw, bh map(float, parts) cid int(cid) 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, 255, 0), 2) cv2.putText(img, classes[cid], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(str(out_dir / img_path.name), img) print(已生成:, out_dir / img_path.name)这个脚本同时检查了四件事图片能不能被OpenCV读出来TXT文件是否存在TXT每行是否恰好5个字段类别id是否在合法范围内。如果cid超过classes的长度说明转换脚本在类别映射时出了问题回头查class2id的构建代码。画出框之后重点看两类样本一类是宽高比极端的框宽高比超过5另一类是框紧贴图片边缘的样本这两类最容易暴露出坐标缩放错误。4. 训练前体检11个类别的分布、坏图筛查与数据划分4.1 统计每个类别的框数量长尾分布决定后续策略正式训练之前先看清楚11个类别里每个类别到底有多少目标框。直接统计TXT第一列的类别id# count_classes.py from pathlib import Path from collections import Counter classes Path(classes.txt).read_text(encodingutf-8).splitlines() classes [c.strip() for c in classes if c.strip()] counter Counter() total_boxes 0 for txt_path in Path(labels).glob(*.txt): for line in txt_path.read_text(encodingutf-8).splitlines(): if not line.strip(): continue cid int(line.split()[0]) counter[cid] 1 total_boxes 1 print(总框数:, total_boxes) for cid in range(len(classes)): print(f{classes[cid]}: {counter[cid]})市容检测数据集的常见特点是长尾重垃圾、涂鸦这类常见问题样本可能成千上万故障路灯这类偶发问题可能只有几百个框。头部类别主导损失尾部类别学不好最终mAP被尾部类别拖垮。看到分布之后决定对策——如果某个类别框数不到总数5%后面要么做样本增强要么直接用带类别权重的损失函数别指望它自己学出来。提示这里看的是“框数量”不是“图片数量”。一张图里有3个垃圾堆就算3个框。类别间差距看框数更准确因为训练时损失是按框算的不是按图算的。4.2 坏图与空标注排查三件事一口气做完数据体检最少做三件事图片能不能解码、TXT是否有内容、XML的size和图片实际尺寸是否一致。# check_dataset.py import cv2 from pathlib import Path img_dir Path(JPEGImages) label_dir Path(labels) xml_dir Path(Annotations) bad_images, empty_labels, size_mismatch [], [], [] for img_path in img_dir.glob(*.jpg): img cv2.imread(str(img_path)) if img is None: bad_images.append(img_path.name) continue h, w img.shape[:2] # 检查对应标签 txt_path label_dir / (img_path.stem .txt) if not txt_path.exists() or txt_path.stat().st_size 0: empty_labels.append(img_path.name) # 检查XML的size与图片实际尺寸 xml_path xml_dir / (img_path.stem .xml) if xml_path.exists(): import xml.etree.ElementTree as ET root ET.parse(xml_path).getroot() size root.find(size) if size is not None: xw int(size.find(width).text) xh int(size.find(height).text) if xw ! w or xh ! h: size_mismatch.append(img_path.name) print(坏图:, len(bad_images), bad_images[:10]) print(缺失/空标签:, len(empty_labels), empty_labels[:10]) print(尺寸不一致:, len(size_mismatch), size_mismatch[:10])尺寸不一致这条最容易被忽略但危害很大如果XML里写的是缩略图尺寸而实际图片是原图VOC转YOLO时归一化坐标按错误尺寸算所有框都会偏移。出现这种问题的时候优先信任图片的实际像素尺寸用cv2.imread读出来的(h, w)去重新计算归一化坐标而不是信XML里的size字段。跑完这个脚本有问题的那批文件从训练目录里移出去别留着。4.3 数据划分按文件名stem切分不按目录切分划分训练/验证/测试集优先按文件名stem而不是按目录。原因是市容数据集的图片可能是按街道或时间分目录收集的同一目录里的图片背景高度相似按目录切会让训练集和验证集出现场景重叠验证指标虚高。洗牌后按7:2:1切分是常见做法# split_data.py import random from pathlib import Path img_dir Path(JPEGImages) train_txt Path(train.txt) val_txt Path(val.txt) test_txt Path(test.txt) stems [p.stem for p in img_dir.glob(*.jpg)] random.seed(42) random.shuffle(stems) n len(stems) train stems[:int(n * 0.7)] val stems[int(n * 0.7):int(n * 0.9)] test stems[int(n * 0.9):] def write_list(path, items): path.write_text(\n.join(fJPEGImages/{s}.jpg for s in items), encodingutf-8) write_list(train_txt, train) write_list(val_txt, val) write_list(test_txt, test) print(ftrain{len(train)} val{len(val)} test{len(test)})random.seed(42)固定了随机序列同一份数据在另一台机器上能复现完全相同的划分这个习惯在多人协作和论文复现时很有用。划分之后的清单里存的是相对路径YOLO训练时data.yaml里指定path根目录YOLO会拿清单里的相对路径去根目录下找文件。注意清单格式遵循YOLO的约定——每行一个图片路径不带前导/否则会被当成绝对路径。5. 常见问题与排查从解压报错到训练崩溃的5条踩坑记录5.1 7z密码正确却一直报Data Error现象输入了正确密码7z解压到一半报Data Error in encrypted file或者直接提示CRC Failed再解压一次还是同一个文件出错。原因最常见的是7-Zip版本太旧对新的AES-256加密头支持不完整其次是压缩包在制作时勾选了“加密文件名”普通的解压参数拿不到文件名列表也会出现诡异报错。还有一个坑在Linux下系统locale不是UTF-8中文文件名解码失败报错信息看起来像密码错误其实不是。解决先把7-Zip升级到19.00以上版本再试一次。加密文件名导致的报错解压命令里显式加上密码参数-p密码不要等它交互式提示。Linux下处理中文目录名先设locale再解压export LANGzh_CN.UTF-8 7z x 数据集.7z -o/dataset -y如果依然报CRC错误用7z t逐文件校验定位具体是哪个文件坏了——大概率是压缩包在传输过程中损坏重新下载比强行修复更省时间。5.2 解压后图片和标注文件数量对不上现象JPEGImages里有19200张jpgAnnotations里只有19000多个XML数了好几遍都不齐。原因压缩包整理时有些不规范比如目录下混入了Thumbs.db、.DS_Store、README图片或者个别标注文件被放在嵌套子目录里直接用ls数顶层目录会漏掉。解决用find递归查找而不是lsfind . -name *.xml | wc -l find . -name *.jpg | wc -l再按文件名list比对差异find JPEGImages -name *.jpg -printf %f\n | sed s/\.jpg$// | sort img_names.txt find Annotations -name *.xml -printf %f\n | sed s/\.xml$// | sort ann_names.txt comm -23 img_names.txt ann_names.txt | head -20查出来的缺标注文件直接写进一个exclude.txt训练时过滤掉。不要试图手工补标注市容目标的标注成本很高为几张图人工补框得不偿失。5.3 VOC转YOLO之后类别id全部错位现象转换后画框检查发现“垃圾”类别上画着“涂鸦”的标签所有框错位但位置正确。原因有人在构建类别映射时用了list.index()或者按字母排序而VOC的classes.txt顺序是与数据集制作者约定好的——顺序一变id全乱。另一个常见原因是classes.txt里有隐藏字符比如\r导致最后一个类名匹配不上。解决转换脚本里的class2id严格按classes.txt的文件顺序构建不要排序。检查隐藏字符cat -A classes.txt | head看到行尾有^M说明是Windows换行符用sed -i s/\r$// classes.txt清理。转完之后用3.3节的可视化脚本抽查框的位置和类别文字都对得上再进入训练。5.4 YOLO训练loss直接NaN超界坐标惹的祸现象训练到第1个epoch就出NaN或者loss在前20个iter内震荡到几百然后崩掉log里没有报错。原因标签坐标没有做边界裁剪。市容数据集的标注经常出现框超出图片边界的情况——特别是涂鸦贴墙根、垃圾堆在角落时标框的人顺手就把框画到画面外了。YOLO训练读标签时遇到x_center 1或者width 0梯度直接爆掉。解决用3.2节脚本里带的max/min裁剪逻辑把坐标裁剪到[0,1]区间。训练前再批量检查一遍所有TXT任何一行的5个字段有超出[0,1]的值单独揪出来from pathlib import Path for txt_path in Path(labels).glob(*.txt): for i, line in enumerate(txt_path.read_text().splitlines(), 1): parts line.split() if len(parts) ! 5: print(txt_path, 第, i, 行字段数不对:, line) else: nums list(map(float, parts[1:])) if any(v 0 or v 1 for v in nums): print(txt_path, 第, i, 行坐标超界:, line)5.5 YOLO报告的混淆矩阵总和对不上现象训练完跑yolo val看混淆矩阵发现列加起来不等于真实的GT数量怎么看总差一点怀疑验证数据有问题。原因YOLO输出混淆矩阵默认不是全部样本的计数而是按置信度阈值算出来的——低于阈值被当成背景的不计入任何类别自然对不上。这个“总合不唯一”的现象让不少人误以为自己数据集出问题了。解决了解混淆矩阵的统计口径后再看。YOLO在验证输出里会额外生成一份confusion_matrix.png和一份confusion_matrix_normalized.png前者是计数后者是比例。确认官方实现里对于未检出框的出现处理方式不要在“矩阵数值不等于样本数”上花时间那是正常的。你要关注的是对角线之外的高亮格子——比如“垃圾”被误判成“涂鸦”那才说明类别间视觉特征太接近需要加强数据增强或者补充训练样本。6. 把数据集喂给YOLOv8训练配置、结果验证与落地建议6.1 data.yaml的写法VOC和YOLO两种格式都到位之后最后一公里是训练配置的编写。用YOLOv8训练自己的数据集先把数据集的配置文件写对路径名和类别顺序错了训练会直接报错或者全乱套# dataset.yaml path: /home/user/dataset train: images/train val: images/val # 11个类名按classes.txt原顺序排列id从0开始 names: 0: graffiti 1: litter 2: broken_streetlight # 后面按classes.txt顺序补齐全部11类names里的顺序必须和转换脚本用的classes.txt严格一致YOLO读取TXT第一列的id就是这里的索引。反差会出现的不是names的键值而是训练时TXT映射出来的框类别名和你图里看到的对不上。6.2 训练命令和三个关键参数直接开始训练第一次运行会自动下载预训练模型yolov8s.pt数据集放在指定路径即可yolo detect train \ datadataset.yaml \ modelyolov8s.pt \ epochs120 \ imgsz640 \ batch16 \ patience20 \ close_mosaic10三个参数值得根据这份数据集单独调。imgsz市容目标里垃圾和涂鸦很多是小目标640起步可以但如果验证时发现小目标漏检提到768比调任何阈值都管用。close_mosaicYOLOv8默认最后10个epoch关闭马赛克增强让模型从“看拼贴样本”过渡到“看整图样本”这个参数改成10是经验值不要设成0。patience验证集指标连续20个epoch不涨就早停省时间。6.3 验证时要关注哪几个输出训练结束后主要看三样东西results.png里mAP50和mAP50-95两条曲线的差距——市容场景业务上报给城管系统的要求一般是“找到目标即可”IoU0.5就算命中所以重点看mAP50而不是被mAP50-95迷惑。第二是confusion_matrix.png看难分对出现在哪两个类别之间。第三是val_batch_pred.jpg直接看预测结果画在验证图上的效果漏检多在小目标误检多在大面积纹理复杂区域这个判断对后续优化比任何指标都有用。市容检测这类长尾任务我个人的习惯是验证完不看总mAP直接看尾部类别的AP比如路灯故障这类样本少的类别AP低于0.3先补数据或增强别急着加大模型。把imgsz调上去、把close_mosaic设对这两件事做完大多数情况能比默认配置涨两三个点。希望今天拆解的这些步骤能帮你把这份数据集顺利跑起来少走几段我走过的弯路。本文还有配套的精品资源点击获取