「血圧を記録するアプリを作る」シリーズです
前回は「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でアップロード許可をする拡張子を設定します
- 他のファイル形式を許可する場合はここを修正する
- 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"
>
×
</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>
-
基本は下記のソースを使ってます
-
差分
-
<Layouts.app flash={@flash}>を追加- これは各画面共通部分です
-
<div class="mt-5 h-52 w-52 bg-blue-100">を追加- 画像が画面いっぱいになるため、高さと幅を制限かけました
- それ以外はサンプルのまま
-
Webサーバーを起動
$ mix phx.server
http://localhost:4000/up アクセスしてください
AI解析ができるアップロード画面を表示できます
使い方は動画を見てください
ソース(各回共通のため更新されます)
次回
血圧を記録するアプリを作る その3 〜 血圧測定の結果を元にグラフを作る 〜
