简介:中分辨率成像光谱仪(MODIS)二〇二二年全球零点零五度归一化植被指数(NDVI)栅格数据集,是依据美国国家航空航天局(NASA)公开发布的MOD13C2 v061月度产品加工而成的年度全球遥感数据。这份数据通过提取子数据集、投影栅格、单位换算与逐月最大合成法处理,得到WGS84坐标系下的年度全球NDVI影像,可直观反映全球陆表植被覆盖与绿度时空分布;在生态遥感监测、气候变化响应分析、农业长势评估、区域干旱监测以及相关教学科研中均有实用价值。资源包共六个文件,以GeoTIFF栅格(tif)为核心,包含tfw坐标参考、ovr金字塔概览、xml与txt元数据说明,整体约二十五点五八MB,下载后可在ArcGIS、QGIS等主流GIS软件中直接加载。目前已有五百零一人学习下载,配合ovr和xml文件可高效预览与读取,txt文档交代了数据来源和处理流程,无需重复预处理即可直接用于制图、统计或模型输入,适合需要标准化全球年度NDVI数据进行研究或项目开发的用户。
1. 全球 2022 年 NDVI 栅格:一张 0.05 度的年度植被快照
做植被遥感的人,每年初都会被同一件事卡住:想拿上一自然年的全球 NDVI 做年度对比,从 NASA 一层层点进去,下载回来却是几十个 HDF,打开全是灰度,根本不知道哪一层能用。这份 MODIS 2022 年全球 0.05 度植被指数(NDVI)栅格数据集,恰恰是替你把这段弯路走完了——它把 NASA MOD13C2 v061 月度产品做了提取子数据集、投影栅格、单位换算,再用最大合成法合并成一张年度 GeoTIFF:0.05 度分辨率、WGS84 地理坐标、覆盖全球。适合做洲际到全国尺度的植被变化、干旱监测和物候分析;不适合做乡镇、地块级别的精细评估,那是 250m 产品的活。
2. 数据溯源与文件解剖:MOD13C2 v061 到 GeoTIFF 的五份关键文件
2.1 MOD13C2 v061:0.05 度 CMG 网格本身意味着什么
MOD13C2 是 NASA 陆面过程分布式数据档案中心(LP DAAC)发布的 MODIS/Terra 植被指数月合成产品,v061 是当前维护的主力版本。C 结尾代表 CMG(Climate Modeling Grid),也就是气候模拟网格,全球按 0.05 度间隔切分成 3600 行、7200 列。一个像元在赤道附近约 5.5 公里,到中纬度约 4 公里出头,做全球一张图、洲际趋势、全国尺度统计,这个分辨率足够稳,文件体积又不像 500m 产品那样压垮内存。
摘要里那句引用已经写清楚了产品全名:MODIS/Terra Vegetation Indices Monthly L3 Global 0.05Deg CMG V061,DOI 对应 10.5067/MODIS/MOD13C2.061。需要说明的是,MOD13C2 原始文件是 HDF 格式,一个文件里同时装着 NDVI、EVI、VI 质量、可靠性等多个子数据集,直接拖进 GIS 看到的往往是灰度图或伪彩色乱七八糟。作者做的第一步就是从中抽出 NDVI 子数据集,再换算单位、写投影。这意味着你拿到的这份 tif 已经是“可食用”的成品,而不是那种需要自己折腾 HDF 的原料。
原始 MOD13C2 的 NDVI 用 int16 存储,官方标定系数是 0.0001。换句话说,文件里存的数字 5321,真实 NDVI 是 0.5321;存的是 -2000,真实值就是 -0.2。有效范围 -2000 到 10000,对应 NDVI -0.2 到 1.0。填充值用 -32768,像素在极夜、云覆盖等情况下会出现。这些数值上的细节直接决定了后续所有统计脚本怎么写,后面第 3 章会演示标准处理流程。
2.2 解压后的五份文件各顶什么用
zip 解压出来是主 tif 加四个配套文件,很多人只把 tif 拷走,后面在别的软件里加载出问题,回头才发现少带了东西。
| 文件 | 类型 | 作用 | 处理时是否必需 |
|---|---|---|---|
| global_2022_NDVI0.05.tif | GeoTIFF 主文件 | 年度 NDVI 栅格,核心数据 | 必需 |
| global_2022_NDVI0.05.tfw | 世界文件 | 文本形式的坐标定位,6 行数字 | 依赖 tfw 的软件必需 |
| global_2022_NDVI0.05.tif.aux.xml | GDAL 辅助 XML | 投影 WKT、统计、色彩解释信息 | 建议保留 |
| global_2022_NDVI0.05.tif.xml | ISO 元数据 XML | 数据来源、处理过程、引用规范 | 存档用 |
| global_2022_NDVI0.05.tif.ovr | 金字塔文件 | 加速大图缩放显示 | 显示优化 |
tfw 值得单独说。它存着六个数字,本质是一个二维仿射变换。这份全球数据的常见形态是:
0.05 0 0 -0.05 -180 90第一行是 X 方向像元尺寸,第四行是 Y 方向像元尺寸,负号表示影像行方向自上而下;第五、六行是左上角像元坐标。第 2、3 行是旋转项,这里都是 0,说明栅格没有被旋转过,严格按经纬度对齐。这个“无旋转、左上角 -180/90”的信息非常有用:如果你自己拿 gdalwarp 重投影后生成的 tfw 里出现了非零旋转项,说明输入数据的网格没对齐,要先回查数据源。
aux.xml 里存了 min/max、mean、stddev 和直方图,GIS 首次加载时会直接读取,避免你一打开看到全黑然后怀疑数据坏了。ovr 是金字塔,没有它,缩放到全国范围会卡顿明显;有了它,渲染器只读概览层。注意:如果你后续对 tif 做了裁剪或重投影,旧的 ovr 会失效,务必用 gdaladdo 重建,否则放大会出现黑块。这个问题我放在第 5 章避坑里细说。
提示:命令行自动化处理时,tif + tfw + aux.xml 三个要成套保留。有的程序只认 tfw,有的只认 tif 内嵌坐标,少任何一个都可能出现坐标错位,而且这类错位用肉眼很难第一时间发现。
3. 从 HDF 到年度 NDVI:子数据集提取、网格对齐与 MVC 最大合成实战
3.1 提取 MOD13C2 的 NDVI 子数据集并完成单位换算
MOD13C2 原文件是 HDF4 格式,打开后不是一张图,而是多个子数据集。要用程序处理,第一步就是遍历子数据集,找到名字结尾是 NDVI 的那一层。常见做法是用 GDAL 的 GetSubDatasets 方法:
from osgeo import gdal import numpy as np hdf_path = "MOD13C2.A2022001.061.2022078.2022079.001.hdf" src = gdal.Open(hdf_path) if src is None: raise RuntimeError("无法打开 HDF,确认 GDAL 编译了 HDF4 驱动") ndvi_path = None for sub in src.GetSubDatasets(): if sub[0].split(":")[-1] == "NDVI": ndvi_path = sub[0] break if ndvi_path is None: sys.exit("未找到 NDVI 子数据集") sub = gdal.Open(ndvi_path) ndvi_int = sub.ReadAsArray() # int16 原始值 ndvi = ndvi_int.astype(np.float32) * 0.0001 fill_mask = ndvi_int == -32768 ndvi[fill_mask] = np.nan print("NDVI 有效值范围:", np.nanmin(ndvi), np.nanmax(ndvi))这段代码的核心是单位换算和填充值掩膜两步。乘 0.0001 是因为 MOD13C2 用 int16 存整数,真实 NDVI = 存储值 × 0.0001;填充值 -32768 换算后变成 -3.2768,必须在缩放之后单独识别并转成 NaN,否则会以假值混进统计。实际落盘时我会把 NaN 转成 -9999 再写 GeoTIFF,因为大部分模型和 GIS 对负值 nodata 的处理比 NaN 稳定。
判断子数据集名称时,sub[0].split(":")[-1] 拿到的是子数据集短名,不同文件可能叫 NDVI 也可能带前缀,更稳妥的做法是把整个子数据集列表打印出来人工确认一次,再写死索引。批量处理几十个月文件时,这个确认环节能省掉后面大量返工。
3.2 对齐 0.05 度全球网格:GeoTransform、投影信息与 tfw 重写
摘要里提到的“投影栅格”这一步,对 MOD13C2 来说不是把经纬度投影成 UTM,而是把子数据集自带的投影和地理变换完整写进输出 GeoTIFF,并让输出严格落在 0.05 度网格上。这一步做不好,后续逐月数据叠在一起会错位半个像元,最大合成后出现条带伪影。
def align_to_grid(gt, pixel=0.05): x0 = np.floor(gt[0] / pixel) * pixel y0 = np.ceil(gt[3] / pixel) * pixel return (x0, pixel, 0.0, y0, 0.0, -pixel) gt_old = sub.GetGeoTransform() gt_new = align_to_grid(gt_old, 0.05) out_ds.SetGeoTransform(gt_new) out_ds.SetProjection(sub.GetProjection())align_to_grid 把左上角坐标吸附到最近的 0.05 度整数倍上,返回的六元组里第二项和第六项是 X/Y 方向像元大小,正负号方向固定。这样做的好处是:1 月到 12 月每一期的像元边界完全重合,后面做 MVC 时逐像元取最大值不会因网格错位引入噪声。
命令行等价写法是 gdal_translate,重点在几个创建选项:
gdal_translate -ot Float32 -a_nodata -9999 -co TILED=YES -co COMPRESS=DEFLATE \ "HDF4_EOS:EOS_GRID:...:NDVI" ndvi_202201.tif-ot Float32 把 int16 转成浮点,-a_nodata -9999 统一填充值,-co TILED=YES 让 tif 采用分块存储,网络磁盘和云对象存储上读取更快,-co COMPRESS=DEFLATE 无损压缩,全球栅格能省不少空间。如果还需要 tfw,加一条 -co TFW=YES,GDAL 会顺手写出和 tif 内嵌信息一致的坐标文件,避免手工写错。
3.3 MVC 最大合成法:为什么取最大值而不是均值
云和大气气溶胶会让 NDVI 显著偏低,这是植被遥感的基础认知。对同一个像元来说,一年里拿到 12 个月观测,其中真正接近无云状态的观测,NDVI 大概率是最高值之一。最大合成法(Maximum Value Composite,MVC)就是逐像元取多时相最大值,用年内最高 NDVI 近似植被生长最旺盛那一刻。这是全球 NDVI 产品几十年的标准做法,比简单平均抗云污染得多。
import glob import numpy as np from osgeo import gdal filelist = sorted(glob.glob("ndvi_2022_*.tif")) if len(filelist) != 12: print("警告:只找到%d个月文件,年份可能不完整" % len(filelist)) stack = [] for f in filelist: ds = gdal.Open(f) band = ds.GetRasterBand(1) data = band.ReadAsArray().astype(np.float32) nodata = band.GetNoDataValue() if band.GetNoDataValue() is not None else -9999 data[data == nodata] = np.nan stack.append(data) gt = ds.GetGeoTransform() proj = ds.GetProjection() cube = np.stack(stack, axis=0) # 形状 [12, rows, cols] annual = np.nanmax(cube, axis=0) # 逐像元最大合成 valid_count = np.sum(~np.isnan(cube), axis=0) # 逐像元有效观测数 annual[valid_count < 3] = np.nan # 有效观测少于3个月不输出 annual[np.isnan(annual)] = -9999 out = gdal.GetDriverByName("GTiff").Create("global_2022_NDVI0.05.tif", cube.shape[2], cube.shape[1], 1, gdal.GDT_Float32) out.SetGeoTransform(gt) out.SetProjection(proj) out.GetRasterBand(1).WriteArray(annual) out.GetRasterBand(1).SetNoDataValue(-9999) out = Nonelp 关键参数是有效观测数阈值。我默认写成 3,这是给中高纬度留的余量——极夜期连续几个月没有有效观测,阈值设 6 以上北极圈会整片空白;设太低,像元只靠一两个月拼出来,代表不了“年度”。热带地区可以提高到 6,寒带用 3 更稳。np.nanmax 和 np.max 的区别在于前者忽略 NaN,如果图省事用 np.max,一个像元只要有一个月缺测,全年结果就跟着消失。最后落盘前把 NaN 转 -9999,后续模型读取时统一按 nodata 处理,比处理 NaN 简单得多。
4. 数据能干什么:NDVI 转 FVC 植被覆盖度与产品选型对比
4.1 2022 年全球 NDVI 的典型应用场景
这张年度 NDVI 最直接的用途是区域对比:非洲之角干旱、中亚草原退化、亚马逊雨林异常,这些 2022 年的植被信号都会在年度 NDVI 上留下痕迹。0.05 度分辨率做洲际制图完全够用,整张全球图加载也不至于卡死。叠加行政区边界后,可以按省、按流域统计平均 NDVI,和多年均值做距平,判断这一年植被是偏绿还是偏干。
要注意的是年度合成把季节变化压缩掉了,做物候分析必须回去用月度数据。比如华北冬小麦的返青和收割在年度 NDVI 里只有“一年最高”这一个数字,看不出生长节律;想监测生长季长度、峰值时间这类指标,这份年度数据帮不上忙。我一般把它定位成“快速摸底”数据:先看全球哪里出了问题,再针对性下更高分辨率的 MOD13A1 或 MOD13Q1 细看。
另一个常用场景是掩膜和分区统计的底图。做土地利用分类、荒漠化评估时,先把 NDVI 低于某个阈值的像元筛掉,能省很多事。0.05 度全球一张图,内存占用可控,用 Python 读一次全图也就几百 MB,适合批量处理多个年份。
4.2 NDVI 转 FVC:植被覆盖度计算的标准操作
最近总有人问“NDVI 怎么算植被覆盖度”,这个 ndvi 计算 fvc 的操作是植被遥感里的标准流程。思路是假设一个像元由裸土和植被两种端元线性混合,FVC 就是 NDVI 在土壤背景和浓密植被之间的相对位置。公式本身很简单,坑在端元怎么取。
import numpy as np from osgeo import gdal src = gdal.Open("global_2022_NDVI0.05.tif") band = src.GetRasterBand(1) ndvi = band.ReadAsArray().astype(np.float32) nodata = band.GetNoDataValue() ndvi[ndvi == nodata] = np.nan valid = ndvi[~np.isnan(ndvi)] ndvi_soil, ndvi_veg = np.percentile(valid, 5), np.percentile(valid, 95) fvc = (ndvi - ndvi_soil) / (ndvi_veg - ndvi_soil) fvc = np.clip(fvc, 0, 1) fvc[np.isnan(fvc)] = -9999 print(f"端元取值 soil={ndvi_soil:.3f}, veg={ndvi_veg:.3f}") print(f"FVC范围: {np.nanmin(fvc):.3f} ~ {np.nanmax(fvc):.3f}")取 5% 和 95% 分位数是统计驱动做法,好处是不依赖先验知识,坏处是全球数据连在一起统计时,沙漠和热带雨林互相污染,可能把土壤端元拉得过低、植被端元拉得过高。做省域或小流域时,我更习惯直接用典型端元值:裸土 NDVI 约 0.1 到 0.2,高覆盖植被约 0.8 到 0.9,再对结果裁剪到 0 到 1。不想写代码的话,ArcGIS 栅格计算器里Con(IsNull(ndvi), -9999, (ndvi - 0.1) / (0.8 - 0.1))也是一样的效果,关键是水体像元要先掩掉,否则负值会被 clip 成 0,看着不脏实际已经污染统计。
4.3 与 MOD13A1、MYD13C2 的选型差异
手边同时备着几类 MODIS 植被指数产品是常态,但选错产品会直接带偏结论。我这里按自己实际使用频率列一张对比表:
| 产品 | 卫星 | 空间分辨率 | 时间分辨率 | 适合尺度 | 典型局限 |
|---|---|---|---|---|---|
| MOD13C2 v061 | Terra | 0.05°(约5km) | 月 / 年合成 | 全球、洲际、全国 | 局地细节不足 |
| MOD13A1 v061 | Terra | 500m | 16 天 | 省域、流域 | 全球处理数据量大 |
| MYD13C2 v061 | Aqua | 0.05°(约5km) | 月 | 全球,与Terra互补 | 与MOD13C2信号高度相关 |
| MOD13Q1 v061 | Terra | 250m | 16 天 | 县域、地块 | 噪声大、需要认真滤波 |
我的判断标准是:只做 2022 年全球一张图,首选这份年度 0.05 度数据;做全国尺度且想看季节转折,用月度 0.05 度,而不是把年度图硬插值回季节;做田间尺度的长势监测,老老实实去下 500m 的 MOD13A1。业务里见过不少人拿 5km 数据做乡镇分析,结论被混合像元带偏,最后返工换数据——这是选型问题,不是数据质量问题。
5. 避坑指南:我在 0.05 度年度 NDVI 上踩过的五个坑
5.1 一打开全黑或数值飘到五位数:scale factor 没除干净
现象:在 ArcGIS 或 QGIS 里加载 tif,栅格拉伸后画面全黑或花白,点查询显示像元值 5312。
原因:这份 GeoTIFF 是已经换算过单位的成品,NDVI 正常值域在 -0.2 到 1.0 之间。显示 5312 这类大数,一般是拿着 MOD13C2 原始 int16 存储值直接展示,没有乘 0.0001 标定系数。
解决:先看随包 aux.xml 里的统计信息,再用 gdalinfo 确认一次:
gdalinfo -stats global_2022_NDVI0.05.tif如果最大值大于 1.0,说明数据本身是整数存储,回到第 3 章的换算流程重新处理;如果统计正常,只是显示问题,在图层属性里手动把拉伸范围设为 -0.2 到 1.0 即可。注意不要对已经换算好的 float 栅格再跑一次-scale -2000 10000 -0.2 1.0,那会把负值区域整体拉伸错位。
5.2 tfw 与内嵌坐标对不上:加载后要素错位半像元
现象:tfw 和 tif 放在同一目录,GIS 里叠到行政边界上,边界与 NDVI 栅格错开约 0.025 到 0.05 度,放大后像元边界呈斜格。
原因:tfw 世界文件只描述仿射变换,不同软件对“tfw 记录的是像元中心还是角点”这类细节有历史差异。更多时候是有人用另一份分辨率相同但左上角坐标不同的 tfw 覆盖了原文件。
解决:以 GeoTIFF 内嵌的 GeoTransform 为准,不要随手生成同名 tfw 覆盖原文件。命令行验证:
gdalinfo global_2022_NDVI0.05.tif | grep -E "Origin|Pixel Size"确认内嵌坐标正确后,删除或忽略同名 tfw 即可;反过来,如果旧软件只认 tfw,就把内嵌信息原样抄写成 tfw,保证两处一致。从那以后我每次处理栅格都会在开头打印一次这一行,省掉很多“看起来没错但叠不上”的玄学问题。
5.3 高纬度年度结果出现大片空洞或全填 -9999
现象:北极圈内、格陵兰边缘等区域,2022 年度 NDVI 整片是 nodata,面积统计时缺掉一大块。
原因:极夜期连续几个月无有效观测。如果 MVC 前没有统计有效观测数,直接用 np.max 或 np.nanmax 处理,缺测月份的 NaN 要么吞掉全年结果,要么该像元全为 NaN 后落盘变成 nodata。
解决:MVC 时先算有效观测数,低于阈值直接标记为无效:
valid_count = np.sum(~np.isnan(cube), axis=0) annual = np.nanmax(cube, axis=0) annual[valid_count < 3] = np.nan阈值按纬度调整:中低纬度 6 个月,高纬度 3 个月。如果研究区就是高纬度,建议改用生长季(4 月到 9 月)合成,信号更干净,也不容易被极夜期的空洞干扰。
5.4 全局分位数算 FVC,把水体当植被覆盖了
现象:用 NDVI 转 FVC 后,海洋附近、大湖区域的植被覆盖度达到 30% 甚至更高,明显不符合常识。
原因:水体 NDVI 常为负值,全局 5% 分位数端元把大量水体噪声算进了土壤端元,整个 FVC 基线被抬高。公式只做了 clip,没有先做水体掩膜。
解决:FVC 计算前先把 NDVI 小于 0.1 的像元直接置为 0,再做 clip:
fvc = np.where(ndvi < 0.1, 0.0, fvc)先置 0 再 clip 的次序很重要。先 clip 会让 0 到 0.1 之间的小正数保留下来,看着不脏,实际仍是水体噪声。更严格的做法是用 MODIS 自带的水体掩膜波段,但 0.1 阈值在多数区域已经够用。
5.5 金字塔过期:切图黑块或加载极慢
现象:缩放到全国图层显示正常,放大到华南局部区域出现黑色斑块;做地图切片时耗时飙升。
原因:随包的 .ovr 是原始打包时生成的。对 tif 做过裁剪、重投影或填 nodata 之后,旧金字塔失效,切片软件会反复读取底层原始数据。
解决:处理完数据后重建金字塔:
gdaladdo -r average global_2022_NDVI0.05.tif 2 4 8 16参数里 2 4 8 16 表示在底层之上生成 4 层概览,-r average 用平均重采样,对 NDVI 这类连续值比最近邻更平滑。处理完的 tif 再进服务发布流程,黑块问题基本绝迹。
6. 手动收尾技巧:裁剪到研究区并在三分钟内完成量纲体检
6.1 用 gdalwarp 把全球栅格切到目标范围
全球 7200 × 3600 个像元,任何分析批量跑起来都偏重,多数时候要先裁剪到研究区。以中国范围为例:
gdalwarp -te 73 3 135 53 -te_srs EPSG:4326 -tr 0.05 0.05 \ -r near -dstnodata -9999 global_2022_NDVI0.05.tif china_2022_ndvi.tif-te 后面四个数字是 minx miny maxx maxy,顺序固定;-te_srs 声明这四个数字的坐标系;-tr 0.05 0.05 强制输出像元大小与源数据一致,防止 gdalwarp 自动改分辨率;-r near 在平移裁剪场景下最保真,不需要做插值。如果只切矩形范围,gdal_translate 更轻:
gdal_translate -projwin 73 53 135 3 -projwin_srs EPSG:4326 \ global_2022_NDVI0.05.tif china_clip.tif注意 -projwin 的顺序是 ulx uly lrx lry,即左上角经纬度和右下角经纬度,和 gdalwarp 的 -te 顺序正好相反,第一次用基本都会写反,写反的后遗症就是裁剪结果空白。
6.2 量纲体检:三分钟确认 NDVI 数值可信
数据进模型前,我习惯跑一遍统计体检,脚本很短但信息量很大:
from osgeo import gdal import numpy as np ds = gdal.Open("china_2022_ndvi.tif") b = ds.GetRasterBand(1) arr = b.ReadAsArray().astype(np.float32) nodata = b.GetNoDataValue() valid = arr[arr != nodata] print("行数列数:", arr.shape) print("min/max:", float(valid.min()), float(valid.max())) print("负值占比:", float((valid < 0).mean())) print("有效像元占比:", float((arr != nodata).sum() / arr.size))判断标准很简单:min 小于 -0.3 或者 max 大于 1.01,基本可以判定换算或填充值处理出错;负值占比在 10% 到 40% 之间正常,沙漠、水体、云残留都会拉低 NDVI,如果负值占比超过 60%,要怀疑是不是把 EVI 当成 NDVI 用;有效像元占比明显低于 90%,检查裁剪范围是不是含了大量海洋,或者高纬度缺测太严重。
从那以后,我每拿到一份栅格数据,都强制先跑一遍 gdalinfo 和量纲统计,确认 min/max、nodata、投影网格都对上了再进后续模型。数据源头错了,后面无论怎么调参数都是白做,这是我在几份“看起来没问题的 NDVI”上缴过的学费。希望帮到你。
本文还有配套的精品资源,点击获取