AnyDoorとは?
(画像は、Geminiで作成されたものです)
AnyDoorの概要
AnyDoorは、論文「AnyDoor: Zero-shot Object-level Image Customization」で提案された、ターゲットとなるオブジェクトをユーザーが指定した新しいシーンの場所に、希望の形状でテレポートさせることができる拡散ベースの画像生成モデルです。
従来のカスタマイズされた画像生成手法の多くは、新しい概念(オブジェクト)を生成するために複数のターゲット画像を用いて約1時間のファインチューニング(パラメータ調整)を必要としており、実際のアプリケーションへの適用が大きく制限されていました。AnyDoorは、この課題を解決するために開発されたモデルであり、オブジェクトごとにパラメータをチューニングすることなく、推論段階で多様なオブジェクトとシーンの組み合わせへと容易に一般化できる「ゼロショット」での画像カスタマイズを実現しています。
このような困難なゼロショット設定では、特定のオブジェクトの特徴を十分に捉える必要があります。AnyDoorでは、一般的に用いられる「アイデンティティ(ID)特徴」に加えて、外観の詳細を維持しつつ、照明や向き、姿勢などの多様な局所的変化を許容するように慎重に設計された「詳細特徴(Detail features)」を補完的に用います。これにより、ターゲットオブジェクトが異なる周囲の環境に自然に溶け込むことをサポートします。
さらに、単一のオブジェクトの様々な形態(時間軸に沿った変化)を観察できるビデオデータセットから知識を借りる(学習に組み込む)アプローチを提案しており、モデルの汎化能力と堅牢性をより強固なものにしています。広範な実験により、仮想試着(virtual try-on)、形状編集(shape editing)、オブジェクトの入れ替え(object swapping)といった実世界のアプリケーションにおいて、既存の代替手法を凌駕する優れたパフォーマンスと大きな可能性が示されています。
AnyDoorの処理概要

(画像は、Geminiで作成されたものです)
AnyDoorは、ターゲットオブジェクト、シーン画像、および配置したい場所(ロケーション)を入力として受け取り、高い忠実度と多様性を持つオブジェクトとシーンの合成画像を生成します。処理のパイプラインは、大きく以下のステップで進行します。
-
Background Removal(背景除去)
セグメンテーションモジュール(自動またはインタラクティブなセグメンター)を使用して、ターゲットオブジェクトの背景を削除し、オブジェクトを画像の中央に配置します。 -
Identity Feature Extraction(ID特徴抽出)
背景が除去された「クリーンな」オブジェクトを自己教師ありエンコーダ(ID Extractor)に入力し、オブジェクトのアイデンティティ(識別)情報を取得します。 -
Detail Feature Extraction(詳細特徴抽出)
クリーンなオブジェクトにハイパスフィルタを適用して高周波マップ(HF-Map)を作成します。この結果得られた高周波マップを希望のロケーションのシーンと合成(Stitch)し、コラージュを作成します。その後、コラージュと形状マスク(Shape Mask)を結合し、Detail Extractor(詳細抽出器)に入力して詳細マップ(Detail Maps)を生成します。 -
Feature Injection(特徴の注入)
最後に、抽出されたIDトークンと詳細マップを、事前学習済みのテキスト条件付き拡散モデル(U-Net)に注入し、ターゲットオブジェクトが周囲と良好に調和した最終的な合成画像を生成します。
AnyDoorの構成技術要素(詳細)
AnyDoorがゼロショットで高精度なオブジェクトの合成を可能にしている背景には、特徴抽出の工夫と、動画と静止画を組み合わせた高度な学習戦略があります。論文「AnyDoor: Zero-shot Object-level Image Customization」で提案されている主要な技術要素を解説します。
-
ID Extractor(アイデンティティ抽出器)
従来のモデルでよく使われるCLIP画像エンコーダは、粗い意味レベルの情報しか埋め込めず、オブジェクトのアイデンティティを保持した識別性の高い表現を生成するのに苦労するという課題がありました。
詳細な仕組み: AnyDoorでは、事前学習済みの強力な自己教師ありモデルであるDINOv2をバックボーンとして採用しています。DINOv2は画像をグローバルトークンとパッチトークンとしてエンコードし、これらを結合して情報を保持します。
技術的な効果: その後、単一の線形レイヤー(Linear projector)を使用して、これらのトークンを事前学習済みテキスト-画像U-Netの埋め込み空間に合わせてアライメントし、「IDトークン」として出力します。これにより、より識別性の高い特徴を維持できます。 -
Detail Extractor(詳細抽出器)とHF-Map
低解像度のIDトークンだけでは、ロゴやテクスチャなどの低レベルの細部を適切に維持することが困難です。
詳細な仕組み: 対象物の詳細を補完するため、水平・垂直のSobelカーネル(ハイパスフィルタ)を使用してオブジェクトから高周波マップ(High-frequency map: HF-Map)を抽出します。これをシーン画像の指定位置にコラージュし、ユーザーが姿勢を制御するための「形状マスク(Shape Mask)」と結合します。
技術的な効果: 高周波マップは、姿勢や照明などの柔軟な局所的変化を許容しつつ細かいディテールを維持する情報ボトルネックとして機能します。これらはControlNetスタイルのU-Netエンコーダに入力され、階層的な解像度を持つ詳細マップ(Detail Maps)が出力されます。 -
Feature Injection(特徴の注入)
事前学習済みのStable Diffusion(U-Net)をベースジェネレータとして使用します。
詳細な仕組み: U-Netのテキスト埋め込みを先ほど抽出したIDトークンに置き換え、各U-Netレイヤーにクロスアテンションを介して注入します。さらに、詳細マップ(Detail Maps)は各解像度においてU-Netのデコーダ特徴量と連結(Concatenate)されます。
技術的な効果: 学習中は事前学習済みのU-Netエンコーダのパラメータを凍結して事前知識を維持し、U-Netデコーダのみをチューニングして新しいタスクに適応させます。 -
Adaptive Timestep Sampling(適応的タイムステップサンプリング)
動画データと画像データの両方の利点を最大限に引き出すための学習戦略です。
詳細な仕組み: 動画フレームは外観の変化(ポーズやビュー)を学習するのに適していますが、解像度やモーションブラーの点で品質が不十分な場合があります。一方、画像データは高品質な詳細を提供しますが、外観の変化がありません。
技術的な効果: 拡散モデルの初期のノイズ除去ステップ(500-1000)は全体的な構造やポーズの生成に焦点を当てるため、この段階では動画データがサンプリングされる確率を50%増加させます。逆に、細かい詳細を生成する後半のステップ(0-500)では、画像データのサンプリング確率を50%増加させます。これにより、品質と多様性の両方を効果的に学習します。
AnyDoorの実装(概念的なシンプルな実装)
ライブラリのインストール
AnyDoorの実装に必要な、PyTorch、Diffusers、Transformersなどのライブラリをインストールします。
# PyTorch, Diffusers, Transformers, OpenCV, Scikit-imageなどの必須ライブラリをインストール
!pip install torch diffusers transformers opencv-python scikit-image
AnyDoorのモデル構築と画像処理に不可欠なライブラリ群をインストールします。torchは深層学習の基本的なフレームワーク、diffusersは拡散モデルを扱うためのライブラリ、transformersはDINOv2などの事前学習済みモデルを利用するために必要です。また、opencv-pythonとscikit-imageは、画像の前処理や評価指標の計算に使用します。
データセットのインストール
モデルの学習と評価に使用する動画データセット「DAVIS 2017」をダウンロードし、展開します。
# 動画セグメンテーションの標準的なベンチマークであるDAVIS 2017データセットをダウンロード
!wget https://data.vision.ee.ethz.ch/csergi/share/davis/DAVIS-2017-trainval-480p.zip
# ダウンロードしたzipファイルを解凍し、DAVIS2017ディレクトリに展開
!unzip DAVIS-2017-trainval-480p.zip -d ./DAVIS2017
AnyDoorの論文では、静止画と動画の両方を使って学習を行いますが、ここでは実装をシンプルにするため、動画データセットであるDAVIS 2017のみを利用します。このデータセットには、オブジェクトの様々な動きや姿勢の変化が含まれており、モデルが多様な状況に対応する能力を学習するのに適しています。
ℹ️ DAVIS 2017データセットのクレジット
本実装で利用している DAVIS 2017 (Densely Annotated Video Segmentation) データセットは、ビデオオブジェクトセグメンテーション(VOS)タスクの標準的なベンチマークとして学術研究目的に公開されているものです。
データセットの詳細な利用規約、ライセンス、および元論文等のクレジット表記については、DAVIS Challenge公式サイトをご参照ください。
学術論文や研究発表等で本データセットを利用・引用する際は、以下の公式BibTeXをご利用ください。
@article{Pont-Tuset_arXiv_2017,
author = {Jordi Pont-Tuset and Federico Perazzi and Sergi Caelles and Pablo Arbel{\'a}ez and Alexander Sorkine-Hornung and Luc {Van Gool}},
title = {The 2017 DAVIS Challenge on Video Object Segmentation},
journal = {arXiv:1704.00675},
year = {2017}
}
MiniAnyDoorの定義
AnyDoorモデルの全体構造を定義します。ID特徴、詳細特徴を抽出し、U-Netに注入してノイズを予測する一連の流れを実装します。
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import Dinov2Model
from diffusers import UNet2DConditionModel, DDPMScheduler, AutoencoderKL
from torch.optim import AdamW
import cv2
import numpy as np
import random
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as T
import os
import glob
from tqdm import tqdm
from PIL import Image
import matplotlib.pyplot as plt
from skimage.metrics import peak_signal_noise_ratio as calculate_psnr
from skimage.metrics import structural_similarity as calculate_ssim
class MiniAnyDoor(nn.Module):
def __init__(self, cross_attention_dim=768):
super().__init__()
# 各特徴抽出器を初期化
self.hf_extractor = HFMapExtractor()
self.id_extractor = IDExtractor(cross_attention_dim=cross_attention_dim)
self.detail_extractor = DetailExtractor()
# Stable Diffusion v1.5のU-Netをベースとしてロード
self.unet = UNet2DConditionModel.from_pretrained(
"runwayml/stable-diffusion-v1-5", subfolder="unet"
)
# U-Netのエンコーダ部分(down_blocks)の重みを凍結し、事前学習された知識を保持
for name, param in self.unet.named_parameters():
if "up_blocks" not in name:
param.requires_grad = False
def forward(self, target_img, target_mask, aligned_target, aligned_mask, scene_bg, shape_mask, noisy_latents, timesteps, drop_prob=0.1):
# 詳細特徴の元となる高周波マップを生成
hf_map = self.hf_extractor(aligned_target, aligned_mask)
# シーン背景と高周波マップを合成してコラージュ画像を作成
collage_img = scene_bg + hf_map
# オブジェクトのID特徴と詳細特徴を抽出
id_tokens = self.id_extractor(target_img * target_mask)
detail_maps = self.detail_extractor(collage_img, shape_mask)
# 学習時の正則化のため、一定確率で条件付けをドロップアウト
if self.training and torch.rand(1).item() < drop_prob:
id_tokens = torch.zeros_like(id_tokens) # IDトークンをゼロに
detail_maps = [torch.zeros_like(d) for d in detail_maps] # 詳細マップをゼロに
# U-Netにノイズのかかった潜在変数、タイムステップ、IDトークン、詳細マップを入力し、ノイズを予測
noise_pred = self.unet(
noisy_latents,
timesteps,
encoder_hidden_states=id_tokens,
down_intrablock_additional_residuals=detail_maps, # 詳細マップをU-Netのデコーダに注入
).sample
return noise_pred
MiniAnyDoorクラスは、AnyDoorモデルの心臓部です。3つの主要な特徴抽出器(HFMapExtractor, IDExtractor, DetailExtractor)と、Stable DiffusionのUNet2DConditionModelを組み合わせて構成されます。
-
初期化 (init):
- 各特徴抽出器をインスタンス化します。
- diffusersライブラリから事前学習済みのU-Netをロードします。
- 論文の記述に従い、U-Netのエンコーダ部分(down_blocksとそれ以外)のパラメータを凍結(requires_grad = False)します。これにより、学習済みの強力な画像生成能力を維持しつつ、デコーダ部分のみを新しいタスク(特徴注入)に適応させることができます。
-
順伝播 (forward):
- 各抽出器を用いて、入力データからIDトークンと詳細マップを生成します。
- 学習の安定化と汎化性能向上のため、drop_probの確率でIDトークンと詳細マップをゼロに置き換える「コンディションドロップアウト」を適用します。
- 最終的に、すべての情報をU-Netに渡し、潜在空間におけるノイズの予測値 noise_pred を計算して返します。詳細マップは down_intrablock_additional_residuals という引数を通じてU-Netのデコーダブロックに直接供給されます。
HFMapExtractorの定義
オブジェクトのテクスチャやロゴなどの詳細情報を保持するための高周波マップ(HF-Map)を抽出するモジュールを定義します。
class HFMapExtractor(nn.Module):
def __init__(self):
super().__init__()
# 水平方向と垂直方向のエッジを検出するためのSobelフィルタを定義
sobel_x = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=torch.float32)
sobel_y = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=torch.float32)
# 畳み込み演算で使えるように、重みを(out_channels, in_channels, H, W)の形式に変形
# register_bufferに登録することで、モデルのパラメータとしては扱われず、しかしstate_dictには保存される
self.register_buffer('weight_x', sobel_x.view(1, 1, 3, 3))
self.register_buffer('weight_y', sobel_y.view(1, 1, 3, 3))
def forward(self, img_rgb, object_mask):
# RGB画像をグレースケールに変換
img_gray = 0.299 * img_rgb[:, 0:1, :, :] + 0.587 * img_rgb[:, 1:2, :, :] + 0.114 * img_rgb[:, 2:3, :, :]
# Sobelフィルタを適用してx方向とy方向のエッジを計算
edge_x = F.conv2d(img_gray, self.weight_x, padding=1)
edge_y = F.conv2d(img_gray, self.weight_y, padding=1)
# 両方向のエッジの強度を合成して、全体のエッジ(高周波成分)の強度を計算
high_freq_intensity = torch.sqrt(edge_x**2 + edge_y**2)
# 元のRGB画像にエッジ強度を乗算し、エッジ部分のみを強調した高周波マップを作成
hf_rgb = img_rgb * high_freq_intensity
# マスクをわずかに侵食させて、オブジェクトの輪郭付近の不要なアーティファクトを除去
eroded_mask = 1.0 - F.max_pool2d(1.0 - object_mask, kernel_size=5, stride=1, padding=2)
# 最終的な高周波マップにマスクを適用し、オブジェクト部分のみを抽出
return hf_rgb * eroded_mask
HFMapExtractorは、オブジェクトの詳細なテクスチャ情報を捉えるための重要なコンポーネントです。
- 処理の流れ:
- 入力されたRGB画像をグレースケールに変換します。
- 水平・垂直のSobelフィルタ(エッジ検出フィルタ)を適用し、画像内のエッジ(輝度変化の激しい部分)を抽出します。
- 抽出したエッジの強度を計算し、これを「高周波成分の強度」とします。
- 元のRGB画像にこの強度マップを乗算することで、エッジやテクスチャが強調された「高周波マップ」を生成します。
- 最後に、オブジェクトのマスクをわずかに縮小(侵食)させて輪郭部分のノイズを抑制し、マスクを適用してオブジェクト部分の高周波情報のみを切り出します。
この高周波マップは、オブジェクトのアイデンティティ(ID)とは別に、細かいディテールをU-Netに伝える役割を果たします。これにより、生成される画像が元のオブジェクトの質感を忠実に再現できるようになります。
IDExtractorの定義
オブジェクトの全体的な形状や色など、その「らしさ」を捉えるためのID特徴(IDトークン)を抽出するモジュールを定義します。
class IDExtractor(nn.Module):
def __init__(self, cross_attention_dim=768):
super().__init__()
# 事前学習済みの強力な自己教師ありモデルDINOv2をバックボーンとしてロード
self.dino = Dinov2Model.from_pretrained("facebook/dinov2-base")
# 特徴量をU-Netの埋め込み空間に射影するためのプロジェクターを定義
hidden_size = self.dino.config.hidden_size
# 論文に記載の通り、単純な線形層で特徴量を射影
self.projector = nn.Linear(hidden_size, cross_attention_dim)
# DINOv2のパラメータは学習中に更新しないように凍結
for param in self.dino.parameters():
param.requires_grad = False
def forward(self, clean_target_img):
# DINOv2は224x224の入力を想定しているため、リサイズ
clean_target_img_224 = F.interpolate(clean_target_img, size=(224, 224), mode='bilinear', align_corners=False)
# DINOv2で特徴を抽出
outputs = self.dino(clean_target_img_224)
# 最後の隠れ層の状態(パッチトークンとCLStoken)を取得
tokens = outputs.last_hidden_state
# プロジェクターを通して、U-Netのクロスアテンション層で扱える次元に変換
id_tokens = self.projector(tokens)
return id_tokens
IDExtractorは、オブジェクトのアイデンティティを保持するIDトークンを生成する役割を担います。
- バックボーン:
- 事前学習済みのDINOv2モデルを利用します。DINOv2は、ラベルなしデータで学習された自己教師ありモデルであり、画像の構造や意味を理解する能力が非常に高いことで知られています。AnyDoorでは、この強力な特徴抽出能力を利用して、オブジェクトの識別性の高い表現を獲得します。
- プロジェクター:
- DINOv2から出力された特徴量(tokens)は、そのままではStable DiffusionのU-Netが受け取れません。そこで、projector(射影層)を使って、U-Netのクロスアテンション層が要求する次元(cross_attention_dim)に変換します。
- ここでは論文の記述に忠実に、単純な線形層(nn.Linear)を使用して特徴量の次元を調整しています。
- パラメータの凍結:
- DINOv2は非常に巨大なモデルであり、学習済みの重みをそのまま活用した方が効率的です。そのため、param.requires_grad = Falseと設定して、学習中にDINOv2の重みが更新されないように「凍結」しています。学習対象はプロジェクター部分のみとなります。
DetailExtractorの定義
HF-Mapと形状マスクから、U-Netの各解像度に対応する階層的な詳細マップ(Detail Maps)を生成するモジュールを定義します。
class DetailExtractor(nn.Module):
def __init__(self, in_channels=4):
super().__init__()
# 畳み込み層を使い、入力をU-Netの中間層と同じ解像度(64x64)までダウンサンプリング
self.down_8x = nn.Sequential(
nn.Conv2d(in_channels, 32, kernel_size=3, stride=2, padding=1), # 512 -> 256
nn.SiLU(),
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # 256 -> 128
nn.SiLU(),
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1), # 128 -> 64
nn.SiLU()
)
# U-Netの各ブロックに対応する解像度の特徴マップを生成する層
# 64x64解像度の特徴マップ (U-Netのup_blocks[0]に対応)
self.feat1 = nn.Sequential(nn.Conv2d(128, 128, kernel_size=3, padding=1), nn.SiLU())
# 32x32解像度の特徴マップ (U-Netのup_blocks[1]に対応)
self.feat2 = nn.Sequential(nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1), nn.SiLU())
# 16x16解像度の特徴マップ (U-Netのup_blocks[2]に対応)
self.feat3 = nn.Sequential(nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1), nn.SiLU())
# U-Netの各デコーダブロックの入力チャネル数(320, 640, 1280)に合わせるためのZero-Convolution層
self.zero_convs = nn.ModuleList([
nn.Conv2d(128, 320, kernel_size=1),
nn.Conv2d(256, 640, kernel_size=1),
nn.Conv2d(512, 1280, kernel_size=1)
])
# Zero-Convolution層の重みとバイアスを0で初期化
# これにより、学習初期では詳細情報がU-Netに影響を与えず、安定した学習が可能になる
for m in self.zero_convs:
nn.init.zeros_(m.weight)
nn.init.zeros_(m.bias)
def forward(self, collage_img, shape_mask):
# 入力としてコラージュ画像と形状マスクをチャネル方向に連結
x = torch.cat([collage_img, shape_mask], dim=1) # 入力サイズ: [B, 4, 512, 512]
# 基準となる64x64の特徴マップを生成
x_base = self.down_8x(x)
# 階層的な特徴マップを抽出
f1 = self.feat1(x_base) # [B, 128, 64, 64]
f2 = self.feat2(f1) # [B, 256, 32, 32]
f3 = self.feat3(f2) # [B, 512, 16, 16]
# Zero-Convolutionを適用して、U-Netデコーダの各ブロックのチャネル数に合わせる
out1 = self.zero_convs[0](f1) # [B, 320, 64, 64]
out2 = self.zero_convs[1](f2) # [B, 640, 32, 32]
out3 = self.zero_convs[2](f3) # [B, 1280, 16, 16]
# 3つの異なる解像度の特徴マップをリストとして返す
return [out1, out2, out3]
DetailExtractorは、AnyDoorがControlNetのアーキテクチャから着想を得て設計したモジュールです。HF-Mapと形状マスクから、U-Netのデコーダ部分に注入するための階層的な特徴マップ(Detail Maps)を生成します。
- 階層的特徴抽出:
- まず、入力画像を一連の畳み込み層(down_8x)を通して、U-Netの潜在空間に近い解像度(64x64)までダウンサンプリングします。
- その後、解像度ごと(64x64, 32x32, 16x16)に特徴を抽出する層(feat1, feat2, feat3)を適用します。これにより、粗いレベルから細かいレベルまでの詳細情報を持つ、マルチスケールの特徴マップが生成されます。
- Zero-Convolution:
- 抽出された特徴マップは、そのままではU-Netの各ブロックに注入できません。チャネル数が異なるためです。
- そこで、zero_convsという特別な畳み込み層を用います。これは、重みとバイアスがすべてゼロで初期化された1x1の畳み込み層です。
- 目的: 学習の初期段階では、zero_convsからの出力はゼロになります。つまり、詳細情報はU-Netに一切伝達されません。これにより、モデルはまず事前学習済みのU-Netの知識をベースに安定して学習を始め、徐々にzero_convsの重みを更新していくことで、詳細情報をどのように利用するかを学んでいきます。このテクニックは、ControlNetで導入され、大規模モデルのファインチューニングを安定させるために非常に有効です。
- 出力:
- 最終的に、U-Netのデコーダの3つのブロックに対応する解像度とチャネル数を持つ特徴マップのリストを返します。これらがMiniAnyDoorクラスでdown_intrablock_additional_residualsとしてU-Netに渡されます。
DavisAnyDoorDatasetの定義
学習に必要なデータをDAVIS 2017データセットから読み込み、モデルが扱える形式に前処理するPyTorchのDatasetクラスを定義します。
class DavisAnyDoorDataset(Dataset):
def __init__(self, davis_root, split="train", output_size=(512, 512), single_video=False):
self.davis_root = davis_root
self.output_size = output_size
# 学習用(train)または検証用(val)の動画リストを読み込む
imagesets_path = os.path.join(davis_root, "ImageSets", "2017", f"{split}.txt")
with open(imagesets_path, 'r') as f:
self.video_names = [line.strip() for line in f.readlines()]
# デバッグや高速な検証のために、動画を1つに限定するオプション
if single_video and len(self.video_names) > 0:
self.video_names = [self.video_names[0]]
# 画像とマスクが保存されているディレクトリのパスを設定
self.img_dir = os.path.join(davis_root, "JPEGImages", "480p")
self.mask_dir = os.path.join(davis_root, "Annotations", "480p")
# 画像用の変換: Tensorへの変換と[-1, 1]への正規化
self.transform = T.Compose([
T.ToTensor(),
T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])
# マスク用の変換: Tensorへの変換のみ
self.mask_transform = T.ToTensor()
def __len__(self):
# データセット内の動画の総数を返す
return len(self.video_names)
def _perturb_mask(self, mask):
# データ拡張: マスクにランダムな変形(膨張・侵食など)を加えて、形状の多様性を学習させる
if random.random() < 0.3:
y, x = np.where(mask > 0)
if len(y) > 0 and len(x) > 0:
y1, y2, x1, x2 = y.min(), y.max(), x.min(), x.max()
box_mask = np.zeros_like(mask)
box_mask[y1:y2, x1:x2] = 1.0
return box_mask
kernel_size = random.randint(3, 15)
kernel = np.ones((kernel_size, kernel_size), np.uint8)
if random.random() < 0.5:
perturbed = cv2.dilate(mask, kernel, iterations=1)
else:
perturbed = cv2.erode(mask, kernel, iterations=1)
return perturbed
def _get_bbox(self, mask):
# マスクからバウンディングボックス(オブジェクトの矩形領域)の座標を取得
y, x = np.where(mask > 0)
if len(y) == 0 or len(x) == 0:
return 0, 0, mask.shape[1], mask.shape[0]
return x.min(), y.min(), x.max(), y.max()
def _align_image(self, src_img, src_mask, dst_mask):
# ソースオブジェクトを、宛先マスクのバウンディングボックスに合わせてリサイズ&ペーストする
x1_s, y1_s, x2_s, y2_s = self._get_bbox(src_mask)
x1_d, y1_d, x2_d, y2_d = self._get_bbox(dst_mask)
aligned_img = np.zeros_like(src_img)
aligned_mask = np.zeros_like(src_mask)
w_d, h_d = x2_d - x1_d, y2_d - y1_d
if w_d > 0 and h_d > 0 and (x2_s - x1_s) > 0 and (y2_s - y1_s) > 0:
crop_img = src_img[y1_s:y2_s, x1_s:x2_s]
crop_mask = src_mask[y1_s:y2_s, x1_s:x2_s]
aligned_img[y1_d:y2_d, x1_d:x2_d] = cv2.resize(crop_img, (w_d, h_d))
aligned_mask[y1_d:y2_d, x1_d:x2_d] = cv2.resize(crop_mask, (w_d, h_d), interpolation=cv2.INTER_NEAREST)
return aligned_img, aligned_mask
def __getitem__(self, idx):
# 1つのデータサンプルを生成するメインの関数
video_name = self.video_names[idx]
img_paths = sorted(glob.glob(os.path.join(self.img_dir, video_name, "*.jpg")))
mask_paths = sorted(glob.glob(os.path.join(self.mask_dir, video_name, "*.png")))
num_frames = len(img_paths)
# 1つの動画からランダムに2つのフレーム(i, j)を選択
frame_i_idx, frame_j_idx = random.sample(range(num_frames), 2)
# 画像とマスクを読み込む
img_i = cv2.imread(img_paths[frame_i_idx])[..., ::-1] # OpenCV(BGR)からRGBに変換
img_j = cv2.imread(img_paths[frame_j_idx])[..., ::-1]
mask_i_raw = cv2.imread(mask_paths[frame_i_idx], cv2.IMREAD_GRAYSCALE)
mask_j_raw = cv2.imread(mask_paths[frame_j_idx], cv2.IMREAD_GRAYSCALE)
# マスクを0-1のfloat値に変換
mask_i = (mask_i_raw > 0).astype(np.float32)
mask_j = (mask_j_raw > 0).astype(np.float32)
# 画像とマスクを指定された出力サイズにリサイズ
img_i = cv2.resize(img_i, self.output_size, interpolation=cv2.INTER_LINEAR)
img_j = cv2.resize(img_j, self.output_size, interpolation=cv2.INTER_LINEAR)
mask_i = cv2.resize(mask_i, self.output_size, interpolation=cv2.INTER_NEAREST)
mask_j = cv2.resize(mask_j, self.output_size, interpolation=cv2.INTER_NEAREST)
# ターゲットオブジェクト画像(フレームiのオブジェクト部分)を作成
target_img = (img_i * mask_i[:, :, None]).astype(np.uint8)
# 形状マスク(フレームjのマスクに変形を加えたもの)を生成
shape_mask = self._perturb_mask(mask_j)
# 正解画像(フレームj)と、シーン背景(フレームjの背景部分)を作成
gt_image = img_j
scene_bg = (img_j * (1.0 - mask_j[:, :, None])).astype(np.uint8)
# モデルの入力となる各種データを辞書形式で返す
return {
"gt_image": self.transform(gt_image),
"target_img": self.transform(target_img),
"target_mask": self.mask_transform(mask_i),
"aligned_target": self.transform(target_img), # 本来は位置合わせした画像
"aligned_mask": self.mask_transform(mask_i), # 本来は位置合わせしたマスク
"scene_bg": self.transform(scene_bg),
"shape_mask": self.mask_transform(shape_mask),
"modality": "video" # データの種類を示す
}
このDavisAnyDoorDatasetクラスは、AnyDoorの学習タスクに合わせてデータを準備する重要な役割を担います。
- データソース:
- initでは、DAVIS 2017データセットのルートパスを受け取り、画像とアノテーション(マスク)が格納されているフォルダへのパスを構築します。single_videoオプションは、コードの動作確認やデバッグを迅速に行うために、1つの動画だけを使って小規模なデータセットを作成するのに便利です。
- データペアの生成 (getitem):
- AnyDoorの学習では、「あるオブジェクト(フレームi)を、別のシーン(フレームj)に合成する」というタスクを解きます。
- getitemは、まず動画リストからランダムに1つの動画を選び、さらにその動画内からランダムに2つのフレーム(frame_i, frame_j)を選択します。
- target_img: フレームiから抽出した、IDと詳細特徴の抽出元となるオブジェクト画像です。
- scene_bg: フレームjからオブジェクトを除いた背景画像です。
- shape_mask: フレームjのオブジェクトマスクにランダムな変形を加えたものです。これにより、モデルは指定された任意の形状にオブジェクトを生成する能力を学習します。
- gt_image: 学習の目標となる正解画像、つまり元のフレームjの画像です。モデルの出力がこの画像に近づくように学習が進められます。
- データ拡張 (_perturb_mask):
- _perturb_maskは、形状マスクにランダムな変形(膨張・侵食)を加えることで、データ拡張を行います。これにより、モデルが見たことのない形状にも対応できる汎化性能を高めます。
- 前処理:
- すべての画像とマスクは、指定されたoutput_size(512x512)にリサイズされ、transformによってTensorに変換され、値が[-1, 1]の範囲に正規化されてからモデルに渡されます。
sample_timestepsの定義
論文で提案されている「適応的タイムステップサンプリング」を実装します。データの種類(動画か画像か)に応じて、ノイズ除去プロセスのどのタイムステップを重点的に学習するかを調整します。
def sample_timesteps(batch_size, modality, num_train_timesteps=1000, device="cpu"):
# modalityが'video'の場合
if modality == "video":
# 50%の確率で、ノイズ除去プロセスの前半(500-1000)からタイムステップをサンプリング
# 動画データは構造やポーズの学習に適しているため、初期ステップを重視
bias_probs = torch.rand(batch_size, device=device)
timesteps = torch.where(
bias_probs < 0.5,
torch.randint(num_train_timesteps // 2, num_train_timesteps, (batch_size,), device=device),
torch.randint(0, num_train_timesteps, (batch_size,), device=device)
)
# modalityが'image'の場合(今回は使用しないが、参考として記載)
elif modality == "image":
# 50%の確率で、ノイズ除去プロセスの後半(0-500)からタイムステップをサンプリング
# 静止画データはディテールの学習に適しているため、後期ステップを重視
bias_probs = torch.rand(batch_size, device=device)
timesteps = torch.where(
bias_probs < 0.5,
torch.randint(0, num_train_timesteps // 2, (batch_size,), device=device),
torch.randint(0, num_train_timesteps, (batch_size,), device=device)
)
# 上記以外の場合は、全範囲から均等にサンプリング
else:
timesteps = torch.randint(0, num_train_timesteps, (batch_size,), device=device)
return timesteps
このsample_timesteps関数は、AnyDoorの学習戦略の核となる「適応的タイムステップサンプリング」を実装したものです。拡散モデルの学習では、タイムステップ(ノイズの量)をランダムに選び、そのノイズを除去するようにモデルを訓練します。このサンプリング方法を工夫することで、学習効率と最終的な生成品質を向上させることができます。
-
動画データの場合 (modality == "video"):
- 動画フレームは、オブジェクトの様々なポーズや視点の変化を含んでいますが、一枚一枚の画質は静止画に劣ることがあります。
- 拡散モデルのノイズ除去プロセスでは、初期のタイムステップ(tが大きい、例: 500-1000)が画像の全体的な構造やレイアウトを決定し、後期のタイムステップ(tが小さい、例: 0-500)が細かいディテールを生成する傾向があります。
- そこで、動画データで学習する際は、50%の確率で初期ステップ(num_train_timesteps // 2からnum_train_timesteps)を重点的にサンプリングします。これにより、モデルはオブジェクトの多様な構造変化を効率的に学習できます。
-
静止画データの場合 (modality == "image"):
- 逆に、高品質な静止画データはディテールの学習に優れています。そのため、静止画で学習する際は、後期ステップ(0からnum_train_timesteps // 2)を重点的にサンプリングすることで、テクスチャや質感の再現性を高めます。
今回は動画データセットのみを使用するため、主にmodality == "video"の条件分岐が機能します。
generate_imageの定義
学習済みのモデルを使って、実際に合成画像を生成(推論)する関数を定義します。Classifier-Free Guidance(CFG)を用いて、生成品質を向上させます。
def generate_image(model, vae, scheduler, batch, device, num_inference_steps=50, guidance_scale=1.0):
model.eval() # モデルを評価モードに設定
with torch.no_grad(): # 勾配計算を無効化
# 推論に必要なデータをデバイスに転送
target_img = batch["target_img"].to(device)
target_mask = batch["target_mask"].to(device)
aligned_target = batch["aligned_target"].to(device)
aligned_mask = batch["aligned_mask"].to(device)
scene_bg = batch["scene_bg"].to(device)
shape_mask = batch["shape_mask"].to(device)
B = target_img.shape[0] # バッチサイズ
# --- 1. 条件付き (Conditional) 特徴量の抽出 ---
# IDトークンと詳細マップをモデルから抽出
hf_map = model.hf_extractor(aligned_target, aligned_mask)
collage_img = scene_bg + hf_map
id_tokens_cond = model.id_extractor(target_img * target_mask)
detail_maps_cond = model.detail_extractor(collage_img, shape_mask)
# --- 2. 無条件 (Unconditional) 特徴量の生成 ---
# CFGのため、条件付けを行わない場合のダミー特徴量(ゼロベクトル)を用意
id_tokens_uncond = torch.zeros_like(id_tokens_cond)
detail_maps_uncond = [torch.zeros_like(d) for d in detail_maps_cond]
# --- 3. 特徴量の結合 ---
# バッチ方向に無条件と条件付きの特徴量を結合
# [uncond, cond] の順で並べる
id_tokens_both = torch.cat([id_tokens_uncond, id_tokens_cond])
detail_maps_both = [torch.cat([u, c]) for u, c in zip(detail_maps_uncond, detail_maps_cond)]
# --- 4. ノイズ除去ループ ---
# 純粋なノイズから潜在変数を初期化
latents = torch.randn((B, 4, 64, 64), device=device)
# スケジューラに推論ステップ数を設定
scheduler.set_timesteps(num_inference_steps)
for t in tqdm(scheduler.timesteps, desc="Denoising Steps"):
# U-Netへの入力も無条件・条件付きのペアにするため、潜在変数を2倍に複製
latent_model_input = torch.cat([latents, latents])
# スケジューラによっては入力のスケーリングが必要
if hasattr(scheduler, "scale_model_input"):
latent_model_input = scheduler.scale_model_input(latent_model_input, t)
# 1回のU-Netフォワードパスで、無条件と条件付きの両方のノイズ予測を同時に計算
noise_pred = model.unet(
latent_model_input,
t,
encoder_hidden_states=id_tokens_both,
down_intrablock_additional_residuals=detail_maps_both,
).sample
# --- 5. CFGの適用 ---
# 予測結果を無条件(uncond)と条件付き(cond)に分割
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
# CFGの計算式: noise = uncond_noise + guidance_scale * (cond_noise - uncond_noise)
# 条件の方向へ、guidance_scale分だけ強く予測を誘導する
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_cond - noise_pred_uncond)
# スケジューラを使って1ステップ前の潜在変数を計算
latents = scheduler.step(noise_pred, t, latents).prev_sample
# --- 6. VAEデコーダによる画像化 ---
# 潜在変数をスケーリングし、VAEデコーダでピクセル空間の画像に変換
latents = (1 / vae.config.scaling_factor) * latents
generated_image = vae.decode(latents).sample
# 画像を[0, 1]の範囲にクリップし、Numpy配列(0-255のuint8)に変換
generated_image = (generated_image / 2 + 0.5).clamp(0, 1)
generated_image = generated_image.cpu().permute(0, 2, 3, 1).numpy()
generated_image = (generated_image * 255).astype(np.uint8)
return generated_image[0]
generate_image関数は、学習済みのAnyDoorモデルを使って実際に画像を生成する推論プロセスを定義します。この中核をなすのがClassifier-Free Guidance (CFG) と呼ばれるテクニックです。
- CFGの仕組み:
- 2つの予測: 1回のノイズ除去ステップで、U-Netは2つのノイズ予測を同時に行います。
- 条件付き予測 (noise_pred_cond): IDトークンと詳細マップという条件情報を与えて予測したノイズ。
- 無条件予測 (noise_pred_uncond): 条件情報をすべてゼロにしたダミー情報を与えて予測したノイズ。これは、モデルが純粋にノイズだけから画像を生成しようとする際の予測に相当します。
- 差分の計算: noise_pred_cond - noise_pred_uncond という差分を計算します。これは「条件付けによってノイズ予測がどちらの方向へ変化したか」というベクトルを意味します。
- 誘導: この差分ベクトルをguidance_scaleでスケーリングし、無条件予測に足し合わせます。
- guidance_scaleが1.0より大きい場合、条件(IDや詳細)に、より忠実な画像を生成するように予測を「誘導」することができます。これにより、生成画像の品質や条件への追従性が向上します。
- 2つの予測: 1回のノイズ除去ステップで、U-Netは2つのノイズ予測を同時に行います。
- 処理の流れ:
- まず、条件付き・無条件の両方の特徴量を準備します。
- 純粋なガウシアンノイズから潜在変数latentsを開始します。
- num_inference_steps(例: 50回)のループを実行し、スケジューラのタイムステップに従って徐々にノイズを除去していきます。各ステップでCFGを適用して、最終的なノイズ予測値を計算します。
- ループが完了したら、ノイズが除去された潜在変数をVAEのデコーダに通し、最終的なピクセル画像を生成します。
学習の定義
モデルの学習を行うための1ステップ分の処理(train_step)を定義します。データセットからバッチを受け取り、損失を計算してパラメータを更新するまでの一連の流れを実装します。
if __name__ == "__main__":
# GPUが利用可能ならGPUを、そうでなければCPUを使用
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")
# モデル、VAE、スケジューラを初期化し、指定したデバイスに配置
model = MiniAnyDoor().to(device)
vae = AutoencoderKL.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="vae").to(device)
noise_scheduler = DDPMScheduler.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="scheduler")
# VAEは特徴抽出にのみ使い、学習は行わないため勾配計算を無効化
vae.requires_grad_(False)
# オプティマイザを定義。学習対象のパラメータ(requires_grad=Trueのもの)のみを渡す
optimizer = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-5)
def train_step(batch):
model.train() # モデルを学習モードに設定
optimizer.zero_grad() # 勾配をリセット
# バッチから各種データを取得し、デバイスに転送
gt_image = batch["gt_image"].to(device)
target_img = batch["target_img"].to(device)
target_mask = batch["target_mask"].to(device)
scene_bg = batch["scene_bg"].to(device)
shape_mask = batch["shape_mask"].to(device)
modality = batch["modality"][0]
B = gt_image.shape[0] # バッチサイズ
with torch.no_grad(): # このブロック内では勾配を計算しない
# VAEエンコーダで正解画像を潜在空間に変換
latents = vae.encode(gt_image).latent_dist.sample()
# VAEのスケーリングファクターを適用
latents = latents * vae.config.scaling_factor
# 純粋なノイズを生成
noise = torch.randn_like(latents)
# 適応的サンプリングでタイムステップを決定
timesteps = sample_timesteps(B, modality, num_train_timesteps=1000, device=device)
# 元の潜在変数にノイズを付加
noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)
# モデルに入力し、ノイズを予測
noise_pred = model(
target_img=target_img,
target_mask=target_mask,
aligned_target=batch["aligned_target"].to(device),
aligned_mask=batch["aligned_mask"].to(device),
scene_bg=scene_bg,
shape_mask=shape_mask,
noisy_latents=noisy_latents,
timesteps=timesteps
)
# 予測したノイズ(noise_pred)と元のノイズ(noise)の平均二乗誤差(MSE)を損失として計算
loss = F.mse_loss(noise_pred, noise)
# 損失に基づいて勾配を計算
loss.backward()
# オプティマイザでモデルのパラメータを更新
optimizer.step()
return loss.item()
このコードブロックでは、拡散モデルの標準的な学習プロセスを定義しています。
- if name == "main":: このブロックは、このスクリプトが直接実行された場合にのみ実行されます。
- 初期化:
- MiniAnyDoorモデル、VAE、ノイズスケジューラを初期化し、利用可能なデバイス(CUDAまたはCPU)に配置します。
- VAEは画像のエンコード/デコードにのみ使用し、その重みは学習中に更新したくないため、vae.requires_grad_(False)でパラメータを凍結します。
- オプティマイザにはAdamWを使用します。filter(lambda p: p.requires_grad, model.parameters())という記述により、MiniAnyDoorモデル内で学習が許可されているパラメータ(主にデコーダ部分とZero-Convolution層、各Extractorのプロジェクタ)のみが最適化の対象となります。
- train_step関数:
- 潜在変数の準備: 正解画像(gt_image)をVAEのエンコーダに通し、潜在表現(latents)に変換します。
- ノイズの付加: randn_likeでlatentsと同じ形状のノイズを生成し、sample_timestepsで決定したタイムステップに基づいて、noise_scheduler.add_noiseでlatentsにノイズを付加します。これがnoisy_latentsです。
- ノイズ予測: noisy_latentsと各種条件情報(target_img, shape_maskなど)をmodelに入力し、モデルが付加されたノイズを予測した結果(noise_pred)を得ます。
- 損失計算と更新: モデルの予測noise_predと、実際に付加したnoiseとの間の平均二乗誤差(MSE)を計算します。この損失が小さくなるように、loss.backward()で勾配を計算し、optimizer.step()でモデルのパラメータを更新します。
このtrain_stepをデータセット全体に対して何度も繰り返すことで、モデルは徐々にノイズを除去し、与えられた条件に合った画像を生成する能力を学習していきます。
学習の実行
実際に学習ループを実行します。データセットをロードし、指定されたエポック数だけtrain_stepを繰り返してモデルを訓練します。
# DAVISデータセットのルートディレクトリを指定
davis_root = "./DAVIS2017/DAVIS"
# データセットが存在するか確認
if os.path.exists(davis_root):
print("DAVIS2017 dataset found. Loading SINGLE video for quick verification...")
# 動作検証のため、1つの動画のみを含むデータセットをロード
dataset = DavisAnyDoorDataset(davis_root, split="train", single_video=True)
# データローダーを作成。バッチサイズ5でデータを供給
dataloader = DataLoader(dataset, batch_size=5, shuffle=True)
# 学習のエポック数を設定
num_epochs = 1500
print("\n--- 学習フェーズ開始 (1つの動画への過学習) ---")
# 指定されたエポック数だけ学習ループを実行
for epoch in range(num_epochs):
epoch_loss = 0 # エポックごとの損失を記録する変数
# データローダーからバッチ単位でデータを取得
for step, batch in enumerate(dataloader):
# 1ステップ分の学習を実行し、損失を取得
loss = train_step(batch)
epoch_loss += loss
# 10エポックごとに進捗(損失)を出力
if (epoch+1) % 10 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss:.4f}")
print("--- 学習フェーズ終了 ---\n")
このブロックは、これまでに定義したコンポーネントを組み合わせて学習を実際に実行する部分です。
- データセットとデータローダー:
- DavisAnyDoorDatasetクラスを使って、データセットのインスタンスを作成します。ここではsingle_video=Trueとすることで、1つの動画(
blackswan)のみを学習対象としています。これは、モデルが正しく学習できるかを迅速に確認するための「過学習テスト」です。少量のデータにモデルを意図的に過学習させ、損失が下がり、それらしい画像が生成できれば、モデルの基本的な構造や学習プロセスが機能していると判断できます。 - DataLoaderは、データセットからミニバッチを効率的に作成し、学習ループに供給する役割を担います。shuffle=Trueにより、エポックごとにデータの順序がシャッフルされ、学習の安定性が向上します。
- DavisAnyDoorDatasetクラスを使って、データセットのインスタンスを作成します。ここではsingle_video=Trueとすることで、1つの動画(
- 学習ループ:
- num_epochsで指定された回数(ここでは1500回)、学習を繰り返します。
- for step, batch in enumerate(dataloader):のループで、データローダーからバッチを受け取り、train_step(batch)を呼び出してモデルのパラメータを更新します。
- 10エポックごとに現在の損失を表示し、学習が順調に進んでいるか(損失が減少しているか)をモニタリングします。
この過学習テストが成功すれば、より多くのデータ(single_video=False)と長いエポック数で本格的な学習を行う準備が整ったと言えます。
推論の実行
学習済みモデルの性能を評価するために、推論を実行します。学習に使用した動画からフレームを選択し、オブジェクトのテレポートを試みます。生成された画像と正解画像を比較し、視覚的な評価と定量的評価(PSNR/SSIM)を行います。
print("--- 推論フェーズ開始 ---")
# 学習に使った動画の名前を取得
video_name = dataset.video_names[0]
img_paths = sorted(glob.glob(os.path.join(dataset.img_dir, video_name, "*.jpg")))
mask_paths = sorted(glob.glob(os.path.join(dataset.mask_dir, video_name, "*.png")))
# ソースオブジェクト(A)、ターゲットシーン(B)、比較用(C)のフレームインデックスを決定
idx_A = 0
idx_C = len(img_paths) - 1
idx_B = len(img_paths) // 2
# 指定したインデックスの画像とマスクをロードするヘルパー関数
def load_img_mask(idx):
img = cv2.imread(img_paths[idx])[..., ::-1]
mask = cv2.imread(mask_paths[idx], cv2.IMREAD_GRAYSCALE)
mask = (mask > 0).astype(np.float32)
img = cv2.resize(img, dataset.output_size, interpolation=cv2.INTER_LINEAR)
mask = cv2.resize(mask, dataset.output_size, interpolation=cv2.INTER_NEAREST)
return img, mask
# 各フレームの画像とマスクをロード
img_A, mask_A = load_img_mask(idx_A) # ソースオブジェクト
img_C, mask_C = load_img_mask(idx_C) # 比較用
img_B, mask_B = load_img_mask(idx_B) # ターゲットシーン / 正解画像
# 推論用の入力データを作成
aligned_target_np, aligned_mask_np = dataset._align_image(img_A, mask_A, mask_B)
target_img_np = (img_A * mask_A[:, :, None]).astype(np.uint8)
scene_bg_np = (img_B * (1.0 - mask_B[:, :, None])).astype(np.uint8)
# PyTorchのTensorに変換し、バッチ次元を追加
inference_batch = {
"target_img": dataset.transform(target_img_np).unsqueeze(0),
"target_mask": dataset.mask_transform(mask_A).unsqueeze(0),
"aligned_target": dataset.transform(target_img_np).unsqueeze(0),
"aligned_mask": dataset.mask_transform(mask_A).unsqueeze(0),
"scene_bg": dataset.transform(scene_bg_np).unsqueeze(0),
"shape_mask": dataset.mask_transform(mask_B).unsqueeze(0)
}
# 画像生成関数を呼び出して推論を実行
result_img_array = generate_image(
model=model,
vae=vae,
scheduler=noise_scheduler,
batch=inference_batch,
device=device,
num_inference_steps=50
)
# --- 精度指標 (PSNR / SSIM) の計算 ---
# 正解画像 (img_B) と生成画像 (result_img_array) を比較
# PSNR (Peak Signal-to-Noise Ratio): 画質の劣化を示す指標。高いほど良い。
psnr_value = calculate_psnr(img_B, result_img_array, data_range=255)
# SSIM (Structural Similarity Index): 構造的な類似度を示す指標。1.0に近いほど良い。
ssim_value = calculate_ssim(img_B, result_img_array, data_range=255, channel_axis=-1)
print("\n--- 評価結果 ---")
print(f"PSNR : {psnr_value:.2f} dB (高いほど良い)")
print(f"SSIM : {ssim_value:.4f} (1.0に近いほど良い)")
print("----------------\n")
# --- Matplotlibによるプレビュー表示 ---
fig, axes = plt.subplots(1, 4, figsize=(20, 5))
axes[0].imshow(img_A)
axes[0].set_title(f"Frame A (Source Object)\nIdx: {idx_A}")
axes[1].imshow(img_C)
axes[1].set_title(f"Frame C (Distant)\nIdx: {idx_C}")
axes[2].imshow(img_B)
axes[2].set_title(f"Frame B (Ground Truth)\nIdx: {idx_B}")
axes[3].imshow(result_img_array)
axes[3].set_title(f"Generated Frame B\nPSNR: {psnr_value:.2f} | SSIM: {ssim_value:.4f}")
for ax in axes:
ax.axis('off')
plt.tight_layout()
plt.show()
else:
print(f"Dataset directory '{davis_root}' not found. Please check the path.")
学習が完了したモデルを使い、実際に画像生成タスクを実行してその性能を評価します。
- 推論データの準備:
- 学習に使用した動画(blackswan)から、3つの異なるフレームを抽出します。
- フレームA (idx_A): テレポートさせたい「ソースオブジェクト」が含まれるフレーム。
- フレームB (idx_B): オブジェクトを配置したい「ターゲットシーン」。これが生成画像の「正解」にもなります。
- フレームC (idx_C): 比較のために表示する、時間的に離れたフレーム。
- これらのフレームから、target_img(ソースオブジェクト)、scene_bg(ターゲットの背景)、shape_mask(ターゲットの形状)など、generate_image関数が必要とする入力データを一式作成します。
- 学習に使用した動画(blackswan)から、3つの異なるフレームを抽出します。
- 推論の実行:
- 作成したinference_batchをgenerate_image関数に渡し、画像生成を実行します。この関数は内部で50ステップのノイズ除去プロセスを経て、最終的な画像をNumpy配列として返します。
- 定量的評価:
- 生成された画像(result_img_array)と正解画像(img_B)を比較し、2つの客観的な画質評価指標を計算します。
- PSNR (Peak Signal-to-Noise Ratio): ピーク信号対雑音比。2つの画像のピクセル値の差から計算され、値が大きいほど元画像に近い(劣化が少ない)ことを意味します。単位はデシベル(dB)です。
- SSIM (Structural Similarity Index): 構造的類似性指数。輝度、コントラスト、構造という3つの要素から画像の類似度を評価します。人間の視覚的な認識に近いとされ、最大値は1.0です。
- 生成された画像(result_img_array)と正解画像(img_B)を比較し、2つの客観的な画質評価指標を計算します。
- 視覚的評価:
- matplotlibを使い、ソース画像、正解画像、安定してモデルが生成した画像を並べて表示します。これにより、客観的な指標だけでは分からない、生成物の自然さや破綻の有無などを視覚的に確認できます。生成画像のタイトルには、計算したPSNRとSSIMの値も表示しています。
実行結果
上記のコードを実行すると、以下の結果が出力されます。
DAVIS2017 dataset found. Loading SINGLE video for quick verification...
--- 学習フェーズ開始 (1つの動画への過学習) ---
Epoch [10/1500], Loss: 0.0735
Epoch [20/1500], Loss: 0.0661
Epoch [30/1500], Loss: 0.0401
...
Epoch [1480/1500], Loss: 0.0593
Epoch [1490/1500], Loss: 0.0402
Epoch [1500/1500], Loss: 0.0085
--- 学習フェーズ終了 ---
--- 推論フェーズ開始 ---
--- 評価結果 ---
PSNR : 15.95 dB (高いほど良い)
SSIM : 0.1999 (1.0に近いほど良い)
----------------
学習と推論を実行した結果を示します。
- 学習フェーズ:
- Epoch [10/1500], Loss: 0.0735 から始まり、学習が進むにつれて損失(Loss)が順調に減少していることが確認できます。これは、モデルがデータに適合しようとパラメータを正しく更新していることを示しています。最終的に損失は0.0085まで下がり、単一動画への過学習が成功したと考えられます。
- 評価結果:
- PSNRは15.95 dB、SSIMは0.1999という結果になりました。これらの値は、高品質な画像生成としてはまだ改善の余地があることを示唆しています。PSNRは一般的に30dB以上で高品質と見なされることが多く、SSIMも1.0に近いほど良いとされます。
- 今回はあくまで概念実証のためのシンプルな実装と小規模なデータでの学習であるため、指標が低いのは想定内です。より多くデータで長時間学習したり、モデルのアーキテクチャやハイパーパラメータを調整したりすることで、これらの指標は向上すると期待されます。
- 視覚的評価:
- Generated Frame Bを見ると、ソースオブジェクトである熊がターゲットシーンの背景に合成されているのがわかります。しかし、実際の正解画像(Frame B (Ground Truth))と比較すると、生成された画像では全体的に白っぽくなってしまっていたり、奥の足の配置が異なっていたり、手前の岩にある草の面積が変わってしまっていたりと、細部のディテール再現において課題が見られます。それでも、ID特徴と詳細特徴を分離して注入するというAnyDoorの基本コンセプトが機能し、指定された形状とシーンへオブジェクトが調和して配置されていることは、この結果から確認できます。
実際のAnyDoorによる画像合成
以下では、Google Colab(L4もしくはA100のGPU環境)を使用し、実際のAnyDoorのライブラリを用いて画像の合成処理を行います。
合成に使う画像
合成は以下のGoogle AI Studioで作成した画像を用います。(サンプル画像をダウンロードして、Google Colabの実行環境にアップロードしてください)
オレンジ画像(orange.jpg)

りんご画像(apple.jpg)

AnyDoorライブラリのインストール
AnyDoorの公式リポジトリをクローンし、Google Colab環境で発生する依存関係のエラーを回避するためにrequirements.txtから問題となる特定のライブラリ定義をsedコマンドで削除します。その後、修正したrequirements.txtを用いて必要なパッケージ群をインストールします。
# リポジトリのクローンとディレクトリへの移動(すでに移動済みの場合は無視して構いません)
!git clone https://github.com/ali-vilab/AnyDoor.git
%cd AnyDoor
# --- 諸悪の根源となっている古いライブラリ群をすべて除外 ---
!sed -i '/numpy/d' requirements.txt
!sed -i '/opencv/d' requirements.txt
!sed -i '/scipy/d' requirements.txt
!sed -i '/share/d' requirements.txt
!sed -i '/torch==/d' requirements.txt
!sed -i '/torchvision==/d' requirements.txt
!sed -i '/xformers/d' requirements.txt
!sed -i '/safetensors/d' requirements.txt
!sed -i '/tokenizers/d' requirements.txt
# ↓今回追加:古いtokenizersを連れてくる元凶を除外
!sed -i '/transformers/d' requirements.txt
# パッケージのインストールを実行
!pip install -r requirements.txt
このコードは、AnyDoorの実行環境をGoogle Colab上にセットアップするためのシェルコマンドです。まず!git cloneでAnyDoorの公式GitHubリポジトリをローカルに複製し、%cd AnyDoorでそのディレクトリに移動します。次に、sed -i '/numpy/d' requirements.txtのような一連のsedコマンドが実行されます。これは、requirements.txtファイルから特定のライブラリ(例: numpy, torch, transformersなど)のバージョン指定行を直接削除する処理です。Colabにプリインストールされているライブラリとのバージョン競合を防ぐために行われます。最後に、編集されたrequirements.txtを元に、!pip install -r requirements.txtで依存関係をインストールします。
DinoV2の重みのダウンロード
AnyDoorのID特徴抽出に不可欠な、事前学習済みモデルであるDINOv2の重みファイル(dinov2_vitg14_pretrain.pth)を、wgetコマンドを用いてMeta社の公式URLからダウンロードします。ダウンロードしたファイルはpretrained_modelsディレクトリに保存されます。
# Meta公式からDINOv2の重みファイルをダウンロード(約4.2GBあるため少し時間がかかります)
!wget -O pretrained_models/dinov2_vitg14_pretrain.pth https://dl.fbaipublicfiles.com/dinov2/dinov2_vitg14/dinov2_vitg14_pretrain.pth
このコマンドは、wgetユーティリティを使用して、AnyDoorモデルが内部で利用するDINOv2の事前学習済み重みファイルをダウンロードしています。-O pretrained_models/dinov2_vitg14_pretrain.pthというオプションは、ダウンロードしたファイルを指定したパス(pretrained_modelsディレクトリの下にdinov2_vitg14_pretrain.pthという名前で)保存することを意味します。URLはMeta社の公式配布元を指しており、AnyDoorがオブジェクトのID特徴を正確に抽出するために、この大規模な事前学習済みモデル(約4.2GB)が必要となります。
anydoor.yamlの編集
AnyDoorの設定ファイルであるconfigs/anydoor.yamlをPythonのファイル操作で読み込みます。設定ファイル内に記述されているDINOv2の重みへのパスを、先ほどダウンロードした実際のパス(pretrained_models/dinov2_vitg14_pretrain.pth)に置換して、設定を更新します。
import os
os.chdir("/content/AnyDoor")
pth_path = "pretrained_models/dinov2_vitg14_pretrain.pth"
yaml_path = "configs/anydoor.yaml"
with open(yaml_path, "r", encoding="utf-8") as f:
content = f.read()
content = content.replace("path/dinov2_vitg14_pretrain.pth", pth_path)
with open(yaml_path, "w", encoding="utf-8") as f:
f.write(content)
このPythonスクリプトは、AnyDoorの設定ファイルをプログラム的に編集する処理です。os.chdirでカレントディレクトリをAnyDoorのリポジトリルートに変更した後、先ほどダウンロードしたDINOv2の重みファイルのパスをpth_path変数に格納します。次に、設定ファイルconfigs/anydoor.yamlを読み込み、content.replace()メソッドを使って、デフォルトで記述されているダミーのパス("path/dinov2_vitg14_pretrain.pth")を、正しいpth_pathの値に置換します。最後に、更新された内容を同じファイルに書き戻すことで、モデルが重みファイルを正しく参照できるようにしています。
関連ライブラリのインストール・アンインストール
AnyDoorの推論実行に必要な追加ライブラリ(pycocotools, transformersなど)をpipでインストールします。同時に、他のライブラリとの依存関係で問題を引き起こす可能性があるdatasetsやkerasといったパッケージをアンインストールし、環境を整備します。
!pip install pycocotools requests matplotlib pillow transformers accelerate
!pip install cog
!pip install open_clip_torch
!pip uninstall datasets -y
!pip install --upgrade einops
!pip uninstall -y keras tf-keras keras-nightly
このセルでは、pipコマンドを用いて、AnyDoorの推論コード(predict.py)を実行するために必要な、しかしrequirements.txtには含まれていない追加ライブラリをインストールしています。具体的には、pycocotools(評価用)、transformers(GroundingDINO/SAM用)、open_clip_torchなどがインストールされます。一方で、!pip uninstall datasets -yのように、Hugging Faceのdatasetsライブラリをアンインストールしています。これは、AnyDoorリポジトリ内のdatasetsという名前のフォルダと名前が衝突し、インポートエラーを引き起こすためです。同様に、不要なkeras関連のパッケージも削除して、環境のクリーンアップを行っています。
フォルダのリネームとコードの書き換え
AnyDoorのリポジトリ内にあるdatasetsというフォルダ名が、Hugging Faceのdatasetsライブラリと名前衝突を起こし、インポートエラーの原因となります。この問題を解決するため、フォルダ名をanydoor_datasetsにmvコマンドでリネームし、sedコマンドで推論コード(predict.py)内の関連するインポート文も新しいフォルダ名を指すように修正します。
# AnyDoorディレクトリに移動
%cd /content/AnyDoor
# 1. 名前の衝突を起こしているフォルダを「anydoor_datasets」にリネーム
!mv datasets anydoor_datasets
# 2. 推論コード(predict.py)の中のインポート文を書き換え
!sed -i 's/from datasets.data_utils/from anydoor_datasets.data_utils/g' predict.py
print("フォルダのリネームとコードの書き換えが完了しました!")
このコードは、ライブラリ名の衝突問題を解決するための重要なステップです。AnyDoorのリポジトリにはdatasetsという名前のフォルダが存在しますが、これはHugging Faceのdatasetsライブラリと名前が同じであるため、Pythonがどちらをインポートすべきか混乱してしまいます。その対策として、まず!mv datasets anydoor_datasetsコマンドで、ローカルのフォルダ名をanydoor_datasetsというユニークな名前に変更します。しかし、フォルダ名を変えただけでは、ソースコード内のインポート文が古い名前のままでエラーになります。そこで、sed -i 's/from datasets.data_utils/from anydoor_datasets.data_utils/g' predict.pyコマンドを使い、推論スクリプトpredict.pyファイル内のfrom datasets.data_utilsという記述を、新しいフォルダ名を指すfrom anydoor_datasets.data_utilsに自動で置換しています。
推論コードの実行
これまでの準備をすべて終え、AnyDoorによる画像合成の推論を実行します。GroundingDINOとSAMを用いてソース画像とターゲット画像からオブジェクトのマスクを生成した後、AnyDoorのメインモデルをロードし、DDIMサンプラーを用いてテレポート処理を行います。最終的に、生成された画像をmatplotlibで表示して結果を確認します。
💡 事前準備
このコードを実行する前に、ダウンロードしたサンプル画像(orange.jpgとapple.jpg)を、Google Colabのカレントディレクトリ(AnyDoorリポジトリの直下)にアップロードしておいてください。
import cv2
import torch
import numpy as np
from PIL import Image
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection, SamModel, SamProcessor
import os
import sys
# Google Colabの環境依存エラー(einops関連)を回避するためのモジュールリロード処理
for module_name in list(sys.modules.keys()):
if "einops" in module_name:
del sys.modules[module_name]
# Python 3.12以降で廃止されたImpImporterをエミュレートするパッチ(古いライブラリとの互換性維持)
import pkgutil
if not hasattr(pkgutil, 'ImpImporter'):
class ImpImporter:
pass
pkgutil.ImpImporter = ImpImporter
# AnyDoor公式のヘルパー関数やモデル定義をインポート
from predict import process_pairs
from cldm.model import create_model, load_state_dict
from pytorch_lightning import seed_everything
# バックエンドにPyTorchを指定
os.environ["EINOPS_BACKEND"] = "torch"
from cldm.ddim_hacked import DDIMSampler
import matplotlib.pyplot as plt
# 実行デバイス(GPUが利用可能ならCUDA、なければCPU)を決定
device = "cuda" if torch.cuda.is_available() else "cpu"
# ==============================================================================
# --- 0. GroundingDINO & SAM(セグメンテーションモデル)のセットアップ ---
# ==============================================================================
print("GroundingDINOとSAMのモデルをロード中...")
# 物体検出モデル GroundingDINO のプロセッサとモデルをロード
gd_processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-base")
gd_model = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-base").to(device)
# 高精度セグメンテーションモデル SAM のプロセッサとモデルをロード
sam_processor = SamProcessor.from_pretrained("facebook/sam-vit-base")
sam_model = SamModel.from_pretrained("facebook/sam-vit-base").to(device)
# テキスト指示から対象のマスク画像を自動生成し、検出枠を視覚化して保存する関数
def generate_mask_and_visualize(image_path, text_prompt, output_mask_path, output_vis_path):
# 画像をRGB形式で読み込む
image = Image.open(image_path).convert("RGB")
# GroundingDINOによる物体検出を実行
inputs = gd_processor(images=image, text=text_prompt, return_tensors="pt").to(device)
with torch.no_grad():
outputs = gd_model(**inputs)
# 検出結果のポストプロセス(境界ボックスの抽出)
results = gd_processor.post_process_grounded_object_detection(
outputs, inputs.input_ids, threshold=0.15, text_threshold=0.25, target_sizes=[image.size[::-1]]
)[0]
# オブジェクトが検出されなかった場合はエラー
if len(results["boxes"]) == 0:
raise ValueError(f"エラー: '{text_prompt}' が検出できませんでした。")
# 最もスコアの高い境界ボックスを取得
best_idx = torch.argmax(results["scores"])
bbox = results["boxes"][best_idx].tolist()
# 境界ボックス(物体検出枠)を画像に描画してデバッグ用に保存
img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
x_min, y_min, x_max, y_max = map(int, bbox)
cv2.rectangle(img_cv, (x_min, y_min), (x_max, y_max), (0, 0, 255), 3)
cv2.putText(img_cv, text_prompt, (x_min, y_min - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imwrite(output_vis_path, img_cv)
# 検出された境界ボックスをプロンプトとして、SAMにより高精度なオブジェクトマスクを生成
inputs_sam = sam_processor(image, input_boxes=[[bbox]], return_tensors="pt").to(device)
with torch.no_grad():
outputs_sam = sam_model(**inputs_sam)
# マスク画像のポストプロセス
mask = sam_processor.image_processor.post_process_masks(
outputs_sam.pred_masks.cpu(), inputs_sam["original_sizes"].cpu(), inputs_sam["reshaped_input_sizes"].cpu()
)[0][0][0].numpy()
# マスク画像を0-255の白黒画像(2値マスク)として保存
mask_img = (mask * 255).astype(np.uint8)
cv2.imwrite(output_mask_path, mask_img)
return output_mask_path
# ==============================================================================
# --- 1. マスク生成の実行 ---
# ==============================================================================
ref_image_path = "orange.jpg" # ソース画像(移植したいオブジェクトを含む画像)
tar_image_path = "apple.jpg" # ターゲット画像(移植先の背景となる画像)
ref_prompt = "orange." # 検出したいソースオブジェクトのプロンプト
tar_prompt = "apple" # 検出したいターゲット位置(入れ替え対象)のプロンプト
print("\n--- ソースオブジェクトの処理 ---")
ref_mask_path = "mask.png"
# ソース画像のオブジェクトを検出してマスクを作成
generate_mask_and_visualize(ref_image_path, ref_prompt, ref_mask_path, "ref_bbox_vis.jpg")
print("\n--- ターゲットシーンの処理 ---")
tar_mask_path = "taget_mask.png"
# ターゲット画像内のオブジェクトを検出してマスクを作成
generate_mask_and_visualize(tar_image_path, tar_prompt, tar_mask_path, "tar_bbox_vis.jpg")
# ==============================================================================
# --- 2. AnyDoorモデルによる前処理と画像合成(テレポート推論) ---
# ==============================================================================
model_ckpt = "pretrained_models/anydoor.ckpt" # ダウンロードしたAnyDoorの学習済み重み
config_file = "configs/anydoor.yaml" # 設定ファイルのパス
print("\nAnyDoorのモデルをロード中...")
# 設定ファイルに基づきAnyDoorモデルを定義
model = create_model(config_file).cpu()
# 学習済みの重みをGPU(CUDA)にロード
model.load_state_dict(load_state_dict(model_ckpt, location='cuda'))
model = model.cuda()
model.eval()
# 再現性確保のため乱数シードを固定
seed_everything(42)
# OpenCVを使って画像とセグメンテーションマスクを読み込む
ref_image = cv2.imread(ref_image_path)
tar_image = cv2.imread(tar_image_path)
ref_mask = cv2.imread(ref_mask_path, cv2.IMREAD_GRAYSCALE)
tar_mask = cv2.imread(tar_mask_path, cv2.IMREAD_GRAYSCALE)
# ノイズやブレを防止するため、マスク画像を確実に2値化(0または255)
_, ref_mask = cv2.threshold(ref_mask, 127, 255, cv2.THRESH_BINARY)
_, tar_mask = cv2.threshold(tar_mask, 127, 255, cv2.THRESH_BINARY)
# AnyDoor公式の前処理関数(process_pairs)を実行してモデルの入力データを生成
# 形状コントロールを無効化(enable_shape_control=False)することで、オブジェクトの元の形状を維持して合成
item = process_pairs(ref_image, ref_mask, tar_image, tar_mask, enable_shape_control=False)
print("AnyDoorのサンプラーを準備中...")
# DDIMサンプラーを初期化
sampler = DDIMSampler(model)
# モデルに入力するためのバッチデータを準備(Numpy配列からPyTorchのTensorに変換してGPUへ転送)
batch = {}
for k, v in item.items():
if isinstance(v, np.ndarray):
tensor_v = torch.from_numpy(v).float()
if tensor_v.ndim == 3:
# 軸を (H, W, C) から PyTorch標準の (C, H, W) に並べ替え
tensor_v = tensor_v.permute(2, 0, 1)
elif tensor_v.ndim == 2:
tensor_v = tensor_v.unsqueeze(0)
# バッチ次元(B=1)を追加してデバイスに配置
batch[k] = tensor_v.unsqueeze(0).to(device)
print("AnyDoorの推論を実行中...")
with torch.no_grad():
ref_img, hint_tensor = batch['ref'], batch['hint']
# ソース画像からオブジェクトのID特徴を抽出
c_crossattn = model.get_learned_conditioning(ref_img)
# 抽出したID特徴と詳細マップ(hint_tensor)を条件(cond)として指定
cond = {"c_crossattn": [c_crossattn], "c_concat": [hint_tensor]}
# Classifier-Free Guidance (CFG) のための無条件(Unconditional)のダミー条件を準備
uc_cross = model.get_unconditional_conditioning(1)
uc = {"c_crossattn": [uc_cross], "c_concat": [hint_tensor]}
# DDIMサンプラーを用いて50ステップのサンプリング(ノイズ除去ループ)を実行
# guidance_scale(強さ)には、公式推奨の 4.5 を指定
samples, _ = sampler.sample(
S=50, batch_size=1, shape=(4, 64, 64),
conditioning=cond, verbose=False,
unconditional_guidance_scale=4.5, unconditional_conditioning=uc
)
# 潜在表現(サンプル)をファーストステージのデコーダを通してピクセル空間の画像に復元
x_samples = model.decode_first_stage(samples)
# 画像のテンソル値を [-1, 1] から [0, 255] の範囲に正規化・スケーリング
x_samples = (torch.clamp(x_samples, -1.0, 1.0) + 1.0) / 2.0 * 255.0
# Numpy配列に変換し、次元を (C, H, W) から (H, W, C) に戻す
out_image = x_samples.cpu().numpy()[0].transpose(1, 2, 0).astype(np.uint8)
# 出力画像(RGB形式)を、OpenCVでの保存用にBGR形式に変換してからファイルに保存
cv2.imwrite("output_anydoor.jpg", cv2.cvtColor(out_image, cv2.COLOR_RGB2BGR))
print("推論が完了しました! 'output_anydoor.jpg' を確認してください!")
# 合成画像のプレビューを表示
plt.figure(figsize=(8, 8))
plt.imshow(out_image)
plt.axis("off") # 軸の目盛りを非表示にする
plt.title("AnyDoor Output")
plt.show()
この長大なコードは、AnyDoorによる画像合成の推論プロセス全体を実装しています。
ステップ0: GroundingDINO & SAM: まず、テキストプロンプト(例: "orange")からオブジェクトの位置を特定するGroundingDINOモデルと、その位置情報(バウンディングボックス)から高精度なセグメンテーションマスクを生成するSAM (Segment Anything Model)をロードします。generate_mask_and_visualize関数は、これら2つのモデルを連携させ、入力画像から指定したオブジェクトのマスクを自動で作成します。
ステップ1: マスク生成: ref_image_path(ソースオブジェクト画像)とtar_image_path(ターゲットシーン画像)それぞれに対してgenerate_mask_and_visualizeを呼び出し、"orange."と"apple"のマスクを生成してファイルに保存します。
ステップ2: AnyDoor推論:
- cldm.model.create_modelで設定ファイルに基づきAnyDoorモデルを構築し、load_state_dictで事前学習済みの重み(anydoor.ckpt)をロードします。
- predict.process_pairs関数で、ソース画像、ソースマスク、ターゲット画像、ターゲットマスクを入力として、AnyDoorモデルが必要とする形式のテンソル(ref, hintなど)に前処理します。
- DDIMSamplerを用いてノイズ除去ループを開始します。このとき、model.get_learned_conditioningでソースオブジェクトからID特徴(c_crossattn)を抽出し、process_pairsの出力(hint)を詳細特徴としてcond辞書に格納します。Classifier-Free Guidanceのために、空の条件ucも用意します。
- sampler.sampleで50ステップのDDIMサンプリングを実行し、ノイズから潜在表現samplesを生成します。
- 最後にmodel.decode_first_stageで潜在表現をピクセル空間の画像にデコードし、matplotlibで結果を表示します。
実行結果
推論コードを実行した際の標準出力を示します。GroundingDINO、SAM、AnyDoorの各モデルが順次ロードされ、最終的にDDIMサンプラーが50ステップのノイズ除去プロセスを実行して画像が生成されるまでの流れが確認できます。
GroundingDINOとSAMのモデルをロード中...
AnyDoorのモデルをロード中...
AnyDoorのサンプラーを準備中...
AnyDoorの推論を実行中...
Data shape for DDIM sampling is (1, 4, 64, 64), eta 0.0
Running DDIM Sampling with 50 timesteps
推論が完了しました! 'output_anydoor.jpg' を確認してください!

この出力は、前のセルの推論コードを実行した際のログです。GroundingDINOとSAMのモデルをロード中...から始まり、AnyDoorのモデルとサンプラーが順次準備されていく様子がわかります。Data shape for DDIM sampling is (1, 4, 64, 64), eta 0.0は、これから始まるノイズ除去プロセスが、バッチサイズ1、4チャンネル、64x64解像度の潜在空間で行われることを示しています。Running DDIM Sampling with 50 timestepsというメッセージが表示された後、サンプリングが実行され、最終的に推論が完了しました!というメッセージと共に処理が終わります。一連のログは、モデルのロードから画像生成まで、すべてのステップがエラーなく正常に完了したことを示しています。
まとめ
本記事では、特定のオブジェクトを違和感なく別の画像に合成する「ゼロショット・オブジェクトレベル画像カスタマイズ」を実現するAnyDoorモデルについて、その全体像から実践的な利用方法までを深く掘り下げて解説しました。
記事を通じて、以下の内容を段階的に学習しました。
- AnyDoorのコア技術の理解: オブジェクトの識別情報を保持する「ID特徴」と、テクスチャを捉える「詳細特徴」を分離してU-Netに注入する独創的なアーキテクチャを学びました。
- コンセプト実装による理論の確認: PyTorchを用いて、ID特徴抽出器や詳細特徴抽出器などの主要コンポーネントをミニマルに実装し、学習から推論までの一連の流れをコードレベルで検証しました。
- 公式ライブラリによる実践的な画像合成: AnyDoorの公式実装をGoogle Colab上で動作させるための環境構築(依存関係の解決)から、GroundingDINOとSAMを連携させたマスク生成、そして最終的な推論コードの実行まで、実用的な利用手順を体験しました。
AnyDoorは、仮想試着や製品画像の自動生成、クリエイティブ制作など、多岐にわたる応用が期待される非常に強力な技術です。本記事が、その仕組みの理論的な理解から、実際にご自身の手で動かしてみるまでの一助となれば幸いです。
本記事の文章・構成の一部に生成AIを使用しています。