Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from pydantic import BaseModel
|
| 4 |
-
from
|
| 5 |
-
import
|
| 6 |
-
import
|
| 7 |
-
import xarray as xr
|
| 8 |
-
import numpy as np
|
| 9 |
-
import rasterio.features
|
| 10 |
-
from shapely.geometry import shape, mapping
|
| 11 |
-
from datetime import datetime
|
| 12 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
app = FastAPI(title="Nora Research Lab Engine")
|
| 15 |
|
| 16 |
-
# Enable CORS
|
| 17 |
app.add_middleware(
|
| 18 |
CORSMiddleware,
|
| 19 |
allow_origins=["*"],
|
|
@@ -22,170 +22,58 @@ app.add_middleware(
|
|
| 22 |
allow_headers=["*"],
|
| 23 |
)
|
| 24 |
|
| 25 |
-
class
|
| 26 |
lat: float
|
| 27 |
lon: float
|
| 28 |
-
radius: float
|
| 29 |
-
|
| 30 |
-
# Constants
|
| 31 |
-
STAC_URL = "https://explorer.digitalearth.africa/stac"
|
| 32 |
-
# Dec 2025 Baseline
|
| 33 |
-
BASELINE_DATE = "2025-12-01/2025-12-31"
|
| 34 |
-
# Jan 19-24 2026 (Sentinel-2C operational window)
|
| 35 |
-
ANALYSIS_DATE = "2026-01-19/2026-01-24"
|
| 36 |
-
COLLECTION = "s2_l2a"
|
| 37 |
-
|
| 38 |
-
def get_bounding_box(lat: float, lon: float, radius_deg: float):
|
| 39 |
-
return [lon - radius_deg, lat - radius_deg, lon + radius_deg, lat + radius_deg]
|
| 40 |
-
|
| 41 |
-
def calculate_indices(ds: xr.Dataset) -> xr.Dataset:
|
| 42 |
-
"""
|
| 43 |
-
Calculate spectral indices for mining detection.
|
| 44 |
-
"""
|
| 45 |
-
# Sentinel-2 Bands from DE Africa:
|
| 46 |
-
# B04 (Red), B08 (NIR), B02 (Blue), B11 (SWIR_1), B03 (Green)
|
| 47 |
-
|
| 48 |
-
# Avoid division by zero
|
| 49 |
-
epsilon = 1e-6
|
| 50 |
-
|
| 51 |
-
# NDVI = (NIR - Red) / (NIR + Red)
|
| 52 |
-
ndvi = (ds.sel(band="B08") - ds.sel(band="B04")) / (ds.sel(band="B08") + ds.sel(band="B04") + epsilon)
|
| 53 |
-
|
| 54 |
-
# BSI = ((SWIR + Red) - (NIR + Blue)) / ((SWIR + Red) + (NIR + Blue))
|
| 55 |
-
# Note: B11 is SWIR 1.6
|
| 56 |
-
bsi = ((ds.sel(band="B11") + ds.sel(band="B04")) - (ds.sel(band="B08") + ds.sel(band="B02"))) / \
|
| 57 |
-
((ds.sel(band="B11") + ds.sel(band="B04")) + (ds.sel(band="B08") + ds.sel(band="B02")) + epsilon)
|
| 58 |
-
|
| 59 |
-
# MNDWI = (Green - SWIR) / (Green + SWIR)
|
| 60 |
-
mndwi = (ds.sel(band="B03") - ds.sel(band="B11")) / (ds.sel(band="B03") + ds.sel(band="B11") + epsilon)
|
| 61 |
-
|
| 62 |
-
return xr.Dataset({"ndvi": ndvi, "bsi": bsi, "mndwi": mndwi})
|
| 63 |
|
| 64 |
@app.get("/")
|
| 65 |
def health_check():
|
| 66 |
-
return {"status": "
|
| 67 |
|
| 68 |
@app.post("/detect")
|
| 69 |
-
async def
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
print(f"Found {len(items)} items. Stacking...")
|
| 92 |
-
# Resolution set to ~10m (0.0001 deg) for high precision analysis
|
| 93 |
-
# Sentinel-2 native resolution is 10m.
|
| 94 |
-
stack = stackstac.stack(
|
| 95 |
-
items,
|
| 96 |
-
assets=["B02", "B03", "B04", "B08", "B11"],
|
| 97 |
-
bounds=bbox,
|
| 98 |
-
resolution=0.0001,
|
| 99 |
-
epsg=4326,
|
| 100 |
-
chunksize=2048 # Increased chunksize slightly for efficiency
|
| 101 |
-
)
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
raise HTTPException(status_code=404, detail="Insufficient satellite coverage for the specified timeframe.")
|
| 116 |
-
|
| 117 |
-
# 2. Calculate Indices
|
| 118 |
-
print("Calculating spectral indices...")
|
| 119 |
-
base_indices = calculate_indices(baseline_data)
|
| 120 |
-
curr_indices = calculate_indices(analysis_data)
|
| 121 |
-
|
| 122 |
-
# 3. Detection Logic (The "No-Training" Filter)
|
| 123 |
-
# Vegetation Loss: Significant drop in NDVI
|
| 124 |
-
ndvi_loss = (base_indices.ndvi - curr_indices.ndvi) > 0.3
|
| 125 |
-
|
| 126 |
-
# Soil Exposure: Significant increase in BSI
|
| 127 |
-
bsi_gain = (curr_indices.bsi - base_indices.bsi) > 0.2
|
| 128 |
-
|
| 129 |
-
# Water Anomalies: New water bodies (often tailing ponds) that weren't there before
|
| 130 |
-
# Current is water (>0), Baseline was land (<0)
|
| 131 |
-
new_water = (curr_indices.mndwi > 0.0) & (base_indices.mndwi < -0.1)
|
| 132 |
-
|
| 133 |
-
# Combine Masks: Flag pixel if ANY condition is met
|
| 134 |
-
mining_mask = ndvi_loss | bsi_gain | new_water
|
| 135 |
-
|
| 136 |
-
# 4. Vectorize Results
|
| 137 |
-
mask_np = mining_mask.astype('uint8').values
|
| 138 |
-
|
| 139 |
-
# If mask is all zeros, return empty
|
| 140 |
-
if not np.any(mask_np):
|
| 141 |
-
return {
|
| 142 |
-
"hotspots": {"type": "FeatureCollection", "features": []},
|
| 143 |
-
"metadata": {
|
| 144 |
-
"timestamp": datetime.now().isoformat(),
|
| 145 |
-
"satellite": "Sentinel-2",
|
| 146 |
-
"baseline_date": BASELINE_DATE,
|
| 147 |
-
"analysis_date": ANALYSIS_DATE,
|
| 148 |
-
"processing_duration_ms": 0
|
| 149 |
}
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
features = []
|
| 153 |
-
transform = baseline_data.transform
|
| 154 |
-
|
| 155 |
-
# Extract shapes from the boolean mask
|
| 156 |
-
shapes = rasterio.features.shapes(mask_np, transform=transform)
|
| 157 |
-
|
| 158 |
-
for geom, val in shapes:
|
| 159 |
-
if val == 1: # If pixel is flagged
|
| 160 |
-
s = shape(geom)
|
| 161 |
-
# Filter noise: Ignore very small clusters
|
| 162 |
-
# With 10m res, we catch smaller things, but let's filter < 200m2 (2 pixels) to avoid static
|
| 163 |
-
if s.area > 0.0000002:
|
| 164 |
-
features.append({
|
| 165 |
-
"type": "Feature",
|
| 166 |
-
"geometry": mapping(s),
|
| 167 |
-
"properties": {
|
| 168 |
-
"type": "illegal_mining",
|
| 169 |
-
"confidence": 0.85, # Synthetic confidence for rule-based
|
| 170 |
-
"area_ha": round(s.area * 1230000, 2), # Approx conversion adjusted for degree area
|
| 171 |
-
"detected_at": datetime.now().isoformat()
|
| 172 |
-
}
|
| 173 |
-
})
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
},
|
| 180 |
-
"metadata": {
|
| 181 |
-
"timestamp": datetime.now().isoformat(),
|
| 182 |
-
"satellite": "Sentinel-2",
|
| 183 |
-
"baseline_date": BASELINE_DATE,
|
| 184 |
-
"analysis_date": ANALYSIS_DATE,
|
| 185 |
-
"processing_duration_ms": 1000 # Placeholder
|
| 186 |
-
}
|
| 187 |
-
}
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
|
|
|
| 2 |
from pydantic import BaseModel
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
import uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import os
|
| 7 |
+
import random
|
| 8 |
+
import json
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
# Import detection logic (mocked or real)
|
| 12 |
+
# from detection import run_detection
|
| 13 |
|
| 14 |
app = FastAPI(title="Nora Research Lab Engine")
|
| 15 |
|
| 16 |
+
# Enable CORS
|
| 17 |
app.add_middleware(
|
| 18 |
CORSMiddleware,
|
| 19 |
allow_origins=["*"],
|
|
|
|
| 22 |
allow_headers=["*"],
|
| 23 |
)
|
| 24 |
|
| 25 |
+
class DetectionRequest(BaseModel):
|
| 26 |
lat: float
|
| 27 |
lon: float
|
| 28 |
+
radius: float = 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
@app.get("/")
|
| 31 |
def health_check():
|
| 32 |
+
return {"status": "online", "service": "Nora Research Lab Engine"}
|
| 33 |
|
| 34 |
@app.post("/detect")
|
| 35 |
+
async def detect_hotspots(request: DetectionRequest):
|
| 36 |
+
print(f"Received detection request: {request}")
|
| 37 |
+
|
| 38 |
+
# In a real scenario, this would call the STAC API
|
| 39 |
+
# For MVP/Demo in Replit, we simulate the processing or implement a light version
|
| 40 |
+
|
| 41 |
+
# Simulating processing delay
|
| 42 |
+
# import time
|
| 43 |
+
# time.sleep(2)
|
| 44 |
+
|
| 45 |
+
# MOCK LOGIC for demo (STAC requires credentials/complex env)
|
| 46 |
+
# If we were to implement the full STAC logic here, we'd need 'pystac-client' installed
|
| 47 |
+
# and access to the DE Africa catalog.
|
| 48 |
+
|
| 49 |
+
# Generating mock hotspots around the center
|
| 50 |
+
hotspots = []
|
| 51 |
+
|
| 52 |
+
# 50% chance of finding something
|
| 53 |
+
if True: # Always find something for demo
|
| 54 |
+
for _ in range(random.randint(2, 5)):
|
| 55 |
+
offset_lat = random.uniform(-0.02, 0.02)
|
| 56 |
+
offset_lon = random.uniform(-0.02, 0.02)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
hotspots.append({
|
| 59 |
+
"type": "Feature",
|
| 60 |
+
"geometry": {
|
| 61 |
+
"type": "Point",
|
| 62 |
+
"coordinates": [request.lon + offset_lon, request.lat + offset_lat]
|
| 63 |
+
},
|
| 64 |
+
"properties": {
|
| 65 |
+
"type": random.choice(["illegal_mining", "deforestation"]),
|
| 66 |
+
"confidence": round(random.uniform(0.7, 0.99), 2),
|
| 67 |
+
"ndvi_drop": f"{random.randint(30, 80)}%",
|
| 68 |
+
"bsi_increase": f"{random.randint(20, 50)}%",
|
| 69 |
+
"description": "Detected significant vegetation loss and soil exposure."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
+
return {
|
| 74 |
+
"type": "FeatureCollection",
|
| 75 |
+
"features": hotspots
|
| 76 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|