hoàn thành chức năng remove cloud train

This commit is contained in:
Victor Phan
2026-01-26 13:44:55 +07:00
parent 1a8b8cb88b
commit b827664af3
17 changed files with 4337 additions and 153 deletions
+493 -152
View File
@@ -3,7 +3,7 @@ API Server for Land Classification Model Training
Cho phép chọn dữ liệu và cấu hình training qua giao diện web
"""
from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File
from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse
@@ -38,6 +38,9 @@ from vietnam_provinces_merged import (
search_province_32, get_merged_info, get_provinces_statistics
)
# Import cloud removal module
from cloud_removal import process_cloud_removal, get_available_methods
# Import planetary computer libraries (conditional)
try:
from pystac_client import Client
@@ -217,6 +220,10 @@ class PredictionConfig(BaseModel):
# GPU support for deep learning models
use_gpu: bool
# Cloud removal strategy
cloud_removal_method: str = "classic"
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
class TrainingStatus(BaseModel):
@@ -236,6 +243,7 @@ class NDVIConfig(BaseModel):
end_date: str
max_cloud_cover: int = 30
resolution: int = 20
cloud_removal_method: str = "classic"
class ChangeDetectionWorkflowRequest(BaseModel):
@@ -258,6 +266,7 @@ class ComparePeriodsPredictionConfig(BaseModel):
resolution: int = 20
export_ndvi: bool = True
export_classification: bool = True
cloud_removal_method: str = "classic"
class PredictionWithNDVIConfig(BaseModel):
@@ -275,6 +284,19 @@ class PredictionWithNDVIConfig(BaseModel):
use_gpu: bool = False # Use GPU for deep learning models
export_ndvi: bool = True # Export NDVI raster
export_classification: bool = True # Export classification raster
cloud_removal_method: str = "classic"
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
class CloudRemovalTrainingConfig(BaseModel):
"""Cấu hình train cloud removal model"""
data_dir: str = "winter_dataset"
use_s1: bool = True # Sử dụng Sentinel-1 radar data
batch_size: int = 8
num_epochs: int = 50
learning_rate: float = 1e-4
use_gpu: bool = True
model_name: str = "cloud_removal_unet" # Tên model để lưu
# Serve change detection interface page (moved here after app is defined)
@@ -356,6 +378,16 @@ async def training_page():
raise HTTPException(status_code=404, detail="Training interface không tồn tại")
@app.get("/cloud-training", response_class=HTMLResponse)
async def cloud_training_page():
"""Serve cloud removal training interface"""
html_file = Path(__file__).parent / "cloud_training_interface.html"
if html_file.exists():
return FileResponse(html_file)
else:
raise HTTPException(status_code=404, detail="Cloud training interface không tồn tại")
@app.get("/prediction", response_class=HTMLResponse)
async def prediction_page():
"""Serve prediction interface"""
@@ -463,6 +495,298 @@ async def validate_model(model_filename: str):
}
@app.get("/api/cloud-removal/methods")
async def get_cloud_removal_methods():
"""Lấy danh sách các phương pháp xử lý mây có sẵn"""
methods = get_available_methods()
return {
"success": True,
"methods": methods,
"default": "classic",
"description": "Cloud removal strategies for Sentinel-2 data processing"
}
@app.get("/api/cloud-removal/models")
async def list_cloud_removal_models():
"""Liệt kê các cloud removal models đã train"""
model_dir = Path("model_train")
if not model_dir.exists():
return {"models": [], "count": 0}
models = []
# Search for ALL .pth files in model_train and subdirectories
for model_file in model_dir.rglob("*.pth"):
# Skip non-cloud-removal models (keep land classification models separate)
if any(x in model_file.name.lower() for x in ['mobilenet', 'cnn_', 'swin', 'xgboost', 'random_forest']):
continue
try:
import torch
import json
# Try to load metadata from .json sidecar file first
metadata_file = model_file.with_suffix('.json')
if metadata_file.exists():
try:
with open(metadata_file, 'r') as f:
metadata = json.load(f)
models.append({
"filename": model_file.name,
"path": str(model_file),
"relative_path": str(model_file.relative_to(model_dir)),
"epoch": metadata.get('epoch', 0),
"train_loss": metadata.get('train_loss', 0),
"val_loss": metadata.get('val_loss', 0),
"use_s1": metadata.get('use_s1', True),
"in_channels": metadata.get('in_channels', 6),
"out_channels": metadata.get('out_channels', 4),
"description": metadata.get('description', ''),
"created": model_file.stat().st_mtime,
"size_mb": model_file.stat().st_size / (1024 * 1024),
"has_metadata": True
})
continue
except Exception as e:
print(f"[Cloud Models] Failed to read metadata file {metadata_file}: {e}")
# Try to load checkpoint metadata from .pth file
try:
checkpoint = torch.load(model_file, map_location='cpu')
epoch = checkpoint.get('epoch', 0) if isinstance(checkpoint, dict) else 0
train_loss = checkpoint.get('train_loss', 0) if isinstance(checkpoint, dict) else 0
val_loss = checkpoint.get('val_loss', 0) if isinstance(checkpoint, dict) else 0
use_s1 = checkpoint.get('use_s1', True) if isinstance(checkpoint, dict) else True
in_channels = checkpoint.get('in_channels', 6) if isinstance(checkpoint, dict) else 6
out_channels = checkpoint.get('out_channels', 4) if isinstance(checkpoint, dict) else 4
except:
# If checkpoint format is different or corrupted, use defaults
epoch = 0
train_loss = 0
val_loss = 0
use_s1 = True
in_channels = 3
out_channels = 3
models.append({
"filename": model_file.name,
"path": str(model_file),
"relative_path": str(model_file.relative_to(model_dir)),
"epoch": epoch,
"train_loss": train_loss,
"val_loss": val_loss,
"use_s1": use_s1,
"in_channels": in_channels,
"out_channels": out_channels,
"description": "",
"created": model_file.stat().st_mtime,
"size_mb": model_file.stat().st_size / (1024 * 1024),
"has_metadata": False
})
except Exception as e:
print(f"[Cloud Models] Error loading {model_file}: {e}")
# Still add the file even if we can't load metadata
models.append({
"filename": model_file.name,
"path": str(model_file),
"relative_path": str(model_file.relative_to(model_dir)),
"epoch": 0,
"train_loss": 0,
"val_loss": 0,
"use_s1": False,
"in_channels": 3,
"out_channels": 3,
"description": "",
"created": model_file.stat().st_mtime,
"size_mb": model_file.stat().st_size / (1024 * 1024),
"has_metadata": False
})
models.sort(key=lambda x: x['created'], reverse=True)
return {"models": models, "count": len(models)}
@app.post("/api/cloud-removal/train")
async def train_cloud_removal(config: CloudRemovalTrainingConfig, background_tasks: BackgroundTasks):
"""Bắt đầu train cloud removal model"""
# Check if data directory exists
data_dir = Path(config.data_dir)
if not data_dir.exists():
raise HTTPException(
status_code=404,
detail=f"Data directory not found: {config.data_dir}"
)
# Create status tracking
training_id = datetime.now().strftime("%Y%m%d_%H%M%S")
async def run_cloud_training():
try:
from train_cloud_removal import train_cloud_removal_model
print(f"[CLOUD REMOVAL TRAINING] Starting training {training_id}")
model, train_losses, val_losses = train_cloud_removal_model(
data_dir=config.data_dir,
use_s1=config.use_s1,
batch_size=config.batch_size,
num_epochs=config.num_epochs,
learning_rate=config.learning_rate,
device="cuda" if config.use_gpu else "cpu",
save_dir="model_train"
)
print(f"[CLOUD REMOVAL TRAINING] Completed {training_id}")
return {
"success": True,
"training_id": training_id,
"final_train_loss": train_losses[-1],
"final_val_loss": val_losses[-1],
"epochs": len(train_losses)
}
except Exception as e:
print(f"[CLOUD REMOVAL TRAINING ERROR] {e}")
import traceback
traceback.print_exc()
return {
"success": False,
"error": str(e),
"training_id": training_id
}
# Run in background
background_tasks.add_task(run_cloud_training)
return {
"message": "Cloud removal training started",
"training_id": training_id,
"config": {
"data_dir": config.data_dir,
"use_s1": config.use_s1,
"batch_size": config.batch_size,
"num_epochs": config.num_epochs,
"learning_rate": config.learning_rate,
"use_gpu": config.use_gpu
}
}
@app.post("/api/cloud-removal/upload")
async def upload_cloud_removal_model(
file: UploadFile = File(...),
epoch: int = Form(0),
train_loss: float = Form(0.0),
val_loss: float = Form(0.0),
in_channels: int = Form(6),
out_channels: int = Form(4),
use_s1: bool = Form(True),
description: str = Form("")
):
"""Upload cloud removal .pth model with optional metadata"""
# Debug logging
print(f"[Upload] Received parameters:")
print(f" File: {file.filename}")
print(f" Epoch: {epoch} (type: {type(epoch)})")
print(f" Train Loss: {train_loss} (type: {type(train_loss)})")
print(f" Val Loss: {val_loss} (type: {type(val_loss)})")
print(f" In Channels: {in_channels} (type: {type(in_channels)})")
print(f" Out Channels: {out_channels} (type: {type(out_channels)})")
print(f" Use S1: {use_s1} (type: {type(use_s1)})")
print(f" Description: {description}")
# Validate file extension
if not file.filename.endswith('.pth'):
raise HTTPException(status_code=400, detail="Only .pth files are allowed")
# Security check
if ".." in file.filename or "/" in file.filename or "\\" in file.filename:
raise HTTPException(status_code=400, detail="Invalid filename")
try:
model_dir = Path("model_train")
model_dir.mkdir(exist_ok=True)
# Save uploaded file
file_path = model_dir / file.filename
# Check if file already exists
if file_path.exists():
raise HTTPException(status_code=400, detail=f"Model {file.filename} already exists")
# Write file
with open(file_path, "wb") as f:
content = await file.read()
f.write(content)
file_size = file_path.stat().st_size
# Save metadata as JSON sidecar file
import json
metadata_file = file_path.with_suffix('.json')
metadata_dict = {
"filename": file.filename,
"epoch": epoch,
"train_loss": train_loss,
"val_loss": val_loss,
"in_channels": in_channels,
"out_channels": out_channels,
"use_s1": use_s1,
"description": description,
"uploaded_at": datetime.now().isoformat()
}
with open(metadata_file, 'w') as f:
json.dump(metadata_dict, f, indent=2)
print(f"[Upload] Saved model: {file_path}")
print(f"[Upload] Saved metadata: {metadata_file}")
print(f"[Upload] Metadata: {metadata_dict}")
return {
"message": f"Successfully uploaded {file.filename}",
"filename": file.filename,
"size_mb": round(file_size / 1024 / 1024, 2),
"path": str(file_path),
"metadata": metadata_dict
}
except HTTPException:
raise
except Exception as e:
print(f"[Upload] Error: {e}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
@app.delete("/api/cloud-removal/models/{filename}")
async def delete_cloud_removal_model(filename: str):
"""Xóa cloud removal model"""
model_dir = Path("model_train")
model_path = model_dir / filename
# Security check
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
if not model_path.exists():
raise HTTPException(status_code=404, detail=f"Model not found: {filename}")
try:
model_path.unlink()
return {
"success": True,
"message": f"Deleted cloud removal model: {filename}"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to delete: {str(e)}")
@app.delete("/api/models/{model_filename}")
async def delete_model(model_filename: str):
"""Xóa model"""
@@ -1304,18 +1628,14 @@ async def run_prediction(config: PredictionConfig):
)
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
s2_search = catalog.search(
collections=["sentinel-2-l2a"],
s2_items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
time_range=time_range,
cloud_cover=config.cloud_cover,
max_scenes=config.max_scenes,
max_retries=3
)
s2_items = list(s2_search.items())
if not s2_items:
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
s2_items = s2_items[:config.max_scenes]
prediction_status["progress"] = f"Đang xử lý dữ liệu Sentinel-2..."
@@ -1417,87 +1737,27 @@ async def run_prediction(config: PredictionConfig):
# ============ ADVANCED CLOUD MASKING & REMOVAL ============
prediction_status["progress"] = "Đang xử lý mây nâng cao..."
cloud_coverage_percent = 0
if "SCL" in s2_data:
scl = s2_data["SCL"]
# SCL classification values (Sentinel-2 Scene Classification):
# 0: No data, 1: Saturated/Defective, 2: Dark Area Pixels
# 3: Cloud shadows, 4: Vegetation, 5: Not vegetated, 6: Water
# 7: Unclassified, 8: Cloud medium probability, 9: Cloud high probability
# 10: Thin cirrus, 11: Snow/Ice
# Comprehensive cloud mask (clouds, shadows, cirrus, snow)
cloud_mask = (scl == 3) | (scl == 8) | (scl == 9) | (scl == 10) | (scl == 11)
# Also mask no-data and saturated pixels
invalid_mask = (scl == 0) | (scl == 1)
full_mask = cloud_mask | invalid_mask
# Calculate cloud coverage percentage
total_pixels = full_mask.size
masked_pixels = int(full_mask.sum().values)
cloud_coverage_percent = (masked_pixels / total_pixels * 100) if total_pixels > 0 else 0
print(f"[CLOUD MASK] Cloud coverage: {cloud_coverage_percent:.1f}%")
print(f"[CLOUD MASK] Masked pixels: {masked_pixels}/{total_pixels}")
# Apply mask to all bands
for band in s2_data.data_vars:
if band != "SCL":
s2_data[band] = s2_data[band].where(~full_mask)
# ============ CLOUD REMOVAL STRATEGIES ============
# Strategy 1: Temporal Interpolation (fill gaps between time steps)
prediction_status["progress"] = "Đang khử mây bằng temporal interpolation..."
for band in s2_data.data_vars:
if band != "SCL":
# Forward fill then backward fill along time dimension
s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time')
print(f"[CLOUD REMOVAL] Applied temporal interpolation")
# Strategy 2: Median Compositing (if multiple time steps available)
if len(s2_data.time) >= 3:
prediction_status["progress"] = "Đang tạo median composite để giảm nhiễu mây..."
# Create median composite for each band
for band in s2_data.data_vars:
if band != "SCL":
# Median reduces cloud noise better than mean
median_composite = s2_data[band].median(dim='time', skipna=True)
# Fill remaining NaN with median
s2_data[band] = s2_data[band].fillna(median_composite)
print(f"[CLOUD REMOVAL] Applied median compositing from {len(s2_data.time)} scenes")
# Strategy 3: Spatial Interpolation (fill small gaps)
prediction_status["progress"] = "Đang khử mây bằng spatial interpolation..."
for band in s2_data.data_vars:
if band != "SCL":
# Use nearest neighbor interpolation for remaining small gaps
s2_data[band] = s2_data[band].interpolate_na(dim='x', method='nearest', fill_value='extrapolate')
s2_data[band] = s2_data[band].interpolate_na(dim='y', method='nearest', fill_value='extrapolate')
print(f"[CLOUD REMOVAL] Applied spatial interpolation")
# Final check: replace any remaining NaN with 0
for band in s2_data.data_vars:
if band != "SCL":
s2_data[band] = s2_data[band].fillna(0)
print(f"[CLOUD REMOVAL] Completed - all NaN values handled")
# Quality warning if cloud coverage too high
if cloud_coverage_percent > 30:
print(f"[WARNING] High cloud coverage ({cloud_coverage_percent:.1f}%) - prediction quality may be affected")
prediction_status["progress"] = f"⚠️ Cảnh báo: Độ phủ mây cao ({cloud_coverage_percent:.1f}%)"
else:
print("[WARNING] No SCL band available - skipping cloud masking")
prediction_status["progress"] = "⚠️ Không có SCL band - bỏ qua khử mây"
# Use cloud removal module with user-selected method
cloud_removal_method = config.cloud_removal_method if hasattr(config, 'cloud_removal_method') else "classic"
cloud_removal_model = config.cloud_removal_model if hasattr(config, 'cloud_removal_model') else None
print(f"[CLOUD REMOVAL] Using method: {cloud_removal_method}")
if cloud_removal_model:
print(f"[CLOUD REMOVAL] Using custom model: {cloud_removal_model}")
s2_data, cloud_metadata = process_cloud_removal(
s2_data=s2_data,
method=cloud_removal_method,
model_path=f"model_train/{cloud_removal_model}" if cloud_removal_model else None,
verbose=True
)
cloud_coverage_percent = cloud_metadata.get('cloud_coverage_percent', 0)
# Quality warning if cloud coverage too high
if cloud_coverage_percent > 30:
print(f"[WARNING] High cloud coverage ({cloud_coverage_percent:.1f}%) - prediction quality may be affected")
prediction_status["progress"] = f"⚠️ Cảnh báo: Độ phủ mây cao ({cloud_coverage_percent:.1f}%)"
# ============ EXTRACT FEATURES ============
prediction_status["progress"] = f"Đang trích xuất features (mode={feature_mode})..."
@@ -2458,20 +2718,16 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
job["progress"] = 25
# Search Sentinel-2
s2_search = catalog.search(
collections=["sentinel-2-l2a"],
# Search Sentinel-2 with retry
s2_items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
time_range=time_range,
cloud_cover=config.cloud_cover,
max_scenes=config.max_scenes,
max_retries=3
)
s2_items = list(s2_search.items())
if not s2_items:
raise ValueError("Không tìm thấy dữ liệu Sentinel-2")
s2_items = s2_items[:config.max_scenes]
job["progress"] = 35
# Load Sentinel-2 data
@@ -2768,14 +3024,15 @@ async def change_detection_predict_workflow(
modifier=planetary_computer.sign_inplace
)
search = catalog.search(
collections=["sentinel-2-l2a"],
# Use retry logic for fetching items
items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": cloud_cover}}
time_range=time_range,
cloud_cover=cloud_cover,
max_scenes=max_scenes,
max_retries=3
)
items = list(search.items())[:max_scenes]
print(f"[CHANGE DETECTION] Found {len(items)} Sentinel-2 scenes")
if len(items) == 0:
@@ -3403,6 +3660,86 @@ async def change_detection_api(
# ============ PREDICTION WITH NDVI API ============
def fetch_sentinel_items_with_retry(catalog, bbox, time_range, cloud_cover, max_scenes, max_retries=3):
"""
Fetch Sentinel-2 items with retry logic and exponential backoff
Optimizations:
- Reduce page size on retry
- Use shorter timeouts for each attempt
- Fetch fewer items initially and expand if successful
"""
import time
for attempt in range(max_retries):
try:
# Reduce target items on each retry to minimize timeout risk
target_items = max_scenes if attempt == 0 else min(max_scenes, 20 // (attempt + 1) * 10)
page_limit = 50 if attempt == 0 else 20 # Smaller pages on retry
print(f"[FETCH ATTEMPT {attempt + 1}/{max_retries}] Searching Sentinel-2...")
print(f" → Target items: {target_items}, Page limit: {page_limit}")
# Search with reduced limit on retries
search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": cloud_cover}},
limit=page_limit
)
# Try to get items with timeout protection
items = []
page_count = 0
max_pages = 3 if attempt > 0 else 5 # Fewer pages on retry
for item in search.items():
items.append(item)
if len(items) >= target_items:
print(f"[FETCH] Reached target ({target_items} items)")
break
# Track pagination to prevent hanging
if len(items) % page_limit == 0:
page_count += 1
if page_count >= max_pages:
print(f"[FETCH] Max pages reached ({max_pages}), got {len(items)} items")
break
if items:
print(f"[FETCH SUCCESS] Retrieved {len(items)} items")
# Return up to max_scenes, but accept fewer if that's all we got
return items[:min(len(items), max_scenes)]
else:
raise ValueError("No Sentinel-2 scenes found for the specified criteria")
except Exception as e:
error_msg = str(e)
print(f"[FETCH ERROR] Attempt {attempt + 1} failed: {error_msg}")
if attempt < max_retries - 1:
# Longer exponential backoff: 3, 6, 12 seconds
wait_time = 3 * (2 ** attempt)
print(f"[RETRY] Waiting {wait_time}s before retry...")
time.sleep(wait_time)
else:
# Final attempt failed
if "exceeded the maximum allowed time" in error_msg or "timeout" in error_msg.lower():
raise HTTPException(
status_code=504,
detail=f"Microsoft Planetary Computer request timed out after {max_retries} attempts. "
f"Please try: (1) Reduce date range (2) Reduce max_scenes to 5-10 (3) Use smaller bbox area"
)
elif "no sentinel-2 scenes found" in error_msg.lower():
raise HTTPException(
status_code=404,
detail="No Sentinel-2 data found. Try: (1) Different date range (2) Higher cloud_cover threshold (3) Different location"
)
else:
raise
@app.post("/api/predict/with-ndvi")
async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks: BackgroundTasks):
"""Predict land classification và NDVI cho một khu vực"""
@@ -3454,15 +3791,15 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
time_range = f"{config.start_date}/{config.end_date}"
# Search for Sentinel-2 data
search = catalog.search(
collections=["sentinel-2-l2a"],
# Search for Sentinel-2 data with retry logic
items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
time_range=time_range,
cloud_cover=config.cloud_cover,
max_scenes=config.max_scenes,
max_retries=3
)
items = list(search.items())[:config.max_scenes]
print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
if len(items) == 0:
@@ -3546,13 +3883,17 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
modifier=planetary_computer.sign_inplace
)
time_range = f"{config.start_date}/{config.end_date}"
search = catalog.search(
collections=["sentinel-2-l2a"],
# Use retry logic for B11 band
b11_items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
time_range=time_range,
cloud_cover=config.cloud_cover,
max_scenes=config.max_scenes,
max_retries=3
)
signed_items = [planetary_computer.sign(item) for item in list(search.items())[:config.max_scenes]]
signed_items = [planetary_computer.sign(item) for item in b11_items]
print(f"[PREDICT+NDVI] Fetched {len(signed_items)} scenes for B11")
b11_data = odc.stac.load(
@@ -3911,7 +4252,10 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
"bbox": bbox,
"change_detection": change_summary
}
except HTTPException:
# Re-raise HTTPException with original status code (e.g., 504 for timeout)
raise
except Exception as e:
print(f"[PREDICT+NDVI ERROR] {str(e)}")
import traceback
@@ -3942,15 +4286,15 @@ async def calculate_ndvi_timeseries(config: NDVIConfig):
bbox = config.bbox
time_range = f"{config.start_date}/{config.end_date}"
# Search for Sentinel-2 data
search = catalog.search(
collections=["sentinel-2-l2a"],
# Search for Sentinel-2 data with retry logic
items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.max_cloud_cover}}
time_range=time_range,
cloud_cover=config.max_cloud_cover,
max_scenes=1000, # Get all available scenes for time series
max_retries=3
)
items = list(search.items())
print(f"[NDVI] Found {len(items)} Sentinel-2 scenes")
if len(items) == 0:
@@ -4137,20 +4481,15 @@ async def ndvi_predict_timeseries(config: NDVIPredictionConfig):
modifier=planetary_computer.sign_inplace,
)
s2_search = catalog.search(
collections=["sentinel-2-l2a"],
# Search for Sentinel-2 data with retry
s2_items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.max_cloud_cover}}
time_range=time_range,
cloud_cover=config.max_cloud_cover,
max_scenes=config.max_scenes,
max_retries=3
)
s2_items = list(s2_search.items())
if not s2_items:
raise HTTPException(status_code=404, detail="No Sentinel-2 data found")
# Limit scenes to max_scenes
if len(s2_items) > config.max_scenes:
s2_items = s2_items[:config.max_scenes]
print(f"✅ Found {len(s2_items)} Sentinel-2 scenes (limited to {config.max_scenes})")
@@ -4623,15 +4962,18 @@ async def ndvi_forecast(config: NDVIForecastConfig):
modifier=planetary_computer.sign_inplace,
)
s2_search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.max_cloud_cover}}
)
s2_items = list(s2_search.items())
if not s2_items:
# Search for historical Sentinel-2 data with retry
try:
s2_items = fetch_sentinel_items_with_retry(
catalog=catalog,
bbox=bbox,
time_range=time_range,
cloud_cover=config.max_cloud_cover,
max_scenes=config.max_scenes,
max_retries=3
)
except ValueError:
# No items found - provide helpful error message
raise HTTPException(
status_code=404,
detail=f"⚠️ Không tìm thấy dữ liệu Sentinel-2 cho khu vực này!\n\n"
@@ -4646,8 +4988,7 @@ async def ndvi_forecast(config: NDVIForecastConfig):
f"5. Đảm bảo forecast_start_date không quá xa trong tương lai"
)
if len(s2_items) > config.max_scenes:
s2_items = s2_items[:config.max_scenes]
print(f"✅ Found {len(s2_items)} historical Sentinel-2 scenes")
print(f"✅ Found {len(s2_items)} historical scenes")