1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像背景除去AIを FastAPI + withoutBG でローカル構築してみた

Last updated at Posted at 2025-12-13

1. はじめに

「ちょっとした画像の背景だけ消したい」
「オンラインサービスに画像をアップしたくない」
という場面が近頃地味に増えてきました。

Pythonの便利ツールを漁っていたところ、オープンソースでローカル実行可能な画像背景除去AI「withoutBG」を発見。
rembg という既存の背景除去パッケージもありますが、せっかくなので、高精度であると謳われている withoutBG を使用して、

  • ローカル環境のみで動作
  • PCやスマホのブラウザから利用可能
  • 画像アップロード → 背景除去 → ダウンロードまで完結

という簡単なWebアプリを実装してみました。
完成したWebアプリのトップ画面はこんな感じです。

withoutBG_top1.png

2. withoutBG とは

withoutBGは、画像の背景を自動で除去できるライブラリです。

withoutBGの特徴をまとめると、以下になります。

  • 画像背景除去に特化
  • Local(オープンソース・無料)とCloud API(有料)を提供
  • Pythonから簡単に利用可能
  • ローカル実行でも十分実用的な精度

今回はオープンソースモデルである v0.1.0 Focus(Focus モデル) を使い、ローカル環境で動作する画像背景除去AIを実装します。

withoutBG_models.png
※ 出典:withoutBG API Documentation

3. 環境構築

3.1 方針

  • Docker版(2GB)も公開されているが、今回は不使用
  • Python + FastAPI + withoutBG でシンプルに実装
  • 仮想環境でWebアプリを構築
  • モデルは仮想環境内に配置して管理

3.2 実行環境

  • OS:Windows 11
  • Python:3.12
  • フレームワーク:FastAPI
  • 背景除去:withoutBG(Focus モデル)

3.3 仮想環境構築

作業フォルダで venv を作成。

py -3.12 -m venv .venv

3.4 フォルダ構成

今回の構成は以下の通りです。

/
├── .venv/                  # Python 仮想環境
│   └── ...
├── app/
│   ├── main.py             # アプリ本体
│   ├── models/             # 背景除去モデル
│   │   └── ...
│   ├── templates/          # HTML テンプレート
│   │   └── index.html
│   └── uploads/            # 画像アップロード関連
│       ├── original/       # 元画像
│       ├── processed/      # 処理済み画像
│       └── hidden/         # 非表示扱いの画像
│           └── ...
├── requirements.txt        # 依存パッケージ
└── run_server.py           # サーバー起動スクリプト

3.5 パッケージインストール

使用するパッケージは最小限。

/requirements.txt
fastapi
jinja2
python-multipart
uvicorn[standard]
withoutbg

インストール手順:

./.venv/Scripts/Activate.ps1
pip install -r ./requirements.txt

4. アプリケーション実装

実装は CodeX で生成したコードをベースに調整。

4.1 実装のポイント

  • WithoutBGOpenSource() を初回実行時に呼ぶと、約320MBのモデル(Focus モデル)が自動ダウンロード
  • モデルの保存先を /app/models に指定(指定しない場合は OS のキャッシュディレクトリに保存される)
  • PC / スマホ両対応のシンプルな UI
  • 処理済み画像の「非表示」が可能(実ファイルは残しつつ画面から消す)

4.2 全体コード

main.py
/app/main.py
from __future__ import annotations

import asyncio
import mimetypes
import os
import shutil
from pathlib import Path
from uuid import uuid4

from fastapi import FastAPI, File, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from huggingface_hub import hf_hub_download
from withoutbg.core import WithoutBGOpenSource


BASE_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = BASE_DIR / "templates"
UPLOAD_ROOT = BASE_DIR / "uploads"
ORIGINAL_DIR = UPLOAD_ROOT / "original"
PROCESSED_DIR = UPLOAD_ROOT / "processed"
HIDDEN_DIR = UPLOAD_ROOT / "hidden"
MODEL_DIR = BASE_DIR / "models"

for directory in (UPLOAD_ROOT, ORIGINAL_DIR, PROCESSED_DIR, HIDDEN_DIR, MODEL_DIR):
    directory.mkdir(parents=True, exist_ok=True)


app = FastAPI(title="withoutbg demo")
templates = Jinja2Templates(directory=str(TEMPLATE_DIR))


def _ensure_model_file(filename: str) -> Path:
    target = MODEL_DIR / filename
    if target.exists():
        return target
    downloaded = Path(hf_hub_download(repo_id="withoutbg/focus", filename=filename))
    shutil.copy2(downloaded, target)
    return target


def _create_model() -> WithoutBGOpenSource:
    return WithoutBGOpenSource(
        depth_model_path=_ensure_model_file("depth_anything_v2_vits_slim.onnx"),
        isnet_model_path=_ensure_model_file("isnet.onnx"),
        matting_model_path=_ensure_model_file("focus_matting_1.0.0.onnx"),
        refiner_model_path=_ensure_model_file("focus_refiner_1.0.0.onnx"),
    )


# 初回のみモデルダウンロード(約320MB)
WITHOUTBG_MODEL = _create_model()


def _build_file_entry(path: Path) -> dict:
    return {
        "name": path.name,
        "size_kb": round(path.stat().st_size / 1024, 1),
        "url": f"/processed/{path.name}",
        "download_url": f"/download/{path.name}",
        "hide_url": f"/hide/{path.name}",
    }


def _list_files(directory: Path) -> list[dict]:
    return [
        _build_file_entry(path)
        for path in sorted(directory.glob("*"), key=os.path.getmtime, reverse=True)
        if path.is_file()
    ]


@app.get("/", response_class=HTMLResponse)
async def index(request: Request) -> HTMLResponse:
    processed = _list_files(PROCESSED_DIR)
    return templates.TemplateResponse(
        "index.html",
        {
            "request": request,
            "processed": processed,
        },
    )


@app.post("/upload")
async def upload_image(file: UploadFile = File(...)) -> RedirectResponse:
    content_type = file.content_type or ""
    if not content_type.startswith("image/"):
        raise HTTPException(
            status_code=400, detail="画像ファイルのみアップロードしてください。"
        )

    data = await file.read()
    if not data:
        raise HTTPException(status_code=400, detail="ファイルが空です。")

    suffix = (
        Path(file.filename or "").suffix
        or mimetypes.guess_extension(content_type)
        or ".png"
    )
    token = uuid4().hex
    original_path = ORIGINAL_DIR / f"{token}{suffix}"
    processed_path = PROCESSED_DIR / f"{token}.png"

    original_path.write_bytes(data)
    try:
        result = await asyncio.to_thread(
            WITHOUTBG_MODEL.remove_background, original_path
        )
        await asyncio.to_thread(result.save, processed_path)
    except Exception as exc:
        original_path.unlink(missing_ok=True)
        processed_path.unlink(missing_ok=True)
        raise HTTPException(
            status_code=500, detail=f"背景削除に失敗しました: {exc}"
        ) from exc

    return RedirectResponse(url="/", status_code=303)


def _safe_processed_file(name: str) -> Path:
    safe_name = Path(name).name
    target = PROCESSED_DIR / safe_name
    if not target.exists():
        raise HTTPException(status_code=404, detail="ファイルが見つかりません。")
    return target


@app.get("/processed/{file_name}")
async def serve_processed(file_name: str) -> FileResponse:
    target = _safe_processed_file(file_name)
    return FileResponse(target, media_type="image/png")


@app.get("/download/{file_name}")
async def download_processed(file_name: str) -> FileResponse:
    target = _safe_processed_file(file_name)
    return FileResponse(
        target,
        media_type="image/png",
        filename=target.name,
    )


@app.post("/hide/{file_name}")
async def hide_image(file_name: str) -> RedirectResponse:
    source = _safe_processed_file(file_name)
    destination = HIDDEN_DIR / source.name
    source.rename(destination)
    return RedirectResponse(url="/", status_code=303)


@app.get("/healthz")
async def healthcheck() -> dict:
    return {"status": "ok"}
run_server.py
/run_server.py
import socket
import uvicorn

PORT = 8001
HOST = "0.0.0.0"
APP_PATH = "app.main:app"


def _resolve_host_ip() -> str:
    hostname = socket.gethostname()
    try:
        return socket.gethostbyname(hostname)
    except OSError:
        return "127.0.0.1"


def main() -> None:
    ip_address = _resolve_host_ip()
    print("withoutbg demo server")
    print(f" - Localhost  : http://127.0.0.1:{PORT}")
    print(f" - LAN IP     : http://{ip_address}:{PORT}")
    print("同一LAN内のPC/スマホからアクセスする場合はLAN IPのURLを使ってください。")

    uvicorn.run(APP_PATH, host=HOST, port=PORT, log_level="info")


if __name__ == "__main__":
    main()

index.html
/app/templates/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>withoutbg 背景削除</title>
  <style>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: "Segoe UI", Arial, sans-serif;
      background: #f6f7fb;
      color: #222;
    }

    main {
      max-width: 960px;
      margin: 0 auto;
      padding: 2rem 1rem 3rem;
    }

    header {
      text-align: center;
      margin-bottom: 2rem;
    }

    h1 {
      margin: 0 0 0.5rem;
      font-size: 2rem;
    }

    .card {
      background: #fff;
      border-radius: 12px;
      padding: 1.5rem;
      margin-bottom: 2rem;
      box-shadow: 0 10px 30px rgba(15, 18, 40, 0.08);
    }

    form {
      display: flex;
      flex-direction: column;
      gap: 1rem;
    }

    input[type="file"] {
      border: 2px dashed #7a7caa;
      padding: 1rem;
      border-radius: 8px;
      background: #f9faff;
    }

    button {
      border: none;
      border-radius: 8px;
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      background: #4c5be0;
      color: #fff;
      cursor: pointer;
    }

    button:hover {
      opacity: 0.9;
    }

    .hint {
      font-size: 0.9rem;
      color: #555;
    }

    .gallery {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
      gap: 1rem;
    }

    article {
      background: #f0f2ff;
      border-radius: 10px;
      padding: 0.75rem;
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
    }

    article img {
      width: 100%;
      border-radius: 8px;
      background: #fff;
    }

    article footer {
      display: flex;
      flex-direction: column;
      gap: 0.4rem;
      font-size: 0.9rem;
    }

    .actions {
      display: flex;
      justify-content: space-between;
      gap: 0.5rem;
      align-items: center;
    }

    .filesize {
      color: #555;
    }

    form .secondary {
      background: #fff;
      color: #4c5be0;
      border: 1px solid #4c5be0;
    }

    a {
      color: #3944bc;
      text-decoration: none;
    }

    a:hover {
      text-decoration: underline;
    }
  </style>
</head>

<body>
  <main>
    <header>
      <h1>withoutbg 背景削除</h1>
      <p>同じWi-Fiに接続したPCやスマホからアクセスできます。</p>
    </header>

    <section class="card">
      <h2>画像をアップロードして背景を削除</h2>
      <form action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept="image/*" required />
        <button type="submit">背景を削除する</button>
      </form>
    </section>

    <section class="card">
      <h2>処理済み画像</h2>
      {% if processed %}
      <div class="gallery">
        {% for item in processed %}
        <article>
          <a href="{{ item.download_url }}" download>
            <img src="{{ item.url }}" alt="Processed image {{ loop.index }}" />
          </a>
          <footer>
            <div class="actions">
              <a href="{{ item.download_url }}">ダウンロード</a>
              <form action="{{ item.hide_url }}" method="post">
                <button type="submit" class="secondary">非表示</button>
              </form>
            </div>
            <span class="filesize">{{ item.size_kb }} KB</span>
          </footer>
        </article>
        {% endfor %}
      </div>
      {% else %}
      <p>まだ処理済み画像はありません。</p>
      {% endif %}
    </section>
  </main>
</body>

</html>

5. アプリケーション実行

Webアプリは以下のコマンドで起動します。

.venv\Scripts\Activate.ps1
python run_server.py      
# または
.venv\Scripts\python run_server.py

起動すると、アクセス先が表示されます。

withoutbg demo server
 - Localhost  : http://127.0.0.1:8001
 - LAN IP     : http://[PCIP]:8001
同一LAN内のPC/スマホからアクセスする場合はLAN IPURLを使ってください。
INFO:     Started server process [プロセスID]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)

動作確認

http://[PCのIP]:8001 にアクセスし、以下の画像で背景除去のテストを実施。

  • マーモットのアイコン風画像(AI生成)
  • 建物の写真
  • 観葉植物の写真

結果はこんな感じ。
アップロードした画像が、背景除去後に一覧表示されます。

withoutBG_top3.png

背景除去前後での比較。背景がきれいに除去されています。
1画像あたり5~10秒程度 で処理が完了します。

withoutBG_comparison.png

ダウンロードして加工すれば、冒頭のような合成画像も簡単に作れます。

6. まとめ

今回やったこと:

  • オープンソースの背景除去AI「withoutBG」をローカルで利用
  • FastAPIでWebアプリ化
  • PC / スマホ両対応の簡易 UI を実装

使ってみた感想:

  • 精度は想像以上に良好
  • ローカル完結なので、画像を外部に出さずに加工できて便利
  • ローカル環境内で共有も可能
  • 小規模ツールとして扱いやすい

「背景削除だけしたい」、「外部に画像を出したくない」そんな用途におすすめです。
アイコン作成資料用画像の切り抜き簡易的な合成素材作成などにも利用できそうです。

おまけ

しいたけは未来の電子部品になる可能性を秘めているらしい。
3 Weird Things You Can Turn Into a Memristor Yes, you might want to plug a mushroom into your circuit

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?