做遥感这两年,但凡用Google Earth Engine(GEE)做过矢量数据处理的,应该都绕不开FeatureCollection这个类型。我第一次被它折磨是在批量统计行政区的NDVI均值,写了几行代码跑不通,最后发现是FeatureCollection和Feature没分清楚,属性字段的访问方式完全不一样。后来在实战里一点点试错、翻文档、看别人代码,才算把这个数据类型吃透。今天借这个机会,把这部分内容系统整理一下,给刚入门GEE的朋友做个参考,也给自己做个沉淀。
这个系列我打算分几篇来讲,第一篇先把FeatureCollection最核心的概念、构建方法、常用操作和常见坑讲清楚。适合刚接触GEE、对矢量数据操作还不熟的人,也适合已经写了一些代码但在数据类型上经常报错的人。内容偏实操,我会把我在项目里真正用过的代码、踩过的坑、查过的官方文档都揉进来,尽量做到拿到就能用。
1. 整体设计与思路拆解:FeatureCollection是什么,为什么非要搞懂它
1.1 数据类型的层级关系:从Geometry到Feature再到FeatureCollection
要理解FeatureCollection,首先要搞清楚GEE里矢量数据的三个层级关系。最底层的是Geometry,就是点、线、面这些几何对象本身,比如ee.Geometry.Point([116.39, 39.9])只是一个坐标点,它不带任何语义信息,不知道这是什么地方、有什么属性。在Geometry之上是Feature,它等于Geometry加上一组属性,比如一个点加上“城市名:北京”“人口:2000万”,这才构成一个有意义的空间要素。最外层是FeatureCollection,它是一组Feature的集合,相当于一个矢量图层的抽象。
你可以拿Excel表格来类比。Geometry是表格里某个单元格的坐标(或者叫位置信息),Feature是表格里的一整行,包含唯一标识符和若干字段,FeatureCollection就是整个表格加上每一行对应的空间位置。平时我们在ArcGIS或QGIS里打开的shp文件,加载到GEE里返回的就是FeatureCollection。很多新手上来就想直接操作FeatureCollection,结果发现它不是一个简单对象,里面包含了很多Feature,而每个Feature又包含Geometry和properties两部分,操作方式完全不一样。
1.2 为什么GEE选择用FeatureCollection作为矢量的核心容器
GEE是一个云端计算平台,最大的特点是分布式计算。它的数据不是存在你的本地硬盘上,而是分布在全球的数据中心。这就要求所有的数据类型必须是服务端对象,可以被序列化、分发到不同的计算节点上去并行处理。FeatureCollection正是为这种场景设计的,它本质上是一种非物化的集合,定义的是“如何获取数据”的逻辑描述,而不是把所有数据一次性拉到本地。
这意味着一个很重要的事情:在GEE里,FeatureCollection的操作绝大多数是惰性执行的,你调用filter、map、reduce的时候,数据并没有真正被处理,而是构建了一个计算图,直到你调用getInfo()、evaluate()或者通过Export导出时才会真正执行。这也是新手最容易困惑的地方,明明写了筛选条件,打印出来的集合大小却没变,因为服务端的数据还没跑过来。
第二个原因是FeatureCollection天然支持空间索引和分布式的并行计算。GEE内部对FeatureCollection做了分片存储,一个上百万要素的集合被切成了很多块,分散在集群的不同节点上。当你要做空间连接、缓冲区叠加、属性统计时,框架会自动把这些任务并行化,大幅提升计算效率。要是把数据全拉到本地再用GeoPandas处理,那根本不是一个量级。
1.3 使用场景:什么时候你离不开FeatureCollection
FeatureCollection在GEE里的使用频率非常高,我列几个最典型的场景。第一类是行政区统计,比如你有一份全国省级行政区划的FeatureCollection,想统计每个省过去十年平均降水量,你会用reduceRegions把栅格数据按矢量边界分区统计,输出结果还是一个FeatureCollection。第二类是站点数据的空间化,比如一批气象站点的坐标和观测值,转成FeatureCollection后可以插值、可以按缓冲区提取栅格值。第三类是空间查询和几何运算,比如两条道路线的相交分析、点要素落在哪个面要素里的判断,这些底层都要操作FeatureCollection。
还有一类容易被忽略的场景是数据清洗与合并。比如你从不同数据源拿到的表格,需要统一字段名、统一坐标系,甚至要做属性表的纵向拼接和横向连接,这些操作GEE都提供了对应的API,底层操作对象就是FeatureCollection。可以这么说,凡是涉及到“多个带属性的空间对象”的数据处理,基本都绕不开FeatureCollection。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点:Feature和FeatureCollection的构建方法
2.1 Feature的构成:Geometry与properties,以及两者的关系
构建FeatureCollection之前,必须先掌握Feature本身。一个Feature由两部分组成:geometry和properties。geometry是一个Geometry对象,可以是Point、LineString、Polygon或者Multi系列,甚至可以是空几何(ee.Geometry())。properties是一个字典对象(Dictionary),存储的是属性字段和对应的值,键必须是字符串,值可以是数字、字符串、数组等基础类型,也可以是嵌套对象。
这里有一个关键点需要注意:GEE对Feature的property有严格的命名限制。属性名不能以数字或下划线开头,也不能包含空格和特殊字符,比如1st_band、_id这种都是不合法的,官方文档明确写了属性名必须匹配[a-zA-Z][a-zA-Z_0-9]*这个正则。我刚开始写代码的时候,从CSV导进来的字段名是“2019_population”,直接拿来构建FeatureCollection就报错了,后来改成pop_2019才通过。还有一点,属性值不能是复杂对象比如ee.Feature本身,但是可以存放ee.Number、ee.String、ee.List、ee.Dictionary这些对象。
创建单个Feature的代码很简单,下面是我在项目里最常用的几种写法,注释里写了适用场景:
javascript复制// 写法一:直接指定几何和属性,适用于临时构建点数据
var point = ee.Feature(
ee.Geometry.Point([116.39, 39.9]),
{name: '北京', population: 21540000}
);
// 写法二:只用几何,属性留空,适用于只做空间分析、不需要属性表的场景
var polygon = ee.Feature(ee.Geometry.Polygon([[
[116.3, 39.8], [116.6, 39.8], [116.6, 40.1], [116.3, 40.1]
]]));
// 写法三:从GeoJSON构建,适用于在外部GIS软件里准备好数据再引入
var geojson = {
type: 'Feature',
geometry: {type: 'Point', coordinates: [116.39, 39.9]},
properties: {name: '北京', population: 21540000}
};
var featureFromJson = ee.Feature(geojson);
2.2 从零构建FeatureCollection:直接传入、fromGeometry、fromFeatures
构建FeatureCollection最常见的方式是传入一组Feature数组,GEE会把它封装成一个服务端集合。比如你的研究区有五个城市,要构建一个点要素集合,代码如下:
javascript复制var cities = ee.FeatureCollection([
ee.Feature(ee.Geometry.Point([116.39, 39.9]), {name: '北京'}),
ee.Feature(ee.Geometry.Point([121.47, 31.23]), {name: '上海'}),
ee.Feature(ee.Geometry.Point([113.26, 23.13]), {name: '广州'}),
ee.Feature(ee.Geometry.Point([104.06, 30.67]), {name: '成都'}),
ee.Feature(ee.Geometry.Point([114.30, 30.59]), {name: '武汉'})
]);
但是如果你的几何对象很多,这样一个个手动写Feature,效率很低。这时候可以用ee.FeatureCollection.fromGeometry这个静态方法,直接把一组Geometry转成FeatureCollection,属性字段会留空。这个方法的底层逻辑是遍历传入的几何序列,逐个包一层空的Feature。当然后续你可以通过map来批量添加属性。
javascript复制var allGeometries = ee.List([
ee.Geometry.Point([116.39, 39.9]),
ee.Geometry.Point([121.47, 31.23]),
ee.Geometry.Point([113.26, 23.13])
]);
// 注意fromGeometry需要传入一个几何的集合对象,不能直接传数组
var fcFromGeom = ee.FeatureCollection.fromGeometry(
ee.Geometry.MultiPoint(allGeometries) // 先合并成Multi系列,再展开
);
这里有一点容易踩坑:fromGeometry接收的是ee.Geometry类型,而不是数组。我当时传了一个JavaScript数组,结果报错,后来把几何合并成MultiPoint才解决。如果你确定几何数量不多,用数组循环也是可以的,但要借助ee.List来操作,不能直接用JS数组去构建服务端对象。
另一种常用方式是ee.FeatureCollection.fromFeatures,它接收一组已经构建好的Feature对象,用法和传入数组是类似的,区别在于这个API更强调“在已有的Feature列表基础上,合并它们为一个集合”。比如你有两个FeatureCollection,想把它们合并成一个,可以直接:
javascript复制var merged = aFeatures.merge(bFeatures);
但如果想把一组分散的Feature对象合并,fromFeatures就很有用:
javascript复制var feat1 = ee.Feature(ee.Geometry.Point([116, 39]), {v: 1});
var feat2 = ee.Feature(ee.Geometry.Point([121, 31]), {v: 2});
var fc = ee.FeatureCollection.fromFeatures([feat1, feat2]);
2.3 从外部数据导入:Asset、CSV、GeoJSON与表连接
在实际项目里,很少会像上面那样手动去写坐标点,绝大多数FeatureCollection都来自数据导入。GEE提供了几种导入方式。
第一是上传shapefile或GeoJSON到Asset存储,然后通过ee.FeatureCollection('users/yourname/folder/filename')加载。这是最推荐的方式,因为Asset是存储在地理服务器里的,读取快、支持大文件、可以被Export反复引用。上传之前要注意检查几何类型是否统一、坐标参考系是否正确(GEE要求是WGS84,也就是EPSG:4326)、属性字段名是否合法,因为上传时GEE会自动转换,转不了的字段会被丢弃。
第二是从CSV或SHP文件直接导入,用ee.FeatureCollection.fromTable(这个方法是历史遗留,现在通常直接用构造函数就行)。CSV数据要包含经度纬度的列,GEE才能生成点要素,否则只会生成一个没有几何的Feature集合。比如你有下面这个csv:
csv复制name,lon,lat,value
station1,110.2,34.1,12.5
station2,111.5,35.3,10.2
上传后在GEE里加载,然后需要把经纬度字段映射成几何。GEE在表格导入面板里让你选择lon和lat列,勾选之后得到的FeatureCollection就带几何了。如果是上传以后再处理,可以这样转换:
javascript复制var csvCol = ee.FeatureCollection('users/yourname/tablecsv');
var withGeom = csvCol.map(function(f) {
var lon = f.getNumber('lon');
var lat = f.getNumber('lat');
return f.setGeometry(ee.Geometry.Point([lon, lat]));
});
setGeometry这个操作我几乎每次写CSV加载都要用到,频率极高。另一个比较实用的场景是矢量数据和表格数据的连接,比如你有一个站点位置Asset和一个站点观测记录CSV,两者通过站点ID关联,就可以用ee.Filter.equals + equals方法做join,这在GEE里叫innerJoin,我会在后面第4部分展开讲。
2.4 几何与属性的增删改查:set、setGeometry、copyProperties的用法
构建FeatureCollection之后,经常需要修改或增补数据,这里就要掌握几个最常用的方法。
修改属性最常用的是set,它接收一个键值对或一个字典。注意set不会修改原对象,而是返回一个新的Feature,这在GEE里所有不可变类型都是一样的。所以你要用map去批量修改:
javascript复制var updated = fc.map(function(f) {
// 新增一个字段:把population单位从人变成万人
var pop = f.getNumber('population');
var popWan = pop.divide(10000);
return f.set('population_wan', popWan);
});
这里我估计很多人第一次写会直接在set里做四则运算,但要注意GEE的属性操作是服务端对象驱动的,pop.divide(10000)返回的是ee.Number,并不是JS的number。如果你直接把两个数字类型做减法和除法,返回的可能是一个普通JS Number,放进属性时会导致类型错误。我的经验是:涉及数值计算一律用ee.Number的方法链,比如pop.divide(10000)、pop.add(1)、pop.multiply(0.8)。
删除属性用unset,它可以传一个属性名或者属性名数组。清空几何用setGeometry(null),这在只需要属性表做统计时不失为一个节省内存的好技巧。还有一种情况是只保留geometry、不要任何属性,可以用select([])。select方法默认是保留所有属性,传一个空数组就相当于把属性全删掉,只留下几何和系统自带的system:index。
copyProperties也是很实用的方法,它通常配合map使用。比如你想把A集合的属性复制到B集合的几何上,可以在map里用sourceFeature.copyProperties(targetFeature),注意参数顺序——源Feature放在前面,目标Feature放在后面,返回值是目标Feature(或者说以目标Feature为几何,带着源Feature的属性)。我一开始总是搞反,copyProperties参数一写错,结果全是空字段。
3. 实操过程与核心环节实现:FeatureCollection的常用操作与典型流程
3.1 最常用的访问操作:size、first、limit、geometry、属性提取
FeatureCollection作为集合容器,最基础的操作就是查看它的大小、取第一个要素、限制数量、取出几何。这些方法虽然简单,但在日常调试和流程控制中无比重要。
size()返回集合中的要素数量。第一次见到这个API的人可能会很兴奋地直接用print(fc.size()),但发现控制台打印的不是一个数字,而是一个ee.Number对象,这就是因为所有操作都是惰性的。你要打印它的具体值,要么用print(fc.size().getInfo())(本地阻塞获取结果),要么在Code Editor里直接打印集合对象本身(它的内置调试机制会自动解析)。我通常用print('size', fc.size().getInfo())在调试阶段看数量,这样直观。
first()返回集合中的第一个要素。注意这个“第一个”并不一定是你上传数据时的第一条记录,因为分布式的集合没有严格的顺序定义。不过对于大多数场景,我们需要的是某个要素去调试,first()是最简单的抓手。配合geometry()方法可以快速画图:
javascript复制var firstFeature = fc.first();
var firstGeom = firstFeature.geometry();
Map.addLayer(firstGeom, {}, 'first feature');
limit(n)返回集合的前n条要素。这个在随机抽样和快速可视化时很有用,比如你要在一个大数据集里随便看几个样本,可以用fc.limit(10)。
属性提取的几种方式要特别注意区分。get方法是在单个Feature上调用的,它返回指定属性的值,比如firstFeature.get('name')。aggregate_*系列是在FeatureCollection上调用的,比如fc.aggregate_mean('population')直接返回整个集合某个字段统计量,fc.aggregate_histogram('type')返回字段直方图。如果你的数据量很大,不要用getInfo去强制运行全表数据,效率极低,我建议用reduceColumns来提取多列,或者用aggregate_array拉出一个属性列表再加后续处理。
3.2 按空间与属性筛选:filterBounds、filterMetadata、filter的完整参数
筛选是FeatureCollection最常用的操作。GEE提供了两种主要的筛选方式,一种是空间筛选,一种是属性筛选,两者可以叠加使用,返回结果依然是一个FeatureCollection。
空间筛选最典型的是filterBounds(geom),它接收一个Geometry或者FeatureCollection,返回所有与该几何相交的要素。比如你有全国气象站点的FeatureCollection,想提取研究区范围内的站点,这一步就是刚需:
javascript复制var region = ee.FeatureCollection('users/yourname/study_area').geometry();
var stationsInRegion = stations.filterBounds(region);
注意filterBounds是“相交”判断,不是“完全包含”。如果你只想保留完全落在面内的要素,需要自己写filter。在实际处理里,站点落在边界上、或者线要素和面边界重合的情况特别多,这个细节有时候会造成数据量偏差,需要结合业务根据实际情况决定是否追加密集操作。
属性筛选的通用方法是filter(ee.Filter)。ee.Filter支持的判断非常多,常用的有equals、lessThan、greaterThan、in、rangeContains、dateRangeContains、stringContains等。例如:
javascript复制var bigCities = cities.filter(ee.Filter.greaterThan('population', 10000000));
var selected = cities.filter(ee.Filter.in('name', ['北京', '上海', '广州']));
如果你想同时满足多个筛选条件,可以用ee.Filter.and、ee.Filter.or,或者在filter后面继续追加filter。GEE的执行器会智能合并多个filter到一个谓词链里。还有一个高效的小技巧:空间筛选和属性筛选同时存在时,先写属性筛选再写空间筛选,两者尽量保持精确条件,减小中间结果集的大小,这对大数据的计算速度提升非常明显。我给用户做城市群分析时,几十万条记录先filter行政区代码,再filterBounds,速度比反过来快了好几倍。
3.3 map批量处理:遍历Feature,逐条更新属性和几何
如果说FeatureCollection只能做查询,那它最多算一个数据库。真正让它变得强大的,是map函数。map接收一个回调函数,对集合中的每个Feature执行逻辑,然后返回一个新的FeatureCollection。在GEE的分布式架构下,这个“对每个Feature执行”是并行进行的,所以用map去处理海量数据效率很高。
举个实际的例子。我之前做过一个PM2.5站点浓度趋势分析,需要给每个站点计算它周边10公里缓冲区内的平均气溶胶光学厚度(AOD),然后用这个值去估算污染水平。数据流程是这样的:
javascript复制var aodImage = ee.ImageCollection('MODIS/061/MCD19A2_GRANULES')
.filterDate('2023-01-01', '2023-01-02')
.select('Optical_Depth_047')
.mean();
var sites = ee.FeatureCollection('users/yourname/sites');
var sitesWithAod = sites.map(function(f) {
var buf = f.geometry().buffer(10000); // 10公里缓冲区
var meanAod = aodImage.reduceRegion({
reducer: ee.Reducer.mean(),
geometry: buf,
scale: 1000,
maxPixels: 1e9
}).get('Optical_Depth_047');
return f.set('aod_mean', meanAod);
});
print('sample', sitesWithAod.first());
这段代码里最核心的就是f.set('aod_mean', meanAod),它不仅添加了属性,而且在map的回调里保持了原来的geometry和所有原有属性,这是map相对于手动构造Feature的优势:它自动保持源要素的一切,无需你手动去复制。
map里面还有个细节,回调必须返回一个Feature,而且返回的Feature必须具备集合中已有的全部属性,否则当你后续做merge或reduce时可能会因为属性不一致而报错。比如集合里有三个要素分别有name属性,你在map里返回了一个没有name的Feature,GEE可能不会提示错误,但当后续使用aggregate_array('name')时,得到的列表里会混入null值,非常坑。我的习惯是:在map里对所有分支都明确返回f或f.set(...),保证属性一致。
3.4 reduce操作:aggregate_*系列、reduceColumns、联合统计
数据统计是GEE里最高频的需求。FeatureCollection提供了两种统计方式:aggregate_*系列和reduceColumns。
aggregate_*系列适合单字段统计,比如这批站点里观测年份的最大值、最小值、均值、总和、中位数等。我最常用的是这几个:
aggregate_mean(property):均值aggregate_sum(property):求和aggregate_min / max:最小值/最大值aggregate_histogram(property):频率直方图aggregate_countDistinct(property):去重计数aggregate_array(property):把一列的所有值变成ee.Listaggregate_stats(property):返回包含最小值、最大值、均值、标准差、方差、总和等所有基础统计量的字典
其中aggregate_stats用得尤其多,因为它一次就可以拿到完整统计摘要,适合写报告或者做直方图。比如:
javascript复制var stats = sites.aggregate_stats('pm25');
print('pm25 statistics', stats);
应用这些统计时要注意:属性必须是数值,否则统计结果会是null或者报错。字符型字段用aggregate_histogram是可以的,但aggregate_mean不行。
reduceColumns更灵活,支持多列、多约减器联合操作。它的典型用法是:指定一组属性列,用ee.Reducer组合器计算多种统计量,返回值是一个字典。我举个例子,我要同时对temp和precip两个字段算最大值和最小值:
javascript复制var columnStats = sites.reduceColumns({
reducer: ee.Reducer.minMax().repeat({
reducer: ee.Reducer.minMax(),
firstInput: 'temp',
secondInput: 'precip'
}),
selectors: ['temp', 'precip']
});
print(columnStats);
等等,上面这个写法其实不太对,GEE提供了更简洁的ee.Reducer.minMax().repeat(...),但语义跟selectors的排列组合要仔细对上。说实话,reduceColumns的坑挺多的,因为reducer的组合顺序和selectors的映射关系极其容易搞错。如果想要更稳妥的写法,我建议先用aggregate_*分别计算,再合并结果:
javascript复制var tempMin = sites.aggregate_min('temp');
var tempMax = sites.aggregate_max('temp');
var precipMin = sites.aggregate_min('precip');
var precipMax = sites.aggregate_max('precip');
虽然这样会多跑几次扫描,但对新手来说,可维护性和正确性优先,等熟悉了再尝试reduceColumns的进阶写法。
3.5 显式执行:getInfo、evaluate、print与导出的区别
这是新手最容易忽略的部分。GEE的客户端API(在Code Editor或JavaScript环境)只是构建了服务端计算图,真正的运行发生在GEE的服务器上。要让结果回到本地,你必须显式地强制计算。有三种方式:getInfo()、evaluate()、Export。
getInfo()是阻塞式调用,它会向服务器发送请求、等待执行完成、返回一个JavaScript对象。比如print(fc.size().getInfo())会输出一个数字,print(fc.first().getInfo())会输出一个Feature的完整JSON表示。在调试阶段这是最快的方式,但注意不要对超大集合调用getInfo(),因为它会把整个集合的内容拉到本地,可能造成浏览器崩溃或请求超时。
evaluate()是异步版本,它接收一个回调函数,适合在客户端写交互逻辑时使用。通常用于自定义UI或者需要把数据传给其他JS库的场景。比如:
javascript复制sites.evaluate(function(data) {
// data 是一个 GeoJSON 风格的 FeatureCollection 对象
console.log(data.features.length);
});
Export是把计算结果写入Asset或Google Drive,适合大数据量、离线处理、批处理任务。这是生产环境最常用的方式,一旦导出成功,结果会持久化保存,后续运行可以直接读取Asset,不需要重复计算。
我在实际开发中还有一个常用的习惯:先对数据做limit(10)再getInfo(),快速验证逻辑正确性,通过后再用Export导出全量结果。这能避免因为一个字段名拼写错误,跑了一个大任务才发现数据全空,白烧了计算配额。写代码的时候,我建议把待导出的集合在Map里加一下图层,确认可视化没问题了再导出,这能省掉一大半调试时间。
4. 常见问题与排查技巧实录:FeatureCollection实战避坑
4.1 属性名不合法与类型不匹配:最常见的两类报错
我在做基础培训时,学员遇到的90%以上的情况是两类:属性名不合法和属性类型不匹配。
属性名不合法前面提过,必须以字母开头,只能包含字母、数字、下划线。如果你从外部导入的shp字段名是中文或包含空格,上传到GEE时GEE会尝试自动转换,但经常会生成一个奇怪的内部名称,比如yourname___5f448b...,很难看且不易操作。我的解决方案是:在本地GIS软件或Python脚本里先统一改名,确保完全合法后再上传到Asset。
属性类型的坑则更隐蔽。比如CSV里的数值列,GEE导入时如果某一行有非数字字符,它会把整列识别成字符串,那么你在后续做aggregate_mean时就会得到null。这种问题有时不会立即报错,而是在统计结果里默默丢数据。排查的方法是打印第一要素的属性,看类型是否如预期:
javascript复制print(sites.first());
如果看到"pm25": "12.5"(注意引号说明是字符串),就知道要提前转换。转换的办法是用ee.Number.parse或者ee.Feature.set配合map做类型转换:
javascript复制var sitesFixed = sites.map(function(f) {
var pm25 = ee.Number.parse(f.get('pm25'));
return f.set('pm25', pm25);
});
这里我用ee.Number.parse而不是直接用Number(),因为f.get返回的是服务端对象,直接传给JS的Number()会被当成一个奇怪对象,大概率会得到NaN。
4.2 客户端与服务端数据类型混淆:getInfo用得太早导致性能灾难
GEE里有一条红线一定要记牢:客户端对象和服务端对象不能混用。怎么区分?服务端对象是以ee.*开头的:ee.Number、ee.String、ee.List、ee.Dictionary、ee.Feature、ee.FeatureCollection等。客户端对象是你直接在JavaScript里声明的普通数据类型:数字、字符串、数组、对象。
一个典型错误是,为了调试,在回调函数里用了getInfo(),结果导致整个循环变成串行,性能急剧下降。比如:
javascript复制// 错误的写法
var sitesBetter = sites.map(function(f) {
var name = f.get('name').getInfo(); // 这里会阻塞
var pm = f.get('pm25').getInfo();
return f.set('name_copy', name + '_' + pm);
});
map回调里千万不能出现getInfo(),因为回调要返回一个服务端的ee.Feature,而你一旦请求客户端数据,就会把一个分布式的并行回调变成串行的本地循环,数据量一大,基本就是卡死。
正确的思路是:所有逻辑都基于服务端对象,用f.getString、f.getNumber等方法取出值,然后使用GEE提供的字符串和数字方法,比如ee.String.cat、ee.Number.add等,最后返回服务端Feature。只有当你最终要输出到外部时才用getInfo()或Export。
4.3 filter不生效:坐标系、几何精度与条件写法的坑
有时候filter明明写了条件,但是结果集合的大小还是和原始集合一样。我遇到过的原因有三个。
第一个是几何坐标参考不一致。你在本地用了UTM投影构建Geometry,上传或者手动构建时坐标值并不是WGS84的经纬度,导致filterBounds没匹配到任何要素。这个很容易排查:在Map上把原始集合和筛选集合都画出来,如果几何位置已经偏离地球实际位置,要么是坐标写反了,要么是投影没转换。GEE全平台强制使用WGS84,凡是外部来的坐标,都必须先转成经纬度再操作。
第二个是几何相交判定逻辑与直观预期不一致。比如点要素严格落在线要素上,filterBounds可能因为浮点精度问题返回空集。解决方法是给几何做一个很小的buffer,比如geometry.buffer(10),再filter。这在做道路周边站点提取时尤其有用。
第三个是条件写反了。ee.Filter.equals的语义是“属性值等于给定值”,如果想提取不满足条件的要素,要用ee.Filter.notEquals,而不是在回调里取反。还有ee.Filter.in和ee.Filter.stringContains也很容易被误用。我在做土地利用分类汇总时,想提取所有林地类别的地块,写成了ee.Filter.stringContains('landuse', 'forest'),结果只匹配到完全等于forest的记录,其实stringContains是匹配任意子串的,对应SQL里的LIKE '%forest%',这个没问题;但如果你改成ee.Filter.equals('landuse', 'forest'),就会漏掉“dense_forest”这类带前缀的记录。这提醒我:filter条件要和字段的实际取值范围精确对齐,最好先打印唯一值列表再决定筛选条件。
4.4 map回调里加入print与Map.addLayer:明明写了却不输出
这不是bug,而是一个很常见的理解误区。在map的回调里写print(f)或Map.addLayer(f),运行时并不会看到任何输出或图层。原因是map在服务端执行,回调里的客户端UI操作不会被执行——更准确地说,这些UI调用根本不会进入服务端计算图。GEE的map回调只关心返回值,其他的语句都会被忽略,或者在某些环境下直接报错。
如果你要调试map中间状态,有三个办法。第一,用map返回必要的中间变量作为属性,然后再从集合里select出来单独打印。第二,用limit(1)取回一个样本Feature,再用getInfo()打印它的详细内容。第三,如果你想可视化中间几何,可以把中间结果作为一个新的FeatureCollection保存下来,再Map.addLayer。
举一个具体调试示例:
javascript复制// 想在map里给站点添加缓冲区,并检查缓冲区是否正常
var buffered = sites.map(function(f) {
var buf = f.geometry().buffer(5000);
return f.setGeometry(buf);
});
// 这个方法在map回调里不可行:
// Map.addLayer(buf) 不会输出
// 正确做法:先取一个样本,打印
print('buffered sample', buffered.first());
Map.addLayer(buffered, {}, 'buffered sites');
这里要注意,buffered是一个集合对象,Map可以直接显示全部要素,但数量特别多的时候会把浏览器卡住,建议配上style参数控制颜色和透明度。
4.5 复杂案例:点要素按面统计的完整流水线
为了把这些知识串起来,我分享一个完整的实战流程:有很多散点(比如土壤采样点),每个点有soil_carbon属性,另外有一份流域面要素,需要按流域统计土壤碳的平均值、最大值、最小值。这个任务我在项目里跑过很多次,属于非常典型的需求。
第一步,准备数据。站点和流域都已上传到Asset:
javascript复制var samples = ee.FeatureCollection('users/yourname/samples');
var watersheds = ee.FeatureCollection('users/yourname/watersheds');
第二步,给每个站点添加它所在的流域ID。这本质上是点面连接,可以用join或filter实现。我推荐用join.saveMatches加innerJoin:
javascript复制var joined = ee.Join.saveMatches('matches').apply({
primary: samples,
secondary: watersheds,
condition: ee.Filter.intersects({
leftField: '.geo',
rightField: '.geo'
})
});
注意Filter.intersects的leftField和rightField要填.geo,这代表几何字段,是GEE内部的标准写法,不加就会报错。
第三步,把matches数组展开。因为一个点可能落入多个流域(边界重叠),所以要用map来处理:
javascript复制var expanded = joined.map(function(f) {
var matches = ee.List(f.get('matches'));
return matches.map(function(match) {
var ws = ee.Feature(match);
return f.set('ws_id', ws.get('ws_id'));
}).flatten(); // 注意flatten
});
var flat = ee.FeatureCollection(expanded.flatten());
这里用到了flatten()来把嵌套的Feature列表变成一维集合,非常容易漏。如果不flatten,后续的统计会因为集合里包含列表而报错。
第四步,按流域分组统计。用reduceColumns加一个group因子:
javascript复制var result = flat.reduceColumns({
reducer: ee.Reducer.mean().setOutputs(['mean_carbon'])
.combine(ee.Reducer.max().setOutputs(['max_carbon']))
.combine(ee.Reducer.min().setOutputs(['min_carbon'])),
selectors: ['ws_id', 'soil_carbon'],
group: 0
});
group: 0表示第一个selector(ws_id)是分组字段。返回的result是一个FeatureCollection,每个Feature对应一个流域id,包含三项统计值。最后再把统计结果连接回流域面要素,就能出图了。
javascript复制var finalMap = watersheds.map(function(ws) {
var id = ws.get('ws_id');
var stat = result.filter(ee.Filter.equals('group', id)).first();
return ws.set({
mean_carbon: stat.get('mean_carbon'),
max_carbon: stat.get('max_carbon'),
min_carbon: stat.get('min_carbon')
});
});
这条流水线里,最容易出错的地方是join后的flatten,以及reduceColumns的group参数。我建议第一次跑的时候把中间变量都打印出来,确认每个环节的数据结构都正确后再继续,能少很多定位问题的时间。
5. 经验总结与后续扩展方向
5.1 我在多次实战中总结的几条硬经验
FeatureCollection这个数据类型,表面上看是一个集合,但它的核心特点是“惰性求值”和“服务端对象”。我最大的体会是:写GEE代码的思维方式,与写本地Python脚本完全不同。本地脚本里可以随意for循环、随意print、随意修改列表,但在GEE里,习惯了这种写法的Python用户往往会写出一个跑不动甚至跑不通的程序。正确的方式是拥抱函数式编程,利用map、filter、reduce对全集合做矢量化操作,把条件、计算全部封装到服务端对象里。
另外,GEE的编辑器能力有限,加上不能本地断点调试,这种“黑盒感”是很多人劝退的原因。我的建议是:务必保持小步快跑,每个阶段都用limit限制数据量,然后打印一个样本验证。宁可多打印50次,也不要一次跑全量,等全量跑失败了再一行行猜哪里出了问题,成本实在太高。
还有一个建议,尽量把上传到Asset的数据在本地就清理干净:字段名统一、字段类型统一、几何无自相交、坐标WGS84。一次干净的输入,能省去后面大量修复数据的代码,也让你的程序简洁易读。
5.2 下一步可以展开的话题预告
FeatureCollection这个主题还有很多值得深挖的内容,我计划在后续几篇里继续整理。第二篇会重点讲FeatureCollection的join操作,包括内连接、外连接、空间连接、时间连接,还有我在处理站点时序数据时常用的saveAll技巧。第三篇会讲讲FeatureCollection和Image的联合分析,也就是reduceRegions、sampleRegions、extract这些空间统计接口的细节,这部分对做遥感反演和生态评估的人特别重要。第四篇可能会聊聊FeatureCollection的性能优化,如何在百万元素级别下让map和filter变得更快,如何避免无意义的重复计算,以及Export的最佳实践。
如果你在实际使用中也踩过什么关于FeatureCollection的坑,或者有特别想了解的场景,欢迎在评论区补充,我尽量在后边的篇幅里一起整理进去。做技术分享最有价值的部分,就是大家共同的实战经验能够互相补齐。
