很多刚接触GEE(Google Earth Engine)的人,会把“画一个矢量边界”理解成在地图上手动描点,或者以为创建宿主集合就像本地GIS软件里新建一个空白图层然后慢慢编辑。真正打开Code Editor操作后你会发现,GEE里的矢量逻辑和桌面GIS有很大差别:所有点线面都封装在Geometry对象里,加上属性字段就成了Feature,大量Feature堆在一起才是FeatureCollection。而要做“空的矢量边界集合”,核心就是弄明白这三个对象之间的关系,并且用对空集合的初始化方式。这篇文章就从一个最具体的场景说起:先创建一个空的FeatureCollection,再以geometry为例,把北京市的矢量边界范围填进去。适合刚入门GEE、被“集合、要素、几何体”这套概念绕晕的人,也适合需要在分析流程里动态组织边界的进阶用户。
1. GEE矢量数据的基础结构:Geometry、Feature与FeatureCollection三者关系
1.1 三个核心对象的定位与用途
在GEE的JavaScript API里,矢量数据的粒度划分得非常清楚。你可以把Geometry理解成“坐标点组成的形状”,它是纯粹的几何信息,不携带任何业务属性。比如一个点就是一个经度纬度坐标,一条线就是一串坐标点连起来,一个面就是首尾闭合的坐标环。Feature则是在Geometry外面包了一层“属性口袋”,等同于给某个形状挂上了名称、面积、编码这些字段。FeatureCollection是若干Feature的集合,相当于一个矢量图层或者一张空间表格。
三者的关系几乎可以直接类比成Excel:Geometry是某个单元格里的数字,Feature是带有表头的一行记录,FeatureCollection则是一张完整的表。GEE里的底层数据格式其实是GeoJSON,FeatureCollection对应的就是GeoJSON里的FeatureCollection类型,Feature对应Feature类型,Geometry则对应Point、LineString、Polygon这些几何类型。
常用构造方式可以参考下面这段代码:
// 构造三种常见Geometry var pt = ee.Geometry.Point([116.40, 39.90]); // 点:北京市中心附近 var line = ee.Geometry.LineString([[116.40, 39.90], [116.42, 39.92]]); // 线 var polygon = ee.Geometry.Polygon([ [115.42, 39.44], [117.51, 39.44], [117.51, 41.06], [115.42, 41.06], [115.42, 39.44] ]); // 面:一个矩形,后面会用它当北京市的近似范围 // 给Geometry挂上属性,变成Feature var feat = ee.Feature(polygon, {name: '北京市参考范围', id: '110000'}); // 把Feature放进FeatureCollection var fc = ee.FeatureCollection([feat]);这里面值得留意的是,ee.Feature的第一个参数必须是一个Geometry或者另一个Feature,第二个参数是属性字典。如果不给属性,Feature仍然是合法的,只是导出到表格时字段是空的。
1.2 为什么GEE的“画矢量”不是真的在画
桌面GIS里,你新建一个shp文件后,可以用编辑工具在地图上点节点。GEE的Code Editor虽然也提供绘制工具,但那个绘制结果只是一个临时的Geometry,不会自动变成项目里的矢量资产。GEE的矢量数据本质上是一套服务端对象:你在编辑器里输入 ee.FeatureCollection、ee.Feature、ee.Geometry 这些构造函数时,GEE并不会立刻去读取和渲染,而是把它当成一个延迟执行的查询计划,直到你需要它的时候才真正从服务器取数据。
所以很多人印象中的“画边界”,在GEE里实际是“用代码描述边界”。这也解释了为什么新手经常遇到的第一个困惑:写了一段代码,Map上却什么都没显示。这不一定是边界没构造成功,而可能是你没有用 Map.addLayer 把这个FeatureCollection叠加到底图上。后面我会专门讲这个坑。
2. 创建空矢量边界集合:两种标准写法和一个常见误区
2.1 空集合的标准写法
创建空集合看起来是件小事,但写法不对会让整个脚本直接报错。我见过不少人在网上提问“为什么我写 ee.FeatureCollection() 会报错”,其实官方推荐的写法是给构造函数传一个空数组:
var emptyCollection = ee.FeatureCollection([]); print('空集合大小:', emptyCollection.size());运行这段代码,控制台会显示空集合大小: 0。这个空集合是合法的服务端对象,它可以参与merge、union、style这些操作,只是暂时没有任何Feature。之所以要传 [],是因为GEE的构造函数需要从参数推断出集合的类型;如果什么都不传,引擎无法确定你到底想要一个什么类型的集合。
另一种等价写法是用 ee.List:
var emptyList = ee.List([]); var emptyCollection2 = ee.FeatureCollection(emptyList);实际开发中,第一种写法就够用了,第二种适合你在已经有ee.List对象、想把它转成集合的场景。两种写法生成的对象完全等价,你可以用 print(emptyCollection.equals(emptyCollection2)) 验证。
2.2 常见误区:null、不传参和客户端空列表
最容易踩的坑是试图用 null 或 undefined 初始化集合:
// 下面两行都会在运行时报错或得到无效对象 var bad1 = ee.FeatureCollection(null); var bad2 = ee.FeatureCollection();原因很简单:GEE的构造函数不接受空参数,也不接受客户端JavaScript的null。它需要的是一个可以转换为服务端列表的参数,[] 是最直接的选择。还有一个隐蔽的问题:如果你在一个循环里反复给变量赋新的集合,一定要记得 GEE 的对象是不可变的。比如 emptyCollection.merge(...) 会返回一个新集合,而不是在原来的 emptyCollection 上追加。我之前看到有同学这样写:
var fc = ee.FeatureCollection([]); fc.merge(ee.FeatureCollection([feature])); // 返回值被丢弃,fc仍然是空的这就直接导致后面 print(fc.size()) 一直是0,但他找半天找不到原因。正确写法是:
fc = fc.merge(ee.FeatureCollection([feature]));2.3 空集合的实际价值:一个会持续生长的容器
空集合本身没有分析意义,但它在动态构建矢量数据的场景里非常有用。比如你想按属性分批加入多个区域的边界,或者在一个循环里根据条件不断添加要素,这时候先初始化一个空集合,再逐步merge,就是一个干净可靠的组织方式。还有一个更实际的用途:当你只关心集合的属性表结构时,先建一个空集合,再把各个Feature合并进去,最后一次性 Export.table.toDrive 导出,所有区域的数据就可以统一落盘。这个思路在后续章节会继续展开。
3. 用Geometry生成北京市边界并填入集合:可以直接抄的完整示例
3.1 从Geometry到Feature再到集合的完整链路
现在我们回到标题的场景:准备一个空集合,然后把北京市的矢量边界范围填进去。由于本文重点是演示“空集合 + Geometry”的组合方式,我这里用一个矩形来代替真实的北京边界。矩形范围用了北京的大致经纬度区间:经度115.42°E到117.51°E,纬度39.44°N到41.06°N。这个范围能覆盖北京的主要区域,但并不是精确的行政区划边界,你后续可以用真实shp替换这一段。
// 第一步:空集合 var borderCollection = ee.FeatureCollection([]); // 第二步:用Geometry定义北京市近似范围的矩形 var beijingBounds = ee.Geometry.Polygon([ [115.42, 39.44], [117.51, 39.44], [117.51, 41.06], [115.42, 41.06], [115.42, 39.44] ]); // 如果你喜欢更简洁的写法,也可以用Rectangle: // var beijingBounds = ee.Geometry.Rectangle(115.42, 39.44, 117.51, 41.06); // 第三步:给Geometry挂上属性,变成Feature var beijingFeature = ee.Feature(beijingBounds, { name: 'Beijing_Approx', id: 110000 }); // 第四步:把Feature放进集合 borderCollection = borderCollection.merge(ee.FeatureCollection([beijingFeature])); // 第五步:验证并可视化 print('集合中有几个要素:', borderCollection.size()); print('第一个要素:', borderCollection.first()); Map.centerObject(borderCollection, 7); Map.addLayer(borderCollection, {color: 'red'}, '北京市矢量边界');把这段代码贴到Code Editor里运行,你会看到控制台输出集合中有几个要素: 1,地图上出现一个红色矩形框。这里最需要理解的是第4步的merge逻辑:merge返回新集合,它会把传入集合里的所有Feature合并进来;如果两个集合的字段名不同,GEE会自动把字段合并成并集,缺少的字段填null,这一点在后面导出表格时要注意。
3.2 为什么要先Merge而不是直接构造FeatureCollection
你可能会觉得,既然最后只有一个Feature,为什么不直接写 ee.FeatureCollection([beijingFeature])?当然可以,而且结果一样。但这里特意演示空集合merge,是为了对齐标题里的“空矢量边界集合”:在真实项目中,尤其是处理批量的、动态追加的区域时,习惯先建空集合再逐步合入,这样做有几个好处:
第一,代码逻辑清晰,哪里是初始容器、哪里是新增数据一目了然。第二,merge是服务端操作,不会把大量数据传输到客户端,即使最终集合里有几千个Feature,性能影响也可控。第三,如果你后面要在循环里按条件添加,空集合merge的方式天然适配。比如你想把北京市各个区县的边界动态加入同一个集合,只需要在循环里反复 merge 即可。
3.3 查看和验证Geometry内容
有人会问,我怎么确认Geometry里的坐标是不是我想要的?最简单的方法是打印:
print('Geometry坐标:', beijingBounds.coordinates()); print('GeoJSON完整表示:', beijingBounds);输出的坐标列表应该是五个点,首尾相同,代表多边形已经闭合。GEE的Polygon要求坐标环闭合,如果你给的坐标列表第一个点和最后一个点不一样,引擎会尝试自动闭合,但这可能会引发意料之外的拓扑行为,所以我建议你写多边形时,习惯性把首尾坐标写重复一次。
4. 真实行政边界加载进GEE的三种路径:JSON坐标、CSV点表和Assets上传
4.1 路径一:直接用公开数据集筛选北京市
如果你的任务不要求本地数据,最快的方式是从GEE内置的公开数据集里直接筛出北京市的行政边界。比如FAO GAUL全球行政区划数据集,可以通过代码直接加载并筛选:
var gaul = ee.FeatureCollection('FAO/GAUL/2015/level1'); var beijingGaul = gaul.filter(ee.Filter.eq('ADM1_NAME', 'Beijing')); print('筛选结果:', beijingGaul);不过要注意,不同数据集的字段名不一样。GAUL里省一级的名称字段是ADM1_NAME,其他数据集可能是NAME或ADMIN。筛选之前,建议先不加filter直接打印集合的第一个要素,或者用 ui.Chart.feature.byFeature 查看字段列表,再确定你需要的行政区名称写法。用公开数据集的好处是省去了上传步骤,缺点是数据的时相和精度不完全由你控制,如果用于严格的行政边界分析,最好还是用自己的数据。
4.2 路径二:把本地shapefile上传到Assets再从代码加载
这是最通用的方案。操作路径是:Code Editor界面右侧 Assets 标签页 → NEW → Shape file → 选择本地文件上传。这里有个关键细节:shapefile不是单一文件,而是至少包含 .shp、.shx、.dbf、.prj 四个文件,上传时需要全部选中一起传。如果你只传了.shp,GEE虽然可能不报错,但加载后会因为没有投影信息而出现坐标错乱。
上传完成后,你会得到一个类似 users/你的用户名/beijing_shp 的资产ID,代码里这样加载:
var beijingShp = ee.FeatureCollection('users/你的用户名/beijing_shp'); Map.centerObject(beijingShp, 9); Map.addLayer(beijingShp, {color: 'blue'}, '北京shp');在上传之前,我还建议用桌面GIS工具提前检查一下数据坐标系。如果原始数据不是WGS84经纬度,而是在CGCS2000或者UTM投影下,最好先转换成WGS84再上传,否则在GEE里和Landsat、Sentinel等影像叠加时可能会有偏移。有些同学看到边界“飘”到海里,十有八九是投影没统一。
4.3 路径三:用坐标点表构造Geometry再填入集合
有时候你手里没有shapefile,只有一份包含经纬度的CSV表格。这种情况不需要上传shapefile,直接通过CSV资产导入即可:Assets → NEW → Table upload → 选择CSV文件。CSV里必须有经度和纬度列,GEE会自动识别。
var points = ee.FeatureCollection('users/你的用户名/my_points'); print(points); // 如果希望把点转成小缓冲区,作为临时边界范围使用 var buffered = points.map(function(f) { return f.buffer(1000); // 1000米缓冲区 });虽然CSV点表生成的是点要素而不是面边界,但在很多分析里,可以先对点做缓冲区,生成圆形范围,再从这些范围构造出一个新的FeatureCollection。如果你有一批北京市的兴趣点坐标,想批量生成对应缓冲区,这种“坐标点表 + map + buffer”的组合方式,比一个一个手画高效得多。这也是空集合思维的一种延伸:集合是一个组织结构,数据从哪里来、是什么几何类型,都取决于你的业务需要。
下面用表格总结一下三种路径的优缺点,方便你选型:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 公开数据集筛选 | 零上传成本、实时加载 | 数据时相和精度不可控 | 快速预览、区域轮廓分析 |
| 上传shp到Assets | 精度可控、字段自定义 | 上传步骤多、需注意投影 | 正式项目、严格行政边界 |
| CSV坐标点表 | 轻量、适合批量坐标 | 只有点要素,面需额外构建 | 点位分布、缓冲区分析 |
5. 集合建好后怎么用:筛选、面积计算与导出中的联动细节
5.1 按属性筛选与空间筛选
FeatureCollection建立起来后,最常见的第一件事就是筛选。按属性筛选用 filter,按空间关系筛选用 filterBounds。假设你的集合里不只是北京,而是全国多个省份,现在想把名字包含“北京”的要素挑出来:
var bi = fc.filter(ee.Filter.stringContains('name', '北京')); print(bi); // 按空间范围筛选:取出和某个Geometry相交的要素 var targetArea = ee.Geometry.Point([116.3, 39.9]).buffer(50000); var nearBeijing = fc.filterBounds(targetArea);这里需要理解的是,GEE的filter是服务端操作,并不会真的把数据拉到浏览器,返回的仍然是一个FeatureCollection引用。你后续对这个筛选结果做面积计算、导出,GEE都只在服务端计算,最终只会把精简结果传回客户端。这也是GEE能处理海量矢量数据的关键原因之一。
5.2 面积计算:平方米、公顷与平方公里
很多人在计算边界面积时被单位坑过。GEE里 Geometry.area() 返回的是平方米,这是默认的椭球面积,基于WGS84椭球体计算。如果直接用数字看会觉得特别大,比如北京市矩形近似范围算出来大约2.8万平方公里左右,也就是280亿平方米。计算代码:
var areaSqm = beijingBounds.area({maxError: 1}); print('面积(平方米):', areaSqm); print('面积(平方公里):', areaSqm.divide(1e6));其中 maxError 参数可以理解成面积计算允许的最大误差,单位是米。设1米已经比较严格,日常分析可以接受。如果你不关心椭球算法,可以用 projected 面积,但那需要明确投影坐标系,复杂度反而更高。我通常就用 area({maxError: 1}),再把结果除以1e6转成平方公里。
5.3 遍历集合里的每个边界
如果你想给集合里每个要素计算面积,并把结果写回属性表,用 map 方法:
var withArea = fc.map(function(f) { var geom = f.geometry(); var a = geom.area({maxError: 1}); return f.set('area_sqm', a); }); print(withArea);这里有一个很容易忽略的点:map 返回的新FeatureCollection,属性是在原来基础上叠加的,原来的 name、id 等字段都会保留,只是多了一个 area_sqm。如果后续要导出,直接用 withArea 就行。
5.4 导出表格和影像裁剪的联动
矢量集合最常见的下游操作,一个是导出属性表,另一个是裁剪影像。导出表格很简单:
Export.table.toDrive({ collection: withArea, description: 'Beijing_Area_Export', fileFormat: 'CSV' });但这里有个经典坑:Export.table 默认只导出属性字段,不会把Geometry的坐标原样写进CSV。如果你想在CSV里看到每条边界对应的坐标,得先把Geometry转成可读属性,例如:
var exportReady = withArea.map(function(f) { return f.set('coordinates', f.geometry().coordinates().toList()); });不过要注意坐标列表可能会很长,这样导出的CSV里会是一长串嵌套数组,阅读体验一般。更常见的做法是保留经纬度坐标范围的最大最小值,或者只导出质心坐标:
var centroid = withArea.map(function(f) { var center = f.geometry().centroid(); return f.set('lon', center.coordinates().get(0)) .set('lat', center.coordinates().get(1)); });影像裁剪则是这样联动:
var image = ee.Image('LANDSAT/LC08/C02/T1_TOA/20230101'); // 示意影像ID,实际请替换 var clipped = image.clipToCollection(withArea);clipToCollection 会把影像裁剪到集合所有要素的范围内,如果集合里有多个要素,影像会被裁剪成多块,但对单个要素的统计你还是需要继续配合 reduceRegions 来做。这块内容再展开又是一篇文章,这里先点到为止。
6. 我踩过的坑:边界“看不见”、属性消失和面积数字不对
6.1 边界明明构造了,地图上却什么都不显示
这是GEE新手最常见的诡异现象。排查链路我建议按下面顺序来:
第一步,先打印集合大小,如果 size() 是1,但地图空白,说明要素确实存在于服务端,问题出在显示环节。第二步,检查有没有执行 Map.centerObject。GEE的地图不会自动定位到你的边界,你不写 centerObject,底图可能一直落在默认视野,边界在视野之外。第三步,检查 Map.addLayer 的参数,特别是是否设置了过于隐蔽的颜色。
我遇到过一个更隐蔽的情况:我上传了一个shp,加载后打印size正常,但地图上就是看不到,后来发现是图层堆叠顺序问题,上面一层不透底影像把边界完全盖住了。解决方法是调整Map.addLayer的shares参数,或者在Layer Manager里把矢量图层拖到最上面。
6.2 导出后属性栏是空的,Coordinate字段也不见
你都把FeatureCollection构造好了,Export.table.toDrive 也成功了,打开CSV却发现除了几何类型之外什么都没有,连坐标也没有。这是因为Export.table导出的是Feature的属性,而Geometry在GeoJSON里是独立于属性的数据。想让坐标进入CSV,需要显式把坐标写进属性,就像前面5.4节写的那样,用 map 给每个Feature添加 coordinates 字段。
同理,如果你构造Feature时忘了给properties参数:
var feat = ee.Feature(beijingBounds); // 没有第二个参数那么导出时这个Feature就是一条空属性的记录,看起来很莫名其妙。所以我在构造矢量要素时,习惯第一时间确定属性结构,哪怕只有一个id也行。属性结构稳定了,后面所有基于属性字段的filter、set、export都会顺很多。
6.3 面积大得离谱,或小得可怜
如果你计算出的北京市面积是2.8e14这种量级,那一定是单位问题。再次强调:GEE默认返回平方米。如果把平方米直接当成平方公里看,数字自然会大6个数量级。反过来,如果你计算一个小地块面积只有0.02,很可能是你把米制单位误写成经纬度单位的平方,也就是把坐标差值直接当距离算了。多边形面积计算应该始终用 geometry.area(),不要自己用坐标差值估算。
另外还可以用 reduceRegion 来计算栅格面积,但那个结果是基于像元个数,和矢量面积定义不一样。如果你做的是地块面积对比,请务必保持统计口径一致,否则后续分析会得出互相矛盾的数字。
6.4 上传shapefile后边界位置完全错乱
这个问题我早期踩得最深。在QGIS里看shp一切正常,传到GEE里整个边界却跑到了别的国家。根因通常是上传时丢失了 .prj 文件,或者原始数据本身不是WGS84坐标系。GEE对矢量数据的要求是WGS84经纬度坐标系(EPSG:4326),如果你的源数据是CGCS2000、UTM或者其他投影,先转换再上传。
还有一个小提醒:在Assets上传面板里,如果上传的是shapefile,GEE会自动识别同名文件里的 .prj;如果从压缩包里只拖出了.shp,其他文件没选,就很容易出现投影缺失。我现在的习惯是:在本地用一个标准的矢量工具箱把所有文件检查一遍,统一转成EPSG:4326并导出为一个包含完整文件集的文件夹,再整体打包上传。这样基本不会再遇到位置漂移。
最后再分享一个我自己的使用习惯:空集合的概念并不只存在于项目初始化阶段。我在做多期数据分析的时候,经常先建一个空的FeatureCollection,然后用循环把每一期的结果算出来逐个merge进去,最后一次性导出。这种做法不需要频繁修改代码结构,也方便中途对任何一期结果做单独检查。你也完全可以沿用这个思路,把北京市边界这个示例替换成你业务里的真实边界,剩下的代码骨架基本不用动。