1172 lines
38 KiB
Python
1172 lines
38 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# -*- coding: utf-8 -*-
|
||
|
|
|
||
|
|
"""
|
||
|
|
_11_validate_onnx.py
|
||
|
|
|
||
|
|
Valida fidelidade entre:
|
||
|
|
- modelo PyTorch .pt
|
||
|
|
- modelo ONNX .onnx
|
||
|
|
|
||
|
|
para o SegFormer OAK-FCC-3 Multi-Head.
|
||
|
|
|
||
|
|
Exemplo:
|
||
|
|
|
||
|
|
python _11_validate_onnx.py --config config.json --max_samples 20 --device cuda --onnx_provider cuda --torch_no_amp
|
||
|
|
|
||
|
|
Para validar o ONNX com saída já redimensionada:
|
||
|
|
|
||
|
|
python _11_validate_onnx.py --config config.json --max_samples 20 --device cuda --onnx_provider cuda --torch_no_amp --compare_at_input_size
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import json
|
||
|
|
import time
|
||
|
|
import copy
|
||
|
|
import argparse
|
||
|
|
import importlib.util
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, List, Optional, Tuple
|
||
|
|
|
||
|
|
import cv2
|
||
|
|
import numpy as np
|
||
|
|
import torch
|
||
|
|
import torch.nn as nn
|
||
|
|
import torch.nn.functional as F
|
||
|
|
|
||
|
|
|
||
|
|
DEFAULT_CHANNEL_ORDER = ["R", "G", "B", "RE", "NIR"]
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Utils
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
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:
|
||
|
|
return None
|
||
|
|
|
||
|
|
p = Path(path_like)
|
||
|
|
if p.is_absolute():
|
||
|
|
return p
|
||
|
|
|
||
|
|
if base is None:
|
||
|
|
base = Path.cwd()
|
||
|
|
|
||
|
|
return (base / p).resolve()
|
||
|
|
|
||
|
|
|
||
|
|
def import_train_module(train_script_path: str | Path):
|
||
|
|
train_script_path = Path(train_script_path)
|
||
|
|
|
||
|
|
if not train_script_path.exists():
|
||
|
|
raise FileNotFoundError(f"Script de treino não encontrado: {train_script_path}")
|
||
|
|
|
||
|
|
spec = importlib.util.spec_from_file_location(
|
||
|
|
"train_multihead_module",
|
||
|
|
str(train_script_path.resolve())
|
||
|
|
)
|
||
|
|
|
||
|
|
if spec is None or spec.loader is None:
|
||
|
|
raise RuntimeError(f"Não consegui importar o script: {train_script_path}")
|
||
|
|
|
||
|
|
module = importlib.util.module_from_spec(spec)
|
||
|
|
spec.loader.exec_module(module)
|
||
|
|
return module
|
||
|
|
|
||
|
|
|
||
|
|
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:
|
||
|
|
"""
|
||
|
|
logits: [N,C,H,W]
|
||
|
|
target_hw: (H,W)
|
||
|
|
"""
|
||
|
|
n, c, h, w = logits.shape
|
||
|
|
th, tw = target_hw
|
||
|
|
|
||
|
|
if (h, w) == (th, tw):
|
||
|
|
return logits
|
||
|
|
|
||
|
|
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]]:
|
||
|
|
"""
|
||
|
|
Mede IoU entre duas predições.
|
||
|
|
|
||
|
|
Classes ausentes nos dois mapas recebem None e NÃO entram no mIoU.
|
||
|
|
Isso evita o caso:
|
||
|
|
máscaras iguais, só classe 0 presente -> [1.0, None, None] -> mIoU=1.0
|
||
|
|
em vez de:
|
||
|
|
[1.0, 0.0, 0.0] -> mIoU=0.333
|
||
|
|
"""
|
||
|
|
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:
|
||
|
|
# Classe ausente nas duas predições.
|
||
|
|
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 load_norm_stats(
|
||
|
|
path: Optional[Path],
|
||
|
|
channels: int,
|
||
|
|
channel_indices: List[int],
|
||
|
|
channel_names: List[str],
|
||
|
|
) -> Tuple[Optional[List[float]], Optional[List[float]], Optional[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
|
||
|
|
|
||
|
|
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}")
|
||
|
|
|
||
|
|
max_idx = max(channel_indices)
|
||
|
|
|
||
|
|
if len(mean) <= max_idx or len(std) <= max_idx:
|
||
|
|
raise RuntimeError(
|
||
|
|
f"norm_stats incompatível: precisa índices={channel_indices}, "
|
||
|
|
f"mean={len(mean)} std={len(std)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
mean_sel = [float(mean[i]) for i in channel_indices]
|
||
|
|
std_sel = [float(std[i]) for i in channel_indices]
|
||
|
|
|
||
|
|
if names:
|
||
|
|
names_sel = [names[i] for i in channel_indices]
|
||
|
|
else:
|
||
|
|
names_sel = channel_names
|
||
|
|
|
||
|
|
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)
|
||
|
|
|
||
|
|
|
||
|
|
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 load_tensor(path: Path, channels: int, channel_indices: List[int]) -> np.ndarray:
|
||
|
|
arr = np.load(str(path)).astype(np.float32)
|
||
|
|
|
||
|
|
if arr.ndim != 3:
|
||
|
|
raise RuntimeError(f"Tensor inválido {path}: shape={arr.shape}, esperado 3D")
|
||
|
|
|
||
|
|
if arr.shape[0] in (3, 4, 5):
|
||
|
|
chw = arr
|
||
|
|
elif arr.shape[-1] in (3, 4, 5):
|
||
|
|
chw = np.transpose(arr, (2, 0, 1))
|
||
|
|
else:
|
||
|
|
raise RuntimeError(f"Tensor com layout inesperado: {path} shape={arr.shape}")
|
||
|
|
|
||
|
|
max_idx = max(channel_indices)
|
||
|
|
if chw.shape[0] <= max_idx:
|
||
|
|
raise RuntimeError(
|
||
|
|
f"Tensor {path} tem {chw.shape[0]} canais, "
|
||
|
|
f"mas precisa acessar índice {max_idx}."
|
||
|
|
)
|
||
|
|
|
||
|
|
chw = chw[channel_indices, :, :]
|
||
|
|
|
||
|
|
finite = np.isfinite(chw)
|
||
|
|
if finite.any():
|
||
|
|
mx = float(np.nanmax(chw[finite]))
|
||
|
|
if mx > 2.0 and mx <= 255.0:
|
||
|
|
chw = chw / 255.0
|
||
|
|
elif mx > 255.0:
|
||
|
|
chw = chw / 65535.0
|
||
|
|
|
||
|
|
chw = np.nan_to_num(chw, nan=0.0, posinf=1.0, neginf=0.0)
|
||
|
|
return np.clip(chw, 0.0, 1.0).astype(np.float32)
|
||
|
|
|
||
|
|
|
||
|
|
def collect_tensor_samples(root: Path, max_samples: int = 20, start_idx: int = 0) -> List[Path]:
|
||
|
|
tensor_paths: List[Path] = []
|
||
|
|
|
||
|
|
direct = root / "tensors"
|
||
|
|
if direct.is_dir():
|
||
|
|
tensor_paths.extend(sorted(direct.glob("*.npy")))
|
||
|
|
|
||
|
|
group_root = root / "group"
|
||
|
|
if group_root.is_dir():
|
||
|
|
for gdir in sorted(group_root.iterdir()):
|
||
|
|
tdir = gdir / "tensors"
|
||
|
|
if tdir.is_dir():
|
||
|
|
tensor_paths.extend(sorted(tdir.glob("*.npy")))
|
||
|
|
|
||
|
|
if not tensor_paths:
|
||
|
|
tensor_paths.extend(sorted(root.glob("**/tensors/*.npy")))
|
||
|
|
|
||
|
|
if not tensor_paths:
|
||
|
|
raise RuntimeError(f"Nenhum tensor .npy encontrado em: {root}")
|
||
|
|
|
||
|
|
start_idx = max(0, int(start_idx))
|
||
|
|
selected = tensor_paths[start_idx:]
|
||
|
|
|
||
|
|
if max_samples > 0:
|
||
|
|
selected = selected[:int(max_samples)]
|
||
|
|
|
||
|
|
return selected
|
||
|
|
|
||
|
|
|
||
|
|
def find_norm_stats(config: dict, config_dir: Path, save_dir: Path, explicit: Optional[str]) -> Optional[Path]:
|
||
|
|
if explicit:
|
||
|
|
return resolve_path(explicit, Path.cwd())
|
||
|
|
|
||
|
|
W, H = config.get("resolucao", [1024, 640])
|
||
|
|
dataset_path = config_dir / "dataset"
|
||
|
|
|
||
|
|
candidates = [
|
||
|
|
dataset_path / f"{int(W)}x{int(H)}" / "group" / "norm_stats.json",
|
||
|
|
save_dir / "norm_stats.json",
|
||
|
|
config_dir / "backup" / config.get("modelo", "segformer_b1") / config.get("model_name", "test") / config.get("stats_source_tag", "stacked_raw5") / "norm_stats.json",
|
||
|
|
]
|
||
|
|
|
||
|
|
for p in candidates:
|
||
|
|
if p.is_file():
|
||
|
|
return p
|
||
|
|
|
||
|
|
return candidates[0]
|
||
|
|
|
||
|
|
|
||
|
|
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 resolve_model_artifact_paths(
|
||
|
|
args,
|
||
|
|
config: dict,
|
||
|
|
config_dir: Path,
|
||
|
|
channels: int,
|
||
|
|
) -> Tuple[Path, Path, str]:
|
||
|
|
"""
|
||
|
|
Resolve checkpoint e ONNX.
|
||
|
|
|
||
|
|
Se --checkpoint ou --onnx forem informados, usa os caminhos informados.
|
||
|
|
Se ficarem vazios, monta a partir do config:
|
||
|
|
|
||
|
|
backup/{modelo}/{model_name}/{fusion_mode}_raw{channels}/{ckpt_name}.pt
|
||
|
|
backup/{modelo}/{model_name}/{fusion_mode}_raw{channels}/{ckpt_name}.onnx
|
||
|
|
|
||
|
|
ckpt_name vem de:
|
||
|
|
config["ckpt_test"] ou "best_score"
|
||
|
|
"""
|
||
|
|
model = config.get("modelo", "segformer_b1")
|
||
|
|
model_name = config.get("model_name", "target_teached")
|
||
|
|
fusion_mode = config.get("fusion_mode", "stacked")
|
||
|
|
|
||
|
|
# Melhor usar o channels real computado pelo script,
|
||
|
|
# porque ele vem do input_channel_names já resolvido.
|
||
|
|
ch = int(channels)
|
||
|
|
|
||
|
|
ckpt_name = config.get("ckpt_test", "best_score")
|
||
|
|
|
||
|
|
base_dir = config_dir / "backup" / model / model_name / f"{fusion_mode}_raw{ch}"
|
||
|
|
|
||
|
|
if args.checkpoint:
|
||
|
|
checkpoint_path = resolve_path(args.checkpoint, Path.cwd())
|
||
|
|
else:
|
||
|
|
checkpoint_path = base_dir / f"{ckpt_name}.pt"
|
||
|
|
|
||
|
|
if args.onnx:
|
||
|
|
onnx_path = resolve_path(args.onnx, Path.cwd())
|
||
|
|
else:
|
||
|
|
onnx_path = base_dir / f"{ckpt_name}.onnx"
|
||
|
|
|
||
|
|
if checkpoint_path is None or 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 onnx_path is None or not onnx_path.is_file():
|
||
|
|
raise FileNotFoundError(
|
||
|
|
f"ONNX não encontrado: {onnx_path}\n"
|
||
|
|
f"Dica: informe --onnx ou ajuste config['ckpt_test']."
|
||
|
|
)
|
||
|
|
|
||
|
|
return checkpoint_path.resolve(), onnx_path.resolve(), str(ckpt_name)
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# PyTorch wrapper
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
class MultiHeadTorchWrapper(nn.Module):
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
model: nn.Module,
|
||
|
|
output_heads: List[str],
|
||
|
|
resize_to_input: bool = False,
|
||
|
|
output_kind: str = "logits",
|
||
|
|
):
|
||
|
|
super().__init__()
|
||
|
|
self.model = model
|
||
|
|
self.output_heads = list(output_heads)
|
||
|
|
self.resize_to_input = bool(resize_to_input)
|
||
|
|
self.output_kind = str(output_kind).lower()
|
||
|
|
|
||
|
|
if self.output_kind not in ("logits", "mask"):
|
||
|
|
raise RuntimeError(f"output_kind inválido: {self.output_kind}")
|
||
|
|
|
||
|
|
def forward(self, pixel_values: torch.Tensor):
|
||
|
|
outputs: Dict[str, torch.Tensor] = self.model(pixel_values=pixel_values)
|
||
|
|
|
||
|
|
result = {}
|
||
|
|
input_hw = pixel_values.shape[-2:]
|
||
|
|
|
||
|
|
for head_name in self.output_heads:
|
||
|
|
logits = outputs[head_name]
|
||
|
|
|
||
|
|
if self.resize_to_input or self.output_kind == "mask":
|
||
|
|
if logits.shape[-2:] != input_hw:
|
||
|
|
logits = F.interpolate(
|
||
|
|
logits,
|
||
|
|
size=input_hw,
|
||
|
|
mode="bilinear",
|
||
|
|
align_corners=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
if self.output_kind == "mask":
|
||
|
|
result[head_name] = torch.argmax(logits, dim=1).to(torch.uint8)
|
||
|
|
else:
|
||
|
|
result[head_name] = logits
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# ONNX
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
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()
|
||
|
|
|
||
|
|
# ========================================================
|
||
|
|
# Windows/DLL helper para TensorRT
|
||
|
|
# ========================================================
|
||
|
|
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"))
|
||
|
|
|
||
|
|
# Fallback comum para CUDA 12.4
|
||
|
|
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),
|
||
|
|
|
||
|
|
# Cache para não reconstruir engine/timing toda vez.
|
||
|
|
"trt_engine_cache_enable": True,
|
||
|
|
"trt_engine_cache_path": str(cache_dir),
|
||
|
|
|
||
|
|
"trt_timing_cache_enable": True,
|
||
|
|
"trt_timing_cache_path": str(cache_dir),
|
||
|
|
|
||
|
|
# 4GB de workspace. Sua RTX 3070 lidou bem no benchmark.
|
||
|
|
"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}")
|
||
|
|
|
||
|
|
# Trava anti-burrice silenciosa: se pedir TensorRT, não pode cair para CPU/CUDA sem avisar.
|
||
|
|
if provider == "tensorrt" and "TensorrtExecutionProvider" not in active:
|
||
|
|
raise RuntimeError(
|
||
|
|
"TensorRTExecutionProvider foi solicitado, mas não ficou ativo. "
|
||
|
|
f"Providers ativos: {active}. "
|
||
|
|
"Provável causa: DLLs TensorRT fora do PATH/add_dll_directory, "
|
||
|
|
"versão incompatível ou fallback interno."
|
||
|
|
)
|
||
|
|
|
||
|
|
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 normalize_onnx_output_names(onnx_outputs: Dict[str, np.ndarray], output_heads: List[str]) -> Dict[str, np.ndarray]:
|
||
|
|
"""
|
||
|
|
Converte:
|
||
|
|
semantic_logits -> semantic
|
||
|
|
vegetation_logits -> vegetation
|
||
|
|
etc.
|
||
|
|
"""
|
||
|
|
out = {}
|
||
|
|
|
||
|
|
for head in output_heads:
|
||
|
|
candidates = [
|
||
|
|
head,
|
||
|
|
f"{head}_logits",
|
||
|
|
f"{head}_mask",
|
||
|
|
f"output_{head}",
|
||
|
|
]
|
||
|
|
|
||
|
|
found = None
|
||
|
|
for c in candidates:
|
||
|
|
if c in onnx_outputs:
|
||
|
|
found = c
|
||
|
|
break
|
||
|
|
|
||
|
|
if found is None:
|
||
|
|
# fallback por ordem caso nomes estejam diferentes
|
||
|
|
keys = list(onnx_outputs.keys())
|
||
|
|
idx = output_heads.index(head)
|
||
|
|
if idx < len(keys):
|
||
|
|
found = keys[idx]
|
||
|
|
|
||
|
|
if found is None:
|
||
|
|
raise RuntimeError(f"Não encontrei saída ONNX para head={head}. Outputs={list(onnx_outputs.keys())}")
|
||
|
|
|
||
|
|
out[head] = onnx_outputs[found]
|
||
|
|
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# 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("--train-script", default="_8_train_multihead.py")
|
||
|
|
parser.add_argument("--labelmap", default="dataset/labelmap.txt")
|
||
|
|
|
||
|
|
parser.add_argument("--split_folder", default="val", choices=["train", "val", "test"])
|
||
|
|
parser.add_argument("--root_override", default=None)
|
||
|
|
|
||
|
|
parser.add_argument("--norm_stats", default=None)
|
||
|
|
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. Ex: C:\\dev\\TensorRT-10.10.0.31. Se omitido, usa TRT_HOME ou fallback padrão.",
|
||
|
|
)
|
||
|
|
|
||
|
|
parser.add_argument(
|
||
|
|
"--trt_no_fp16",
|
||
|
|
action="store_true",
|
||
|
|
help="Desativa FP16 no TensorRT. Normalmente NÃO usar; deixamos FP16 ligado.",
|
||
|
|
)
|
||
|
|
|
||
|
|
parser.add_argument(
|
||
|
|
"--torch_no_amp",
|
||
|
|
action="store_true",
|
||
|
|
help="Desativa AMP no PyTorch. Recomendado para comparação mais rígida contra ONNX FP32.",
|
||
|
|
)
|
||
|
|
|
||
|
|
parser.add_argument(
|
||
|
|
"--resize_torch_to_input",
|
||
|
|
action="store_true",
|
||
|
|
help="Força saída PyTorch redimensionada para HxW antes de comparar.",
|
||
|
|
)
|
||
|
|
|
||
|
|
parser.add_argument(
|
||
|
|
"--onnx_has_norm",
|
||
|
|
action="store_true",
|
||
|
|
help="Use quando o ONNX já inclui normalização interna. Nesse caso o ONNX recebe tensor 0..1, não tensor normalizado.",
|
||
|
|
)
|
||
|
|
|
||
|
|
parser.add_argument(
|
||
|
|
"--onnx_output_kind",
|
||
|
|
default="logits",
|
||
|
|
choices=["logits", "mask"],
|
||
|
|
help="Tipo de saída do ONNX: logits para modelo cru, mask para ONNX com argmax/postprocess embutido.",
|
||
|
|
)
|
||
|
|
|
||
|
|
parser.add_argument(
|
||
|
|
"--compare_at_input_size",
|
||
|
|
action="store_true",
|
||
|
|
help="Redimensiona ambos os outputs para HxW antes de comparar.",
|
||
|
|
)
|
||
|
|
|
||
|
|
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())
|
||
|
|
train_script_path = resolve_path(args.train_script, Path.cwd())
|
||
|
|
labelmap_path = resolve_path(args.labelmap, Path.cwd())
|
||
|
|
|
||
|
|
if config_path is None or not config_path.is_file():
|
||
|
|
raise FileNotFoundError(f"Config não encontrado: {config_path}")
|
||
|
|
if train_script_path is None or not train_script_path.is_file():
|
||
|
|
raise FileNotFoundError(f"Train script não encontrado: {train_script_path}")
|
||
|
|
if labelmap_path is None or not labelmap_path.is_file():
|
||
|
|
raise FileNotFoundError(f"Labelmap não encontrado: {labelmap_path}")
|
||
|
|
|
||
|
|
config_dir = config_path.parent
|
||
|
|
config = load_json(config_path)
|
||
|
|
|
||
|
|
train_mod = import_train_module(train_script_path)
|
||
|
|
|
||
|
|
W, H = config.get("resolucao", [1024, 640])
|
||
|
|
W = int(W)
|
||
|
|
H = int(H)
|
||
|
|
|
||
|
|
backbone = config.get("backbone", "nvidia/mit-b1")
|
||
|
|
input_channel_names = train_mod.get_input_channel_names(config)
|
||
|
|
input_channel_indices = train_mod.get_input_channel_indices(config)
|
||
|
|
channels = len(input_channel_names)
|
||
|
|
|
||
|
|
checkpoint_path, onnx_path, ckpt_name = resolve_model_artifact_paths(
|
||
|
|
args=args,
|
||
|
|
config=config,
|
||
|
|
config_dir=config_dir,
|
||
|
|
channels=channels,
|
||
|
|
)
|
||
|
|
|
||
|
|
semantic_id2label, semantic_label2id, ignore_from_labelmap = train_mod.load_labelmap(
|
||
|
|
str(labelmap_path)
|
||
|
|
)
|
||
|
|
|
||
|
|
heads_config = train_mod.build_heads_config(
|
||
|
|
config,
|
||
|
|
ignore_index=int(ignore_from_labelmap)
|
||
|
|
)
|
||
|
|
|
||
|
|
heads_config["semantic"]["num_classes"] = int(len(semantic_id2label))
|
||
|
|
heads_config["semantic"]["ignore_index"] = int(ignore_from_labelmap)
|
||
|
|
|
||
|
|
output_heads = list(heads_config.keys())
|
||
|
|
|
||
|
|
save_dir = (
|
||
|
|
config_dir
|
||
|
|
/ "backup"
|
||
|
|
/ config.get("modelo", "segformer_b1")
|
||
|
|
/ config.get("model_name", "test")
|
||
|
|
/ f"{config.get('fusion_mode', 'stacked')}_raw{channels}"
|
||
|
|
)
|
||
|
|
|
||
|
|
norm_stats_path = find_norm_stats(
|
||
|
|
config=config,
|
||
|
|
config_dir=config_dir,
|
||
|
|
save_dir=save_dir,
|
||
|
|
explicit=args.norm_stats,
|
||
|
|
)
|
||
|
|
|
||
|
|
mean, std, norm_stats_used = load_norm_stats(
|
||
|
|
norm_stats_path,
|
||
|
|
channels=channels,
|
||
|
|
channel_indices=input_channel_indices,
|
||
|
|
channel_names=input_channel_names,
|
||
|
|
)
|
||
|
|
|
||
|
|
if args.root_override:
|
||
|
|
root = resolve_path(args.root_override, Path.cwd())
|
||
|
|
else:
|
||
|
|
root = (config_dir / "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 = collect_tensor_samples(
|
||
|
|
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 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, {channels}, {H}, {W}]")
|
||
|
|
print(f"Channels : {input_channel_names} idx={input_channel_indices}")
|
||
|
|
print(f"Heads : {output_heads}")
|
||
|
|
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"Compare HxW : {args.compare_at_input_size}")
|
||
|
|
print("==========================================")
|
||
|
|
|
||
|
|
print("[MODEL] Montando PyTorch...")
|
||
|
|
model = train_mod.build_model(
|
||
|
|
backbone=backbone,
|
||
|
|
channels=channels,
|
||
|
|
heads_config=heads_config,
|
||
|
|
semantic_id2label=semantic_id2label,
|
||
|
|
semantic_label2id=semantic_label2id,
|
||
|
|
)
|
||
|
|
|
||
|
|
ckpt = torch.load(str(checkpoint_path), map_location="cpu", weights_only=False)
|
||
|
|
if "model" not in ckpt:
|
||
|
|
raise RuntimeError("Checkpoint não contém chave 'model'.")
|
||
|
|
|
||
|
|
model.load_state_dict(ckpt["model"], strict=True)
|
||
|
|
model.to(device)
|
||
|
|
model.eval()
|
||
|
|
|
||
|
|
torch_wrapper = MultiHeadTorchWrapper(
|
||
|
|
model=model,
|
||
|
|
output_heads=output_heads,
|
||
|
|
resize_to_input=args.resize_torch_to_input or args.onnx_output_kind == "mask",
|
||
|
|
output_kind=args.onnx_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
|
||
|
|
print(f"[ONNX] input name: {onnx_input_name}")
|
||
|
|
print(f"[ONNX] outputs: {[o.name for o in onnx_session.get_outputs()]}")
|
||
|
|
|
||
|
|
per_head_accum = {
|
||
|
|
h: {
|
||
|
|
"n": 0,
|
||
|
|
"logits_abs_mean": [],
|
||
|
|
"logits_abs_max": [],
|
||
|
|
"prob_abs_mean": [],
|
||
|
|
"prob_abs_max": [],
|
||
|
|
"argmax_equal_ratio": [],
|
||
|
|
"pred_miou_torch_vs_onnx": [],
|
||
|
|
"pred_iou_per_class": [],
|
||
|
|
}
|
||
|
|
for h in output_heads
|
||
|
|
}
|
||
|
|
|
||
|
|
sample_reports = []
|
||
|
|
|
||
|
|
for i, tensor_path in enumerate(samples):
|
||
|
|
chw01 = load_tensor(
|
||
|
|
tensor_path,
|
||
|
|
channels=channels,
|
||
|
|
channel_indices=input_channel_indices,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Garante resolução do contrato.
|
||
|
|
if chw01.shape[-2:] != (H, W):
|
||
|
|
hwc = np.transpose(chw01, (1, 2, 0))
|
||
|
|
hwc = cv2.resize(hwc, (W, H), interpolation=cv2.INTER_LINEAR)
|
||
|
|
chw01 = np.transpose(hwc, (2, 0, 1)).astype(np.float32)
|
||
|
|
|
||
|
|
chw_norm = normalize_numpy_chw(chw01, mean=mean, std=std)
|
||
|
|
|
||
|
|
# PyTorch continua recebendo normalizado, porque o modelo PyTorch puro espera isso.
|
||
|
|
x_torch_np = np.expand_dims(chw_norm, axis=0).astype(np.float32)
|
||
|
|
|
||
|
|
# ONNX novo com include_norm recebe 0..1 cru.
|
||
|
|
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_outputs = {
|
||
|
|
h: v.detach().float().cpu().numpy()
|
||
|
|
for h, v in torch_outputs_t.items()
|
||
|
|
}
|
||
|
|
|
||
|
|
t0 = time.perf_counter()
|
||
|
|
onnx_raw_outputs = run_onnx(onnx_session, onnx_input_name, x_onnx_np)
|
||
|
|
onnx_ms = (time.perf_counter() - t0) * 1000.0
|
||
|
|
|
||
|
|
onnx_outputs = normalize_onnx_output_names(
|
||
|
|
onnx_raw_outputs,
|
||
|
|
output_heads=output_heads,
|
||
|
|
)
|
||
|
|
|
||
|
|
report_item = {
|
||
|
|
"idx": i,
|
||
|
|
"tensor": str(tensor_path),
|
||
|
|
"torch_ms": float(torch_ms),
|
||
|
|
"onnx_ms": float(onnx_ms),
|
||
|
|
"heads": {},
|
||
|
|
}
|
||
|
|
|
||
|
|
print(f"\n[{i + 1:03d}/{len(samples):03d}] {tensor_path.name} | torch={torch_ms:.2f}ms | onnx={onnx_ms:.2f}ms")
|
||
|
|
|
||
|
|
for head in output_heads:
|
||
|
|
pt = torch_outputs[head]
|
||
|
|
ox = onnx_outputs[head]
|
||
|
|
|
||
|
|
if args.onnx_output_kind == "mask":
|
||
|
|
# Esperado:
|
||
|
|
# PT : [1,H,W]
|
||
|
|
# ONNX : [1,H,W]
|
||
|
|
pt_mask = np.asarray(pt).astype(np.uint8)
|
||
|
|
ox_mask = np.asarray(ox).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))
|
||
|
|
|
||
|
|
num_classes = int(heads_config[head]["num_classes"])
|
||
|
|
iou_per_class, miou, present_classes = compute_mask_iou_between_preds(
|
||
|
|
pt_mask,
|
||
|
|
ox_mask,
|
||
|
|
num_classes=num_classes,
|
||
|
|
)
|
||
|
|
|
||
|
|
head_report = {
|
||
|
|
"torch_shape": list(np.asarray(pt).shape),
|
||
|
|
"onnx_shape": list(np.asarray(ox).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": float(equal_ratio),
|
||
|
|
"pred_miou_torch_vs_onnx": float(miou),
|
||
|
|
"pred_iou_per_class": [
|
||
|
|
None if x is None else float(x)
|
||
|
|
for x in iou_per_class
|
||
|
|
],
|
||
|
|
"pred_present_classes": [int(x) for x in present_classes],
|
||
|
|
}
|
||
|
|
|
||
|
|
print(
|
||
|
|
f" {head:<10} "
|
||
|
|
f"shape PT={tuple(np.asarray(pt).shape)} ONNX={tuple(np.asarray(ox).shape)} "
|
||
|
|
f"CMP={tuple(pt_mask.shape)} | "
|
||
|
|
f"mask_equal={equal_ratio * 100:.3f}% "
|
||
|
|
f"mIoU={miou:.6f}"
|
||
|
|
)
|
||
|
|
|
||
|
|
else:
|
||
|
|
pt = np.asarray(pt).astype(np.float32)
|
||
|
|
ox = np.asarray(ox).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 incompatível na head {head}: "
|
||
|
|
f"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))
|
||
|
|
|
||
|
|
num_classes = int(heads_config[head]["num_classes"])
|
||
|
|
iou_per_class, miou, present_classes = compute_mask_iou_between_preds(
|
||
|
|
pred_pt,
|
||
|
|
pred_ox,
|
||
|
|
num_classes=num_classes,
|
||
|
|
)
|
||
|
|
|
||
|
|
head_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": float(equal_ratio),
|
||
|
|
"pred_miou_torch_vs_onnx": float(miou),
|
||
|
|
"pred_iou_per_class": [
|
||
|
|
None if x is None else float(x)
|
||
|
|
for x in iou_per_class
|
||
|
|
],
|
||
|
|
"pred_present_classes": [int(x) for x in present_classes],
|
||
|
|
}
|
||
|
|
|
||
|
|
print(
|
||
|
|
f" {head:<10} "
|
||
|
|
f"shape PT={tuple(pt.shape)} ONNX={tuple(ox.shape)} CMP={tuple(pt_cmp.shape)} | "
|
||
|
|
f"logit_mean={head_report['logits_abs_mean']:.6g} "
|
||
|
|
f"prob_mean={head_report['prob_abs_mean']:.6g} "
|
||
|
|
f"argmax_equal={equal_ratio * 100:.3f}% "
|
||
|
|
f"mIoU={miou:.6f}"
|
||
|
|
)
|
||
|
|
|
||
|
|
report_item["heads"][head] = head_report
|
||
|
|
|
||
|
|
acc = per_head_accum[head]
|
||
|
|
acc["n"] += 1
|
||
|
|
|
||
|
|
if head_report["logits_abs_mean"] is not None:
|
||
|
|
acc["logits_abs_mean"].append(head_report["logits_abs_mean"])
|
||
|
|
|
||
|
|
if head_report["logits_abs_max"] is not None:
|
||
|
|
acc["logits_abs_max"].append(head_report["logits_abs_max"])
|
||
|
|
|
||
|
|
if head_report["prob_abs_mean"] is not None:
|
||
|
|
acc["prob_abs_mean"].append(head_report["prob_abs_mean"])
|
||
|
|
|
||
|
|
if head_report["prob_abs_max"] is not None:
|
||
|
|
acc["prob_abs_max"].append(head_report["prob_abs_max"])
|
||
|
|
|
||
|
|
acc["argmax_equal_ratio"].append(head_report["argmax_equal_ratio"])
|
||
|
|
acc["pred_miou_torch_vs_onnx"].append(head_report["pred_miou_torch_vs_onnx"])
|
||
|
|
acc["pred_iou_per_class"].append(head_report["pred_iou_per_class"])
|
||
|
|
|
||
|
|
#print(
|
||
|
|
# f" {head:<10} "
|
||
|
|
# f"shape PT={tuple(pt.shape)} ONNX={tuple(ox.shape)} CMP={tuple(pt_cmp.shape)} | "
|
||
|
|
# f"logit_mean={head_report['logits_abs_mean']:.6g} "
|
||
|
|
# f"prob_mean={head_report['prob_abs_mean']:.6g} "
|
||
|
|
# f"argmax_equal={equal_ratio * 100:.3f}% "
|
||
|
|
# f"mIoU={miou:.6f}"
|
||
|
|
#)
|
||
|
|
|
||
|
|
sample_reports.append(report_item)
|
||
|
|
|
||
|
|
summary = {
|
||
|
|
"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, channels, H, W],
|
||
|
|
"input_channel_names": input_channel_names,
|
||
|
|
"input_channel_indices": input_channel_indices,
|
||
|
|
"heads": output_heads,
|
||
|
|
"norm_stats_used": norm_stats_used,
|
||
|
|
"torch_amp": bool(not args.torch_no_amp and device.type == "cuda"),
|
||
|
|
"onnx_provider": args.onnx_provider,
|
||
|
|
"trt_home": args.trt_home or os.environ.get("TRT_HOME", None),
|
||
|
|
"trt_fp16": bool(not args.trt_no_fp16),
|
||
|
|
"compare_at_input_size": bool(args.compare_at_input_size),
|
||
|
|
"per_head": {},
|
||
|
|
"sample_reports": sample_reports,
|
||
|
|
}
|
||
|
|
|
||
|
|
print("\n========== RESUMO ==========")
|
||
|
|
|
||
|
|
for head, acc in per_head_accum.items():
|
||
|
|
if acc["n"] <= 0:
|
||
|
|
continue
|
||
|
|
|
||
|
|
ious_raw = 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)
|
||
|
|
|
||
|
|
def safe_mean(values):
|
||
|
|
return None if not values else float(np.mean(values))
|
||
|
|
|
||
|
|
def safe_max(values):
|
||
|
|
return None if not values else float(np.max(values))
|
||
|
|
|
||
|
|
head_summary = {
|
||
|
|
"n": int(acc["n"]),
|
||
|
|
"logits_abs_mean_avg": safe_mean(acc["logits_abs_mean"]),
|
||
|
|
"logits_abs_mean_max": safe_max(acc["logits_abs_mean"]),
|
||
|
|
"logits_abs_max_avg": safe_mean(acc["logits_abs_max"]),
|
||
|
|
"logits_abs_max_max": safe_max(acc["logits_abs_max"]),
|
||
|
|
"prob_abs_mean_avg": safe_mean(acc["prob_abs_mean"]),
|
||
|
|
"prob_abs_mean_max": safe_max(acc["prob_abs_mean"]),
|
||
|
|
"prob_abs_max_avg": safe_mean(acc["prob_abs_max"]),
|
||
|
|
"prob_abs_max_max": safe_max(acc["prob_abs_max"]),
|
||
|
|
"argmax_equal_ratio_avg": float(np.mean(acc["argmax_equal_ratio"])),
|
||
|
|
"argmax_equal_ratio_min": float(np.min(acc["argmax_equal_ratio"])),
|
||
|
|
"pred_miou_avg": float(np.mean(acc["pred_miou_torch_vs_onnx"])),
|
||
|
|
"pred_miou_min": float(np.min(acc["pred_miou_torch_vs_onnx"])),
|
||
|
|
"pred_iou_per_class_avg": nanmean_list(ious_arr),
|
||
|
|
"pred_iou_per_class_min": nanmin_list(ious_arr),
|
||
|
|
}
|
||
|
|
|
||
|
|
summary["per_head"][head] = head_summary
|
||
|
|
|
||
|
|
def fmt_iou_list(values):
|
||
|
|
return [
|
||
|
|
None if x is None else round(float(x), 6)
|
||
|
|
for x in values
|
||
|
|
]
|
||
|
|
|
||
|
|
def fmt_optional(v, casas=8):
|
||
|
|
if v is None:
|
||
|
|
return "N/A"
|
||
|
|
return f"{float(v):.{casas}f}"
|
||
|
|
|
||
|
|
print(f"\n[{head}]")
|
||
|
|
print(f" logits_abs_mean avg : {fmt_optional(head_summary['logits_abs_mean_avg'])}")
|
||
|
|
print(f" prob_abs_mean avg : {fmt_optional(head_summary['prob_abs_mean_avg'])}")
|
||
|
|
print(f" argmax_equal avg : {head_summary['argmax_equal_ratio_avg'] * 100:.4f}%")
|
||
|
|
print(f" argmax_equal min : {head_summary['argmax_equal_ratio_min'] * 100:.4f}%")
|
||
|
|
print(f" pred_mIoU avg : {head_summary['pred_miou_avg']:.8f}")
|
||
|
|
print(f" pred_mIoU min : {head_summary['pred_miou_min']:.8f}")
|
||
|
|
print(f" IoU/classes avg : {fmt_iou_list(head_summary['pred_iou_per_class_avg'])}")
|
||
|
|
print(f" IoU/classes min : {fmt_iou_list(head_summary['pred_iou_per_class_min'])}")
|
||
|
|
|
||
|
|
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()
|