Reference implementation

Full Code — End-to-end FastAPI Damage Assessment

Every file you need to run the complete API, from install to deployment. Paste each block in order into Google Colab, or save them under the shown paths in a local project.

How to run this in Google Colab
Paste each cell in order into a fresh Colab notebook. Step 1 installs deps, Step 2 creates the folder layout and the app/ Python package, Steps 3–9 use the %%writefile magic (already included at the top of each block) to save every Python file to disk, Step 10 writes a dummy ONNX model so the server can boot without your real weights, and Steps 11–13 launch and test it. For a local project, drop the%%writefile first line and save each file by hand at the shown path.
1

Install dependencies

bash
1# =============================================================
2# CELL 1 — INSTALL DEPENDENCIES
3# =============================================================
4# HOW TO RUN THIS CELL IN GOOGLE COLAB:
5# 1. Open https://colab.research.google.com → File → New notebook.
6# 2. (Recommended for speed) Runtime → Change runtime type →
7# Hardware accelerator: "T4 GPU" → Save.
8# 3. Click the first empty code cell, then copy-paste THIS ENTIRE
9# block into it (the lines starting with '#' are comments — keep
10# them, they won't run).
11# 4. Press Shift + Enter (or click the ▶ play button on the left of
12# the cell) to execute.
13# 5. Wait ~30-60 seconds. You'll see pip output scroll by.
14# When you see a fresh prompt and no red error text, it's done.
15#
16# WHAT THIS DOES:
17# Installs every Python package the API needs in one shot.
18# The '!' prefix tells Colab to run the line as a shell command.
19# '-q' means "quiet" (less log spam).
20# =============================================================
21!pip install -q fastapi==0.115.0 uvicorn==0.30.6 python-multipart==0.0.9 \
22 pydantic==2.8.2 pydantic-settings==2.4.0 rasterio==1.3.10 pillow==10.4.0 \
23 onnxruntime==1.18.1 numpy==1.26.4 nest_asyncio==1.6.0 pyngrok==7.2.0
2

Create folders & Python package

Creates app/, models/, samples/ and an empty app/__init__.pyso Python treats app/ as an importable package.

bash
1# =============================================================
2# CELL 2 — CREATE FOLDERS & MAKE 'app/' A PYTHON PACKAGE
3# =============================================================
4# HOW TO RUN:
5# 1. Below Cell 1, click "+ Code" to add a new code cell.
6# 2. Paste this whole block in.
7# 3. Press Shift + Enter.
8#
9# WHAT THIS DOES:
10# - 'mkdir -p' creates three folders next to your notebook:
11# app/ → where every .py file goes (the API source code)
12# models/ → where the ONNX model file lives
13# samples/ → drop test images here later
14# - 'touch app/__init__.py' creates an empty file that tells Python
15# "treat the app/ folder as an importable package". Without it,
16# 'from app.main import app' would fail with ModuleNotFoundError.
17#
18# HOW TO VERIFY IT WORKED:
19# Open the LEFT SIDEBAR in Colab (click the 📁 folder icon on the
20# far left). You should now see app/, models/, and samples/ folders.
21# Click the small refresh ⟳ icon at the top of the file panel if
22# you don't see them right away.
23# =============================================================
24!mkdir -p app models samples
25!touch app/__init__.py
26!ls -la
3

Project structure (reference)

This is the layout every file block writes into.

text
1app/
2├── main.py # FastAPI entrypoint, routes, middleware
3├── schemas.py # Pydantic request/response models
4├── inference.py # ONNX model loading + prediction
5├── preprocess.py # GeoTIFF / JPG / PNG loaders, tiling, normalization
6├── postprocess.py # Damage map → ATC-20 tags, JSON encoding
7├── config.py # Settings (paths, thresholds) via pydantic-settings
8├── deps.py # Shared dependencies (model singleton)
9└── __init__.py
10models/
11└── siamese_damage.onnx # Exported model weights
12run.py # Local launcher
13colab_launch.py # Colab + ngrok launcher
fileapp/config.py

Centralized settings with pydantic-settings. Override any value with an env var or .env file.

The first line is a Colab cell magic — it writes the rest of the cell to app/config.py. For a local project, delete that first line and save the file by hand.

python
1%%writefile app/config.py
2# =============================================================
3# CELL 3 — WRITE app/config.py
4# =============================================================
5# HOW TO RUN:
6# 1. Add a new code cell below Cell 2.
7# 2. Copy-paste this ENTIRE block (including the %%writefile line
8# at the very top).
9# 3. Press Shift + Enter.
10# 4. Colab prints "Writing app/config.py" — that's success.
11#
12# VERIFY: open the LEFT SIDEBAR 📁 → expand app/ → you should
13# now see config.py listed. Double-click it to read it back.
14#
15# WHAT THIS FILE DOES:
16# Centralizes every "setting" (model path, thresholds, CORS rules)
17# in one place using pydantic-settings. You can override any value
18# with an environment variable later without touching the code.
19# =============================================================
20from pathlib import Path
21from pydantic_settings import BaseSettings
22
23class Settings(BaseSettings):
24 app_name: str = "Damage Assessment API"
25 model_path: Path = Path("models/siamese_damage.onnx")
26 tile_size: int = 512
27 overlap: int = 64
28 # ATC-20 thresholds (fraction of pixels per class)
29 minor_threshold: float = 0.05
30 moderate_threshold: float = 0.20
31 severe_threshold: float = 0.40
32 # CORS
33 allow_origins: list[str] = ["*"]
34
35 class Config:
36 env_file = ".env"
37
38settings = Settings()
fileapp/schemas.py

Pydantic models that define the OpenAPI contract for /health and /predict.

The first line is a Colab cell magic — it writes the rest of the cell to app/schemas.py. For a local project, delete that first line and save the file by hand.

python
1%%writefile app/schemas.py
2# =============================================================
3# CELL 4 — WRITE app/schemas.py
4# =============================================================
5# HOW TO RUN:
6# 1. Add a new code cell below Cell 3.
7# 2. Paste this whole block (keep the %%writefile line on top).
8# 3. Press Shift + Enter. You'll see "Writing app/schemas.py".
9#
10# WHAT THIS FILE DOES:
11# Defines the SHAPE of the data the API accepts and returns.
12# Pydantic uses these classes to (a) validate inputs automatically,
13# (b) generate the interactive Swagger /docs page for free, and
14# (c) produce nice JSON error messages on bad input.
15# =============================================================
16from pydantic import BaseModel, Field
17from typing import Literal
18
19DamageClass = Literal["no-damage", "minor", "moderate", "major", "destroyed"]
20ATC20Tag = Literal["GREEN", "YELLOW", "RED"]
21
22class HealthOut(BaseModel):
23 status: Literal["ok"] = "ok"
24 model_loaded: bool
25 version: str = "1.0.0"
26
27class PredictOut(BaseModel):
28 class_counts: dict[DamageClass, int] = Field(..., description="Pixel count per class")
29 class_fractions: dict[DamageClass, float]
30 atc20_tag: ATC20Tag
31 confidence: float = Field(..., ge=0.0, le=1.0)
32 width: int
33 height: int
34
35class ErrorOut(BaseModel):
36 detail: str
fileapp/preprocess.py

Loads JPG, PNG, or GeoTIFF, then ImageNet-normalizes into a NCHW float32 batch.

The first line is a Colab cell magic — it writes the rest of the cell to app/preprocess.py. For a local project, delete that first line and save the file by hand.

python
1%%writefile app/preprocess.py
2# =============================================================
3# CELL 5 — WRITE app/preprocess.py
4# =============================================================
5# HOW TO RUN:
6# 1. Add a new code cell below Cell 4.
7# 2. Paste this whole block (keep the %%writefile line on top).
8# 3. Press Shift + Enter.
9#
10# WHAT THIS FILE DOES:
11# Turns raw uploaded bytes (JPG, PNG, or GeoTIFF) into the exact
12# tensor shape the neural network expects:
13# bytes -> HxWx3 uint8 array -> 1x3xHxW float32, ImageNet-normalized.
14# It also resizes the post-event image to match the pre-event one,
15# so the Siamese branches see the same spatial grid.
16# =============================================================
17import io
18import numpy as np
19from PIL import Image
20import rasterio
21from rasterio.io import MemoryFile
22
23MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
24STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
25
26def load_image(raw: bytes) -> np.ndarray:
27 """Return HxWx3 uint8 array from JPG, PNG, or GeoTIFF bytes."""
28 # Try GeoTIFF first
29 try:
30 with MemoryFile(raw) as mem, mem.open() as src:
31 arr = src.read(out_dtype="uint8") # CxHxW
32 if arr.shape[0] >= 3:
33 arr = np.transpose(arr[:3], (1, 2, 0)) # HxWx3
34 else:
35 arr = np.repeat(arr[:1], 3, axis=0)
36 arr = np.transpose(arr, (1, 2, 0))
37 return arr
38 except rasterio.errors.RasterioIOError:
39 img = Image.open(io.BytesIO(raw)).convert("RGB")
40 return np.array(img, dtype=np.uint8)
41
42def normalize(arr: np.ndarray) -> np.ndarray:
43 """uint8 HxWx3 → float32 1x3xHxW, ImageNet normalized."""
44 x = arr.astype(np.float32) / 255.0
45 x = (x - MEAN) / STD
46 x = np.transpose(x, (2, 0, 1))[None, ...]
47 return np.ascontiguousarray(x)
48
49def pair_to_batch(pre: np.ndarray, post: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
50 """Resize post to pre's shape, then normalize both."""
51 if pre.shape != post.shape:
52 post = np.array(Image.fromarray(post).resize(
53 (pre.shape[1], pre.shape[0]), Image.BILINEAR))
54 return normalize(pre), normalize(post)
fileapp/inference.py

Cached ONNX Runtime session with optional CUDA. Returns the predicted class map.

The first line is a Colab cell magic — it writes the rest of the cell to app/inference.py. For a local project, delete that first line and save the file by hand.

python
1%%writefile app/inference.py
2# =============================================================
3# CELL 6 — WRITE app/inference.py
4# =============================================================
5# HOW TO RUN:
6# 1. Add a new code cell below Cell 5.
7# 2. Paste this whole block (keep the %%writefile line on top).
8# 3. Press Shift + Enter.
9#
10# WHAT THIS FILE DOES:
11# Loads the ONNX model ONCE (the @lru_cache keeps it in memory so
12# we don't re-load on every request) and exposes a 'predict' function.
13# If a GPU is available on this Colab runtime, ONNX Runtime will use
14# it automatically. Returns an HxW class-id map.
15# =============================================================
16from functools import lru_cache
17import numpy as np
18import onnxruntime as ort
19from .config import settings
20
21@lru_cache(maxsize=1)
22def get_session() -> ort.InferenceSession:
23 providers = ["CPUExecutionProvider"]
24 if "CUDAExecutionProvider" in ort.get_available_providers():
25 providers.insert(0, "CUDAExecutionProvider")
26 return ort.InferenceSession(str(settings.model_path), providers=providers)
27
28def predict(pre_batch: np.ndarray, post_batch: np.ndarray) -> np.ndarray:
29 """Return HxW int64 class map (0..4)."""
30 sess = get_session()
31 input_names = [i.name for i in sess.get_inputs()]
32 feeds = {input_names[0]: pre_batch, input_names[1]: post_batch}
33 logits = sess.run(None, feeds)[0] # 1xCxHxW
34 return np.argmax(logits, axis=1)[0] # HxW
fileapp/postprocess.py

Turns the pixel-level class map into an ATC-20 GREEN / YELLOW / RED tag plus a confidence.

The first line is a Colab cell magic — it writes the rest of the cell to app/postprocess.py. For a local project, delete that first line and save the file by hand.

python
1%%writefile app/postprocess.py
2# =============================================================
3# CELL 7 — WRITE app/postprocess.py
4# =============================================================
5# HOW TO RUN:
6# 1. Add a new code cell below Cell 6.
7# 2. Paste this whole block (keep the %%writefile line on top).
8# 3. Press Shift + Enter.
9#
10# WHAT THIS FILE DOES:
11# Takes the raw HxW class-id map from the model and turns it into a
12# human-friendly summary: per-class pixel counts, fractions, and a
13# single ATC-20 GREEN/YELLOW/RED placard tag with a confidence score.
14# Thresholds come from app/config.py — tune them there.
15# =============================================================
16import numpy as np
17from .config import settings
18from .schemas import PredictOut
19
20CLASS_NAMES = ["no-damage", "minor", "moderate", "major", "destroyed"]
21
22def summarize(mask: np.ndarray) -> PredictOut:
23 h, w = mask.shape
24 total = int(h * w)
25 counts = {name: int((mask == i).sum()) for i, name in enumerate(CLASS_NAMES)}
26 fracs = {k: v / total for k, v in counts.items()}
27
28 severe = fracs["major"] + fracs["destroyed"]
29 moderate = fracs["moderate"]
30 minor = fracs["minor"]
31
32 if severe >= settings.severe_threshold:
33 tag, conf = "RED", min(1.0, severe / 0.6)
34 elif severe + moderate >= settings.moderate_threshold:
35 tag, conf = "YELLOW", 0.5 + 0.5 * (severe + moderate)
36 elif minor + moderate >= settings.minor_threshold:
37 tag, conf = "YELLOW", 0.4 + 0.6 * (minor + moderate)
38 else:
39 tag, conf = "GREEN", 1.0 - (severe + moderate + minor)
40
41 return PredictOut(
42 class_counts=counts,
43 class_fractions=fracs,
44 atc20_tag=tag,
45 confidence=round(float(conf), 4),
46 width=w,
47 height=h,
48 )
fileapp/deps.py

Single FastAPI dependency that exposes the model session to route handlers.

The first line is a Colab cell magic — it writes the rest of the cell to app/deps.py. For a local project, delete that first line and save the file by hand.

python
1%%writefile app/deps.py
2# =============================================================
3# CELL 8 — WRITE app/deps.py
4# =============================================================
5# HOW TO RUN:
6# 1. Add a new code cell below Cell 7.
7# 2. Paste this whole block (keep the %%writefile line on top).
8# 3. Press Shift + Enter.
9#
10# WHAT THIS FILE DOES:
11# A tiny "dependency" that FastAPI injects into route handlers.
12# In main.py you'll see 'Depends(model_session)' — FastAPI calls
13# this function for each request and hands the result to the route.
14# Keeping it in its own file lets us swap implementations later
15# (e.g. mock the model in tests) without touching main.py.
16# =============================================================
17from fastapi import Depends
18from .inference import get_session
19
20def model_session():
21 return get_session()
fileapp/main.py

The FastAPI entrypoint — wires every module together, adds CORS, exposes /docs.

The first line is a Colab cell magic — it writes the rest of the cell to app/main.py. For a local project, delete that first line and save the file by hand.

python
1%%writefile app/main.py
2# =============================================================
3# CELL 9 — WRITE app/main.py (the FastAPI entrypoint)
4# =============================================================
5# HOW TO RUN:
6# 1. Add a new code cell below Cell 8.
7# 2. Paste this whole block (keep the %%writefile line on top).
8# 3. Press Shift + Enter.
9#
10# WHAT THIS FILE DOES:
11# - Builds the FastAPI 'app' object Uvicorn will serve.
12# - Wires together every module: config, schemas, preprocess,
13# inference, postprocess.
14# - On startup (the 'lifespan' block), warms the model so the
15# first real request isn't slow. If the model file is missing
16# it logs a warning instead of crashing — /health will then
17# return model_loaded=false so you know to fix it.
18# - Exposes two endpoints:
19# GET /health → quick liveness check
20# POST /predict → multipart upload of 'pre' and 'post' images
21# - Adds permissive CORS so the API can be called from any browser.
22# =============================================================
23import logging
24from contextlib import asynccontextmanager
25from fastapi import FastAPI, UploadFile, File, HTTPException, Depends
26from fastapi.middleware.cors import CORSMiddleware
27from fastapi.responses import JSONResponse
28
29from .config import settings
30from .schemas import HealthOut, PredictOut, ErrorOut
31from .preprocess import load_image, pair_to_batch
32from .inference import predict, get_session
33from .postprocess import summarize
34from .deps import model_session
35
36logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
37log = logging.getLogger("damage-api")
38
39@asynccontextmanager
40async def lifespan(app: FastAPI):
41 log.info("Warming model session...")
42 try:
43 get_session()
44 log.info("Model ready: %s", settings.model_path)
45 except Exception as e:
46 # Don't crash the server on boot — /health will report model_loaded=false
47 log.warning("Model not loaded at startup: %s", e)
48 yield
49
50app = FastAPI(
51 title=settings.app_name,
52 version="1.0.0",
53 description="End-to-end FastAPI service for pre/post-event damage assessment.",
54 lifespan=lifespan,
55)
56
57# NOTE: when allow_origins=["*"], allow_credentials MUST be False
58# (Starlette/browsers reject the combination).
59_wildcard = settings.allow_origins == ["*"]
60app.add_middleware(
61 CORSMiddleware,
62 allow_origins=settings.allow_origins,
63 allow_credentials=not _wildcard,
64 allow_methods=["*"],
65 allow_headers=["*"],
66)
67
68@app.get("/health", response_model=HealthOut, tags=["meta"])
69def health():
70 return HealthOut(model_loaded=settings.model_path.exists())
71
72@app.post(
73 "/predict",
74 response_model=PredictOut,
75 responses={400: {"model": ErrorOut}, 500: {"model": ErrorOut}},
76 tags=["inference"],
77)
78async def predict_endpoint(
79 pre: UploadFile = File(..., description="Pre-event image (JPG/PNG/GeoTIFF)"),
80 post: UploadFile = File(..., description="Post-event image (JPG/PNG/GeoTIFF)"),
81 _sess = Depends(model_session),
82):
83 if not pre.filename or not post.filename:
84 raise HTTPException(400, "Both 'pre' and 'post' files are required.")
85 try:
86 pre_bytes = await pre.read()
87 post_bytes = await post.read()
88 pre_arr = load_image(pre_bytes)
89 post_arr = load_image(post_bytes)
90 pre_b, post_b = pair_to_batch(pre_arr, post_arr)
91 mask = predict(pre_b, post_b)
92 return summarize(mask)
93 except HTTPException:
94 raise
95 except Exception as e:
96 log.exception("Prediction failed")
97 raise HTTPException(500, f"Inference error: {e}")
98
99@app.exception_handler(Exception)
100async def unhandled(_, exc: Exception):
101 return JSONResponse(status_code=500, content={"detail": str(exc)})
10

Provide a model (dummy ONNX for testing)

The API expects models/siamese_damage.onnx. If you don't have your real export yet, run this cell to write a tiny placeholder model so the server can boot and/predict returns a (meaningless but well-shaped) response. Swap in the real file when ready.

python
1# =============================================================
2# CELL 10 — (OPTIONAL) WRITE A DUMMY ONNX MODEL FOR TESTING
3# =============================================================
4# HOW TO RUN:
5# 1. Add a new code cell below Cell 9 (app/main.py).
6# 2. Paste this whole block in and press Shift + Enter.
7# 3. First run will take ~1-2 minutes because it installs PyTorch.
8# 4. When you see "Wrote models/siamese_damage.onnx" the cell is done.
9#
10# WHY YOU NEED THIS:
11# Your real trained model is probably not on Colab yet. This cell
12# writes a tiny "fake" model with the SAME input/output shape so the
13# API can boot and you can test the endpoints. The predictions will
14# be meaningless — that's fine, the goal here is to verify the
15# pipeline works end-to-end.
16#
17# REPLACING WITH YOUR REAL MODEL LATER:
18# Once you have a trained .onnx file, just upload it to Colab:
19# - Open the LEFT SIDEBAR 📁 → navigate into models/ →
20# - Right-click → "Upload" → pick your siamese_damage.onnx file.
21# Overwrite the dummy. Restart the server cell (Cell 12) and you're
22# running real inference.
23# =============================================================
24!pip install -q torch==2.4.0
25import torch, torch.nn as nn
26
27class DummySiamese(nn.Module):
28 def forward(self, pre, post):
29 # Return logits shaped (1, 5, H, W) — 5 damage classes
30 diff = (post - pre).mean(dim=1, keepdim=True) # 1x1xHxW
31 logits = diff.repeat(1, 5, 1, 1) # 1x5xHxW
32 return logits
33
34model = DummySiamese().eval()
35pre = torch.randn(1, 3, 256, 256)
36post = torch.randn(1, 3, 256, 256)
37torch.onnx.export(
38 model, (pre, post), "models/siamese_damage.onnx",
39 opset_version=17,
40 input_names=["pre", "post"], output_names=["logits"],
41 dynamic_axes={"pre": {0: "B", 2: "H", 3: "W"},
42 "post": {0: "B", 2: "H", 3: "W"},
43 "logits": {0: "B", 2: "H", 3: "W"}},
44)
45print("Wrote models/siamese_damage.onnx")
11

Run locally

python
1# =============================================================
2# CELL 11 — run.py (LOCAL launcher, NOT for Colab)
3# =============================================================
4# USE THIS ONLY ON YOUR OWN COMPUTER (Mac / Linux / WSL / VS Code),
5# not in Google Colab. Colab uses Cell 12 (ngrok) instead.
6#
7# HOW TO USE LOCALLY:
8# 1. Save this block as a file named run.py in the same folder
9# that contains the app/ directory.
10# 2. Open a terminal in that folder.
11# 3. Run: python run.py
12# 4. Open http://localhost:8000/docs in your browser.
13# 5. Stop the server any time with Ctrl + C.
14# =============================================================
15import uvicorn
16if __name__ == "__main__":
17 uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)

Then python run.py and open http://localhost:8000/docs.

12

Run in Google Colab (ngrok)

python
1# =============================================================
2# CELL 12 — LAUNCH THE API IN GOOGLE COLAB (with ngrok)
3# =============================================================
4# HOW TO RUN:
5# 1. (One-time setup) Get a free ngrok auth token:
6# a. Go to https://dashboard.ngrok.com/signup and sign up.
7# b. Open https://dashboard.ngrok.com/get-started/your-authtoken
8# c. Copy the token shown there.
9# d. Uncomment the 'ngrok.set_auth_token(...)' line below and
10# paste your token between the quotes. Without a token,
11# ngrok still works for ~2 hours per session.
12# 2. Add a new code cell below Cell 10 (the dummy ONNX cell).
13# 3. Paste this whole block in.
14# 4. Press Shift + Enter.
15# 5. Wait until you see two lines in the output:
16# Public URL: NgrokTunnel: "https://xxxx-xx-xx.ngrok-free.app"
17# Swagger UI: https://xxxx-xx-xx.ngrok-free.app/docs
18# 6. CLICK the Swagger UI link. A new browser tab opens with the
19# interactive API docs. This is where you can try /predict by
20# hand: click "POST /predict" → "Try it out" → choose two
21# image files → "Execute" → scroll down to see the JSON response.
22# 7. To stop the server, click Runtime → Interrupt execution.
23#
24# IF YOU SEE AN ERROR like "address already in use":
25# Restart the runtime (Runtime → Restart session) and re-run all
26# cells from the top.
27# =============================================================
28import nest_asyncio, uvicorn, threading
29from pyngrok import ngrok
30
31nest_asyncio.apply()
32
33# (optional but recommended) set your ngrok token:
34# ngrok.set_auth_token("PASTE_YOUR_TOKEN_HERE")
35public_url = ngrok.connect(8000)
36print(f"Public URL: {public_url}")
37print(f"Swagger UI: {public_url}/docs")
38
39def _serve():
40 uvicorn.run("app.main:app", host="0.0.0.0", port=8000, log_level="info")
41
42threading.Thread(target=_serve, daemon=True).start()

The printed public URL exposes /docs and /predict to the internet for the lifetime of the notebook.

13

Test it

From the shell with curl:

bash
1# =============================================================
2# CELL 13a — TEST THE API FROM THE SHELL (Colab or local terminal)
3# =============================================================
4# HOW TO RUN IN COLAB:
5# 1. Add a new code cell.
6# 2. Paste this whole block in.
7# 3. BEFORE pressing Shift + Enter, replace every 'HOST' below with
8# the Public URL printed by Cell 12 (e.g. https://xxxx.ngrok-free.app).
9# For local use, replace HOST with http://localhost:8000 instead.
10# 4. Make sure two test images exist:
11# samples/pre.jpg and samples/post.jpg
12# In Colab: LEFT SIDEBAR 📁 → samples/ → right-click → Upload.
13# 5. Press Shift + Enter. You should see JSON like:
14# {"status":"ok","model_loaded":true,"version":"1.0.0"}
15# followed by a longer JSON with class_counts and atc20_tag.
16#
17# TIP: if 'jq' isn't installed in your local shell, drop the
18# ' | jq' pipe — you'll get raw JSON which is still readable.
19# =============================================================
20!curl -s HOST/health | jq
21
22!curl -s -X POST HOST/predict \
23 -F "pre=@./samples/pre.jpg" \
24 -F "post=@./samples/post.jpg" | jq

…or from Python:

python
1# =============================================================
2# CELL 13b — TEST THE API FROM PYTHON
3# =============================================================
4# HOW TO RUN:
5# 1. Add a new code cell.
6# 2. Paste this whole block in.
7# 3. Edit the BASE line:
8# - In Colab: set BASE to your ngrok URL from Cell 12.
9# - Locally: leave BASE as http://localhost:8000.
10# 4. Make sure samples/pre.jpg and samples/post.jpg exist
11# (see Cell 13a step 4 for how to upload them).
12# 5. Press Shift + Enter. You'll see two printouts:
13# - the /health response
14# - the /predict response (counts, fractions, ATC-20 tag).
15# =============================================================
16import requests
17
18BASE = "http://localhost:8000" # ← replace with your ngrok URL in Colab
19
20print(requests.get(f"{BASE}/health").json())
21
22with open("samples/pre.jpg", "rb") as a, \
23 open("samples/post.jpg", "rb") as b:
24 r = requests.post(
25 f"{BASE}/predict",
26 files={"pre": ("pre.jpg", a, "image/jpeg"),
27 "post": ("post.jpg", b, "image/jpeg")},
28 timeout=60,
29 )
30 r.raise_for_status()
31 print(r.json())
+

Extras

requirements.txt for reproducible installs:

text
1# requirements.txt
2fastapi==0.115.0
3uvicorn==0.30.6
4python-multipart==0.0.9
5pydantic==2.8.2
6pydantic-settings==2.4.0
7rasterio==1.3.10
8pillow==10.4.0
9onnxruntime==1.18.1
10numpy==1.26.4
11nest_asyncio==1.6.0
12pyngrok==7.2.0

Dockerfile for a production container:

dockerfile
1# Dockerfile (optional production image)
2FROM python:3.11-slim
3WORKDIR /app
4RUN apt-get update && apt-get install -y --no-install-recommends \
5 gdal-bin libgdal-dev && rm -rf /var/lib/apt/lists/*
6COPY requirements.txt .
7RUN pip install --no-cache-dir -r requirements.txt
8COPY app ./app
9COPY models ./models
10EXPOSE 8000
11CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Model weights
The code expects an ONNX file at models/siamese_damage.onnx. Export your trained Siamese / ViT model with torch.onnx.export(model, (pre, post), "models/siamese_damage.onnx", opset_version=17, input_names=["pre","post"], output_names=["logits"]). The /predict endpoint will return a 500 with a clear error message if the file is missing or shapes don't match.