hoàn thành chức năng tính ndvi analysys 2 màn hình

This commit is contained in:
Victor Phan
2025-12-24 13:57:08 +07:00
parent 389c7c141f
commit e86709df85
25 changed files with 4755 additions and 460 deletions
+397 -346
View File
@@ -26,6 +26,9 @@ import traceback
# Import report generator
from report_generator import generate_training_report, generate_prediction_report
# Import Model Manager
from model_manager import ModelManager, get_model_manager
# Import planetary computer libraries (conditional)
try:
from pystac_client import Client
@@ -277,6 +280,84 @@ async def reports_page():
raise HTTPException(status_code=404, detail="Reports interface không tồn tại")
@app.get("/api/models/list")
async def list_models():
"""Liệt kê tất cả models có sẵn với metadata"""
try:
model_manager = get_model_manager()
models = model_manager.list_models()
return {
"success": True,
"models": models,
"count": len(models)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"models": []
}
@app.get("/api/models/{model_filename}/info")
async def get_model_info(model_filename: str):
"""Lấy thông tin chi tiết về model"""
try:
model_manager = get_model_manager()
info = model_manager.get_model_info(model_filename)
if info is None:
raise HTTPException(status_code=404, detail=f"Model không tồn tại: {model_filename}")
return {
"success": True,
"model": info
}
except HTTPException:
raise
except Exception as e:
return {
"success": False,
"error": str(e)
}
@app.get("/api/models/{model_filename}/validate")
async def validate_model(model_filename: str):
"""Validate model file"""
try:
model_manager = get_model_manager()
validation = model_manager.validate_model(model_filename)
return {
"success": True,
"validation": validation
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
@app.delete("/api/models/{model_filename}")
async def delete_model(model_filename: str):
"""Xóa model"""
try:
model_manager = get_model_manager()
success = model_manager.delete_model(model_filename)
if not success:
raise HTTPException(status_code=404, detail=f"Model không tồn tại: {model_filename}")
return {
"success": True,
"message": f"Đã xóa model: {model_filename}"
}
except HTTPException:
raise
except Exception as e:
return {
"success": False,
"error": str(e)
}
@app.get("/api/config/presets")
async def get_presets():
"""Lấy các preset cấu hình sẵn"""
@@ -751,18 +832,18 @@ def update_prediction_progress(message: str):
async def run_prediction(config: PredictionConfig):
"""Chạy prediction process - Áp dụng phương pháp từ 02.predict_ODC.ipynb"""
"""Chạy prediction process - Sử dụng FeatureExtractor để đồng bộ với training"""
global prediction_status
try:
prediction_status["progress"] = "Đang import thư viện..."
# Import required libraries
import xarray as xr
import numpy as np
from datetime import datetime as dt
import xarray as xr
import rioxarray
import dask.array as da
from datetime import datetime as dt
from feature_extractor import get_feature_extractor
# Validate bbox
if (config.min_lon < -180 or config.max_lon > 180 or
@@ -772,331 +853,160 @@ async def run_prediction(config: PredictionConfig):
prediction_status["progress"] = "Đang load model..."
# Load model
model_path = Path("model_train") / config.model_filename
if not model_path.exists():
raise FileNotFoundError(f"Model không tồn tại: {config.model_filename}")
# Load model using ModelManager
model_manager = get_model_manager()
model, label_encoder, model_metadata = model_manager.load_model(config.model_filename)
model_data = joblib.load(model_path)
# Extract model from dict (models are saved as {'model': xgb_model, 'label_encoder': encoder})
if isinstance(model_data, dict):
model = model_data.get('model')
label_encoder = model_data.get('label_encoder')
else:
model = model_data
label_encoder = None
# Get feature_mode and features from metadata (default to 'simple' if not specified)
feature_mode = model_metadata.get("feature_mode", "simple")
required_features = model_metadata.get("features", [])
n_features_expected = model_metadata.get("n_features", len(required_features))
prediction_status["progress"] = f"Model: {model_metadata.get('model_type', 'unknown')}, mode={feature_mode}, features={n_features_expected}"
# Initialize FeatureExtractor với đúng mode như lúc training
extractor = get_feature_extractor(mode=feature_mode)
# Check if it's a CNN model (PyTorch)
is_cnn_model = hasattr(model, '__class__') and 'CNN' in model.__class__.__name__
if is_cnn_model:
prediction_status["progress"] = "Phát hiện PyTorch CNN model..."
# Import PyTorch if needed
try:
import torch
except ImportError:
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
prediction_status["progress"] = "Đang kiểm tra cache dữ liệu đầu vào..."
import hashlib, os
cache_dir = Path("dataset_cache")
cache_dir.mkdir(exist_ok=True)
# Tạo cache key từ bbox, time_range, max_scenes, cloud_cover, resolution
cache_key = f"pred_{config.min_lon}_{config.min_lat}_{config.max_lon}_{config.max_lat}_{config.start_date}_{config.end_date}_{config.max_scenes}_{config.cloud_cover}_{config.resolution}"
cache_hash = hashlib.md5(cache_key.encode()).hexdigest()
cache_file = cache_dir / f"prediction_input_{cache_hash}.joblib"
raise ImportError("PyTorch required for CNN models. Install: pip install torch")
# Initialize common variables
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
time_range = f"{config.start_date}/{config.end_date}"
# Try to load from cache first
s2_data = None
use_cache = False
if cache_file.exists():
prediction_status["progress"] = "Đang load dữ liệu từ cache..."
try:
cached = joblib.load(cache_file)
s2_data_temp = cached["s2_data"]
# Verify that cached data is not lazy (to avoid 403 errors from expired URLs)
# If s2_data has chunks attribute, it's a dask array (lazy)
is_lazy = False
try:
is_lazy = any(hasattr(s2_data_temp[var].data, 'chunks') for var in s2_data_temp.data_vars)
except:
pass
if is_lazy:
print(f"[WARNING] Cache contains lazy data with potentially expired URLs. Deleting cache...")
cache_file.unlink()
raise ValueError("Cache invalid - contains lazy data")
# Cache is valid, use it
s2_data = s2_data_temp
s2_items = cached.get("s2_items", [])
vh_monthly = cached.get("vh_monthly")
vv_monthly = cached.get("vv_monthly")
use_radar = cached.get("use_radar", False)
use_cache = True
print(f"[INFO] Loaded valid cache from {cache_file.name}")
except Exception as e:
print(f"[WARNING] Failed to load cache: {e}. Fetching fresh data...")
s2_data = None
# ============ LOAD SENTINEL-2 DATA ============
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
import pystac_client
import planetary_computer
from odc.stac import load
# If cache not available or invalid, fetch from Microsoft
if s2_data is None:
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
import pystac_client
import planetary_computer
from odc.stac import load
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
s2_search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
)
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ý {len(s2_items)} scenes Sentinel-2..."
s2_data_lazy = load(
s2_items,
bbox=bbox,
chunks={"time": 1, "x": 2048, "y": 2048},
groupby="solar_day",
resolution=config.resolution
)
# Compute s2_data to load into memory (avoid lazy loading from expired URLs)
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2 vào bộ nhớ..."
s2_data = s2_data_lazy.compute()
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (Radar)... ============
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
s2_search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
)
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ý {len(s2_items)} scenes Sentinel-2..."
# Load different bands based on feature mode
if feature_mode == 'simple':
bands_to_load = ["B04", "B08", "SCL"]
else: # temporal or extended
bands_to_load = ["B02", "B03", "B04", "B08", "B11", "SCL"]
s2_data = load(
s2_items,
bbox=bbox,
bands=bands_to_load,
chunks={"time": 1, "x": 2048, "y": 2048},
groupby="solar_day",
resolution=config.resolution
).compute()
prediction_status["progress"] = "Đã load Sentinel-2 data"
# ============ LOAD SENTINEL-1 DATA (RADAR) ============
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
use_radar = False
vh_data = None
vv_data = None
try:
s1_search = catalog.search(
collections=["sentinel-1-rtc"],
bbox=bbox,
datetime=time_range,
)
s1_items = list(s1_search.items())
if s1_items:
s1_items = s1_items[:config.max_scenes]
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
s1_data = load(
s1_items,
bbox=bbox,
bands=["vh", "vv"],
chunks={"time": 1, "x": 2048, "y": 2048},
groupby="sat:absolute_orbit",
groupby="solar_day",
resolution=config.resolution
)
if "vh" in s1_data and "vv" in s1_data:
vh = s1_data["vh"].astype('float32')
vv = s1_data["vv"].astype('float32')
vh_monthly = vh.resample(time="1ME").mean().compute()
vv_monthly = vv.resample(time="1ME").mean().compute()
use_radar = True
else:
vh_monthly = None
vv_monthly = None
use_radar = False
).compute()
# Convert to dB
vh_data = 10 * np.log10(s1_data['vh'].where(s1_data['vh'] > 0))
vv_data = 10 * np.log10(s1_data['vv'].where(s1_data['vv'] > 0))
use_radar = True
prediction_status["progress"] = f"Đã load Sentinel-1 data ({len(s1_items)} scenes)"
else:
vh_monthly = None
vv_monthly = None
use_radar = False
# Lưu cache
joblib.dump({
"s2_data": s2_data,
"s2_items": s2_items,
"vh_monthly": vh_monthly,
"vv_monthly": vv_monthly,
"use_radar": use_radar
}, cache_file)
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, bỏ qua radar features"
except Exception as e:
prediction_status["progress"] = f"Lỗi load Sentinel-1: {str(e)}, bỏ qua radar features"
# ============ BƯỚC 2: TÍNH NDVI VÀ XỬ LÝ MÂY ============
prediction_status["progress"] = "Đang tính toán NDVI và xử lý mây..."
# Calculate NDVI using Sentinel-2 band names (B08 = NIR, B04 = Red)
nir = s2_data["B08"].astype('float32')
red = s2_data["B04"].astype('float32')
ndvi = (nir - red) / (nir + red + 1e-8)
# Mask clouds using SCL band if available
# ============ APPLY CLOUD MASK ============
prediction_status["progress"] = "Đang xử lý mây..."
if "SCL" in s2_data:
scl = s2_data["SCL"]
# SCL values: 4=vegetation, 5=bare soil, 6=water - these are clear
# 3=cloud shadow, 8=cloud medium, 9=cloud high, 10=cirrus - mask these
# SCL values: 3=cloud shadow, 8=cloud medium, 9=cloud high, 10=cirrus
cloud_mask = (scl == 3) | (scl == 8) | (scl == 9) | (scl == 10)
ndvi = ndvi.where(~cloud_mask)
for band in s2_data.data_vars:
if band != "SCL":
s2_data[band] = s2_data[band].where(~cloud_mask)
# ============ BƯỚC 3: ĐIỀN GIÁ TRỊ NAN (FILL NAN) ============
prediction_status["progress"] = "Đang điền giá trị bị che mây..."
# ============ EXTRACT FEATURES ============
prediction_status["progress"] = f"Đang trích xuất features (mode={feature_mode})..."
# Fill NaN using forward fill and backward fill
ndvi_filled = ndvi.ffill(dim='time').bfill(dim='time')
# Resample to monthly average
prediction_status["progress"] = "Đang tính trung bình NDVI theo tháng..."
ndvi_monthly = ndvi_filled.resample(time="1ME").mean()
# Compute NDVI (convert from dask to numpy)
ndvi_monthly = ndvi_monthly.compute()
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (VH, VV) ============
# Only load radar if not already in cache
if not cache_file.exists() or (cache_file.exists() and not use_radar):
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
try:
# Initialize catalog if not already done
if not cache_file.exists():
pass
else:
import pystac_client
import planetary_computer
from odc.stac import load
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
# Search Sentinel-1 data
s1_search = catalog.search(
collections=["sentinel-1-rtc"],
bbox=bbox,
datetime=time_range,
)
s1_items = list(s1_search.items())
if s1_items:
s1_items = s1_items[:config.max_scenes]
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
try:
s1_data = load(
s1_items,
bbox=bbox,
chunks={"time": 1, "x": 2048, "y": 2048},
groupby="sat:absolute_orbit",
resolution=config.resolution
)
if "vh" in s1_data and "vv" in s1_data:
vh = s1_data["vh"].astype('float32')
vv = s1_data["vv"].astype('float32')
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
try:
vh_monthly = vh.resample(time="1ME").mean().compute()
vv_monthly = vv.resample(time="1ME").mean().compute()
use_radar = True
except Exception as radar_exc:
print(f"[RADAR WARNING] Không thể tính radar monthly: {radar_exc}")
vh_monthly = None
vv_monthly = None
use_radar = False
else:
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
use_radar = False
except Exception as radar_exc:
print(f"[RADAR WARNING] Không thể tải dữ liệu Sentinel-1: {radar_exc}")
vh_monthly = None
vv_monthly = None
use_radar = False
else:
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
use_radar = False
except Exception as radar_exc:
print(f"[RADAR WARNING] Không thể truy cập Sentinel-1: {radar_exc}")
prediction_status["progress"] = "Không thể truy cập Sentinel-1, tiếp tục với NDVI..."
vh_monthly = None
vv_monthly = None
use_radar = False
# ============ BƯỚC 5: CHUẨN BỊ FEATURES CHO DỰ ĐOÁN ============
prediction_status["progress"] = "Đang chuẩn bị features cho dự đoán..."
# Get shape information
n_times_ndvi = len(ndvi_monthly.time)
y_size = len(ndvi_monthly.y)
x_size = len(ndvi_monthly.x)
n_pixels = y_size * x_size
# Prepare NDVI features (flatten each time step)
ndvi_features = []
for t in range(n_times_ndvi):
ndvi_t = ndvi_monthly.isel(time=t).values.flatten()
ndvi_features.append(ndvi_t)
# Stack NDVI features
features = np.column_stack(ndvi_features)
# Add radar features if available
if use_radar:
n_times_vh = len(vh_monthly.time)
n_times_vv = len(vv_monthly.time)
# Add VH features
for t in range(min(n_times_vh, n_times_ndvi)):
vh_t = vh_monthly.isel(time=t).values.flatten()
# Resize if needed
if len(vh_t) != n_pixels:
vh_t = np.resize(vh_t, n_pixels)
features = np.column_stack([features, vh_t])
# Add VV features
for t in range(min(n_times_vv, n_times_ndvi)):
vv_t = vv_monthly.isel(time=t).values.flatten()
# Resize if needed
if len(vv_t) != n_pixels:
vv_t = np.resize(vv_t, n_pixels)
features = np.column_stack([features, vv_t])
# Handle NaN values in features✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch
# Always fill NaN for all bands in s2_data if present
for band in ["B02", "B03", "B04", "B08", "B11"]:
if band in s2_data:
s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time')
# Calculate NDVI if needed (for simple mode)
ndvi_filled = None
if feature_mode == 'simple' and 'B08' in s2_data and 'B04' in s2_data:
nir = s2_data["B08"].astype('float32')
red = s2_data["B04"].astype('float32')
ndvi = (nir - red) / (nir + red + 1e-8)
ndvi_filled = ndvi.ffill(dim='time').bfill(dim='time')
# Extract features using FeatureExtractor, always pass all possible data
features = extractor.extract(
s2_data=s2_data,
ndvi_data=ndvi_filled,
vh_data=vh_data,
vv_data=vv_data
)
# Handle NaN values
features = np.nan_to_num(features, nan=0.0)
# Ensure features shape matches model expectation
if features.shape[1] != n_features_expected:
raise ValueError(f"Số lượng features ({features.shape[1]}) không khớp với model ({n_features_expected}). Hãy kiểm tra lại cấu hình trích xuất đặc trưng và metadata của model.")
prediction_status["progress"] = f"Đã extract {features.shape[1]} features cho {features.shape[0]} pixels"
# ============ BƯỚC 6: DỰ ĐOÁN ============
# Check model's expected feature count and adjust
try:
# Get expected number of features from model
if is_cnn_model:
# For PyTorch CNN, get n_features from model
expected_features = model.n_features
elif hasattr(model, 'n_features_in_'):
expected_features = model.n_features_in_
elif hasattr(model, 'feature_names_in_'):
expected_features = len(model.feature_names_in_)
else:
# Try to get from booster for XGBoost
try:
expected_features = model.get_booster().num_features()
except:
expected_features = features.shape[1]
prediction_status["progress"] = f"Model cần {expected_features} features, đang có {features.shape[1]} features..."
# Adjust features to match model
if features.shape[1] > expected_features:
# Trim to expected number (use only first N features - NDVI only)
prediction_status["progress"] = f"Cắt bớt features từ {features.shape[1]} xuống {expected_features}..."
features = features[:, :expected_features]
elif features.shape[1] < expected_features:
# Pad with zeros or repeat last features
prediction_status["progress"] = f"Thêm features từ {features.shape[1]} lên {expected_features}..."
n_missing = expected_features - features.shape[1]
# Repeat last feature column to fill
padding = np.tile(features[:, -1:], (1, n_missing))
features = np.column_stack([features, padding])
except Exception as e:
prediction_status["progress"] = f"Không thể xác định số features của model, tiếp tục với {features.shape[1]} features..."
prediction_status["progress"] = f"Đang dự đoán với {features.shape[1]} features..."
# ============ PREDICT ============
prediction_status["progress"] = "Đang dự đoán..."
# Make prediction
if is_cnn_model:
# PyTorch CNN prediction
predictions = model.predict(features)
else:
predictions = model.predict(features)
@@ -1104,23 +1014,31 @@ async def run_prediction(config: PredictionConfig):
# Decode labels if label_encoder exists
if label_encoder is not None:
try:
predictions = label_encoder.inverse_transform(predictions)
predictions = label_encoder.inverse_transform(predictions.astype(int))
except:
pass # Keep numeric predictions if inverse_transform fails
pass
# Reshape to original shape
if feature_mode == 'simple' and 'B08' in s2_data:
# Use B08 to get shape
y_size = len(s2_data.y)
x_size = len(s2_data.x)
else:
y_size = len(s2_data.y)
x_size = len(s2_data.x)
pred_shape = (y_size, x_size)
predictions_2d = predictions.reshape(pred_shape)
# ============ BƯỚC 7: TẠO OUTPUT VÀ LƯU KẾT QUẢ ============
# ============ CREATE OUTPUT ============
prediction_status["progress"] = "Đang tạo bản đồ phân loại..."
# Create output xarray
prediction_da = xr.DataArray(
predictions_2d,
coords={
"y": ndvi_monthly.y,
"x": ndvi_monthly.x
"y": s2_data.y,
"x": s2_data.x
},
dims=["y", "x"],
name="classification"
@@ -1159,10 +1077,73 @@ async def run_prediction(config: PredictionConfig):
ax.set_title(f'Prediction Result - {timestamp}', fontsize=14, fontweight='bold')
ax.set_xlabel('X (pixels)', fontsize=10)
ax.set_ylabel('Y (pixels)', fontsize=10)
# Add colorbar
# Build mapping from numeric class value -> display label
class_map = None
try:
# 1) Try label_encoder (preferred)
if label_encoder is not None:
try:
# label_encoder.classes_ may be strings or numbers
le_classes = list(label_encoder.classes_)
# If classes are strings like names, we'll map indices -> names
if all(isinstance(x, str) for x in le_classes):
class_map = {i: name for i, name in enumerate(le_classes)}
else:
# If classes are numeric labels matching values, map value->str(value)
class_map = {int(v): str(v) for v in le_classes}
except Exception:
class_map = None
except Exception:
class_map = None
# 2) Try model metadata 'class_names' (list ordered by class code)
if class_map is None and isinstance(model_metadata, dict):
try:
cn = model_metadata.get('class_names')
if isinstance(cn, list):
class_map = {i: str(name) for i, name in enumerate(cn)}
except Exception:
pass
# 3) Try invert label_mapping in metadata if exists (name->code)
if class_map is None and isinstance(model_metadata, dict):
try:
lm = model_metadata.get('label_mapping') or model_metadata.get('labels')
if isinstance(lm, dict):
# invert mapping: code -> name
inv = {}
for k, v in lm.items():
try:
key_int = int(v)
except Exception:
continue
inv[key_int] = str(k)
if inv:
class_map = inv
except Exception:
pass
# Create colorbar
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.set_label('Class', rotation=270, labelpad=15)
# If we have a class_map, set ticks and labels
try:
if class_map:
vals = np.array(sorted(class_map.keys()))
cbar.set_ticks(vals)
cbar.set_ticklabels([class_map[int(v)] for v in vals])
else:
# fallback: label numeric ticks from min..max
if np.issubdtype(predictions_2d.dtype, np.number):
minv = int(np.nanmin(predictions_2d))
maxv = int(np.nanmax(predictions_2d))
ticks = np.arange(minv, maxv + 1)
cbar.set_ticks(ticks)
cbar.set_ticklabels([str(t) for t in ticks])
except Exception:
pass
# Add grid
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
@@ -1192,7 +1173,7 @@ async def run_prediction(config: PredictionConfig):
"bbox": bbox,
"time_range": time_range,
"n_features": features.shape[1],
"n_times_ndvi": n_times_ndvi,
"feature_mode": feature_mode,
"used_radar": use_radar,
"model_used": config.model_filename
}
@@ -1669,25 +1650,15 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
job["progress"] = 15
# Load model
model_path = Path("model_train") / config.model_filename
if not model_path.exists():
raise FileNotFoundError(f"Model không tồn tại: {config.model_filename}")
# Load model using ModelManager
model_manager = get_model_manager()
model, label_encoder, model_metadata = model_manager.load_model(config.model_filename)
model_data = joblib.load(model_path)
if isinstance(model_data, dict):
model = model_data.get('model')
label_encoder = model_data.get('label_encoder')
else:
model = model_data
label_encoder = None
# Check if it's a CNN model
is_cnn_model = hasattr(model, '__class__') and 'CNN' in model.__class__.__name__
job["progress"] = 20
# Check if CNN model
is_cnn_model = hasattr(model, '__class__') and 'CNN' in model.__class__.__name__
# Load data from Microsoft Planetary Computer
import pystac_client
import planetary_computer
@@ -1838,6 +1809,50 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.set_label('Class', rotation=270, labelpad=15)
try:
# Build mapping from numeric class value -> label (reuse logic from above)
class_map = None
if label_encoder is not None:
try:
le_classes = list(label_encoder.classes_)
if all(isinstance(x, str) for x in le_classes):
class_map = {i: name for i, name in enumerate(le_classes)}
else:
class_map = {int(v): str(v) for v in le_classes}
except Exception:
class_map = None
if class_map is None and isinstance(model_metadata, dict):
cn = model_metadata.get('class_names')
if isinstance(cn, list):
class_map = {i: str(name) for i, name in enumerate(cn)}
if class_map is None and isinstance(model_metadata, dict):
lm = model_metadata.get('label_mapping') or model_metadata.get('labels')
if isinstance(lm, dict):
inv = {}
for k, v in lm.items():
try:
key_int = int(v)
except Exception:
continue
inv[key_int] = str(k)
if inv:
class_map = inv
if class_map:
vals = np.array(sorted(class_map.keys()))
cbar.set_ticks(vals)
cbar.set_ticklabels([class_map[int(v)] for v in vals])
else:
if np.issubdtype(predictions_2d.dtype, np.number):
minv = int(np.nanmin(predictions_2d))
maxv = int(np.nanmax(predictions_2d))
ticks = np.arange(minv, maxv + 1)
cbar.set_ticks(ticks)
cbar.set_ticklabels([str(t) for t in ticks])
except Exception:
pass
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.tight_layout()
@@ -1955,20 +1970,12 @@ async def change_detection_predict_workflow(
"""
try:
# --- STEP 1: LOAD MODEL ---
model_path = Path("model_train") / model_filename
if not model_path.exists():
raise HTTPException(status_code=404, detail=f"Model not found: {model_filename}")
model_data = joblib.load(model_path)
if isinstance(model_data, dict):
model = model_data.get('model')
label_encoder = model_data.get('label_encoder')
else:
model = model_data
label_encoder = None
model_manager = get_model_manager()
model, label_encoder, model_metadata = model_manager.load_model(model_filename)
print(f"[CHANGE DETECTION] Loaded model: {model_filename}")
print(f" - Type: {model_metadata.get('model_type', 'unknown')}")
print(f" - Features: {model_metadata.get('features', [])}")
# --- STEP 2: LOAD SENTINEL-2 DATA ---
bbox = [min_lon, min_lat, max_lon, max_lat]
@@ -2580,22 +2587,13 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
try:
import numpy as np
# Load model
model_path = Path(f"model_train/{config.model_filename}")
if not model_path.exists():
raise HTTPException(status_code=404, detail=f"Model {config.model_filename} không tồn tại")
model_data = joblib.load(model_path)
# Extract model from dict (models are saved as {'model': xgb_model, 'label_encoder': encoder})
if isinstance(model_data, dict):
model = model_data.get('model')
label_encoder = model_data.get('label_encoder')
else:
model = model_data
label_encoder = None
# Load model using ModelManager
model_manager = get_model_manager()
model, label_encoder, model_metadata = model_manager.load_model(config.model_filename)
print(f"[PREDICT+NDVI] Loaded model: {config.model_filename}")
print(f" - Type: {model_metadata.get('model_type', 'unknown')}")
print(f" - Features: {model_metadata.get('features', [])}")
# Check cache first
cache_dir = Path("dataset_cache")
@@ -2662,13 +2660,17 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
print(f"[PREDICT+NDVI] Loaded data shape: {data.dims}")
# Get expected number of features from model metadata
expected_n_features = model_metadata.get("n_features", 3)
print(f"[PREDICT+NDVI] Model expects {expected_n_features} features")
# Calculate NDVI and other indices
blue = data["B02"].values
green = data["B03"].values
red = data["B04"].values
nir = data["B08"].values
# Calculate indices
# Calculate indices for each time step
# NDVI = (NIR - Red) / (NIR + Red)
ndvi = (nir - red) / (nir + red + 1e-8)
@@ -2679,17 +2681,66 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
ndbi = (red - nir) / (red + nir + 1e-8)
# Prepare features for prediction
# Assuming model was trained with [NDVI, NDWI, NDBI] features
height, width = ndvi.shape[1:3] # Skip time dimension
n_pixels = height * width
n_times = ndvi.shape[0]
# Average over time dimension
ndvi_mean = np.nanmean(ndvi, axis=0)
ndwi_mean = np.nanmean(ndwi, axis=0)
ndbi_mean = np.nanmean(ndbi, axis=0)
print(f"[PREDICT+NDVI] Data has {n_times} time steps, spatial size: {height}x{width}")
# Reshape for prediction
features = np.stack([ndvi_mean.flatten(), ndwi_mean.flatten(), ndbi_mean.flatten()], axis=1)
# Build features based on what model expects
# Model metadata should tell us what features were used
model_features = model_metadata.get("features", ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"])
# If model was trained with temporal features (multiple time steps)
if expected_n_features > 10: # Likely temporal features
print(f"[PREDICT+NDVI] Building temporal features (all time steps)")
# Use all time steps for each index
feature_list = []
# Add NDVI for each time step
for t in range(n_times):
feature_list.append(ndvi[t].flatten())
# If model has more features, add NDWI and NDBI time series
if expected_n_features >= n_times * 2:
for t in range(n_times):
feature_list.append(ndwi[t].flatten())
if expected_n_features >= n_times * 3:
for t in range(n_times):
feature_list.append(ndbi[t].flatten())
features = np.stack(feature_list, axis=1)
# Adjust to match expected features
if features.shape[1] < expected_n_features:
# Pad with mean values
n_missing = expected_n_features - features.shape[1]
padding = np.tile(features[:, -1:], (1, n_missing))
features = np.column_stack([features, padding])
elif features.shape[1] > expected_n_features:
# Trim to expected
features = features[:, :expected_n_features]
else:
# Use mean values (aggregate features)
print(f"[PREDICT+NDVI] Building aggregate features (mean values)")
# Average over time dimension
ndvi_mean = np.nanmean(ndvi, axis=0)
ndwi_mean = np.nanmean(ndwi, axis=0)
ndbi_mean = np.nanmean(ndbi, axis=0)
# Reshape for prediction
features = np.stack([ndvi_mean.flatten(), ndwi_mean.flatten(), ndbi_mean.flatten()], axis=1)
# Adjust to match expected features if needed
if features.shape[1] < expected_n_features:
n_missing = expected_n_features - features.shape[1]
padding = np.tile(features[:, -1:], (1, n_missing))
features = np.column_stack([features, padding])
elif features.shape[1] > expected_n_features:
features = features[:, :expected_n_features]
print(f"[PREDICT+NDVI] Built features shape: {features.shape}")
# Handle NaN values
valid_mask = ~np.isnan(features).any(axis=1)