ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

NPP趋势分析:Theil-Sen与Mann-Kendall的Python实现全攻略

NPP趋势分析:Theil-Sen与Mann-Kendall的Python实现全攻略 做遥感时间序列分析的人大概率都跟NPP打过交道。净初级生产力Net Primary Productivity, NPP是评估植被固碳能力和生态系统健康状况的核心指标MODIS的MOD17A3产品可以稳定提供2001年至今逐年、500米分辨率的全球NPP数据。数据拿到手之后大家问得最多的一个问题就是这二十几年我研究区里的植被生产力到底是在变好还是变差回答这个问题目前学术圈用得最广、审稿人也最认的一套工具就是Theil-Sen Median斜率估计配合Mann-Kendall趋势检验。这套组合在NDVI、LAI、GPP、NPP等各类植被参数的趋势分析里几乎成了标配但说句实话能把原理讲明白、把代码一次跑通、把结果正确解读出来的人并不多。这篇文章就以多年NPP数据为例把算法原理、数据预处理、Python实现、结果出图和避坑经验一条龙讲清楚不管是刚入门的研究生还是已经在处理遥感大数据、需要批量产出趋势图的从业者都能直接照着操作。1. 为什么不用普通线性回归项目思路拆解1.1 NPP趋势分析到底在回答什么问题做NPP趋势分析的动机通常很朴素研究区这些年植被固碳能力是在增强还是减弱气候变化背景下温度升高、降水格局改变植被生产力是否出现了可检测的方向性变化这类问题看起来简单实际分析时却绕不开三个核心指标变化方向增加还是减少、变化幅度每年变化多少、显著性这个变化是否可信。如果手里只有两个时间点的NPP做差值就够了但像MOD17A3这种从2001年连续出到今天的年度产品已经积累了超过20期。时间序列的优势在于可以分离出长期趋势和年际波动比如某一年因为极端干旱导致的NPP暴跌并不代表植被在持续退化反过来某一年风调雨顺出现的NPP峰值也不能证明生态系统在变好。趋势分析要做的是从这些波动中提取稳定信号。NPP趋势结果通常直接服务于生态修复成效评估、碳汇潜力计算、自然保护区管理以及模型验证等工作所以分析结果的稳健性非常重要不能因为一两个极端年份就把结论带偏。1.2 Theil-Sen Mann-Kendall组合的优势先说说为什么不直接用Excel里那个普通最小二乘线性回归。NPP时间序列有几个特征第一非正态植被生产力受降水、温度、病虫害、火灾等影响年与年之间经常出现厚尾分布第二存在极端值比如异常干旱年份NPP骤降这些点对最小二乘拟合影响极大第三样本量往往只有十几到二十几个小样本下更不满足回归分析对误差正态性和方差齐性的要求。普通线性回归在这些条件下斜率的估计值很容易被一两个离群点带偏显著性检验也会失真。Theil-Sen Median斜率估计和Mann-Kendall检验都是非参数方法不假设数据服从特定分布对离群值天然免疫。Theil-Sen的核心思想是计算所有数据点两两之间的斜率然后取中位数作为整体趋势斜率这相当于把一个极端点对结果的影响降到了最低。Mann-Kendall检验则是基于数据的秩次而不是原始数值通过比较所有前后样本对的大小关系来判断是否存在单调趋势即使序列里有部分缺测值也能处理而且它检验的是稳健的单调趋势不要求趋势一定是直线。这两个方法配合使用一个给变化幅度一个给显著性判断互补性非常强这也是我在实际项目中处理几十个像元、几十幅影像时首选这套组合的原因。2. 两个核心算法的原理这次彻底讲清楚2.1 Theil-Sen取中位数而不是最小二乘Theil-Sen斜率估计的原理一句话就能概括对时间序列上的任意两个点用纵坐标之差除以时间间隔得到一条斜率把所有两两组合的斜率取中位数就是序列的整体趋势斜率。假设有两个年份i和j对应的NPP值为y_i和y_j那么这两点间的斜率为slope_ij (y_j - y_i) / (j - i)对n期数据一共有n(n-1)/2个这样的斜率Sens slope就是这些斜率的中位数。比如有5年数据n(n-1)/2 10个斜率把这10个值从小到大排列取第5和第6个的平均值偶数个时就是最终斜率。这个斜率代表每单位时间通常是每一年NPP的变化量单位是g C/m²/yr每年。为什么取中位数这么管用还是拿5年数据举例子假设其中一年NPP因为严重干旱从500掉到了200普通最小二乘会把这一年当成真实现象去拟合斜率会被明显拉低但Theil-Sen只需要这一个点对参与形成的若干斜率被其他正常年份形成的斜率中和掉中位数天然不受少数极端斜率影响。这种稳健性还有一个副产品我不需要对数据做平滑、剔除异常值等预处理减少了主观干预结果可重复性也更高。2.2 Mann-Kendall从秩次角度看趋势Mann-Kendall检验本质上是在回答一个问题这个序列是否存在单调上升或下降的趋势它不关心中间的波动到底有多大只看数据的相对顺序。具体做法是计算统计量SS Σ_{k1}^{n-1} Σ_{jk1}^{n} sign(x_j - x_k)其中sign函数在x_j大于x_k时取1相等时取0小于时取-1。如果序列整体在上升后面的值普遍比前面的值大S就是较大的正数如果整体下降S是较大的负数如果没有趋势S会接近0。但S本身的大小受样本量影响不好直接用。当n大于10时可以用正态近似把S标准化为Z统计量需要先计算S的方差Var(S) [n(n-1)(2n5) - Σ t_i(t_i-1)(2t_i5)] / 18其中t_i是第i组相同值的个数这个修正项用于处理序列中出现的结相等值。然后用连续性修正公式计算Z值S大于0时Z (S-1)/√Var(S)S小于0时Z (S1)/√Var(S)S等于0时Z等于0。最后通过标准正态分布算出双尾p值。原假设H0是序列无单调趋势如果p值小于显著性水平通常取0.05就拒绝原假设认为趋势显著。有个细节值得注意MK检验检验的是单调趋势它不要求趋势是直线。只要数据持续上升哪怕上升速率在变化MK也能识别出来这对生态数据其实特别友好因为NPP的长期变化往往受多种因素叠加很难是严格线性关系。2.3 斜率与显著性合起来怎么看单独看Sen斜率可能得到一个正数但不知道这个正数是不是噪声导致的单独看MK检验的p值知道趋势显著但不知道变化幅度到底有多大。所以标准做法是两者结合。判断结果条件含义显著增加Sen斜率 0 且 p 0.05NPP在显著上升生态状况改善不显著增加Sen斜率 0 且 p ≥ 0.05有上升迹象但证据不足稳定Sen斜率 0 或接近0无明显变化不显著减少Sen斜率 0 且 p ≥ 0.05有下降迹象但证据不足显著减少Sen斜率 0 且 p 0.05NPP在显著下降需要警惕这套分级方法在论文里非常常见。我在实际项目中还会加一步如果研究区很大几十万个像元同时做检验会出现多重比较问题即纯噪声数据中也会有一定比例的假显著性。这种情况下可以再做一次FDRBenjamini-Hochberg校正对p值这一列做整体调整再判断显著性。虽然很多论文不做这一步但审稿人问到的时候做了就比没做主动。3. 多年NPP数据来源选择与预处理3.1 NPP数据源怎么选目前做年度NPP趋势分析最常用的就是MODIS的MOD17A3HGF产品。这个产品提供2001年至今的逐年全球NPP空间分辨率500米单位是kg C/m²/yr存储时乘以了10000的缩放系数也就是说影像里的DN值乘以0.0001才等于真实NPP值。MOD17A3HGF在MOD17A3的基础上用了HGFHigh-resolution daily Gap-Filled算法对受云污染的遥感输入做了插补连续性更好是趋势分析的首选。如果需要更长时间序列可以选用GIMMS3g NDVI反演的NPP数据能够追溯到1981年但空间分辨率只有8公里适合大区域宏观分析不适合县级这类小尺度研究。还有一些人用BEPS、CASA等模型自己估算NPP灵活性高但需要大量气象和遥感输入数据工作量会明显增加。我个人的经验是先明确研究区尺度省域以上用粗分辨率长序列做宏观判断县市级和具体样地用MOD17A3HGF的500米产品两者结论可以相互印证。3.2 下载与预处理从原始分片到标准时间序列MOD17A3HGF是按全球瓦片发布的比如h26v04、h27v04下载方式主要有三种NASA AppEEARS在线提取、USGS EarthExplorer下载原始瓦片、Google Earth Engine直接filter导出。研究区跨多个瓦片时需要先拼接再裁剪。下载时建议直接把QC质量控制波段一起下载后面用得着。拿到每一年NPP影像之后预处理顺序基本是这样先检查投影和坐标系统一到WGS84或研究区所在的投影坐标系然后裁剪到研究区边界接着读像元值并乘以0.0001还原成真实NPP单位kg C/m²/yr如果要跟文献里常见的g C/m²/yr对齐再乘1000最后把多年影像按时间顺序堆叠成一个三维数组n_year, rows, cols方便后续逐像元计算。这里有个特别容易翻车的点MOD17A3HGF的单位是kg C/m²/yr很多文献里的NPP趋势斜率单位是g C/m²/yr²如果不做单位换算结果会差1000倍趋势图上的颜色分级完全没法跟别人的研究对比。我习惯一开始就把单位统一换算成g C/m²/yr后面所有中间结果都带单位标注省得事后返工。3.3 数据质量控制与异常值处理MOD17A3HGF虽然做了gap-filling但个别像元在多年里仍然可能出现异常值或无效值。质量控制的第一步是结合QC波段把质量差、填充比例过高的像元标记为无效第二步是结合土地利用数据把水体、城市、裸地等不生长植被的区域直接掩膜掉这些像元的NPP本身没有生态学意义计算趋势不但浪费算力还会让统计结果失真。时间序列中的极端单年值要谨慎处理。比如研究区某年发生严重干旱NPP只有正常年份的一半这个点从数据质量角度是真实的不能简单剔除否则人为制造趋势。正确的做法是保留真实极端值让Theil-Sen和MK这两个稳健方法自己去消化。但如果发现某个像元出现物理上不可能的数值比如NPP为负、数值跳变量异常大就要仔细检查是不是数据配准或拼接出了问题。一般情况下单像元有效年份低于10年的我建议直接不计算趋势样本太少算出来的结果没有统计意义出图时显示为空白即可。4. Python全流程实操从单点验证到逐像元批量4.1 环境准备与依赖安装整个流程用到的库不算多核心是科学计算和栅格读写两块。我推荐直接创建一个干净的conda环境然后安装下面这些依赖pip install numpy pandas scipy matplotlib rasterio pymannkendall joblib其中pymannkendall是专门做多种Mann-Kendall变体检验的库比手搓MK方便得多后面会用到。scipy提供了现成的theilslopes函数不用自己写两两斜率循环。rasterio负责读写GeoTIFFjoblib用来做并行加速。版本方面我目前用的是Python 3.10、numpy 1.24、scipy 1.10都比较稳定。4.2 单点时间序列验证先跑通一个像元批处理之前一定要先拿一个像元把流程验一遍不然几十万像元跑完才发现错误代价太大。假设我已经把研究区某像元2001年到2020年的NPP逐年值读出来了单位是g C/m²/yrimport numpy as np from scipy import stats import pymannkendall as mk # 模拟一个像元的20年NPP序列单位 g C/m2/yr years np.arange(2001, 2021) npp np.array([612, 608, 631, 645, 602, 655, 668, 672, 659, 681, 690, 672, 685, 701, 716, 708, 725, 731, 740, 729]) # Theil-Sen斜率估计 res stats.theilslopes(npp, years) sen_slope res.slope print(fSens slope {sen_slope:.3f} (g C/m2/yr per year)) print(f截距 {res.intercept:.2f}) # Mann-Kendall趋势检验 mk_result mk.original_test(npp) print(mk_result)这段代码的输出里mk_result会给出trend趋势方向、h是否拒绝原假设、pp值、z标准化统计量、TauKendall tau、sS统计量、var_sS方差、slope和intercept。拿到结果后先仔细看看p值和slope是否符合直觉再开始批处理。比如上面这个模拟序列整体是上升的Sen斜率大约5.73p值远小于0.05trend显示increasingh为True这就是一个显著增加的像元。scipy的theilslopes会自动处理数据中的NaN但pymannkendall的original_test不会自动剔除NaN所以在批量处理时我会先对每个像元把NaN滤掉确保传入的序列是干净且按时间顺序排列的。4.3 全区域逐像元批量计算向量化与并行有了单点验证的基础接下来就是把计算扩展到全区域。最笨的写法是两层for循环逐像元调用pymannkendall好处是代码简单坏处是慢得离谱。以1000×1000的像元矩阵为例100万个像元每个都要做一次Theil-Sen加MK纯Python循环跑一天都跑不完。更聪明的做法是利用numpy的向量化能力。Theil-Sen斜率本质上是对所有两两差分的操作MK的S统计量也是对两两符号差求和这两种操作都可以转成矩阵运算。我先写一个向量化的Theil-Sen斜率函数def theil_sen_slope_vectorized(data2d): 对每个像元列计算Theil-Sen斜率 data2d: (n_time, n_pixels) 的二维数组 n_time, n_pixels data2d.shape idx np.arange(n_time) i_idx, j_idx np.meshgrid(idx, idx, indexingij) valid_pair j_idx i_idx i_flat i_idx[valid_pair] j_flat j_idx[valid_pair] denom (j_flat - i_flat).astype(np.float64) # 所有两两差分每行是一对时间点每列是一个像元 diffs data2d[j_flat, :] - data2d[i_flat, :] slopes diffs / denom[:, None] slopes np.where(np.isfinite(slopes), slopes, np.nan) sen_slope np.nanmedian(slopes, axis0) return sen_slopeMK的S统计量同样可以向量化def mk_s_statistic_vectorized(data2d): 计算每个像元的Mann-Kendall S统计量 n_time, n_pixels data2d.shape idx np.arange(n_time) i_idx, j_idx np.meshgrid(idx, idx, indexingij) valid_pair j_idx i_idx i_flat i_idx[valid_pair] j_flat j_idx[valid_pair] x_i data2d[i_flat, :] x_j data2d[j_flat, :] s np.sum(np.sign(x_j - x_i), axis0) return s有了S统计量以后还需要计算方差、Z和p值。这里要注意一个细节如果序列里有相等的值需要做结修正否则方差不准确。为了处理结修正我通常会保留一个对每个像元统计相同值数量的步骤像元数很多时这个操作稍微有点慢但比逐像元跑整个MK要快得多from scipy import stats as sp_stats def mk_z_p_value_vectorized(data2d): n_time, n_pixels data2d.shape s mk_s_statistic_vectorized(data2d) # 结修正项 tie_corr np.zeros(n_pixels) for p in range(n_pixels): col data2d[:, p] _, counts np.unique(col, return_countsTrue) counts counts[counts 1] if len(counts) 0: tie_corr[p] np.sum(counts * (counts - 1) * (2 * counts 5)) var_s (n_time * (n_time - 1) * (2 * n_time 5) - tie_corr) / 18.0 var_s np.maximum(var_s, 1e-12) z np.zeros_like(s, dtypenp.float64) z[s 0] (s[s 0] - 1) / np.sqrt(var_s[s 0]) z[s 0] (s[s 0] 1) / np.sqrt(var_s[s 0]) p 2 * (1 - sp_stats.norm.cdf(np.abs(z))) return z, p向量化函数虽然快但要注意内存。比如20期数据两两组合有190对100万个像元时slopes矩阵是190×1000000浮点64位就是1.5GB很容易把内存吃光。我的办法是分块处理每批读几万像元用向量化函数算完存结果再读下一批。配合joblib做多进程并行速度非常理想。实测下来100万像元、20期数据分块加并行大概几分钟就能跑完。4.4 结果保存为GeoTIFF计算得到sen_slope和p之后下一步是保存成带地理信息的GeoTIFF方便后续在ArcGIS、QGIS里出图。保存的关键是沿用原始的投影、地理范围和分辨率import rasterio def write_result(output_path, data, reference_tif): with rasterio.open(reference_tif) as src: profile src.profile.copy() profile.update(dtyperasterio.float32, count1, compresslzw, nodata-9999) with rasterio.open(output_path, w, **profile) as dst: dst.write(data.astype(rasterio.float32), 1)我一般会同时输出三个文件sen_slope.tif趋势斜率、mk_pvalue.tifp值、trend_class.tif分级结果。这三个文件是后续统计和出图的原材料。另外提醒一句保存之前记得把NaN像元统一替换成nodata值比如-9999不然很多后期处理软件不认NaN。5. 趋势结果解读与出图5.1 趋势等级划分标准拿到slope和p值之后不能直接拿slope去出图因为slope的值域跨度大颜色很难映射。我习惯先按生态含义分成五类前面2.3的表格就是分类依据。具体代码trend_class np.zeros_like(sen_slope, dtypenp.uint8) trend_class[(sen_slope 0) (p_value 0.05)] 1 # 显著增加 trend_class[(sen_slope 0) (p_value 0.05)] 2 # 不显著增加 trend_class[(sen_slope 0)] 3 # 基本不变 trend_class[(sen_slope 0) (p_value 0.05)] 4 # 不显著减少 trend_class[(sen_slope 0) (p_value 0.05)] 5 # 显著减少有时候研究区范围小、趋势比较一致五类会出现某一类占比很小的情况这时可以合并成三类显著增加、显著减少、无明显趋势。但不管是几类出图时一定要在图例里写清楚分类条件和显著性阈值不能只给颜色否则读者无法判断图的实际含义。5.2 面积统计与分区分析趋势图出来以后我通常还会做一步定量统计因为论文和报告里不能只放一张图还得有一个显著增加面积占研究区比例、显著减少面积占XXX%的段落。用np.unique加上分类结果就能算面积占比import numpy as np unique, counts np.unique(trend_class, return_countsTrue) area_km2 counts * 0.25 # 500米分辨率像元面积约0.25 km² total_area area_km2.sum() for cls, cnt, area in zip(unique, counts, area_km2): print(f类别{cls}: 面积 {area:.1f} km², 占比 {area/total_area*100:.2f}%)注意这个0.25km²只有在等积投影下才严格成立如果影像本身是经纬度坐标像元面积随纬度变化严谨做法是投影到Albers或Lambert等积投影后再统计。更进一步我常常把趋势分类结果跟土地覆盖类型叠加分析比如统计林地里显著增加的像元有多少、草地退化面积有多大这一步能直接把趋势分析结果落到生态管理场景里。做法很简单对每个土地覆盖类型单独统计各类趋势像元占比然后画堆叠柱状图。5.3 趋势结果的可视化建议出图是很多人的痛处。我的经验是二级专题图优先展示趋势分类结果用顺序色或分类色表达五类配一个研究区位置小图一级研究图可以用连续slope作底图、用点画或透明度叠加显著性信息或者直接展示slope和p的双变量图。绘图用matplotlib配合cartopy或basemap都可以import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap cmap ListedColormap([#1a6e1a, #8dd3a1, #f7f7f7, #f7b6b6, #b2182b]) fig, ax plt.subplots(figsize(8, 8)) im ax.imshow(trend_class, cmapcmap, vmin0.5, vmax5.5) ax.set_title(NPP Trend Classification (2001-2020))除了空间分布图我还会选几个典型像元画出原始时间序列叠加Sen斜率线这能非常直观地展示统计量背后到底长什么样。比如找一个显著增加和显著减少的像元各画一幅读者对结果可信度会更有直观判断。唯一要注意的是matplotlib默认颜色比较丑出正式图前花点时间配置一下字体和中文字体支持。6. 实操中遇到的那些坑常见问题与排查技巧6.1 时间序列自相关导致假显著这是我在做植被指数趋势分析时踩过最深的坑。Mann-Kendall检验假设样本之间相互独立但NPP这种生态数据本身就有很强的年际持续性比如湿润年份之后往往孕育了更好的植被条件次年NPP也偏高。这种正自相关会让MK检验的方差被低估p值偏小明明没有真实趋势检验结果却显示显著这就是假显著。解决办法有两类。一类是先用残差或预处理消除自相关常用的有预漂白Pre-Whitening和趋势自由预漂白TFPWTrend-Free Pre-Whitening原理是先估出趋势项并去掉对剩余项做一阶自回归过滤再把过滤后的残差和趋势项加回去重新检验。另一类是直接使用考虑自相关的修正MK检验pymannkendall里带了一批现成的# 原版MK result_original mk.original_test(npp) # 考虑自相关的Hamed Rao修正MK result_modified mk.hamed_rao_modification_test(npp) # 趋势自由预漂白MK result_tfpw mk.trend_free_prewhitening_test(npp)我通常会在正式计算前先对研究区随机抽几百个像元对比original_test和modified_test的结果如果两者显著性差异很大说明序列自相关明显则全区域改用修正版本。虽然修正在算法上更稳妥但论文中描述时也要写清楚用的是哪种版本方便别人复现。6.2 计算慢到怀疑人生怎么办早期我用纯Python循环跑全区域趋势分析1000×1000的像元矩阵跑了整整一晚上。后来总结下来优化重点有三个向量化、分块、并行。前面的4.3已经写了向量化思路这里再说分块和并行的关键细节。分块的核心是控制内存。假设20期数据两两组合190对每批处理5000个像元slopes矩阵只有190×5000×8字节约7.6MB这个体量可以随便算。分块代码大致是from joblib import Parallel, delayed def process_chunk(chunk_data): # chunk_data: (n_time, n_chunk) slope theil_sen_slope_vectorized(chunk_data) z, p mk_z_p_value_vectorized(chunk_data) return slope, p # 将像元分成块 n_pixels stack_3d.shape[1] * stack_3d.shape[2] pixels_2d stack_3d.reshape(stack_3d.shape[0], -1) chunk_size 5000 chunks [pixels_2d[:, i:ichunk_size] for i in range(0, n_pixels, chunk_size)] # 多进程并行 results Parallel(n_jobs8, verbose1)(delayed(process_chunk)(c) for c in chunks)需要注意两点一是mask掉无效像元后再做向量化否则NaN会扩散二是如果研究区面积不大直接用scipy和pymannkendall逐像元算也行没必要为了优化而优化。另外有些区域数据已经发布在云端平台比如大区域时序产品直接在上面处理再下载结果比本地跑更省事。6.3 单位换算与坐标系翻车现场NPP单位问题我之前提过这里再强调一遍因为实际项目中每次都有学生来问。MOD17A3HGF原始DN值需要乘以0.0001才是kg C/m²/yr如果要转成g C/m²/yr还要再乘1000。也就是说DN值10000对应的NPP是1 kg C/m²/yr也就是1000 g C/m²/yr。如果直接在DN值上算斜率得到的斜率单位是DN/年跟文献里g C/m²/yr²完全没法比。我的经验是导入数据后第一时间完成单位换算并将结果另存之后所有脚本都基于换算后的数据运行。坐标系是另一个高频翻车点。原始TIF可能是经纬度坐标EPSG:4326也可能是正弦投影直接做面积统计和分区统计会得出错误结果。在进行面积占比统计前一定要把数据重投影到研究区所在区域合适的等积投影。重投影本身会引入插值误差所以最好在预处理阶段统一完成而不是在已经计算完趋势之后再做投影。6.4 时间长度与数据断点问题MK检验的效果跟样本量高度相关。少于10年数据时检验功效非常低即使真实存在趋势也容易检不出来而样本量越多年份越多趋势判断越可靠。因此我一般建议至少使用15期以上的年度数据做分析低于10年的结果不建议进入论文正式结果可以作为初步探索。另外如果研究区在时间段内出现过重大扰动如火灾、大规模虫灾NPP会先骤降再恢复这种情况MK检验可能会被断点干扰把所有年份当成一个整体来检验反而不合适。更严谨的做法是先做断点检测比如BFAST方法在分段基础上再算趋势或者把扰动区单独出来分析。我在实际做这类项目时还有一个个人习惯无论分析结果如何都把原始序列里每个像元的有效样本数输出一份跟趋势结果放在一起检查。很多异常趋势其实是因为有效年份太少或某些年份数据缺失导致的看到有效样本数分布图很多数据问题一眼就能发现。这个习惯帮我避免了好几次把数据坑写成生态学结论的尴尬。最后再分享一个小技巧在跑全区域批量计算之前永远先随机抽50到100个像元把它们的原始时间序列、Sen斜率和MK结果全部打印出来人工检查一遍。我见过太多人直接把向量化函数甩上去跑了一晚上最终发现是当年影像的波段顺序没对齐或者某个年份的文件路径写错了整个结果作废。花十分钟做抽查比事后返工省出几十个小时。这套Theil-Sen加Mann-Kendall流程跑通之后换一套数据、换一个研究区改改路径和参数就能复用是遥感时序分析里性价比极高的通用技能。
返回列表