daidedou commited on
Commit
856dd67
·
1 Parent(s): eb67e2d

Iniital commit

Browse files
Files changed (3) hide show
  1. app.py +277 -0
  2. requirements.txt +5 -0
  3. 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