ElixirDesktop on Axon 画像分類

# はじめに
この記事はElixirでマルチプラットフォームなネィティブアプリケーションをLiveViewベースで作れるElixirDesktopというライブラリで以下のことを実装した記事になります

- プロジェクトをつくる
- saasからTailwind,PetalComponentに差し替える
- Axonで画像分類を実装する

# ElixirDesktopとは
WxWidgetとPhoenixを組み合わせてwebサーバーを起動することなくスタンドアローンでネイティブアプリを起動・実装できるライブラリです

DBにはSqlite３を使います

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

また、自分はまだ成功していませんが、Android, iOSでも同一のコードでネィティブアプリを起動することができます
iOS: https://github.com/elixir-desktop/ios-example-app
Android: https://github.com/elixir-desktop/android-example-app

# PetalCompoentとは
PETALとは
Phoenix
Elixir
Tailwind
AlpineJS
LiveView
の技術スタックの頭文字です

Tailwindで実装したLiveViewComponentのUIコンポーネント集になります

https://petal.build/

# 環境

 Elixir　1.13以降
 Erlang 24以降 wxwidgetsを含む

https://qiita.com/hayabusa333/items/b8805d7274f60e0deb4d

# プロジェクトの作成
では早速作っていこうと思いますが、まだ`mix phx.new`に該当するコマンドは無いようなので
サンプルプロジェクトをgit cloneしてきます

https://github.com/elixir-desktop/desktop-example-app

```
git clone https://github.com/elixir-desktop/desktop-example-app.git desktop
cd desktop
```
次にassetsをインストールします
```
cd assets
npm install
```
準備が整ったので起動しましょう
```
mix deps.get
iex -S mix
```

無事起動しました！
![スクリーンショット 2022-07-20 14.33.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3def6e78-f47b-d8d3-84be-563bddb5fd3e.png)

# ライブラリのインストール
画像分類に必要なものとPetalComponentsをインストールしていきます
petalで0.17以降のLiveViewが要求されるのでバージョンをあげます

```mix.exs
defmodule Todo.MixProject do
  use Mix.Project
  ...
  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:ecto_sqlite3, "~> 0.7"},
      # {:desktop, path: "../desktop"},
      {:desktop, github: "elixir-desktop/desktop", tag: "v1.4.0"},

      # Phoenix
      {:phoenix, "~> 1.6"},
      {:phoenix_live_view, "~> 0.17.4"}, # 0.16から0.17.4に変更
      {:phoenix_html, "~> 3.0"},
      {:phoenix_live_reload, "~> 1.3", only: [:dev]},
      {:gettext, "~> 0.18"},
      {:plug_cowboy, "~> 2.5"},
      {:jason, "~> 1.2"},

      # Assets
      {:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
      {:tailwind, "~> 0.1", runtime: Mix.env() == :dev}, # 追加
      {:petal_components, "~> 0.17"}, # 追加

      # Credo
      {:credo, "~> 1.5", only: [:dev, :test], runtime: false},

      # 以下追加
      # ml
      {:axon_onnx, github: "elixir-nx/axon_onnx"},
      {:exla, "~> 0.3.0-dev", github: "elixir-nx/nx", sparse: "exla"},
      {:nx, "~> 0.3.0-dev", [env: :prod, git: "https://github.com/elixir-nx/nx.git", sparse: "nx", override: true]},
      {:stb_image, "~> 0.4.0"}
    ]
  end
end
```

追加したので更新します
```
mix deps.get
```

# Tailwind,PetalComponentセットアップ

tailwindはこちらを参考にしてください

https://tailwindcss.com/docs/guides/phoenix

完了したらconfigを以下のようにします
https://petal.build/components#install_tailwind

```assets/tailwind.config.js
const colors = require("tailwindcss/colors");

module.exports = {
  content: [
    "../lib/*_web.ex",
    "../lib/*_web/**/*.*ex",
    "./js/**/*.js",
    "../deps/petal_components/**/*.*ex",
  ],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        primary: colors.blue,
        secondary: colors.pink,
      },
    },
  },
  plugins: [require("@tailwindcss/forms")],
};
```

次に ViewHelperにPetalComponentを追加します

```lib/todo_web.ex
defmodule TodoWeb do
  ...
  defp view_helpers do
    quote do
      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML
      use PetalComponents # 追加

      # Import LiveView helpers (live_render, live_component, live_patch, etc)
      import Phoenix.LiveView.Helpers

      # Import basic rendering functionality (render, render_layout, etc)
      import Phoenix.View

      import TodoWeb.ErrorHelpers
      import TodoWeb.Gettext

    end
  end
end
```

次に元のCSSとSASSの設定を削除していきます

```diff_elixir:config/config.exs
config :esbuild,
  version: "0.12.18",
  default: [
    args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

+ config :tailwind,
+  version: "3.1.6",
+  default: [
+    args: ~w(
+      --config=tailwind.config.js
+      --input=css/app.css
+      --output=../priv/static/assets/app.css
+    ),
+    cd: Path.expand("../assets", __DIR__)
+  ]

- config :dart_sass,
-  version: "1.39.0",
-  default: [
-    args: ~w(css/app.scss ../priv/static/assets/app.css),
-    cd: Path.expand("../assets", __DIR__)
-  ]

```

```diff_elixir:config/dev.exs
config :todo_app, TodoWeb.Endpoint,
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
    esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
+   tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
-   sass:
-     {DartSass, :install_and_run,
-      [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)]}
  ],
  # Watch static and templates for browser reloading.
  live_reload: [
    patterns: [
      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
      ~r"priv/gettext/.*(po)$",
      ~r"lib/todo_app/.*(ee?x)$",
      ~r"lib/todo_web/(live|views)/.*(ex)$",
      ~r"lib/todo_web/templates/.*(eex)$"
    ]
  ]
```

```diff_elixir:mix.exs
defmodule Todo.MixProject do
  use Mix.Project
  ...
  defp aliases do
    [
      "assets.deploy": [
        "phx.digest.clean --all",
        "esbuild default --minify",
+       "tailwind default --minify",
-       "sass default --no-source-map --style=compressed",        
        "phx.digest"
      ]
    ]
  end
end
```

app.scssをapp.cssにリネームします
```diff_cssassets/app.css
/* This file is for your main application css. */
@use "../node_modules/nprogress/nprogress.css";
- @import "./todo.scss";
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;
```

最後にレイアウトファイルをPetalComponentに対応させます
rename live.html.leex -> live.html.heex
```elixir:lib/todo_web/templates/layout/live.html.heex
<.container>
  <.alert
    color="info"
    class="mb-5"
    label={live_flash(@flash, :info)}
    phx-click="lv:clear-flash"
    phx-value-key="info"
  />

  <.alert
    color="danger"
    class="mb-5"
    label={live_flash(@flash, :error)}
    phx-click="lv:clear-flash"
    phx-value-key="error"
  />

  <%= @inner_content %>
</.container>

```
これでcssをsassからTailwindに変更できました！

# 画像分類部分の実装
基本的にこちらをベースにしていきます

https://github.com/thehaigo/live_onnx

https://qiita.com/the_haigo/items/8f5157a185e08f6d6bce

## ファイルをアップロードして画像を表示させる

```lib/todo_web/live/todo_live.ex
defmodule TodoWeb.TodoLive do
  @moduledoc """
    Main live view of our TodoApp. Just allows adding, removing and checking off
    todo items
  """
  use TodoWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:list, File.read!("model/classlist.json") |> Jason.decode!())
      |> assign(:upload_file, nil)
      |> assign(:ans, [])
      |> allow_upload(
        :image,
        accept: :any,
        chunk_size: 6400_000,
        progress: &handle_progress/3,
        auto_upload: true
      )

    {:ok, socket}
  end

  def handle_progress(:image, _entry, socket) do
    upload_file =
      consume_uploaded_entries(socket, :image, fn %{path: path}, _entry ->
        File.read(path)
      end)
      |> List.first()

    {:noreply, assign(socket, :upload_file, upload_file)}
  end

  @impl true
  def handle_event("validate", _params, socket) do
    {:noreply, socket}
  end
end
```

画像をドラッグアンドドロップしたら、バイナリデータにして imgタグのsrcにbase64でエンコードして表示させます
残りのイベントは後ほど実装します

```elixir:lib/todo_web/live/todo_live.html.heex
<div class="flex items-start justify-between pt-4 h-screen">
  <div class="relative w-60 mr-4 h-full mb-4">
    <.card class="h-5/6">
      <.card_content category="Actions">
          <.button class="m-1 w-full" label="Detect" phx-click="detect" variant="shadow" />
          <.button class="m-1 w-full" label="Clear" color="white" variant="shadow" phx-click="clear" />
        <%= for {ans, index} <- Enum.with_index(@ans) do %>
          <h5><%= "#{index + 1}: " <> Map.get(@list, to_string(ans)) %></h5>
        <% end %>
      </.card_content>
    </.card>
  </div>
  <div class="flex flex-col w-full pr-4 h-full">
    <.card class="h-5/6">
      <.card_content class="p-4">
        <div style={ if @upload_file != nil, do: "display:none" }>
          <form phx-change="validate" >
            <div class="border border-dashed border-gray-500 relative"
            phx-drop-target={@uploads.image.ref}
            >
              <%= live_file_input @uploads.image, class: "cursor-pointer relative block opacity-0 w-full h-full p-20 z-50" %>
              <div class="text-center p-10 absolute top-0 right-0 left-0 m-auto">
              <h4>
                Drop files anywhere to upload
              <br/>or
              </h4>
              <p class="">Select Files</p>
              </div>
            </div>          
          </form>
        </div>
        <%= if @upload_file do %>
          <div class="w-full h-full mb-4">
            <img alt="" class="object-cover" src={"data:image/png;base64,#{Base.encode64(@upload_file)}"}/></div>
        <% end %>
      </.card_content>
    </.card>
  </div>
</div>
```

![83a974e8f9654a504fbf7f482eb54a23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/1464a9cc-f3d2-98b3-bed5-244e25b959da.gif)


## Nxのセットアップ
Axonの計算部分を行うNxは設定をしないとpureElixirで計算を行い大変遅いので
計算部分(Backendという)にgoogle xlaを使うExlaをセットします

```config/config.exs
config :nx, :default_backend, EXLA.Backend
config :nx, :default_defn_options, compiler: EXLA
```

## 推論に使うモデルの準備
torchvisonからonnxにコンバートした動作確認済みリストがここにあるので、お好きなモデルを使ってください
今回はsqueezenetを使っています
https://github.com/thehaigo/live_onnx/tree/main/model

## 推論部分の実装

画像のバイナリファイルを引数にとり
- 前処理
- モデルの読み込み
- 推論の実行
する関数を実装します

```lib/todo_app/worker.ex
defmodule TodoApp.Worker do
  require Axon

  def detect(binary) do
    tensor = preprocess(binary)
    {model, params} = AxonOnnx.import("model/squeezenet/model.onnx")

    Axon.predict(model, params, tensor)
    |> Nx.flatten()
    |> Nx.argsort()
    |> Nx.reverse()
    |> Nx.slice([0], [5])
    |> Nx.to_flat_list()
  end

  def preprocess(binary) do
    {:ok, image} = StbImage.from_binary(binary)
    {:ok, image} = StbImage.resize(image, 224, 224)

      # Nx.Tensorへ変換
      StbImage.to_nx(image)
      # 値が0~1の範囲になるように変換
      |> Nx.divide(255)
      # 正規化 by torchvisonのドキュメント
      |> Nx.subtract(Nx.tensor([0.485, 0.456, 0.406]))
      |> Nx.divide(Nx.tensor([0.229, 0.224, 0.225]))
      # 224x224x3を3x224x224に変換
      |> Nx.transpose()
      # 1x3x224x224になるように軸を追加
      |> Nx.new_axis(0)
  end
end

```
正規化ですが精度を上げるように 全体からmeanの値を引いてstbの値で除算を行います
https://pytorch.org/vision/stable/models.html#models-and-pre-trained-weights


detectボタンを押したときに推論の開始
celarボタンを押したときに答えと画像を初期化する処理を書いて完了です

```lib/todo_web/live/todo_live.ex
defmodule TodoWeb.TodoLive do
  @moduledoc """
    Main live view of our TodoApp. Just allows adding, removing and checking off
    todo items
  """
  use TodoWeb, :live_view

  ...
  @impl true
  def handle_event("detect",_params,%{assigns: %{upload_file: binary}} = socket) do
    {:noreply, assign(socket, :ans, TodoApp.Worker.detect(binary))}
  end

  @impl true
  def handle_event("clear", _params, socket) do
    {
     :noreply,
      socket
      |> assign(:upload_file, nil)
      |> assign(:ans, [])
    }
  end
end
```

# DEMO

![d803cb000e0518a2950856ef4bfe147a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/4864505c-5d82-c33c-0de1-f2a7171bfb7c.gif)

# 最後に
無事ElixirDesktopでもAxon, Nx, Exlaが動きました！
まだ　ElixirDesktopはパッケージングは実装されていませんが
Livebookのデスクトップアプリケーション版がElixirDesktopをベースにしたAppBuilderというモジュールを作成し、使用しているようなので近いうちに実装されるかもしれません

これを応用して
スマートフォン等のエッジで完結する図鑑アプリ
YOLOなどの物体検知を読み込んでアノテーションツール
果てはNodeでつないでスマホで分散学習
など夢が広がりますね！

本記事は以上になりますありがとうございました

# コード

https://github.com/thehaigo/axon_desktop

# 参考
https://petal.build/components
https://github.com/elixir-desktop/desktop
https://github.com/elixir-desktop/ios-example-app
https://github.com/elixir-desktop/android-example-app
https://github.com/elixir-desktop/desktop-example-app
https://qiita.com/hayabusa333/items/b8805d7274f60e0deb4d
https://github.com/thehaigo/live_onnx
https://github.com/thehaigo/live_onnx/tree/main/model
https://qiita.com/the_haigo/items/8f5157a185e08f6d6bce
https://pytorch.org/vision/stable/models.html#models-and-pre-trained-weights
https://hexdocs.pm/exla/EXLA.html
https://tailwindcss.com/
https://tailwindcss.com/docs/guides/phoenix
https://tailwindcomponents.com/component/file-upload-drop-zone
https://hexdocs.pm/phoenix_live_view/0.17.0/uploads.html#content