ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GEE空FeatureCollection创建与北京矢量边界填充详解

GEE空FeatureCollection创建与北京矢量边界填充详解 很多刚接触GEEGoogle 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的集合相当于一个矢量图层或者一张空间表格。三者的关系几乎可以直接类比成ExcelGeometry是某个单元格里的数字Feature是带有表头的一行记录FeatureCollection则是一张完整的表。GEE里的底层数据格式其实是GeoJSONFeatureCollection对应的就是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.Listvar 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 四个文件上传时需要全部选中一起传。如果你只传了.shpGEE虽然可能不报错但加载后会因为没有投影信息而出现坐标错乱。上传完成后你会得到一个类似 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上传面板里如果上传的是shapefileGEE会自动识别同名文件里的 .prj如果从压缩包里只拖出了.shp其他文件没选就很容易出现投影缺失。我现在的习惯是在本地用一个标准的矢量工具箱把所有文件检查一遍统一转成EPSG:4326并导出为一个包含完整文件集的文件夹再整体打包上传。这样基本不会再遇到位置漂移。最后再分享一个我自己的使用习惯空集合的概念并不只存在于项目初始化阶段。我在做多期数据分析的时候经常先建一个空的FeatureCollection然后用循环把每一期的结果算出来逐个merge进去最后一次性导出。这种做法不需要频繁修改代码结构也方便中途对任何一期结果做单独检查。你也完全可以沿用这个思路把北京市边界这个示例替换成你业务里的真实边界剩下的代码骨架基本不用动。
返回列表