
Stability AI のStable Diffusion をGoogle のColaboratory で使ってみます。
Stable Diffusion は現時点でtxt2img(Text-To-Image)とimg2img(Image-To-Image)の2つのモードが扱えます。
将来的には使えるメディアはaudioやvideoにも拡張されるそうです(そうだろうなぁ、という気はします)。
ここでは、Colab でStable Diffusion を扱うための要件をクリアしたあとで、txt2img(Text-To-Image)とimg2img(Image-To-Image)の2つのモードをやってみます。
参照するのはHugging Face が公開しているコード、Diffusers です。
ColabでStable Diffusionを使うにはGoogle のアカウントとHugging Face のアカウントとアクセストークンの3つが必要です。
1/3〜3/3でこの3つの要件をクリアした後で、Stable Diffusionの2つのモードを試してみます。
1/3 Google アカウント作成
以下にアクセスしてください(GMail アドレスをお持ちの場合は不要)
このアカウントでColab を開きます。
2/3 Hugging Face アカウント作成
Hugging Faceで公開されているStable Diffusionのページにアクセス
stable-diffusion-v-1-4-original
Stable Diffusionのライセンスに関する確認事項です。
まぁ適当に読んで「Access repository」をクリック。
Hugging Faceのログイン画面が表示されます、初回の場合は「Sign UP」をクリック
登録したいメールアドレスとパスワードを入力して「Next」をクリック
ユーザー名とフルネームを入力してからチェックを入れ、「Create Account」をクリック
登録したメールアドレス宛てに確認メールが届くので、本文内のURLをクリック
「Your email address has been verified successfully」と表示されれば、Hugging Faceのアカウント作成は完了
再度Stable Diffusionのページにアクセスして、ライセンスに同意するチェックを入れてから「Access repository」をクリック
3/3 アクセストークン取得
Stable Diffusionのページにアクセス
stable-diffusion-v-1-4-original
画面右上の円形のアイコンをクリックしてから「Settings」をクリック
アカウントの設定画面が開いたら「Access Tokens」を選択
トークンの発行画面が表示されたら覚えやすい名前を入力して「Generate a token」をクリック
トークンが発行されたら、showをクリックして伏せられた文字を表示してコピーしておきます。
Colab を開いてText-To-Image をやってみる
Colab にアクセス
Google アカウントでログイン
「編集」をクリックしてから「ノートブックの設定」をクリック
GPU を選択して保存
ファイルー>ノートブックを新規作成
コードは「セル」に記述して実行しますが、セルは自動では表示されません。いちいち「+ コード」をクリックして追加表示し、実行はセルの左端のアイコンをクリックします。
例えば、以下を実行すれば、現在のスタッツやどんなGPUが割当てられているのか確認できます。
1 |
!nvidia-smi |
ここでは、Tesla T4 がアサインされていますが、運がよければA100が割当てられるかも…..。
Stable Diffusionをインストール
1 2 3 |
!pip install diffusers==0.2.4 !pip install transformers scipy ftfy !pip install "ipywidgets>=7,<8" |
Hugging Face にログインします。
1 2 3 4 5 6 7 |
from google.colab import output output.enable_custom_widget_manager() from huggingface_hub import notebook_login notebook_login() |
Token を聞いてくるので、上記(3/4)でコピーしておいたものをペーストしてlogin
以下のようなメッセージが出ればログイン完了
以下のコマンドを実行
StableDiffusionPipeline
は、わずか数行のコードでテキストから画像を生成するために使用できるエンド ツー エンドの推論パイプラインです。
以下ではGoogle Colab で Stable Diffusion をfloat16 精度で実行するようにしています。
可能な限り最高の精度を確保したい場合は、メモリ使用量が高くなりますが、 revision="fp16"
と torch_dtype=torch.float16
を削除してください。
1 2 3 4 5 |
import torch from diffusers import StableDiffusionPipeline # make sure you're logged in with `huggingface-cli login` pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", revision="fp16", torch_dtype=torch.float16, use_auth_token=True) |
各種ファイルがダウンロードされます。
終了後以下を実行
パイプラインをGPU に移動して、推論を高速化します。
1 |
pipe = pipe.to("cuda") |
プロンプトで画像生成用テキストを発行して画像生成
autocastを使用すると、半精度を使用するため、推論がより高速に実行されます。
「a photograph of an astronaut riding a horse (馬にまたがった宇宙飛行士の写真)」という要求です。
1 2 3 4 5 6 7 8 9 10 11 |
from torch import autocast prompt = "a photograph of an astronaut riding a horse" with autocast("cuda"): image = pipe(prompt)["sample"][0] # image here is in [PIL format](https://pillow.readthedocs.io/en/stable/) # Now to display an image you can do either save it such as: image.save(f"astronaut_rides_horse.png") # or if you're in a google colab you can directly display it with image |
こんな画像が生成されました。
Appendix1
Text-To-Image でパラメータををいじってみる
以下のmanual_seed値で画像を生成
1 2 3 4 5 6 7 8 |
import torch generator = torch.Generator("cuda").manual_seed(1024) with autocast("cuda"): image = pipe(prompt, generator=generator)["sample"][0] image |
num_inference_stepsは50がデフォですが15に落としてみます。
これで、画像生成の速度があがりますが、描画のための情報量が減るので、描画精度は落ちます。
1 2 3 4 5 6 7 8 |
import torch generator = torch.Generator("cuda").manual_seed(1024) with autocast("cuda"): image = pipe(prompt, num_inference_steps=15, generator=generator)["sample"][0] image |
複数画像生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from PIL import Image def image_grid(imgs, rows, cols): assert len(imgs) == rows*cols w, h = imgs[0].size grid = Image.new('RGB', size=(cols*w, rows*h)) grid_w, grid_h = grid.size for i, img in enumerate(imgs): grid.paste(img, box=(i%cols*w, i//cols*h)) return grid num_images = 3 prompt = ["a photograph of an astronaut riding a horse"] * num_images with autocast("cuda"): images = pipe(prompt)["sample"] grid = image_grid(images, rows=1, cols=3) grid |
複数画像生成−2
1 2 3 4 5 6 7 8 9 10 11 12 13 |
num_cols = 3 num_rows = 4 prompt = ["a photograph of an astronaut riding a horse"] * num_cols all_images = [] for i in range(num_rows): with autocast("cuda"): images = pipe(prompt)["sample"] all_images.extend(images) grid = image_grid(all_images, rows=num_rows, cols=num_cols) grid |
正方形じゃない画像を生成
Stable Diffuse の生成する画像はデフォルトでは512×512の正方画像です。
これをheight
および width
引数を使用してデフォルトをオーバーライドして横長な画像を生成してみます。
ただし適切な画像サイズを選択するための推奨事項が3つあります。
1: 「高さ」と「幅」が両方とも「8」の倍数であること
2 : 512 を下回ると、画質が低下する可能性があること
3: 両方向に 512 を超えると、イメージ領域が繰り返されること (グローバル コヒーレンスが失われる)
したがって、正方形でない画像を作成する最善の方法は、一方の次元で「512」を使用し、もう一方の次元でそれより大きい値を使用すること…だそうです。
1 2 3 4 |
prompt = "a photograph of an astronaut riding a horse" with autocast("cuda"): image = pipe(prompt, height=512, width=768)["sample"][0] image |
以下のパラメータを使った場合はセッションが変わっても同じ画像が生成されました。
パイプラインにランダム シードを渡すことができます。 同じシードを使用するたびに、同じ画像結果が得られます。
seed値を変えれば、また別の画像が生成されます。
1 2 3 4 5 6 7 8 |
import torch generator = torch.Generator("cuda").manual_seed(1024) with autocast("cuda"): image = pipe(prompt, generator=generator)["sample"][0] image |
Colab を開いてImage-To-Image をやってみる
Image-To-Image もText-To-Image 同様にプロンプト・テキストを使うのですが違いは何かというと、Text-To-Imageのプロンプト・テキストは画像生成を直接指示するものなのに対して、Image-To-Imageのプロンプト・テキストはImage の変性を指示するということです。
やってみましょう。
Colab にアクセス
Google アカウントでログイン
GPUなどをチェック
1 |
!nvidia-smi |
セットアップ
1 2 3 4 |
! pip install transformers gradio scipy ftfy "ipywidgets>=7,<8" datasets ! git clone https://github.com/huggingface/diffusers.git ! pip install git+https://github.com/huggingface/diffusers.git %cd diffusers |
Hugging Faceにログイン
1 2 3 |
from huggingface_hub import notebook_login notebook_login() |
再度、上記のアクセストークンを使ってログインします。
必要なファイルを取得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import gradio as gr import torch from torch import autocast from diffusers import StableDiffusionPipeline, LMSDiscreteScheduler import requests from PIL import Image from io import BytesIO from IPython.display import clear_output ### lms = LMSDiscreteScheduler( beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear" ) pipe = StableDiffusionPipeline.from_pretrained( "CompVis/stable-diffusion-v1-4", scheduler=lms, revision="fp16", use_auth_token=True ).to("cuda") |
StableDiffusionImg2ImgPipelineのクラスを定義
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
import inspect import warnings from typing import List, Optional, Union import torch from torch import autocast from tqdm.auto import tqdm from diffusers import ( AutoencoderKL, DDIMScheduler, DiffusionPipeline, PNDMScheduler, UNet2DConditionModel, ) from diffusers.pipelines.stable_diffusion import StableDiffusionSafetyChecker from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer class StableDiffusionImg2ImgPipeline(DiffusionPipeline): def __init__( self, vae: AutoencoderKL, text_encoder: CLIPTextModel, tokenizer: CLIPTokenizer, unet: UNet2DConditionModel, scheduler: Union[DDIMScheduler, PNDMScheduler], safety_checker: StableDiffusionSafetyChecker, feature_extractor: CLIPFeatureExtractor, ): super().__init__() scheduler = scheduler.set_format("pt") self.register_modules( vae=vae, text_encoder=text_encoder, tokenizer=tokenizer, unet=unet, scheduler=scheduler, safety_checker=safety_checker, feature_extractor=feature_extractor, ) @torch.no_grad() def __call__( self, prompt: Union[str, List[str]], init_image: torch.FloatTensor, strength: float = 0.8, num_inference_steps: Optional[int] = 50, guidance_scale: Optional[float] = 7.5, eta: Optional[float] = 0.0, generator: Optional[torch.Generator] = None, output_type: Optional[str] = "pil", ): if isinstance(prompt, str): batch_size = 1 elif isinstance(prompt, list): batch_size = len(prompt) else: raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") if strength < 0 or strength > 1: raise ValueError(f'The value of strength should in [0.0, 1.0] but is {strength}') # set timesteps accepts_offset = "offset" in set(inspect.signature(self.scheduler.set_timesteps).parameters.keys()) extra_set_kwargs = {} offset = 0 if accepts_offset: offset = 1 extra_set_kwargs["offset"] = 1 self.scheduler.set_timesteps(num_inference_steps, **extra_set_kwargs) # encode the init image into latents and scale the latents init_latents = self.vae.encode(init_image.to(self.device)).sample() init_latents = 0.18215 * init_latents # prepare init_latents noise to latents init_latents = torch.cat([init_latents] * batch_size) # get the original timestep using init_timestep init_timestep = int(num_inference_steps * strength) + offset init_timestep = min(init_timestep, num_inference_steps) timesteps = self.scheduler.timesteps[-init_timestep] timesteps = torch.tensor([timesteps] * batch_size, dtype=torch.long, device=self.device) # add noise to latents using the timesteps noise = torch.randn(init_latents.shape, generator=generator, device=self.device) init_latents = self.scheduler.add_noise(init_latents, noise, timesteps) # get prompt text embeddings text_input = self.tokenizer( prompt, padding="max_length", max_length=self.tokenizer.model_max_length, truncation=True, return_tensors="pt", ) text_embeddings = self.text_encoder(text_input.input_ids.to(self.device))[0] # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` # corresponds to doing no classifier free guidance. do_classifier_free_guidance = guidance_scale > 1.0 # get unconditional embeddings for classifier free guidance if do_classifier_free_guidance: max_length = text_input.input_ids.shape[-1] uncond_input = self.tokenizer( [""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt" ) uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] # For classifier free guidance, we need to do two forward passes. # Here we concatenate the unconditional and text embeddings into a single batch # to avoid doing two forward passes text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 # and should be between [0, 1] accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) extra_step_kwargs = {} if accepts_eta: extra_step_kwargs["eta"] = eta latents = init_latents t_start = max(num_inference_steps - init_timestep + offset, 0) for i, t in tqdm(enumerate(self.scheduler.timesteps[t_start:])): # expand the latents if we are doing classifier free guidance latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents # predict the noise residual noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings)["sample"] # perform guidance if do_classifier_free_guidance: noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) # compute the previous noisy sample x_t -> x_t-1 latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs)["prev_sample"] # scale and decode the image latents with vae latents = 1 / 0.18215 * latents image = self.vae.decode(latents) image = (image / 2 + 0.5).clamp(0, 1) image = image.cpu().permute(0, 2, 3, 1).numpy() # run safety checker safety_cheker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(self.device) image, has_nsfw_concept = self.safety_checker(images=image, clip_input=safety_cheker_input.pixel_values) if output_type == "pil": image = self.numpy_to_pil(image) return {"sample": image, "nsfw_content_detected": has_nsfw_concept} |
1 2 3 4 5 6 |
pipeimg = StableDiffusionImg2ImgPipeline.from_pretrained( "CompVis/stable-diffusion-v1-4", revision="fp16", torch_dtype=torch.float16, use_auth_token=True ).to("cuda") |
preprocess 定義
1 2 3 4 5 6 7 8 9 10 11 12 |
import PIL from PIL import Image import numpy as np def preprocess(image): w, h = image.size w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 image = image.resize((w, h), resample=PIL.Image.LANCZOS) image = np.array(image).astype(np.float32) / 255.0 image = image[None].transpose(0, 3, 1, 2) image = torch.from_numpy(image) return 2.*image - 1. |
実行
debugがTrue になっているので、セルの左のアイコンは回転したままです。このままでも問題ないですが、気持ち悪いとお感じならFalseに変更してください。ただし、問題が起きても何も言ってくれません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
block = gr.Blocks(css=".container { max-width: 800px; margin: auto; }") num_samples = 2 def infer(prompt, init_image, strength): if init_image != None: init_image = init_image.resize((512, 512)) init_image = preprocess(init_image) with autocast("cuda"): images = pipeimg([prompt] * num_samples, init_image=init_image, strength=strength, guidance_scale=7.5)["sample"] else: with autocast("cuda"): images = pipe([prompt] * num_samples, guidance_scale=7.5)["sample"] return images with block as demo: gr.Markdown("<h1><center>Stable Diffusion</center></h1>") gr.Markdown( "Stable Diffusion is an AI model that generates images from any prompt you give!" ) with gr.Group(): with gr.Box(): with gr.Row().style(mobile_collapse=False, equal_height=True): text = gr.Textbox( label="Enter your prompt", show_label=False, max_lines=1 ).style( border=(True, False, True, True), rounded=(True, False, False, True), container=False, ) btn = gr.Button("Run").style( margin=False, rounded=(False, True, True, False), ) strength_slider = gr.Slider( label="Strength", maximum = 1, value = 0.75 ) image = gr.Image( label="Intial Image", type="pil" ) gallery = gr.Gallery(label="Generated images", show_label=False).style( grid=[2], height="auto" ) text.submit(infer, inputs=[text,image,strength_slider], outputs=gallery) btn.click(infer, inputs=[text,image,strength_slider], outputs=gallery) gr.Markdown( """___ <p style='text-align: center'> Created by CompVis and Stability AI <br/> </p>""" ) clear_output() ### demo.launch(debug=True) |
ImageTo-ImageではプロンプトのテキストでImage をどう処理するか指示します。
建物の写真をimageとして、プロンプトで人物をはめ込んで、映画の一場面のような画像を作ってみます。
朱色の領域にimegeをドロップ、赤色のフィールドにプロンプト・テキストを入れてRunします。
青色のスライダー(strength)はプロンプトテキストのImageに対する影響度です。
こんなimage を使ってみます(青山にあった同潤会アパートのモノクロ写真)
このImageを以下のテキストを使って加工します。
プロンプト
a lady by apartment house
こんな感じ
雰囲気のみ残して、別の場面になっています。そして、この写真のすごいところはよく見ると細部は破綻しているのに、全体の整合性はとれているというところです。
ポートレートを使って、スタイルを変えてみます。
モジリアニ風に描かれた柴犬に加工。
左のイメージの背景が非常に気になります。深層学習モデルはなんでこんな背景を描いたんでしょう?
また、このimg2imgを使えば簡単なラフ画像から、詳細なグラフィックを生成することもできるそうです。
手順を参考にするとこういうことが可能でした。
こんな画像をベースにします。
img2imgで画像の加工を2〜3回繰り返します。
こんな画像が生成できました。少々驚きです。
Appendix2
Image-To-Image ではIn-Painting も実行できます。
In-Painting というのは画像内のある領域を修正・修復するものです。
例えばこんな感じ。
これは、わんちゃんの領域のマスク画像を作っておいて、プロンプトでそこに猫を座らせています。
マスク画像はこんな感じですが、矩形で切り取ってもいいようです。
OR
上記、Image-To-ImageのHugging Face へのログイン後に以下を実行
コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
from io import BytesIO from torch import autocast import torch import requests import PIL from diffusers import StableDiffusionInpaintPipeline def download_image(url): response = requests.get(url) return PIL.Image.open(BytesIO(response.content)).convert("RGB") img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" init_image = download_image(img_url).resize((512, 512)) mask_image = download_image(mask_url).resize((512, 512)) device = "cuda" model_id_or_path = "CompVis/stable-diffusion-v1-4" pipe = StableDiffusionInpaintPipeline.from_pretrained( model_id_or_path, revision="fp16", torch_dtype=torch.float16, use_auth_token=True ) # or download via git clone https://huggingface.co/CompVis/stable-diffusion-v1-4 # and pass `model_id_or_path="./stable-diffusion-v1-4"` without having to use `use_auth_token=True`. pipe = pipe.to(device) prompt = "a cat sitting on a bench" with autocast("cuda"): images = pipe(prompt=prompt, init_image=init_image, mask_image=mask_image, strength=0.75)["sample"] images[0] |
わんちゃんの代わりに少女を座らせてみます。
1 2 3 4 5 6 |
prompt = "a girl sitting on a bench" with autocast("cuda"): images = pipe(prompt=prompt, init_image=init_image, mask_image=mask_image, strength=0.75)["sample"] images[0] |
後ろ向いてますね。
Leave a Reply