こんにちは!AIエンジニア兼バックエンドエンジニア兼プロダクトオーナーを担当している植草です!
突然ですが、皆さんPython使っていますか?画像処理や機械学習を実装する場合、ライブラリが充実しているPythonはサクッと実装できて便利ですよね。Pythonだけで良い感じのGUIを構築できる Flet
を紹介します!
3行で
- FletはPythonでイケてるGUIを手軽に作成できるライブラリだよ
- Pythonなので、潤沢な機械学習や画像処理のライブラリの恩恵をフルに得られるよ
- FletとRembgというライブラリを使ってAI(学習済みモデル)を用いた簡単なGUIアプリを作る例を紹介するよ
はじめに: Fletとは?
flet.dev FletはPythonでGUIを構築できるライブラリです。私が触ったことのあるライブラリは
- PySimpleGUI
- Streamlit
- Flet
の3種ですが、Fletは他と比べて以下のような特徴があります。
- イケてるGUIを手軽に作れる
- 手軽さの割にレイアウトや制御の自由度が高い
- 公式のドキュメントが非常に充実している
イケてる度、実装の手軽さ、制御の自由度のバランスが抜群に良く、実際にプロトタイプの実装にもガッツリと利用しています。ドキュメントが充実している点も素晴らしく、ChatGPT 4oよりも頼りになります。 Pythonという言語がプロダクトとして配布する上で適しているかの判断は必要ですが、少なくとも画像処理やデータ分析、機械学習が絡むプロジェクトの仮説検証フェーズにおいては非常に有用なライブラリだと言えます!爆速でアプリを作って仮説検証を回すことができます!
以降では学習済みのAIを用いた簡単なアプリケーションの実装をハンズオンで説明します。
実際にFletを使ってみよう!
作成するアプリケーション
Rembg
というライブラリを用いたアプリケーションを作成します。 Rembg
は入力された画像の前景と背景を認識し、背景を除去してくれるモデルを使用することができます。画像上の座標上を指定すると、その部分を前景として認識させることも可能です(一部のモデルのみ)。詳細は以下のリンクを確認してください。
github.com
- ユーザーが画像を選択し、それを表示する
- ユーザーが表示された画像をクリックし、クリックした箇所を前景として、背景除去した画像を表示する
というアプリケーションを作ってみることにします!開発環境はWindows11でデスクトップアプリとします。
とりあえず動かすところまで
0. 必要パッケージのインストール
まずはプロジェクトを作成し、必要なパッケージをインストールします。パッケージ管理にはpoetryを用います。
プロジェクトの作成
$ poetry init This command will guide you through creating your pyproject.toml config. Package name [fletblog]: flet-introduction Version [0.1.0]: Description []: Author [kohchan0913 <koji.uekusa@adacotech.co.jp>, n to skip]: kohchan0913 License []: Compatible Python versions [^3.9]: >=3.9,<3.13 Would you like to define your main dependencies interactively? (yes/no) [yes] no Would you like to define your development dependencies interactively? (yes/no) [yes] no Generated file [tool.poetry] name = "flet-introduction" version = "0.1.0" description = "" authors = ["kohchan0913"] readme = "README.md" [tool.poetry.dependencies] python = ">=3.9,<3.13" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" Do you confirm generation? (yes/no) [yes] yes $
続いて必要なパッケージのインストール。
poetry add flet poetry add rembg poetry add numpy<2.0.0 poetry add --group dev ruff poetry add --group dev mypy
※ruffとmypyは静的解析用なので任意です。
1. 配置を決める
UIとして用意すべき部品は大きく以下の3点です。
- 画像選択用ボタン
- 元画像
- 背景除去を行った画像
それぞれの部品の配置を決めていきます。こんなコードを書いてみましょう!
"""main.py""" import flet as ft def main(page: ft.Page) -> None: """メイン関数""" # 画像アップロードボタン upload_button = ft.ElevatedButton("画像アップロードボタン") # オリジナル画像を配置する領域 origin_image = ft.Container( content=ft.Text("オリジナル画像", color="black"), bgcolor="orange", width=500, height=500, alignment=ft.alignment.center, ) # 背景除去画像を配置する領域 bg_removed_image = ft.Container( content=ft.Text("背景除去後の画像", color="black"), width=500, height=500, bgcolor="blue", alignment=ft.alignment.center, ) main_view = ft.Column( controls=[ upload_button, ft.Row( controls=[ origin_image, bg_removed_image, ] ), ] ) page.add(main_view) if __name__ == "__main__": ft.app(target=main)
実行
poetry run flet run .\main.py
(私のPCはダークモードになっているため、背景が黒っぽくなっています)
【ソースコードの解説】
重要な点は Column
Row
Container
です!この3つを覚えておけばレイアウトは自由にできるようになります。
Column
はcontrols
にリストで与えられた部品を縦に配置していくことができます。(今回は2つ)Row
はcontrols
にリストで与えられた部品を横に配置していくことができます。(今回は2つ)
Container
は content
に与えられた部品に対して装飾を行うことができます。このコードでは画像を配置する場所を分かりやすくために色を塗ってみました。今回アップロードボタンは特に装飾する気が無いので Contener
の中に入れていませんが、「とりあえずContainerの中に入れとけ!」という方針も問題ありません。
Container
の content
や Colum
の controls
に与えているUIの部品をFletでは Controlと呼びます。これからControlを充実していくことになります。
なお、定義したControlは以下のように ft.Page
オブジェクトに add
しないと画面に反映されないので注意してください。親となるControlを指定すれば自動的に関連するControlも反映されます。
2. アップロード~画像の表示
アップロードボタンを使ってユーザーの画像を読み込ませてみましょう。デスクトップアプリケーションとして作っているので画像のファイルパスが分かればOKです。以下のような機能が実現できれば良さそうですね。
- クリックしたときあとエクスプローラーが開き、ファイルを選択させる
- 選択したファイルのパスを読み込む
エクスプローラーを開くには ft.FilePicker
を使います。ポイントは以下2点です
ft.FilePicker
をft.ElevatedButton
のクリックイベントに指定することでエクスプローラーを開くことができる。- ユーザーがファイル選択した後は
ft.FilePicker
のon_result
に登録に指定した関数が発火する。(パスの取得方法はコードを見て頂くのが早いです)
まずはファイルパスを取得できたことを確認するために print
してみましょう!
# 画像アップロードボタン def upload_image(e: ft.FilePickerResultEvent) -> None: """ファイルアップロード時のイベント""" if not e.files: # キャンセルした時の挙動 return image_path = e.files[0].path print(image_path) file_picker = ft.FilePicker(on_result=upload_image) upload_button = ft.ElevatedButton( "画像アップロードボタン", on_click=lambda _: file_picker.pick_files( allow_multiple=False, allowed_extensions=["png", "jpg", "jpeg", "bmp"] ), ) (中略) page.add(file_picker) # 忘れずに
これでファイル選択が可能になりました。ファイル選択後コンソール上で以下のようにパスが表示できていればOKです。
次に取得した画像パスから画像表示をしてみましょう。画像の表示には ft.Image
を用います。パスを与えてあげるだけでOKです。base64エンコードした画像を与えることも可能です(後で使います)。 fit
を指定しておくと与えられた領域( ここでは ft.Container
)に合わせて自動的に画像をリサイズしてくれます。指定しておかないと画像サイズによってはレイアウトやアスペクト比が壊れてしまうので指定しておくのが無難です。(公式も指定しとけと推奨しています。)
Controlは update
メソッドが実行されたタイミングで表示に反映されるので忘れないようにしましょう。
# オリジナル画像を配置する領域 VIEW_IMAGE_WIDTH = 500 VIEW_IMAGE_HEIGHT = 500 origin_image_empty_text = ft.Text("画像をアップロードしてください") origin_image = ft.Container( content=origin_image_empty_text, width=VIEW_IMAGE_WIDTH, height=VIEW_IMAGE_HEIGHT, alignment=ft.alignment.center, ) def show_origin_image(file_path: str) -> None: """画像を表示する""" origin_image.content = ft.Image(src=file_path, fit=ft.ImageFit.FIT_HEIGHT) origin_image.update() # 忘れずに
show_origin_image
関数を画像のファイルパスを取得したタイミングで呼んであげるようにしましょう。
# 画像アップロードボタン def upload_image(e: ft.FilePickerResultEvent) -> None: """ファイルアップロード時のイベント""" if not e.files: # キャンセルした時の挙動 return image_path = e.files[0].path show_origin_image(image_path)
こんな感じで画像を表示できるようになったはずです! ※人工知能学会が気になる方はこちらもご覧ください techblog.adacotech.co.jp
3. 画像をクリックした時に座標を取得する
次は画像をクリックした際に座標を取得するロジックを作成していきます。以下の処理の流れを作ることが目標になります。UI上における座標系と画像上における座標系は異なることに注意が必要です。
- クリックイベントを検知する
- クリックイベントの情報からUI上の座標を取得する
- UI上の座標から画像上の座標に変換する
クリックイベントを検知する方法は複数ありますが、座標を取得する場合は ft.GestureDetector
を使うのが良いです。このControlはドラッグイベントなどの検知することが可能です。
クリックイベントの検知~UI上の座標を取得するところまで実装してみます。画像を表示する時点で ft.GestureDetector
を仕込んでおきます。
def on_image_click(e: ft.TapEvent) -> None: """画像クリック時の処理""" print(e.local_x, e.local_y) def main(page: ft.Page) -> None: """メイン関数""" (中略) def show_origin_image(file_path: str) -> None: """画像を表示する""" image_content = ft.Image(src=file_path, fit=ft.ImageFit.FIT_HEIGHT) origin_image.content = ft.GestureDetector( mouse_cursor=ft.MouseCursor.BASIC, on_tap_down=on_image_click, content=image_content, ) origin_image.update()
これで画像をクリックするとコンソール上で座標が表示されるようになったはずです。
更に on_image_click
でui上の座標から画像上の座標を取得するロジックを書いてみます。少々ややこしいので、コード中にコメントを記載しています。
from PIL import Image def on_image_click(e: ft.TapEvent, assigned_width: float, assigned_height: float, image_path: str) -> None: """画像クリック時の処理""" image = Image.open(image_path) # UI上に割り当てられたアスペクト比 ui_aspect = assigned_width / assigned_height # 画像のアスペクト比 image_aspect = image.width / image.height if image_aspect > ui_aspect: # 画像が横長 # widthは割り当てられた領域いっぱい利用し # heightはwidthと画像のアスペクト比から計算する ui_width = assigned_width ui_height = ui_width / image_aspect else: # 画像が縦長 # heightは割り当てられた領域いっぱい利用し # widthはheightと画像のアスペクト比から計算する ui_height = assigned_height ui_width = ui_height * image_aspect # UI上の座標 ui_point_x = e.local_x ui_point_y = e.local_y # 画像上の座標 image_point_x = image.width * ui_point_x / ui_width image_point_y = image.height * ui_point_y / ui_height print(image_point_x, image_point_y)
引数が複数になってしまいました。この場合はlambda
式を使って以下のように書くことができます。
def show_origin_image(file_path: str) -> None: """画像を表示する""" image_content = ft.Image(src=file_path, fit=ft.ImageFit.FIT_HEIGHT) origin_image.content = ft.GestureDetector( mouse_cursor=ft.MouseCursor.BASIC, on_tap_down=lambda e: on_image_click(e, VIEW_IMAGE_WIDTH, VIEW_IMAGE_HEIGHT, file_path), content=image_content, ) origin_image.update()
これで画像クリック時に画像上の座標としてコンソールに出力されるようになりました。画像の左上の方をクリックすると (0, 0)
、画像の右下の方をクリックすると (画像のwidth, 画像のheight)
が出力されていれば想定通りです。
4. 背景除去した画像を表示する
いよいよ Rembg
を使って選択した領域を前景として背景領域を削除する処理を作ります。画像と座標を与えて背景除去する関数を準備しておきます。
from rembg import new_session, remove (中略) def remove_bg(image: Image.Image, point_x: int, point_y: int) -> Image.Image: """背景除去処理""" sam_prompt = [ { "type": "point", "data": [point_x, point_y], "label": 1, } ] kwargs = { "post_process_mask": True, "sam_prompt": sam_prompt, "bgcolor": (255, 255, 255, 0), "session": new_session("sam"), } return remove(image, **kwargs)
この関数を用いて、一連の処理の流れとしては以下となります。
- クリックイベントから画像上の座標を取得(対応済み)
- 画像と座標を
remove_bg()
に投げて背景を除去した画像を取得 - 取得した画像をUIに反映する
全てクリックイベントに起因しますので、 on_image_click()
を拡張すれば良さそうです。
import base64 from io import BytesIO (中略) def on_image_click( e: ft.TapEvent, assigned_width: float, assigned_height: float, image_path: str, dist_view: ft.Container, # 追加 ) -> None: """画像クリック時の処理""" image = Image.open(image_path) (中略) # 画像上の座標 image_point_x = image.width * ui_point_x / ui_width image_point_y = image.height * ui_point_y / ui_height # 背景除去 print("runnning...") res_image = remove_bg(image, int(image_point_x), int(image_point_y)) # PIL.Imageをbase64エンコード buff = BytesIO() res_image.save(buff, "png") res_base64 = base64.b64encode(buff.getvalue()).decode("utf-8") # UIの更新 res_content = ft.Image(src_base64=res_base64, fit=ft.ImageFit.FIT_HEIGHT) dist_view.content = res_content dist_view.update()
UIの更新を行う必要があるため、引数に dist_view
が追加されました。背景除去した画像をbase64エンコードし、画像を表示した時の要領でUIを更新しています。
また引数が変わってしまったので、 show_origin_image
関数を以下のように変更します。
# 背景除去画像を配置する領域 bg_removed_image = ft.Container( content=ft.Text("背景除去後の画像", color="black"), width=500, height=500, bgcolor="white", alignment=ft.alignment.center, ) def show_origin_image(file_path: str) -> None: """画像を表示する""" image_content = ft.Image(src=file_path, fit=ft.ImageFit.FIT_HEIGHT) origin_image.content = ft.GestureDetector( mouse_cursor=ft.MouseCursor.BASIC, on_tap_down=lambda e: on_image_click( e, VIEW_IMAGE_WIDTH, VIEW_IMAGE_HEIGHT, file_path, bg_removed_image, # 追加 ), content=image_content, ) origin_image.update()
動かしてみると以下のようになります。処理には多少時間かかるので少しお待ちください。
まとめ
ハンズオン形式で簡単なプロトタイプが動かすところまで解説しました。「0からGUI付きで機械学習モデルを動かす」という、それなりに難度が高い仕様でしたが、簡単に実装できてしまいました。Pythonの強みであるライブラリの力を上手く活用できていますね!Fletは爆速でプロトタイプを作り仮説検証のサイクルを回すのに強力なライブラリだと感じて頂けたなら幸いです。
アダコテックでは実際にFletを用いていくつかのプロダクトアイディアの仮説検証を行っています!
なんだかコードが整理されていない気がするから何とかしたい?続編でそのあたりの改善についての提案をしたいと思います。 待ちきれない方は以下の記事がとても参考になります! qiita.com
メンバー募集中です!!
アダコテックでは画像処理や機械学習を用いたプロダクトを作っています。我々の事業やプロダクトに興味がある方は、ぜひカジュアル面談にいらしてください! herp.careers