""" API Server for Land Classification Model Training Cho phép chọn dữ liệu và cấu hình training qua giao diện web """ from fastapi import FastAPI, BackgroundTasks, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, FileResponse from pydantic import BaseModel from typing import Optional, List import uvicorn import joblib import json from datetime import datetime from pathlib import Path import sys app = FastAPI(title="Land Classification Training API", version="1.0.0") # Enable CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Global training status training_status = { "is_training": False, "progress": "", "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 } class TrainingConfig(BaseModel): """Cấu hình training""" # 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 # 10m hoặc 20m # Model parameters n_estimators: int = 100 max_depth: int = 20 learning_rate: float = 0.1 use_gpu: bool = True # Training data 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 progress: str error: Optional[str] result: Optional[dict] start_time: Optional[str] end_time: Optional[str] @app.get("/", response_class=HTMLResponse) async def root(): """Serve giao diện web""" html_file = Path(__file__).parent / "training_interface.html" if html_file.exists(): return FileResponse(html_file) else: return HTMLResponse(""" Training Interface

Land Classification Training API

API Documentation: /docs

Training Interface: Tạo file training_interface.html

""") @app.get("/api/config/presets") async def get_presets(): """Lấy các preset cấu hình sẵn""" return { "presets": [ { "name": "PC - Nhỏ (3 tháng, 20m, 12 scenes)", "config": { "min_lon": 105.6, "min_lat": 9.3, "max_lon": 106.2, "max_lat": 9.8, "start_date": "2023-03-01", "end_date": "2023-05-31", "max_scenes": 12, "cloud_cover": 30, "resolution": 20 } }, { "name": "Server - Trung bình (6 tháng, 10m, 30 scenes)", "config": { "min_lon": 105.5, "min_lat": 9.2, "max_lon": 106.4, "max_lat": 10.0, "start_date": "2023-01-01", "end_date": "2023-06-30", "max_scenes": 30, "cloud_cover": 30, "resolution": 10 } }, { "name": "Full - Lớn (1 năm, 10m, 60 scenes)", "config": { "min_lon": 105.5, "min_lat": 9.2, "max_lon": 106.4, "max_lat": 10.0, "start_date": "2022-09-01", "end_date": "2023-10-01", "max_scenes": 60, "cloud_cover": 50, "resolution": 10 } } ] } @app.get("/api/training/status", response_model=TrainingStatus) async def get_training_status(): """Kiểm tra trạng thái training""" return training_status @app.post("/api/training/start") async def start_training(config: TrainingConfig, background_tasks: BackgroundTasks): """Bắt đầu training với config đã chọn""" global training_status if training_status["is_training"]: raise HTTPException(status_code=400, detail="Training đang chạy, vui lòng đợi") # Reset status training_status = { "is_training": True, "progress": "Đang khởi tạo...", "error": None, "result": None, "start_time": datetime.now().isoformat(), "end_time": None } # Run training in background background_tasks.add_task(run_training, config) return {"message": "Training đã bắt đầu", "status": training_status} @app.post("/api/training/stop") async def stop_training(): """Dừng training (nếu đang chạy)""" global training_status 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") async def list_models(): """Liệt kê các model đã train""" model_dir = Path("model_train") if not model_dir.exists(): return {"models": []} models = [] for model_file in model_dir.glob("*.joblib"): info_file = model_file.with_suffix('.json') info = {} if info_file.exists(): with open(info_file) as f: info = json.load(f) models.append({ "filename": model_file.name, "created": datetime.fromtimestamp(model_file.stat().st_mtime).isoformat(), "size_mb": round(model_file.stat().st_size / 1024 / 1024, 2), "info": info }) # Sort by creation time (newest first) models.sort(key=lambda x: x["created"], reverse=True) 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 from train_module import train_model 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], time_range=f"{config.start_date}/{config.end_date}", max_scenes=config.max_scenes, cloud_cover=config.cloud_cover, resolution=config.resolution, training_shapefile=config.training_shapefile, n_estimators=config.n_estimators, max_depth=config.max_depth, learning_rate=config.learning_rate, use_gpu=config.use_gpu, status_callback=lambda msg: update_progress(msg), cancel_check=should_cancel ) 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: training_status["is_training"] = False training_status["error"] = str(e) training_status["progress"] = f"Lỗi: {str(e)}" training_status["end_time"] = datetime.now().isoformat() import traceback print(traceback.format_exc()) def update_progress(message: str): """Cập nhật progress message""" global training_status training_status["progress"] = message print(f"[PROGRESS] {message}") def update_prediction_progress(message: str): """Cập nhật prediction progress message""" global prediction_status prediction_status["progress"] = message print(f"[PREDICTION PROGRESS] {message}") async def run_prediction(config: PredictionConfig): """Chạy prediction process""" global prediction_status try: prediction_status["progress"] = "Đang import thư viện..." # Import required libraries import xarray as xr import numpy as np from datetime import datetime as dt import rioxarray prediction_status["progress"] = "Đang load model..." # Load model model_path = Path("model_train") / config.model_filename if not model_path.exists(): raise FileNotFoundError(f"Model không tồn tại: {config.model_filename}") model_data = joblib.load(model_path) # Extract model from dict (models are saved as {'model': xgb_model, 'label_encoder': encoder}) if isinstance(model_data, dict): model = model_data.get('model') label_encoder = model_data.get('label_encoder') else: model = model_data label_encoder = None prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..." # Import and use Microsoft Planetary Computer STAC API import pystac_client import planetary_computer catalog = pystac_client.Client.open( "https://planetarycomputer.microsoft.com/api/stac/v1", modifier=planetary_computer.sign_inplace, ) bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat] time_range = f"{config.start_date}/{config.end_date}" prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..." # Search Sentinel-2 data search = catalog.search( collections=["sentinel-2-l2a"], bbox=bbox, datetime=time_range, query={"eo:cloud_cover": {"lt": config.cloud_cover}} ) items = list(search.items()) # Changed from items_as_dicts() to items() if not items: raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này") items = items[:config.max_scenes] prediction_status["progress"] = f"Đang xử lý {len(items)} scenes Sentinel-2..." # Load and process Sentinel-2 data (simplified) # Note: This is a simplified version. Full implementation would need more processing from odc.stac import load s2_data = load( items, bbox=bbox, chunks={"time": 1, "x": 2048, "y": 2048}, groupby="solar_day", resolution=config.resolution ) prediction_status["progress"] = "Đang tính toán các chỉ số..." # Calculate NDVI using Sentinel-2 band names # B08 = NIR, B04 = Red nir = s2_data["B08"] # NIR band red = s2_data["B04"] # Red band ndvi = (nir - red) / (nir + red + 1e-8) # Add small value to avoid division by zero # Resample to monthly ndvi_monthly = ndvi.resample(time="1M").mean() prediction_status["progress"] = "Đang dự đoán..." # Prepare features for prediction features_list = [] for t in range(len(ndvi_monthly.time)): ndvi_t = ndvi_monthly.isel(time=t).values features_list.append(ndvi_t.flatten()) # Stack features features = np.column_stack(features_list) # Make prediction predictions = model.predict(features) # Reshape to original shape pred_shape = ndvi_monthly.isel(time=0).shape predictions_2d = predictions.reshape(pred_shape) # Create output xarray prediction_da = xr.DataArray( predictions_2d, coords={ "y": ndvi_monthly.y, "x": ndvi_monthly.x }, dims=["y", "x"], name="classification" ) # Save output output_dir = Path("predictions") output_dir.mkdir(exist_ok=True) timestamp = dt.now().strftime("%Y%m%d_%H%M%S") output_file = output_dir / f"prediction_{timestamp}.tif" prediction_status["progress"] = "Đang lưu kết quả..." # Save as GeoTIFF prediction_da.rio.write_crs(s2_data.rio.crs, inplace=True) prediction_da.rio.to_raster(output_file, driver="GTiff") prediction_status["is_predicting"] = False prediction_status["progress"] = "Hoàn thành!" prediction_status["output_file"] = str(output_file) prediction_status["result"] = { "output_file": str(output_file), "shape": pred_shape, "unique_classes": np.unique(predictions).tolist(), "bbox": bbox, "time_range": time_range } prediction_status["end_time"] = dt.now().isoformat() except Exception as e: prediction_status["is_predicting"] = False prediction_status["error"] = str(e) prediction_status["progress"] = f"Lỗi: {str(e)}" prediction_status["end_time"] = dt.now().isoformat() import traceback print(traceback.format_exc()) if __name__ == "__main__": print("=" * 70) print("🚀 LAND CLASSIFICATION TRAINING API SERVER") print("=" * 70) print("\n📍 Endpoints:") print(" - Web Interface: http://localhost:8000") print(" - API Docs: http://localhost:8000/docs") print(" - Start Training: POST http://localhost:8000/api/training/start") print(" - Check Status: GET http://localhost:8000/api/training/status") print("\n" + "=" * 70) uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")