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