简介基于Python的卫星云层图像识别系统面向计算机视觉初学者与高校课程实践、期末项目场景完整覆盖图像预处理、特征提取与分类识别全流程。系统集成图像增强、噪声去除、灰度转换等手段并采用边缘检测、纹理特征、颜色直方图等方法提取关键信息配合机器学习分类器实现自动识别界面友好操作门槛低。资源包共168个文件其中70个.py主程序、44个.pyc缓存、27个.zbak备份及5个.csv数据文件等压缩包62.33MB另含实验报告PDF与结果展示图便于对照验证。已有43人学习下载适合需快速搭建图像识别系统或参考完整项目架构的读者。配套分析文档详细梳理了各模块设计思路与调用关系可直接用于课程实验、毕业设计或科研辅助对深入理解遥感图像处理流程有较高参考价值。1. 卫星云层图像识别为什么真正该从数据管线动手把卫星云图直接塞进CNN十个项目有九个会翻车。这是我做气象识别项目前期的真实教训。卫星云层图像识别在Python里常被当成图像分割任务但真正难的是让模型吃到的数据是对的多通道扫描时间差、辐射定标、投影网格哪一环错了再好的网络也学不到稳定特征。这篇笔记想讲清楚一套能落地的方法——从Himawari和风云系列原始文件入手完成通道选取、定标、重采样再让阈值法和U-Net各司其职最后落成一条能每天自动出云量结果的处理链路。适合气象算法岗、遥感方向研究者以及做光伏功率预测、灾害监测的开发者照着实现。2. 用satpy读取HSD与NetCDF通道选取和辐射定标怎么做卫星云层图像识别系统的地基是数据读取。大多数人第一次接触原始遥感数据时都会懵文件名是一长串大写字母加数字打开后不是图片而是数组。这里需要先做两件事——把文件读成带坐标的DataArray再决定用哪几个通道作为识别输入。2.1 两种主流输入HSD原始块和NetCDF投影数据Himawari-8/9的HSD格式是散装文件每个通道、每个扫描时段各存一个.dat风云四号则习惯给NetCDF/NC文件一个文件里带上辐射值和经纬度坐标。读取方式完全不同。HSD数据常见做法是用satpy它把多个散文件组合成一个Scene对象。示例代码from satpy import Scene from glob import glob # 匹配同一时刻所有通道的HSD文件 hsd_files sorted(glob(data/hsd/HS_H08_20230101_0300_B??_FLDK_R10_RJD*)) scene Scene(filenameshsd_files, readerahi_hsd) # 加载可见光近红外和长波红外通道 scene.load([B03, B06, B13]) print(scene.available_dataset_names())这里B03是0.64微米可见光B06是2.3微米短波红外B13是10.4微米热红外。选择这三个通道的原因后面第2.2节展开。satpy读取后会给出带坐标的DataArray其中scene[B13]的单位已经换算成开尔文亮温不用再手动做辐射定标。如果拿到的是风云四号NetCDF文件直接用xarray读取但要注意产品变量名。常见结构是import xarray as xr ds xr.open_dataset(data/fy4a/20230101_0300.nc) print(ds.variables) # 典型变量CMI通道辐射、CLM云掩码、Latitude、LongitudeCMI在不同文件中可能是辐射率、反射率或亮温必须先看文件属性里的units字段再决定要不要换算。我一般会先跑一句ds[CMI].attrs确认单位再决定后续处理。这一步偷懒后面算法就全错。2.2 亮温和反射率定标云识别模型到底该吃哪几个通道云识别的核心依据其实很物理云在可见光波段反射率高在热红外波段的亮温比地表低。所谓定标就是把原始计数值转换成有物理含义的反射率或亮温否则阈值没法设深度学习模型也学不到稳定分布。具体通道选择上我建议至少保留三组通道角色代表通道物理含义识别价值可见光反射率0.64微米云反射太阳光白天区分云与地面热红外亮温10.4微米云顶温度全天候识别高云红外分裂窗10.4与12.4微米亮温差云的厚度与相态夜间识别低云做完通道加载后还需要确认每一帧数据里没有无效值。卫星扫描圆盘边缘有天底角过大导致的几何畸变那些像素在阈值法里很容易误判。一个实用做法是把天底角超过60度的区域直接掩掉。import numpy as np from satpy import Scene # 部分产品自带太阳天顶角和卫星天顶角 scene.load([B03, B13, sunz]) sza scene[sunz].data # 天底角过大区域置为NaN bt13 scene[B13].data.where(scene[satz].data 60) refl03 scene[B03].data.where(sza 80)这里用了.where()而不是.mask()是为了让无效值保持NaN传递下去而不是被当成0参与计算。0在红外亮温里会被误判成极冷的云顶那才是灾难。2.3 用AreaDefinition统一投影多源数据对齐的关键参数HSD原始数据是标称投影近似圆盘NetCDF产品则可能是经纬度网格。两个数据源要做同一套识别流程第一步就是把它们投影到同一张网格上。用satpy的resample可以做到但要用pyresample的AreaDefinition定义一个目标区域。from pyresample.geometric import AreaDefinition region AreaDefinition( china_test, custom area, custom, {proj: lcc, lon_0: 105.0, lat_0: 35.0, lat_ts: 35.0, ellps: WGS84}, x_size1200, y_size900, area_extent(-2500000.0, -2000000.0, 2500000.0, 2000000.0) ) scene_resampled scene.resample(region, resamplernearest)参数说明lon_0105、lat_035是投影中心大概覆盖中国中部area_extent的单位是米四个数字依次是左下角x、左下角y、右上角x、右上角y范围4000公里乘4000公里左右。resamplernearest速度最快适合初版如果做定量分析推荐换bilinear。不要小看这一步的投影参数。lat_ts写错整张图会倾斜area_extent写反出图是镜像。常见做法是先输出一张带经纬度网格的底图自查确认海岸线方向正确后再往下走。3. 云识别算法阈值法和U-Net各就各位数据管线搭好之后识别算法反而简单了。云识别业界一般分两条路线一条是物理阈值法适合快速出基线、处理夜间和极区场景另一条是深度学习语义分割适合处理薄云、碎云等阈值法容易翻车的区域。两条我都建议同时保留。3.1 阈值法做基线三个通道的组合决策阈值法的逻辑是云在可见光更亮、在红外更冷、在分裂窗差值上更强。白天和夜间要分开处理因为可见光夜间没有信号。import numpy as np def cloud_mask_by_threshold(refl06, bt13, bt15, sza): refl06: 0.64微米反射率0-1 bt13: 10.4微米亮温K bt15: 12.4微米亮温K sza: 太阳天顶角度 day sza 80 # 白天高反射率 亮温低于暖地表 day_cloud (refl06 0.30) (bt13 293) # 夜间亮温低于265K或分裂窗亮温差明显 night_cloud (bt13 265) | ((bt13 - bt15) 4.0) return np.where(day, day_cloud, night_cloud).astype(np.uint8)参数说明0.30这个反射率阈值在裸地、沙漠区域要下调到0.25在积雪区要上调到0.50否则雪会被成片误判成云。293K对应约20摄氏度这是夏季暖地表的近似值冬季北方地表也能到260K以下纯粹用亮温阈值会翻车。bt13 - bt15 4.0是夜间低云判据薄云区域这个差值一般在2到5K之间。阈值法的价值是给深度学习模型提供伪标签和对照基线。当U-Net结果和阈值法偏差过大时先怀疑的是训练数据而不是模型结构。3.2 准备U-Net训练数据裁剪、归一化和掩码配对U-Net是语义分割的经典结构在遥感云识别里足够稳定。训练前要把整盘卫星数据切成小补丁否则单张全圆盘图动辄上万像素显存放不下。补丁大小常用256乘256或512乘512步长设为补丁的一半做重叠采样增加样本量。import torch from torch.utils.data import Dataset class CloudDataset(Dataset): def __init__(self, image_list, mask_list, patch_size256, stride128): self.patches [] for img, msk in zip(image_list, mask_list): h, w img.shape[:2] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): img_p img[y:ypatch_size, x:xpatch_size] msk_p msk[y:ypatch_size, x:xpatch_size] self.patches.append((img_p, msk_p)) def __len__(self): return len(self.patches) def __getitem__(self, idx): img, msk self.patches[idx] img_t torch.from_numpy(img.transpose(2, 0, 1)).float() msk_t torch.from_numpy(msk).long() return img_t, msk_t输入通道建议组装成4通道可见光反射率、热红外亮温、亮温差、太阳天顶角余弦值。太阳天顶角作为输入通道是让模型学会区分白天和夜间的关键。很多人只给两个通道就训练模型在昼夜交替时段就乱猜。归一化上反射率除以50亮温减去200再除以100把数值压到0到1附近即可。不要用ImageNet的均值和方差那是自然图像的分布对遥感数据反而有害。3.3 训练参数Dice Loss、学习率和类别不平衡云在海陆边界少、大片晴空多正负样本极不平衡。单纯用交叉熵损失模型会学成“全都预测为晴空”因为准确率照样很高。常见做法是Dice Loss加交叉熵的组合。import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.softmax(pred, dim1)[:, 1] pred pred.contiguous().view(-1) target target.contiguous().view(-1).float() intersection (pred * target).sum() return 1.0 - (2.0 * intersection smooth) / (pred.sum() target.sum() smooth)训练参数我一般这样设初始学习率1e-3用AdamWbatch size在8到16之间输入尺寸512乘512时batch取4更稳。学习率用余弦退火50个epoch后降到1e-5。验证集上重点看mIoU而不是只看loss值当验证mIoU连续10个epoch不涨就早停省时间也避免过拟合。数据增强里最有用的是随机旋转90度和随机翻转卫星云图是各向同性的旋转增强几乎不引入伪影但不要做随机裁剪缩放那会改变云的物理尺度。4. 工程落地把识别流程装进可维护的Python系统单张图的识别脚本谁都能写真正拉开差距的是系统设计新数据来了自动触发、中间失败可重试、结果可追溯。这一章讲模块划分和一套能跑起来的主流程骨架。4.1 模块职责数据监听、预处理、推理、状态落盘一个可维护的卫星云图识别系统至少分成四个模块数据监听定时扫描数据目录发现新文件进入待处理列表预处理上面第2章的读取、定标、重采样输出统一的numpy数组推理阈值法或U-Net推理输出云掩码状态落盘把掩码、云量统计、处理日志写回磁盘之前我见过有人把所有逻辑写在一个脚本里数据一来跑一遍中途断网后全部重来。模块化后预处理和推理结果都落盘后续可以只重跑失败环节。4.2 主流程代码骨架从文件到达成云量JSON一个典型的处理链路代码骨架如下from pathlib import Path from satpy import Scene import numpy as np class CloudPipeline: def __init__(self, output_dir: str): self.output_dir Path(output_dir) def process_file(self, hsd_path: str): # 1. 读取并加载通道 scene Scene(filenamessorted(hsd_path), readerahi_hsd) scene.load([B03, B06, B13]) # 2. 重采样到统一区域 area self.get_target_area() scene scene.resample(area, resamplernearest) # 3. 推理 mask self.infer(scene) # 阈值法或U-Net # 4. 统计云量 cloud_fraction float((mask 0).sum()) / mask.size self.save_outputs(scene, mask, cloud_fraction)说明get_target_area返回一个AreaDefinition可以做成配置文件不要把投影参数散落在代码里。infer函数里做一个开关白天用阈值法或模型夜间只需阈值法。save_outputs是落盘方法。落盘格式我用两份一份是PNG可视化服务人工盯屏一份是JSON供下游调用。def save_outputs(self, scene, mask, cloud_fraction): import json import pandas as pd # 保存云量统计 result { cloud_fraction: round(cloud_fraction, 4), mask_shape: list(mask.shape), time: str(scene.start_time), } Path(self.output_dir / result.json).write_text( json.dumps(result, indent2) )这里写float((mask 0).sum()) / mask.size是为了避免Python的整数除法在mask元素为0时返回0。JSON里带上mask_shape下游处理时能确认尺寸没被意外缩放。4.3 输出与可视化不只是画一张云图可视化不只是给领导看更是给自己调试用的。我习惯输出一张RGB合成图把云掩码以半透明红色叠加在可见光图上再叠加经纬网格线。这里用Python的标准图像库就能实现不需要额外引入重型地理库。from PIL import Image def render_mask_overlay(base_img, mask, save_path): import numpy as np base np.array(base_img.convert(RGB)) overlay_channel base.copy() overlay_channel[:, :, 0] np.where(mask 0, 255, overlay_channel[:, :, 0]) Image.fromarray(overlay_channel).save(save_path)叠加图只做调试参考最终对外发布建议用GeoTIFF保存掩码方便下游的GIS系统直接使用。如果团队没有GIS栈至少保存一份带坐标的四角经纬度文本放进JSON里别让人家猜这张图覆盖了哪里。5. 避坑指南五个现场级踩坑记录卫星云层图像识别系统的坑大多数不在算法而在数据和工程。以下五条全是实际生产环境里血泪换来的经验。5.1 现象云图边缘出现斜向切割亮道现象识别结果输出后图上有一条明显的斜向亮带从图边缘切到图内既不是云也不是陆地。原因Himawari的圆盘扫描是分块完成的不同通道文件的时间戳其实不一致相差最长可达10分钟。重采样时把不同时刻的扫描块拼到一起运动中的云在边界处错位形成亮道。解决在Scene加载前校验文件名里的小时和分钟字段只取同一标称时间内的文件。必要时把过旧的通道文件丢弃不要硬拼。这个校验放在数据监听模块比在算法里补救便宜得多。5.2 现象冬季陆地区域非云被误判为云现象华北冬季的晴空地表被阈值法成片识别成云。细化识别后U-Net在同样区域也出现大片假正例。原因冬季地表亮温接近260K低于很多代码里硬编码的265K阈值同时积雪或裸地的可见光反射率也不低具备云的“高反射、低温”特征。阈值法会误判模型也会学到这种混淆。解决阈值法里加入土地覆盖辅助数据对积雪区单独处理U-Net训练时把冬季样本占比提高并且在输入里加入太阳天顶角余弦通道让模型学会“低太阳高度角下的高反射不一定是云”。5.3 现象U-Net训练loss下降但验证mIoU徘徊在0.5现象训练集损失从0.8降到0.2看起来在收敛但验证集上的mIoU一直卡在0.5左右不上不下。原因云掩码里晴空像素占比过高模型学会了输出全晴空。loss被大量易分类的晴空样本主导下降只是因为把晴空分得更对云像素的学习信号被稀释。解决改用Dice Loss或者给交叉熵加上正类权重权重设为云像素比例的反比。另一个有效技巧是训练时按“每张补丁至少含20%云像素”来采样这样每个batch里都有可学的正样本。5.4 现象把亮温图存成JPG后识别率骤降现象为了省存储有人把红外通道数据转成JPG保存再拿去做识别结果模型在云边界处产生大量碎片掩码。原因JPG是有损压缩对自然图像视觉影响小但会引入高频噪声。云识别主要靠亮温的细微梯度来判断边界压缩噪声直接把梯度打乱模型在边界处失去判别信息。解决中间处理链路全程保存为16bit整数或32bit浮点的tif/npy格式只在最终可视化时输出JPG。存储成本高一点但换来的是识别结果稳定这笔账划算。5.5 现象推理结果在投影边界变形拉丝现象同一套模型在测试图上表现正常部署到全圆盘图后边缘区域的识别结果出现拉丝、扭曲。原因模型训练用的补丁是从投影后的规则网格上切的而推理时直接把全圆盘原始投影丢进模型。原始投影边缘像素间距大物体被拉伸变形模型没见过这种分布。解决推理前先对全图做重采样切成和训练时一致的补丁推理完再把掩码重采样回原始网格。永远不要让模型直接吃训练分布之外的投影方式。6. 验证与进阶技巧让系统经得起校准系统上线前先做一轮客观校验别直接看几张图拍脑袋说能用。这里介绍我自己一直在用的两套验证方法和一个进阶技巧。6.1 自己搭的判读校验POD/FAR/目视拿一周的历史数据把模型输出的云掩码和人工判读结果对比。统计三个指标命中率POD模型识别出的云像素占真实云的多少、误报率FAR模型标记为云但实际不是的比例、以及F1分数。POD高、FAR低才算合格只盯着准确率会被晴天样本骗过去。人工判读不必逐像素画随机抽20个场景各框一个区域即可重点看云边界、碎云和夜间这三个老大难。6.2 进阶光照归一化和多时次一致性检验白天云识别受太阳高度角影响下午和早晨的反射率差异很大。一个进阶思路是利用信号通道做归一化对固定位置的地表用一个无云的参考反射率库做比值消除光照影响。做法不算复杂——先选半个月的晴空样本训练一个参考背景场推理时把当前反射率除以背景值得到相对反射率作为输入。这一招对裸地和农田区域效果特别明显。另一个容易被忽略的验证是时序一致性。同一片云连续几帧的识别结果应当是平滑演化的如果某一帧掩码突然大面积消失又在下帧重现多半是数据时间戳或太阳角度切换出了问题。用一条简单的云量时间序列曲线做目视基线能挡掉很多模型更新后的回归问题。回头看我做过的项目最贵的一课是“模型没有错数据不对”。自从把预处理链路稳定下来之后阈值法和U-Net的表现都稳稳提升。你调试时如果发现模型怎么调都不涨先回头检查第2章里的每一步尤其是时间戳校验和投影参数。希望帮到你。本文还有配套的精品资源点击获取