14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 6

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

Last updated at Posted at 2024-11-06

はじめに

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

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

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

実装したコードはこちら

ElixirDesktop による iOS アプリ実装

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

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

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

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

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

mix phx.new elixir_desktop_evision --no-ecto

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

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

cd elixir_desktop_evision

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

mix phx.server

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

スクリーンショット 2024-11-03 7.53.13.png

Evision の追加

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

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

...
  # 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

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

mix deps.get

カメラ画面の追加

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

lib/elixir_desktop_evision_web/live/camera_live.ex

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

<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

...
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

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

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

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

mix phx.server

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

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

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

web-evision.gif

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

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

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

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

...
  # 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

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

mix deps.get

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

mix desktop.install

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

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

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

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

mix desktop.setup.ios

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

cd native/ios

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

carthage update --platform iOS --use-xcframeworks

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

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

...
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/

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

$ 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

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

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

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

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

native/ios/run_mix を実行します

./run_mix

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

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

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

open ElixirDesktopEvision.xcodeproj

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

スクリーンショット 2024-11-03 21.35.02.png

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

スクリーンショット 2024-11-03 21.48.56.png

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

スクリーンショット 2024-11-03 21.49.29.png

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

スクリーンショット 2024-11-03 21.49.39.png

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

スクリーンショット 2024-11-03 21.50.06.png

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

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

iPhone を MacBook に接続します

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

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

mobile-evision.gif

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

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

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

まとめ

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

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

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?