| """Generate paper-ready patch figures from a real TextVQA image. |
| |
| Outputs three groups, each in BOTH 2D (square, flat) and oblique (lying-flat |
| trapezoid with shadow) form: |
| |
| 1. Full 5x5 split — 25 patches, individual files (the source-image |
| decomposition the paper uses to recover the original). |
| 2. M ROI-highlighted patches — query-relevant patches with a red border. |
| 3. M attention-map figures — synthetic per-patch heatmap (jet colormap |
| overlaid on the patch) corresponding 1:1 with the ROI patches. |
| |
| No text / axes / arrows / decorations on any tile — strictly figures. |
| """ |
| from __future__ import annotations |
| import os |
| import numpy as np |
| from PIL import Image, ImageDraw, ImageFilter |
|
|
| |
| |
| |
| TEXTVQA_IMG = "/opt/tiger/thothvl_pretrain/doc/figures/textvqa_patch_tiles_src/camera_0.png" |
| NAS_OUT = "/mnt/bn/leonworkspace/terry/ce-task/figures/textvqa_patch_tiles" |
| LOCAL_OUT = "/opt/tiger/thothvl_pretrain/doc/figures/textvqa_patch_tiles" |
|
|
| |
| |
| |
| GRID = 5 |
| PATCH_PX = 180 |
|
|
| |
| TILE_W, TILE_H = 240, 200 |
| SHADOW_BLUR = 7 |
| SHADOW_OFFSET = (6, 10) |
| SHADOW_OPACITY = 110 |
| SHADOW_COLOR = (60, 65, 75) |
|
|
| |
| FLAT_W, FLAT_H = 190, 190 |
| FLAT_PADDING = 5 |
|
|
| |
| |
| |
| |
| |
| |
| ROI_PATCHES = [(1, 0), (1, 1), (1, 2)] |
| SINK_PATCHES = [(3, 0), (3, 1)] |
| M = len(ROI_PATCHES) |
| N_SINK = len(SINK_PATCHES) |
|
|
| ROI_BORDER_COLOR = (235, 60, 50, 255) |
| SINK_BORDER_COLOR = (90, 110, 140, 255) |
| BORDER_WIDTH = 7 |
| HEATMAP_ALPHA = 0.55 |
|
|
| |
| |
| |
| def perspective_coeffs(src_corners, dst_corners): |
| """Solve 8-coefficient PIL perspective transform mapping dst → src.""" |
| rows = [] |
| for (sx, sy), (dx, dy) in zip(src_corners, dst_corners): |
| rows.append([dx, dy, 1, 0, 0, 0, -sx * dx, -sx * dy]) |
| rows.append([0, 0, 0, dx, dy, 1, -sy * dx, -sy * dy]) |
| A = np.array(rows, dtype=np.float64) |
| B = np.array(src_corners, dtype=np.float64).reshape(8) |
| return tuple(np.linalg.solve(A, B)) |
|
|
|
|
| def trapezoid_corners(cw: int, ch: int): |
| """Symmetric trapezoid pulled toward the canvas edges.""" |
| return [ |
| (cw * 0.235, ch * 0.080), |
| (cw * 0.765, ch * 0.080), |
| (cw * 0.945, ch * 0.840), |
| (cw * 0.055, ch * 0.840), |
| ] |
|
|
|
|
| |
| |
| |
| def load_square_source(path: str, size: int) -> Image.Image: |
| im = Image.open(path).convert("RGB") |
| w, h = im.size |
| s = min(w, h) |
| left, top = (w - s) // 2, (h - s) // 2 |
| im = im.crop((left, top, left + s, top + s)) |
| return im.resize((size, size), Image.LANCZOS) |
|
|
|
|
| def jet_colormap(t: np.ndarray) -> np.ndarray: |
| """Approximate the matplotlib 'jet' colormap on a 2-D array t in [0,1]. |
| |
| Returns (H, W, 3) uint8. |
| """ |
| t = np.clip(t, 0.0, 1.0) |
| r = np.clip(1.5 - np.abs(4.0 * t - 3.0), 0.0, 1.0) |
| g = np.clip(1.5 - np.abs(4.0 * t - 2.0), 0.0, 1.0) |
| b = np.clip(1.5 - np.abs(4.0 * t - 1.0), 0.0, 1.0) |
| return (np.stack([r, g, b], axis=-1) * 255.0).astype(np.uint8) |
|
|
|
|
| def synth_heatmap(patch: Image.Image, peak_xy: tuple[float, float], |
| sigma_frac: float = 0.30) -> Image.Image: |
| """Render an attention-map overlay on a patch. |
| |
| A 2-D Gaussian centred at peak_xy (in [0,1]^2 patch coords) is colour |
| -mapped (jet) and alpha-blended onto the source patch. |
| """ |
| w, h = patch.size |
| y_idx, x_idx = np.mgrid[0:h, 0:w] |
| cx, cy = peak_xy[0] * w, peak_xy[1] * h |
| sigma = sigma_frac * max(w, h) |
| g = np.exp(-((x_idx - cx) ** 2 + (y_idx - cy) ** 2) / (2.0 * sigma ** 2)) |
| g = (g - g.min()) / (g.max() - g.min() + 1e-9) |
| cmap = jet_colormap(g) |
| base = np.asarray(patch.convert("RGB"), dtype=np.float32) |
| over = cmap.astype(np.float32) |
| blended = ((1.0 - HEATMAP_ALPHA) * base + HEATMAP_ALPHA * over) |
| return Image.fromarray(np.clip(blended, 0, 255).astype(np.uint8)) |
|
|
|
|
| def add_border(patch: Image.Image, color=ROI_BORDER_COLOR, |
| width: int = BORDER_WIDTH) -> Image.Image: |
| """Draw a thick coloured rectangle just inside the patch edges.""" |
| out = patch.convert("RGBA").copy() |
| d = ImageDraw.Draw(out) |
| w, h = out.size |
| d.rectangle([(width // 2, width // 2), |
| (w - width // 2 - 1, h - width // 2 - 1)], |
| outline=color, width=width) |
| return out |
|
|
|
|
| |
| |
| |
| def render_oblique(patch_img: Image.Image) -> Image.Image: |
| cw, ch = TILE_W, TILE_H |
| dst = trapezoid_corners(cw, ch) |
| src = [(0, 0), (patch_img.width, 0), |
| (patch_img.width, patch_img.height), (0, patch_img.height)] |
| coeffs = perspective_coeffs(src, dst) |
| warped = patch_img.convert("RGBA").transform( |
| (cw, ch), Image.PERSPECTIVE, coeffs, resample=Image.BICUBIC, |
| ) |
| bg = Image.new("RGBA", (cw, ch), (0, 0, 0, 0)) |
| shadow_quad = [(x + SHADOW_OFFSET[0], y + SHADOW_OFFSET[1]) for x, y in dst] |
| sm = Image.new("L", (cw, ch), 0) |
| ImageDraw.Draw(sm).polygon(shadow_quad, fill=SHADOW_OPACITY) |
| sm = sm.filter(ImageFilter.GaussianBlur(SHADOW_BLUR)) |
| sl = np.zeros((ch, cw, 4), dtype=np.uint8) |
| sl[:, :, 0:3] = SHADOW_COLOR |
| sl[:, :, 3] = np.asarray(sm, dtype=np.uint8) |
| bg = Image.alpha_composite(bg, Image.fromarray(sl, "RGBA")) |
| bg = Image.alpha_composite(bg, warped) |
| return bg |
|
|
|
|
| def render_flat(patch_img: Image.Image) -> Image.Image: |
| """Square 2-D tile, no perspective, transparent background.""" |
| p = patch_img.convert("RGBA") |
| inner = FLAT_W - 2 * FLAT_PADDING |
| p = p.resize((inner, inner), Image.LANCZOS) |
| bg = Image.new("RGBA", (FLAT_W, FLAT_H), (0, 0, 0, 0)) |
| bg.paste(p, (FLAT_PADDING, FLAT_PADDING), p) |
| return bg |
|
|
|
|
| |
| |
| |
| def save_pair(im: Image.Image, name: str): |
| """Save PNG with alpha (modest resolution for paper figures).""" |
| for d in (NAS_OUT, LOCAL_OUT): |
| im.save(os.path.join(d, f"{name}.png"), dpi=(150, 150)) |
|
|
|
|
| |
| |
| |
| def draw_grid(img: Image.Image, n: int = GRID, |
| color=(255, 255, 255, 230), width: int = 4) -> Image.Image: |
| """Draw n×n grid lines on top of a square source image.""" |
| out = img.convert("RGBA").copy() |
| d = ImageDraw.Draw(out) |
| w, h = out.size |
| for k in range(1, n): |
| x = round(k * w / n) |
| y = round(k * h / n) |
| d.line([(x, 0), (x, h)], fill=color, width=width) |
| d.line([(0, y), (w, y)], fill=color, width=width) |
| return out |
|
|
|
|
| def render_all(): |
| os.makedirs(NAS_OUT, exist_ok=True) |
| os.makedirs(LOCAL_OUT, exist_ok=True) |
|
|
| src_size = PATCH_PX * GRID |
| sq = load_square_source(TEXTVQA_IMG, src_size) |
| print(f"[src] {TEXTVQA_IMG} -> {src_size}x{src_size} square") |
|
|
| |
| |
| full_grid = draw_grid(sq) |
| full_flat = full_grid.resize((600, 600), Image.LANCZOS) |
| save_pair(full_flat, "source_grid_flat") |
|
|
| |
| |
| big_w, big_h = 720, 280 |
| dst = [ |
| (big_w * 0.300, big_h * 0.120), |
| (big_w * 0.700, big_h * 0.120), |
| (big_w * 0.970, big_h * 0.880), |
| (big_w * 0.030, big_h * 0.880), |
| ] |
| src = [(0, 0), (full_grid.width, 0), |
| (full_grid.width, full_grid.height), (0, full_grid.height)] |
| coeffs = perspective_coeffs(src, dst) |
| warped = full_grid.convert("RGBA").transform( |
| (big_w, big_h), Image.PERSPECTIVE, coeffs, resample=Image.BICUBIC) |
| bg = Image.new("RGBA", (big_w, big_h), (0, 0, 0, 0)) |
| sm = Image.new("L", (big_w, big_h), 0) |
| ImageDraw.Draw(sm).polygon( |
| [(x + SHADOW_OFFSET[0] * 2, y + SHADOW_OFFSET[1] * 2) for x, y in dst], |
| fill=SHADOW_OPACITY) |
| sm = sm.filter(ImageFilter.GaussianBlur(SHADOW_BLUR * 2)) |
| sl = np.zeros((big_h, big_w, 4), dtype=np.uint8) |
| sl[:, :, 0:3] = SHADOW_COLOR |
| sl[:, :, 3] = np.asarray(sm, dtype=np.uint8) |
| bg = Image.alpha_composite(bg, Image.fromarray(sl, "RGBA")) |
| bg = Image.alpha_composite(bg, warped) |
| save_pair(bg, "source_grid_oblique") |
| |
| save_pair(sq.resize((600, 600), Image.LANCZOS), "source_flat") |
| print(f" source — flat + oblique (with and without grid)") |
|
|
| |
| for i in range(GRID): |
| for j in range(GRID): |
| x0, y0 = j * PATCH_PX, i * PATCH_PX |
| patch = sq.crop((x0, y0, x0 + PATCH_PX, y0 + PATCH_PX)) |
| save_pair(render_oblique(patch), f"split_{i}_{j}_oblique") |
| save_pair(render_flat(patch), f"split_{i}_{j}_flat") |
| print(f" split ({i},{j}) — oblique + flat") |
|
|
| |
| for k, (i, j) in enumerate(ROI_PATCHES): |
| x0, y0 = j * PATCH_PX, i * PATCH_PX |
| patch = sq.crop((x0, y0, x0 + PATCH_PX, y0 + PATCH_PX)) |
|
|
| |
| roi = add_border(patch, color=ROI_BORDER_COLOR) |
| save_pair(render_oblique(roi), f"roi_{k}_oblique") |
| save_pair(render_flat(roi), f"roi_{k}_flat") |
|
|
| |
| |
| |
| att = synth_heatmap(patch, peak_xy=(0.5, 0.5), sigma_frac=0.28) |
| save_pair(render_oblique(att), f"attn_{k}_oblique") |
| save_pair(render_flat(att), f"attn_{k}_flat") |
| print(f" ROI {k} = grid({i},{j}) — roi + attn (oblique + flat)") |
|
|
| |
| for k, (i, j) in enumerate(SINK_PATCHES): |
| x0, y0 = j * PATCH_PX, i * PATCH_PX |
| patch = sq.crop((x0, y0, x0 + PATCH_PX, y0 + PATCH_PX)) |
| sink = add_border(patch, color=SINK_BORDER_COLOR) |
| save_pair(render_oblique(sink), f"sink_{k}_oblique") |
| save_pair(render_flat(sink), f"sink_{k}_flat") |
| print(f" SINK {k} = grid({i},{j}) — sink (oblique + flat)") |
|
|
| print(f"[done] wrote split + ROI + attn + sink → {NAS_OUT}") |
|
|
|
|
| if __name__ == "__main__": |
| render_all() |
|
|