ajustes parametros deteccao ervas

This commit is contained in:
Diego Freitas 2025-08-07 16:35:30 -03:00
parent 1c79eb32d3
commit 56b268a804
8 changed files with 301 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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

View File

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