Download src/image3d_student/data.py from Lucasgitguy-co/forma3d-tiny: direct link, hf CLI and curl.
- Browser
- Download file 7.41 kB
-
https://huggingface.co/Lucasgitguy-co/forma3d-tiny/resolve/main/src/image3d_student/data.py
- Command line
-
hf download hf://Lucasgitguy-co/forma3d-tiny/src/image3d_student/data.py
-
curl -L -o data.py https://huggingface.co/Lucasgitguy-co/forma3d-tiny/resolve/main/src/image3d_student/data.py
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 | |