FLUX.1 [schnell] の生成途中の画像を diffusers の FluxPipeline のcallback 関数を使用して出力

1. 概要

こちらのブログに記載した内容の続きになります。

上記のブログでは、2025年12月21日(日)に開催予定の第10回 岐阜AI勉強会のため、Black Forest Labs の FLUX.1 [schnell]をデバッグ実行しました。今回は、ノイズ除去処理 (denoising) を適用した画像を 1 ステップずつ出力するスクリプトを用意して動かしました。

調査前はこちらのブログに記載した方法と同様、git clone したスクリプトを修正する方法での実装を考えていました。diffusers には git clone したスクリプトを修正しなくても FluxPipeline に callback 関数を渡して生成途中の潜在空間の多次元配列 (latent) を参照するインターフェースが用意されています。そのため、callback 関数を使用して生成途中の画像を出力するようにしました。

Google Colab PRO で A100 GPU を選択して実行しました。

2. 生成途中の画像の出力例

下の動画は FLUX.1 [schnell] で 4 step のノイズ除去処理 (denoising) を適用していく途中の画像を並べた例になります。画像生成に使用したプロンプトは下記のプロンプトになります。4 枚の画像が切り替わっていく動画になります。(下記のスクリプトで得られた約 3MB の GIF ファイルを約 100KB の MP4 ファイルに変換したものです。)

The phrase “Gifu AI Study Group” formed from small colorful geometric shapes.

3. callback 関数を渡して生成途中の画像を出力するスクリプト

こちらのリンク先に用意した Google Colab のページのコードセルを順に実行すると生成途中の画像を含めた 4 枚の画像が切り替わっていく動画が得られます。

git clone したコードのデバッグ実行を試したこちらのブログの Google Colab のページを流用したため git clone したスクリプトを実行しています。callback 関数を使用しているので pip で diffusers をインストールした場合でも動作するスクリプトになっています。

下記のコードセルが callback 関数を使用して生成途中の画像を含めた 4 枚の画像が切り替わっていく GIF 画像を生成するコードセルになります。

from PIL import Image
import os

prompt = 'The phrase "Gifu AI Study Group" formed from small colorful geometric shapes.'

height = 1024
width = 1024

out_dir = "steps"
os.makedirs(out_dir, exist_ok=True)

# 生成途中の画像を追加する GIF ファイル用の空のリスト
gif_frames = []

def callback(pipe, step_index, timestep, callback_kwargs):
    latents = callback_kwargs["latents"]

    with torch.no_grad():
        latents = pipe._unpack_latents(latents, height, width, pipe.vae_scale_factor)
        latents = (latents / pipe.vae.config.scaling_factor) + pipe.vae.config.shift_factor
        images = pipe.vae.decode(latents, return_dict=False)[0]
        images = pipe.image_processor.postprocess(images, output_type="pil")

        images[0].save(f"steps/step_{step_index:03d}.png")
        gif_frames.append(images[0])

    return callback_kwargs

pipe(
    prompt,
    height=height,
    width=width,
    guidance_scale=0.0,
    num_inference_steps=4,
    max_sequence_length=256,
    callback_on_step_end=callback
)

# =====  GIF を作成 =====
gif_path = "Gifu-AI-Study-Group.gif"

gif_frames[0].save(
    gif_path,
    save_all=True,
    append_images=gif_frames[1:],
    duration=500,  # ms / frame
    loop=0
)

print(f"GIF saved to {gif_path}")

上記のコードのハイライトした行は下記のスクリプトの

src/diffusers/pipelines/flux/pipeline_flux.py

下記のハイライトした行に対応する変換をしています。callback 関数内では output_type=”pil” を指定し、潜在空間の多次元配列 (tensor) のリスト latents を PIL(Python Imaging Library)形式の画像のインスタンスのリストに変換しています。

    def __call__(
        self,
        ...
    ):
        ...
        if output_type == "latent":
            image = latents
        else:
            latents = self._unpack_latents(latents, height, width, self.vae_scale_factor)
            latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor
            image = self.vae.decode(latents, return_dict=False)[0]
            image = self.image_processor.postprocess(image, output_type=output_type)
        ...
4. FluxPipeline の callback 関数について

diffusers の Pipeline の callback 関数については Hugging Face のこちらのページに解説があります。

下記のスクリプトの FluxPipeline の __call__ メソッドのコメントには下記のように記載されています。

src/diffusers/pipelines/flux/pipeline_flux.py

callback_on_step_end (`Callable`, *optional*):
A function that calls at the end of each denoising steps during the inference. The function is called with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int, callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by `callback_on_step_end_tensor_inputs`.
callback_on_step_end_tensor_inputs (`List`, *optional*):
The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the `._callback_tensor_inputs` attribute of your pipeline class.

FluxPipeline の __call__ メソッドでは下記のように参照されています。

    def __call__(
        self,
        ...
        callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
        callback_on_step_end_tensor_inputs: List[str] = ["latents"],
        ...
    ):
        ...
        with self.progress_bar(total=num_inference_steps) as progress_bar:
            for i, t in enumerate(timesteps):
                ...
                if callback_on_step_end is not None:
                    callback_kwargs = {}
                    for k in callback_on_step_end_tensor_inputs:
                        callback_kwargs[k] = locals()[k]
                    callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)

                    latents = callback_outputs.pop("latents", latents)
                    prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)

上記のハイライトした行で callback_on_step_end(self, i, t, callback_kwargs) に渡される引数は下記のようになっています。

  • self: FluxPipeline のインスタンス
  • i: 現在のステップ番号
  • t: 現在のステップのタイムステップ値
  • callback_kwargs: 該当ステップのテンソルなどをローカル変数名を key, インスタンスを value として格納した dict 型のインスタンス。__call__ メソッドに callback_on_step_end_tensor_inputs を渡さない場合は、latents のインスタンスだけがセットされています。

上記 3. の callback 関数内ではこれらのインスタンスを参照し、ノイズ除去の各ステップが完了した時点の画像を生成しています。

今回試したスクリプトでは git clone したスクリプトは修正しませんでしたが、その場合でも diffusers のスクリプトをローカルの PC で見られるようにしておいたほうが実装は進め易い気がしました。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA