
简介基于YOLOv11的卫星遥感图像道路提取与变化检测方案是一份面向遥感、计算机视觉与目标检测学习者的PDF技术文档压缩包内含1个PDF文件共31页约2.12MB。文档从卫星遥感图像特点及道路提取、变化检测的基本概念出发系统梳理YOLO系列从YOLOv1到YOLOv11的发展脉络重点讲解YOLOv11的骨干网络、颈部、检测头、损失函数与训练过程。在此基础上分别给出基于YOLOv11的道路提取和道路变化检测完整方案涵盖数据预处理、图像配准、模型训练、NMS后处理、结果可视化与评估指标并辅以实验设计、应用场景及挑战展望。全文结构清晰支持目录跳转与大纲定位便于按需查阅。目前已有90人学习浏览。对于需要快速理解YOLOv11并落地遥感道路分析项目的读者这份资料能提供从原理到实验对比的系统参考。1. 把 YOLOv11 从“框住目标”改成“画出道路”这套方案到底在做什么先给结论用 YOLOv11 做卫星遥感图像道路提取并不是让模型学会输出分割掩码而是把它当成一个多尺度目标检测器用密集的旋转框或定向框把道路段“一段段框出来”再用后处理把框拼成连续路网。而“变化检测”也不是另起炉灶训一个模型而是拿同一区域两个时相的检测结果做空间对齐和差异比较从而回答“哪里多了路、哪里路断了、哪里路被覆盖”这三个问题。所以这份方案的技术本质是检测模型 时序差分逻辑而不是语义分割模型。这个思路能成立是因为遥感图像里的道路有几个让通用检测头疼、却恰好能被 YOLOv11 改进点吃掉的特征道路是细长结构长宽比极端常规锚框对它不友好道路的灰度/纹理与周围建筑、裸地有时难以区分道路在 0.5m 分辨率下图幅里只占几个像素宽属于典型的小目标。而 YOLOv11 相比前代把 C3 模块改成 C3k2同时引入了更精细的梯度流控制在保证推理速度的前提下颈部和骨干对小目标的敏感度更高。也就是说这份方案不是拿 YOLOv11 原封不动去跑而是围绕道路提取的场景做针对性的输入切块、标签设计和后处理。适合读这篇笔记的人是手里已经有遥感影像数据、想在本地或 Jetson 这类边缘设备上把“道路提取”和“变化检测”跑通的算法工程师或 GIS 开发。你可能试过 Unet 分割但觉得工程化太重也可能试过 YOLO 检测但发现小目标漏检严重。这套方案给了一条中间路线改动量小、训练成本低、推理速度快而且变化检测部分不需要额外标注。下面从方案选型、数据准备、训练调参、变化检测逻辑到推理部署一步步拆开讲踩坑记录放在最后一章那几条血泪经验基本能把你的翻车概率减半。2. 方案拆解与选型为什么是检测框而不是分割掩码做道路提取2.1 道路提取任务的三种技术路线对比当前遥感道路提取的主流做法其实有三条路线但每个方案的工程代价差别很大。第一条是纯语义分割代表模型是 Unet、DeepLabV3 和 Segformer优点是道路轮廓完整缺点是标注成本高而且对高分辨率影像的连续大图推理需要做切块拼接容易在瓦片边界留下断痕。第二条是矢量追踪类算法像经典的 Snakes 主动轮廓模型或图割方法这类方法对噪声很敏感在城区阴影多的场景基本是黑匣子调参过程很容易让人怀疑人生。第三条就是这份方案的路线把道路段当作目标去检测用定向框或旋转框框出道路分段再用后处理串联。选择 YOLOv11 而不是直接上分割模型核心考量在于可维护性和部署成本。遥感项目最终要交付的往往不是一张像素图而是矢量路网或变化图斑检测框天然带坐标直接接 GIS 后处理比分割掩码转矢量的骨架化提点操作要简单得多。另一个理由是标签格式。如果你手头只有道路中心线或 OpenStreetMap 导出数据转成检测框只需要按线段生成矩形框而做分割掩码则需要栅格化、去锯齿、补空洞等一系列脏活。从工程效率出发检测路线在数据准备上至少能省一天的人工。2.2 YOLOv11 的哪些结构改动真正对遥感道路有效YOLOv11 的网络结构相比 V8 有几个值得注意的变化但并非所有改动都对遥感有效。C3k2 模块替代了部分 C2f减少了冗余计算这对高分辨率输入下的显存占用是利好因为道路检测通常需要把输入图缩放到 1280 甚至 1536 才看得清细长目标。另一个关键改动是分类头和检测头的解耦进一步加深在颈部增加了更轻量的注意力机制实际是 C3k2 内部的通道注意力分支对微小目标而言注意力能帮助抑制建筑物屋顶和海量背景的干扰。值得强调的是YOLOv11 的 anchor-free 设计天然适配细长目标。V8 之后的 YOLO 系已经不再依赖预设锚框尺寸而是通过回归直接预测物体中心和宽高道路段这种长宽比极大、方向各异的物体用 anchor-free 的方式比用预设锚框更合理。如果你想让道路框带上方向角即 OBB 形式常见做法是在检测头后面额外增加一个角度回归分支数据标注时用四点矩形框存储路段的四个角点。需要注意的是这份方案如果只输出水平框交叉路口的拼接会变得比较麻烦加上角度分支后旋转框能减少重叠和漏接。2.3 确认这个方案的任务边界和输出形态在动手之前建议先明确方案的两个阶段各自的产物形态。道路提取阶段的输出是每个瓦片图像上检测到的道路段每个段用一个旋转矩形框或水平矩形框表示附带置信度、类别比如“主干道/次干道/小路”。变化检测阶段的输出是一个二值变化图或 GeoJSON 矢量文件标注新增道路、消失道路和几何变化区域。这里要提前防一个误解变化检测不是“拿两个时相的图像直接让模型判断变化与否”而是先分别提取两个时相的路网再做矢量层面的差分。因为像素级变化检测极易把季节差异、光照差异误判为道路变化而检测结果的差分天然免疫这类噪声。这一点是整个方案的核心设计决策越早理解后面跑结果时越不容易对着误报发愁。3. 从数据准备到训练调参让 YOLOv11 在遥感影像上跑出可用结果3.1 影像预处理与切片策略瓦片大小直接决定道路断头的频率遥感影像动辄上万像素宽YOLO 类模型不可能直接吃进去所以要切瓦片。常见的做法是滑窗切图窗口大小根据 GPU 显存和道路目标尺寸共同决定。我的建议是优先用1280×1280的窗口步长为 640也就是 50% 重叠覆盖。这个参数的逻辑是道路段的实际像素长度通常在 200~800 像素之间1280 的切块能比较完整地框住一整段直路如果降到 512×512道路在单块中的上下文太少模型很容易把断头路误判成完整路段。切块之后要做两步预处理。第一步是保留地理坐标信息也就是把每个瓦片的左上角经纬度或投影坐标写进文件名或一个 JSON 索引这步漏了后面的变化检测空间对齐就是纸上谈兵。第二步是对瓦片做 BGR 通道顺序检查因为遥感影像有些是 RGBN 多波段YOLO 训练通常只取 RGB 三通道建议把 NIR 波段舍弃或通过索引融合抬升道路对比度。另一个常见技巧是线性拉伸把 16 位影像的 DN 值拉伸到 0~255而不是直接截断否则夜间或阴影场景的道路会整体偏暗。import cv2 import numpy as np import glob def preprocess_16bit_to_8bit(img_path, out_path, clip_percent2): 把16位遥感影像转8位同时做百分比截断拉伸. img cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if img.dtype np.uint16: # 取2%和98%分位做线性拉伸避免单点极值影响对比度 lower np.percentile(img, clip_percent) upper np.percentile(img, 100 - clip_percent) img np.clip((img - lower) / (upper - lower) * 255.0, 0, 255).astype(np.uint8) # 若有多波段只取前3通道 if img.ndim 3 and img.shape[2] 3: img img[:, :, :3] cv2.imwrite(out_path, img, [cv2.IMWRITE_JPEG_QUALITY, 95]) print(fwrote {out_path})这里 clip_percent 参数是拉伸的关键默认 2 表示舍弃两端 2% 的极亮极暗像素避免镜头耀斑或云影把直方图拉坏。如果你的影像经常有薄云覆盖建议把 clip_percent 提高到 5牺牲一点对比度换稳定性。注意输出格式尽量用 JPEG 质量 95 或 PNGPNG 无损但文件大JPEG 压缩对道路边缘的锯齿影响在 1280 输入下可以接受。3.2 道路标注与标签格式转换从矢量道路网到 YOLO 训练框如果你手上只有 OpenStreetMap 的线矢量数据不能直接拿来训练 YOLO需要把线段转成多边形框。常见做法是以线段为中线按道路宽度向两侧扩张生成矩形再按一定间隔做打断避免一条长路直接变成一个长宽比几十倍的极端框。我一般按每 100~150 米打断一次或者按目标像素长度 300~600 打断。具体做法是先用 GDAL 读矢量对每个 LineString 沿线采样每两个采样点之间形成路段再对路段做缓冲区生成矩形面最后把面转成四点角标。注意YOLO 的旋转框格式通常是 cx, cy, width, height, angle 或者四点坐标。如果你的标签工具是 LabelImg 这类水平框工具那角度信息会丢失建议用 X-AnyLabeling 或 Roboflow 的定向框标注这两种工具对四点和旋转框支持比较好。实际工时上一万个框的人工标注大约要 8~12 小时如果纯粹从 OSM 数据自动转框则需要接受标签噪声部分道路边界偏移一到两个像素。from osgeo import ogr, osr import shapely from shapely.geometry import LineString, box from shapely.affinity import translate import math def linestring_to_boxes(line_coords, road_width_px8, segment_len_px400): 把一条线段切成若干段矩形框供YOLO OBB训练. line_coords: [(x1,y1), (x2,y2), ...] 像素坐标 road_width_px: 道路宽度像素 line LineString(line_coords) total_len line.length boxes [] distance 0 while distance total_len: # 截取从distance开始的一段固定长度 segment line.interpolate(distance).buffer(0) # 占位实际用cut方法 end_dist min(distance segment_len_px, total_len) sub_line _cut_line_at_dist(line, distance, end_dist) if sub_line is not None and not sub_line.is_empty: # 按道路宽度做缓冲区形成矩形 poly sub_line.buffer(road_width_px / 2.0, cap_style2) # 转成YOLO OBB四点: (cx, cy, w, h, angle) minx, miny, maxx, maxy poly.bounds cx (minx maxx) / 2.0 cy (miny maxy) / 2.0 w maxx - minx h maxy - miny # 根据道路走向估计角度 dx sub_line.coords[-1][0] - sub_line.coords[0][0] dy sub_line.coords[-1][1] - sub_line.coords[0][1] angle math.degrees(math.atan2(dy, dx)) boxes.append((cx, cy, w, h, angle)) distance end_dist return boxes def _cut_line_at_dist(line, start, end): 用shapely的interpolate构造子线段. start_pt line.interpolate(start) end_pt line.interpolate(end) return LineString([start_pt, end_pt])这里 road_width_px 直接决定了检测框的贴合程度。如果设为 8实际道路宽 12 像素框只覆盖了道路中间三分之二可能导致模型把路缘误当成背景如果设为 16框盖住了两边的建筑物边缘模型学习时会被迫把建筑纹理纳入框内特征。我建议从影像上量三条不同等级道路的实际像素宽度取中位数做全局值或按 OSM 的 highway 标签分级设置不同宽度。angle 的计算走了简化路线只取了首尾两点方位角对弯曲道路会有一点误差但训练时框是矩形的整体影响可控。3.3 训练配置与 Mosaic 增强的正确姿势YOLOv11 训练遥感影像时的环境配置没有太多幺蛾子PyTorch 2.x CUDA 11.8 是常见组合。仓库里官方提供的 requirements 并不多但有一个坑容易出现opencv-python 的版本和 albumentations 冲突安装完 albumentations 后 cv2 的 imshow 可能失效。建议严格按 requirements.txt 顺序安装不要一口气全装。训练命令一般是yolo train dataroad.yaml modelyolo11s.pt imgsz1280 batch4 epochs100 lr00.001 mosaic0.0 close_mosaic0其中 data 文件里需要指定 train 和 val 的图片路径以及类别名。一个值得强调的参数是mosaic0.0。遥感影像切块后瓦片之间的地物边界天然连续mosaic 增强会把四个完全不同区域的瓦片拼在一起这在自然图像上能提升泛化但在遥感道路上会制造出大量“跨区域伪道路”比如一块瓦片的道路边缘恰好和另一块的屋顶边缘拼成直线模型被迫学习这些假连接。我实测过开 mosaic 的模型在验证集 mAP 会高 1~2 个点但实际推到大图拼接时断头率明显上升。close_mosaic0是配合用的YOLO 的训练策略里 close_mosaic 表示最后多少个 epoch 关闭 mosaic 增强既然已经显式关了 mosaic这里设 0 即可。batch size 在 1280 输入下不要盲目调大12G 显存跑 batch4 是安全上界如果显存不足优先降低 imgsz 到 1024 而不是减 batch因为道路检测对分辨率更敏感。# road.yaml path: ./datasets/road train: images/train val: images/val nc: 1 names: 0: road_segment # 训练/验证时的增强参数由命令行控制不写在这里有几个参数建议在训练时留意lr00.001是遥感微调的常见起点预训练权重 yolo11s.pt 是 COCO 上的通用权重遥感影像的分布差异大完全从头训收敛会慢很多而直接加载预训练权重再把训练轮次拉到 150 更稳妥。epochs100对于一个类别、一万张瓦片规模的数据集足够看到 mAP 趋于平稳。另外建议在训练前确认类别数 nc1因为道路提取只需要一类多分类反而会让长尾分布恶化。3.4 小目标优化让道路的细碎化问题不再拖后腿遥感道路的小目标问题是热词里关注最多的方向。这里给出三个实际有效的优化手段按性价比从高到低排列。第一个手段是输入分辨率。从 640 提到 1280小目标的 mAP 通常能提升 8~12 个点代价是训练时间增加约三倍。如果你的 GPU 无法承受 1280 训练可以分两步走先在 640 下训 60 个 epoch再加载权重在 1216 或 1280 下微调 40 个 epoch这比直接训 1280 省显存且效果接近。第二个手段是加入 SAHI 式的切片推理。推理时用带重叠的切片跑模型最后把结果合并到大图中这一步到推理章节会展开。第三个手段是改检测头的 anchor-free 回归范围YOLO 默认对象的尺寸回归范围是相对特征图网格的倍数对小目标并不友好。# 在yolo11的model配置文件里调整回归范围示意 # 对应yaml里head部分的reg_max或类似参数需要修改为更小的值 # 通常默认是16小目标场景尝试改为8 head: reg_max: 8 # 减小回归范围让模型专注小尺度对象需要说明的是reg_max 参数在不同 YOLO 版本里的名字和位置有差异有些版本叫 dfl_branch如果你用的不是官方实现建议在代码里搜reg_max或DFL的初始化处手动改。改成 8 之后模型对大目标的定位精度会略微下降但对于遥感瓦片中的道路段目标尺寸相对集中损失很小。我一般会以 640 输入 reg_max8 的组合在 Jetson 设备上跑实时检测平衡性最好。4. 变化检测的实现路径双时相路网差分与后处理修正4.1 为什么不能用像素对比做道路变化检测很多团队尝试直接对比两期影像的像素差来检测道路变化最后都被误报逼疯了。原因很简单卫星影像两期拍摄的时间、角度、光照、土壤湿度都不一样像素级差异图里会充满伪变化。比如农田收割前后、树木落叶前后的纹理变化在像素层面远超道路本身的几何变化。即便把道路区域先用分割掩码遮住再差分掩码边缘的锯齿误差也会被放大成“道路偏移变化”。可靠的做法是把变化检测放在检测结果的矢量层面来做。两个时相的模型分别输出路网检测框后空间关系已经确定这时再做几何叠置分析天然规避了像素噪声。常见流程是对 A 时相道路框集合和 B 时相道路框集合计算每个框之间的交并比和中心距离再用一个规则引擎判断新增、消失和不变。4.2 基于检测框的差分算法交并比 空间连续性约束具体实现上我推荐一个轻量高效的方案计算两个时相道路框之间的 IoU 和中心距离然后按道路连续性做聚类。如果 B 时相某个框和 A 时相某个框的 IoU 超过 0.3认为这属于不变道路如果 B 时相有框但 A 时相在该位置完全没有 IoU 匹配标记为“新增候选”。候选需要进一步按空间连续性过滤如果新增框的邻居框并没有同步新增这个孤立的新增框大概率是误检而不是真实变化。import numpy as np from shapely.geometry import box from shapely.ops import unary_union def compute_change_detection(boxes_a, boxes_b, iou_thresh0.3, neighbor_radius150): 双时相道路检测框差分. boxes_a: A时相检测框列表,每个为(x1,y1,x2,y2) boxes_b: B时相检测框列表 added [] removed [] unchanged [] # 对B中每个框找A中最大IoU匹配 for bb in boxes_b: best_iou 0 best_box None bb_poly box(*bb) for ba in boxes_a: ba_poly box(*ba) inter bb_poly.intersection(ba_poly).area union bb_poly.union(ba_poly).area iou inter / union if union 0 else 0 if iou best_iou: best_iou iou best_box ba if best_iou iou_thresh: unchanged.append((bb, best_box)) else: added.append(bb) # 对A中未匹配的框视为消失 matched_a set([id(box) for _, box in unchanged]) for ba in boxes_a: if id(ba) not in matched_a: removed.append(ba) # 空间连续性过滤新增框若周围无其他新增框可能是误检 filtered_added [] for add in added: cx (add[0] add[2]) / 2.0 cy (add[1] add[3]) / 2.0 neighbor_cnt sum( 1 for other in added if np.hypot((other[0]other[2])/2 - cx, (other[1]other[3])/2 - cy) neighbor_radius ) if neighbor_cnt 2: # 至少包含自身和1个邻居 filtered_added.append(add) return filtered_added, removed, unchanged这段代码的关键在于 iou_thresh 和 neighbor_radius 两个参数。iou_thresh 设 0.3 偏保守因为道路检测框本身带有角度和宽度的不确定性两个时相同一段路的框如果角度差 15 度IoU 就可能掉到 0.4 以下如果做的是高分辨率影像配准良好的两张图可以放宽到 0.25。neighbor_radius 取值取决于瓦片切分尺度在 1280 像素下 150 像素半径大概覆盖两三个路段框如果新增路段是几百米长的新修公路那么沿线会有连串新增框这个条件很容易满足如果是孤立的误检框在 150 像素内找不到邻居就被过滤掉了。4.3 双时相配准是变化检测的命门变化检测的所有算法都建立在“两期图在同一坐标系下严格对齐”的假设上。卫星影像的单景产品做过分幅和正射校正但如果两期影像来自不同传感器或不同轨道直接叠上去可能错位几十个像素。这个错位不会体现在单图检测里却会在差分阶段制造大量假变化。配准的常见做法是用 ORB 特征匹配或相位相关做粗配准再用道路交叉口或建筑物角点做精配准。如果你的影像自带 RPC 参数可以用 GDAL 的 GCP 点做多项式校正到同一基准。一个省事的替代方案是先把两期影像分别做成瓦片然后用“瓦片编号地理坐标”的映射关系做对齐而不是直接对原始影像操作。后者在瓦片边缘处可能因为投影变形而错位前者则保证了同名瓦片的像素网格完全一致。另外要注意变化检测之前一定要用同一套模型权重。不要 A 时相用一个模型、B 时相又微调过否则模型偏好差异会被解释成道路变化这类“模型漂移”引发的误报很难排查。正确做法是两期影像都使用训练集覆盖时间范围内表现最优的同一权重文件。5. 推理落地与边缘部署保存结果、切片合并和 Jetson Nano 的注意事项5.1 推理脚本至少要能保存三种结果YOLOv11 推理后保存结果是热词里被反复问到的事实际场景中并不只是保存一张标注图那么简单。一个完整的推理脚本应该能同时输出三种东西标注可视化图、JSON 格式的检测元数据含框坐标和置信度、以及跑完变化检测后的 GeoJSON。尤其是第二项单独保存成 JSON 非常关键因为连续项目里会有二次分析、矢量化转换和地图叠加的需求只有可视化图的话后期返工会很痛苦。from ultralytics import YOLO import json model YOLO(runs/segment/train/weights/best.pt) results model.predict( sourcetest_imgs/, imgsz1280, conf0.35, iou0.45, saveTrue, # 保存标注可视化图 save_jsonTrue, # 保存检测结果为json save_txtTrue, # 保存坐标txt projectruns/inference, nametile_results, ) # 把多个瓦片的json汇总成一个便于后续差分处理的列表 all_detections [] for r in results: boxes r.boxes.xyxy.cpu().numpy().tolist() confs r.boxes.conf.cpu().numpy().tolist() for box, conf in zip(boxes, confs): all_detections.append({ bbox: box, confidence: round(conf, 4) }) with open(tile_detections.json, w) as f: json.dump(all_detections, f, indent2)conf0.35 和 iou0.45 两个参数需要根据场景微调。遥感道路提取中漏检比误检更可怕因为后处理无法凭空补出漏掉的路段所以 conf 不宜设太高0.25~0.35 比较合理。iou 参数影响 NMS 对重叠框的抑制强度道路段之间如果 50% 重叠且两个框都可能是真实路段建议 iou 设 0.5 以上避免相邻路段框被合并成一个大框。5.2 大图拼接从瓦片检测结果到连续路网模型推断时按 1280 瓦片切图推断完成后需要把瓦片坐标映射回大图坐标系。因为切图时设置了 50% 重叠同一道路段可能出现在相邻瓦片中产生重复框。合并时常见做法是按 IoU 或中心距离做非极大抑制两个框的置信度取最大值而不是平均值——因为重叠瓦片中模型对道路中段的置信度往往高于边缘部分取平均值会把高置信度拉低。def merge_tile_boxes(all_boxes, iou_thresh0.4): 合并重复框按置信度优先抑制低置信度重叠框. all_boxes: [{bbox: (x1,y1,x2,y2), confidence: 0.82, tile_id: 3}] kept [] # 按置信度从高到低排序 sorted_boxes sorted(all_boxes, keylambda x: x[confidence], reverseTrue) for current in sorted_boxes: duplicate False for kept_box in kept: # 计算两个框的IoU cx1, cy1, cx2, cy2 current[bbox] kx1, ky1, kx2, ky2 kept_box[bbox] inter_w max(0, min(cx2, kx2) - max(cx1, kx1)) inter_h max(0, min(cy2, ky2) - max(cy1, ky1)) inter_area inter_w * inter_h union_area (cx2 - cx1) * (cy2 - cy1) (kx2 - kx1) * (ky2 - ky1) - inter_area iou inter_area / union_area if union_area 0 else 0 if iou iou_thresh: duplicate True break if not duplicate: kept.append(current) return kept这段代码有两点要说明。第一坐标必须先转换到大图坐标系再合并否则不同瓦片的框没有可比性建议在切图时就记录每个瓦片在大图中的偏移量推理结果统一加上偏移量后再进 merge 函数。第二iou_thresh 取 0.4 是一个平衡值如果设 0.5道路交叉口处相邻路段可能重复保留如果设 0.3同一段路被切成两半的框可能被误删一个导致路网断裂。5.3 Jetson Nano 部署降低分辨率、开启 TensorRT 终端Jetson Nano 部署 YOLOv11 的详细步骤可以浓缩成一句话别直接跑 PyTorch转 TensorRT再把输入分辨率降到 640。Nano 的算力只有 472 GFLOPS跑 1280 输入的 YOLO11s 大约只有 2~3 FPS而转 TensorRT FP16 后可以在 640 输入下跑到 8~12 FPS基本满足道路巡查类的准实时需求。部署流程上推荐在 Jetson 上重新源码编译安装 PyTorch而不是用 pip 直接装桌面版 wheel。Nano 是 aarch64 架构官方 PyTorch wheel 对 CUDA 版本有严格要求常见做法是使用 NVIDIA 提供的 JetPack 对应版本容器镜像。具体命令如下# 在Jetson Nano上导出ONNX再转TRT yolo export modelbest.pt formatonnx imgsz640 trtexec --onnxbest.onnx --saveEnginebest_fp16.engine --fp16导出时注意imgsz640必须与部署时的推理尺寸一致否则 TensorRT engine 的输入形状不匹配。如果 ONNX 导出过程报算子不支持优先检查 YOLO11 的 C3k2 模块是否包含某些自定义算子通常的解法是把 ultralytics 升级到较新版本再试。部署时如果 TensorRT engine 加载后速度仍上不去可以用trtexec --dumpProfile查看各层耗时瓶颈通常集中在 Detect 头的回归分支这种情况下可以尝试关闭 NMS 后处理把多个候选框输出到后处理脚本里自己做。6. 避坑指南五个最容易让你怀疑人生的坑6.1 切块重叠率不足导致路网断头现象训练和验证指标都正常但拼图后路网在瓦片边界处大量断裂每条路看起来都“错位了一截”。原因切块步长太大比如 1280 窗口配 1280 步长零重叠道路恰好被切成两段时模型看不到完整的上下文边界处的道路段被截断后模型输出的框置信度偏低在 NMS 阶段被误删。解决切块步长改为窗口的 50%即 640 步长推理合并时不要用全局置信度阈值而是对每个瓦片的边缘区域单独降低 10% 的置信度阈值。6.2 Mosaic 增强让模型学到“假道路拼接”现象开 Mosaic 训练时 mAP 值稳步上涨但用在大图上推理时屋顶边缘、田埂被大量检测成道路。原因Mosaic 把四个瓦片拼接道路边缘和另一个瓦片的屋顶轮廓线可能在拼接缝处形成直线模型学到的不是“道路的纹理”而是“线性边缘结构”。解决训练时设置mosaic0.0如果经验丰富想保留部分增强可以在最后 20 个 epoch 关闭 mosaic 做微调命令里加close_mosaic20。6.3 OBB 角度标注不一致导致回归震荡现象训练损失曲线呈锯齿状震荡验证 mAP 始终在 0.2 附近跳动。原因道路方向是 0~180 度周期性角度如果标注工具在角度接近 180 度时跳变回 0 度模型无法稳定回归。解决标注时统一角度范围为 -90 到 90 度或 0 到 180 度并在预处理脚本里把所有角度归一化到同一区间对接近垂直的道路段宁可选择反向角度代表也不要在角度边界附近产生跳变。6.4 两期影像光照差异被变化检测误报为新增道路现象变化检测输出大量新增道路但人工查看后发现只是农田纹理变化。原因两期影像拍摄季节不同农田的颜色和纹理存在天然差异虽然检测模型已经屏蔽了大部分背景干扰但仍有个别误检框在 A 时相和 B 时相位置不同导致 IoU 低于阈值被标记为新增。解决不要把单帧检测置信度作为唯一依据。对标记为“新增”的框提取两期影像对应位置的纹理特征如灰度直方图、LBP 特征做二次验证如果纹理相似而几何位置差异大说明是同一个场景的误检漂移应归为未变化。6.5 边缘设备上 ONNX 导出失败和推理内存溢出现象在 Jetson Nano 上导出 ONNX 时提示某些算子不兼容或 TensorRT 构建 engine 时爆显存。原因YOLOv11 的新结构里包含若干高层算子旧版 TensorRT 不支持或者输入尺寸 1280 对 Nano 的 4GB 显存而言过大构建 engine 时的中间张量超过了可用内存。解决先将模型导出为 ONNX opset 17 及以上版本在导出时加入simplifyTrue参数做图优化如果仍报算子错误考虑降级使用 YOLOv8 模型并在 Nano 上部署牺牲少量精度换取兼容性。显存不足则把 imgsz 降到 640 或 512并在trtexec构建时加上--maxWorkspaceSize1024限制工作区大小。7. 模型的输出怎么验证和落盘经验指标与图像解释部署图和矢量文件都生成之后还有一个步骤经常被忽略验证检测结果和变化区域是否是真实道路而不是“看起来像道路”的噪声。这里分享一个实用的经验指标体系比单看 mAP 更有说服力。第一个指标叫“断头率”计算方式是把检测框连接成路网后统计度数为 1 的端点数量与单位面积路网总长度的比值断头率高说明模型漏检明显。第二个指标叫“孤立框率”一个道路框如果周围 100 像素内没有任何其他道路框则视为孤立误检框孤立框占全部框的百分比应控制在 5% 以下。可视化验证方面我习惯把变化检测结果输出为三色叠加图新增道路用红色消失道路用蓝色不变道路用绿色叠加在最新时相影像上。这样一图就能直观判断变化区域是否符合事实不必反复切换两期影像。输出时要避免只用 matplotlib 画到屏幕应该直接写成 GeoTIFF 或带地理坐标的 PNG方便在 QGIS 里叠加查看。from PIL import Image import numpy as np def visualize_change(base_img_path, unchanged_boxes, added_boxes, removed_boxes, out_path): 把变化检测结果叠加到最新影像上红新增蓝消失绿不变. base Image.open(base_img_path).convert(RGB) arr np.array(base).copy() for box in unchanged_boxes: cv2.rectangle(arr, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (0, 255, 0), 2) for box in added_boxes: cv2.rectangle(arr, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (255, 0, 0), 2) for box in removed_boxes: cv2.rectangle(arr, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (0, 0, 255), 2) Image.fromarray(arr).save(out_path)这个函数本身没有太多技术含量但有一个参数上的细节框的线宽不要太粗。道路框在 1280 瓦片里往往只有几像素到几十像素宽如果线宽设为 4整条路会被边框盖住看不清实际道路形态。建议线宽与框的短边成比例短边 10 像素以下用 110~30 像素用 2超过 30 像素才用 3 以上。最后说一句我的个人习惯每次跑完一轮训练或变化检测我会强制自己把检测结果导出一份 CSV至少包含框中心坐标、置信度和所属瓦片编号。这份 CSV 是后续做路网图斑分析、误检复判和设备端回灌的后悔药没有它后面想追查每个框是谁、在哪个瓦片、哪次跑出来的基本只能翻黑匣子。卫星遥感道路提取这个方向模型精度固然重要但工程上的可追溯性往往决定项目能不能真正交付。如果一个方案跑出来的结果讲不清来历再高的 mAP 也只是实验室里的数字。以上这套做法的每一步我都踩过坑把它们整理成文字也是希望你能少走这些弯路希望帮到你。本文还有配套的精品资源点击获取