SplatAtlas / tools /build_and_compose_fig1_v3.py
KCBtheone's picture
Upload SplatAtlas benchmark pipeline code
23e73f9 verified
Raw
History Blame Contribute Delete
11.1 kB
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
# =========================================================================
# Config
# =========================================================================
SCENE = 'truck'
REF_VIEW = '00014.png' # vanilla's view as reference for hash matching
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']
# =========================================================================
# Helpers
# =========================================================================
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()
# =========================================================================
# Step 1: build right panel 3x2 grid via PIL
# =========================================================================
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}")
# =========================================================================
# Layout
# =========================================================================
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
# top-left method label
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
)
# bottom-right PSNR, overlaid on the image
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
)
# bottom bar metrics
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
)
# =========================================================================
# Step 2: load left panel from PDF
# =========================================================================
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))
# =========================================================================
# Step 3: compose with matplotlib
# =========================================================================
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")