| | """ |
| | vit_mosaic.py |
| | |
| | ViT-style patch mosaic generator |
| | Supports: |
| | - Auto grid selection (12 / 16 patches) |
| | - Transparent or colored padding |
| | - Rounded borders |
| | - True rounded clipping |
| | - Supersampling |
| | - Downscale or keep resolution |
| | """ |
| |
|
| | import math |
| | import numpy as np |
| | from PIL import Image, ImageDraw |
| | from typing import Iterable, Tuple, Union |
| |
|
| |
|
| | ColorType = Union[Tuple[int, int, int], str] |
| |
|
| |
|
| | def parse_color(color: ColorType): |
| | if isinstance(color, tuple): |
| | return (*color, 255) |
| | if isinstance(color, str): |
| | color = color.strip() |
| | if color.startswith("#"): |
| | r = int(color[1:3], 16) |
| | g = int(color[3:5], 16) |
| | b = int(color[5:7], 16) |
| | return (r, g, b, 255) |
| | raise ValueError("Color must be RGB tuple or hex string '#RRGGBB'") |
| |
|
| |
|
| | def make_vit_mosaic( |
| | image: Image.Image, |
| | target_total_patches: Iterable[int] = (12, 16), |
| | max_long_side: int = 256, |
| | spacing: int = 12, |
| | border_thickness: int = 14, |
| | border_color: ColorType = "#00FFFF", |
| | padding_color: Union[None, ColorType] = None, |
| | corner_radius: int = 22, |
| | rounded: bool = True, |
| | true_clipping: bool = True, |
| | supersample: int = 1, |
| | output_scale_mode: str = "keep", |
| | ): |
| | border_rgba = parse_color(border_color) |
| | image = image.convert("RGBA") |
| | w, h = image.size |
| |
|
| | scale = max_long_side / max(w, h) |
| | new_w = int(w * scale) |
| | new_h = int(h * scale) |
| | image = image.resize((new_w, new_h), Image.LANCZOS) |
| |
|
| | aspect = new_w / new_h |
| | best_choice = None |
| | best_diff = float("inf") |
| |
|
| | for total in target_total_patches: |
| | for rows in range(1, total + 1): |
| | if total % rows == 0: |
| | cols = total // rows |
| | diff = abs((cols / rows) - aspect) |
| | if diff < best_diff: |
| | best_diff = diff |
| | best_choice = (rows, cols) |
| |
|
| | rows, cols = best_choice |
| |
|
| | patch_w = math.ceil(new_w / cols) |
| | patch_h = math.ceil(new_h / rows) |
| | patch_size = max(patch_w, patch_h) |
| |
|
| | pad_w = patch_size * cols |
| | pad_h = patch_size * rows |
| |
|
| | if padding_color is None: |
| | canvas = Image.new("RGBA", (pad_w, pad_h), (0, 0, 0, 0)) |
| | else: |
| | canvas = Image.new("RGBA", (pad_w, pad_h), parse_color(padding_color)) |
| |
|
| | offset_x = (pad_w - new_w) // 2 |
| | offset_y = (pad_h - new_h) // 2 |
| | canvas.paste(image, (offset_x, offset_y), image) |
| |
|
| | arr = np.array(canvas, dtype=np.uint8) |
| |
|
| | patches = ( |
| | arr.reshape(rows, patch_size, cols, patch_size, 4) |
| | .transpose(0, 2, 1, 3, 4) |
| | .reshape(rows * cols, patch_size, patch_size, 4) |
| | ) |
| |
|
| | ss = max(1, supersample) |
| |
|
| | scaled_patch = patch_size * ss |
| | scaled_border = border_thickness * ss |
| | scaled_radius = corner_radius * ss |
| | scaled_spacing = spacing * ss |
| |
|
| | tile_w = scaled_patch + 2 * scaled_border |
| | tile_h = scaled_patch + 2 * scaled_border |
| |
|
| | mosaic_w = cols * tile_w + (cols + 1) * scaled_spacing |
| | mosaic_h = rows * tile_h + (rows + 1) * scaled_spacing |
| |
|
| | mosaic = Image.new("RGBA", (mosaic_w, mosaic_h), (0, 0, 0, 0)) |
| |
|
| | def create_tile(patch_img): |
| | patch_img = patch_img.resize( |
| | (scaled_patch, scaled_patch), |
| | Image.NEAREST |
| | ) |
| |
|
| | tile = Image.new("RGBA", (tile_w, tile_h), (0, 0, 0, 0)) |
| | draw = ImageDraw.Draw(tile) |
| |
|
| | if rounded: |
| | draw.rounded_rectangle( |
| | [0, 0, tile_w - 1, tile_h - 1], |
| | radius=scaled_radius, |
| | fill=border_rgba, |
| | ) |
| | else: |
| | draw.rectangle( |
| | [0, 0, tile_w - 1, tile_h - 1], |
| | fill=border_rgba, |
| | ) |
| |
|
| | if rounded and true_clipping: |
| | mask = Image.new("L", (scaled_patch, scaled_patch), 0) |
| | mask_draw = ImageDraw.Draw(mask) |
| | mask_draw.rounded_rectangle( |
| | [0, 0, scaled_patch - 1, scaled_patch - 1], |
| | radius=max(0, scaled_radius - scaled_border), |
| | fill=255, |
| | ) |
| | tile.paste(patch_img, (scaled_border, scaled_border), mask) |
| | else: |
| | tile.paste(patch_img, (scaled_border, scaled_border), patch_img) |
| |
|
| | return tile |
| |
|
| | for idx in range(patches.shape[0]): |
| | r = idx // cols |
| | c = idx % cols |
| | patch_img = Image.fromarray(patches[idx]) |
| | tile = create_tile(patch_img) |
| |
|
| | x = scaled_spacing + c * (tile_w + scaled_spacing) |
| | y = scaled_spacing + r * (tile_h + scaled_spacing) |
| | mosaic.paste(tile, (x, y), tile) |
| |
|
| | if ss > 1 and output_scale_mode == "downscale": |
| | mosaic = mosaic.resize( |
| | (mosaic_w // ss, mosaic_h // ss), |
| | Image.LANCZOS |
| | ) |
| |
|
| | return mosaic, patches |