#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Torchvision Transforms Playground (Gradio framework) Interactive sandbox to transforming images using torchvision that includes this features: - Upload one or multiple images - Toggle transforms and tune parameters - Preview one example per enabled transform + a final MIX pipeline with multiple random variants - See a dynamically generated torchvision Compose code snippet - Switch UI language (EN/FR) - Disable all transforms in one click - Quick links to torchvision documentation per section Usage: python3 app.py """ from __future__ import annotations import json import random from dataclasses import dataclass from typing import Any, Dict, List, Tuple, Optional from pathlib import Path import gradio as gr import torch from PIL import Image from torchvision.transforms import v2 as T from torchvision.transforms.functional import to_pil_image # Assets DEFAULT_I18N_PATH = "assets/i18n.json" DEFAULT_CSS_PATH = "assets/styles.css" # Small utilities (image / html / i18n) def load_texts_json(path: str) -> str: """ Load i18n JSON string from file. :param path: Path to JSON file. :type path: str :return: JSON string. :rtype: str """ return Path(path).read_text(encoding="utf-8") def load_css(path: str) -> str: """ Load CSS string from file. :param path: Path to CSS file. :type path: str :return: CSS string. :rtype: str """ return Path(path).read_text(encoding="utf-8") def to_rgb(img: Image.Image) -> Image.Image: if img.mode == "RGB": return img if img.mode in ("RGBA", "LA"): bg = Image.new("RGB", img.size, (255, 255, 255)) bg.paste(img, mask=img.split()[-1]) return bg return img.convert("RGB") class I18N: """ Simple i18n manager backed by a JSON string. :param texts_json: JSON string holding UI texts for each language. :type texts_json: str :param default_lang: Default language key (e.g. "EN"). :type default_lang: str """ def __init__(self, texts_json: str, default_lang: str = "EN") -> None: self._texts = json.loads(texts_json) self._default_lang = default_lang def get(self, lang: str, key: str, default: Optional[str] = None) -> str: """ Get a text key for a given language. :param lang: Language key (e.g. "EN", "FR"). :type lang: str :param key: Text key. :type key: str :param default: Default value if missing. :type default: Optional[str] :return: Text value. :rtype: str """ return self._texts.get(lang, {}).get( key, default if default is not None else "" ) def section(self, lang: str, section_key: str) -> str: """ Get a section name (translated). :param lang: Language key. :type lang: str :param section_key: Section identifier (e.g. "geometric"). :type section_key: str :return: Section label. :rtype: str """ return ( self._texts.get(lang, {}).get("sections", {}).get(section_key, section_key) ) def subtitles(self, lang: str) -> List[str]: """ Get the list of subtitles for a language. :param lang: Language key. :type lang: str :return: Subtitles list. :rtype: List[str] """ return list(self._texts.get(lang, {}).get("app_subtitles", [])) def status_dot(active: bool) -> str: """ Render a small colored dot (HTML). :param active: Whether the section is active. :type active: bool :return: HTML span for a dot. :rtype: str """ color = "#22c55e" if active else "#94a3b8" # green / gray return ( "" ) def as_pil_list(gallery_value: Any) -> List[Image.Image]: """ Convert a Gradio Gallery input value to a list of PIL images. :param gallery_value: Gallery value from gr.Gallery. :type gallery_value: Any :return: List of PIL.Image objects. :rtype: List[Image.Image] """ if not gallery_value: return [] imgs: List[Image.Image] = [] for item in gallery_value: im = item[0] if isinstance(item, tuple) and len(item) >= 1 else item imgs.append(to_rgb(im)) return imgs def ensure_pil(x: Any) -> Image.Image: """ Ensure output is a PIL image (convert torch.Tensor if needed). :param x: Transform output (PIL.Image or torch.Tensor). :type x: Any :return: PIL image. :rtype: PIL.Image.Image :raises TypeError: If unsupported type. """ if isinstance(x, Image.Image): return to_rgb(x) if isinstance(x, torch.Tensor): return to_rgb(to_pil_image(x.clamp(0, 1))) raise TypeError(f"Unsupported output type: {type(x)}") @dataclass class TransformItem: """ A single transform descriptor. :param name: Display name. :type name: str :param op: Transform object OR a special sentinel string. :type op: Any """ name: str op: Any class TransformFactory: """ Factory for building: - a list of enabled single transforms (one-by-one examples) - the final Compose pipeline (MIX) This encapsulates transform construction and keeps UI code clean. """ TENSOR_ERASE_ONLY = "TENSOR_ERASE_ONLY" TENSOR_NORM_ONLY = "TENSOR_NORM_ONLY" def build_single_transforms(self, p: Dict[str, Any]) -> List[TransformItem]: """ Build a list of single transforms (one transform = one operation). :param p: Parameters dict (toggles + params). :type p: Dict[str, Any] :return: List of enabled transforms. :rtype: List[TransformItem] """ L: List[TransformItem] = [] # Geometric if p["use_pad"]: L.append( TransformItem( "Pad", T.Pad( padding=int(p["pad_px"]), fill=int(p["pad_fill"]), padding_mode=p["pad_mode"], ), ) ) if p["use_resize"]: L.append( TransformItem( "Resize", T.Resize((int(p["resize_size"]), int(p["resize_size"]))) ) ) if p["use_center_crop"]: L.append( TransformItem( "CenterCrop", T.CenterCrop((int(p["crop_size"]), int(p["crop_size"]))), ) ) if p["use_five_crop"]: L.append( TransformItem( "FiveCrop", T.FiveCrop((int(p["five_crop_size"]), int(p["five_crop_size"]))), ) ) if p["use_random_perspective"]: L.append( TransformItem( "RandomPerspective", T.RandomPerspective( distortion_scale=float(p["persp_dist"]), p=float(p["persp_p"]) ), ) ) if p["use_random_rotation"]: L.append( TransformItem( "RandomRotation", T.RandomRotation(degrees=int(p["rot_deg"])) ) ) if p["use_random_affine"]: L.append( TransformItem( "RandomAffine", T.RandomAffine( degrees=int(p["aff_deg"]), translate=( float(p["aff_translate"]), float(p["aff_translate"]), ), scale=(float(p["aff_scale_min"]), float(p["aff_scale_max"])), shear=int(p["aff_shear"]), ), ) ) if p["use_elastic"]: L.append( TransformItem( "ElasticTransform", T.ElasticTransform( alpha=float(p["elastic_alpha"]), sigma=float(p["elastic_sigma"]) ), ) ) if p["use_random_crop"]: L.append( TransformItem( "RandomCrop", T.RandomCrop((int(p["rand_crop_size"]), int(p["rand_crop_size"]))), ) ) if p["use_rrc"]: L.append( TransformItem( "RandomResizedCrop", T.RandomResizedCrop( (int(p["rrc_size"]), int(p["rrc_size"])), scale=(float(p["rrc_scale_min"]), float(p["rrc_scale_max"])), ), ) ) # Photometric if p["use_grayscale"]: L.append( TransformItem( "Grayscale", T.Grayscale(num_output_channels=int(p["gray_channels"])), ) ) if p["use_cj"]: L.append( TransformItem( "ColorJitter", T.ColorJitter( brightness=float(p["cj_b"]), contrast=float(p["cj_c"]), saturation=float(p["cj_s"]), hue=float(p["cj_h"]), ), ) ) if p["use_blur"]: k = int(p["blur_k"]) if k % 2 == 0: k += 1 L.append( TransformItem( "GaussianBlur", T.GaussianBlur( kernel_size=k, sigma=(float(p["blur_sigma_min"]), float(p["blur_sigma_max"])), ), ) ) if p["use_inv"]: L.append(TransformItem("RandomInvert", T.RandomInvert(p=float(p["inv_p"])))) if p["use_post"]: L.append( TransformItem( "RandomPosterize", T.RandomPosterize(bits=int(p["post_bits"]), p=float(p["post_p"])), ) ) if p["use_sol"]: L.append( TransformItem( "RandomSolarize", T.RandomSolarize( threshold=int(p["sol_thresh"]), p=float(p["sol_p"]) ), ) ) if p["use_sharp"]: L.append( TransformItem( "RandomAdjustSharpness", T.RandomAdjustSharpness( sharpness_factor=float(p["sharp_factor"]), p=float(p["sharp_p"]) ), ) ) if p["use_autoc"]: L.append(TransformItem("RandomAutocontrast", T.RandomAutocontrast())) if p["use_eq"]: L.append(TransformItem("RandomEqualize", T.RandomEqualize())) if p["use_jpeg"]: L.append( TransformItem( "JPEG", T.JPEG(quality=(int(p["jpeg_qmin"]), int(p["jpeg_qmax"]))) ) ) # Policies if p["use_autoaugment"]: policy = getattr(T.AutoAugmentPolicy, p["aa_policy"]) L.append(TransformItem("AutoAugment", T.AutoAugment(policy=policy))) if p["use_randaugment"]: L.append( TransformItem( "RandAugment", T.RandAugment( num_ops=int(p["ra_num_ops"]), magnitude=int(p["ra_mag"]) ), ) ) if p["use_trivial"]: L.append( TransformItem( "TrivialAugmentWide", T.TrivialAugmentWide(num_magnitude_bins=int(p["tw_bins"])), ) ) if p["use_augmix"]: L.append( TransformItem( "AugMix", T.AugMix( severity=int(p["am_severity"]), mixture_width=int(p["am_width"]), chain_depth=int(p["am_depth"]), alpha=float(p["am_alpha"]), ), ) ) # Randomly-applied if p["use_hflip"]: L.append( TransformItem( "RandomHorizontalFlip", T.RandomHorizontalFlip(p=float(p["hflip_p"])), ) ) if p["use_vflip"]: L.append( TransformItem( "RandomVerticalFlip", T.RandomVerticalFlip(p=float(p["vflip_p"])) ) ) if p["use_random_apply"]: inner = [T.RandomCrop((int(p["ra_crop"]), int(p["ra_crop"])))] L.append( TransformItem( "RandomApply(RandomCrop)", T.RandomApply(transforms=inner, p=float(p["ra_p"])), ) ) # Tensor bonus as single examples if p["use_erase"]: L.append(TransformItem("RandomErasing (tensor)", self.TENSOR_ERASE_ONLY)) if p["use_norm"]: L.append(TransformItem("Normalize (tensor)", self.TENSOR_NORM_ONLY)) return L def build_compose(self, p: Dict[str, Any]) -> T.Compose: """ Build the final Compose pipeline (MIX). :param p: Parameters dict. :type p: Dict[str, Any] :return: Torchvision v2 Compose transform. :rtype: torchvision.transforms.v2.Compose """ transforms: List[Any] = [] # Geometric if p["use_pad"]: transforms.append( T.Pad( padding=int(p["pad_px"]), fill=int(p["pad_fill"]), padding_mode=p["pad_mode"], ) ) if p["use_resize"]: transforms.append(T.Resize((int(p["resize_size"]), int(p["resize_size"])))) if p["use_center_crop"]: transforms.append(T.CenterCrop((int(p["crop_size"]), int(p["crop_size"])))) if p["use_five_crop"]: transforms.append( T.FiveCrop((int(p["five_crop_size"]), int(p["five_crop_size"]))) ) # returns 5 if p["use_random_perspective"]: transforms.append( T.RandomPerspective( distortion_scale=float(p["persp_dist"]), p=float(p["persp_p"]) ) ) if p["use_random_rotation"]: transforms.append(T.RandomRotation(degrees=int(p["rot_deg"]))) if p["use_random_affine"]: transforms.append( T.RandomAffine( degrees=int(p["aff_deg"]), translate=(float(p["aff_translate"]), float(p["aff_translate"])), scale=(float(p["aff_scale_min"]), float(p["aff_scale_max"])), shear=int(p["aff_shear"]), ) ) if p["use_elastic"]: transforms.append( T.ElasticTransform( alpha=float(p["elastic_alpha"]), sigma=float(p["elastic_sigma"]) ) ) if p["use_random_crop"]: transforms.append( T.RandomCrop((int(p["rand_crop_size"]), int(p["rand_crop_size"]))) ) if p["use_rrc"]: transforms.append( T.RandomResizedCrop( (int(p["rrc_size"]), int(p["rrc_size"])), scale=(float(p["rrc_scale_min"]), float(p["rrc_scale_max"])), ) ) # Photometric if p["use_grayscale"]: transforms.append(T.Grayscale(num_output_channels=int(p["gray_channels"]))) if p["use_cj"]: transforms.append( T.ColorJitter( brightness=float(p["cj_b"]), contrast=float(p["cj_c"]), saturation=float(p["cj_s"]), hue=float(p["cj_h"]), ) ) if p["use_blur"]: k = int(p["blur_k"]) if k % 2 == 0: k += 1 transforms.append( T.GaussianBlur( kernel_size=k, sigma=(float(p["blur_sigma_min"]), float(p["blur_sigma_max"])), ) ) if p["use_inv"]: transforms.append(T.RandomInvert(p=float(p["inv_p"]))) if p["use_post"]: transforms.append( T.RandomPosterize(bits=int(p["post_bits"]), p=float(p["post_p"])) ) if p["use_sol"]: transforms.append( T.RandomSolarize(threshold=int(p["sol_thresh"]), p=float(p["sol_p"])) ) if p["use_sharp"]: transforms.append( T.RandomAdjustSharpness( sharpness_factor=float(p["sharp_factor"]), p=float(p["sharp_p"]) ) ) if p["use_autoc"]: transforms.append(T.RandomAutocontrast()) if p["use_eq"]: transforms.append(T.RandomEqualize()) if p["use_jpeg"]: transforms.append( T.JPEG(quality=(int(p["jpeg_qmin"]), int(p["jpeg_qmax"]))) ) # Policies if p["use_autoaugment"]: policy = getattr(T.AutoAugmentPolicy, p["aa_policy"]) transforms.append(T.AutoAugment(policy=policy)) if p["use_randaugment"]: transforms.append( T.RandAugment(num_ops=int(p["ra_num_ops"]), magnitude=int(p["ra_mag"])) ) if p["use_trivial"]: transforms.append( T.TrivialAugmentWide(num_magnitude_bins=int(p["tw_bins"])) ) if p["use_augmix"]: transforms.append( T.AugMix( severity=int(p["am_severity"]), mixture_width=int(p["am_width"]), chain_depth=int(p["am_depth"]), alpha=float(p["am_alpha"]), ) ) # Randomly-applied if p["use_hflip"]: transforms.append(T.RandomHorizontalFlip(p=float(p["hflip_p"]))) if p["use_vflip"]: transforms.append(T.RandomVerticalFlip(p=float(p["vflip_p"]))) if p["use_random_apply"]: inner = [T.RandomCrop((int(p["ra_crop"]), int(p["ra_crop"])))] transforms.append(T.RandomApply(transforms=inner, p=float(p["ra_p"]))) # Tensor-only need_tensor = p["use_erase"] or p["use_norm"] if need_tensor: transforms.append(T.ToImage()) transforms.append(T.ToDtype(torch.float32, scale=True)) if p["use_erase"]: transforms.append( T.RandomErasing( p=float(p["erase_p"]), scale=( float(p["erase_scale_min"]), float(p["erase_scale_max"]), ), ratio=( float(p["erase_ratio_min"]), float(p["erase_ratio_max"]), ), value="random", ) ) if p["use_norm"]: mean = [float(x.strip()) for x in str(p["norm_mean"]).split(",")] std = [float(x.strip()) for x in str(p["norm_std"]).split(",")] transforms.append(T.Normalize(mean=mean, std=std)) return T.Compose(transforms) def tensor_only_example(self, p: Dict[str, Any], which: str) -> T.Compose: """ Create a local tensor-only pipeline used for single-transform previews. :param p: Parameters dict. :type p: Dict[str, Any] :param which: Sentinel ("TENSOR_ERASE_ONLY" or "TENSOR_NORM_ONLY"). :type which: str :return: Compose pipeline that converts image to tensor then applies the tensor op. :rtype: torchvision.transforms.v2.Compose """ base = [T.ToImage(), T.ToDtype(torch.float32, scale=True)] if which == self.TENSOR_ERASE_ONLY: base.append( T.RandomErasing( p=float(p["erase_p"]), scale=(float(p["erase_scale_min"]), float(p["erase_scale_max"])), ratio=(float(p["erase_ratio_min"]), float(p["erase_ratio_max"])), value="random", ) ) elif which == self.TENSOR_NORM_ONLY: mean = [float(x.strip()) for x in str(p["norm_mean"]).split(",")] std = [float(x.strip()) for x in str(p["norm_std"]).split(",")] base.append(T.Normalize(mean=mean, std=std)) else: raise ValueError(f"Unknown tensor sentinel: {which}") return T.Compose(base) class CodeGenerator: """ Generate the torchvision v2 Compose python code from parameters. """ def to_code(self, p: Dict[str, Any]) -> str: """ Create a code snippet reflecting the current pipeline in interface. :param p: Parameters dict. :type p: Dict[str, Any] :return: Python code snippet. :rtype: str """ lines: List[str] = [ "from torchvision.transforms import v2 as T", "import torch", "", "transform = T.Compose([", ] def add(s: str) -> None: lines.append(f" {s},") # Geometric if p["use_pad"]: add( f"T.Pad(padding={int(p['pad_px'])}, fill={int(p['pad_fill'])}, padding_mode='{p['pad_mode']}')" ) if p["use_resize"]: add(f"T.Resize(({int(p['resize_size'])}, {int(p['resize_size'])}))") if p["use_center_crop"]: add(f"T.CenterCrop(({int(p['crop_size'])}, {int(p['crop_size'])}))") if p["use_five_crop"]: add( f"T.FiveCrop(({int(p['five_crop_size'])}, {int(p['five_crop_size'])})) # returns 5 images" ) if p["use_random_perspective"]: add( f"T.RandomPerspective(distortion_scale={float(p['persp_dist']):.2f}, p={float(p['persp_p']):.2f})" ) if p["use_random_rotation"]: add(f"T.RandomRotation(degrees={int(p['rot_deg'])})") if p["use_random_affine"]: add( "T.RandomAffine(" f"degrees={int(p['aff_deg'])}, " f"translate=({float(p['aff_translate']):.2f}, {float(p['aff_translate']):.2f}), " f"scale=({float(p['aff_scale_min']):.2f}, {float(p['aff_scale_max']):.2f}), " f"shear={int(p['aff_shear'])}" ")" ) if p["use_elastic"]: add( f"T.ElasticTransform(alpha={float(p['elastic_alpha']):.2f}, sigma={float(p['elastic_sigma']):.2f})" ) if p["use_random_crop"]: add( f"T.RandomCrop(({int(p['rand_crop_size'])}, {int(p['rand_crop_size'])}))" ) if p["use_rrc"]: add( "T.RandomResizedCrop(" f"({int(p['rrc_size'])}, {int(p['rrc_size'])}), " f"scale=({float(p['rrc_scale_min']):.3f}, {float(p['rrc_scale_max']):.3f})" ")" ) # Photometric if p["use_grayscale"]: add(f"T.Grayscale(num_output_channels={int(p['gray_channels'])})") if p["use_cj"]: add( "T.ColorJitter(" f"brightness={float(p['cj_b']):.2f}, contrast={float(p['cj_c']):.2f}, " f"saturation={float(p['cj_s']):.2f}, hue={float(p['cj_h']):.2f}" ")" ) if p["use_blur"]: k = int(p["blur_k"]) if k % 2 == 0: k += 1 add( f"T.GaussianBlur(kernel_size={k}, sigma=({float(p['blur_sigma_min']):.2f}, {float(p['blur_sigma_max']):.2f}))" ) if p["use_inv"]: add(f"T.RandomInvert(p={float(p['inv_p']):.2f})") if p["use_post"]: add( f"T.RandomPosterize(bits={int(p['post_bits'])}, p={float(p['post_p']):.2f})" ) if p["use_sol"]: add( f"T.RandomSolarize(threshold={int(p['sol_thresh'])}, p={float(p['sol_p']):.2f})" ) if p["use_sharp"]: add( f"T.RandomAdjustSharpness(sharpness_factor={float(p['sharp_factor']):.2f}, p={float(p['sharp_p']):.2f})" ) if p["use_autoc"]: add("T.RandomAutocontrast()") if p["use_eq"]: add("T.RandomEqualize()") if p["use_jpeg"]: add(f"T.JPEG(quality=({int(p['jpeg_qmin'])}, {int(p['jpeg_qmax'])}))") # Policies if p["use_autoaugment"]: add(f"T.AutoAugment(policy=T.AutoAugmentPolicy.{p['aa_policy']})") if p["use_randaugment"]: add( f"T.RandAugment(num_ops={int(p['ra_num_ops'])}, magnitude={int(p['ra_mag'])})" ) if p["use_trivial"]: add(f"T.TrivialAugmentWide(num_magnitude_bins={int(p['tw_bins'])})") if p["use_augmix"]: add( "T.AugMix(" f"severity={int(p['am_severity'])}, mixture_width={int(p['am_width'])}, " f"chain_depth={int(p['am_depth'])}, alpha={float(p['am_alpha']):.2f}" ")" ) # Randomly-applied if p["use_hflip"]: add(f"T.RandomHorizontalFlip(p={float(p['hflip_p']):.2f})") if p["use_vflip"]: add(f"T.RandomVerticalFlip(p={float(p['vflip_p']):.2f})") if p["use_random_apply"]: add( f"T.RandomApply(transforms=[T.RandomCrop(({int(p['ra_crop'])}, {int(p['ra_crop'])}))], p={float(p['ra_p']):.2f})" ) # Tensor-only need_tensor = p["use_erase"] or p["use_norm"] if need_tensor: add("T.ToImage()") add("T.ToDtype(torch.float32, scale=True)") if p["use_erase"]: add( "T.RandomErasing(" f"p={float(p['erase_p']):.2f}, " f"scale=({float(p['erase_scale_min']):.3f}, {float(p['erase_scale_max']):.3f}), " f"ratio=({float(p['erase_ratio_min']):.2f}, {float(p['erase_ratio_max']):.2f}), " 'value="random"' ")" ) if p["use_norm"]: add( f"T.Normalize(mean=[{p['norm_mean']}], std=[{p['norm_std']}]) # CSV -> list" ) lines.append("])") return "\n".join(lines) class TransformationEngine: """ Apply transforms: - one example per enabled transform - final MIX pipeline with N variants (define by user) :param factory: TransformFactory instance. :type factory: TransformFactory """ def __init__(self, factory: TransformFactory) -> None: self.factory = factory def apply( self, gallery_in: Any, n_variants: int, seed: int, reseed_each_variant: bool, params: Dict[str, Any], ) -> List[Dict[str, Any]]: """ Apply transformations and return HTML. :param gallery_in: Gradio gallery value. :type gallery_in: Any :param n_variants: Number of variants for the MIX pipeline. :type n_variants: int :param seed: Base random seed. :type seed: int :param reseed_each_variant: Whether to re-seed each variant for reproducibility. :type reseed_each_variant: bool :param params: Transform parameters. :type params: Dict[str, Any] :return: Rendered HTML results. :rtype: str """ images = as_pil_list(gallery_in) if not images: return "" base_seed = int(seed) singles = self.factory.build_single_transforms(params) grouped: Dict[int, Dict[str, Any]] = {} for idx, img in enumerate(images): grouped[idx] = {"original": img, "singles": [], "mix": []} # one example per transform for item in singles: tname, tform = item.name, item.op s = base_seed + idx * 10_000 + (abs(hash(tname)) % 10_000) random.seed(s) torch.manual_seed(s) if tname == "FiveCrop": y = tform(img) # tuple of 5 for i, crop in enumerate(y): grouped[idx]["singles"].append( (f"FiveCrop #{i + 1}", ensure_pil(crop)) ) continue if tform == TransformFactory.TENSOR_ERASE_ONLY: tt = self.factory.tensor_only_example( params, TransformFactory.TENSOR_ERASE_ONLY ) grouped[idx]["singles"].append((tname, ensure_pil(tt(img)))) continue if tform == TransformFactory.TENSOR_NORM_ONLY: tt = self.factory.tensor_only_example( params, TransformFactory.TENSOR_NORM_ONLY ) grouped[idx]["singles"].append((tname, ensure_pil(tt(img)))) continue grouped[idx]["singles"].append((tname, ensure_pil(tform(img)))) # + MIX pipe = self.factory.build_compose(params) for v in range(int(n_variants)): if reseed_each_variant: s = base_seed + idx * 1000 + v random.seed(s) torch.manual_seed(s) y = pipe(img) # FiveCrop inside Compose returns tuple - for mix we show first crop if isinstance(y, (tuple, list)) and len(y) > 0: grouped[idx]["mix"].append( (f"aug #{v + 1} (FiveCrop→#1)", ensure_pil(y[0])) ) else: grouped[idx]["mix"].append((f"aug #{v + 1}", ensure_pil(y))) out = [] for idx, block in grouped.items(): out.append( { "idx": idx, "original": block["original"], "singles": block.get("singles", []), # list[(name, PIL)] "mix": block.get("mix", []), # list[(cap, PIL)] } ) return out # App logic class TTPApp: """ Main Gradio application class. :param i18n: I18N manager. :type i18n: I18N :param engine: Transformation engine. :type engine: TransformationEngine :param codegen: Code generator. :type codegen: CodeGenerator """ def __init__( self, i18n: I18N, engine: TransformationEngine, codegen: CodeGenerator ) -> None: self.i18n = i18n self.engine = engine self.codegen = codegen # cache (for future?) # self._cache_key = None # self._cache_singles = None # self._cache_pipe = None # populated when building UI self._toggles: List[gr.Checkbox] = [] self._params_inputs: List[gr.components.Component] = [] def _active_sections_html(self, lang: str, p: Dict[str, Any]) -> str: """ Build the "active sections" status block. :param p: Parameters dict. :type p: Dict[str, Any] :return: HTML snippet. :rtype: str """ active_geo = any( p[k] for k in [ "use_pad", "use_resize", "use_center_crop", "use_five_crop", "use_random_perspective", "use_random_rotation", "use_random_affine", "use_elastic", "use_random_crop", "use_rrc", ] ) active_photo = any( p[k] for k in [ "use_grayscale", "use_cj", "use_blur", "use_inv", "use_post", "use_sol", "use_sharp", "use_autoc", "use_eq", "use_jpeg", ] ) active_aug = any( p[k] for k in ["use_autoaugment", "use_randaugment", "use_trivial", "use_augmix"] ) active_randomly = any( p[k] for k in ["use_hflip", "use_vflip", "use_random_apply"] ) active_tensor = any(p[k] for k in ["use_erase", "use_norm"]) return f"""