geomind-ai 1.0.0__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geomind/__init__.py +11 -11
- geomind/agent.py +441 -445
- geomind/cli.py +125 -125
- geomind/config.py +55 -55
- geomind/tools/__init__.py +27 -27
- geomind/tools/geocoding.py +108 -108
- geomind/tools/processing.py +351 -349
- geomind/tools/stac_search.py +234 -231
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/METADATA +78 -85
- geomind_ai-1.0.2.dist-info/RECORD +14 -0
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/licenses/LICENSE +21 -21
- geomind_ai-1.0.0.dist-info/RECORD +0 -14
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/WHEEL +0 -0
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/entry_points.txt +0 -0
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/top_level.txt +0 -0
geomind/tools/processing.py
CHANGED
|
@@ -1,349 +1,351 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Image processing tools for Sentinel-2 data.
|
|
3
|
-
|
|
4
|
-
Handles loading Zarr data, applying corrections, and creating visualizations.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from typing import Optional, List
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
import numpy as np
|
|
10
|
-
|
|
11
|
-
from ..config import (
|
|
12
|
-
REFLECTANCE_SCALE,
|
|
13
|
-
REFLECTANCE_OFFSET,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
valid
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
import
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
output_path =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
ax.
|
|
160
|
-
ax.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
NDVI
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
output_path =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
ax.
|
|
253
|
-
ax.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return "
|
|
283
|
-
elif mean_ndvi < 0.
|
|
284
|
-
return "
|
|
285
|
-
elif mean_ndvi < 0.
|
|
286
|
-
return "
|
|
287
|
-
elif mean_ndvi < 0.
|
|
288
|
-
return "
|
|
289
|
-
|
|
290
|
-
return "
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
"
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
1
|
+
"""
|
|
2
|
+
Image processing tools for Sentinel-2 data.
|
|
3
|
+
|
|
4
|
+
Handles loading Zarr data, applying corrections, and creating visualizations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Tuple
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ..config import (
|
|
12
|
+
REFLECTANCE_SCALE,
|
|
13
|
+
REFLECTANCE_OFFSET,
|
|
14
|
+
RGB_BANDS,
|
|
15
|
+
OUTPUT_DIR,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _apply_scale_offset(
|
|
20
|
+
data: np.ndarray,
|
|
21
|
+
scale: float = REFLECTANCE_SCALE,
|
|
22
|
+
offset: float = REFLECTANCE_OFFSET,
|
|
23
|
+
nodata: int = 0,
|
|
24
|
+
) -> np.ndarray:
|
|
25
|
+
"""
|
|
26
|
+
Apply scale and offset to convert DN to surface reflectance.
|
|
27
|
+
|
|
28
|
+
Formula: reflectance = (DN * scale) + offset
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data: Raw digital number values
|
|
32
|
+
scale: Scale factor (default: 0.0001)
|
|
33
|
+
offset: Offset value (default: -0.1)
|
|
34
|
+
nodata: NoData value to mask (default: 0)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Surface reflectance values
|
|
38
|
+
"""
|
|
39
|
+
# Create mask for nodata
|
|
40
|
+
mask = data == nodata
|
|
41
|
+
|
|
42
|
+
# Apply transformation
|
|
43
|
+
result = (data.astype(np.float32) * scale) + offset
|
|
44
|
+
|
|
45
|
+
# Set nodata pixels to NaN
|
|
46
|
+
result[mask] = np.nan
|
|
47
|
+
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normalize_for_display(
|
|
52
|
+
data: np.ndarray,
|
|
53
|
+
percentile_low: float = 2,
|
|
54
|
+
percentile_high: float = 98,
|
|
55
|
+
) -> np.ndarray:
|
|
56
|
+
"""
|
|
57
|
+
Normalize data to 0-1 range for display using percentile stretch.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
data: Input array
|
|
61
|
+
percentile_low: Lower percentile for clipping
|
|
62
|
+
percentile_high: Upper percentile for clipping
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Normalized array in 0-1 range
|
|
66
|
+
"""
|
|
67
|
+
# Get valid (non-NaN) values
|
|
68
|
+
valid = data[~np.isnan(data)]
|
|
69
|
+
|
|
70
|
+
if len(valid) == 0:
|
|
71
|
+
return np.zeros_like(data)
|
|
72
|
+
|
|
73
|
+
# Calculate percentiles
|
|
74
|
+
low = np.percentile(valid, percentile_low)
|
|
75
|
+
high = np.percentile(valid, percentile_high)
|
|
76
|
+
|
|
77
|
+
# Normalize
|
|
78
|
+
if high > low:
|
|
79
|
+
result = (data - low) / (high - low)
|
|
80
|
+
else:
|
|
81
|
+
result = np.zeros_like(data)
|
|
82
|
+
|
|
83
|
+
# Clip to 0-1
|
|
84
|
+
result = np.clip(result, 0, 1)
|
|
85
|
+
|
|
86
|
+
# Set NaN to 0 for display
|
|
87
|
+
result = np.nan_to_num(result, nan=0)
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_rgb_composite(
|
|
93
|
+
zarr_url: str,
|
|
94
|
+
output_path: Optional[str] = None,
|
|
95
|
+
subset_size: Optional[int] = 1000,
|
|
96
|
+
) -> dict:
|
|
97
|
+
"""
|
|
98
|
+
Create an RGB composite image from Sentinel-2 10m bands.
|
|
99
|
+
|
|
100
|
+
Uses B04 (Red), B03 (Green), B02 (Blue) bands.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
zarr_url: URL to the SR_10m Zarr asset
|
|
104
|
+
output_path: Optional path to save the image
|
|
105
|
+
subset_size: Size to subset the image (for faster processing)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary with path to saved image and metadata
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
import xarray as xr
|
|
112
|
+
import matplotlib.pyplot as plt
|
|
113
|
+
import zarr
|
|
114
|
+
|
|
115
|
+
# Open the Zarr store
|
|
116
|
+
# The SR_10m asset contains b02, b03, b04, b08
|
|
117
|
+
store = zarr.open(zarr_url, mode="r")
|
|
118
|
+
|
|
119
|
+
# Read the bands
|
|
120
|
+
# Note: Band names are lowercase in the Zarr structure
|
|
121
|
+
red = np.array(store["b04"])
|
|
122
|
+
green = np.array(store["b03"])
|
|
123
|
+
blue = np.array(store["b02"])
|
|
124
|
+
|
|
125
|
+
# Subset if requested (for faster processing)
|
|
126
|
+
if subset_size and red.shape[0] > subset_size:
|
|
127
|
+
# Take center subset
|
|
128
|
+
h, w = red.shape
|
|
129
|
+
start_h = (h - subset_size) // 2
|
|
130
|
+
start_w = (w - subset_size) // 2
|
|
131
|
+
red = red[start_h : start_h + subset_size, start_w : start_w + subset_size]
|
|
132
|
+
green = green[
|
|
133
|
+
start_h : start_h + subset_size, start_w : start_w + subset_size
|
|
134
|
+
]
|
|
135
|
+
blue = blue[
|
|
136
|
+
start_h : start_h + subset_size, start_w : start_w + subset_size
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
# Apply scale and offset
|
|
140
|
+
red = _apply_scale_offset(red)
|
|
141
|
+
green = _apply_scale_offset(green)
|
|
142
|
+
blue = _apply_scale_offset(blue)
|
|
143
|
+
|
|
144
|
+
# Normalize for display
|
|
145
|
+
red = _normalize_for_display(red)
|
|
146
|
+
green = _normalize_for_display(green)
|
|
147
|
+
blue = _normalize_for_display(blue)
|
|
148
|
+
|
|
149
|
+
# Stack into RGB
|
|
150
|
+
rgb = np.dstack([red, green, blue])
|
|
151
|
+
|
|
152
|
+
# Generate output path
|
|
153
|
+
if output_path is None:
|
|
154
|
+
output_path = OUTPUT_DIR / f"rgb_composite_{np.random.randint(10000)}.png"
|
|
155
|
+
else:
|
|
156
|
+
output_path = Path(output_path)
|
|
157
|
+
|
|
158
|
+
# Create figure
|
|
159
|
+
fig, ax = plt.subplots(figsize=(10, 10))
|
|
160
|
+
ax.imshow(rgb)
|
|
161
|
+
ax.set_title("Sentinel-2 RGB Composite (B4/B3/B2)")
|
|
162
|
+
ax.axis("off")
|
|
163
|
+
|
|
164
|
+
# Save
|
|
165
|
+
plt.savefig(output_path, dpi=150, bbox_inches="tight", pad_inches=0.1)
|
|
166
|
+
plt.close(fig)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"success": True,
|
|
170
|
+
"output_path": str(output_path),
|
|
171
|
+
"image_size": rgb.shape[:2],
|
|
172
|
+
"bands_used": ["B04 (Red)", "B03 (Green)", "B02 (Blue)"],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
return {
|
|
177
|
+
"success": False,
|
|
178
|
+
"error": str(e),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def calculate_ndvi(
|
|
183
|
+
zarr_url: str,
|
|
184
|
+
output_path: Optional[str] = None,
|
|
185
|
+
subset_size: Optional[int] = 1000,
|
|
186
|
+
) -> dict:
|
|
187
|
+
"""
|
|
188
|
+
Calculate NDVI (Normalized Difference Vegetation Index) from Sentinel-2 data.
|
|
189
|
+
|
|
190
|
+
NDVI = (NIR - Red) / (NIR + Red)
|
|
191
|
+
Uses B08 (NIR) and B04 (Red) bands.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
zarr_url: URL to the SR_10m Zarr asset
|
|
195
|
+
output_path: Optional path to save the NDVI image
|
|
196
|
+
subset_size: Size to subset the image
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dictionary with NDVI statistics and output path
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
import zarr
|
|
203
|
+
import matplotlib.pyplot as plt
|
|
204
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
205
|
+
|
|
206
|
+
# Open the Zarr store
|
|
207
|
+
store = zarr.open(zarr_url, mode="r")
|
|
208
|
+
|
|
209
|
+
# Read the bands
|
|
210
|
+
nir = np.array(store["b08"]) # NIR
|
|
211
|
+
red = np.array(store["b04"]) # Red
|
|
212
|
+
|
|
213
|
+
# Subset if requested
|
|
214
|
+
if subset_size and nir.shape[0] > subset_size:
|
|
215
|
+
h, w = nir.shape
|
|
216
|
+
start_h = (h - subset_size) // 2
|
|
217
|
+
start_w = (w - subset_size) // 2
|
|
218
|
+
nir = nir[start_h : start_h + subset_size, start_w : start_w + subset_size]
|
|
219
|
+
red = red[start_h : start_h + subset_size, start_w : start_w + subset_size]
|
|
220
|
+
|
|
221
|
+
# Apply scale and offset
|
|
222
|
+
nir = _apply_scale_offset(nir)
|
|
223
|
+
red = _apply_scale_offset(red)
|
|
224
|
+
|
|
225
|
+
# Calculate NDVI
|
|
226
|
+
# Avoid division by zero
|
|
227
|
+
denominator = nir + red
|
|
228
|
+
denominator[denominator == 0] = np.nan
|
|
229
|
+
|
|
230
|
+
ndvi = (nir - red) / denominator
|
|
231
|
+
|
|
232
|
+
# NDVI statistics
|
|
233
|
+
valid_ndvi = ndvi[~np.isnan(ndvi)]
|
|
234
|
+
stats = {
|
|
235
|
+
"min": float(np.min(valid_ndvi)) if len(valid_ndvi) > 0 else None,
|
|
236
|
+
"max": float(np.max(valid_ndvi)) if len(valid_ndvi) > 0 else None,
|
|
237
|
+
"mean": float(np.mean(valid_ndvi)) if len(valid_ndvi) > 0 else None,
|
|
238
|
+
"std": float(np.std(valid_ndvi)) if len(valid_ndvi) > 0 else None,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Generate output path
|
|
242
|
+
if output_path is None:
|
|
243
|
+
output_path = OUTPUT_DIR / f"ndvi_{np.random.randint(10000)}.png"
|
|
244
|
+
else:
|
|
245
|
+
output_path = Path(output_path)
|
|
246
|
+
|
|
247
|
+
# Create NDVI colormap (brown -> yellow -> green)
|
|
248
|
+
colors = ["#8B4513", "#D2691E", "#FFD700", "#ADFF2F", "#228B22", "#006400"]
|
|
249
|
+
ndvi_cmap = LinearSegmentedColormap.from_list("ndvi", colors)
|
|
250
|
+
|
|
251
|
+
# Create figure
|
|
252
|
+
fig, ax = plt.subplots(figsize=(10, 10))
|
|
253
|
+
im = ax.imshow(ndvi, cmap=ndvi_cmap, vmin=-1, vmax=1)
|
|
254
|
+
ax.set_title("NDVI - Normalized Difference Vegetation Index")
|
|
255
|
+
ax.axis("off")
|
|
256
|
+
|
|
257
|
+
# Add colorbar
|
|
258
|
+
cbar = plt.colorbar(im, ax=ax, shrink=0.8)
|
|
259
|
+
cbar.set_label("NDVI")
|
|
260
|
+
|
|
261
|
+
# Save
|
|
262
|
+
plt.savefig(output_path, dpi=150, bbox_inches="tight", pad_inches=0.1)
|
|
263
|
+
plt.close(fig)
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
"success": True,
|
|
267
|
+
"output_path": str(output_path),
|
|
268
|
+
"statistics": stats,
|
|
269
|
+
"interpretation": _interpret_ndvi(stats["mean"]) if stats["mean"] else None,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
return {
|
|
274
|
+
"success": False,
|
|
275
|
+
"error": str(e),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _interpret_ndvi(mean_ndvi: float) -> str:
|
|
280
|
+
"""Provide interpretation of mean NDVI value."""
|
|
281
|
+
if mean_ndvi < 0:
|
|
282
|
+
return "Water or bare surfaces dominant"
|
|
283
|
+
elif mean_ndvi < 0.1:
|
|
284
|
+
return "Bare soil or built-up areas"
|
|
285
|
+
elif mean_ndvi < 0.2:
|
|
286
|
+
return "Sparse vegetation or stressed plants"
|
|
287
|
+
elif mean_ndvi < 0.4:
|
|
288
|
+
return "Moderate vegetation"
|
|
289
|
+
elif mean_ndvi < 0.6:
|
|
290
|
+
return "Dense vegetation"
|
|
291
|
+
else:
|
|
292
|
+
return "Very dense/healthy vegetation"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_band_statistics(
|
|
296
|
+
zarr_url: str,
|
|
297
|
+
bands: Optional[List[str]] = None,
|
|
298
|
+
) -> dict:
|
|
299
|
+
"""
|
|
300
|
+
Get statistics for specified bands from a Sentinel-2 Zarr asset.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
zarr_url: URL to the Zarr asset (e.g., SR_10m)
|
|
304
|
+
bands: List of band names (default: all available)
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Dictionary with statistics for each band
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
import zarr
|
|
311
|
+
|
|
312
|
+
store = zarr.open(zarr_url, mode="r")
|
|
313
|
+
|
|
314
|
+
# Get available bands if not specified
|
|
315
|
+
if bands is None:
|
|
316
|
+
bands = [key for key in store.keys() if key.startswith("b")]
|
|
317
|
+
|
|
318
|
+
results = {}
|
|
319
|
+
|
|
320
|
+
for band in bands:
|
|
321
|
+
if band not in store:
|
|
322
|
+
results[band] = {"error": "Band not found"}
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
data = np.array(store[band])
|
|
326
|
+
|
|
327
|
+
# Apply scale/offset
|
|
328
|
+
data = _apply_scale_offset(data)
|
|
329
|
+
valid = data[~np.isnan(data)]
|
|
330
|
+
|
|
331
|
+
if len(valid) > 0:
|
|
332
|
+
results[band] = {
|
|
333
|
+
"min": float(np.min(valid)),
|
|
334
|
+
"max": float(np.max(valid)),
|
|
335
|
+
"mean": float(np.mean(valid)),
|
|
336
|
+
"std": float(np.std(valid)),
|
|
337
|
+
"shape": data.shape,
|
|
338
|
+
}
|
|
339
|
+
else:
|
|
340
|
+
results[band] = {"error": "No valid data"}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
"success": True,
|
|
344
|
+
"band_statistics": results,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
except Exception as e:
|
|
348
|
+
return {
|
|
349
|
+
"success": False,
|
|
350
|
+
"error": str(e),
|
|
351
|
+
}
|