1. 从零开始理解变化检测与LEVIR-CD数据集如果你手头有两张不同时间拍摄的同一区域的卫星图让你找出这期间哪些地方新建了房子或者哪些老建筑被拆除了你会怎么做一张张像素去对比那工作量简直不敢想。这就是变化检测技术要解决的难题它就像一位不知疲倦的“找茬”大师能自动、精准地识别出地表的变化区域。今天我要跟你分享的就是如何用经典的UNet网络在LEVIR-CD这个专门针对建筑物变化的遥感数据集上从数据准备、模型训练一直到模型优化部署ONNX的完整实战流程。整个过程我会尽量用大白话讲清楚哪怕你刚接触深度学习也能跟着一步步做出来。咱们先聊聊这个项目的核心——LEVIR-CD数据集。它可不是随便找的图片而是一套非常专业的遥感变化检测基准数据集。简单说它包含了637对谷歌地球的高清卫星图像块每一对都是同一地点在不同年份时间跨度5到14年拍摄的“前后对比照”分辨率高达0.5米每像素尺寸是1024x1024。这些变化主要集中在建筑物上比如空地上盖起了别墅老仓库被拆除变成了绿地等等。数据集已经由专家人工标注好了变化的地方标记为1没变化的地方标记为0直接给我们省去了最头疼的标注工作。你可以把它想象成一个已经标注好答案的“找茬”图库我们的任务就是训练一个AI模型让它学会这个找茬的规则。为什么选择UNet呢在图像分割领域UNet就像一位老将结构清晰、效果稳定。它的结构像一个“U”形先下采样提取图像深层特征再上采样逐步恢复细节特别适合做像素级的预测。对于变化检测任务我们通常采用“孪生网络”结构的UNet也就是用两个共享权重的编码器可以理解为两个一模一样的特征提取器分别处理前后两期图像然后在某个阶段把两者的特征融合起来最后解码输出变化图。这种设计能让模型更好地比较两幅图像的差异。likyoo大神开源的change_detection.pytorch库就提供了这种现成的架构我们站在巨人的肩膀上能省不少力气。2. 实战第一步搭建你的开发环境工欲善其事必先利其器。咱们先把跑代码的环境给搭起来这里我会把每一步都掰开揉碎了讲确保你一次成功。首先我强烈建议使用Anaconda来管理Python环境它能帮你把不同项目所需的库隔离开避免版本冲突。安装好Anaconda后打开你的命令行Windows用Anaconda PromptMac/Linux用终端我们来创建一个专属的Python环境。# 创建一个名为change_det的新环境指定Python版本为3.8 conda create -n change_det python3.8 # 激活这个环境 conda activate change_det接下来是安装PyTorch这是深度学习的核心框架。安装时一定要注意版本匹配尤其是CUDA版本如果你有NVIDIA显卡的话。我这次实战用的是torch1.12.0对应CUDA 11.3。你可以根据自己显卡的驱动去PyTorch官网找对应的命令。这里给出我当时用的两种安装方式选一个就行# 方式一使用conda安装有时下载慢 conda install pytorch1.12.0 torchvision0.13.0 torchaudio0.12.0 cudatoolkit11.3 -c pytorch # 方式二使用pip安装国内建议换源加速 pip install torch1.12.0cu113 torchvision0.13.0cu113 torchaudio0.12.0 --extra-index-url https://download.pytorch.org/whl/cu113安装完后一定要验证一下PyTorch是否能正确调用GPU。新建一个Python脚本或者直接在命令行里输入python进入交互模式运行下面几行代码import torch print(torch.__version__) # 应该输出 1.12.0 print(torch.cuda.is_available()) # 最关键输出 True 才算成功 print(torch.cuda.get_device_name(0)) # 打印出你的显卡型号例如 NVIDIA GeForce RTX 4080看到True和你的显卡型号心里就踏实了。然后我们把likyoo的代码仓库克隆下来并安装其他依赖。# 克隆代码仓库 git clone https://github.com/likyoo/change_detection.pytorch.git cd change_detection.pytorch # 安装requirements.txt里列出的所有依赖包 pip install -r requirements.txt # 额外安装一个可能会缺的库避免后续报错 pip install six到这里基础环境就准备好了。你可能还会需要一些图像处理库不过requirements.txt里基本都涵盖了。如果训练或预测时提示缺什么再用pip install补上就行。3. 数据准备与模型训练让AI学会“找茬”环境搞定接下来就是喂数据训练模型了。你得先去把LEVIR-CD数据集下载下来。原始论文的官网和百度AI Studio上都有资源。下载后数据集通常包含train、val和test三个文件夹每个文件夹下又有A、B和label子文件夹分别存放第一期图像、第二期图像和对应的变化标签。你需要按照代码仓库里要求的目录结构放好一般在datasets文件夹下。比如我的路径是这样的E:/datasets/LEVIR-CD/下面直接就是train、test等文件夹。数据准备好了我们来看看怎么训练。核心的训练脚本是local_train.py有些版本可能是local_test.py本质一样。在运行之前有几个关键参数你需要根据自己情况调整数据路径打开脚本找到设置数据集路径的地方改成你自己的路径。模型参数在脚本中你会看到类似cdp.Unet(...)的模型定义。这里encoder_name可以选择不同的主干网络比如resnet34、efficientnet-b0等resnet34是个不错的起点在速度和精度上比较平衡。in_channels3对应RGB三通道图像classes2表示输出变化和未变化两类。siam_encoderTrue表示使用孪生编码器这是变化检测的关键。训练超参数batch_size批大小和MAX_EPOCH总训练轮数直接影响训练速度和效果。batch_size越大训练越快但显存占用也越高。我用的RTX 4080显卡16G显存把batch_size从默认的8调到32也能跑起来训练速度明显快了很多。MAX_EPOCH可以先设为60看看效果。调整好参数后就可以开始训练了。命令很简单python local_train.py训练过程中你会看到损失loss在逐渐下降评估指标比如IoU在慢慢上升。如果一切顺利几十个epoch后模型就会收敛。训练完的模型会保存为.pth文件比如best_model.pth这就是我们得到的“找茬”AI大脑。这里我踩过一个坑分享给你训练时可能会报一个关于map_location的错误。这是因为代码在加载预训练权重时设备指定可能有问题。解决方法通常是找到项目里的hub.py文件或其他加载模型的文件检查加载模型的那行代码有时需要删掉或修改map_location这个参数确保它和你的当前设备CPU或GPU匹配。4. 模型预测与结果分析看看AI“找茬”准不准模型训练好了得试试它的本事。我们需要写一个预测脚本加载训练好的模型输入一对新的前后期图像让它输出变化图。预测脚本的核心步骤包括加载模型、读取图像、进行与训练时相同的前处理归一化等、将图像输入模型得到预测结果、最后将结果从概率图转换为二值掩码图0和255并保存。下面是一个精简版的预测流程代码关键部分我都加了注释import cv2 import torch import numpy as np import albumentations as A import change_detection_pytorch as cdp # 1. 定义设备并加载模型 device cuda if torch.cuda.is_available() else cpu model cdp.Unet(encoder_nameresnet34, encoder_weightsNone, in_channels3, classes2, siam_encoderTrue) model.load_state_dict(torch.load(best_model.pth)) # 加载训练好的权重 model.to(device) model.eval() # 切换到评估模式 # 2. 定义图像预处理必须和训练时一致 transform A.Compose([A.Normalize()]) # 3. 读取一对测试图像 img1 cv2.imread(test/A/test_1.png) # 第一期图像 img2 cv2.imread(test/B/test_1.png) # 第二期图像 # 4. 预处理并转换为Tensor img1 transform(imageimg1)[image].transpose(2, 0, 1) # HWC - CHW img2 transform(imageimg2)[image].transpose(2, 0, 1) img1 torch.from_numpy(img1).unsqueeze(0).to(device) # 增加批次维度 img2 torch.from_numpy(img2).unsqueeze(0).to(device) # 5. 模型预测 with torch.no_grad(): # 关闭梯度计算加速推理 prediction model(img1, img2) # 6. 后处理取概率最大的类别并转为0-255的图像 pred_mask torch.argmax(prediction, dim1).squeeze().cpu().numpy() # 得到0/1的掩码 pred_mask_vis (pred_mask * 255).astype(np.uint8) # 0-0(黑)1-255(白) # 7. 保存结果 cv2.imwrite(change_result.png, pred_mask_vis)运行这个脚本你就能得到一张黑白的变化图其中白色像素代表模型认为发生变化如新建建筑的区域。你可以直观地对比原图和预测结果看看AI找得对不对。通常训练充分的模型在LEVIR-CD测试集上能取得不错的效果变化建筑的轮廓都能大致勾勒出来。当然也可能会有一些误检比如阴影被当成变化或漏检这可以通过调整模型结构、数据增强或后处理来优化。5. 模型部署优化导出ONNX并加速推理用PyTorch训练和测试模型很方便但当我们想把它集成到其他应用比如C程序、移动端或Web服务中时就需要一个更通用、高效的格式。这就是ONNX的用武之地。ONNX就像一个“中间翻译官”能把不同框架PyTorch, TensorFlow等训练的模型转换成统一的格式方便在各种平台上运行。5.1 导出ONNX模型静态与动态输入导出ONNX模型并不复杂。我们需要准备一个示例输入dummy input然后调用torch.onnx.export函数。这里有个关键选择静态形状还是动态形状导出。静态ONNX输入图像的尺寸如[1, 3, 1024, 1024]是固定的。导出简单运行时效率可能更高但不够灵活只能推理固定尺寸的图片。动态ONNX允许输入图像的批次大小batch size、高度、宽度是变化的。这在实际应用中非常有用比如处理不同分辨率的遥感影像。下面这个export_onnx.py脚本演示了如何导出两种格式import torch import change_detection_pytorch as cdp # 1. 构建模型并加载权重和训练时一样 model cdp.Unet(encoder_nameresnet34, encoder_weightsNone, in_channels3, classes2, siam_encoderTrue) model.load_state_dict(torch.load(best_model.pth)) model.eval() model.cuda() # 放到GPU上 # 2. 准备示例输入两个随机张量模拟前后期图像 dummy_input1 torch.randn(1, 3, 1024, 1024, devicecuda) dummy_input2 torch.randn(1, 3, 1024, 1024, devicecuda) # 3. 导出静态ONNX模型 torch.onnx.export(model, (dummy_input1, dummy_input2), # 模型输入注意是元组 change_det_static.onnx, # 输出文件名 input_names[image_A, image_B], # 输入节点名 output_names[output], # 输出节点名 opset_version12, # ONNX算子集版本 verboseTrue) # 打印导出详情 # 4. 导出动态ONNX模型 torch.onnx.export(model, (dummy_input1, dummy_input2), change_det_dynamic.onnx, input_names[image_A, image_B], output_names[output], opset_version12, verboseTrue, # 关键指定哪些维度是动态的 dynamic_axes{ image_A: {0: batch_size, 2: height, 3: width}, image_B: {0: batch_size, 2: height, 3: width}, output: {0: batch_size, 2: out_height, 3: out_width} })运行脚本后你会得到两个.onnx文件。你可以用Netron这个在线工具打开浏览器访问netron.app然后上传文件可视化模型结构检查输入输出是否符合预期。你会看到模型接收两个输入输出一个[1, 2, H, W]的张量代表每个像素在“未变”和“变化”两类上的概率。5.2 ONNX Runtime推理与性能对比模型导出后我们就可以用ONNX Runtime这个高性能推理引擎来运行它了。相比PyTorchONNX Runtime通常能带来更快的推理速度和更低的内存占用特别适合生产环境部署。首先安装ONNX Runtime注意如果你有GPU就安装GPU版本以获得加速pip install onnxruntime-gpu1.12.0然后我们编写一个推理类把前处理缩放、归一化、推理、后处理argmax都封装起来。这里有个细节要特别注意ONNX模型推理时的前处理必须和PyTorch训练/推理时完全一致包括归一化用的均值和标准差。如果不一致结果会差很多。下面是我封装的一个推理类的主要部分import onnxruntime as ort import cv2 import numpy as np class ChangeDetectorONNX: def __init__(self, onnx_path, input_size1024): self.input_size input_size # 重要这里的均值和标准差必须和训练时用的保持一致 self.mean [0.406, 0.456, 0.485] # BGR顺序的均值 self.std [0.225, 0.224, 0.229] # BGR顺序的标准差 # 创建ONNX Runtime会话优先使用GPU providers [CUDAExecutionProvider, CPUExecutionProvider] if ort.get_device() GPU else [CPUExecutionProvider] self.session ort.InferenceSession(onnx_path, providersproviders) def preprocess(self, img): 将单张OpenCV读取的BGR图像处理为模型输入 # 缩放到固定尺寸 if img.shape[0] ! self.input_size or img.shape[1] ! self.input_size: img cv2.resize(img, (self.input_size, self.input_size), interpolationcv2.INTER_LINEAR) # 归一化 (这里假设训练时是对0-255范围除以255后再用mean/std归一化) img img.astype(np.float32) / 255.0 img (img - self.mean) / self.std # 转换维度: HWC - CHW, 并转换为RGB如果模型训练时输入是RGB img img.transpose(2, 0, 1)[::-1, :, :] # [::-1] 将BGR转为RGB img np.ascontiguousarray(img, dtypenp.float32) return img def infer(self, img_a_path, img_b_path): 对前后两期图像进行推理 img_a cv2.imread(img_a_path) img_b cv2.imread(img_b_path) # 前处理 input_a self.preprocess(img_a)[None, ...] # 增加batch维度 input_b self.preprocess(img_b)[None, ...] # ONNX Runtime推理 # get_inputs()[0].name 获取第一个输入节点的名称与导出时设置的input_names对应 outputs self.session.run(None, {self.session.get_inputs()[0].name: input_a, self.session.get_inputs()[1].name: input_b}) # outputs[0] 形状为 [1, 2, H, W] prediction outputs[0] # 后处理取argmax得到类别索引然后转为0-255图像 change_map np.argmax(prediction, axis1)[0] # 形状 [H, W] change_map_vis (change_map * 255).astype(np.uint8) return change_map_vis # 使用示例 detector ChangeDetectorONNX(change_det_static.onnx) result detector.infer(test/A/test_1.png, test/B/test_1.png) cv2.imwrite(onnx_result.png, result)在我的RTX 4080上测试ONNX Runtime推理一张1024x1024的图片大约只需要100毫秒左右显存占用约2GB相比PyTorch推理模式速度和资源消耗都有明显优化。这意味着你可以用更低的成本部署这个变化检测服务处理海量的遥感影像。5.3 结果后处理与可视化优化模型直接输出的二值变化图往往比较“粗糙”可能存在一些散落的噪声点椒盐噪声或者边界不够平滑。我们可以用一些简单的图像后处理技术来优化视觉效果也让结果更准确。最常用的就是形态学操作比如开运算和闭运算。开运算先腐蚀后膨胀可以消除小的白色噪声点闭运算先膨胀后腐蚀可以填充小的黑色空洞连接邻近的白色区域。我们可以用OpenCV轻松实现import cv2 import numpy as np def postprocess_mask(mask, kernel_size3): 对二值变化掩码进行后处理 mask: 输入的二值图像 (0和255) kernel_size: 形态学操作核的大小 kernel np.ones((kernel_size, kernel_size), np.uint8) # 开运算去除小白点 mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 闭运算填充小空洞连接区域 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return mask # 读取原始预测结果 raw_result cv2.imread(onnx_result.png, cv2.IMREAD_GRAYSCALE) # 进行后处理 cleaned_result postprocess_mask(raw_result, kernel_size5) cv2.imwrite(cleaned_result.png, cleaned_result)仅仅把变化区域标成白色有时还不够直观。更好的方式是将变化区域叠加显示在原始影像上比如用半透明的红色高亮显示变化区域这样一眼就能看出具体是哪里新建或拆除了建筑。def overlay_change(bgr_image, change_mask, color(0, 0, 255), alpha0.5): 将变化掩码以半透明颜色叠加到原图上 bgr_image: 背景原图 (BGR格式) change_mask: 二值变化掩码 (单通道变化区域为255) color: 叠加颜色默认红色 (B,G,R) (0,0,255) alpha: 叠加透明度 # 创建一个着色的掩码 colored_mask np.zeros_like(bgr_image) colored_mask[change_mask 255] color # 将原图转换为浮点数进行混合 overlay cv2.addWeighted(bgr_image, 1, colored_mask, alpha, 0) return overlay # 读取第二期影像作为背景和清洗后的变化图 background cv2.imread(test/B/test_1.png) change_mask cv2.imread(cleaned_result.png, cv2.IMREAD_GRAYSCALE) # 生成叠加可视化结果 visualization overlay_change(background, change_mask, color(0, 0, 255), alpha0.6) cv2.imwrite(change_overlay.png, visualization)经过后处理和可视化叠加最终得到的结果图就非常清晰易懂了。红色的半透明区域清晰地指示出建筑的变化你可以直接拿这份结果去做进一步的分析或汇报。从数据准备到模型训练再到ONNX优化部署和结果美化这一整套流程走下来你已经亲手搭建了一个可以投入实际应用的遥感建筑变化检测系统。