📸

streamlitでベストショット判定アプリを作成し、Hugging spaceで無償公開する

2024/06/30に公開

想定読者

  • pythonを勉強されている方
  • 画像認識や表情分析に興味がある方
  • SreramlitでWebアプリを作成、公開してみたい方

はじめに

友人や家族とたくさん写真を撮った際、その中からベストショットを選ぶのはとても大変かと思います。そこで今回は、表情分析ができるPy-Featというライブラリを使って複数の写真からベストショットを自動で選んでくれるアプリをStreamlitで作成し、Hugging spaceで無償公開してみたいと思います。

公開したアプリはこちら
https://huggingface.co/spaces/shach1995/perfect_shot_analyzer

デプロイしたファイルはこちら
https://huggingface.co/spaces/shach1995/perfect_shot_analyzer/tree/main

Py-Featについて

Py-Featは、画像中の人物の顔を特定し表情分析を行うことができるライブラリです。表情から怒り、喜び、悲しみ、驚き、恐怖などの感情を数値化し、可視化までしてくれます。また、画像だけでなく動画中の人の表情も分析できるそうです。

https://py-feat.org/pages/intro.html

ベストショットの定義について

ユーザーが喜び、怒りといった各感情の数値を1~5の中で設定できるようにし、その比率に最も近い分析結果となった画像をベストショットとして選出します。これにより、最も嬉しさの値が高い写真を選べるだけでなく、嬉しさと驚きを両方の感情が混在している写真などもベストショットとして選べるようにします。

例えば、怒り=1、嫌悪=1、恐怖=1、喜しさ=5、悲しみ=3、驚き=1、真顔=1の場合、比率は以下のようになります。

怒り = 1/13 = 0.076923
嫌悪 = 1/13 = 0.076923
恐怖 = 1/13 = 0.076923
喜しさ = 5/13 = 0.38461
悲しみ = 3/13 = 0.230769
驚き = 1/13 = 0.076923
真顔 = 1/13 = 0.076923

この比率に分析結果が最も近い写真がベストショットとなります。

処理の流れ

  1. 複数の画像ファイルを格納したディクトリのzipファイルをアップロード
  2. ユーザーが設定した各感情の数値の比率を計算
  3. zipファイルを解凍し、解凍したディレクトリをあらかじめ用意したimgsディレクトリに保存
  4. 保存した画像ファイルを読み込み、表情分析を実施
  5. 各感情の比率が設定値と最も近い写真を選定し、ベストショットとする
  6. ベストショットの写真と感情分析の結果を表示

プロジェクトの構造

はじめに、HuggingFaceSpacesで、リポジトリを作成し、ローカルにクローンしてください。
基本操作については、以下の記事で丁寧に解説されておりますので、参照ください。
https://zenn.dev/paxdare_labo/articles/0ae79951afed45#huggingfacespacesでの操作

今回のリポジトリ名はperfect_shot_analyzerとしました。クローンすると、リポジトリ名のディレクトリが作成されます。ディレクトリ内の配置は、以下のようになります。imgsはアップしたzipファイルを解凍する用のディレクトリです。ただし、HuggingFaceSpacesにpushする際に、ディレクトリ内が空の場合、pushされないため、for_unzipped_file.txt(ファイル名は任意)という空ファイルを用意しておきます。

perfect_shot_analyzer
├── README.md
├── requirements.txt
├── app.py
├── emotion_analyzer.py
└── imgs (アップしたzipファイルを解凍する用のディレクトリ)
    └──  for_unzipped_file.txt (空ファイル)

開発環境

PC

OS:macOS14.4.1
プロセッサ:2.6 GHz 6コアIntel Core i7
メモリ:16 GB 2667 MHz DDR4

Python環境

Pythonのバージョンは必ず3.8.Xにしてください。ライブラリの構成がバージョン3.8.X向けとなっております。今回は、バージョン3.8.2としました。エディタはPycharm、仮想環境はvirtualenvを使用しました。

ライブラリ

requirement.txtを次の通りとし、pip install -r requirements.txtを実行してください

requirement.txt
requirement.txt
altair==5.3.0
attrs==23.2.0
av==12.0.0
blinker==1.8.2
cachetools==5.3.3
celluloid==0.2.0
certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
contourpy==1.1.1
cycler==0.12.1
easing-functions==1.0.4
filelock==3.14.0
fonttools==4.52.1
fsspec==2024.5.0
gitdb==4.0.11
GitPython==3.1.43
h5py==3.11.0
idna==3.7
imageio==2.34.1
importlib_resources==6.4.0
Jinja2==3.1.4
joblib==1.4.2
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
kiwisolver==1.4.5
kornia==0.7.2
kornia_rs==0.1.3
lazy_loader==0.4
lxml==5.2.2
markdown-it-py==3.0.0
MarkupSafe==2.1.5
matplotlib==3.7.5
mdurl==0.1.2
mpmath==1.3.0
networkx==3.1
nibabel==5.2.1
nilearn==0.10.4
nltools==0.5.1
numexpr==2.8.4
numpy==1.23.5
packaging==24.0
pandas==2.0.3
pillow==10.3.0
pipdeptree==2.23.0
pkgutil_resolve_name==1.3.10
protobuf==4.25.3
py-feat==0.6.2
pyarrow==16.1.0
pydeck==0.9.1
Pygments==2.18.0
pynv==0.3
pyparsing==3.1.2
python-dateutil==2.9.0.post0
pytz==2024.1
PyWavelets==1.4.1
referencing==0.35.1
requests==2.32.2
rich==13.7.1
rpds-py==0.18.1
scikit-image==0.21.0
scikit-learn==1.3.2
scipy==1.10.1
seaborn==0.13.2
six==1.16.0
smmap==5.0.1
streamlit==1.35.0
sympy==1.12
tenacity==8.3.0
threadpoolctl==3.5.0
tifffile==2023.7.10
toml==0.10.2
toolz==0.12.1
torch==2.2.2
torchvision==0.17.2
tornado==6.4
tqdm==4.66.4
typing_extensions==4.12.0
tzdata==2024.1
urllib3==2.2.1
xgboost==2.0.3
zipp==3.18.2

HuggingFaceSpacesの開発環境の設定

HuggingFaceSpacesでは、Pythonのバージョンがデフォルトで3.10(2024/6/25時点)のため、バージョンを3.8.2に指定する必要があります。README.mdpython_version: 3.8.2と記述することで、バージョンを指定できます。(他の項目は任意です)

README.md
---
title: Perfect Shot Analyzer
python_version: 3.8.2
emoji: 🐢
colorFrom: yellow
colorTo: gray
sdk: streamlit
sdk_version: 1.35.0
app_file: app.py
pinned: false
---

Pythonプログラムの作成

step1では、表情分析を行うクラス、step2ではメインプログラムを記述していきます。

step1:表情分析

まずは、必要なライブラリ(pandasとPy-Feat)をインストールします。Py-FeatのDetectorクラスもここでインスタンス化しておきます。

emotion_analyzer.py
import pandas as pd
from feat import Detector

# 検出器の定義
detector = Detector()

次に画像内の人の表情を分析し、各感情を数字化するEmotionAnalyzerクラスを作成していきます。アップロードされた画像ファイルを取得し、create_emotion_dataflameで1枚ずつ分析し、画像分の結果をデータフレームdf_emotion_resultに格納します。なお、画像ファイルが格納されたzipファイルをアップロード、解凍する処理はstep2で作成するメインプログラムにて記述します。

emotion_analyzer.py
class EmotionAnalyzer:
    def __init__(self, img_file_path_list):
        self.img_file_path_list = img_file_path_list

    def create_emotion_dataflame(self):
        """
        イメージファイルの分析結果をデータフレーム化
        """
        emotions_deg_list = []
        result_detections_list = []
        for img_file_path in self.img_file_path_list:
            result_emotions_mean, result_detections = self.analyze_emotion(img_file_path)
            emotions_deg_list.append(result_emotions_mean)
            result_detections_list.append(result_detections)

        df_emotion_result = pd.DataFrame(emotions_deg_list, index=self.img_file_path_list)
        df_emotion_result['detections'] = result_detections_list

        return df_emotion_result

表情分析はanalyze_emotionで行います。resultには画像内に写っている全員分の表情分析の結果や特定された画像中の顔の座標などが格納されます。続いて、result.emotionsとすることで、表情分析の結果のみを抽出できます。さらに、mean()を付けることで、全員の各感情の平均値を計算し、返り値とします。

emotion_analyzer.py
    @staticmethod
    def analyze_emotion(img_file_path):
        """
        イメージファイルから感情を分析
        """
        # 検出器の定義
        detector = Detector()
        result = detector.detect_image(img_file_path)
        result_emotions_mean = result.emotions.mean()
        return result_emotions_mean, result

result_emotions_meanはdict型で、中身は以下の通りです。

result_emotions_mean = {
    "anger": 0.003573,
    "disgust": 0.000065,
    "fear": 0.001937,
    "happiness": 0.713879,
    "sadness": 0.001336,
    "surprise": 0.272220,
    "neutral": 0.006990
}

step2:メイン処理

まずは、ライブラリのインポートとst.titleアプリ名を設定します

app.py
import sys
import os
import glob

import numpy as np
import streamlit as st
from PIL import Image
import zipfile
from emotion_analyzer import EmotionAnalyzer

st.title('ベストショット判定アプリ')

ユーザーがベストショットの条件を定義できるようにするため、各感情の数値を設定できるようにします。数値はスライダーを動かすこと設定できるようにし、スライダーの位置はサイドバーとしました。値は1~5まで選べるようしており、デフォルト値は「嬉しさ」だけ5とし、それ以外は1としています。

app.py
st.sidebar.markdown('### 条件設定')
sld_anger_deg = st.sidebar.slider('怒り', 1, 5, 1)
sld_disgust_deg = st.sidebar.slider('嫌悪', 1, 5, 1)
sld_fear_deg = st.sidebar.slider('恐怖', 1, 5, 1)
sld_happiness_deg = st.sidebar.slider('喜しさ', 1, 5, 5)
sld_sadness_deg = st.sidebar.slider('悲しみ', 1, 5, 1)
sld_surprise_deg = st.sidebar.slider('驚き', 1, 5, 1)
sld_neutral_deg = st.sidebar.slider('真顔', 1, 5, 1)

設定された各感情の比率を計算します。

app.py
# 各変数の比率を計算
sld_total_deg = (sld_anger_deg + sld_disgust_deg + sld_fear_deg +
                 sld_happiness_deg + sld_sadness_deg +
                 sld_surprise_deg + sld_neutral_deg)

emotion_ratios = {
    'anger': sld_anger_deg / sld_total_deg,
    'disgust': sld_disgust_deg / sld_total_deg,
    'fear': sld_fear_deg / sld_total_deg,
    'happiness': sld_happiness_deg / sld_total_deg,
    'sadness': sld_sadness_deg / sld_total_deg,
    'surprise': sld_surprise_deg / sld_total_deg,
    'neutral': sld_neutral_deg / sld_total_deg
}

アップローダを用意します。アップされたzipファイルをimgsディレクトリに解凍後、解凍された画像ファイルのパスをimg_file_path_listに格納します。

app.py
image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.bmp']
extract_dir = 'imgs'

# アップローダー
uploaded_file = st.file_uploader('ZIPファイルをアップロードしてください', type='zip')

# イメージファイルのパスを格納するリスト
img_file_path_list = []

if uploaded_file:
    # 一時ディレクトリを作成してZIPファイルを解凍
    with zipfile.ZipFile(uploaded_file, 'r') as zip_ref:
        zip_ref.extractall(extract_dir)

    # 解凍したファイル・ディレクトリの一覧を取得
    extracted_items = os.listdir(extract_dir)
    # zipファイルのベースファイルを取得
    uploaded_file_base_name = uploaded_file.name.split('.zip')[0]
    # 解凍したディレクトリのパスを取得
    extract_dir_path = os.path.join(extract_dir, uploaded_file_base_name)
    # 各拡張子に対してglobを使用してファイルを取得
    for ext in image_extensions:
        img_file_path_list.extend(glob.glob(os.path.join(extract_dir_path, ext)))

    if img_file_path_list:
        st.write(f'{len(img_file_path_list)}枚のイメージファイルが見つかりました')
    else:
        st.write('イメージファイルが見つかりませんでした')
        st.write('再度アップロードしなおしてください')
        sys.exit()

表情分析開始ボタンをクリック後に、step1で作成したEmotionAnalyzerを呼び出し、各画像の表情分析を行い、df_emotionsに格納します。
次に、np.sqrtの部分で、スライダーで設定した各感情の値と分析結果との差分の二乗和を計算し、df_emotionsに'distance'というカラム名で計算結果を格納します。すなわち、'distance'が小さいほど、分析結果の値が設定値と最も近いことになり、ベストショットとして定義します。ベストショットとして選出された画像の分析結果をbest_image_recordに格納しています。

app.py
    if st.button('表情分析開始'):
        # 画像を指定して表情認識を実行
        with (st.spinner('表情分析中...')):
            ema = EmotionAnalyzer(img_file_path_list)
            df_emotions = ema.create_emotion_dataflame()

            # 各表情の値と設定値との二乗和を取り、最も値が小さい(設定値に近い)写真をベストショットをする
            df_emotions['distance'] = \
                np.sqrt(sum((df_emotions[emotion] - ratio) ** 2 for emotion, ratio in emotion_ratios.items()))
            best_image_record = df_emotions.loc[df_emotions['distance'].idxmin()]

最後に、ベストショットの写真とその分析結果を表示します。

app.py
            # 保存した画像を表示
            result_detection = df_emotions.loc[best_image_record_filename]['detections']
            fig_list = result_detection.plot_detections(faces='landmarks', faceboxes=True, muscles=False, poses=False,
                                                        gazes=False,
                                                        add_titles=True,
                                                        au_barplot=False, emotion_barplot=True,
                                                        plot_original_image=True)
            st.markdown('ベストショットはこちら!')

            # 保存した画像を表示
            img = Image.open(best_image_record_filename)
            st.image(img, width=500)

            # 分析結果を表示
            st.markdown('分析結果')
            st.pyplot(fig_list[0])

アプリの実行

ローカルでの動作確認

次のコマンドを実行してStreamlitを起動させます。

streamlit run app.py

はじめに画像ファイルを格納したディレクトリを用意し、zipファイルを作成します。
画像は、写真AC様のフリーの写真を使用しました。

https://www.photo-ac.com/

初期画面はこちら

zipファイルをアップロードします。

サイドバーのスライダーで感情の比率を設定、表情分析開始ボタンをクリックし、分析を開始します。

設定値に最も近い写真がベストショットとして表示されます。複数人でも全員問題なく分析できています。

続いて、設定を変更して再度分析を開始します。

別の写真がベストショットとして選出されました。

ローカルでの動作確認できましたら、HuggingFaceSpacesにPushします。
Pushの方法については、こちらの記事を参考にしてください。
https://zenn.dev/paxdare_labo/articles/0ae79951afed45#huggingfacespacesにpush

まとめ

今回は表情分析を用いたベストショットを選出するアプリを作成しました。本記事を通して、pythonによるWebアプリ作成や物体検出の学習の一助になれば幸いです。

Discussion