Building Patches, Augmentation, and DataLoader
Steps 3–5 of the CEAMLS guide. Crop 512×512 patches centred on every building polygon, save as .npy, then wrap them in a PyTorch DataLoader ready for training on Jun 23.
Crop 512×512 patches around every building
python1import json, numpy as np, rasterio2from shapely import wkt3from shapely.geometry import shape4import pandas as pd, os56meta = pd.read_csv('/content/drive/MyDrive/ceamls-project/data/pairs.csv')7PATCH_DIR = '/content/drive/MyDrive/ceamls-project/data/patches'8os.makedirs(PATCH_DIR, exist_ok=True)9PATCH = 5121011records = []12for _, row in meta.iterrows():13 with rasterio.open(row['post_img']) as src:14 img = src.read([5, 3, 2]).astype(np.float32) / 65535.015 img = np.transpose(img, (1, 2, 0))16 H, W = img.shape[:2]17 with open(row['post_lbl']) as f:18 lbl = json.load(f)19 for feat in lbl['features']['xy']:20 poly = wkt.loads(feat['wkt']) if isinstance(feat['wkt'], str) else shape(feat['wkt'])21 cx, cy = int(poly.centroid.x), int(poly.centroid.y)22 x0 = max(0, min(W - PATCH, cx - PATCH // 2))23 y0 = max(0, min(H - PATCH, cy - PATCH // 2))24 patch = img[y0:y0 + PATCH, x0:x0 + PATCH]25 cls = feat['properties'].get('subtype', 'no-damage')26 fname = f"{os.path.basename(row['post_img'])[:-4]}_b{feat['properties']['uid']}.npy"27 np.save(os.path.join(PATCH_DIR, fname), patch)28 records.append({'patch': fname, 'class': cls, 'event': row['event']})2930pd.DataFrame(records).to_csv(f'{PATCH_DIR}/index.csv', index=False)31print('Saved', len(records), 'building patches')
Plain-English explanation
Walk every (pre, post) pair, read the polygon centroid for each building, and snip a 512×512 square centred on it.
Why it matters
The damage class (no-damage / minor / major / destroyed) is a property of each building polygon. Cropping per-building turns one tile into 10–80 training samples.
Line by line
poly.centroidShapely returns the (x,y) centre of the polygon in pixel coordinates.max(0, min(W-PATCH, ...))Clamps the crop so it never falls off the tile edge.feat['properties']['subtype']The xBD damage label: 'no-damage', 'minor-damage', 'major-damage', 'destroyed'..npyPre-cropped patches save 20× I/O versus re-opening the GeoTIFF every step.
Calculate class weights for imbalanced damage classes
python1import pandas as pd, numpy as np23idx = pd.read_csv('/content/drive/MyDrive/ceamls-project/data/patches/index.csv')4class_to_id = {'no-damage':0, 'minor-damage':1, 'major-damage':2, 'destroyed':3}5counts = idx['class'].map(class_to_id).value_counts().sort_index()6print('Per-class counts:\n', counts)78# inverse-frequency weights (then normalised so weights sum to 4)9weights = 1.0 / counts10weights = weights / weights.sum() * len(counts)11print('Class weights:', weights.values)
Plain-English explanation
In xBD ~80% of buildings are 'no-damage'. Without weighting, the model wins by always predicting 'no-damage'.
Why it matters
Pass these weights into torch.nn.CrossEntropyLoss(weight=...) so a missed 'destroyed' costs ~10× more than a missed 'no-damage'.
Augmentation pipeline (Albumentations)
python1!pip install albumentations --quiet2import albumentations as A3from albumentations.pytorch import ToTensorV245def train_aug():6 return A.Compose([7 A.HorizontalFlip(p=0.5),8 A.VerticalFlip(p=0.5),9 A.RandomRotate90(p=0.5),10 A.RandomBrightnessContrast(0.2, 0.2, p=0.5),11 A.GaussNoise(var_limit=(5, 25), p=0.3),12 ToTensorV2(),13 ])1415def val_aug():16 return A.Compose([ToTensorV2()]) # NEVER augment validation
Plain-English explanation
Augmentation = synthetic data via flips, rotations, brightness jitter. It multiplies your effective dataset size.
Why it matters
Satellite imagery is rotation-invariant (a building looks 'damaged' regardless of orientation). Flipping/rotating is free additional training data — but only during training, never validation.
Common errors
PyTorch Dataset class
python1import torch, numpy as np, pandas as pd2from torch.utils.data import Dataset34CLASS_TO_ID = {'no-damage':0, 'minor-damage':1, 'major-damage':2, 'destroyed':3}56class XBDPatchDataset(Dataset):7 def __init__(self, index_csv, patch_dir, transform=None, siamese=False):8 self.idx = pd.read_csv(index_csv)9 self.patch_dir = patch_dir10 self.transform = transform11 self.siamese = siamese1213 def __len__(self):14 return len(self.idx)1516 def __getitem__(self, i):17 row = self.idx.iloc[i]18 post = np.load(f"{self.patch_dir}/{row['patch']}")19 pre = np.load(f"{self.patch_dir}/{row['patch'].replace('_post_', '_pre_')}") \20 if self.siamese else post.copy()21 cls = CLASS_TO_ID[row['class']]22 label = torch.tensor(cls, dtype=torch.long)23 if self.transform:24 post = self.transform(image=post)['image']25 pre = self.transform(image=pre)['image']26 return {'pre': pre, 'post': post, 'label': label}
Plain-English explanation
A Dataset is just a class with __len__ and __getitem__. PyTorch's DataLoader handles batching, shuffling, and parallel I/O for you.
Why it matters
One Dataset class serves three models: baseline (post only), Siamese (pre + post), ViT (post only with different transforms).
DataLoader with train/val/test split
python1from torch.utils.data import DataLoader, random_split23def get_loaders(index_csv, patch_dir, batch=16, siamese=False):4 full = XBDPatchDataset(index_csv, patch_dir, transform=train_aug(), siamese=siamese)5 n = len(full)6 n_tr, n_va = int(0.8*n), int(0.1*n)7 tr, va, te = random_split(full, [n_tr, n_va, n - n_tr - n_va],8 generator=torch.Generator().manual_seed(42))9 return (10 DataLoader(tr, batch_size=batch, shuffle=True, num_workers=2, pin_memory=True),11 DataLoader(va, batch_size=batch, shuffle=False, num_workers=2, pin_memory=True),12 DataLoader(te, batch_size=batch, shuffle=False, num_workers=2, pin_memory=True),13 )1415# Sanity check16tr, va, te = get_loaders('/content/drive/MyDrive/ceamls-project/data/patches/index.csv',17 '/content/drive/MyDrive/ceamls-project/data/patches', batch=16)18batch = next(iter(tr))19assert batch['post'].shape == (16, 3, 512, 512)20print('DataLoader working correctly!')
Plain-English explanation
The DataLoader feeds the model in shuffled mini-batches with parallel disk I/O.
Why it matters
seed=42 makes the 80/10/10 split reproducible. Every teammate gets the exact same training, validation, and test rows.
Common errors
Why fix the random_split seed?