From 7952cfdd53f6b2080ae7fcc984e27350a7229849 Mon Sep 17 00:00:00 2001 From: Victor Phan Date: Sun, 14 Dec 2025 18:26:25 +0700 Subject: [PATCH] =?UTF-8?q?ho=C3=A0n=20=20th=C3=A0nh=20c=C6=A1=20b?= =?UTF-8?q?=E1=BA=A3n=20c=C3=A1c=20ch=E1=BB=A9c=20n=C4=83ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/api_server.cpython-310.pyc | Bin 13759 -> 17812 bytes __pycache__/train_module.cpython-310.pyc | Bin 7203 -> 12773 bytes api_server.py | 190 ++++++- ...ta_59838d7be931abe93b5dd38e7cd89ad7.joblib | 3 + ...ta_73f65eba2eb052d78cdbf76250e1e68a.joblib | 3 + model_train/model_cnn_20251214_180423.joblib | 3 + .../model_cnn_20251214_180423_info.json | 33 ++ model_train/model_cnn_20251214_181104.joblib | 3 + .../model_cnn_20251214_181104_info.json | 33 ++ model_train/model_cnn_20251214_182307.joblib | 3 + .../model_cnn_20251214_182307_info.json | 33 ++ .../model_xgboost_gpu_20251214_133256.joblib | 3 + ...odel_xgboost_gpu_20251214_133256_info.json | 31 + .../model_xgboost_gpu_20251214_164426.joblib | 3 + ...odel_xgboost_gpu_20251214_164426_info.json | 31 + predictions/prediction_20251214_100243.tif | 3 + predictions/prediction_20251214_165405.tif | 3 + predictions/prediction_20251214_182353.tif | 3 + train_module.py | 528 +++++++++++++----- training_interface.html | 364 +++++++++++- 20 files changed, 1105 insertions(+), 168 deletions(-) create mode 100644 dataset_cache/training_data_59838d7be931abe93b5dd38e7cd89ad7.joblib create mode 100644 dataset_cache/training_data_73f65eba2eb052d78cdbf76250e1e68a.joblib create mode 100644 model_train/model_cnn_20251214_180423.joblib create mode 100644 model_train/model_cnn_20251214_180423_info.json create mode 100644 model_train/model_cnn_20251214_181104.joblib create mode 100644 model_train/model_cnn_20251214_181104_info.json create mode 100644 model_train/model_cnn_20251214_182307.joblib create mode 100644 model_train/model_cnn_20251214_182307_info.json create mode 100644 model_train/model_xgboost_gpu_20251214_133256.joblib create mode 100644 model_train/model_xgboost_gpu_20251214_133256_info.json create mode 100644 model_train/model_xgboost_gpu_20251214_164426.joblib create mode 100644 model_train/model_xgboost_gpu_20251214_164426_info.json create mode 100644 predictions/prediction_20251214_100243.tif create mode 100644 predictions/prediction_20251214_165405.tif create mode 100644 predictions/prediction_20251214_182353.tif diff --git a/__pycache__/api_server.cpython-310.pyc b/__pycache__/api_server.cpython-310.pyc index 4463aa3ef2f36cfec00a12fc6194ecf1bfcb8c88..6557ea648485184760720bb82bca3c7c59d96b3a 100644 GIT binary patch delta 8527 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 delta 4393 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 diff --git a/__pycache__/train_module.cpython-310.pyc b/__pycache__/train_module.cpython-310.pyc index cfb31ff24e96f3e33042a09454fc74eed23db114..47c9e56f9356872bed627c6b5fbbf360aeccaaeb 100644 GIT binary patch literal 12773 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 expected_features: + # Trim to expected number (use only first N features - NDVI only) + prediction_status["progress"] = f"Cắt bớt features từ {features.shape[1]} xuống {expected_features}..." + features = features[:, :expected_features] + elif features.shape[1] < expected_features: + # Pad with zeros or repeat last features + prediction_status["progress"] = f"Thêm features từ {features.shape[1]} lên {expected_features}..." + n_missing = expected_features - features.shape[1] + # Repeat last feature column to fill + padding = np.tile(features[:, -1:], (1, n_missing)) + features = np.column_stack([features, padding]) + except Exception as e: + prediction_status["progress"] = f"Không thể xác định số features của model, tiếp tục với {features.shape[1]} features..." + prediction_status["progress"] = f"Đang dự đoán với {features.shape[1]} features..." # Make prediction - predictions = model.predict(features) + if is_cnn_model: + # PyTorch CNN prediction + predictions = model.predict(features) + else: + predictions = model.predict(features) # Decode labels if label_encoder exists if label_encoder is not None: @@ -594,6 +731,49 @@ async def run_prediction(config: PredictionConfig): print(traceback.format_exc()) +@app.get("/api/predictions/list") +async def list_predictions(): + """Lấy danh sách các file prediction đã tạo""" + predictions_dir = Path("predictions") + predictions_dir.mkdir(exist_ok=True) + + predictions = [] + for pred_file in predictions_dir.glob("*.tif"): + predictions.append({ + "filename": pred_file.name, + "created": datetime.fromtimestamp(pred_file.stat().st_mtime).isoformat(), + "size_mb": round(pred_file.stat().st_size / 1024 / 1024, 2), + "download_url": f"/api/predictions/download/{pred_file.name}" + }) + + # Sort by creation time (newest first) + predictions.sort(key=lambda x: x["created"], reverse=True) + return {"predictions": predictions} + + +@app.get("/api/predictions/download/{filename}") +async def download_prediction(filename: str): + """Download file prediction GeoTIFF""" + predictions_dir = Path("predictions") + file_path = predictions_dir / filename + + # Security check: ensure filename doesn't contain path traversal + 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"File không tồn tại: {filename}") + + return FileResponse( + path=str(file_path), + filename=filename, + media_type="image/tiff", + headers={ + "Content-Disposition": f"attachment; filename={filename}" + } + ) + + if __name__ == "__main__": print("=" * 70) print("🚀 LAND CLASSIFICATION TRAINING API SERVER") diff --git a/dataset_cache/training_data_59838d7be931abe93b5dd38e7cd89ad7.joblib b/dataset_cache/training_data_59838d7be931abe93b5dd38e7cd89ad7.joblib new file mode 100644 index 0000000..e3e6077 --- /dev/null +++ b/dataset_cache/training_data_59838d7be931abe93b5dd38e7cd89ad7.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7318566231680cb0c97883b7a5e4177aa1bfeec462a0f52ed34aa103ddec210b +size 20903 diff --git a/dataset_cache/training_data_73f65eba2eb052d78cdbf76250e1e68a.joblib b/dataset_cache/training_data_73f65eba2eb052d78cdbf76250e1e68a.joblib new file mode 100644 index 0000000..f03043e --- /dev/null +++ b/dataset_cache/training_data_73f65eba2eb052d78cdbf76250e1e68a.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9cabf5e0241dcc3a73133ac8ae11171f34042c491f895b268c016407619bdfe1 +size 20903 diff --git a/model_train/model_cnn_20251214_180423.joblib b/model_train/model_cnn_20251214_180423.joblib new file mode 100644 index 0000000..746561c --- /dev/null +++ b/model_train/model_cnn_20251214_180423.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae2f82f6c837396729cc63efa41ee3048d9a7de3197e28318dc846be830239b9 +size 41536 diff --git a/model_train/model_cnn_20251214_180423_info.json b/model_train/model_cnn_20251214_180423_info.json new file mode 100644 index 0000000..1f8c6ed --- /dev/null +++ b/model_train/model_cnn_20251214_180423_info.json @@ -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 +} \ No newline at end of file diff --git a/model_train/model_cnn_20251214_181104.joblib b/model_train/model_cnn_20251214_181104.joblib new file mode 100644 index 0000000..d94bf3a --- /dev/null +++ b/model_train/model_cnn_20251214_181104.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abc03d62c7f620b88150a6481143026d516fd3a34bd5ab67c734adac4a8900f9 +size 41536 diff --git a/model_train/model_cnn_20251214_181104_info.json b/model_train/model_cnn_20251214_181104_info.json new file mode 100644 index 0000000..c198133 --- /dev/null +++ b/model_train/model_cnn_20251214_181104_info.json @@ -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 +} \ No newline at end of file diff --git a/model_train/model_cnn_20251214_182307.joblib b/model_train/model_cnn_20251214_182307.joblib new file mode 100644 index 0000000..1ecbfc2 --- /dev/null +++ b/model_train/model_cnn_20251214_182307.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21f906aa2a61d2e793a3463df95fe3e364134934af326771efae90d80caca419 +size 41536 diff --git a/model_train/model_cnn_20251214_182307_info.json b/model_train/model_cnn_20251214_182307_info.json new file mode 100644 index 0000000..71e0f37 --- /dev/null +++ b/model_train/model_cnn_20251214_182307_info.json @@ -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 +} \ No newline at end of file diff --git a/model_train/model_xgboost_gpu_20251214_133256.joblib b/model_train/model_xgboost_gpu_20251214_133256.joblib new file mode 100644 index 0000000..a8bca94 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251214_133256.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b717f564f9413a6e5c9cd3f7011cbc18be02479691d01c554986defb400f0490 +size 1347520 diff --git a/model_train/model_xgboost_gpu_20251214_133256_info.json b/model_train/model_xgboost_gpu_20251214_133256_info.json new file mode 100644 index 0000000..790ed09 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251214_133256_info.json @@ -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 +} \ No newline at end of file diff --git a/model_train/model_xgboost_gpu_20251214_164426.joblib b/model_train/model_xgboost_gpu_20251214_164426.joblib new file mode 100644 index 0000000..2843e78 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251214_164426.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a44efc173619782c8add224e9024f33ef0150ab192b5435d009cb119c379f03c +size 2088632 diff --git a/model_train/model_xgboost_gpu_20251214_164426_info.json b/model_train/model_xgboost_gpu_20251214_164426_info.json new file mode 100644 index 0000000..a0ddcb0 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251214_164426_info.json @@ -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 +} \ No newline at end of file diff --git a/predictions/prediction_20251214_100243.tif b/predictions/prediction_20251214_100243.tif new file mode 100644 index 0000000..84cafa6 --- /dev/null +++ b/predictions/prediction_20251214_100243.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687 +size 73272920 diff --git a/predictions/prediction_20251214_165405.tif b/predictions/prediction_20251214_165405.tif new file mode 100644 index 0000000..07cc4d8 --- /dev/null +++ b/predictions/prediction_20251214_165405.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8fb13c2e466b9811104cbd7747ea6ca8c14ddb44802d934aadfe52a4b5dd916 +size 73272920 diff --git a/predictions/prediction_20251214_182353.tif b/predictions/prediction_20251214_182353.tif new file mode 100644 index 0000000..4876e4b --- /dev/null +++ b/predictions/prediction_20251214_182353.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2fe092e1fd96e56da519acffe5ca5c9a8246e796bc5700cffb9694dc99f3aec +size 73272920 diff --git a/train_module.py b/train_module.py index e4b58e7..f741a41 100644 --- a/train_module.py +++ b/train_module.py @@ -9,11 +9,109 @@ import geopandas as gpd from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder from sklearn.metrics import classification_report, confusion_matrix +from sklearn.ensemble import RandomForestClassifier +from sklearn.tree import DecisionTreeClassifier +from sklearn.svm import SVC from xgboost import XGBClassifier import joblib from datetime import datetime import json import os +import warnings +import hashlib +from pathlib import Path +warnings.filterwarnings('ignore') + +# PyTorch for CNN +try: + import torch + import torch.nn as nn + import torch.nn.functional as F + import torch.optim as optim + from torch.utils.data import TensorDataset, DataLoader + PYTORCH_AVAILABLE = True +except ImportError: + PYTORCH_AVAILABLE = False + print("Warning: PyTorch not available. CNN model will not work.") + +# Define CNN model class for PyTorch +class CNNClassifier(nn.Module): + def __init__(self, n_features, n_classes): + super(CNNClassifier, self).__init__() + self.n_features = n_features + self.n_classes = n_classes + + # For small feature sets (like 3 features), use simpler architecture + if n_features < 8: + # Simple fully connected network for small features + self.use_conv = False + self.fc1 = nn.Linear(n_features, 64) + self.dropout1 = nn.Dropout(0.3) + self.fc2 = nn.Linear(64, 128) + self.dropout2 = nn.Dropout(0.5) + self.fc3 = nn.Linear(128, n_classes) + else: + # CNN architecture for larger feature sets + self.use_conv = True + self.conv1 = nn.Conv1d(in_channels=1, out_channels=32, kernel_size=3, padding=1) + self.pool1 = nn.MaxPool1d(kernel_size=2) + self.conv2 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1) + self.pool2 = nn.MaxPool1d(kernel_size=2) + + # Calculate size after convolutions + conv_output_size = (n_features // 2 // 2) * 64 + + # Fully connected layers + self.fc1 = nn.Linear(conv_output_size, 128) + self.dropout = nn.Dropout(0.5) + self.fc2 = nn.Linear(128, n_classes) + + def forward(self, x): + # x shape: (batch, n_features) or (batch, 1, n_features) + if self.use_conv: + # CNN path for larger feature sets + if len(x.shape) == 2: + x = x.unsqueeze(1) # Add channel dimension + x = F.relu(self.conv1(x)) + x = self.pool1(x) + x = F.relu(self.conv2(x)) + x = self.pool2(x) + x = x.view(x.size(0), -1) # Flatten + x = F.relu(self.fc1(x)) + x = self.dropout(x) + x = self.fc2(x) + else: + # Fully connected path for small feature sets + if len(x.shape) == 3: + x = x.squeeze(1) # Remove channel dimension if present + x = F.relu(self.fc1(x)) + x = self.dropout1(x) + x = F.relu(self.fc2(x)) + x = self.dropout2(x) + x = self.fc3(x) + return x + + def predict(self, X): + """Scikit-learn style predict method""" + self.eval() + with torch.no_grad(): + if isinstance(X, np.ndarray): + X = torch.FloatTensor(X) + # Handle both 2D and 3D inputs + if not self.use_conv and len(X.shape) == 3: + X = X.squeeze(1) + elif self.use_conv and len(X.shape) == 2: + X = X.unsqueeze(1) + outputs = self(X) + _, predicted = torch.max(outputs, 1) + return predicted.cpu().numpy() + + def score(self, X, y): + """Scikit-learn style score method""" + predictions = self.predict(X) + if isinstance(y, torch.Tensor): + y = y.cpu().numpy() + return np.mean(predictions == y) # Microsoft Planetary Computer imports import planetary_computer @@ -28,10 +126,12 @@ def train_model( cloud_cover=30, resolution=20, training_shapefile='train/ST_training data_updated_1130points_new.shp', + model_type='xgboost', n_estimators=100, max_depth=20, learning_rate=0.1, use_gpu=True, + use_cache=True, output_model_path=None, status_callback=None, cancel_check=None @@ -77,139 +177,185 @@ def train_model( # 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' + output_model_path = f'model_train/model_{model_type}_{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() + # ============ CACHE SYSTEM ============ + # Create cache directory + cache_dir = Path("dataset_cache") + cache_dir.mkdir(exist_ok=True) - # 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()) + # Generate cache key from parameters + cache_params = f"{bbox}_{time_range}_{max_scenes}_{cloud_cover}_{resolution}" + cache_key = hashlib.md5(cache_params.encode()).hexdigest() + cache_file = cache_dir / f"training_data_{cache_key}.joblib" - check_cancellation() + features = None + labels = None - # 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 to load from cache + if use_cache and cache_file.exists(): + update_status(f"📦 Loading cached dataset from {cache_file.name}...", 5) 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 + cached_data = joblib.load(cache_file) + features = cached_data['features'] + labels = cached_data['labels'] + update_status(f"✅ Loaded {len(features)} samples from cache (skipped satellite download!)", 50) + except Exception as e: + update_status(f"⚠️ Cache load failed: {str(e)}, downloading fresh data...", 10) + features = None + + # If no cache or cache failed, download data + if features is None: + update_status("📡 Cache not found or disabled, downloading satellite data...", 10) + + # Connect to Microsoft Planetary Computer + update_status("Connecting to Microsoft Planetary Computer...", 12) + catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1") + check_cancellation() + + # Search for Sentinel-2 scenes + update_status("Searching for Sentinel-2 scenes...", 10) + query_s2 = catalog.search( + collections=["sentinel-2-l2a"], + bbox=bbox, + datetime=time_range, + query={"eo:cloud_cover": {"lt": cloud_cover}} + ) + items_s2 = list(query_s2.item_collection()) + + check_cancellation() + + # Limit scenes + if len(items_s2) > max_scenes: + step = len(items_s2) // max_scenes + items_s2 = items_s2[::step][:max_scenes] + + update_status(f"Found {len(items_s2)} Sentinel-2 scenes", 20) + + # Sign and load Sentinel-2 data + update_status("Loading Sentinel-2 data...", 25) + items_s2 = [planetary_computer.sign(item) for item in items_s2] + ds_s2 = stac_load( + items_s2, + bands=["B04", "B08", "SCL"], + crs="EPSG:32648", + resolution=resolution, + bbox=bbox, + patch_url=planetary_computer.sign, + fail_on_error=False, + ) + ds_s2 = ds_s2.rename({"B04": "red", "B08": "nir", "SCL": "scl"}) + + check_cancellation() + + # Search for Sentinel-1 scenes + update_status("Searching for Sentinel-1 scenes...", 35) + query_s1 = catalog.search( + collections=["sentinel-1-rtc"], + bbox=bbox, + datetime=time_range, + ) + items_s1 = list(query_s1.item_collection()) + + # Limit scenes + if len(items_s1) > max_scenes: + step = len(items_s1) // max_scenes + items_s1 = items_s1[::step][:max_scenes] + + update_status(f"Found {len(items_s1)} Sentinel-1 scenes", 40) + + # Sign and load Sentinel-1 data + update_status("Loading Sentinel-1 data...", 45) + items_s1 = [planetary_computer.sign(item) for item in items_s1] + ds_s1 = stac_load( + items_s1, + bands=["vv", "vh"], + crs="EPSG:32648", + resolution=resolution, + bbox=bbox, + patch_url=planetary_computer.sign, + fail_on_error=False, + ) + + # Convert to dB + ds_s1['vv_db'] = 10 * np.log10(ds_s1['vv'].where(ds_s1['vv'] > 0)) + ds_s1['vh_db'] = 10 * np.log10(ds_s1['vh'].where(ds_s1['vh'] > 0)) + + check_cancellation() + + # Calculate NDVI + update_status("Calculating NDVI...", 50) + ndvi = (ds_s2['nir'] - ds_s2['red']) / (ds_s2['nir'] + ds_s2['red'] + 1e-8) + + # Apply cloud mask + cloud_mask = ds_s2['scl'].isin([1, 3, 8, 9, 10]) + ndvi_masked = ndvi.where(~cloud_mask) + ndvi_mean = ndvi_masked.mean(dim='time') + + # Load training data + update_status("Loading training data...", 55) + train_gdf = gpd.read_file(training_shapefile) + + if train_gdf.crs != 'EPSG:32648': + train_gdf = train_gdf.to_crs('EPSG:32648') + + # Auto-detect label column + label_column = None + for col in ['HT_code', 'Ma_LU', 'LU2022', 'Hientrang', 'class', 'Class', 'CLASS']: + if col in train_gdf.columns: + label_column = col + break + + if label_column is None: + raise ValueError(f"Cannot find label column in shapefile. Available: {list(train_gdf.columns)}") + + # Extract features + update_status("Extracting features from training points...", 60) + features = [] + labels = [] + + for idx, row in train_gdf.iterrows(): + point = row.geometry + x_coord = point.x + y_coord = point.y + label = row[label_column] - 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) + try: + ndvi_val = ndvi_mean.sel(x=x_coord, y=y_coord, method='nearest').values + vh_val = ds_s1['vh_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values + vv_val = ds_s1['vv_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values + + feature_vec = [ndvi_val, vh_val, vv_val] + + if not np.isnan(feature_vec).any(): + features.append(feature_vec) + labels.append(label) + except: + continue + + features = np.array(features) + labels = np.array(labels) + + check_cancellation() + + update_status(f"Extracted {len(features)} valid training samples", 70) + + # ============ SAVE TO CACHE ============ + if use_cache: + update_status(f"💾 Saving dataset to cache for future use...", 72) + try: + cache_data = { + 'features': features, + 'labels': labels, + 'bbox': bbox, + 'time_range': time_range, + 'resolution': resolution, + 'timestamp': datetime.now().isoformat() + } + joblib.dump(cache_data, cache_file) + update_status(f"✅ Cached to {cache_file.name}", 75) + except Exception as e: + update_status(f"⚠️ Cache save failed: {str(e)}", 75) # Encode labels label_encoder = LabelEncoder() @@ -220,33 +366,115 @@ def train_model( features, labels_encoded, test_size=0.2, random_state=42, stratify=labels_encoded ) - # Train XGBoost model - update_status("Training XGBoost model on GPU...", 75) + # Train model based on selected type + update_status(f"Training {model_type.upper()} model...", 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 - ) + if model_type == 'xgboost': + model = XGBClassifier( + n_estimators=n_estimators, + max_depth=max_depth, + learning_rate=learning_rate, + device=device if use_gpu else 'cpu', + tree_method='hist', + random_state=42, + eval_metric='mlogloss', + verbosity=0 + ) + elif model_type == 'random_forest': + model = RandomForestClassifier( + n_estimators=n_estimators, + max_depth=max_depth, + random_state=42, + n_jobs=-1, # Use all cores + verbose=0 + ) + elif model_type == 'decision_tree': + model = DecisionTreeClassifier( + max_depth=max_depth, + random_state=42 + ) + elif model_type == 'svm': + model = SVC( + kernel='rbf', + random_state=42, + verbose=False + ) + elif model_type == 'cnn': + if not PYTORCH_AVAILABLE: + raise ImportError("PyTorch is required for CNN. Install: pip install torch") + + # CNN requires reshaping data + n_features = X_train.shape[1] + n_classes = len(np.unique(y_train)) + + # Build PyTorch CNN model + device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu') + update_status(f"Building CNN model on {device}...", 75) + + model = CNNClassifier(n_features, n_classes).to(device) + + # Convert to PyTorch tensors + X_train_tensor = torch.FloatTensor(X_train).unsqueeze(1) # Add channel dim: (N, 1, features) + y_train_tensor = torch.LongTensor(y_train) + X_test_tensor = torch.FloatTensor(X_test).unsqueeze(1) + y_test_tensor = torch.LongTensor(y_test) + + # Create data loaders + train_dataset = TensorDataset(X_train_tensor, y_train_tensor) + train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) + + # Loss and optimizer + criterion = nn.CrossEntropyLoss() + optimizer = optim.Adam(model.parameters(), lr=0.001) + + # Train CNN + update_status("Training CNN model with PyTorch...", 80) + epochs = min(50, n_estimators // 2) # Use n_estimators as epochs + + model.train() + for epoch in range(epochs): + epoch_loss = 0.0 + for batch_X, batch_y in train_loader: + batch_X, batch_y = batch_X.to(device), batch_y.to(device) + + optimizer.zero_grad() + outputs = model(batch_X) + loss = criterion(outputs, batch_y) + loss.backward() + optimizer.step() + + epoch_loss += loss.item() + + if (epoch + 1) % 10 == 0: + avg_loss = epoch_loss / len(train_loader) + update_status(f"CNN Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}", 80 + (epoch / epochs) * 10) + + # Move model to CPU for saving (compatible with non-GPU systems) + model = model.cpu() + model.device_used = str(device) + else: + raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn") - xgb_model.fit(X_train, y_train) + # Fit non-CNN models + if model_type != 'cnn': + 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) + if model_type == 'cnn': + # PyTorch CNN evaluation + train_score = model.score(X_train, y_train) + test_score = model.score(X_test, y_test) + else: + train_score = model.score(X_train, y_train) + test_score = 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) + joblib.dump({'model': model, 'label_encoder': label_encoder}, output_model_path) # Save model info info = { @@ -258,12 +486,14 @@ def train_model( "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, + "model_type": model_type, + "device": device if model_type == 'xgboost' else 'cpu', + "n_estimators": n_estimators if model_type in ['xgboost', 'random_forest', 'cnn'] else None, + "max_depth": max_depth if model_type != 'cnn' else None, + "learning_rate": learning_rate if model_type == 'xgboost' else None, + "cnn_epochs": min(50, n_estimators // 2) if model_type == 'cnn' else None, + "n_features": X_train.shape[1], + "n_classes": len(np.unique(y_train)), "bbox": bbox, "time_range": time_range, "resolution": resolution diff --git a/training_interface.html b/training_interface.html index 3899c0c..89332e9 100644 --- a/training_interface.html +++ b/training_interface.html @@ -258,6 +258,29 @@ margin: 5px 0; color: #666; } + + /* Animations for notifications */ + @keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(400px); + opacity: 0; + } + } @@ -369,7 +392,18 @@ -

🛰️ Dữ Liệu Vệ Tinh

+

� Dataset Cache Preset

+
+ + +
+ 💡 Chọn dataset cache có sẵn để tự động điền các thông số tương ứng +
+
+ +

�🛰️ Dữ Liệu Vệ Tinh

@@ -390,8 +424,27 @@

🤖 Model Parameters

+ + +
+ + +
+ + ✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh + +
+
+ +
-
+
@@ -399,11 +452,11 @@
-
+
-
+
+ 💾 Sử dụng Cache Dataset + +
+ ✅ Khuyến nghị: Bật để test nhanh hơn. Lần đầu load dữ liệu sẽ chậm, nhưng các lần sau rất nhanh (không cần download lại từ satellite).
+ 📊 Đang kiểm tra cache... +
+
+ + +
+
+
- -
+ +
+
+ + +
+

📂 Các File Dự Đoán Đã Tạo

+
+

Đang tải...

@@ -631,10 +705,12 @@ max_scenes: parseInt(document.getElementById('maxScenes').value), cloud_cover: parseInt(document.getElementById('cloudCover').value), resolution: parseInt(document.getElementById('resolution').value), + model_type: document.getElementById('modelType').value, n_estimators: parseInt(document.getElementById('nEstimators').value), max_depth: parseInt(document.getElementById('maxDepth').value), learning_rate: parseFloat(document.getElementById('learningRate').value), - use_gpu: document.getElementById('useGpu').value === 'true' + use_gpu: document.getElementById('useGpu').value === 'true', + use_cache: document.getElementById('useCache').checked }; try { @@ -994,10 +1070,209 @@ } } + // Model type change handler + function updateModelTypeUI() { + const modelType = document.getElementById('modelType').value; + const desc = document.getElementById('modelTypeDesc'); + const nEstimatorsGroup = document.getElementById('nEstimatorsGroup'); + const learningRateGroup = document.getElementById('learningRateGroup'); + const useGpuGroup = document.getElementById('useGpuGroup'); + + const descriptions = { + 'xgboost': '✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh', + 'random_forest': '✓ Random Forest: Ổn định, không overfitting, phù hợp mọi kích thước dữ liệu', + 'decision_tree': '✓ Decision Tree: Đơn giản nhất, nhanh nhất, dễ hiểu, phù hợp để test nhanh', + 'svm': '✓ SVM: Chính xác cao với dữ liệu nhỏ, chậm với dữ liệu lớn', + 'cnn': '✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch' + }; + + desc.textContent = descriptions[modelType]; + + // Show/hide parameters based on model type + if (modelType === 'xgboost') { + nEstimatorsGroup.style.display = ''; + learningRateGroup.style.display = ''; + useGpuGroup.style.display = ''; + } else if (modelType === 'random_forest') { + nEstimatorsGroup.style.display = ''; + learningRateGroup.style.display = 'none'; + useGpuGroup.style.display = 'none'; + } else if (modelType === 'decision_tree') { + nEstimatorsGroup.style.display = 'none'; + learningRateGroup.style.display = 'none'; + useGpuGroup.style.display = 'none'; + } else if (modelType === 'svm') { + nEstimatorsGroup.style.display = 'none'; + learningRateGroup.style.display = 'none'; + useGpuGroup.style.display = 'none'; + } else if (modelType === 'cnn') { + // CNN uses n_estimators as epochs and supports GPU + nEstimatorsGroup.style.display = ''; + document.querySelector('#nEstimatorsGroup label').textContent = 'Epochs (số lần training):'; + document.getElementById('nEstimators').value = 50; + learningRateGroup.style.display = 'none'; + useGpuGroup.style.display = ''; // Show GPU option for CNN + } + + // Reset n_estimators label for non-CNN + if (modelType !== 'cnn' && modelType !== 'decision_tree' && modelType !== 'svm') { + document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:'; + } + } + + // ============== CACHE MANAGEMENT ============== + + let cacheFiles = []; // Store cache files data + + // Load cache info and populate preset dropdown + async function loadCacheInfo() { + try { + const response = await fetch(`${API_BASE}/cache/info`); + const data = await response.json(); + + // Update cache info display + const cacheInfoSpan = document.getElementById('cacheInfo'); + if (data.exists && data.count > 0) { + cacheInfoSpan.innerHTML = `✅ Có ${data.count} file cache (${data.total_size_mb} MB)`; + cacheInfoSpan.style.color = '#28a745'; + } else { + cacheInfoSpan.innerHTML = '❌ Chưa có cache nào'; + cacheInfoSpan.style.color = '#6c757d'; + } + + // Populate cache preset dropdown + const cachePreset = document.getElementById('cachePreset'); + cachePreset.innerHTML = ''; + + if (data.exists && data.files && data.files.length > 0) { + cacheFiles = data.files; + data.files.forEach((file, index) => { + const meta = file.metadata; + const option = document.createElement('option'); + option.value = index; + + // Format display text with metadata + let displayText = `Cache #${index + 1}`; + if (meta && meta.n_samples) { + displayText += ` (${meta.n_samples} mẫu`; + if (meta.start_date && meta.end_date) { + displayText += `, ${meta.start_date} → ${meta.end_date}`; + } + if (meta.resolution) { + displayText += `, ${meta.resolution}m`; + } + displayText += ')'; + } else { + displayText += ` (${file.size_mb} MB, ${new Date(file.modified).toLocaleString('vi-VN')})`; + } + + option.textContent = displayText; + cachePreset.appendChild(option); + }); + } + } catch (error) { + console.error('Error loading cache info:', error); + document.getElementById('cacheInfo').innerHTML = '⚠️ Lỗi kiểm tra cache'; + } + } + + // Apply cache preset to form inputs + function applyCachePreset() { + const selectIndex = document.getElementById('cachePreset').value; + + if (selectIndex === '') { + // Clear preset - no auto-fill + return; + } + + const fileData = cacheFiles[parseInt(selectIndex)]; + if (!fileData || !fileData.metadata) { + alert('Không có metadata cho cache này'); + return; + } + + const meta = fileData.metadata; + + // Fill bbox inputs + if (meta.min_lon !== undefined) { + document.getElementById('minLon').value = meta.min_lon; + document.getElementById('maxLon').value = meta.max_lon; + document.getElementById('minLat').value = meta.min_lat; + document.getElementById('maxLat').value = meta.max_lat; + + // Update map rectangle + if (rectangle) { + drawnItems.removeLayer(rectangle); + } + const bounds = L.latLngBounds( + [meta.min_lat, meta.min_lon], + [meta.max_lat, meta.max_lon] + ); + rectangle = L.rectangle(bounds, {color: '#3388ff', weight: 3, fillOpacity: 0.2}); + drawnItems.addLayer(rectangle); + map.fitBounds(bounds); + } + + // Fill time range inputs + if (meta.start_date) { + document.getElementById('startDate').value = meta.start_date; + } + if (meta.end_date) { + document.getElementById('endDate').value = meta.end_date; + } + + // Fill resolution if available + if (meta.resolution) { + document.getElementById('resolution').value = meta.resolution; + } + + // Note to user + const presetNote = `📌 Đã áp dụng cache preset: ${meta.n_samples || '?'} mẫu, ${meta.start_date || '?'} → ${meta.end_date || '?'}`; + console.log(presetNote); + + // Show notification + const notification = document.createElement('div'); + notification.style.cssText = 'position:fixed;top:20px;right:20px;background:#28a745;color:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 6px rgba(0,0,0,0.1);z-index:10000;animation:slideIn 0.3s ease-out;'; + notification.innerHTML = `✅ Cache Preset Applied
${presetNote}`; + document.body.appendChild(notification); + + setTimeout(() => { + notification.style.animation = 'slideOut 0.3s ease-out'; + setTimeout(() => notification.remove(), 300); + }, 3000); + } + + // Clear cache + async function clearCache() { + if (!confirm('Bạn có chắc muốn xóa toàn bộ cache?\n\nCache giúp test nhanh hơn bằng cách lưu lại dữ liệu đã download.')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/cache/clear`, { + method: 'POST' + }); + const result = await response.json(); + alert(result.message); + loadCacheInfo(); // Refresh info + } catch (error) { + alert('Lỗi xóa cache: ' + error.message); + } + } + // Initialize map when page loads document.addEventListener('DOMContentLoaded', function() { initMap(); initPredictionMap(); + loadPredictionsList(); // Load predictions list on page load + loadCacheInfo(); // Load cache info + + // Add model type change listener + document.getElementById('modelType').addEventListener('change', updateModelTypeUI); + updateModelTypeUI(); // Initial update + + // Add cache preset change listener + document.getElementById('cachePreset').addEventListener('change', applyCachePreset); }); // ============== PREDICTION FUNCTIONALITY ============== @@ -1207,6 +1482,10 @@ // 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 = `
📁 File kết quả:
@@ -1220,19 +1499,76 @@
`; + // Add download link + const downloadContainer = document.getElementById('downloadLinkContainer'); + downloadContainer.innerHTML = ` + + 📥 Tải GeoTIFF + + +
+ Hoặc copy link: ${downloadUrl} +
+ `; + resultDiv.style.display = 'block'; + + // Refresh predictions list + loadPredictionsList(); } // 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.'); + 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 => ` +
+
+ 📄 ${pred.filename} +
+ 📅 ${new Date(pred.created).toLocaleString('vi-VN')} | 💾 ${pred.size_mb} MB +
+
+ + 📥 Tải về + +
+ `).join(''); + } else { + listDiv.innerHTML = '

Chưa có file dự đoán nào.

'; + } + } catch (error) { + console.error('Error loading predictions list:', error); + document.getElementById('predictionsList').innerHTML = + '

Lỗi tải danh sách: ' + error.message + '

'; + } + } + // View prediction result details function viewPredictionResult() { if (window.lastPredictionResult) {