简介:本资源为2012—2020年NPP-VIIRS夜间灯光遥感数据的预处理成果,面向从事城市扩张、经济活动评估、碳排放与人口空间化等研究的高校师生及科研人员,可省去自行下载、裁剪与去噪的繁琐环节。数据分辨率500m,已用WGS84中国矢量边界裁剪出全国范围VNL2数据,并转换为Asia_Lambert_Conformal_Conic平面投影;针对负值及gas flares引起的极端异常值,参考Kang Wu等(2019)以472.86为全国像元辐射阈值进行剔除。压缩包共36个文件,以9个tif栅格为主体,配套tfw坐标文件、aux.xml元数据与ovr金字塔文件,整体约176.73MB,按年份命名便于逐年调用。目前已有3505人学习下载,适合直接用于长时间序列夜光分析、论文复现与空间建模,省去重复预处理成本。
1. 夜光遥感数据预处理:从2012到2020,这套NPP-VIIRS数据能帮你省掉多少清洗时间
如果你做过城市扩张、经济活动估算或者灾后重建评估,大概率绕不开夜光遥感。DMSP-OLS 从 2013 年停更之后,NPP-VIIRS 就成了主力。但真正上手的人都知道,原始 VIIRS 月度产品有一堆让人头疼的问题:背景噪声、极光污染、火光异常值、月度间辐射定标不一致,还有 2012 年 4 月到 2020 年之间传感器状态漂移带来的连续性断裂。这套「预处理后的 2012-2020 年 NPP-VIIRS 夜光遥感数据」就是冲着这些痛点来的——它把原始月度影像做了去噪、去异常、年际合成和跨年一致性校正,输出的是可以直接进分析管道的栅格数据。适合做长时间序列分析的研究生、做城市夜光指数建模的工程师,以及需要快速验证区域经济指标的从业者。你不用再从 NOAA 的原始归档开始啃,省下的时间够你多跑几轮回归。
2. 数据规格与预处理链路:为什么不是直接下原始月度产品
2.1 原始 VIIRS 月度产品的四个硬伤
NPP-VIIRS 的 VCMCFG 月度产品(vcmcfg)和 VCMSLCFG 版本,在 2012-2020 这个区间里,有几个绕不过去的问题。第一,2012 年和 2013 年的部分月份存在明显的背景噪声,尤其在低纬度地区,非城市像元的 DN 值被抬高,导致城市提取阈值失效。第二,极光带和油气田火炬在夏季高纬度地区会产生瞬时高亮像元,这些像元在月度合成里不会被完全剔除。第三,月度之间的辐射定标存在系统性差异,直接做时间序列会出现「台阶」——某个月突然整体偏亮或偏暗。第四,2017 年之后部分月份的杂散光校正版本切换,导致同一区域在不同年份的 DN 值不可比。
这套预处理数据针对的就是这四个问题。它没有直接给你原始月度文件,而是做了去噪、异常值掩膜、年际中值合成和跨年辐射归一化。你拿到的是年尺度的合成产品,像元值已经过一致性处理,可以直接做 2012-2020 的逐年对比。
2.2 预处理链路拆解:从月度到年际的五个步骤
常见做法是走这么一条链路,我按顺序拆开说:
第一步,背景噪声去除。对每个月度影像,用中值滤波加阈值分割把非城市像元的低值噪声压掉。具体阈值不是固定的,而是按纬度带和月份动态调整。低纬度用较低阈值,高纬度夏季用较高阈值,避免把极光残留当成城市。
第二步,异常值掩膜。火炬、渔船灯光、火山活动这些瞬时高亮像元,用「月度最大值与中值差异」来识别。如果某像元在某个月的 DN 值超过该像元全年中值的 3 倍标准差,就标记为异常,在年际合成时剔除。
第三步,年际中值合成。对每个年份,取该年所有可用月份的中值。中值比均值抗异常值,比最大值合成更能反映稳定的人类活动灯光。这一步会输出 2012 到 2020 共 9 个年度的合成影像。
第四步,跨年辐射归一化。以 2015 年为参考年,用伪不变区域(比如大型城市核心区)做线性回归,把其他年份的 DN 值校正到 2015 年的辐射尺度上。这一步是保证时间序列可比的关键。
第五步,无效值编码与元数据写入。水体、无数据区域统一编码为特定值(常见是 -9999 或 0),并在头文件里写入投影、分辨率、年份和预处理版本号。
2.3 输出格式与文件组织
这套数据通常以 GeoTIFF 格式提供,每年一个文件,命名规则类似NPP_VIIRS_Annual_2012.tif到NPP_VIIRS_Annual_2020.tif。投影一般是地理坐标系 WGS84,空间分辨率约 500 米。部分版本会额外提供裁剪后的区域子集和对应的质量标记层(QA layer),QA 层用位编码记录每个像元是否被异常值掩膜、是否做过归一化校正。
如果你拿到的包里有 QA 层,强烈建议在分析前先读 QA 层,把被标记的像元排除掉。很多人直接拿 DN 值跑回归,结果被火炬和极光残留带偏,还以为是模型问题。
提示:不同预处理版本对无效值的编码可能不同,拿到数据后先用
gdalinfo看一眼元数据,确认 NoData 值和投影信息。
3. 上手实操:用 Python 读取、裁剪和做时间序列分析
3.1 环境准备与依赖安装
我一般用rasterio加numpy处理这类栅格数据,geopandas用来做区域裁剪。如果你还没装,一条命令搞定:
pip install rasterio numpy geopandas matplotlibrasterio负责读写 GeoTIFF 和提取元数据,numpy做数组运算,geopandas用来读矢量边界做裁剪。版本上,rasterio1.3 以上对 GDAL 的兼容性比较稳,不建议用太老的版本。
3.2 读取单年影像并查看元数据
先读一年看看数据长什么样:
import rasterio import numpy as np # 打开2015年的年际合成影像 with rasterio.open("NPP_VIIRS_Annual_2015.tif") as src: # 读取第一个波段,通常是DN值 data = src.read(1) # 获取元数据 meta = src.meta # 获取NoData值 nodata = src.nodata # 获取地理变换参数 transform = src.transform # 获取投影 crs = src.crs print(f"影像尺寸: {data.shape}") print(f"NoData值: {nodata}") print(f"投影: {crs}") print(f"地理变换: {transform}") print(f"DN值范围: {data.min()} - {data.max()}")这段代码做了三件事:读取波段数据、提取元数据、打印关键参数。src.read(1)读的是第一个波段,如果文件有多个波段(比如 DN 加 QA),需要按波段索引分别读。src.nodata返回 NoData 值,常见是 -9999 或 0,后续做统计前要先把这些值排除。src.transform是仿射变换参数,用来把行列号转成经纬度。
3.3 按行政边界裁剪并统计区域夜光总量
假设你有一个研究区的矢量边界,想统计每年的夜光总量:
import rasterio from rasterio.mask import mask import geopandas as gpd import numpy as np # 读取研究区边界 region = gpd.read_file("study_area.shp") # 存储每年的夜光总量 yearly_sum = {} for year in range(2012, 2021): filepath = f"NPP_VIIRS_Annual_{year}.tif" with rasterio.open(filepath) as src: # 用矢量边界裁剪栅格 # crop=True 表示裁剪后只保留边界内区域 # nodata=src.nodata 保证裁剪后NoData值一致 clipped, out_transform = mask(src, region.geometry, crop=True, nodata=src.nodata) clipped_data = clipped[0] # 排除NoData值 valid_mask = clipped_data != src.nodata valid_data = clipped_data[valid_mask] # 计算夜光总量和均值 yearly_sum[year] = { "sum": float(valid_data.sum()), "mean": float(valid_data.mean()), "valid_pixels": int(valid_mask.sum()) } # 打印结果 for year, stats in yearly_sum.items(): print(f"{year}: 总量={stats['sum']:.0f}, 均值={stats['mean']:.2f}, 有效像元={stats['valid_pixels']}")mask函数是rasterio里做矢量裁剪的核心方法。crop=True会把输出栅格裁剪到边界的最小外接矩形,减少内存占用。nodata=src.nodata保证裁剪后边界外的像元被正确标记为 NoData,不会混进统计。valid_mask用来排除 NoData 像元,这一步很多人会漏掉,导致均值被 -9999 拉低。
3.4 做 2012-2020 夜光总量趋势图
拿到每年的统计值之后,画个趋势图看看:
import matplotlib.pyplot as plt years = list(yearly_sum.keys()) sums = [yearly_sum[y]["sum"] for y in years] fig, ax1 = plt.subplots(figsize=(10, 5)) # 绘制夜光总量趋势 ax1.plot(years, sums, marker='o', color='tab:orange', label='夜光总量') ax1.set_xlabel('年份') ax1.set_ylabel('夜光总量 (DN)') ax1.set_title('2012-2020年夜光总量变化趋势') ax1.grid(True, alpha=0.3) # 标注最大值和最小值 max_year = years[sums.index(max(sums))] min_year = years[sums.index(min(sums))] ax1.annotate(f'峰值 {max_year}', xy=(max_year, max(sums)), xytext=(max_year-1, max(sums)*1.05), arrowprops=dict(arrowstyle='->', color='red')) ax1.annotate(f'谷值 {min_year}', xy=(min_year, min(sums)), xytext=(min_year+0.5, min(sums)*0.95), arrowprops=dict(arrowstyle='->', color='blue')) plt.tight_layout() plt.savefig("nightlight_trend.png", dpi=150) plt.show()这段代码用matplotlib画折线图,标注了峰值和谷值年份。如果你做的是多区域对比,可以把多个区域的曲线画在同一张图上,用不同颜色区分。注意纵轴单位是 DN 总量,不是辐射亮度,如果要转成物理单位,需要乘上辐射定标系数——这个系数在原始产品的元数据里有,预处理后的数据一般会保留。
3.5 参数怎么调:阈值、裁剪和归一化
如果你要自己复现预处理链路,有几个参数需要根据研究区调整:
| 参数 | 作用 | 常见取值 | 调整建议 |
|---|---|---|---|
| 背景噪声阈值 | 区分城市灯光和背景噪声 | 低纬度 3-5,高纬度 8-12 | 研究区城市化程度低时调低,避免漏掉小城镇 |
| 异常值倍数 | 识别火炬和极光 | 3 倍标准差 | 油气田密集区调到 4-5 倍,避免误删 |
| 归一化参考年 | 跨年校正基准 | 2015 或 2016 | 选传感器状态最稳定的年份 |
| 中值合成窗口 | 年际合成方法 | 12 个月中值 | 缺月较多时改用均值或最大值 |
这些参数没有绝对标准,我一般会先用默认值跑一遍,看统计结果有没有明显跳变,再针对性调整。比如某年总量突然比前后年低 20%,大概率是缺月太多或者归一化没做好。
4. 避坑与排查:夜光数据预处理里最容易翻车的五个地方
4.1 现象:某年 DN 值整体偏高,时间序列出现台阶
原因:跨年辐射归一化没做,或者参考年选得不对。2012 和 2013 年的部分月份传感器状态和后面几年差异较大,如果不做归一化,直接拼在一起就会出现台阶。
解决:检查预处理版本是否包含归一化步骤。如果自己处理,选 2015 或 2016 做参考年,用伪不变区域做线性回归。伪不变区域选大型城市核心区,避开港口和机场——这些地方灯光变化大,不适合做参考。
4.2 现象:城市边界提取结果里出现大量零星高亮像元
原因:火炬、渔船灯光和极光残留没被完全掩膜。这些像元 DN 值很高,但和城市灯光的时间模式不同——它们可能只在某几个月出现。
解决:用月度数据做异常检测,而不是直接用年际合成。具体做法是:对每个像元,计算全年 12 个月的中值和标准差,如果某月 DN 值超过中值加 3 倍标准差,标记为异常。年际合成时把这些异常月份剔除。
4.3 现象:裁剪后统计的夜光总量比预期低很多
原因:矢量边界和栅格的坐标系不一致,或者裁剪时没排除 NoData 值。常见情况是边界是 WGS84 经纬度,栅格是投影坐标系,直接裁剪会错位。
解决:裁剪前先用geopandas把边界转成和栅格一致的投影。用region.to_crs(src.crs)做转换。统计时务必用valid_mask排除 NoData,否则 -9999 会把均值拉成负数。
4.4 现象:2012 年数据缺失严重,无法做完整时间序列
原因:NPP-VIIRS 在 2012 年初刚上线,部分月份数据质量差或缺失。这是原始产品的问题,不是预处理能完全修复的。
解决:如果研究必须从 2012 年开始,考虑用 DMSP-OLS 做 2012 年的补充,或者用插值方法补全。但插值会引入不确定性,论文里要说明。另一个做法是把时间序列起点定在 2013 年,避开 2012 年的数据质量问题。
4.5 现象:同一区域不同年份的 DN 值不可比,回归系数不稳定
原因:除了辐射归一化,还有一个容易被忽略的因素——像元内的土地利用变化。如果研究区在 2012-2020 年间经历了大规模城市扩张,像元内的灯光组成变了,DN 值的变化不完全是经济活动的变化。
解决:做时间序列分析时,把土地利用变化作为协变量纳入模型,或者用不变像元子集做敏感性分析。如果只是做趋势描述,在论文里说明这个局限性。
注意:夜光数据不是经济数据的替代品,它反映的是灯光辐射,不是 GDP。做回归时别把因果关系说得太满。
5. 进阶用法:用夜光数据做区域经济差异的基尼系数估算
5.1 从夜光总量到基尼系数
夜光数据的一个进阶用法是估算区域经济差异。思路是:把研究区划分为若干网格,统计每个网格的夜光总量,然后计算这些网格之间的基尼系数。基尼系数越高,说明灯光分布越不均衡,间接反映经济活动的空间差异。
具体步骤:先用rasterio把年际影像重采样到统一网格(比如 1km×1km),然后对每个网格统计 DN 总量,最后用基尼系数公式计算。基尼系数的计算可以用numpy手写,也可以用scipy的统计函数。
import rasterio from rasterio.warp import reproject, Resampling import numpy as np def gini_coefficient(values): """计算基尼系数""" values = np.sort(values) n = len(values) cumulative = np.cumsum(values) # 基尼系数公式 gini = (2 * np.sum((np.arange(1, n+1)) * values)) / (n * np.sum(values)) - (n + 1) / n return gini # 读取并重采样到1km网格 with rasterio.open("NPP_VIIRS_Annual_2020.tif") as src: # 计算重采样后的尺寸 new_width = int(src.width * src.res[0] / 0.01) new_height = int(src.height * src.res[1] / 0.01) # 重采样 resampled = np.zeros((new_height, new_width), dtype=np.float32) reproject( source=rasterio.band(src, 1), destination=resampled, src_transform=src.transform, src_crs=src.crs, dst_transform=rasterio.transform.from_bounds(*src.bounds, new_width, new_height), dst_crs=src.crs, resampling=Resampling.average # 用平均值重采样,保留灯光强度信息 ) # 排除NoData和零值 valid = resampled[resampled > 0] gini = gini_coefficient(valid) print(f"2020年区域夜光基尼系数: {gini:.4f}")Resampling.average是关键参数——用平均值重采样可以保留灯光强度的空间分布特征,比最近邻更适合做经济差异分析。gini_coefficient函数用的是标准基尼公式,输入是一维数组。注意排除零值和 NoData,否则基尼系数会被拉高。
5.2 多区域对比与可视化
如果你有多个研究区,可以分别算基尼系数,然后画对比图。我一般会做一个 2012-2020 的基尼系数变化曲线,看区域差异是在扩大还是缩小。如果某年基尼系数突然跳变,回去检查那一年的数据质量——大概率是归一化或异常值掩膜出了问题。
5.3 验证方法:和统计年鉴做交叉验证
夜光基尼系数算出来之后,怎么知道它靠不靠谱?常见做法是拿统计年鉴里的区域经济数据做交叉验证。如果夜光基尼系数和统计年鉴算出的基尼系数趋势一致,说明数据可用。如果差异很大,检查两个地方:一是重采样网格大小是否合理,二是异常值掩膜是否过度——把该保留的灯光删掉了,基尼系数会失真。
我自己的习惯是,每次拿到新的夜光数据,先跑一遍 2015 年的基尼系数,和已知的统计年鉴值对比。如果偏差在 10% 以内,就认为数据质量可以接受。从那以后我每次处理夜光数据都强制走一遍这个验证步骤,省得后面返工。希望帮到你。
本文还有配套的精品资源,点击获取