Andrid1's picture
Add PBR material predictor demo (3 curated runs)
94316fa verified
Raw
History Blame Contribute Delete
6.22 kB
"""PyTorch dataset for MatSynth PBR materials via HuggingFace streaming."""
import os
import time
import torch
from torch.utils.data import IterableDataset, DataLoader
from datasets import load_dataset
from PIL import Image
from src.transforms import get_resize_transform, get_train_transform, MAP_NAMES
# Set MATSYNTH_DEBUG=1 to enable per-sample timing logs
_DEBUG = os.environ.get("MATSYNTH_DEBUG", "0") == "1"
class MatSynthDataset(IterableDataset):
"""Streams MatSynth materials and returns 4 PBR maps as tensors.
Each sample yields a dict:
- "basecolor": (3, H, W) float32 tensor [0,1]
- "normal": (3, H, W) float32 tensor [0,1]
- "roughness": (3, H, W) float32 tensor [0,1]
- "metallic": (3, H, W) float32 tensor [0,1]
- "name": str
- "category": str
"""
def __init__(
self,
split: str = "train",
size: int = 256,
max_samples: int | None = None,
use_augmentation: bool = False,
seed: int = 42,
):
self.split = split
self.size = size
self.max_samples = max_samples
self.seed = seed
self.transform = get_train_transform(size) if use_augmentation else get_resize_transform(size)
def _load_stream(self):
"""Create a fresh streaming dataset (needed for multi-epoch iteration)."""
ds = load_dataset(
"gvecchio/MatSynth",
split=self.split,
streaming=True,
)
# Drop heavy columns we don't need (avoids select_columns downloading everything)
keep = {"name", "metadata", *MAP_NAMES}
try:
drop = [c for c in ds.column_names if c not in keep]
if drop:
ds = ds.remove_columns(drop)
except Exception:
pass # column_names may not be available on all streaming configs
# buffer_size=500 with 4096x4096 images causes OOM (~500 * 4 * 64MB = 128GB)
# Use small buffer; images are already diverse across the dataset
ds = ds.shuffle(seed=self.seed, buffer_size=20)
return ds
def _process_sample(self, sample: dict) -> dict | None:
"""Convert a HF sample to tensors. Returns None if maps are missing."""
tensors = {}
for map_name in MAP_NAMES:
img = sample.get(map_name)
if img is None:
return None
# HF returns PIL Image; ensure RGB
if not isinstance(img, Image.Image):
return None
img = img.convert("RGB")
tensors[map_name] = self.transform(img)
# Extract metadata
meta = sample.get("metadata", {})
category = meta.get("category", "unknown")
if isinstance(category, (list, dict)):
category = str(category)
tensors["name"] = sample.get("name", "unknown")
tensors["category"] = category
return tensors
def __iter__(self):
t0 = time.perf_counter()
ds = self._load_stream()
if _DEBUG:
print(f"[DEBUG] _load_stream: {time.perf_counter() - t0:.2f}s")
count = 0
for sample in ds:
t1 = time.perf_counter()
if _DEBUG and count == 0:
print(f"[DEBUG] first sample from stream: {t1 - t0:.2f}s")
result = self._process_sample(sample)
if _DEBUG:
print(f"[DEBUG] _process_sample #{count}: {time.perf_counter() - t1:.3f}s"
f" (result={'ok' if result else 'None'})")
if result is None:
continue
yield result
count += 1
if self.max_samples is not None and count >= self.max_samples:
break
class CachedMatSynthDataset(torch.utils.data.Dataset):
"""Reads pre-downloaded .pt samples from disk. Supports random access and shuffling."""
def __init__(self, cache_dir: str, use_augmentation: bool = False, size: int = 256):
self.cache_dir = cache_dir
self.files = sorted(f for f in os.listdir(cache_dir) if f.endswith(".pt"))
if not self.files:
raise FileNotFoundError(f"No .pt files in {cache_dir}")
self.augment = None
if use_augmentation:
from src.transforms import PBRAugmentation
self.augment = PBRAugmentation()
def __len__(self):
return len(self.files)
def __getitem__(self, idx):
path = os.path.join(self.cache_dir, self.files[idx])
sample = torch.load(path, weights_only=False)
if self.augment is not None:
sample = self.augment(sample)
return sample
def create_dataloader(
split: str = "train",
size: int = 256,
batch_size: int = 4,
max_samples: int | None = None,
use_augmentation: bool = False,
num_workers: int = 0,
cache_dir: str | None = None,
) -> DataLoader:
"""Create a DataLoader for MatSynth PBR materials.
Args:
cache_dir: Path to pre-downloaded .pt files (from predownload.py).
If provided, loads from disk instead of streaming.
Default path: data/processed/{split}_{size}
"""
if cache_dir is not None:
dataset = CachedMatSynthDataset(cache_dir, use_augmentation=use_augmentation, size=size)
else:
dataset = MatSynthDataset(
split=split,
size=size,
max_samples=max_samples,
use_augmentation=use_augmentation,
)
def collate_fn(batch):
"""Custom collate to handle mixed tensor/string fields."""
result = {}
for key in MAP_NAMES:
stacked = torch.stack([b[key] for b in batch])
if key in ("roughness", "metallic"):
stacked = stacked[:, :1, :, :]
result[key] = stacked
result["name"] = [b["name"] for b in batch]
result["category"] = [b["category"] for b in batch]
return result
is_map_style = isinstance(dataset, CachedMatSynthDataset)
return DataLoader(
dataset,
batch_size=batch_size,
collate_fn=collate_fn,
num_workers=num_workers,
shuffle=is_map_style,
)