WEBサービスで動画を配信するとき、HTMLのVideoタグを利用して、動画を再生させることがあります。
しかし、videoタグは指定された動画をそのまま読み込んで再生するだけです。そのため、データ容量の大きい動画ファイルの場合に、読み込みに時間がかかってしまうという問題があります。
そこで、WEBサーバー側で動画をストリーミングするのにどうしようかを考えていました。その過程で、FlaskとOpenCVでWEBカメラの映像をストリーミングする、ということをやったので、その方法について書きます。
ストリーミングとは
ストリーミングとは、画像や動画などの容量の大きなデータを、小さいまとまりに分けて少しずつ転送/再生することにより、ユーザーの待ち時間を少なくする技術です。
たとえば、HTML5のvideoタグでは、ブラウザで動画を開いた直後、ユーザーはすべてのデータをまとめて受け取り、その後で再生が行われます。そのため、データ量が大きい場合には、再生されるまでに待ち時間が発生します。
これによって、ユーザーが待ちきれずにサイトから離脱してしまう、といった問題が起こり得ます。
一方、ストリーミングを行うと、ユーザーはデータを少しずつ受け取りながら動画を再生できます。そのため、動画全体の読みことを待つことなく、すぐに再生を始めることができるのです。
今回は、まず、Flaskで画像のストリーミングを行います。その後、WEBカメラから映像を取得し、画像と同様にストリーミングを行います。
実行環境
- Windows 10 home
- Python 3.6.3
- Flask 1.0.2
- opencv-python 3.3.0.10
Flaskで画像をストリーミング配信する
今回は、まず簡単なHTMLテンプレートを用意し、その中にimg
タグを埋め込んで、画像をストリーミング配信します。言葉で説明するよりも、まずは、コードを見て頂くと分かりやすいです。
HTMLテンプレートを用意します。
<html>
<head>
<title>Flask Streaming Test</title>
</head>
<body>
<h1>Flask Streaming Test</h1>
<img src="{{ url_for('feed') }}">
</body>
</html>
index.html
では、imgタグを設置して、srcをurl_for('feed')
で読み込んでいます。ここで画像をストリーミングします。
次に、/feed
へのアクセスに対する、レスポンスを書いていきます。
from flask import Flask, render_template, Response
from camera import Camera
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
def generate(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('/feed')
def feed():
return Response(generate(Camera()), mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
feed
へのアクセスは、generate(camera)
メソッドを返します。generate(camera)
メソッドの引数には、Camera
オブジェクトを設定します。
そしてgenerate(camera)
メソッドでは、Camera
クラスのget_frame()
メソッドを呼び出し、frame
を取得して、それをyield
して返します。
続いて、Camera
クラスを実装します。
from time import time
class Camera(object):
def __init__(self):
self.frames = []
self.files = ['1', '2', '3', '4', '5']
for f in self.files:
self.frames.append(open('./static/img/' + f + '.jpg', 'rb').read())
def get_frame(self):
return self.frames[int(time()) % len(self.files)]
cam = Camera()
if __name__=='__main__':
cam.open_camera()
Cameraクラスのインスタンスが生成されると、./static/img/
直下にあるファイル 1.jpg
, 2.jpg
, ... , 5.jpg
が読み込まれ、frames
という名前のリストに格納されます。そして、get_frame()
メソッドで、時間の変化に応じて、それらのフレームが返されます。
非常にシンプルですが、これで、ストリーミング処理は完成です。
今回は、以下の画像を 1.jpg
, 2.jpg
, ... , 5.jpg
として、動かしてみました。
それぞれの画像は、Windowsに標準インストールされている「ペイント」という素晴らしいお絵かきソフトで作成しました。
コードを完成させたら、python stream.py
を実行して、サーバーを起動します。 http://localhost:5000 にアクセスすると、以下のように画像がストリーミングされます。
これは、ブラウザをキャプチャした動画をgif画像に変換していますが、実際には、ちゃんとストリーミングされています。
WEBカメラの映像をストリーミングする
続いて、WEBカメラで撮影した映像をストリーミングしてみましょう。映像とはいうものの、その実態は、一定間隔で撮影した画像をストリーミングするだけです。コマ撮りです。
WEBカメラで撮影した画像を使うこと以外、上記の画像のストリーミングとなんら変わりません。WEBカメラによる撮影と画像の保存には、OpenCVを利用します。
camera.py
にVideoCamera
クラスを追加します。
from time import time, sleep
import cv2
# VideoCameraを追加
class VideoCamera():
def __init__(self):
self.frames = []
self.files = []
self.MAX_FRAME_NUM = 10 # 撮影枚数を指定
self.RECORD_INTERVAL = 1 # 撮影のインターバルを指定
self.record()
def get_frame(self):
# 撮影された画像を順に返す
for file in self.files:
self.frames.append(open(file, 'rb').read())
return self.frames[int(time()) % len(self.frames)]
def record(self):
self.cap = cv2.VideoCapture(0)
cnt = 0
while True:
# 撮影したファイルの保存先を指定
path = './static/img/img' + str(cnt) + '.jpg'
ret, frame = self.cap.read()
self.files.append(path)
cv2.imwrite(path, frame)
# ESCキーで停止する
k = cv2.waitKey(1)
if k == 27:
break
# 指定した枚数を撮影したら停止する
cnt += 1
if cnt == self.MAX_FRAME_NUM:
break
# 撮影のインターバルを取る
sleep(self.RECORD_INTERVAL)
self.cap.release()
cv2.destroyAllWindows()
stream.py
は、VideoCamera
クラスを利用できるように書き換えます。
from flask import Flask, render_template, Response
# from camera import Camera
# VideoCameraに変更
from camera import VideoCamera
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
def generate(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('/feed')
def feed():
# generateメソッドの引数をVideoCameraに変更
return Response(generate(VideoCamera()), mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
やっていることは非常に単純です。OpenCVで画像を撮影して保存します。その後、前述の画像のストリーミングと同様にストリーミングを行います。
これで、http://localhost:5000 にアクセスするとWEBカメラが起動し、撮影が始まります。撮影が終わると、撮影された画像がストリーミングされます。
特に撮るものがなかったので、JET BRAINSのヨーヨーを揺らしています。
カクカクですね。
今後の課題
課題は様々ありますが、大きなものは以下の2点です。
- 写真を撮り終わらないと、ストリーミングが始まらない(ストリーミングの意味がない)
- コマ撮りではなく、動画を撮影してストリーミングしたい
1については、マルチスレッドで書けば良さそうですね。2については、今回は力不足で実装できなかったため、何か方法を考えて、改めてやってみようと思います。