Compare commits
7 Commits
new-feature
...
dev01
| Author | SHA1 | Date | |
|---|---|---|---|
| 7660bcb37c | |||
| d9e54bfb28 | |||
| aec4d213cf | |||
| dcf80c8295 | |||
| 46da481029 | |||
| 82aa814777 | |||
| 2d7b3b1011 |
@@ -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
@@ -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/
|
||||
@@ -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
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:conda",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 $3–5/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, ~8–12 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` và `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
@@ -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
|
||||
}
|
||||
@@ -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
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
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
@@ -0,0 +1 @@
|
||||
uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
|
||||
+2368
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
Reference in New Issue
Block a user