LoginSignup
13
2

推しのグッズ画像を透過して眺める平和なWebサイトを作ってみた

Last updated at Posted at 2023-12-07

クソアプリアドベントカレンダー2023 8日目担当の@Funobuです。
普段は大学4年生として、単位と戦いながらソフトウェアエンジニアをやっています。
この記事では、以下のことについて学べます。

  1. rembgを使った、Pythonにおける画像透過処理の方法
  2. ReactとFastAPIの環境における画像透過処理の応用

Qiitaではさり気なく初めての投稿です。
私の投稿が少しでも誰かの役に立てば幸いです。

クソアプリとは

私の作った作品を紹介する前に、今一度クソアプリの定義を確認しておきましょう。

何の生産性もない、何も世の中の役に立たない、でもそれでいい、そんなサービスやアプリ。それがクソアプリ

それでは、勇気を出して...私の作ったクソアプリをご紹介します。

私の作ったクソアプリの紹介

私の作ったクソアプリは、推しのグッズを透過してただ眺めるWebサイトです。
機能はとても簡単で、2つしかありません。

  1. 推しのグッズ画像を牧場で眺める機能
  2. 推しのグッズ画像を透過する機能

機能的には少ないですが、あまりゲームを作ったことのない私にとって、実際に実装してみて学ぶことは多かったです。

技術周りの話

私が今回このアプリを作る上で使用した技術は以下の通りです。

  • フロントエンド
    • TypeScript
    • React
    • Chakra-UI
    • axios
  • API
    • Python
    • FastAPI
    • rembg

今回は、その中でも比較的聞いたことがないであろう... rembgによる画像透過処理 について詳しく紹介します。

rembgによる画像透過処理

rembgとは、対象の画像を透過するためのPythonライブラリです。
GitHub上でオープンソースで公開されています。

使い方は簡単で、PILという画像ライブラリを使った方法ならば、以下の少ない量のコードで透過処理を実装することができます。

from rembg import remove
from PIL import Image

input_path = 'input.png'
output_path = 'output.png'

input = Image.open(input_path)
output = remove(input)
output.save(output_path)

他にもnumpyを使った方法や、ライブラリを使わずにbytesとして入出力する方法もあります。詳しくは上に貼ったGitHubのリポジトリからご覧ください。

POSTリクエストで画像透過する方法 (FastAPI × rembg)

今回は、指定のエンドポイントにアクセスすると、POSTリクエストで送った画像ファイルを透過してレスポンスとして返すAPIを実装しました。

早速コードの全体像を見てみましょう。

import io
from typing import Annotated
from rembg import remove
from PIL import Image
from fastapi import FastAPI, UploadFile, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

@app.post("/")
async def remove_charactor_image_bg(file: UploadFile):
    charactor_file = await file.read()
    charactor_file_image = Image.open(io.BytesIO(charactor_file))

    removed_image = remove(charactor_file_image)
    output = io.BytesIO()
    removed_image.save(output, "PNG")
    output_image_bytes = output.getvalue()

    await file.close()
    return Response(content=output_image_bytes, media_type="image/png")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

意外とみんな引っかかる!?CORSに注意という話

まず注意するべき点は、フロントエンド(React)とバックエンド(FastAPI)は別オリジンにある扱いであることです。(※ローカル環境の場合)

SPAを作る上で、慣れない方はぶつかるであろう「CORS」です。
この言葉を聞いて意味を理解できなかった人は、暇な時に以下の記事を読んでみると良いでしょう。

今回のようなフロントエンドとバックエンドが別オリジンに存在するケースでは、CORSの設定が必要です。
FastAPIでは以下のように設定を行いました。

app = FastAPI()

app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

本来ならallow_originを*ではなく特定のオリジンに指定するのですが、今回は自分の環境でしか動かさない前提で*を指定しておきます。

これでPOSTリクエスト時にCORSエラーで弾かれなくなりました。

画像透過するエンドポイントをFastAPIで作る話

次に、画像を透過するためのエンドポイントを作ります。

@app.post("/")
async def remove_charactor_image_bg(file: UploadFile):
    charactor_file = await file.read()
    charactor_file_image = Image.open(io.BytesIO(charactor_file))

    removed_image = remove(charactor_file_image)
    output = io.BytesIO()
    removed_image.save(output, "PNG")
    output_image_bytes = output.getvalue()

    await file.close()
    return Response(content=output_image_bytes, media_type="image/png")

まず最初に、HTTPリクエストとレスポンスの部分を確認します。
今回の場合は下記コードのように、"/"にPOSTリクエストでアクセスした時という風に設定しています。

@app.post("/")
async def remove_charactor_image_bg(file: UploadFile):
    ...
    return Response(content=output_image_bytes, media_type="image/png")

引数にはUploadFile型のfileという名前の変数を用意しておくことで、この変数から画像ファイルを取得することができます。また、レスポンスではメディアタイプを"image/png"に設定した上で、画像ファイルのバイト配列を返すようにしています。以上がHTTPリクエストとレスポンスの部分に関する説明です。

次に、画像ファイルを透過加工する部分の説明です。
HTTPリクエストで受け取った画像ファイルは、file.read()というメソッドを使うことでバイト配列に変換することができます。また、そうしてバイト配列変換したものをImage.open(io.BytesIO())に渡してあげることでPILライブラリで扱う画像の型に変換することができます。

以下のコードがその例です。

charactor_file = await file.read()
charactor_file_image = Image.open(io.BytesIO(charactor_file))

removed_image = remove(charactor_file_image)
output = io.BytesIO()
removed_image.save(output, "PNG")
output_image_bytes = output.getvalue()

await file.close()

PILで扱う形式となった画像ファイルをrembgのremoveメソッドで透過し、その後PILのsaveメソッドを使うことでpng形式の画像にし、getvalue()でバイト配列に変換しています。

React (TypeScript)で画像を透過するためのリクエストを出す話

フロントエンド側では以下のようなコードを使って画像をAPIに送信し、透過された画像を受け取ります。

import Axios from 'axios';
import { API_BASE_URL } from './config.ts';

export const authClient = Axios.create({
  baseURL: API_BASE_URL,
});

const inputImageRef = useRef<HTMLInputElement>(null);

const handleFileChange = async () => {
    const file = inputImageRef.current?.files?.[0];
    if (file === undefined) {
      console.log('file is undefined');
      return;
    }
    const formData = new FormData();
    formData.append('file', file);

    console.log('requesting...');
    const res = await authClient.post('/', formData, {
      responseType: 'blob',
    });
    console.log('response done!', res.headers['Content-Type']);
    // 返ってきた画像ファイルをBlob型として扱えるようにする
    const imageBlob = new Blob([res.data], { type: 'image/png' });
    console.log(imageBlob);
    // 画像ファイルをURLに変換
    const imageURL = URL.createObjectURL(imageBlob);
    console.log(imageURL);
  };

以上が、透過処理に関する説明です。

最後に

お疲れ様でした。
透過処理は有料の写真加工アプリによく機能として存在しますが、エンジニアなら意外と簡単に(しかも無料で)透過処理の機能を実装することができます。皆さんの作っているアプリやサービスにも、ぜひrembgの導入を検討されてみてはいかがでしょうか。

最後に、私の作ったクソアプリは以下のGitHubリポジトリに公開しています。
フロントエンド・API含めて全てのコードを確認できるので、気になる方はぜひ。
(紹介した内容の他にも、グッズを活き活きとアニメーションさせていたり...細かな工夫がそこにありますw)

13
2
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
13
2