stable_diffusion
配置
!pip install -Uq diffusers ftfy accelerate
# Installing transformers from source for now since we need the latest version for Depth2Img:
!pip install -Uq git+https://github.com/huggingface/transformers
import torch
import requests
from PIL import Image
from io import BytesIO
from matplotlib import pyplot as plt
# We'll be exploring a number of pipelines today!
from diffusers import (
StableDiffusionPipeline,
StableDiffusionImg2ImgPipeline,
StableDiffusionInpaintPipeline,
StableDiffusionDepth2ImgPipeline
)
# We'll use a couple of demo images later in the notebook
def download_image(url):
response = requests.get(url)
return Image.open(BytesIO(response.content)).convert("RGB")
# Download images for inpainting example
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))
# Set device
device = (
"mps"
if torch.backends.mps.is_available()
else "cuda"
if torch.cuda.is_available()
else "cpu"
)
从文本生成图像
我们先载入 Stable Diffusion 的管线,看看我们能做点什么。现有的 Stable Diffusion 模型有好多不同版本,截至本文书写时的最新版本是第2.1版。如果你想探究更旧的版本,只需要在 model_id
处修改即可(比如你可以试试将其改成 CompVis/stable-diffusion-v1-4
或从dreambooth concepts library选一个模型)。
# Load the pipeline
model_id = "stabilityai/stable-diffusion-2-1-base"
pipe = StableDiffusionPipeline.from_pretrained(model_id).to(device)
pipe.enable_attention_slicing()
unet\diffusion_pytorch_model.safetensors not found
Loading pipeline components...: 0%| | 0/6 [00:00<?, ?it/s]
如果你的GPU内存不够用,这里有些办法也许可以减少内存使用:
-
载入 FP16 精度的版本(但并不是所有的系统上都支持)。与此同时,在你对管线的某个特定部分实验时,你也需要把所有的张量换成 torch.float16 精度:
pipe = StableDiffusionPipeline.from_pretrained(model_id, revision="fp16", torch_dtype=torch.float16).to(device)
-
开启注意力机制切分(attention slicing)。这会牺牲一点点速度来减少GPU内存的使用:
pipe.enable_attention_slicing()
- 降低要生成的图片的尺寸
当管线加载好了以后,我们可以用以下代码去使用文字提示生成图片:
# Set up a generator for reproducibility
generator = torch.Generator(device=device).manual_seed(42)
# Run the pipeline, showing some of the available arguments
pipe_output = pipe(
prompt="What to generate Palette knife painting of an autumn cityscape",
negative_prompt="Oversaturated, blurry, low quality", # What NOT to generate
height=480, width=640, # Specify the image size
guidance_scale=8, # How strongly to follow the prompt
num_inference_steps=35, # How many steps to take
generator=generator # Fixed random seed
)
# View the resulting image:
pipe_output.images[0]
0%| | 0/35 [00:00<?, ?it/s]
练习: 花点时间去用上面的代码块实验,使用你自己的文字提示,反复调整各项设置看看这些设置如何影响生成效果。使用不同的随机种子或者移除掉 generator
这个输入参数,看看能获取什么不同的效果。
主要的要调节参数介绍:
width
和height
指定了生成图片的尺寸。它们必须是可被 8 整除的数字,只有这样我们的可变分自编码器(VAE)才能正常工作(我们在将来的章节会了解到)。- 步数
num_inference_steps
也会影响生成的质量。默认设成 50 已经很好了,但有些时候你也可以用少到像 20 步这样,这对做实验就方便多了。 - 使用
negative_prompt
来强调不希望生成的内容,一般会在无分类器引导(classifier-free guidance)的过程中用到,这可以是个非常有用的添加额外控制的方式。你可以留空这个地方不管,但很多用户觉得列出一些不想要的特性对更好的生成很有帮助。 guidance_scale
这个参数决定了无分类器引导(CFG)的影响强度有多大。增大这个值会使得生成的内容更接近文字提示;但这个值如果过大,可能会使得结果变得过饱和、不好看。
如果你想为文字提示找点灵感,你也可以从这里开始:Stable Diffusion Prompt Book
你可在下面看到增大guidance_scale
这个参数所带来的作用:
#@markdown comparing guidance scales:
cfg_scales = [1.1, 8, 12] #@param
prompt = "A collie with a pink hat" #@param
fig, axs = plt.subplots(1, len(cfg_scales), figsize=(16, 5))
for i, ax in enumerate(axs):
im = pipe(prompt, height=480, width=480,
guidance_scale=cfg_scales[i], num_inference_steps=35,
generator=torch.Generator(device=device).manual_seed(42)).images[0]
ax.imshow(im); ax.set_title(f'CFG Scale {cfg_scales[i]}');
0%| | 0/35 [00:00<?, ?it/s]
0%| | 0/35 [00:00<?, ?it/s]
0%| | 0/35 [00:00<?, ?it/s]
调一调上面的值,尝试不同的幅度和提示。当然了,如何解读这些参数是个很主观的事情,但对我来说,我觉得 8 到 12 这个值区间比其它情况产出的结果都好。
管线的组成部分
这里我们用的 StableDiffusionPipeline
比前面几个单元的 DDPMPipeline
要复杂一点。除了 UNet 和调度器之外,管线内还有很多其它的组成部分:
print(list(pipe.components.keys())) # List components
['vae', 'text_encoder', 'tokenizer', 'unet', 'scheduler', 'safety_checker', 'feature_extractor']
为了更好地理解管线如何工作,我们简要地一个一个看看各个组成部分,然后我们自己动手,把它们组合在一起,以此复现出整个管线功能。
可变分自编码器(VAE)
可变分自编码器(VAE)是一种模型,它可以将输入编码成一种被压缩过的表示形式,再把这个“隐式的”表示形式解码成某种接近输入的输出。当我们使用 Stable Diffusion 生成图片时,我们先在VAE的“隐空间”应用扩散过程生成隐编码,然后在结尾对它们解码来查看结果图片。
这里就是一个例子,使用VAE把输入图片编码成隐式的表示形式,再对它解码:
# Create some fake data (a random image, range (-1, 1))
images = torch.rand(1, 3, 512, 512).to(device) * 2 - 1
print("Input images shape:", images.shape)
# Encode to latent space
with torch.no_grad():
latents = 0.18215 * pipe.vae.encode(images).latent_dist.mean
print("Encoded latents shape:", latents.shape)
# Decode again
with torch.no_grad():
decoded_images = pipe.vae.decode(latents / 0.18215).sample
print("Decoded images shape:", decoded_images.shape)
Input images shape: torch.Size([1, 3, 512, 512])
Encoded latents shape: torch.Size([1, 4, 64, 64])
Decoded images shape: torch.Size([1, 3, 512, 512])
如你所见,原本 512x512 尺寸的图片被压缩到 64x64的隐式表示形式(有四个通道)中。每一空间维度上都被压缩到了原有的八分之一,这也是为什么我们设定 width
和 height
时需要它们是 8 的倍数。
使用这些信息量充裕的 4x64x64 隐编码可比使用 512px 大小的图片要高效多了,可以让我们的扩散模型更快,并使用更少的资源来训练和使用。VAE的解码过程并不是完美的,但即使损失了一点点质量,总的来说也足够好了。
注意:上面的代码例子包含了一个值为 0.18215 的缩放因子,以此来适配stable diffusion训练时的处理流程。
分词器(Tokenizer)和文本编码器(Text Encoder)
文本编码器的作用是将输入的字符串(文本提示)转化成数值表示形式,这样才能输入进 UNet 作为条件。文本首先要被管线中的分词器(tokenizer)转换成一系列的分词(token)。文本编码器有大约五万分词的词汇量 —— 任何不存在于这些词汇量中的词语都会被细分成多个更小的词语。这些分词然后就被送入文本编码器模型中 —— 文本编码器是一个transformer模型,最初被训练作为CLIP的文本编码器。这里我们希望这个经过了预训练的transformer模型学习到了足够好的文本表示能力,可以对我们这里的扩散任务一样有用。
我们这里通过对一个文字提示进行编码,验证一下这个过程。首先,我们手动进行分词,并将它输入到文本编码器中,再使用管线的 _encode_prompt
方法,观察一下完成的过程,这包括补全或截断分词串的长度,使得分词串的长度等于最大长度 77 :
# Tokenizing and encoding an example prompt manualy:
# Tokenize
input_ids = pipe.tokenizer(["A painting of a flooble"])['input_ids']
print("Input ID -> decoded token")
for input_id in input_ids[0]:
print(f"{input_id} -> {pipe.tokenizer.decode(input_id)}")
# Feed through CLIP text encoder
input_ids = torch.tensor(input_ids).to(device)
with torch.no_grad():
text_embeddings = pipe.text_encoder(input_ids)['last_hidden_state']
print("Text embeddings shape:", text_embeddings.shape)
Input ID -> decoded token
49406 -> <|startoftext|>
320 -> a
3086 -> painting
539 -> of
320 -> a
4062 -> floo
1059 -> ble
49407 -> <|endoftext|>
Text embeddings shape: torch.Size([1, 8, 1024])
# Get the final text embeddings using the pipeline's _encode_prompt function:
text_embeddings = pipe._encode_prompt("A painting of a flooble", device, 1, False, '')
text_embeddings.shape
F:\software\Anaconda\envs\test\lib\site-packages\diffusers\pipelines\stable_diffusion\pipeline_stable_diffusion.py:237: FutureWarning: `_encode_prompt()` is deprecated and it will be removed in a future version. Use `encode_prompt()` instead. Also, be aware that the output format changed from a concatenated tensor to a tuple.
deprecate("_encode_prompt()", "1.0.0", deprecation_message, standard_warn=False)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 2
1 # Get the final text embeddings using the pipeline's _encode_prompt function:
----> 2 text_embeddings = pipe._encode_prompt("A painting of a flooble", device, 1, False, '')
3 text_embeddings.shape
File F:\software\Anaconda\envs\test\lib\site-packages\diffusers\pipelines\stable_diffusion\pipeline_stable_diffusion.py:251, in StableDiffusionPipeline._encode_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt, prompt_embeds, negative_prompt_embeds, lora_scale)
239 prompt_embeds_tuple = self.encode_prompt(
240 prompt=prompt,
241 device=device,
(...)
247 lora_scale=lora_scale,
248 )
250 # concatenate for backwards comp
--> 251 prompt_embeds = torch.cat([prompt_embeds_tuple[1], prompt_embeds_tuple[0]])
253 return prompt_embeds
TypeError: expected Tensor as element 0 in argument 0, but got NoneType
这些文本嵌入(text embedding),也即文本编码器中最后一个transformer模块的“隐状态(hidden state)”,将会被送入 UNet 中作为 forward
函数的一个额外输入,下面部分我们会详细看到。
UNet
UNet 模型接收一个带噪的输入,并预测噪声,和我们之前单元中看到的 UNet 一样。但与以往例子不同的是,这里的输入并不是图片了,而是图片的隐式表示形式(latent representation)。此外,除了把用于暗示带噪程度的timestep输入进 UNet 作为条件外,这里模型也把文字提示(prompt)的文本嵌入(text embeddings)作为了额外输入。这里我们假数据试着让它预测一下:
# Dummy inputs:
timestep = pipe.scheduler.timesteps[0]
latents = torch.randn(1, 4, 64, 64).to(device)
text_embeddings = torch.randn(1, 77, 1024).to(device)
# Model prediction:
with torch.no_grad():
unet_output = pipe.unet(latents, timestep, text_embeddings).sample
print('UNet output shape:', unet_output.shape) # Same shape as the input latents
调度器(Scheduler)
调度器保存了如何加噪的计划安排,管理着如何基于模型的预测更新带噪样本。默认的调度器是 PNDMScheduler
调度器,但你也可以用其它的(比如 LMSDiscreteScheduler
调度器),只要它们用相同的配置初始化。
我们可以画出图像来观察随着timestep添加噪声的计划安排,看看不同时间的噪声水平(基于 α ˉ \bar{\alpha} αˉ这个参数)是什么样的:
plt.plot(pipe.scheduler.alphas_cumprod, label=r'$\bar{\alpha}$')
plt.xlabel('Timestep (high noise to low noise ->)');
plt.title('Noise schedule');plt.legend();
pip install scipy
Looking in indexes: http://mirrors.aliyun.com/pypi/simple/
Collecting scipy
Downloading http://mirrors.aliyun.com/pypi/packages/32/8e/7f403535ddf826348c9b8417791e28712019962f7e90ff845896d6325d09/scipy-1.10.1-cp38-cp38-win_amd64.whl (42.2 MB)
Requirement already satisfied: numpy<1.27.0,>=1.19.5 in f:\software\anaconda\envs\test\lib\site-packages (from scipy) (1.23.0)
Installing collected packages: scipy
Successfully installed scipy-1.10.1
Note: you may need to restart the kernel to use updated packages.
如果你想尝试不同的调度器,你可以像下面代码中一样换一个新的:
from diffusers import LMSDiscreteScheduler
# Replace the scheduler
pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config)
# Print the config
print('Scheduler config:', pipe.scheduler)
# Generate an image with this new scheduler
pipe(prompt="Palette knife painting of an winter cityscape", height=480, width=480,
generator=torch.Generator(device=device).manual_seed(42)).images[0]
Scheduler config: LMSDiscreteScheduler {
"_class_name": "LMSDiscreteScheduler",
"_diffusers_version": "0.21.4",
"beta_end": 0.012,
"beta_schedule": "scaled_linear",
"beta_start": 0.00085,
"clip_sample": false,
"num_train_timesteps": 1000,
"prediction_type": "epsilon",
"set_alpha_to_one": false,
"skip_prk_steps": true,
"steps_offset": 1,
"timestep_spacing": "linspace",
"trained_betas": null,
"use_karras_sigmas": false
}
0%| | 0/50 [00:00<?, ?it/s]
在这里你可以了解更多使用不同调度器的信息。
DIY一个采样循环
现在我们已经一个个看过这些组成部分了,我们可以把它们拼装到一起,来复现一下整个管线的功能:
guidance_scale = 8 #@param
num_inference_steps=30 #@param
prompt = "Beautiful picture of a wave breaking" #@param
negative_prompt = "zoomed in, blurry, oversaturated, warped" #@param
# Encode the prompt
text_embeddings = pipe._encode_prompt(prompt, device, 1, True, negative_prompt)
# Create our random starting point
latents = torch.randn((1, 4, 64, 64), device=device, generator=generator)
latents *= pipe.scheduler.init_noise_sigma
# Prepare the scheduler
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Loop through the sampling timesteps
for i, t in enumerate(pipe.scheduler.timesteps):
# expand the latents if we are doing classifier free guidance
latent_model_input = torch.cat([latents] * 2)
# Apply any scaling required by the scheduler
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# predict the noise residual with the unet
with torch.no_grad():
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# perform 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 = pipe.scheduler.step(noise_pred, t, latents).prev_sample
# Decode the resulting latents into an image
with torch.no_grad():
image = pipe.decode_latents(latents.detach())
# View
pipe.numpy_to_pil(image)[0]
F:\software\Anaconda\envs\test\lib\site-packages\diffusers\pipelines\stable_diffusion\pipeline_stable_diffusion.py:430: FutureWarning: The decode_latents method is deprecated and will be removed in 1.0.0. Please use VaeImageProcessor.postprocess(...) instead
deprecate("decode_latents", "1.0.0", deprecation_message, standard_warn=False)
其它的一些管线
Img2Img
直到现在,我们生成的图片还都是完全从随机的隐变量来开始生成的,而且也都使用了完整的扩散模型采样循环。但其实我们不必从头开始。Img2Img 这个管线首先将一张已有的图片进行编码,编码成一系列的隐变量,然后在这些隐变量上随机加噪声,以这些作为起始点。噪声加多大量、去噪需要的步数决定了这个 img2img 过程的“强度”。只加一点点噪声(强度低)只会带来微小改变,而加入最大量的噪声并跑完完整的去噪过程又会生成出几乎完全不像原始图片的结果,即使可能在整体结构上还多少有点相似。
这个管线无需什么特殊模型,只要模型的 ID 和我们的文字到图像模型一样就行,没有新的需要下载的文件。
# Loading an Img2Img pipeline
model_id = "stabilityai/stable-diffusion-2-1-base"
img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id).to(device)
unet\diffusion_pytorch_model.safetensors not found
Loading pipeline components...: 0%| | 0/6 [00:00<?, ?it/s]
在本节的“配置”部分,我们加载了一个名为 init_image
的图片来用于这里的演示,当然你也可以用你自己的图片替换它。这里是使用该管线的代码:
# Apply Img2Img
result_image = img2img_pipe(
prompt="An oil painting of a man on a bench",
image = init_image, # The starting image
strength = 0.6, # 0 for no change, 1.0 for max strength
).images[0]
# View the result
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(result_image);axs[1].set_title('Result');
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
RuntimeError: CUDA out of memory. Tried to allocate 128.00 MiB (GPU 0; 4.00 GiB total capacity; 9.90 GiB already allocated; 0 bytes free; 10.01 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation. See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF
练习: 用这个管线做实验,试试你自己的图片,或者试试不同的强度和文字提示。你可以使用很多和文字到图片管线相同的输入参数。所以尽可能试试不同的图片尺寸、生成步数吧。
In-Painting
如果我们想在一张图中保留一部分不变而在其它部分生成新东西,那该怎么办呢?这种技术叫 inpainting 。虽然我们可以通过前面演示中的同一个模型(用 StableDiffusionInpaintPipelineLegacy
管线)来实现,但我们这里可以用一个自定义的微调版 Stable Diffusion 模型来得到更好的效果。这里的 Stable Diffusion 模型接收一个掩模(mask)作为额外条件性输入。这个掩模图片需要和输入图片尺寸一致,白色区域表示要被替换的部分,黑色区域表示要保留的部分。以下代码就展示了我们如何载入这个管线并如何应用到前面载入的示例图片和掩模上:
# Load the inpainting pipeline (requires a suitable inpainting model)
pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting")
pipe = pipe.to(device)
Downloading (…)ain/model_index.json: 0%| | 0.00/548 [00:00<?, ?B/s]
F:\software\Anaconda\envs\test\lib\site-packages\huggingface_hub\file_download.py:137: UserWarning: `huggingface_hub` cache-system uses symlinks by default to efficiently store duplicated files but your machine does not support them in C:\Users\11637\.cache\huggingface\hub. Caching files will still work but in a degraded version that might require more space on your disk. This warning can be disabled by setting the `HF_HUB_DISABLE_SYMLINKS_WARNING` environment variable. For more details, see https://huggingface.co/docs/huggingface_hub/how-to-cache#limitations.
To support symlinks on Windows, you either need to activate Developer Mode or to run Python as an administrator. In order to see activate developer mode, see this article: https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
warnings.warn(message)
unet\diffusion_pytorch_model.safetensors not found
Fetching 16 files: 0%| | 0/16 [00:00<?, ?it/s]
Downloading pytorch_model.bin: 0%| | 0.00/1.22G [00:00<?, ?B/s]
Downloading (…)_checker/config.json: 0%| | 0.00/4.78k [00:00<?, ?B/s]
Downloading pytorch_model.bin: 0%| | 0.00/492M [00:00<?, ?B/s]
Downloading (…)tokenizer/merges.txt: 0%| | 0.00/525k [00:00<?, ?B/s]
Downloading (…)cial_tokens_map.json: 0%| | 0.00/472 [00:00<?, ?B/s]
Downloading (…)cheduler_config.json: 0%| | 0.00/313 [00:00<?, ?B/s]
Downloading (…)okenizer_config.json: 0%| | 0.00/806 [00:00<?, ?B/s]
Downloading (…)tokenizer/vocab.json: 0%| | 0.00/1.06M [00:00<?, ?B/s]
Downloading (…)rocessor_config.json: 0%| | 0.00/342 [00:00<?, ?B/s]
Downloading (…)49d/unet/config.json: 0%| | 0.00/748 [00:00<?, ?B/s]
Downloading (…)_encoder/config.json: 0%| | 0.00/617 [00:00<?, ?B/s]
Downloading (…)b49d/vae/config.json: 0%| | 0.00/552 [00:00<?, ?B/s]
Downloading (…)on_pytorch_model.bin: 0%| | 0.00/335M [00:00<?, ?B/s]
ProxyError: (MaxRetryError("HTTPSConnectionPool(host='huggingface.co', port=443): Max retries exceeded with url: /runwayml/stable-diffusion-inpainting/resolve/51388a731f57604945fddd703ecb5c50e8e7b49d/config.json (Caused by ProxyError('Cannot connect to proxy.', timeout('_ssl.c:1114: The handshake operation timed out')))"), '(Request ID: fb3b77aa-4c46-4dd6-9245-5a2f602160e4)')
# Inpaint with a prompt for what we want the result to look like
prompt = "A small robot, high resolution, sitting on a park bench"
image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0]
# View the result
fig, axs = plt.subplots(1, 3, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(mask_image);axs[1].set_title('Mask')
axs[2].imshow(image);axs[2].set_title('Result');
当和其它可以自动生成掩模的模型结合起来的时候,这个模型将会相当强大。比如这个示例space就用了一个名为 CLIPSeg 的模型,它可以根据文字描述自动地用掩模去掉一个物体。
题外话:管理你的模型缓存
探索不同的管线和模型可能会占满你的硬盘空间。你可用这个指令看看你都下载了哪些模型到你的硬盘上:
!ls ~/.cache/huggingface/diffusers/ # List the contents of the cache directory
models--CompVis--stable-diffusion-v1-4
models--ddpm-bedroom-256
models--google--ddpm-bedroom-256
models--google--ddpm-celebahq-256
models--runwayml--stable-diffusion-inpainting
models--stabilityai--stable-diffusion-2-1-base
看看缓存相关文档来了解如何高效地查看和管理缓存。
Depth2Image
Input image, depth image and generated examples (image source: StabilityAI)
Img2Img 已经很厉害了,但有时我们还想用原始图片的组成成分但使用完全不同的颜色或纹理来生成新图片。通过调节 Img2Img 的“强度”来保留图片整体结构但却不保留原有颜色将会很困难。
所以这里就需要另一个微调的模型了!这个模型需要输入额外的深度信息作为生成条件。相关管线使用了一个深度预测模型来预测出一个深度图,然后这个深度图会被输入微调过的 UNet 中用以生成图片。我们这里希望生成的图片能够保留原始图片的深度信息和总体结构,同时又在相关部分填入全新的内容。
# Load the Depth2Img pipeline (requires a suitable model)
pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth")
pipe = pipe.to(device)
# Inpaint with a prompt for what we want the result to look like
prompt = "An oil painting of a man on a bench"
image = pipe(prompt=prompt, image=init_image).images[0]
# View the result
fig, axs = plt.subplots(1, 2, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(image);axs[1].set_title('Result');
0%| | 0/40 [00:00<?, ?it/s]