Python GDAL 处理 MODIS ET 数据:从8天合成到月尺度的科学加权方法

发布时间:2026/6/19 16:57:51
Python GDAL 处理 MODIS ET 数据:从8天合成到月尺度的科学加权方法 1. MODIS ET数据与时间尺度转换的挑战处理MODIS蒸散发数据ET时最让人头疼的就是时间尺度转换问题。官方提供的MOD16A2GF数据是8天合成产品一年共有46景数据。但很多水文气象分析需要月尺度数据这就涉及到如何科学地将8天数据聚合到月尺度。我最初以为这是个简单问题上网一搜却发现现有方法大多采用粗暴的平均值或最大值合成。比如某个月有4景数据就直接取这4景的平均值作为月值。这种方法忽略了两个关键问题一是8天数据可能存在跨月情况二是不同时间段对月份的贡献权重不同。举个例子1月25日到2月1日这8天的数据有7天属于1月只有1天属于2月直接把它归入1月或2月都不合理。2. 数据处理前的准备工作2.1 数据获取与格式转换MOD16A2GF数据最初是HDF格式需要先转换为更易处理的GeoTIFF格式。我推荐两种转换方式使用NASA官方的HEG工具转换用GDAL的gdal_translate命令转换# 使用GDAL转换HDF到TIFF gdal_translate HDF4_EOS:EOS_GRID:MOD16A2GF.A2003001.h28v06.061.2021347204055.hdf:MOD_Grid_MOD16A2GF:ET_500m output.tif转换后还需要进行重投影和裁剪。我常用的是gdal.Warp函数from osgeo import gdal # 重投影和裁剪示例 input_file input.tif output_file output_projected.tif gdal.Warp(output_file, input_file, dstSRSEPSG:4326, outputBounds[min_lon, min_lat, max_lon, max_lat], xRes0.0045, yRes0.0045) # 约500m分辨率2.2 理解MODIS日期编码MODIS使用年年积日的日期编码方式这是很多新手容易出错的地方。比如2003001表示2003年第1天1月1日2003025表示2003年第25天1月25日在Python中我们可以这样解析日期import datetime modis_date 2003025 date_obj datetime.datetime.strptime(modis_date, %Y%j) print(date_obj) # 输出2003-01-25 00:00:003. 科学加权方法的实现3.1 时间权重分配原理我的加权方法核心思想是根据每景8天数据在各个月份中实际覆盖的天数来分配权重。具体步骤识别跨月影像检查每景数据的起始日期计算跨月部分的天数占比按比例分配ET值到相应月份举个例子2003年1月25日-2月1日这景数据1月覆盖天数7天1月25-31日2月覆盖天数1天2月1日权重分配1月占7/82月占1/83.2 Python实现代码详解以下是核心代码实现我添加了详细注释import os import re import glob import datetime import numpy as np from osgeo import gdal class RasterProcessor: def __init__(self): gdal.AllRegister() def read_raster(self, filename): 读取栅格数据 dataset gdal.Open(filename) if not dataset: raise ValueError(f无法打开文件: {filename}) # 获取栅格信息 cols dataset.RasterXSize rows dataset.RasterYSize bands dataset.RasterCount geotrans dataset.GetGeoTransform() proj dataset.GetProjection() # 读取数据为numpy数组 data dataset.ReadAsArray() del dataset # 及时释放内存 return rows, cols, bands, geotrans, proj, data def write_raster(self, filename, proj, geotrans, data): 写入栅格数据 driver gdal.GetDriverByName(GTiff) # 确定数据类型 if data.dtype np.float32: dtype gdal.GDT_Float32 else: dtype gdal.GDT_Int16 # 创建数据集 if len(data.shape) 3: bands, rows, cols data.shape else: bands, rows, cols 1, *data.shape dataset driver.Create(filename, cols, rows, bands, dtype) dataset.SetGeoTransform(geotrans) dataset.SetProjection(proj) if bands 1: dataset.GetRasterBand(1).WriteArray(data) else: for i in range(bands): dataset.GetRasterBand(i1).WriteArray(data[i]) del dataset def get_month_days(year, month): 获取某个月的天数信息 first_day datetime.date(year, month, 1) if month 12: next_month datetime.date(year1, 1, 1) else: next_month datetime.date(year, month1, 1) last_day next_month - datetime.timedelta(days1) return first_day, last_day, last_day.day def process_monthly_et(input_dir, output_dir, start_year2003, end_year2014): 处理8天数据到月尺度 processor RasterProcessor() os.makedirs(output_dir, exist_okTrue) # 获取所有输入文件 input_files glob.glob(os.path.join(input_dir, *.tif)) for year in range(start_year, end_year1): for month in range(1, 13): monthly_data [] print(fProcessing {year}-{month:02d}) # 获取当月第一天和最后一天 first_day, last_day, days_in_month get_month_days(year, month) # 收集当月所有影像 for file_path in input_files: # 从文件名提取日期 match re.search(rMOD16A2GF\.A(\d{7}), os.path.basename(file_path)) if not match: continue modis_date match.group(1) date datetime.datetime.strptime(modis_date, %Y%j).date() # 检查是否属于当前处理月份 if date.year year and date.month month: # 初始权重设为1完整属于当月 monthly_data.append([file_path, 1.0]) # 处理跨月影像 if monthly_data: # 检查首景是否跨上月 first_file_date datetime.datetime.strptime( re.search(rMOD16A2GF\.A(\d{7}), os.path.basename(monthly_data[0][0])).group(1), %Y%j).date() if first_file_date.day ! 1: # 跨月影像 # 计算在上月的天数 prev_month_last_day first_file_date - datetime.timedelta(days1) days_in_prev_month (first_file_date - prev_month_last_day.replace(day1)).days # 调整权重 monthly_data[0][1] (8 - days_in_prev_month) / 8 # 检查尾景是否跨下月12月除外 if month ! 12: last_file_date datetime.datetime.strptime( re.search(rMOD16A2GF\.A(\d{7}), os.path.basename(monthly_data[-1][0])).group(1), %Y%j).date() next_day last_file_date datetime.timedelta(days7) if next_day.month ! month: # 跨月影像 days_in_current_month (last_day - last_file_date).days 1 monthly_data[-1][1] days_in_current_month / 8 # 计算月累计ET if monthly_data: # 初始化结果数组 sample_data processor.read_raster(monthly_data[0][0])[-1] result np.zeros_like(sample_data, dtypenp.float32) # 加权累加 for file_path, weight in monthly_data: _, _, _, _, _, data processor.read_raster(file_path) data data.astype(np.float32) # 处理无效值 data[(data -32767) | (data 32700)] np.nan # 应用权重 result data * weight # 应用尺度因子 result * 0.1 # MOD16A2GF的尺度因子 # 保存结果 output_file os.path.join(output_dir, fMOD16A2GF_ET_{year}_{month:02d}.tif) _, _, _, geotrans, proj, _ processor.read_raster(monthly_data[0][0]) processor.write_raster(output_file, proj, geotrans, result) print(所有月份处理完成)4. 实际应用中的注意事项4.1 数据质量控制MOD16A2GF数据包含质量评估(QA)波段理想情况下应该先进行质量控制。虽然我的代码中暂时没有实现QA处理但建议在实际应用中加入以下步骤读取QA波段根据官方文档解析QA值剔除质量差的数据点# 伪代码示例 qa_value qa_band_data[y, x] if (qa_value 0x03) ! 0: # 根据QA掩码判断 et_data[y, x] np.nan # 标记为无效值4.2 边缘月份处理处理时间序列的首尾月份时需要特别注意第一年1月可能缺少前一年的12月数据最后一年12月可能缺少下一年的1月数据跨年数据确保日期计算正确4.3 性能优化建议处理长时间序列数据时内存和计算效率很重要。几个优化建议使用分块处理大数据并行处理不同年份数据及时释放GDAL数据集内存# 分块处理示例 for i in range(0, rows, block_size): for j in range(0, cols, block_size): block data[i:iblock_size, j:jblock_size] # 处理数据块4.4 结果验证方法为确保转换结果正确建议进行以下验证检查权重分配是否正确对比原始8天数据和月数据的统计特征可视化检查空间分布是否合理与站点观测数据或其他产品交叉验证我在实际项目中遇到过权重计算错误的情况导致某些月份的ET值异常偏高。后来通过输出中间权重值逐步排查发现是跨月天数计算有误。这个教训告诉我处理时间序列数据时必须仔细验证每个中间步骤。