File size: 8,239 Bytes
b041b2b bdaf195 b041b2b df29645 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b bdaf195 b041b2b 4f2f2dd bdaf195 4f2f2dd bdaf195 4f2f2dd bdaf195 4f2f2dd b041b2b 4f2f2dd b041b2b 4f2f2dd b041b2b 4f2f2dd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# simple_mosaic.py
from pathlib import Path
from typing import List, Tuple, Iterator
import numpy as np
from PIL import Image, ImageDraw
from tile_library import build_cifar10_tile_library, build_cifar100_tile_library
class SimpleMosaicImage:
def __init__(self, path: str):
self.path = path
self.img = Image.open(path).convert("RGB")
self.width, self.height = self.img.size
print(f"[INFO] Loaded: {path} | size={self.width}x{self.height}")
def resize(self, longest_side: int = 4000) -> "SimpleMosaicImage":
w, h = self.width, self.height
scale = longest_side / max(w, h)
if scale < 1.0:
new_w = int(w * scale)
new_h = int(h * scale)
self.img = self.img.resize((new_w, new_h), Image.BICUBIC)
self.width, self.height = new_w, new_h
print(f"[INFO] Resized to {new_w}x{new_h}")
return self
def quantize_colors(self, n_colors: int = 16) -> "SimpleMosaicImage":
"""Apply color quantization using PIL's built-in algorithm"""
quantized = self.img.quantize(colors=n_colors, method=Image.MEDIANCUT)
self.img = quantized.convert('RGB')
print(f"[INFO] Color quantized to {n_colors} colors")
return self
def crop_to_grid(self, grid_size: int = 32) -> "SimpleMosaicImage":
"""Smart boundary handling: preserve original size when possible"""
# Only crop if loss is minimal (<2%), otherwise preserve original size
new_w = (self.width // grid_size) * grid_size
new_h = (self.height // grid_size) * grid_size
lost_pixels = (self.width - new_w) + (self.height - new_h)
total_pixels = self.width + self.height
loss_ratio = lost_pixels / total_pixels
if loss_ratio < 0.02: # Only crop if loss < 2%
self.img = self.img.crop((0, 0, new_w, new_h))
self.width, self.height = new_w, new_h
print(f"[INFO] Cropped to {new_w}x{new_h} for grid {grid_size} (loss: {loss_ratio:.1%})")
else:
print(f"[INFO] Preserved original size {self.width}x{self.height} (would lose {loss_ratio:.1%})")
return self
def _as_array(self):
return np.asarray(self.img, dtype=np.uint8)
def iter_cells(self, grid_size: int):
for y in range(0, self.height, grid_size):
for x in range(0, self.width, grid_size):
yield (x, y, grid_size, grid_size)
def draw_cells(self, cells, outline=(255, 0, 0), width=0.1):
"""
Draw cell borders on the original image, returns a new image.
outline: border color
width: border line width
"""
canvas = self.img.copy()
draw = ImageDraw.Draw(canvas)
for (x, y, w, h) in cells:
# PIL rectangle bottom-right is inclusive, -1 to avoid overflow
draw.rectangle((x, y, x + w - 1, y + h - 1), outline=outline, width=width)
return canvas
@staticmethod
def _cell_mean(arr, x, y, w, h):
block = arr[y:y+h, x:x+w, :]
mean = block.mean(axis=(0,1))
return tuple(int(round(v)) for v in mean)
@staticmethod
def _nearest_color(target, palette):
tr, tg, tb = target
pal = np.array(palette, dtype=np.int16)
diff = pal - np.array([tr, tg, tb], dtype=np.int16)
dist2 = np.sum(diff*diff, axis=1)
idx = int(np.argmin(dist2))
return tuple(int(v) for v in pal[idx])
def build_adaptive_cells(
self,
start_size: int = 64,
min_size: int = 16,
threshold: float = 20.0, # Use grayscale variance as complexity measure
) -> list[tuple[int,int,int,int]]:
"""
Returns [(x,y,w,h), ...]: Quadtree-style adaptive grid using iterative stack.
Requirement: Image should be resized/cropped to be divisible by start_size for better alignment.
"""
arr = self._as_array()
# Grayscale (BT.601)
gray = (0.299*arr[...,0] + 0.587*arr[...,1] + 0.114*arr[...,2]).astype(np.float32)
cells: list[tuple[int,int,int,int]] = []
# First rough division by start_size, push large blocks to stack
stack: list[tuple[int,int,int,int]] = []
for yy in range(0, self.height, start_size):
for xx in range(0, self.width, start_size):
ww = min(start_size, self.width - xx)
hh = min(start_size, self.height - yy)
stack.append((xx, yy, ww, hh))
# Process stack: decide whether to keep or subdivide into 4 blocks
while stack:
x, y, w, h = stack.pop()
# Keep if reached minimum size
if w <= min_size or h <= min_size:
cells.append((x, y, w, h))
continue
# Complexity: grayscale variance
region = gray[y:y+h, x:x+w]
score = float(region.var())
# Below threshold -> keep without subdivision
if score < threshold:
cells.append((x, y, w, h))
continue
# Otherwise subdivide into 4 blocks (try to halve), handle boundary remainder
w2 = max(min_size, w // 2)
h2 = max(min_size, h // 2)
# Fallback: keep if cannot subdivide further (avoid infinite loop)
if w2 == w and h2 == h:
cells.append((x, y, w, h))
continue
# Top-left
stack.append((x, y, w2, h2))
# Top-right
x2 = x + w2
wR = min(w - w2, self.width - x2)
if wR > 0:
stack.append((x2, y, wR, h2))
# Bottom-left
y2 = y + h2
hB = min(h - h2, self.height - y2)
if hB > 0:
stack.append((x, y2, w2, hB))
# Bottom-right
if wR > 0 and hB > 0:
stack.append((x2, y2, wR, hB))
return cells
def mosaic_average_color_adaptive(self, cells):
arr = self._as_array()
out = np.empty_like(arr)
for (x, y, w, h) in cells:
color = self._cell_mean(arr, x, y, w, h)
out[y:y+h, x:x+w, :] = color
return Image.fromarray(out, mode="RGB")
def mosaic_with_tiles_adaptive(self, cells, tiles, tile_means: np.ndarray):
"""
Adaptive grid version: pass in cells from build_adaptive_cells.
"""
out_img = Image.new("RGB", (self.width, self.height))
arr = self._as_array()
means = tile_means.astype(np.float32)
for (x, y, w, h) in cells:
block_mean = np.array(self._cell_mean(arr, x, y, w, h), dtype=np.float32)
diff = means - block_mean[None, :]
idx = int(np.argmin(np.sum(diff*diff, axis=1)))
tile = tiles[idx].resize((w, h), Image.BILINEAR)
out_img.paste(tile, (x, y))
return out_img
def save(self, image: Image.Image, out_path: str) -> None:
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
image.save(out_path)
print(f"[INFO] Saved: {out_path}")
# loader = SimpleMosaicImage("./samples/akaza.jpg")
# loader.quantize_colors(16).crop_to_grid(2)
# cells = loader.build_adaptive_cells(
# start_size=64, # Initial block size
# min_size=4, # Minimum block size
# threshold=5.0 # Lower value = more subdivision
# )
# tiles_10, tile_means_10, tile_labels_10 = build_cifar10_tile_library(max_per_class=1000)
# tiles_100, tile_means_100, tile_labels_100 = build_cifar100_tile_library(max_per_class=400)
# vis1 = loader.draw_cells(cells, outline=(0, 255, 0), width=1)
# vis1.save("./out/cells_outline_akaza.png")
# mosaic_adapt = loader.mosaic_average_color_adaptive(cells)
# loader.save(mosaic_adapt, "./out/mosaic_adaptive_akaza.png")
# mosaic_tiles_adapt_10 = loader.mosaic_with_tiles_adaptive(cells, tiles=tiles_10, tile_means=tile_means_10)
# loader.save(mosaic_tiles_adapt_10, "./out/mosaic_cifar_adaptive_akaza_10.png")
# mosaic_tiles_adapt_100 = loader.mosaic_with_tiles_adaptive(cells, tiles=tiles_100, tile_means=tile_means_100)
# loader.save(mosaic_tiles_adapt_100, "./out/mosaic_cifar_adaptive_akaza_100.png") |