7 Commits

Author SHA1 Message Date
basketballcantho 7660bcb37c admin swin-unet train file 2026-04-03 18:49:18 +07:00
basketballcantho d9e54bfb28 update gitignore 2026-04-03 14:46:53 +07:00
basketballcantho aec4d213cf đã chạy được Mic supplyer trên ODC 2026-04-03 14:40:58 +07:00
basketballcantho dcf80c8295 hướng dẫn chạy với google colab 2026-04-03 10:12:11 +07:00
Victor Phan 46da481029 cơ bản hoàn tát các chức năng chính 2025-12-21 17:31:51 +07:00
Victor Phan 82aa814777 update 01 2025-12-21 14:34:18 +07:00
Victor Phan 2d7b3b1011 Track large files with Git LFS 2025-12-12 20:36:12 +07:00
53 changed files with 67934 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Disabled LFS tracking to avoid pushing large files
#*.tif filter=lfs diff=lfs merge=lfs -text
#*.joblib filter=lfs diff=lfs merge=lfs -text
#*.nc filter=lfs diff=lfs merge=lfs -text
#*.ipynb filter=lfs diff=lfs merge=lfs -text
+84
View File
@@ -0,0 +1,84 @@
# Ignore all model weights and large data
*.joblib
*.nc
*.tif
*.tiff
*.png
*.jpg
*.jpeg
*.h5
*.pt
*.ckpt
*.pb
*.npz
*.npy
*.hdf5
*.pth
*.onnx
*.zip
*.tar
*.tar.gz
*.7z
*.rar
*.exe
*.dll
*.so
*.bin
*.sav
*.csv
*.parquet
*.feather
*.db
*.sqlite
*.log
*.bak
*.tmp
*~
# Ignore model info/metadata if không cần backup
# *.json
# Ignore cache, prediction, backup folders
dataset_cache/
predictions/
backup_model_train/
backup_ketquaphanloai/
backup_S3_download_Amazon/
model_train/
__pycache__/
# Ignore large data files
ndvi_results/
ndvi_cache/
prediction_cache/
dataset_cache/
bfg.jar
..bfg-report/
.dvc/
# Ignore model outputs but keep info json
model_train/*.joblib
model_train/*.tif
model_train/*.png
model_train/*.h5
model_train/*.pt
model_train/*.pth
model_train/*.ckpt
model_train/*.npz
model_train/*.npy
model_train/*.zip
model_train/*.tar
model_train/*.tar.gz
model_train/*.7z
model_train/*.rar
model_train/*.csv
model_train/*.parquet
model_train/*.feather
model_train/*.db
model_train/*.sqlite
model_train/*.log
# VSCode settings
.vscode/
# Jupyter checkpoints
.ipynb_checkpoints/
+5
View File
@@ -0,0 +1,5 @@
# This directory is a Syncthing folder marker.
# Do not delete.
folderID: rs-data
created: 2026-04-03T11:35:19+07:00
+5
View File
@@ -0,0 +1,5 @@
{
"python-envs.defaultEnvManager": "ms-python.python:conda",
"python-envs.defaultPackageManager": "ms-python.python:conda",
"python-envs.pythonProjects": []
}
+947
View File
@@ -0,0 +1,947 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "912ed572-1658-406b-976c-cd6de2d4e89e",
"metadata": {
"tags": []
},
"outputs": [
{
"ename": "ModuleNotFoundError",
"evalue": "No module named 'easi_tools'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)",
"File \u001b[0;32m<timed exec>:4\u001b[0m\n",
"File \u001b[0;32m~/CSIROBoeingPhase5-Vietnam/new_import_ODC.py:23\u001b[0m\n\u001b[1;32m 21\u001b[0m easinotebooksrepo \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m/home/jovyan/easi-notebooks\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m easinotebooksrepo \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m sys\u001b[38;5;241m.\u001b[39mpath: sys\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mappend(easinotebooksrepo)\n\u001b[0;32m---> 23\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01measi_tools\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m EasiDefaults, xarray_object_size, notebook_utils, unset_cachingproxy\n\u001b[1;32m 24\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01measi_tools\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mload_s2l2a\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m load_s2l2a_with_offset\n\u001b[1;32m 25\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mdask\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdistributed\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m progress\n",
"\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'easi_tools'"
]
}
],
"source": [
"%%time\n",
"%matplotlib inline\n",
"\n",
"import importlib\n",
"import new_import_ODC \n",
"\n",
"importlib.reload(new_import_ODC)\n",
"\n",
"from new_import_ODC import *\n",
"\n",
"print(\"✅ All modules loaded successfully\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "d824dc4f-994b-4d1c-8d24-ce6674da141c",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ AWS credentials loaded from environment variables\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/x79/miniconda/envs/env_01/lib/python3.10/site-packages/distributed/node.py:187: UserWarning: Port 8787 is already in use.\n",
"Perhaps you already have a cluster running?\n",
"Hosting the HTTP server on port 41709 instead\n",
" warnings.warn(\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ Dask cluster initialized\n",
" Cluster: LocalCluster(9f2167a3, 'tcp://127.0.0.1:41233', workers=4, threads=24, memory=31.26 GiB)\n",
"✅ Datacube connected (metadata only)\n",
"\n",
"======================================================================\n",
"CPU times: user 4.98 s, sys: 831 ms, total: 5.81 s\n",
"Wall time: 7.69 s\n"
]
}
],
"source": [
"%%time\n",
"import os\n",
"import sys\n",
"\n",
"print(\"✅ AWS credentials loaded from environment variables\")\n",
"\n",
"# Cấu hình Dask local\n",
"from dask.distributed import Client, LocalCluster\n",
"\n",
"cluster = LocalCluster(n_workers=4)\n",
"client = Client(cluster)\n",
"print(\"✅ Dask cluster initialized\")\n",
"print(f\" Cluster: {cluster}\")\n",
"\n",
"# Khai báo Datacube (chỉ để lấy metadata, không dùng load())\n",
"import datacube\n",
"try:\n",
" dc = datacube.Datacube()\n",
" print(\"✅ Datacube connected (metadata only)\")\n",
"except Exception as e:\n",
" print(f\"⚠️ Datacube connection not critical: {e}\")\n",
" dc = None\n",
"\n",
"print(\"\\n\" + \"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "1e113730",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"GETTING SENTINEL-2 SCENE METADATA\n",
"======================================================================\n",
"\n",
"[1] Loading metadata from datacube...\n",
" ✅ Found 40 scenes\n",
"\n",
"[2] Selected scene: S2A_48PWR_20231226_0_L2A\n",
" Date: 2023-12-26 03:35:26.919000+00:00\n",
"\n",
"[3] Available bands:\n",
" - nir: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - red: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - scl: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - blue: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - green: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - nir08: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - nir09: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - swir16: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - swir22: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - coastal: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - rededge1: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - rededge2: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
" - rededge3: https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/48/P/WR/20\n",
"======================================================================\n",
"CPU times: user 3.34 s, sys: 76.3 ms, total: 3.41 s\n",
"Wall time: 3.23 s\n"
]
}
],
"source": [
"%%time\n",
"# 🔧 Get Sentinel-2 scene metadata from datacube\n",
"print(\"=\"*70)\n",
"print(\"GETTING SENTINEL-2 SCENE METADATA\")\n",
"print(\"=\"*70)\n",
"\n",
"date_range = (\"2023-03-01\", \"2023-12-31\")\n",
"longtitude_range = (105.5, 106.4)\n",
"latitude_range = (9.2, 10.0)\n",
"\n",
"try:\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",
" \n",
"except Exception as e:\n",
" print(f\"❌ Error: {e}\")\n",
" import traceback\n",
" traceback.print_exc()\n",
"\n",
"print(\"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "3cd69645",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"CHECKING FOR CACHED DATASET\n",
"======================================================================\n",
"\n",
"⏳ Cache file not found: dataset_cache/sentinel2_timeseries_40scenes.nc\n",
" Will download from S3 and save cache\n",
" (Next run will use cache automatically)\n",
"======================================================================\n",
"CPU times: user 4.36 ms, sys: 3.7 ms, total: 8.06 ms\n",
"Wall time: 7.22 ms\n"
]
}
],
"source": [
"%%time\n",
"# 🔍 CHECK IF DATASET CACHE EXISTS (Skip download if available)\n",
"print(\"=\"*70)\n",
"print(\"CHECKING FOR CACHED DATASET\")\n",
"print(\"=\"*70)\n",
"\n",
"import os\n",
"import xarray as xr\n",
"\n",
"cache_dir = \"dataset_cache\"\n",
"cache_file = f\"{cache_dir}/sentinel2_timeseries_40scenes.nc\"\n",
"\n",
"use_cache = False\n",
"\n",
"if 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\n",
"else:\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",
"\n",
"print(\"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "435f9f78-a9a4-4226-86ca-d4bec42d454e",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"LOADING SENTINEL-2 DATA FROM S3 COGs (RASTERIO) - OPTIMAL ACCURACY\n",
"======================================================================\n",
"\n",
"📥 Downloading from S3...\n",
"\n",
"📦 Found 40 available scenes\n",
" Date range: 2023-03-01 to 2023-12-31\n",
"\n",
"[LOADING] Loading ALL 40 scenes with ALL available bands...\n",
" (Keeping NATIVE resolution - NO upsampling/magnification)\n",
" Available bands: ['nir', 'red', 'scl', 'blue', 'green', 'nir08', 'nir09', 'swir16', 'swir22', 'coastal', 'rededge1', 'rededge2', 'rededge3']\n",
"\n",
" [ 1/1] S2A_48PWR_20231226_0_L2A (2023-12-26)\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
" ✅ 13 bands loaded\n",
"\n",
"✅ Successfully loaded 1 scenes!\n",
"\n",
"[RESOLUTION NORMALIZATION] Aligning all bands to native resolution (NO magnification)...\n",
" Reference resolution: 10980×10980 pixels (native nir)\n",
" Resampling scl: 5490×5490 → 10980×10980\n",
" Resampling nir08: 5490×5490 → 10980×10980\n",
" Resampling nir09: 1830×1830 → 10980×10980\n",
" Resampling swir16: 5490×5490 → 10980×10980\n",
" Resampling swir22: 5490×5490 → 10980×10980\n",
" Resampling coastal: 1830×1830 → 10980×10980\n",
" Resampling rededge1: 5490×5490 → 10980×10980\n",
" Resampling rededge2: 5490×5490 → 10980×10980\n",
" Resampling rededge3: 5490×5490 → 10980×10980\n",
"✅ Resolution normalization complete! (9 bands resampled)\n",
"\n",
"[SPECTRAL INDICES] Calculating spectral indices for each scene...\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"<timed exec>:169: RuntimeWarning: divide by zero encountered in divide\n",
"<timed exec>:169: RuntimeWarning: invalid value encountered in divide\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ Calculated 2 spectral indices per scene\n",
"\n",
"[STACKING] Stacking all 1 scenes to create time-series...\n",
"\n",
"[TEMPORAL FEATURES] Computing temporal features from time-series...\n",
"✅ Added 6 temporal/aggregate features\n",
"\n",
"[CACHE] Saving dataset to cache...\n",
"✅ Dataset saved to cache: dataset_cache/sentinel2_timeseries_40scenes.nc\n",
" Cache size: 6.40 GB\n",
"\n",
"✅ OPTIMAL Dataset with native resolution + temporal features created!\n",
" ======================================================================\n",
" 🎬 Total scenes (time steps): 1\n",
" 📊 Total bands/variables: 21\n",
" 🖼️ Spatial size: 10980 × 10980 pixels (NATIVE resolution)\n",
" 📏 Native resolution: 10m (Sentinel-2 L2A)\n",
" ⏰ Temporal range: 2023-12-26 to 2023-12-26\n",
"❌ Error: name 'notebook_utils' is not defined\n",
"======================================================================\n",
"CPU times: user 9min, sys: 4min 16s, total: 13min 16s\n",
"Wall time: 16min 42s\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Traceback (most recent call last):\n",
" File \"<timed exec>\", line 262, in <module>\n",
"NameError: name 'notebook_utils' is not defined\n"
]
}
],
"source": [
"%%time\n",
"# 💾 LOAD SENTINEL-2 DATA DIRECTLY FROM S3 COGS USING RASTERIO - WITH TEMPORAL FEATURES\n",
"print(\"=\"*70)\n",
"print(\"LOADING SENTINEL-2 DATA FROM S3 COGs (RASTERIO) - OPTIMAL ACCURACY\")\n",
"print(\"=\"*70)\n",
"\n",
"try:\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",
" \n",
"except Exception as e:\n",
" print(f\"❌ Error: {e}\")\n",
" import traceback\n",
" traceback.print_exc()\n",
" data = None\n",
" ndvi = None\n",
"\n",
"print(\"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d2585562-88aa-4c7d-bf70-1f6affcf65d4",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"TRAINING DATA SETUP\n",
"======================================================================\n",
"\n",
"[1] Loading training data: train/ST_training data_updated_1130points_new.shp\n",
" ❌ Error: name 'load_train_data' is not defined\n",
"\n",
"[2] Label mapping:\n",
" 0: Lua tom\n",
" 1: Lua\n",
" 2: CHN\n",
" 3: CLN\n",
" 4: TS\n",
" 5: Song\n",
" 6: Dat xay dung\n",
" 7: Rung\n",
"\n",
"======================================================================\n"
]
}
],
"source": [
"# 🎯 LOAD TRAINING DATA & EXTRACT FEATURES\n",
"print(\"=\"*70)\n",
"print(\"TRAINING DATA SETUP\")\n",
"print(\"=\"*70)\n",
"\n",
"# Load training points\n",
"train_path = \"train/ST_training data_updated_1130points_new.shp\"\n",
"print(f\"\\n[1] Loading training data: {train_path}\")\n",
"\n",
"try:\n",
" train = load_train_data(train_path)\n",
" print(f\" ✅ Loaded {len(train)} training points\")\n",
" print(f\" Columns: {list(train.columns)}\")\n",
" train.head()\n",
"except Exception as e:\n",
" print(f\" ❌ Error: {e}\")\n",
" train = None\n",
"\n",
"# Label mapping\n",
"label_mapping = {\n",
" \"Lua tom\": \"0\",\n",
" \"Lua\": \"1\",\n",
" \"CHN\": \"2\",\n",
" \"CLN\": \"3\",\n",
" \"TS\": \"4\",\n",
" \"Song\": \"5\",\n",
" \"Dat xay dung\": \"6\",\n",
" \"Rung\": \"7\",\n",
"}\n",
"\n",
"print(f\"\\n[2] Label mapping:\")\n",
"for label, code in label_mapping.items():\n",
" print(f\" {code}: {label}\")\n",
"\n",
"print(\"\\n\" + \"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "2e955884-d4af-422d-a8e6-d436199540e0",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"MODEL TRAINING\n",
"======================================================================\n",
"❌ Missing training data or NDVI\n",
"======================================================================\n",
"CPU times: user 700 μs, sys: 0 ns, total: 700 μs\n",
"Wall time: 638 μs\n"
]
}
],
"source": [
"%%time\n",
"# 🤖 RANDOM FOREST MODEL TRAINING\n",
"print(\"=\"*70)\n",
"print(\"MODEL TRAINING\")\n",
"print(\"=\"*70)\n",
"\n",
"if train is not None and ndvi is not None:\n",
" print(\"\\n[1] Extracting features from NDVI...\")\n",
" try:\n",
" # Extract NDVI values at training point locations\n",
" X = []\n",
" y = []\n",
" \n",
" for idx, point in train.iterrows():\n",
" try:\n",
" # Get NDVI value at point location (nearest neighbor)\n",
" ndvi_val = float(ndvi.sel(x=point.geometry.x, y=point.geometry.y, method='nearest').values)\n",
" label = label_mapping[point.Hientrang]\n",
" \n",
" X.append([ndvi_val])\n",
" y.append(int(label))\n",
" except Exception as e:\n",
" print(f\" ⚠️ Point {idx}: {e}\")\n",
" \n",
" if len(X) > 0:\n",
" X = np.array(X)\n",
" y = np.array(y)\n",
" print(f\" ✅ Extracted {len(X)} samples\")\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\n",
" )\n",
" print(f\" Train: {len(X_train)}, Test: {len(X_test)}\")\n",
" \n",
" # Train model\n",
" print(f\"\\n[3] Training Random Forest...\")\n",
" from sklearn.ensemble import RandomForestClassifier\n",
" from sklearn.metrics import accuracy_score\n",
" \n",
" model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)\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",
" print(f\" ✅ Model trained!\")\n",
" print(f\" Accuracy: {accuracy*100:.2f}%\")\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\n",
"else:\n",
" print(\"❌ Missing training data or NDVI\")\n",
" model = None\n",
"\n",
"print(\"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "f1a14379-ed6e-4897-9ca4-2669743fab40",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"MODEL SAVING\n",
"======================================================================\n",
"❌ No model to save\n",
"======================================================================\n"
]
}
],
"source": [
"# 💾 SAVE MODEL\n",
"print(\"=\"*70)\n",
"print(\"MODEL SAVING\")\n",
"print(\"=\"*70)\n",
"\n",
"if model is not None:\n",
" print(\"\\n🔄 Saving trained model...\")\n",
" try:\n",
" save_model(\"model_rasterio.joblib\", model)\n",
" print(\"✅ Model saved to model_train/model_rasterio.joblib\")\n",
" except Exception as e:\n",
" print(f\"❌ Error saving model: {e}\")\n",
"else:\n",
" print(\"❌ No model to save\")\n",
"\n",
"print(\"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "33dd516d-9824-499e-96b9-5cd9224c194c",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"CLEANUP\n",
"======================================================================\n",
"\n",
"🔄 Closing Dask client and cluster...\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ Cleanup complete\n",
"\n",
"======================================================================\n",
"✅ PIPELINE COMPLETE\n",
"======================================================================\n"
]
},
{
"ename": "",
"evalue": "",
"output_type": "error",
"traceback": [
"\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n",
"\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n",
"\u001b[1;31mClick <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info. \n",
"\u001b[1;31mView Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details."
]
}
],
"source": [
"# 🛑 CLEANUP\n",
"print(\"=\"*70)\n",
"print(\"CLEANUP\")\n",
"print(\"=\"*70)\n",
"\n",
"print(\"\\n🔄 Closing Dask client and cluster...\")\n",
"try:\n",
" client.close()\n",
" cluster.close()\n",
" print(\"✅ Cleanup complete\")\n",
"except Exception as e:\n",
" print(f\"⚠️ Error during cleanup: {e}\")\n",
"\n",
"print(\"\\n\" + \"=\"*70)\n",
"print(\"✅ PIPELINE COMPLETE\")\n",
"print(\"=\"*70)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "env_01",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.18"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4465
View File
File diff suppressed because one or more lines are too long
+1406
View File
File diff suppressed because one or more lines are too long
+445
View File
@@ -0,0 +1,445 @@
# Demo Website trên Google Colab với FRP
Hướng dẫn này giúp bạn chạy toàn bộ hệ thống **remote-sensing** trên Google Colab
và expose ra internet qua **FRP (Fast Reverse Proxy)** — không cần ngrok, URL cố định, không giới hạn session.
---
## Yêu cầu
| Thành phần | Mô tả |
|---|---|
| **Google Colab** | Tài khoản Google thông thường (free tier là đủ) |
| **Google Drive** | Dùng để lưu project và models |
| **1 VPS có IP public** | Chạy `frps` server — VPS $35/tháng là đủ |
| **Port mở trên VPS** | 7000 (FRP control) + 8080 (web traffic) |
---
## Phần 1 — Chuẩn bị VPS (chạy 1 lần, giữ mãi)
### Bước 1.1 — Download FRP lên VPS
SSH vào VPS rồi chạy:
```bash
wget https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz
tar -xzf frp_0.61.1_linux_amd64.tar.gz
cd frp_0.61.1_linux_amd64
```
### Bước 1.2 — Tạo file cấu hình `frps.toml`
```bash
cat > frps.toml << 'EOF'
bindPort = 7000
auth.token = "your_secret_token_here"
# Dashboard để theo dõi kết nối (tuỳ chọn)
webServer.port = 7500
webServer.user = "admin"
webServer.password = "admin123"
EOF
```
> ⚠️ Đặt `auth.token` thành chuỗi bí mật của bạn, ví dụ: `"rs_demo_2026_abc123"`. Phải giống với phía Colab.
### Bước 1.3 — Chạy frps
```bash
# Test chạy foreground (Ctrl+C để dừng)
./frps -c frps.toml
# Chạy nền (production)
nohup ./frps -c frps.toml > frps.log 2>&1 &
# Kiểm tra đang chạy
ps aux | grep frps
```
### Bước 1.4 — Mở firewall VPS
```bash
# Ubuntu/Debian
ufw allow 7000 # FRP control port
ufw allow 8080 # Web traffic port
ufw allow 7500 # Dashboard (tuỳ chọn)
ufw reload
# CentOS/RHEL
firewall-cmd --permanent --add-port=7000/tcp
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --reload
```
### Bước 1.5 — Kiểm tra frps hoạt động
```bash
# Xem log
tail -f frps.log
# Kết quả kỳ vọng:
# [frps] frp service started, listen on 0.0.0.0:7000
```
---
## Phần 2 — Chuẩn bị Google Drive
### Bước 2.1 — Upload project lên Drive
Cấu trúc thư mục trên Google Drive:
```
My Drive/
└── remote-sensing/
├── api_server.py
├── train_module.py
├── feature_extractor.py
├── model_manager.py
├── cloud_removal.py
├── report_generator.py
├── generate_previews.py
├── vietnam_provinces.py
├── vietnam_provinces_merged.py
├── utils.py
├── model_train/ ← copy toàn bộ models đã train
│ ├── *.joblib
│ └── *.json
├── cloud_removal_model/ ← copy nếu dùng cloud removal DL
│ └── *.pth
├── predictions/ ← để trống, Colab sẽ tạo output vào đây
├── reports/ ← để trống
└── training_interface.html ← và tất cả *.html
```
> 💡 Upload nhanh nhất: zip toàn bộ folder `remote-sensing`, upload 1 file zip lên Drive, rồi giải nén bằng Colab.
---
## Phần 3 — Notebook Google Colab
Tạo notebook mới tại [colab.google.com](https://colab.google.com) và paste từng cell sau.
---
### Cell 1 — Mount Drive và di chuyển vào project
```python
from google.colab import drive
drive.mount('/content/drive')
import os
PROJECT_PATH = '/content/drive/MyDrive/remote-sensing'
os.chdir(PROJECT_PATH)
print(f"Working directory: {os.getcwd()}")
print("Files:", os.listdir()[:10])
```
---
### Cell 2 — Giải nén nếu upload dạng zip (tuỳ chọn)
```python
# Chỉ chạy nếu bạn upload file zip
import zipfile
ZIP_PATH = '/content/drive/MyDrive/remote-sensing.zip'
EXTRACT_TO = '/content/drive/MyDrive/'
if os.path.exists(ZIP_PATH):
with zipfile.ZipFile(ZIP_PATH, 'r') as z:
z.extractall(EXTRACT_TO)
print("✅ Extracted successfully")
else:
print("⏭️ No zip found, skipping")
```
---
### Cell 3 — Cài dependencies (chạy lần đầu, ~812 phút)
```python
print("Installing core API dependencies...")
!pip install -q fastapi uvicorn pydantic
print("Installing geospatial + ML dependencies...")
!pip install -q \
numpy pandas xarray rasterio rioxarray geopandas shapely \
scikit-learn xgboost joblib \
matplotlib pillow markdown
print("Installing Planetary Computer dependencies...")
!pip install -q pystac-client planetary-computer odc-stac
print("Installing PyTorch (GPU)...")
!pip install -q torch torchvision \
--extra-index-url https://download.pytorch.org/whl/cu118
print("✅ All dependencies installed")
```
---
### Cell 4 — Kiểm tra GPU và môi trường
```python
import torch
print(f"PyTorch version : {torch.__version__}")
print(f"GPU available : {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU name : {torch.cuda.get_device_name(0)}")
import rasterio, xarray, geopandas
print(f"rasterio : {rasterio.__version__}")
print(f"xarray : {xarray.__version__}")
print(f"geopandas : {geopandas.__version__}")
```
---
### Cell 5 — Download và cấu hình frpc
```python
import subprocess, os
# Download frpc
!wget -q https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz \
-O /tmp/frp.tar.gz
!tar -xzf /tmp/frp.tar.gz -C /tmp/
!chmod +x /tmp/frp_0.61.1_linux_amd64/frpc
# ============================================================
# ⚠️ SỬA 2 DÒNG NÀY TRƯỚC KHI CHẠY
VPS_IP = "123.456.789.000" # IP public của VPS bạn
FRP_TOKEN = "your_secret_token_here" # Phải giống frps.toml trên VPS
# ============================================================
frpc_config = f"""
serverAddr = "{VPS_IP}"
serverPort = 7000
auth.token = "{FRP_TOKEN}"
[[proxies]]
name = "remote-sensing-web"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8000
remotePort = 8080
"""
with open('/tmp/frpc.toml', 'w') as f:
f.write(frpc_config)
print(f"✅ frpc configured → VPS: {VPS_IP}:8080")
```
---
### Cell 6 — Khởi động FastAPI server
```python
import subprocess, time, os
os.chdir('/content/drive/MyDrive/remote-sensing')
# Khởi động FastAPI
server = subprocess.Popen(
["uvicorn", "api_server:app",
"--host", "127.0.0.1",
"--port", "8000",
"--log-level", "warning"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
time.sleep(4)
# Kiểm tra server đã lên chưa
if server.poll() is None:
print("✅ FastAPI server is running on port 8000")
else:
out, err = server.communicate()
print("❌ Server failed to start:")
print(err.decode()[:1000])
```
---
### Cell 7 — Khởi động frpc tunnel
```python
import subprocess, time
frpc = subprocess.Popen(
["/tmp/frp_0.61.1_linux_amd64/frpc", "-c", "/tmp/frpc.toml"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
time.sleep(3)
if frpc.poll() is None:
print("✅ FRP tunnel is active")
print("=" * 55)
print(f"🌐 Main site : http://{VPS_IP}:8080")
print(f"📊 Dashboard : http://{VPS_IP}:8080/dashboard")
print(f"🏋️ Training : http://{VPS_IP}:8080/training")
print(f"🔮 Prediction : http://{VPS_IP}:8080/prediction")
print(f"📦 Batch : http://{VPS_IP}:8080/batch")
print(f"🌿 NDVI : http://{VPS_IP}:8080/ndvi")
print(f"📈 Reports : http://{VPS_IP}:8080/reports")
print("=" * 55)
else:
out, err = frpc.communicate()
print("❌ FRP failed:")
print(err.decode()[:500])
```
---
### Cell 8 — Kiểm tra toàn bộ hệ thống
```python
import urllib.request, json
BASE = "http://127.0.0.1:8000"
checks = [
("/api/network/check", "Network connectivity"),
("/api/models/list", "Model list"),
("/api/provinces/list", "Province list"),
("/api/cloud-removal/methods", "Cloud removal methods"),
]
for path, label in checks:
try:
r = urllib.request.urlopen(BASE + path, timeout=5)
data = json.loads(r.read())
status = ""
except Exception as e:
data = str(e)
status = ""
print(f"{status} {label:<30}{str(data)[:80]}")
```
---
### Cell 9 — Xem log nếu có lỗi (tuỳ chọn)
```python
# Xem stderr của FastAPI
import select, sys
if server.poll() is not None:
_, err = server.communicate()
print("FastAPI stderr:")
print(err.decode())
else:
# Đọc log không block
import os
flags = os.O_RDONLY | os.O_NONBLOCK
try:
fd = server.stderr.fileno()
os.set_blocking(fd, False)
print(server.stderr.read(2000).decode())
except:
print("Server is running (no errors captured)")
```
---
### Cell 10 — Dừng server khi xong demo
```python
import subprocess
server.terminate()
frpc.terminate()
print("✅ FastAPI stopped")
print("✅ FRP tunnel closed")
```
---
## Phần 4 — Dùng domain thay vì IP (nâng cao)
Nếu VPS có domain riêng, bạn có thể truy cập bằng URL đẹp hơn.
### Sửa `frps.toml` trên VPS
```toml
bindPort = 7000
auth.token = "your_secret_token_here"
vhostHTTPPort = 80
```
### Sửa Cell 5 (`frpc.toml`) trên Colab
```toml
serverAddr = "your-vps.com"
serverPort = 7000
auth.token = "your_secret_token_here"
[[proxies]]
name = "remote-sensing-web"
type = "http"
localPort = 8000
customDomains = ["demo.your-vps.com"]
```
### DNS — trỏ subdomain về VPS
```
demo.your-vps.com → A record → 123.456.789.000
```
→ Truy cập: `http://demo.your-vps.com` (không cần `:8080`)
---
## Phần 5 — Giữ session Colab sống lâu hơn
Colab tự disconnect sau ~90 phút idle. Để tránh:
```javascript
// Paste vào console trình duyệt (F12 → Console)
function keepAlive() {
document.querySelector('#connect button')?.click();
console.log('keep-alive ping:', new Date().toLocaleTimeString());
}
setInterval(keepAlive, 60000);
```
Hoặc dùng **Colab Pro** ($10/tháng) để không bị giới hạn session.
---
## Phần 6 — Tóm tắt so sánh FRP vs ngrok
| Tiêu chí | FRP (self-host) | ngrok (free) |
|---|---|---|
| URL cố định | ✅ Có | ❌ Đổi mỗi session |
| Session timeout | ✅ Không giới hạn | ⚠️ 2 giờ |
| Chi phí | Free (cần VPS) | Free tier có giới hạn |
| Dữ liệu qua server bên thứ 3 | ❌ Không | ✅ Qua server ngrok |
| Cần setup | ⚠️ Cần cài frps trên VPS | ✅ Chạy ngay |
| Phù hợp | Demo dài hạn, production | Demo nhanh 1 lần |
---
## Checklist trước khi demo
- [ ] VPS đang chạy `frps` và port 7000, 8080 đã mở
- [ ] Project đã upload đầy đủ lên Google Drive (kể cả `model_train/`)
- [ ] Đã điền đúng `VPS_IP``FRP_TOKEN` trong Cell 5
- [ ] Cell 3 (cài deps) đã chạy thành công
- [ ] Cell 8 (health check) cho thấy tất cả ✅
- [ ] Truy cập `http://VPS_IP:8080/dashboard` từ trình duyệt → hiện trang
---
*Tạo ngày: 03/04/2026 — dự án remote-sensing Vietnam Land Classification*
+252
View File
@@ -0,0 +1,252 @@
# 🎉 Chức năng mới đã được phục hồi
## 📊 1. Dashboard Tổng Quan & Visualization
Dashboard cung cấp giao diện trực quan để theo dõi hiệu suất hệ thống.
### Truy cập Dashboard
```
http://localhost:8000/dashboard
```
### Tính năng
- **📈 Tổng Quan**: Hiển thị thống kê tổng hợp
- Số models đã train
- Số predictions đã tạo
- Số reports đã generate
- Accuracy của model mới nhất
- **📊 Accuracy Trends**: Biểu đồ theo dõi accuracy qua thời gian
- Line chart: Accuracy, Precision, Recall
- Bar chart: F1-Score comparison
- Export PNG/PDF
- **📊 Class Distribution**: Phân bố các lớp đất
- Bar chart: Số lượng mẫu mỗi lớp
- Chọn model để xem
- Export PNG/PDF
### API Endpoints
```python
# Lấy accuracy trends
GET /api/dashboard/accuracy-trends
# Lấy thống kê tổng quan
GET /api/dashboard/statistics
# Lấy phân bố lớp của model
GET /api/dashboard/class-distribution/{model_filename}
```
### Export Charts
- **PNG**: Click nút "💾 Export PNG"
- **PDF**: Click nút "📄 Export PDF"
---
## 📝 2. Auto Report Generator
Report tự động được tạo sau khi training và prediction hoàn thành.
### Reports đã có
- **Training Report**: Tự động tạo sau khi train xong
- Metrics, confusion matrix, class distribution
- Lưu trong folder `reports/`
- **Prediction Report**: Tự động tạo sau khi predict xong
- Thông tin về output file, bbox, features
- Lưu trong folder `reports/`
### API Endpoints
```python
# Liệt kê reports
GET /api/reports/list
# Xem report
GET /api/reports/view/{filename}
# Download report
GET /api/reports/download/{filename}
```
### Xem Reports
- Web interface: http://localhost:8000/
- Hoặc truy cập trực tiếp: http://localhost:8000/api/reports/view/{filename}
---
## 🔄 3. Batch Processing
Predict nhiều khu vực cùng lúc với queue management.
### Cách sử dụng
#### Bước 1: Tạo CSV file
Tạo file CSV với format:
```csv
name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
Region_1,105.6,9.3,105.8,9.5,2023-03-01,2023-05-31,12,30,20
Region_2,105.8,9.3,106.0,9.5,2023-03-01,2023-05-31,12,30,20
```
**File mẫu**: `batch_regions_example.csv`
#### Bước 2: Upload và Start Batch
1. Truy cập: http://localhost:8000/dashboard
2. Chọn tab "🔄 Batch Processing"
3. Upload CSV file
4. Chọn model để predict
5. Click "🚀 Start Batch Prediction"
#### Bước 3: Theo dõi Progress
Dashboard sẽ tự động refresh mỗi 3 giây và hiển thị:
- ⏳ Queued: Đang chờ
- ▶️ Running: Đang chạy
- ✅ Completed: Hoàn thành
- ❌ Failed: Lỗi
### API Endpoints
```python
# Bắt đầu batch prediction
POST /api/batch/start
{
"model_filename": "model_20231221.joblib",
"items": [
{
"name": "Region_1",
"min_lon": 105.6,
"min_lat": 9.3,
"max_lon": 105.8,
"max_lat": 9.5,
"start_date": "2023-03-01",
"end_date": "2023-05-31",
"max_scenes": 12,
"cloud_cover": 30,
"resolution": 20
}
],
"auto_retry": true,
"max_retries": 3
}
# Kiểm tra queue status
GET /api/batch/status
# Lấy kết quả batch
GET /api/batch/results/{batch_id}
# Hủy batch
POST /api/batch/cancel/{batch_id}
```
### Auto-Retry
- Tự động retry khi failed (default: max 3 lần)
- Có thể tắt bằng cách set `auto_retry: false`
### Progress Tracking
- Mỗi job có progress bar riêng
- Real-time update status
- Hiển thị error message nếu failed
---
## 🚀 Khởi động Server
```bash
# Activate môi trường
conda activate env_01
# Chạy API server
python api_server.py
```
Server sẽ chạy tại: http://localhost:8000
## 📍 Các URL quan trọng
- **Training Interface**: http://localhost:8000/
- **Dashboard**: http://localhost:8000/dashboard
- **API Docs**: http://localhost:8000/docs
- **Redoc**: http://localhost:8000/redoc
---
## 🔧 Cấu trúc Folders
```
remote-sensing/
├── api_server.py # API server với các chức năng mới
├── dashboard.html # Dashboard UI (MỚI)
├── training_interface.html # Training UI
├── report_generator.py # Auto report generator
├── batch_regions_example.csv # CSV mẫu cho batch (MỚI)
├── model_train/ # Models đã train
├── predictions/ # Prediction outputs
└── reports/ # Auto-generated reports
```
---
## 🎯 Use Cases
### Use Case 1: Theo dõi Model Performance
1. Train nhiều models với configs khác nhau
2. Mở Dashboard → Tab "📊 Accuracy Trends"
3. So sánh accuracy/F1-score qua thời gian
4. Export charts để báo cáo
### Use Case 2: Batch Prediction cho nhiều khu vực
1. Chuẩn bị CSV với danh sách khu vực
2. Upload vào Dashboard → Tab "🔄 Batch Processing"
3. Chọn model tốt nhất
4. Start batch và theo dõi progress
5. Download results khi hoàn thành
### Use Case 3: Tạo Reports tự động
1. Chạy training/prediction
2. Report tự động được tạo
3. Xem qua Dashboard hoặc `/api/reports/list`
4. Download để chia sẻ
---
## ⚠️ Lưu ý
1. **Batch Processing**: Hiện tại chỉ xử lý tuần tự (từng job một)
2. **Auto-retry**: Chỉ retry khi lỗi kỹ thuật, không retry nếu config sai
3. **Charts Export**: Cần browser hỗ trợ Canvas API
4. **Memory**: Batch lớn có thể tốn RAM, nên chia nhỏ
---
## 🐛 Troubleshooting
### Dashboard không hiển thị data
- Kiểm tra có models/predictions trong folders chưa
- Refresh lại trang
- Check console log (F12)
### Batch processing không chạy
- Kiểm tra format CSV đúng chưa
- Kiểm tra model đã chọn có tồn tại không
- Xem API logs để debug
### Charts không export được
- Browser phải hỗ trợ Canvas.toDataURL()
- Thử browser khác (Chrome/Firefox)
---
## 📞 Support
Nếu gặp vấn đề, check:
1. API logs: `python api_server.py`
2. Browser console: F12 → Console
3. Network tab: F12 → Network
---
**🎉 Tất cả chức năng đã được phục hồi và nâng cấp!**
@@ -0,0 +1,10 @@
{
"cells": [],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+254
View File
@@ -0,0 +1,254 @@
# 🎉 Hệ thống đã được cập nhật hoàn chỉnh!
## 📁 Cấu trúc hệ thống mới
```
remote-sensing/
├── index.html # 🆕 Trang chính với tab navigation
├── training_interface.html # ✅ Interface training (độc lập)
├── prediction_interface.html # 🆕 Interface prediction (tách riêng)
├── dashboard.html # ✅ Dashboard visualization
├── api_server.py # ✅ API server (đã cập nhật đầy đủ)
├── train_module.py # Training logic
├── report_generator.py # Auto report generator
├── batch_regions_example.csv # 🆕 CSV mẫu cho batch processing
├── NEW_FEATURES.md # Documentation
└── test_new_features.py # Test script
```
## 🚀 Các URL hiện tại
### Main Pages
- **Trang chủ với tabs**: http://localhost:8000/
- **Training standalone**: http://localhost:8000/training
- **Prediction standalone**: http://localhost:8000/prediction
- **Dashboard standalone**: http://localhost:8000/dashboard
- **API Docs**: http://localhost:8000/docs
### Tab Navigation trong Index
1. 🏠 **Trang Chủ** - Tổng quan & quick start
2. 🎓 **Training** - Training interface (iframe)
3. 🗺️ **Prediction** - Prediction interface (iframe)
4. 📊 **Dashboard** - Visualization & charts
5. 🤖 **Models** - Quản lý models
6. 📄 **Reports** - Xem & download reports
7. 🔄 **Batch Processing** - Batch prediction queue
## ✨ Chức năng đã cập nhật
### 1. Tab Navigation System
- ✅ Giao diện thống nhất với 7 tabs
- ✅ Smooth transition animations
- ✅ Responsive design
- ✅ Real-time data loading
### 2. Training Interface (Tách riêng)
- ✅ Có thể truy cập độc lập tại `/training`
- ✅ Hoặc embed trong tab của index.html
- ✅ Đầy đủ chức năng như cũ
### 3. Prediction Interface (Mới tách riêng)
- ✅ Giao diện riêng biệt tại `/prediction`
- ✅ Map selector với Leaflet
- ✅ Model dropdown với info preview
- ✅ Time & data configuration
- ✅ Real-time status tracking
- ✅ Download results & view reports
- ✅ History của tất cả predictions
### 4. Dashboard & Visualization
- ✅ Accuracy trends charts
- ✅ F1-Score comparison
- ✅ Class distribution
- ✅ Export PNG/PDF
- ✅ Real-time statistics
### 5. Batch Processing
- ✅ Upload CSV file
- ✅ Auto-retry mechanism
- ✅ Queue management
- ✅ Progress tracking
- ✅ Real-time status updates
## 🔧 API Endpoints mới
### Dashboard APIs
```
GET /api/dashboard/accuracy-trends # Accuracy trends over time
GET /api/dashboard/statistics # Tổng quan thống kê
GET /api/dashboard/class-distribution/{model} # Phân bố classes
```
### Batch Processing APIs
```
POST /api/batch/start # Bắt đầu batch prediction
GET /api/batch/status # Kiểm tra queue status
GET /api/batch/results/{batch_id} # Lấy kết quả batch
POST /api/batch/cancel/{batch_id} # Hủy batch
```
### Existing APIs (đã có)
```
# Training
POST /api/training/start
GET /api/training/status
POST /api/training/stop
# Prediction
POST /api/prediction/start
GET /api/prediction/status
# Models
GET /api/models/list
# Reports
GET /api/reports/list
GET /api/reports/view/{filename}
GET /api/reports/download/{filename}
# Predictions
GET /api/predictions/list
GET /api/predictions/download/{filename}
# Cache
GET /api/cache/info
POST /api/cache/clear
```
## 🎯 Cách sử dụng
### 1. Khởi động server
```bash
conda activate env_01
python api_server.py
```
### 2. Truy cập hệ thống
Mở browser: http://localhost:8000/
### 3. Workflow cơ bản
#### A. Training
1. Click tab "🎓 Training"
2. Vẽ bbox hoặc chọn preset
3. Cấu hình model type, parameters
4. Click "Start Training"
5. Theo dõi progress
6. Download model & view report
#### B. Prediction
1. Click tab "🗺️ Prediction"
2. Chọn model đã train
3. Vẽ bbox khu vực cần predict
4. Cấu hình time range & data
5. Click "Start Prediction"
6. Download GeoTIFF khi hoàn thành
#### C. Dashboard
1. Click tab "📊 Dashboard"
2. Xem accuracy trends
3. So sánh models
4. Export charts PNG/PDF
#### D. Batch Processing
1. Click tab "🔄 Batch Processing"
2. Upload CSV file (xem batch_regions_example.csv)
3. Chọn model
4. Click "Start Batch Prediction"
5. Theo dõi progress từng job
## 📊 Format CSV cho Batch Processing
```csv
name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
Region_1,105.6,9.3,105.8,9.5,2023-03-01,2023-05-31,12,30,20
Region_2,105.8,9.3,106.0,9.5,2023-03-01,2023-05-31,12,30,20
```
## 🔍 Test các chức năng
```bash
# Test tất cả APIs
python test_new_features.py
# Hoặc test thủ công
curl http://localhost:8000/api/dashboard/statistics
curl http://localhost:8000/api/models/list
curl http://localhost:8000/api/batch/status
```
## 📝 Notes
### Import Warnings
Các warning về import (xarray, numpy, etc.) là bình thường vì:
- Các thư viện này được import động trong runtime
- Chỉ khi thực sự cần thiết (prediction/training)
- Không ảnh hưởng đến hoạt động của server
### Browser Compatibility
- Khuyến nghị: Chrome, Firefox, Edge (latest)
- Mobile responsive: Đã optimize
- Chart.js & Leaflet: CDN loaded automatically
### Performance
- Training: Tùy vào config (5-30 phút)
- Prediction: 2-10 phút tùy khu vực
- Batch: Sequential processing (1 job/time)
- Dashboard: Real-time updates mỗi 3s
## 🎨 Tính năng UI/UX
### Design
- ✅ Modern gradient backgrounds
- ✅ Card-based layouts
- ✅ Smooth animations
- ✅ Consistent color scheme
- ✅ Responsive grid system
### Interactions
- ✅ Real-time progress bars
- ✅ Status badges
- ✅ Loading spinners
- ✅ Error alerts
- ✅ Success notifications
### Charts
- ✅ Interactive tooltips
- ✅ Zoom & pan
- ✅ Export functionality
- ✅ Responsive sizing
## 🚨 Troubleshooting
### Server không start
```bash
# Check port 8000
lsof -i :8000
# Kill if needed
kill -9 <PID>
```
### Tab không load
- Clear browser cache
- Check console (F12)
- Verify file paths
### Batch không chạy
- Check CSV format
- Verify model exists
- Check API logs
## 📞 Support
Nếu gặp vấn đề:
1. Check terminal logs
2. Check browser console (F12)
3. Verify all HTML files exist
4. Test API endpoints với curl/Postman
---
**🎉 Hệ thống đã sẵn sàng sử dụng!**
Start server: `python api_server.py`
Access: http://localhost:8000/
+1437
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
{
"filename": "CRGAN_generator.pth",
"epoch": 0,
"train_loss": 0.0,
"val_loss": 0.0,
"in_channels": 6,
"out_channels": 4,
"use_s1": true,
"description": "",
"uploaded_at": "2026-01-26T13:36:58.433105"
}
+11
View File
@@ -0,0 +1,11 @@
{
"filename": "GLFCR_generator.pth",
"epoch": 10,
"train_loss": 0.014,
"val_loss": 0.01,
"in_channels": 6,
"out_channels": 4,
"use_s1": true,
"description": "No describe",
"uploaded_at": "2026-01-27T10:38:28.134503"
}
+11
View File
@@ -0,0 +1,11 @@
{
"filename": "SpAGAN_generator.pth",
"epoch": 30,
"train_loss": 0.222,
"val_loss": 0.11,
"in_channels": 6,
"out_channels": 4,
"use_s1": true,
"description": "No data",
"uploaded_at": "2026-01-26T13:43:06.751319"
}
+935
View File
@@ -0,0 +1,935 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Land Classification System</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
text-align: center;
}
.header h1 {
color: #667eea;
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 1.1em;
}
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
background: white;
padding: 15px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.nav-tab {
flex: 1;
padding: 15px 25px;
background: #f5f5f5;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: all 0.3s;
color: #666;
}
.nav-tab:hover {
background: #e0e0e0;
}
.nav-tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card .icon {
font-size: 3em;
margin-bottom: 15px;
}
.stat-card .value {
font-size: 2.5em;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.stat-card .label {
color: #666;
font-size: 1.1em;
}
.chart-container {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.chart-container h3 {
margin-bottom: 20px;
color: #333;
font-size: 1.5em;
}
.chart-wrapper {
position: relative;
height: 400px;
}
canvas {
max-height: 100%;
}
.batch-queue {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.batch-item {
padding: 20px;
border: 2px solid #e0e0e0;
border-radius: 10px;
margin-bottom: 15px;
transition: all 0.3s;
}
.batch-item:hover {
border-color: #667eea;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
}
.batch-item.running {
border-color: #4caf50;
background: #f1f8f4;
}
.batch-item.completed {
border-color: #2196f3;
background: #e3f2fd;
}
.batch-item.failed {
border-color: #f44336;
background: #ffebee;
}
.batch-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.batch-name {
font-size: 1.2em;
font-weight: 600;
color: #333;
}
.batch-status {
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 0.9em;
}
.batch-status.queued {
background: #fff3cd;
color: #856404;
}
.batch-status.running {
background: #d4edda;
color: #155724;
}
.batch-status.completed {
background: #cce5ff;
color: #004085;
}
.batch-status.failed {
background: #f8d7da;
color: #721c24;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin-top: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-success {
background: #4caf50;
color: white;
}
.btn-success:hover {
background: #45a049;
}
.export-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.file-upload {
margin-bottom: 20px;
}
.file-upload input[type="file"] {
display: none;
}
.file-upload label {
display: inline-block;
padding: 12px 30px;
background: #667eea;
color: white;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
}
.file-upload label:hover {
background: #5568d3;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loading::after {
content: '...';
animation: loading 1.5s infinite;
}
@keyframes loading {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
.model-selector {
margin-bottom: 20px;
}
.model-selector select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
background: white;
cursor: pointer;
}
.model-selector select:focus {
outline: none;
border-color: #667eea;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Dashboard - Land Classification System</h1>
<p>Tổng quan hệ thống phân loại đất từ xa</p>
</div>
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('overview')">📈 Tổng Quan</button>
<button class="nav-tab" onclick="switchTab('trends')">📊 Accuracy Trends</button>
<button class="nav-tab" onclick="switchTab('batch')">🔄 Batch Processing</button>
</div>
<!-- Tab: Tổng Quan -->
<div id="overview" class="tab-content active">
<div class="stats-grid">
<div class="stat-card">
<div class="icon">🤖</div>
<div class="value" id="totalModels">-</div>
<div class="label">Models Trained</div>
</div>
<div class="stat-card">
<div class="icon">🗺️</div>
<div class="value" id="totalPredictions">-</div>
<div class="label">Predictions Generated</div>
</div>
<div class="stat-card">
<div class="icon">📄</div>
<div class="value" id="totalReports">-</div>
<div class="label">Reports Created</div>
</div>
<div class="stat-card">
<div class="icon"></div>
<div class="value" id="latestAccuracy">-</div>
<div class="label">Latest Model Accuracy</div>
</div>
</div>
<div class="chart-container">
<h3>📊 Phân bố các lớp đất (Model mới nhất)</h3>
<div class="model-selector">
<select id="modelSelect" onchange="loadClassDistribution()">
<option value="">Chọn model...</option>
</select>
</div>
<div class="chart-wrapper">
<canvas id="classDistChart"></canvas>
</div>
<div class="export-buttons">
<button class="btn btn-primary" onclick="exportChart('classDistChart', 'class-distribution.png')">
💾 Export PNG
</button>
<button class="btn btn-success" onclick="exportChartPDF('classDistChart', 'class-distribution.pdf')">
📄 Export PDF
</button>
</div>
</div>
</div>
<!-- Tab: Accuracy Trends -->
<div id="trends" class="tab-content">
<div class="chart-container">
<h3>📈 Accuracy Trends Over Time</h3>
<div class="chart-wrapper">
<canvas id="accuracyTrendChart"></canvas>
</div>
<div class="export-buttons">
<button class="btn btn-primary" onclick="exportChart('accuracyTrendChart', 'accuracy-trends.png')">
💾 Export PNG
</button>
<button class="btn btn-success" onclick="exportChartPDF('accuracyTrendChart', 'accuracy-trends.pdf')">
📄 Export PDF
</button>
</div>
</div>
<div class="chart-container">
<h3>📊 F1-Score Comparison</h3>
<div class="chart-wrapper">
<canvas id="f1ScoreChart"></canvas>
</div>
</div>
</div>
<!-- Tab: Batch Processing -->
<div id="batch" class="tab-content">
<div class="batch-queue">
<h3>🔄 Batch Prediction Queue</h3>
<div class="file-upload">
<label for="csvFile">📁 Upload CSV File</label>
<input type="file" id="csvFile" accept=".csv" onchange="handleCSVUpload(event)">
<p style="margin-top: 10px; color: #666;">
Format CSV: name,min_lon,min_lat,max_lon,max_lat
</p>
</div>
<div class="model-selector">
<select id="batchModelSelect">
<option value="">Chọn model để predict...</option>
</select>
</div>
<button class="btn btn-primary" onclick="startBatchPrediction()" style="margin-bottom: 30px;">
🚀 Start Batch Prediction
</button>
<h4 style="margin: 20px 0;">Queue Status</h4>
<div class="stats-grid" style="margin-bottom: 30px;">
<div class="stat-card">
<div class="value" id="queuedJobs">0</div>
<div class="label">⏳ Queued</div>
</div>
<div class="stat-card">
<div class="value" id="runningJobs">0</div>
<div class="label">▶️ Running</div>
</div>
<div class="stat-card">
<div class="value" id="completedJobs">0</div>
<div class="label">✅ Completed</div>
</div>
<div class="stat-card">
<div class="value" id="failedJobs">0</div>
<div class="label">❌ Failed</div>
</div>
</div>
<h4 style="margin: 20px 0;">Active Jobs</h4>
<div id="batchJobs">
<p class="loading">Đang tải dữ liệu</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script>
let charts = {};
let batchItems = [];
let refreshInterval = null;
// Tab switching
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
// Load data for the active tab
if (tabName === 'overview') {
loadDashboardStats();
} else if (tabName === 'trends') {
loadAccuracyTrends();
} else if (tabName === 'batch') {
loadBatchStatus();
startBatchRefresh();
} else {
stopBatchRefresh();
}
}
// Load dashboard statistics
async function loadDashboardStats() {
try {
const response = await fetch('/api/dashboard/statistics');
const data = await response.json();
document.getElementById('totalModels').textContent = data.models.total;
document.getElementById('totalPredictions').textContent = data.predictions.total;
document.getElementById('totalReports').textContent = data.reports.total;
if (data.models.latest && data.models.latest.metrics) {
const accuracy = (data.models.latest.metrics.accuracy * 100).toFixed(2);
document.getElementById('latestAccuracy').textContent = accuracy + '%';
}
// Load models for selector
await loadModelsList();
} catch (error) {
console.error('Error loading dashboard stats:', error);
}
}
// Load models list
async function loadModelsList() {
try {
const response = await fetch('/api/models/list');
const data = await response.json();
const modelSelect = document.getElementById('modelSelect');
const batchModelSelect = document.getElementById('batchModelSelect');
modelSelect.innerHTML = '<option value="">Chọn model...</option>';
batchModelSelect.innerHTML = '<option value="">Chọn model...</option>';
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.filename;
option.textContent = `${model.filename} (${model.created})`;
modelSelect.appendChild(option.cloneNode(true));
batchModelSelect.appendChild(option);
});
// Auto-select latest model
if (data.models.length > 0) {
modelSelect.value = data.models[0].filename;
await loadClassDistribution();
}
} catch (error) {
console.error('Error loading models:', error);
}
}
// Load class distribution
async function loadClassDistribution() {
const modelFilename = document.getElementById('modelSelect').value;
if (!modelFilename) return;
try {
const response = await fetch(`/api/dashboard/class-distribution/${modelFilename}`);
const data = await response.json();
const labels = Object.keys(data.class_distribution);
const values = Object.values(data.class_distribution);
if (charts.classDistChart) {
charts.classDistChart.destroy();
}
const ctx = document.getElementById('classDistChart').getContext('2d');
charts.classDistChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Số lượng mẫu',
data: values,
backgroundColor: [
'rgba(102, 126, 234, 0.7)',
'rgba(118, 75, 162, 0.7)',
'rgba(76, 175, 80, 0.7)',
'rgba(244, 67, 54, 0.7)',
'rgba(33, 150, 243, 0.7)',
'rgba(255, 193, 7, 0.7)',
],
borderColor: [
'rgba(102, 126, 234, 1)',
'rgba(118, 75, 162, 1)',
'rgba(76, 175, 80, 1)',
'rgba(244, 67, 54, 1)',
'rgba(33, 150, 243, 1)',
'rgba(255, 193, 7, 1)',
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
title: {
display: true,
text: `Tổng: ${data.total_samples} mẫu`
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
} catch (error) {
console.error('Error loading class distribution:', error);
}
}
// Load accuracy trends
async function loadAccuracyTrends() {
try {
const response = await fetch('/api/dashboard/accuracy-trends');
const data = await response.json();
if (data.trends.length === 0) {
return;
}
// Prepare data
const labels = data.trends.map(d => new Date(d.date).toLocaleDateString('vi-VN'));
const accuracies = data.trends.map(d => d.accuracy * 100);
const f1Scores = data.trends.map(d => d.f1_score * 100);
const precisions = data.trends.map(d => d.precision * 100);
const recalls = data.trends.map(d => d.recall * 100);
// Accuracy Trend Chart
if (charts.accuracyTrendChart) {
charts.accuracyTrendChart.destroy();
}
const ctx1 = document.getElementById('accuracyTrendChart').getContext('2d');
charts.accuracyTrendChart = new Chart(ctx1, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Accuracy (%)',
data: accuracies,
borderColor: 'rgba(102, 126, 234, 1)',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.4
},
{
label: 'Precision (%)',
data: precisions,
borderColor: 'rgba(76, 175, 80, 1)',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
fill: false,
tension: 0.4
},
{
label: 'Recall (%)',
data: recalls,
borderColor: 'rgba(244, 67, 54, 1)',
backgroundColor: 'rgba(244, 67, 54, 0.1)',
fill: false,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
// F1-Score Chart
if (charts.f1ScoreChart) {
charts.f1ScoreChart.destroy();
}
const ctx2 = document.getElementById('f1ScoreChart').getContext('2d');
charts.f1ScoreChart = new Chart(ctx2, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'F1-Score (%)',
data: f1Scores,
backgroundColor: 'rgba(118, 75, 162, 0.7)',
borderColor: 'rgba(118, 75, 162, 1)',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
} catch (error) {
console.error('Error loading accuracy trends:', error);
}
}
// Export chart as PNG
function exportChart(chartId, filename) {
const canvas = document.getElementById(chartId);
const url = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = filename;
link.href = url;
link.click();
}
// Export chart as PDF
function exportChartPDF(chartId, filename) {
const canvas = document.getElementById(chartId);
const imgData = canvas.toDataURL('image/png');
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [canvas.width, canvas.height]
});
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
pdf.save(filename);
}
// Handle CSV upload
function handleCSVUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
parseCSV(text);
};
reader.readAsText(file);
}
// Parse CSV
function parseCSV(text) {
const lines = text.trim().split('\n');
batchItems = [];
// Skip header
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].split(',');
if (parts.length >= 5) {
batchItems.push({
name: parts[0].trim(),
min_lon: parseFloat(parts[1]),
min_lat: parseFloat(parts[2]),
max_lon: parseFloat(parts[3]),
max_lat: parseFloat(parts[4]),
start_date: parts[5]?.trim() || "2023-03-01",
end_date: parts[6]?.trim() || "2023-05-31",
max_scenes: parseInt(parts[7]) || 12,
cloud_cover: parseInt(parts[8]) || 30,
resolution: parseInt(parts[9]) || 20
});
}
}
alert(`✅ Đã tải ${batchItems.length} khu vực từ CSV`);
}
// Start batch prediction
async function startBatchPrediction() {
const modelFilename = document.getElementById('batchModelSelect').value;
if (!modelFilename) {
alert('❌ Vui lòng chọn model');
return;
}
if (batchItems.length === 0) {
alert('❌ Vui lòng upload file CSV trước');
return;
}
try {
const response = await fetch('/api/batch/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model_filename: modelFilename,
items: batchItems,
auto_retry: true,
max_retries: 3
})
});
const result = await response.json();
alert(`${result.message}`);
// Refresh batch status
loadBatchStatus();
} catch (error) {
console.error('Error starting batch:', error);
alert('❌ Lỗi khi bắt đầu batch prediction');
}
}
// Load batch status
async function loadBatchStatus() {
try {
const response = await fetch('/api/batch/status');
const data = await response.json();
// Update counters
document.getElementById('queuedJobs').textContent = data.queue.queued;
document.getElementById('runningJobs').textContent = data.queue.running;
document.getElementById('completedJobs').textContent = data.queue.completed;
document.getElementById('failedJobs').textContent = data.queue.failed;
// Display jobs
const jobsContainer = document.getElementById('batchJobs');
jobsContainer.innerHTML = '';
// Combine all jobs
const allJobs = [
...data.jobs.running,
...data.jobs.queued,
...data.jobs.recent_completed,
...data.jobs.recent_failed
];
if (allJobs.length === 0) {
jobsContainer.innerHTML = '<p style="text-align: center; color: #666;">Chưa có job nào</p>';
return;
}
allJobs.forEach(job => {
const jobElement = document.createElement('div');
jobElement.className = `batch-item ${job.status}`;
const progress = job.progress || 0;
const errorMsg = job.error ? `<p style="color: #f44336; margin-top: 10px;">⚠️ ${job.error}</p>` : '';
jobElement.innerHTML = `
<div class="batch-header">
<div class="batch-name">${job.name}</div>
<div class="batch-status ${job.status}">${job.status.toUpperCase()}</div>
</div>
<p style="color: #666; margin: 5px 0;">Job ID: ${job.job_id}</p>
<p style="color: #666; margin: 5px 0;">
📍 [${job.config.min_lon.toFixed(2)}, ${job.config.min_lat.toFixed(2)}] →
[${job.config.max_lon.toFixed(2)}, ${job.config.max_lat.toFixed(2)}]
</p>
${job.retries > 0 ? `<p style="color: #ff9800; margin: 5px 0;">🔄 Retries: ${job.retries}/${job.max_retries}</p>` : ''}
${errorMsg}
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
`;
jobsContainer.appendChild(jobElement);
});
} catch (error) {
console.error('Error loading batch status:', error);
}
}
// Auto-refresh batch status
function startBatchRefresh() {
if (refreshInterval) return;
refreshInterval = setInterval(loadBatchStatus, 3000);
}
function stopBatchRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
// Initialize on page load
window.onload = function() {
loadDashboardStats();
};
// Cleanup on page unload
window.onbeforeunload = function() {
stopBatchRefresh();
};
</script>
</body>
</html>
@@ -0,0 +1,164 @@
# 🌾 Giải Thích Quy Trình Phân Loại Đất Trồng Cây
File notebook `02.predict_ODC.ipynb` sử dụng **Machine Learning** kết hợp với **dữ liệu vệ tinh** để phân loại các loại đất/cây trồng. Dưới đây là quy trình chi tiết:
---
## **Bước 1: Thu thập dữ liệu vệ tinh** (Cell 3-4)
```python
date_range = ('2022-09-01', '2023-10-01')
longtitude_range = (105.86575, 105.94120)
latitude_range = (9.65070, 9.69850)
data = load_data(dc, date_range, longtitude_range, latitude_range)
```
- Lấy ảnh **Sentinel-2** (ảnh quang học) từ kho dữ liệu trong khoảng thời gian và vị trí cụ thể
---
## **Bước 2: Xử lý mây** (Cell 5)
```python
result = mask_clean(data)
```
- Loại bỏ các pixel bị mây che phủ để đảm bảo dữ liệu chính xác
---
## **Bước 3: Tính chỉ số NDVI** (Cell 6-10)
```python
ndvi = calculate_indices(result, index='NDVI', satellite_mission='s2')
fill_nan_ndvi = fill_nan(ndvi, time_split)
average_ndvi = fill_nan_ndvi.resample(time='1M').mean()
```
- **NDVI** (Normalized Difference Vegetation Index) = (NIR - Red) / (NIR + Red)
- Giá trị từ **-1 đến 1**: cao = thực vật xanh tốt, thấp = đất trống/nước
- Điền giá trị nan (mây) và tính trung bình theo tháng
---
## **Bước 4: Lấy dữ liệu Radar Sentinel-1** (Cell 11)
```python
dsvh, dsvv = load_data_sen1(dc, date_range, coordinates)
average_vv = calculate_average(dsvv, time_pattern='1M')
average_vh = calculate_average(dsvh, time_pattern='1M')
```
- **VH, VV**: Dữ liệu radar (xuyên mây), cho biết cấu trúc bề mặt
- Giúp phân biệt lúa ngập nước, cây trồng cạn, mặt nước...
---
## **Bước 5: Dự đoán bằng Model ML** (Cell 12) ⭐ **QUAN TRỌNG NHẤT**
```python
loaded_model = joblib.load("model_train/model_odc.joblib")
data_array = predict(loaded_model, data.rio.crs, average_ndvi, average_vh, average_vv)
```
**Model đã được train trước** với dữ liệu mẫu (training data) gồm:
- **Đầu vào (Features)**: NDVI theo tháng + VH + VV (chuỗi thời gian)
- **Đầu ra (Labels)**: Loại đất đã được gắn nhãn thủ công
### Cách model phân loại:
| Đặc điểm | Loại đất |
|----------|----------|
| NDVI cao đều, VV thấp | Rừng |
| NDVI biến đổi theo mùa vụ, VH cao (nước) | Lúa |
| NDVI thấp, VV rất thấp | Sông/nước |
| NDVI trung bình ổn định | Cây lâu năm (CLN) |
---
## **Bước 6: Hiển thị kết quả** (Cell 13-15)
```python
colors = ["#abcee9", "#ffef44", "#c4ff9e", "#ffd6a8", "#93ddda", "#1aeef7", "#ffa7f2", "#33ee33"]
labels = ["Lúa tôm", "Lúa", "CHN", "CLN", "TS", "Sông", "Đất xây dựng", "Rừng"]
```
### 8 lớp phân loại:
| Mã | Tên | Màu | Ý nghĩa |
|----|-----|-----|---------|
| 0 | Lúa tôm | 🔵 Xanh nhạt | Luân canh lúa-tôm |
| 1 | Lúa | 🟡 Vàng | Đất trồng lúa |
| 2 | CHN | 🟢 Xanh lá nhạt | Cây hàng năm |
| 3 | CLN | 🟠 Cam nhạt | Cây lâu năm (cây ăn trái) |
| 4 | TS | 🩵 Xanh ngọc | Thủy sản |
| 5 | Sông | 🔷 Cyan | Mặt nước sông |
| 6 | Đất XD | 💗 Hồng | Đất xây dựng |
| 7 | Rừng | 💚 Xanh đậm | Rừng |
---
## **Bước 7: Lưu kết quả** (Cell 16)
```python
region_result.rio.to_raster("KetQuaPhanLoaiDatODC.tif")
```
- Xuất file GeoTIFF chứa mã phân loại (0-7) cho từng pixel
---
## 📊 **Tóm tắt quy trình:**
```
Ảnh vệ tinh (Sentinel-1 + Sentinel-2)
Xử lý (loại mây, tính NDVI, VH, VV)
Kết hợp features theo thời gian (13 tháng)
Model ML (Random Forest/XGBoost) dự đoán
Bản đồ phân loại 8 lớp đất
File .tif (mỗi pixel = 1 mã loại đất)
```
---
## 📁 Cấu trúc dữ liệu đầu vào cho Model
### Features (Đặc trưng):
- **NDVI theo 13 tháng**: 13 bands
- **VH (radar) theo 13 tháng**: 13 bands
- **VV (radar) theo 13 tháng**: 13 bands
- **Tổng cộng**: ~39 features cho mỗi pixel
### Labels (Nhãn):
- Được lấy từ shapefile training: `train/ST_training data_updated_1130points_new.shp`
- 1130 điểm mẫu đã được gắn nhãn thủ công bởi chuyên gia
---
## 🔧 Các thư viện sử dụng
| Thư viện | Mục đích |
|----------|----------|
| `datacube` | Truy vấn dữ liệu vệ tinh |
| `xarray` | Xử lý dữ liệu đa chiều |
| `rioxarray` | Đọc/ghi GeoTIFF |
| `joblib` | Load/save model ML |
| `sklearn` / `xgboost` | Training model |
| `matplotlib` / `hvplot` | Trực quan hóa |
---
## 📝 Ghi chú
- **Độ phân giải**: 10-20m (tùy cấu hình)
- **Thời gian xử lý**: Phụ thuộc vào kích thước vùng và số scenes
- **Yêu cầu**: Cần kết nối internet để tải dữ liệu vệ tinh từ Planetary Computer hoặc ODC
---
*Tài liệu được tạo ngày 14/12/2025*
+991
View File
@@ -0,0 +1,991 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Land Classification System - Complete Platform</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.main-container {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
margin-bottom: 20px;
text-align: center;
}
.header h1 {
color: #667eea;
font-size: 2.8em;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
color: #666;
font-size: 1.2em;
}
/* Navigation Tabs */
.nav-tabs {
background: white;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
padding: 15px;
margin-bottom: 20px;
display: flex;
gap: 10px;
overflow-x: auto;
}
.nav-tab {
flex: 1;
min-width: 150px;
padding: 15px 25px;
background: #f5f5f5;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: all 0.3s;
color: #666;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.nav-tab:hover {
background: #e0e0e0;
transform: translateY(-2px);
}
.nav-tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
/* Tab Content */
.tab-content {
display: none;
animation: fadeIn 0.3s;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Content Container */
.content-wrapper {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
padding: 30px;
min-height: 600px;
}
/* Common Styles */
.section {
margin-bottom: 30px;
}
.section h2 {
color: #667eea;
margin-bottom: 15px;
font-size: 1.8em;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
.section h3 {
color: #333;
margin-bottom: 15px;
font-size: 1.3em;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
}
.btn-success {
background: #4caf50;
color: white;
}
.btn-success:hover {
background: #45a049;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
/* Grid layouts */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
@media (max-width: 768px) {
.grid-2 {
grid-template-columns: 1fr;
}
}
/* Cards */
.card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
border: 2px solid #e0e0e0;
transition: all 0.3s;
}
.card:hover {
border-color: #667eea;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
}
/* Stats cards */
.stat-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card .icon {
font-size: 3em;
margin-bottom: 15px;
}
.stat-card .value {
font-size: 2.5em;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.stat-card .label {
color: #666;
font-size: 1.1em;
}
/* Alert boxes */
.alert {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.alert-info {
background: #e3f2fd;
border-left: 4px solid #2196f3;
color: #1565c0;
}
.alert-success {
background: #e8f5e9;
border-left: 4px solid #4caf50;
color: #2e7d32;
}
.alert-warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
color: #856404;
}
.alert-danger {
background: #ffebee;
border-left: 4px solid #f44336;
color: #c62828;
}
/* Progress bar */
.progress {
width: 100%;
height: 30px;
background: #e0e0e0;
border-radius: 15px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
/* Map styles */
#trainMap, #predictMap {
height: 500px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
/* Loading spinner */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Status badge */
.status-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
}
.status-badge.running {
background: #d4edda;
color: #155724;
}
.status-badge.completed {
background: #cce5ff;
color: #004085;
}
.status-badge.error {
background: #f8d7da;
color: #721c24;
}
/* Table */
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
th {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
tr:hover {
background: #f9f9f9;
}
/* Footer */
.footer {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
margin-top: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="main-container">
<!-- Header -->
<div class="header">
<h1>🛰️ Land Classification System</h1>
<p>Hệ thống phân loại đất từ xa sử dụng Sentinel-2 & Sentinel-1</p>
</div>
<!-- Navigation Tabs -->
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('home')">
🏠 Trang Chủ
</button>
<button class="nav-tab" onclick="switchTab('train')">
🎓 Training
</button>
<button class="nav-tab" onclick="switchTab('predict')">
🗺️ Prediction
</button>
<button class="nav-tab" onclick="switchTab('dashboard')">
📊 Dashboard
</button>
<button class="nav-tab" onclick="switchTab('models')">
🤖 Models
</button>
<button class="nav-tab" onclick="switchTab('reports')">
📄 Reports
</button>
<button class="nav-tab" onclick="switchTab('batch')">
🔄 Batch Processing
</button>
</div>
<!-- Tab Content: Home -->
<div id="home" class="tab-content active">
<div class="content-wrapper">
<div class="section">
<h2>🎯 Chào mừng đến với Land Classification System</h2>
<p style="font-size: 1.2em; color: #666; margin-bottom: 30px;">
Nền tảng phân loại đất tự động sử dụng dữ liệu vệ tinh Sentinel và Machine Learning
</p>
</div>
<div class="grid-3">
<div class="stat-card">
<div class="icon">🎓</div>
<div class="value" id="homeModelsCount">-</div>
<div class="label">Models Trained</div>
</div>
<div class="stat-card">
<div class="icon">🗺️</div>
<div class="value" id="homePredictionsCount">-</div>
<div class="label">Predictions Created</div>
</div>
<div class="stat-card">
<div class="icon">📄</div>
<div class="value" id="homeReportsCount">-</div>
<div class="label">Reports Generated</div>
</div>
</div>
<div class="section" style="margin-top: 40px;">
<h3>🚀 Bắt đầu nhanh</h3>
<div class="grid-2">
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">1️⃣ Training Model</h4>
<p style="color: #666; margin-bottom: 15px;">
Train model mới với dữ liệu Sentinel-2/1 và shapefile training data
</p>
<button class="btn btn-primary" onclick="switchTab('train')">
🎓 Bắt đầu Training
</button>
</div>
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">2️⃣ Prediction</h4>
<p style="color: #666; margin-bottom: 15px;">
Sử dụng model đã train để phân loại khu vực mới
</p>
<button class="btn btn-success" onclick="switchTab('predict')">
🗺️ Bắt đầu Prediction
</button>
</div>
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">3️⃣ Dashboard</h4>
<p style="color: #666; margin-bottom: 15px;">
Xem thống kê, biểu đồ accuracy trends và so sánh models
</p>
<button class="btn btn-secondary" onclick="switchTab('dashboard')">
📊 Mở Dashboard
</button>
</div>
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">4️⃣ Batch Processing</h4>
<p style="color: #666; margin-bottom: 15px;">
Predict nhiều khu vực cùng lúc với CSV file
</p>
<button class="btn btn-secondary" onclick="switchTab('batch')">
🔄 Batch Processing
</button>
</div>
</div>
</div>
<div class="section" style="margin-top: 40px;">
<h3>📚 Tài liệu & Hướng dẫn</h3>
<div class="alert alert-info">
<span style="font-size: 1.5em;"></span>
<div>
<strong>API Documentation:</strong>
<a href="/docs" target="_blank" style="color: #1565c0; text-decoration: none; font-weight: 600;">
/docs
</a>
<br>
<strong>Features Guide:</strong> Xem file NEW_FEATURES.md để biết chi tiết
</div>
</div>
</div>
</div>
</div>
<!-- Tab Content: Training -->
<div id="train" class="tab-content">
<div class="content-wrapper">
<iframe src="/training" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
</div>
</div>
<!-- Tab Content: Prediction -->
<div id="predict" class="tab-content">
<div class="content-wrapper">
<iframe src="/prediction" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
</div>
</div>
<!-- Tab Content: Dashboard -->
<div id="dashboard" class="tab-content">
<div class="content-wrapper">
<iframe src="/dashboard" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
</div>
</div>
<!-- Tab Content: Models -->
<div id="models" class="tab-content">
<div class="content-wrapper">
<div class="section">
<h2>🤖 Model Management</h2>
<p style="color: #666; margin-bottom: 20px;">Quản lý các models đã train</p>
</div>
<div id="modelsLoading" class="loading">
<div class="spinner"></div>
<p>Đang tải danh sách models...</p>
</div>
<div id="modelsList" style="display: none;">
<table>
<thead>
<tr>
<th>Tên File</th>
<th>Model Type</th>
<th>Accuracy</th>
<th>Ngày Tạo</th>
<th>Kích Thước</th>
<th>Thao Tác</th>
</tr>
</thead>
<tbody id="modelsTableBody"></tbody>
</table>
</div>
</div>
</div>
<!-- Tab Content: Reports -->
<div id="reports" class="tab-content">
<div class="content-wrapper">
<div class="section">
<h2>📄 Reports Management</h2>
<p style="color: #666; margin-bottom: 20px;">Quản lý các báo cáo đã tạo</p>
</div>
<div id="reportsLoading" class="loading">
<div class="spinner"></div>
<p>Đang tải danh sách reports...</p>
</div>
<div id="reportsList" style="display: none;">
<table>
<thead>
<tr>
<th>Tên File</th>
<th>Loại</th>
<th>Ngày Tạo</th>
<th>Kích Thước</th>
<th>Thao Tác</th>
</tr>
</thead>
<tbody id="reportsTableBody"></tbody>
</table>
</div>
</div>
</div>
<!-- Tab Content: Batch Processing -->
<div id="batch" class="tab-content">
<div class="content-wrapper">
<div class="section">
<h2>🔄 Batch Processing</h2>
<p style="color: #666; margin-bottom: 20px;">Predict nhiều khu vực cùng lúc</p>
</div>
<div class="alert alert-info">
<span style="font-size: 1.5em;"></span>
<div>
<strong>CSV Format:</strong> name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
<br>
<strong>File mẫu:</strong> batch_regions_example.csv
</div>
</div>
<div class="grid-2">
<div class="section">
<h3>📁 Upload CSV</h3>
<div class="form-group">
<label>Chọn file CSV:</label>
<input type="file" id="batchCSVFile" accept=".csv" onchange="handleBatchCSV(event)">
</div>
<div class="form-group">
<label>Chọn Model:</label>
<select id="batchModelSelect">
<option value="">Đang tải...</option>
</select>
</div>
<button class="btn btn-primary" onclick="startBatch()">
🚀 Start Batch Prediction
</button>
</div>
<div class="section">
<h3>📊 Queue Status</h3>
<div class="grid-2">
<div class="stat-card">
<div class="value" id="batchQueued">0</div>
<div class="label">⏳ Queued</div>
</div>
<div class="stat-card">
<div class="value" id="batchRunning">0</div>
<div class="label">▶️ Running</div>
</div>
<div class="stat-card">
<div class="value" id="batchCompleted">0</div>
<div class="label">✅ Completed</div>
</div>
<div class="stat-card">
<div class="value" id="batchFailed">0</div>
<div class="label">❌ Failed</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top: 30px;">
<h3>📋 Jobs List</h3>
<div id="batchJobsList"></div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>🛰️ Land Classification System v2.0 | Powered by Sentinel-2/1 & Microsoft Planetary Computer</p>
</div>
</div>
<!-- Scripts -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
let batchCSVData = [];
let refreshInterval = null;
// Tab switching
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
// Load data for specific tabs
if (tabName === 'home') {
loadHomeStats();
} else if (tabName === 'models') {
loadModelsList();
} else if (tabName === 'reports') {
loadReportsList();
} else if (tabName === 'batch') {
loadBatchModels();
loadBatchStatus();
startBatchRefresh();
} else {
stopBatchRefresh();
}
}
// Load home statistics
async function loadHomeStats() {
try {
const response = await fetch('/api/dashboard/statistics');
const data = await response.json();
document.getElementById('homeModelsCount').textContent = data.models.total;
document.getElementById('homePredictionsCount').textContent = data.predictions.total;
document.getElementById('homeReportsCount').textContent = data.reports.total;
} catch (error) {
console.error('Error loading home stats:', error);
}
}
// Load models list
async function loadModelsList() {
const loading = document.getElementById('modelsLoading');
const list = document.getElementById('modelsList');
const tbody = document.getElementById('modelsTableBody');
loading.style.display = 'block';
list.style.display = 'none';
try {
const response = await fetch('/api/models/list');
const data = await response.json();
tbody.innerHTML = '';
data.models.forEach(model => {
const row = document.createElement('tr');
const accuracy = model.info.metrics?.accuracy
? (model.info.metrics.accuracy * 100).toFixed(2) + '%'
: 'N/A';
row.innerHTML = `
<td><strong>${model.filename}</strong></td>
<td>${model.info.model_type || 'N/A'}</td>
<td><span style="color: #4caf50; font-weight: 600;">${accuracy}</span></td>
<td>${new Date(model.created).toLocaleString('vi-VN')}</td>
<td>${model.size_mb} MB</td>
<td>
<button class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9em;"
onclick="window.open('/api/reports/view/training_report_${model.filename.replace('.joblib', '')}.html', '_blank')">
📄 Report
</button>
</td>
`;
tbody.appendChild(row);
});
loading.style.display = 'none';
list.style.display = 'block';
} catch (error) {
console.error('Error loading models:', error);
loading.innerHTML = '<p style="color: #f44336;">❌ Lỗi khi tải danh sách models</p>';
}
}
// Load reports list
async function loadReportsList() {
const loading = document.getElementById('reportsLoading');
const list = document.getElementById('reportsList');
const tbody = document.getElementById('reportsTableBody');
loading.style.display = 'block';
list.style.display = 'none';
try {
const response = await fetch('/api/reports/list');
const data = await response.json();
tbody.innerHTML = '';
data.reports.forEach(report => {
const row = document.createElement('tr');
const typeIcon = report.type === 'training' ? '🎓' : '🗺️';
row.innerHTML = `
<td><strong>${report.filename}</strong></td>
<td>${typeIcon} ${report.type}</td>
<td>${new Date(report.created).toLocaleString('vi-VN')}</td>
<td>${report.size_kb} KB</td>
<td>
<button class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9em;"
onclick="window.open('${report.view_url}', '_blank')">
👁️ Xem
</button>
<button class="btn btn-success" style="padding: 8px 16px; font-size: 0.9em; margin-left: 5px;"
onclick="window.location.href='${report.download_url}'">
💾 Download
</button>
</td>
`;
tbody.appendChild(row);
});
loading.style.display = 'none';
list.style.display = 'block';
} catch (error) {
console.error('Error loading reports:', error);
loading.innerHTML = '<p style="color: #f44336;">❌ Lỗi khi tải danh sách reports</p>';
}
}
// Batch processing functions
async function loadBatchModels() {
try {
const response = await fetch('/api/models/list');
const data = await response.json();
const select = document.getElementById('batchModelSelect');
select.innerHTML = '<option value="">Chọn model...</option>';
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.filename;
option.textContent = `${model.filename} (${model.created})`;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading batch models:', error);
}
}
function handleBatchCSV(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
const lines = text.trim().split('\n');
batchCSVData = [];
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].split(',');
if (parts.length >= 5) {
batchCSVData.push({
name: parts[0].trim(),
min_lon: parseFloat(parts[1]),
min_lat: parseFloat(parts[2]),
max_lon: parseFloat(parts[3]),
max_lat: parseFloat(parts[4]),
start_date: parts[5]?.trim() || "2023-03-01",
end_date: parts[6]?.trim() || "2023-05-31",
max_scenes: parseInt(parts[7]) || 12,
cloud_cover: parseInt(parts[8]) || 30,
resolution: parseInt(parts[9]) || 20
});
}
}
alert(`✅ Đã tải ${batchCSVData.length} khu vực từ CSV`);
};
reader.readAsText(file);
}
async function startBatch() {
const modelFilename = document.getElementById('batchModelSelect').value;
if (!modelFilename) {
alert('❌ Vui lòng chọn model');
return;
}
if (batchCSVData.length === 0) {
alert('❌ Vui lòng upload file CSV trước');
return;
}
try {
const response = await fetch('/api/batch/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_filename: modelFilename,
items: batchCSVData,
auto_retry: true,
max_retries: 3
})
});
const result = await response.json();
alert(`${result.message}`);
loadBatchStatus();
} catch (error) {
console.error('Error starting batch:', error);
alert('❌ Lỗi khi bắt đầu batch prediction');
}
}
async function loadBatchStatus() {
try {
const response = await fetch('/api/batch/status');
const data = await response.json();
document.getElementById('batchQueued').textContent = data.queue.queued;
document.getElementById('batchRunning').textContent = data.queue.running;
document.getElementById('batchCompleted').textContent = data.queue.completed;
document.getElementById('batchFailed').textContent = data.queue.failed;
// Display jobs
const jobsList = document.getElementById('batchJobsList');
const allJobs = [
...data.jobs.running,
...data.jobs.queued,
...data.jobs.recent_completed.slice(0, 5)
];
if (allJobs.length === 0) {
jobsList.innerHTML = '<p style="text-align: center; color: #666;">Chưa có job nào</p>';
return;
}
jobsList.innerHTML = allJobs.map(job => `
<div class="card" style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<strong>${job.name}</strong>
<span class="status-badge ${job.status}">${job.status.toUpperCase()}</span>
</div>
<p style="color: #666; margin: 10px 0;">
📍 [${job.config.min_lon.toFixed(2)}, ${job.config.min_lat.toFixed(2)}] →
[${job.config.max_lon.toFixed(2)}, ${job.config.max_lat.toFixed(2)}]
</p>
${job.error ? `<p style="color: #f44336;">⚠️ ${job.error}</p>` : ''}
</div>
`).join('');
} catch (error) {
console.error('Error loading batch status:', error);
}
}
function startBatchRefresh() {
if (refreshInterval) return;
refreshInterval = setInterval(loadBatchStatus, 3000);
}
function stopBatchRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
// Initialize on page load
window.onload = function() {
loadHomeStats();
};
// Cleanup on page unload
window.onbeforeunload = function() {
stopBatchRefresh();
};
</script>
</body>
</html>
+3728
View File
File diff suppressed because one or more lines are too long
+803
View File
@@ -0,0 +1,803 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Interface - Land Classification</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.content {
padding: 30px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
#predictMap {
height: 500px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.map-container {
grid-column: 1 / -1;
}
.map-instructions {
background: #e3f2fd;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
border-left: 4px solid #2196f3;
}
.map-instructions h3 {
color: #1976d2;
margin-bottom: 8px;
}
.map-instructions p {
color: #555;
margin: 5px 0;
}
.section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
}
.section h2 {
color: #667eea;
margin-bottom: 15px;
font-size: 1.5em;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #333;
font-weight: 600;
}
.form-group input, .form-group select {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 5px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-right: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status-box {
padding: 20px;
background: white;
border-radius: 10px;
border-left: 5px solid #667eea;
margin-bottom: 20px;
}
.status-box.success {
border-left-color: #28a745;
background: #d4edda;
}
.status-box.error {
border-left-color: #dc3545;
background: #f8d7da;
}
.status-box.predicting {
border-left-color: #ffc107;
background: #fff3cd;
}
.progress {
height: 30px;
background: #e0e0e0;
border-radius: 15px;
overflow: hidden;
margin: 10px 0;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
width: 0%;
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
.metric-card {
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
text-align: center;
}
.metric-card h4 {
color: #667eea;
margin-bottom: 10px;
}
.metric-card .value {
font-size: 2em;
font-weight: bold;
color: #333;
}
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert-info {
background: #d1ecf1;
border-left: 4px solid #0c5460;
color: #0c5460;
}
.alert-success {
background: #d4edda;
border-left: 4px solid #155724;
color: #155724;
}
.alert-danger {
background: #f8d7da;
border-left: 4px solid #721c24;
color: #721c24;
}
.predictions-list {
max-height: 400px;
overflow-y: auto;
}
.prediction-item {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
border-left: 4px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.prediction-item:hover {
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
.content {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Prediction Interface</h1>
<p>Phân loại đất cho khu vực mới sử dụng model đã train</p>
</div>
<div class="content">
<!-- Map Section -->
<div class="map-container">
<div class="map-instructions">
<h3>📍 Chọn khu vực để predict</h3>
<p>✏️ Click vào nút hình vuông bên phải để vẽ bbox</p>
<p>🖱️ Kéo và thả để tạo vùng muốn phân loại</p>
<p>🔄 Có thể chỉnh sửa sau khi vẽ</p>
</div>
<div id="predictMap"></div>
</div>
<!-- Model Selection -->
<div class="section">
<h2>🤖 Chọn Model</h2>
<div class="form-group">
<label for="modelSelect">Model đã train:</label>
<select id="modelSelect">
<option value="">Đang tải...</option>
</select>
</div>
<!-- Cache selection dropdown -->
<div class="form-group" style="margin-top:15px;">
<label for="cacheSelect">Chọn cache dữ liệu đầu vào:</label>
<select id="cacheSelect">
<option value="">-- Không dùng cache --</option>
</select>
</div>
<div id="modelInfo" style="display: none; background: #e8f5e9; padding: 15px; border-radius: 8px; margin-top: 15px;">
<h4 style="color: #2e7d32; margin-bottom: 10px;">📊 Thông tin Model</h4>
<p><strong>Type:</strong> <span id="modelType">-</span></p>
<p><strong>Accuracy:</strong> <span id="modelAccuracy">-</span></p>
<p><strong>Training Date:</strong> <span id="modelDate">-</span></p>
</div>
</div>
<!-- Time & Data Configuration -->
<div class="section">
<h2>⏰ Thời gian & Dữ liệu</h2>
<div class="form-row">
<div class="form-group">
<label for="predStartDate">Từ ngày:</label>
<input type="date" id="predStartDate" value="2023-03-01">
</div>
<div class="form-group">
<label for="predEndDate">Đến ngày:</label>
<input type="date" id="predEndDate" value="2023-05-31">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="predMaxScenes">Max Scenes:</label>
<input type="number" id="predMaxScenes" value="12" min="1" max="100">
</div>
<div class="form-group">
<label for="predCloudCover">Cloud Cover (%):</label>
<input type="number" id="predCloudCover" value="30" min="0" max="100">
</div>
</div>
<div class="form-group">
<label for="predResolution">Resolution:</label>
<select id="predResolution">
<option value="10">10m (Chi tiết cao - Chậm)</option>
<option value="20" selected>20m (Cân bằng)</option>
</select>
</div>
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn">
🚀 Start Prediction
</button>
</div>
<!-- Status Section -->
<div class="section" style="grid-column: 1 / -1;">
<h2>📊 Trạng thái Prediction</h2>
<div id="predictionStatus" class="status-box" style="display: none;">
<h3>⏳ Đang xử lý...</h3>
<p id="predictionProgress">Đang khởi tạo...</p>
<div class="progress">
<div class="progress-bar" id="predictionProgressBar">0%</div>
</div>
</div>
<div id="predictionResult" style="display: none;">
<div class="alert alert-success">
<h3>✅ Prediction hoàn thành!</h3>
<p><strong>Output file:</strong> <span id="resultFile"></span></p>
<p><strong>Shape:</strong> <span id="resultShape"></span></p>
<p><strong>Unique classes:</strong> <span id="resultClasses"></span></p>
<!-- PNG Preview -->
<div id="pngPreviewContainer" style="display: none; margin: 20px 0;">
<h4 style="margin-bottom: 10px;">🖼️ Preview:</h4>
<img id="pngPreview" style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);" />
</div>
<div style="margin-top: 15px;">
<button class="btn btn-success" onclick="downloadPrediction()">
💾 Download GeoTIFF
</button>
<button class="btn btn-secondary" onclick="viewReport()">
📄 View Report
</button>
</div>
</div>
</div>
<div id="predictionError" class="alert alert-danger" style="display: none;">
<h3>❌ Lỗi</h3>
<p id="errorMessage"></p>
</div>
</div>
<!-- Previous Predictions -->
<div class="section" style="grid-column: 1 / -1;">
<h2>📋 Predictions đã tạo</h2>
<div id="predictionsList" class="predictions-list">
<p style="text-align: center; color: #666;">Đang tải...</p>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script>
// Map setup
let map, drawnItems, drawControl;
let selectedBbox = null;
let currentPredictionFile = null;
let currentReportFile = null;
let statusCheckInterval = null;
// Initialize map
function initMap() {
map = L.map('predictMap').setView([9.5, 105.9], 9);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Initialize drawing
drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
drawControl = new L.Control.Draw({
draw: {
rectangle: true,
polygon: false,
circle: false,
marker: false,
polyline: false,
circlemarker: false
},
edit: {
featureGroup: drawnItems,
remove: true
}
});
map.addControl(drawControl);
// Handle drawing
map.on(L.Draw.Event.CREATED, function(event) {
drawnItems.clearLayers();
const layer = event.layer;
drawnItems.addLayer(layer);
const bounds = layer.getBounds();
let bbox = {
min_lon: bounds.getWest(),
min_lat: bounds.getSouth(),
max_lon: bounds.getEast(),
max_lat: bounds.getNorth()
};
// Validate bbox (must be within valid geographic coordinates)
if (bbox.min_lon < -180 || bbox.max_lon > 180 || bbox.min_lat < -90 || bbox.max_lat > 90) {
alert('❌ Bbox không hợp lệ! Vui lòng vẽ trong phạm vi bản đồ hợp lệ.\nKinh độ: -180 đến 180, Vĩ độ: -90 đến 90');
drawnItems.clearLayers();
return;
}
selectedBbox = bbox;
// Cache bbox to localStorage
localStorage.setItem('prediction_bbox', JSON.stringify(selectedBbox));
console.log('Selected bbox:', selectedBbox);
});
// On load, restore bbox from cache if exists
const cachedBbox = localStorage.getItem('prediction_bbox');
if (cachedBbox) {
try {
const bbox = JSON.parse(cachedBbox);
// Validate bbox before restoring
if (bbox.min_lon < -180 || bbox.max_lon > 180 ||
bbox.min_lat < -90 || bbox.max_lat > 90) {
console.warn('Cache bbox không hợp lệ, đã xóa:', bbox);
localStorage.removeItem('prediction_bbox');
} else {
// Draw rectangle on map
const bounds = [
[bbox.min_lat, bbox.min_lon],
[bbox.max_lat, bbox.max_lon]
];
const rectangle = L.rectangle(bounds, {
color: '#667eea',
weight: 3,
fillOpacity: 0.2
});
drawnItems.addLayer(rectangle);
map.fitBounds(bounds);
selectedBbox = bbox;
}
} catch (e) {
console.warn('Không thể khôi phục bbox từ cache:', e);
localStorage.removeItem('prediction_bbox');
}
}
}
// Load models list
async function loadModels() {
try {
const response = await fetch('/api/models/list');
const data = await response.json();
const select = document.getElementById('modelSelect');
select.innerHTML = '<option value="">Chọn model...</option>';
// Chỉ lấy các file model thực sự (.joblib), loại bỏ các file có chứa '_info.joblib'
data.models
.filter(m => m.filename.endsWith('.joblib') && !m.filename.includes('_info.joblib'))
.forEach(model => {
const option = document.createElement('option');
option.value = model.filename;
option.textContent = `${model.filename} - ${model.created}`;
option.dataset.info = JSON.stringify(model.info);
select.appendChild(option);
});
// Auto-select first model đúng
const firstJoblib = data.models.find(m => m.filename.endsWith('.joblib') && !m.filename.includes('_info.joblib'));
if (firstJoblib) {
select.value = firstJoblib.filename;
updateModelInfo();
}
} catch (error) {
console.error('Error loading models:', error);
}
}
// Update model info display
function updateModelInfo() {
const select = document.getElementById('modelSelect');
const option = select.options[select.selectedIndex];
if (option.dataset.info) {
const info = JSON.parse(option.dataset.info);
const infoDiv = document.getElementById('modelInfo');
document.getElementById('modelType').textContent = info.model_type || 'N/A';
document.getElementById('modelAccuracy').textContent = info.metrics?.accuracy
? (info.metrics.accuracy * 100).toFixed(2) + '%'
: 'N/A';
document.getElementById('modelDate').textContent = info.training_date || 'N/A';
infoDiv.style.display = 'block';
} else {
document.getElementById('modelInfo').style.display = 'none';
}
}
// Start prediction
async function startPrediction() {
if (!selectedBbox) {
alert('❌ Vui lòng vẽ bbox trên bản đồ trước!');
return;
}
// Validate bbox before sending
if (selectedBbox.min_lon < -180 || selectedBbox.max_lon > 180 ||
selectedBbox.min_lat < -90 || selectedBbox.max_lat > 90) {
alert('❌ Bbox không hợp lệ! Vui lòng vẽ lại trong phạm vi bản đồ hợp lệ.');
drawnItems.clearLayers();
selectedBbox = null;
localStorage.removeItem('prediction_bbox');
return;
}
const modelFilename = document.getElementById('modelSelect').value;
if (!modelFilename) {
alert('❌ Vui lòng chọn model!');
return;
}
const config = {
model_filename: modelFilename,
min_lon: selectedBbox.min_lon,
min_lat: selectedBbox.min_lat,
max_lon: selectedBbox.max_lon,
max_lat: selectedBbox.max_lat,
start_date: document.getElementById('predStartDate').value,
end_date: document.getElementById('predEndDate').value,
max_scenes: parseInt(document.getElementById('predMaxScenes').value),
cloud_cover: parseInt(document.getElementById('predCloudCover').value),
resolution: parseInt(document.getElementById('predResolution').value)
};
try {
document.getElementById('predictBtn').disabled = true;
document.getElementById('predictionStatus').style.display = 'block';
document.getElementById('predictionResult').style.display = 'none';
document.getElementById('predictionError').style.display = 'none';
const response = await fetch('/api/prediction/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const result = await response.json();
if (response.ok) {
// Start monitoring status
startStatusCheck();
} else {
throw new Error(result.detail || 'Lỗi khi bắt đầu prediction');
}
} catch (error) {
console.error('Error starting prediction:', error);
document.getElementById('predictionError').style.display = 'block';
document.getElementById('errorMessage').textContent = error.message;
document.getElementById('predictBtn').disabled = false;
}
}
// Check prediction status
async function checkStatus() {
try {
const response = await fetch('/api/prediction/status');
const status = await response.json();
document.getElementById('predictionProgress').textContent = status.progress;
// Update progress bar (estimate based on message)
let progress = 0;
if (status.progress.includes('khởi')) progress = 10;
else if (status.progress.includes('Sentinel-2')) progress = 30;
else if (status.progress.includes('NDVI')) progress = 50;
else if (status.progress.includes('Sentinel-1')) progress = 60;
else if (status.progress.includes('features')) progress = 70;
else if (status.progress.includes('dự đoán')) progress = 80;
else if (status.progress.includes('lưu')) progress = 90;
else if (status.progress.includes('Hoàn thành')) progress = 100;
document.getElementById('predictionProgressBar').style.width = progress + '%';
document.getElementById('predictionProgressBar').textContent = progress + '%';
if (!status.is_predicting) {
stopStatusCheck();
document.getElementById('predictBtn').disabled = false;
if (status.error) {
document.getElementById('predictionStatus').style.display = 'none';
document.getElementById('predictionError').style.display = 'block';
document.getElementById('errorMessage').textContent = status.error;
} else if (status.result) {
document.getElementById('predictionStatus').style.display = 'none';
document.getElementById('predictionResult').style.display = 'block';
currentPredictionFile = status.result.output_file;
currentReportFile = status.result.report_filename;
document.getElementById('resultFile').textContent = status.result.output_file;
document.getElementById('resultShape').textContent = status.result.shape.join(' x ');
document.getElementById('resultClasses').textContent = status.result.unique_classes.join(', ');
// Show PNG preview if available
if (status.result.png_file) {
const pngFilename = status.result.png_file.split('/').pop();
const previewImg = document.getElementById('pngPreview');
const previewContainer = document.getElementById('pngPreviewContainer');
previewImg.src = `/api/predictions/preview/${pngFilename}`;
previewContainer.style.display = 'block';
}
// Reload predictions list
loadPredictionsList();
}
}
} catch (error) {
console.error('Error checking status:', error);
}
}
// Start/stop status monitoring
function startStatusCheck() {
if (statusCheckInterval) clearInterval(statusCheckInterval);
statusCheckInterval = setInterval(checkStatus, 2000);
}
function stopStatusCheck() {
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
statusCheckInterval = null;
}
}
// Download prediction
function downloadPrediction() {
if (currentPredictionFile) {
const filename = currentPredictionFile.split('/').pop();
window.location.href = `/api/predictions/download/${filename}`;
}
}
// View report
function viewReport() {
if (currentReportFile) {
window.open(`/api/reports/view/${currentReportFile}`, '_blank');
}
}
// Load predictions list
async function loadPredictionsList() {
try {
const response = await fetch('/api/predictions/list');
const data = await response.json();
const listDiv = document.getElementById('predictionsList');
if (data.predictions.length === 0) {
listDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có prediction nào</p>';
return;
}
listDiv.innerHTML = data.predictions.map(pred => `
<div class="prediction-item">
<div>
<strong>${pred.filename}</strong>
<br>
<small style="color: #666;">
${new Date(pred.created).toLocaleString('vi-VN')} - ${pred.size_mb} MB
</small>
</div>
<div>
<button class="btn btn-success" style="padding: 8px 16px; font-size: 0.9em;"
onclick="window.location.href='${pred.download_url}'">
💾 Download
</button>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading predictions:', error);
}
}
// Load cache list
async function loadCacheList() {
try {
const response = await fetch('/api/cache/info');
const data = await response.json();
const select = document.getElementById('cacheSelect');
select.innerHTML = '<option value="">-- Không dùng cache --</option>';
if (data.files && data.files.length > 0) {
data.files.forEach((file, idx) => {
if (file.filename.startsWith('prediction_input_')) {
let label = `#${idx+1} | ${file.filename}`;
if (file.metadata && file.metadata.bbox) {
label += ` | BBox: [${file.metadata.bbox.join(', ')}]`;
}
if (file.metadata && file.metadata.time_range) {
label += ` | Time: ${file.metadata.time_range}`;
}
select.innerHTML += `<option value="${file.filename}">${label}</option>`;
}
});
}
} catch (e) {
console.warn('Không thể tải danh sách cache:', e);
}
}
// Initialize on page load
window.onload = function() {
initMap();
loadModels();
loadPredictionsList();
loadCacheList();
// Add event listener for model selection
document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
};
// Cleanup on page unload
window.onbeforeunload = function() {
stopStatusCheck();
};
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
+3300
View File
File diff suppressed because one or more lines are too long
+786
View File
@@ -0,0 +1,786 @@
"""
Auto Report Generator for Land Classification
Tự động tạo báo cáo HTML chi tiết sau training/prediction
"""
import json
from datetime import datetime
from pathlib import Path
import base64
import io
# Optional: for generating charts
try:
import matplotlib
matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt
import numpy as np
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
def generate_confusion_matrix_image(conf_matrix, class_names):
"""Tạo hình ảnh confusion matrix dạng base64"""
if not MATPLOTLIB_AVAILABLE:
return None
try:
fig, ax = plt.subplots(figsize=(10, 8))
conf_matrix = np.array(conf_matrix)
im = ax.imshow(conf_matrix, interpolation='nearest', cmap=plt.cm.Blues)
ax.figure.colorbar(im, ax=ax)
ax.set(xticks=np.arange(len(class_names)),
yticks=np.arange(len(class_names)),
xticklabels=class_names, yticklabels=class_names,
title='Confusion Matrix',
ylabel='Thực tế (True)',
xlabel='Dự đoán (Predicted)')
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
# Add text annotations
thresh = conf_matrix.max() / 2.
for i in range(len(class_names)):
for j in range(len(class_names)):
ax.text(j, i, format(conf_matrix[i, j], 'd'),
ha="center", va="center",
color="white" if conf_matrix[i, j] > thresh else "black")
fig.tight_layout()
# Convert to base64
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return img_base64
except Exception as e:
print(f"Error generating confusion matrix image: {e}")
return None
def generate_class_distribution_chart(class_names, classification_report):
"""Tạo biểu đồ phân bố các class dạng base64"""
if not MATPLOTLIB_AVAILABLE:
return None
try:
# Extract support (number of samples) for each class
supports = []
for cls in class_names:
if cls in classification_report:
supports.append(classification_report[cls].get('support', 0))
else:
supports.append(0)
fig, ax = plt.subplots(figsize=(10, 6))
colors = plt.cm.Set3(np.linspace(0, 1, len(class_names)))
bars = ax.bar(class_names, supports, color=colors)
ax.set_xlabel('Loại đất')
ax.set_ylabel('Số mẫu')
ax.set_title('Phân bố số mẫu theo loại đất')
plt.xticks(rotation=45, ha='right')
# Add value labels on bars
for bar, val in zip(bars, supports):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
str(int(val)), ha='center', va='bottom', fontsize=9)
fig.tight_layout()
# Convert to base64
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return img_base64
except Exception as e:
print(f"Error generating class distribution chart: {e}")
return None
def generate_metrics_chart(class_names, classification_report):
"""Tạo biểu đồ precision/recall/f1 cho từng class"""
if not MATPLOTLIB_AVAILABLE:
return None
try:
precisions = []
recalls = []
f1_scores = []
for cls in class_names:
if cls in classification_report:
precisions.append(classification_report[cls].get('precision', 0))
recalls.append(classification_report[cls].get('recall', 0))
f1_scores.append(classification_report[cls].get('f1-score', 0))
else:
precisions.append(0)
recalls.append(0)
f1_scores.append(0)
x = np.arange(len(class_names))
width = 0.25
fig, ax = plt.subplots(figsize=(12, 6))
bars1 = ax.bar(x - width, precisions, width, label='Precision', color='#3498db')
bars2 = ax.bar(x, recalls, width, label='Recall', color='#2ecc71')
bars3 = ax.bar(x + width, f1_scores, width, label='F1-Score', color='#e74c3c')
ax.set_xlabel('Loại đất')
ax.set_ylabel('Score')
ax.set_title('Precision / Recall / F1-Score theo loại đất')
ax.set_xticks(x)
ax.set_xticklabels(class_names, rotation=45, ha='right')
ax.legend()
ax.set_ylim(0, 1.1)
# Add grid
ax.yaxis.grid(True, linestyle='--', alpha=0.7)
fig.tight_layout()
# Convert to base64
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return img_base64
except Exception as e:
print(f"Error generating metrics chart: {e}")
return None
def generate_training_report(training_result, config=None):
"""
Tạo báo cáo HTML cho kết quả training
Args:
training_result: Dict chứa kết quả từ train_model()
config: Dict chứa cấu hình training (optional)
Returns:
Tuple (report_path, report_html)
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Extract data from result
train_acc = training_result.get('train_accuracy', 0) * 100
test_acc = training_result.get('test_accuracy', 0) * 100
train_samples = training_result.get('training_samples', 0)
test_samples = training_result.get('testing_samples', 0)
test_size = training_result.get('test_size', 0.2)
classes = training_result.get('classes', [])
cls_report = training_result.get('classification_report', {})
conf_matrix = training_result.get('confusion_matrix', [])
model_type = training_result.get('model_type', 'unknown')
model_path = training_result.get('model_path', '')
bbox = training_result.get('bbox', [])
time_range = training_result.get('time_range', '')
resolution = training_result.get('resolution', 20)
# Generate charts
conf_matrix_img = generate_confusion_matrix_image(conf_matrix, classes) if conf_matrix else None
class_dist_img = generate_class_distribution_chart(classes, cls_report) if cls_report else None
metrics_img = generate_metrics_chart(classes, cls_report) if cls_report else None
# Build classification report table
cls_report_rows = ""
for cls in classes:
if cls in cls_report:
metrics = cls_report[cls]
cls_report_rows += f"""
<tr>
<td><strong>{cls}</strong></td>
<td>{metrics.get('precision', 0):.3f}</td>
<td>{metrics.get('recall', 0):.3f}</td>
<td>{metrics.get('f1-score', 0):.3f}</td>
<td>{int(metrics.get('support', 0))}</td>
</tr>
"""
# Add averages
for avg_type in ['macro avg', 'weighted avg']:
if avg_type in cls_report:
metrics = cls_report[avg_type]
cls_report_rows += f"""
<tr style="background-color: #f0f0f0; font-weight: bold;">
<td>{avg_type}</td>
<td>{metrics.get('precision', 0):.3f}</td>
<td>{metrics.get('recall', 0):.3f}</td>
<td>{metrics.get('f1-score', 0):.3f}</td>
<td>{int(metrics.get('support', 0))}</td>
</tr>
"""
# Build confusion matrix table (fallback if no image)
conf_matrix_table = ""
if conf_matrix:
conf_matrix_table = "<table class='conf-matrix'><tr><th></th>"
for cls in classes:
conf_matrix_table += f"<th>{cls}</th>"
conf_matrix_table += "</tr>"
for i, row in enumerate(conf_matrix):
conf_matrix_table += f"<tr><th>{classes[i]}</th>"
for val in row:
conf_matrix_table += f"<td>{val}</td>"
conf_matrix_table += "</tr>"
conf_matrix_table += "</table>"
# HTML Template
html = f"""
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Training Report - {timestamp}</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
}}
.header h1 {{
font-size: 2.5em;
margin-bottom: 10px;
}}
.header .subtitle {{
opacity: 0.9;
font-size: 1.1em;
}}
.content {{
padding: 40px;
}}
.section {{
margin-bottom: 40px;
}}
.section h2 {{
color: #667eea;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
margin-bottom: 20px;
font-size: 1.5em;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}}
.stat-card {{
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #667eea30;
}}
.stat-card .value {{
font-size: 2.5em;
font-weight: bold;
color: #667eea;
}}
.stat-card .label {{
color: #666;
margin-top: 5px;
}}
.stat-card.success .value {{
color: #28a745;
}}
.stat-card.warning .value {{
color: #ffc107;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}}
th, td {{
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}}
th {{
background: #667eea;
color: white;
}}
tr:hover {{
background-color: #f5f5f5;
}}
.conf-matrix {{
font-size: 14px;
}}
.conf-matrix th, .conf-matrix td {{
text-align: center;
padding: 8px;
}}
.chart-container {{
text-align: center;
margin: 20px 0;
}}
.chart-container img {{
max-width: 100%;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}}
.info-box {{
background: #e3f2fd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #2196f3;
margin: 20px 0;
}}
.info-row {{
display: flex;
margin: 10px 0;
}}
.info-label {{
font-weight: bold;
width: 200px;
color: #555;
}}
.info-value {{
color: #333;
}}
.footer {{
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
font-size: 14px;
}}
@media print {{
body {{
background: white;
padding: 0;
}}
.container {{
box-shadow: none;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Báo Cáo Training Model</h1>
<p class="subtitle">Land Classification - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
</div>
<div class="content">
<!-- Summary Stats -->
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card success">
<div class="value">{train_acc:.1f}%</div>
<div class="label">Train Accuracy</div>
</div>
<div class="stat-card {'success' if test_acc >= 80 else 'warning'}">
<div class="value">{test_acc:.1f}%</div>
<div class="label">Test Accuracy</div>
</div>
<div class="stat-card">
<div class="value">{train_samples}</div>
<div class="label">Training Samples</div>
</div>
<div class="stat-card">
<div class="value">{test_samples}</div>
<div class="label">Testing Samples</div>
</div>
<div class="stat-card">
<div class="value">{len(classes)}</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">{test_size*100:.0f}%</div>
<div class="label">Test Size</div>
</div>
</div>
</div>
<!-- Configuration Info -->
<div class="section">
<h2>⚙️ Cấu Hình Training</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model Type:</span>
<span class="info-value">{model_type.upper()}</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span class="info-value">{bbox}</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span class="info-value">{time_range}</span>
</div>
<div class="info-row">
<span class="info-label">📐 Độ phân giải:</span>
<span class="info-value">{resolution}m</span>
</div>
<div class="info-row">
<span class="info-label">💾 Model Path:</span>
<span class="info-value">{model_path}</span>
</div>
</div>
</div>
<!-- Classification Report -->
<div class="section">
<h2>📋 Classification Report</h2>
<table>
<thead>
<tr>
<th>Loại đất</th>
<th>Precision</th>
<th>Recall</th>
<th>F1-Score</th>
<th>Support</th>
</tr>
</thead>
<tbody>
{cls_report_rows}
</tbody>
</table>
</div>
<!-- Metrics Chart -->
{'<div class="section"><h2>📊 Biểu Đồ Metrics</h2><div class="chart-container"><img src="data:image/png;base64,' + metrics_img + '" alt="Metrics Chart"></div></div>' if metrics_img else ''}
<!-- Class Distribution -->
{'<div class="section"><h2>📊 Phân Bố Số Mẫu</h2><div class="chart-container"><img src="data:image/png;base64,' + class_dist_img + '" alt="Class Distribution"></div></div>' if class_dist_img else ''}
<!-- Confusion Matrix -->
<div class="section">
<h2>🔢 Confusion Matrix</h2>
{'<div class="chart-container"><img src="data:image/png;base64,' + conf_matrix_img + '" alt="Confusion Matrix"></div>' if conf_matrix_img else conf_matrix_table}
</div>
<!-- Classes List -->
<div class="section">
<h2>🏷️ Danh Sách Các Loại Đất</h2>
<div class="info-box">
<ul style="list-style: none; display: flex; flex-wrap: wrap; gap: 10px;">
{''.join([f'<li style="background: #667eea; color: white; padding: 8px 15px; border-radius: 20px;">{cls}</li>' for cls in classes])}
</ul>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification Training System | Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
<p>Data Source: Microsoft Planetary Computer (Sentinel-2 L2A, Sentinel-1 RTC)</p>
</div>
</div>
</body>
</html>
"""
# Save report
reports_dir = Path("reports")
reports_dir.mkdir(exist_ok=True)
report_filename = f"training_report_{timestamp}.html"
report_path = reports_dir / report_filename
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html)
return str(report_path), html
def generate_prediction_report(prediction_result, config=None):
"""
Tạo báo cáo HTML cho kết quả prediction
Args:
prediction_result: Dict chứa kết quả prediction
config: Dict chứa cấu hình prediction (optional)
Returns:
Tuple (report_path, report_html)
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Extract data
output_file = prediction_result.get('output_file', '')
shape = prediction_result.get('shape', [0, 0])
unique_classes = prediction_result.get('unique_classes', [])
bbox = prediction_result.get('bbox', [])
time_range = prediction_result.get('time_range', '')
n_features = prediction_result.get('n_features', 0)
used_radar = prediction_result.get('used_radar', False)
model_used = prediction_result.get('model_used', '')
# Calculate area (approximate)
if len(bbox) == 4:
# Approximate calculation (1 degree ≈ 111km at equator)
width_km = (bbox[2] - bbox[0]) * 111 * 0.85 # cos adjustment for Vietnam
height_km = (bbox[3] - bbox[1]) * 111
area_km2 = width_km * height_km
else:
area_km2 = 0
total_pixels = shape[0] * shape[1] if len(shape) == 2 else 0
# HTML Template
html = f"""
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - {timestamp}</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}}
.header h1 {{
font-size: 2.5em;
margin-bottom: 10px;
}}
.content {{
padding: 40px;
}}
.section {{
margin-bottom: 40px;
}}
.section h2 {{
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}}
.stat-card {{
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}}
.stat-card .value {{
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}}
.stat-card .label {{
color: #666;
margin-top: 5px;
}}
.info-box {{
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}}
.info-row {{
display: flex;
margin: 10px 0;
}}
.info-label {{
font-weight: bold;
width: 200px;
color: #555;
}}
.class-badge {{
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}}
.footer {{
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">{total_pixels:,}</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">{shape[0]}x{shape[1]}</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">{area_km2:.1f}</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">{len(unique_classes)}</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">{n_features}</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value">{'' if used_radar else ''}</div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>{model_used}</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>{bbox}</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>{time_range}</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>{output_file}</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
{''.join([f'<span class="class-badge">{cls}</span>' for cls in unique_classes])}
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
</div>
</div>
</body>
</html>
"""
# Save report
reports_dir = Path("reports")
reports_dir.mkdir(exist_ok=True)
report_filename = f"prediction_report_{timestamp}.html"
report_path = reports_dir / report_filename
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html)
return str(report_path), html
if __name__ == "__main__":
# Test report generation
test_result = {
"success": True,
"train_accuracy": 0.95,
"test_accuracy": 0.87,
"training_samples": 800,
"testing_samples": 200,
"test_size": 0.2,
"classes": ["Lua", "Rung", "Nuoc", "Dan_cu", "Cay_lau_nam"],
"model_type": "xgboost",
"model_path": "model_train/model_xgboost_20251221.joblib",
"bbox": [105.6, 9.3, 106.2, 9.8],
"time_range": "2023-03-01/2023-05-31",
"resolution": 20,
"classification_report": {
"Lua": {"precision": 0.92, "recall": 0.89, "f1-score": 0.90, "support": 50},
"Rung": {"precision": 0.88, "recall": 0.91, "f1-score": 0.89, "support": 45},
"Nuoc": {"precision": 0.95, "recall": 0.93, "f1-score": 0.94, "support": 40},
"Dan_cu": {"precision": 0.85, "recall": 0.82, "f1-score": 0.83, "support": 35},
"Cay_lau_nam": {"precision": 0.80, "recall": 0.85, "f1-score": 0.82, "support": 30},
"macro avg": {"precision": 0.88, "recall": 0.88, "f1-score": 0.88, "support": 200},
"weighted avg": {"precision": 0.88, "recall": 0.87, "f1-score": 0.87, "support": 200}
},
"confusion_matrix": [
[45, 2, 1, 1, 1],
[3, 41, 0, 1, 0],
[1, 0, 37, 1, 1],
[2, 1, 1, 29, 2],
[1, 1, 1, 2, 26]
]
}
path, html = generate_training_report(test_result)
print(f"Report generated: {path}")
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251221_122210</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 21/12/2025 12:22:10</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">9,156,974</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">2774x3301</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">3141.9</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_xgboost_20251221_122105.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.6, 9.3, 106.2, 9.8]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251221_122209.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">3</span><span class="class-badge">5</span><span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 21/12/2025 12:22:10</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251221_171732</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 21/12/2025 17:17:32</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">420</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">20x21</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.1</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">1</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_cnn_20251221_163841.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251221_171732.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 21/12/2025 17:17:32</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251221_172119</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 21/12/2025 17:21:19</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">420</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">20x21</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.1</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">1</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_cnn_20251221_163841.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251221_172118.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 21/12/2025 17:21:19</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251221_172815</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 21/12/2025 17:28:15</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">420</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">20x21</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.1</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">2</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_xgboost_20251221_172351.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251221_172814.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">3</span><span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 21/12/2025 17:28:15</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251221_172829</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 21/12/2025 17:28:29</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">420</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">20x21</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.1</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">2</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_xgboost_20251221_172351.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.16372919082643, 9.182049314243548, 105.16746282577516, 9.185480898286633]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251221_172828.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">3</span><span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 21/12/2025 17:28:29</p>
</div>
</div>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+309
View File
@@ -0,0 +1,309 @@
affine @ file:///home/conda/feedstock_root/build_artifacts/affine_1733762038348/work
aiobotocore==2.25.0
aiohappyeyeballs==2.6.1
aiohttp==3.12.15
aioitertools==0.12.0
aiosignal==1.4.0
alembic==1.16.5
annotated-doc==0.0.4
annotated-types==0.7.0
antimeridian @ file:///home/conda/feedstock_root/build_artifacts/antimeridian_1753706324394/work
anyio @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_anyio_1758634638/work
argon2-cffi @ file:///home/conda/feedstock_root/build_artifacts/argon2-cffi_1749017159514/work
argon2-cffi-bindings @ file:///home/conda/feedstock_root/build_artifacts/argon2-cffi-bindings_1649500328244/work
arrow @ file:///home/conda/feedstock_root/build_artifacts/arrow_1733584251875/work
asciitree==0.3.3
asttokens @ file:///home/conda/feedstock_root/build_artifacts/asttokens_1733250440834/work
async-lru @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_async-lru_1742153708/work
async-timeout==3.0.1
attrs @ file:///home/conda/feedstock_root/build_artifacts/attrs_1741918516150/work
babel @ file:///home/conda/feedstock_root/build_artifacts/babel_1738490167835/work
beautifulsoup4 @ file:///home/conda/feedstock_root/build_artifacts/beautifulsoup4_1759146011391/work
bleach @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_bleach_1737382993/work
blinker==1.9.0
bokeh==3.7.3
boto3==1.40.18
botocore==1.40.49
Bottleneck @ file:///croot/bottleneck_1731058641041/work
branca @ file:///croot/branca_1675157607453/work
Brotli @ file:///croot/brotli-split_1736182456865/work
brotlicffi @ file:///croot/brotlicffi_1736182461069/work
cached-property @ file:///home/conda/feedstock_root/build_artifacts/cached_property_1615209429212/work
cachetools==6.2.0
Cartopy==0.25.0
certifi @ file:///home/conda/feedstock_root/build_artifacts/certifi_1759648874697/work/certifi
cffi @ file:///croot/cffi_1736182485317/work
cftime @ file:///home/conda/feedstock_root/build_artifacts/cftime_1649636873066/work
chardet @ file:///home/conda/feedstock_root/build_artifacts/chardet_1649184137891/work
charset-normalizer @ file:///croot/charset-normalizer_1721748349566/work
ciso8601==2.3.3
click @ file:///home/conda/feedstock_root/build_artifacts/click_1747811314515/work
click-plugins @ file:///home/conda/feedstock_root/build_artifacts/click-plugins_1750848229740/work
cligj @ file:///home/conda/feedstock_root/build_artifacts/cligj_1733749956636/work
cloudpickle @ file:///home/conda/feedstock_root/build_artifacts/cloudpickle_1736947526808/work
colorama==0.4.6
colorcet==3.1.0
comm @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_comm_1753453984/work
contourpy @ file:///croot/contourpy_1732540045555/work
cycler @ file:///tmp/build/80754af9/cycler_1637851556182/work
cytoolz==0.11.2
dask @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_dask-core_1760473436/work
dask-gateway @ file:///Users/runner/miniforge3/conda-bld/bld/rattler-build_dask-gateway_1744370153/work/dask-gateway
dask-glm @ file:///home/conda/feedstock_root/build_artifacts/dask-glm_1701346265909/work
dask-image==2024.5.3
dask-ml @ file:///home/conda/feedstock_root/build_artifacts/dask-ml_1679705292494/work
datacube==1.8.15
datacube_ows==1.9.4
datashader==0.18.2
dea-tools==0.3.0
debugpy @ file:///home/task_175706711740264/conda-bld/debugpy_1757067131873/work
decorator @ file:///home/conda/feedstock_root/build_artifacts/decorator_1740384970518/work
deepdiff==8.6.1
defusedxml @ file:///home/conda/feedstock_root/build_artifacts/defusedxml_1615232257335/work
deprecat @ file:///home/conda/feedstock_root/build_artifacts/deprecat_1734684036993/work
distributed @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_distributed_1760476147/work
eo-tides==0.8.2
exceptiongroup @ file:///home/conda/feedstock_root/build_artifacts/exceptiongroup_1746947292760/work
executing @ file:///home/conda/feedstock_root/build_artifacts/executing_1756729339227/work
fastapi==0.124.3
fasteners @ file:///home/conda/feedstock_root/build_artifacts/fasteners_1734943108928/work
fastjsonschema @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_python-fastjsonschema_1755304154/work/dist
filelock==3.19.1
fiona==1.10.1
Flask==3.1.2
flask-babel==4.0.0
flatbuffers==25.2.10
folium==0.20.0
fonttools @ file:///croot/fonttools_1737039080035/work
fqdn @ file:///home/conda/feedstock_root/build_artifacts/fqdn_1733327382592/work/dist
frozenlist==1.7.0
fsspec @ file:///home/conda/feedstock_root/build_artifacts/fsspec_1756908513222/work
GDAL @ file:///croot/gdal-split_1734448174900/work/build/swig/python
GeoAlchemy2 @ file:///home/conda/feedstock_root/build_artifacts/geoalchemy2_1753372953474/work
geographiclib==2.1
geojson==3.2.0
geomad==1.0.0
geopandas @ file:///croot/geopandas-split_1755761494241/work
geopy==2.4.1
greenlet @ file:///home/conda/feedstock_root/build_artifacts/greenlet_1648882383677/work
h11 @ file:///home/conda/feedstock_root/build_artifacts/h11_1745526374115/work
h2 @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_h2_1756364871/work
h3==4.3.1
hdstats==0.2.1
holoviews==1.21.0
hpack @ file:///home/conda/feedstock_root/build_artifacts/hpack_1737618293087/work
httpcore @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_httpcore_1745602916/work
httpx @ file:///home/conda/feedstock_root/build_artifacts/httpx_1733663348460/work
hvplot==0.12.1
hyperframe @ file:///home/conda/feedstock_root/build_artifacts/hyperframe_1737618333194/work
idna==3.10
imagecodecs==2025.3.30
imageio==2.37.0
importlib_metadata @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_importlib-metadata_1747934053/work
ipykernel @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_ipykernel_1760459840/work
ipyleaflet==0.20.0
ipython @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_ipython_1748711175/work
ipywidgets==8.1.7
iso8601==2.1.0
isoduration @ file:///home/conda/feedstock_root/build_artifacts/isoduration_1733493628631/work/dist
itsdangerous==2.2.0
jedi @ file:///home/conda/feedstock_root/build_artifacts/jedi_1733300866624/work
Jinja2 @ file:///croot/jinja2_1741710844255/work
jmespath @ file:///home/conda/feedstock_root/build_artifacts/jmespath_1733229141657/work
joblib @ file:///home/conda/feedstock_root/build_artifacts/joblib_1756321760188/work
json5 @ file:///home/conda/feedstock_root/build_artifacts/json5_1755034879854/work
jsonpointer @ file:///home/conda/feedstock_root/build_artifacts/jsonpointer_1756754132747/work
jsonschema @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_jsonschema_1755595646/work
jsonschema-specifications==2025.4.1
jupyter-events @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_jupyter_events_1738765986/work
jupyter-leaflet==0.20.0
jupyter-lsp @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_jupyter-lsp_1756388269/work/jupyter-lsp
jupyter-ui-poll==1.0.0
jupyter_client @ file:///home/conda/feedstock_root/build_artifacts/jupyter_client_1733440914442/work
jupyter_core @ file:///home/conda/feedstock_root/build_artifacts/jupyter_core_1748333051527/work
jupyter_server @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_jupyter_server_1755870522/work
jupyter_server_terminals @ file:///home/conda/feedstock_root/build_artifacts/jupyter_server_terminals_1733427956852/work
jupyterlab @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_1758913905644/work
jupyterlab_pygments @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_pygments_1733328101776/work
jupyterlab_server @ file:///home/conda/feedstock_root/build_artifacts/jupyterlab_server_1733599573484/work
jupyterlab_widgets==3.0.15
kiwisolver @ file:///croot/kiwisolver_1737039087198/work
lark==1.2.2
lark-parser==0.12.0
lazy_loader==0.4
linkify-it-py==2.0.3
llvmlite @ file:///croot/llvmlite_1741209858218/work
locket @ file:///home/conda/feedstock_root/build_artifacts/locket_1650660393415/work
lxml==5.4.0
lz4 @ file:///croot/lz4_1736366683208/work
Mako @ file:///home/conda/feedstock_root/build_artifacts/mako_1744317760971/work
mapclassify @ file:///croot/mapclassify_1675157730177/work
Markdown==3.9
markdown-it-py==4.0.0
MarkupSafe @ file:///croot/markupsafe_1738584038848/work
matplotlib==3.10.5
matplotlib-inline @ file:///home/conda/feedstock_root/build_artifacts/matplotlib-inline_1733416936468/work
mdit-py-plugins==0.5.0
mdurl==0.1.2
mistune @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_mistune_1756495311/work
mpmath==1.3.0
msgpack @ file:///home/conda/feedstock_root/build_artifacts/msgpack-python_1648745999384/work
multidict @ file:///home/conda/feedstock_root/build_artifacts/multidict_1648882415384/work
multipledispatch @ file:///home/conda/feedstock_root/build_artifacts/multipledispatch_1721907546485/work
narwhals==2.3.0
nbclient @ file:///home/conda/feedstock_root/build_artifacts/nbclient_1734628800805/work
nbconvert @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_nbconvert-core_1738067871/work
nbformat @ file:///home/conda/feedstock_root/build_artifacts/nbformat_1733402752141/work
nest_asyncio @ file:///home/conda/feedstock_root/build_artifacts/nest-asyncio_1733325553580/work
netCDF4 @ file:///croot/netcdf4_1743512888672/work
networkx @ file:///croot/networkx_1737039604450/work
notebook @ file:///home/conda/feedstock_root/build_artifacts/notebook_1759152069573/work
notebook_shim @ file:///home/conda/feedstock_root/build_artifacts/notebook-shim_1733408315203/work
numba @ file:///croot/numba_1750798165355/work
numcodecs @ file:///croot/numcodecs_1707513121886/work
numexpr @ file:///croot/numexpr_1755766469354/work
numpy @ file:///croot/numpy_and_numpy_base_1755590845055/work/dist/numpy-1.26.4-cp310-cp310-linux_x86_64.whl#sha256=1096d33ad9a9757a1b4b46634d809e894263fc8b78780bff36801684b6e8cc88
nvidia-cublas-cu12==12.8.4.1
nvidia-cuda-cupti-cu12==12.8.90
nvidia-cuda-nvrtc-cu12==12.8.93
nvidia-cuda-runtime-cu12==12.8.90
nvidia-cudnn-cu12==9.10.2.21
nvidia-cufft-cu12==11.3.3.83
nvidia-cufile-cu12==1.13.1.3
nvidia-curand-cu12==10.3.9.90
nvidia-cusolver-cu12==11.7.3.90
nvidia-cusparse-cu12==12.5.8.93
nvidia-cusparselt-cu12==0.7.1
nvidia-nccl-cu12==2.27.3
nvidia-nvjitlink-cu12==12.8.93
nvidia-nvtx-cu12==12.8.90
odc-algo==0.2.3
odc-geo==0.4.10
odc-io==0.2.2
odc-loader @ file:///home/conda/feedstock_root/build_artifacts/odc-loader_1743656085024/work
odc-stac @ file:///home/conda/feedstock_root/build_artifacts/odc-stac_1746136311934/work
odc-ui==0.2.1
orderly-set==5.5.0
overrides @ file:///home/conda/feedstock_root/build_artifacts/overrides_1734587627321/work
OWSLib==0.34.1
packaging @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_packaging_1745345660/work
pandas @ file:///home/task_175982153789305/conda-bld/pandas_1759822248912/work/dist/pandas-2.3.3-cp310-cp310-linux_x86_64.whl#sha256=0de7c83109c411cc2a74419a396c92f65e3d1e457fb4d835e5f100cfb04393a7
pandocfilters @ file:///home/conda/feedstock_root/build_artifacts/pandocfilters_1631603243851/work
panel==1.7.5
param==2.2.1
parso @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_parso_1755974222/work
partd @ file:///home/conda/feedstock_root/build_artifacts/partd_1715026491486/work
pexpect @ file:///home/conda/feedstock_root/build_artifacts/pexpect_1733301927746/work
pickleshare @ file:///home/conda/feedstock_root/build_artifacts/pickleshare_1733327343728/work
pillow @ file:///croot/pillow_1738010226202/work
PIMS==0.7
planetary-computer==1.0.0
platformdirs @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_platformdirs_1756227402/work
prometheus_client==0.22.1
prometheus_flask_exporter==0.23.2
prompt_toolkit @ file:///home/conda/feedstock_root/build_artifacts/prompt-toolkit_1756321756983/work
propcache==0.3.2
psutil @ file:///home/conda/feedstock_root/build_artifacts/psutil_1653089181607/work
psycopg2 @ file:///croot/psycopg2_1744919787325/work
ptyprocess @ file:///home/conda/feedstock_root/build_artifacts/ptyprocess_1733302279685/work/dist/ptyprocess-0.7.0-py2.py3-none-any.whl#sha256=92c32ff62b5fd8cf325bec5ab90d7be3d2a8ca8c8a3813ff487a8d2002630d1f
pure_eval @ file:///home/conda/feedstock_root/build_artifacts/pure_eval_1733569405015/work
pyarrow @ file:///home/task_175983338836370/conda-bld/pyarrow_1759833584228/work/python
pycparser @ file:///tmp/build/80754af9/pycparser_1636541352034/work
pyct==0.5.0
pydantic==2.11.7
pydantic_core==2.33.2
Pygments @ file:///home/conda/feedstock_root/build_artifacts/pygments_1750615794071/work
pyogrio @ file:///croot/pyogrio_1741107161422/work
pyows==0.3.1
pyparsing @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_pyparsing_1753873557/work
pyproj @ file:///croot/pyproj_1739284761968/work
PyQt6==6.7.1
PyQt6_sip @ file:///croot/pyqt-split_1753427276959/work/pyqt_sip
pyshp==2.3.1
PySocks @ file:///home/builder/ci_310/pysocks_1640793678128/work
pystac @ file:///home/conda/feedstock_root/build_artifacts/pystac_1758218055393/work
pystac-client==0.9.0
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-json-logger @ file:///home/conda/feedstock_root/build_artifacts/python-json-logger_1677079630776/work
python-slugify==8.0.4
pyTMD==2.2.8
pytz @ file:///home/conda/feedstock_root/build_artifacts/pytz_1742920838005/work
pyviz_comms==3.0.6
PyYAML==6.0.2
pyzmq @ file:///croot/pyzmq_1734687138743/work
rasterio @ file:///croot/rasterio_1740069178893/work
rasterstats==0.20.0
referencing==0.36.2
regex==2025.9.1
requests @ file:///croot/requests_1756709366904/work
rfc3339_validator @ file:///home/conda/feedstock_root/build_artifacts/rfc3339-validator_1733599910982/work
rfc3986-validator @ file:///home/conda/feedstock_root/build_artifacts/rfc3986-validator_1598024191506/work
rfc3987==1.3.8
rfc3987-syntax @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_rfc3987-syntax_1752876729/work
rioxarray @ file:///home/conda/feedstock_root/build_artifacts/rioxarray_1737140588464/work
rpds-py @ file:///croot/rpds-py_1736541261634/work
ruamel.yaml @ file:///home/conda/feedstock_root/build_artifacts/ruamel.yaml_1649033201098/work
ruamel.yaml.clib==0.2.12
s3fs==2025.9.0
s3transfer==0.13.1
scikit-image==0.25.2
scikit-learn==1.7.1
scipy @ file:///croot/scipy_1747238027288/work/dist/scipy-1.15.3-cp310-cp310-linux_x86_64.whl#sha256=2a791554880ad4f358fcc4cd2a982ffe1e9d472e9241011216b2be797457f1f9
seaborn==0.13.2
Send2Trash @ file:///home/conda/feedstock_root/build_artifacts/send2trash_1733322040660/work
setuptools-scm==9.2.0
shapely @ file:///croot/shapely_1754380812723/work
simplejson==3.20.1
sip @ file:///croot/sip_1738856193618/work
six==1.17.0
slicerator==1.1.0
sniffio @ file:///home/conda/feedstock_root/build_artifacts/sniffio_1733244044561/work
snuggs @ file:///home/conda/feedstock_root/build_artifacts/snuggs_1733818638588/work
sortedcontainers @ file:///home/conda/feedstock_root/build_artifacts/sortedcontainers_1738440353519/work
soupsieve @ file:///home/conda/feedstock_root/build_artifacts/soupsieve_1756330469801/work
sparse @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_sparse_1747799051/work
SQLAlchemy==1.4.54
stack_data @ file:///home/conda/feedstock_root/build_artifacts/stack_data_1733569443808/work
starlette==0.50.0
sympy==1.14.0
tblib @ file:///home/conda/feedstock_root/build_artifacts/tblib_1743515515538/work
terminado @ file:///home/conda/feedstock_root/build_artifacts/terminado_1710262609923/work
text-unidecode==1.3
threadpoolctl @ file:///home/conda/feedstock_root/build_artifacts/threadpoolctl_1741878222898/work
tifffile==2025.5.10
timescale==0.0.9
timezonefinder==8.0.0
tinycss2 @ file:///home/conda/feedstock_root/build_artifacts/tinycss2_1729802851396/work
tomli @ file:///croot/tomli_1753774587605/work
toolz @ file:///home/conda/feedstock_root/build_artifacts/toolz_1733736030883/work
torch==2.8.0
tornado @ file:///croot/tornado_1748956929273/work
tqdm==4.67.1
traitlets @ file:///home/conda/feedstock_root/build_artifacts/traitlets_1733367359838/work
traittypes==0.2.1
triton==3.4.0
types-python-dateutil @ file:///home/conda/feedstock_root/build_artifacts/types-python-dateutil_1759899809376/work
typing-inspection==0.4.1
typing_extensions @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_typing_extensions_1756220668/work
typing_utils @ file:///home/conda/feedstock_root/build_artifacts/typing_utils_1733331286120/work
tzdata @ file:///croot/python-tzdata_1746123641790/work
uc-micro-py==1.0.3
unicodedata2 @ file:///croot/unicodedata2_1736541023050/work
uri-template @ file:///home/conda/feedstock_root/build_artifacts/uri-template_1733323593477/work/dist
urllib3 @ file:///croot/urllib3_1750775463400/work
uvicorn==0.38.0
wcwidth @ file:///home/conda/feedstock_root/build_artifacts/wcwidth_1733231326287/work
webcolors @ file:///home/conda/feedstock_root/build_artifacts/webcolors_1733359735138/work
webencodings @ file:///home/conda/feedstock_root/build_artifacts/webencodings_1733236011802/work
websocket-client @ file:///home/conda/feedstock_root/build_artifacts/websocket-client_1759928050786/work
Werkzeug==3.1.3
widgetsnbextension==4.0.14
wrapt @ file:///home/conda/feedstock_root/build_artifacts/wrapt_1651495243689/work
xarray @ file:///home/conda/feedstock_root/build_artifacts/xarray_1749743207754/work
xgboost==3.1.2
xyzservices @ file:///croot/xyzservices_1675159059961/work
yarl==1.20.1
zarr @ file:///home/conda/feedstock_root/build_artifacts/zarr_1733237197728/work
zict @ file:///home/conda/feedstock_root/build_artifacts/zict_1733261551178/work
zipp @ file:///home/conda/feedstock_root/build_artifacts/zipp_1749421620841/work
+3
View File
@@ -0,0 +1,3 @@
fastapi
uvicorn
pydantic
Executable
+1
View File
@@ -0,0 +1 @@
uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
+2368
View File
File diff suppressed because one or more lines are too long
+191
View File
@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""
Demo script để test các chức năng mới của API
"""
import requests
import json
import time
from pathlib import Path
BASE_URL = "http://localhost:8000"
def print_section(title):
print("\n" + "=" * 70)
print(f" {title}")
print("=" * 70)
def test_dashboard_statistics():
print_section("📊 Test Dashboard Statistics")
try:
response = requests.get(f"{BASE_URL}/api/dashboard/statistics")
if response.status_code == 200:
data = response.json()
print(f"✅ Success!")
print(f" Models: {data['models']['total']}")
print(f" Predictions: {data['predictions']['total']}")
print(f" Reports: {data['reports']['total']}")
else:
print(f"❌ Error: {response.status_code}")
except Exception as e:
print(f"❌ Exception: {e}")
def test_accuracy_trends():
print_section("📈 Test Accuracy Trends")
try:
response = requests.get(f"{BASE_URL}/api/dashboard/accuracy-trends")
if response.status_code == 200:
data = response.json()
print(f"✅ Success!")
print(f" Trends: {len(data['trends'])} records")
print(f" Models: {data['models']}")
else:
print(f"❌ Error: {response.status_code}")
except Exception as e:
print(f"❌ Exception: {e}")
def test_class_distribution():
print_section("📊 Test Class Distribution")
try:
# First, get list of models
response = requests.get(f"{BASE_URL}/api/models/list")
if response.status_code == 200:
models = response.json()['models']
if models:
model_filename = models[0]['filename']
print(f" Using model: {model_filename}")
# Get class distribution
response = requests.get(f"{BASE_URL}/api/dashboard/class-distribution/{model_filename}")
if response.status_code == 200:
data = response.json()
print(f"✅ Success!")
print(f" Total samples: {data['total_samples']}")
print(f" Classes: {list(data['class_distribution'].keys())}")
else:
print(f"❌ Error: {response.status_code}")
else:
print("⚠️ No models found")
else:
print(f"❌ Error getting models: {response.status_code}")
except Exception as e:
print(f"❌ Exception: {e}")
def test_batch_status():
print_section("🔄 Test Batch Status")
try:
response = requests.get(f"{BASE_URL}/api/batch/status")
if response.status_code == 200:
data = response.json()
print(f"✅ Success!")
print(f" Queued: {data['queue']['queued']}")
print(f" Running: {data['queue']['running']}")
print(f" Completed: {data['queue']['completed']}")
print(f" Failed: {data['queue']['failed']}")
else:
print(f"❌ Error: {response.status_code}")
except Exception as e:
print(f"❌ Exception: {e}")
def test_batch_prediction_demo():
print_section("🚀 Test Batch Prediction (Demo)")
try:
# Get a model
response = requests.get(f"{BASE_URL}/api/models/list")
if response.status_code != 200:
print("❌ Cannot get models list")
return
models = response.json()['models']
if not models:
print("⚠️ No models available for testing")
return
model_filename = models[0]['filename']
print(f" Using model: {model_filename}")
# Create test batch
batch_config = {
"model_filename": model_filename,
"items": [
{
"name": "Test_Region_1",
"min_lon": 105.6,
"min_lat": 9.3,
"max_lon": 105.7,
"max_lat": 9.4,
"start_date": "2023-03-01",
"end_date": "2023-03-31",
"max_scenes": 5,
"cloud_cover": 30,
"resolution": 20
}
],
"auto_retry": True,
"max_retries": 2
}
print(" Creating batch job...")
response = requests.post(
f"{BASE_URL}/api/batch/start",
json=batch_config
)
if response.status_code == 200:
data = response.json()
print(f"✅ Success!")
print(f" {data['message']}")
print(f" Batch ID: {data['batch_id']}")
# Check status after a moment
time.sleep(2)
response = requests.get(f"{BASE_URL}/api/batch/status")
if response.status_code == 200:
status = response.json()
print(f" Current queue: {status['queue']}")
else:
print(f"❌ Error: {response.status_code} - {response.text}")
except Exception as e:
print(f"❌ Exception: {e}")
def test_reports_list():
print_section("📄 Test Reports List")
try:
response = requests.get(f"{BASE_URL}/api/reports/list")
if response.status_code == 200:
data = response.json()
print(f"✅ Success!")
print(f" Total reports: {data['count']}")
if data['reports']:
print(f" Latest report: {data['reports'][0]['filename']}")
else:
print(f"❌ Error: {response.status_code}")
except Exception as e:
print(f"❌ Exception: {e}")
def main():
print("=" * 70)
print(" 🧪 API Testing Suite - New Features")
print("=" * 70)
print(f"\n Base URL: {BASE_URL}")
print(f" Đảm bảo server đang chạy: python api_server.py")
input("\n Press ENTER to start testing...")
# Run all tests
test_dashboard_statistics()
test_accuracy_trends()
test_class_distribution()
test_reports_list()
test_batch_status()
test_batch_prediction_demo()
print("\n" + "=" * 70)
print(" ✅ Testing completed!")
print("=" * 70)
print(f"\n Dashboard: {BASE_URL}/dashboard")
print(f" API Docs: {BASE_URL}/docs")
print("=" * 70 + "\n")
if __name__ == "__main__":
main()
+558
View File
@@ -0,0 +1,558 @@
"""
Training module for land classification using Sentinel-2 and Sentinel-1 data
from Microsoft Planetary Computer STAC API
"""
import numpy as np
import xarray as xr
import geopandas as gpd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from xgboost import XGBClassifier
import joblib
from datetime import datetime
import json
import os
import warnings
import hashlib
from pathlib import Path
warnings.filterwarnings('ignore')
# PyTorch for CNN
try:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
PYTORCH_AVAILABLE = True
except ImportError:
PYTORCH_AVAILABLE = False
print("Warning: PyTorch not available. CNN model will not work.")
# Define CNN model class for PyTorch
class CNNClassifier(nn.Module):
def __init__(self, n_features, n_classes):
super(CNNClassifier, self).__init__()
self.n_features = n_features
self.n_classes = n_classes
# For small feature sets (like 3 features), use simpler architecture
if n_features < 8:
# Simple fully connected network for small features
self.use_conv = False
self.fc1 = nn.Linear(n_features, 64)
self.dropout1 = nn.Dropout(0.3)
self.fc2 = nn.Linear(64, 128)
self.dropout2 = nn.Dropout(0.5)
self.fc3 = nn.Linear(128, n_classes)
else:
# CNN architecture for larger feature sets
self.use_conv = True
self.conv1 = nn.Conv1d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
self.pool1 = nn.MaxPool1d(kernel_size=2)
self.conv2 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
self.pool2 = nn.MaxPool1d(kernel_size=2)
# Calculate size after convolutions
conv_output_size = (n_features // 2 // 2) * 64
# Fully connected layers
self.fc1 = nn.Linear(conv_output_size, 128)
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(128, n_classes)
def forward(self, x):
# x shape: (batch, n_features) or (batch, 1, n_features)
if self.use_conv:
# CNN path for larger feature sets
if len(x.shape) == 2:
x = x.unsqueeze(1) # Add channel dimension
x = F.relu(self.conv1(x))
x = self.pool1(x)
x = F.relu(self.conv2(x))
x = self.pool2(x)
x = x.view(x.size(0), -1) # Flatten
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
else:
# Fully connected path for small feature sets
if len(x.shape) == 3:
x = x.squeeze(1) # Remove channel dimension if present
x = F.relu(self.fc1(x))
x = self.dropout1(x)
x = F.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
def predict(self, X):
"""Scikit-learn style predict method"""
self.eval()
with torch.no_grad():
if isinstance(X, np.ndarray):
X = torch.FloatTensor(X)
# Handle both 2D and 3D inputs
if not self.use_conv and len(X.shape) == 3:
X = X.squeeze(1)
elif self.use_conv and len(X.shape) == 2:
X = X.unsqueeze(1)
outputs = self(X)
_, predicted = torch.max(outputs, 1)
return predicted.cpu().numpy()
def score(self, X, y):
"""Scikit-learn style score method"""
predictions = self.predict(X)
if isinstance(y, torch.Tensor):
y = y.cpu().numpy()
return np.mean(predictions == y)
# Microsoft Planetary Computer imports
import planetary_computer
from pystac_client import Client
from odc.stac import load as stac_load
def train_model(
bbox=[105.6, 9.3, 106.2, 9.8],
time_range='2023-03-01/2023-05-31',
max_scenes=12,
cloud_cover=30,
resolution=20,
training_shapefile='train/ST_training data_updated_1130points_new.shp',
model_type='xgboost',
n_estimators=100,
max_depth=20,
learning_rate=0.1,
use_gpu=True,
use_cache=True,
test_size=0.2,
output_model_path=None,
status_callback=None,
cancel_check=None
):
"""
Train a land classification model using Sentinel-2 and Sentinel-1 data
Args:
bbox: [min_lon, min_lat, max_lon, max_lat]
time_range: "YYYY-MM-DD/YYYY-MM-DD"
max_scenes: maximum number of scenes to load
cloud_cover: maximum cloud cover percentage
resolution: resolution in meters (e.g., 20)
training_shapefile: path to training shapefile
n_estimators: number of trees for XGBoost
max_depth: maximum tree depth
learning_rate: learning rate for XGBoost
use_gpu: whether to use GPU for training
output_model_path: path to save trained model (auto-generated if None)
status_callback: Optional callback function to report progress
cancel_check: Optional function that returns True if training should be cancelled
test_size: Fraction of data to use for test set (0-1)
Returns:
Dictionary containing training results
"""
def update_status(message, progress=None):
"""Helper to update status"""
if status_callback:
# Try calling with both arguments, fallback to just message
try:
status_callback(message, progress)
except TypeError:
status_callback(message)
print(message)
def check_cancellation():
"""Check if training should be cancelled"""
if cancel_check and cancel_check():
raise InterruptedError("Training cancelled by user")
try:
# Auto-generate output path if not provided
if output_model_path is None:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_model_path = f'model_train/model_{model_type}_{timestamp}.joblib'
# ============ CACHE SYSTEM ============
# Create cache directory
cache_dir = Path("dataset_cache")
cache_dir.mkdir(exist_ok=True)
# Generate cache key from parameters
cache_params = f"{bbox}_{time_range}_{max_scenes}_{cloud_cover}_{resolution}"
cache_key = hashlib.md5(cache_params.encode()).hexdigest()
cache_file = cache_dir / f"training_data_{cache_key}.joblib"
features = None
labels = None
# Try to load from cache
if use_cache and cache_file.exists():
update_status(f"📦 Loading cached dataset from {cache_file.name}...", 5)
try:
cached_data = joblib.load(cache_file)
features = cached_data['features']
labels = cached_data['labels']
update_status(f"✅ Loaded {len(features)} samples from cache (skipped satellite download!)", 50)
except Exception as e:
update_status(f"⚠️ Cache load failed: {str(e)}, downloading fresh data...", 10)
features = None
# If no cache or cache failed, download data
if features is None:
update_status("📡 Cache not found or disabled, downloading satellite data...", 10)
# Connect to Microsoft Planetary Computer
update_status("Connecting to Microsoft Planetary Computer...", 12)
catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
check_cancellation()
# Search for Sentinel-2 scenes
update_status("Searching for Sentinel-2 scenes...", 10)
query_s2 = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": cloud_cover}}
)
items_s2 = list(query_s2.item_collection())
check_cancellation()
# Limit scenes
if len(items_s2) > max_scenes:
step = len(items_s2) // max_scenes
items_s2 = items_s2[::step][:max_scenes]
update_status(f"Found {len(items_s2)} Sentinel-2 scenes", 20)
# Sign and load Sentinel-2 data
update_status("Loading Sentinel-2 data...", 25)
items_s2 = [planetary_computer.sign(item) for item in items_s2]
ds_s2 = stac_load(
items_s2,
bands=["B04", "B08", "SCL"],
crs="EPSG:32648",
resolution=resolution,
bbox=bbox,
patch_url=planetary_computer.sign,
fail_on_error=False,
)
ds_s2 = ds_s2.rename({"B04": "red", "B08": "nir", "SCL": "scl"})
check_cancellation()
# Search for Sentinel-1 scenes
update_status("Searching for Sentinel-1 scenes...", 35)
query_s1 = catalog.search(
collections=["sentinel-1-rtc"],
bbox=bbox,
datetime=time_range,
)
items_s1 = list(query_s1.item_collection())
# Limit scenes
if len(items_s1) > max_scenes:
step = len(items_s1) // max_scenes
items_s1 = items_s1[::step][:max_scenes]
update_status(f"Found {len(items_s1)} Sentinel-1 scenes", 40)
# Sign and load Sentinel-1 data
update_status("Loading Sentinel-1 data...", 45)
items_s1 = [planetary_computer.sign(item) for item in items_s1]
ds_s1 = stac_load(
items_s1,
bands=["vv", "vh"],
crs="EPSG:32648",
resolution=resolution,
bbox=bbox,
patch_url=planetary_computer.sign,
fail_on_error=False,
)
# Convert to dB
ds_s1['vv_db'] = 10 * np.log10(ds_s1['vv'].where(ds_s1['vv'] > 0))
ds_s1['vh_db'] = 10 * np.log10(ds_s1['vh'].where(ds_s1['vh'] > 0))
check_cancellation()
# Calculate NDVI
update_status("Calculating NDVI...", 50)
ndvi = (ds_s2['nir'] - ds_s2['red']) / (ds_s2['nir'] + ds_s2['red'] + 1e-8)
# Apply cloud mask
cloud_mask = ds_s2['scl'].isin([1, 3, 8, 9, 10])
ndvi_masked = ndvi.where(~cloud_mask)
ndvi_mean = ndvi_masked.mean(dim='time')
# Load training data
update_status("Loading training data...", 55)
train_gdf = gpd.read_file(training_shapefile)
if train_gdf.crs != 'EPSG:32648':
train_gdf = train_gdf.to_crs('EPSG:32648')
# Auto-detect label column
label_column = None
for col in ['HT_code', 'Ma_LU', 'LU2022', 'Hientrang', 'class', 'Class', 'CLASS']:
if col in train_gdf.columns:
label_column = col
break
if label_column is None:
raise ValueError(f"Cannot find label column in shapefile. Available: {list(train_gdf.columns)}")
# Extract features
update_status("Extracting features from training points...", 60)
features = []
labels = []
for idx, row in train_gdf.iterrows():
point = row.geometry
x_coord = point.x
y_coord = point.y
label = row[label_column]
try:
ndvi_val = ndvi_mean.sel(x=x_coord, y=y_coord, method='nearest').values
vh_val = ds_s1['vh_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values
vv_val = ds_s1['vv_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values
feature_vec = [ndvi_val, vh_val, vv_val]
if not np.isnan(feature_vec).any():
features.append(feature_vec)
labels.append(label)
except:
continue
features = np.array(features)
labels = np.array(labels)
check_cancellation()
update_status(f"Extracted {len(features)} valid training samples", 70)
# ============ SAVE TO CACHE ============
if use_cache:
update_status(f"💾 Saving dataset to cache for future use...", 72)
try:
cache_data = {
'features': features,
'labels': labels,
'bbox': bbox,
'time_range': time_range,
'resolution': resolution,
'timestamp': datetime.now().isoformat()
}
joblib.dump(cache_data, cache_file)
update_status(f"✅ Cached to {cache_file.name}", 75)
except Exception as e:
update_status(f"⚠️ Cache save failed: {str(e)}", 75)
# Encode labels
label_encoder = LabelEncoder()
labels_encoded = label_encoder.fit_transform(labels)
# Split data
X_train, X_test, y_train, y_test = train_test_split(
features, labels_encoded, test_size=test_size, random_state=42, stratify=labels_encoded
)
# Train model based on selected type
update_status(f"Training {model_type.upper()} model...", 75)
device = 'cuda:0' if use_gpu else 'cpu'
if model_type == 'xgboost':
model = XGBClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
learning_rate=learning_rate,
device=device if use_gpu else 'cpu',
tree_method='hist',
random_state=42,
eval_metric='mlogloss',
verbosity=0
)
elif model_type == 'random_forest':
model = RandomForestClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
random_state=42,
n_jobs=-1, # Use all cores
verbose=0
)
elif model_type == 'decision_tree':
model = DecisionTreeClassifier(
max_depth=max_depth,
random_state=42
)
elif model_type == 'svm':
model = SVC(
kernel='rbf',
random_state=42,
verbose=False
)
elif model_type == 'cnn':
if not PYTORCH_AVAILABLE:
raise ImportError("PyTorch is required for CNN. Install: pip install torch")
# CNN requires reshaping data
n_features = X_train.shape[1]
n_classes = len(np.unique(y_train))
# Build PyTorch CNN model
device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu')
update_status(f"Building CNN model on {device}...", 75)
model = CNNClassifier(n_features, n_classes).to(device)
# Convert to PyTorch tensors
X_train_tensor = torch.FloatTensor(X_train).unsqueeze(1) # Add channel dim: (N, 1, features)
y_train_tensor = torch.LongTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test).unsqueeze(1)
y_test_tensor = torch.LongTensor(y_test)
# Create data loaders
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
# Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Train CNN
update_status("Training CNN model with PyTorch...", 80)
epochs = min(50, n_estimators // 2) # Use n_estimators as epochs
model.train()
for epoch in range(epochs):
epoch_loss = 0.0
for batch_X, batch_y in train_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
if (epoch + 1) % 10 == 0:
avg_loss = epoch_loss / len(train_loader)
update_status(f"CNN Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}", 80 + (epoch / epochs) * 10)
# Move model to CPU for saving (compatible with non-GPU systems)
model = model.cpu()
model.device_used = str(device)
else:
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn")
# Fit non-CNN models
if model_type != 'cnn':
model.fit(X_train, y_train)
# Evaluate
update_status("Evaluating model...", 90)
if model_type == 'cnn':
# PyTorch CNN evaluation
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
y_pred = model.predict(X_test)
else:
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
y_pred = model.predict(X_test)
# Generate classification report and confusion matrix
update_status("Generating classification report...", 92)
class_names = label_encoder.classes_.tolist()
# Classification report as dict
from sklearn.metrics import classification_report, confusion_matrix
cls_report = classification_report(y_test, y_pred, target_names=class_names, output_dict=True, zero_division=0)
# Confusion matrix
conf_matrix = confusion_matrix(y_test, y_pred).tolist()
# Save model
update_status("Saving model...", 95)
os.makedirs(os.path.dirname(output_model_path), exist_ok=True)
joblib.dump({'model': model, 'label_encoder': label_encoder}, output_model_path)
# Save model info
info = {
"timestamp": datetime.now().isoformat(),
"data_source": "Microsoft Planetary Computer STAC",
"collections": ["sentinel-2-l2a", "sentinel-1-rtc"],
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
"training_samples": len(X_train),
"testing_samples": len(X_test),
"test_size": test_size,
"train_accuracy": float(train_score),
"test_accuracy": float(test_score),
"model_type": model_type,
"device": device if model_type == 'xgboost' else 'cpu',
"n_estimators": n_estimators if model_type in ['xgboost', 'random_forest', 'cnn'] else None,
"max_depth": max_depth if model_type != 'cnn' else None,
"learning_rate": learning_rate if model_type == 'xgboost' else None,
"cnn_epochs": min(50, n_estimators // 2) if model_type == 'cnn' else None,
"n_features": X_train.shape[1],
"n_classes": len(np.unique(y_train)),
"class_names": class_names,
"classification_report": cls_report,
"confusion_matrix": conf_matrix,
"bbox": bbox,
"time_range": time_range,
"resolution": resolution
}
info_path = output_model_path.replace('.joblib', '_info.json')
with open(info_path, 'w') as f:
json.dump(info, f, indent=2)
update_status("Training complete!", 100)
return {
"success": True,
"model_path": output_model_path,
"info_path": info_path,
"train_accuracy": train_score,
"test_accuracy": test_score,
"training_samples": len(X_train),
"testing_samples": len(X_test),
"test_size": test_size,
"classes": class_names,
"classification_report": cls_report,
"confusion_matrix": conf_matrix,
"model_type": model_type,
"bbox": bbox,
"time_range": time_range,
"resolution": resolution
}
except InterruptedError as e:
update_status(f"Cancelled: {str(e)}", -1)
return {
"success": False,
"error": str(e),
"cancelled": True
}
except Exception as e:
update_status(f"Error: {str(e)}", -1)
return {
"success": False,
"error": str(e)
}
File diff suppressed because it is too large Load Diff