【初心者向け React×Flask】axiosでPostされた画像を処理するチュートリアル

# この記事でやること

**フロントエンド側**
- ユーザーから画像を受け取り
- バックエンドに画像をアップロード
- 返ってきた画像を表示
- React、axiosを使用

**バックエンド側**
- フロントエンドから画像を受け取り
- 画像をモノクロに処理
- 処理済み画像をフロントエンドに返す
- Flask、Pillowを使用

⭐いずれも、コードが何をやってるか理解してもらうため、最小構成で書こうと思います。冗長ですが、バックエンド側のフォルダ構成は[Github](https://github.com/Melonps/FlaskStartSample)に記載しています。


# 環境
**フロントエンド側**
- react 18.2.0
- axios 1.4.0

axiosのインストール
```
$ npm install axios
```

**バックエンド側**
```
$ python -V
Python 3.11.4
```

⭐以下のモジュールを`pip install`してください
- flask
- flask_cors
- Pillow
# 先にコード
**フロントエンド側**
⭐コンポーネントの全文を載せます。適当なviewsで読み込んでください。

```jsx:ConvertMonoImage.jsx
import React, { useState } from "react";
import axios from "axios";

const ConvertMonoImage = () => {
    // 選択された元の画像ファイルと変換後の画像ファイルを管理するための状態
    const [selectedFile, setSelectedFile] = useState(null);
    const [convertedFile, setConvertedFile] = useState(null);

    // ローディング状態やエラーメッセージを管理するための状態
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);

    // Flask APIのURL
    const url =
        "http://127.0.0.1:5000/convertToMonochrome";

    // フォームの送信時のハンドラ関数
    const handleSubmit = async (event) => {
        event.preventDefault();

        // 選択されたファイルがない場合はエラーメッセージをセットして処理を中断
        if (!selectedFile) {
            setError("No image file selected");
            return;
        }

        // FormDataオブジェクトに選択されたファイルを追加
        const formData = new FormData();
        formData.append("image", selectedFile);

        try {
            setIsLoading(true);

            // フォームデータを使ってFlask APIにPOSTリクエストを送信
            const response = await axios.post(url, formData, {
                headers: {
                    "Content-Type": "multipart/form-data",
                },
                responseType: "blob",
            });

            // レスポンスデータをBlobオブジェクトとして扱い、変換後の画像URLをセット
            const blob = new Blob([response.data], { type: "image/png" });
            setConvertedFile(URL.createObjectURL(blob));
            setError(null);
        } catch (error) {
            console.error(error);
            setError("An error occurred during the request");
        } finally {
            setIsLoading(false);
        }
    };

    // 画像ファイルのアップロード時のハンドラ関数
    const handleImageUpload = (event) => {
        const file = event.target.files[0];
        setSelectedFile(file);
        setConvertedFile(null);
        setError(null);
    };

    return (
        <div className="container">
            {/* 画像ファイルの選択用の入力フィールド */}
            <input type="file" accept="image/*" onChange={handleImageUpload} />

            {/* 変換ボタン */}
            <button
                onClick={handleSubmit}
                disabled={!selectedFile || isLoading}
            >
                {isLoading ? "Loading..." : "Convert to Monochrome"}
            </button>

            {/* エラーメッセージの表示 */}
            {error && <p>Error: {error}</p>}

            {/* 変換後の画像の表示 */}
            {convertedFile && (
                <div>
                    <p>Image Created!!</p>
                    <img src={convertedFile} alt="Image" />
                </div>
            )}
        </div>
    );
};

export default ConvertMonoImage;

```
⭐今回、コンポーネントの呼び出し側は省いています。


**バックエンド側**
✅様々な機能を省いています。実運用だと例えば、本来は無効な拡張子が来たらはじくなど、工夫が必要です。

```python:app.py
from flask import Flask, request, send_file  # Flaskおよび関連するモジュール
from flask_cors import CORS  # CORSを使用するため
from PIL import Image  # 画像処理をするため
import io  # バイナリデータを扱うため
import os  # 環境変数を扱うため

app = Flask(__name__)  # Flaskアプリケーションを作成
CORS(app)  # CORSを有効に

@app.route("/convertToMonochrome", methods=["POST"])  # /convertToMonochromeエンドポイントにPOSTメソッドでアクセスがあった場合の処理を定義
def convert_to_monochrome():
    if "image" not in request.files:  # リクエストにimageというファイルが含まれていない場合
        return "No image file provided", 400  # エラーメッセージを返し、ステータスコード400をレスポンス
    image_file = request.files["image"]  # リクエストからimageファイルを取得
    image = Image.open(image_file)  # 画像ファイルをopen

    # 画像をモノクロに変換する処理
    monochrome_image = image.convert("L")

    # モノクロ画像をバイナリデータに変換
    output = io.BytesIO()
    monochrome_image.save(output, format="PNG")
    output.seek(0)

    return send_file(output, mimetype="image/png")  # 変換されたモノクロ画像をレスポンス

if __name__ == "__main__":
    app.run(debug=True, port=os.getenv("PORT", default=5000))  # アプリケーションをデバッグモードで実行し、環境変数からポート番号を取得して指定（デフォルトは5000番ポート）

```
⭐flaskサーバの立ち上げ
```shell
$ flask run
* Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [03/Jul/2023 12:00:39] "POST /convertToMonochrome HTTP/1.1" 200 -
```
## うまく行った場合
⭐自作キャラクター（かき氷ちゃん）の画像をグレースケール化してみました。styleは何も指定してません。
![スクリーンショット 2023-07-03 121119.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2770316/56c44973-b1d8-3e42-17fc-afb2992f220a.png "モノクロ化されたかき氷ちゃん")

# 解説(フロントエンド側)
⭐各ブロックについて解説してみます。
**1. 状態の管理**
```jsx
const [selectedFile, setSelectedFile] = useState(null);
const [convertedFile, setConvertedFile] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
```
- `selectedFile`: 選択された元の画像ファイル
- `convertedFile`: 変換後の画像ファイル
- `isLoading`: ローディング状態を管理するフラグ
- `error`: エラーメッセージ

**2.フォームの送信**
```jsx
const handleSubmit = async (event) => {
  event.preventDefault();
  if (!selectedFile) {
    setError("No image file selected");
    return;
  }
  // 選択されたファイルをFormDataに追加
  const formData = new FormData();
  formData.append("image", selectedFile);
  try {
    setIsLoading(true);
    // Flask APIにPOSTリクエストを送信
    const response = await axios.post(url, formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
      responseType: "blob",
    });
    // レスポンスデータをBlobオブジェクトとして扱い、変換後の画像URLをセット
    const blob = new Blob([response.data], { type: "image/png" });
    setConvertedFile(URL.createObjectURL(blob));
    setError(null);
  } catch (error) {
    console.error(error);
    setError("An error occurred during the request");
  } finally {
    setIsLoading(false);
  }
};
```
- `event.preventDefault()` 
    - フォームの送信を防ぐために、フォームのイベントデフォルトアクションをキャンセル
    - この関数は、通常フォームが送信されるとページがリロードされるのを防止
- `FormData`
    - ファイルを送信するための準備として、オブジェクトを作成
    - ここでは選択されたファイルを `image` というキーで追加
- `axios.post() `
    - 第1引数：APIのURLで、変数 url に格納されていると仮定
    - 第2引数：`formData` オブジェクトで、サーバーに送信するファイル
    - 第3引数：リクエストのヘッダーを指定するオブジェクト。
    ここでは、「Content-Type」を`"multipart/form-data"` に設定。
    これは、PDFや動画、画像などファイルを含むフォームデータを送信する際に必要
    - オプション： `responseType` で、レスポンスのデータ型を指定。
      ここでは、`"blob"` を指定していて、バイナリデータを表すオブジェクトのことを言っている。
- `Blob `
    - 成功した場合：レスポンスのデータを `Blob` オブジェクトとして受け取り
    それを新しい `blob` 変数に格納
    - 失敗した場合：コンソールにエラーメッセージを表示し、というエラーメッセージをセット
- `URL.createObjectURL()`
    - 変換後の画像URLを取得するため。`<img src={}>`に渡したいからね
    - `blob` を引数として渡して、`Blob`オブジェクトを参照するためのURLを作成

**3.レンダリング**
```jsx
return (
  <div className="image">
    <input type="file" accept="image/*" onChange={handleImageUpload} />

    <button onClick={handleSubmit} disabled={!selectedFile || isLoading}>
      {isLoading ? "Loading..." : "Convert to Monochrome"}
    </button>

    {error && <p>Error: {error}</p>}

    {convertedFile && (
      <div>
        <p>Image Created!!</p>
        <img src={convertedFile} alt="Image" />
      </div>
    )}
  </div>
);
```

- `<input type="file" accept="image/*" onChange={handleImageUpload} />`
    -  ファイルのアップロードを行うためのinput要素
    -  type属性が"file"に設定されており、accept属性が`"image/*"`に設定
    →ユーザーは画像ファイルのみを選択可
    -  ユーザーがファイルを選択したときには、`onChange`イベントで指定された`handleImageUpload`関数が実行
-  `<button onClick={handleSubmit} disabled={!selectedFile || isLoading}>`
    -   ボタンがクリックされたとき、onClickイベントで指定された`handleSubmit`関数を実行
    -   disabled属性は、selectedFile（選択されたファイル）がないか、`isLoading`（ロード中）の場合にボタンを無効化
- `{isLoading ? "Loading..." : "Convert to Monochrome"}`
    - `isLoading`(画像の変換が行われている間)がtrueの場合は"Loading..."と表示
    - それ以外の場合は`"Convert to Monochrome"`と表示
- `{convertedFile && (...)}`
    - `convertedFile`(変換された画像ファイルのURL)が存在する場合に、括弧内の要素を表示
# 解説(バックエンド側)
**1. 画像の受け取り**
```python
image_file = request.files["image"]
image = Image.open(image_file)
monochrome_image = image.convert("L")
```
1. リクエストから`image`ファイルを取得
2. `request.files["image"]`でファイルオブジェクトを取得（フロントの`FormData`と合致！）
3. それを`Image.open()`で画像オブジェクトに変換
4. `convert()`メソッドを使用して画像をモノクロに変換

**2. バイナリデータへの変換**
```python
output = io.BytesIO()
monochrome_image.save(output, format="PNG")
output.seek(0)
```
⭐メモリ上にバイナリデータとして保管します。
1. `io.BytesIO()`：出力用のバッファを作成（一時的なストレージ）
2. `save()`：モノクロ画像をPNG形式でバッファに保存
    `format`パラメータには保存するファイル形式を指定
3. `output.seek(0)`: outputバッファ内のポインタを先頭位置に移動
→後続の操作でバッファのデータを読み取る際に、先頭から順番に読み込むことが可能

**3. レスポンス**
```python
return send_file(output, mimetype="image/png")
```
`send_file` 関数を使用して、バイナリデータをレスポンスとして送信
`mimetype`パラメータは、レスポンスのMIMEタイプを指定します。
    この場合、PNG形式の画像を返すために`"image/png"`としてます。
# さいごに

✨今回はReactとFlaskを用いて互いにつじつまが合うような実装と解説を行ってみました。
responseTypeだのheaderだのとっつきにくい部分が多いフロントエンド側、
フロント側を理解しないと書けないバックエンド側ですが、
これが理解できると色々な機能の実装に繋げることができると思います。

最後まで読んでいただき、ありがとうございました！！