0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Photoshop不要】1枚のHTML上のPythonで画像の背景除去を行う方法

0
Posted at

「背景を抜いて別の画像と合成したい」という場面はよくあります。通常、こうした作業にはPythonの環境構築やライブラリのインストールが必要ですが、今回はブラウザだけで動くPython REPLの fudebako を使い、セットアップ不要で完結させる方法を紹介します。

筆者は fudebako の開発者です。自プロダクトの紹介も兼ねていますが、ツールの利便性を感じていただければ幸いです。


fudebako とは

fudebako は、Pyodide(WebAssembly版CPython)を1枚のHTMLファイルにパッケージングしたブラウザ内Python実行環境です。Python本体だけでなく、pandas、matplotlib、scipy、onnxruntime、そして opencv-python も使えます。HTMLファイルをダブルクリックするだけで、すぐにコードを動かせます。

v0.4.5からは WebGPU × onnxruntime によるGPU推論にも対応しました。Chrome 113以上のWebGPUが有効な環境(M1/M2/M3 MacやWindowsのGPU搭載機など)であれば、ブラウザから直接GPUを叩いて高速な処理が可能です。


今回の流れ

以下の3つの工程をすべてブラウザ内で完結させます。

  1. gpt-image-2 で被写体画像を生成(OpenAI APIを使用)
  2. RMBG-1.4(ONNX量子化版、44MB)で背景マスクの初期版を作成
  3. OpenCVのGrabCut でマスクの境界を精緻化し、別の背景画像に合成

合成結果の 3 パネル


事前準備

1. fudebako を用意する

fudebakoのGitHub から最新のHTMLファイルをダウンロードし、ブラウザで開きます。file:// 形式でそのまま開いて問題ありません。

2. OpenAI APIキーの設定

右上の設定アイコン(歯車)から「環境変数」を開き、OPENAI_API_KEY を追加して保存してください。

環境変数をブラウザ内に暗号化して保存するためのパスフレーズ機能も搭載しています

3. 学習済みモデル(RMBG-1.4)のダウンロード

以下のコードを実行して、背景除去モデルをダウンロードします(約44MB、ネット環境によりますが数十秒程度で終わります)。

# Cell 0: モデルのダウンロード(初回のみ)
import requests, os

URL = "https://huggingface.co/briaai/RMBG-1.4/resolve/main/onnx/model_quantized.onnx"
DEST = "rmbg14_quantized.onnx"

if not os.path.exists(DEST):
    print("Downloading RMBG-1.4 quantized ONNX (~44 MB)...")
    r = requests.get(URL)
    with open(DEST, "wb") as f:
        f.write(r.content)
    print(f"{len(r.content)/1e6:.1f} MB saved")
else:
    print(f"{DEST}: already exists")

実装コード

1. 被写体画像の生成

まずは切り抜きの対象となる画像を生成します。

import os, json, base64
from pyodide.http import pyfetch
from PIL import Image
from io import BytesIO
import time

prompt = "A fluffy white cat sitting on a wooden table, studio lighting, white background, photorealistic"

r = await pyfetch(
    "https://api.openai.com/v1/images/generations",
    method="POST",
    headers={
        "Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
        "Content-Type": "application/json",
    },
    body=json.dumps({
        "model": "gpt-image-2",
        "prompt": prompt,
        "size": "1024x1024",
        "n": 1,
        "output_format": "png",
    }),
)
data = await r.json()
img_bytes = base64.b64decode(data["data"][0]["b64_json"])
with open("subject.png", "wb") as f:
    f.write(img_bytes)

subject = Image.open("subject.png").convert("RGB")
print(f"Generated: {subject.size}")

2. RMBG-1.4 による初期マスク作成

背景除去モデルを走らせます。WebGPUが有効な環境なら、1024pxの画像でも数秒で推論が終わります。

import onnxruntime as ort
import numpy as np
import time

print("Available providers:", ort.get_available_providers())
PROVIDERS = ["WebGpuExecutionProvider", "CPUExecutionProvider"]

sess = ort.InferenceSession("/drive/rmbg14_quantized.onnx", providers=PROVIDERS)
input_name = sess.get_inputs()[0].name
print(f"Input: {input_name}  shape: {sess.get_inputs()[0].shape}")

# RMBG-1.4 の前処理(正規化は独自設定: mean=0.5, std=1.0)
img_np = np.array(subject.resize((1024, 1024)), dtype=np.float32) / 255.0
img_input = ((img_np - 0.5) / 1.0).transpose(2, 0, 1)[np.newaxis]

t0 = time.time()
output = sess.run(None, {input_name: img_input})
print(f"RMBG inference: {time.time()-t0:.2f}s")

mask_raw = output[0][0, 0]
mask_raw = (mask_raw - mask_raw.min()) / (mask_raw.max() - mask_raw.min() + 1e-8)
alpha_rmbg = (mask_raw * 255).clip(0, 255).astype(np.uint8)

# RMBG 結果を透過 PNG として保存
result_rmbg = subject.convert("RGBA")
result_rmbg.putalpha(Image.fromarray(alpha_rmbg).resize(subject.size, Image.LANCZOS))
result_rmbg.save("rmbg_raw.png")
print("RMBG mask saved")

白猫に白背景という難しい条件だと、RMBGだけでは輪郭にモヤっとしたノイズが残ることがあります。

RMBG-1.4 単体結果(白背景ノイズあり)

3. OpenCV GrabCut で境界をきれいに整える

RMBGが出力した大まかなマスクを「ヒント」としてOpenCVのGrabCutアルゴリズムに渡し、境界線を精緻化します。

import cv2

orig_np = np.array(subject)
alpha = np.array(Image.fromarray(alpha_rmbg).resize((orig_np.shape[1], orig_np.shape[0])))

# GrabCut 用のマスク作成: 確実な背景/前景と、迷う領域を分ける
gc_mask = np.full(alpha.shape, cv2.GC_PR_BGD, dtype=np.uint8)
gc_mask[alpha > 180] = cv2.GC_FGD  # ほぼ間違いなく前景
gc_mask[alpha < 20]  = cv2.GC_BGD  # ほぼ間違いなく背景

bgr = cv2.cvtColor(orig_np, cv2.COLOR_RGB2BGR)
fgModel = np.zeros((1, 65), np.float64)
bgModel = np.zeros((1, 65), np.float64)
cv2.grabCut(bgr, gc_mask, None, bgModel, fgModel, 5, cv2.GC_INIT_WITH_MASK)

fg = np.where((gc_mask == cv2.GC_FGD) | (gc_mask == cv2.GC_PR_FGD), 255, 0).astype(np.uint8)

# 小さな浮きノイズを除去
n_labels, labels, stats, _ = cv2.connectedComponentsWithStats(fg)
areas = stats[1:, cv2.CC_STAT_AREA]
clean = np.zeros_like(fg)
if len(areas) > 0:
    main_area = areas.max()
    for i, area in enumerate(areas, start=1):
        if area > main_area * 0.01:
            clean[labels == i] = 255

# 輪郭を少し滑らかにする
k5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
k3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
clean = cv2.morphologyEx(clean, cv2.MORPH_CLOSE, k5)
clean = cv2.morphologyEx(clean, cv2.MORPH_OPEN,  k3)

result_grabcut = subject.convert("RGBA")
result_grabcut.putalpha(Image.fromarray(clean))
result_grabcut.save("rmbg_grabcut.png")
print(f"GrabCut done  FG pixels={int(clean.sum()//255)}")

GrabCutを通すことで、白背景の残骸が消え、輪郭がスッキリしました。

GrabCut 精緻化後の 3 パネル比較

4. 背景の生成と合成

最後に、新しい背景画像を生成して合成します。

bg_prompt = (
    "A cozy Japanese living room interior, tatami straw mat floor, "
    "shoji sliding doors with warm afternoon light, wooden low table, "
    "clean and bright, photorealistic, no animals, no people"
)

r = await pyfetch(
    "https://api.openai.com/v1/images/generations",
    method="POST",
    headers={
        "Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
        "Content-Type": "application/json",
    },
    body=json.dumps({
        "model": "gpt-image-2",
        "prompt": bg_prompt,
        "size": "1024x1024",
        "n": 1,
        "output_format": "png",
    }),
)
data = await r.json()
bg_bytes = base64.b64decode(data["data"][0]["b64_json"])
with open("background.png", "wb") as f:
    f.write(bg_bytes)
bg = Image.open("background.png").convert("RGB")
print(f"Background generated: {bg.size}")
import matplotlib.pyplot as plt

cat = Image.open("/drive/rmbg_grabcut.png").convert("RGBA")

# サイズを調整して配置
scale = 0.55
new_w, new_h = int(cat.width * scale), int(cat.height * scale)
cat_r = cat.resize((new_w, new_h), Image.LANCZOS)

composite = bg.copy().convert("RGBA")
x = bg.width - new_w - 60
y = bg.height - new_h - 20
composite.paste(cat_r, (x, y), cat_r)
composite.save("composite_result.png")

fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].imshow(bg);                 axes[0].set_title("背景(和室)")
axes[1].imshow(cat);                axes[1].set_title("被写体(GrabCut 透過)")
axes[2].imshow(composite);          axes[2].set_title("合成結果")
for ax in axes: ax.axis('off')
plt.suptitle("gpt-image-2 生成 → RMBG-1.4 → GrabCut → 合成")
plt.tight_layout()
plt.show()

実際に動かしてみた結果

M2 MacBook ProのChrome環境での計測結果です。

  • RMBG-1.4 推論: 約8.8秒(1024px、WebGPU使用)
  • GrabCut処理: 約15秒(OpenCV on WebAssembly)

背景除去ツールが苦手とする「白い被写体 × 白い背景」という組み合わせでも、RMBGとGrabCutを組み合わせることで、手作業なしにかなり精度の高い切り抜きができました。

GrabCut 後の切り抜き(クリーン)

最終的な和室背景との合成結果です。

最終合成結果


よくあるトラブルと対策

症状 原因と対策
WebGPUが動かない ブラウザが最新のChrome系か確認してください。

まとめ

fudebakoを使えば、複雑な環境構築を一切行わずに、ブラウザだけで実用的な背景除去と合成のパイプラインを構築できます。RMBG-1.4のような強力なモデルがWebGPU経由で手軽に動かせるようになったのは、Web技術の大きな進歩だと感じます。

Python環境を汚したくない方や、サクッと画像処理を試したい方は、ぜひ活用してみてください。


転載・二次利用について

この記事の内容やコードは、YouTube動画のネタや社内勉強会、Podcastなどで自由に使っていただいて構いません。事前連絡は不要ですが、もし活用事例などあればコメントいただけると励みになります。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?