简介:围绕风云四号(FY4A)卫星数据读取与解析的 Python 代码包,面向气象、遥感及卫星数据处理初学者和开发者,解决原始观测数据解析与预处理问题,可用于气象预报、气候监测、环境灾害预警等场景。资源共含6个文件,以 Python 脚本为主,另附 XML 配置,压缩包仅12KB,轻量精简。目前已有2299人学习或下载,具备一定参考热度。包内代码构建了从核心读取模块、通用工具函数、数据处理组件到主流程调度、测试脚本和配置文件的完整框架:核心模块负责将原始二进制数据转为结构化信息;工具函数覆盖数据校验、时间序列处理与预处理;组件可组合完成复杂任务;测试脚本用于验证多种场景下的正确性;配置文件支持灵活调整数据源路径、解码参数与输出格式。借助这套框架,读者能快速掌握 FY4A 数据读入与预处理思路,并据此搭建自己的数据处理系统,用于气象研究、环境监测及相关领域分析。
1. FY4A数据读取代码到底解决了什么
做遥感的人第一次拿到风云四号A星(FY-4A)的L1级数据,最惊喜的不是一打开HDF5文件就能看到十几个大气通道,而是被一堆嵌套数据集的路径绊住。这份代码包想解决的就是这个“绊住”的问题:把FY4A数据从原始DN值一路处理成带经纬度的亮温图、反射率图和彩色云图。
它适合两类人——刚接触静止轨道卫星数据、不想花一周时间啃文件格式说明的学生;以及做气象可视化产品、只是想快速把底层读取逻辑跑通再改的开发者。代码包覆盖了文件打开、通道读取、辐射定标、地理定位和图像输出五个环节。你很快会发现,读HDF5本身不值钱,真正影响结果可靠性的,是定标系数怎么拿、经纬度查找表怎么对齐这两件事。
2. 工程结构拆解:解压后先认清四类文件
2.1 解压后的目录与文件职责
一份给开发者用的数据读取代码包,结构通常不会太复杂。这个 code 包解压后,我一般会按下面的方式组织:
codes/ ├── read_fy4a.py # 主入口:读取、定标、出图 ├── fy4a_utils.py # 辅助函数:亮温/反射率/经纬度规整 ├── config.yaml # 输入文件、波段、输出路径配置 ├── requirements.txt # Python 依赖列表 └── samples/ ├── input/ # 放置待读取的 FY4A L1 HDF5 文件 └── output/ # 运行的输出图片和日志拿到代码包后,不要急着跑。先按顺序确认三件事:依赖装没装、samples/input 里有没有数据文件、output 目录是否存在。read_fy4a.py 是主脚本,负责找文件、调函数、出图;fy4a_utils.py 是把脏活集中起来的地方,比如将无符号整数转成浮点辐射值、把开尔文温度转成摄氏度;config.yaml 把输入输出路径从代码里剥离开,方便每次换数据时只改一行配置。
2.2 数据组织方式与脚本的对应关系
FY-4A 的 L1 级产品一般以 HDF5 格式分发,文件里并不仅仅是一张二维图像。除了各个波段的观测数组,还带有一大堆属性:卫星标识、观测时间、波段中心波长、定标系数,以及用于地理定位的经纬度查找表。
代码包里的脚本就是围绕着这份 HDF5 的层次结构写的。h5py 打开文件后,顶层会出现若干数据集或组;波段数据可能按通道号展开,也可能放在某个子组下。规范的写法是先遍历一遍文件对象,把 keys 打印出来,确认实际路径,再决定用哪个名字去取数。直接写死一个路径,换一个数据版本就容易翻车。read_fy4a.py 里通常会写一个 inspect 函数干这件事,它不做任何计算,只是把文件的组织结构展示出来。
2.3 依赖清单与推荐工具链
依赖方面,这份代码基于 Python,核心只有三个库:h5py 读 HDF5、numpy 做数组运算、matplotlib 出图。如果代码包还提供 GeoTIFF 输出功能,则会额外依赖 rasterio;如果产品文件是 NetCDF4 格式,则会把 netCDF4 一起列进 requirements.txt。
建议直接用 Anaconda 或 venv 建一个干净环境。下面这段命令是我拿到代码包后习惯性先执行的:
pip install h5py numpy matplotlib pyyaml rasterioPyYAML 不是读取数据必需的,但它能让 config.yaml 真正被 load 出来。若代码包里没提供 config 读取,也可以把配置直接写成函数参数。我更推荐保留配置文件,因为后续要批量处理几十个文件时,改动输入目录就会非常顺手。
3. 核心代码逐段拆解:从DN值到带经纬度的云图
3.1 先摸清HDF5文件的真实结构
数据读取的第一步不是“读”,而是“看”。拿到一个没见过的 FY-4A 文件,我从来不会直接去读某个猜出来的通道名,而是先执行一个很短的探查脚本:
import h5py def inspect_fy4a(filepath): with h5py.File(filepath, "r") as f: print("顶层条目:", list(f.keys())) for name, item in f.items(): if isinstance(item, h5py.Dataset): print(name, item.shape, item.dtype)这段代码的逻辑很直白:以只读模式打开 HDF5,遍历根组下的所有条目;如果是 Dataset,就打印它的形状和数据类型。参数 filepath 是 HDF5 文件路径;返回的 shape 通常是一个二维数组,例如 (2748, 2748),dtype 一般是 uint16,也就是原始 DN 值。
这里要留个心眼:有的产品把波段数据放在名为 Data 的子组里,有的直接摊在根组下。分发的版本不同,布局会有差异。先跑一遍 inspect,把真实路径打出来,之后所有代码都以这个输出为准。这一步能省掉后面至少半小时的排查时间。
3.2 从DN值到亮温:辐射定标
得到原始数组后,下一个关键动作是辐射定标。L1 数据里的数值不是物理量,定标公式在文件属性里以 scale 和 offset 的形式给出。对可见光通道,最终目标是反射率;对红外通道,则是亮温。先看反射率部分的实现:
import numpy as np def dn_to_reflectance(dn, scale, offset, solar_zenith=None): refl = dn.astype(np.float64) * scale + offset if solar_zenith is not None: refl = refl / np.cos(np.deg2rad(solar_zenith)) return np.clip(refl, 0, 1)这段代码先把 uint16 转成 float64,再乘定标斜率加偏移量,得到辐射亮度或反射率初值。参数 scale 和 offset 通常来自文件属性里的 CALIBRATION 字段;solar_zenith 是观测时刻的太阳天顶角,做太阳高度角修正时使用。这里我必须强调:不要省略 dtype 转换,uint16 直接参与浮点乘除法时会截断精度,尤其在云图边缘容易出带状伪影。
红外通道的定标则是先算辐射值,再用普朗克函数的反函数求亮温:
def bt_from_radiance(rad, c1=1.191042e-16, c2=1.4387769e-2, wavelength=1.04e-5): t = c2 / (wavelength * np.log(c1 / (wavelength**5 * rad) + 1.0)) return t - 273.15参数 rad 是前一步得到的辐射亮度,wavelength 是波段中心波长,这里 1.04e-5 米对应 10.4 微米左右的长波红外通道,常用来反演云顶温度。c1、c2 是辐射公式里的两个常数,除非做高精度研究,否则保持默认即可。返回的是摄氏度,比开尔文更直观。实际工程里不建议每个通道手动传波长,而是在读文件属性时把中心波长短横线拉出来,自动传给这个函数。
3.3 地理定位:经纬度查找表怎么对齐
有了物理量,还缺坐标。FY-4A 圆盘图每个像素对应哪个经纬度,一般由文件内置的查找表提供。读取方式很简单:
lon = f["NOM_LLPT_LON"][:] lat = f["NOM_LLPT_LAT"][:]这个查找表的尺寸可能与波段数据完全一致,也可能是降采样后的。如果遇到降采样版本,需要先对查找表做插值,或者直接用最近邻索引,让纬度数组和亮温数组形状一致。查找表的值范围也有讲究:有的经度是 0 到 360,有的是 -180 到 180。我会在画图前先做个规整:
lon = np.where(lon > 180, lon - 360, lon)这是一个很实用的统一方式。先把经度范围压到 -180 到 180,之后不管是用 Cartopy 画地图,还是转成 GeoTIFF,都不会出现坐标跑到地图另一侧的问题。参数 lon 是原始经度查找表;np.where 做条件替换,不改变数组形状。
3.4 波段合成与图像渲染
单通道亮温图是灰度图,实际展示时往往需要把多个波段合成为彩色图像。核心是每个波段的拉伸和叠放:
def stretch(band_01, lo=2, hi=98): vmin, vmax = np.percentile(band_01, [lo, hi]) band_01 = np.clip(band_01, vmin, vmax) return (band_01 - vmin) / (vmax - vmin + 1e-6) rgb = np.stack([stretch(b_red), stretch(b_green), stretch(b_blue)], axis=-1)stretch 函数的作用是把一个波段的数值范围压缩到 0 到 1。这里用百分比截断而不是直接最大最小归一化,是为了防止个别极亮噪声点把整幅图拉暗。参数 lo 和 hi 分别控制截断的低百分位和高百分位,云图场景里 2% 和 98% 是经验值。将三个波段分别拉伸后拼到最后一维,就是一张 RGB 图。红外云图若要体现温度层次,可以直接把亮温数组做线性映射,再用 reversed 的 colormap 表示低温亮色。
4. 跑通全流程:配置参数、执行命令与结果验证
4.1 配置参数:把文件路径和波段号抽出去
我第一次跑这类代码时,最吃亏的就是把文件名写死在脚本里,换个时次就得改代码。这个代码包用 config.yaml 管理输入输出,我很认同这种设计:
input_file: "FY4A-_AGRI_FULLEDISK_L1.HDF" output_dir: "./output" band: 12 mask_earth: trueinput_file 填测试数据的文件名,output_dir 是出图目录,band 决定读取第几通道,mask_earth 控制是否把圆盘外部的空白区域置为 NaN。若代码包里没有 config 解析逻辑,用一个简单的 yaml.safe_load 就可以加载:
import yaml cfg = yaml.safe_load(open("config.yaml"))这样每次换数据只需要改 yaml 文件。批处理时甚至可以写一个循环,把 input_file 依次替换为不同时次的文件名。
4.2 执行命令与常用参数
主脚本运行方式通常是:
python read_fy4a.py --config config.yaml如果代码包支持命令行覆盖参数,也可以临时指定波段和输出目录:
python read_fy4a.py --input FY4A_xxxx.HDF --band 12 --output result.png有的实现里每个参数都有默认值,像 band 默认 12,落到长波红外通道。表里面我把最常用的几个参数整理了一下:
| 参数 | 类型 | 说明 |
|---|---|---|
| --input | str | HDF5 文件路径 |
| --band | int | 波段号,不同通道对应不同物理含义 |
| --output | str | 输出图片路径 |
| --mask-earth | bool | 是否裁剪地球圆盘外的无效区 |
这里尤其要注意 --band 和通道物理含义的对应。可见光通道适合白天;水汽通道看高空风场;长波红外全天可用但分辨率相对低。出图时需要根据目标选择。
4.3 结果验证:不要只看图“像不像”
跑出第一张图后,先不要急着调色。我会用一小段代码验证数值合理性:
bt = dn_to_radiance(...) print("亮温范围: %.2f ~ %.2f 摄氏度" % (np.nanmin(bt), np.nanmax(bt)))如果波段是 12,也就是 10.4 微米左右,夏季晴空地表亮温一般在 10 到 40 摄氏度,云顶亮温可以低到 -60 摄氏度。看到这个范围,基本能确认定标系数用对了。再进一步,可以拿经纬度查找表叠加地图海岸线,确认定位没有整体偏移。具体做法是把 lon、lat 和亮温数组用散点画在地图上,若海岸线完全对不上,多半是查找表读取路径或降采样对齐出了问题。
5. 常见问题排查与避坑记录:五个最容易翻车的点
5.1 HDF5 文件打不开或报 EOF 错误
现象:h5py.File 打开文件后直接抛 OSError,说文件不是 HDF5 格式或文件结尾异常。原因就两种,一是文件下载不完整,二是在 Windows 上把 7z 解压后拿到的文件被二次改名破坏了格式。解决:先看文件大小是否和原始数据站点一致;再确认文件头是不是 \x89HDF;如果是从 FTP 断点下载的,重新下完整文件。这类问题看似低级,但占了我遇到的数据读取问题三成以上。
5.2 亮温数值全部异常,出现几万开尔文
现象:图像能出,但亮温最大值是 30000 多,明显不物理。原因基本是漏乘 scale 或 offset。L1 数据的 DN 值是 16 位整型,范围 0 到 65535,如果直接把 DN 当作辐射亮度代进普朗克公式,结果会爆炸。解决:仔细检查定标系数读取逻辑,确认是从文件属性还是从外部参数表读取。我踩过一次坑,是因为把 scale 和 offset 的顺序搞反了,先加后乘,结果低值区域全变成负辐射值,亮温变成负几千度。从那以后,我每换一种新产品都会先手动验证一个像素的换算过程。
5.3 图像左右颠倒或经纬度错位
现象:云图整体看起来正常,但叠加地图后大陆轮廓完全对不上,或者图像像在镜子里看一样左右反了。原因是查找表的起始扫描方向和数据数组的行列方向不一致,不同版本的产品可能不一样。解决:先做一个极简验证,找一个容易识别的海岸线像素,读它的经纬度,再把它画在地图上对比。全圆盘图常用 np.fliplr 或 np.flipud 修正方向。记住一个原则:在配准这一步多做一次可视化验证,永远比后面处理完再发现错误划算。
5.4 一次加载多个通道把内存吃满
现象:同时读 14 个通道做合成,机器直接卡死。原因是每个通道都是 float64 数组,全圆盘数据 14 个波段叠一起对内存压力很大。解决:不要一次性把全部波段读进内存。按需读取,先明确到底需要哪几个波段做合成,处理完一个通道立刻释放引用。若必须全波段分析,可以采用分块读取:用 h5py 的 memmap 或 dask 数组按块读入,只做必要计算并累积统计量。参数上,我会在 config.yaml 里加一个 process_channels 列表,白名单之外的通道一律不读。
5.5 多通道拼接出“花斑图”,不同波段之间对不齐
现象:用可见光和长波红外做 RGB 合成,云图边缘出现红蓝重影。原因是不同波段空间分辨率不一致,FY-4A 部分通道分辨率存在差异,直接按像素序号拼就会错位。解决:先统一分辨率。以最高分辨率通道为基准,其余通道用双线性插值重采样到同一尺寸。代码实现可以交给 scipy.ndimage.zoom,也可以用 PyTorch 的 interpolate,但注意保持经纬度查找表不变。重采样后做一次像素级对比,保证轮廓对齐,再进合成流程。
6. 二次开发:把读取代码变成批量处理工具
代码包本身是一个可运行的 Demo,但它真正值钱的地方在于可以被快速改造成生产管线。我自己的习惯是拿到这种项目先做三件事:批量扫描、GeoTIFF 输出、配置外置。
批处理改造很简单。把单文件入口包成一个函数,参数是 input_path 和 output_path,然后用 os.walk 扫描目录,把所有 .HDF 文件全部处理一遍:
import glob files = glob.glob("./data/*.HDF") for f in files: proc_one(f, cfg)若要对接 GIS 工具,输出 GeoTIFF 是最常用的方向。借助 rasterio,把亮温数组写到磁盘时,需要把经纬度查找表写入地理变换信息:
import rasterio with rasterio.open("bt.tif", "w", driver="GTiff", height=bt.shape[0], width=bt.shape[1], count=1, dtype="float32") as dst: dst.write(bt.astype("float32"), 1)这里把 float64 转成 float32 能省一半磁盘空间,对云图产品足够。配合 PyYAML 的配置体系,整个工具就变成一个可以接入定时任务的批处理管道,输入目录下一有新文件,处理完自动在 output 里新增一张带坐标的云图。
还记得我第一次拿到某批 FY-4A 测试数据时,因为想当然地写死了数据集路径,结果换了文件版本就报 KeyError。从那以后,我每次拿到一种新卫星数据,哪怕再急,也会先花十分钟把数据集列表和属性打印出来看一遍,再决定要不要写死任何路径,这个习惯帮我避开了不少坑。这份代码包直接下载解压后,对照上面的流程就能跑出第一张带经纬度的云图,希望帮到你。
本文还有配套的精品资源,点击获取