JulioContrerasH commited on
Commit
9d86ecf
·
verified ·
1 Parent(s): 5543b53

Add mlstac loader, weights, examples and model card

Browse files
Files changed (1) hide show
  1. load.py +119 -1
load.py CHANGED
@@ -286,4 +286,122 @@ def predict_large(image, model, **kwargs):
286
  apply_nodata_mask=kwargs.get("apply_nodata_mask", True),
287
  return_probs=kwargs.get("return_probs", False),
288
  verbose=kwargs.get("verbose", False),
289
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  apply_nodata_mask=kwargs.get("apply_nodata_mask", True),
287
  return_probs=kwargs.get("return_probs", False),
288
  verbose=kwargs.get("verbose", False),
289
+ )
290
+
291
+
292
+ # ============================================================================
293
+ # CHRIS/PROBA-1 preprocessing (the 6 acquisition modes)
294
+ # ============================================================================
295
+ # For each CHRIS mode, which raw band indices (1-based) average into each of
296
+ # the R, G, B, NIR groups. Taken from the inference pipeline.
297
+ BAND_SELECTION = {
298
+ 1: {"B4": [23, 24, 25],
299
+ "B3": [13, 14, 15],
300
+ "B2": [4, 5, 6, 7, 8, 9, 10],
301
+ "B8": [43, 44, 45, 46, 47, 48, 49, 50, 51]},
302
+ 2: {"B4": [10, 11, 12], "B3": [6, 7], "B2": [3, 4], "B8": [17]},
303
+ 3: {"B4": [7, 8], "B3": [4, 5], "B2": [2], "B8": [15]},
304
+ 4: {"B4": [4], "B3": [2], "B2": [1], "B8": [18]},
305
+ 5: {"B4": [7, 8], "B3": [4, 5], "B2": [2], "B8": [23, 24, 25, 26]},
306
+ 6: {"B2": [1], "B3": [2], "B4": [3], "B8": [4]},
307
+ }
308
+ BAND_KEYS_RGBN = ["B4", "B3", "B2", "B8"] # R, G, B, NIR
309
+
310
+ DN_SCALE = 100_000.0
311
+ TOA_SCALE = 10_000.0
312
+ CAP = 5.0 # clip ceiling used during training; keep it identical at inference
313
+
314
+
315
+ def _avg_bands(cube, band_list):
316
+ """Average the given 1-based raw bands into a single (H, W) layer."""
317
+ idx = [b - 1 for b in band_list]
318
+ return cube[idx[0]] if len(idx) == 1 else cube[idx].mean(axis=0)
319
+
320
+
321
+ def _infer_source_from_name(tif_path):
322
+ """Guess 'dn' or 'toa' from the file name. Returns None if unclear."""
323
+ name = Path(tif_path).name.lower()
324
+ if "toa" in name:
325
+ return "toa"
326
+ if "dn" in name:
327
+ return "dn"
328
+ return None
329
+
330
+
331
+ def build_rgbn(cube, mode_n, source):
332
+ """Build the (4, H, W) RGBN stack and the nodata mask from a raw cube.
333
+
334
+ Args:
335
+ cube: (bands, H, W) float array read from the CHRIS GeoTIFF.
336
+ mode_n: CHRIS acquisition mode, 1 to 6.
337
+ source: 'dn' or 'toa', selects the radiometric scale.
338
+
339
+ Returns:
340
+ (stack, nodata) where stack is (4, H, W) float32 and nodata is (H, W) bool.
341
+ """
342
+ sel = BAND_SELECTION.get(int(mode_n))
343
+ if sel is None:
344
+ raise ValueError(f"Unsupported CHRIS mode {mode_n}; expected 1-6.")
345
+
346
+ needed_max = max(max(sel[k]) for k in BAND_KEYS_RGBN)
347
+ if cube.shape[0] < needed_max:
348
+ raise ValueError(
349
+ f"Cube has {cube.shape[0]} bands but mode {mode_n} needs {needed_max}."
350
+ )
351
+
352
+ # nodata = pixel is 0 in every RGBN group
353
+ H, W = cube.shape[1:]
354
+ nodata = np.ones((H, W), dtype=bool)
355
+ for k in BAND_KEYS_RGBN:
356
+ nodata &= (_avg_bands(cube, sel[k]) == 0)
357
+
358
+ scale = DN_SCALE if source == "dn" else TOA_SCALE
359
+ layers = [_avg_bands(cube, sel[k]) for k in BAND_KEYS_RGBN]
360
+ stack = np.stack(layers, axis=0).astype(np.float32) / scale
361
+ stack = np.clip(stack, 0.0, CAP)
362
+ stack[:, nodata] = 0.0
363
+ return stack, nodata
364
+
365
+
366
+ def predict_chris(tif_path, model, mode_n, source=None, **kwargs):
367
+ """Segment a raw CHRIS/PROBA-1 GeoTIFF end to end.
368
+
369
+ Reads the cube, builds the RGBN stack for the given mode, runs the
370
+ ensemble, and restores nodata as 99.
371
+
372
+ Args:
373
+ tif_path: path to the CHRIS GeoTIFF.
374
+ model: object returned by compiled_model().
375
+ mode_n: CHRIS acquisition mode, 1 to 6.
376
+ source: 'dn' or 'toa'. If None, it is guessed from the file name.
377
+ return_probs: if True, return (num_classes, H, W) probabilities.
378
+
379
+ Returns:
380
+ (H, W) uint8 label map (nodata = 99), or probabilities if requested.
381
+ """
382
+ import rasterio as rio
383
+
384
+ if source is None:
385
+ source = _infer_source_from_name(tif_path)
386
+ if source is None:
387
+ raise ValueError(
388
+ "Could not tell DN from TOA by the file name; "
389
+ "pass source='dn' or source='toa'."
390
+ )
391
+
392
+ with rio.open(tif_path) as src:
393
+ cube = src.read().astype(np.float32)
394
+
395
+ stack, nodata = build_rgbn(cube, mode_n, source)
396
+
397
+ return_probs = kwargs.get("return_probs", False)
398
+ pred = model.predict_array(
399
+ stack,
400
+ apply_nodata_mask=False, # we apply the CHRIS nodata mask below
401
+ return_probs=return_probs,
402
+ verbose=kwargs.get("verbose", False),
403
+ )
404
+ if not return_probs:
405
+ pred = np.asarray(pred).astype(np.uint8)
406
+ pred[nodata] = 99
407
+ return pred