Mingze's picture
Refine urban case card grid
b55c7c3 verified
Raw
History Blame Contribute Delete
29.6 kB
"""Professional Hugging Face Space for multi-level satellite image analysis."""
from __future__ import annotations
import os
import time
import uuid
from functools import lru_cache
from pathlib import Path
import gradio as gr
import numpy as np
import torch
from huggingface_hub import hf_hub_download
from PIL import Image
from transformers import (
AutoImageProcessor,
AutoModelForImageClassification,
Mask2FormerForUniversalSegmentation,
)
try:
import spaces
except ImportError:
class _SpacesFallback:
@staticmethod
def GPU(*_args, **_kwargs):
def decorator(function):
return function
return decorator
spaces = _SpacesFallback()
from satellite_utils import (
build_analysis_summary,
build_class_table,
build_detection_summary,
build_detection_table,
build_lulc_table,
normalized_entropy,
render_detections,
render_lulc_assessment,
render_segmentation,
resize_for_inference,
write_class_csv,
write_detection_csv,
write_json,
write_lulc_csv,
write_pixel_geojson,
)
CLASSIFICATION_MODEL_ID = "mrm8488/convnext-tiny-finetuned-eurosat"
SEGMENTATION_MODEL_ID = "mfaytin/mask2former-satellite"
DETECTION_MODEL_ID = "bluelabel/satellite-equipment-detection-yolov8n-vhr10"
DETECTION_FILENAME = "best.pt"
OUTPUT_ROOT = Path("/tmp/satellite-vision-toolkit")
OPEN_EARTH_MAP_LABELS = {
0: "background",
1: "bareland",
2: "grass",
3: "pavement",
4: "road",
5: "tree",
6: "water",
7: "cropland",
8: "building",
}
CASE_STUDIES = {
"residential": {
"title": "Dense residential block",
"image": "assets/cases/dense_residential.jpg",
"source": "https://huggingface.co/datasets/blanchon/UC_Merced",
"sensor": "USGS Urban Area Imagery · RGB · 0.3 m · 256×256 px",
"focus": "Inspect buildings, impervious surfaces, street texture, and residential scene confidence.",
},
"intersection": {
"title": "Urban intersection",
"image": "assets/cases/urban_intersection.jpg",
"source": "https://huggingface.co/datasets/blanchon/UC_Merced",
"sensor": "USGS Urban Area Imagery · RGB · 0.3 m · 256×256 px",
"focus": "Test road/pavement segmentation and small-vehicle sensitivity at a city junction.",
},
"harbor": {
"title": "Urban marina / harbor",
"image": "assets/cases/harbor_marina.jpg",
"source": "https://huggingface.co/datasets/blanchon/UC_Merced",
"sensor": "USGS Urban Area Imagery · RGB · 0.3 m · 256×256 px",
"focus": "Compare water segmentation with supported ship/harbor object predictions.",
},
"parking": {
"title": "Urban parking lot",
"image": "assets/cases/parking_lot.jpg",
"source": "https://huggingface.co/datasets/blanchon/UC_Merced",
"sensor": "USGS Urban Area Imagery · RGB · 0.3 m · 256×256 px",
"focus": "Probe pavement coverage and the detector's limits for tightly packed small vehicles.",
},
}
def _device() -> torch.device:
torch.set_num_threads(max(1, min(4, os.cpu_count() or 1)))
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
@lru_cache(maxsize=1)
def load_classifier():
device = _device()
processor = AutoImageProcessor.from_pretrained(CLASSIFICATION_MODEL_ID)
model = AutoModelForImageClassification.from_pretrained(CLASSIFICATION_MODEL_ID).to(device).eval()
id2label = {int(key): str(value) for key, value in model.config.id2label.items()}
return processor, model, id2label, device
@lru_cache(maxsize=1)
def load_segmenter():
device = _device()
processor = AutoImageProcessor.from_pretrained(SEGMENTATION_MODEL_ID)
model = Mask2FormerForUniversalSegmentation.from_pretrained(SEGMENTATION_MODEL_ID).to(device).eval()
return processor, model, OPEN_EARTH_MAP_LABELS, device
@lru_cache(maxsize=1)
def load_detector():
from ultralytics import YOLO
weights = hf_hub_download(repo_id=DETECTION_MODEL_ID, filename=DETECTION_FILENAME)
return YOLO(weights)
def _new_output_dir() -> Path:
output_dir = OUTPUT_ROOT / uuid.uuid4().hex
output_dir.mkdir(parents=True, exist_ok=True)
return output_dir
def _require_image(image: Image.Image | None) -> Image.Image:
if image is None:
raise gr.Error("Please upload a satellite or aerial image first.")
return resize_for_inference(image)
def load_case_study(case_key: str):
"""Load a documented NASA case into the shared image input."""
case = CASE_STUDIES[case_key]
note = (
f"### {case['title']}\n"
f"**Acquisition:** {case['sensor']} \n"
f"**Suggested analysis:** {case['focus']} \n"
f"[Open UC Merced / USGS source]({case['source']}) · "
"High-resolution aerial imagery is intentionally outside EuroSAT's Sentinel-2 scale; review domain shift and do not treat the dataset label as model ground truth."
)
return case["image"], note
def _classify_impl(prepared: Image.Image, top_k: int, output_dir: Path) -> dict[str, object]:
processor, model, id2label, device = load_classifier()
inputs = {name: tensor.to(device) for name, tensor in processor(images=prepared, return_tensors="pt").items()}
with torch.inference_mode():
logits = model(**inputs).logits[0]
probabilities = torch.softmax(logits, dim=-1).detach().cpu().tolist()
rows = build_lulc_table(probabilities, id2label, top_k)
entropy = normalized_entropy(probabilities)
csv_path = output_dir / "lulc_classification.csv"
json_path = output_dir / "lulc_classification.json"
write_lulc_csv(csv_path, rows)
write_json(
json_path,
{
"task": "scene_level_lulc_classification",
"model": CLASSIFICATION_MODEL_ID,
"processed_image_size": {"width": prepared.width, "height": prepared.height},
"normalized_entropy": round(entropy, 6),
"predictions": [
{"rank": row[0], "class": row[1], "probability_percent": row[2], "confidence_tier": row[3]}
for row in rows
],
"scope_note": "Whole-scene EuroSAT class; not a cadastral or planning land-use designation.",
},
)
return {
"rows": rows,
"entropy": entropy,
"assessment": render_lulc_assessment(rows, entropy),
"files": [str(csv_path), str(json_path)],
"device": device.type,
}
def _segment_impl(
prepared: Image.Image,
opacity: float,
min_share_percent: float,
output_dir: Path,
) -> dict[str, object]:
processor, model, id2label, device = load_segmenter()
inputs = {name: tensor.to(device) for name, tensor in processor(images=prepared, return_tensors="pt").items()}
with torch.inference_mode():
outputs = model(**inputs)
class_map = processor.post_process_semantic_segmentation(
outputs,
target_sizes=[(prepared.height, prepared.width)],
)[0].cpu().numpy().astype(np.uint8)
overlay, color_mask = render_segmentation(prepared, class_map, id2label, float(opacity))
rows = build_class_table(class_map, id2label, float(min_share_percent))
overlay_path = output_dir / "land_cover_overlay.png"
mask_path = output_dir / "land_cover_color_mask.png"
ids_path = output_dir / "land_cover_class_ids.png"
csv_path = output_dir / "land_cover_summary.csv"
overlay.save(overlay_path)
color_mask.save(mask_path)
Image.fromarray(class_map).save(ids_path)
write_class_csv(csv_path, rows)
return {
"overlay": overlay,
"mask": color_mask,
"rows": rows,
"files": [str(overlay_path), str(mask_path), str(ids_path), str(csv_path)],
"device": device.type,
}
def _detect_impl(
prepared: Image.Image,
confidence_threshold: float,
iou_threshold: float,
output_dir: Path,
) -> dict[str, object]:
device = "cuda" if torch.cuda.is_available() else "cpu"
detector = load_detector()
prediction = detector.predict(
source=np.asarray(prepared),
conf=float(confidence_threshold),
iou=float(iou_threshold),
imgsz=1024,
device=device,
max_det=500,
verbose=False,
)[0]
detections: list[dict[str, object]] = []
if prediction.boxes is not None:
for coordinates, confidence, class_id_value in zip(
prediction.boxes.xyxy.detach().cpu().tolist(),
prediction.boxes.conf.detach().cpu().tolist(),
prediction.boxes.cls.detach().cpu().tolist(),
):
class_id = int(class_id_value)
detections.append(
{
"class_id": class_id,
"class_name": str(prediction.names[class_id]),
"confidence": float(confidence),
"x1": float(coordinates[0]),
"y1": float(coordinates[1]),
"x2": float(coordinates[2]),
"y2": float(coordinates[3]),
}
)
overlay = render_detections(prepared, detections)
summary_rows = build_detection_summary(detections)
detail_rows = build_detection_table(detections, prepared.size)
overlay_path = output_dir / "satellite_detection_overlay.png"
csv_path = output_dir / "satellite_detections.csv"
geojson_path = output_dir / "satellite_detections_pixel_coordinates.geojson"
overlay.save(overlay_path)
write_detection_csv(csv_path, detail_rows)
write_pixel_geojson(geojson_path, detections, prepared.size)
return {
"overlay": overlay,
"summary": summary_rows,
"details": detail_rows,
"files": [str(overlay_path), str(csv_path), str(geojson_path)],
"device": device,
}
@spaces.GPU(duration=120)
def classify_lulc(image: Image.Image | None, top_k: int):
started_at = time.perf_counter()
prepared = _require_image(image)
try:
result = _classify_impl(prepared, int(top_k), _new_output_dir())
except Exception as exc:
raise gr.Error(f"LULC classification failed: {type(exc).__name__}: {exc}") from exc
status = (
f"Complete · {prepared.width}×{prepared.height} · top class {result['rows'][0][1]} "
f"({result['rows'][0][2]:.1f}%) · {time.perf_counter() - started_at:.1f}s · device={result['device']}"
)
return result["assessment"], result["rows"], result["files"], status
@spaces.GPU(duration=120)
def segment_satellite_image(image: Image.Image | None, opacity: float, min_share_percent: float):
started_at = time.perf_counter()
prepared = _require_image(image)
try:
result = _segment_impl(prepared, opacity, min_share_percent, _new_output_dir())
except Exception as exc:
raise gr.Error(f"Land-cover segmentation failed: {type(exc).__name__}: {exc}") from exc
status = (
f"Complete · {prepared.width}×{prepared.height} · {len(result['rows'])} reported cover classes · "
f"{time.perf_counter() - started_at:.1f}s · device={result['device']}"
)
return result["overlay"], result["mask"], result["rows"], result["files"], status
@spaces.GPU(duration=120)
def detect_satellite_objects(image: Image.Image | None, confidence_threshold: float, iou_threshold: float):
started_at = time.perf_counter()
prepared = _require_image(image)
try:
result = _detect_impl(prepared, confidence_threshold, iou_threshold, _new_output_dir())
except Exception as exc:
raise gr.Error(f"Satellite object detection failed: {type(exc).__name__}: {exc}") from exc
status = (
f"Complete · {prepared.width}×{prepared.height} · {len(result['details'])} objects · "
f"{len(result['summary'])} classes · {time.perf_counter() - started_at:.1f}s · device={result['device']}"
)
return result["overlay"], result["summary"], result["details"], result["files"], status
@spaces.GPU(duration=180)
def analyze_satellite_image(
image: Image.Image | None,
top_k: int,
opacity: float,
min_share_percent: float,
confidence_threshold: float,
iou_threshold: float,
):
started_at = time.perf_counter()
prepared = _require_image(image)
output_dir = _new_output_dir()
try:
classification = _classify_impl(prepared, int(top_k), output_dir)
segmentation = _segment_impl(prepared, opacity, min_share_percent, output_dir)
detection = _detect_impl(prepared, confidence_threshold, iou_threshold, output_dir)
except Exception as exc:
raise gr.Error(f"Complete analysis failed: {type(exc).__name__}: {exc}") from exc
elapsed = time.perf_counter() - started_at
summary = build_analysis_summary(
classification["rows"],
float(classification["entropy"]),
segmentation["rows"],
detection["summary"],
elapsed,
)
report_path = output_dir / "analysis_report.json"
write_json(
report_path,
{
"processed_image_size": {"width": prepared.width, "height": prepared.height},
"models": {
"classification": CLASSIFICATION_MODEL_ID,
"segmentation": SEGMENTATION_MODEL_ID,
"detection": DETECTION_MODEL_ID,
},
"lulc_classification": classification["rows"],
"lulc_normalized_entropy": round(float(classification["entropy"]), 6),
"land_cover_pixel_shares": segmentation["rows"],
"detection_summary": detection["summary"],
"detection_details": detection["details"],
"elapsed_seconds": round(elapsed, 3),
"coordinate_note": "Detection GeoJSON is in top-left-origin image pixels and has no geographic CRS.",
},
)
files = classification["files"] + segmentation["files"] + detection["files"] + [str(report_path)]
status = f"Complete multi-model assessment · {prepared.width}×{prepared.height} · {elapsed:.1f}s"
return (
summary,
classification["assessment"],
classification["rows"],
segmentation["overlay"],
segmentation["mask"],
segmentation["rows"],
detection["overlay"],
detection["summary"],
detection["details"],
files,
status,
)
CSS = """
.gradio-container {max-width: 1380px !important; background: #f4f7f9; color:#172b35;}
.hero {padding: 1.55rem 1.8rem; border-radius: 20px; color:#fff !important; background: linear-gradient(118deg,#071d31 0%,#0c4656 58%,#13806b 100%); box-shadow:0 14px 36px rgba(7,29,49,.22); margin: .35rem 0 .8rem;}
.hero-grid {display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:center;}
.hero h1,#hero-title {font-size:2.25rem;line-height:1.08;margin:.3rem 0 .5rem;letter-spacing:-.035em;color:#fff !important;text-shadow:0 1px 1px rgba(0,0,0,.12);}
.hero p {max-width:800px;margin:.35rem 0;color:#e5f7f5 !important;font-size:.98rem;line-height:1.5;}
.hero .eyebrow {color:#8ff0dc !important;}.hero-links{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.hero-links a{color:#fff!important;text-decoration:none;border:1px solid rgba(255,255,255,.35);background:rgba(255,255,255,.09);padding:5px 10px;border-radius:999px;font-size:.8rem;font-weight:700}.hero-links a:hover{background:rgba(255,255,255,.18)}
.hero-stats{display:grid;grid-template-columns:repeat(2,88px);gap:8px}.hero-stats div{border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);border-radius:13px;padding:11px;text-align:center}.hero-stats strong{display:block;color:#fff;font-size:1.3rem}.hero-stats span{color:#cce9e5;font-size:.7rem}
.method-strip {display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:0 0 1rem;}
.method-strip div,.assessment-card,.metric-card {background:white;border:1px solid #d9e4e9;border-radius:13px;padding:12px 14px;box-shadow:0 4px 14px rgba(20,50,70,.045);}
.method-strip b {display:block;color:#0b5363;margin-bottom:3px;font-size:.86rem}.method-strip span,.micro-note {color:#617681;font-size:.78rem;}
.summary-grid {display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:12px 0;}
.metric-card span,.eyebrow {display:block;color:#66808c;font-size:.72rem;font-weight:750;letter-spacing:.1em;text-transform:uppercase;}
.metric-card strong {display:block;font-size:1.45rem;margin:6px 0;color:#113544;}.metric-card small {color:#60747e;}
.assessment-card h2 {margin:.25rem 0;color:#123c49}.assessment-card p {color:#526b76;}
.prob-row {display:grid;grid-template-columns:155px 1fr 62px;gap:10px;align-items:center;margin:8px 0;font-size:.86rem;}
.prob-row b {text-align:right}.prob-track {height:9px;background:#e5edf1;border-radius:20px;overflow:hidden}.prob-track i {display:block;height:100%;background:linear-gradient(90deg,#169c7d,#36b7c5);border-radius:20px;}
.section-note {padding:12px 14px;border-left:4px solid #15947a;background:#eef9f6;border-radius:8px;color:#315c62;}
.case-heading{display:flex;justify-content:space-between;align-items:end;margin:.35rem 2px .5rem}.case-heading h2{margin:0;color:#123746;font-size:1.2rem}.case-heading p{margin:0;color:#647984;font-size:.82rem}
.case-grid{display:grid!important;grid-template-columns:repeat(4,minmax(0,1fr))!important;gap:12px!important}.case-grid>div{min-width:0!important}
.case-card {background:#fff;border:1px solid #d7e2e8!important;border-radius:15px!important;padding:8px!important;box-shadow:0 6px 16px rgba(20,50,70,.06);min-width:0!important}.case-card:hover{border-color:#62a99a!important;box-shadow:0 9px 22px rgba(20,80,70,.1)}
.case-thumb {border-radius:10px!important;overflow:hidden;background:#e7eef1}.case-thumb img{width:100%!important;height:100%!important;object-fit:cover!important;image-rendering:auto!important}.case-card h3{margin:2px 2px 0!important;color:#143643;font-size:.92rem!important}.case-card p{margin:0 2px 3px!important;color:#667b85;font-size:.73rem!important;line-height:1.35}.case-card button{min-height:34px!important;font-size:.78rem!important}
.case-note{background:#eaf7f3;border:1px solid #b9ded3;border-radius:11px;padding:1px 12px;margin:.4rem 0 .75rem}.case-note h3{font-size:.95rem;margin:.6rem 0 .2rem}.case-note p{font-size:.8rem}
.workspace-title h3{margin-bottom:.25rem!important}.controls-card{background:#fff;border:1px solid #dae5ea;border-radius:15px;padding:14px!important}
@media(max-width:950px){.case-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}}
@media(max-width:850px){.hero-grid{grid-template-columns:1fr}.hero-stats{display:none}.method-strip,.summary-grid{grid-template-columns:1fr}.prob-row{grid-template-columns:115px 1fr 56px}.case-heading{display:block}}
@media(max-width:560px){.case-grid{grid-template-columns:1fr!important}}
"""
with gr.Blocks(title="Satellite Vision Toolkit Pro", css=CSS, theme=gr.themes.Soft()) as demo:
gr.HTML("""
<div class="hero"><div class="hero-grid"><div>
<div class="eyebrow">URBAN REMOTE SENSING WORKBENCH</div>
<h1 id="hero-title">Satellite Vision Toolkit</h1>
<p>Clear, multi-level interpretation of local urban overhead imagery—from whole-scene LULC context to pixel cover and individual objects.</p>
<div class="hero-links"><a href="https://github.com/LabMingzeChen/SatelliteVisionToolkit">GitHub</a><a href="https://huggingface.co/mrm8488/convnext-tiny-finetuned-eurosat">LULC model</a><a href="https://huggingface.co/mfaytin/mask2former-satellite">Segmentation</a><a href="https://huggingface.co/bluelabel/satellite-equipment-detection-yolov8n-vhr10">Detection</a></div>
</div><div class="hero-stats"><div><strong>3</strong><span>MODEL LEVELS</span></div><div><strong>4</strong><span>URBAN CASES</span></div><div><strong>10</strong><span>LULC CLASSES</span></div><div><strong>10</strong><span>OBJECT TYPES</span></div></div></div></div>
<div class="method-strip">
<div><b>01 · Scene classification</b><span>EuroSAT probability profile across 10 LULC scene types.</span></div>
<div><b>02 · Semantic segmentation</b><span>Per-pixel OpenEarthMap land-cover composition and masks.</span></div>
<div><b>03 · Object detection</b><span>Bounding boxes and inventory-style summaries for 10 VHR object types.</span></div>
</div>
""")
gr.HTML("<div class='case-heading'><h2>Urban sample scenes</h2><p>High-resolution 256×256 USGS aerial chips · click any card to load</p></div>")
with gr.Row(elem_classes="case-grid"):
with gr.Column(elem_classes="case-card"):
gr.Image("assets/cases/dense_residential.jpg", show_label=False, height=144, interactive=False, show_download_button=False, show_fullscreen_button=False, show_share_button=False, elem_classes="case-thumb")
gr.Markdown("### Dense residential\nBuildings · streets · impervious cover")
residential_case = gr.Button("Load residential scene")
with gr.Column(elem_classes="case-card"):
gr.Image("assets/cases/urban_intersection.jpg", show_label=False, height=144, interactive=False, show_download_button=False, show_fullscreen_button=False, show_share_button=False, elem_classes="case-thumb")
gr.Markdown("### Urban intersection\nRoads · vehicles · pavement")
intersection_case = gr.Button("Load intersection scene")
with gr.Column(elem_classes="case-card"):
gr.Image("assets/cases/harbor_marina.jpg", show_label=False, height=144, interactive=False, show_download_button=False, show_fullscreen_button=False, show_share_button=False, elem_classes="case-thumb")
gr.Markdown("### Marina / harbor\nWater · boats · harbor context")
harbor_case = gr.Button("Load harbor scene")
with gr.Column(elem_classes="case-card"):
gr.Image("assets/cases/parking_lot.jpg", show_label=False, height=144, interactive=False, show_download_button=False, show_fullscreen_button=False, show_share_button=False, elem_classes="case-thumb")
gr.Markdown("### Parking lot\nPavement · dense small vehicles")
parking_case = gr.Button("Load parking scene")
case_note = gr.Markdown("Select an urban case to load its acquisition details and analysis prompt.", elem_classes="case-note")
with gr.Row(equal_height=True):
image_input = gr.Image(type="pil", label="Analysis image", height=380)
with gr.Column(elem_classes="controls-card"):
gr.Markdown("### Run analysis\nUpload your own image or start with an urban case. Default settings suit most previews.", elem_classes="workspace-title")
analyze_button = gr.Button("Run complete professional assessment", variant="primary", size="lg")
with gr.Accordion("Advanced thresholds", open=False):
top_k = gr.Slider(3, 10, value=5, step=1, label="LULC alternatives (top-k)")
opacity = gr.Slider(0.1, 0.9, value=0.55, step=0.05, label="Segmentation overlay opacity")
min_share = gr.Slider(0.0, 5.0, value=0.1, step=0.1, label="Minimum reported cover share (%)")
confidence = gr.Slider(0.05, 0.9, value=0.25, step=0.05, label="Detection confidence threshold")
iou = gr.Slider(0.1, 0.9, value=0.45, step=0.05, label="Detection NMS IoU threshold")
gr.Markdown("**Best for:** local RGB satellite/aerial chips where buildings, roads, water, or supported objects are visible. Results are model estimates, not surveyed GIS data.")
with gr.Tabs():
with gr.Tab("Executive overview"):
analysis_status = gr.Markdown()
executive_summary = gr.HTML()
overview_lulc = gr.HTML()
overview_lulc_table = gr.Dataframe(
headers=["Rank", "LULC class", "Probability (%)", "Confidence tier"],
interactive=False,
label="Scene classification probability profile",
)
with gr.Row():
overview_segment = gr.Image(label="Pixel-level land-cover overlay")
overview_detection = gr.Image(label="Detected objects")
overview_files = gr.File(label="Download complete evidence package", file_count="multiple")
with gr.Tab("LULC classification"):
gr.Markdown("<div class='section-note'><b>Scene-level interpretation.</b> Assigns the whole image to EuroSAT land-use/land-cover classes. This is distinct from pixel segmentation and is not a legal land-use designation.</div>")
classify_button = gr.Button("Classify scene LULC", variant="primary")
classify_status = gr.Markdown()
classification_assessment = gr.HTML()
classification_table = gr.Dataframe(
headers=["Rank", "LULC class", "Probability (%)", "Confidence tier"],
interactive=False,
label="Ranked LULC alternatives",
)
classification_files = gr.File(label="Download classification CSV / JSON", file_count="multiple")
with gr.Tab("Land-cover segmentation"):
gr.Markdown("<div class='section-note'><b>Pixel-level interpretation.</b> Maps nine OpenEarthMap surface classes and reports image-pixel composition.</div>")
segment_button = gr.Button("Segment land cover", variant="primary")
segment_status = gr.Markdown()
with gr.Row():
segment_overlay = gr.Image(label="Land-cover overlay")
segment_mask = gr.Image(label="Categorical mask")
segment_table = gr.Dataframe(
headers=["Class ID", "Class", "Pixels", "Share (%)", "Color"],
interactive=False,
label="Land-cover area summary",
)
segment_files = gr.File(label="Download segmentation outputs", file_count="multiple")
with gr.Tab("Object detection"):
gr.Markdown("<div class='section-note'><b>Instance-level interpretation.</b> Locates supported objects with confidence-filtered bounding boxes.</div>")
detect_button = gr.Button("Detect satellite objects", variant="primary")
detect_status = gr.Markdown()
detect_overlay = gr.Image(label="Detection overlay")
detection_summary = gr.Dataframe(
headers=["Class", "Count", "Average confidence", "Maximum confidence"],
interactive=False,
label="Detection summary",
)
detection_details = gr.Dataframe(
headers=["ID", "Class", "Confidence", "x1", "y1", "x2", "y2", "Area (px²)", "Center x", "Center y"],
interactive=False,
label="Per-object results",
)
detection_files = gr.File(label="Download detection outputs", file_count="multiple")
with gr.Tab("Methodology & scope"):
gr.Markdown("""
### Analytical hierarchy
| Level | Question answered | Model / training domain | Output |
|---|---|---|---|
| Scene | What broad LULC type best characterizes this image? | ConvNeXT-Tiny / EuroSAT Sentinel-2 RGB | Ranked probabilities + entropy |
| Pixel | Which cover class is predicted at each pixel? | Mask2Former / OpenEarthMap | Overlay, mask, pixel shares |
| Object | Where are supported discrete objects? | YOLOv8n / NWPU VHR-10 | Boxes, counts, CSV, pixel GeoJSON |
**Interpretation guardrails:** EuroSAT is a European Sentinel-2 scene dataset; classification may shift on other sensors, regions, resolutions, or crops. Pixel shares are not automatically physical ground-area shares. Pixel-coordinate GeoJSON is not georeferenced. Models can miss small or obscured objects. Do not use outputs alone for legal, surveillance, emergency, navigation, or safety-critical decisions.
""")
residential_case.click(
lambda: load_case_study("residential"),
outputs=[image_input, case_note],
)
intersection_case.click(
lambda: load_case_study("intersection"),
outputs=[image_input, case_note],
)
harbor_case.click(
lambda: load_case_study("harbor"),
outputs=[image_input, case_note],
)
parking_case.click(
lambda: load_case_study("parking"),
outputs=[image_input, case_note],
)
classify_button.click(
classify_lulc,
inputs=[image_input, top_k],
outputs=[classification_assessment, classification_table, classification_files, classify_status],
api_name="classify",
)
segment_button.click(
segment_satellite_image,
inputs=[image_input, opacity, min_share],
outputs=[segment_overlay, segment_mask, segment_table, segment_files, segment_status],
api_name="segment",
)
detect_button.click(
detect_satellite_objects,
inputs=[image_input, confidence, iou],
outputs=[detect_overlay, detection_summary, detection_details, detection_files, detect_status],
api_name="detect",
)
analyze_button.click(
analyze_satellite_image,
inputs=[image_input, top_k, opacity, min_share, confidence, iou],
outputs=[
executive_summary,
overview_lulc,
overview_lulc_table,
overview_segment,
segment_mask,
segment_table,
overview_detection,
detection_summary,
detection_details,
overview_files,
analysis_status,
],
api_name="analyze",
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=2).launch()