news 2026/10/1 6:30:15

DeepLab-ResNet建筑物变化检测实战:双塔共享权重与工程避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepLab-ResNet建筑物变化检测实战:双塔共享权重与工程避坑指南

简介:面向遥感与 GIS 领域研究者和深度学习开发者,这份资源提供基于 Deeplab-resnet 的建筑物变化检测完整 Python 实现。算法融合 Deeplab 空洞卷积与 ResNet 残差连接,可对高分辨率影像中的建筑边界进行精细分割,适用于城市规划、灾害评估、环境监测等需要及时识别建筑新建、拆除或损坏的场景。包内共 40 个文件,zip 压缩包约 170KB,以 33 个 Python 脚本为主体,覆盖训练、预测、参数配置、数据加载、损失计算、评估指标等完整流程;另有 5 个文本文件用于说明与使用指引,1 份 LICENSE 和 1 个 .gitignore 保证开源规范与仓库整洁。已有 330 人浏览学习。通过学习源码,可完整了解数据预处理、模型搭建、训练评估和推理部署的实现思路,工程目录组织清晰,适合希望复现语义分割算法、开展变化检测实验的初学者,也方便研究者在已有框架上扩展改进。

1. 拿到两期遥感影像,你的第一个判断不该是“我要训练一个变化检测网络”

如果你手里正好有两个时间点的同一片区影像,想搞清楚哪些位置冒出了新房子、哪些楼被拆掉了,那你搜到Deeplab-resnet 建筑物变化检测源码这个方向不算绕路。常见做法不是去端到端地学习“两期影像到变化图”的映射,而是把 DeepLab-ResNet 当作一个非常耐用的特征提取骨架,先把每一期影像分割语义提出来,再让两期特征去“对账”。这套方案稳定、可复现、翻车点明确,适合做自然资源监测、项目验收复核,以及遥感方向课程的课程设计。这篇文章按我自己的落地路径来写,从框架设定、数据预处理、训练参数一路讲到排查血泪经验,新手可以照着跑,熟手可以直接跳到避坑章节。

2. 为什么选 Deeplab-resnet 做变化检测:双塔共享权重才是真正的主角

2.1 先搞懂 Deeplab-resnet 不是分割模型,而是特征提取器

很多人第一次接触Deeplab-resnet是在语义分割任务里,于是拿到变化检测题目后第一反应是:把两期影像拼成六个通道,直接喂给 DeepLab-ResNet 做二分类分割。这个思路能跑通,但指标往往很难看,原因是网络被要求同时理解“地物长什么样”和“两期差了什么”,两件事挤在同一组卷积里,互相干扰。

DeepLab-ResNet 真正值钱的部分是两个:底层的 ResNet 负责把影像从像素空间抽象到语义特征空间,顶层的 ASPP 空洞空间金字塔池化负责在不同感受野下聚合上下文。ASPP 用不同空洞率的并行卷积捕捉多尺度信息,这对建筑物变化特别友好。建筑物边缘尖锐、尺度差异大,一层普通卷积很难同时照顾到城中村密布的小房子和工业厂房的超大屋顶,而 ASPP 的多分支设计恰好把这个矛盾拆开了。

在变化检测里,我一般把 ResNet 当作“特征提取器”而不是“分类器”来用,ASPP 则作为特征精炼模块。两个时相的影像各自经过同一套参数的前向计算,得到两组特征图,再做逐像素差异或者通道拼接,最后用一个轻量卷积头判断每个像素是“变化”还是“不变”。这个结构和单图语义分割的最大区别在于:参数是共享的,但判断是相对的。

2.2 变化检测的三种框架怎么选:单流输入、双流拼接、共享权重双塔

把两期影像送进网络,从业界落地经验看主要有三种组织方式。

第一种是单流输入,把 T1、T2 影像在通道维度上拼接成 6 通道输入,直接训练一个分割网络。优点是代码最简单,缺点也明显:网络要自己学会“先分别提取特征再比较”这件事,而且 3 通道预训练权重不能直接用,第一层卷积得重新初始化,训练数据不够时非常容易过拟合。

第二种是双流不共享权重,T1 和 T2 各走一套独立的 ResNet,最后把特征拼接或求差。这个方案的表达能力强,但参数量翻倍,训练难度也随之上涨。小数据集上两套网络会各自学到不同的特征分布,变化检测本身是在找“差异”,如果两侧特征分布本身不一致,模型会倾向于把这种不一致误判为变化。

第三种是共享权重双塔,T1 和 T2 分别输入,但经过的是同一套网络参数。这是我在建筑物变化检测里最常用的方案。它既保留了双流的“先提取再比较”结构,又让两侧特征保持在同一个语义空间里,差异计算更干净。参数量只比单图分割略多一个变化头,训练稳定,预训练权重也能完整迁移。

我自己做选型判断时就看三点:数据量够不够支撑双流独立参数、两期影像是不是同传感器同季节、项目周期允不允许做精细调参。数据量小、时间紧,无脑选共享权重双塔;数据量大且两期影像风格差异大,可以考虑不共享权重的双流,但需要在损失函数上额外约束两侧特征分布的一致性。

2.3 最小可跑通的主干拼接:把分割头换成变化头

下面给出一段可以直接放进训练脚本的主干构造代码。以 torchvision 官方的 DeepLabV3-ResNet101 为起点,取它的 Backbone 和 ASPP 部分,再接一个输出 1 个通道的变化头。

import torch import torch.nn as nn from torchvision.models.segmentation import deeplabv3_resnet101 from torchvision.models.segmentation import DeepLabV3_ResNet101_Weights def build_change_detection_model(in_channels=3): # 新版 torchvision 传 weights,旧版传 pretrained=True deeplab = deeplabv3_resnet101(weights=DeepLabV3_ResNet101_Weights.DEFAULT) backbone = deeplab.backbone # ResNet101 特征提取部分 aspp = deeplab.classifier # ASPP + 1x1 卷积 class SharedChangeHead(nn.Module): def __init__(self, aspp, out_channels=1): super().__init__() self.aspp = aspp self.change_conv = nn.Sequential( nn.Conv2d(256 * 2, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.Conv2d(256, out_channels, kernel_size=1) ) def forward(self, feat_t1, feat_t2): # 共享权重体现在:同一个 self.aspp 处理两期特征 out_t1 = self.aspp(feat_t1) out_t2 = self.aspp(feat_t2) # 拼接而不是直接做差,保留更多信息 diff = torch.cat([out_t1, out_t2], dim=1) return self.change_conv(diff) class ChangeDetector(nn.Module): def __init__(self, backbone, aspp): super().__init__() self.backbone = backbone self.change_head = SharedChangeHead(aspp) def forward(self, img_t1, img_t2): feat_t1 = self.backbone(img_t1)['out'] feat_t2 = self.backbone(img_t2)['out'] return self.change_head(feat_t1, feat_t2) return ChangeDetector(backbone, aspp)

这段代码的逻辑是:两期影像分别过同一个backbone,得到各自的语义特征图;这两组特征再分别过同一个aspp模块,完成多尺度上下文聚合;最后一层变化头把两期特征拼接起来,用一个小卷积网络判断变化。注意self.aspp和self.backbone在SharedChangeHead和ChangeDetector中都是同一个对象引用,所以两期输入的权重天然就是共享的。

这里的关键参数是通道数。torchvision 中 DeepLabV3-ResNet101 的 ASPP 输出通道是 256,拼接后变成 512,所以变化头第一层卷积输入是256 * 2。如果你换用 ResNet50 版本,这个数字还是 256,因为 ASPP 的输出通道和 Backbone 层数无关。变化头最后输出 1 个通道,配合 Sigmoid 做二分类。

3. 数据准备是变化检测的胜负手:对齐、归一化、patch 切分

3.1 两期影像必须先做相对归一化,否则模型学到的是“亮度差”

建筑物变化检测的训练数据比普通分割数据难伺候得多。最常见的翻车现场是:训练集和验证集都是从同一对两期影像里切的 patch,模型 F1 分数高得惊人,一放到不同季节、不同光照的新影像上就大面积误检。

原因出在两期影像的辐射差异上。如果 T1 是上午拍的、T2 是下午拍的,或者 T1 是夏天、T2 是冬天,哪怕地面上什么都没变,像素值也差了一大截。模型如果只靠颜色亮度差异来判断变化,就会把阴影、植被枯荣、水体倒影全部报成变化区域。所以数据预处理的第一步不是增强,而是把两期影像拉到同一个辐射水平上。

我一般用直方图匹配做相对归一化。以 T1 影像为基准,把 T2 影像的每个波段的直方图匹配到 T1 的对应波段上。注意不是匹配全局灰度,而是逐波段匹配。如果传感器不同,还要先做重采样保证空间分辨率一致。这一步做完后再做归一化,常见做法是把两期影像都缩放到[0, 1],或者按整幅影像的均值和标准差做 z-score。

需要特别强调的是,不要直接套用 ImageNet 的均值标准差。遥感影像的波段分布和自然图像差异很大,尤其多光谱影像,用 ImageNet 参数归一化会把近红外等波段的数据压到一个非常窄的区间,导致网络在浅层就丢失信息。

3.2 写一个最省事的 PyTorch Dataset:同时读取 T1、T2 和标签

下面给出一个适合遥感影像变化检测的 Dataset 类,用 Rasterio 读取 GeoTIFF,支持随机裁剪、翻转和旋转增强。

import numpy as np import rasterio import torch from torch.utils.data import Dataset import random class ChangeDetectionDataset(Dataset): def __init__(self, t1_path, t2_path, label_path, patch_size=512, augment=True): self.t1_path = t1_path self.t2_path = t2_path self.label_path = label_path self.patch_size = patch_size self.augment = augment # 读取影像元信息,用于计算patch的起始位置 with rasterio.open(t1_path) as src: self.height, self.width = src.height, src.width self.bands = src.count self.patches = [] for top in range(0, self.height - patch_size + 1, patch_size): for left in range(0, self.width - patch_size + 1, patch_size): self.patches.append((top, left)) def __len__(self): return len(self.patches) def __getitem__(self, idx): top, left = self.patches[idx] with rasterio.open(self.t1_path) as src1, \ rasterio.open(self.t2_path) as src2, \ rasterio.open(self.label_path) as src_label: t1 = src1.read(window=((top, top + self.patch_size), (left, left + self.patch_size))) t2 = src2.read(window=((top, top + self.patch_size), (left, left + self.patch_size))) label = src_label.read(window=((top, top + self.patch_size), (left, left + self.patch_size))) # 转成 float32 并缩放到 [0,1] t1 = t1.astype(np.float32) / 255.0 t2 = t2.astype(np.float32) / 255.0 label = label.astype(np.float32) / 255.0 label = (label > 0.5).astype(np.float32) # 标签二值化 # 数据增强:保持 t1、t2、label 做同样的几何变换 if self.augment: if random.random() > 0.5: t1 = t1[:, :, ::-1].copy() t2 = t2[:, :, ::-1].copy() label = label[:, :, ::-1].copy() if random.random() > 0.5: # 随机旋转 90 度的倍数 k = random.choice([1, 2, 3]) t1 = np.rot90(t1, k=k, axes=(1, 2)).copy() t2 = np.rot90(t2, k=k, axes=(1, 2)).copy() label = np.rot90(label, k=k, axes=(1, 2)).copy() return ( torch.from_numpy(t1), torch.from_numpy(t2), torch.from_numpy(label).unsqueeze(0) )

这个 Dataset 的核心逻辑是坐标对齐。t1、t2、label必须来自同一套地理坐标,切 patch 时用同一个top和left,保证三个文件在窗口范围内严格对应。增强部分的关键点是 T1、T2、标签必须做完全相同的变换,否则模型会被迫去学习“错位”的伪变化。

patch_size=512是兼顾显存和感受野的常用选择。小于 256 会让 ASPP 的大空洞率卷积失去意义,大于 1024 则会让训练显存压力很大。影像分辨率特别高时,建议先按比例压缩到 1024 像素以内再切 patch,而不是直接从原始分辨率硬切。

3.3 没有双时相标注时怎么办

很多读者拿到手的只有一份模型源码,没有配套数据。如果手上只有单一时相影像和建筑物标注,又想验证变化检测流程,可以自己造模拟变化样本。做法是对原图做一块区域的平移、遮挡或颜色扰动,把扰动区域当成“变化标签”。这种数据能帮你调通训练和推理流程,也能让模型学到一些边缘特征,但不要指望它能代表真实的变化,真实世界的变化更多是结构变化,不是颜色扰动。

真实项目里更可靠的路径是找同一区域两期公开影像,手动标注或半自动标注变化区域。标注时注意一条边界:只有建筑物本体结构发生变化才算变化,屋顶颜色翻新、阴影变化、车辆移动都不算。这个标注标准直接影响模型学到的语义,很多人后期误检严重,回头查标注才发现标注本身的噪声太大。

4. 训练和评估:损失函数怎么定、超参数怎么设、指标怎么读

4.1 损失函数选型:BCE 不够,Dice 来凑

变化检测的本质是一个极度不平衡的二分类任务。绝大多数遥感场景里,变化像素占比不超过 5%,有些项目甚至低于 1%。如果直接用 BCE Loss,模型很快学会“全部预测为不变”,因为这样 Loss 已经很低了。

我一般用 BCE 和 Dice 的加权和。Dice Loss 对类别不平衡不敏感,能让模型把注意力放到变化区域的结构完整性上。两部分的权重通常各取 1.0,变化区域极稀疏时可以把 Dice 权重提高到 1.2。

import torch import torch.nn.functional as F def dice_loss(pred, target, smooth=1.0): # pred 是未经过 sigmoid 的 logits pred = torch.sigmoid(pred) inter = (pred * target).sum(dim=(1, 2, 3)) union = pred.sum(dim=(1, 2, 3)) + target.sum(dim=(1, 2, 3)) dice = (2.0 * inter + smooth) / (union + smooth) return 1.0 - dice.mean() def bce_dice_loss(pred, target, dice_weight=1.0): bce = F.binary_cross_entropy_with_logits(pred, target) dice = dice_loss(pred, target) return bce + dice_weight * dice

这段代码里pred是模型直接输出的 logits,没有做 Sigmoid,F.binary_cross_entropy_with_logits内部会处理数值稳定性。Dice 计算时先做 Sigmoid 再计算交并比。smooth=1.0是为了防止公式里的分母为 0,也有一点平滑作用。

如果训练过程中发现模型对变化区域的大小极端敏感,比如大块变化区域被你检测出来了,小块变化区域几乎全丢,可以把 Dice 换成带平方项的变体。但一般先用上面这个版本就够了,换损失函数属于调参的最后手段,前面数据对齐和标签质量的影响更大。

4.2 训练循环关键代码:冻结 BatchNorm 值得单独提

共享权重双塔模型在训练时有一个容易被忽略的细节:BatchNorm 层的统计量更新。因为两期影像的亮度分布不完全相同,即使做了直方图匹配,T1 和 T2 的 BatchNorm 统计量也会被拉到两个不同的状态,这会削弱共享权重的意义。常见做法是冻结 Backbone 里所有 BatchNorm 的统计量更新,只让它们使用预训练累积的全局统计量。

def freeze_batchnorm(model): for m in model.modules(): if isinstance(m, torch.nn.modules.batchnorm._BatchNorm): m.eval() # 切换到推理模式,停止更新 running_mean/running_var

这段代码要在训练开始前调用。注意m.eval()会让 BatchNorm 使用 running 统计量而不是当前 batch 统计量,对于 batch size 偏小的训练场景尤其有效。如果你的显卡只允许 batch size 为 4 或 8,这个技巧几乎是必做的,否则 BatchNorm 在小 batch 上估计的统计量波动太大,训练过程会非常不稳定。

训练循环相对常规,下面给出一段骨架:

model = build_change_detection_model() model = model.cuda() freeze_batchnorm(model) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): model.train() # 注意:freeze_batchnorm 后仍要调 model.train(), # 但 BatchNorm 会因为之前 eval() 的覆盖而保持冻结 for t1, t2, label in train_loader: t1, t2, label = t1.cuda(), t2.cuda(), label.cuda() pred = model(t1, t2) loss = bce_dice_loss(pred, label) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()

学习率我一般从1e-4起步,用 AdamW 而不是原始 Adam,因为权重衰减对遥感小数据集防止过拟合更有效。T_max=50配合余弦退火,可以在训练后期自动降低学习率,让损失函数在极小值附近稳定下来。

4.3 评估指标不能只看 Accuracy:变化检测的及格线是 F1

变化检测领域里,全体像素 Accuracy 是一个极具欺骗性的指标。变化区域只占 1% 的情况下,模型什么都不预测,Accuracy 也有 99%。所以我一般要求项目至少汇报四个指标:Precision、Recall、F1、IoU。Precision 衡量预测的变化像素里有多少是真的,Recall 衡量真实变化像素里有多少被找出来,F1 是两者的调和平均,IoU 对边界质量更敏感。

这里给一份常用的计算代码:

def compute_metrics(pred, target, threshold=0.5): pred_bin = (pred > threshold).astype(np.uint8) target_bin = (target > 0.5).astype(np.uint8) pred_pos = pred_bin.sum() target_pos = target_bin.sum() tp = ((pred_bin == 1) & (target_bin == 1)).sum() fp = ((pred_bin == 1) & (target_bin == 0)).sum() fn = ((pred_bin == 0) & (target_bin == 1)).sum() precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0 iou = tp / (tp + fp + fn) if (tp + fp + fn) > 0 else 0.0 return {'precision': precision, 'recall': recall, 'f1': f1, 'iou': iou}

实际项目里我更关心 Recall 和 F1 的组合。如果项目的目标是“发现所有疑似违建”,Recall 优先,宁可多报一些让人工复核;如果目标是“自动出图直接入库”,Precision 要拉高,误检太多会导致成果被质疑。

训练轮数方面,遥感变化检测数据集通常不大,50 到 100 epoch 就够。我见过有人用 300 epoch 硬训小数据集,最后 F1 在训练集上接近 1.0,验证集上却不升反降,这是典型的过拟合。把训练过程可视化出来,确认验证 Loss 的最低点,然后选那个 checkpoint 而不是最后一个。

5. 避坑指南:变化检测常见的五个翻车现场

5.1 验证指标很漂亮,一换影像整片误检

现象:训练集和测试集都是从同一地区切出来的,F1 能到 0.85 以上,但把模型迁移到邻区影像上,大量绿地、水体被报成变化。

原因:两期影像之间存在光照、季节或土壤湿度差异,模型没有真正学会“建筑物结构变化”,而是学会了“两期像素值差异大就报变化”。植物枯荣造成的绿光波段变化、水体反射角度变化,都会被当成变化。

解决:数据预处理阶段强制做直方图匹配,两期影像都匹配到同一个参考影像上。另外可以把特征从 RGB 空间扩展到 NDVI 等指数特征,抑制植被变化对模型的干扰。如果项目允许,尽量在训练数据里混入不同季节的影像对,让模型见过足够多的“干扰变化”才能学会忽略它们。

5.2 显存不够调小 batch size,训练直接不收敛

现象:batch size 从 16 降到 4 后,Loss 曲线在初期下降一点后开始震荡,甚至直接发散。换用更大的显卡后同样的代码又正常了。

原因:ResNet 的 BatchNorm 层依赖 batch 内统计量估计,batch size 过小时统计量噪声太大,训练过程不稳定。共享权重双塔模型尤其明显,因为两期影像的 batch 统计差异被放大了。

解决:先冻结 Backbone 的 BatchNorm,让小 batch 只影响变化头和 ASPP 的新增层。如果仍然不稳定,把 Backbone 里的 BatchNorm 替换成 GroupNorm,GroupNorm 的统计不依赖 batch 维度,单卡 batch size 为 1 也能稳定训练。这个替换只需改一行代码,但要注意替换后预训练权重的 BN 参数就失效了,需要重新训练足够轮数。

5.3 整幅影像推理结果全是棋盘格拼接缝

现象:训练和验证都是按 patch 切出来的,单块 patch 预测效果不错,但拼回整幅影像后,patch 边界一条一条的,变化区域在边界处断成锯齿。

原因:训练时做了随机翻转和旋转增强,模型学到了不同朝向的特征,但推理时每个 patch 是独立预测的,patch 之间没有重叠区域,边界像素缺少上下文信息,预测结果在边界处跳变。

解决:推理时做带重叠的滑窗处理。patch size 保持 512,每次滑动 256 像素,让相邻 patch 有 50% 重叠,重叠区域取两次预测的平均值。这样边界处的预测会被两个 patch 的上下文共同决定,拼接缝基本消失。代价是推理时间增加约一倍,但效果提升非常明显,属于性价比最高的后处理手段。

5.4 模型预测结果全是不变

现象:训练 Loss 正常下降,但所有预测结果接近全黑,变化区域一个像素都没有。

原因:类别不平衡走到极端了。变化像素占比低于 1% 时,即使加了 Dice Loss,模型的初始预测也可能把梯度主导权让给“预测不变”的一方,处于一种损失局部最小值附近。

解决:先把 BCE 和 Dice 的权重比调成 1:2,并提高学习率到3e-4让模型跳出局部极小值。如果还不行,检查标签文件是否有问题,比如标签二值化阈值设错,全部小于 0.5 导致真值全为零。最后的手段是给变化像素加权重采样。

5.5 模型预测结果覆盖了整栋房子,但房子边缘向外扩了一圈

现象:建筑物的变化区域基本被识别出来了,但预测结果总是比真实建筑轮廓大一圈,相邻较近的房子会连成一片。

原因:ASPP 的大空洞率卷积感受野很大,会引入周围地物的上下文信息。同时训练标签里建筑边缘标注不够精细,模型学到的是“边缘附近也属于变化”的模糊印象。

解决:训练结束后使用条件随机场或多尺度融合来处理边缘。更简单的方法是推理时把变化概率图先做一次形态学腐蚀,去掉边缘外扩。形态学腐蚀的 kernel size 先设为 3,如果外扩仍然严重再扩大到 5。注意腐蚀会同时让细小的变化目标消失,所以要配合面积过滤来保护小建筑。

6. 进阶验证技巧:多尺度融合测试与变化区域矢量化输出

模型训练完成之后,我习惯做两件事来验证它是否真的能用:多尺度测试增强和变化结果矢量化。

多尺度测试增强的思路是,推理时不只使用原始分辨率,而是把影像缩小到 0.75 倍和放大到 1.25 倍,分别预测,再把多尺度的预测结果按双线性插值统一到原始尺寸后取平均。这个做法能让 ASPP 的多尺度特性真正发挥作用,对小房子和大厂房同时保持较好的召回能力。实测变化检测的 F1 通常能涨 1 到 3 个点,边缘也更稳定。代价是推理时间增加两倍,适合在出成果前做一次全面的验证评估,而不是在实时系统的每次推理里都开。

变化区域矢量化是让结果可交付的关键一步。模型输出的概率图是一张 GeoTIFF,但甲方和 GIS 部门需要的通常是 Shapefile 或 GeoJSON。我一般先做连通域分析,把小于某个面积阈值的碎块过滤掉,再对每个连通域生成轮廓多边形,最后写入 GeoJSON。如果影像有地理坐标信息,用 Rasterio 读取仿射变换参数,把像素坐标转换为经纬度或平面坐标。

这段代码值得单独贴出来:

from scipy import ndimage import numpy as np def mask_to_geojson(prob_map, transform, min_area=100, threshold=0.5): binary = (prob_map > threshold).astype(np.uint8) labeled, num_features = ndimage.label(binary) features = [] for i in range(1, num_features + 1): mask = (labeled == i) area = mask.sum() if area < min_area: continue # 提取外轮廓 filled = ndimage.binary_fill_holes(mask) contour = ndimage.find_objects(filled)[0] # contour 是切片元组,可以拿到外接矩形,进一步用 marching squares 提取精细边界 # 在遥感代码里通常直接调用 GDAL 的 Polygonize,这里用简化方式示意 coords = [] rows, cols = np.where(filled) xs = transform[2] + cols * transform[0] ys = transform[5] + rows * transform[4] for x, y in zip(xs, ys): coords.append([float(x), float(y)]) features.append({ 'type': 'Feature', 'properties': {'area_px': int(area)}, 'geometry': {'type': 'MultiPoint', 'coordinates': coords} }) return {'type': 'FeatureCollection', 'features': features}

这段代码的关键点是transform参数来自 Rasterio 的src.transform,它把像素坐标映射到投影坐标。实际项目里通常不会用 MultiPoint 来交付,而是用rasterio.features.shapes直接提取多边形,这里简化是为了展示面积过滤和坐标映射的逻辑。min_area的设置取决于影像分辨率,0.5 米分辨率下 100 像素对应约 25 平方米,太小就只剩噪声。

我早期做变化检测项目时,总觉得多尺度融合是锦上添花、矢量化是后处理杂活,把精力全放在调模型网络上。后来回过头来才发现,真正让成果被认可的不是 F1 涨了 0.5 个点,而是交付的矢量边界不碎、不糊、能直接叠到 GIS 里和现状地形图比对。从那以后我把这两个环节放到了和训练同等重要的位置。这个方案从数据预处理到矢量输出,每一步都有明确的可验证产出,希望你按这个路径跑下来,能少走我之前走过的弯路。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 6:28:53

Madeira:ARM64 macOS上的x86-64二进制翻译引擎

1. “Madeira”到底是什么&#xff1f;一个被严重误读的兼容层项目真相最近在技术社区和开发者论坛里&#xff0c;“Madeira”这个词突然高频出现&#xff0c;常和FEX-Emu、Wine、DXMT、iOS、x86-64这些词捆在一起刷屏。很多人第一反应是&#xff1a;“又一个iOS模拟器&#xf…

作者头像 李华
网站建设 2026/10/1 6:28:39

Model-Optimizer实战:工业视觉模型从220MB压缩到28MB并提速90%

去年接了个工业视觉检测的项目&#xff0c;要把训练好的缺陷分类模型从GPU服务器搬到工控机上的嵌入式GPU里。模型在服务器上跑得很欢&#xff0c;单张推理30毫秒&#xff0c;可一上嵌入式设备就原形毕露&#xff1a;显存直接溢出&#xff0c;推理延迟拉到800多毫秒&#xff0c…

作者头像 李华
网站建设 2026/10/1 6:28:22

私有化RAG知识库搭建实战:从文档清洗到检索调优的完整复盘

企业内部知识库里躺着上万份文档&#xff1a;产品手册、故障工单、历史方案、制度流程……散落在 NAS、Wiki、SharePoint 里&#xff0c;员工每天都在用搜索引擎"考古式找资料"。我花了两周从零搭起一套私有化 RAG 知识库&#xff0c;从文档接入、向量化、检索重排到…

作者头像 李华
网站建设 2026/10/1 6:27:48

微信小程序订单号复制的全链路实践:兼容、安全与体验

1. 项目概述&#xff1a;为什么一个“复制订单号”功能值得单独深挖&#xff1f;在微信小程序里点一下就复制订单号&#xff0c;看起来就是调个wx.setClipboardData的事——我刚入行那会儿也这么想。直到有次上线后凌晨两点被运营电话叫醒&#xff1a;“用户投诉订单号复制不了…

作者头像 李华
网站建设 2026/10/1 6:27:47

欧洲小众海岛马德拉:徒步levada步道+酒庄品鉴全攻略

“Madeira”这五个字母&#xff0c;第一次出现在行程单上时&#xff0c;我脑子里只有两个画面&#xff1a;一杯琥珀色的强化酒&#xff0c;一座飘在深蓝海洋上的火山岛。现实比我预想的更丰富——它们本来就是同一个地方。马德拉群岛&#xff0c;葡萄牙人嘴里“永远的春天”&am…

作者头像 李华