☰
GEE与Sentinel-2网格化城市绿化评估:NDVI阈值提取与reduceRegions实战
2026/10/14 13:58:31 网站建设 项目流程

做城市绿化评估这几年,有个问题总被反复问:能不能把绿化覆盖算到每个街道,哪怕每个小区?用行政边界去统计,其实很容易踩坑。边界面积有大有小、形状千奇百怪,统计出来的绿化率根本没法横向比较;更麻烦的是,专题图一旦强调边界,总有人会问“这块绿地明明在街对面,怎么没算进来”。后来我彻底换了个思路——不管行政边界,先按固定大小的空间网格把城市切成格子,再用Sentinel-2影像识别每个格子里的绿化到底有多少。这个思路放进Google Earth Engine(GEE)里执行,效率和效果都远超预期。

这篇文章就是这个项目的完整复盘,内容包括:为什么选空间网格化而不是行政单元统计、Sentinel-2影像预处理怎么做、NDVI阈值提取绿化的调参套路、GEE里如何用reduceRegions做网格级统计,以及我实测跑通之后踩过的坑。适合正在用GEE做土地利用/生态环境评估的从业者,也适合GIS和遥感方向的入门同学拿来当模板改。

1. 为什么非要用网格:被行政边界统计坑过之后的想法

1.1 传统城市绿化调查的痛点

以前做城市绿化调查,最常见的做法是拿到测绘矢量边界,比如街道、社区,然后与遥感分类结果做空间统计。听起来没毛病,真跑起来一堆问题。

首先是面积不一致。一个中心城区的街道可能只有两三平方公里,另一个城乡结合部的街道可能一百多平方公里。绿化率是比值,但面积差异导致方差差异巨大,统计结果一放一起,做排名、做对比都缺乏说服力。再一个是边界不规则的连锁反应。行政边界常常沿着河、沿着路、沿着山脚走,和非绿化地块交叉在一起,统计时边缘像元的归属完全取决于矢量的切割方式,同一个地块在不同年份因为边界微调就可能得到完全不同的结果。

更实际的问题是横向叠加。想看看绿化覆盖率和人口密度、房价、热岛效应的关系时,行政单元的粒度太粗,内部差异被平均掉了。城区里一个街道,可能一半是高密度老城区、一半是新建公园,街道整体绿化率看着还不错,实际上老城区居民根本享受不到。这种时候,空间网格化的优势就很明显了。

1.2 为什么是Sentinel-2而不是别的影像

这项目选Sentinel-2,理由其实很朴素。

Sentinel-2的多光谱波段有10米分辨率,这个尺度对城市绿化评估来说恰好合适。比Landsat的30米细很多,城区路网两侧的行道树、小区里的绿地组团,至少能看出轮廓。比商业高分辨率影像便宜得多,而且是开放存档、历史影像随取随用,做时间序列非常有价值。

还有一个容易被忽略的点:Sentinel-2含红边波段(B5-B7)。城市绿化里大量存在的树冠、草皮在红边区间光谱差异明显,后期如果想做乔灌木细分、健康度评估,不需要换数据源,直接在原影像上多算几个指数就行。比起以前用Spot或高分系列,Sentinel-2的波段设计更贴近植被定量分析的实际需求。

搭配GEE之后,“选影像”这一步的成本几乎降到零。不用下载、不用预处理、不用本地算NDVI,所有过程都在平台上完成,几百元配置的办公电脑就能跑一个城市级别的分析,这放在以前很难想象。

1.3 “空间网格化”到底解决了什么

所谓空间网格化,简单说就是给研究区铺一层规则格网,比如500米乘500米的方格,然后以每个格子为统计单元,计算格子内绿化面积占比。

它解决了一个核心问题:分析单元的标准化。行政边界是不规则、不可比的,网格则是完全规则的,彼此之间没有包含关系,也不受权属边界调整影响。2019年算的网格绿化率和2023年算的网格绿化率,只要网格定义没变,结果就能严格对比,这个性质对长期监测太重要了。

同时,网格是“可再聚合”的。500米网格统计完之后,想输出街道尺度,可以按街道边界做空间连接,简单加权就能聚合。反过来街道统计结果则没办法轻松拆回网格。所以网格化等于做了底层标准化,上层应用想怎么接都行。

我在这个项目里选择的是500米网格。这个尺寸不是随便拍的,后面专门有一节讲尺度权衡,这里先记住结论:想让结果既表达空间差异、又具备统计稳定性,500米对多数中国城市是比较折中的选择。

2. 数据准备阶段:Sentinel-2影像的筛选、云掩膜与指数合成

2.1 研究范围与时间窗口的选择

做遥感项目,第一步永远是“把范围和时间定死”。范围很好理解,时间窗口则直接影响结果。

城市绿化评估最好在植物生长季做影像合成。以我这次处理的某南方城市为例,影像就取2023年6月1日到9月30日。这四个月里大部分植物处于叶片完全展开、叶绿素含量最高的状态,NDVI数值稳定,不容易把落叶期的稀疏植被误判成裸地。

如果你是北方城市,建议把窗口再集中到7月至8月;如果是热带城市,注意避开雨季云覆盖密集的月份,宁可选短一点的时间段,也不能让太多云污染参与合成。最忌讳的是不同年份各取各的季节——2019年取春天、2023年取秋天,做时间序列时NDVI系统性偏差会盖过真实变化。

研究范围我这里用的是本地矢量边界,直接上传到GEE资产,命名成study_area。再次强调,矢量范围宁大勿小,后面所有区域统计都会做clip,多出来的部分不影响结果,但范围太小会裁掉边缘网格。

2.2 云掩膜与合成:别让一朵云毁了整个统计

Sentinel-2的Level-2A表面反射率产品在GEE里集合名称是COPERNICUS/S2_SR,不需要自己跑大气校正,直接用就行。但云掩膜必须做,否则合成影像里的云和阴影会让NDVI变成一团乱麻。

这里用最经典的做法:读取QA60波段,对第10位和第11位做位掩膜。第10位标记云,第11位标记卷云,两者都不要。

var studyArea = ee.FeatureCollection('users/yourname/study_area').geometry(); function maskS2clouds(image) { var qa = image.select('QA60'); var cloudBitMask = 1 << 10; var cirrusBitMask = 1 << 11; var mask = qa.bitwiseAnd(cloudBitMask).eq(0) .and(qa.bitwiseAnd(cirrusBitMask).eq(0)); return image.updateMask(mask).divide(10000); } var s2sr = ee.ImageCollection('COPERNICUS/S2_SR') .filterBounds(studyArea) .filterDate('2023-06-01', '2023-09-30') .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20)) .map(maskS2clouds) .median() .clip(studyArea);

这串代码里值得注意的地方:第一步filter先是粗略筛选,用元数据里的CLOUDY_PIXEL_PERCENTAGE去掉云量超过20%的景;第二步map逐景做像元级云掩膜;第三步用median中值合成。中值合成比均值合成抗异常值能力强,个别云掩膜遗漏的亮像元不会把结果拉偏。

为什么用SR而不是大气层顶反射率TOA?因为TOA受气溶胶和视角影响大,多景影像合成后会遗留明显的条带感。SR数据已经做了大气校正,像元值除以10000之后就得到0到1的地表反射率,不同日期的影像可比性大幅提升。

2.3 光谱指数计算:NDVI和它的局限

影像合成完毕,接下来的核心动作是算植被指数。绝大部分城市绿化提取任务里,NDVI仍然是性价比最高的起点。

var ndvi = s2sr.normalizedDifference(['B8', 'B4']).rename('NDVI');

NDVI的原理是近红外和红光的归一化比值。健康植被在近红外强反射、在红光强吸收,所以NDVI能稳定捕捉“有叶绿素”的地面。10米分辨率的Sentinel-2算出来,城区绿地的NDVI通常在0.3到0.7之间,裸土、水泥面通常在0.1以下,水面则在负值附近摇摆。

不过NDVI也有两个明显短板。第一个是容易饱和:高密度森林和普通公园草地之间的NDVI差异会压缩,但这在“是否绿化”的二分类场景里影响不大。第二个是对阴影敏感:高层建筑和树冠阴影区域的NDVI会显著偏低,甚至跌到0.1以下,导致阴影里的绿地被漏分。这个问题在后面的坑位章节会重点展开,先记住结论:NDVI适合做初筛,不适合做唯一标准。

3. 绿化像元到底怎么切:NDVI阈值法及其精度自检

3.1 为什么选阈值法而不是机器学习分类

接到这个项目时,有好几种提取绿化影像的方案摆在我面前,最显眼的是随机森林或SVM监督分类。说实话,监督分类在土地覆盖大分类里确实猛,但拿来单独做城市绿化提取,有一个麻烦点:绿化既要包括大片公园,也要包括行道树、小区附属绿地、零碎苗圃,训练样本很难覆盖全部形态。更麻烦的是不同城市的光谱差异很大,换个城市模型就崩,还需要重新采样本。

本项目最终选择NDVI阈值法,核心考虑是可解释性和可迁移性。阈值法只有一个参数,调起来容易,出图后可以理直气壮解释“我们是按NDVI大于0.3判为绿化像元”,甲方和业务部门都能听懂。后续换城市、换年份,只需重新标定阈值,不需要重训模型。

当然阈值法上限也明显:无法区分布地类型、无法识别绿地的质量。这就看项目目标了,如果只要“绿化覆盖面积”,阈值法在各城市里都能达到90%左右的总量精度,够了。

3.2 阈值标定的实操套路

说“阈值0.3”是节奏最快的说法,实际工作里必须花时间标定。我的做法分三步。

第一步,在影像上随机撒300个点。这些点上人眼判断“是否可见绿化”,记录成一个标记集合。第二步,提取每个点对应的NDVI值,把样本按“绿化”和“非绿化”分开,看两个分布的重叠区间。第三步,在重叠区间里试阈值,目标是让总体精度最高、或者根据业务需要让漏分和错分平衡。

以这次某南方城市的结果为例,NDVI=0.2时漏掉了一部分稀疏草皮,但很多树荫下的水泥地和车道也被当成了绿化,总体绿化面积虚高10%左右;NDVI=0.4时绿地面积偏向保守,高密度老城区的林荫道丢失严重;最终用的0.3正好卡在误判和漏判交界处。

不同场景的阈值经验值,可以参考这张表:

阈值典型误判更适合的场景
0.2阴影、湿润裸地易混入植被稀疏、生长季短的城市
0.3相对均衡多数中国城市默认起点
0.4稀疏草地漏分高密度建成区、乔木主导区域

这里给的0.3只是起点,不代表标准答案。

var threshold = 0.30; var green = ndvi.gt(threshold).rename('green');

3.3 提取结果的初步检查

阈值定了,绿化掩膜就出来了。但别急着做网格统计,先做一遍目视检查和定量检查。

目视检查的方法很笨但有效:在GEE里把Sentinel-2真彩色影像和green掩膜叠加,透明度调低一点,随机在城市各处滚动缩放看。最容易发现的问题是水面误分——比较浑浊的水库、布满青苔的水塘,NDVI会冲到0.2以上,需要用水体掩膜再清一遍。

定量检查我建议用随机点混淆矩阵。准备300个验证点,人工判定标签,然后统计绿化掩膜和标签的一致率。如果总体精度低于85%,先不要做任何区域统计,回头重新调阈值或检查影像合成质量。这个过程治好了我很多“感觉差不多就行”的毛病。

4. 500米渔网统计:reduceRegions做绿化覆盖评估的关键实现

4.1 创建网格:分辨率与尺度的权衡

网格大小的选择直接影响两类误差。网格越小,空间细节越丰富,但每个格子的像元数量少,绿化率方差波动大,而且后期统计计算量成倍增加;网格越大,统计越稳定,但空间格局被平均掉,热点区域看不清。

我这次从100米到1公里都试过。100米网格单格面积一公顷,约100个10米像元,城市里一个小区就能占好几格,细节确实好,但导出表格有上万行,很多网格只有一半被建筑覆盖、另一半是街道,绿化率分布噪声很大。1公里网格统计很稳定,可老城区和新建公园被揉在一起,供决策的指示意义就差了很多。

500米网格是平衡点:单格25公顷、约2500个像元,统计稳定性有一定保证,城区空间差异还能保留。如果你关注的是一座几千万人口的大城市,建议从800米到1公里起步跑通流程,再局部加密到200米看重点片区。

网格矢量我在GIS软件里预先做好,上传到GEE资产,命名为roi_grid_500m。在GEE里直接用循环生成渔网也可以,但涉及投影坐标计算,不如外部做完再上传省事。注意:网格一定要允许覆盖到研究区外一圈,至少多出半个格宽,避免统计时边缘网格面积不完整。

4.2 网格绿化率统计的代码实现

网格准备好之后,主角函数是reduceRegions。它把一个影像在一个FeatureCollection内按格网做聚合,输出每个网格的统计值。这里的核心是设置scale参数,它决定了影像在聚合时的重采样像元大小。虽然原始影像分辨率是10米,但如果不显式设置scale,reduceRegions可能会按金字塔层级去取一个偏大的分辨率,导致结果偏粗。

var grid = ee.FeatureCollection('users/yourname/roi_grid_500m'); var gridResult = green.reduceRegions({ collection: grid, reducer: ee.Reducer.mean(), scale: 10, tileScale: 4 }).map(function(f) { var ratio = f.getNumber('green_mean'); return f.set({ green_pct: ratio.multiply(100) }); });

这里返回的FeatureCollection里,每个网格会有一个green_mean字段,值域是0到1,乘以100就是绿化覆盖率。

如果网格数量有几万个,reduceRegions可能在一次性重新投影时消耗过多内存。tileScale就是应对这个问题的:把计算分块执行,越大计算越稳但越慢。我习惯先从2开始,报错就往上加到4或8。

还有一个统计口径细节:green这一层是0/1掩膜,但水体、云阴影区域如果有nodata,reduceRegions的mean会把nodata排除,导致看起来负片区域被“空”掉。如果某些网格里有大块水体,绿化率就会被低估。做正式成果前,要么把水体从分母中剔除,要么单独加一个水域面积字段。

4.3 网格结果的可视化与导出

统计完成后,GEE地图上直接展示FeatureCollection不太友好,因为默认不会按字段值上色。我习惯用自定义样式把网格分成几个等级:高绿化(大于50%)、中绿化(30%-50%)、低绿化(10%-30%)、极低(小于10%)。

var styled = gridResult.map(function(f) { var pct = f.getNumber('green_pct'); var color = ee.String(ee.Algorithms.If( pct.gte(50), '1a9850', ee.Algorithms.If( pct.gte(30), 'd9ef8b', ee.Algorithms.If(pct.gte(10), 'fdae61', 'd73027')))); return f.set({style: {color: color, width: 1, fillColor: color + '66'}}); }); Map.addLayer(styled.style({styleProperty: 'style'}), {}, 'grid green pct');

这里用styleProperty把颜色字段传给矢量样式。示例里的配色就是浅绿化暖色、高绿化冷绿的渐变,出图直观。

导出也是必须做的,常见有两种:

Export.table.toDrive({ collection: gridResult, description: 'grid_green_ratio_500m', fileFormat: 'CSV', selectors: ['system:index', 'green_pct'] }); Export.image.toDrive({ image: ndvi, description: 'ndvi_s2_2023', region: studyArea, scale: 10, maxPixels: 1e13 });

CSV用于后续统计分析,GeoTIFF用于GIS制图和存档。实测下来,500米网格在20公里见方的区域内导出的CSV只有几千行,Excel就能处理,非常轻量。

5. 跑通流程后遇到的三个真实问题:混合像元、季节干扰和边界统计

5.1 混合像元导致绿化率虚高

第一次出图时,我发现高密度老城区的绿化率普遍偏高,和实地印象不符。原因很容易想到:10米像元在城市里经常一半是树冠、一半是路面。这种混合像元的光谱被两边共同影响,NDVI大约在0.2到0.3之间。按0.3阈值判,很多“大树加窄路”的混合像元进入了绿化类,绿化覆盖面积被高估。

这不是说阈值本身错了,而是“绿化覆盖”间接受到了路面贡献。要解决这个问题,方法不复杂:一是把阈值从0.3提到0.35,牺牲一点稀疏草地,换掉一部分混合像元;二是叠加上不透水面数据,比如现有全球不透水面产品或本地建筑矢量,把建筑像元强制排除在绿化外。

更严谨的做法是使用红边指数。Sentinel-2的B5、B6、B7红边波段对叶绿素敏感程度更高,混合像元里只要有少量植被,红边区域的反应比红光更早“跳出”裸地背景。我后面测试过NDVI和红边归一化指数(NDIre)的组合,高密度老城区的精度能再提升两三个百分点,代价是流程多一步。

5.2 季节变化影响NDVI,比想象中还大

项目做到中段,我要把2023年和2019年的绿化率做对比。当时图省事,2019年直接取了同是6到9月的合成影像。结果比较下来,两年绿化率总体下降了约6个百分点,这个变化幅度明显超出常理,我第一反应是发生了开发建设。

后来细查发现,2019年那个6到9月窗口里,影像有效覆盖集中在6月上旬,而2023年有效覆盖集中在9月下旬。6月上旬很多行道树和草地刚从春季恢复期出来,叶面积指数还没达到峰值,NDVI整个偏低;9月下旬的植被则是秋末状态。这个季节不一致直接把统计结果带偏了。

从那以后,凡是跨年份比较,我都强行把时间窗口再收紧,比如统一到7月15日到8月15日,并且确认每个年份的有效影像数量大于3景。宁可窗口短一点,也不能让影像质量拖垮统计口径。

5.3 网格边缘像元:统计里的隐性变量

第三个问题,是我在对比相邻网格时发现的。有些网格绿化率明显低于周围,但实地看那块地并没有特别缺绿。把影像放大后才发现,这些网格的边缘正好切在行道树连续带上,行道树树冠一半在本格、一半在邻格。reduceRegions在处理跨网格像元时,边缘像元被邻格或栅格化规则吃掉了一部分。

这个现象在小网格(比如100米)时尤其明显。解决思路是给每个网格加一个向内收缩的缓冲,只统计“核心区域”,但这样又会丢掉网格边缘绿化信息。折中办法是先统计500米网格,再叠加一个50米内缩缓冲区做敏感性测试。如果去掉边缘后结果变化不大,说明整格统计可信;如果变化很大,说明边缘混合像元主导了结果,需要改阈值或重定义绿化类型。

6. 网格结果的延伸应用:从一张绿化率图到动态监测体系

6.1 与人口、建筑数据叠加,找出真正的绿化盲区

网格绿化率出来后,最有价值的一步是和其他城市数据做叠加。

我在这个项目里把它和人口栅格做了网格级z-score标准化。所谓z-score,就是看每个网格的绿化率和人口密度各自偏离均值的程度。一眼就能识别出“高人口密度、低绿化率”的网格,这类网格才是城市更新的重点。传统按街道统计时,这些地方被街道内公园的高值平均掉,完全看不出来。

操作上不复杂:把人口栅格也用相同的reduceRegions按500米网格聚合,算平均人口密度,然后把绿化率字段和人口字段放在同一个表格里做相关分析和排序。这种网格级叠加,本质上是把城市微观环境数据打通,而不是各算各的然后文字描述。

6.2 时间序列监测:把五年的网格绿化串成趋势

网格化的另一个隐藏红利,是时间维度的变化检测可以做得很干净。因为网格是固定的,每一年的绿化率都在同一个单元上计算,前后对比不需要做“找对应区域”这一步。

我这里直接用GEE循环处理了2019年到2023年共五年的影像,每年输出一个网格绿化率字段。五个字段放到一张大表里,每个网格一行,然后计算斜率。绿化率持续下降的网格,哪怕每年降幅只有1个百分点,五年累计下来也意味着成片绿地的消失,足够触发一个预警。这个“趋势识别”功能如果换成行政边界统计,几乎没法实现——边界稍微调整,整条时间线就不连续了。

代码上可以写一个函数,按年份循环调用影像过滤和reduceRegions,把结果合并到一张表里。注意所有年份的影像窗口必须一致,这一步无论如何不能省。

6.3 再往下走:生态服务量和路网可达性

网格化绿化率不只是“绿不绿”的表征,它还可以作为很多生态模型的输入项。比如把网格绿化率和地表温度栅格回归,可以直接估算城市热岛效应中绿化的降温贡献,每个网格拎出来做散点图,比堆大段描述更有说服力。

又比如结合路网做“步行十分钟可达绿地”的覆盖分析。网格化绿化为这种分析提供了稳定底图:把绿化率高的网格转成绿地斑块,然后沿路网做网络分析,算出居住区到绿地的实际步行距离。这项指标在不少地方已经被纳入规划评估体系了,如果手里有网格级绿化基底,扩展出这套分析会非常顺。

从我的实际经验看,空间网格化加Sentinel-2影像这套组合,最大的价值不在于“做一张绿化覆盖图”,而在于给后续所有空间分析铺了一层标准化的底。网格在,口径就在;口径在,时间序列就能形成,跨部门、跨年份的对话也才有了基础。

最后分享一个小技巧:做网格绿化率趋势分析时,我习惯把每一年统计结果里的平均NDVI也一起导出,而不只是绿化率。NDVI是连续值,比0/1阈值后的绿化率保留了更多梯度信息,后期一旦调整阈值,不需要重新跑全年影像,直接拿NDVI字段重切就行。就这一步,经常能帮你省下几个小时的重算时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询