今回は前回記事に引き続きラズパイ4BとUSBカメラを使ったMJPG(Motion JPEG)のブラウザストリーミングですが、新たにマルチプロセスを使ってストリーミング中にWeb画面のボタンで画角サイズを変更したり、ストリーミングを一時停止したり、Bottleサーバーをシャットダウンさせるなど、双方向コントロールに挑戦してみたいと思います。
ネット検索しても、なかなかPython Bottleサーバーを使ったMJPG(Motion JPEG)ストリーミングストリーミングで双方向通信の具体例が無く、かなりの時間を割いて試行錯誤しました。
そして、今回、ようやく納得のいく動作ができました。
まずは以下の動画をご覧ください。
いい感じでストリーミング中にボタン操作で画角変更できていますよね。
しかも、640×480 pixel で約30fps出せています。ここまでできれば十分です。
そして個人的に特に気に入っているのは、ボタン操作でBottleサーバーのシャットダウンができたことです。
後で詳しく述べますが、Python Bottleとmultiprocessingを使う時、Bottleサーバーの強制シャットダウンがとても面倒だったんですよね。
VSCodeのターミナルで強制終了用のキーを打っても、複数プロセスが裏で動いていて、killコマンドで強制終了させなければならなかったんです。
でも、今回、Web上のボタンで全てのプロセスを一気に強制終了できたのはウレシイですね。
ということで、今回やった実験を紹介してみようと思います。
なお、いつも言っていることですが、私はラズパイもプログラミングもド素人です。
何か誤り等がありましたら、コメント投稿でご連絡いただけると助かります。
- 使ったもの
- 自分のラズパイ4BおよびPython環境
- マルチプロセスとマルチスレッド
- multiprocessingを使ったプロセス間の値の共有
- ストリーミング中にBottleサーバーを強制終了させるには
- Pythonコード(shutdownボタンのみの場合)
- Pythonコード(画角サイズ変更、一時停止、shutdownボタン有り)
- まとめ
【目次】
1.使ったもの
Raspberry Pi 4 model B
(図01-01)
以前、以下の記事で紹介した、OKdo製のRaspberry Pi 4 Model B 4GBの全部入りセットを使っています。
ラズパイ(Raspberry Pi 4 Model B)をSSDブートにする(備忘録)
USB-SSD
これも以前のこちらの記事で紹介した、スティック型のUSB-SSDを使っています。
BUFFALO (バッファロー)
【名称】 SSD 外付け 250GB 超小型 コンパクト ポータブル PS5/PS4対応(メーカー動作確認済) USB3.2Gen1 ブラック
【型式】 SSD-PUT250U3-B/N
USB3.0ハブ(外部電源供給)
外部電源供給可能なUSBハブを使ったのは以前のこちらの記事でも説明したように、ラズパイ4BのUSBポートでは電力供給に難がありそうだったからという理由です。
エレコム U3H-T410SBK
USBカメラ
Logicool製 C920n を使いました。
オートフォーカス、フルHD、デュアルマイク内蔵です。
このUSBカメラを選んだ理由は前回記事を参照してください。
パソコン環境
Windows 10
Visual Sutudio Code (VSCode) SSHリモート
ルーター環境
一般的な有線LAN環境です。
また、スマホとのストリーミングはローカルエリアのWiFiを使っています。
2.自分のラズパイ4BおよびPython環境
まずは現在の自分のラズパイ4BのブートローダーやOSの環境、そしてPythonの環境を示しておきます。
ラズパイ4Bブートローダーバージョン
ラズパイ4Bのブートローダーバージョンは以下です。
$ sudo rpi-eeprom-update BOOTLOADER: up to date CURRENT: 2022年 1月 25日 火曜日 14:30:41 UTC (1643121041) LATEST: 2022年 1月 25日 火曜日 14:30:41 UTC (1643121041) RELEASE: default (/lib/firmware/raspberrypi/bootloader/default) Use raspi-config to change the release. VL805_FW: Dedicated VL805 EEPROM VL805: up to date CURRENT: 000138a1 LATEST: 000138a1
OSバージョン
次に、OSのUbuntu Serverのバージョンは以下です。
$ cat /etc/os-release PRETTY_NAME="Ubuntu 22.04.2 LTS" NAME="Ubuntu" VERSION_ID="22.04" VERSION="22.04.2 LTS (Jammy Jellyfish)" VERSION_CODENAME=jammy ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" UBUNTU_CODENAME=jammy
カーネルバージョン
次にカーネルのバージョンは以下です。
$ cat /proc/version Linux version 5.15.0-1029-raspi (buildd@bos02-arm64-006) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #31-Ubuntu SMP PREEMPT Sat Apr 22 12:26:40 UTC 2023
Pythonバージョン
pyenvを使っていて、Python 3.10.10 です。
Python環境は以前の以下の記事で紹介しています。
ラズパイ(Ubuntu Server)とVSCodeでPythonプログラミング環境構築する(備忘録)
pyenv バージョン
$ pyenv --version pyenv 2.3.18
Python各パッケージバージョン
pipでインストールしたPythonパッケージのバージョンは以下です。
$ pip list --format=freeze bottle==0.12.25 numpy==1.24.3 opencv-python-headless==4.7.0.72 pip==23.1.2 setuptools==67.8.0 somepackage==1.2.3 wheel==0.40.0
3.マルチプロセスとマルチスレッド
このブログでは以前、ESP32やM5Stackを扱っていた時、マルチタスク(マルチプロセス)でC/C++を走らせてカメラストリーミングしたことがありました。
その経験から、MJPGストリーミング中のブラウザからのボタン操作は、CPUを複数使って並列処理、つまりマルチプロセスを使うしかないと思っていました。
例えば以下の感じでプロセス1とプロセス2を並列処理します。
【プロセス1】
ポート番号8080で、ボタン操作で画角サイズ変更、一時停止、シャットダウンなど、常時ブラウザとのHTTP通信可能な状態にする。
【プロセス2】
ポート番号8081で Motion JPEG over HTTP を走らせる。
ただ、Pythonにはマルチプロセスとマルチスレッドという処理があって、どっちを使ったらよいのか最初はよく分かりませんでした。
マルチプロセスならば multiprocessing
を使います。マルチスレッドならば threading
です。
その違いについては以下のサイトが分かり易かったです。
参考: https://qiita.com/Jungle-King/items/1d332a91647a3d996b82
要はラズパイのマルチコアCPUを使って完全な並列処理させるなら、マルチプロセスを使うということです。
今回の実験についてはマルチスレッドでも可能だと思いますが、せっかくクアッドコアCPUのラズパイ4Bを使っているので、マルチプロセスを使うことにしました。
threading については一度試してみましたが、Bottleフレームワークと併用するとうまく動いてくれませんでした。結局、multiprocessing が今回の実験にマッチしていたということです。
ただ、Bottleフレームワーク自体がマルチプロセスで動いているっぽく、メイン処理でBottleの run 関数を走らせると、別プロセスで動かしているUSBカメラ制御と競合して、意味不明な挙動になるということがありました。
また、multiprocessingをよく理解しないまま、プロセス間で変数の共有をしようと、global変数を使いましたが、当然、意図したとおりに動かず、どっぷり沼にハマりました。
結局、プロセス間の変数の共有は、次で説明するValue
やQueue
を使えばよいことがわかって解決できたんです。
4.multiprocessing を使ったプロセス間の値の共有
Pythonの multiprocessing を使うときに注意しなければならないのは、グローバル変数等でプロセス間の変数共有ができないということです。
Arduino core ESP32などのC/C++ならばグローバル変数で殆ど共有できたんですけど、Python の multiprocessing ではそう簡単に行きません。
例えば以下のようなコードです。
from multiprocessing import Process import time x1 = 1 x2 = 1 def test1(): global x1 while True: x1 += 1 time.sleep(1) def test2(): global x2 while True: x2 -= 1 print("x1={}, x2={}".format(x1, x2)) time.sleep(1) p1 = Process(target=test1) p2 = Process(target=test2) p1.start() p2.start()
これを実行すると、以下のようになります。
x1=1, x2=0 x1=1, x2=-1 x1=1, x2=-2 x1=1, x2=-3 x1=1, x2=-4 x1=1, x2=-5 x1=1, x2=-6 x1=1, x2=-7 x1=1, x2=-8 x1=1, x2=-9 .....etc
これだと、p2プロセスのx1の値が変化していません。
最初に定義した1だけが代入されている状態で、p1プロセスで加算しても反映されていないわけです。
これでは他のプロセスでボタンが押されたらメインプロセスが停止するという処理が実現できないです。
では、どうするかというと、multiprocessing のValue
を使えば良いのです。
4-01. Value による共有
異なるプロセス間で変数を共有するには、Value
を使います。
公式の以下のサイトを参照ください。
https://docs.python.org/ja/3/library/multiprocessing.html#sharing-state-between-processes
例えば、以下のようなコードを組んでみます。
from multiprocessing import Process, Value import time x1 = Value("i", 1) x2 = Value("i", 1) def test1(): while True: x1.value += 1 time.sleep(1) def test2(): while True: x2.value -= 1 print("x1={}, x2={}".format(x1.value, x2.value)) time.sleep(1) p1 = Process(target=test1) p2 = Process(target=test2) p1.start() p2.start()
この実行結果は以下のようになります。
x1=2, x2=0 x1=3, x2=-1 x1=4, x2=-2 x1=5, x2=-3 x1=6, x2=-4 x1=7, x2=-5 x1=8, x2=-6 x1=9, x2=-7 x1=10, x2=-8 x1=11, x2=-9 x1=12, x2=-10 x1=13, x2=-11 x1=14, x2=-12 x1=15, x2=-13
見事、意図した結果になりましたね。
異なるプロセス間で変数が共有できています。
これを使えばいけそうです。
ただ、この場合気を付けなければならないのは、別のプロセスで値を書き込んでいる最中に別のプロセスで値を読み込むような状況が起きる可能性があります。それは出来るだけ避けたいですよね。
その解決策は、次で説明するQueue
を使えばいいんです。
4-02. Queue による画像共有は動画ストリーミングに最適かもね
Queue
を使うと、本プロセスでqueに値を書き込み終わるまで別プロセスからqueに入っている値の読み込みを開始できず、その手前でプログラムが自動停止(ブロック)してくれるんです。これは便利ですね。threadingの動作とちょっと似ていますね。
例えば、以下のコードです。
from multiprocessing import Process, Queue import time def func1(): while True: a = que1.get() print("proc1 a = ", a) time.sleep(0.2) if a > 10: break def func2(): a = 0 while True: que1.put(a) print(" proc2 a = ", a) a += 1 time.sleep(1) if a > 10: break que1 = Queue(maxsize=1) proc1 = Process(target=func1) proc2 = Process(target=func2) proc1.start() proc2.start() proc1.join() proc2.join() proc1.terminate() proc2.terminate() proc1.close() proc2.close()
これを実行すると、以下のようになります。
proc2 a = 0 proc1 a = 0 proc2 a = 1 proc1 a = 1 proc2 a = 2 proc1 a = 2 proc2 a = 3 proc1 a = 3 proc2 a = 4 proc1 a = 4 proc2 a = 5 proc1 a = 5 proc2 a = 6 proc1 a = 6 proc2 a = 7 proc1 a = 7 proc2 a = 8 proc1 a = 8 proc2 a = 9 proc1 a = 9 proc2 a = 10 proc1 a = 10
このように、異なるプロセス間でもque1によって値は共有できていますね。
そして、マルチプロセスでfunc1とfunc2を並列動作させると、func1のループは0.2秒で繰り返していて速いのに対し、func2のループは1秒と遅いです。なのに、結果は1秒毎に表示されます。
つまり、func1の a = que1.get()
のところで、func2の que1.put(a)
に値が入力されるまで進行が停止しているということです。
この que1 には、que1 = Queue(maxsize=1)
で定義されているので、データは1個分しか入れることが出来ません。よって、データを1個入れてしまったら、それを取り出す(読み出す)まではque1.put
に書き込むことが出来ないのです。
逆に、func1とfunc2のtime.sleep
の値を逆にしてみてください。
それを実行すると全く同じ結果になると思います。
つまり、que1.get()
でデータを取り出す時間が遅いと、que1.put(a)
に値が入力できないので、自動的に待たされることが分かると思います。
カメラストリーミングの場合はこの挙動を利用して、プロセス1でカメラを動かして画像を出力させ、プロセス2でWebへ出力するようにし、que1に画像1枚を書き込み終わるまでWebへ出力するのを自動的に抑制するようなプログラミングができるわけです。
このようなQueue
の動作は、カメラ動画をブラウザでストリーミングするには個人的に好都合だと思いました。
ところで、動画ストリーミングの場合、que待ち状態のフレームは破棄して、次の画像を即出力したい場合ありますよね。
その場合、以下のように、put関数のオプションをFalseにして、try~except
でくくるという方式がありました。
try: que1.put(a, False) except: print(".", end="")
公式のこちらのサイトによると、Trueの場合、queに空きがない場合は延々と待たされますが、Falseにした場合、queに空きがないと queue.Full
例外が発生します。
try~except
で括らずに、ただ単に que1.put(a, False)
だけだとターミナルに例外メッセージが表示されてプログラムが停止してしまいますが、try~except
で括ることによって、例外が発生してもプログラムの進行を止めずにターミナルに任意のメッセージを表示させて続行させることができるようになります。
例えば、先のコードを修正して以下のコードにしてみます。
from multiprocessing import Process, Queue import time def func1(): while True: a = que1.get() print("proc1 a = ", a) time.sleep(1) if a > 10: break def func2(): a = 0 while True: try: que1.put(a, False) except: print(".", end="") print(" proc2 a = ", a) a += 1 time.sleep(0.2) if a > 10: break que1 = Queue(maxsize=1) proc1 = Process(target=func1) proc2 = Process(target=func2) proc1.start() proc2.start() proc1.join() proc2.join() proc1.terminate() proc2.terminate() proc1.close() proc2.close()
実行結果は以下のようになります。
proc2 a = 0 proc1 a = 0 proc2 a = 1 . proc2 a = 2 . proc2 a = 3 . proc2 a = 4 proc1 a = 1 proc2 a = 5 . proc2 a = 6 . proc2 a = 7 . proc2 a = 8 . proc2 a = 9 proc1 a = 5 proc2 a = 10 proc1 a = 10
このように、例外が発生したところは、ターミナルに”.”
を表示させて、func2のWhileループを続行させていることがわかります。
これを使えば、動画ストリーミングの場合にfunc1の遅れによってqueの空きが無くなっても、フレーム画像を破棄してループ動作を進めることができますね。
これは使えますよ!!!
5.ストリーミング中にBottleサーバーを強制終了させるには
前回記事のPython-BottleサーバーでMJPG over HTTP ストリーミング中に走らせているプログラムを強制停止させるには、VSCodeターミナルで「Ctrl」+「C」キーを打って強制割り込みさせて終了させるしかありませんでした。
ならば、Web画面のボタンで終了させるにはどうしたら良いのでしょうか?
5-01. ターミナルでpsコマンドやkillコマンドを打って強制終了させるのは面倒
Python のmultiprocessing を使って並列処理をする時、「Ctrl」+「C」キーで強制終了しても、まだ裏で別のプロセスが動いていることがありました。
その場合は以下のコマンドを打って、裏で動いているプロセスのIDを調べます。
ps aux | grep python
すると、以下のように表示されます。
$ ps aux | grep python root 801 0.2 0.4 33668 18064 ? Ss 19:49 0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers root 901 0.2 0.5 110632 20516 ? Ssl 19:49 0:00 /usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal xxxxxx 1842 2.5 0.7 187928 30668 ? Sl 19:50 0:02 /home/xxxxxx/.pyenv/versions/3.10.10/bin/python3.10 /home/xxxxxx/.vscode-server/extensions/ms-python.isort-2022.8.0/bundled/tool/server.py xxxxxx 1873 33.8 7.3 1048568 286872 ? Sl 19:50 0:32 /home/xxxxxx/.vscode-server/bin/123456789abc/node /home/xxxxxx/.vscode-server/extensions/ms-python.vscode-pylance-2023.5.50/dist/server.bundle.js --cancellationReceive=file:277c5b55cb5ba5bc5b77 --node-ipc --clientProcessId=1361 xxxxxx 3167 4.1 1.3 341388 50460 pts/0 S 19:51 0:01 /home/xxxxxx/.pyenv/versions/3.10.10/bin/python3.10 /home/xxxxxx/py/test01.py xxxxxx 3208 28.3 1.0 618996 40436 pts/0 Sl 19:51 0:07 /home/xxxxxx/.pyenv/versions/3.10.10/bin/python3.10 /home/xxxxxx/py/test01.py xxxxxx 3475 0.0 0.0 7084 2088 pts/0 R+ 19:52 0:00 grep --color=auto python
これで、ユーザー名xxxxxxが test01.py というPythonコードを動かしているので、ユーザー名のすぐ右隣の数値が裏で動いているプロセスID(PID)です。ここでは、3167と3208になります。
これを以下のようにkillコマンドで強制終了させます。
kill 3167 3208
multiprocessing を使ったプログラミングを試していると、このようなことを何度も行わねばならなかった訳です。とても面倒ですよね~。
そこで、せっかくブラウザのWeb表示でカメラストリーミングさせているのだから、Web画面の操作で停止したいですね。
その場合、先ほど説明したmultiprocessingを使って、ストリーミングとは別のプロセスで動かしているコントロール用のプロセスでブラウザからshutdownというURLにアクセスし、それを検知したら、それぞれのプロセスを停止してクローズさせます。例えば、以下の感じです。
proc1.terminate() proc1.join() proc1.close() proc2.terminate() proc2.join() proc2.close()
これで裏で動いているプロセスを停止できますが、Pythonのメインプログラムは停止できません。
これをWeb画面から停止させるにはどうすれば良いでしょうか?
5-02. Pythonメインプログラムを終了させるには
では、Web画面の操作で自身のPython Bottleサーバーのメインプロセスを停止させるにはどうすれば良いかと言うと、以下のサイトに答えがありました。
https://kapibara-sos.net/archives/208
こんなコードです。
import os import signal os.kill(os.getpid(), signal.SIGTERM)
ブラウザからshutdownのアクセスが来たら、このコードを実行させれば良いわけです。これは便利ですね。
このコードの意味はまだよくわかっていませんが、たぶん、以下の公式記事
https://docs.python.org/ja/3/library/os.html
によれば、メインプロセスのPIDを取得して、そのPIDにterminate(プロセス終了)シグナルを送ってメインプロセスを終了させるということなのだと思われます。
でも、ちょっと納得いかないのは、普通のプログラムならメインの処理が終わると終了するのに、multiprocessing で複数のプロセスを起動させるとなぜかメインプロセスが終了しないことです。
これは謎です。
6.Pythonコード(shutdownボタンのみの場合)
では、以上を踏まえて、Python の multiprocessing を使って、MJPG(Motion JPEG)ストリーミング中にWeb画面のボタン操作で全プロセスを終了させるコードを作ってみました。
とりあえず、以下のコードです。
#!/usr/bin/env python3.10 # MJPGストリーミング中にShutdownボタンを押すとPythonサーバーが完全終了するコード import cv2 from bottle import route, run, response import time from multiprocessing import Process, Queue, Value # ※ufwファイアウォールで、ポート番号を8080と8081を開放しておくこと def bt(): host_url = "192.168.0.18" control_port = 8080 stream_port = 8081 proc1 = None proc2 = None que_img = Queue(maxsize=1) cam_fps = Value("i", 0) goShutdown = Value("b", False) @route('/', method="GET") def top(): nonlocal proc1, proc2 # USBカメラのハードウェア制御は、メインスレッドで動かすとBottleのrun関数と競合するっぽいので、Bottle関数内からUSBカメラプロセスを作動させるとうまくいく。 proc1 = Process(target=camera_capture, args=(que_img, cam_fps, )) proc1.start() # MJPG over HTTP用の8081ポートは別プロセスでBottleフレームワークを動かしておく。 proc2 = Process(target=run, kwargs={'host': host_url, 'port': stream_port, 'reloader': True, 'debug': True}) proc2.start() return "<img src='http://{host_url}:{stream_port}/streaming'/><br>"\ "<button type='button' onclick='location.href=\""\ "http://{host_url}:{control_port}/shutdown\"'>Shutdown(サーバー停止)"\ "</button>".format(host_url=host_url, control_port=control_port, stream_port=stream_port) @route('/streaming') def streaming(): # カメラ画像にフレームレート値テキストを書き込む関数 def put_txt(img, fps, col_r, col_g, col_b, tn): cv2.putText(img, text=str(fps).rjust(3) + ' fps', org=(0, 50), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=2.0, color=(col_r, col_g, col_b), thickness=tn, lineType=cv2.LINE_4) response.set_header('Content-type', 'multipart/x-mixed-replace;boundary=--frame') fps = 0 fps_count = 0 fps_time = time.time() while not goShutdown.value: img = que_img.get() # フレームレート値テキスト(黒縁取り)をカメラ画像に上書きする put_txt(img, fps, 0, 0, 0, 5) put_txt(img, fps, 255, 255, 255, 2) # JPEG最高品質の場合 ret2, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 100]) # ret2, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 50]) # JPEG品質50の場合 if not ret2: continue frame_data = jpeg.tobytes() # ブラウザへMJPG送信 yield (b'--frame\r\n' + b'Access-Control-Allow-Origin: *\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n\r\n') # フレームレート計算 if (time.time() - fps_time) >= 1: fps = fps_count fps_count = 0 fps_time = time.time() else: fps_count += 1 @route('/shutdown', method="GET") def shutdown_server(): nonlocal proc1, proc2 goShutdown.value = True time.sleep(0.5) # 0.5秒以上無いと確実に停止できないかも # USBカメラプロセスの停止 proc1.terminate() proc1.join() proc1.close() yield "USBカメラを停止しました<br>" # MJPG over HTTPプロセスの停止 proc2.terminate() proc2.join() proc2.close() yield "BottleサーバーをShutdownしました" # メインプロセスのPython Bottleサーバーを完全終了させる import os import signal os.kill(os.getpid(), signal.SIGTERM) # USBカメラ制御 def camera_capture(que_img, cam_fps): try: cap = cv2.VideoCapture(0, cv2.CAP_V4L2) except TypeError: cap = cv2.VideoCapture(0) if cap.isOpened() is False: raise IOError print("Capture Set OK!") print("default camera_fps=", cap.get(cv2.CAP_PROP_FPS)) #予め次のコマンドでUSBカメラの使用可能な解像度とフレームレートを確認しておく # v4l2-ctl --list-formats-ext cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) cam_fps.value = int(cap.get(cv2.CAP_PROP_FPS)) print("now camera_fps=", cam_fps.value) while not goShutdown.value: ret, img = cap.read() if not ret: continue # que_img.put(img, False) のようにオプションがFalseの場合、queが満杯だと例外 queue.Full が発生する。その場合以下のようにする try: que_img.put(img, False) except: print(".", end="") # Queが満杯の場合に表示させる。 print("Released VideoCapture") cap.release() run(host=host_url, port=control_port, reloader=True, debug=True) if __name__ == '__main__': bt()
【ザッと解説】
まず、前回記事でも説明したように、予めファイアウォール(ufw)でポート8080と8081を開放しておきます。
メインプロセスでは8080ポートで通信し、Topページ表示や画角サイズ変更、一時停止等のコントロール用専用とし、プロセス1(proc1)ではUSBカメラ制御専用とし、プロセス2(proc2)では8081ポートを使ってMJPGストリーミング専用としました。
最初はメインプロセスでUSBカメラを動かして、Bottleフレームワークを全てmultiprocessingのProcessで動かそうと思ったのですが、なぜかUSBカメラ制御とBottleがメインプロセスで競合しているっぽく、うまく動いてくれませんでした。
個人的な予想ですが、おそらくBottle自身がマルチプロセスで動いているせいなのかな? と、思いました。
ということで、メインプロセスでBottleによる8080ポートでTopページを表示させてから、別プロセスのproc1でUSBカメラを動かし、そして8081のポートでMJPGストリーミングをプロセスproc2で走らせています。そうするとうまく動いてくれました。
Bottleによるこのマルチプロセス手法はネットの情報では見つけられず、いろいろ試行錯誤しました結果できた方法ですが、我ながらよくできたと思いました。この方法が最善かどうかわかりませんが、もし、もっと良い方法があればコメント投稿で教えてください。
また、先ほど説明したValueを使ってプロセス間の値を共有し、フレームレート値を表示させたり、ストリーミング中にshutdown指令をbool値で受け取ったりしています。
そして、17行目では先ほど説明した Queue を使っています。maxsize=1
としているので、queに画像データは1枚分しか貯められないようにしています。複数枚画像を貯めてしまうと、遅延が大きくなり過ぎるので今回は1枚としています。
USBカメラ制御プロセスproc1実行中では、140行目にあるように que_img.put(img, False)
で画像データを受け取るのを待ってから、proc2の動作でブラウザに画像を出力するようにしています。
では、このコードを走らせる前に、自分のルーターのファイアウォール等の設定を確認しておき、ラズパイと通信できるようにしておきます。
そしたら、このPythonコードを実行し、パソコンやスマホなどのブラウザを開き、URL入力欄に以下のように入力します。
http://192.168.0.18:8080
すると以下のように表示されればOKです。
(図06-01)
カメラ画像の下に「Shutdown (サーバー停止)」というボタンがあって、そこをタッチまたはクリックすると、マルチプロセスも終了して、ブラウザには以下のように表示されます。
(図06-02)
そして、VSCode(Visual Studio Code)のターミナルには、
Released VideoCapture
と表示されて、コマンドプロンプト状態になると思います。
つまり、メインプロセスも終了できたことになります。
これは、前節で説明したように、106~108行目でkillを使ってメインプロセスを終了させたわけです。
では、次はボタンで画角サイズ変更や一時停止や再開などができるようなコードを組んでみます。
7.Pythonコード(画角サイズ変更、一時停止、shutdownボタン有り)
では、次は最初に紹介した動画のような動作をするコードを作ってみます。
MJPG(Motion JPEG)ストリーミング中にWeb画面のボタンで画角サイズを変更したり、ストリーミングを一時停止および再開したり、Pythonサーバーをシャットダウンさせたりします。
まず、views
フォルダにTop画面用の以下のHTMLファイルを作成します。
ファイル名は mjpg_multi_top.html
としておきます。
<style> button{font-size: 2em;} </style> <div> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=1920&height=1080'">1920 x 1080</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=1280&height=960'">1280 x 960</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=800&height=600'">800 x 600</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=800&height=448'">800 x 448</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=640&height=480'">640 x 480</button><br> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=640&height=360'">640 x 360</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=320&height=240'">320 x 240</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=320&height=176'">320 x 176</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/size?width=160&height=120'">160 x 120</button> </div> <div> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/pause'">Pause(一時停止)</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/restart'">Re-Start(再開)</button> <button type="button" onclick="location.href='http://{{host_url}}:{{control_port}}/shutdown'">Server Shutdown</button> </div> <div style="font-size: 1.5em;"> 画像サイズ:{{width}} × {{height}}<br> カメラデバイス側のフレームレート:{{frame_rate}} fps<br> </div>
二重波括弧内の変数は次のPythonコードから返された値が反映されるようになります。
では次にPythonコードです。
multiprocessing のValueとQueueを使ってプロセス間のデータ共有をしています。
#!/usr/bin/env python3.10 # MJPGストリーミング中にボタンで画角サイズ変更、一時停止、Shutdownなどのコントロールが可能なコード import cv2 from bottle import route, run, response, request, template import time from multiprocessing import Process, Queue, Value # ※ufwファイアウォールで、ポート番号を8080と8081を開放しておくこと def bt(): host_url = "192.168.0.18" control_port = 8080 stream_port = 8081 proc1 = None proc2 = None que_img = Queue(maxsize=1) que_fps = Queue(maxsize=1) img_width = Value("i", 640) img_height = Value("i", 480) cam_fps = Value("i", 0) isPause = Value("b", False) isChange_size = Value("b", False) goShutdown = Value("b", False) @route('/', method="GET") def top(): nonlocal proc1, proc2 isPause.value = False # USBカメラのハードウェア制御は、メインスレッドで動かすとBottleのrun関数と競合するっぽいので、Bottle関数内からUSBカメラプロセスを作動させるとうまくいく。 proc1 = Process( target=camera_capture, args=(que_img, que_fps, isPause, img_width, img_height, cam_fps, isChange_size, )) proc1.start() # MJPG over HTTP用の8081ポートは、別プロセスでBottleフレームワークを動かしておく。 proc2 = Process(target=run, kwargs={'host': host_url, 'port': stream_port, 'reloader': True, 'debug': True}) proc2.start() cam_fps.value = que_fps.get() return ret_template(False) @route('/streaming') def streaming(): # カメラ画像にフレームレートテキストを書き込む関数 def put_txt(img, fps, col_r, col_g, col_b, tn): cv2.putText(img, text=str(fps).rjust(3) + ' fps', org=(0, 50), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=2.0, color=(col_r, col_g, col_b), thickness=tn, lineType=cv2.LINE_4) response.set_header('Content-type', 'multipart/x-mixed-replace;boundary=--frame') fps = 0 fps_count = 0 fps_time = time.time() while not goShutdown.value: if isPause.value: time.sleep(0.3) continue img = que_img.get() # フレームレート値テキスト(黒縁取り)をカメラ画像に上書きする put_txt(img, fps, 0, 0, 0, 5) put_txt(img, fps, 255, 255, 255, 2) # ret2, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 50]) # JPEG品質50の場合 ret2, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 100]) if not ret2: continue frame_data = jpeg.tobytes() # ブラウザへMJPG送信 yield (b'--frame\r\n' + b'Access-Control-Allow-Origin: *\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n\r\n') # フレームレート計算 if (time.time() - fps_time) >= 1: fps = fps_count fps_count = 0 fps_time = time.time() else: fps_count += 1 @route('/size', method="GET") def c_size(): # アクセス方法:http://192.168.0.18:8080/size?width=640&height=480 isPause.value = False img_width.value = int(request.query.get("width")) img_height.value = int(request.query.get("height")) print("size w h = ", img_width.value, img_height.value) isChange_size.value = True cam_fps.value = que_fps.get() return ret_template(False) @route('/pause', method="GET") def pause_stream(): isPause.value = True time.sleep(0.5) return ret_template(True) @route('/restart', method="GET") def restart(): isPause.value = False time.sleep(0.5) return ret_template(False) @route('/shutdown', method="GET") def shutdown_server(): nonlocal proc1, proc2 isPause.value = True goShutdown.value = True time.sleep(0.5) # 0.5秒以上無いと確実に停止できないかも # USBカメラプロセスの停止 proc1.terminate() proc1.join() proc1.close() yield "USBカメラを停止しました<br>" # MJPG over HTTPプロセスの停止 proc2.terminate() proc2.join() proc2.close() yield "BottleサーバーをShutdownしました" # メインプロセスのPython Bottleサーバーを完全終了させる import os import signal os.kill(os.getpid(), signal.SIGTERM) # HTMLを返す関数。Pauseボタンが押され具合によって返すページを変える。 def ret_template(isPause): img_tag = None if isPause: img_tag = "<div style='font-size:2.0em'>Pause(一時停止)</div>" else: img_tag = "<img src='http://" + host_url + ":" + str(stream_port) + "/streaming'/><br>" yield template("./mjpg_multi_top.html", host_url=host_url, control_port=str(control_port), stream_port=str(stream_port), width=str(img_width.value), height=str(img_height.value), frame_rate=str(cam_fps.value)) yield img_tag # USBカメラ制御 def camera_capture(que_img, que_fps, isPause, img_width, img_height, cam_fps, isChange_size): try: cap = cv2.VideoCapture(0, cv2.CAP_V4L2) except TypeError: cap = cv2.VideoCapture(0) if cap.isOpened() is False: raise IOError print("Capture Set OK!") print("default camera_fps=", cap.get(cv2.CAP_PROP_FPS)) #予め次のコマンドでUSBカメラの使用可能な解像度とフレームレートを確認しておく # v4l2-ctl --list-formats-ext cap.set(cv2.CAP_PROP_FRAME_WIDTH, img_width.value) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, img_height.value) cap.set(cv2.CAP_PROP_FPS, 30) cam_fps.value = int(cap.get(cv2.CAP_PROP_FPS)) print("now camera_fps=", cam_fps.value) que_fps.put(cam_fps.value) while not goShutdown.value: if isPause.value: time.sleep(0.3) continue if isChange_size.value: cap.set(cv2.CAP_PROP_FRAME_WIDTH, img_width.value) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, img_height.value) cap.set(cv2.CAP_PROP_FPS, 30) # ここでFPSを30としておけば、可能な限りの最速フレームレートを自動で設定してくれる print("change w h = ", img_width.value, img_height.value) cam_fps.value = int(cap.get(cv2.CAP_PROP_FPS)) print("now camera_fps=", cam_fps.value) que_fps.put(cam_fps.value) isChange_size.value = False ret, img = cap.read() if not ret: continue # que_img.put(img, False) のようにオプションがFalseの場合、queが満杯だと例外 queue.Full が発生する。その場合以下のようにする try: que_img.put(img, False) except: print(".", end="") # Queが満杯の場合に表示させる。 print("Released VideoCapture") cap.release() run(host=host_url, port=control_port, reloader=True, debug=True) if __name__ == '__main__': bt()
【ザッと解説】
※先ほどのコードと同様、事前にファイアウォール(ufw)でポート番号8080と8081を開放しておきます。
このコードでは、Bottleの使い方として以前のこちらの記事で扱ったように、外部ファイルのHTMLコードにPythonコード内で取得したデータを反映させて表示させるという手法を使いました。
先ほど紹介したHTML内の二重波括弧内の変数値は、154~159行で返されて反映するようになっています。
ただ、MJPGストリーミングするための以下の様なimgタグ、
<img src='http://192.168.0.18:8080/streaming'/>
は、161行で送信して追加する方式にしました。
どうしてそうしたかというと、一時停止した場合にHTMLファイルを2つ表示させるよりもこちらの方法が簡略化できたからです。
その他、プロセス間のデータ共有のためのValueを使って、コード画角サイズやストリーミング一時停止、などのボタン操作で制御できるようにしました。
例えば、「640×480」ボタンを押すと、
http://192.168.0.18:8080/size?width=640&height=480
というようにブラウザからクエリパラメータの入ったGETリクエストが送られてきます。
それからrequest.query.get
関数で取得できるわけです。
これについては以前のこちらの記事を参照してみてください。
一時停止については、静止画像を表示させて停止させるところまで作り込みたかったんですが、今回は省いて、文字列出力だけにしました。やろうと思えば出来ないことは無いと思います。
では、このコードを走らせてみます。
その後、ブラウザのURL入力欄に
http://192.168.0.18:8080
と入力すると、以下のように表示されればOKです。
(図07-01)
あとは最初に紹介した動画のようにボタン操作で画角サイズを変更したり、一時停止したり、シャットダウンしてみてください。
これでラズパイ4BとUSBカメラのPythonによるストリーミング表示はバッチリですね。
ただ、何分素人なので、間違いやもっと簡単な方法があるよとかあればコメント投稿で教えていいただけると助かります。
8.まとめ
マルチタスク、マルチプロセスについては以前、ESP32やM5Stackでやったことはありましたが、ラズパイのPythonでここまでできれば何でもできそうな気がしましたね。
Bottleサーバーでmultiprocessingを使った例があまり見当たらなかった中で、MJPGストリーミング中に双方向通信ができたのは我ながら大満足です。
そして、プロセス間のデータ共有やQueueの理解が進んだことは個人的に進歩したかなと思います。
これでラズパイによるAI開発に取り組めそうな気がします。
ということで今回はここまでです。
ではまた・・・。
コメント