[React+Flask+OpenCV] FlaskとOpenCVで投稿された画像をOpenCVで加工して返す

##はじめに
Flaskで作られたWebサーバに画像が投稿されたらOpenCVで加工して結果をブラウザに表示するようにした。
axiosでimageをPOSTして、Viewに反映するのに手こずったのでメモ

##大まかな流れ
1. Indexでフォームを表示する
2. フォームで追加されたファイルを受け取る
3. OpenCVで読み込む
4. ファイルを加工する（縮小とか、色を変えたりとか）
5. 加工したファイルをクライアント側へ返す

##手順
1. Githubのリポジトリをクローン
https://github.com/toshi1127/My-tutorial-flask-and-react.git
　```git clone https://github.com/toshi1127/My-tutorial-flask-and-react.git ```
2. node_modulesをインストールする
　```npm insatll ```
3. webpackを使ってReact.jsをコンパイルする
　```npm run build ```

##使用例
###アプリケーションサーバのコード

```python:server.py
import os
import io
import time
import numpy as np
import cv2
import base64
from flask import Flask, render_template, request, redirect, url_for, Response
from io import BytesIO
from werkzeug import secure_filename
import math
import sys

app = Flask(__name__)

UPLOAD_FOLDER = './uploads'
IMAGE_FOLDER = './image'
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'PNG', 'JPG'])
IMAGE_WIDTH = 640
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['IMAGE_FOLDER'] = IMAGE_FOLDER
app.config['SECRET_KEY'] = os.urandom(24)

def allowed_file(filename):
    return '.' in filename and \
        filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

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

@app.route('/send', methods=['GET', 'POST'])
def send():
    if request.method == 'POST':
        img_file = request.files['img_file']

        # 変なファイル弾き
        if img_file and allowed_file(img_file.filename):
            filename = secure_filename(img_file.filename)
        else:
            return ''' <p>許可されていない拡張子です</p> '''

        # BytesIOで読み込んでOpenCVで扱える型にする
        f = img_file.stream.read()
        bin_data = io.BytesIO(f)
        file_bytes = np.asarray(bytearray(bin_data.read()), dtype=np.uint8)
        img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)

        # ここでOpenCVで好きな処理をする
        raw_img = cv2.resize(img, (IMAGE_WIDTH, int(IMAGE_WIDTH*img.shape[0]/img.shape[1])))
        gray_img = cv2.cvtColor(raw_img, cv2.COLOR_BGR2GRAY)

        retval, buffer = cv2.imencode('.png', gray_img)
        jpg_as_text = base64.b64encode(buffer)
        return Response(response=jpg_as_text, content_type='image/jpeg')

    else:
        return redirect(url_for('index'))

if __name__ == '__main__':
    app.debug = True
    app.run()
```
###アプリケーションクライアントのコードの流れ
1. 送信ボタンが押された時に、画像データをFormDataオブジェクトに追加し、APIを叩く
2. StateのisLoadをtureに変更して、roadComponentをレンダリングする
3. レスポンスが返ってきたら、StateのreceiveDateに画像データを格納に、Viewに反映させる

```javascript:frontend/index.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import {
    BrowserRouter,
    Route, Switch
} from 'react-router-dom'
import App from './app';

const ImageApp = () => (
    <BrowserRouter>
        <div>
            <Route path='/' component={App} />
        </div>
    </BrowserRouter>
)


ReactDOM.render(
<ImageApp/>,document.getElementById('root')
);
```
```javascript:frontend/app.js
import React from "react";
import ImageSend from './container/imageSend';

export default class App extends React.Component {

    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div>
              <ImageSend/>
            </div>
        );
    }
}
```
```javascript:container/imageSend.js
import React from "react";
import axios from "axios";
import Loading from "../component/roadComponent";

export default class ImageSend extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      imageDate: null,
      receiveDate: null,
      isLoad: false
    };
    this.onSubmit = this.onSubmit.bind(this);
    this.onInput = this.onInput.bind(this);
  }

  onSubmit(e) {
    const { imageDate } = this.state;
    const formData = new FormData();
    formData.append("img_file", this.state.imageDate, "testImage.png");
    this.setState({
      isLoad: !this.state.isLoad
    });

    axios({
      method: "post",
      url: "/send",
      data: formData,
      config: { headers: { "Content-Type": "multipart/form-data" } }
    }).then((response) => {
      this.setState({
        receiveDate: response.data,
        isLoad: !this.state.isLoad
      });
    });
  }

  onInput(e) {
    const files = e.target.files;

    this.setState({
      imageDate: files[0]
    });
  }

  render() {
    const { receiveDate, isLoad } = this.state;
    if (!isLoad) {
      return (
        <div>
          <input
            type="file"
            name="upfile"
            id="upfile"
            accept="image/*"
            capture="camera"
            onInput={this.onInput}
          />
          <button onClick={this.onSubmit}>送信</button>
          {receiveDate && <img src={`data:image/png;base64,${receiveDate}`} />}
        </div>
      );
    }
    return <Loading />;
  }
}
```
axiosでAPIを叩いて、レスポンスが返ってくるまで描画するView

```javascript:component/roadComponent.js
import React from "react";
import styled, { keyframes } from "styled-components";
import { media } from "../utile/Helper";

const rotate360 = keyframes`
  from {
    transform: rotate(0);
  }

  to {
    transform: rotate(360deg);
  }
`;

export const LoadingContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 400px;
`;

export const LoadingContent = styled.p`
  width: 70px;
  height: 70px;
  border: 4px solid #00f;
  border-top-color: #fff;
  border-radius: 100%;
  animation: ${rotate360} 0.6s linear infinite;
  background-color: #fff;
  box-sizing: border-box;
`;

const Loading = () => (
  <LoadingContainer>
    <LoadingContent />
  </LoadingContainer>
);

export default Loading;
```

```html:templates.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>File</title>
</head>
<body>
  <div id="root"></div>
  <script src="./static/bundle.js" ></script>
</body>
</html>
```


##実行結果
![image.png](https://qiita-image-store.s3.amazonaws.com/0/218201/e7c9abd2-266e-9134-b07b-610d47d090f5.png)
<img width="1019" alt="スクリーンショット 2018-09-29 10.05.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/218201/e457bd20-0329-34e7-29dd-47979cb8b173.png">