update 01

This commit is contained in:
Victor Phan
2025-12-21 14:34:18 +07:00
parent 671a6f851b
commit 6104856031
29 changed files with 60073 additions and 4 deletions
+786
View File
@@ -0,0 +1,786 @@
"""
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}")