はじめに
以前の記事で、 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 の初期画面が表示されます
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」ボタンをクリックすると、加工した画像が表示されます
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
署名の確認時、 Authority
と TeamIdentifier
内のチーム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 に書かれていた方法を参考にしました
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 に登録しているチームを選択します
「info」タブの「Custom macOS Application Target Properties」内で右クリックし、コンテキストメニューから「Add Row」をクリックします
追加された行の「Key」列に NSCameraUsageDescription
と入力します
エンターキーを押下すると値が Privacy - Camera Usage Description
に変化します
「Value」列に任意の文字列を入力します
同様に NSMicrophoneUsageDescription
にも任意の文字列を設定します
iOS アプリケーションの起動
iPhone を MacBook に接続します
iPhone の「設定」から「プライバシーとセキュリティ」 |> 「デベロッパーモード」をオンにしておきましょう
実行対象に iPhone を設定し、アプリケーションを実行します
iPhone 上で Evision による画像処理が実行できました
初回起動時、信頼されていないというようなメッセージが表示されるので、以下のヘルプを参考に信頼してください
まとめ
Evision が動いたことにより、モバイルでも画像処理や行列演算の可能性がグッと広がりました
もっと色々試してみたいと思います