From d1a453465344a64022464a7c6e1a398c9a7cb4af Mon Sep 17 00:00:00 2001 From: Victor Phan Date: Sat, 13 Dec 2025 14:20:57 +0700 Subject: [PATCH] =?UTF-8?q?ho=C3=A0n=20th=C3=A0nh=20server=20api=20d?= =?UTF-8?q?=E1=BB=B1=20=C4=91o=C3=A1n=20ra=20file=20tiff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + __pycache__/api_server.cpython-310.pyc | Bin 0 -> 11817 bytes __pycache__/api_server.cpython-313.pyc | Bin 0 -> 10502 bytes __pycache__/new_import_ODC.cpython-310.pyc | Bin 0 -> 12350 bytes __pycache__/train_module.cpython-310.pyc | Bin 0 -> 7203 bytes api_server.py | 251 +++++- .../model_xgboost_gpu_20251212_125754.joblib | 3 + ...odel_xgboost_gpu_20251212_125754_info.json | 24 + .../model_xgboost_gpu_20251212_220850.joblib | 3 + ...odel_xgboost_gpu_20251212_220850_info.json | 31 + .../model_xgboost_gpu_20251212_223236.joblib | 3 + ...odel_xgboost_gpu_20251212_223236_info.json | 31 + predictions/prediction_20251212_230423.tif | 3 + predictions/prediction_20251212_230549.tif | 3 + start.sh | 1 + train_module.py | 302 +++++++ training_interface.html | 762 +++++++++++++++++- 17 files changed, 1399 insertions(+), 25 deletions(-) create mode 100644 .gitignore create mode 100644 __pycache__/api_server.cpython-310.pyc create mode 100644 __pycache__/api_server.cpython-313.pyc create mode 100644 __pycache__/new_import_ODC.cpython-310.pyc create mode 100644 __pycache__/train_module.cpython-310.pyc create mode 100644 model_train/model_xgboost_gpu_20251212_125754.joblib create mode 100644 model_train/model_xgboost_gpu_20251212_125754_info.json create mode 100644 model_train/model_xgboost_gpu_20251212_220850.joblib create mode 100644 model_train/model_xgboost_gpu_20251212_220850_info.json create mode 100644 model_train/model_xgboost_gpu_20251212_223236.joblib create mode 100644 model_train/model_xgboost_gpu_20251212_223236_info.json create mode 100644 predictions/prediction_20251212_230423.tif create mode 100644 predictions/prediction_20251212_230549.tif create mode 100755 start.sh create mode 100644 train_module.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22e5749 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/__pycache__/api_server.cpython-310.pyc b/__pycache__/api_server.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6efe027e6c6f2d457565731b1489d1fd21759de GIT binary patch literal 11817 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 literal 0 HcmV?d00001 diff --git a/__pycache__/api_server.cpython-313.pyc b/__pycache__/api_server.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..773cc43fcf94c7900768e552da326e83bffd9ab3 GIT binary patch literal 10502 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 literal 0 HcmV?d00001 diff --git a/__pycache__/new_import_ODC.cpython-310.pyc b/__pycache__/new_import_ODC.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d968ea4b679119e8090d6c88314ee397aa251384 GIT binary patch literal 12350 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 literal 0 HcmV?d00001 diff --git a/api_server.py b/api_server.py index 44e5dd2..e2f670d 100644 --- a/api_server.py +++ b/api_server.py @@ -34,6 +34,18 @@ training_status = { "error": None, "result": 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 } @@ -65,6 +77,27 @@ class TrainingConfig(BaseModel): 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): """Trạng thái training""" is_training: bool @@ -161,10 +194,15 @@ async def start_training(config: TrainingConfig, background_tasks: BackgroundTas async def stop_training(): """Dừng training (nếu đang chạy)""" global training_status - training_status["is_training"] = False - training_status["error"] = "Đã dừng bởi người dùng" - training_status["end_time"] = datetime.now().isoformat() - return {"message": "Training đã dừng"} + + if not training_status["is_training"]: + return {"message": "Không có training nào đang chạy"} + + # 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") @@ -194,11 +232,43 @@ async def list_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): """Chạy training process""" global training_status try: + training_status["cancel_requested"] = False training_status["progress"] = "Đang import thư viện..." # Import training module @@ -206,6 +276,10 @@ async def run_training(config: TrainingConfig): 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 result = train_model( 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, learning_rate=config.learning_rate, 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 - training_status["progress"] = "Hoàn thành!" - training_status["result"] = result + if training_status.get("cancel_requested", False): + training_status["is_training"] = False + 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() except Exception as e: @@ -242,6 +322,161 @@ def update_progress(message: str): 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__": print("=" * 70) print("🚀 LAND CLASSIFICATION TRAINING API SERVER") diff --git a/model_train/model_xgboost_gpu_20251212_125754.joblib b/model_train/model_xgboost_gpu_20251212_125754.joblib new file mode 100644 index 0000000..3c862ad --- /dev/null +++ b/model_train/model_xgboost_gpu_20251212_125754.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fe99f96ad3d3ba7aaacc0e742572a8f5b22947a328c74b245e0aa5f2913c757 +size 1347520 diff --git a/model_train/model_xgboost_gpu_20251212_125754_info.json b/model_train/model_xgboost_gpu_20251212_125754_info.json new file mode 100644 index 0000000..3ea4e20 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251212_125754_info.json @@ -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 +} \ No newline at end of file diff --git a/model_train/model_xgboost_gpu_20251212_220850.joblib b/model_train/model_xgboost_gpu_20251212_220850.joblib new file mode 100644 index 0000000..a8bca94 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251212_220850.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_20251212_220850_info.json b/model_train/model_xgboost_gpu_20251212_220850_info.json new file mode 100644 index 0000000..8628464 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251212_220850_info.json @@ -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 +} \ No newline at end of file diff --git a/model_train/model_xgboost_gpu_20251212_223236.joblib b/model_train/model_xgboost_gpu_20251212_223236.joblib new file mode 100644 index 0000000..590a376 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251212_223236.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9c6cabb59d9cdbba22a438ae935911f1711d7434dcf3728db4e518ec1b90190 +size 556184 diff --git a/model_train/model_xgboost_gpu_20251212_223236_info.json b/model_train/model_xgboost_gpu_20251212_223236_info.json new file mode 100644 index 0000000..4d85ac9 --- /dev/null +++ b/model_train/model_xgboost_gpu_20251212_223236_info.json @@ -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 +} \ No newline at end of file diff --git a/predictions/prediction_20251212_230423.tif b/predictions/prediction_20251212_230423.tif new file mode 100644 index 0000000..84cafa6 --- /dev/null +++ b/predictions/prediction_20251212_230423.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687 +size 73272920 diff --git a/predictions/prediction_20251212_230549.tif b/predictions/prediction_20251212_230549.tif new file mode 100644 index 0000000..84cafa6 --- /dev/null +++ b/predictions/prediction_20251212_230549.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687 +size 73272920 diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..a40958f --- /dev/null +++ b/start.sh @@ -0,0 +1 @@ +uvicorn api_server:app --reload --host 0.0.0.0 --port 8000 diff --git a/train_module.py b/train_module.py new file mode 100644 index 0000000..e4b58e7 --- /dev/null +++ b/train_module.py @@ -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) + } diff --git a/training_interface.html b/training_interface.html index 1040287..3899c0c 100644 --- a/training_interface.html +++ b/training_interface.html @@ -4,6 +4,11 @@ Land Classification Training Interface + + + + +