back to site

RUN IT YOURSELF

The model is portable.

CS-Mamba ships as a single ONNX file. The browser demo is one way to run it — here's the exact input/output contract and copy-paste code so you can run the same weights on your own machine, in a script, or behind your own API. No browser, no account, no GPU required.

By design this is a portable artifact, not a locked cloud API — we hand you the model to own and run anywhere, with nothing to meter, bill, or shut down.

The contract

Modelcs_mamba_fp16.onnx · fp16 weights, ~82 MB
Inputfloat32 [1, 6, 224, 224]
Channelscat([pre-2, pre-1, post]) × [VV, VH] — six bands, oldest first
Preprocessclamp to [0, C] · NaN→C · standardize per channel (x − μ) / σ
Outputfloat32 [1, 3, 224, 224] logits → argmax(axis=1)
Classes0 = no water · 1 = permanent water · 2 = flood

Normalization is the load-bearing choice (the "clamp story"): in-distribution KuroSiwo tiles use clamp 0.15; the bright Banda Aceh OOD scene needs the adapted clamp 0.30 to recover the flood. Both preset values are in the Python example below.

Python · 30-second quickstart

Runs the model on a tile we already ship — no data prep.

import numpy as np
import onnxruntime as ort
import urllib.request

# 1. Grab the model (fp16 weights, ~82 MB) and one shipped sample tile.
urllib.request.urlretrieve("https://pub-c834930e7f05452caf062e40c59ef9e8.r2.dev/cs_mamba_fp16.onnx", "cs_mamba.onnx")
urllib.request.urlretrieve("https://remote-sensing-platform.pages.dev/cases/ks-germany-497/input.bin", "tile.bin")

# 2. input.bin is already normalized: float32 [6, 224, 224].
x = np.fromfile("tile.bin", dtype="<f4").reshape(1, 6, 224, 224)

sess = ort.InferenceSession("cs_mamba.onnx", providers=["CPUExecutionProvider"])
logits = sess.run(None, {sess.get_inputs()[0].name: x})[0]   # [1, 3, 224, 224]
mask = logits.argmax(1)[0]                                     # 0=land 1=permanent 2=flood

for k, name in enumerate(["No water", "Permanent water", "Flood"]):
    print(f"{name:16s} {(mask == k).mean() * 100:5.1f}%")

Python · your own 6 GeoTIFF bands

Bring raw linear σ0 bands; this applies the training preprocessing.

import numpy as np, rasterio, onnxruntime as ort

# Normalization presets (clamp ceiling + per-pol mean/std).
PRESETS = {
    # In-distribution (KuroSiwo). Use for KuroSiwo-domain tiles.
    "kurosiwo":      dict(clamp=0.15, vv=(0.0953, 0.0427), vh=(0.0264, 0.0215)),
    # Banda Aceh out-of-distribution, recommended config (recovers the flood).
    "banda_clamp03": dict(clamp=0.30, vv=(0.0538, 0.0489), vh=(0.2078, 0.0916)),
}

def preprocess(arr, pol, p):                 # arr: raw linear sigma0 (224,224)
    mean, std = p["vv"] if pol == "vv" else p["vh"]
    arr = np.nan_to_num(arr, nan=p["clamp"])
    arr = np.clip(arr, 0.0, p["clamp"])
    return (arr - mean) / std

def band(path):                              # one single-band GeoTIFF -> (224,224)
    with rasterio.open(path) as ds:
        return ds.read(1, out_shape=(224, 224)).astype("float32")

p = PRESETS["banda_clamp03"]
# Channel order: cat([pre2, pre1, post]) x [VV, VH]
files = [("pre2_VV","vv"),("pre2_VH","vh"),("pre1_VV","vv"),
         ("pre1_VH","vh"),("post_VV","vv"),("post_VH","vh")]
x = np.stack([preprocess(band(f"{n}.tif"), pol, p) for n, pol in files])[None]  # [1,6,224,224]

sess = ort.InferenceSession("cs_mamba.onnx", providers=["CPUExecutionProvider"])
mask = sess.run(None, {sess.get_inputs()[0].name: x.astype("<f4")})[0].argmax(1)[0]

Node / JavaScript

import * as ort from "onnxruntime-node";   // or onnxruntime-web
import { readFileSync } from "fs";

const data = new Float32Array(readFileSync("tile.bin").buffer);   // [6*224*224]
const sess = await ort.InferenceSession.create("cs_mamba.onnx");
const out = await sess.run({
  [sess.inputNames[0]]: new ort.Tensor("float32", data, [1, 6, 224, 224]),
});
const logits = out[sess.outputNames[0]].data;                     // length 3*224*224
const HW = 224 * 224, hist = [0, 0, 0];
for (let p = 0; p < HW; p++) {
  let a = 0, v = logits[p];
  if (logits[HW + p] > v) { v = logits[HW + p]; a = 1; }
  if (logits[2 * HW + p] > v) a = 2;
  hist[a]++;
}
console.log("flood %", (hist[2] / HW * 100).toFixed(1));

Self-host an HTTP API

The repo ships a FastAPI wrapper so you can serve the model to your own tools.

# Self-host the same model as an HTTP service
# https://github.com/yyy735934-prog/rs-observatory (wsl-inference/server.py)
git clone https://github.com/yyy735934-prog/rs-observatory && cd rs-observatory/wsl-inference
pip install fastapi uvicorn onnxruntime rasterio numpy
uvicorn server:app --host 0.0.0.0 --port 8000

# POST a 224x224 tile, get back the 3-class mask + class histogram:
#   GET  /health
#   POST /predict/tile     (config = original | clamp015 | clamp03 | clamp05)

Weights are the paper's CS-Mamba (UNetRSMamba_FloodFocus), trained on KuroSiwo. Full source — site, inference tools, and training code — is open at github.com/yyy735934-prog/rs-observatory. Please cite the project and KuroSiwo if you build on it. The exported ONNX is deterministic — same input, identical output, byte for byte.