This commit is contained in:
Diego Freitas 2026-05-21 19:41:46 -03:00
parent 01f17ae988
commit d42a7d4005
26 changed files with 19036 additions and 959 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}")

View File

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

View File

@ -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):
"""
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"
)

View File

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

View File

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

View File

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

View File

@ -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():

View File

@ -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 montado Versão otimizada do debug: recebe o overlay BGR montado
(RGB + segmentação) e desenha faixa, bicos, bboxes e HUD. (RGB + segmentação) e 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"""