From 4767ad40eae9c3b9580e15ad9f5d12ccc3306125 Mon Sep 17 00:00:00 2001 From: Victor Phan Date: Wed, 7 Jan 2026 10:50:01 +0700 Subject: [PATCH] =?UTF-8?q?ho=C3=A0n=20th=C3=A0nh=20ch=E1=BB=A9c=20n=C4=83?= =?UTF-8?q?ng=20predict=20ndvi=20time=20series=20analysis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api_server.py | 138 +++++++++++++++++++++++++++++--------------- ndvi_interface.html | 109 +++++++++++++++++++++++++++------- 2 files changed, 177 insertions(+), 70 deletions(-) diff --git a/api_server.py b/api_server.py index e9e5771..6bba65e 100644 --- a/api_server.py +++ b/api_server.py @@ -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)...") diff --git a/ndvi_interface.html b/ndvi_interface.html index 995948b..a6ea41e 100644 --- a/ndvi_interface.html +++ b/ndvi_interface.html @@ -3,7 +3,7 @@ - NDVI Time Series Analysis + NDVI Time Series Predicts @@ -131,6 +131,22 @@ opacity: 0.5; cursor: not-allowed; } + + .region-filter-btn { + padding: 3px 10px; + background: #e0e0e0; + color: #666; + border: none; + border-radius: 15px; + font-size: 0.85em; + cursor: pointer; + transition: background 0.2s, color 0.2s; + } + + .region-filter-btn.active { + background: #27ae60; + color: white; + } #map { height: 400px; @@ -199,7 +215,7 @@
-

🌿 NDVI Time Series Analysis

+

🌿 NDVI Time Series Predicts

Phân tích chỉ số thực vật theo thời gian từ dữ liệu Sentinel-2

@@ -208,7 +224,7 @@ 🎓 Training 🗺️ Prediction 🚀 Batch Processing - 🌿 NDVI Analysis (Active) + 🌿 NDVI Predicts (Active) 📝 Reports
@@ -251,7 +267,7 @@
- +