Delta LakeとUnity Catalogによる画像の管理

以前に以下の記事を書きました。

https://qiita.com/taka_yayoi/items/01ad0a40679874f2aa61

こちらでは、Unity Catalogの[ボリューム機能](https://docs.databricks.com/ja/connect/unity-catalog/volumes.html)を用いることで画像ファイルを簡単に扱えることを説明しました。

今回、さらに話を広げて、非構造化の画像ファイルからどのように構造化したテーブルに変換するのかを説明します。

# オブジェクトストレージにおける小さなファイル問題

上述のボリューム機能も実態はクラウドのオブジェクトストレージです。ファイルの登録は簡単ですが、大量の画像ファイルに対して一括で処理を行いたい、集計処理を行いたいというケースでは、小さな大量のファイルに対するアクセスによるパフォーマンスの低下や管理が大変というような問題も出てきます。

# Delta Lakeによる画像の格納

![Screenshot 2024-02-05 at 17.00.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/76b683fa-d487-0416-aeba-ffb1a3e64169.png)

Delta Lakeでは、[Sparkの画像データソース](https://docs.databricks.com/ja/query/formats/image.html)と組み合わせることで、テーブルの一つのカラムとして画像を格納することができます。これによって、テーブルで画像を管理しつつも、以下のようなDelta Lakeのメリットを享受することができます。

- 高速なクエリー
- ストリーム処理
- ACIDトランザクション
- 設定可能な圧縮処理

## 探索メインか格納メインか

Delta lakeでは画像の2つのフォーマットをサポートしています。

- イメージフォーマット - 探索向け
    画像をデコードし、サムネイルを作成します。データのサブセットを可視化し、サンプルデータの高さや幅、チャンネル数をチェックする際に有用です。
- バイナリーファイルフォーマット - 格納向け
    それぞれのファイルは、バイナリーのコンテンツとメタデータを持つ単一行として格納されます。画像のデコードは行いません。

## 格納形態 - 生の画像ファイル

![Screenshot 2024-02-05 at 17.37.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/edf9ad8a-3301-0310-d458-07dc7ab7424a.png)

この形態ではDeltaは使いません。画像ファイルをオブジェクトストレージに格納するだけです。

- メリット
    - 管理が簡単
- デメリット
    - パフォーマンスが出ない
    - バージョン管理ができない(タイムトラベル)
    - データを拡張できない(列を追加できない)

## 画像の格納形態 - Deltaテーブルに格納

![Screenshot 2024-02-05 at 17.38.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/7ab0b1ce-8b02-abf8-a4d3-661e9b2118e3.png)

- メリット
    - バージョン管理(タイムトラベル)
    - 大規模処理でも高いパフォーマンス(OPTIMIZE、Z-Orderなど)
    - Single source of truth(データが散在しない)
    - 管理が簡単
- デメリット
    - DeltaのAPIを使う必要あり

この形態はメリットが多いですが、カラムに画像を持つことになるので、レコードあたりのサイズが大きくなりボトルネックになる場合もあります。この場合、次のハイブリッドアプローチを取ります。

## 画像の格納形態 - ハイブリッド

画像データ自体はファイルで持ち、ファイルへのパスをDelta Lakeテーブルで保持します。

![Screenshot 2024-02-05 at 17.42.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/fafbbe65-a313-db26-9268-a50517df3819.png)

# サンプルノートブックのウォークスルー

[こちら](https://qiita.com/taka_yayoi/items/0256a3d1fc57413420d5)で紹介したノートブックを追加います。

最初にS3から画像ファイルをダウンロードします。この時点ではDatabricksクラスターのドライバーノードのローカルストレージに保持しています。

```py
%pip install awscli
```

```sh
%sh
mkdir -p /tmp/data
aws s3 cp --no-progress --no-sign-request s3://amazon-visual-anomaly/VisA_20220922.tar /tmp
```

```sh
%sh
mkdir -p /tmp/data
tar xf /tmp/VisA_20220922.tar --no-same-owner -C /tmp/data/ 
```

## 画像ファイルをボリュームに移動

```sh
%sh
rm -rf /Volumes/takaakiyayoi_catalog/pcb/data/pcb1
mkdir -p /Volumes/takaakiyayoi_catalog/pcb/data/pcb1/labels 
cp -r /tmp/data/pcb1/Data/Images/ /Volumes/takaakiyayoi_catalog/pcb/data/pcb1/
cp /tmp/data/pcb1/image_anno.csv /Volumes/takaakiyayoi_catalog/pcb/data/pcb1/labels/
```

カタログエクスプローラからボリューム上のファイルを確認できます。
![Screenshot 2024-02-05 at 17.45.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/6b333ab2-72c5-27b1-f1c2-c99ea81266fe.png)

ボリューム上の画像ファイルにはPILなどでもアクセスできます。

```py
from PIL import Image
import matplotlib.pyplot as plt

def display_image(path, dpi=300):
    img = Image.open(path)
    width, height = img.size
    plt.figure(figsize=(width / dpi, height / dpi))
    plt.imshow(img, interpolation="nearest", aspect="auto")


display_image("/Volumes/takaakiyayoi_catalog/pcb/data/pcb1/Images/Normal/0000.JPG")
display_image("/Volumes/takaakiyayoi_catalog/pcb/data/pcb1/Images/Anomaly/000.JPG")
```
![Screenshot 2024-02-05 at 17.46.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e32fd218-e753-f73e-9945-cfad3fb22bd2.png)

## テーブルへの保存

```sql
%sql
-- 使用するDeltaテーブルのリセット
USE takaakiyayoi_catalog.pcb;
DROP TABLE IF EXISTS circuit_board;
DROP TABLE IF EXISTS circuit_board_gold;
DROP TABLE IF EXISTS circuit_board_label;
DROP TABLE IF EXISTS circuit_board_prediction;
```

```py
cloud_storage_path="/Volumes/takaakiyayoi_catalog/pcb/data/pcb1"
```

## Auto LoaderによるCSVラベルファイルのロード

こちらは画像ファイルではないのですが、画像のラベルファイルをテーブルにロードしておきます。

```py
from pyspark.sql.functions import substring_index, col

(
    spark.readStream.format("cloudFiles")
    .option("cloudFiles.format", "csv")
    .option("header", True)
    .option("cloudFiles.schemaLocation", f"{cloud_storage_path}/circuit_board_label_schema")
    .load(f"{cloud_storage_path}/labels/")
    .withColumn("filename", substring_index(col("image"), "/", -1))
    .select("filename", "label")
    .withColumnRenamed("label", "labelDetail")
    .writeStream.trigger(availableNow=True)
    .option("checkpointLocation", f"{cloud_storage_path}/circuit_board_label_checkpoint")
    .toTable("circuit_board_label")
    .awaitTermination()
)
display(spark.table("circuit_board_label"))
```
![Screenshot 2024-02-05 at 17.47.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/3f383f1d-ffc8-1d30-9cb7-5ea871eaa14a.png)

可視化してラベルの割合を確認します。
![Screenshot 2024-02-05 at 17.48.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/2242f479-8332-4d37-b53f-1068a810f3b8.png)

## Auto Loaderによるバイナリーファイルのロード

これで、画像をロードするためにAuto Loaderを用い、ラベルのカラムを作成するためにspark関数を活用することができます。また、テーブルとして画像のコンテンツとラベルを簡単に表示することができます。DatabricksにおけるテーブルのデフォルトフォーマットはDelta Lakeなので、`toTable`でテーブルに保存すると自動でDeltaテーブルになります。

そして、以下ではPySparkを使っているので、大量の画像ファイルに対して同じ処理を適用する際には、クラスターのコア数を増加させることでパフォーマンスを簡単に改善することができます。

```py
from pyspark.sql.functions import substring_index, col, when

(
    spark.readStream.format("cloudFiles")
    .option("cloudFiles.format", "binaryFile")
    .option("pathGlobFilter", "*.JPG")
    .option("recursiveFileLookup", "true")
    .option("cloudFiles.schemaLocation", f"{cloud_storage_path}/circuit_board_schema")
    .load(f"{cloud_storage_path}/Images/")
    .withColumn("filename", substring_index(col("path"), "/", -1))
    .withColumn(
        "labelName",
        when(col("path").contains("Anomaly"), "anomaly").otherwise("normal"),
    )
    .withColumn("label", when(col("labelName").eqNullSafe("anomaly"), 1).otherwise(0))
    .select("filename", "content", "label", "labelName")
    .writeStream.trigger(availableNow=True)
    .option("checkpointLocation", f"{cloud_storage_path}/circuit_board_checkpoint")
    .toTable("circuit_board")
    .awaitTermination()
)
display(spark.table("circuit_board"))
```

`content`列にサムネイルが表示されています。
![Screenshot 2024-02-05 at 17.49.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e93d28fb-3752-49da-9263-d6e7b27e6927.png)

## ラベルと画像テーブルをマージ

画像が格納されていてもテーブルとして問題なく扱えるので、joinも可能です。

```sql
%sql
CREATE TABLE circuit_board_gold as (
  select
    cb.*,
    labelDetail
  from
    circuit_board cb
    inner join circuit_board_label cbl on cb.filename = cbl.filename
);
```

## 画像テーブルに対する自動最適化を有効化

自動最適化は2つの補完的な機能となります: 最適化書き込みとオートコンパクションです。

```sql
%sql
ALTER TABLE circuit_board_gold SET TBLPROPERTIES (delta.autoOptimize.optimizeWrite = true, delta.autoOptimize.autoCompact = true);
ALTER TABLE circuit_board SET TBLPROPERTIES (delta.autoOptimize.optimizeWrite = true, delta.autoOptimize.autoCompact = true)
```

## 最終テーブルに対するクエリー

```sql
%sql
select
  *
from
  circuit_board_gold
limit 10
```
![Screenshot 2024-02-05 at 17.54.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/158e948b-bd72-e2fc-ff3a-6e7569428a39.png)

## リネージの確認

ボリューム、テーブルとデータを加工してきましたが、これらの依存関係はすべてリネージとして記録されています。
![Screenshot 2024-02-05 at 17.55.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/6859cc36-38b8-8d1e-11d5-fc3eb91212e2.png)

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)