17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

地理院標高タイルを Cloudflare Workers でTerrain RGBに変換して利用する

Last updated at Posted at 2023-08-11

国土地理院は「標高タイル」という標高データ (DEMデータ) を表現した地図タイルを無償で配信してくれています。しかしこのタイルは独自のエンコード形式になっているため、QGISなどの一般的なアプリケーションや地図系ライブラリで簡単に読み込んで利用することができません。今回は、Cloudflare Workers というエッジコンピューティングサービスを使って「Terrain-RGB」という、より一般的なエンコーディング形式に「オンデマンド」で変換することでQGISで扱えるようにしてみます。

Screenshot 2023-08-12 at 6.54.13.png
(↑今回、国土地理院標高タイルをQGISで表示してみた例)

Cloudflare Workers は、いわゆる「CDN」でのエッジコンピューティングを行えるサービスです。今回はこれを使って、ユーザからタイルが要求されるたびに、地理院の標高タイル (PNG画像) を取得しつつ、オンザフライで Terrain RGB 形式のPNG画像に変換して配信するようにします。CDNエッジコンピューティングならではの利点として、変換したファイルをCDNにキャッシュさせることも容易です。

地理院標高タイル vs. Terrain RGB

両者はいずれも24bitカラー画像のRGB値をうまく使って標高 (DEM) データを表現する手法ですが、RGB値の表現の仕方が異なります。

地理院標高タイルの標高 h は以下のような式で表現されます:

$x = 2^{16} R + 2^{8} G + B$
$x < 2^{23}$ の場合 $h = xu$
$x = 2^{23}$ の場合 $h = NA$ (無効値)
$x > 2^{23}$ の場合 $h = (x-2^{24})u$
uは標高分解能(0.01m)を表します。

一方で、世界でより広く使われている Terrain RGB という形式では以下のスタイルで標高を表現します:

elevation = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)

分解能は標高タイルの形式のほうが優れているのですが、そのほかは各ピクセルの値の表現が違うだけです。したがってCloudflare Workers上で実行するJavaScriptでPNG画像を読み書きして、全ピクセルについてRGB値の変換を行えば今回の目的が実現できそうです。

Cloudflare Workers のプロジェクトをセットアップする

Cloudflare Workers についての説明は省略して、Cloudflareのアカウントも登録済みだとして、その後の手順のみを示します。

  1. Wrangler (Cloudflare WorkersのCLI開発ツール) をインストールします

    $ npm install -g wrangler
    
  2. Cloudflare Workers にログインします

    $ wrangler login
    
  3. Wrangler にプロジェクトのひな形を出力させましょう

    $ wrangler init gsi-terrain-rgb
    

画像を変換するコードを実装する

wranglerによって生成されたひな形の src/workers.ts を編集して、地理院タイルのPNG画像を Terrain RGB 形式のPNG画像に変換するコードを書きます。

PNG画像のデコードと再エンコードには fast-png というモジュールを使ってみました。

src/workers.ts
import { decode, encode } from 'fast-png';

export interface Env {
  API_KEY: string;
}

function gsiToTerrainRGB(r: number, g: number, b: number): [number, number, number] {
  // Terrain RGB 形式のRGB値に変換
  if (r == 128 && g == 0 && b == 0) return [1, 134, 160]; // NA値は0にマップ
  let x = 65536 * r + 256 * g + b;
  const elev = x < 8388608 ? x * 0.01 : (x - 16777216) * 0.01;
  let value = (elev + 10000) / 0.1;
  r = Math.floor(value / (256 * 256));
  value %= 256 * 256;
  g = Math.floor(value / 256);
  b = Math.floor(value % 256);
  return [r, g, b];
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    // AUTH
    if (url.searchParams.get('key') != env.API_KEY) {
      return new Response('authentication failed', { status: 401 });
    }

    const cache = caches.default;
    const cacheKey = new Request(url.toString(), request);
    let response = await cache.match(cacheKey);
    if (response) return response;

    const m = url.pathname.match(/dem(.+)\/\d+\/\d+\/\d+.png/);
    if (m == null) {
      return new Response('not found', { status: 404 });
    }

    const res = await fetch('https://cyberjapandata.gsi.go.jp/xyz/' + m[0], {
      cf: { cacheEverything: true, cacheTtlByStatus: { '200-299': 14400, 404: 7200, '500-599': 0 } },
    });
    if (!res.ok) {
      return res;
    }

    // 画像の各ピクセルの値を変換
    const img = decode(await (await res.blob()).arrayBuffer());
    for (let i = 0; i < img.height * img.width; i++) {
      const p = i * 3;
      [img.data[p], img.data[p + 1], img.data[p + 2]] = gsiToTerrainRGB(img.data[p], img.data[p + 1], img.data[p + 2]);
    }

    response = new Response(encode(img), { status: 200, headers: { 'Content-Type': 'image/png' } });
    ctx.waitUntil(cache.put(cacheKey, response.clone()));
    return response;
  },
};

リクエストの処理と画像の変換を行うworkerのコードは以上だけで、短いです。(キャッシュのための処理を省けばさらにシンプルにもなります)。リクエストの処理の仕方などはCloudflare公式のサンプルコードを参考にしました。

それでは、さっそくCloudflare Workersにデプロイしてみましょう。以下を実行するだけでOKです:

$ wrangler deploy

デプロイ結果として https://gsi-terrain-rgb.xxxxxxxx.workers.dev のような worker のURLが表示されると思います。

今回は不特定多数からのアクセスを防ぐために、?key= というリクエストパラメータを要求するコードにしてあります。そこで使っている環境変数 API_KEY の値は今回はCloudflareのコンソールからセットしました。

なお、Cloudflare のキャッシュを利用する場合は、Cloudflare のDNSで管理している独自ドメイン名をworkerに割り当てる必要があります。しかし以下の説明では Cloudflare によって付与された workers.dev ドメインのまま利用しています(つまりキャッシュは機能しません)。

QGISで読み込んでみる

QGISのXYZタイルソースとして読み込んでみましょう。デプロイされた worker のURLを使って https://gsi-terrain-rgb.xxxxxxxx.workers.dev/dem_png/{z}/{x}/{y}.png をタイルソースとして参照します。また、今回は不特定多数から閲覧されないようにするため ?key={秘密のキー} というキーを要求するようなコードにしているので、それもURLに付与します。

標高タイルとして認識させるため、「データの解釈」で「MapTiler Terrain RGB」を選択するのを忘れずに!(今回は、地理院の標高タイルを、Cloudflare Workers で Terrain RGB の形式に変換しているのでした)。

Screenshot 2023-08-12 at 6.42.51.png

このソースを読み込むと、無事に標高データが表示されました!

Screenshot 2023-08-12 at 6.55.32.png

レンダリングタイプを「陰影図 (hillshade)」にすると次のような表示になります。

Screenshot 2023-08-12 at 7.06.50.png

Cloudflare Workers のダッシュボードでメトリクスを見ると、おおむね各タイルを1ミリ秒以下のCPU時間で処理できているようです。

追記: 3D表示もできる

地理院の写真タイルと一緒に読み込んでQGISの3Dビューを開けば、簡単にそれらしい3D表示ができます(この例は黒部ダムのあたり)。

Screenshot 2023-08-20 at 12.10.44.png

地形のタイル解像度は 256px に設定しましょう(デフォルトの16pxだとかなり荒い地形になってしまいます)。

Screenshot 2023-08-20 at 12.11.18.png

おわりに

Cloudflare Workersなどのエッジコンピューティングには様々な活用方法がありそうですし、もっともっと高度なことができるでしょうが、今回は、ちょっとしたJavaScriptを書くだけで、ピクセル操作を伴う画像のオンデマンド加工がCDNエッジ上で手軽にできることを示しました。

17
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?