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 414f1f8e2..7ebf97b97 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 @@ -105,6 +105,11 @@ class CameraManager: self.perf = VisualPerfMonitor(janela=180) self.gpu_controller = None + self._ultimo_posproc_save_ts = 0.0 + self._posproc_save_lock = threading.Lock() + self._posproc_salvando = False + self.posproc_intervalo_min_s = 5.0 + self._reset_runtime_state() # ============================================================ @@ -197,6 +202,8 @@ class CameraManager: self.seg_config = load_seg_config() self.det_config = load_det_config() + self.posproc_intervalo_min_s = float(self.seg_config.get("posproc_intervalo_min_s", 5.0)) + self.debug_visual = bool(self.seg_config.get("debug_visual", False)) self.debug_perf = bool(self.seg_config.get("debug_perf", False)) @@ -1252,6 +1259,8 @@ class CameraManager: if agora - self._ultimo_perf_publish >= 1.0: self._ultimo_perf_publish = agora + self._atualizar_config_dinamica_visual(intervalo_s=1.0) + ctrl = self.gpu_controller if ctrl is not None: ctrl.update_from_redis() @@ -1291,6 +1300,33 @@ class CameraManager: except Exception as e: self.mostrar_log(f"[saude] erro: {e}") + def _atualizar_config_dinamica_visual(self, intervalo_s=1.0): + agora = time.time() + + if not hasattr(self, "_ultimo_config_update_ts"): + self._ultimo_config_update_ts = 0.0 + + if (agora - self._ultimo_config_update_ts) < intervalo_s: + return + + try: + from visual_worker.config import load_seg_config + + cfg = load_seg_config() or {} + self.seg_config = cfg + + self.debug_visual = bool(cfg.get("debug_visual", self.debug_visual)) + self.debug_perf = bool(cfg.get("debug_perf", self.debug_perf)) + + self.posproc_intervalo_min_s = float( + cfg.get("posproc_intervalo_min_s", self.posproc_intervalo_min_s) + ) + + self._ultimo_config_update_ts = agora + + except Exception as e: + self.mostrar_log(f"[visual] erro ao atualizar config dinâmica: {e}") + def _verificar_desconexao_camera(self): if self.iniciando or self._em_warmup or time.time() < self._startup_grace_until: return @@ -1441,7 +1477,6 @@ class CameraManager: 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: try: @@ -1463,13 +1498,48 @@ class CameraManager: # 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") + pode_salvar, motivo = self._pode_salvar_posprocessamento() - ok = cv2.imwrite(caminho, frame) + if not pode_salvar: + self.mostrar_log( + f"[visual][SAVE_PP] pulado | motivo={motivo} " + f"intervalo_min={self.posproc_intervalo_min_s:.1f}s " + f"nome={nome_base}" + ) + continue - if ok: - frames_salvos.append(caminho) + frame_pp = frame.copy() + def salvar_pp_async(nome_base_async, pasta_async, frame_async): + try: + t_pp0 = time.perf_counter() + + caminho = os.path.join(pasta_async, f"{nome_base_async}.png") + + ok = cv2.imwrite(caminho, frame_async) + + t_pp1 = time.perf_counter() + + self.mostrar_log( + f"[visual][SAVE_PP] salvo async | " + f"nome={nome_base_async} " + f"ok={ok} " + f"tempo_ms={(t_pp1 - t_pp0) * 1000.0:.1f}" + ) + + except Exception as e: + self.mostrar_log(f"❌ Erro no visual SAVE_PP async: {e}") + + finally: + self._finalizar_salvamento_posprocessamento() + + threading.Thread( + target=salvar_pp_async, + args=(nome_base, pasta, frame_pp), + daemon=True + ).start() + + frames_salvos.append(f"ASYNC_STARTED:{nome_base}") continue # ==================================================== @@ -1493,6 +1563,26 @@ class CameraManager: self.mostrar_log(f"❌ Erro ao salvar frames visual: {e}") return [] + def _pode_salvar_posprocessamento(self): + agora = time.time() + + with self._posproc_save_lock: + if self._posproc_salvando: + return False, "busy" + + dt = agora - self._ultimo_posproc_save_ts + if dt < self.posproc_intervalo_min_s: + return False, f"cooldown_{dt:.2f}s" + + self._posproc_salvando = True + self._ultimo_posproc_save_ts = agora + + return True, "ok" + + def _finalizar_salvamento_posprocessamento(self): + with self._posproc_save_lock: + self._posproc_salvando = False + def _visual_disponivel_para_frame(self) -> bool: """ Indica se o Visual Worker tem o mínimo necessário para gerar/salvar frames. 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 dee483851..cacf2a798 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 @@ -115,6 +115,7 @@ VISUAL_DEFAULT_CONFIG = { # ============================================================ # Mostra janelas OpenCV/debug visual. Não usar em runtime de campo. "debug_visual": False, + "posproc_intervalo_min_s": 5.0, # Publica logs de performance no console. "debug_perf": False, 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 72c8ceaaa..87ceec0fe 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 @@ -82,6 +82,11 @@ class CameraManager: self.perf = VisualPerfMonitor(janela=180) + self._ultimo_posproc_save_ts = 0.0 + self._posproc_save_lock = threading.Lock() + self._posproc_salvando = False + self.posproc_intervalo_min_s = 5.0 + self._reset_runtime_state() # ============================================================ @@ -188,6 +193,8 @@ class CameraManager: from weed_worker.config import load_seg_config self.seg_config = load_seg_config() + self.posproc_intervalo_min_s = float(self.seg_config.get("posproc_intervalo_min_s", 5.0)) + self._inicializar_camera(mx_id, self.seg_config) if self.camera is None: @@ -526,6 +533,7 @@ class CameraManager: cfg = load_seg_config() self.seg_config = cfg self.qtd_bicos = int(cfg.get("qtd_bicos", self.qtd_bicos or 7) or 7) + self.posproc_intervalo_min_s = float(cfg.get("posproc_intervalo_min_s", self.posproc_intervalo_min_s)) if self.weed_detector is not None: self.weed_detector.atualizar_config(cfg) @@ -1533,17 +1541,49 @@ class CameraManager: nome_frame = f"{frame_name}_{tipo_nome}" if tipo_enum == TipoFrameCamera.PosProcessamento: - caminhos = self._salvar_bruto_multiespectral( - nome_base=nome_frame, - pasta=pasta - ) + pode_salvar, motivo = self._pode_salvar_posprocessamento() - if caminhos: - if isinstance(caminhos, list): - frames_salvos.extend(caminhos) - else: - frames_salvos.append(caminhos) + if not pode_salvar: + self.mostrar_log( + f"[weed][SAVE_PP] pulado | motivo={motivo} " + f"intervalo_min={self.posproc_intervalo_min_s:.1f}s " + f"nome={nome_frame}" + ) + continue + def salvar_pp_async(nome_frame_async, pasta_async): + try: + t_pp0 = time.perf_counter() + + caminhos = self._salvar_bruto_multiespectral( + nome_base=nome_frame_async, + pasta=pasta_async + ) + + t_pp1 = time.perf_counter() + + qtd = len(caminhos) if isinstance(caminhos, list) else 1 if caminhos else 0 + + self.mostrar_log( + f"[weed][SAVE_PP] salvo async | " + f"nome={nome_frame_async} " + f"arquivos={qtd} " + f"tempo_ms={(t_pp1 - t_pp0) * 1000.0:.1f}" + ) + + except Exception as e: + self.mostrar_log(f"❌ Erro no SAVE_PP async: {e}") + + finally: + self._finalizar_salvamento_posprocessamento() + + threading.Thread( + target=salvar_pp_async, + args=(nome_frame, pasta), + daemon=True + ).start() + + frames_salvos.append(f"{nome_frame}_PosProcessamento_ASYNC_STARTED") continue # ==================================================== @@ -1635,3 +1675,24 @@ class CameraManager: except Exception as e: self.mostrar_log(f"❌ Erro ao salvar bruto multiespectral: {e}") return [] + + def _pode_salvar_posprocessamento(self): + agora = time.time() + + with self._posproc_save_lock: + if self._posproc_salvando: + return False, "busy" + + dt = agora - self._ultimo_posproc_save_ts + if dt < self.posproc_intervalo_min_s: + return False, f"cooldown_{dt:.2f}s" + + self._posproc_salvando = True + self._ultimo_posproc_save_ts = agora + + return True, "ok" + + def _finalizar_salvamento_posprocessamento(self): + with self._posproc_save_lock: + self._posproc_salvando = False + \ No newline at end of file diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py index 903deb615..23e33e17f 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py @@ -41,6 +41,7 @@ WEED_DEFAULT_CONFIG = { # ============================================================ # Mostra janela OpenCV/debug visual. Não usar em runtime de campo. "debug_visual": False, + "posproc_intervalo_min_s": 5.0, # Publica logs de performance no console. "debug_perf": False,