ITと雑記とド田舎と

ド田舎在住エンジニアがIT備忘録と雑記を書くブログです

Raspberry pi 3で暗視カメラシステムの構築 その3

時間が空きましたが、前回の続きです。今回はカメラから画像を送信する処理と画像を受け取って保存しておき、要求に応じて画像を表示するhtmlを返すwebサーバーを切り分けた部分の紹介になります。ここまでで、下図の設計の左半分が出来ました。こうすることで画像を送信するRaspberr pi3とwebサーバーを別々にします。

 

f:id:kdn-1017-wttd:20181111094136j:plain

システム設計図

 

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

【ChatDeTornado】TornadoでWebSocketを使ってチャットを作る - Qiita