| 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: |
| |
| 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) |
| |
| |
| geo = data.get("geometric", {}) |
| return { |
| "opacity": geo.get("alpha_mean", 0.0), |
| "aniso": geo.get("gamma_median", 0.0), |
| "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)) |
| |
| 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) |
|
|
| |
| 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)) |
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
| print("正在构建右侧 6 宫格面板...") |
|
|
| images = [] |
| labels = ["GT"] + METHODS |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
|
|
| |
| |
| |
| 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} (供快速预览)") |
|
|