简介:这份课件面向遥感、地理信息及相关专业的师生与技术人员,围绕哨兵2A卫星数据处理展开,帮助读者系统掌握从数据源认知到预处理流程的关键知识。压缩包内为1个pptx文件,约5.25MB,以幻灯片形式组织,便于课堂讲授与自学查阅。内容涵盖哨兵卫星系列的整体介绍,包括Sentinel-1至Sentinel-6各自的应用领域,并重点讲解哨兵2A的13个光谱波段、290千米幅宽、10天重访周期及10米、20米、60米三种空间分辨率。课件还梳理了海岸气溶胶、可见光、红边、近红外、水蒸气与短波红外各波段的具体用途,尤其强调红边三波段对植被健康监测的独特价值,并给出多波段合成、图像镶嵌、图像裁剪与快速大气校正等预处理流程。目前已有259人学习,适合需要快速建立哨兵2A数据处理框架的读者参考。
1. 哨兵2A数据处理:从原始JP2到可分析栅格的完整链路
拿到一份哨兵2A的L2A级产品,解压后看到的是十几个JPEG2000文件、三个分辨率层级、外加一堆XML元数据——这是很多人第一次接触哨兵2A数据的真实场景。遥感技术应用的核心不在“拿到数据”,而在“把数据变成能算的东西”。哨兵2A每10天重访一次,单景覆盖290公里幅宽,13个光谱波段从443nm到2190nm,空间分辨率分10米、20米、60米三档。这些数字意味着:一个县域的农作物长势监测,用两景就能覆盖;但如果不做辐射定标、大气校正和重采样,直接拿DN值算NDVI,不同时相之间的差异可能比作物本身的变化还大。
这份课件对应的技术方向,解决的就是从原始JP2到可分析栅格的完整预处理链路。适合做农业遥感、林业变化检测、城市热岛分析的一线从业者,也适合需要把哨兵2A数据接入数仓数据处理采集、清洗、存储流程的工程团队。下面按实际操作顺序拆开讲。
2. 哨兵2A数据解构与处理环境搭建
2.1 产品目录结构与波段分组逻辑
哨兵2A L2A级产品解压后的目录结构是固定的,以S2A_MSIL2A_20240115T102351_N0500_R108_T50TMK_20240115T134712.SAFE为例,核心目录只有两个:GRANULE下面放影像数据,AUX_DATA和HTML基本可以忽略。进入GRANULE/L2A_T50TMK_A034567_20240115T102351/IMG_DATA后,会看到三个子目录:R10m、R20m、R60m,分别对应不同分辨率的波段。
R10m目录下有4个波段:B02(蓝,490nm)、B03(绿,560nm)、B04(红,665nm)、B08(近红外,842nm)。R20m目录下有6个:B05、B06、B07、B8A、B11、B12。R60m目录下有3个:B01、B09、B10。做植被分析最常用的组合是B04+B08算NDVI,B11+B12算NDMI,都在10米和20米档。
注意:L2A产品已经过大气校正,地表反射率值域通常在0到10000之间(乘以10000的整数存储),不是0到1的浮点。直接当反射率用会差四个数量级。
2.2 用Python搭建处理环境:rasterio + numpy + sen2cor替代方案
哨兵2A的L2A级产品虽然已经做了大气校正,但很多团队拿到的还是L1C级。L1C到L2A的转换,官方推荐sen2cor,但那个工具依赖ESA的Python环境,装起来经常翻车。我一般直接用Python生态替代:rasterio读栅格,numpy做波段运算,pyproj处理投影。
# 创建虚拟环境并安装核心依赖 python -m venv s2env source s2env/bin/activate # Windows用 s2env\Scripts\activate pip install rasterio numpy pyproj matplotlib安装完成后验证rasterio能否正常读取JP2:
import rasterio # 打开一个10米波段的JP2文件 with rasterio.open('S2A_MSIL2A_20240115T102351_N0500_R108_T50TMK_20240115T134712.SAFE/GRANULE/L2A_T50TMK_A034567_20240115T102351/IMG_DATA/R10m/T50TMK_20240115T102351_B04.jp2') as src: print(f"波段数: {src.count}") print(f"尺寸: {src.width} x {src.height}") print(f"CRS: {src.crs}") print(f"数据类型: {src.dtypes[0]}") print(f"NoData值: {src.nodata}")这段代码输出的是单波段信息。哨兵2A的JP2文件每个波段单独存储,不像Landsat那样打包成一个GeoTIFF。src.crs通常是EPSG:32650这类UTM投影,src.nodata一般是0。如果src.nodata返回None,说明元数据里没写,需要手动设0为无效值。
参数说明:src.count对哨兵2A单波段文件永远是1;src.dtypes[0]通常是uint16;src.width和src.height在10米档约10980×10980,20米档5490×5490,60米档1830×1830。这些数字决定了后续做全幅处理时的内存占用——一个10米波段约240MB,四个波段同时读入约1GB,普通笔记本能扛住,但做时间序列分析时要注意分批。
2.3 波段重采样:把20米和60米统一到10米网格
做多波段运算时,不同分辨率的波段不能直接逐像素计算。常见做法是把20米和60米波段重采样到10米。rasterio提供了两种方式:out_shape参数直接指定输出尺寸,或者用reproject做投影级重采样。前者更快,后者更准。
import rasterio from rasterio.enums import Resampling import numpy as np # 读取10米参考波段,获取目标网格 with rasterio.open('.../R10m/T50TMK_20240115T102351_B04.jp2') as ref: ref_profile = ref.profile.copy() ref_shape = (ref.height, ref.width) # 读取20米波段并重采样到10米 with rasterio.open('.../R20m/T50TMK_20240115T102351_B11.jp2') as src: b11_resampled = src.read( out_shape=(1, ref_shape[0], ref_shape[1]), resampling=Resampling.bilinear ) # 保存重采样后的波段 ref_profile.update(dtype='uint16', count=1) with rasterio.open('B11_10m.tif', 'w', **ref_profile) as dst: dst.write(b11_resampled)逻辑说明:out_shape指定输出尺寸为10米网格的尺寸,Resampling.bilinear做双线性插值。对于分类任务,建议用Resampling.nearest保留原始光谱值;对于连续变量(如NDVI),双线性更平滑。参数上,ref_profile继承了参考波段的投影、变换矩阵和尺寸,确保输出和10米波段像素对齐。如果直接对20米波段做reproject到10米,计算量会大3到5倍,但几何精度更高——两种方式按项目精度要求选。
3. 辐射定标与大气校正的工程化实现
3.1 L1C到L2A:为什么不用sen2cor也能做
L1C级产品存储的是TOA(大气顶层)反射率,L2A是BOA(地表)反射率。官方sen2cor工具做的是辐射传输模型计算,依赖6S模型和DEM数据。但实际项目中,如果只是做NDVI时间序列对比,L1C的TOA反射率经过简单的大气校正近似后,也能满足需求。常见做法是用暗像元法:找影像中反射率最低的像元(通常是深水体),假设其地表反射率为0.01,反推大气程辐射。
import numpy as np def dark_object_correction(blue_band, red_band, nir_band): """ 暗像元大气校正简化版 blue_band: 蓝波段TOA反射率(已除以10000) red_band: 红波段TOA反射率 nir_band: 近红外波段TOA反射率 """ # 找蓝波段最低1%像元作为暗像元 threshold = np.percentile(blue_band, 1) dark_mask = blue_band <= threshold # 计算大气程辐射(假设暗像元地表反射率为0.01) path_radiance = np.mean(blue_band[dark_mask]) - 0.01 # 各波段减去程辐射 blue_corrected = blue_band - path_radiance red_corrected = red_band - path_radiance * 0.8 # 红波段程辐射约为蓝波段的0.8 nir_corrected = nir_band - path_radiance * 0.5 # 近红外更低 return blue_corrected, red_corrected, nir_corrected这段代码的核心假设是:暗像元的地表反射率已知且大气程辐射在空间上均一。参数上,蓝波段程辐射最大,红波段约为蓝的0.8倍,近红外约为0.5倍——这是基于气溶胶散射的波长依赖关系。实际使用时,如果影像内有清洁水体,校正效果不错;如果全是浓密植被,暗像元法会高估程辐射,导致NDVI偏低。这时候还是老老实实跑sen2cor,或者用L2A产品。
3.2 用numpy做波段运算:NDVI、NDMI、NDRE的批量计算
波段运算本身不复杂,但批量处理时要注意内存和NoData传播。下面是一个完整的NDVI计算函数,包含无效值处理和结果压缩。
import rasterio import numpy as np def calculate_ndvi(red_path, nir_path, output_path): """ 计算NDVI并保存为GeoTIFF red_path: 红波段JP2路径 nir_path: 近红外波段JP2路径 output_path: 输出GeoTIFF路径 """ with rasterio.open(red_path) as red_src: red = red_src.read(1).astype(np.float32) profile = red_src.profile.copy() nodata = red_src.nodata if red_src.nodata is not None else 0 with rasterio.open(nir_path) as nir_src: nir = nir_src.read(1).astype(np.float32) # 无效值掩膜 valid_mask = (red != nodata) & (nir != nodata) & (red > 0) & (nir > 0) # 计算NDVI ndvi = np.full(red.shape, -9999, dtype=np.float32) ndvi[valid_mask] = (nir[valid_mask] - red[valid_mask]) / (nir[valid_mask] + red[valid_mask]) # 更新输出profile profile.update(dtype='float32', count=1, nodata=-9999, compress='lzw') with rasterio.open(output_path, 'w', **profile) as dst: dst.write(ndvi, 1) return ndvi逻辑说明:先读红和近红外波段,转float32避免整数除法截断。valid_mask同时排除NoData和零值——哨兵2A的JP2里,零值通常是填充像元。NDVI结果用-9999标记无效区域,这是遥感领域的通用NoData值。compress='lzw'能显著减小文件体积,NDVI的浮点数据压缩率通常在50%以上。
参数说明:red_src.nodata如果返回None,代码里默认用0。实际项目中,哨兵2A的L2A产品NoData值在元数据里可能不写,但影像边缘的填充值确实是0。NDVI值域理论上是-1到1,但实际地表通常在-0.2到0.9之间。如果算出来大量像元接近1,检查是否用了TOA反射率没做大气校正。
3.3 批量处理的时间窗口与云掩膜策略
哨兵2A单景处理很快,但时间序列分析要处理几十上百景。云掩膜是绕不过去的坎。L2A产品自带MSK_CLDPRB_20m.jp2云概率图,但很多人不知道这个文件在哪——它在GRANULE/L2A_T50TMK_A034567_20240115T102351/QI_DATA目录下。
import rasterio import numpy as np def apply_cloud_mask(ndvi_path, cloud_prob_path, output_path, threshold=30): """ 用云概率图掩膜NDVI threshold: 云概率阈值,大于此值视为云 """ with rasterio.open(ndvi_path) as ndvi_src: ndvi = ndvi_src.read(1) profile = ndvi_src.profile.copy() with rasterio.open(cloud_prob_path) as cloud_src: # 云概率图是20米分辨率,需要重采样到10米 cloud_prob = cloud_src.read( 1, out_shape=ndvi.shape, resampling=rasterio.enums.Resampling.bilinear ) # 云掩膜 cloud_mask = cloud_prob > threshold ndvi[cloud_mask] = -9999 profile.update(nodata=-9999) with rasterio.open(output_path, 'w', **profile) as dst: dst.write(ndvi, 1) return np.sum(cloud_mask) / cloud_mask.size # 返回云覆盖比例阈值设30是经验值:云概率大于30%的像元基本可以确定是云或云阴影。设太低会误杀薄云下的地表信号,设太高会漏掉边缘云。返回的云覆盖比例可以用来筛选影像——如果单景云覆盖超过40%,这景数据基本废了,不如等下一个重访周期。
4. 避坑与排查:哨兵2A数据处理中的五个血泪教训
4.1 坑一:JP2读取报错“not recognized as a supported file format”
现象:rasterio打开JP2文件时报错,提示格式不支持。原因:GDAL编译时没带JPEG2000驱动,或者OpenJPEG库版本不匹配。解决:先跑gdalinfo --formats | grep JP2确认驱动是否存在。如果没有,conda安装时用conda install -c conda-forge gdal,pip安装的rasterio经常缺这个驱动。另一个办法是用imagecodecs库先解码JP2再传给rasterio,但性能会下降30%左右。
4.2 坑二:NDVI算出来全是负数或超过1
现象:NDVI值域异常,大量像元小于-1或大于1。原因:用了DN值没转反射率,或者波段对应错了。哨兵2A的L2A产品,反射率是DN/10000,L1C是DN/10000再除以太阳辐照度。解决:先确认产品级别,L2A直接除10000,L1C需要查元数据里的QUANTIFICATION_VALUE和SOLAR_IRRADIANCE。波段对应上,B04是红,B08是近红外,别搞反。
4.3 坑三:重采样后影像出现条带或错位
现象:20米波段重采样到10米后,和10米波段叠加显示时有明显错位或条带。原因:哨兵2A不同分辨率的波段虽然覆盖同一区域,但像素网格原点可能差半个像素。解决:不要直接用out_shape重采样,先用rasterio.warp.calculate_default_transform计算精确的变换矩阵,再用reproject做重采样。或者更简单:用ESA的SNAP软件先做重采样和图层堆叠,导出为ENVI格式再处理。
4.4 坑四:批量处理时内存溢出
现象:处理到第20景时程序崩溃,报MemoryError。原因:每景都读全幅10米波段,四个波段约1GB,加上中间变量和NDVI结果,单景峰值内存约3GB。解决:用rasterio.windows分块读取,每次处理1024×1024的窗口。或者用dask做延迟计算,但dask对JP2的支持不如GeoTIFF好,建议先转成COG格式再上dask。
4.5 坑五:云掩膜后有效像元太少
现象:应用云掩膜后,NDVI影像上大片-9999,有效像元不到30%。原因:云概率阈值设太低,或者用了错误的云掩膜文件。解决:哨兵2A L2A产品有两个云相关文件:MSK_CLDPRB_20m.jp2是云概率,MSK_CLASSI_20m.jp2是分类掩膜(0=无云,1=云,2=云阴影,3=雪)。做NDVI时间序列时,用分类掩膜更直接——只保留值为0的像元。如果还是太少,考虑用多时相合成:取一个月的所有影像,逐像元取最大值或中值,能有效填补云空洞。
5. 从单景处理到数仓流水线:哨兵2A数据的工程化进阶
单景处理跑通后,下一步是把流程做成可复用的流水线。这里的关键词是“数据处理采集、清洗、存储流程”——遥感数据的数仓化和业务数据的数仓化,底层逻辑是一样的:采集、清洗、存储、服务。
采集层:哨兵2A数据从ESA Copernicus Hub下载,用sentinelhub的Python SDK可以按AOI和时间范围批量拉取。下载后的SAFE格式直接进对象存储(MinIO或S3),保持原始目录结构,不要解压后散落存储。
清洗层:就是前面讲的辐射定标、大气校正、重采样、波段运算、云掩膜。这一步的输出应该是Analysis Ready Data(ARD),即已经对齐网格、校正辐射、标记无效值的栅格。ARD的存储格式建议用COG(Cloud Optimized GeoTIFF),支持范围读取,后续做时间序列分析时不用整幅加载。
存储层:ARD数据按“传感器/级别/年份/月份/日期/瓦片”的目录结构组织。元数据抽出来存PostGIS,包括:景ID、获取时间、云覆盖比例、AOI范围、波段列表、处理版本。这样查“2024年1月覆盖T50TMK瓦片且云量小于20%的所有L2A数据”就是一条SQL的事。
-- PostGIS中查询符合条件的哨兵2A景 SELECT scene_id, acquisition_date, cloud_cover, tile_id FROM sentinel2_scenes WHERE tile_id = 'T50TMK' AND acquisition_date BETWEEN '2024-01-01' AND '2024-01-31' AND cloud_cover < 20 AND processing_level = 'L2A' ORDER BY acquisition_date;服务层:ARD数据可以直接接GeoServer或TiTiler发布WMTS服务,也可以接Jupyter Hub做交互式分析。如果做农作物长势监测,把NDVI时间序列喂给LSTM或随机森林,输出长势分级图。这一步的输入是ARD,输出是业务图层,中间不再碰原始JP2。
一个具体的技巧:做时间序列合成时,不要逐景算NDVI再合成,而是先做多时相波段合成再算NDVI。因为NDVI是非线性运算,先合成后计算和先计算后合成的结果不一样。先合成能保留更多光谱信息,尤其对红边波段(B05、B06、B07)的利用更充分。我一般用中值合成:取时间窗口内所有有效像元的中值,既能去云又能保留物候特征。
最后说一个习惯:每次处理完一批数据,把处理参数(大气校正方法、重采样方式、云掩膜阈值、合成窗口)写进一个JSON文件,和ARD数据放在一起。三个月后回头看,能省下大量“当时怎么设的”这种后悔药时间。遥感数据处理的可复现性,一半靠代码版本控制,一半靠参数记录。希望帮到你。
本文还有配套的精品资源,点击获取