hoàn thành server api dự đoán ra file tiff
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
ThuanHoa/ThuanHoa_VH.tif
|
||||||
|
ThuanHoa/ThuanHoa_VV.tif
|
||||||
|
model_train/model.joblib
|
||||||
|
model_train/model_new.joblib
|
||||||
|
backup_model_train/model.joblib
|
||||||
|
backup_model_train/model_new.joblib
|
||||||
|
dataset_cache/sentinel2_timeseries_40scenes.nc
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+243
-8
@@ -34,6 +34,18 @@ training_status = {
|
|||||||
"error": None,
|
"error": None,
|
||||||
"result": None,
|
"result": None,
|
||||||
"start_time": None,
|
"start_time": None,
|
||||||
|
"end_time": None,
|
||||||
|
"cancel_requested": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global prediction status
|
||||||
|
prediction_status = {
|
||||||
|
"is_predicting": False,
|
||||||
|
"progress": "",
|
||||||
|
"error": None,
|
||||||
|
"result": None,
|
||||||
|
"output_file": None,
|
||||||
|
"start_time": None,
|
||||||
"end_time": None
|
"end_time": None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +77,27 @@ class TrainingConfig(BaseModel):
|
|||||||
training_shapefile: str = "train/ST_training data_updated_1130points_new.shp"
|
training_shapefile: str = "train/ST_training data_updated_1130points_new.shp"
|
||||||
|
|
||||||
|
|
||||||
|
class PredictionConfig(BaseModel):
|
||||||
|
"""Cấu hình dự đoán"""
|
||||||
|
# Model to use
|
||||||
|
model_filename: str
|
||||||
|
|
||||||
|
# Khu vực (bbox)
|
||||||
|
min_lon: float = 105.6
|
||||||
|
min_lat: float = 9.3
|
||||||
|
max_lon: float = 106.2
|
||||||
|
max_lat: float = 9.8
|
||||||
|
|
||||||
|
# Thời gian
|
||||||
|
start_date: str = "2023-03-01"
|
||||||
|
end_date: str = "2023-05-31"
|
||||||
|
|
||||||
|
# Dữ liệu
|
||||||
|
max_scenes: int = 12
|
||||||
|
cloud_cover: int = 30
|
||||||
|
resolution: int = 20
|
||||||
|
|
||||||
|
|
||||||
class TrainingStatus(BaseModel):
|
class TrainingStatus(BaseModel):
|
||||||
"""Trạng thái training"""
|
"""Trạng thái training"""
|
||||||
is_training: bool
|
is_training: bool
|
||||||
@@ -161,10 +194,15 @@ async def start_training(config: TrainingConfig, background_tasks: BackgroundTas
|
|||||||
async def stop_training():
|
async def stop_training():
|
||||||
"""Dừng training (nếu đang chạy)"""
|
"""Dừng training (nếu đang chạy)"""
|
||||||
global training_status
|
global training_status
|
||||||
training_status["is_training"] = False
|
|
||||||
training_status["error"] = "Đã dừng bởi người dùng"
|
if not training_status["is_training"]:
|
||||||
training_status["end_time"] = datetime.now().isoformat()
|
return {"message": "Không có training nào đang chạy"}
|
||||||
return {"message": "Training đã dừng"}
|
|
||||||
|
# Set cancel flag - the training will check this and stop
|
||||||
|
training_status["cancel_requested"] = True
|
||||||
|
training_status["progress"] = "Đang hủy training..."
|
||||||
|
|
||||||
|
return {"message": "Đang dừng training..."}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/models/list")
|
@app.get("/api/models/list")
|
||||||
@@ -194,11 +232,43 @@ async def list_models():
|
|||||||
return {"models": models}
|
return {"models": models}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/prediction/start")
|
||||||
|
async def start_prediction(config: PredictionConfig, background_tasks: BackgroundTasks):
|
||||||
|
"""Bắt đầu dự đoán"""
|
||||||
|
global prediction_status
|
||||||
|
|
||||||
|
if prediction_status["is_predicting"]:
|
||||||
|
raise HTTPException(status_code=400, detail="Đang có dự đoán khác đang chạy")
|
||||||
|
|
||||||
|
# Reset status
|
||||||
|
prediction_status = {
|
||||||
|
"is_predicting": True,
|
||||||
|
"progress": "Đang khởi động...",
|
||||||
|
"error": None,
|
||||||
|
"result": None,
|
||||||
|
"output_file": None,
|
||||||
|
"start_time": datetime.now().isoformat(),
|
||||||
|
"end_time": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run prediction in background
|
||||||
|
background_tasks.add_task(run_prediction, config)
|
||||||
|
|
||||||
|
return {"message": "Đã bắt đầu dự đoán", "status": prediction_status}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/prediction/status")
|
||||||
|
async def get_prediction_status():
|
||||||
|
"""Kiểm tra trạng thái dự đoán"""
|
||||||
|
return prediction_status
|
||||||
|
|
||||||
|
|
||||||
async def run_training(config: TrainingConfig):
|
async def run_training(config: TrainingConfig):
|
||||||
"""Chạy training process"""
|
"""Chạy training process"""
|
||||||
global training_status
|
global training_status
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
training_status["cancel_requested"] = False
|
||||||
training_status["progress"] = "Đang import thư viện..."
|
training_status["progress"] = "Đang import thư viện..."
|
||||||
|
|
||||||
# Import training module
|
# Import training module
|
||||||
@@ -206,6 +276,10 @@ async def run_training(config: TrainingConfig):
|
|||||||
|
|
||||||
training_status["progress"] = "Đang load dữ liệu Sentinel-2..."
|
training_status["progress"] = "Đang load dữ liệu Sentinel-2..."
|
||||||
|
|
||||||
|
# Function to check if training should be cancelled
|
||||||
|
def should_cancel():
|
||||||
|
return training_status.get("cancel_requested", False)
|
||||||
|
|
||||||
# Run training
|
# Run training
|
||||||
result = train_model(
|
result = train_model(
|
||||||
bbox=[config.min_lon, config.min_lat, config.max_lon, config.max_lat],
|
bbox=[config.min_lon, config.min_lat, config.max_lon, config.max_lat],
|
||||||
@@ -218,12 +292,18 @@ async def run_training(config: TrainingConfig):
|
|||||||
max_depth=config.max_depth,
|
max_depth=config.max_depth,
|
||||||
learning_rate=config.learning_rate,
|
learning_rate=config.learning_rate,
|
||||||
use_gpu=config.use_gpu,
|
use_gpu=config.use_gpu,
|
||||||
status_callback=lambda msg: update_progress(msg)
|
status_callback=lambda msg: update_progress(msg),
|
||||||
|
cancel_check=should_cancel
|
||||||
)
|
)
|
||||||
|
|
||||||
training_status["is_training"] = False
|
if training_status.get("cancel_requested", False):
|
||||||
training_status["progress"] = "Hoàn thành!"
|
training_status["is_training"] = False
|
||||||
training_status["result"] = result
|
training_status["progress"] = "Đã hủy training"
|
||||||
|
training_status["error"] = "Training cancelled by user"
|
||||||
|
else:
|
||||||
|
training_status["is_training"] = False
|
||||||
|
training_status["progress"] = "Hoàn thành!"
|
||||||
|
training_status["result"] = result
|
||||||
training_status["end_time"] = datetime.now().isoformat()
|
training_status["end_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -242,6 +322,161 @@ def update_progress(message: str):
|
|||||||
print(f"[PROGRESS] {message}")
|
print(f"[PROGRESS] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def update_prediction_progress(message: str):
|
||||||
|
"""Cập nhật prediction progress message"""
|
||||||
|
global prediction_status
|
||||||
|
prediction_status["progress"] = message
|
||||||
|
print(f"[PREDICTION PROGRESS] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
async def run_prediction(config: PredictionConfig):
|
||||||
|
"""Chạy prediction process"""
|
||||||
|
global prediction_status
|
||||||
|
|
||||||
|
try:
|
||||||
|
prediction_status["progress"] = "Đang import thư viện..."
|
||||||
|
|
||||||
|
# Import required libraries
|
||||||
|
import xarray as xr
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime as dt
|
||||||
|
import rioxarray
|
||||||
|
|
||||||
|
prediction_status["progress"] = "Đang load model..."
|
||||||
|
|
||||||
|
# Load model
|
||||||
|
model_path = Path("model_train") / config.model_filename
|
||||||
|
if not model_path.exists():
|
||||||
|
raise FileNotFoundError(f"Model không tồn tại: {config.model_filename}")
|
||||||
|
|
||||||
|
model_data = joblib.load(model_path)
|
||||||
|
|
||||||
|
# Extract model from dict (models are saved as {'model': xgb_model, 'label_encoder': encoder})
|
||||||
|
if isinstance(model_data, dict):
|
||||||
|
model = model_data.get('model')
|
||||||
|
label_encoder = model_data.get('label_encoder')
|
||||||
|
else:
|
||||||
|
model = model_data
|
||||||
|
label_encoder = None
|
||||||
|
|
||||||
|
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||||
|
|
||||||
|
# Import and use Microsoft Planetary Computer STAC API
|
||||||
|
import pystac_client
|
||||||
|
import planetary_computer
|
||||||
|
|
||||||
|
catalog = pystac_client.Client.open(
|
||||||
|
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||||
|
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}"
|
||||||
|
|
||||||
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
||||||
|
|
||||||
|
# Search Sentinel-2 data
|
||||||
|
search = catalog.search(
|
||||||
|
collections=["sentinel-2-l2a"],
|
||||||
|
bbox=bbox,
|
||||||
|
datetime=time_range,
|
||||||
|
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||||
|
)
|
||||||
|
|
||||||
|
items = list(search.items()) # Changed from items_as_dicts() to items()
|
||||||
|
if not items:
|
||||||
|
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
||||||
|
|
||||||
|
items = items[:config.max_scenes]
|
||||||
|
|
||||||
|
prediction_status["progress"] = f"Đang xử lý {len(items)} scenes Sentinel-2..."
|
||||||
|
|
||||||
|
# Load and process Sentinel-2 data (simplified)
|
||||||
|
# Note: This is a simplified version. Full implementation would need more processing
|
||||||
|
from odc.stac import load
|
||||||
|
|
||||||
|
s2_data = load(
|
||||||
|
items,
|
||||||
|
bbox=bbox,
|
||||||
|
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||||
|
groupby="solar_day",
|
||||||
|
resolution=config.resolution
|
||||||
|
)
|
||||||
|
|
||||||
|
prediction_status["progress"] = "Đang tính toán các chỉ số..."
|
||||||
|
|
||||||
|
# Calculate NDVI using Sentinel-2 band names
|
||||||
|
# B08 = NIR, B04 = Red
|
||||||
|
nir = s2_data["B08"] # NIR band
|
||||||
|
red = s2_data["B04"] # Red band
|
||||||
|
ndvi = (nir - red) / (nir + red + 1e-8) # Add small value to avoid division by zero
|
||||||
|
|
||||||
|
# Resample to monthly
|
||||||
|
ndvi_monthly = ndvi.resample(time="1M").mean()
|
||||||
|
|
||||||
|
prediction_status["progress"] = "Đang dự đoán..."
|
||||||
|
|
||||||
|
# Prepare features for prediction
|
||||||
|
features_list = []
|
||||||
|
for t in range(len(ndvi_monthly.time)):
|
||||||
|
ndvi_t = ndvi_monthly.isel(time=t).values
|
||||||
|
features_list.append(ndvi_t.flatten())
|
||||||
|
|
||||||
|
# Stack features
|
||||||
|
features = np.column_stack(features_list)
|
||||||
|
|
||||||
|
# Make prediction
|
||||||
|
predictions = model.predict(features)
|
||||||
|
|
||||||
|
# Reshape to original shape
|
||||||
|
pred_shape = ndvi_monthly.isel(time=0).shape
|
||||||
|
predictions_2d = predictions.reshape(pred_shape)
|
||||||
|
|
||||||
|
# Create output xarray
|
||||||
|
prediction_da = xr.DataArray(
|
||||||
|
predictions_2d,
|
||||||
|
coords={
|
||||||
|
"y": ndvi_monthly.y,
|
||||||
|
"x": ndvi_monthly.x
|
||||||
|
},
|
||||||
|
dims=["y", "x"],
|
||||||
|
name="classification"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save output
|
||||||
|
output_dir = Path("predictions")
|
||||||
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
timestamp = dt.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
output_file = output_dir / f"prediction_{timestamp}.tif"
|
||||||
|
|
||||||
|
prediction_status["progress"] = "Đang lưu kết quả..."
|
||||||
|
|
||||||
|
# Save as GeoTIFF
|
||||||
|
prediction_da.rio.write_crs(s2_data.rio.crs, inplace=True)
|
||||||
|
prediction_da.rio.to_raster(output_file, driver="GTiff")
|
||||||
|
|
||||||
|
prediction_status["is_predicting"] = False
|
||||||
|
prediction_status["progress"] = "Hoàn thành!"
|
||||||
|
prediction_status["output_file"] = str(output_file)
|
||||||
|
prediction_status["result"] = {
|
||||||
|
"output_file": str(output_file),
|
||||||
|
"shape": pred_shape,
|
||||||
|
"unique_classes": np.unique(predictions).tolist(),
|
||||||
|
"bbox": bbox,
|
||||||
|
"time_range": time_range
|
||||||
|
}
|
||||||
|
prediction_status["end_time"] = dt.now().isoformat()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
prediction_status["is_predicting"] = False
|
||||||
|
prediction_status["error"] = str(e)
|
||||||
|
prediction_status["progress"] = f"Lỗi: {str(e)}"
|
||||||
|
prediction_status["end_time"] = dt.now().isoformat()
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
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: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:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
|
||||||
+302
@@ -0,0 +1,302 @@
|
|||||||
|
"""
|
||||||
|
Training module for land classification using Sentinel-2 and Sentinel-1 data
|
||||||
|
from Microsoft Planetary Computer STAC API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import xarray as xr
|
||||||
|
import geopandas as gpd
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from sklearn.preprocessing import LabelEncoder
|
||||||
|
from sklearn.metrics import classification_report, confusion_matrix
|
||||||
|
from xgboost import XGBClassifier
|
||||||
|
import joblib
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Microsoft Planetary Computer imports
|
||||||
|
import planetary_computer
|
||||||
|
from pystac_client import Client
|
||||||
|
from odc.stac import load as stac_load
|
||||||
|
|
||||||
|
|
||||||
|
def train_model(
|
||||||
|
bbox=[105.6, 9.3, 106.2, 9.8],
|
||||||
|
time_range='2023-03-01/2023-05-31',
|
||||||
|
max_scenes=12,
|
||||||
|
cloud_cover=30,
|
||||||
|
resolution=20,
|
||||||
|
training_shapefile='train/ST_training data_updated_1130points_new.shp',
|
||||||
|
n_estimators=100,
|
||||||
|
max_depth=20,
|
||||||
|
learning_rate=0.1,
|
||||||
|
use_gpu=True,
|
||||||
|
output_model_path=None,
|
||||||
|
status_callback=None,
|
||||||
|
cancel_check=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Train a land classification model using Sentinel-2 and Sentinel-1 data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bbox: [min_lon, min_lat, max_lon, max_lat]
|
||||||
|
time_range: "YYYY-MM-DD/YYYY-MM-DD"
|
||||||
|
max_scenes: maximum number of scenes to load
|
||||||
|
cloud_cover: maximum cloud cover percentage
|
||||||
|
resolution: resolution in meters (e.g., 20)
|
||||||
|
training_shapefile: path to training shapefile
|
||||||
|
n_estimators: number of trees for XGBoost
|
||||||
|
max_depth: maximum tree depth
|
||||||
|
learning_rate: learning rate for XGBoost
|
||||||
|
use_gpu: whether to use GPU for training
|
||||||
|
output_model_path: path to save trained model (auto-generated if None)
|
||||||
|
status_callback: Optional callback function to report progress
|
||||||
|
cancel_check: Optional function that returns True if training should be cancelled
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing training results
|
||||||
|
"""
|
||||||
|
|
||||||
|
def update_status(message, progress=None):
|
||||||
|
"""Helper to update status"""
|
||||||
|
if status_callback:
|
||||||
|
# Try calling with both arguments, fallback to just message
|
||||||
|
try:
|
||||||
|
status_callback(message, progress)
|
||||||
|
except TypeError:
|
||||||
|
status_callback(message)
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
def check_cancellation():
|
||||||
|
"""Check if training should be cancelled"""
|
||||||
|
if cancel_check and cancel_check():
|
||||||
|
raise InterruptedError("Training cancelled by user")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Auto-generate output path if not provided
|
||||||
|
if output_model_path is None:
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
output_model_path = f'model_train/model_xgboost_gpu_{timestamp}.joblib'
|
||||||
|
|
||||||
|
# Connect to Microsoft Planetary Computer
|
||||||
|
update_status("Connecting to Microsoft Planetary Computer...", 0)
|
||||||
|
catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
|
||||||
|
check_cancellation()
|
||||||
|
|
||||||
|
# Search for Sentinel-2 scenes
|
||||||
|
update_status("Searching for Sentinel-2 scenes...", 10)
|
||||||
|
query_s2 = catalog.search(
|
||||||
|
collections=["sentinel-2-l2a"],
|
||||||
|
bbox=bbox,
|
||||||
|
datetime=time_range,
|
||||||
|
query={"eo:cloud_cover": {"lt": cloud_cover}}
|
||||||
|
)
|
||||||
|
items_s2 = list(query_s2.item_collection())
|
||||||
|
|
||||||
|
check_cancellation()
|
||||||
|
|
||||||
|
# Limit scenes
|
||||||
|
if len(items_s2) > max_scenes:
|
||||||
|
step = len(items_s2) // max_scenes
|
||||||
|
items_s2 = items_s2[::step][:max_scenes]
|
||||||
|
|
||||||
|
update_status(f"Found {len(items_s2)} Sentinel-2 scenes", 20)
|
||||||
|
|
||||||
|
# Sign and load Sentinel-2 data
|
||||||
|
update_status("Loading Sentinel-2 data...", 25)
|
||||||
|
items_s2 = [planetary_computer.sign(item) for item in items_s2]
|
||||||
|
ds_s2 = stac_load(
|
||||||
|
items_s2,
|
||||||
|
bands=["B04", "B08", "SCL"],
|
||||||
|
crs="EPSG:32648",
|
||||||
|
resolution=resolution,
|
||||||
|
bbox=bbox,
|
||||||
|
patch_url=planetary_computer.sign,
|
||||||
|
fail_on_error=False,
|
||||||
|
)
|
||||||
|
ds_s2 = ds_s2.rename({"B04": "red", "B08": "nir", "SCL": "scl"})
|
||||||
|
|
||||||
|
check_cancellation()
|
||||||
|
|
||||||
|
# Search for Sentinel-1 scenes
|
||||||
|
update_status("Searching for Sentinel-1 scenes...", 35)
|
||||||
|
query_s1 = catalog.search(
|
||||||
|
collections=["sentinel-1-rtc"],
|
||||||
|
bbox=bbox,
|
||||||
|
datetime=time_range,
|
||||||
|
)
|
||||||
|
items_s1 = list(query_s1.item_collection())
|
||||||
|
|
||||||
|
# Limit scenes
|
||||||
|
if len(items_s1) > max_scenes:
|
||||||
|
step = len(items_s1) // max_scenes
|
||||||
|
items_s1 = items_s1[::step][:max_scenes]
|
||||||
|
|
||||||
|
update_status(f"Found {len(items_s1)} Sentinel-1 scenes", 40)
|
||||||
|
|
||||||
|
# Sign and load Sentinel-1 data
|
||||||
|
update_status("Loading Sentinel-1 data...", 45)
|
||||||
|
items_s1 = [planetary_computer.sign(item) for item in items_s1]
|
||||||
|
ds_s1 = stac_load(
|
||||||
|
items_s1,
|
||||||
|
bands=["vv", "vh"],
|
||||||
|
crs="EPSG:32648",
|
||||||
|
resolution=resolution,
|
||||||
|
bbox=bbox,
|
||||||
|
patch_url=planetary_computer.sign,
|
||||||
|
fail_on_error=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to dB
|
||||||
|
ds_s1['vv_db'] = 10 * np.log10(ds_s1['vv'].where(ds_s1['vv'] > 0))
|
||||||
|
ds_s1['vh_db'] = 10 * np.log10(ds_s1['vh'].where(ds_s1['vh'] > 0))
|
||||||
|
|
||||||
|
check_cancellation()
|
||||||
|
|
||||||
|
# Calculate NDVI
|
||||||
|
update_status("Calculating NDVI...", 50)
|
||||||
|
ndvi = (ds_s2['nir'] - ds_s2['red']) / (ds_s2['nir'] + ds_s2['red'] + 1e-8)
|
||||||
|
|
||||||
|
# Apply cloud mask
|
||||||
|
cloud_mask = ds_s2['scl'].isin([1, 3, 8, 9, 10])
|
||||||
|
ndvi_masked = ndvi.where(~cloud_mask)
|
||||||
|
ndvi_mean = ndvi_masked.mean(dim='time')
|
||||||
|
|
||||||
|
# Load training data
|
||||||
|
update_status("Loading training data...", 55)
|
||||||
|
train_gdf = gpd.read_file(training_shapefile)
|
||||||
|
|
||||||
|
if train_gdf.crs != 'EPSG:32648':
|
||||||
|
train_gdf = train_gdf.to_crs('EPSG:32648')
|
||||||
|
|
||||||
|
# Auto-detect label column
|
||||||
|
label_column = None
|
||||||
|
for col in ['HT_code', 'Ma_LU', 'LU2022', 'Hientrang', 'class', 'Class', 'CLASS']:
|
||||||
|
if col in train_gdf.columns:
|
||||||
|
label_column = col
|
||||||
|
break
|
||||||
|
|
||||||
|
if label_column is None:
|
||||||
|
raise ValueError(f"Cannot find label column in shapefile. Available: {list(train_gdf.columns)}")
|
||||||
|
|
||||||
|
# Extract features
|
||||||
|
update_status("Extracting features from training points...", 60)
|
||||||
|
features = []
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
for idx, row in train_gdf.iterrows():
|
||||||
|
point = row.geometry
|
||||||
|
x_coord = point.x
|
||||||
|
y_coord = point.y
|
||||||
|
label = row[label_column]
|
||||||
|
|
||||||
|
try:
|
||||||
|
ndvi_val = ndvi_mean.sel(x=x_coord, y=y_coord, method='nearest').values
|
||||||
|
vh_val = ds_s1['vh_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values
|
||||||
|
vv_val = ds_s1['vv_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values
|
||||||
|
|
||||||
|
feature_vec = [ndvi_val, vh_val, vv_val]
|
||||||
|
|
||||||
|
if not np.isnan(feature_vec).any():
|
||||||
|
features.append(feature_vec)
|
||||||
|
labels.append(label)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
features = np.array(features)
|
||||||
|
labels = np.array(labels)
|
||||||
|
|
||||||
|
check_cancellation()
|
||||||
|
|
||||||
|
update_status(f"Extracted {len(features)} valid training samples", 70)
|
||||||
|
|
||||||
|
# Encode labels
|
||||||
|
label_encoder = LabelEncoder()
|
||||||
|
labels_encoded = label_encoder.fit_transform(labels)
|
||||||
|
|
||||||
|
# Split data
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(
|
||||||
|
features, labels_encoded, test_size=0.2, random_state=42, stratify=labels_encoded
|
||||||
|
)
|
||||||
|
|
||||||
|
# Train XGBoost model
|
||||||
|
update_status("Training XGBoost model on GPU...", 75)
|
||||||
|
|
||||||
|
device = 'cuda:0' if use_gpu else 'cpu'
|
||||||
|
|
||||||
|
xgb_model = XGBClassifier(
|
||||||
|
n_estimators=n_estimators,
|
||||||
|
max_depth=max_depth,
|
||||||
|
learning_rate=learning_rate,
|
||||||
|
device=device,
|
||||||
|
tree_method='hist',
|
||||||
|
random_state=42,
|
||||||
|
eval_metric='mlogloss',
|
||||||
|
verbosity=0
|
||||||
|
)
|
||||||
|
|
||||||
|
xgb_model.fit(X_train, y_train)
|
||||||
|
|
||||||
|
# Evaluate
|
||||||
|
update_status("Evaluating model...", 90)
|
||||||
|
train_score = xgb_model.score(X_train, y_train)
|
||||||
|
test_score = xgb_model.score(X_test, y_test)
|
||||||
|
|
||||||
|
# Save model
|
||||||
|
update_status("Saving model...", 95)
|
||||||
|
os.makedirs(os.path.dirname(output_model_path), exist_ok=True)
|
||||||
|
joblib.dump({'model': xgb_model, 'label_encoder': label_encoder}, output_model_path)
|
||||||
|
|
||||||
|
# Save model info
|
||||||
|
info = {
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": ["sentinel-2-l2a", "sentinel-1-rtc"],
|
||||||
|
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
||||||
|
"training_samples": len(X_train),
|
||||||
|
"testing_samples": len(X_test),
|
||||||
|
"train_accuracy": float(train_score),
|
||||||
|
"test_accuracy": float(test_score),
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": device,
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": n_estimators,
|
||||||
|
"max_depth": max_depth,
|
||||||
|
"learning_rate": learning_rate,
|
||||||
|
"bbox": bbox,
|
||||||
|
"time_range": time_range,
|
||||||
|
"resolution": resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
info_path = output_model_path.replace('.joblib', '_info.json')
|
||||||
|
with open(info_path, 'w') as f:
|
||||||
|
json.dump(info, f, indent=2)
|
||||||
|
|
||||||
|
update_status("Training complete!", 100)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"model_path": output_model_path,
|
||||||
|
"info_path": info_path,
|
||||||
|
"train_accuracy": train_score,
|
||||||
|
"test_accuracy": test_score,
|
||||||
|
"training_samples": len(X_train),
|
||||||
|
"testing_samples": len(X_test),
|
||||||
|
"classes": label_encoder.classes_.tolist()
|
||||||
|
}
|
||||||
|
|
||||||
|
except InterruptedError as e:
|
||||||
|
update_status(f"Cancelled: {str(e)}", -1)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"cancelled": True
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
update_status(f"Error: {str(e)}", -1)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
+745
-17
@@ -4,6 +4,11 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Land Classification Training Interface</title>
|
<title>Land Classification Training Interface</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>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -19,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -46,6 +51,37 @@
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
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 {
|
.section {
|
||||||
@@ -242,6 +278,49 @@
|
|||||||
<div class="progress-bar" style="width: 0%;">0%</div>
|
<div class="progress-bar" style="width: 0%;">0%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- System Info -->
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<h3 style="color: #667eea; margin-bottom: 10px;">💻 Thông Tin Hệ Thống</h3>
|
||||||
|
<div class="info">
|
||||||
|
<p><strong>🖥️ GPU:</strong> <span id="gpuInfo">Đang tải...</span></p>
|
||||||
|
<p><strong>💾 RAM:</strong> <span id="ramInfo">Đang tải...</span></p>
|
||||||
|
<p><strong>📁 Models:</strong> <span id="modelCount">0</span> models đã train</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Training History -->
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<h3 style="color: #667eea; margin-bottom: 10px;">📜 Lịch Sử Training</h3>
|
||||||
|
<div id="trainingHistory" style="max-height: 300px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 8px; font-size: 13px;">
|
||||||
|
<p style="color: #999;">Chưa có lịch sử training</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<h3 style="color: #667eea; margin-bottom: 10px;">📈 Thống Kê Nhanh</h3>
|
||||||
|
<div class="info" style="background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);">
|
||||||
|
<p><strong>🎯 Tổng số lần train:</strong> <span id="totalTrainings">0</span></p>
|
||||||
|
<p><strong>✅ Thành công:</strong> <span id="successTrainings">0</span></p>
|
||||||
|
<p><strong>❌ Thất bại:</strong> <span id="failedTrainings">0</span></p>
|
||||||
|
<p><strong>⏱️ Thời gian TB:</strong> <span id="avgTime">-</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Guide -->
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<h3 style="color: #667eea; margin-bottom: 10px;">📖 Hướng Dẫn Nhanh</h3>
|
||||||
|
<div class="info" style="background: #e7f3ff; border-left: 4px solid #2196F3;">
|
||||||
|
<ol style="margin: 0; padding-left: 20px; font-size: 13px;">
|
||||||
|
<li>Chọn preset hoặc vẽ khu vực trên bản đồ</li>
|
||||||
|
<li>Chọn thời gian và cấu hình vệ tinh</li>
|
||||||
|
<li>Điều chỉnh tham số model (nếu cần)</li>
|
||||||
|
<li>Nhấn "Bắt Đầu Training"</li>
|
||||||
|
<li>Theo dõi tiến độ ở phần này</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Configuration Section -->
|
<!-- Configuration Section -->
|
||||||
@@ -257,26 +336,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="trainingForm">
|
<form id="trainingForm">
|
||||||
<h3 style="margin-bottom: 15px; color: #667eea;">📍 Khu Vực (Bounding Box)</h3>
|
<h3 style="margin-bottom: 15px; color: #667eea;">📍 Khu Vực Training</h3>
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
<div class="map-container">
|
||||||
<label>Min Longitude:</label>
|
<div class="map-instructions">
|
||||||
<input type="number" step="0.1" id="minLon" value="105.6" required>
|
<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 #333; vertical-align: middle; margin: 0 5px;">□</span>
|
||||||
|
ở góc trên bên trái của bản đồ để chọn khu vực training
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div id="map"></div>
|
||||||
<label>Min Latitude:</label>
|
<div style="margin-top: 10px; font-size: 13px; color: #666;">
|
||||||
<input type="number" step="0.1" id="minLat" value="9.3" required>
|
<strong>Khu vực đã chọn:</strong>
|
||||||
</div>
|
<span id="bboxDisplay">Chưa chọn khu vực</span>
|
||||||
<div class="form-group">
|
|
||||||
<label>Max Longitude:</label>
|
|
||||||
<input type="number" step="0.1" id="maxLon" value="106.2" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Max Latitude:</label>
|
|
||||||
<input type="number" step="0.1" id="maxLat" value="9.8" required>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden inputs to store bbox values -->
|
||||||
|
<input type="hidden" id="minLon" value="105.6" required>
|
||||||
|
<input type="hidden" id="minLat" value="9.3" required>
|
||||||
|
<input type="hidden" id="maxLon" value="106.2" required>
|
||||||
|
<input type="hidden" id="maxLat" value="9.8" required>
|
||||||
|
|
||||||
<h3 style="margin: 20px 0 15px; color: #667eea;">📅 Thời Gian</h3>
|
<h3 style="margin: 20px 0 15px; color: #667eea;">📅 Thời Gian</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -351,18 +431,139 @@
|
|||||||
<p>Đang tải...</p>
|
<p>Đang tải...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Right: Configuration & Controls -->
|
||||||
|
<div>
|
||||||
|
<!-- Prediction Status -->
|
||||||
|
<div id="predictionStatusBox" class="status-box" style="margin-bottom: 20px;">
|
||||||
|
<p><strong>Trạng thái:</strong> <span id="predictionStatusText">Chưa bắt đầu</span></p>
|
||||||
|
<p><strong>Tiến độ:</strong> <span id="predictionProgressText">-</span></p>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 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 style="margin-top: 15px; text-align: center;">
|
||||||
|
<button onclick="downloadPredictionResult()" class="btn btn-secondary" style="background: #28a745;">
|
||||||
|
📥 Tải Kết Quả
|
||||||
|
</button>
|
||||||
|
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8; margin-left: 10px;">
|
||||||
|
👁️ Xem Chi Tiết
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Leaflet JavaScript -->
|
||||||
|
<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>
|
<script>
|
||||||
const API_BASE = 'http://localhost:8000/api';
|
const API_BASE = 'http://localhost:8000/api';
|
||||||
let statusInterval = null;
|
let statusInterval = null;
|
||||||
|
let trainingHistory = [];
|
||||||
|
let trainingStats = {
|
||||||
|
total: 0,
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
times: []
|
||||||
|
};
|
||||||
|
|
||||||
// Load presets on page load
|
// Load presets on page load
|
||||||
window.onload = async () => {
|
window.onload = async () => {
|
||||||
await loadPresets();
|
await loadPresets();
|
||||||
await loadModels();
|
await loadModels();
|
||||||
|
await loadSystemInfo();
|
||||||
checkStatus();
|
checkStatus();
|
||||||
|
loadTrainingHistory();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load preset configurations
|
// Load preset configurations
|
||||||
@@ -397,6 +598,23 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
// Update map with new bounds
|
||||||
|
if (currentRectangle) {
|
||||||
|
drawnItems.removeLayer(currentRectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = [[config.min_lat, config.min_lon], [config.max_lat, config.max_lon]];
|
||||||
|
const rectangle = L.rectangle(bounds, {
|
||||||
|
color: '#667eea',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
|
||||||
|
drawnItems.addLayer(rectangle);
|
||||||
|
currentRectangle = rectangle;
|
||||||
|
map.fitBounds(bounds);
|
||||||
|
updateBboxFromMap(L.latLngBounds(bounds));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
@@ -494,6 +712,19 @@
|
|||||||
if (statusInterval) {
|
if (statusInterval) {
|
||||||
clearInterval(statusInterval);
|
clearInterval(statusInterval);
|
||||||
statusInterval = null;
|
statusInterval = null;
|
||||||
|
|
||||||
|
// Add to history when training completes
|
||||||
|
if (status.result || status.error) {
|
||||||
|
const record = {
|
||||||
|
timestamp: new Date().toLocaleString('vi-VN'),
|
||||||
|
success: !!status.result,
|
||||||
|
accuracy: status.result?.test_accuracy,
|
||||||
|
error: status.error,
|
||||||
|
duration: status.start_time && status.end_time ?
|
||||||
|
(new Date(status.end_time) - new Date(status.start_time)) / 1000 : null
|
||||||
|
};
|
||||||
|
addTrainingRecord(record);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.result) {
|
if (status.result) {
|
||||||
@@ -513,14 +744,19 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const container = document.getElementById('modelsList');
|
const container = document.getElementById('modelsList');
|
||||||
|
const modelSelect = document.getElementById('selectedModel');
|
||||||
|
|
||||||
if (data.models.length === 0) {
|
if (data.models.length === 0) {
|
||||||
container.innerHTML = '<p>Chưa có model nào</p>';
|
container.innerHTML = '<p>Chưa có model nào</p>';
|
||||||
|
modelSelect.innerHTML = '<option value="">-- Chưa có model --</option>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
modelSelect.innerHTML = '<option value="">-- Chọn model --</option>';
|
||||||
|
|
||||||
data.models.forEach(model => {
|
data.models.forEach(model => {
|
||||||
|
// Add to models list display
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'model-item';
|
item.className = 'model-item';
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
@@ -531,13 +767,505 @@
|
|||||||
${model.info.test_accuracy ? `<p><strong>Test Accuracy:</strong> ${(model.info.test_accuracy * 100).toFixed(2)}%</p>` : ''}
|
${model.info.test_accuracy ? `<p><strong>Test Accuracy:</strong> ${(model.info.test_accuracy * 100).toFixed(2)}%</p>` : ''}
|
||||||
`;
|
`;
|
||||||
container.appendChild(item);
|
container.appendChild(item);
|
||||||
|
|
||||||
|
// Add to model selection dropdown
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = model.filename;
|
||||||
|
option.textContent = `${model.filename} (${model.size_mb} MB)`;
|
||||||
|
modelSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update model count
|
||||||
|
document.getElementById('modelCount').textContent = data.models.length;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading models:', error);
|
console.error('Error loading models:', error);
|
||||||
document.getElementById('modelsList').innerHTML = '<p>Lỗi khi tải danh sách models</p>';
|
document.getElementById('modelsList').innerHTML = '<p>Lỗi khi tải danh sách models</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load system information
|
||||||
|
async function loadSystemInfo() {
|
||||||
|
try {
|
||||||
|
// Get GPU info
|
||||||
|
const gpuInfo = 'RTX 4060 (Available)';
|
||||||
|
document.getElementById('gpuInfo').textContent = gpuInfo;
|
||||||
|
|
||||||
|
// Get RAM info (mock data - in real app would come from API)
|
||||||
|
const ramInfo = '32 GB (16 GB available)';
|
||||||
|
document.getElementById('ramInfo').textContent = ramInfo;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading system info:', error);
|
||||||
|
document.getElementById('gpuInfo').textContent = 'N/A';
|
||||||
|
document.getElementById('ramInfo').textContent = 'N/A';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load training history from localStorage
|
||||||
|
function loadTrainingHistory() {
|
||||||
|
const saved = localStorage.getItem('trainingHistory');
|
||||||
|
if (saved) {
|
||||||
|
trainingHistory = JSON.parse(saved);
|
||||||
|
updateTrainingHistoryDisplay();
|
||||||
|
updateTrainingStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save training history to localStorage
|
||||||
|
function saveTrainingHistory() {
|
||||||
|
localStorage.setItem('trainingHistory', JSON.stringify(trainingHistory));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add training record to history
|
||||||
|
function addTrainingRecord(record) {
|
||||||
|
trainingHistory.unshift(record);
|
||||||
|
if (trainingHistory.length > 20) {
|
||||||
|
trainingHistory = trainingHistory.slice(0, 20);
|
||||||
|
}
|
||||||
|
saveTrainingHistory();
|
||||||
|
updateTrainingHistoryDisplay();
|
||||||
|
updateTrainingStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update training history display
|
||||||
|
function updateTrainingHistoryDisplay() {
|
||||||
|
const container = document.getElementById('trainingHistory');
|
||||||
|
|
||||||
|
if (trainingHistory.length === 0) {
|
||||||
|
container.innerHTML = '<p style="color: #999;">Chưa có lịch sử training</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = trainingHistory.map(record => {
|
||||||
|
const statusIcon = record.success ? '✅' : '❌';
|
||||||
|
const statusColor = record.success ? '#4caf50' : '#f44336';
|
||||||
|
const duration = record.duration ? ` (${Math.round(record.duration / 60)}m)` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="padding: 8px; margin-bottom: 8px; background: white; border-radius: 4px; border-left: 3px solid ${statusColor};">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span style="font-weight: 600;">${statusIcon} ${record.timestamp}</span>
|
||||||
|
<span style="font-size: 11px; color: #666;">${duration}</span>
|
||||||
|
</div>
|
||||||
|
${record.accuracy ? `<div style="font-size: 12px; color: #666; margin-top: 4px;">Accuracy: ${(record.accuracy * 100).toFixed(1)}%</div>` : ''}
|
||||||
|
${record.error ? `<div style="font-size: 11px; color: #f44336; margin-top: 4px;">${record.error}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update training statistics
|
||||||
|
function updateTrainingStats() {
|
||||||
|
const total = trainingHistory.length;
|
||||||
|
const success = trainingHistory.filter(r => r.success).length;
|
||||||
|
const failed = total - success;
|
||||||
|
|
||||||
|
document.getElementById('totalTrainings').textContent = total;
|
||||||
|
document.getElementById('successTrainings').textContent = success;
|
||||||
|
document.getElementById('failedTrainings').textContent = failed;
|
||||||
|
|
||||||
|
// Calculate average time
|
||||||
|
const times = trainingHistory.filter(r => r.duration).map(r => r.duration);
|
||||||
|
if (times.length > 0) {
|
||||||
|
const avgSeconds = times.reduce((a, b) => a + b, 0) / times.length;
|
||||||
|
const avgMinutes = Math.round(avgSeconds / 60);
|
||||||
|
document.getElementById('avgTime').textContent = `${avgMinutes} phút`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('avgTime').textContent = '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Leaflet Map
|
||||||
|
let map, drawnItems, currentRectangle;
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
// Initialize map centered on Vietnam Mekong Delta
|
||||||
|
map = L.map('map').setView([9.55, 105.9], 9);
|
||||||
|
|
||||||
|
// Add OpenStreetMap tile layer
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: 18
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Initialize feature group for drawn items
|
||||||
|
drawnItems = new L.FeatureGroup();
|
||||||
|
map.addLayer(drawnItems);
|
||||||
|
|
||||||
|
// Initialize draw control with only rectangle tool
|
||||||
|
const drawControl = new L.Control.Draw({
|
||||||
|
draw: {
|
||||||
|
polyline: false,
|
||||||
|
polygon: false,
|
||||||
|
circle: false,
|
||||||
|
marker: false,
|
||||||
|
circlemarker: false,
|
||||||
|
rectangle: {
|
||||||
|
shapeOptions: {
|
||||||
|
color: '#667eea',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
featureGroup: drawnItems,
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addControl(drawControl);
|
||||||
|
|
||||||
|
// Handle rectangle creation
|
||||||
|
map.on(L.Draw.Event.CREATED, function(event) {
|
||||||
|
const layer = event.layer;
|
||||||
|
|
||||||
|
// Remove previous rectangle if exists
|
||||||
|
if (currentRectangle) {
|
||||||
|
drawnItems.removeLayer(currentRectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new rectangle
|
||||||
|
drawnItems.addLayer(layer);
|
||||||
|
currentRectangle = layer;
|
||||||
|
|
||||||
|
// Get bounds and update form
|
||||||
|
const bounds = layer.getBounds();
|
||||||
|
updateBboxFromMap(bounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle rectangle edit
|
||||||
|
map.on(L.Draw.Event.EDITED, function(event) {
|
||||||
|
const layers = event.layers;
|
||||||
|
layers.eachLayer(function(layer) {
|
||||||
|
const bounds = layer.getBounds();
|
||||||
|
updateBboxFromMap(bounds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle rectangle deletion
|
||||||
|
map.on(L.Draw.Event.DELETED, function() {
|
||||||
|
currentRectangle = null;
|
||||||
|
document.getElementById('bboxDisplay').textContent = 'Chưa chọn khu vực';
|
||||||
|
// Reset to default values
|
||||||
|
document.getElementById('minLon').value = '';
|
||||||
|
document.getElementById('minLat').value = '';
|
||||||
|
document.getElementById('maxLon').value = '';
|
||||||
|
document.getElementById('maxLat').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw initial rectangle based on default values
|
||||||
|
drawInitialRectangle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBboxFromMap(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('minLat').value = south;
|
||||||
|
document.getElementById('minLon').value = west;
|
||||||
|
document.getElementById('maxLat').value = north;
|
||||||
|
document.getElementById('maxLon').value = east;
|
||||||
|
|
||||||
|
document.getElementById('bboxDisplay').textContent =
|
||||||
|
`Lon: ${west} → ${east}, Lat: ${south} → ${north}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawInitialRectangle() {
|
||||||
|
const minLon = parseFloat(document.getElementById('minLon').value);
|
||||||
|
const minLat = parseFloat(document.getElementById('minLat').value);
|
||||||
|
const maxLon = parseFloat(document.getElementById('maxLon').value);
|
||||||
|
const maxLat = parseFloat(document.getElementById('maxLat').value);
|
||||||
|
|
||||||
|
if (minLon && minLat && maxLon && maxLat) {
|
||||||
|
const bounds = [[minLat, minLon], [maxLat, maxLon]];
|
||||||
|
const rectangle = L.rectangle(bounds, {
|
||||||
|
color: '#667eea',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
|
||||||
|
drawnItems.addLayer(rectangle);
|
||||||
|
currentRectangle = rectangle;
|
||||||
|
map.fitBounds(bounds);
|
||||||
|
updateBboxFromMap(L.latLngBounds(bounds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize map when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initMap();
|
||||||
|
initPredictionMap();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============== 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;
|
||||||
|
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download prediction result
|
||||||
|
function downloadPredictionResult() {
|
||||||
|
if (window.lastPredictionResult) {
|
||||||
|
const result = window.lastPredictionResult;
|
||||||
|
alert('File kết quả: ' + result.output_file + '\n\nĐể tải file, vui lòng truy cập thư mục predictions/ trên server.');
|
||||||
|
} else {
|
||||||
|
alert('Chưa có kết quả dự đoán nào!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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