| import os, glob |
| import numpy as np |
| import matplotlib.pyplot as plt |
| from PIL import Image, ImageDraw, ImageFont |
| from plyfile import PlyData |
|
|
| try: |
| from pdf2image import convert_from_path |
| HAS_PDF2IMAGE = True |
| except ImportError: |
| HAS_PDF2IMAGE = False |
|
|
| |
| |
| |
| SCENE = 'truck' |
| REF_VIEW = '00014.png' |
| OUTPUTS_BASE = '/root/autodl-tmp/SplatAtlas/outputs' |
| ASSETS_DIR = '/root/autodl-tmp/SplatAtlas/outputs/phase5b/gallery_assets_31' |
| LEFT_PDF = '/root/autodl-tmp/SplatAtlas/outputs/phase5b/fig1_left_panel.pdf' |
| OUTPUT_PREFIX = '/root/autodl-tmp/SplatAtlas/tex/figures/fig1_saturation_puzzle' |
|
|
| METHODS = ['minisplatting', '3dgsmcmc', 'gof', 'pixelgs', 'vanilla_3dgs'] |
|
|
| |
| |
| |
| def get_valid_dir(method, scene): |
| for suffix in ["", "_bak"]: |
| d = f"{OUTPUTS_BASE}/{method}_{scene}{suffix}" |
| if os.path.exists(d) and len(glob.glob(f"{d}/gt_test*")) > 0: |
| return d |
| return None |
|
|
|
|
| def find_matching_view(cell_dir, ref_arr): |
| """ |
| Hash-match: find the file in this method's gt_test/ that depicts |
| the same viewpoint as ref_arr. Returns (filename, gt_arr) or (None, None). |
| """ |
| gt_dirs = sorted(glob.glob(f"{cell_dir}/gt_test*")) |
| if not gt_dirs: |
| return None, None |
|
|
| best_name, best_mse, best_arr = None, float('inf'), None |
|
|
| for f in sorted(glob.glob(f"{gt_dirs[-1]}/*.png")): |
| try: |
| cand = np.array(Image.open(f).convert('RGB')).astype(np.float32) |
|
|
| if cand.shape != ref_arr.shape: |
| continue |
|
|
| diff = np.abs(ref_arr - cand) |
| diff[diff > 240] = 0 |
| diff[np.abs(diff - 128) < 15] = 0 |
|
|
| mse = float(np.mean(diff ** 2)) |
|
|
| if mse < best_mse: |
| best_mse, best_name, best_arr = mse, os.path.basename(f), cand |
|
|
| except Exception: |
| continue |
|
|
| return (best_name, best_arr) if best_name and best_mse < 50.0 else (None, None) |
|
|
|
|
| def get_render_and_psnr(cell_dir, view_name, gt_arr): |
| if view_name is None or gt_arr is None: |
| return None, None |
|
|
| rdirs = sorted(glob.glob(f"{cell_dir}/renders_test*")) |
| if not rdirs: |
| return None, None |
|
|
| rp = f"{rdirs[-1]}/{view_name}" |
| if not os.path.exists(rp): |
| return None, None |
|
|
| try: |
| r_pil = Image.open(rp).convert('RGB') |
| r_arr = np.array(r_pil).astype(np.float32) |
|
|
| if r_arr.shape != gt_arr.shape: |
| return None, None |
|
|
| mse = float(np.mean((r_arr - gt_arr) ** 2)) |
| psnr = 100.0 if mse == 0 else 20 * np.log10(255.0 / np.sqrt(mse)) |
|
|
| return r_pil, psnr |
|
|
| except Exception: |
| return None, None |
|
|
|
|
| def get_ply_metrics(cell_dir): |
| ply_paths = sorted(glob.glob(f"{cell_dir}/point_cloud/iteration_*/point_cloud.ply")) |
| if not ply_paths: |
| return None |
|
|
| try: |
| v = PlyData.read(ply_paths[-1])['vertex'] |
|
|
| opacities = 1.0 / (1.0 + np.exp(-v['opacity'])) |
|
|
| scales_log = np.vstack([ |
| v['scale_0'], |
| v['scale_1'], |
| v['scale_2'] |
| ]) |
|
|
| scales_phys = np.exp(scales_log) |
|
|
| log_aniso = np.max(scales_log, axis=0) - np.min(scales_log, axis=0) |
|
|
| return { |
| 'alpha_med': float(np.median(opacities)), |
| 'scale_med': float(np.median(scales_phys)), |
| 'aniso_med': float(np.exp(np.median(log_aniso))), |
| 'n_gauss': int(scales_log.shape[1]), |
| } |
|
|
| except Exception: |
| return None |
|
|
|
|
| def fmt_aniso(a): |
| return f"{a:.1f}" if a < 100 else f"{a:,.0f}" |
|
|
|
|
| def safe_load_font(font_name, size): |
| try: |
| return ImageFont.truetype(font_name, size) |
| except Exception: |
| return ImageFont.load_default() |
|
|
|
|
| |
| |
| |
| print("[1/3] Building right panel (6 cells)...") |
|
|
| ref_path = f"{ASSETS_DIR}/gt_{SCENE}_{REF_VIEW}" |
| if not os.path.exists(ref_path): |
| raise SystemExit(f"Missing reference GT: {ref_path}") |
|
|
| ref_pil = Image.open(ref_path).convert('RGB') |
| ref_arr = np.array(ref_pil).astype(np.float32) |
|
|
| W, H = ref_pil.size |
|
|
| panels = [ |
| (ref_pil, 'GROUND TRUTH', None, None, True) |
| ] |
|
|
| for m in METHODS: |
| cell_dir = get_valid_dir(m, SCENE) |
|
|
| if cell_dir is None: |
| panels.append((Image.new('RGB', (W, H), (50, 50, 50)), m, None, None, False)) |
| print(f" [{m}] MISSING_DIR") |
| continue |
|
|
| view_name, gt_arr = find_matching_view(cell_dir, ref_arr) |
| img, psnr = get_render_and_psnr(cell_dir, view_name, gt_arr) |
|
|
| if img is None or img.size != (W, H): |
| fallback = f"{ASSETS_DIR}/{m}_{SCENE}_{REF_VIEW}" |
|
|
| if os.path.exists(fallback): |
| img = Image.open(fallback).convert('RGB') |
|
|
| if img.size == (W, H): |
| img_arr = np.array(img).astype(np.float32) |
| mse = float(np.mean((img_arr - ref_arr) ** 2)) |
| psnr = 100.0 if mse == 0 else 20 * np.log10(255.0 / np.sqrt(mse)) |
| else: |
| img = Image.new('RGB', (W, H), (50, 50, 50)) |
| psnr = None |
| else: |
| img = Image.new('RGB', (W, H), (50, 50, 50)) |
| psnr = None |
|
|
| metrics = get_ply_metrics(cell_dir) |
| panels.append((img, m, psnr, metrics, False)) |
|
|
| if psnr is not None: |
| print(f" [{m}] view={view_name} PSNR={psnr:.2f}") |
| else: |
| print(f" [{m}] view={view_name}") |
|
|
|
|
| |
| |
| |
| gap = 12 |
| bottom_bar = int(H * 0.11) |
|
|
| font_main = safe_load_font("DejaVuSans-Bold.ttf", int(H * 0.045)) |
| font_metric = safe_load_font("DejaVuSans-Bold.ttf", int(H * 0.030)) |
|
|
| canvas_w = W * 3 + gap * 2 |
| canvas_h = (H + bottom_bar) * 2 + gap |
|
|
| right_canvas = Image.new('RGB', (canvas_w, canvas_h), (30, 30, 30)) |
| draw = ImageDraw.Draw(right_canvas) |
|
|
| positions = [ |
| (0, 0), (1, 0), (2, 0), |
| (0, 1), (1, 1), (2, 1) |
| ] |
|
|
| for (img, name, psnr, metrics, is_gt), (col, row) in zip(panels, positions): |
| x = col * (W + gap) |
| y = row * (H + bottom_bar + gap) |
|
|
| right_canvas.paste(img, (x, y)) |
|
|
| pad = 8 |
|
|
| |
| label = name.upper() |
| bb = draw.textbbox((0, 0), label, font=font_main) |
| lw, lh = bb[2] - bb[0], bb[3] - bb[1] |
|
|
| draw.rectangle( |
| [x, y, x + lw + pad * 2, y + lh + pad * 2], |
| fill=(0, 0, 0) |
| ) |
|
|
| color = (255, 255, 255) if is_gt else (76, 175, 80) |
|
|
| draw.text( |
| (x + pad, y + pad), |
| label, |
| fill=color, |
| font=font_main |
| ) |
|
|
| |
| if is_gt: |
| psnr_text = "Reference" |
| elif psnr is not None: |
| psnr_text = f"PSNR: {psnr:.2f} dB" |
| else: |
| psnr_text = "PSNR: ---" |
|
|
| bb = draw.textbbox((0, 0), psnr_text, font=font_main) |
| pw, ph = bb[2] - bb[0], bb[3] - bb[1] |
|
|
| draw.rectangle( |
| [x + W - pw - pad * 2, y + H - ph - pad * 2, x + W, y + H], |
| fill=(0, 0, 0) |
| ) |
|
|
| draw.text( |
| (x + W - pw - pad, y + H - ph - pad), |
| psnr_text, |
| fill=(255, 255, 255), |
| font=font_main |
| ) |
|
|
| |
| if metrics is not None: |
| line1 = ( |
| f"Opacity μ: {metrics['alpha_med']:.3f} | " |
| f"Scale μ: {metrics['scale_med']:.4f} | " |
| f"N: {metrics['n_gauss'] / 1e6:.2f}M" |
| ) |
|
|
| line2 = f"Anisotropy μ: {fmt_aniso(metrics['aniso_med'])}" |
|
|
| b1 = draw.textbbox((0, 0), line1, font=font_metric) |
| b2 = draw.textbbox((0, 0), line2, font=font_metric) |
|
|
| l1h = b1[3] - b1[1] |
| l2h = b2[3] - b2[1] |
|
|
| total = l1h + l2h + 6 |
|
|
| t1y = y + H + (bottom_bar - total) // 2 |
| t2y = t1y + l1h + 6 |
|
|
| draw.text( |
| (x + (W - (b1[2] - b1[0])) // 2, t1y), |
| line1, |
| fill=(200, 200, 200), |
| font=font_metric |
| ) |
|
|
| draw.text( |
| (x + (W - (b2[2] - b2[0])) // 2, t2y), |
| line2, |
| fill=(255, 152, 0), |
| font=font_metric |
| ) |
|
|
| elif is_gt: |
| txt = "(reference)" |
| b = draw.textbbox((0, 0), txt, font=font_metric) |
|
|
| draw.text( |
| ( |
| x + (W - (b[2] - b[0])) // 2, |
| y + H + (bottom_bar - (b[3] - b[1])) // 2 |
| ), |
| txt, |
| fill=(150, 150, 150), |
| font=font_metric |
| ) |
|
|
|
|
| |
| |
| |
| print("[2/3] Loading left panel from PDF...") |
|
|
| if HAS_PDF2IMAGE and os.path.exists(LEFT_PDF): |
| try: |
| left_img = convert_from_path(LEFT_PDF, dpi=300)[0].convert('RGB') |
| except Exception as e: |
| print(f" WARNING: failed to load PDF with pdf2image: {e}") |
| print(" Placeholder used instead.") |
| left_img = Image.new('RGB', (1200, 900), (240, 240, 240)) |
| else: |
| print(f" WARNING: pdf2image missing or {LEFT_PDF} absent — placeholder used") |
| left_img = Image.new('RGB', (1200, 900), (240, 240, 240)) |
|
|
|
|
| |
| |
| |
| print("[3/3] Composing final figure...") |
|
|
| aspect_l = left_img.width / left_img.height |
| aspect_r = right_canvas.width / right_canvas.height |
|
|
| fig_w = 18.0 |
| fig_h = fig_w * 1.0 / (aspect_l + aspect_r + 0.05) |
|
|
| fig = plt.figure(figsize=(fig_w, fig_h + 0.5)) |
|
|
| gs = fig.add_gridspec( |
| 1, |
| 2, |
| width_ratios=[aspect_l, aspect_r], |
| wspace=0.05, |
| top=0.90, |
| bottom=0.02, |
| left=0.01, |
| right=0.99 |
| ) |
|
|
| ax1 = fig.add_subplot(gs[0]) |
| ax1.imshow(np.array(left_img)) |
| ax1.axis('off') |
|
|
| ax2 = fig.add_subplot(gs[1]) |
| ax2.imshow(np.array(right_canvas)) |
| ax2.axis('off') |
|
|
| ax1.text( |
| 0.0, |
| 1.015, |
| "(a) Per-seed PSNR rank instability · Bonsai & Lego", |
| transform=ax1.transAxes, |
| ha='left', |
| va='bottom', |
| fontsize=13, |
| fontweight='bold' |
| ) |
|
|
| ax2.text( |
| 0.0, |
| 1.015, |
| f"(b) Saturated cluster ({SCENE.title()}) · render vs. representation", |
| transform=ax2.transAxes, |
| ha='left', |
| va='bottom', |
| fontsize=13, |
| fontweight='bold' |
| ) |
|
|
| os.makedirs(os.path.dirname(OUTPUT_PREFIX), exist_ok=True) |
|
|
| plt.savefig( |
| f"{OUTPUT_PREFIX}.pdf", |
| dpi=300, |
| bbox_inches='tight', |
| format='pdf' |
| ) |
|
|
| plt.savefig( |
| f"{OUTPUT_PREFIX}.png", |
| dpi=300, |
| bbox_inches='tight', |
| format='png', |
| facecolor='white' |
| ) |
|
|
| plt.close() |
|
|
| print(f"\n✓ Saved: {OUTPUT_PREFIX}.pdf") |
| print(f"✓ Saved: {OUTPUT_PREFIX}.png") |
|
|