ITと雑記とド田舎と

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

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

前回の記事の続きになります。今回はRaspberry Pi 3 BにpythonTronadoのWebサーバーを立てて暗視カメラで撮影した映像をWebブラウザから確認するところまでを紹介します。

 

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

 

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ページのものを少し改良して使用させていただきました。

 

f:id:kdn-1017-wttd:20181129222849p:plain

Live映像

 

今回はここまでとなります。最初にシステムの概要図を再掲しましたが、このままだとすべてRaspberry Pi上で完結しています。そこで次回ではWebサーバーを別のPC上にたてて、そこにRaspberry Piから画像データを送信し、クライアントからの要求に対して受け取った画像データを送るというように構成を変更するところまでできればと思っています。

 

参考

 

websocketでRaspberryPiからwindowsPCに画像を送る(Python) - ロボット、電子工作、IoT、AIなどの開発記録

Tornadoを使ってブラウザをPythonの表示器代わりに使うメモ - Qiita

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

AjaxでバイナリのJPEG画像データを受け取って表示する - Qiita