この記事でやること
フロントエンド側
- ユーザーから画像を受け取り
- バックエンドに画像をアップロード
- 返ってきた画像を表示
- React、axiosを使用
バックエンド側
- フロントエンドから画像を受け取り
- 画像をモノクロに処理
- 処理済み画像をフロントエンドに返す
- Flask、Pillowを使用
⭐いずれも、コードが何をやってるか理解してもらうため、最小構成で書こうと思います。冗長ですが、バックエンド側のフォルダ構成はGithubに記載しています。
環境
フロントエンド側
- 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で読み込んでください。
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;
⭐今回、コンポーネントの呼び出し側は省いています。
バックエンド側
✅様々な機能を省いています。実運用だと例えば、本来は無効な拡張子が来たらはじくなど、工夫が必要です。
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サーバの立ち上げ
$ 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は何も指定してません。
解説(フロントエンド側)
⭐各ブロックについて解説してみます。
1. 状態の管理
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.フォームの送信
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を作成
- 変換後の画像URLを取得するため。
3.レンダリング
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
(ロード中)の場合にボタンを無効化
- ボタンがクリックされたとき、onClickイベントで指定された
-
{isLoading ? "Loading..." : "Convert to Monochrome"}
-
isLoading
(画像の変換が行われている間)がtrueの場合は"Loading..."と表示 - それ以外の場合は
"Convert to Monochrome"
と表示
-
-
{convertedFile && (...)}
-
convertedFile
(変換された画像ファイルのURL)が存在する場合に、括弧内の要素を表示
-
解説(バックエンド側)
1. 画像の受け取り
image_file = request.files["image"]
image = Image.open(image_file)
monochrome_image = image.convert("L")
- リクエストから
image
ファイルを取得 -
request.files["image"]
でファイルオブジェクトを取得(フロントのFormData
と合致!) - それを
Image.open()
で画像オブジェクトに変換 -
convert()
メソッドを使用して画像をモノクロに変換
2. バイナリデータへの変換
output = io.BytesIO()
monochrome_image.save(output, format="PNG")
output.seek(0)
⭐メモリ上にバイナリデータとして保管します。
-
io.BytesIO()
:出力用のバッファを作成(一時的なストレージ) -
save()
:モノクロ画像をPNG形式でバッファに保存
format
パラメータには保存するファイル形式を指定 -
output.seek(0)
: outputバッファ内のポインタを先頭位置に移動
→後続の操作でバッファのデータを読み取る際に、先頭から順番に読み込むことが可能
3. レスポンス
return send_file(output, mimetype="image/png")
send_file
関数を使用して、バイナリデータをレスポンスとして送信
mimetype
パラメータは、レスポンスのMIMEタイプを指定します。
この場合、PNG形式の画像を返すために"image/png"
としてます。
さいごに
✨今回はReactとFlaskを用いて互いにつじつまが合うような実装と解説を行ってみました。
responseTypeだのheaderだのとっつきにくい部分が多いフロントエンド側、
フロント側を理解しないと書けないバックエンド側ですが、
これが理解できると色々な機能の実装に繋げることができると思います。
最後まで読んでいただき、ありがとうございました!!