Files
remote-sensing/report_generator.py
T

1315 lines
66 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Auto Report Generator for Land Classification
Tự động tạo báo cáo HTML chi tiết sau training/prediction
"""
import json
from datetime import datetime
from pathlib import Path
import base64
import io
# Optional: for generating charts
try:
import matplotlib
matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt
import numpy as np
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
def generate_confusion_matrix_image(conf_matrix, class_names):
"""Tạo hình ảnh confusion matrix dạng base64"""
if not MATPLOTLIB_AVAILABLE:
return None
try:
# Normalize class names to strings
class_names = [str(c) for c in class_names]
fig, ax = plt.subplots(figsize=(10, 8))
conf_matrix = np.array(conf_matrix)
im = ax.imshow(conf_matrix, interpolation='nearest', cmap=plt.cm.Blues)
ax.figure.colorbar(im, ax=ax)
ax.set(xticks=np.arange(len(class_names)),
yticks=np.arange(len(class_names)),
xticklabels=class_names, yticklabels=class_names,
title='Confusion Matrix',
ylabel='Thực tế (True)',
xlabel='Dự đoán (Predicted)')
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
# Add text annotations
thresh = conf_matrix.max() / 2.
for i in range(len(class_names)):
for j in range(len(class_names)):
ax.text(j, i, format(conf_matrix[i, j], 'd'),
ha="center", va="center",
color="white" if conf_matrix[i, j] > thresh else "black")
fig.tight_layout()
# Convert to base64
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return img_base64
except Exception as e:
print(f"Error generating confusion matrix image: {e}")
return None
def generate_class_distribution_chart(class_names, classification_report):
"""Tạo biểu đồ phân bố các class dạng base64"""
if not MATPLOTLIB_AVAILABLE:
return None
try:
# Normalize keys to strings to handle integer/string mismatch
cls_report_str = {str(k): v for k, v in classification_report.items()}
class_names_str = [str(c) for c in class_names]
# Extract support (number of samples) for each class
supports = []
for cls in class_names_str:
if cls in cls_report_str:
supports.append(int(cls_report_str[cls].get('support', 0)))
else:
supports.append(0)
fig, ax = plt.subplots(figsize=(10, 6))
colors = plt.cm.Set3(np.linspace(0, 1, len(class_names)))
bars = ax.bar(class_names_str, supports, color=colors)
ax.set_xlabel('Loại đất')
ax.set_ylabel('Số mẫu')
ax.set_title('Phân bố số mẫu theo loại đất')
plt.xticks(rotation=45, ha='right')
# Add value labels on bars
for bar, val in zip(bars, supports):
if val > 0:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
str(val), ha='center', va='bottom', fontsize=9)
fig.tight_layout()
# Convert to base64
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return img_base64
except Exception as e:
print(f"Error generating class distribution chart: {e}")
return None
def generate_metrics_chart(class_names, classification_report):
"""Tạo biểu đồ precision/recall/f1 cho từng class"""
if not MATPLOTLIB_AVAILABLE:
return None
try:
# Normalize keys to strings to handle integer/string mismatch
cls_report_str = {str(k): v for k, v in classification_report.items()}
class_names_str = [str(c) for c in class_names]
precisions = []
recalls = []
f1_scores = []
for cls in class_names_str:
if cls in cls_report_str:
precisions.append(cls_report_str[cls].get('precision', 0))
recalls.append(cls_report_str[cls].get('recall', 0))
f1_scores.append(cls_report_str[cls].get('f1-score', 0))
else:
precisions.append(0)
recalls.append(0)
f1_scores.append(0)
x = np.arange(len(class_names_str))
width = 0.25
fig, ax = plt.subplots(figsize=(12, 6))
bars1 = ax.bar(x - width, precisions, width, label='Precision', color='#3498db')
bars2 = ax.bar(x, recalls, width, label='Recall', color='#2ecc71')
bars3 = ax.bar(x + width, f1_scores, width, label='F1-Score', color='#e74c3c')
ax.set_xlabel('Loại đất')
ax.set_ylabel('Score')
ax.set_title('Precision / Recall / F1-Score theo loại đất')
ax.set_xticks(x)
ax.set_xticklabels(class_names_str, rotation=45, ha='right')
ax.legend()
ax.set_ylim(0, 1.1)
# Add grid
ax.yaxis.grid(True, linestyle='--', alpha=0.7)
fig.tight_layout()
# Convert to base64
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return img_base64
except Exception as e:
print(f"Error generating metrics chart: {e}")
return None
def generate_training_report(training_result, config=None):
"""
Tạo báo cáo HTML cho kết quả training
Args:
training_result: Dict chứa kết quả từ train_model()
config: Dict chứa cấu hình training (optional)
Returns:
Tuple (report_path, report_html)
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Extract data from result
train_acc = training_result.get('train_accuracy', 0) * 100
test_acc = training_result.get('test_accuracy', 0) * 100
train_samples = training_result.get('training_samples', 0)
test_samples = training_result.get('testing_samples', 0)
test_size = training_result.get('test_size', 0.2)
classes = [str(c) for c in training_result.get('classes', [])] # normalize to strings
cls_report = training_result.get('classification_report', {})
# Also normalize cls_report keys to strings (handles integer keys from JSON)
cls_report = {str(k): v for k, v in cls_report.items()}
conf_matrix = training_result.get('confusion_matrix', [])
model_type = training_result.get('model_type', 'unknown')
model_path = training_result.get('model_path', '')
bbox = training_result.get('bbox', [])
time_range = training_result.get('time_range', '')
resolution = training_result.get('resolution', 20)
# NEW: Extract PSNR metrics (for cloud removal)
val_psnr = training_result.get('val_psnr', None)
baseline_psnr = training_result.get('baseline_psnr', None)
improvement = None
if val_psnr is not None and baseline_psnr is not None:
improvement = val_psnr - baseline_psnr
# NEW: Extract Best Checkpoint data
best_checkpoint = training_result.get('best_checkpoint', training_result.get('bestCheckpoint', None))
# NEW: Extract Worst Checkpoint data (for performance range analysis)
worst_checkpoint = training_result.get('worst_checkpoint', training_result.get('worstCheckpoint', None))
# Calculate baseline comparison message for cloud removal Performance Range
baseline_comparison_msg = ""
if worst_checkpoint and 'modelPSNR' in worst_checkpoint and 'baselinePSNR' in worst_checkpoint:
worst_psnr = worst_checkpoint.get('modelPSNR', 0)
baseline = worst_checkpoint.get('baselinePSNR', 0)
margin = worst_psnr - baseline
if margin > 5:
baseline_comparison_msg = f'• <strong style="color: #2ecc71;">✅ Tốt:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) vượt baseline +{margin:.2f}dB - Model học tốt ngay cả trong worst case'
elif margin > 2:
baseline_comparison_msg = f'• <strong style="color: #f39c12;">⚠️ Khá:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) vượt baseline +{margin:.2f}dB - Cần cải thiện stability'
elif margin > 0:
baseline_comparison_msg = f'• <strong style="color: #e67e22;">⚠️ Yếu:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) chỉ vượt baseline +{margin:.2f}dB - Model không ổn định'
else:
baseline_comparison_msg = f'• <strong style="color: #e74c3c;">❌ Kém:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) không vượt baseline ({baseline:.2f}dB) - Model thất bại'
# NEW: Extract Random Baseline (for land classification)
random_baseline = training_result.get('random_baseline', None)
# If backend provided exact baselines, prefer majority_class_baseline over uniform 1/n_classes
majority_class_baseline = training_result.get('majority_class_baseline', None)
weighted_random_baseline = training_result.get('weighted_random_baseline', None)
# random_baseline from frontend is already in % (e.g. 28.5);
# majority_class_baseline from backend is a fraction (e.g. 0.285)
if random_baseline is None and majority_class_baseline is not None:
random_baseline = majority_class_baseline * 100
# NEW: Extract detailed hyperparameters
hyperparams = training_result.get('hyperparameters', {})
if not hyperparams and config:
hyperparams = config
# Extract common hyperparameters
n_estimators = training_result.get('n_estimators', hyperparams.get('n_estimators', 'N/A'))
max_depth = training_result.get('max_depth', hyperparams.get('max_depth', 'N/A'))
learning_rate = training_result.get('learning_rate', hyperparams.get('learning_rate', 'N/A'))
batch_size = training_result.get('batch_size', hyperparams.get('batch_size', 'N/A'))
num_epochs = training_result.get('num_epochs', hyperparams.get('num_epochs', 'N/A'))
use_gpu = training_result.get('use_gpu', hyperparams.get('use_gpu', False))
use_s1 = training_result.get('use_s1', hyperparams.get('use_s1', False))
# Feature information
features = training_result.get('features', [])
feature_mode = training_result.get('feature_mode', 'simple')
n_features = training_result.get('n_features', len(features) if features else 'N/A')
# Data source info
data_source = training_result.get('data_source', 'Unknown')
collections = training_result.get('collections', [])
# Generate charts
conf_matrix_img = generate_confusion_matrix_image(conf_matrix, classes) if conf_matrix else None
class_dist_img = generate_class_distribution_chart(classes, cls_report) if cls_report else None
metrics_img = generate_metrics_chart(classes, cls_report) if cls_report else None
# Build classification report table
cls_report_rows = ""
for cls in classes:
if cls in cls_report:
metrics = cls_report[cls]
elif str(cls) in cls_report:
metrics = cls_report[str(cls)]
else:
metrics = None
if metrics:
cls_report_rows += f"""
<tr>
<td><strong>{cls}</strong></td>
<td>{metrics.get('precision', 0):.3f}</td>
<td>{metrics.get('recall', 0):.3f}</td>
<td>{metrics.get('f1-score', 0):.3f}</td>
<td>{int(metrics.get('support', 0))}</td>
</tr>
"""
# Add averages
for avg_type in ['macro avg', 'weighted avg']:
if avg_type in cls_report:
metrics = cls_report[avg_type]
cls_report_rows += f"""
<tr style="background-color: #f0f0f0; font-weight: bold;">
<td>{avg_type}</td>
<td>{metrics.get('precision', 0):.3f}</td>
<td>{metrics.get('recall', 0):.3f}</td>
<td>{metrics.get('f1-score', 0):.3f}</td>
<td>{int(metrics.get('support', 0))}</td>
</tr>
"""
# Build confusion matrix table (fallback if no image)
conf_matrix_table = ""
if conf_matrix:
conf_matrix_table = "<table class='conf-matrix'><tr><th></th>"
for cls in classes:
conf_matrix_table += f"<th>{cls}</th>"
conf_matrix_table += "</tr>"
for i, row in enumerate(conf_matrix):
conf_matrix_table += f"<tr><th>{classes[i]}</th>"
for val in row:
conf_matrix_table += f"<td>{val}</td>"
conf_matrix_table += "</tr>"
conf_matrix_table += "</table>"
# Pre-compute accuracy Performance Range stats (avoid division-by-zero in f-strings)
_bc = best_checkpoint or {}
_wc = worst_checkpoint or {}
if best_checkpoint and worst_checkpoint and 'accuracy' in _bc and 'accuracy' in _wc:
_best_acc = _bc.get('accuracy', 0)
_worst_acc = _wc.get('accuracy', 0)
acc_range_pct = (_best_acc - _worst_acc) * 100
acc_avg_pct = ((_best_acc + _worst_acc) / 2) * 100
acc_avg_raw = (_best_acc + _worst_acc) / 2
if acc_avg_raw > 0:
acc_stability = max(0, (1 - (_best_acc - _worst_acc) / acc_avg_raw) * 100)
else:
acc_stability = 100.0 # best == worst == 0, consider stable
acc_stable_label = 'ổn định ✅' if acc_stability >= 80 else 'cần cải thiện ⚠️'
rnd_bl = random_baseline or 0
rnd_bl_display = f'{rnd_bl:.2f}' if rnd_bl else 'N/A'
worst_beat_baseline = rnd_bl > 0 and _worst_acc * 100 > rnd_bl
best_beat_margin = (_best_acc * 100 - rnd_bl) if rnd_bl else None
n_classes_display = len(classes) if classes else training_result.get('n_classes', '?')
else:
acc_range_pct = acc_avg_pct = acc_stability = 0.0
acc_stable_label = ''
rnd_bl = rnd_bl_display = 0
worst_beat_baseline = False
best_beat_margin = None
n_classes_display = '?'
# HTML Template
html = f"""
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Training Report - {timestamp}</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
}}
.header h1 {{
font-size: 2.5em;
margin-bottom: 10px;
}}
.header .subtitle {{
opacity: 0.9;
font-size: 1.1em;
}}
.content {{
padding: 40px;
}}
.section {{
margin-bottom: 40px;
}}
.section h2 {{
color: #667eea;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
margin-bottom: 20px;
font-size: 1.5em;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}}
.stat-card {{
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #667eea30;
}}
.stat-card .value {{
font-size: 2.5em;
font-weight: bold;
color: #667eea;
}}
.stat-card .label {{
color: #666;
margin-top: 5px;
}}
.stat-card.success .value {{
color: #28a745;
}}
.stat-card.warning .value {{
color: #ffc107;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}}
th, td {{
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}}
th {{
background: #667eea;
color: white;
}}
tr:hover {{
background-color: #f5f5f5;
}}
.conf-matrix {{
font-size: 14px;
}}
.conf-matrix th, .conf-matrix td {{
text-align: center;
padding: 8px;
}}
.chart-container {{
text-align: center;
margin: 20px 0;
}}
.chart-container img {{
max-width: 100%;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}}
.info-box {{
background: #e3f2fd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #2196f3;
margin: 20px 0;
}}
.info-row {{
display: flex;
margin: 10px 0;
}}
.info-label {{
font-weight: bold;
width: 200px;
color: #555;
}}
.info-value {{
color: #333;
}}
.footer {{
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
font-size: 14px;
}}
@media print {{
body {{
background: white;
padding: 0;
}}
.container {{
box-shadow: none;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Báo Cáo Training Model</h1>
<p class="subtitle">Land Classification - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
</div>
<div class="content">
<!-- Summary Stats -->
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card success">
<div class="value">{train_acc:.1f}%</div>
<div class="label">Train Accuracy</div>
</div>
<div class="stat-card {'success' if test_acc >= 80 else 'warning'}">
<div class="value">{test_acc:.1f}%</div>
<div class="label">Test Accuracy</div>
</div>
<div class="stat-card">
<div class="value">{train_samples}</div>
<div class="label">Training Samples</div>
</div>
<div class="stat-card">
<div class="value">{test_samples}</div>
<div class="label">Testing Samples</div>
</div>
<div class="stat-card">
<div class="value">{len(classes)}</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">{test_size*100:.0f}%</div>
<div class="label">Test Size</div>
</div>
{f'''<div class="stat-card success">
<div class="value">{val_psnr:.2f} dB</div>
<div class="label">📈 Model PSNR</div>
</div>''' if val_psnr is not None else ''}
{f'''<div class="stat-card">
<div class="value">{baseline_psnr:.2f} dB</div>
<div class="label">📉 Baseline PSNR</div>
</div>''' if baseline_psnr is not None else ''}
{f'''<div class="stat-card {'success' if improvement > 0 else 'warning'}">
<div class="value">{'+' if improvement > 0 else ''}{improvement:.2f} dB</div>
<div class="label">⚡ Improvement</div>
</div>''' if improvement is not None else ''}
</div>
</div>
<!-- Best Checkpoint (if available) -->
{f'''<div class="section">
<h2>🏆 Best Accuracy Checkpoint</h2>
<div style="background: linear-gradient(135deg, #ffd70020 0%, #ffa50020 100%); padding: 25px; border-radius: 10px; border: 2px solid #ffa500;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #ff8c00;">
{best_checkpoint.get('accuracy', 0) * 100:.2f}%
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
Đạt tại epoch {best_checkpoint.get('epoch', 'N/A')}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(best_checkpoint.get('trainAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(best_checkpoint.get('valAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{best_checkpoint.get('trainLoss'):.4f}" if best_checkpoint.get('trainLoss') is not None else 'N/A'}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{best_checkpoint.get('valLoss'):.4f}" if best_checkpoint.get('valLoss') is not None else 'N/A'}
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là điểm checkpoint tốt nhất trong toàn bộ quá trình training, được ghi nhận khi validation accuracy đạt giá trị cao nhất.
Các metrics này thể hiện hiệu suất thực sự của model tại thời điểm tối ưu.
</div>
</div>
</div>''' if best_checkpoint else ''}
<!-- Worst Accuracy Checkpoint for Land Classification (if available) -->
{f'''<div class="section">
<h2>📉 Worst Accuracy Checkpoint (Cận Dưới)</h2>
<div style="background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%); padding: 25px; border-radius: 10px; border: 2px solid #667eea;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #667eea;">
{worst_checkpoint.get('accuracy', 0) * 100:.2f}%
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
Accuracy thấp nhất tại epoch {worst_checkpoint.get('epoch', 'N/A')} (cận dưới training)
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(worst_checkpoint.get('trainAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(worst_checkpoint.get('valAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{worst_checkpoint.get('trainLoss'):.4f}" if worst_checkpoint.get('trainLoss') is not None else 'N/A'}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{worst_checkpoint.get('valLoss'):.4f}" if worst_checkpoint.get('valLoss') is not None else 'N/A'}
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là epoch có accuracy thấp nhất trong quá trình training, thể hiện điểm cận dưới của khả năng model.
{f"Ngay cả ở epoch tệ nhất, accuracy vẫn {worst_checkpoint.get('accuracy', 0) * 100:.2f}% {'> random baseline' if random_baseline and worst_checkpoint.get('accuracy', 0) * 100 > random_baseline else '≤ random baseline'}." if random_baseline else ''}
</div>
</div>
</div>''' if worst_checkpoint and 'accuracy' in worst_checkpoint else ''}
<!-- Performance Range Analysis for Land Classification (if both best and worst available) -->
{f'''<div class="section">
<h2>📊 Performance Range & Random Baseline Analysis</h2>
<div style="background: linear-gradient(135deg, #f093fb20 0%, #f5576c20 100%); padding: 25px; border-radius: 10px; border: 2px solid #f093fb;">
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px;">
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Accuracy Range</div>
<div style="font-size: 2.5em; font-weight: bold; color: #667eea;">
{acc_range_pct:.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">% (max - min)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">
Max: {_bc.get('accuracy', 0) * 100:.2f}%<br>
Min: {_wc.get('accuracy', 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Average Accuracy</div>
<div style="font-size: 2.5em; font-weight: bold; color: #43e97b;">
{acc_avg_pct:.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">%</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Stability Score</div>
<div style="font-size: 2.5em; font-weight: bold; color: #f093fb;">
{acc_stability:.1f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">%</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">{acc_stable_label}</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Random Baseline</div>
<div style="font-size: 2.5em; font-weight: bold; color: #ff6b6b;">
{rnd_bl_display}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">% (lớp chiếm đa số)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">{n_classes_display} classes{f'<br><span style="font-size:0.9em;color:#aaa;">Weighted: {weighted_random_baseline*100:.2f}%</span>' if weighted_random_baseline else ''}</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>💡 Đánh giá:</strong><br>
• <strong>Range</strong>: {acc_range_pct:.2f}% - Model {"ổn định" if acc_range_pct < 5 else "có dao động"} trong quá trình training<br>
• <strong>Worst Accuracy vs Random Baseline</strong>: {_wc.get('accuracy', 0) * 100:.2f}% {">" if worst_beat_baseline else "≤"} {rnd_bl_display}% → {"✅ Model luôn tốt hơn đoán ngẫu nhiên ngay cả ở điểm tệ nhất!" if worst_beat_baseline else ("⚠️ Điểm tệ nhất chưa vượt mức ngẫu nhiên - cần cải thiện hyperparameters!" if rnd_bl else "️ Không có dữ liệu random baseline")}<br>
• <strong>Stability</strong>: {acc_stability:.1f}% - Training {acc_stable_label}<br>
• <strong>Best Accuracy</strong>: {_bc.get('accuracy', 0) * 100:.2f}% {">" if best_beat_margin is not None and best_beat_margin > 0 else "≤"} {rnd_bl_display}% → {"✅ Vượt random baseline " + f"{best_beat_margin:.2f}%" if best_beat_margin is not None and best_beat_margin > 0 else "⚠️ Không vượt random baseline"}
</div>
</div>
</div>''' if best_checkpoint and worst_checkpoint and 'accuracy' in _bc and 'accuracy' in _wc else ''}
<!-- Best PSNR Checkpoint for Cloud Removal (if available) -->
{f'''<div class="section">
<h2>🏆 Best PSNR Checkpoint</h2>
<div style="background: linear-gradient(135deg, #ffd70020 0%, #ffa50020 100%); padding: 25px; border-radius: 10px; border: 2px solid #ffa500;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #ff8c00;">
{best_checkpoint.get('modelPSNR', 0):.2f} dB
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
Model PSNR đạt tại epoch {best_checkpoint.get('epoch', 'N/A')}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{best_checkpoint.get('trainLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{best_checkpoint.get('valLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Baseline PSNR</div>
<div style="font-size: 1.5em; font-weight: bold; color: #6c757d; margin-top: 5px;">
{best_checkpoint.get('baselinePSNR', 0):.2f} dB
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Improvement</div>
<div style="font-size: 1.5em; font-weight: bold; color: {'#28a745' if best_checkpoint.get('improvement', 0) > 0 else '#ffc107'}; margin-top: 5px;">
{'+' if best_checkpoint.get('improvement', 0) > 0 else ''}{best_checkpoint.get('improvement', 0):.2f} dB
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là điểm checkpoint tốt nhất trong toàn bộ quá trình training cloud removal, được ghi nhận khi Model PSNR đạt giá trị cao nhất.
Improvement thể hiện mức cải thiện so với baseline (không xử lý gì).
</div>
</div>
</div>''' if best_checkpoint and 'modelPSNR' in best_checkpoint else ''}
<!-- Worst PSNR Checkpoint for Cloud Removal (if available) -->
{f'''<div class="section">
<h2>📉 Worst PSNR Checkpoint (Cận Dưới)</h2>
<div style="background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%); padding: 25px; border-radius: 10px; border: 2px solid #667eea;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #667eea;">
{worst_checkpoint.get('modelPSNR', 0):.2f} dB
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
PSNR thấp nhất tại epoch {worst_checkpoint.get('epoch', 'N/A')} (cận dưới training)
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{worst_checkpoint.get('trainLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{worst_checkpoint.get('valLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Baseline PSNR</div>
<div style="font-size: 1.5em; font-weight: bold; color: #6c757d; margin-top: 5px;">
{worst_checkpoint.get('baselinePSNR', 0):.2f} dB
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Gap from Baseline</div>
<div style="font-size: 1.5em; font-weight: bold; color: {'#28a745' if worst_checkpoint.get('improvement', 0) > 0 else '#dc3545'}; margin-top: 5px;">
{'+' if worst_checkpoint.get('improvement', 0) > 0 else ''}{worst_checkpoint.get('improvement', 0):.2f} dB
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là epoch có PSNR thấp nhất trong quá trình training, thể hiện điểm cận dưới của khả năng model.
Nếu worst PSNR vẫn > baseline, có nghĩa là ngay cả ở epoch tệ nhất, model vẫn tốt hơn không làm gì!
</div>
</div>
</div>''' if worst_checkpoint and 'modelPSNR' in worst_checkpoint else ''}
<!-- Performance Range Analysis (if both best and worst available) -->
{f'''<div class="section">
<h2>📊 Performance Range Analysis</h2>
<div style="background: linear-gradient(135deg, #f093fb20 0%, #f5576c20 100%); padding: 25px; border-radius: 10px; border: 2px solid #f093fb;">
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;">
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">PSNR Range</div>
<div style="font-size: 2.5em; font-weight: bold; color: #667eea;">
{(best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)):.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">dB (max - min)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">
Max: {best_checkpoint.get('modelPSNR', 0):.2f} dB<br>
Min: {worst_checkpoint.get('modelPSNR', 0):.2f} dB
</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Average PSNR</div>
<div style="font-size: 2.5em; font-weight: bold; color: #43e97b;">
{((best_checkpoint.get('modelPSNR', 0) + worst_checkpoint.get('modelPSNR', 0)) / 2):.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">dB (estimated)</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Baseline PSNR</div>
<div style="font-size: 2.5em; font-weight: bold; color: #ff6b6b;">
{worst_checkpoint.get('baselinePSNR', 0):.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">dB (cận dưới)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">
Cloudy vs Clean
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>💡 Baseline Comparison:</strong><br>
{baseline_comparison_msg}<br>
• <strong>Range {'nhỏ' if (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) < 2 else 'lớn'}</strong> ({(best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)):.2f} dB): Training {'ổn định' if (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) < 2 else 'dao động'}<br>
• <strong>Stability {'>=' if max(0, (1 - (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) / ((best_checkpoint.get('modelPSNR', 0) + worst_checkpoint.get('modelPSNR', 0)) / 2)) * 100) >= 80 else '<'} 80%</strong>: Training {'rất ổn định ✅' if max(0, (1 - (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) / ((best_checkpoint.get('modelPSNR', 0) + worst_checkpoint.get('modelPSNR', 0)) / 2)) * 100) >= 80 else 'cần cải thiện hyperparameters ⚠️'}
</div>
</div>
</div>''' if best_checkpoint and worst_checkpoint and 'modelPSNR' in best_checkpoint and 'modelPSNR' in worst_checkpoint else ''}
<!-- PSNR Analysis (if available) -->
{f'''<div class="section">
<h2>🎯 PSNR Baseline Analysis</h2>
<div class="info-box" style="background: {'#d4edda' if improvement > 0 else '#fff3cd'}; border-left-color: {'#28a745' if improvement > 0 else '#ffc107'};">
<div class="info-row">
<span class="info-label">📈 Model PSNR:</span>
<span class="info-value" style="font-weight: bold; font-size: 1.1em;">{val_psnr:.2f} dB</span>
</div>
<div class="info-row">
<span class="info-label">📉 Baseline PSNR (cận dưới):</span>
<span class="info-value">{baseline_psnr:.2f} dB</span>
</div>
<div class="info-row">
<span class="info-label">⚡ Improvement:</span>
<span class="info-value" style="color: {'green' if improvement > 0 else 'orange'}; font-weight: bold;">{'+' if improvement > 0 else ''}{improvement:.2f} dB ({'+' if improvement > 0 else ''}{(improvement/baseline_psnr*100):.1f}%)</span>
</div>
<div class="info-row" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #ddd;">
<span class="info-label">📊 Đánh giá:</span>
<span class="info-value" style="font-weight: bold;">{'✅ Model TỐT HƠN baseline - Kết quả đáng tin cậy!' if improvement > 0 else '⚠️ Model chưa vượt qua baseline - Cần điều chỉnh!'}</span>
</div>
<div style="margin-top: 15px; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 5px; font-size: 0.9em;">
<strong>Giải thích:</strong><br>
- <strong>Baseline PSNR</strong>: PSNR giữa ảnh cloudy và ảnh clean (không làm gì)<br>
- <strong>Model PSNR</strong>: PSNR giữa output model và ảnh clean<br>
- <strong>Improvement > 0</strong>: Model đang khử mây hiệu quả!<br>
- <strong>Improvement < 0</strong>: Model làm tồi hơn không làm gì!
</div>
</div>
</div>''' if val_psnr is not None and baseline_psnr is not None else ''}
<!-- Configuration Info -->
<div class="section">
<h2>⚙️ Cấu Hình Training</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model Type:</span>
<span class="info-value">{model_type.upper()}</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span class="info-value">{bbox}</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span class="info-value">{time_range}</span>
</div>
<div class="info-row">
<span class="info-label">📐 Độ phân giải:</span>
<span class="info-value">{resolution}m</span>
</div>
<div class="info-row">
<span class="info-label">💾 Model Path:</span>
<span class="info-value" style="font-family: monospace; font-size: 0.9em;">{model_path}</span>
</div>
<div class="info-row">
<span class="info-label">📊 Data Source:</span>
<span class="info-value">{data_source}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">🛰️ Collections:</span>
<span class="info-value">{', '.join(collections)}</span>
</div>''' if collections else ''}
</div>
</div>
<!-- Hyperparameters Detail -->
<div class="section">
<h2>🎯 Hyperparameters - Chi Tiết Tái Hiện</h2>
<div class="info-box" style="background: #fff3cd; border-left-color: #ffc107;">
<div style="margin-bottom: 15px; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 5px;">
<strong>🔄 Để tái hiện kết quả training này, sử dụng các tham số dưới đây:</strong>
</div>
<h3 style="color: #ffc107; margin-top: 20px; margin-bottom: 10px;">📊 Model Hyperparameters</h3>
<div class="info-row">
<span class="info-label">Model Type:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{model_type}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">n_estimators:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{n_estimators}</span>
</div>''' if n_estimators != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">max_depth:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{max_depth}</span>
</div>''' if max_depth != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">learning_rate:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{learning_rate}</span>
</div>''' if learning_rate != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">batch_size:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{batch_size}</span>
</div>''' if batch_size != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">num_epochs:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{num_epochs}</span>
</div>''' if num_epochs != 'N/A' else ''}
<div class="info-row">
<span class="info-label">use_gpu:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{use_gpu}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">use_s1 (Sentinel-1):</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{use_s1}</span>
</div>''' if model_type in ['cloud_removal', 'unet'] else ''}
<h3 style="color: #ffc107; margin-top: 20px; margin-bottom: 10px;">📦 Data Processing</h3>
<div class="info-row">
<span class="info-label">test_size:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{test_size}</span>
</div>
<div class="info-row">
<span class="info-label">feature_mode:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{feature_mode}</span>
</div>
<div class="info-row">
<span class="info-label">n_features:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{n_features}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">features:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">{', '.join(features)}</span>
</div>''' if features else ''}
<h3 style="color: #ffc107; margin-top: 20px; margin-bottom: 10px;">🌍 Geo & Time Parameters</h3>
<div class="info-row">
<span class="info-label">bbox:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">{bbox}</span>
</div>
<div class="info-row">
<span class="info-label">time_range:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{time_range}</span>
</div>
<div class="info-row">
<span class="info-label">resolution:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{resolution}m</span>
</div>
<div style="margin-top: 20px; padding: 15px; background: #e3f2fd; border-radius: 5px; border-left: 4px solid #2196f3;">
<strong>📝 Lưu ý:</strong><br>
• Lưu toàn bộ các tham số trên để reproduce kết quả<br>
• Sử dụng cùng dataset và time range để đảm bảo tính nhất quán<br>
• Random seed: 42 (mặc định)<br>
• Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}
</div>
</div>
</div>
<!-- Classification Report -->
<div class="section">
<h2>📋 Classification Report</h2>
<table>
<thead>
<tr>
<th>Loại đất</th>
<th>Precision</th>
<th>Recall</th>
<th>F1-Score</th>
<th>Support</th>
</tr>
</thead>
<tbody>
{cls_report_rows}
</tbody>
</table>
</div>
<!-- Metrics Chart -->
{'<div class="section"><h2>📊 Biểu Đồ Metrics</h2><div class="chart-container"><img src="data:image/png;base64,' + metrics_img + '" alt="Metrics Chart"></div></div>' if metrics_img else ''}
<!-- Class Distribution -->
{'<div class="section"><h2>📊 Phân Bố Số Mẫu</h2><div class="chart-container"><img src="data:image/png;base64,' + class_dist_img + '" alt="Class Distribution"></div></div>' if class_dist_img else ''}
<!-- Confusion Matrix -->
<div class="section">
<h2>🔢 Confusion Matrix</h2>
{'<div class="chart-container"><img src="data:image/png;base64,' + conf_matrix_img + '" alt="Confusion Matrix"></div>' if conf_matrix_img else conf_matrix_table}
</div>
<!-- Classes List -->
<div class="section">
<h2>🏷️ Danh Sách Các Loại Đất</h2>
<div class="info-box">
<ul style="list-style: none; display: flex; flex-wrap: wrap; gap: 10px;">
{''.join([f'<li style="background: #667eea; color: white; padding: 8px 15px; border-radius: 20px;">{cls}</li>' for cls in classes])}
</ul>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification Training System | Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
<p>Data Source: Microsoft Planetary Computer (Sentinel-2 L2A, Sentinel-1 RTC)</p>
</div>
</div>
</body>
</html>
"""
# Save report
reports_dir = Path("reports")
reports_dir.mkdir(exist_ok=True)
report_filename = f"training_report_{timestamp}.html"
report_path = reports_dir / report_filename
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html)
return str(report_path), html
def generate_prediction_report(prediction_result, config=None):
"""
Tạo báo cáo HTML cho kết quả prediction
Args:
prediction_result: Dict chứa kết quả prediction
config: Dict chứa cấu hình prediction (optional)
Returns:
Tuple (report_path, report_html)
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Extract data
output_file = prediction_result.get('output_file', '')
shape = prediction_result.get('shape', [0, 0])
unique_classes = prediction_result.get('unique_classes', [])
bbox = prediction_result.get('bbox', [])
time_range = prediction_result.get('time_range', '')
n_features = prediction_result.get('n_features', 0)
used_radar = prediction_result.get('used_radar', False)
model_used = prediction_result.get('model_used', '')
# Calculate area (approximate)
if len(bbox) == 4:
# Approximate calculation (1 degree ≈ 111km at equator)
width_km = (bbox[2] - bbox[0]) * 111 * 0.85 # cos adjustment for Vietnam
height_km = (bbox[3] - bbox[1]) * 111
area_km2 = width_km * height_km
else:
area_km2 = 0
total_pixels = shape[0] * shape[1] if len(shape) == 2 else 0
# HTML Template
html = f"""
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - {timestamp}</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}}
.header h1 {{
font-size: 2.5em;
margin-bottom: 10px;
}}
.content {{
padding: 40px;
}}
.section {{
margin-bottom: 40px;
}}
.section h2 {{
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}}
.stat-card {{
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}}
.stat-card .value {{
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}}
.stat-card .label {{
color: #666;
margin-top: 5px;
}}
.info-box {{
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}}
.info-row {{
display: flex;
margin: 10px 0;
}}
.info-label {{
font-weight: bold;
width: 200px;
color: #555;
}}
.class-badge {{
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}}
.footer {{
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">{total_pixels:,}</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">{shape[0]}x{shape[1]}</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">{area_km2:.1f}</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">{len(unique_classes)}</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">{n_features}</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value">{'✅' if used_radar else '❌'}</div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>{model_used}</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>{bbox}</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>{time_range}</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>{output_file}</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
{''.join([f'<span class="class-badge">{cls}</span>' for cls in unique_classes])}
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
</div>
</div>
</body>
</html>
"""
# Save report
reports_dir = Path("reports")
reports_dir.mkdir(exist_ok=True)
report_filename = f"prediction_report_{timestamp}.html"
report_path = reports_dir / report_filename
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html)
return str(report_path), html
if __name__ == "__main__":
# Test report generation
test_result = {
"success": True,
"train_accuracy": 0.95,
"test_accuracy": 0.87,
"training_samples": 800,
"testing_samples": 200,
"test_size": 0.2,
"classes": ["Lua", "Rung", "Nuoc", "Dan_cu", "Cay_lau_nam"],
"model_type": "xgboost",
"model_path": "model_train/model_xgboost_20251221.joblib",
"bbox": [105.6, 9.3, 106.2, 9.8],
"time_range": "2023-03-01/2023-05-31",
"resolution": 20,
"classification_report": {
"Lua": {"precision": 0.92, "recall": 0.89, "f1-score": 0.90, "support": 50},
"Rung": {"precision": 0.88, "recall": 0.91, "f1-score": 0.89, "support": 45},
"Nuoc": {"precision": 0.95, "recall": 0.93, "f1-score": 0.94, "support": 40},
"Dan_cu": {"precision": 0.85, "recall": 0.82, "f1-score": 0.83, "support": 35},
"Cay_lau_nam": {"precision": 0.80, "recall": 0.85, "f1-score": 0.82, "support": 30},
"macro avg": {"precision": 0.88, "recall": 0.88, "f1-score": 0.88, "support": 200},
"weighted avg": {"precision": 0.88, "recall": 0.87, "f1-score": 0.87, "support": 200}
},
"confusion_matrix": [
[45, 2, 1, 1, 1],
[3, 41, 0, 1, 0],
[1, 0, 37, 1, 1],
[2, 1, 1, 29, 2],
[1, 1, 1, 2, 26]
]
}
path, html = generate_training_report(test_result)
print(f"Report generated: {path}")