787 lines
27 KiB
Python
787 lines
27 KiB
Python
"""
|
|
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:
|
|
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:
|
|
# Extract support (number of samples) for each class
|
|
supports = []
|
|
for cls in class_names:
|
|
if cls in classification_report:
|
|
supports.append(classification_report[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, 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):
|
|
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
|
|
str(int(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:
|
|
precisions = []
|
|
recalls = []
|
|
f1_scores = []
|
|
|
|
for cls in class_names:
|
|
if cls in classification_report:
|
|
precisions.append(classification_report[cls].get('precision', 0))
|
|
recalls.append(classification_report[cls].get('recall', 0))
|
|
f1_scores.append(classification_report[cls].get('f1-score', 0))
|
|
else:
|
|
precisions.append(0)
|
|
recalls.append(0)
|
|
f1_scores.append(0)
|
|
|
|
x = np.arange(len(class_names))
|
|
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, 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 = training_result.get('classes', [])
|
|
cls_report = training_result.get('classification_report', {})
|
|
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)
|
|
|
|
# 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]
|
|
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>"
|
|
|
|
# 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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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">{model_path}</span>
|
|
</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}")
|