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 0000000..c6efe02 Binary files /dev/null and b/__pycache__/api_server.cpython-310.pyc differ diff --git a/__pycache__/api_server.cpython-313.pyc b/__pycache__/api_server.cpython-313.pyc new file mode 100644 index 0000000..773cc43 Binary files /dev/null and b/__pycache__/api_server.cpython-313.pyc differ diff --git a/__pycache__/new_import_ODC.cpython-310.pyc b/__pycache__/new_import_ODC.cpython-310.pyc new file mode 100644 index 0000000..d968ea4 Binary files /dev/null and b/__pycache__/new_import_ODC.cpython-310.pyc differ diff --git a/__pycache__/train_module.cpython-310.pyc b/__pycache__/train_module.cpython-310.pyc new file mode 100644 index 0000000..cfb31ff Binary files /dev/null and b/__pycache__/train_module.cpython-310.pyc differ 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 + + + + +