序文
物体検出の結果をビデオでフロントエンドに送信するという最近のタスクがあります。このタスクは簡単に達成できますが、実際には、ビデオの各フレームが検出され、フロントエンドで表示するために画像ストリームに戻されます。ただし、上記のリクエストはビデオ ストリームを返すのではなく、検出結果を返します。このタスクを聞いたときは戸惑いました. 理論的にはデータストリームだけを返せばいいのですが、何かおかしいと感じたので、ビデオストリーム全体を整理して返すようにこの記事を書きました. このブログは主に、「Flask を使用したビデオ ストリーミング」と「Flask ビデオ ストリーミングの再訪」を参照しています。コードについては、 flask-video-streamingを参照してください。
ストリーミング
Flask でストリームを使用するには、主なアプリケーション シナリオが 2 つあります。
- 大きな応答が
大きなデータ ブロックを返す場合は、ストリームを使用して生成して返す方が適切なソリューションです。もちろん、リターン レスポンスをディスクに書き込んで、ファイルを返すこともできますflask.send_file()
。ただし、この状況では、追加の I/O オーバーヘッドが追加されます。 - リアルタイム データ伝送 ビデオ
または音声伝送などのリアルタイム データ伝送は、ストリーミングを使用できます。
Flask はストリーミングを実装します
Flask は、generator functions
使用によるストリーミング レスポンスのサポートを提供します。次generator function
のようになります。
def gen():
yield 1
yield 2
yield 3
上記のジェネレーターを簡単に理解した後、次の例は、ストリームを使用して大量のデータ レポートを処理および生成し、それらを返す方法を示しています。
from flask import Response, render_template
from app.models import Stock
def generate_stock_table():
yield render_template('stock_header.html')
for stock in Stock.query.all():
yield render_template('stock_row.html', stock=stock)
yield render_template('stock_footer.html')
@app.route('/stock-table')
def stock_table():
return Response(generate_stock_table())
この例では、ストリームを返す応答ルートは、ジェネレーター関数で初期化されたオブジェクトを返す必要がありResponse
、Flask
ジェネレーターを呼び出して結果をチャンクでクライアントに送信する必要があります。これの利点は、プログラム内で大きなデータ ブロックを生成する必要があることと、ストリーミングによって、ブロックが大きくなっても応答を返す要求が大きくならないことです。
ストリームは、大きなデータのチャンクをチャンクに分割できるだけでなく、Multipart Responses
. この点で最も重要なアプリケーション シナリオは、ビデオ ストリームまたはオーディオ ストリームのリターン再生です。このストリームの興味深い使い方の 1 つは、ページの前のチャンクを各チャンクに置き換えることです。これにより、ブラウザ ウィンドウでストリームを「再生」できます。Multipart/Response
マルチパート コンテンツ タイプの 1 つを含むヘッダーと、それに続く境界タグの分割セクションで構成され、それぞれが独自の特定のコンテンツ タイプを持ちます。Multipart
ビデオ ストリームの構造は次のとおりです。
HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame
--frame
Content-Type: image/jpeg
<jpeg data here>
--frame
Content-Type: image/jpeg
<jpeg data here>
...
前述のように、Content-Type
ヘッダーの設定はmultipart/x-mixed-replace
明確に定義されています。次に、 のプレフィックスが付いたデータの各フレーム--
を、独自の行に境界文字列とContent-type
ヘッダーを追加します。各セクションには、オプションで、Content-Length
ペイロードのバイト単位の長さを示すものを含めることができます。
上記の基本的な知識を理解したら、次のステップはリアルタイム ビデオ ストリーミング サーバーを構築することです。原理は比較的単純です。または、ビデオの各フレームがMultipart/Response
クライアントにストリーミングされます。
ライブ ビデオ ストリームを構築する
Motion JPEG ストリームを提供する単純な FlaskWeb プログラムです。Motion JPEG は広く使用されていることに注意してください。JPEG 圧縮はモーション ビデオにはあまり効果的ではないため、この方法のレイテンシは低くなりますが、最高の品質とは言えません。
カメラからビデオ フレームを取得します。
from time import time
class Camera(object):
def __init__(self):
self.frames = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]
def get_frame(self):
return self.frames[int(time()) % 3]
コードの上記の部分は例です。カメラ デバイスを使用せずにデバッグする場合、イメージ ストリームはプロジェクトの下のイメージを読み取ることによって構築されます。
#!/usr/bin/env python
from flask import Flask, render_template, Response
from camera import Camera
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
def gen(camera):
while True:
frame = camera.get_frame()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/video_feed')
def video_feed():
return Response(gen(Camera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
このアプリケーションは、Camera
フレームのシーケンスを提供するクラスを定義します。フロントエンド HTML コンテンツ:
<html>
<head>
<title>Video Streaming Demonstration</title>
</head>
<body>
<h1>Video Streaming Demonstration</h1>
<img src="{
{ url_for('video_feed') }}">
</body>
</html>
video_feed
gen
ジェネレーター関数はroute で呼び出され、```Camera`` クラスを呼び出してビデオ ストリームを取得します。全体のプロセスは比較的単純です。ただし、ストリームの使用にはいくつかの制限があります.Flaskアプリケーションが通常のリクエストを処理する場合、リクエストサイクルは非常に短いです. Web Worker はリクエストを受け入れ、ハンドラー関数を呼び出し、最後にクライアントにレスポンスを返します。クライアントがストリームを受信するとき、クライアントはストリーミング中に接続を維持する必要があります。一方、クライアントが切断された場合、サーバーはクライアントにサービスを提供し続け、ストリーム送信を終了することが困難になる場合があります.同時に、このサービスは、同じ数のWeb Workerを持つクライアントにのみ提供できます. . コルーチンまたはマルチスレッドを使用することで、上記の問題を克服する方法がいくつかあります。次に、上記のプログラムを最適化する方法を見てみましょう。
ビデオストリーミングの最適化
上記のビデオ ストリーミング プログラムには 2 つの主な問題があります。
まず、最初の質問については、前回の回答のタイムスタンプを記録するのが原則で、前回の回答のタイムスタンプと現在のタイムスタンプの差が閾値(10秒まで設定可能ですが、小さすぎると、リクエストが正常に失敗します)。最適化されたコードは次のとおりです。
Camera
基本クラスを定義します。
class BaseCamera(object):
thread = None # background thread that reads frames from camera
frame = None # current frame is stored here by background thread
last_access = 0 # time of last client access to the camera
# ...
@staticmethod
def frames():
"""Generator that returns frames from the camera."""
raise RuntimeError('Must be implemented by subclasses.')
@classmethod
def _thread(cls):
"""Camera background thread."""
print('Starting camera thread.')
frames_iterator = cls.frames()
for frame in frames_iterator:
BaseCamera.frame = frame
# if there hasn't been any clients asking for frames in
# the last 10 seconds then stop the thread
if time.time() - BaseCamera.last_access > 10:
frames_iterator.close()
print('Stopping camera thread due to inactivity.')
break
BaseCamera.thread = None
- 継承された
BaseCamera
クラスCamera
:
class Camera(BaseCamera):
"""An emulated camera implementation that streams a repeated sequence of
files 1.jpg, 2.jpg and 3.jpg at a rate of one frame per second."""
imgs = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]
@staticmethod
def frames():
while True:
time.sleep(1)
yield Camera.imgs[int(time.time()) % 3]
次に、2 番目の質問については、マルチスレッドを使用してマルチクライアント リクエストのパフォーマンスを向上させることができますが、一方で、テスト中にサーバーが多くの CPU を消費することがわかりました。その理由は、フレームをキャプチャするバックグラウンド スレッドと、それらのフレームをクライアントに提供するジェネレータとの間に同期がないためです。どちらも、他方の速度に関係なく、可能な限り速く実行されます。
そのため、ジェネレーターが生のフレームのみをクライアントに配信するメカニズムが必要であり、ジェネレーター内の配信ループがカメラ スレッドのフレーム レートよりも速い場合、ジェネレーターは新しいフレームが利用可能になるまで待機して調整できるようにする必要があります。カメラの速度に合わせて速度自体を調整します。一方、配信ループがカメラ スレッドよりも低速で実行される場合、フレームの処理が遅れることはありませんが、代わりにフレームをスキップして、常に最新のフレームを配信する必要があります。解決策は、新しいフレームが利用可能になったときに、カメラ スレッドが実行中のジェネレーターに信号を送るようにすることです。ジェネレーターは、次のフレームを送信する前に信号を待機している間にブロックできます。
ジェネレーターにイベント処理ロジックを追加しないようにするには、呼び出し元のスレッド ID を使用してクライアント スレッドごとに個別のイベントを自動的に作成および管理するカスタム イベント クラスを実装します。
class CameraEvent(object):
"""An Event-like class that signals all active clients when a new frame is available.
"""
def __init__(self):
self.events = {
}
def wait(self):
"""Invoked from each client's thread to wait for the next frame."""
ident = get_ident()
if ident not in self.events:
# this is a new client
# add an entry for it in the self.events dict
# each entry has two elements, a threading.Event() and a timestamp
self.events[ident] = [threading.Event(), time.time()]
return self.events[ident][0].wait()
def set(self):
"""Invoked by the camera thread when a new frame is available."""
now = time.time()
remove = None
for ident, event in self.events.items():
if not event[0].isSet():
# if this client's event is not set, then set it
# also update the last set timestamp to now
event[0].set()
event[1] = now
else:
# if the client's event is already set, it means the client
# did not process a previous frame
# if the event stays set for more than 5 seconds, then assume
# the client is gone and remove it
if now - event[1] > 5:
remove = ident
if remove:
del self.events[remove]
def clear(self):
"""Invoked from each client's thread after a frame was processed."""
self.events[get_ident()][0].clear()
class BaseCamera(object):
# ...
event = CameraEvent()
# ...
def get_frame(self):
"""Return the current camera frame."""
BaseCamera.last_access = time.time()
# wait for a signal from the camera thread
BaseCamera.event.wait()
BaseCamera.event.clear()
return BaseCamera.frame
@classmethod
def _thread(cls):
# ...
for frame in frames_iterator:
BaseCamera.frame = frame
BaseCamera.event.set() # send signal to clients
# ...
全体的なコード:
base_camera.py
import time
import threading
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
class CameraEvent(object):
"""An Event-like class that signals all active clients when a new frame is
available.
"""
def __init__(self):
self.events = {
}
def wait(self):
"""Invoked from each client's thread to wait for the next frame."""
ident = get_ident()
if ident not in self.events:
# this is a new client
# add an entry for it in the self.events dict
# each entry has two elements, a threading.Event() and a timestamp
self.events[ident] = [threading.Event(), time.time()]
return self.events[ident][0].wait()
def set(self):
"""Invoked by the camera thread when a new frame is available."""
now = time.time()
remove = None
for ident, event in self.events.items():
if not event[0].isSet():
# if this client's event is not set, then set it
# also update the last set timestamp to now
event[0].set()
event[1] = now
else:
# if the client's event is already set, it means the client
# did not process a previous frame
# if the event stays set for more than 5 seconds, then assume
# the client is gone and remove it
if now - event[1] > 5:
remove = ident
if remove:
del self.events[remove]
def clear(self):
"""Invoked from each client's thread after a frame was processed."""
self.events[get_ident()][0].clear()
class BaseCamera(object):
thread = None # background thread that reads frames from camera
frame = None # current frame is stored here by background thread
last_access = 0 # time of last client access to the camera
event = CameraEvent()
def __init__(self):
"""Start the background camera thread if it isn't running yet."""
if BaseCamera.thread is None:
BaseCamera.last_access = time.time()
# start background frame thread
BaseCamera.thread = threading.Thread(target=self._thread)
BaseCamera.thread.start()
# wait until first frame is available
BaseCamera.event.wait()
def get_frame(self):
"""Return the current camera frame."""
BaseCamera.last_access = time.time()
# wait for a signal from the camera thread
BaseCamera.event.wait()
BaseCamera.event.clear()
return BaseCamera.frame
@staticmethod
def frames():
""""Generator that returns frames from the camera."""
raise RuntimeError('Must be implemented by subclasses.')
@classmethod
def _thread(cls):
"""Camera background thread."""
print('Starting camera thread.')
frames_iterator = cls.frames()
for frame in frames_iterator:
BaseCamera.frame = frame
BaseCamera.event.set() # send signal to clients
time.sleep(0)
# if there hasn't been any clients asking for frames in
# the last 10 seconds then stop the thread
if time.time() - BaseCamera.last_access > 10:
frames_iterator.close()
print('Stopping camera thread due to inactivity.')
break
BaseCamera.thread = None
camera.py
import os
import cv2
from base_camera import BaseCamera
class Camera(BaseCamera):
video_source = 0
def __init__(self):
if os.environ.get('OPENCV_CAMERA_SOURCE'):
Camera.set_video_source(int(os.environ['OPENCV_CAMERA_SOURCE']))
super(Camera, self).__init__()
@staticmethod
def set_video_source(source):
Camera.video_source = source
@staticmethod
def frames():
camera = cv2.VideoCapture(Camera.video_source)
if not camera.isOpened():
raise RuntimeError('Could not start camera.')
while True:
# read current frame
_, img = camera.read()
# encode as a jpeg image and return it
yield cv2.imencode('.jpg', img)[1].tobytes()
app.py
#!/usr/bin/env python
from importlib import import_module
import os
from flask import Flask, render_template, Response
# import camera driver
if os.environ.get('CAMERA'):
Camera = import_module('camera_' + os.environ['CAMERA']).Camera
else:
from camera import Camera
# Raspberry Pi camera module (requires picamera package)
# from camera_pi import Camera
app = Flask(__name__)
@app.route('/')
def index():
"""Video streaming home page."""
return render_template('index.html')
def gen(camera):
"""Video streaming generator function."""
yield b'--frame\r\n'
while True:
frame = camera.get_frame()
yield b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n--frame\r\n'
@app.route('/video_feed')
def video_feed():
"""Video streaming route. Put this in the src attribute of an img tag."""
return Response(gen(Camera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', threaded=True)
特定のコードは、私の github を参照できます: Flask-video-Stream