agrobot_base/Python/OAK/datasets/_11_validate_onnx.py

1519 lines
53 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
_11_validate_visual_onnx.py
Valida fidelidade entre:
- checkpoint PyTorch .pt do Visual Worker
- modelo ONNX .onnx exportado pelo _10_export_visual_onnx.py
Contrato esperado:
- Entrada RGB [N, 3, H, W], float32
- Cabeça semântica: semantic_logits ou semantic_mask
- Cabeça de status: label_logits ou label_probs
Valida:
- Segmentação PyTorch vs ONNX:
logits/prob diff, argmax_equal, mIoU entre predições
- Label/status PyTorch vs ONNX:
logits/probs diff, top1_equal, classe prevista, confiança
Exemplos:
# ONNX exportado com --include-norm e saída semantic_logits + label_probs
python _11_validate_visual_onnx.py ^
--config config.json ^
--device cuda ^
--onnx_provider cuda ^
--torch_no_amp ^
--onnx_has_norm ^
--semantic_output_kind logits ^
--label_output_kind probs ^
--compare_at_input_size
# TensorRT
python _11_validate_visual_onnx.py ^
--config config.json ^
--device cuda ^
--onnx_provider tensorrt ^
--torch_no_amp ^
--onnx_has_norm ^
--semantic_output_kind logits ^
--label_output_kind probs ^
--compare_at_input_size
"""
from __future__ import annotations
import os
import glob
import json
import time
import argparse
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import cv2
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import SegformerForSemanticSegmentation
# ============================================================
# Utils básicos
# ============================================================
def load_json(path: str | Path) -> dict:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path: str | Path, data: dict):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def resolve_path(path_like: Optional[str], base: Optional[Path] = None) -> Optional[Path]:
if path_like is None or str(path_like).strip() == "":
return None
p = Path(path_like)
if p.is_absolute():
return p
if base is None:
base = Path.cwd()
return (base / p).resolve()
def _try_int(text: str) -> Optional[int]:
try:
return int(str(text).strip())
except Exception:
return None
def clean_label_name(raw_name: str) -> str:
name = str(raw_name).strip()
if "::" in name:
name = name.split("::", 1)[0].strip()
if ":" in name:
left, right = name.split(":", 1)
right_clean = right.replace(",", "").replace(" ", "")
if right_clean.isdigit():
name = left.strip()
return name.strip()
def load_labelmap(labelmap_path: Path) -> Tuple[Dict[int, str], Dict[str, int], int]:
if not labelmap_path.is_file():
raise FileNotFoundError(f"Labelmap não encontrado: {labelmap_path}")
id2label: Dict[int, str] = {}
ignore_index = 255
next_id = 0
with labelmap_path.open("r", encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
lower = line.lower()
if lower.startswith("ignore") or lower.startswith("ignore_index"):
for sep in ("=", ":", ",", " "):
if sep in line:
maybe = _try_int(line.split(sep)[-1])
if maybe is not None:
ignore_index = maybe
break
continue
cls_id: Optional[int] = None
cls_name: Optional[str] = None
# Exemplo: navegavel:0,128,0::
if "::" in line and ":" in line:
before = line.split("::", 1)[0].strip()
maybe_name = before.split(":", 1)[0].strip()
if maybe_name:
cls_id = next_id
cls_name = maybe_name
if cls_id is None:
for sep in (":", ",", "\t", " "):
if sep in line:
parts = [p.strip() for p in line.split(sep) if p.strip()]
if len(parts) >= 2:
left_id = _try_int(parts[0])
right_id = _try_int(parts[-1])
if left_id is not None:
cls_id = left_id
cls_name = sep.join(parts[1:]).strip() if sep in (":", ",") else " ".join(parts[1:]).strip()
break
if right_id is not None:
cls_id = right_id
cls_name = sep.join(parts[:-1]).strip() if sep in (":", ",") else " ".join(parts[:-1]).strip()
break
if cls_id is None:
cls_id = next_id
cls_name = line
if cls_name is None or cls_name == "":
raise RuntimeError(f"Linha inválida no labelmap: {raw_line!r}")
id2label[int(cls_id)] = clean_label_name(str(cls_name))
next_id = max(next_id, int(cls_id) + 1)
if not id2label:
raise RuntimeError(f"Labelmap vazio ou inválido: {labelmap_path}")
ids_sorted = sorted(id2label.keys())
if ids_sorted != list(range(len(ids_sorted))):
remap = {old_id: new_id for new_id, old_id in enumerate(ids_sorted)}
id2label = {remap[old_id]: name for old_id, name in id2label.items()}
label2id = {name: idx for idx, name in id2label.items()}
return id2label, label2id, ignore_index
def get_visual_mode(config: dict) -> str:
use_mask2 = bool(config.get("dual_head_mask", config.get("dual_head", False)))
use_label = bool(config.get("dual_head_label", False))
if use_mask2 and use_label:
raise RuntimeError("Config inválido: dual_head_mask e dual_head_label ativos juntos.")
if use_label:
return "label"
if use_mask2:
return "mask2"
return "single"
def get_save_suffix(mode: str) -> str:
if mode == "single":
return "_single"
if mode == "mask2":
return "_dual_mask"
if mode == "label":
return "_dual_label"
raise RuntimeError(f"Modo desconhecido: {mode}")
def resolve_labelmap_path(args, config: dict, config_dir: Path) -> Path:
explicit = resolve_path(args.labelmap, Path.cwd())
if explicit is not None:
return explicit.resolve()
camera = str(config.get("camera", "oak-d"))
return (config_dir / camera / "dataset" / "labelmap.txt").resolve()
def resolve_default_paths(args, config: dict, config_dir: Path) -> Tuple[Path, Path, str, str, Path]:
camera = str(config.get("camera", "oak-d"))
model_key = str(config.get("modelo", "segformer_b0"))
model_name = str(config.get("model_name", "visual"))
mode = get_visual_mode(config)
suffix = get_save_suffix(mode)
save_dir = config_dir / camera / "backup" / model_key / f"{model_name}{suffix}"
if mode == "label":
default_ckpt_name = "best_label"
elif mode == "mask2":
default_ckpt_name = "best_mask2"
else:
default_ckpt_name = "best_main"
ckpt_name = str(config.get("ckpt_test", default_ckpt_name))
checkpoint_path = resolve_path(args.checkpoint, Path.cwd())
if checkpoint_path is None:
checkpoint_path = save_dir / f"{ckpt_name}.pt"
onnx_path = resolve_path(args.onnx, Path.cwd())
if onnx_path is None:
onnx_path = save_dir / f"{ckpt_name}.onnx"
if not checkpoint_path.is_file():
raise FileNotFoundError(
f"Checkpoint não encontrado: {checkpoint_path}\n"
f"Dica: informe --checkpoint ou ajuste config['ckpt_test']."
)
if not onnx_path.is_file():
raise FileNotFoundError(
f"ONNX não encontrado: {onnx_path}\n"
f"Dica: informe --onnx ou exporte antes com _10_export_visual_onnx.py."
)
return checkpoint_path.resolve(), onnx_path.resolve(), ckpt_name, mode, save_dir.resolve()
def resolve_norm_stats_path(args, config: dict, config_dir: Path, save_dir: Path) -> Optional[Path]:
explicit = resolve_path(args.norm_stats, Path.cwd())
if explicit is not None:
return explicit.resolve()
p = save_dir / "norm_stats.json"
if p.is_file():
return p.resolve()
W, H = config.get("resolucao", [1024, 640])
camera = str(config.get("camera", "oak-d"))
p = config_dir / camera / "dataset" / f"{int(W)}x{int(H)}" / "group" / "norm_stats.json"
if p.is_file():
return p.resolve()
return p.resolve()
def load_rgb_norm_stats(path: Optional[Path]) -> Tuple[Optional[List[float]], Optional[List[float]], Optional[str], List[str]]:
if path is None or not path.is_file():
if path is not None:
print(f"[NORM] norm_stats não encontrado: {path}")
print("[NORM] Sem norm_stats. Usando tensor 0..1 sem padronização.")
return None, None, None, ["R", "G", "B"]
js = load_json(path)
mean = js.get("mean")
std = js.get("std")
names = js.get("channels", [])
if mean is None or std is None:
raise RuntimeError(f"norm_stats inválido, faltando mean/std: {path}")
if names:
name_to_idx = {str(n).upper(): i for i, n in enumerate(names)}
required = ["R", "G", "B"]
missing = [ch for ch in required if ch not in name_to_idx]
if missing:
raise RuntimeError(f"norm_stats incompatível: faltam canais {missing}. channels={names}")
idx = [name_to_idx[ch] for ch in required]
mean_sel = [float(mean[i]) for i in idx]
std_sel = [float(std[i]) for i in idx]
names_sel = required
else:
if len(mean) < 3 or len(std) < 3:
raise RuntimeError(f"norm_stats precisa de pelo menos 3 valores RGB: {path}")
mean_sel = [float(mean[i]) for i in range(3)]
std_sel = [float(std[i]) for i in range(3)]
names_sel = ["R", "G", "B"]
print(f"[NORM] usando {path}")
print(f"[NORM] channels={names_sel}")
print(f"[NORM] mean={mean_sel}")
print(f"[NORM] std ={std_sel}")
return mean_sel, std_sel, str(path), names_sel
def normalize_numpy_chw(chw: np.ndarray, mean: Optional[List[float]], std: Optional[List[float]]) -> np.ndarray:
if mean is None or std is None:
return chw.astype(np.float32)
mean_np = np.asarray(mean, dtype=np.float32).reshape(-1, 1, 1)
std_np = np.asarray(std, dtype=np.float32).reshape(-1, 1, 1)
std_np = np.clip(std_np, 1e-6, None)
return ((chw.astype(np.float32) - mean_np) / std_np).astype(np.float32)
def resolve_label_classes(config: dict, ckpt: Optional[dict] = None) -> Tuple[Dict[int, str], int]:
label_classes = config.get("label_classes", None)
if label_classes is not None:
label_name_by_id = {i: str(name) for i, name in enumerate(label_classes)}
return label_name_by_id, len(label_name_by_id)
if ckpt is not None:
extra = ckpt.get("extra", {}) if isinstance(ckpt, dict) else {}
maybe = extra.get("label_name_by_id", None)
if isinstance(maybe, dict) and maybe:
label_name_by_id = {int(k): str(v) for k, v in maybe.items()}
return label_name_by_id, max(label_name_by_id.keys()) + 1
maybe_config = extra.get("config", {}) if isinstance(extra, dict) else {}
maybe_classes = maybe_config.get("label_classes", None) if isinstance(maybe_config, dict) else None
if maybe_classes is not None:
label_name_by_id = {i: str(name) for i, name in enumerate(maybe_classes)}
return label_name_by_id, len(label_name_by_id)
raise RuntimeError(
"Não consegui resolver label_classes. "
"Adicione config['label_classes'] ou use um checkpoint com extra['label_name_by_id']."
)
# ============================================================
# Dataset / imagens
# ============================================================
@dataclass
class Sample:
img_path: str
mask_path: Optional[str]
mask2_path: Optional[str]
label_json_path: Optional[str]
label_npy_path: Optional[str]
group_name: str
filename: str
IMG_EXTS = (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp")
def find_by_stem(dir_path: str, filename: str, exts: Tuple[str, ...]) -> Optional[str]:
if not dir_path or not os.path.isdir(dir_path):
return None
stem, _ = os.path.splitext(filename)
for ext in exts:
p = os.path.join(dir_path, stem + ext)
if os.path.exists(p):
return p
return None
def discover_samples(split_root: Path, max_samples: int = 20, start_idx: int = 0) -> List[Sample]:
"""
Descobre amostras no layout do visual worker:
split/val/group/<grupo>/images/*.jpeg
split/val/group/<grupo>/masks/*.png
split/val/group/<grupo>/labels/*.json ou .npy
O validate compara PyTorch vs ONNX usando a imagem. Máscara/label GT são
apenas metadados úteis no relatório, não entram na fidelidade PT vs ONNX.
"""
split_root = Path(split_root)
group_root = split_root / "group"
if not group_root.is_dir():
raise RuntimeError(f"Não achei pasta: {group_root}")
samples: List[Sample] = []
img_dirs = glob.glob(str(group_root / "**" / "images"), recursive=True)
img_dirs = [d for d in img_dirs if os.path.isdir(d)]
for idir in sorted(img_dirs):
base = os.path.dirname(idir)
group_name = os.path.relpath(base, str(group_root)).replace("\\", "/")
mdir = os.path.join(base, "masks")
m2dir = os.path.join(base, "masks2")
ldir = os.path.join(base, "labels")
img_paths: List[str] = []
for ext in IMG_EXTS:
img_paths.extend(glob.glob(os.path.join(idir, f"*{ext}")))
img_paths.extend(glob.glob(os.path.join(idir, f"*{ext.upper()}")))
for ip in sorted(set(img_paths)):
fn = os.path.basename(ip)
mask_path = find_by_stem(mdir, fn, (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"))
mask2_path = find_by_stem(m2dir, fn, (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"))
label_npy_path = find_by_stem(ldir, fn, (".npy",))
label_json_path = find_by_stem(ldir, fn, (".json", ".txt"))
samples.append(Sample(
img_path=ip,
mask_path=mask_path,
mask2_path=mask2_path,
label_json_path=label_json_path,
label_npy_path=label_npy_path,
group_name=group_name,
filename=fn,
))
if not samples:
raise RuntimeError(f"Nenhuma imagem encontrada em: {group_root}/**/images")
start_idx = max(0, int(start_idx))
selected = samples[start_idx:]
if max_samples > 0:
selected = selected[:int(max_samples)]
return selected
def discover_image_folder(folder: Path, max_samples: int = 20, start_idx: int = 0) -> List[Sample]:
folder = Path(folder)
samples: List[Sample] = []
img_paths: List[str] = []
for ext in IMG_EXTS:
img_paths.extend(glob.glob(str(folder / f"*{ext}")))
img_paths.extend(glob.glob(str(folder / f"*{ext.upper()}")))
for ip in sorted(set(img_paths)):
samples.append(Sample(
img_path=ip,
mask_path=None,
mask2_path=None,
label_json_path=None,
label_npy_path=None,
group_name="external",
filename=os.path.basename(ip),
))
if not samples:
raise RuntimeError(f"Nenhuma imagem encontrada em: {folder}")
start_idx = max(0, int(start_idx))
selected = samples[start_idx:]
if max_samples > 0:
selected = selected[:int(max_samples)]
return selected
def read_gt_label(sample: Sample) -> Tuple[Optional[int], Optional[str]]:
if sample.label_npy_path and os.path.exists(sample.label_npy_path):
try:
v = np.load(sample.label_npy_path)
return int(np.array(v).reshape(-1)[0]), None
except Exception:
pass
if sample.label_json_path and os.path.exists(sample.label_json_path):
ext = os.path.splitext(sample.label_json_path)[1].lower()
if ext == ".json":
with open(sample.label_json_path, "r", encoding="utf-8") as f:
data = json.load(f)
lid = data.get("label_id")
lname = data.get("estado_corredor") or data.get("label") or data.get("state")
return (int(lid) if lid is not None else None), lname
if ext == ".txt":
with open(sample.label_json_path, "r", encoding="utf-8") as f:
txt = f.read().strip()
try:
return int(txt), None
except Exception:
return None, txt
return None, None
def load_rgb_image(path: str | Path) -> np.ndarray:
img_bgr = cv2.imread(str(path), cv2.IMREAD_COLOR)
if img_bgr is None:
raise RuntimeError(f"Falha ao ler imagem: {path}")
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
chw = np.transpose(img_rgb.astype(np.float32), (2, 0, 1)) / 255.0
return np.clip(chw, 0.0, 1.0).astype(np.float32)
def resize_chw(chw: np.ndarray, target_hw: Tuple[int, int]) -> np.ndarray:
H, W = target_hw
if chw.shape[-2:] == (H, W):
return chw.astype(np.float32, copy=False)
hwc = np.transpose(chw, (1, 2, 0))
hwc = cv2.resize(hwc, (W, H), interpolation=cv2.INTER_AREA)
return np.transpose(hwc, (2, 0, 1)).astype(np.float32)
# ============================================================
# Métricas
# ============================================================
def softmax_np(logits: np.ndarray, axis: int = 1) -> np.ndarray:
x = logits.astype(np.float32)
x = x - np.max(x, axis=axis, keepdims=True)
e = np.exp(x)
return e / np.clip(np.sum(e, axis=axis, keepdims=True), 1e-12, None)
def resize_logits_np_nchw(logits: np.ndarray, target_hw: Tuple[int, int]) -> np.ndarray:
n, c, h, w = logits.shape
th, tw = target_hw
if (h, w) == (th, tw):
return logits.astype(np.float32, copy=False)
out = np.empty((n, c, th, tw), dtype=np.float32)
for bi in range(n):
for ci in range(c):
out[bi, ci] = cv2.resize(
logits[bi, ci].astype(np.float32),
(tw, th),
interpolation=cv2.INTER_LINEAR,
)
return out
def compute_mask_iou_between_preds(
pred_a: np.ndarray,
pred_b: np.ndarray,
num_classes: int,
) -> Tuple[List[Optional[float]], float, List[int]]:
a = pred_a.reshape(-1).astype(np.int64)
b = pred_b.reshape(-1).astype(np.int64)
valid = (a >= 0) & (a < num_classes) & (b >= 0) & (b < num_classes)
a = a[valid]
b = b[valid]
if a.size == 0:
return [None for _ in range(num_classes)], 0.0, []
cm = np.bincount(
num_classes * a + b,
minlength=num_classes * num_classes,
).reshape(num_classes, num_classes)
tp = np.diag(cm).astype(np.float64)
fp = cm.sum(axis=0).astype(np.float64) - tp
fn = cm.sum(axis=1).astype(np.float64) - tp
den = tp + fp + fn
iou_per_class: List[Optional[float]] = []
present_classes: List[int] = []
for cls in range(num_classes):
if den[cls] <= 0:
iou_per_class.append(None)
else:
iou_per_class.append(float(tp[cls] / den[cls]))
present_classes.append(cls)
valid_ious = [x for x in iou_per_class if x is not None]
miou = float(np.mean(valid_ious)) if valid_ious else 0.0
return iou_per_class, miou, present_classes
def mean_or_none(values: List[float]) -> Optional[float]:
return None if not values else float(np.mean(values))
def min_or_none(values: List[float]) -> Optional[float]:
return None if not values else float(np.min(values))
def max_or_none(values: List[float]) -> Optional[float]:
return None if not values else float(np.max(values))
def nanmean_list(arr: np.ndarray) -> List[Optional[float]]:
if arr.size == 0:
return []
out = []
for col in range(arr.shape[1]):
v = arr[:, col]
v = v[~np.isnan(v)]
out.append(None if v.size == 0 else float(np.mean(v)))
return out
def nanmin_list(arr: np.ndarray) -> List[Optional[float]]:
if arr.size == 0:
return []
out = []
for col in range(arr.shape[1]):
v = arr[:, col]
v = v[~np.isnan(v)]
out.append(None if v.size == 0 else float(np.min(v)))
return out
def fmt_optional(v: Optional[float], casas: int = 8) -> str:
if v is None:
return "N/A"
return f"{float(v):.{casas}f}"
# ============================================================
# Modelo PyTorch visual
# ============================================================
class LabelHead(nn.Module):
def __init__(
self,
feat_ch: int,
num_seg_classes: int,
num_label_classes: int,
hidden: int = 256,
dropout: float = 0.2,
):
super().__init__()
in_ch = int(feat_ch) + int(num_seg_classes)
self.in_ch = in_ch
self.pool = nn.AdaptiveAvgPool2d((1, 1))
self.net = nn.Sequential(
nn.Linear(in_ch, hidden),
nn.ReLU(inplace=True),
nn.Dropout(dropout),
nn.Linear(hidden, num_label_classes),
)
def forward(self, feat: torch.Tensor, logits_seg: torch.Tensor) -> torch.Tensor:
feat = F.interpolate(
feat,
size=logits_seg.shape[-2:],
mode="bilinear",
align_corners=False,
)
x = torch.cat([feat, logits_seg], dim=1)
x = self.pool(x).flatten(1)
return self.net(x)
def get_last_feat(out, logits: torch.Tensor) -> torch.Tensor:
if hasattr(out, "hidden_states") and out.hidden_states is not None:
feat = out.hidden_states[-1]
else:
feat = logits
feat = F.interpolate(
feat,
size=logits.shape[-2:],
mode="bilinear",
align_corners=False,
)
return feat
class VisualSegformerDualLabel(nn.Module):
def __init__(self, base_model: nn.Module, label_head: nn.Module):
super().__init__()
self.base_model = base_model
self.label_head = label_head
def forward(self, pixel_values: torch.Tensor) -> Dict[str, torch.Tensor]:
out = self.base_model(pixel_values=pixel_values)
logits_seg = out.logits
feat = get_last_feat(out, logits_seg)
logits_label = self.label_head(feat, logits_seg)
return {
"semantic_logits": logits_seg,
"label_logits": logits_label,
"label_probs": torch.softmax(logits_label, dim=1),
}
def build_visual_model(
backbone: str,
num_seg_classes: int,
num_label_classes: int,
device: torch.device,
input_hw: Tuple[int, int],
) -> VisualSegformerDualLabel:
H, W = input_hw
base_model = SegformerForSemanticSegmentation.from_pretrained(
backbone,
num_labels=int(num_seg_classes),
ignore_mismatched_sizes=True,
use_safetensors=True,
)
base_model.config.output_hidden_states = True
base_model.to(device)
base_model.eval()
with torch.no_grad():
dummy = torch.zeros((1, 3, int(H), int(W)), dtype=torch.float32, device=device)
out = base_model(pixel_values=dummy)
logits = out.logits
feat = get_last_feat(out, logits)
feat_ch = int(feat.shape[1])
label_head = LabelHead(
feat_ch=feat_ch,
num_seg_classes=int(num_seg_classes),
num_label_classes=int(num_label_classes),
hidden=256,
dropout=0.2,
).to(device)
label_head.eval()
return VisualSegformerDualLabel(base_model=base_model, label_head=label_head).to(device)
def load_checkpoint_into_model(model: VisualSegformerDualLabel, checkpoint_path: Path):
ckpt = torch.load(str(checkpoint_path), map_location="cpu", weights_only=False)
if "model" not in ckpt:
raise RuntimeError(f"Checkpoint não contém chave 'model': {checkpoint_path}")
if "aux_head" not in ckpt:
raise RuntimeError(f"Checkpoint não contém chave 'aux_head': {checkpoint_path}")
model.base_model.load_state_dict(ckpt["model"], strict=True)
model.label_head.load_state_dict(ckpt["aux_head"], strict=True)
return ckpt
class VisualTorchWrapper(nn.Module):
def __init__(
self,
model: VisualSegformerDualLabel,
resize_semantic_to_input: bool = False,
semantic_output_kind: str = "logits",
label_output_kind: str = "probs",
):
super().__init__()
self.model = model
self.resize_semantic_to_input = bool(resize_semantic_to_input)
self.semantic_output_kind = str(semantic_output_kind).lower()
self.label_output_kind = str(label_output_kind).lower()
if self.semantic_output_kind not in ("logits", "mask"):
raise RuntimeError(f"semantic_output_kind inválido: {self.semantic_output_kind}")
if self.label_output_kind not in ("logits", "probs"):
raise RuntimeError(f"label_output_kind inválido: {self.label_output_kind}")
def forward(self, pixel_values: torch.Tensor) -> Dict[str, torch.Tensor]:
outputs = self.model(pixel_values=pixel_values)
semantic = outputs["semantic_logits"]
label_logits = outputs["label_logits"]
input_hw = pixel_values.shape[-2:]
if self.resize_semantic_to_input or self.semantic_output_kind == "mask":
semantic = F.interpolate(
semantic,
size=input_hw,
mode="bilinear",
align_corners=False,
)
if self.semantic_output_kind == "mask":
semantic_out = torch.argmax(semantic, dim=1).to(torch.uint8)
else:
semantic_out = semantic
if self.label_output_kind == "probs":
label_out = torch.softmax(label_logits, dim=1)
else:
label_out = label_logits
return {
"semantic": semantic_out,
"label": label_out,
}
# ============================================================
# ONNX Runtime
# ============================================================
def create_onnx_session(
onnx_path: Path,
provider: str,
trt_home: Optional[str] = None,
trt_fp16: bool = True,
):
try:
import onnxruntime as ort
except ImportError:
raise ImportError(
"onnxruntime não está instalado. Instale com:\n"
" pip install onnxruntime-gpu\n"
"ou, para CPU:\n"
" pip install onnxruntime"
)
provider = provider.lower()
if provider == "tensorrt":
trt_home = trt_home or os.environ.get("TRT_HOME", r"C:\dev\TensorRT-10.10.0.31")
dll_dirs = [
os.path.join(trt_home, "lib"),
os.path.join(trt_home, "bin"),
]
cuda_home = os.environ.get("CUDA_PATH")
if cuda_home:
dll_dirs.append(os.path.join(cuda_home, "bin"))
dll_dirs.append(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\bin")
for dll_dir in dll_dirs:
if os.path.isdir(dll_dir):
try:
os.add_dll_directory(dll_dir)
print(f"[DLL] add_dll_directory: {dll_dir}")
except Exception as e:
print(f"[DLL][WARN] falha em {dll_dir}: {e}")
available = ort.get_available_providers()
print(f"[ONNX] providers disponíveis: {available}")
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
if provider == "cuda":
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
elif provider == "cpu":
providers = ["CPUExecutionProvider"]
elif provider == "tensorrt":
cache_dir = onnx_path.parent / "trt_cache"
cache_dir.mkdir(parents=True, exist_ok=True)
trt_options = {
"device_id": 0,
"trt_fp16_enable": bool(trt_fp16),
"trt_engine_cache_enable": True,
"trt_engine_cache_path": str(cache_dir),
"trt_timing_cache_enable": True,
"trt_timing_cache_path": str(cache_dir),
"trt_max_workspace_size": 4 * 1024 * 1024 * 1024,
}
providers = [
("TensorrtExecutionProvider", trt_options),
"CUDAExecutionProvider",
"CPUExecutionProvider",
]
else:
raise RuntimeError(f"Provider desconhecido: {provider}")
requested_names = [p[0] if isinstance(p, tuple) else p for p in providers]
providers_ok = [p for p in providers if (p[0] if isinstance(p, tuple) else p) in available]
if not providers_ok:
raise RuntimeError(
f"Nenhum provider solicitado está disponível. "
f"Solicitado={requested_names}, disponível={available}"
)
sess = ort.InferenceSession(
str(onnx_path),
sess_options=sess_options,
providers=providers_ok,
)
active = sess.get_providers()
print(f"[ONNX] usando providers: {active}")
if provider == "tensorrt" and "TensorrtExecutionProvider" not in active:
raise RuntimeError(
"TensorRTExecutionProvider foi solicitado, mas não ficou ativo. "
f"Providers ativos: {active}."
)
if provider == "cuda" and "CUDAExecutionProvider" not in active:
raise RuntimeError(
"CUDAExecutionProvider foi solicitado, mas não ficou ativo. "
f"Providers ativos: {active}."
)
return sess
def run_onnx(session, input_name: str, x_nchw: np.ndarray) -> Dict[str, np.ndarray]:
outputs = session.run(None, {input_name: x_nchw.astype(np.float32)})
output_names = [o.name for o in session.get_outputs()]
if len(outputs) != len(output_names):
raise RuntimeError("Quantidade de outputs ONNX inesperada.")
return {name: arr.astype(np.float32) for name, arr in zip(output_names, outputs)}
def get_onnx_output_pair(onnx_outputs: Dict[str, np.ndarray]) -> Tuple[np.ndarray, np.ndarray, str, str]:
keys = list(onnx_outputs.keys())
semantic_candidates = ["semantic_logits", "semantic_mask", "semantic", "output_semantic"]
label_candidates = ["label_probs", "label_logits", "label", "output_label"]
semantic_name = None
label_name = None
for c in semantic_candidates:
if c in onnx_outputs:
semantic_name = c
break
for c in label_candidates:
if c in onnx_outputs:
label_name = c
break
if semantic_name is None or label_name is None:
if len(keys) != 2:
raise RuntimeError(f"Esperava 2 outputs ONNX, recebi {keys}")
semantic_name = semantic_name or keys[0]
label_name = label_name or keys[1]
return onnx_outputs[semantic_name], onnx_outputs[label_name], semantic_name, label_name
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="config.json")
parser.add_argument("--checkpoint", default="")
parser.add_argument("--onnx", default="")
parser.add_argument("--labelmap", default="")
parser.add_argument("--norm_stats", default="")
parser.add_argument("--split_folder", default="val", choices=["train", "val", "test"])
parser.add_argument("--root_override", default=None)
parser.add_argument(
"--test_folder",
default=None,
help="Pasta externa com imagens soltas para validar fidelidade PT vs ONNX.",
)
parser.add_argument("--max_samples", type=int, default=20)
parser.add_argument("--start_idx", type=int, default=0)
parser.add_argument("--device", default="cuda", choices=["cuda", "cpu"])
parser.add_argument("--onnx_provider", default="cuda", choices=["cuda", "cpu", "tensorrt"])
parser.add_argument(
"--trt_home",
default="C:\\dev\\TensorRT-10.10.0.31",
help="Pasta raiz do TensorRT.",
)
parser.add_argument("--trt_no_fp16", action="store_true")
parser.add_argument("--torch_no_amp", action="store_true")
parser.add_argument(
"--onnx_has_norm",
action="store_true",
help="Use quando o ONNX foi exportado com --include-norm. Nesse caso o ONNX recebe RGB 0..1.",
)
parser.add_argument(
"--semantic_output_kind",
default="logits",
choices=["logits", "mask"],
help="Tipo da saída semântica do ONNX: logits ou mask.",
)
parser.add_argument(
"--label_output_kind",
default="probs",
choices=["logits", "probs"],
help="Tipo da saída label do ONNX: logits ou probs.",
)
parser.add_argument(
"--compare_at_input_size",
action="store_true",
help="Compara segmentação em HxW da entrada.",
)
parser.add_argument(
"--save_report",
default=None,
help="Caminho do JSON de relatório. Se omitido, salva ao lado do ONNX.",
)
args = parser.parse_args()
config_path = resolve_path(args.config, Path.cwd())
if config_path is None or not config_path.is_file():
raise FileNotFoundError(f"Config não encontrado: {config_path}")
config_dir = config_path.parent
config = load_json(config_path)
checkpoint_path, onnx_path, ckpt_name, mode, save_dir = resolve_default_paths(args, config, config_dir)
if mode != "label":
raise RuntimeError(f"Este validate foi preparado para dual_head_label. Modo detectado: {mode}")
labelmap_path = resolve_labelmap_path(args, config, config_dir)
semantic_id2label, semantic_label2id, ignore_index = load_labelmap(labelmap_path)
num_seg_classes = len(semantic_id2label)
W, H = config.get("resolucao", [1024, 640])
W = int(W)
H = int(H)
backbone = str(config.get("backbone", "nvidia/mit-b0"))
ckpt_meta = torch.load(str(checkpoint_path), map_location="cpu", weights_only=False)
label_name_by_id, num_label_classes = resolve_label_classes(config, ckpt_meta)
norm_stats_path = resolve_norm_stats_path(args, config, config_dir, save_dir)
mean, std, norm_stats_used, norm_channels = load_rgb_norm_stats(norm_stats_path)
if args.test_folder:
root = resolve_path(args.test_folder, Path.cwd())
if root is None or not root.is_dir():
raise FileNotFoundError(f"Pasta de teste não encontrada: {root}")
samples = discover_image_folder(
folder=root,
max_samples=args.max_samples,
start_idx=args.start_idx,
)
else:
if args.root_override:
root = resolve_path(args.root_override, Path.cwd())
else:
camera = str(config.get("camera", "oak-d"))
root = (config_dir / camera / "dataset" / "split" / args.split_folder).resolve()
if root is None or not root.is_dir():
raise FileNotFoundError(f"Root de dados não encontrado: {root}")
samples = discover_samples(
split_root=root,
max_samples=args.max_samples,
start_idx=args.start_idx,
)
use_cuda = args.device == "cuda" and torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
if args.device == "cuda" and not torch.cuda.is_available():
print("[WARN] CUDA indisponível. Usando CPU no PyTorch.")
print("==========================================")
print("Validate Visual Worker PyTorch vs ONNX")
print(f"Config : {config_path}")
print(f"Checkpoint : {checkpoint_path}")
print(f"ONNX : {onnx_path}")
print(f"Root : {root}")
print(f"Samples : {len(samples)}")
print(f"Backbone : {backbone}")
print(f"Input shape : [1, 3, {H}, {W}]")
print(f"Semantic classes : {num_seg_classes} {semantic_id2label}")
print(f"Label classes : {num_label_classes} {label_name_by_id}")
print(f"Device : {device}")
print(f"ONNX provider : {args.onnx_provider}")
print(f"Torch AMP : {not args.torch_no_amp and device.type == 'cuda'}")
print(f"ONNX has norm : {args.onnx_has_norm}")
print(f"Semantic output kind: {args.semantic_output_kind}")
print(f"Label output kind : {args.label_output_kind}")
print(f"Compare HxW : {args.compare_at_input_size}")
print("==========================================")
print("[MODEL] Montando PyTorch...")
model = build_visual_model(
backbone=backbone,
num_seg_classes=num_seg_classes,
num_label_classes=num_label_classes,
device=device,
input_hw=(H, W),
)
print("[CKPT] Carregando checkpoint...")
ckpt = load_checkpoint_into_model(model, checkpoint_path)
model.to(device)
model.eval()
torch_wrapper = VisualTorchWrapper(
model=model,
resize_semantic_to_input=args.compare_at_input_size or args.semantic_output_kind == "mask",
semantic_output_kind=args.semantic_output_kind,
label_output_kind=args.label_output_kind,
).to(device)
torch_wrapper.eval()
print("[ONNX] Carregando sessão...")
onnx_session = create_onnx_session(
onnx_path=onnx_path,
provider=args.onnx_provider,
trt_home=args.trt_home,
trt_fp16=not args.trt_no_fp16,
)
onnx_input_name = onnx_session.get_inputs()[0].name
onnx_output_names = [o.name for o in onnx_session.get_outputs()]
print(f"[ONNX] input name: {onnx_input_name}")
print(f"[ONNX] outputs: {onnx_output_names}")
semantic_acc = {
"n": 0,
"logits_abs_mean": [],
"logits_abs_max": [],
"prob_abs_mean": [],
"prob_abs_max": [],
"argmax_equal_ratio": [],
"pred_miou": [],
"pred_iou_per_class": [],
}
label_acc = {
"n": 0,
"abs_mean": [],
"abs_max": [],
"top1_equal": [],
"top1_pt": [],
"top1_onnx": [],
"conf_pt": [],
"conf_onnx": [],
}
sample_reports = []
for i, sample in enumerate(samples):
chw01 = load_rgb_image(sample.img_path)
chw01 = resize_chw(chw01, target_hw=(H, W))
chw_norm = normalize_numpy_chw(chw01, mean=mean, std=std)
# PyTorch puro espera tensor já normalizado.
x_torch_np = np.expand_dims(chw_norm, axis=0).astype(np.float32)
# ONNX com include_norm recebe RGB cru 0..1.
if args.onnx_has_norm:
x_onnx_np = np.expand_dims(chw01, axis=0).astype(np.float32)
else:
x_onnx_np = x_torch_np
x_torch = torch.from_numpy(x_torch_np).to(device, non_blocking=True)
if device.type == "cuda":
torch.cuda.synchronize()
t0 = time.perf_counter()
with torch.inference_mode():
with torch.autocast(
device_type="cuda",
dtype=torch.float16,
enabled=(not args.torch_no_amp and device.type == "cuda"),
):
torch_outputs_t = torch_wrapper(x_torch)
if device.type == "cuda":
torch.cuda.synchronize()
torch_ms = (time.perf_counter() - t0) * 1000.0
torch_semantic = torch_outputs_t["semantic"].detach().float().cpu().numpy()
torch_label = torch_outputs_t["label"].detach().float().cpu().numpy()
t0 = time.perf_counter()
onnx_raw = run_onnx(onnx_session, onnx_input_name, x_onnx_np)
onnx_ms = (time.perf_counter() - t0) * 1000.0
onnx_semantic, onnx_label, onnx_semantic_name, onnx_label_name = get_onnx_output_pair(onnx_raw)
gt_label_id, gt_label_name = read_gt_label(sample)
report_item = {
"idx": i,
"image": str(sample.img_path),
"mask": str(sample.mask_path) if sample.mask_path else None,
"label_json": str(sample.label_json_path) if sample.label_json_path else None,
"label_npy": str(sample.label_npy_path) if sample.label_npy_path else None,
"group_name": sample.group_name,
"filename": sample.filename,
"gt_label_id": gt_label_id,
"gt_label_name": gt_label_name,
"torch_ms": float(torch_ms),
"onnx_ms": float(onnx_ms),
"onnx_semantic_name": onnx_semantic_name,
"onnx_label_name": onnx_label_name,
}
print(f"[{i + 1:03d}/{len(samples):03d}] {sample.group_name}/{sample.filename} | torch={torch_ms:.2f}ms | onnx={onnx_ms:.2f}ms")
# ====================================================
# Segmentação
# ====================================================
if args.semantic_output_kind == "mask":
pt_mask = np.asarray(torch_semantic).astype(np.uint8)
ox_mask = np.asarray(onnx_semantic).astype(np.uint8)
if pt_mask.ndim == 3:
pt_mask = pt_mask[0]
if ox_mask.ndim == 3:
ox_mask = ox_mask[0]
if pt_mask.shape != ox_mask.shape:
ox_mask = cv2.resize(
ox_mask,
(pt_mask.shape[1], pt_mask.shape[0]),
interpolation=cv2.INTER_NEAREST,
)
equal_ratio = float(np.mean(pt_mask == ox_mask))
iou_per_class, miou, present_classes = compute_mask_iou_between_preds(
pt_mask,
ox_mask,
num_classes=num_seg_classes,
)
semantic_report = {
"torch_shape": list(np.asarray(torch_semantic).shape),
"onnx_shape": list(np.asarray(onnx_semantic).shape),
"compare_shape": list(pt_mask.shape),
"logits_abs_mean": None,
"logits_abs_max": None,
"prob_abs_mean": None,
"prob_abs_max": None,
"argmax_equal_ratio": equal_ratio,
"pred_miou_torch_vs_onnx": miou,
"pred_iou_per_class": [None if x is None else float(x) for x in iou_per_class],
"present_classes": [int(x) for x in present_classes],
}
print(
f" semantic "
f"shape PT={tuple(np.asarray(torch_semantic).shape)} ONNX={tuple(np.asarray(onnx_semantic).shape)} "
f"CMP={tuple(pt_mask.shape)} | "
f"mask_equal={equal_ratio * 100:.4f}% "
f"mIoU={miou:.8f}"
)
else:
pt = np.asarray(torch_semantic).astype(np.float32)
ox = np.asarray(onnx_semantic).astype(np.float32)
compare_hw = (H, W) if args.compare_at_input_size else None
if pt.shape != ox.shape:
compare_hw = (H, W)
if compare_hw is not None:
pt_cmp = resize_logits_np_nchw(pt, compare_hw)
ox_cmp = resize_logits_np_nchw(ox, compare_hw)
else:
pt_cmp = pt
ox_cmp = ox
if pt_cmp.shape != ox_cmp.shape:
raise RuntimeError(f"Shape semântico incompatível: torch={pt_cmp.shape}, onnx={ox_cmp.shape}")
diff_logits = np.abs(pt_cmp - ox_cmp)
prob_pt = softmax_np(pt_cmp, axis=1)
prob_ox = softmax_np(ox_cmp, axis=1)
diff_prob = np.abs(prob_pt - prob_ox)
pred_pt = np.argmax(prob_pt, axis=1)[0].astype(np.uint8)
pred_ox = np.argmax(prob_ox, axis=1)[0].astype(np.uint8)
equal_ratio = float(np.mean(pred_pt == pred_ox))
iou_per_class, miou, present_classes = compute_mask_iou_between_preds(
pred_pt,
pred_ox,
num_classes=num_seg_classes,
)
semantic_report = {
"torch_shape": list(pt.shape),
"onnx_shape": list(ox.shape),
"compare_shape": list(pt_cmp.shape),
"logits_abs_mean": float(diff_logits.mean()),
"logits_abs_max": float(diff_logits.max()),
"prob_abs_mean": float(diff_prob.mean()),
"prob_abs_max": float(diff_prob.max()),
"argmax_equal_ratio": equal_ratio,
"pred_miou_torch_vs_onnx": miou,
"pred_iou_per_class": [None if x is None else float(x) for x in iou_per_class],
"present_classes": [int(x) for x in present_classes],
}
print(
f" semantic "
f"shape PT={tuple(pt.shape)} ONNX={tuple(ox.shape)} CMP={tuple(pt_cmp.shape)} | "
f"logit_mean={semantic_report['logits_abs_mean']:.6g} "
f"prob_mean={semantic_report['prob_abs_mean']:.6g} "
f"argmax_equal={equal_ratio * 100:.4f}% "
f"mIoU={miou:.8f}"
)
semantic_acc["n"] += 1
if semantic_report["logits_abs_mean"] is not None:
semantic_acc["logits_abs_mean"].append(semantic_report["logits_abs_mean"])
semantic_acc["logits_abs_max"].append(semantic_report["logits_abs_max"])
semantic_acc["prob_abs_mean"].append(semantic_report["prob_abs_mean"])
semantic_acc["prob_abs_max"].append(semantic_report["prob_abs_max"])
semantic_acc["argmax_equal_ratio"].append(semantic_report["argmax_equal_ratio"])
semantic_acc["pred_miou"].append(semantic_report["pred_miou_torch_vs_onnx"])
semantic_acc["pred_iou_per_class"].append(semantic_report["pred_iou_per_class"])
# ====================================================
# Label/status
# ====================================================
pt_label = np.asarray(torch_label).astype(np.float32)
ox_label = np.asarray(onnx_label).astype(np.float32)
if pt_label.shape != ox_label.shape:
raise RuntimeError(f"Shape label incompatível: torch={pt_label.shape}, onnx={ox_label.shape}")
if args.label_output_kind == "logits":
pt_label_probs = softmax_np(pt_label, axis=1)
ox_label_probs = softmax_np(ox_label, axis=1)
label_diff_base = np.abs(pt_label - ox_label)
else:
pt_label_probs = pt_label
ox_label_probs = ox_label
label_diff_base = np.abs(pt_label_probs - ox_label_probs)
top1_pt = int(np.argmax(pt_label_probs, axis=1)[0])
top1_ox = int(np.argmax(ox_label_probs, axis=1)[0])
conf_pt = float(pt_label_probs[0, top1_pt])
conf_ox = float(ox_label_probs[0, top1_ox])
top1_equal = bool(top1_pt == top1_ox)
label_report = {
"torch_shape": list(pt_label.shape),
"onnx_shape": list(ox_label.shape),
"abs_mean": float(label_diff_base.mean()),
"abs_max": float(label_diff_base.max()),
"top1_equal": top1_equal,
"top1_torch": top1_pt,
"top1_onnx": top1_ox,
"top1_torch_name": label_name_by_id.get(top1_pt, str(top1_pt)),
"top1_onnx_name": label_name_by_id.get(top1_ox, str(top1_ox)),
"conf_torch": conf_pt,
"conf_onnx": conf_ox,
"gt_label_id": gt_label_id,
"gt_label_name": gt_label_name,
"probs_torch": pt_label_probs.reshape(-1).astype(float).tolist(),
"probs_onnx": ox_label_probs.reshape(-1).astype(float).tolist(),
}
label_acc["n"] += 1
label_acc["abs_mean"].append(label_report["abs_mean"])
label_acc["abs_max"].append(label_report["abs_max"])
label_acc["top1_equal"].append(1.0 if top1_equal else 0.0)
label_acc["top1_pt"].append(top1_pt)
label_acc["top1_onnx"].append(top1_ox)
label_acc["conf_pt"].append(conf_pt)
label_acc["conf_onnx"].append(conf_ox)
print(
f" label "
f"shape PT={tuple(pt_label.shape)} ONNX={tuple(ox_label.shape)} | "
f"abs_mean={label_report['abs_mean']:.6g} "
f"abs_max={label_report['abs_max']:.6g} "
f"top1_equal={top1_equal} "
f"PT={top1_pt}:{label_report['top1_torch_name']}({conf_pt:.4f}) "
f"ONNX={top1_ox}:{label_report['top1_onnx_name']}({conf_ox:.4f}) "
f"GT={gt_label_id}:{gt_label_name}"
)
report_item["semantic"] = semantic_report
report_item["label"] = label_report
sample_reports.append(report_item)
# ========================================================
# Resumo
# ========================================================
ious_raw = semantic_acc["pred_iou_per_class"]
if ious_raw:
ious_arr = np.array(
[[np.nan if x is None else float(x) for x in row] for row in ious_raw],
dtype=np.float64,
)
else:
ious_arr = np.empty((0, 0), dtype=np.float64)
semantic_summary = {
"n": int(semantic_acc["n"]),
"logits_abs_mean_avg": mean_or_none(semantic_acc["logits_abs_mean"]),
"logits_abs_mean_max": max_or_none(semantic_acc["logits_abs_mean"]),
"logits_abs_max_avg": mean_or_none(semantic_acc["logits_abs_max"]),
"logits_abs_max_max": max_or_none(semantic_acc["logits_abs_max"]),
"prob_abs_mean_avg": mean_or_none(semantic_acc["prob_abs_mean"]),
"prob_abs_mean_max": max_or_none(semantic_acc["prob_abs_mean"]),
"prob_abs_max_avg": mean_or_none(semantic_acc["prob_abs_max"]),
"prob_abs_max_max": max_or_none(semantic_acc["prob_abs_max"]),
"argmax_equal_ratio_avg": float(np.mean(semantic_acc["argmax_equal_ratio"])),
"argmax_equal_ratio_min": float(np.min(semantic_acc["argmax_equal_ratio"])),
"pred_miou_avg": float(np.mean(semantic_acc["pred_miou"])),
"pred_miou_min": float(np.min(semantic_acc["pred_miou"])),
"pred_iou_per_class_avg": nanmean_list(ious_arr),
"pred_iou_per_class_min": nanmin_list(ious_arr),
}
label_summary = {
"n": int(label_acc["n"]),
"abs_mean_avg": mean_or_none(label_acc["abs_mean"]),
"abs_mean_max": max_or_none(label_acc["abs_mean"]),
"abs_max_avg": mean_or_none(label_acc["abs_max"]),
"abs_max_max": max_or_none(label_acc["abs_max"]),
"top1_equal_ratio_avg": float(np.mean(label_acc["top1_equal"])),
"top1_equal_ratio_min": float(np.min(label_acc["top1_equal"])),
"conf_torch_avg": mean_or_none(label_acc["conf_pt"]),
"conf_onnx_avg": mean_or_none(label_acc["conf_onnx"]),
}
summary = {
"kind": "visual_worker_validate_onnx",
"config": str(config_path),
"checkpoint": str(checkpoint_path),
"ckpt_name": ckpt_name,
"onnx": str(onnx_path),
"root": str(root),
"samples": len(samples),
"input_shape": [1, 3, H, W],
"input_channel_names": ["R", "G", "B"],
"semantic_id2label": semantic_id2label,
"label_name_by_id": label_name_by_id,
"norm_stats_used": norm_stats_used,
"norm_channels": norm_channels,
"torch_amp": bool(not args.torch_no_amp and device.type == "cuda"),
"onnx_provider": args.onnx_provider,
"onnx_has_norm": bool(args.onnx_has_norm),
"semantic_output_kind": args.semantic_output_kind,
"label_output_kind": args.label_output_kind,
"compare_at_input_size": bool(args.compare_at_input_size),
"trt_home": args.trt_home or os.environ.get("TRT_HOME", None),
"trt_fp16": bool(not args.trt_no_fp16),
"semantic": semantic_summary,
"label": label_summary,
"sample_reports": sample_reports,
}
print("\n========== RESUMO ==========")
print("\n[semantic]")
print(f" logits_abs_mean avg : {fmt_optional(semantic_summary['logits_abs_mean_avg'])}")
print(f" prob_abs_mean avg : {fmt_optional(semantic_summary['prob_abs_mean_avg'])}")
print(f" argmax_equal avg : {semantic_summary['argmax_equal_ratio_avg'] * 100:.4f}%")
print(f" argmax_equal min : {semantic_summary['argmax_equal_ratio_min'] * 100:.4f}%")
print(f" pred_mIoU avg : {semantic_summary['pred_miou_avg']:.8f}")
print(f" pred_mIoU min : {semantic_summary['pred_miou_min']:.8f}")
print(
" IoU/classes avg : "
f"{[None if x is None else round(float(x), 6) for x in semantic_summary['pred_iou_per_class_avg']]}"
)
print(
" IoU/classes min : "
f"{[None if x is None else round(float(x), 6) for x in semantic_summary['pred_iou_per_class_min']]}"
)
print("\n[label]")
print(f" abs_mean avg : {fmt_optional(label_summary['abs_mean_avg'])}")
print(f" abs_max max : {fmt_optional(label_summary['abs_max_max'])}")
print(f" top1_equal avg : {label_summary['top1_equal_ratio_avg'] * 100:.4f}%")
print(f" top1_equal min : {label_summary['top1_equal_ratio_min'] * 100:.4f}%")
print(f" conf_torch avg : {fmt_optional(label_summary['conf_torch_avg'])}")
print(f" conf_onnx avg : {fmt_optional(label_summary['conf_onnx_avg'])}")
if args.save_report:
report_path = resolve_path(args.save_report, Path.cwd())
else:
suffix = f".validate_{args.onnx_provider}_report.json"
report_path = onnx_path.with_suffix(suffix)
save_json(report_path, summary)
print(f"\n[OK] Relatório salvo em: {report_path}")
print("\nValidação finalizada.")
if __name__ == "__main__":
main()