ajustes mpc e raw_processor_core

This commit is contained in:
Diego Freitas 2026-06-08 07:36:41 -03:00
parent ca8b0123b5
commit fec398d5a4
14 changed files with 1375 additions and 1208 deletions

View File

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

View File

@ -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": {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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