1. 概要
こちらのブログに記載した内容の続きになります。
上記のブログでは、2025年12月21日(日)に開催予定の第10回 岐阜AI勉強会のため、Stability AI の Stable Diffusion 3.5 の参照実装を git clone して Google Colab PRO で動かす準備をしました。
このブログでは生成途中の画像を出力するよう、参照実装のスクリプトを修正して動かしました。参照実装の処理内容の確認の一環として試してみることにしました。
2. 生成途中の画像の出力例
下の動画は Stable Diffusion 3.5 で 28 step のノイズ除去処理 (denoising) を適用していく途中の画像を並べた例になります。画像生成に使用したプロンプトは下記のプロンプトになります。
A dog and a cat sitting side by side.
3. スクリプトの修正箇所
スクリプト実行時に –steps で指定した回数だけノイズ除去を実行する sd3_impls.py の下記の関数にハイライトした行のコードを追加します。
ノイズ除去は実際の画像より小さな潜在空間 (latent space) で適用されます。 追加したスクリプトはノイズ除去が 1 ステップ適用されるごとに潜在空間の多次元配列データ x の複製を作成し、順に lantent_list に追加しています。最後の行で latent_list を戻り値として返すようにしていますが、修正前は x を返していました。
def sample_dpmpp_2m(model, x, sigmas, extra_args=None):
"""DPM-Solver++(2M)."""
# list of latent tensor during denoising steps
latent_list = []
...
for i in tqdm(range(len(sigmas) - 1)):
denoised = model(x, sigmas[i] * s_in, **extra_args)
t, t_next = t_fn(sigmas[i]), t_fn(sigmas[i + 1])
h = t_next - t
if old_denoised is None or sigmas[i + 1] == 0:
x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised
else:
h_last = t - t_fn(sigmas[i - 1])
r = h_last / h
denoised_d = (1 + 1 / (2 * r)) * denoised - (1 / (2 * r)) * old_denoised
x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised_d
old_denoised = denoised
# append copy of latent tensor during denoising steps
latent_list.append(x.clone().detach())
return latent_list
上記の関数 sample_dpmpp_2m を呼び出す側の sd_infer.py に記載された SD3Inferencer クラスの do_sampling メソッドを下記のように書き換えます。sample_fn が上記の関数 sample_dpmpp_2m です。変更前の戻り値は一つの潜在空間 latent でしたが、潜在空間のリストを返すようにしたので名前を latent から latent_list に変えました。
latent = SD3LatentFormat().process_out(latent) は一つの潜在空間の多次元配列 (tensor) を受け取り、全要素をある定数で割ってからある定数を加えて返すメソッドです。このタイミングでは変換を適用しないことにし、コメントアウトしました。
def do_sampling(
self,
latent,
...
) -> torch.Tensor:
...
sample_fn = getattr(sd3_impls, f"sample_{sampler}")
...
latent_list = sample_fn(
denoiser(self.sd3.model, steps, skip_layer_config),
noise_scaled,
sigmas,
extra_args=extra_args,
)
# latent = SD3LatentFormat().process_out(latent)
self.sd3.model = self.sd3.model.cpu()
self.print("Sampling done")
return latent_list
上記の do_sampling メソッドを呼び出す SD3Inferencer クラスの gen_image メソッドを下記のように書き換えました。
do_sampling メソッドの戻り値の潜在空間のリストを sampled_latent_list にセットし、下記のコードの 20 から 26 行目のコードで潜在空間から画像データに戻し、JPG 画像として書き出しています。
潜在空間をリストから取り出したら、先ほどコメントアウトした sampled_latent = SD3LatentFormat().process_out(sampled_latent) による変換をまず適用しています。次に image = self.vae_decode(sampled_latent) を実行し、VAE (Variational AutoEncoder) のデコード処理を適用して潜在空間から画像データに戻しています。戻した画像データを image_list に追加し、確認のため JPG 画像として書き出しています。
28 から 34 行目のコードは image_list に格納しておいた画像を並べた GIF ファイルを保存するコードです。duration=200 で 1 画像あたりの表示時間(ミリ秒)、loop=0 で最後の画像を表示したら最初から繰り返し表示することを指定しています。
def gen_image(
self,
...
):
...
if init_image:
latent = self._image_to_latent(init_image, width, height)
else:
latent = self.get_empty_latent(1, width, height, seed, "cpu")
latent = latent.cuda()
...
for i, prompt in pbar:
...
sampled_latent_list = self.do_sampling(
latent,
...
)
image_list = []
for j, sampled_latent in enumerate(sampled_latent_list):
sampled_latent = SD3LatentFormat().process_out(sampled_latent)
image = self.vae_decode(sampled_latent)
image_list.append(image)
save_path = os.path.join(out_dir, f"{i:06d}-{j:03d}.jpg")
self.print(f"Saving to to {save_path}")
image.save(save_path)
image_list[0].save(
fp=os.path.join(out_dir, f"{i:06d}.gif"),
save_all=True,
append_images=image_list[1:],
duration=200,
loop=0
)
self.print("Done")
このコードで生成した GIF ファイルは 23 MB ほどありました。そのため、Linux 上で下記の例のようなコマンドを実行し、500 KB ほどの MP4 ファイルに変換したものをこのブログページでは使用しています。
ffmpeg -i stable-diffusion-3-5-dog-and-cat.gif -movflags +faststart -pix_fmt yuv420p -vf "scale=iw/2:ih/2" -crf 28 -preset fast stable-diffusion-3-5-dog-and-cat-small.mp4