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.
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.Install dependencies
bash1# =============================================================2# CELL 1 — INSTALL DEPENDENCIES3# =============================================================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 ENTIRE9# block into it (the lines starting with '#' are comments — keep10# them, they won't run).11# 4. Press Shift + Enter (or click the ▶ play button on the left of12# 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
Create folders & Python package
Creates app/, models/, samples/ and an empty app/__init__.pyso Python treats app/ as an importable package.
bash1# =============================================================2# CELL 2 — CREATE FOLDERS & MAKE 'app/' A PYTHON PACKAGE3# =============================================================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 lives13# samples/ → drop test images here later14# - 'touch app/__init__.py' creates an empty file that tells Python15# "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 the20# far left). You should now see app/, models/, and samples/ folders.21# Click the small refresh ⟳ icon at the top of the file panel if22# you don't see them right away.23# =============================================================24!mkdir -p app models samples25!touch app/__init__.py26!ls -la
Project structure (reference)
This is the layout every file block writes into.
text1app/2├── main.py # FastAPI entrypoint, routes, middleware3├── schemas.py # Pydantic request/response models4├── inference.py # ONNX model loading + prediction5├── preprocess.py # GeoTIFF / JPG / PNG loaders, tiling, normalization6├── postprocess.py # Damage map → ATC-20 tags, JSON encoding7├── config.py # Settings (paths, thresholds) via pydantic-settings8├── deps.py # Shared dependencies (model singleton)9└── __init__.py10models/11└── siamese_damage.onnx # Exported model weights12run.py # Local launcher13colab_launch.py # Colab + ngrok launcher
app/config.pyCentralized 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.
python1%%writefile app/config.py2# =============================================================3# CELL 3 — WRITE app/config.py4# =============================================================5# HOW TO RUN:6# 1. Add a new code cell below Cell 2.7# 2. Copy-paste this ENTIRE block (including the %%writefile line8# 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 should13# 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 value18# with an environment variable later without touching the code.19# =============================================================20from pathlib import Path21from pydantic_settings import BaseSettings2223class Settings(BaseSettings):24 app_name: str = "Damage Assessment API"25 model_path: Path = Path("models/siamese_damage.onnx")26 tile_size: int = 51227 overlap: int = 6428 # ATC-20 thresholds (fraction of pixels per class)29 minor_threshold: float = 0.0530 moderate_threshold: float = 0.2031 severe_threshold: float = 0.4032 # CORS33 allow_origins: list[str] = ["*"]3435 class Config:36 env_file = ".env"3738settings = Settings()
app/schemas.pyPydantic 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.
python1%%writefile app/schemas.py2# =============================================================3# CELL 4 — WRITE app/schemas.py4# =============================================================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, and14# (c) produce nice JSON error messages on bad input.15# =============================================================16from pydantic import BaseModel, Field17from typing import Literal1819DamageClass = Literal["no-damage", "minor", "moderate", "major", "destroyed"]20ATC20Tag = Literal["GREEN", "YELLOW", "RED"]2122class HealthOut(BaseModel):23 status: Literal["ok"] = "ok"24 model_loaded: bool25 version: str = "1.0.0"2627class PredictOut(BaseModel):28 class_counts: dict[DamageClass, int] = Field(..., description="Pixel count per class")29 class_fractions: dict[DamageClass, float]30 atc20_tag: ATC20Tag31 confidence: float = Field(..., ge=0.0, le=1.0)32 width: int33 height: int3435class ErrorOut(BaseModel):36 detail: str
app/preprocess.pyLoads 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.
python1%%writefile app/preprocess.py2# =============================================================3# CELL 5 — WRITE app/preprocess.py4# =============================================================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 exact12# 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 io18import numpy as np19from PIL import Image20import rasterio21from rasterio.io import MemoryFile2223MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)24STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)2526def load_image(raw: bytes) -> np.ndarray:27 """Return HxWx3 uint8 array from JPG, PNG, or GeoTIFF bytes."""28 # Try GeoTIFF first29 try:30 with MemoryFile(raw) as mem, mem.open() as src:31 arr = src.read(out_dtype="uint8") # CxHxW32 if arr.shape[0] >= 3:33 arr = np.transpose(arr[:3], (1, 2, 0)) # HxWx334 else:35 arr = np.repeat(arr[:1], 3, axis=0)36 arr = np.transpose(arr, (1, 2, 0))37 return arr38 except rasterio.errors.RasterioIOError:39 img = Image.open(io.BytesIO(raw)).convert("RGB")40 return np.array(img, dtype=np.uint8)4142def normalize(arr: np.ndarray) -> np.ndarray:43 """uint8 HxWx3 → float32 1x3xHxW, ImageNet normalized."""44 x = arr.astype(np.float32) / 255.045 x = (x - MEAN) / STD46 x = np.transpose(x, (2, 0, 1))[None, ...]47 return np.ascontiguousarray(x)4849def 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)
app/inference.pyCached 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.
python1%%writefile app/inference.py2# =============================================================3# CELL 6 — WRITE app/inference.py4# =============================================================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 so12# 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 use14# it automatically. Returns an HxW class-id map.15# =============================================================16from functools import lru_cache17import numpy as np18import onnxruntime as ort19from .config import settings2021@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)2728def 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] # 1xCxHxW34 return np.argmax(logits, axis=1)[0] # HxW
app/postprocess.pyTurns 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.
python1%%writefile app/postprocess.py2# =============================================================3# CELL 7 — WRITE app/postprocess.py4# =============================================================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 a12# human-friendly summary: per-class pixel counts, fractions, and a13# 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 np17from .config import settings18from .schemas import PredictOut1920CLASS_NAMES = ["no-damage", "minor", "moderate", "major", "destroyed"]2122def summarize(mask: np.ndarray) -> PredictOut:23 h, w = mask.shape24 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()}2728 severe = fracs["major"] + fracs["destroyed"]29 moderate = fracs["moderate"]30 minor = fracs["minor"]3132 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)4041 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 )
app/deps.pySingle 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.
python1%%writefile app/deps.py2# =============================================================3# CELL 8 — WRITE app/deps.py4# =============================================================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 calls13# this function for each request and hands the result to the route.14# Keeping it in its own file lets us swap implementations later15# (e.g. mock the model in tests) without touching main.py.16# =============================================================17from fastapi import Depends18from .inference import get_session1920def model_session():21 return get_session()
app/main.pyThe 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.
python1%%writefile app/main.py2# =============================================================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 the15# first real request isn't slow. If the model file is missing16# it logs a warning instead of crashing — /health will then17# return model_loaded=false so you know to fix it.18# - Exposes two endpoints:19# GET /health → quick liveness check20# POST /predict → multipart upload of 'pre' and 'post' images21# - Adds permissive CORS so the API can be called from any browser.22# =============================================================23import logging24from contextlib import asynccontextmanager25from fastapi import FastAPI, UploadFile, File, HTTPException, Depends26from fastapi.middleware.cors import CORSMiddleware27from fastapi.responses import JSONResponse2829from .config import settings30from .schemas import HealthOut, PredictOut, ErrorOut31from .preprocess import load_image, pair_to_batch32from .inference import predict, get_session33from .postprocess import summarize34from .deps import model_session3536logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")37log = logging.getLogger("damage-api")3839@asynccontextmanager40async 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=false47 log.warning("Model not loaded at startup: %s", e)48 yield4950app = 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)5657# NOTE: when allow_origins=["*"], allow_credentials MUST be False58# (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)6768@app.get("/health", response_model=HealthOut, tags=["meta"])69def health():70 return HealthOut(model_loaded=settings.model_path.exists())7172@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 raise95 except Exception as e:96 log.exception("Prediction failed")97 raise HTTPException(500, f"Inference error: {e}")9899@app.exception_handler(Exception)100async def unhandled(_, exc: Exception):101 return JSONResponse(status_code=500, content={"detail": str(exc)})
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.
python1# =============================================================2# CELL 10 — (OPTIONAL) WRITE A DUMMY ONNX MODEL FOR TESTING3# =============================================================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 cell12# writes a tiny "fake" model with the SAME input/output shape so the13# API can boot and you can test the endpoints. The predictions will14# be meaningless — that's fine, the goal here is to verify the15# 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're22# running real inference.23# =============================================================24!pip install -q torch==2.4.025import torch, torch.nn as nn2627class DummySiamese(nn.Module):28 def forward(self, pre, post):29 # Return logits shaped (1, 5, H, W) — 5 damage classes30 diff = (post - pre).mean(dim=1, keepdim=True) # 1x1xHxW31 logits = diff.repeat(1, 5, 1, 1) # 1x5xHxW32 return logits3334model = 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")
Run locally
python1# =============================================================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 folder9# that contains the app/ directory.10# 2. Open a terminal in that folder.11# 3. Run: python run.py12# 4. Open http://localhost:8000/docs in your browser.13# 5. Stop the server any time with Ctrl + C.14# =============================================================15import uvicorn16if __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.
Run in Google Colab (ngrok)
python1# =============================================================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-authtoken8# c. Copy the token shown there.9# d. Uncomment the 'ngrok.set_auth_token(...)' line below and10# 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/docs18# 6. CLICK the Swagger UI link. A new browser tab opens with the19# interactive API docs. This is where you can try /predict by20# hand: click "POST /predict" → "Try it out" → choose two21# 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 all26# cells from the top.27# =============================================================28import nest_asyncio, uvicorn, threading29from pyngrok import ngrok3031nest_asyncio.apply()3233# (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")3839def _serve():40 uvicorn.run("app.main:app", host="0.0.0.0", port=8000, log_level="info")4142threading.Thread(target=_serve, daemon=True).start()
The printed public URL exposes /docs and /predict to the internet for the lifetime of the notebook.
Test it
From the shell with curl:
bash1# =============================================================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 with8# 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.jpg12# 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 the18# ' | jq' pipe — you'll get raw JSON which is still readable.19# =============================================================20!curl -s HOST/health | jq2122!curl -s -X POST HOST/predict \23 -F "pre=@./samples/pre.jpg" \24 -F "post=@./samples/post.jpg" | jq
…or from Python:
python1# =============================================================2# CELL 13b — TEST THE API FROM PYTHON3# =============================================================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 exist11# (see Cell 13a step 4 for how to upload them).12# 5. Press Shift + Enter. You'll see two printouts:13# - the /health response14# - the /predict response (counts, fractions, ATC-20 tag).15# =============================================================16import requests1718BASE = "http://localhost:8000" # ← replace with your ngrok URL in Colab1920print(requests.get(f"{BASE}/health").json())2122with 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:
text1# requirements.txt2fastapi==0.115.03uvicorn==0.30.64python-multipart==0.0.95pydantic==2.8.26pydantic-settings==2.4.07rasterio==1.3.108pillow==10.4.09onnxruntime==1.18.110numpy==1.26.411nest_asyncio==1.6.012pyngrok==7.2.0
Dockerfile for a production container:
dockerfile1# Dockerfile (optional production image)2FROM python:3.11-slim3WORKDIR /app4RUN 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.txt8COPY app ./app9COPY models ./models10EXPOSE 800011CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
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.