SAHILCBR/GLAMAX-virtual-try-on
0
1import os
2
3import math
4import PIL
5import numpy as np
6import torch
7from PIL import Image
8from accelerate.state import AcceleratorState
9from packaging import version
10import accelerate
11from typing import List, Optional, Tuple
12from torch.nn import functional as F
13from diffusers import UNet2DConditionModel, SchedulerMixin
14
15# Compute DREAM and update latents for diffusion sampling
16def compute_dream_and_update_latents_for_inpaint(
17 unet: UNet2DConditionModel,
18 noise_scheduler: SchedulerMixin,
19 timesteps: torch.Tensor,
20 noise: torch.Tensor,
21
22 encoder_hidden_states: torch.Tensor,
23 dream_detail_preservation: float = 1.0,
24) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor]]:
25 """
26 Implements "DREAM (Diffusion Rectification and Estimation-Adaptive Models)" from http://arxiv.org/abs/2312.00210.
27 DREAM helps align training with sampling to help training be more efficient and accurate at the cost of an extra
28 forward step without gradients.
29
30 Args:
31 `unet`: The state unet to use to make a prediction.
32 `noise_scheduler`: The noise scheduler used to add noise for the given timestep.
33 `timesteps`: The timesteps for the noise_scheduler to user.
34 `noise`: A tensor of noise in the shape of noisy_latents.
35 `noisy_latents`: Previously noise latents from the training loop.
36 `target`: The ground-truth tensor to predict after eps is removed.
37 `encoder_hidden_states`: Text embeddings from the text model.
38 `dream_detail_preservation`: A float value that indicates detail preservation level.
39 See reference.
40
41 Returns:
42 `tuple[torch.Tensor, torch.Tensor]`: Adjusted noisy_latents and target.
43 """
44 alphas_cumprod = noise_scheduler.alphas_cumprod.to(timesteps.device)[timesteps, None, None, None]
45 sqrt_one_minus_alphas_cumprod = (1.0 - alphas_cumprod) ** 0.5
46
47 # The paper uses lambda = sqrt(1 - alpha) ** p, with p = 1 in their experiments.
48 dream_lambda = sqrt_one_minus_alphas_cumprod**dream_detail_preservation
49
50 pred = None # b, 4, h, w
51 with torch.no_grad():
52 pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample
53
54 noisy_latents_no_condition = noisy_latents[:, :4]
55 _noisy_latents, _target = (None, None)
56 if noise_scheduler.config.prediction_type == "epsilon":
57 predicted_noise = pred
58 delta_noise = (noise - predicted_noise).detach()
59 delta_noise.mul_(dream_lambda)
60 _noisy_latents = noisy_latents_no_condition.add(sqrt_one_minus_alphas_cumprod * delta_noise)
61 _target = target.add(delta_noise)
62 elif noise_scheduler.config.prediction_type == "v_prediction":
63 raise NotImplementedError("DREAM has not been implemented for v-prediction")
64 else:
65 raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}")
66
67 _noisy_latents = torch.cat([_noisy_latents, noisy_latents[:, 4:]], dim=1)
68 return _noisy_latents, _target
69
70# Prepare the input for inpainting model.
71def prepare_inpainting_input(
72 noisy_latents: torch.Tensor,
73 mask_latents: torch.Tensor,
74 condition_latents: torch.Tensor,
75 enable_condition_noise: bool = True,
76 condition_concat_dim: int = -1,
77) -> torch.Tensor:
78 """
79 Prepare the input for inpainting model.
80
81 Args:
82 noisy_latents (torch.Tensor): Noisy latents.
83 mask_latents (torch.Tensor): Mask latents.
84 condition_latents (torch.Tensor): Condition latents.
85 enable_condition_noise (bool): Enable condition noise.
86
87 Returns:
88 torch.Tensor: Inpainting input.
89 """
90 if not enable_condition_noise:
91 condition_latents_ = condition_latents.chunk(2, dim=condition_concat_dim)[-1]
92 noisy_latents = torch.cat([noisy_latents, condition_latents_], dim=condition_concat_dim)
93 noisy_latents = torch.cat([noisy_latents, mask_latents, condition_latents], dim=1)
94 return noisy_latents
95
96# Compute VAE encodings
97def compute_vae_encodings(image: torch.Tensor, vae: torch.nn.Module) -> torch.Tensor:
98 """
99 Args:
100 images (torch.Tensor): image to be encoded
101 vae (torch.nn.Module): vae model
102
103 Returns:
104 torch.Tensor: latent encoding of the image
105 """
106 pixel_values = image.to(memory_format=torch.contiguous_format).float()
107 pixel_values = pixel_values.to(vae.device, dtype=vae.dtype)
108 with torch.no_grad():
109 model_input = vae.encode(pixel_values).latent_dist.sample()
110 model_input = model_input * vae.config.scaling_factor
111 return model_input
112
113
114# Init Accelerator
115from accelerate import Accelerator, DistributedDataParallelKwargs
116from accelerate.utils import ProjectConfiguration
117
118def init_accelerator(config):
119 accelerator_project_config = ProjectConfiguration(
120 project_dir=config.project_name,
121 logging_dir=os.path.join(config.project_name, "logs"),
122 )
123 accelerator_ddp_config = DistributedDataParallelKwargs(find_unused_parameters=True)
124 accelerator = Accelerator(
125 mixed_precision=config.mixed_precision,
126 log_with=config.report_to,
127 project_config=accelerator_project_config,
128 kwargs_handlers=[accelerator_ddp_config],
129 gradient_accumulation_steps=config.gradient_accumulation_steps,
130 )
131 # Disable AMP for MPS.
132 if torch.backends.mps.is_available():
133 accelerator.native_amp = False
134
135 if accelerator.is_main_process:
136 accelerator.init_trackers(
137 project_name=config.project_name,
138 config={
139 "learning_rate": config.learning_rate,
140 "train_batch_size": config.train_batch_size,
141 "image_size": f"{config.width}x{config.height}",
142 },
143 )
144
145 return accelerator
146
147
148def init_weight_dtype(wight_dtype):
149 return {
150 "no": torch.float32,
151 "fp16": torch.float16,
152 "bf16": torch.bfloat16,
153 }[wight_dtype]
154
155
156def init_add_item_id(config):
157 return torch.tensor(
158 [
159 config.height,
160 config.width * 2,
161 0,
162 0,
163 config.height,
164 config.width * 2,
165 ]
166 ).repeat(config.train_batch_size, 1)
167
168
169def prepare_eval_data(dataset_root, dataset_name, is_pair=True):
170 assert dataset_name in ["vitonhd", "dresscode", "farfetch"], "Unknown dataset name {}.".format(dataset_name)
171 if dataset_name == "vitonhd":
172 data_root = os.path.join(dataset_root, "VITONHD-1024", "test")
173 if is_pair:
174 keys = os.listdir(os.path.join(data_root, "Images"))
175 cloth_image_paths = [
176 os.path.join(data_root, "Images", key, key + "-0.jpg") for key in keys
177 ]
178 person_image_paths = [
179 os.path.join(data_root, "Images", key, key + "-1.jpg") for key in keys
180 ]
181 else:
182 # read ../test_pairs.txt
183 cloth_image_paths = []
184 person_image_paths = []
185 with open(
186 os.path.join(dataset_root, "VITONHD-1024", "test_pairs.txt"), "r"
187 ) as f:
188 lines = f.readlines()
189 for line in lines:
190 cloth_image, person_image = (
191 line.replace(".jpg", "").strip().split(" ")
192 )
193 cloth_image_paths.append(
194 os.path.join(
195 data_root, "Images", cloth_image, cloth_image + "-0.jpg"
196 )
197 )
198 person_image_paths.append(
199 os.path.join(
200 data_root, "Images", person_image, person_image + "-1.jpg"
201 )
202 )
203 elif dataset_name == "dresscode":
204 data_root = os.path.join(dataset_root, "DressCode-1024")
205 if is_pair:
206 part = ["lower", "lower", "upper", "upper", "dresses", "dresses"]
207 ids = ["013581", "051685", "000190", "050072", "020829", "053742"]
208 cloth_image_paths = [
209 os.path.join(data_root, "Images", part[i], ids[i], ids[i] + "_1.jpg")
210 for i in range(len(part))
211 ]
212 person_image_paths = [
213 os.path.join(data_root, "Images", part[i], ids[i], ids[i] + "_0.jpg")
214 for i in range(len(part))
215 ]
216 else:
217 raise ValueError("DressCode dataset does not support non-pair evaluation.")
218 elif dataset_name == "farfetch":
219 data_root = os.path.join(dataset_root, "FARFETCH-1024")
220 cloth_image_paths = [
221 # TryOn
222 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Blouses/13732751/13732751-2.jpg",
223 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Hoodies/14661627/14661627-4.jpg",
224 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Vests & Tank Tops/16532697/16532697-4.jpg",
225 "Images/men/Pants/Loose Fit Pants/14750720/14750720-6.jpg",
226 # Garment Transfer
227 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Shirts/10889688/10889688-3.jpg",
228 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Shorts/Leather & Faux Leather Shorts/20143338/20143338-1.jpg",
229 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Jackets/Blazers/15541224/15541224-2.jpg",
230 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/men/Polo Shirts/Polo Shirts/17652415/17652415-0.jpg"
231
232 # "Images/men/Jackets/Hooded Jackets/12550261/12550261-1.jpg",
233 # "Images/men/Shirts/Shirts/15614589/15614589-4.jpg",
234 # "Images/women/Dresses/Day Dresses/10372515/10372515-3.jpg",
235 # "Images/women/Dresses/Sundresses/18520992/18520992-4.jpg",
236 # "Images/women/Skirts/Asymmetric & Draped Skirts/12404908/12404908-2.jpg",
237 ]
238 person_image_paths = [
239 # TryOn
240 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Blouses/13732751/13732751-0.jpg",
241 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Hoodies/14661627/14661627-2.jpg",
242 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Vests & Tank Tops/16532697/16532697-1.jpg",
243 "Images/men/Pants/Loose Fit Pants/14750720/14750720-5.jpg",
244 # Garment Transfer
245 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Tops/Shirts/10889688/10889688-1.jpg",
246 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Shorts/Leather & Faux Leather Shorts/20143338/20143338-2.jpg",
247 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/women/Jackets/Blazers/15541224/15541224-0.jpg",
248 "/home/chongzheng/Projects/hivton/Datasets/FARFETCH-1024/Images/men/Polo Shirts/Polo Shirts/17652415/17652415-4.jpg",
249
250 # "Images/men/Jackets/Hooded Jackets/12550261/12550261-3.jpg",
251 # "Images/men/Shirts/Shirts/15614589/15614589-3.jpg",
252 # "Images/women/Dresses/Day Dresses/10372515/10372515-0.jpg",
253 # "Images/women/Dresses/Sundresses/18520992/18520992-1.jpg",
254 # "Images/women/Skirts/Asymmetric & Draped Skirts/12404908/12404908-1.jpg",
255 ]
256 cloth_image_paths = [
257 os.path.join(data_root, path) for path in cloth_image_paths
258 ]
259 person_image_paths = [
260 os.path.join(data_root, path) for path in person_image_paths
261 ]
262 else:
263 raise ValueError(f"Unknown dataset name: {dataset_name}")
264
265 samples = [
266 {
267 "folder": os.path.basename(os.path.dirname(cloth_image)),
268 "cloth": cloth_image,
269 "person": person_image,
270 }
271 for cloth_image, person_image in zip(
272 cloth_image_paths, person_image_paths
273 )
274 ]
275 return samples
276
277
278def repaint_result(result, person_image, mask_image):
279 result, person, mask = np.array(result), np.array(person_image), np.array(mask_image)
280 # expand the mask to 3 channels & to 0~1
281 mask = np.expand_dims(mask, axis=2)
282 mask = mask / 255.0
283 # mask for result, ~mask for person
284 result_ = result * mask + person * (1 - mask)
285 return Image.fromarray(result_.astype(np.uint8))
286
287
288# 多通道 Sobel 算子处理 (用于获取模特图像的损失注意力图)
289def sobel(batch_image, mask=None, scale=4.0):
290 """
291 计算输入批量图像的Sobel梯度.
292
293 batch_image: 输入的批量图像张量,大小为 [batch, channels, height, width]
294 """
295 w, h = batch_image.size(3), batch_image.size(2)
296 pool_kernel = (max(w, h) // 16) * 2 + 1
297 # 定义Sobel核
298 kernel_x = (
299 torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=torch.float32)
300 .view(1, 1, 3, 3)
301 .to(batch_image.device)
302 .repeat(1, batch_image.size(1), 1, 1)
303 )
304 kernel_y = (
305 torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=torch.float32)
306 .view(1, 1, 3, 3)
307 .to(batch_image.device)
308 .repeat(1, batch_image.size(1), 1, 1)
309 )
310 # 初始化梯度张量
311 grad_x = torch.zeros_like(batch_image)
312 grad_y = torch.zeros_like(batch_image)
313 # 边缘填充
314 batch_image = F.pad(batch_image, (1, 1, 1, 1), mode="reflect")
315 # 应用Sobel算子
316 grad_x = F.conv2d(batch_image, kernel_x, padding=0)
317 grad_y = F.conv2d(batch_image, kernel_y, padding=0)
318 # 计算梯度的幅度
319 grad_magnitude = torch.sqrt(grad_x.pow(2) + grad_y.pow(2))
320 # Mask 处理
321 if mask is not None:
322 grad_magnitude = grad_magnitude * mask
323 # 剃度裁剪
324 grad_magnitude = torch.clamp(grad_magnitude, 0.2, 2.5)
325 # 平均池化
326 grad_magnitude = F.avg_pool2d(
327 grad_magnitude, kernel_size=pool_kernel, stride=1, padding=pool_kernel // 2
328 )
329 # 归一化
330 grad_magnitude = (grad_magnitude / grad_magnitude.max()) * scale
331 return grad_magnitude
332
333
334# Sobel 加权平方误差, 增强边缘区域的损失(直接用于损失计算)
335def sobel_aug_squared_error(x, y, reference, mask=None, reduction="mean"):
336 """
337 计算x,y的逐元素平方误差,其中x和y是图像张量.
338 然后利用 x 的 sobel 结果作为权重,计算加权平方误差.
339 x: Tensor, shape [batch, channels, height, width]
340 y: Tensor, shape [batch, channels, height, width]
341 """
342 ref_sobel = sobel(reference, mask=mask) # 计算 sobel 梯度作为损失权重
343 if ref_sobel.isnan().any():
344 print("Error: NaN Sobel Gradient")
345 loss = F.mse_loss(x, y, reduction="mean") # 如果梯度为nan,则直接退化为MSE损失
346 else:
347 squared_error = (x - y).pow(2)
348 weighted_squared_error = squared_error * ref_sobel
349 if reduction == "mean":
350 loss = weighted_squared_error.mean()
351 elif reduction == "sum":
352 loss = weighted_squared_error.sum()
353 elif reduction == "none":
354 loss = weighted_squared_error
355 # print("WSE Loss:", loss.mean(), loss.dtype)
356 return loss
357
358
359# 准备图像(转换为 Batch 张量)
360def prepare_image(image):
361 if isinstance(image, torch.Tensor):
362 # Batch single image
363 if image.ndim == 3:
364 image = image.unsqueeze(0)
365 image = image.to(dtype=torch.float32)
366 else:
367 # preprocess image
368 if isinstance(image, (PIL.Image.Image, np.ndarray)):
369 image = [image]
370 if isinstance(image, list) and isinstance(image[0], PIL.Image.Image):
371 image = [np.array(i.convert("RGB"))[None, :] for i in image]
372 image = np.concatenate(image, axis=0)
373 elif isinstance(image, list) and isinstance(image[0], np.ndarray):
374 image = np.concatenate([i[None, :] for i in image], axis=0)
375 image = image.transpose(0, 3, 1, 2)
376 image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0
377 return image
378
379
380def prepare_mask_image(mask_image):
381 if isinstance(mask_image, torch.Tensor):
382 if mask_image.ndim == 2:
383 # Batch and add channel dim for single mask
384 mask_image = mask_image.unsqueeze(0).unsqueeze(0)
385 elif mask_image.ndim == 3 and mask_image.shape[0] == 1:
386 # Single mask, the 0'th dimension is considered to be
387 # the existing batch size of 1
388 mask_image = mask_image.unsqueeze(0)
389 elif mask_image.ndim == 3 and mask_image.shape[0] != 1:
390 # Batch of mask, the 0'th dimension is considered to be
391 # the batching dimension
392 mask_image = mask_image.unsqueeze(1)
393
394 # Binarize mask
395 mask_image[mask_image < 0.5] = 0
396 mask_image[mask_image >= 0.5] = 1
397 else:
398 # preprocess mask
399 if isinstance(mask_image, (PIL.Image.Image, np.ndarray)):
400 mask_image = [mask_image]
401
402 if isinstance(mask_image, list) and isinstance(mask_image[0], PIL.Image.Image):
403 mask_image = np.concatenate(
404 [np.array(m.convert("L"))[None, None, :] for m in mask_image], axis=0
405 )
406 mask_image = mask_image.astype(np.float32) / 255.0
407 elif isinstance(mask_image, list) and isinstance(mask_image[0], np.ndarray):
408 mask_image = np.concatenate([m[None, None, :] for m in mask_image], axis=0)
409
410 mask_image[mask_image < 0.5] = 0
411 mask_image[mask_image >= 0.5] = 1
412 mask_image = torch.from_numpy(mask_image)
413
414 return mask_image
415
416
417def numpy_to_pil(images):
418 """
419 Convert a numpy image or a batch of images to a PIL image.
420 """
421 if images.ndim == 3:
422 images = images[None, ...]
423 images = (images * 255).round().astype("uint8")
424 if images.shape[-1] == 1:
425 # special case for grayscale (single channel) images
426 pil_images = [Image.fromarray(image.squeeze(), mode="L") for image in images]
427 else:
428 pil_images = [Image.fromarray(image) for image in images]
429
430 return pil_images
431
432
433def load_eval_image_pairs(root, mode="logo"):
434 # TODO 加载测试图像对,包括配对和非配对的图像对
435 test_name = "test"
436 person_image_paths = [
437 os.path.join(root, test_name, "image", _)
438 for _ in os.listdir(os.path.join(root, test_name, "image"))
439 if _.endswith(".jpg")
440 ]
441 cloth_image_paths = [
442 person_image_path.replace("image", "cloth")
443 for person_image_path in person_image_paths
444 ]
445 # 包含图案和文字的部分图像
446 if mode == "logo":
447 filter_pairs = [
448 6648,
449 6744,
450 6967,
451 6985,
452 14031,
453 12358,
454 4963,
455 4680,
456 499,
457 396,
458 345,
459 6648,
460 6744,
461 6967,
462 6985,
463 7510,
464 8205,
465 8254,
466 10545,
467 11485,
468 11632,
469 12354,
470 13144,
471 14112,
472 12570,
473 11766,
474 ]
475 filter_pairs.sort()
476 filter_pairs = [f"{_:05d}_00.jpg" for _ in filter_pairs]
477 cloth_image_paths = [
478 cloth_image_paths[i]
479 for i in range(len(cloth_image_paths))
480 if os.path.basename(cloth_image_paths[i]) in filter_pairs
481 ]
482 person_image_paths = [
483 person_image_paths[i]
484 for i in range(len(person_image_paths))
485 if os.path.basename(person_image_paths[i]) in filter_pairs
486 ]
487 return cloth_image_paths, person_image_paths
488
489
490def tensor_to_image(tensor: torch.Tensor):
491 """
492 Converts a torch tensor to PIL Image.
493 """
494 assert tensor.dim() == 3, "Input tensor should be 3-dimensional."
495 assert tensor.dtype == torch.float32, "Input tensor should be float32."
496 assert (
497 tensor.min() >= 0 and tensor.max() <= 1
498 ), "Input tensor should be in range [0, 1]."
499 tensor = tensor.cpu()
500 tensor = tensor * 255
501 tensor = tensor.permute(1, 2, 0)
502 tensor = tensor.numpy().astype(np.uint8)
503 image = Image.fromarray(tensor)
504 return image
505
506
507def concat_images(images: List[Image.Image], divider: int = 4, cols: int = 4):
508 """
509 Concatenates images horizontally and with
510 """
511 widths = [image.size[0] for image in images]
512 heights = [image.size[1] for image in images]
513 total_width = cols * max(widths)
514 total_width += divider * (cols - 1)
515 # `col` images each row
516 rows = math.ceil(len(images) / cols)
517 total_height = max(heights) * rows
518 # add divider between rows
519 total_height += divider * (len(heights) // cols - 1)
520
521 # all black image
522 concat_image = Image.new("RGB", (total_width, total_height), (0, 0, 0))
523
524 x_offset = 0
525 y_offset = 0
526 for i, image in enumerate(images):
527 concat_image.paste(image, (x_offset, y_offset))
528 x_offset += image.size[0] + divider
529 if (i + 1) % cols == 0:
530 x_offset = 0
531 y_offset += image.size[1] + divider
532
533 return concat_image
534
535
536def read_prompt_file(prompt_file: str):
537 if prompt_file is not None and os.path.isfile(prompt_file):
538 with open(prompt_file, "r") as sample_prompt_file:
539 sample_prompts = sample_prompt_file.readlines()
540 sample_prompts = [sample_prompt.strip() for sample_prompt in sample_prompts]
541 else:
542 sample_prompts = []
543 return sample_prompts
544
545
546def save_tensors_to_npz(tensors: torch.Tensor, paths: List[str]):
547 assert len(tensors) == len(paths), "Length of tensors and paths should be the same!"
548 for tensor, path in zip(tensors, paths):
549 np.savez_compressed(path, latent=tensor.cpu().numpy())
550
551
552def deepspeed_zero_init_disabled_context_manager():
553 """
554 returns either a context list that includes one that will disable zero.Init or an empty context list
555 """
556 deepspeed_plugin = (
557 AcceleratorState().deepspeed_plugin
558 if accelerate.state.is_initialized()
559 else None
560 )
561 if deepspeed_plugin is None:
562 return []
563
564 return [deepspeed_plugin.zero3_init_context_manager(enable=False)]
565
566
567def is_xformers_available():
568 try:
569 import xformers
570
571 xformers_version = version.parse(xformers.__version__)
572 if xformers_version == version.parse("0.0.16"):
573 print(
574 "xFormers 0.0.16 cannot be used for training in some GPUs. If you observe problems during training, "
575 "please update xFormers to at least 0.0.17. "
576 "See https://huggingface.co/docs/diffusers/main/en/optimization/xformers for more details."
577 )
578 return True
579 except ImportError:
580 raise ValueError(
581 "xformers is not available. Make sure it is installed correctly"
582 )
583
584
585def resize_and_crop(image, size):
586 # Crop to size ratio
587 w, h = image.size
588 target_w, target_h = size
589 if w / h < target_w / target_h:
590 new_w = w
591 new_h = w * target_h // target_w
592 else:
593 new_h = h
594 new_w = h * target_w // target_h
595 image = image.crop(
596 ((w - new_w) // 2, (h - new_h) // 2, (w + new_w) // 2, (h + new_h) // 2)
597 )
598 # resize
599 image = image.resize(size, Image.LANCZOS)
600 return image
601
602
603def resize_and_padding(image, size):
604 # Padding to size ratio
605 w, h = image.size
606 target_w, target_h = size
607 if w / h < target_w / target_h:
608 new_h = target_h
609 new_w = w * target_h // h
610 else:
611 new_w = target_w
612 new_h = h * target_w // w
613 image = image.resize((new_w, new_h), Image.LANCZOS)
614 # padding
615 padding = Image.new("RGB", size, (255, 255, 255))
616 padding.paste(image, ((target_w - new_w) // 2, (target_h - new_h) // 2))
617 return padding
618
619
620
621if __name__ == "__main__":
622 pass
623 