diff --git a/AgroBase/AgroBase/Models/Enums.cs b/AgroBase/AgroBase/Models/Enums.cs index fe609bf24..b545a1743 100644 --- a/AgroBase/AgroBase/Models/Enums.cs +++ b/AgroBase/AgroBase/Models/Enums.cs @@ -372,7 +372,7 @@ MatrizCusto = 5, Deteccoes = 6, Corredor = 7, - Raw4 = 8, + PosProcessamento = 8, } public enum ModoAgitadorCalda diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs index e6cfe1575..dee49133c 100644 --- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs +++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs @@ -2139,7 +2139,7 @@ namespace AgroBase.Models string nome = Variaveis.OperacaoEmAndamento.idxLog.ToString(); var frames_salvar = new List() { TipoFrameCamera.Rgb, TipoFrameCamera.Segmentacao }; - if (Variaveis.OperacaoEmAndamento.Parametros.Controle.RegistrarDadosPosProcessamento) frames_salvar.Add(TipoFrameCamera.Raw4); + if (Variaveis.OperacaoEmAndamento.Parametros.Controle.RegistrarDadosPosProcessamento) frames_salvar.Add(TipoFrameCamera.PosProcessamento); Camera.SaveFrames(frames_salvar, nome, Caminho); var cameras_conectadas = JsonConvert.DeserializeObject>(RedisService.Get(CtxKey.DadosCameras)); @@ -2164,7 +2164,7 @@ namespace AgroBase.Models string nome = Variaveis.OperacaoEmAndamento.idxLog.ToString(); var frames_salvar = new List() { TipoFrameCamera.Rgb, TipoFrameCamera.Segmentacao }; - if (Variaveis.OperacaoEmAndamento.Parametros.Controle.RegistrarDadosPosProcessamento) frames_salvar.Add(TipoFrameCamera.Heatmap); + if (Variaveis.OperacaoEmAndamento.Parametros.Controle.RegistrarDadosPosProcessamento) frames_salvar.AddRange(new List() { TipoFrameCamera.PosProcessamento, TipoFrameCamera.Heatmap }); Camera.SaveFrames(frames_salvar, nome, Caminho); var cameras_conectadas = JsonConvert.DeserializeObject>(RedisService.Get(CtxKey.DadosCameras)); diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py index 47f0340d1..b543342d4 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py @@ -576,7 +576,6 @@ class ControladorMPC: # Convenção: e_lat > 0 quando você está mais perto da ESQUERDA (dir > esq) # (Se quiser o oposto, troque o sinal.) e_lat = float((dist_dir - dist_esq) / 2.0) - print(e_lat) # 3) Largura efetiva e peso para o ângulo do corredor half = max((dist_esq + dist_dir) * 0.5, 1e-6) @@ -1024,7 +1023,7 @@ class ControladorMPC: "angulo": comando_anterior.get("angulo", 0), "tipo": comando_anterior.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value), "simulacao": [], - "erro_latreral": 0, + "erro_lateral": 0, "erro_orientacao": 0, "debug_custo": {}, "candidatos_testados": 0, diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/enums.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/enums.py index 2c2ed34e1..3485efc9a 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/enums.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/enums.py @@ -183,4 +183,4 @@ class TipoFrameCamera(IntEnum): MatrizCusto = 5 Deteccoes = 6 Corredor = 7 - Raw4 = 8 + PosProcessamento = 8 diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py index 483efdbfb..414f1f8e2 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py @@ -819,25 +819,41 @@ class CameraManager: # ============================================================ def get_selected_frame(self, frame_type: TipoFrameCamera): - with self._cache_lock: - rgb = self._ultimo_rgb_frame - depth = self._ultimo_depth_frame - pred = self._ultimo_predictions - dets = list(self._ultimo_detections or []) - snapshot = self._ultimo_snapshot + if not self._visual_disponivel_para_frame(): + return None - params = getattr(self.camera, "parametros", {}) if self.camera is not None else {} + try: + with self._cache_lock: + rgb = self._ultimo_rgb_frame + depth = self._ultimo_depth_frame + pred = self._ultimo_predictions + dets = list(self._ultimo_detections or []) + snapshot = self._ultimo_snapshot - return self.renderer.get_selected_frame( - frame_type=frame_type, - rgb_frame=rgb, - pred_ids=pred, - depth_frame=depth, - detections=dets, - snapshot=snapshot, - camera_params=params, - alpha=0.50, - ) + params = getattr(self.camera, "parametros", {}) if self.camera is not None else {} + + # PosProcessamento no Visual Worker = imagem RGB original em melhor qualidade. + # Aqui não renderiza overlay, segmentação ou debug. + if frame_type == TipoFrameCamera.PosProcessamento: + if rgb is None or not hasattr(rgb, "size") or rgb.size <= 0: + return None + + return rgb.copy() + + return self.renderer.get_selected_frame( + frame_type=frame_type, + rgb_frame=rgb, + pred_ids=pred, + depth_frame=depth, + detections=dets, + snapshot=snapshot, + camera_params=params, + alpha=0.50, + ) + + except Exception as e: + self.mostrar_log(f"[visual] erro em get_selected_frame({frame_type}): {e}") + return None # ============================================================ # Loops @@ -1177,15 +1193,17 @@ class CameraManager: camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {} stream_on = bool(camera_ctx.get("streaming", False)) - if stream_on: + if stream_on and self._visual_disponivel_para_frame(): frame_type = TipoFrameCamera( camera_ctx.get("frame_type", TipoFrameCamera.Rgb.value) ) frame = self.get_selected_frame(frame_type) - self.camera.enviar_frame_tcp(frame) - if self.debug_visual: + if frame is not None: + self.camera.enviar_frame_tcp(frame) + + if self.debug_visual and self._visual_disponivel_para_frame(): dbg = self.get_selected_frame(TipoFrameCamera.Debug) if dbg is not None: cv2.imshow("Visual Worker Debug", dbg) @@ -1401,30 +1419,97 @@ class CameraManager: # ============================================================ def salvar_frames(self, tipos: list, nome: str, pasta="frames_salvos"): - if not self.operante: + """ + Salva frames sob demanda. + + Padrão: + - Frames de replay/debug: + JPEG leve, qualidade configurada em replay_jpeg_quality. + + - Tipo PosProcessamento: + imagem original em qualidade máxima, sem compressão destrutiva forte. + No Visual Worker, isso é apenas a imagem RGB limpa. + """ + + if not self._visual_disponivel_para_frame(): return [] try: os.makedirs(pasta, exist_ok=True) frames_salvos = [] - frame_name = nome or datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + frame_name = nome.strip() if nome else datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] + + replay_jpeg_quality = 70 + pos_jpeg_quality = 100 for frame_type in tipos: - frame = self.get_selected_frame(TipoFrameCamera(frame_type)) + try: + tipo_enum = TipoFrameCamera(frame_type) + except Exception: + self.mostrar_log(f"[visual] TipoFrameCamera inválido para salvar: {frame_type}") + continue - if frame is not None and hasattr(frame, "size") and frame.size > 0: - nome_frame = f"{frame_name}_{TipoFrameCamera(frame_type).name}.jpeg" - caminho = os.path.join(pasta, nome_frame) - cv2.imwrite(caminho, frame) + frame = self.get_selected_frame(tipo_enum) + + if frame is None or not hasattr(frame, "size") or frame.size <= 0: + continue + + nome_base = f"{frame_name}_{tipo_enum.name}" + + # ==================================================== + # Pós-processamento + # ==================================================== + # No Visual Worker, o dado científico é só a imagem limpa. + # Salva com qualidade máxima. + if tipo_enum == TipoFrameCamera.PosProcessamento: + caminho = os.path.join(pasta, f"{nome_base}.png") + + ok = cv2.imwrite(caminho, frame) + + if ok: + frames_salvos.append(caminho) + + continue + + # ==================================================== + # Replay/debug + # ==================================================== + # Salva JPEG leve para não entupir disco durante operação. + caminho = os.path.join(pasta, f"{nome_base}.jpg") + + ok = cv2.imwrite( + caminho, + frame, + [int(cv2.IMWRITE_JPEG_QUALITY), int(replay_jpeg_quality)] + ) + + if ok: frames_salvos.append(caminho) return frames_salvos except Exception as e: - self.mostrar_log(f"❌ Erro ao salvar frames: {e}") + self.mostrar_log(f"❌ Erro ao salvar frames visual: {e}") return [] + def _visual_disponivel_para_frame(self) -> bool: + """ + Indica se o Visual Worker tem o mínimo necessário para gerar/salvar frames. + Evita erro quando a OAK-D Lite não está conectada ou o manager não iniciou. + """ + + if self.camera is None: + return False + + if not self.operante and not self._em_warmup: + return False + + if self.renderer is None: + return False + + return True + @staticmethod def _fmt(v, casas=1, default=0.0): try: diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py index 8984b1ca8..bc722588a 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py @@ -127,16 +127,16 @@ VISUAL_DEFAULT_CONFIG = { # 2) Frequências dos loops # ============================================================ # Loop supervisor: saúde, performance e GPU controller. - "analise_fps": 15.0, + "analise_fps": 10.0, # Loop de segmentação ONNX/TensorRT. - "inferencia_fps": 15.0, + "inferencia_fps": 8.0, # Loop de detecção onboard MobileNet-SSD na OAK. "deteccao_fps": 5.0, # Loop de construção da matriz de confiança/custo. - "grid_fps": 15.0, + "grid_fps": 5.0, # Loop de publicação Redis. "publicacao_fps": 5.0, diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py index d7dae6731..72c8ceaaa 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py @@ -4,6 +4,7 @@ import threading import time import cv2 +import numpy as np from camera_worker.camera_multispectral import CameraMultispectral from camera_worker.oak_fcc3_core.segformer_service import MultiSpecSegformerService @@ -1498,35 +1499,139 @@ class CameraManager: # Utilidades externas # ============================================================ - def salvar_frames(self, tipos: list, nome: str, pasta="frames_salvos"): - if not self.operante: + def salvar_frames(self, tipos: list, nome: str = "", pasta: str = "frames_salvos"): + """ + Salva frames sob demanda para replay e/ou pós-processamento. + + Fluxos esperados: + - RGB / Segmentação / Prediction / Overlay: + salva imagem leve (.jpg), ideal para replay. + + - MultispecRaw / RawMultispecBundle: + chama rotina dedicada para salvar pacote bruto científico, + com arquivos binários + metadados, semelhante ao capture. + """ + + if not self._weed_disponivel_para_salvar(): return [] try: os.makedirs(pasta, exist_ok=True) + frames_salvos = [] - frame_name = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - if nome: - frame_name = nome + frame_name = nome.strip() if nome else datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] for frame_type in tipos: + try: + tipo_enum = TipoFrameCamera(frame_type) + tipo_nome = tipo_enum.name + except Exception: + tipo_enum = frame_type + tipo_nome = str(frame_type) + + nome_frame = f"{frame_name}_{tipo_nome}" + + if tipo_enum == TipoFrameCamera.PosProcessamento: + caminhos = self._salvar_bruto_multiespectral( + nome_base=nome_frame, + pasta=pasta + ) + + if caminhos: + if isinstance(caminhos, list): + frames_salvos.extend(caminhos) + else: + frames_salvos.append(caminhos) + + continue + + # ==================================================== + # 2) FLUXO ATUAL: imagens para replay + # ==================================================== frame = self.get_selected_frame(frame_type) - if frame is not None and frame.size > 0: - nome_frame = f"{frame_name}_{TipoFrameCamera(frame_type).name}" + if frame is None: + continue - if frame_type == TipoFrameCamera.Raw4: - caminho = os.path.join(pasta, f"{nome_frame}.raw") - frame.astype("float32").tofile(caminho) - else: - caminho = os.path.join(pasta, f"{nome_frame}.jpeg") - cv2.imwrite(caminho, frame) + if not hasattr(frame, "size") or frame.size <= 0: + continue - frames_salvos.append(caminho) + caminho = os.path.join(pasta, f"{nome_frame}.jpg") + + # Replay leve: JPEG comprimido. + # Mantém baixo uso de disco durante operação longa. + if frame.dtype != "uint8": + frame_salvar = self._normalizar_frame_para_uint8(frame) + else: + frame_salvar = frame + + cv2.imwrite( + caminho, + frame_salvar, + [int(cv2.IMWRITE_JPEG_QUALITY), 70] + ) + + frames_salvos.append(caminho) return frames_salvos except Exception as e: self.mostrar_log(f"❌ Erro ao salvar frames: {e}") return [] + + def _weed_disponivel_para_salvar(self) -> bool: + if not self.operante: + return False + + if self.camera is None: + return False + + return True + + def _normalizar_frame_para_uint8(self, frame): + """ + Converte frame float/uint16/etc para uint8 apenas para replay visual. + Não deve ser usado para dado científico bruto. + """ + + try: + arr = frame.astype("float32") + + min_v = float(arr.min()) + max_v = float(arr.max()) + + if max_v <= min_v: + return np.zeros(arr.shape, dtype=np.uint8) + + arr = (arr - min_v) / (max_v - min_v) + arr = np.clip(arr * 255.0, 0, 255).astype("uint8") + + return arr + + except Exception: + return frame + + def _salvar_bruto_multiespectral(self, nome_base: str, pasta: str): + try: + if not hasattr(self, "camera") or self.camera is None: + self.mostrar_log("⚠️ CameraMultispectral indisponível para salvar RAW multispec.") + return [] + + if not hasattr(self.camera, "salvar_bundle_raw_multispec"): + self.mostrar_log("⚠️ CameraMultispectral ainda não possui salvar_bundle_raw_multispec().") + return [] + + return self.camera.salvar_bundle_raw_multispec( + pasta=pasta, + nome=nome_base, + nota="pos_processamento_operacao", + extra_meta={ + "origem_chamada": "weed_worker.salvar_frames", + "tipo_requisitado": "PosProcessamento", + }, + ) + + except Exception as e: + self.mostrar_log(f"❌ Erro ao salvar bruto multiespectral: {e}") + return [] diff --git a/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs b/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs index a4efed753..5a6eaffb0 100644 --- a/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs +++ b/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs @@ -1190,6 +1190,7 @@ namespace OperationControl.ViewModels return; } + var tipoMapa = _viewOperacaoCenter?.Mapa?.TipoMapaSelecionado ?? TipoMapaOperacao.Indefinido; var dadosMapa = _viewOperacaoCenter?.Mapa?.CriarDadosMapa(); var ruasPercorrer = _viewOperacaoCenter?.Mapa?.RuasMapaCarregado?.Where(x => x.Selected)?.OrderBy(x => x.OrderSelection.Value)?.Select(x => x.Id)?.ToList() ?? new List(); @@ -1358,6 +1359,7 @@ namespace OperationControl.ViewModels ModulosMandatorios = parametros?.ModulosMandatorios, ParametrosMandatorios = parametros?.ParametrosMandatorios, + TipoMapa = tipoMapa, RuasPercorrer = ruasPercorrer, Mapa = dadosMapa }; @@ -1372,8 +1374,9 @@ namespace OperationControl.ViewModels rover.Controle = novosParametros.Controle; rover.ModulosMandatorios = novosParametros.ModulosMandatorios; rover.ParametrosMandatorios = novosParametros.ParametrosMandatorios; - rover.RuasPercorrer = novosParametros.RuasPercorrer; rover.PontosRetorno = novosParametros.PontosRetorno; + rover.TipoMapa = novosParametros.TipoMapa; + rover.RuasPercorrer = novosParametros.RuasPercorrer; rover.Mapa = novosParametros.Mapa; AtualizarDadosDiagnostico(rover);