pythonで生成したImageをWebSocketでブラウザに送信してcanvasに表示する


# はじめに

RaspberryPiのカメラモジュールで撮影した画像をブラウザで表示するためにWebSocketの仕組みを調べてみました。

pythonで作成した画像データをWebSocketでブラウザに送信してcanvasに表示するテンプレートとしてまとめておきます。


# サンプルソースの仕様

1. エディットボックスに文字列を入力し、ボタンをクリックするとWebSocketで文字列を送信する

2. 文字列を受信したサーバーは文字列を画像に変換し、応答としてバイナリデータを返す

3. 画像のバイナリデータを受信したjavascriptでcanvasに描画する

![anime_websock.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264524/bcfce7dd-e162-c3c5-f182-232b67c3ac64.gif)


# クライアント側

## HTML部

テキストボックス、ボタン、canvasを持つhtmlを用意します。

```html:index.html

<html>
  <head>
    <script src="./client.js"></script>
  </head>
  <body onload="on_load();">
    <input type="button" value="send text" onclick="on_button_send_text();">
    <input type="text" id="text_input" name="text_input" value="">
    <br>
    <div>
      <canvas id="canvas_image" width="300" height="150"></canvas>
    </div>
  </body>
</html>

```

## javascript部

onloadとonclickのイベントをjavascriptで実装します。

ピクセルデータを描画するモードと、ファイル形式のバイナリデータを描画するモードを用意しました。
mode_pixcelの値を設定してください。

### ピクセルデータを描画する場合(mode_pixcel=true)

画像のピクセルデータの配列をcanvasに設定して描画します。
処理はシンプルで余計な変換処理がないので高速に描画できるはず。
画像サイズなどのメタデータは送信されないので画面側で必要になる場合は別途通信が必要になる。

### ファイル形式のデータを描画する場合(mode_pixcel=false)

data URLでImageを作成してcanvasに描画します。
PNGやjpegなどの圧縮形式で送ることができるのでデータ転送量は少ないですが、javascript側でdata URLのテキストにするので処理は重そう。


```javascript:client.js

var web_socket = null;
var mode_pixcel = true; // true:ピクセルデータ形式,false=ファイル形式

function on_load()
{
  web_socket = new WebSocket('ws://localhost:60000'); // サーバーのアドレスを指定
  web_socket.binaryType = 'arraybuffer';
  web_socket.onmessage = on_message;
};

function on_button_send_text()
{
  var text_input = document.getElementById('text_input')
  web_socket.send(text_input.value);
}

function on_message(recv_data)
{
  var recv_image_data = new Uint8Array(recv_data.data);

  var canvas_image = document.getElementById('canvas_image');
  var ctx = canvas_image.getContext('2d');

  if(mode_pixcel){
    // ピクセルデータを受信する場合
    var imageData = ctx.createImageData(300, 150);
    for (var i=0; i < imageData.data.length; i++){
      imageData.data[i] = recv_image_data[i];
    }
    ctx.putImageData(imageData, 0, 0);
  }
  else{
    // ファイル形式のデータを受信する場合
    var image = new Image();
    image.src = 'data:image/png;base64,' + window.btoa(String.fromCharCode.apply(null, recv_image_data));
    image.onload = function() {
      ctx.drawImage(image, 0, 0);
    }
  }
}



```


### 大きいサイズ画像だとエラーになる場合
大きい画像だとbase64で文字列の生成に失敗するので下記事も参照してください。
[RaspberryPiで監視カメラ（カメラモジュール＋USB Audioのデータをブラウザで表示＋再生）](https://qiita.com/magiclib/items/6e09eb22b4c8816e24c4)





# サーバー側

## パッケージ

websockets、Pillow、numpyを使用しています。

```
pip install websockets Pillow numpy

```


## Python部

接続されたらrecv()でデータの受信待ちになります。
クライアントが切断するまでサーバー側は接続を切りません。

ピクセルデータを送信する場合と、ファイル形式のバイナリデータを送信する場合のコードがあります。
client.jsの実装に合わせて、mode_pixcelの引数の値を設定してください。


```python:server.py

import asyncio
import websockets
from PIL import Image, ImageDraw, ImageFont
import numpy
import io

class WebSockets_Server:

    def __init__(self, loop, address , port, mode_pixcel):
        self.loop = loop
        self.address = address
        self.port = port
        self.mode_pixcel = mode_pixcel  # True:ピクセルデータ形式,False=ファイル形式

        self.font_path = "(font path)" # フォントのパスを指定
        self.font = ImageFont.truetype(font=self.font_path, size=80)

    async def _handler(self, websocket, path):
        while True:
            recv_data = await websocket.recv()

            image = Image.new("RGBA", (300, 150))
            draw = ImageDraw.Draw(image)
            draw.text((0, 0), recv_data, (0, 0, 255), font=self.font)

            if self.mode_pixcel:
                # ピクセルデータを送信する場合
                image_np = numpy.array(image)
                await websocket.send(image_np.tobytes())
            else:
                # ファイル形式のデータを送信する場合
                with io.BytesIO() as image_temp:
                    image.save(image_temp, format="png")
                    await websocket.send(image_temp.getvalue())

    def run(self):
        self._server = websockets.serve(self._handler, self.address, self.port)
        self.loop.run_until_complete(self._server)
        self.loop.run_forever()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    wss = WebSockets_Server(loop, '0.0.0.0', 60000, mode_pixcel=True)
    wss.run()



```
font_pathは私の環境だと以下の値を指定していました。
Windows10："C:/WINDOWS/Fonts/MSGOTHIC.ttc"
RaspberryPi："/usr/share/fonts/truetype/freefont/FreeMono.ttf"


# おわりに

WebSocketのサンプルが公開されているのはnode.jsが多く、pythonのサンプルが少なくて実現するのに時間がかかりました。

カメラモジュール用に作りましたが、仕組みはいろいろ使えそうに感じました。画像を表示するツールはTkinterで作っていましたが、WebSocket＋HTML5でUIを作るのもありな気がしてきました。機械学習結果をHTMLで表示できるようにしておけば、HTMLを保存するだけで報告レポートにすることができて便利な予感がする。


# 参考

https://gist.github.com/hagino3000/1447986