workers
This commit is contained in:
parent
01f17ae988
commit
d42a7d4005
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,7 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import depthai as dai
|
import depthai as dai
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
from camera_worker.tcp_streamer import CameraTcpStreamer
|
from camera_worker.tcp_streamer import CameraTcpStreamer
|
||||||
from health_worker.modulos.imu import IMUCamera
|
from health_worker.modulos.imu import IMUCamera
|
||||||
|
|
@ -8,10 +9,11 @@ from shared.enums import StatusModulo, T_Code
|
||||||
from shared.contexto_global_redis import ContextoGlobalRedis
|
from shared.contexto_global_redis import ContextoGlobalRedis
|
||||||
|
|
||||||
class CameraOak:
|
class CameraOak:
|
||||||
def __init__(self, mostrar_log, mx_id, modelo_ia_seg=None, modelo_ia_det=None, iniciar_imu=False):
|
def __init__(self, mostrar_log, mx_id, modelo_ia_seg=None, modelo_ia_det=None, iniciar_imu=False, perf_monitor=None):
|
||||||
self.mostrar_log = mostrar_log
|
self.mostrar_log = mostrar_log
|
||||||
self.modelo_ia_seg = modelo_ia_seg
|
self.modelo_ia_seg = modelo_ia_seg
|
||||||
self.modelo_ia_det = modelo_ia_det
|
self.modelo_ia_det = modelo_ia_det
|
||||||
|
self.perf = perf_monitor
|
||||||
self.stream = None
|
self.stream = None
|
||||||
|
|
||||||
self.dispositivo = T_Code.Vzo
|
self.dispositivo = T_Code.Vzo
|
||||||
|
|
@ -27,12 +29,39 @@ class CameraOak:
|
||||||
self.rodando = False
|
self.rodando = False
|
||||||
self.iniciado = False
|
self.iniciado = False
|
||||||
|
|
||||||
|
self._rgb_seq = None
|
||||||
|
self._depth_seq = None
|
||||||
|
self._rgb_device_ts = None
|
||||||
|
self._depth_device_ts = None
|
||||||
|
|
||||||
|
self._cache_lock = threading.Lock()
|
||||||
|
self._cache_thread = None
|
||||||
|
|
||||||
|
self._rgb_cache = None
|
||||||
|
self._rgb_cache_ts = 0.0
|
||||||
|
self._rgb_cache_resultado = {
|
||||||
|
"erro": "sem frame rgb em cache",
|
||||||
|
"duracao": 0,
|
||||||
|
"frame_valido": False
|
||||||
|
}
|
||||||
|
|
||||||
|
self._depth_cache = None
|
||||||
|
self._depth_cache_ts = 0.0
|
||||||
|
self._depth_cache_resultado = {
|
||||||
|
"erro": "sem frame depth em cache",
|
||||||
|
"duracao": 0,
|
||||||
|
"frame_valido": False
|
||||||
|
}
|
||||||
|
|
||||||
|
self._cache_rodando = False
|
||||||
|
|
||||||
|
self.parametros = {}
|
||||||
|
|
||||||
disp_list = dai.Device.getAllAvailableDevices()
|
disp_list = dai.Device.getAllAvailableDevices()
|
||||||
disp_info = next((d for d in disp_list if d.getMxId() == mx_id), None)
|
disp_info = next((d for d in disp_list if d.getMxId() == mx_id), None)
|
||||||
|
|
||||||
if not disp_info:
|
if not disp_info:
|
||||||
return
|
raise RuntimeError(f"Dispositivo com mxid {mx_id} não encontrado")
|
||||||
#raise RuntimeError(f"Dispositivo com mxid {mx_id} não encontrado")
|
|
||||||
|
|
||||||
self.dev_info = disp_info
|
self.dev_info = disp_info
|
||||||
|
|
||||||
|
|
@ -47,7 +76,7 @@ class CameraOak:
|
||||||
iniciando=True,
|
iniciando=True,
|
||||||
iniciado=False,
|
iniciado=False,
|
||||||
rodando=False,
|
rodando=False,
|
||||||
parametros={}
|
parametros=self.parametros
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fase 1: detectar sensores sem pipeline
|
# Fase 1: detectar sensores sem pipeline
|
||||||
|
|
@ -70,9 +99,10 @@ class CameraOak:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
self.mostrar_log(f"Falha ao detectar sensores: {e}")
|
self.mostrar_log(f"Falha ao detectar sensores: {e}")
|
||||||
|
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
ContextoGlobalRedis.CamKey(self.mx_id),
|
ContextoGlobalRedis.CamKey(self.mx_id),
|
||||||
|
timestamp=time.time(),
|
||||||
modelo=self.modelo,
|
modelo=self.modelo,
|
||||||
dispositivo=self.dispositivo.value,
|
dispositivo=self.dispositivo.value,
|
||||||
tem_depth=self.tem_depth,
|
tem_depth=self.tem_depth,
|
||||||
|
|
@ -90,7 +120,7 @@ class CameraOak:
|
||||||
|
|
||||||
if self.tem_imu and iniciar_imu:
|
if self.tem_imu and iniciar_imu:
|
||||||
self.q_imu = self.device.getOutputQueue(name="imu", maxSize=50, blocking=False)
|
self.q_imu = self.device.getOutputQueue(name="imu", maxSize=50, blocking=False)
|
||||||
self.imu = IMUCamera(self.q_imu, freq=100, angulo_inicial=26.3)
|
self.imu = IMUCamera(self.mx_id, self.q_imu, freq=100, angulo_inicial=26.3)
|
||||||
|
|
||||||
if self.modelo_ia_seg is not None:
|
if self.modelo_ia_seg is not None:
|
||||||
self.q_seg = self.device.getOutputQueue(name="seg", maxSize=1, blocking=False)
|
self.q_seg = self.device.getOutputQueue(name="seg", maxSize=1, blocking=False)
|
||||||
|
|
@ -104,6 +134,8 @@ class CameraOak:
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
self._iniciar_cache_frames()
|
||||||
|
|
||||||
if self.dispositivo == T_Code.Snr:
|
if self.dispositivo == T_Code.Snr:
|
||||||
dadosSnr = ContextoGlobalRedis.get_operacao().get("Snr", {})
|
dadosSnr = ContextoGlobalRedis.get_operacao().get("Snr", {})
|
||||||
self.parametros = {
|
self.parametros = {
|
||||||
|
|
@ -152,22 +184,392 @@ class CameraOak:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.mostrar_log(f"Erro ao iniciar camera: {e}")
|
self.mostrar_log(f"Erro ao iniciar camera: {e}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
ContextoGlobalRedis.CamKey(self.mx_id),
|
ContextoGlobalRedis.CamKey(self.mx_id),
|
||||||
|
timestamp=time.time(),
|
||||||
parametros=self.parametros,
|
parametros=self.parametros,
|
||||||
iniciado=self.iniciado,
|
iniciado=self.iniciado,
|
||||||
iniciando=False,
|
iniciando=False,
|
||||||
iniciado_em=time.time() if self.iniciado else None
|
iniciado_em=time.time() if self.iniciado else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def parar(self):
|
||||||
|
self._cache_rodando = False
|
||||||
|
self.rodando = False
|
||||||
|
self.iniciado = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._cache_thread is not None and self._cache_thread.is_alive():
|
||||||
|
self._cache_thread.join(timeout=1.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._cache_thread = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.imu is not None:
|
||||||
|
self.imu.parar()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.imu = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(self, "device") and self.device is not None:
|
||||||
|
self.device.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.device = None
|
||||||
|
self.pipeline = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.q_video = None
|
||||||
|
self.q_depth = None
|
||||||
|
self.q_imu = None
|
||||||
|
self.q_seg = None
|
||||||
|
self.q_det = None
|
||||||
|
self.q_det_track = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self._cache_lock:
|
||||||
|
self._rgb_cache = None
|
||||||
|
self._depth_cache = None
|
||||||
|
|
||||||
|
self._rgb_cache_ts = 0.0
|
||||||
|
self._depth_cache_ts = 0.0
|
||||||
|
|
||||||
|
self.timestamp_ultimo_frame_rgb = None
|
||||||
|
self.timestamp_ultimo_frame_depth = None
|
||||||
|
self.timestamp_ultima_deteccao = None
|
||||||
|
self.timestamp_ultima_segmentacao = None
|
||||||
|
|
||||||
|
self._rgb_cache_resultado = {
|
||||||
|
"erro": "pipeline parado",
|
||||||
|
"duracao": 0.0,
|
||||||
|
"frame_valido": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
self._depth_cache_resultado = {
|
||||||
|
"erro": "pipeline parado",
|
||||||
|
"duracao": 0.0,
|
||||||
|
"frame_valido": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.stream = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _is_erro_fatal_depthai(self, erro):
|
||||||
|
if erro is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
txt = str(erro)
|
||||||
|
|
||||||
|
sinais_fatais = [
|
||||||
|
"X_LINK_ERROR",
|
||||||
|
"Communication exception",
|
||||||
|
"Couldn't read data from stream",
|
||||||
|
"Device already closed",
|
||||||
|
"device has been closed",
|
||||||
|
"Nenhum dispositivo DepthAI",
|
||||||
|
"No available devices",
|
||||||
|
"não encontrado",
|
||||||
|
"not found",
|
||||||
|
]
|
||||||
|
|
||||||
|
return any(s in txt for s in sinais_fatais)
|
||||||
|
|
||||||
|
def esta_utilizavel(self):
|
||||||
|
if not self.iniciado:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.device is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
erro_rgb = (self._rgb_cache_resultado or {}).get("erro")
|
||||||
|
erro_depth = (self._depth_cache_resultado or {}).get("erro")
|
||||||
|
|
||||||
|
if self._is_erro_fatal_depthai(erro_rgb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._is_erro_fatal_depthai(erro_depth):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(self.device, "isPipelineRunning"):
|
||||||
|
if not self.device.isPipelineRunning():
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
if self._is_erro_fatal_depthai(e):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def tem_frame_recente(self, timeout=5.0):
|
||||||
|
agora = time.time()
|
||||||
|
|
||||||
|
ts_rgb = self.timestamp_ultimo_frame_rgb or 0.0
|
||||||
|
ts_depth = self.timestamp_ultimo_frame_depth or 0.0
|
||||||
|
|
||||||
|
rgb_ok = (agora - ts_rgb) <= timeout
|
||||||
|
depth_ok = (agora - ts_depth) <= timeout if self.tem_depth else True
|
||||||
|
|
||||||
|
return rgb_ok or depth_ok
|
||||||
|
|
||||||
|
def _marcar_desconectada(self, motivo):
|
||||||
|
agora = time.time()
|
||||||
|
|
||||||
|
self.iniciado = False
|
||||||
|
self.rodando = False
|
||||||
|
|
||||||
|
self.ultima_saude = {
|
||||||
|
"timestamp": agora,
|
||||||
|
"conectado": False,
|
||||||
|
"status": StatusModulo.DESCONECTADO.value,
|
||||||
|
"saude": 0,
|
||||||
|
"motivos": [str(motivo)],
|
||||||
|
"saude_individual": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from camera_worker.manager import definir_saude_camera
|
||||||
|
|
||||||
|
definir_saude_camera(
|
||||||
|
self.mx_id,
|
||||||
|
StatusModulo.DESCONECTADO,
|
||||||
|
0,
|
||||||
|
[str(motivo)],
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
"temperatura": 0.0,
|
||||||
|
"memoria_usada": 0.0,
|
||||||
|
"executando": False,
|
||||||
|
"velocidade": "",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
agora,
|
||||||
|
self.dispositivo,
|
||||||
|
imu={
|
||||||
|
"valido": False,
|
||||||
|
"timestamp": 0.0,
|
||||||
|
"roll": 0.0,
|
||||||
|
"pitch": 0.0,
|
||||||
|
"yaw": 0.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _falha_fatal_depthai(self, motivo):
|
||||||
|
self.mostrar_log(f"[CameraOak] Falha fatal DepthAI. Fechando pipeline: {motivo}")
|
||||||
|
|
||||||
|
self._marcar_desconectada(motivo)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.parar()
|
||||||
|
except Exception as e:
|
||||||
|
self.mostrar_log(f"[CameraOak] Erro ao parar após falha fatal: {e}")
|
||||||
|
|
||||||
|
def _iniciar_cache_frames(self):
|
||||||
|
if self._cache_rodando:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._cache_rodando = True
|
||||||
|
|
||||||
|
def loop():
|
||||||
|
while self._cache_rodando:
|
||||||
|
try:
|
||||||
|
teve_pkt = False
|
||||||
|
# RGB
|
||||||
|
if hasattr(self, "q_video") and self.q_video is not None:
|
||||||
|
pkts = self.q_video.getAll()
|
||||||
|
n_pkts = len(pkts)
|
||||||
|
pkt = pkts[-1] if pkts else self.q_video.tryGet()
|
||||||
|
|
||||||
|
if pkt is not None:
|
||||||
|
teve_pkt = True
|
||||||
|
t0 = time.time()
|
||||||
|
frame = pkt.getCvFrame()
|
||||||
|
dur = time.time() - t0
|
||||||
|
|
||||||
|
resultado = {
|
||||||
|
"erro": None,
|
||||||
|
"duracao": dur,
|
||||||
|
"frame_valido": frame is not None and frame.shape[0] > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultado["frame_valido"]:
|
||||||
|
self.rodando = True
|
||||||
|
|
||||||
|
pkt_ts_device = None
|
||||||
|
try:
|
||||||
|
pkt_ts_device = pkt.getTimestamp().total_seconds()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
pkt_seq = None
|
||||||
|
try:
|
||||||
|
pkt_seq = pkt.getSequenceNum()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
seq_anterior = self._rgb_seq
|
||||||
|
seq_delta = None
|
||||||
|
frames_descartados = 0
|
||||||
|
if seq_anterior is not None and pkt_seq is not None:
|
||||||
|
seq_delta = pkt_seq - seq_anterior
|
||||||
|
frames_descartados = max(0, seq_delta - 1)
|
||||||
|
|
||||||
|
if hasattr(self, "perf") and self.perf is not None:
|
||||||
|
self.perf.tick(
|
||||||
|
"camera_rgb",
|
||||||
|
latencia_ms=dur * 1000.0,
|
||||||
|
frame_ts_host=self._rgb_cache_ts,
|
||||||
|
frame_ts_device=pkt_ts_device,
|
||||||
|
seq=pkt_seq,
|
||||||
|
seq_delta=seq_delta,
|
||||||
|
frames_descartados=frames_descartados,
|
||||||
|
n_pkts_getall=n_pkts,
|
||||||
|
valido=resultado["frame_valido"]
|
||||||
|
)
|
||||||
|
|
||||||
|
with self._cache_lock:
|
||||||
|
self._rgb_cache = frame
|
||||||
|
self._rgb_cache_ts = time.time()
|
||||||
|
self._rgb_cache_resultado = resultado
|
||||||
|
self.timestamp_ultimo_frame_rgb = self._rgb_cache_ts
|
||||||
|
self._rgb_seq = pkt_seq
|
||||||
|
self._rgb_device_ts = pkt_ts_device
|
||||||
|
|
||||||
|
# DEPTH
|
||||||
|
if self.tem_depth and hasattr(self, "q_depth") and self.q_depth is not None:
|
||||||
|
pkts = self.q_depth.getAll()
|
||||||
|
n_pkts = len(pkts)
|
||||||
|
pkt = pkts[-1] if pkts else self.q_depth.tryGet()
|
||||||
|
|
||||||
|
if pkt is not None:
|
||||||
|
teve_pkt = True
|
||||||
|
t0 = time.time()
|
||||||
|
frame = pkt.getFrame()
|
||||||
|
dur = time.time() - t0
|
||||||
|
|
||||||
|
resultado = {
|
||||||
|
"erro": None,
|
||||||
|
"duracao": dur,
|
||||||
|
"frame_valido": frame is not None and frame.shape[0] > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultado["frame_valido"]:
|
||||||
|
self.rodando = True
|
||||||
|
|
||||||
|
pkt_ts_device = None
|
||||||
|
try:
|
||||||
|
pkt_ts_device = pkt.getTimestamp().total_seconds()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
pkt_seq = None
|
||||||
|
try:
|
||||||
|
pkt_seq = pkt.getSequenceNum()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
seq_anterior = self._depth_seq
|
||||||
|
seq_delta = None
|
||||||
|
frames_descartados = 0
|
||||||
|
if seq_anterior is not None and pkt_seq is not None:
|
||||||
|
seq_delta = pkt_seq - seq_anterior
|
||||||
|
frames_descartados = max(0, seq_delta - 1)
|
||||||
|
|
||||||
|
if hasattr(self, "perf") and self.perf is not None:
|
||||||
|
self.perf.tick(
|
||||||
|
"camera_depth",
|
||||||
|
latencia_ms=dur * 1000.0,
|
||||||
|
frame_ts_host=self._depth_cache_ts,
|
||||||
|
frame_ts_device=pkt_ts_device,
|
||||||
|
seq=pkt_seq,
|
||||||
|
seq_delta=seq_delta,
|
||||||
|
frames_descartados=frames_descartados,
|
||||||
|
n_pkts_getall=n_pkts,
|
||||||
|
valido=resultado["frame_valido"]
|
||||||
|
)
|
||||||
|
|
||||||
|
with self._cache_lock:
|
||||||
|
self._depth_cache = frame
|
||||||
|
self._depth_cache_ts = time.time()
|
||||||
|
self._depth_cache_resultado = resultado
|
||||||
|
self.timestamp_ultimo_frame_depth = self._depth_cache_ts
|
||||||
|
self.ultimo_resultado_depth = resultado
|
||||||
|
self._depth_seq = pkt_seq
|
||||||
|
self._depth_device_ts = pkt_ts_device
|
||||||
|
|
||||||
|
if not teve_pkt:
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
erro = str(e)
|
||||||
|
|
||||||
|
with self._cache_lock:
|
||||||
|
self._rgb_cache_resultado = {
|
||||||
|
"erro": erro,
|
||||||
|
"duracao": 0,
|
||||||
|
"frame_valido": False
|
||||||
|
}
|
||||||
|
|
||||||
|
self._depth_cache_resultado = {
|
||||||
|
"erro": erro,
|
||||||
|
"duracao": 0,
|
||||||
|
"frame_valido": False
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._is_erro_fatal_depthai(e):
|
||||||
|
self.mostrar_log(f"[CameraOak] Erro fatal no cache loop: {e}")
|
||||||
|
self._cache_rodando = False
|
||||||
|
|
||||||
|
# Não chama parar() diretamente de dentro da própria thread,
|
||||||
|
# porque parar() tenta dar join nela mesma.
|
||||||
|
self._marcar_desconectada(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.device is not None:
|
||||||
|
self.device.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.device = None
|
||||||
|
self.pipeline = None
|
||||||
|
self.iniciado = False
|
||||||
|
self.rodando = False
|
||||||
|
break
|
||||||
|
|
||||||
|
self._cache_thread = threading.Thread(target=loop, daemon=True)
|
||||||
|
self._cache_thread.start()
|
||||||
|
|
||||||
def atualizar_saude(self):
|
def atualizar_saude(self):
|
||||||
#self.mostrar_log(f"[{self.mx_id}] Atualizando saude {self.dispositivo.name}...")
|
#self.mostrar_log(f"[{self.mx_id}] Atualizando saude {self.dispositivo.name}...")
|
||||||
|
|
||||||
if self.imu is not None:
|
if self.imu is not None:
|
||||||
self.imu.atualizar_saude()
|
self.imu.atualizar_saude()
|
||||||
|
|
||||||
conectado = ContextoGlobalRedis.get_cameras().get(self.mx_id) is not None
|
agora = time.time()
|
||||||
|
|
||||||
|
ts_rgb = self.timestamp_ultimo_frame_rgb or 0.0
|
||||||
|
ts_depth = self.timestamp_ultimo_frame_depth or 0.0
|
||||||
|
|
||||||
|
frame_rgb_recente = (agora - ts_rgb) <= 5.0
|
||||||
|
frame_depth_recente = (agora - ts_depth) <= 5.0 if self.tem_depth else True
|
||||||
|
|
||||||
|
camera_no_ctx = (ContextoGlobalRedis.get_cameras() or {}).get(self.mx_id) is not None
|
||||||
|
|
||||||
|
erro_cache = (self._rgb_cache_resultado or {}).get("erro")
|
||||||
|
erro_fatal = self._is_erro_fatal_depthai(erro_cache)
|
||||||
|
|
||||||
|
conectado = bool(
|
||||||
|
self.iniciado
|
||||||
|
and self.device is not None
|
||||||
|
and not erro_fatal
|
||||||
|
and (frame_rgb_recente or camera_no_ctx)
|
||||||
|
)
|
||||||
|
|
||||||
saude = 50
|
saude = 50
|
||||||
motivos = []
|
motivos = []
|
||||||
|
|
@ -175,9 +577,20 @@ class CameraOak:
|
||||||
performance = {
|
performance = {
|
||||||
"temperatura": 0,
|
"temperatura": 0,
|
||||||
"memoria_usada": 0,
|
"memoria_usada": 0,
|
||||||
"executando": False,
|
"executando": False,
|
||||||
"velocidade": ""
|
"velocidade": ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imu = self.imu.get_dados() if self.imu else {
|
||||||
|
"valido": False,
|
||||||
|
"timestamp": 0.0,
|
||||||
|
"roll": 0.0,
|
||||||
|
"pitch": 0.0,
|
||||||
|
"yaw": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultado = dict(self._rgb_cache_resultado or {})
|
||||||
|
|
||||||
if conectado:
|
if conectado:
|
||||||
try:
|
try:
|
||||||
dev = self.device
|
dev = self.device
|
||||||
|
|
@ -223,26 +636,30 @@ class CameraOak:
|
||||||
motivos.append(f"USB lenta: {speed}")
|
motivos.append(f"USB lenta: {speed}")
|
||||||
saude -= 15
|
saude -= 15
|
||||||
|
|
||||||
frame, resultado = self.requisitar_frame_rgb()
|
resultado = self._rgb_cache_resultado
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.mostrar_log(f"Erro ao requisitar dados da camera para atualizar saude: {e}")
|
self.mostrar_log(f"Erro ao requisitar dados da camera para atualizar saude: {e}")
|
||||||
|
|
||||||
resultado = {
|
resultado = {
|
||||||
"erro": str(e),
|
"erro": str(e),
|
||||||
"frame_valido": False,
|
"frame_valido": False,
|
||||||
"duracao": 0
|
"duracao": 0
|
||||||
}
|
}
|
||||||
if "Communication exception" in str(e) or "X_LINK_ERROR" in str(e):
|
|
||||||
|
if self._is_erro_fatal_depthai(e):
|
||||||
conectado = False
|
conectado = False
|
||||||
|
self._falha_fatal_depthai(e)
|
||||||
|
|
||||||
if not conectado:
|
if not conectado:
|
||||||
motivos.append("desconectado")
|
motivos.append("desconectado")
|
||||||
saude = 0
|
saude = 0
|
||||||
elif resultado["erro"]:
|
elif resultado.get("erro"):
|
||||||
motivos.append(resultado["erro"])
|
motivos.append(resultado.get("erro"))
|
||||||
saude = 0
|
saude = 0
|
||||||
if "Communication exception" in resultado["erro"] or "X_LINK_ERROR" in resultado["erro"]:
|
if self._is_erro_fatal_depthai(resultado.get("erro")):
|
||||||
conectado = False
|
conectado = False
|
||||||
elif not resultado["frame_valido"]:
|
self._falha_fatal_depthai(resultado.get("erro"))
|
||||||
|
elif not resultado.get("frame_valido", False):
|
||||||
motivos.append("Frame inválido ou vazio")
|
motivos.append("Frame inválido ou vazio")
|
||||||
saude = 0
|
saude = 0
|
||||||
else:
|
else:
|
||||||
|
|
@ -277,10 +694,23 @@ class CameraOak:
|
||||||
timeout = 2.0
|
timeout = 2.0
|
||||||
ts_depth = self.timestamp_ultimo_frame_depth or 0
|
ts_depth = self.timestamp_ultimo_frame_depth or 0
|
||||||
ts_rgb = self.timestamp_ultimo_frame_rgb or 0
|
ts_rgb = self.timestamp_ultimo_frame_rgb or 0
|
||||||
self.rodando = ((agora - ts_depth) <= timeout or (agora - ts_rgb) <= timeout)
|
self.rodando = bool(
|
||||||
|
conectado
|
||||||
|
and (
|
||||||
|
(agora - (self.timestamp_ultimo_frame_rgb or 0.0)) <= 2.0
|
||||||
|
or (
|
||||||
|
self.tem_depth
|
||||||
|
and (agora - (self.timestamp_ultimo_frame_depth or 0.0)) <= 2.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
from camera_worker.manager import definir_saude_camera
|
from camera_worker.manager import definir_saude_camera
|
||||||
definir_saude_camera(self.mx_id, status, saude, motivos, self.rodando, performance, conectado, agora, self.dispositivo)
|
definir_saude_camera(self.mx_id, status, saude, motivos, self.rodando, performance, conectado, agora, self.dispositivo, imu=imu)
|
||||||
|
|
||||||
|
def _definir_heartbeat(self):
|
||||||
|
from camera_worker.manager import definir_heartbeat_camera
|
||||||
|
definir_heartbeat_camera(self.mx_id)
|
||||||
|
|
||||||
def enviar_frame_tcp(self, frame_bgr):
|
def enviar_frame_tcp(self, frame_bgr):
|
||||||
self.stream.enviar_frame_tcp(frame_bgr)
|
self.stream.enviar_frame_tcp(frame_bgr)
|
||||||
|
|
@ -341,30 +771,34 @@ class CameraOak:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.mostrar_log(f"[WARN] Falha ao montar pipeline imu: {e}")
|
self.mostrar_log(f"[WARN] Falha ao montar pipeline imu: {e}")
|
||||||
|
|
||||||
script = pipeline.create(dai.node.Script)
|
script = None
|
||||||
if self.modelo_ia_seg is not None or self.modelo_ia_det is not None:
|
if self.modelo_ia_seg is not None or self.modelo_ia_det is not None:
|
||||||
try:
|
try:
|
||||||
|
script = pipeline.create(dai.node.Script)
|
||||||
|
|
||||||
N_seg = (self.modelo_ia_seg or {}).get("seg_every_n", 1)
|
N_seg = (self.modelo_ia_seg or {}).get("seg_every_n", 1)
|
||||||
N_det = (self.modelo_ia_det or {}).get("det_every_n", 1)
|
N_det = (self.modelo_ia_det or {}).get("det_every_n", 1)
|
||||||
|
|
||||||
script.setProcessor(dai.ProcessorType.LEON_CSS)
|
script.setProcessor(dai.ProcessorType.LEON_CSS)
|
||||||
script.setScript(f"""
|
script.setScript(f"""
|
||||||
i = 0
|
i = 0
|
||||||
while True:
|
while True:
|
||||||
f = node.io['in'].get()
|
f = node.io['in'].get()
|
||||||
send_seg = {1 if self.modelo_ia_seg else 0} and (i % {N_seg} == 0)
|
send_seg = {1 if self.modelo_ia_seg else 0} and (i % {N_seg} == 0)
|
||||||
send_det = {1 if self.modelo_ia_det else 0} and (i % {N_det} == 0)
|
send_det = {1 if self.modelo_ia_det else 0} and (i % {N_det} == 0)
|
||||||
|
|
||||||
if send_seg and send_det:
|
if send_seg and send_det:
|
||||||
node.io['toSeg'].send(f) # move o original
|
node.io['toSeg'].send(f)
|
||||||
node.io['toDet'].send(f) # clone para a segunda rota
|
node.io['toDet'].send(f)
|
||||||
elif send_seg:
|
elif send_seg:
|
||||||
node.io['toSeg'].send(f)
|
node.io['toSeg'].send(f)
|
||||||
elif send_det:
|
elif send_det:
|
||||||
node.io['toDet'].send(f)
|
node.io['toDet'].send(f)
|
||||||
# se nenhum for enviar, apenas descarta 'f'
|
|
||||||
i += 1
|
i += 1
|
||||||
""")
|
""")
|
||||||
cam.video.link(script.inputs['in'])
|
cam.video.link(script.inputs['in'])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.mostrar_log(f"Erro ao criar script: {e}")
|
self.mostrar_log(f"Erro ao criar script: {e}")
|
||||||
|
|
||||||
|
|
@ -495,43 +929,31 @@ class CameraOak:
|
||||||
#self.mostrar_log(f"[CALIB] {self.parametros}")
|
#self.mostrar_log(f"[CALIB] {self.parametros}")
|
||||||
|
|
||||||
def _latest_pkt(self, q):
|
def _latest_pkt(self, q):
|
||||||
# pega tudo que chegou e fica só com o último
|
|
||||||
pkts = q.getAll()
|
pkts = q.getAll()
|
||||||
if pkts:
|
if pkts:
|
||||||
return pkts[-1]
|
return pkts[-1]
|
||||||
# fallback: tentativa não-bloqueante
|
return q.tryGet()
|
||||||
pkt = q.tryGet()
|
|
||||||
if pkt is not None:
|
|
||||||
return pkt
|
|
||||||
# último recurso: uma única espera curta
|
|
||||||
return q.get() # só se não tiver nada mesmo
|
|
||||||
|
|
||||||
def requisitar_frame_rgb(self):
|
def requisitar_frame_rgb(self):
|
||||||
#print("Requisitando frame")
|
with self._cache_lock:
|
||||||
try:
|
resultado = dict(self._rgb_cache_resultado)
|
||||||
start = time.time()
|
|
||||||
pkt = self._latest_pkt(self.q_video)
|
|
||||||
frame = pkt.getCvFrame()
|
|
||||||
self.timestamp_ultimo_frame_rgb = pkt.getTimestamp().total_seconds() # ou device ts
|
|
||||||
#frame = self.q_video.get().getCvFrame()
|
|
||||||
#self.timestamp_ultimo_frame_rgb = time.time()
|
|
||||||
dur = time.time() - start
|
|
||||||
|
|
||||||
resultado = {
|
if self._is_erro_fatal_depthai(resultado.get("erro")):
|
||||||
"erro": None,
|
erro = resultado.get("erro")
|
||||||
"duracao": dur,
|
else:
|
||||||
"frame_valido": frame is not None and frame.shape[0] > 0
|
erro = None
|
||||||
}
|
|
||||||
|
|
||||||
return frame, resultado
|
|
||||||
|
|
||||||
except Exception as e:
|
if self._rgb_cache is None:
|
||||||
print(f"Erro ao requisitar frame: {e}")
|
frame = None
|
||||||
return None, {
|
else:
|
||||||
"erro": str(e),
|
frame = self._rgb_cache.copy()
|
||||||
"duracao": 0,
|
|
||||||
"frame_valido": False
|
if erro:
|
||||||
}
|
self._falha_fatal_depthai(erro)
|
||||||
|
return None, resultado
|
||||||
|
|
||||||
|
self._definir_heartbeat()
|
||||||
|
return frame, resultado
|
||||||
|
|
||||||
def requisitar_frame_depth(self):
|
def requisitar_frame_depth(self):
|
||||||
if not self.tem_depth:
|
if not self.tem_depth:
|
||||||
|
|
@ -540,27 +962,26 @@ class CameraOak:
|
||||||
"duracao": 0,
|
"duracao": 0,
|
||||||
"frame_valido": False
|
"frame_valido": False
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
start = time.time()
|
|
||||||
pkt = self._latest_pkt(self.q_depth)
|
|
||||||
frame = pkt.getFrame()
|
|
||||||
self.timestamp_ultimo_frame_depth = pkt.getTimestamp().total_seconds()
|
|
||||||
#frame = self.q_depth.get().getFrame()
|
|
||||||
#self.timestamp_ultimo_frame_depth = time.time()
|
|
||||||
dur = time.time() - start
|
|
||||||
|
|
||||||
return frame, {
|
with self._cache_lock:
|
||||||
"erro": None,
|
resultado = dict(self._depth_cache_resultado)
|
||||||
"duracao": dur,
|
|
||||||
"frame_valido": frame is not None and frame.shape[0] > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
if self._is_erro_fatal_depthai(resultado.get("erro")):
|
||||||
return None, {
|
erro = resultado.get("erro")
|
||||||
"erro": str(e),
|
else:
|
||||||
"duracao": 0,
|
erro = None
|
||||||
"frame_valido": False
|
|
||||||
}
|
if self._depth_cache is None:
|
||||||
|
frame = None
|
||||||
|
else:
|
||||||
|
frame = self._depth_cache.copy()
|
||||||
|
|
||||||
|
if erro:
|
||||||
|
self._falha_fatal_depthai(erro)
|
||||||
|
return None, resultado
|
||||||
|
|
||||||
|
self._definir_heartbeat()
|
||||||
|
return frame, resultado
|
||||||
|
|
||||||
def requisitar_segmentacao(self):
|
def requisitar_segmentacao(self):
|
||||||
if not hasattr(self, "q_seg"):
|
if not hasattr(self, "q_seg"):
|
||||||
|
|
@ -581,7 +1002,16 @@ class CameraOak:
|
||||||
return pred_ids, {"erro": None, "duracao": dur, "frame_valido": True}
|
return pred_ids, {"erro": None, "duracao": dur, "frame_valido": True}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dur = time.time() - start
|
dur = time.time() - start
|
||||||
return None, {"erro": str(e), "duracao": dur, "frame_valido": False}
|
erro = str(e)
|
||||||
|
|
||||||
|
if self._is_erro_fatal_depthai(e):
|
||||||
|
self._falha_fatal_depthai(e)
|
||||||
|
|
||||||
|
return None, {
|
||||||
|
"erro": erro,
|
||||||
|
"duracao": dur,
|
||||||
|
"frame_valido": False
|
||||||
|
}
|
||||||
|
|
||||||
def requisitar_deteccao(self, mapear_para_fullframe: bool = False):
|
def requisitar_deteccao(self, mapear_para_fullframe: bool = False):
|
||||||
"""
|
"""
|
||||||
|
|
@ -750,6 +1180,28 @@ class CameraOak:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dur = time.time() - start
|
dur = time.time() - start
|
||||||
return [], {"erro": str(e), "duracao": dur, "frame_valido": False}
|
erro = str(e)
|
||||||
|
|
||||||
|
if self._is_erro_fatal_depthai(e):
|
||||||
|
self._falha_fatal_depthai(e)
|
||||||
|
|
||||||
|
return [], {
|
||||||
|
"erro": erro,
|
||||||
|
"duracao": dur,
|
||||||
|
"frame_valido": False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_stats(self):
|
||||||
|
with self._cache_lock:
|
||||||
|
return {
|
||||||
|
"rgb_ts_host": self._rgb_cache_ts,
|
||||||
|
"depth_ts_host": self._depth_cache_ts,
|
||||||
|
"rgb_ts_device": self._rgb_device_ts,
|
||||||
|
"depth_ts_device": self._depth_device_ts,
|
||||||
|
"rgb_seq": self._rgb_seq,
|
||||||
|
"depth_seq": self._depth_seq,
|
||||||
|
"rgb_resultado": dict(self._rgb_cache_resultado or {}),
|
||||||
|
"depth_resultado": dict(self._depth_cache_resultado or {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -10,61 +10,180 @@ class CameraManager:
|
||||||
self._ultimo_scan = 0
|
self._ultimo_scan = 0
|
||||||
|
|
||||||
def atualizar_cameras(self):
|
def atualizar_cameras(self):
|
||||||
if time.time() - self._ultimo_scan < 10:
|
agora = time.time()
|
||||||
|
|
||||||
|
if agora - self._ultimo_scan < 10:
|
||||||
return
|
return
|
||||||
self._ultimo_scan = time.time()
|
|
||||||
|
self._ultimo_scan = agora
|
||||||
|
|
||||||
|
TIMEOUT_CAMERA_ABERTA = 30 # pipeline aberto, worker precisa responder saúde
|
||||||
|
TIMEOUT_CAMERA_SUMIR = 60 # remove do mapa se passou disso
|
||||||
|
|
||||||
|
dispositivos_serializados = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dispositivos = dai.Device.getAllAvailableDevices()
|
dispositivos = dai.Device.getAllAvailableDevices()
|
||||||
dispositivos_serializados = [d.getMxId() for d in dispositivos]
|
|
||||||
|
for d in dispositivos:
|
||||||
|
device_id = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
device_id = d.getMxId()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not device_id:
|
||||||
|
try:
|
||||||
|
device_id = d.getDeviceId()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not device_id:
|
||||||
|
device_id = getattr(d, "deviceId", None)
|
||||||
|
|
||||||
|
if device_id:
|
||||||
|
dispositivos_serializados.append(str(device_id))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dispositivos_serializados = []
|
self.mostrar_log("[CameraManager] Erro ao listar dispositivos DepthAI:", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with GalService() as cam:
|
with GalService() as cam:
|
||||||
if cam:
|
info = cam.get_device_info() if cam else None
|
||||||
info = cam.get_device_info()
|
serial = info.get("serial") if info else None
|
||||||
if info:
|
|
||||||
dispositivos_serializados.append(info.get("serial"))
|
if serial:
|
||||||
except Exception as e:
|
dispositivos_serializados.append(str(serial))
|
||||||
|
|
||||||
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cameras_mapeadas = ContextoGlobalRedis.get_cameras()
|
dispositivos_serializados = list(set(dispositivos_serializados))
|
||||||
|
|
||||||
|
#self.mostrar_log("Dispositivos Serializados", dispositivos_serializados)
|
||||||
|
|
||||||
|
cameras_mapeadas = ContextoGlobalRedis.get_cameras() or {}
|
||||||
|
|
||||||
|
# 1) Atualiza câmeras vistas fisicamente no scan
|
||||||
for mx_id in dispositivos_serializados:
|
for mx_id in dispositivos_serializados:
|
||||||
cam_existente = cameras_mapeadas.get(mx_id)
|
cam_existente = cameras_mapeadas.get(mx_id)
|
||||||
|
|
||||||
if not cam_existente:
|
if not cam_existente:
|
||||||
cameras_mapeadas[mx_id] = {
|
cameras_mapeadas[mx_id] = {
|
||||||
"timestamp": self._ultimo_scan,
|
"timestamp": agora,
|
||||||
"mx_id": mx_id,
|
"mx_id": mx_id,
|
||||||
|
"presente_scan": True,
|
||||||
|
"status_scan": "detectada",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
cam_existente["timestamp"] = self._ultimo_scan
|
#cam_existente["timestamp"] = agora
|
||||||
|
cam_existente["presente_scan"] = True
|
||||||
|
cam_existente["status_scan"] = "detectada"
|
||||||
|
|
||||||
ids_para_remover = []
|
ids_para_remover = []
|
||||||
for cam_id, val in cameras_mapeadas.items():
|
|
||||||
cam = ContextoGlobalRedis.get_camera(cam_id)
|
# 2) Avalia câmeras já conhecidas
|
||||||
if cam is not None and cam.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value) != StatusModulo.DESCONECTADO.value:
|
for cam_id, val in list(cameras_mapeadas.items()):
|
||||||
if cam.get("dispositivo", T_Code.Vzo.value) == T_Code.Snr.value:
|
cam = ContextoGlobalRedis.get_camera(cam_id) or val
|
||||||
ContextoGlobalRedis.publicar_comando(CmdKey.VisualWorkerRx, { "cmd": VisualWorkerCommandType.AtualizarSaudeCamera.value } )
|
|
||||||
elif cam.get("dispositivo", T_Code.Vzo.value) == T_Code.Cam.value:
|
timestamp = cam.get("timestamp", val.get("timestamp", 0))
|
||||||
ContextoGlobalRedis.publicar_comando(CmdKey.WeedWorkerRx, { "cmd": WeedWorkerCommandType.AtualizarSaudeCamera.value } )
|
idade = agora - timestamp
|
||||||
elif cam is not None and val.get("timestamp", 0) < self._ultimo_scan:
|
#self.mostrar_log(f"mx_id: {cam_id}, ts: {timestamp}, idade: {idade}")
|
||||||
|
|
||||||
|
vista_no_scan = cam_id in dispositivos_serializados
|
||||||
|
|
||||||
|
dispositivo = cam.get("dispositivo", val.get("dispositivo"))
|
||||||
|
|
||||||
|
# Se não apareceu no scan, pode estar com pipeline aberto.
|
||||||
|
# Então pedimos saúde, mas só se ainda estiver dentro da janela tolerável.
|
||||||
|
if not vista_no_scan and idade <= TIMEOUT_CAMERA_ABERTA:
|
||||||
|
if dispositivo == T_Code.Snr.value:
|
||||||
|
ContextoGlobalRedis.publicar_comando(
|
||||||
|
CmdKey.VisualWorkerRx,
|
||||||
|
{"cmd": VisualWorkerCommandType.AtualizarSaudeCamera.value}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif dispositivo == T_Code.Cam.value:
|
||||||
|
ContextoGlobalRedis.publicar_comando(
|
||||||
|
CmdKey.WeedWorkerRx,
|
||||||
|
{"cmd": WeedWorkerCommandType.AtualizarSaudeCamera.value}
|
||||||
|
)
|
||||||
|
|
||||||
|
val["presente_scan"] = False
|
||||||
|
val["status_scan"] = "provavelmente_em_uso"
|
||||||
|
|
||||||
|
# Se passou muito tempo sem scan e sem saúde, remove
|
||||||
|
elif not vista_no_scan and idade > TIMEOUT_CAMERA_SUMIR:
|
||||||
ids_para_remover.append(cam_id)
|
ids_para_remover.append(cam_id)
|
||||||
|
|
||||||
|
# Se apareceu no scan, mantém
|
||||||
|
elif vista_no_scan:
|
||||||
|
val["presente_scan"] = True
|
||||||
|
val["status_scan"] = "detectada"
|
||||||
|
|
||||||
for cam_id in ids_para_remover:
|
for cam_id in ids_para_remover:
|
||||||
|
self.mostrar_log(f"[CameraManager] Removendo câmera inativa: {cam_id}")
|
||||||
cameras_mapeadas.pop(cam_id, None)
|
cameras_mapeadas.pop(cam_id, None)
|
||||||
|
|
||||||
ContextoGlobalRedis.set(CtxKey.DadosCameras, cameras_mapeadas)
|
ContextoGlobalRedis.set(CtxKey.DadosCameras, cameras_mapeadas)
|
||||||
|
|
||||||
visual_worker_camera_id = ContextoGlobalRedis.get_equipamento().get("camera_caminho_id")
|
#self.mostrar_log("Cameras Mapeadas", cameras_mapeadas)
|
||||||
if visual_worker_camera_id is not None and visual_worker_camera_id in cameras_mapeadas:
|
|
||||||
ContextoGlobalRedis.publicar_comando(CmdKey.VisualWorkerRx, { "cmd": VisualWorkerCommandType.IniciarCameraManager.value, "params": visual_worker_camera_id } )
|
equipamento = ContextoGlobalRedis.get_equipamento() or {}
|
||||||
weed_worker_camera_id = ContextoGlobalRedis.get_equipamento().get("camera_solo_id")
|
|
||||||
if (weed_worker_camera_id is not None and weed_worker_camera_id in cameras_mapeadas):
|
visual_worker_camera_id = equipamento.get("camera_caminho_id")
|
||||||
ContextoGlobalRedis.publicar_comando(CmdKey.WeedWorkerRx, { "cmd": WeedWorkerCommandType.IniciarCameraManager.value, "params": weed_worker_camera_id } )
|
weed_worker_camera_id = equipamento.get("camera_solo_id")
|
||||||
|
|
||||||
|
# 3) Só tenta iniciar se a câmera estiver realmente válida
|
||||||
|
if self._camera_pode_iniciar(visual_worker_camera_id, cameras_mapeadas, agora):
|
||||||
|
ContextoGlobalRedis.publicar_comando(
|
||||||
|
CmdKey.VisualWorkerRx,
|
||||||
|
{
|
||||||
|
"cmd": VisualWorkerCommandType.IniciarCameraManager.value,
|
||||||
|
"params": visual_worker_camera_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._camera_pode_iniciar(weed_worker_camera_id, cameras_mapeadas, agora):
|
||||||
|
ContextoGlobalRedis.publicar_comando(
|
||||||
|
CmdKey.WeedWorkerRx,
|
||||||
|
{
|
||||||
|
"cmd": WeedWorkerCommandType.IniciarCameraManager.value,
|
||||||
|
"params": weed_worker_camera_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _camera_pode_iniciar(self, camera_id, cameras_mapeadas, agora):
|
||||||
|
if not camera_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cam = cameras_mapeadas.get(camera_id)
|
||||||
|
if not cam:
|
||||||
|
return False
|
||||||
|
|
||||||
|
timestamp = cam.get("timestamp", 0)
|
||||||
|
idade = agora - timestamp
|
||||||
|
|
||||||
|
presente_scan = cam.get("presente_scan", False)
|
||||||
|
status_scan = cam.get("status_scan")
|
||||||
|
|
||||||
|
# Pode iniciar se foi vista no scan agora.
|
||||||
|
if presente_scan:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Se não apareceu no scan porque já está em uso, NÃO manda iniciar de novo.
|
||||||
|
if status_scan == "provavelmente_em_uso":
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Segurança extra: se está velha, não inicia.
|
||||||
|
if idade > 30:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def definir_saude_camera(mx_id: str, status: StatusModulo, saude: int, motivos: list, rodando: bool, performance: dict, conectado: bool, ts=None, disp: T_Code = None):
|
def definir_saude_camera(mx_id: str, status: StatusModulo, saude: int, motivos: list, rodando: bool, performance: dict, conectado: bool, ts=None, disp: T_Code = None, imu: dict = None):
|
||||||
#print(f"Definindo saude da camera {mx_id}")
|
#print(f"Definindo saude da camera {mx_id}")
|
||||||
_camera = ContextoGlobalRedis.get_camera(mx_id) or {}
|
_camera = ContextoGlobalRedis.get_camera(mx_id) or {}
|
||||||
#print(_cameras)
|
#print(_cameras)
|
||||||
|
|
@ -78,9 +197,12 @@ def definir_saude_camera(mx_id: str, status: StatusModulo, saude: int, motivos:
|
||||||
}
|
}
|
||||||
#if not conectado:
|
#if not conectado:
|
||||||
# _cameras[mx_id] = {}
|
# _cameras[mx_id] = {}
|
||||||
|
if rodando:
|
||||||
|
_camera["timestamp"] = time.time()
|
||||||
_camera["saude"] = saude_geral
|
_camera["saude"] = saude_geral
|
||||||
_camera["rodando"] = rodando
|
_camera["rodando"] = rodando
|
||||||
_camera["performance"] = performance
|
_camera["performance"] = performance
|
||||||
|
_camera["imu"] = imu
|
||||||
if disp is not None:
|
if disp is not None:
|
||||||
_camera["dispositivo"] = disp.value
|
_camera["dispositivo"] = disp.value
|
||||||
disp = T_Code(_camera.get("dispositivo", T_Code.Vzo.value))
|
disp = T_Code(_camera.get("dispositivo", T_Code.Vzo.value))
|
||||||
|
|
@ -90,4 +212,18 @@ def definir_saude_camera(mx_id: str, status: StatusModulo, saude: int, motivos:
|
||||||
saude=saude_geral
|
saude=saude_geral
|
||||||
)
|
)
|
||||||
ContextoGlobalRedis.set(ContextoGlobalRedis.CamKey(mx_id), _camera)
|
ContextoGlobalRedis.set(ContextoGlobalRedis.CamKey(mx_id), _camera)
|
||||||
|
|
||||||
|
def definir_imu_camera(mx_id: str, dados: dict, saude: dict):
|
||||||
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
|
ContextoGlobalRedis.CamImuKey(mx_id),
|
||||||
|
timestamp=time.time(),
|
||||||
|
dados=dados,
|
||||||
|
saude=saude
|
||||||
|
)
|
||||||
|
|
||||||
|
def definir_heartbeat_camera(mx_id: str):
|
||||||
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
|
ContextoGlobalRedis.CamKey(mx_id),
|
||||||
|
timestamp=time.time()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,620 @@
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
except Exception:
|
||||||
|
torch = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Ajuste de import local
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
THIS_FILE = Path(__file__).resolve()
|
||||||
|
|
||||||
|
# Esperado:
|
||||||
|
# .../Python/Scripts/workers/camera_worker/oak_fcc3_core/benchmark_raw_bruto_scientific.py
|
||||||
|
WORKERS_DIR = THIS_FILE.parents[2]
|
||||||
|
|
||||||
|
if str(WORKERS_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(WORKERS_DIR))
|
||||||
|
|
||||||
|
from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client
|
||||||
|
|
||||||
|
try:
|
||||||
|
from camera_worker.oak_fcc3_core.segformer_service import MultiSpecSegformerService
|
||||||
|
except Exception:
|
||||||
|
MultiSpecSegformerService = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Utils
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def now_ms():
|
||||||
|
return time.perf_counter() * 1000.0
|
||||||
|
|
||||||
|
|
||||||
|
def mean(xs):
|
||||||
|
return float(np.mean(xs)) if xs else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def p95(xs):
|
||||||
|
return float(np.percentile(xs, 95)) if xs else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def maxv(xs):
|
||||||
|
return float(np.max(xs)) if xs else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def last_mean(xs, n=30):
|
||||||
|
return float(np.mean(xs[-n:])) if xs else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def last_max(xs, n=30):
|
||||||
|
return float(np.max(xs[-n:])) if xs else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_if_exists(path):
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_float_list(s, expected=5, default=None):
|
||||||
|
if s is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
vals = [float(x.strip()) for x in str(s).split(",") if x.strip() != ""]
|
||||||
|
|
||||||
|
if len(vals) != expected:
|
||||||
|
raise ValueError(f"Esperado {expected} valores, veio {len(vals)}: {s}")
|
||||||
|
|
||||||
|
return np.array(vals, dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_mean_std_from_model_config(cfg):
|
||||||
|
if not isinstance(cfg, dict):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
mean_cfg = cfg.get("mean") or cfg.get("channel_mean") or cfg.get("norm_mean")
|
||||||
|
std_cfg = cfg.get("std") or cfg.get("channel_std") or cfg.get("norm_std")
|
||||||
|
|
||||||
|
norm = cfg.get("normalization") or cfg.get("norm") or {}
|
||||||
|
|
||||||
|
if mean_cfg is None and isinstance(norm, dict):
|
||||||
|
mean_cfg = norm.get("mean") or norm.get("channel_mean")
|
||||||
|
|
||||||
|
if std_cfg is None and isinstance(norm, dict):
|
||||||
|
std_cfg = norm.get("std") or norm.get("channel_std")
|
||||||
|
|
||||||
|
if mean_cfg is None or std_cfg is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
mean_arr = np.array(mean_cfg, dtype=np.float32)
|
||||||
|
std_arr = np.array(std_cfg, dtype=np.float32)
|
||||||
|
|
||||||
|
if mean_arr.size != 5 or std_arr.size != 5:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return mean_arr, std_arr
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_stats(tensor):
|
||||||
|
names = ["R", "G", "B", "RE", "NIR"]
|
||||||
|
out = {}
|
||||||
|
|
||||||
|
for i, name in enumerate(names):
|
||||||
|
ch = tensor[i].astype(np.float32)
|
||||||
|
out[name] = {
|
||||||
|
"min": float(np.min(ch)),
|
||||||
|
"p01": float(np.percentile(ch, 1)),
|
||||||
|
"p50": float(np.percentile(ch, 50)),
|
||||||
|
"p99": float(np.percentile(ch, 99)),
|
||||||
|
"max": float(np.max(ch)),
|
||||||
|
"mean": float(np.mean(ch)),
|
||||||
|
"std": float(np.std(ch)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def print_tensor_stats(label, tensor):
|
||||||
|
print("============================================")
|
||||||
|
print(f"[{label}] TENSOR")
|
||||||
|
print(f"shape={tensor.shape} dtype={tensor.dtype}")
|
||||||
|
|
||||||
|
stats = tensor_stats(tensor)
|
||||||
|
for ch, s in stats.items():
|
||||||
|
print(
|
||||||
|
f"{ch:>3} | "
|
||||||
|
f"min={s['min']:.4f} "
|
||||||
|
f"p01={s['p01']:.4f} "
|
||||||
|
f"p50={s['p50']:.4f} "
|
||||||
|
f"p99={s['p99']:.4f} "
|
||||||
|
f"max={s['max']:.4f} "
|
||||||
|
f"mean={s['mean']:.4f} "
|
||||||
|
f"std={s['std']:.4f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("============================================")
|
||||||
|
|
||||||
|
|
||||||
|
def to_u8_01(arr):
|
||||||
|
arr = np.asarray(arr, dtype=np.float32)
|
||||||
|
arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=0.0)
|
||||||
|
arr = np.clip(arr, 0.0, 1.0)
|
||||||
|
return (arr * 255.0).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
def make_panel(tensor):
|
||||||
|
rgb = np.stack([tensor[0], tensor[1], tensor[2]], axis=2)
|
||||||
|
rgb_bgr = cv2.cvtColor(to_u8_01(rgb), cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
|
re_bgr = cv2.cvtColor(to_u8_01(tensor[3]), cv2.COLOR_GRAY2BGR)
|
||||||
|
nir_bgr = cv2.cvtColor(to_u8_01(tensor[4]), cv2.COLOR_GRAY2BGR)
|
||||||
|
|
||||||
|
false_rgb = np.stack(
|
||||||
|
[
|
||||||
|
tensor[4], # visual R = NIR
|
||||||
|
tensor[3], # visual G = RE
|
||||||
|
tensor[0], # visual B = R real
|
||||||
|
],
|
||||||
|
axis=2,
|
||||||
|
)
|
||||||
|
false_bgr = cv2.cvtColor(to_u8_01(false_rgb), cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
|
def title(img, text):
|
||||||
|
h, w = img.shape[:2]
|
||||||
|
bar_h = 34
|
||||||
|
bar = np.zeros((bar_h, w, 3), dtype=np.uint8)
|
||||||
|
cv2.putText(
|
||||||
|
bar,
|
||||||
|
text,
|
||||||
|
(10, 24),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.7,
|
||||||
|
(255, 255, 255),
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
return np.vstack([bar, img])
|
||||||
|
|
||||||
|
rgb_bgr = title(rgb_bgr, "RGB")
|
||||||
|
re_bgr = title(re_bgr, "RE")
|
||||||
|
nir_bgr = title(nir_bgr, "NIR")
|
||||||
|
false_bgr = title(false_bgr, "Falso color NIR/RE/R")
|
||||||
|
|
||||||
|
top = np.hstack([rgb_bgr, re_bgr])
|
||||||
|
bottom = np.hstack([nir_bgr, false_bgr])
|
||||||
|
|
||||||
|
return np.vstack([top, bottom])
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Benchmark processor
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class ScientificBenchmark:
|
||||||
|
def __init__(self, args):
|
||||||
|
self.args = args
|
||||||
|
self.target_size = (int(args.width), int(args.height))
|
||||||
|
|
||||||
|
self.model_cfg = load_json_if_exists(args.model_config_json)
|
||||||
|
mean_cfg, std_cfg = extract_mean_std_from_model_config(self.model_cfg)
|
||||||
|
|
||||||
|
if args.model_mean is not None:
|
||||||
|
self.mean = parse_float_list(args.model_mean, expected=5)
|
||||||
|
elif mean_cfg is not None:
|
||||||
|
self.mean = mean_cfg
|
||||||
|
else:
|
||||||
|
self.mean = np.array([0.5, 0.5, 0.5, 0.5, 0.5], dtype=np.float32)
|
||||||
|
|
||||||
|
if args.model_std is not None:
|
||||||
|
self.std = parse_float_list(args.model_std, expected=5)
|
||||||
|
elif std_cfg is not None:
|
||||||
|
self.std = std_cfg
|
||||||
|
else:
|
||||||
|
self.std = np.array([0.25, 0.25, 0.25, 0.25, 0.25], dtype=np.float32)
|
||||||
|
|
||||||
|
self.mean_chw = self.mean[:, None, None].astype(np.float32)
|
||||||
|
self.std_chw = self.std[:, None, None].astype(np.float32)
|
||||||
|
|
||||||
|
self.device = None
|
||||||
|
if torch is not None and torch.cuda.is_available():
|
||||||
|
self.device = torch.device("cuda")
|
||||||
|
elif torch is not None:
|
||||||
|
self.device = torch.device("cpu")
|
||||||
|
|
||||||
|
self.model_svc = None
|
||||||
|
|
||||||
|
if args.run_model:
|
||||||
|
if MultiSpecSegformerService is None:
|
||||||
|
raise RuntimeError("MultiSpecSegformerService não pôde ser importado.")
|
||||||
|
|
||||||
|
if not isinstance(self.model_cfg, dict):
|
||||||
|
raise RuntimeError("--run_model requer --model_config_json válido.")
|
||||||
|
|
||||||
|
self.model_svc = MultiSpecSegformerService(
|
||||||
|
model_config=self.model_cfg,
|
||||||
|
mostrar_log=print,
|
||||||
|
)
|
||||||
|
|
||||||
|
dummy = np.zeros((5, args.height, args.width), dtype=np.float32)
|
||||||
|
|
||||||
|
for _ in range(max(0, int(args.warmup_model))):
|
||||||
|
self.model_svc.infer_tensor_fast(dummy, keep_probs=False)
|
||||||
|
|
||||||
|
print(f"[BENCH] Warmup modelo concluído: {args.warmup_model}x")
|
||||||
|
|
||||||
|
def process_once(self, client):
|
||||||
|
"""
|
||||||
|
Mede uma iteração completa do fluxo científico.
|
||||||
|
"""
|
||||||
|
times = {
|
||||||
|
"capture_ms": 0.0,
|
||||||
|
"decode_ms": 0.0,
|
||||||
|
"controller_ms": 0.0,
|
||||||
|
"fuse_total_ms": 0.0,
|
||||||
|
"fuse_dark_ms": 0.0,
|
||||||
|
"fuse_radnorm_ms": 0.0,
|
||||||
|
"fuse_flat_ms": 0.0,
|
||||||
|
"fuse_prepare_ms": 0.0,
|
||||||
|
"fuse_warp_ms": 0.0,
|
||||||
|
"fuse_crop_resize_ms": 0.0,
|
||||||
|
"fuse_concat_ms": 0.0,
|
||||||
|
"model_norm_ms": 0.0,
|
||||||
|
"torch_ms": 0.0,
|
||||||
|
"infer_ms": 0.0,
|
||||||
|
"total_ms": 0.0,
|
||||||
|
"sync_dt_ms": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
t_total0 = now_ms()
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# 1. Captura RAW_BRUTO
|
||||||
|
# ========================================================
|
||||||
|
t0 = now_ms()
|
||||||
|
raw_frame, raw_meta = client.get_next_raw_frame(timeout=self.args.timeout)
|
||||||
|
times["capture_ms"] = now_ms() - t0
|
||||||
|
|
||||||
|
#cp = raw_meta.get("capture_perf", {})
|
||||||
|
#print(
|
||||||
|
# "[CAP_ASYNC] "
|
||||||
|
# f"get_wait={cp.get('async_get_wait_ms',0):.2f}ms "
|
||||||
|
# f"age={cp.get('async_packet_age_ms',0):.2f}ms "
|
||||||
|
# f"seq={cp.get('async_packet_seq')} "
|
||||||
|
# f"thread_wait={cp.get('wait_total_ms',0):.2f}ms "
|
||||||
|
# f"sleep={cp.get('sleep_ms',0):.2f}ms/{cp.get('sleep_count',0)} "
|
||||||
|
# f"drain={cp.get('drain_total_ms',0):.2f}ms "
|
||||||
|
# f"copy={cp.get('drain_frombuffer_copy_ms',0):.2f}ms "
|
||||||
|
# f"status={cp.get('async_status',{})}"
|
||||||
|
#)
|
||||||
|
|
||||||
|
times["sync_dt_ms"] = float(raw_meta.get("sync_dt_ms", 0.0) or 0.0)
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# 2. Decode RAW10 packed -> float científico por câmera
|
||||||
|
# ========================================================
|
||||||
|
t0 = now_ms()
|
||||||
|
decoded = client.decode_stream_cameras(raw_frame, raw_meta)
|
||||||
|
times["decode_ms"] = now_ms() - t0
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# 3. RadiometricController update, se ativo
|
||||||
|
# Isso NÃO é a radiometric_normalization do tensor.
|
||||||
|
# É o controlador de exposição/ganho.
|
||||||
|
# ========================================================
|
||||||
|
t0 = now_ms()
|
||||||
|
client.update_radiometry(decoded, raw_meta)
|
||||||
|
times["controller_ms"] = now_ms() - t0
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# 4. Fusão científica no RawProcessorCore
|
||||||
|
# dark + radnorm + flat + homografia + crop/resize + concat
|
||||||
|
# ========================================================
|
||||||
|
t0 = now_ms()
|
||||||
|
tensor = client.build_infer_tensor_from_decoded(
|
||||||
|
decoded=decoded,
|
||||||
|
meta=raw_meta,
|
||||||
|
channels_expected=5,
|
||||||
|
target_size=self.target_size,
|
||||||
|
)
|
||||||
|
times["fuse_total_ms"] = now_ms() - t0
|
||||||
|
|
||||||
|
# Pega detalhamento interno do core
|
||||||
|
try:
|
||||||
|
perf = (client.core.last_fusion_result or {}).get("perf", {}) or {}
|
||||||
|
times["fuse_dark_ms"] = float(perf.get("dark_ms", 0.0) or 0.0)
|
||||||
|
times["fuse_radnorm_ms"] = float(perf.get("radnorm_ms", 0.0) or 0.0)
|
||||||
|
times["fuse_flat_ms"] = float(perf.get("flat_ms", 0.0) or 0.0)
|
||||||
|
times["fuse_prepare_ms"] = float(perf.get("prepare_ms", 0.0) or 0.0)
|
||||||
|
times["fuse_warp_ms"] = float(perf.get("warp_total_ms", 0.0) or 0.0)
|
||||||
|
times["fuse_crop_resize_ms"] = float(perf.get("crop_resize_ms", 0.0) or 0.0)
|
||||||
|
times["fuse_concat_ms"] = float(perf.get("concat_ms", 0.0) or 0.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# 5. Normalização do modelo, opcional
|
||||||
|
# ========================================================
|
||||||
|
if self.args.simulate_model_norm:
|
||||||
|
t0 = now_ms()
|
||||||
|
tensor = (tensor - self.mean_chw) / np.maximum(self.std_chw, 1e-6)
|
||||||
|
tensor = np.ascontiguousarray(tensor, dtype=np.float32)
|
||||||
|
times["model_norm_ms"] = now_ms() - t0
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# 6. Transferência para torch/cuda, opcional
|
||||||
|
# ========================================================
|
||||||
|
if self.args.to_torch:
|
||||||
|
if torch is None:
|
||||||
|
raise RuntimeError("--to_torch requer torch instalado.")
|
||||||
|
|
||||||
|
t0 = now_ms()
|
||||||
|
x = torch.from_numpy(tensor).unsqueeze(0).to(self.device, non_blocking=True)
|
||||||
|
|
||||||
|
if self.device is not None and self.device.type == "cuda":
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
|
||||||
|
times["torch_ms"] = now_ms() - t0
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# 7. Inferência real, opcional
|
||||||
|
# ========================================================
|
||||||
|
pred = None
|
||||||
|
if self.args.run_model:
|
||||||
|
t0 = now_ms()
|
||||||
|
pred = self.model_svc.infer_tensor_fast(tensor, keep_probs=False)
|
||||||
|
times["infer_ms"] = now_ms() - t0
|
||||||
|
|
||||||
|
times["total_ms"] = now_ms() - t_total0
|
||||||
|
|
||||||
|
return tensor, pred, raw_frame, raw_meta, decoded, times
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
description="Benchmark científico OAK-FCC-3 RAW_BRUTO -> tensor multiespectral final."
|
||||||
|
)
|
||||||
|
|
||||||
|
ap.add_argument(
|
||||||
|
"--module_params",
|
||||||
|
default=r"C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\calibration\module_params.json",
|
||||||
|
help="Caminho do module_params.json.",
|
||||||
|
)
|
||||||
|
|
||||||
|
ap.add_argument("--width", type=int, default=1024, help="Largura final do tensor.")
|
||||||
|
ap.add_argument("--height", type=int, default=640, help="Altura final do tensor.")
|
||||||
|
ap.add_argument("--fps", type=float, default=30.0)
|
||||||
|
ap.add_argument("--seconds", type=float, default=20.0)
|
||||||
|
ap.add_argument("--timeout", type=float, default=3.0)
|
||||||
|
ap.add_argument("--warmup", type=int, default=5)
|
||||||
|
ap.add_argument("--mx_id", default=None)
|
||||||
|
ap.add_argument("--sync_tolerance_ms", type=float, default=25.0)
|
||||||
|
ap.add_argument("--buffer_size", type=int, default=8)
|
||||||
|
|
||||||
|
ap.add_argument("--simulate_model_norm", action="store_true")
|
||||||
|
ap.add_argument("--model_mean", default=None)
|
||||||
|
ap.add_argument("--model_std", default=None)
|
||||||
|
ap.add_argument("--to_torch", action="store_true")
|
||||||
|
|
||||||
|
ap.add_argument("--run_model", action="store_true")
|
||||||
|
ap.add_argument("--model_config_json", default=None)
|
||||||
|
ap.add_argument("--warmup_model", type=int, default=3)
|
||||||
|
|
||||||
|
ap.add_argument("--save_debug", action="store_true")
|
||||||
|
ap.add_argument("--debug_dir", default="raw_bruto_scientific_benchmark")
|
||||||
|
ap.add_argument("--show", action="store_true")
|
||||||
|
ap.add_argument("--display_scale", type=float, default=0.65)
|
||||||
|
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
bench = ScientificBenchmark(args)
|
||||||
|
|
||||||
|
client = OakFcc3Client(
|
||||||
|
width=args.width,
|
||||||
|
height=args.height,
|
||||||
|
fps=args.fps,
|
||||||
|
frame_type="RAW_BRUTO",
|
||||||
|
output_dtype="uint8",
|
||||||
|
capture_mode="TRIPLE",
|
||||||
|
raw_policy="require_triple",
|
||||||
|
module_calibration_json=args.module_params,
|
||||||
|
sync_tolerance_ms=args.sync_tolerance_ms,
|
||||||
|
buffer_size=args.buffer_size,
|
||||||
|
mx_id=args.mx_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.core.warmup_numba_raw10_decode()
|
||||||
|
|
||||||
|
samples = {
|
||||||
|
"capture_ms": [],
|
||||||
|
"decode_ms": [],
|
||||||
|
"controller_ms": [],
|
||||||
|
"fuse_total_ms": [],
|
||||||
|
"fuse_dark_ms": [],
|
||||||
|
"fuse_radnorm_ms": [],
|
||||||
|
"fuse_flat_ms": [],
|
||||||
|
"fuse_prepare_ms": [],
|
||||||
|
"fuse_warp_ms": [],
|
||||||
|
"fuse_crop_resize_ms": [],
|
||||||
|
"fuse_concat_ms": [],
|
||||||
|
"model_norm_ms": [],
|
||||||
|
"torch_ms": [],
|
||||||
|
"infer_ms": [],
|
||||||
|
"total_ms": [],
|
||||||
|
"sync_dt_ms": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
n_frames = 0
|
||||||
|
t_start = time.perf_counter()
|
||||||
|
t_last_log = t_start
|
||||||
|
|
||||||
|
last_tensor = None
|
||||||
|
last_meta = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.start(print_debug=True)
|
||||||
|
|
||||||
|
# Warmup de câmera/controlador/filas
|
||||||
|
print(f"[BENCH] Warmup frames: {args.warmup}")
|
||||||
|
for _ in range(max(0, int(args.warmup))):
|
||||||
|
try:
|
||||||
|
bench.process_once(client)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARN] warmup falhou: {type(e).__name__}: {e}")
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
print("============================================")
|
||||||
|
print("[BENCH] Iniciando benchmark científico RAW_BRUTO")
|
||||||
|
print(f"target tensor : (5,{args.height},{args.width})")
|
||||||
|
print(f"duration : {args.seconds}s")
|
||||||
|
print("============================================")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = time.perf_counter()
|
||||||
|
elapsed = now - t_start
|
||||||
|
|
||||||
|
if elapsed >= args.seconds:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
tensor, pred, raw_frame, raw_meta, decoded, times = bench.process_once(client)
|
||||||
|
except TimeoutError as e:
|
||||||
|
print(f"[TIMEOUT] {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
n_frames += 1
|
||||||
|
last_tensor = tensor
|
||||||
|
last_meta = raw_meta
|
||||||
|
|
||||||
|
for k in samples:
|
||||||
|
samples[k].append(float(times.get(k, 0.0) or 0.0))
|
||||||
|
|
||||||
|
if now - t_last_log >= 1.0:
|
||||||
|
elapsed = now - t_start
|
||||||
|
fps = n_frames / max(elapsed, 1e-6)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[RAW_SCI_PERF] "
|
||||||
|
f"elapsed={elapsed:.1f}s "
|
||||||
|
f"frames={n_frames} "
|
||||||
|
f"fps={fps:.2f} "
|
||||||
|
f"sync={last_mean(samples['sync_dt_ms']):.2f}ms "
|
||||||
|
f"capture={last_mean(samples['capture_ms']):.2f}ms "
|
||||||
|
f"decode={last_mean(samples['decode_ms']):.2f}ms "
|
||||||
|
f"controller={last_mean(samples['controller_ms']):.2f}ms "
|
||||||
|
f"fuse={last_mean(samples['fuse_total_ms']):.2f}ms "
|
||||||
|
f"radnorm={last_mean(samples['fuse_radnorm_ms']):.2f}ms "
|
||||||
|
f"flat={last_mean(samples['fuse_flat_ms']):.2f}ms "
|
||||||
|
f"warp={last_mean(samples['fuse_warp_ms']):.2f}ms "
|
||||||
|
f"crop_resize={last_mean(samples['fuse_crop_resize_ms']):.2f}ms "
|
||||||
|
f"concat={last_mean(samples['fuse_concat_ms']):.2f}ms "
|
||||||
|
f"model_norm={last_mean(samples['model_norm_ms']):.2f}ms "
|
||||||
|
f"torch={last_mean(samples['torch_ms']):.2f}ms "
|
||||||
|
f"infer={last_mean(samples['infer_ms']):.2f}ms "
|
||||||
|
f"total={last_mean(samples['total_ms']):.2f}ms "
|
||||||
|
f"tensor_shape={tuple(tensor.shape)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
t_last_log = now
|
||||||
|
|
||||||
|
elapsed_total = time.perf_counter() - t_start
|
||||||
|
fps_total = n_frames / max(elapsed_total, 1e-6)
|
||||||
|
|
||||||
|
print("============================================")
|
||||||
|
print("RESULTADO FINAL RAW_BRUTO CIENTÍFICO")
|
||||||
|
print(f"elapsed : {elapsed_total:.2f}s")
|
||||||
|
print(f"frames : {n_frames}")
|
||||||
|
print(f"fps : {fps_total:.2f}")
|
||||||
|
print("--------------------------------------------")
|
||||||
|
|
||||||
|
def print_metric(name):
|
||||||
|
xs = samples[name]
|
||||||
|
print(
|
||||||
|
f"{name:18s} "
|
||||||
|
f"mean={mean(xs):8.2f}ms "
|
||||||
|
f"p95={p95(xs):8.2f}ms "
|
||||||
|
f"max={maxv(xs):8.2f}ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in [
|
||||||
|
"sync_dt_ms",
|
||||||
|
"capture_ms",
|
||||||
|
"decode_ms",
|
||||||
|
"controller_ms",
|
||||||
|
"fuse_total_ms",
|
||||||
|
"fuse_dark_ms",
|
||||||
|
"fuse_radnorm_ms",
|
||||||
|
"fuse_flat_ms",
|
||||||
|
"fuse_prepare_ms",
|
||||||
|
"fuse_warp_ms",
|
||||||
|
"fuse_crop_resize_ms",
|
||||||
|
"fuse_concat_ms",
|
||||||
|
"model_norm_ms",
|
||||||
|
"torch_ms",
|
||||||
|
"infer_ms",
|
||||||
|
"total_ms",
|
||||||
|
]:
|
||||||
|
print_metric(name)
|
||||||
|
|
||||||
|
print("============================================")
|
||||||
|
|
||||||
|
if last_tensor is not None:
|
||||||
|
print_tensor_stats("LAST RAW_BRUTO SCI", last_tensor)
|
||||||
|
|
||||||
|
if args.save_debug:
|
||||||
|
debug_dir = Path(args.debug_dir)
|
||||||
|
debug_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
np.save(str(debug_dir / "last_tensor.npy"), last_tensor)
|
||||||
|
|
||||||
|
stats_path = debug_dir / "last_tensor_stats.json"
|
||||||
|
with open(stats_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(tensor_stats(last_tensor), f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
panel = make_panel(last_tensor)
|
||||||
|
cv2.imwrite(str(debug_dir / "last_tensor_panel.png"), panel)
|
||||||
|
|
||||||
|
if last_meta is not None:
|
||||||
|
with open(debug_dir / "last_meta.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(last_meta, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"[SAVE] Debug salvo em: {debug_dir}")
|
||||||
|
|
||||||
|
if args.show:
|
||||||
|
panel = make_panel(last_tensor)
|
||||||
|
|
||||||
|
if args.display_scale and abs(args.display_scale - 1.0) > 1e-6:
|
||||||
|
new_w = max(1, int(panel.shape[1] * args.display_scale))
|
||||||
|
new_h = max(1, int(panel.shape[0] * args.display_scale))
|
||||||
|
panel = cv2.resize(panel, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
||||||
|
|
||||||
|
cv2.imshow("RAW_BRUTO scientific tensor", panel)
|
||||||
|
print("[INFO] Pressione qualquer tecla para fechar.")
|
||||||
|
cv2.waitKey(0)
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
client.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,645 @@
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Ajuste de import local
|
||||||
|
# ============================================================
|
||||||
|
THIS_FILE = Path(__file__).resolve()
|
||||||
|
WORKERS_DIR = THIS_FILE.parents[2]
|
||||||
|
if str(WORKERS_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(WORKERS_DIR))
|
||||||
|
|
||||||
|
from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client
|
||||||
|
|
||||||
|
try:
|
||||||
|
from camera_worker.oak_fcc3_core.segformer_service import MultiSpecSegformerService
|
||||||
|
except Exception:
|
||||||
|
MultiSpecSegformerService = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# FPS / utilidades
|
||||||
|
# ============================================================
|
||||||
|
class FpsMeter:
|
||||||
|
def __init__(self, alpha=0.15):
|
||||||
|
self.alpha = float(alpha)
|
||||||
|
self.last_ts = None
|
||||||
|
self.fps = 0.0
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
now = time.perf_counter()
|
||||||
|
if self.last_ts is not None:
|
||||||
|
dt = now - self.last_ts
|
||||||
|
if dt > 1e-9:
|
||||||
|
inst = 1.0 / dt
|
||||||
|
self.fps = inst if self.fps <= 0 else (1.0 - self.alpha) * self.fps + self.alpha * inst
|
||||||
|
self.last_ts = now
|
||||||
|
return self.fps
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_if_exists(path):
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def init_model_service(args):
|
||||||
|
"""
|
||||||
|
Mesmo contrato do benchmark científico:
|
||||||
|
--run_model exige --model_config_json
|
||||||
|
MultiSpecSegformerService(model_config=cfg).infer_tensor_fast(tensor, keep_probs=False)
|
||||||
|
"""
|
||||||
|
if not args.run_model:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if MultiSpecSegformerService is None:
|
||||||
|
raise RuntimeError("MultiSpecSegformerService não pôde ser importado.")
|
||||||
|
|
||||||
|
model_cfg = load_json_if_exists(args.model_config_json)
|
||||||
|
if not isinstance(model_cfg, dict):
|
||||||
|
raise RuntimeError("--run_model requer --model_config_json válido.")
|
||||||
|
|
||||||
|
svc = MultiSpecSegformerService(
|
||||||
|
model_config=model_cfg,
|
||||||
|
mostrar_log=print,
|
||||||
|
)
|
||||||
|
|
||||||
|
dummy = np.zeros((5, int(args.height), int(args.width)), dtype=np.float32)
|
||||||
|
for _ in range(max(0, int(args.warmup_model))):
|
||||||
|
svc.infer_tensor_fast(dummy, keep_probs=False)
|
||||||
|
|
||||||
|
print(f"[MODEL] Warmup concluído: {args.warmup_model}x")
|
||||||
|
return svc
|
||||||
|
|
||||||
|
|
||||||
|
def to_u8_01(arr, auto_level=False):
|
||||||
|
arr = np.asarray(arr, dtype=np.float32)
|
||||||
|
arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=0.0)
|
||||||
|
|
||||||
|
if auto_level:
|
||||||
|
p1 = float(np.percentile(arr, 1))
|
||||||
|
p99 = float(np.percentile(arr, 99))
|
||||||
|
den = max(p99 - p1, 1e-6)
|
||||||
|
arr = (arr - p1) / den
|
||||||
|
|
||||||
|
arr = np.clip(arr, 0.0, 1.0)
|
||||||
|
return (arr * 255.0).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_bgr(img, auto_level=False):
|
||||||
|
img = np.asarray(img)
|
||||||
|
|
||||||
|
if img.ndim == 2:
|
||||||
|
g = to_u8_01(img, auto_level=auto_level)
|
||||||
|
return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR)
|
||||||
|
|
||||||
|
if img.ndim == 3 and img.shape[2] == 3:
|
||||||
|
u8 = to_u8_01(img, auto_level=auto_level)
|
||||||
|
# decoded/tensor RGB vem em RGB; OpenCV mostra BGR
|
||||||
|
return cv2.cvtColor(u8, cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Imagem inválida para visualização: shape={img.shape}")
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_rgb_to_bgr(tensor, auto_level=False):
|
||||||
|
rgb = np.stack([tensor[0], tensor[1], tensor[2]], axis=2)
|
||||||
|
return ensure_bgr(rgb, auto_level=auto_level)
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_channel_to_bgr(tensor, idx, auto_level=False):
|
||||||
|
return ensure_bgr(tensor[idx], auto_level=auto_level)
|
||||||
|
|
||||||
|
|
||||||
|
def add_title(img, title, color=(255, 255, 255)):
|
||||||
|
out = img.copy()
|
||||||
|
h, w = out.shape[:2]
|
||||||
|
bar_h = 34
|
||||||
|
bar = np.zeros((bar_h, w, 3), dtype=np.uint8)
|
||||||
|
cv2.putText(bar, str(title), (10, 23), cv2.FONT_HERSHEY_SIMPLEX, 0.62, color, 2, cv2.LINE_AA)
|
||||||
|
return np.vstack([bar, out])
|
||||||
|
|
||||||
|
|
||||||
|
def add_hud(panel, lines):
|
||||||
|
out = panel.copy()
|
||||||
|
x, y = 12, 45
|
||||||
|
for line in lines:
|
||||||
|
cv2.putText(out, line, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.62, (0, 255, 255), 2, cv2.LINE_AA)
|
||||||
|
y += 24
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def resize_tile(img, tile_w, tile_h):
|
||||||
|
return cv2.resize(img, (int(tile_w), int(tile_h)), interpolation=cv2.INTER_AREA)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cam_by_role(decoded, role):
|
||||||
|
role = str(role).lower()
|
||||||
|
for cam_id, item in decoded.items():
|
||||||
|
r = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
|
||||||
|
if r == role:
|
||||||
|
return cam_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def decoded_raw_tiles(decoded, tile_w, tile_h, auto_level=False):
|
||||||
|
"""
|
||||||
|
Retorna tiles RAW/decoded para RGB, RE, NIR antes da fusão final.
|
||||||
|
Aqui 'RAW_BRUTO' significa o conteúdo decodificado vindo das câmeras, ainda no espaço nativo.
|
||||||
|
"""
|
||||||
|
tiles = {}
|
||||||
|
|
||||||
|
rgb_id = get_cam_by_role(decoded, "rgb")
|
||||||
|
re_id = get_cam_by_role(decoded, "re")
|
||||||
|
nir_id = get_cam_by_role(decoded, "nir")
|
||||||
|
|
||||||
|
if rgb_id is not None:
|
||||||
|
img = decoded[rgb_id]["image"]
|
||||||
|
tiles["rgb"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h)
|
||||||
|
else:
|
||||||
|
tiles["rgb"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
if re_id is not None:
|
||||||
|
img = decoded[re_id]["image"]
|
||||||
|
tiles["re"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h)
|
||||||
|
else:
|
||||||
|
tiles["re"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
if nir_id is not None:
|
||||||
|
img = decoded[nir_id]["image"]
|
||||||
|
tiles["nir"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h)
|
||||||
|
else:
|
||||||
|
tiles["nir"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
return tiles
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_tiles(tensor, tile_w, tile_h, auto_level=False):
|
||||||
|
return {
|
||||||
|
"rgb": resize_tile(tensor_rgb_to_bgr(tensor, auto_level=auto_level), tile_w, tile_h),
|
||||||
|
"re": resize_tile(tensor_channel_to_bgr(tensor, 3, auto_level=auto_level), tile_w, tile_h),
|
||||||
|
"nir": resize_tile(tensor_channel_to_bgr(tensor, 4, auto_level=auto_level), tile_w, tile_h),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def colorize_label_map(label_map, num_classes=None):
|
||||||
|
label = np.asarray(label_map)
|
||||||
|
if label.ndim == 3:
|
||||||
|
label = np.argmax(label, axis=0)
|
||||||
|
label = label.astype(np.int32)
|
||||||
|
|
||||||
|
if num_classes is None:
|
||||||
|
num_classes = int(max(1, label.max() + 1))
|
||||||
|
|
||||||
|
# Paleta simples e estável. BGR.
|
||||||
|
palette = np.array([
|
||||||
|
[40, 40, 40],
|
||||||
|
[60, 180, 60],
|
||||||
|
[60, 60, 220],
|
||||||
|
[220, 180, 60],
|
||||||
|
[180, 60, 180],
|
||||||
|
[180, 180, 60],
|
||||||
|
[60, 180, 180],
|
||||||
|
[220, 220, 220],
|
||||||
|
], dtype=np.uint8)
|
||||||
|
|
||||||
|
out = palette[label % len(palette)]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def try_extract_prediction_tiles(pred, target_w, target_h):
|
||||||
|
"""
|
||||||
|
Tentativa genérica. Adapte aqui se o benchmark tiver nomes específicos das cabeças.
|
||||||
|
Retorna até 3 tiles BGR: semântica/head0/head1.
|
||||||
|
"""
|
||||||
|
if pred is None:
|
||||||
|
blank = np.zeros((target_h, target_w, 3), dtype=np.uint8)
|
||||||
|
return [blank, blank.copy(), blank.copy()], ["Pred vazio", "Head 1", "Head 2"]
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
names = []
|
||||||
|
|
||||||
|
if isinstance(pred, dict):
|
||||||
|
# nomes comuns
|
||||||
|
for key in ("mask", "pred_mask", "class_map", "semantic", "semantic_mask", "segmentation"):
|
||||||
|
if key in pred:
|
||||||
|
candidates.append(pred[key])
|
||||||
|
names.append(key)
|
||||||
|
|
||||||
|
heads = pred.get("heads") or pred.get("head_outputs") or pred.get("predictions")
|
||||||
|
if isinstance(heads, dict):
|
||||||
|
for k, v in heads.items():
|
||||||
|
candidates.append(v)
|
||||||
|
names.append(str(k))
|
||||||
|
elif isinstance(heads, (list, tuple)):
|
||||||
|
for i, v in enumerate(heads):
|
||||||
|
candidates.append(v)
|
||||||
|
names.append(f"head_{i}")
|
||||||
|
else:
|
||||||
|
candidates.append(pred)
|
||||||
|
names.append("prediction")
|
||||||
|
|
||||||
|
tiles = []
|
||||||
|
out_names = []
|
||||||
|
|
||||||
|
for name, arr in zip(names, candidates):
|
||||||
|
arr = np.asarray(arr)
|
||||||
|
|
||||||
|
# remove batch se existir
|
||||||
|
if arr.ndim == 4 and arr.shape[0] == 1:
|
||||||
|
arr = arr[0]
|
||||||
|
|
||||||
|
if arr.ndim == 3:
|
||||||
|
# CHW logits/probs ou HWC RGB/probs
|
||||||
|
if arr.shape[0] <= 32:
|
||||||
|
vis = colorize_label_map(np.argmax(arr, axis=0), num_classes=arr.shape[0])
|
||||||
|
elif arr.shape[2] in (1, 3):
|
||||||
|
vis = ensure_bgr(arr[:, :, 0] if arr.shape[2] == 1 else arr, auto_level=True)
|
||||||
|
else:
|
||||||
|
vis = ensure_bgr(np.max(arr, axis=2), auto_level=True)
|
||||||
|
elif arr.ndim == 2:
|
||||||
|
# se parecer label map, colore; se parecer float, cinza auto-level
|
||||||
|
if np.issubdtype(arr.dtype, np.integer) and int(np.max(arr)) <= 64:
|
||||||
|
vis = colorize_label_map(arr)
|
||||||
|
else:
|
||||||
|
vis = ensure_bgr(arr, auto_level=True)
|
||||||
|
elif arr.ndim == 1:
|
||||||
|
# vetor de classe/score: desenha texto
|
||||||
|
vis = np.zeros((target_h, target_w, 3), dtype=np.uint8)
|
||||||
|
txt = np.array2string(arr[:8], precision=2, separator=", ")
|
||||||
|
cv2.putText(vis, txt[:80], (10, target_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
vis = resize_tile(vis, target_w, target_h)
|
||||||
|
tiles.append(vis)
|
||||||
|
out_names.append(name)
|
||||||
|
|
||||||
|
if len(tiles) >= 3:
|
||||||
|
break
|
||||||
|
|
||||||
|
while len(tiles) < 3:
|
||||||
|
tiles.append(np.zeros((target_h, target_w, 3), dtype=np.uint8))
|
||||||
|
out_names.append(f"pred_{len(tiles)}")
|
||||||
|
|
||||||
|
return tiles[:3], out_names[:3]
|
||||||
|
|
||||||
|
|
||||||
|
def try_run_model(model_svc, tensor):
|
||||||
|
"""
|
||||||
|
Inferência real, igual ao benchmark científico.
|
||||||
|
Mantém esta função isolada para adaptar fácil caso o retorno do modelo mude.
|
||||||
|
"""
|
||||||
|
if model_svc is None:
|
||||||
|
return None, "model_svc_none"
|
||||||
|
|
||||||
|
if hasattr(model_svc, "infer_tensor_fast"):
|
||||||
|
return model_svc.infer_tensor_fast(tensor, keep_probs=False), None
|
||||||
|
|
||||||
|
if hasattr(model_svc, "infer"):
|
||||||
|
return model_svc.infer(tensor), None
|
||||||
|
|
||||||
|
return None, "model_svc_sem_infer"
|
||||||
|
|
||||||
|
|
||||||
|
def build_grid(raw_tiles, final_tiles, pred_tiles=None, pred_names=None, tile_w=420, tile_h=260):
|
||||||
|
rows = []
|
||||||
|
row_defs = [
|
||||||
|
("RGB", "rgb"),
|
||||||
|
("RE", "re"),
|
||||||
|
("NIR", "nir"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (label, key) in enumerate(row_defs):
|
||||||
|
left = add_title(raw_tiles[key], f"RAW_BRUTO decoded {label}")
|
||||||
|
mid = add_title(final_tiles[key], f"Tensor final {label}")
|
||||||
|
|
||||||
|
cells = [left, mid]
|
||||||
|
|
||||||
|
if pred_tiles is not None:
|
||||||
|
name = pred_names[i] if pred_names and i < len(pred_names) else f"Pred {i}"
|
||||||
|
cells.append(add_title(pred_tiles[i], name))
|
||||||
|
|
||||||
|
# iguala altura após título
|
||||||
|
h_min = min(c.shape[0] for c in cells)
|
||||||
|
norm = [cv2.resize(c, (tile_w, h_min), interpolation=cv2.INTER_AREA) if c.shape[1] != tile_w or c.shape[0] != h_min else c for c in cells]
|
||||||
|
rows.append(np.hstack(norm))
|
||||||
|
|
||||||
|
return np.vstack(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def get_core_perf(client):
|
||||||
|
for attr in ("raw_processor", "processor", "core", "raw_processor_core"):
|
||||||
|
obj = getattr(client, attr, None)
|
||||||
|
if obj is not None and getattr(obj, "last_fusion_result", None) is not None:
|
||||||
|
return obj.last_fusion_result.get("perf", {}) or {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Estado compartilhado / workers assíncronos
|
||||||
|
# ============================================================
|
||||||
|
class SharedState:
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.running = True
|
||||||
|
self.latest_decoded = None
|
||||||
|
self.latest_meta = None
|
||||||
|
self.latest_tensor = None
|
||||||
|
self.latest_perf = {}
|
||||||
|
self.latest_pred = None
|
||||||
|
self.latest_model_warn = None
|
||||||
|
self.latest_model_ms = 0.0
|
||||||
|
self.latest_error = None
|
||||||
|
self.tensor_fps = FpsMeter()
|
||||||
|
self.model_fps = FpsMeter()
|
||||||
|
self.preview_fps = FpsMeter()
|
||||||
|
self.tensor_seq = 0
|
||||||
|
self.model_seq = 0
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
with self.lock:
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def is_running(self):
|
||||||
|
with self.lock:
|
||||||
|
return bool(self.running)
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_worker(client, state, args):
|
||||||
|
"""
|
||||||
|
Roda no talo: captura RAW_BRUTO, monta tensor final e atualiza cache.
|
||||||
|
Não depende do FPS da janela.
|
||||||
|
"""
|
||||||
|
while state.is_running():
|
||||||
|
try:
|
||||||
|
frame, meta, decoded = client.get_next_decoded(timeout=args.timeout)
|
||||||
|
if not isinstance(frame, dict):
|
||||||
|
raise RuntimeError(f"RAW_BRUTO esperado como dict. Veio {type(frame)}")
|
||||||
|
|
||||||
|
tensor = client.build_infer_tensor_from_decoded(
|
||||||
|
decoded=decoded,
|
||||||
|
meta=meta,
|
||||||
|
channels_expected=5,
|
||||||
|
target_size=(args.width, args.height),
|
||||||
|
)
|
||||||
|
tensor = np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
||||||
|
perf = get_core_perf(client)
|
||||||
|
fps = state.tensor_fps.tick()
|
||||||
|
|
||||||
|
with state.lock:
|
||||||
|
state.latest_decoded = decoded
|
||||||
|
state.latest_meta = meta
|
||||||
|
state.latest_tensor = tensor
|
||||||
|
state.latest_perf = dict(perf or {})
|
||||||
|
state.tensor_seq += 1
|
||||||
|
state.latest_error = None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
with state.lock:
|
||||||
|
state.latest_error = f"tensor_worker: {type(e).__name__}: {e}"
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
|
||||||
|
def model_worker(model_svc, state, args):
|
||||||
|
"""
|
||||||
|
Opcional: roda inferência no último tensor disponível.
|
||||||
|
Não bloqueia o worker de tensor nem a janela.
|
||||||
|
"""
|
||||||
|
last_seq = -1
|
||||||
|
|
||||||
|
while state.is_running():
|
||||||
|
with state.lock:
|
||||||
|
tensor = None if state.latest_tensor is None else state.latest_tensor.copy()
|
||||||
|
seq = state.tensor_seq
|
||||||
|
|
||||||
|
if tensor is None or seq == last_seq:
|
||||||
|
time.sleep(0.005)
|
||||||
|
continue
|
||||||
|
|
||||||
|
last_seq = seq
|
||||||
|
|
||||||
|
try:
|
||||||
|
t0_model = time.perf_counter()
|
||||||
|
pred, warn = try_run_model(model_svc, tensor)
|
||||||
|
model_ms = (time.perf_counter() - t0_model) * 1000.0
|
||||||
|
if warn is None:
|
||||||
|
state.model_fps.tick()
|
||||||
|
|
||||||
|
with state.lock:
|
||||||
|
state.latest_pred = pred
|
||||||
|
state.latest_model_warn = warn
|
||||||
|
state.latest_model_ms = float(model_ms)
|
||||||
|
state.model_seq += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
with state.lock:
|
||||||
|
state.latest_model_warn = f"model_worker: {type(e).__name__}: {e}"
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_state(state):
|
||||||
|
"""
|
||||||
|
Copia referências do cache para desenhar. A janela só roda na cadência do preview.
|
||||||
|
"""
|
||||||
|
with state.lock:
|
||||||
|
return {
|
||||||
|
"decoded": state.latest_decoded,
|
||||||
|
"meta": state.latest_meta,
|
||||||
|
"tensor": state.latest_tensor,
|
||||||
|
"perf": dict(state.latest_perf or {}),
|
||||||
|
"pred": state.latest_pred,
|
||||||
|
"model_warn": state.latest_model_warn,
|
||||||
|
"model_ms": state.latest_model_ms,
|
||||||
|
"error": state.latest_error,
|
||||||
|
"tensor_fps": state.tensor_fps.fps,
|
||||||
|
"model_fps": state.model_fps.fps,
|
||||||
|
"tensor_seq": state.tensor_seq,
|
||||||
|
"model_seq": state.model_seq,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Main loop assíncrono
|
||||||
|
# ============================================================
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Preview assíncrono RAW_BRUTO decoded vs tensor final multispectral.")
|
||||||
|
ap.add_argument("--module_params", default=r"C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\calibration\module_params.json")
|
||||||
|
ap.add_argument("--width", type=int, default=1024, help="Largura final do tensor.")
|
||||||
|
ap.add_argument("--height", type=int, default=640, help="Altura final do tensor.")
|
||||||
|
ap.add_argument("--fps", type=float, default=40.0, help="FPS alvo da câmera.")
|
||||||
|
ap.add_argument("--preview_fps", type=float, default=5.0, help="FPS da janela OpenCV apenas.")
|
||||||
|
ap.add_argument("--timeout", type=float, default=2.0)
|
||||||
|
ap.add_argument("--warmup", type=int, default=5)
|
||||||
|
ap.add_argument("--mx_id", default=None)
|
||||||
|
ap.add_argument("--display_scale", type=float, default=0.75)
|
||||||
|
ap.add_argument("--tile_w", type=int, default=420)
|
||||||
|
ap.add_argument("--tile_h", type=int, default=260)
|
||||||
|
ap.add_argument("--auto_level_raw", action="store_true")
|
||||||
|
ap.add_argument("--auto_level_tensor", action="store_true")
|
||||||
|
ap.add_argument("--run_model", action="store_true", help="Roda inferência em thread separada usando o último tensor cacheado.")
|
||||||
|
ap.add_argument("--model_config_json", default=None, help="JSON de configuração do modelo SegFormer, igual ao benchmark.")
|
||||||
|
ap.add_argument("--warmup_model", type=int, default=3, help="Número de inferências dummy para aquecer o modelo.")
|
||||||
|
ap.add_argument("--save_last", default=None)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
client = OakFcc3Client(
|
||||||
|
width=args.width,
|
||||||
|
height=args.height,
|
||||||
|
fps=args.fps,
|
||||||
|
frame_type="RAW_BRUTO",
|
||||||
|
capture_mode="TRIPLE",
|
||||||
|
raw_policy="require_triple",
|
||||||
|
module_calibration_json=args.module_params,
|
||||||
|
mx_id=args.mx_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
model_svc = init_model_service(args) if args.run_model else None
|
||||||
|
|
||||||
|
state = SharedState()
|
||||||
|
last_panel = None
|
||||||
|
last_warn_ts = 0.0
|
||||||
|
last_seq_drawn = -1
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.start(print_debug=True)
|
||||||
|
|
||||||
|
for _ in range(max(0, int(args.warmup))):
|
||||||
|
try:
|
||||||
|
client.get_next_decoded(timeout=args.timeout)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(0.03)
|
||||||
|
|
||||||
|
tw = threading.Thread(target=tensor_worker, args=(client, state, args), daemon=True)
|
||||||
|
tw.start()
|
||||||
|
|
||||||
|
mw = None
|
||||||
|
if args.run_model:
|
||||||
|
mw = threading.Thread(target=model_worker, args=(model_svc, state, args), daemon=True)
|
||||||
|
mw.start()
|
||||||
|
|
||||||
|
min_period = 1.0 / max(float(args.preview_fps), 0.1)
|
||||||
|
next_draw_ts = 0.0
|
||||||
|
|
||||||
|
print("[INFO] Preview assíncrono iniciado. Pressione Q ou ESC para sair.")
|
||||||
|
print("[INFO] Tensor FPS = geração real do tensor. Preview FPS = janela. Model FPS = inferência, se habilitada.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = time.perf_counter()
|
||||||
|
if now < next_draw_ts:
|
||||||
|
time.sleep(min(0.005, next_draw_ts - now))
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key in (27, ord('q'), ord('Q')):
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
next_draw_ts = now + min_period
|
||||||
|
snap = snapshot_state(state)
|
||||||
|
|
||||||
|
if snap["tensor"] is None or snap["decoded"] is None:
|
||||||
|
blank = np.zeros((360, 900, 3), dtype=np.uint8)
|
||||||
|
cv2.putText(blank, "Aguardando primeiro tensor...", (30, 180), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2, cv2.LINE_AA)
|
||||||
|
cv2.imshow("OAK RAW_BRUTO vs Tensor Final", blank)
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key in (27, ord('q'), ord('Q')):
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Desenha só usando o cache. Não captura nem monta tensor aqui.
|
||||||
|
t_draw0 = time.perf_counter()
|
||||||
|
raw_tiles = decoded_raw_tiles(
|
||||||
|
snap["decoded"],
|
||||||
|
tile_w=args.tile_w,
|
||||||
|
tile_h=args.tile_h,
|
||||||
|
auto_level=args.auto_level_raw,
|
||||||
|
)
|
||||||
|
final_tiles = tensor_tiles(
|
||||||
|
snap["tensor"],
|
||||||
|
tile_w=args.tile_w,
|
||||||
|
tile_h=args.tile_h,
|
||||||
|
auto_level=args.auto_level_tensor,
|
||||||
|
)
|
||||||
|
|
||||||
|
pred_tiles = None
|
||||||
|
pred_names = None
|
||||||
|
if args.run_model:
|
||||||
|
pred_tiles, pred_names = try_extract_prediction_tiles(
|
||||||
|
snap["pred"],
|
||||||
|
target_w=args.tile_w,
|
||||||
|
target_h=args.tile_h,
|
||||||
|
)
|
||||||
|
if snap["model_warn"]:
|
||||||
|
tnow = time.time()
|
||||||
|
if tnow - last_warn_ts > 2.0:
|
||||||
|
print(f"[WARN][MODEL] {snap['model_warn']}")
|
||||||
|
last_warn_ts = tnow
|
||||||
|
|
||||||
|
panel = build_grid(
|
||||||
|
raw_tiles=raw_tiles,
|
||||||
|
final_tiles=final_tiles,
|
||||||
|
pred_tiles=pred_tiles,
|
||||||
|
pred_names=pred_names,
|
||||||
|
tile_w=args.tile_w,
|
||||||
|
tile_h=args.tile_h,
|
||||||
|
)
|
||||||
|
|
||||||
|
preview_fps = state.preview_fps.tick()
|
||||||
|
draw_ms = (time.perf_counter() - t_draw0) * 1000.0
|
||||||
|
perf = snap["perf"]
|
||||||
|
meta = snap["meta"] or {}
|
||||||
|
tensor_seq = int(snap["tensor_seq"])
|
||||||
|
dropped_for_preview = max(0, tensor_seq - last_seq_drawn - 1) if last_seq_drawn >= 0 else 0
|
||||||
|
last_seq_drawn = tensor_seq
|
||||||
|
|
||||||
|
hud = [
|
||||||
|
f"Preview FPS: {preview_fps:.1f} | Tensor FPS: {snap['tensor_fps']:.1f} | Model FPS: {snap['model_fps']:.1f} | infer={snap.get('model_ms', 0.0):.1f}ms",
|
||||||
|
f"draw={draw_ms:.1f}ms flat={perf.get('flat_ms', 0):.1f} warp={perf.get('warp_total_ms', 0):.1f} crop={perf.get('crop_resize_ms', 0):.1f} fuse={perf.get('total_ms', 0):.1f}",
|
||||||
|
f"seq={tensor_seq} skipped_preview={dropped_for_preview} frame_type={meta.get('frame_type')} run_model={args.run_model}",
|
||||||
|
]
|
||||||
|
if snap["error"]:
|
||||||
|
hud.append(str(snap["error"])[:120])
|
||||||
|
|
||||||
|
panel = add_hud(panel, hud)
|
||||||
|
last_panel = panel
|
||||||
|
|
||||||
|
disp = panel
|
||||||
|
if args.display_scale and abs(args.display_scale - 1.0) > 1e-6:
|
||||||
|
disp = cv2.resize(
|
||||||
|
disp,
|
||||||
|
(max(1, int(disp.shape[1] * args.display_scale)), max(1, int(disp.shape[0] * args.display_scale))),
|
||||||
|
interpolation=cv2.INTER_AREA,
|
||||||
|
)
|
||||||
|
|
||||||
|
cv2.imshow("OAK RAW_BRUTO vs Tensor Final", disp)
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key in (27, ord('q'), ord('Q')):
|
||||||
|
break
|
||||||
|
|
||||||
|
finally:
|
||||||
|
state.stop()
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
if args.save_last and last_panel is not None:
|
||||||
|
out_path = Path(args.save_last)
|
||||||
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
cv2.imwrite(str(out_path), last_panel)
|
||||||
|
print(f"[SAVE] {out_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,672 @@
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .oak_fcc3_service import OakFcc3Service
|
||||||
|
from .raw_processor_core import RawProcessorCore
|
||||||
|
from .raw_processor_preview import RawProcessorPreview
|
||||||
|
from .radiometric_controller import RadiometricController
|
||||||
|
|
||||||
|
|
||||||
|
class OakFcc3Client:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
width=640,
|
||||||
|
height=400,
|
||||||
|
bayer="BGGR",
|
||||||
|
fps=30,
|
||||||
|
frame_type="RAW_BRUTO",
|
||||||
|
output_dtype="uint8",
|
||||||
|
capture_mode="AUTO",
|
||||||
|
raw_policy="allow_single",
|
||||||
|
module_calibration_json=None,
|
||||||
|
sync_mode="best",
|
||||||
|
sync_tolerance_ms=25.0,
|
||||||
|
mx_id=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.bayer = bayer
|
||||||
|
self.fps = fps
|
||||||
|
self.frame_type = frame_type
|
||||||
|
self.output_dtype = output_dtype
|
||||||
|
self.capture_mode = capture_mode
|
||||||
|
self.raw_policy = raw_policy
|
||||||
|
self.module_calibration_json = module_calibration_json
|
||||||
|
self.module_params = self._load_module_params(module_calibration_json)
|
||||||
|
self.fusion_config = self.module_params.get("fusion_config", {}) or {}
|
||||||
|
|
||||||
|
self.mx_id = str(mx_id) if mx_id else None
|
||||||
|
|
||||||
|
self.svc = OakFcc3Service(
|
||||||
|
timeout=10,
|
||||||
|
fps=fps,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
frame_type=frame_type,
|
||||||
|
output_dtype=output_dtype,
|
||||||
|
capture_mode=capture_mode,
|
||||||
|
raw_policy=raw_policy,
|
||||||
|
sync_mode=sync_mode,
|
||||||
|
sync_tolerance_ms=sync_tolerance_ms,
|
||||||
|
mx_id=self.mx_id,
|
||||||
|
module_calibration_json=module_calibration_json,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.applied_camera_controls = {}
|
||||||
|
self.radiometric_controller = None
|
||||||
|
self.core = RawProcessorCore(
|
||||||
|
sensor_width=width,
|
||||||
|
sensor_height=height,
|
||||||
|
bayer_pattern=bayer,
|
||||||
|
calibration_json_path=module_calibration_json,
|
||||||
|
)
|
||||||
|
self.preview = RawProcessorPreview(
|
||||||
|
sensor_width=width,
|
||||||
|
sensor_height=height,
|
||||||
|
bayer_pattern=bayer,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.start()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def _load_module_params(self, path):
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def apply_module_camera_settings(self):
|
||||||
|
camera_settings = self.module_params.get("camera_settings", {}) or {}
|
||||||
|
|
||||||
|
applied = {}
|
||||||
|
|
||||||
|
for role, settings in camera_settings.items():
|
||||||
|
if not isinstance(settings, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = self.svc.apply_camera_controls(
|
||||||
|
role=role,
|
||||||
|
controls=settings,
|
||||||
|
)
|
||||||
|
applied[role] = resp
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
applied[role] = {
|
||||||
|
"ok": False,
|
||||||
|
"error": str(e),
|
||||||
|
"requested": settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.applied_camera_controls = applied
|
||||||
|
return applied
|
||||||
|
|
||||||
|
def enable_radiometric_controller(self):
|
||||||
|
self.radiometric_controller = RadiometricController(
|
||||||
|
client=self,
|
||||||
|
config_json_path=self.module_calibration_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.radiometric_controller.sync_from_camera_controls(self.applied_camera_controls)
|
||||||
|
self.radiometric_controller.sync_from_actual_camera_controls()
|
||||||
|
|
||||||
|
return self.radiometric_controller
|
||||||
|
|
||||||
|
def update_radiometry(self, decoded, meta=None):
|
||||||
|
if self.radiometric_controller is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.radiometric_controller.update(decoded, meta)
|
||||||
|
|
||||||
|
def get_current_camera_controls(self):
|
||||||
|
controls = {}
|
||||||
|
|
||||||
|
for role in ("rgb", "re", "nir"):
|
||||||
|
try:
|
||||||
|
controls[role] = self.svc.get_camera_controls(role=role)
|
||||||
|
except Exception as e:
|
||||||
|
controls[role] = {
|
||||||
|
"ok": False,
|
||||||
|
"role": role,
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
return controls
|
||||||
|
|
||||||
|
def get_radiometric_last_result(self):
|
||||||
|
if self.radiometric_controller is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.radiometric_controller.last_result
|
||||||
|
|
||||||
|
def start(self, print_debug=False):
|
||||||
|
self.svc.connect()
|
||||||
|
|
||||||
|
resp = self.svc.begin(
|
||||||
|
frame_type=self.frame_type,
|
||||||
|
output_dtype=self.output_dtype,
|
||||||
|
capture_mode=self.capture_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.mx_id = self.svc.manager.mx_id
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
applied = self.apply_module_camera_settings()
|
||||||
|
|
||||||
|
if print_debug:
|
||||||
|
print("[OAK CLIENT] START:", resp)
|
||||||
|
print("[OAK CLIENT] APPLIED CAMERA SETTINGS:", applied)
|
||||||
|
|
||||||
|
self.enable_radiometric_controller()
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
try:
|
||||||
|
self.svc.stop()
|
||||||
|
finally:
|
||||||
|
self.svc.disconnect()
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
return self.svc.get_status()
|
||||||
|
|
||||||
|
def get_next_raw_frame(self, timeout=1.0):
|
||||||
|
return self.svc.capture_frame(timeout=timeout)
|
||||||
|
|
||||||
|
def get_next_frame(self, timeout=2.0):
|
||||||
|
frame, meta, _ = self.get_next_decoded(timeout=timeout)
|
||||||
|
return frame, meta
|
||||||
|
|
||||||
|
def get_next_decoded(self, timeout=2.0):
|
||||||
|
raw_frame, raw_meta = self.get_next_raw_frame(timeout=timeout)
|
||||||
|
|
||||||
|
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()
|
||||||
|
meta = dict(raw_meta)
|
||||||
|
|
||||||
|
if frame_type == "RAW_BRUTO":
|
||||||
|
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
|
||||||
|
|
||||||
|
self.update_radiometry(decoded, raw_meta)
|
||||||
|
|
||||||
|
frame = raw_frame
|
||||||
|
return frame, meta, decoded
|
||||||
|
|
||||||
|
elif frame_type == "RGB":
|
||||||
|
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
|
||||||
|
|
||||||
|
self.update_radiometry(decoded, raw_meta)
|
||||||
|
|
||||||
|
frame = self.build_rgb_tensor(decoded)
|
||||||
|
meta["output_layout"] = "CHW"
|
||||||
|
meta["channels"] = ["R", "G", "B"]
|
||||||
|
meta["shape"] = list(frame.shape)
|
||||||
|
meta["dtype"] = str(frame.dtype)
|
||||||
|
|
||||||
|
return frame, meta, decoded
|
||||||
|
|
||||||
|
elif frame_type == "MULTISPEC":
|
||||||
|
decoded = self.decode_oak_aligned_multispec(raw_frame, raw_meta)
|
||||||
|
|
||||||
|
self.update_radiometry(decoded, raw_meta)
|
||||||
|
|
||||||
|
# Versão inicial segura:
|
||||||
|
# não chama core.fuse_multispec_cameras(), porque ali teria homografia de novo.
|
||||||
|
frame = self.build_multispec_tensor_from_oak_aligned(decoded, meta=raw_meta)
|
||||||
|
|
||||||
|
meta["output_layout"] = "CHW"
|
||||||
|
meta["channels"] = ["R", "G", "B", "RE", "NIR"]
|
||||||
|
meta["shape"] = list(frame.shape)
|
||||||
|
meta["dtype"] = str(frame.dtype)
|
||||||
|
meta["aligned_by_oak"] = True
|
||||||
|
meta["geometry_stage"] = "oak"
|
||||||
|
|
||||||
|
return frame, meta, decoded
|
||||||
|
|
||||||
|
elif frame_type == "PREVIEW":
|
||||||
|
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
|
||||||
|
frame = raw_frame
|
||||||
|
return frame, meta, decoded
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"frame_type não suportado: {frame_type}")
|
||||||
|
|
||||||
|
def get_next_tensor_preview(self, timeout=2.0):
|
||||||
|
frame, meta, decoded = self.get_next_decoded(timeout=timeout)
|
||||||
|
|
||||||
|
frame_type = str(meta.get("frame_type", self.frame_type)).upper()
|
||||||
|
|
||||||
|
if frame_type == "RGB":
|
||||||
|
rgb_hwc = np.transpose(frame[:3], (1, 2, 0))
|
||||||
|
preview = self._rgb01_to_bgr(rgb_hwc)
|
||||||
|
return {"rgb_tensor": preview}, meta
|
||||||
|
|
||||||
|
if frame_type == "MULTISPEC":
|
||||||
|
rgb_hwc = np.transpose(frame[:3], (1, 2, 0))
|
||||||
|
re01 = frame[3]
|
||||||
|
nir01 = frame[4]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"rgb_tensor": self._rgb01_to_bgr(rgb_hwc),
|
||||||
|
"re_tensor": self._gray01_to_bgr(re01),
|
||||||
|
"nir_tensor": self._gray01_to_bgr(nir01),
|
||||||
|
}, meta
|
||||||
|
|
||||||
|
else:
|
||||||
|
return self.build_visual_preview_from_raw(frame, meta), meta
|
||||||
|
|
||||||
|
raise RuntimeError(f"frame_type não suportado para preview: {frame_type}")
|
||||||
|
|
||||||
|
def get_next_preview(self, timeout=2.0):
|
||||||
|
raw_frame, raw_meta = self.get_next_raw_frame(timeout=timeout)
|
||||||
|
|
||||||
|
meta = dict(raw_meta)
|
||||||
|
|
||||||
|
previews = self.build_visual_preview_from_raw(raw_frame, meta)
|
||||||
|
|
||||||
|
return previews, meta
|
||||||
|
|
||||||
|
def get_last_patch_normalization_result(self):
|
||||||
|
try:
|
||||||
|
return self.core.last_patch_normalization_result
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_last_radiometric_normalization_result(self):
|
||||||
|
try:
|
||||||
|
return self.core.get_last_radiometric_normalization_result()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_infer_tensor(self, frame, meta, channels_expected, target_size=None):
|
||||||
|
return self.core.build_infer_tensor_from_stream(
|
||||||
|
frame,
|
||||||
|
meta,
|
||||||
|
channels_expected=channels_expected,
|
||||||
|
target_size=target_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_infer_tensor_from_decoded(self, decoded, meta, channels_expected, target_size=None):
|
||||||
|
tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected)
|
||||||
|
return self.core.resize_tensor_chw(tensor, target_size=target_size)
|
||||||
|
|
||||||
|
def decode_stream_cameras(self, frame, meta):
|
||||||
|
if str(meta.get("frame_type", self.frame_type)).upper() == "PREVIEW":
|
||||||
|
decoded = {}
|
||||||
|
|
||||||
|
camera_info = meta.get("camera_info", {}) or {}
|
||||||
|
|
||||||
|
for cam_id, img in frame.items():
|
||||||
|
info = camera_info.get(cam_id, {}) or {}
|
||||||
|
role = info.get("role", cam_id)
|
||||||
|
img01 = self._frame_to_float01(cam_id, img, role)
|
||||||
|
|
||||||
|
decoded[cam_id] = {
|
||||||
|
"name": role.upper(),
|
||||||
|
"role": role,
|
||||||
|
"image": img01,
|
||||||
|
"meta": {
|
||||||
|
"cam_id": cam_id,
|
||||||
|
"role": role,
|
||||||
|
"socket": info.get("socket"),
|
||||||
|
"sensor": info.get("sensor"),
|
||||||
|
"timestamp": (meta.get("timestamps") or {}).get(cam_id),
|
||||||
|
"shape": list(img.shape),
|
||||||
|
"dtype": str(img.dtype),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
return self.core.decode_stream_cameras(frame, meta)
|
||||||
|
|
||||||
|
def build_rgb_tensor(self, decoded):
|
||||||
|
cam_id, item = self._find_decoded_by_role(decoded, "rgb")
|
||||||
|
|
||||||
|
rgb01 = item["image"]
|
||||||
|
|
||||||
|
if rgb01.ndim != 3 or rgb01.shape[2] != 3:
|
||||||
|
raise RuntimeError(f"{cam_id} RGB inválida: shape={rgb01.shape}")
|
||||||
|
|
||||||
|
tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
|
||||||
|
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
||||||
|
|
||||||
|
def build_multispec_tensor(self, decoded, meta=None):
|
||||||
|
tensor = self.build_infer_tensor_from_decoded(
|
||||||
|
decoded=decoded,
|
||||||
|
meta=meta,
|
||||||
|
channels_expected=5,
|
||||||
|
)
|
||||||
|
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
|
||||||
|
|
||||||
|
def build_preview_from_raw_payload(self, frame, meta):
|
||||||
|
"""
|
||||||
|
Gera preview priorizando a câmera com role='rgb'.
|
||||||
|
Retorna:
|
||||||
|
preview_bgr: imagem BGR uint8 para OpenCV
|
||||||
|
payload_float_preview: tensor CHW float32 [0..1]
|
||||||
|
preview_source_id: cam_id usado
|
||||||
|
"""
|
||||||
|
decoded = self.decode_stream_cameras(frame, meta)
|
||||||
|
|
||||||
|
if not decoded:
|
||||||
|
raise RuntimeError("Nenhum frame decodificado disponível para preview.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cam_id, item = self._find_decoded_by_role(decoded, "rgb")
|
||||||
|
rgb01 = item["image"]
|
||||||
|
|
||||||
|
if rgb01.ndim != 3 or rgb01.shape[2] != 3:
|
||||||
|
raise RuntimeError(f"{cam_id} decodificada inválida para preview RGB: shape={rgb01.shape}")
|
||||||
|
|
||||||
|
preview_bgr = self._rgb01_to_bgr(rgb01)
|
||||||
|
payload_float = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
|
||||||
|
|
||||||
|
return preview_bgr, np.ascontiguousarray(payload_float), cam_id
|
||||||
|
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
first_id = list(decoded.keys())[0]
|
||||||
|
img01 = decoded[first_id]["image"]
|
||||||
|
|
||||||
|
if img01.ndim == 2:
|
||||||
|
preview_bgr = self._gray01_to_bgr(img01)
|
||||||
|
payload_float = np.stack([img01, img01, img01], axis=0).astype(np.float32)
|
||||||
|
|
||||||
|
elif img01.ndim == 3 and img01.shape[2] == 3:
|
||||||
|
preview_bgr = self._rgb01_to_bgr(img01)
|
||||||
|
payload_float = np.transpose(img01.astype(np.float32), (2, 0, 1))
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Frame decodificado inválido para preview: cam={first_id}, shape={img01.shape}")
|
||||||
|
|
||||||
|
return preview_bgr, np.ascontiguousarray(payload_float), first_id
|
||||||
|
|
||||||
|
def build_visual_preview_from_raw(self, frame, meta):
|
||||||
|
camera_info = meta.get("camera_info", {}) or {}
|
||||||
|
previews = {}
|
||||||
|
|
||||||
|
for cam_id, arr in frame.items():
|
||||||
|
info = camera_info.get(cam_id, {}) or {}
|
||||||
|
role = info.get("role", cam_id)
|
||||||
|
bit_depth = int(info.get("bit_depth", 10))
|
||||||
|
|
||||||
|
if arr.ndim == 3 and arr.shape[2] == 1:
|
||||||
|
arr = arr[:, :, 0]
|
||||||
|
|
||||||
|
if bit_depth == 10 and arr.ndim == 2:
|
||||||
|
raw16 = self.core.unpack_raw10_packed(
|
||||||
|
arr,
|
||||||
|
sensor_width=int(info.get("width", self.width)),
|
||||||
|
sensor_height=int(info.get("height", self.height)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if role == "rgb":
|
||||||
|
previews[cam_id] = self.preview.raw16_to_preview_bgr(
|
||||||
|
raw16,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
apply_wb=True,
|
||||||
|
apply_contrast=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
vis8 = self.preview.raw16_to_vis8(
|
||||||
|
raw16,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
gamma=2.2,
|
||||||
|
)
|
||||||
|
previews[cam_id] = cv2.cvtColor(vis8, cv2.COLOR_GRAY2BGR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
decoded = self.decode_stream_cameras({cam_id: arr}, {"camera_info": {cam_id: info}})
|
||||||
|
img01 = decoded[cam_id]["image"]
|
||||||
|
|
||||||
|
if img01.ndim == 2:
|
||||||
|
previews[cam_id] = self._gray01_to_bgr(img01)
|
||||||
|
else:
|
||||||
|
previews[cam_id] = self._rgb01_to_bgr(img01)
|
||||||
|
|
||||||
|
return previews
|
||||||
|
|
||||||
|
def build_save_preview_from_cam_a(
|
||||||
|
self,
|
||||||
|
packed_raw_by_camera: dict | None,
|
||||||
|
meta_stream: dict,
|
||||||
|
sensor_width: int,
|
||||||
|
sensor_height: int,
|
||||||
|
bayer_pattern: str,
|
||||||
|
) -> np.ndarray | None:
|
||||||
|
"""
|
||||||
|
Gera o preview salvo no mesmo padrão do 'CAM_A reconstruido'.
|
||||||
|
|
||||||
|
Usa apenas CAM_A do RAW_BRUTO:
|
||||||
|
CAM_A packed RAW10
|
||||||
|
-> unpack_raw10_packed
|
||||||
|
-> RawProcessorPreview.raw16_to_preview_bgr
|
||||||
|
|
||||||
|
Retorna BGR uint8 pronto para cv2.imwrite.
|
||||||
|
"""
|
||||||
|
if not packed_raw_by_camera or "CAM_A" not in packed_raw_by_camera:
|
||||||
|
return None
|
||||||
|
|
||||||
|
stream_meta = meta_stream or {}
|
||||||
|
|
||||||
|
camera_info = stream_meta.get("camera_info", {}) or {}
|
||||||
|
cam_meta = camera_info.get("CAM_A", {}) or {}
|
||||||
|
|
||||||
|
bit_depth = int(cam_meta.get("bit_depth", 10))
|
||||||
|
|
||||||
|
bayer = (
|
||||||
|
cam_meta.get("bayer_pattern")
|
||||||
|
or cam_meta.get("bayer")
|
||||||
|
or stream_meta.get("bayer_pattern")
|
||||||
|
or bayer_pattern
|
||||||
|
or "RGGB"
|
||||||
|
)
|
||||||
|
bayer = str(bayer).upper()
|
||||||
|
|
||||||
|
arr = packed_raw_by_camera["CAM_A"]
|
||||||
|
|
||||||
|
if arr is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
packed = arr
|
||||||
|
if packed.ndim == 3 and packed.shape[2] == 1:
|
||||||
|
packed = packed[:, :, 0]
|
||||||
|
|
||||||
|
if packed.ndim != 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Mantém a mesma lógica do validador:
|
||||||
|
# RAW10 packed => sensor_w = packed_w * 4 // 5
|
||||||
|
packed_h, packed_w = packed.shape[:2]
|
||||||
|
|
||||||
|
if bit_depth == 10:
|
||||||
|
real_w = int(cam_meta.get("width", sensor_width))
|
||||||
|
real_h = int(cam_meta.get("height", sensor_height))
|
||||||
|
|
||||||
|
# Fallback caso o meta não tenha width/height confiáveis
|
||||||
|
if real_w <= 0 or real_h <= 0:
|
||||||
|
real_w = int((packed_w * 4) // 5)
|
||||||
|
real_h = int(packed_h)
|
||||||
|
else:
|
||||||
|
real_w = int(cam_meta.get("width", sensor_width))
|
||||||
|
real_h = int(cam_meta.get("height", sensor_height))
|
||||||
|
|
||||||
|
core = RawProcessorCore(
|
||||||
|
sensor_width=real_w,
|
||||||
|
sensor_height=real_h,
|
||||||
|
bayer_pattern=bayer,
|
||||||
|
)
|
||||||
|
|
||||||
|
preview = RawProcessorPreview(
|
||||||
|
sensor_width=real_w,
|
||||||
|
sensor_height=real_h,
|
||||||
|
bayer_pattern=bayer,
|
||||||
|
)
|
||||||
|
|
||||||
|
raw16 = core.unpack_raw10_packed(
|
||||||
|
packed,
|
||||||
|
sensor_width=real_w,
|
||||||
|
sensor_height=real_h,
|
||||||
|
)
|
||||||
|
|
||||||
|
preview_bgr = preview.raw16_to_preview_bgr(
|
||||||
|
raw16,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
)
|
||||||
|
|
||||||
|
return preview_bgr
|
||||||
|
|
||||||
|
def _find_decoded_by_role(self, decoded, role):
|
||||||
|
role = str(role).lower()
|
||||||
|
|
||||||
|
for cam_id, item in decoded.items():
|
||||||
|
if str(item.get("role", "")).lower() == role:
|
||||||
|
return cam_id, item
|
||||||
|
|
||||||
|
raise RuntimeError(f"Nenhuma câmera com role={role} encontrada.")
|
||||||
|
|
||||||
|
def _frame_to_float01(self, cam_id, img, role):
|
||||||
|
if img is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
arr = img
|
||||||
|
|
||||||
|
if arr.dtype == np.uint8:
|
||||||
|
arr01 = arr.astype(np.float32) / 255.0
|
||||||
|
elif arr.dtype == np.uint16:
|
||||||
|
arr01 = arr.astype(np.float32) / 65535.0
|
||||||
|
else:
|
||||||
|
arr01 = arr.astype(np.float32)
|
||||||
|
if arr01.max() > 1.5:
|
||||||
|
arr01 = arr01 / 255.0
|
||||||
|
|
||||||
|
arr01 = np.clip(arr01, 0.0, 1.0)
|
||||||
|
|
||||||
|
if role == "rgb":
|
||||||
|
# DepthAI/OpenCV entrega BGR HWC. Calibradores esperam RGB HWC.
|
||||||
|
if arr01.ndim == 3 and arr01.shape[2] == 3:
|
||||||
|
arr01 = cv2.cvtColor((arr01 * 255).astype(np.uint8), cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
|
||||||
|
elif arr01.ndim == 2:
|
||||||
|
arr01 = np.stack([arr01, arr01, arr01], axis=2)
|
||||||
|
|
||||||
|
return arr01
|
||||||
|
|
||||||
|
# Espectrais devem virar mono HW.
|
||||||
|
if arr01.ndim == 3:
|
||||||
|
arr01 = cv2.cvtColor((arr01 * 255).astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
|
||||||
|
|
||||||
|
return arr01
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rgb01_to_bgr(rgb01):
|
||||||
|
rgb_u8 = np.clip(rgb01 * 255.0, 0, 255).astype(np.uint8)
|
||||||
|
return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _gray01_to_bgr(gray01):
|
||||||
|
g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8)
|
||||||
|
return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def decode_oak_aligned_multispec(self, frame, meta):
|
||||||
|
"""
|
||||||
|
Decodifica frames já alinhados pela OAK.
|
||||||
|
|
||||||
|
Entrada esperada:
|
||||||
|
frame = {
|
||||||
|
"CAM_A": BGR uint8 HWC,
|
||||||
|
"CAM_B": GRAY uint8 HW,
|
||||||
|
"CAM_C": GRAY uint8 HW,
|
||||||
|
}
|
||||||
|
|
||||||
|
Saída:
|
||||||
|
decoded por role, em float32 0..1.
|
||||||
|
"""
|
||||||
|
if not isinstance(frame, dict):
|
||||||
|
raise RuntimeError("MULTISPEC alinhado esperado como dict de câmeras.")
|
||||||
|
|
||||||
|
decoded = {}
|
||||||
|
camera_info = meta.get("camera_info", {}) or {}
|
||||||
|
|
||||||
|
for cam_id, img in frame.items():
|
||||||
|
info = camera_info.get(cam_id, {}) or {}
|
||||||
|
role = str(info.get("role", "")).lower()
|
||||||
|
|
||||||
|
if not role:
|
||||||
|
role = str(self.svc.manager.roles.get(cam_id, cam_id)).lower()
|
||||||
|
|
||||||
|
img01 = self._frame_to_float01(cam_id, img, role)
|
||||||
|
|
||||||
|
decoded[cam_id] = {
|
||||||
|
"name": role.upper(),
|
||||||
|
"role": role,
|
||||||
|
"image": img01,
|
||||||
|
"meta": {
|
||||||
|
**info,
|
||||||
|
"cam_id": cam_id,
|
||||||
|
"role": role,
|
||||||
|
"aligned_by_oak": True,
|
||||||
|
"geometry_stage": "oak",
|
||||||
|
"homography_applied": role in ("re", "nir"),
|
||||||
|
"crop_resize_applied": True,
|
||||||
|
"shape": list(img.shape),
|
||||||
|
"dtype": str(img.dtype),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
def build_multispec_tensor_from_oak_aligned(self, decoded, meta=None):
|
||||||
|
"""
|
||||||
|
Monta CHW [R,G,B,RE,NIR] sem reaplicar homografia.
|
||||||
|
"""
|
||||||
|
_, rgb_item = self._find_decoded_by_role(decoded, "rgb")
|
||||||
|
_, re_item = self._find_decoded_by_role(decoded, "re")
|
||||||
|
_, nir_item = self._find_decoded_by_role(decoded, "nir")
|
||||||
|
|
||||||
|
rgb = rgb_item["image"].astype(np.float32, copy=False)
|
||||||
|
re = re_item["image"].astype(np.float32, copy=False)
|
||||||
|
nir = nir_item["image"].astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
if rgb.ndim != 3 or rgb.shape[2] != 3:
|
||||||
|
raise RuntimeError(f"RGB alinhado inválido: shape={rgb.shape}")
|
||||||
|
|
||||||
|
h, w = rgb.shape[:2]
|
||||||
|
|
||||||
|
if re.ndim == 3:
|
||||||
|
re = re[:, :, 0]
|
||||||
|
|
||||||
|
if nir.ndim == 3:
|
||||||
|
nir = nir[:, :, 0]
|
||||||
|
|
||||||
|
if re.shape[:2] != (h, w):
|
||||||
|
re = cv2.resize(re, (w, h), interpolation=cv2.INTER_LINEAR)
|
||||||
|
|
||||||
|
if nir.shape[:2] != (h, w):
|
||||||
|
nir = cv2.resize(nir, (w, h), interpolation=cv2.INTER_LINEAR)
|
||||||
|
|
||||||
|
tensor = np.stack(
|
||||||
|
[
|
||||||
|
rgb[:, :, 0], # R
|
||||||
|
rgb[:, :, 1], # G
|
||||||
|
rgb[:, :, 2], # B
|
||||||
|
re,
|
||||||
|
nir,
|
||||||
|
],
|
||||||
|
axis=0,
|
||||||
|
).astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
return np.ascontiguousarray(tensor)
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,230 @@
|
||||||
|
import time
|
||||||
|
from .oak_fcc3_manager import OakFcc3Manager
|
||||||
|
|
||||||
|
|
||||||
|
class OakFcc3Service:
|
||||||
|
def __init__(self, timeout=10, **kwargs):
|
||||||
|
self.timeout = timeout
|
||||||
|
self.manager = OakFcc3Manager(**kwargs)
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.connected = True
|
||||||
|
return {"ok": True, "backend": "oak_fcc3", "connected": True}
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.stop()
|
||||||
|
self.connected = False
|
||||||
|
return {"ok": True, "connected": False}
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"backend": "oak_fcc3",
|
||||||
|
"msg": "pong",
|
||||||
|
"ts": time.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
status = self.manager.get_status()
|
||||||
|
active_ids = [c["id"] for c in status.get("cameras", [])]
|
||||||
|
active_roles = {
|
||||||
|
c.get("role"): c.get("id")
|
||||||
|
for c in status.get("cameras", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
status.update({
|
||||||
|
"ok": True,
|
||||||
|
"connected": self.connected,
|
||||||
|
"active_camera_ids": active_ids,
|
||||||
|
"active_roles": active_roles,
|
||||||
|
"camera_count_active": len(active_ids),
|
||||||
|
})
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
return {
|
||||||
|
"mx_id": self.manager.mx_id,
|
||||||
|
"ok": True,
|
||||||
|
"fps": self.manager.fps,
|
||||||
|
"width": self.manager.width,
|
||||||
|
"height": self.manager.height,
|
||||||
|
"frame_type": self.manager.frame_type,
|
||||||
|
"output_dtype": self.manager.output_dtype,
|
||||||
|
"capture_mode": self.manager.capture_mode,
|
||||||
|
"raw_policy": self.manager.raw_policy,
|
||||||
|
"sync_mode": getattr(self.manager, "sync_mode", "best"),
|
||||||
|
"sync_tolerance_ms": self.manager.sync_tolerance_ms,
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_fps(self, fps):
|
||||||
|
self._ensure_stopped_for_config()
|
||||||
|
self.manager.fps = int(fps)
|
||||||
|
return {"ok": True, "fps": self.manager.fps}
|
||||||
|
|
||||||
|
def set_resolution(self, width, height):
|
||||||
|
self._ensure_stopped_for_config()
|
||||||
|
self.manager.width = int(width)
|
||||||
|
self.manager.height = int(height)
|
||||||
|
self.manager.size = (self.manager.width, self.manager.height)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"width": self.manager.width,
|
||||||
|
"height": self.manager.height,
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_capture_mode(self, mode):
|
||||||
|
self._ensure_stopped_for_config()
|
||||||
|
mode = str(mode).upper()
|
||||||
|
self.manager.capture_mode = self._validate_capture_mode(mode)
|
||||||
|
return {"ok": True, "capture_mode": self.manager.capture_mode}
|
||||||
|
|
||||||
|
def set_frame_type(self, frame_type):
|
||||||
|
self._ensure_stopped_for_config()
|
||||||
|
frame_type = str(frame_type).upper()
|
||||||
|
self.manager.frame_type = self._validate_frame_type(frame_type)
|
||||||
|
return {"ok": True, "frame_type": self.manager.frame_type}
|
||||||
|
|
||||||
|
def set_output_dtype(self, dtype):
|
||||||
|
self._ensure_stopped_for_config()
|
||||||
|
dtype = str(dtype).lower()
|
||||||
|
self.manager.output_dtype = self._validate_output_dtype(dtype)
|
||||||
|
return {"ok": True, "output_dtype": self.manager.output_dtype}
|
||||||
|
|
||||||
|
def begin(self, frame_type=None, output_dtype=None, capture_mode=None):
|
||||||
|
if not self.connected:
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
if self.manager.running:
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"started": True,
|
||||||
|
"already_running": True,
|
||||||
|
"status": self.get_status(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame_type is not None:
|
||||||
|
self.manager.frame_type = self._validate_frame_type(frame_type)
|
||||||
|
|
||||||
|
if output_dtype is not None:
|
||||||
|
self.manager.output_dtype = self._validate_output_dtype(output_dtype)
|
||||||
|
|
||||||
|
if capture_mode is not None:
|
||||||
|
self.manager.capture_mode = self._validate_capture_mode(capture_mode)
|
||||||
|
|
||||||
|
self.manager.start()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"started": True,
|
||||||
|
"already_running": False,
|
||||||
|
"status": self.get_status(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def capture_frame(self, timeout=None):
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
|
||||||
|
frame, meta = self.manager.get_next_frame(timeout=timeout)
|
||||||
|
|
||||||
|
return frame, meta
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.manager.stop()
|
||||||
|
return {"ok": True, "stopped": True}
|
||||||
|
|
||||||
|
def _ensure_stopped_for_config(self):
|
||||||
|
if self.manager.running:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Configuração estrutural só pode ser alterada com o manager parado. "
|
||||||
|
"Chame stop() antes."
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_camera_id(self, cam_id=None, role=None):
|
||||||
|
if role is not None:
|
||||||
|
role = str(role).lower()
|
||||||
|
|
||||||
|
status = self.manager.get_status()
|
||||||
|
for cam in status.get("cameras", []):
|
||||||
|
if str(cam.get("role", "")).lower() == role:
|
||||||
|
return cam["id"]
|
||||||
|
|
||||||
|
raise ValueError(f"Nenhuma câmera ativa encontrada para role={role}")
|
||||||
|
|
||||||
|
if cam_id is None:
|
||||||
|
raise ValueError("Informe cam_id ou role.")
|
||||||
|
|
||||||
|
return str(cam_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_camera_controls(self, cam_id=None, role=None):
|
||||||
|
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
|
||||||
|
ctrl = self.manager.get_camera_controls(cam_id)
|
||||||
|
ctrl["ok"] = True
|
||||||
|
ctrl["camera_id"] = cam_id
|
||||||
|
ctrl["role"] = role
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
def set_ae_enable(self, cam_id=None, role=None, enable=False):
|
||||||
|
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
|
||||||
|
ctrl = self.manager.set_ae_enable(cam_id, bool(enable))
|
||||||
|
ctrl["ok"] = True
|
||||||
|
ctrl["camera_id"] = cam_id
|
||||||
|
ctrl["role"] = role
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
def set_awb_enable(self, cam_id=None, role=None, enable=False):
|
||||||
|
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
|
||||||
|
ctrl = self.manager.set_awb_enable(cam_id, bool(enable))
|
||||||
|
ctrl["ok"] = True
|
||||||
|
ctrl["camera_id"] = cam_id
|
||||||
|
ctrl["role"] = role
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
def set_exposure_time(self, cam_id=None, role=None, exposure_time_us=None):
|
||||||
|
if exposure_time_us is None:
|
||||||
|
raise ValueError("exposure_time_us é obrigatório.")
|
||||||
|
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
|
||||||
|
ctrl = self.manager.set_exposure_time(cam_id, int(exposure_time_us))
|
||||||
|
ctrl["ok"] = True
|
||||||
|
ctrl["camera_id"] = cam_id
|
||||||
|
ctrl["role"] = role
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
def set_analogue_gain(self, cam_id=None, role=None, analogue_gain=None):
|
||||||
|
if analogue_gain is None:
|
||||||
|
raise ValueError("analogue_gain é obrigatório.")
|
||||||
|
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
|
||||||
|
ctrl = self.manager.set_analogue_gain(cam_id, float(analogue_gain))
|
||||||
|
ctrl["ok"] = True
|
||||||
|
ctrl["camera_id"] = cam_id
|
||||||
|
ctrl["role"] = role
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
def apply_camera_controls(self, cam_id=None, role=None, controls=None):
|
||||||
|
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
|
||||||
|
ctrl = self.manager.apply_camera_controls(cam_id, controls or {})
|
||||||
|
ctrl["ok"] = True
|
||||||
|
ctrl["camera_id"] = cam_id
|
||||||
|
ctrl["role"] = role
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_frame_type(self, frame_type):
|
||||||
|
frame_type = str(frame_type).upper()
|
||||||
|
if frame_type not in ("RAW_BRUTO", "RGB", "MULTISPEC", "PREVIEW"):
|
||||||
|
raise ValueError(f"frame_type inválido: {frame_type}")
|
||||||
|
return frame_type
|
||||||
|
|
||||||
|
def _validate_output_dtype(self, dtype):
|
||||||
|
dtype = str(dtype).lower()
|
||||||
|
if dtype not in ("uint8", "uint16", "float32"):
|
||||||
|
raise ValueError(f"output_dtype inválido: {dtype}")
|
||||||
|
return dtype
|
||||||
|
|
||||||
|
def _validate_capture_mode(self, mode):
|
||||||
|
mode = str(mode).upper()
|
||||||
|
if mode not in ("AUTO", "SINGLE", "DOUBLE", "TRIPLE"):
|
||||||
|
raise ValueError(f"capture_mode inválido: {mode}")
|
||||||
|
return mode
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class RawProcessorPreview:
|
||||||
|
def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"):
|
||||||
|
self.sensor_width = sensor_width
|
||||||
|
self.sensor_height = sensor_height
|
||||||
|
self.bayer_pattern = bayer_pattern.upper()
|
||||||
|
|
||||||
|
def raw16_to_vis8(
|
||||||
|
self, raw16: np.ndarray,
|
||||||
|
black_level: Optional[int] = None,
|
||||||
|
white_level: Optional[int] = None,
|
||||||
|
gamma: float = 2.2,
|
||||||
|
bit_depth: int = 10
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Conversão para visualização:
|
||||||
|
- auto-level
|
||||||
|
- gamma
|
||||||
|
"""
|
||||||
|
max_val = float((1 << bit_depth) - 1)
|
||||||
|
|
||||||
|
raw = raw16.astype(np.float32)
|
||||||
|
|
||||||
|
if black_level is None:
|
||||||
|
black_level = float(raw.min())
|
||||||
|
if white_level is None:
|
||||||
|
white_level = float(raw.max())
|
||||||
|
|
||||||
|
if white_level <= black_level:
|
||||||
|
norm = raw / max_val
|
||||||
|
else:
|
||||||
|
norm = (raw - black_level) / (white_level - black_level)
|
||||||
|
|
||||||
|
norm = np.clip(norm, 0.0, 1.0)
|
||||||
|
|
||||||
|
if gamma is not None and gamma > 0:
|
||||||
|
norm = np.power(norm, 1.0 / gamma)
|
||||||
|
|
||||||
|
return (norm * 255.0).clip(0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _debayer_code(self):
|
||||||
|
mapping = {
|
||||||
|
"RGGB": cv2.COLOR_BayerRG2RGB_EA,
|
||||||
|
"BGGR": cv2.COLOR_BayerBG2RGB_EA,
|
||||||
|
"GRBG": cv2.COLOR_BayerGR2RGB_EA,
|
||||||
|
"GBRG": cv2.COLOR_BayerGB2RGB_EA,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.bayer_pattern not in mapping:
|
||||||
|
raise ValueError(f"Padrão Bayer não suportado: {self.bayer_pattern}")
|
||||||
|
|
||||||
|
return mapping[self.bayer_pattern]
|
||||||
|
|
||||||
|
def apply_preview_white_balance(self, bgr: np.ndarray, strength: float = 1.0) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Gray-world simples para deixar o preview mais agradável.
|
||||||
|
Não usar no raw de treino.
|
||||||
|
"""
|
||||||
|
img = bgr.astype(np.float32)
|
||||||
|
|
||||||
|
mean_b = float(img[:, :, 0].mean())
|
||||||
|
mean_g = float(img[:, :, 1].mean())
|
||||||
|
mean_r = float(img[:, :, 2].mean())
|
||||||
|
|
||||||
|
mean_gray = (mean_b + mean_g + mean_r) / 3.0
|
||||||
|
|
||||||
|
eps = 1e-6
|
||||||
|
gain_b = mean_gray / max(mean_b, eps)
|
||||||
|
gain_g = mean_gray / max(mean_g, eps)
|
||||||
|
gain_r = mean_gray / max(mean_r, eps)
|
||||||
|
|
||||||
|
# strength=1 aplica total, strength=0 não aplica
|
||||||
|
gain_b = 1.0 + (gain_b - 1.0) * strength
|
||||||
|
gain_g = 1.0 + (gain_g - 1.0) * strength
|
||||||
|
gain_r = 1.0 + (gain_r - 1.0) * strength
|
||||||
|
|
||||||
|
img[:, :, 0] *= gain_b
|
||||||
|
img[:, :, 1] *= gain_g
|
||||||
|
img[:, :, 2] *= gain_r
|
||||||
|
|
||||||
|
return np.clip(img, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def apply_preview_contrast(self, bgr: np.ndarray, alpha: float = 1.08, beta: float = 0.0) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Ajuste leve de contraste/brilho para preview.
|
||||||
|
"""
|
||||||
|
out = cv2.convertScaleAbs(bgr, alpha=alpha, beta=beta)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def raw16_to_preview_bgr(
|
||||||
|
self,
|
||||||
|
raw16: np.ndarray,
|
||||||
|
gamma: float = 2.2,
|
||||||
|
wb_strength: float = 0.8,
|
||||||
|
apply_wb: bool = True,
|
||||||
|
apply_contrast: bool = True,
|
||||||
|
bit_depth: int = 10,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Pipeline de preview bonito:
|
||||||
|
1. auto-level + gamma no mosaico
|
||||||
|
2. demosaic
|
||||||
|
3. white balance simples
|
||||||
|
4. leve contraste final
|
||||||
|
"""
|
||||||
|
vis8 = self.raw16_to_vis8(raw16, gamma=gamma, bit_depth=bit_depth)
|
||||||
|
bgr = cv2.cvtColor(vis8, self._debayer_code())
|
||||||
|
|
||||||
|
if apply_wb:
|
||||||
|
bgr = self.apply_preview_white_balance(bgr, strength=wb_strength)
|
||||||
|
|
||||||
|
if apply_contrast:
|
||||||
|
bgr = self.apply_preview_contrast(bgr, alpha=1.08, beta=0.0)
|
||||||
|
|
||||||
|
return bgr
|
||||||
|
|
||||||
|
def raw16_to_preview_jpg_bytes(self, raw16: np.ndarray, jpeg_quality: int = 95) -> bytes:
|
||||||
|
bgr = self.raw16_to_preview_bgr(raw16)
|
||||||
|
ok, enc = cv2.imencode(".jpg", bgr, [int(cv2.IMWRITE_JPEG_QUALITY), int(jpeg_quality)])
|
||||||
|
if not ok:
|
||||||
|
raise RuntimeError("Falha ao codificar preview JPG")
|
||||||
|
return enc.tobytes()
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"debug_visual": 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,
|
||||||
|
"ia_resolution": [1024,640],
|
||||||
|
"ia_channels": 5,
|
||||||
|
"ia_use_ndvi": 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,
|
||||||
|
|
||||||
|
"analise_fps": 20,
|
||||||
|
"tensor_fps": 20,
|
||||||
|
|
||||||
|
"fps": 20,
|
||||||
|
"module_params": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\calibration\\module_params.json",
|
||||||
|
|
||||||
|
"qtd_bicos": 7,
|
||||||
|
"velocidade_robo": 0,
|
||||||
|
"ia_model_path": "C:\\AgroBaseModels\\Ervas\\model-3_1.pt",
|
||||||
|
"ia_labelmap_path": "C:\\AgroBaseModels\\Ervas\\model-3_1.txt",
|
||||||
|
"ia_norm_stats_path": "C:\\AgroBaseModels\\Ervas\\model-1_1.json",
|
||||||
|
"ia_backbone": "nvidia/mit-b1",
|
||||||
|
|
||||||
|
"faixa_atuacao_bicos": 0.7,
|
||||||
|
"area_atuacao_bicos": 0.1,
|
||||||
|
"min_frac_erva_por_bico_on": 0.02,
|
||||||
|
"min_frac_erva_por_bico_off": 0.01,
|
||||||
|
|
||||||
|
"tipo_camera_solo": "multispectral",
|
||||||
|
"channels": 5,
|
||||||
|
"input_channels": ["R", "G", "B", "RE", "NIR"],
|
||||||
|
"backbone": "nvidia/mit-b1",
|
||||||
|
"ckpt": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\backup\\segformer_b1\\target_teached\\stacked_raw5\\best_score.pt",
|
||||||
|
"norm_stats_path": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\backup\\segformer_b1\\target_teached\\stacked_raw5\\norm_stats.json",
|
||||||
|
"module_calibration_json": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\calibration\\module_params.json",
|
||||||
|
"camera_width": 1280,
|
||||||
|
"camera_height": 800,
|
||||||
|
"camera_fps": 40,
|
||||||
|
"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",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,34 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from PIL import Image
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
from transformers import SegformerForSemanticSegmentation
|
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_MEAN = [0.485, 0.456, 0.406]
|
||||||
|
|
@ -15,16 +38,30 @@ class SegformerNavRunner:
|
||||||
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.device = torch.device(device if torch.cuda.is_available() else "cpu")
|
||||||
self.cor_para_id, self.colormap_rgb, self.classes, self.ignore_rgb = carregar_labelmap_completo(seg_config["ia_labelmap_path"])
|
|
||||||
self.model = self.load_segformer_from_checkpoint(seg_config, self.device)
|
self.use_amp = bool(seg_config.get("use_amp", True))
|
||||||
self.resolucao = tuple(seg_config["ia_resolution"])
|
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._last_aux_result = None
|
||||||
|
self._last_aux_ts = 0.0
|
||||||
|
|
||||||
|
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_inicio = seg_config["ia_roi_begin"]
|
||||||
self.roi_tamanho = seg_config["ia_roi_size"]
|
self.roi_tamanho = seg_config["ia_roi_size"]
|
||||||
self.last_infer = None
|
self.last_infer = None
|
||||||
|
|
||||||
# ==========================
|
self.mode = seg_config.get("ia_mode", "dual_label")
|
||||||
# Normalização fixa (igual treino)
|
self.model = None
|
||||||
# ==========================
|
self.aux_head = None
|
||||||
|
self.label_names = {}
|
||||||
|
|
||||||
|
self._load_dual_checkpoint(seg_config)
|
||||||
|
|
||||||
norm_mean = self.IMAGENET_MEAN
|
norm_mean = self.IMAGENET_MEAN
|
||||||
norm_std = self.IMAGENET_STD
|
norm_std = self.IMAGENET_STD
|
||||||
|
|
||||||
|
|
@ -35,161 +72,344 @@ class SegformerNavRunner:
|
||||||
|
|
||||||
stats_channels = norm_stats.get("channels", [])
|
stats_channels = norm_stats.get("channels", [])
|
||||||
stats_mean = norm_stats.get("mean", [])
|
stats_mean = norm_stats.get("mean", [])
|
||||||
stats_std = norm_stats.get("std", [])
|
stats_std = norm_stats.get("std", [])
|
||||||
|
|
||||||
print(f"[NORM] usando stats fixos de: {norm_stats_path}")
|
|
||||||
print(f"[NORM] channels={stats_channels}")
|
|
||||||
print(f"[NORM] mean={stats_mean}")
|
|
||||||
print(f"[NORM] std ={stats_std}")
|
|
||||||
|
|
||||||
idx_by_name = {name: i for i, name in enumerate(stats_channels)}
|
idx_by_name = {name: i for i, name in enumerate(stats_channels)}
|
||||||
|
|
||||||
m_R = stats_mean[idx_by_name["R"]]
|
if all(ch in idx_by_name for ch in ["R", "G", "B"]):
|
||||||
m_G = stats_mean[idx_by_name["G"]]
|
norm_mean = [
|
||||||
m_B = stats_mean[idx_by_name["B"]]
|
stats_mean[idx_by_name["R"]],
|
||||||
|
stats_mean[idx_by_name["G"]],
|
||||||
s_R = stats_std[idx_by_name["R"]]
|
stats_mean[idx_by_name["B"]],
|
||||||
s_G = stats_std[idx_by_name["G"]]
|
]
|
||||||
s_B = stats_std[idx_by_name["B"]]
|
norm_std = [
|
||||||
|
stats_std[idx_by_name["R"]],
|
||||||
norm_mean = [m_R, m_G, m_B]
|
stats_std[idx_by_name["G"]],
|
||||||
norm_std = [s_R, s_G, s_B]
|
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:
|
else:
|
||||||
print(f"[NORM] norm_stats.json não encontrado em {norm_stats_path}. "
|
print(f"[NORM] norm_stats não encontrado em {norm_stats_path}. Usando ImageNet.")
|
||||||
f"Usando normalização dinâmica por frame.")
|
|
||||||
|
|
||||||
self.set_norm_stats(norm_mean, norm_std)
|
self.set_norm_stats(norm_mean, norm_std)
|
||||||
|
|
||||||
def set_norm_stats(self, mean, std):
|
def set_norm_stats(self, mean, std):
|
||||||
mean = torch.tensor(mean, dtype=torch.float32).view(3, 1, 1)
|
self._norm_mean = torch.tensor(mean, dtype=torch.float32, device=self.device).view(3, 1, 1)
|
||||||
std = torch.tensor(std, dtype=torch.float32).view(3, 1, 1).clamp_min(1e-6)
|
self._norm_std = torch.tensor(std, dtype=torch.float32, device=self.device).view(3, 1, 1).clamp_min(1e-6)
|
||||||
self._norm_mean = mean
|
|
||||||
self._norm_std = std
|
|
||||||
|
|
||||||
def _extract_state_dict(self, ckpt):
|
def normalize_img(self, img):
|
||||||
"""
|
return (img - self._norm_mean) / self._norm_std
|
||||||
Aceita:
|
|
||||||
- state_dict puro (dict de tensores)
|
|
||||||
- checkpoint com chaves comuns: state_dict / model_state_dict / model
|
|
||||||
"""
|
|
||||||
if not isinstance(ckpt, dict):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# caso já seja um state_dict puro
|
def _build_base_model(self, backbone: str, num_classes: int):
|
||||||
if any(isinstance(v, torch.Tensor) for v in ckpt.values()):
|
config = SegformerConfig.from_pretrained(
|
||||||
return ckpt
|
|
||||||
|
|
||||||
for k in ("state_dict", "model_state_dict", "model"):
|
|
||||||
if k in ckpt and isinstance(ckpt[k], dict):
|
|
||||||
return ckpt[k]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def load_segformer_from_checkpoint(
|
|
||||||
self,
|
|
||||||
seg_config: dict,
|
|
||||||
device: torch.device,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Carrega um SegFormer (B0, B1, B2, B3...) compatível com o treino:
|
|
||||||
|
|
||||||
- Cria o modelo via from_pretrained(backbone, num_labels=num_classes)
|
|
||||||
- Carrega o state_dict salvo pelo script de treino
|
|
||||||
"""
|
|
||||||
pt_path = seg_config.get("ia_model_path")
|
|
||||||
backbone = seg_config.get("ia_backbone")
|
|
||||||
num_classes = len(self.classes)
|
|
||||||
ckpt = torch.load(pt_path, map_location="cpu", weights_only=True)
|
|
||||||
state_dict = self._extract_state_dict(ckpt)
|
|
||||||
if state_dict is None:
|
|
||||||
raise RuntimeError(f"Não consegui extrair state_dict de {pt_path}. keys={list(ckpt.keys())}")
|
|
||||||
|
|
||||||
# limpar prefixos comuns
|
|
||||||
cleaned = {}
|
|
||||||
for k, v in state_dict.items():
|
|
||||||
nk = k
|
|
||||||
if nk.startswith("model."):
|
|
||||||
nk = nk[len("model."):]
|
|
||||||
if nk.startswith("module."):
|
|
||||||
nk = nk[len("module."):]
|
|
||||||
cleaned[nk] = v
|
|
||||||
|
|
||||||
model = SegformerForSemanticSegmentation.from_pretrained(
|
|
||||||
backbone,
|
backbone,
|
||||||
num_labels=num_classes,
|
local_files_only=True
|
||||||
ignore_mismatched_sizes=True,
|
|
||||||
use_safetensors=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
missing, unexpected = model.load_state_dict(cleaned, strict=False)
|
config.num_labels = int(num_classes)
|
||||||
print(f"[load] missing={len(missing)} unexpected={len(unexpected)}")
|
config.output_hidden_states = True
|
||||||
if missing:
|
|
||||||
print("[load] missing sample:", missing[:10])
|
|
||||||
if unexpected:
|
|
||||||
print("[load] unexpected sample:", unexpected[:10])
|
|
||||||
|
|
||||||
model.to(device).eval()
|
model = SegformerForSemanticSegmentation(config)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
def normalize_img(self, img: torch.Tensor) -> torch.Tensor:
|
def _load_dual_checkpoint(self, seg_config):
|
||||||
return (img - self._norm_mean.to(img.device)) / self._norm_std.to(img.device)
|
pt_path = seg_config["ia_model_path"]
|
||||||
|
backbone = seg_config["ia_backbone"]
|
||||||
|
num_classes = len(self.classes)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if self.use_channels_last and self.device.type == "cuda":
|
||||||
|
self.model.to(memory_format=torch.channels_last)
|
||||||
|
|
||||||
|
torch.backends.cudnn.benchmark = True
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
print(f"[DUAL] LabelHead carregada: classes={num_label_classes} names={self.label_names}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.aux_head = None
|
||||||
|
print("[SEG] Modo single carregado.")
|
||||||
|
|
||||||
|
print(f"[MODEL] ckpt={pt_path}")
|
||||||
|
print(f"[MODEL] epoch={ckpt.get('epoch')} bests={ckpt.get('bests')}")
|
||||||
|
|
||||||
def compute_roi_indices(self, H: int, zona_inicio: float, faixa_atuacao: float):
|
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)
|
||||||
|
|
||||||
y_fim = max(0, min(H, y_fim))
|
y_fim = max(0, min(H, y_fim))
|
||||||
y_inicio = max(0, min(H, y_inicio))
|
y_inicio = max(0, min(H, y_inicio))
|
||||||
|
|
||||||
if y_fim >= y_inicio:
|
if y_fim >= y_inicio:
|
||||||
y_fim = max(0, y_inicio - 1)
|
y_fim = max(0, y_inicio - 1)
|
||||||
|
|
||||||
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 resize_keep_width(self, img: np.ndarray, new_w: int, min_h: int, interpolation: int) -> np.ndarray:
|
||||||
h, w = img.shape[:2]
|
h, w = img.shape[:2]
|
||||||
new_h = int(round(new_w * (h / w)))
|
new_h = int(round(new_w * (h / max(1, w))))
|
||||||
|
|
||||||
if min_h is not None and new_h < min_h:
|
if min_h is not None and new_h < min_h:
|
||||||
new_h = min_h
|
new_h = min_h
|
||||||
|
|
||||||
return cv2.resize(img, (new_w, new_h), interpolation=interpolation)
|
return cv2.resize(img, (new_w, new_h), interpolation=interpolation)
|
||||||
|
|
||||||
@torch.no_grad()
|
def _preprocess_roi(self, roi_rgb: np.ndarray):
|
||||||
def segformer_predict_ids(self, img_tensor):
|
roi_resized = self.resize_keep_width(
|
||||||
"""
|
roi_rgb,
|
||||||
img_tensor: [1,3,H,W] float32 normalizado.
|
self.resolucao[0],
|
||||||
retorna: pred_ids [H,W] (numpy int)
|
self.resolucao[1],
|
||||||
"""
|
cv2.INTER_AREA,
|
||||||
out = self.model(pixel_values=img_tensor)
|
|
||||||
logits = out.logits # [B, C, h, w] (pode ser menor que input)
|
|
||||||
# Upsample logits para o tamanho do input
|
|
||||||
logits = torch.nn.functional.interpolate(
|
|
||||||
logits,
|
|
||||||
size=img_tensor.shape[-2:],
|
|
||||||
mode="bilinear",
|
|
||||||
align_corners=False
|
|
||||||
)
|
)
|
||||||
pred = torch.argmax(logits, dim=1) # [B,H,W]
|
|
||||||
return pred.squeeze(0).cpu().numpy().astype(np.uint8)
|
|
||||||
|
|
||||||
|
# Garante array contínuo para reduzir cópia torta no torch.from_numpy
|
||||||
|
roi_resized = np.ascontiguousarray(roi_resized)
|
||||||
|
|
||||||
|
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):
|
def infer_ids(self, frame_rgb):
|
||||||
if False:
|
if frame_rgb is None or not hasattr(frame_rgb, "shape") or frame_rgb.size == 0:
|
||||||
img_rgb = np.array(Image.open(r"C:\ZendionInc\agrobot_base\t_cor\108_rgb.jpeg").convert("RGB"))
|
return None, None, None, None, None
|
||||||
H, W = img_rgb.shape[:2]
|
t0 = time.perf_counter()
|
||||||
y_fim, y_inicio = self.compute_roi_indices(H, self.roi_inicio, self.roi_tamanho)
|
H, W = frame_rgb.shape[:2]
|
||||||
roi = img_rgb[y_fim:y_inicio, 0:W]
|
y_fim, y_inicio = self.compute_roi_indices(H, self.roi_inicio, self.roi_tamanho)
|
||||||
else:
|
|
||||||
H, W = frame_rgb.shape[:2]
|
|
||||||
y_fim, y_inicio = self.compute_roi_indices(H, self.roi_inicio, self.roi_tamanho)
|
|
||||||
roi = frame_rgb[y_fim:y_inicio, 0:W]
|
|
||||||
|
|
||||||
roi_resized = self.resize_keep_width(roi, self.resolucao[0], self.resolucao[1], cv2.INTER_AREA)
|
roi_rgb = frame_rgb[y_fim:y_inicio, 0:W]
|
||||||
roi_norm = roi_resized.astype(np.float32) / 255.0
|
t_crop = time.perf_counter()
|
||||||
img_tensor = torch.from_numpy(roi_norm).permute(2, 0, 1).unsqueeze(0).to(self.device)
|
if roi_rgb is None or roi_rgb.size == 0:
|
||||||
img_tensor = self.normalize_img(img_tensor).float()
|
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:
|
||||||
|
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()
|
||||||
|
|
||||||
|
t_aux1 = time.perf_counter()
|
||||||
|
|
||||||
pred_ids = self.segformer_predict_ids(img_tensor) # (H,W)
|
|
||||||
self.last_infer = time.time()
|
self.last_infer = time.time()
|
||||||
return pred_ids, self.last_infer, roi_resized, (y_fim, y_inicio)
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ def main():
|
||||||
T_Code.Sen: ModuloSensoriamento(),
|
T_Code.Sen: ModuloSensoriamento(),
|
||||||
T_Code.Atu: ModuloAtuador(),
|
T_Code.Atu: ModuloAtuador(),
|
||||||
T_Code.Lra: ModuloLoRa(),
|
T_Code.Lra: ModuloLoRa(),
|
||||||
T_Code.Imu: IMUCamera(),
|
#T_Code.Imu: IMUCamera(),
|
||||||
T_Code.Npc: ModuloPC(),
|
T_Code.Npc: ModuloPC(),
|
||||||
T_Code.Lvx: ModuloLivox(),
|
T_Code.Lvx: ModuloLivox(),
|
||||||
T_Code.Ipb: ModuloIPBribge(),
|
T_Code.Ipb: ModuloIPBribge(),
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,18 @@ from shared.enums import StatusModulo, T_Code
|
||||||
from health_worker.modulos.base import ModuloDiagnosticoBase
|
from health_worker.modulos.base import ModuloDiagnosticoBase
|
||||||
|
|
||||||
class IMUCamera(ModuloDiagnosticoBase):
|
class IMUCamera(ModuloDiagnosticoBase):
|
||||||
def __init__(self, queue=None, freq=100, angulo_inicial=26.3):
|
def __init__(self, mx_id, queue=None, freq=100, angulo_inicial=26.3):
|
||||||
|
self.mx_id = mx_id
|
||||||
self.freq = freq
|
self.freq = freq
|
||||||
|
|
||||||
|
self.ultima_saude = None
|
||||||
|
|
||||||
self.ativo = False
|
self.ativo = False
|
||||||
self.last_packet_ts = 0.0
|
self.last_packet_ts = 0.0
|
||||||
self.last_publish_ts = 0.0
|
self.last_publish_ts = 0.0
|
||||||
self.ultimo_erro_ts = 0.0
|
self.ultimo_erro_ts = 0.0
|
||||||
|
|
||||||
|
self.last_data = None
|
||||||
|
|
||||||
if queue is None:
|
if queue is None:
|
||||||
return
|
return
|
||||||
|
|
@ -75,6 +80,13 @@ class IMUCamera(ModuloDiagnosticoBase):
|
||||||
self.ativo = False
|
self.ativo = False
|
||||||
self.mostrar_log("Task parada")
|
self.mostrar_log("Task parada")
|
||||||
|
|
||||||
|
try:
|
||||||
|
th = getattr(self, "imu_thread", None)
|
||||||
|
if th is not None and th.is_alive():
|
||||||
|
th.join(timeout=1.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _mediana_curta(self, hist, valor):
|
def _mediana_curta(self, hist, valor):
|
||||||
hist.append(valor)
|
hist.append(valor)
|
||||||
return float(np.median(hist))
|
return float(np.median(hist))
|
||||||
|
|
@ -177,6 +189,7 @@ class IMUCamera(ModuloDiagnosticoBase):
|
||||||
self.estabilidade_idx = round(self.clamp(estabilidade_idx, 0.0, 100.0), 1)
|
self.estabilidade_idx = round(self.clamp(estabilidade_idx, 0.0, 100.0), 1)
|
||||||
|
|
||||||
def imu_task_loop(self):
|
def imu_task_loop(self):
|
||||||
|
from camera_worker.manager import definir_imu_camera
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
while self.ativo:
|
while self.ativo:
|
||||||
|
|
@ -308,26 +321,48 @@ class IMUCamera(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
ts_imu = (time.time() * 1000)
|
ts_imu = (time.time() * 1000)
|
||||||
|
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
#ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
ContextoGlobalRedis.ModKey(T_Code.Imu),
|
# ContextoGlobalRedis.ModKey(T_Code.Imu),
|
||||||
roll=round(-roll, 2),
|
# roll=round(-roll, 2),
|
||||||
pitch=round(pitch, 2),
|
# pitch=round(pitch, 2),
|
||||||
yaw=round(yaw, 2),
|
# yaw=round(yaw, 2),
|
||||||
roll_seg=round(-self.roll_seg, 2),
|
# roll_seg=round(-self.roll_seg, 2),
|
||||||
pitch_seg=round(self.pitch_seg, 2),
|
# pitch_seg=round(self.pitch_seg, 2),
|
||||||
vel_mps=round(velocidade_mps, 3),
|
# vel_mps=round(velocidade_mps, 3),
|
||||||
em_movimento=self.em_movimento,
|
# em_movimento=self.em_movimento,
|
||||||
movimento_idx=round(self.movimento_idx, 1),
|
# movimento_idx=round(self.movimento_idx, 1),
|
||||||
rugosidade_idx=round(self.rugosidade_idx, 1),
|
# rugosidade_idx=round(self.rugosidade_idx, 1),
|
||||||
impacto_idx=round(self.impacto_idx, 1),
|
# impacto_idx=round(self.impacto_idx, 1),
|
||||||
estabilidade_idx=round(self.estabilidade_idx, 1),
|
# estabilidade_idx=round(self.estabilidade_idx, 1),
|
||||||
timestamp=ts_imu,
|
# timestamp=ts_imu,
|
||||||
last_packet_ts=self.last_packet_ts,
|
# last_packet_ts=self.last_packet_ts,
|
||||||
last_publish_ts=self.last_publish_ts,
|
# last_publish_ts=self.last_publish_ts,
|
||||||
ultimo_erro_ts=self.ultimo_erro_ts,
|
# ultimo_erro_ts=self.ultimo_erro_ts,
|
||||||
latencia=latencia,
|
# latencia=latencia,
|
||||||
frequencia=f_tick
|
# frequencia=f_tick
|
||||||
)
|
#)
|
||||||
|
|
||||||
|
self.last_data = {
|
||||||
|
"roll": round(-roll, 2),
|
||||||
|
"pitch": round(pitch, 2),
|
||||||
|
"yaw": round(yaw, 2),
|
||||||
|
"roll_seg": round(-self.roll_seg, 2),
|
||||||
|
"pitch_seg": round(self.pitch_seg, 2),
|
||||||
|
"vel_mps": round(velocidade_mps, 3),
|
||||||
|
"em_movimento": self.em_movimento,
|
||||||
|
"movimento_idx": round(self.movimento_idx, 1),
|
||||||
|
"rugosidade_idx": round(self.rugosidade_idx, 1),
|
||||||
|
"impacto_idx": round(self.impacto_idx, 1),
|
||||||
|
"estabilidade_idx": round(self.estabilidade_idx, 1),
|
||||||
|
"timestamp": ts_imu,
|
||||||
|
"last_packet_ts": self.last_packet_ts,
|
||||||
|
"last_publish_ts": self.last_publish_ts,
|
||||||
|
"ultimo_erro_ts": self.ultimo_erro_ts,
|
||||||
|
"latencia": latencia,
|
||||||
|
"frequencia": f_tick
|
||||||
|
}
|
||||||
|
|
||||||
|
definir_imu_camera(self.mx_id, self.last_data, self.ultima_saude)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.ultimo_erro_ts = time.time()
|
self.ultimo_erro_ts = time.time()
|
||||||
|
|
@ -345,7 +380,7 @@ class IMUCamera(ModuloDiagnosticoBase):
|
||||||
delay_corrigido = max(0.0, (1.0 / self.freq) - latencia)
|
delay_corrigido = max(0.0, (1.0 / self.freq) - latencia)
|
||||||
time.sleep(delay_corrigido)
|
time.sleep(delay_corrigido)
|
||||||
|
|
||||||
def atualizar_saude(self):
|
def atualizar_saude_bkp(self):
|
||||||
try:
|
try:
|
||||||
agora = time.time()
|
agora = time.time()
|
||||||
modulo = ContextoGlobalRedis.get_modulo(T_Code.Imu) or {}
|
modulo = ContextoGlobalRedis.get_modulo(T_Code.Imu) or {}
|
||||||
|
|
@ -469,5 +504,156 @@ class IMUCamera(ModuloDiagnosticoBase):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.mostrar_log(f"Erro ao atualizar saude: {e}")
|
self.mostrar_log(f"Erro ao atualizar saude: {e}")
|
||||||
|
|
||||||
|
def atualizar_saude(self):
|
||||||
|
try:
|
||||||
|
agora = time.time()
|
||||||
|
|
||||||
|
FREQ_BASE = float(self.freq)
|
||||||
|
FREQ_MIN = FREQ_BASE * 0.5
|
||||||
|
SAUDE_MIN_ALERTA = 80
|
||||||
|
SAUDE_MIN_FALHA = 45
|
||||||
|
|
||||||
|
data = self.get_dados()
|
||||||
|
|
||||||
|
last_packet_ts = float(data.get("last_packet_ts", self.last_packet_ts) or 0.0)
|
||||||
|
last_publish_ts = float(data.get("last_publish_ts", self.last_publish_ts) or 0.0)
|
||||||
|
ultimo_erro_ts = float(data.get("ultimo_erro_ts", self.ultimo_erro_ts) or 0.0)
|
||||||
|
|
||||||
|
freq_hz = float(data.get("frequencia", 0.0) or 0.0)
|
||||||
|
latencia = float(data.get("latencia", 0.0) or 0.0)
|
||||||
|
valido = bool(data.get("valido", False))
|
||||||
|
|
||||||
|
tempo_sem_packet = (agora - last_packet_ts) if last_packet_ts > 0 else 999.0
|
||||||
|
tempo_sem_atualizar = (agora - last_publish_ts) if last_publish_ts > 0 else 999.0
|
||||||
|
tempo_desde_erro = (agora - ultimo_erro_ts) if ultimo_erro_ts > 0 else 999.0
|
||||||
|
|
||||||
|
conectado = bool(self.ativo and tempo_sem_packet < 10.0)
|
||||||
|
|
||||||
|
saude = 100
|
||||||
|
motivos = []
|
||||||
|
|
||||||
|
if not self.ativo:
|
||||||
|
conectado = False
|
||||||
|
saude = 0
|
||||||
|
motivos.append("Thread da IMU parada")
|
||||||
|
|
||||||
|
elif not valido:
|
||||||
|
saude = 0
|
||||||
|
motivos.append("IMU ainda sem leitura válida")
|
||||||
|
|
||||||
|
elif not conectado:
|
||||||
|
saude = 0
|
||||||
|
motivos.append(f"Sem packets da IMU há {tempo_sem_packet:.2f}s")
|
||||||
|
|
||||||
|
else:
|
||||||
|
if tempo_sem_packet > 1.5:
|
||||||
|
saude -= 45
|
||||||
|
motivos.append(f"Sem packets há {tempo_sem_packet:.2f}s")
|
||||||
|
elif tempo_sem_packet > 0.7:
|
||||||
|
saude -= 25
|
||||||
|
motivos.append(f"Packets atrasados há {tempo_sem_packet:.2f}s")
|
||||||
|
elif tempo_sem_packet > 0.3:
|
||||||
|
saude -= 10
|
||||||
|
motivos.append(f"Leve atraso de packets: {tempo_sem_packet:.2f}s")
|
||||||
|
|
||||||
|
if tempo_sem_atualizar > 1.5:
|
||||||
|
saude -= 30
|
||||||
|
motivos.append(f"Sem atualizar dados há {tempo_sem_atualizar:.2f}s")
|
||||||
|
elif tempo_sem_atualizar > 0.8:
|
||||||
|
saude -= 15
|
||||||
|
motivos.append(f"Atualização atrasada há {tempo_sem_atualizar:.2f}s")
|
||||||
|
elif tempo_sem_atualizar > 0.3:
|
||||||
|
saude -= 5
|
||||||
|
motivos.append(f"Leve atraso na atualização: {tempo_sem_atualizar:.2f}s")
|
||||||
|
|
||||||
|
if freq_hz <= 0:
|
||||||
|
saude -= 20
|
||||||
|
motivos.append("Frequência zerada")
|
||||||
|
elif freq_hz <= FREQ_MIN:
|
||||||
|
saude -= 35
|
||||||
|
motivos.append(f"Frequência baixa: {freq_hz:.2f} Hz")
|
||||||
|
else:
|
||||||
|
f = min(freq_hz, FREQ_BASE)
|
||||||
|
erro = (FREQ_BASE - f) / FREQ_BASE
|
||||||
|
p = min(int(erro * 100), 20)
|
||||||
|
saude -= p
|
||||||
|
if p > 0:
|
||||||
|
motivos.append(f"Frequência abaixo do ideal: {freq_hz:.2f} Hz")
|
||||||
|
|
||||||
|
if latencia > 1.0:
|
||||||
|
saude -= 25
|
||||||
|
motivos.append(f"Latência muito alta: {latencia:.2f}s")
|
||||||
|
elif latencia > 0.5:
|
||||||
|
saude -= 15
|
||||||
|
motivos.append(f"Latência alta: {latencia:.2f}s")
|
||||||
|
elif latencia > 0.2:
|
||||||
|
saude -= 5
|
||||||
|
motivos.append(f"Latência moderada: {latencia:.2f}s")
|
||||||
|
|
||||||
|
if tempo_desde_erro < 2.0:
|
||||||
|
saude -= 20
|
||||||
|
motivos.append("Erro muito recente no loop")
|
||||||
|
elif tempo_desde_erro < 5.0:
|
||||||
|
saude -= 10
|
||||||
|
motivos.append("Erro recente no loop")
|
||||||
|
|
||||||
|
saude = max(0, min(100, saude))
|
||||||
|
|
||||||
|
status = StatusModulo.OPERANTE
|
||||||
|
if not conectado:
|
||||||
|
status = StatusModulo.DESCONECTADO
|
||||||
|
elif saude <= SAUDE_MIN_FALHA:
|
||||||
|
status = StatusModulo.FALHA
|
||||||
|
elif saude < SAUDE_MIN_ALERTA:
|
||||||
|
status = StatusModulo.ALERTA
|
||||||
|
|
||||||
|
self.ultima_saude = {
|
||||||
|
"conectado": conectado,
|
||||||
|
"status": status.value,
|
||||||
|
"saude": saude,
|
||||||
|
"motivos": motivos,
|
||||||
|
"saude_individual": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.ultima_saude
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.mostrar_log(f"Erro ao atualizar saude: {e}")
|
||||||
|
self.ultima_saude = {
|
||||||
|
"conectado": False,
|
||||||
|
"status": StatusModulo.FALHA.value,
|
||||||
|
"saude": 0,
|
||||||
|
"motivos": [str(e)],
|
||||||
|
"saude_individual": [],
|
||||||
|
}
|
||||||
|
return self.ultima_saude
|
||||||
|
|
||||||
|
def get_dados(self):
|
||||||
|
if self.last_data is None:
|
||||||
|
return {
|
||||||
|
"roll": 0.0,
|
||||||
|
"pitch": 0.0,
|
||||||
|
"yaw": 0.0,
|
||||||
|
"roll_seg": 0.0,
|
||||||
|
"pitch_seg": 0.0,
|
||||||
|
"vel_mps": 0.0,
|
||||||
|
"em_movimento": False,
|
||||||
|
"movimento_idx": 0.0,
|
||||||
|
"rugosidade_idx": 0.0,
|
||||||
|
"impacto_idx": 0.0,
|
||||||
|
"estabilidade_idx": 100.0,
|
||||||
|
"timestamp": 0.0,
|
||||||
|
"last_packet_ts": 0.0,
|
||||||
|
"last_publish_ts": 0.0,
|
||||||
|
"ultimo_erro_ts": 0.0,
|
||||||
|
"latencia": 0.0,
|
||||||
|
"frequencia": 0.0,
|
||||||
|
"valido": False
|
||||||
|
}
|
||||||
|
|
||||||
|
data = dict(self.last_data)
|
||||||
|
data["valido"] = True
|
||||||
|
return data
|
||||||
|
|
||||||
def mostrar_log(self, mensagem):
|
def mostrar_log(self, mensagem):
|
||||||
print(f"{time.time()} - [IMU][id={id(self)}] {mensagem}")
|
print(f"{time.time()} - [IMU][id={id(self)}] {mensagem}")
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -158,6 +158,11 @@ class ContextoGlobalRedis:
|
||||||
def CamKey(cls, mx_id: str):
|
def CamKey(cls, mx_id: str):
|
||||||
"""Lê a estrutura e decodifica de volta para objeto"""
|
"""Lê a estrutura e decodifica de volta para objeto"""
|
||||||
return CtxKey.DadosCameras.value + mx_id
|
return CtxKey.DadosCameras.value + mx_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def CamImuKey(cls, mx_id: str):
|
||||||
|
"""Lê a estrutura e decodifica de volta para objeto"""
|
||||||
|
return CtxKey.DadosCameras.value + mx_id + "_Imu"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cameras(cls):
|
def get_cameras(cls):
|
||||||
|
|
@ -166,6 +171,10 @@ class ContextoGlobalRedis:
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_camera(cls, mx_id: str):
|
def get_camera(cls, mx_id: str):
|
||||||
return cls.get(cls.CamKey(mx_id), None)
|
return cls.get(cls.CamKey(mx_id), None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_camera_imu(cls, mx_id: str):
|
||||||
|
return cls.get(cls.CamImuKey(mx_id), None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_equipamento(cls):
|
def get_equipamento(cls):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,544 @@
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GpuModeConfig:
|
||||||
|
mode: str
|
||||||
|
visual_fps: Dict[str, float]
|
||||||
|
|
||||||
|
|
||||||
|
class GpuPriorityController:
|
||||||
|
"""
|
||||||
|
Controlador adaptativo de prioridade de GPU.
|
||||||
|
|
||||||
|
Primeira versão:
|
||||||
|
- Não usa lock real de GPU.
|
||||||
|
- Não centraliza inferência.
|
||||||
|
- Apenas reduz/aumenta FPS do visual_worker conforme saúde do weed_worker.
|
||||||
|
|
||||||
|
O objetivo é proteger o weed_worker, mantendo o visual_worker vivo,
|
||||||
|
mas descartável quando o sistema estiver pesado.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODES = ["critical", "safe", "eco", "normal"]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mostrar_log=None,
|
||||||
|
enabled: bool = True,
|
||||||
|
update_interval_s: float = 1.0,
|
||||||
|
log_interval_s: float = 3.0,
|
||||||
|
weed_targets: Optional[Dict[str, float]] = None,
|
||||||
|
visual_targets_normal: Optional[Dict[str, float]] = None,
|
||||||
|
mode_configs: Optional[Dict[str, Dict[str, float]]] = None,
|
||||||
|
bad_cycles_to_degrade: int = 3,
|
||||||
|
good_cycles_to_recover: int = 5,
|
||||||
|
min_data_age_s: float = 0.0,
|
||||||
|
max_data_age_s: float = 3.0,
|
||||||
|
log_periodic: bool = False,
|
||||||
|
log_warnings: bool = True,
|
||||||
|
):
|
||||||
|
self.mostrar_log = mostrar_log
|
||||||
|
self.enabled = bool(enabled)
|
||||||
|
|
||||||
|
self.update_interval_s = float(update_interval_s)
|
||||||
|
self.log_interval_s = float(log_interval_s)
|
||||||
|
|
||||||
|
self.weed_targets = weed_targets or {
|
||||||
|
"tensor": 18.0,
|
||||||
|
"inferencia": 15.0,
|
||||||
|
"deteccao": 15.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.visual_targets_normal = visual_targets_normal or {
|
||||||
|
"segmentacao": 8.0,
|
||||||
|
"grid": 6.0,
|
||||||
|
"deteccao": 5.0,
|
||||||
|
"publicacao": 15.0,
|
||||||
|
"analise": 8.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Modos do visual. Ajustamos principalmente segmentação e grid.
|
||||||
|
self.mode_configs = mode_configs or {
|
||||||
|
"normal": {
|
||||||
|
"segmentacao": self.visual_targets_normal.get("segmentacao", 8.0),
|
||||||
|
"grid": self.visual_targets_normal.get("grid", 6.0),
|
||||||
|
"deteccao": self.visual_targets_normal.get("deteccao", 5.0),
|
||||||
|
"publicacao": self.visual_targets_normal.get("publicacao", 15.0),
|
||||||
|
"analise": self.visual_targets_normal.get("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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bad_cycles_to_degrade = int(bad_cycles_to_degrade)
|
||||||
|
self.good_cycles_to_recover = int(good_cycles_to_recover)
|
||||||
|
|
||||||
|
self.min_data_age_s = float(min_data_age_s)
|
||||||
|
self.max_data_age_s = float(max_data_age_s)
|
||||||
|
|
||||||
|
self._mode = "normal"
|
||||||
|
self._desired_mode = "normal"
|
||||||
|
|
||||||
|
self._bad_cycles = 0
|
||||||
|
self._good_cycles = 0
|
||||||
|
|
||||||
|
self._last_update = 0.0
|
||||||
|
self._last_log = 0.0
|
||||||
|
|
||||||
|
self._last_weed_perf = {}
|
||||||
|
self._last_health = {
|
||||||
|
"health": 1.0,
|
||||||
|
"reason": "init",
|
||||||
|
"fps": {},
|
||||||
|
"ratios": {},
|
||||||
|
"gpu_ms": 0.0,
|
||||||
|
"data_age_s": None,
|
||||||
|
"data_ok": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.log_periodic = bool(log_periodic)
|
||||||
|
self.log_warnings = bool(log_warnings)
|
||||||
|
self._last_warning_reason = None
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# API pública
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
def update_from_redis(self):
|
||||||
|
"""
|
||||||
|
Lê Redis e atualiza modo.
|
||||||
|
Seguro para chamar várias vezes; respeita update_interval_s.
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
if not self.enabled:
|
||||||
|
self._mode = "normal"
|
||||||
|
return self._last_health
|
||||||
|
|
||||||
|
if now - self._last_update < self.update_interval_s:
|
||||||
|
return self._last_health
|
||||||
|
|
||||||
|
self._last_update = now
|
||||||
|
|
||||||
|
weed_ctx = self._read_weed_context()
|
||||||
|
health = self._evaluate_weed_health(weed_ctx)
|
||||||
|
|
||||||
|
self._last_weed_perf = weed_ctx or {}
|
||||||
|
self._last_health = health
|
||||||
|
|
||||||
|
desired = self._choose_desired_mode(health)
|
||||||
|
self._apply_hysteresis(desired)
|
||||||
|
|
||||||
|
if self.log_periodic and (now - self._last_log >= self.log_interval_s):
|
||||||
|
self._last_log = now
|
||||||
|
self._log_status()
|
||||||
|
|
||||||
|
if self.log_warnings:
|
||||||
|
self._log_warning_if_needed()
|
||||||
|
|
||||||
|
return self._last_health
|
||||||
|
|
||||||
|
def get_mode(self) -> str:
|
||||||
|
return self._mode
|
||||||
|
|
||||||
|
def get_health(self) -> Dict[str, Any]:
|
||||||
|
return dict(self._last_health)
|
||||||
|
|
||||||
|
def get_fps(self, task_name: str, fallback: Optional[float] = None) -> float:
|
||||||
|
"""
|
||||||
|
Retorna FPS atual para uma tarefa do visual_worker.
|
||||||
|
|
||||||
|
task_name esperado:
|
||||||
|
- segmentacao
|
||||||
|
- grid
|
||||||
|
- deteccao
|
||||||
|
- publicacao
|
||||||
|
- analise
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
if fallback is not None:
|
||||||
|
return float(fallback)
|
||||||
|
return float(self.visual_targets_normal.get(task_name, 1.0))
|
||||||
|
|
||||||
|
cfg = self.mode_configs.get(self._mode, self.mode_configs["normal"])
|
||||||
|
fps = cfg.get(task_name)
|
||||||
|
|
||||||
|
if fps is None:
|
||||||
|
fps = fallback if fallback is not None else self.visual_targets_normal.get(task_name, 1.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return max(0.0, float(fps))
|
||||||
|
except Exception:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def allow(self, task_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Por enquanto é simples:
|
||||||
|
- se FPS da tarefa <= 0, bloqueia.
|
||||||
|
- caso contrário, permite.
|
||||||
|
"""
|
||||||
|
return self.get_fps(task_name, fallback=1.0) > 0.01
|
||||||
|
|
||||||
|
def sleep_for_task(self, task_name: str, t0_wall: float, fallback_fps: float):
|
||||||
|
"""
|
||||||
|
Sleep adaptativo para usar no finally dos loops.
|
||||||
|
"""
|
||||||
|
fps = self.get_fps(task_name, fallback=fallback_fps)
|
||||||
|
|
||||||
|
if fps <= 0.01:
|
||||||
|
time.sleep(0.25)
|
||||||
|
return
|
||||||
|
|
||||||
|
elapsed = time.time() - t0_wall
|
||||||
|
period = 1.0 / max(fps, 0.01)
|
||||||
|
time.sleep(max(0.0, period - elapsed))
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# Leitura Redis
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
def _read_weed_context(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Leitura robusta do contexto do weed_worker.
|
||||||
|
|
||||||
|
Ajuste o nome da chave se no seu CtxKey estiver diferente.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
|
||||||
|
|
||||||
|
# Tenta nomes prováveis sem quebrar se algum não existir.
|
||||||
|
possible_attrs = [
|
||||||
|
"DadosWeedWorker",
|
||||||
|
"DadosErvasWorker",
|
||||||
|
"DadosWeed",
|
||||||
|
"WeedWorker",
|
||||||
|
]
|
||||||
|
|
||||||
|
for attr in possible_attrs:
|
||||||
|
if hasattr(CtxKey, attr):
|
||||||
|
key = getattr(CtxKey, attr)
|
||||||
|
ctx = ContextoGlobalRedis.get(key, {})
|
||||||
|
if isinstance(ctx, dict) and ctx:
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
# Fallback opcional caso seu Redis aceite string.
|
||||||
|
try:
|
||||||
|
ctx = ContextoGlobalRedis.get("DadosWeedWorker", {})
|
||||||
|
if isinstance(ctx, dict):
|
||||||
|
return ctx
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"[GPU_CTRL] erro lendo contexto weed: {e}")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# Avaliação de saúde
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
def _evaluate_weed_health(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
if not isinstance(ctx, dict) or not ctx:
|
||||||
|
return {
|
||||||
|
"health": 0.0,
|
||||||
|
"reason": "sem_ctx_weed",
|
||||||
|
"fps": {},
|
||||||
|
"ratios": {},
|
||||||
|
"gpu_ms": 0.0,
|
||||||
|
"data_age_s": None,
|
||||||
|
"data_ok": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
perf = self._extract_perf_root(ctx)
|
||||||
|
|
||||||
|
ts = self._extract_timestamp(ctx, perf)
|
||||||
|
data_age_s = None
|
||||||
|
|
||||||
|
if ts:
|
||||||
|
data_age_s = max(0.0, now - float(ts))
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
fps_tensor = self._extract_loop_fps(perf, ["tensor", "tensores"])
|
||||||
|
fps_inf = self._extract_loop_fps(perf, ["inferencia", "inf", "infer"])
|
||||||
|
fps_det = self._extract_loop_fps(perf, ["deteccao", "det", "detector"])
|
||||||
|
|
||||||
|
gpu_ms = self._extract_gpu_ms(perf)
|
||||||
|
|
||||||
|
fps = {
|
||||||
|
"tensor": fps_tensor,
|
||||||
|
"inferencia": fps_inf,
|
||||||
|
"deteccao": fps_det,
|
||||||
|
}
|
||||||
|
|
||||||
|
ratios = {}
|
||||||
|
for name, target in self.weed_targets.items():
|
||||||
|
real = fps.get(name, 0.0)
|
||||||
|
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
|
||||||
|
|
||||||
|
reason = "ok"
|
||||||
|
|
||||||
|
if not data_ok:
|
||||||
|
health = min(health, 0.40)
|
||||||
|
reason = "ctx_weed_desatualizado"
|
||||||
|
|
||||||
|
if fps_inf <= 0.1:
|
||||||
|
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"
|
||||||
|
elif gpu_ms >= 95:
|
||||||
|
health = min(health, 0.60)
|
||||||
|
reason = "gpu_ms_alto"
|
||||||
|
elif gpu_ms >= 80:
|
||||||
|
health = min(health, 0.80)
|
||||||
|
reason = "gpu_ms_moderado"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"health": float(health),
|
||||||
|
"reason": reason,
|
||||||
|
"fps": fps,
|
||||||
|
"ratios": ratios,
|
||||||
|
"gpu_ms": float(gpu_ms or 0.0),
|
||||||
|
"data_age_s": data_age_s,
|
||||||
|
"data_ok": bool(data_ok),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_perf_root(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Aceita formatos variados:
|
||||||
|
- ctx["performance_weed"]
|
||||||
|
- ctx["performance"]
|
||||||
|
- ctx direto com loops
|
||||||
|
"""
|
||||||
|
for key in ["performance_weed", "performance", "perf", "performance_visual"]:
|
||||||
|
val = ctx.get(key)
|
||||||
|
if isinstance(val, dict):
|
||||||
|
return val
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def _extract_timestamp(self, ctx: Dict[str, Any], perf: Dict[str, Any]) -> Optional[float]:
|
||||||
|
for root in [perf, ctx]:
|
||||||
|
for key in ["ts", "timestamp", "ts_analise", "ultima_chamada"]:
|
||||||
|
val = root.get(key)
|
||||||
|
if isinstance(val, (int, float)) and val > 0:
|
||||||
|
return float(val)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_loop_fps(self, perf: Dict[str, Any], aliases) -> float:
|
||||||
|
loops = perf.get("loops", {})
|
||||||
|
|
||||||
|
if isinstance(loops, dict):
|
||||||
|
for alias in aliases:
|
||||||
|
loop = loops.get(alias)
|
||||||
|
if isinstance(loop, dict):
|
||||||
|
fps = loop.get("fps_real", loop.get("fps", None))
|
||||||
|
if fps is not None:
|
||||||
|
try:
|
||||||
|
return float(fps)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallbacks planos
|
||||||
|
for alias in aliases:
|
||||||
|
for key in [
|
||||||
|
f"fps_{alias}",
|
||||||
|
f"{alias}_fps",
|
||||||
|
alias,
|
||||||
|
]:
|
||||||
|
val = perf.get(key)
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
return float(val)
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _extract_gpu_ms(self, perf: Dict[str, Any]) -> float:
|
||||||
|
loops = perf.get("loops", {})
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
if isinstance(loops, dict):
|
||||||
|
for name in ["inferencia", "inf", "infer"]:
|
||||||
|
loop = loops.get(name)
|
||||||
|
if not isinstance(loop, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
metrics = loop.get("metrics_ms", {})
|
||||||
|
if isinstance(metrics, dict):
|
||||||
|
for metric_name in ["gpu_ms", "infer_gpu_ms", "infer_ms"]:
|
||||||
|
metric = metrics.get(metric_name)
|
||||||
|
if isinstance(metric, dict):
|
||||||
|
val = metric.get("med", metric.get("avg", metric.get("last")))
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
candidates.append(float(val))
|
||||||
|
elif isinstance(metric, (int, float)):
|
||||||
|
candidates.append(float(metric))
|
||||||
|
|
||||||
|
for key in ["gpu_ms", "infer_gpu_ms", "weed_gpu_ms"]:
|
||||||
|
val = perf.get(key)
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
candidates.append(float(val))
|
||||||
|
|
||||||
|
return candidates[0] if candidates else 0.0
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# Decisão de modo
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
def _choose_desired_mode(self, health: Dict[str, Any]) -> str:
|
||||||
|
h = float(health.get("health", 0.0))
|
||||||
|
|
||||||
|
if h >= 0.90:
|
||||||
|
return "normal"
|
||||||
|
if h >= 0.75:
|
||||||
|
return "eco"
|
||||||
|
if h >= 0.45:
|
||||||
|
return "safe"
|
||||||
|
return "critical"
|
||||||
|
|
||||||
|
def _mode_index(self, mode: str) -> int:
|
||||||
|
try:
|
||||||
|
return self.MODES.index(mode)
|
||||||
|
except ValueError:
|
||||||
|
return self.MODES.index("normal")
|
||||||
|
|
||||||
|
def _apply_hysteresis(self, desired: str):
|
||||||
|
current_i = self._mode_index(self._mode)
|
||||||
|
desired_i = self._mode_index(desired)
|
||||||
|
|
||||||
|
self._desired_mode = desired
|
||||||
|
|
||||||
|
# Quanto maior o índice, mais leve/restritivo? Aqui:
|
||||||
|
# critical=0, safe=1, eco=2, normal=3.
|
||||||
|
if desired_i < current_i:
|
||||||
|
# Piorar modo.
|
||||||
|
self._bad_cycles += 1
|
||||||
|
self._good_cycles = 0
|
||||||
|
|
||||||
|
if self._bad_cycles >= self.bad_cycles_to_degrade:
|
||||||
|
old = self._mode
|
||||||
|
self._mode = desired
|
||||||
|
self._bad_cycles = 0
|
||||||
|
self._log(f"[GPU_CTRL] modo visual: {old} -> {self._mode}")
|
||||||
|
|
||||||
|
elif desired_i > current_i:
|
||||||
|
# Melhorar modo.
|
||||||
|
self._good_cycles += 1
|
||||||
|
self._bad_cycles = 0
|
||||||
|
|
||||||
|
if self._good_cycles >= self.good_cycles_to_recover:
|
||||||
|
old = self._mode
|
||||||
|
self._mode = desired
|
||||||
|
self._good_cycles = 0
|
||||||
|
self._log(f"[GPU_CTRL] modo visual: {old} -> {self._mode}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._bad_cycles = 0
|
||||||
|
self._good_cycles = 0
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# Logs
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
def _log_status(self):
|
||||||
|
h = self._last_health
|
||||||
|
fps = h.get("fps", {})
|
||||||
|
ratios = h.get("ratios", {})
|
||||||
|
|
||||||
|
self._log(
|
||||||
|
"[GPU_CTRL] "
|
||||||
|
f"mode={self._mode} desired={self._desired_mode} "
|
||||||
|
f"health={h.get('health', 0.0):.2f} reason={h.get('reason')} "
|
||||||
|
f"weed_fps tensor={fps.get('tensor', 0.0):.1f} "
|
||||||
|
f"inf={fps.get('inferencia', 0.0):.1f} "
|
||||||
|
f"det={fps.get('deteccao', 0.0):.1f} "
|
||||||
|
f"ratios tensor={ratios.get('tensor', 0.0):.2f} "
|
||||||
|
f"inf={ratios.get('inferencia', 0.0):.2f} "
|
||||||
|
f"det={ratios.get('deteccao', 0.0):.2f} "
|
||||||
|
f"gpu={h.get('gpu_ms', 0.0):.1f}ms "
|
||||||
|
f"visual_fps seg={self.get_fps('segmentacao'):.1f} "
|
||||||
|
f"grid={self.get_fps('grid'):.1f} "
|
||||||
|
f"det={self.get_fps('deteccao'):.1f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _log(self, msg: str):
|
||||||
|
if self.mostrar_log:
|
||||||
|
try:
|
||||||
|
self.mostrar_log(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _log_warning_if_needed(self):
|
||||||
|
h = self._last_health
|
||||||
|
reason = h.get("reason", "ok")
|
||||||
|
health = float(h.get("health", 1.0) or 0.0)
|
||||||
|
|
||||||
|
# Só avisa quando é algo realmente relevante.
|
||||||
|
warning_reason = None
|
||||||
|
|
||||||
|
if reason not in ["ok", "init"]:
|
||||||
|
warning_reason = reason
|
||||||
|
elif health < 0.45:
|
||||||
|
warning_reason = "weed_health_critical"
|
||||||
|
elif health < 0.75:
|
||||||
|
warning_reason = "weed_health_low"
|
||||||
|
|
||||||
|
if warning_reason is None:
|
||||||
|
self._last_warning_reason = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# Evita repetir o mesmo aviso em todo ciclo.
|
||||||
|
if warning_reason == self._last_warning_reason:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_warning_reason = warning_reason
|
||||||
|
|
||||||
|
fps = h.get("fps", {})
|
||||||
|
self._log(
|
||||||
|
"[GPU_CTRL][WARN] "
|
||||||
|
f"{warning_reason} | "
|
||||||
|
f"mode={self._mode} desired={self._desired_mode} "
|
||||||
|
f"health={health:.2f} reason={reason} "
|
||||||
|
f"weed_fps tensor={fps.get('tensor', 0.0):.1f} "
|
||||||
|
f"inf={fps.get('inferencia', 0.0):.1f} "
|
||||||
|
f"det={fps.get('deteccao', 0.0):.1f} "
|
||||||
|
f"gpu={h.get('gpu_ms', 0.0):.1f}ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class VisualPerfMonitor:
|
||||||
|
def __init__(self, janela=120):
|
||||||
|
self.janela = janela
|
||||||
|
self.lock = threading.RLock()
|
||||||
|
self.eventos = defaultdict(lambda: deque(maxlen=janela))
|
||||||
|
self.contadores = defaultdict(int)
|
||||||
|
self.ultimo_log = 0.0
|
||||||
|
|
||||||
|
def tick(self, nome, **dados):
|
||||||
|
agora = time.time()
|
||||||
|
item = {
|
||||||
|
"ts": agora,
|
||||||
|
**dados
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
self.eventos[nome].append(item)
|
||||||
|
self.contadores[f"{nome}_count"] += 1
|
||||||
|
|
||||||
|
def inc(self, nome, n=1):
|
||||||
|
with self.lock:
|
||||||
|
self.contadores[nome] += n
|
||||||
|
|
||||||
|
def _stats_lista(self, valores):
|
||||||
|
valores = [v for v in valores if v is not None and np.isfinite(v)]
|
||||||
|
if not valores:
|
||||||
|
return {
|
||||||
|
"min": 0.0,
|
||||||
|
"med": 0.0,
|
||||||
|
"p95": 0.0,
|
||||||
|
"max": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
arr = np.asarray(valores, dtype=np.float32)
|
||||||
|
return {
|
||||||
|
"min": float(np.min(arr)),
|
||||||
|
"med": float(np.mean(arr)),
|
||||||
|
"p95": float(np.percentile(arr, 95)),
|
||||||
|
"max": float(np.max(arr))
|
||||||
|
}
|
||||||
|
|
||||||
|
def resumo_loop(self, nome):
|
||||||
|
with self.lock:
|
||||||
|
evs = list(self.eventos.get(nome, []))
|
||||||
|
|
||||||
|
if len(evs) < 2:
|
||||||
|
return {
|
||||||
|
"fps_real": 0.0,
|
||||||
|
"periodo_ms": {},
|
||||||
|
"latencia_ms": {},
|
||||||
|
"idade_ms": {},
|
||||||
|
"metrics_ms": {},
|
||||||
|
"n": len(evs),
|
||||||
|
}
|
||||||
|
|
||||||
|
tss = [e["ts"] for e in evs]
|
||||||
|
dts = np.diff(tss)
|
||||||
|
|
||||||
|
fps_real = 1.0 / max(float(np.mean(dts)), 1e-6)
|
||||||
|
|
||||||
|
latencias = [e.get("latencia_ms") for e in evs]
|
||||||
|
idades = [e.get("idade_frame_ms") for e in evs]
|
||||||
|
|
||||||
|
metrics_ms = {}
|
||||||
|
|
||||||
|
ignorar = {
|
||||||
|
"ts",
|
||||||
|
"frame_ts",
|
||||||
|
"depth_ts",
|
||||||
|
"seg_ts",
|
||||||
|
"det_ts",
|
||||||
|
"seq",
|
||||||
|
"seq_delta",
|
||||||
|
"frames_descartados",
|
||||||
|
"n_pkts_getall",
|
||||||
|
"valido",
|
||||||
|
"n_dets",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k in evs[-1].keys():
|
||||||
|
if k in ignorar:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not (
|
||||||
|
k.endswith("_ms")
|
||||||
|
or k in ["latencia_ms", "idade_frame_ms"]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
vals = []
|
||||||
|
for e in evs:
|
||||||
|
v = e.get(k)
|
||||||
|
if isinstance(v, (int, float)) and np.isfinite(v):
|
||||||
|
vals.append(float(v))
|
||||||
|
|
||||||
|
if vals:
|
||||||
|
metrics_ms[k] = self._stats_lista(vals)
|
||||||
|
|
||||||
|
ultimo = evs[-1] if evs else {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"fps_real": float(fps_real),
|
||||||
|
"periodo_ms": self._stats_lista([dt * 1000.0 for dt in dts]),
|
||||||
|
"latencia_ms": self._stats_lista(latencias),
|
||||||
|
"idade_ms": self._stats_lista(idades),
|
||||||
|
"metrics_ms": metrics_ms,
|
||||||
|
"last": ultimo,
|
||||||
|
"n": len(evs),
|
||||||
|
}
|
||||||
|
|
||||||
|
def resumo(self):
|
||||||
|
nomes = [
|
||||||
|
"camera_rgb",
|
||||||
|
"camera_depth",
|
||||||
|
"tensor",
|
||||||
|
"inferencia",
|
||||||
|
"segmentacao",
|
||||||
|
"deteccao",
|
||||||
|
"grid",
|
||||||
|
"stream",
|
||||||
|
"publicacao",
|
||||||
|
]
|
||||||
|
|
||||||
|
saida = {
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"loops": {},
|
||||||
|
"contadores": dict(self.contadores)
|
||||||
|
}
|
||||||
|
|
||||||
|
for nome in nomes:
|
||||||
|
saida["loops"][nome] = self.resumo_loop(nome)
|
||||||
|
|
||||||
|
# Sincronismo RGB/depth
|
||||||
|
with self.lock:
|
||||||
|
rgb = list(self.eventos.get("camera_rgb", []))
|
||||||
|
depth = list(self.eventos.get("camera_depth", []))
|
||||||
|
|
||||||
|
if rgb and depth:
|
||||||
|
rgb_ts = rgb[-1].get("frame_ts_host", rgb[-1].get("ts"))
|
||||||
|
depth_ts = depth[-1].get("frame_ts_host", depth[-1].get("ts"))
|
||||||
|
saida["sync"] = {
|
||||||
|
"rgb_depth_dt_ms": float(abs(rgb_ts - depth_ts) * 1000.0),
|
||||||
|
"rgb_age_ms": float((time.time() - rgb_ts) * 1000.0),
|
||||||
|
"depth_age_ms": float((time.time() - depth_ts) * 1000.0),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
saida["sync"] = {
|
||||||
|
"rgb_depth_dt_ms": None,
|
||||||
|
"rgb_age_ms": None,
|
||||||
|
"depth_age_ms": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return saida
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -40,50 +40,80 @@ _CONFIG_LOCK = threading.Lock()
|
||||||
def load_seg_config(force_reload=False):
|
def load_seg_config(force_reload=False):
|
||||||
global _CONFIG_CACHE, _CONFIG_MTIME
|
global _CONFIG_CACHE, _CONFIG_MTIME
|
||||||
with _CONFIG_LOCK:
|
with _CONFIG_LOCK:
|
||||||
#try:
|
|
||||||
# mtime = os.path.getmtime(_CONFIG_PATH)
|
|
||||||
# if force_reload or _CONFIG_CACHE is None or mtime != _CONFIG_MTIME:
|
|
||||||
# with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
||||||
# _CONFIG_CACHE = json.load(f)
|
|
||||||
# _CONFIG_MTIME = mtime
|
|
||||||
#except Exception as e:
|
|
||||||
# mostrar_log(f"Erro ao ler config: {e}")
|
|
||||||
# if _CONFIG_CACHE is None:
|
|
||||||
# # Valores default se der ruim no primeiro load
|
|
||||||
# _CONFIG_CACHE = {
|
|
||||||
# "debug_visual": True,
|
|
||||||
# "frames_consecutivos": 3,
|
|
||||||
# "frames_histerese": 2,
|
|
||||||
# "min_area_px": 400,
|
|
||||||
# "max_area_frac": 0.2,
|
|
||||||
# "area_atuacao_bicos": 0.1,
|
|
||||||
# "ia_roi_begin": 0.0,
|
|
||||||
# "ia_roi_size": 1.0,
|
|
||||||
# "ia_resolution": [512,288],
|
|
||||||
# "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
|
|
||||||
# }
|
|
||||||
_CONFIG_CACHE = {
|
_CONFIG_CACHE = {
|
||||||
"debug_visual": False,
|
"debug_visual": False,
|
||||||
|
"debug_perf": False,
|
||||||
"ia_roi_begin": 0.0,
|
"ia_roi_begin": 0.0,
|
||||||
"ia_roi_size": 1.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,
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"ia_resolution": [1024,576],
|
"ia_resolution": [1024,576],
|
||||||
"seg_every_n": 1,
|
"seg_every_n": 1,
|
||||||
"det_every_n": 3,
|
"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
|
||||||
}
|
}
|
||||||
_CONFIG_CACHE["ia_model_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_model_ruas_seg")
|
_equipamento = ContextoGlobalRedis.get_equipamento()
|
||||||
_CONFIG_CACHE["ia_labelmap_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_labelmap_ruas_seg")
|
_CONFIG_CACHE["ia_mode"] = _equipamento.get("ia_mode_ruas")
|
||||||
_CONFIG_CACHE["ia_norm_stats_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_norm_stats_ruas_seg")
|
_CONFIG_CACHE["ia_backbone"] = _equipamento.get("ia_backbone_ruas_seg")
|
||||||
_CONFIG_CACHE["ia_backbone"] = ContextoGlobalRedis.get_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
|
return _CONFIG_CACHE
|
||||||
|
|
||||||
def reload_seg_config():
|
def reload_seg_config():
|
||||||
|
|
@ -97,7 +127,7 @@ def load_det_config():
|
||||||
"ia_roi_size": 1.0,
|
"ia_roi_size": 1.0,
|
||||||
"ia_resolution": [300,300],
|
"ia_resolution": [300,300],
|
||||||
"seg_every_n": 1,
|
"seg_every_n": 1,
|
||||||
"det_every_n": 3,
|
"det_every_n": 1,
|
||||||
"ia_conf": 0.5,
|
"ia_conf": 0.5,
|
||||||
"classes": [
|
"classes": [
|
||||||
"background",
|
"background",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ class SegmentacaoManager:
|
||||||
def __init__(self, color_map, classes):
|
def __init__(self, color_map, classes):
|
||||||
from visual_worker.config import load_seg_config
|
from visual_worker.config import load_seg_config
|
||||||
config = load_seg_config()
|
config = load_seg_config()
|
||||||
|
self.runtime_fast = bool(config.get("runtime_fast", True))
|
||||||
|
self.gerar_mask_color = bool(config.get("gerar_mask_color", False))
|
||||||
|
self.gerar_debug_status = bool(config.get("gerar_debug_status", False))
|
||||||
|
self.usar_connected_components = bool(config.get("usar_connected_components", True))
|
||||||
|
self.usar_corridor_grid = bool(config.get("usar_corridor_grid", True))
|
||||||
|
|
||||||
resolucao = config.get("ia_resolution")
|
resolucao = config.get("ia_resolution")
|
||||||
self.color_map = color_map
|
self.color_map = color_map
|
||||||
self.classes = classes
|
self.classes = classes
|
||||||
|
|
@ -49,40 +55,65 @@ class SegmentacaoManager:
|
||||||
self._status_hist = deque(maxlen=self.max_len)
|
self._status_hist = deque(maxlen=self.max_len)
|
||||||
self._status_final_hist = deque(maxlen=2)
|
self._status_final_hist = deque(maxlen=2)
|
||||||
|
|
||||||
|
self.debug_timing = bool(config.get("debug_timing_segmentacao", False))
|
||||||
|
|
||||||
def segmentar(self, predictions):
|
|
||||||
|
def segmentar(self, predictions, aux_result=None):
|
||||||
try:
|
try:
|
||||||
# 🔸 Constrói a máscara colorida e outras saídas com base na predictions já pronta
|
t0 = time.perf_counter()
|
||||||
resultado = self._segmentar_predictions(predictions)
|
|
||||||
|
resultado = self._segmentar_predictions(
|
||||||
|
predictions,
|
||||||
|
gerar_mask_color=self.gerar_mask_color
|
||||||
|
)
|
||||||
|
t_mask = time.perf_counter()
|
||||||
|
|
||||||
if resultado is None:
|
if resultado is None:
|
||||||
print("[Erro] Segmentação vazia ou falhou")
|
print("[Erro] Segmentação vazia ou falhou")
|
||||||
return None
|
return None, self.log
|
||||||
|
|
||||||
if predictions is None:
|
if predictions is None:
|
||||||
print("[Erro] Máscara de classes não encontrada no resultado")
|
print("[Erro] Máscara de classes não encontrada no resultado")
|
||||||
return None
|
return None, self.log
|
||||||
|
|
||||||
self.dados_visuais = self._analisar_corredor_visual(predictions)
|
self.dados_visuais = self._analisar_corredor_visual(
|
||||||
|
predictions,
|
||||||
|
aux_result=aux_result
|
||||||
|
)
|
||||||
|
t_ana = time.perf_counter()
|
||||||
|
|
||||||
resultado["dados_visuais"] = self.dados_visuais
|
resultado["dados_visuais"] = self.dados_visuais
|
||||||
|
|
||||||
|
if getattr(self, "debug_timing", False):
|
||||||
|
print(
|
||||||
|
f"SEGMENTACAO_MANAGER | "
|
||||||
|
f"mask={(t_mask-t0)*1000:.1f}ms | "
|
||||||
|
f"analise={(t_ana-t_mask)*1000:.1f}ms | "
|
||||||
|
f"total={(t_ana-t0)*1000:.1f}ms"
|
||||||
|
)
|
||||||
|
|
||||||
return resultado, self.log
|
return resultado, self.log
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log = f"❌ Erro na segmentação: {e}"
|
self.log = f"❌ Erro na segmentação: {e}"
|
||||||
return None, self.log
|
return None, self.log
|
||||||
|
|
||||||
def _segmentar_predictions(self, predictions):
|
def _segmentar_predictions(self, predictions, gerar_mask_color=None):
|
||||||
try:
|
try:
|
||||||
self.pred_rgb[:] = self.lut[predictions]
|
if gerar_mask_color is None:
|
||||||
|
gerar_mask_color = self.gerar_mask_color
|
||||||
|
|
||||||
mask_color = self.pred_rgb
|
mask_color = None
|
||||||
frame_color = encode_image_base64(mask_color)
|
|
||||||
|
if gerar_mask_color:
|
||||||
|
self.pred_rgb[:] = self.lut[predictions]
|
||||||
|
mask_color = self.pred_rgb
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"frame": {
|
"frame": {
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"frame": frame_color
|
"frame": None
|
||||||
},
|
},
|
||||||
"mask_color": mask_color,
|
"mask_color": mask_color,
|
||||||
"classes": predictions
|
"classes": predictions
|
||||||
|
|
@ -92,12 +123,15 @@ class SegmentacaoManager:
|
||||||
print(f"Erro ao processar predictions: {e}")
|
print(f"Erro ao processar predictions: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _analisar_corredor_visual(self, predictions, grid_rows_y_px=None, near_is_bottom=True):
|
def _analisar_corredor_visual(self, predictions, aux_result=None, grid_rows_y_px=None, near_is_bottom=True):
|
||||||
# --- inputs/base ---
|
# --- inputs/base ---
|
||||||
self.predictions = predictions
|
self.predictions = predictions
|
||||||
H, W = self.predictions.shape
|
H, W = self.predictions.shape
|
||||||
cx_img = W // 2
|
cx_img = W // 2
|
||||||
|
|
||||||
|
status_modelo_now, status_final_fast, status_before_fast, debug_fast = self._resolver_status_modelo_rapido(aux_result)
|
||||||
|
status_fast_ok = status_final_fast is not None
|
||||||
|
|
||||||
mask_corredor = self._extrair_corredor_principal(self.predictions).astype(bool)
|
mask_corredor = self._extrair_corredor_principal(self.predictions).astype(bool)
|
||||||
|
|
||||||
# --- score de corredor por grid (robusto) ---
|
# --- score de corredor por grid (robusto) ---
|
||||||
|
|
@ -143,24 +177,9 @@ class SegmentacaoManager:
|
||||||
if len(pts) >= 2:
|
if len(pts) >= 2:
|
||||||
ys_fit = np.array([p[1] for p in pts], dtype=np.float32)
|
ys_fit = np.array([p[1] for p in pts], dtype=np.float32)
|
||||||
xs_fit = np.array([p[0] for p in pts], dtype=np.float32)
|
xs_fit = np.array([p[0] for p in pts], dtype=np.float32)
|
||||||
n = len(ys_fit)
|
a = self._fit_linha_ponderada(ys_fit, xs_fit, near_is_bottom=near_is_bottom)
|
||||||
|
if a is not None:
|
||||||
# pesos: linhas "mais perto" pesam mais
|
ang_rad = np.arctan(a)
|
||||||
if near_is_bottom:
|
|
||||||
wts = np.linspace(2.0, 1.0, n, dtype=np.float32)
|
|
||||||
else:
|
|
||||||
wts = np.linspace(1.0, 2.0, n, dtype=np.float32)
|
|
||||||
|
|
||||||
Wm = np.diag(wts)
|
|
||||||
Y = ys_fit.reshape(-1, 1)
|
|
||||||
X = np.hstack([Y, np.ones_like(Y)])
|
|
||||||
XtW = X.T @ Wm
|
|
||||||
beta = np.linalg.pinv(XtW @ X) @ (XtW @ xs_fit)
|
|
||||||
a = float(beta[0])
|
|
||||||
|
|
||||||
ang_rad = np.arctan(a)
|
|
||||||
# unwrap simples
|
|
||||||
ang_rad = (ang_rad + np.pi) % (2*np.pi) - np.pi
|
|
||||||
# EMA em graus
|
# EMA em graus
|
||||||
deg = float(np.degrees(ang_rad))
|
deg = float(np.degrees(ang_rad))
|
||||||
if getattr(self, "_ema_ang", None) is None:
|
if getattr(self, "_ema_ang", None) is None:
|
||||||
|
|
@ -189,8 +208,16 @@ class SegmentacaoManager:
|
||||||
self._ema_lat = (1 - self._ema_alpha_lat) * self._ema_lat + self._ema_alpha_lat * float(erro_lateral_pct)
|
self._ema_lat = (1 - self._ema_alpha_lat) * self._ema_lat + self._ema_alpha_lat * float(erro_lateral_pct)
|
||||||
lat_out = round(float(self._ema_lat), 3)
|
lat_out = round(float(self._ema_lat), 3)
|
||||||
|
|
||||||
mask_nav = (self.predictions == ClassesSegmentacao.NAVEGAVEL.value)
|
if status_fast_ok:
|
||||||
status_now, status_final, status_before, debug = self.classificar_status_corredor(mask_nav)
|
status_final = status_final_fast
|
||||||
|
status_before = status_before_fast
|
||||||
|
debug = debug_fast
|
||||||
|
else:
|
||||||
|
mask_nav = (self.predictions == ClassesSegmentacao.NAVEGAVEL.value)
|
||||||
|
status_now, status_final, status_before, debug = self.resolver_status_corredor(
|
||||||
|
mask_nav=mask_nav,
|
||||||
|
aux_result=aux_result
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
|
|
@ -199,12 +226,82 @@ class SegmentacaoManager:
|
||||||
"erro_angular": ang_out,
|
"erro_angular": ang_out,
|
||||||
"erro_lateral_pct": lat_out,
|
"erro_lateral_pct": lat_out,
|
||||||
"status_corredor": status_final.value,
|
"status_corredor": status_final.value,
|
||||||
|
"status_corredor_nome": status_final.name,
|
||||||
"status_corredor_anterior": status_before.value,
|
"status_corredor_anterior": status_before.value,
|
||||||
|
"status_corredor_anterior_nome": status_before.name,
|
||||||
"status_corredor_debug": debug,
|
"status_corredor_debug": debug,
|
||||||
"centros_corredor": centros,
|
"centros_corredor": centros,
|
||||||
"larguras_px": larguras,
|
"larguras_px": larguras,
|
||||||
"confianca": round(float(score), 3),
|
"confianca": round(float(score), 3),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def resolver_status_corredor(self, mask_nav: np.ndarray, aux_result=None):
|
||||||
|
if aux_result and aux_result.get("type") == "label":
|
||||||
|
conf = float(aux_result.get("label_conf", 0.0))
|
||||||
|
label_id = int(aux_result.get("label_id", StatusCarroMapa.Indefinido.value))
|
||||||
|
|
||||||
|
if conf >= 0.70:
|
||||||
|
status_now = StatusCarroMapa(label_id)
|
||||||
|
debug = {
|
||||||
|
"origem_status": "modelo",
|
||||||
|
"status_modelo": label_id,
|
||||||
|
"status_modelo_conf": conf,
|
||||||
|
"heuristica_executada": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
self._status_hist.append((status_now, self._now()))
|
||||||
|
status_final = self._maioria_ultimos()
|
||||||
|
self._status_final_hist.append(status_final)
|
||||||
|
status_before = self._status_final_hist[0] if len(self._status_final_hist) > 1 else status_final
|
||||||
|
|
||||||
|
return status_now, status_final, status_before, debug
|
||||||
|
|
||||||
|
status_heur, _, _, debug_heur = self.classificar_status_corredor(mask_nav)
|
||||||
|
|
||||||
|
status_modelo = None
|
||||||
|
label_conf = 0.0
|
||||||
|
label_probs = None
|
||||||
|
label_name = None
|
||||||
|
|
||||||
|
if aux_result and aux_result.get("type") == "label":
|
||||||
|
try:
|
||||||
|
label_id = int(aux_result.get("label_id"))
|
||||||
|
label_conf = float(aux_result.get("label_conf", 0.0))
|
||||||
|
label_probs = aux_result.get("label_probs")
|
||||||
|
label_name = aux_result.get("label_name")
|
||||||
|
status_modelo = StatusCarroMapa(label_id)
|
||||||
|
except Exception:
|
||||||
|
status_modelo = None
|
||||||
|
|
||||||
|
if status_modelo is not None and label_conf >= 0.70:
|
||||||
|
status_now = status_modelo
|
||||||
|
origem = "modelo"
|
||||||
|
elif status_modelo is not None and label_conf >= 0.45:
|
||||||
|
status_now = status_modelo
|
||||||
|
origem = "modelo_baixa_conf"
|
||||||
|
else:
|
||||||
|
status_now = status_heur
|
||||||
|
origem = "heuristica"
|
||||||
|
|
||||||
|
self._status_hist.append((status_now, self._now()))
|
||||||
|
status_final = self._maioria_ultimos()
|
||||||
|
|
||||||
|
self._status_final_hist.append(status_final)
|
||||||
|
status_before = self._status_final_hist[0] if len(self._status_final_hist) > 1 else status_final
|
||||||
|
|
||||||
|
debug = {
|
||||||
|
"origem_status": origem,
|
||||||
|
"status_modelo": status_modelo.value if status_modelo is not None else None,
|
||||||
|
"status_modelo_nome": status_modelo.name if status_modelo is not None else None,
|
||||||
|
"status_modelo_label_name": label_name,
|
||||||
|
"status_modelo_conf": round(label_conf, 4),
|
||||||
|
"status_modelo_probs": label_probs,
|
||||||
|
"status_heuristico": status_heur.value,
|
||||||
|
"status_heuristico_nome": status_heur.name,
|
||||||
|
"heuristica_debug": debug_heur,
|
||||||
|
}
|
||||||
|
|
||||||
|
return status_now, status_final, status_before, debug
|
||||||
|
|
||||||
def _extrair_corredor_principal(self, mask_nav):
|
def _extrair_corredor_principal(self, mask_nav):
|
||||||
H, W = mask_nav.shape
|
H, W = mask_nav.shape
|
||||||
|
|
@ -267,15 +364,22 @@ class SegmentacaoManager:
|
||||||
row_h = H // rows
|
row_h = H // rows
|
||||||
col_w = W // cols
|
col_w = W // cols
|
||||||
|
|
||||||
# fração de NAO NAVEGAVEL por célula
|
mask_nao = (mask_classes == ClassesSegmentacao.NAONAVEGAVEL.value).astype(np.uint8)
|
||||||
|
ii = cv2.integral(mask_nao) # shape (H+1, W+1)
|
||||||
|
|
||||||
frac = np.zeros((rows, cols), np.float32)
|
frac = np.zeros((rows, cols), np.float32)
|
||||||
|
|
||||||
for i in range(rows):
|
for i in range(rows):
|
||||||
y0, y1 = i*row_h, H if i==rows-1 else (i+1)*row_h
|
y0 = i * row_h
|
||||||
|
y1 = H if i == rows - 1 else (i + 1) * row_h
|
||||||
|
|
||||||
for j in range(cols):
|
for j in range(cols):
|
||||||
x0, x1 = j*col_w, W if j==cols-1 else (j+1)*col_w
|
x0 = j * col_w
|
||||||
cell = mask_classes[y0:y1, x0:x1]
|
x1 = W if j == cols - 1 else (j + 1) * col_w
|
||||||
if cell.size:
|
|
||||||
frac[i, j] = np.mean(cell == ClassesSegmentacao.NAONAVEGAVEL.value)
|
area = max(1, (y1 - y0) * (x1 - x0))
|
||||||
|
s = self._sum_integral(ii, y0, y1, x0, x1)
|
||||||
|
frac[i, j] = s / float(area)
|
||||||
|
|
||||||
# limiares por faixa (modelo em “A”: mais chão embaixo, mais cana no topo)
|
# limiares por faixa (modelo em “A”: mais chão embaixo, mais cana no topo)
|
||||||
near_lado_min, near_canal_max = 0.05, 0.70
|
near_lado_min, near_canal_max = 0.05, 0.70
|
||||||
|
|
@ -767,3 +871,63 @@ class SegmentacaoManager:
|
||||||
return overlay
|
return overlay
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro ao gerar display_segmentation_debug: {e}")
|
print(f"Erro ao gerar display_segmentation_debug: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolver_status_modelo_rapido(self, aux_result):
|
||||||
|
if not aux_result or aux_result.get("type") != "label":
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
conf = float(aux_result.get("label_conf", 0.0))
|
||||||
|
label_id = int(aux_result.get("label_id", StatusCarroMapa.Indefinido.value))
|
||||||
|
|
||||||
|
if conf < 0.70:
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
status_now = StatusCarroMapa(label_id)
|
||||||
|
|
||||||
|
self._status_hist.append((status_now, self._now()))
|
||||||
|
status_final = self._maioria_ultimos()
|
||||||
|
self._status_final_hist.append(status_final)
|
||||||
|
status_before = self._status_final_hist[0] if len(self._status_final_hist) > 1 else status_final
|
||||||
|
|
||||||
|
debug = None
|
||||||
|
if self.gerar_debug_status:
|
||||||
|
debug = {
|
||||||
|
"origem_status": "modelo_fast",
|
||||||
|
"status_modelo": label_id,
|
||||||
|
"status_modelo_conf": conf,
|
||||||
|
"heuristica_executada": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
return status_now, status_final, status_before, debug
|
||||||
|
|
||||||
|
def _fit_linha_ponderada(self, ys_fit, xs_fit, near_is_bottom=True):
|
||||||
|
n = len(ys_fit)
|
||||||
|
if n < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if near_is_bottom:
|
||||||
|
w = np.linspace(2.0, 1.0, n, dtype=np.float32)
|
||||||
|
else:
|
||||||
|
w = np.linspace(1.0, 2.0, n, dtype=np.float32)
|
||||||
|
|
||||||
|
y = ys_fit.astype(np.float32)
|
||||||
|
x = xs_fit.astype(np.float32)
|
||||||
|
|
||||||
|
sw = np.sum(w)
|
||||||
|
y_mean = np.sum(w * y) / max(sw, 1e-6)
|
||||||
|
x_mean = np.sum(w * x) / max(sw, 1e-6)
|
||||||
|
|
||||||
|
dy = y - y_mean
|
||||||
|
dx = x - x_mean
|
||||||
|
|
||||||
|
denom = np.sum(w * dy * dy)
|
||||||
|
if abs(denom) < 1e-6:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
a = np.sum(w * dy * dx) / denom
|
||||||
|
return float(a)
|
||||||
|
|
||||||
|
def _sum_integral(self, ii, y0, y1, x0, x1):
|
||||||
|
return ii[y1, x1] - ii[y0, x1] - ii[y1, x0] + ii[y0, x0]
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -42,47 +42,44 @@ _CONFIG_LOCK = threading.Lock()
|
||||||
def load_seg_config(force_reload=False):
|
def load_seg_config(force_reload=False):
|
||||||
global _CONFIG_CACHE, _CONFIG_MTIME
|
global _CONFIG_CACHE, _CONFIG_MTIME
|
||||||
with _CONFIG_LOCK:
|
with _CONFIG_LOCK:
|
||||||
#try:
|
|
||||||
# mtime = os.path.getmtime(_CONFIG_PATH)
|
|
||||||
# if force_reload or _CONFIG_CACHE is None or mtime != _CONFIG_MTIME:
|
|
||||||
# with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
||||||
# _CONFIG_CACHE = json.load(f)
|
|
||||||
# _CONFIG_MTIME = mtime
|
|
||||||
#except Exception as e:
|
|
||||||
# mostrar_log(f"Erro ao ler config: {e}")
|
|
||||||
# if _CONFIG_CACHE is None:
|
|
||||||
# # Valores default se der ruim no primeiro load
|
|
||||||
# _CONFIG_CACHE = {
|
|
||||||
# "debug_visual": True,
|
|
||||||
# "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,
|
|
||||||
# "ia_resolution": [512,288],
|
|
||||||
# "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
|
|
||||||
# }
|
|
||||||
_CONFIG_CACHE = {
|
_CONFIG_CACHE = {
|
||||||
"debug_visual": False,
|
"debug_visual": False,
|
||||||
|
"debug_perf": False,
|
||||||
"frames_consecutivos": 3,
|
"frames_consecutivos": 3,
|
||||||
"frames_histerese": 2,
|
"frames_histerese": 2,
|
||||||
"min_area_px": 400,
|
"min_area_px": 400,
|
||||||
"max_area_frac": 0.2,
|
"max_area_frac": 0.2,
|
||||||
"ia_roi_begin": 0.0,
|
"ia_roi_begin": 0.0,
|
||||||
"ia_roi_size": 1.0,
|
"ia_roi_size": 1.0,
|
||||||
"ia_resolution": [672,544],
|
|
||||||
|
"analise_fps": 15.0,
|
||||||
|
"inferencia_fps": 15.0,
|
||||||
|
"deteccao_fps": 15.0,
|
||||||
|
"tensor_fps": 18.0,
|
||||||
|
"publicacao_fps": 15.0,
|
||||||
|
|
||||||
|
"tipo_camera_solo": "multispectral",
|
||||||
|
"camera_width": 1280,
|
||||||
|
"camera_height": 800,
|
||||||
|
"fps": 40,
|
||||||
|
"ia_resolution": [1024,640],
|
||||||
"ia_channels": 5,
|
"ia_channels": 5,
|
||||||
"ia_use_ndvi": True,
|
"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_top_band_frac": 0.30,
|
||||||
"erva_frac_ema": 0.3,
|
"erva_frac_ema": 0.3,
|
||||||
"erva_thresh_vel_gain": 0.4,
|
"erva_thresh_vel_gain": 0.4,
|
||||||
|
|
@ -102,7 +99,38 @@ def load_seg_config(force_reload=False):
|
||||||
"cana_halo_px": 5,
|
"cana_halo_px": 5,
|
||||||
"min_area_erva_px": 80,
|
"min_area_erva_px": 80,
|
||||||
"erva_thresh_vel_gain_local": 0.6,
|
"erva_thresh_vel_gain_local": 0.6,
|
||||||
"k_roi_shift_px_per_vnorm": 24.0
|
"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", {})
|
dadosAtu = ContextoGlobalRedis.get_operacao().get("Atu", {})
|
||||||
contexto = ContextoGlobalRedis.get_contexto()
|
contexto = ContextoGlobalRedis.get_contexto()
|
||||||
|
|
@ -113,12 +141,39 @@ def load_seg_config(force_reload=False):
|
||||||
_CONFIG_CACHE["ia_model_path"] = ContextoGlobalRedis.get_equipamento().get("path_ia_model_ervas")
|
_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_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_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["ia_backbone"] = ContextoGlobalRedis.get_equipamento().get("ia_backbone_ervas")
|
||||||
_CONFIG_CACHE["faixa_atuacao_bicos"] = dadosAtu.get("percent_vertical_deteccao", 0.7)
|
|
||||||
|
|
||||||
|
_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["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_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)
|
_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
|
return _CONFIG_CACHE
|
||||||
|
|
||||||
def reload_seg_config():
|
def reload_seg_config():
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,41 @@ class WeedDetector:
|
||||||
resolucao = config.get("ia_resolution")
|
resolucao = config.get("ia_resolution")
|
||||||
self.color_map = color_map
|
self.color_map = color_map
|
||||||
self.classes = classes
|
self.classes = classes
|
||||||
|
self.runtime_mode = str(config.get("runtime_mode", "semantic")).lower()
|
||||||
|
self.prediction_contract = str(config.get("prediction_contract", "") or "").lower()
|
||||||
|
if not self.prediction_contract:
|
||||||
|
if self.runtime_mode in ("target_direct", "direct_target", "target_head", "target", "spray", "operational"):
|
||||||
|
self.prediction_contract = "target_binary"
|
||||||
|
else:
|
||||||
|
self.prediction_contract = "semantic"
|
||||||
|
if self.prediction_contract in ("target_binary", "binary_target", "target"):
|
||||||
|
self.classes = {"background": 0, "target": 1}
|
||||||
|
self.color_map = [
|
||||||
|
(30, 30, 30), # background
|
||||||
|
(255, 70, 30), # target
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.classes = classes
|
||||||
|
self.color_map = color_map
|
||||||
self.resolucao = (resolucao[0], resolucao[1])
|
self.resolucao = (resolucao[0], resolucao[1])
|
||||||
self.color_lut = np.array(self.color_map, np.uint8)
|
# LUT completa 0..255 em BGR, segura para OpenCV e ignore_id.
|
||||||
self.lut = np.zeros((256, 3), dtype=np.uint8)
|
self.lut = np.zeros((256, 3), dtype=np.uint8)
|
||||||
for i, color in enumerate(color_map):
|
|
||||||
#self.lut[i] = color
|
for i, color in enumerate(self.color_map):
|
||||||
self.lut[i] = (color[2], color[1], color[0]) # converte pra (B, G, R)
|
if i >= 256:
|
||||||
|
break
|
||||||
|
|
||||||
|
# color_map vem em RGB. OpenCV/debug/base64 atual usa BGR.
|
||||||
|
r, g, b = int(color[0]), int(color[1]), int(color[2])
|
||||||
|
self.lut[i] = (b, g, r)
|
||||||
|
|
||||||
IGNORE_ID = 255
|
IGNORE_ID = 255
|
||||||
self.lut[IGNORE_ID] = (255, 255, 255)
|
self.lut[IGNORE_ID] = (255, 255, 255)
|
||||||
|
|
||||||
|
# Mantém compatibilidade com métodos antigos de debug.
|
||||||
|
# Antes era array curto; agora fica LUT completa também.
|
||||||
|
self.color_lut = self.lut
|
||||||
|
|
||||||
self._reiniciar_deteccoes()
|
self._reiniciar_deteccoes()
|
||||||
self.use_mock = False
|
self.use_mock = False
|
||||||
self.img_mock = "C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\bin\\x64\\Debug\\Operacoes\\25_07_2025_14_39_14\\Cam0\\85_rgb.jpeg"
|
self.img_mock = "C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\bin\\x64\\Debug\\Operacoes\\25_07_2025_14_39_14\\Cam0\\85_rgb.jpeg"
|
||||||
|
|
@ -52,11 +78,39 @@ class WeedDetector:
|
||||||
self.ervas_registradas_bico = [set() for _ in range(qtd_bicos)]
|
self.ervas_registradas_bico = [set() for _ in range(qtd_bicos)]
|
||||||
self.pred_rgb = np.empty((self.resolucao[1], self.resolucao[0], 3), dtype=np.uint8)
|
self.pred_rgb = np.empty((self.resolucao[1], self.resolucao[0], 3), dtype=np.uint8)
|
||||||
|
|
||||||
# LUTs
|
# LUTs de interpretação da máscara de entrada.
|
||||||
|
# semantic:
|
||||||
|
# 0=chao, 1=cana, 2=erva
|
||||||
|
# target_binary:
|
||||||
|
# 0=background, 1=target/alvo pulverizável
|
||||||
self._is_weed = np.zeros(256, dtype=bool)
|
self._is_weed = np.zeros(256, dtype=bool)
|
||||||
self._is_weed[int(ClassesSegmentacao.ERVA.value)] = True
|
|
||||||
self._is_cane = np.zeros(256, dtype=bool)
|
self._is_cane = np.zeros(256, dtype=bool)
|
||||||
self._is_cane[int(ClassesSegmentacao.CANA.value)] = True
|
|
||||||
|
runtime_mode = str(config.get("runtime_mode", getattr(self, "runtime_mode", "semantic"))).lower()
|
||||||
|
contract = str(config.get("prediction_contract", getattr(self, "prediction_contract", "")) or "").lower()
|
||||||
|
|
||||||
|
if not contract:
|
||||||
|
if runtime_mode in ("target_direct", "direct_target", "target_head", "target", "spray", "operational"):
|
||||||
|
contract = "target_binary"
|
||||||
|
else:
|
||||||
|
contract = "semantic"
|
||||||
|
|
||||||
|
self.prediction_contract = contract
|
||||||
|
|
||||||
|
if contract in ("target_binary", "binary_target", "target"):
|
||||||
|
# A máscara já é o alvo final: 1 = pulverizável.
|
||||||
|
self._is_weed[1] = True
|
||||||
|
|
||||||
|
# Não há classe cana nessa saída.
|
||||||
|
# O veto por cana fica naturalmente inativo porque mask_cana será tudo False.
|
||||||
|
self._is_cane[:] = False
|
||||||
|
|
||||||
|
elif contract in ("semantic", "semantic_3class"):
|
||||||
|
self._is_weed[int(ClassesSegmentacao.ERVA.value)] = True
|
||||||
|
self._is_cane[int(ClassesSegmentacao.CANA.value)] = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"prediction_contract inválido: {contract}")
|
||||||
|
|
||||||
self._inv_total = 1.0 / (self.resolucao[1] * self.resolucao[0])
|
self._inv_total = 1.0 / (self.resolucao[1] * self.resolucao[0])
|
||||||
|
|
||||||
|
|
@ -427,7 +481,7 @@ class WeedDetector:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro ao mostrar debug: {e}")
|
print(f"Erro ao mostrar debug: {e}")
|
||||||
|
|
||||||
def _mostrar_debug_bicos_overlay(self, overlay_bgr, detections, atuacao_bicos, config=None, show: bool = True):
|
def _mostrar_debug_bicos_overlay(self, overlay_bgr, detections, atuacao_bicos, config=None, show: bool = True, metricas_perf=None):
|
||||||
"""
|
"""
|
||||||
Versão otimizada do debug: recebe o overlay BGR já montado
|
Versão otimizada do debug: recebe o overlay BGR já montado
|
||||||
(RGB + segmentação) e só desenha faixa, bicos, bboxes e HUD.
|
(RGB + segmentação) e só desenha faixa, bicos, bboxes e HUD.
|
||||||
|
|
@ -545,6 +599,12 @@ class WeedDetector:
|
||||||
(0, 255, 0),
|
(0, 255, 0),
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metricas_perf = metricas_perf or {}
|
||||||
|
fps_infer = float(metricas_perf.get("fps_infer") or 0.0)
|
||||||
|
infer_ms = float(metricas_perf.get("infer_ms") or 0.0)
|
||||||
|
infer_gpu_ms = float(metricas_perf.get("infer_gpu_ms") or 0.0)
|
||||||
|
fps_loop = float(metricas_perf.get("fps_loop") or 0.0)
|
||||||
cv2.putText(
|
cv2.putText(
|
||||||
self._dbg_img,
|
self._dbg_img,
|
||||||
f"Dbg FPS: {dbg_fps:.1f}",
|
f"Dbg FPS: {dbg_fps:.1f}",
|
||||||
|
|
@ -554,13 +614,34 @@ class WeedDetector:
|
||||||
(0, 255, 0),
|
(0, 255, 0),
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
cv2.putText(
|
||||||
|
self._dbg_img,
|
||||||
|
f"Infer FPS: {fps_infer:.1f} | infer: {infer_ms:.1f}ms",
|
||||||
|
(10, 90),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.75,
|
||||||
|
(0, 255, 255),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
cv2.putText(
|
||||||
|
self._dbg_img,
|
||||||
|
f"Loop FPS: {fps_loop:.1f} | GPU: {infer_gpu_ms:.1f}ms",
|
||||||
|
(10, 120),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.75,
|
||||||
|
(0, 255, 255),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
if (show):
|
if (show):
|
||||||
cv2.imshow("Debug Weed Worker", self._dbg_img)
|
cv2.imshow("Debug Weed Worker", self._dbg_img)
|
||||||
cv2.waitKey(1)
|
cv2.waitKey(1)
|
||||||
|
|
||||||
|
return self._dbg_img
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro ao mostrar debug (overlay): {e}")
|
print(f"Erro ao mostrar debug (overlay): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def _fps_update(self, last_ts_attr: str, ema_attr: str, alpha: float = 0.2):
|
def _fps_update(self, last_ts_attr: str, ema_attr: str, alpha: float = 0.2):
|
||||||
"""Atualiza e retorna FPS (EMA) baseado no timestamp anterior salvo em self"""
|
"""Atualiza e retorna FPS (EMA) baseado no timestamp anterior salvo em self"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue