アダコテック技術ブログ

株式会社アダコテックの技術ブログです。

Pythonだけでフロントエンド経験ゼロから爆速でGUIアプリケーションを構築する【Flet】

こんにちは!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つを覚えておけばレイアウトは自由にできるようになります。

  • Columncontrols にリストで与えられた部品を縦に配置していくことができます。(今回は2つ)
  • Rowcontrols にリストで与えられた部品を横に配置していくことができます。(今回は2つ)

Containercontentに与えられた部品に対して装飾を行うことができます。このコードでは画像を配置する場所を分かりやすくために色を塗ってみました。今回アップロードボタンは特に装飾する気が無いので Contener の中に入れていませんが、「とりあえずContainerの中に入れとけ!」という方針も問題ありません。

ContainercontentColumcontrols に与えているUIの部品をFletでは Controlと呼びます。これからControlを充実していくことになります。

なお、定義したControlは以下のように ft.Page オブジェクトに add しないと画面に反映されないので注意してください。親となるControlを指定すれば自動的に関連するControlも反映されます。

2. アップロード~画像の表示

アップロードボタンを使ってユーザーの画像を読み込ませてみましょう。デスクトップアプリケーションとして作っているので画像のファイルパスが分かればOKです。以下のような機能が実現できれば良さそうですね。

  • クリックしたときあとエクスプローラーが開き、ファイルを選択させる
  • 選択したファイルのパスを読み込む

エクスプローラーを開くには ft.FilePicker を使います。ポイントは以下2点です

  • ft.FilePickerft.ElevatedButton のクリックイベントに指定することでエクスプローラーを開くことができる。
  • ユーザーがファイル選択した後は ft.FilePickeron_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上における座標系と画像上における座標系は異なることに注意が必要です。

  1. クリックイベントを検知する
  2. クリックイベントの情報からUI上の座標を取得する
  3. 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)

この関数を用いて、一連の処理の流れとしては以下となります。

  1. クリックイベントから画像上の座標を取得(対応済み)
  2. 画像と座標を remove_bg() に投げて背景を除去した画像を取得
  3. 取得した画像を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