Download experiments/decision-20260928-v2/source/data.py from User-2468/mini-unet-colorizer: direct link, hf CLI and curl.
- Browser
- Download file 5.44 kB
-
https://huggingface.co/User-2468/mini-unet-colorizer/resolve/1cca2ddeb5f7c0e6167c452f52541c7684182248/experiments/decision-20260928-v2/source/data.py
- Command line
-
hf download hf://User-2468/mini-unet-colorizer@1cca2ddeb5f7c0e6167c452f52541c7684182248/experiments/decision-20260928-v2/source/data.py
-
curl -L -o data.py https://huggingface.co/User-2468/mini-unet-colorizer/resolve/1cca2ddeb5f7c0e6167c452f52541c7684182248/experiments/decision-20260928-v2/source/data.py
5.44 kB
| """Fixed color-bin semantics and reproducible dataset membership.""" | |
| import hashlib | |
| import io | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| from PIL import Image | |
| import pyarrow.parquet as pq | |
| from scipy.spatial import cKDTree | |
| from skimage.color import rgb2lab | |
| import torch | |
| from torch.utils.data import Dataset | |
| class ColorBins: | |
| def __init__(self, centers, weights=None): | |
| self.centers = np.asarray(centers, np.float32) | |
| if self.centers.ndim != 2 or self.centers.shape[1] != 2: | |
| raise ValueError("Expected Q by 2 color centers") | |
| self.tree = cKDTree(self.centers) | |
| self.weights = np.ones(len(self.centers), np.float32) if weights is None else np.asarray(weights,np.float32) | |
| def encode(self, ab, k=5, sigma=5): | |
| k = min(k, len(self.centers)) | |
| if sigma <= 0 or k < 1: | |
| raise ValueError("Invalid encoding parameters") | |
| dist, idx = self.tree.query(ab.reshape(-1,2), k=k) | |
| dist, idx = dist.reshape(-1,k), idx.reshape(-1,k) | |
| logw = -dist**2/(2*sigma**2) | |
| logw -= logw.max(1, keepdims=True) | |
| weight = np.exp(logw); weight /= weight.sum(1,keepdims=True) | |
| shape = (*ab.shape[:-1], k) | |
| return idx.reshape(shape).astype(np.int64), weight.reshape(shape).astype(np.float32) | |
| def load_table(path): | |
| table = pq.read_table(path) | |
| return table | |
| def validate_manifest(path, table, manifest): | |
| with open(path,'rb') as f: | |
| digest=hashlib.file_digest(f,'sha256').hexdigest() | |
| if digest!=manifest['dataset_sha256'] or len(table)!=manifest['rows']: | |
| raise ValueError('Dataset differs from pinned split manifest') | |
| groups=[list(map(int,manifest[k])) for k in ['train','validation','test']] | |
| if any(not g or len(g)!=len(set(g)) or min(g)<0 or max(g)>=len(table) for g in groups): | |
| raise ValueError('Invalid or repeated indices in manifest') | |
| if any(set(groups[i])&set(groups[j]) for i,j in [(0,1),(0,2),(1,2)]): | |
| raise ValueError('Train/validation/test overlap') | |
| def decode_image(table, index): | |
| obj = table['image'][int(index)].as_py() | |
| return Image.open(io.BytesIO(obj['bytes'])).convert('RGB') | |
| def legacy_split(n, seed=0): | |
| # Exactly reproduce datasets.Dataset.train_test_split(test_size=.05, seed=0). | |
| order = np.random.default_rng(seed).permutation(n) | |
| nval = int(np.ceil(.05*n)) | |
| return order[nval:], order[:nval] | |
| def stratified_subset(indices, labels, per_class, seed): | |
| rng = np.random.default_rng(seed) | |
| selected = [] | |
| for label in sorted(set(labels)): | |
| group = np.asarray([i for i in indices if labels[i] == label]) | |
| selected.extend(rng.choice(group, min(per_class,len(group)), replace=False).tolist()) | |
| return np.asarray(selected, dtype=np.int64) | |
| class PhotoDataset(Dataset): | |
| def __init__(self, table, indices, bins, size=256, augment=False): | |
| self.table, self.indices, self.bins = table, list(map(int,indices)), bins | |
| self.size, self.augment = size, augment | |
| def __len__(self): | |
| return len(self.indices) | |
| def __getitem__(self, item): | |
| # Keep square preprocessing for controlled comparison with historical runs. | |
| # Production inference preserves aspect ratio separately. | |
| image = decode_image(self.table,self.indices[item]).resize((self.size,self.size),Image.Resampling.BILINEAR) | |
| arr = np.asarray(image,dtype=np.float32)/255 | |
| if self.augment and torch.rand(()) < .5: | |
| arr = arr[:,::-1].copy() | |
| lab = rgb2lab(arr).astype(np.float32) | |
| idx, weight = self.bins.encode(lab[...,1:]) | |
| return (torch.from_numpy((lab[...,:1]/50-1).transpose(2,0,1).copy()), | |
| torch.from_numpy(lab[...,1:].transpose(2,0,1).copy()), | |
| torch.from_numpy(idx),torch.from_numpy(weight)) | |
| def estimate_weights(table, indices, bins, mix=.7, limit=1000, seed=0): | |
| """Re-estimate the prior ON checkpoint bins; never replace the bin vocabulary.""" | |
| if not 0 <= mix <= 1: | |
| raise ValueError("Rebalance mixture must be in [0,1]") | |
| rng=np.random.default_rng(seed) | |
| chosen=rng.choice(indices,min(limit,len(indices)),replace=False) | |
| counts=np.ones(len(bins.centers),np.float64)*1e-3 | |
| for index in chosen: | |
| image=decode_image(table,index).resize((32,32)) | |
| ab=rgb2lab(np.asarray(image,dtype=np.float32)/255)[...,1:] | |
| # Low chroma is a heuristic, not proof that an image was originally B&W. | |
| if np.linalg.norm(ab,axis=-1).mean()<3: | |
| continue | |
| idx,weight=bins.encode(ab) | |
| np.add.at(counts,idx.ravel(),weight.ravel()) | |
| prior=counts/counts.sum() | |
| weights=1/((1-mix)*prior+mix/len(prior)) | |
| weights/=np.sum(prior*weights) | |
| return weights.astype(np.float32),prior.astype(np.float32) | |
| def write_manifest(path, table, train, validation, test, dataset_sha256): | |
| groups=[set(map(int,g)) for g in [train,validation,test]] | |
| assert not groups[0]&groups[1] and not groups[0]&groups[2] and not groups[1]&groups[2] | |
| content={'dataset':'johnowhitaker/imagenette2-320','dataset_sha256':dataset_sha256, | |
| 'rows':len(table),'split_seed':0,'legacy_holdout_fraction':.05, | |
| 'train':list(map(int,train)),'validation':list(map(int,validation)), 'test':list(map(int,test)), | |
| 'caveat':'Matches the recent seed-0 holdout; older upstream training exposure is not established.'} | |
| Path(path).write_text(json.dumps(content,indent=2)) | |
| return content | |