FMEでラスタ解析のパイプラインを作ってみた

## はじめに
前回の記事では、[FMEでWhiteboxToolsを使うために、PythonのパッケージをFMEで使えるようにする](https://qiita.com/hirooimaki/items/046ad5d8410895ee9d51)部分について試してみましたが、今回は、処理したデータを次のワークスペースに渡すための方法を試してみました。ちなみにわからないことは全てLLMとFMEのAI Assistantに聞きました。

## 動機
FME（Feature Manipulation Engine）はカナダのSafe Software社の製品で、空間データの変換と処理を自動化できるデータパイプライン作成のためのツールです。FMEは素晴らしい製品なのですが、ちびっとわかりにくい。ということで、いろいろ試しながら、少しでも使いやすくして、空間解析の自動化、スケーリングへの道をつけていきたい、なんてことを考えています。

[Whitebox Workflows](https://www.whiteboxgeo.com/whitebox-workflows-for-python/) (WbWと略すらしい) は、ラスタ、点群、ベクタ解析のための400以上のツールが揃っていて、データパイプライン構築のための最強のツールです。[FME](https://fme.safe.com/) が400以上のファイルフォーマットの読み書きができることを考えると、この組み合わせは、最強の空間解析環境になるのでは？と想像しているわけです。

## やる事
やりたいことは、前回 PythonCaller 単独で処理したラスターデータを、次のトランスフォーマに渡す、というただそれだけです。次に渡せれば、あとはどんどん繋げるだけになりますよね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59906/4a2e9e3c-ff70-4113-b6ea-aafc091039d4.png)

ということで、前回と同様、LLMに聞いてみました。

`インプットをGeoTiff Readerから持ってきて、出力のslopeラスタを次のトランスフォーマに渡すにはどうしたら良いの？`

これに対しての回答は、

```
import whitebox
import os

class FeatureProcessor:
    def __init__(self):
        self.wbt = whitebox.WhiteboxTools()
        self.processed_files = set()  # 重複処理防止
        
    def input(self, feature):
        # GeoTIFF の元DEMファイル（Readerから渡されたもの）
        input_dem = feature.getAttribute("fme_dataset")

        # 出力パスの作成
        working_dir = os.path.dirname(input_dem)
        dem_name = os.path.splitext(os.path.basename(input_dem))[0]
        output_slope = os.path.join(working_dir, f"{dem_name}_slope.tif")

        # すでに処理済みでなければSlopeを計算
        if output_slope not in self.processed_files:
            self.wbt.set_working_dir(working_dir)
            self.wbt.slope(
                dem=input_dem,
                output=output_slope,
                zfactor=1.0
            )
            self.processed_files.add(output_slope)

        # 出力スロープファイルを次のトランスフォーマに渡すために
        feature.setAttribute("slope_path", output_slope)
        return feature
```

だったので、ただ貼り付けてみましたが、PythonCallerは動いているものの、次のFeatureReaderには、パラメタが渡せませんでした。FeatureReaderをつなげたのは、色々調べたら、PythonCallerで処理したラスタデータそのものを渡すのではなく、出力したデータのPATHを渡すのがよい、とのお達しでしたので、そのようにしています。
___

### ちょっと横道
ちなみに、Readerを使って、あらかじめファイルフォーマットを指定して読むやり方と、FeatureReaderを使ってデータを読み込む方式のメリットとデメリットをChatGPTに聞いてみたところ

### ✅ まとめ

| 比較項目    | 通常のReader    | FeatureReader           |
| ------- | ------------ | ----------------------- |
| 設定の簡単さ  | ◎ GUI操作だけでOK | △ 属性とトランスフォーマ設定が必要      |
| 柔軟性     | △ 固定されたファイル  | ◎ 属性によって動的にファイルを読み込める   |
| スキーマ管理  | ◎ 自動で取得される   | △ SchemaMapperなどで補う必要あり |
| 適している場面 | 静的なファイル読み込み  | 条件で入力ファイルが変わる処理         |

### 🏁 どっちを使うべき？

* **静的な処理（1つのファイルだけ読むなど）**：通常のReaderでOK
* **ファイル名が可変・属性に応じて読みたい**：`FeatureReader` が最適

という、なんとも素敵なまとめを返してくれました:kissing_heart:
特に今回のような、パイプラインの途中でデータを読み込むには、FeatureReader一択ですね。
___

### 本題に戻る
ところで、なんで隣のトランスフォーマーにパラメタが渡せないのか深く調べる前にChatGPTにお伺いを立てたところ、

`PythonCallerで処理した内容を次のトランスフォーマにパスするには？`

FMEの `PythonCaller` で処理した内容（結果）を**次のトランスフォーマに渡す方法**は、大きく分けて以下の3つです。

### 🧠 ポイントまとめ

| 渡し方   | 方法                           | 用途例               |
| ----- | ---------------------------- | ----------------- |
| 属性    | `setAttribute()`             | パスや計算結果を渡す        |
| ジオメトリ | `setGeometry()`              | ラスタやシェープなど        |
| 複数出力  | `self.pyoutput(new_feature)` | 複数の出力フィーチャを作る場合など |

というお達しでしたので、最後の、`self.pyoutput(new_feature)` を使ってみました。これが最終的に動いたコードです。正しいか、とか、洗練されているか、とかは聞かないでください。動きます。

```
import whitebox
import os

class FeatureProcessor:
    def __init__(self):
        self.wbt = whitebox.WhiteboxTools()
        self.processed_files = set()  # 重複処理防止

    def input(self, feature):  # ← このメソッド名が必要
        # input raster
        input_dem = feature.getAttribute("fme_dataset")
        
        # 出力パスの作成
        working_dir = os.path.dirname(input_dem)
        dem_name = os.path.splitext(os.path.basename(input_dem))[0]
        output_slope = os.path.join(working_dir, f"{dem_name}_slope.tif")        
        
        # すでに処理済みでなければSlopeを計算
        if output_slope not in self.processed_files:
            self.wbt.set_working_dir(working_dir)
            self.wbt.slope(
                dem=input_dem,
                output=output_slope,
                zfactor=1.0
            )
            self.processed_files.add(output_slope)

        # 出力スロープファイルを次のトランスフォーマに渡すために
        feature.setAttribute("slope_path", output_slope)
        print('出力：{}'.format(output_slope))
        
        # これを入れないと次に値を渡せない！！
        self.pyoutput(feature)
        
        return feature
```
これで、PythonCallerからのアプトプットが以下のように出てきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59906/e3dc5bb3-8042-4737-aaf2-14378d49e814.png)

なので、slope_path をFeatureReaderに手渡して、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59906/afd25ad5-552d-47ca-9c81-8af9f54b64e3.png)

じゃじゃーん。DEMから傾斜角を計算したラスタをFeatureReaderで読めました。ここまでできたら、あとは好きに料理できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59906/01dea9e2-0a40-496e-ad9b-0af19407ad32.png)

ということで、ラスタ解析のパイプラインをFME+Whiteboxで作ってみました。**隣のトランスフォーマーにパラメタが渡せないのか問題** 、についてはどこかで深く調べる必要もありそうですが、まずは動くものを作ってから考えるという方針で進めていますので、今回は目的達成。

最終的には、日本全国で地形解析や水文解析をやってみようと思うのですが、沖縄の比地川の氾濫に興味があったので、そこで今回の取り組みの一連の成果もとりあえずアウトプットにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59906/806bdc31-53dc-4960-b01f-afe5565a4e07.png)
[沖縄で雨が降ったら気をつけないとならない道路と橋](https://felt.com/map/Chong-Nawa-deYu-gaJiang-tutaraQi-wotukenaitonaranaiDao-Lu-toQiao-PjcJKgQGTNapmkNfLseggD?loc=26.723125,128.169142,15.37z&share=1)

[Felt](https://felt.com/) を使ってこの地図を作成していますが、[Felt](https://felt.com/) についても別に記事を書きたいと思います。