方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【Flask/Python】Webサイト上で目のアニメーションの検証をできるようにする

この記事は約11分で読めます。

はじめに

現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

https://mia-cat.com

ミーアの表情、特に目の動き、を検証するために、今までは、3Dプリンターで印刷した筐体に、目に相当するLCDディスプレイを当てはめて、実際にマイコン起動して目の表情を動かしてみて、その場で確認するという方法をとっていた。

ただ、それだと、検証に時間がかかるのと、協力していただけるようになったデザイナーへの共有が大変なので、2Dでwebサイト上でリモートでも検証できるようにしたいと思う。

Flaskでローカル環境でアプリケーション作成

下記のようにpythonでコード作成

HTML
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>猫の顔の表情アップロード</title>
        <!-- Bootstrap CSSの追加 -->
        <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
        <style>
            #cat-face-container {
                position: relative;
                display: inline-block;
            }
            #cat-face {
                width: 450px; /* 猫の画像の幅を設定 */
                display: block; /* 画像をブロック要素として配置 */
            }
            /* Bootstrap風のデザインのための追加スタイル */
            form {
                margin-top: 15px;
            }
        </style>
    </head>
    
<body>
    <div id="cat-face-container" class="container">
        <!-- 省略 -->
        {% if cat_image_data %}
            <!-- Base64エンコードされた画像データを表示 -->
            <img src="data:image/png;base64,{{ cat_image_data }}">
        {% else %}
            <img id="cat-face" src="{{ url_for('static', filename='mia_face.png') }}" alt="猫の顔">
        {% endif %}
        <form action="/" method="post" enctype="multipart/form-data" class="form-inline">
            <div class="form-group mb-2">
                <input type="file" name="eye_image" accept="image/png, image/jpeg, image/gif" class="form-control-file">
            </div>
            <button type="submit" class="btn btn-primary mb-2">アップロード</button>
        </form>
        
        <!-- 初期化ボタンをBootstrap風に装飾 -->
        <form action="/reset" method="get" class="form-inline">
            <button type="submit" class="btn btn-secondary mb-2">初期化</button>
        </form>
    </div>
</body>

</html>
Python
from flask import Flask, request, render_template
from PIL import Image, ImageSequence
import io
import base64

# appという名前でFlaskのインスタンスを作成
app = Flask(__name__)

cat_face_image_path = 'static/mia_face.png'

# どこのアドレスで実行するか指定
# 今回は http://127.0.0.1:5000/ にアクセスされたらindex()を実行
@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        if 'eye_image' not in request.files:
            return 'ファイルがありません', 400
        file = request.files['eye_image']

        uploaded_image = Image.open(file.stream)

        # GIF画像かどうかをチェック
        if uploaded_image.format == 'GIF':
            frames = []  # 新しいGIFのフレームを保存するリスト
            for frame in ImageSequence.Iterator(uploaded_image):
                frame = frame.convert('RGBA')
                frame = frame.resize((128, 128))  # サイズ調整

                cat_face = Image.open(cat_face_image_path)
                cat_face = cat_face.resize((450, 450))
                cat_face.paste(frame, (85, 165), frame)  # 左目の位置
                cat_face.paste(frame, (240, 165), frame)  # 右目の位置

                # 各フレームをリストに追加
                frame_byte_arr = io.BytesIO()
                cat_face.save(frame_byte_arr, format='PNG')
                frame_byte_arr.seek(0)
                frames.append(Image.open(frame_byte_arr))

            # 新しいGIFを生成
            img_byte_arr = io.BytesIO()
            frames[0].save(img_byte_arr, format='GIF', save_all=True, append_images=frames[1:], loop=0)
        else:
            uploaded_image = uploaded_image.convert('RGBA')
            uploaded_image = uploaded_image.resize((128, 128))

            cat_face = Image.open(cat_face_image_path)
            cat_face = cat_face.resize((450, 450))
            cat_face.paste(uploaded_image, (85, 165), uploaded_image)  # 左目の位置
            cat_face.paste(uploaded_image, (240, 165), uploaded_image)  # 右目の位置

            img_byte_arr = io.BytesIO()
            cat_face.save(img_byte_arr, format='PNG')

        encoded_img_data = base64.b64encode(img_byte_arr.getvalue()).decode('utf-8')
        return render_template('index.html', cat_image_data=encoded_img_data)

    return render_template('index.html')

@app.route('/reset')
def reset():
    return render_template('index.html')

if __name__ == '__main__':
    # 作成したappを起動
    # ここでflaskの起動が始まる
    app.run(debug=True)

python3 app.pyでアプリケーションを実行すると、下記のような結果になり、http://127.0.0.1:5000 にアクセスすると、routeに定義したindexファイルが開く。Flaskのデフォルトポートは5000。

Zsh
~/dev/automate/mia-eye-test  python3 app.py                                                                                                          
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 339-865-232

コード解説

画像データをHTML内で直接表示する部分

  1. img_byte_arr という名前の io.BytesIO オブジェクトを作成。画像データをバイナリ形式で一時的に保持するためのもの
  2. cat_face という、猫の筐体の画像(目がくり抜かれた部分)をロードし、その画像上にアップロードされた目の画像を合成する(Pillowライブラリの.paste() メソッドを使用)。
  3. cat_face に合成された画像が含まれた後、cat_faceio.BytesIO オブジェクトに保存。これにより、処理された画像がメモリ内でバイナリデータとして利用可能になる。
  4. base64.b64encode 関数を使用して、バイナリデータをBase64エンコード。このエンコードされたデータはテキスト文字列として取得できる。
  5. エンコードされた画像データをUTF-8エンコードして文字列に変換し、HTMLテンプレートの cat_image_data として渡す。この変数を使用して、フロントエンド側で画像を表示できる。

画像の合成はバイナリ形式のままPillowライブラリを用いる

画像の読み込み、変換、合成、保存などの操作は、バイナリ形式のままPythonのPillowライブラリを用いて行うことができる。

画像の埋め込みはBase64エンコードを用いる

ただし、画像データはバイナリ形式のままではHTML内に直接埋め込むことはできない。HTMLではテキストデータのみを直接埋め込むことができる。そのため、画像データをHTML内に表示するためには、画像データをテキストデータに変換する必要があり、そのためにBase64エンコードを使う。Base64エンコードは、バイナリ形式の画像データをテキストデータに変換する。

完成!

これで、無事ローカル環境では、目の画像をアップロードすると、猫の正面の画像の目の部分に入るようになった。GIFにも対応

ただ、このままだとローカル環境で、私しか検証できないので、FlaskアプリケーションをDocker化して、Cloud Runにデプロイを次に進める。続きはこちら。

コメント

タイトルとURLをコピーしました