| """ |
| AgriFM PASTIS - Training visualization and analysis. |
| Generates all graphs, confusion matrix, prediction maps, and comparison plots. |
| |
| Usage: |
| python visualize_results.py \ |
| --work_dir ./work_dirs/fold1_v3 \ |
| --data_root /workspace/project/PASTIS \ |
| --fold 1 |
| """ |
|
|
| import os |
| import sys |
| import json |
| import argparse |
| import numpy as np |
| import torch |
| from pathlib import Path |
| from torch.utils.data import DataLoader |
| from torch.amp import autocast |
|
|
| sys.path.insert(0, str(Path(__file__).parent)) |
|
|
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| import matplotlib.patches as mpatches |
| import matplotlib.gridspec as gridspec |
| from matplotlib.colors import ListedColormap |
| import matplotlib.ticker as mticker |
| from sklearn.metrics import confusion_matrix |
| import seaborn as sns |
|
|
| from models.agrifm import build_agrifm_pastis_small, build_agrifm_pastis_tiny, build_agrifm_pastis |
| from datasets.pastis_dataset import PASTISDataset, PASTIS_CLASSES, IGNORE_INDEX |
| from losses.loss import CropCELoss |
| from evaluation.metrics import SegmentationMetrics |
|
|
|
|
| |
| |
| |
|
|
| CLASS_NAMES = [PASTIS_CLASSES[i] for i in range(20)] |
| SHORT_NAMES = [ |
| 'BG', 'Meadow', 'S.Wheat', 'Corn', 'W.Barley', 'W.Rape', |
| 'Sp.Barley', 'Sunflwr', 'Grapevn', 'Beet', 'W.Trit', |
| 'W.Durum', 'Fruits', 'Potato', 'Leg.Fod', 'Soybeans', |
| 'Orchard', 'Mixed', 'Sorghum', 'Void' |
| ] |
|
|
| |
| CLASS_COLORS = [ |
| '#000000', |
| '#3cb371', |
| '#ffd700', |
| '#ff8c00', |
| '#8b4513', |
| '#ff1493', |
| '#adff2f', |
| '#ffff00', |
| '#800080', |
| '#dc143c', |
| '#00bfff', |
| '#daa520', |
| '#32cd32', |
| '#a0522d', |
| '#90ee90', |
| '#006400', |
| '#ff7f50', |
| '#87ceeb', |
| '#bc8f8f', |
| '#808080', |
| ] |
|
|
| CMAP = ListedColormap(CLASS_COLORS) |
|
|
|
|
| def get_args(): |
| p = argparse.ArgumentParser() |
| p.add_argument('--work_dir', default='./work_dirs/fold1_v3') |
| p.add_argument('--data_root', default='/workspace/project/PASTIS') |
| p.add_argument('--fold', type=int, default=1) |
| p.add_argument('--model_size', default='small', |
| choices=['small','tiny','base']) |
| p.add_argument('--num_classes',type=int, default=20) |
| p.add_argument('--num_frames', type=int, default=32) |
| p.add_argument('--batch_size', type=int, default=8) |
| p.add_argument('--num_workers',type=int, default=4) |
| p.add_argument('--n_samples', type=int, default=6, |
| help='Number of sample prediction maps to show') |
| p.add_argument('--out_dir', default=None, |
| help='Output dir for plots (default: work_dir/plots)') |
| return p.parse_args() |
|
|
|
|
| |
| |
| |
|
|
| def plot_training_curves(log_data, out_dir): |
| print(" Plotting training curves...") |
| epochs = [d['epoch'] for d in log_data] |
| train_loss = [d['train_loss'] for d in log_data] |
| val_loss = [d.get('val_loss', None) for d in log_data] |
| mfscore = [d.get('mFscore', None) for d in log_data] |
| miou = [d.get('mIoU', None) for d in log_data] |
| oa = [d.get('OA', None) for d in log_data] |
| kappa = [d.get('Kappa', None) for d in log_data] |
| prec = [d.get('mPrecision', None) for d in log_data] |
| rec = [d.get('mRecall', None) for d in log_data] |
|
|
| fig, axes = plt.subplots(2, 3, figsize=(18, 10)) |
| fig.suptitle('AgriFM × PASTIS — Training History', fontsize=16, fontweight='bold') |
|
|
| |
| ax = axes[0, 0] |
| ax.plot(epochs, train_loss, 'b-', linewidth=2, label='Train Loss') |
| vl = [v for v in val_loss if v is not None] |
| ve = [e for e, v in zip(epochs, val_loss) if v is not None] |
| ax.plot(ve, vl, 'r-', linewidth=2, label='Val Loss') |
| ax.set_title('Loss Curves', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('Loss') |
| ax.legend() |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| |
| ax = axes[0, 1] |
| mf = [v for v in mfscore if v is not None] |
| me = [e for e, v in zip(epochs, mfscore) if v is not None] |
| ax.plot(me, mf, 'g-', linewidth=2, label='mFscore') |
| best_epoch = me[mf.index(max(mf))] |
| best_val = max(mf) |
| ax.axhline(best_val, color='g', linestyle='--', alpha=0.5) |
| ax.annotate(f'Best: {best_val:.1f}%\n@ epoch {best_epoch}', |
| xy=(best_epoch, best_val), |
| xytext=(best_epoch + len(me)*0.05, best_val - 5), |
| fontsize=9, color='green', |
| arrowprops=dict(arrowstyle='->', color='green', lw=1.5)) |
| ax.set_title('mFscore (F1)', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('mFscore (%)') |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| |
| ax = axes[0, 2] |
| mi = [v for v in miou if v is not None] |
| ax.plot(me, mi, 'orange', linewidth=2, label='mIoU') |
| ax.set_title('Mean IoU', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('mIoU (%)') |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| |
| ax = axes[1, 0] |
| oa_v = [v for v in oa if v is not None] |
| ka_v = [v for v in kappa if v is not None] |
| ax.plot(me, oa_v, 'purple', linewidth=2, label='OA') |
| ax.plot(me, ka_v, 'brown', linewidth=2, label='Kappa') |
| ax.set_title('Overall Accuracy & Kappa', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('%') |
| ax.legend() |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| |
| ax = axes[1, 1] |
| pr_v = [v for v in prec if v is not None] |
| re_v = [v for v in rec if v is not None] |
| ax.plot(me, pr_v, 'teal', linewidth=2, label='mPrecision') |
| ax.plot(me, re_v, 'salmon', linewidth=2, label='mRecall') |
| ax.set_title('Precision vs Recall', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('%') |
| ax.legend() |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| |
| ax = axes[1, 2] |
| for vals, name, color in [ |
| (mf, 'mFscore', 'green'), |
| (mi, 'mIoU', 'orange'), |
| (oa_v, 'OA', 'purple'), |
| (ka_v, 'Kappa', 'brown'), |
| ]: |
| ax.plot(me, vals, linewidth=2, label=name, color=color) |
| ax.set_title('All Metrics', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('%') |
| ax.legend(fontsize=8) |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| plt.tight_layout() |
| path = os.path.join(out_dir, '1_training_curves.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def plot_per_class_iou(test_results, out_dir): |
| print(" Plotting per-class IoU...") |
| per_cls = test_results['per_class_iou'] |
|
|
| |
| items = [(k, v) for k, v in per_cls.items()] |
| items = sorted(items, key=lambda x: -x[1]) |
| names = [x[0] for x in items] |
| values = [x[1] for x in items] |
| colors = [CLASS_COLORS[CLASS_NAMES.index(n)] if n in CLASS_NAMES |
| else '#888888' for n in names] |
| |
| colors = ['#444444' if c == '#000000' else c for c in colors] |
|
|
| fig, ax = plt.subplots(figsize=(14, 7)) |
| bars = ax.barh(range(len(names)), values, color=colors, |
| edgecolor='white', linewidth=0.5) |
|
|
| |
| for i, (bar, val) in enumerate(zip(bars, values)): |
| ax.text(val + 0.5, i, f'{val:.1f}%', |
| va='center', fontsize=9, fontweight='bold') |
|
|
| |
| ax.axvline(50, color='gray', linestyle='--', alpha=0.5, linewidth=1) |
| ax.axvline(25, color='gray', linestyle=':', alpha=0.5, linewidth=1) |
|
|
| ax.set_yticks(range(len(names))) |
| ax.set_yticklabels(names, fontsize=10) |
| ax.set_xlabel('IoU (%)', fontsize=12) |
| ax.set_title('Per-Class IoU — Test Set\n' |
| f'(mIoU = {test_results["test_metrics"]["mIoU"]:.2f}% ' |
| f'mFscore = {test_results["test_metrics"]["mFscore"]:.2f}%)', |
| fontsize=13, fontweight='bold') |
| ax.set_xlim(0, 100) |
| ax.grid(True, axis='x', alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| |
| ax.text(26, -1.2, '25%', fontsize=8, color='gray', ha='center') |
| ax.text(51, -1.2, '50%', fontsize=8, color='gray', ha='center') |
|
|
| plt.tight_layout() |
| path = os.path.join(out_dir, '2_per_class_iou.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def plot_radar(test_results, out_dir): |
| print(" Plotting radar chart...") |
| metrics = test_results['test_metrics'] |
| keys = ['OA', 'mIoU', 'mFscore', 'mPrecision', 'mRecall', 'Kappa'] |
| values = [metrics[k] for k in keys] |
|
|
| angles = np.linspace(0, 2*np.pi, len(keys), endpoint=False).tolist() |
| values_ = values + [values[0]] |
| angles_ = angles + [angles[0]] |
| labels = keys + [keys[0]] |
|
|
| fig, ax = plt.subplots(figsize=(8, 8), |
| subplot_kw=dict(polar=True)) |
|
|
| ax.plot(angles_, values_, 'o-', linewidth=2, |
| color='#2196F3', markersize=8) |
| ax.fill(angles_, values_, alpha=0.25, color='#2196F3') |
|
|
| ax.set_xticks(angles) |
| ax.set_xticklabels(keys, fontsize=12, fontweight='bold') |
| ax.set_ylim(0, 100) |
| ax.set_yticks([20, 40, 60, 80, 100]) |
| ax.set_yticklabels(['20%','40%','60%','80%','100%'], |
| fontsize=8, color='gray') |
| ax.grid(color='gray', linestyle='--', linewidth=0.5, alpha=0.7) |
|
|
| |
| for angle, val, key in zip(angles, values, keys): |
| ax.annotate(f'{val:.1f}%', |
| xy=(angle, val), |
| xytext=(angle, val + 5), |
| ha='center', fontsize=10, fontweight='bold', |
| color='#1565C0') |
|
|
| ax.set_title('Test Set Metrics Overview', |
| fontsize=14, fontweight='bold', pad=20) |
|
|
| plt.tight_layout() |
| path = os.path.join(out_dir, '3_metrics_radar.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def plot_confusion_matrix(model, loader, device, args, out_dir): |
| print(" Computing confusion matrix...") |
| model.eval() |
| all_preds = [] |
| all_labels = [] |
|
|
| with torch.no_grad(): |
| for batch in loader: |
| s2 = batch['S2'].to(device) |
| label = batch['label'] |
| with autocast('cuda', enabled=True): |
| logits = model(s2) |
| pred = logits.argmax(dim=1).cpu().numpy() |
| lbl = label.numpy() |
| mask = lbl != IGNORE_INDEX |
| all_preds.append(pred[mask]) |
| all_labels.append(lbl[mask]) |
|
|
| all_preds = np.concatenate(all_preds) |
| all_labels = np.concatenate(all_labels) |
|
|
| |
| present = sorted(set(all_labels.tolist()) | set(all_preds.tolist())) |
| present = [c for c in present if c != IGNORE_INDEX] |
|
|
| cm = confusion_matrix(all_labels, all_preds, labels=present) |
| cm_norm= cm.astype(float) / (cm.sum(axis=1, keepdims=True) + 1e-8) |
|
|
| short = [SHORT_NAMES[c] for c in present] |
| n = len(present) |
| figsize= max(12, n * 0.7) |
|
|
| fig, axes = plt.subplots(1, 2, figsize=(figsize*2 + 2, figsize)) |
|
|
| |
| ax = axes[0] |
| im = ax.imshow(cm, cmap='Blues') |
| ax.set_xticks(range(n)); ax.set_xticklabels(short, rotation=45, ha='right', fontsize=8) |
| ax.set_yticks(range(n)); ax.set_yticklabels(short, fontsize=8) |
| ax.set_title('Confusion Matrix (counts)', fontweight='bold', fontsize=12) |
| ax.set_xlabel('Predicted'); ax.set_ylabel('True') |
| plt.colorbar(im, ax=ax, shrink=0.8) |
|
|
| |
| ax = axes[1] |
| im2= ax.imshow(cm_norm, cmap='Blues', vmin=0, vmax=1) |
| ax.set_xticks(range(n)); ax.set_xticklabels(short, rotation=45, ha='right', fontsize=8) |
| ax.set_yticks(range(n)); ax.set_yticklabels(short, fontsize=8) |
| |
| for i in range(n): |
| for j in range(n): |
| val = cm_norm[i, j] |
| if val > 0.05: |
| color = 'white' if val > 0.5 else 'black' |
| ax.text(j, i, f'{val:.2f}', ha='center', va='center', |
| fontsize=6, color=color) |
| ax.set_title('Confusion Matrix (normalized)', fontweight='bold', fontsize=12) |
| ax.set_xlabel('Predicted'); ax.set_ylabel('True') |
| plt.colorbar(im2, ax=ax, shrink=0.8) |
|
|
| plt.suptitle('AgriFM × PASTIS — Test Set Confusion Matrix', |
| fontsize=14, fontweight='bold') |
| plt.tight_layout() |
| path = os.path.join(out_dir, '4_confusion_matrix.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def plot_prediction_maps(model, dataset, device, args, out_dir): |
| print(f" Plotting {args.n_samples} prediction maps...") |
| model.eval() |
|
|
| |
| np.random.seed(42) |
| indices = np.random.choice(len(dataset), args.n_samples, replace=False) |
|
|
| fig, axes = plt.subplots(args.n_samples, 4, |
| figsize=(20, 5 * args.n_samples)) |
| if args.n_samples == 1: |
| axes = axes[np.newaxis, :] |
|
|
| fig.suptitle('AgriFM × PASTIS — Prediction Examples\n' |
| '(RGB | Ground Truth | Prediction | Difference)', |
| fontsize=14, fontweight='bold') |
|
|
| for row, idx in enumerate(indices): |
| sample = dataset[idx] |
| s2 = sample['S2'] |
| label = sample['label'].numpy() |
|
|
| |
| with torch.no_grad(): |
| inp = s2.unsqueeze(0).to(device) |
| with autocast('cuda', enabled=True): |
| logits = model(inp) |
| pred = logits.argmax(dim=1).squeeze(0).cpu().numpy() |
|
|
| |
| mid = s2.shape[0] // 2 |
| rgb = s2[mid, [2,1,0], :, :].numpy() |
| |
| rgb = (rgb * 0.3 + 0.5).clip(0, 1) |
| rgb = np.transpose(rgb, (1, 2, 0)) |
|
|
| |
| pred_show = pred.copy() |
| label_show = label.copy() |
| pred_show[label == IGNORE_INDEX] = IGNORE_INDEX |
| label_show[label == IGNORE_INDEX] = IGNORE_INDEX |
|
|
| |
| diff = np.zeros_like(pred) |
| diff[pred_show == label_show] = 1 |
| diff[pred_show != label_show] = 0 |
| diff[label == IGNORE_INDEX] = 2 |
|
|
| pid = dataset.ids[idx] |
|
|
| |
| axes[row,0].imshow(rgb) |
| axes[row,0].set_title(f'Patch {pid} — RGB (T={mid})', |
| fontsize=9) |
| axes[row,0].axis('off') |
|
|
| |
| axes[row,1].imshow(label_show, cmap=CMAP, vmin=0, vmax=19, |
| interpolation='nearest') |
| axes[row,1].set_title('Ground Truth', fontsize=9) |
| axes[row,1].axis('off') |
|
|
| |
| axes[row,2].imshow(pred_show, cmap=CMAP, vmin=0, vmax=19, |
| interpolation='nearest') |
| axes[row,2].set_title('Prediction', fontsize=9) |
| axes[row,2].axis('off') |
|
|
| |
| diff_cmap = ListedColormap(['#e74c3c','#2ecc71','#95a5a6']) |
| axes[row,3].imshow(diff, cmap=diff_cmap, vmin=0, vmax=2, |
| interpolation='nearest') |
| |
| valid = label != IGNORE_INDEX |
| if valid.sum() > 0: |
| acc = (pred[valid] == label[valid]).mean() * 100 |
| axes[row,3].set_title(f'Diff (acc={acc:.1f}%)\n' |
| f'■ Wrong ■ Correct ■ Void', |
| fontsize=9) |
| axes[row,3].axis('off') |
|
|
| |
| patches = [] |
| for c in range(19): |
| col = CLASS_COLORS[c] |
| if col == '#000000': col = '#333333' |
| patches.append(mpatches.Patch(color=col, |
| label=f'{c}: {CLASS_NAMES[c]}')) |
| fig.legend(handles=patches, loc='lower center', |
| ncol=7, fontsize=7, |
| bbox_to_anchor=(0.5, -0.01), |
| framealpha=0.9) |
|
|
| plt.tight_layout(rect=[0, 0.04, 1, 1]) |
| path = os.path.join(out_dir, '5_prediction_maps.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def plot_freq_vs_iou(test_results, out_dir): |
| print(" Plotting frequency vs IoU scatter...") |
| per_cls = test_results['per_class_iou'] |
|
|
| |
| fig, ax = plt.subplots(figsize=(11, 7)) |
|
|
| ious = list(per_cls.values()) |
| names = list(per_cls.keys()) |
| colors = [CLASS_COLORS[CLASS_NAMES.index(n)] if n in CLASS_NAMES |
| else '#888' for n in names] |
| colors = ['#444444' if c == '#000000' else c for c in colors] |
|
|
| scatter = ax.scatter(range(len(names)), ious, |
| c=colors, s=200, zorder=5, |
| edgecolors='white', linewidths=1.5) |
|
|
| for i, (name, iou) in enumerate(zip(names, ious)): |
| ax.annotate(f'{iou:.1f}%', |
| xy=(i, iou), |
| xytext=(i, iou + 1.5), |
| ha='center', fontsize=8, fontweight='bold') |
|
|
| ax.axhline(np.mean(ious), color='red', linestyle='--', |
| linewidth=2, label=f'mIoU = {np.mean(ious):.1f}%') |
| ax.axhline(50, color='gray', linestyle=':', alpha=0.7, |
| label='50% threshold') |
|
|
| ax.set_xticks(range(len(names))) |
| ax.set_xticklabels(names, rotation=45, ha='right', fontsize=9) |
| ax.set_ylabel('IoU (%)', fontsize=12) |
| ax.set_title('Per-Class IoU Overview\n(sorted by class index)', |
| fontsize=13, fontweight='bold') |
| ax.legend(fontsize=10) |
| ax.grid(True, axis='y', alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
| ax.set_ylim(0, 100) |
|
|
| plt.tight_layout() |
| path = os.path.join(out_dir, '6_class_iou_scatter.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def plot_overfitting_analysis(log_data, out_dir): |
| print(" Plotting overfitting analysis...") |
| epochs = [d['epoch'] for d in log_data] |
| train_loss = [d['train_loss'] for d in log_data] |
| val_loss = [d.get('val_loss', None) for d in log_data] |
|
|
| ve = [e for e, v in zip(epochs, val_loss) if v is not None] |
| tl = [t for t, v in zip(train_loss, val_loss) if v is not None] |
| vl = [v for v in val_loss if v is not None] |
| gap= [v - t for t, v in zip(tl, vl)] |
|
|
| fig, axes = plt.subplots(1, 2, figsize=(14, 5)) |
| fig.suptitle('Overfitting Analysis', fontsize=14, fontweight='bold') |
|
|
| |
| ax = axes[0] |
| ax.plot(ve, tl, 'b-', linewidth=2, label='Train Loss') |
| ax.plot(ve, vl, 'r-', linewidth=2, label='Val Loss') |
| ax.fill_between(ve, tl, vl, |
| where=[v > t for t, v in zip(tl, vl)], |
| alpha=0.15, color='red', label='Overfit gap') |
| ax.set_title('Train vs Val Loss', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('Loss') |
| ax.legend() |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| |
| ax = axes[1] |
| ax.plot(ve, gap, 'purple', linewidth=2) |
| ax.fill_between(ve, 0, gap, |
| where=[g > 0 for g in gap], |
| alpha=0.3, color='red', label='Overfit') |
| ax.fill_between(ve, 0, gap, |
| where=[g <= 0 for g in gap], |
| alpha=0.3, color='green', label='Underfit') |
| ax.axhline(0, color='black', linewidth=1) |
| ax.set_title('Val Loss − Train Loss (gap)', fontweight='bold') |
| ax.set_xlabel('Epoch') |
| ax.set_ylabel('Loss Gap') |
| ax.legend() |
| ax.grid(True, alpha=0.3) |
| ax.set_facecolor('#f8f8f8') |
|
|
| plt.tight_layout() |
| path = os.path.join(out_dir, '7_overfitting_analysis.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def plot_summary_card(test_results, log_data, out_dir): |
| print(" Plotting summary card...") |
| metrics = test_results['test_metrics'] |
| args_d = test_results.get('args', {}) |
|
|
| fig = plt.figure(figsize=(16, 9)) |
| fig.patch.set_facecolor('#1a1a2e') |
|
|
| gs = gridspec.GridSpec(3, 4, figure=fig, |
| hspace=0.5, wspace=0.4) |
|
|
| title_color = '#e0e0e0' |
| val_color = '#00d4ff' |
| bg_card_color = '#16213e' |
|
|
| def add_card(ax, title, value, unit='%', color=val_color): |
| ax.set_facecolor(bg_card_color) |
| ax.set_xticks([]); ax.set_yticks([]) |
| for spine in ax.spines.values(): |
| spine.set_edgecolor('#0f3460') |
| spine.set_linewidth(2) |
| ax.text(0.5, 0.65, f'{value}{unit}', |
| transform=ax.transAxes, |
| ha='center', va='center', |
| fontsize=22, fontweight='bold', color=color) |
| ax.text(0.5, 0.25, title, |
| transform=ax.transAxes, |
| ha='center', va='center', |
| fontsize=10, color='#a0a0a0') |
|
|
| |
| card_data = [ |
| ('mFscore', f"{metrics['mFscore']:.1f}", '#00d4ff'), |
| ('mIoU', f"{metrics['mIoU']:.1f}", '#00ff88'), |
| ('OA', f"{metrics['OA']:.1f}", '#ffaa00'), |
| ('Kappa', f"{metrics['Kappa']:.1f}", '#ff6b6b'), |
| ('mPrecision', f"{metrics['mPrecision']:.1f}", '#c084fc'), |
| ('mRecall', f"{metrics['mRecall']:.1f}", '#fb923c'), |
| ] |
| positions = [(0,0),(0,1),(0,2),(0,3),(1,0),(1,1)] |
| for (r,c), (name, val, color) in zip(positions, card_data): |
| ax = fig.add_subplot(gs[r, c]) |
| add_card(ax, name, val, '%', color) |
|
|
| |
| ax = fig.add_subplot(gs[1, 2:]) |
| ax.set_facecolor(bg_card_color) |
| for spine in ax.spines.values(): |
| spine.set_edgecolor('#0f3460'); spine.set_linewidth(2) |
|
|
| mf = [d.get('mFscore', None) for d in log_data] |
| me = [d['epoch'] for d, v in zip(log_data, mf) if v is not None] |
| mf = [v for v in mf if v is not None] |
| tl = [d['train_loss'] for d in log_data] |
| vl = [d.get('val_loss', None) for d in log_data] |
| ve = [d['epoch'] for d, v in zip(log_data, vl) if v is not None] |
| vl = [v for v in vl if v is not None] |
|
|
| ax2 = ax.twinx() |
| ax.plot(me, mf, color='#00d4ff', linewidth=2, label='mFscore') |
| ax2.plot([d['epoch'] for d in log_data], tl, |
| color='#ff6b6b', linewidth=1.5, alpha=0.7, label='Train Loss') |
| ax2.plot(ve, vl, |
| color='#ffaa00', linewidth=1.5, alpha=0.7, label='Val Loss') |
| ax.set_ylabel('mFscore (%)', color='#00d4ff', fontsize=9) |
| ax2.set_ylabel('Loss', color='#ffaa00', fontsize=9) |
| ax.set_xlabel('Epoch', color=title_color, fontsize=9) |
| ax.tick_params(colors=title_color) |
| ax2.tick_params(colors=title_color) |
| ax.set_title('Training Progress', |
| color=title_color, fontsize=10, fontweight='bold') |
| ax.set_facecolor(bg_card_color) |
| ax.grid(True, alpha=0.2, color='white') |
|
|
| |
| ax = fig.add_subplot(gs[2, :]) |
| ax.set_facecolor(bg_card_color) |
| for spine in ax.spines.values(): |
| spine.set_edgecolor('#0f3460'); spine.set_linewidth(2) |
|
|
| per_cls = test_results['per_class_iou'] |
| items = sorted(per_cls.items(), key=lambda x: -x[1]) |
| names = [x[0][:10] for x in items] |
| vals = [x[1] for x in items] |
| cols = [CLASS_COLORS[CLASS_NAMES.index(x[0])] |
| if x[0] in CLASS_NAMES else '#888' for x in items] |
| cols = ['#444444' if c == '#000000' else c for c in cols] |
|
|
| bars = ax.bar(range(len(names)), vals, color=cols, |
| edgecolor='#1a1a2e', linewidth=0.5) |
| ax.axhline(np.mean(vals), color='white', linestyle='--', |
| alpha=0.7, linewidth=1) |
| ax.set_xticks(range(len(names))) |
| ax.set_xticklabels(names, rotation=45, ha='right', |
| fontsize=7, color=title_color) |
| ax.set_ylabel('IoU (%)', color=title_color, fontsize=9) |
| ax.tick_params(colors=title_color) |
| ax.set_title('Per-Class IoU (sorted)', |
| color=title_color, fontsize=10, fontweight='bold') |
| ax.set_facecolor(bg_card_color) |
| ax.set_ylim(0, 100) |
| ax.grid(True, axis='y', alpha=0.2, color='white') |
|
|
| fig.text(0.5, 0.97, |
| 'AgriFM × PASTIS — Training Summary', |
| ha='center', fontsize=16, fontweight='bold', |
| color=title_color) |
| fig.text(0.5, 0.935, |
| f'Model: small (39.6M params) | ' |
| f'Fold: {args_d.get("fold","1")} | ' |
| f'Epochs: {len(log_data)} | ' |
| f'Batch: {args_d.get("batch_size",16)} | ' |
| f'LR: {args_d.get("lr","5e-5")}', |
| ha='center', fontsize=10, color='#a0a0a0') |
|
|
| plt.tight_layout(rect=[0, 0, 1, 0.93]) |
| path = os.path.join(out_dir, '0_summary_card.png') |
| plt.savefig(path, dpi=150, bbox_inches='tight', |
| facecolor='#1a1a2e') |
| plt.close() |
| print(f" Saved: {path}") |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| args = get_args() |
| out_dir = args.out_dir or os.path.join(args.work_dir, 'plots') |
| os.makedirs(out_dir, exist_ok=True) |
|
|
| print(f"\nAgriFM PASTIS — Visualization") |
| print(f"Work dir : {args.work_dir}") |
| print(f"Out dir : {out_dir}") |
| print(f"{'─'*50}") |
|
|
| |
| log_path = os.path.join(args.work_dir, 'log.json') |
| with open(log_path) as f: |
| log_data = json.load(f) |
| print(f"Loaded {len(log_data)} epochs from log.json") |
|
|
| |
| res_path = os.path.join(args.work_dir, 'test_results.json') |
| with open(res_path) as f: |
| test_results = json.load(f) |
| print(f"Loaded test results: mFscore={test_results['test_metrics']['mFscore']}%") |
|
|
| |
| try: |
| import seaborn as sns |
| sns.set_style("whitegrid") |
| except ImportError: |
| pass |
|
|
| |
| print("\nGenerating plots...") |
| plot_summary_card(test_results, log_data, out_dir) |
| plot_training_curves(log_data, out_dir) |
| plot_per_class_iou(test_results, out_dir) |
| plot_radar(test_results, out_dir) |
| plot_freq_vs_iou(test_results, out_dir) |
| plot_overfitting_analysis(log_data, out_dir) |
|
|
| |
| print("\nLoading model for prediction maps...") |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
|
|
| if args.model_size == 'small': |
| model = build_agrifm_pastis_small(num_classes=args.num_classes) |
| elif args.model_size == 'tiny': |
| model = build_agrifm_pastis_tiny(num_classes=args.num_classes) |
| else: |
| model = build_agrifm_pastis(num_classes=args.num_classes) |
|
|
| ckpt = torch.load( |
| os.path.join(args.work_dir, 'best_model.pth'), |
| map_location=device, weights_only=False |
| ) |
| model.load_state_dict(ckpt['model']) |
| model = model.to(device) |
| model.eval() |
| print(f"Loaded best model (epoch {ckpt.get('epoch','?')}, " |
| f"mFscore={ckpt.get('best_mfscore','?'):.2f}%)") |
|
|
| |
| test_ds = PASTISDataset( |
| args.data_root, fold=args.fold, split='test', |
| num_frames=args.num_frames, augment=False |
| ) |
| test_loader = DataLoader( |
| test_ds, batch_size=args.batch_size, |
| shuffle=False, num_workers=args.num_workers, |
| pin_memory=True |
| ) |
|
|
| plot_confusion_matrix(model, test_loader, device, args, out_dir) |
| plot_prediction_maps(model, test_ds, device, args, out_dir) |
|
|
| print(f"\n{'═'*50}") |
| print(f"All plots saved to: {out_dir}") |
| print(f"\nFiles created:") |
| for f in sorted(os.listdir(out_dir)): |
| if f.endswith('.png'): |
| size = os.path.getsize(os.path.join(out_dir, f)) / 1024 |
| print(f" {f} ({size:.0f} KB)") |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|