すべてのプロダクト
Search
ドキュメントセンター

PolarDB:タイル分割なしのリモートセンシング画像ブラウジングのための GanosGanosBase ベースのローコード実装: ピラミッド

最終更新日:Apr 03, 2025

PolarDB for PostgreSQL データベースと GanosBase 時空間データベースエンジンを使用すると、サードパーティツールに依存することなく、SQL 文のみを使用してリモートセンシング画像データを迅速に管理および表示できます。GanosBase は、事前タイル分割なしで画像をブラウジングするための 2 つの方法を提供します。1 つは、GanosBase Raster 拡張機能を使用して、ウィンドウ範囲を使用して画像データを取得および表示する方法です。もう 1 つは、固定タイル範囲を使用して画像データを取得および表示する方法です。このトピックでは、最初の方法の使用方法について説明し、フロントエンドとバックエンドのサンプルコードを提供して、GanosBase Raster の使用方法をすぐに理解できるようにします。

背景

増え続けるリモートセンシングデータを管理する場合、マップに画像を表示するために次の質問をするかもしれません。

  • 数回しか使用されない可能性のある画像に対して、従来のタイル分割と公開プロセスを行う価値はありますか?タイルはどこに保存されますか?タイルデータはその後どのように管理されますか?

  • リアルタイムタイル分割ソリューションは、事前タイル分割なしで応答要件を満たすことができますか?特に大きな画像を処理する場合、リアルタイムタイル分割はどの程度影響を受けますか?

ただし、PolarDB for PostgreSQLGanosBase Raster 拡張機能を使用すると、サードパーティツールに依存することなく、SQL 文のみを使用してデータベースからマップに画像を効率的に表示できます。

ベストプラクティス

前提条件

画像データをインポートする

  1. ganos_raster 拡張機能をインストールします。

    CREATE EXTENSION ganos_raster CASCADE;
  2. raster 列を持つ raster_table という名前のテストテーブルを作成します。

    CREATE TABLE raster_table (ID INT PRIMARY KEY NOT NULL,name text,rast raster);
  3. OSS からテストデータとして画像をインポートします。ST_ImportFrom 関数を使用して、画像データをチャンクテーブル chunk_table にインポートします。詳細については、「ST_ImportFrom」をご参照ください。

    INSERT INTO raster_table VALUES (1, 'xxxx image', ST_ImportFrom('chunk_table','oss://<access_id>:<secret_key>@<Endpoint>/<bucket>/path_to/file.tif'));

ピラミッドを作成する

ピラミッドは、画像データをすばやくブラウジングするための基礎となります。新しくインポートされたデータについては、最初にピラミッドを作成することをお勧めします。GanosBase は、ピラミッドを作成するための ST_BuildPyramid 関数を提供します。詳細については、「ST_BuildPyramid」をご参照ください。

UPDATE raster_table SET rast = st_buildpyramid(raster_table,'chunk_table') WHERE name = 'xxxx image';

文の分析

ピラミッドを作成した後、GanosBase が提供する ST_AsImage 関数を使用して、データベースから指定された範囲の画像を取得できます。ST_AsImage 関数の基本的な構文を以下に示します。詳細については、「ST_AsImage」をご参照ください。

bytea ST_AsImage(raster raster_obj,
        box extent,
        integer pyramidLevel default 0,
        cstring bands default '',
        cstring format default 'PNG',
        cstring option default '');

ST_AsImage 関数のパラメーターは、静的パラメーターと動的パラメーターの 2 つのカテゴリに分類されます。

静的パラメーター

静的パラメーターは、一般的に操作によって変化せず、コードに固定して反復作業を削減できます。

  • bands: 取得するバンドのリスト。

    このパラメーターは、'0-3' または '0,1,2,3' の 2 つの形式で指定できますが、値は画像のバンドを超えることはできません。

  • format: 出力画像形式。

    PNG または JPEG を指定します。デフォルト値は PNG です。PNG 形式のデータ圧縮は JPEG 形式の非可逆圧縮ほど効果的ではないため、PNG 形式は転送中に多くの時間を消費します。透過性が不要な場合は、JPEG 形式を使用することをお勧めします。

  • option: JSON 文字列型の変換オプション。追加のレンダリングパラメーターを定義できます。

動的パラメーター

動的パラメーターは操作によって変化し、動的に生成する必要があります。

  • extent: 取得する画像範囲。

    同じ条件下では、表示範囲が大きいほど、データベースの処理時間が長くなり、返される画像サイズが大きくなり、全体的な応答時間が長くなります。そのため、転送効率を確保するために、ユーザーの視野内の画像のみを取得することをお勧めします。

  • pyramidLevel: 画像ピラミッドのレベル。

    使用するピラミッドレベルが高いほど、画像の定義が高くなり、画像サイズが大きくなります。そのため、転送効率を確保するために、最も適切なピラミッドレベルを選択してください。

画像バウンディングボックス範囲を取得する

GanosBase ST_Envelope 関数を使用して、画像のバウンディングボックス範囲を取得します。次に、ST_Transform 関数を使用して、画像を一般的に使用される座標系 (この場合は WGS 84 座標系) に変換します。最後に、フロントエンドで使用するための形式に変換します。

SELECT replace((box2d(st_transform(st_envelope(geom),4326)))::text,'BOX','') FROM rat_clip WHERE name = 'xxxx image';

ピラミッドレベルを取得する

GanosBase の ST_BestPyramidLevel 関数を使用して、特定の画像範囲内で最適なピラミッドレベルを計算します。ST_BestPyramidLevel 関数の基本的な構文を以下に示します。詳細については、「ST_BestPyramidLevel」をご参照ください。

integer ST_BestPyramidLevel(raster rast, 
                            Box extent, 
                            integer width, 
                            integer height)

次のパラメーターに注意してください。

  • extent: 視野内で取得する画像範囲。ST_AsImage 関数で使用される範囲と同じです。

  • width/height: 視野のピクセル幅と高さ。一般に、フロントエンドマップフレームのサイズです。

ST_BestPyramidLevel 関数と ST_AsImage 関数は、ジオメトリ型ではなくネイティブの box 型を使用します。変換が必要です。フロントエンドから返される bbox 配列は、次の手順でネイティブの box 型に変換する必要があります。

  1. 次の SQL 文を実行して、フロントエンドから返された文字列をジオメトリオブジェクトに構築します。

  2. ジオメトリオブジェクトをテキストオブジェクトに変換します。

  3. テキスト置換を使用して、テキストオブジェクトを box 型と互換性のあるテキストオブジェクトに変換します。

  4. テキストオブジェクトをネイティブの box オブジェクトに変換します。

SELECT  Replace(Replace(Replace(box2d(st_transform(st_setsrid(ST_Multi(ST_Union(st_point(-180,-58.077876),st_point(180,58.077876))),4326),st_srid(rast)))::text, 'BOX', '') , ',', '),('),' ',',')::box FROM rat_clip WHERE name = 'xxxx image';
説明

GanosBase 6.0 は、ラスター box 型とジオメトリ box2d 型の間の変換関数を提供します。上記のネストされた置換操作は、::box を呼び出して型変換を行うことで簡略化できます。

SELECT st_extent(rast)::box2d::box FROM rat_clip WHERE name = 'xxxx image';

ユースケース

ラスターデータは通常、Geoserver を介してサービスを公開することでブラウジングできます。GanosBase は、Geoserver ベースのラスターおよびベクターサービスの公開もサポートしています。詳細については、「マップサービス」をご参照ください。この例では、GIS Server ツールに依存しない、よりシンプルなローコード方式を紹介します。最小限の Python コードを使用すると、ユーザーがマップをドラッグおよびズームしたときに指定された画像データの表示を動的に更新するマップアプリケーションをすばやく構築できます。このアプリケーションは、業務システムと簡単に統合できます。

アーキテクチャ

image

バックエンドコード

コードの簡潔さとロジックの説明のために、Python をバックエンド言語として使用します。Python 用の Flask フレームワークを Web フレームワークとして使用します。Python ベースで開発された Psycopg2 をデータベース接続フレームワークとして使用します。 pip install psycopg2 コマンドを実行することで、Psycopg2 をインストールできます。この例では、シンプルなマップサービスがバックエンドに作成されます。ピラミッドを自動的に構築し、指定された範囲内の画像データを返すことでフロントエンドリクエストに応答できます。次のコードを Raster.py という名前のファイルに保存し、python Raster.py コマンドを実行してサービスを開始します。

## -*- coding: utf-8 -*-
## @File : Raster.py

# 必要なライブラリをインポートします
import json
from flask import Flask, request, Response, send_from_directory
import binascii
import psycopg2

# 接続パラメーターを設定します
CONNECTION =  "dbname=<database_name> user=<user_name> password=<user_password> host=<host> port=<port>"

# 画像アドレスを設定します
OSS_RASTER = "oss://<access_id>:<secret_key>@<Endpoint>/<bucket>/path_to/file.tif"

# ラスター名を設定します
RASTER_NAME = "xxxx image"

# チャンクテーブル名を設定します
CHUNK_TABLE = "chunk_table"

# プライマリテーブル名を設定します
RASTER_TABLE = "raster_table"

# フィールド名を設定します
RASTER_COLUMN = "rast"

# デフォルトのレンダリングパラメーターを設定します
DEFAULT_CONFIG = {
    "strength": "ratio",
    "quality": 70
}


class RasterViewer:
    def __init__(self):
        # データベースに接続します
        self.pg_connection = psycopg2.connect(CONNECTION)
        self.column_name = RASTER_COLUMN
        self.table_name = RASTER_TABLE
        self._make_table()  # テーブルを作成します
        self._import_raster(OSS_RASTER) # ラスターをインポートします

    def poll_query(self, query: str):
        # クエリを実行し、結果を返します
        pg_cursor = self.pg_connection.cursor()
        pg_cursor.execute(query)
        record = pg_cursor.fetchone()
        self.pg_connection.commit()
        pg_cursor.close()
        if record is not None:
            return record[0]

    def poll_command(self, query: str):
        # コマンドを実行します
        pg_cursor = self.pg_connection.cursor()
        pg_cursor.execute(query)
        self.pg_connection.commit()
        pg_cursor.close()

    def _make_table(self):
        # テーブルを作成します
        sql = f"create table if not exists {self.table_name} (ID INT PRIMARY KEY NOT NULL,name text, {self.column_name} raster);"
        self.poll_command(sql)

    def _import_raster(self, raster):
        # ラスターをインポートし、ピラミッドを構築し、統計情報を収集します
        sql = f"insert into {self.table_name} values (1, '{RASTER_NAME}', ST_ComputeStatistics(st_buildpyramid(ST_ImportFrom('{CHUNK_TABLE}','{raster}'),'{CHUNK_TABLE}'))) on conflict (id) do nothing;;"
        self.poll_command(sql)
        self.identify = f" name= '{RASTER_NAME}'"

    def get_extent(self) -> list:
        """画像範囲を取得します"""
        import re
        # 画像範囲を取得します
        sql = f"select replace((box2d(st_transform(st_envelope({self.column_name}),4326)))::text,'BOX','') from {self.table_name} where {self.identify}"
        result = self.poll_query(sql)

        # フロントエンドで簡単に認識できる形式に変換します
        bbox = [float(x) for x in re.split(
                '\(|,|\s|\)', result) if x != '']
        return bbox

    def get_jpeg(self, bbox: list, width: int, height: int) -> bytes:
        """
        指定された場所の画像を取得します
        :param bbox: 指定された場所のバウンディングボックス
        :param width: 視野コントロールの幅
        :param height: 視野コントロールの高さ
        """

        # バンドとレンダリングパラメーターを指定します
        bands = "0-2"
        options = json.dumps(DEFAULT_CONFIG)

        # 範囲を取得します
        boxSQl = f"Replace(Replace(Replace(box2d(st_transform(st_setsrid(ST_Multi(ST_Union(st_point({bbox[0]},{bbox[1]}),st_point({bbox[2]},{bbox[3]}))),4326),st_srid({self.column_name})))::text, 'BOX', ''), ',', '),('),' ',',')::box"
        # 指定された範囲の画像を取得します
        sql = f"select encode(ST_AsImage({self.column_name},{boxSQl} ,ST_BestPyramidLevel({self.column_name},{boxSQl},{width},{height}),'{bands}','jpeg','{options}'),'hex')  from {self.table_name} where {self.identify}"
        result = self.poll_query(sql)
        result = binascii.a2b_hex(result)
        return result


# RasterViewer オブジェクトを作成します
rasterViewer = RasterViewer()

# Flask アプリケーションを作成します
app = Flask(__name__)


@app.route('/raster/image')
def raster_image():
    # 画像を取得するためのリクエストを処理します
    bbox = request.args['bbox'].split(',')
    width = int(request.args['width'])
    height = int(request.args['height'])
    return Response(
        response=rasterViewer.get_jpeg(bbox, width, height),
        mimetype="image/jpeg"
    )


@app.route('/raster/extent')
def raster_extent():
    # 画像範囲を取得するためのリクエストを処理します
    return Response(
        response=json.dumps(rasterViewer.get_extent()),
        mimetype="application/json",
    )


@app.route('/raster')
def raster_demo():
    """フロントエンドページのプロキシ"""
    return send_from_directory("./", "Raster.html")


if __name__ == "__main__":
    # アプリケーションを実行します
    app.run(port=5000, threaded=True)

画像データの場合、バックエンドコードは主に次の操作を実行します。

  • テストテーブルの作成、データのインポート、ピラミッドの構築、統計情報の収集など、データを初期化します。

  • フロントエンドマップが画像の場所にすばやくジャンプできるように、画像を特定します。

  • 画像形式で画像を取得します。

    コードは、画像を取得する前に現在の画像のメタデータを取得し、この画像が 3 つのバンドで構成されていることを理解します。バンドの数は、ST_NumBands 関数を使用して動的に取得することもできます。

    コードは、フロントエンドから返された範囲情報と視野コントロールのピクセル幅と高さに基づいて画像範囲を決定します。

    psycopg2 ライブラリを使用する場合、16 進数データを送信する方が効率的です。他のフレームワークまたは言語を使用する場合は、バイナリデータを直接使用します。

この例では Python を使用しています。他の言語を使用する場合は、同じロジックでサービスを開発します。

フロントエンドコード

この例では、Mapbox をフロントエンドマップフレームワークとして使用し、フロントエンド空間ライブラリ Turf を導入して、ユーザーがマップをドラッグまたはズームした後の現在の視野と画像の交差部分を計算します。次に、領域のより鮮明な画像がサーバーからリクエストされ、マップ操作で画像が更新されます。バックエンドコードと同じファイルディレクトリに、Raster.html という名前のファイルを作成し、ファイルに次のコードを記述します。バックエンドサービスが開始された後、http://localhost:5000/raster にアクセスすることでマップにアクセスできます。

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <title>ラスタービューアー</title>
  <link href="https://cdn.bootcdn.net/ajax/libs/mapbox-gl/1.13.0/mapbox-gl.min.css" rel="stylesheet" />
</head>
<script src="https://cdn.bootcdn.net/ajax/libs/mapbox-gl/1.13.0/mapbox-gl.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/Turf.js/5.1.6/turf.min.js"></script>

<body>
  <div id="map" style="height: 100vh" />
  <script>

    // マップコントロールを初期化します
    const map = new mapboxgl.Map({
      container: "map",
      style: { version: 8, layers: [], sources: {} },
    });

    class Extent {
      constructor(geojson) {
        // デフォルトで geojson 形式を使用します
        this._extent = geojson;
      }

      static FromBBox(bbox) {
        // bbox 形式オブジェクトから Extent オブジェクトを生成します
        return new Extent(turf.bboxPolygon(bbox));
      }

      static FromBounds(bounds) {
        // Mapbox の bounds から extent オブジェクトを生成します
        const bbox = [
          bounds._sw.lng,
          bounds._sw.lat,
          bounds._ne.lng,
          bounds._ne.lat,
        ];
        return Extent.FromBBox(bbox);
      }

      intersect(another) {
        // 交差領域を決定します
        const intersect = turf.intersect(this._extent, another._extent);
        return intersect ? new Extent(intersect) : null;
      }

      toQuery() {
        // クエリ形式に変換します
        return turf.bbox(this._extent).join(",");
      }

      toBBox() {
        // bbox 形式に変換します
        return turf.bbox(this._extent);
      }

      toMapboxCoordinates() {
        // Mapbox 座標形式に変換します
        const bbox = this.toBBox();
        const coordinates = [
          [bbox[0], bbox[3]],
          [bbox[2], bbox[3]],
          [bbox[2], bbox[1]],
          [bbox[0], bbox[1]],
        ];
        return coordinates;
      }
    }

    map.on("load", async () => {
      map.resize();

      const location = window.location.href;

      // クエリ文を構築します
      const getUrl = (extent) => {
        const params = {
          bbox: extent.toQuery(),
          height: map.getCanvas().height,
          width: map.getCanvas().width,
        };
        // リクエストを結合します
        const url = `${location}/image?${Object.keys(params)
          .map((key) => `${key}=${params[key]}`)
          .join("&")}`;
        return url;
      };

      // 画像範囲をクエリします
      const result = await axios.get(`${location}/extent`);
      const extent = Extent.FromBBox(result.data);
      const coordinates = extent.toMapboxCoordinates();

      // データソースを追加します
      map.addSource("raster_source", {
        type: "image",
        url: getUrl(extent),
        coordinates,
      });

      // レイヤーを追加します
      // Mapbox の image タイプレイヤーを使用して、画像を指定された場所にアタッチしてマップに表示します
      map.addLayer({
        id: "raster_layer",
        paint: { "raster-fade-duration": 300 },
        type: "raster",
        layout: { visibility: "visible" },
        source: "raster_source",
      });

      // 画像の場所にジャンプします
      map.fitBounds(extent.toBBox());

      // 更新メソッドをバインドします
      map.once("moveend", () => {
        const updateRaster = () => {
          const _extent = Extent.FromBounds(map.getBounds());
          let intersect;
          // 視野にグラフィックが存在しない場合は、再度リクエストしません
          if (!_extent || !(intersect = extent.intersect(_extent))) return;

          // グラフィックを更新します
          map.getSource("raster_source").updateImage({
            url: getUrl(intersect),
            coordinates: intersect.toMapboxCoordinates(),
          });
        };

        // 無効なリクエストを減らすためにデバウンスを追加します
        const _updateRaster = _.debounce(updateRaster, 200);
        map.on("zoomend", _updateRaster);
        map.on("moveend", _updateRaster);
      });
    });
  </script>
</body>

</html>
image

画像取得の現在の操作は標準のマッププロトコル操作ではないため、画像の更新に関連する機能を手動で実装する必要があります。重要な問題は次のとおりです。ユーザーの視野内の画像範囲をどのように決定するか。基本的なロジックは次のとおりです。

  • 画像の空間範囲を取得します。

  • ユーザーの視野の空間範囲を取得します。

  • 2 つの空間範囲の交差部分を取得します。これが想定される画像範囲です。

フロントエンドで空間識別を実現するために、Turf フレームワークが導入され、フロントエンドで簡単な計算を実行し、不要なリクエストを削減します。空間識別と形式変換を容易にするために、次の機能を提供する補助クラス Extent が実装されています。

  • 形式変換:

    • BBox <=> Geojson

    • Mapbox 座標 <=> Geojson

    • Geojson => クエリ

    • Bounds => Geojson

  • Turf によって提供される空間識別メソッドのカプセル化。

結果

概要

3

pgAdmin に画像ブラウジングを統合する

画像ブラウジング機能は、PolarDB と互換性のあるデータベースクライアント pgAdmin に統合できます。データベース内の画像データをすばやくブラウジングおよび評価できるため、データ管理エクスペリエンスが向上します。

まとめ

GanosBase Raster 関数を使用すると、データベースから画像データを取得し、ローコード方式でリモートセンシング画像をインタラクティブにブラウジングできるマップアプリケーションを実装できます。GanosBase を使用してリモートセンシング画像を管理することで、管理コストを削減できます。GanosBase Raster 拡張機能と少量のコードを使用すると、複雑なサードパーティツールに依存することなく、データベース内の画像データをブラウジングできます。これにより、データ管理エクスペリエンスが大幅に向上します。