今、話題の人工知能(AI)などで人気のPython。初心者に優しいとか言われていますが、全然優しくない! という事を、つらつら、愚痴っていきます

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フレームワークで配信する

#!/usr/bin/env python3 # -*- coding: utf-8 -*-

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()関数を使用すれば、複数ブラウザからのアクセスに耐えられるのではないか?という事です。

#!/usr/bin/env python3 # -*- coding: utf-8 -*-

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の仮想女性の心の声です。

Comment(0)

コメント

コメントを投稿する