Files
remote-sensing/.temp/01.train_ODC.py
T

234 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
get_ipython().run_cell_magic('time', '', '%matplotlib inline\n\nimport importlib\nimport new_import_ODC \n\nimportlib.reload(new_import_ODC)\n\nfrom new_import_ODC import *\n\nprint("✅ All modules loaded successfully")\n')
# In[2]:
get_ipython().run_cell_magic('time', '', 'import os\nimport sys\n\nprint("✅ AWS credentials loaded from environment variables")\n\n# Cấu hình Dask local\nfrom dask.distributed import Client, LocalCluster\n\ncluster = LocalCluster(n_workers=4)\nclient = Client(cluster)\nprint("✅ Dask cluster initialized")\nprint(f" Cluster: {cluster}")\n\n# Khai báo Datacube (chỉ để lấy metadata, không dùng load())\nimport datacube\ntry:\n dc = datacube.Datacube()\n print("✅ Datacube connected (metadata only)")\nexcept Exception as e:\n print(f"⚠️ Datacube connection not critical: {e}")\n dc = None\n\nprint("\\n" + "="*70)\n')
# In[3]:
get_ipython().run_cell_magic('time', '', '# 🔧 Get Sentinel-2 scene metadata from datacube\nprint("="*70)\nprint("GETTING SENTINEL-2 SCENE METADATA")\nprint("="*70)\n\ndate_range = ("2023-03-01", "2023-12-31")\nlongtitude_range = (105.5, 106.4)\nlatitude_range = (9.2, 10.0)\n\ntry:\n print(f"\\n[1] Loading metadata from datacube...")\n datasets = list(dc.find_datasets(product=\'s2_l2a\', time=date_range))\n print(f" ✅ Found {len(datasets)} scenes")\n \n if datasets:\n selected = datasets[0]\n print(f"\\n[2] Selected scene: {selected.metadata.label}")\n scene_datetime = selected.time.begin if hasattr(selected.time, \'begin\') else selected.time\n print(f" Date: {scene_datetime}")\n \n # Display measurement paths\n print(f"\\n[3] Available bands:")\n for name, measurement in selected.measurements.items():\n print(f" - {name}: {measurement[\'path\'][:80]}")\n \nexcept Exception as e:\n print(f"❌ Error: {e}")\n import traceback\n traceback.print_exc()\n\nprint("="*70)\n')
# In[4]:
get_ipython().run_cell_magic('time', '', '# 🔍 CHECK IF DATASET CACHE EXISTS (Skip download if available)\nprint("="*70)\nprint("CHECKING FOR CACHED DATASET")\nprint("="*70)\n\nimport os\nimport xarray as xr\n\ncache_dir = "dataset_cache"\ncache_file = f"{cache_dir}/sentinel2_timeseries_40scenes.nc"\n\nuse_cache = False\n\nif os.path.exists(cache_file):\n print(f"\\n✅ Cache file found: {cache_file}")\n \n # Get file info\n file_size_gb = os.path.getsize(cache_file) / (1024**3)\n print(f" File size: {file_size_gb:.2f} GB")\n \n # Try to load\n try:\n print(f"\\n🔄 Loading dataset from cache...")\n data = xr.open_dataset(cache_file)\n \n print(f"✅ Dataset loaded from cache!")\n print(f" Total scenes: {len(data[\'time\'])}")\n print(f" Variables: {len(data.data_vars)}")\n print(f" Dimensions: {dict(data.dims)}")\n print(f"\\n ⏭️ Skipping S3 download (using cached data)")\n \n use_cache = True\n \n except Exception as e:\n print(f"❌ Error loading cache: {e}")\n print(f" Will download fresh data from S3")\n use_cache = False\nelse:\n print(f"\\n⏳ Cache file not found: {cache_file}")\n print(f" Will download from S3 and save cache")\n print(f" (Next run will use cache automatically)")\n\nprint("="*70)\n')
# In[5]:
get_ipython().run_cell_magic('time', '', '# 💾 LOAD SENTINEL-2 DATA DIRECTLY FROM S3 COGS USING RASTERIO - WITH TEMPORAL FEATURES\nprint("="*70)\nprint("LOADING SENTINEL-2 DATA FROM S3 COGs (RASTERIO) - OPTIMAL ACCURACY")\nprint("="*70)\n\ntry:\n import rasterio\n import xarray as xr\n import numpy as np\n from scipy import ndimage\n \n # ===== CHECK IF SHOULD SKIP DOWNLOAD =====\n if use_cache and data is not None:\n print(f"\\n✅ Using cached dataset - skipping download!")\n print(f" Variables: {len(data.data_vars)}")\n print(f" Shape: {data.dims}")\n display(data)\n \n else:\n # ===== DOWNLOAD FROM S3 =====\n print(f"\\n📥 Downloading from S3...")\n \n # Get all scenes from datacube metadata\n datasets = list(dc.find_datasets(\n product=\'s2_l2a\',\n time=date_range\n ))\n \n if not datasets:\n raise ValueError("No datasets found for date range")\n \n print(f"\\n📦 Found {len(datasets)} available scenes")\n print(f" Date range: {date_range[0]} to {date_range[1]}")\n \n # ===== LOAD ALL SCENES WITH ALL AVAILABLE BANDS (NO MAGNIFICATION) =====\n print(f"\\n[LOADING] Loading ALL {len(datasets)} scenes with ALL available bands...")\n print(f" (Keeping NATIVE resolution - NO upsampling/magnification)")\n \n # num_scenes = len(datasets) # Load ALL scenes\n num_scenes = 1 # Load ALL scenes\n all_data_dict = {}\n failed_scenes = []\n scene_dates = []\n \n # Discover all available bands from first scene\n first_scene = datasets[0]\n all_available_bands = list(first_scene.measurements.keys())\n print(f" Available bands: {all_available_bands}")\n \n for scene_idx in range(num_scenes):\n selected = datasets[scene_idx]\n scene_label = selected.metadata.label\n scene_datetime = selected.time.begin if hasattr(selected.time, \'begin\') else selected.time\n scene_dates.append(scene_datetime)\n \n # Print progress every 5 scenes\n if scene_idx % 5 == 0 or scene_idx == 0 or scene_idx == num_scenes - 1:\n print(f"\\n [{scene_idx + 1:2d}/{num_scenes}] {scene_label} ({scene_datetime.date()})")\n \n # Load ALL available bands from S3 COGs\n scene_data_dict = {}\n \n for band_name in all_available_bands:\n if band_name in selected.measurements:\n band_path = selected.measurements[band_name][\'path\']\n \n try:\n with rasterio.open(band_path) as src:\n data_band = src.read(1)\n scene_data_dict[band_name] = data_band\n except Exception as e:\n if scene_idx % 5 == 0:\n print(f" ⚠️ Error loading {band_name}: {str(e)[:30]}")\n failed_scenes.append((scene_idx, scene_label, band_name, str(e)))\n \n if scene_data_dict:\n all_data_dict[scene_idx] = scene_data_dict\n if scene_idx % 5 == 0 or scene_idx == num_scenes - 1:\n print(f" ✅ {len(scene_data_dict)} bands loaded")\n else:\n failed_scenes.append((scene_idx, scene_label, "all", "No bands loaded"))\n \n if not all_data_dict:\n raise ValueError("Could not load any bands from any scene")\n \n print(f"\\n✅ Successfully loaded {len(all_data_dict)} scenes!")\n if failed_scenes:\n print(f"⚠️ Failed to load {len(failed_scenes)} band instances (will be skipped)")\n \n # ===== NORMALIZE RESOLUTION (No upsampling - just match to highest) =====\n print(f"\\n[RESOLUTION NORMALIZATION] Aligning all bands to native resolution (NO magnification)...")\n \n # Find max resolution\n ref_resolution = None\n max_size = 0\n max_band = None\n \n for scene_idx in all_data_dict.keys():\n for band_name, data_band in all_data_dict[scene_idx].items():\n size = data_band.shape[0]\n if size > max_size:\n max_size = size\n ref_resolution = size\n max_band = band_name\n \n print(f" Reference resolution: {max_size}×{max_size} pixels (native {max_band})")\n \n # Resample all bands to match reference resolution (both up and down)\n resampled_count = 0\n for scene_idx in all_data_dict.keys():\n for band_name in list(all_data_dict[scene_idx].keys()):\n band_data_arr = all_data_dict[scene_idx][band_name]\n current_size = band_data_arr.shape[0]\n \n if current_size != ref_resolution:\n scale_factor = ref_resolution / current_size\n \n # Resample to match reference resolution (both up and down)\n if band_name == \'scl\':\n resampled_data = ndimage.zoom(band_data_arr, scale_factor, order=0)\n else:\n resampled_data = ndimage.zoom(band_data_arr, scale_factor, order=1)\n \n all_data_dict[scene_idx][band_name] = resampled_data\n new_size = resampled_data.shape[0]\n if scene_idx == 0: # Print for first scene only to reduce clutter\n print(f" Resampling {band_name}: {current_size}×{current_size}{new_size}×{new_size}")\n resampled_count += 1\n \n print(f"✅ Resolution normalization complete! ({resampled_count} bands resampled)")\n \n # ===== CALCULATE SPECTRAL INDICES FOR EACH SCENE =====\n print(f"\\n[SPECTRAL INDICES] Calculating spectral indices for each scene...")\n \n indices_count = 0\n for scene_idx in all_data_dict.keys():\n scene_data = all_data_dict[scene_idx]\n \n try:\n # NDVI: (NIR - Red) / (NIR + Red)\n if \'nir\' in scene_data and \'red\' in scene_data:\n nir = scene_data[\'nir\'].astype(float)\n red = scene_data[\'red\'].astype(float)\n ndvi = (nir - red) / (nir + red + 1e-8)\n scene_data[\'ndvi\'] = ndvi.astype(np.float32)\n indices_count += 1\n \n # NDBI: (SWIR - NIR) / (SWIR + NIR)\n if \'b11\' in scene_data and \'nir\' in scene_data:\n swir = scene_data[\'b11\'].astype(float)\n nir = scene_data[\'nir\'].astype(float)\n ndbi = (swir - nir) / (swir + nir + 1e-8)\n scene_data[\'ndbi\'] = ndbi.astype(np.float32)\n indices_count += 1\n \n # NDWI: (NIR - SWIR) / (NIR + SWIR)\n if \'nir\' in scene_data and \'b11\' in scene_data:\n nir = scene_data[\'nir\'].astype(float)\n swir = scene_data[\'b11\'].astype(float)\n ndwi = (nir - swir) / (nir + swir + 1e-8)\n scene_data[\'ndwi\'] = ndwi.astype(np.float32)\n indices_count += 1\n \n # EVI: Enhanced Vegetation Index\n if \'nir\' in scene_data and \'red\' in scene_data and \'blue\' in scene_data:\n nir = scene_data[\'nir\'].astype(float)\n red = scene_data[\'red\'].astype(float)\n blue = scene_data[\'blue\'].astype(float)\n evi = 2.5 * (nir - red) / (nir + 6*red - 7.5*blue + 1)\n scene_data[\'evi\'] = evi.astype(np.float32)\n indices_count += 1\n \n except Exception as e:\n pass\n \n print(f"✅ Calculated {indices_count} spectral indices per scene")\n \n # ===== STACK SCENES ALONG TIME DIMENSION =====\n print(f"\\n[STACKING] Stacking all {len(all_data_dict)} scenes to create time-series...")\n \n data_vars = {}\n band_names = list(all_data_dict[0].keys())\n \n for band_name in band_names:\n band_data_list = []\n for scene_idx in sorted(all_data_dict.keys()):\n if band_name in all_data_dict[scene_idx]:\n band_data_list.append(all_data_dict[scene_idx][band_name])\n \n if band_data_list:\n stacked = np.stack(band_data_list, axis=0)\n data_vars[band_name] = ([\'time\', \'y\', \'x\'], stacked)\n \n # Create xarray Dataset with time dimension\n first_band_data = list(all_data_dict[0].values())[0]\n y_size, x_size = first_band_data.shape\n \n data = xr.Dataset(\n data_vars,\n coords={\n \'time\': np.arange(len(all_data_dict)),\n \'x\': np.arange(x_size),\n \'y\': np.arange(y_size)\n }\n )\n \n # ===== CALCULATE TEMPORAL FEATURES FOR ACCURACY =====\n print(f"\\n[TEMPORAL FEATURES] Computing temporal features from time-series...")\n \n temporal_features_added = 0\n \n # For NDVI: temporal statistics\n if \'ndvi\' in data.data_vars:\n ndvi_ts = data[\'ndvi\']\n \n # Min NDVI (vegetation stress indicator)\n data[\'ndvi_min\'] = ndvi_ts.min(dim=\'time\')\n temporal_features_added += 1\n \n # Max NDVI (peak vegetation)\n data[\'ndvi_max\'] = ndvi_ts.max(dim=\'time\')\n temporal_features_added += 1\n \n # Mean NDVI\n data[\'ndvi_mean\'] = ndvi_ts.mean(dim=\'time\')\n temporal_features_added += 1\n \n # NDVI range (variability)\n data[\'ndvi_range\'] = data[\'ndvi_max\'] - data[\'ndvi_min\']\n temporal_features_added += 1\n \n # NDVI std (temporal consistency)\n data[\'ndvi_std\'] = ndvi_ts.std(dim=\'time\')\n temporal_features_added += 1\n \n # For all indices: mean values (aggregate features)\n for band_name in [\'ndbi\', \'ndwi\', \'evi\']:\n if band_name in data.data_vars:\n band_ts = data[band_name]\n data[f\'{band_name}_mean\'] = band_ts.mean(dim=\'time\')\n temporal_features_added += 1\n \n print(f"✅ Added {temporal_features_added} temporal/aggregate features")\n \n # ===== SAVE TO CACHE =====\n print(f"\\n[CACHE] Saving dataset to cache...")\n try:\n data.to_netcdf(cache_file, engine=\'netcdf4\')\n cache_size = os.path.getsize(cache_file) / (1024**3)\n print(f"✅ Dataset saved to cache: {cache_file}")\n print(f" Cache size: {cache_size:.2f} GB")\n except Exception as e:\n print(f"⚠️ Error saving cache: {e}")\n \n print(f"\\n✅ OPTIMAL Dataset with native resolution + temporal features created!")\n print(f" {\'=\'*70}")\n print(f" 🎬 Total scenes (time steps): {len(all_data_dict)}")\n print(f" 📊 Total bands/variables: {len(data.data_vars)}")\n print(f" 🖼️ Spatial size: {x_size} × {y_size} pixels (NATIVE resolution)")\n print(f" 📏 Native resolution: 10m (Sentinel-2 L2A)")\n print(f" ⏰ Temporal range: {scene_dates[0].date()} to {scene_dates[-1].date()}")\n print(f" 💾 Total dataset size: {notebook_utils.xarray_object_size(data)}")\n print(f" 💿 Cached at: {cache_file}")\n print(f" {\'=\'*70}")\n \n print(f"\\n Dataset dimensions:")\n for dim, size in data.dims.items():\n print(f" {dim}: {size}")\n \n print(f"\\n Variables ({len(data.data_vars)}):")\n spatial_vars = []\n temporal_vars = []\n for var_name in sorted(data.data_vars):\n if len(data[var_name].shape) == 3:\n spatial_vars.append(f"{var_name} {data[var_name].shape}")\n else:\n temporal_vars.append(f"{var_name} {data[var_name].shape}")\n \n print(f" Spatial time-series ({len(spatial_vars)}):")\n for v in spatial_vars:\n print(f" - {v}")\n print(f" Temporal aggregates ({len(temporal_vars)}):")\n for v in temporal_vars:\n print(f" - {v}")\n \n print(f" {\'=\'*70}")\n \n display(data)\n \n # ===== EXTRACT NDVI FOR TRAINING =====\n print(f"\\n[NDVI EXTRACTION] Extracting NDVI for model training...")\n if \'ndvi_mean\' in data.data_vars:\n # Use mean NDVI across time\n ndvi = data[\'ndvi_mean\']\n print(f"✅ NDVI extracted (mean across time)")\n print(f" Shape: {ndvi.shape}")\n elif \'ndvi\' in data.data_vars:\n # Use first time step if mean not available\n ndvi = data[\'ndvi\'].isel(time=0)\n print(f"✅ NDVI extracted (first time step)")\n print(f" Shape: {ndvi.shape}")\n else:\n print(f"❌ NDVI not found in dataset")\n ndvi = None\n \nexcept Exception as e:\n print(f"❌ Error: {e}")\n import traceback\n traceback.print_exc()\n data = None\n ndvi = None\n\nprint("="*70)\n')
# In[6]:
# 🎯 LOAD TRAINING DATA & EXTRACT FEATURES
print("="*70)
print("TRAINING DATA SETUP")
print("="*70)
# Load training points
train_path = "train/ST_training data_updated_1130points_new.shp"
print(f"\n[1] Loading training data: {train_path}")
try:
train = load_train_data(train_path)
print(f" ✅ Loaded {len(train)} training points")
print(f" Columns: {list(train.columns)}")
train.head()
except Exception as e:
print(f" ❌ Error: {e}")
train = None
# Label mapping
label_mapping = {
"Lua tom": "0",
"Lua": "1",
"CHN": "2",
"CLN": "3",
"TS": "4",
"Song": "5",
"Dat xay dung": "6",
"Rung": "7",
}
print(f"\n[2] Label mapping:")
for label, code in label_mapping.items():
print(f" {code}: {label}")
print("\n" + "="*70)
# In[ ]:
get_ipython().run_cell_magic('time', '', '# 🤖 LAND USE CLASSIFICATION MODEL TRAINING (MỤC TIÊU CHÍNH)\nprint("="*70)\nprint("LAND USE CLASSIFICATION TRAINING")\nprint("="*70)\nprint("\\n🎯 Mục tiêu: Dự đoán phân loại sử dụng đất (8 lớp)")\nprint(" - NDVI/NDWI/NDBI/EVI là INPUT FEATURES")\nprint(" - Sau khi predict xong → có thể hiển thị NDVI map như chỉ số phụ")\nprint("="*70)\n\nif train is not None and data is not None:\n print("\\n[1] Extracting MULTIPLE features from satellite data...")\n print(" (Sử dụng nhiều spectral indices để cải thiện accuracy)")\n \n try:\n # Extract features at training point locations\n X = []\n y = []\n \n # Available features from data\n available_features = [\'ndvi_mean\', \'ndvi_min\', \'ndvi_max\', \'ndvi_std\', \'ndvi_range\',\n \'ndwi_mean\', \'ndbi_mean\', \'evi_mean\']\n \n # Check which features are actually available\n features_to_use = [f for f in available_features if f in data.data_vars]\n \n if not features_to_use:\n print(" ❌ No spectral features found in dataset!")\n print(" Available variables:", list(data.data_vars))\n model = None\n else:\n print(f" Using {len(features_to_use)} features: {features_to_use}")\n \n for idx, point in train.iterrows():\n try:\n # Extract all available features at this point\n feature_vec = []\n for feat_name in features_to_use:\n feat_val = float(data[feat_name].sel(\n x=point.geometry.x, \n y=point.geometry.y, \n method=\'nearest\'\n ).values)\n feature_vec.append(feat_val)\n \n # Get label\n label = label_mapping[point.Hientrang]\n \n # Only add if no NaN values\n if not np.isnan(feature_vec).any():\n X.append(feature_vec)\n y.append(int(label))\n except Exception as e:\n # Skip points with errors\n continue\n \n if len(X) > 0:\n X = np.array(X)\n y = np.array(y)\n print(f" ✅ Extracted {len(X)} samples with {X.shape[1]} features each")\n \n # Show feature statistics\n print(f"\\n Feature statistics:")\n for i, feat_name in enumerate(features_to_use):\n print(f" {feat_name:15s}: mean={X[:,i].mean():.3f}, std={X[:,i].std():.3f}")\n \n # Split data\n print(f"\\n[2] Splitting data (80-20)...")\n from sklearn.model_selection import train_test_split\n X_train, X_test, y_train, y_test = train_test_split(\n X, y, test_size=0.2, random_state=42, stratify=y\n )\n print(f" Train: {len(X_train)}, Test: {len(X_test)}")\n \n # Show class distribution\n unique, counts = np.unique(y_train, return_counts=True)\n print(f"\\n Class distribution in training set:")\n for cls, count in zip(unique, counts):\n cls_name = [k for k, v in label_mapping.items() if v == str(cls)][0]\n print(f" {cls}: {cls_name:15s} - {count:4d} samples ({count/len(y_train)*100:.1f}%)")\n \n # Train model\n print(f"\\n[3] Training Random Forest for LAND USE CLASSIFICATION...")\n from xgboost import XGBClassifier\n from sklearn.metrics import accuracy_score, classification_report\n \n model = XGBClassifier(\n n_estimators=200,\n max_depth=30,\n tree_method="hist",\n device="cuda",\n random_state=42,\n n_jobs=-1,\n verbosity=1\n )\n model.fit(X_train, y_train)\n \n # Evaluate\n y_pred = model.predict(X_test)\n accuracy = accuracy_score(y_test, y_pred)\n \n print(f"\\n ✅ Model trained!")\n print(f" Training accuracy: {model.score(X_train, y_train)*100:.2f}%")\n print(f" Testing accuracy: {accuracy*100:.2f}%")\n \n # Show feature importance\n print(f"\\n Feature importance:")\n importances = model.feature_importances_\n indices = np.argsort(importances)[::-1]\n for i, idx in enumerate(indices):\n print(f" {i+1}. {features_to_use[idx]:15s}: {importances[idx]:.4f}")\n \n # Classification report\n print(f"\\n[4] Classification Report:")\n class_names = [k for k, v in sorted(label_mapping.items(), key=lambda x: x[1])]\n print(classification_report(y_test, y_pred, target_names=class_names, zero_division=0))\n \n else:\n print(f" ❌ No samples extracted")\n model = None\n \n except Exception as e:\n print(f" ❌ Error: {e}")\n import traceback\n traceback.print_exc()\n model = None\nelse:\n print("❌ Missing training data or satellite data")\n model = None\n\nprint("\\n" + "="*70)\nprint("📝 NOTE: Model này dự đoán PHÂN LOẠI SỬ DỤNG ĐẤT (8 lớp)")\nprint(" NDVI là một trong các features đầu vào, không phải mục tiêu dự đoán")\nprint(" Sau khi predict → có thể hiển thị NDVI map như chỉ số phụ")\nprint("="*70)\n')
# In[ ]:
# 💾 SAVE MODEL WITH METADATA
print("="*70)
print("MODEL SAVING")
print("="*70)
if model is not None:
print("\n🔄 Saving trained LAND USE CLASSIFICATION model with metadata...")
try:
from datetime import datetime
# Prepare metadata for ModelManager
metadata = {
"timestamp": datetime.now().isoformat(),
"data_source": "Local S3 ODC (Open Data Cube)",
"collections": ["sentinel-2-l2a"],
"features": features_to_use, # All features used
"feature_mode": "extended", # Using extended aggregate features
"training_samples": len(X_train),
"testing_samples": len(X_test),
"test_size": 0.2,
"train_accuracy": float(model.score(X_train, y_train)),
"test_accuracy": float(accuracy),
"model_type": "random_forest",
"device": "cpu",
"n_estimators": 200,
"max_depth": 30,
"learning_rate": None,
"cnn_epochs": None,
"n_features": X_train.shape[1],
"n_classes": len(np.unique(y)),
"class_names": list(label_mapping.keys()),
"classification_report": classification_report(y_test, y_pred,
target_names=class_names,
output_dict=True,
zero_division=0),
"bbox": None,
"time_range": f"{date_range[0]}/{date_range[1]}",
"resolution": 10,
"notes": "LAND USE CLASSIFICATION model trained from 01.train_ODC.ipynb. Predicts 8 land use classes using multiple spectral indices. NDVI is one of the input features, not the prediction target."
}
# Save model with metadata using updated save_model function
save_model("model_land_use_odc.joblib", model, metadata=metadata, label_encoder=None)
print("✅ Model saved to model_train/model_land_use_odc.joblib")
print(f" - Purpose: Land Use Classification (8 classes)")
print(f" - Features: {len(features_to_use)} ({', '.join(features_to_use[:3])}...)")
print(f" - Train Accuracy: {metadata['train_accuracy']*100:.2f}%")
print(f" - Test Accuracy: {metadata['test_accuracy']*100:.2f}%")
print(f" - Classes: {metadata['n_classes']}")
print(f"\n📝 NDVI là một trong các features, không phải prediction target")
print(f" Sau khi predict → có thể tính NDVI map riêng để hiển thị")
except Exception as e:
print(f"❌ Error saving model: {e}")
import traceback
traceback.print_exc()
else:
print("❌ No model to save")
print("="*70)
# # 📖 Hướng dẫn sử dụng Model
#
# ## Mục đích của Model
#
# Model này được train để **DỰ ĐOÁN PHÂN LOẠI SỬ DỤNG ĐẤT** với 8 lớp:
#
# 1. **Lua tom** (0) - Lúa tôm
# 2. **Lua** (1) - Lúa
# 3. **CHN** (2) - Cây hàng năm
# 4. **CLN** (3) - Cây lâu năm
# 5. **TS** (4) - Thủy sản
# 6. **Song** (5) - Sông
# 7. **Dat xay dung** (6) - Đất xây dựng
# 8. **Rung** (7) - Rừng
#
# ## Features đầu vào
#
# Model sử dụng **nhiều spectral indices** làm features:
# - NDVI (mean, min, max, std, range)
# - NDWI (mean)
# - NDBI (mean)
# - EVI (mean)
#
# ## NDVI là gì trong hệ thống này?
#
# ⚠️ **QUAN TRỌNG**: NDVI **KHÔNG PHẢI** là mục tiêu dự đoán!
#
# - **NDVI là INPUT FEATURE**: Một trong các chỉ số dùng để train model
# - **Mục tiêu dự đoán**: Phân loại sử dụng đất (8 lớp)
# - **NDVI map**: Có thể hiển thị NDVI map như chỉ số phụ sau khi predict xong
#
# ## Workflow Prediction
#
# ```python
# # 1. Load model
# model, label_encoder, metadata = model_manager.load_model("model_land_use_odc.joblib")
#
# # 2. Extract features từ satellite data
# features = extract_features(satellite_data) # NDVI, NDWI, NDBI, EVI
#
# # 3. Predict land use classification
# land_use_prediction = model.predict(features)
# # → Kết quả: Mảng với giá trị 0-7 (8 lớp sử dụng đất)
#
# # 4. (Optional) Tính NDVI map riêng để hiển thị
# ndvi_map = (NIR - Red) / (NIR + Red)
# # → NDVI map chỉ để visualize, không phải prediction target
# ```
#
# ## So sánh với approach cũ
#
# | Approach | Features | Target | NDVI Role |
# |----------|----------|--------|-----------|
# | ❌ Cũ (sai) | Chỉ NDVI | 8 lớp đất | Input duy nhất |
# | ✅ Mới (đúng) | NDVI + NDWI + NDBI + EVI | 8 lớp đất | Một trong nhiều features |
#
# ## Test Model
#
# ```python
# # Test với website
# # 1. Upload model_land_use_odc.joblib lên server
# # 2. Chọn model trong prediction interface
# # 3. Chọn vùng và thời gian
# # 4. System sẽ tự động:
# # - Extract features (NDVI, NDWI, NDBI, EVI)
# # - Predict land use classification
# # - (Optional) Generate NDVI visualization map
# ```
# In[ ]:
# 🛑 CLEANUP
print("="*70)
print("CLEANUP")
print("="*70)
print("\n🔄 Closing Dask client and cluster...")
try:
client.close()
cluster.close()
print("✅ Cleanup complete")
except Exception as e:
print(f"⚠️ Error during cleanup: {e}")
print("\n" + "="*70)
print("✅ PIPELINE COMPLETE")
print("="*70)