RhodWeo commited on
Commit
84591e4
·
verified ·
1 Parent(s): b148422

Add/update predictor.py

Browse files
Files changed (1) hide show
  1. predictor.py +36 -25
predictor.py CHANGED
@@ -5,12 +5,15 @@ Unified CHM inference for WEO-SAS/chm-meta (v1) and WEO-SAS/chm-meta-v2 (v2).
5
 
6
  Both versions expose the same interface — only the model directory changes:
7
 
8
- predictor = CHMPredictor("WEO-SAS/chm-meta") # v1: SSL ViT-H + DPT
9
- predictor = CHMPredictor("WEO-SAS/chm-meta-v2") # v2: DINOv3 ViT-L + DPT
10
 
11
  chm = predictor.predict(image) # (3,H,W) float32 → (H,W) metres
12
  predictor.predict_tif("in.tif", "out.tif") # full GeoTIFF pipeline
13
 
 
 
 
14
  Requirements
15
  ------------
16
  Both versions: torch, numpy, rasterio, Pillow
@@ -38,11 +41,19 @@ class CHMPredictor:
38
 
39
  Parameters
40
  ----------
41
- model_dir : local path to a downloaded WEO-SAS CHM model repo
42
- device : torch device (auto-detected if None)
 
 
43
  """
44
 
45
- def __init__(self, model_dir: str, device: Optional[torch.device] = None):
 
 
 
 
 
 
46
  model_dir = Path(model_dir)
47
  with open(model_dir / "predictor_config.json") as f:
48
  cfg = json.load(f)
@@ -59,17 +70,21 @@ class CHMPredictor:
59
  "cuda" if torch.cuda.is_available() else "cpu"
60
  )
61
 
62
- weights_path = model_dir / cfg["weights_file"]
63
-
64
- if self.model_version == "v1":
65
- self._load_v1(model_dir, weights_path)
 
 
66
  elif self.model_version == "v2":
67
  self._load_v2(model_dir)
68
  else:
69
  raise ValueError(f"Unknown model_version '{self.model_version}' in predictor_config.json")
70
 
 
 
71
  # ------------------------------------------------------------------
72
- # Model loading
73
  # ------------------------------------------------------------------
74
 
75
  def _load_v1(self, model_dir: Path, weights_path: Path) -> None:
@@ -78,7 +93,7 @@ class CHMPredictor:
78
 
79
  self.model = SSLModule(ssl_path=str(weights_path), local_path=str(weights_path))
80
  self.processor = None
81
- self.model.to(self.device).eval()
82
 
83
  def _load_v2(self, model_dir: Path) -> None:
84
  try:
@@ -91,7 +106,7 @@ class CHMPredictor:
91
 
92
  self.model = CHMv2ForDepthEstimation.from_pretrained(str(model_dir))
93
  self.processor = CHMv2ImageProcessorFast.from_pretrained(str(model_dir))
94
- self.model.to(self.device).eval()
95
 
96
  # ------------------------------------------------------------------
97
  # Per-tile inference
@@ -109,12 +124,11 @@ class CHMPredictor:
109
  """tile: (3, patch_size, patch_size) float32 in [0, 1] → (patch_size, patch_size)"""
110
  from PIL import Image # noqa: PLC0415
111
 
112
- # Processor expects uint8 HWC; it applies its own rescale + normalisation
113
- arr_hwc = (tile * 255).clip(0, 255).astype(np.uint8).transpose(1, 2, 0)
114
- pil_img = Image.fromarray(arr_hwc)
115
- H, W = pil_img.height, pil_img.width
116
- inputs = self.processor(images=pil_img, return_tensors="pt")
117
- inputs = {k: v.to(self.device) for k, v in inputs.items()}
118
  with torch.no_grad():
119
  outputs = self.model(**inputs)
120
  depth = self.processor.post_process_depth_estimation(
@@ -149,11 +163,9 @@ class CHMPredictor:
149
  if image.ndim != 3 or image.shape[0] != 3:
150
  raise ValueError(f"Expected (3, H, W), got {image.shape}")
151
 
152
- _, H, W = image.shape
153
- ps = self.patch_size
154
- st = self.stride
155
 
156
- # Image fits in a single tile
157
  if H <= ps and W <= ps:
158
  pad = np.zeros((3, ps, ps), dtype=np.float32)
159
  pad[:, :H, :W] = image
@@ -173,7 +185,7 @@ class CHMPredictor:
173
  tile = np.zeros((3, ps, ps), dtype=np.float32)
174
  tile[:, :th, :tw] = image[:, y:y2, x:x2]
175
 
176
- pred = self._infer_tile(tile) # (ps, ps)
177
  output[y:y2, x:x2] += pred[:th, :tw]
178
  count [y:y2, x:x2] += 1.0
179
 
@@ -207,14 +219,13 @@ class CHMPredictor:
207
  arr = src.read([b + 1 for b in bands]).astype(np.float32)
208
  profile = src.profile.copy()
209
 
210
- # Percentile normalise to [0, 1] per band
211
  for b in range(arr.shape[0]):
212
  vmin = float(np.nanpercentile(arr[b], 1))
213
  vmax = float(np.nanpercentile(arr[b], 99))
214
  arr[b] = np.clip((arr[b] - vmin) / max(vmax - vmin, 1e-6), 0.0, 1.0)
215
 
216
  print(f"CHM inference model={self.model_version} input={arr.shape} {input_path}")
217
- chm = self.predict(arr) # (H, W)
218
  print(f"Output shape {chm.shape} range [{chm.min():.2f}, {chm.max():.2f}] m")
219
 
220
  out_profile = profile.copy()
 
5
 
6
  Both versions expose the same interface — only the model directory changes:
7
 
8
+ predictor = CHMPredictor("./chm-meta") # v1: SSL ViT-H + DPT
9
+ predictor = CHMPredictor("./chm-meta-v2") # v2: DINOv3 ViT-L + DPT
10
 
11
  chm = predictor.predict(image) # (3,H,W) float32 → (H,W) metres
12
  predictor.predict_tif("in.tif", "out.tif") # full GeoTIFF pipeline
13
 
14
+ When called from chm_pt.py the pre-built model is injected via model= so that
15
+ weights are not loaded twice.
16
+
17
  Requirements
18
  ------------
19
  Both versions: torch, numpy, rasterio, Pillow
 
41
 
42
  Parameters
43
  ----------
44
+ model_dir : local path to a downloaded WEO-SAS CHM model repo
45
+ device : torch device (auto-detected if None)
46
+ model : pre-built model; bypasses weights loading (used by chm_pt.py)
47
+ processor : pre-built HF processor for v2; bypasses processor loading
48
  """
49
 
50
+ def __init__(
51
+ self,
52
+ model_dir: str,
53
+ device: Optional[torch.device] = None,
54
+ model = None,
55
+ processor = None,
56
+ ):
57
  model_dir = Path(model_dir)
58
  with open(model_dir / "predictor_config.json") as f:
59
  cfg = json.load(f)
 
70
  "cuda" if torch.cuda.is_available() else "cpu"
71
  )
72
 
73
+ if model is not None:
74
+ # Pre-built model injected by chm_pt.py — skip weight loading
75
+ self.model = model.to(self.device)
76
+ self.processor = processor
77
+ elif self.model_version == "v1":
78
+ self._load_v1(model_dir, model_dir / cfg["weights_file"])
79
  elif self.model_version == "v2":
80
  self._load_v2(model_dir)
81
  else:
82
  raise ValueError(f"Unknown model_version '{self.model_version}' in predictor_config.json")
83
 
84
+ self.model.eval()
85
+
86
  # ------------------------------------------------------------------
87
+ # Model loading (only used when model= is not injected)
88
  # ------------------------------------------------------------------
89
 
90
  def _load_v1(self, model_dir: Path, weights_path: Path) -> None:
 
93
 
94
  self.model = SSLModule(ssl_path=str(weights_path), local_path=str(weights_path))
95
  self.processor = None
96
+ self.model.to(self.device)
97
 
98
  def _load_v2(self, model_dir: Path) -> None:
99
  try:
 
106
 
107
  self.model = CHMv2ForDepthEstimation.from_pretrained(str(model_dir))
108
  self.processor = CHMv2ImageProcessorFast.from_pretrained(str(model_dir))
109
+ self.model.to(self.device)
110
 
111
  # ------------------------------------------------------------------
112
  # Per-tile inference
 
124
  """tile: (3, patch_size, patch_size) float32 in [0, 1] → (patch_size, patch_size)"""
125
  from PIL import Image # noqa: PLC0415
126
 
127
+ arr_hwc = (tile * 255).clip(0, 255).astype(np.uint8).transpose(1, 2, 0)
128
+ pil_img = Image.fromarray(arr_hwc)
129
+ H, W = pil_img.height, pil_img.width
130
+ inputs = self.processor(images=pil_img, return_tensors="pt")
131
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
 
132
  with torch.no_grad():
133
  outputs = self.model(**inputs)
134
  depth = self.processor.post_process_depth_estimation(
 
163
  if image.ndim != 3 or image.shape[0] != 3:
164
  raise ValueError(f"Expected (3, H, W), got {image.shape}")
165
 
166
+ _, H, W = image.shape
167
+ ps, st = self.patch_size, self.stride
 
168
 
 
169
  if H <= ps and W <= ps:
170
  pad = np.zeros((3, ps, ps), dtype=np.float32)
171
  pad[:, :H, :W] = image
 
185
  tile = np.zeros((3, ps, ps), dtype=np.float32)
186
  tile[:, :th, :tw] = image[:, y:y2, x:x2]
187
 
188
+ pred = self._infer_tile(tile)
189
  output[y:y2, x:x2] += pred[:th, :tw]
190
  count [y:y2, x:x2] += 1.0
191
 
 
219
  arr = src.read([b + 1 for b in bands]).astype(np.float32)
220
  profile = src.profile.copy()
221
 
 
222
  for b in range(arr.shape[0]):
223
  vmin = float(np.nanpercentile(arr[b], 1))
224
  vmax = float(np.nanpercentile(arr[b], 99))
225
  arr[b] = np.clip((arr[b] - vmin) / max(vmax - vmin, 1e-6), 0.0, 1.0)
226
 
227
  print(f"CHM inference model={self.model_version} input={arr.shape} {input_path}")
228
+ chm = self.predict(arr)
229
  print(f"Output shape {chm.shape} range [{chm.min():.2f}, {chm.max():.2f}] m")
230
 
231
  out_profile = profile.copy()