| import os |
|
|
| import matplotlib.animation as animation |
| import matplotlib.pyplot as plt |
|
|
| from optgs.misc.io import CustomPath |
| from optgs.model.types import Gaussians |
|
|
| plt.rcParams.update({'font.size': 18, |
| |
| 'lines.linewidth': 6, |
| }) |
|
|
| import matplotlib.gridspec as gridspec |
| import subprocess |
| from torch import Tensor |
|
|
|
|
| def calc_hist(values, bins=100, density=True): |
| """Utility: return (x, y) for a histogram.""" |
| v = values.detach().cpu().numpy().flatten() |
| y, x = np.histogram(v, bins=bins, density=density) |
| x = 0.5 * (x[:-1] + x[1:]) |
| return x, y |
|
|
|
|
| def plot_gaussians_params_histograms( |
| data_groups: dict[str, list[Tensor]], |
| psnrs, |
| iters, |
| out_path=CustomPath("dashboard.mp4"), |
| max_frames=None, |
| last_k_hist=5, |
| save_last_time_only=False, |
| save_video=False |
| ): |
| """ |
| Create a dashboard video visualizing parameter distributions and PSNR over iterations. |
| Shows histograms of the last K iterations with color fading for comparison. |
| """ |
|
|
| |
| assert not (save_video and save_last_time_only), "Cannot save video when save_last_time_only is True." |
|
|
| |
| sh_d = data_groups["shs"][0].shape[-1] // 3 |
| param_axis_names = { |
| "opacities": [""], |
| "means": ["x", "y", "z"], |
| "scales": ["x", "y", "z"], |
| "quats": ["x", "y", "z", "w"], |
| "shs": [f"r{i}" for i in range(sh_d)] |
| + [f"g{i}" for i in range(sh_d)] |
| + [f"b{i}" for i in range(sh_d)], |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| T = len(iters) |
| if max_frames is not None: |
| T = min(T, max_frames) |
|
|
| |
| total_dims = sum(g[0].shape[-1] for g in data_groups.values()) |
| ncols = 5 |
| nrows = int(np.ceil(total_dims / ncols)) |
|
|
| fig = plt.figure(figsize=(5 * ncols, 3.5 * (nrows + 1))) |
| gs = gridspec.GridSpec(nrows + 1, ncols, height_ratios=[1] * nrows + [0.5]) |
| axes = [fig.add_subplot(gs[i // ncols, i % ncols]) for i in range(nrows * ncols)] |
| ax_psnr = fig.add_subplot(gs[-1, :]) |
|
|
| |
| print("🔍 Precomputing histograms and axis limits...") |
| subplot_map = [] |
| i = 0 |
| for key, iters_params in data_groups.items(): |
|
|
| D = iters_params.shape[-1] |
|
|
| coord_names = [f"{key} {param_axis_names[key][d]}" for d in range(D)] |
|
|
| for d in range(D): |
| |
| all_hist_data = [calc_hist(iters_params[t][..., d], density=True) for t in range(T)] |
| all_x, all_y = zip(*all_hist_data) |
| xmin = min(x.min() for x in all_x) |
| xmax = max(x.max() for x in all_x) |
| |
| x_max_abs = max(abs(xmin), abs(xmax)) |
| xmin, xmax = -x_max_abs, x_max_abs |
| ymin = 0.0 |
| ymax = max(y.max() for y in all_y) * 1.1 |
| subplot_map.append((key, d, axes[i], coord_names[d], all_x, all_y, xmin, xmax, ymin, ymax)) |
| i += 1 |
|
|
| |
| total_used_subplots = len(subplot_map) |
| for j in range(total_used_subplots, len(axes)): |
| axes[j].set_visible(False) |
|
|
| |
| out_dir = out_path.parent |
| inter_dir = out_dir / "gaussians_histograms" |
| inter_dir.mkdir(parents=True, exist_ok=True) |
|
|
| print(f"📸 Generating histograms frames in {inter_dir:link}") |
|
|
| |
| for frame_idx in range(T): |
|
|
| if save_last_time_only and frame_idx < T - 1: |
| continue |
|
|
| fig.suptitle(f"Iteration {iters[frame_idx]} — PSNR: {psnrs[frame_idx]:.2f}", fontsize=18) |
|
|
| for key, d, ax, name, all_x, all_y, xmin, xmax, ymin, ymax in subplot_map: |
| ax.clear() |
|
|
| |
| k = min(last_k_hist, frame_idx + 1) |
| idxs = list(range(frame_idx - k + 1, frame_idx + 1)) |
| for rel_i, hist_idx in enumerate(idxs): |
| color = plt.cm.viridis(rel_i / max(1, k - 1)) |
| label = f"Iter {iters[hist_idx]}" |
| ax.plot(all_x[hist_idx], all_y[hist_idx], color=color, alpha=0.9, lw=6, label=label) |
|
|
| ax.set_xlim(xmin, xmax) |
| ax.set_ylim(ymin, ymax) |
| ax.set_title(name) |
| ax.legend(frameon=False, loc="upper right", fontsize=7) |
| ax.grid(True, linestyle='--', alpha=0.5) |
| |
| ax.axvline(0, color='black', linewidth=1, linestyle=':', alpha=0.7) |
|
|
| |
| ax_psnr.clear() |
| ax_psnr.plot(iters[:frame_idx + 1], psnrs[:frame_idx + 1], color="#ffbc42", linewidth=8) |
| ax_psnr.scatter(iters[frame_idx], psnrs[frame_idx], color="#ffbc42", s=60, zorder=3, linewidth=8) |
| ax_psnr.set_xlim(min(iters), max(iters)) |
| ax_psnr.set_ylim(max(psnrs) * 0.7, max(psnrs) * 1.1) |
| ax_psnr.set_title("PSNR Progress") |
| ax_psnr.set_xlabel("Iteration") |
| ax_psnr.set_ylabel("PSNR") |
|
|
| plt.tight_layout(rect=[0, 0, 1, 0.97]) |
|
|
| frame_path = inter_dir / f"hist_{frame_idx:05d}.png" |
| fig.savefig(frame_path, dpi=400) |
|
|
| plt.close(fig) |
|
|
| if not save_video: |
| print(f"✅ Saved dashboard frames to {inter_dir} ({T} frames total)") |
| return |
|
|
| |
| total_duration_sec = 20.0 |
| fps = T / total_duration_sec |
|
|
| cmd = [ |
| "ffmpeg", "-y", "-framerate", f"{fps}", |
| "-i", str(inter_dir / "hist_%05d.png"), |
| "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", |
| "-c:v", "libx264", "-pix_fmt", "yuv420p", |
| "-crf", "18", str(out_path) |
|
|
| ] |
| print("🎞️ Running FFmpeg to create video...") |
| subprocess.run(cmd, check=True) |
|
|
| print(f"✅ Saved dashboard video to {out_path} ({total_duration_sec:.1f}s total)") |
|
|
|
|
| def make_gaussians_dashboard_video_with_ani(data_groups, psnrs, iters, out_path=CustomPath("dashboard.mp4"), |
| max_frames=None, scene=0): |
| """ |
| Create a dashboard video visualizing parameter distributions and PSNR over iterations. |
| Args: |
| data_groups (dict): Dictionary containing parameter groups as keys and list of tensors as values. |
| Each list should have T entry of shape (N, D) where T is time |
| psnrs (list): List of PSNR values over iterations. |
| iters (list): List of iteration numbers corresponding to the PSNR values. |
| out_path (CustomPath): Path to save the output video. |
| max_frames (int, optional): Maximum number of frames to include in the video. If None, include all frames. |
| """ |
| |
|
|
| |
| |
| sh_d = data_groups["shs"][0][0].shape[-1] // 3 |
| param_axis_names = { |
| "opacities": [""], |
| "means": ["x", "y", "z"], |
| "scales": ["x", "y", "z"], |
| "quats": ["x", "y", "z", "w"], |
| "shs": ["r" + str(i) for i in range(sh_d)] + [f"g{i}" for i in range(sh_d)] + [f"b{i}" for i in range(sh_d)], |
| } |
|
|
| T = list(data_groups.values())[0].shape[0] |
| if max_frames is not None: |
| T = min(T, max_frames) |
| if iters is None: |
| iters = list(range(T)) |
|
|
| |
| total_dims = sum(g.shape[-1] for g in data_groups.values()) |
| ncols = 4 |
| nrows = int(np.ceil(total_dims / ncols)) |
|
|
| |
| fig = plt.figure(figsize=(5 * ncols, 3.5 * (nrows + 1))) |
| gs = gridspec.GridSpec(nrows + 1, ncols, height_ratios=[1] * nrows + [0.5]) |
| axes = [fig.add_subplot(gs[i // ncols, i % ncols]) for i in range(nrows * ncols)] |
| ax_psnr = fig.add_subplot(gs[-1, :]) |
|
|
| |
| subplot_map = [] |
| i = 0 |
| for key, g in data_groups.items(): |
| D = g.shape[-1] |
| coord_names = [f"{key} {param_axis_names[key][i]}" for i in range(D)] |
| for d in range(D): |
| all_hist_data = [calc_hist(g[t, scene, :, d], density=True) for t in range(T)] |
| all_x, all_y = zip(*all_hist_data) |
| |
| xmin = min(x.min() for x in all_x) |
| xmax = max(x.max() for x in all_x) |
| ymin = 0.0 |
| ymax = max(y.max() for y in all_y) * 1.1 |
| subplot_map.append((key, d, axes[i], coord_names[d], all_x, all_y, xmin, xmax, ymin, ymax)) |
| i += 1 |
|
|
| |
| def update(frame_idx): |
| fig.suptitle(f"Iteration {iters[frame_idx]} — PSNR: {psnrs[frame_idx]:.2f}", fontsize=18) |
|
|
| for key, d, ax, name, all_x, all_y, xmin, xmax, ymin, ymax in subplot_map: |
| ax.clear() |
| ax.plot(all_x[frame_idx], all_y[frame_idx], color="#17becf", label=r"Resplat $\Delta$") |
|
|
| ax.set_xlim(xmin, xmax) |
| ax.set_ylim(ymin, ymax) |
| ax.set_title(name) |
| ax.legend(frameon=False, loc="upper left") |
|
|
| |
| ax_psnr.clear() |
| ax_psnr.plot(iters[:frame_idx + 1], psnrs[:frame_idx + 1], color="#ffbc42") |
| ax_psnr.scatter(iters[frame_idx], psnrs[frame_idx], color="#ffbc42", s=60, zorder=3) |
| ax_psnr.set_xlim(min(iters), max(iters)) |
| ax_psnr.set_ylim(min(psnrs) * 0.98, max(psnrs) * 1.02) |
| ax_psnr.set_title("PSNR Progress") |
| ax_psnr.set_xlabel("Iteration") |
| ax_psnr.set_ylabel("PSNR") |
|
|
| plt.tight_layout(rect=[0, 0, 1, 0.97]) |
| return axes + [ax_psnr] |
|
|
| |
| Path(out_path).parent.mkdir(parents=True, exist_ok=True) |
|
|
| total_duration_sec = 20.0 |
| interval_ms = total_duration_sec * 1000 / T |
|
|
| try: |
| ani = animation.FuncAnimation(fig, update, frames=T, interval=interval_ms, blit=False) |
| ani.save(out_path, writer="ffmpeg", dpi=300) |
| except FileNotFoundError: |
| print("⚠️ FFmpeg not found. Saving as GIF instead.") |
| ani = animation.FuncAnimation(fig, update, frames=T, interval=interval_ms, blit=False) |
| ani.save(out_path.replace(".mp4", ".gif"), writer="pillow", dpi=300) |
|
|
| plt.close(fig) |
| print(f"✅ Saved dashboard video to {out_path} ({total_duration_sec:.1f}s total)") |
|
|
| plt.close(fig) |
| print(f"✅ Saved dashboard video to {out_path}") |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import numpy as np |
| import torch |
| from pathlib import Path |
|
|
|
|
| def calc_hist(data, max_percentile=99.9, min_percentile=0.1, density=False): |
| max_val = np.percentile(data, max_percentile) |
| min_val = np.percentile(data, min_percentile) |
| curr_data = data.clip(min_val, max_val) |
| counts, bin_edges = np.histogram(curr_data, bins=100, range=(min_val, max_val), density=density) |
| bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:]) |
|
|
| return bin_centers, counts |
|
|
|
|
| def debugging_convergence( |
| deltas_list: list[dict[str, Tensor]], |
| states_norms_list: list[Tensor], |
| grads_raw_list: list[dict[str, Tensor]], |
| normalized_grads_list: list[dict[str, Tensor]], |
| psnr_list: list[float], |
| iterations_list: list[int], |
| output_path: Path, |
| scene_name: str |
| ): |
| print("📈 Generating convergence plots...") |
| assert len(iterations_list) > 0, "Iterations list cannot be empty." |
| assert len(psnr_list) == len(iterations_list), "PSNR list length must match iterations list length." |
|
|
| iters = iterations_list |
| psnrs = psnr_list |
| states_norms = [] |
| for state_norms in states_norms_list: |
| states_norms.append(state_norms.mean().item()) |
|
|
| deltas_abs_means = [] |
| for deltas in deltas_list: |
| total_mean = 0.0 |
| count = 0 |
| for key, delta in deltas.items(): |
| total_mean += delta.abs().mean().item() |
| count += 1 |
| deltas_abs_means.append(total_mean / count) |
|
|
| grads_raw_abs_means = [] |
| for grads in grads_raw_list: |
| total_mean = 0.0 |
| count = 0 |
| for key, grad in grads.items(): |
| total_mean += grad.abs().mean().item() |
| count += 1 |
| grads_raw_abs_means.append(total_mean / count) |
|
|
| normalized_grads_abs_means = [] |
| for normalized_grads in normalized_grads_list: |
| total_mean = 0.0 |
| count = 0 |
| for key, grad in normalized_grads.items(): |
| total_mean += grad.abs().mean().item() |
| count += 1 |
| normalized_grads_abs_means.append(total_mean / count) |
|
|
| |
| rc = { |
| 'axes.titlesize': 17, |
| 'axes.labelsize': 15, |
| 'xtick.labelsize': 15, |
| 'ytick.labelsize': 15, |
| 'legend.fontsize': 11 |
| } |
|
|
| with plt.rc_context(rc): |
| |
| fig, axs = plt.subplots(5, 1, figsize=(10, 15)) |
| |
| axs[0].plot(iters, psnrs, marker='o', color='blue') |
| axs[0].set_title('PSNR over Iterations') |
| axs[0].set_xlabel('Iteration') |
| axs[0].set_ylabel('PSNR') |
| axs[0].grid(True, alpha=0.3) |
| |
| axs[1].plot(iters, states_norms, marker='o', color='orange') |
| axs[1].set_title('State Norm over Iterations') |
| axs[1].set_xlabel('Iteration') |
| axs[1].set_ylabel('State Norm') |
| axs[1].grid(True, alpha=0.3) |
| |
| axs[2].plot(iters, deltas_abs_means, marker='o', color='green') |
| axs[2].set_title('Mean Absolute Delta over Iterations') |
| axs[2].set_xlabel('Iteration') |
| axs[2].set_ylabel('Mean Absolute Delta') |
| axs[2].grid(True, alpha=0.3) |
| |
| axs[3].plot(iters, grads_raw_abs_means, marker='o', color='red', label='Raw Grads') |
| axs[3].set_title('Mean Absolute Gradient over Iterations') |
| axs[3].set_xlabel('Iteration') |
| axs[3].set_ylabel('Mean Absolute Gradient') |
| axs[3].grid(True, alpha=0.3) |
| |
| axs[4].plot(iters, normalized_grads_abs_means, marker='o', color='purple', label='Normalized Grads') |
| axs[4].set_title('Mean Absolute Normalized Gradient over Iterations') |
| axs[4].set_xlabel('Iteration') |
| axs[4].set_ylabel('Mean Absolute Normalized Gradient') |
| axs[4].grid(True, alpha=0.3) |
| plt.tight_layout() |
| (output_path / "plots" / scene_name).mkdir(parents=True, exist_ok=True) |
| plt.savefig(output_path / "plots" / scene_name / "convergence_plot.png", dpi=300) |
| plt.close() |
|
|
|
|
| def debugging_deltas( |
| deltas_list: list[dict[str, Tensor]], |
| grads_list: list[dict[str, Tensor]], |
| normalized_grads_list: list[dict[str, Tensor]], |
| learning_rates: list[dict[str, float]], |
| psnr_list: list[float], |
| iterations_list: list[int], |
| output_path: Path, |
| scene_name: str |
| ): |
| assert len(iterations_list) > 0, "Iterations list cannot be empty." |
| assert len(psnr_list) == len(iterations_list), "PSNR list length must match iterations list length." |
|
|
| |
| psnr_list = psnr_list[1:] |
| iterations_list = iterations_list[1:] |
|
|
| assert len(deltas_list) == len(iterations_list), "Deltas list length must match iterations list length." |
| assert len(grads_list) == len(iterations_list), "Grads list length must match iterations list length." |
| assert len(normalized_grads_list) == len( |
| iterations_list), "Normalized grads list length must match iterations list length." |
| if len(learning_rates) > 0: |
| assert len(learning_rates) == len( |
| iterations_list), "Learning rates list length must match iterations list length." |
| iters = iterations_list |
| psnrs = psnr_list |
| |
| nr_iters = len(iters) |
|
|
| |
| rc = { |
| 'axes.titlesize': 17, |
| 'axes.labelsize': 15, |
| 'xtick.labelsize': 15, |
| 'ytick.labelsize': 15, |
| 'legend.fontsize': 11 |
| } |
|
|
| |
| for key in ["opacities", "means", "scales", "rotations"]: |
| |
| |
|
|
| |
| delta_data = [deltas[key] for deltas in deltas_list] |
| grads_data = [grads[key] for grads in grads_list] |
| normalized_grads_data = [normalized_grads[key] for normalized_grads in normalized_grads_list] |
| |
| |
| |
| |
|
|
| |
| D = delta_data[0].shape[-1] |
|
|
| rows = 3 |
|
|
| with plt.rc_context(rc): |
| plt.figure(figsize=(10 * D, 8 * rows)) |
|
|
| if D in [3, 4]: |
| coord_names = ['X', 'Y', 'Z', 'W'][:D] |
| elif D == 1: |
| coord_names = [""] |
| else: |
| coord_names = [f"Dim {i}" for i in range(D)] |
|
|
| for r, kind in enumerate(["delta", "grad", "grad_norm"]): |
| for d in range(D): |
| ax = plt.subplot(rows, D, r * D + d + 1) |
|
|
| for i, t in enumerate(iters): |
| color_frac = float(i) / float(nr_iters) |
|
|
| |
| if kind == "delta": |
| curr = delta_data[i][:, d].float().cpu().numpy() |
| cmap = plt.cm.viridis |
| elif kind == "grad": |
| curr = grads_data[i][:, d].float().cpu().numpy() |
| cmap = plt.cm.cividis |
| else: |
| curr = normalized_grads_data[i][:, d].float().cpu().numpy() |
| cmap = plt.cm.plasma |
|
|
| |
| bin_centers, counts = calc_hist(curr) |
| max_counts = counts.max() |
| if max_counts > 0: |
| counts = counts / max_counts |
|
|
| label = f"step: {t}, psnr: {psnrs[i]}" |
| ax.plot(bin_centers, counts, label=label, |
| color=cmap(color_frac), linewidth=2) |
|
|
| xlim = (-np.max(np.abs(bin_centers)), np.max(np.abs(bin_centers))) |
| ax.set_xlim(xlim) |
| ax.axvline(0, color='black', linewidth=1, linestyle=':') |
|
|
| if r == rows - 1: |
| ax.set_xlabel(f"{coord_names[d]}") |
| if d == 0: |
| ax.set_ylabel("Density") |
|
|
| |
| ax.set_title(f"{kind.replace('_', ' ').title()} {key.replace('_', ' ').title()} {coord_names[d]}") |
|
|
| ax.legend(fontsize=9) |
| ax.grid(True, alpha=0.3) |
|
|
| plt.suptitle(f"{key.replace('_', ' ').title()} histograms (centered & normalized)", fontsize=18) |
| plt.tight_layout(rect=[0, 0, 1, 0.97]) |
|
|
| |
| save_dir = os.path.join(output_path, "plots", scene_name) |
| os.makedirs(save_dir, exist_ok=True) |
| save_path = os.path.join(save_dir, f"{key}_deltas_histogram.png") |
| plt.savefig(save_path, dpi=300, bbox_inches='tight') |
| plt.close() |
| print(f"Saved delta histogram plot to {save_path}") |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| def debugging_gaussians(gaussian_list: list[Gaussians], psnr_list: list[float], iter_list: list[int], output_path: Path, |
| scene_name: str): |
| assert len(gaussian_list) > 0, "Gaussian list cannot be empty." |
| assert len(gaussian_list) == len(iter_list), "Gaussian list length must match iterations list length." |
| assert len(psnr_list) == len(iter_list), "PSNR list length must match iterations list length." |
|
|
| if gaussian_list[0].stores_activated: |
| |
| scales_fn = torch.log |
| opacities_fn = torch.logit |
| else: |
| |
| scales_fn = lambda x: x |
| opacities_fn = lambda x: x |
|
|
| |
| data_groups = { |
| "opacities": [opacities_fn(g.opacities).squeeze(0).detach().cpu().unsqueeze(-1) for g in gaussian_list], |
| "scales": [scales_fn(g.scales).squeeze(0).detach().cpu() for g in gaussian_list], |
| "quats": [g.rotations.squeeze(0).detach().cpu() for g in gaussian_list], |
| "means": [g.means.squeeze(0).detach().cpu() for g in gaussian_list], |
| "shs": [g.harmonics.squeeze(0).detach().cpu() for g in gaussian_list]} |
|
|
| plot_gaussians_params_histograms( |
| data_groups=data_groups, |
| psnrs=psnr_list, |
| iters=iter_list, |
| out_path=output_path / f"plots/{scene_name}/params.mp4" |
| ) |
|
|
|
|
| |
|
|
| |
| |
| |
| |
| |
|
|
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| def debugging_invisible_gaussians( |
| gaussian_list, |
| grads_raw_list, |
| normalized_grads_list, |
| means2d_list, |
| radii_list, |
| psnr_list, |
| iterations_list, |
| output_path, |
| scene_name |
| ): |
| def concat_grads(grads_list): |
| grads_per_params = [] |
| G = grads_list[0][list(grads_list[0].keys())[0]].shape[0] |
| for key in grads_list[0].keys(): |
| grads_val = [grads[key].reshape(G, -1) for grads in grads_list] |
|
|
| grads_per_params.append(torch.stack(grads_val, dim=0)) |
|
|
| grads_mat = torch.cat(grads_per_params, dim=-1) |
| return grads_mat, grads_per_params |
|
|
| |
| grads_mat, grads_per_params = concat_grads(grads_raw_list) |
| norm_grads_mat, norm_grads_per_params = concat_grads(normalized_grads_list) |
| scales_grads = grads_per_params[1] |
| opacities_grads = grads_per_params[3] |
| scales_norm_grads = norm_grads_per_params[1] |
| opacities_norm_grads = norm_grads_per_params[3] |
| means2d = torch.cat(means2d_list, dim=0).cpu()[1:] |
| radii_list = torch.cat(means2d_list, dim=0).cpu()[1:] |
|
|
| T, G, D = grads_mat.shape |
| iterations_list = iterations_list[1:] |
|
|
| |
| def extract_params(gaussians: list[Gaussians], grads): |
| params = [] |
| for k in grads[0].keys(): |
| if k in ["shNs", "sh0s"]: |
| continue |
| params.append(torch.stack([getattr(g, k)[0].detach().cpu() for g in gaussians])) |
| params.append(torch.stack([g.harmonics[0].detach().cpu() for g in gaussians])) |
| params = [p[1:] for p in params] |
| gaussians_mat = torch.cat([p.reshape(T, G, -1) for p in params], dim=-1) |
| return params, gaussians_mat |
|
|
| params_mat, gaussians_mat = extract_params(gaussian_list, grads_raw_list) |
| means = params_mat[0] |
| scales = params_mat[1] |
| rotations = params_mat[2] |
| opacities = params_mat[3] |
| harmonics = params_mat[4] |
|
|
| |
| zero_grad_mask = (grads_mat == 0) |
| zero_grad_cnt = (zero_grad_mask).sum(dim=-1) |
| is_zero = (zero_grad_mask).all(dim=-1) |
| is_nonzero = (~zero_grad_mask).all(dim=-1) |
| is_partial = ~(is_zero | is_nonzero) |
| validation = is_zero.float() + is_nonzero.float() + is_partial.float() |
| assert (validation == 1).all(), "Gradient classification error: some Gaussians are not classified properly." |
|
|
| |
| state = torch.zeros_like(is_zero, dtype=torch.int8) |
| state[is_partial] = 1 |
| state[is_nonzero] = 2 |
|
|
| |
| transition = state[1:] - state[:-1] |
| transition_per_gaussian = (transition != 0).sum(dim=0) |
|
|
| |
| zero_cnt = is_zero.sum(dim=1).cpu().numpy() |
| partial_cnt = is_partial.sum(dim=1).cpu().numpy() |
|
|
| |
| zero_to_partial = ((state[:-1] == 0) & (state[1:] == 1)).sum(dim=1) |
| zero_to_nonzero = ((state[:-1] == 0) & (state[1:] == 2)).sum(dim=1) |
| partial_to_nonzero = ((state[:-1] == 1) & (state[1:] == 2)).sum(dim=1) |
| partial_to_zero = ((state[:-1] == 1) & (state[1:] == 0)).sum(dim=1) |
| nonzero_to_zero = ((state[:-1] == 2) & (state[1:] == 0)).sum(dim=1) |
| nonzero_to_partial = ((state[:-1] == 2) & (state[1:] == 1)).sum(dim=1) |
|
|
| |
| zero_to_zero = ((state[:-1] == 0) & (state[1:] == 0)).sum(dim=1) |
| partial_to_partial = ((state[:-1] == 1) & (state[1:] == 1)).sum(dim=1) |
| nonzero_to_nonzero = ((state[:-1] == 2) & (state[1:] == 2)).sum(dim=1) |
|
|
| total = (zero_to_nonzero + zero_to_partial + partial_to_nonzero + partial_to_zero + nonzero_to_zero |
| + nonzero_to_partial + zero_to_zero + partial_to_partial + nonzero_to_nonzero) |
| assert (total == G).all(), "Transition counts do not sum up to total number" |
|
|
| |
| n_vis = 30 |
| |
| |
| |
| |
|
|
| |
| top_scales = torch.topk(scales[-1, ..., 0], k=n_vis, largest=True).indices |
| random_indices = top_scales |
|
|
| |
| |
| grad_norms = grads_mat.norm(dim=-1) |
|
|
| |
| fig, axes = plt.subplots(10, 1, figsize=(12, 18), sharex=True) |
| fig.suptitle(f"Debugging Invisible Gaussians — {scene_name}", fontsize=16) |
|
|
| |
| i = 0 |
| axes[i].plot(iterations_list, zero_cnt, label="Zero Grad Gaussians") |
| axes[i].plot(iterations_list, partial_cnt, label="Partial Grad Gaussians") |
| axes[i].set_ylabel("Count") |
| axes[i].set_title("Zero vs Partial Grad Gaussians Count") |
| axes[i].legend() |
|
|
| |
| i += 1 |
| axes[i].plot(iterations_list[1:], zero_to_partial.cpu(), label='Zero → Partial') |
| axes[i].plot(iterations_list[1:], zero_to_nonzero.cpu(), label='Zero → Nonzero') |
| axes[i].plot(iterations_list[1:], partial_to_nonzero.cpu(), label='Partial → Nonzero') |
| axes[i].plot(iterations_list[1:], partial_to_zero.cpu(), label='Partial → Zero') |
| axes[i].plot(iterations_list[1:], nonzero_to_zero.cpu(), label='Nonzero → Zero') |
| axes[i].plot(iterations_list[1:], nonzero_to_partial.cpu(), label='Nonzero → Partial') |
| axes[i].set_ylabel("Count") |
| axes[i].set_title("Transition Grad Gaussians Count") |
| axes[i].legend() |
|
|
| |
| i += 1 |
| axes[i].plot(iterations_list, zero_grad_cnt[:, random_indices]) |
| axes[i].set_title("Gaussians zero grad count") |
| axes[i].set_ylabel("Zero grad cnt") |
|
|
| |
| i += 1 |
| axes[i].plot(iterations_list, grad_norms[:, random_indices]) |
| axes[i].set_title("Gaussians gradient magnitude") |
| axes[i].set_ylabel("Gradient norm") |
|
|
| |
| i += 1 |
| axes[i].plot(iterations_list, scales[:, random_indices].mean(-1)) |
| axes[i].set_title("Gaussians scales") |
| axes[i].set_ylabel("Scales") |
|
|
| |
| i += 1 |
| axes[i].plot(iterations_list, opacities[:, random_indices]) |
| axes[i].set_title("Gaussians opacities") |
| axes[i].set_ylabel("Opacities") |
|
|
| |
| i += 1 |
| axes[i].plot(iterations_list, scales_norm_grads[:, random_indices, 0]) |
| axes[i].set_title("Gaussians scales X adam grad") |
| axes[i].set_ylabel("Scales X grad") |
|
|
| i += 1 |
| axes[i].plot(iterations_list, opacities_grads[:, random_indices, 0]) |
| axes[i].set_title("Gaussians opacities adam grad") |
| axes[i].set_ylabel("Opacities X grad") |
|
|
| i += 1 |
| axes[i].plot(iterations_list, means2d[:, 0, random_indices, 0]) |
| axes[i].set_title("Gaussians means 2D X") |
| axes[i].set_ylabel("Means 2D X") |
| axes[i].set_xlabel("Iteration") |
|
|
| i += 1 |
| axes[i].plot(iterations_list, radii_list[:, :, random_indices, 0].sum(1)) |
| axes[i].set_title("Gaussians radii 2D X") |
| axes[i].set_ylabel("Radii 2D X") |
| axes[i].set_xlabel("Iteration") |
|
|
| plt.tight_layout(rect=[0, 0, 1, 0.96]) |
|
|
| |
| |
| |
| |
| |
| plt.show() |
|
|
| print(f"✅ Saved time-evolution debug plot → {fig_path}") |
|
|