Raspberry pi 3で暗視カメラシステムの構築 その2
前回の記事の続きになります。今回はRaspberry Pi 3 BにpythonでTronadoのWebサーバーを立てて暗視カメラで撮影した映像をWebブラウザから確認するところまでを紹介します。
Raspberry Pi上のwebサーバー
TornadoのWebServer
以下がpython3で書いたコードになります。
import cv2 import time import asyncio import tornado import tornado.httpserver import tornado.websocket import tornado.ioloop import tornado.web from capture_video import CaptureVideo def capture_frame(): CaptureVideo.capture_start() class HttpHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def initialize(self): pass def get(self): self.render('./index.html') class WSHandler(tornado.websocket.WebSocketHandler): def open(self): print(self.request.remote_ip, ": connection opened") self.ioloop = tornado.ioloop.IOLoop.current() self.loop() def on_close(self): print("Session closed") self.close() def check_origin(self, origin): return True def loop(self): self.ioloop.add_timeout(time.time() + 0.1, self.loop) frame = CaptureVideo.get_frame() encode_param = [int(cv2.IMWRITE_JPEG_QUALITY),90] ret, decimg = cv2.imencode('.jpg', frame, encode_param) if self.ws_connection: if ret: message = decimg.tobytes() self.write_message(message, binary=True) def main(): try: app = tornado.web.Application([ (r'/', HttpHandler), (r'/camera', WSHandler), ]) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(8888) print('server start') io_loop = tornado.ioloop.IOLoop.current() capture_frame() io_loop.start() except KeyboardInterrupt: print('server stop') ioloop = tornado.ioloop.IOLoop.current() ioloop.stop() ioloop.close() if __name__=='__main__': main()
httpでgetリクエストを待ち受けて、Webブラウザに動画表示用のhtmlファイルを返します。Webブラウザ側はWebSocketで接続して画像を取得するようにするため、WebSocketの通信を待ち受けるHandlerも実装しています。WebSocketの通信が確立されるとloopメソッドが呼び出されて画像をクライアント側に送信し続けるようにようにしました。
画像はOpenCVを利用して処理します。OepnCVの導入については前回の記事をご覧ください。WebSocketでWebブラウザに画像を送信する前にJPEG形式に変換してバイナリデータとして送信するという流れになります。
Cameraから画像キャプチャ
続いて上記のコードで利用しているCaptureVideoクラスについて説明します。このクラスはpythonのasyncioライブラリを利用してWebサーバーとは非同期にカメラから画像を取得するために作成しました。
import asyncio import cv2 class CaptureVideo: is_running = False frame = None @classmethod def capture_start(cls, camera_id=0): if not cls.is_running: cls.is_running = True asyncio.ensure_future(cls.__capture_loop(camera_id)) @classmethod def get_frame(cls): if cls.is_running: return cls.frame else: print('capture loop is not running!') @classmethod def stop_capture(cls): if cls.is_running: cls.is_running = False @classmethod async def __capture_loop(cls, camera_id): cap = cv2.VideoCapture(camera_id) try: while cls.is_running: ret, cls.frame = cap.read() await asyncio.sleep(0.1) except: loop = asyncio.get_event_loop() loop.stop() cls.isrunning = False cap.release()
__capture_loopメソッドが非同期にカメラの画像を取得し続けるメソッドです。awaitの部分で処理を中断して制御をWebサーバー側に渡しています。
詳しくはこちら。このクラスを先に記載したWebサーバーの方でimportしてio_loop.startの前にカメラから画像を取得する処理を開始し、WebSocketのループの中で必要に応じてget_frameメソッドにより画像を取得するという仕組みです。
最新のTornadoではpython3.5から導入されたこのasyncioライブラリを利用して非同期処理を行うのことを推奨しています。この辺りは日本語で説明されている記事がとても少ない印象でしたので、興味のある方は先ほどリンクを張ったTronadoの公式リファレンスを読まれるのが一番良いと思います。
また、日本語の記事ですとTornadoでWebサーバーを待ち受け開始する際に利用するメソッドであるIOLoop.current()がIOLoop.instance()になっているものが多いのです。こちらは公式で非推奨になっているメソッドで、IOLoop.current()を利用するように記載されています。
ライブ動画を表示するhtml
最後にWebサーバーから返されるhtmlファイルを説明します。
<html> <head> <title>livecamera</title> <font size="5"><b>livecamera by Raspberry Pi3</b></font><br> <img id="liveImg" src=""> <script type="text/javascript"> var img = document.getElementById("liveImg"); var ws = new WebSocket("ws://192.168.11.205:8888/camera"); ws.onopen = function(){console.log("connection was established")}; ws.onmessage = function(evt){ window.URL.revokeObjectURL(img.src) img.src = window.URL.createObjectURL(evt.data); }; window.onbeforelaod = function(){ ws.close(1000); }; </script> </head> </html>
ブラウザでWebサーバーにアクセスするとこちらのhtmlファイルが表示され、Javascriptで書かれたWebSocketに接続する部分が実行されます。あとは受け取った画像をhtml5のimgタグで表示するというものです。こちらは、参考にさせていただいたWebページのものを少し改良して使用させていただきました。
今回はここまでとなります。最初にシステムの概要図を再掲しましたが、このままだとすべてRaspberry Pi上で完結しています。そこで次回ではWebサーバーを別のPC上にたてて、そこにRaspberry Piから画像データを送信し、クライアントからの要求に対して受け取った画像データを送るというように構成を変更するところまでできればと思っています。
参考
websocketでRaspberryPiからwindowsPCに画像を送る(Python) - ロボット、電子工作、IoT、AIなどの開発記録
Tornadoを使ってブラウザをPythonの表示器代わりに使うメモ - Qiita
【ChatDeTornado】TornadoでWebSocketを使ってチャットを作る - Qiita
AjaxでバイナリのJPEG画像データを受け取って表示する - Qiita