Spaces:
Sleeping
Sleeping
Commit ·
a404eb8
1
Parent(s): cb7fde2
Add Gaussian smoothing and field boundary overlay
Browse filesFeatures:
- gaussian_sigma: Control smoothing strength (default 1.5, 0 = no smoothing)
- show_field_boundary: Toggle field boundary overlay (default true)
- Light dashed rectangle with corner markers
- Added scipy dependency for gaussian_filter
- app.py +55 -6
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -12,6 +12,7 @@ import base64
|
|
| 12 |
import logging
|
| 13 |
from datetime import datetime, timedelta
|
| 14 |
from typing import Optional
|
|
|
|
| 15 |
|
| 16 |
import numpy as np
|
| 17 |
import matplotlib
|
|
@@ -69,6 +70,8 @@ class HeatmapRequest(BaseModel):
|
|
| 69 |
center_lon: float
|
| 70 |
field_size_hectares: float = 10.0
|
| 71 |
index_type: str = "NDVI" # Any index from vegetation_indices.py
|
|
|
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
class HeatmapResponse(BaseModel):
|
|
@@ -145,8 +148,13 @@ def parse_timestamp(ts_str: str) -> datetime:
|
|
| 145 |
return datetime.fromisoformat(ts_str)
|
| 146 |
|
| 147 |
|
| 148 |
-
def generate_heatmap_image(
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
# Handle NaN values
|
| 152 |
valid_mask = ~np.isnan(data)
|
|
@@ -161,6 +169,10 @@ def generate_heatmap_image(data: np.ndarray, index_type: str) -> tuple:
|
|
| 161 |
data_normalized = np.clip((data - min_val) / (max_val - min_val + 1e-8), 0, 1)
|
| 162 |
data_normalized = np.nan_to_num(data_normalized, nan=0.5)
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
# Create figure
|
| 165 |
fig, ax = plt.subplots(figsize=(8, 8), dpi=100)
|
| 166 |
|
|
@@ -172,6 +184,34 @@ def generate_heatmap_image(data: np.ndarray, index_type: str) -> tuple:
|
|
| 172 |
|
| 173 |
im = ax.imshow(data_normalized, cmap=cmap, interpolation='bilinear')
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
# Add colorbar
|
| 176 |
cbar = plt.colorbar(im, ax=ax, shrink=0.8, pad=0.02)
|
| 177 |
cbar.set_label(f'{index_type} Value', fontsize=10)
|
|
@@ -299,8 +339,13 @@ async def generate_heatmap(request: HeatmapRequest):
|
|
| 299 |
|
| 300 |
logger.info(f"Calculated {request.index_type}: min={np.nanmin(index_data):.4f}, max={np.nanmax(index_data):.4f}")
|
| 301 |
|
| 302 |
-
# Generate heatmap
|
| 303 |
-
img_base64, min_val, max_val, mean_val = generate_heatmap_image(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
return HeatmapResponse(
|
| 306 |
success=True,
|
|
@@ -325,7 +370,9 @@ async def generate_heatmap_image_direct(
|
|
| 325 |
center_lat: float,
|
| 326 |
center_lon: float,
|
| 327 |
field_size_hectares: float = 10.0,
|
| 328 |
-
index_type: str = "NDVI"
|
|
|
|
|
|
|
| 329 |
):
|
| 330 |
"""Generate and return heatmap as PNG image directly."""
|
| 331 |
|
|
@@ -333,7 +380,9 @@ async def generate_heatmap_image_direct(
|
|
| 333 |
center_lat=center_lat,
|
| 334 |
center_lon=center_lon,
|
| 335 |
field_size_hectares=field_size_hectares,
|
| 336 |
-
index_type=index_type
|
|
|
|
|
|
|
| 337 |
)
|
| 338 |
|
| 339 |
response = await generate_heatmap(request)
|
|
|
|
| 12 |
import logging
|
| 13 |
from datetime import datetime, timedelta
|
| 14 |
from typing import Optional
|
| 15 |
+
from scipy.ndimage import gaussian_filter
|
| 16 |
|
| 17 |
import numpy as np
|
| 18 |
import matplotlib
|
|
|
|
| 70 |
center_lon: float
|
| 71 |
field_size_hectares: float = 10.0
|
| 72 |
index_type: str = "NDVI" # Any index from vegetation_indices.py
|
| 73 |
+
gaussian_sigma: float = 1.5 # Gaussian smoothing strength (0 = no smoothing)
|
| 74 |
+
show_field_boundary: bool = True # Whether to show field boundary
|
| 75 |
|
| 76 |
|
| 77 |
class HeatmapResponse(BaseModel):
|
|
|
|
| 148 |
return datetime.fromisoformat(ts_str)
|
| 149 |
|
| 150 |
|
| 151 |
+
def generate_heatmap_image(
|
| 152 |
+
data: np.ndarray,
|
| 153 |
+
index_type: str,
|
| 154 |
+
gaussian_sigma: float = 1.5,
|
| 155 |
+
show_field_boundary: bool = True
|
| 156 |
+
) -> tuple:
|
| 157 |
+
"""Generate a heatmap image from index data with optional smoothing and field boundary."""
|
| 158 |
|
| 159 |
# Handle NaN values
|
| 160 |
valid_mask = ~np.isnan(data)
|
|
|
|
| 169 |
data_normalized = np.clip((data - min_val) / (max_val - min_val + 1e-8), 0, 1)
|
| 170 |
data_normalized = np.nan_to_num(data_normalized, nan=0.5)
|
| 171 |
|
| 172 |
+
# Apply Gaussian smoothing if sigma > 0
|
| 173 |
+
if gaussian_sigma > 0:
|
| 174 |
+
data_normalized = gaussian_filter(data_normalized, sigma=gaussian_sigma)
|
| 175 |
+
|
| 176 |
# Create figure
|
| 177 |
fig, ax = plt.subplots(figsize=(8, 8), dpi=100)
|
| 178 |
|
|
|
|
| 184 |
|
| 185 |
im = ax.imshow(data_normalized, cmap=cmap, interpolation='bilinear')
|
| 186 |
|
| 187 |
+
# Draw field boundary if enabled
|
| 188 |
+
if show_field_boundary:
|
| 189 |
+
h, w = data_normalized.shape
|
| 190 |
+
# Draw a light rectangular boundary with some padding
|
| 191 |
+
padding = 0.05 # 5% padding from edges
|
| 192 |
+
rect = plt.Rectangle(
|
| 193 |
+
(w * padding, h * padding),
|
| 194 |
+
w * (1 - 2 * padding),
|
| 195 |
+
h * (1 - 2 * padding),
|
| 196 |
+
fill=False,
|
| 197 |
+
edgecolor='white',
|
| 198 |
+
linewidth=2,
|
| 199 |
+
linestyle='--',
|
| 200 |
+
alpha=0.6
|
| 201 |
+
)
|
| 202 |
+
ax.add_patch(rect)
|
| 203 |
+
|
| 204 |
+
# Add corner markers
|
| 205 |
+
corner_size = min(h, w) * 0.05
|
| 206 |
+
corners = [
|
| 207 |
+
(w * padding, h * padding), # Top-left
|
| 208 |
+
(w * (1 - padding), h * padding), # Top-right
|
| 209 |
+
(w * padding, h * (1 - padding)), # Bottom-left
|
| 210 |
+
(w * (1 - padding), h * (1 - padding)) # Bottom-right
|
| 211 |
+
]
|
| 212 |
+
for cx, cy in corners:
|
| 213 |
+
ax.plot(cx, cy, 'o', color='white', markersize=6, alpha=0.7)
|
| 214 |
+
|
| 215 |
# Add colorbar
|
| 216 |
cbar = plt.colorbar(im, ax=ax, shrink=0.8, pad=0.02)
|
| 217 |
cbar.set_label(f'{index_type} Value', fontsize=10)
|
|
|
|
| 339 |
|
| 340 |
logger.info(f"Calculated {request.index_type}: min={np.nanmin(index_data):.4f}, max={np.nanmax(index_data):.4f}")
|
| 341 |
|
| 342 |
+
# Generate heatmap with Gaussian smoothing and optional field boundary
|
| 343 |
+
img_base64, min_val, max_val, mean_val = generate_heatmap_image(
|
| 344 |
+
index_data,
|
| 345 |
+
request.index_type,
|
| 346 |
+
gaussian_sigma=request.gaussian_sigma,
|
| 347 |
+
show_field_boundary=request.show_field_boundary
|
| 348 |
+
)
|
| 349 |
|
| 350 |
return HeatmapResponse(
|
| 351 |
success=True,
|
|
|
|
| 370 |
center_lat: float,
|
| 371 |
center_lon: float,
|
| 372 |
field_size_hectares: float = 10.0,
|
| 373 |
+
index_type: str = "NDVI",
|
| 374 |
+
gaussian_sigma: float = 1.5,
|
| 375 |
+
show_field_boundary: bool = True
|
| 376 |
):
|
| 377 |
"""Generate and return heatmap as PNG image directly."""
|
| 378 |
|
|
|
|
| 380 |
center_lat=center_lat,
|
| 381 |
center_lon=center_lon,
|
| 382 |
field_size_hectares=field_size_hectares,
|
| 383 |
+
index_type=index_type,
|
| 384 |
+
gaussian_sigma=gaussian_sigma,
|
| 385 |
+
show_field_boundary=show_field_boundary
|
| 386 |
)
|
| 387 |
|
| 388 |
response = await generate_heatmap(request)
|
requirements.txt
CHANGED
|
@@ -2,6 +2,7 @@ fastapi==0.104.1
|
|
| 2 |
uvicorn[standard]==0.24.0
|
| 3 |
pydantic==2.5.2
|
| 4 |
numpy>=1.24.0
|
|
|
|
| 5 |
matplotlib>=3.7.0
|
| 6 |
Pillow>=9.0.0
|
| 7 |
sentinelhub>=3.9.0
|
|
|
|
| 2 |
uvicorn[standard]==0.24.0
|
| 3 |
pydantic==2.5.2
|
| 4 |
numpy>=1.24.0
|
| 5 |
+
scipy>=1.10.0
|
| 6 |
matplotlib>=3.7.0
|
| 7 |
Pillow>=9.0.0
|
| 8 |
sentinelhub>=3.9.0
|