""" 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""" {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"" conf_matrix_table += "" for i, row in enumerate(conf_matrix): conf_matrix_table += f"" for val in row: conf_matrix_table += f"" conf_matrix_table += "" conf_matrix_table += "
{cls}
{classes[i]}{val}
" # HTML Template html = f""" Training Report - {timestamp}

📊 Báo Cáo Training Model

Land Classification - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}

📈 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

⚙️ 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}

📋 Classification Report

{cls_report_rows}
Loại đất Precision Recall F1-Score Support
{'

📊 Biểu Đồ Metrics

Metrics Chart
' if metrics_img else ''} {'

📊 Phân Bố Số Mẫu

Class Distribution
' if class_dist_img else ''}

🔢 Confusion Matrix

{'
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}

🗺️ Báo Cáo Dự Đoán

Land Classification Prediction - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}

📈 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
{n_features}
Số Features
{'✅' 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}")