cơ bản hoàn tát các chức năng chính
This commit is contained in:
+603
-83
@@ -52,6 +52,10 @@ prediction_status = {
|
||||
"end_time": None
|
||||
}
|
||||
|
||||
# Batch prediction queue
|
||||
batch_queue = []
|
||||
batch_results = []
|
||||
|
||||
|
||||
class TrainingConfig(BaseModel):
|
||||
"""Cấu hình training"""
|
||||
@@ -120,23 +124,55 @@ class TrainingStatus(BaseModel):
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
"""Serve giao diện web"""
|
||||
html_file = Path(__file__).parent / "training_interface.html"
|
||||
"""Serve main index page with tabs"""
|
||||
html_file = Path(__file__).parent / "index.html"
|
||||
if html_file.exists():
|
||||
return FileResponse(html_file)
|
||||
else:
|
||||
return HTMLResponse("""
|
||||
<html>
|
||||
<head><title>Training Interface</title></head>
|
||||
<head><title>Land Classification System</title></head>
|
||||
<body>
|
||||
<h1>Land Classification Training API</h1>
|
||||
<h1>Land Classification System</h1>
|
||||
<p>API Documentation: <a href="/docs">/docs</a></p>
|
||||
<p>Training Interface: Tạo file training_interface.html</p>
|
||||
<p>Training: <a href="/training">/training</a></p>
|
||||
<p>Prediction: <a href="/prediction">/prediction</a></p>
|
||||
<p>Dashboard: <a href="/dashboard">/dashboard</a></p>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
|
||||
|
||||
@app.get("/training", response_class=HTMLResponse)
|
||||
async def training_page():
|
||||
"""Serve training interface"""
|
||||
html_file = Path(__file__).parent / "training_interface.html"
|
||||
if html_file.exists():
|
||||
return FileResponse(html_file)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Training interface không tồn tại")
|
||||
|
||||
|
||||
@app.get("/prediction", response_class=HTMLResponse)
|
||||
async def prediction_page():
|
||||
"""Serve prediction interface"""
|
||||
html_file = Path(__file__).parent / "prediction_interface.html"
|
||||
if html_file.exists():
|
||||
return FileResponse(html_file)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Prediction interface không tồn tại")
|
||||
|
||||
|
||||
@app.get("/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard():
|
||||
"""Serve dashboard visualization"""
|
||||
html_file = Path(__file__).parent / "dashboard.html"
|
||||
if html_file.exists():
|
||||
return FileResponse(html_file)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Dashboard không tồn tại")
|
||||
|
||||
|
||||
@app.get("/api/config/presets")
|
||||
async def get_presets():
|
||||
"""Lấy các preset cấu hình sẵn"""
|
||||
@@ -308,22 +344,37 @@ async def list_models():
|
||||
model_dir = Path("model_train")
|
||||
if not model_dir.exists():
|
||||
return {"models": []}
|
||||
|
||||
|
||||
models = []
|
||||
# List all .joblib model files (actual trained models)
|
||||
for model_file in model_dir.glob("*.joblib"):
|
||||
info_file = model_file.with_suffix('.json')
|
||||
# Skip any file that contains '_info' in its name
|
||||
if '_info' in model_file.stem:
|
||||
continue
|
||||
|
||||
info = {}
|
||||
# Try to find corresponding .json info file
|
||||
# Remove .joblib and try with _info.json
|
||||
base_name = model_file.stem # e.g., "model_cnn_20251221_163841"
|
||||
info_file = model_dir / f"{base_name}_info.json"
|
||||
|
||||
if info_file.exists():
|
||||
with open(info_file) as f:
|
||||
info = json.load(f)
|
||||
try:
|
||||
with open(info_file) as f:
|
||||
info = json.load(f)
|
||||
except Exception as e:
|
||||
info = {"error": str(e)}
|
||||
|
||||
size_mb = round(model_file.stat().st_size / 1024 / 1024, 2)
|
||||
created = datetime.fromtimestamp(model_file.stat().st_mtime).isoformat()
|
||||
|
||||
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),
|
||||
"created": created,
|
||||
"size_mb": size_mb,
|
||||
"info": info
|
||||
})
|
||||
|
||||
|
||||
# Sort by creation time (newest first)
|
||||
models.sort(key=lambda x: x["created"], reverse=True)
|
||||
return {"models": models}
|
||||
@@ -550,6 +601,12 @@ async def run_prediction(config: PredictionConfig):
|
||||
import rioxarray
|
||||
import dask.array as da
|
||||
|
||||
# Validate bbox
|
||||
if (config.min_lon < -180 or config.max_lon > 180 or
|
||||
config.min_lat < -90 or config.max_lat > 90):
|
||||
raise ValueError(f"Bbox không hợp lệ: ({config.min_lon}, {config.min_lat}, {config.max_lon}, {config.max_lat}). "
|
||||
f"Phải trong phạm vi (-180, -90, 180, 90)")
|
||||
|
||||
prediction_status["progress"] = "Đang load model..."
|
||||
|
||||
# Load model
|
||||
@@ -577,47 +634,96 @@ async def run_prediction(config: PredictionConfig):
|
||||
except ImportError:
|
||||
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
|
||||
|
||||
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
|
||||
from odc.stac import load
|
||||
|
||||
catalog = pystac_client.Client.open(
|
||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||
modifier=planetary_computer.sign_inplace,
|
||||
)
|
||||
prediction_status["progress"] = "Đang kiểm tra cache dữ liệu đầu vào..."
|
||||
import hashlib, os
|
||||
cache_dir = Path("dataset_cache")
|
||||
cache_dir.mkdir(exist_ok=True)
|
||||
# Tạo cache key từ bbox, time_range, max_scenes, cloud_cover, resolution
|
||||
cache_key = f"pred_{config.min_lon}_{config.min_lat}_{config.max_lon}_{config.max_lat}_{config.start_date}_{config.end_date}_{config.max_scenes}_{config.cloud_cover}_{config.resolution}"
|
||||
cache_hash = hashlib.md5(cache_key.encode()).hexdigest()
|
||||
cache_file = cache_dir / f"prediction_input_{cache_hash}.joblib"
|
||||
|
||||
# Initialize common variables
|
||||
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
|
||||
time_range = f"{config.start_date}/{config.end_date}"
|
||||
|
||||
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
||||
|
||||
# Search Sentinel-2 data
|
||||
s2_search = catalog.search(
|
||||
collections=["sentinel-2-l2a"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||
)
|
||||
|
||||
s2_items = list(s2_search.items())
|
||||
if not s2_items:
|
||||
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
||||
|
||||
s2_items = s2_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
||||
|
||||
# Load Sentinel-2 data
|
||||
s2_data = load(
|
||||
s2_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="solar_day",
|
||||
resolution=config.resolution
|
||||
)
|
||||
|
||||
if cache_file.exists():
|
||||
prediction_status["progress"] = "Đang load dữ liệu từ cache..."
|
||||
cached = joblib.load(cache_file)
|
||||
s2_data = cached["s2_data"]
|
||||
s2_items = cached["s2_items"]
|
||||
vh_monthly = cached.get("vh_monthly")
|
||||
vv_monthly = cached.get("vv_monthly")
|
||||
use_radar = cached.get("use_radar", False)
|
||||
else:
|
||||
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||
import pystac_client
|
||||
import planetary_computer
|
||||
from odc.stac import load
|
||||
catalog = pystac_client.Client.open(
|
||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||
modifier=planetary_computer.sign_inplace,
|
||||
)
|
||||
# ============ BƯỚC 1: TẢI DỮ LIỆU SENTINEL-2 ============
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
||||
s2_search = catalog.search(
|
||||
collections=["sentinel-2-l2a"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||
)
|
||||
s2_items = list(s2_search.items())
|
||||
if not s2_items:
|
||||
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
||||
s2_items = s2_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
||||
s2_data = load(
|
||||
s2_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="solar_day",
|
||||
resolution=config.resolution
|
||||
)
|
||||
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (Radar)... ============
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||
s1_search = catalog.search(
|
||||
collections=["sentinel-1-rtc"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
)
|
||||
s1_items = list(s1_search.items())
|
||||
if s1_items:
|
||||
s1_items = s1_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
||||
s1_data = load(
|
||||
s1_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="sat:absolute_orbit",
|
||||
resolution=config.resolution
|
||||
)
|
||||
if "vh" in s1_data and "vv" in s1_data:
|
||||
vh = s1_data["vh"].astype('float32')
|
||||
vv = s1_data["vv"].astype('float32')
|
||||
vh_monthly = vh.resample(time="1ME").mean().compute()
|
||||
vv_monthly = vv.resample(time="1ME").mean().compute()
|
||||
use_radar = True
|
||||
else:
|
||||
vh_monthly = None
|
||||
vv_monthly = None
|
||||
use_radar = False
|
||||
else:
|
||||
vh_monthly = None
|
||||
vv_monthly = None
|
||||
use_radar = False
|
||||
# Lưu cache
|
||||
joblib.dump({
|
||||
"s2_data": s2_data,
|
||||
"s2_items": s2_items,
|
||||
"vh_monthly": vh_monthly,
|
||||
"vv_monthly": vv_monthly,
|
||||
"use_radar": use_radar
|
||||
}, cache_file)
|
||||
|
||||
# ============ BƯỚC 2: TÍNH NDVI VÀ XỬ LÝ MÂY ============
|
||||
prediction_status["progress"] = "Đang tính toán NDVI và xử lý mây..."
|
||||
@@ -649,47 +755,63 @@ async def run_prediction(config: PredictionConfig):
|
||||
ndvi_monthly = ndvi_monthly.compute()
|
||||
|
||||
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (VH, VV) ============
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||
|
||||
# Search Sentinel-1 data
|
||||
s1_search = catalog.search(
|
||||
collections=["sentinel-1-rtc"],
|
||||
bbox=bbox,
|
||||
datetime=time_range,
|
||||
)
|
||||
|
||||
s1_items = list(s1_search.items())
|
||||
|
||||
if s1_items:
|
||||
s1_items = s1_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
||||
# Only load radar if not already in cache
|
||||
if not cache_file.exists() or (cache_file.exists() and not use_radar):
|
||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
|
||||
|
||||
# Load Sentinel-1 data (without like= to avoid conflict with bbox/resolution)
|
||||
s1_data = load(
|
||||
s1_items,
|
||||
# Initialize catalog if not already done
|
||||
if not cache_file.exists():
|
||||
# catalog already initialized in the else block above
|
||||
pass
|
||||
else:
|
||||
# Need to initialize catalog for radar search
|
||||
import pystac_client
|
||||
import planetary_computer
|
||||
from odc.stac import load
|
||||
catalog = pystac_client.Client.open(
|
||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||
modifier=planetary_computer.sign_inplace,
|
||||
)
|
||||
|
||||
# Search Sentinel-1 data
|
||||
s1_search = catalog.search(
|
||||
collections=["sentinel-1-rtc"],
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="sat:absolute_orbit",
|
||||
resolution=config.resolution
|
||||
datetime=time_range,
|
||||
)
|
||||
|
||||
# Extract VH and VV bands
|
||||
if "vh" in s1_data and "vv" in s1_data:
|
||||
vh = s1_data["vh"].astype('float32')
|
||||
vv = s1_data["vv"].astype('float32')
|
||||
s1_items = list(s1_search.items())
|
||||
|
||||
if s1_items:
|
||||
s1_items = s1_items[:config.max_scenes]
|
||||
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
||||
|
||||
# Resample to monthly average
|
||||
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
|
||||
vh_monthly = vh.resample(time="1ME").mean().compute()
|
||||
vv_monthly = vv.resample(time="1ME").mean().compute()
|
||||
# Load Sentinel-1 data (without like= to avoid conflict with bbox/resolution)
|
||||
s1_data = load(
|
||||
s1_items,
|
||||
bbox=bbox,
|
||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||
groupby="sat:absolute_orbit",
|
||||
resolution=config.resolution
|
||||
)
|
||||
|
||||
use_radar = True
|
||||
# Extract VH and VV bands
|
||||
if "vh" in s1_data and "vv" in s1_data:
|
||||
vh = s1_data["vh"].astype('float32')
|
||||
vv = s1_data["vv"].astype('float32')
|
||||
|
||||
# Resample to monthly average
|
||||
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
|
||||
vh_monthly = vh.resample(time="1ME").mean().compute()
|
||||
vv_monthly = vv.resample(time="1ME").mean().compute()
|
||||
|
||||
use_radar = True
|
||||
else:
|
||||
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
|
||||
use_radar = False
|
||||
else:
|
||||
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
|
||||
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
|
||||
use_radar = False
|
||||
else:
|
||||
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
|
||||
use_radar = False
|
||||
|
||||
# ============ BƯỚC 5: CHUẨN BỊ FEATURES CHO DỰ ĐOÁN ============
|
||||
prediction_status["progress"] = "Đang chuẩn bị features cho dự đoán..."
|
||||
@@ -819,6 +941,40 @@ async def run_prediction(config: PredictionConfig):
|
||||
|
||||
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
|
||||
|
||||
# Generate PNG preview for web display
|
||||
prediction_status["progress"] = "Đang tạo PNG preview..."
|
||||
png_file = output_dir / f"prediction_{timestamp}.png"
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use('Agg') # Non-interactive backend
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Create a figure with prediction result
|
||||
fig, ax = plt.subplots(figsize=(12, 10), dpi=150)
|
||||
|
||||
# Plot prediction with colormap
|
||||
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
||||
ax.set_title(f'Prediction Result - {timestamp}', fontsize=14, fontweight='bold')
|
||||
ax.set_xlabel('X (pixels)', fontsize=10)
|
||||
ax.set_ylabel('Y (pixels)', fontsize=10)
|
||||
|
||||
# Add colorbar
|
||||
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
||||
cbar.set_label('Class', rotation=270, labelpad=15)
|
||||
|
||||
# Add grid
|
||||
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
||||
|
||||
# Save PNG
|
||||
plt.tight_layout()
|
||||
plt.savefig(str(png_file), dpi=150, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f"[PNG PREVIEW] Created: {png_file}")
|
||||
except Exception as e:
|
||||
print(f"[PNG PREVIEW ERROR] Failed to create PNG: {e}")
|
||||
png_file = None
|
||||
|
||||
# Get unique classes for result
|
||||
unique_classes = np.unique(predictions_2d)
|
||||
unique_classes = unique_classes[~np.isnan(unique_classes)].tolist()
|
||||
@@ -828,6 +984,7 @@ async def run_prediction(config: PredictionConfig):
|
||||
prediction_status["output_file"] = str(output_file)
|
||||
prediction_status["result"] = {
|
||||
"output_file": str(output_file),
|
||||
"png_file": str(png_file) if png_file else None,
|
||||
"shape": list(pred_shape),
|
||||
"unique_classes": unique_classes,
|
||||
"bbox": bbox,
|
||||
@@ -903,6 +1060,369 @@ async def download_prediction(filename: str):
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/predictions/preview/{filename}")
|
||||
async def preview_prediction_png(filename: str):
|
||||
"""Preview PNG image of prediction"""
|
||||
predictions_dir = Path("predictions")
|
||||
file_path = predictions_dir / filename
|
||||
|
||||
# Security check
|
||||
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"PNG preview không tồn tại: {filename}")
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type="image/png"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/predictions/preview/{filename}")
|
||||
async def preview_prediction_png(filename: str):
|
||||
"""Preview PNG image of prediction"""
|
||||
predictions_dir = Path("predictions")
|
||||
file_path = predictions_dir / filename
|
||||
|
||||
# Security check
|
||||
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"PNG preview không tồn tại: {filename}")
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type="image/png"
|
||||
)
|
||||
|
||||
|
||||
# ============ DASHBOARD & VISUALIZATION API ============
|
||||
|
||||
@app.get("/api/dashboard/accuracy-trends")
|
||||
async def get_accuracy_trends():
|
||||
"""Lấy dữ liệu accuracy trends của các models theo thời gian"""
|
||||
model_dir = Path("model_train")
|
||||
if not model_dir.exists():
|
||||
return {"trends": [], "models": []}
|
||||
|
||||
trends_data = []
|
||||
for info_file in sorted(model_dir.glob("*.json")):
|
||||
try:
|
||||
with open(info_file) as f:
|
||||
info = json.load(f)
|
||||
|
||||
# Extract relevant data
|
||||
if "training_date" in info and "metrics" in info:
|
||||
trends_data.append({
|
||||
"date": info["training_date"],
|
||||
"model_name": info.get("model_type", "unknown"),
|
||||
"accuracy": info["metrics"].get("accuracy", 0),
|
||||
"f1_score": info["metrics"].get("macro avg", {}).get("f1-score", 0),
|
||||
"precision": info["metrics"].get("macro avg", {}).get("precision", 0),
|
||||
"recall": info["metrics"].get("macro avg", {}).get("recall", 0),
|
||||
"filename": info_file.stem + ".joblib"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error loading {info_file}: {e}")
|
||||
continue
|
||||
|
||||
# Sort by date
|
||||
trends_data.sort(key=lambda x: x["date"])
|
||||
|
||||
return {
|
||||
"trends": trends_data,
|
||||
"models": list(set(d["model_name"] for d in trends_data))
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/dashboard/statistics")
|
||||
async def get_statistics():
|
||||
"""Lấy thống kê tổng quan: số models, predictions, reports"""
|
||||
model_dir = Path("model_train")
|
||||
predictions_dir = Path("predictions")
|
||||
reports_dir = Path("reports")
|
||||
|
||||
# Count items
|
||||
n_models = len(list(model_dir.glob("*.joblib"))) if model_dir.exists() else 0
|
||||
n_predictions = len(list(predictions_dir.glob("*.tif"))) if predictions_dir.exists() else 0
|
||||
n_reports = len(list(reports_dir.glob("*.html"))) if reports_dir.exists() else 0
|
||||
|
||||
# Get latest model info
|
||||
latest_model = None
|
||||
if model_dir.exists():
|
||||
model_files = sorted(model_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
if model_files:
|
||||
try:
|
||||
with open(model_files[0]) as f:
|
||||
latest_model = json.load(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get latest prediction
|
||||
latest_prediction = None
|
||||
if predictions_dir.exists():
|
||||
pred_files = sorted(predictions_dir.glob("*.tif"), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
if pred_files:
|
||||
latest_prediction = {
|
||||
"filename": pred_files[0].name,
|
||||
"created": datetime.fromtimestamp(pred_files[0].stat().st_mtime).isoformat(),
|
||||
"size_mb": round(pred_files[0].stat().st_size / 1024 / 1024, 2)
|
||||
}
|
||||
|
||||
return {
|
||||
"models": {
|
||||
"total": n_models,
|
||||
"latest": latest_model
|
||||
},
|
||||
"predictions": {
|
||||
"total": n_predictions,
|
||||
"latest": latest_prediction
|
||||
},
|
||||
"reports": {
|
||||
"total": n_reports
|
||||
},
|
||||
"training_status": training_status,
|
||||
"prediction_status": prediction_status
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/dashboard/class-distribution/{model_filename}")
|
||||
async def get_class_distribution(model_filename: str):
|
||||
"""Lấy phân bố các lớp từ model info"""
|
||||
# Convert model filename to info filename
|
||||
# e.g., model_cnn_20251221_163841.joblib -> model_cnn_20251221_163841_info.json
|
||||
base_name = model_filename.replace(".joblib", "")
|
||||
info_file = Path("model_train") / f"{base_name}_info.json"
|
||||
|
||||
if not info_file.exists():
|
||||
raise HTTPException(status_code=404, detail="Model info không tồn tại")
|
||||
|
||||
with open(info_file) as f:
|
||||
info = json.load(f)
|
||||
|
||||
# Extract class distribution from classification report
|
||||
class_dist = {}
|
||||
if "classification_report" in info:
|
||||
for class_name, metrics in info["classification_report"].items():
|
||||
if isinstance(metrics, dict) and "support" in metrics:
|
||||
class_dist[class_name] = int(metrics["support"])
|
||||
|
||||
return {
|
||||
"model": model_filename,
|
||||
"class_distribution": class_dist,
|
||||
"total_samples": sum(class_dist.values()) if class_dist else 0
|
||||
}
|
||||
|
||||
|
||||
# ============ BATCH PROCESSING API ============
|
||||
|
||||
class BatchPredictionItem(BaseModel):
|
||||
"""Một item trong batch prediction"""
|
||||
name: str
|
||||
min_lon: float
|
||||
min_lat: float
|
||||
max_lon: float
|
||||
max_lat: float
|
||||
start_date: str = "2023-03-01"
|
||||
end_date: str = "2023-05-31"
|
||||
max_scenes: int = 12
|
||||
cloud_cover: int = 30
|
||||
resolution: int = 20
|
||||
|
||||
|
||||
class BatchPredictionConfig(BaseModel):
|
||||
"""Cấu hình cho batch prediction"""
|
||||
model_filename: str
|
||||
items: List[BatchPredictionItem]
|
||||
auto_retry: bool = True
|
||||
max_retries: int = 3
|
||||
|
||||
|
||||
@app.post("/api/batch/start")
|
||||
async def start_batch_prediction(config: BatchPredictionConfig, background_tasks: BackgroundTasks):
|
||||
"""Bắt đầu batch prediction"""
|
||||
global batch_queue, batch_results
|
||||
|
||||
# Create batch jobs
|
||||
batch_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
for idx, item in enumerate(config.items):
|
||||
job = {
|
||||
"batch_id": batch_id,
|
||||
"job_id": f"{batch_id}_{idx}",
|
||||
"name": item.name,
|
||||
"status": "queued",
|
||||
"progress": 0,
|
||||
"error": None,
|
||||
"result": None,
|
||||
"retries": 0,
|
||||
"max_retries": config.max_retries if config.auto_retry else 0,
|
||||
"config": {
|
||||
"model_filename": config.model_filename,
|
||||
"min_lon": item.min_lon,
|
||||
"min_lat": item.min_lat,
|
||||
"max_lon": item.max_lon,
|
||||
"max_lat": item.max_lat,
|
||||
"start_date": item.start_date,
|
||||
"end_date": item.end_date,
|
||||
"max_scenes": item.max_scenes,
|
||||
"cloud_cover": item.cloud_cover,
|
||||
"resolution": item.resolution
|
||||
},
|
||||
"created_at": datetime.now().isoformat()
|
||||
}
|
||||
batch_queue.append(job)
|
||||
|
||||
# Start processing in background
|
||||
background_tasks.add_task(process_batch_queue)
|
||||
|
||||
return {
|
||||
"message": f"Đã tạo {len(config.items)} batch jobs",
|
||||
"batch_id": batch_id,
|
||||
"total_jobs": len(config.items)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/batch/status")
|
||||
async def get_batch_status():
|
||||
"""Lấy trạng thái của batch queue"""
|
||||
global batch_queue, batch_results
|
||||
|
||||
queued = [j for j in batch_queue if j["status"] == "queued"]
|
||||
running = [j for j in batch_queue if j["status"] == "running"]
|
||||
completed = [j for j in batch_results if j["status"] == "completed"]
|
||||
failed = [j for j in batch_results if j["status"] == "failed"]
|
||||
|
||||
return {
|
||||
"queue": {
|
||||
"queued": len(queued),
|
||||
"running": len(running),
|
||||
"completed": len(completed),
|
||||
"failed": len(failed),
|
||||
"total": len(batch_queue) + len(batch_results)
|
||||
},
|
||||
"jobs": {
|
||||
"queued": queued[:5], # Show first 5
|
||||
"running": running,
|
||||
"recent_completed": completed[:10], # Show last 10
|
||||
"recent_failed": failed[:10]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/batch/results/{batch_id}")
|
||||
async def get_batch_results(batch_id: str):
|
||||
"""Lấy kết quả của một batch"""
|
||||
global batch_results
|
||||
|
||||
results = [j for j in batch_results if j["batch_id"] == batch_id]
|
||||
|
||||
if not results:
|
||||
# Check if still in queue
|
||||
queued = [j for j in batch_queue if j["batch_id"] == batch_id]
|
||||
if queued:
|
||||
return {
|
||||
"batch_id": batch_id,
|
||||
"status": "processing",
|
||||
"jobs": queued
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Batch không tồn tại")
|
||||
|
||||
return {
|
||||
"batch_id": batch_id,
|
||||
"status": "completed",
|
||||
"jobs": results,
|
||||
"summary": {
|
||||
"total": len(results),
|
||||
"successful": len([j for j in results if j["status"] == "completed"]),
|
||||
"failed": len([j for j in results if j["status"] == "failed"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/batch/cancel/{batch_id}")
|
||||
async def cancel_batch(batch_id: str):
|
||||
"""Hủy một batch đang chạy"""
|
||||
global batch_queue
|
||||
|
||||
# Remove from queue
|
||||
removed = 0
|
||||
batch_queue_copy = batch_queue.copy()
|
||||
for job in batch_queue_copy:
|
||||
if job["batch_id"] == batch_id and job["status"] == "queued":
|
||||
batch_queue.remove(job)
|
||||
removed += 1
|
||||
|
||||
return {
|
||||
"message": f"Đã hủy {removed} jobs",
|
||||
"batch_id": batch_id
|
||||
}
|
||||
|
||||
|
||||
async def process_batch_queue():
|
||||
"""Process batch prediction queue"""
|
||||
global batch_queue, batch_results
|
||||
|
||||
while batch_queue:
|
||||
# Get next job
|
||||
job = None
|
||||
for j in batch_queue:
|
||||
if j["status"] == "queued":
|
||||
job = j
|
||||
break
|
||||
|
||||
if not job:
|
||||
break
|
||||
|
||||
# Mark as running
|
||||
job["status"] = "running"
|
||||
job["started_at"] = datetime.now().isoformat()
|
||||
|
||||
try:
|
||||
# Create PredictionConfig from job config
|
||||
pred_config = PredictionConfig(**job["config"])
|
||||
|
||||
# Run prediction (simplified version)
|
||||
# In real implementation, call the actual prediction function
|
||||
print(f"[BATCH] Processing job: {job['name']}")
|
||||
|
||||
# Simulate prediction (replace with actual prediction call)
|
||||
# await run_prediction(pred_config)
|
||||
|
||||
# For now, mark as completed
|
||||
job["status"] = "completed"
|
||||
job["completed_at"] = datetime.now().isoformat()
|
||||
job["result"] = {
|
||||
"output_file": f"predictions/batch_{job['job_id']}.tif",
|
||||
"message": "Prediction completed successfully"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
job["error"] = str(e)
|
||||
|
||||
# Retry logic
|
||||
if job["retries"] < job["max_retries"]:
|
||||
job["retries"] += 1
|
||||
job["status"] = "queued" # Retry
|
||||
print(f"[BATCH] Job {job['name']} failed, retrying ({job['retries']}/{job['max_retries']})")
|
||||
continue
|
||||
else:
|
||||
job["status"] = "failed"
|
||||
job["completed_at"] = datetime.now().isoformat()
|
||||
print(f"[BATCH] Job {job['name']} failed permanently: {e}")
|
||||
|
||||
# Move to results
|
||||
batch_queue.remove(job)
|
||||
batch_results.append(job)
|
||||
|
||||
# Keep only last 100 results
|
||||
if len(batch_results) > 100:
|
||||
batch_results = batch_results[-100:]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
||||
|
||||
Reference in New Issue
Block a user