LoginSignup
10
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-29

はじめに

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

使用例

アプリケーションサーバのコード

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に反映させる
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')
);
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>
        );
    }
}
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

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;
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
スクリーンショット 2018-09-29 10.05.31.png

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5