prateeksharmacoder commited on
Commit
4b76f6f
·
1 Parent(s): 20598ce

Update TerraVision application and documentation

Browse files

Commit the modified Streamlit application and expanded project documentation.

Files changed (2) hide show
  1. README.md +716 -44
  2. app.py +313 -268
README.md CHANGED
@@ -1,70 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: TerraVision
3
- emoji: 🌍
4
- colorFrom: green
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: "5.33.0"
8
- app_file: app.py
9
- pinned: false
10
- license: mit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- # 🌍 TerraVision — Sentinel-2 Super Resolution & Analysis
 
 
 
 
14
 
15
- **Sharper Earth. Brighter Decisions.**
 
 
 
16
 
17
- TerraVision performs 4× satellite super-resolution on Sentinel-2 L2A imagery
18
- using the ESA **LDSR-S2** latent diffusion model and **SEN2SR**, upscaling from
19
- 10 m to 2.5 m spatial resolution across all 10 bands.
 
 
20
 
21
  ---
22
 
23
- ## How to Use
24
 
25
- 1. **Click on the map** to select any location on Earth, or type coordinates manually.
26
- 2. Set a **date range** (a few days is enough; the pipeline picks the first available image).
27
- 3. Click **🚀 Generate Super-Resolution**.
28
- 4. Explore the analysis layers and download the georeferenced GeoTIFF ZIP.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  ---
31
 
32
- ## Output Layers
 
 
33
 
34
- | Layer | Description |
35
- |---|---|
36
- | **RGB** | True-colour super-resolved composite |
37
- | **False Color / NIR** | NIR–Red–Green (vegetation highlighting) |
38
- | **SWIR Composite** | SWIR2–SWIR1–Red (geology & burn scars) |
39
- | **NDVI** | Normalised Difference Vegetation Index |
40
- | **NDWI** | Normalised Difference Water Index |
41
- | **NDBI** | Normalised Difference Built-up Index |
42
- | **NBR** | Normalised Burn Ratio |
43
- | **LDSR-S2 Uncertainty** | Pixel-wise model uncertainty from the diffusion ensemble |
44
 
45
- All layers are exported as **georeferenced GeoTIFF** files (EPSG as acquired)
46
- and bundled into a single ZIP download.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  ---
49
 
50
- ## Models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- - **LDSR-S2** — ESA / tacofoundation latent diffusion SR for RGB+NIR bands
53
- Weights: [`tacofoundation/RS-SR-LTDF`](https://huggingface.co/tacofoundation/RS-SR-LTDF)
54
- - **SEN2SR** — Swin Transformer SR for 20 m Sentinel-2 bands guided by LDSR-S2 output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  ---
57
 
58
- ## Technical Details
 
 
 
 
 
 
59
 
60
- - Input: 10-band Sentinel-2 L2A at 128 × 128 pixels (10 m native resolution)
61
- - Output: 10-band SR product at 512 × 512 pixels (2.5 m effective resolution)
62
- - Uncertainty: 5-sample diffusion ensemble (`n_variations=5`, `sampling_steps=50`)
63
- - Data source: [cubo](https://github.com/ESDS-Leipzig/cubo) → Microsoft Planetary Computer STAC
 
 
 
64
 
65
  ---
66
 
67
- ## Acknowledgements
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- Built on top of [OpenSR](https://github.com/ESA-PhiLab/OpenSR) and
70
- [SEN2SR](https://github.com/ESA-PhiLab/SEN2SR) by ESA Phi-Lab / ESDS Leipzig.
 
1
+
2
+ # 🌍 TerraVision
3
+
4
+ ### Sharper Earth. Brighter Decisions.
5
+
6
+ **TerraVision** is a GIS-based satellite imagery analysis platform that uses **Sentinel-2 satellite data** and the official **ESA OpenSR LDSR-S2 + SEN2SR super-resolution pipeline** to generate model-based **2.5 m resolution imagery** from freely available Sentinel-2 data.
7
+
8
+ The platform combines satellite image super-resolution, geospatial processing, spectral analysis, visualization, and GeoTIFF export into a single web-based workflow.
9
+
10
+ ---
11
+
12
+ ## 🚀 Overview
13
+
14
+ Sentinel-2 provides freely available multispectral imagery, but its spatial resolution can be limiting for detailed geospatial analysis.
15
+
16
+ TerraVision addresses this challenge by:
17
+
18
+ 1. Selecting a location using latitude/longitude or an interactive map.
19
+ 2. Selecting a date range for Sentinel-2 imagery.
20
+ 3. Retrieving Sentinel-2 L2A data.
21
+ 4. Applying the **ESA LDSR-S2 + SEN2SR** super-resolution pipeline.
22
+ 5. Generating a model-based **2.5 m resolution, 10-band output**.
23
+ 6. Producing multiple GIS analysis layers.
24
+ 7. Providing an original-vs-super-resolved comparison.
25
+ 8. Exporting georeferenced GeoTIFF files and a ZIP containing all outputs.
26
+
27
+ > **Important:** The 2.5 m imagery generated by TerraVision is a model-based reconstruction. It is not native 2.5 m satellite observation.
28
+
29
+ ---
30
+
31
+ # ✨ Key Features
32
+
33
+ ### 🛰️ Sentinel-2 Data
34
+ - Sentinel-2 L2A imagery
35
+ - 10 spectral bands
36
+ - User-defined geographical location
37
+ - User-defined date range
38
+
39
+ ### 🔬 AI-Based Super Resolution
40
+ - ESA OpenSR **LDSR-S2 + SEN2SR**
41
+ - 10 m input
42
+ - Model-generated 2.5 m output
43
+ - 4× spatial upscaling
44
+ - 10-band super-resolved output
45
+
46
+ ### 🗺️ GIS Visualization
47
+ TerraVision generates:
48
+
49
+ - Super-Resolved RGB
50
+ - Original Sentinel-2 RGB
51
+ - False Color / Infrared
52
+ - SWIR Composite
53
+ - NDVI
54
+ - NDWI
55
+ - NDBI
56
+ - NBR
57
+ - LDSR-S2 Uncertainty
58
+
59
+ ### 📊 Image Comparison
60
+ An interactive before/after comparison allows users to visually compare:
61
+
62
+ **Original Sentinel-2 → Super-Resolved Output**
63
+
64
+ ### 💾 Geospatial Export
65
+ Users can download:
66
+
67
+ - Individual GeoTIFF layers
68
+ - Complete ZIP package containing all generated layers
69
+
70
+ All exported layers preserve their geospatial reference information.
71
+
72
+ ---
73
+
74
+ # 🧠 Super-Resolution Pipeline
75
+
76
+ TerraVision uses the official ESA OpenSR approach combining:
77
+
78
+ ```text
79
+ Sentinel-2 L2A
80
+ │
81
+ ▼
82
+ Data Retrieval
83
+ │
84
+ ▼
85
+ 10-band Sentinel-2 Input
86
+ │
87
+ ▼
88
+ LDSR-S2 + SEN2SR
89
+ │
90
+ ▼
91
+ 2.5 m Super-Resolved Output
92
+ │
93
+ ├── B02
94
+ ├── B03
95
+ ├── B04
96
+ ├── B05
97
+ ├── B06
98
+ ├── B07
99
+ ├── B08
100
+ ├── B8A
101
+ ├── B11
102
+ └── B12
103
+ │
104
+ ▼
105
+ GIS Analysis Layers
106
+ ````
107
+
108
+ The pipeline combines the strengths of:
109
+
110
+ ### LDSR-S2
111
+
112
+ Latent diffusion super-resolution for Sentinel-2 RGB-NIR information.
113
+
114
+ ### SEN2SR
115
+
116
+ Super-resolution processing for the additional Sentinel-2 spectral bands.
117
+
118
+ Together, they provide the 10-band super-resolved output used by TerraVision.
119
+
120
+ ---
121
+
122
+ # 🛰️ Sentinel-2 Bands
123
+
124
+ | Band | Description | Resolution |
125
+ | ---- | ------------------- | ---------- |
126
+ | B02 | Blue | 10 m |
127
+ | B03 | Green | 10 m |
128
+ | B04 | Red | 10 m |
129
+ | B05 | Vegetation Red Edge | 20 m |
130
+ | B06 | Vegetation Red Edge | 20 m |
131
+ | B07 | Vegetation Red Edge | 20 m |
132
+ | B08 | NIR | 10 m |
133
+ | B8A | Narrow NIR | 20 m |
134
+ | B11 | SWIR 1 | 20 m |
135
+ | B12 | SWIR 2 | 20 m |
136
+
137
+ The super-resolution pipeline produces these bands at the model-generated 2.5 m output scale.
138
+
139
+ ---
140
+
141
+ # 📊 Spectral Analysis
142
+
143
+ TerraVision converts the super-resolved bands into several commonly used remote-sensing indices.
144
+
145
+ ## 🌱 NDVI
146
+
147
+ **Normalized Difference Vegetation Index**
148
+
149
+ ```text
150
+ NDVI = (B08 - B04) / (B08 + B04)
151
+ ```
152
+
153
+ Used to analyze vegetation density and vegetation condition.
154
+
155
+ ---
156
+
157
+ ## 💧 NDWI
158
+
159
+ TerraVision uses the Green-NIR formulation:
160
+
161
+ ```text
162
+ NDWI = (B03 - B08) / (B03 + B08)
163
+ ```
164
+
165
+ Used to highlight water-related features.
166
+
167
+ ---
168
+
169
+ ## 🏙️ NDBI
170
+
171
+ **Normalized Difference Built-up Index**
172
+
173
+ ```text
174
+ NDBI = (B11 - B08) / (B11 + B08)
175
+ ```
176
+
177
+ Used for identifying built-up and urban areas.
178
+
179
+ ---
180
+
181
+ ## 🔥 NBR
182
+
183
+ **Normalized Burn Ratio**
184
+
185
+ ```text
186
+ NBR = (B08 - B12) / (B08 + B12)
187
+ ```
188
+
189
+ Useful for analyzing burn-affected areas and vegetation disturbance.
190
+
191
  ---
192
+
193
+ # 🎨 Visualization Layers
194
+
195
+ TerraVision provides several visualization products.
196
+
197
+ ### RGB Composite
198
+
199
+ ```text
200
+ R = B04
201
+ G = B03
202
+ B = B02
203
+ ```
204
+
205
+ Provides a natural-color representation.
206
+
207
+ ### False Color Composite
208
+
209
+ ```text
210
+ R = B08
211
+ G = B04
212
+ B = B03
213
+ ```
214
+
215
+ Useful for vegetation analysis.
216
+
217
+ ### SWIR Composite
218
+
219
+ ```text
220
+ R = B12
221
+ G = B11
222
+ B = B04
223
+ ```
224
+
225
+ Useful for analyzing moisture, soil, built-up areas and burned regions.
226
+
227
  ---
228
 
229
+ # 📈 Uncertainty Map
230
+
231
+ TerraVision also exposes the uncertainty estimation available from the underlying **LDSR-S2 RGB-NIR component**.
232
+
233
+ The uncertainty map highlights areas where the super-resolution model has greater variation across generated samples.
234
 
235
+ ```text
236
+ Dark → Lower estimated uncertainty
237
+ Bright → Higher estimated uncertainty
238
+ ```
239
 
240
+ ### Important limitation
241
+
242
+ This should **not** be interpreted as a calibrated confidence score for the complete 10-band output.
243
+
244
+ The current implementation calculates uncertainty from the underlying **4-band LDSR-S2 RGB-NIR model**.
245
 
246
  ---
247
 
248
+ # 🗺️ TerraVision Workflow
249
 
250
+ ```text
251
+ User
252
+ │
253
+ ├── Select Location
254
+ │ └── Latitude / Longitude
255
+ │
256
+ ├── Select Date Range
257
+ │
258
+ ▼
259
+ Sentinel-2 L2A Data
260
+ │
261
+ ▼
262
+ Preprocessing
263
+ │
264
+ ▼
265
+ ESA LDSR-S2 + SEN2SR
266
+ │
267
+ ▼
268
+ 2.5 m Super-Resolved Imagery
269
+ │
270
+ ├── RGB
271
+ ├── False Color
272
+ ├── SWIR
273
+ ├── NDVI
274
+ ├── NDWI
275
+ ├── NDBI
276
+ ├── NBR
277
+ └── Uncertainty
278
+ │
279
+ ▼
280
+ Interactive Visualization
281
+ │
282
+ ▼
283
+ GeoTIFF / ZIP Export
284
+ ```
285
 
286
  ---
287
 
288
+ # 🛠️ Technology Stack
289
+
290
+ ## Frontend / UI
291
 
292
+ * Python
293
+ * Gradio
294
+ * Interactive visualization
295
+ * Image comparison slider
296
+ * GIS map integration
 
 
 
 
 
297
 
298
+ ## Machine Learning
299
+
300
+ * PyTorch
301
+ * ESA OpenSR
302
+ * LDSR-S2
303
+ * SEN2SR
304
+
305
+ ## Geospatial Processing
306
+
307
+ * Rasterio
308
+ * Rioxarray
309
+ * GeoPandas
310
+ * PyProj
311
+ * Xarray
312
+ * Dask
313
+ * Cubo
314
+
315
+ ## Satellite Data
316
+
317
+ * Sentinel-2 L2A
318
+ * STAC-based data access
319
+
320
+ ## Model / Data Management
321
+
322
+ * MLSTAC
323
+ * Hugging Face ecosystem
324
+
325
+ ## Deployment
326
+
327
+ * Hugging Face Spaces
328
+ * Gradio
329
+ * ZeroGPU-compatible architecture
330
 
331
  ---
332
 
333
+ # 📦 Installation
334
+
335
+ Clone the repository:
336
+
337
+ ```bash
338
+ git clone https://github.com/prateeksharmacoder-sys/satellite-LDRS-SEN2SR.git
339
+ cd TerraVision
340
+ ```
341
+
342
+ Create a virtual environment:
343
+
344
+ ```bash
345
+ python -m venv venv
346
+ ```
347
+
348
+ Activate it.
349
 
350
+ ### Windows
351
+
352
+ ```bash
353
+ venv\Scripts\activate
354
+ ```
355
+
356
+ ### Linux / macOS
357
+
358
+ ```bash
359
+ source venv/bin/activate
360
+ ```
361
+
362
+ Install dependencies:
363
+
364
+ ```bash
365
+ pip install -r requirements.txt
366
+ ```
367
 
368
  ---
369
 
370
+ # ▶️ Running the Application
371
+
372
+ Run:
373
+
374
+ ```bash
375
+ python app.py
376
+ ```
377
 
378
+ The Gradio interface will provide a local web address.
379
+
380
+ For development, the application can also be launched with:
381
+
382
+ ```python
383
+ demo.launch()
384
+ ```
385
 
386
  ---
387
 
388
+ # 📁 Project Structure
389
+
390
+ ```text
391
+ TerraVision/
392
+ │
393
+ ├── app.py
394
+ ├── requirements.txt
395
+ ├── README.md
396
+ │
397
+ ├── models/
398
+ │ └── model configuration / model assets
399
+ │
400
+ ├── utils/
401
+ │ ├── preprocessing.py
402
+ │ ├── visualization.py
403
+ │ ├── geotiff.py
404
+ │ └── analysis.py
405
+ │
406
+ ├── outputs/
407
+ │ └── generated products
408
+ │
409
+ └── assets/
410
+ └── UI images / project assets
411
+ ```
412
+
413
+ > The exact structure may vary depending on the final deployment version.
414
+
415
+ ---
416
+
417
+ # 🔬 Technical Implementation
418
+
419
+ ### 1. Data Retrieval
420
+
421
+ TerraVision retrieves Sentinel-2 L2A imagery using a STAC-based workflow.
422
+
423
+ The application requests:
424
+
425
+ ```text
426
+ B02
427
+ B03
428
+ B04
429
+ B05
430
+ B06
431
+ B07
432
+ B08
433
+ B8A
434
+ B11
435
+ B12
436
+ ```
437
+
438
+ ---
439
+
440
+ ### 2. Preprocessing
441
+
442
+ Sentinel-2 reflectance values are converted into normalized floating-point values before inference.
443
+
444
+ ```python
445
+ low_resolution = low_resolution / 10000
446
+ ```
447
+
448
+ ---
449
+
450
+ ### 3. Super Resolution
451
+
452
+ The combined model is loaded and executed using SEN2SR:
453
+
454
+ ```python
455
+ super_resolution = sen2sr.predict_large(
456
+ model=model,
457
+ X=low_resolution,
458
+ overlap=16
459
+ )
460
+ ```
461
+
462
+ The model converts:
463
+
464
+ ```text
465
+ 10 × 128 × 128
466
+ ```
467
+
468
+ into approximately:
469
+
470
+ ```text
471
+ 10 × 512 × 512
472
+ ```
473
+
474
+ ---
475
+
476
+ ### 4. Spectral Products
477
+
478
+ The generated bands are used to calculate spectral indices such as:
479
+
480
+ ```python
481
+ NDVI = (B08 - B04) / (B08 + B04)
482
+ NDWI = (B03 - B08) / (B03 + B08)
483
+ NDBI = (B11 - B08) / (B11 + B08)
484
+ NBR = (B08 - B12) / (B08 + B12)
485
+ ```
486
+
487
+ ---
488
+
489
+ ### 5. Geospatial Export
490
+
491
+ Generated products are exported as GeoTIFF files while preserving:
492
+
493
+ * CRS
494
+ * Spatial transform
495
+ * Resolution
496
+ * Geographic bounds
497
+
498
+ The prototype output was verified at:
499
+
500
+ ```text
501
+ Resolution: 2.5 m
502
+ CRS: EPSG:32630
503
+ Output size: 512 × 512
504
+ ```
505
+
506
+ The exact CRS changes according to the selected geographic location.
507
+
508
+ ---
509
+
510
+ # 📥 Output Files
511
+
512
+ A typical output package contains:
513
+
514
+ ```text
515
+ terravision_layers.zip
516
+ │
517
+ ├── sr_rgb.tif
518
+ ├── false_color.tif
519
+ ├── swir.tif
520
+ ├── ndvi.tif
521
+ ├── ndwi.tif
522
+ ├── ndbi.tif
523
+ ├── nbr.tif
524
+ └── uncertainty.tif
525
+ ```
526
+
527
+ ---
528
+
529
+ # ⚡ Performance
530
+
531
+ The current prototype has been successfully tested end-to-end with:
532
+
533
+ ```text
534
+ Sentinel-2 input: 10 × 128 × 128
535
+ Super-resolution: 10 × 512 × 512
536
+ Output resolution: 2.5 m
537
+ Analysis layers: 8
538
+ GeoTIFF export: Successful
539
+ ZIP generation: Successful
540
+ ```
541
+
542
+ The super-resolution inference is the computationally expensive part of the pipeline.
543
+
544
+ Uncertainty estimation requires additional inference and is therefore treated as an optional computational component for deployment optimization.
545
+
546
+ ---
547
+
548
+ # ⚠️ Limitations
549
+
550
+ TerraVision currently has several limitations.
551
+
552
+ ### 1. Model-Based Resolution
553
+
554
+ The 2.5 m output is reconstructed by an AI model and is not equivalent to native 2.5 m satellite imagery.
555
+
556
+ ### 2. Reconstruction Errors
557
+
558
+ Super-resolution models can introduce artifacts or reconstruct details that are not directly observed in the original imagery.
559
+
560
+ ### 3. Cloud and Atmospheric Effects
561
+
562
+ Clouds, haze and poor-quality Sentinel-2 observations can affect the output.
563
+
564
+ ### 4. Scientific Validation
565
+
566
+ The current prototype has been technically validated for:
567
+
568
+ * successful model inference
569
+ * correct output dimensions
570
+ * spectral layer generation
571
+ * GeoTIFF generation
572
+ * geospatial referencing
573
+
574
+ A comprehensive validation against independent high-resolution reference imagery using metrics such as **PSNR, SSIM and spectral consistency** remains future work.
575
+
576
+ ### 5. Uncertainty
577
+
578
+ The current uncertainty output represents the LDSR-S2 RGB-NIR component rather than a calibrated uncertainty estimate for all 10 bands.
579
+
580
+ ### 6. Temporal Analysis
581
+
582
+ Change detection such as **dNBR** requires pre-event and post-event imagery and is not part of the current single-image workflow.
583
+
584
+ ---
585
+
586
+ # 🔮 Future Scope
587
+
588
+ Potential future improvements include:
589
+
590
+ * Multi-date satellite analysis
591
+ * Automated change detection
592
+ * dNDVI / dNBR analysis
593
+ * Cloud masking
594
+ * Larger-area processing
595
+ * Batch processing
596
+ * More advanced GIS layers
597
+ * Scientific validation using higher-resolution reference imagery
598
+ * Performance optimization
599
+ * GPU acceleration
600
+ * Caching of satellite data
601
+ * Production-scale deployment
602
+ * User accounts and project history
603
+ * Export to additional GIS formats
604
+
605
+ ---
606
+
607
+ # 🌐 Deployment
608
+
609
+ TerraVision is designed to be deployed as a Gradio application.
610
+
611
+ The planned deployment architecture is:
612
+
613
+ ```text
614
+ User
615
+ │
616
+ ▼
617
+ TerraVision Web Interface
618
+ │
619
+ ▼
620
+ Gradio Application
621
+ │
622
+ ├── Satellite Data Retrieval
623
+ │
624
+ ├── GPU Inference
625
+ │
626
+ ├── Spectral Analysis
627
+ │
628
+ └── GeoTIFF Generation
629
+ │
630
+ ▼
631
+ Results + Downloads
632
+ ```
633
+
634
+ The application can be adapted for GPU-backed deployment using **Hugging Face Spaces ZeroGPU**.
635
+
636
+ ---
637
+
638
+ # 🧪 Validation Status
639
+
640
+ | Component | Status |
641
+ | ------------------------------- | -------------- |
642
+ | Sentinel-2 data retrieval | ✅ Tested |
643
+ | 10-band input generation | ✅ Tested |
644
+ | LDSR-S2 + SEN2SR inference | ✅ Tested |
645
+ | 2.5 m output generation | ✅ Tested |
646
+ | RGB generation | ✅ Tested |
647
+ | False Color | ✅ Tested |
648
+ | SWIR | ✅ Tested |
649
+ | NDVI | ✅ Tested |
650
+ | NDWI | ✅ Tested |
651
+ | NDBI | ✅ Tested |
652
+ | NBR | ✅ Tested |
653
+ | LDSR-S2 uncertainty | ✅ Tested |
654
+ | GeoTIFF export | ✅ Tested |
655
+ | Georeferencing | ✅ Verified |
656
+ | ZIP export | ✅ Tested |
657
+ | Interactive web UI | ✅ Prototype |
658
+ | Scientific benchmark validation | 🔄 Future work |
659
+ | Production-scale deployment | 🔄 Planned |
660
+
661
+ ---
662
+
663
+ # 🎯 Use Cases
664
+
665
+ TerraVision can support exploratory analysis in areas such as:
666
+
667
+ * 🌱 Vegetation monitoring
668
+ * 💧 Water-body analysis
669
+ * 🏙️ Urban expansion studies
670
+ * 🔥 Burn-area analysis
671
+ * 🌾 Agricultural monitoring
672
+ * 🛰️ Remote-sensing research
673
+ * 🗺️ GIS analysis
674
+ * 🌍 Environmental monitoring
675
+
676
+ ---
677
+
678
+ # 📚 Acknowledgements
679
+
680
+ TerraVision builds upon open-source satellite super-resolution research and software from the **European Space Agency (ESA) OpenSR project**, including LDSR-S2 and SEN2SR.
681
+
682
+ The project also uses open geospatial and satellite-data technologies including:
683
+
684
+ * Sentinel-2
685
+ * Cubo
686
+ * MLSTAC
687
+ * PyTorch
688
+ * Rasterio
689
+ * Xarray
690
+ * Gradio
691
+
692
+ ---
693
+
694
+ # 📜 Disclaimer
695
+
696
+ TerraVision is a research and demonstration project.
697
+
698
+ The generated 2.5 m imagery should not automatically be treated as equivalent to native high-resolution satellite imagery. Results may contain model reconstruction artifacts and should be independently validated before being used for critical scientific, commercial, legal, or operational decisions.
699
+
700
+ ---
701
+
702
+ # 👨‍💻 Project
703
+
704
+ **TerraVision**
705
+
706
+ > *Sharper Earth. Brighter Decisions.*
707
+
708
+ Built as a geospatial AI project for exploring the potential of satellite-image super-resolution and GIS analysis.
709
+
710
+ ---
711
+
712
+ ## ⭐ If you find TerraVision interesting
713
+
714
+ Consider giving the repository a ⭐ and exploring the implementation.
715
+
716
+ ````
717
+
718
+ ### Recommended GitHub repository files
719
+
720
+ For your current project, I would keep the repository clean like this:
721
+
722
+ ```text
723
+ TerraVision/
724
+ │
725
+ ├── README.md ← this file
726
+ ├── app.py ← main Gradio application
727
+ ├── requirements.txt
728
+ ├── .gitignore
729
+ │
730
+ ├── utils/
731
+ │ ├── satellite.py
732
+ │ ├── analysis.py
733
+ │ ├── visualization.py
734
+ │ └── geotiff.py
735
+ │
736
+ └── assets/
737
+ └── screenshots/
738
+ ````
739
+
740
+ **One important point:** don't upload the ~1.4 GB model file directly into GitHub. Your code can download/load the model from its Hugging Face model repository at runtime.
741
 
742
+ If you want, I can also create the **actual `README.md` file for you as a downloadable file**, ready to put directly into your GitHub repository.
 
app.py CHANGED
@@ -120,148 +120,264 @@ def save_tensor_as_geotiff(tensor, attrs, out_path, super_resolved=False, sr_fac
120
  DEFAULT_LAT = 39.39785676571274
121
  DEFAULT_LON = -0.3798517619438821
122
 
123
- # --------------------------------------------------------
124
- # Leaflet CDN — injected into <head> via gr.Blocks(head=).
125
- # This is the correct Gradio 5 pattern for loading external
126
- # JS/CSS libraries: gr.HTML strips <script> tags, but
127
- # gr.Blocks(head=) injects them into the real page <head>.
128
- # --------------------------------------------------------
129
- LEAFLET_HEAD = """
130
- <link rel="stylesheet"
131
- href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
132
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
133
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
- # --------------------------------------------------------
136
- # Map container — just the div.
137
- # Leaflet and the init script are handled separately.
138
- # --------------------------------------------------------
139
- MAP_HTML_VALUE = '<div id="terrativision-map"></div>'
140
-
141
- # --------------------------------------------------------
142
- # Map init JavaScript — passed to gr.Blocks(js=).
143
- # Must be a function string. Uses retry loop because
144
- # Gradio renders the DOM asynchronously.
145
- # --------------------------------------------------------
146
- MAP_JS = f"""
147
- () => {{
148
- function tryInitTerraVisionMap() {{
149
-
150
- // Wait for Leaflet library to load
151
- if (typeof L === "undefined") {{
152
- setTimeout(tryInitTerraVisionMap, 300);
153
- return;
154
- }}
155
-
156
- const mapDiv = document.getElementById("terrativision-map");
157
-
158
- // Wait for the map container div to appear in the DOM
159
- if (!mapDiv) {{
160
- setTimeout(tryInitTerraVisionMap, 300);
161
- return;
162
- }}
163
-
164
- // Prevent double-initialisation
165
- if (mapDiv._terraVisionMapInitialized) return;
166
- mapDiv._terraVisionMapInitialized = true;
167
-
168
- const defaultLat = {DEFAULT_LAT};
169
- const defaultLon = {DEFAULT_LON};
170
-
171
- // ------------------------------------------------
172
- // CREATE MAP
173
- // ------------------------------------------------
174
- const map = L.map(mapDiv).setView([defaultLat, defaultLon], 10);
175
-
176
- // ------------------------------------------------
177
- // STREET MAP
178
- // ------------------------------------------------
179
- const osmLayer = L.tileLayer(
180
- "https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png",
181
- {{ maxZoom: 19, attribution: "&copy; OpenStreetMap contributors" }}
182
- ).addTo(map);
183
-
184
- // ------------------------------------------------
185
- // SATELLITE MAP
186
- // ------------------------------------------------
187
- const satelliteLayer = L.tileLayer(
188
- "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{{z}}/{{y}}/{{x}}",
189
- {{ maxZoom: 19, attribution: "Tiles &copy; Esri" }}
190
  );
191
 
192
- // ------------------------------------------------
193
- // LAYER CONTROL
194
- // ------------------------------------------------
195
- L.control.layers(
196
- {{ "Street Map": osmLayer, "Satellite": satelliteLayer }}
197
- ).addTo(map);
198
-
199
- // ------------------------------------------------
200
- // MARKER
201
- // ------------------------------------------------
202
- let marker = L.marker([defaultLat, defaultLon]).addTo(map);
203
-
204
- // ------------------------------------------------
205
- // LOCATION DISPLAY
206
- // ------------------------------------------------
207
- const locationDisplay = document.querySelector("#map-location-display");
208
-
209
- function updateLocationDisplay(lat, lon) {{
210
- if (locationDisplay) {{
211
- locationDisplay.innerHTML =
212
- "📍 <b>Selected:</b> " + lat.toFixed(8) + ", " + lon.toFixed(8);
213
- }}
214
- }}
215
-
216
- updateLocationDisplay(defaultLat, defaultLon);
217
  marker.bindPopup(
218
  "<b>Selected Location</b><br>" +
219
- defaultLat.toFixed(8) + ", " + defaultLon.toFixed(8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
- // ------------------------------------------------
223
- // MAP CLICK
224
- // ------------------------------------------------
225
- map.on("click", function(e) {{
226
- const lat = e.latlng.lat;
227
- const lon = e.latlng.lng;
228
-
229
- marker.setLatLng([lat, lon]);
230
- marker.bindPopup(
231
- "<b>Selected Location</b><br>" + lat.toFixed(8) + ", " + lon.toFixed(8)
232
- ).openPopup();
233
-
234
- updateLocationDisplay(lat, lon);
235
-
236
- // Update Gradio Number inputs
237
- function updateGradioNumber(elemId, value) {{
238
- const container = document.getElementById(elemId);
239
- if (!container) return;
240
- const input = container.querySelector("input");
241
- if (!input) return;
242
- const nativeSetter = Object.getOwnPropertyDescriptor(
243
- HTMLInputElement.prototype, "value"
244
- ).set;
245
- nativeSetter.call(input, String(value));
246
- input.dispatchEvent(new Event("input", {{ bubbles: true }}));
247
- input.dispatchEvent(new Event("change", {{ bubbles: true }}));
248
- }}
249
-
250
- updateGradioNumber("latitude_input", lat);
251
- updateGradioNumber("longitude_input", lon);
252
- }});
253
-
254
- // Fix map tile rendering after Gradio layout settles
255
- setTimeout(function() {{ map.invalidateSize(); }}, 500);
256
-
257
- console.log("✅ TerraVision Leaflet map initialized successfully.");
258
- }}
259
-
260
- // Delay first attempt to let Gradio finish rendering
261
- setTimeout(tryInitTerraVisionMap, 800);
262
- }}
263
  """
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
 
267
  # ============================================================
@@ -384,74 +500,6 @@ def clean_uncertainty(image):
384
  return (image * 255.0).round().astype(np.uint8)
385
 
386
 
387
- # ============================================================
388
- # INDEX COLORIZATION
389
- # Converts a [-1, 1] spectral index to an RGB thematic map
390
- # using a linear interpolation between three anchor colours.
391
- # Uses only numpy (no matplotlib dependency needed).
392
- # ============================================================
393
-
394
- def _lerp_color(t, c0, c1):
395
- """Linearly interpolate between two RGB tuples, t in [0, 1]."""
396
- t = np.clip(t, 0.0, 1.0)[..., np.newaxis] # (..., 1)
397
- return c0 * (1.0 - t) + c1 * t # (..., 3)
398
-
399
-
400
- def colorize_index(index_arr, scheme):
401
- """
402
- Convert a 2-D spectral index array (values in [-1, 1]) to a
403
- uint8 RGB image using a three-stop colour ramp.
404
-
405
- scheme : one of 'ndvi' | 'ndwi' | 'ndbi'
406
-
407
- Colour stops (low / mid / high) in float RGB [0, 1]:
408
- ndvi : red (1,0,0) -> yellow (1,1,0) -> green (0,0.5,0)
409
- ndwi : brown(0.6,0.4,0.2) -> white (1,1,1) -> blue (0,0.3,1)
410
- ndbi : green(0,0.5,0) -> yellow (1,1,0) -> red (1,0,0)
411
- """
412
- arr = np.asarray(index_arr, dtype=np.float32)
413
- arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=-1.0)
414
- arr = np.clip(arr, -1.0, 1.0)
415
-
416
- # Map [-1, 1] -> [0, 1]
417
- t = (arr + 1.0) / 2.0 # 0 = low, 0.5 = mid, 1 = high
418
-
419
- COLOR_STOPS = {
420
- # (low_rgb, mid_rgb, high_rgb)
421
- "ndvi": (
422
- np.array([1.00, 0.00, 0.00]), # red
423
- np.array([1.00, 1.00, 0.00]), # yellow
424
- np.array([0.00, 0.50, 0.00]), # green
425
- ),
426
- "ndwi": (
427
- np.array([0.60, 0.40, 0.20]), # brown
428
- np.array([1.00, 1.00, 1.00]), # white
429
- np.array([0.00, 0.30, 1.00]), # blue
430
- ),
431
- "ndbi": (
432
- np.array([0.00, 0.50, 0.00]), # green
433
- np.array([1.00, 1.00, 0.00]), # yellow
434
- np.array([1.00, 0.00, 0.00]), # red
435
- ),
436
- }
437
-
438
- low_c, mid_c, high_c = COLOR_STOPS[scheme]
439
-
440
- # Two-segment ramp: [0, 0.5] -> low..mid, [0.5, 1] -> mid..high
441
- t_lo = np.clip(t / 0.5, 0.0, 1.0) # normalised within lower half
442
- t_hi = np.clip((t - 0.5) / 0.5, 0.0, 1.0) # normalised within upper half
443
-
444
- rgb_lo = _lerp_color(t_lo, low_c, mid_c) # (..., 3)
445
- rgb_hi = _lerp_color(t_hi, mid_c, high_c) # (..., 3)
446
-
447
- # Blend: use lower half for t < 0.5, upper half otherwise
448
- mask = (t >= 0.5)[..., np.newaxis]
449
- rgb = np.where(mask, rgb_hi, rgb_lo)
450
-
451
- rgb = np.clip(rgb, 0.0, 1.0)
452
- return (rgb * 255.0).round().astype(np.uint8)
453
-
454
-
455
  # ============================================================
456
  # MAIN PROCESSING PIPELINE
457
  # Called from inside @spaces.GPU so all GPU ops run there.
@@ -606,6 +654,8 @@ def generate_satellite_products(
606
 
607
  ndbi = (B11 - B08) / (B11 + B08 + eps)
608
 
 
 
609
  # --------------------------------------------------
610
  # 6. Uncertainty [GPU]
611
  # --------------------------------------------------
@@ -690,6 +740,11 @@ def generate_satellite_products(
690
  "false_color.tif"
691
  )
692
 
 
 
 
 
 
693
  paths["NDVI"] = save_layer(
694
  ndvi,
695
  "ndvi.tif"
@@ -705,12 +760,16 @@ def generate_satellite_products(
705
  "ndbi.tif"
706
  )
707
 
 
 
 
 
 
708
  paths["Uncertainty"] = save_layer(
709
  uncertainty_np,
710
  "uncertainty.tif"
711
  )
712
 
713
-
714
  # --------------------------------------------------
715
  # ZIP all GeoTIFFs
716
  # --------------------------------------------------
@@ -741,9 +800,11 @@ def generate_satellite_products(
741
  "rgb": rgb,
742
  "original_rgb": original_rgb,
743
  "false_color": false_color,
 
744
  "ndvi": ndvi,
745
  "ndwi": ndwi,
746
  "ndbi": ndbi,
 
747
  "uncertainty": uncertainty_np,
748
  "files": paths,
749
  "zip": zip_path
@@ -795,11 +856,15 @@ def run_app(latitude, longitude, start_date, end_date):
795
  # -------------------------------------------------
796
  false_color_img = clean_rgb(results["false_color"])
797
 
798
- # Colored thematic maps for UI display.
799
- # GeoTIFFs use the original numerical arrays (saved in pipeline).
800
- ndvi_colored = colorize_index(results["ndvi"], "ndvi")
801
- ndwi_colored = colorize_index(results["ndwi"], "ndwi")
802
- ndbi_colored = colorize_index(results["ndbi"], "ndbi")
 
 
 
 
803
 
804
  uncertainty_img = clean_uncertainty(
805
  results["uncertainty"]
@@ -811,21 +876,16 @@ def run_app(latitude, longitude, start_date, end_date):
811
  zip_file_path = results["zip"]
812
 
813
  # -------------------------------------------------
814
- # RETURN EXACTLY 7 OUTPUTS (matches UI components)
815
- # 1. comparison slider
816
- # 2. false color / NIR
817
- # 3. NDVI colored
818
- # 4. NDWI colored
819
- # 5. NDBI colored
820
- # 6. uncertainty
821
- # 7. ZIP download
822
  # -------------------------------------------------
823
  return (
824
  comparison_images,
825
  false_color_img,
826
- ndvi_colored,
827
- ndwi_colored,
828
- ndbi_colored,
 
 
829
  uncertainty_img,
830
  zip_file_path
831
  )
@@ -837,9 +897,7 @@ def run_app(latitude, longitude, start_date, end_date):
837
 
838
  with gr.Blocks(
839
  title="TerraVision — Sentinel-2 Super Resolution",
840
- css=custom_css,
841
- head=LEAFLET_HEAD, # Injects Leaflet CSS/JS into the page <head>
842
- js=MAP_JS # Runs map init after Gradio finishes rendering
843
  ) as demo:
844
 
845
  # ========================================================
@@ -1022,7 +1080,7 @@ with gr.Blocks(
1022
 
1023
 
1024
  # ------------------------------------------------
1025
- # FALSE COLOR
1026
  # ------------------------------------------------
1027
 
1028
  with gr.Row():
@@ -1033,65 +1091,50 @@ with gr.Blocks(
1033
  height=320
1034
  )
1035
 
 
 
 
 
 
 
1036
 
1037
  # ------------------------------------------------
1038
- # NDVI / NDWI / NDBI (colored thematic maps)
1039
  # ------------------------------------------------
1040
 
1041
  with gr.Row():
1042
 
1043
- with gr.Column():
1044
- ndvi_output = gr.Image(
1045
- label="NDVI",
1046
- type="numpy",
1047
- height=300
1048
- )
1049
- gr.HTML(
1050
- '<div style="display:flex;justify-content:space-between;'
1051
- 'font-size:11px;padding:2px 4px;">'
1052
- '<span style="color:#e00;">&#9632; Low vegetation</span>'
1053
- '<span style="color:#cc0;">&#9632; Medium</span>'
1054
- '<span style="color:#080;">&#9632; High vegetation</span>'
1055
- '</div>'
1056
- )
1057
-
1058
- with gr.Column():
1059
- ndwi_output = gr.Image(
1060
- label="NDWI",
1061
- type="numpy",
1062
- height=300
1063
- )
1064
- gr.HTML(
1065
- '<div style="display:flex;justify-content:space-between;'
1066
- 'font-size:11px;padding:2px 4px;">'
1067
- '<span style="color:#8B4513;">&#9632; Low water</span>'
1068
- '<span style="color:#aaa;">&#9632; Medium</span>'
1069
- '<span style="color:#04c;">&#9632; High water</span>'
1070
- '</div>'
1071
- )
1072
-
1073
- with gr.Column():
1074
- ndbi_output = gr.Image(
1075
- label="NDBI",
1076
- type="numpy",
1077
- height=300
1078
- )
1079
- gr.HTML(
1080
- '<div style="display:flex;justify-content:space-between;'
1081
- 'font-size:11px;padding:2px 4px;">'
1082
- '<span style="color:#080;">&#9632; Low built-up</span>'
1083
- '<span style="color:#cc0;">&#9632; Medium</span>'
1084
- '<span style="color:#e00;">&#9632; High built-up</span>'
1085
- '</div>'
1086
- )
1087
 
1088
 
1089
  # ------------------------------------------------
1090
- # UNCERTAINTY
1091
  # ------------------------------------------------
1092
 
1093
  with gr.Row():
1094
 
 
 
 
 
 
 
1095
  uncertainty_output = gr.Image(
1096
  label="LDSR-S2 Uncertainty",
1097
  type="numpy",
@@ -1128,9 +1171,11 @@ with gr.Blocks(
1128
  outputs=[
1129
  comparison_output,
1130
  false_color_output,
 
1131
  ndvi_output,
1132
  ndwi_output,
1133
  ndbi_output,
 
1134
  uncertainty_output,
1135
  zip_output
1136
  ]
 
120
  DEFAULT_LAT = 39.39785676571274
121
  DEFAULT_LON = -0.3798517619438821
122
 
123
+ # LEAFLET_HEAD removed — Gradio 5 gr.HTML does not support 'head'.
124
+ # Leaflet CSS/JS are inlined into MAP_HTML_VALUE below.
125
+
126
+
127
+ # ============================================================
128
+ # MAP JAVASCRIPT
129
+ # ============================================================
130
+
131
+ MAP_JS = """
132
+ // ============================================================
133
+ // TERRAVISION LEAFLET MAP
134
+ // ============================================================
135
+
136
+ const mapDiv = document.getElementById("terrativision-map");
137
+
138
+ if (!mapDiv) {
139
+
140
+ console.error("❌ TerraVision map container not found.");
141
+
142
+ } else if (mapDiv._terraVisionMapInitialized) {
143
+
144
+ console.log("ℹ️ TerraVision map already initialized.");
145
+
146
+ } else {
147
+
148
+ mapDiv._terraVisionMapInitialized = true;
149
+
150
+ const defaultLat = __DEFAULT_LAT__;
151
+ const defaultLon = __DEFAULT_LON__;
152
+
153
+ // --------------------------------------------------------
154
+ // CREATE MAP
155
+ // --------------------------------------------------------
156
+
157
+ const map = L.map(mapDiv).setView(
158
+ [defaultLat, defaultLon],
159
+ 10
160
+ );
161
+
162
+ // --------------------------------------------------------
163
+ // STREET MAP
164
+ // --------------------------------------------------------
165
+
166
+ const osmLayer = L.tileLayer(
167
+ "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
168
+ {
169
+ maxZoom: 19,
170
+ attribution: "&copy; OpenStreetMap contributors"
171
+ }
172
+ ).addTo(map);
173
+
174
+ // --------------------------------------------------------
175
+ // SATELLITE MAP
176
+ // --------------------------------------------------------
177
+
178
+ const satelliteLayer = L.tileLayer(
179
+ "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
180
+ {
181
+ maxZoom: 19,
182
+ attribution: "Tiles &copy; Esri"
183
+ }
184
+ );
185
+
186
+ // --------------------------------------------------------
187
+ // LAYER CONTROL
188
+ // --------------------------------------------------------
189
+
190
+ L.control.layers(
191
+ {
192
+ "Street Map": osmLayer,
193
+ "Satellite": satelliteLayer
194
+ }
195
+ ).addTo(map);
196
+
197
+ // --------------------------------------------------------
198
+ // MARKER
199
+ // --------------------------------------------------------
200
+
201
+ let marker = L.marker(
202
+ [defaultLat, defaultLon]
203
+ ).addTo(map);
204
+
205
+ // --------------------------------------------------------
206
+ // LOCATION DISPLAY
207
+ // --------------------------------------------------------
208
 
209
+ const locationDisplay =
210
+ document.querySelector("#map-location-display");
211
+
212
+ function updateLocationDisplay(lat, lon) {
213
+
214
+ if (locationDisplay) {
215
+
216
+ locationDisplay.innerHTML =
217
+ "📍 <b>Selected:</b> " +
218
+ lat.toFixed(8) +
219
+ ", " +
220
+ lon.toFixed(8);
221
+ }
222
+ }
223
+
224
+ updateLocationDisplay(
225
+ defaultLat,
226
+ defaultLon
227
+ );
228
+
229
+ marker.bindPopup(
230
+ "<b>Selected Location</b><br>" +
231
+ defaultLat.toFixed(8) +
232
+ ", " +
233
+ defaultLon.toFixed(8)
234
+ );
235
+
236
+ // --------------------------------------------------------
237
+ // MAP CLICK
238
+ // --------------------------------------------------------
239
+
240
+ map.on("click", function(e) {
241
+
242
+ const lat = e.latlng.lat;
243
+ const lon = e.latlng.lng;
244
+
245
+ console.log(
246
+ "📍 TerraVision selected:",
247
+ lat,
248
+ lon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  );
250
 
251
+ // Move marker
252
+ marker.setLatLng([lat, lon]);
253
+
254
+ // Update popup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  marker.bindPopup(
256
  "<b>Selected Location</b><br>" +
257
+ lat.toFixed(8) +
258
+ ", " +
259
+ lon.toFixed(8)
260
+ ).openPopup();
261
+
262
+ // Update visible selected text
263
+ updateLocationDisplay(lat, lon);
264
+
265
+ // --------------------------------------------------------
266
+ // UPDATE GRADIO LATITUDE / LONGITUDE
267
+ // --------------------------------------------------------
268
+
269
+ function updateGradioNumber(elemId, value) {
270
+
271
+ const container = document.getElementById(elemId);
272
+
273
+ if (!container) {
274
+ console.error(
275
+ "❌ Gradio component not found:",
276
+ elemId
277
  );
278
+ return;
279
+ }
280
+
281
+ const input = container.querySelector("input");
282
+
283
+ if (!input) {
284
+ console.error(
285
+ "❌ Input element not found:",
286
+ elemId
287
+ );
288
+ return;
289
+ }
290
+
291
+ // Use the native HTML value setter.
292
+ // This is important because Gradio uses a controlled input.
293
+ const nativeSetter =
294
+ Object.getOwnPropertyDescriptor(
295
+ HTMLInputElement.prototype,
296
+ "value"
297
+ ).set;
298
+
299
+ nativeSetter.call(
300
+ input,
301
+ String(value)
302
+ );
303
+
304
+ // Tell Gradio that the user changed the input
305
+ input.dispatchEvent(
306
+ new Event("input", {
307
+ bubbles: true
308
+ })
309
+ );
310
+
311
+ input.dispatchEvent(
312
+ new Event("change", {
313
+ bubbles: true
314
+ })
315
+ );
316
+
317
+ console.log(
318
+ "✅ Updated Gradio:",
319
+ elemId,
320
+ value
321
+ );
322
+ }
323
+
324
+ updateGradioNumber(
325
+ "latitude_input",
326
+ lat
327
+ );
328
+
329
+ updateGradioNumber(
330
+ "longitude_input",
331
+ lon
332
+ );
333
+
334
+ });
335
+
336
+ // --------------------------------------------------------
337
+ // FIX MAP SIZE
338
+ // --------------------------------------------------------
339
 
340
+ setTimeout(function() {
341
+
342
+ map.invalidateSize();
343
+
344
+ }, 500);
345
+
346
+ // Store map reference
347
+ element._terraVisionMap = map;
348
+
349
+ console.log(
350
+ "✅ TerraVision Leaflet map initialized successfully."
351
+ );
352
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  """
354
 
355
+ # Insert Python values without using an f-string
356
+ MAP_JS = MAP_JS.replace(
357
+ "__DEFAULT_LAT__",
358
+ str(DEFAULT_LAT)
359
+ ).replace(
360
+ "__DEFAULT_LON__",
361
+ str(DEFAULT_LON)
362
+ )
363
+
364
+ # ============================================================
365
+ # COMPLETE MAP HTML
366
+ # Gradio 5 gr.HTML has no 'head' or 'js_on_load' parameters.
367
+ # Inline Leaflet CSS, Leaflet JS, the map div, and the init
368
+ # script all inside the value string.
369
+ # ============================================================
370
+ MAP_HTML_VALUE = f"""
371
+ <link rel="stylesheet"
372
+ href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
373
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
374
+ <div id="terrativision-map"
375
+ style="width:100%;height:400px;border-radius:14px;
376
+ overflow:hidden;border:1px solid rgba(128,128,128,0.35);"></div>
377
+ <script>
378
+ {MAP_JS}
379
+ </script>
380
+ """
381
 
382
 
383
  # ============================================================
 
500
  return (image * 255.0).round().astype(np.uint8)
501
 
502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  # ============================================================
504
  # MAIN PROCESSING PIPELINE
505
  # Called from inside @spaces.GPU so all GPU ops run there.
 
654
 
655
  ndbi = (B11 - B08) / (B11 + B08 + eps)
656
 
657
+ nbr = (B08 - B12) / (B08 + B12 + eps)
658
+
659
  # --------------------------------------------------
660
  # 6. Uncertainty [GPU]
661
  # --------------------------------------------------
 
740
  "false_color.tif"
741
  )
742
 
743
+ paths["SWIR"] = save_layer(
744
+ swir,
745
+ "swir.tif"
746
+ )
747
+
748
  paths["NDVI"] = save_layer(
749
  ndvi,
750
  "ndvi.tif"
 
760
  "ndbi.tif"
761
  )
762
 
763
+ paths["NBR"] = save_layer(
764
+ nbr,
765
+ "nbr.tif"
766
+ )
767
+
768
  paths["Uncertainty"] = save_layer(
769
  uncertainty_np,
770
  "uncertainty.tif"
771
  )
772
 
 
773
  # --------------------------------------------------
774
  # ZIP all GeoTIFFs
775
  # --------------------------------------------------
 
800
  "rgb": rgb,
801
  "original_rgb": original_rgb,
802
  "false_color": false_color,
803
+ "swir": swir,
804
  "ndvi": ndvi,
805
  "ndwi": ndwi,
806
  "ndbi": ndbi,
807
+ "nbr": nbr,
808
  "uncertainty": uncertainty_np,
809
  "files": paths,
810
  "zip": zip_path
 
856
  # -------------------------------------------------
857
  false_color_img = clean_rgb(results["false_color"])
858
 
859
+ swir_img = clean_rgb(results["swir"])
860
+
861
+ ndvi_img = clean_index(results["ndvi"])
862
+
863
+ ndwi_img = clean_index(results["ndwi"])
864
+
865
+ ndbi_img = clean_index(results["ndbi"])
866
+
867
+ nbr_img = clean_index(results["nbr"])
868
 
869
  uncertainty_img = clean_uncertainty(
870
  results["uncertainty"]
 
876
  zip_file_path = results["zip"]
877
 
878
  # -------------------------------------------------
879
+ # RETURN EXACTLY 9 OUTPUTS
 
 
 
 
 
 
 
880
  # -------------------------------------------------
881
  return (
882
  comparison_images,
883
  false_color_img,
884
+ swir_img,
885
+ ndvi_img,
886
+ ndwi_img,
887
+ ndbi_img,
888
+ nbr_img,
889
  uncertainty_img,
890
  zip_file_path
891
  )
 
897
 
898
  with gr.Blocks(
899
  title="TerraVision — Sentinel-2 Super Resolution",
900
+ css=custom_css
 
 
901
  ) as demo:
902
 
903
  # ========================================================
 
1080
 
1081
 
1082
  # ------------------------------------------------
1083
+ # FALSE COLOR + SWIR
1084
  # ------------------------------------------------
1085
 
1086
  with gr.Row():
 
1091
  height=320
1092
  )
1093
 
1094
+ swir_output = gr.Image(
1095
+ label="SWIR Composite",
1096
+ type="numpy",
1097
+ height=320
1098
+ )
1099
+
1100
 
1101
  # ------------------------------------------------
1102
+ # NDVI / NDWI / NDBI
1103
  # ------------------------------------------------
1104
 
1105
  with gr.Row():
1106
 
1107
+ ndvi_output = gr.Image(
1108
+ label="NDVI",
1109
+ type="numpy",
1110
+ height=300
1111
+ )
1112
+
1113
+ ndwi_output = gr.Image(
1114
+ label="NDWI",
1115
+ type="numpy",
1116
+ height=300
1117
+ )
1118
+
1119
+ ndbi_output = gr.Image(
1120
+ label="NDBI",
1121
+ type="numpy",
1122
+ height=300
1123
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
 
1125
 
1126
  # ------------------------------------------------
1127
+ # NBR + UNCERTAINTY
1128
  # ------------------------------------------------
1129
 
1130
  with gr.Row():
1131
 
1132
+ nbr_output = gr.Image(
1133
+ label="NBR",
1134
+ type="numpy",
1135
+ height=320
1136
+ )
1137
+
1138
  uncertainty_output = gr.Image(
1139
  label="LDSR-S2 Uncertainty",
1140
  type="numpy",
 
1171
  outputs=[
1172
  comparison_output,
1173
  false_color_output,
1174
+ swir_output,
1175
  ndvi_output,
1176
  ndwi_output,
1177
  ndbi_output,
1178
+ nbr_output,
1179
  uncertainty_output,
1180
  zip_output
1181
  ]