多次元配列に特化したユニバーサル・データエンジン「tileDB」でLiDARデータを3Dグラフ化してみた
本記事では、ユニバーサル・データエンジンを称するtileDBの紹介とデモを実施していきます。
こういうだいぶ尖った製品にめちゃ惹かれるのは私だけでしょうか?
tileDBについて
tileDBは、2017年にマサチューセッツ工科大学(MIT)とIntel Labsのプロジェクトからスピンアウトしたベンチャー企業です。投資しているVCの数も多い注目の企業で、特に日本のNTT Docomo Venturesも投資していたことには驚きました。
tileDBは、行志向データやフラットファイル、SQL-Onlyのサービスといった様々なデータ形式の差異を乗り越え、全て多次元配列のデータとして扱うことで、シームレスなデータハンドリングを実現してくれるサービスです。データサイエンスティストは日々、様々な形式のデータを使ってモデル作成やデータプロダクトを開発していますが、その中でもかなり時間と労力がかかるデータハンドリングの生産性向上に焦点を当てています。
tileDBはオープンソースでも提供している他、クラウド版のアカウントを開設すると$10分の無料クレジットも受け取ることができます。本記事ではクラウド版を使用して、tileDBの機能やチュートリアルを試していきます。
TileDB-Inc/TileDB: The Universal Storage Engine
アセット管理の機能
公式HPよりGet started
をクリックします。
tileDBでは、GitHubかGoogleアカウントでもサインアップが可能です。今回はGoogle認証を選択します。
認証後、ホーム画面にランディングします。ホーム画面はアセットやアクティビティのサマリを確認することができます。
Explore
の画面では、パブリックに公開されているアセットを探索することができます。アセットはArrays
、Notebooks
、UDFs
、Dashboards
、ML Models
、Files
の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 Docsのpreview
をクリックすると、下記の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を取り上げていきますので、引き続き乞うご期待ください!