492 lines
16 KiB
Python
492 lines
16 KiB
Python
"""
|
|
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("""
|
|
<html>
|
|
<head><title>Training Interface</title></head>
|
|
<body>
|
|
<h1>Land Classification Training API</h1>
|
|
<p>API Documentation: <a href="/docs">/docs</a></p>
|
|
<p>Training Interface: Tạo file training_interface.html</p>
|
|
</body>
|
|
</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")
|