【Python】BiRefNetで画像の背景を除去する方法

BiRefNetで画像の背景を除去する方法

本記事はアフィリエイト広告(PR)を含みます

画像から背景を消去して被写体だけの画像を作る場合、背景差分処理などが思い浮かぶと思います。
背景差分処理とは、あらかじめ背景だけの画像を撮っておいて、被写体が写っている画像との差分を取ることで被写体の領域だけを抽出するという手法です。
ただ、同じ背景である必要があるので使用できる状況が限られます。

最近はAIが発達してきたのでAIによる背景除去も注目されています。
そこで今回はBiRefNetという背景除去用のモデルを使用し、Pythonで画像の背景を除去してみます。

1. BiRefNetとは

BiRefNetは画像を前景と背景の2領域に分割するために開発されたセグメンテーションモデルです。
入力画像から被写体の領域を推定し、背景を除去するためのマスク画像を生成できます。

主な特徴として、以下が挙げられます。

  • 髪の毛や動物の毛など、細かな輪郭を抽出しやすい
  • 高解像度画像の細部を維持しやすい
  • 人物だけでなく、動物や商品など幅広い被写体に対応できる
  • 生成したマスクを使用して、背景が透明なRGBA画像を作成できる

BiRefNetでは、画像全体の情報を使って被写体の位置を推定する処理と、画像の局所的な情報や輪郭を利用して細部を復元する処理を組み合わせています。

そのため、U²-Netなどの従来モデルと比較して、髪の毛や動物の毛、細い構造などの境界を高精度に抽出できるとされています。

スポンサーリンク

2. 従来の画像処理による背景除去との違い

背景を除去する代表的な画像処理として、前述した背景差分やクロマキー処理があります。

背景差分は、あらかじめ用意した背景画像と被写体が写っている画像の差分から、被写体の領域を抽出する方法です。
そのため、カメラの位置や背景が変化すると正しく抽出できない場合があります。

クロマキー処理は、グリーンバックなどの特定の背景色を抽出し、その領域を透明化する方法です。高速に処理できますが、被写体に背景と同じ色が含まれていると、その部分まで除去される可能性があります。

一方、BiRefNetなどのセグメンテーションモデルは入力画像から前景と背景をAIが推定します。
背景画像やグリーンバックを用意する必要がなく、一般的な写真から被写体を切り抜ける点が大きな違いです。

前回の記事では、画像生成AIで被写体の画像(グリーンバック)と背景の画像を別々に生成し、クロマキー処理で合成することで、バウンティングボックスやラベルデータを付与した状態のデータセットが作れるのではないかと考えていました。

実際にSD1.5で画像生成してみたところ、グリーンバックの画像がうまく生成されなかったのでBiRefNetで被写体の背景を除去してから背景画像と合成しようと考えています。

スポンサーリンク

3. BiRefNetのインストール

BiRefNetをインストールする前に、PyTorchをインストールしておく必要があります。

※ PyTorchのインストールはこちら。

PyTorchをインストールしたら、追加で以下のライブラリをインストールします。

pip install transformers pillow
ライブラリ説明
transformersHugging Faceが開発している機械学習ライブラリ。
BERTやGPTなどの自然言語処理モデルだけでなく、BiRefNetのような画像処理モデルも簡単に利用可能。
pillowPythonで画像を読み込み・保存・編集するためのライブラリ。

今回は、Hugging Faceで公開されているBiRefNetの学習済みモデルを、Transformersライブラリ経由で読み込みます。

BiRefNet本体をpipで直接インストールするのではなく、Transformersライブラリのfrom_pretrainedを使用すると、必要なモデルとコードが初回実行時に自動でダウンロードされるためGitHubからソースコードを取得しなくても利用できます。

BiRefNetの学習済みモデルはHugging Faceで公開されています。
https://huggingface.co/ZhengPeng7/BiRefNet

スポンサーリンク

4. Pythonで背景除去してみる

ここからはPythonで画像とBiRefNetモデルを読み込み、背景除去を行った画像とマスク画像を保存するコードを実装していきます。
今回はHugging Faceで公開されている汎用モデル「ZhengPeng7/BiRefNet」を使用します。

14行目の「INPUT_PATH」は背景除去したい画像のファイルパスを指定してください。

from pathlib import Path
import time

import torch
from PIL import Image
from torchvision import transforms
from transformers import AutoModelForImageSegmentation


# ============================================================
# 設定
# ============================================================
# 背景除去したい画像のファイルパスを指定する!!!
INPUT_PATH = Path("target/dog/00015_Toy_Poodle.png")

# 背景透過PNG
OUTPUT_PATH = Path("output_transparent.png")

# 白黒マスク画像
MASK_PATH = Path("output_mask.png")

MODEL_NAME = "ZhengPeng7/BiRefNet"

# BiRefNetへ入力する解像度
MODEL_INPUT_SIZE = (512, 512)


# ============================================================
# モデル読み込み
# ============================================================

def load_model(
    device: torch.device,
) -> AutoModelForImageSegmentation:
    """BiRefNetを読み込む。"""

    print(f"Loading model: {MODEL_NAME}")

    model = AutoModelForImageSegmentation.from_pretrained(
        MODEL_NAME,
        trust_remote_code=True,
    )

    model.to(device)
    model.eval()

    return model


# ============================================================
# 前処理
# ============================================================

def create_transform() -> transforms.Compose:
    """BiRefNet用の前処理を作成する。"""

    return transforms.Compose(
        [
            transforms.Resize(MODEL_INPUT_SIZE),
            transforms.ToTensor(),
            transforms.Normalize(
                mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225],
            ),
        ]
    )


# ============================================================
# 背景除去
# ============================================================

def remove_background(
    image: Image.Image,
    model: AutoModelForImageSegmentation,
    device: torch.device,
) -> tuple[Image.Image, Image.Image]:
    """
    BiRefNetで背景を除去する。

    戻り値:
        transparent_image:
            背景を透明化したRGBA画像

        mask:
            前景を白、背景を黒としたマスク画像
    """

    rgb_image = image.convert("RGB")
    original_size = rgb_image.size

    transform = create_transform()

    input_tensor = transform(rgb_image).unsqueeze(0)

    # モデルと入力Tensorのdtypeを揃える
    model_dtype = next(model.parameters()).dtype

    input_tensor = input_tensor.to(
        device=device,
        dtype=model_dtype,
    )

    print(f"Model dtype: {model_dtype}")
    print(f"Input dtype: {input_tensor.dtype}")

    with torch.inference_mode():
        outputs = model(input_tensor)

        prediction = outputs[-1].sigmoid().float().cpu()

    prediction = prediction[0].squeeze()

    mask = transforms.ToPILImage()(prediction)

    mask = mask.resize(
        original_size,
        resample=Image.Resampling.BILINEAR,
    )

    transparent_image = rgb_image.convert("RGBA")
    transparent_image.putalpha(mask)

    return transparent_image, mask


# ============================================================
# 実行
# ============================================================

def main() -> None:
    if not INPUT_PATH.exists():
        raise FileNotFoundError(
            f"入力画像が見つかりません: {INPUT_PATH.resolve()}"
        )

    device = torch.device(
        "cuda" if torch.cuda.is_available() else "cpu"
    )

    print(f"Device: {device}")

    if device.type == "cuda":
        print(f"GPU: {torch.cuda.get_device_name(0)}")

    total_start = time.perf_counter()

    image = Image.open(INPUT_PATH)

    model_start = time.perf_counter()
    model = load_model(device)
    model_elapsed = time.perf_counter() - model_start

    inference_start = time.perf_counter()

    transparent_image, mask = remove_background(
        image=image,
        model=model,
        device=device,
    )

    inference_elapsed = time.perf_counter() - inference_start

    OUTPUT_PATH.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    MASK_PATH.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    transparent_image.save(OUTPUT_PATH)
    mask.save(MASK_PATH)

    total_elapsed = time.perf_counter() - total_start

    print()
    print(f"Transparent image: {OUTPUT_PATH.resolve()}")
    print(f"Mask image       : {MASK_PATH.resolve()}")
    print(f"Model load time  : {model_elapsed:.2f} sec")
    print(f"Inference time   : {inference_elapsed:.2f} sec")
    print(f"Total time       : {total_elapsed:.2f} sec")


if __name__ == "__main__":
    main()

簡単な解説

まず、入力画像や出力ファイル名、使用するBiRefNetのモデル名を設定します。

INPUT_PATH = Path("target/dog/00015_Toy_Poodle.png")
OUTPUT_PATH = Path("output_transparent.png")
MASK_PATH = Path("output_mask.png")
MODEL_NAME = "ZhengPeng7/BiRefNet"

load_modelでBiRefNetを読み込みます。

model = AutoModelForImageSegmentation.from_pretrained(
    MODEL_NAME,
    trust_remote_code=True,
)

from_pretrainedを実行すると、初回のみHugging Faceからモデルが自動的にダウンロードされます。

その後、GPU(CUDA)またはCPUへ転送し、推論モードへ切り替えています。

model.to(device)
model.eval()

次に、前処理としてBiRefNetへ入力できる形式へ画像を変換します。

transforms.Resize(MODEL_INPUT_SIZE),
transforms.ToTensor(),
transforms.Normalize(
           mean=[0.485, 0.456, 0.406],
           std=[0.229, 0.224, 0.225],
           )

それぞれの役割は以下の通りです。

処理役割
Resize画像を512×512へリサイズ
ToTensor画像をPyTorchのTensorへ変換
Normalize学習時と同じ形式へ正規化

remove_background()では実際に背景除去を行います。
まず、画像をRGB形式へ変換し、前処理を実行します。
unsqueeze(0)はバッチ次元を追加し、1枚の画像を推論できる形式へ変換しています。

input_tensor = transform(rgb_image).unsqueeze(0)

次に推論を実行します。

outputs = model(input_tensor)

BiRefNetは前景の確率を表すマスクを出力するので以下の処理で0~1の値へ変換しています。
※ここではfloat32へ変換し、GPUからCPUへの転送も行っています。

prediction = outputs[-1].sigmoid().float().cpu()

最後に、生成したマスク画像を元のサイズにリサイズし、αチャンネルとして設定することで背景が透明なPNG画像を作成しています。

mask = mask.resize(
        original_size,
        resample=Image.Resampling.BILINEAR,
    )

transparent_image = rgb_image.convert("RGBA")
transparent_image.putalpha(mask)
スポンサーリンク

5. 実行結果

今回はこちらの犬の画像を読み込み、背景を除去してみました。
※前回の記事で生成した犬の中の一匹です。

実行結果はこちら。

背景除去画像
マスク画像

この画像は犬の体に緑の苔が生えているように見えるので、背景色とかなり近かったのですがきれいに分離できました。
その他頭の毛羽立っている部分も細部まで高精度に分離できています。

スポンサーリンク

6. まとめ

今回はBiRefNetをPython経由で読み込み画像の背景除去を試してみました。
試した限りでは毛並みや背景との境界も自然に抽出できていたので精度は良さそうです。
これならわざわざAIにグリーンバックの画像を生成するように指示する必要はないかもしれませんね。

次回は画像生成+背景除去でデータセット生成を自動化するところまでやろうと思います。

今回は以上です。

スポンサーリンク

7. 参考サイト

Bilateral Reference for High-Resolution Dichotomous Image Segmentation
https://arxiv.org/abs/2401.03407

高精度・高解像度・境界を綺麗 に背景を透過できるBiRefNetを使って大量のデータを処理する
https://memo.eightban.com/python/birefnet

8. 関連書籍

スポンサーリンク

コメント

タイトルとURLをコピーしました