yx21e commited on
Commit
7c326b9
·
verified ·
1 Parent(s): 977e52c

Add serving-oriented tiled inference and jittered training support

Browse files
Files changed (1) hide show
  1. models/wildfire_fm/tiled_inference.py +112 -0
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"]