ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python卫星云图识别:从HDF5到ResNet模型部署

Python卫星云图识别:从HDF5到ResNet模型部署 简介一套基于Python的卫星云层图像识别系统面向计算机视觉、遥感图像处理方向的课程实践与期末项目开发。系统围绕图像预处理、特征提取与分类识别三个核心环节涵盖图像增强、噪声去除、灰度转换、边缘检测、纹理特征与颜色直方图等关键方法并支持多种机器学习模型切换便于对比不同算法对识别精度的影响。整体界面友好操作直观非专业用户也能快速上手。压缩包共168个文件约62.33MB以Python源代码为主包含pyc编译产物、CSV数据文件、YAML配置文件、实验报告PDF与演示文稿PPT目录结构清晰。目前已有43人学习浏览。配套分析文档详细说明了系统设计思路、各功能模块作用及实验流程代码注释完整可支撑二次开发与教学演示。借助这套资源读者既能掌握卫星云图识别的完整处理链路又能获得课程设计或期末项目所需的代码基础与实验素材。1. 卫星云层图像识别不只是给云图打个标签卫星云图是气象预报、农业防灾、光伏功率预测里最直观的输入但卫星影像的频率高、数据量大靠人眼扫图做“晴空、低云、高云、积雨云”的分类既跟不上节奏也容易把薄云和霾混为一谈。这套基于Python的卫星云层图像识别系统核心就是让卷积神经网络替人眼完成这件事——把一景一景的卫星云图裁成小图块逐块判读云层类型再拼回一张带类别标签的云图。它适合两类人一是做气象或遥感方向的学生需要一套能跑通、能答辩的完整链路二是刚接触深度学习落地的小团队想把“图像分类分割”套到卫星数据上却卡在了数据格式、类别体系、模型选型这些具体问题上。本文会把数据从哪里来、怎么预处理、用什么网络、怎么调参数、部署时有什么坑按一条可复现的路径讲清楚。2. 卫星云图数据准备从 HDF5 到“模型能吃的图块”2.1 先搞清楚卫星云图到底是哪种文件卫星云图的原始文件通常不是 PNG而是 HDF5 或 NetCDF 格式例如葵花8号Himawari-8的 L1 级数据就是 HDF5 容器里面放着多个波段Band 1~Band 16、经纬度查找表LUT和太阳几何角等信息。直接把整个文件交给卷积网络是行不通的因为模型只能吃规则形状的数值矩阵而 HDF5 里装的是异构数据块。常见做法是先用 h5py 打开文件挑出需要的波段可见光、水汽、红外再按经纬度范围裁剪出目标区域。以下是一个最小读取示例import h5py import numpy as np # 打开 HDF5 文件只读模式 with h5py.File(AHI_H08_20250101_0300_B01_FLDK_R20_JPF.nc, r) as f: # 打印顶层 key确认波段名称常见命名如 B13 或 image print(list(f.keys())) # 假设可见光波段叫 B03红外波段叫 B13 channel_b03 f[B03][:] channel_b13 f[B13][:] # 原始数据通常是 0~4095 的整型计数值不是反射率 img channel_b03.astype(np.float32)读取后有三个必须处理的问题。一是波段数据是 16 bit 计数值要先除以缩放因子转成物理量再把 0~1 范围压到 0~255二是不同波段的空间分辨率不同可见光 0.5 km红外 2 km要统一重采样到同一网格三是云图带有“圆盘外”的空值区域数据值全为 65535这些像素要标记为 0不能参与训练。2.2 把大图裁成图块滑窗裁剪与标签生成卫星云图单张可能达到 1024×1024 甚至更大直接整图丢进网络显存撑不住分类也粗。常见的做法是滑窗裁剪成 256×256 或 128×128 的小图每张图块对应一个类别标签。标签来源于人工标注或已有的云分类产品如 JAXA 的 CLIM 产品把“高层云、中低层云、晴空、积雨云”映射成 0~3 的整数索引。裁剪时的步长选择是第一个坑。步长等于图块尺寸时图块没有重叠信息不冗余但标签边界生硬步长取一半128时同一片云区会被多个图块覆盖相当于隐式数据增强但样本量膨胀四倍。我一般先按步长图块尺寸裁剪一遍拿到底图再对每个底图做随机偏移裁剪一次让训练集比测试集多一倍的样本。import cv2 def extract_patches(image, gray_label_map, patch_size256, stride128): h, w image.shape[:2] patches, labels [], [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): img_patch image[y:y patch_size, x:x patch_size] label_patch gray_label_map[y:y patch_size, x:x patch_size] # 多数投票以图块中心50%区域的最常见类别作为标签 center_region label_patch[ patch_size // 4: 3 * patch_size // 4, patch_size // 4: 3 * patch_size // 4, ] label np.bincount(center_region.flatten()).argmax() patches.append(img_patch) labels.append(label) return np.array(patches), np.array(labels)这里用中心区多数投票而不是整块统计是为了避免图块边界的类别穿插导致标签噪声。参数 stride128 时图块之间有 50% 重叠样本量充足但不能无限调小否则相邻样本几乎一样训练集和验证集会泄漏重叠信息模型指标虚高。2.3 归一化与数据增强参数不能拍脑袋卫星影像的像素分布比自然图像“尖锐”——大量像素集中在低值区只有云区是高亮。直接用 ImageNet 的均值方差[0.485, 0.456, 0.406]去归一化卫星云图并不合适因为各波段的量纲根本不匹配。正确的做法是统计训练集每个通道的均值和标准差存成 JSON 文件训练和推理时共用同一份参数。# 统计训练集通道均值和标准差 channel_sum np.zeros(3, dtypenp.float64) channel_sqsum np.zeros(3, dtypenp.float64) pixel_count 0 for img_patch in train_patches: # train_patches: (N, 256, 256, 3) channel_sum img_patch.reshape(-1, 3).sum(axis0) channel_sqsum (img_patch.reshape(-1, 3) ** 2).sum(axis0) pixel_count img_patch.shape[0] * img_patch.shape[1] mean channel_sum / pixel_count std np.sqrt(channel_sqsum / pixel_count - mean ** 2) print(mean:, mean, std:, std)数据增强方面云图是俯视影像不应当做自然图像处理——没有“上下颠倒”的物理意义但也有技巧旋转 90 度、翻转、小角度旋转±10 度对模型鲁棒性增益明显而亮度调整要慎用因为云图亮度就是物理量反射率动太多会让模型学到错误关联。色彩抖动和随机擦除可以保留用来模拟传感器噪声和局部缺失。3. 模型选型与训练分类用 ResNet分割用 UNet3.1 为什么不用 VGG 而用 ResNet云层识别的输入是规则图块不需要检测边界框或实例掩码本质上是图像分类任务。VGG 堆叠卷积层能加深网络但梯度消失严重收敛慢ResNet 的残差结构让网络至少有“恒等映射”的保底选项在样本量不够大时不容易训崩。实践里 ResNet18 和 ResNet50 是性价比最高的两个选择。如果任务只需要区分“晴空/有云”ResNet18 就够用如果要区分云型积雨云、卷云、层云需要 ResNet50 以上的特征表达能力因为不同云型的纹理差异细微。这里有一个容易被忽略的点预训练权重是关键。直接用 ImageNet 预训练模型做迁移学习比从零训练收敛快得多但要冻结前几层、只微调后几层否则小数据集会把预训练参数冲掉。import torch import torch.nn as nn import torchvision.models as models def build_model(num_classes4, freeze_layersTrue): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) if freeze_layers: for name, param in model.named_parameters(): # 冻结前四层卷积只训练最后两个 block 和全连接 if layer4 not in name and fc not in name: param.requires_grad False # 替换全连接层适配云类别数 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes), ) return model冻结策略的取舍逻辑是卫星云图和 ImageNet 的自然图像在底层纹理上有共性边缘、梯度、纹理方向而高层语义完全不同。所以冻结底层卷积layer1~layer3只训练 layer4 和全连接层既能利用预训练特征又避免小数据集把底层参数“带偏”。如果数据量超过 1 万张图块可以放宽冻结范围让模型自己重新学底层纹理。3.2 损失函数与优化器这几个参数决定生死云层分类是典型的多分类问题先用带权重的交叉熵损失。权重一定要设——卫星影像里晴空图块占比可以超过 70%积雨云可能只有 5%如果不加权模型会学成“永远预测晴空”。from torch.utils.data import DataLoader, TensorDataset from torch.optim import AdamW import torch.nn.functional as F # 假设 train_patches, train_labels 已准备好 train_ds TensorDataset( torch.from_numpy(train_patches).permute(0, 3, 1, 2), torch.from_numpy(train_labels).long() ) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) # 类别权重样本多的类别权重小样本少的权重大 class_counts np.bincount(train_labels, minlength4) class_weights torch.tensor(1.0 / np.sqrt(class_counts 1), dtypetorch.float32) criterion nn.CrossEntropyLoss(weightclass_weights) model build_model(num_classes4) optimizer AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20)优化器选择 AdamW 而不是传统 Adam是因为 AdamW 的权重衰减实现更规范配合开箱即用的 lr3e-4在不调参的情况下也比 Adam 稳一点。学习率调度用余弦退火前 5 个 epoch 不要上大的学习率先拿 1e-4 跑 3 个 epoch 做“热身”再切到 3e-4最后由余弦退火拉到 1e-5 左右。这能让模型在训练后期还有微调能力而不是学习率一降到底丧失了调整能力。3.3 训练监控除了 loss还要盯混乱矩阵训练时只盯着 loss 曲线是最常见的翻车方式——loss 下降了但混淆矩阵里小类别全错。我在每个 epoch 末尾计算验证集的准确率、F1、Kappa 系数并打印每个类别的召回率。from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, val_loader): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: outputs model(imgs) preds torch.argmax(outputs, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds))如果看到“晴空”的召回率 95%但“积雨云”召回率只有 30%说明类别权重还不够强或增强策略没有把积雨云样本的多样性提上来。此时优先做的是对有云类别做过采样而不是盲调整权重因为权重过大会让模型把很多晴空误判成积雨云整体 IoU 反而下降。4. 模型导出与推理部署从训练服务器到业务侧4.1 用 TorchScript 导出摆脱后处理依赖训练好的 PyTorch 模型要部署到业务侧需要导出成不依赖 Python 开发环境的格式。TorchScript 是常见方案把模型结构和权重打包成一个 .pt 文件用 C 或纯 Python 都能加载推理。导出前先说一个关键细节必须把数据预处理也“写进”模型否则线上调用方很容易忘了归一化推理结果直接变随机。import torch # 把归一化写进模型前向过程 class DeployModel(torch.nn.Module): def __init__(self, backbone, mean, std): super().__init__() self.backbone backbone self.mean torch.tensor(mean).view(1, -1, 1, 1) self.std torch.tensor(std).view(1, -1, 1, 1) def forward(self, x): # x 输入是 0~255 的 uint8 转为 float x x.float() / 255.0 x (x - self.mean) / self.std return self.backbone(x) deploy_model DeployModel(model.cpu().eval(), mean, std) deploy_model torch.jit.script(deploy_model) deploy_model.save(cloud_type_resnet18.pt)把归一化参数封装进球内后调用方只需要把 0~255 的图像块传进去输出的 logits 就是直接可用的类别概率不用再去查 JSON 文件做预处理。这一步能拦住 90% 的线上推理异常——最常出现的就是有人用 0~255 的图传入模型缺失了 /255 和减均值操作结果所有图块的概率都异常。4.2 推理侧的大图重建按块推理再拼盘线上推理很少只跑一张图块常见的是把一张 1024×1024 的卫星云图裁成 16 个 256×256 图块分别推理再把结果拼成整图。这个流程的坑在拼接处——图块推理时边缘像素的类别往往比中心区域置信度低直接拼图会产生“马赛克边界”。解决办法是推理时也做重叠裁剪步长取 128把相邻图块的重叠区域的预测结果做平均投票。这种方式虽然增加了 4 倍计算量但拼接后的块状感明显减弱。def inference_full_scene(deploy_model, scene, patch_size256, stride128): h, w scene.shape[:2] prob_map np.zeros((h, w, 4), dtypenp.float32) count_map np.zeros((h, w, 1), dtypenp.float32) for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patch scene[y:y patch_size, x:x patch_size] patch_input torch.from_numpy(patch).unsqueeze(0).permute(0, 3, 1, 2) with torch.no_grad(): prob torch.softmax(deploy_model(patch_input), dim1).squeeze(0).numpy() prob_map[y:y patch_size, x:x patch_size] prob.transpose(1, 2, 0) count_map[y:y patch_size, x:x patch_size] 1 prob_map / np.maximum(count_map, 1) label_map np.argmax(prob_map, axis-1) return label_map推理阶段的工程参数有两个要实测。stride128 时输出能保持类别粒度小时的空间连贯性stride256 则推理速度快一倍但云层边缘容易出现锯齿。如果业务对速度要求高可以先用 stride256 推理只对“发现边缘不规则的图块区域”再重推理一遍大部分区域可以省掉一半计算量。4.3 CPU 推理还是 GPU看吞吐量不看单帧延迟卫星云图识别业务通常不是实时交互而是批处理。一片区域的 8 张连续时次云图跑一遍全场景推理几秒到几十秒都能接受。这种情况下优先选 CPU 推理部署简单、没有显存管理问题只有当单张场景推理延迟要求小于 500ms 时才需要 GPU。CPU 上有个免费的提速手段开启 TorchScript 的 intra-op 并行度。默认 PyTorch 可能只用单核手动设置 torch.set_num_threads(8) 配合 ONNX Runtime 的 CPU 推理单张 1024×1024 场景的推理时间能压到 2 秒以内。ONNX Runtime 导出方式与 TorchScript 类似但要注意算子兼容性常见做法是先用 TorchScript 跑通全流程再尝试 ONNX。5. 卫星云图识别的避坑清单从数据到推理的四个大坑5.1 投影坐标系没对齐裁剪出的图块全是“斜的”现象从两个波段数据里裁同一个区域的图块叠在一起发现边缘错位模型训练集里出现了大量“重影”图块。原因不同波段的原始空间分辨率不同配准网格也不一致。直接按数组下标裁剪忽略了每个波段的经纬度查找表。解决先读取每个波段的 LUTLongitude/Latitude 查找表插值到一个公共等经纬网格上再做裁剪。这个步骤在数据预处理阶段做一次后面所有图块都基于统一网格。5.2 类别不平衡导致模型“永远晴空”现象验证集整体准确率 85% 以上但每个有云类别的召回率都不到 40%。原因晴空样本占比超过 70%交叉熵损失的优化方向被大类别主导模型学到了“猜晴空”的捷径。解决先做类别加权权重与样本数成反比如果加权后小类别召回率上来了但大类别掉太多改用焦点损失Focal Loss通过调节 γ 把训练注意力压到难样本上。γ2 时晴空误报率会有一定上升但积雨云召回率明显改善需要在验证集上权衡。5.3 验证集混入了训练集的重叠图块现象训练准确率 98%验证准确率也是 97%但上线后出现大量误报。原因滑窗裁剪 stride128 时相邻图块有 50% 重叠如果先裁剪再划分训练集/验证集同一张云的大块会被分进两侧模型等于“见过”验证集的内容。解决必须先按场景划分。把每景卫星云图整体分到训练集或验证集在场景内部再做裁剪严禁把同一景的图块打散混进两边。5.4 白平衡与黑边现象推理时云图边缘一大块区域全部识别成类 0。原因卫星圆盘外是 65535 的填充值转成 255 白色后模型把这部分当成“云”而不是“空”。解决预处理时把 65535 像素统一置 0并生成一个掩膜参与训练损失函数只统计掩膜内像素的预测值。推理时对掩膜外的输出强制归为“无数据”类。6. 验证与进阶用 IoU 和逐时次序列检查模型到底行不行模型收敛后准确率和 F1 只是一方面做云图识别还得回答“判得准误报高不高、边缘贴不贴”。我的习惯是拿一景完整云图跑全场景推理把类别标签叠在原图上逐块检查云区边缘与原始亮度转折处是否吻合。这里最容易发现问题是“碎云漏判”——小块的积雨云被模型忽略或者层云的边缘被拉得太宽。定量上我看两个指标。一是各类别的 IoU尤其是有云类别的 IoU低于 0.5 的类别说明模型对那块区域定位能力不足常见做法是加大该类别在损失中的权重或收集更多该云型的样本二是时间连续性——同一片区域相邻半小时的两景云图推理出来的积雨云簇位置应当有小幅位移而不是突然消失。如果出现“闪烁”说明模型对光照角度的变化太敏感需要在增强阶段加入模拟太阳高度角变化的处理。进阶方向上值得做的是把分类网络换成 UNet 做逐像素语义分割。分类网络丢掉了空间上下文对云团边界刻画能力有限UNet 能把类别标注从“图块”细化到每个像素。代价是标注成本高得多——逐像素标注至少是逐块标注五倍工作量。如果业务只关心“哪个区域有积雨云”分类网络就够用如果要做云的覆盖率统计UNet 才是更稳的方案。最后提醒一个我自己踩过的坑不要把训练时统计出来的类别权重直接带到推理服务里。推理只要输出原始 logits 或 softmax 概率权重只参与损失计算带进推理会让概率分布整体偏移下游阈值全得重新调。做完这套流程模型的每一张输出图块能对应到哪类云型、置信度多少、覆盖区域在哪都能在日志里倒查。希望这篇笔记能帮你少走几趟弯路。本文还有配套的精品资源点击获取
返回列表