Raspberry pi 3で暗視カメラシステムの構築 その3
時間が空きましたが、前回の続きです。今回はカメラから画像を送信する処理と画像を受け取って保存しておき、要求に応じて画像を表示するhtmlを返すwebサーバーを切り分けた部分の紹介になります。ここまでで、下図の設計の左半分が出来ました。こうすることで画像を送信するRaspberr pi3とwebサーバーを別々にします。
Raspberry Pi3側画像送信スクリプト
ソースコードは以下になります。
import tornado.websocket import asyncio import tornado.ioloop import pickle import time from capture_video import CaptureVideo async def main(): url = 'ws://192.168.100.50:8888/camera?mode=send_frame' conn = await tornado.websocket.websocket_connect(url) # 画像データ送信 await send_frame_loop(conn) conn.close() async def send_frame_loop(conn): try: while True: ret, frame = CaptureVideo.get_frame() if ret: binary_image = pickle.dumps(frame) conn.write_message(binary_image, binary=True) await asyncio.sleep(0.1) except KeyboardInterrupt: conn.close() CaptureVideo.capture_start() ioloop = tornado.ioloop.IOLoop.current() ioloop.stop() ioloop.close() if __name__ == '__main__': CaptureVideo.capture_start() tornado.ioloop.IOLoop.current().run_sync(main)
webサーバー側をtornadoで実装しているため、WebSocketクライアントもtornadoを利用しました。WebSocketサーバーに接続するときに、クエリストリングでパラメーター'send_frame'を渡して、画像を送信することをサーバー側に伝えています。あとはsend_frame_loop関数で非同期定期的に画像を送り続けます。前回まででwebサーバー側でカメラから直接画像を取得していた部分を、こちらに置き換えています。画像を取得するクラスCaptureVideoに関しては前回の記事をご覧ください。
Webサーバー
WebSocketサーバーの変更
さて、Raspberr Pi3側で画像を送り、別のマシンに配置したWebサーバーで画像を受け取る構成になったため、サーバー側も変更する必要があります。ソースコードが以下になります。
import cv2 import time import asyncio import tornado import tornado.httpserver import tornado.websocket import tornado.ioloop import tornado.web import pickle from capture_video import CaptureVideo from frame_data import FrameData 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, args): print(self.request.remote_ip, ": connection opened") self.mode = self.get_query_argument('mode') print(self.mode) if self.mode == 'get_frame': 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 on_message(self, message): if self.mode == 'send_frame': frame = pickle.loads(message) frame_data = FrameData.get_instance() frame_data.set_frame(frame) def loop(self): self.ioloop.add_timeout(time.time() + 0.1, self.loop) frame_data = FrameData.get_instance() encode_param = [int(cv2.IMWRITE_JPEG_QUALITY),90] ret, decimg = cv2.imencode('.jpg', frame_data.get_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() io_loop.start() except KeyboardInterrupt: print('server stop') ioloop = tornado.ioloop.IOLoop.current() ioloop.stop() ioloop.close() if __name__=='__main__': main()
主な変更はWebSocketサーバーの部分です。WebSocketの通信を画像を受け取る場合と、画像をhttpクライアント側に送信する場合で処理を分けました。接続を開始するOpen関数でGetリクエストでクエリストリングを受け取るようにし、受け取ったパラメータが'send_frame'なら画像を受け取って保存、'get_frame'なら今まで通り画像を送信してhtmlで表示するという流れです。
これに合わせてサーバーが返すindex.htmlも少しだけ変更しています。クライアントで実行されるWebSocketに接続するjavascriptの部分で、接続するときにパラメータ'get_frame'を送るようにしました。
var ws = new WebSocket("ws://192.168.100.50:8888/camera?mode=get_frame");
Singletonで画像データの共有
また、カメラから受け取った画像は保存しておいてhttpクライアントに送らないといけないため、画像データを保存しておくためのSingletonクラスFrameDataを作成しました。Singletonはオブジェクト指向プログラミングのデザインパターンの一つで、プログラム内で、「あるクラスのインスタンスを1つしか生成しない」という仕組みを実装するものです。コードは以下です。
from threading import Lock class FrameData: # Singleton _unique_instance = None _lock = Lock() def __new__(cls): raise NotImplementedError('Cannot initialize via Constructor') def set_frame(self, frame): self._unique_instance.frame = frame def get_frame(self): if self._unique_instance.frame is not None: return self._unique_instance.frame else: print('Frame is None') return None @classmethod def __internal_new__(cls): return super().__new__(cls) @classmethod def get_instance(cls): if not cls._unique_instance: with cls._lock: if not cls._unique_instance: cls._unique_instance = cls.__internal_new__() cls._unique_instance.frame = None return cls._unique_instance
画像を保存するときにget_instanceでインスタンスを取得し、set_frame関数で画像を保存。画像を取り出すときはget_instanceでインスタンスを取得し、get_frame関数で画像を取り出しという流れです。pythonはSingletonの仕組みは標準では実装されていないようでしたので、参考にさせてもらったwebサイトに記載されていたコードを改良して自前で用意しました。Raspberry pi3カメラで接続される場合と、httpクライアントからの接続では別のWebSocketインスタンスが生成されるため、その中で画像を共有するためにSingletonで共通のインスタンスを利用する方法を取りました。
基本的な部分は今回までで完成しました。公開サーバーへのデプロイ、画像認識やレイアウトなどを次回以降でやっていこうと思います。
参考
Pythonでシングルトン(Singleton)を実装してみる - [Dd]enzow(ill)? with DB and Python