1519 lines
53 KiB
Python
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()
|