ba-t / ba_t /data.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
452a3e0 verified
Raw History Blame Contribute Delete
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()