多次元配列に特化したユニバーサル・データエンジン「tileDB」でLiDARデータを3Dグラフ化してみた

本記事では、ユニバーサル・データエンジンを称するtileDBの紹介とデモを実施していきます。

こういうだいぶ尖った製品にめちゃ惹かれるのは私だけでしょうか?

tileDBについて

tileDBは、2017年にマサチューセッツ工科大学(MIT)とIntel Labsのプロジェクトからスピンアウトしたベンチャー企業です。投資しているVCの数も多い注目の企業で、特に日本のNTT Docomo Venturesも投資していたことには驚きました。

NTT DOCOMO Ventures Invests in TileDB, Inc.Which Offers a Universal Data Management Platform | NTT DOCOMO Ventures, Inc.

tileDBは、行志向データやフラットファイル、SQL-Onlyのサービスといった様々なデータ形式の差異を乗り越え、全て多次元配列のデータとして扱うことで、シームレスなデータハンドリングを実現してくれるサービスです。データサイエンスティストは日々、様々な形式のデータを使ってモデル作成やデータプロダクトを開発していますが、その中でもかなり時間と労力がかかるデータハンドリングの生産性向上に焦点を当てています。

tileDBはオープンソースでも提供している他、クラウド版のアカウントを開設すると$10分の無料クレジットも受け取ることができます。本記事ではクラウド版を使用して、tileDBの機能やチュートリアルを試していきます。

TileDB-Inc/TileDB: The Universal Storage Engine

アセット管理の機能

公式HPよりGet startedをクリックします。

tileDBでは、GitHubかGoogleアカウントでもサインアップが可能です。今回はGoogle認証を選択します。

認証後、ホーム画面にランディングします。ホーム画面はアセットやアクティビティのサマリを確認することができます。

Exploreの画面では、パブリックに公開されているアセットを探索することができます。アセットはArraysNotebooksUDFsDashboardsML ModelsFilesの6種類です。自身のアセットに関しては、左メニューのタブで同様に確認することができます。

Arraysは、tileDBで一番特徴的な多次元配列を格納できるデータベース機能です。試しにboulderの中身を確認してみます。

ArraysのOverview画面では、tileDBでデータをインポートする用のURLや、容量やデータ型を確認することができます。Descriptionもうまく活用して、そのArrayに関する情報をドキュメント化できると運用もうまく回りそうですね。

Schemaタブでは、Arrayの次元情報や保持しているAttributeの一覧を確認することができます。

MetadataはいわゆるTagのような機能で、Key-Value形式でメタデータを付与することができます。

続いてNotebooksのタブを見ていきます。tileDBではマネージドのJupyter Notebookインスタンスをホストして開発することができます。tutorials_lidar_quickstartをクリックしてみます。

Notebooksも同様に専用のURIが発行されていたり、Descriptionが記述できたりします。

PreviewをクリックするとNotebookの内容を閲覧することができます。ちなみにtutorials_lidar_quickstartは次節のチュートリアルで試していきます。

UDFsはPython関数を登録しておくことができる機能です。パラメータを指定して実行するとリターン値が返ってくるFaaSのように使用することができます。

サンプルとしては以下のような関数が用意されていました。

Dashboardsは、分析結果を閲覧しやすいように整理した画面です。LiDAR-Dashboardをクリックしてみます。

LiDARのデータが3Dで描画されたダッシュボードが表示されました。3Dグラフは、マウスで図を回転させながら見ることも可能です。

ML Modelsには、Python製の機械学習モデルをデプロイすることができます。

Pythonのバージョンやフレームワークの情報も付与された形で保存できるみたいです。

Filesには基本どんなファイルでもアップロードできるみたいです。

サンプルではPDFがアップロードされていました。

以上のように、多次元データ分析に必要なアセットをアップロードして、Jupyter Notebookで開発していくのがtileDBの主な使い方になります。tileDBは別環境からAPIアクセスも可能なので、ローカル端末のJupyter Labsなどお好みの環境から使用することもできます。

ユーザー管理の機能

ユーザーはOrganizationという単位で管理されています。Create organizationをクリックして、試しに1つ作成してみます。

任意の組織名を入力します。

作成完了後Overviewの画面に入ります。組織全体のサマリや概要を確認できるページですね。

Membersはユーザーの招待や管理ができるページです。

Cloud credentialsにAWSのシークレットキーを登録することで、tileDBから任意のAWSリソースへアクセスできるようになります。

左メニュー末尾のTasksでは、ユーザーのアクティビティを確認することができます。使用した金額まで表示されている点が、ユーザーにコスト感覚への意識を根付けられそうですね。

UIから把握できる機能は大方以上です!次節より、Jupyter Notebookのチュートリアルを進めていきます。

LiDARデータをグラフ化する

サンプルのNotebookはいくつか用意されていますが、多次元分析を試してみたいので今回はLiDARデータのグラフ化を実施していきます。LiDAR Quickstart - TileDB Cloud Docspreviewをクリックすると、下記のNotebookが表示されます。Launchをクリックするとインスタンスが起動します。

最初に、PDALというライブラリが提供しているサンプルデータを取得します。

$ wget https://github.com/PDAL/data/blob/master/workshop/autzen.laz?raw=true -O autzen.laz
$ pdal info autzen.laz | jq .
{
  "file_size": 56350988,
  "filename": "autzen.laz",
  "now": "2021-12-16T09:08:59+0000",
  "pdal_version": "2.3.0 (git-version: Release)",
  "reader": "readers.las",
  "stats": {
    "bbox": {
      "EPSG:4326": {
        "bbox": {
          "maxx": -123.0619599,
          "maxy": 44.06278031,
          "maxz": 615.26,
          "minx": -123.0755422,
          "miny": 44.04971882,
          "minz": 406.14
        },
        "boundary": {
          "type": "Polygon",
          "coordinates": [
            [
              [
                -123.07498674355058,
                ...

続いて、Pythonコードでアウトプット先の指定と、パイプライン用の設定ファイルを作成し、pdal pipelineコマンドで実行させます。

import tiledb

output_array = '~/autzen_tiledb'

import json
import shutil


# Clean up any previous runs
try:
    shutil.rmtree(output_array)
except:
    pass

initial_pipeline = [
   {
      'type': 'readers.las',
      'filename': "autzen.laz"
   },
   {
       'type' : 'filters.stats'
   },
   {
      'type': 'writers.tiledb',
      'array_name': f"{output_array}",
      'chunk_size': 10000000
   }
]

with open('pipeline.json', 'w') as f:
    json.dump(initial_pipeline, f)
$ pdal pipeline -i pipeline.json --nostream

出力された配列をtiledb.openメソッドで展開してみます。3次元の13属性のデータを持った配列となっていますね。

with tiledb.open(output_array) as arr:
    print(f"Non-empty domain: {arr.nonempty_domain()}")
    print(arr.schema)
Non-empty domain: ((635577.79, 639003.73), (848882.15, 853537.66), (406.14, 615.26))
ArraySchema(
  domain=Domain(*[
    Dim(name='X', domain=(635576.79, 639004.73), tile='None', dtype='float64'),
    Dim(name='Y', domain=(848881.15, 853538.66), tile='None', dtype='float64'),
    Dim(name='Z', domain=(405.14, 616.26), tile='None', dtype='float64'),
  ]),
  attrs=[
    Attr(name='Intensity', dtype='uint16', var=False, nullable=False, filters=FilterList([Bzip2Filter(level=5), ])),
    Attr(name='ReturnNumber', dtype='uint8', var=False, nullable=False, filters=FilterList([ZstdFilter(level=7), ])),
    Attr(name='NumberOfReturns', dtype='uint8', var=False, nullable=False, filters=FilterList([ZstdFilter(level=7), ])),
    Attr(name='ScanDirectionFlag', dtype='uint8', var=False, nullable=False, filters=FilterList([Bzip2Filter(level=5), ])),
    Attr(name='EdgeOfFlightLine', dtype='uint8', var=False, nullable=False, filters=FilterList([Bzip2Filter(level=5), ])),
    Attr(name='Classification', dtype='uint8', var=False, nullable=False, filters=FilterList([GzipFilter(level=9), ])),
    Attr(name='ScanAngleRank', dtype='float32', var=False, nullable=False, filters=FilterList([Bzip2Filter(level=5), ])),
    Attr(name='UserData', dtype='uint8', var=False, nullable=False, filters=FilterList([GzipFilter(level=9), ])),
    Attr(name='PointSourceId', dtype='uint16', var=False, nullable=False, filters=FilterList([Bzip2Filter(level=-1), ])),
    Attr(name='GpsTime', dtype='float64', var=False, nullable=False, filters=FilterList([ZstdFilter(level=7), ])),
    Attr(name='Red', dtype='uint16', var=False, nullable=False, filters=FilterList([ZstdFilter(level=7), ])),
    Attr(name='Green', dtype='uint16', var=False, nullable=False, filters=FilterList([ZstdFilter(level=7), ])),
    Attr(name='Blue', dtype='uint16', var=False, nullable=False, filters=FilterList([ZstdFilter(level=7), ])),
  ],
  cell_order='hilbert',
  tile_order=None,
  capacity=100000,
  sparse=True,
  allows_duplicates=True,
  coords_filters=FilterList([ZstdFilter(level=7)]),
)

配列をPandasのデータフレームに格納します。この際の処理がゼロコピーで行われているようで、高いパフォーマンスでハンドリングできるのがtileDBの特徴です。

with tiledb.open(output_array) as arr:
    df = arr.df[636800:637800, 851000:853000, 406.14:615.26]

格納後、matplotlibで描画を行います。

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np

def plot(pts):
    rgbs = np.array(list(zip(pts['Red'], pts['Green'], pts['Blue']))) / 255.0

    fig = plt.figure(figsize=(10, 10))
    ax = fig.add_subplot(111, projection='3d')
    ax.ticklabel_format(useOffset=False)

    ax.scatter(pts['X'], pts['Y'], pts['Z'], c=rgbs)

    ax.set_xlabel('x', fontsize=20, labelpad=20)
    ax.set_ylabel('y', fontsize=20, labelpad=35)
    ax.set_zlabel('z', fontsize=20, labelpad=25)
    ax.set_title('Autzen', fontsize=20, pad=20)
    ax.view_init(60, 96)
    ax.tick_params(axis='y', pad=20)
    ax.tick_params(axis='z', pad=10)
    plt.show()

plot(df)

以下のように描画されました。スタジアムっぽい形になってます。

動的なグラフとして出力するために、BabylonJSも使ってみます。

import pybabylonjs

data = {
    'X': df['X'][::10],
    'Y': df['Y'][::10],
    'Z': df['Z'][::10],
    'Red': df['Red'][::10] / 255.0,
    'Green': df['Green'][::10] / 255.0,
    'Blue': df['Blue'][::10] / 255.0
}

babylon = pybabylonjs.BabylonJS()
babylon.value = data
babylon.z_scale = .2

babylon

下の画像は静止画ですが、Jupyter上ではドラッグでグリグリ回転できるようになっています。こうすると立体感が把握しやすいですね。

tileDBはMariaDBと連携されているため、SQLでデータを操作することも可能です。

import tiledb, tiledb.sql, pandas

db = tiledb.sql.connect()
res = pandas.read_sql(sql=
                f"""
                select COUNT(*) from `{output_array}` 
                WHERE X >= 636800 AND X <= 637800 AND
                      Y >= 851000 AND Y <= 853000 AND
                      Z >= 406.14 AND Z <= 615.26                      
                """
                , con=db)

res
COUNT(*)
1311299

デモの実施は以上です!

所感

何も考えずに捌こうとすると挙動が重くなりがちな多次元配列ですが、その解決を目指したエンジンだという特徴がひしひし伝わってきました。Jupyterで多次元データをよく使う方で、処理スピードの遅さにストレスを感じている方は試してみる価値のある製品だと思います。

本アドベントカレンダーでは、今話題のデータ関連SaaSを取り上げていきますので、引き続き乞うご期待ください!