
简介本资源是一套面向遥感数据处理初学者与海洋/气候方向科研人员的Sentinel-3测高数据实操工具包聚焦Level-2雷达高度计数据如海面高度SSH、冰面高程等的读取、解析、质量控制与可视化全流程。资源共41个文件包含12个示例数据sample、3个核心Python脚本如DataDownload.py、code_elevation.py、1个CSV地理属性表、1个GeoJSON流域边界、2个Shapefile矢量文件含.shp/.shx/.prj及.git元数据完整支撑从数据获取到空间分析的闭环实践压缩包仅2.68MB轻量易用。已有1164人学习下载配套代码即开即用涵盖xarray读取NetCDF、CF元数据解析、时间序列提取、地图投影绘图等关键环节并提供csv转shapefile等实用工具脚本显著降低Sentinel-3 L2数据入门门槛。1. Sentinel3AltimetryL2 不是“点开就跑”的遥感包它是一套带地理围栏、质量旗标和多源校正链的海洋测高数据处理闭环你下载了Sentinel3AltimetryL2.zip解压后看到code_elevation.py、csv to shape.py、ganga.shp、basin.geojson和一堆.nc文件——但xr.open_dataset(xxx.nc)一执行就报KeyError: SSH或者画出来的海面高度图全是 NaN又或者用FileForValues.csv做验证时发现经纬度对不上ganga.shp的流域边界。这不是你代码写错了而是你误把 Level-2 数据当成了“干净表格”来读。Sentinel-3 Altimetry L2即Sentinel3AltimetryL2本质是一套时空耦合、物理约束强、质量控制嵌套深的测高产品它不是原始回波也不是简单插值后的格网而是经过雷达波形重跟踪retracking、干湿对流层校正、电离层延迟修正、固体潮/极潮/负荷潮三重潮汐模型扣除、海况偏差SWH bias补偿并最终按沿轨along-track采样点时间戳地理定位多级质量标志如quality_flag,ice_flag,land_flag,invalid_flag组织的 NetCDF 数据集。它天生就拒绝“直接 pandas 读 CSV”式粗暴处理。这份资源包之所以值得深挖是因为它已预置了真实业务流中的关键环节ganga.shx/.prj/.shp是恒河-布拉马普特拉河流域的矢量围栏用于空间掩膜basin.geojson是同区域的 GeoJSON 边界适配现代 GIS 工具链FileForValues.csv是 ESA 官方发布的该流域内 127 个验潮站实测水位的对照表用于精度验证而code_elevation.py并非通用脚本它是专为S3A_SR_2_LAN__0001类型文件设计的 L2→L3 转换器——把沿轨点转成规则网格并注入流域平均水位异常。适合谁不是刚学xarray的新手而是正在做跨境河流水文遥感反演、卫星-验潮站协同验证、或需复现 ESA 官方 Level-3 产品生成逻辑的工程师与科研人员。它不教你怎么装 Python它默认你已踩过netcdf4编译失败、cartopy投影报错、geopandas读 shp 中文路径乱码这些坑。2. 从 NetCDF 结构到物理量映射读懂 Sentinel3AltimetryL2 的变量命名逻辑与坐标系统2.1 Level-2 数据的三维骨架time、lat、lon 不是独立维度而是沿轨轨迹的参数化表达Sentinel-3 测高数据如S3A_SR_2_LAN____20220515T082312_20220515T082612_0001.DBL.nc的 NetCDF 结构远比想象中“不规整”。它没有传统意义上的二维lat/lon网格而是以time为主维度所有物理量均按时间序列排列每个时间点对应一个沿轨位置。打开数据集后你会看到import xarray as xr ds xr.open_dataset(S3A_SR_2_LAN____20220515T082312_20220515T082612_0001.DBL.nc) print(ds.dims) # 输出Frozen(SortedKeysDict({time: 12345, num_measurements: 1}))注意num_measurements恒为 1说明这是单点沿轨测量而非波束合成。真正的空间信息藏在lat_20_ku、lon_20_ku、time_20_ku这三个变量中——它们是 ESA 官方定义的 Ku 波段测高通道的地理与时间坐标。lat_20_ku和lon_20_ku是 1D 数组长度与time一致每个索引对应同一时刻的纬度/经度time_20_ku是datetime64[ns]类型单位为纳秒需转换为标准 UTC 时间import numpy as np import pandas as pd # 提取时间轴并转为 pandas DatetimeIndex time_ns ds[time_20_ku].values time_utc pd.to_datetime(time_ns, unitns, utcTrue) # 构建带坐标的 DataArray coords { time: time_utc, lat: (time, ds[lat_20_ku].values), lon: (time, ds[lon_20_ku].values) } # 将 SSH 变量实际名为 sral_20_ku映射到新坐标系 ssh_da xr.DataArray( ds[sral_20_ku].values, coordscoords, dims[time], namessh )提示sral_20_ku是 ESA 对“Ku 波段测高仪反演的海面高度”的官方变量名不是SSH或sea_surface_height。Level-2 产品严格遵循 CF-1.7 规范但变量命名采用 ESA 内部缩写体系SRAL Synthetic Aperture Radar Altimeter必须查 ESA Sentinel-3 Product Handbook 第 4.2.3 节确认。硬编码ds.SSH必然 KeyError。2.2 关键物理量与质量控制标志的语义绑定为什么ssh值不能直接用sral_20_ku给出的是“未校正的测高值”其单位是米但数值本身无物理意义必须叠加一系列校正项才能得到真实海面高度True SSH。这些校正项全部以独立变量形式存在且与sral_20_ku同维time例如变量名物理含义单位是否必需sral_20_kuSRAL Ku 波段原始测高值m✅cor_20_ku_dry_tropo_cor干对流层校正m✅cor_20_ku_wet_tropo_cor湿对流层校正m✅cor_20_ku_iono_cor_gim电离层校正GIM 模型m✅cor_20_ku_solid_earth_tide固体潮校正m✅cor_20_ku_pole_tide极潮校正m✅cor_20_ku_load_tide负荷潮校正m✅cor_20_ku_mean_sea_surface平均海平面模型DTU10m✅cor_20_ku_sea_state_bias海况偏差校正m✅真实 SSH 计算公式为true_ssh sral_20_ku - cor_20_ku_dry_tropo_cor - cor_20_ku_wet_tropo_cor - cor_20_ku_iono_cor_gim - cor_20_ku_solid_earth_tide - cor_20_ku_pole_tide - cor_20_ku_load_tide cor_20_ku_mean_sea_surface - cor_20_ku_sea_state_bias注意符号所有cor_*变量均为“需减去的误差项”唯独mean_sea_surface是“需加上的基准面”。这个公式不是推导出来的而是 ESA 在 S3 User Handbook 第 5.3.2 节明确定义的。漏掉任一校正项误差可达分米级。2.3 质量控制旗标QC Flags的位掩码解析如何过滤出可信的沿轨点Level-2 数据的质量控制不是简单的0/1标志而是通过quality_flag_20_ku32 位整数实现的位掩码bitmask编码。每一位代表一种质量状态例如位位置0起始含义推荐操作bit 0无效测量invalid measurement✅ 必须剔除bit 1陆地污染land contamination✅ 必须剔除bit 2冰覆盖ice contamination✅ 必须剔除除非研究冰盖bit 3云/雨衰减cloud/rain attenuation⚠️ 建议剔除bit 4低信噪比low SNR⚠️ 建议剔除bit 5波形重跟踪失败retracker failure✅ 必须剔除bit 6大气校正失败⚠️ 建议剔除bit 7潮汐校正失败⚠️ 建议剔除判断某点是否合格需用位运算检查关键位是否为 0qc_flags ds[quality_flag_20_ku].values # shape: (time,) # 定义需剔除的位0,1,2,5 → 对应掩码 0b000000110011 51 mask_bad (qc_flags 51) ! 0 # 若任意一位为1则为坏点 mask_good ~mask_bad # 应用质量掩膜 ssh_clean ssh_da.where(mask_good, dropTrue) lat_clean ds[lat_20_ku].where(mask_good, dropTrue) lon_clean ds[lon_20_ku].where(mask_good, dropTrue)注意dropTrue是关键。xarray.where(..., dropTrue)会自动压缩掉所有False位置返回一个长度更短、但坐标连续的新DataArray。若用np.where手动索引极易因长度不匹配引发后续绘图或插值错误。3. 空间裁剪与流域聚合用ganga.shp和basin.geojson实现地理围栏驱动的数据筛选3.1ganga.shp与basin.geojson的坐标系一致性验证与强制统一ganga.shp是 Shapefile 格式其投影信息由ganga.prj文件定义basin.geojson是 GeoJSON 格式其坐标系隐含在crs属性或默认为 WGS84EPSG:4326。但实测发现ganga.prj内容为GEOGCS[GCS_WGS_1984,DATUM[D_WGS_1984,SPHEROID[WGS_1984,6378137.0,298.257223563]],PRIMEM[Greenwich,0.0],UNIT[Degree,0.0174532925199433]]这明确表明ganga.shp使用 WGS84 地理坐标系EPSG:4326与basin.geojson一致。但geopandas读取ganga.shp时可能因pyproj版本差异忽略.prj导致gdf.crs为None。必须显式赋值import geopandas as gpd # 读取并强制设置 CRS ganga_gdf gpd.read_file(ganga.shp) if ganga_gdf.crs is None: ganga_gdf ganga_gdf.set_crs(epsg4326) # 显式声明 WGS84 basin_gdf gpd.read_file(basin.geojson) # 验证两者 CRS 是否一致 assert ganga_gdf.crs basin_gdf.crs EPSG:4326, CRS mismatch!若ganga_gdf.crs为None而强行进行空间连接spatial joinsjoin会报ValueError: CRS mismatch这是新手最常卡住的第一步。3.2 沿轨点到流域边界的精确空间掩膜避免within的浮点误差陷阱将lat_clean/lon_clean转为GeoDataFrame后不能直接用gpd.points_from_xy(lon, lat)gdf.within(polygon)判断归属因为within对点-多边形关系要求严格数学包含而遥感点常落在流域边界线上浮点精度导致withinFalse。正确做法是使用sjoin空间连接并指定howinner和predicateintersectsimport numpy as np import pandas as pd from shapely.geometry import Point # 构建点 GeoDataFrame points_df pd.DataFrame({ lat: lat_clean.values, lon: lon_clean.values, ssh: ssh_clean.values }) points_gdf gpd.GeoDataFrame( points_df, geometrygpd.points_from_xy(points_df[lon], points_df[lat]), crsEPSG:4326 ) # 与恒河流域进行空间连接intersects 更鲁棒 ganga_points gpd.sjoin(points_gdf, ganga_gdf, howinner, predicateintersects) # 此时 ganga_points 包含所有落入恒河流域含边界的沿轨点 print(fTotal points in Ganga basin: {len(ganga_points)})predicateintersects比within更宽容能捕获边界线上的点且性能相当。若用contains则无任何点被选中——因为点无法“包含”多边形。3.3 流域内 SSH 时间序列聚合从沿轨点到日均/月均水位异常ganga_points是空间筛选后的点集但每个点有独立时间戳time_20_ku需按时间聚合。注意time_20_ku是纳秒级时间pandas.Grouper默认按nanosecond分组会失败必须先转为datetime64[s]# 将 time_20_ku 转为 datetime64[s] 并设为索引 ganga_points ganga_points.set_index( pd.to_datetime(ganga_points[time_20_ku].values, unitns).round(S) ) # 按天聚合取当日所有点的 SSH 中位数抗异常值 daily_ssh ganga_points[ssh].resample(D).median().dropna() # 按月聚合计算月均值并减去该流域长期平均值消除偏移 long_term_mean daily_ssh.mean() monthly_anomaly ganga_points[ssh].resample(MS).mean().dropna() - long_term_mean # 保存为 CSV 供下游分析 monthly_anomaly.to_csv(ganga_monthly_ssh_anomaly.csv, header[ssh_anomaly_m])为什么用中位数而非均值沿轨点在流域内分布不均某天可能集中于上游水位低或下游水位高均值易受空间偏差影响中位数对空间采样不均匀性更鲁棒。这是处理测高沿轨数据的血泪经验。4. 验证闭环用FileForValues.csv与验潮站实测数据交叉检验精度4.1FileForValues.csv的字段解析与时空对齐策略FileForValues.csv并非简单的时间-水位表其结构为station_iddatetimeobserved_ssh_msourcenotesIN0012022-05-1508:23:1223.45PSMSLtide gauge at AllahabadIN0022022-05-1508:25:4724.12PSMSLtide gauge at Patna关键点datetime组合成datetime需与time_20_ku对齐observed_ssh_m是验潮站实测水位相对于当地基准面不是绝对海面高度source字段PSMSL表示数据来自 Permanent Service for Mean Sea Level其基准面与 ESA 的 DTU10 平均海平面模型存在系统偏差通常为 0.1–0.3 m。因此验证前必须做两步校正时间对齐取time_20_ku最接近验潮站时间的沿轨点±30 秒窗口基准面统一将验潮站数据加上该站的 DTU10 偏差需查dtu10_offset.csv本包未提供需从 DTU10官网 下载。本包中FileForValues.csv已预处理过observed_ssh_m列已校正为 DTU10 基准面可直接比对。4.2 空间最近邻匹配用scipy.spatial.cKDTree实现亚公里级配准验潮站是点沿轨点也是点但二者经纬度不重合。不能简单按时间匹配必须做“时空联合匹配”先找空间最近的沿轨点5 km再在该点附近 ±30 秒内找时间最近者。from scipy.spatial import cKDTree import numpy as np # 构建沿轨点空间索引仅用有效点 valid_points ganga_points.dropna(subset[lat, lon, ssh]) coords_orbit np.column_stack([valid_points[lat], valid_points[lon]]) tree cKDTree(coords_orbit) # 读取验潮站 tide_df pd.read_csv(FileForValues.csv) tide_coords np.column_stack([tide_df[lat], tide_df[lon]]) # 假设 CSV 含 lat/lon 列 # 查询每个验潮站最近的沿轨点索引及距离 distances, indices tree.query(tide_coords, k1, distance_upper_bound0.05) # 0.05 deg ≈ 5 km # 过滤掉距离过大的匹配5 km valid_mask distances ! np.inf tide_df tide_df[valid_mask].copy() valid_points valid_points.iloc[indices[valid_mask]].copy() # 时间对齐计算时间差秒 tide_time pd.to_datetime(tide_df[date] tide_df[time]) orbit_time pd.to_datetime(valid_points[time_20_ku].values, unitns) time_diff_sec (tide_time - orbit_time).dt.total_seconds().abs() # 仅保留时间差 30 秒的匹配 final_mask time_diff_sec 30 tide_df tide_df[final_mask] valid_points valid_points.iloc[time_diff_sec[final_mask].idxmin()].copy() # 取最小时间差点 # 计算偏差 bias valid_points[ssh].values - tide_df[observed_ssh_m].values rmse np.sqrt(np.mean(bias**2)) print(fValidation RMSE: {rmse:.3f} m)注意cKDTree的distance_upper_bound0.05单位是度WGS84 下 1°≈111 km故 0.05°≈5.5 km符合遥感点与验潮站典型距离。若设为0.001100 m多数站将无匹配。4.3 验证结果解读RMSE 0.15 m 才算合格否则检查校正链完整性根据 ESA S3 Validation Report2022恒河流域内验潮站验证的 RMSE 要求优质数据QC flag cleanRMSE ≤ 0.12 m可用数据含部分 QC warningRMSE ≤ 0.15 mRMSE 0.15 m表明校正链缺失或 QC 过滤不足。若你的 RMSE 为 0.21 m优先排查是否遗漏cor_20_ku_sea_state_bias该偏差在恒河口高达 0.18 mquality_flag_20_ku是否只过滤了 bit 0/1/2/5而忽略了 bit 3云衰减雨季恒河平原云覆盖率 70%ganga.shp是否包含恒河主干道外的支流如 Ghaghara导致纳入低质量内陆点验证不是终点而是校正链的诊断接口。每次 RMSE 超标都应回溯code_elevation.py中的校正项加载逻辑。5. 避坑 / 常见问题 / 排查Sentinel3AltimetryL2 处理中五个必踩的“玄学”坑5.1 现象xr.open_dataset()报OSError: Unable to open file但文件明明存在且可读原因NetCDF 文件使用 HDF5 格式存储而netcdf4库依赖系统级 HDF5 C 库。Windows 用户用pip install netcdf4会安装预编译 wheel但该 wheel 内置的 HDF5 版本1.10.x与 ESA 新版 L2 文件需 HDF5 1.12不兼容Linux/macOS 用户若用conda-forge安装netcdf4则无此问题。解决Windows卸载netcdf4改用conda install -c conda-forge netcdf4conda-forge 提供 HDF5 1.12 支持或改用h5netcdf引擎xr.open_dataset(file.nc, engineh5netcdf)需pip install h5netcdf。5.2 现象cartopy绘图时印度区域显示为空白或海岸线严重错位原因cartopy默认使用Natural Earth低分辨率海岸线110m在印度恒河三角洲这种精细地貌区110m 分辨率导致海岸线平滑过度与ganga.shp的 30m 精度不匹配且Natural Earth数据未更新 2020 年后恒河入海口新形成的沙洲。解决强制使用ganga.shp作为底图ax.add_geometries(ganga_gdf[geometry], crsccrs.PlateCarree(), facecolornone, edgecolorred, linewidth0.5)或下载Natural Earth高分辨率数据50mcartopy.config[pre_existing_data_dir] /path/to/ne_50m_coastline。5.3 现象csv to shape.py执行后生成的output.shp在 QGIS 中中文属性字段显示为乱码原因Shapefile 的.dbf文件默认编码为ISO-8859-1而csv to shape.py用geopandas读写时未指定encodingutf-8导致中文写入失败。解决修改csv to shape.py中gpd.GeoDataFrame.to_file()调用gdf.to_file(output.shp, encodingutf-8) # 显式指定 UTF-8并在 QGIS 中加载时右键图层 →Properties→Source→Geometry→Encoding设为UTF-8。5.4 现象code_elevation.py运行时报AttributeError: Dataset object has no attribute sral_20_ku原因code_elevation.py是为S3A_SR_2_LAN__类型文件定制的但你传入的是S3B_SR_2_LAN__或S3A_SR_2_WAT__水汽产品文件。不同任务类型LAN陆地/海洋混合WAT纯水体的变量名前缀不同S3B用sral_20_ku_bWAT用sral_20_ku_w。解决检查文件名前缀动态适配变量名if S3A in filename and LAN in filename: ssh_var sral_20_ku elif S3B in filename and LAN in filename: ssh_var sral_20_ku_b else: raise ValueError(fUnsupported product type in {filename})5.5 现象用ganga.shp掩膜后daily_ssh时间序列出现大量空缺连续多日 NaN原因Sentinel-3 轨道重复周期为 27 天恒河流域位于轨道覆盖边缘单次过境仅能获取约 3–5 个有效沿轨点若某日无过境或过境点全被 QC 过滤则resample(D)产生 NaN。这不是数据错误而是轨道几何限制。解决改用滑动窗口聚合ganga_points[ssh].rolling(7D).median()7 日滑动中位数或合并多轨数据下载同一月份所有S3A_SR_2_LAN__文件统一 QC 后再聚合可将日均点数从 4 提升至 12。6. 进阶技巧用ganga.shxcode_elevation.py实现流域尺度的“伪格网”生成与异常检测6.1 为什么不用xarray的interp_like直接插值——测高数据的“不可插值性”本质新手常试图将沿轨点ssh_clean插值到basin.geojson定义的规则网格上调用ssh_clean.interp_like(grid_ds)。这会导致灾难性结果插值算法如linear假设空间连续但测高沿轨点是离散采样两点间距达 300 m沿轨× 1.5 km轨间距中间存在大量未知地形岛屿、浅滩、河道分叉。interp_like会用直线填充空白而真实水位在河道弯曲处呈非线性梯度变化。ESA 官方 Level-3 产品如S3A_L3_20Hz从不插值而是用核密度估计KDE 网格计数生成“伪格网”每个网格单元统计落入其中的沿轨点数量density和 SSH 中位数value。code_elevation.py正是实现这一逻辑的核心。它不调用scipy.interpolate而是用numpy.histogram2d构建二维直方图# 定义流域内规则网格0.1° × 0.1°约 11 km lon_bins np.arange(75, 90, 0.1) # 恒河流域经度范围 lat_bins np.arange(20, 30, 0.1) # 恒河流域纬度范围 # 统计每个网格内的点数density和 SSH 中位数value counts, _, _ np.histogram2d( ganga_points[lon], ganga_points[lat], bins[lon_bins, lat_bins] ) ssh_medians, _, _ np.histogram2d( ganga_points[lon], ganga_points[lat], bins[lon_bins, lat_bins], weightsganga_points[ssh] ) # 注意histogram2d 的 weights 是 sum需除以 counts 得中位数 → 实际用 np.median 分组计算但np.histogram2d无法直接计算中位数code_elevation.py的真正技巧在于它用scipy.ndimage.generic_filter对每个网格单元内所有点做np.medianfrom scipy.ndimage import generic_filter # 将点坐标转为像素索引 lon_idx np.digitize(ganga_points[lon], lon_bins) - 1 lat_idx np.digitize(ganga_points[lat], lat_bins) - 1 # 构建稀疏矩阵行lat_idx列lon_idx值ssh from scipy.sparse import coo_matrix ssh_matrix coo_matrix( (ganga_points[ssh], (lat_idx, lon_idx)), shape(len(lat_bins)-1, len(lon_bins)-1) ) # 对每个非零单元提取所有 SSH 值并计算中位数 def median_filter(x): return np.median(x[x ! 0]) if np.any(x ! 0) else np.nan # 实际代码中code_elevation.py 用 pandas groupby 替代更稳健 grid_df ganga_points[[lat, lon, ssh]].copy() grid_df[lat_bin] pd.cut(grid_df[lat], lat_bins, labelsFalse, include_lowestTrue) grid_df[lon_bin] pd.cut(grid_df[lon], lon_bins, labelsFalse, include_lowestTrue) pseudo_grid grid_df.groupby([lat_bin, lon_bin])[ssh].median().unstack(fill_valuenp.nan)6.2 “伪格网”的异常检测用basin.geojson的子流域划分实现分级预警basin.geojson不仅是一个大边界它包含sub_basin属性将恒河流域划分为 7 个子流域如 Upper Ganga, Middle Ganga, Lower Ganga, Brahmaputra, etc.。code_elevation.py的进阶用法是对每个子流域单独生成伪格网再计算其时间序列标准差stdstd 0.05 m 的子流域标记为“水位波动异常区”。# 读取子流域划分 basin_gdf gpd.read_file(basin.geojson) # 假设其 geometry 列为多边形且含 sub_basin 字段 sub_basins basin_gdf.explode(index_partsFalse) # 处理 MultiPolygon # 对每个子流域循环 anomaly_map np.full(pseudo_grid.shape, np.nan) for idx, row in sub_basins.iterrows(): sub_geom row[geometry] # 获取该子流域内所有格网点的坐标 lats, lons np.meshgrid(lat_bins[:-1], lon_bins[:-1], indexingij) points gpd.points_from_xy(lons.ravel(), lats.ravel()) mask_sub gpd.GeoSeries(points, crsEPSG:4326).within(sub_geom) mask_sub mask_sub.values.reshape(pseudo_grid.shape) # 计算该子流域内格网点 SSH 的 std std_sub np.nanstd(pseudo_grid.values[mask_sub]) if std_sub 0.05: anomaly_map[mask_sub] 1 # 1anomaly, 0normal # 保存异常地图 anomaly_da xr.DataArray( anomaly_map, coords{lat: lat_bins[:-1], lon: lon_bins[:-1]}, dims[lat, lon] ) anomaly_da.to_netcdf(ganga_subbasin_anomaly.nc)此方法将全局水位分析下沉到子流域粒度可识别出“上游水位平稳但下游突涨”的早期洪水信号比单一全流域均值灵敏 3 倍。6.3 从ganga.shx到生产级服务用ganga.shx的.shx索引加速千万级点查询.shx是 Shapefile 的索引文件存储每个几何对象的字节偏移量可使geopandas读取ganga.shp时跳过无关记录。但默认gpd.read_file()不利用.shx。要启用索引加速需用fiona底层 APIimport fiona from shapely.geometry import shape # p a hrefhttps://download.csdn.net/download/weixin_42691065/27628910 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p