iPhone 上の画像処理を ElixirDesktop + Evision で実装する

## はじめに

以前の記事で、 ElixirDesktop を使って iOS 上のカメラ撮影を実装しました 

https://qiita.com/RyoWakabayashi/items/554a88cb35f24ec594ca

https://qiita.com/RyoWakabayashi/items/3ad0ba5771246e7f867d

ただし、当時の実装では画像データの行列化がかなり重くなってしまいました

本記事では @the_haigo さんの記事に従って iOS アプリを実装した上で、 evision (OpenCV の Elixir 用モジュール)による画像処理を組み込みます

https://qiita.com/the_haigo/items/f419bda951bb60145556

実装したコードはこちら

https://github.com/RyoWakabayashi/elixir-desktop-evision

## ElixirDesktop による iOS アプリ実装

@the_haigo さんの記事に従って iOS アプリを実装します

asdf で Erlang や Elixir 等をインストールした後、 Phoenix アプリケーションを生成します

:::note warn
必ず wxwidgets をインストールしてから Erlang をインストールしてください

すでに Erlang がインストール済の場合、一度アンインストールして、 wxwidgets をインストールした後に再インストールしてください
:::

この記事ではアプリケーションを `elixir_desktop_evision` という名前にしています

```bash
mix phx.new elixir_desktop_evision --no-ecto
```

依存モジュールをインストールするか確認されるので、そのままエンターを押下（デフォルトの Yes で応答）します

作成されたディレクトリーに移動します

```bash
cd elixir_desktop_evision
```

アプリケーションを起動します

```bash
mix phx.server
```

<http://localhost:4000/> にアクセスすると Phoenix の初期画面が表示されます

![スクリーンショット 2024-11-03 7.53.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/680c1880-4131-41fc-3d06-7a4dc9ad9e6b.png)

## Evision の追加

アプリケーションの依存モジュールとして Evision を追加インストールします

https://github.com/cocoa-xu/evision

`mix.exs` に Evision を追記してください

```diff_elixir
...
  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.7.14"},
      ...
-     {:bandit, "~> 1.5"}
+     {:bandit, "~> 1.5"},
+     {:evision, "~> 0.2"}
    ]
  end
```

依存モジュールを改めてインストールします

```bash
mix deps.get
```

## カメラ画面の追加

以下のファイルを追加します

`lib/elixir_desktop_evision_web/live/camera_live.ex`

```elixir
defmodule ElixirDesktopEvisionWeb.CameraLive do
  use ElixirDesktopEvisionWeb, :live_view

  @impl true
  def mount(_args, _session, socket) do
    socket
    |> assign(processed_image: nil)
    |> then(&{:ok, &1})
  end

  # 写真撮影時の処理
  @impl true
  def handle_event("take", %{"image" => base64}, socket) do
    "data:image/jpeg;base64," <> raw = base64

    image =
      raw
      |> Base.decode64!()
      |> Evision.imdecode(Evision.Constant.cv_IMREAD_COLOR())

    {_dims, [height, width]} = Evision.Mat.size(image)
    affine = Evision.getRotationMatrix2D({width / 2, height / 2}, 30, 1)

    processed_image =
      image
      # 四角形の描画
      |> Evision.rectangle(
        # 左上座標{x, y}
        {50, 30},
        # 右下座標{x, y}
        {80, 70},
        # 色{R, G, B}
        {0, 0, 255},
        # 線の太さ
        thickness: 5,
        # 線の引き方（角がギザギザになる）
        lineType: Evision.Constant.cv_LINE_4()
      )
      # 回転
      |> Evision.warpAffine(affine, {width, height})
      # 文字列の描画
      |> Evision.putText(
        # 文字列
        "Hello",
        # 文字の左下座標{x, y}
        {100, 100},
        # フォント種類
        Evision.Constant.cv_FONT_HERSHEY_SIMPLEX(),
        # フォントサイズ
        1,
        # 文字色
        {0, 0, 255},
        # 文字太さ
        thickness: 2
      )
      |> then(&Evision.imencode(".jpg", &1))

    {:noreply, assign(socket, processed_image: processed_image)}
  end
end
```

`lib/elixir_desktop_evision_web/live/camera_live.html.heex`

```heex
<div class="">
  <video id="local-video" playsinline autoplay muted width="300"></video>
  <button id="shutter" class="border rounded p-2 bg-blue-500 text-white" phx-hook="TakePicture">
    Take a pickture
  </button>
  <canvas id="canvas" style="display: none"></canvas>
  <%= if @processed_image do %>
    <img alt="" src={"data:image/jpeg;base64,#{Base.encode64(@processed_image)}"} />
  <% end %>
</div>
```

以下のファイルを編集します

`assets/js/app.js`

```diff_javascript
...
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

+// video にカメラ映像を流す
+async function initStream() {
+  try {
+    const stream = await navigator.mediaDevices.getUserMedia({audio: true, + video: true, width: "1280"})
+    localStream = stream
+    document.getElementById("local-video").srcObject = stream
+  } catch (e) {
+    console.log(e)
+  }
+}
+
+let Hooks = {}
+
+// 写真撮影用フック
+Hooks.TakePicture = {
+  mounted() {
+    initStream()
+
+    this.el.addEventListener("click", event => {
+      const width = 300;
+      var canvas = document.getElementById("canvas");
+      var video = document.getElementById("local-video");
+      const height = parseInt(width * video.videoHeight / video.videoWidth);
+      canvas.width = width;
+      canvas.height = height;
+      canvas.getContext('2d').drawImage(video, 0, 0, width, height);
+      const picture = canvas.toDataURL("image/jpeg", 1.0)
+      this.pushEvent("take", {"image": picture})
+    })
+  }
+}
+
let liveSocket = new LiveSocket("/live", Socket, {
+  hooks: Hooks,
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken}
})
...
```

`lib/elixir_desktop_evision_web/router.ex`

```diff_elixir
...
  scope "/", ElixirDesktopEvisionWeb do
    pipe_through :browser

-   get "/", PageController, :home
+   live_session :default,
+     layout: {ElixirDesktopEvisionWeb.Layouts, :app} do
+     live "/", CameraLive, :index
+   end
  end
...
```

改めてアプリケーションを起動します

```bash
mix phx.server
```

<http://localhost:4000/> にアクセスすると、カメラアクセスの許可を求められます（初回のみ）

許可すると、カメラ映像が動画として表示されます

「Take a pickture」ボタンをクリックすると、加工した画像が表示されます

![web-evision.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9ad7d72c-8bdb-6986-a23d-3d7139b93fc1.gif)

Evision を使うことで、簡単に画像処理が実装できました

## デスクトップアプリケーションへの変更

アプリケーションの依存モジュールとして DesktopSetup を追加インストールします

https://github.com/thehaigo/desktop_setup

`mix.exs` に DesktopSetup を追記してください

```diff_elixir
...
  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.7.14"},
      ...
      {:bandit, "~> 1.5"},
-     {:evision, "~> 0.2"}
+     {:evision, "~> 0.2"},
+     {:desktop_setup, github: "thehaigo/desktop_setup", only: :dev}
    ]
  end
```

依存モジュールを改めてインストールします

```bash
mix deps.get
```

デスクトップアプリケーションへの変更を実施します

```bash
mix desktop.install
```

:::note warn
iOS アプリケーションだけを実装したい場合であっても、デスクトップアプリケーション化を先に実行する必要があります
:::

:::note warn
`iex -S mix` でデスクトップアプリケーションを起動できますが、カメラが起動しません
:::

## iOS アプリケーションの作成

iOS アプリケーション用のコードを追加します

```bash
mix desktop.setup.ios
```

作成された `native/ios` ディレクトリーに移動します

```bash
cd native/ios
```

iOS アプリケーションに必要な ZIPFoundation をインストールします

```bash
carthage update --platform iOS --use-xcframeworks
```

iOS 上で `.so` などのライブラリーを使用する場合、ファイルに署名する必要があります

`evision.so` に署名するため、 `native/ios/run_mix` を以下のように編集します

```diff_shell
...
BASE=`pwd`
export MIX_ENV=prod
export MIX_TARGET=ios
+ export EVISION_PREFER_PRECOMPILED=false
+ export CODESIGN_ID=`security find-identity -v -p codesigning | sed -n 's/.*"\(.*\)".*/\1/p' | tail -n 1`
...
- if [ ! -d "deps/desktop" ]; then
-   mix deps.get
- fi
+ mix deps.get
...
mix assets.deploy && \
  mix release --overwrite && \
  cd _build/ios_prod/rel/elixir_desktop_evision && \
- zip -9r "$BASE/ElixirDesktopEvision/app.zip" lib/ releases/ --exclude "*.so"  
+ echo "CODESIGN_ID: ${CODESIGN_ID}" && \
+ find . -name "*.so" -exec codesign -v --force --sign "${CODESIGN_ID}" {} \; && \
+ zip -9qr "$BASE/ElixirDesktopEvision/app.zip" lib/ releases/
```

:::note info
`security find-identity -v -p codesigning` は MacBook 上の有効な証明書一覧を取得します

```bash
$ security find-identity -v -p codesigning
  1) ABC123123123ABC123123123ABC123123123ABC1 "Apple Development: xxx@xxx.co.jp (123ABC123Z)"
  2) ABC123123123ABC123123123ABC123123123ABC1 "Apple Development: xxx@yyy.co.jp (456DEF456D)"
  3) ABC123123123ABC123123123ABC123123123ABC1 "Apple Development: xxx@zzz.co.jp (789GHI789G)"
     3 valid identities found
```

`sed -n 's/.*"\(.*\)".*/\1/p'` にパイプすることで、署名に必要な部分（`Apple Development: xxx@xxx.co.jp (123ABC123Z)` など）だけを取り出します

最後に `tail -n 1` で最終行のものを取得します

従って、上記の例では `Apple Development: xxx@zzz.co.jp (789GHI789G)` が取得されます

`tail -n 1` の部分はどの証明書を使うかによって、適宜変更してください

証明書の生成、署名の確認方法は以下の記事を参考にしてください

https://qiita.com/Arime/items/e1df2a8c3d4c2ce75069

署名の確認時、 `Authority` と `TeamIdentifier` 内のチームIDが一致していないと、実行時に `code signature invalid` というエラーが発生します

```bash
$ codesign -dvvv ../../_build/ios_prod/rel/elixir_desktop_evision/lib/evision-0.2.9/priv/evision.so 
...
Authority=Apple Development: 諒 若林 (789GHI789G)
...
TeamIdentifier=789GHI789G
```
:::

:::note info
Evision の PR に書かれていた方法を参考にしました

https://github.com/cocoa-xu/evision/pull/79
:::

`native/ios/run_mix` を実行します

```bash
./run_mix
```

`native/ios/ElixirDesktopEvision/WebView.swift` を以下のように編集します

```diff_swift
        let configuration = WKWebViewConfiguration()
        configuration.limitsNavigationsToAppBoundDomains = true
        configuration.preferences = preferences
        configuration.defaultWebpagePreferences = page
+       configuration.allowsInlineMediaPlayback = true
```

アプリケーションのビルド完了後、 XCode で iOS 用プロジェクトを開きます

```bash
open ElixirDesktopEvision.xcodeproj
```

「General」タブの「Team」で Apple Developer Program に登録しているチームを選択します

![スクリーンショット 2024-11-03 21.35.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b7ce991a-db99-f49e-a13a-89076fc02f48.png)

「info」タブの「Custom macOS Application Target Properties」内で右クリックし、コンテキストメニューから「Add Row」をクリックします

![スクリーンショット 2024-11-03 21.48.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bbee4e1d-094c-96ba-78b3-7560b0f4d373.png)

追加された行の「Key」列に `NSCameraUsageDescription` と入力します

![スクリーンショット 2024-11-03 21.49.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d60b5485-119c-01a0-232f-36ccce8b1719.png)

エンターキーを押下すると値が `Privacy - Camera Usage Description` に変化します

![スクリーンショット 2024-11-03 21.49.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6e7ff10b-ffb3-e129-d516-656ad11ebe4a.png)

「Value」列に任意の文字列を入力します

![スクリーンショット 2024-11-03 21.50.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b7b8535a-f55d-e691-7edf-737a00286bc3.png)

同様に `NSMicrophoneUsageDescription` にも任意の文字列を設定します

## iOS アプリケーションの起動

iPhone を MacBook に接続します

:::note info
iPhone の「設定」から「プライバシーとセキュリティ」 |> 「デベロッパーモード」をオンにしておきましょう
:::

実行対象に iPhone を設定し、アプリケーションを実行します

![mobile-evision.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/625f0b5e-f556-4366-5820-fe1416f6ba45.gif)

iPhone 上で Evision による画像処理が実行できました

:::note warn
初回起動時、信頼されていないというようなメッセージが表示されるので、以下のヘルプを参考に信頼してください

https://support.apple.com/ja-jp/118254
:::

## まとめ

Evision が動いたことにより、モバイルでも画像処理や行列演算の可能性がグッと広がりました

もっと色々試してみたいと思います