Visual e Weed Workers refatorados V1

This commit is contained in:
Diego Freitas 2026-05-22 19:31:00 -03:00
parent 2a5bb43232
commit b74555301c
13 changed files with 6570 additions and 6681 deletions

View File

@ -3,58 +3,67 @@
"id": 1, "id": 1,
"Arquivo": "model", "Arquivo": "model",
"Diretorio": "C:\\AgroBaseModels\\Ervas\\", "Diretorio": "C:\\AgroBaseModels\\Ervas\\",
"Extensao": ".pt", "Extensao": ".onnx",
"Versao": "3_1", "Versao": "4_0",
"TipoArquivo": 1, "TipoArquivo": 1,
"ArquivoDownload": "models/weed_detector_model-3_1.pt" "ArquivoDownload": "models/weed_detector_model-4_0.onnx"
}, },
{ {
"id": 2, "id": 2,
"Arquivo": "model", "Arquivo": "model",
"Diretorio": "C:\\AgroBaseModels\\Ervas\\", "Diretorio": "C:\\AgroBaseModels\\Ervas\\",
"Extensao": ".txt", "Extensao": ".txt",
"Versao": "3_1", "Versao": "4_0",
"TipoArquivo": 1, "TipoArquivo": 1,
"ArquivoDownload": "models/weed_detector_labelmap-3_1.txt" "ArquivoDownload": "models/weed_detector_labelmap-4_0.txt"
}, },
{ {
"id": 3, "id": 3,
"Arquivo": "model", "Arquivo": "model",
"Diretorio": "C:\\AgroBaseModels\\Ervas\\", "Diretorio": "C:\\AgroBaseModels\\Ervas\\",
"Extensao": ".json", "Extensao": ".json",
"Versao": "1_1", "Versao": "4_0",
"TipoArquivo": 1, "TipoArquivo": 1,
"ArquivoDownload": "models/weed_detector_normstats-1_1.json" "ArquivoDownload": "models/weed_detector_normstats-4_0.json"
}, },
{ {
"id": 4, "id": 4,
"Arquivo": "modelseg", "Arquivo": "modelmp",
"Diretorio": "C:\\AgroBaseModels\\Ruas\\", "Diretorio": "C:\\AgroBaseModels\\Ervas\\",
"Extensao": ".pt", "Extensao": ".json",
"Versao": "1_3", "Versao": "4_0",
"TipoArquivo": 0, "TipoArquivo": 1,
"ArquivoDownload": "models/street_detector_model_seg-1_3.pt", "ArquivoDownload": "models/weed_detector_moduleparams-4_0.json"
}, },
{ {
"id": 5, "id": 5,
"Arquivo": "modelseg", "Arquivo": "modelseg",
"Diretorio": "C:\\AgroBaseModels\\Ruas\\", "Diretorio": "C:\\AgroBaseModels\\Ruas\\",
"Extensao": ".txt", "Extensao": ".onnx",
"Versao": "1_3", "Versao": "2_0",
"TipoArquivo": 0, "TipoArquivo": 0,
"ArquivoDownload": "models/street_detector_labelmap_seg-1_3.txt" "ArquivoDownload": "models/street_detector_model_seg-2_0.onnx",
}, },
{ {
"id": 6, "id": 6,
"Arquivo": "modelseg", "Arquivo": "modelseg",
"Diretorio": "C:\\AgroBaseModels\\Ruas\\", "Diretorio": "C:\\AgroBaseModels\\Ruas\\",
"Extensao": ".json", "Extensao": ".txt",
"Versao": "1_1", "Versao": "2_0",
"TipoArquivo": 0, "TipoArquivo": 0,
"ArquivoDownload": "models/street_detector_normstats-1_1.json" "ArquivoDownload": "models/street_detector_labelmap_seg-2_0.txt"
}, },
{ {
"id": 7, "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", "Arquivo": "modeldet",
"Diretorio": "C:\\AgroBaseModels\\Ruas\\", "Diretorio": "C:\\AgroBaseModels\\Ruas\\",
"Extensao": ".blob", "Extensao": ".blob",
@ -63,7 +72,7 @@
"ArquivoDownload": "models/street_detector_model_det-1_0.blob", "ArquivoDownload": "models/street_detector_model_det-1_0.blob",
}, },
{ {
"id": 8, "id": 9,
"Arquivo": "parametersAtu", "Arquivo": "parametersAtu",
"Diretorio": "Parametros/", "Diretorio": "Parametros/",
"Extensao": ".par", "Extensao": ".par",
@ -72,7 +81,7 @@
"ArquivoDownload": "parameters/parametersAtu-2_0.par", "ArquivoDownload": "parameters/parametersAtu-2_0.par",
}, },
{ {
"id": 9, "id": 10,
"Arquivo": "parametersMvd", "Arquivo": "parametersMvd",
"Diretorio": "Parametros/", "Diretorio": "Parametros/",
"Extensao": ".par", "Extensao": ".par",
@ -81,7 +90,7 @@
"ArquivoDownload": "parameters/parametersMvd-2_0.par" "ArquivoDownload": "parameters/parametersMvd-2_0.par"
}, },
{ {
"id": 10, "id": 11,
"Arquivo": "parametersSen", "Arquivo": "parametersSen",
"Diretorio": "Parametros/", "Diretorio": "Parametros/",
"Extensao": ".par", "Extensao": ".par",
@ -90,7 +99,7 @@
"ArquivoDownload": "parameters/parametersSen-2_0.par" "ArquivoDownload": "parameters/parametersSen-2_0.par"
}, },
{ {
"id": 11, "id": 12,
"Arquivo": "pinoutAtu", "Arquivo": "pinoutAtu",
"Diretorio": "Parametros/", "Diretorio": "Parametros/",
"Extensao": ".pin", "Extensao": ".pin",
@ -99,7 +108,7 @@
"ArquivoDownload": "parameters/pinoutAtu-2_0.pin" "ArquivoDownload": "parameters/pinoutAtu-2_0.pin"
}, },
{ {
"id": 12, "id": 13,
"Arquivo": "pinoutSen", "Arquivo": "pinoutSen",
"Diretorio": "Parametros/", "Diretorio": "Parametros/",
"Extensao": ".pin", "Extensao": ".pin",
@ -108,7 +117,7 @@
"ArquivoDownload": "parameters/pinoutSen-2_0.pin" "ArquivoDownload": "parameters/pinoutSen-2_0.pin"
}, },
{ {
"id": 13, "id": 14,
"Arquivo": "weed_detector_oak", "Arquivo": "weed_detector_oak",
"Diretorio": "Python\\Scripts\\", "Diretorio": "Python\\Scripts\\",
"Extensao": ".py", "Extensao": ".py",
@ -117,7 +126,7 @@
"ArquivoDownload": "weed_detector_oak-1_0.py" "ArquivoDownload": "weed_detector_oak-1_0.py"
}, },
{ {
"id": 14, "id": 15,
"Arquivo": "map_load", "Arquivo": "map_load",
"Diretorio": "Python\\Scripts\\", "Diretorio": "Python\\Scripts\\",
"Extensao": ".py", "Extensao": ".py",
@ -126,7 +135,7 @@
"ArquivoDownload": "scripts/map_load-1_0.py" "ArquivoDownload": "scripts/map_load-1_0.py"
}, },
{ {
"id": 15, "id": 16,
"Arquivo": "map_follow", "Arquivo": "map_follow",
"Diretorio": "Python\\Scripts\\", "Diretorio": "Python\\Scripts\\",
"Extensao": ".py", "Extensao": ".py",
@ -135,7 +144,7 @@
"ArquivoDownload": "scripts/map_follow-1_0.py" "ArquivoDownload": "scripts/map_follow-1_0.py"
}, },
{ {
"id": 16, "id": 17,
"Arquivo": "gps_viewer", "Arquivo": "gps_viewer",
"Diretorio": "Python\\Scripts\\", "Diretorio": "Python\\Scripts\\",
"Extensao": ".py", "Extensao": ".py",
@ -144,7 +153,7 @@
"ArquivoDownload": "scripts/gps_viewer-1_0.py" "ArquivoDownload": "scripts/gps_viewer-1_0.py"
}, },
{ {
"id": 17, "id": 18,
"Arquivo": "modelo_3d", "Arquivo": "modelo_3d",
"Diretorio": "Python\\Output\\", "Diretorio": "Python\\Output\\",
"Extensao": ".obj", "Extensao": ".obj",
@ -153,7 +162,7 @@
"ArquivoDownload": "modelo_3d-1_0.obj" "ArquivoDownload": "modelo_3d-1_0.obj"
}, },
{ {
"id": 18, "id": 19,
"Arquivo": "modelo_3d", "Arquivo": "modelo_3d",
"Diretorio": "Python\\Output\\", "Diretorio": "Python\\Output\\",
"Extensao": ".mtl", "Extensao": ".mtl",

View File

@ -1,184 +1,575 @@
import json import json
import os import os
import time import time
from typing import Any, Dict, List, Optional, Sequence, Tuple
import cv2 import cv2
import numpy as np import numpy as np
import torch import onnxruntime as ort
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)
class SegformerNavRunner: 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 norm_stats para normalizar fora.
- Assume que o ONNX 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 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_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_result = None
self._last_aux_ts = 0.0 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( self.cor_para_id, self.colormap_rgb, self.classes, self.ignore_rgb = carregar_labelmap_completo(
seg_config["ia_labelmap_path"] seg_config["ia_labelmap_path"]
) )
self.resolucao = tuple(seg_config["ia_resolution"]) # [W,H] self.session = self._create_session()
self.roi_inicio = seg_config["ia_roi_begin"] self.input_name = self._resolve_input_name()
self.roi_tamanho = seg_config["ia_roi_size"] self.output_names = [o.name for o in self.session.get_outputs()]
self.last_infer = None
self.mode = seg_config.get("ia_mode", "dual_label") self.input_meta = next(i for i in self.session.get_inputs() if i.name == self.input_name)
self.model = None self.input_type = str(self.input_meta.type)
self.aux_head = None self.input_shape = self.input_meta.shape
self.label_names = {}
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 if self.debug_session:
norm_std = self.IMAGENET_STD 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): # Inicialização ONNX
with open(norm_stats_path, "r", encoding="utf-8") as f: # -------------------------------------------------------------------------
norm_stats = json.load(f)
stats_channels = norm_stats.get("channels", []) def _create_session(self) -> ort.InferenceSession:
stats_mean = norm_stats.get("mean", []) sess_options = ort.SessionOptions()
stats_std = norm_stats.get("std", []) 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"]): if intra_threads > 0:
norm_mean = [ sess_options.intra_op_num_threads = intra_threads
stats_mean[idx_by_name["R"]], if inter_threads > 0:
stats_mean[idx_by_name["G"]], sess_options.inter_op_num_threads = inter_threads
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.")
self.set_norm_stats(norm_mean, norm_std) providers = self._build_providers()
def set_norm_stats(self, mean, std): try:
self._norm_mean = torch.tensor(mean, dtype=torch.float32, device=self.device).view(3, 1, 1) return ort.InferenceSession(
self._norm_std = torch.tensor(std, dtype=torch.float32, device=self.device).view(3, 1, 1).clamp_min(1e-6) 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): def _build_providers(self) -> List[Any]:
return (img - self._norm_mean) / self._norm_std available = set(ort.get_available_providers())
def _build_base_model(self, backbone: str, num_classes: int): if self.provider_mode in ("tensorrt", "trt"):
config = SegformerConfig.from_pretrained( providers: List[Any] = []
backbone,
local_files_only=True 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) roi_rgb = frame_rgb[y_fim:y_inicio, 0:W]
config.output_hidden_states = True t_crop = time.perf_counter()
model = SegformerForSemanticSegmentation(config) if roi_rgb is None or roi_rgb.size == 0:
return model return None, None, None, (y_fim, y_inicio), None
def _load_dual_checkpoint(self, seg_config): input_tensor, roi_resized = self._prepare_input(roi_rgb)
pt_path = seg_config["ia_model_path"] t_pre = time.perf_counter()
backbone = seg_config["ia_backbone"]
num_classes = len(self.classes)
ckpt = torch.load(pt_path, map_location="cpu", weights_only=False) requested_outputs = [self.seg_output_name]
self.model = self._build_base_model(backbone, num_classes) if self.aux_output_name:
self.model.load_state_dict(ckpt["model"], strict=True) requested_outputs.append(self.aux_output_name)
self.model.to(self.device).eval()
if self.use_channels_last and self.device.type == "cuda": outputs = self.session.run(
self.model.to(memory_format=torch.channels_last) 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 {} expected_w, expected_h = self.resolucao
label_names_raw = extra.get("label_name_by_id", {}) or {} if pred_ids_np.shape[:2] != (expected_h, expected_w):
self.label_names = {int(k): str(v) for k, v in label_names_raw.items()} if label_names_raw else {} pred_ids_np = cv2.resize(
pred_ids_np,
if self.mode == "dual_label": (expected_w, expected_h),
aux_sd = ckpt.get("aux_head") interpolation=cv2.INTER_NEAREST,
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,
) )
self.aux_head.load_state_dict(aux_sd, strict=True) pred_ids_np = np.ascontiguousarray(pred_ids_np.astype(np.uint8, copy=False))
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
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 é 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: else:
self.aux_head = None base = roi_resized
print("[SEG] Modo single carregado.")
print(f"[MODEL] ckpt={pt_path}") if self.input_layout == "nhwc_uint8":
print(f"[MODEL] epoch={ckpt.get('epoch')} bests={ckpt.get('bests')}") 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_inicio = int((1.0 - zona_inicio) * H)
y_fim = int((1.0 - (zona_inicio + faixa_atuacao)) * H) y_fim = int((1.0 - (zona_inicio + faixa_atuacao)) * H)
@ -190,226 +581,49 @@ class SegformerNavRunner:
return y_fim, y_inicio return y_fim, y_inicio
def resize_keep_width(self, img: np.ndarray, new_w: int, min_h: int, interpolation: int) -> np.ndarray: def _load_label_names(self, seg_config: Dict[str, Any]) -> Dict[int, str]:
h, w = img.shape[:2] """
new_h = int(round(new_w * (h / max(1, w)))) Carrega nomes do label/status auxiliar, se existir.
if min_h is not None and new_h < min_h: Aceita:
new_h = min_h - 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): path = (
roi_resized = self.resize_keep_width( seg_config.get("ia_label_names_path")
roi_rgb, or seg_config.get("onnx_label_names_path")
self.resolucao[0],
self.resolucao[1],
cv2.INTER_AREA,
) )
# Garante array contínuo para reduzir cópia torta no torch.from_numpy if raw is None and path and os.path.isfile(path):
roi_resized = np.ascontiguousarray(roi_resized) with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
img_tensor = torch.from_numpy(roi_resized).to( if isinstance(data, dict) and "label_name_by_id" in data:
device=self.device, raw = data["label_name_by_id"]
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,
}
else: else:
aux_result = { raw = data
"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()
t_aux1 = time.perf_counter() if raw is None:
return {}
self.last_infer = time.time() if isinstance(raw, list):
return {i: str(name) for i, name in enumerate(raw)}
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, dict):
result = {}
for k, v in raw.items():
try:
result[int(k)] = str(v)
except Exception:
pass
return result
return {}

View File

@ -37,6 +37,8 @@ class GpuPriorityController:
good_cycles_to_recover: int = 5, good_cycles_to_recover: int = 5,
min_data_age_s: float = 0.0, min_data_age_s: float = 0.0,
max_data_age_s: float = 3.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_periodic: bool = False,
log_warnings: bool = True, log_warnings: bool = True,
): ):
@ -118,6 +120,9 @@ class GpuPriorityController:
"data_ok": False, "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_periodic = bool(log_periodic)
self.log_warnings = bool(log_warnings) self.log_warnings = bool(log_warnings)
self._last_warning_reason = None self._last_warning_reason = None
@ -264,6 +269,18 @@ class GpuPriorityController:
now = time.time() now = time.time()
if not isinstance(ctx, dict) or not ctx: 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 { return {
"health": 0.0, "health": 0.0,
"reason": "sem_ctx_weed", "reason": "sem_ctx_weed",
@ -272,6 +289,7 @@ class GpuPriorityController:
"gpu_ms": 0.0, "gpu_ms": 0.0,
"data_age_s": None, "data_age_s": None,
"data_ok": False, "data_ok": False,
"weed_active": False,
} }
perf = self._extract_perf_root(ctx) perf = self._extract_perf_root(ctx)
@ -282,6 +300,36 @@ class GpuPriorityController:
if ts: if ts:
data_age_s = max(0.0, now - float(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 data_ok = True
if data_age_s is not None: if data_age_s is not None:
data_ok = self.min_data_age_s <= data_age_s <= self.max_data_age_s 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) target = max(float(target), 0.01)
ratios[name] = max(0.0, min(1.5, float(real) / target)) 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] valid_ratios = [v for v in ratios.values() if v is not None]
health = min(valid_ratios) if valid_ratios else 0.0 health = min(valid_ratios) if valid_ratios else 0.0
@ -318,8 +365,6 @@ class GpuPriorityController:
health = 0.0 health = 0.0
reason = "weed_sem_inferencia" 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: if gpu_ms >= 120:
health = min(health, 0.35) health = min(health, 0.35)
reason = "gpu_ms_muito_alto" reason = "gpu_ms_muito_alto"
@ -338,6 +383,7 @@ class GpuPriorityController:
"gpu_ms": float(gpu_ms or 0.0), "gpu_ms": float(gpu_ms or 0.0),
"data_age_s": data_age_s, "data_age_s": data_age_s,
"data_ok": bool(data_ok), "data_ok": bool(data_ok),
"weed_active": True,
} }
def _extract_perf_root(self, ctx: Dict[str, Any]) -> Dict[str, Any]: def _extract_perf_root(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
@ -507,6 +553,11 @@ class GpuPriorityController:
def _log_warning_if_needed(self): def _log_warning_if_needed(self):
h = self._last_health h = self._last_health
if h.get("weed_active") is False:
self._last_warning_reason = None
return
reason = h.get("reason", "ok") reason = h.get("reason", "ok")
health = float(h.get("health", 1.0) or 0.0) health = float(h.get("health", 1.0) or 0.0)

View File

@ -1,142 +1,665 @@
import os
import threading import threading
import time import time
from visual_worker.camera_manager import CameraManager from visual_worker.camera_manager import CameraManager
from shared.contexto_global_redis import ContextoGlobalRedis from shared.contexto_global_redis import ContextoGlobalRedis
module_id = "visual" module_id = "visual"
topico_tx = f"operador/{module_id}/tx" topico_tx = f"operador/{module_id}/tx"
topico_rx = f"operador/{module_id}/rx" topico_rx = f"operador/{module_id}/rx"
debug = True debug = True
ultima_atualizacao = time.time() ultima_atualizacao = time.time()
def mostrar_log(mensagem): def mostrar_log(mensagem):
if debug: if debug:
print(f"{time.time()} - [{module_id}] {mensagem}") print(f"{time.time()} - [{module_id}] {mensagem}")
manager = CameraManager(mostrar_log) manager = CameraManager(mostrar_log)
def get_camera_manager(): def get_camera_manager():
return manager return manager
def iniciar_camera_manager(mx_id): def iniciar_camera_manager(mx_id):
global manager, ultima_atualizacao global manager, ultima_atualizacao
if mx_id is None: if mx_id is None:
return return
agora = time.time() 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 ultima_atualizacao = agora
mostrar_log("Iniciando Camera Manager...") mostrar_log("Iniciando Camera Manager...")
manager.inicializar(mx_id=mx_id) manager.inicializar(mx_id=mx_id)
if manager.camera is not None and manager.operante: if manager.camera is not None and manager.operante:
mostrar_log(f"✅ Camera manager iniciado, com MX_ID: {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_CACHE = None
_CONFIG_MTIME = None
_CONFIG_LOCK = threading.Lock() _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, # CONFIG V1 - VISUAL WORKER
"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,
"bad_cycles_to_degrade": 3,
"good_cycles_to_recover": 5,
"max_data_age_s": 3.0,
"mode_configs": { def construir_mode_configs_adaptativos(cfg: dict) -> dict:
"normal": { gpu = cfg.get("gpu_priority", {}) or {}
"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
}
}
},
"ia_resolution": [1024,576], base = {
"seg_every_n": 1, "segmentacao": float(cfg.get("inferencia_fps", 8.0)),
"det_every_n": 1, "grid": float(cfg.get("grid_fps", 6.0)),
"use_amp": True, "deteccao": float(cfg.get("deteccao_fps", 5.0)),
"use_channels_last": True, "publicacao": float(cfg.get("publicacao_fps", 15.0)),
"use_compact_aux": True, "analise": float(cfg.get("analise_fps", 8.0)),
"debug_timing": False, }
"runtime_fast": True,
"gerar_mask_color": False, if not bool(gpu.get("adaptive_mode_configs", True)):
"gerar_debug_status": False, return gpu.get("mode_configs", {
"usar_connected_components": True, "normal": base,
"usar_corridor_grid": True "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 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 reload_seg_config():
return load_seg_config(force_reload=True)
def load_det_config(): def load_det_config():
_CONFIG_DET = { cfg = dict(DET_DEFAULT_CONFIG)
"debug_visual": False, cfg = aplicar_overrides_redis_det(cfg)
"com_track": False, cfg = normalizar_config_runtime_det(cfg)
"ia_roi_begin": 0.0, return cfg
"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

View File

@ -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,
)

View File

@ -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

View File

@ -1,7 +1,5 @@
import time import time
import json
import threading import threading
import os
from weed_worker.camera_manager import CameraManager from weed_worker.camera_manager import CameraManager
from shared.contexto_global_redis import ContextoGlobalRedis 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}") 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_CACHE = None
_CONFIG_MTIME = None
_CONFIG_LOCK = threading.Lock() _CONFIG_LOCK = threading.Lock()
def load_seg_config(force_reload=False): WEED_DEFAULT_CONFIG = {
global _CONFIG_CACHE, _CONFIG_MTIME # ============================================================
# 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: with _CONFIG_LOCK:
_CONFIG_CACHE = { cfg = dict(WEED_DEFAULT_CONFIG)
"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,
"analise_fps": 15.0, cfg = aplicar_overrides_redis(cfg)
"inferencia_fps": 15.0, cfg = normalizar_config_runtime(cfg)
"deteccao_fps": 15.0,
"tensor_fps": 18.0,
"publicacao_fps": 15.0,
"tipo_camera_solo": "multispectral", _CONFIG_CACHE = cfg
"camera_width": 1280, return _CONFIG_CACHE
"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)