daidedou commited on
Commit ·
856dd67
1
Parent(s): eb67e2d
Iniital commit
Browse files- app.py +277 -0
- requirements.txt +5 -0
- vit_mosaic.py +164 -0
app.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from PIL import Image
|
| 3 |
+
from vit_mosaic import make_vit_mosaic
|
| 4 |
+
import tempfile
|
| 5 |
+
import os
|
| 6 |
+
import svgwrite
|
| 7 |
+
import base64
|
| 8 |
+
import requests
|
| 9 |
+
from io import BytesIO
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# -------------------------------------------------
|
| 13 |
+
# Example Images
|
| 14 |
+
# -------------------------------------------------
|
| 15 |
+
|
| 16 |
+
EXAMPLES = {
|
| 17 |
+
"Dogs": {
|
| 18 |
+
"url": "https://raw.githubusercontent.com/daidedou/daidedou.github.io/master/images/dogs.jpg",
|
| 19 |
+
"credit": "Photo by Jackielsy — Pixabay (CC0)"
|
| 20 |
+
},
|
| 21 |
+
"Landscape": {
|
| 22 |
+
"url": "https://raw.githubusercontent.com/daidedou/daidedou.github.io/master/images/landscape.jpg",
|
| 23 |
+
"credit": "Photo by brenkee — Pixabay (CC0)"
|
| 24 |
+
},
|
| 25 |
+
"Illustration": {
|
| 26 |
+
"url": "https://raw.githubusercontent.com/daidedou/daidedou.github.io/master/images/illustration.jpg",
|
| 27 |
+
"credit": "Illustration by the_iop — Pixabay (CC0)"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
# -------------------------------------------------
|
| 32 |
+
# Utilities
|
| 33 |
+
# -------------------------------------------------
|
| 34 |
+
|
| 35 |
+
def rgb_to_hex(r, g, b):
|
| 36 |
+
return f"#{r:02X}{g:02X}{b:02X}"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def update_color_preview(r, g, b):
|
| 40 |
+
hex_color = rgb_to_hex(r, g, b)
|
| 41 |
+
return f"""
|
| 42 |
+
<div style="
|
| 43 |
+
width:100%;
|
| 44 |
+
height:40px;
|
| 45 |
+
border-radius:8px;
|
| 46 |
+
border:1px solid #ccc;
|
| 47 |
+
background:{hex_color};
|
| 48 |
+
"></div>
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def toggle_clipping(enabled):
|
| 53 |
+
return gr.update(interactive=enabled)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def load_image_from_url(url):
|
| 57 |
+
response = requests.get(url, timeout=10)
|
| 58 |
+
response.raise_for_status()
|
| 59 |
+
return Image.open(BytesIO(response.content)).convert("RGB")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def on_gallery_select(evt: gr.SelectData):
|
| 63 |
+
name = list(EXAMPLES.keys())[evt.index]
|
| 64 |
+
data = EXAMPLES[name]
|
| 65 |
+
image = load_image_from_url(data["url"])
|
| 66 |
+
credit = f"**Image Credit:** {data['credit']}"
|
| 67 |
+
return image, credit
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def export_svg(image, path):
|
| 71 |
+
width, height = image.size
|
| 72 |
+
dwg = svgwrite.Drawing(path, size=(width, height))
|
| 73 |
+
|
| 74 |
+
buffer = BytesIO()
|
| 75 |
+
image.save(buffer, format="PNG")
|
| 76 |
+
encoded = base64.b64encode(buffer.getvalue()).decode()
|
| 77 |
+
|
| 78 |
+
dwg.add(
|
| 79 |
+
dwg.image(
|
| 80 |
+
href=f"data:image/png;base64,{encoded}",
|
| 81 |
+
insert=(0, 0),
|
| 82 |
+
size=(width, height),
|
| 83 |
+
)
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
dwg.save()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# -------------------------------------------------
|
| 90 |
+
# Generation
|
| 91 |
+
# -------------------------------------------------
|
| 92 |
+
|
| 93 |
+
def generate(
|
| 94 |
+
image,
|
| 95 |
+
patches,
|
| 96 |
+
max_size,
|
| 97 |
+
spacing,
|
| 98 |
+
border_thickness,
|
| 99 |
+
border_r,
|
| 100 |
+
border_g,
|
| 101 |
+
border_b,
|
| 102 |
+
use_padding,
|
| 103 |
+
pad_r,
|
| 104 |
+
pad_g,
|
| 105 |
+
pad_b,
|
| 106 |
+
supersample,
|
| 107 |
+
scale_mode,
|
| 108 |
+
dpi_value,
|
| 109 |
+
background_mode,
|
| 110 |
+
rounded,
|
| 111 |
+
clipping,
|
| 112 |
+
):
|
| 113 |
+
|
| 114 |
+
if image is None:
|
| 115 |
+
return None, None, None
|
| 116 |
+
|
| 117 |
+
border_color = rgb_to_hex(border_r, border_g, border_b)
|
| 118 |
+
padding_color = None
|
| 119 |
+
|
| 120 |
+
if use_padding:
|
| 121 |
+
padding_color = rgb_to_hex(pad_r, pad_g, pad_b)
|
| 122 |
+
|
| 123 |
+
mosaic, _ = make_vit_mosaic(
|
| 124 |
+
image,
|
| 125 |
+
target_total_patches=(patches,),
|
| 126 |
+
max_long_side=max_size,
|
| 127 |
+
spacing=spacing,
|
| 128 |
+
border_thickness=border_thickness,
|
| 129 |
+
border_color=border_color,
|
| 130 |
+
padding_color=padding_color,
|
| 131 |
+
supersample=supersample,
|
| 132 |
+
output_scale_mode=scale_mode,
|
| 133 |
+
rounded=rounded,
|
| 134 |
+
true_clipping=clipping,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
if background_mode == "White":
|
| 138 |
+
white_bg = Image.new("RGBA", mosaic.size, (255, 255, 255, 255))
|
| 139 |
+
white_bg.paste(mosaic, (0, 0), mosaic)
|
| 140 |
+
mosaic = white_bg
|
| 141 |
+
|
| 142 |
+
tmp_dir = tempfile.mkdtemp()
|
| 143 |
+
png_path = os.path.join(tmp_dir, "vit_mosaic.png")
|
| 144 |
+
svg_path = os.path.join(tmp_dir, "vit_mosaic.svg")
|
| 145 |
+
|
| 146 |
+
mosaic.save(png_path, dpi=(dpi_value, dpi_value))
|
| 147 |
+
export_svg(mosaic, svg_path)
|
| 148 |
+
|
| 149 |
+
return mosaic, png_path, svg_path
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# -------------------------------------------------
|
| 153 |
+
# UI
|
| 154 |
+
# -------------------------------------------------
|
| 155 |
+
|
| 156 |
+
with gr.Blocks() as demo:
|
| 157 |
+
|
| 158 |
+
gr.Markdown("# 🧩 ViT Patch Mosaic Generator")
|
| 159 |
+
|
| 160 |
+
with gr.Row():
|
| 161 |
+
|
| 162 |
+
# LEFT COLUMN
|
| 163 |
+
with gr.Column(scale=1):
|
| 164 |
+
|
| 165 |
+
gr.Markdown("### ✨ Example Images")
|
| 166 |
+
|
| 167 |
+
gallery = gr.Gallery(
|
| 168 |
+
value=[v["url"] for v in EXAMPLES.values()],
|
| 169 |
+
columns=3,
|
| 170 |
+
height=250,
|
| 171 |
+
allow_preview=False,
|
| 172 |
+
object_fit="contain",
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
gr.Markdown("### ⚙ Parameters")
|
| 176 |
+
|
| 177 |
+
patches = gr.Radio([12, 16], value=16, label="Number of patches")
|
| 178 |
+
|
| 179 |
+
max_size = gr.Slider(
|
| 180 |
+
128, 1024,
|
| 181 |
+
value=512,
|
| 182 |
+
step=64,
|
| 183 |
+
label="Max long side"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
spacing = gr.Slider(0, 40, value=12, label="Spacing")
|
| 187 |
+
|
| 188 |
+
border_thickness = gr.Slider(
|
| 189 |
+
0, 50,
|
| 190 |
+
value=14,
|
| 191 |
+
label="Border thickness"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
gr.Markdown("### 🎨 Border Color")
|
| 195 |
+
|
| 196 |
+
border_r = gr.Slider(0, 255, value=0, label="R")
|
| 197 |
+
border_g = gr.Slider(0, 255, value=255, label="G")
|
| 198 |
+
border_b = gr.Slider(0, 255, value=255, label="B")
|
| 199 |
+
|
| 200 |
+
color_preview = gr.HTML(update_color_preview(0, 255, 255))
|
| 201 |
+
|
| 202 |
+
with gr.Accordion("🧱 Padding Settings", open=False):
|
| 203 |
+
|
| 204 |
+
use_padding = gr.Checkbox(value=False, label="Enable padding color")
|
| 205 |
+
pad_r = gr.Slider(0, 255, value=255, label="Pad R")
|
| 206 |
+
pad_g = gr.Slider(0, 255, value=255, label="Pad G")
|
| 207 |
+
pad_b = gr.Slider(0, 255, value=255, label="Pad B")
|
| 208 |
+
|
| 209 |
+
with gr.Accordion("⚙ Advanced Settings", open=False):
|
| 210 |
+
|
| 211 |
+
rounded = gr.Checkbox(value=True, label="Enable rounded corners")
|
| 212 |
+
clipping = gr.Checkbox(value=True, label="True rounded clipping")
|
| 213 |
+
|
| 214 |
+
supersample = gr.Slider(1, 4, value=2, step=1)
|
| 215 |
+
scale_mode = gr.Radio(["keep", "downscale"], value="keep")
|
| 216 |
+
dpi_value = gr.Slider(72, 600, value=300, step=1)
|
| 217 |
+
background_mode = gr.Radio(["Transparent", "White"], value="Transparent")
|
| 218 |
+
|
| 219 |
+
generate_btn = gr.Button("Generate Mosaic")
|
| 220 |
+
|
| 221 |
+
# RIGHT COLUMN
|
| 222 |
+
with gr.Column(scale=1):
|
| 223 |
+
|
| 224 |
+
gr.Markdown("### 📥 Selected Image")
|
| 225 |
+
|
| 226 |
+
input_image = gr.Image(type="pil", height=250)
|
| 227 |
+
credit_display = gr.Markdown("")
|
| 228 |
+
|
| 229 |
+
gr.Markdown("### 🖼 Mosaic Preview")
|
| 230 |
+
|
| 231 |
+
output_image = gr.Image(type="pil", height=350)
|
| 232 |
+
|
| 233 |
+
download_png = gr.File(label="Download PNG")
|
| 234 |
+
download_svg = gr.File(label="Download SVG")
|
| 235 |
+
|
| 236 |
+
# -------------------------------------------------
|
| 237 |
+
# INTERACTIONS (IMPORTANT: OUTSIDE LAYOUT BLOCKS)
|
| 238 |
+
# -------------------------------------------------
|
| 239 |
+
|
| 240 |
+
gallery.select(
|
| 241 |
+
fn=on_gallery_select,
|
| 242 |
+
outputs=[input_image, credit_display],
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
border_r.change(update_color_preview, [border_r, border_g, border_b], color_preview)
|
| 246 |
+
border_g.change(update_color_preview, [border_r, border_g, border_b], color_preview)
|
| 247 |
+
border_b.change(update_color_preview, [border_r, border_g, border_b], color_preview)
|
| 248 |
+
|
| 249 |
+
rounded.change(toggle_clipping, rounded, clipping)
|
| 250 |
+
|
| 251 |
+
generate_btn.click(
|
| 252 |
+
fn=generate,
|
| 253 |
+
inputs=[
|
| 254 |
+
input_image,
|
| 255 |
+
patches,
|
| 256 |
+
max_size,
|
| 257 |
+
spacing,
|
| 258 |
+
border_thickness,
|
| 259 |
+
border_r,
|
| 260 |
+
border_g,
|
| 261 |
+
border_b,
|
| 262 |
+
use_padding,
|
| 263 |
+
pad_r,
|
| 264 |
+
pad_g,
|
| 265 |
+
pad_b,
|
| 266 |
+
supersample,
|
| 267 |
+
scale_mode,
|
| 268 |
+
dpi_value,
|
| 269 |
+
background_mode,
|
| 270 |
+
rounded,
|
| 271 |
+
clipping,
|
| 272 |
+
],
|
| 273 |
+
outputs=[output_image, download_png, download_svg],
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio~=4.44.0
|
| 2 |
+
numpy~=1.26.0
|
| 3 |
+
pillow~=10.4.0
|
| 4 |
+
svgwrite==1.4.3
|
| 5 |
+
requests==2.31.0
|
vit_mosaic.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
vit_mosaic.py
|
| 3 |
+
|
| 4 |
+
ViT-style patch mosaic generator
|
| 5 |
+
Supports:
|
| 6 |
+
- Auto grid selection (12 / 16 patches)
|
| 7 |
+
- Transparent or colored padding
|
| 8 |
+
- Rounded borders
|
| 9 |
+
- True rounded clipping
|
| 10 |
+
- Supersampling
|
| 11 |
+
- Downscale or keep resolution
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import math
|
| 15 |
+
import numpy as np
|
| 16 |
+
from PIL import Image, ImageDraw
|
| 17 |
+
from typing import Iterable, Tuple, Union
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
ColorType = Union[Tuple[int, int, int], str]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def parse_color(color: ColorType):
|
| 24 |
+
if isinstance(color, tuple):
|
| 25 |
+
return (*color, 255)
|
| 26 |
+
if isinstance(color, str):
|
| 27 |
+
color = color.strip()
|
| 28 |
+
if color.startswith("#"):
|
| 29 |
+
r = int(color[1:3], 16)
|
| 30 |
+
g = int(color[3:5], 16)
|
| 31 |
+
b = int(color[5:7], 16)
|
| 32 |
+
return (r, g, b, 255)
|
| 33 |
+
raise ValueError("Color must be RGB tuple or hex string '#RRGGBB'")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def make_vit_mosaic(
|
| 37 |
+
image: Image.Image,
|
| 38 |
+
target_total_patches: Iterable[int] = (12, 16),
|
| 39 |
+
max_long_side: int = 256,
|
| 40 |
+
spacing: int = 12,
|
| 41 |
+
border_thickness: int = 14,
|
| 42 |
+
border_color: ColorType = "#00FFFF",
|
| 43 |
+
padding_color: Union[None, ColorType] = None,
|
| 44 |
+
corner_radius: int = 22,
|
| 45 |
+
rounded: bool = True,
|
| 46 |
+
true_clipping: bool = True,
|
| 47 |
+
supersample: int = 1,
|
| 48 |
+
output_scale_mode: str = "keep", # "keep" or "downscale"
|
| 49 |
+
):
|
| 50 |
+
border_rgba = parse_color(border_color)
|
| 51 |
+
image = image.convert("RGBA")
|
| 52 |
+
w, h = image.size
|
| 53 |
+
|
| 54 |
+
scale = max_long_side / max(w, h)
|
| 55 |
+
new_w = int(w * scale)
|
| 56 |
+
new_h = int(h * scale)
|
| 57 |
+
image = image.resize((new_w, new_h), Image.LANCZOS)
|
| 58 |
+
|
| 59 |
+
aspect = new_w / new_h
|
| 60 |
+
best_choice = None
|
| 61 |
+
best_diff = float("inf")
|
| 62 |
+
|
| 63 |
+
for total in target_total_patches:
|
| 64 |
+
for rows in range(1, total + 1):
|
| 65 |
+
if total % rows == 0:
|
| 66 |
+
cols = total // rows
|
| 67 |
+
diff = abs((cols / rows) - aspect)
|
| 68 |
+
if diff < best_diff:
|
| 69 |
+
best_diff = diff
|
| 70 |
+
best_choice = (rows, cols)
|
| 71 |
+
|
| 72 |
+
rows, cols = best_choice
|
| 73 |
+
|
| 74 |
+
patch_w = math.ceil(new_w / cols)
|
| 75 |
+
patch_h = math.ceil(new_h / rows)
|
| 76 |
+
patch_size = max(patch_w, patch_h)
|
| 77 |
+
|
| 78 |
+
pad_w = patch_size * cols
|
| 79 |
+
pad_h = patch_size * rows
|
| 80 |
+
|
| 81 |
+
if padding_color is None:
|
| 82 |
+
canvas = Image.new("RGBA", (pad_w, pad_h), (0, 0, 0, 0))
|
| 83 |
+
else:
|
| 84 |
+
canvas = Image.new("RGBA", (pad_w, pad_h), parse_color(padding_color))
|
| 85 |
+
|
| 86 |
+
offset_x = (pad_w - new_w) // 2
|
| 87 |
+
offset_y = (pad_h - new_h) // 2
|
| 88 |
+
canvas.paste(image, (offset_x, offset_y), image)
|
| 89 |
+
|
| 90 |
+
arr = np.array(canvas, dtype=np.uint8)
|
| 91 |
+
|
| 92 |
+
patches = (
|
| 93 |
+
arr.reshape(rows, patch_size, cols, patch_size, 4)
|
| 94 |
+
.transpose(0, 2, 1, 3, 4)
|
| 95 |
+
.reshape(rows * cols, patch_size, patch_size, 4)
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
ss = max(1, supersample)
|
| 99 |
+
|
| 100 |
+
scaled_patch = patch_size * ss
|
| 101 |
+
scaled_border = border_thickness * ss
|
| 102 |
+
scaled_radius = corner_radius * ss
|
| 103 |
+
scaled_spacing = spacing * ss
|
| 104 |
+
|
| 105 |
+
tile_w = scaled_patch + 2 * scaled_border
|
| 106 |
+
tile_h = scaled_patch + 2 * scaled_border
|
| 107 |
+
|
| 108 |
+
mosaic_w = cols * tile_w + (cols + 1) * scaled_spacing
|
| 109 |
+
mosaic_h = rows * tile_h + (rows + 1) * scaled_spacing
|
| 110 |
+
|
| 111 |
+
mosaic = Image.new("RGBA", (mosaic_w, mosaic_h), (0, 0, 0, 0))
|
| 112 |
+
|
| 113 |
+
def create_tile(patch_img):
|
| 114 |
+
patch_img = patch_img.resize(
|
| 115 |
+
(scaled_patch, scaled_patch),
|
| 116 |
+
Image.NEAREST
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
tile = Image.new("RGBA", (tile_w, tile_h), (0, 0, 0, 0))
|
| 120 |
+
draw = ImageDraw.Draw(tile)
|
| 121 |
+
|
| 122 |
+
if rounded:
|
| 123 |
+
draw.rounded_rectangle(
|
| 124 |
+
[0, 0, tile_w - 1, tile_h - 1],
|
| 125 |
+
radius=scaled_radius,
|
| 126 |
+
fill=border_rgba,
|
| 127 |
+
)
|
| 128 |
+
else:
|
| 129 |
+
draw.rectangle(
|
| 130 |
+
[0, 0, tile_w - 1, tile_h - 1],
|
| 131 |
+
fill=border_rgba,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
if rounded and true_clipping:
|
| 135 |
+
mask = Image.new("L", (scaled_patch, scaled_patch), 0)
|
| 136 |
+
mask_draw = ImageDraw.Draw(mask)
|
| 137 |
+
mask_draw.rounded_rectangle(
|
| 138 |
+
[0, 0, scaled_patch - 1, scaled_patch - 1],
|
| 139 |
+
radius=max(0, scaled_radius - scaled_border),
|
| 140 |
+
fill=255,
|
| 141 |
+
)
|
| 142 |
+
tile.paste(patch_img, (scaled_border, scaled_border), mask)
|
| 143 |
+
else:
|
| 144 |
+
tile.paste(patch_img, (scaled_border, scaled_border), patch_img)
|
| 145 |
+
|
| 146 |
+
return tile
|
| 147 |
+
|
| 148 |
+
for idx in range(patches.shape[0]):
|
| 149 |
+
r = idx // cols
|
| 150 |
+
c = idx % cols
|
| 151 |
+
patch_img = Image.fromarray(patches[idx])
|
| 152 |
+
tile = create_tile(patch_img)
|
| 153 |
+
|
| 154 |
+
x = scaled_spacing + c * (tile_w + scaled_spacing)
|
| 155 |
+
y = scaled_spacing + r * (tile_h + scaled_spacing)
|
| 156 |
+
mosaic.paste(tile, (x, y), tile)
|
| 157 |
+
|
| 158 |
+
if ss > 1 and output_scale_mode == "downscale":
|
| 159 |
+
mosaic = mosaic.resize(
|
| 160 |
+
(mosaic_w // ss, mosaic_h // ss),
|
| 161 |
+
Image.LANCZOS
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
return mosaic, patches
|