尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ICESAT-1/2激光测高数据可视化与去噪Python实践

ICESAT-1/2激光测高数据可视化与去噪Python实践 简介面向ICESAT系列卫星数据的科研与工程人员这份Python程序包实现了光子计数与波形数据的加载、去噪和可视化适用于冰川高度变化分析、全球气候变化研究等场景。资源压缩包共三十五个文件整体约七百一十七兆主要包含Python源码、编译缓存文件、图形界面脚本、样本数据文件、可执行程序、构建配置文件与依赖说明等其中既提供可直接运行的桌面程序也保留了便于改写的源码模块。内置数据加载器可读取常见格式的卫星高度数据去噪模块针对光子和波形信号分别设计了处理算法交互式可视化画布支持对剖面结果进行查看与比较。另外附带环境依赖清单和打包配置便于复现运行。目前已有超过一千人学习下载适合具备一定Python基础、需要快速处理冰卫星数据的研究生和科研人员参考使用。 说实话第一次听到“ICESAT-1 和 ICESAT-2 数据可视化与去噪 Python 程序”这个项目时我就知道这不是那种跑个matplotlib就能交差的活儿。ICESAT-1 代表的是全波形激光测高时代ICESAT-2 则是光子计数时代两代卫星数据格式、噪声形态、处理链路完全不同却要在同一个 Python 框架里被可视化、被去噪、被解释。这篇文章就是把我的完整实现思路、代码结构和踩坑记录整理出来。如果你正在做极地冰盖、植被高度或者地形反演手里刚拿到 ATL03 或 GLAH 系列数据不知道怎么下手这篇文章能帮你把“从 HDF5 到一张干净的剖面图”这条路走通。做这类东西的人其实最头疼的不是不懂 Python也不是不会滤波而是不熟悉遥感数据的物理含义和存储逻辑。你要知道每一个字段是什么单位、参考的是哪个坐标系统、哪些值是填充值然后才能把可视化做对把去噪做好。下面我就按数据源拆解、环境搭建、读取预处理、可视化、去噪、排错调参这个顺序往下讲全程按我实际跑通过的项目结构来。1. 数据源拆解两代激光卫星的差异决定了处理思路不同1.1 GLAS 与 ATLAS从全波形到光子计数ICESAT-1 搭载的 GLAS 传感器是典型的全波形激光测高仪。激光脉冲打到地表后接收端记录整个回波波形每一个波形采样点都是地表不同高度反射能量的叠加。冰川表面可能只有一个尖锐回峰而森林区域则会看到冠层顶、冠层内部、地面三个峰连在一起的宽波形。正因为处理对象是一维波形信号所以去噪方式偏向于底噪估计、高斯滤波、小波阈值那一套信号处理方法。ICESAT-2 搭载的 ATLAS 完全不同它用了微脉冲光子计数技术。激光以很高的重复频率发射弱脉冲接收端逐个记录返回的单光子事件。这个体制带来的最大变化是ATL03 产品里不仅有地表信号光子还有大量太阳背景噪声和探测器暗计数。你在剖面图上看到的景象是噪声光子像雾一样均匀铺满整个高程范围而信号光子密集地沿着地表轮廓排成一条线。这其实和我们做通信网络流量去噪、点云数据处理面临的处境很像——数据从“一维曲线”变成了“离散点云”处理重点也从“平滑曲线”变成了“从海量噪声点中识别高密度目标”。这两代数据放在同一个项目里最直接的影响是数据读取和去噪模块没法共用。我当时的设计是把程序拆成两个数据源模块一个管 GLAS 的 HDF5 波形/高程一个管 ATLAS 的 ATL03 光子可视化模块尽量共用。1.2 产品选型处理哪一层数据最划算ICESAT-1 能拿到的产品很多对我来说最常用的是两层GLAH06NASA 已经反演好的 40Hz 沿轨高程点适合快速出剖面图但是对异常点敏感需要做剔除噪声点的后处理。GLAH01 / GLAH05原始全波形或波形参数化结果适合自己实现底噪估计、波形分解灵活度高但处理量也大。ICESAT-2 这边我的建议是直接用 ATL03 光子级产品。ATL03 是光子云数据只有到了光子级别才能做自定义去噪和可视化。如果直接拿 ATL06 那种已经平滑好的冰盖高度产品去噪环节就没意义了因为 NASA 官方已经帮你滤干净了。“处理哪一层数据”决定了整个程序的复杂度我当时的决定是 GLAS 处理 GLAH06 高程 GLAH01 波形两个模块ATLAS 只处理 ATL03。2. 环境准备与 Python 技术栈选型2.1 核心依赖库选型整个程序我建议用 Python 3.9 以上版本核心依赖如下h5py读写 HDF5 文件ATL03 和 GLAH 系列都是 HDF5 格式。numpy、scipy数组计算、KDTree 邻域搜索去噪算法的基础。matplotlib所有剖面图和对比图的绘制。cartopy地理轨迹图和投影地图可选但强烈推荐。pywt小波阈值去噪处理 GLAS 波形数据时用得上。安装时有个坑cartopy 直接用pip install cartopy在部分机器上很容易因为 GEOS 依赖编译失败。我实测下来用 conda 安装最稳conda create -n icesat python3.9 conda activate icesat conda install -c conda-forge h5py numpy scipy matplotlib cartopy pywavelets2.2 数据获取与工程目录组织数据从 NSDIC Earthdata 下载需要注册账号。ATL03 单条轨道文件大约几百 MBGLAH 系列单文件也有几十 MB建议下载后用目录区分版本icesat_tool/ ├── data/ │ ├── ATL03/ │ │ └── ATL03_20200101000000_00000001_001_01.h5 │ ├── GLAH01/ │ └── GLAH06/ │ └── GLAH06_634_2101_001_0079_0_01_0001.H5 ├── src/ │ ├── read_atl03.py │ ├── read_glas.py │ ├── denoise.py │ └── viz.py └── main.py目录清晰一点后续调参数会方便很多。我最早全部脚本堆在一个目录里后来参数多了根本分不清哪个文件对应哪个实验。3. 核心实现一HDF5 数据读取与预处理3.1 ATL03 光子数据的字段提取与单位转换ATL03 内部按波束分组共有 6 个波束gt1l、gt1r、gt2l、gt2r、gt3l、gt3r。每个波束下需要读取三个部分geolocation 下的光子坐标和沿轨距离heights 下的光子高度和置信度。其中有两个非常容易踩坑的单位转换点reference_photon_lat和reference_photon_lon是 int32 类型单位是 1e-7 度必须乘以1e-7才是十进制度。h_ph单位是米参考 WGS84 椭球面是可以直接用的。我实际用的读取代码如下import h5py import numpy as np def load_atl03_photons(path, beamgt1l): with h5py.File(path, r) as f: lat f[f/{beam}/geolocation/reference_photon_lat][:] * 1e-7 lon f[f/{beam}/geolocation/reference_photon_lon][:] * 1e-7 dist f[f/{beam}/geolocation/reference_photon_dist][:] h f[f/{beam}/heights/h_ph][:] conf f[f/{beam}/heights/conf_ph][:] return lat, lon, dist, h, confconf_ph 是官方置信度字段通常conf_ph 2被认为是信号光子0 和 1 基本对应噪声。这个字段后面可以用来验证我自己写的去噪算法是否靠谱相当于官方给了我们一个参考答案非常宝贵。程序里还要循环处理 6 个波束每个波束的光子数量不同处理时建议用一个 for 循环把结果存成字典beams [gt1l, gt1r, gt2l, gt2r, gt3l, gt3r] atl03_data {} for b in beams: lat, lon, dist, h, conf load_atl03_photons(file_path, beamb) atl03_data[b] {lat: lat, lon: lon, dist: dist, h: h, conf: conf}3.2 GLAS 高程产品的读取与清洗GLAS 的 GLAH06 文件读取逻辑和 ATL03 类似但字段路径不同。我需要读取 40Hz 沿轨数据主要字段是经纬度和冰盖表面高程def load_glas06_elevation(path): with h5py.File(path, r) as f: lat f[Data_40HZ/Geolocation/d_lat][:] lon f[Data_40HZ/Geolocation/d_lon][:] elev f[Data_40HZ/Elevation_Surfaces/d_elev][:] # d_elev 可能是二维数组形状为 (记录数, 1)需要压缩成一位数组 elev np.squeeze(elev) lat np.squeeze(lat) lon np.squeeze(lon) # 剔除填充值 valid elev -9999 return lat[valid], lon[valid], elev[valid]这里最容易犯的错是忘记处理填充值。HDF5 存储时无效值一般用-9999或-999填充如果不剔除后面画剖面图会出现一个直接扎到地心去的异常点去噪时也会被当成“信号异常点”处理影响整条轨道的统计量。对于 GLAH01 波形数据读取后是二维数组每一行对应一个激光脉冲的完整波形。单个波形通常有 544 个采样点这些点代表激光回波随时间的能量变化def load_glas01_waveforms(path, shot_index0): with h5py.File(path, r) as f: wave f[Data_40HZ/Waveform/wf][shot_index, :] return np.array(wave, dtypenp.float64)4. 核心实现二可视化方案4.1 光子云图一张图看清全部噪声和信号分布ATL03 的可视化核心是绘制沿轨剖面图X 轴用沿轨距离Y 轴用高程直接把所有光子画成散点。这一步看着简单但点数量实在太大一个波束可能就有上百万个光子硬画很容易卡。我实际用的是rasterizeTrue把散点图栅格化导出 PDF 时不会卡死import matplotlib.pyplot as plt def plot_photon_cloud(dist, h, titleATL03 Photon Cloud): fig, ax plt.subplots(figsize(14, 5)) ax.scatter(dist, h, s0.5, c0.6, alpha0.4, rasterizedTrue) ax.set_xlabel(Distance along track (m)) ax.set_ylabel(Height (m)) ax.set_title(title) return fig, ax如果内存比较紧张可以先用np.random.choice抽一个子集再画可视化阶段没必要把全部点都展示出来。但如果是为了检查去噪结果我会把官方置信度字段映射成颜色一张图上噪声画成灰色信号画成红色colors np.where(conf 2, red, gray) ax.scatter(dist, h, s0.5, ccolors, alpha0.5, rasterizedTrue)这样一眼就能看出信号光子和噪声光子的大致分布范围后面验证自己的去噪算法时也方便对比。4.2 剖面线图、轨迹地图与对比图GLAS 的 GLAH06 高程点数量比 ATL03 光子少得多画剖面图可以直接用ax.plotax.plot(dist_along, elev_cleaned, linewidth1.2, labelGLAH06 elevation)这里dist_along可以直接用沿轨序号乘以脉冲间距近似或者从数据文件里找沿轨距离字段。地图轨迹可视化用 cartopy 加 PlateCarree 投影最省事极地研究建议改用ccrs.SouthPolarStereo()或ccrs.NorthPolarStereo()不然高纬度区域的形变非常严重import cartopy.crs as ccrs def plot_track(lon, lat): fig plt.figure(figsize(10, 8)) ax plt.axes(projectionccrs.SouthPolarStereo()) ax.set_extent([-180, 180, -60, -90], crsccrs.PlateCarree()) ax.scatter(lon, lat, s1, transformccrs.PlateCarree()) ax.gridlines(draw_labelsTrue) return fig, ax去噪前后的对比图我建议用上下两个子图共享 X 轴原始数据放上面去噪数据放下面视觉效果最直观。不要用左右子图跨轨道剖面图纵向趋势变化很快左右对眼睛不友好。4.3 可视化输出细节科研图片输出建议用fig.savefig(result.png, dpi300, bbox_inchestight)如果后续要投期刊可以导出 PDF 矢量图配合rasterizedTrue保证散点部分不会让文件体积爆炸。我自己习惯把每个波束去噪前后的评估指标信号光子数、噪声抑制率写在图上方便横向比较参数。5. 核心实现三去噪算法设计5.1 光子计数数据的密度去噪与聚类ATL03 去噪的核心假设是信号光子在局部区域密度远高于噪声光子。所以最简单的去噪方式是密度过滤对每个光子统计它在一定空间邻域内有多少个邻近光子如果数量超过阈值就保留否则剔除。我当时第一时间想到用scipy.spatial.cKDTree因为用暴力双层循环处理几十万光子速度慢到无法接受。cKDTree 是空间索引结构一次建树后邻域查询非常快from scipy.spatial import cKDTree def denoise_by_density(dist, h, radius10.0, min_points5): coords np.column_stack([dist, h]) tree cKDTree(coords) counts tree.query_ball_point(coords, rradius, return_lengthTrue) return counts min_points这里的难点是radius怎么取。因为光子云图里 X 轴是沿轨距离单位是米Y 轴是高程单位也是米理论上可以直接用欧氏距离。但实际中地表会倾斜同一段信号光子沿着地表排布不是水平的如果直接用固定的圆形邻域倾斜度大的山坡区域可能漏检。我做了一个小改进把每个光子的高度先减去局部中位数趋势再做邻域统计。这一步很简单却能明显提升倾斜地表上的信号识别效果。如果希望去噪结果更规则可以用 DBSCAN 聚类。sklearn.cluster.DBSCAN的eps对应邻域半径min_samples对应最少点数from sklearn.cluster import DBSCAN def denoise_by_dbscan(dist, h, eps10.0, min_samples5): coords np.column_stack([dist, h]) labels DBSCAN(epseps, min_samplesmin_samples).fit_predict(coords) return labels ! -1DBSCAN 的好处是能把间距较远的信号簇也识别出来但参数同样敏感而且大规模数据下内存占用比 cKDTree 方案高。另外要提一下ATL03 中有强弱两个波束之分强波束的信号光子密度高于弱波束。我实际调试时发现去噪参数不能两个波束通用弱波束的min_points要适当调低否则信号会被成片误删。5.2 波形数据的底噪估计与小波去噪GLAS 全波形去噪是另一条思路。波形数据是一维离散信号噪声主要体现为高频抖动的底噪。常用的处理分两步第一步是估计噪声底限。取波形两端没有信号的 bin 作为纯噪声区计算平均值和标准差def estimate_noise(waveform, margin20): noise_samples np.concatenate([waveform[:margin], waveform[-margin:]]) return np.mean(noise_samples), np.std(noise_samples)第二步是设置阈值低于“均值 n 倍标准差”的采样点置零然后再做平滑。n 通常取 2 到 4我一般先看波形直方图再决定。如果底部噪声比较大阈值太低会留下大量毛刺后面高斯拟合时会出现伪峰值。如果要用小波阈值去噪我推荐pywt库。小波变换能把信号分解成不同频率的分量然后把高频噪声分量的小波系数压缩最后重构回去。这是一个广泛应用于信号处理的经典流程算是我个人处理 GLAS 波形时比较顺手的方式。代码如下import pywt import numpy as np def wavelet_denoise(waveform, waveletdb4, level3, modesoft): coeffs pywt.wavedec(waveform, wavelet, levellevel) sigma np.median(np.abs(coeffs[-1])) / 0.6745 threshold sigma * np.sqrt(2 * np.log(len(waveform))) coeffs_threshed [coeffs[0]] for i in range(1, len(coeffs)): coeffs_threshed.append(pywt.threshold(coeffs[i], threshold, modemode)) return pywt.waverec(coeffs_threshed, wavelet)这段代码里sigma * np.sqrt(2 * np.log(N))是经典的通用阈值。实测下来对于信噪比一般的 GLAS 波形db4三层分解已经够用。如果你发现去噪后波形顶部被削平说明阈值太大可以把mode改成soft换成hard或者调低 level。5.3 参数选择经验与验证方法去噪参数不是拍脑袋定的我的习惯是先用官方置信度做验证。对 ATL03以conf_ph 2为“标准答案”然后对比我自己写的密度去噪算法结果算一下查全率和查准率查全率 我识别出的信号光子数 / 官方信号光子数查准率 我识别出的信号光子数 / 我保留的全部光子数这样试几个参数组合就能找到最合适的radius和min_points。比如平坦冰盖区域radius取 5-20 米、min_points取 3-6 个效果较好而城市或森林区域光子特别密则需要更小的邻域半径。对于 GLAS 波形验证方法更直接看去噪后波形是否保留了主峰的形状同时底部噪声是否明显削弱。如果一个波形原本能看到两个回峰去噪后两个峰都在且位置没偏那就说明小波去噪没有破坏有效信息。6. 实际问题排查与参数调优经验6.1 常见异常与解决方案我整理了一下实际处理过程中最容易遇到的几个问题下面这张表可以当速查手册用现象可能原因解决办法纬度范围看起来不对出现几百甚至几千度的值没有把 ref_ph_lat / ref_ph_lon 乘以 1e-7读取后立刻转成十进制角度剖面图中有一个点扎到 -9000 米GLAH06 填充值如 -9999没剔除读取时加valid elev -9999散点图绘制极慢导出的 PDF 上百 MB光子点太多且是矢量散点加rasterizedTrue或者先抽样再画去噪后信号被全部删掉radius 或 min_points 设置过严调大 radius调低 min_points去噪后弱波束效果明显差于强波束强弱波束参数混用分别设置参数弱波束降低阈值小波去噪后波形顶部变形严重小波去噪阈值过大改用hard阈值或降低分解层数还有就是 HDF5 文件下载不完整的问题。ATL03 文件较大用浏览器下载很容易中断但又有部分写入h5py 打开时报Unable to synchronously open object这类错误。我后来所有数据都用 curl 断点续传脚本下载再验文件名大小能省掉很多麻烦。6.2 密度去噪的三条避坑心得第一窗口形状要会变通。固定圆形邻域在水平地表好用但冰盖表面经常有坡度。我实测下来把高度减去一个滑动窗口的中位数后再做密度统计对倾斜地表的信号识别效果提升明显。做法很简单先用np.convolve做平滑估计地表趋势再算残差最后在残差上做密度去噪。第二别忽视沿轨方向的距离单位。ATL03 的reference_photon_dist是沿轨距离单位是米而h_ph也是米但一些低版本数据或者自己拼接的数据容易出现单位不一致的问题。如果去噪结果把所有信号都删掉先检查单位。第三把去噪和可视化拆开跑。我最初把去噪和画图放在一个脚本里每次调参数都要重新绘图浪费时间。后来改成先去噪、保存一个二进制筛选结果比如保持 index 的 npy 文件再单独跑绘图脚本。这样调参数流程能快很多。6.3 调参时的一个高效工具我写了一个简单循环用来批量测试不同参数的组合并输出统计指标from itertools import product for radius, min_points in product([5, 10, 20], [3, 5, 8]): mask denoise_by_density(dist, h, radiusradius, min_pointsmin_points) precision np.sum(mask (conf 2)) / np.sum(mask) recall np.sum(mask (conf 2)) / np.sum(conf 2) print(fradius{radius}, min_points{min_points}, precision{precision:.3f}, recall{recall:.3f})这样扫一轮参数大概十分钟就能找到合适的量级。拿到量级之后再在附近做小范围的细调比纯粹拍脑袋试参数高效太多。最后再分享一个小技巧。如果你是在 Jupyter Notebook 里调试%matplotlib inline之后记得加plt.rcParams[agg.path.chunksize] 10000不然画大规模散点图时 matplotlib 会报Path has too many points的坑。这个报错我第一次遇到时查了半天其实就是点太多渲染内存炸了设置一下块大小就能解决。这个项目做完之后我最大的感受是两代卫星虽然不是同一时代的技术但它们在数据分析和可视化层面其实有很多共性。无论数据形态怎么变核心都是把物理信号从噪声中分离出来再用直观的方式呈现给研究者。你在程序的架构上保留好扩展性未来不管又来什么新型号的激光卫星数据都能快速接入同一套框架里跑起来。本文还有配套的精品资源点击获取
返回列表