SplatAtlas / tools /build_and_compose_fig1.py
KCBtheone's picture
Upload SplatAtlas benchmark pipeline code
23e73f9 verified
Raw
History Blame Contribute Delete
8.6 kB
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont
import os
import glob
import json
import numpy as np
from pdf2image import convert_from_path
# =========================================================================
# 配置项
# =========================================================================
SCENE = "bonsai"
VIEW = "00000.png"
OUTPUTS_BASE = "/root/autodl-tmp/SplatAtlas/outputs"
METHODS = ["vanilla_3dgs", "pgsr", "ges", "opti3dgs", "steepgs"]
LEFT_PDF_PATH = "/root/autodl-tmp/SplatAtlas/outputs/phase5b/fig1_left_panel.pdf"
OUTPUT_PREFIX = "/root/autodl-tmp/SplatAtlas/tex/figures/fig1_saturation_puzzle"
# =========================================================================
# 辅助函数
# =========================================================================
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}/renders_test*")) > 0:
return d
return None
def get_metrics(cell_dir):
try:
# 尝试读取 metrics json 以获取底栏数据
metric_files = sorted(glob.glob(f"{cell_dir}/metrics_test_iter*.json"))
if not metric_files: return None
with open(metric_files[-1], 'r') as f:
data = json.load(f)
# 根据你之前的截图,提取 alpha_mean 和 gamma_median
# 如果你已经按我建议改成了倒数,请自行在此处调整计算
geo = data.get("geometric", {})
return {
"opacity": geo.get("alpha_mean", 0.0),
"aniso": geo.get("gamma_median", 0.0), # 如果是倒数可能是 condition_number 等
"psnr": data.get("photometric", {}).get("PSNR", 0.0)
}
except Exception:
return None
def add_label(img, title, psnr, opacity=None, aniso=None, is_gt=False):
"""在图片上绘制带有半透明黑底的白字标签 (模拟截图效果)"""
draw = ImageDraw.Draw(img, 'RGBA')
w, h = img.size
# 标题 (顶部居中)
title_text = title.upper()
font_size = max(16, int(h * 0.03))
# 简单回退:如果找不到系统字体,使用默认字体。
# 为了更好效果,这里尽量用比较大的字号,由于默认字体不可缩放,我们可能需要绘制矩形框来显眼
try:
font = ImageFont.truetype("DejaVuSans-Bold.ttf", font_size)
except:
font = ImageFont.load_default()
# 标题底框
padding = 5
title_bbox = draw.textbbox((0, 0), title_text, font=font)
tw, th = title_bbox[2] - title_bbox[0], title_bbox[3] - title_bbox[1]
title_x = (w - tw) // 2
title_y = 10
draw.rectangle([title_x - padding, title_y - padding,
title_x + tw + padding, title_y + th + padding],
fill=(0, 0, 0, 180))
# GT 用白色,Method 用绿色
title_color = (255, 255, 255) if is_gt else (100, 255, 100)
draw.text((title_x, title_y), title_text, font=font, fill=title_color)
# 底部 PSNR 标签 (右下)
if psnr:
psnr_text = f"PSNR: {psnr:.2f} dB" if not is_gt else "Reference"
p_bbox = draw.textbbox((0, 0), psnr_text, font=font)
pw, ph = p_bbox[2] - p_bbox[0], p_bbox[3] - p_bbox[1]
px = w - pw - 10
py = h - ph - 40 # 留出底栏空间
draw.rectangle([px - padding, py - padding,
px + pw + padding, py + ph + padding],
fill=(0, 0, 0, 180))
draw.text((px, py), psnr_text, font=font, fill=(255,255,255))
# 底部黑条 (Opacity & Aniso)
if opacity is not None and aniso is not None:
bar_h = 30
draw.rectangle([0, h - bar_h, w, h], fill=(30, 30, 30, 255))
stat_text = f"opacity μ: {opacity:.3f} | anisotropy μ: {aniso:.3f}" if not is_gt else "opacity μ: 1.000 | anisotropy μ: 1.000"
s_bbox = draw.textbbox((0, 0), stat_text, font=font)
sw, sh = s_bbox[2] - s_bbox[0], s_bbox[3] - s_bbox[1]
draw.text(((w-sw)//2, h - bar_h + (bar_h-sh)//2), stat_text, font=font, fill=(200,200,200))
return img
# =========================================================================
# 步骤 1: 构建右侧 6 宫格图片
# =========================================================================
print("正在构建右侧 6 宫格面板...")
images = []
labels = ["GT"] + METHODS
# 提取 GT
gt_dir = get_valid_dir("vanilla_3dgs", SCENE)
if gt_dir:
gt_path = os.path.join(gt_dir, "gt_test_30000", VIEW)
if not os.path.exists(gt_path):
gt_path = os.path.join(gt_dir, "gt_test", VIEW)
if os.path.exists(gt_path):
img_gt = Image.open(gt_path).convert("RGBA")
img_gt = add_label(img_gt, "GT", None, opacity=1.0, aniso=1.0, is_gt=True)
images.append(img_gt)
else:
print(f"❌ 找不到 GT 图像: {gt_path}")
exit(1)
else:
print("❌ 找不到 vanilla_3dgs 目录以提取 GT")
exit(1)
# 提取 Methods
for method in METHODS:
cell_dir = get_valid_dir(method, SCENE)
if not cell_dir:
print(f"⚠️ 找不到方法目录: {method}")
# 创建占位黑图
img_placeholder = Image.new("RGBA", images[0].size, (50,50,50,255))
images.append(img_placeholder)
continue
render_dirs = sorted(glob.glob(f"{cell_dir}/renders_test*"))
if not render_dirs:
print(f"⚠️ 找不到渲染目录: {method}")
images.append(Image.new("RGBA", images[0].size, (50,50,50,255)))
continue
img_path = os.path.join(render_dirs[-1], VIEW)
if not os.path.exists(img_path):
print(f"⚠️ 找不到图像文件: {img_path}")
images.append(Image.new("RGBA", images[0].size, (50,50,50,255)))
continue
img_m = Image.open(img_path).convert("RGBA")
# 提取底栏数据
metrics = get_metrics(cell_dir)
if metrics:
img_m = add_label(img_m, method, metrics['psnr'], metrics['opacity'], metrics['aniso'])
else:
img_m = add_label(img_m, method, 0.0)
images.append(img_m)
# 拼接 6 宫格
iw, ih = images[0].size
padding = 10
grid_w = iw * 3 + padding * 2
grid_h = ih * 2 + padding
img_right = Image.new("RGBA", (grid_w, grid_h), (30,30,30,255))
positions = [
(0, 0), (iw + padding, 0), ((iw + padding) * 2, 0),
(0, ih + padding), (iw + padding, ih + padding), ((iw + padding) * 2, ih + padding)
]
for img, pos in zip(images, positions):
img_right.paste(img, pos)
# =========================================================================
# 步骤 2: 与左侧 PDF 拼接
# =========================================================================
print("正在从 PDF 提取左侧面板 (DPI=300)...")
if not os.path.exists(LEFT_PDF_PATH):
print(f"❌ 找不到左图 (PDF): {LEFT_PDF_PATH}")
exit(1)
try:
pages = convert_from_path(LEFT_PDF_PATH, dpi=300)
img_left = pages[0]
except Exception as e:
print(f"❌ PDF 转换失败: {e}\n请确保已安装 poppler-utils")
exit(1)
print("正在拼接最终图像...")
target_height = 1500
aspect_l = img_left.width / img_left.height
aspect_r = img_right.width / img_right.height
new_w_l = int(target_height * aspect_l)
new_w_r = int(target_height * aspect_r)
img_left = img_left.resize((new_w_l, target_height), Image.Resampling.LANCZOS)
img_right = img_right.resize((new_w_r, target_height), Image.Resampling.LANCZOS)
fig, (ax1, ax2) = plt.subplots(
1, 2,
figsize=(18, 6),
gridspec_kw={'width_ratios': [aspect_l, aspect_r]}
)
ax1.imshow(img_left)
ax1.axis('off')
ax1.set_title("(a) Per-seed PSNR Rank Instability (BONSAI & LEGO)",
fontsize=18, pad=12, loc='left', fontweight='bold')
ax2.imshow(img_right)
ax2.axis('off')
ax2.set_title(f"(b) Saturated Cluster ({SCENE.upper()}): Render vs. Representation",
fontsize=18, pad=12, loc='left', fontweight='bold')
plt.subplots_adjust(wspace=0.03)
os.makedirs(os.path.dirname(OUTPUT_PREFIX), exist_ok=True)
pdf_path = f"{OUTPUT_PREFIX}.pdf"
png_path = f"{OUTPUT_PREFIX}.png"
print("正在保存最终拼接文件...")
plt.savefig(pdf_path, dpi=300, bbox_inches='tight', format='pdf', transparent=True)
plt.savefig(png_path, dpi=300, bbox_inches='tight', format='png', transparent=False)
print(f"=== Figure 1 (Saturation Puzzle) 拼图合成完毕 ===")
print(f"✅ Saved PDF: {pdf_path} (供 LaTeX 插入)")
print(f"✅ Saved PNG: {png_path} (供快速预览)")