Lucasgitguy-co's picture
Publish evaluated Forma3D Tiny 3 ABO continuation
3469f52 verified
Raw History Blame Contribute Delete
7.41 kB
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Lucasgitguy-co
import hashlib
import json
from pathlib import Path
import numpy as np
import torch
from PIL import Image, ImageOps
from torch.utils.data import Dataset
from .splats import Splats, read_ply
def grid_centers(grid):
axis = (np.arange(grid, dtype=np.float32) + .5) / grid - .5
return np.stack(np.meshgrid(axis, axis, axis, indexing="ij"), axis=-1).reshape(-1, 3)
def cell_indices(xyz, grid):
if not np.isfinite(xyz).all() or (np.abs(xyz) > .50001).any():
raise ValueError("Targets must be aligned inside [-0.5,0.5]^3. No silent normalization is performed.")
ijk = np.floor((xyz + .5) * grid).astype(np.int64).clip(0, grid-1)
return (ijk[:, 0] * grid + ijk[:, 1]) * grid + ijk[:, 2]
def splat_targets(splats, grid):
"""One highest-opacity teacher Gaussian per occupied cell; explicitly lossy."""
cells = cell_indices(splats.xyz, grid)
order = np.argsort(-splats.opacity[:, 0], kind="stable")
_, first = np.unique(cells[order], return_index=True)
keep = order[first]
slots = cells[keep]
result = {"occ": np.zeros((grid**3, 1), np.float32), "xyz": grid_centers(grid),
"rgb": np.zeros((grid**3, 3), np.float32),
"scale": np.full((grid**3, 3), 1 / grid / 3, np.float32),
"quat": np.tile(np.array([1, 0, 0, 0], np.float32), (grid**3, 1)),
"opacity": np.zeros((grid**3, 1), np.float32),
"geometry": np.zeros((grid**3, 2), np.float32),
"geometry_valid": np.zeros(2, np.float32), "grid": np.array(grid, np.int32)}
result["occ"][slots] = 1
for key in ("xyz", "rgb", "scale", "quat", "opacity"):
result[key][slots] = getattr(splats, key)[keep]
return result
def mesh_occupancy(path, transform_path, grid):
import trimesh
if not transform_path:
raise ValueError("Every geometry teacher needs an explicit --*-transform JSON matrix, even for identity.")
transform = np.asarray(json.loads(Path(transform_path).read_text()), dtype=float)
if transform.shape != (4, 4) or not np.isfinite(transform).all() or not np.allclose(transform[3], [0,0,0,1]):
raise ValueError("Transform must be a finite affine 4x4 matrix")
mesh = trimesh.load(str(path), force="mesh", process=False)
if not isinstance(mesh, trimesh.Trimesh) or len(mesh.faces) == 0:
raise ValueError("Geometry teacher must contain a triangle mesh")
mesh.apply_transform(transform)
cell_indices(np.asarray(mesh.vertices), grid) # validates the whole object
points, _ = trimesh.sample.sample_surface(mesh, min(200_000, grid**3 * 16), seed=42)
mask = np.zeros(grid**3, np.float32)
mask[np.unique(cell_indices(points, grid))] = 1
return mask
def cache_sample(root, sample_id, image_path, splats, grid=16, source="idol",
coarse=None, coarse_transform=None, fine=None, fine_transform=None, extra=None):
root = Path(root)
if not sample_id or any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" for c in sample_id):
raise ValueError("Sample ID may contain only letters, digits, - and _")
if grid not in (8, 16, 32):
raise ValueError("Grid must be 8, 16 or 32")
target = splat_targets(splats, grid)
for channel, mesh, transform in [(0, coarse, coarse_transform), (1, fine, fine_transform)]:
if mesh:
target["geometry"][:, channel] = mesh_occupancy(mesh, transform, grid)
target["geometry_valid"][channel] = 1
row = {"id": sample_id, "image": f"images/{sample_id}.png", "target": f"targets/{sample_id}.npz",
"source": source, "grid": grid, "teachers": {"appearance": source,
"coarse": "InstantMesh" if coarse else None, "fine": "Hi3D" if fine else None},
"teacher_gaussians": len(splats.xyz), "retained_gaussians": int(target["occ"].sum()),
"geometry_transforms": {"coarse": json.loads(Path(coarse_transform).read_text()) if coarse else None,
"fine": json.loads(Path(fine_transform).read_text()) if fine else None},
"metadata": extra or {}}
manifest = root / "manifest.jsonl"
rows = load_manifest(manifest) if manifest.exists() else []
if any(r["id"] == sample_id for r in rows):
raise ValueError(f"Sample {sample_id} already exists; choose a new ID")
if rows and any(r["grid"] != grid for r in rows):
raise ValueError("Do not mix grid resolutions in one dataset")
image = Image.open(image_path)
image.load()
if root.joinpath(row["image"]).exists() or root.joinpath(row["target"]).exists():
raise ValueError("Sample files already exist; refusing to overwrite")
root.joinpath("images").mkdir(parents=True, exist_ok=True)
root.joinpath("targets").mkdir(parents=True, exist_ok=True)
image.save(root / row["image"])
row["image_sha256"] = hashlib.sha256((root / row["image"]).read_bytes()).hexdigest()
np.savez_compressed(root / row["target"], **target)
row["target_sha256"] = hashlib.sha256((root / row["target"]).read_bytes()).hexdigest()
# Single-writer dataset. Atomic manifest replacement avoids partial JSON lines.
tmp = manifest.with_suffix(".tmp")
tmp.write_text("".join(json.dumps(r) + "\n" for r in [*rows, row]))
tmp.replace(manifest)
return row
def load_manifest(path):
rows = [json.loads(line) for line in Path(path).read_text().splitlines() if line.strip()]
if not rows or len({r["id"] for r in rows}) != len(rows):
raise ValueError("Manifest must contain nonempty, unique sample IDs")
return rows
def image_tensor(path, size=128):
"""Already-framed RGB or RGBA cutout; black background, square letterbox."""
with Image.open(path) as original:
rgba = ImageOps.exif_transpose(original).convert("RGBA")
bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255))
bg.alpha_composite(rgba)
image = ImageOps.pad(bg.convert("RGB"), (size, size), method=Image.Resampling.LANCZOS, color="black")
return torch.from_numpy(np.array(image, dtype=np.float32).transpose(2, 0, 1) / 255)
class CachedDataset(Dataset):
def __init__(self, manifest, rows=None):
self.root = Path(manifest).resolve().parent
self.rows = load_manifest(manifest) if rows is None else rows
if not self.rows:
raise ValueError("Empty dataset")
self.grid = self.rows[0]["grid"]
for row in self.rows:
if row["grid"] != self.grid:
raise ValueError("Mixed grids")
for key in ("image", "target"):
p = (self.root / row[key]).resolve()
if not p.is_relative_to(self.root) or not p.is_file():
raise ValueError(f"Invalid dataset {key} path")
digest = hashlib.sha256(p.read_bytes()).hexdigest()
if digest != row[f"{key}_sha256"]:
raise ValueError(f"Dataset {key} hash mismatch for {row['id']}")
def __len__(self):
return len(self.rows)
def __getitem__(self, i):
row = self.rows[i]
with np.load(self.root / row["target"], allow_pickle=False) as cached:
target = {k: torch.from_numpy(cached[k].copy()).float() for k in cached.files if k != "grid"}
return image_tensor(self.root / row["image"]), target