geomind-ai 1.1.0__py3-none-any.whl → 1.1.1__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/agent.py +23 -14
- geomind/cli.py +215 -22
- geomind/tools/processing.py +32 -17
- geomind/tools/stac_search.py +19 -9
- {geomind_ai-1.1.0.dist-info → geomind_ai-1.1.1.dist-info}/METADATA +1 -1
- geomind_ai-1.1.1.dist-info/RECORD +14 -0
- {geomind_ai-1.1.0.dist-info → geomind_ai-1.1.1.dist-info}/WHEEL +1 -1
- geomind_ai-1.1.0.dist-info/RECORD +0 -14
- {geomind_ai-1.1.0.dist-info → geomind_ai-1.1.1.dist-info}/entry_points.txt +0 -0
- {geomind_ai-1.1.0.dist-info → geomind_ai-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {geomind_ai-1.1.0.dist-info → geomind_ai-1.1.1.dist-info}/top_level.txt +0 -0
geomind/agent.py
CHANGED
|
@@ -263,7 +263,7 @@ class GeoMindAgent:
|
|
|
263
263
|
"3. Create .env file with: OPENROUTER_API_KEY=YOUR_KEY"
|
|
264
264
|
)
|
|
265
265
|
|
|
266
|
-
print(f"
|
|
266
|
+
print(f"GeoMind Agent initialized with {self.model_name}")
|
|
267
267
|
|
|
268
268
|
# Create OpenAI-compatible client
|
|
269
269
|
self.client = OpenAI(base_url=self.base_url, api_key=self.api_key)
|
|
@@ -292,10 +292,19 @@ Key information:
|
|
|
292
292
|
- Bands available: B01-B12 at 10m, 20m, or 60m resolution
|
|
293
293
|
- Current date: {datetime.now().strftime('%Y-%m-%d')}
|
|
294
294
|
|
|
295
|
+
IMPORTANT - Zarr URL usage:
|
|
296
|
+
- STAC search results include both SR_10m (base URL) and individual band assets (B02_10m, B03_10m, B04_10m, B08_10m)
|
|
297
|
+
- EITHER type of URL works for create_rgb_composite and calculate_ndvi:
|
|
298
|
+
* SR_10m URL: Points to .../measurements/reflectance/r10m (contains all bands as subdirectories)
|
|
299
|
+
* Individual band URLs: Point directly to specific bands like .../r10m/b02
|
|
300
|
+
- Prefer using SR_10m URL as it's simpler and works for all bands
|
|
301
|
+
- The processing functions automatically handle the correct path structure
|
|
302
|
+
|
|
295
303
|
When users ask for imagery:
|
|
296
304
|
1. First use get_bbox_from_location or list_recent_imagery to search
|
|
297
|
-
2. Present the results clearly with key metadata
|
|
305
|
+
2. Present the results clearly with key metadata (ID, date, cloud cover)
|
|
298
306
|
3. Offer to create visualizations if data is found
|
|
307
|
+
4. For visualizations, use the SR_10m asset URL from search results
|
|
299
308
|
|
|
300
309
|
Always explain what you're doing and interpret results in a helpful way."""
|
|
301
310
|
|
|
@@ -312,7 +321,7 @@ Always explain what you're doing and interpret results in a helpful way."""
|
|
|
312
321
|
|
|
313
322
|
def _execute_function(self, name: str, args: dict) -> dict:
|
|
314
323
|
"""Execute a function call and return the result."""
|
|
315
|
-
print(f"
|
|
324
|
+
print(f" Executing: {name}({args})")
|
|
316
325
|
|
|
317
326
|
if name not in TOOL_FUNCTIONS:
|
|
318
327
|
return {"error": f"Unknown function: {name}"}
|
|
@@ -328,8 +337,8 @@ Always explain what you're doing and interpret results in a helpful way."""
|
|
|
328
337
|
Send a message to the agent and get a response.
|
|
329
338
|
"""
|
|
330
339
|
if verbose:
|
|
331
|
-
print(f"\
|
|
332
|
-
print("
|
|
340
|
+
print(f"\nUser: {message}")
|
|
341
|
+
print("Processing...")
|
|
333
342
|
|
|
334
343
|
# Add user message to history
|
|
335
344
|
self.history.append({"role": "user", "content": message})
|
|
@@ -395,7 +404,7 @@ Always explain what you're doing and interpret results in a helpful way."""
|
|
|
395
404
|
self.history.append({"role": "assistant", "content": final_text})
|
|
396
405
|
|
|
397
406
|
if verbose:
|
|
398
|
-
print(f"\
|
|
407
|
+
print(f"\nGeoMind: {final_text}")
|
|
399
408
|
|
|
400
409
|
return final_text
|
|
401
410
|
|
|
@@ -404,7 +413,7 @@ Always explain what you're doing and interpret results in a helpful way."""
|
|
|
404
413
|
def reset(self):
|
|
405
414
|
"""Reset the chat session."""
|
|
406
415
|
self.history = []
|
|
407
|
-
print("
|
|
416
|
+
print("Chat session reset")
|
|
408
417
|
|
|
409
418
|
|
|
410
419
|
def main(model: Optional[str] = None):
|
|
@@ -412,7 +421,7 @@ def main(model: Optional[str] = None):
|
|
|
412
421
|
import sys
|
|
413
422
|
|
|
414
423
|
print("=" * 60)
|
|
415
|
-
print("
|
|
424
|
+
print("GeoMind - Geospatial AI Agent")
|
|
416
425
|
print("=" * 60)
|
|
417
426
|
print("Powered by OpenRouter | Sentinel-2 Imagery")
|
|
418
427
|
print("Type 'quit' or 'exit' to end the session")
|
|
@@ -422,22 +431,22 @@ def main(model: Optional[str] = None):
|
|
|
422
431
|
try:
|
|
423
432
|
agent = GeoMindAgent(model=model)
|
|
424
433
|
except ValueError as e:
|
|
425
|
-
print(f"\
|
|
434
|
+
print(f"\nError: {e}")
|
|
426
435
|
sys.exit(1)
|
|
427
436
|
except Exception as e:
|
|
428
|
-
print(f"\
|
|
437
|
+
print(f"\nError: {e}")
|
|
429
438
|
print("\nPlease check your API key and internet connection.")
|
|
430
439
|
sys.exit(1)
|
|
431
440
|
|
|
432
441
|
while True:
|
|
433
442
|
try:
|
|
434
|
-
user_input = input("\
|
|
443
|
+
user_input = input("\nYou: ").strip()
|
|
435
444
|
|
|
436
445
|
if not user_input:
|
|
437
446
|
continue
|
|
438
447
|
|
|
439
448
|
if user_input.lower() in ["quit", "exit", "q"]:
|
|
440
|
-
print("\
|
|
449
|
+
print("\nGoodbye!")
|
|
441
450
|
break
|
|
442
451
|
|
|
443
452
|
if user_input.lower() == "reset":
|
|
@@ -447,10 +456,10 @@ def main(model: Optional[str] = None):
|
|
|
447
456
|
agent.chat(user_input)
|
|
448
457
|
|
|
449
458
|
except KeyboardInterrupt:
|
|
450
|
-
print("\n\
|
|
459
|
+
print("\n\nGoodbye!")
|
|
451
460
|
break
|
|
452
461
|
except Exception as e:
|
|
453
|
-
print(f"\
|
|
462
|
+
print(f"\nError: {e}")
|
|
454
463
|
|
|
455
464
|
|
|
456
465
|
if __name__ == "__main__":
|
geomind/cli.py
CHANGED
|
@@ -7,6 +7,10 @@ import os
|
|
|
7
7
|
import argparse
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Optional
|
|
10
|
+
import subprocess
|
|
11
|
+
import platform
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
10
14
|
|
|
11
15
|
from .agent import GeoMindAgent
|
|
12
16
|
|
|
@@ -36,6 +40,130 @@ def save_api_key(api_key: str) -> bool:
|
|
|
36
40
|
return False
|
|
37
41
|
|
|
38
42
|
|
|
43
|
+
def display_recent_images():
|
|
44
|
+
"""Display recently created images if any exist."""
|
|
45
|
+
outputs_dir = Path("outputs")
|
|
46
|
+
if not outputs_dir.exists():
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Get recent image files (created in last few seconds)
|
|
50
|
+
import time
|
|
51
|
+
recent_threshold = time.time() - 30 # 30 seconds ago
|
|
52
|
+
|
|
53
|
+
recent_images = []
|
|
54
|
+
for ext in ['*.png', '*.jpg', '*.jpeg', '*.tiff']:
|
|
55
|
+
for img_file in outputs_dir.glob(ext):
|
|
56
|
+
if img_file.stat().st_mtime > recent_threshold:
|
|
57
|
+
recent_images.append(img_file)
|
|
58
|
+
|
|
59
|
+
if recent_images:
|
|
60
|
+
print("\n" + "="*60)
|
|
61
|
+
print("Generated Images:")
|
|
62
|
+
for img in recent_images:
|
|
63
|
+
print(f" • {img.name} ({img.stat().st_size // 1024}KB)")
|
|
64
|
+
|
|
65
|
+
# Try to open the most recent image
|
|
66
|
+
if recent_images:
|
|
67
|
+
latest_image = max(recent_images, key=lambda x: x.stat().st_mtime)
|
|
68
|
+
open_image_viewer(latest_image)
|
|
69
|
+
print("="*60)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def open_image_viewer(image_path: Path):
|
|
73
|
+
"""Open image in default viewer."""
|
|
74
|
+
try:
|
|
75
|
+
system = platform.system()
|
|
76
|
+
if system == "Windows":
|
|
77
|
+
os.startfile(str(image_path))
|
|
78
|
+
elif system == "Darwin": # macOS
|
|
79
|
+
subprocess.run(["open", str(image_path)], check=False)
|
|
80
|
+
else: # Linux
|
|
81
|
+
subprocess.run(["xdg-open", str(image_path)], check=False)
|
|
82
|
+
print(f" -> Opened {image_path.name} in default viewer")
|
|
83
|
+
except Exception:
|
|
84
|
+
print(f" -> Saved to: {image_path}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def format_response_box(title: str, content: str, color_code: str = "\033[94m") -> str:
|
|
88
|
+
"""Format response in an attractive box."""
|
|
89
|
+
RESET = "\033[0m"
|
|
90
|
+
lines = content.split('\n')
|
|
91
|
+
max_width = max(len(line) for line in lines) if lines else 0
|
|
92
|
+
max_width = max(max_width, len(title) + 4)
|
|
93
|
+
width = min(max_width + 4, 80)
|
|
94
|
+
|
|
95
|
+
box = f"{color_code}"
|
|
96
|
+
box += "┌" + "─" * (width - 2) + "┐\n"
|
|
97
|
+
box += f"│ {title:<{width-4}} │\n"
|
|
98
|
+
box += "├" + "─" * (width - 2) + "┤\n"
|
|
99
|
+
|
|
100
|
+
for line in lines:
|
|
101
|
+
if line.strip():
|
|
102
|
+
box += f"│ {line:<{width-4}} │\n"
|
|
103
|
+
else:
|
|
104
|
+
box += f"│{' ' * (width-2)}│\n"
|
|
105
|
+
|
|
106
|
+
box += "└" + "─" * (width - 2) + "┘"
|
|
107
|
+
box += RESET
|
|
108
|
+
return box
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ThinkingIndicator:
|
|
112
|
+
"""Claude Code style thinking animation."""
|
|
113
|
+
|
|
114
|
+
def __init__(self):
|
|
115
|
+
self.is_thinking = False
|
|
116
|
+
self.thread = None
|
|
117
|
+
self.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
118
|
+
self.thinking_messages = [
|
|
119
|
+
"Thinking",
|
|
120
|
+
"Analyzing satellite data",
|
|
121
|
+
"Processing request",
|
|
122
|
+
"Searching imagery"
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
def start(self):
|
|
126
|
+
"""Start the thinking animation."""
|
|
127
|
+
self.is_thinking = True
|
|
128
|
+
self.thread = threading.Thread(target=self._animate)
|
|
129
|
+
self.thread.daemon = True
|
|
130
|
+
self.thread.start()
|
|
131
|
+
|
|
132
|
+
def stop(self):
|
|
133
|
+
"""Stop the thinking animation."""
|
|
134
|
+
self.is_thinking = False
|
|
135
|
+
if self.thread:
|
|
136
|
+
self.thread.join(timeout=0.1)
|
|
137
|
+
# Clear the line
|
|
138
|
+
print("\r" + " " * 60 + "\r", end="", flush=True)
|
|
139
|
+
|
|
140
|
+
def _animate(self):
|
|
141
|
+
"""Run the thinking animation."""
|
|
142
|
+
frame_idx = 0
|
|
143
|
+
message_idx = 0
|
|
144
|
+
message_counter = 0
|
|
145
|
+
|
|
146
|
+
# Colors like Claude Code
|
|
147
|
+
DIM = '\033[2m'
|
|
148
|
+
RESET = '\033[0m'
|
|
149
|
+
|
|
150
|
+
while self.is_thinking:
|
|
151
|
+
spinner = self.frames[frame_idx % len(self.frames)]
|
|
152
|
+
|
|
153
|
+
# Cycle through thinking messages every 30 frames (3 seconds)
|
|
154
|
+
if message_counter % 30 == 0:
|
|
155
|
+
message_idx = (message_idx + 1) % len(self.thinking_messages)
|
|
156
|
+
|
|
157
|
+
message = self.thinking_messages[message_idx]
|
|
158
|
+
|
|
159
|
+
# Show thinking with shimmer effect like Claude Code
|
|
160
|
+
print(f"\r{DIM}{spinner} {message}...{RESET}", end="", flush=True)
|
|
161
|
+
|
|
162
|
+
time.sleep(0.1)
|
|
163
|
+
frame_idx += 1
|
|
164
|
+
message_counter += 1
|
|
165
|
+
|
|
166
|
+
|
|
39
167
|
def main():
|
|
40
168
|
"""Main CLI entry point for the geomind package."""
|
|
41
169
|
parser = argparse.ArgumentParser(
|
|
@@ -93,9 +221,9 @@ Environment Variables:
|
|
|
93
221
|
if args.clear_key:
|
|
94
222
|
if CONFIG_FILE.exists():
|
|
95
223
|
CONFIG_FILE.unlink()
|
|
96
|
-
print("
|
|
224
|
+
print("Saved API key cleared.")
|
|
97
225
|
else:
|
|
98
|
-
print("
|
|
226
|
+
print("No saved API key found.")
|
|
99
227
|
sys.exit(0)
|
|
100
228
|
|
|
101
229
|
if args.version:
|
|
@@ -112,7 +240,7 @@ Environment Variables:
|
|
|
112
240
|
|
|
113
241
|
api_key = args.api_key or OPENROUTER_API_KEY or get_saved_api_key()
|
|
114
242
|
if not api_key:
|
|
115
|
-
print("
|
|
243
|
+
print("Error: No API key found. Run 'geomind' first to set up.")
|
|
116
244
|
sys.exit(1)
|
|
117
245
|
agent = GeoMindAgent(model=args.model, api_key=api_key)
|
|
118
246
|
agent.chat(args.query)
|
|
@@ -120,27 +248,64 @@ Environment Variables:
|
|
|
120
248
|
# Interactive mode
|
|
121
249
|
run_interactive(model=args.model, api_key=args.api_key)
|
|
122
250
|
except ValueError as e:
|
|
123
|
-
print(f"\
|
|
251
|
+
print(f"\nError: {e}")
|
|
124
252
|
sys.exit(1)
|
|
125
253
|
except KeyboardInterrupt:
|
|
126
|
-
print("\n\
|
|
254
|
+
print("\n\nGoodbye!")
|
|
127
255
|
sys.exit(0)
|
|
128
256
|
except Exception as e:
|
|
129
|
-
print(f"\
|
|
257
|
+
print(f"\nUnexpected error: {e}")
|
|
130
258
|
sys.exit(1)
|
|
131
259
|
|
|
132
260
|
|
|
261
|
+
def print_banner():
|
|
262
|
+
from . import __version__
|
|
263
|
+
|
|
264
|
+
# ANSI color codes
|
|
265
|
+
BOLD = '\033[1m'
|
|
266
|
+
DIM = '\033[2m'
|
|
267
|
+
RESET = '\033[0m'
|
|
268
|
+
|
|
269
|
+
banner = f"""
|
|
270
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
271
|
+
│ {BOLD}>_ GeoMind{RESET} (v{__version__}) │
|
|
272
|
+
│ │
|
|
273
|
+
│ model: nvidia/nemotron-3-nano-30b-a3b:free │
|
|
274
|
+
│ docs: https://harshshinde0.github.io/GeoMind │
|
|
275
|
+
│ authors: Harsh Shinde, Rajat Shinde │
|
|
276
|
+
│ official: https://harshshinde0.github.io/GeoMind │
|
|
277
|
+
│ │
|
|
278
|
+
│ Type "?" for help, "quit" to exit. │
|
|
279
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
280
|
+
"""
|
|
281
|
+
print(banner)
|
|
282
|
+
print()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def print_help():
|
|
286
|
+
"""Print interactive session help."""
|
|
287
|
+
help_text = """
|
|
288
|
+
Interactive Commands:
|
|
289
|
+
help, ? Show this help
|
|
290
|
+
reset Reset conversation
|
|
291
|
+
exit, quit, q Exit GeoMind
|
|
292
|
+
|
|
293
|
+
Query Examples:
|
|
294
|
+
> Find recent Sentinel-2 imagery of Paris
|
|
295
|
+
> Show me NDVI data for the Amazon rainforest
|
|
296
|
+
> Search for images with less than 10% cloud cover in London
|
|
297
|
+
> Get satellite data for coordinates 40.7128, -74.0060
|
|
298
|
+
|
|
299
|
+
For CLI options, run: geomind --help
|
|
300
|
+
"""
|
|
301
|
+
print(help_text)
|
|
302
|
+
|
|
303
|
+
|
|
133
304
|
def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
|
|
134
305
|
"""Run interactive CLI mode."""
|
|
135
306
|
from . import __version__
|
|
136
307
|
|
|
137
|
-
|
|
138
|
-
print("🌍 GeoMind - Geospatial AI Agent")
|
|
139
|
-
print("=" * 60)
|
|
140
|
-
print(f"Version: {__version__} | Authors: Harsh Shinde, Rajat Shinde")
|
|
141
|
-
print("Type 'quit' or 'exit' to end the session")
|
|
142
|
-
print("Type 'reset' to start a new conversation")
|
|
143
|
-
print("Type 'geomind --help' for more options")
|
|
308
|
+
print_banner()
|
|
144
309
|
|
|
145
310
|
# Check for API key in order: argument > env > saved file
|
|
146
311
|
from .config import OPENROUTER_API_KEY
|
|
@@ -155,44 +320,72 @@ def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
|
|
|
155
320
|
api_key = get_saved_api_key()
|
|
156
321
|
|
|
157
322
|
if not api_key:
|
|
158
|
-
print("\
|
|
323
|
+
print("\nOpenRouter API key required (FREE)")
|
|
159
324
|
print(" Get yours at: https://openrouter.ai/settings/keys\n")
|
|
160
325
|
api_key = input(" Enter your API key: ").strip()
|
|
161
326
|
|
|
162
327
|
if not api_key:
|
|
163
|
-
print("\
|
|
328
|
+
print("\nNo API key provided. Exiting.")
|
|
164
329
|
return
|
|
165
330
|
|
|
166
331
|
# Save the key for future use
|
|
167
332
|
if save_api_key(api_key):
|
|
168
|
-
print("
|
|
333
|
+
print(" API key saved! You won't need to enter it again.\n")
|
|
169
334
|
else:
|
|
170
|
-
print("
|
|
335
|
+
print(" Warning: Could not save API key. You'll need to enter it next time.\n")
|
|
171
336
|
|
|
172
337
|
agent = GeoMindAgent(model=model, api_key=api_key)
|
|
173
338
|
|
|
339
|
+
# Claude Code style color scheme
|
|
340
|
+
CYAN = '\033[96m'
|
|
341
|
+
DIM = '\033[2m'
|
|
342
|
+
BOLD = '\033[1m'
|
|
343
|
+
RESET = '\033[0m'
|
|
344
|
+
|
|
174
345
|
while True:
|
|
175
346
|
try:
|
|
176
|
-
|
|
347
|
+
# Simple prompt like Claude Code
|
|
348
|
+
user_input = input(f"\n{CYAN}>{RESET} ").strip()
|
|
177
349
|
|
|
178
350
|
if not user_input:
|
|
179
351
|
continue
|
|
180
352
|
|
|
181
353
|
if user_input.lower() in ["quit", "exit", "q"]:
|
|
182
|
-
print("\n
|
|
354
|
+
print(f"\n{DIM}Goodbye!{RESET}")
|
|
183
355
|
break
|
|
184
356
|
|
|
185
357
|
if user_input.lower() == "reset":
|
|
186
358
|
agent.reset()
|
|
359
|
+
print(f"{DIM}Started new conversation{RESET}")
|
|
187
360
|
continue
|
|
188
361
|
|
|
189
|
-
|
|
362
|
+
if user_input.lower() in ["help", "?"]:
|
|
363
|
+
print_help()
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
# Start thinking animation
|
|
367
|
+
thinking = ThinkingIndicator()
|
|
368
|
+
thinking.start()
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
# Get response from agent
|
|
372
|
+
response = agent.chat(user_input, verbose=False)
|
|
373
|
+
|
|
374
|
+
# Stop thinking animation
|
|
375
|
+
thinking.stop()
|
|
376
|
+
|
|
377
|
+
# Display response cleanly like Claude Code
|
|
378
|
+
print(f"\n{response}")
|
|
379
|
+
|
|
380
|
+
except Exception as chat_error:
|
|
381
|
+
thinking.stop()
|
|
382
|
+
raise chat_error
|
|
190
383
|
|
|
191
384
|
except KeyboardInterrupt:
|
|
192
|
-
print("\n\n
|
|
385
|
+
print(f"\n\n{DIM}Goodbye!{RESET}")
|
|
193
386
|
break
|
|
194
387
|
except Exception as e:
|
|
195
|
-
print(f"\n
|
|
388
|
+
print(f"\n{DIM}Error: {e}{RESET}")
|
|
196
389
|
|
|
197
390
|
|
|
198
391
|
if __name__ == "__main__":
|
geomind/tools/processing.py
CHANGED
|
@@ -106,7 +106,7 @@ def create_rgb_composite(
|
|
|
106
106
|
Uses B04 (Red), B03 (Green), B02 (Blue) bands.
|
|
107
107
|
|
|
108
108
|
Args:
|
|
109
|
-
zarr_url: URL to the SR_10m Zarr asset
|
|
109
|
+
zarr_url: URL to the SR_10m Zarr asset or individual band asset URL
|
|
110
110
|
output_path: Optional path to save the image
|
|
111
111
|
subset_size: Size to subset the image (for faster processing)
|
|
112
112
|
|
|
@@ -117,15 +117,23 @@ def create_rgb_composite(
|
|
|
117
117
|
import xarray as xr
|
|
118
118
|
import zarr
|
|
119
119
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
# Determine if this is a band-specific URL or base SR_10m URL
|
|
121
|
+
# Band-specific URLs end with /b02, /b03, /b04, etc.
|
|
122
|
+
# Base SR_10m URLs end with /r10m
|
|
123
|
+
is_band_url = zarr_url.rstrip('/').split('/')[-1].startswith('b')
|
|
124
|
+
|
|
125
|
+
if is_band_url:
|
|
126
|
+
# Individual band URL provided - need to construct URLs for each band
|
|
127
|
+
base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
|
|
128
|
+
red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
|
|
129
|
+
green = np.array(zarr.open(f"{base_url}/b03", mode="r"))
|
|
130
|
+
blue = np.array(zarr.open(f"{base_url}/b02", mode="r"))
|
|
131
|
+
else:
|
|
132
|
+
# Base SR_10m URL - bands are subdirectories
|
|
133
|
+
base_url = zarr_url.rstrip('/')
|
|
134
|
+
red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
|
|
135
|
+
green = np.array(zarr.open(f"{base_url}/b03", mode="r"))
|
|
136
|
+
blue = np.array(zarr.open(f"{base_url}/b02", mode="r"))
|
|
129
137
|
|
|
130
138
|
# Subset if requested (for faster processing)
|
|
131
139
|
if subset_size and red.shape[0] > subset_size:
|
|
@@ -197,7 +205,7 @@ def calculate_ndvi(
|
|
|
197
205
|
Uses B08 (NIR) and B04 (Red) bands.
|
|
198
206
|
|
|
199
207
|
Args:
|
|
200
|
-
zarr_url: URL to the SR_10m Zarr asset
|
|
208
|
+
zarr_url: URL to the SR_10m Zarr asset or individual band asset URL
|
|
201
209
|
output_path: Optional path to save the NDVI image
|
|
202
210
|
subset_size: Size to subset the image
|
|
203
211
|
|
|
@@ -208,12 +216,19 @@ def calculate_ndvi(
|
|
|
208
216
|
import zarr
|
|
209
217
|
from matplotlib.colors import LinearSegmentedColormap
|
|
210
218
|
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
# Determine if this is a band-specific URL or base SR_10m URL
|
|
220
|
+
is_band_url = zarr_url.rstrip('/').split('/')[-1].startswith('b')
|
|
221
|
+
|
|
222
|
+
if is_band_url:
|
|
223
|
+
# Individual band URL provided
|
|
224
|
+
base_url = '/'.join(zarr_url.rstrip('/').split('/')[:-1])
|
|
225
|
+
nir = np.array(zarr.open(f"{base_url}/b08", mode="r"))
|
|
226
|
+
red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
|
|
227
|
+
else:
|
|
228
|
+
# Base SR_10m URL
|
|
229
|
+
base_url = zarr_url.rstrip('/')
|
|
230
|
+
nir = np.array(zarr.open(f"{base_url}/b08", mode="r"))
|
|
231
|
+
red = np.array(zarr.open(f"{base_url}/b04", mode="r"))
|
|
217
232
|
|
|
218
233
|
# Subset if requested
|
|
219
234
|
if subset_size and nir.shape[0] > subset_size:
|
geomind/tools/stac_search.py
CHANGED
|
@@ -25,6 +25,24 @@ def _format_item(item) -> dict:
|
|
|
25
25
|
"""Format a STAC item into a simplified dictionary."""
|
|
26
26
|
props = item.properties
|
|
27
27
|
|
|
28
|
+
# Extract individual band assets for direct access
|
|
29
|
+
assets = {}
|
|
30
|
+
for key, asset in item.assets.items():
|
|
31
|
+
if key in ["SR_10m", "SR_20m", "SR_60m", "TCI_10m", "product"]:
|
|
32
|
+
assets[key] = {
|
|
33
|
+
"title": asset.title,
|
|
34
|
+
"href": asset.href,
|
|
35
|
+
"type": asset.media_type,
|
|
36
|
+
}
|
|
37
|
+
# Include individual 10m band assets for direct access
|
|
38
|
+
elif key in ["B02_10m", "B03_10m", "B04_10m", "B08_10m"]:
|
|
39
|
+
assets[key] = {
|
|
40
|
+
"title": asset.title,
|
|
41
|
+
"href": asset.href,
|
|
42
|
+
"type": asset.media_type,
|
|
43
|
+
"band": key.split("_")[0].lower(), # Extract band name (b02, b03, b04, b08)
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
return {
|
|
29
47
|
"id": item.id,
|
|
30
48
|
"datetime": props.get("datetime"),
|
|
@@ -32,15 +50,7 @@ def _format_item(item) -> dict:
|
|
|
32
50
|
"platform": props.get("platform"),
|
|
33
51
|
"bbox": item.bbox,
|
|
34
52
|
"geometry": item.geometry,
|
|
35
|
-
"assets":
|
|
36
|
-
key: {
|
|
37
|
-
"title": asset.title,
|
|
38
|
-
"href": asset.href,
|
|
39
|
-
"type": asset.media_type,
|
|
40
|
-
}
|
|
41
|
-
for key, asset in item.assets.items()
|
|
42
|
-
if key in ["SR_10m", "SR_20m", "SR_60m", "TCI_10m", "product"]
|
|
43
|
-
},
|
|
53
|
+
"assets": assets,
|
|
44
54
|
"stac_url": f"{STAC_API_URL}/collections/{STAC_COLLECTION}/items/{item.id}",
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
geomind/__init__.py,sha256=MZ0Zr2vGCJ816ilSApbwhA6iEfCwEBk40etbvIGfpqs,165
|
|
2
|
+
geomind/agent.py,sha256=-TF2VqOyTAJDrBPMbPKFoUwAHahBUUYOGtzqAKoNiUs,16226
|
|
3
|
+
geomind/cli.py,sha256=ipuMF0ZPseHHNKDD5VUMUG4fJDNmG_RQXNvAvT8Gbww,12085
|
|
4
|
+
geomind/config.py,sha256=7zPr0OKvK2SqQArUbjMvf5GVLQsmHcbn7e-BsSB1yv0,2030
|
|
5
|
+
geomind/tools/__init__.py,sha256=8iumGwIFHh8Bj1VJNgZtmKnEBqCy6_cRkzYENDUH7x4,720
|
|
6
|
+
geomind/tools/geocoding.py,sha256=hiLpzHpkJP6IgWAUtZMnHL6qpkWcYWVLpGe0yfYxXv8,3007
|
|
7
|
+
geomind/tools/processing.py,sha256=hb_Y9JV6o8XxS8_HI9DuW-hjzkv8S5ZdBpm45NpWwXQ,11287
|
|
8
|
+
geomind/tools/stac_search.py,sha256=3W9iZRObY6EkDn9K06Sp9ME-fYmubZRjPNDKDLEBUmM,6898
|
|
9
|
+
geomind_ai-1.1.1.dist-info/licenses/LICENSE,sha256=aveu0ERm7I3NnIu8rtpKdvd0eyRpmktXKU0PBABtSN0,1069
|
|
10
|
+
geomind_ai-1.1.1.dist-info/METADATA,sha256=3QKoizXhvfTGs6Bi73RdRFtx748Wzb7jGtbe39DObGw,2405
|
|
11
|
+
geomind_ai-1.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
+
geomind_ai-1.1.1.dist-info/entry_points.txt,sha256=2nPR3faYKl0-1epccvzMJ2xdi-Q1Vt7aOSvA84oIWnw,45
|
|
13
|
+
geomind_ai-1.1.1.dist-info/top_level.txt,sha256=rjKWNSNRhq4R9xJoZGsG-eAaH7BmTVNvfrrbcaJMIIs,8
|
|
14
|
+
geomind_ai-1.1.1.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
geomind/__init__.py,sha256=MZ0Zr2vGCJ816ilSApbwhA6iEfCwEBk40etbvIGfpqs,165
|
|
2
|
-
geomind/agent.py,sha256=2VDhPK9kvxf80KVoJLXbh8JGKbB8a3EiayojXd7ODOM,15661
|
|
3
|
-
geomind/cli.py,sha256=hUONvWUW2QTVawMxpDtFonIAqbS-SiV-bMFthgqnr2g,5614
|
|
4
|
-
geomind/config.py,sha256=7zPr0OKvK2SqQArUbjMvf5GVLQsmHcbn7e-BsSB1yv0,2030
|
|
5
|
-
geomind/tools/__init__.py,sha256=8iumGwIFHh8Bj1VJNgZtmKnEBqCy6_cRkzYENDUH7x4,720
|
|
6
|
-
geomind/tools/geocoding.py,sha256=hiLpzHpkJP6IgWAUtZMnHL6qpkWcYWVLpGe0yfYxXv8,3007
|
|
7
|
-
geomind/tools/processing.py,sha256=mGu20uvpAVil2w2BEqj-0zYhKhCFuZHr1-3-vq0VzqM,10152
|
|
8
|
-
geomind/tools/stac_search.py,sha256=V6230l4aHjedPWXu-3Cjmfc6diSFh5zsycewUko0W8k,6452
|
|
9
|
-
geomind_ai-1.1.0.dist-info/licenses/LICENSE,sha256=aveu0ERm7I3NnIu8rtpKdvd0eyRpmktXKU0PBABtSN0,1069
|
|
10
|
-
geomind_ai-1.1.0.dist-info/METADATA,sha256=EJROiPf8W_iht6qEaSyOzosyVob_hoodOe98ayFXAUE,2405
|
|
11
|
-
geomind_ai-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
geomind_ai-1.1.0.dist-info/entry_points.txt,sha256=2nPR3faYKl0-1epccvzMJ2xdi-Q1Vt7aOSvA84oIWnw,45
|
|
13
|
-
geomind_ai-1.1.0.dist-info/top_level.txt,sha256=rjKWNSNRhq4R9xJoZGsG-eAaH7BmTVNvfrrbcaJMIIs,8
|
|
14
|
-
geomind_ai-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|