ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLOv11的遥感道路提取与变化检测实践

基于YOLOv11的遥感道路提取与变化检测实践 简介一份面向计算机视觉与遥感应用学习者的技术方案文档围绕YOLOv11在卫星遥感图像道路提取与变化检测中的应用展开。内容系统梳理了YOLO系列算法演进重点讲解YOLOv11的骨干网络、颈部网络、检测头及损失函数并分别给出道路提取与变化检测两个子方案从数据采集、预处理、模型初始化与训练到非极大值抑制、图像配准、变化类型识别和结果可视化覆盖完整落地流程。后续还包含实验设计、结果分析以及城市规划、交通管理、灾害评估等应用场景结构清晰、理论与实践并重适合需要完成相关课题或工程验证的读者参考。文档共31页压缩包仅含1个PDF文件体积2.12MB目录与章节可快速跳转方便按需阅读。目前已有90人学习下载。1. 为什么说“基于YOLOv11”可以同时做道路提取和变化检测拿到一批亚米级卫星影像老板要求一周之内把新增道路和断头路位置标出来。传统做法是训练两个模型一个用语义分割做道路提取一个用孪生网络做变化检测。部署链路长显存开销大两个模型的目标空间还不一致对齐时经常要手写坐标转换。真实工程里更现实的做法是用 YOLOv11 的实例分割能力先把道路段完整切出来把每条路当成一个有独立掩码和置信度的实例再用两期检测结果做匹配这样“提取”和“变化检测”可以在同一套特征抽取器上完成。标题里的方案并不是把 YOLOv11 包装成一个万能分割器而是让模型输出可被下游比较的结构化目标把“这一期有、下一期没有”变成实例之间的集合差。这条路径适合已经会用检测/分割模型、手里有遥感瓦片数据、希望快速得到可叠加 GIS 图层的团队。需要先说明的是如果目标是稠密街区的完整路网YOLO 类检测框会相互遮挡这种方案会吃亏正文你会看到边界。2. 准备数据与YOLOv11实例分割训练把道路提取落成可迭代的工程2.1 数据集从哪里来把语义掩码转成道路段实例遥感道路数据有两种常见形态。第一种是公开道路分割数据集标签是与影像同尺寸的二值掩码道路像素为255第二种是自有业务数据来自矢量测绘或人工标注最终要转成道路面。我一般不会直接把掩码喂给 YOLO因为 YOLO 分割训练需要的是多边形实例标注而不是语义掩码。两条平行道路在语义掩码里连成一个连通域直接训练会被当成一条路后续变化检测的“新增/消失”统计就完全失真。第一步必须把掩码转成道路段实例按连通域拆分保留面积和长宽比符合道路特征的连通域再转成 YOLO seg 格式的 txt。下面是一个轻量转换函数用findContours提取轮廓用 RDP 算法简化点数量。import cv2 import numpy as np def mask_to_yolo_seg(mask_path, img_w, img_h, txt_out, min_area200): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) lines [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: continue approx cv2.approxPolyDP(cnt, 1.0, True) pts approx.reshape(-1, 2).astype(np.float32) pts[:, 0] / float(img_w) pts[:, 1] / float(img_h) seg .join([f{x:.6f} {y:.6f} for x, y in pts]) lines.append(0 seg) with open(txt_out, w, encodingutf-8) as f: f.write(\n.join(lines)) # 示例掩码和原图同尺寸输出到 labels/ 目录 mask_to_yolo_seg(data/tile_001_mask.png, 1024, 1024, labels/tile_001.txt, 500)参数说明min_area过滤掉面积小于该像素值的噪声连通域一般道路在 1024 瓦片里至少占几百像素取 200 到 500 都可以取决于道路宽度approxPolyDP的第二个参数1.0是点到拟合线的最大距离值越大轮廓点越少道路交叉口处越容易粘连我建议先设置 1.0看生成的 txt 可视化后再决定是否提高到 2.0。img_w和img_h必须跟原图尺寸一致如果掩码和影像分辨率不一致先缩放掩码再做轮廓提取否则归一化坐标会整体偏移。转换完成后按 Ultralytics 惯例放置数据集images/存原始影像labels/存分割标注训练集、验证集分开data.yaml里声明路径和类别0: road。注意不要把道路中心线直接当掩码训练。中心线是“线”不是“面”经过 YOLO 的下采样和 mask 分支压缩后很容易丢失。若最终要中心线应对训练好的道路掩码做骨架提取而不是让模型直接回归线目标。2.2 环境配置与最小可运行目录YOLOv11 的环境配置比旧版 YOLO 简单很多不需要手动下载 cfg 和分类权重文件。拿到一台有 NVIDIA 显卡的机器先建一个 Python 3.10 环境再安装 Ultralytics。python -m venv .venv source .venv/bin/activate pip install -U ultralytics这一步会把 PyTorch、OpenCV 一起装好。如果你的 CUDA 是 11.8 或 12.1建议先到 PyTorch 官网按对应命令安装torch和torchvision再装ultralytics否则 pip 默认装的 torch 版本可能与驱动不匹配。遥感影像的瓦片通常较大训练阶段分辨率建议从 640 起步确认数据链路通了之后再升到 1024 或 1280。推理阶段可以无限切块训练尺寸不等于推理尺寸这一点后面细说。为了实验可追溯我习惯把每次运行的任务名写成可读形式例如road_y11_1024_lr1e-3并保证同一个数据集版本固定。Ultralytics 的data.yaml内容如下path: /data/road_extraction train: images/train val: images/val test: images/test names: 0: road2.3 训练自己的道路数据集CLI 命令与关键参数训练一个最小可用的道路提取模型命令如下yolo segment train \ modelyolo11n-seg.pt \ dataconfig/data.yaml \ epochs80 \ imgsz1024 \ batch8 \ device0 \ workers4 \ patience20 \ projectexp \ nameroad_yolo11这里modelyolo11n-seg.pt是 COCO 预训练权重遥感影像与自然图像差异大很多人担心预训练没用。实际收敛速度仍然比从零训练快很多backbone 底层的边缘和纹理特征是可迁移的。如果担心过拟合可以先用freeze5冻结前 5 层主干训练 20 轮后再解冻。imgsz1024是速度和精度的折中道路宽度只有几个像素时建议升到 1280但显存也会跟着涨。patience20用于早停避免验证集 mAP 长时间不涨时继续空跑。训练完成后权重保存在exp/road_yolo11/weights/下评估和推理都使用best.pt。在实际调参中下面这张表可以作为一个起点参数推荐初值调整建议imgsz1024道路细、断裂多时升到 1280/1536显存不够降到 640epochs80数据量小且增强多时增加到 120batch8确保总样本量不变显存不够时降低 batch 并增大 epochslr00.01小数据集用 0.001大数据集用 0.01mask_ratio1.0道路宽高比极大时增大 mask 分辨率或提升 mask 损失权重scale0.5影像尺度差异不大时改成 0.25避免道路被过度缩放YOLOv11 的网络结构里backbone 使用了 C3k2 和 SPPF检测头之外还有独立的 mask 分支。遥感道路是细长目标如果直接照搬 COCO 的超参很容易出现掩码边缘锯齿。我一般会把分割损失的权重调高到检测损失的 1.5 到 2 倍具体做法是在训练代码中修改 loss 项的权重。观察训练输出的 mask 可视化图如果目标掩码被压成了一团说明 mask 分辨率太低优先提高imgsz而不是盲目增加数据量。3. 用训练好的模型批量提取道路并把推理结果保存成可叠加的瓦片3.1 预测后保存一次推理得到可视化、掩码和裁剪图模型训练完成后批量推理的命令可以写成yolo segment predict \ modelexp/road_yolo11/weights/best.pt \ sourceval/images \ imgsz1024 \ conf0.25 \ iou0.45 \ save_txtTrue \ save_confTrue \ save_imgTrue \ save_cropTrue \ projectinference \ nameroad_predict这条命令会把source下所有瓦片都跑一遍结果输出到inference/road_predict/。save_txtTrue会在labels/下生成与图片同名的 txt每一行代表一个道路实例save_confTrue会把置信度写入文件save_cropTrue会把每个道路目标的外接矩形单独裁出来方便人工抽查漏检和误检。conf0.25对遥感道路偏保守树荫和阴影里的支路可能只有 0.15 左右我一般先设 0.15跑完看误检率再逐步上调。iou0.45控制 NMS 的抑制程度道路交叉口目标重叠非常大默认值会把垂直相交的两条路吞掉一条降到 0.3 能明显改善交叉口召回。3.2 保存推理结果从归一化掩码坐标到 GeoJSON如果推理结果只停留在 PNG拿到 GIS 里还是没法叠加。遥感瓦片一般带有仿射变换参数最常见的做法是用rasterio读原始瓦片的 transform把 YOLO 输出的归一化掩码坐标换算成地理坐标再写成 GeoJSON。import json import numpy as np import rasterio from pathlib import Path def yolo_seg_to_geojson(img_path, label_path, out_geojson): with rasterio.open(img_path) as src: transform src.transform width, height src.width, src.height features [] with open(label_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 8: continue conf float(parts[6]) raw np.array([float(p) for p in parts[7:]], dtypenp.float32) nx, ny raw[0::2], raw[1::2] px, py nx * width, ny * height coords [] for i in range(len(px)): x, y px[i], py[i] gx, gy transform * (x, y) coords.append([gx, gy]) features.append({ type: Feature, properties: {conf: conf}, geometry: {type: Polygon, coordinates: [coords]} }) with open(out_geojson, w, encodingutf-8) as f: json.dump({type: FeatureCollection, features: features}, f) # 遍历推理目录中的 labels for label_path in Path(inference/road_predict/labels).glob(*.txt): img_path Path(val/images) / (label_path.stem .png) yolo_seg_to_geojson(img_path, label_path, fgeojson/{label_path.stem}.geojson)这里有一个非常容易踩坑的地方parts[6]是置信度字段的前提是save_confTrue。Ultralytics 的 txt 行格式为class x1 y1 x2 y2 conf mask...下标 6 正好是置信度如果关闭save_conf下标 6 变成第一个掩码坐标解析会错。掩码坐标是归一化的需要先乘以图像的宽高再做仿射变换。transform * (x, y)会把像素坐标转到地理坐标如果瓦片没有地理参考这一步可以省略输出本地坐标即可。3.3 瓦片接边同一条路别出现在两个瓦片里滑窗推理带来的最大问题是接边。两个相邻瓦片重叠 20% 时同一条道路会被检测两次直接导致变化检测里重复计数。我通常先对 GeoJSON 做一步合并用shapely的unary_union把重叠的 Polygon 合并同时保留置信度最高的那条属性。如果不想引入新依赖也可以用道路掩码的骨架线做距离匹配把两个瓦片里距离小于 1 个像素的线段合并。这一步不做后面无论检测精度多高变化检测的结果都不可信。4. 双时相检测结果如何落到变化检测框匹配与掩码差分4.1 为什么不用整图像素差分卫星影像两期之间可能存在光照、植被和传感器角度差异直接对两期影像做像素差分会得到大量伪变化。基于 YOLOv11 的方案是先提取道路目标再比较“前一期有、后一期没有”的道路段。变化检测的三种典型输出是新增道路、消失道路、改建道路。由于模型输出的是实例分割掩码变化检测就变成了一个集合比较问题同一坐标附近是否出现相同方向的道路实例。流程上我用同一套best.pt分别推理两期瓦片得到两个时期的 GeoJSON 集合。然后需要判断哪些道路实例是同一个实体。这里不能直接做多边形 IoU因为同一路段在两期影像中可能偏移几米单车道和改造后的双车道外观差异大IoU 可能只有 0.1 甚至更低。更稳妥的是计算两个实例的中心距离和主轴方向用这两个条件做软匹配。4.2 掩码匹配变化检测的核心代码下面的函数从一个 GeoJSON 中读取道路实例计算每个实例的质心、主方向和近似长度然后在两期之间做最近邻匹配。import json import math import numpy as np def load_geojson(path): with open(path, r, encodingutf-8) as f: return json.load(f)[features] def polygon_stats(coords): coords np.array(coords) xs, ys coords[:, 0], coords[:, 1] cx, cy xs.mean(), ys.mean() dx xs - cx dy ys - cy cov np.cov(np.stack([dx, dy], axis1).T) eigvals, eigvecs np.linalg.eigh(cov) angle math.degrees(math.atan2(eigvecs[1, -1], eigvecs[0, -1])) length math.hypot(xs.max() - xs.min(), ys.max() - ys.min()) return (cx, cy, angle, length) def match_roads(prev_features, curr_features, dist_thresh15.0, angle_thresh30): changes [] used [False] * len(curr_features) for p in prev_features: sp polygon_stats(p[geometry][coordinates][0]) matched None for i, c in enumerate(curr_features): if used[i]: continue sc polygon_stats(c[geometry][coordinates][0]) dist math.hypot(sp[0] - sc[0], sp[1] - sc[1]) angle_diff abs(sp[2] - sc[2]) % 180 if angle_diff 90: angle_diff 180 - angle_diff if dist dist_thresh and angle_diff angle_thresh: matched i used[i] True break if matched is None: changes.append({type: removed, geom: p[geometry]}) for i, c in enumerate(curr_features): if not used[i]: changes.append({type: added, geom: c[geometry]}) return changes # 示例t0 为早期影像t1 为后期影像 changes match_roads( load_geojson(geojson/t0.geojson), load_geojson(geojson/t1.geojson) )这段代码里的dist_thresh和angle_thresh是变化检测的主要旋钮。dist_thresh不能拍脑袋定要根据影像分辨率换算0.5 米分辨率影像中同一条道路中心线偏移 3 米已经算重建dist_thresh可以设 3.02 米分辨率影像可以放宽到 10 到 15 米。angle_thresh取 30 度是因为瓦片切分后弯曲道路的主方向可能与整体方向不一致角度限制太严会漏匹配。如果两个多边形中心距离在阈值内但方向差 30 度以上大概率不是同一条路而是交叉口附近的错检。如果需要检测“改建”类型只做框匹配不够。匹配成功后计算两个掩码之间的双向 Hausdorff 距离如果距离超过道路宽度的一半说明路段发生了明显偏移可以标记为改道。4.3 变化检测的后验证与误报控制匹配之后变化列表里会有不少“消失”目标其中一部分是检测器的偶发漏检。比如前一期的某个路段置信度只有 0.2在后一期没被检测出来就会被误判成消失。因此需要两道闸门第一只保留两期检测置信度都高于一定阈值的实例第二过滤面积过小的碎片。当imgsz1024时道路掩码面积通常应在 500 像素以上低于 200 的零散检出一半是建筑物边缘。如果业务对细支路召回要求高面积阈值降到 200但要在变化列表里额外标记“低置信度候选”交给人工复核。我还会做一步掩码差分验证把匹配到同一实例的两期掩码分别栅格化做异或计算异或区域占两个掩码并集的比例。如果比例超过 20%说明该道路不仅位置变形状也变化明显应列入“改建”而不是“不变”。这比单纯比较检测框稳定很多能避免光照变化引起的边缘抖动。公开的森林变化检测数据集虽然目标不是道路但其中包含大量“前后时相 变化掩码”的配对样本可以用来检验这套双时相匹配流程的稳定性。直接拿道路模型推理森林影像没有意义但数据集的时间配准和空间对齐格式可以复用。5. 从切片到注意力YOLOv11 小目标优化的几个优先项5.1 切片推理比改结构收益更明显的优化遥感大图直接喂给 YOLOv11 会被压缩到 1024很多细路直接消失。我一般先做切片推理把大图切成 1024×1024步长 768保留 20% 重叠边界将每个瓦片归一化坐标回贴到原图。下面这段代码是核心逻辑def sliding_window_predict(model, big_img, size1024, stride768): h, w big_img.shape[:2] results [] for y in range(0, h, stride): for x in range(0, w, stride): y2 min(y size, h) x2 min(x size, w) tile big_img[y:y2, x:x2] r model.predict(tile, imgszsize, conf0.15, iou0.3, verboseFalse) for res in r: for box in res.boxes: x1, y1, x2b, y2b box.xyxy[0].tolist() results.append((x x1, y y1, x x2b, y y2b, box.conf.item())) return resultsstride小于size是为了让目标跨界时至少有一次完整出现在某个瓦片里。切片推理之后还要做一次全局 NMS否则重叠区的重复框会进入变化检测。对道路这种条带目标按中心距离做聚类更合适把中心距离小于 20 像素、方向角差小于 15 度的检测合并保留置信度最高者。5.2 网络结构与损失改进自注意力、CARAFE 与 PIOUv2如果切片和高分辨率输入都做了仍然漏长距离直线再考虑改 YOLOv11 网络结构。YOLOv11 的 backbone 已经包含 C3k2 和 C2PSA后者是轻量自注意力模块能增强长距离上下文。对道路这种跨大尺度连续的目标可以在主干最后一层之后再加一层自注意力机制增强道路段的连续性。上采样方面CARAFE 比默认的最近邻上采样保留更多边缘细节适合替换 neck 中的上采样算子。这些改动每次只加一个参数量增幅控制在 5% 以内否则小数据集上很容易过拟合。损失函数方面PIOUv2 对高宽比极大的目标有更好的边界回归效果可以在 Ultralytics 的自定义训练代码中替换原有的 box loss。5.3 一个更可信的验证技巧基于骨架线的 95 分位 Hausdorff 距离变化检测的精度评估很难只用 mIoU因为道路细长一点点偏移就会让 IoU 掉到零但这个结果其实并不影响业务判断。我通常用骨架线端点的 95 分位 Hausdorff 距离来评估变化区域定位质量。import numpy as np from scipy.spatial import cKDTree def hausdorff_95(pts1, pts2): if len(pts1) 0 or len(pts2) 0: return float(inf) tree cKDTree(pts2) dist, _ tree.query(pts1) return np.percentile(dist, 95)把预测变化区域和人工标注变化区域分别做骨架提取再计算这个指标。当 95 分位距离小于 1.5 个像素时说明变化区域定位准确大于 3 个像素说明边界抖动或匹配阶段错位。相比逐像素 IoU这个指标对道路这种细长目标更稳定每一轮切片、注意力或损失改进之后跑一遍这个数字比只看最后的 mAP 更能反映变化检测的真实质量。本文还有配套的精品资源点击获取
返回列表