
我一直觉得ICESat-2这名字含冰量太高导致很多非冰川方向的人下意识把它归成一颗“极地卫星”。做内陆水体或者植被研究的朋友来问我数据下载和处理的时候都会先加一句“这星是不是只能看冰”实际上是误会。ICESat-2的确是为冰而生的但它的ATLAS激光测高系统记录的是全球地表光子只要处理链路选对了做湖泊水位、地形剖面、冠层高度、甚至城市建筑高度都有不错的表现。这篇随笔就把我从下载到处理分析这条链路里踩过的、总结出来的东西一次说清楚全是折腾过的实操记录希望能帮刚开始接触ICESat-2数据下载及处理分析的朋友少绕几个弯子。数据获取这件事最开始的成本往往不是网速而是“选择”。很多初学者一上来就奔着ATL03去理由是“原始光子数据信息最全”结果下载完一打开几千万个光子点直接交互式绘图卡死整个人懵在屏幕前。这不是你电脑不行而是产品选错了。先说结论除非你要做光子级别的精确定位或算法开发否则从ATL06、ATL08、ATL13这类沿轨产品上手才是正常人的工作方式。1. 先搞清楚自己要哪个产品否则后面全白干1.1 ATL03、ATL06、ATL08、ATL13到底差在哪ICESat-2的ATLAS激光器每秒发出一万个脉冲每个脉冲打到地面后会形成大约10米左右的光斑沿轨方向相邻光斑间隔约0.7米。所有这些光子先被放在ATL03产品里以90米为一个segment进行组织。所以ATL03本质上是一个巨大的“光子云”它不仅包含地表反射的光子还有大量大气散射、太阳背景造成的噪声光子。ATL03是基础数据但直接拿ATL03做应用分析相当于把一袋没筛过的面粉拿去做面包你得自己筛、自己配。NSIDC官方已经把筛子和配方做成了后续产品这才是日常主力。产品全称主要用途沿轨采样特点ATL03Global Geolocated Photon Data所有光子定位数据含噪声点约90米一个段光子级ATL06Land Ice Height陆地冰、冰盖高程也常被用在湖面等平坦地表沿轨40米一个高程点ATL08Land and Vegetation Height陆地地形、树冠高度固定100米沿轨段ATL13Inland Water Surface Height湖泊、水库、河流水位高程沿轨100米左右聚合具体分段不规则我自己的习惯是如果研究目标是“测一段连续地表的高程剖面”优先看ATL08因为它的地形参数是经过信号筛选和噪声剔除的能省掉好多预处理如果目标是水体水位ATL13的河湖内部产品专门做了波形和水体分类如果在冰盖上做长时间序列那就老老实实用ATL06。1.2 读一读ATBD真的比乱搜教程管用很多人下载数据前喜欢直接查代码库我却建议先花一个小时翻对应产品的 Algorithm Theoretical Basis DocumentATBD。听名字很吓人实际上里面每个参数的定义、坐标系、误差来源写得很清晰。最关键的是它会告诉你这个产品里哪些字段是“可用且稳定”的哪些字段“还没定标好”。以ATL08为例ATBD里明确指出在坡度较大、植被茂密的复杂地形区地形和冠层分类容易混淆使用confidence字段时要特别小心。你看完这段就知道自己在山区提取树顶高程时不能单纯取canopy_h_max而是要结合ph_confidence一起判断。尤其是做跨时间段、跨轨道的对比时不同版本号例如v5、v6之间的字段定义会有差异不读文档直接沿用旧代码等跑出来的数据严重漂移甚至为负时你就知道坑有多深了。我在初版代码里踩过ATL08 v5到v6的字段名变化——上一个版本叫terrain_slope后一个版本改成了terrain_slope_deg凡是没看版本说明的全在这里挂掉。2. 数据下载从网页点到脚本抓取两条路线都给你摸透了2.1 手动下载Earthdata Search配套NSIDCICESat-2的数据托管在NASA的NSIDC DAAC但真正的数据发现入口是 Earthdata Search 。第一次使用需要注册一个NASA Earthdata账号这个账号同时覆盖GES DISC、ASF等多个NASA数据分发中心。注册完记得登录NSIDC官网把数据集访问权限勾选上不然下单之后会卡在权限校验。手动下载的流程大致是打开Earthdata Search在左侧搜索框输入产品名比如ATL08在右侧地图上拉一个你关心的范围框或用左下角的时间筛选器设定起止日期点击“Download All”或者先加入购物车再统一下载系统会生成一个订单并在准备完成后发送下载链接列表拿wget或浏览器逐条下载。这个流程对偶尔下载一两景数据的朋友来说完全够用。但要注意Earthdata Search的“Download All”给的是一个只包含当前地图可见范围内granule的列表如果你把地图缩得太小它可能只会列出少数几颗数据块而如果拉得范围很大又可能一次性返回好几百个文件浏览器直接崩掉。建议手动下载时用右上角的“Search”结果面板做一次时间范围压缩通常一天内的ATL08全球数据大概有1400个granule你用研究区框选后实际命中的通常是个位数到几十个文件这个体量就很友好了。2.2 脚本化下载icepyx才是批量操作的正解当你需要拉一个水文站点周边过去五年所有经过的轨道数据手动下载就要疯掉了。这时候必须上Python包icepyx。这是ICESat-2社区最主流的Python客户端封装了Earthdata CMR检索和NSIDC下单流程。基本用法如下import icepyx as ipx # 设定研究区域给一个中心点经纬度和半宽范围单位米 region ipx.Query( ATL08, spatial_extent[120.9, 30.5, 121.1, 30.7], # [west, south, east, north] date_range[2023-06-01, 2023-08-31], version6 ) # 查看符合条件的granule数量和具体文件 region.avail_granules() # 下单并下载 region.earthdata_login(sso_tokenNone) region.download_granules(path./icesat2_data/)icepyx最实用的点在于它内置了数据范围和时间的统一表达不需要你再手动拼CMR请求。用sso_token参数传一个NASA Earthdata的Bearer Token可以避免脚本每次弹窗登录。2.3 CMR API当你连icepyx都想绕开时的最后一招icepyx挺好的但有时候在服务器上装依赖费劲或者你只想快速搜一下有哪些granule那可以直接调CMR API。REST接口支持在URL里传空间范围curl -G https://cmr.earthdata.nasa.gov/search/granules.json \ -d short_nameATL08 \ -d version6 \ -d bounding_box120.9,30.5,121.1,30.7 \ -d temporal2023-06-01T00:00:00Z,2023-08-31T23:59:59Z \ -H Authorization: Bearer YOUR_TOKEN返回的JSON里包含每一个granule的下载链接。拿到链接列表后可以拼接出一份wget下载清单wget -i download_links.txt但NSIDC的HTTPS下载需要认证wget时要带上.netrc文件或--user和--password参数。公司内网如果代理比较严格这一步常常弹出401需要先检查能不能直接访问e4ftl01.cr.usgs.gov类似域名。2.4 下载体量控制不要一上来就全选还有一个赤裸裸的教训不要在第一次下载时把时间范围拉到全生命周期然后点全选。ICESat-2一颗星一小时大约产生30多个granule全球一天的数据量在几百GB级别。你要是把五年ATL03全下下来光硬盘就把你安排了。即使只想下载ATL08也要先通过region查询接口确认granule数量再决定要不要二次缩小区间。业务研究完全可以从“经过研究区的单条track”出发几条track的高程剖面已经能说明问题。3. 打开HDF5文件后第一件事不是画图而是自查3.1 HDF5的嵌套结构得先摸清ICESat-2的HDF5文件层级很深ATL08长这样/gt1l/ /land_segments/ /canopy/ /terrain/ /geolocation/ /gt1r/ ...其中gt1l、gt1r、gt2l...共六个光束组对应ATLAS的六束激光三对强波和弱波组合。在利用Python打开之前先写几行代码列出所有顶层key能帮你快速形成全局印象。import h5py f h5py.File(ATL08_20230601000000_20230601000000_006.h5, r) print(f.keys()) print(f[gt1r][land_segments].keys())如果嫌HDF5直接操作麻烦也可以下载photonprint或ICESat2Veg这类可视化工具做快速干脆的PDF报告。但在写处理脚本时HDF5的“原生”操作是不可替代的——因为你要精确定位到每一个group里的每一个字段。3.2 重点自查字段start_time、quality_summary、signal_conf_ph去瑕疵画图之前先自查至少三个内容数据覆盖时间段一条granule在JSON文件里看时间是平滑的但打开HDF5后你会发现每个segment都有自己的时间戳激光在大气中衰减或云层遮挡会导致部分segment没有有效观测。直接看quality_summary值0表示基本可用1或者2就是有问题。每个segment里的有效光子数ATL08里对应的字段是n_seg_ph段内光子计数值如果某段的光子数不到10心里就该打个问号。这不是硬性阈值而是结合信噪比看晴天和云天的信号差异很悬殊。信号置信度signal_conf_ph对于ATL03和ATL08都有区分噪声/信号光子的置信度标签。如果你做的是光子去噪研究这个字段是你的起点。但是千万不要盲信signal_conf_ph2就是地表光子在坡度过大的区域这个置信度也会有大量误判。3.3 坐标参考系检查很多人在这一步之后直接拿latitude、longitude、h三个字段去画图却忘了ICESat-2的高度参考面是WGS84椭球体。如果你的比较对象是常用的DEM数据比如SRTM或ASTER GDEM它们使用的是EGM96大地水准面两个参考面之间存在数十米的差值。如果你想和DEM做差值必须引入大地水准面改正。实用做法是下载EGM96或者使用pyproj的Geod和Transformer操作或者直接去USGS的在线网格工具查geoid height。不改正的话你算出来的“湖面相对周边地形的高差”会整体偏移几十米在海拔低的湖区尤其致命。4. 用一条实际轨道串起完整处理链路从一个湖面高程剖面开始4.1 用ATL08提取沿轨地形剖面假设我想看某高原湖泊群沿一条轨道的高程变化处理思路是用icepyx查这个湖泊周围500米缓冲区有哪些轨道经过下载对应的ATL08文件读取terrain/h_te_best_fit作为地表高程对水体而言这个值就是水面高程的一个近似同时读取latitude、longitude和terrain/slope。逻辑上这一段处理的核心就是过滤出水面这一部分而不是把岸边山体也混进来。所以我要根据湖泊边界做一个空间裁剪。用shapely里的within或栅格掩膜来做。import geopandas as gpd import numpy as np import h5py from shapely.geometry import Point f h5py.File(ATL08_example.h5, r) lat f[gt1r][land_segments][latitude][:] lon f[gt1r][land_segments][longitude][:] h f[gt1r][land_segments][terrain][h_te_best_fit][:] slope f[gt1r][land_segments][terrain][slope][:] # 假设你已经用 polygon 表示湖泊边界 df gpd.GeoDataFrame( {elevation: h, slope: slope}, geometry[Point(lon_i, lat_i) for lon_i, lat_i in zip(lon, lat)], crsEPSG:4326 ) lake_points gpd.sjoin(df, lake_polygon, opwithin)这里有个容易忽略的细节ATL08的高程是地形拟合值在开阔水面上光子往往会穿透浅水或在水面产生镜面反射得到的h_te_best_fit还可能混入水底信号。所以如果strict一点我会参考ATL13的水体高程产品或者对ATL08的这个字段做一次基于n_seg_ph的中值滤波。4.2 计算沿轨距离画出剖面拿到了湖面点的高程和经纬度后下一步需要把经纬度投影成沿轨距离。最简单粗暴的方式是用pyproj做局部投影from pyproj import Geod geod Geod(ellpsWGS84) _, _, dist geod.inv( lon[:-1], lat[:-1], lon[1:], lat[1:] ) dist_along np.concatenate([[0], np.cumsum(dist) / 1000]) # 单位km然后你就能画一条“沿轨距离 vs 高程”的曲线。这条曲线能直观看到湖面是否平坦、轨迹穿过湖盆时有没有地形畸变。把同一条轨道、不同日期的多次过境数据叠加在一起就能看到水位在季节尺度的变化。注意ICESat-2同一参考轨道的重复周期约91天但是子轨和邻近轨道每天都在变动实际重访同一个湖泊的频率可能一周一次到一个月一次不等。查询数据时不要锁死某一条轨道号反而应该用空间范围去“捞”所有经过湖区的数据。4.3 多轨数据合并注意轨道方向系统性偏差当你把不同飞行方向的轨道数据放在一起做统计时最容易出现一个“幽灵信号”上升轨道ascending和下降轨道descending在坡度较大的湖盆区域显示出的水面高程系统性偏差可能是几厘米到十几厘米。这并不一定是真实水位差而是激光入射角、水面波浪和沿轨不平度的综合结果。如果只是要季节水位均值可以把上升/下降轨道的高程分别统计但一起出图如果要更讲究就建模校正轨道方向的偏差。做长时序分析的朋友至少要把每一条剖面绘制出来“人眼扫一遍”再决定是否把某次明显异常的过境点剔除。4.4 我的建议流程总结把以上步骤串成一个可复用的流程空间范围定义湖周边缓冲。时间范围分段按月或季节。批量检索并下载ATL08/ATL13。读取关键字段做quality_summary过滤剔除无效点。按湖泊边界空间筛点。沿轨距离计算并可视化。多次过境数据叠加对比计算统计值和变化速率。这套流程我推荐给湖泊水位、河道比降、植被垂直结构方向的同事直接改改坐标和文件路径就能跑通。5. 处理过程中绕不开的坑与性能优化5.1 订单为空空间范围太小和轨道覆盖问题的双重锅有段时间我连续下单ATL08都返回“0 granules”排查了很久发现两个原因一个是我把空间范围设计成点坐标而不是有面积的面。CMR检索是要求有面积边界的单纯给一个点会匹配不到轨道。这个好解决把点扩展成0.1度乘0.1度的方框就正常了。另一个更隐蔽研究区虽然在地图上看挺大但ATLAS的激光光斑扫描很窄每条轨道在地面上覆盖宽度只有几十米到几百米并不像光学卫星那样一次拍一大片。如果你的研究区恰好处于轨道间隙或者只落在一条轨道边缘那么可能在某一段时间内真的没有强束覆盖。这时候宁可把检索范围放宽一倍多做一次空间裁剪也比干瞪眼强。5.2 HDF5大文件读取的内存优化一条ATL08文件通常在50~100MB但ATL03动辄GB级别。直接用h5py把全量字段load进内存再处理8GB内存的笔记本直接卡死。处理ATL03的正确姿势是分段读取with h5py.File(ATL03_example.h5, r) as f: n f[gt1l][heights][delta_time].shape[0] chunk_size 100000 for start in range(0, n, chunk_size): end min(start chunk_size, n) lat_chunk f[gt1l][heights][lat_ph][start:end] lon_chunk f[gt1l][heights][lon_ph][start:end] h_chunk f[gt1l][heights][h_ph][start:end] # 处理当前chunk分段读取看起来慢但换来的是内存稳定。尤其做光子去噪时你需要使用局部窗口统计分段正好提供了一个天然滑动窗口既控制内存又方便并行。5.3 并行下载和并行处理的正确姿势如果你有多核CPU可以用concurrent.futures对多个granule并行下载或并行处理。但要注意HDF5本身不支持多进程安全地写同一个文件所以并行处理时应该每个进程独立读并把结果单独输出为csv或npz最后再做合并。千万别开多个进程同时往一个HDF5里写数据会直接报段错误。5.4 单位与量纲读数据时多看一眼属性每个字段都有单位属性比如h_te_best_fit是米但坡度是度速度是米每秒。别问我为什么提醒这个东西——你见过有人把度当成百分数直接代入公式结果坡度“异常”到80多度还在分析的吗我就见过。数据读进来第一件事把attrs[units]打出来养成习惯。6. 我的一些碎碎念怎么从“下载成功”走到“分析可靠”下载成功只是万里长征第一步。真正让ICESat-2数据可用关键是“质控”两个字。拿到每条轨道的剖面图先看有没有离谱的高程突变这类突变大概率是云层遮挡或地形边缘造成的不是真实地表。把突变、置信度低的光子、空间上孤立点先剔掉再谈统计。我个人现在的习惯是每下载一批数据先把对应区域的图像和DEM拉出来做一次目视对比。这一步看着土但能发现很多潜在问题。比如湖面出现了一个“台阶状”跳跃十有八九是水陆边界混到了剖面里而不是湖面真的有个断层。很多朋友想要一套“万能脚本”我理解这种心情但ICESat-2产品不同版本、不同场地的行为差异很大完全不加判断的自动化流程反而是最危险的。最好的状态是把所有默认参数都写清楚、每一步的根据都写明白然后拿到新区域先跑一遍小样本人工审核通过后再放大。最后再分享一个我常用的检查技巧把ICESat-2提取的湖面高程和其他遥感水位产品比如Hydroweb、DAHITI或者Sentinel-3的测高产品做一个简单的相关性分析。如果趋势一致性非常高说明你的ICESat-2处理流程质量可靠如果系统性偏差好几十米先不要怀疑卫星回头检查坐标参考面和过滤条件。这个交叉验证方法比我写过的任何字段检查都管用。