264.ラズパイ無双[10_opencvストリーミング1]
初回:2022/7/6
Raspberry Pi (ラズベリーパイ、通称"ラズパイ")で何か作ってみようという新シリーズです。カメラ画像をストリーミング出来るサーバーのベースとなる『multipart/x-mixed-replace』について、説明したいと思います。
前回同様に、『bottle』上に構築していきます。
P子「ついにストリーミングのお話ね」※1
1.基本
動画をMotion JPEG(multipart jpeg)で配信するサーバーのサンプルです。
≪参考資料≫
https://symfoware.blog.fc2.com/blog-entry-2425.html
動画をMotion JPEG(multipart jpeg)に変換し、Bottleフレームワークで配信する
import time
import bottle
import cv2
app = bottle.Bottle()
def gen():
cap = cv2.VideoCapture(0)
while True:
# 動画から1フレーム分の画像を読み込み
ret_val, image = cap.read()
if not ret_val:
break
# jpg形式に変換
flag, frame = cv2.imencode('.jpg', image)
yield b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame.tobytes() + b'\r\n\r\n'
# 連続再生されるのでwaitを入れる
time.sleep(1/60)
@app.route('/')
def main():
return bottle.static_file('index.html', root='./')
@app.route('/video_feed')
def video_feed():
bottle.response.content_type = 'multipart/x-mixed-replace; boundary=frame'
return gen()
app.run(host='0.0.0.0', port=8088, reloader=True, debug=True)
あくまで、考え方のベースとなるサンプルですので、このままでは実用化できませんが、逆にこれくらい短くないと理解も大変でしょう。
ポイントは、/video_feed のルーティングで、content_type をmultipart/x-mixed-replaceで、繰り返しjpegファイルを返すことと、その方法が、gen()関数で、return ではなく、yield を使っている事でしょう。
≪参考資料≫
https://www.sejuku.net/blog/23716
【Python入門】yield文の基本的な使い方を解説
return の場合は、値を返すとそのまま制御も戻りますが、yield の場合は、値を返した後、次の処理に制御が移ります。ループの中で使うと、値を順番に返し続けるという動きになります。そして、content_type のmultipart/x-mixed-replace は、httpでレスポンスを受け取った後、セッションが終了せずに、次の値を受け取るというもので、今回の例でいうと、jpegファイルを順番に受け取り続けることで、動画のように見える(実際の動画も〇〇fpsと静止画が一定周期で切り替わっているだけ)という手法です。
≪参考資料≫
https://wiki.suikawiki.org/n/multipart%2Fx-mixed-replace
multipart/x-mixed-replace (MIME型)
2.waitressを適用
waitressの適用は、前回に示した通り、難しくありません、
from waitress import serve
の追加と、
# app.run(host='0.0.0.0', port=8088, reloader=True, debug=True)
serve( app , host='0.0.0.0', port=8088,threads=10 ) # threadsはデフォルト 4
app.run 部分の置き換えです。
これで、マルチスレッドで動作する...という事はなく、gen()関数内で、毎回、
cap = cv2.VideoCapture(0)
しているので、ブラウザからの同時アクセスはできません。
3.Cameraインスタンス
そこで思いつくのが、cv2.VideoCapture(0) インスタンスを別のクラスのコンストラクターで生成しておき、そのクラスのgen()関数を使用すれば、複数ブラウザからのアクセスに耐えられるのではないか?という事です。
import time
import bottle
import cv2
from waitress import serve
class Camera :
#############################################################################
def __init__(self):
self.cap = cv2.VideoCapture(0)
self.runFlag = True
#############################################################################
def gen(self):
while self.runFlag:
# 動画から1フレーム分の画像を読み込み
ret_val, image = self.cap.read()
if not ret_val:
break
# jpg形式に変換
flag, frame = cv2.imencode('.jpg', image)
yield b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame.tobytes() + b'\r\n\r\n'
# 連続再生されるのでwaitを入れる
time.sleep(1/60)
#############################################################################
def close(self):
self.runFlag = False
if self.cap is not None : self.cap.release()
print( "Video 終了" )
app = bottle.Bottle()
cam = Camera()
@app.route('/')
def main():
return bottle.static_file('index2.html', root='./')
@app.route('/video_feed')
def video_feed():
bottle.response.content_type = 'multipart/x-mixed-replace; boundary=frame'
xx = cam.gen()
return xx
# app.run(host='localhost', port=8080, reloader=True, debug=True)
# app.run(host='0.0.0.0', port=8088, reloader=True, debug=True)
serve( app , host='0.0.0.0', port=8088,threads=10 ) # threadsはデフォルト 4
cam.close()
cv2.destroyAllWindows()
print( "終了" )
何となく、複数個所からのアクセスには対応出来ているようですが、画面をリロードすると待ちに入ります。
gen()関数の処理を、スレッド制御しないとうまくいかないようです。
4.filterメソッド
最終形のソースに入る前に、折角、openCV のイメージをストリーミング出来るようになってきたので、画像のフィルタリング処理を入れてみたいと思います。
#############################################################################
def gen(self):
while self.runFlag:
# 動画から1フレーム分の画像を読み込み
ret_val, image = self.cap.read()
if not ret_val:
break
image = self.filter( image ) # ← フィルタ処理
# jpg形式に変換
flag, frame = cv2.imencode('.jpg', image)
yield b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame.tobytes() + b'\r\n\r\n'
# 連続再生されるのでwaitを入れる
time.sleep(1/60)
###########################################################################
def filter(self,image):
# 何らかの画像処理
return image
filter(self,image) 関数にオリジナルのイメージを渡して、何らかの処理を行った後、イメージを返します。
このfilter関数ですが、mjpg-streamer の input_opencv.so の 互換を意識した作りになっています。
$ ./mjpg_streamer -i "input_opencv.so --filter cvfilter_py.so --fargs puttext.py" -o "output_http.so -p 8080 -w ./www"
残念ながら、このinput_opencv.soは、openCV バージョン3の実験的な試みだったようで、openCVのバージョン4以降ではコンパイルもうまくいきませんでした。
どちらにしても、自作のWebAPサーバーでWeb画面も作る必要があり、bottle 上にストリーミングサーバーを構築して、フィルター処理ができるのなら、良しとしたいと思います。
≪参考資料≫
https://qiita.com/zuttonetetai/items/e0c4b13a6012b285db01
ラズパイでmjpg-streamerのopencvプラグインを使う+OpenCVのビルドインストール(備忘録)
5.まとめ
だいぶ駆け足ですが、ストリーミングサーバーの原型が出来上がりました。原理だけはこれで理解できると思いますが、本番適用するにはまだまだ足りません。
次回は、スレッドを使用して、gen()関数を制御する原型となるシステムの解説を行い、次々回に完成品をアップしたいと思います。
P子「引っ張るわねね」
そんなつもりはありませんが、結構難しいので順番に説明していこうと思っただけです。
ほな、さいなら
======= <<注釈>>=======
※1 P子「ついにストリーミングのお話ね」
P子とは、私があこがれているツンデレPythonの仮想女性の心の声です。