Spaces:
Running
Running
| """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, | |
| ) | |