Add serving-oriented tiled inference and jittered training support
Browse files
models/wildfire_fm/tiled_inference.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Overlap-tiled inference helpers for WildFIRE-FM probability maps."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Iterable, Tuple
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _starts(length: int, tile_size: int, stride: int) -> list[int]:
|
| 12 |
+
if length <= tile_size:
|
| 13 |
+
return [0]
|
| 14 |
+
out = list(range(0, max(length - tile_size + 1, 1), stride))
|
| 15 |
+
last = length - tile_size
|
| 16 |
+
if out[-1] != last:
|
| 17 |
+
out.append(last)
|
| 18 |
+
return out
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _crop_slices(top: int, left: int, tile_size: int, height: int, width: int, halo: int) -> Tuple[slice, slice, slice, slice]:
|
| 22 |
+
y0 = 0 if top == 0 else halo
|
| 23 |
+
x0 = 0 if left == 0 else halo
|
| 24 |
+
y1 = tile_size if top + tile_size >= height else tile_size - halo
|
| 25 |
+
x1 = tile_size if left + tile_size >= width else tile_size - halo
|
| 26 |
+
return slice(y0, y1), slice(x0, x1), slice(top + y0, top + y1), slice(left + x0, left + x1)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def predict_probability_tiled(
|
| 30 |
+
model: torch.nn.Module,
|
| 31 |
+
x: torch.Tensor,
|
| 32 |
+
tile_size: int = 32,
|
| 33 |
+
stride: int = 16,
|
| 34 |
+
halo: int = 8,
|
| 35 |
+
device: torch.device | str | None = None,
|
| 36 |
+
batch_size: int = 16,
|
| 37 |
+
) -> torch.Tensor:
|
| 38 |
+
"""Predict a full probability map from an input tensor using overlap tiles.
|
| 39 |
+
|
| 40 |
+
Parameters
|
| 41 |
+
----------
|
| 42 |
+
model:
|
| 43 |
+
WildFIRE-FM model returning logits or ``(logits, aux_logits)``.
|
| 44 |
+
x:
|
| 45 |
+
Input tensor in ``[C, H, W]`` or ``[1, C, H, W]`` order.
|
| 46 |
+
tile_size:
|
| 47 |
+
Spatial tile size used for model calls.
|
| 48 |
+
stride:
|
| 49 |
+
Distance between tile origins. Use a value smaller than ``tile_size``
|
| 50 |
+
for overlap.
|
| 51 |
+
halo:
|
| 52 |
+
Number of pixels cropped away from interior tile borders before
|
| 53 |
+
stitching. Border tiles keep the image edge.
|
| 54 |
+
device:
|
| 55 |
+
Device for inference. Defaults to the model parameter device.
|
| 56 |
+
batch_size:
|
| 57 |
+
Number of tiles evaluated per model call.
|
| 58 |
+
|
| 59 |
+
Returns
|
| 60 |
+
-------
|
| 61 |
+
torch.Tensor
|
| 62 |
+
Probability map in ``[H, W]`` order on CPU.
|
| 63 |
+
"""
|
| 64 |
+
if x.ndim == 3:
|
| 65 |
+
x = x.unsqueeze(0)
|
| 66 |
+
if x.ndim != 4 or x.shape[0] != 1:
|
| 67 |
+
raise ValueError("x must have shape [C, H, W] or [1, C, H, W].")
|
| 68 |
+
if tile_size <= 0 or stride <= 0:
|
| 69 |
+
raise ValueError("tile_size and stride must be positive.")
|
| 70 |
+
if halo < 0 or halo * 2 >= tile_size:
|
| 71 |
+
raise ValueError("halo must be non-negative and smaller than tile_size / 2.")
|
| 72 |
+
|
| 73 |
+
if device is None:
|
| 74 |
+
try:
|
| 75 |
+
device = next(model.parameters()).device
|
| 76 |
+
except StopIteration:
|
| 77 |
+
device = torch.device("cpu")
|
| 78 |
+
device = torch.device(device)
|
| 79 |
+
model.eval()
|
| 80 |
+
|
| 81 |
+
_, channels, height, width = x.shape
|
| 82 |
+
pad_h = max(tile_size - height, 0)
|
| 83 |
+
pad_w = max(tile_size - width, 0)
|
| 84 |
+
if pad_h or pad_w:
|
| 85 |
+
x_work = F.pad(x, (0, pad_w, 0, pad_h), mode="replicate")
|
| 86 |
+
else:
|
| 87 |
+
x_work = x
|
| 88 |
+
_, _, work_h, work_w = x_work.shape
|
| 89 |
+
|
| 90 |
+
output = torch.zeros((work_h, work_w), dtype=torch.float32)
|
| 91 |
+
weight = torch.zeros((work_h, work_w), dtype=torch.float32)
|
| 92 |
+
coords = [(top, left) for top in _starts(work_h, tile_size, stride) for left in _starts(work_w, tile_size, stride)]
|
| 93 |
+
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
for start in range(0, len(coords), batch_size):
|
| 96 |
+
batch_coords = coords[start : start + batch_size]
|
| 97 |
+
tiles = torch.cat(
|
| 98 |
+
[x_work[:, :, top : top + tile_size, left : left + tile_size] for top, left in batch_coords],
|
| 99 |
+
dim=0,
|
| 100 |
+
).to(device)
|
| 101 |
+
pred = model(tiles)
|
| 102 |
+
logits = pred[0] if isinstance(pred, tuple) else pred
|
| 103 |
+
probs = torch.sigmoid(logits.float()).detach().cpu()[:, 0]
|
| 104 |
+
for prob, (top, left) in zip(probs, batch_coords):
|
| 105 |
+
sy, sx, dy, dx = _crop_slices(top, left, tile_size, work_h, work_w, halo)
|
| 106 |
+
output[dy, dx] += prob[sy, sx]
|
| 107 |
+
weight[dy, dx] += 1.0
|
| 108 |
+
output = output / weight.clamp_min(1.0)
|
| 109 |
+
return output[:height, :width].contiguous()
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
__all__ = ["predict_probability_tiled"]
|