
简介智慧城市道路病害检测与市政设施损坏评估的YOLOv11完整方案文档面向城市管理、智慧交通及目标检测相关开发与研究人员。全书共40页覆盖YOLOv11算法原理、系统架构设计、数据采集与预处理、模型训练优化、集成部署及实际应用案例适合希望系统掌握单阶段目标检测落地流程的中高级读者。压缩包仅含1个PDF文件体积2.24MB章节结构完整支持目录跳转、大纲定位与快速检索文字、图表显示清晰。目前已有60人学习使用。阅读该文档可获得系统级设计思路、算法实现细节与可参考的城市级评估框架有助于快速上手或迁移至智能安防、自动驾驶等场景。1. 智慧城市里的道路病害检测YOLOv11为什么能扛起市政设施评估这杆旗市政巡检员每天都要拍几百张路面照片裂缝、坑槽、井盖破损、护栏歪斜。照片拍了一堆但真正能从里面自动找出“哪里坏了、坏到什么程度”的系统却不多。我做过几轮这类智慧城市应用后发现YOLOv11恰好是能用一套检测模型同时覆盖道路病害和市政设施损坏评估的务实选择——它既能在服务器上跑出高精度也能压缩后放到Jetson Nano这类边缘设备上做实时巡检。这个标题对应的不是单点算法而是一条从数据标注、训练、部署到评估工单闭环的技术链路适合市政单位的算法工程师、智慧城市集成商的交付团队以及想在巡检车上落地视觉检测的硬件负责人。下面按我自己的落地路径把整个系统拆开讲。2. 先搞数据道路病害与市政设施数据集的标注、平衡与小目标优化2.1 病害和设施的分类体系怎么定第一版分类别贪多也别太粗。病害侧我建议先固定四类裂缝、坑槽、松散、修补痕迹设施侧固定四类井盖、路缘石、护栏、标志牌。这个体量对应YOLOv11的默认COCO训练经验大概每类攒到8001500个实例就有可用的第一版模型。如果一开始就把沥青修补、水泥修补分开再把井盖分成圆形和方形标注工作量会翻倍而且类别间视觉差异太小模型容易混淆。我的做法是检测阶段只负责“识别物体是什么”损坏状态完好、开裂、严重破损全部留给后续评估模块不掺进检测标签里。这样做的好处是检测模型的任务边界清晰训练标签干净后续换评估逻辑不用重训模型。表格列出建议的类别和损坏评估项分类域类别名检测框特征评估项道路病害裂缝细长框长宽比大长度、宽度、走向道路病害坑槽近圆形框面积、深度估计道路病害松散不规则块面积、边缘破碎度道路病害修补矩形块完整性、边缘脱落市政设施井盖近圆形破损面积比、错位市政设施路缘石细长框断角、位移市政设施护栏竖条或横杆变形、缺失市政设施标志牌矩形框污损、遮挡2.2 标注格式与数据清洗从VOC到YOLO的转换脚本巡检单位拿到的历史标注大多是VOC格式也就是XML文件里面是xmin、ymin、xmax、ymax。YOLOv11训练用的是YOLO格式的txt每行是“class cx cy w h”cxywh全部归一化到0-1。我一般会先写一个转换脚本把VOC统一转成YOLO格式同时过滤掉两类坏样本一是宽高小于10像素的目标二是标注框超出图像边界的XML。下面是常用的转换脚本核心片段import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): cls obj.find(name).text if cls not in class_names: continue box obj.find(bndbox) x1, y1 float(box.find(xmin).text), float(box.find(ymin).text) x2, y2 float(box.find(xmax).text), float(box.find(ymax).text) # 过滤太小的标注框 if (x2 - x1) 10 or (y2 - y1) 10: continue # 裁剪到图像边界避免归一化后越界 x1, x2 max(0, min(x1, img_w)), max(0, min(x2, img_w)) y1, y2 max(0, min(y1, img_h)), max(0, min(y2, img_h)) cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{class_names.index(cls)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines))逻辑说明这段代码做的事情是把XML里的绝对坐标转成归一化坐标同时做了两层保护。第一层是过滤掉过小的标注框——对道路病害数据来说小于10像素的目标本身就是噪声连人眼都很难确认类别留着只会干扰模型训练。第二层是把越界的坐标裁回图像范围实际巡检照片经常出现标注人员把框拉到图像外的情况不处理的话归一化坐标会大于1或小于0训练时损失直接异常。参数说明class_names的顺序就是最后模型的类别ID顺序转完格式后要检查一下txt文件里的第一列数字是否对得上类别表。最好按“背景占比越少越靠前”的思路排列比如裂缝、坑槽在前这样训练时类别权重也方便控制。转完后跑一句find . -name *.txt | xargs wc -l对比原XML里的目标数量能快速发现漏转的文件。2.3 小目标优化Tile切图和Mosaic增强道路病害里最典型的小目标是远处的裂缝和井盖破损。整张1920×1080的巡检图上一条裂缝可能只有30×8像素直接缩到640×640训练目标就只剩10×3像素了YOLOv11再强也难学出特征。我的做法是用滑窗切图把大图切成小块再按小块训练。切图时有两个关键点一是窗口之间要留重叠区域让被切到边缘的目标有完整样本二是在切图之前先放大2倍然后切成640×640这样相当于用小感受野去拟合大图上的细长裂缝。下面是一个可用的切图脚本片段import cv2 import os import json def tile_image(img_path, save_dir, tile_size640, overlap100, resize_scale2.0): img cv2.imread(img_path) img cv2.resize(img, None, fxresize_scale, fyresize_scale) h, w img.shape[:2] step tile_size - overlap tiles [] for y in range(0, h - tile_size 1, step): for x in range(0, w - tile_size 1, step): tile img[y:ytile_size, x:xtile_size] tile_name f{os.path.splitext(os.path.basename(img_path))[0]}_{x}_{y}.jpg cv2.imwrite(os.path.join(save_dir, tile_name), tile) tiles.append({name: tile_name, x: x, y: y, scale: resize_scale}) # 保存坐标映射供标签转换使用 with open(os.path.join(save_dir, os.path.splitext(os.path.basename(img_path))[0] _tiles.json), w) as f: json.dump(tiles, f)逻辑说明切图后原始的YOLO标签不能直接用需要根据每个tile左上角坐标和缩放比例重新裁剪。比如原图的目标中心点在(1000, 800)放大2倍变成(2000, 1600)如果tile起点是(1200, 800)那目标在tile里的坐标就是(800, 800)中心归一化后再除以640。实际工程中我会把这个转换逻辑和切图脚本写在一起直接输出新的txt标签避免手工改半天。参数说明overlap100表示相邻tile有100像素重叠这是为了让跨切块的目标至少在一个tile里保留完整形状。对有目标的区域我还会做一次“只切目标附近”的策略——先用原图的检测框确定目标位置再在目标周围生成一个或多个tile这样小目标样本密度能提升很多这是处理道路病害数据集最实用的小目标优化手段。Mosaic增强在YOLOv11默认开启它把四张图拼在一起对常规目标提升很大但纯裂缝场景反而会制造大量混杂背景。我会在ultralytics的训练配置里把mosaic1.0降到0.5并且在最后20个epoch关闭mosaic因为后期要让模型稳定收敛不再引入过度扰动。3. 环境、训练与推理结果保存从零跑通YOLOv11最小闭环3.1 环境配置显卡、CUDA、Ultralytics的版本搭配YOLOv11目前最稳定的路径是直接用Ultralytics库。这里说的“环境配置”不是无脑装最新版而是要锁版本。我踩过PyTorch 2.2配老显卡驱动导致CUDA不可用的坑所以现在有一个固定的搭配清单conda create -n yolov11 python3.10 -y conda activate yolov11 pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics8.3.40逻辑说明第一行建虚拟环境Python 3.10是和Ultralytics兼容性最好的版本3.11以上偶尔会遇到onnx库编译问题。第二行装PyTorchCUDA 11.8的轮子在2080/3090/4090上都正常跑如果是Jetson设备则不能这样装需要JetPack预编译的PyTorch。第三行锁Ultralytics版本8.3.x系列内置yolo11模型的加载逻辑。参数说明如果你只有核显没有N卡也可以CPU训练但速度会慢得怀疑人生。我的建议是云上租一张24G显存的卡跑一晚上把权重拿到本地做推理。训练完成后验证环境用yolo modechecks它会列出版本和GPU状态。3.2 网络结构YOLOv11和YOLOv8区别在哪YOLOv11网络结构最大的变化是Backbone中用C3k2模块替换了C3模块同时加入了C2PSA注意力模块。C3k2把原来的两分支结构做了简化用更少参数保住梯度流C2PSA相当于在SPPF之前加了一个自注意力层让模型对细长裂缝这类低纹理目标有更强的特征提取能力。实际使用中我不太会改它的网络结构因为改YAML的风险大于收益。如果你熟悉yaml结构可以试一下在Neck部分增加一个P2检测头来提升小目标召回但训练时间会涨30%左右。我更推荐的做法是保持原结构通过tile切图和imgsz提升小目标效果这样部署时不用带自改模型兼容性最好。3.3 训练命令与关键参数收集好数据和标签后按下面组织目录datasets/ road_damage/ images/train/ images/val/ labels/train/ labels/val/然后写一个road_damage.yamlpath: /home/yourname/datasets/road_damage train: images/train val: images/val nc: 8 names: [crack, pothole, loose, patch, manhole, curb, barrier, sign]训练命令我常用yolo detect train \ dataroad_damage.yaml \ modelyolo11n.pt \ epochs120 \ imgsz800 \ batch16 \ workers8 \ patience20 \ optimizerAdamW \ lr00.0005 \ cos_lrTrue \ close_mosaic20逻辑说明modelyolo11n.pt是从官方预训练权重开始训练n是nanoscale参数最少对道路病害这种类别差异明显的场景足够用。imgsz800是为了照顾小目标如果显存不够就降到640但别低于544否则裂缝基本就丢了。close_mosaic20表示最后20个epoch关闭马赛克增强让模型从“看图拼凑”转到精细拟合真实分布上。参数说明patience20是早停轮数我一般设为总轮数的1/5避免无效训练浪费电力。lr00.0005比默认的0.01低很多因为用AdamW配合低学习率在细长目标上更稳定用SGD跑裂缝很容易损失震荡这是改过几次后的经验。训练完看runs/detect/train下的results.png如果val/box_loss和val/cls_loss在曲线尾部还在下降说明epoch不够继续加。3.4 保存推理结果与预测参数调优训练完拿到best.pt后推理端最常见的问题就是“结果没保存下来”。Ultralytics自带保存逻辑但很多人不知道只保存了图片没保存标注文件。我这边推荐的保存推理结果方式是用Python脚本既保存图像又保存JSONfrom ultralytics import YOLO import json, cv2, os model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcetest_dir, conf0.15, iou0.4, imgsz800, saveTrue, save_txtTrue, save_confTrue, projectruns/predict, nameroad_vis, ) # 额外保存结构化结果供损坏评估阶段使用 out_dir runs/predict/road_vis with open(os.path.join(out_dir, results.json), w) as f: json.dump(results[0].boxes.data.cpu().numpy().tolist(), f)逻辑说明conf0.15是关键。道路病害的小目标置信度普遍不高默认的0.25会把许多真实裂缝滤掉。我宁可在评估阶段用规则再筛也不让检测阶段漏掉。save_txtTrue保存的是归一化的框坐标save_confTrue会在每行后面附置信度这对接后续损坏评估很有用。参数说明iou0.4比默认的0.45低一些因为裂缝细长框之间的IoU计算容易偏高过高的NMS阈值会把相近的两条裂缝并成一条。另外推理时imgsz要和训练时保持一致不要训练800推理640搏精度否则小目标只掉不涨。results.json里每行是x1 y1 x2 y2 conf cls后面写评估规则时直接读这个结构。4. 模型评估与边缘部署Jetson Nano上跑YOLOv11的精度与速度权衡4.1 指标怎么看mAP50、mAP50-95、混淆矩阵和PR曲线训练完别只盯一个mAP。YOLOv11默认会输出mAP50和mAP50-95。mAP50是IoU阈值为0.5的平均精度这个数容易虚高尤其对裂缝这种类别区分度低的目标mAP50-95是50到95逐档IoU的平均更严格。我用这套系统的标准是裂缝类mAP50达到0.7mAP50-95达到0.35就算过基线。看混淆矩阵时重点看“漏检”而非“误检”。道路病害场景里把坑槽看成松散问题不大因为评估阶段可以合并但把裂缝漏掉就是大事故因为裂缝是路面结构性问题。我一般在sorted混淆矩阵里检查裂缝和井盖的召回行如果某一类对角线数值低于0.5就回头补数据或切图。4.2 模型导出ONNX、TensorRT与INT8量化部署的第一步是把PyTorch权重导出成ONNXyolo export modelruns/detect/train/weights/best.pt formatonnx opset12 imgsz800导出ONNX后先用onnxruntime在PC上验证一次精度一致性import onnxruntime as ort import numpy as np from ultralytics import YOLO pt_model YOLO(best.pt) img cv2.imread(test.jpg) img cv2.resize(img, (800, 800)) img img[:, :, ::-1] # BGR to RGB onnx_model ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name onnx_model.get_inputs()[0].name outputs onnx_model.run(None, {input_name: img.astype(np.float32) / 255.0})逻辑说明ONNX模型的输入需要是归一化到0-1的RGB张量顺序是[N, C, H, W]。直接用np.float32数组跑不要用uint8否则输出偏差很大。这里只是验证导出后数值上有没有变化真正部署到Jetson Nano上还要转成TensorRT。参数说明opset12兼容性最好。如果你要端到端部署ONNX里会包含NMS节点但转TensorRT时我建议导出时去掉NMS使用外部NMS或在应用层做后处理因为TensorRT对NMS插件的兼容在不同JetPack版本上表现不一。4.3 Jetson Nano部署TensorRT加速与内存限制Jetson Nano是很多巡检小车和手持设备的首选边缘板子但它的GPU算力有限跑yolo11n也需要认真做优化。我的部署步骤是先装好JetPack 4.6.1然后安装配套的PyTorch再编译TensorRT。转TensorRT引擎的命令trtexec --onnxbest.onnx \ --saveEnginebest_fp16.engine \ --fp16 \ --maxBatchSize1 \ --workspace1024逻辑说明--fp16是半精度精度损失小速度翻倍。Jetson Nano的GPU对FP16支持很好跑yolo11n时FP16比FP32能快30%以上。--maxBatchSize1很重要因为巡检推理是逐帧做的把batch设为1能减少显存占用和优化难度。--workspace1024给TensorRT构建过程分配1GB工作空间如果设置太小转换会失败。参数说明转换完的engine文件是极简的不包含NMS。实际推理时我使用TensorRT Python绑定来加载engine输入预处理用CUDA内存拷入输出直接拿到NMS前的结果然后用自己写的后处理做NMS。在Jetson Nano上输入分辨率从800降到640能让FPS从8提升到12左右如果业务上允许我建议用640部署只对近处病害做精确检测远处的交给二次变焦巡检。5. 智慧城市落地最容易踩的坑数据、训练、部署三层的翻车实录5.1 裂缝标注很准但推理时漏检率居高不下现象标注时肉眼能看出的横向裂缝模型输出的置信度只有0.1框也断断续续。原因裂缝是极细长目标长宽比经常大于1:10YOLO系列anchor-based的分配策略对极端长宽比不敏感。另外标注框里的“正样本区域”大部分是背景因为裂缝本身只占框面积的5%以下。解决第一个办法是切图后把图像沿裂缝方向旋转让训练样本里出现各种角度的细长目标第二个办法是直接降低conf_thres到0.05把低置信度框全部捞出来再用裂缝的连续性判断来拼接。我最后是用后处理拼接解决的——把检测到的碎片裂缝按相同角度合并成一条完整裂缝。5.2 白天效果好夜间灯光下一片乱现象路灯下的裂缝能检出光线被树影遮挡的路段井盖漏检严重夜间巡检视频里误报率暴增。原因训练集绝大部分是白天拍摄缺失低光、逆光、阴影样本。YOLOv11的Mosaic增强虽然能提升光照多样性但它改变的是拼接方式不是光照分布。解决直接从巡检记录里找夜间素材单独建一个night/目录按一定比例混入训练集。训练前对低光样本做CLAHE直方图均衡化用cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8))处理后保存。推理时也做同样预处理。一版之后夜间漏检能降低一半。5.3 训练loss下降但mAP不涨现象训练时train_loss稳定下降val_loss也降但mAP曲线一直趴在0.3附近。原因类别不平衡是主要凶手。如果数据里“修补”类有5000个实例“护栏缺失”只有200个模型会把后者学成噪声。另一个原因是用了默认的0.7置信度阈值去看mAP漏掉了大量低置信度正样本。解决统计每一类的实例数对数量少于500的类做特殊处理要么复制样本做重采样要么在数据目录里多放几遍该类的图片。mAP评估直接用yolo val conf0.05看曲线不要用默认阈值。如果还不涨检查lr0是不是太大把lr0降到0.0003重新跑。5.4 Jetson Nano部署后帧率只有3FPS现象用yolo predict跑Jetson Nano输出显示每次推理耗时300毫秒帧率约3FPS实际巡检车开起来根本不够用。原因三个瓶颈叠加一是没有转TensorRT还在用PyTorch浮点推理二是输入图片缩放用了隐式的cv2.resize在CPU做CPU成了瓶颈三是NMS在Python层完成每次推理要触发Python解释器开销。解决全部改为TensorRT engine推理预处理用Jetson的CUDA加速批量resizeNMS改用cv2.dnn.NMSBoxes在C底层完成。最后我跑到了16FPS在640分辨率下满足慢速巡检要求。如果你想上到20FPS以上建议换Jetson Orin Nano。5.5 保存推理结果时没有框现象saveTrue后图片存下来了但图片上没有任何BN框save_txt生成的txt文件是空的。原因conf参数设得太高或者模型只输出了NMS后为空的结果。这个问题在裂缝检测里特别常见因为很多真实目标的置信度只有0.030.15。解决先不要动代码用model.predict(sourcetest.jpg, conf0.01, iou0.5)跑一次看results[0].boxes长度是否为0。如果还是0说明模型完全没学到这个类去检查训练集里该类的标注格式有没有问题——最常见的是类别ID和yaml里的名字对不上导致模型把这类当成背景。6. 从检测框到损坏评估市政设施损坏程度分级与工单闭环的进阶做法6.1 损坏评估的规则面积比、置信度、边缘破损率检测框只是第一步智慧城市系统真正要输出的不是“有井盖”而是“这个井盖破损面积约35%需要维修”。我一般用检测框内的图像做二次处理。以井盖破损评估为例裁出检测框区域用灰度阈值分割出破损像素import cv2 import numpy as np def evaluate_manhole(roi, conf): gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 井盖通常为深色破损区域灰度变化更大 blur cv2.GaussianBlur(gray, (5, 5), 0) # 用自适应阈值分离破损边缘 mask cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2) # 形态学闭运算让破损区域连成片 kernel np.ones((3, 3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) damage_ratio cv2.countNonZero(mask) / (roi.shape[0] * roi.shape[1]) if conf 0.3: level 需现场核实 elif damage_ratio 0.08: level 轻度 elif damage_ratio 0.25: level 中度 else: level 严重 return {damage_ratio: round(damage_ratio, 3), level: level}逻辑说明这个函数只在检测框内做图像分析所以快而且稳定。adaptiveThreshold对光照不均的适应性比全局阈值好形态学闭运算能把碎裂处的小空洞连成整体避免把井盖纹路误判成破损。conf参与评估是因为低置信度检测框本身可能就定位不准框歪了面积比自然失真。参数说明adaptiveThreshold的blockSize11适合井盖这类中等纹理目标如果是路面裂缝场景blockSize改为15更稳。damage_ratio的三个阈值是我在多个项目上试出来的经验值实际使用前用几十张人工标注图回归一下。6.2 分级模型规则打分还是二次分类规则评估的好处是没有标注成本缺点是遇到复杂场景容易翻车比如井盖上有落叶、泥水damage_ratio会虚高。所以我做第二版时引入了一个小的分类头在检测框内再做一次三分类完好、破损、严重破损。这个二次分类模型不用重新训YOLO直接把检测框裁出来按64×64缩放到一个简单CNN里训练。数据可以从检测模型的训练集里顺手生成不需要额外标注——因为检测标注本身只有类别和框需要人工补标破损等级。我一般是抽样1/3的框用半天时间完成人工分级然后训练一个ResNet18的轻量分类器。最终效果比纯规则稳定很多代价是部署时多一个ONNX模型在Jetson Nano上只多占用约5毫秒。6.3 与城市管理工单系统的联动识别和评估都做完还需要把结果推到市政管理平台。常规做法是写成JSON通过HTTP接口推送。下面是一个简单的推送函数import requests import json def push_work_order(detection_id, evaluate_result, image_path, location): payload { source: road_scan, detection_id: detection_id, category: evaluate_result[category], level: evaluate_result[level], image_path: image_path, location: {lat: location[0], lng: location[1]}, timestamp: time.time(), } resp requests.post(http://your-municipal-platform/api/work_order, jsonpayload, timeout3) return resp.status_code逻辑说明这里的关键是确保评估结果和检测框能一一对应。我用detection_id存成检测框在图片里的序号评估时带着这个ID走全流程工单系统回执后还能反查原始证据图。location字段来自巡检车GPS巡检车的照片命名里通常有时间戳和坐标解析出来后拼到这个结构中即可。参数说明超时设置3秒挂掉时不阻塞巡检车的主流程。推送失败的照片会落到本地pending/目录每次启动前先重试避免漏工单。6.4 验证方法用真实工单回流标定评估准确率评估系统上线前一定要做一次回测。我的做法是找到市政单位过去三个月的维修工单把工单对应的维修前后照片收集起来用模型跑一遍看自动评估的“严重”等级是否和实际维修的紧急程度匹配。这个回测能发现阈值设得太严或太松的问题。有一次我发现井盖评估为“严重”的工单里有一半只是表面锈蚀并不影响通行后来就在评估规则里加了“是否存在边缘塌陷”的判断才把误报降下来。最后一件事不要信任任何一次单独的指标数字。我在多个市政项目里最深的教训是检测模型在测试集上mAP漂亮不等于真实巡检路上好用。雨天反光、行道树阴影、沥青补丁的旧痕都会让评估系统变成黑匣子。现在我的习惯是每次部署后留一周影子模式把模型的评估结果和人工巡检记录放在同一个库里隔天抽看对比。在影子模式里跑够500个真实场景再决定要不要把输出接入工单系统这样最稳。这条路我没有一次走顺过但每一步踩出来的参数和逻辑现在只需要半天就能从一个新数据集跑到可部署状态。希望帮到你。本文还有配套的精品资源点击获取