ajustes mpc e raw_processor_core
This commit is contained in:
parent
ca8b0123b5
commit
fec398d5a4
|
|
@ -38,7 +38,7 @@ namespace AgroBase.Services
|
||||||
new DispositivoDetalhesModel()
|
new DispositivoDetalhesModel()
|
||||||
{
|
{
|
||||||
Dispositivo = T_Code.Npc,
|
Dispositivo = T_Code.Npc,
|
||||||
Endereco = EthernetService.ObterIpAtual(null),
|
Endereco = EthernetService.ObterIpAtual(VariaveisEquipamento.Parametros.rover_interface),
|
||||||
Versao = Variaveis.Versao,
|
Versao = Variaveis.Versao,
|
||||||
Mod_ID = ""
|
Mod_ID = ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import time
|
import time
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from shared.enums import ModoOperacao, StatusCarroMapa, StatusModulo, T_Code, TipoMovimentoDirecional, TiposControladorDirecional
|
from shared.enums import ModoOperacao, StatusCarroMapa, StatusModulo, T_Code, TipoMovimentoDirecional, TiposControladorDirecional
|
||||||
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
|
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
|
||||||
|
|
@ -17,46 +18,81 @@ def definir_comando(pid: PIDAdaptativo, envio_necessario: bool):
|
||||||
_equipamento = ContextoGlobalRedis.get_equipamento()
|
_equipamento = ContextoGlobalRedis.get_equipamento()
|
||||||
_trajetoria = _contexto.get("Trajetoria", {})
|
_trajetoria = _contexto.get("Trajetoria", {})
|
||||||
|
|
||||||
usar_dados_sonar = _controle.get("dir_auxilio_sonar", False)
|
usar_auxilio_visual = _controle.get("dir_auxilio_sonar", False)
|
||||||
dados_visual_worker = None
|
dados_visual_worker = None
|
||||||
if usar_dados_sonar:
|
if usar_auxilio_visual:
|
||||||
_snr = ContextoGlobalRedis.get_modulo(T_Code.Snr)
|
_snr = ContextoGlobalRedis.get_modulo(T_Code.Snr)
|
||||||
visual_worker_operante = (_snr.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value)) == StatusModulo.OPERANTE.value
|
visual_worker_operante = (
|
||||||
|
_snr.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value)
|
||||||
|
== StatusModulo.OPERANTE.value
|
||||||
|
)
|
||||||
|
|
||||||
_dados_vw = ContextoGlobalRedis.get(CtxKey.DadosVisualWorker, {})
|
_dados_vw = ContextoGlobalRedis.get(CtxKey.DadosVisualWorker, {})
|
||||||
_snapshot_vw = _dados_vw.get("matriz_confianca")
|
_snapshot_vw = _dados_vw.get("matriz_confianca")
|
||||||
|
|
||||||
custo = None
|
custo = None
|
||||||
|
conf = None
|
||||||
|
anom = None
|
||||||
nav = None
|
nav = None
|
||||||
dists = None
|
dists = None
|
||||||
escalas = None
|
escalas = None
|
||||||
block = None
|
block = None
|
||||||
if not _snapshot_vw:
|
|
||||||
visual_worker_atualizado = False
|
max_idade_ms = 1000.0
|
||||||
else:
|
visual_worker_atualizado = False
|
||||||
visual_worker_atualizado = (time.time() - _snapshot_vw.get("ts", 0.0) <= 1.0)
|
|
||||||
|
if _snapshot_vw:
|
||||||
|
ts_snapshot = float(_snapshot_vw.get("ts", 0.0) or 0.0)
|
||||||
|
idade_ms = (time.time() - ts_snapshot) * 1000.0 if ts_snapshot > 0 else None
|
||||||
|
visual_worker_atualizado = idade_ms is not None and idade_ms <= max_idade_ms
|
||||||
|
|
||||||
if visual_worker_atualizado:
|
if visual_worker_atualizado:
|
||||||
custo, conf, anom, nav = unpack_snapshot(_snapshot_vw)
|
custo, conf, anom, nav = unpack_snapshot(_snapshot_vw)
|
||||||
dists = _snapshot_vw.get("row_dist_m", None)
|
dists = _snapshot_vw.get("row_dist_m", None)
|
||||||
escalas = _snapshot_vw.get("row_scale_x_m", None)
|
escalas = _snapshot_vw.get("row_scale_x_m", None)
|
||||||
block = _snapshot_vw.get("block", None)
|
block = _snapshot_vw.get("block", None)
|
||||||
matriz_valida = (
|
|
||||||
visual_worker_operante
|
costmap_direcional = _montar_costmap_direcional(
|
||||||
and visual_worker_atualizado
|
habilitado=usar_auxilio_visual,
|
||||||
and _snapshot_vw is not None
|
operante=visual_worker_operante,
|
||||||
and custo is not None
|
atualizado=visual_worker_atualizado,
|
||||||
and nav is not None
|
snapshot=_snapshot_vw,
|
||||||
and dists is not None
|
custo=custo,
|
||||||
and escalas is not None
|
nav=nav,
|
||||||
and block is not None
|
dists=dists,
|
||||||
|
escalas=escalas,
|
||||||
|
conf=conf,
|
||||||
|
anom=anom,
|
||||||
|
block=block,
|
||||||
|
max_idade_ms=max_idade_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
|
matriz_valida = bool(costmap_direcional.get("valido", False))
|
||||||
|
|
||||||
dados_visual_worker = {
|
dados_visual_worker = {
|
||||||
"Operante": visual_worker_operante,
|
"Operante": visual_worker_operante,
|
||||||
"Atualizado": visual_worker_atualizado,
|
"Atualizado": visual_worker_atualizado,
|
||||||
|
|
||||||
|
# Contrato novo, será o principal daqui pra frente.
|
||||||
|
"CostMapDirecional": costmap_direcional,
|
||||||
|
|
||||||
|
# Compatibilidade temporária com o MPC atual.
|
||||||
"MatrizCusto": {
|
"MatrizCusto": {
|
||||||
"Valida": matriz_valida,
|
"Valida": matriz_valida,
|
||||||
"Custo": custo,
|
"EmUso": matriz_valida,
|
||||||
"Navegavel": nav,
|
"Custo": custo,
|
||||||
"DistanciasRef": dists,
|
"Navegavel": nav,
|
||||||
|
"DistanciasRef": dists,
|
||||||
"EscalasX": escalas,
|
"EscalasX": escalas,
|
||||||
|
|
||||||
|
# extras úteis mesmo no contrato antigo
|
||||||
|
"Confianca": conf,
|
||||||
|
"Anomalia": anom,
|
||||||
|
"Bloqueio": block,
|
||||||
|
"Ts": costmap_direcional.get("ts", 0.0),
|
||||||
|
"IdadeMs": costmap_direcional.get("idade_ms", None),
|
||||||
|
"MotivoInvalido": costmap_direcional.get("motivo_invalido", ""),
|
||||||
|
"MotivosInvalidos": costmap_direcional.get("motivos_invalidos", [])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,3 +316,231 @@ def _montar_comando_retorno(comando, latencia: float = -1):
|
||||||
if latencia > -1:
|
if latencia > -1:
|
||||||
comando["latencia"] = latencia
|
comando["latencia"] = latencia
|
||||||
return comando
|
return comando
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _shape_matriz(m):
|
||||||
|
try:
|
||||||
|
if m is None:
|
||||||
|
return {"linhas": 0, "colunas": 0}
|
||||||
|
s = getattr(m, "shape", None)
|
||||||
|
if s is None or len(s) < 2:
|
||||||
|
return {"linhas": 0, "colunas": 0}
|
||||||
|
return {"linhas": int(s[0]), "colunas": int(s[1])}
|
||||||
|
except Exception:
|
||||||
|
return {"linhas": 0, "colunas": 0}
|
||||||
|
|
||||||
|
def _mesmo_shape(a, b):
|
||||||
|
try:
|
||||||
|
if a is None or b is None:
|
||||||
|
return False
|
||||||
|
return tuple(a.shape) == tuple(b.shape)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _len_igual_shape_linhas(v, matriz):
|
||||||
|
try:
|
||||||
|
if v is None or matriz is None:
|
||||||
|
return False
|
||||||
|
return len(v) == int(matriz.shape[0])
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _resumo_matriz(custo, nav, conf=None, anom=None, block=None):
|
||||||
|
resumo = {
|
||||||
|
"frac_navegavel": None,
|
||||||
|
"frac_bloqueado": None,
|
||||||
|
"custo_min": None,
|
||||||
|
"custo_max": None,
|
||||||
|
"custo_medio": None,
|
||||||
|
"confianca_media": None,
|
||||||
|
"anomalia_media": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if nav is not None:
|
||||||
|
nav_np = np.asarray(nav, dtype=bool)
|
||||||
|
resumo["frac_navegavel"] = float(np.mean(nav_np))
|
||||||
|
resumo["frac_bloqueado"] = float(1.0 - np.mean(nav_np))
|
||||||
|
|
||||||
|
if block is not None:
|
||||||
|
block_np = np.asarray(block, dtype=bool)
|
||||||
|
resumo["frac_bloqueado"] = float(np.mean(block_np))
|
||||||
|
|
||||||
|
if custo is not None:
|
||||||
|
custo_np = np.asarray(custo, dtype=np.float32)
|
||||||
|
if custo_np.size > 0:
|
||||||
|
resumo["custo_min"] = float(np.nanmin(custo_np))
|
||||||
|
resumo["custo_max"] = float(np.nanmax(custo_np))
|
||||||
|
resumo["custo_medio"] = float(np.nanmean(custo_np))
|
||||||
|
|
||||||
|
if conf is not None:
|
||||||
|
conf_np = np.asarray(conf, dtype=np.float32)
|
||||||
|
if conf_np.size > 0:
|
||||||
|
resumo["confianca_media"] = float(np.nanmean(conf_np))
|
||||||
|
|
||||||
|
if anom is not None:
|
||||||
|
anom_np = np.asarray(anom, dtype=np.float32)
|
||||||
|
if anom_np.size > 0:
|
||||||
|
resumo["anomalia_media"] = float(np.nanmean(anom_np))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
mostrar_log(f"⚠️ Erro ao gerar resumo do CostMapDirecional: {e}")
|
||||||
|
|
||||||
|
return resumo
|
||||||
|
|
||||||
|
def _montar_costmap_direcional(
|
||||||
|
*,
|
||||||
|
habilitado,
|
||||||
|
operante,
|
||||||
|
atualizado,
|
||||||
|
snapshot,
|
||||||
|
custo,
|
||||||
|
nav,
|
||||||
|
dists,
|
||||||
|
escalas,
|
||||||
|
conf=None,
|
||||||
|
anom=None,
|
||||||
|
block=None,
|
||||||
|
max_idade_ms=1000.0
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
ts = 0.0
|
||||||
|
idade_ms = None
|
||||||
|
seq = None
|
||||||
|
fonte = "visual_worker"
|
||||||
|
|
||||||
|
if snapshot:
|
||||||
|
ts = float(snapshot.get("ts", 0.0) or 0.0)
|
||||||
|
idade_ms = (time.time() - ts) * 1000.0 if ts > 0 else None
|
||||||
|
seq = snapshot.get("seq", snapshot.get("id", None))
|
||||||
|
fonte = snapshot.get("fonte", snapshot.get("origem", "visual_worker"))
|
||||||
|
|
||||||
|
shape = _shape_matriz(custo)
|
||||||
|
|
||||||
|
motivos_invalidos = []
|
||||||
|
|
||||||
|
if not habilitado:
|
||||||
|
motivos_invalidos.append("auxilio_visual_desabilitado")
|
||||||
|
if not operante:
|
||||||
|
motivos_invalidos.append("visual_worker_nao_operante")
|
||||||
|
if not atualizado:
|
||||||
|
motivos_invalidos.append("snapshot_desatualizado")
|
||||||
|
if snapshot is None:
|
||||||
|
motivos_invalidos.append("snapshot_ausente")
|
||||||
|
if custo is None:
|
||||||
|
motivos_invalidos.append("matriz_custo_ausente")
|
||||||
|
if nav is None:
|
||||||
|
motivos_invalidos.append("matriz_navegavel_ausente")
|
||||||
|
if dists is None:
|
||||||
|
motivos_invalidos.append("row_dist_m_ausente")
|
||||||
|
if escalas is None:
|
||||||
|
motivos_invalidos.append("row_scale_x_m_ausente")
|
||||||
|
if block is None:
|
||||||
|
motivos_invalidos.append("block_ausente")
|
||||||
|
|
||||||
|
if custo is not None and nav is not None and not _mesmo_shape(custo, nav):
|
||||||
|
motivos_invalidos.append("shape_custo_navegavel_incompativel")
|
||||||
|
|
||||||
|
if custo is not None and conf is not None and not _mesmo_shape(custo, conf):
|
||||||
|
motivos_invalidos.append("shape_custo_confianca_incompativel")
|
||||||
|
|
||||||
|
if custo is not None and anom is not None and not _mesmo_shape(custo, anom):
|
||||||
|
motivos_invalidos.append("shape_custo_anomalia_incompativel")
|
||||||
|
|
||||||
|
if custo is not None and block is not None and not _mesmo_shape(custo, block):
|
||||||
|
motivos_invalidos.append("shape_custo_block_incompativel")
|
||||||
|
|
||||||
|
if custo is not None and dists is not None and not _len_igual_shape_linhas(dists, custo):
|
||||||
|
motivos_invalidos.append("row_dist_m_tamanho_incompativel")
|
||||||
|
|
||||||
|
if custo is not None and escalas is not None and not _len_igual_shape_linhas(escalas, custo):
|
||||||
|
motivos_invalidos.append("row_scale_x_m_tamanho_incompativel")
|
||||||
|
|
||||||
|
valido = len(motivos_invalidos) == 0
|
||||||
|
|
||||||
|
# Por enquanto, este contrato ainda descreve a convenção atual:
|
||||||
|
# row_dist_m corresponde ao vetor físico em ordem crescente,
|
||||||
|
# mas a matriz atual ainda pode estar no padrão legado/reverso.
|
||||||
|
# Na próxima etapa, o MPC normaliza isso.
|
||||||
|
contrato = {
|
||||||
|
"versao": 2,
|
||||||
|
|
||||||
|
"habilitado": bool(habilitado),
|
||||||
|
"valido": bool(valido),
|
||||||
|
"em_uso": bool(valido),
|
||||||
|
"motivo_invalido": "" if valido else ";".join(motivos_invalidos),
|
||||||
|
"motivos_invalidos": motivos_invalidos,
|
||||||
|
|
||||||
|
"ts": ts,
|
||||||
|
"idade_ms": idade_ms,
|
||||||
|
"max_idade_ms": float(max_idade_ms),
|
||||||
|
"seq": seq,
|
||||||
|
|
||||||
|
"frame": "robo",
|
||||||
|
"origem": fonte,
|
||||||
|
|
||||||
|
"shape": shape,
|
||||||
|
|
||||||
|
"geometria": {
|
||||||
|
"row_dist_m": dists,
|
||||||
|
"row_scale_x_m": escalas,
|
||||||
|
|
||||||
|
# Importante: deixa explícito para o MPC não depender de magia escondida.
|
||||||
|
# Se depois o VisualWorker já mandar matriz física direta, mudamos aqui.
|
||||||
|
"row_order": "near_to_far",
|
||||||
|
"matrix_row_order": "legacy_reversed",
|
||||||
|
"col_order": "left_to_right",
|
||||||
|
"x_zero": "centro",
|
||||||
|
"y_zero": "frente_robo",
|
||||||
|
"unidade": "m"
|
||||||
|
},
|
||||||
|
|
||||||
|
"matrizes": {
|
||||||
|
"custo": custo,
|
||||||
|
"navegavel": nav,
|
||||||
|
"confianca": conf,
|
||||||
|
"anomalia": anom,
|
||||||
|
"bloqueio": block
|
||||||
|
},
|
||||||
|
|
||||||
|
"normalizacao": {
|
||||||
|
"custo_min": 0.0,
|
||||||
|
"custo_max": 1.0,
|
||||||
|
"custo_baixo_melhor": True,
|
||||||
|
"navegavel_true_livre": True,
|
||||||
|
"bloqueio_true_ocupado": True,
|
||||||
|
"confianca_max_melhor": True,
|
||||||
|
"anomalia_max_pior": True
|
||||||
|
},
|
||||||
|
|
||||||
|
"politica": {
|
||||||
|
"usar_bloqueio_duro": True,
|
||||||
|
"limiar_bloqueio_frac": 0.20,
|
||||||
|
"limiar_hot_stop_frac": 0.45,
|
||||||
|
"peso_visual": 1.0,
|
||||||
|
"distancia_min_avaliacao_m": 0.60
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug": {
|
||||||
|
"resumo": _resumo_matriz(custo, nav, conf=conf, anom=anom, block=block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contrato
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
mostrar_log(f"❌ Erro ao montar CostMapDirecional: {e}")
|
||||||
|
return {
|
||||||
|
"versao": 2,
|
||||||
|
"habilitado": bool(habilitado),
|
||||||
|
"valido": False,
|
||||||
|
"em_uso": False,
|
||||||
|
"motivo_invalido": f"erro_montagem_contrato:{e}",
|
||||||
|
"motivos_invalidos": [f"erro_montagem_contrato:{e}"],
|
||||||
|
"matrizes": {},
|
||||||
|
"geometria": {},
|
||||||
|
"politica": {},
|
||||||
|
"debug": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,8 +9,22 @@
|
||||||
"sensor_height": 800,
|
"sensor_height": 800,
|
||||||
"bayer_pattern": "BGGR",
|
"bayer_pattern": "BGGR",
|
||||||
"rgb_processing": {
|
"rgb_processing": {
|
||||||
"mode": "linear_demosaic",
|
"mode": "bayer_planes",
|
||||||
"demosaic_algorithm": "bilinear"
|
"demosaic_algorithm": "ea",
|
||||||
|
"enhancement": {
|
||||||
|
"enabled": true,
|
||||||
|
"backend": "hybrid",
|
||||||
|
"preset": "soft",
|
||||||
|
"apply_to_preview_input": false,
|
||||||
|
"use_u8_pipeline": true,
|
||||||
|
"auto_stretch": {
|
||||||
|
"enabled": false,
|
||||||
|
"low_pct": 0.2,
|
||||||
|
"high_pct": 99.8
|
||||||
|
},
|
||||||
|
"clip_output": true,
|
||||||
|
"save_debug": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"camera_settings": {
|
"camera_settings": {
|
||||||
"rgb": {
|
"rgb": {
|
||||||
|
|
@ -239,7 +253,7 @@
|
||||||
},
|
},
|
||||||
"crop_valid_common": true,
|
"crop_valid_common": true,
|
||||||
"resize_after_crop": true,
|
"resize_after_crop": true,
|
||||||
"target_size": [1024,640]
|
"target_size": [640, 400]
|
||||||
},
|
},
|
||||||
"radiometric_config": {
|
"radiometric_config": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,40 @@ class RawProcessorCore:
|
||||||
self.rgb_processing_config = {
|
self.rgb_processing_config = {
|
||||||
"mode": "linear_demosaic", # "linear_demosaic", "linear_demosaic_half" ou "bayer_planes"
|
"mode": "linear_demosaic", # "linear_demosaic", "linear_demosaic_half" ou "bayer_planes"
|
||||||
"demosaic_algorithm": "ea", # "ea" ou "bilinear"
|
"demosaic_algorithm": "ea", # "ea" ou "bilinear"
|
||||||
|
|
||||||
|
# Pós-processamento RGB opcional, aplicado logo após o decode/debayer
|
||||||
|
# e antes da fusão com RE/NIR.
|
||||||
|
#
|
||||||
|
# backend aceitos:
|
||||||
|
# "none" / None / false
|
||||||
|
# "hybrid" / "hybrid:balanced" / "hybrid_balanced"
|
||||||
|
# "hybrid_soft", "hybrid_strong"
|
||||||
|
#
|
||||||
|
# Observação:
|
||||||
|
# - Por padrão fica desligado para manter compatibilidade total.
|
||||||
|
# - Para o experimento atual, use backend="hybrid" e preset="balanced".
|
||||||
|
"enhancement": {
|
||||||
|
"enabled": False,
|
||||||
|
"backend": "none",
|
||||||
|
"preset": "balanced", # "soft", "balanced", "strong"
|
||||||
|
"apply_to_preview_input": False,
|
||||||
|
|
||||||
|
# Mantém o pipeline parecido com o script de preview/regens:
|
||||||
|
# float01 -> uint8 -> OpenCV -> float01.
|
||||||
|
"use_u8_pipeline": True,
|
||||||
|
|
||||||
|
# Desligado por padrão para preservar escala radiométrica.
|
||||||
|
# Se quiser reproduzir exatamente o visual do script de previews,
|
||||||
|
# pode ligar este auto_stretch.
|
||||||
|
"auto_stretch": {
|
||||||
|
"enabled": False,
|
||||||
|
"low_pct": 0.2,
|
||||||
|
"high_pct": 99.8
|
||||||
|
},
|
||||||
|
|
||||||
|
"clip_output": True,
|
||||||
|
"save_debug": True
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.fusion_config = {
|
self.fusion_config = {
|
||||||
|
|
@ -389,6 +423,7 @@ class RawProcessorCore:
|
||||||
|
|
||||||
self.last_decode_perf = {}
|
self.last_decode_perf = {}
|
||||||
self._last_decode_perf_log_ts = 0.0
|
self._last_decode_perf_log_ts = 0.0
|
||||||
|
self.last_rgb_enhancement_result = None
|
||||||
|
|
||||||
if calibration_json_path:
|
if calibration_json_path:
|
||||||
self.load_config_json(calibration_json_path)
|
self.load_config_json(calibration_json_path)
|
||||||
|
|
@ -553,6 +588,301 @@ class RawProcessorCore:
|
||||||
|
|
||||||
return rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
|
return rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# RGB ENHANCEMENT / REGEN BACKENDS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _get_rgb_enhancement_config(self) -> dict:
|
||||||
|
"""
|
||||||
|
Resolve a configuração de pós-processamento RGB dentro de rgb_processing.
|
||||||
|
|
||||||
|
Contrato recomendado no module_params.json:
|
||||||
|
|
||||||
|
"rgb_processing": {
|
||||||
|
"mode": "bayer_planes",
|
||||||
|
"demosaic_algorithm": "ea",
|
||||||
|
"enhancement": {
|
||||||
|
"enabled": true,
|
||||||
|
"backend": "hybrid",
|
||||||
|
"preset": "balanced",
|
||||||
|
"use_u8_pipeline": true,
|
||||||
|
"auto_stretch": {
|
||||||
|
"enabled": false,
|
||||||
|
"low_pct": 0.2,
|
||||||
|
"high_pct": 99.8
|
||||||
|
},
|
||||||
|
"clip_output": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Compatibilidade:
|
||||||
|
- também aceita rgb_processing.enhancement_backend = "hybrid:balanced"
|
||||||
|
- também aceita rgb_processing.enhancement_preset = "balanced"
|
||||||
|
"""
|
||||||
|
rgb_cfg = getattr(self, "rgb_processing_config", {}) or {}
|
||||||
|
enh = rgb_cfg.get("enhancement", {}) or {}
|
||||||
|
|
||||||
|
if not isinstance(enh, dict):
|
||||||
|
enh = {}
|
||||||
|
|
||||||
|
# Atalhos opcionais no nível de rgb_processing.
|
||||||
|
if "enhancement_backend" in rgb_cfg and "backend" not in enh:
|
||||||
|
enh["backend"] = rgb_cfg.get("enhancement_backend")
|
||||||
|
if "enhancement_preset" in rgb_cfg and "preset" not in enh:
|
||||||
|
enh["preset"] = rgb_cfg.get("enhancement_preset")
|
||||||
|
if "enhancement_enabled" in rgb_cfg and "enabled" not in enh:
|
||||||
|
enh["enabled"] = bool(rgb_cfg.get("enhancement_enabled"))
|
||||||
|
|
||||||
|
backend = enh.get("backend", "none")
|
||||||
|
backend_norm, preset_norm = self._resolve_rgb_enhancement_backend_and_preset(
|
||||||
|
backend=backend,
|
||||||
|
preset=enh.get("preset", "balanced"),
|
||||||
|
)
|
||||||
|
|
||||||
|
enabled = bool(enh.get("enabled", False))
|
||||||
|
if backend_norm in ("none", "", "off", "disabled"):
|
||||||
|
enabled = False
|
||||||
|
|
||||||
|
auto_stretch = enh.get("auto_stretch", {}) or {}
|
||||||
|
if not isinstance(auto_stretch, dict):
|
||||||
|
auto_stretch = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"backend": backend_norm,
|
||||||
|
"preset": preset_norm,
|
||||||
|
"apply_to_preview_input": bool(enh.get("apply_to_preview_input", False)),
|
||||||
|
"use_u8_pipeline": bool(enh.get("use_u8_pipeline", True)),
|
||||||
|
"auto_stretch": {
|
||||||
|
"enabled": bool(auto_stretch.get("enabled", False)),
|
||||||
|
"low_pct": float(auto_stretch.get("low_pct", 0.2)),
|
||||||
|
"high_pct": float(auto_stretch.get("high_pct", 99.8)),
|
||||||
|
},
|
||||||
|
"clip_output": bool(enh.get("clip_output", True)),
|
||||||
|
"save_debug": bool(enh.get("save_debug", True)),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _resolve_rgb_enhancement_backend_and_preset(self, backend, preset="balanced") -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Aceita strings amigáveis:
|
||||||
|
none
|
||||||
|
hybrid
|
||||||
|
hybrid:balanced
|
||||||
|
hybrid_balanced
|
||||||
|
hybrid-soft
|
||||||
|
"""
|
||||||
|
if backend is None or backend is False:
|
||||||
|
return "none", "balanced"
|
||||||
|
|
||||||
|
b = str(backend).strip().lower()
|
||||||
|
p = str(preset or "balanced").strip().lower()
|
||||||
|
|
||||||
|
if b in ("", "none", "off", "false", "disabled", "raw"):
|
||||||
|
return "none", "balanced"
|
||||||
|
|
||||||
|
# hybrid:balanced
|
||||||
|
if ":" in b:
|
||||||
|
parts = [x.strip() for x in b.split(":") if x.strip()]
|
||||||
|
if len(parts) >= 1:
|
||||||
|
b = parts[0]
|
||||||
|
if len(parts) >= 2:
|
||||||
|
p = parts[1]
|
||||||
|
|
||||||
|
# hybrid_balanced / hybrid-balanced
|
||||||
|
for sep in ("_", "-"):
|
||||||
|
if b.startswith(f"hybrid{sep}"):
|
||||||
|
p = b.split(sep, 1)[1]
|
||||||
|
b = "hybrid"
|
||||||
|
|
||||||
|
if p not in ("soft", "balanced", "strong"):
|
||||||
|
p = "balanced"
|
||||||
|
|
||||||
|
if b not in ("hybrid",):
|
||||||
|
raise ValueError(
|
||||||
|
f"rgb_processing.enhancement.backend inválido: {backend}. "
|
||||||
|
"Use 'none' ou 'hybrid'."
|
||||||
|
)
|
||||||
|
|
||||||
|
return b, p
|
||||||
|
|
||||||
|
def _rgb_float01_to_u8_for_enhancement(self, rgb: np.ndarray, cfg: dict) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Converte RGB float01 para uint8.
|
||||||
|
Opcionalmente aplica auto_stretch por percentil para reproduzir melhor
|
||||||
|
o visual dos scripts de preview/regens.
|
||||||
|
|
||||||
|
Por padrão auto_stretch fica desligado, porque preservar a escala do tensor
|
||||||
|
tende a ser mais seguro para treino/inferência.
|
||||||
|
"""
|
||||||
|
x = np.asarray(rgb, dtype=np.float32)
|
||||||
|
|
||||||
|
auto = (cfg or {}).get("auto_stretch", {}) or {}
|
||||||
|
if bool(auto.get("enabled", False)):
|
||||||
|
low_pct = float(auto.get("low_pct", 0.2))
|
||||||
|
high_pct = float(auto.get("high_pct", 99.8))
|
||||||
|
|
||||||
|
lo = np.percentile(x, low_pct)
|
||||||
|
hi = np.percentile(x, high_pct)
|
||||||
|
|
||||||
|
if hi <= lo + 1e-6:
|
||||||
|
lo = float(np.min(x))
|
||||||
|
hi = float(np.max(x))
|
||||||
|
|
||||||
|
x = (x - float(lo)) / max(float(hi - lo), 1e-6)
|
||||||
|
|
||||||
|
x = np.clip(x, 0.0, 1.0)
|
||||||
|
return np.clip(x * 255.0 + 0.5, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _gray_world_wb_u8(self, rgb_u8: np.ndarray, strength: float = 0.55) -> np.ndarray:
|
||||||
|
img = rgb_u8.astype(np.float32)
|
||||||
|
means = img.reshape(-1, 3).mean(axis=0)
|
||||||
|
target = float(means.mean())
|
||||||
|
gains = target / np.maximum(means, 1e-6)
|
||||||
|
gains = np.clip(gains, 0.60, 1.70)
|
||||||
|
gains = 1.0 + (gains - 1.0) * float(strength)
|
||||||
|
return np.clip(img * gains[None, None, :], 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _apply_gamma_u8(self, rgb_u8: np.ndarray, gamma: float = 0.94) -> np.ndarray:
|
||||||
|
x = rgb_u8.astype(np.float32) / 255.0
|
||||||
|
y = np.power(np.clip(x, 0.0, 1.0), float(gamma))
|
||||||
|
return np.clip(y * 255.0 + 0.5, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _clahe_luminance_u8(self, rgb_u8: np.ndarray, clip_limit: float = 1.7, tile_grid_size: int = 8) -> np.ndarray:
|
||||||
|
lab = cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2LAB)
|
||||||
|
l, a, b = cv2.split(lab)
|
||||||
|
clahe = cv2.createCLAHE(
|
||||||
|
clipLimit=float(clip_limit),
|
||||||
|
tileGridSize=(int(tile_grid_size), int(tile_grid_size)),
|
||||||
|
)
|
||||||
|
l2 = clahe.apply(l)
|
||||||
|
return cv2.cvtColor(cv2.merge([l2, a, b]), cv2.COLOR_LAB2RGB)
|
||||||
|
|
||||||
|
def _denoise_fast_u8(self, rgb_u8: np.ndarray, strength: float = 3.0) -> np.ndarray:
|
||||||
|
bgr = cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
|
||||||
|
out = cv2.bilateralFilter(
|
||||||
|
bgr,
|
||||||
|
d=5,
|
||||||
|
sigmaColor=float(strength) * 12.0,
|
||||||
|
sigmaSpace=3.0,
|
||||||
|
)
|
||||||
|
return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
|
||||||
|
|
||||||
|
def _unsharp_u8(self, rgb_u8: np.ndarray, sigma: float = 0.9, amount: float = 0.85, threshold: int = 2) -> np.ndarray:
|
||||||
|
img = rgb_u8.astype(np.float32)
|
||||||
|
blur = cv2.GaussianBlur(img, (0, 0), sigmaX=float(sigma), sigmaY=float(sigma))
|
||||||
|
sharp = img + float(amount) * (img - blur)
|
||||||
|
|
||||||
|
if int(threshold) > 0:
|
||||||
|
diff = np.max(np.abs(img - blur), axis=2)
|
||||||
|
mask = diff >= int(threshold)
|
||||||
|
out = img.copy()
|
||||||
|
out[mask] = sharp[mask]
|
||||||
|
else:
|
||||||
|
out = sharp
|
||||||
|
|
||||||
|
return np.clip(out, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _local_contrast_u8(self, rgb_u8: np.ndarray, sigma: float = 9.0, amount: float = 0.14) -> np.ndarray:
|
||||||
|
img = rgb_u8.astype(np.float32)
|
||||||
|
blur = cv2.GaussianBlur(img, (0, 0), sigmaX=float(sigma), sigmaY=float(sigma))
|
||||||
|
return np.clip(img + float(amount) * (img - blur), 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def _hybrid_enhance_u8(self, rgb_u8: np.ndarray, preset: str = "balanced") -> np.ndarray:
|
||||||
|
preset = str(preset or "balanced").lower()
|
||||||
|
|
||||||
|
if preset == "soft":
|
||||||
|
wb, gamma, clahe, den, lc, us = 0.35, 0.96, 1.35, 1.8, 0.08, 0.55
|
||||||
|
elif preset == "strong":
|
||||||
|
wb, gamma, clahe, den, lc, us = 0.65, 0.90, 2.25, 3.2, 0.22, 1.25
|
||||||
|
else:
|
||||||
|
wb, gamma, clahe, den, lc, us = 0.50, 0.94, 1.75, 2.5, 0.14, 0.85
|
||||||
|
|
||||||
|
out = self._gray_world_wb_u8(rgb_u8, strength=wb)
|
||||||
|
out = self._apply_gamma_u8(out, gamma=gamma)
|
||||||
|
out = self._clahe_luminance_u8(out, clip_limit=clahe, tile_grid_size=8)
|
||||||
|
out = self._denoise_fast_u8(out, strength=den)
|
||||||
|
out = self._local_contrast_u8(out, sigma=9.0, amount=lc)
|
||||||
|
out = self._unsharp_u8(out, sigma=0.85, amount=us, threshold=2)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def apply_rgb_enhancement_to_hwc_float01(
|
||||||
|
self,
|
||||||
|
rgb: np.ndarray,
|
||||||
|
stage: str = "after_decode",
|
||||||
|
source_kind: str = "raw",
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Aplica pós-processamento RGB opcional em imagem HWC float32 0..1.
|
||||||
|
|
||||||
|
Local correto no pipeline:
|
||||||
|
- depois do debayer/decode RGB;
|
||||||
|
- depois do rgb_calibration;
|
||||||
|
- antes de flatfield native/fusão/crop/resize final.
|
||||||
|
|
||||||
|
Isso garante que treinamento e inferência usem exatamente a mesma transformação
|
||||||
|
quando ambos usam o mesmo module_params.json.
|
||||||
|
"""
|
||||||
|
cfg = self._get_rgb_enhancement_config()
|
||||||
|
self.last_rgb_enhancement_result = {
|
||||||
|
"enabled": bool(cfg.get("enabled", False)),
|
||||||
|
"applied": False,
|
||||||
|
"stage": stage,
|
||||||
|
"source_kind": source_kind,
|
||||||
|
"config": cfg,
|
||||||
|
"warnings": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if not cfg.get("enabled", False):
|
||||||
|
return rgb
|
||||||
|
|
||||||
|
if source_kind == "preview" and not cfg.get("apply_to_preview_input", False):
|
||||||
|
self.last_rgb_enhancement_result["warnings"].append("skipped_preview_input")
|
||||||
|
return rgb
|
||||||
|
|
||||||
|
if rgb is None or not isinstance(rgb, np.ndarray) or rgb.ndim != 3 or rgb.shape[2] != 3:
|
||||||
|
self.last_rgb_enhancement_result["warnings"].append(
|
||||||
|
f"invalid_rgb_shape:{None if rgb is None else rgb.shape}"
|
||||||
|
)
|
||||||
|
return rgb
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
|
backend = cfg.get("backend", "none")
|
||||||
|
preset = cfg.get("preset", "balanced")
|
||||||
|
|
||||||
|
if backend == "hybrid":
|
||||||
|
if bool(cfg.get("use_u8_pipeline", True)):
|
||||||
|
rgb_u8 = self._rgb_float01_to_u8_for_enhancement(rgb, cfg)
|
||||||
|
out_u8 = self._hybrid_enhance_u8(rgb_u8, preset=preset)
|
||||||
|
out = out_u8.astype(np.float32) / 255.0
|
||||||
|
else:
|
||||||
|
# Caminho defensivo. Hoje mantemos o u8 como padrão porque é
|
||||||
|
# exatamente o mesmo tipo de operação usado no script visual.
|
||||||
|
rgb_u8 = self._rgb_float01_to_u8_for_enhancement(rgb, cfg)
|
||||||
|
out_u8 = self._hybrid_enhance_u8(rgb_u8, preset=preset)
|
||||||
|
out = out_u8.astype(np.float32) / 255.0
|
||||||
|
else:
|
||||||
|
raise ValueError(f"RGB enhancement backend não suportado: {backend}")
|
||||||
|
|
||||||
|
if bool(cfg.get("clip_output", True)):
|
||||||
|
np.clip(out, 0.0, 1.0, out=out)
|
||||||
|
|
||||||
|
dt_ms = (time.perf_counter() - t0) * 1000.0
|
||||||
|
|
||||||
|
self.last_rgb_enhancement_result.update({
|
||||||
|
"applied": True,
|
||||||
|
"backend": backend,
|
||||||
|
"preset": preset,
|
||||||
|
"time_ms": float(dt_ms),
|
||||||
|
"input_shape": list(rgb.shape),
|
||||||
|
"output_shape": list(out.shape),
|
||||||
|
"output_dtype": str(out.dtype),
|
||||||
|
})
|
||||||
|
|
||||||
|
return out.astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
|
||||||
def build_training_rgb(
|
def build_training_rgb(
|
||||||
self,
|
self,
|
||||||
raw16: np.ndarray,
|
raw16: np.ndarray,
|
||||||
|
|
@ -585,8 +915,19 @@ class RawProcessorCore:
|
||||||
g = g * float(gains.get("G", 1.0))
|
g = g * float(gains.get("G", 1.0))
|
||||||
b = b * float(gains.get("B", 1.0))
|
b = b * float(gains.get("B", 1.0))
|
||||||
|
|
||||||
chw = np.stack([r, g, b], axis=0).astype(np.float32)
|
rgb_hwc = np.stack([r, g, b], axis=2).astype(np.float32)
|
||||||
chw = np.clip(chw, 0.0, 1.0)
|
np.clip(rgb_hwc, 0.0, 1.0, out=rgb_hwc)
|
||||||
|
|
||||||
|
# Pós-processamento RGB opcional.
|
||||||
|
# Aplicado aqui para o caminho de treino/offline que chama build_training_rgb().
|
||||||
|
rgb_hwc = self.apply_rgb_enhancement_to_hwc_float01(
|
||||||
|
rgb_hwc,
|
||||||
|
stage="build_training_rgb.after_decode",
|
||||||
|
source_kind="raw",
|
||||||
|
)
|
||||||
|
|
||||||
|
chw = np.transpose(rgb_hwc, (2, 0, 1)).astype(np.float32, copy=False)
|
||||||
|
np.clip(chw, 0.0, 1.0, out=chw)
|
||||||
|
|
||||||
if output_dtype == "float32":
|
if output_dtype == "float32":
|
||||||
return chw
|
return chw
|
||||||
|
|
@ -639,9 +980,34 @@ class RawProcessorCore:
|
||||||
cam_id = meta.get("cam_id") or meta.get("camera_id") or meta.get("id") or role
|
cam_id = meta.get("cam_id") or meta.get("camera_id") or meta.get("id") or role
|
||||||
|
|
||||||
if role == "rgb":
|
if role == "rgb":
|
||||||
|
# Caminho offline: se o RGB veio como RAW16 Bayer 2D,
|
||||||
|
# monta RGB de treino usando a mesma configuração de rgb_processing.
|
||||||
|
if isinstance(data, np.ndarray) and data.ndim == 2:
|
||||||
|
rgb_chw = self.build_training_rgb(
|
||||||
|
data,
|
||||||
|
output_dtype="float32",
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
)
|
||||||
|
rgb_img = np.transpose(rgb_chw, (1, 2, 0)).astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
# Compatibilidade: se já veio HWC RGB processado.
|
||||||
|
elif isinstance(data, np.ndarray) and data.ndim == 3 and data.shape[2] == 3:
|
||||||
|
rgb_img = data.astype(np.float32)
|
||||||
|
if rgb_img.max() > 1.5:
|
||||||
|
rgb_img /= 255.0
|
||||||
|
rgb_img = np.clip(rgb_img, 0.0, 1.0)
|
||||||
|
|
||||||
|
rgb_img = self.apply_rgb_enhancement_to_hwc_float01(
|
||||||
|
rgb_img,
|
||||||
|
stage="decode_bins_cameras.preview_or_processed_input",
|
||||||
|
source_kind="preview",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"RGB offline inválido em {cam_id}: shape={getattr(data, 'shape', None)}")
|
||||||
|
|
||||||
decoded[cam_id] = {
|
decoded[cam_id] = {
|
||||||
"name": "RGB",
|
"name": "RGB",
|
||||||
"image": data.astype(np.float32) / max_val,
|
"image": rgb_img,
|
||||||
"meta": meta,
|
"meta": meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -798,6 +1164,14 @@ class RawProcessorCore:
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"rgb_processing.mode inválido: {rgb_mode}")
|
raise ValueError(f"rgb_processing.mode inválido: {rgb_mode}")
|
||||||
|
|
||||||
|
# Pós-processamento RGB opcional.
|
||||||
|
# Este é o caminho principal de inferência RAW_BRUTO.
|
||||||
|
rgb_hwc = self.apply_rgb_enhancement_to_hwc_float01(
|
||||||
|
rgb_hwc,
|
||||||
|
stage="decode_stream_cameras.after_raw_decode",
|
||||||
|
source_kind="raw",
|
||||||
|
)
|
||||||
|
|
||||||
decoded[cam_id] = {
|
decoded[cam_id] = {
|
||||||
"name": "RGB",
|
"name": "RGB",
|
||||||
"role": "rgb",
|
"role": "rgb",
|
||||||
|
|
@ -812,10 +1186,21 @@ class RawProcessorCore:
|
||||||
|
|
||||||
rgb = data[:, :, ::-1].astype(np.float32) / 255.0
|
rgb = data[:, :, ::-1].astype(np.float32) / 255.0
|
||||||
|
|
||||||
|
rgb = np.clip(rgb, 0.0, 1.0)
|
||||||
|
|
||||||
|
# Por padrão não mexe em preview/RGB já processado.
|
||||||
|
# Se quiser aplicar também neste caminho, use:
|
||||||
|
# rgb_processing.enhancement.apply_to_preview_input=true
|
||||||
|
rgb = self.apply_rgb_enhancement_to_hwc_float01(
|
||||||
|
rgb,
|
||||||
|
stage="decode_stream_cameras.preview_input",
|
||||||
|
source_kind="preview",
|
||||||
|
)
|
||||||
|
|
||||||
decoded[cam_id] = {
|
decoded[cam_id] = {
|
||||||
"name": "RGB",
|
"name": "RGB",
|
||||||
"role": "rgb",
|
"role": "rgb",
|
||||||
"image": np.clip(rgb, 0.0, 1.0),
|
"image": rgb,
|
||||||
"meta": cam_meta,
|
"meta": cam_meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
# camera_manager.py
|
|
||||||
|
|
||||||
import depthai as dai
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
|
|
||||||
# Parâmetros da câmera
|
|
||||||
RGB_WIDTH, RGB_HEIGHT = 640, 480
|
|
||||||
DEPTH_WIDTH, DEPTH_HEIGHT = 320, 240
|
|
||||||
FX = 440.0 # distância focal em pixels (aproximado)
|
|
||||||
BASELINE = 0.075 # distância entre câmeras estéreo (em metros)
|
|
||||||
|
|
||||||
# Variáveis globais
|
|
||||||
device = None
|
|
||||||
device_info = None
|
|
||||||
rgb_queue = None
|
|
||||||
depth_queue = None
|
|
||||||
ultimo_frame_depth = None
|
|
||||||
timestamp_ultimo_depth_frame = None
|
|
||||||
ultimo_frame_rgb = None
|
|
||||||
timestamp_ultimo_rgb_frame = None
|
|
||||||
|
|
||||||
def iniciar_camera(index=0):
|
|
||||||
global device, device_info, rgb_queue, depth_queue
|
|
||||||
|
|
||||||
print("Iniciando câmera OAK-D Lite...")
|
|
||||||
|
|
||||||
pipeline = dai.Pipeline()
|
|
||||||
|
|
||||||
# RGB
|
|
||||||
cam_rgb = pipeline.create(dai.node.ColorCamera)
|
|
||||||
cam_rgb.setPreviewSize(RGB_WIDTH, RGB_HEIGHT)
|
|
||||||
cam_rgb.setInterleaved(False)
|
|
||||||
cam_rgb.setBoardSocket(dai.CameraBoardSocket.RGB)
|
|
||||||
|
|
||||||
xout_rgb = pipeline.create(dai.node.XLinkOut)
|
|
||||||
xout_rgb.setStreamName("rgb")
|
|
||||||
cam_rgb.preview.link(xout_rgb.input)
|
|
||||||
|
|
||||||
# Profundidade
|
|
||||||
mono_left = pipeline.create(dai.node.MonoCamera)
|
|
||||||
mono_right = pipeline.create(dai.node.MonoCamera)
|
|
||||||
stereo = pipeline.create(dai.node.StereoDepth)
|
|
||||||
|
|
||||||
mono_left.setResolution(dai.MonoCameraProperties.SensorResolution.THE_480_P)
|
|
||||||
mono_right.setResolution(dai.MonoCameraProperties.SensorResolution.THE_480_P)
|
|
||||||
mono_left.setBoardSocket(dai.CameraBoardSocket.LEFT)
|
|
||||||
mono_right.setBoardSocket(dai.CameraBoardSocket.RIGHT)
|
|
||||||
|
|
||||||
stereo.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
|
|
||||||
|
|
||||||
mono_left.out.link(stereo.left)
|
|
||||||
mono_right.out.link(stereo.right)
|
|
||||||
|
|
||||||
xout_depth = pipeline.create(dai.node.XLinkOut)
|
|
||||||
xout_depth.setStreamName("depth")
|
|
||||||
stereo.depth.link(xout_depth.input)
|
|
||||||
|
|
||||||
# Dispositivo
|
|
||||||
available_devices = dai.Device.getAllAvailableDevices()
|
|
||||||
if index >= len(available_devices):
|
|
||||||
raise ValueError(f"Câmera de índice {index} não encontrada.")
|
|
||||||
|
|
||||||
device_info = available_devices[index]
|
|
||||||
device = dai.Device(pipeline, device_info)
|
|
||||||
|
|
||||||
rgb_queue = device.getOutputQueue(name="rgb", maxSize=1, blocking=False)
|
|
||||||
depth_queue = device.getOutputQueue(name="depth", maxSize=1, blocking=False)
|
|
||||||
|
|
||||||
print(f"Câmera iniciada: {device_info.name} (ID: {device_info.getMxId()})")
|
|
||||||
|
|
||||||
get_camera_calib()
|
|
||||||
|
|
||||||
def get_camera_calib():
|
|
||||||
global FX, BASELINE
|
|
||||||
calib = device.readCalibration()
|
|
||||||
|
|
||||||
# Obtem matriz intrínseca da câmera LEFT (com resolução padrão 640x400)
|
|
||||||
intrinsics = calib.getCameraIntrinsics(dai.CameraBoardSocket.LEFT, 640, 400)
|
|
||||||
FX = intrinsics[0][0] # fx
|
|
||||||
BASELINE = calib.getBaselineDistance() / 1000.0 # de mm → m
|
|
||||||
|
|
||||||
print(f"[CALIB] FX: {FX:.2f} px, BASELINE: {BASELINE:.4f} m")
|
|
||||||
|
|
||||||
def get_rgb_frame():
|
|
||||||
global ultimo_frame_rgb, timestamp_ultimo_rgb_frame
|
|
||||||
if rgb_queue is None:
|
|
||||||
return None, None
|
|
||||||
frame = rgb_queue.tryGet()
|
|
||||||
if frame is not None:
|
|
||||||
ultimo_frame_rgb = frame.getCvFrame()
|
|
||||||
timestamp_ultimo_rgb_frame = time.time()
|
|
||||||
return ultimo_frame_rgb, timestamp_ultimo_rgb_frame
|
|
||||||
|
|
||||||
def get_heatmap_frame():
|
|
||||||
frame = get_depth_frame()
|
|
||||||
if frame is not None:
|
|
||||||
return gerar_heatmap(frame), timestamp_ultimo_depth_frame
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def gerar_heatmap(depth_frame):
|
|
||||||
# Normaliza e aplica colormap
|
|
||||||
normalized = cv2.normalize(depth_frame, None, 0, 255, cv2.NORM_MINMAX)
|
|
||||||
heatmap = cv2.applyColorMap(normalized.astype(np.uint8), cv2.COLORMAP_JET)
|
|
||||||
return heatmap
|
|
||||||
|
|
||||||
def get_depth_frame():
|
|
||||||
global ultimo_frame_depth, timestamp_ultimo_depth_frame
|
|
||||||
if depth_queue is None:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
frame = depth_queue.tryGet()
|
|
||||||
if frame is not None:
|
|
||||||
ultimo_frame_depth = frame.getFrame()
|
|
||||||
timestamp_ultimo_depth_frame = time.time()
|
|
||||||
return ultimo_frame_depth, timestamp_ultimo_depth_frame
|
|
||||||
|
|
||||||
def get_status_dispositivo():
|
|
||||||
from depthai import UsbSpeed
|
|
||||||
|
|
||||||
try:
|
|
||||||
memory = device.getDdrMemoryUsage()
|
|
||||||
memory_info = {
|
|
||||||
"used": memory.used,
|
|
||||||
"remaining": memory.remaining,
|
|
||||||
"total": memory.total
|
|
||||||
}
|
|
||||||
except:
|
|
||||||
memory_info = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
temp = device.getChipTemperature()
|
|
||||||
temp_info = {
|
|
||||||
"css": temp.css,
|
|
||||||
"mss": temp.mss,
|
|
||||||
"upa": temp.upa,
|
|
||||||
"dss": temp.dss
|
|
||||||
}
|
|
||||||
except:
|
|
||||||
temp_info = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
info = device.getDeviceInfo()
|
|
||||||
protocol = str(info.protocol)
|
|
||||||
except:
|
|
||||||
protocol = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
bootloader = str(device.getBootloaderVersion())
|
|
||||||
except:
|
|
||||||
bootloader = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
usb_speed = str(device.getUsbSpeed().name)
|
|
||||||
except:
|
|
||||||
usb_speed = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
pipeline_running = device.isPipelineRunning()
|
|
||||||
except:
|
|
||||||
pipeline_running = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
cameras = [sensor.name for sensor in device.getConnectedCameras()]
|
|
||||||
except:
|
|
||||||
cameras = None
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": device_info.getMxId(),
|
|
||||||
"name": device_info.name,
|
|
||||||
"state": device_info.state.name,
|
|
||||||
"usb_speed": usb_speed,
|
|
||||||
"available_camera_sensors": cameras,
|
|
||||||
"version": protocol,
|
|
||||||
"bootloader_version": bootloader,
|
|
||||||
"is_pipeline_running": pipeline_running,
|
|
||||||
"memory_usage": memory_info,
|
|
||||||
"temperature": temp_info
|
|
||||||
}
|
|
||||||
|
|
||||||
def analisar_obstaculos(velocidade=0.0, refinar=False):
|
|
||||||
from processamento.obstaculos import analisar_macro_grid, analisar_micro_grid
|
|
||||||
global timestamp_ultimo_depth_frame
|
|
||||||
|
|
||||||
frame = get_depth_frame()
|
|
||||||
|
|
||||||
if frame is None:
|
|
||||||
if ultimo_frame_depth is None:
|
|
||||||
return { "erro": "Sem frame disponível", "timestamp": None }
|
|
||||||
frame = ultimo_frame_depth
|
|
||||||
|
|
||||||
macro = analisar_macro_grid(frame, velocidade)
|
|
||||||
|
|
||||||
if macro["precisa_micro"] and refinar:
|
|
||||||
micro = analisar_micro_grid(frame)
|
|
||||||
micro["timestamp"] = timestamp_ultimo_depth_frame
|
|
||||||
return micro
|
|
||||||
|
|
||||||
macro["timestamp"] = timestamp_ultimo_depth_frame
|
|
||||||
return macro
|
|
||||||
|
|
||||||
def analisar_corredor():
|
|
||||||
from processamento.corredor import estimar_largura_corredor
|
|
||||||
global timestamp_ultimo_depth_frame # precisa garantir que fx e baseline existam
|
|
||||||
|
|
||||||
frame = get_depth_frame()
|
|
||||||
|
|
||||||
if frame is None:
|
|
||||||
if ultimo_frame_depth is None:
|
|
||||||
return { "erro": "Sem frame disponível", "timestamp": None }
|
|
||||||
frame = ultimo_frame_depth
|
|
||||||
|
|
||||||
largura_mm, pos_central, pontos_debug = estimar_largura_corredor(frame, FX, BASELINE)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"largura_corredor_mm": float(largura_mm) if largura_mm is not None else None,
|
|
||||||
"posicao_central_fracao": float(pos_central) if pos_central is not None else None,
|
|
||||||
"timestamp": timestamp_ultimo_depth_frame,
|
|
||||||
"pontos_debug": [ [int(x), float(y)] for x, y in pontos_debug ]
|
|
||||||
}
|
|
||||||
|
|
||||||
def analisar_obstaculos_3d():
|
|
||||||
from processamento.visao3d import detectar_obstaculos_em_frente
|
|
||||||
global timestamp_ultimo_depth_frame
|
|
||||||
|
|
||||||
frame = get_depth_frame()
|
|
||||||
|
|
||||||
if frame is None:
|
|
||||||
if ultimo_frame_depth is None:
|
|
||||||
return { "erro": "Sem frame disponível", "timestamp": None }
|
|
||||||
frame = ultimo_frame_depth
|
|
||||||
|
|
||||||
lista = detectar_obstaculos_em_frente(frame, FX, BASELINE)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"obstaculos_detectados": lista,
|
|
||||||
"timestamp": timestamp_ultimo_depth_frame
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
from enum import IntEnum
|
|
||||||
|
|
||||||
class Comando(IntEnum):
|
|
||||||
PING = 1
|
|
||||||
GET_RGB_FRAME = 2
|
|
||||||
GET_HEATMAP_FRAME = 3
|
|
||||||
GET_OBSTACULOS = 4
|
|
||||||
GET_STATUS_DISPOSITIVO = 5
|
|
||||||
CALIBRAR_GRADES = 6
|
|
||||||
GET_MAPA_PROFUNDIDADE = 7
|
|
||||||
GET_LARGURA_CORREDOR = 8
|
|
||||||
GET_OBSTACULOS_3D = 9
|
|
||||||
|
|
||||||
class TipoComando(IntEnum):
|
|
||||||
TX = 1
|
|
||||||
RX = 2
|
|
||||||
|
|
||||||
class TipoDeteccao(IntEnum):
|
|
||||||
SEGURO = 1
|
|
||||||
OBSTACULO = 2
|
|
||||||
DEPRESSAO = 3
|
|
||||||
|
|
||||||
class TipoRegiaoRadar(IntEnum):
|
|
||||||
SOLO = 1
|
|
||||||
AEREO = 2
|
|
||||||
|
|
||||||
class Direcao(IntEnum):
|
|
||||||
PARADO = 0
|
|
||||||
FRENTE = 1
|
|
||||||
TRAS = 2
|
|
||||||
ESQUERDA = 3
|
|
||||||
DIREITA = 4
|
|
||||||
CIMA = 5
|
|
||||||
BAIXO = 6
|
|
||||||
ESQUERDABAIXO = 7
|
|
||||||
ESQUERDACIMA = 8
|
|
||||||
DIREITABAIXO = 9
|
|
||||||
DIREITACIMA = 10
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
from queue import Queue
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from enums import Comando, TipoComando
|
|
||||||
|
|
||||||
from mqtt_handler import enviar_mensagem_mqtt
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# 🔹 Fila de Mensagens para Processamento de Comandos
|
|
||||||
# ─────────────────────────────
|
|
||||||
fila_comandos = Queue()
|
|
||||||
def worker():
|
|
||||||
while True:
|
|
||||||
topico, payload = fila_comandos.get()
|
|
||||||
try:
|
|
||||||
processar_comando(topico, payload)
|
|
||||||
except Exception as e:
|
|
||||||
print("❌ Erro:", e)
|
|
||||||
fila_comandos.task_done()
|
|
||||||
# 🔥 Cria 4 workers
|
|
||||||
for _ in range(4):
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# 🔹 Parâmetros de Execução
|
|
||||||
# ─────────────────────────────
|
|
||||||
mqtt_topic = sys.argv[1] if len(sys.argv) > 1 else "agrobot/operador/sonar"
|
|
||||||
camera_index = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# 🔹 Callback de Comando MQTT
|
|
||||||
# ─────────────────────────────
|
|
||||||
def ao_receber_comando(topico, payload):
|
|
||||||
fila_comandos.put((topico, payload))
|
|
||||||
|
|
||||||
def processar_comando(topico, payload):
|
|
||||||
try:
|
|
||||||
comando = json.loads(payload.decode())
|
|
||||||
tipo_cmd = int(comando.get("tipo_cmd"))
|
|
||||||
codigo = int(comando.get("cmd"))
|
|
||||||
|
|
||||||
# Ignora mensagens enviadas por si proprio
|
|
||||||
if (tipo_cmd == TipoComando.RX):
|
|
||||||
return
|
|
||||||
|
|
||||||
resposta = {}
|
|
||||||
|
|
||||||
if codigo == Comando.PING:
|
|
||||||
resposta = {
|
|
||||||
"ping": {
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"status": True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.GET_STATUS_DISPOSITIVO:
|
|
||||||
from camera_manager import get_status_dispositivo
|
|
||||||
resposta = {
|
|
||||||
"device_status": get_status_dispositivo()
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.GET_RGB_FRAME:
|
|
||||||
from camera_manager import get_rgb_frame
|
|
||||||
frame, timestamp = get_rgb_frame()
|
|
||||||
base64_img = None
|
|
||||||
if frame is not None:
|
|
||||||
from utils import encode_image_base64
|
|
||||||
base64_img = encode_image_base64(frame)
|
|
||||||
resposta = {
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"frame": base64_img
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.GET_HEATMAP_FRAME:
|
|
||||||
from camera_manager import get_heatmap_frame
|
|
||||||
frame, timestamp = get_heatmap_frame()
|
|
||||||
base64_img = None
|
|
||||||
if frame is not None:
|
|
||||||
from utils import encode_image_base64
|
|
||||||
base64_img = encode_image_base64(frame)
|
|
||||||
resposta = {
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"frame": base64_img
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.CALIBRAR_GRADES:
|
|
||||||
from camera_manager import get_depth_frame
|
|
||||||
from processamento.obstaculos import calibrar_grades
|
|
||||||
sucesso = calibrar_grades(get_depth_frame, n_frames=50)
|
|
||||||
resposta = {
|
|
||||||
"calibragem": {
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"status": sucesso
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.GET_OBSTACULOS:
|
|
||||||
from camera_manager import analisar_obstaculos
|
|
||||||
velocidade = float(comando.get("velocidade", 0.0)) # padrão = 0.0 m/s, parado
|
|
||||||
precisao_micro = float(comando.get("precisao_micro", False))
|
|
||||||
resposta = {
|
|
||||||
"obstaculos": analisar_obstaculos(velocidade=velocidade, refinar=precisao_micro)
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.GET_LARGURA_CORREDOR:
|
|
||||||
from camera_manager import analisar_corredor
|
|
||||||
resposta = {
|
|
||||||
"corredor": analisar_corredor()
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.GET_MAPA_PROFUNDIDADE:
|
|
||||||
from camera_manager import get_depth_frame
|
|
||||||
from utils import gerar_mapa_profundidade
|
|
||||||
|
|
||||||
depth, timestamp = get_depth_frame()
|
|
||||||
if depth is not None:
|
|
||||||
resolucao = str(comando.get("resolucao", "10x10"))
|
|
||||||
try:
|
|
||||||
linhas, colunas = map(int, resolucao.lower().split("x"))
|
|
||||||
except:
|
|
||||||
linhas, colunas = 10, 10
|
|
||||||
|
|
||||||
grid = gerar_mapa_profundidade(depth, linhas, colunas)
|
|
||||||
grid["timestamp"] = timestamp
|
|
||||||
resposta = {
|
|
||||||
"mapa_profundidade": grid
|
|
||||||
}
|
|
||||||
|
|
||||||
elif codigo == Comando.GET_OBSTACULOS_3D:
|
|
||||||
from camera_manager import analisar_obstaculos_3d
|
|
||||||
resposta = {
|
|
||||||
"obstaculos_3d": analisar_obstaculos_3d()
|
|
||||||
}
|
|
||||||
|
|
||||||
enviar_mensagem_mqtt(codigo, resposta)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print("❌ Erro ao processar comando MQTT:", e)
|
|
||||||
|
|
||||||
# ─────────────────────────────
|
|
||||||
# 🔹 Execução Principal
|
|
||||||
# ─────────────────────────────
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("🚀 Iniciando Operário Visual com OAK-D Lite...")
|
|
||||||
|
|
||||||
# 1. Inicia a câmera
|
|
||||||
from camera_manager import iniciar_camera
|
|
||||||
iniciar_camera(camera_index)
|
|
||||||
|
|
||||||
from mqtt_handler import iniciar_mqtt, enviar_mensagem_script_carregado
|
|
||||||
|
|
||||||
# 2. Inicia MQTT e registra callback
|
|
||||||
iniciar_mqtt(mqtt_topic, ao_receber_comando)
|
|
||||||
|
|
||||||
print(f"✅ Escutando comandos no tópico: {mqtt_topic}")
|
|
||||||
|
|
||||||
enviar_mensagem_script_carregado()
|
|
||||||
|
|
||||||
# 3. Loop principal
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("Encerrando...")
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import paho.mqtt.client as mqtt
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
from enums import TipoComando
|
|
||||||
|
|
||||||
mqtt_client = None
|
|
||||||
mqtt_topic = None
|
|
||||||
|
|
||||||
def iniciar_mqtt(topico, funcao_callback, broker="localhost", porta=1883):
|
|
||||||
"""
|
|
||||||
Inicia conexão MQTT e escuta o tópico.
|
|
||||||
:param tópico: string com o nome do tópico
|
|
||||||
:param funcao_callback: função que será chamada ao receber mensagem
|
|
||||||
"""
|
|
||||||
global mqtt_client, mqtt_topic
|
|
||||||
|
|
||||||
mqtt_client = mqtt.Client(f"visual_worker_{uuid.uuid4()}")
|
|
||||||
mqtt_topic = topico
|
|
||||||
|
|
||||||
def on_connect(client, userdata, flags, rc):
|
|
||||||
if rc == 0:
|
|
||||||
print(f"[MQTT] Conectado ao broker em {broker}:{porta}")
|
|
||||||
client.subscribe(mqtt_topic)
|
|
||||||
else:
|
|
||||||
print("[MQTT] Falha ao conectar, código de retorno:", rc)
|
|
||||||
|
|
||||||
def on_message(client, userdata, msg):
|
|
||||||
try:
|
|
||||||
funcao_callback(msg.topic, msg.payload)
|
|
||||||
except Exception as e:
|
|
||||||
print("[MQTT] Erro no callback:", e)
|
|
||||||
|
|
||||||
mqtt_client.on_connect = on_connect
|
|
||||||
mqtt_client.on_message = on_message
|
|
||||||
|
|
||||||
mqtt_client.connect(broker, porta)
|
|
||||||
mqtt_client.loop_start()
|
|
||||||
|
|
||||||
def enviar_mensagem_script_carregado():
|
|
||||||
mqtt_client.publish(mqtt_topic, "OK")
|
|
||||||
|
|
||||||
def enviar_mensagem_mqtt(comando, objeto):
|
|
||||||
mensagem = {
|
|
||||||
"momento": time.time(),
|
|
||||||
"tipo_cmd": TipoComando.RX,
|
|
||||||
"cmd": comando,
|
|
||||||
"obj": objeto
|
|
||||||
}
|
|
||||||
mqtt_client.publish(mqtt_topic, json.dumps(mensagem))
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import numpy as np
|
|
||||||
import cv2
|
|
||||||
|
|
||||||
def estimar_largura_corredor(depth_frame, fx, baseline, largura_mm_max=2000):
|
|
||||||
"""
|
|
||||||
Estima a largura do corredor com base no mapa de profundidade.
|
|
||||||
|
|
||||||
Parâmetros:
|
|
||||||
- depth_frame: imagem de profundidade (em disparidades)
|
|
||||||
- fx: distância focal da câmera (pixels)
|
|
||||||
- baseline: distância entre as câmeras estéreo (metros)
|
|
||||||
- largura_mm_max: largura máxima esperada do corredor (em mm)
|
|
||||||
|
|
||||||
Retorna:
|
|
||||||
- largura_estimada (em mm)
|
|
||||||
- posicao_central (0 a 1 → fração da imagem)
|
|
||||||
- lista de pontos com (x, distancia_mm)
|
|
||||||
"""
|
|
||||||
|
|
||||||
altura, largura = depth_frame.shape
|
|
||||||
faixa_inicio = int(altura * 0.45)
|
|
||||||
faixa_fim = int(altura * 0.55)
|
|
||||||
|
|
||||||
# Recorte central horizontal
|
|
||||||
faixa = depth_frame[faixa_inicio:faixa_fim, :]
|
|
||||||
|
|
||||||
# Média por coluna
|
|
||||||
profundidade_media = np.median(faixa, axis=0)
|
|
||||||
|
|
||||||
# Converte de disparidade para distância (mm)
|
|
||||||
with np.errstate(divide='ignore'): # evita divisão por zero
|
|
||||||
distancias_mm = (fx * baseline * 1000) / profundidade_media
|
|
||||||
distancias_mm = np.clip(distancias_mm, 0, largura_mm_max)
|
|
||||||
|
|
||||||
# Aplica filtro para remover ruído (média móvel simples)
|
|
||||||
distancias_mm_suave = cv2.blur(distancias_mm.reshape(1, -1).astype(np.float32), (15, 1)).flatten()
|
|
||||||
|
|
||||||
# Detecta bordas esquerda e direita: picos de distância próxima
|
|
||||||
limite = 2500 # Limite máximo para considerar obstáculo lateral (em mm)
|
|
||||||
idx_validos = np.where((distancias_mm_suave > 100) & (distancias_mm_suave < limite))[0]
|
|
||||||
|
|
||||||
if len(idx_validos) < 2:
|
|
||||||
return None, None, []
|
|
||||||
|
|
||||||
esquerda = idx_validos[0]
|
|
||||||
direita = idx_validos[-1]
|
|
||||||
|
|
||||||
largura_estimada = abs(distancias_mm_suave[direita] - distancias_mm_suave[esquerda])
|
|
||||||
posicao_central = (esquerda + direita) / 2 / largura
|
|
||||||
|
|
||||||
# Para visualização/debug
|
|
||||||
pontos_debug = [(int(x), float(distancias_mm_suave[x])) for x in range(0, largura, 10)]
|
|
||||||
|
|
||||||
return largura_estimada, posicao_central, pontos_debug
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
# obstaculos.py
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
from scipy import stats
|
|
||||||
|
|
||||||
from visual_worker.enums import Direcao, TipoDeteccao, TipoRegiaoRadar
|
|
||||||
|
|
||||||
LIMIAR_OBSTACULO = 1000 # mm
|
|
||||||
LIMIAR_DEPRESSAO = 2500 # mm
|
|
||||||
|
|
||||||
buffer_macro = None
|
|
||||||
buffer_micro = None
|
|
||||||
ref_macro = None
|
|
||||||
ref_micro = None
|
|
||||||
|
|
||||||
MACRO_ROWS = 3
|
|
||||||
MACRO_COLS = 4
|
|
||||||
MICRO_ROWS = 10
|
|
||||||
MICRO_COLS = 10
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def analisar_macro_grid(depth_frame, velocidade):
|
|
||||||
altura, largura = depth_frame.shape
|
|
||||||
row_h = altura // MACRO_ROWS
|
|
||||||
col_w = largura // MACRO_COLS
|
|
||||||
|
|
||||||
LIMIAR_OBSTACULO = calcular_limiar_dinamico(velocidade)
|
|
||||||
LIMIAR_DEPRESSAO = 2500 # Pode virar dinâmico depois
|
|
||||||
|
|
||||||
zonas_ocupadas = []
|
|
||||||
dist_min = 9999
|
|
||||||
frontal_bloqueado = False
|
|
||||||
laterais = { Direcao.ESQUERDA: False, Direcao.DIREITA: False }
|
|
||||||
zona_livre = Direcao.FRENTE
|
|
||||||
|
|
||||||
for i in range(MACRO_ROWS):
|
|
||||||
for j in range(MACRO_COLS):
|
|
||||||
y1, y2 = i * row_h, (i + 1) * row_h
|
|
||||||
x1, x2 = j * col_w, (j + 1) * col_w
|
|
||||||
|
|
||||||
area = depth_frame[y1:y2, x1:x2]
|
|
||||||
validos = area[(area > 0) & (area < 10000)]
|
|
||||||
if validos.size == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
media = np.mean(validos)
|
|
||||||
erro = media - ref_macro[i][j]
|
|
||||||
dist_min = min(dist_min, media)
|
|
||||||
|
|
||||||
tipo = TipoDeteccao.SEGURO
|
|
||||||
if erro < LIMIAR_OBSTACULO:
|
|
||||||
tipo = TipoDeteccao.OBSTACULO
|
|
||||||
elif erro > LIMIAR_DEPRESSAO:
|
|
||||||
tipo = TipoDeteccao.DEPRESSAO
|
|
||||||
|
|
||||||
if tipo != TipoDeteccao.SEGURO:
|
|
||||||
regiao = TipoRegiaoRadar.SOLO if i == MACRO_ROWS - 1 else TipoRegiaoRadar.AEREO
|
|
||||||
|
|
||||||
zonas_ocupadas.append({
|
|
||||||
"linha": i,
|
|
||||||
"coluna": j,
|
|
||||||
"distancia": round(float(media) / 1000, 2),
|
|
||||||
"tipo": tipo,
|
|
||||||
"regiao": regiao
|
|
||||||
})
|
|
||||||
|
|
||||||
if i == MACRO_ROWS - 1 and j in [1, 2]:
|
|
||||||
frontal_bloqueado = True
|
|
||||||
if j == 0:
|
|
||||||
laterais[Direcao.ESQUERDA] = True
|
|
||||||
if j == MACRO_COLS - 1:
|
|
||||||
laterais[Direcao.DIREITA] = True
|
|
||||||
|
|
||||||
if not zonas_ocupadas:
|
|
||||||
zona_livre = Direcao.FRENTE
|
|
||||||
elif all(z["coluna"] < MACRO_COLS // 2 for z in zonas_ocupadas):
|
|
||||||
zona_livre = Direcao.DIREITA
|
|
||||||
elif all(z["coluna"] >= MACRO_COLS // 2 for z in zonas_ocupadas):
|
|
||||||
zona_livre = Direcao.ESQUERDA
|
|
||||||
else:
|
|
||||||
zona_livre = Direcao.PARADO
|
|
||||||
|
|
||||||
return {
|
|
||||||
"alerta": len(zonas_ocupadas) > 0,
|
|
||||||
"frontal_bloqueado": frontal_bloqueado,
|
|
||||||
"laterais": laterais,
|
|
||||||
"zona_livre": zona_livre,
|
|
||||||
"distancia_minima": round(float(dist_min) / 1000, 2),
|
|
||||||
"detalhes": zonas_ocupadas,
|
|
||||||
"precisa_micro": frontal_bloqueado
|
|
||||||
}
|
|
||||||
|
|
||||||
def analisar_micro_grid(depth_frame):
|
|
||||||
altura, largura = depth_frame.shape
|
|
||||||
row_h = altura // MICRO_ROWS
|
|
||||||
col_w = largura // MICRO_COLS
|
|
||||||
|
|
||||||
detalhes = []
|
|
||||||
dist_min = 9999
|
|
||||||
|
|
||||||
for i in range(MICRO_ROWS):
|
|
||||||
for j in range(MICRO_COLS):
|
|
||||||
y1, y2 = i * row_h, (i + 1) * row_h
|
|
||||||
x1, x2 = j * col_w, (j + 1) * col_w
|
|
||||||
|
|
||||||
area = depth_frame[y1:y2, x1:x2]
|
|
||||||
validos = area[(area > 0) & (area < 10000)]
|
|
||||||
if validos.size == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
media = np.mean(validos)
|
|
||||||
erro = media - ref_macro[i][j]
|
|
||||||
dist_min = min(dist_min, media)
|
|
||||||
|
|
||||||
tipo = TipoDeteccao.SEGURO
|
|
||||||
if erro < LIMIAR_OBSTACULO:
|
|
||||||
tipo = TipoDeteccao.OBSTACULO
|
|
||||||
elif erro > LIMIAR_DEPRESSAO:
|
|
||||||
tipo = TipoDeteccao.DEPRESSAO
|
|
||||||
|
|
||||||
if tipo != TipoDeteccao.SEGURO:
|
|
||||||
regiao = TipoRegiaoRadar.SOLO if i >= MICRO_ROWS * 0.6 else TipoRegiaoRadar.AEREO
|
|
||||||
|
|
||||||
detalhes.append({
|
|
||||||
"linha": i,
|
|
||||||
"coluna": j,
|
|
||||||
"distancia": round(float(media) / 1000, 2),
|
|
||||||
"tipo": tipo,
|
|
||||||
"regiao": regiao
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"alerta": len(detalhes) > 0,
|
|
||||||
"quantidade": len(detalhes),
|
|
||||||
"distancia_minima": round(float(dist_min) / 1000, 2),
|
|
||||||
"detalhes": detalhes
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def calcular_limiar_dinamico(velocidade):
|
|
||||||
# Exemplo: de 0.6m a 1.5m dependendo da velocidade
|
|
||||||
return int(min(max(600 + velocidade * 800, 600), 1500))
|
|
||||||
|
|
||||||
def gerar_ref(buffer):
|
|
||||||
rows = len(buffer)
|
|
||||||
cols = len(buffer[0])
|
|
||||||
ref_out = np.zeros((rows, cols), dtype=np.float32)
|
|
||||||
|
|
||||||
for i in range(rows):
|
|
||||||
for j in range(cols):
|
|
||||||
valores = buffer[i][j]
|
|
||||||
if len(valores) == 0:
|
|
||||||
ref_out[i][j] = 0 # ou -1 para representar célula vazia
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Tenta usar moda
|
|
||||||
moda = stats.mode(valores, keepdims=True).mode
|
|
||||||
if moda.size > 0:
|
|
||||||
ref_out[i][j] = moda[0]
|
|
||||||
else:
|
|
||||||
ref_out[i][j] = np.median(valores)
|
|
||||||
|
|
||||||
return ref_out
|
|
||||||
|
|
||||||
def calibrar_macro(frame):
|
|
||||||
global buffer_macro
|
|
||||||
altura, largura = frame.shape
|
|
||||||
row_h = altura // MACRO_ROWS
|
|
||||||
col_w = largura // MACRO_COLS
|
|
||||||
|
|
||||||
for i in range(MACRO_ROWS):
|
|
||||||
for j in range(MACRO_COLS):
|
|
||||||
y1, y2 = i * row_h, (i + 1) * row_h
|
|
||||||
x1, x2 = j * col_w, (j + 1) * col_w
|
|
||||||
|
|
||||||
area = frame[y1:y2, x1:x2]
|
|
||||||
validos = area[(area > 0) & (area < 10000)]
|
|
||||||
if validos.size > 0:
|
|
||||||
buffer_macro[i][j].extend(validos.tolist())
|
|
||||||
|
|
||||||
def calibrar_micro(frame):
|
|
||||||
global buffer_micro
|
|
||||||
altura, largura = frame.shape
|
|
||||||
row_h = altura // MICRO_ROWS
|
|
||||||
col_w = largura // MICRO_COLS
|
|
||||||
|
|
||||||
for i in range(MICRO_ROWS):
|
|
||||||
for j in range(MICRO_COLS):
|
|
||||||
y1, y2 = i * row_h, (i + 1) * row_h
|
|
||||||
x1, x2 = j * col_w, (j + 1) * col_w
|
|
||||||
|
|
||||||
area = frame[y1:y2, x1:x2]
|
|
||||||
validos = area[(area > 0) & (area < 10000)]
|
|
||||||
if validos.size > 0:
|
|
||||||
buffer_micro[i][j].extend(validos.tolist())
|
|
||||||
|
|
||||||
def calibrar_grades(capturar_depth_frame, n_frames=50):
|
|
||||||
global buffer_macro, buffer_micro, ref_macro, ref_micro
|
|
||||||
|
|
||||||
buffer_macro = [[[] for _ in range(MACRO_COLS)] for _ in range(MACRO_ROWS)]
|
|
||||||
buffer_micro = [[[] for _ in range(MICRO_COLS)] for _ in range(MICRO_ROWS)]
|
|
||||||
|
|
||||||
print(f"📡 Iniciando calibração de {n_frames} frames...")
|
|
||||||
|
|
||||||
total_validos = 0
|
|
||||||
for i in range(n_frames):
|
|
||||||
frame = capturar_depth_frame()
|
|
||||||
if frame is not None:
|
|
||||||
calibrar_macro(frame)
|
|
||||||
calibrar_micro(frame)
|
|
||||||
total_validos += 1
|
|
||||||
print(f"✔️ Frame {i+1}/{n_frames} calibrado.")
|
|
||||||
else:
|
|
||||||
print(f"⚠️ Frame {i+1}/{n_frames} inválido, ignorado.")
|
|
||||||
time.sleep(0.03) # pausa leve pra estabilizar captura
|
|
||||||
|
|
||||||
if total_validos == 0:
|
|
||||||
print("❌ Nenhum frame válido recebido. Calibração cancelada.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
ref_macro = gerar_ref(buffer_macro)
|
|
||||||
ref_micro = gerar_ref(buffer_micro)
|
|
||||||
|
|
||||||
print("✅ Calibração concluída com sucesso!")
|
|
||||||
return True
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def detectar_obstaculos_em_frente(depth_frame, fx, baseline, faixa_altura=(0.45, 0.55), max_distancia_mm=2500):
|
|
||||||
"""
|
|
||||||
Detecta obstáculos com base no perfil de profundidade frontal.
|
|
||||||
|
|
||||||
Retorna uma lista de obstáculos com:
|
|
||||||
- posição_percentual (0 à 100)
|
|
||||||
- distancia (em metros)
|
|
||||||
- largura_aproximada (em px)
|
|
||||||
"""
|
|
||||||
|
|
||||||
altura, largura = depth_frame.shape
|
|
||||||
y1 = int(altura * faixa_altura[0])
|
|
||||||
y2 = int(altura * faixa_altura[1])
|
|
||||||
|
|
||||||
faixa = depth_frame[y1:y2, :]
|
|
||||||
|
|
||||||
profundidade_media = np.median(faixa, axis=0)
|
|
||||||
with np.errstate(divide='ignore'):
|
|
||||||
distancias_mm = (fx * baseline * 1000) / profundidade_media
|
|
||||||
distancias_mm = np.clip(distancias_mm, 0, max_distancia_mm)
|
|
||||||
|
|
||||||
# Simplifica usando limiar: onde há objetos "próximos"
|
|
||||||
mascara = (distancias_mm > 100) & (distancias_mm < max_distancia_mm)
|
|
||||||
|
|
||||||
obstaculos = []
|
|
||||||
inicio = None
|
|
||||||
|
|
||||||
for x in range(largura):
|
|
||||||
if mascara[x]:
|
|
||||||
if inicio is None:
|
|
||||||
inicio = x
|
|
||||||
elif inicio is not None:
|
|
||||||
fim = x
|
|
||||||
centro = (inicio + fim) // 2
|
|
||||||
largura_px = fim - inicio
|
|
||||||
distancia = np.min(distancias_mm[inicio:fim])
|
|
||||||
posicao_pct = 100 * centro / largura
|
|
||||||
|
|
||||||
obstaculos.append({
|
|
||||||
"posicao": round(float(posicao_pct), 1),
|
|
||||||
"distancia": round(float(distancia) / 1000, 2),
|
|
||||||
"largura_px": largura_px
|
|
||||||
})
|
|
||||||
inicio = None
|
|
||||||
|
|
||||||
return obstaculos
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# utils.py
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import base64
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def encode_image_base64(frame):
|
|
||||||
"""
|
|
||||||
Codifica uma imagem (np.ndarray) como string base64 JPEG.
|
|
||||||
"""
|
|
||||||
ret, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
|
|
||||||
if not ret:
|
|
||||||
return None
|
|
||||||
base64_str = base64.b64encode(buffer).decode('utf-8')
|
|
||||||
return base64_str
|
|
||||||
|
|
||||||
def gerar_mapa_profundidade(depth_frame, linhas=10, colunas=10):
|
|
||||||
altura, largura = depth_frame.shape
|
|
||||||
h = altura // linhas
|
|
||||||
w = largura // colunas
|
|
||||||
|
|
||||||
grid = []
|
|
||||||
resposta = {
|
|
||||||
"resolucao": f"{linhas}x{colunas}",
|
|
||||||
"unidade": "cm",
|
|
||||||
"grid": grid
|
|
||||||
}
|
|
||||||
|
|
||||||
if depth_frame is None:
|
|
||||||
return resposta
|
|
||||||
|
|
||||||
for i in range(linhas):
|
|
||||||
linha = []
|
|
||||||
for j in range(colunas):
|
|
||||||
y1, y2 = i * h, (i + 1) * h
|
|
||||||
x1, x2 = j * w, (j + 1) * w
|
|
||||||
celula = depth_frame[y1:y2, x1:x2]
|
|
||||||
validos = celula[(celula > 0) & (celula < 10000)]
|
|
||||||
if validos.size == 0:
|
|
||||||
linha.append(None)
|
|
||||||
else:
|
|
||||||
media_cm = np.mean(validos) / 100.0
|
|
||||||
linha.append(round(float(media_cm), 2))
|
|
||||||
grid.append(linha)
|
|
||||||
|
|
||||||
resposta["grid"] = grid
|
|
||||||
return resposta
|
|
||||||
Loading…
Reference in New Issue