Spaces:
Running on Zero
Running on Zero
Download ba_t/data.py from hugging-apps/ba-t: direct link, hf CLI and curl.
- Browser
- Download file 6 kB
-
https://huggingface.co/spaces/hugging-apps/ba-t/resolve/main/ba_t/data.py
- Command line
-
hf download hf://spaces/hugging-apps/ba-t/ba_t/data.py
-
curl -L -o data.py https://huggingface.co/spaces/hugging-apps/ba-t/resolve/main/ba_t/data.py
6 kB
| """Load images for BA-T inference from image paths and/or folders. | |
| Every image goes through the same deterministic preprocessing as the training / | |
| evaluation dataloaders, with the principal point assumed at the image centre: | |
| 1. crop to a window centred on the image centre (a no-op for even sizes); | |
| 2. Lanczos-rescale so that the image just covers the target resolution | |
| (the short side becomes 224 for the released 224x224 model); | |
| 3. centre-crop to the target resolution; | |
| 4. normalize RGB to [-1, 1] (``ImgNorm``). | |
| A view is a dict ``{'img': (3,H,W) float tensor, 'true_shape': (2,) int32 tensor | |
| (H, W), 'path': str, 'crop': dict}``; ``crop`` records the transform so that | |
| pixels can be mapped back to the original image (``to_original_pixels``). | |
| ``collate_views`` stacks views into the batched dicts that ``BAT.forward`` | |
| expects. | |
| """ | |
| import os | |
| import re | |
| import numpy as np | |
| import PIL.Image | |
| import PIL.ImageOps | |
| import torch | |
| from .utils.cropping import rescale_image, crop_image, center_crop_bbox | |
| from .utils.image import ImgNorm | |
| IMG_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff', '.webp') | |
| DEFAULT_RESOLUTION = (224, 224) # (W, H) the released model was trained at | |
| def _natural_key(s): | |
| return [int(t) if t.isdigit() else t.lower() for t in re.split(r'(\d+)', s)] | |
| def collect_image_paths(inputs): | |
| """Expand a path or a list of paths (image files and/or folders) into a list of image files. | |
| Folders are scanned non-recursively and sorted in natural order (frame2 < frame10); | |
| explicitly listed files keep the given order. | |
| """ | |
| if isinstance(inputs, (str, os.PathLike)): | |
| inputs = [inputs] | |
| paths = [] | |
| for p in inputs: | |
| p = os.fspath(p) | |
| if os.path.isdir(p): | |
| files = [os.path.join(p, f) for f in os.listdir(p) if f.lower().endswith(IMG_EXTENSIONS)] | |
| if not files: | |
| raise FileNotFoundError(f'no images ({", ".join(IMG_EXTENSIONS)}) found in folder {p}') | |
| paths += sorted(files, key=_natural_key) | |
| elif os.path.isfile(p): | |
| paths.append(p) | |
| else: | |
| raise FileNotFoundError(f'{p} is neither an image file nor a folder') | |
| return paths | |
| def _parse_resolution(resolution): | |
| if isinstance(resolution, int): | |
| return resolution, resolution | |
| width, height = resolution | |
| assert width % 16 == 0 and height % 16 == 0, f'resolution {resolution} must be a multiple of the patch size 16' | |
| return int(width), int(height) | |
| def preprocess_image(image, resolution=DEFAULT_RESOLUTION): | |
| """Crop + resize a PIL image like the training dataloader (deterministic, no augmentation). | |
| Returns the processed PIL image and a dict describing the transform. | |
| """ | |
| width, height = _parse_resolution(resolution) | |
| image = image.convert('RGB') | |
| W, H = image.size | |
| # 1. crop centred on the principal point (assumed at the image centre) | |
| cx, cy = int(W/2), int(H/2) | |
| min_margin_x = min(cx, W-cx) | |
| min_margin_y = min(cy, H-cy) | |
| l1, t1 = cx - min_margin_x, cy - min_margin_y | |
| image = crop_image(image, (l1, t1, cx + min_margin_x, cy + min_margin_y)) | |
| # the target resolution is landscape; use it transposed for portrait images | |
| target = (width, height) | |
| if width != height and image.size[1] > 1.1 * image.size[0]: | |
| target = (height, width) | |
| # 2. high-quality Lanczos down-scaling, 3. centre crop | |
| image, scale = rescale_image(image, target) | |
| l2, t2, r2, b2 = center_crop_bbox(image.size, target) | |
| image = crop_image(image, (l2, t2, r2, b2)) | |
| crop = {'orig_size': (W, H), 'offset1': (int(l1), int(t1)), 'scale': float(scale), | |
| 'offset2': (int(l2), int(t2)), 'size': image.size} | |
| return image, crop | |
| def to_original_pixels(uv, crop): | |
| """Map pixel coordinates (..., 2) [u=column, v=row] of a preprocessed image back to | |
| the original image (pixel centres at integer coordinates).""" | |
| uv = np.asarray(uv, dtype=np.float64) | |
| off2 = np.asarray(crop['offset2'], dtype=np.float64) | |
| off1 = np.asarray(crop['offset1'], dtype=np.float64) | |
| return (uv + off2 + 0.5) / crop['scale'] - 0.5 + off1 | |
| def load_image(path, resolution=DEFAULT_RESOLUTION): | |
| """Read one image file (EXIF orientation applied) and preprocess it into a view dict.""" | |
| with PIL.Image.open(path) as im: | |
| image = PIL.ImageOps.exif_transpose(im).convert('RGB') | |
| image, crop = preprocess_image(image, resolution) | |
| width, height = image.size | |
| return {'img': ImgNorm(image), | |
| 'true_shape': torch.tensor((height, width), dtype=torch.int32), | |
| 'path': os.fspath(path), | |
| 'crop': crop} | |
| class ImageViews(torch.utils.data.Dataset): | |
| """Dataset over image paths / folders, yielding preprocessed view dicts.""" | |
| def __init__(self, inputs, resolution=DEFAULT_RESOLUTION): | |
| self.paths = collect_image_paths(inputs) | |
| self.resolution = resolution | |
| def __len__(self): | |
| return len(self.paths) | |
| def __getitem__(self, idx): | |
| return load_image(self.paths[idx], self.resolution) | |
| def collate_views(views): | |
| """Stack a list of view dicts into one batched view: 'img' (B,3,H,W), 'true_shape' (B,2). | |
| All views must have the same shape (always true for square resolutions). | |
| """ | |
| return {'img': torch.stack([v['img'] for v in views]), | |
| 'true_shape': torch.stack([v['true_shape'] for v in views]), | |
| 'path': [v['path'] for v in views], | |
| 'crop': [v['crop'] for v in views]} | |
| def load_views(inputs, resolution=DEFAULT_RESOLUTION): | |
| """Load images from paths / folders as a list of single-image batched views (B=1).""" | |
| ds = ImageViews(inputs, resolution) | |
| return [collate_views([ds[i]]) for i in range(len(ds))] | |
| def denormalize(img): | |
| """(3,H,W) or (B,3,H,W) tensor in [-1,1] -> uint8 numpy (H,W,3) or (B,H,W,3).""" | |
| img = ((img.detach().cpu().float() + 1.0) * 127.5).round().clamp(0, 255).to(torch.uint8) | |
| return img.movedim(-3, -1).numpy() | |