Spaces:
Running on Zero
Running on Zero
Add guided NASA satellite case studies
Browse files- README.md +12 -0
- app.py +56 -0
- tests/test_app_contract.py +4 -0
README.md
CHANGED
|
@@ -50,6 +50,18 @@ Every run creates visual and machine-readable outputs:
|
|
| 50 |
- Independent Gradio API endpoints at `/classify`, `/segment`, `/detect`, and `/analyze`.
|
| 51 |
- A reusable Codex skill and command-line Space client.
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
## How it works
|
| 54 |
|
| 55 |
```text
|
|
|
|
| 50 |
- Independent Gradio API endpoints at `/classify`, `/segment`, `/detect`, and `/analyze`.
|
| 51 |
- A reusable Codex skill and command-line Space client.
|
| 52 |
|
| 53 |
+
## Guided case studies
|
| 54 |
+
|
| 55 |
+
The interface includes three one-click NASA Earth Observatory cases. Each case loads the reference image and shows the acquisition context, recommended analytical question, and source attribution.
|
| 56 |
+
|
| 57 |
+
| Case | Sensor / date | Suggested use |
|
| 58 |
+
|---|---|---|
|
| 59 |
+
| [Indus River irrigated agriculture](https://earthobservatory.nasa.gov/images/52076/seasonal-changes-along-the-indus-river) | Landsat 5 TM / 2009-09-10 | Compare crop and river scene alternatives with cropland/water pixel shares |
|
| 60 |
+
| [Lluta River desert agriculture](https://earthobservatory.nasa.gov/images/82296/lluta-river-chile) | EO-1 ALI / 2012-07-19 | Inspect ambiguity where narrow irrigated valleys cross dominant bare land |
|
| 61 |
+
| [Zambezi wet-season floodplain](https://earthobservatory.nasa.gov/images/80835/wet-season-transforms-the-zambezi-river) | EO-1 ALI / 2013-03-31 | Evaluate water, vegetation, and bare-land composition |
|
| 62 |
+
|
| 63 |
+
These are method-exploration examples, not ground-truth demonstrations. The imagery remains credited to NASA Earth Observatory and the source instrument teams.
|
| 64 |
+
|
| 65 |
## How it works
|
| 66 |
|
| 67 |
```text
|
app.py
CHANGED
|
@@ -65,6 +65,29 @@ OPEN_EARTH_MAP_LABELS = {
|
|
| 65 |
7: "cropland",
|
| 66 |
8: "building",
|
| 67 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
def _device() -> torch.device:
|
|
@@ -109,6 +132,19 @@ def _require_image(image: Image.Image | None) -> Image.Image:
|
|
| 109 |
return resize_for_inference(image)
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def _classify_impl(prepared: Image.Image, top_k: int, output_dir: Path) -> dict[str, object]:
|
| 113 |
processor, model, id2label, device = load_classifier()
|
| 114 |
inputs = {name: tensor.to(device) for name, tensor in processor(images=prepared, return_tensors="pt").items()}
|
|
@@ -353,6 +389,7 @@ CSS = """
|
|
| 353 |
.prob-row {display:grid;grid-template-columns:155px 1fr 62px;gap:10px;align-items:center;margin:8px 0;font-size:.86rem;}
|
| 354 |
.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;}
|
| 355 |
.section-note {padding:12px 14px;border-left:4px solid #15947a;background:#eef9f6;border-radius:8px;color:#315c62;}
|
|
|
|
| 356 |
@media(max-width:850px){.pipeline,.summary-grid{grid-template-columns:1fr}.prob-row{grid-template-columns:115px 1fr 56px}}
|
| 357 |
"""
|
| 358 |
|
|
@@ -371,6 +408,12 @@ with gr.Blocks(title="Satellite Vision Toolkit Pro", css=CSS, theme=gr.themes.So
|
|
| 371 |
<div><b>03 · Object detection</b><span>Bounding boxes and inventory-style summaries for 10 VHR object types.</span></div>
|
| 372 |
</div>
|
| 373 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
with gr.Row(equal_height=True):
|
| 375 |
image_input = gr.Image(type="pil", label="Satellite / aerial RGB image", height=430)
|
| 376 |
with gr.Column():
|
|
@@ -453,6 +496,19 @@ with gr.Blocks(title="Satellite Vision Toolkit Pro", css=CSS, theme=gr.themes.So
|
|
| 453 |
**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.
|
| 454 |
""")
|
| 455 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
classify_button.click(
|
| 457 |
classify_lulc,
|
| 458 |
inputs=[image_input, top_k],
|
|
|
|
| 65 |
7: "cropland",
|
| 66 |
8: "building",
|
| 67 |
}
|
| 68 |
+
CASE_STUDIES = {
|
| 69 |
+
"indus": {
|
| 70 |
+
"title": "Indus River · irrigated agriculture",
|
| 71 |
+
"image": "https://eoimages.gsfc.nasa.gov/images/imagerecords/52000/52076/indus_tm5_20090910_lrg.jpg",
|
| 72 |
+
"source": "https://earthobservatory.nasa.gov/images/52076/seasonal-changes-along-the-indus-river",
|
| 73 |
+
"sensor": "Landsat 5 TM · natural color · 2009-09-10",
|
| 74 |
+
"focus": "Compare scene-level crop/river alternatives with pixel-level cropland and water shares.",
|
| 75 |
+
},
|
| 76 |
+
"lluta": {
|
| 77 |
+
"title": "Lluta River · desert agriculture",
|
| 78 |
+
"image": "https://eoimages.gsfc.nasa.gov/images/imagerecords/82000/82296/Lluta_ali_2012201_lrg.jpg",
|
| 79 |
+
"source": "https://earthobservatory.nasa.gov/images/82296/lluta-river-chile",
|
| 80 |
+
"sensor": "EO-1 ALI · natural color · 2012-07-19",
|
| 81 |
+
"focus": "Inspect classification ambiguity where narrow irrigated valleys cross dominant bare land.",
|
| 82 |
+
},
|
| 83 |
+
"zambezi": {
|
| 84 |
+
"title": "Zambezi River · wet-season floodplain",
|
| 85 |
+
"image": "https://eoimages.gsfc.nasa.gov/images/imagerecords/80000/80835/zambezi_ali_2013090_lrg.jpg",
|
| 86 |
+
"source": "https://earthobservatory.nasa.gov/images/80835/wet-season-transforms-the-zambezi-river",
|
| 87 |
+
"sensor": "EO-1 ALI · 2013-03-31",
|
| 88 |
+
"focus": "Evaluate water, vegetation, and bare-land composition across a seasonal floodplain.",
|
| 89 |
+
},
|
| 90 |
+
}
|
| 91 |
|
| 92 |
|
| 93 |
def _device() -> torch.device:
|
|
|
|
| 132 |
return resize_for_inference(image)
|
| 133 |
|
| 134 |
|
| 135 |
+
def load_case_study(case_key: str):
|
| 136 |
+
"""Load a documented NASA case into the shared image input."""
|
| 137 |
+
case = CASE_STUDIES[case_key]
|
| 138 |
+
note = (
|
| 139 |
+
f"### {case['title']}\n"
|
| 140 |
+
f"**Acquisition:** {case['sensor']} \n"
|
| 141 |
+
f"**Suggested analysis:** {case['focus']} \n"
|
| 142 |
+
f"[Open NASA Earth Observatory source]({case['source']}) · "
|
| 143 |
+
"Reference imagery is provided for method exploration; model outputs are not ground truth."
|
| 144 |
+
)
|
| 145 |
+
return case["image"], note
|
| 146 |
+
|
| 147 |
+
|
| 148 |
def _classify_impl(prepared: Image.Image, top_k: int, output_dir: Path) -> dict[str, object]:
|
| 149 |
processor, model, id2label, device = load_classifier()
|
| 150 |
inputs = {name: tensor.to(device) for name, tensor in processor(images=prepared, return_tensors="pt").items()}
|
|
|
|
| 389 |
.prob-row {display:grid;grid-template-columns:155px 1fr 62px;gap:10px;align-items:center;margin:8px 0;font-size:.86rem;}
|
| 390 |
.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;}
|
| 391 |
.section-note {padding:12px 14px;border-left:4px solid #15947a;background:#eef9f6;border-radius:8px;color:#315c62;}
|
| 392 |
+
.case-panel {padding:16px 18px;border:1px solid #dce6ed;border-radius:16px;background:#fff;margin:8px 0 14px}.case-panel h3{margin:0 0 4px;color:#123c49}.case-panel p{margin:0;color:#647984}
|
| 393 |
@media(max-width:850px){.pipeline,.summary-grid{grid-template-columns:1fr}.prob-row{grid-template-columns:115px 1fr 56px}}
|
| 394 |
"""
|
| 395 |
|
|
|
|
| 408 |
<div><b>03 · Object detection</b><span>Bounding boxes and inventory-style summaries for 10 VHR object types.</span></div>
|
| 409 |
</div>
|
| 410 |
""")
|
| 411 |
+
gr.HTML("<div class='case-panel'><h3>Guided case studies</h3><p>Load a documented NASA scene, review the analytical question, then run the complete assessment or an individual model.</p></div>")
|
| 412 |
+
with gr.Row():
|
| 413 |
+
indus_case = gr.Button("🌾 Indus agriculture")
|
| 414 |
+
lluta_case = gr.Button("🏜️ Lluta desert valley")
|
| 415 |
+
zambezi_case = gr.Button("🌊 Zambezi floodplain")
|
| 416 |
+
case_note = gr.Markdown("Select a case study to load its image and methodological prompt.")
|
| 417 |
with gr.Row(equal_height=True):
|
| 418 |
image_input = gr.Image(type="pil", label="Satellite / aerial RGB image", height=430)
|
| 419 |
with gr.Column():
|
|
|
|
| 496 |
**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.
|
| 497 |
""")
|
| 498 |
|
| 499 |
+
indus_case.click(
|
| 500 |
+
lambda: load_case_study("indus"),
|
| 501 |
+
outputs=[image_input, case_note],
|
| 502 |
+
)
|
| 503 |
+
lluta_case.click(
|
| 504 |
+
lambda: load_case_study("lluta"),
|
| 505 |
+
outputs=[image_input, case_note],
|
| 506 |
+
)
|
| 507 |
+
zambezi_case.click(
|
| 508 |
+
lambda: load_case_study("zambezi"),
|
| 509 |
+
outputs=[image_input, case_note],
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
classify_button.click(
|
| 513 |
classify_lulc,
|
| 514 |
inputs=[image_input, top_k],
|
tests/test_app_contract.py
CHANGED
|
@@ -18,3 +18,7 @@ def test_app_exposes_all_api_endpoints_and_models():
|
|
| 18 |
assert "mfaytin/mask2former-satellite" in constants
|
| 19 |
assert "bluelabel/satellite-equipment-detection-yolov8n-vhr10" in constants
|
| 20 |
assert "cropland" in constants
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
assert "mfaytin/mask2former-satellite" in constants
|
| 19 |
assert "bluelabel/satellite-equipment-detection-yolov8n-vhr10" in constants
|
| 20 |
assert "cropland" in constants
|
| 21 |
+
assert "indus" in constants
|
| 22 |
+
assert "lluta" in constants
|
| 23 |
+
assert "zambezi" in constants
|
| 24 |
+
assert "Guided case studies" in source
|