Download src/pmdm/dataset.py from siddhant20/task1: direct link, hf CLI and curl.
- Browser
- Download file 7.51 kB
-
https://huggingface.co/siddhant20/task1/resolve/main/src/pmdm/dataset.py
- Command line
-
hf download hf://siddhant20/task1/src/pmdm/dataset.py
-
curl -L -o dataset.py https://huggingface.co/siddhant20/task1/resolve/main/src/pmdm/dataset.py
7.51 kB
| """Torch datasets and CenterNet-style target encoding.""" | |
| from __future__ import annotations | |
| from collections import defaultdict | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from torch.utils.data import Dataset | |
| from .config import OUT_STRIDE, PREP, SYNTH, TILE, TRAIN_CSV | |
| from .preprocess import load_prepared, stream_tensors | |
| from .tiles import boxes_in_tile, pad_to | |
| def load_gt(csv_path: Path | None = None) -> dict[int, np.ndarray]: | |
| """Ground-truth boxes keyed by train pair index.""" | |
| df = pd.read_csv(csv_path or TRAIN_CSV) | |
| out: dict[int, list] = defaultdict(list) | |
| for r in df.itertuples(index=False): | |
| idx = int(str(r.template_image).split("_")[-1].split(".")[0]) | |
| out[idx].append([r.left_x, r.top_y, r.right_x, r.bottom_y]) | |
| return {k: np.asarray(v, np.float32) for k, v in out.items()} | |
| def gaussian_radius(h: float, w: float, min_overlap: float = 0.5) -> float: | |
| """CenterNet radius: keeps a shifted box above min_overlap IoU.""" | |
| a1, b1, c1 = 1, (h + w), w * h * (1 - min_overlap) / (1 + min_overlap) | |
| r1 = (b1 - np.sqrt(max(b1 ** 2 - 4 * a1 * c1, 0))) / (2 * a1) | |
| a2, b2, c2 = 4, 2 * (h + w), (1 - min_overlap) * w * h | |
| r2 = (b2 - np.sqrt(max(b2 ** 2 - 4 * a2 * c2, 0))) / (2 * a2) | |
| a3 = 4 * min_overlap | |
| b3 = -2 * min_overlap * (h + w) | |
| c3 = (min_overlap - 1) * w * h | |
| r3 = (-b3 + np.sqrt(max(b3 ** 2 - 4 * a3 * c3, 0))) / (2 * a3) | |
| return max(1.0, float(min(r1, r2, r3))) | |
| def draw_gaussian(heatmap: np.ndarray, cx: int, cy: int, radius: int) -> None: | |
| diameter = 2 * radius + 1 | |
| sigma = diameter / 6.0 | |
| ax = np.arange(-radius, radius + 1, dtype=np.float32) | |
| g = np.exp(-(ax[None, :] ** 2 + ax[:, None] ** 2) / (2 * sigma ** 2)) | |
| h, w = heatmap.shape | |
| left, right = min(cx, radius), min(w - cx, radius + 1) | |
| top, bottom = min(cy, radius), min(h - cy, radius + 1) | |
| if right <= -left or bottom <= -top: | |
| return | |
| region = heatmap[cy - top:cy + bottom, cx - left:cx + right] | |
| masked = g[radius - top:radius + bottom, radius - left:radius + right] | |
| np.maximum(region, masked, out=region) | |
| def encode_targets(boxes: np.ndarray, tile: int = TILE, stride: int = OUT_STRIDE): | |
| """Boxes in tile coordinates -> heatmap, wh, offset, regression mask, seg mask.""" | |
| size = tile // stride | |
| hm = np.zeros((1, size, size), np.float32) | |
| wh = np.zeros((2, size, size), np.float32) | |
| off = np.zeros((2, size, size), np.float32) | |
| reg = np.zeros((1, size, size), np.float32) | |
| seg = np.zeros((1, size, size), np.float32) | |
| for x1, y1, x2, y2 in boxes: | |
| bw, bh = x2 - x1, y2 - y1 | |
| if bw <= 1 or bh <= 1: | |
| continue | |
| cx, cy = (x1 + x2) / 2 / stride, (y1 + y2) / 2 / stride | |
| ix, iy = int(cx), int(cy) | |
| if not (0 <= ix < size and 0 <= iy < size): | |
| continue | |
| draw_gaussian(hm[0], ix, iy, int(gaussian_radius(bh / stride, bw / stride))) | |
| wh[:, iy, ix] = [bw, bh] | |
| off[:, iy, ix] = [cx - ix, cy - iy] | |
| reg[0, iy, ix] = 1.0 | |
| sx1, sy1 = int(np.floor(x1 / stride)), int(np.floor(y1 / stride)) | |
| sx2, sy2 = int(np.ceil(x2 / stride)), int(np.ceil(y2 / stride)) | |
| seg[0, max(0, sy1):min(size, sy2), max(0, sx1):min(size, sx2)] = 1.0 | |
| return hm, wh, off, reg, seg | |
| def photometric_jitter(photo: np.ndarray, rng: np.random.RandomState) -> np.ndarray: | |
| """Extra print/scan-like noise on the photo stream only.""" | |
| out = photo.astype(np.float32) | |
| out = out * rng.uniform(0.92, 1.08) + rng.uniform(-12, 12) | |
| if rng.rand() < 0.5: | |
| out = cv2.GaussianBlur(out, (0, 0), rng.uniform(0.3, 1.0)) | |
| if rng.rand() < 0.7: | |
| out += rng.normal(0, rng.uniform(1.0, 5.0), out.shape).astype(np.float32) | |
| return np.clip(out, 0, 255).astype(np.uint8) | |
| class GroupedSampler(torch.utils.data.Sampler): | |
| """Shuffle pairs, keep a pair's samples adjacent. | |
| Every sample decodes four full-page PNGs, so random access would decode the | |
| same pair `samples_per_pair` times per epoch. Grouping lets the one-entry | |
| cache in the dataset serve the rest of the group. | |
| """ | |
| def __init__(self, n_pairs: int, samples_per_pair: int, seed: int = 0): | |
| self.n_pairs = n_pairs | |
| self.samples_per_pair = samples_per_pair | |
| self.seed = seed | |
| self.epoch = 0 | |
| def set_epoch(self, epoch: int) -> None: | |
| self.epoch = epoch | |
| def __len__(self) -> int: | |
| return self.n_pairs * self.samples_per_pair | |
| def __iter__(self): | |
| rng = np.random.RandomState(self.seed + self.epoch) | |
| for pair in rng.permutation(self.n_pairs): | |
| base = int(pair) * self.samples_per_pair | |
| for k in rng.permutation(self.samples_per_pair): | |
| yield base + int(k) | |
| class PairTileDataset(Dataset): | |
| """Random tiles from prepared pairs, biased towards annotated differences.""" | |
| def __init__(self, items: list[tuple[str, int]], gt: dict, samples_per_pair: int = 8, | |
| tile: int = TILE, positive_ratio: float = 0.6, augment: bool = True, | |
| prep_root: Path | None = None): | |
| self.items = items | |
| self.gt = gt | |
| self.samples_per_pair = samples_per_pair | |
| self.tile = tile | |
| self.positive_ratio = positive_ratio | |
| self.augment = augment | |
| self.prep_root = prep_root or PREP | |
| self._cache_key = None | |
| self._cache_val = None | |
| def __len__(self) -> int: | |
| return len(self.items) * self.samples_per_pair | |
| def _key(self, split: str, idx: int): | |
| return (split, idx) | |
| def _load(self, split: str, idx: int): | |
| key = (split, idx) | |
| if key != self._cache_key: | |
| root = self.prep_root if split != "synth" else SYNTH | |
| self._cache_val = load_prepared(split, idx, root) | |
| self._cache_key = key | |
| return self._cache_val | |
| def __getitem__(self, i: int): | |
| split, idx = self.items[i // self.samples_per_pair] | |
| rng = np.random.RandomState((i * 9973 + idx * 7919) % (2 ** 31)) | |
| t, p, tn, pn = self._load(split, idx) | |
| boxes = self.gt.get(self._key(split, idx), np.zeros((0, 4), np.float32)) | |
| t, p = pad_to(t, self.tile), pad_to(p, self.tile) | |
| tn, pn = pad_to(tn, self.tile), pad_to(pn, self.tile) | |
| h, w = t.shape[:2] | |
| if len(boxes) and rng.rand() < self.positive_ratio: | |
| bx = boxes[rng.randint(len(boxes))] | |
| cx, cy = (bx[0] + bx[2]) / 2, (bx[1] + bx[3]) / 2 | |
| x0 = int(np.clip(cx - self.tile / 2 + rng.randint(-200, 201), 0, max(0, w - self.tile))) | |
| y0 = int(np.clip(cy - self.tile / 2 + rng.randint(-200, 201), 0, max(0, h - self.tile))) | |
| else: | |
| x0 = rng.randint(0, max(1, w - self.tile + 1)) | |
| y0 = rng.randint(0, max(1, h - self.tile + 1)) | |
| sl = (slice(y0, y0 + self.tile), slice(x0, x0 + self.tile)) | |
| tc, pc, tnc, pnc = t[sl], p[sl], tn[sl], pn[sl] | |
| if self.augment: | |
| pc = photometric_jitter(pc, rng) | |
| a, b = stream_tensors(tc, pc, tnc, pnc) | |
| local = boxes_in_tile(boxes, x0, y0, self.tile) | |
| hm, wh, off, reg, seg = encode_targets(local, self.tile) | |
| return { | |
| "a": torch.from_numpy(a), | |
| "b": torch.from_numpy(b), | |
| "hm": torch.from_numpy(hm), | |
| "wh": torch.from_numpy(wh), | |
| "off": torch.from_numpy(off), | |
| "reg": torch.from_numpy(reg), | |
| "seg": torch.from_numpy(seg), | |
| } | |