🎈

ブラウザで動くリアルタイム画像/音声処理アプリをStreamlitでサクッと作る

17 min read

Overview

画像/音声処理をリアルタイムで行う、Webブラウザから利用できるアプリをStreamlitで作る方法を解説します。

StreamlitのおかげでPythonだけでwebアプリが作れます。さらに、一番簡単な例なら10行程度のPythonコードで、webカメラを入力にしてブラウザから利用できるリアルタイム画像処理アプリケーションになります。

Webベースなのでクラウドにデプロイでき、ユーザに簡単に共有して使ってもらえ、UIもイマドキで綺麗です。

人物・物体検知、スタイル変換、画像フィルタ、文字起こし、ビデオチャット、その他様々な画像・音声処理の実装アイディアをデモ・プロトタイピングするのになかなかハマる技術スタックではないでしょうか。


Webブラウザから利用できる物体検知デモの例。実行中に閾値をスライダーで変えられる。オンラインデモ🎈


同様にスタイル変換デモの例。実行中にモデル切り替えやパラメータ調整ができる。オンラインデモ🎈

他のサンプルも気になる方はサンプル集へどうぞ。

これらのデモアプリはクラウドサーバ(Streamlit Cloud)にホストされており、動画・音声はサーバに送信されて処理されます。データは保存されず、全てメモリ上で処理され破棄されますが、気になる方は利用を控えてください。
本稿の以下の内容はローカルで実行しますし、上記デモもローカルにホストして試す方法を後述します。

なぜwebアプリか

従来、画像処理のリアルタイムデモを作る際にはOpenCVを利用することが多かったと思います。
以下のようなコードは親の顔より見たというCV系研究者・開発者の方は多いのではないでしょうか(音声系はどうなんでしょう、すみません詳しくなくて)。

import cv2

cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()

    img = cv2.Canny(frame, 100, 200)  # 何か画像処理

    cv2.imshow('frame', img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

このような cv2.VideoCapturecv2.imshow を利用したローカルで動作するGUIアプリと比べて、Webベースのアプリには以下のようなメリットがあります。

  • 共有・実行が簡単
    • クラウドにデプロイすればURLを共有するだけでユーザに使ってもらえます。
    • ユーザはWebブラウザでアクセスするだけでアプリを利用できます。デモのための環境構築は不要です。
  • スマホからでも試せる
    • 上と関連しますが、webブラウザがあれば動くのでスマホでも使えます。手軽に持ち運べるデバイスでデモができるのは便利です。
  • 操作・データ入力が簡単
    • Webアプリなので、テキスト入力やスライダーなどを利用できます[1]。ユーザにとっても最近はネイティブアプリよりwebベースのUIの方が馴染みがあると思います。

作ってみよう

実際に簡単なwebベースリアルタイム画像処理アプリを作っていきます。Pythonコード10〜20行程度の規模です。
Webカメラ(とマイク)を利用できる環境で作業してください。

先に完成品を見たい方はこちらをどうぞ。クラウド上のデモはこちら🎈

このチュートリアルではapp.pyにコードを書いていきます。空のapp.pyを用意します。

$ touch app.py

必要なパッケージのインストール

このチュートリアルで利用するパッケージをインストールします。

$ pip install -U streamlit streamlit-webrtc opencv-python-headless
  • streamlit: Streamlit本体
  • streamlit-webrtc: Streamlitでリアルタイム映像・音声を扱うコンポーネント
  • opencv-python-headless: OpenCV。UIはStreamlitで作るのでOpenCVはheadless版にします

初めてのStreamlit

Streamlitを使ったことがある人はこの章は飛ばして大丈夫です。

一度Streamlitを立ち上げてみましょう。app.pyと同じディレクトリで以下のコマンドを実行します。

$ streamlit run app.py

しばらくするとStreamlitサーバプロセスが立ち上がるので、http://localhost:8501 にアクセスします(もしくは、デフォルト設定では自動でブラウザが開きます)。

以下のようなwebページが表示されているはずです。私はダークモードを使っているので背景色が黒ですが、ノーマルモードでは白基調のUIとなります。
現時点ではapp.pyは空なのでwebページの内容も空です。これからStreamlitアプリのコードをapp.pyに記述していきます。

エディタでapp.pyを開き、以下のコードを記述します。

import streamlit as st

st.title("My first Streamlit app")
st.write("Hello, world")

内容を保存すると、ファイルの変更をStreamlitが検知して、webページの右上に"Rerun"と"Always rerun"の2つのボタンが表示されます。

"Rerun"を押すとページが更新され、以下のような内容に変わります。app.pyに記述した通りの内容が表示されていることがわかります。

また、"Always rerun"を押しておくと、今後ファイルに更新があるたびに自動でページが再読み込みされるようになります。

以後、app.pyを書き換えたら同様に再読み込みを行ってください。

これがStreamlitでwebアプリを開発する際の基本的な流れです。
Pythonでst.title()st.write()を呼び出すコードを書きstreamlit runコマンドに渡すと、対応するコンテンツを持ったwebページを表示してくれます。

さて、本稿のテーマはリアルタイム画像処理アプリケーションの構築ですので、ここからはそちらに進んでいきます。
一方でStreamlit自体は機械学習・データサイエンスなどのユースケースを幅広くカバーしており、それらに関しては公式チュートリアルなどをご覧ください。

リアルタイム映像・音声ストリーミングコンポーネントの導入

app.pyを以下のように更新します。

import streamlit as st
from streamlit_webrtc import webrtc_streamer

st.title("My first Streamlit app")
st.write("Hello, world")

webrtc_streamer(key="example")

Webアプリの画面は以下のようになります。新しいコンポーネントwebrtc_streamer(key="example")が追加されました。

(初回実行時にはstreamlit_webrtcのコンパイルに時間がかかってしばらく"running"状態の表示が続くかもしれません。しばらく待てば大丈夫です)

"START"ボタンを押すとしばらくスピナーが回った後に映像と音声が流れ始めます。初回はカメラとマイクへのアクセス許可を求められるかもしれません。その場合は許可してください。

ここで呼び出したwebrtc_streamer(key="example")は、Streamlitアプリケーションでwebブラウザを介した映像・音声の入出力を扱うコンポーネントです。
key引数は、app.pyの中でこのコンポーネントの呼び出しを一意に特定するIDとして機能します。ここでは"example"としましたが、任意の文字列を指定すれば大丈夫です。
今はwebカメラとマイクから映像・音声を入力し、そのまま出力するだけの動作です。これを基本として、ここからkey以外の引数を足して肉付けしていきます。

リアルタイム画像処理アプリケーションの構築

app.pyを以下のように書き換えます。

import streamlit as st
from streamlit_webrtc import webrtc_streamer
import av
import cv2

st.title("My first Streamlit app")
st.write("Hello, world")


class VideoProcessor:
    def recv(self, frame):
        img = frame.to_ndarray(format="bgr24")

        img = cv2.cvtColor(cv2.Canny(img, 100, 200), cv2.COLOR_GRAY2BGR)

        return av.VideoFrame.from_ndarray(img, format="bgr24")


webrtc_streamer(key="example", video_processor_factory=VideoProcessor)

同様に"START"ボタンを押して実行してみましょう。今度は映像にフィルタがかかって表示されます。

フレームを受け取ってフレームを返すコールバックVideoProcessor#recv()を定義し、その中に画像処理(ここではエッジ検出)のコードを仕込みました。直感的に何をやっているかわかると思います。

詳細なコードの解説は以下です。

  • webrtc_streamer()は、.recv()メソッドを持ったクラスをvideo_processor_factory引数に取れます[2]
  • .recv()は画像フレームを受け取り、画像フレームを返します。これらはPyAVライブラリのVideoFrameクラスのインスタンスです。
    • PyAVはffmpegのPythonバインディングで、映像・音声を扱うための機能を提供します。streamlit-webrtcの依存としてインストールされます。
  • .recv()の引数frameはwebカメラから入力された映像ストリーム中の画像フレームです。frame.to_ndarray()でNumPy配列に変換できます。
  • .recv()から返したVideoFrameオブジェクトが画面に表示されます。このサンプルではav.VideoFrame.from_ndarray(img, format="bgr24")でNumPy配列から新しいVideoFrameオブジェクトを生成しています[3]
  • 従って、.recv()のフレーム入出力の間に任意の画像処理を挟むことができます。ここでは例としてエッジ検出フィルタcv2.Canny(img, 100, 200)(とグレースケールをBGRに戻すためのcv2.cvtColor(img, cv2.COLOR_GRAY2BGR))を利用しました。

これで、webブラウザから利用できる、画像処理アプリケーションを作ることができました!
この例ではシンプルな例としてCannyエッジ検出フィルタを使いましたが、この部分を任意の画像処理に差し替えることができます。

物体検出やスタイル変換のコードをここに差し込むと冒頭に貼ったスクリーンショットのようなアプリになります[4]

ユーザ入力を受け取る

app.pyをさらに以下のように更新します。

import streamlit as st
from streamlit_webrtc import webrtc_streamer
import av
import cv2

st.title("My first Streamlit app")
st.write("Hello, world")


class VideoProcessor:
    def __init__(self) -> None:
        self.threshold1 = 100
        self.threshold2 = 200

    def recv(self, frame):
        img = frame.to_ndarray(format="bgr24")

        img = cv2.cvtColor(cv2.Canny(img, self.threshold1, self.threshold2), cv2.COLOR_GRAY2BGR)

        return av.VideoFrame.from_ndarray(img, format="bgr24")


ctx = webrtc_streamer(key="example", video_processor_factory=VideoProcessor)
if ctx.video_processor:
    ctx.video_processor.threshold1 = st.slider("Threshold1", min_value=0, max_value=1000, step=1, value=100)
    ctx.video_processor.threshold2 = st.slider("Threshold2", min_value=0, max_value=1000, step=1, value=200)

実行し"START"ボタンを押すとスライダーが2個現れます。
このスライダーを操作してcv2.Canny()のパラメータを調整できます。
映像ストリームの実行中にリアルタイムでパラメータを変更できるのがわかります。

コードの解説です。

  • VideoProcessor.threshold1.threshold2を加えました。.recv()の内部ではこれらがcv2.Canny()のパラメータとして使われます。
  • webrtc_streamer()はコンテキストオブジェクトを返すので、それをctxに保持するようにしました。
  • webrtc_streamer()video_processor_factoryに渡したクラスは、映像ストリーム開始時に内部でインスタンス化され、ctx.video_processorにセットされます。
    • 映像が流れていない時はセットされないので、ifで存在チェックをしています。
  • ctx.video_processorVideoProcessorのインスタンスがセットされるので、.threshold1.threshold2にアクセスできます。スライダーの値をこれらにセットします。これで、スライダーでcv2.Canny()のパラメータの値を調整できる機構ができました。
    • 値を直接渡すのではなく、このようなインスタンス変数を介した仕組みになっている理由は後述します
  • スライダーはst.slider()で作ります。これはStreamlitの組み込みウィジェットです

.recv()の呼び出しモデルと注意点

streamlit-webrtcを利用した実装では、画像処理のコードはコールバックメソッド.recv()の中に置かれることになります。
これはwhileループを利用するOpenCVのコードと大きく異なる点です。

また、.recv()はフォークした別スレッドで実行されます。この点には注意しておいてください。

関連して、.recv()内部では以下の制約があります。

  • globalは意図した通りに機能しない
  • Streamlitの機能は使えない(st.write()などは呼び出せない)
  • .recv()の外側と内側のやりとりはスレッドセーフに実装する必要がある

これらは.recv()がその他のStreamlitアプリコードとは別のスレッドで実行されることからくる制約です。

前節の例でも、.recv()内から直接外側の変数を参照できない(globalが使えない)ので、VideoProcessorのインスタンスアトリビュートを介して.recv()の内外で値のやりとりをするデザインになっています。

クラウドにデプロイする

せっかくwebアプリを作ったので、クラウドにデプロイしてみんなに使ってもらえるようにしましょう。

WebRTCの設定

クラウドにデプロイするにあたって、以下のようにwebrtc_streamer()rtc_configuration引数を足します。

ctx = webrtc_streamer(
    key="example",
    video_processor_factory=VideoProcessor,
    rtc_configuration={  # この設定を足す
        "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
    }
)
app.py全体を見る
import streamlit as st
from streamlit_webrtc import webrtc_streamer
import av
import cv2

st.title("My first Streamlit app")
st.write("Hello, world")


class VideoProcessor:
    def __init__(self) -> None:
        self.threshold1 = 100
        self.threshold2 = 200

    def recv(self, frame):
        img = frame.to_ndarray(format="bgr24")

        img = cv2.cvtColor(cv2.Canny(img, self.threshold1, self.threshold2), cv2.COLOR_GRAY2BGR)

        return av.VideoFrame.from_ndarray(img, format="bgr24")


ctx = webrtc_streamer(
    key="example",
    video_processor_factory=VideoProcessor,
    rtc_configuration={
        "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
    }
)
if ctx.video_processor:
    ctx.video_processor.threshold1 = st.slider("Threshold1", min_value=0, max_value=1000, step=1, value=100)
    ctx.video_processor.threshold2 = st.slider("Threshold2", min_value=0, max_value=1000, step=1, value=200)

サーバがローカルにない場合、映像・音声伝送の通信を確立するためにこちらの設定が必要です。

streamlit_webrtcによる動画・音声の伝送にはWebRTCが使われています。
そして、本稿では詳細は省きますが、 リモートの(正確にはNAT越しの)のpeer同士でWebRTC接続するには、グローバルネットワークにあるSTUNサーバへの問い合わせが必要になります。

上記サンプルでは、Googleが公開していてフリーで利用できるSTUNサーバを利用するよう設定しました。
有効なSTUNサーバであればこれ以外を設定しても大丈夫です[5]

ちなみに、rtc_configuration引数の内容は最終的にフロントエンドでRTCPeerConnectionのコンストラクタに渡されます。

HTTPS

リモートにホストされたwebアプリでは、webカメラやマイクを使うためにHTTPSが必要です。

本稿で使ったwebrtc_streamer()コンポーネントに限らず、ブラウザからクライアントのwebカメラやマイクにアクセスするためにはMediaDevices.getUserMedia()APIが利用されます。
そしてこのAPIは"insecure context"(セキュアでないコンテキスト)では利用できません。

またドキュメントには

A secure context is, in short, a page loaded using HTTPS or the file:/// URL scheme, or a page loaded from localhost.
(セキュアなコンテキストとは、端的には、HTTPSかfile:///スキーマのURLで読み込まれたページ、もしくはlocalhostから読み込まれたページです。)
MediaDevices.getUserMedia() - Privacy and security

とあります。

従って、webカメラやマイクを利用するためには、リモートから読み込む場合はHTTPSが必要、となります。

Streamlit Cloud

StreamlitアプリをホストするならStreamlit Cloudがおすすめです。
StreamlitアプリのソースコードをGitHubに置いておけば、数ステップでマネージド環境にデプロイしてHTTPSで提供できます。
また、あくまで体感ですが、Herokuの無料プランより強力な環境で実行されていると思います。

Streamlit Cloudの使い方は公式ドキュメントなどを参照してください。

実際に本稿のチュートリアルで作ったアプリをデプロイしてみました→ https://share.streamlit.io/whitphx/streamlit-webrtc-article-tutorial-sample/main/app.py

GitHubレポジトリはこちらです→ https://github.com/whitphx/streamlit-webrtc-article-tutorial-sample

Streamlit Cloud環境に必要な依存(streamlit-webrtcopencv-python-headless)をインストールするためにリポジトリにrequirements.txtを追加している点に注意してください。https://github.com/whitphx/streamlit-webrtc-article-tutorial-sample/blob/main/requirements.txt

注意点

  • 前述の通り、クライアントサイドのカメラから取得した映像はネットワーク越しにサーバに送られ、画像処理のコードはサーバサイドで実行されます[6]
    • 従って全くスケーラブルではないですし、ネットワーク接続に依存します。あくまでプロトタイピングや社内共有用として捉えるのが良いでしょう。
    • 映像の外部送信に法的な懸念がある場合などは、自社内のサーバにホストするなどの構成を検討してください。

サンプル集

https://github.com/whitphx/streamlit-webrtc に記載のリストの転記となります。ソースコードのリポジトリと、Streamlit Cloudにホストされたデモそれぞれへのリンクがあります。

⚡️Showcase including following examples and more: 🎈Online demo

以下のようなデモを複数含んだショーケースです。サイドバーのドロップダウンからデモを切り替えられます。

  • 物体検知(object detection)
    • 本稿冒頭のスクリーンショットはこれです
  • OpenCVフィルタ
  • 一方向の動画・音声送信
  • 音声処理

streamlitをインストールした環境で以下のコマンドを実行してローカルで試すこともできます。

$ pip install streamlit-webrtc opencv-python-headless matplotlib pydub
$ streamlit run https://raw.githubusercontent.com/whitphx/streamlit-webrtc-example/main/app.py

streamlit runコマンドはURLを引数に渡しても実行できます。便利ですね。

⚡️Real-time Speech-to-Text: 🎈Online demo

リアルタイム文字起こし(Speech-to-Text; STT)です。
STTエンジンにはオープンソースのmozilla/DeepSpeechを利用しています。アメリカ英語で訓練されたモデルです。
DeepSpeechも含めて全て単一のStreamlitアプリ内で動いており、外部APIは使っていません。

⚡️Real-time video style transfer: 🎈Online demo

リアルタイムスタイル変換です。
本稿冒頭のスクリーンショットはこれです。

⚡️Video chat

(Online demo not available)

ビデオチャットのサンプルです。
認証を実装していないのでパブリックなデモは用意していません。
ローカル環境で試してみてください。

⚡️Tokyo 2020 Pictogram: 🎈Online demo

@KzhtTkhs氏の東京オリンピックのピクトグラムのアプリをforkしてStreamlitアプリ化したものです。
姿勢推定にMediaPipeを利用しています。MediaPipeとの組み合わせ方の参考にどうぞ。

https://twitter.com/whitphx_ja/status/1420442166740996098

音声は?

画像と同様に.recv()メソッドを持ったクラスを定義し、それをaudio_processor_factory引数に渡すと、コールバックが呼ばれるようになります。
音声の場合は引数と返り値がAudioFrameクラスのインスタンスになります。

上記のショーケースアプリ中の音声のゲインを変えるサンプルのコードや、Speech-to-Textサンプルのコードなどを参考にしてみてください。


すみません書ききれなかったというのが正直なところです。
長くなりすぎ、また何より私が音声・信号処理自体に詳しくないというのがあり…音声界隈のどなたか、何か書いてみていただけませんか🙇‍♂️

脚注
  1. OpenCVでもGUIを構築してユーザ入力を受け付ける機能はあります(例: https://docs.opencv.org/4.x/d9/dc8/tutorial_py_trackbar.html) ↩︎

  2. 正確には、「callableな要素.recvを持つオブジェクト」を返すcallableをvideo_processor_factoryに渡せます。Pythonではクラスオブジェクトがそのままインスタンスオブジェクトを返すcallableなのでクラスを渡すことができます。 ↩︎

  3. 詳しい方はフレームのタイミング情報をセットしていないことが気になったかもしれません。それらの情報はフレームオブジェクトが.recv()から返された後にstreamlit-webrtcが自動的に設定します。プログラマが手動で.recv()内で設定することもでき、その場合streamlit-webrtcは何もしません。 ↩︎

  4. 物体検出のソースコード スタイル変換のソースコード ↩︎

  5. 以前、接続に非常に時間がかかるという内容のissueが投稿されたことがありました。中国のネットワークからこのGoogleのSTUNサーバを使おうとしたのが原因だったらしく、別のSTUNサーバを使う設定にしたら解決したとのことでした。 https://github.com/whitphx/streamlit-webrtc/issues/283#issuecomment-889753789 ↩︎

  6. Pyodideと組み合わせてクライアントサイドで画像処理関数を実行するコンポーネントも試作してみたのですが…まだまだです ↩︎

Discussion

ログインするとコメントできます