CoolFace
Apppublic

SAHILCBR/GLAMAX-virtual-try-on

sourceHugging Facecc-by-nc-sa-4.0updated 4mo agoView on Hugging Face
0likes
utils.py623 linesDownload Raw Back to root
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