hoàn thành chức năng predict ndvi time series analysis
This commit is contained in:
+90
-48
@@ -4230,6 +4230,26 @@ async def ndvi_predict_timeseries(config: NDVIPredictionConfig):
|
||||
print(f" Mean NDVI: {result['mean_ndvi']:.3f}")
|
||||
print(f" NDVI range: [{result['min_ndvi']:.3f}, {result['max_ndvi']:.3f}]")
|
||||
print(f"{'='*70}\n")
|
||||
|
||||
# Auto-save prediction cache (Sentinel-2 data + metadata) similar to rice predict
|
||||
try:
|
||||
cache_config = {
|
||||
"min_lon": config.bbox[0],
|
||||
"min_lat": config.bbox[1],
|
||||
"max_lon": config.bbox[2],
|
||||
"max_lat": config.bbox[3],
|
||||
"start_date": config.start_date,
|
||||
"end_date": config.end_date,
|
||||
"max_scenes": config.max_scenes,
|
||||
"cloud_cover": config.max_cloud_cover,
|
||||
"resolution": config.resolution,
|
||||
"model_filename": config.model_filename
|
||||
}
|
||||
|
||||
cache_result = save_prediction_cache_sync(cache_config, s2_data)
|
||||
print(f"[NDVI TIMESERIES][CACHE] Saved cache: {cache_result.get('filename')} (with data: {s2_data is not None})")
|
||||
except Exception as cache_exc:
|
||||
print(f"[NDVI TIMESERIES][CACHE ERROR] {cache_exc}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -4380,10 +4400,17 @@ async def ndvi_forecast(config: NDVIForecastConfig):
|
||||
|
||||
# Mask cloud pixels
|
||||
scl = s2_data['SCL']
|
||||
cloud_mask = ~np.isin(scl, [3, 8, 9, 10, 0, 1])
|
||||
# cloud_mask: shape (time, y, x) if scl is xarray.DataArray, else (time, y, x) ndarray
|
||||
# If scl is xarray.DataArray, cloud_mask will be xarray.DataArray, else numpy ndarray
|
||||
if hasattr(scl, 'isel'):
|
||||
cloud_mask = ~np.isin(scl, [3, 8, 9, 10, 0, 1]) # xarray.DataArray
|
||||
else:
|
||||
# scl is numpy ndarray, so cloud_mask is ndarray
|
||||
cloud_mask = ~np.isin(scl, [3, 8, 9, 10, 0, 1])
|
||||
|
||||
# LAND-TYPE-SPECIFIC FORECASTING
|
||||
use_ml_classification = config.model_filename is not None
|
||||
force_simple_forecast = False
|
||||
|
||||
if use_ml_classification:
|
||||
print(f"\n🤖 Using ML model for land-type-specific forecasting...")
|
||||
@@ -4447,10 +4474,8 @@ async def ndvi_forecast(config: NDVIForecastConfig):
|
||||
continue
|
||||
|
||||
if len(point_features) == 0:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Không tìm thấy điểm hợp lệ để phân loại trong khu vực này. Thử: (1) Chọn khu vực lớn hơn, (2) Tăng historical_months, (3) Giảm max_cloud_cover, hoặc (4) Chọn khu vực có dữ liệu vệ tinh tốt hơn. Đã thử {config.sample_points} điểm ngẫu nhiên nhưng tất cả đều bị masked (mây/nước)."
|
||||
)
|
||||
print("[NDVI FORECAST][FALLBACK] No valid classification points. Falling back to simple seasonal forecast.")
|
||||
force_simple_forecast = True
|
||||
|
||||
print(f"✅ Extracted features for {len(point_features)} valid points")
|
||||
|
||||
@@ -4480,8 +4505,13 @@ async def ndvi_forecast(config: NDVIForecastConfig):
|
||||
month = time_val.month
|
||||
|
||||
# Get valid pixels for this time step
|
||||
mask_t = cloud_mask.isel(time=time_idx)
|
||||
|
||||
if hasattr(cloud_mask, 'isel'):
|
||||
mask_t = cloud_mask.isel(time=time_idx)
|
||||
else:
|
||||
# Wrap numpy mask to xarray with same coords/dims as ndvi slice
|
||||
ndvi_slice = ndvi.isel(time=time_idx)
|
||||
mask_t = xr.DataArray(mask_t := cloud_mask[time_idx], coords=ndvi_slice.coords, dims=ndvi_slice.dims)
|
||||
|
||||
ndvi_t = ndvi.isel(time=time_idx).where(mask_t)
|
||||
ndwi_t = ndwi.isel(time=time_idx).where(mask_t)
|
||||
ndbi_t = ndbi.isel(time=time_idx).where(mask_t)
|
||||
@@ -4539,55 +4569,67 @@ async def ndvi_forecast(config: NDVIForecastConfig):
|
||||
}
|
||||
|
||||
print(f"✅ Calculated patterns for {len(land_type_seasonal_stats)} land types")
|
||||
|
||||
if len(land_type_seasonal_stats) == 0:
|
||||
print("[NDVI FORECAST][FALLBACK] No seasonal patterns. Falling back to simple seasonal forecast.")
|
||||
force_simple_forecast = True
|
||||
|
||||
# Generate forecast using land-type-weighted average
|
||||
print(f"\n🔮 Generating land-type-specific forecast...")
|
||||
|
||||
forecast_timeseries = []
|
||||
current_date = forecast_start
|
||||
|
||||
# Calculate land type weights
|
||||
total_points = len(predictions)
|
||||
land_type_weights = {lt: np.sum(predictions == lt) / total_points
|
||||
for lt in unique_types}
|
||||
|
||||
while current_date <= forecast_end:
|
||||
month = current_date.month
|
||||
if force_simple_forecast:
|
||||
print("[NDVI FORECAST][FALLBACK] Skipping ML forecast, will use simple seasonal averaging.")
|
||||
else:
|
||||
print(f"\n🔮 Generating land-type-specific forecast...")
|
||||
|
||||
# Aggregate forecast across all land types (weighted)
|
||||
weighted_forecast = {
|
||||
'ndvi_mean': 0, 'ndvi_min': 0, 'ndvi_max': 0, 'ndvi_std': 0,
|
||||
'ndvi_range': 0, 'ndwi_mean': 0, 'ndbi_mean': 0, 'evi_mean': 0
|
||||
}
|
||||
forecast_timeseries = []
|
||||
current_date = forecast_start
|
||||
|
||||
land_type_contributions = {}
|
||||
# Calculate land type weights
|
||||
total_points = len(predictions)
|
||||
if total_points == 0:
|
||||
print("[NDVI FORECAST][FALLBACK] Zero valid classified points. Switching to simple seasonal forecast.")
|
||||
force_simple_forecast = True
|
||||
else:
|
||||
land_type_weights = {lt: np.sum(predictions == lt) / total_points
|
||||
for lt in unique_types}
|
||||
|
||||
for land_type, weight in land_type_weights.items():
|
||||
if land_type in land_type_seasonal_stats and month in land_type_seasonal_stats[land_type]:
|
||||
stats = land_type_seasonal_stats[land_type][month]
|
||||
if not force_simple_forecast:
|
||||
while current_date <= forecast_end:
|
||||
month = current_date.month
|
||||
|
||||
land_type_contributions[int(land_type)] = {
|
||||
**stats,
|
||||
'weight': float(weight)
|
||||
# Aggregate forecast across all land types (weighted)
|
||||
weighted_forecast = {
|
||||
'ndvi_mean': 0, 'ndvi_min': 0, 'ndvi_max': 0, 'ndvi_std': 0,
|
||||
'ndvi_range': 0, 'ndwi_mean': 0, 'ndbi_mean': 0, 'evi_mean': 0
|
||||
}
|
||||
|
||||
for key in weighted_forecast:
|
||||
weighted_forecast[key] += stats[key] * weight
|
||||
|
||||
if land_type_contributions:
|
||||
forecast_data = {
|
||||
'date': current_date.strftime('%Y-%m-%d'),
|
||||
'is_forecast': True,
|
||||
'land_type_specific': land_type_contributions,
|
||||
**weighted_forecast
|
||||
}
|
||||
forecast_timeseries.append(forecast_data)
|
||||
|
||||
current_date += relativedelta(months=1)
|
||||
|
||||
method_used = "Land-Type-Specific Forecasting (ML-Enhanced)"
|
||||
|
||||
else:
|
||||
land_type_contributions = {}
|
||||
|
||||
for land_type, weight in land_type_weights.items():
|
||||
if land_type in land_type_seasonal_stats and month in land_type_seasonal_stats[land_type]:
|
||||
stats = land_type_seasonal_stats[land_type][month]
|
||||
|
||||
land_type_contributions[int(land_type)] = {
|
||||
**stats,
|
||||
'weight': float(weight)
|
||||
}
|
||||
|
||||
for key in weighted_forecast:
|
||||
weighted_forecast[key] += stats[key] * weight
|
||||
|
||||
if land_type_contributions:
|
||||
forecast_data = {
|
||||
'date': current_date.strftime('%Y-%m-%d'),
|
||||
'is_forecast': True,
|
||||
'land_type_specific': land_type_contributions,
|
||||
**weighted_forecast
|
||||
}
|
||||
forecast_timeseries.append(forecast_data)
|
||||
|
||||
current_date += relativedelta(months=1)
|
||||
|
||||
method_used = "Land-Type-Specific Forecasting (ML-Enhanced)"
|
||||
|
||||
if force_simple_forecast or not use_ml_classification:
|
||||
# Simple seasonal averaging (fallback)
|
||||
print(f"\n📈 Calculating simple seasonal patterns (no ML)...")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user