読者です 読者をやめる 読者になる 読者になる

ami_GS's diary

情報系大学院生の備忘録。ネットワークの勉強にハマっています。

Raspberry Piのカメラモジュールで撮った映像をWebSocketでブラウザに送る!!

Python WebSocket javascript Raspberry Pi

はじめに

RPiのカメラモジュールで撮った映像をリアルタイムで見たくありませんか??

というわけで、色々調べてみると、ffmpegやらMJPG-streamerやら使っていますね・・・・

自分の手で実装してぇんだよ!!!!

と思ったので、書いてみたものを紹介します。

構成

必要なものは、

  • camera.py

RPi側でブラウザからのアクセスを受け付けるwebサーバ、及びカメラから映像を撮り、ブラウザへ送る。

  • index.html

WebSocketでブラウザに送られてきた画像を表示する。

の2つです。

camera.py

import割愛

WIDTH = 480
HEIGHT = 360
FPS = 30
class HttpHandler(tornado.web.RequestHandler):
    def initialize(self):
            pass

    def get(self):
        self.render("./index.html")  #最初のHTTPアクセスを受け付け、WebSocket接続を確立させるスクリプトが入ったindex.htmlを返す

class WSHandler(WebSocketHandler):
    def initialize(self, camera):
        self.camera = camera
        self.state = True

    def open(self):
        print(self.request.remote_ip, ": connection opened")
        t = Thread(target=self.loop)    #撮影&送信スレッドの作成
        t.setDaemon(True)
        t.start()

    def loop(self):
        for foo in self.camera.capture_continuous(self.camera.stream, "jpeg", use_video_port=True):
            self.camera.stream.seek(0)
            self.write_message(self.camera.stream.read(), binary=True)
            self.camera.stream.seek(0)
            self.camera.stream.truncate()
            if not self.state:
                break

    def on_close(self):
        self.state = False     #映像送信のループを終了させる
        self.close()     #WebSocketセッションを閉じる
        print(self.request.remote_ip, ": connection closed")

def piCamera():
    camera = picamera.PiCamera()
    camera.resolution = (WIDTH, HEIGHT)
    camera.framerate = FPS
    camera.stream = io.BytesIO()     #ストリームIO
    time.sleep(2)        #カメラ初期化
    return camera

def main():
    camera = piCamera()
    print("complete initialization")
    app = tornado.web.Application([
        (r"/", HttpHandler),                     #最初のアクセスを受け付けるHTTPハンドラ
        (r"/camera", WSHandler, dict(camera=camera)),   #WebSocket接続を待ち受けるハンドラ
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(8080)
    IOLoop.instance().start()

if __name__ == "__main__":
    main()


main関数で2つのハンドラ(HTTP、WebSocket)を作ることで、最初の接続をHTTPで受け付け、そのレスポンスとしてWebSocket接続を行うHTMLファイルを返します。


また、撮影&送信のメソッドを別のスレッドにしているのは、Tornadoで作ったサーバがノンブロッキングでイベントループの動作をするからです。
サーバと撮影&送信を一つのスレッドで動かすとクライアント側で映像の取得は出来るのですが、サーバ側WebSocketのイベントハンドラが機能しなくなるので別スレッドにしました。
カメラモジュールを速く動作させるためにも以下のループが必要なので、スレッドを分けるのは必須かもしれません。

    def loop(self):
        for foo in self.camera.capture_continuous(self.camera.stream, "jpeg", use_video_port=True):
            self.camera.stream.seek(0)
            self.write_message(self.camera.stream.read(), binary=True)
            self.camera.stream.seek(0)
            self.camera.stream.truncate()

index.html

htmlファイルだけどjavascriptがほとんどです。

<html>
<head>
<title>livecamera</title>
<img id="liveImg" src="" width="480" height="360">
<script type="text/javascript">
var img = document.getElementById("liveImg");  
var arrayBuffer;

//WebSocketでサーバに接続
var ws = new WebSocket("ws://使っているRPiのホスト名 or IPアドレス:8080/camera"); ws.binaryType = 'arraybuffer';   //受診するデータがバイナリであるので設定

ws.onopen = function(){console.log("connection was established");};  //接続が確立した時に呼ばれる
ws.onmessage = function(evt){
	arrayBuffer = evt.data;
        //受信したデータを復号しbase64でエンコード
	img.src = "data:image/jpeg;base64," + encode(new Uint8Array(arrayBuffer));
};

window.onbeforeunload = function(){
    //ウィンドウ(タブ)を閉じたらサーバにセッションの終了を知らせる
    ws.close(1000);
};

function encode (input) {
    var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    var output = "";
    var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
    var i = 0;

    while (i < input.length) {
        chr1 = input[i++];
        chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index
        chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here

        enc1 = chr1 >> 2;
        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
        enc4 = chr3 & 63;

        if (isNaN(chr2)) {
            enc3 = enc4 = 64;
        } else if (isNaN(chr3)) {
            enc4 = 64;
        }
        output += keyStr.charAt(enc1) + keyStr.charAt(enc2) +
                  keyStr.charAt(enc3) + keyStr.charAt(enc4);
    }
    return output;
}
</script>
</head>
</html>


基本的にセッションを確立したら画像を受け取り続けるだけです。
ただ、WebSocketはデータをバイナリ(またはUTF-8)で送っくるので、ちょっとトリッキーな処理が必要です。

以下の1行が必要なことに気づかずに3週間ほど手を止めていましたがStackOverflowのおかげで解決しました。

ws.binaryType = 'arraybuffer';

また、ブラウザでデータを表示させる場合にはbase64エンコードが必要なので、以下の様な処理をしています。

"data:image/jpeg;base64," + encode(new Uint8Array(arrayBuffer));

画像データによってpngやらgifやらに置き換えるとよいでしょう。


encode関数は
How can you encode a string to Base64 in JavaScript? - Stack Overflow
ここから使わせていただいたので、あまりよくわかっていません・・・
送られてきたバイナリをどうにかしているんだと思います。

動かす

以上2つのファイルをRPiの同じディレクトリに置き、次のコマンドでRPiがHTTPサーバとして動き出します。

python camera.py

"complete initialization"

が出力されたことを確認したら、PCのブラウザからアクセスしてみましょう。

※PCがRaspberry Piを参照できるネットワークに接続されていることを確認して下さい。
URL入力フォームに
使っているRPiのホスト名 or IPアドレス:8080
と入力すれば完了です!
WebSocketの接続が確立し、映像が送られてきます。

こんな感じになりました(人形はGopher君)
f:id:ami_GS:20140409225641p:plain


最後に

何が難しいかって、「Pythonで撮った映像をバイナリで送り、javascriptで受け取って復号する」事が本当にわけわからなかった・・・
javascriptもっと勉強しなきゃいけないなぁと思った今日このごろでした。