智慧城市市容检测数据集:VOC+YOLO双格式标注与YOLOv8实战指南

发布时间:2026/10/11 19:39:50
智慧城市市容检测数据集:VOC+YOLO双格式标注与YOLOv8实战指南
简介面向智慧城市街道场景的市容检测数据集包含19263张实景图片及对应标注覆盖11个目标类别适合计算机视觉学习者与算法工程师基于YOLO等框架开展街道涂鸦、垃圾堆放、故障路灯等城市管理问题的自动识别研究。数据采用Pascal VOC与YOLO双格式每张图片均配有XML与TXT标注标注内容完整无需额外格式转换即可接入主流目标检测训练与评估流程。资源压缩包为7z格式整体约978.91MB文件总数为2000个其中VOC格式XML标注文件多达1999个另附说明TXT文件结构清晰便于按需筛选使用。数据集已有225人学习/下载可作为市容检测方向的训练语料与算法验证数据便于在真实项目中快速验证模型效果。另需注意超过一半样本为四图拼接的增强图片下载前建议先查看博文预览图确认与项目需求匹配后再使用。1. 智慧城市市容检测数据集19263张双格式样本一次覆盖涂鸦、垃圾与故障路灯智慧城市巡检每天产生成千上万张街道照片光靠人工把涂鸦、垃圾堆放、故障路灯这些市容问题从照片里筛出来既费眼又费人。这份数据集的价值在于19263张jpg全部带着对应的Pascal VOC xml和YOLO txt标注11个类别覆盖了街道常见隐患而且训练、验证、测试的格式问题直接帮你省掉了最烦人的转换环节。适合做市政巡检算法的人、跑YOLO目标检测的团队以及想拿真实场景数据练手的学生。需要注意的是超过一半样本是4张图拼接的增强图处理不好会坑训练这一点放到后几章细说。2. 解压与目录结构先把双格式数据理顺再谈训练拿到压缩包第一步不是急着训练而是确认文件结构。这份数据集是7z打包里面每一张jpg都对应一个同名xml和一个同名txt三者一一对应。xml是Pascal VOC格式给人看的txt是YOLO格式给模型吃的jpg是原始图像。如果三者对不上训练时要么报“No labels found”要么标注错位返工成本极高。2.1 解压7z与文件落地检查Linux下解压7z需要p7zip组件命令用sudo apt install p7zip-full # 安装7z解压工具 7z x 智慧城市街道涂鸦垃圾故障路灯市容检测数据集VOCYOLO格式19263张11类别.7z -o./street_dataset cd street_dataset ls -la | head -20解压参数-o指定输出目录避免直接摊在当前目录里弄乱工作区。解压完先做三件事看使用前必读.txt里的补充说明、统计jpg/xml/txt数量、抽查几个xml对照图片内容。很多下载资源的问题就藏在这个txt里比如坐标是从左上角起始还是右下角起始、有没有切片约定不看的人后面肯定踩坑。文件统计我习惯用一条命令完成echo jpg: $(find . -name *.jpg | wc -l) echo xml: $(find . -name *.xml | wc -l) echo txt: $(find . -name *.txt | wc -l)三个数字都应该接近19263差异大于几十个就要小心说明有文件缺失或文件名不匹配。如果Windows下解压过还可能遇到txt扩展名被系统隐藏的假象这时候看数量是准的但训练时路径解析会出问题这个问题在第5章专门讲。2.2 XML与TXT标注内容解读Pascal VOC的xml是树状结构核心字段如下字段含义作用filename图片文件名把xml和jpg关联起来size/width图片宽坐标归一化的分母size/height图片高坐标归一化的分母object/name类别名训练时的class标签object/bndbox真实框坐标xmin/ymin/xmax/ymax对应的YOLO txt每行是一段标注五个数字依次是类别索引、归一化中心x、归一化中心y、归一化宽w、归一化高h。每个object占一行一张图有多个目标就有多行。比如3 0.4523 0.6112 0.0874 0.1250 7 0.3854 0.7289 0.0661 0.0933第一列3和7是类别索引不是类别名。具体哪个索引对应哪个类名要按训练时data.yaml里的names顺序来定。这也是很多人换数据集训练时翻车的高发点VOC的类别顺序和YOLO的索引顺序没对齐模型训练出来的类别全错位。所以拿到资源后第一件事是把全部xml里的name字段去重导出一份清单按固定顺序建档之后所有转换都围绕这个清单做。2.3 三个文件是否对得上快速体检脚本训练前我建议先跑一遍这个核对脚本把文件对应关系、空标注、异常框一次查清楚import os from pathlib import Path root Path(street_dataset) jpgs {p.stem: p for p in root.glob(*.jpg)} xmls {p.stem: p for p in root.glob(*.xml)} txts {p.stem: p for p in root.glob(*.txt)} all_names set(jpgs) | set(xmls) | set(txts) print(fjpg{len(jpgs)} xml{len(xmls)} txt{len(txts)}) for name in sorted(all_names): if name not in jpgs: print(f[缺失jpg] {name}) if name not in xmls: print(f[缺失xml] {name}) if name not in txts: print(f[缺失txt] {name}) if name in txts and txts[name].stat().st_size 0: print(f[空标注] {name}.txt)脚本逻辑是先把三个目录按文件名建索引再取并集逐个比对。st_size 0是Linux下判断空文件的常见做法Windows下可以换成读文件判断。跑完后无输出说明三件套齐整可以往下走。跑脚本只需要python check_dataset.py纯标准库就能运行。脚本输出“空标注”的时候先别急着删图有些是拼接图边缘裁切后把目标裁掉了但保留这张图会让训练迭代到无梯度样本常见做法是统计一下全数据集空标注比例低于1%直接剔除高于5%就要去核对xml是不是漏转。我一般会顺手把空标注清单导出成csv方便追溯。3. VOC转YOLO与YOLOv8实战坐标换算里的三个关键参数数据集虽然同时给了xml和txt但实际训练时你可能会动标注比如换了类别体系、发现某些框要剔除、或者想把拼接图拆开重新标。这时候不能手工改得把VOC转YOLO的换算吃透。3.1 从XML到TXT的坐标换算公式VOC的坐标是像素级绝对坐标xmin、ymin、xmax、ymax左上角为原点。YOLO要的是归一化后的相对值换算公式x_center ((xmin xmax) / 2) / image_width y_center ((ymin ymax) / 2) / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height网上很多转格式脚本栽在两点一是忘记除以图像宽高拿像素值直接当归一化值用二是xml里标签大小写不统一有的写object有的写Object解析时漏掉一半框。我写转换脚本时通常按下面的思路import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue cls_id class_map[name] 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) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return \n.join(lines)root.iter(object)比root.findall(object)更抗造子节点嵌套再深也能遍历到。class_map是从类别名到索引的字典它的顺序直接决定训练出来的类别编号转换前务必和data.yaml里的names列表保持一致。转换后验证离不开反查用YOLO的归一化坐标乘以宽高应该还原出像素坐标误差不超过1个像素。我一般抽5个xml做这步校验防止脚本里变量写反。还有一个常见误用有人会把“左上角坐标宽高”直接除以图片宽高得到的是错误结果因为YOLO要的是中心点坐标不是左上角坐标。3.2 训练数据配置与YOLOv8训练参数YOLOv8的训练入口比老版本友好得多数据组织成images和labels两级目录就能跑。结构大概是dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml按实际路径写train: ./dataset/images/train val: ./dataset/images/val nc: 11 names: [class0, class1, class2, class3, class4, class5, class6, class7, class8, class9, class10]这里class0到class10是占位写法训练前务必把xml里全部object/name去重导出来按你的类别顺序替换成真实类名。这里有个很实际的坑如果一条标注里出现names没覆盖的类别名转换脚本里if name not in class_map: continue会把这条直接丢掉不报错也不提醒训练时才发现类别数量对不上。训练命令按分辨率给两个版本# 先看拼接图分辨率如果边长超过1600用大分辨率 yolo detect train datadata.yaml modelyolov8n.pt imgsz1280 batch8 epochs100 workers4 # 如果大部分图是640级别用常规尺寸 yolo detect train datadata.yaml modelyolov8s.pt imgsz640 batch16 epochs100 workers8batch大小取决于显存12G显存跑imgsz1280时batch8已经紧张可以配合--cache缓解workers用CPU核数的一半比较稳。imgsz会被自动缩放到模型输入拼接图分辨率高但imgsz设小等于把原图目标缩小好几倍小目标漏检会非常明显。这个参数看似不起眼实际上对市容小目标的影响比模型选哪个版本还大。3.3 拼接增强图对尺寸的影响这份数据集超过一半是四张图拼接的增强图假设子图是640x640拼接后就是1280x1280。它带来的直接后果是直接训练时目标占比被稀释原本在640图上占64x64像素的物体拼进1280图后只占32x32模型特征学习难度陡增。常见两种处理方案做法适用场景整图大分辨率训练imgsz1280显存要求高想保留拼接图里的上下文空间关系切片后训练把拼接图切成4张子图并重新映射坐标目标大小与真实巡检图一致显存压力小我一般先按后者跑因为市容小目标多切片后模型精度更容易提上去。切片的具体操作和坐标映射放到下一章这里先记住一个结论拼接图不能简单当普通大图用它的坐标体系和真实单图不一致。4. 超过一半是拼接增强图坐标映射与切片取舍增强图多不是坏事但如果不知道它是怎么拼的、坐标是否已经按拼图位置重新映射训练结果就是一场赌博。这一章把拼接图从原理到处理讲清楚。4.1 四图拼接的标注现状与判断方法四图拼接常见的是2x2网格左上、右上、左下、右下各一张子图拼成一张大图。如果标注工具在拼图前就把框标好了不重新映射正方形拼接图上的框会全部挤在各自子图区域如果标注是在拼图后重新做的框坐标就是大图坐标可以直接用于训练。区分方法很简单xml里的width/height是大图尺寸但框集中分布在大图的某个区块说明坐标没映射全图。用脚本统计一下框的位置分布能很快发现规律import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter bins Counter() for xml_path in Path(street_dataset).glob(*.xml): root ET.parse(xml_path).getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) for obj in root.iter(object): 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) cx (xmin xmax) / 2 / w cy (ymin ymax) / 2 / h if cx 0.5 and cy 0.5: bins[左上] 1 elif cx 0.5 and cy 0.5: bins[右上] 1 elif cx 0.5 and cy 0.5: bins[左下] 1 else: bins[右下] 1 print(bins)如果四个区块的框数大致均匀说明坐标已经按拼接位置重映射过如果某几个区块数量异常或者框几乎都落在某一块多半是原坐标漏映射。这个统计脚本也适用于发现拼图边缘的特殊标注比如框横跨两个子图的边界这种框切片时要不要保留得单独定策略。4.2 拼接图切片与坐标重新映射确认需要切片后以2x2网格为例做拆分和坐标修正。子图宽cw w // 2、子图高ch h // 2原框坐标(xmin, ymin, xmax, ymax)落左半还是右半、上半还是下半按中心点判断然后把坐标减掉对应子图的偏移量from PIL import Image import xml.etree.ElementTree as ET def slice_and_remap(image_path, xml_path, out_dir): img Image.open(image_path) w, h img.size cw, ch w // 2, h // 2 tree ET.parse(xml_path) root tree.getroot() boxes_by_cell {TL: [], TR: [], BL: [], BR: []} for obj in root.iter(object): box obj.find(bndbox) xmin, ymin float(box.find(xmin).text), float(box.find(ymin).text) xmax, ymax float(box.find(xmax).text), float(box.find(ymax).text) cx (xmin xmax) / 2 cy (ymin ymax) / 2 if cy ch and cx cw: cell TL; ox, oy 0, 0 elif cy ch and cx cw: cell TR; ox, oy cw, 0 elif cy ch and cx cw: cell BL; ox, oy 0, ch else: cell BR; ox, oy cw, ch boxes_by_cell[cell].append((xmin - ox, ymin - oy, xmax - ox, ymax - oy, obj)) for cell, (sx, sy) in {TL: (0, 0), TR: (cw, 0), BL: (0, ch), BR: (cw, ch)}.items(): sub img.crop((sx, sy, sx cw, sy ch)) sub.save(f{out_dir}/{image_path.stem}_{cell}.jpg) # 子图坐标写新的xml注意过滤越界框 for (nx1, ny1, nx2, ny2, obj) in boxes_by_cell[cell]: if nx1 0 or ny1 0 or nx2 cw or ny2 ch: continue # 跨子图边界的框丢弃或裁切代码核心是三个数子图宽cw、高ch、偏移量(ox, oy)。坐标减去偏移后就是子图内的真实位置跨边界框这里用continue直接丢弃避免留下被截断的错标。切片后还要把新xml跑一遍第3章的转换脚本重新生成YOLO txt同时记得同步修改xml里的size字段否则归一化时用大图宽高算小图坐标所有框都会偏。这一步是我见过最多的隐性错误比写错转换公式还隐蔽。4.3 切片还是不切片两个方案怎么选如果原图宽度高度不是偶数切片前先做resize到偶数尺寸否则w // 2取整会丢像素。这是切片最容易踩的隐藏问题。假设验证集里有9000多张非拼接图、9000多张拼接图直接混合训练会把模型搞出双重性格验证时用真实单图训练时老看到拼接大图loss曲线看着正常map却上不去。切片能统一尺度但也损失了拼接图里“同一张巡检照片出现多个问题”的上下文信息。我的建议是第一轮训通用模型用切片图第二轮用整体图微调让小目标上下文被重新激活。这个二段式训练比单纯选一个方案省调参时间。5. 实战避坑从解压到训练最常见的五个翻车点这些坑不一定会同时出现但凡是处理带增强图的双格式数据集我基本每轮都会碰到其中两三个。按现象、原因、解决三步写清楚。5.1 路径、隐藏扩展名与空txt训练前先排查的三类文件问题现象训练启动后提示找不到label文件或者loss曲线前半段直接是0。原因Windows解压7z时txt扩展名被系统隐藏部分文件名变成“xxx.txt.txt”另一种情况是脚本用了反斜杠拼接路径在Linux下解析失败。解决先写个脚本把路径统一成/再检查扩展名from pathlib import Path for p in Path(street_dataset).rglob(*): if p.suffix.lower() not in (.jpg, .xml, .txt): continue if str(p).find(\\) ! -1: print(f反斜杠路径: {p})代码里用Path(.).rglob(*)递归遍历只保留三类文件遇到反斜杠就提示。实际修复时用rename把文件复制到干净目录统一小写后缀即可。序列里第二条空txt。现象是xml里有object但txt文件字节数为0训练时这一张图等于没有监督信号。原因通常是转换脚本里xml解析失败比如root.find(size)返回None异常被异常处理吞掉。解决是打印异常xml的路径把xml里标签大小写统一后再转。5.2 越界框、类别索引与验证集泄漏模型精度起不来的真凶先看越界框。现象训练早期出现NaN loss或者某个类别的mAP比随机还低。原因xml里的宽高比实际图片小框坐标超出范围归一化后出现大于1的宽高。解决转换时加合法性检查import xml.etree.ElementTree as ET def check_bounds(xml_path): root ET.parse(xml_path).getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) bad [] for obj in root.iter(object): b obj.find(bndbox) xmin, ymin float(b.find(xmin).text), float(b.find(ymin).text) xmax, ymax float(b.find(xmax).text), float(b.find(ymax).text) if xmin 0 or ymin 0 or xmax w or ymax h or xmax xmin or ymax ymin: bad.append((obj.find(name).text, xmin, ymin, xmax, ymax)) return bad判定条件比想象中宽松很多转换脚本只查空文件不查越界导致模型前几轮loss正常、后期突然发散。越界框的处理不是简单截断而是整框剔除因为截断后的框中心和宽高都对不上目标。再看类别索引错位。现象预测结果里“涂鸦”的高置信度框经常叠在“垃圾”的位置上可视化时标签全乱。原因YOLO的类别索引是从0开始VOC转YOLO时如果按1开始的枚举生成索引整体偏移一位。解决转换脚本里用一个有序字典给每个name固定编号生成data.yaml的names时按同一顺序导出不要手写。最后是验证集泄漏这个坑最隐蔽。现象训练loss持平但验证mAP异常高拿模型去实拍图上一测就现原形。原因超过一半图是四张拼接生成随机划分时同一张子图可能同时进了训练集和验证集等于模型提前看过答案。解决切片后按子图来源ID分桶划分同源子图只能进同一个集合非拼接图正常随机。一句话总结就是数据划分不能只看文件名随机要看生成关系。6. 训练后的评估mAP之外的多类盲区与切片推理经验模型训完要看的不只是总mAP。11个类别本身的样本量差异很大涂鸦和垃圾可能几千个故障路灯可能几百个这时候总体mAP好看不代表每条街道都能用。我习惯分开看每个类别的AP和PR曲线样本少的类别AP低于0.35就说明特征没学到优先补样本而不是调参。评估时还有一个容易忽视的点拼接增强图上切出来的小目标验证时用的还是大图测试这会让实测精度低于报告值。常见做法是在验证阶段同样跑切片推理把4个子图分别检测再拼回大图坐标mAP才真正反映部署场景。yolo detect val modelruns/detect/train/weights/best.pt datadata.yaml imgsz640输出里重点看每个类别的map50和map50-95的差值。差值超过0.3说明模型对边界框的定位精度差多半是斜框、小目标或者遮挡严重的样本拉低这些样本在混淆矩阵里通常集中在相邻类别之间比如涂鸦和污渍。最后一个小技巧整图训练大分辨率模型时如果显存只够batch4可以配合MixUp和Mosaic增强凑多样性但拼接图本身就是增强数据此时mosaic要关闭或降低强度否则一张输入图里叠了七八种目标模型容易把上下文学成噪声。从那以后我每次拿到含增强图的数据集都会先写一个数据体检脚本跑一遍数量、坐标、类别三个维度再决定训练方案而不是解压完直接丢进yolo train。这个习惯帮我避掉了大部分翻车现场希望帮到你。本文还有配套的精品资源点击获取