"""
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'• ✅ Tốt: 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'• ⚠️ Khá: 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'• ⚠️ Yếu: 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'• ❌ Kém: 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"""
| {cls} |
{metrics.get('precision', 0):.3f} |
{metrics.get('recall', 0):.3f} |
{metrics.get('f1-score', 0):.3f} |
{int(metrics.get('support', 0))} |
"""
# 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"""
| {avg_type} |
{metrics.get('precision', 0):.3f} |
{metrics.get('recall', 0):.3f} |
{metrics.get('f1-score', 0):.3f} |
{int(metrics.get('support', 0))} |
"""
# Build confusion matrix table (fallback if no image)
conf_matrix_table = ""
if conf_matrix:
conf_matrix_table = " | "
for cls in classes:
conf_matrix_table += f"{cls} | "
conf_matrix_table += "
"
for i, row in enumerate(conf_matrix):
conf_matrix_table += f"| {classes[i]} | "
for val in row:
conf_matrix_table += f"{val} | "
conf_matrix_table += "
"
conf_matrix_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"""
Training Report - {timestamp}
📈 Tóm Tắt Kết Quả
{train_acc:.1f}%
Train Accuracy
{test_acc:.1f}%
Test Accuracy
{train_samples}
Training Samples
{test_samples}
Testing Samples
{len(classes)}
Số Classes
{test_size*100:.0f}%
Test Size
{f'''
{val_psnr:.2f} dB
📈 Model PSNR
''' if val_psnr is not None else ''}
{f'''
{baseline_psnr:.2f} dB
📉 Baseline PSNR
''' if baseline_psnr is not None else ''}
{f'''
{'+' if improvement > 0 else ''}{improvement:.2f} dB
⚡ Improvement
''' if improvement is not None else ''}
{f'''
🏆 Best Accuracy Checkpoint
{best_checkpoint.get('accuracy', 0) * 100:.2f}%
Đạt tại epoch {best_checkpoint.get('epoch', 'N/A')}
Train Accuracy
{(best_checkpoint.get('trainAcc') or 0) * 100:.2f}%
Val Accuracy
{(best_checkpoint.get('valAcc') or 0) * 100:.2f}%
Train Loss
{f"{best_checkpoint.get('trainLoss'):.4f}" if best_checkpoint.get('trainLoss') is not None else 'N/A'}
Val Loss
{f"{best_checkpoint.get('valLoss'):.4f}" if best_checkpoint.get('valLoss') is not None else 'N/A'}
📊 Giải thích:
Đâ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.
''' if best_checkpoint else ''}
{f'''
📉 Worst Accuracy Checkpoint (Cận Dưới)
{worst_checkpoint.get('accuracy', 0) * 100:.2f}%
Accuracy thấp nhất tại epoch {worst_checkpoint.get('epoch', 'N/A')} (cận dưới training)
Train Accuracy
{(worst_checkpoint.get('trainAcc') or 0) * 100:.2f}%
Val Accuracy
{(worst_checkpoint.get('valAcc') or 0) * 100:.2f}%
Train Loss
{f"{worst_checkpoint.get('trainLoss'):.4f}" if worst_checkpoint.get('trainLoss') is not None else 'N/A'}
Val Loss
{f"{worst_checkpoint.get('valLoss'):.4f}" if worst_checkpoint.get('valLoss') is not None else 'N/A'}
📊 Giải thích:
Đâ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 ''}
''' if worst_checkpoint and 'accuracy' in worst_checkpoint else ''}
{f'''
📊 Performance Range & Random Baseline Analysis
Accuracy Range
{acc_range_pct:.2f}
% (max - min)
Max: {_bc.get('accuracy', 0) * 100:.2f}%
Min: {_wc.get('accuracy', 0) * 100:.2f}%
Average Accuracy
{acc_avg_pct:.2f}
%
Stability Score
{acc_stability:.1f}
%
{acc_stable_label}
Random Baseline
{rnd_bl_display}
% (lớp chiếm đa số)
{n_classes_display} classes{f'
Weighted: {weighted_random_baseline*100:.2f}%' if weighted_random_baseline else ''}
💡 Đánh giá:
• Range: {acc_range_pct:.2f}% - Model {"ổn định" if acc_range_pct < 5 else "có dao động"} trong quá trình training
• Worst Accuracy vs Random Baseline: {_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")}
• Stability: {acc_stability:.1f}% - Training {acc_stable_label}
• Best Accuracy: {_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"}
''' if best_checkpoint and worst_checkpoint and 'accuracy' in _bc and 'accuracy' in _wc else ''}
{f'''
🏆 Best PSNR Checkpoint
{best_checkpoint.get('modelPSNR', 0):.2f} dB
Model PSNR đạt tại epoch {best_checkpoint.get('epoch', 'N/A')}
Train Loss
{best_checkpoint.get('trainLoss', 0):.6f}
Val Loss
{best_checkpoint.get('valLoss', 0):.6f}
Baseline PSNR
{best_checkpoint.get('baselinePSNR', 0):.2f} dB
Improvement
{'+' if best_checkpoint.get('improvement', 0) > 0 else ''}{best_checkpoint.get('improvement', 0):.2f} dB
📊 Giải thích:
Đâ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ì).
''' if best_checkpoint and 'modelPSNR' in best_checkpoint else ''}
{f'''
📉 Worst PSNR Checkpoint (Cận Dưới)
{worst_checkpoint.get('modelPSNR', 0):.2f} dB
PSNR thấp nhất tại epoch {worst_checkpoint.get('epoch', 'N/A')} (cận dưới training)
Train Loss
{worst_checkpoint.get('trainLoss', 0):.6f}
Val Loss
{worst_checkpoint.get('valLoss', 0):.6f}
Baseline PSNR
{worst_checkpoint.get('baselinePSNR', 0):.2f} dB
Gap from Baseline
{'+' if worst_checkpoint.get('improvement', 0) > 0 else ''}{worst_checkpoint.get('improvement', 0):.2f} dB
📊 Giải thích:
Đâ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ì!
''' if worst_checkpoint and 'modelPSNR' in worst_checkpoint else ''}
{f'''
📊 Performance Range Analysis
PSNR Range
{(best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)):.2f}
dB (max - min)
Max: {best_checkpoint.get('modelPSNR', 0):.2f} dB
Min: {worst_checkpoint.get('modelPSNR', 0):.2f} dB
Average PSNR
{((best_checkpoint.get('modelPSNR', 0) + worst_checkpoint.get('modelPSNR', 0)) / 2):.2f}
dB (estimated)
Baseline PSNR
{worst_checkpoint.get('baselinePSNR', 0):.2f}
dB (cận dưới)
Cloudy vs Clean
💡 Baseline Comparison:
{baseline_comparison_msg}
• Range {'nhỏ' if (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) < 2 else 'lớn'} ({(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'}
• 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%: 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 ⚠️'}
''' if best_checkpoint and worst_checkpoint and 'modelPSNR' in best_checkpoint and 'modelPSNR' in worst_checkpoint else ''}
{f'''
🎯 PSNR Baseline Analysis
📈 Model PSNR:
{val_psnr:.2f} dB
📉 Baseline PSNR (cận dưới):
{baseline_psnr:.2f} dB
⚡ Improvement:
{'+' if improvement > 0 else ''}{improvement:.2f} dB ({'+' if improvement > 0 else ''}{(improvement/baseline_psnr*100):.1f}%)
📊 Đánh giá:
{'✅ 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!'}
Giải thích:
- Baseline PSNR: PSNR giữa ảnh cloudy và ảnh clean (không làm gì)
- Model PSNR: PSNR giữa output model và ảnh clean
- Improvement > 0: Model đang khử mây hiệu quả!
- Improvement < 0: Model làm tồi hơn không làm gì!
''' if val_psnr is not None and baseline_psnr is not None else ''}
⚙️ Cấu Hình Training
🤖 Model Type:
{model_type.upper()}
📍 Khu vực (bbox):
{bbox}
📅 Thời gian:
{time_range}
📐 Độ phân giải:
{resolution}m
💾 Model Path:
{model_path}
📊 Data Source:
{data_source}
{f'''
🛰️ Collections:
{', '.join(collections)}
''' if collections else ''}
🎯 Hyperparameters - Chi Tiết Tái Hiện
🔄 Để tái hiện kết quả training này, sử dụng các tham số dưới đây:
📊 Model Hyperparameters
Model Type:
{model_type}
{f'''
n_estimators:
{n_estimators}
''' if n_estimators != 'N/A' else ''}
{f'''
max_depth:
{max_depth}
''' if max_depth != 'N/A' else ''}
{f'''
learning_rate:
{learning_rate}
''' if learning_rate != 'N/A' else ''}
{f'''
batch_size:
{batch_size}
''' if batch_size != 'N/A' else ''}
{f'''
num_epochs:
{num_epochs}
''' if num_epochs != 'N/A' else ''}
use_gpu:
{use_gpu}
{f'''
use_s1 (Sentinel-1):
{use_s1}
''' if model_type in ['cloud_removal', 'unet'] else ''}
📦 Data Processing
test_size:
{test_size}
feature_mode:
{feature_mode}
n_features:
{n_features}
{f'''
features:
{', '.join(features)}
''' if features else ''}
🌍 Geo & Time Parameters
bbox:
{bbox}
time_range:
{time_range}
resolution:
{resolution}m
📝 Lưu ý:
• Lưu toàn bộ các tham số trên để reproduce kết quả
• Sử dụng cùng dataset và time range để đảm bảo tính nhất quán
• Random seed: 42 (mặc định)
• Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}
📋 Classification Report
| Loại đất |
Precision |
Recall |
F1-Score |
Support |
{cls_report_rows}
{'
' if metrics_img else ''}
{'
' if class_dist_img else ''}
🔢 Confusion Matrix
{'
' if conf_matrix_img else conf_matrix_table}
🏷️ Danh Sách Các Loại Đất
{''.join([f'- {cls}
' for cls in classes])}
"""
# 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"""
Prediction Report - {timestamp}
📈 Tóm Tắt Kết Quả
{total_pixels:,}
Tổng số Pixels
{shape[0]}x{shape[1]}
Kích thước (px)
{area_km2:.1f}
Diện tích (km²)
{len(unique_classes)}
Số Classes
{'✅' if used_radar else '❌'}
Sử dụng Radar
⚙️ Thông Tin Chi Tiết
🤖 Model sử dụng:
{model_used}
📍 Khu vực (bbox):
{bbox}
📅 Thời gian:
{time_range}
💾 Output file:
{output_file}
🏷️ Các Classes Phát Hiện
{''.join([f'{cls}' for cls in unique_classes])}
"""
# 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}")