Flask-SocketIOを用いてサーバに画像を送ろう

# はじめに
Flaskで構築したWebサイトでWebカメラから取得された情報を処理し、表示したいと思い実装を行なっていた。
しかし、ローカル環境ではうまくいくが実際にVPSなどで実装を行うとうまくいかないことがあった。
自分が実装でつまずいたところなどを記述する。

:::note info
最終的にはFlask-SocketIOの実装を説明します
:::

# やりたいこと
サーバーで画像を処理しその画像をWebサイトに表示したい!!!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3464628/b646914c-ca29-c789-3628-16e70bc07572.png)
1. Webサイトを開く
2. Webカメラの情報をサーバに送る
3. サーバで情報を処理
4. Webサイト処理した情報を送る
5. Webサイトで処理された画像を表示

## 失敗したこと
最初はローカル環境(カメラがついたパソコン)で開発を行っていたため、openCVでカメラ画像の取得しその情報を表示していた。
↓ ローカル環境で動いたため、VPSでの本番環境で動かそうとしたら
**Webカメラの情報が表示されない????**

### なぜだ？
調べてみたところ、別の端末でサーバにカメラの情報を送る必要があるらしい。
ローカル環境では、自分の端末でサーバーにアクセスを行っていたため情報を送る必要がなかった。
ここが落とし穴だった....
### 解決
調べてみたところ、[WebRTC](https://webrtchacks.com/)と[Flask-SocketIo](https://flask-socketio.readthedocs.io/en/latest/)で実装することができる。
最初はWebRTCで実装を行なっていたが、SSL通信化しなければならないのでFlask-SocketIoで実装を行なった。

# Flask-SocketIoとは？
JavaScriptのSocketIOライブラリとメッセージをやりとりするためのプロトコルを実装することができるFlaskのライブラリーである。
Webサイトから情報をサーバーに送ることができる便利なライブラリだとぉ〜
サーバーに送る情報を画像情報にすれば実現できると考えた。


## 使ったライブラリー
1. Falsk
1. flask_socketio
1. base64
ソケット通信を行うために写真情報をbase64に変換する際用いた。[参考URL](https://qiita.com/TsubasaSato/items/908d4f5c241091ecbf9b)
1. OpenCV
取得された画像の色を反転する際に用いた。
1. numpy
OpenCVで写真情報を読み込む際に用いた


## コード
※このサンプルコードは、取得された画像の色を反転し表示するコードです
### ファイル構成
```
├─app.py
├── /templates
    └── index.html
```
### 全体コード
```python:app.py
from flask import Flask, render_template
from flask_socketio import SocketIO, emit
import cv2
import numpy as np
import base64

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('image')
def handle_image(image):
    # 画像データをデコード
    image_data = base64.b64decode(image.split(',')[1])
    # OpenCVで画像を読み込む
    nparr = np.frombuffer(image_data, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
    print(img.shape)
    # 画像の色を反転させる
    inverted_img = cv2.bitwise_not(img)
    
    # 反転した画像をエンコードしてクライアントに送信
    _, buffer = cv2.imencode('.jpg', inverted_img)
    inverted_image_data = base64.b64encode(buffer)
    inverted_image_str = 'data:image/jpeg;base64,' + inverted_image_data.decode('utf-8')
    emit('processed_image', inverted_image_str)

if __name__ == '__main__':
    socketio.run(app)

```
```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Camera Stream</title>
</head>
<body>
    <video id="video" width="640" height="480" autoplay></video>
    <canvas id="canvas" width="640" height="480" style="display: none;"></canvas>
    <img id="output" width="640" height="480">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
    <script>
        var socket = io();
        var video = document.getElementById('video');
        var canvas = document.getElementById('canvas');
        var context = canvas.getContext('2d');
        var output = document.getElementById('output');

        navigator.mediaDevices.getUserMedia({ video: true })
            .then(function(stream) {
                video.srcObject = stream;
            })
            .catch(function(err) {
                console.log("An error occurred: " + err);
            });

        video.addEventListener('play', function() {
            setInterval(function() {
                context.drawImage(video, 0, 0, 640, 480);
                var imageData = canvas.toDataURL('image/jpeg');
                socket.emit('image', imageData);
            }, 100); // 100ms毎に繰り返される
        });
        
        socket.on('processed_image', function(image) {
            output.src = image;
        });
    </script>
</body>
</html>

```

## コードの説明(ソケット通信の箇所を詳しく)
### pythonのコードの説明
1. **画像データの受信**:
    ```python
    image_data = base64.b64decode(image.split(',')[1])
    ```
    - クライアントから送られてくる画像データはBase64でエンコードされた文字列形式
    - 画像データは`data:image/jpeg;base64,`のような形式になっており、カンマ（`,`）で分割してBase64エンコード部分を抽出
    - `base64.b64decode()`関数を使って、このBase64エンコードされた画像データをバイナリ形式にデコード

2. **OpenCVで画像を読み込む**:
    ```python
    nparr = np.frombuffer(image_data, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
    ```
    - デコードされたバイナリデータをNumPy配列に変換
    - OpenCVの`cv2.imdecode()`関数を使用して、NumPy配列から画像をデコードし、BGR形式のカラー画像として読み込む

3. **反転した画像をエンコードしてクライアントに送信**:
    ```python
    _, buffer = cv2.imencode('.jpg', inverted_img)
    inverted_image_data = base64.b64encode(buffer)
    inverted_image_str = 'data:image/jpeg;base64,' + inverted_image_data.decode('utf-8')
    emit('processed_image', inverted_image_str)
    ```
    - 反転した画像をJPEG形式でエンコードする。このエンコードされたデータはバイナリ形式のバッファに格納される
    - バイナリ形式のデータをBase64エンコードし、クライアントに送信するための文字列に変換
    - `data:image/jpeg;base64,`というプレフィックスを付けて、画像データがBase64エンコードされたJPEG画像であることを示す
    - `emit('processed_image', inverted_image_str)`を使って、反転した画像データを`'processed_image'`イベントとしてクライアントに送信


---
### HTML側の説明 
※JavaScrptの知識があまりなかったのでAIでHTMLコードを書きました😅

1.**ソケット通信を用いるライブラリをインポート**:

```haml
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>

```

2.**サーバ側に画像を送信**:
```javascript
video.addEventListener('play', function() {
    setInterval(function() {
        context.drawImage(video, 0, 0, 640, 480);
        var imageData = canvas.toDataURL('image/jpeg');
        socket.emit('image', imageData);
    }, 100); // 100ms毎に繰り返される
});
```
- `canvas.toDataURL('image/jpeg')`でWebカメラの情報をデータURLに変換
- `socket.emit('image', imageData)`を使って、サーバ側にソケット通信で画像を送信

3.**ソケット通信で送られた情報を表示**

```javascript
socket.on('processed_image', function(image) {
    output.src = image;
});
```

## まとめ
今回の問題でFlaskを用いたソケット通信について学ぶことができた。最初はソケット通信などを使わずに実装して他のことに時間をかけようと思っていたが、ソケット通信を使わないとできないということが判明したので時間がかかってしまった。
まだ授業のプロジェクトだから良かったが、社会に出た際に納期があったらと思ったら怖い怖い。

## 参考URL
[Flask-SocketIO](https://flask-socketio.readthedocs.io/en/latest/)
[【備忘録】Flask-SocketIOを使ってリアルタイム更新のチャットbotを作成した話。](https://qiita.com/Yu_unI1/items/53570f021363d9f57d4c)
[Flask APIでsocketioを使いリアルタイム通信をする](https://www.abc.osaka/flask-api/socketio)