Compare commits
2 Commits
2a5bb43232
...
4ec3c35759
| Author | SHA1 | Date |
|---|---|---|
|
|
4ec3c35759 | |
|
|
b74555301c |
|
|
@ -3,58 +3,67 @@
|
|||
"id": 1,
|
||||
"Arquivo": "model",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ervas\\",
|
||||
"Extensao": ".pt",
|
||||
"Versao": "3_1",
|
||||
"Extensao": ".onnx",
|
||||
"Versao": "4_0",
|
||||
"TipoArquivo": 1,
|
||||
"ArquivoDownload": "models/weed_detector_model-3_1.pt"
|
||||
"ArquivoDownload": "models/weed_detector_model-4_0.onnx"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"Arquivo": "model",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ervas\\",
|
||||
"Extensao": ".txt",
|
||||
"Versao": "3_1",
|
||||
"Versao": "4_0",
|
||||
"TipoArquivo": 1,
|
||||
"ArquivoDownload": "models/weed_detector_labelmap-3_1.txt"
|
||||
"ArquivoDownload": "models/weed_detector_labelmap-4_0.txt"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"Arquivo": "model",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ervas\\",
|
||||
"Extensao": ".json",
|
||||
"Versao": "1_1",
|
||||
"Versao": "4_0",
|
||||
"TipoArquivo": 1,
|
||||
"ArquivoDownload": "models/weed_detector_normstats-1_1.json"
|
||||
"ArquivoDownload": "models/weed_detector_normstats-4_0.json"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"Arquivo": "modelseg",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
||||
"Extensao": ".pt",
|
||||
"Versao": "1_3",
|
||||
"TipoArquivo": 0,
|
||||
"ArquivoDownload": "models/street_detector_model_seg-1_3.pt",
|
||||
"Arquivo": "modelmp",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ervas\\",
|
||||
"Extensao": ".json",
|
||||
"Versao": "4_0",
|
||||
"TipoArquivo": 1,
|
||||
"ArquivoDownload": "models/weed_detector_moduleparams-4_0.json"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"Arquivo": "modelseg",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
||||
"Extensao": ".txt",
|
||||
"Versao": "1_3",
|
||||
"Extensao": ".onnx",
|
||||
"Versao": "2_0",
|
||||
"TipoArquivo": 0,
|
||||
"ArquivoDownload": "models/street_detector_labelmap_seg-1_3.txt"
|
||||
"ArquivoDownload": "models/street_detector_model_seg-2_0.onnx",
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"Arquivo": "modelseg",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
||||
"Extensao": ".json",
|
||||
"Versao": "1_1",
|
||||
"Extensao": ".txt",
|
||||
"Versao": "2_0",
|
||||
"TipoArquivo": 0,
|
||||
"ArquivoDownload": "models/street_detector_normstats-1_1.json"
|
||||
"ArquivoDownload": "models/street_detector_labelmap_seg-2_0.txt"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"Arquivo": "modelseg",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
||||
"Extensao": ".json",
|
||||
"Versao": "1_4",
|
||||
"TipoArquivo": 0,
|
||||
"ArquivoDownload": "models/street_detector_normstats-1_4.json"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"Arquivo": "modeldet",
|
||||
"Diretorio": "C:\\AgroBaseModels\\Ruas\\",
|
||||
"Extensao": ".blob",
|
||||
|
|
@ -63,7 +72,7 @@
|
|||
"ArquivoDownload": "models/street_detector_model_det-1_0.blob",
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"id": 9,
|
||||
"Arquivo": "parametersAtu",
|
||||
"Diretorio": "Parametros/",
|
||||
"Extensao": ".par",
|
||||
|
|
@ -72,7 +81,7 @@
|
|||
"ArquivoDownload": "parameters/parametersAtu-2_0.par",
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"id": 10,
|
||||
"Arquivo": "parametersMvd",
|
||||
"Diretorio": "Parametros/",
|
||||
"Extensao": ".par",
|
||||
|
|
@ -81,7 +90,7 @@
|
|||
"ArquivoDownload": "parameters/parametersMvd-2_0.par"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"id": 11,
|
||||
"Arquivo": "parametersSen",
|
||||
"Diretorio": "Parametros/",
|
||||
"Extensao": ".par",
|
||||
|
|
@ -90,7 +99,7 @@
|
|||
"ArquivoDownload": "parameters/parametersSen-2_0.par"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"id": 12,
|
||||
"Arquivo": "pinoutAtu",
|
||||
"Diretorio": "Parametros/",
|
||||
"Extensao": ".pin",
|
||||
|
|
@ -99,7 +108,7 @@
|
|||
"ArquivoDownload": "parameters/pinoutAtu-2_0.pin"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"id": 13,
|
||||
"Arquivo": "pinoutSen",
|
||||
"Diretorio": "Parametros/",
|
||||
"Extensao": ".pin",
|
||||
|
|
@ -108,7 +117,7 @@
|
|||
"ArquivoDownload": "parameters/pinoutSen-2_0.pin"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"id": 14,
|
||||
"Arquivo": "weed_detector_oak",
|
||||
"Diretorio": "Python\\Scripts\\",
|
||||
"Extensao": ".py",
|
||||
|
|
@ -117,7 +126,7 @@
|
|||
"ArquivoDownload": "weed_detector_oak-1_0.py"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"id": 15,
|
||||
"Arquivo": "map_load",
|
||||
"Diretorio": "Python\\Scripts\\",
|
||||
"Extensao": ".py",
|
||||
|
|
@ -126,7 +135,7 @@
|
|||
"ArquivoDownload": "scripts/map_load-1_0.py"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"id": 16,
|
||||
"Arquivo": "map_follow",
|
||||
"Diretorio": "Python\\Scripts\\",
|
||||
"Extensao": ".py",
|
||||
|
|
@ -135,7 +144,7 @@
|
|||
"ArquivoDownload": "scripts/map_follow-1_0.py"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"id": 17,
|
||||
"Arquivo": "gps_viewer",
|
||||
"Diretorio": "Python\\Scripts\\",
|
||||
"Extensao": ".py",
|
||||
|
|
@ -144,7 +153,7 @@
|
|||
"ArquivoDownload": "scripts/gps_viewer-1_0.py"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"id": 18,
|
||||
"Arquivo": "modelo_3d",
|
||||
"Diretorio": "Python\\Output\\",
|
||||
"Extensao": ".obj",
|
||||
|
|
@ -153,7 +162,7 @@
|
|||
"ArquivoDownload": "modelo_3d-1_0.obj"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"id": 19,
|
||||
"Arquivo": "modelo_3d",
|
||||
"Diretorio": "Python\\Output\\",
|
||||
"Extensao": ".mtl",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,184 +1,575 @@
|
|||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from transformers import SegformerConfig, SegformerForSemanticSegmentation
|
||||
|
||||
|
||||
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 = feat_ch + num_seg_classes
|
||||
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:
|
||||
if feat.shape[-2:] != logits_seg.shape[-2:]:
|
||||
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)
|
||||
import onnxruntime as ort
|
||||
|
||||
|
||||
class SegformerNavRunner:
|
||||
IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
||||
IMAGENET_STD = [0.229, 0.224, 0.225]
|
||||
"""
|
||||
Runner ONNX/TensorRT para segmentação navegável do Visual Worker.
|
||||
|
||||
def __init__(self, seg_config, device="cuda"):
|
||||
Contrato novo:
|
||||
- Não carrega .pt.
|
||||
- Não usa Torch.
|
||||
- Não usa HuggingFace.
|
||||
- Não lê norm_stats para normalizar fora.
|
||||
- Assume que o ONNX já contém resize/normalização quando onnx_has_preprocess=True.
|
||||
|
||||
Entrada principal:
|
||||
frame_rgb: np.ndarray HxWx3, RGB, uint8
|
||||
|
||||
Retorno de infer_ids:
|
||||
pred_ids_np, ts, roi_resized, roi_info, aux_result
|
||||
|
||||
Onde:
|
||||
pred_ids_np : máscara HxW com ids de classe
|
||||
ts : timestamp da inferência
|
||||
roi_resized : ROI RGB redimensionada para debug/visualização
|
||||
roi_info : (y_fim, y_inicio) no frame original
|
||||
aux_result : dict de label/status, se o ONNX exportar saída auxiliar
|
||||
"""
|
||||
|
||||
def __init__(self, seg_config: Dict[str, Any]):
|
||||
from shared.utils import carregar_labelmap_completo
|
||||
|
||||
self.device = torch.device(device if torch.cuda.is_available() else "cpu")
|
||||
self.config = dict(seg_config)
|
||||
|
||||
self.use_amp = bool(seg_config.get("use_amp", True))
|
||||
self.use_channels_last = bool(seg_config.get("use_channels_last", True))
|
||||
self.use_compact_aux = bool(seg_config.get("use_compact_aux", True))
|
||||
self.debug_timing = bool(seg_config.get("debug_timing", False))
|
||||
self.debug_session = bool(seg_config.get("debug_session", True))
|
||||
self.use_compact_aux = bool(seg_config.get("use_compact_aux", True))
|
||||
|
||||
self.resolucao = self._read_resolution(seg_config)
|
||||
self.roi_inicio = float(seg_config.get("ia_roi_begin", 0.0))
|
||||
self.roi_tamanho = float(seg_config.get("ia_roi_size", 1.0))
|
||||
|
||||
self.onnx_path = (
|
||||
seg_config.get("ia_onnx_path")
|
||||
or seg_config.get("onnx_path")
|
||||
or seg_config.get("ia_model_path")
|
||||
)
|
||||
|
||||
if not self.onnx_path:
|
||||
raise ValueError(
|
||||
"SegformerNavRunner ONNX precisa de 'ia_onnx_path', 'onnx_path' "
|
||||
"ou 'ia_model_path' apontando para o arquivo .onnx."
|
||||
)
|
||||
|
||||
if not os.path.isfile(self.onnx_path):
|
||||
raise FileNotFoundError(f"Arquivo ONNX não encontrado: {self.onnx_path}")
|
||||
|
||||
self.provider_mode = str(seg_config.get("onnx_provider", "tensorrt")).lower()
|
||||
self.onnx_has_preprocess = bool(seg_config.get("onnx_has_preprocess", True))
|
||||
|
||||
# Formatos aceitos:
|
||||
# - auto: tenta inferir pela saída
|
||||
# - ids: saída já é máscara de ids
|
||||
# - logits: saída é [N,C,H,W] ou [C,H,W]
|
||||
self.seg_output_format = str(seg_config.get("onnx_seg_output_format", "auto")).lower()
|
||||
|
||||
# Formato de entrada quando o ONNX já tem preprocess embutido.
|
||||
# Recomendado para seu contrato novo: nhwc_uint8.
|
||||
#
|
||||
# Opções:
|
||||
# - nhwc_uint8 -> [1,H,W,3] uint8
|
||||
# - nhwc_float -> [1,H,W,3] float32, 0..255 ou 0..1 conforme input_scale
|
||||
# - nchw_float -> [1,3,H,W] float32
|
||||
self.input_layout = str(seg_config.get("onnx_input_layout", "nhwc_uint8")).lower()
|
||||
self.input_scale = float(seg_config.get("onnx_input_scale", 1.0))
|
||||
|
||||
self.input_name_forced = seg_config.get("onnx_input_name")
|
||||
self.seg_output_name_forced = seg_config.get("onnx_seg_output_name")
|
||||
self.aux_output_name_forced = seg_config.get("onnx_aux_output_name")
|
||||
|
||||
self.label_names = self._load_label_names(seg_config)
|
||||
|
||||
self._last_aux_result = None
|
||||
self._last_aux_ts = 0.0
|
||||
self.last_infer = None
|
||||
|
||||
self.cor_para_id, self.colormap_rgb, self.classes, self.ignore_rgb = carregar_labelmap_completo(
|
||||
seg_config["ia_labelmap_path"]
|
||||
)
|
||||
|
||||
self.resolucao = tuple(seg_config["ia_resolution"]) # [W,H]
|
||||
self.roi_inicio = seg_config["ia_roi_begin"]
|
||||
self.roi_tamanho = seg_config["ia_roi_size"]
|
||||
self.last_infer = None
|
||||
self.session = self._create_session()
|
||||
self.input_name = self._resolve_input_name()
|
||||
self.output_names = [o.name for o in self.session.get_outputs()]
|
||||
|
||||
self.mode = seg_config.get("ia_mode", "dual_label")
|
||||
self.model = None
|
||||
self.aux_head = None
|
||||
self.label_names = {}
|
||||
self.input_meta = next(i for i in self.session.get_inputs() if i.name == self.input_name)
|
||||
self.input_type = str(self.input_meta.type)
|
||||
self.input_shape = self.input_meta.shape
|
||||
|
||||
self._load_dual_checkpoint(seg_config)
|
||||
self.seg_output_name = self._resolve_seg_output_name()
|
||||
self.aux_output_name = self._resolve_aux_output_name()
|
||||
|
||||
norm_mean = self.IMAGENET_MEAN
|
||||
norm_std = self.IMAGENET_STD
|
||||
if self.debug_session:
|
||||
self._print_session_summary()
|
||||
|
||||
norm_stats_path = seg_config.get("ia_norm_stats_path")
|
||||
if norm_stats_path and os.path.isfile(norm_stats_path):
|
||||
with open(norm_stats_path, "r", encoding="utf-8") as f:
|
||||
norm_stats = json.load(f)
|
||||
# -------------------------------------------------------------------------
|
||||
# Inicialização ONNX
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
stats_channels = norm_stats.get("channels", [])
|
||||
stats_mean = norm_stats.get("mean", [])
|
||||
stats_std = norm_stats.get("std", [])
|
||||
def _create_session(self) -> ort.InferenceSession:
|
||||
sess_options = ort.SessionOptions()
|
||||
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
|
||||
idx_by_name = {name: i for i, name in enumerate(stats_channels)}
|
||||
intra_threads = int(self.config.get("onnx_intra_op_num_threads", 1))
|
||||
inter_threads = int(self.config.get("onnx_inter_op_num_threads", 1))
|
||||
|
||||
if all(ch in idx_by_name for ch in ["R", "G", "B"]):
|
||||
norm_mean = [
|
||||
stats_mean[idx_by_name["R"]],
|
||||
stats_mean[idx_by_name["G"]],
|
||||
stats_mean[idx_by_name["B"]],
|
||||
]
|
||||
norm_std = [
|
||||
stats_std[idx_by_name["R"]],
|
||||
stats_std[idx_by_name["G"]],
|
||||
stats_std[idx_by_name["B"]],
|
||||
]
|
||||
print(f"[NORM] usando stats fixos de: {norm_stats_path}")
|
||||
else:
|
||||
print("[NORM] norm_stats não contém R,G,B. Usando ImageNet.")
|
||||
else:
|
||||
print(f"[NORM] norm_stats não encontrado em {norm_stats_path}. Usando ImageNet.")
|
||||
if intra_threads > 0:
|
||||
sess_options.intra_op_num_threads = intra_threads
|
||||
if inter_threads > 0:
|
||||
sess_options.inter_op_num_threads = inter_threads
|
||||
|
||||
self.set_norm_stats(norm_mean, norm_std)
|
||||
providers = self._build_providers()
|
||||
|
||||
def set_norm_stats(self, mean, std):
|
||||
self._norm_mean = torch.tensor(mean, dtype=torch.float32, device=self.device).view(3, 1, 1)
|
||||
self._norm_std = torch.tensor(std, dtype=torch.float32, device=self.device).view(3, 1, 1).clamp_min(1e-6)
|
||||
try:
|
||||
return ort.InferenceSession(
|
||||
self.onnx_path,
|
||||
sess_options=sess_options,
|
||||
providers=providers,
|
||||
)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"Falha ao carregar ONNX: {self.onnx_path} | "
|
||||
f"provider_mode={self.provider_mode} | erro={e}"
|
||||
) from e
|
||||
|
||||
def normalize_img(self, img):
|
||||
return (img - self._norm_mean) / self._norm_std
|
||||
def _build_providers(self) -> List[Any]:
|
||||
available = set(ort.get_available_providers())
|
||||
|
||||
def _build_base_model(self, backbone: str, num_classes: int):
|
||||
config = SegformerConfig.from_pretrained(
|
||||
backbone,
|
||||
local_files_only=True
|
||||
if self.provider_mode in ("tensorrt", "trt"):
|
||||
providers: List[Any] = []
|
||||
|
||||
if "TensorrtExecutionProvider" in available:
|
||||
trt_options = {
|
||||
"trt_fp16_enable": bool(self.config.get("trt_fp16_enable", True)),
|
||||
"trt_engine_cache_enable": bool(self.config.get("trt_engine_cache_enable", True)),
|
||||
"trt_engine_cache_path": str(
|
||||
self.config.get("trt_engine_cache_path", "./trt_cache_visual_worker")
|
||||
),
|
||||
}
|
||||
|
||||
max_workspace = self.config.get("trt_max_workspace_size")
|
||||
if max_workspace is not None:
|
||||
trt_options["trt_max_workspace_size"] = int(max_workspace)
|
||||
|
||||
providers.append(("TensorrtExecutionProvider", trt_options))
|
||||
|
||||
if "CUDAExecutionProvider" in available:
|
||||
providers.append("CUDAExecutionProvider")
|
||||
|
||||
providers.append("CPUExecutionProvider")
|
||||
return providers
|
||||
|
||||
if self.provider_mode in ("cuda", "gpu"):
|
||||
providers = []
|
||||
if "CUDAExecutionProvider" in available:
|
||||
providers.append("CUDAExecutionProvider")
|
||||
providers.append("CPUExecutionProvider")
|
||||
return providers
|
||||
|
||||
return ["CPUExecutionProvider"]
|
||||
|
||||
def _resolve_input_name(self) -> str:
|
||||
inputs = self.session.get_inputs()
|
||||
|
||||
if not inputs:
|
||||
raise RuntimeError("ONNX não possui entradas.")
|
||||
|
||||
if self.input_name_forced:
|
||||
names = [i.name for i in inputs]
|
||||
if self.input_name_forced not in names:
|
||||
raise RuntimeError(
|
||||
f"onnx_input_name='{self.input_name_forced}' não encontrado. "
|
||||
f"Entradas disponíveis: {names}"
|
||||
)
|
||||
return str(self.input_name_forced)
|
||||
|
||||
return inputs[0].name
|
||||
|
||||
def _resolve_seg_output_name(self) -> str:
|
||||
if self.seg_output_name_forced:
|
||||
if self.seg_output_name_forced not in self.output_names:
|
||||
raise RuntimeError(
|
||||
f"onnx_seg_output_name='{self.seg_output_name_forced}' não encontrado. "
|
||||
f"Saídas disponíveis: {self.output_names}"
|
||||
)
|
||||
return str(self.seg_output_name_forced)
|
||||
|
||||
# Preferências comuns.
|
||||
preferred = [
|
||||
"seg_ids",
|
||||
"pred_ids",
|
||||
"mask_ids",
|
||||
"segmentation",
|
||||
"seg",
|
||||
"logits",
|
||||
"seg_logits",
|
||||
"output",
|
||||
]
|
||||
|
||||
lower_by_name = {name.lower(): name for name in self.output_names}
|
||||
|
||||
for p in preferred:
|
||||
if p in lower_by_name:
|
||||
return lower_by_name[p]
|
||||
|
||||
return self.output_names[0]
|
||||
|
||||
def _resolve_aux_output_name(self) -> Optional[str]:
|
||||
if self.aux_output_name_forced:
|
||||
if self.aux_output_name_forced not in self.output_names:
|
||||
raise RuntimeError(
|
||||
f"onnx_aux_output_name='{self.aux_output_name_forced}' não encontrado. "
|
||||
f"Saídas disponíveis: {self.output_names}"
|
||||
)
|
||||
return str(self.aux_output_name_forced)
|
||||
|
||||
if len(self.output_names) <= 1:
|
||||
return None
|
||||
|
||||
preferred = [
|
||||
"label_logits",
|
||||
"aux_logits",
|
||||
"status_logits",
|
||||
"label",
|
||||
"aux",
|
||||
"status",
|
||||
]
|
||||
|
||||
lower_by_name = {name.lower(): name for name in self.output_names}
|
||||
|
||||
for p in preferred:
|
||||
if p in lower_by_name:
|
||||
return lower_by_name[p]
|
||||
|
||||
for name in self.output_names:
|
||||
if name != self.seg_output_name:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
def _print_session_summary(self):
|
||||
print("[SEG_ONNX] Runner carregado")
|
||||
print(f"[SEG_ONNX] modelo : {self.onnx_path}")
|
||||
print(f"[SEG_ONNX] provider_mode : {self.provider_mode}")
|
||||
print(f"[SEG_ONNX] providers ativos: {self.session.get_providers()}")
|
||||
print(f"[SEG_ONNX] input : {self.input_name}")
|
||||
print(f"[SEG_ONNX] outputs : {self.output_names}")
|
||||
print(f"[SEG_ONNX] seg_output : {self.seg_output_name}")
|
||||
print(f"[SEG_ONNX] aux_output : {self.aux_output_name}")
|
||||
print(f"[SEG_ONNX] resolução : {self.resolucao}")
|
||||
print(f"[SEG_ONNX] input_layout : {self.input_layout}")
|
||||
print(f"[SEG_ONNX] has_preprocess : {self.onnx_has_preprocess}")
|
||||
input_meta = self.session.get_inputs()[0]
|
||||
print(f"[SEG_ONNX] input type : {input_meta.type}")
|
||||
print(f"[SEG_ONNX] input shape : {input_meta.shape}")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# API principal
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def infer_ids(self, frame_rgb: np.ndarray):
|
||||
if frame_rgb is None or not hasattr(frame_rgb, "shape") or frame_rgb.size == 0:
|
||||
return None, None, None, None, None
|
||||
|
||||
t0 = time.perf_counter()
|
||||
|
||||
H, W = frame_rgb.shape[:2]
|
||||
y_fim, y_inicio = self.compute_roi_indices(
|
||||
H,
|
||||
self.roi_inicio,
|
||||
self.roi_tamanho,
|
||||
)
|
||||
|
||||
config.num_labels = int(num_classes)
|
||||
config.output_hidden_states = True
|
||||
roi_rgb = frame_rgb[y_fim:y_inicio, 0:W]
|
||||
t_crop = time.perf_counter()
|
||||
|
||||
model = SegformerForSemanticSegmentation(config)
|
||||
return model
|
||||
if roi_rgb is None or roi_rgb.size == 0:
|
||||
return None, None, None, (y_fim, y_inicio), None
|
||||
|
||||
def _load_dual_checkpoint(self, seg_config):
|
||||
pt_path = seg_config["ia_model_path"]
|
||||
backbone = seg_config["ia_backbone"]
|
||||
num_classes = len(self.classes)
|
||||
input_tensor, roi_resized = self._prepare_input(roi_rgb)
|
||||
t_pre = time.perf_counter()
|
||||
|
||||
ckpt = torch.load(pt_path, map_location="cpu", weights_only=False)
|
||||
self.model = self._build_base_model(backbone, num_classes)
|
||||
self.model.load_state_dict(ckpt["model"], strict=True)
|
||||
self.model.to(self.device).eval()
|
||||
requested_outputs = [self.seg_output_name]
|
||||
if self.aux_output_name:
|
||||
requested_outputs.append(self.aux_output_name)
|
||||
|
||||
if self.use_channels_last and self.device.type == "cuda":
|
||||
self.model.to(memory_format=torch.channels_last)
|
||||
outputs = self.session.run(
|
||||
requested_outputs,
|
||||
{self.input_name: input_tensor},
|
||||
)
|
||||
t_model = time.perf_counter()
|
||||
|
||||
torch.backends.cudnn.benchmark = True
|
||||
seg_out = outputs[0]
|
||||
pred_ids_np = self._decode_seg_output(seg_out)
|
||||
|
||||
extra = ckpt.get("extra", {}) or {}
|
||||
label_names_raw = extra.get("label_name_by_id", {}) or {}
|
||||
self.label_names = {int(k): str(v) for k, v in label_names_raw.items()} if label_names_raw else {}
|
||||
|
||||
if self.mode == "dual_label":
|
||||
aux_sd = ckpt.get("aux_head")
|
||||
if aux_sd is None:
|
||||
raise RuntimeError("Checkpoint não possui aux_head. Este arquivo parece não ser dual_head_label.")
|
||||
|
||||
max_label_id = -1
|
||||
|
||||
for k, v in aux_sd.items():
|
||||
if k.endswith("net.3.weight") or k.endswith("net.3.bias"):
|
||||
max_label_id = int(v.shape[0]) - 1
|
||||
break
|
||||
|
||||
if max_label_id < 0:
|
||||
raise RuntimeError("Não consegui inferir número de classes da LabelHead.")
|
||||
|
||||
num_label_classes = max_label_id + 1
|
||||
|
||||
for i in range(num_label_classes):
|
||||
self.label_names.setdefault(i, f"label_{i}")
|
||||
|
||||
feat_ch = int(self.model.config.hidden_sizes[-1])
|
||||
|
||||
self.aux_head = LabelHead(
|
||||
feat_ch=feat_ch,
|
||||
num_seg_classes=num_classes,
|
||||
num_label_classes=num_label_classes,
|
||||
hidden=256,
|
||||
dropout=0.2,
|
||||
expected_w, expected_h = self.resolucao
|
||||
if pred_ids_np.shape[:2] != (expected_h, expected_w):
|
||||
pred_ids_np = cv2.resize(
|
||||
pred_ids_np,
|
||||
(expected_w, expected_h),
|
||||
interpolation=cv2.INTER_NEAREST,
|
||||
)
|
||||
|
||||
self.aux_head.load_state_dict(aux_sd, strict=True)
|
||||
self.aux_head.to(self.device).eval()
|
||||
if self.use_channels_last and self.device.type == "cuda":
|
||||
# Linear não usa channels_last, mas manter aqui não atrapalha.
|
||||
pass
|
||||
pred_ids_np = np.ascontiguousarray(pred_ids_np.astype(np.uint8, copy=False))
|
||||
|
||||
print(f"[DUAL] LabelHead carregada: classes={num_label_classes} names={self.label_names}")
|
||||
aux_result = None
|
||||
if self.aux_output_name and len(outputs) > 1:
|
||||
aux_result = self._decode_aux_output(outputs[1])
|
||||
|
||||
self.last_infer = time.time()
|
||||
t_end = time.perf_counter()
|
||||
|
||||
if self.debug_timing:
|
||||
print(
|
||||
f"RUNNER_ONNX | "
|
||||
f"crop={(t_crop - t0) * 1000:.1f}ms | "
|
||||
f"pre={(t_pre - t_crop) * 1000:.1f}ms | "
|
||||
f"model={(t_model - t_pre) * 1000:.1f}ms | "
|
||||
f"post={(t_end - t_model) * 1000:.1f}ms | "
|
||||
f"total={(t_end - t0) * 1000:.1f}ms"
|
||||
)
|
||||
|
||||
return pred_ids_np, self.last_infer, roi_resized, (y_fim, y_inicio), aux_result
|
||||
|
||||
def infer_ids_seg_only(self, frame_rgb: np.ndarray):
|
||||
"""
|
||||
Mantido como alias para o novo contrato.
|
||||
Como este runner já é ONNX puro, a segmentação é o caminho principal.
|
||||
"""
|
||||
pred_ids_np, ts, roi_resized, roi_info, _aux = self.infer_ids(frame_rgb)
|
||||
|
||||
aux_result = self._last_aux_result
|
||||
|
||||
if aux_result is not None:
|
||||
aux_result = dict(aux_result)
|
||||
aux_result["stale"] = True
|
||||
aux_result["age_ms"] = (time.time() - self._last_aux_ts) * 1000.0
|
||||
|
||||
return pred_ids_np, ts, roi_resized, roi_info, aux_result
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Preprocess
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _prepare_input(self, roi_rgb: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
||||
roi_rgb = np.ascontiguousarray(roi_rgb)
|
||||
|
||||
# Mesmo quando o ONNX tem preprocess embutido, mantemos uma ROI redimensionada
|
||||
# para debug/visualização e compatibilidade com o retorno.
|
||||
roi_resized = cv2.resize(
|
||||
roi_rgb,
|
||||
self.resolucao,
|
||||
interpolation=cv2.INTER_AREA,
|
||||
)
|
||||
roi_resized = np.ascontiguousarray(roi_resized)
|
||||
|
||||
if self.onnx_has_preprocess:
|
||||
base = roi_rgb
|
||||
else:
|
||||
self.aux_head = None
|
||||
print("[SEG] Modo single carregado.")
|
||||
base = roi_resized
|
||||
|
||||
print(f"[MODEL] ckpt={pt_path}")
|
||||
print(f"[MODEL] epoch={ckpt.get('epoch')} bests={ckpt.get('bests')}")
|
||||
if self.input_layout == "nhwc_uint8":
|
||||
inp = base[None, ...]
|
||||
if inp.dtype != np.uint8:
|
||||
inp = inp.astype(np.uint8, copy=False)
|
||||
return np.ascontiguousarray(inp), roi_resized
|
||||
|
||||
if self.input_layout == "nhwc_float":
|
||||
inp = base.astype(np.float32, copy=False)
|
||||
|
||||
if self.input_scale != 1.0:
|
||||
inp = inp * self.input_scale
|
||||
|
||||
inp = inp[None, ...]
|
||||
return np.ascontiguousarray(inp), roi_resized
|
||||
|
||||
if self.input_layout == "nchw_float":
|
||||
inp = base.astype(np.float32, copy=False)
|
||||
|
||||
if self.input_scale != 1.0:
|
||||
inp = inp * self.input_scale
|
||||
|
||||
inp = np.transpose(inp, (2, 0, 1))[None, ...]
|
||||
return np.ascontiguousarray(inp), roi_resized
|
||||
|
||||
raise ValueError(
|
||||
f"onnx_input_layout inválido: {self.input_layout}. "
|
||||
"Use 'nhwc_uint8', 'nhwc_float' ou 'nchw_float'."
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Pós-processamento
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _decode_seg_output(self, out: np.ndarray) -> np.ndarray:
|
||||
arr = np.asarray(out)
|
||||
|
||||
fmt = self.seg_output_format
|
||||
|
||||
if fmt == "ids":
|
||||
return self._squeeze_ids(arr)
|
||||
|
||||
if fmt == "logits":
|
||||
return self._logits_to_ids(arr)
|
||||
|
||||
if fmt != "auto":
|
||||
raise ValueError(
|
||||
f"onnx_seg_output_format inválido: {fmt}. "
|
||||
"Use 'auto', 'ids' ou 'logits'."
|
||||
)
|
||||
|
||||
# AUTO:
|
||||
# [N,C,H,W] com C pequeno normalmente é logits.
|
||||
if arr.ndim == 4:
|
||||
if arr.shape[1] > 1:
|
||||
return self._logits_to_ids(arr)
|
||||
return self._squeeze_ids(arr)
|
||||
|
||||
# [C,H,W] com C pequeno também pode ser logits.
|
||||
if arr.ndim == 3:
|
||||
num_classes = len(getattr(self, "classes", []) or [])
|
||||
if num_classes > 1 and arr.shape[0] == num_classes:
|
||||
return self._logits_to_ids(arr)
|
||||
return self._squeeze_ids(arr)
|
||||
|
||||
if arr.ndim == 2:
|
||||
return self._squeeze_ids(arr)
|
||||
|
||||
raise RuntimeError(f"Formato de saída de segmentação não suportado: shape={arr.shape}")
|
||||
|
||||
def _squeeze_ids(self, arr: np.ndarray) -> np.ndarray:
|
||||
arr = np.asarray(arr)
|
||||
|
||||
# [1,1,H,W] -> [H,W]
|
||||
if arr.ndim == 4 and arr.shape[0] == 1 and arr.shape[1] == 1:
|
||||
return arr[0, 0]
|
||||
|
||||
# [1,H,W] -> [H,W]
|
||||
if arr.ndim == 3 and arr.shape[0] == 1:
|
||||
return arr[0]
|
||||
|
||||
# [H,W,1] -> [H,W]
|
||||
if arr.ndim == 3 and arr.shape[-1] == 1:
|
||||
return arr[:, :, 0]
|
||||
|
||||
# [H,W]
|
||||
if arr.ndim == 2:
|
||||
return arr
|
||||
|
||||
# fallback seguro
|
||||
return np.squeeze(arr)
|
||||
|
||||
def _logits_to_ids(self, arr: np.ndarray) -> np.ndarray:
|
||||
arr = np.asarray(arr)
|
||||
|
||||
# [N,C,H,W] -> [H,W]
|
||||
if arr.ndim == 4:
|
||||
return np.argmax(arr, axis=1)[0]
|
||||
|
||||
# [C,H,W] -> [H,W]
|
||||
if arr.ndim == 3:
|
||||
return np.argmax(arr, axis=0)
|
||||
|
||||
raise RuntimeError(f"Formato de logits não suportado: shape={arr.shape}")
|
||||
|
||||
def _decode_aux_output(self, out: np.ndarray) -> Optional[Dict[str, Any]]:
|
||||
arr = np.asarray(out)
|
||||
|
||||
if arr.size == 0:
|
||||
return None
|
||||
|
||||
values = np.squeeze(arr).astype(np.float32)
|
||||
|
||||
if values.ndim != 1:
|
||||
values = values.reshape(-1).astype(np.float32)
|
||||
|
||||
aux_format = str(
|
||||
self.config.get("onnx_aux_output_format", "")
|
||||
).lower()
|
||||
|
||||
if not aux_format:
|
||||
name = str(getattr(self, "aux_output_name", "") or "").lower()
|
||||
aux_format = "probs" if "prob" in name else "logits"
|
||||
|
||||
if aux_format == "probs":
|
||||
probs = values
|
||||
s = float(np.sum(probs))
|
||||
if s > 1e-6:
|
||||
probs = probs / s
|
||||
else:
|
||||
probs = self._softmax_np(values)
|
||||
|
||||
label_id = int(np.argmax(probs))
|
||||
label_conf = float(probs[label_id])
|
||||
label_name = self.label_names.get(label_id, f"label_{label_id}")
|
||||
|
||||
if self.use_compact_aux:
|
||||
aux_result = {
|
||||
"type": "label",
|
||||
"label_id": label_id,
|
||||
"label_name": label_name,
|
||||
"label_conf": label_conf,
|
||||
}
|
||||
else:
|
||||
aux_result = {
|
||||
"type": "label",
|
||||
"label_id": label_id,
|
||||
"label_name": label_name,
|
||||
"label_conf": label_conf,
|
||||
"label_probs": probs.astype(float).tolist(),
|
||||
"label_names": self.label_names,
|
||||
}
|
||||
|
||||
self._last_aux_result = aux_result
|
||||
self._last_aux_ts = time.time()
|
||||
|
||||
return aux_result
|
||||
|
||||
@staticmethod
|
||||
def _softmax_np(x: np.ndarray) -> np.ndarray:
|
||||
x = x.astype(np.float32, copy=False)
|
||||
x = x - np.max(x)
|
||||
exp = np.exp(x)
|
||||
denom = float(np.sum(exp))
|
||||
|
||||
if denom <= 1e-12:
|
||||
return np.zeros_like(exp, dtype=np.float32)
|
||||
|
||||
return exp / denom
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Utilitários
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _read_resolution(seg_config: Dict[str, Any]) -> Tuple[int, int]:
|
||||
res = seg_config.get("ia_resolution", [1024, 576])
|
||||
|
||||
if not isinstance(res, Sequence) or len(res) != 2:
|
||||
raise ValueError(f"ia_resolution inválida: {res}")
|
||||
|
||||
w = int(res[0])
|
||||
h = int(res[1])
|
||||
|
||||
if w <= 0 or h <= 0:
|
||||
raise ValueError(f"ia_resolution precisa ser positiva: {res}")
|
||||
|
||||
return w, h
|
||||
|
||||
@staticmethod
|
||||
def compute_roi_indices(H: int, zona_inicio: float, faixa_atuacao: float) -> Tuple[int, int]:
|
||||
zona_inicio = float(zona_inicio)
|
||||
faixa_atuacao = float(faixa_atuacao)
|
||||
|
||||
zona_inicio = max(0.0, min(1.0, zona_inicio))
|
||||
faixa_atuacao = max(0.0, min(1.0, faixa_atuacao))
|
||||
|
||||
def compute_roi_indices(self, H: int, zona_inicio: float, faixa_atuacao: float):
|
||||
y_inicio = int((1.0 - zona_inicio) * H)
|
||||
y_fim = int((1.0 - (zona_inicio + faixa_atuacao)) * H)
|
||||
|
||||
|
|
@ -190,226 +581,49 @@ class SegformerNavRunner:
|
|||
|
||||
return y_fim, y_inicio
|
||||
|
||||
def resize_keep_width(self, img: np.ndarray, new_w: int, min_h: int, interpolation: int) -> np.ndarray:
|
||||
h, w = img.shape[:2]
|
||||
new_h = int(round(new_w * (h / max(1, w))))
|
||||
def _load_label_names(self, seg_config: Dict[str, Any]) -> Dict[int, str]:
|
||||
"""
|
||||
Carrega nomes do label/status auxiliar, se existir.
|
||||
|
||||
if min_h is not None and new_h < min_h:
|
||||
new_h = min_h
|
||||
Aceita:
|
||||
- label_name_by_id direto no config
|
||||
- ia_label_names_path apontando para JSON
|
||||
- onnx_label_names_path apontando para JSON
|
||||
|
||||
return cv2.resize(img, (new_w, new_h), interpolation=interpolation)
|
||||
O JSON pode ser:
|
||||
- {"0": "Parado", "1": "CaminhandoRua"}
|
||||
- {"label_name_by_id": {"0": "Parado", "1": "CaminhandoRua"}}
|
||||
- ["Parado", "CaminhandoRua"]
|
||||
"""
|
||||
raw = seg_config.get("label_name_by_id")
|
||||
|
||||
def _preprocess_roi(self, roi_rgb: np.ndarray):
|
||||
roi_resized = self.resize_keep_width(
|
||||
roi_rgb,
|
||||
self.resolucao[0],
|
||||
self.resolucao[1],
|
||||
cv2.INTER_AREA,
|
||||
path = (
|
||||
seg_config.get("ia_label_names_path")
|
||||
or seg_config.get("onnx_label_names_path")
|
||||
)
|
||||
|
||||
# Garante array contínuo para reduzir cópia torta no torch.from_numpy
|
||||
roi_resized = np.ascontiguousarray(roi_resized)
|
||||
if raw is None and path and os.path.isfile(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
img_tensor = torch.from_numpy(roi_resized).to(
|
||||
device=self.device,
|
||||
dtype=torch.float32,
|
||||
non_blocking=True
|
||||
)
|
||||
|
||||
# HWC -> NCHW
|
||||
img_tensor = img_tensor.permute(2, 0, 1).unsqueeze(0)
|
||||
img_tensor = img_tensor.div_(255.0)
|
||||
|
||||
img_tensor = self.normalize_img(img_tensor)
|
||||
|
||||
if self.use_channels_last and self.device.type == "cuda":
|
||||
img_tensor = img_tensor.contiguous(memory_format=torch.channels_last)
|
||||
|
||||
return img_tensor, roi_resized
|
||||
|
||||
@torch.inference_mode()
|
||||
def infer_ids(self, frame_rgb):
|
||||
if frame_rgb is None or not hasattr(frame_rgb, "shape") or frame_rgb.size == 0:
|
||||
return None, None, None, None, None
|
||||
t0 = time.perf_counter()
|
||||
H, W = frame_rgb.shape[:2]
|
||||
y_fim, y_inicio = self.compute_roi_indices(H, self.roi_inicio, self.roi_tamanho)
|
||||
|
||||
roi_rgb = frame_rgb[y_fim:y_inicio, 0:W]
|
||||
t_crop = time.perf_counter()
|
||||
if roi_rgb is None or roi_rgb.size == 0:
|
||||
return None, None, None, (y_fim, y_inicio), None
|
||||
img_tensor, roi_resized = self._preprocess_roi(roi_rgb)
|
||||
t_pre = time.perf_counter()
|
||||
|
||||
use_amp_now = self.use_amp and self.device.type == "cuda"
|
||||
|
||||
with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=use_amp_now):
|
||||
out = self.model(pixel_values=img_tensor, output_hidden_states=True)
|
||||
|
||||
t_model = time.perf_counter()
|
||||
|
||||
logits_seg = out.logits
|
||||
|
||||
t_arg0 = time.perf_counter()
|
||||
pred_low = torch.argmax(logits_seg, dim=1)[0]
|
||||
pred_low_np = pred_low.detach().cpu().numpy().astype(np.uint8)
|
||||
pred_ids_np = cv2.resize(
|
||||
pred_low_np,
|
||||
(roi_resized.shape[1], roi_resized.shape[0]),
|
||||
interpolation=cv2.INTER_NEAREST
|
||||
)
|
||||
t_arg1 = time.perf_counter()
|
||||
|
||||
aux_result = None
|
||||
|
||||
t_aux0 = time.perf_counter()
|
||||
if self.mode == "dual_label" and self.aux_head is not None:
|
||||
feat = out.hidden_states[-1]
|
||||
|
||||
if feat.shape[-2:] != logits_seg.shape[-2:]:
|
||||
feat = F.interpolate(
|
||||
feat,
|
||||
size=logits_seg.shape[-2:],
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
|
||||
with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=use_amp_now):
|
||||
logits_label = self.aux_head(feat, logits_seg)
|
||||
|
||||
probs = torch.softmax(logits_label, dim=1)[0].detach().cpu().numpy()
|
||||
|
||||
label_id = int(np.argmax(probs))
|
||||
label_conf = float(probs[label_id])
|
||||
label_name = self.label_names.get(label_id, f"label_{label_id}")
|
||||
|
||||
if self.use_compact_aux:
|
||||
aux_result = {
|
||||
"type": "label",
|
||||
"label_id": label_id,
|
||||
"label_name": label_name,
|
||||
"label_conf": label_conf,
|
||||
}
|
||||
if isinstance(data, dict) and "label_name_by_id" in data:
|
||||
raw = data["label_name_by_id"]
|
||||
else:
|
||||
aux_result = {
|
||||
"type": "label",
|
||||
"label_id": label_id,
|
||||
"label_name": label_name,
|
||||
"label_conf": label_conf,
|
||||
"label_probs": probs.astype(float).tolist(),
|
||||
"label_names": self.label_names,
|
||||
}
|
||||
if aux_result is not None:
|
||||
self._last_aux_result = aux_result
|
||||
self._last_aux_ts = time.time()
|
||||
raw = data
|
||||
|
||||
t_aux1 = time.perf_counter()
|
||||
if raw is None:
|
||||
return {}
|
||||
|
||||
self.last_infer = time.time()
|
||||
|
||||
t_end = time.perf_counter()
|
||||
|
||||
if self.debug_timing:
|
||||
print(
|
||||
f"RUNNER | crop={(t_crop-t0)*1000:.1f}ms | "
|
||||
f"pre={(t_pre-t_crop)*1000:.1f}ms | "
|
||||
f"model={(t_model-t_pre)*1000:.1f}ms | "
|
||||
f"arg={(t_arg1-t_arg0)*1000:.1f}ms | "
|
||||
f"aux={(t_aux1-t_aux0)*1000:.1f}ms | "
|
||||
f"total={(t_end-t0)*1000:.1f}ms"
|
||||
)
|
||||
|
||||
return pred_ids_np, self.last_infer, roi_resized, (y_fim, y_inicio), aux_result
|
||||
|
||||
@torch.inference_mode()
|
||||
def infer_ids_seg_only(self, frame_rgb):
|
||||
"""
|
||||
Inferência rápida apenas da cabeça de segmentação.
|
||||
|
||||
Diferenças para infer_ids():
|
||||
- não pede hidden_states;
|
||||
- não roda aux_head;
|
||||
- reutiliza self._last_aux_result, se existir;
|
||||
- mantém o mesmo formato de retorno:
|
||||
pred_ids_np, ts, roi_resized, roi_info, aux_result
|
||||
"""
|
||||
if frame_rgb is None or not hasattr(frame_rgb, "shape") or frame_rgb.size == 0:
|
||||
return None, None, None, None, None
|
||||
|
||||
t0 = time.perf_counter()
|
||||
|
||||
H, W = frame_rgb.shape[:2]
|
||||
y_fim, y_inicio = self.compute_roi_indices(
|
||||
H,
|
||||
self.roi_inicio,
|
||||
self.roi_tamanho
|
||||
)
|
||||
|
||||
roi_rgb = frame_rgb[y_fim:y_inicio, 0:W]
|
||||
t_crop = time.perf_counter()
|
||||
|
||||
if roi_rgb is None or roi_rgb.size == 0:
|
||||
return None, None, None, (y_fim, y_inicio), None
|
||||
|
||||
img_tensor, roi_resized = self._preprocess_roi(roi_rgb)
|
||||
t_pre = time.perf_counter()
|
||||
|
||||
use_amp_now = getattr(self, "use_amp", True) and self.device.type == "cuda"
|
||||
|
||||
# Aqui está o ponto principal do teste:
|
||||
# NÃO pede hidden_states, então o modelo só precisa entregar logits de segmentação.
|
||||
with torch.autocast(
|
||||
device_type="cuda",
|
||||
dtype=torch.float16,
|
||||
enabled=use_amp_now
|
||||
):
|
||||
out = self.model(
|
||||
pixel_values=img_tensor,
|
||||
output_hidden_states=False
|
||||
)
|
||||
|
||||
t_model = time.perf_counter()
|
||||
|
||||
logits_seg = out.logits
|
||||
|
||||
t_arg0 = time.perf_counter()
|
||||
|
||||
pred_low = torch.argmax(logits_seg, dim=1)[0]
|
||||
pred_low_np = pred_low.detach().cpu().numpy().astype(np.uint8)
|
||||
|
||||
pred_ids_np = cv2.resize(
|
||||
pred_low_np,
|
||||
(roi_resized.shape[1], roi_resized.shape[0]),
|
||||
interpolation=cv2.INTER_NEAREST
|
||||
)
|
||||
|
||||
t_arg1 = time.perf_counter()
|
||||
|
||||
# Reaproveita o último status conhecido.
|
||||
# Para o teste inicial, pode ser None mesmo.
|
||||
aux_result = getattr(self, "_last_aux_result", None)
|
||||
|
||||
if aux_result is not None:
|
||||
aux_result = dict(aux_result)
|
||||
aux_result["stale"] = True
|
||||
aux_result["age_ms"] = (
|
||||
time.time() - getattr(self, "_last_aux_ts", time.time())
|
||||
) * 1000.0
|
||||
|
||||
self.last_infer = time.time()
|
||||
t_end = time.perf_counter()
|
||||
|
||||
if getattr(self, "debug_timing", False):
|
||||
print(
|
||||
f"RUNNER_SEG_ONLY | "
|
||||
f"crop={(t_crop - t0) * 1000:.1f}ms | "
|
||||
f"pre={(t_pre - t_crop) * 1000:.1f}ms | "
|
||||
f"model={(t_model - t_pre) * 1000:.1f}ms | "
|
||||
f"arg={(t_arg1 - t_arg0) * 1000:.1f}ms | "
|
||||
f"total={(t_end - t0) * 1000:.1f}ms"
|
||||
)
|
||||
|
||||
return pred_ids_np, self.last_infer, roi_resized, (y_fim, y_inicio), aux_result
|
||||
if isinstance(raw, list):
|
||||
return {i: str(name) for i, name in enumerate(raw)}
|
||||
|
||||
if isinstance(raw, dict):
|
||||
result = {}
|
||||
for k, v in raw.items():
|
||||
try:
|
||||
result[int(k)] = str(v)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ class GpuPriorityController:
|
|||
good_cycles_to_recover: int = 5,
|
||||
min_data_age_s: float = 0.0,
|
||||
max_data_age_s: float = 3.0,
|
||||
ignore_inactive_weed: bool = True,
|
||||
weed_inactive_age_s: float = 5.0,
|
||||
log_periodic: bool = False,
|
||||
log_warnings: bool = True,
|
||||
):
|
||||
|
|
@ -118,6 +120,9 @@ class GpuPriorityController:
|
|||
"data_ok": False,
|
||||
}
|
||||
|
||||
self.ignore_inactive_weed = bool(ignore_inactive_weed)
|
||||
self.weed_inactive_age_s = float(weed_inactive_age_s)
|
||||
|
||||
self.log_periodic = bool(log_periodic)
|
||||
self.log_warnings = bool(log_warnings)
|
||||
self._last_warning_reason = None
|
||||
|
|
@ -264,6 +269,18 @@ class GpuPriorityController:
|
|||
now = time.time()
|
||||
|
||||
if not isinstance(ctx, dict) or not ctx:
|
||||
if self.ignore_inactive_weed:
|
||||
return {
|
||||
"health": 1.0,
|
||||
"reason": "weed_inativo_sem_ctx",
|
||||
"fps": {},
|
||||
"ratios": {},
|
||||
"gpu_ms": 0.0,
|
||||
"data_age_s": None,
|
||||
"data_ok": False,
|
||||
"weed_active": False,
|
||||
}
|
||||
|
||||
return {
|
||||
"health": 0.0,
|
||||
"reason": "sem_ctx_weed",
|
||||
|
|
@ -272,6 +289,7 @@ class GpuPriorityController:
|
|||
"gpu_ms": 0.0,
|
||||
"data_age_s": None,
|
||||
"data_ok": False,
|
||||
"weed_active": False,
|
||||
}
|
||||
|
||||
perf = self._extract_perf_root(ctx)
|
||||
|
|
@ -282,6 +300,36 @@ class GpuPriorityController:
|
|||
if ts:
|
||||
data_age_s = max(0.0, now - float(ts))
|
||||
|
||||
# Se tem timestamp velho demais, considera weed parado/inativo.
|
||||
if (
|
||||
self.ignore_inactive_weed
|
||||
and data_age_s is not None
|
||||
and data_age_s > self.weed_inactive_age_s
|
||||
):
|
||||
return {
|
||||
"health": 1.0,
|
||||
"reason": "weed_inativo_timestamp_velho",
|
||||
"fps": {},
|
||||
"ratios": {},
|
||||
"gpu_ms": 0.0,
|
||||
"data_age_s": data_age_s,
|
||||
"data_ok": False,
|
||||
"weed_active": False,
|
||||
}
|
||||
|
||||
# Se não tem timestamp, mas tem contexto velho/estranho, também não deve derrubar visual.
|
||||
if self.ignore_inactive_weed and ts is None:
|
||||
return {
|
||||
"health": 1.0,
|
||||
"reason": "weed_inativo_sem_timestamp",
|
||||
"fps": {},
|
||||
"ratios": {},
|
||||
"gpu_ms": 0.0,
|
||||
"data_age_s": None,
|
||||
"data_ok": False,
|
||||
"weed_active": False,
|
||||
}
|
||||
|
||||
data_ok = True
|
||||
if data_age_s is not None:
|
||||
data_ok = self.min_data_age_s <= data_age_s <= self.max_data_age_s
|
||||
|
|
@ -304,7 +352,6 @@ class GpuPriorityController:
|
|||
target = max(float(target), 0.01)
|
||||
ratios[name] = max(0.0, min(1.5, float(real) / target))
|
||||
|
||||
# O gargalo manda. Se um deles despencou, saúde despenca.
|
||||
valid_ratios = [v for v in ratios.values() if v is not None]
|
||||
health = min(valid_ratios) if valid_ratios else 0.0
|
||||
|
||||
|
|
@ -318,8 +365,6 @@ class GpuPriorityController:
|
|||
health = 0.0
|
||||
reason = "weed_sem_inferencia"
|
||||
|
||||
# Latência GPU como alarme extra.
|
||||
# Não derruba direto para zero, mas limita a saúde.
|
||||
if gpu_ms >= 120:
|
||||
health = min(health, 0.35)
|
||||
reason = "gpu_ms_muito_alto"
|
||||
|
|
@ -338,6 +383,7 @@ class GpuPriorityController:
|
|||
"gpu_ms": float(gpu_ms or 0.0),
|
||||
"data_age_s": data_age_s,
|
||||
"data_ok": bool(data_ok),
|
||||
"weed_active": True,
|
||||
}
|
||||
|
||||
def _extract_perf_root(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
|
@ -507,6 +553,11 @@ class GpuPriorityController:
|
|||
|
||||
def _log_warning_if_needed(self):
|
||||
h = self._last_health
|
||||
|
||||
if h.get("weed_active") is False:
|
||||
self._last_warning_reason = None
|
||||
return
|
||||
|
||||
reason = h.get("reason", "ok")
|
||||
health = float(h.get("health", 1.0) or 0.0)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,142 +1,665 @@
|
|||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from visual_worker.camera_manager import CameraManager
|
||||
from shared.contexto_global_redis import ContextoGlobalRedis
|
||||
|
||||
|
||||
module_id = "visual"
|
||||
topico_tx = f"operador/{module_id}/tx"
|
||||
topico_rx = f"operador/{module_id}/rx"
|
||||
debug = True
|
||||
ultima_atualizacao = time.time()
|
||||
|
||||
|
||||
def mostrar_log(mensagem):
|
||||
if debug:
|
||||
print(f"{time.time()} - [{module_id}] {mensagem}")
|
||||
|
||||
|
||||
manager = CameraManager(mostrar_log)
|
||||
|
||||
|
||||
def get_camera_manager():
|
||||
return manager
|
||||
|
||||
|
||||
def iniciar_camera_manager(mx_id):
|
||||
global manager, ultima_atualizacao
|
||||
|
||||
if mx_id is None:
|
||||
return
|
||||
|
||||
agora = time.time()
|
||||
if (not manager.iniciando and (agora - ultima_atualizacao) > 5 and (manager.camera is None or manager.mx_id != mx_id)):
|
||||
|
||||
if (
|
||||
not manager.iniciando
|
||||
and (agora - ultima_atualizacao) > 5
|
||||
and (manager.camera is None or manager.mx_id != mx_id)
|
||||
):
|
||||
ultima_atualizacao = agora
|
||||
mostrar_log("Iniciando Camera Manager...")
|
||||
manager.inicializar(mx_id=mx_id)
|
||||
|
||||
if manager.camera is not None and manager.operante:
|
||||
mostrar_log(f"✅ Camera manager iniciado, com MX_ID: {mx_id}")
|
||||
|
||||
_CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
|
||||
|
||||
_CONFIG_CACHE = None
|
||||
_CONFIG_MTIME = None
|
||||
_CONFIG_LOCK = threading.Lock()
|
||||
|
||||
def load_seg_config(force_reload=False):
|
||||
global _CONFIG_CACHE, _CONFIG_MTIME
|
||||
with _CONFIG_LOCK:
|
||||
_CONFIG_CACHE = {
|
||||
"debug_visual": False,
|
||||
"debug_perf": False,
|
||||
"ia_roi_begin": 0.0,
|
||||
"ia_roi_size": 1.0,
|
||||
|
||||
"analise_fps": 8.0,
|
||||
"grid_fps": 5.0,
|
||||
"inferencia_fps": 8.0,
|
||||
"deteccao_fps": 5.0,
|
||||
"publicacao_fps": 15.0,
|
||||
"gpu_priority": {
|
||||
"enabled": True,
|
||||
"update_interval_s": 1.0,
|
||||
"log_interval_s": 3.0,
|
||||
"log_periodic": False,
|
||||
"log_warnings": True,
|
||||
# ============================================================
|
||||
# CONFIG V1 - VISUAL WORKER
|
||||
# ============================================================
|
||||
|
||||
"bad_cycles_to_degrade": 3,
|
||||
"good_cycles_to_recover": 5,
|
||||
"max_data_age_s": 3.0,
|
||||
|
||||
"mode_configs": {
|
||||
"normal": {
|
||||
"segmentacao": 8.0,
|
||||
"grid": 6.0,
|
||||
"deteccao": 5.0,
|
||||
"publicacao": 15.0,
|
||||
"analise": 8.0
|
||||
},
|
||||
"eco": {
|
||||
"segmentacao": 4.0,
|
||||
"grid": 4.0,
|
||||
"deteccao": 5.0,
|
||||
"publicacao": 10.0,
|
||||
"analise": 6.0
|
||||
},
|
||||
"safe": {
|
||||
"segmentacao": 2.0,
|
||||
"grid": 2.0,
|
||||
"deteccao": 3.0,
|
||||
"publicacao": 5.0,
|
||||
"analise": 4.0
|
||||
},
|
||||
"critical": {
|
||||
"segmentacao": 0.5,
|
||||
"grid": 1.0,
|
||||
"deteccao": 2.0,
|
||||
"publicacao": 5.0,
|
||||
"analise": 2.0
|
||||
}
|
||||
}
|
||||
},
|
||||
def construir_mode_configs_adaptativos(cfg: dict) -> dict:
|
||||
gpu = cfg.get("gpu_priority", {}) or {}
|
||||
|
||||
"ia_resolution": [1024,576],
|
||||
"seg_every_n": 1,
|
||||
"det_every_n": 1,
|
||||
"use_amp": True,
|
||||
"use_channels_last": True,
|
||||
"use_compact_aux": True,
|
||||
"debug_timing": False,
|
||||
"runtime_fast": True,
|
||||
"gerar_mask_color": False,
|
||||
"gerar_debug_status": False,
|
||||
"usar_connected_components": True,
|
||||
"usar_corridor_grid": True
|
||||
base = {
|
||||
"segmentacao": float(cfg.get("inferencia_fps", 8.0)),
|
||||
"grid": float(cfg.get("grid_fps", 6.0)),
|
||||
"deteccao": float(cfg.get("deteccao_fps", 5.0)),
|
||||
"publicacao": float(cfg.get("publicacao_fps", 15.0)),
|
||||
"analise": float(cfg.get("analise_fps", 8.0)),
|
||||
}
|
||||
|
||||
if not bool(gpu.get("adaptive_mode_configs", True)):
|
||||
return gpu.get("mode_configs", {
|
||||
"normal": base,
|
||||
"eco": base,
|
||||
"safe": base,
|
||||
"critical": gpu.get("critical_mode", base),
|
||||
})
|
||||
|
||||
scales = gpu.get("mode_scales", {}) or {}
|
||||
floors = gpu.get("mode_floors", {}) or {}
|
||||
|
||||
def scaled_mode(mode_name: str, scale: float) -> dict:
|
||||
floor = floors.get(mode_name, {}) or {}
|
||||
|
||||
return {
|
||||
nome: max(
|
||||
float(floor.get(nome, 0.0)),
|
||||
round(float(valor) * float(scale), 3),
|
||||
)
|
||||
for nome, valor in base.items()
|
||||
}
|
||||
_equipamento = ContextoGlobalRedis.get_equipamento()
|
||||
_CONFIG_CACHE["ia_mode"] = _equipamento.get("ia_mode_ruas")
|
||||
_CONFIG_CACHE["ia_backbone"] = _equipamento.get("ia_backbone_ruas_seg")
|
||||
_CONFIG_CACHE["ia_model_path"] = _equipamento.get("path_ia_model_ruas_seg")
|
||||
_CONFIG_CACHE["ia_labelmap_path"] = _equipamento.get("path_ia_labelmap_ruas_seg")
|
||||
_CONFIG_CACHE["ia_norm_stats_path"] = _equipamento.get("path_ia_norm_stats_ruas_seg")
|
||||
|
||||
return _CONFIG_CACHE
|
||||
|
||||
def reload_seg_config():
|
||||
return load_seg_config(force_reload=True)
|
||||
critical = gpu.get("critical_mode", {}) or {
|
||||
"segmentacao": 0.5,
|
||||
"grid": 1.0,
|
||||
"deteccao": 2.0,
|
||||
"publicacao": 5.0,
|
||||
"analise": 2.0,
|
||||
}
|
||||
|
||||
return {
|
||||
"normal": dict(base),
|
||||
"eco": scaled_mode("eco", float(scales.get("eco", 0.65))),
|
||||
"safe": scaled_mode("safe", float(scales.get("safe", 0.35))),
|
||||
"critical": {
|
||||
"segmentacao": float(critical.get("segmentacao", 0.5)),
|
||||
"grid": float(critical.get("grid", 1.0)),
|
||||
"deteccao": float(critical.get("deteccao", 2.0)),
|
||||
"publicacao": float(critical.get("publicacao", 5.0)),
|
||||
"analise": float(critical.get("analise", 2.0)),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
VISUAL_DEFAULT_CONFIG = {
|
||||
# ============================================================
|
||||
# 1) Debug e telemetria
|
||||
# ============================================================
|
||||
# Mostra janelas OpenCV/debug visual. Não usar em runtime de campo.
|
||||
"debug_visual": False,
|
||||
|
||||
# Publica logs de performance no console.
|
||||
"debug_perf": True,
|
||||
|
||||
# Tamanho dos frames de preview/debug enviados ao C#.
|
||||
"preview_size": (1280, 720),
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2) Frequências dos loops
|
||||
# ============================================================
|
||||
# Loop supervisor: saúde, performance e GPU controller.
|
||||
"analise_fps": 15.0,
|
||||
|
||||
# Loop de segmentação ONNX/TensorRT.
|
||||
"inferencia_fps": 15.0,
|
||||
|
||||
# Loop de detecção onboard MobileNet-SSD na OAK.
|
||||
"deteccao_fps": 5.0,
|
||||
|
||||
# Loop de construção da matriz de confiança/custo.
|
||||
"grid_fps": 15.0,
|
||||
|
||||
# Loop de publicação Redis.
|
||||
"publicacao_fps": 5.0,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3) Modelo ONNX/TensorRT - segmentação de ruas/corredor
|
||||
# ============================================================
|
||||
# Runtime oficial da v1.
|
||||
"runtime_backend": "onnx",
|
||||
"onnx_provider": "tensorrt",
|
||||
|
||||
# Resolução esperada pelo ONNX: [W, H].
|
||||
"ia_resolution": [1024, 576],
|
||||
|
||||
# ROI vertical da segmentação.
|
||||
# 0.0 + 1.0 = frame inteiro.
|
||||
"ia_roi_begin": 0.0,
|
||||
"ia_roi_size": 1.0,
|
||||
|
||||
# Contrato validado do modelo modelseg-2_0.onnx:
|
||||
# entrada: float32 [1, 3, 576, 1024], RGB 0..1, NCHW
|
||||
# saída 1: semantic_logits
|
||||
# saída 2: label_probs
|
||||
"onnx_has_preprocess": False,
|
||||
"onnx_input_layout": "nchw_float",
|
||||
"onnx_input_scale": 1.0 / 255.0,
|
||||
"onnx_seg_output_format": "logits",
|
||||
|
||||
# Nomes das entradas/saídas.
|
||||
# input_name None deixa o runner detectar automaticamente.
|
||||
"onnx_input_name": None,
|
||||
"onnx_seg_output_name": "semantic_logits",
|
||||
"onnx_aux_output_name": "label_probs",
|
||||
"onnx_aux_output_format": "probs",
|
||||
|
||||
# Auxiliar do status de corredor.
|
||||
# True = retorna label_id, label_name, label_conf.
|
||||
# False = inclui também vetor de probabilidades.
|
||||
"use_compact_aux": True,
|
||||
|
||||
# Logs internos do runner ONNX.
|
||||
"debug_timing": False,
|
||||
"debug_session": True,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 4) TensorRT
|
||||
# ============================================================
|
||||
"trt_fp16_enable": True,
|
||||
"trt_engine_cache_enable": True,
|
||||
"trt_engine_cache_path": "./trt_cache_visual_worker",
|
||||
|
||||
# Deixe None salvo se não quiser fixar workspace.
|
||||
"trt_max_workspace_size": None,
|
||||
|
||||
# Threads do ONNX Runtime.
|
||||
# Para TensorRT/CUDA, 1 costuma ser suficiente e evita ruído.
|
||||
"onnx_intra_op_num_threads": 1,
|
||||
"onnx_inter_op_num_threads": 1,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 5) SegmentacaoManager v1
|
||||
# ============================================================
|
||||
# Este bloco controla apenas a análise da máscara:
|
||||
# pred_ids + label_probs -> dados_visuais.
|
||||
"segmentacao": {
|
||||
# Inclui debug textual no payload de segmentação.
|
||||
# Não gera imagem.
|
||||
"include_debug": False,
|
||||
|
||||
# Inclui timing interno do SegmentacaoManager.
|
||||
"debug_timing": False,
|
||||
|
||||
# ID das classes no modelo de segmentação.
|
||||
"id_nao_navegavel": 0,
|
||||
"id_navegavel": 1,
|
||||
|
||||
# Frações verticais usadas para estimar centro/ângulo do corredor.
|
||||
# 0.0 = topo, 1.0 = base.
|
||||
"scanline_fracs": (0.96, 0.86, 0.74, 0.62, 0.50, 0.38, 0.26),
|
||||
|
||||
# A região próxima ao robô está na base da imagem.
|
||||
"near_is_bottom": True,
|
||||
|
||||
# Suavização das saídas usadas pelo controle.
|
||||
"ema_alpha_ang": 0.25,
|
||||
"ema_alpha_lat": 0.25,
|
||||
"ema_alpha_conf": 0.20,
|
||||
|
||||
# Histerese temporal do status do corredor.
|
||||
"status_window_s": 1.5,
|
||||
"status_expected_fps": 10.0,
|
||||
|
||||
# Confiança da cabeça auxiliar ONNX.
|
||||
# >= accept: modelo manda.
|
||||
# >= soft: modelo ajuda quando heurística está indefinida.
|
||||
"model_conf_accept": 0.70,
|
||||
"model_conf_soft": 0.45,
|
||||
|
||||
# Grid leve interna para centro/fallback do corredor.
|
||||
"corridor_grid_rows": 6,
|
||||
"corridor_grid_cols": 21,
|
||||
"corridor_jump_penalty": 0.60,
|
||||
|
||||
# Runs navegáveis nas scanlines.
|
||||
"min_run_width_frac": 0.035,
|
||||
"prefer_center_weight": 1.10,
|
||||
"prefer_prev_weight": 0.65,
|
||||
"prefer_width_weight": 1.00,
|
||||
|
||||
# Heurística de status quando o modelo auxiliar não está confiante.
|
||||
"thr_parado_global": 0.30,
|
||||
"thr_direcionando_global": 0.82,
|
||||
"thr_caminhando_score": 0.48,
|
||||
"thr_entrando_near": 0.78,
|
||||
"thr_saindo_far": 0.70,
|
||||
},
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 6) Grid de confiança/custo
|
||||
# ============================================================
|
||||
"grid": {
|
||||
# Formato global da grid: (cols, rows).
|
||||
"grid_shape": (15, 10),
|
||||
|
||||
# Geometria da câmera para gerar grid_ref por linha.
|
||||
"geometry": {
|
||||
"grid_shape": (15, 10),
|
||||
|
||||
# Inclinação fixa da câmera em relação ao solo.
|
||||
"camera_pitch_deg": 28.91,
|
||||
|
||||
# Altura física da câmera.
|
||||
"camera_height_m": 0.74,
|
||||
|
||||
# FOV vertical usado na aproximação geométrica.
|
||||
"fov_v_deg": 43.28,
|
||||
|
||||
# Correção dinâmica usando IMU.
|
||||
"pitch_gain": 1.0,
|
||||
"pitch_limit_deg": 10.0,
|
||||
|
||||
# Saturação de distâncias plausíveis.
|
||||
"min_dist_m": 0.2,
|
||||
"max_dist_m": 20.0,
|
||||
},
|
||||
|
||||
# Construção da matriz de confiança/custo por célula.
|
||||
"confidence": {
|
||||
# Faixa válida do depth em mm.
|
||||
"valid_mm": (300, 10000),
|
||||
|
||||
# Fração mínima de pixels depth válidos na célula.
|
||||
"min_valid_frac": 0.30,
|
||||
|
||||
# Mapeamento de fração válida de depth para confiança.
|
||||
"conf_params": (0.30, 0.80),
|
||||
|
||||
# Pesos do custo:
|
||||
# nao_navegavel, anomalia, incerteza.
|
||||
"weights": (0.65, 0.25, 0.10),
|
||||
|
||||
# Usa depth para anomalia física.
|
||||
"usar_depth": True,
|
||||
|
||||
# Obstáculo/saliência: algo mais perto que o esperado.
|
||||
"anom_tau_up": 0.15,
|
||||
"anom_satur_up_m": 0.35,
|
||||
|
||||
# Buraco/queda: algo mais longe que o esperado.
|
||||
"anom_tau_down": 0.30,
|
||||
"anom_satur_down_m": 0.55,
|
||||
"anom_down_weight": 0.65,
|
||||
|
||||
# Regras para célula navegável.
|
||||
"min_pct_navegavel": 0.55,
|
||||
"max_anom_navegavel": 0.45,
|
||||
"min_conf_navegavel": 0.35,
|
||||
|
||||
# Integração de detecções onboard na grid.
|
||||
"det": {
|
||||
# Peso da detecção no custo suave.
|
||||
"w4": 0.18,
|
||||
|
||||
# Acima disso, detecção crítica pode bloquear navegabilidade.
|
||||
"thr_det_soft": 0.45,
|
||||
|
||||
# Cobertura mínima da bbox sobre uma célula.
|
||||
"min_cell_coverage": 0.10,
|
||||
|
||||
# Confiança mínima da detecção.
|
||||
"min_det_conf": 0.45,
|
||||
|
||||
# Pesos por classe.
|
||||
"class_weights": {
|
||||
"person": 1.0,
|
||||
"dog": 0.7,
|
||||
"cat": 0.5,
|
||||
},
|
||||
|
||||
# Classes críticas para segurança.
|
||||
"veto_labels": {"person"},
|
||||
|
||||
# Como combinar múltiplas detecções na mesma célula.
|
||||
# "max" ou "sum_clamped".
|
||||
"combine": "max",
|
||||
|
||||
# v1: detecção não derruba confiança, só custo/navegabilidade.
|
||||
"conf_drop_alpha": 0.0,
|
||||
|
||||
# Se True, só classes veto bloqueiam navegabilidade.
|
||||
"only_veto_blocks_nav": True,
|
||||
|
||||
# Classes não críticas pesam menos no custo.
|
||||
"non_veto_cost_scale": 0.50,
|
||||
},
|
||||
},
|
||||
|
||||
# Fusão temporal e decisão de bloqueio.
|
||||
"fuser": {
|
||||
"K": 3,
|
||||
"M": 2,
|
||||
|
||||
"central_cols": None,
|
||||
"y_range_m": (0.5, 5.0),
|
||||
"near_is_bottom": True,
|
||||
|
||||
"margin_m": 0.12,
|
||||
|
||||
"fuse_cost": "ema",
|
||||
"fuse_anom": "ema",
|
||||
"fuse_conf": "mean",
|
||||
"fuse_det": "ema",
|
||||
"ema_alpha": 0.60,
|
||||
|
||||
"thr_anom_block": 0.50,
|
||||
"thr_cost_block": 0.70,
|
||||
"thr_conf_low": 0.35,
|
||||
"thr_nav_low": 0.45,
|
||||
|
||||
"rho_block_central": 0.70,
|
||||
"rho_conf_blackout_global": 0.40,
|
||||
|
||||
"thr_det_consider": 0.25,
|
||||
"thr_det_block": 0.45,
|
||||
|
||||
# MobileNet-SSD: person normalmente é ID 15 no labelmap usado.
|
||||
"det_veto_label_ids": {15},
|
||||
|
||||
"use_persistence": True,
|
||||
"a_max_freio": 0.20,
|
||||
"margem_parada_m": 0.60,
|
||||
"stop_on_frames": 2,
|
||||
"stop_off_frames": 3,
|
||||
|
||||
"use_trend": True,
|
||||
"trend_alpha": 0.35,
|
||||
"trend_watch": 0.38,
|
||||
"trend_slowdown": 0.58,
|
||||
"trend_prepare_stop": 0.78,
|
||||
|
||||
"include_debug": True,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 7) GPU Priority Controller
|
||||
# ============================================================
|
||||
# Controla FPS do Visual Worker conforme saúde do Weed Worker.
|
||||
"gpu_priority": {
|
||||
"enabled": True,
|
||||
|
||||
"update_interval_s": 1.0,
|
||||
"log_interval_s": 3.0,
|
||||
"log_periodic": False,
|
||||
"log_warnings": True,
|
||||
|
||||
# Se o weed estiver parado/inativo, não rebaixa o visual.
|
||||
"ignore_inactive_weed": True,
|
||||
"weed_inactive_age_s": 5.0,
|
||||
|
||||
# Histerese dos modos.
|
||||
"bad_cycles_to_degrade": 3,
|
||||
"good_cycles_to_recover": 5,
|
||||
"max_data_age_s": 3.0,
|
||||
|
||||
# FPS por modo do Visual Worker.
|
||||
# Se adaptive_mode_configs=True, os modos normal/eco/safe
|
||||
# são calculados a partir dos FPS principais definidos no bloco 2.
|
||||
"adaptive_mode_configs": True,
|
||||
|
||||
# Multiplicadores dos modos intermediários.
|
||||
# normal sempre usa os FPS principais.
|
||||
"mode_scales": {
|
||||
"eco": 0.65,
|
||||
"safe": 0.35,
|
||||
},
|
||||
|
||||
# Pisos mínimos dos modos intermediários.
|
||||
"mode_floors": {
|
||||
"eco": {
|
||||
"segmentacao": 4.0,
|
||||
"grid": 4.0,
|
||||
"deteccao": 3.0,
|
||||
"publicacao": 5.0,
|
||||
"analise": 6.0,
|
||||
},
|
||||
"safe": {
|
||||
"segmentacao": 2.0,
|
||||
"grid": 2.0,
|
||||
"deteccao": 2.0,
|
||||
"publicacao": 5.0,
|
||||
"analise": 4.0,
|
||||
},
|
||||
},
|
||||
|
||||
# Modo crítico fica fixo, como piso de sobrevivência.
|
||||
"critical_mode": {
|
||||
"segmentacao": 0.5,
|
||||
"grid": 1.0,
|
||||
"deteccao": 2.0,
|
||||
"publicacao": 5.0,
|
||||
"analise": 2.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
DET_DEFAULT_CONFIG = {
|
||||
# ============================================================
|
||||
# 1) Debug
|
||||
# ============================================================
|
||||
"debug_visual": False,
|
||||
|
||||
# Tracker onboard/offboard. Desligado no baseline.
|
||||
"com_track": False,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2) Detector OAK MobileNet-SSD
|
||||
# ============================================================
|
||||
"ia_roi_begin": 0.0,
|
||||
"ia_roi_size": 1.0,
|
||||
|
||||
# Resolução do modelo detector.
|
||||
"ia_resolution": [300, 300],
|
||||
|
||||
# Confiança mínima do detector.
|
||||
"ia_conf": 0.5,
|
||||
|
||||
# Classes do MobileNet-SSD.
|
||||
"classes": [
|
||||
"background",
|
||||
"aeroplane", "bicycle", "bird", "boat", "bottle",
|
||||
"bus", "car", "cat", "chair", "cow",
|
||||
"diningtable", "dog", "horse", "motorbike", "person",
|
||||
"pottedplant", "sheep", "sofa", "train", "tvmonitor",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Overrides Redis / runtime
|
||||
# ============================================================
|
||||
|
||||
def aplicar_overrides_redis_seg(cfg: dict) -> dict:
|
||||
equipamento = ContextoGlobalRedis.get_equipamento()
|
||||
|
||||
# Modelo ONNX oficial de ruas/corredor.
|
||||
cfg["ia_onnx_path"] = equipamento.get("path_ia_model_ruas_seg")
|
||||
|
||||
# Labelmap da segmentação.
|
||||
cfg["ia_labelmap_path"] = equipamento.get("path_ia_labelmap_ruas_seg")
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def normalizar_config_runtime_seg(cfg: dict) -> dict:
|
||||
# Alias único interno, para logs e validações.
|
||||
cfg["onnx_model_path"] = cfg.get("ia_onnx_path")
|
||||
|
||||
if not cfg.get("ia_onnx_path"):
|
||||
mostrar_log("[WARN] path do modelo ONNX de ruas não definido no Redis/equipamento.")
|
||||
|
||||
if not cfg.get("ia_labelmap_path"):
|
||||
mostrar_log("[WARN] path do labelmap de ruas não definido no Redis/equipamento.")
|
||||
|
||||
# Garante tuplas onde as dataclasses esperam tuplas.
|
||||
cfg["preview_size"] = tuple(cfg.get("preview_size", (1280, 720)))
|
||||
|
||||
grid = cfg.get("grid", {}) or {}
|
||||
|
||||
if "grid_shape" in grid:
|
||||
grid["grid_shape"] = tuple(grid["grid_shape"])
|
||||
|
||||
geometry = grid.get("geometry", {}) or {}
|
||||
if "grid_shape" in geometry:
|
||||
geometry["grid_shape"] = tuple(geometry["grid_shape"])
|
||||
|
||||
confidence = grid.get("confidence", {}) or {}
|
||||
|
||||
if "valid_mm" in confidence:
|
||||
confidence["valid_mm"] = tuple(confidence["valid_mm"])
|
||||
|
||||
if "conf_params" in confidence:
|
||||
confidence["conf_params"] = tuple(confidence["conf_params"])
|
||||
|
||||
if "weights" in confidence:
|
||||
confidence["weights"] = tuple(confidence["weights"])
|
||||
|
||||
det = confidence.get("det", {}) or {}
|
||||
if "veto_labels" in det and not isinstance(det["veto_labels"], set):
|
||||
det["veto_labels"] = set(det["veto_labels"])
|
||||
|
||||
fuser = grid.get("fuser", {}) or {}
|
||||
if "y_range_m" in fuser:
|
||||
fuser["y_range_m"] = tuple(fuser["y_range_m"])
|
||||
|
||||
cfg["grid"] = grid
|
||||
|
||||
segmentacao = cfg.get("segmentacao", {}) or {}
|
||||
if "scanline_fracs" in segmentacao:
|
||||
segmentacao["scanline_fracs"] = tuple(segmentacao["scanline_fracs"])
|
||||
|
||||
cfg["segmentacao"] = segmentacao
|
||||
|
||||
gpu = cfg.get("gpu_priority", {}) or {}
|
||||
gpu["mode_configs"] = construir_mode_configs_adaptativos(cfg)
|
||||
cfg["gpu_priority"] = gpu
|
||||
|
||||
fuser = grid.get("fuser", {}) or {}
|
||||
if "y_range_m" in fuser:
|
||||
fuser["y_range_m"] = tuple(fuser["y_range_m"])
|
||||
|
||||
if "central_cols" in fuser and fuser["central_cols"] is not None:
|
||||
fuser["central_cols"] = tuple(fuser["central_cols"])
|
||||
|
||||
if "det_veto_label_ids" in fuser and not isinstance(fuser["det_veto_label_ids"], set):
|
||||
fuser["det_veto_label_ids"] = set(fuser["det_veto_label_ids"])
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def aplicar_overrides_redis_det(cfg: dict) -> dict:
|
||||
equipamento = ContextoGlobalRedis.get_equipamento()
|
||||
|
||||
cfg["ia_model_path"] = equipamento.get(
|
||||
"path_ia_model_ruas_det",
|
||||
"C:/AgroBaseModels/Ruas/modeldet-1_0.blob",
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def normalizar_config_runtime_det(cfg: dict) -> dict:
|
||||
if not cfg.get("ia_model_path"):
|
||||
mostrar_log("[WARN] path do modelo detector de ruas não definido.")
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Loaders públicos
|
||||
# ============================================================
|
||||
|
||||
def load_seg_config():
|
||||
global _CONFIG_CACHE
|
||||
|
||||
with _CONFIG_LOCK:
|
||||
cfg = dict(VISUAL_DEFAULT_CONFIG)
|
||||
|
||||
# Cópia profunda simples dos blocos aninhados.
|
||||
# Evita compartilhar dict interno entre chamadas.
|
||||
cfg["segmentacao"] = dict(VISUAL_DEFAULT_CONFIG["segmentacao"])
|
||||
cfg["grid"] = {
|
||||
"grid_shape": VISUAL_DEFAULT_CONFIG["grid"]["grid_shape"],
|
||||
"geometry": dict(VISUAL_DEFAULT_CONFIG["grid"]["geometry"]),
|
||||
"confidence": dict(VISUAL_DEFAULT_CONFIG["grid"]["confidence"]),
|
||||
"fuser": dict(VISUAL_DEFAULT_CONFIG["grid"]["fuser"]),
|
||||
}
|
||||
cfg["grid"]["confidence"]["det"] = dict(
|
||||
VISUAL_DEFAULT_CONFIG["grid"]["confidence"]["det"]
|
||||
)
|
||||
cfg["grid"]["confidence"]["det"]["class_weights"] = dict(
|
||||
VISUAL_DEFAULT_CONFIG["grid"]["confidence"]["det"]["class_weights"]
|
||||
)
|
||||
cfg["grid"]["confidence"]["det"]["veto_labels"] = set(
|
||||
VISUAL_DEFAULT_CONFIG["grid"]["confidence"]["det"]["veto_labels"]
|
||||
)
|
||||
cfg["gpu_priority"] = dict(VISUAL_DEFAULT_CONFIG["gpu_priority"])
|
||||
|
||||
cfg["gpu_priority"]["mode_scales"] = dict(
|
||||
VISUAL_DEFAULT_CONFIG["gpu_priority"].get("mode_scales", {})
|
||||
)
|
||||
|
||||
cfg["gpu_priority"]["mode_floors"] = {
|
||||
k: dict(v)
|
||||
for k, v in VISUAL_DEFAULT_CONFIG["gpu_priority"].get("mode_floors", {}).items()
|
||||
}
|
||||
|
||||
cfg["gpu_priority"]["critical_mode"] = dict(
|
||||
VISUAL_DEFAULT_CONFIG["gpu_priority"].get("critical_mode", {})
|
||||
)
|
||||
|
||||
if "mode_configs" in VISUAL_DEFAULT_CONFIG["gpu_priority"]:
|
||||
cfg["gpu_priority"]["mode_configs"] = {
|
||||
k: dict(v)
|
||||
for k, v in VISUAL_DEFAULT_CONFIG["gpu_priority"]["mode_configs"].items()
|
||||
}
|
||||
|
||||
cfg = aplicar_overrides_redis_seg(cfg)
|
||||
cfg = normalizar_config_runtime_seg(cfg)
|
||||
|
||||
_CONFIG_CACHE = cfg
|
||||
return _CONFIG_CACHE
|
||||
|
||||
|
||||
def load_det_config():
|
||||
_CONFIG_DET = {
|
||||
"debug_visual": False,
|
||||
"com_track": False,
|
||||
"ia_roi_begin": 0.0,
|
||||
"ia_roi_size": 1.0,
|
||||
"ia_resolution": [300,300],
|
||||
"seg_every_n": 1,
|
||||
"det_every_n": 1,
|
||||
"ia_conf": 0.5,
|
||||
"classes": [
|
||||
"background",
|
||||
"aeroplane","bicycle","bird","boat","bottle",
|
||||
"bus","car","cat","chair","cow",
|
||||
"diningtable","dog","horse","motorbike","person",
|
||||
"pottedplant","sheep","sofa","train","tvmonitor",
|
||||
]
|
||||
}
|
||||
_CONFIG_DET["ia_model_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_model_ruas_det", "C:/AgroBaseModels/Ruas/modeldet-1_0.blob")
|
||||
return _CONFIG_DET
|
||||
|
||||
cfg = dict(DET_DEFAULT_CONFIG)
|
||||
cfg = aplicar_overrides_redis_det(cfg)
|
||||
cfg = normalizar_config_runtime_det(cfg)
|
||||
return cfg
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,285 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional, Sequence, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from shared.enums import TipoFrameCamera
|
||||
from shared.utils import converter_mask_ids_para_bgr
|
||||
from visual_worker.processamento.costmap_fuser import unpack_snapshot
|
||||
from visual_worker.utils import gerar_heatmap
|
||||
|
||||
|
||||
class VisualDebugRenderer:
|
||||
def __init__(self, color_map=None, preview_size=(1280, 720)):
|
||||
self.color_map = color_map
|
||||
self.preview_size = tuple(preview_size)
|
||||
|
||||
self._ultimo_preview_ts = 0.0
|
||||
self._ultimo_rgb = None
|
||||
self._ultimo_seg = None
|
||||
self._ultimo_overlay = None
|
||||
self._ultimo_heatmap = None
|
||||
self._ultimo_grid = None
|
||||
self._ultimo_dets = None
|
||||
|
||||
def get_selected_frame(
|
||||
self,
|
||||
frame_type: TipoFrameCamera,
|
||||
rgb_frame=None,
|
||||
pred_ids=None,
|
||||
depth_frame=None,
|
||||
detections=None,
|
||||
snapshot=None,
|
||||
camera_params=None,
|
||||
alpha=0.50,
|
||||
):
|
||||
if frame_type == TipoFrameCamera.Rgb:
|
||||
return self._resize_rgb(rgb_frame)
|
||||
|
||||
if frame_type == TipoFrameCamera.Segmentacao:
|
||||
return self.build_segmentation_preview(pred_ids)
|
||||
|
||||
if frame_type == TipoFrameCamera.Overlay:
|
||||
return self.build_overlay(rgb_frame, pred_ids, alpha=alpha)
|
||||
|
||||
if frame_type == TipoFrameCamera.Heatmap:
|
||||
distancia_maxima = 10000
|
||||
if camera_params:
|
||||
distancia_maxima = camera_params.get("distancia_maxima", distancia_maxima)
|
||||
return self.build_heatmap(depth_frame, distancia_maxima=distancia_maxima)
|
||||
|
||||
if frame_type == TipoFrameCamera.MatrizCusto:
|
||||
return self.build_costmap_debug(rgb_frame, snapshot)
|
||||
|
||||
if frame_type == TipoFrameCamera.Deteccoes:
|
||||
return self.build_detection_overlay(rgb_frame, detections)
|
||||
|
||||
if frame_type == TipoFrameCamera.Debug:
|
||||
overlay = self.build_overlay(rgb_frame, pred_ids, alpha=alpha)
|
||||
grid = self.build_costmap_debug(rgb_frame, snapshot)
|
||||
return grid if grid is not None else overlay
|
||||
|
||||
return None
|
||||
|
||||
def _resize_rgb(self, rgb_frame):
|
||||
if rgb_frame is None:
|
||||
return None
|
||||
|
||||
img = rgb_frame.copy()
|
||||
|
||||
if img.shape[1::-1] != self.preview_size:
|
||||
img = cv2.resize(img, self.preview_size, interpolation=cv2.INTER_AREA)
|
||||
|
||||
return img
|
||||
|
||||
def build_segmentation_preview(self, pred_ids):
|
||||
if pred_ids is None or self.color_map is None:
|
||||
return None
|
||||
|
||||
pred_ids = np.asarray(pred_ids)
|
||||
|
||||
if pred_ids.ndim == 3 and pred_ids.shape[-1] == 1:
|
||||
pred_ids = pred_ids[..., 0]
|
||||
|
||||
seg_bgr = converter_mask_ids_para_bgr(pred_ids, self.color_map)
|
||||
|
||||
if seg_bgr.shape[1::-1] != self.preview_size:
|
||||
seg_bgr = cv2.resize(seg_bgr, self.preview_size, interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
self._ultimo_seg = seg_bgr
|
||||
return seg_bgr
|
||||
|
||||
def build_overlay(self, rgb_frame, pred_ids, alpha=0.50):
|
||||
if rgb_frame is None or pred_ids is None or self.color_map is None:
|
||||
return None
|
||||
|
||||
rgb = self._resize_rgb(rgb_frame)
|
||||
seg = self.build_segmentation_preview(pred_ids)
|
||||
|
||||
if rgb is None or seg is None:
|
||||
return None
|
||||
|
||||
a = float(np.clip(alpha, 0.0, 1.0))
|
||||
overlay = cv2.addWeighted(rgb, 1.0 - a, seg, a, 0)
|
||||
|
||||
self._ultimo_overlay = overlay
|
||||
return overlay
|
||||
|
||||
def build_heatmap(self, depth_frame, distancia_maxima=10000):
|
||||
if depth_frame is None:
|
||||
return None
|
||||
|
||||
heatmap = gerar_heatmap(depth_frame, distancia_maxima)
|
||||
|
||||
if heatmap.shape[1::-1] != self.preview_size:
|
||||
heatmap = cv2.resize(heatmap, self.preview_size, interpolation=cv2.INTER_AREA)
|
||||
|
||||
self._ultimo_heatmap = heatmap
|
||||
return heatmap
|
||||
|
||||
def build_detection_overlay(self, rgb_frame, dets, conf_thr=0.5):
|
||||
if rgb_frame is None:
|
||||
return None
|
||||
|
||||
img = self._resize_rgb(rgb_frame)
|
||||
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
if not dets:
|
||||
self._ultimo_dets = img
|
||||
return img
|
||||
|
||||
H, W = img.shape[:2]
|
||||
|
||||
palette = [
|
||||
(255, 56, 56), (255, 157, 151), (72, 249, 10),
|
||||
(0, 255, 0), (0, 0, 255), (255, 0, 255),
|
||||
(0, 255, 255), (255, 191, 0), (52, 148, 230),
|
||||
(147, 112, 219),
|
||||
]
|
||||
|
||||
for d in dets:
|
||||
if float(d.get("conf", 0.0)) < conf_thr:
|
||||
continue
|
||||
|
||||
bbox = d.get("bbox_norm")
|
||||
bbox_full = d.get("bbox_full") or d.get("bbox_px")
|
||||
|
||||
if bbox_full:
|
||||
x0, y0, x1, y1 = bbox_full
|
||||
|
||||
# Assume origem 1920x1080 quando bbox vem do frame cheio.
|
||||
sx = W / 1920.0
|
||||
sy = H / 1080.0
|
||||
|
||||
x0 = int(round(x0 * sx))
|
||||
x1 = int(round(x1 * sx))
|
||||
y0 = int(round(y0 * sy))
|
||||
y1 = int(round(y1 * sy))
|
||||
|
||||
elif bbox:
|
||||
x0n, y0n, x1n, y1n = bbox
|
||||
x0 = int(round(x0n * W))
|
||||
x1 = int(round(x1n * W))
|
||||
y0 = int(round(y0n * H))
|
||||
y1 = int(round(y1n * H))
|
||||
else:
|
||||
continue
|
||||
|
||||
x0 = max(0, min(W - 1, x0))
|
||||
x1 = max(0, min(W - 1, x1))
|
||||
y0 = max(0, min(H - 1, y0))
|
||||
y1 = max(0, min(H - 1, y1))
|
||||
|
||||
if x1 <= x0 or y1 <= y0:
|
||||
continue
|
||||
|
||||
lid = int(d.get("label_id", -1))
|
||||
color = palette[lid % len(palette)] if lid >= 0 else (0, 255, 0)
|
||||
|
||||
cv2.rectangle(img, (x0, y0), (x1, y1), color, 2)
|
||||
|
||||
label = d.get("label") or f"id:{lid}"
|
||||
conf = float(d.get("conf", 0.0))
|
||||
txt = f"{label} {conf:.2f}"
|
||||
|
||||
dist = d.get("distancia_m")
|
||||
if dist is not None:
|
||||
txt += f" {float(dist):.2f}m"
|
||||
|
||||
self._put_label(img, txt, x0, y0, color)
|
||||
|
||||
self._ultimo_dets = img
|
||||
return img
|
||||
|
||||
def build_costmap_debug(self, rgb_frame, snapshot, alpha=0.35):
|
||||
if rgb_frame is None or snapshot is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
img = self._resize_rgb(rgb_frame)
|
||||
|
||||
custo, conf, anom, nav = unpack_snapshot(snapshot)
|
||||
|
||||
mask = self._colorize_costmap(anom, custo, conf)
|
||||
mask = cv2.resize(mask, self.preview_size, interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
vis = cv2.addWeighted(img, 1.0, mask, alpha, 0)
|
||||
|
||||
metrics = snapshot.get("block", {}) or {}
|
||||
blocked = bool(metrics.get("blocked", False))
|
||||
reason = str(metrics.get("reason", "free"))
|
||||
d_obs = metrics.get("d_obs_true_min_m")
|
||||
|
||||
status_txt = f"{'PARAR' if blocked else 'LIVRE'} | {reason}"
|
||||
if d_obs is not None:
|
||||
status_txt += f" | d={float(d_obs):.2f}m"
|
||||
|
||||
color = (0, 0, 255) if blocked else (0, 220, 0)
|
||||
|
||||
cv2.putText(
|
||||
vis,
|
||||
status_txt,
|
||||
(20, 35),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.8,
|
||||
(0, 0, 0),
|
||||
4,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
cv2.putText(
|
||||
vis,
|
||||
status_txt,
|
||||
(20, 35),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.8,
|
||||
color,
|
||||
2,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
|
||||
self._ultimo_grid = vis
|
||||
return vis
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _colorize_costmap(anom_f, custo_f, conf_f, thr_anom=0.50, thr_cost=0.65, thr_conf=0.35):
|
||||
H, W = anom_f.shape
|
||||
over = np.zeros((H, W, 3), np.uint8)
|
||||
|
||||
masks = [
|
||||
(anom_f >= thr_anom, (255, 0, 255)),
|
||||
(custo_f >= thr_cost, (0, 165, 255)),
|
||||
(conf_f < thr_conf, (255, 0, 0)),
|
||||
]
|
||||
|
||||
for mask, bgr in masks:
|
||||
if np.any(mask):
|
||||
tmp = over[mask].astype(np.int16)
|
||||
tmp += np.array(bgr, dtype=np.int16)
|
||||
np.clip(tmp, 0, 255, out=tmp)
|
||||
over[mask] = tmp.astype(np.uint8)
|
||||
|
||||
return over
|
||||
|
||||
@staticmethod
|
||||
def _put_label(img, text, x, y, bg):
|
||||
(tw, th), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
|
||||
|
||||
y0 = max(0, y - th - 6)
|
||||
cv2.rectangle(img, (x, y0), (x + tw + 6, y), bg, -1)
|
||||
cv2.putText(
|
||||
img,
|
||||
text,
|
||||
(x + 3, y - 4),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.5,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
|
|
@ -0,0 +1,456 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional, Sequence, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from visual_worker.processamento.segmentacao_semantica import ClassesSegmentacao
|
||||
|
||||
|
||||
@dataclass
|
||||
class GridGeometryConfig:
|
||||
grid_shape: Tuple[int, int] = (15, 10) # (cols, rows)
|
||||
camera_pitch_deg: float = 28.91
|
||||
camera_height_m: float = 0.74
|
||||
fov_v_deg: float = 43.28
|
||||
pitch_gain: float = 1.0
|
||||
pitch_limit_deg: float = 10.0
|
||||
min_dist_m: float = 0.2
|
||||
max_dist_m: float = 20.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectionGridConfig:
|
||||
w4: float = 0.18
|
||||
thr_det_soft: float = 0.45
|
||||
min_cell_coverage: float = 0.10
|
||||
min_det_conf: float = 0.45
|
||||
class_weights: Dict[str, float] = field(default_factory=lambda: {
|
||||
"person": 1.0,
|
||||
"dog": 0.7,
|
||||
"cat": 0.5,
|
||||
})
|
||||
veto_labels: set[str] = field(default_factory=lambda: {"person"})
|
||||
combine: str = "max"
|
||||
conf_drop_alpha: float = 0.0
|
||||
only_veto_blocks_nav: bool = True
|
||||
non_veto_cost_scale: float = 0.50
|
||||
|
||||
|
||||
@dataclass
|
||||
class GridConfidenceConfig:
|
||||
valid_mm: Tuple[int, int] = (300, 10000)
|
||||
min_valid_frac: float = 0.30
|
||||
conf_params: Tuple[float, float] = (0.30, 0.80)
|
||||
|
||||
weights: Tuple[float, float, float] = (0.65, 0.25, 0.10)
|
||||
|
||||
usar_depth: bool = True
|
||||
|
||||
anom_tau_up: float = 0.15
|
||||
anom_satur_up_m: float = 0.35
|
||||
|
||||
anom_tau_down: float = 0.30
|
||||
anom_satur_down_m: float = 0.55
|
||||
anom_down_weight: float = 0.65
|
||||
|
||||
min_pct_navegavel: float = 0.55
|
||||
max_anom_navegavel: float = 0.45
|
||||
min_conf_navegavel: float = 0.35
|
||||
|
||||
det: DetectionGridConfig = field(default_factory=DetectionGridConfig)
|
||||
|
||||
|
||||
class GridReferenceBuilder:
|
||||
def __init__(self, config: Optional[GridGeometryConfig | Dict[str, Any]] = None):
|
||||
self.config = self._normalizar_config(config)
|
||||
self.grid_ref_base = self.gerar_grid_ref(pitch_graus=0.0)
|
||||
self.grid_ref_atual = self.grid_ref_base.copy()
|
||||
|
||||
@staticmethod
|
||||
def _normalizar_config(config):
|
||||
if config is None:
|
||||
return GridGeometryConfig()
|
||||
if isinstance(config, GridGeometryConfig):
|
||||
return config
|
||||
if isinstance(config, dict):
|
||||
allowed = set(GridGeometryConfig.__dataclass_fields__.keys())
|
||||
return GridGeometryConfig(**{k: v for k, v in config.items() if k in allowed})
|
||||
raise TypeError("config inválido para GridReferenceBuilder")
|
||||
|
||||
@property
|
||||
def grid_shape(self) -> Tuple[int, int]:
|
||||
return self.config.grid_shape
|
||||
|
||||
def gerar_grid_ref(self, pitch_graus: float = 0.0) -> np.ndarray:
|
||||
cfg = self.config
|
||||
grid_w, grid_h = cfg.grid_shape
|
||||
|
||||
pitch_corr = float(np.clip(
|
||||
pitch_graus * cfg.pitch_gain,
|
||||
-cfg.pitch_limit_deg,
|
||||
cfg.pitch_limit_deg,
|
||||
))
|
||||
|
||||
incl_deg = cfg.camera_pitch_deg + pitch_corr
|
||||
|
||||
dist = []
|
||||
for i in range(grid_h):
|
||||
alpha_v = ((i + 0.5) / grid_h - 0.5) * np.radians(abs(cfg.fov_v_deg))
|
||||
gamma = np.radians(incl_deg) + alpha_v
|
||||
gamma = max(gamma, np.radians(2.0))
|
||||
|
||||
d = cfg.camera_height_m / np.tan(gamma)
|
||||
d = float(np.clip(d, cfg.min_dist_m, cfg.max_dist_m))
|
||||
dist.append(d)
|
||||
|
||||
return np.asarray(dist, dtype=np.float32)
|
||||
|
||||
def atualizar_por_pitch(self, pitch_graus: float) -> np.ndarray:
|
||||
try:
|
||||
ref = self.gerar_grid_ref(pitch_graus=pitch_graus)
|
||||
|
||||
grid_h = self.config.grid_shape[1]
|
||||
|
||||
if ref is None or len(ref) != grid_h:
|
||||
return self.grid_ref_atual
|
||||
|
||||
if not np.all(np.isfinite(ref)) or np.any(ref <= 0):
|
||||
return self.grid_ref_atual
|
||||
|
||||
self.grid_ref_atual = ref
|
||||
return self.grid_ref_atual
|
||||
|
||||
except Exception:
|
||||
return self.grid_ref_atual
|
||||
|
||||
|
||||
class VisualGridBuilder:
|
||||
"""
|
||||
Constrói a grid de confiança/custo usada pelo CostmapFuser.
|
||||
|
||||
Entrada:
|
||||
- depth_mm
|
||||
- seg_ids
|
||||
- grid_ref
|
||||
- detecções
|
||||
|
||||
Saída:
|
||||
dict com pct_navegavel, conf, anom, custo, navegavel e canais de detecção.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[GridConfidenceConfig | Dict[str, Any]] = None):
|
||||
self.config = self._normalizar_config(config)
|
||||
|
||||
@staticmethod
|
||||
def _normalizar_config(config):
|
||||
if config is None:
|
||||
return GridConfidenceConfig()
|
||||
if isinstance(config, GridConfidenceConfig):
|
||||
return config
|
||||
if isinstance(config, dict):
|
||||
allowed = set(GridConfidenceConfig.__dataclass_fields__.keys())
|
||||
data = {k: v for k, v in config.items() if k in allowed}
|
||||
|
||||
if isinstance(data.get("det"), dict):
|
||||
det_allowed = set(DetectionGridConfig.__dataclass_fields__.keys())
|
||||
data["det"] = DetectionGridConfig(**{
|
||||
k: v for k, v in data["det"].items() if k in det_allowed
|
||||
})
|
||||
|
||||
return GridConfidenceConfig(**data)
|
||||
|
||||
raise TypeError("config inválido para VisualGridBuilder")
|
||||
|
||||
def build(
|
||||
self,
|
||||
depth_mm: Optional[np.ndarray],
|
||||
seg_ids: np.ndarray,
|
||||
grid_ref: np.ndarray,
|
||||
grid_shape: Tuple[int, int],
|
||||
deteccoes: Optional[Sequence[Dict[str, Any]]] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
cfg = self.config
|
||||
det_cfg = cfg.det
|
||||
|
||||
seg_ids = np.asarray(seg_ids)
|
||||
|
||||
if seg_ids.ndim != 2:
|
||||
raise ValueError(f"seg_ids precisa ser 2D, veio {seg_ids.shape}")
|
||||
|
||||
grid_w, grid_h = grid_shape
|
||||
H1, W1 = seg_ids.shape
|
||||
|
||||
d_small = None
|
||||
if cfg.usar_depth and depth_mm is not None:
|
||||
d_small = cv2.resize(
|
||||
depth_mm,
|
||||
(W1, H1),
|
||||
interpolation=cv2.INTER_NEAREST,
|
||||
).astype(np.float32)
|
||||
|
||||
d_small[
|
||||
(d_small < cfg.valid_mm[0]) |
|
||||
(d_small > cfg.valid_mm[1])
|
||||
] = np.nan
|
||||
|
||||
x_edges = np.linspace(0, W1, grid_w + 1, dtype=int)
|
||||
y_edges = np.linspace(0, H1, grid_h + 1, dtype=int)
|
||||
|
||||
pct_navegavel = np.zeros((grid_h, grid_w), np.float32)
|
||||
pct_nao_navegavel = np.zeros((grid_h, grid_w), np.float32)
|
||||
z_med = np.full((grid_h, grid_w), np.nan, np.float32)
|
||||
depth_valid_frac = np.zeros((grid_h, grid_w), np.float32)
|
||||
|
||||
det_cov_max = np.zeros((grid_h, grid_w), np.float32)
|
||||
det_conf_max = np.zeros((grid_h, grid_w), np.float32)
|
||||
det_score = np.zeros((grid_h, grid_w), np.float32)
|
||||
det_top_label_id = -np.ones((grid_h, grid_w), np.int32)
|
||||
det_top_conf = np.zeros((grid_h, grid_w), np.float32)
|
||||
det_is_veto = np.zeros((grid_h, grid_w), np.float32)
|
||||
|
||||
grid_ref = np.asarray(grid_ref, dtype=np.float32)
|
||||
|
||||
if grid_ref.ndim == 1:
|
||||
if grid_ref.shape[0] != grid_h:
|
||||
raise ValueError(f"grid_ref 1D deve ter len={grid_h}, veio {grid_ref.shape}")
|
||||
z_ref = np.repeat(grid_ref[:, None], grid_w, axis=1)
|
||||
else:
|
||||
z_ref = grid_ref
|
||||
if z_ref.shape != (grid_h, grid_w):
|
||||
raise ValueError(f"grid_ref 2D deve ser {(grid_h, grid_w)}, veio {z_ref.shape}")
|
||||
|
||||
for j in range(grid_h):
|
||||
y0, y1 = int(y_edges[j]), int(y_edges[j + 1])
|
||||
seg_row = seg_ids[y0:y1, :]
|
||||
depth_row = d_small[y0:y1, :] if d_small is not None else None
|
||||
|
||||
for i in range(grid_w):
|
||||
x0, x1 = int(x_edges[i]), int(x_edges[i + 1])
|
||||
|
||||
seg_block = seg_row[:, x0:x1]
|
||||
n = seg_block.size
|
||||
if n == 0:
|
||||
continue
|
||||
|
||||
n_nav = np.count_nonzero(seg_block == ClassesSegmentacao.NAVEGAVEL.value)
|
||||
n_naonav = np.count_nonzero(seg_block == ClassesSegmentacao.NAONAVEGAVEL.value)
|
||||
|
||||
pct_navegavel[j, i] = n_nav / n
|
||||
pct_nao_navegavel[j, i] = n_naonav / n
|
||||
|
||||
if depth_row is not None:
|
||||
depth_block = depth_row[:, x0:x1]
|
||||
vals = depth_block[~np.isnan(depth_block)]
|
||||
valid = vals.size
|
||||
depth_valid_frac[j, i] = valid / n
|
||||
|
||||
if valid >= max(int(cfg.min_valid_frac * n), 1):
|
||||
z_med[j, i] = np.nanmedian(vals) / 1000.0
|
||||
|
||||
t0, t1 = cfg.conf_params
|
||||
conf_seg = np.maximum(pct_navegavel, pct_nao_navegavel)
|
||||
|
||||
if cfg.usar_depth and d_small is not None:
|
||||
conf_dep = np.clip(
|
||||
(depth_valid_frac - t0) / max(1e-6, (t1 - t0)),
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
conf_cell = 0.65 * conf_seg + 0.35 * conf_dep
|
||||
else:
|
||||
conf_dep = np.zeros_like(conf_seg, dtype=np.float32)
|
||||
conf_cell = conf_seg.copy()
|
||||
|
||||
if deteccoes:
|
||||
self._rasterizar_deteccoes(
|
||||
deteccoes=deteccoes,
|
||||
x_edges=x_edges,
|
||||
y_edges=y_edges,
|
||||
W1=W1,
|
||||
H1=H1,
|
||||
det_cfg=det_cfg,
|
||||
det_cov_max=det_cov_max,
|
||||
det_conf_max=det_conf_max,
|
||||
det_score=det_score,
|
||||
det_top_label_id=det_top_label_id,
|
||||
det_top_conf=det_top_conf,
|
||||
det_is_veto=det_is_veto,
|
||||
)
|
||||
|
||||
if cfg.usar_depth and d_small is not None:
|
||||
delta_signed = z_ref - z_med
|
||||
delta_signed = np.where(np.isnan(z_med), 0.0, delta_signed)
|
||||
|
||||
delta_up = np.maximum(delta_signed, 0.0)
|
||||
anom_up_raw = np.clip(delta_up / max(1e-6, cfg.anom_satur_up_m), 0.0, 1.0)
|
||||
anom_up = (
|
||||
anom_up_raw *
|
||||
(delta_up > cfg.anom_tau_up).astype(np.float32) *
|
||||
np.maximum(conf_dep, 0.25)
|
||||
)
|
||||
|
||||
delta_down = np.maximum(-delta_signed, 0.0)
|
||||
anom_down_raw = np.clip(delta_down / max(1e-6, cfg.anom_satur_down_m), 0.0, 1.0)
|
||||
anom_down = (
|
||||
anom_down_raw *
|
||||
(delta_down > cfg.anom_tau_down).astype(np.float32) *
|
||||
np.maximum(conf_dep, 0.25)
|
||||
)
|
||||
|
||||
anom = np.clip(
|
||||
np.maximum(anom_up, cfg.anom_down_weight * anom_down),
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
else:
|
||||
anom_up = np.zeros_like(pct_navegavel, dtype=np.float32)
|
||||
anom_down = np.zeros_like(pct_navegavel, dtype=np.float32)
|
||||
anom = np.zeros_like(pct_navegavel, dtype=np.float32)
|
||||
|
||||
nao_navegavel = 1.0 - pct_navegavel
|
||||
w1, w2, w3 = cfg.weights
|
||||
|
||||
custo_base = (
|
||||
w1 * nao_navegavel +
|
||||
w2 * anom +
|
||||
w3 * (1.0 - conf_cell)
|
||||
)
|
||||
|
||||
custo = np.clip(custo_base + det_cfg.w4 * det_score, 0.0, 1.0)
|
||||
|
||||
veto_soft_block = (det_is_veto > 0.5) & (det_score >= det_cfg.thr_det_soft)
|
||||
|
||||
navegavel = (
|
||||
(pct_navegavel >= cfg.min_pct_navegavel) &
|
||||
(anom < cfg.max_anom_navegavel) &
|
||||
(conf_cell >= cfg.min_conf_navegavel) &
|
||||
(
|
||||
~veto_soft_block
|
||||
if det_cfg.only_veto_blocks_nav
|
||||
else (det_score < det_cfg.thr_det_soft)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"pct_navegavel": np.clip(pct_navegavel, 0.0, 1.0),
|
||||
"pct_nao_navegavel": np.clip(pct_nao_navegavel, 0.0, 1.0),
|
||||
|
||||
"z_med": z_med,
|
||||
"z_ref": z_ref,
|
||||
"depth_valid_frac": np.clip(depth_valid_frac, 0.0, 1.0),
|
||||
|
||||
"conf": np.clip(conf_cell, 0.0, 1.0),
|
||||
"anom_up": np.clip(anom_up, 0.0, 1.0),
|
||||
"anom_down": np.clip(anom_down, 0.0, 1.0),
|
||||
"anom": np.clip(anom, 0.0, 1.0),
|
||||
"custo": np.clip(custo, 0.0, 1.0),
|
||||
"navegavel": navegavel.astype(np.uint8),
|
||||
|
||||
"det_cov_max": np.clip(det_cov_max, 0.0, 1.0),
|
||||
"det_conf_max": np.clip(det_conf_max, 0.0, 1.0),
|
||||
"det_score": np.clip(det_score, 0.0, 1.0),
|
||||
"det_top_label_id": det_top_label_id,
|
||||
"det_top_conf": np.clip(det_top_conf, 0.0, 1.0),
|
||||
"det_is_veto": det_is_veto.astype(np.float32),
|
||||
}
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _rasterizar_deteccoes(
|
||||
deteccoes,
|
||||
x_edges,
|
||||
y_edges,
|
||||
W1,
|
||||
H1,
|
||||
det_cfg,
|
||||
det_cov_max,
|
||||
det_conf_max,
|
||||
det_score,
|
||||
det_top_label_id,
|
||||
det_top_conf,
|
||||
det_is_veto,
|
||||
):
|
||||
grid_h, grid_w = det_score.shape
|
||||
|
||||
for det in deteccoes:
|
||||
conf = float(det.get("conf", 0.0))
|
||||
if conf < det_cfg.min_det_conf:
|
||||
continue
|
||||
|
||||
label = str(det.get("label", "")).strip()
|
||||
label_id = int(det.get("label_id", -1))
|
||||
|
||||
is_veto = label in det_cfg.veto_labels
|
||||
w_class = float(det_cfg.class_weights.get(label, 1.0))
|
||||
|
||||
if not is_veto:
|
||||
w_class *= float(det_cfg.non_veto_cost_scale)
|
||||
|
||||
if "bbox_px" in det and det["bbox_px"]:
|
||||
x0p, y0p, x1p, y1p = det["bbox_px"]
|
||||
else:
|
||||
bbox = det.get("bbox_norm")
|
||||
if not bbox:
|
||||
continue
|
||||
|
||||
x0n, y0n, x1n, y1n = bbox
|
||||
x0p = int(np.clip(x0n * W1, 0, W1 - 1))
|
||||
x1p = int(np.clip(x1n * W1, 0, W1))
|
||||
y0p = int(np.clip(y0n * H1, 0, H1 - 1))
|
||||
y1p = int(np.clip(y1n * H1, 0, H1))
|
||||
|
||||
if x1p <= x0p or y1p <= y0p:
|
||||
continue
|
||||
|
||||
i0 = max(0, np.searchsorted(x_edges, x0p, side="right") - 1)
|
||||
i1 = min(grid_w - 1, np.searchsorted(x_edges, x1p, side="left"))
|
||||
j0 = max(0, np.searchsorted(y_edges, y0p, side="right") - 1)
|
||||
j1 = min(grid_h - 1, np.searchsorted(y_edges, y1p, side="left"))
|
||||
|
||||
for j in range(j0, j1 + 1):
|
||||
y0c, y1c = int(y_edges[j]), int(y_edges[j + 1])
|
||||
|
||||
for i in range(i0, i1 + 1):
|
||||
x0c, x1c = int(x_edges[i]), int(x_edges[i + 1])
|
||||
|
||||
ix0 = max(x0c, x0p)
|
||||
ix1 = min(x1c, x1p)
|
||||
iy0 = max(y0c, y0p)
|
||||
iy1 = min(y1c, y1p)
|
||||
|
||||
if ix1 <= ix0 or iy1 <= iy0:
|
||||
continue
|
||||
|
||||
inter = float((ix1 - ix0) * (iy1 - iy0))
|
||||
cell_area = float((x1c - x0c) * (y1c - y0c))
|
||||
|
||||
if cell_area <= 0:
|
||||
continue
|
||||
|
||||
cov = inter / cell_area
|
||||
|
||||
if cov < det_cfg.min_cell_coverage:
|
||||
continue
|
||||
|
||||
score_local = conf * cov * w_class
|
||||
|
||||
det_cov_max[j, i] = max(det_cov_max[j, i], cov)
|
||||
det_conf_max[j, i] = max(det_conf_max[j, i], conf)
|
||||
|
||||
if det_cfg.combine == "sum_clamped":
|
||||
det_score[j, i] = np.clip(det_score[j, i] + score_local, 0.0, 1.0)
|
||||
else:
|
||||
det_score[j, i] = max(det_score[j, i], score_local)
|
||||
|
||||
priority = (2.0 if is_veto else 1.0) * conf * cov
|
||||
|
||||
if priority > det_top_conf[j, i]:
|
||||
det_top_conf[j, i] = priority
|
||||
det_top_label_id[j, i] = label_id
|
||||
det_is_veto[j, i] = 1.0 if is_veto else 0.0
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,5 @@
|
|||
import time
|
||||
import json
|
||||
import threading
|
||||
import os
|
||||
|
||||
from weed_worker.camera_manager import CameraManager
|
||||
from shared.contexto_global_redis import ContextoGlobalRedis
|
||||
|
|
@ -34,147 +32,209 @@ def iniciar_camera_manager(mx_id):
|
|||
mostrar_log(f"✅ Camera manager iniciado, com MX_ID: {mx_id}")
|
||||
|
||||
|
||||
_CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
|
||||
_CONFIG_CACHE = None
|
||||
_CONFIG_MTIME = None
|
||||
_CONFIG_LOCK = threading.Lock()
|
||||
|
||||
def load_seg_config(force_reload=False):
|
||||
global _CONFIG_CACHE, _CONFIG_MTIME
|
||||
WEED_DEFAULT_CONFIG = {
|
||||
# ============================================================
|
||||
# 1) Debug e telemetria
|
||||
# ============================================================
|
||||
# Mostra janela OpenCV/debug visual. Não usar em runtime de campo.
|
||||
"debug_visual": False,
|
||||
|
||||
# Publica logs de performance no console.
|
||||
"debug_perf": True,
|
||||
|
||||
# Inclui tempos internos do WeedDetector no payload de análise.
|
||||
# Barato e útil nesta fase; pode desligar na versão final final.
|
||||
"detector_debug_perf": True,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2) Frequências dos loops
|
||||
# ============================================================
|
||||
# Pipeline oficial validado em ~25 FPS.
|
||||
"tensor_fps": 25.0,
|
||||
"inferencia_fps": 25.0,
|
||||
"deteccao_fps": 25.0,
|
||||
|
||||
# Supervisor/performance. Não precisa ser igual ao pipeline.
|
||||
"analise_fps": 15.0,
|
||||
|
||||
# Publicação Redis. Mantém baixo para não virar ruído.
|
||||
"publicacao_fps": 5.0,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3) Câmera multiespectral
|
||||
# ============================================================
|
||||
"camera_width": 1280,
|
||||
"camera_height": 800,
|
||||
|
||||
# FPS solicitado na câmera/OAK. Pode ser maior que o pipeline.
|
||||
"camera_fps": 40,
|
||||
|
||||
# Tamanho final do tensor entregue ao modelo: [W, H].
|
||||
"ia_resolution": [1024, 640],
|
||||
|
||||
# Ordem oficial do tensor multiespectral.
|
||||
# Deve bater com o modelo ONNX exportado.
|
||||
"input_channels": ["R", "G", "B", "RE", "NIR"],
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 4) Modelo ONNX/TensorRT
|
||||
# ============================================================
|
||||
# Runtime oficial da primeira versão.
|
||||
"runtime_backend": "onnx",
|
||||
"onnx_provider": "tensorrt",
|
||||
|
||||
# Modo operacional: usar diretamente a cabeça target.
|
||||
"runtime_mode": "target_direct",
|
||||
"onnx_output_mode": "target_direct",
|
||||
|
||||
# Contrato do ONNX novo:
|
||||
# entrada 0..1 crua -> normalização interna -> resize -> argmax -> target_mask.
|
||||
"onnx_output_kind": "mask",
|
||||
"onnx_preprocess_norm": False,
|
||||
|
||||
# TensorRT FP16 validado.
|
||||
"trt_fp16": True,
|
||||
|
||||
# Assume que o tensor já vem float32 CHW 0..1, contíguo e limpo.
|
||||
"trust_input": True,
|
||||
|
||||
# Não retornar dict completo no caminho quente.
|
||||
"return_full_fast": False,
|
||||
|
||||
# Sincronização CUDA só para debug fino. Deixar False no runtime.
|
||||
"sync_for_timing": False,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 5) Contrato da máscara para o WeedDetector
|
||||
# ============================================================
|
||||
# target_binary:
|
||||
# 0 = fundo / não pulverizar
|
||||
# 1 = alvo pulverizável
|
||||
"prediction_contract": "target_binary",
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 6) Radar global de alvo
|
||||
# ============================================================
|
||||
# Gate global: só libera bicos quando existe alvo suficiente no frame.
|
||||
"usar_radar_global_gate": True,
|
||||
|
||||
# Histerese global da fração de alvo no frame completo.
|
||||
"min_frac_erva_global_on": 0.0020,
|
||||
"min_frac_erva_global_off": 0.0015,
|
||||
|
||||
# Suavização temporal da fração global.
|
||||
"erva_frac_ema": 0.30,
|
||||
|
||||
# Ajuste do limiar global pela velocidade.
|
||||
# 0.0 desativa; valores maiores deixam o gate mais sensível com velocidade.
|
||||
"erva_thresh_vel_gain": 0.40,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 7) Controle por bico
|
||||
# ============================================================
|
||||
"qtd_bicos": 7,
|
||||
|
||||
# Suavização temporal por bico.
|
||||
"ema_frac_bico": 0.35,
|
||||
|
||||
# Debounce temporal por bico.
|
||||
"on_frames_required": 3,
|
||||
"off_frames_required": 2,
|
||||
|
||||
# Ajuste local do limiar por velocidade.
|
||||
"erva_thresh_vel_gain_local": 0.60,
|
||||
|
||||
# Desloca a ROI vertical para compensar latência em movimento.
|
||||
"k_roi_shift_px_per_vnorm": 24.0,
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 8) Filtros opcionais da máscara
|
||||
# ============================================================
|
||||
# No contrato target_binary, a própria IA já entrega o alvo final.
|
||||
# Deixar desligado no baseline oficial.
|
||||
"usar_morfologia": False,
|
||||
"kernel_morf": 3,
|
||||
|
||||
# Remove componentes pequenos. 0 desativa.
|
||||
"min_area_erva_px": 0,
|
||||
}
|
||||
|
||||
def aplicar_overrides_redis(cfg: dict) -> dict:
|
||||
dados_atu = ContextoGlobalRedis.get_operacao().get("Atu", {})
|
||||
contexto = ContextoGlobalRedis.get_contexto()
|
||||
equipamento = ContextoGlobalRedis.get_equipamento()
|
||||
|
||||
cfg["qtd_bicos"] = int(equipamento.get("qtd_bicos") or cfg.get("qtd_bicos", 7) or 7)
|
||||
|
||||
# Modelo ONNX full-runtime validado para o Weed Worker.
|
||||
cfg["ia_model_path"] = equipamento.get("path_ia_model_ervas")
|
||||
cfg["ia_module_params_path"] = equipamento.get("path_ia_module_params_ervas")
|
||||
|
||||
cfg["velocidade_robo"] = float(
|
||||
contexto.get("Gerais", {}).get("velocidade_ms", 0.0) or 0.0
|
||||
)
|
||||
|
||||
cfg["faixa_atuacao_bicos"] = float(
|
||||
dados_atu.get("percent_vertical_deteccao", 0.7)
|
||||
)
|
||||
|
||||
cfg["area_atuacao_bicos"] = float(
|
||||
dados_atu.get("height_area_deteccao", 0.1)
|
||||
)
|
||||
|
||||
cfg["min_frac_erva_por_bico_on"] = float(
|
||||
dados_atu.get("pct_erva_bico_on", 0.02)
|
||||
)
|
||||
|
||||
cfg["min_frac_erva_por_bico_off"] = float(
|
||||
dados_atu.get("pct_erva_bico_off", 0.01)
|
||||
)
|
||||
|
||||
return cfg
|
||||
|
||||
def normalizar_config_runtime(cfg: dict) -> dict:
|
||||
cfg["onnx_model_path"] = cfg.get("ia_model_path")
|
||||
cfg["module_calibration_json"] = cfg.get("ia_module_params_path")
|
||||
|
||||
cfg["camera_fps"] = int(cfg.get("camera_fps", 40))
|
||||
|
||||
input_channels = cfg.get("input_channels", ["R", "G", "B", "RE", "NIR"])
|
||||
if isinstance(input_channels, str):
|
||||
input_channels = [c.strip().upper() for c in input_channels.split(",") if c.strip()]
|
||||
else:
|
||||
input_channels = [str(c).upper() for c in input_channels]
|
||||
|
||||
cfg["input_channels"] = input_channels
|
||||
|
||||
if not cfg.get("onnx_model_path"):
|
||||
mostrar_log("[WARN] path do modelo ONNX de ervas não definido no Redis/equipamento.")
|
||||
|
||||
if not cfg.get("module_calibration_json"):
|
||||
mostrar_log("[WARN] path de calibração/module_params do WeedWorker não definido.")
|
||||
|
||||
if cfg["runtime_backend"] == "onnx" and cfg["onnx_preprocess_norm"]:
|
||||
mostrar_log("[WARN] onnx_preprocess_norm=True não é permitido no modelo full-runtime.")
|
||||
|
||||
return cfg
|
||||
|
||||
def load_seg_config():
|
||||
global _CONFIG_CACHE
|
||||
|
||||
with _CONFIG_LOCK:
|
||||
_CONFIG_CACHE = {
|
||||
"debug_visual": False,
|
||||
"debug_perf": False,
|
||||
"frames_consecutivos": 3,
|
||||
"frames_histerese": 2,
|
||||
"min_area_px": 400,
|
||||
"max_area_frac": 0.2,
|
||||
"ia_roi_begin": 0.0,
|
||||
"ia_roi_size": 1.0,
|
||||
cfg = dict(WEED_DEFAULT_CONFIG)
|
||||
|
||||
"analise_fps": 15.0,
|
||||
"inferencia_fps": 15.0,
|
||||
"deteccao_fps": 15.0,
|
||||
"tensor_fps": 18.0,
|
||||
"publicacao_fps": 15.0,
|
||||
cfg = aplicar_overrides_redis(cfg)
|
||||
cfg = normalizar_config_runtime(cfg)
|
||||
|
||||
"tipo_camera_solo": "multispectral",
|
||||
"camera_width": 1280,
|
||||
"camera_height": 800,
|
||||
"fps": 40,
|
||||
"ia_resolution": [1024,640],
|
||||
"ia_channels": 5,
|
||||
"ia_input_channels": ["R", "G", "B", "RE", "NIR"],
|
||||
"ia_use_ndvi": False,
|
||||
"amp": True,
|
||||
"fold_input_norm": True,
|
||||
"runtime_mode": "target_direct",
|
||||
"prediction_contract": "target_binary",
|
||||
"output_mask_fullres": False,
|
||||
"lowres_argmax": True,
|
||||
"trust_input": True,
|
||||
"channels_last": False,
|
||||
"model_half": True,
|
||||
"sync_for_timing": False,
|
||||
"torch_compile": False,
|
||||
"torch_compile_mode": "reduce-overhead",
|
||||
"return_full_fast": False,
|
||||
|
||||
"erva_top_band_frac": 0.30,
|
||||
"erva_frac_ema": 0.3,
|
||||
"erva_thresh_vel_gain": 0.4,
|
||||
"min_frac_erva_global_on": 0.0020,
|
||||
"min_frac_erva_global_off": 0.0015,
|
||||
"min_frac_erva_top_on": 0.0015,
|
||||
"min_frac_erva_top_off": 0.0010,
|
||||
"min_frac_erva_por_bico": 0.02,
|
||||
"usar_morfologia": True,
|
||||
"kernel_morf": 3,
|
||||
|
||||
"usar_radar_global_gate": True,
|
||||
"max_frac_cana_por_bico": 0.009,
|
||||
"ema_frac_bico": 0.35,
|
||||
"on_frames_required": 3,
|
||||
"off_frames_required": 2,
|
||||
"cana_halo_px": 5,
|
||||
"min_area_erva_px": 80,
|
||||
"erva_thresh_vel_gain_local": 0.6,
|
||||
"k_roi_shift_px_per_vnorm": 24.0,
|
||||
|
||||
"heads": {
|
||||
"semantic": {
|
||||
"enabled": True,
|
||||
"type": "multiclass",
|
||||
"num_classes": 3,
|
||||
"classes": {"chao": 0, "cana": 1, "erva": 2},
|
||||
"ignore_index": 255
|
||||
},
|
||||
"vegetation": {
|
||||
"enabled": True,
|
||||
"type": "binary",
|
||||
"num_classes": 2,
|
||||
"classes": {"background": 0, "vegetation": 1},
|
||||
"ignore_index": 255
|
||||
},
|
||||
"cana": {
|
||||
"enabled": True,
|
||||
"type": "binary",
|
||||
"num_classes": 2,
|
||||
"classes": {"not_cana": 0, "cana": 1},
|
||||
"ignore_index": 255
|
||||
},
|
||||
"target": {
|
||||
"enabled": True,
|
||||
"type": "binary",
|
||||
"num_classes": 2,
|
||||
"classes": {"background": 0, "target": 1},
|
||||
"ignore_index": 255
|
||||
},
|
||||
}
|
||||
}
|
||||
dadosAtu = ContextoGlobalRedis.get_operacao().get("Atu", {})
|
||||
contexto = ContextoGlobalRedis.get_contexto()
|
||||
equipamento = ContextoGlobalRedis.get_equipamento()
|
||||
_CONFIG_CACHE["qtd_bicos"] = equipamento.get("qtd_bicos")
|
||||
_CONFIG_CACHE["velocidade_robo"] = contexto.get("Gerais", {}).get("velocidade_ms", 0.0)
|
||||
|
||||
_CONFIG_CACHE["ia_model_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_model_ervas")
|
||||
_CONFIG_CACHE["ia_labelmap_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_labelmap_ervas")
|
||||
_CONFIG_CACHE["ia_norm_stats_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_norm_stats_ervas")
|
||||
_CONFIG_CACHE["ia_module_params_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_module_params_ervas")
|
||||
_CONFIG_CACHE["ia_backbone"] = ContextoGlobalRedis.get_equipamento().get("ia_backbone_ervas")
|
||||
|
||||
_CONFIG_CACHE["faixa_atuacao_bicos"] = dadosAtu.get("percent_vertical_deteccao", 0.7)
|
||||
_CONFIG_CACHE["area_atuacao_bicos"] = dadosAtu.get("height_area_deteccao", 0.1)
|
||||
_CONFIG_CACHE["min_frac_erva_por_bico_on"] = dadosAtu.get("pct_erva_bico_on", 0.02)
|
||||
_CONFIG_CACHE["min_frac_erva_por_bico_off"] = dadosAtu.get("pct_erva_bico_off", 0.01)
|
||||
|
||||
# ============================================================
|
||||
# Compatibilidade MultiSpecSegformerService
|
||||
# ============================================================
|
||||
input_channels = _CONFIG_CACHE.get("ia_input_channels", ["R", "G", "B", "RE", "NIR"])
|
||||
if isinstance(input_channels, str):
|
||||
input_channels = [c.strip().upper() for c in input_channels.split(",") if c.strip()]
|
||||
else:
|
||||
input_channels = [str(c).upper() for c in input_channels]
|
||||
|
||||
_CONFIG_CACHE["input_channels"] = input_channels
|
||||
_CONFIG_CACHE["channels"] = int(_CONFIG_CACHE.get("ia_channels") or len(input_channels))
|
||||
|
||||
if _CONFIG_CACHE["channels"] != len(input_channels):
|
||||
mostrar_log(
|
||||
f"[WARN] ia_channels={_CONFIG_CACHE['channels']} diferente de "
|
||||
f"len(input_channels)={len(input_channels)}. Usando len(input_channels)."
|
||||
)
|
||||
_CONFIG_CACHE["channels"] = len(input_channels)
|
||||
|
||||
_CONFIG_CACHE["backbone"] = _CONFIG_CACHE.get("ia_backbone") or "nvidia/mit-b1"
|
||||
_CONFIG_CACHE["ckpt"] = _CONFIG_CACHE.get("ia_model_path")
|
||||
_CONFIG_CACHE["norm_stats_path"] = _CONFIG_CACHE.get("ia_norm_stats_path")
|
||||
_CONFIG_CACHE["module_calibration_json"] = (_CONFIG_CACHE.get("ia_module_params_path"))
|
||||
_CONFIG_CACHE["camera_fps"] = int(_CONFIG_CACHE.get("fps"))
|
||||
|
||||
return _CONFIG_CACHE
|
||||
|
||||
def reload_seg_config():
|
||||
return load_seg_config(force_reload=True)
|
||||
_CONFIG_CACHE = cfg
|
||||
return _CONFIG_CACHE
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,841 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
_10_export_visual_onnx.py
|
||||
|
||||
Exporta o checkpoint PyTorch do Visual Worker para ONNX.
|
||||
|
||||
Contrato esperado do modelo visual:
|
||||
- Backbone SegFormer Hugging Face, ex: nvidia/mit-b0
|
||||
- Entrada RGB: [N, 3, H, W], float32
|
||||
- Cabeça 1: segmentação semântica do corredor
|
||||
- Cabeça 2: classificação global/status do corredor
|
||||
|
||||
Suporta:
|
||||
- ONNX com logits crus de segmentação
|
||||
- ONNX com logits de segmentação redimensionados para HxW
|
||||
- ONNX com máscara argmax de segmentação
|
||||
- ONNX com label_logits ou label_probs
|
||||
- Normalização embutida no grafo: x = (x - mean) / std
|
||||
|
||||
Exemplos:
|
||||
|
||||
# ONNX recomendado para runtime: recebe RGB 0..1 e já normaliza internamente.
|
||||
python _10_export_visual_onnx.py ^
|
||||
--config config.json ^
|
||||
--checkpoint oak-d/backup/segformer_b0/nav_mit_dual_label/best_label.pt ^
|
||||
--out oak-d/backup/segformer_b0/nav_mit_dual_label/best_label.onnx ^
|
||||
--device cuda ^
|
||||
--include-norm ^
|
||||
--semantic-postprocess resize_logits ^
|
||||
--label-postprocess probs
|
||||
|
||||
# ONNX cru: runtime precisa enviar tensor já normalizado.
|
||||
python _10_export_visual_onnx.py ^
|
||||
--config config.json ^
|
||||
--device cuda ^
|
||||
--semantic-postprocess none ^
|
||||
--label-postprocess logits
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from transformers import SegformerForSemanticSegmentation
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 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 ensure_dir(path: str | Path):
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
Limpa nomes vindos de labelmap no estilo:
|
||||
naonavegavel:128,0,0::
|
||||
navegavel:0,128,0::
|
||||
|
||||
Mantém apenas o nome lógico da classe.
|
||||
"""
|
||||
name = str(raw_name).strip()
|
||||
|
||||
# Remove sufixos visuais comuns do labelmap: classe:R,G,B::
|
||||
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]:
|
||||
"""
|
||||
Parser tolerante para labelmap.txt.
|
||||
|
||||
Aceita formatos comuns:
|
||||
navegavel
|
||||
0:navegavel
|
||||
0 navegavel
|
||||
0,navegavel
|
||||
navegavel:0
|
||||
navegavel:0,128,0::
|
||||
|
||||
Retorna:
|
||||
id2label, label2id, ignore_index
|
||||
"""
|
||||
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
|
||||
|
||||
# Caso CVAT/labelmap visual: nome:R,G,B::
|
||||
# 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 (":", ",", " ", " "):
|
||||
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}")
|
||||
|
||||
# Garante ids contínuos para o SegFormer.
|
||||
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_default_paths(args, config: dict, config_dir: Path) -> Tuple[Path, Path, str, str, Path]:
|
||||
"""
|
||||
Resolve checkpoint e ONNX padrão seguindo o padrão do treino visual:
|
||||
{camera}/backup/{modelo}/{model_name}_{suffix}/{ckpt_name}.pt
|
||||
"""
|
||||
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"
|
||||
|
||||
out_path = resolve_path(args.out, Path.cwd())
|
||||
if out_path is None:
|
||||
out_path = save_dir / f"{ckpt_name}.onnx"
|
||||
|
||||
return checkpoint_path.resolve(), out_path.resolve(), ckpt_name, mode, save_dir.resolve()
|
||||
|
||||
|
||||
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_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()
|
||||
|
||||
# Prioridade 1: norm_stats salvo junto ao treino/checkpoint.
|
||||
p = save_dir / "norm_stats.json"
|
||||
if p.is_file():
|
||||
return p.resolve()
|
||||
|
||||
# Prioridade 2: dataset normalizado na resolução do contrato.
|
||||
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()
|
||||
|
||||
# Prioridade 3: deixa explícito no erro quando --include-norm for usado.
|
||||
return p.resolve()
|
||||
|
||||
|
||||
def load_rgb_norm_stats(path: Path) -> Tuple[List[float], List[float], List[str]]:
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"norm_stats não encontrado: {path}")
|
||||
|
||||
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:
|
||||
# Visual worker espera RGB. Aceita stats com canais extras, desde que R,G,B existam.
|
||||
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}. "
|
||||
f"channels={names} path={path}"
|
||||
)
|
||||
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"]
|
||||
|
||||
return mean_sel, std_sel, names_sel
|
||||
|
||||
|
||||
def resolve_label_classes(config: dict, ckpt: Optional[dict] = None) -> Tuple[Dict[int, str], int]:
|
||||
"""Resolve classes da cabeça de status/corredor."""
|
||||
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'] com a lista de status do corredor, "
|
||||
"ou use um checkpoint que tenha extra['label_name_by_id']."
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Cabeça auxiliar igual ao treino visual
|
||||
# ============================================================
|
||||
|
||||
|
||||
class LabelHead(nn.Module):
|
||||
"""Head de classificação global do frame/status do corredor."""
|
||||
|
||||
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:
|
||||
# Interpola sempre para evitar branch Python durante o trace ONNX.
|
||||
# Se o tamanho já for igual, o resultado é equivalente e o grafo fica estável.
|
||||
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
|
||||
|
||||
# Interpola sempre para evitar TracerWarning por comparação de shapes.
|
||||
# O contrato do export é resolução fixa, então isso não muda o comportamento útil.
|
||||
feat = F.interpolate(
|
||||
feat,
|
||||
size=logits.shape[-2:],
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
return feat
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Modelo composto Visual
|
||||
# ============================================================
|
||||
|
||||
|
||||
class VisualSegformerDualLabel(nn.Module):
|
||||
"""
|
||||
Modelo composto para export:
|
||||
- base_model: SegformerForSemanticSegmentation
|
||||
- label_head: LabelHead treinada junto
|
||||
|
||||
forward retorna dict:
|
||||
semantic: [N, Cseg, h, w]
|
||||
label : [N, Cstatus]
|
||||
"""
|
||||
|
||||
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_seg,
|
||||
"label": logits_label,
|
||||
}
|
||||
|
||||
|
||||
class VisualOnnxWrapper(nn.Module):
|
||||
"""
|
||||
Wrapper final exportável para ONNX.
|
||||
|
||||
semantic_postprocess:
|
||||
- none -> semantic_logits em baixa resolução do decode_head
|
||||
- resize_logits -> semantic_logits em HxW da entrada
|
||||
- argmax_lowres -> semantic_mask em baixa resolução
|
||||
- argmax_fullres-> semantic_mask em HxW da entrada
|
||||
|
||||
label_postprocess:
|
||||
- logits -> label_logits
|
||||
- probs -> label_probs via softmax
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: nn.Module,
|
||||
include_norm: bool = False,
|
||||
norm_mean: Optional[List[float]] = None,
|
||||
norm_std: Optional[List[float]] = None,
|
||||
semantic_postprocess: str = "none",
|
||||
label_postprocess: str = "logits",
|
||||
):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.include_norm = bool(include_norm)
|
||||
self.semantic_postprocess = str(semantic_postprocess).lower()
|
||||
self.label_postprocess = str(label_postprocess).lower()
|
||||
|
||||
if self.semantic_postprocess not in ("none", "resize_logits", "argmax_lowres", "argmax_fullres"):
|
||||
raise RuntimeError(f"semantic_postprocess inválido: {self.semantic_postprocess}")
|
||||
|
||||
if self.label_postprocess not in ("logits", "probs"):
|
||||
raise RuntimeError(f"label_postprocess inválido: {self.label_postprocess}")
|
||||
|
||||
if self.include_norm:
|
||||
if norm_mean is None or norm_std is None:
|
||||
raise RuntimeError("include_norm=True requer norm_mean/norm_std")
|
||||
mean_t = torch.tensor(norm_mean, dtype=torch.float32).view(1, 3, 1, 1)
|
||||
std_t = torch.tensor(norm_std, dtype=torch.float32).view(1, 3, 1, 1)
|
||||
self.register_buffer("norm_mean", mean_t)
|
||||
self.register_buffer("norm_std", std_t)
|
||||
else:
|
||||
self.register_buffer("norm_mean", torch.empty(0))
|
||||
self.register_buffer("norm_std", torch.empty(0))
|
||||
|
||||
def forward(self, pixel_values: torch.Tensor):
|
||||
input_hw = pixel_values.shape[-2:]
|
||||
|
||||
x = pixel_values
|
||||
if self.include_norm:
|
||||
x = (x - self.norm_mean) / torch.clamp(self.norm_std, min=1e-6)
|
||||
|
||||
outputs = self.model(pixel_values=x)
|
||||
semantic = outputs["semantic"]
|
||||
label = outputs["label"]
|
||||
|
||||
if self.semantic_postprocess == "none":
|
||||
semantic_out = semantic
|
||||
elif self.semantic_postprocess == "resize_logits":
|
||||
semantic_out = F.interpolate(
|
||||
semantic,
|
||||
size=input_hw,
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
elif self.semantic_postprocess == "argmax_lowres":
|
||||
semantic_out = torch.argmax(semantic, dim=1).to(torch.uint8)
|
||||
elif self.semantic_postprocess == "argmax_fullres":
|
||||
semantic = F.interpolate(
|
||||
semantic,
|
||||
size=input_hw,
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
semantic_out = torch.argmax(semantic, dim=1).to(torch.uint8)
|
||||
else:
|
||||
raise RuntimeError("semantic_postprocess inválido")
|
||||
|
||||
if self.label_postprocess == "probs":
|
||||
label_out = torch.softmax(label, dim=1)
|
||||
else:
|
||||
label_out = label
|
||||
|
||||
return semantic_out, label_out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Build/load
|
||||
# ============================================================
|
||||
|
||||
|
||||
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}. "
|
||||
"Confirme se foi salvo pelo script de treino visual."
|
||||
)
|
||||
|
||||
if "aux_head" not in ckpt:
|
||||
raise RuntimeError(
|
||||
f"Checkpoint não contém chave 'aux_head': {checkpoint_path}. "
|
||||
"Este exportador é para o modelo visual dual_head_label."
|
||||
)
|
||||
|
||||
model.base_model.load_state_dict(ckpt["model"], strict=True)
|
||||
model.label_head.load_state_dict(ckpt["aux_head"], strict=True)
|
||||
return ckpt
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("--config", default="config.json")
|
||||
parser.add_argument("--checkpoint", default="")
|
||||
parser.add_argument("--out", default="")
|
||||
parser.add_argument("--labelmap", default="")
|
||||
parser.add_argument("--norm_stats", default="")
|
||||
|
||||
parser.add_argument("--opset", type=int, default=17)
|
||||
parser.add_argument("--device", default="cuda", choices=["cuda", "cpu"])
|
||||
parser.add_argument("--dynamic-batch", action="store_true")
|
||||
|
||||
parser.add_argument(
|
||||
"--include-norm",
|
||||
action="store_true",
|
||||
help="Inclui normalização RGB x=(x-mean)/std dentro do ONNX. Runtime deve enviar RGB 0..1.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--semantic-postprocess",
|
||||
default="none",
|
||||
choices=["none", "resize_logits", "argmax_lowres", "argmax_fullres"],
|
||||
help="Define a saída semântica exportada.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--label-postprocess",
|
||||
default="logits",
|
||||
choices=["logits", "probs"],
|
||||
help="Define a saída da cabeça de status/corredor.",
|
||||
)
|
||||
|
||||
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, out_path, ckpt_name, mode, save_dir = resolve_default_paths(
|
||||
args=args,
|
||||
config=config,
|
||||
config_dir=config_dir,
|
||||
)
|
||||
|
||||
if mode != "label":
|
||||
raise RuntimeError(
|
||||
f"Este exportador foi preparado para dual_head_label. "
|
||||
f"Modo detectado no config: {mode}"
|
||||
)
|
||||
|
||||
if not checkpoint_path.is_file():
|
||||
raise FileNotFoundError(f"Checkpoint não encontrado: {checkpoint_path}")
|
||||
|
||||
ensure_dir(out_path.parent)
|
||||
|
||||
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)
|
||||
|
||||
# Lê ckpt antes para recuperar label_name_by_id, se necessário.
|
||||
ckpt_for_meta = torch.load(str(checkpoint_path), map_location="cpu", weights_only=False)
|
||||
label_name_by_id, num_label_classes = resolve_label_classes(config, ckpt_for_meta)
|
||||
|
||||
W, H = config.get("resolucao", [1024, 640])
|
||||
W = int(W)
|
||||
H = int(H)
|
||||
backbone = str(config.get("backbone", "nvidia/mit-b0"))
|
||||
|
||||
norm_stats_path = None
|
||||
norm_mean = None
|
||||
norm_std = None
|
||||
norm_channels = ["R", "G", "B"]
|
||||
|
||||
if args.include_norm:
|
||||
norm_stats_path = resolve_norm_stats_path(args, config, config_dir, save_dir)
|
||||
if norm_stats_path is None:
|
||||
raise RuntimeError("include_norm=True, mas norm_stats_path não foi resolvido.")
|
||||
norm_mean, norm_std, norm_channels = load_rgb_norm_stats(norm_stats_path)
|
||||
|
||||
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 não disponível. Exportando em CPU.")
|
||||
|
||||
print("==========================================")
|
||||
print("Export Visual Worker SegFormer Dual Label para ONNX")
|
||||
print(f"Config : {config_path}")
|
||||
print(f"Checkpoint : {checkpoint_path}")
|
||||
print(f"Output ONNX : {out_path}")
|
||||
print(f"Save dir : {save_dir}")
|
||||
print(f"Backbone : {backbone}")
|
||||
print(f"Resolution : {W}x{H}")
|
||||
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"Include norm : {args.include_norm}")
|
||||
print(f"Norm stats : {norm_stats_path}")
|
||||
print(f"Semantic output : {args.semantic_postprocess}")
|
||||
print(f"Label output : {args.label_postprocess}")
|
||||
print(f"Dynamic batch : {args.dynamic_batch}")
|
||||
print(f"Opset : {args.opset}")
|
||||
print(f"Device : {device}")
|
||||
print("==========================================")
|
||||
|
||||
print("[MODEL] Montando modelo visual...")
|
||||
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()
|
||||
|
||||
wrapper = VisualOnnxWrapper(
|
||||
model=model,
|
||||
include_norm=args.include_norm,
|
||||
norm_mean=norm_mean,
|
||||
norm_std=norm_std,
|
||||
semantic_postprocess=args.semantic_postprocess,
|
||||
label_postprocess=args.label_postprocess,
|
||||
).to(device)
|
||||
wrapper.eval()
|
||||
|
||||
dummy_input = torch.randn(
|
||||
1,
|
||||
3,
|
||||
H,
|
||||
W,
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
|
||||
input_names = ["pixel_values"]
|
||||
|
||||
if args.semantic_postprocess in ("argmax_lowres", "argmax_fullres"):
|
||||
semantic_output_name = "semantic_mask"
|
||||
else:
|
||||
semantic_output_name = "semantic_logits"
|
||||
|
||||
label_output_name = "label_probs" if args.label_postprocess == "probs" else "label_logits"
|
||||
output_names = [semantic_output_name, label_output_name]
|
||||
|
||||
dynamic_axes = None
|
||||
if args.dynamic_batch:
|
||||
dynamic_axes = {
|
||||
"pixel_values": {0: "batch"},
|
||||
semantic_output_name: {0: "batch"},
|
||||
label_output_name: {0: "batch"},
|
||||
}
|
||||
|
||||
print("[CHECK] Rodando forward PyTorch antes do export...")
|
||||
with torch.no_grad():
|
||||
y = wrapper(dummy_input)
|
||||
|
||||
for name, tensor in zip(output_names, y):
|
||||
print(f" {name}: shape={tuple(tensor.shape)} dtype={tensor.dtype}")
|
||||
|
||||
print("[EXPORT] Exportando ONNX...")
|
||||
with torch.no_grad():
|
||||
torch.onnx.export(
|
||||
wrapper,
|
||||
dummy_input,
|
||||
str(out_path),
|
||||
export_params=True,
|
||||
opset_version=int(args.opset),
|
||||
do_constant_folding=True,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
dynamic_axes=dynamic_axes,
|
||||
)
|
||||
|
||||
print(f"[OK] ONNX salvo em: {out_path}")
|
||||
|
||||
meta_path = out_path.with_suffix(".export_meta.json")
|
||||
save_json(meta_path, {
|
||||
"kind": "visual_worker_segformer_dual_label",
|
||||
"config": str(config_path),
|
||||
"checkpoint": str(checkpoint_path),
|
||||
"onnx": str(out_path),
|
||||
"ckpt_name": ckpt_name,
|
||||
"mode": mode,
|
||||
"backbone": backbone,
|
||||
"input_shape": [1, 3, H, W],
|
||||
"input_channel_names": ["R", "G", "B"],
|
||||
"input_channel_indices": [0, 1, 2],
|
||||
"semantic_output_name": semantic_output_name,
|
||||
"label_output_name": label_output_name,
|
||||
"output_names": output_names,
|
||||
"semantic_postprocess": str(args.semantic_postprocess),
|
||||
"label_postprocess": str(args.label_postprocess),
|
||||
"opset": int(args.opset),
|
||||
"dynamic_batch": bool(args.dynamic_batch),
|
||||
"include_norm": bool(args.include_norm),
|
||||
"norm_stats_path": str(norm_stats_path) if norm_stats_path is not None else None,
|
||||
"norm_channels": norm_channels,
|
||||
"norm_mean": norm_mean,
|
||||
"norm_std": norm_std,
|
||||
"semantic_id2label": semantic_id2label,
|
||||
"semantic_label2id": semantic_label2id,
|
||||
"ignore_index": int(ignore_index),
|
||||
"label_name_by_id": label_name_by_id,
|
||||
"num_label_classes": int(num_label_classes),
|
||||
"checkpoint_epoch": ckpt.get("epoch", None),
|
||||
"checkpoint_bests": ckpt.get("bests", None),
|
||||
"checkpoint_extra": ckpt.get("extra", None),
|
||||
})
|
||||
print(f"[OK] Metadata salvo em: {meta_path}")
|
||||
|
||||
try:
|
||||
import onnx
|
||||
print("[ONNX] Verificando grafo com onnx.checker...")
|
||||
onnx_model = onnx.load(str(out_path))
|
||||
onnx.checker.check_model(onnx_model)
|
||||
print("[OK] onnx.checker passou.")
|
||||
except ImportError:
|
||||
print("[WARN] Pacote onnx não instalado. Pulei onnx.checker.")
|
||||
print(" Instale com: pip install onnx")
|
||||
except Exception as e:
|
||||
print(f"[WARN] onnx.checker encontrou problema: {e}")
|
||||
|
||||
print("\nExport finalizado.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -49,10 +49,14 @@ from transformers import SegformerForSemanticSegmentation
|
|||
# Utils básicos
|
||||
# ============================================================
|
||||
|
||||
IMAGENET_MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
|
||||
IMAGENET_STD = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
|
||||
# Normalização padrão. Pode ser sobrescrita por norm_stats.json.
|
||||
NORM_MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
|
||||
NORM_STD = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
|
||||
|
||||
def normalize_img(img: torch.Tensor) -> torch.Tensor:
|
||||
return (img - IMAGENET_MEAN.to(img.device)) / IMAGENET_STD.to(img.device)
|
||||
mean = NORM_MEAN.to(img.device)
|
||||
std = NORM_STD.to(img.device).clamp_min(1e-6)
|
||||
return (img - mean) / std
|
||||
|
||||
|
||||
def find_device():
|
||||
|
|
@ -477,6 +481,229 @@ def infer_sample(base_model, aux_head, img_bgr: np.ndarray, device: torch.device
|
|||
return result
|
||||
|
||||
|
||||
def add_runtime_dll_dirs(trt_home: Optional[str] = None):
|
||||
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")
|
||||
dll_dirs.append(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.3\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}")
|
||||
|
||||
|
||||
def create_onnx_session(onnx_path: str, provider: str = "tensorrt", trt_home: Optional[str] = None):
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"onnxruntime não está instalado. Instale com:\n"
|
||||
" pip install onnxruntime-gpu\n"
|
||||
"ou CPU:\n"
|
||||
" pip install onnxruntime"
|
||||
)
|
||||
|
||||
provider = provider.lower()
|
||||
|
||||
if provider == "tensorrt":
|
||||
add_runtime_dll_dirs(trt_home)
|
||||
|
||||
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 = os.path.join(os.path.dirname(onnx_path), "trt_cache")
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
trt_options = {
|
||||
"device_id": 0,
|
||||
"trt_fp16_enable": True,
|
||||
"trt_engine_cache_enable": True,
|
||||
"trt_engine_cache_path": cache_dir,
|
||||
"trt_timing_cache_enable": True,
|
||||
"trt_timing_cache_path": cache_dir,
|
||||
"trt_max_workspace_size": 4 * 1024 * 1024 * 1024,
|
||||
}
|
||||
|
||||
providers = [
|
||||
("TensorrtExecutionProvider", trt_options),
|
||||
"CUDAExecutionProvider",
|
||||
"CPUExecutionProvider",
|
||||
]
|
||||
else:
|
||||
raise RuntimeError(f"Provider desconhecido: {provider}")
|
||||
|
||||
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. disponível={available}")
|
||||
|
||||
sess = ort.InferenceSession(
|
||||
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(f"TensorRT solicitado, mas não ficou ativo. Providers ativos: {active}")
|
||||
|
||||
if provider == "cuda" and "CUDAExecutionProvider" not in active:
|
||||
raise RuntimeError(f"CUDA solicitado, mas não ficou ativo. Providers ativos: {active}")
|
||||
|
||||
return sess
|
||||
|
||||
|
||||
def preprocess_img_onnx(img_bgr: np.ndarray, resolucao, onnx_has_norm: bool):
|
||||
"""
|
||||
Retorna input NCHW float32.
|
||||
|
||||
Se ONNX tem normalização embutida:
|
||||
envia RGB 0..1
|
||||
|
||||
Se ONNX NÃO tem normalização embutida:
|
||||
envia RGB já normalizado com NORM_MEAN/NORM_STD
|
||||
"""
|
||||
w, h = int(resolucao[0]), int(resolucao[1])
|
||||
|
||||
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
||||
img_res = cv2.resize(img_rgb, (w, h), interpolation=cv2.INTER_AREA)
|
||||
|
||||
x = img_res.astype(np.float32) / 255.0
|
||||
x = np.transpose(x, (2, 0, 1)) # CHW
|
||||
|
||||
if not onnx_has_norm:
|
||||
mean = NORM_MEAN.detach().cpu().numpy().astype(np.float32)
|
||||
std = NORM_STD.detach().cpu().numpy().astype(np.float32)
|
||||
std = np.clip(std, 1e-6, None)
|
||||
x = (x - mean) / std
|
||||
|
||||
return np.expand_dims(x, axis=0).astype(np.float32)
|
||||
|
||||
|
||||
def get_onnx_outputs(outputs_dict: Dict[str, np.ndarray]):
|
||||
keys = list(outputs_dict.keys())
|
||||
|
||||
semantic_name = None
|
||||
label_name = None
|
||||
|
||||
for k in ["semantic_logits", "semantic_mask", "semantic", "output_semantic"]:
|
||||
if k in outputs_dict:
|
||||
semantic_name = k
|
||||
break
|
||||
|
||||
for k in ["label_probs", "label_logits", "label", "output_label"]:
|
||||
if k in outputs_dict:
|
||||
label_name = k
|
||||
break
|
||||
|
||||
if semantic_name is None or label_name is None:
|
||||
if len(keys) != 2:
|
||||
raise RuntimeError(f"Outputs ONNX inesperados: {keys}")
|
||||
semantic_name = semantic_name or keys[0]
|
||||
label_name = label_name or keys[1]
|
||||
|
||||
return outputs_dict[semantic_name], outputs_dict[label_name], semantic_name, label_name
|
||||
|
||||
|
||||
def infer_sample_onnx(
|
||||
onnx_session,
|
||||
img_bgr: np.ndarray,
|
||||
mode: str,
|
||||
resolucao,
|
||||
onnx_has_norm: bool,
|
||||
mask2_thr: float = 0.5,
|
||||
):
|
||||
h0, w0 = img_bgr.shape[:2]
|
||||
|
||||
x = preprocess_img_onnx(
|
||||
img_bgr=img_bgr,
|
||||
resolucao=resolucao,
|
||||
onnx_has_norm=onnx_has_norm,
|
||||
)
|
||||
|
||||
input_name = onnx_session.get_inputs()[0].name
|
||||
output_names = [o.name for o in onnx_session.get_outputs()]
|
||||
raw_outputs = onnx_session.run(None, {input_name: x})
|
||||
|
||||
outputs_dict = {
|
||||
name: arr.astype(np.float32)
|
||||
for name, arr in zip(output_names, raw_outputs)
|
||||
}
|
||||
|
||||
semantic_out, label_out, semantic_name, label_name = get_onnx_outputs(outputs_dict)
|
||||
|
||||
result = {
|
||||
"pred_seg": None,
|
||||
"mask2_prob": None,
|
||||
"mask2_bin": None,
|
||||
"label_id": None,
|
||||
"label_conf": None,
|
||||
"label_probs": None,
|
||||
}
|
||||
|
||||
# semantic_logits: [1,C,H,W]
|
||||
# semantic_mask : [1,H,W]
|
||||
if semantic_out.ndim == 4:
|
||||
pred_ids = np.argmax(semantic_out, axis=1)[0].astype(np.uint8)
|
||||
elif semantic_out.ndim == 3:
|
||||
pred_ids = semantic_out[0].astype(np.uint8)
|
||||
else:
|
||||
raise RuntimeError(f"Saída semântica ONNX inválida: {semantic_name} shape={semantic_out.shape}")
|
||||
|
||||
pred_ids_full = cv2.resize(pred_ids, (w0, h0), interpolation=cv2.INTER_NEAREST)
|
||||
result["pred_seg"] = pred_ids_full
|
||||
|
||||
# Neste modelo atual não estamos usando mask2 no ONNX.
|
||||
if mode == "mask2":
|
||||
result["mask2_prob"] = None
|
||||
result["mask2_bin"] = None
|
||||
return result
|
||||
|
||||
if mode == "label":
|
||||
if label_name == "label_probs":
|
||||
probs = label_out[0].astype(np.float32)
|
||||
else:
|
||||
logits = label_out.astype(np.float32)
|
||||
logits = logits - np.max(logits, axis=1, keepdims=True)
|
||||
e = np.exp(logits)
|
||||
probs = (e / np.clip(np.sum(e, axis=1, keepdims=True), 1e-12, None))[0]
|
||||
|
||||
lid = int(np.argmax(probs))
|
||||
result["label_id"] = lid
|
||||
result["label_conf"] = float(probs[lid])
|
||||
result["label_probs"] = probs
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Painéis
|
||||
# ============================================================
|
||||
|
|
@ -636,6 +863,10 @@ def main():
|
|||
ap.add_argument("--camera", action="store_true", help="Inferir em tempo real pela OAK-D Lite")
|
||||
ap.add_argument("--camera_fps", type=int, default=20)
|
||||
ap.add_argument("--camera_res", type=str, default="720p", choices=["720p", "1080p"])
|
||||
ap.add_argument("--onnx", type=str, default=None, help="Se informado, usa ONNX Runtime em vez de PyTorch.")
|
||||
ap.add_argument("--onnx_provider", type=str, default="tensorrt", choices=["cuda", "cpu", "tensorrt"])
|
||||
ap.add_argument("--onnx_has_norm", action="store_true", help="Use se o ONNX foi exportado com --include-norm.")
|
||||
ap.add_argument("--trt_home", type=str, default=r"C:\dev\TensorRT-10.10.0.31")
|
||||
args = ap.parse_args()
|
||||
|
||||
device = find_device()
|
||||
|
|
@ -690,26 +921,36 @@ def main():
|
|||
raise SystemExit(f"Checkpoint não encontrado: {ckpt_path}")
|
||||
print(f"[model] ckpt={ckpt_path}")
|
||||
|
||||
base_model = build_base_model(num_classes=num_classes, device=device, backbone=BACKBONE)
|
||||
use_onnx = args.onnx is not None and str(args.onnx).strip() != ""
|
||||
onnx_session = None
|
||||
|
||||
base_model = None
|
||||
aux_head = None
|
||||
label_names = {}
|
||||
feat_ch = get_feat_ch(base_model, device, input_h=args.input_size, input_w=args.input_size)
|
||||
|
||||
# Carrega ckpt antes para descobrir metadados se necessário
|
||||
ckpt_pre = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
||||
extra = ckpt_pre.get("extra", {}) or {}
|
||||
if use_onnx:
|
||||
if not os.path.isfile(args.onnx):
|
||||
raise SystemExit(f"ONNX não encontrado: {args.onnx}")
|
||||
|
||||
if mode == "mask2":
|
||||
aux_head = Mask2Head(feat_ch=feat_ch, num_classes=num_classes, hidden=256, dropout=0.1)
|
||||
elif mode == "label":
|
||||
print(extra)
|
||||
label_names_raw = extra.get("label_name_by_id", {}) or {}
|
||||
# JSON pode salvar keys como string
|
||||
label_names = {int(k): str(v) for k, v in label_names_raw.items()} if label_names_raw else {}
|
||||
print(label_names_raw)
|
||||
print(f"[runtime] usando ONNX: {args.onnx}")
|
||||
print(f"[runtime] provider: {args.onnx_provider}")
|
||||
print(f"[runtime] onnx_has_norm: {args.onnx_has_norm}")
|
||||
|
||||
# Se não tiver no ckpt, tenta inferir do dataset de labels
|
||||
onnx_session = create_onnx_session(
|
||||
onnx_path=args.onnx,
|
||||
provider=args.onnx_provider,
|
||||
trt_home=args.trt_home,
|
||||
)
|
||||
|
||||
# Ainda precisamos dos nomes dos labels para HUD.
|
||||
# Primeiro tenta buscar no checkpoint, se existir.
|
||||
if os.path.isfile(ckpt_path):
|
||||
ckpt_pre = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
||||
extra = ckpt_pre.get("extra", {}) or {}
|
||||
label_names_raw = extra.get("label_name_by_id", {}) or {}
|
||||
label_names = {int(k): str(v) for k, v in label_names_raw.items()} if label_names_raw else {}
|
||||
|
||||
# Fallback: tenta inferir dos labels do dataset.
|
||||
max_label_id = -1
|
||||
for s in samples:
|
||||
lid, lname = read_gt_label(s)
|
||||
|
|
@ -717,23 +958,94 @@ def main():
|
|||
max_label_id = max(max_label_id, int(lid))
|
||||
if lname:
|
||||
label_names[int(lid)] = str(lname)
|
||||
if max_label_id < 0:
|
||||
# último fallback: pelo shape da última camada do checkpoint
|
||||
|
||||
if mode == "label":
|
||||
if max_label_id < 0 and label_names:
|
||||
max_label_id = max(label_names.keys())
|
||||
|
||||
for i in range(max_label_id + 1):
|
||||
label_names.setdefault(i, f"label_{i}")
|
||||
|
||||
# Fallback final para seu caso conhecido.
|
||||
if not label_names:
|
||||
label_names = {
|
||||
0: "Parado",
|
||||
1: "EntrandoRua",
|
||||
2: "CaminhandoRua",
|
||||
3: "SaindoRua",
|
||||
4: "Manobrando",
|
||||
5: "Direcionando",
|
||||
6: "RetornandoBase",
|
||||
7: "Indefinido",
|
||||
}
|
||||
|
||||
print(f"[label_names] {label_names}")
|
||||
|
||||
else:
|
||||
print(f"[runtime] usando PyTorch: {ckpt_path}")
|
||||
|
||||
base_model = build_base_model(num_classes=num_classes, device=device, backbone=BACKBONE)
|
||||
|
||||
aux_head = None
|
||||
label_names = {}
|
||||
feat_ch = get_feat_ch(base_model, device, input_h=args.input_size, input_w=args.input_size)
|
||||
|
||||
ckpt_pre = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
||||
extra = ckpt_pre.get("extra", {}) or {}
|
||||
|
||||
if mode == "mask2":
|
||||
aux_head = Mask2Head(feat_ch=feat_ch, num_classes=num_classes, hidden=256, dropout=0.1)
|
||||
|
||||
elif mode == "label":
|
||||
label_names_raw = extra.get("label_name_by_id", {}) or {}
|
||||
label_names = {int(k): str(v) for k, v in label_names_raw.items()} if label_names_raw else {}
|
||||
|
||||
max_label_id = -1
|
||||
for s in samples:
|
||||
lid, lname = read_gt_label(s)
|
||||
if lid is not None:
|
||||
max_label_id = max(max_label_id, int(lid))
|
||||
if lname:
|
||||
label_names[int(lid)] = str(lname)
|
||||
|
||||
# Fonte mais confiável: shape da última camada salva no checkpoint.
|
||||
ckpt_num_label_classes = None
|
||||
sd = ckpt_pre.get("aux_head", {})
|
||||
|
||||
for k, v in sd.items():
|
||||
if k.endswith("net.3.weight") or k.endswith("net.3.bias"):
|
||||
max_label_id = int(v.shape[0]) - 1
|
||||
ckpt_num_label_classes = int(v.shape[0])
|
||||
break
|
||||
num_label_classes = max_label_id + 1
|
||||
if num_label_classes <= 0:
|
||||
raise RuntimeError("Não consegui inferir num_label_classes para LabelHead.")
|
||||
for i in range(num_label_classes):
|
||||
label_names.setdefault(i, f"label_{i}")
|
||||
aux_head = LabelHead(feat_ch=feat_ch, num_seg_classes=num_classes, num_label_classes=num_label_classes, hidden=256, dropout=0.2)
|
||||
print(f"[label_head] classes={num_label_classes} names={label_names}")
|
||||
|
||||
ckpt = load_checkpoint(ckpt_path, base_model, aux_head, device)
|
||||
print(f"[ckpt] epoch={ckpt.get('epoch')} bests={ckpt.get('bests')}")
|
||||
if ckpt_num_label_classes is not None and ckpt_num_label_classes > 0:
|
||||
num_label_classes = ckpt_num_label_classes
|
||||
else:
|
||||
# Fallback: labels vistos no dataset atual.
|
||||
num_label_classes = max_label_id + 1
|
||||
|
||||
if num_label_classes <= 0:
|
||||
raise RuntimeError("Não consegui inferir num_label_classes para LabelHead.")
|
||||
|
||||
for i in range(num_label_classes):
|
||||
label_names.setdefault(i, f"label_{i}")
|
||||
if num_label_classes <= 0:
|
||||
raise RuntimeError("Não consegui inferir num_label_classes para LabelHead.")
|
||||
|
||||
for i in range(num_label_classes):
|
||||
label_names.setdefault(i, f"label_{i}")
|
||||
|
||||
aux_head = LabelHead(
|
||||
feat_ch=feat_ch,
|
||||
num_seg_classes=num_classes,
|
||||
num_label_classes=num_label_classes,
|
||||
hidden=256,
|
||||
dropout=0.2,
|
||||
)
|
||||
|
||||
print(f"[label_head] classes={num_label_classes} names={label_names}")
|
||||
|
||||
ckpt = load_checkpoint(ckpt_path, base_model, aux_head, device)
|
||||
print(f"[ckpt] epoch={ckpt.get('epoch')} bests={ckpt.get('bests')}")
|
||||
|
||||
if args.save_dir:
|
||||
os.makedirs(args.save_dir, exist_ok=True)
|
||||
|
|
@ -809,15 +1121,25 @@ def main():
|
|||
roi_bgr = frame_bgr[y0:y1, 0:W]
|
||||
roi_input = resize_to_config(roi_bgr, RESOLUCAO)
|
||||
|
||||
res = infer_sample(
|
||||
base_model=base_model,
|
||||
aux_head=aux_head,
|
||||
img_bgr=roi_input,
|
||||
device=device,
|
||||
mode=mode,
|
||||
input_size=args.input_size,
|
||||
mask2_thr=args.mask2_thr,
|
||||
)
|
||||
if use_onnx:
|
||||
res = infer_sample_onnx(
|
||||
onnx_session=onnx_session,
|
||||
img_bgr=roi_input,
|
||||
mode=mode,
|
||||
resolucao=RESOLUCAO,
|
||||
onnx_has_norm=args.onnx_has_norm,
|
||||
mask2_thr=args.mask2_thr,
|
||||
)
|
||||
else:
|
||||
res = infer_sample(
|
||||
base_model=base_model,
|
||||
aux_head=aux_head,
|
||||
img_bgr=roi_input,
|
||||
device=device,
|
||||
mode=mode,
|
||||
input_size=args.input_size,
|
||||
mask2_thr=args.mask2_thr,
|
||||
)
|
||||
|
||||
pred_seg = res["pred_seg"]
|
||||
pred_col = colorize_seg(pred_seg, lut_bgr)
|
||||
|
|
@ -896,15 +1218,25 @@ def main():
|
|||
gt2 = imread_gray(s.mask2_path) if s.mask2_path else None
|
||||
gt_label_id, gt_label_name = read_gt_label(s)
|
||||
|
||||
res = infer_sample(
|
||||
base_model=base_model,
|
||||
aux_head=aux_head,
|
||||
img_bgr=img,
|
||||
device=device,
|
||||
mode=mode,
|
||||
input_size=args.input_size,
|
||||
mask2_thr=args.mask2_thr,
|
||||
)
|
||||
if use_onnx:
|
||||
res = infer_sample_onnx(
|
||||
onnx_session=onnx_session,
|
||||
img_bgr=img,
|
||||
mode=mode,
|
||||
resolucao=RESOLUCAO,
|
||||
onnx_has_norm=args.onnx_has_norm,
|
||||
mask2_thr=args.mask2_thr,
|
||||
)
|
||||
else:
|
||||
res = infer_sample(
|
||||
base_model=base_model,
|
||||
aux_head=aux_head,
|
||||
img_bgr=img,
|
||||
device=device,
|
||||
mode=mode,
|
||||
input_size=args.input_size,
|
||||
mask2_thr=args.mask2_thr,
|
||||
)
|
||||
|
||||
pred1 = res["pred_seg"]
|
||||
segm = seg_metrics(pred1, gt1, num_classes=num_classes, ignore_id=ignore_id)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"main_class_name": "navegavel",
|
||||
"es_classes": "",
|
||||
"model_to_use": "geral",
|
||||
"ckpt_test": "best_main",
|
||||
"raw_size": [1296, 1028],
|
||||
"resolucao": [1024, 576],
|
||||
"roi_inicio": 0.0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,508 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
_10_export_onnx.py
|
||||
|
||||
Exporta o checkpoint PyTorch do SegFormer Multi-Head OAK-FCC-3 para ONNX.
|
||||
|
||||
Exemplo:
|
||||
|
||||
python _10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/target_teached/stacked_raw5/best_score.pt --out backup/segformer_b1/target_teached/stacked_raw5/best_score.onnx --train-script _8_train_multihead.py --opset 17 --device cuda
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Utils
|
||||
# ============================================================
|
||||
|
||||
def load_json(path: str | Path) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def ensure_dir(path: Path):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def import_train_module(train_script_path: str | Path):
|
||||
"""
|
||||
Importa o script de treinamento como módulo, para reaproveitar:
|
||||
- load_labelmap
|
||||
- build_heads_config
|
||||
- get_input_channel_names
|
||||
- get_input_channel_indices
|
||||
- build_model
|
||||
- load_checkpoint
|
||||
"""
|
||||
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 save_export_metadata(path: Path, data: dict):
|
||||
ensure_dir(path.parent)
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def resolve_norm_stats_path(config: dict, config_path: Path) -> Path | None:
|
||||
fusion_mode = config.get("fusion_mode", "stacked")
|
||||
model = config.get("modelo")
|
||||
model_name = config.get("model_name")
|
||||
ch = config.get("channels")
|
||||
|
||||
normstats_path = Path(f"backup/{model}/{model_name}/{fusion_mode}_raw{ch}/norm_stats.json")
|
||||
if normstats_path.exists():
|
||||
return normstats_path
|
||||
|
||||
ia_resolution = config.get("resolucao", [1024, 640])
|
||||
w, h = int(ia_resolution[0]), int(ia_resolution[1])
|
||||
p = Path("dataset") / f"{w}x{h}" / "group" / "norm_stats.json"
|
||||
if not p.is_absolute():
|
||||
p = (config_path.parent / p).resolve()
|
||||
return p
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def load_selected_norm_stats(config: dict, config_path: Path, input_channel_indices: List[int]):
|
||||
path = resolve_norm_stats_path(config, config_path)
|
||||
|
||||
if path is None or not path.is_file():
|
||||
raise FileNotFoundError(f"norm_stats não encontrado: {path}")
|
||||
|
||||
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(input_channel_indices)
|
||||
|
||||
if len(mean) <= max_idx or len(std) <= max_idx:
|
||||
raise RuntimeError(
|
||||
f"norm_stats incompatível: precisa índices={input_channel_indices}, "
|
||||
f"mean={len(mean)} std={len(std)} path={path}"
|
||||
)
|
||||
|
||||
mean_sel = [float(mean[i]) for i in input_channel_indices]
|
||||
std_sel = [float(std[i]) for i in input_channel_indices]
|
||||
|
||||
if names:
|
||||
names_sel = [names[i] for i in input_channel_indices]
|
||||
else:
|
||||
names_sel = []
|
||||
|
||||
return path, mean_sel, std_sel, names_sel
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Wrapper ONNX
|
||||
# ============================================================
|
||||
|
||||
class MultiHeadOnnxWrapper(nn.Module):
|
||||
"""
|
||||
Wrapper ONNX para exportar o SegFormer Multi-Head.
|
||||
|
||||
Pode exportar:
|
||||
- logits crus
|
||||
- logits redimensionados
|
||||
- argmax em baixa resolução
|
||||
- argmax em resolução da entrada
|
||||
|
||||
Também pode embutir a normalização:
|
||||
x = (x - mean) / std
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: nn.Module,
|
||||
output_heads: List[str],
|
||||
resize_to_input: bool = False,
|
||||
include_norm: bool = False,
|
||||
norm_mean: List[float] | None = None,
|
||||
norm_std: List[float] | None = None,
|
||||
postprocess: str = "none",
|
||||
):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.output_heads = list(output_heads)
|
||||
self.resize_to_input = bool(resize_to_input)
|
||||
self.include_norm = bool(include_norm)
|
||||
self.postprocess = str(postprocess).lower()
|
||||
|
||||
if self.postprocess not in ("none", "resize_logits", "argmax_lowres", "argmax_fullres"):
|
||||
raise RuntimeError(f"postprocess inválido: {self.postprocess}")
|
||||
|
||||
if self.postprocess == "resize_logits":
|
||||
self.resize_to_input = True
|
||||
|
||||
if self.include_norm:
|
||||
if norm_mean is None or norm_std is None:
|
||||
raise RuntimeError("include_norm=True requer norm_mean e norm_std")
|
||||
|
||||
mean_t = torch.tensor(norm_mean, dtype=torch.float32).view(1, len(norm_mean), 1, 1)
|
||||
std_t = torch.tensor(norm_std, dtype=torch.float32).view(1, len(norm_std), 1, 1)
|
||||
|
||||
self.register_buffer("norm_mean", mean_t)
|
||||
self.register_buffer("norm_std", std_t)
|
||||
else:
|
||||
self.register_buffer("norm_mean", torch.empty(0))
|
||||
self.register_buffer("norm_std", torch.empty(0))
|
||||
|
||||
def forward(self, pixel_values: torch.Tensor):
|
||||
input_hw = pixel_values.shape[-2:]
|
||||
|
||||
x = pixel_values
|
||||
|
||||
if self.include_norm:
|
||||
x = (x - self.norm_mean) / torch.clamp(self.norm_std, min=1e-6)
|
||||
|
||||
outputs: Dict[str, torch.Tensor] = self.model(pixel_values=x)
|
||||
|
||||
result = []
|
||||
|
||||
for head_name in self.output_heads:
|
||||
if head_name not in outputs:
|
||||
raise RuntimeError(f"Head ausente no modelo: {head_name}")
|
||||
|
||||
logits = outputs[head_name]
|
||||
|
||||
if self.postprocess in ("none", "resize_logits"):
|
||||
if self.resize_to_input:
|
||||
logits = torch.nn.functional.interpolate(
|
||||
logits,
|
||||
size=input_hw,
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
|
||||
result.append(logits)
|
||||
|
||||
elif self.postprocess == "argmax_lowres":
|
||||
mask = torch.argmax(logits, dim=1).to(torch.uint8)
|
||||
result.append(mask)
|
||||
|
||||
elif self.postprocess == "argmax_fullres":
|
||||
logits = torch.nn.functional.interpolate(
|
||||
logits,
|
||||
size=input_hw,
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
mask = torch.argmax(logits, dim=1).to(torch.uint8)
|
||||
result.append(mask)
|
||||
|
||||
return tuple(result)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("--config", default="config.json")
|
||||
parser.add_argument("--checkpoint", default="")
|
||||
parser.add_argument("--out", default="")
|
||||
|
||||
parser.add_argument(
|
||||
"--train-script",
|
||||
default="_8_train_multihead.py",
|
||||
help="Script de treino usado para montar exatamente o mesmo modelo.",
|
||||
)
|
||||
|
||||
parser.add_argument("--labelmap", default="dataset/labelmap.txt")
|
||||
parser.add_argument("--opset", type=int, default=17)
|
||||
parser.add_argument("--device", default="cuda", choices=["cuda", "cpu"])
|
||||
|
||||
parser.add_argument(
|
||||
"--resize-to-input",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Se ativo, exporta cada head já redimensionada para HxW da entrada. "
|
||||
"Se desativo, exporta logits brutos do decode_head."
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--include-norm",
|
||||
action="store_true",
|
||||
help="Inclui normalização x=(x-mean)/std dentro do grafo ONNX.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--postprocess",
|
||||
default="none",
|
||||
choices=["none", "resize_logits", "argmax_lowres", "argmax_fullres"],
|
||||
help=(
|
||||
"Define o pós-processamento exportado no ONNX. "
|
||||
"none=logits crus; resize_logits=logits em HxW; "
|
||||
"argmax_lowres=máscara na resolução do decode_head; "
|
||||
"argmax_fullres=máscara HxW pronta."
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dynamic-batch",
|
||||
action="store_true",
|
||||
help="Permite batch dinâmico no ONNX. H e W continuam fixos.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
config_path = Path(args.config)
|
||||
checkpoint_path = Path(args.checkpoint)
|
||||
out_path = Path(args.out)
|
||||
labelmap_path = Path(args.labelmap)
|
||||
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"Config não encontrado: {config_path}")
|
||||
|
||||
if args.checkpoint != "" and not checkpoint_path.exists():
|
||||
raise FileNotFoundError(f"Checkpoint não encontrado: {checkpoint_path}")
|
||||
|
||||
if not labelmap_path.exists():
|
||||
raise FileNotFoundError(f"Labelmap não encontrado: {labelmap_path}")
|
||||
|
||||
if args.out != "":
|
||||
ensure_dir(out_path.parent)
|
||||
|
||||
train_mod = import_train_module(args.train_script)
|
||||
|
||||
config = load_json(config_path)
|
||||
|
||||
W, H = config["resolucao"]
|
||||
backbone = config.get("backbone", "nvidia/mit-b1")
|
||||
fusion_mode = config.get("fusion_mode", "stacked")
|
||||
model = config.get("modelo")
|
||||
model_name = config.get("model_name")
|
||||
ch = config.get("channels")
|
||||
ckpt_name = config.get("ckpt_test", "best_score")
|
||||
|
||||
if args.checkpoint == "":
|
||||
checkpoint_path = Path(f"backup/{model}/{model_name}/{fusion_mode}_raw{ch}/{ckpt_name}.pt")
|
||||
if args.out == "":
|
||||
out_path = Path(f"backup/{model}/{model_name}/{fusion_mode}_raw{ch}/{ckpt_name}.onnx")
|
||||
|
||||
if fusion_mode != "stacked":
|
||||
raise RuntimeError("Este exportador foi pensado para fusion_mode='stacked'.")
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
# Mesmo ajuste feito no treino:
|
||||
# semantic usa o número real do 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())
|
||||
|
||||
print("==========================================")
|
||||
print("Export SegFormer Multi-Head para ONNX")
|
||||
print(f"Config : {config_path}")
|
||||
print(f"Checkpoint : {checkpoint_path}")
|
||||
print(f"Output ONNX : {out_path}")
|
||||
print(f"Backbone : {backbone}")
|
||||
print(f"Resolution : {W}x{H}")
|
||||
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"Resize output: {args.resize_to_input}")
|
||||
print(f"Include norm : {args.include_norm}")
|
||||
print(f"Postprocess : {args.postprocess}")
|
||||
print("==========================================")
|
||||
|
||||
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 não disponível. Exportando em CPU.")
|
||||
|
||||
model = train_mod.build_model(
|
||||
backbone=backbone,
|
||||
channels=channels,
|
||||
heads_config=heads_config,
|
||||
semantic_id2label=semantic_id2label,
|
||||
semantic_label2id=semantic_label2id,
|
||||
)
|
||||
|
||||
print("[CKPT] Carregando checkpoint...")
|
||||
ckpt = torch.load(
|
||||
str(checkpoint_path),
|
||||
map_location="cpu",
|
||||
weights_only=False,
|
||||
)
|
||||
|
||||
if "model" not in ckpt:
|
||||
raise RuntimeError(
|
||||
"Checkpoint não contém a chave 'model'. "
|
||||
"Confirme se é um checkpoint salvo pelo script de treino."
|
||||
)
|
||||
|
||||
model.load_state_dict(ckpt["model"], strict=True)
|
||||
model.to(device)
|
||||
model.eval()
|
||||
|
||||
norm_stats_path = None
|
||||
norm_mean = None
|
||||
norm_std = None
|
||||
norm_channels = []
|
||||
|
||||
if args.include_norm:
|
||||
norm_stats_path, norm_mean, norm_std, norm_channels = load_selected_norm_stats(
|
||||
config=config,
|
||||
config_path=config_path,
|
||||
input_channel_indices=input_channel_indices,
|
||||
)
|
||||
|
||||
print(f"[NORM] Embutindo normalização no ONNX: {norm_stats_path}")
|
||||
print(f"[NORM] channels={norm_channels if norm_channels else input_channel_names}")
|
||||
print(f"[NORM] mean={norm_mean}")
|
||||
print(f"[NORM] std ={norm_std}")
|
||||
|
||||
wrapper = MultiHeadOnnxWrapper(
|
||||
model=model,
|
||||
output_heads=output_heads,
|
||||
resize_to_input=args.resize_to_input,
|
||||
include_norm=args.include_norm,
|
||||
norm_mean=norm_mean,
|
||||
norm_std=norm_std,
|
||||
postprocess=args.postprocess,
|
||||
)
|
||||
wrapper.to(device)
|
||||
wrapper.eval()
|
||||
|
||||
dummy_input = torch.randn(
|
||||
1,
|
||||
channels,
|
||||
int(H),
|
||||
int(W),
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
|
||||
input_names = ["pixel_values"]
|
||||
if args.postprocess in ("argmax_lowres", "argmax_fullres"):
|
||||
output_names = [f"{name}_mask" for name in output_heads]
|
||||
else:
|
||||
output_names = [f"{name}_logits" for name in output_heads]
|
||||
|
||||
dynamic_axes = None
|
||||
if args.dynamic_batch:
|
||||
dynamic_axes = {
|
||||
"pixel_values": {0: "batch"},
|
||||
}
|
||||
for out_name in output_names:
|
||||
dynamic_axes[out_name] = {0: "batch"}
|
||||
|
||||
print("[CHECK] Rodando forward PyTorch antes do export...")
|
||||
with torch.no_grad():
|
||||
y = wrapper(dummy_input)
|
||||
|
||||
for name, tensor in zip(output_names, y):
|
||||
print(f" {name}: shape={tuple(tensor.shape)} dtype={tensor.dtype}")
|
||||
|
||||
print("[EXPORT] Exportando ONNX...")
|
||||
with torch.no_grad():
|
||||
torch.onnx.export(
|
||||
wrapper,
|
||||
dummy_input,
|
||||
str(out_path),
|
||||
export_params=True,
|
||||
opset_version=int(args.opset),
|
||||
do_constant_folding=True,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
dynamic_axes=dynamic_axes,
|
||||
)
|
||||
|
||||
print(f"[OK] ONNX salvo em: {out_path}")
|
||||
|
||||
meta_path = out_path.with_suffix(".export_meta.json")
|
||||
save_export_metadata(meta_path, {
|
||||
"config": str(config_path),
|
||||
"checkpoint": str(checkpoint_path),
|
||||
"onnx": str(out_path),
|
||||
"backbone": backbone,
|
||||
"input_shape": [1, channels, int(H), int(W)],
|
||||
"input_channel_names": input_channel_names,
|
||||
"input_channel_indices": input_channel_indices,
|
||||
"heads": output_heads,
|
||||
"output_names": output_names,
|
||||
"resize_to_input": bool(args.resize_to_input),
|
||||
"opset": int(args.opset),
|
||||
"dynamic_batch": bool(args.dynamic_batch),
|
||||
"semantic_id2label": semantic_id2label,
|
||||
"heads_config": heads_config,
|
||||
"checkpoint_epoch": ckpt.get("epoch", None),
|
||||
"checkpoint_best": ckpt.get("best", None),
|
||||
"include_norm": bool(args.include_norm),
|
||||
"norm_stats_path": str(norm_stats_path) if norm_stats_path is not None else None,
|
||||
"norm_channels": norm_channels if norm_channels else input_channel_names,
|
||||
"norm_mean": norm_mean,
|
||||
"norm_std": norm_std,
|
||||
"postprocess": str(args.postprocess),
|
||||
})
|
||||
|
||||
print(f"[OK] Metadata salvo em: {meta_path}")
|
||||
|
||||
# Verificação opcional do pacote onnx, se instalado.
|
||||
try:
|
||||
import onnx
|
||||
print("[ONNX] Verificando grafo com onnx.checker...")
|
||||
onnx_model = onnx.load(str(out_path))
|
||||
onnx.checker.check_model(onnx_model)
|
||||
print("[OK] onnx.checker passou.")
|
||||
except ImportError:
|
||||
print("[WARN] Pacote onnx não instalado. Pulei onnx.checker.")
|
||||
print(" Instale com: pip install onnx")
|
||||
except Exception as e:
|
||||
print(f"[WARN] onnx.checker encontrou problema: {e}")
|
||||
|
||||
print("\nExport finalizado.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,946 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
_12_benchmark_onnx.py
|
||||
|
||||
Benchmark PyTorch vs ONNX Runtime para SegFormer OAK-FCC-3 Multi-Head.
|
||||
|
||||
Mede:
|
||||
- PyTorch FP32
|
||||
- PyTorch AMP/FP16
|
||||
- ONNX Runtime CUDA ou CPU
|
||||
|
||||
Exemplos:
|
||||
|
||||
Benchmark ONNX cru 160x256:
|
||||
|
||||
python _12_benchmark_onnx.py --config config.json --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider cuda
|
||||
|
||||
Benchmark ONNX resized 640x1024:
|
||||
|
||||
python _12_benchmark_onnx.py --config config.json --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider cuda
|
||||
|
||||
|
||||
TensorRT
|
||||
python _12_benchmark_onnx.py --config config.json --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider tensorrt --skip_torch_fp32 --skip_torch_amp
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import csv
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 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 save_csv(path: str | Path, rows: List[dict]):
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
keys = list(rows[0].keys())
|
||||
|
||||
with path.open("w", newline="", encoding="utf-8") as f:
|
||||
w = csv.DictWriter(f, fieldnames=keys)
|
||||
w.writeheader()
|
||||
w.writerows(rows)
|
||||
|
||||
|
||||
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 synchronize_if_cuda(device: torch.device):
|
||||
if device.type == "cuda":
|
||||
torch.cuda.synchronize()
|
||||
|
||||
|
||||
def clear_cuda():
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
|
||||
def percentile(values: List[float], p: float) -> float:
|
||||
if not values:
|
||||
return 0.0
|
||||
return float(np.percentile(np.asarray(values, dtype=np.float64), p))
|
||||
|
||||
|
||||
def summarize_times(times_ms: List[float]) -> dict:
|
||||
arr = np.asarray(times_ms, dtype=np.float64)
|
||||
|
||||
if arr.size == 0:
|
||||
return {
|
||||
"n": 0,
|
||||
"mean_ms": 0.0,
|
||||
"median_ms": 0.0,
|
||||
"min_ms": 0.0,
|
||||
"max_ms": 0.0,
|
||||
"p95_ms": 0.0,
|
||||
"p99_ms": 0.0,
|
||||
"fps_mean": 0.0,
|
||||
"fps_p95_latency": 0.0,
|
||||
}
|
||||
|
||||
mean_ms = float(arr.mean())
|
||||
p95_ms = float(np.percentile(arr, 95))
|
||||
p99_ms = float(np.percentile(arr, 99))
|
||||
|
||||
return {
|
||||
"n": int(arr.size),
|
||||
"mean_ms": mean_ms,
|
||||
"median_ms": float(np.median(arr)),
|
||||
"min_ms": float(arr.min()),
|
||||
"max_ms": float(arr.max()),
|
||||
"p95_ms": p95_ms,
|
||||
"p99_ms": p99_ms,
|
||||
"fps_mean": float(1000.0 / mean_ms) if mean_ms > 0 else 0.0,
|
||||
"fps_p95_latency": float(1000.0 / p95_ms) if p95_ms > 0 else 0.0,
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
ckpt_name = config.get("ckpt_test", "best_score")
|
||||
|
||||
# Usa o número real de canais selecionados,
|
||||
# não necessariamente config["channels"].
|
||||
ch = int(channels)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Dataset / normalização
|
||||
# ============================================================
|
||||
|
||||
def collect_tensor_samples(root: Path, max_samples: int = 50, 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 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 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 load_norm_stats(
|
||||
path: Optional[Path],
|
||||
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():
|
||||
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_inputs_as_numpy(
|
||||
samples: List[Path],
|
||||
channels: int,
|
||||
channel_indices: List[int],
|
||||
mean: Optional[List[float]],
|
||||
std: Optional[List[float]],
|
||||
target_hw: Tuple[int, int],
|
||||
normalize_input: bool = True,
|
||||
) -> List[np.ndarray]:
|
||||
H, W = target_hw
|
||||
xs = []
|
||||
|
||||
for p in samples:
|
||||
chw01 = load_tensor(
|
||||
p,
|
||||
channels=channels,
|
||||
channel_indices=channel_indices,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
if normalize_input:
|
||||
chw = normalize_numpy_chw(chw01, mean=mean, std=std)
|
||||
else:
|
||||
chw = chw01.astype(np.float32, copy=False)
|
||||
|
||||
x = np.expand_dims(chw, axis=0).astype(np.float32)
|
||||
xs.append(x)
|
||||
|
||||
return xs
|
||||
|
||||
|
||||
# ============================================================
|
||||
# PyTorch
|
||||
# ============================================================
|
||||
|
||||
class TorchTupleWrapper(nn.Module):
|
||||
def __init__(self, model: nn.Module, output_heads: List[str]):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.output_heads = list(output_heads)
|
||||
|
||||
def forward(self, pixel_values: torch.Tensor):
|
||||
outputs = self.model(pixel_values=pixel_values)
|
||||
return tuple(outputs[h] for h in self.output_heads)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def benchmark_torch(
|
||||
model: nn.Module,
|
||||
inputs_np: List[np.ndarray],
|
||||
device: torch.device,
|
||||
warmup: int,
|
||||
repeat: int,
|
||||
amp: bool,
|
||||
label: str,
|
||||
) -> Tuple[dict, List[dict]]:
|
||||
model.eval()
|
||||
|
||||
times = []
|
||||
rows = []
|
||||
|
||||
# Precarrega tensors na GPU para medir só inferência do modelo.
|
||||
inputs_t = [
|
||||
torch.from_numpy(x).to(device, non_blocking=True)
|
||||
for x in inputs_np
|
||||
]
|
||||
|
||||
if device.type == "cuda":
|
||||
torch.cuda.synchronize()
|
||||
|
||||
print(f"\n[BENCH] {label} | warmup={warmup} repeat={repeat}")
|
||||
|
||||
# Warmup
|
||||
for i in range(max(0, warmup)):
|
||||
x = inputs_t[i % len(inputs_t)]
|
||||
with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=amp and device.type == "cuda"):
|
||||
_ = model(x)
|
||||
|
||||
synchronize_if_cuda(device)
|
||||
|
||||
# Medição
|
||||
total_iter = len(inputs_t) * max(1, repeat)
|
||||
idx = 0
|
||||
|
||||
for r in range(max(1, repeat)):
|
||||
for sample_idx, x in enumerate(inputs_t):
|
||||
synchronize_if_cuda(device)
|
||||
t0 = time.perf_counter()
|
||||
|
||||
with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=amp and device.type == "cuda"):
|
||||
_ = model(x)
|
||||
|
||||
synchronize_if_cuda(device)
|
||||
dt_ms = (time.perf_counter() - t0) * 1000.0
|
||||
|
||||
times.append(dt_ms)
|
||||
rows.append({
|
||||
"engine": label,
|
||||
"repeat": r,
|
||||
"sample_idx": sample_idx,
|
||||
"iter_idx": idx,
|
||||
"latency_ms": dt_ms,
|
||||
})
|
||||
|
||||
idx += 1
|
||||
|
||||
if idx % 25 == 0 or idx == total_iter:
|
||||
print(f" {idx:04d}/{total_iter:04d} | last={dt_ms:.2f}ms")
|
||||
|
||||
summary = summarize_times(times)
|
||||
summary["engine"] = label
|
||||
|
||||
return summary, rows
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ONNX Runtime
|
||||
# ============================================================
|
||||
|
||||
def create_onnx_session(onnx_path: Path, provider: str):
|
||||
import os
|
||||
|
||||
trt_home = os.environ.get("TRT_HOME", r"C:\dev\TensorRT-10.10.0.31")
|
||||
|
||||
for dll_dir in [
|
||||
os.path.join(trt_home, "lib"),
|
||||
os.path.join(trt_home, "bin"),
|
||||
r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\bin",
|
||||
]:
|
||||
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}")
|
||||
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"onnxruntime não está instalado. Instale com:\n"
|
||||
" pip install onnxruntime-gpu\n"
|
||||
"ou CPU:\n"
|
||||
" pip install onnxruntime"
|
||||
)
|
||||
|
||||
available = ort.get_available_providers()
|
||||
print(f"[ONNX] providers disponíveis: {available}")
|
||||
|
||||
provider = provider.lower()
|
||||
|
||||
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,
|
||||
|
||||
# FP16: o ponto principal do nosso teste.
|
||||
"trt_fp16_enable": True,
|
||||
|
||||
# Cache: evita rebuild do engine a cada execução.
|
||||
"trt_engine_cache_enable": True,
|
||||
"trt_engine_cache_path": str(cache_dir),
|
||||
|
||||
# Timing cache ajuda a acelerar builds futuros.
|
||||
"trt_timing_cache_enable": True,
|
||||
"trt_timing_cache_path": str(cache_dir),
|
||||
|
||||
# Workspace. 4GB é razoável para RTX 3070, ajuste se faltar VRAM.
|
||||
"trt_max_workspace_size": 4 * 1024 * 1024 * 1024,
|
||||
}
|
||||
|
||||
providers = [
|
||||
("TensorrtExecutionProvider", trt_options),
|
||||
"CUDAExecutionProvider",
|
||||
"CPUExecutionProvider",
|
||||
]
|
||||
|
||||
else:
|
||||
providers = [provider]
|
||||
|
||||
# Checagem de disponibilidade, lidando com provider tuple.
|
||||
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}"
|
||||
)
|
||||
|
||||
session = ort.InferenceSession(
|
||||
str(onnx_path),
|
||||
sess_options=sess_options,
|
||||
providers=providers_ok,
|
||||
)
|
||||
|
||||
print(f"[ONNX] usando providers: {session.get_providers()}")
|
||||
|
||||
active_providers = session.get_providers()
|
||||
|
||||
if provider == "tensorrt" and "TensorrtExecutionProvider" not in active_providers:
|
||||
raise RuntimeError(
|
||||
"TensorRTExecutionProvider foi solicitado, mas não ficou ativo. "
|
||||
f"Providers ativos: {active_providers}. "
|
||||
"Provável causa: TensorRT não instalado, DLLs fora do PATH, "
|
||||
"ou versão incompatível com onnxruntime-gpu."
|
||||
)
|
||||
|
||||
if provider == "cuda" and "CUDAExecutionProvider" not in active_providers:
|
||||
raise RuntimeError(
|
||||
"CUDAExecutionProvider foi solicitado, mas não ficou ativo. "
|
||||
f"Providers ativos: {active_providers}."
|
||||
)
|
||||
|
||||
return session
|
||||
|
||||
|
||||
def benchmark_onnx(
|
||||
session,
|
||||
inputs_np: List[np.ndarray],
|
||||
warmup: int,
|
||||
repeat: int,
|
||||
label: str,
|
||||
) -> Tuple[dict, List[dict]]:
|
||||
input_name = session.get_inputs()[0].name
|
||||
|
||||
times = []
|
||||
rows = []
|
||||
|
||||
print(f"\n[BENCH] {label} | warmup={warmup} repeat={repeat}")
|
||||
|
||||
# Warmup
|
||||
for i in range(max(0, warmup)):
|
||||
x = inputs_np[i % len(inputs_np)]
|
||||
_ = session.run(None, {input_name: x})
|
||||
|
||||
# Medição
|
||||
total_iter = len(inputs_np) * max(1, repeat)
|
||||
idx = 0
|
||||
|
||||
for r in range(max(1, repeat)):
|
||||
for sample_idx, x in enumerate(inputs_np):
|
||||
t0 = time.perf_counter()
|
||||
_ = session.run(None, {input_name: x})
|
||||
dt_ms = (time.perf_counter() - t0) * 1000.0
|
||||
|
||||
times.append(dt_ms)
|
||||
rows.append({
|
||||
"engine": label,
|
||||
"repeat": r,
|
||||
"sample_idx": sample_idx,
|
||||
"iter_idx": idx,
|
||||
"latency_ms": dt_ms,
|
||||
})
|
||||
|
||||
idx += 1
|
||||
|
||||
if idx % 25 == 0 or idx == total_iter:
|
||||
print(f" {idx:04d}/{total_iter:04d} | last={dt_ms:.2f}ms")
|
||||
|
||||
summary = summarize_times(times)
|
||||
summary["engine"] = label
|
||||
|
||||
return summary, rows
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 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=50)
|
||||
parser.add_argument("--start_idx", type=int, default=0)
|
||||
parser.add_argument("--warmup", type=int, default=10)
|
||||
parser.add_argument("--repeat", type=int, default=5)
|
||||
|
||||
parser.add_argument("--device", default="cuda", choices=["cuda", "cpu"])
|
||||
parser.add_argument("--onnx_provider", default="cuda", choices=["cuda", "cpu", "tensorrt"])
|
||||
|
||||
parser.add_argument("--skip_torch_fp32", action="store_true")
|
||||
parser.add_argument("--skip_torch_amp", action="store_true")
|
||||
parser.add_argument("--skip_onnx", action="store_true")
|
||||
|
||||
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 cru.",
|
||||
)
|
||||
|
||||
parser.add_argument("--out_dir", default=None)
|
||||
|
||||
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,
|
||||
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,
|
||||
)
|
||||
|
||||
if args.out_dir:
|
||||
out_dir = resolve_path(args.out_dir, Path.cwd())
|
||||
else:
|
||||
out_dir = onnx_path.parent / "benchmarks"
|
||||
|
||||
assert out_dir is not None
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
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("Benchmark 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"Warmup : {args.warmup}")
|
||||
print(f"Repeat : {args.repeat}")
|
||||
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"ONNX has norm: {args.onnx_has_norm}")
|
||||
print(f"Out dir : {out_dir}")
|
||||
print("==========================================")
|
||||
|
||||
if args.onnx_has_norm:
|
||||
print("\n[DATA] Carregando inputs 0..1 crus na RAM...")
|
||||
else:
|
||||
print("\n[DATA] Carregando inputs normalizados na RAM...")
|
||||
inputs_np = load_inputs_as_numpy(
|
||||
samples=samples,
|
||||
channels=channels,
|
||||
channel_indices=input_channel_indices,
|
||||
mean=mean,
|
||||
std=std,
|
||||
target_hw=(H, W),
|
||||
normalize_input=not args.onnx_has_norm,
|
||||
)
|
||||
print(f"[DATA] Inputs carregados: {len(inputs_np)}")
|
||||
|
||||
summaries = []
|
||||
all_rows = []
|
||||
|
||||
# ========================================================
|
||||
# PyTorch
|
||||
# ========================================================
|
||||
need_torch = not args.skip_torch_fp32 or not args.skip_torch_amp
|
||||
|
||||
if need_torch:
|
||||
print("\n[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_model = TorchTupleWrapper(
|
||||
model=model,
|
||||
output_heads=output_heads,
|
||||
).to(device)
|
||||
torch_model.eval()
|
||||
|
||||
clear_cuda()
|
||||
|
||||
if not args.skip_torch_fp32:
|
||||
summary, rows = benchmark_torch(
|
||||
model=torch_model,
|
||||
inputs_np=inputs_np,
|
||||
device=device,
|
||||
warmup=args.warmup,
|
||||
repeat=args.repeat,
|
||||
amp=False,
|
||||
label="torch_fp32",
|
||||
)
|
||||
summaries.append(summary)
|
||||
all_rows.extend(rows)
|
||||
|
||||
clear_cuda()
|
||||
|
||||
if not args.skip_torch_amp:
|
||||
summary, rows = benchmark_torch(
|
||||
model=torch_model,
|
||||
inputs_np=inputs_np,
|
||||
device=device,
|
||||
warmup=args.warmup,
|
||||
repeat=args.repeat,
|
||||
amp=True,
|
||||
label="torch_amp_fp16",
|
||||
)
|
||||
summaries.append(summary)
|
||||
all_rows.extend(rows)
|
||||
|
||||
del torch_model
|
||||
del model
|
||||
clear_cuda()
|
||||
|
||||
# ========================================================
|
||||
# ONNX
|
||||
# ========================================================
|
||||
if not args.skip_onnx:
|
||||
print("\n[ONNX] Carregando sessão...")
|
||||
session = create_onnx_session(
|
||||
onnx_path=onnx_path,
|
||||
provider=args.onnx_provider,
|
||||
)
|
||||
|
||||
summary, rows = benchmark_onnx(
|
||||
session=session,
|
||||
inputs_np=inputs_np,
|
||||
warmup=args.warmup,
|
||||
repeat=args.repeat,
|
||||
label=f"onnx_{args.onnx_provider}",
|
||||
)
|
||||
summaries.append(summary)
|
||||
all_rows.extend(rows)
|
||||
|
||||
# ========================================================
|
||||
# Relatório
|
||||
# ========================================================
|
||||
print("\n========== RESUMO ==========")
|
||||
|
||||
for s in summaries:
|
||||
print(
|
||||
f"{s['engine']:<16} "
|
||||
f"n={s['n']:<4} "
|
||||
f"mean={s['mean_ms']:.3f}ms "
|
||||
f"median={s['median_ms']:.3f}ms "
|
||||
f"p95={s['p95_ms']:.3f}ms "
|
||||
f"p99={s['p99_ms']:.3f}ms "
|
||||
f"fps_mean={s['fps_mean']:.2f} "
|
||||
f"fps_p95={s['fps_p95_latency']:.2f}"
|
||||
)
|
||||
|
||||
base_name = f"{onnx_path.stem}_{args.onnx_provider}"
|
||||
report_json = out_dir / f"{base_name}_benchmark_report.json"
|
||||
report_csv = out_dir / f"{base_name}_benchmark_rows.csv"
|
||||
|
||||
report = {
|
||||
"config": str(config_path),
|
||||
"checkpoint": str(checkpoint_path),
|
||||
"ckpt_name": ckpt_name,
|
||||
"onnx": str(onnx_path),
|
||||
"onnx_has_norm": bool(args.onnx_has_norm),
|
||||
"root": str(root),
|
||||
"samples": len(samples),
|
||||
"warmup": int(args.warmup),
|
||||
"repeat": int(args.repeat),
|
||||
"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,
|
||||
"onnx_provider": args.onnx_provider,
|
||||
"device": str(device),
|
||||
"summaries": summaries,
|
||||
}
|
||||
|
||||
save_json(report_json, report)
|
||||
save_csv(report_csv, all_rows)
|
||||
|
||||
print(f"\n[OK] JSON salvo em: {report_json}")
|
||||
print(f"[OK] CSV salvo em : {report_csv}")
|
||||
print("\nBenchmark finalizado.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -814,7 +814,11 @@ def main():
|
|||
description="Normaliza RAW_BRUTO OAK-FCC-3 para tensor MULTISPEC final de treino."
|
||||
)
|
||||
|
||||
ap.add_argument("--src-root", default="dataset/original/group")
|
||||
ap.add_argument(
|
||||
"--src-roots",
|
||||
default="dataset/original/group;dataset/augmented/group",
|
||||
help="Raízes de entrada separadas por ';'. Ex: dataset/original/group;dataset/augmented/group"
|
||||
)
|
||||
ap.add_argument("--out-root", default="dataset")
|
||||
ap.add_argument("--module-params", default=DEFAULT_MODULE_PARAMS)
|
||||
ap.add_argument("--res", default=f"{DEFAULT_RES[0]}x{DEFAULT_RES[1]}", help="Resolução final WxH.")
|
||||
|
|
@ -827,11 +831,23 @@ def main():
|
|||
|
||||
args = ap.parse_args()
|
||||
|
||||
src_root = Path(args.src_root)
|
||||
src_roots = [
|
||||
Path(x.strip())
|
||||
for x in str(args.src_roots).split(";")
|
||||
if x.strip()
|
||||
]
|
||||
|
||||
out_dataset_root = Path(args.out_root)
|
||||
|
||||
if not src_root.is_dir():
|
||||
raise SystemExit(f"[ERRO] src-root não encontrado: {src_root}")
|
||||
valid_src_roots = []
|
||||
for src_root in src_roots:
|
||||
if src_root.is_dir():
|
||||
valid_src_roots.append(src_root)
|
||||
else:
|
||||
print(f"[WARN] src-root não encontrado, ignorando: {src_root}")
|
||||
|
||||
if not valid_src_roots:
|
||||
raise SystemExit(f"[ERRO] Nenhum src-root válido encontrado: {src_roots}")
|
||||
|
||||
try:
|
||||
res_w, res_h = [int(x) for x in args.res.lower().split("x")]
|
||||
|
|
@ -856,44 +872,53 @@ def main():
|
|||
cor_para_id, _, _, ignore_rgb = carregar_labelmap_completo(str(labelmap_path))
|
||||
ignore_id = _infer_ignore_id(ignore_rgb, 255)
|
||||
|
||||
all_groups = list_groups(src_root)
|
||||
groups_by_root = []
|
||||
|
||||
if args.groups:
|
||||
want = {g.strip() for g in args.groups.split(",") if g.strip()}
|
||||
all_groups = [g for g in all_groups if g in want]
|
||||
for src_root in valid_src_roots:
|
||||
groups = list_groups(src_root)
|
||||
|
||||
if not all_groups:
|
||||
if args.groups:
|
||||
want = {g.strip() for g in args.groups.split(",") if g.strip()}
|
||||
groups = [g for g in groups if g in want]
|
||||
|
||||
if groups:
|
||||
groups_by_root.append((src_root, groups))
|
||||
|
||||
if not groups_by_root:
|
||||
raise SystemExit("[ERRO] Nenhum grupo encontrado.")
|
||||
|
||||
print("============================================")
|
||||
print("Normalize OAK-FCC-3")
|
||||
print(f"SRC : {src_root}")
|
||||
print(f"SRC : {[str(x) for x in valid_src_roots]}")
|
||||
print(f"OUT : {output_root}")
|
||||
print(f"MODULE PARAM : {args.module_params}")
|
||||
print(f"RES : {res}")
|
||||
print(f"RAW SIZE : {raw_size}")
|
||||
print(f"GROUPS : {', '.join(all_groups)}")
|
||||
print("GROUPS :")
|
||||
for root, groups in groups_by_root:
|
||||
print(f" - {root}: {', '.join(groups)}")
|
||||
print(f"SKIP BAD : {args.skip_bad_quality}")
|
||||
print("============================================")
|
||||
|
||||
running_stats = RunningStats()
|
||||
all_rows = []
|
||||
|
||||
for group_name in all_groups:
|
||||
rows = process_group(
|
||||
group_name=group_name,
|
||||
src_root=src_root,
|
||||
output_root=output_root,
|
||||
res=res,
|
||||
raw_size=raw_size,
|
||||
module_params_arg=args.module_params,
|
||||
cor_para_id=cor_para_id,
|
||||
ignore_id=ignore_id,
|
||||
stats=running_stats,
|
||||
dataset_root=out_dataset_root,
|
||||
skip_bad_quality=args.skip_bad_quality,
|
||||
)
|
||||
all_rows.extend(rows)
|
||||
for src_root, all_groups in groups_by_root:
|
||||
for group_name in all_groups:
|
||||
rows = process_group(
|
||||
group_name=group_name,
|
||||
src_root=src_root,
|
||||
output_root=output_root,
|
||||
res=res,
|
||||
raw_size=raw_size,
|
||||
module_params_arg=args.module_params,
|
||||
cor_para_id=cor_para_id,
|
||||
ignore_id=ignore_id,
|
||||
stats=running_stats,
|
||||
dataset_root=out_dataset_root,
|
||||
skip_bad_quality=args.skip_bad_quality,
|
||||
)
|
||||
all_rows.extend(rows)
|
||||
|
||||
manifest_path = Path(args.manifest) if args.manifest else output_root / "normalize_manifest.csv"
|
||||
write_manifest(manifest_path, all_rows)
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,241 @@ class MultiHeadTester:
|
|||
return preds, probs, t_ms
|
||||
|
||||
|
||||
class OnnxMultiHeadTester:
|
||||
def __init__(
|
||||
self,
|
||||
config: dict,
|
||||
onnx_path: Path,
|
||||
provider: str,
|
||||
channels: int,
|
||||
heads_config: Dict[str, dict],
|
||||
mean: Optional[Sequence[float]],
|
||||
std: Optional[Sequence[float]],
|
||||
trt_home: Optional[str] = None,
|
||||
trt_fp16: bool = True,
|
||||
):
|
||||
self.config = config
|
||||
self.onnx_path = onnx_path
|
||||
self.provider = provider.lower()
|
||||
self.channels = int(channels)
|
||||
self.heads_config = heads_config
|
||||
self.runtime_mode = str(config.get("runtime_mode", "all")).lower()
|
||||
|
||||
self.mean = None if mean is None else np.asarray(mean, dtype=np.float32).reshape(1, channels, 1, 1)
|
||||
self.std = None if std is None else np.asarray(std, dtype=np.float32).reshape(1, channels, 1, 1)
|
||||
|
||||
print(f"[ONNX_MODEL] onnx={onnx_path}")
|
||||
print(f"[ONNX_MODEL] provider={provider}")
|
||||
|
||||
self.session = self._create_session(
|
||||
onnx_path=onnx_path,
|
||||
provider=provider,
|
||||
trt_home=trt_home,
|
||||
trt_fp16=trt_fp16,
|
||||
)
|
||||
|
||||
self.input_name = self.session.get_inputs()[0].name
|
||||
self.output_names = [o.name for o in self.session.get_outputs()]
|
||||
print(f"[ONNX_MODEL] input={self.input_name}")
|
||||
print(f"[ONNX_MODEL] outputs={self.output_names}")
|
||||
|
||||
def _create_session(
|
||||
self,
|
||||
onnx_path: Path,
|
||||
provider: str,
|
||||
trt_home: Optional[str],
|
||||
trt_fp16: bool,
|
||||
):
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"onnxruntime não está instalado. Use:\n"
|
||||
" pip install onnxruntime-gpu"
|
||||
)
|
||||
|
||||
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"))
|
||||
|
||||
# fallback comum que vocês estão usando
|
||||
dll_dirs.append(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.3\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 == "cpu":
|
||||
providers = ["CPUExecutionProvider"]
|
||||
|
||||
elif provider == "cuda":
|
||||
providers = ["CUDAExecutionProvider", "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 ONNX inválido: {provider}")
|
||||
|
||||
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 ONNX disponível. Pedido={providers}, disponíveis={available}")
|
||||
|
||||
session = ort.InferenceSession(
|
||||
str(onnx_path),
|
||||
sess_options=sess_options,
|
||||
providers=providers_ok,
|
||||
)
|
||||
|
||||
active = session.get_providers()
|
||||
print(f"[ONNX] usando providers: {active}")
|
||||
|
||||
if provider == "tensorrt" and "TensorrtExecutionProvider" not in active:
|
||||
raise RuntimeError(f"TensorRT solicitado, mas não ficou ativo. Providers ativos: {active}")
|
||||
|
||||
if provider == "cuda" and "CUDAExecutionProvider" not in active:
|
||||
raise RuntimeError(f"CUDA solicitado, mas não ficou ativo. Providers ativos: {active}")
|
||||
|
||||
return session
|
||||
|
||||
def _normalize(self, x: np.ndarray) -> np.ndarray:
|
||||
if self.mean is not None and self.std is not None:
|
||||
return ((x - self.mean[0]) / np.clip(self.std[0], 1e-6, None)).astype(np.float32)
|
||||
return x.astype(np.float32)
|
||||
|
||||
def _selected_heads(self) -> Optional[List[str]]:
|
||||
if self.runtime_mode in ("target_direct", "target_head"):
|
||||
return ["target"]
|
||||
if self.runtime_mode in ("operational", "target_op"):
|
||||
return ["vegetation", "cana"]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _resize_logits_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
|
||||
|
||||
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 _map_outputs(self, outputs: List[np.ndarray]) -> Dict[str, np.ndarray]:
|
||||
raw = {
|
||||
name: arr.astype(np.float32)
|
||||
for name, arr in zip(self.output_names, outputs)
|
||||
}
|
||||
|
||||
mapped = {}
|
||||
for head in self.heads_config.keys():
|
||||
candidates = [
|
||||
head,
|
||||
f"{head}_logits",
|
||||
f"output_{head}",
|
||||
]
|
||||
|
||||
found = None
|
||||
for c in candidates:
|
||||
if c in raw:
|
||||
found = c
|
||||
break
|
||||
|
||||
if found is not None:
|
||||
mapped[head] = raw[found]
|
||||
|
||||
return mapped
|
||||
|
||||
def infer(self, chw_01: np.ndarray) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], float]:
|
||||
h, w = int(chw_01.shape[1]), int(chw_01.shape[2])
|
||||
|
||||
x = self._normalize(chw_01)
|
||||
x = np.expand_dims(x, axis=0).astype(np.float32)
|
||||
|
||||
t0 = time.perf_counter()
|
||||
outputs = self.session.run(None, {self.input_name: x})
|
||||
t_ms = (time.perf_counter() - t0) * 1000.0
|
||||
|
||||
logits_by_head = self._map_outputs(outputs)
|
||||
|
||||
selected = self._selected_heads()
|
||||
if selected is not None:
|
||||
logits_by_head = {
|
||||
hname: logits
|
||||
for hname, logits in logits_by_head.items()
|
||||
if hname in selected
|
||||
}
|
||||
|
||||
preds = {}
|
||||
probs = {}
|
||||
|
||||
for head_name, logits in logits_by_head.items():
|
||||
logits = self._resize_logits_nchw(logits, (h, w))
|
||||
prob = self._softmax_np(logits, axis=1)[0]
|
||||
pred = np.argmax(prob, axis=0).astype(np.uint8)
|
||||
|
||||
preds[head_name] = pred
|
||||
probs[head_name] = prob.astype(np.float32)
|
||||
|
||||
return preds, probs, t_ms
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Checkpoints / paths
|
||||
# ============================================================
|
||||
|
|
@ -1084,6 +1319,43 @@ def find_checkpoint(save_dir: Path, preferred: Optional[str] = None) -> Path:
|
|||
raise FileNotFoundError("Nenhum checkpoint encontrado. Procurei:\n" + "\n".join(str(c) for c in candidates))
|
||||
|
||||
|
||||
def find_onnx_model(save_dir: Path, ckpt_path: Path, preferred: Optional[str] = None) -> Path:
|
||||
"""
|
||||
Resolve o .onnx.
|
||||
|
||||
Se preferred for informado, usa ele.
|
||||
Caso contrário, usa o mesmo stem do checkpoint:
|
||||
best_score.pt -> best_score.onnx
|
||||
"""
|
||||
if preferred:
|
||||
p = Path(preferred)
|
||||
if not p.is_absolute():
|
||||
p_cwd = (Path.cwd() / p).resolve()
|
||||
p_save = (save_dir / p).resolve()
|
||||
p = p_cwd if p_cwd.is_file() else p_save
|
||||
|
||||
if not p.is_file():
|
||||
raise FileNotFoundError(f"ONNX não encontrado: {p}")
|
||||
|
||||
return p.resolve()
|
||||
|
||||
p = ckpt_path.with_suffix(".onnx")
|
||||
|
||||
if not p.is_file():
|
||||
alt = save_dir / f"{ckpt_path.stem}.onnx"
|
||||
p = alt
|
||||
|
||||
if not p.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"ONNX não encontrado para checkpoint {ckpt_path.name}. Procurei:\n"
|
||||
f" {ckpt_path.with_suffix('.onnx')}\n"
|
||||
f" {save_dir / (ckpt_path.stem + '.onnx')}\n"
|
||||
f"Informe manualmente com --onnx."
|
||||
)
|
||||
|
||||
return p.resolve()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Visualização
|
||||
# ============================================================
|
||||
|
|
@ -1179,6 +1451,37 @@ def class_percent(mask: np.ndarray, class_id: int, ignore_id: int = 255) -> floa
|
|||
return float(((mask == class_id) & valid).sum() * 100.0 / den)
|
||||
|
||||
|
||||
def compare_pred_equal_percent(a: Optional[np.ndarray], b: Optional[np.ndarray]) -> Optional[float]:
|
||||
if a is None or b is None:
|
||||
return None
|
||||
|
||||
if a.shape != b.shape:
|
||||
b = cv2.resize(
|
||||
b.astype(np.uint8),
|
||||
(a.shape[1], a.shape[0]),
|
||||
interpolation=cv2.INTER_NEAREST,
|
||||
)
|
||||
|
||||
return float(np.mean(a == b) * 100.0)
|
||||
|
||||
|
||||
def diff_mask_rgb(a: Optional[np.ndarray], b: Optional[np.ndarray]) -> Optional[np.ndarray]:
|
||||
if a is None or b is None:
|
||||
return None
|
||||
|
||||
if a.shape != b.shape:
|
||||
b = cv2.resize(
|
||||
b.astype(np.uint8),
|
||||
(a.shape[1], a.shape[0]),
|
||||
interpolation=cv2.INTER_NEAREST,
|
||||
)
|
||||
|
||||
diff = (a != b).astype(np.uint8) * 255
|
||||
rgb = np.zeros((diff.shape[0], diff.shape[1], 3), dtype=np.uint8)
|
||||
rgb[:, :, 0] = diff # vermelho em RGB
|
||||
return rgb
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Métricas numpy
|
||||
# ============================================================
|
||||
|
|
@ -1240,6 +1543,10 @@ def main():
|
|||
parser.add_argument("--start_idx", type=int, default=0)
|
||||
parser.add_argument("--max_width", type=int, default=1800)
|
||||
parser.add_argument("--runtime_mode", default="all", choices=["all", "target_direct", "operational"])
|
||||
parser.add_argument("--onnx", default="")
|
||||
parser.add_argument("--onnx_provider", default="", choices=["", "cpu", "cuda", "tensorrt"])
|
||||
parser.add_argument("--trt_home", default=None)
|
||||
parser.add_argument("--trt_no_fp16", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
config_path = resolve_path(args.config, Path.cwd())
|
||||
|
|
@ -1336,6 +1643,32 @@ def main():
|
|||
use_amp=not args.no_amp,
|
||||
)
|
||||
|
||||
onnx_tester = None
|
||||
onnx_path = None
|
||||
|
||||
if args.onnx_provider:
|
||||
onnx_path = find_onnx_model(
|
||||
save_dir=save_dir,
|
||||
ckpt_path=ckpt_path,
|
||||
preferred=args.onnx,
|
||||
)
|
||||
|
||||
onnx_tester = OnnxMultiHeadTester(
|
||||
config=config,
|
||||
onnx_path=onnx_path,
|
||||
provider=args.onnx_provider,
|
||||
channels=channels,
|
||||
heads_config=heads_config,
|
||||
mean=mean,
|
||||
std=std,
|
||||
trt_home=args.trt_home,
|
||||
trt_fp16=not args.trt_no_fp16,
|
||||
)
|
||||
|
||||
if onnx_tester is not None:
|
||||
print(f"ONNX : {onnx_path}")
|
||||
print(f"ONNX provider: {args.onnx_provider}")
|
||||
|
||||
out_dir = Path(args.out_dir)
|
||||
ensure_dir(out_dir)
|
||||
|
||||
|
|
@ -1351,6 +1684,10 @@ def main():
|
|||
|
||||
win_name = "OAK-FCC-3 MultiHead Test | D/A navega | S salva | SPACE detalhado | Q sai"
|
||||
cv2.namedWindow(win_name, cv2.WINDOW_NORMAL)
|
||||
win_name_onnx = None
|
||||
if onnx_tester is not None:
|
||||
win_name_onnx = "ONNX/TensorRT MultiHead Test | comparação visual"
|
||||
cv2.namedWindow(win_name_onnx, cv2.WINDOW_NORMAL)
|
||||
|
||||
while True:
|
||||
sample = samples[idx]
|
||||
|
|
@ -1368,6 +1705,13 @@ def main():
|
|||
preds, probs, t_inf = tester.infer(chw)
|
||||
preview_rgb = tensor_to_preview_rgb(chw)
|
||||
|
||||
onnx_preds = None
|
||||
onnx_probs = None
|
||||
t_onnx = None
|
||||
|
||||
if onnx_tester is not None:
|
||||
onnx_preds, onnx_probs, t_onnx = onnx_tester.infer(chw)
|
||||
|
||||
first_pred = next(iter(preds.values()))
|
||||
pred_h, pred_w = first_pred.shape[:2]
|
||||
|
||||
|
|
@ -1508,7 +1852,143 @@ def main():
|
|||
cv2.putText(header, h2, (12, 58), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (0, 255, 120), 1, cv2.LINE_AA)
|
||||
canvas = np.vstack([header, canvas])
|
||||
|
||||
onnx_canvas = None
|
||||
if onnx_tester is not None and onnx_preds is not None and onnx_probs is not None:
|
||||
onnx_pred_sem = onnx_preds.get("semantic")
|
||||
onnx_pred_veg = onnx_preds.get("vegetation")
|
||||
onnx_pred_cana = onnx_preds.get("cana")
|
||||
|
||||
onnx_pred_target_op = None
|
||||
if onnx_pred_veg is not None and onnx_pred_cana is not None:
|
||||
onnx_pred_target_op = operational_target_mask(
|
||||
onnx_pred_veg,
|
||||
onnx_pred_cana,
|
||||
ignore_id=ignore_id,
|
||||
)
|
||||
|
||||
onnx_pred_target_head = onnx_preds.get("target")
|
||||
onnx_pred_target = onnx_pred_target_head if onnx_pred_target_head is not None else onnx_pred_target_op
|
||||
|
||||
onnx_prob_veg = onnx_probs["vegetation"][1] if "vegetation" in onnx_probs and onnx_probs["vegetation"].shape[0] > 1 else None
|
||||
onnx_prob_cana = onnx_probs["cana"][1] if "cana" in onnx_probs and onnx_probs["cana"].shape[0] > 1 else None
|
||||
|
||||
onnx_prob_target_op = None
|
||||
if onnx_prob_veg is not None and onnx_prob_cana is not None:
|
||||
onnx_prob_target_op = np.clip(onnx_prob_veg * (1.0 - onnx_prob_cana), 0.0, 1.0)
|
||||
|
||||
onnx_prob_target_head = None
|
||||
if "target" in onnx_probs:
|
||||
onnx_prob_target_head = onnx_probs["target"][1] if onnx_probs["target"].shape[0] > 1 else onnx_probs["target"][0]
|
||||
|
||||
onnx_prob_target = onnx_prob_target_head if onnx_prob_target_head is not None else onnx_prob_target_op
|
||||
|
||||
eq_sem = compare_pred_equal_percent(pred_sem, onnx_pred_sem)
|
||||
eq_veg = compare_pred_equal_percent(pred_veg, onnx_pred_veg)
|
||||
eq_cana = compare_pred_equal_percent(pred_cana, onnx_pred_cana)
|
||||
eq_target = compare_pred_equal_percent(pred_target, onnx_pred_target)
|
||||
|
||||
onnx_panels: List[Tuple[str, np.ndarray, str]] = []
|
||||
|
||||
if onnx_pred_sem is not None:
|
||||
onnx_sem_rgb = ids_to_rgb(onnx_pred_sem, semantic_cmap, ignore_id)
|
||||
onnx_panels.append((
|
||||
"ONNX semantic",
|
||||
onnx_sem_rgb,
|
||||
"" if eq_sem is None else f"igual PT={eq_sem:.3f}%"
|
||||
))
|
||||
onnx_panels.append((
|
||||
"ONNX overlay semantic",
|
||||
overlay_rgb(preview_rgb, onnx_sem_rgb, args.alpha),
|
||||
""
|
||||
))
|
||||
|
||||
d = diff_mask_rgb(pred_sem, onnx_pred_sem)
|
||||
if d is not None:
|
||||
onnx_panels.append(("Diff semantic", d, "vermelho=diferente"))
|
||||
|
||||
if onnx_pred_veg is not None:
|
||||
onnx_veg_rgb = ids_to_rgb(onnx_pred_veg, BINARY_COLORS_RGB, ignore_id)
|
||||
onnx_panels.append((
|
||||
"ONNX vegetation",
|
||||
onnx_veg_rgb,
|
||||
"" if eq_veg is None else f"igual PT={eq_veg:.3f}%"
|
||||
))
|
||||
|
||||
if onnx_prob_veg is not None:
|
||||
onnx_panels.append((
|
||||
"ONNX P vegetation",
|
||||
prob_to_heat_rgb(onnx_prob_veg),
|
||||
f"mean={float(onnx_prob_veg.mean()):.3f}"
|
||||
))
|
||||
|
||||
if onnx_pred_cana is not None:
|
||||
onnx_cana_rgb = ids_to_rgb(onnx_pred_cana, CANA_COLORS_RGB, ignore_id)
|
||||
onnx_panels.append((
|
||||
"ONNX cana",
|
||||
onnx_cana_rgb,
|
||||
"" if eq_cana is None else f"igual PT={eq_cana:.3f}%"
|
||||
))
|
||||
|
||||
if onnx_prob_cana is not None:
|
||||
onnx_panels.append((
|
||||
"ONNX P cana",
|
||||
prob_to_heat_rgb(onnx_prob_cana),
|
||||
f"mean={float(onnx_prob_cana.mean()):.3f}"
|
||||
))
|
||||
|
||||
if onnx_pred_target is not None:
|
||||
onnx_target_rgb = ids_to_rgb(onnx_pred_target, TARGET_COLORS_RGB, ignore_id)
|
||||
title = "ONNX target HEAD" if onnx_pred_target_head is not None else "ONNX target OP"
|
||||
|
||||
onnx_panels.append((
|
||||
title,
|
||||
onnx_target_rgb,
|
||||
"" if eq_target is None else f"igual PT={eq_target:.3f}%"
|
||||
))
|
||||
onnx_panels.append((
|
||||
"ONNX overlay target",
|
||||
overlay_rgb(preview_rgb, onnx_target_rgb, args.alpha),
|
||||
f"inf={t_onnx:.1f}ms"
|
||||
))
|
||||
|
||||
if onnx_prob_target is not None:
|
||||
onnx_panels.append((
|
||||
"ONNX P target",
|
||||
prob_to_heat_rgb(onnx_prob_target),
|
||||
f"mean={float(onnx_prob_target.mean()):.3f}"
|
||||
))
|
||||
|
||||
d = diff_mask_rgb(pred_target, onnx_pred_target)
|
||||
if d is not None:
|
||||
onnx_panels.append(("Diff target", d, "vermelho=diferente"))
|
||||
|
||||
onnx_canvas = compose_grid(onnx_panels, cols=3, max_width=args.max_width)
|
||||
|
||||
header_h_onnx = 78
|
||||
header_onnx = np.zeros((header_h_onnx, onnx_canvas.shape[1], 3), dtype=np.uint8)
|
||||
header_onnx[:] = (18, 18, 35)
|
||||
|
||||
h1_onnx = f"ONNX {args.onnx_provider} | {source_name} | inf={t_onnx:.1f}ms"
|
||||
h2_parts = []
|
||||
if eq_sem is not None:
|
||||
h2_parts.append(f"sem={eq_sem:.3f}%")
|
||||
if eq_veg is not None:
|
||||
h2_parts.append(f"veg={eq_veg:.3f}%")
|
||||
if eq_cana is not None:
|
||||
h2_parts.append(f"cana={eq_cana:.3f}%")
|
||||
if eq_target is not None:
|
||||
h2_parts.append(f"target={eq_target:.3f}%")
|
||||
h2_onnx = "igual PyTorch: " + " | ".join(h2_parts) if h2_parts else "comparação indisponível"
|
||||
|
||||
cv2.putText(header_onnx, h1_onnx, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (235, 235, 235), 1, cv2.LINE_AA)
|
||||
cv2.putText(header_onnx, h2_onnx, (12, 58), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (0, 255, 120), 1, cv2.LINE_AA)
|
||||
|
||||
onnx_canvas = np.vstack([header_onnx, onnx_canvas])
|
||||
|
||||
cv2.imshow(win_name, cv2.cvtColor(canvas, cv2.COLOR_RGB2BGR))
|
||||
if onnx_canvas is not None and win_name_onnx is not None:
|
||||
cv2.imshow(win_name_onnx, cv2.cvtColor(onnx_canvas, cv2.COLOR_RGB2BGR))
|
||||
|
||||
k = cv2.waitKey(0) & 0xFF
|
||||
|
||||
if k in (ord("q"), ord("Q"), 27):
|
||||
|
|
@ -1520,10 +2000,15 @@ def main():
|
|||
elif k == ord(" "):
|
||||
detailed = not detailed
|
||||
elif k in (ord("s"), ord("S")):
|
||||
out_path = out_dir / f"multihead_{idx:05d}_{sample.base}.png"
|
||||
out_path = out_dir / f"multihead_pytorch_{idx:05d}_{sample.base}.png"
|
||||
cv2.imwrite(str(out_path), cv2.cvtColor(canvas, cv2.COLOR_RGB2BGR))
|
||||
print(f"[SAVE] {out_path}")
|
||||
|
||||
if onnx_canvas is not None:
|
||||
out_path_onnx = out_dir / f"multihead_onnx_{args.onnx_provider}_{idx:05d}_{sample.base}.png"
|
||||
cv2.imwrite(str(out_path_onnx), cv2.cvtColor(onnx_canvas, cv2.COLOR_RGB2BGR))
|
||||
print(f"[SAVE] {out_path_onnx}")
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if visited:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"camera": "oak-fcc-3",
|
||||
"modelo": "segformer_b1",
|
||||
"model_name": "target_teached",
|
||||
"model_name": "target_aug",
|
||||
"main_class_name": "cana",
|
||||
"es_classes": "",
|
||||
"model_to_use": "geral",
|
||||
|
|
@ -15,8 +15,9 @@
|
|||
"use_ndvi": false,
|
||||
"backbone": "nvidia/mit-b1",
|
||||
"fusion_mode": "stacked",
|
||||
"stats_source_tag": "stacked_raw4",
|
||||
"stats_source_tag": "stacked_raw5",
|
||||
"module_params_json": "calibration/module_params.json",
|
||||
"ckpt_test": "best_target",
|
||||
"multi_head": true,
|
||||
"heads": {
|
||||
"semantic": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue