diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/pid.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/pid.py new file mode 100644 index 000000000..a09edab33 --- /dev/null +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/pid.py @@ -0,0 +1,90 @@ +import time + +class PIDAdaptativo: + def __init__(self, kp_base, kp, ki_base, ki, kd_base, kd, saida_min, saida_max): + self.kp_base = kp_base + self.ki_base = ki_base + self.kd_base = kd_base + self.kp = kp + self.ki = ki + self.kd = kd + + self.saida_min = saida_min + self.saida_max = saida_max + self.saida = 0.0 + + self.setpoint = 0.0 + self.valor_atual = 0.0 + self.soma_erro = 0.0 + self.erro_anterior = 0.0 + self.derivada_suavizada = 0.0 + self.ultima_atualizacao = time.time() + + self.limite_soma_erro = 50.0 + self.filtro_derivativo = 0.2 + + def ajustar_ganhos(self, velocidade_percentual): + v = max(0.0, min(velocidade_percentual / 100.0, 1.0)) + + self.kp = self.kp_base * (1.0 - 0.4 * v ** 2) + self.ki = 0.0 if v >= 0.6 else self.ki_base * (1.0 - v ** 1.5) + self.kd = self.kd_base * (1.0 + 1.2 * v ** 1.2) + + def atualizar(self, valor_atual, setpoint=None): + agora = time.time() + dt = max(0.01, min(agora - self.ultima_atualizacao, 0.5)) + self.ultima_atualizacao = agora + + if setpoint is not None: + self.setpoint = setpoint + + self.valor_atual = valor_atual + erro = self.setpoint - self.valor_atual + + # Reset da integral se sinal do erro muda + if (erro * self.erro_anterior) < 0: + self.soma_erro = 0.0 + + # Atualiza integral com anti-windup + self.soma_erro += erro * dt + self.soma_erro = max(-self.limite_soma_erro, min(self.soma_erro, self.limite_soma_erro)) + + # Derivada com suavização + delta_erro = (erro - self.erro_anterior) / dt + if abs(erro - self.erro_anterior) > 10.0: + delta_erro *= 0.5 + + self.derivada_suavizada = ( + self.filtro_derivativo * delta_erro + + (1 - self.filtro_derivativo) * self.derivada_suavizada + ) + self.derivada_suavizada = max(-500.0, min(500.0, self.derivada_suavizada)) + + # PID + saida_temp = ( + self.kp * erro + + self.ki * self.soma_erro + + self.kd * self.derivada_suavizada + ) + + # Anti-windup na saída + if saida_temp > self.saida_max: + saida_temp = self.saida_max + self.soma_erro -= erro * dt + elif saida_temp < self.saida_min: + saida_temp = self.saida_min + self.soma_erro -= erro * dt + + self.saida = saida_temp + self.erro_anterior = erro + + return self.saida + + def resetar(self): + self.valor_atual = 0.0 + self.setpoint = 0.0 + self.saida = 0.0 + self.soma_erro = 0.0 + self.erro_anterior = 0.0 + self.derivada_suavizada = 0.0 + self.ultima_atualizacao = time.time() diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/camera_manager.cpython-311.pyc b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/camera_manager.cpython-311.pyc index d688853ce..16274820f 100644 Binary files a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/camera_manager.cpython-311.pyc and b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/camera_manager.cpython-311.pyc differ diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/config.cpython-311.pyc b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/config.cpython-311.pyc index 636ad0d30..f0ae9cb1f 100644 Binary files a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/config.cpython-311.pyc and b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/config.cpython-311.pyc differ diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/weed_detector.cpython-311.pyc b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/weed_detector.cpython-311.pyc index 9ec094fce..8e58ced09 100644 Binary files a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/weed_detector.cpython-311.pyc and b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/__pycache__/weed_detector.cpython-311.pyc differ 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 0e549feb5..422696e1b 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 @@ -156,10 +156,9 @@ class CameraManager: predictions, ts, res = self.get_segmentation_predictions() if ts == self._ts_segmentacao_anterior: return # já analisado - fps = 1.0 / (ts - self._ts_segmentacao_anterior) self._ts_segmentacao_anterior = ts if predictions is not None: - analise_completa = self.detectar_ervas(predictions, fps) + analise_completa = self.detectar_ervas(predictions) analise = analise_completa.get("dados_visuais", {}) #self.mostrar_log(f"Deteccoes no radar: {len(analise.get('deteccoes', []))}") @@ -186,12 +185,12 @@ class CameraManager: self._ultima_analise = analise_completa.copy() - def detectar_ervas(self, predictions, fps): + def detectar_ervas(self, predictions): if self.weed_detector is None: self.mostrar_log("WeedDetector não inicializado!") return [] try: - return self.weed_detector.detectar(predictions, fps) + return self.weed_detector.detectar(predictions) except Exception as e: self.mostrar_log(f"Erro na detecção de ervas: {e}") return [] diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.json b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.json index 306390f56..f4174d956 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.json +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.json @@ -5,5 +5,16 @@ "min_area_px": 400, "max_area_frac": 0.2, "ia_roi_size": 0.2, - "ia_resolution": [384,384] + "ia_resolution": [384,384], + + "erva_top_band_frac": 0.30, + "erva_frac_ema": 0.3, + "erva_thresh_vel_gain": 0.4, + "min_frac_erva_global_on": 0.0020, + "min_frac_erva_global_off": 0.0015, + "min_frac_erva_top_on": 0.0015, + "min_frac_erva_top_off": 0.0010, + "min_frac_erva_por_bico": 0.02, + "usar_morfologia": true, + "kernel_morf": 3 } \ 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 bd8b888af..4bc5fd9aa 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 @@ -59,7 +59,18 @@ def load_config(force_reload=False): "min_area_px": 400, "max_area_frac": 0.2, "ia_roi_size": 0.2, - "ia_resolution": [384,384] + "ia_resolution": [384,384], + + "erva_top_band_frac": 0.30, + "erva_frac_ema": 0.3, + "erva_thresh_vel_gain": 0.4, + "min_frac_erva_global_on": 0.0020, + "min_frac_erva_global_off": 0.0015, + "min_frac_erva_top_on": 0.0015, + "min_frac_erva_top_off": 0.0010, + "min_frac_erva_por_bico": 0.02, + "usar_morfologia": True, + "kernel_morf": 3 } dadosAtu = ContextoGlobalRedis.get_operacao().get("Atu", {}) contexto = ContextoGlobalRedis.get_contexto() diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py index 8fe29b4b7..1b8ca3d4a 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py @@ -72,7 +72,7 @@ class WeedDetector: print(f"Erro ao processar predictions: {e}") return None - def detectar(self, predictions, freq): + def detectar(self, predictions): try: # 🔸 Constrói a máscara colorida e outras saídas com base na predictions já pronta resultado = self._segmentar_predictions(predictions) @@ -86,37 +86,53 @@ class WeedDetector: return None # 🔸 Extrai blobs da classe ERVA (classe_id = 0) - mask_erva = (classes_mask == ClassesSegmentacao.ERVA.value).astype(np.uint8) - num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_erva, connectivity=8) + #mask_erva = (classes_mask == ClassesSegmentacao.ERVA.value).astype(np.uint8) + #num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_erva, connectivity=8) - deteccoes = [] - for i in range(1, num_labels): # ignora fundo - x, y, w, h, area = stats[i] - deteccoes.append({ - 'id': 0, - 'x': x, - 'y': y, - 'largura': w, - 'altura': h, - 'confianca': 1.0, - 'descricao': self.classes[ClassesSegmentacao.ERVA.value] - }) + #deteccoes = [] + #for i in range(1, num_labels): # ignora fundo + # x, y, w, h, area = stats[i] + # deteccoes.append({ + # 'id': 0, + # 'x': x, + # 'y': y, + # 'largura': w, + # 'altura': h, + # 'confianca': 1.0, + # 'descricao': self.classes[ClassesSegmentacao.ERVA.value] + # }) # 🔸 Calcula controle de bicos com base na segmentação - controle_bicos, deteccoes_filtro = self._calcular_atuacao_bicos(deteccoes, classes_mask.shape) + #controle_bicos, deteccoes_filtro = self._calcular_atuacao_bicos(deteccoes, classes_mask.shape) + + # 🔸 Decisão por máscara (sem bbox) + #controle_bicos, estat, ervas_no_radar = self._atuacao_por_mascara(classes_mask) + #self.frame_idx += 1 + + from weed_worker.config import load_config + config = load_config() + + # vel_norm pode vir do contexto (0..1 da sua Vmax). Se não tiver, manda 0.0 + vel_norm = float(config.get("velocidade_robo", 0.0)) + ervas_no_radar, estat_erva = self._decidir_ervas_no_radar(classes_mask, config, vel_norm=vel_norm) + controle_bicos, estat_bicos = self._atuacao_por_mascara(classes_mask, config) # sua lógica de CANA por setor - self.frame_idx += 1 resultado["dados_visuais"] = { "timestamp": time.time(), "height": classes_mask.shape[0], "width": classes_mask.shape[1], - "deteccoes": deteccoes_filtro, + "deteccoes": [], # deteccoes_filtro, "controle": controle_bicos, - "ervas_identificadas": self.ervas_identificadas + "ervas_identificadas": self.ervas_identificadas, + "ervas_no_radar": ervas_no_radar, # <- NOVO sinal + "estatisticas": { + "erva": estat_erva, + "bicos": estat_bicos + } } - self._mostrar_debug_bicos(resultado["mask_color"], deteccoes_filtro, controle_bicos, freq) + self._mostrar_debug_bicos(resultado["mask_color"], [], controle_bicos, ervas_no_radar) return resultado @@ -283,7 +299,7 @@ class WeedDetector: self.ervas_registradas_bico[idx_bico].add(id_erva) - def _mostrar_debug_bicos(self, frame, detections, atuacao_bicos, freq): + def _mostrar_debug_bicos(self, frame, detections, atuacao_bicos, ervas_no_radar): try: # mede FPS do "ciclo de debug" (render + imshow) dbg_fps = self._fps_update('_dbg_last_ts', '_dbg_fps_ema') @@ -294,7 +310,8 @@ class WeedDetector: # seg_color[self.predictions == class_id] = color #overlay = cv2.addWeighted(original, 0.5, seg_color, 0.5, 0) - debug_img = frame.copy() + #debug_img = frame.copy() + debug_img = cv2.resize(frame.copy(), (1920,1080), interpolation=cv2.INTER_NEAREST) H, W = debug_img.shape[:2] from weed_worker.config import load_config @@ -335,9 +352,9 @@ class WeedDetector: cv2.rectangle(overlay2, (x0, y_fim), (x1, y_inicio), cor, -1) cv2.addWeighted(overlay2, 0.15, debug_img, 0.85, 0, debug_img) cv2.rectangle(debug_img, (x0, y_fim), (x1, y_inicio), cor, 1) - status = "ON" if controle_bicos.get(i, False) else "OFF" - cv2.putText(debug_img, f"Bico {i} {status}", (x0+5, y_inicio+25), - cv2.FONT_HERSHEY_SIMPLEX, 0.6, cor, 2) + status = "ON" if atuacao_bicos.get(i, False) else "OFF" + status_controle = "ON" if controle_bicos.get(i, False) else "OFF" + cv2.putText(debug_img, f"Bico {i} {status} ({status_controle})", (x0+5, y_inicio+25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, cor, 2) escala_x = self.resolucao[0] / frame.shape[1] escala_y = self.resolucao[1] / frame.shape[0] @@ -356,8 +373,7 @@ class WeedDetector: cv2.FONT_HERSHEY_SIMPLEX, 0.5, bbox_cor, 2) # --- HUD de performance --- - seg_fps = self._seg_fps_ema if self._seg_fps_ema is not None else 0.0 - cv2.putText(debug_img, f"Seg FPS: {freq:.1f}", (10, 30), + cv2.putText(debug_img, f"Ervas no radar: {'Sim' if ervas_no_radar else 'Nao'}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,255,0), 2) cv2.putText(debug_img, f"Dbg FPS: {dbg_fps:.1f}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,255,0), 2) @@ -381,3 +397,144 @@ class WeedDetector: setattr(self, ema_attr, fps_ema) return fps_ema if fps_ema is not None else 0.0 + + def _atuacao_por_mascara(self, classes_mask, config): + """ + Decide atuação dos bicos por máscara (sem bbox): + - Para cada bico: ativa se a fração de CANA no setor >= limiar. + - Limiar aceita dois formatos: + • < 1.0 => fração (recomendado, independente da resolução) + • >= 1 => pixels absolutos (retrocompat) + Retorna: (atuacao_bicos: dict, estatisticas: dict) + """ + qtd_bicos = int(config.get("qtd_bicos")) + zona_inicio = float(config.get("ia_roi_begin")) + zona_altura = float(config.get("ia_roi_size")) + usar_morf = bool(config.get("usar_morfologia", False)) + kernel_morf = int(config.get("kernel_morf", 3)) + + # Limiar “dinâmico”: fração (<1) ou absoluto (>=1) + thr_erva_cfg = config.get("min_frac_erva_por_bico", 0.02) # 2% do setor por padrão + try: + thr_erva_cfg = float(thr_erva_cfg) + except: + thr_erva_cfg = 0.02 + + H, W = classes_mask.shape[:2] + y_inicio = int((1.0 - zona_inicio) * H) + y_fim = int((1.0 - (zona_inicio + zona_altura)) * H) + + y_top = min(y_inicio, y_fim) + y_bot = max(y_inicio, y_fim) + band = classes_mask[y_top:y_bot, :] + + # Máscara binária de CANA na banda + mask_erva = (band == ClassesSegmentacao.ERVA.value).astype(np.uint8) + + # Anti-ruído opcional + if usar_morf and kernel_morf >= 3 and kernel_morf % 2 == 1: + k = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_morf, kernel_morf)) + mask_erva = cv2.morphologyEx(mask_erva, cv2.MORPH_OPEN, k) + + largura_bico = W / float(qtd_bicos) + atuacao_bicos = {i: False for i in range(qtd_bicos)} + cont_erva_px = {} + cont_erva_frac = {} + + for i in range(qtd_bicos): + x0 = int(i * largura_bico) + x1 = int((i + 1) * largura_bico) + x0 = max(0, min(W, x0)) + x1 = max(0, min(W, x1)) + if x1 <= x0: + cont_erva_px[i] = 0 + cont_erva_frac[i] = 0.0 + continue + + region = mask_erva[:, x0:x1] + px_erva = int(region.sum()) + area_setor = float(region.size) + frac_erva = (px_erva / area_setor) if area_setor > 0 else 0.0 + + cont_erva_px[i] = px_erva + cont_erva_frac[i] = frac_erva + + # Se threshold <1: comparar por fração; se >=1: comparar por pixels + if thr_erva_cfg < 1.0: + ativa = (frac_erva >= thr_erva_cfg) + else: + ativa = (px_erva >= int(thr_erva_cfg)) + + atuacao_bicos[i] = bool(ativa) + + estatisticas = { + "contagem_cana_px_por_bico": cont_erva_px, + "contagem_cana_frac_por_bico": cont_erva_frac, + "faixa": {"y_top": y_top, "y_bot": y_bot} + } + self.ultimo_status_bicos = atuacao_bicos.copy() + return atuacao_bicos, estatisticas + + def _fractions_erva(self, classes_mask): + H, W = classes_mask.shape[:2] + total_px = H * W + + # global + frac_global = np.count_nonzero(classes_mask == ClassesSegmentacao.ERVA.value) / float(total_px) + + # lookahead (top band p/ antecipar redução) + from weed_worker.config import load_config + cfg = load_config() + top_frac = float(cfg.get("erva_top_band_frac", 0.30)) # 30% do topo + top_h = max(1, int(H * top_frac)) + band_top = classes_mask[0:top_h, :] + frac_top = np.count_nonzero(band_top == ClassesSegmentacao.ERVA.value) / float(band_top.size) + + return frac_global, frac_top + + def _decidir_ervas_no_radar(self, classes_mask, cfg, vel_norm=0.0): + """ + Decide ErvasNoRadar por fração (global e lookahead). + Aplica histerese e opcionalmente ajusta threshold pela velocidade. + """ + + # thresholds base (frações) + on_global = float(cfg.get("min_frac_erva_global_on", 0.0020)) # 0,20% + off_global = float(cfg.get("min_frac_erva_global_off", 0.0015)) # 0,15% + on_top = float(cfg.get("min_frac_erva_top_on", 0.0015)) + off_top = float(cfg.get("min_frac_erva_top_off", 0.0010)) + + # ajuste por velocidade (opcional) + k = float(cfg.get("erva_thresh_vel_gain", 0.0)) # 0.0 desliga + adj = max(0.5, min(1.0, 1.0 - k * float(vel_norm))) # clamp [0.5, 1.0] + on_global *= adj; on_top *= adj + # (tipicamente só mexe no ON; OFF pode ficar fixo) + + frac_global, frac_top = self._fractions_erva(classes_mask) + + # EMA (suavização) opcional + alpha = float(cfg.get("erva_frac_ema", 0.3)) + self._erva_frac_global_ema = (1-alpha)*getattr(self, "_erva_frac_global_ema", frac_global) + alpha*frac_global + self._erva_frac_top_ema = (1-alpha)*getattr(self, "_erva_frac_top_ema", frac_top) + alpha*frac_top + + # Histerese global + prev = getattr(self, "_ervas_no_radar", False) + hit_global = self._erva_frac_global_ema >= (on_global if not prev else off_global) + hit_top = self._erva_frac_top_ema >= (on_top if not prev else off_top) + + ervas_no_radar = bool(hit_global or hit_top) + self._ervas_no_radar = ervas_no_radar + + estat = { + "frac_global": frac_global, + "frac_top": frac_top, + "ema_global": self._erva_frac_global_ema, + "ema_top": self._erva_frac_top_ema, + "thr_on_global": on_global, + "thr_off_global": off_global, + "thr_on_top": on_top, + "thr_off_top": off_top, + "vel_adj": adj + } + return ervas_no_radar, estat +