Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 623bc9c7dd | |||
| 174de1034b | |||
| d1a4534653 | |||
| 57fbdede1b | |||
| c60dea728f | |||
| d96c0c9d69 | |||
| 0d48844822 | |||
| c80b43eeac | |||
| ca71aff891 | |||
| 94eca6bb29 | |||
| c40aafef03 | |||
| 0e8d85d972 | |||
| 208f1fab6e | |||
| b0fba1a5ff | |||
| bb25aa12cc | |||
| 2ffe74357e | |||
| e684d9c5c1 | |||
| bc678f3e6b | |||
| 94ebec25f2 | |||
| 3199ff242a | |||
| 06ef6bee92 | |||
| ad40578284 | |||
| 120cd84b66 | |||
| d2834725a2 | |||
| 3712787689 | |||
| 3783bdf678 | |||
| 2854ad170d | |||
| 3a32ccd90a | |||
| df7294eb1a | |||
| 95ed7cfc08 | |||
| 6c2baeb7d5 | |||
| fb67ca6498 | |||
| 62b07c52e3 | |||
| 44626c7e34 | |||
| 95bfb52281 | |||
| 47d9aa99f3 | |||
| ea2c49104c | |||
| f31982c50a | |||
| ac0a1b6e5f | |||
| 23bc443245 | |||
| 2f49eec082 | |||
| 5daf9882a7 |
+4
-5
@@ -1,5 +1,4 @@
|
|||||||
# Disabled LFS tracking to avoid pushing large files
|
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||||
#*.tif filter=lfs diff=lfs merge=lfs -text
|
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||||
#*.joblib filter=lfs diff=lfs merge=lfs -text
|
*.nc filter=lfs diff=lfs merge=lfs -text
|
||||||
#*.nc filter=lfs diff=lfs merge=lfs -text
|
*.ipynb filter=lfs diff=lfs merge=lfs -text
|
||||||
#*.ipynb filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
|||||||
+7
-84
@@ -1,84 +1,7 @@
|
|||||||
# Ignore all model weights and large data
|
ThuanHoa/ThuanHoa_VH.tif
|
||||||
*.joblib
|
ThuanHoa/ThuanHoa_VV.tif
|
||||||
*.nc
|
model_train/model.joblib
|
||||||
*.tif
|
model_train/model_new.joblib
|
||||||
*.tiff
|
backup_model_train/model.joblib
|
||||||
*.png
|
backup_model_train/model_new.joblib
|
||||||
*.jpg
|
dataset_cache/sentinel2_timeseries_40scenes.nc
|
||||||
*.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/
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# This directory is a Syncthing folder marker.
|
|
||||||
# Do not delete.
|
|
||||||
|
|
||||||
folderID: rs-data
|
|
||||||
created: 2026-04-03T11:35:19+07:00
|
|
||||||
Vendored
-5
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
|
||||||
"python-envs.defaultPackageManager": "ms-python.python:conda",
|
|
||||||
"python-envs.pythonProjects": []
|
|
||||||
}
|
|
||||||
+3
-947
@@ -1,947 +1,3 @@
|
|||||||
{
|
version https://git-lfs.github.com/spec/v1
|
||||||
"cells": [
|
oid sha256:dd3fecbef4c80ff1bd20922e6b2250d94ac05e55c3a2efbff4da20fcea3d739f
|
||||||
{
|
size 39812
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-3097
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-4465
File diff suppressed because one or more lines are too long
+3
-1406
File diff suppressed because one or more lines are too long
@@ -1,445 +0,0 @@
|
|||||||
# 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*
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ff33ab950e3524342a99e70a02598ccb1ca57e60627c887c10b34466eabf3e87
|
||||||
|
size 3536032
|
||||||
-252
@@ -1,252 +0,0 @@
|
|||||||
# 🎉 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!**
|
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
{
|
version https://git-lfs.github.com/spec/v1
|
||||||
"cells": [],
|
oid sha256:4a62b68a633d79c53a6fd8893e8ea42dcf2b9a8a3e907b1b9861661f04f21517
|
||||||
"metadata": {
|
size 72
|
||||||
"language_info": {
|
|
||||||
"name": "python"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nbformat": 4,
|
|
||||||
"nbformat_minor": 5
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:520c3480efaafdf844c3bba3dd165a8e44f3318ab21209539c78201f18fd6312
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7d3bff20682fc510a9c41b5aa2b42e3ebef8098ed81e27b4436f9ae86f47b9a6
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:322f48d2d54ee921facfc3c0c44b320a86a90694e1779b683824264f9affa3fc
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2948384b4c43483675d366b75b7bcdceb63fe5a8ad7d9b7a84ee4a1018cfc826
|
||||||
|
size 55333632
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1d5a53bba7a6db30b7f80bc4250d09edacfd244e97d7993382927a9bde85c6f7
|
||||||
|
size 55738194
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
# 🎉 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/
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+27
-676
@@ -16,9 +16,6 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Import report generator
|
|
||||||
from report_generator import generate_training_report, generate_prediction_report
|
|
||||||
|
|
||||||
app = FastAPI(title="Land Classification Training API", version="1.0.0")
|
app = FastAPI(title="Land Classification Training API", version="1.0.0")
|
||||||
|
|
||||||
# Enable CORS
|
# Enable CORS
|
||||||
@@ -30,7 +27,7 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Global training status``
|
# Global training status
|
||||||
training_status = {
|
training_status = {
|
||||||
"is_training": False,
|
"is_training": False,
|
||||||
"progress": "",
|
"progress": "",
|
||||||
@@ -52,10 +49,6 @@ prediction_status = {
|
|||||||
"end_time": None
|
"end_time": None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Batch prediction queue
|
|
||||||
batch_queue = []
|
|
||||||
batch_results = []
|
|
||||||
|
|
||||||
|
|
||||||
class TrainingConfig(BaseModel):
|
class TrainingConfig(BaseModel):
|
||||||
"""Cấu hình training"""
|
"""Cấu hình training"""
|
||||||
@@ -81,9 +74,6 @@ class TrainingConfig(BaseModel):
|
|||||||
learning_rate: float = 0.1
|
learning_rate: float = 0.1
|
||||||
use_gpu: bool = True
|
use_gpu: bool = True
|
||||||
|
|
||||||
# Train/test split
|
|
||||||
test_size: float = 0.2 # Tỷ lệ dữ liệu dùng làm test (0-1)
|
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
use_cache: bool = True # Cache dataset để test nhanh hơn
|
use_cache: bool = True # Cache dataset để test nhanh hơn
|
||||||
|
|
||||||
@@ -124,55 +114,23 @@ class TrainingStatus(BaseModel):
|
|||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def root():
|
async def root():
|
||||||
"""Serve main index page with tabs"""
|
"""Serve giao diện web"""
|
||||||
html_file = Path(__file__).parent / "index.html"
|
html_file = Path(__file__).parent / "training_interface.html"
|
||||||
if html_file.exists():
|
if html_file.exists():
|
||||||
return FileResponse(html_file)
|
return FileResponse(html_file)
|
||||||
else:
|
else:
|
||||||
return HTMLResponse("""
|
return HTMLResponse("""
|
||||||
<html>
|
<html>
|
||||||
<head><title>Land Classification System</title></head>
|
<head><title>Training Interface</title></head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Land Classification System</h1>
|
<h1>Land Classification Training API</h1>
|
||||||
<p>API Documentation: <a href="/docs">/docs</a></p>
|
<p>API Documentation: <a href="/docs">/docs</a></p>
|
||||||
<p>Training: <a href="/training">/training</a></p>
|
<p>Training Interface: Tạo file training_interface.html</p>
|
||||||
<p>Prediction: <a href="/prediction">/prediction</a></p>
|
|
||||||
<p>Dashboard: <a href="/dashboard">/dashboard</a></p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/training", response_class=HTMLResponse)
|
|
||||||
async def training_page():
|
|
||||||
"""Serve training interface"""
|
|
||||||
html_file = Path(__file__).parent / "training_interface.html"
|
|
||||||
if html_file.exists():
|
|
||||||
return FileResponse(html_file)
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=404, detail="Training interface không tồn tại")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/prediction", response_class=HTMLResponse)
|
|
||||||
async def prediction_page():
|
|
||||||
"""Serve prediction interface"""
|
|
||||||
html_file = Path(__file__).parent / "prediction_interface.html"
|
|
||||||
if html_file.exists():
|
|
||||||
return FileResponse(html_file)
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=404, detail="Prediction interface không tồn tại")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/dashboard", response_class=HTMLResponse)
|
|
||||||
async def dashboard():
|
|
||||||
"""Serve dashboard visualization"""
|
|
||||||
html_file = Path(__file__).parent / "dashboard.html"
|
|
||||||
if html_file.exists():
|
|
||||||
return FileResponse(html_file)
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=404, detail="Dashboard không tồn tại")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/config/presets")
|
@app.get("/api/config/presets")
|
||||||
async def get_presets():
|
async def get_presets():
|
||||||
"""Lấy các preset cấu hình sẵn"""
|
"""Lấy các preset cấu hình sẵn"""
|
||||||
@@ -183,8 +141,7 @@ async def get_presets():
|
|||||||
"config": {
|
"config": {
|
||||||
"min_lon": 105.6, "min_lat": 9.3, "max_lon": 106.2, "max_lat": 9.8,
|
"min_lon": 105.6, "min_lat": 9.3, "max_lon": 106.2, "max_lat": 9.8,
|
||||||
"start_date": "2023-03-01", "end_date": "2023-05-31",
|
"start_date": "2023-03-01", "end_date": "2023-05-31",
|
||||||
"max_scenes": 12, "cloud_cover": 30, "resolution": 20,
|
"max_scenes": 12, "cloud_cover": 30, "resolution": 20
|
||||||
"test_size": 0.2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -192,8 +149,7 @@ async def get_presets():
|
|||||||
"config": {
|
"config": {
|
||||||
"min_lon": 105.5, "min_lat": 9.2, "max_lon": 106.4, "max_lat": 10.0,
|
"min_lon": 105.5, "min_lat": 9.2, "max_lon": 106.4, "max_lat": 10.0,
|
||||||
"start_date": "2023-01-01", "end_date": "2023-06-30",
|
"start_date": "2023-01-01", "end_date": "2023-06-30",
|
||||||
"max_scenes": 30, "cloud_cover": 30, "resolution": 10,
|
"max_scenes": 30, "cloud_cover": 30, "resolution": 10
|
||||||
"test_size": 0.2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -201,8 +157,7 @@ async def get_presets():
|
|||||||
"config": {
|
"config": {
|
||||||
"min_lon": 105.5, "min_lat": 9.2, "max_lon": 106.4, "max_lat": 10.0,
|
"min_lon": 105.5, "min_lat": 9.2, "max_lon": 106.4, "max_lat": 10.0,
|
||||||
"start_date": "2022-09-01", "end_date": "2023-10-01",
|
"start_date": "2022-09-01", "end_date": "2023-10-01",
|
||||||
"max_scenes": 60, "cloud_cover": 50, "resolution": 10,
|
"max_scenes": 60, "cloud_cover": 50, "resolution": 10
|
||||||
"test_size": 0.2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -346,32 +301,17 @@ async def list_models():
|
|||||||
return {"models": []}
|
return {"models": []}
|
||||||
|
|
||||||
models = []
|
models = []
|
||||||
# List all .joblib model files (actual trained models)
|
|
||||||
for model_file in model_dir.glob("*.joblib"):
|
for model_file in model_dir.glob("*.joblib"):
|
||||||
# Skip any file that contains '_info' in its name
|
info_file = model_file.with_suffix('.json')
|
||||||
if '_info' in model_file.stem:
|
|
||||||
continue
|
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
# Try to find corresponding .json info file
|
|
||||||
# Remove .joblib and try with _info.json
|
|
||||||
base_name = model_file.stem # e.g., "model_cnn_20251221_163841"
|
|
||||||
info_file = model_dir / f"{base_name}_info.json"
|
|
||||||
|
|
||||||
if info_file.exists():
|
if info_file.exists():
|
||||||
try:
|
|
||||||
with open(info_file) as f:
|
with open(info_file) as f:
|
||||||
info = json.load(f)
|
info = json.load(f)
|
||||||
except Exception as e:
|
|
||||||
info = {"error": str(e)}
|
|
||||||
|
|
||||||
size_mb = round(model_file.stat().st_size / 1024 / 1024, 2)
|
|
||||||
created = datetime.fromtimestamp(model_file.stat().st_mtime).isoformat()
|
|
||||||
|
|
||||||
models.append({
|
models.append({
|
||||||
"filename": model_file.name,
|
"filename": model_file.name,
|
||||||
"created": created,
|
"created": datetime.fromtimestamp(model_file.stat().st_mtime).isoformat(),
|
||||||
"size_mb": size_mb,
|
"size_mb": round(model_file.stat().st_size / 1024 / 1024, 2),
|
||||||
"info": info
|
"info": info
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -380,100 +320,6 @@ async def list_models():
|
|||||||
return {"models": models}
|
return {"models": models}
|
||||||
|
|
||||||
|
|
||||||
# ============ REPORTS API ============
|
|
||||||
|
|
||||||
@app.get("/api/reports/list")
|
|
||||||
async def list_reports():
|
|
||||||
"""Liệt kê các báo cáo đã tạo"""
|
|
||||||
reports_dir = Path("reports")
|
|
||||||
reports_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
reports = []
|
|
||||||
for report_file in reports_dir.glob("*.html"):
|
|
||||||
# Determine report type from filename
|
|
||||||
if "training" in report_file.name:
|
|
||||||
report_type = "training"
|
|
||||||
elif "prediction" in report_file.name:
|
|
||||||
report_type = "prediction"
|
|
||||||
else:
|
|
||||||
report_type = "unknown"
|
|
||||||
|
|
||||||
reports.append({
|
|
||||||
"filename": report_file.name,
|
|
||||||
"type": report_type,
|
|
||||||
"created": datetime.fromtimestamp(report_file.stat().st_mtime).isoformat(),
|
|
||||||
"size_kb": round(report_file.stat().st_size / 1024, 2),
|
|
||||||
"view_url": f"/api/reports/view/{report_file.name}",
|
|
||||||
"download_url": f"/api/reports/download/{report_file.name}"
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sort by creation time (newest first)
|
|
||||||
reports.sort(key=lambda x: x["created"], reverse=True)
|
|
||||||
return {"reports": reports, "count": len(reports)}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/reports/view/{filename}", response_class=HTMLResponse)
|
|
||||||
async def view_report(filename: str):
|
|
||||||
"""Xem báo cáo HTML trực tiếp"""
|
|
||||||
reports_dir = Path("reports")
|
|
||||||
file_path = reports_dir / filename
|
|
||||||
|
|
||||||
# Security check
|
|
||||||
if ".." in filename or "/" in filename or "\\" in filename:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=404, detail=f"Report không tồn tại: {filename}")
|
|
||||||
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
html_content = f.read()
|
|
||||||
|
|
||||||
return HTMLResponse(content=html_content)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/reports/download/{filename}")
|
|
||||||
async def download_report(filename: str):
|
|
||||||
"""Download báo cáo HTML"""
|
|
||||||
reports_dir = Path("reports")
|
|
||||||
file_path = reports_dir / filename
|
|
||||||
|
|
||||||
# Security check
|
|
||||||
if ".." in filename or "/" in filename or "\\" in filename:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=404, detail=f"Report không tồn tại: {filename}")
|
|
||||||
|
|
||||||
return FileResponse(
|
|
||||||
path=str(file_path),
|
|
||||||
filename=filename,
|
|
||||||
media_type="text/html",
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": f"attachment; filename={filename}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/reports/delete/{filename}")
|
|
||||||
async def delete_report(filename: str):
|
|
||||||
"""Xóa một báo cáo"""
|
|
||||||
reports_dir = Path("reports")
|
|
||||||
file_path = reports_dir / filename
|
|
||||||
|
|
||||||
# Security check
|
|
||||||
if ".." in filename or "/" in filename or "\\" in filename:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=404, detail=f"Report không tồn tại: {filename}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
file_path.unlink()
|
|
||||||
return {"message": f"Đã xóa báo cáo: {filename}", "success": True}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Không thể xóa: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/prediction/start")
|
@app.post("/api/prediction/start")
|
||||||
async def start_prediction(config: PredictionConfig, background_tasks: BackgroundTasks):
|
async def start_prediction(config: PredictionConfig, background_tasks: BackgroundTasks):
|
||||||
"""Bắt đầu dự đoán"""
|
"""Bắt đầu dự đoán"""
|
||||||
@@ -536,7 +382,6 @@ async def run_training(config: TrainingConfig):
|
|||||||
learning_rate=config.learning_rate,
|
learning_rate=config.learning_rate,
|
||||||
use_gpu=config.use_gpu,
|
use_gpu=config.use_gpu,
|
||||||
use_cache=config.use_cache,
|
use_cache=config.use_cache,
|
||||||
test_size=config.test_size,
|
|
||||||
status_callback=lambda msg: update_progress(msg),
|
status_callback=lambda msg: update_progress(msg),
|
||||||
cancel_check=should_cancel
|
cancel_check=should_cancel
|
||||||
)
|
)
|
||||||
@@ -547,21 +392,8 @@ async def run_training(config: TrainingConfig):
|
|||||||
training_status["error"] = "Training cancelled by user"
|
training_status["error"] = "Training cancelled by user"
|
||||||
else:
|
else:
|
||||||
training_status["is_training"] = False
|
training_status["is_training"] = False
|
||||||
training_status["progress"] = "Hoàn thành! Đang tạo báo cáo..."
|
training_status["progress"] = "Hoàn thành!"
|
||||||
training_status["result"] = result
|
training_status["result"] = result
|
||||||
|
|
||||||
# Auto generate report
|
|
||||||
if result.get("success", False):
|
|
||||||
try:
|
|
||||||
report_path, _ = generate_training_report(result)
|
|
||||||
training_status["result"]["report_path"] = report_path
|
|
||||||
training_status["result"]["report_filename"] = Path(report_path).name
|
|
||||||
training_status["progress"] = "Hoàn thành! Báo cáo đã được tạo."
|
|
||||||
print(f"[REPORT] Generated: {report_path}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[REPORT ERROR] Failed to generate report: {e}")
|
|
||||||
training_status["progress"] = "Hoàn thành! (Không thể tạo báo cáo)"
|
|
||||||
|
|
||||||
training_status["end_time"] = datetime.now().isoformat()
|
training_status["end_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -601,12 +433,6 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
import rioxarray
|
import rioxarray
|
||||||
import dask.array as da
|
import dask.array as da
|
||||||
|
|
||||||
# Validate bbox
|
|
||||||
if (config.min_lon < -180 or config.max_lon > 180 or
|
|
||||||
config.min_lat < -90 or config.max_lat > 90):
|
|
||||||
raise ValueError(f"Bbox không hợp lệ: ({config.min_lon}, {config.min_lat}, {config.max_lon}, {config.max_lat}). "
|
|
||||||
f"Phải trong phạm vi (-180, -90, 180, 90)")
|
|
||||||
|
|
||||||
prediction_status["progress"] = "Đang load model..."
|
prediction_status["progress"] = "Đang load model..."
|
||||||
|
|
||||||
# Load model
|
# Load model
|
||||||
@@ -634,49 +460,40 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
|
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
|
||||||
|
|
||||||
prediction_status["progress"] = "Đang kiểm tra cache dữ liệu đầu vào..."
|
|
||||||
import hashlib, os
|
|
||||||
cache_dir = Path("dataset_cache")
|
|
||||||
cache_dir.mkdir(exist_ok=True)
|
|
||||||
# Tạo cache key từ bbox, time_range, max_scenes, cloud_cover, resolution
|
|
||||||
cache_key = f"pred_{config.min_lon}_{config.min_lat}_{config.max_lon}_{config.max_lat}_{config.start_date}_{config.end_date}_{config.max_scenes}_{config.cloud_cover}_{config.resolution}"
|
|
||||||
cache_hash = hashlib.md5(cache_key.encode()).hexdigest()
|
|
||||||
cache_file = cache_dir / f"prediction_input_{cache_hash}.joblib"
|
|
||||||
|
|
||||||
# Initialize common variables
|
|
||||||
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
|
||||||
time_range = f"{config.start_date}/{config.end_date}"
|
|
||||||
|
|
||||||
if cache_file.exists():
|
|
||||||
prediction_status["progress"] = "Đang load dữ liệu từ cache..."
|
|
||||||
cached = joblib.load(cache_file)
|
|
||||||
s2_data = cached["s2_data"]
|
|
||||||
s2_items = cached["s2_items"]
|
|
||||||
vh_monthly = cached.get("vh_monthly")
|
|
||||||
vv_monthly = cached.get("vv_monthly")
|
|
||||||
use_radar = cached.get("use_radar", False)
|
|
||||||
else:
|
|
||||||
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||||
|
|
||||||
|
# Import and use Microsoft Planetary Computer STAC API
|
||||||
import pystac_client
|
import pystac_client
|
||||||
import planetary_computer
|
import planetary_computer
|
||||||
from odc.stac import load
|
from odc.stac import load
|
||||||
|
|
||||||
catalog = pystac_client.Client.open(
|
catalog = pystac_client.Client.open(
|
||||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||||
modifier=planetary_computer.sign_inplace,
|
modifier=planetary_computer.sign_inplace,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
||||||
|
time_range = f"{config.start_date}/{config.end_date}"
|
||||||
|
|
||||||
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
|
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
||||||
|
|
||||||
|
# Search Sentinel-2 data
|
||||||
s2_search = catalog.search(
|
s2_search = catalog.search(
|
||||||
collections=["sentinel-2-l2a"],
|
collections=["sentinel-2-l2a"],
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
datetime=time_range,
|
datetime=time_range,
|
||||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||||
)
|
)
|
||||||
|
|
||||||
s2_items = list(s2_search.items())
|
s2_items = list(s2_search.items())
|
||||||
if not s2_items:
|
if not s2_items:
|
||||||
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
||||||
|
|
||||||
s2_items = s2_items[:config.max_scenes]
|
s2_items = s2_items[:config.max_scenes]
|
||||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
||||||
|
|
||||||
|
# Load Sentinel-2 data
|
||||||
s2_data = load(
|
s2_data = load(
|
||||||
s2_items,
|
s2_items,
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
@@ -684,46 +501,6 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
groupby="solar_day",
|
groupby="solar_day",
|
||||||
resolution=config.resolution
|
resolution=config.resolution
|
||||||
)
|
)
|
||||||
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (Radar)... ============
|
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
|
||||||
s1_search = catalog.search(
|
|
||||||
collections=["sentinel-1-rtc"],
|
|
||||||
bbox=bbox,
|
|
||||||
datetime=time_range,
|
|
||||||
)
|
|
||||||
s1_items = list(s1_search.items())
|
|
||||||
if s1_items:
|
|
||||||
s1_items = s1_items[:config.max_scenes]
|
|
||||||
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
|
||||||
s1_data = load(
|
|
||||||
s1_items,
|
|
||||||
bbox=bbox,
|
|
||||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
|
||||||
groupby="sat:absolute_orbit",
|
|
||||||
resolution=config.resolution
|
|
||||||
)
|
|
||||||
if "vh" in s1_data and "vv" in s1_data:
|
|
||||||
vh = s1_data["vh"].astype('float32')
|
|
||||||
vv = s1_data["vv"].astype('float32')
|
|
||||||
vh_monthly = vh.resample(time="1ME").mean().compute()
|
|
||||||
vv_monthly = vv.resample(time="1ME").mean().compute()
|
|
||||||
use_radar = True
|
|
||||||
else:
|
|
||||||
vh_monthly = None
|
|
||||||
vv_monthly = None
|
|
||||||
use_radar = False
|
|
||||||
else:
|
|
||||||
vh_monthly = None
|
|
||||||
vv_monthly = None
|
|
||||||
use_radar = False
|
|
||||||
# Lưu cache
|
|
||||||
joblib.dump({
|
|
||||||
"s2_data": s2_data,
|
|
||||||
"s2_items": s2_items,
|
|
||||||
"vh_monthly": vh_monthly,
|
|
||||||
"vv_monthly": vv_monthly,
|
|
||||||
"use_radar": use_radar
|
|
||||||
}, cache_file)
|
|
||||||
|
|
||||||
# ============ BƯỚC 2: TÍNH NDVI VÀ XỬ LÝ MÂY ============
|
# ============ BƯỚC 2: TÍNH NDVI VÀ XỬ LÝ MÂY ============
|
||||||
prediction_status["progress"] = "Đang tính toán NDVI và xử lý mây..."
|
prediction_status["progress"] = "Đang tính toán NDVI và xử lý mây..."
|
||||||
@@ -755,24 +532,8 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
ndvi_monthly = ndvi_monthly.compute()
|
ndvi_monthly = ndvi_monthly.compute()
|
||||||
|
|
||||||
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (VH, VV) ============
|
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (VH, VV) ============
|
||||||
# Only load radar if not already in cache
|
|
||||||
if not cache_file.exists() or (cache_file.exists() and not use_radar):
|
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||||
|
|
||||||
# Initialize catalog if not already done
|
|
||||||
if not cache_file.exists():
|
|
||||||
# catalog already initialized in the else block above
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Need to initialize catalog for radar search
|
|
||||||
import pystac_client
|
|
||||||
import planetary_computer
|
|
||||||
from odc.stac import load
|
|
||||||
catalog = pystac_client.Client.open(
|
|
||||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
|
||||||
modifier=planetary_computer.sign_inplace,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Search Sentinel-1 data
|
# Search Sentinel-1 data
|
||||||
s1_search = catalog.search(
|
s1_search = catalog.search(
|
||||||
collections=["sentinel-1-rtc"],
|
collections=["sentinel-1-rtc"],
|
||||||
@@ -941,50 +702,15 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
|
|
||||||
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
|
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
|
||||||
|
|
||||||
# Generate PNG preview for web display
|
|
||||||
prediction_status["progress"] = "Đang tạo PNG preview..."
|
|
||||||
png_file = output_dir / f"prediction_{timestamp}.png"
|
|
||||||
try:
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use('Agg') # Non-interactive backend
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# Create a figure with prediction result
|
|
||||||
fig, ax = plt.subplots(figsize=(12, 10), dpi=150)
|
|
||||||
|
|
||||||
# Plot prediction with colormap
|
|
||||||
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
|
||||||
ax.set_title(f'Prediction Result - {timestamp}', fontsize=14, fontweight='bold')
|
|
||||||
ax.set_xlabel('X (pixels)', fontsize=10)
|
|
||||||
ax.set_ylabel('Y (pixels)', fontsize=10)
|
|
||||||
|
|
||||||
# Add colorbar
|
|
||||||
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
||||||
cbar.set_label('Class', rotation=270, labelpad=15)
|
|
||||||
|
|
||||||
# Add grid
|
|
||||||
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
|
||||||
|
|
||||||
# Save PNG
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig(str(png_file), dpi=150, bbox_inches='tight')
|
|
||||||
plt.close(fig)
|
|
||||||
|
|
||||||
print(f"[PNG PREVIEW] Created: {png_file}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[PNG PREVIEW ERROR] Failed to create PNG: {e}")
|
|
||||||
png_file = None
|
|
||||||
|
|
||||||
# Get unique classes for result
|
# Get unique classes for result
|
||||||
unique_classes = np.unique(predictions_2d)
|
unique_classes = np.unique(predictions_2d)
|
||||||
unique_classes = unique_classes[~np.isnan(unique_classes)].tolist()
|
unique_classes = unique_classes[~np.isnan(unique_classes)].tolist()
|
||||||
|
|
||||||
prediction_status["is_predicting"] = False
|
prediction_status["is_predicting"] = False
|
||||||
prediction_status["progress"] = "Hoàn thành! Đang tạo báo cáo..."
|
prediction_status["progress"] = "Hoàn thành!"
|
||||||
prediction_status["output_file"] = str(output_file)
|
prediction_status["output_file"] = str(output_file)
|
||||||
prediction_status["result"] = {
|
prediction_status["result"] = {
|
||||||
"output_file": str(output_file),
|
"output_file": str(output_file),
|
||||||
"png_file": str(png_file) if png_file else None,
|
|
||||||
"shape": list(pred_shape),
|
"shape": list(pred_shape),
|
||||||
"unique_classes": unique_classes,
|
"unique_classes": unique_classes,
|
||||||
"bbox": bbox,
|
"bbox": bbox,
|
||||||
@@ -994,18 +720,6 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
"used_radar": use_radar,
|
"used_radar": use_radar,
|
||||||
"model_used": config.model_filename
|
"model_used": config.model_filename
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto generate prediction report
|
|
||||||
try:
|
|
||||||
report_path, _ = generate_prediction_report(prediction_status["result"])
|
|
||||||
prediction_status["result"]["report_path"] = report_path
|
|
||||||
prediction_status["result"]["report_filename"] = Path(report_path).name
|
|
||||||
prediction_status["progress"] = "Hoàn thành! Báo cáo đã được tạo."
|
|
||||||
print(f"[PREDICTION REPORT] Generated: {report_path}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[PREDICTION REPORT ERROR] Failed to generate report: {e}")
|
|
||||||
prediction_status["progress"] = "Hoàn thành! (Không thể tạo báo cáo)"
|
|
||||||
|
|
||||||
prediction_status["end_time"] = dt.now().isoformat()
|
prediction_status["end_time"] = dt.now().isoformat()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1060,369 +774,6 @@ async def download_prediction(filename: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/predictions/preview/{filename}")
|
|
||||||
async def preview_prediction_png(filename: str):
|
|
||||||
"""Preview PNG image of prediction"""
|
|
||||||
predictions_dir = Path("predictions")
|
|
||||||
file_path = predictions_dir / filename
|
|
||||||
|
|
||||||
# Security check
|
|
||||||
if ".." in filename or "/" in filename or "\\" in filename:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=404, detail=f"PNG preview không tồn tại: {filename}")
|
|
||||||
|
|
||||||
return FileResponse(
|
|
||||||
path=str(file_path),
|
|
||||||
media_type="image/png"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/predictions/preview/{filename}")
|
|
||||||
async def preview_prediction_png(filename: str):
|
|
||||||
"""Preview PNG image of prediction"""
|
|
||||||
predictions_dir = Path("predictions")
|
|
||||||
file_path = predictions_dir / filename
|
|
||||||
|
|
||||||
# Security check
|
|
||||||
if ".." in filename or "/" in filename or "\\" in filename:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=404, detail=f"PNG preview không tồn tại: {filename}")
|
|
||||||
|
|
||||||
return FileResponse(
|
|
||||||
path=str(file_path),
|
|
||||||
media_type="image/png"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============ DASHBOARD & VISUALIZATION API ============
|
|
||||||
|
|
||||||
@app.get("/api/dashboard/accuracy-trends")
|
|
||||||
async def get_accuracy_trends():
|
|
||||||
"""Lấy dữ liệu accuracy trends của các models theo thời gian"""
|
|
||||||
model_dir = Path("model_train")
|
|
||||||
if not model_dir.exists():
|
|
||||||
return {"trends": [], "models": []}
|
|
||||||
|
|
||||||
trends_data = []
|
|
||||||
for info_file in sorted(model_dir.glob("*.json")):
|
|
||||||
try:
|
|
||||||
with open(info_file) as f:
|
|
||||||
info = json.load(f)
|
|
||||||
|
|
||||||
# Extract relevant data
|
|
||||||
if "training_date" in info and "metrics" in info:
|
|
||||||
trends_data.append({
|
|
||||||
"date": info["training_date"],
|
|
||||||
"model_name": info.get("model_type", "unknown"),
|
|
||||||
"accuracy": info["metrics"].get("accuracy", 0),
|
|
||||||
"f1_score": info["metrics"].get("macro avg", {}).get("f1-score", 0),
|
|
||||||
"precision": info["metrics"].get("macro avg", {}).get("precision", 0),
|
|
||||||
"recall": info["metrics"].get("macro avg", {}).get("recall", 0),
|
|
||||||
"filename": info_file.stem + ".joblib"
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading {info_file}: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Sort by date
|
|
||||||
trends_data.sort(key=lambda x: x["date"])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"trends": trends_data,
|
|
||||||
"models": list(set(d["model_name"] for d in trends_data))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/dashboard/statistics")
|
|
||||||
async def get_statistics():
|
|
||||||
"""Lấy thống kê tổng quan: số models, predictions, reports"""
|
|
||||||
model_dir = Path("model_train")
|
|
||||||
predictions_dir = Path("predictions")
|
|
||||||
reports_dir = Path("reports")
|
|
||||||
|
|
||||||
# Count items
|
|
||||||
n_models = len(list(model_dir.glob("*.joblib"))) if model_dir.exists() else 0
|
|
||||||
n_predictions = len(list(predictions_dir.glob("*.tif"))) if predictions_dir.exists() else 0
|
|
||||||
n_reports = len(list(reports_dir.glob("*.html"))) if reports_dir.exists() else 0
|
|
||||||
|
|
||||||
# Get latest model info
|
|
||||||
latest_model = None
|
|
||||||
if model_dir.exists():
|
|
||||||
model_files = sorted(model_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)
|
|
||||||
if model_files:
|
|
||||||
try:
|
|
||||||
with open(model_files[0]) as f:
|
|
||||||
latest_model = json.load(f)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Get latest prediction
|
|
||||||
latest_prediction = None
|
|
||||||
if predictions_dir.exists():
|
|
||||||
pred_files = sorted(predictions_dir.glob("*.tif"), key=lambda x: x.stat().st_mtime, reverse=True)
|
|
||||||
if pred_files:
|
|
||||||
latest_prediction = {
|
|
||||||
"filename": pred_files[0].name,
|
|
||||||
"created": datetime.fromtimestamp(pred_files[0].stat().st_mtime).isoformat(),
|
|
||||||
"size_mb": round(pred_files[0].stat().st_size / 1024 / 1024, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"models": {
|
|
||||||
"total": n_models,
|
|
||||||
"latest": latest_model
|
|
||||||
},
|
|
||||||
"predictions": {
|
|
||||||
"total": n_predictions,
|
|
||||||
"latest": latest_prediction
|
|
||||||
},
|
|
||||||
"reports": {
|
|
||||||
"total": n_reports
|
|
||||||
},
|
|
||||||
"training_status": training_status,
|
|
||||||
"prediction_status": prediction_status
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/dashboard/class-distribution/{model_filename}")
|
|
||||||
async def get_class_distribution(model_filename: str):
|
|
||||||
"""Lấy phân bố các lớp từ model info"""
|
|
||||||
# Convert model filename to info filename
|
|
||||||
# e.g., model_cnn_20251221_163841.joblib -> model_cnn_20251221_163841_info.json
|
|
||||||
base_name = model_filename.replace(".joblib", "")
|
|
||||||
info_file = Path("model_train") / f"{base_name}_info.json"
|
|
||||||
|
|
||||||
if not info_file.exists():
|
|
||||||
raise HTTPException(status_code=404, detail="Model info không tồn tại")
|
|
||||||
|
|
||||||
with open(info_file) as f:
|
|
||||||
info = json.load(f)
|
|
||||||
|
|
||||||
# Extract class distribution from classification report
|
|
||||||
class_dist = {}
|
|
||||||
if "classification_report" in info:
|
|
||||||
for class_name, metrics in info["classification_report"].items():
|
|
||||||
if isinstance(metrics, dict) and "support" in metrics:
|
|
||||||
class_dist[class_name] = int(metrics["support"])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"model": model_filename,
|
|
||||||
"class_distribution": class_dist,
|
|
||||||
"total_samples": sum(class_dist.values()) if class_dist else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ============ BATCH PROCESSING API ============
|
|
||||||
|
|
||||||
class BatchPredictionItem(BaseModel):
|
|
||||||
"""Một item trong batch prediction"""
|
|
||||||
name: str
|
|
||||||
min_lon: float
|
|
||||||
min_lat: float
|
|
||||||
max_lon: float
|
|
||||||
max_lat: float
|
|
||||||
start_date: str = "2023-03-01"
|
|
||||||
end_date: str = "2023-05-31"
|
|
||||||
max_scenes: int = 12
|
|
||||||
cloud_cover: int = 30
|
|
||||||
resolution: int = 20
|
|
||||||
|
|
||||||
|
|
||||||
class BatchPredictionConfig(BaseModel):
|
|
||||||
"""Cấu hình cho batch prediction"""
|
|
||||||
model_filename: str
|
|
||||||
items: List[BatchPredictionItem]
|
|
||||||
auto_retry: bool = True
|
|
||||||
max_retries: int = 3
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/batch/start")
|
|
||||||
async def start_batch_prediction(config: BatchPredictionConfig, background_tasks: BackgroundTasks):
|
|
||||||
"""Bắt đầu batch prediction"""
|
|
||||||
global batch_queue, batch_results
|
|
||||||
|
|
||||||
# Create batch jobs
|
|
||||||
batch_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
|
|
||||||
for idx, item in enumerate(config.items):
|
|
||||||
job = {
|
|
||||||
"batch_id": batch_id,
|
|
||||||
"job_id": f"{batch_id}_{idx}",
|
|
||||||
"name": item.name,
|
|
||||||
"status": "queued",
|
|
||||||
"progress": 0,
|
|
||||||
"error": None,
|
|
||||||
"result": None,
|
|
||||||
"retries": 0,
|
|
||||||
"max_retries": config.max_retries if config.auto_retry else 0,
|
|
||||||
"config": {
|
|
||||||
"model_filename": config.model_filename,
|
|
||||||
"min_lon": item.min_lon,
|
|
||||||
"min_lat": item.min_lat,
|
|
||||||
"max_lon": item.max_lon,
|
|
||||||
"max_lat": item.max_lat,
|
|
||||||
"start_date": item.start_date,
|
|
||||||
"end_date": item.end_date,
|
|
||||||
"max_scenes": item.max_scenes,
|
|
||||||
"cloud_cover": item.cloud_cover,
|
|
||||||
"resolution": item.resolution
|
|
||||||
},
|
|
||||||
"created_at": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
batch_queue.append(job)
|
|
||||||
|
|
||||||
# Start processing in background
|
|
||||||
background_tasks.add_task(process_batch_queue)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"message": f"Đã tạo {len(config.items)} batch jobs",
|
|
||||||
"batch_id": batch_id,
|
|
||||||
"total_jobs": len(config.items)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/batch/status")
|
|
||||||
async def get_batch_status():
|
|
||||||
"""Lấy trạng thái của batch queue"""
|
|
||||||
global batch_queue, batch_results
|
|
||||||
|
|
||||||
queued = [j for j in batch_queue if j["status"] == "queued"]
|
|
||||||
running = [j for j in batch_queue if j["status"] == "running"]
|
|
||||||
completed = [j for j in batch_results if j["status"] == "completed"]
|
|
||||||
failed = [j for j in batch_results if j["status"] == "failed"]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"queue": {
|
|
||||||
"queued": len(queued),
|
|
||||||
"running": len(running),
|
|
||||||
"completed": len(completed),
|
|
||||||
"failed": len(failed),
|
|
||||||
"total": len(batch_queue) + len(batch_results)
|
|
||||||
},
|
|
||||||
"jobs": {
|
|
||||||
"queued": queued[:5], # Show first 5
|
|
||||||
"running": running,
|
|
||||||
"recent_completed": completed[:10], # Show last 10
|
|
||||||
"recent_failed": failed[:10]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/batch/results/{batch_id}")
|
|
||||||
async def get_batch_results(batch_id: str):
|
|
||||||
"""Lấy kết quả của một batch"""
|
|
||||||
global batch_results
|
|
||||||
|
|
||||||
results = [j for j in batch_results if j["batch_id"] == batch_id]
|
|
||||||
|
|
||||||
if not results:
|
|
||||||
# Check if still in queue
|
|
||||||
queued = [j for j in batch_queue if j["batch_id"] == batch_id]
|
|
||||||
if queued:
|
|
||||||
return {
|
|
||||||
"batch_id": batch_id,
|
|
||||||
"status": "processing",
|
|
||||||
"jobs": queued
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=404, detail="Batch không tồn tại")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"batch_id": batch_id,
|
|
||||||
"status": "completed",
|
|
||||||
"jobs": results,
|
|
||||||
"summary": {
|
|
||||||
"total": len(results),
|
|
||||||
"successful": len([j for j in results if j["status"] == "completed"]),
|
|
||||||
"failed": len([j for j in results if j["status"] == "failed"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/batch/cancel/{batch_id}")
|
|
||||||
async def cancel_batch(batch_id: str):
|
|
||||||
"""Hủy một batch đang chạy"""
|
|
||||||
global batch_queue
|
|
||||||
|
|
||||||
# Remove from queue
|
|
||||||
removed = 0
|
|
||||||
batch_queue_copy = batch_queue.copy()
|
|
||||||
for job in batch_queue_copy:
|
|
||||||
if job["batch_id"] == batch_id and job["status"] == "queued":
|
|
||||||
batch_queue.remove(job)
|
|
||||||
removed += 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
"message": f"Đã hủy {removed} jobs",
|
|
||||||
"batch_id": batch_id
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def process_batch_queue():
|
|
||||||
"""Process batch prediction queue"""
|
|
||||||
global batch_queue, batch_results
|
|
||||||
|
|
||||||
while batch_queue:
|
|
||||||
# Get next job
|
|
||||||
job = None
|
|
||||||
for j in batch_queue:
|
|
||||||
if j["status"] == "queued":
|
|
||||||
job = j
|
|
||||||
break
|
|
||||||
|
|
||||||
if not job:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Mark as running
|
|
||||||
job["status"] = "running"
|
|
||||||
job["started_at"] = datetime.now().isoformat()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create PredictionConfig from job config
|
|
||||||
pred_config = PredictionConfig(**job["config"])
|
|
||||||
|
|
||||||
# Run prediction (simplified version)
|
|
||||||
# In real implementation, call the actual prediction function
|
|
||||||
print(f"[BATCH] Processing job: {job['name']}")
|
|
||||||
|
|
||||||
# Simulate prediction (replace with actual prediction call)
|
|
||||||
# await run_prediction(pred_config)
|
|
||||||
|
|
||||||
# For now, mark as completed
|
|
||||||
job["status"] = "completed"
|
|
||||||
job["completed_at"] = datetime.now().isoformat()
|
|
||||||
job["result"] = {
|
|
||||||
"output_file": f"predictions/batch_{job['job_id']}.tif",
|
|
||||||
"message": "Prediction completed successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
job["error"] = str(e)
|
|
||||||
|
|
||||||
# Retry logic
|
|
||||||
if job["retries"] < job["max_retries"]:
|
|
||||||
job["retries"] += 1
|
|
||||||
job["status"] = "queued" # Retry
|
|
||||||
print(f"[BATCH] Job {job['name']} failed, retrying ({job['retries']}/{job['max_retries']})")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
job["status"] = "failed"
|
|
||||||
job["completed_at"] = datetime.now().isoformat()
|
|
||||||
print(f"[BATCH] Job {job['name']} failed permanently: {e}")
|
|
||||||
|
|
||||||
# Move to results
|
|
||||||
batch_queue.remove(job)
|
|
||||||
batch_results.append(job)
|
|
||||||
|
|
||||||
# Keep only last 100 results
|
|
||||||
if len(batch_results) > 100:
|
|
||||||
batch_results = batch_results[-100:]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:37686f6bbdd4590ce2dc5474eef8268f680498c2b78881a486625e264d2a1015
|
||||||
|
size 841901
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:78d28d677d39cc67e607bb418948b9e3e921351762d63b4737036fc02d1d361a
|
||||||
|
size 3375913
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:892cabc8da5cfcc0f9295ddb3ad32fda077027efa53e7b48e2f81965ad6ca202
|
||||||
|
size 3361173
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fbab8f823ca6965dcbe43d9ce914f8912e3976833de1b205032eec48bfa83970
|
||||||
|
size 1670930
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7c33115ec7d6896758abf1eb8d07eba94ae15444c63f318e8adbd5d032df9a4f
|
||||||
|
size 1671094
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e71c696efbcb9e766eb1a666cc85b4f604992a65efc7258a46a46c7f81c596db
|
||||||
|
size 3536032
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:520c3480efaafdf844c3bba3dd165a8e44f3318ab21209539c78201f18fd6312
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7d3bff20682fc510a9c41b5aa2b42e3ebef8098ed81e27b4436f9ae86f47b9a6
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:322f48d2d54ee921facfc3c0c44b320a86a90694e1779b683824264f9affa3fc
|
||||||
|
size 3066028
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a446d333bf7f6d0cb7df014f12b0da3f7298f85bdfb4de06893173e90fbd5ccb
|
||||||
|
size 14112695
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3eec576dffe1cc1393fdb584fb99d28db62abdd0977d41030aee5c4fa5377180
|
||||||
|
size 11474743
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cc125de4c172c98d9e50123bfcc2f8dd856e4ccd6bb04edd639badf090af2eef
|
||||||
|
size 5077735
|
||||||
+3
-6236
File diff suppressed because one or more lines are too long
+3
-5851
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
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"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
@@ -1,935 +0,0 @@
|
|||||||
<!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,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7318566231680cb0c97883b7a5e4177aa1bfeec462a0f52ed34aa103ddec210b
|
||||||
|
size 20903
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9cabf5e0241dcc3a73133ac8ae11171f34042c491f895b268c016407619bdfe1
|
||||||
|
size 20903
|
||||||
-991
@@ -1,991 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a446d333bf7f6d0cb7df014f12b0da3f7298f85bdfb4de06893173e90fbd5ccb
|
||||||
|
size 14112695
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae2f82f6c837396729cc63efa41ee3048d9a7de3197e28318dc846be830239b9
|
||||||
|
size 41536
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T18:04:27.406540",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 0.515686274509804,
|
||||||
|
"test_accuracy": 0.5,
|
||||||
|
"model_type": "cnn",
|
||||||
|
"device": "cpu",
|
||||||
|
"n_estimators": 50,
|
||||||
|
"max_depth": null,
|
||||||
|
"learning_rate": null,
|
||||||
|
"cnn_epochs": 25,
|
||||||
|
"n_features": 3,
|
||||||
|
"n_classes": 7,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:abc03d62c7f620b88150a6481143026d516fd3a34bd5ab67c734adac4a8900f9
|
||||||
|
size 41536
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T18:17:48.399298",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 0.4803921568627451,
|
||||||
|
"test_accuracy": 0.484375,
|
||||||
|
"model_type": "cnn",
|
||||||
|
"device": "cpu",
|
||||||
|
"n_estimators": 50,
|
||||||
|
"max_depth": null,
|
||||||
|
"learning_rate": null,
|
||||||
|
"cnn_epochs": 25,
|
||||||
|
"n_features": 3,
|
||||||
|
"n_classes": 7,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-25",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:21f906aa2a61d2e793a3463df95fe3e364134934af326771efae90d80caca419
|
||||||
|
size 41536
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T18:23:10.713912",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 0.46862745098039216,
|
||||||
|
"test_accuracy": 0.4609375,
|
||||||
|
"model_type": "cnn",
|
||||||
|
"device": "cpu",
|
||||||
|
"n_estimators": 50,
|
||||||
|
"max_depth": null,
|
||||||
|
"learning_rate": null,
|
||||||
|
"cnn_epochs": 25,
|
||||||
|
"n_features": 3,
|
||||||
|
"n_classes": 7,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3eec576dffe1cc1393fdb584fb99d28db62abdd0977d41030aee5c4fa5377180
|
||||||
|
size 11474743
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0fe99f96ad3d3ba7aaacc0e742572a8f5b22947a328c74b245e0aa5f2913c757
|
||||||
|
size 1347520
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-12T12:57:54.509336",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 1.0,
|
||||||
|
"test_accuracy": 0.578125,
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": "cuda:0",
|
||||||
|
"gpu_device": "RTX 4060",
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": 100,
|
||||||
|
"max_depth": 20,
|
||||||
|
"learning_rate": 0.1
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b717f564f9413a6e5c9cd3f7011cbc18be02479691d01c554986defb400f0490
|
||||||
|
size 1347520
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-12T22:15:25.794614",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 1.0,
|
||||||
|
"test_accuracy": 0.578125,
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": "cuda:0",
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": 100,
|
||||||
|
"max_depth": 20,
|
||||||
|
"learning_rate": 0.1,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b9c6cabb59d9cdbba22a438ae935911f1711d7434dcf3728db4e518ec1b90190
|
||||||
|
size 556184
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-12T22:33:42.950629",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 904,
|
||||||
|
"testing_samples": 226,
|
||||||
|
"train_accuracy": 0.19911504424778761,
|
||||||
|
"test_accuracy": 0.19911504424778761,
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": "cuda:0",
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": 100,
|
||||||
|
"max_depth": 20,
|
||||||
|
"learning_rate": 0.1,
|
||||||
|
"bbox": [
|
||||||
|
104.89032,
|
||||||
|
10.944563,
|
||||||
|
104.972717,
|
||||||
|
11.016689
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b717f564f9413a6e5c9cd3f7011cbc18be02479691d01c554986defb400f0490
|
||||||
|
size 1347520
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T13:40:03.325930",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 1.0,
|
||||||
|
"test_accuracy": 0.578125,
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": "cuda:0",
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": 100,
|
||||||
|
"max_depth": 20,
|
||||||
|
"learning_rate": 0.1,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a44efc173619782c8add224e9024f33ef0150ab192b5435d009cb119c379f03c
|
||||||
|
size 2088632
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T16:52:19.862770",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 859,
|
||||||
|
"testing_samples": 215,
|
||||||
|
"train_accuracy": 0.9976717112922002,
|
||||||
|
"test_accuracy": 0.6837209302325581,
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": "cuda:0",
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": 100,
|
||||||
|
"max_depth": 20,
|
||||||
|
"learning_rate": 0.1,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-12-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
+3
-3728
File diff suppressed because one or more lines are too long
@@ -1,803 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d8fb13c2e466b9811104cbd7747ea6ca8c14ddb44802d934aadfe52a4b5dd916
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a2fe092e1fd96e56da519acffe5ca5c9a8246e796bc5700cffb9694dc99f3aec
|
||||||
|
size 73272920
|
||||||
+3
-4605
File diff suppressed because one or more lines are too long
+3
-3300
File diff suppressed because one or more lines are too long
@@ -1,786 +0,0 @@
|
|||||||
"""
|
|
||||||
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}")
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
|
|
||||||
<!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>
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
|
|
||||||
<!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>
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
|
|
||||||
<!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>
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
|
|
||||||
<!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>
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
|
|
||||||
<!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
+3
-2368
File diff suppressed because one or more lines are too long
@@ -1,191 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
+2
-28
@@ -132,7 +132,6 @@ def train_model(
|
|||||||
learning_rate=0.1,
|
learning_rate=0.1,
|
||||||
use_gpu=True,
|
use_gpu=True,
|
||||||
use_cache=True,
|
use_cache=True,
|
||||||
test_size=0.2,
|
|
||||||
output_model_path=None,
|
output_model_path=None,
|
||||||
status_callback=None,
|
status_callback=None,
|
||||||
cancel_check=None
|
cancel_check=None
|
||||||
@@ -154,7 +153,6 @@ def train_model(
|
|||||||
output_model_path: path to save trained model (auto-generated if None)
|
output_model_path: path to save trained model (auto-generated if None)
|
||||||
status_callback: Optional callback function to report progress
|
status_callback: Optional callback function to report progress
|
||||||
cancel_check: Optional function that returns True if training should be cancelled
|
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:
|
Returns:
|
||||||
Dictionary containing training results
|
Dictionary containing training results
|
||||||
@@ -365,7 +363,7 @@ def train_model(
|
|||||||
|
|
||||||
# Split data
|
# Split data
|
||||||
X_train, X_test, y_train, y_test = train_test_split(
|
X_train, X_test, y_train, y_test = train_test_split(
|
||||||
features, labels_encoded, test_size=test_size, random_state=42, stratify=labels_encoded
|
features, labels_encoded, test_size=0.2, random_state=42, stratify=labels_encoded
|
||||||
)
|
)
|
||||||
|
|
||||||
# Train model based on selected type
|
# Train model based on selected type
|
||||||
@@ -469,22 +467,9 @@ def train_model(
|
|||||||
# PyTorch CNN evaluation
|
# PyTorch CNN evaluation
|
||||||
train_score = model.score(X_train, y_train)
|
train_score = model.score(X_train, y_train)
|
||||||
test_score = model.score(X_test, y_test)
|
test_score = model.score(X_test, y_test)
|
||||||
y_pred = model.predict(X_test)
|
|
||||||
else:
|
else:
|
||||||
train_score = model.score(X_train, y_train)
|
train_score = model.score(X_train, y_train)
|
||||||
test_score = model.score(X_test, y_test)
|
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
|
# Save model
|
||||||
update_status("Saving model...", 95)
|
update_status("Saving model...", 95)
|
||||||
@@ -499,7 +484,6 @@ def train_model(
|
|||||||
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
||||||
"training_samples": len(X_train),
|
"training_samples": len(X_train),
|
||||||
"testing_samples": len(X_test),
|
"testing_samples": len(X_test),
|
||||||
"test_size": test_size,
|
|
||||||
"train_accuracy": float(train_score),
|
"train_accuracy": float(train_score),
|
||||||
"test_accuracy": float(test_score),
|
"test_accuracy": float(test_score),
|
||||||
"model_type": model_type,
|
"model_type": model_type,
|
||||||
@@ -510,9 +494,6 @@ def train_model(
|
|||||||
"cnn_epochs": min(50, n_estimators // 2) if model_type == 'cnn' else None,
|
"cnn_epochs": min(50, n_estimators // 2) if model_type == 'cnn' else None,
|
||||||
"n_features": X_train.shape[1],
|
"n_features": X_train.shape[1],
|
||||||
"n_classes": len(np.unique(y_train)),
|
"n_classes": len(np.unique(y_train)),
|
||||||
"class_names": class_names,
|
|
||||||
"classification_report": cls_report,
|
|
||||||
"confusion_matrix": conf_matrix,
|
|
||||||
"bbox": bbox,
|
"bbox": bbox,
|
||||||
"time_range": time_range,
|
"time_range": time_range,
|
||||||
"resolution": resolution
|
"resolution": resolution
|
||||||
@@ -532,14 +513,7 @@ def train_model(
|
|||||||
"test_accuracy": test_score,
|
"test_accuracy": test_score,
|
||||||
"training_samples": len(X_train),
|
"training_samples": len(X_train),
|
||||||
"testing_samples": len(X_test),
|
"testing_samples": len(X_test),
|
||||||
"test_size": test_size,
|
"classes": label_encoder.classes_.tolist()
|
||||||
"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:
|
except InterruptedError as e:
|
||||||
|
|||||||
+434
-116
@@ -286,7 +286,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Training Interface</h1>
|
<h1>🌍 Land Classification Training</h1>
|
||||||
<p>Giao diện training model phân loại đất từ ảnh vệ tinh</p>
|
<p>Giao diện training model phân loại đất từ ảnh vệ tinh</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -456,10 +456,6 @@
|
|||||||
<label>Learning Rate:</label>
|
<label>Learning Rate:</label>
|
||||||
<input type="number" step="0.01" id="learningRate" value="0.1" min="0.01" max="1" required>
|
<input type="number" step="0.01" id="learningRate" value="0.1" min="0.01" max="1" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="testSizeGroup">
|
|
||||||
<label>Tỷ lệ dữ liệu test (0-1):</label>
|
|
||||||
<input type="number" step="0.05" id="testSize" value="0.2" min="0.05" max="0.5" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" id="useGpuGroup">
|
<div class="form-group" id="useGpuGroup">
|
||||||
<label>Use GPU:</label>
|
<label>Use GPU:</label>
|
||||||
<select id="useGpu" required>
|
<select id="useGpu" required>
|
||||||
@@ -507,22 +503,116 @@
|
|||||||
<div id="modelsList" class="model-list" style="margin-top: 15px;">
|
<div id="modelsList" class="model-list" style="margin-top: 15px;">
|
||||||
<p>Đang tải...</p>
|
<p>Đang tải...</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 15px; visibility: hidden;">
|
</div>
|
||||||
<label for="selectedModel" style="font-weight:600; color:#667eea;">Chọn model để dự đoán:</label>
|
|
||||||
<select id="selectedModel" style="width:100%; padding:10px; border-radius:5px; font-size:1em; margin-top:5px;"></select>
|
<!-- Prediction Section -->
|
||||||
|
<div class="section" style="grid-column: 1 / -1;">
|
||||||
|
<h2 style="text-align: center; margin-bottom: 30px;">🔮 Dự Đoán & Phân Loại (Prediction & Classification)</h2>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1.2fr 1fr; gap: 30px;">
|
||||||
|
<!-- Left: Prediction Map -->
|
||||||
|
<div>
|
||||||
|
<div style="background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%); padding: 20px; border-radius: 12px; border: 2px solid #ff6b6b40;">
|
||||||
|
<h3 style="margin: 0 0 15px 0; color: #ff6b6b; font-size: 18px;">🗺️ Bản Đồ Khu Vực Dự Đoán</h3>
|
||||||
|
|
||||||
|
<div class="map-instructions" style="background: #fff3cd; border-left: 4px solid #ff6b6b; margin-bottom: 15px;">
|
||||||
|
<strong>💡 Hướng dẫn:</strong> Sử dụng công cụ vẽ hình chữ nhật
|
||||||
|
<span style="display: inline-block; width: 24px; height: 24px; background: white; border: 2px solid #ff6b6b; vertical-align: middle; margin: 0 5px;">□</span>
|
||||||
|
để chọn khu vực cần dự đoán
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="predictionMap" style="height: 600px; border-radius: 8px; border: 3px solid #ff6b6b; box-shadow: 0 4px 12px rgba(255,107,107,0.3);"></div>
|
||||||
|
|
||||||
|
<div style="margin-top: 15px; padding: 12px; background: white; border-radius: 6px; border: 1px solid #ddd;">
|
||||||
|
<strong style="color: #ff6b6b;">📍 Tọa độ khu vực:</strong><br>
|
||||||
|
<span id="predBboxDisplay" style="font-family: monospace; color: #333; font-size: 13px;">Chưa chọn khu vực</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reports Section -->
|
<!-- Right: Configuration & Controls -->
|
||||||
<div class="section">
|
<div>
|
||||||
<h2>📝 Báo Cáo (Auto Reports)</h2>
|
<!-- Prediction Status -->
|
||||||
<button class="btn btn-secondary" onclick="loadReports()">🔄 Refresh</button>
|
<div id="predictionStatusBox" class="status-box" style="margin-bottom: 20px;">
|
||||||
<div id="reportsList" class="model-list" style="margin-top: 15px;">
|
<p><strong>Trạng thái:</strong> <span id="predictionStatusText">Chưa bắt đầu</span></p>
|
||||||
<p>Đang tải...</p>
|
<p><strong>Tiến độ:</strong> <span id="predictionProgressText">-</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Prediction Configuration -->
|
||||||
|
<form id="predictionForm">
|
||||||
|
<h3 style="margin-bottom: 15px; color: #ff6b6b;">🤖 Chọn Model</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Model để sử dụng:</label>
|
||||||
|
<select id="selectedModel" required style="border-color: #ff6b6b;">
|
||||||
|
<option value="">-- Chọn model --</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Hidden inputs for prediction bbox -->
|
||||||
|
<input type="hidden" id="predMinLon" value="105.6" required>
|
||||||
|
<input type="hidden" id="predMinLat" value="9.3" required>
|
||||||
|
<input type="hidden" id="predMaxLon" value="106.2" required>
|
||||||
|
<input type="hidden" id="predMaxLat" value="9.8" required>
|
||||||
|
|
||||||
|
<h3 style="margin: 20px 0 15px; color: #ff6b6b;">📅 Thời Gian Dự Đoán</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ngày bắt đầu:</label>
|
||||||
|
<input type="date" id="predStartDate" value="2023-03-01" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ngày kết thúc:</label>
|
||||||
|
<input type="date" id="predEndDate" value="2023-05-31" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin: 20px 0 15px; color: #ff6b6b;">🛰️ Dữ Liệu Vệ Tinh</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Số scenes tối đa:</label>
|
||||||
|
<input type="number" id="predMaxScenes" value="12" min="1" max="100" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Cloud cover (%):</label>
|
||||||
|
<input type="number" id="predCloudCover" value="30" min="0" max="100" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Độ phân giải (m):</label>
|
||||||
|
<select id="predResolution" required>
|
||||||
|
<option value="10">10m (Chính xác cao)</option>
|
||||||
|
<option value="20" selected>20m (Cân bằng)</option>
|
||||||
|
<option value="30">30m (Nhanh)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 30px; text-align: center;">
|
||||||
|
<button type="submit" class="btn btn-primary" id="predictBtn" style="background: linear-gradient(135deg, #ff6b6b, #ee5a6f); width: 100%; padding: 15px; font-size: 16px; font-weight: 600;">
|
||||||
|
🔮 Bắt Đầu Dự Đoán
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Prediction Result -->
|
||||||
|
<div id="predictionResult" style="margin-top: 20px; display: none;">
|
||||||
|
<h3 style="color: #28a745; margin-bottom: 10px;">✅ Kết Quả Dự Đoán</h3>
|
||||||
|
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 20px; border-radius: 8px; border: 2px solid #28a745;">
|
||||||
|
<div id="predResultText" style="font-size: 14px; line-height: 1.8;"></div>
|
||||||
|
<div id="downloadLinkContainer" style="margin-top: 15px; text-align: center;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Previous Predictions List -->
|
||||||
|
<div id="predictionsListSection" style="margin-top: 20px;">
|
||||||
|
<h3 style="color: #ff6b6b; margin-bottom: 10px;">📂 Các File Dự Đoán Đã Tạo</h3>
|
||||||
|
<div id="predictionsList" style="background: #f8f9fa; padding: 15px; border-radius: 8px; max-height: 200px; overflow-y: auto;">
|
||||||
|
<p style="color: #666; text-align: center;">Đang tải...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -541,15 +631,14 @@
|
|||||||
times: []
|
times: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load presets and models only after DOM is ready
|
// Load presets on page load
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
window.onload = async () => {
|
||||||
await loadPresets();
|
await loadPresets();
|
||||||
await loadModels();
|
await loadModels();
|
||||||
await loadReports();
|
|
||||||
await loadSystemInfo();
|
await loadSystemInfo();
|
||||||
checkStatus();
|
checkStatus();
|
||||||
loadTrainingHistory();
|
loadTrainingHistory();
|
||||||
});
|
};
|
||||||
|
|
||||||
// Load preset configurations
|
// Load preset configurations
|
||||||
async function loadPresets() {
|
async function loadPresets() {
|
||||||
@@ -583,9 +672,6 @@
|
|||||||
document.getElementById('maxScenes').value = config.max_scenes;
|
document.getElementById('maxScenes').value = config.max_scenes;
|
||||||
document.getElementById('cloudCover').value = config.cloud_cover;
|
document.getElementById('cloudCover').value = config.cloud_cover;
|
||||||
document.getElementById('resolution').value = config.resolution;
|
document.getElementById('resolution').value = config.resolution;
|
||||||
if (config.test_size !== undefined) {
|
|
||||||
document.getElementById('testSize').value = config.test_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update map with new bounds
|
// Update map with new bounds
|
||||||
if (currentRectangle) {
|
if (currentRectangle) {
|
||||||
@@ -623,7 +709,6 @@
|
|||||||
n_estimators: parseInt(document.getElementById('nEstimators').value),
|
n_estimators: parseInt(document.getElementById('nEstimators').value),
|
||||||
max_depth: parseInt(document.getElementById('maxDepth').value),
|
max_depth: parseInt(document.getElementById('maxDepth').value),
|
||||||
learning_rate: parseFloat(document.getElementById('learningRate').value),
|
learning_rate: parseFloat(document.getElementById('learningRate').value),
|
||||||
test_size: parseFloat(document.getElementById('testSize').value),
|
|
||||||
use_gpu: document.getElementById('useGpu').value === 'true',
|
use_gpu: document.getElementById('useGpu').value === 'true',
|
||||||
use_cache: document.getElementById('useCache').checked
|
use_cache: document.getElementById('useCache').checked
|
||||||
};
|
};
|
||||||
@@ -720,7 +805,6 @@
|
|||||||
|
|
||||||
if (status.result) {
|
if (status.result) {
|
||||||
await loadModels();
|
await loadModels();
|
||||||
await loadReports();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,99 +860,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reports list
|
|
||||||
async function loadReports() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/reports/list`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const container = document.getElementById('reportsList');
|
|
||||||
|
|
||||||
if (data.reports.length === 0) {
|
|
||||||
container.innerHTML = '<p style="color: #666;">Chưa có báo cáo nào. Báo cáo sẽ tự động tạo sau mỗi lần training/prediction.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
data.reports.forEach(report => {
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.className = 'model-item';
|
|
||||||
|
|
||||||
const typeIcon = report.type === 'training' ? '📊' : '🗺️';
|
|
||||||
const typeLabel = report.type === 'training' ? 'Training Report' : 'Prediction Report';
|
|
||||||
const typeBg = report.type === 'training' ? '#667eea' : '#ff6b6b';
|
|
||||||
|
|
||||||
item.innerHTML = `
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<div>
|
|
||||||
<h3 style="margin-bottom: 8px;">
|
|
||||||
${typeIcon} ${report.filename}
|
|
||||||
</h3>
|
|
||||||
<p style="margin: 0;">
|
|
||||||
<span style="background: ${typeBg}; color: white; padding: 3px 10px; border-radius: 12px; font-size: 11px;">${typeLabel}</span>
|
|
||||||
</p>
|
|
||||||
<p style="margin-top: 8px; font-size: 13px; color: #666;">
|
|
||||||
<strong>Tạo lúc:</strong> ${new Date(report.created).toLocaleString('vi-VN')} |
|
|
||||||
<strong>Kích thước:</strong> ${report.size_kb} KB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<button onclick="viewReport('${report.filename}')" class="btn btn-primary" style="padding: 8px 15px; font-size: 13px;">
|
|
||||||
👁️ Xem
|
|
||||||
</button>
|
|
||||||
<button onclick="downloadReport('${report.filename}')" class="btn btn-secondary" style="padding: 8px 15px; font-size: 13px;">
|
|
||||||
📥 Tải
|
|
||||||
</button>
|
|
||||||
<button onclick="deleteReport('${report.filename}')" class="btn btn-danger" style="padding: 8px 15px; font-size: 13px;">
|
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading reports:', error);
|
|
||||||
document.getElementById('reportsList').innerHTML = '<p style="color: red;">Lỗi khi tải danh sách báo cáo</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View report in new tab
|
|
||||||
function viewReport(filename) {
|
|
||||||
window.open(`${API_BASE}/reports/view/${filename}`, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download report
|
|
||||||
function downloadReport(filename) {
|
|
||||||
window.location.href = `${API_BASE}/reports/download/${filename}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete report
|
|
||||||
async function deleteReport(filename) {
|
|
||||||
if (!confirm(`Bạn có chắc muốn xóa báo cáo: ${filename}?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/reports/delete/${filename}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
showNotification('success', 'Đã xóa báo cáo thành công!');
|
|
||||||
loadReports();
|
|
||||||
} else {
|
|
||||||
showNotification('error', 'Không thể xóa báo cáo!');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting report:', error);
|
|
||||||
showNotification('error', 'Lỗi khi xóa báo cáo!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load system information
|
// Load system information
|
||||||
async function loadSystemInfo() {
|
async function loadSystemInfo() {
|
||||||
try {
|
try {
|
||||||
@@ -1284,6 +1275,333 @@
|
|||||||
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
|
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============== PREDICTION FUNCTIONALITY ==============
|
||||||
|
|
||||||
|
let predictionMap, predictionDrawnItems, predictionRectangle;
|
||||||
|
let predictionStatusInterval = null;
|
||||||
|
|
||||||
|
// Initialize prediction map
|
||||||
|
function initPredictionMap() {
|
||||||
|
predictionMap = L.map('predictionMap').setView([9.55, 105.9], 9);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: 18
|
||||||
|
}).addTo(predictionMap);
|
||||||
|
|
||||||
|
predictionDrawnItems = new L.FeatureGroup();
|
||||||
|
predictionMap.addLayer(predictionDrawnItems);
|
||||||
|
|
||||||
|
const drawControl = new L.Control.Draw({
|
||||||
|
draw: {
|
||||||
|
polyline: false,
|
||||||
|
polygon: false,
|
||||||
|
circle: false,
|
||||||
|
marker: false,
|
||||||
|
circlemarker: false,
|
||||||
|
rectangle: {
|
||||||
|
shapeOptions: {
|
||||||
|
color: '#ff6b6b',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
featureGroup: predictionDrawnItems,
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
predictionMap.addControl(drawControl);
|
||||||
|
|
||||||
|
predictionMap.on(L.Draw.Event.CREATED, function(event) {
|
||||||
|
const layer = event.layer;
|
||||||
|
if (predictionRectangle) {
|
||||||
|
predictionDrawnItems.removeLayer(predictionRectangle);
|
||||||
|
}
|
||||||
|
predictionDrawnItems.addLayer(layer);
|
||||||
|
predictionRectangle = layer;
|
||||||
|
const bounds = layer.getBounds();
|
||||||
|
updatePredictionBbox(bounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
predictionMap.on(L.Draw.Event.EDITED, function(event) {
|
||||||
|
const layers = event.layers;
|
||||||
|
layers.eachLayer(function(layer) {
|
||||||
|
const bounds = layer.getBounds();
|
||||||
|
updatePredictionBbox(bounds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
predictionMap.on(L.Draw.Event.DELETED, function() {
|
||||||
|
predictionRectangle = null;
|
||||||
|
document.getElementById('predBboxDisplay').textContent = 'Chưa chọn khu vực';
|
||||||
|
document.getElementById('predMinLon').value = '';
|
||||||
|
document.getElementById('predMinLat').value = '';
|
||||||
|
document.getElementById('predMaxLon').value = '';
|
||||||
|
document.getElementById('predMaxLat').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
drawInitialPredictionRectangle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePredictionBbox(bounds) {
|
||||||
|
const south = bounds.getSouth().toFixed(6);
|
||||||
|
const west = bounds.getWest().toFixed(6);
|
||||||
|
const north = bounds.getNorth().toFixed(6);
|
||||||
|
const east = bounds.getEast().toFixed(6);
|
||||||
|
|
||||||
|
document.getElementById('predMinLat').value = south;
|
||||||
|
document.getElementById('predMinLon').value = west;
|
||||||
|
document.getElementById('predMaxLat').value = north;
|
||||||
|
document.getElementById('predMaxLon').value = east;
|
||||||
|
|
||||||
|
document.getElementById('predBboxDisplay').textContent =
|
||||||
|
`Lon: ${west} → ${east}, Lat: ${south} → ${north}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawInitialPredictionRectangle() {
|
||||||
|
const minLon = parseFloat(document.getElementById('predMinLon').value);
|
||||||
|
const minLat = parseFloat(document.getElementById('predMinLat').value);
|
||||||
|
const maxLon = parseFloat(document.getElementById('predMaxLon').value);
|
||||||
|
const maxLat = parseFloat(document.getElementById('predMaxLat').value);
|
||||||
|
|
||||||
|
if (minLon && minLat && maxLon && maxLat) {
|
||||||
|
const bounds = [[minLat, minLon], [maxLat, maxLon]];
|
||||||
|
const rectangle = L.rectangle(bounds, {
|
||||||
|
color: '#ff6b6b',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
|
||||||
|
predictionDrawnItems.addLayer(rectangle);
|
||||||
|
predictionRectangle = rectangle;
|
||||||
|
predictionMap.fitBounds(bounds);
|
||||||
|
updatePredictionBbox(L.latLngBounds(bounds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle prediction form submission
|
||||||
|
document.getElementById('predictionForm').onsubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
model_filename: document.getElementById('selectedModel').value,
|
||||||
|
min_lon: parseFloat(document.getElementById('predMinLon').value),
|
||||||
|
min_lat: parseFloat(document.getElementById('predMinLat').value),
|
||||||
|
max_lon: parseFloat(document.getElementById('predMaxLon').value),
|
||||||
|
max_lat: parseFloat(document.getElementById('predMaxLat').value),
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.model_filename) {
|
||||||
|
alert('Vui lòng chọn model để dự đoán!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/prediction/start`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
alert('Lỗi: ' + error.detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
alert(result.message);
|
||||||
|
|
||||||
|
// Start monitoring prediction status
|
||||||
|
if (predictionStatusInterval) clearInterval(predictionStatusInterval);
|
||||||
|
predictionStatusInterval = setInterval(checkPredictionStatus, 2000);
|
||||||
|
|
||||||
|
document.getElementById('predictBtn').disabled = true;
|
||||||
|
document.getElementById('predictionResult').style.display = 'none';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert('Lỗi kết nối: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check prediction status
|
||||||
|
async function checkPredictionStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/prediction/status`);
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
const statusBox = document.getElementById('predictionStatusBox');
|
||||||
|
const statusText = document.getElementById('predictionStatusText');
|
||||||
|
const progressText = document.getElementById('predictionProgressText');
|
||||||
|
|
||||||
|
statusText.textContent = status.is_predicting ? 'Đang dự đoán...' :
|
||||||
|
(status.error ? 'Lỗi' : (status.result ? 'Hoàn thành' : 'Chờ'));
|
||||||
|
progressText.textContent = status.progress || '-';
|
||||||
|
|
||||||
|
// Update status box styling
|
||||||
|
statusBox.className = 'status-box';
|
||||||
|
if (status.is_predicting) {
|
||||||
|
statusBox.classList.add('training');
|
||||||
|
} else if (status.error) {
|
||||||
|
statusBox.classList.add('error');
|
||||||
|
} else if (status.result) {
|
||||||
|
statusBox.classList.add('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable/disable button
|
||||||
|
if (!status.is_predicting) {
|
||||||
|
document.getElementById('predictBtn').disabled = false;
|
||||||
|
|
||||||
|
if (predictionStatusInterval) {
|
||||||
|
clearInterval(predictionStatusInterval);
|
||||||
|
predictionStatusInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.result) {
|
||||||
|
displayPredictionResult(status.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking prediction status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display prediction result
|
||||||
|
function displayPredictionResult(result) {
|
||||||
|
const resultDiv = document.getElementById('predictionResult');
|
||||||
|
const resultText = document.getElementById('predResultText');
|
||||||
|
|
||||||
|
// Store result globally for download/view functions
|
||||||
|
window.lastPredictionResult = result;
|
||||||
|
|
||||||
|
// Extract filename from path
|
||||||
|
const filename = result.output_file.split('/').pop();
|
||||||
|
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
|
||||||
|
|
||||||
|
resultText.innerHTML = `
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>📁 File kết quả:</strong><br>
|
||||||
|
<code style="background: #fff; padding: 5px 10px; border-radius: 4px; display: inline-block; margin-top: 5px;">${result.output_file}</code>
|
||||||
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px;">
|
||||||
|
<div><strong>📏 Kích thước:</strong> ${result.shape[0]} x ${result.shape[1]} pixels</div>
|
||||||
|
<div><strong>🎨 Các lớp:</strong> ${result.unique_classes.join(', ')}</div>
|
||||||
|
<div style="grid-column: 1 / -1;"><strong>📍 Khu vực:</strong> [${result.bbox.map(v => v.toFixed(4)).join(', ')}]</div>
|
||||||
|
<div style="grid-column: 1 / -1;"><strong>⏰ Thời gian:</strong> ${result.time_range}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add download link
|
||||||
|
const downloadContainer = document.getElementById('downloadLinkContainer');
|
||||||
|
downloadContainer.innerHTML = `
|
||||||
|
<a href="${downloadUrl}"
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="background: #28a745; padding: 12px 24px; text-decoration: none; display: inline-block; margin-right: 10px;"
|
||||||
|
download="${filename}">
|
||||||
|
📥 Tải GeoTIFF
|
||||||
|
</a>
|
||||||
|
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8;">
|
||||||
|
👁️ Xem Chi Tiết
|
||||||
|
</button>
|
||||||
|
<div style="margin-top: 10px; font-size: 12px; color: #666;">
|
||||||
|
Hoặc copy link: <a href="${downloadUrl}" target="_blank" style="color: #28a745;">${downloadUrl}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Refresh predictions list
|
||||||
|
loadPredictionsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download prediction result
|
||||||
|
function downloadPredictionResult() {
|
||||||
|
if (window.lastPredictionResult) {
|
||||||
|
const result = window.lastPredictionResult;
|
||||||
|
const filename = result.output_file.split('/').pop();
|
||||||
|
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
|
||||||
|
window.open(downloadUrl, '_blank');
|
||||||
|
} else {
|
||||||
|
alert('Chưa có kết quả dự đoán nào!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load list of previous predictions
|
||||||
|
async function loadPredictionsList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/predictions/list`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const listDiv = document.getElementById('predictionsList');
|
||||||
|
|
||||||
|
if (data.predictions && data.predictions.length > 0) {
|
||||||
|
listDiv.innerHTML = data.predictions.map(pred => `
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border: 1px solid #ddd;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<strong style="color: #333;">📄 ${pred.filename}</strong>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 3px;">
|
||||||
|
📅 ${new Date(pred.created).toLocaleString('vi-VN')} | 💾 ${pred.size_mb} MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="${pred.download_url}"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
style="background: #28a745; padding: 6px 12px; font-size: 12px; text-decoration: none;"
|
||||||
|
download="${pred.filename}">
|
||||||
|
📥 Tải về
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
listDiv.innerHTML = '<p style="color: #666; text-align: center;">Chưa có file dự đoán nào.</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading predictions list:', error);
|
||||||
|
document.getElementById('predictionsList').innerHTML =
|
||||||
|
'<p style="color: #dc3545; text-align: center;">Lỗi tải danh sách: ' + error.message + '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// View prediction result details
|
||||||
|
function viewPredictionResult() {
|
||||||
|
if (window.lastPredictionResult) {
|
||||||
|
const result = window.lastPredictionResult;
|
||||||
|
const details = `
|
||||||
|
=== CHI TIẾT KẾT QUẢ DỰ ĐOÁN ===
|
||||||
|
|
||||||
|
📁 File Output: ${result.output_file}
|
||||||
|
|
||||||
|
📊 Thông số ảnh:
|
||||||
|
- Kích thước: ${result.shape[0]} x ${result.shape[1]} pixels
|
||||||
|
- Tổng số pixels: ${result.shape[0] * result.shape[1]}
|
||||||
|
|
||||||
|
🎨 Phân loại:
|
||||||
|
- Các lớp tìm thấy: ${result.unique_classes.join(', ')}
|
||||||
|
- Số lớp phân biệt: ${result.unique_classes.length}
|
||||||
|
|
||||||
|
📍 Vị trí địa lý:
|
||||||
|
- Bbox: [${result.bbox.map(v => v.toFixed(6)).join(', ')}]
|
||||||
|
- Min Lon: ${result.bbox[0].toFixed(6)}°
|
||||||
|
- Min Lat: ${result.bbox[1].toFixed(6)}°
|
||||||
|
- Max Lon: ${result.bbox[2].toFixed(6)}°
|
||||||
|
- Max Lat: ${result.bbox[3].toFixed(6)}°
|
||||||
|
|
||||||
|
⏰ Khoảng thời gian: ${result.time_range}
|
||||||
|
|
||||||
|
✅ Trạng thái: Hoàn thành
|
||||||
|
`;
|
||||||
|
alert(details);
|
||||||
|
} else {
|
||||||
|
alert('Chưa có kết quả dự đoán nào!');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user