Spaces:
Configuration error
Configuration error
| """ | |
| Shared dataset machinery for RCLane. | |
| `LaneEncodeDataset` holds everything common to any annotation format: image resize + | |
| normalization, running `encode` to build the 5 GT maps, and caching that GT to disk in | |
| SPARSE form (foreground pixels only) so we don't recompute the slow shapely encode every | |
| epoch, nor store hundreds of GB of dense maps. | |
| A concrete dataset only implements `_load(idx)`, returning the raw image and lane | |
| polylines in the original image coordinate space. See `dataset_curvelanes.py`. | |
| """ | |
| import os | |
| import hashlib | |
| import numpy as np | |
| import cv2 | |
| import torch | |
| from torch.utils.data import Dataset | |
| from encode import encode | |
| # ImageNet normalization (lets us plug in pretrained MiT later) | |
| _MEAN = np.array([0.485, 0.456, 0.406], np.float32).reshape(1, 1, 3) | |
| _STD = np.array([0.229, 0.224, 0.225], np.float32).reshape(1, 1, 3) | |
| def normalize_image(img_bgr, W, H): | |
| """BGR uint8 -> resized, ImageNet-normalized (3, H, W) float tensor.""" | |
| img_r = cv2.resize(img_bgr, (W, H), interpolation=cv2.INTER_LINEAR) | |
| x = img_r[:, :, ::-1].astype(np.float32) / 255.0 # BGR -> RGB, [0,1] | |
| x = (x - _MEAN) / _STD | |
| return torch.from_numpy(np.ascontiguousarray(x.transpose(2, 0, 1))) | |
| def normalize_image_numpy(img_bgr, W, H): | |
| """Fast inference-only BGR uint8 -> normalized ``(1, 3, H, W)`` NumPy. | |
| ``cv2.dnn.blobFromImage`` performs resize, RGB channel swap and NCHW | |
| packing in compiled code. The result is numerically equivalent to | |
| :func:`normalize_image` within float32 rounding, while avoiding the | |
| temporary HWC float image and Torch tensor wrapper. | |
| """ | |
| images = cv2.dnn.blobFromImage( | |
| img_bgr, scalefactor=1.0 / 255.0, size=(W, H), mean=(0, 0, 0), | |
| swapRB=True, crop=False, | |
| ) | |
| images[0] -= _MEAN.reshape(3, 1, 1) | |
| images[0] /= _STD.reshape(3, 1, 1) | |
| return images | |
| def sparse_from_dense(gt): | |
| """Keep only foreground pixels of the dense GT maps.""" | |
| ys, xs = np.where(gt["seg_map"] > 0.5) | |
| return dict( | |
| ys=ys.astype(np.int32), | |
| xs=xs.astype(np.int32), | |
| up_arrow=gt["up_arrow"][:, ys, xs].T.astype(np.float32), # (N,2) | |
| down_arrow=gt["down_arrow"][:, ys, xs].T.astype(np.float32), | |
| up_bound=gt["up_bound"][0, ys, xs].astype(np.float32), # (N,) | |
| down_bound=gt["down_bound"][0, ys, xs].astype(np.float32), | |
| ) | |
| def dense_from_sparse(s, H, W): | |
| """Scatter sparse foreground values back to dense (2,H,W) maps.""" | |
| seg = np.zeros((H, W), np.float32) | |
| ua = np.zeros((2, H, W), np.float32) | |
| da = np.zeros((2, H, W), np.float32) | |
| ub = np.zeros((2, H, W), np.float32) | |
| db = np.zeros((2, H, W), np.float32) | |
| ys, xs = s["ys"], s["xs"] | |
| if len(ys) > 0: | |
| seg[ys, xs] = 1.0 | |
| ua[:, ys, xs] = s["up_arrow"].T | |
| da[:, ys, xs] = s["down_arrow"].T | |
| ub[:, ys, xs] = s["up_bound"] | |
| db[:, ys, xs] = s["down_bound"] | |
| return dict(seg_map=seg, up_arrow=ua, down_arrow=da, up_bound=ub, down_bound=db) | |
| class LaneEncodeDataset(Dataset): | |
| """Base class: resize + normalize + encode(+cache). Subclass provides `_load`.""" | |
| def __init__(self, img_size=(320, 800), cache_dir=None): | |
| self.H, self.W = img_size | |
| self.cache_dir = cache_dir | |
| if cache_dir: | |
| os.makedirs(cache_dir, exist_ok=True) | |
| # ---- to be implemented by subclasses ---- | |
| def _load(self, idx): | |
| """Return (img_bgr, lanes_orig, ow, oh, cache_key). | |
| lanes_orig: list of lanes, each a list of (x, y) in the ORIGINAL image space. | |
| cache_key : a unique string per sample (usually the image path). | |
| """ | |
| raise NotImplementedError | |
| # ---- shared machinery ---- | |
| def _scale(self, lanes, ow, oh): | |
| sx, sy = self.W / ow, self.H / oh | |
| return [[(x * sx, y * sy) for x, y in lane] for lane in lanes] | |
| def _cache_path(self, key): | |
| h = hashlib.md5(key.encode()).hexdigest() | |
| return os.path.join(self.cache_dir, h + ".npz") | |
| def _get_gt(self, key, lanes_orig, ow, oh): | |
| if self.cache_dir: | |
| cp = self._cache_path(key) | |
| if os.path.exists(cp): | |
| try: | |
| with np.load(cp) as sparse: | |
| return dense_from_sparse(sparse, self.H, self.W) | |
| except (OSError, ValueError, EOFError): | |
| # A job killed during an older, non-atomic cache write may | |
| # leave a truncated npz. Rebuild it instead of killing a | |
| # long multi-GPU run. | |
| pass | |
| gt = encode(self._scale(lanes_orig, ow, oh), img_size=(self.H, self.W)) | |
| if self.cache_dir: | |
| # Many DataLoader/DDP processes can discover the same missing key | |
| # at once (DistributedSampler may pad one sample). Write privately | |
| # and atomically publish the completed archive so readers never see | |
| # a half-written npz. | |
| tmp = f"{cp}.{os.getpid()}.tmp.npz" | |
| try: | |
| np.savez(tmp, **sparse_from_dense(gt)) | |
| os.replace(tmp, cp) | |
| finally: | |
| if os.path.exists(tmp): | |
| os.remove(tmp) | |
| return gt | |
| def __getitem__(self, idx): | |
| img_bgr, lanes_orig, ow, oh, key = self._load(idx) | |
| gt = self._get_gt(key, lanes_orig, ow, oh) | |
| x = normalize_image(img_bgr, self.W, self.H) | |
| target = { | |
| "seg_map": torch.from_numpy(gt["seg_map"]).long(), | |
| "up_arrow": torch.from_numpy(gt["up_arrow"]).float(), | |
| "down_arrow": torch.from_numpy(gt["down_arrow"]).float(), | |
| "up_bound": torch.from_numpy(gt["up_bound"]).float(), | |
| "down_bound": torch.from_numpy(gt["down_bound"]).float(), | |
| } | |
| return x, target | |
| def collate(batch): | |
| """Stack images and each target map along a new batch dim.""" | |
| imgs = torch.stack([b[0] for b in batch], 0) | |
| keys = batch[0][1].keys() | |
| targets = {k: torch.stack([b[1][k] for b in batch], 0) for k in keys} | |
| return imgs, targets | |