☰
遥感道路二值分割数据集实战:从分块训练到后处理全流程
2026/10/1 8:58:32 网站建设 项目流程

简介:图像分割数据集聚焦大分辨率遥感影像中的道路提取任务,采用背景与道路两类像素级标注。整包包含约6000张带标签图像,划分为训练集(约4300张)与测试集(约1800张),两个目录内均设images图片与masks标签文件夹,并提供classes.txt说明类别。压缩包共2000个文件,以png图像为主(1998个),另有1个txt与1个py可视化脚本,包体大小359.11MB,目录结构一目了然,适合直接输入到UNet、SwinUNet等分割网络训练。py脚本会随机抽取一张影像,同时输出原始图、GT掩膜以及GT叠加在原图上的效果图,可用于快速检查标注质量、观察道路轮廓与模型预测的对照。面向遥感图像分割入门与进阶学习者,已有104人学习,是开展道路提取实验和算法验证的实用数据集。

1. 遥感道路二值分割数据集:6000张图到底能不能训练出可用的道路提取模型

拿到“图像分割数据集:大分辨率下的遥感位置道路图像分割数据集(2值分割,约6000张数据和标签)”,第一反应别急着找模型。先把任务看清楚:这是一份遥感影像的道路提取数据,每个像素只分两类——道路和非道路,也就是典型的二值分割。6000张图和对应标签,规模在大分辨率遥感分割里算中等偏上,够训练一个能用的分割网络,前提是处理方式正确。很多人在这种数据集上翻车,不是模型不够强,而是把大图直接缩小塞进网络,把二值标签当成普通灰度图,最后道路断裂、边界糊成一团。这篇笔记按数据核验、分块、训练、踩坑、后处理的顺序,把整个流程拆开讲。适合刚接触遥感分割的算法工程师,也适合想判断这份数据值不值得投入时间的团队。

2. 数据集内部结构:标签不是一张黑图,二值分割也有边界标准

2.1 图像与标签的配对组织方式

拿到这类数据集,先不要假设它的目录结构是统一的。常见做法有两种:一种是 images 和 masks 两个文件夹,文件名一一对应,比如image_0001.png对应label_0001.png;另一种是只有原图,标签是单独导出的 GeoJSON 或 shapefile 矢量文件,需要自己栅格化成 mask。第二种更麻烦,因为坐标对齐、投影转换、栅格化分辨率都要自己处理。我建议第一步先把所有图像和标签统一成同尺寸、同格式的 PNG,再做配对校验。

做配对校验时最容易翻车的点是:图像是 3 通道 RGB,标签可能是单通道灰度,也可能是 3 通道的彩色 PNG(人眼看到的黑色和白色,读进数组却是三维的)。如果是后者,需要先做mask[:, :, 0]取单通道,否则后续计算二值交叉熵会直接报维度错误。另一个隐蔽问题是标签里的道路像素是 0,背景是 255,和常见的 0/1 约定相反。这类不一致不会影响训练收敛,但会影响你打印可视化和计算 IoU 时的阈值判断。

import os import cv2 import numpy as np img_dir = "data/road/images" mask_dir = "data/road/labels" for name in os.listdir(img_dir): img_path = os.path.join(img_dir, name) mask_path = os.path.join(mask_dir, name) if not os.path.exists(mask_path): print(f"[MISSING] {name} 标签不存在") continue img = cv2.imread(img_path) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if img.shape[:2] != mask.shape[:2]: print(f"[SIZE MISMATCH] {name}: img={img.shape}, mask={mask.shape}") continue unique = np.unique(mask) if len(unique) > 2: print(f"[MULTI-CLASS] {name}: 标签不是二值,包含 {unique}") else: print(f"[OK] {name}, img={img.shape}, mask_values={unique}")

这段代码做的事情很直接:遍历图像目录,找同名标签,检查尺寸是否一致,检查标签像素值是不是只有两类。逻辑上要注意的是cv2.imread对中文路径支持不好,如果数据目录带中文,建议先用cv2.imdecode读。参数层面有两个关键点:第一,IMREAD_GRAYSCALE强制把彩色标签压成单通道,避免通道数不一致;第二,打印unique时看到[0, 255]不要慌,后面归一化到[0, 1]就行,真正要警惕的是出现[0, 1, 255]这类多值,说明某张标签里混入了其他类别。

2.2 标签里“道路”的标准:从车行道到人行道的判定差别

二值分割的难点不在“二值”,而在“道路”的定义。同一景影像上,有的标注把机动车道、非机动车道、人行道全部算作道路,有的只标车行道,有的连小区内部路都标进去了。这直接影响模型的泛化边界。你在训练前一定要抽 20 到 30 张图,把原图和 mask 叠在一起,用肉眼确认标注标准。我见过最典型的情况:训练集里道路都是主干道,验证集里出现大量狭窄巷道,模型 IoU 直接掉 10 个点,不是过拟合,是标注标准不一致。

还有一个容易被忽略的点:标注边界偏移。遥感影像分辨率高的时候,道路边缘在像素级上是模糊的,标注员手动勾边难免偏 1 到 2 个像素。这种偏移对 mIoU 这类指标影响有限,但对细道路的连续性影响很大。可以用膨胀测试快速验证标签边界质量:把 mask 膨胀几个像素,再和原 mask 计算 IoU,如果 IoU 超过 0.95,说明边界膨胀后几乎没变化,标签本身就很粗;如果只有 0.85,说明边界细节相对丰富。

import cv2 import numpy as np def boundary_check(mask_path, kernel_size=5): mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask = (mask > 0).astype(np.uint8) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size)) dilated = cv2.dilate(mask, kernel, iterations=1) intersection = np.logical_and(dilated == 1, mask == 1).sum() union = np.logical_or(dilated == 1, mask == 1).sum() return intersection / union # 用法示例:对抽样标签计算边界扩张后的 IoU iou = boundary_check("data/road/labels/image_0001.png", kernel_size=5) print(f"膨胀 5x5 后 IoU = {iou:.3f}")

这段代码的核心是cv2.dilate对二值 mask 做形态学膨胀,然后计算膨胀前后 mask 的 IoU。kernel_size控制膨胀范围,一般从 5 试到 15,逐步加大看 IoU 变化曲线。如果 IoU 本身接近 0.9 以上,说明标签边缘预留了较大缓冲区;如果加一点膨胀 IoU 就明显下降,说明标签贴合影像边缘,精度较高。这个测试的价值在于:它决定了你在训练时能不能放心用强数据增强。标签边界粗的数据,翻转、缩放都不太容易出错;标签边界精细的,过度增强反而会把道路宽度学歪。

2.3 用 40 行代码核验数据完整性:尺寸、通道、道路占比一次查完

训练前做一次全量数据体检是值得的,6000 张图用脚本跑一遍只需几分钟,能避免训练到一半才发现有坏图。体检项目包括四项:图像标签尺寸是否一致、图像是否有损坏、标签是否严格二值、道路像素占比是否合理。道路占比是个硬指标,遥感道路数据集的典型占比在 2% 到 15% 之间,如果某张图的道路占比超过 20%,要么是大片密集路网,要么标签里混入了非道路内容;如果低于 0.5%,这张图很可能是背景主导的难样本。

import os import cv2 import numpy as np img_dir = "data/road/images" mask_dir = "data/road/labels" report = [] for name in sorted(os.listdir(img_dir)): img_path = os.path.join(img_dir, name) mask_path = os.path.join(mask_dir, name.replace(".jpg", ".png")) if not os.path.exists(mask_path): report.append((name, "missing_mask")) continue img = cv2.imread(img_path) if img is None: report.append((name, "broken_image")) continue mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: report.append((name, "broken_mask")) continue if img.shape[:2] != mask.shape[:2]: report.append((name, "size_mismatch")) continue road_ratio = (mask > 0).mean() if road_ratio < 0.005 or road_ratio > 0.20: report.append((name, "extreme_ratio", round(road_ratio, 4))) for item in report[:20]: print(item) print(f"total issues: {len(report)}, checked: {len(os.listdir(img_dir))}")

逻辑说明:mask > 0把标签统一成布尔二值,再求均值得到道路像素占比。这个脚本没有做多线程,6000 张图全量跑大约需要 3 到 5 分钟。参数方面,extreme_ratio的上下限可以根据标签标准调整,如果数据集包含郊区影像,道路占比普遍偏低,下限可以放宽到 0.002。这个体检报告建议保存成 CSV,在划分训练验证集时排除掉明显有问题的样本,尤其是size_mismatch和broken_mask这两类,它们会在训练时直接导致崩溃。

3. 大分辨率影像不能直接进网络:分块裁剪、重叠拼接与坐标恢复

3.1 为什么要把原图裁开:显存只是表面原因

6000 张大分辨率遥感图,单张尺寸常见 4000x4000 甚至更大。直接送进 U-Net 这类编码器-解码器结构,先不说显存够不够,下采样链会把特征图缩到很小,一次下采样道路细线就消失了。所以必须采用滑窗裁剪,把大图切成若干个 patch,训练和推理都基于 patch 进行。切块的尺寸、重叠率、坐标记录方式共同决定了模型输出能否拼回完整语义图。

显存问题确实存在,但不是核心矛盾。核心矛盾是感受野和分辨率的权衡:patch 太小,道路的走向和连通信息被截断,模型只能看到局部线段;patch 太大,batch 数量受限,训练速度下降,同时对显存的压力成倍增加。常见做法是 patch 边长取 512 或 1024,配合 128 到 256 像素的重叠区域。这样做的好处有两个:一是重叠区域给推理拼接留了缓冲,二是训练时每张图能裁出大量 patch,相当于扩充了样本数量。

3.2 带坐标的滑窗裁剪脚本:训练、验证、推理共用同一份元数据

裁剪不是简单把图切开,关键是要把每个 patch 在原图中的坐标记录下来。训练时按顺序读 patch,推理时按坐标把预测结果拼回大图。如果裁剪和拼接两套坐标不统一,后面拼接接缝错位是必然的。最好的做法是:一开始就把坐标信息写入一个 CSV,训练、验证、推理全部依赖这个 CSV 操作。

import cv2 import numpy as np import pandas as pd def slide_crop(image_path, mask_path, save_dir, patch_size=512, overlap=128): img = cv2.imread(image_path) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) h, w = img.shape[:2] stride = patch_size - overlap records = [] for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): img_patch = img[y:y + patch_size, x:x + patch_size] mask_patch = mask[y:y + patch_size, x:x + patch_size] patch_name = f"{image_path.stem}_y{y}_x{x}" cv2.imwrite(f"{save_dir}/images/{patch_name}.png", img_patch) cv2.imwrite(f"{save_dir}/labels/{patch_name}.png", mask_patch) records.append({"patch": patch_name, "y": y, "x": x, "h": h, "w": w}) return pd.DataFrame(records) # 执行裁剪并保存坐标 CSV df = slide_crop( image_path="data/raw/image_0001.png", mask_path="data/raw/label_0001.png", save_dir="data/patches", patch_size=512, overlap=128 ) df.to_csv("data/patches/coords.csv", index=False)

逻辑说明:裁剪窗口遍历是从左上角开始,步长stride = patch_size - overlap,保证了相邻 patch 之间有 overlap 像素的重叠区域。records里记录了每个 patch 的左上角坐标(y, x)和原始大图尺寸(h, w),推理拼接时靠这两组数据把 patch 放回原位。代码里用image_path.stem生成 patch 文件名,避免与同名目录混淆。参数层面,patch_size 512、overlap 128 是多数情况下的稳妥起点;如果道路特别宽或目标是大尺度连通结构,建议把 patch 提到 1024,overlap 提到 256,代价是单张图裁剪出的 patch 数量减少。

3.3 推理时把 patch 拼回大图:重叠区梯度融合与去接缝

分块推理之后拼图,直接按照坐标把预测结果贴回去,会在重叠区域出现明显的接缝——通常是两条平行的伪影线。原因是模型在 patch 边界处的预测置信度偏低,重叠区的预测结果和相邻 patch 不一致。解决办法是重叠区不取单一预测值,而是做加权融合,越靠近 patch 中心,权重越大。

import numpy as np def reconstruct(pred_patches, coords, image_size, overlap=128): h, w = image_size accumulator = np.zeros((h, w), dtype=np.float32) weight_map = np.zeros((h, w), dtype=np.float32) patch_size = pred_patches[0].shape[0] for pred, (y, x) in zip(pred_patches, coords): # 构建三角权重:左右/上下边缘低,中心高 w_row = np.minimum(np.arange(patch_size), patch_size - 1 - np.arange(patch_size)) w_col = np.minimum(np.arange(patch_size), patch_size - 1 - np.arange(patch_size)) w_2d = np.minimum(w_row[:, None], w_col[None, :]).astype(np.float32) + 1.0 accumulator[y:y + patch_size, x:x + patch_size] += pred * w_2d weight_map[y:y + patch_size, x:x + patch_size] += w_2d final = accumulator / np.maximum(weight_map, 1e-6) return (final > 0.5).astype(np.uint8)

说明:w_2d是一个从边缘向中心渐变的二维权重矩阵。边缘处权重接近 1,中心处权重接近 patch_size/2,这样拼接时重叠区域的预测值按权重混合,避免某一边的预测结果“硬切换”。accumulator和weight_map分别累计加权预测值和权重总和,最终做除法得到归一化结果。最后用> 0.5阈值转回二值 mask,阈值大小可调,如果希望提取结果更保守、减少假阳性,可以提高到 0.6 或 0.7。这个拼接结果和有没有重叠区、重叠区大不大有直接关系,overlap 小于 64 时拼缝几乎无法完全消除。

3.4 分块带来的上下文丢失:用多尺度推理或更大感受野缓解

分块裁剪把大图的全局信息切碎了,这是所有基于 patch 的分割方法都要付出的代价。道路这种长条形目标,单张 512 patch 里往往只有几段短线段,模型很难判断它应该向左连通还是向右延伸。缓解措施有两个方向:一是在训练时用更大的输入尺寸,比如 1024,让模型在单次前向中看到更长的道路走向;二是在推理时做多尺度,分别用 512 和 1024 的patch 预测,把两个尺度的预测结果叠加再取平均。

多尺度推理会增加一倍以上的推理耗时,但效果明显,尤其是在道路密集区域,能减少断线。SegFormer 这类基于 Transformer 的分割模型对 patch 大小更敏感,因为它有全局注意力机制,patch 切得越小,注意力能覆盖的范围就越有限。所以如果用 SegFormer 做遥感道路分割,建议 patch 至少取 1024,序列长度会变得很大,显存占用也会随之上升,训练时要把 batch size 相应调小。

4. 训练与评估:把 6000 张数据变成一条可用的道路提取模型

4.1 模型选择:U-Net、DeepLabV3Plus、SegFormer 怎么选

道路分割是经典的二值分割任务,可选模型很多,但结合 6000 张的规模和遥感影像特点,常用的就是三条路线。第一类是 U-Net 及其变体,结构轻、显存占用小,从零开始训练也能收敛,适合快速验证数据质量。第二类是 DeepLabV3Plus,空洞卷积带来更大的感受野,对道路这种长条形目标有一定优势,建议加载 ResNet 预训练权重。第三类是 SegFormer,近年在遥感分割上很常用,Transformer 编码器对全局上下文的建模能力强,但数据需求量也更大,6000 张规模下务必加载预训练权重,否则很容易欠拟合。

模型预训练依赖显存占用道路分割效果适用阶段
U-Net (ResNet-34)低低,约 6GB中上,细线易断快速验证
DeepLabV3Plus (ResNet-50)中中,约 8GB好,边界干净正式训练
SegFormer-B2高高,约 12GB好,连通性佳追求精度

6000 张数据量不算大,但也不是特别小。如果是从零训练 U-Net,大概需要 80 到 100 个 epoch 才能看到一个稳定可用的结果;加载预训练编码器后,30 到 50 个 epoch 就能达到相近水平。建议先跑一个 8 到 10 个 epoch 的短实验,看验证集 mIoU 是否随训练稳定上升。如果完全不涨,大概率是数据配错或标签问题,这种情况排查数据比调模型参数更优先。

4.2 损失函数与超参:为什么交叉熵在道路分割上常常不够

道路像素在整张图中占比通常在 5% 以下,普通交叉熵损失会让模型倾向于把大多数像素预测为背景,因为这样做损失就已经很小了。解决方法是把 Dice 损失和交叉熵加权组合。Dice 损失直接优化区域重叠度,对正负样本不敏感;交叉熵保留像素级梯度,让训练初期更容易收敛。

import torch import torch.nn as nn import torch.nn.functional as F class DiceCE(nn.Module): def __init__(self, dice_weight=0.5, ce_weight=0.5): super().__init__() self.dice_weight = dice_weight self.ce_weight = ce_weight def forward(self, logits, targets): probs = torch.sigmoid(logits) # Dice 损失 smooth = 1.0 intersection = (probs * targets).sum() dice = (2.0 * intersection + smooth) / (probs.sum() + targets.sum() + smooth) dice_loss = 1.0 - dice # 二值交叉熵 ce_loss = F.binary_cross_entropy(probs, targets) return self.dice_weight * dice_loss + self.ce_weight * ce_loss

逻辑说明:probs是 sigmoid 输出的概率图,targets是 0/1 二值标签。Dice 损失计算的是预测概率和标签的重叠率,范围在 0 到 1 之间,越接近 0 说明预测越准。smooth防止除零。实际训练中dice_weight可以设到 0.7,ce_weight设 0.3,对道路这种正样本占比低的任务更友好。如果发现训练早期损失波动很大,先把两个权重设成 0.5 对半,稳定后再调整。

训练超参方面,patch_size 512 时一个常见配置是 batch_size 8、初始学习率 3e-4、优化器 AdamW、训练 60 个 epoch。学习率用 warmup 加余弦衰减,warmup 步数 500 到 1000 步,避免初始阶段 loss 震荡。数据增强要克制:随机水平翻转、垂直翻转、随机亮度对比度、高斯噪声可以用;随机旋转建议只做 90 度的倍数,任意角度旋转会破坏道路的线性结构;随机裁剪不建议超过原本 patch 的 20%,因为裁剪后道路拓扑信息进一步丢失。

4.3 指标计算:mIoU、F1、Recall,用哪两个指标决定模型是否可用

分类任务看 accuracy 就够了,分割任务不行。道路分割里背景占比太高,一个全预测背景的模型 accuracy 也能超过 90%,但没有任何实际价值。判断模型是否可用,核心看两个指标:IoU 和 Recall。IoU 反映预测区域和真实标签的重叠程度,Recall 反映真实道路有多少被找回来。重点关注 IoU 和 Recall 的组合:IoU 高但 Recall 低,说明预测的区域很精准但漏检严重,道路断线多;IoU 中上且 Recall 也高,才是可用的提取模型。

import numpy as np def segmentation_metrics(pred, target): pred = (pred > 0.5).astype(np.uint8) target = (target > 0).astype(np.uint8) intersection = np.logical_and(pred == 1, target == 1).sum() union = np.logical_or(pred == 1, target == 1).sum() iou = intersection / max(union, 1) tp = intersection fp = np.logical_and(pred == 1, target == 0).sum() fn = np.logical_and(pred == 0, target == 1).sum() precision = tp / max(tp + fp, 1) recall = tp / max(tp + fn, 1) f1 = 2 * precision * recall / max(precision + recall, 1e-6) return {"iou": iou, "precision": precision, "recall": recall, "f1": f1}

这段代码逻辑直接:交集、并集、假阳、假阴全部用布尔运算统计,没有黑匣子。评价模型时,建议按整张大图计算指标,而不是按 patch 平均。按 patch 平均会高估性能,因为很多 patch 道路占比为 0,模型预测全背景也能得到很高的 patch 级 IoU。正确的做法是先拼接整景预测图,再和整景标签计算指标。一般来说,IoU 到 0.55 以上、Recall 到 0.6 以上,模型输出的道路拓扑基本连通,可以作为初版结果;IoU 低于 0.45 时不要急着调模型,先回头看标签和分块参数是不是有问题。

5. 避坑:遥感道路二值分割数据集使用中的 5 个高频翻车点

5.1 标签错位:loss 在降,预测的道路却不在正确位置上

现象:训练 loss 曲线正常下降,验证 mIoU 也不低,但可视化预测结果时发现模型提取的道路位置和影像道路明显对不上,歪了一块。

原因:图像与标签存在像素级偏移,通常来自出图时的坐标基准不一致,或者图像做了配准、裁剪但标签没有同步变换。这类错误在缩略图上看不出来,放大到 patch 级别才明显。

解决:在分块之前抽 5 张图,把原图转成灰度后与 mask 做边缘叠加,肉眼检查道路边缘和 mask 边缘是否对齐。如果整体偏移固定,可以用np.roll或cv2.warpAffine对齐;如果偏移随空间变化,说明坐标系出了问题,需要重新检查地图投影和裁剪范围。

5.2 标签是彩色 PNG,训练时直接报维度错误

现象:数据加载时报expected 1 channel, got 3,或者 loss 出现 NaN。

原因:标签存储为 RGB 彩图,黑色背景和白色道路是视觉上的,但读进来是三个通道。常见的 PNG 标注导出工具会默认保存成彩色索引图,也有的保存成带透明通道的 RGBA。

解决:统一读取时用cv2.imread(path, cv2.IMREAD_GRAYSCALE)强制转单通道。不要在数据加载器里做mask[:, :, 0]这种临时处理,因为不同文件的通道顺序可能不一致。最好在数据预处理阶段就把所有标签统一重存为 8 位单通道 PNG。

5.3 道路只有 1 到 3 个像素宽,U-Net 下采样后直接消失

现象:主干道提取正常,但巷道、小路、匝道等细道路在预测结果中完全断裂或消失。

原因:连续五次下采样后,1 到 3 像素宽的道路在特征图上只占不到 1 个像素,编码器直接把它当噪声滤掉了。这不是损失函数能解决的,是网络结构本身的限制。

解决:一是把输入 patch 尺寸提高,保留更多原始分辨率信息;二是在损失函数中对细道路像素加权,计算每个像素到最近背景的距离,距离越近权重越大;三是数据预处理时对标签中的细道路做适度膨胀,让网络先学会“粗道路”,再逐步恢复细节。最后一个方法虽然降低精度,但能保证拓扑连通性,很多实际工程里连通性优先于像素精度。

5.4 随机划分训练集导致同景影像泄漏

现象:训练 mIoU 很高,验证 mIoU 也很高,但换一组城市或换一个时间段测试,效果暴跌。

原因:6000 张图往往来自有限数量的原始影像,切块后同一张原始影像的多个 patch 被同时分到训练集和验证集。模型相当于提前“见过”验证集的一部分内容,评价指标虚高。

解决:按原始影像的 ID 分组划分数据集,而不是按 patch 文件随机分。也就是说,同一张大图裁剪出的所有 patch 必须全部进训练集或全部进验证集。做法是在坐标 CSV 中加入source_id字段,划分时按source_id做 group split。

5.5 推理拼接后出现规则网格状伪影

现象:拼接完成的大图上有一圈一圈的矩形边界,尤其在道路密集区域特别明显,像是给预测结果盖了一层网格。

原因:patch 边缘的预测结果和中心区域不一致,重叠区域融合权重使用不当,或者推理时没有使用重叠区,直接硬贴。网格状伪影本质上是模型对边界上下文感知不足。

解决:推理时必须使用带重叠的滑窗,拼接时用三角权重融合。重叠区不要低于 64 像素,推荐 128 到 256。另外可以尝试推理时做左右翻转(TTA),把两次预测结果平均后再拼接,能进一步压掉边界差异。

6. 进阶技巧:不动网络结构,把道路提取结果“修”出来的三个后处理

训练好的模型输出往往不是最终可交付结果。模型预测的 mask 里,通常有三个常见毛病:碎块噪声、道路边缘锯齿、短线断裂。这三个问题都不需要重新训练,靠后处理就能修掉大半。

先做形态学开运算去掉碎块噪声。开运算是先腐蚀再膨胀,能把面积很小的独立预测块消掉,同时保留道路主干。核的大小一般取 5x5 到 7x7,核太大会把细道路也腐蚀掉,需要针对验证集调一次。然后是去小连通域:用连通域分析把所有预测区域标记出来,面积小于阈值的区域直接置为背景。阈值怎么定?按 patch 尺寸估算,512 patch 上面积小于 100 像素的区域基本都是误检,可以安全删掉。

最后是道路断线连接。直接做闭运算(先膨胀再腐蚀)能拉近断口,但对断口距离较大的情况效果有限。更可靠的方法是把道路 mask 做骨架化,检测断点,然后用最短路径把相邻断点连接起来。这一步在工程上稍微复杂,但效果很直观。

import cv2 import numpy as np def postprocess(mask, min_area=100, kernel_size=5): # 消除小块噪声 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size)) opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 删除小连通域 num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(opened, connectivity=8) cleaned = np.zeros_like(opened) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] >= min_area: cleaned[labels == i] = 1 # 闭运算连接短断口 closed = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel, iterations=2) return closed

后处理参数需要针对验证集调一遍,核心是 min_area 和 kernel_size 的配合。我的习惯是:先跑 5 组参数组合,统计后处理前后的 IoU 和连通域数量变化,选 IoU 不降且碎块数量明显减少的那组。这里有个细节容易忽略:后处理是把双刃剑,开运算和去小连通域会把一些真实但细小的道路也删掉,闭运算会把原本分离的两条平行道路黏在一起。所以后处理完一定要抽样做视觉确认,不能只看 IoU 数字。

我的教训很直接:第一次用这种道路数据集时,我把整张大图直接 resize 到 512 塞进网络训练,出来的结果惨不忍睹,断线多到没法看。后来老老实实分 patch、记坐标、拼接融合、后处理四件套走完,才稳定下来。这个流程不复杂,但每一步都省不得。数据决定上限,分块和拼接方式决定下限,模型只是在中间承上启下。希望这些踩坑记录对你有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询