2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

血圧を記録するアプリを作る その2 〜 AIを使って血圧計の写真を元に値を入力する 〜

Last updated at Posted at 2025-12-01

「血圧を記録するアプリを作る」シリーズです
前回は「LiveViewを使ってWeb画面を作る」でした

今回は「AIを使って血圧計の写真を元に値を入力する」を作ります

実行環境

  • OS Ubuntu 24.04
  • GPU RTX4090
  • Elixir 1.17.1-otp-27
  • Erlang 27.2.1
  • Phoenix 1.8.2
  • Olama 0.13.0
  • モデル gemma3:27b

今回の実行イメージ

前提知識

AIを使って文字を取得する

精度を上げるため、グレースケールにする

LiveViewでUploadする

これらを組み合わせて作ります

プログラムを書く

ollamaとevisionを追加

mix.exs
defmodule BloodPressureRecord.MixProject do
  use Mix.Project
# 省略 #
 defp deps do
    [
# 省略 #
+     {:ollama, "0.8.0"},
+     {:evision, "~> 0.2.14"}
    ]    
# 省略 #

ライブラリーを取得

$ mix deps.get

ルータ追加

/upを追加

lib/blood_pressure_record_web/router.ex
defmodule BloodPressureRecordWeb.Router do
# 省略 #
  scope "/", BloodPressureRecordWeb do
    pipe_through :browser

    get "/", PageController, :home
    live "/blood_pressures", BloodPressureLive.Index, :index
    live "/blood_pressures/new", BloodPressureLive.Form, :new
    live "/blood_pressures/:id", BloodPressureLive.Show, :show
    live "/blood_pressures/:id/edit", BloodPressureLive.Form, :edit
+   live "/up", BloodPressureLive.UploadLive, :index
  end
# 省略 #

アップロード処理作成

lib/blood_pressure_record_web/live/blood_pressure_live/upload_live.ex
defmodule BloodPressureRecordWeb.BloodPressureLive.UploadLive do
  use BloodPressureRecordWeb, :live_view
  alias BloodPressureRecord.BloodPressures
  alias Evision, as: Ev
  alias Evision.ColorConversionCodes, as: Evc

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:uploaded_files, [])
     |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 1)}
  end

  @impl Phoenix.LiveView
  def handle_event("validate", _params, socket) do
    {:noreply, socket}
  end

  @impl Phoenix.LiveView
  def handle_event("cancel-upload", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :avatar, ref)}
  end

  @impl Phoenix.LiveView
  def handle_event("save", _params, socket) do
    data =
      consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
        {:ok, run(path)}
      end)
      |> List.first()

    systolic = Enum.at(data, 0) |> String.to_integer()
    diastolic = Enum.at(data, 1) |> String.to_integer()
    pulse = Enum.at(data, 2) |> String.to_integer()
    measured_at = NaiveDateTime.utc_now()

    save_blood_pressure(socket, %{
      systolic: systolic,
      diastolic: diastolic,
      pulse: pulse,
      measured_at: measured_at
    })

  end

  def run(file) do
    client = Ollama.init()

    {:ok, ret} =
      Ollama.completion(client,
        model: "gemma3:27b",
        prompt: prompt(),
        images: [get_base64_image(file)]
      )

    ret
    |> Map.get("response")
    |> String.split(",")
  end

  defp get_base64_image(image_file_path) do
    Ev.imread(image_file_path)
    |> Ev.cvtColor(Evc.cv_COLOR_BGR2GRAY())
    |> then(&Ev.imencode(".jpg", &1))
    |> Base.encode64()
  end

  defp prompt() do
    """
    次の数値のみ取得してください
    - 最高血圧
    - 最低血圧
    - 脈拍

    出力はCSV形式で
    フォーマット例
    120,70,80
    """
  end

  defp save_blood_pressure(socket, blood_pressure_params) do
    case BloodPressures.create_blood_pressure(blood_pressure_params) do
      {:ok, _} ->
        {:noreply,
         socket
         |> put_flash(:info, "Blood pressure created successfully")
         |> push_navigate(to: "/blood_pressures")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp error_to_string(:too_large), do: "Too large"
  defp error_to_string(:too_many_files), do: "You have selected too many files"
  defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
end

解説

  • 基本は下記のソースを使ってます

  • mount

    • allow_uploadでアップロード許可をする拡張子を設定します
      • 他のファイル形式を許可する場合はここを修正する
  • handle_event("save", _params, socket) do

    • 画面のUploadボタンを押した時に動作します
    • consume_uploaded_entriesでファイルを取得できます
    • run(アップロードした画像のパス)でAIを実行します成功すると[最高血圧,最低血圧,脈拍]の形式で返ってきます
    • 最高血圧,最低血圧,脈拍,現在時刻をセットします
    • save_blood_pressureでDBに書き込みます
  • run(file)

    • Ollama.completionで画像解析します
    • csv形式で返します
  • get_base64_image(image_file_path

    • 画像を読み込む
    • グレースケールにする
    • jpg形式にする
    • Base64にします
  • prompt

    • AIの指示です
  • save_blood_pressure

    • DBに書き込みます
  • それ以外はサンプルのまま

アップロード画面デザイン

lib/blood_pressure_record_web/live/blood_pressure_live/upload_live.html.heex
<%!-- lib/my_app_web/live/upload_live.html.heex --%>
<%!-- use phx-drop-target with the upload ref to enable file drag and drop --%>
<Layouts.app flash={@flash}>
  <form id="upload-form" phx-change="validate" phx-submit="save">
    <.live_file_input class="btn btn-primary w-36 h-6 cursor-pointer" upload={@uploads.avatar} />
    <button type="submit" class="btn btn-primary w-36 h-6 cursor-pointer">Upload</button>
  </form>
  <div class="mt-5 h-52 w-52 bg-blue-100">
    <section phx-drop-target={@uploads.avatar.ref}>
      <%!-- render each avatar entry --%>
      <article :for={entry <- @uploads.avatar.entries} class="upload-entry">
        <figure>
          <.live_img_preview entry={entry} />
          <figcaption>{entry.client_name}</figcaption>
        </figure>

        <%!-- entry.progress will update automatically for in-flight entries --%>
        <progress value={entry.progress} max="100">{entry.progress}% </progress>

        <%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/3
                                    --%>
        <button
          type="button"
          phx-click="cancel-upload"
          phx-value-ref={entry.ref}
          aria-label="cancel"
        >
          &times;
        </button>

        <%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%>
        <p
          :for={err <- upload_errors(@uploads.avatar, entry)}
          class="alert alert-danger"
        >
          {error_to_string(err)}
        </p>
      </article>

      <%!-- Phoenix.Component.upload_errors/1 returns a list of error atoms --%>
      <p :for={err <- upload_errors(@uploads.avatar)} class="alert alert-danger">
        {error_to_string(err)}
      </p>
    </section>
  </div>
</Layouts.app>

Webサーバーを起動

$ mix phx.server

http://localhost:4000/up アクセスしてください

AI解析ができるアップロード画面を表示できます
使い方は動画を見てください

image.png

ソース(各回共通のため更新されます)

次回

血圧を記録するアプリを作る その3 〜 血圧測定の結果を元にグラフを作る 〜

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?