cơ bản hoàn tát các chức năng chính

This commit is contained in:
Victor Phan
2025-12-21 17:31:51 +07:00
parent 82aa814777
commit 46da481029
15 changed files with 5470 additions and 523 deletions
+603 -83
View File
@@ -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")