ARTICLE DETAIL

资讯详情

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

基于PaddlePaddle的遥感图像解译平台:从数据到部署全流程

基于PaddlePaddle的遥感图像解译平台:从数据到部署全流程 简介这份资源是「中国软件杯」A4赛题的完整项目源码包基于百度飞桨PaddlePaddle构建遥感图像解译平台面向参加软件杯赛事的高校学生、人工智能方向初学者及需要课程设计或毕业设计素材的开发者。项目涵盖前端、后端与深度学习模型推理等模块可用于快速理解遥感图像解译任务的工程落地流程也适合在现有代码基础上修改复刻、扩展功能。压缩包共130个文件以49个py源码文件和78个pyc编译文件为主另含少量md说明、txt配置与gitignore文件整体约88KB体量轻便便于本地部署与阅读。目录中可见views、predictors、settings、models及数据库迁移脚本等模块能帮助读者梳理从数据预测到服务接口的调用链路。目前已有205人学习下载适合作为赛题复现、工程实训与二次开发的参考起点。1. 遥感图像解译平台从赛题到能跑通的工程链路遥感图像解译这件事放在赛题里往往被压缩成一句话给一张卫星或航拍图输出地物分类、目标检测或变化检测的结果。但真动手做「基于百度 PaddlePaddle 的遥感图像解译平台」你会发现难点不在模型本身而在数据怎么组织、推理怎么封装、前后端怎么串起来。中国软件杯这类赛题通常要求提交一个可运行的系统而不是一个 notebook所以「平台」两个字才是核心——它意味着你要把 PaddlePaddle 的推理能力包成服务再配一个能上传图片、展示解译结果的前端。这篇文章面向准备做这个赛题、或者想搭一套遥感解译 demo 的工程师从数据准备讲到服务封装和踩坑每一步都给出可复现的命令和参数。2. 遥感数据怎么进 PaddlePaddle从原始影像到可训练张量遥感图像和普通图像最大的区别在于尺寸和通道。一张高分卫星图动辄几千乘几千像素直接 resize 到 224 会丢掉小目标通道上又常见 RGB、多光谱甚至 SAR 的单通道浮点数据。PaddlePaddle 的paddle.io.Dataset和paddle.vision.transforms能覆盖大部分需求但滑窗切图和归一化这两步必须自己写清楚。2.1 滑窗切图与标签对齐的脚本写法遥感解译里最常见的做法是先切图再训练。切图不是简单裁剪要保证训练集和验证集之间没有重叠区域否则验证指标会虚高。下面这个脚本按固定步长切图同时把标签图做同样切分。import os import numpy as np from PIL import Image def slide_crop(img_path, label_path, out_dir, crop_size512, stride256): 对遥感影像和标签做同步滑窗切图。 crop_size: 切图边长常见 512 或 1024 stride: 滑动步长一般取 crop_size 的一半保证边缘目标不被截断 img np.array(Image.open(img_path)) label np.array(Image.open(label_path)) h, w img.shape[:2] idx 0 for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): img_crop img[y:ycrop_size, x:xcrop_size] label_crop label[y:ycrop_size, x:xcrop_size] # 过滤掉标签全为背景的图块减少无效样本 if label_crop.max() 0: continue Image.fromarray(img_crop).save( os.path.join(out_dir, img, f{idx:06d}.png)) Image.fromarray(label_crop).save( os.path.join(out_dir, label, f{idx:06d}.png)) idx 1 return idx逻辑说明crop_size决定单次推理的感受野512 在 8GB 显存上比较稳stride取一半是为了让相邻图块有重叠推理时再拼接回去避免边界出现拼接缝。过滤全背景图块这一步很关键遥感数据里背景占比经常超过 70%不过滤会导致正负样本严重失衡。参数上如果你做的是建筑物提取crop_size可以降到 256因为建筑物尺度小做水体或林地分类512 甚至 1024 更合适。2.2 多光谱通道的归一化与 Dataset 封装多光谱数据不能直接套 ImageNet 的均值和方差。常见做法是按波段统计均值和标准差或者简单除以 255 再减 0.5。下面用paddle.io.Dataset封装一个可迭代的数据集。import paddle from paddle.io import Dataset import cv2 import numpy as np class RemoteSensingDataset(Dataset): def __init__(self, img_dir, label_dir, transformNone): self.img_list sorted(os.listdir(img_dir)) self.img_dir img_dir self.label_dir label_dir self.transform transform def __getitem__(self, idx): name self.img_list[idx] img cv2.imread(os.path.join(self.img_dir, name), cv2.IMREAD_UNCHANGED) label cv2.imread(os.path.join(self.label_dir, name), cv2.IMREAD_GRAYSCALE) # 多光谱归一化按 16bit 转 8bit 再归一化避免数值溢出 if img.dtype np.uint16: img (img / 256).astype(np.uint8) img img.astype(float32) / 255.0 img (img - 0.5) / 0.5 img img.transpose(2, 0, 1) # HWC - CHW label label.astype(int64) return paddle.to_tensor(img), paddle.to_tensor(label) def __len__(self): return len(self.img_list)逻辑说明IMREAD_UNCHANGED保证 16bit 影像不被截断除以 256 再归一化是遥感里常用的降位深做法比直接除以 65535 更稳定。transpose把通道提前因为 PaddlePaddle 的卷积默认输入是 NCHW。标签用int64是因为交叉熵损失要求类别索引为整型。如果你的数据是单通道 SAR把transpose那行去掉并在模型第一层把in_channels改成 1。3. 用 PaddlePaddle 搭解译模型选型、训练与推理导出遥感解译任务通常分三类语义分割、目标检测、变化检测。赛题里最常见的是语义分割因为标注成本相对低评价指标也直观。PaddlePaddle 生态里可以直接用 PaddleSeg 或 PaddleDetection但赛题往往要求你体现「自己搭」的过程所以这里给一个轻量 U-Net 的实现和训练循环。3.1 轻量 U-Net 的 PaddlePaddle 实现import paddle import paddle.nn as nn class DoubleConv(nn.Layer): 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(), nn.Conv2D(out_ch, out_ch, 3, padding1), nn.BatchNorm2D(out_ch), nn.ReLU() ) def forward(self, x): return self.conv(x) class UNet(nn.Layer): def __init__(self, in_ch3, num_classes2): super().__init__() self.down1 DoubleConv(in_ch, 64) self.down2 DoubleConv(64, 128) self.pool nn.MaxPool2D(2) self.up1 nn.Conv2DTranspose(128, 64, 2, stride2) self.up_conv1 DoubleConv(128, 64) self.out nn.Conv2D(64, num_classes, 1) def forward(self, x): d1 self.down1(x) d2 self.down2(self.pool(d1)) u1 self.up1(d2) u1 paddle.concat([u1, d1], axis1) u1 self.up_conv1(u1) return self.out(u1)逻辑说明这是一个两层下采样的简化 U-Net适合 512 尺寸输入和 2 到 5 类地物。DoubleConv里用 BatchNorm 加速收敛遥感数据批次小的时候可以换成 GroupNorm。Conv2DTranspose做上采样拼接时注意通道对齐。如果你的显存够把通道数翻倍到 64/128/256 效果会更好但训练时间也翻倍。3.2 训练循环与学习率调度model UNet(in_ch3, num_classes2) optimizer paddle.optimizer.Adam(learning_rate1e-3, parametersmodel.parameters()) scheduler paddle.optimizer.lr.CosineAnnealingDecay(learning_rate1e-3, T_max50) loss_fn nn.CrossEntropyLoss(ignore_index255) for epoch in range(50): model.train() for img, label in train_loader: pred model(img) loss loss_fn(pred, label) loss.backward() optimizer.step() optimizer.clear_grad() scheduler.step() print(fepoch {epoch}, loss {loss.numpy()[0]:.4f})逻辑说明ignore_index255用来忽略标注里的无效区域遥感标签经常有未标注像素。CosineAnnealingDecay的T_max设成总 epoch 数让学习率平滑降到接近零。如果 loss 在前几个 epoch 不降先检查标签是不是从 0 开始连续编码很多翻车现场都是标签从 1 开始导致 CrossEntropy 越界。3.3 推理导出与动态图转静态图赛题提交通常要求能脱离训练环境跑推理所以要把动态图模型导出成静态图。model.eval() input_spec paddle.static.InputSpec(shape[None, 3, 512, 512], dtypefloat32) paddle.jit.save(model, unet_infer, input_spec[input_spec])逻辑说明InputSpec的 batch 维度写None这样导出后的模型支持任意 batch。paddle.jit.save会生成unet_infer.pdmodel和unet_infer.pdiparams两个文件部署时用paddle.jit.load加载。注意导出前必须调model.eval()否则 BatchNorm 会带着训练时的统计量推理结果会飘。4. 把模型包成解译平台服务端与前端的最小闭环「平台」意味着别人能通过界面上传图片、看到解译结果。最小闭环是 Flask 或 FastAPI 做后端前端一个 HTML 页面加 canvas 展示。这里用 FastAPI因为异步和文件上传写起来更干净。4.1 FastAPI 推理接口与图像编码from fastapi import FastAPI, File, UploadFile from fastapi.responses import JSONResponse import paddle import numpy as np import cv2 import base64 app FastAPI() model paddle.jit.load(unet_infer) model.eval() app.post(/predict) async def predict(file: UploadFile File(...)): contents await file.read() img cv2.imdecode(np.frombuffer(contents, np.uint8), cv2.IMREAD_COLOR) img cv2.resize(img, (512, 512)) img img.astype(float32) / 255.0 img (img - 0.5) / 0.5 img img.transpose(2, 0, 1)[None, ...] with paddle.no_grad(): pred model(paddle.to_tensor(img)) mask paddle.argmax(pred, axis1).numpy()[0] # 把 mask 叠加成彩色图返回 color_mask np.zeros((512, 512, 3), dtypenp.uint8) color_mask[mask 1] [0, 255, 0] _, buffer cv2.imencode(.png, color_mask) b64 base64.b64encode(buffer).decode() return JSONResponse({mask: b64})逻辑说明cv2.imdecode直接从字节流解码省去落盘。paddle.no_grad()关闭梯度推理速度能快 20% 左右。返回 base64 是为了前端直接塞进img标签的src不用再起静态文件服务。参数上resize到 512 是跟训练对齐如果训练用了滑窗这里也要改成滑窗推理再拼接。4.2 前端上传与结果叠加!DOCTYPE html html body input typefile idupload acceptimage/* canvas idcanvas width512 height512/canvas script document.getElementById(upload).onchange async (e) { const file e.target.files[0]; const form new FormData(); form.append(file, file); const res await fetch(/predict, {method: POST, body: form}); const data await res.json(); const img new Image(); img.onload () { const ctx document.getElementById(canvas).getContext(2d); ctx.drawImage(img, 0, 0); }; img.src data:image/png;base64, data.mask; }; /script /body /html逻辑说明前端只做两件事——上传和展示。FormData直接传文件后端返回的 base64 塞进Image对象再画到 canvas。如果你要叠加在原图上可以再画一层半透明 mask用globalAlpha控制透明度。这个闭环跑通之后赛题要求的「平台」基本就成立了剩下的就是加登录、历史记录这些外围功能。5. 避坑与排查遥感解译平台最常见的五个翻车点5.1 推理结果全黑或全白现象上传图片后返回的 mask 要么全黑要么全白没有任何地物轮廓。原因通常是归一化不一致——训练时用了(img - 0.5) / 0.5推理时只做了/ 255。解决把训练和推理的预处理写成一个函数两边调用同一个实现别复制粘贴。5.2 显存溢出但 batch size 已经设为 1现象crop_size1024时单张图就爆显存。原因是 U-Net 在浅层特征图尺寸大BatchNorm 和中间激活占显存。解决把crop_size降到 512或者把模型通道数减半再或者用paddle.amp开混合精度。混合精度在 PaddlePaddle 里一行scaler paddle.amp.GradScaler()就能开显存能省 30% 到 40%。5.3 验证集指标高但实际效果差现象验证集 mIoU 到 0.85但拿真实卫星图跑出来一塌糊涂。原因是切图时训练集和验证集有重叠区域模型记住了验证集。解决按地理区域划分而不是随机划分图块。如果数据没有地理信息至少保证验证集的图块在训练集里找不到重叠。5.4 多光谱影像读取后通道数不对现象cv2.imread读 4 波段影像返回 3 通道或者读 16bit 影像返回 8bit。原因是 OpenCV 默认做色彩空间转换。解决用cv2.IMREAD_UNCHANGED读进来后手动取需要的波段。如果波段数超过 4OpenCV 支持有限改用rasterio或tifffile读。5.5 静态图导出后推理结果和动态图不一致现象动态图验证正常paddle.jit.save之后推理结果全乱。原因通常是导出时没设eval()或者InputSpec的 shape 和实际输入不匹配。解决导出前打印一次model.training确认是 FalseInputSpec的 H/W 必须和推理时一致不一致的话在推理前 resize。6. 进阶技巧用滑窗拼接和 TTA 把解译精度再拉一截滑窗推理是遥感解译里性价比最高的技巧。训练时切了图推理时如果直接 resize 整图小目标会丢正确做法是按训练时的crop_size和stride滑窗推理再把每个窗口的预测拼回原图。拼接时重叠区域取平均或投票能消掉窗口边界的突变。def slide_inference(model, img, crop_size512, stride256, num_classes2): 滑窗推理并拼接重叠区域用概率平均。 img: HWC float32 已归一化 h, w, c img.shape prob_map np.zeros((num_classes, h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): crop img[y:ycrop_size, x:xcrop_size] crop crop.transpose(2, 0, 1)[None, ...] with paddle.no_grad(): pred model(paddle.to_tensor(crop)) prob paddle.nn.functional.softmax(pred, axis1).numpy()[0] prob_map[:, y:ycrop_size, x:xcrop_size] prob count_map[y:ycrop_size, x:xcrop_size] 1 count_map[count_map 0] 1 prob_map / count_map[None, ...] return np.argmax(prob_map, axis0)逻辑说明prob_map累加每个窗口的 softmax 概率count_map记录每个像素被覆盖的次数最后取平均。这样重叠区域的预测更平滑。stride越小重叠越多精度越高但推理越慢一般取crop_size的一半是精度和速度的平衡点。TTA测试时增强是另一个能稳定涨点的技巧。对同一张图做水平翻转、垂直翻转、旋转 90 度分别推理后再把结果翻转回来取平均。遥感图像地物通常具有旋转不变性TTA 一般能涨 1 到 3 个点。代价是推理时间翻四倍赛题如果有时限要求建议只做水平翻转这一种。我自己的习惯是训练完先跑一遍滑窗推理看拼接缝如果缝明显就把stride再调小然后加水平翻转 TTA 对比 mIoU涨点不到 0.5 就不加因为部署复杂度上去了。这套流程在几次遥感解译任务里都稳希望帮到你。本文还有配套的精品资源点击获取
返回列表