ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

本地模型选型:语义分割与实例分割在建筑足迹提取中的实战对比

2026/8/28 1:34:31 拓冰建站 浏览量
本地模型选型:语义分割与实例分割在建筑足迹提取中的实战对比 关于“Local models head to head for extraction”这个主题最近在遥感、GIS 和文档智能领域讨论得很多。尤其是 building footprint extraction建筑足迹提取成为热词之后越来越多团队开始纠结一个问题同样跑在本地机器上做提取任务时到底应该选语义分割模型还是实例分割模型如果引入本地部署的视觉大模型又是否值得本文会围绕这个“选型对比”的核心问题用一套可复现的实战流程完整对比两条主流技术路线并给出适合不同场景的落地方案。1. 本地模型与 extraction先搞清楚要比什么1.1 本地模型的双重含义“Local models”在不同语境下有两层含义。第一层是指部署在本地服务器或工作站上的深度学习模型不依赖云端 API数据不出内网适合遥感影像、医疗影像、涉密文档等对数据安全要求较高的场景。第二层是指本地运行的预训练模型权重比如从 Hugging Face 或 Ultralytics 下载到本地的模型文件推理时完全离线。这两层含义在建筑足迹提取场景中是叠加的既要模型文件在本地也要训练和推理都在本地完成。为什么大家开始关注本地模型因为遥感影像和地理信息数据的敏感性比较高很多项目不允许把影像送到公有云识别接口。另一方面建筑足迹提取任务本身需要频繁迭代样本、调整后处理逻辑如果每次都要走远程 API调试成本会很高。把模型放在本地意味着你可以随时改参数、重新训练、批量推理整个过程可控性更强。1.2 extraction 任务到底在提取什么“Extraction”这个词在算法领域非常宽泛但在 building footprint extraction 这个热词里它特指从遥感影像或航拍影像中提取建筑物轮廓得到每个建筑的边界。输出通常有两种形式一种是栅格形式的二值掩码也就是每个像素标记为“建筑”或“背景”另一种是矢量多边形也就是一个个闭合的轮廓坐标点方便直接进入 GIS 系统做面积统计、城市规划、变化检测等业务。除了建筑足迹提取任务还可以是文档信息抽取、道路中心线提取、水体提取、农田地块提取等等。这些任务底层都依赖“像素分类”或“目标轮廓回归”能力所以本文虽然以建筑足迹提取为主线但对比结论对其他提取任务同样有参考价值。1.3 为什么 building footprint extraction 是合适的对比场景建筑足迹提取非常适合用来做本地模型的“head to head”评测。原因是这个任务对模型能力的要求比较综合既需要模型理解建筑的纹理、阴影、屋顶颜色等视觉特征还需要模型输出相对精确的边界同时建筑目标的大小差异很大既有几百像素的大厂区也有几个像素的乡村小屋。这样的任务复杂度足以暴露出不同模型家族在提取能力上的真实差距。对比时我们最关心三个问题边界完整度怎么样、小目标召回率怎么样、训练和推理成本高不高。下面我会用两个真实可跑的本地模型方案来回答这三个问题。2. 建筑足迹提取的主流技术路线2.1 路线一语义分割以 U-Net 为代表语义分割的思路是把影像上的每一个像素都分类。建筑足迹提取中一般就是二分类前景是建筑背景是其他。U-Net 是这类任务最经典的结构它通过编码器逐层下采样提取特征再通过解码器逐步恢复分辨率并利用跳跃连接把浅层细节和深层语义融合起来。语义分割的优势是边界相对细腻因为模型在像素级别做预测只要数据集标注足够精细预测出来的建筑边缘会比较贴合真实轮廓。缺点是它天然不区分“这栋建筑”和“那栋建筑”如果两栋建筑紧挨着模型很可能把它们连成一个整体。后处理阶段通常需要借助连通域分析对掩码做实例拆分。2.2 路线二实例分割以 YOLOv8-seg 为代表实例分割要解决的正是“区分不同个体”的问题。以 YOLOv8-seg 为例它先检测出每一个建筑目标的位置再在每个检测框内预测对应的分割掩码。这种方式输出天然带实例编号每个建筑都是独立对象非常适合需要矢量化和逐栋统计的业务。实例分割的挑战在于对小目标和密集排列目标的处理。小建筑在检测阶段如果不被召回后面就不可能有掩码密集建筑如果互相遮挡严重检测框之间会产生大量重叠影响掩码质量。不过在工程落地中实例分割的后处理比语义分割简单很多这也是它越来越受欢迎的原因。2.3 两条路线的选择逻辑选择哪条路线本质上取决于业务输出要求。如果最终成果是一张二值分类图用于整体覆盖率统计语义分割足够如果最终成果要进入 GIS 系统做逐栋属性关联、面积计算、矢量叠加实例分割更合适。也有不少项目采用“分割 后处理拆分”的混合方案但流程会长一些。下面两章会分别给出两条路线的完整实战示例从数据准备到训练推理全部在本地完成。3. 环境准备与数据组织3.1 本地开发环境本文的示例代码基于 Python 和 PyTorch 编写YOLOv8-seg 部分使用 Ultralytics 框架。具体版本需要根据你的项目实际情况调整示例以常见环境为例重点演示配置思路。简单来说需要准备以下内容操作系统Windows 10/11 或 Ubuntu 20.04/22.04 均可显卡建议 NVIDIA GPU显存 8GB 以上用于训练和推理加速Python3.8 以上版本深度学习框架PyTorch 2.xCUDA 版根据显卡驱动安装图像处理库OpenCV、Pillow、NumPy模型框架Ultralytics安装后即可使用 YOLOv8安装核心依赖的命令如下pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python pillow numpy如果你的机器没有 GPU也可以把训练设备改成 CPU但训练时间会明显变长建议先用小尺寸影像做验证。3.2 数据集组织方式为了让两条路线都能跑起来我们需要把数据整理成两种格式分割模型需要“影像 掩码”的配对目录YOLO 模型需要 COCO 或 YOLO 格式的标注文件。这里我们以经典的开源遥感建筑数据集为例比如 Massachusetts Buildings Dataset 或 Inria Aerial Image Labeling Dataset它们的原始数据一般包含原始影像和对应的建筑掩码。先看分割模型需要的数据结构datasets/building_seg/ ├── images/ │ ├── train/ │ │ ├── a0001.png │ │ ├── a0002.png │ │ └── ... │ └── val/ │ ├── b0001.png │ └── ... └── masks/ ├── train/ │ ├── a0001.png │ ├── a0002.png │ └── ... └── val/ ├── b0001.png └── ...YOLO 实例分割模型需要的是标签文件夹每张影像对应一个同名 txt 文件每行表示一个建筑的轮廓坐标格式为class_id x1 y1 x2 y2 ... xn yn其中坐标是相对于影像宽高的归一化值。从掩码生成这种轮廓标注可以用 OpenCV 的 findContours 实现后面会给出转换脚本思路。3.3 项目目录结构建议整个项目文件组织如下方便管理不同路线的代码和权重local-extraction-comparison/ ├── datasets/ │ ├── building_seg/ │ └── building_yolo/ ├── models/ │ └── unet.py ├── scripts/ │ ├── train_unet.py │ ├── train_yolo.py │ ├── convert_mask_to_yolo.py │ └── evaluate.py ├── runs/ │ ├── unet/ │ └── yolo/ └── requirements.txt4. 实战U-Net 分割式提取方案4.1 U-Net 模型结构这一节给出一个轻量 U-Net 的完整实现。为了控制参数量我们把编码器通道数从常见的 64 起步仍然保留四层下采样和四层上采样。这种结构在建筑足迹提取任务上已经有大量验证属于最稳妥的起步方案。# 文件路径models/unet.py import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, num_classes1): super().__init__() self.enc1 DoubleConv(in_channels, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.enc4 DoubleConv(256, 512) self.pool nn.MaxPool2d(2) self.bottleneck DoubleConv(512, 1024) self.up4 nn.ConvTranspose2d(1024, 512, 2, stride2) self.dec4 DoubleConv(1024, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(128, 64) self.out nn.Conv2d(64, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(e4)) d4 self.dec4(torch.cat([self.up4(b), e4], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return torch.sigmoid(self.out(d1))这份代码可以直接复制到models/unet.py中使用。需要注意torch.sigmoid放在最后一层说明我们把它当作单通道二值分割任务来训练。如果后续要扩展到多类别提取任务只需要修改num_classes并把输出层后的 sigmoid 换成 softmax 逻辑。4.2 数据集读取为了训练 U-Net我们需要自定义一个 Dataset 类负责读取影像和掩码。这里的掩码读取后会自动二值化把大于 0.5 的像素视为建筑区域。为了统一输入尺寸这里直接使用 PIL 的 resize 实现简单缩放实际项目中更推荐使用随机裁剪增强避免小尺寸缩放导致建筑细节丢失。# 文件路径scripts/dataset.py import os import numpy as np from PIL import Image from torch.utils.data import Dataset import torch class BuildingSegDataset(Dataset): def __init__(self, image_dir, mask_dir, image_size(512, 512)): self.image_list sorted(os.listdir(image_dir)) self.image_dir image_dir self.mask_dir mask_dir self.image_size image_size def __len__(self): return len(self.image_list) def __getitem__(self, idx): img_name self.image_list[idx] img_path os.path.join(self.image_dir, img_name) mask_path os.path.join(self.mask_dir, img_name) image Image.open(img_path).convert(RGB) mask Image.open(mask_path).convert(L) image image.resize(self.image_size) mask mask.resize(self.image_size) image np.array(image, dtypenp.float32) / 255.0 mask np.array(mask, dtypenp.float32) / 255.0 mask (mask 0.5).astype(np.float32) image image.transpose(2, 0, 1) mask mask[np.newaxis, ...] return torch.tensor(image), torch.tensor(mask)这里有一点需要特别注意影像与掩码的文件名必须一一对应。很多开源数据集的掩码后缀可能与影像不同比如影像叫a0001.jpg掩码叫a0001_mask.png此时需要修改文件名对齐逻辑通常可以统一按影像名拼接掩码路径。4.3 训练脚本训练时我们把二值交叉熵损失和 Dice 损失结合起来。Dice 损失在分割任务里能有效缓解正负样本不均衡的问题因为建筑区域在整张影像中往往只占一小部分单纯使用 BCE 容易让模型倾向于把所有像素预测为背景。# 文件路径scripts/train_unet.py import torch import torch.nn as nn from torch.utils.data import DataLoader from dataset import BuildingSegDataset from models.unet import UNet def dice_loss(pred, target, smooth1e-6): pred pred.reshape(pred.size(0), -1) target target.reshape(target.size(0), -1) intersection (pred * target).sum(dim1) return 1 - (2.0 * intersection smooth) / (pred.sum(dim1) target.sum(dim1) smooth) device torch.device(cuda if torch.cuda.is_available() else cpu) train_dataset BuildingSegDataset( image_dirdatasets/building_seg/images/train, mask_dirdatasets/building_seg/masks/train ) val_dataset BuildingSegDataset( image_dirdatasets/building_seg/images/val, mask_dirdatasets/building_seg/masks/val ) train_loader DataLoader(train_dataset, batch_size8, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size8, shuffleFalse, num_workers4) model UNet(in_channels3, num_classes1).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-4) criterion nn.BCEWithLogitsLoss() for epoch in range(50): model.train() total_loss 0.0 for images, masks in train_loader: images images.to(device) masks masks.to(device) outputs model(images) loss criterion(outputs, masks) dice_loss(outputs, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() avg_loss total_loss / len(train_loader) print(fEpoch {epoch 1}/50, Train Loss: {avg_loss:.4f}) if (epoch 1) % 10 0: torch.save(model.state_dict(), fruns/unet/unet_epoch_{epoch 1}.pth)这段训练脚本比较精简适合作为起步版本。每 10 个 epoch 保存一次权重方便在训练中途停下来观察效果。如果你希望训练更稳定可以在此基础上加入学习率衰减、早停和验证集指标监控。4.4 推理与保存掩码训练完成后推理脚本只需要加载权重对输入影像做一次前向传播然后对概率图设置阈值即可得到二值掩码。# 文件路径scripts/predict_unet.py import torch import numpy as np from PIL import Image from models.unet import UNet device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(in_channels3, num_classes1).to(device) model.load_state_dict(torch.load(runs/unet/unet_epoch_50.pth, map_locationdevice)) model.eval() image Image.open(test_image.png).convert(RGB) image_resized image.resize((512, 512)) input_tensor torch.tensor(np.array(image_resized, dtypenp.float32) / 255.0).permute(2, 0, 1).unsqueeze(0).to(device) with torch.no_grad(): prob model(input_tensor).squeeze().cpu().numpy() mask (prob 0.5).astype(np.uint8) * 255 result Image.fromarray(mask) result result.resize(image.size) result.save(test_image_mask.png)这里最后把 512×512 的掩码恢复到了原始影像尺寸方便直接叠图查看效果。如果需要输出矢量多边形下一步可以使用 OpenCV 的 findContours 把掩码转成 GeoJSON 或 Shapefile。5. 实战YOLOv8-seg 实例提取方案5.1 安装 Ultralytics 并准备模型YOLOv8-seg 的接入成本非常低Ultralytics 把训练、验证、预测、导出全流程都封装成了 API。安装好依赖后我们选择yolov8n-seg.pt作为初始权重。这是 YOLOv8 系列中最轻量的分割权重适合在本地 GPU 上快速迭代。pip install ultralytics如果网络下载模型权重较慢也可以手动下载后放到项目目录再用本地路径加载。加载方式如下from ultralytics import YOLO model YOLO(yolov8n-seg.pt)5.2 将掩码标注转换为 YOLO 格式YOLO-seg 训练需要多边形标注而不是像素掩码。如果你的数据集只有掩码可以用 OpenCV 提取每个建筑轮廓然后归一化写入 txt。# 文件路径scripts/convert_mask_to_yolo.py import cv2 import numpy as np import os def mask_to_yolo(mask_path, txt_path, img_width, img_height, class_id0): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) lines [] for contour in contours: area cv2.contourArea(contour) if area 50: continue contour contour.squeeze(axis1).astype(np.float32) contour[:, 0] / img_width contour[:, 1] / img_height coordinates contour.flatten().tolist() line f{class_id} .join([f{coord:.6f} for coord in coordinates]) lines.append(line) with open(txt_path, w) as f: f.write(\n.join(lines))这里需要注意过滤掉面积过小的轮廓可以避免把噪声点也当成建筑。转换之后数据目录结构要调整为 YOLO 要求的布局datasets/building_yolo/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/5.3 配置数据文件并训练YOLO 训练需要一份 YAML 配置文件指明数据路径和类别名称。建筑提取只有建筑一个类别所以类别编号为 0。# 文件路径building.yaml path: ./datasets/building_yolo train: images/train val: images/val names: 0: building训练命令很简洁# 文件路径scripts/train_yolo.py from ultralytics import YOLO model YOLO(yolov8n-seg.pt) model.train( databuilding.yaml, epochs100, imgsz1024, batch8, device0, projectruns/yolo, namebuilding_yolov8n )训练结束后最佳权重通常保存在runs/yolo/building_yolov8n/weights/best.pt。如果数据量不大建议开启patience参数设置早停避免训练后期过拟合浪费时间。5.4 推理与结果导出YOLO 推理可以直接得到每个建筑的掩码和对应的置信度。如果需要把结果导出为 GeoJSON可以在得到掩码后调用 findContours 和多边形简化算法。# 文件路径scripts/predict_yolo.py from ultralytics import YOLO model YOLO(runs/yolo/building_yolov8n/weights/best.pt) results model.predict( sourcetest_image.png, imgsz1024, conf0.25, saveTrue, save_txtTrue, save_confTrue ) for result in results: if result.masks is not None: print(f检测到 {len(result.masks)} 个建筑实例)这里save_txtTrue会把每个目标的归一化轮廓坐标保存为 txtsave_confTrue会附带置信度。拿到这些轮廓后乘以影像宽高即为像素坐标再配合地理参考信息就能完成矢量化。6. 两个本地模型的 head to head 对比6.1 评估指标怎么设对比两个模型不能只凭肉眼看几张效果图需要设置统一的量化指标。分割类任务最常用的是 IoUIntersection over Union它计算预测掩码与真实掩码的交并比。实例分割任务除了整体 IoU还要关注每个实例的召回率因为建筑数量统计错误会直接影响业务结论。计算单张影像 IoU 的核心逻辑如下# 文件路径scripts/evaluate.py import numpy as np def compute_iou(pred_mask, gt_mask): pred pred_mask 0.5 gt gt_mask 0.5 intersection np.logical_and(pred, gt).sum() union np.logical_or(pred, gt).sum() return intersection / (union 1e-6) def compute_recall(pred_mask, gt_mask): pred pred_mask 0.5 gt gt_mask 0.5 true_positive np.logical_and(pred, gt).sum() actual_positive gt.sum() return true_positive / (actual_positive 1e-6)在对比过程中建议对三类典型影像分别评估城区密集建筑、郊区零散建筑、带有阴影和树木遮挡的建筑。这样才能全面反映模型在小目标、粘连目标和复杂背景下的表现。6.2 多维度对比结果从技术特点来看两条路线的差距主要体现在以下维度对比维度U-Net 分割方案YOLOv8-seg 方案输出形态逐像素二值掩码每个建筑独立掩码实例区分能力较弱需要连通域后处理天然支持实例区分小目标召回依赖损失函数和图像分辨率依赖检测分支的锚框设计边界精细度较高尤其是规则建筑受检测框约束细长建筑可能丢失标注成本只需要掩码需要多边形标注推理速度取决于输入分辨率相对更快适合大图切块GIS 对接需要额外矢量化和拆分掩码可直接转矢量并关联属性这里需要强调实际效果会随着数据分布变化而变化不建议直接照搬结论。如果你的数据以密集城中村为主U-Net 加后处理拆分不一定比 YOLOv8-seg 差如果你的数据以独立别墅为主YOLOv8-seg 的体验会明显更好。6.3 第三条路线本地视觉大模型现在提到本地模型还不能忽视本地部署的视觉语言模型比如基于 Qwen-VL 或 LLaVA 这类开源权重构建的提取方案。这类模型可以直接用自然语言提问“请帮我提取这张遥感影像中的所有建筑轮廓”输出 JSON 或其他结构化的坐标信息。不过从工程实践经验看视觉语言模型在 building footprint extraction 这种高精度任务上还很难替代专门的像素级模型。它们输出坐标的精度往往不够而且推理耗时高、显存占用大更适合做后处理校验、属性信息抽取和交互式标注辅助。我把它们列为第三条路线但并不建议作为生产环境唯一依赖。7. 常见问题与排查思路本地模型训练和推理过程中最容易碰到的问题其实集中在数据、环境和后处理三个方面。下面整理了一份排查清单遇到问题可以按表快速定位。问题现象常见原因解决思路训练 Loss 不下降学习率设置过大或过小调整初始学习率观察前几个 epoch 的 Loss 变化预测掩码全是背景训练样本类别不平衡引入 Dice 损失或 Focal Loss增加建筑样本裁剪YOLO 检测不到建筑标注文件为空或坐标越界检查 txt 文件格式确认坐标做了归一化建筑边界过粗掩码标注本身不精确检查训练数据标注质量统一标注规范大影像推理内存不足输入尺寸过大使用滑窗切块推理或降低推理分辨率模型大小需要裁剪业务要求更轻量使用 ONNX 导出并量化观察精度损失另一个常见问题是切块推理导致建筑被切断。如果影像尺寸很大我们通常把它切成 512×512 或 1024×1024 的小块分别推理但建筑被切到边界时模型只能看到半个房子输出掩码不完整。建议切块时设置重叠率比如重叠 100 像素推理后再根据位置拼接并去重。8. 工程落地与扩展建议8.1 数据标注规范是第一优先级建筑足迹提取的效果上限基本由数据决定。标注时应该约定建筑的最小面积阈值、边界贴合标准、遮挡情况下如何处理、临时建筑是否计入等规则。如果标注标准不统一模型在边界区域的预测会非常不稳定后处理阶段也无法通过规则修复。8.2 推理阶段的性能优化生产环境中推理速度往往比训练时的指标更重要。模型导出为 ONNX 或 TensorRT 后推理速度能提升数倍。导出时要注意输入尺寸固定尽量减少动态维度带来的额外开销。如果对显存占用敏感还可以用半精度推理在精度影响很小的情况下显著降低显存占用。8.3 从建筑足迹扩展到更多提取场景掌握了 building footprint extraction 的整个流程之后你会发现这套方案几乎可以复用到所有二值提取任务。道路提取、水体提取、耕地地块提取只需要更换数据集并在输出层保持二分类文档版面分析、表格结构提取则可以在分割或检测基础上增加类别数。核心能力是数据组织、模型训练、后处理和评估闭环这套能力在不同域之间是可迁移的。最后想说的是本地模型选型没有绝对的“最好”只有“更合适”。启动一个新项目时先理清输出需求和数据条件再用本文的对比方法跑一轮小实验比直接套用别人的模型配置更可靠。如果你正在做 building footprint extraction 或类似的提取任务可以把本文的代码作为基线逐步优化后沉淀成自己的提取工具箱。