cesium 直接加载 geotiff 影像图

前言

最近碰到了一个需求,需要通过 cesium 直接加载 geotiff 影像图

咋一听,这个需求好像蛮奇怪,cesium 本身本来就支持加载 tile 影像图,也就是所谓的切片地图。原理其实就是,通过 geoserver 等工具,按照一定的规则和坐标系规则,切好对应的切片。

而 cesium 里面,加载瓦片地图也很简单,想要显示哪个区域的地图,就根据对应的规则,去 geoserver 里请求对应的切片。这些逻辑在 cesium 里面,也已经封装好了,直接调用就好了。

但是如果不想发布到 geoserver,想直接通过 cesium,加载 geotiff 影像文件,来预览影像图呢?

说实话,刚开始碰到这个需求,内心也是没底的,毕竟翻遍了 cesium 的 api,也没有发现,其能支持这种加载方式。

而且,geotiff 影像图的格式,对于我来说,也是一片未知的领域。要不是去年开始接触 cesium 和 geoserver,我根本不知道它的存在。

当然,碰到问题,还是得发挥一个程序员的 geek 精神,先搜索下,看有没有人碰到同样的烦恼。

虽然这种方式不常见,但是,还是有同道中人的,但是结果多不理想,甚至有人直接回复,说不支持这种加载方式。

就在我一度想要放弃的时候,忽然有了灵感。

几个小问题

既然 geotiff 本质上是一张图片,文件不太大的图,甚至直接用一些常见的看图软件就能打开,那么想要贴在 cesium 的 globe 上,又有何难呢?

现在摆在面前的有几个问题:

  1. 如果 cesium 支持贴 tif 后缀的图,那么皆大欢喜,只要想方设法解析到 geotiff 的坐标范围信息,然后调用 cesium 提供的加载单张图作为图层的 api,再传入范围信息,即可正常的加载该 geotiff 图。
  2. 如果很不幸,cesium 不支持贴 tif 后缀的图,那么我们就得先解析 geotiff 文件,想办法获取到相关的地理信息和像素信息,拿到像素信息和地理信息以后,像第一种情形一样处理,无非就是多了一步将像素信息处理成 cesium 可以支持的图像信息而已。
  3. 我们该如何解析出 geotiff 内部的信息呢?

接下来,就让我们对提出的问题,一个个尝试解决方案,如果能够迎刃而解,那么用 cesium 加载影像图,不是如同探囊取物么!

尝试寻找解决方案

我们先来找找,看能否找到前端解析 geotiff 的解决方案。

我们知道,如果用桌面软件,查看 geotiff 图像,很多常见的软件都能支持,大到像 arcgis,小到像 windows 看图,都能查看。

但是前端,是否有现成的工具,可以用来解析 geotiff 图像呢?

带着这样的疑问,开始了我们的探寻之旅。

经过一番尝试以后,发现前端有个开源的库—— geotiff.js ,可以用来解析 .tif 格式的文件。

具体 geotiff.js 的 api,在这里就不做过多的介绍了,有兴趣了解的,可以去看下官方提供的 readme 文件,上面有用法的详细说明。

原始影像图

假设现在有个 geotiff 文件,用 IrfanView 文件打开,是这个样子的:
cesium 直接加载 geotiff 影像图

解析 geotiff 文件

geotiff.js 提供了几种写入读取 二进制文件的方式,为了方便使用,我们就尝试采用 fromBlob 的方式。
cesium 直接加载 geotiff 影像图

我们先调用 geotiff.js 提供的 api,将文件读取成 js 对象,再通过对象提供的 getImage api,获取到图像的相关信息。

const tiff = await fromBlob(blob);

let image = await tiff.getImage();
let [west, south, east, north] = image.getBoundingBox();

const code =
  image.geoKeys.ProjectedCSTypeGeoKey ||
  image.geoKeys.GeographicTypeGeoKey;

为了准确的把图贴到 cesium 的球面上去,我们必须要先获取到图像的范围,并且要获取到图像采用的是哪种坐标系。

我们测试的这张图,打印出上述信息,发现采用的是 4527 坐标系,范围如图示:
cesium 直接加载 geotiff 影像图

转换坐标的方法

现在问题是,cesium 目前已知的,只支持月球、标准的球体和 WGS84 体系的坐标体系,不支持我们的 CGCS2000 坐标系。
cesium 直接加载 geotiff 影像图

怎么办呢?我们必须能找到一个换算的方式,将我们的坐标换算成 WGS84 坐标体系里的点。

可是,由于本身对 gis 专业相关的基础知识的匮乏,对于坐标体系转换,毫无经验,根本不知道怎么转换该如何是好?

虽然,怎么转化,论文里都有,但是等学会那些,再来解决这个问题,都不知道要等到猴年马月去呀。

不过不要着急,我发现了一个网站支持这种服务,提供了这种转换的接口。

不用自己写转换坐标的算法,岂不是很舒服!

http://epsg.io/

首页长这样:

cesium 直接加载 geotiff 影像图

点击进入这个 transform coordinates 页面:

cesium 直接加载 geotiff 影像图

我们试着输入一个坐标:

cesium 直接加载 geotiff 影像图

返现返回了我们想要的结果,点进去看下位置:
cesium 直接加载 geotiff 影像图

现在问题是,虽然我们能在页面上获取转换结果,但是总不能每次都打开页面,输入地址,来获取转换后的坐标吧?

无妨,我们打开控制台看一下,转换的过程到底经历了写什么。

我们点一下 transform,发现页面发了一个 ajax 请求,里面包含了一些相关的信息

cesium 直接加载 geotiff 影像图

而返回的结果,正是在 4326 体系下,的经纬度坐标信息:
cesium 直接加载 geotiff 影像图

既然有了转换方式,可以转换坐标,那么接下来要做的就很简单了。

通过接口,获取该影像图所表示的地理区域的范围:

let { x: w, y: n } = await (
  await fetch(
    `//epsg.io/trans?x=${west}&y=${north}&s_srs=${code}&t_srs=4326`
  )
).json();
let { x: e, y: s } = await (
  await fetch(
    `//epsg.io/trans?x=${east}&y=${south}&s_srs=${code}&t_srs=4326`
  )
).json();

将 geotiff 像素信息写入 canvas

按理说,走到了这一步后,如果 cesium 支持直接加载 geotiff 图为静态的 图层,是最理想的状态,可惜的是,它并不支持。

既然它不支持,我们就要想办法另辟蹊径了。

// 读取像素信息
const [red = [], green = [], blue = []] = await image.readRasters();

// 将像素信息写入canvas
const canvas = document.createElement("canvas");
let width = image.getWidth();
let height = image.getHeight();
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext("2d");
let imageData = ctx.createImageData(width, height);

console.time("写入像素");
for (var i = 0; i < imageData.data.length / 4; i += 1) {
  imageData.data[i * 4 + 0] = red[i];
  imageData.data[i * 4 + 1] = green[i] || 0;
  imageData.data[i * 4 + 2] = blue[i] || 0;
  imageData.data[i * 4 + 3] = red[i] === 0 ? 0 : 255;
}

ctx.putImageData(imageData, 0, 0);

console.timeEnd("写入像素");

我们可以通过 image 对象提供的 readRasters 接口,将像素信息读取出来,然后写入 canvas,形成一张前端可以操控的图。

在 cesium 中加载

遗憾的是,cesium 的 SingleTileImageryProvider 接口,并不支持对 canvas 的直接载入,需要转换成图片才能进行操作。

我们可以调用 canvas 自带的 toDataURL 将 canvas 转换成图片,然后传进去即可。

let rectangle = Cesium.Rectangle.fromDegrees(w, s, e, n);
let du = canvas.toDataURL();

viewer.imageryLayers.addImageryProvider(
  new Cesium.SingleTileImageryProvider({
    url: du,
    rectangle,
  })
);

viewer.camera.setView({
  destination: rectangle,
});

这样,我们就成功的将该 geotiff 影像图,直接加载到 cesium 里面去了。
cesium 直接加载 geotiff 影像图

调整颜色

到了这一步,我们要做的差不多就结束了。

但是细心的同学可能会发现,加载到 cesium 里的影像图的颜色跟我们前面用软件打开的时候不太一样。

这是为什么呢?

要理解这个问题,可能需要童鞋们去了解下颜色的构成方式。

这里我们采用的是 rgb 的表示方法。

当我们运行代码的时候,进入调试模式,你会发现,

默认这个影像里面,只存储了 R 的信息,G、B 的信息并没有。

那么怎么处理呢?

其实很简单,只需要改一行代码即可:

const [red = [], green = red, blue = red] = await image.readRasters();

将 green 和 blue 均赋值一个初始值,等于 red 即可。

然后,我们再次尝试运行一下代码,就会得到下图所示的场景了:

cesium 直接加载 geotiff 影像图

此刻,细心的童鞋就会发现,这与我们之前打开的图一般无二了。

后记

当然,有一些情况,我们这里并没有考虑到,有兴趣的同学可以自己研究下:

  1. 一般情况下,geotiff 影像图都非常的大,我们的示例并未考虑到影像图的大小对系统的影响。
  2. 我们这里只考虑了单文件的情况,有时候,geotiff 的表示形式,存在多文件的情况。
  3. 可以尝试对配色进行修改,从而调出不同的风格的影像图,这是个很酷的功能。
上一篇:cesium播放视频


下一篇:Cesium之鼠标事件绑定和移除