AITRIOSのEdgeApplicationで採用されているFlatbuffersで遊んでみた

こんにちは。
ソニーセミコンダクタソリューションズの細井です。

今回はAITRIOSのエッジとクラウドを繋ぐインタフェースとして機能している[FlatBuffers](https://github.com/google/flatbuffers)について遊んでみましたので解説させていただきます。

**この記事は2024/8/7時点でDeveloper Siteに公開されているDeveloper Edition v1.8.0の情報を元に書いています。**

# FlatBuffersとは
Google製の2014年に公開されたバイナリシリアライズフォーマットです。
2014年に公開されてますが、意外と今年も更新は続けられてます。

下記が公式ドキュメントに記載されているFlatBuffersの概要です。

> - **Access to serialized data without parsing/unpacking** - What sets FlatBuffers apart is that it represents hierarchical data in a flat binary buffer in such a way that it can still be accessed directly without parsing/unpacking, while also still supporting data structure evolution (forwards/backwards compatibility).
> - **Memory efficiency and speed** - The only memory needed to access your data is that of the buffer. It requires 0 additional allocations (in C++, other languages may vary). FlatBuffers is also very suitable for use with mmap (or streaming), requiring only part of the buffer to be in memory. Access is close to the speed of raw struct access with only one extra indirection (a kind of vtable) to allow for format evolution and optional fields. It is aimed at projects where spending time and space (many memory allocations) to be able to access or construct serialized data is undesirable, such as in games or any other performance sensitive applications. See the benchmarks for details.
> - **Flexible** - Optional fields means not only do you get great forwards and backwards compatibility (increasingly important for long-lived games: don't have to update all data with each new version!). It also means you have a lot of choice in what data you write and what data you don't, and how you design data structures.
> - **Tiny code footprint** - Small amounts of generated code, and just a single small header as the minimum dependency, which is very easy to integrate. Again, see the benchmark section for details.
> - **Strongly typed** - Errors happen at compile time rather than manually having to write repetitive and error prone run-time checks. Useful code can be generated for you.
> - **Convenient to use** - Generated C++ code allows for terse access & construction code. Then there's optional functionality for parsing schemas and JSON-like text representations at runtime efficiently if needed (faster and more memory efficient than other JSON parsers).Java, Kotlin and Go code supports object-reuse. C# has efficient struct based accessors.
> - **Cross platform code with no dependencies** - C++ code will work with any recent gcc/clang and VS2010. Comes with build files for the tests & samples (Android .mk files, and cmake for all other platforms).

色々とメリットが語られていますが、AITRIOSとしてFlatBuffersを使う旨味は下記だと思っています。

- **Access to serialized data without parsing/unpacking**
- **Cross platform code with no dependencies**

## Access to serialized data without parsing/unpacking
FlatBuffersは「ゼロコピー」でシリアライズをサポートしているため、シリアル化されたデータにアクセスするため最初のメモリの別部分にコピーする必要はありません。
つまりどういうことかというとデータアクセスをする際に、JSONやCSVなどと比較してはるかに高速になるようです。
ちょっと試しに下記のコードで計測をしてみました。

```python
import json
import time
from data_deserializer.object_detection import object_detection_top, bounding_box, bounding_box_2d

json_string_data = '{"1": {"C": 0, "P": 0.99609375, "X": 172, "Y": 88, "x": 254, "y": 195}, "2": {"C": 1, "P": 0.8984375, "X": 99, "Y": 85, "x": 173, "y": 273} }'
serialize_data_enc = "DAAAAAAABgAKAAQABgAAAAwAAAAAAAYACAAEAAYAAAAEAAAAAgAAAEgAAAAQAAAADAAUAAgABwAMABAADAAAAAAAAAEBAAAACAAAAAAAZj/Q////YwAAAFUAAACtAAAAEQEAAAwAEAAAAAcACAAMAAwAAAAAAAABFAAAAAAAfz8MABQABAAIAAwAEAAMAAAArAAAAFgAAAD+AAAAwwAAAA=="
serialize_data = base64.b64decode(serialize_data_enc)

elapsed_time = 0
for i in range(100):
    start_time = time.perf_counter()
    json_data = json.loads(json_string_data)
    class_id = json_data["1"]["C"]
    score = json_data["1"]["P"]
    end_time = time.perf_counter()
    elapsed_time = elapsed_time + (end_time - start_time)

elapsed_time_ave = elapsed_time / 100
print("json parse time:", elapsed_time_ave)

elapsed_time = 0
for i in range(100):
    start_time = time.perf_counter()
    fb_data = object_detection_top.ObjectDetectionTop.GetRootAsObjectDetectionTop(
        serialize_data, 0
    ).Perception()
    class_id = fb_data.ObjectDetectionList(0).ClassId()
    score = fb_data.ObjectDetectionList(0).Score()
    end_time = time.perf_counter()
    elapsed_time = elapsed_time + (end_time - start_time)

elapsed_time_ave = elapsed_time / 100
print("flatbuffers time:", elapsed_time_ave)
```

結果はなんとJSON Parseのほうが早い・・・！

```
json parse time: 4.505019996940973e-06
flatbuffers time: 2.993138000874751e-05
```

調べてみたところ、Pythonのデシリアライズにおいてはオーバーヘッドが乗るのでFlatBuffersよりもJSON側の方が早いそうです。
https://stackoverflow.com/questions/56731661/how-to-use-flatbuffers-in-python-the-right-way

別の記事ではJava、C++におけるベンチマークがありました。こちらではFlatBuffersの方が優位という記載が多く見受けられました。
現在販売されているエッジデバイスにおけるエッジアプリケーションはC＋＋で実装されている点からもFlatBuffersの恩恵を受けられることがわかります。

https://flatbuffers.dev/flatbuffers_benchmarks.html

![Flatbuffers_benchmark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724589/142438a3-57af-78c3-d173-9df0c33f6a49.png)

https://codeburst.io/json-vs-protocol-buffers-vs-flatbuffers-a4247f8bda6f

## Cross platform code with no dependencies
言語間の相互運用性という観点でFlatBuffersは、複数のプログラミング言語で利用することができます。C++を始めとするさまざまな言語でサポートされており、異なるプラットフォームやアプリケーション間でデータを共有する際に便利です。（これが一番AITRIOSにとっては便利！！！だと思っています。）データの共有にはSchemaというファイルを用います。また、データのバージョン管理も容易に行うことができます。

# AITRIOSにおけるFlatBuffersの役割
AITRIOSでは下記の図のようにエッジデバイスにデプロイするアプリケーションと、クラウドにデプロイされるアプリケーションは別のユーザによって開発されることが多いように思えます。
常に、エッジアプリケーションベンダーとクラウドアプリケーションベンダーが密に会話をすれば不具合なくシステム結合が可能ですが、コミュニケーションコストの高さや開発効率の低下という課題があるように思えます。

![FlatBuffers_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724589/974aa597-8184-ac96-115f-52bc100ba849.png)

またFlatBuffersとは異なる複数プログラミング言語対応していないシリアライザなどで開発を進めてしまうことによる課題として、エッジアプリケーション1とクラウドアプリケーション1は問題なく結合できたが、クラウドアプリケーション2では再度IFの調整が最初から必要という話も出てきてしまいます。

![Flatbuffers_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724589/e3f50003-9569-c623-7f13-68354b5ebec2.png)

開発者としてはできる限り楽にIFを定義して、楽に共有したいわけです。
かつエッジアプリケーションとクラウドアプリケーションはできる限り疎結合にしたい。
FlatBuffersを使うと楽にIF定義と共有、そしてそのIFを見ながら楽に実装ができてしまうのです！

興味が少し出た人は最後まで記事を読んでいただけると幸いです。

# FlatBuffersで遊んでみた

今回も[AITRIOSのConsole REST APIを利用してメタデータを取得してみた](https://qiita.com/SSS-izumuhosoi/items/51008a971ce8041392a4)と同じように[Tutorial Checkdata: Sample Application on AITRIOS](https://github.com/SonySemiconductorSolutions/aitrios-sample-application-check-data-tool-python)のCodespaces環境を利用しました。

console_access_settings.yamlを設定したあとは
Jupyter Notebook環境におけるStep4までは、Jupyter Notebookのコードに沿って実行します。

そのあと、CodeSpaces上の左上の+コードを押して、新しいpythonセルを追加します。
このあと実際にpythonコードのデシリアライズをしてみようと思います。

まずGet Inference DataのAPIはbase64でエンコードされているのでデコードするためにbase64ライブラリをImportします。
次に、FlatBuffersのヘッダーをImportします。

ヘッダーってなんぞや！！という声が聞こえてきた気がするのでヘッダーの作り方についても説明します。
ここでいうヘッダーはユーザーが定義したIF（Schema）を元にデータのシリアライズとデシリアライズを効率的に行うために用いられるコンポーネントと思っていただければ良いかなと思います。

## FlatBuffersのヘッダーの作り方

基本的なユースケースだと、エッジアプリケーションの開発者からSchemaファイルが提供されます。
サンプルとして今回はDeveloper Siteで公開されているObject Detecionのサンプルを例に説明します。[Github](https://github.com/SonySemiconductorSolutions/aitrios-sdk-vision-sensing-app/blob/main/tutorials/4_prepare_application/1_develop/sdk/schema/objectdetection.fbs)で公開されている`objectdetecion.fbs`を見てみます。

下記のようなファイルになっています。

```objectdetection.fbs
// version 1.0.0 

/*
 * Copyright 2023 Sony Semiconductor Solutions Corp. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

namespace SmartCamera;

table BoundingBox2d {
  left:int;
  top:int;
  right:int;
  bottom:int;
}

union BoundingBox {
  BoundingBox2d,
}

table GeneralObject {
  class_id:uint;
  bounding_box:BoundingBox;
  score:float;
}

table ObjectDetectionData {
  object_detection_list:[GeneralObject];
}

table ObjectDetectionTop {
  perception:ObjectDetectionData;
}

root_type ObjectDetectionTop;
```

なんとなーくみていると`ObjectDetectionTop`から始まり構造体のような形になっていることが確認できると思います。
そうなんです。Schemaファイルではエッジアプリケーションの出力データの構造を定義することができます。

さらに、.fbsファイルからflatbuffersのコマンドを打つことで、さまざまな言語に対応したヘッダーファイルを作成することもできます。

Codespaces上のターミナルで、devcontainer上にFlatBuffersをインストールします。

```
sudo apt update
sudo apt -y install build-essential cmake git

export FLATBUFFERS_BUILD_PATH=.flatbuffers
git clone --depth 1 https://github.com/google/flatbuffers.git "${FLATBUFFERS_BUILD_PATH}"

cmake -S "${FLATBUFFERS_BUILD_PATH}" -B "${FLATBUFFERS_BUILD_PATH}" -G 'Unix Makefiles' -DFLATBUFFERS_BUILD_TEST=OFF -DFLATBUFFERS_BUILD_FLATLIB=OFF -DFLATBUFFERS_BUILD_FLATHASH=OFF


sudo make install -C "${FLATBUFFERS_BUILD_PATH}"
flatc -–version
```

インストールが無事完了するとversionが下記のように表示されるはずです。

```
@izumuhosoi ➜ /workspace (main) $ flatc --version
flatc version 24.3.25
```

では実際にヘッダーを作ってみましょう。
使い方は簡単です。

.fbsファイルの置いてある場所で下記のコマンドを実行するだけです。
私は、workspace/jupyter_notebook以下にfbs_testディレクトリを作成し実行してみました。

```
@izumuhosoi ➜ /workspace (main) $ cd jupyter_notebook/fbs_test/
@izumuhosoi ➜ /workspace/jupyter_notebook/fbs_test (main) $ flatc --python *fbs
```

`fbs_test`ディレクトリ以下に`objectdetection.fbs`内のnamespaceで指定された`SmartCamera`ディレクトリができていることが確認できました。

中を見てみるとそれぞれヘッダーができていることが確認できますね。

![Flatbuffers_python_header.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724589/fcf62edf-e7b1-2a37-5682-51761c844051.png)

他にも`flatc --cpp *fbs`とすることでC++のヘッダーファイルも簡単に作れちゃいます。
他にも他にも`flatc --python --gen-onefile *fbs`とすることで、SmartCamera以下にバラバラにできていたファイルを1ファイルとして生成しちゃうこともできます。

こんな感じですぐにヘッダーができちゃいます。

![Flatbuffers_headers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724589/c7cfee9b-550d-e547-1416-9197b3e5c6dd.png)


:::note info
後で使うので、`flatc --python --gen-onefile *fbs`は実行しておいてください。
:::

こちらを活用して作ったアプリのIFをどんどん共有し合いましょう！

## FlatBuffersのヘッダーを用いたデシリアライズ方法

最後に、つまづく人が多いFlatBuffersのヘッダーを用いたデシリアライズ方法について説明します。
ヘッダーを作ってしまえば、後は意外と簡単です。

先ほど`flatc --python --gen-onefile *fbs`で作成したファイルをImportします。

```
from fbs_test import objectdetection_generated as OD
```

実際にJupyter Notebookで提供されるチュートリアルのStep4までで取得したメタデータを用いてデシリアライズをしてみます。
まずメタデータをir_dataに格納します。
とりあえず試したい人はサンプルデータとして`DAAAAAAABgAKAAQABgAAAAwAAAAAAAYACAAEAAYAAAAEAAAAAgAAAEgAAAAQAAAADAAUAAgABwAMABAADAAAAAAAAAEBAAAACAAAAAAAZj/Q////YwAAAFUAAACtAAAAEQEAAAwAEAAAAAcACAAMAAwAAAAAAAABFAAAAAAAfz8MABQABAAIAAwAEAAMAAAArAAAAFgAAAD+AAAAwwAAAA==`を使ってください。

AITRIOSのサンプルエッジアプリケーションから出力されるメタデータはBase64エンコーディングされているので、デコードします。

```
import base64
from fbs_test import objectdetection_generated as OD

ir_data = inference_list[0]["O"]
# or
# ir_data = 'DAAAAAAABgAKAAQABgAAAAwAAAAAAAYACAAEAAYAAAAEAAAAAgAAAEgAAAAQAAAADAAUAAgABwAMABAADAAAAAAAAAEBAAAACAAAAAAAZj/Q////YwAAAFUAAACtAAAAEQEAAAwAEAAAAAcACAAMAAwAAAAAAAABFAAAAAAAfz8MABQABAAIAAwAEAAMAAAArAAAAFgAAAD+AAAAwwAAAA=='
print(ir_data)

ir_data_dec = base64.b64decode(ir_data)
```

次に下記を実行します。
```
OD.ObjectDetectionTop.GetRootAsObjectDetectionTop(ir_data_dec,0)
```
こちらを実行することでFlatBuffersのルートオブジェクトへアクセスに可能なります。
試しに、fbsに記載があったように、特定のフィールドにアクセスしてみましょう。

.fbsファイルを再度下に添付します。

```
table GeneralObject {
  class_id:uint;
  bounding_box:BoundingBox;
  score:float;
}

table ObjectDetectionData {
  object_detection_list:[GeneralObject];
}

table ObjectDetectionTop {
  perception:ObjectDetectionData;
}
```

これをみるとObjectDetectionTopの下にはperceptionというフィールドがあり、その中にはobject_dtection_listというList形式のフィールドがあります。
さらにその下にscoreという値があることを確認することができます。
このスコアにアクセスしてみましょう。

下記がそのコードです。

```
ir_Score = ir_data_obj.Perception().ObjectDetectionList(0).Score()
print("Score:", ir_Score)
```

意外と直感的ですね。
print文を見てみると下記のようにScoreの値が取れていることがわかります。

```
Score: 0.99609375
```

最後に、今デシリアライズに使用したコードの全文を貼ります。
短い！！


```
import base64
from fbs_test import objectdetection_generated as OD

ir_data = inference_list[0]["O"]
print(ir_data)

ir_data_dec = base64.b64decode(ir_data)
ir_data_obj = OD.ObjectDetectionTop.GetRootAsObjectDetectionTop(ir_data_dec,0)

ir_Score = ir_data_obj.Perception().ObjectDetectionList(0).Score()
print("Score:", ir_Score)
```

またサンプルコードを書いてみて便利だなと思うのが、
ヘッダーをImportすると補完機能がかかる点です。
JSONでIFをもらっても意外とこう言った使い方はできないのではないでしょうか。
fbsファイルと睨めっこをしなくても特定のフィールドまで辿りけるので意外とすんなりかけました。

![Flatbuffers_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724589/9c6cddd2-b066-300b-f8ba-1da5f77412c0.png)

ここまで記事を読んでくださった方はおそらくデシリアライズへの苦手意識が少しは減ったのではないかなと思っています。

## 困った時は
もし、記事の途中でうまくいかなかったなどの場合には、以下のサポートのページをご覧の上、解決しなければサポートまでお問い合わせください。
- [Support](https://developer.aitrios.sony-semicon.com/edge-ai-sensing/support)
- [Contact Us](https://developer.aitrios.sony-semicon.com/edge-ai-sensing/contact-us)


## さいごに

今回はAITRIOSで使われるFlatBuffersについて簡単な解説をさせていただきました。
ハンズオンをしていても、デシリアライズ部分でつまづきやすいポイントだと思います。
今回の記事を通して、FlatBuffersも意外と使いやすくて便利なんだぞ！というところを伝えられたら嬉しいです。
またお会いしましょう〜