GeoTrellis 如何对全球范围影像数据进行切片

612 阅读4分钟

记录一次排查 GeoTrellis 进行影像切片时的错误排查,主要是探讨 GeoTrellis 提供的两种默认切片布局:

WebMercator 和 Latlng,如何切全球范围的影像数据,并通过 Cesium 进行可视化。

1 常见的两种 TMS 瓦片布局

这里我主要是介绍目前用的最多的,也是 Cesium 等 WebGIS 框架中提供的两种默认切片布局,分别是经纬度与 Web 墨卡托,在 Cesium 中对应布局参数为:WebMercatorTilingScheme 和 GeographicTilingScheme

CesiumJS 能用的 TMS 目前只支持两种切片方案(TilingScheme):

  • 0 级瓦片有 2 个的地理坐标系投影 GeographicTilingScheme,EPSG:4326
    • 基于 WGS84 的经纬度投影,使用度作为单位。
    • 瓦片布局通常是从左下角 (90°S, 180°W) 到右上角 (90°N, 180°E)。
    • 这种布局方式较常见于一些以全球范围地理数据为主的应用场景。
    • 瓦片分割通常按照经纬度直接划分,因此与墨卡托投影的矩形分割不同。

0 级瓦片有 2 个的投影,是直接以经纬度数值展平成平面,众所周知:

纬度跨度:经度跨度 = 180:360 = 1:2

所以 GeographicTilingScheme 的样子就是一个一比二的矩形,刚好就在 0 级瓦片时有两个,后续就按常规的四叉树切分即可

  • 0 级瓦片只有 1 个的 Web 墨卡托投影 WebMercatorTilingScheme,EPSG:3857
    • 这是目前最常用的地图投影方式之一,尤其在网络地图应用中被广泛采用。
    • 坐标系为球形墨卡托(Pseudo-Mercator),单位是米,覆盖范围为全球(85.05113°S 至 85.05113°N 的纬度范围)。
    • 瓦片的分辨率和缩放级别与常见的 Web 地图服务(如 Google Maps、OpenStreetMap)兼容。

WebMercator 投影后的坐标系 xy 值域是 [-20037508.34, 20037508.34]^2,是一个正方形,所以可以按单个0级瓦片进行四叉树切分

2 GeoTrellis 提供的切片布局

GeoTrellis 也是默认提供了两种切片布局,分别是 WebMercator 和 Latlng,阅读其源码,得知,其实就是使用了 EPSG:3857EPSG:4326,下面代码是如何使用 GeoTrellis 构造布局。

import geotrellis.proj4.{CRS, LatLng, WebMercator}

// WebMercator 布局
val layoutScheme: ZoomedLayoutScheme = ZoomedLayoutScheme(CRS.fromEpsgCode(3857), tileSize = 256)

val layoutScheme: ZoomedLayoutScheme = ZoomedLayoutScheme(WebMercator, tileSize = 256)

// Latlng 布局,以下两种方式一样
val layoutScheme: ZoomedLayoutScheme = ZoomedLayoutScheme(CRS.fromEpsgCode(4326), tileSize = 256)

val layoutScheme: ZoomedLayoutScheme = ZoomedLayoutScheme(Latlng, tileSize = 256)
  • 存在的问题

GeoTrellis 提供的 WebMercator 布局与我们熟知的 Web 墨卡托,布局一致,可以正常在 Cesium 中进行加载,但是其提供的 Latlng 布局,与我们熟知的地理坐标系投影不一样,其 0 级瓦片只有 1 个,因此不能在 Cesium 中正常加载。

3 解决方案

我认为主要有三种解决手段:

  • 方案一:在 GeoTrellis 中自定义一种瓦片布局,使其向 Cesium 兼容
  • 方案二:在 Cesium 中自定义一种瓦片布局,使其向 GeoTrellis 兼容
  • 方案三:在 GeoTrellis 切片时,先裁剪掉维度85°以外的区域,然后使用 WebMercator 切片方式

这三种方式,前两种我都已经测试过,可以解决不兼容的问题,第三种方式我推测也是可以。

3.1 方案一

  • 通过 LayoutDefinition 构造自定义瓦片布局
val tileSize: Int = 256
val initialExtent: Extent = Extent(-180, -90, 180, 90)
val tileLayout = TileLayout(2, 1, tileSize, tileSize)
val layoutScheme: LayoutDefinition = LayoutDefinition(initialExtent, tileLayout)

val (zoom, reprojected): (Int, RDD[(SpatialKey, MultibandTile)] with Metadata[TileLayerMetadata[SpatialKey]]) = coverage.reproject(CRS.fromEpsgCode(4326), layoutScheme)
  • Cesium 中加载 TMS 服务
var tmsLayer = viewer.imageryLayers.addImageryProvider(
    new Cesium.UrlTemplateImageryProvider({
        tilingScheme: new Cesium.GeographicTilingScheme(),
        url: "http://10.101.240.60/tms_test/{z}/{x}/{y}.png",
    })
);

但是这种方式有如个缺点,用上述代码定义的 layoutScheme 生成的 reprojected 会被默认指定为0级,即 zoom = 0,这是因为:

LayoutDefinition 是单一层级布局

  • LayoutDefinition 只是一个单独的布局定义,它没有层级的概念。
  • 当你手动定义 TileLayoutExtent,GeoTrellis 会将其视为当前分辨率下的初始层级(zoom = 0),而不会根据瓦片数量或地理范围自动推导层级。

未使用层级推导机制

  • ZoomedLayoutScheme 提供了自动化的多层级划分逻辑,基于分辨率的缩小比例计算每个层级的 zoom
  • 手动定义的 LayoutDefinition 不会通过 ZoomedLayoutScheme 的逻辑进行层级映射。

3.2 方案二

  • GeoTrellis 中就使用其提供的经纬度布局方案
// Latlng 布局,以下两种方式一样
val layoutScheme: ZoomedLayoutScheme = ZoomedLayoutScheme(CRS.fromEpsgCode(4326), tileSize = 256)

val layoutScheme: ZoomedLayoutScheme = ZoomedLayoutScheme(Latlng, tileSize = 256)
  • 在 Cesium 中基于 GeographicTilingScheme 修改布局参数,得到自定义的布局方案,并在加载时指定布局
const customTilingScheme = new Cesium.GeographicTilingScheme({
    rectangle: Cesium.Rectangle.fromDegrees(
        -180.0,
        -90.0,
        180.0,
        90.0
    ), // 瓦片覆盖范围
    numberOfLevelZeroTilesX: 1, // 0 级横向瓦片数(默认是 2)
    numberOfLevelZeroTilesY: 1, // 0 级纵向瓦片数(默认是 1)
});

var tmsLayer = viewer.imageryLayers.addImageryProvider(
    new Cesium.UrlTemplateImageryProvider({),
        tilingScheme: customTilingScheme,
        url: "http://10.101.240.60/tms_test/{z}/{x}/{y}.png",
    })
);

3.3 方案三

方案三主要是考虑到 EPSG:3857 支持的纬度范围在:85.05113°S 至 85.05113°N,而极地区域的数据如果我们不是很需要的话,实际上我们可以在读取影像数据为 RDD 时就将这部分数据过滤掉,因为比较简单,就没有给出代码,推测也是可以达到效果的。

4 结果展示

image-20241126153723558