Spaces:
Sleeping
Sleeping
Download models/feature_utils.py from AthithyaLogan/SAMSpade: direct link, hf CLI and curl.
- Browser
- Download file 5.69 kB
-
https://huggingface.co/spaces/AthithyaLogan/SAMSpade/resolve/main/models/feature_utils.py
- Command line
-
hf download hf://spaces/AthithyaLogan/SAMSpade/models/feature_utils.py
-
curl -L -o feature_utils.py https://huggingface.co/spaces/AthithyaLogan/SAMSpade/resolve/main/models/feature_utils.py
5.69 kB
| """Utilities for tiling large aerial images and stitching probability maps. | |
| Also contains the GeoTIFF writer that preserves CRS/transform metadata so | |
| interpreters can load output rasters directly in QGIS / ArcGIS. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import List, Tuple | |
| import numpy as np | |
| import rasterio | |
| from rasterio.transform import from_bounds | |
| # --------------------------------------------------------------------------- | |
| # Image tiling | |
| # --------------------------------------------------------------------------- | |
| def tile_image_for_inference( | |
| image: np.ndarray, | |
| tile_size: int = 1024, | |
| overlap: int = 128, | |
| ) -> Tuple[List[np.ndarray], List[Tuple[int, int, int, int]]]: | |
| """Split a large HxWxC image into overlapping tiles. | |
| Returns: | |
| tiles: list of (tile_size x tile_size x C) uint8 arrays. | |
| coords: list of (x0, y0, x1, y1) pixel coordinates in the source image. | |
| """ | |
| h, w = image.shape[:2] | |
| stride = tile_size - overlap | |
| tiles: List[np.ndarray] = [] | |
| coords: List[Tuple[int, int, int, int]] = [] | |
| y = 0 | |
| while y < h: | |
| x = 0 | |
| while x < w: | |
| x1 = min(x + tile_size, w) | |
| y1 = min(y + tile_size, h) | |
| x0 = max(0, x1 - tile_size) | |
| y0 = max(0, y1 - tile_size) | |
| tile = image[y0:y1, x0:x1] | |
| # Pad to tile_size if the image edge is smaller | |
| if tile.shape[0] < tile_size or tile.shape[1] < tile_size: | |
| pad_h = tile_size - tile.shape[0] | |
| pad_w = tile_size - tile.shape[1] | |
| if image.ndim == 3: | |
| tile = np.pad(tile, ((0, pad_h), (0, pad_w), (0, 0))) | |
| else: | |
| tile = np.pad(tile, ((0, pad_h), (0, pad_w))) | |
| tiles.append(tile) | |
| coords.append((x0, y0, x1, y1)) | |
| x += stride | |
| y += stride | |
| return tiles, coords | |
| def stitch_tiles( | |
| tile_probs: List[np.ndarray], | |
| coords: List[Tuple[int, int, int, int]], | |
| original_shape: Tuple[int, int], | |
| ) -> np.ndarray: | |
| """Reconstruct a full probability map from tiled predictions. | |
| Overlap regions are resolved by Gaussian-weighted blending, which avoids | |
| hard seam artefacts. | |
| Args: | |
| tile_probs: List of HxW float32 probability arrays (values in [0,1]). | |
| coords: Matching list of (x0, y0, x1, y1) pixel coords. | |
| original_shape: (H, W) of the full image. | |
| Returns: | |
| HxW float32 probability map. | |
| """ | |
| H, W = original_shape | |
| accum = np.zeros((H, W), dtype=np.float64) | |
| weight = np.zeros((H, W), dtype=np.float64) | |
| for prob, (x0, y0, x1, y1) in zip(tile_probs, coords): | |
| h, w = y1 - y0, x1 - x0 | |
| prob_crop = prob[:h, :w] | |
| # 2-D Gaussian window centred on the tile | |
| gw = _gaussian_window(h, w) | |
| accum [y0:y1, x0:x1] += prob_crop * gw | |
| weight[y0:y1, x0:x1] += gw | |
| weight = np.where(weight > 0, weight, 1.0) | |
| return (accum / weight).astype(np.float32) | |
| # --------------------------------------------------------------------------- | |
| # GeoTIFF I/O | |
| # --------------------------------------------------------------------------- | |
| def load_geotiff_rgb(path: str) -> Tuple[np.ndarray, rasterio.DatasetReader]: | |
| """Load a GeoTIFF as HxWx3 uint8 array and return it with its open dataset | |
| (for CRS / transform metadata). Caller should close the dataset when done.""" | |
| src = rasterio.open(path) | |
| n = min(src.count, 3) | |
| bands = src.read(list(range(1, n + 1))) # [C, H, W] | |
| img = np.transpose(bands, (1, 2, 0)) # [H, W, C] | |
| if img.dtype != np.uint8: | |
| img = np.clip(img, 0, 255).astype(np.uint8) | |
| return img, src | |
| def save_probability_geotiff( | |
| prob_bands: List[np.ndarray], | |
| band_names: List[str], | |
| reference_path: str, | |
| output_path: str, | |
| ) -> None: | |
| """Save one or more probability bands as a multi-band GeoTIFF. | |
| CRS and affine transform are copied from *reference_path* so the output is | |
| immediately loadable in QGIS / ArcGIS at the correct location. | |
| Args: | |
| prob_bands: List of HxW float32 arrays, each in [0, 1]. | |
| band_names: Human-readable label for each band (written as metadata). | |
| reference_path: Source GeoTIFF from which CRS/transform are copied. | |
| output_path: Destination file path. | |
| """ | |
| Path(output_path).parent.mkdir(parents=True, exist_ok=True) | |
| with rasterio.open(reference_path) as ref: | |
| crs = ref.crs | |
| transform = ref.transform | |
| height, width = prob_bands[0].shape | |
| with rasterio.open( | |
| output_path, | |
| "w", | |
| driver="GTiff", | |
| height=height, | |
| width=width, | |
| count=len(prob_bands), | |
| dtype="float32", | |
| crs=crs, | |
| transform=transform, | |
| nodata=-1.0, | |
| compress="lzw", | |
| ) as dst: | |
| for i, (band, name) in enumerate(zip(prob_bands, band_names), start=1): | |
| dst.write(band.astype(np.float32), i) | |
| dst.update_tags(i, name=name) | |
| # --------------------------------------------------------------------------- | |
| # Internal helpers | |
| # --------------------------------------------------------------------------- | |
| def _gaussian_window(h: int, w: int, sigma_ratio: float = 0.3) -> np.ndarray: | |
| """Return a 2-D Gaussian weight matrix of shape (h, w).""" | |
| sigma_y = h * sigma_ratio | |
| sigma_x = w * sigma_ratio | |
| cy, cx = h / 2.0, w / 2.0 | |
| ys = np.arange(h) | |
| xs = np.arange(w) | |
| yy, xx = np.meshgrid(ys, xs, indexing="ij") | |
| return np.exp( | |
| -0.5 * ((yy - cy) ** 2 / sigma_y ** 2 + (xx - cx) ** 2 / sigma_x ** 2) | |
| ) | |