hoàn thành chức năng remove cloud train
This commit is contained in:
+493
-152
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user