import cv2 import numpy as np import argparse import os from pathlib import Path def list_images(folder: str, exts=(".png", ".jpg", ".jpeg", ".bmp")): p = Path(folder) if not p.exists() or not p.is_dir(): raise FileNotFoundError(f"Pasta inválida: {folder}") files = [x for x in p.rglob("*") if x.suffix.lower() in exts] files.sort() return files def color_mask_bgr(img_bgr: np.ndarray, target_bgr: tuple[int, int, int], tol: int = 40) -> np.ndarray: """ Retorna máscara binária (uint8 0/255) onde a cor está próxima de target_bgr. """ b, g, r = target_bgr lower = np.array([max(0, b - tol), max(0, g - tol), max(0, r - tol)], dtype=np.uint8) upper = np.array([min(255, b + tol), min(255, g + tol), min(255, r + tol)], dtype=np.uint8) mask = cv2.inRange(img_bgr, lower, upper) return mask def smooth_1d(x: np.ndarray, k: int = 31) -> np.ndarray: k = int(k) if k < 3: return x.astype(np.float32) if k % 2 == 0: k += 1 kernel = np.ones(k, dtype=np.float32) / k return np.convolve(x.astype(np.float32), kernel, mode="same") def find_best_pair(score_cana: np.ndarray, score_chao: np.ndarray, x_center: int, min_w: int, max_w: int, k_peaks: int = 6) -> tuple[int, int, float] | None: """ Escolhe (l, r) maximizando uma função objetivo: J = cana[l] + cana[r] + mean(chao[l:r]) - penalidades Retorna (l, r, J) onde l < r. """ W = score_cana.shape[0] left_half = score_cana[:x_center] right_half = score_cana[x_center:] # Picos candidatos (índices das maiores colunas) left_idx = np.argsort(left_half)[-k_peaks:] right_idx = np.argsort(right_half)[-k_peaks:] + x_center best = None for l in left_idx: for r in right_idx: if r <= l + 10: continue width = r - l if width < min_w or width > max_w: continue corridor_chao = float(np.mean(score_chao[l:r])) J = float(score_cana[l] + score_cana[r] + corridor_chao) # Penaliza se o centro do corredor fugir muito do centro esperado mid = (l + r) // 2 J -= 0.002 * abs(mid - x_center) # Penaliza largura muito nas bordas do range if width < (min_w * 1.15): J -= 0.2 if width > (max_w * 0.9): J -= 0.2 if best is None or J > best[2]: best = (int(l), int(r), float(J)) return best def refine_edges_from_peaks(score_cana: np.ndarray, l_peak: int, r_peak: int, x_center: int, drop_ratio: float = 0.55) -> tuple[int, int]: """ Ajusta as bordas do corredor a partir dos picos de cana. Para a esquerda: anda do pico em direção ao centro até cair para drop_ratio * pico. Para a direita: anda do pico em direção ao centro até cair para drop_ratio * pico. """ W = score_cana.shape[0] l_val = score_cana[l_peak] r_val = score_cana[r_peak] l_thr = l_val * drop_ratio r_thr = r_val * drop_ratio # Borda esquerda (dentro do corredor): começa no pico e vai para a direita xL = l_peak for x in range(l_peak, min(x_center, W - 1)): if score_cana[x] <= l_thr: xL = x break # Borda direita (dentro do corredor): começa no pico e vai para a esquerda xR = r_peak for x in range(r_peak, max(x_center, 0), -1): if score_cana[x] <= r_thr: xR = x break if xR <= xL + 10: # fallback simples xL = min(xL, x_center - 20) xR = max(xR, x_center + 20) return int(xL), int(xR) def detect_corridor_trapezoid(mask_bgr: np.ndarray, bands: int = 7, y0_frac: float = 0.45, y1_frac: float = 0.95, smooth_k: int = 31, min_w_frac: float = 0.25, max_w_frac: float = 0.85, tol_color: int = 40, debug: bool = False): """ Detecta trapézio do corredor por projeção em bandas horizontais. Retorna: - pts_trap (4x2 int) ou None - conf (0..1) - extras dict """ img = mask_bgr H, W = img.shape[:2] # Cores alvo em BGR (OpenCV) CHAO_BGR = (0, 0, 128) # vermelho CANA_BGR = (0, 128, 0) # verde OBST_BGR = (128, 0, 0) # azul m_chao = color_mask_bgr(img, CHAO_BGR, tol=tol_color) # 0/255 m_cana = color_mask_bgr(img, CANA_BGR, tol=tol_color) m_obst = color_mask_bgr(img, OBST_BGR, tol=tol_color) m_known = ((m_chao > 0) | (m_cana > 0) | (m_obst > 0)).astype(np.uint8) * 255 m_ignore = cv2.bitwise_not(m_known) # 255 onde é "branco/ignore" # Considera obstáculo como "não chão" na métrica de corredor (mas não impede cana) # score_chao vai considerar só pixels vermelhos mesmo. y0 = int(H * y0_frac) y1 = int(H * y1_frac) if y1 <= y0 + 20: y0 = int(H * 0.5) y1 = int(H * 0.95) x_center = W // 2 min_w = int(W * min_w_frac) max_w = int(W * max_w_frac) band_edges = [] # (y_mid, xL, xR, J) bands_dbg = [] # lista de dicts para debug, não interfere no resto band_h = max(10, (y1 - y0) // bands) for bi in range(bands): ya = y0 + bi * band_h yb = min(y1, ya + band_h) y_mid = (ya + yb) // 2 if yb <= ya + 5: continue # recorte da banda chao_band = (m_chao[ya:yb, :] > 0).astype(np.float32) cana_band = (m_cana[ya:yb, :] > 0).astype(np.float32) ignore_band = (m_ignore[ya:yb, :] > 0).astype(np.float32) # score por coluna = fração de pixels na banda score_chao = np.mean(chao_band, axis=0) score_cana = np.mean(cana_band, axis=0) score_ignore = np.mean(ignore_band, axis=0) # suaviza score_chao_s = smooth_1d(score_chao, smooth_k) score_cana_s = smooth_1d(score_cana, smooth_k) best = find_best_pair(score_cana_s, score_chao_s, x_center, min_w, max_w, k_peaks=7) if best is None: continue l_peak, r_peak, J = best xL, xR = refine_edges_from_peaks(score_cana_s, l_peak, r_peak, x_center, drop_ratio=0.55) if debug: bands_dbg.append({ "y_mid": int(y_mid), "xL": int(xL), "xR": int(xR), "l_peak": int(l_peak), "r_peak": int(r_peak), "J": float(J), "corridor_chao": float(np.mean(score_chao_s[xL:xR])) if xR > xL else 0.0, "ok": True, "ya": int(ya), "yb": int(yb), }) # validações rápidas width = xR - xL if width < min_w or width > max_w: if debug: bands_dbg.append({ "y_mid": int(y_mid), "xL": None, "xR": None, "l_peak": int(l_peak), "r_peak": int(r_peak), "J": float(J), "corridor_chao": 0.0, "ok": False, "ya": int(ya), "yb": int(yb), }) continue # miolo precisa ter chão razoável (senão pode ser engano) corridor_chao = float(np.mean(score_chao_s[xL:xR])) if xR > xL else 0.0 if corridor_chao < 0.10: if debug: bands_dbg.append({ "y_mid": int(y_mid), "xL": None, "xR": None, "l_peak": int(l_peak), "r_peak": int(r_peak), "J": float(J), "corridor_chao": 0.0, "ok": False, "ya": int(ya), "yb": int(yb), }) continue # corredor não pode ser "vazio" / ignore demais no miolo ignore_in_corridor = float(np.mean(score_ignore[xL:xR])) if xR > xL else 1.0 if ignore_in_corridor > 0.10: if debug: bands_dbg.append({ "y_mid": int(y_mid), "xL": None, "xR": None, "l_peak": int(l_peak), "r_peak": int(r_peak), "J": float(J), "corridor_chao": 0.0, "ok": False, "ya": int(ya), "yb": int(yb), }) continue band_edges.append((int(y_mid), int(xL), int(xR), float(J))) if len(band_edges) < max(2, bands // 3): return None, 0.0, {"reason": "poucas bandas válidas", "bands_ok": len(band_edges)} # Remove outliers por largura e por centro band_edges = sorted(band_edges, key=lambda t: t[0]) widths = np.array([xR - xL for _, xL, xR, _ in band_edges], dtype=np.float32) centers = np.array([(xL + xR) / 2.0 for _, xL, xR, _ in band_edges], dtype=np.float32) w_med = float(np.median(widths)) c_med = float(np.median(centers)) filtered = [] for (y, xL, xR, J) in band_edges: w = xR - xL c = (xL + xR) / 2.0 if abs(w - w_med) > 0.35 * w_med: if debug: bands_dbg.append({ "y_mid": int(y_mid), "xL": None, "xR": None, "l_peak": int(l_peak), "r_peak": int(r_peak), "J": float(J), "corridor_chao": 0.0, "ok": False, "ya": int(ya), "yb": int(yb), }) continue if abs(c - c_med) > 0.20 * W: if debug: bands_dbg.append({ "y_mid": int(y_mid), "xL": None, "xR": None, "l_peak": int(l_peak), "r_peak": int(r_peak), "J": float(J), "corridor_chao": 0.0, "ok": False, "ya": int(ya), "yb": int(yb), }) continue filtered.append((y, xL, xR, J)) if len(filtered) < 2: return None, 0.0, {"reason": "outliers demais", "bands_ok": len(band_edges), "bands_filt": len(filtered)} ys = np.array([y for y, _, _, _ in filtered], dtype=np.float32) xLs = np.array([xL for _, xL, _, _ in filtered], dtype=np.float32) xRs = np.array([xR for _, _, xR, _ in filtered], dtype=np.float32) # Fit linear x = a*y + b para esquerda e direita aL, bL = np.polyfit(ys, xLs, 1) aR, bR = np.polyfit(ys, xRs, 1) y_top = int(ys[0]) y_bot = int(ys[-1]) xL_top = int(aL * y_top + bL) xL_bot = int(aL * y_bot + bL) xR_top = int(aR * y_top + bR) xR_bot = int(aR * y_bot + bR) # clamp xL_top = int(np.clip(xL_top, 0, W - 1)) xL_bot = int(np.clip(xL_bot, 0, W - 1)) xR_top = int(np.clip(xR_top, 0, W - 1)) xR_bot = int(np.clip(xR_bot, 0, W - 1)) if xR_top <= xL_top + 10 or xR_bot <= xL_bot + 10: return None, 0.0, {"reason": "trapézio degenerado"} pts = np.array([ [xL_top, y_top], [xR_top, y_top], [xR_bot, y_bot], [xL_bot, y_bot] ], dtype=np.int32) # Confiança simples: proporção de bandas boas + estabilidade de centro/largura bands_ok = len(filtered) bands_total = bands conf = bands_ok / float(max(1, bands_total)) w_std = float(np.std([xR - xL for _, xL, xR, _ in filtered])) c_std = float(np.std([(xL + xR) / 2.0 for _, xL, xR, _ in filtered])) conf *= float(np.clip(1.0 - (w_std / (0.35 * W)), 0.0, 1.0)) conf *= float(np.clip(1.0 - (c_std / (0.25 * W)), 0.0, 1.0)) extras = { "bands_ok": bands_ok, "bands_total": bands_total, "y_top": y_top, "y_bot": y_bot, "fit_left": (float(aL), float(bL)), "fit_right": (float(aR), float(bR)), "conf": float(conf), } if debug: extras["filtered"] = filtered extras["bands_dbg"] = bands_dbg extras["dbg_params"] = {"y0_frac": y0_frac, "y1_frac": y1_frac, "bands": bands} return pts, float(conf), extras def draw_trapezoid(img_bgr: np.ndarray, pts: np.ndarray, conf: float): out = img_bgr.copy() cv2.polylines(out, [pts], isClosed=True, color=(255, 255, 0), thickness=3) # amarelo/ciano cx = int(np.mean(pts[:, 0])) cy = int(np.mean(pts[:, 1])) cv2.putText(out, f"corridor conf={conf:.2f}", (max(10, cx - 120), max(30, cy)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 0), 2, cv2.LINE_AA) return out def draw_band_debug(img_bgr: np.ndarray, extras: dict, y0_frac: float, y1_frac: float, bands: int): out = img_bgr.copy() H, W = out.shape[:2] bands_dbg = extras.get("bands_dbg", []) if not bands_dbg: return out # desenha faixas horizontais y0 = int(H * y0_frac) y1 = int(H * y1_frac) band_h = max(10, (y1 - y0) // bands) for bi in range(bands): ya = y0 + bi * band_h yb = min(y1, ya + band_h) if yb <= ya: continue cv2.line(out, (0, ya), (W-1, ya), (80, 80, 80), 1) # desenha pontos por banda for d in bands_dbg: y = int(d["y_mid"]) ok = bool(d.get("ok", True)) # cor: verde ok, vermelho rejeitada color = (0, 255, 0) if ok else (0, 0, 255) # marca picos (cristas cana) lp = d.get("l_peak", None) rp = d.get("r_peak", None) if lp is not None: cv2.circle(out, (int(lp), y), 5, (0, 255, 255), -1) # amarelo if rp is not None: cv2.circle(out, (int(rp), y), 5, (0, 255, 255), -1) xL = d.get("xL", None) xR = d.get("xR", None) if xL is not None and xR is not None: # bordas cv2.circle(out, (int(xL), y), 6, color, -1) cv2.circle(out, (int(xR), y), 6, color, -1) # centro da banda xc = int((xL + xR) / 2) cv2.circle(out, (xc, y), 5, (255, 0, 255), -1) # roxo # linha do corredor nessa banda cv2.line(out, (int(xL), y), (int(xR), y), color, 2) # label curtinho if ok and xL is not None and xR is not None: txt = f"J={d.get('J',0):.2f} ch={d.get('corridor_chao',0):.2f}" else: txt = f"rej" cv2.putText(out, txt, (10, max(20, y-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1, cv2.LINE_AA) return out def detect_corridor_regime(mask_bgr: np.ndarray, bands: int = 15, y0_frac: float = 0.1, y1_frac: float = 0.9, tol_color: int = 40, smooth_k: int = 51, cane_peak_min: float = 0.12, notfloor_thr: float = 0.25, floor_open_thr: float = 0.70, cane_present_thr: float = 0.06): """ Classifica o "regime" do frame, sem calcular trapézio. Retorna: (regime_str, metrics_dict) Regimes: - OPEN_FIELD: só chão (sem paredes) em quase todas as bandas - ENTERING: topo tem 2 paredes, base não tem - EXITING: base tem 2 paredes, topo não tem - IN_CORRIDOR: topo e base com 2 paredes - ONE_WALL: muitas bandas com 1 parede, poucas com 2 - UNKNOWN: não bateu com regras """ img = mask_bgr H, W = img.shape[:2] x_center = W // 2 # Cores alvo em BGR CHAO_BGR = (0, 0, 128) # vermelho CANA_BGR = (0, 128, 0) # verde OBST_BGR = (128, 0, 0) # azul m_chao = color_mask_bgr(img, CHAO_BGR, tol=tol_color) # 0/255 m_cana = color_mask_bgr(img, CANA_BGR, tol=tol_color) m_obst = color_mask_bgr(img, OBST_BGR, tol=tol_color) m_known = ((m_chao > 0) | (m_cana > 0) | (m_obst > 0)).astype(np.uint8) * 255 m_ignore = cv2.bitwise_not(m_known) y0 = int(H * y0_frac) y1 = int(H * y1_frac) if y1 <= y0 + 20: y0 = int(H * 0.5) y1 = int(H * 0.95) band_h = max(10, (y1 - y0) // max(1, bands)) per_band = [] n0 = n1 = n2 = 0 # define "top" e "bottom" pela metade das bandas válidas for bi in range(bands): ya = y0 + bi * band_h yb = min(y1, ya + band_h) if yb <= ya + 5: continue y_mid = (ya + yb) // 2 chao_band = (m_chao[ya:yb, :] > 0).astype(np.float32) cana_band = (m_cana[ya:yb, :] > 0).astype(np.float32) ignore_band = (m_ignore[ya:yb, :] > 0).astype(np.float32) obst_band = (m_obst[ya:yb, :] > 0).astype(np.float32) score_chao = smooth_1d(np.mean(chao_band, axis=0), smooth_k) score_cana = smooth_1d(np.mean(cana_band, axis=0), smooth_k) score_ign = np.mean(ignore_band, axis=0) score_obst = np.mean(obst_band, axis=0) # métricas simples floor_ratio = float(np.mean(score_chao)) # fração média de chão por coluna cane_ratio = float(np.mean(score_cana)) ignore_ratio = float(np.mean(score_ign)) obst_ratio = float(np.mean(score_obst)) # "parede" = pico de cana suficientemente alto em cada metade left_peak = float(np.max(score_cana[:x_center])) if x_center > 5 else 0.0 right_peak = float(np.max(score_cana[x_center:])) if (W - x_center) > 5 else 0.0 has_left = left_peak >= cane_peak_min has_right = right_peak >= cane_peak_min walls = int(has_left) + int(has_right) # sanity: se quase tudo é ignore/obst, essa banda é ruim bad_band = (ignore_ratio > 0.35) or (obst_ratio > 0.25) # contagem final só se banda não estiver "podre" if not bad_band: if walls == 0: n0 += 1 elif walls == 1: n1 += 1 else: n2 += 1 per_band.append({ "y_mid": y_mid, "walls": walls, "bad": bad_band, "floor_ratio": floor_ratio, "cane_ratio": cane_ratio, "left_peak": left_peak, "right_peak": right_peak, "ignore_ratio": ignore_ratio, "obst_ratio": obst_ratio, }) valid_bands = [b for b in per_band if not b["bad"]] if len(valid_bands) < max(3, bands // 3): return "UNKNOWN", {"reason": "poucas bandas válidas", "n_valid": len(valid_bands), "n0": n0, "n1": n1, "n2": n2} half = max(1, len(valid_bands) // 2) top_bands = valid_bands[:half] bot_bands = valid_bands[half:] def count_walls(bands_list): c0 = sum(1 for b in bands_list if b["walls"] == 0) c1 = sum(1 for b in bands_list if b["walls"] == 1) c2 = sum(1 for b in bands_list if b["walls"] == 2) return c0, c1, c2 t0, t1, t2 = count_walls(top_bands) b0, b1, b2 = count_walls(bot_bands) # médias globais (ajudam a decidir OPEN_FIELD) mean_floor = float(np.mean([b["floor_ratio"] for b in valid_bands])) if valid_bands else 0.0 mean_cane = float(np.mean([b["cane_ratio"] for b in valid_bands])) if valid_bands else 0.0 # Regras principais # 1) OPEN_FIELD: chão alto, cana baixa, quase ninguém com 2 paredes if mean_floor >= floor_open_thr and mean_cane <= cane_present_thr and n2 <= max(1, len(valid_bands) // 10): return "OPEN_FIELD", { "n_valid": len(valid_bands), "n0": n0, "n1": n1, "n2": n2, "top": (t0, t1, t2), "bot": (b0, b1, b2), "mean_floor": mean_floor, "mean_cane": mean_cane } # 2) IN_CORRIDOR: topo e base com várias bandas de 2 paredes if t2 >= max(2, len(top_bands) // 3) and b2 >= max(2, len(bot_bands) // 3): return "IN_CORRIDOR", { "n_valid": len(valid_bands), "n0": n0, "n1": n1, "n2": n2, "top": (t0, t1, t2), "bot": (b0, b1, b2), "mean_floor": mean_floor, "mean_cane": mean_cane } # 3) ENTERING: topo tem 2 paredes, base não if t2 >= max(2, len(top_bands) // 3) and b2 <= max(1, len(bot_bands) // 6) and b0 >= max(2, len(bot_bands) // 3): return "ENTERING", { "n_valid": len(valid_bands), "n0": n0, "n1": n1, "n2": n2, "top": (t0, t1, t2), "bot": (b0, b1, b2), "mean_floor": mean_floor, "mean_cane": mean_cane } # 4) EXITING: base tem 2 paredes, topo não if b2 >= max(2, len(bot_bands) // 3) and t2 <= max(1, len(top_bands) // 6) and t0 >= max(2, len(top_bands) // 3): return "EXITING", { "n_valid": len(valid_bands), "n0": n0, "n1": n1, "n2": n2, "top": (t0, t1, t2), "bot": (b0, b1, b2), "mean_floor": mean_floor, "mean_cane": mean_cane } # 5) ONE_WALL: domina 1 parede, e 2 paredes é raro if n1 >= max(3, len(valid_bands) // 3) and n2 <= max(1, len(valid_bands) // 6): return "ONE_WALL", { "n_valid": len(valid_bands), "n0": n0, "n1": n1, "n2": n2, "top": (t0, t1, t2), "bot": (b0, b1, b2), "mean_floor": mean_floor, "mean_cane": mean_cane } return "UNKNOWN", { "n_valid": len(valid_bands), "n0": n0, "n1": n1, "n2": n2, "top": (t0, t1, t2), "bot": (b0, b1, b2), "mean_floor": mean_floor, "mean_cane": mean_cane } def _safe_smooth_k(W: int, k: int) -> int: # k ímpar e <= W (e pelo menos 3) k = int(k) if W <= 3: return 3 k = min(k, W if (W % 2 == 1) else (W - 1)) if k < 3: k = 3 if k % 2 == 0: k -= 1 return k def _clip01(x: float) -> float: return 0.0 if x < 0.0 else (1.0 if x > 1.0 else x) def _run_length_from_bottom(vec: list[int], value: int) -> int: k = 0 for v in reversed(vec): if v == value: k += 1 else: break return k def _longest_run_geq(vec: np.ndarray, thr: float) -> int: """Maior run-length contínuo onde vec >= thr.""" if vec.size == 0: return 0 m = (vec >= thr).astype(np.uint8) best = cur = 0 for v in m: if v: cur += 1 best = max(best, cur) else: cur = 0 return int(best) def band_has_corridor_valley(score_cana: np.ndarray, score_chao: np.ndarray, cane_thr: float = 0.12, floor_min_ratio: float = 0.40, # teu limiar valley_min_width_frac: float = 0.18, peaks_min_sep_frac: float = 0.22, # parede real (anti-folha) wall_run_thr: float = 0.10, wall_min_run_frac: float = 0.04, wall_side_margin_frac: float = 0.10, wall_min_peak: float = 0.12, # opcional: evita caso “miolo totalmente verde” valley_cane_max_ratio: float = 0.90): """ Corredor por 'vale': - duas paredes (run-length em regiões laterais) - vale largo entre elas - no vale: chão >= floor_min_ratio (não precisa dominar) """ W = int(score_cana.shape[0]) if W < 20: return False, {"reason": "small_W"} sc = np.clip(score_cana, 0, 1) sf = np.clip(score_chao, 0, 1) # 1) dois picos separados (só pra achar L e R) i1 = int(np.argmax(sc)) p1 = float(sc[i1]) min_sep = int(W * peaks_min_sep_frac) mask_far = np.ones(W, dtype=bool) mask_far[max(0, i1 - min_sep): min(W, i1 + min_sep)] = False if not np.any(mask_far): return False, {"reason": "no_far_region"} i2 = int(np.argmax(sc * mask_far)) p2 = float(sc[i2]) if (p1 < cane_thr) or (p2 < cane_thr): return False, {"reason": "peaks_weak", "p1": p1, "p2": p2} L = min(i1, i2) R = max(i1, i2) valley_min_w = int(W * valley_min_width_frac) if (R - L) < valley_min_w: return False, {"reason": "valley_too_narrow", "valley_w": (R-L), "valley_min_w": valley_min_w} # 2) validar “paredes” com run-length (anti-folha) margin = int(W * wall_side_margin_frac) left_end = max(1, min(L+1, W - margin)) right_start = min(max(R, margin), W-1) left_region = sc[:left_end] right_region = sc[right_start:] min_run = int(W * wall_min_run_frac) left_run = _longest_run_geq(left_region, wall_run_thr) right_run = _longest_run_geq(right_region, wall_run_thr) left_peak = float(np.max(left_region)) if left_region.size else 0.0 right_peak = float(np.max(right_region)) if right_region.size else 0.0 ok_left_wall = (left_run >= min_run) and (left_peak >= wall_min_peak) ok_right_wall = (right_run >= min_run) and (right_peak >= wall_min_peak) wall_dbg = { "left_run": left_run, "right_run": right_run, "min_run": min_run, "left_peak": left_peak, "right_peak": right_peak, "ok_left_wall": bool(ok_left_wall), "ok_right_wall": bool(ok_right_wall), } if not (ok_left_wall and ok_right_wall): return False, { "reason": "thin_walls", "left_run": left_run, "right_run": right_run, "min_run": min_run, "left_peak": left_peak, "right_peak": right_peak, "iL": L, "iR": R, **wall_dbg } # 3) medir o vale por proporção de chão gap = slice(L+1, R-1) if (R - L) <= 3: return False, {"reason": "valley_too_small", **wall_dbg} valley_floor_ratio = float(np.mean(sf[gap] >= 0.50)) # chão “forte” (ajustável) valley_cane_ratio = float(np.mean(sc[gap] >= 0.50)) # cana “forte” (debug) ok_valley_floor = (valley_floor_ratio >= floor_min_ratio) ok_valley_cane = (valley_cane_ratio <= valley_cane_max_ratio) if not (ok_valley_floor and ok_valley_cane): return False, { "reason": "valley_fail", "valley_floor_ratio": valley_floor_ratio, "valley_cane_ratio": valley_cane_ratio, "iL": L, "iR": R, "pL": float(sc[L]), "pR": float(sc[R]), **wall_dbg } center = 0.5 * (L + R) / max(1, (W-1)) width = float(R - L) / max(1, W) return True, { "iL": L, "iR": R, "pL": float(sc[L]), "pR": float(sc[R]), "valley_floor_ratio": valley_floor_ratio, "valley_cane_ratio": valley_cane_ratio, "left_run": left_run, "right_run": right_run, "center_frac": center, "width_frac": width, **wall_dbg } def band_has_corridor(score_cana: np.ndarray, score_chao: np.ndarray, cane_thr: float = 0.12, # gap: chão precisa "ganhar" da cana no trecho central gap_floor_thr: float = 0.45, gap_cane_max: float = 0.18, # largura mínima do gap (fração da largura da imagem) gap_min_width_frac: float = 0.18, # separação mínima entre picos (fração da largura) peaks_min_sep_frac: float = 0.22): """ Decide se existe padrão CANA-CHAO-CANA em uma banda (invariante a deslocamento no X). Retorna (has_corridor, info_dict). Estratégia: - acha índices onde cana é forte (>= cane_thr) - separa em região esquerda e direita por um "corte" móvel: usa o pico global e procura um segundo pico distante - garante que entre picos há um trecho com chão alto e cana baixa """ W = score_cana.shape[0] if W < 20: return False, {"reason": "small_W"} # normaliza segurança (não obrigatório, mas ajuda se tiver ruído) sc = np.clip(score_cana, 0, 1) sf = np.clip(score_chao, 0, 1) # picos candidatos idx_strong = np.where(sc >= cane_thr)[0] if idx_strong.size == 0: return False, {"reason": "no_strong_cane"} # pega o pico mais alto como referência i1 = int(np.argmax(sc)) p1 = float(sc[i1]) # procura segundo pico longe o suficiente min_sep = int(W * peaks_min_sep_frac) # mascara pontos longe do i1 mask_far = np.ones(W, dtype=bool) mask_far[max(0, i1 - min_sep): min(W, i1 + min_sep)] = False if not np.any(mask_far): return False, {"reason": "no_far_region"} i2 = int(np.argmax(sc * mask_far)) p2 = float(sc[i2]) # ambos precisam ser fortes if p2 < cane_thr: return False, {"reason": "second_peak_weak", "p1": p1, "p2": p2} # ordena esquerda/direita L = min(i1, i2) R = max(i1, i2) # gap entre picos if R <= L + 3: return False, {"reason": "peaks_too_close"} gap = slice(L+1, R-1) if (R - L) <= 3: return False, {"reason": "peaks_too_close"} gap_floor = float(np.mean(sf[gap])) gap_cane = float(np.mean(sc[gap])) gap_min_w = int(W * gap_min_width_frac) if (R - L) < gap_min_w: return False, {"reason": "gap_too_narrow", "gap_w": (R-L), "gap_min_w": gap_min_w} # critério: no gap o chão domina e cana é baixa has_gap = (gap_floor >= gap_floor_thr) and (gap_cane <= gap_cane_max) return bool(has_gap), { "iL": L, "iR": R, "pL": float(sc[L]), "pR": float(sc[R]), "gap_floor": gap_floor, "gap_cane": gap_cane, "gap_w": (R - L), "has_gap": has_gap } def corridor_bridge_ok(corr_vec: list[int], max_gap_run: int = 4) -> tuple[bool, dict]: """ Retorna True se existe conectividade topo->base permitindo buracos (0s) de até max_gap_run. """ idx_ones = [i for i, v in enumerate(corr_vec) if v == 1] if len(idx_ones) < 2: return False, {"reason": "few_ones", "n_ones": len(idx_ones)} # maior gap entre 1s consecutivos gaps = [] for a, b in zip(idx_ones[:-1], idx_ones[1:]): gaps.append((b - a - 1)) # quantos zeros entre eles max_gap = max(gaps) if gaps else 0 ok = (idx_ones[0] <= 1) and (idx_ones[-1] >= len(corr_vec) - 2) and (max_gap <= max_gap_run) return ok, {"max_gap": max_gap, "idx_first": idx_ones[0], "idx_last": idx_ones[-1], "n_ones": len(idx_ones)} def corridor_center_consistent(per_band_ok: list[dict], max_center_jump: float = 0.12) -> tuple[bool, dict]: """ Exige que o centro do corredor não pule demais entre bandas OK. """ centers = [b["center_frac"] for b in per_band_ok if b.get("center_frac") is not None] if len(centers) < 2: return True, {"reason": "few_centers"} # não bloqueia jumps = [abs(b - a) for a, b in zip(centers[:-1], centers[1:])] max_jump = max(jumps) if jumps else 0.0 return (max_jump <= max_center_jump), {"max_center_jump": max_jump, "n_centers": len(centers)} def is_open_field(mask_bgr: np.ndarray, bands: int = 15, y0_frac: float = 0.10, y1_frac: float = 0.90, tol_color: int = 40, smooth_k: int = 51, cane_peak_min: float = 0.12, floor_open_thr: float = 0.70, cane_present_thr: float = 0.06, max_n2_ratio: float = 0.10, # máximo % de bandas com 2 paredes min_n0_ratio: float = 0.70, # mínimo % de bandas com 0 paredes top_corridor_veto_ratio: float = 0.25, # veto se topo já tem muito 2-paredes bad_ignore_thr: float = 0.35, bad_obst_thr: float = 0.25, min_valid_bands_ratio: float = 0.33): """ Decide APENAS se o frame está em OPEN_FIELD (campo aberto: só chão, sem paredes). Retorna: (is_open: bool, metrics: dict) Robustez extra: - exige chão alto e cana baixa - exige dominância de 0 paredes (n0 alto) - limita 2 paredes (n2 baixo) - VETA OPEN_FIELD se topo já apresenta padrão de corredor (t2 alto) - trata bandas ruins (ignore/obst) """ img = mask_bgr H, W = img.shape[:2] x_center = W // 2 smooth_k = _safe_smooth_k(W, smooth_k) # cores alvo em BGR (ajuste conforme teu mask) CHAO_BGR = (0, 0, 128) # vermelho CANA_BGR = (0, 128, 0) # verde OBST_BGR = (128, 0, 0) # azul m_chao = color_mask_bgr(img, CHAO_BGR, tol=tol_color) m_cana = color_mask_bgr(img, CANA_BGR, tol=tol_color) m_obst = color_mask_bgr(img, OBST_BGR, tol=tol_color) m_known = ((m_chao > 0) | (m_cana > 0) | (m_obst > 0)).astype(np.uint8) * 255 m_ignore = cv2.bitwise_not(m_known) # região vertical analisada y0 = int(H * y0_frac) y1 = int(H * y1_frac) if y1 <= y0 + 20: y0 = int(H * 0.5) y1 = int(H * 0.95) band_h = max(10, (y1 - y0) // max(1, bands)) per_band = [] n0 = n1 = n2 = 0 for bi in range(bands): ya = y0 + bi * band_h yb = min(y1, ya + band_h) if yb <= ya + 5: continue chao_band = (m_chao[ya:yb, :] > 0).astype(np.float32) cana_band = (m_cana[ya:yb, :] > 0).astype(np.float32) ignore_band = (m_ignore[ya:yb, :] > 0).astype(np.float32) obst_band = (m_obst[ya:yb, :] > 0).astype(np.float32) score_chao = smooth_1d(np.mean(chao_band, axis=0), smooth_k) score_cana = smooth_1d(np.mean(cana_band, axis=0), smooth_k) ignore_ratio = float(np.mean(ignore_band)) obst_ratio = float(np.mean(obst_band)) floor_ratio = float(np.mean(score_chao)) cane_ratio = float(np.mean(score_cana)) left_peak = float(np.max(score_cana[:x_center])) if x_center > 5 else 0.0 right_peak = float(np.max(score_cana[x_center:])) if (W - x_center) > 5 else 0.0 has_left = left_peak >= cane_peak_min has_right = right_peak >= cane_peak_min walls = int(has_left) + int(has_right) bad_band = (ignore_ratio > bad_ignore_thr) or (obst_ratio > bad_obst_thr) if not bad_band: if walls == 0: n0 += 1 elif walls == 1: n1 += 1 else: n2 += 1 per_band.append({ "bi": bi, "walls": walls, "bad": bad_band, "floor_ratio": floor_ratio, "cane_ratio": cane_ratio, "left_peak": left_peak, "right_peak": right_peak, "ignore_ratio": ignore_ratio, "obst_ratio": obst_ratio, }) valid = [b for b in per_band if not b["bad"]] n_valid = len(valid) min_valid = max(3, int(bands * min_valid_bands_ratio)) if n_valid < min_valid: return False, { "reason": "few_valid_bands", "n_valid": n_valid, "min_valid": min_valid, "n0": n0, "n1": n1, "n2": n2 } # split topo/base mid = bands / 2.0 top = [b for b in valid if b["bi"] < mid] bot = [b for b in valid if b["bi"] >= mid] # se por algum motivo um lado ficou vazio (muito ruído), fallback simples: if len(top) == 0 or len(bot) == 0: half = max(1, n_valid // 2) top = valid[:half] bot = valid[half:] t2 = sum(1 for b in top if b["walls"] == 2) b2 = sum(1 for b in bot if b["walls"] == 2) mean_floor = float(np.mean([b["floor_ratio"] for b in valid])) mean_cane = float(np.mean([b["cane_ratio"] for b in valid])) n0_ratio = n0 / max(1, n_valid) n2_ratio = n2 / max(1, n_valid) # VETO: se topo já tem “cara de corredor”, não é campo aberto # (mesmo que a base ainda seja chão) top_veto_thr = max(2, int(len(top) * top_corridor_veto_ratio)) top_has_corridor = (t2 >= top_veto_thr) # regra base OPEN_FIELD robusta ok_floor = (mean_floor >= floor_open_thr) ok_cane = (mean_cane <= cane_present_thr) ok_n2 = (n2_ratio <= max_n2_ratio) ok_n0 = (n0_ratio >= min_n0_ratio) is_open = (ok_floor and ok_cane and ok_n2 and ok_n0 and (not top_has_corridor)) # 1) chão: acima do thr é bom (vira 1), abaixo cai floor_strength = _clip01(mean_floor / max(1e-6, floor_open_thr)) # 2) cana: abaixo do thr é bom (vira 1), acima cai cane_strength = _clip01(1.0 - (mean_cane / max(1e-6, cane_present_thr))) # 3) dominância de 0 paredes: acima do mínimo é bom n0_strength = _clip01(n0_ratio / max(1e-6, min_n0_ratio)) # 4) poucas bandas com 2 paredes: abaixo do máximo é bom n2_strength = _clip01(1.0 - (n2_ratio / max(1e-6, max_n2_ratio))) # 5) qualidade (quantas bandas válidas) quality = _clip01(n_valid / max(1, bands)) # 6) penalidade forte se topo já parece corredor (veto) corridor_pen = 1.0 if top_has_corridor else 0.0 # mistura ponderada (ajusta pesos se quiser) base_conf = ( 0.30 * floor_strength + 0.25 * cane_strength + 0.20 * n0_strength + 0.15 * n2_strength + 0.10 * quality ) # Gates: se falhou algum critério, confiança cai junto gate = 1.0 gate *= 1.0 if ok_floor else 0.30 gate *= 1.0 if ok_cane else 0.25 gate *= 1.0 if ok_n0 else 0.35 gate *= 1.0 if ok_n2 else 0.35 # Penalidade forte se topo já tem “cara de corredor” if top_has_corridor: gate *= 0.08 # Penalidade leve se está com pouca banda válida (passou raspando) # (só ajuda a deixar conf mais honesta perto do limiar) min_valid = max(3, int(bands * min_valid_bands_ratio)) if n_valid <= min_valid + 1: gate *= 0.70 open_conf = 100.0 * _clip01(base_conf) * _clip01(gate) # aplica veto como penalidade (derruba bem se topo tem corredor) open_conf = 100.0 * _clip01(base_conf) * (0.15 if corridor_pen > 0 else 1.0) return is_open, { "n_valid": n_valid, "n0": n0, "n1": n1, "n2": n2, "n0_ratio": n0_ratio, "n2_ratio": n2_ratio, "t2": t2, "b2": b2, "top_veto_thr": top_veto_thr, "top_has_corridor": top_has_corridor, "mean_floor": mean_floor, "mean_cane": mean_cane, "ok_floor": ok_floor, "ok_cane": ok_cane, "ok_n2": ok_n2, "ok_n0": ok_n0, "open_conf": round(open_conf, 2), "conf_dbg": { "floor_strength": round(floor_strength, 3), "cane_strength": round(cane_strength, 3), "n0_strength": round(n0_strength, 3), "n2_strength": round(n2_strength, 3), "quality": round(quality, 3), "base_conf": round(base_conf, 3), "gate": round(gate, 3), }, } def is_entering_corridor(mask_bgr: np.ndarray, bands: int = 15, y0_frac: float = 0.10, y1_frac: float = 0.90, tol_color: int = 40, smooth_k: int = 51, cane_peak_min: float = 0.12, # topo: precisa ter "corredor" (via cana-chão-cana) top_corr_ratio: float = 0.35, top_min_corr_bands: int = 2, # base: precisa ainda ter "aberto" (fronteira móvel) min_open_bottom_bands: int = 1, # >=1 banda sem corredor no fundo max_bot_corr_ratio: float = 0.30, # base não pode estar "cheia" de corredor # ainda usamos walls pra vetar ONE_WALL e IN_CORRIDOR bot_b1_ratio_max: float = 0.20, bot_max_bands_1wall: int = 2, in_corridor_bot_t2_ratio: float = 0.33, top_t2_ratio: float = 0.40, top_min_bands_2walls: int = 2, # qualidade bad_ignore_thr: float = 0.35, bad_obst_thr: float = 0.25, min_valid_bands_ratio: float = 0.33, # parâmetros do auxiliar (gap) gap_floor_thr: float = 0.45, gap_cane_max: float = 0.18, gap_min_width_frac: float = 0.18, peaks_min_sep_frac: float = 0.22, ): """ ENTERING (transição para dentro do corredor, com fronteira móvel): - Existe corredor no horizonte (muitas bandas superiores com corr_ok) - Ainda sobra chão aberto no fundo (open_run_bottom >= 1, ou >= limiar) - Base não está "dominada" por corredor (evita confundir com IN_CORRIDOR) - Vetos: ONE_WALL consistente na base, ou já IN_CORRIDOR Retorna: (is_entering: bool, metrics: dict) metrics inclui: - enter_progress 0..100 (quanto já está entrando) - enter_conf 0..100 (confiança do ENTERING) """ img = mask_bgr H, W = img.shape[:2] x_center = W // 2 smooth_k = _safe_smooth_k(W, smooth_k) CHAO_BGR = (0, 0, 128) CANA_BGR = (0, 128, 0) OBST_BGR = (128, 0, 0) m_chao = color_mask_bgr(img, CHAO_BGR, tol=tol_color) m_cana = color_mask_bgr(img, CANA_BGR, tol=tol_color) m_obst = color_mask_bgr(img, OBST_BGR, tol=tol_color) m_known = ((m_chao > 0) | (m_cana > 0) | (m_obst > 0)).astype(np.uint8) * 255 m_ignore = cv2.bitwise_not(m_known) y0 = int(H * y0_frac) y1 = int(H * y1_frac) if y1 <= y0 + 20: y0 = int(H * 0.5) y1 = int(H * 0.95) band_h = max(10, (y1 - y0) // max(1, bands)) per_band = [] for bi in range(bands): ya = y0 + bi * band_h yb = min(y1, ya + band_h) if yb <= ya + 5: continue chao_band = (m_chao[ya:yb, :] > 0).astype(np.float32) cana_band = (m_cana[ya:yb, :] > 0).astype(np.float32) ignore_band = (m_ignore[ya:yb, :] > 0).astype(np.float32) obst_band = (m_obst[ya:yb, :] > 0).astype(np.float32) score_chao = smooth_1d(np.mean(chao_band, axis=0), smooth_k) score_cana = smooth_1d(np.mean(cana_band, axis=0), smooth_k) ignore_ratio = float(np.mean(ignore_band)) obst_ratio = float(np.mean(obst_band)) # walls (secundário, mas útil p/ vetos) left_peak = float(np.max(score_cana[:x_center])) if x_center > 5 else 0.0 right_peak = float(np.max(score_cana[x_center:])) if (W - x_center) > 5 else 0.0 has_left = left_peak >= cane_peak_min has_right = right_peak >= cane_peak_min walls = int(has_left) + int(has_right) side = "none" if walls == 1: side = "L" if has_left else "R" elif walls == 2: side = "LR" # corredor invariante no X (principal) corr_ok, corr_info = band_has_corridor( score_cana, score_chao, cane_thr=cane_peak_min, gap_floor_thr=gap_floor_thr, gap_cane_max=gap_cane_max, gap_min_width_frac=gap_min_width_frac, peaks_min_sep_frac=peaks_min_sep_frac, ) bad_band = (ignore_ratio > bad_ignore_thr) or (obst_ratio > bad_obst_thr) per_band.append({ "bi": bi, "bad": bad_band, "walls": walls, "side": side, "corr_ok": bool(corr_ok), "left_peak": left_peak, "right_peak": right_peak, "ignore_ratio": ignore_ratio, "obst_ratio": obst_ratio, "corr_info": corr_info, }) valid = [b for b in per_band if not b["bad"]] n_valid = len(valid) min_valid = max(3, int(bands * min_valid_bands_ratio)) if n_valid < min_valid: return False, { "reason": "few_valid_bands", "n_valid": n_valid, "min_valid": min_valid } # ordena por bi (de cima -> baixo) valid_sorted = sorted(valid, key=lambda b: b["bi"]) corr_vec = [1 if b["corr_ok"] else 0 for b in valid_sorted] n_corr_total = sum(corr_vec) corr_ratio_total = n_corr_total / max(1, len(corr_vec)) # fronteira móvel: quantas bandas "abertas" ainda sobram no fundo? open_run_bottom = _run_length_from_bottom(corr_vec, 0) corr_run_bottom = _run_length_from_bottom(corr_vec, 1) # progresso (0..100): 0 = começou a aparecer corredor lá em cima; 100 = sobra só 1 banda aberta no fundo N = len(corr_vec) enter_progress = 100.0 * (1.0 - _clip01((open_run_bottom - 1) / max(1, (N - 1)))) # ainda mantemos topo/base para métricas e vetos (secundário) mid = bands / 2.0 top = [b for b in valid_sorted if b["bi"] < mid] bot = [b for b in valid_sorted if b["bi"] >= mid] if len(top) == 0 or len(bot) == 0: half = max(1, n_valid // 2) top = valid_sorted[:half] bot = valid_sorted[half:] def counts_walls(lst): c0 = sum(1 for b in lst if b["walls"] == 0) c1 = sum(1 for b in lst if b["walls"] == 1) c2 = sum(1 for b in lst if b["walls"] == 2) return c0, c1, c2 def count_corr(lst): return sum(1 for b in lst if b["corr_ok"]) t0, t1, t2 = counts_walls(top) b0, b1, b2 = counts_walls(bot) tCorr = count_corr(top) bCorr = count_corr(bot) t2_ratio = t2 / max(1, len(top)) b2_ratio = b2 / max(1, len(bot)) b1_ratio = b1 / max(1, len(bot)) tCorr_ratio = tCorr / max(1, len(top)) bCorr_ratio = bCorr / max(1, len(bot)) # Regras principais ok_top_corr = (tCorr >= max(top_min_corr_bands, int(len(top) * top_corr_ratio))) # Base aberta por fronteira móvel (quebra corte fixo!) ok_bot_open_by_frontier = (open_run_bottom >= min_open_bottom_bands) # Base não pode estar "dominada" por corredor (evita IN_CORRIDOR travestido) ok_bot_not_full_corridor = (bCorr_ratio <= max_bot_corr_ratio) # evitar ONE_WALL na base ok_bot_no_onewall = (b1 <= bot_max_bands_1wall) and (b1_ratio <= bot_b1_ratio_max) # veto IN_CORRIDOR (secundário por walls2, ainda útil) veto_in_corridor = (b2_ratio >= in_corridor_bot_t2_ratio) and (t2_ratio >= top_t2_ratio) # debug extra: topo com 2 paredes suficiente (selo secundário) ok_top_walls2_dbg = (t2 >= max(top_min_bands_2walls, int(len(top) * top_t2_ratio))) # veto one-wall consistente na base (mesmo lado) bot_onewall_sides = [b["side"] for b in bot if b["walls"] == 1] veto_bot_onewall_consistent = (len(bot_onewall_sides) >= 3) and ( bot_onewall_sides.count("R") / len(bot_onewall_sides) >= 0.85 or bot_onewall_sides.count("L") / len(bot_onewall_sides) >= 0.85 ) is_entering = ( ok_top_corr and ok_bot_open_by_frontier and ok_bot_not_full_corridor and ok_bot_no_onewall and (not veto_in_corridor) and (not veto_bot_onewall_consistent) ) # ---------------- CONF (0..100) com gates + penalidades ---------------- # “Força” do topo: precisa ter corredor e também bater mínimo absoluto (top_min_corr_bands) top_ratio_strength = _clip01(tCorr_ratio / max(1e-6, top_corr_ratio)) top_abs_strength = _clip01(tCorr / max(1, top_min_corr_bands)) top_strength = min(top_ratio_strength, top_abs_strength) # “Força” do aberto no fundo: não satura tão cedo # Queremos: 0 quando open_run_bottom < min_open_bottom_bands, # 1 quando open_run_bottom >= (min_open_bottom_bands + 3) por exemplo. open_margin = 3 # ajusta (2 a 4 costuma ficar bom) open_strength = _clip01((open_run_bottom - min_open_bottom_bands) / max(1, open_margin)) # Base não “dominada” por corredor: 1 bom, 0 ruim base_ok = _clip01(1.0 - (bCorr_ratio / max(1e-6, max_bot_corr_ratio))) # One-wall na base: 1 bom, 0 ruim (usa tanto contagem quanto razão) onewall_ratio_ok = _clip01(1.0 - (b1_ratio / max(1e-6, bot_b1_ratio_max))) onewall_abs_ok = _clip01(1.0 - (b1 / max(1, bot_max_bands_1wall))) no_onewall_strength = min(onewall_ratio_ok, onewall_abs_ok) # Qualidade quality = _clip01(n_valid / max(1, bands)) # Score base (sem veto) score = ( 0.45 * top_strength + 0.25 * open_strength + 0.20 * base_ok + 0.10 * quality ) # Gates: se alguma condição principal falha, a confiança cai junto gate = 1.0 gate *= 1.0 if ok_top_corr else 0.35 gate *= 1.0 if ok_bot_open_by_frontier else 0.25 gate *= 1.0 if ok_bot_not_full_corridor else 0.35 gate *= 1.0 if ok_bot_no_onewall else 0.40 # Penalidades de veto (derruba forte) if veto_in_corridor: gate *= 0.08 if veto_bot_onewall_consistent: gate *= 0.12 enter_conf = 100.0 * _clip01(score) * _clip01(gate) return is_entering, { "n_valid": n_valid, # métricas globais "corr_ratio_total": corr_ratio_total, "open_run_bottom": open_run_bottom, "corr_run_bottom_dbg": corr_run_bottom, # novo: progressão + confiança "enter_progress": round(enter_progress, 2), "enter_conf": round(enter_conf, 2), "conf_dbg": { "top_strength": round(top_strength, 3), "open_strength": round(open_strength, 3), "base_ok": round(base_ok, 3), "no_onewall_strength": round(no_onewall_strength, 3), "quality": round(quality, 3), "gate": round(gate, 3), "score": round(score, 3), }, # métricas topo/base (secundárias) "top": (t0, t1, t2), "bot": (b0, b1, b2), "t2_ratio": t2_ratio, "b1_ratio": b1_ratio, "b2_ratio": b2_ratio, "tCorr": tCorr, "bCorr": bCorr, "tCorr_ratio": tCorr_ratio, "bCorr_ratio": bCorr_ratio, # regras "ok_top_corr": ok_top_corr, "ok_bot_open_by_frontier": ok_bot_open_by_frontier, "ok_bot_not_full_corridor": ok_bot_not_full_corridor, "ok_bot_no_onewall": ok_bot_no_onewall, "veto_in_corridor": veto_in_corridor, "veto_bot_onewall_consistent": veto_bot_onewall_consistent, # debug extra "ok_top_walls2_dbg": ok_top_walls2_dbg, } def is_exiting_corridor(mask_bgr: np.ndarray, bands: int = 15, y0_frac: float = 0.10, y1_frac: float = 0.90, tol_color: int = 40, smooth_k: int = 51, cane_peak_min: float = 0.12, # topo: precisa já estar "aberto" (pouco corredor no horizonte) top_corr_ratio_max: float = 0.15, top_max_corr_bands: int = 1, top_t0_ratio: float = 0.50, # ajuda a confirmar "aberto" # base: ainda tem corredor (fronteira móvel) min_corr_bottom_bands: int = 1, # >=1 banda com corredor no fundo min_bot_corr_ratio: float = 0.30, # base não pode estar "vazia" de corredor # vetos (secundários) in_corridor_top_corr_ratio: float = 0.30, # se topo ainda tem corredor forte => IN_CORRIDOR in_corridor_bot_corr_ratio: float = 0.30, # evitar confundir com ONE_WALL no topo (secundário) top_t1_ratio_max: float = 0.30, top_max_bands_1wall: int = 2, # qualidade bad_ignore_thr: float = 0.35, bad_obst_thr: float = 0.25, min_valid_bands_ratio: float = 0.33, # parâmetros do auxiliar (gap) gap_floor_thr: float = 0.45, gap_cane_max: float = 0.18, gap_min_width_frac: float = 0.18, peaks_min_sep_frac: float = 0.22, ): """ EXITING (transição saindo do corredor, com fronteira móvel): - Base: ainda há corredor embaixo (corr_run_bottom >= limiar) - Topo: corredor já sumiu (tCorr_ratio baixo) - Vetos: se topo ainda tem corredor forte junto com base forte => IN_CORRIDOR Retorna: (is_exiting: bool, metrics: dict) metrics inclui: - exit_progress 0..100 (quanto já está saindo; 100 = quase fora, sobra 1 banda de corredor no fundo) - exit_conf 0..100 (confiança do EXITING) """ img = mask_bgr H, W = img.shape[:2] x_center = W // 2 smooth_k = _safe_smooth_k(W, smooth_k) CHAO_BGR = (0, 0, 128) CANA_BGR = (0, 128, 0) OBST_BGR = (128, 0, 0) m_chao = color_mask_bgr(img, CHAO_BGR, tol=tol_color) m_cana = color_mask_bgr(img, CANA_BGR, tol=tol_color) m_obst = color_mask_bgr(img, OBST_BGR, tol=tol_color) m_known = ((m_chao > 0) | (m_cana > 0) | (m_obst > 0)).astype(np.uint8) * 255 m_ignore = cv2.bitwise_not(m_known) y0 = int(H * y0_frac) y1 = int(H * y1_frac) if y1 <= y0 + 20: y0 = int(H * 0.5) y1 = int(H * 0.95) band_h = max(10, (y1 - y0) // max(1, bands)) per_band = [] for bi in range(bands): ya = y0 + bi * band_h yb = min(y1, ya + band_h) if yb <= ya + 5: continue chao_band = (m_chao[ya:yb, :] > 0).astype(np.float32) cana_band = (m_cana[ya:yb, :] > 0).astype(np.float32) ignore_band = (m_ignore[ya:yb, :] > 0).astype(np.float32) obst_band = (m_obst[ya:yb, :] > 0).astype(np.float32) score_chao = smooth_1d(np.mean(chao_band, axis=0), smooth_k) score_cana = smooth_1d(np.mean(cana_band, axis=0), smooth_k) ignore_ratio = float(np.mean(ignore_band)) obst_ratio = float(np.mean(obst_band)) # walls (secundário, mas útil p/ depuração e veto de one-wall) left_peak = float(np.max(score_cana[:x_center])) if x_center > 5 else 0.0 right_peak = float(np.max(score_cana[x_center:])) if (W - x_center) > 5 else 0.0 has_left = left_peak >= cane_peak_min has_right = right_peak >= cane_peak_min walls = int(has_left) + int(has_right) side = "none" if walls == 1: side = "L" if has_left else "R" elif walls == 2: side = "LR" # corredor invariante no X (principal) corr_ok, corr_info = band_has_corridor( score_cana, score_chao, cane_thr=cane_peak_min, gap_floor_thr=gap_floor_thr, gap_cane_max=gap_cane_max, gap_min_width_frac=gap_min_width_frac, peaks_min_sep_frac=peaks_min_sep_frac, ) bad_band = (ignore_ratio > bad_ignore_thr) or (obst_ratio > bad_obst_thr) per_band.append({ "bi": bi, "bad": bad_band, "walls": walls, "side": side, "corr_ok": bool(corr_ok), "left_peak": left_peak, "right_peak": right_peak, "ignore_ratio": ignore_ratio, "obst_ratio": obst_ratio, "corr_info": corr_info, }) valid = [b for b in per_band if not b["bad"]] n_valid = len(valid) min_valid = max(3, int(bands * min_valid_bands_ratio)) if n_valid < min_valid: return False, { "reason": "few_valid_bands", "n_valid": n_valid, "min_valid": min_valid } # ordena por bi (de cima -> baixo) valid_sorted = sorted(valid, key=lambda b: b["bi"]) corr_vec = [1 if b["corr_ok"] else 0 for b in valid_sorted] n_corr_total = sum(corr_vec) corr_ratio_total = n_corr_total / max(1, len(corr_vec)) # fronteira móvel: quantas bandas ainda têm corredor no fundo? corr_run_bottom = _run_length_from_bottom(corr_vec, 1) open_run_bottom = _run_length_from_bottom(corr_vec, 0) # progresso (0..100): 0 = começou a abrir no topo; 100 = sobra só 1 banda de corredor no fundo (quase fora) N = len(corr_vec) exit_progress = 100.0 * (1.0 - _clip01((corr_run_bottom - 1) / max(1, (N - 1)))) # topo/base para métricas e vetos (secundário) mid = bands / 2.0 top = [b for b in valid_sorted if b["bi"] < mid] bot = [b for b in valid_sorted if b["bi"] >= mid] if len(top) == 0 or len(bot) == 0: half = max(1, n_valid // 2) top = valid_sorted[:half] bot = valid_sorted[half:] def counts_walls(lst): c0 = sum(1 for b in lst if b["walls"] == 0) c1 = sum(1 for b in lst if b["walls"] == 1) c2 = sum(1 for b in lst if b["walls"] == 2) return c0, c1, c2 def count_corr(lst): return sum(1 for b in lst if b["corr_ok"]) t0, t1, t2 = counts_walls(top) b0, b1, b2 = counts_walls(bot) tCorr = count_corr(top) bCorr = count_corr(bot) t0_ratio = t0 / max(1, len(top)) t1_ratio = t1 / max(1, len(top)) tCorr_ratio = tCorr / max(1, len(top)) bCorr_ratio = bCorr / max(1, len(bot)) # Regras principais ok_top_no_corr = (tCorr <= top_max_corr_bands) and (tCorr_ratio <= top_corr_ratio_max) ok_top_open = (t0_ratio >= top_t0_ratio) ok_bot_corr_by_frontier = (corr_run_bottom >= min_corr_bottom_bands) ok_bot_not_empty_corr = (bCorr_ratio >= min_bot_corr_ratio) # evitar confundir com ONE_WALL persistente no topo (secundário) ok_top_no_onewall = (t1 <= top_max_bands_1wall) and (t1_ratio <= top_t1_ratio_max) # veto IN_CORRIDOR: se topo ainda tem corredor forte e base tem corredor forte, então é IN_CORRIDOR veto_in_corridor = (tCorr_ratio >= in_corridor_top_corr_ratio) and (bCorr_ratio >= in_corridor_bot_corr_ratio) is_exiting = ( ok_top_no_corr and ok_top_open and ok_top_no_onewall and ok_bot_corr_by_frontier and ok_bot_not_empty_corr and (not veto_in_corridor) ) # ---------------- CONF (0..100) com gates + penalidades ---------------- # 1) topo aberto (precisa bater ratio e também "pouco corredor" no topo) top_open_strength = _clip01(t0_ratio / max(1e-6, top_t0_ratio)) # topo sem corredor: 1 bom, 0 ruim (cai rápido se passa do limiar) top_no_corr_strength = _clip01(1.0 - (tCorr_ratio / max(1e-6, top_corr_ratio_max))) # 2) ainda tem corredor no fundo (mas não saturar cedo) # 0 quando corr_run_bottom < min_corr_bottom_bands # 1 quando corr_run_bottom >= min + margem corr_margin = 3 # 2..4 costuma ficar bom corr_bottom_strength = _clip01((corr_run_bottom - min_corr_bottom_bands) / max(1, corr_margin)) # 3) base não pode estar "vazia" de corredor (bCorr_ratio >= min_bot_corr_ratio) bot_corr_strength = _clip01(bCorr_ratio / max(1e-6, min_bot_corr_ratio)) # 4) qualidade quality = _clip01(n_valid / max(1, bands)) score = ( 0.30 * top_open_strength + 0.30 * top_no_corr_strength + 0.25 * corr_bottom_strength + 0.10 * bot_corr_strength + 0.05 * quality ) # Gates (amarrar no booleano real) gate = 1.0 gate *= 1.0 if ok_top_no_corr else 0.20 gate *= 1.0 if ok_top_open else 0.35 gate *= 1.0 if ok_top_no_onewall else 0.45 gate *= 1.0 if ok_bot_corr_by_frontier else 0.25 gate *= 1.0 if ok_bot_not_empty_corr else 0.35 # Penalidade forte se parece IN_CORRIDOR if veto_in_corridor: gate *= 0.08 exit_conf = 100.0 * _clip01(score) * _clip01(gate) return is_exiting, { "n_valid": n_valid, # métricas globais "corr_ratio_total": corr_ratio_total, "corr_run_bottom": corr_run_bottom, "open_run_bottom_dbg": open_run_bottom, # novo: progressão + confiança "exit_progress": round(exit_progress, 2), "exit_conf": round(exit_conf, 2), "conf_dbg": { "top_open_strength": round(top_open_strength, 3), "top_no_corr_strength": round(top_no_corr_strength, 3), "corr_bottom_strength": round(corr_bottom_strength, 3), "bot_corr_strength": round(bot_corr_strength, 3), "quality": round(quality, 3), "gate": round(gate, 3), "score": round(score, 3), }, # métricas topo/base (secundárias) "top": (t0, t1, t2), "bot": (b0, b1, b2), "t0_ratio": t0_ratio, "t1_ratio": t1_ratio, "tCorr": tCorr, "bCorr": bCorr, "tCorr_ratio": tCorr_ratio, "bCorr_ratio": bCorr_ratio, # regras "ok_top_no_corr": ok_top_no_corr, "ok_top_open": ok_top_open, "ok_top_no_onewall": ok_top_no_onewall, "ok_bot_corr_by_frontier": ok_bot_corr_by_frontier, "ok_bot_not_empty_corr": ok_bot_not_empty_corr, "veto_in_corridor": veto_in_corridor, } def is_in_corridor(mask_bgr: np.ndarray, bands: int = 15, y0_frac: float = 0.10, y1_frac: float = 0.90, tol_color: int = 40, smooth_k: int = 51, cane_peak_min: float = 0.12, # modo "estrito" (cana adulta) min_corr_ratio_total: float = 0.60, min_top_corr_ratio: float = 0.55, min_bot_corr_ratio: float = 0.55, max_open_run_bottom: int = 1, # senão parece ENTERING max_open_run_top: int = 1, # senão parece EXITING # modo "ponte" (cana jovem / falhas no meio) bridge_max_gap_run: int = 4, # buraco máximo (0s seguidos) entre evidências bridge_top_margin: int = 1, # quantos índices do topo contam como “topo” bridge_bot_margin: int = 1, # quantos índices do fundo contam como “fundo” max_center_jump: float = 0.12, # consistência do corredor em x # qualidade bad_ignore_thr: float = 0.35, bad_obst_thr: float = 0.25, min_valid_bands_ratio: float = 0.33, # parâmetros do valley detector (repasse se quiser tunar) floor_min_ratio: float = 0.40, valley_min_width_frac: float = 0.18, peaks_min_sep_frac: float = 0.22, wall_run_thr: float = 0.10, wall_min_run_frac: float = 0.04, wall_side_margin_frac: float = 0.10, wall_min_peak: float = 0.12, valley_cane_max_ratio: float = 0.90, min_end_corr_ratio: float = 0.55, # extremidade clara min_end_thickwall_ratio: float = 0.55, # fallback: pelo menos 1 parede grossa numa extremidade “ruim” max_open_run_relaxed: int = 7, # (opcional) exige que não pareça transição demais ): """ IN_CORRIDOR (v2): - Estrito: corr_ratio_total/top/bot + sem "rabos abertos" (evita ENTERING/EXITING) - Robusto: existe ponte topo->base (bridge_ok) permitindo falhas no meio + centro do corredor consistente (center_ok) - Confiança: combina evidência total + evidência topo/base + ponte + penalidade de transição Retorna (is_in: bool, metrics: dict) """ img = mask_bgr H, W = img.shape[:2] x_center = W // 2 smooth_k = _safe_smooth_k(W, smooth_k) CHAO_BGR = (0, 0, 128) CANA_BGR = (0, 128, 0) OBST_BGR = (128, 0, 0) m_chao = color_mask_bgr(img, CHAO_BGR, tol=tol_color) m_cana = color_mask_bgr(img, CANA_BGR, tol=tol_color) m_obst = color_mask_bgr(img, OBST_BGR, tol=tol_color) m_known = ((m_chao > 0) | (m_cana > 0) | (m_obst > 0)).astype(np.uint8) * 255 m_ignore = cv2.bitwise_not(m_known) y0 = int(H * y0_frac) y1 = int(H * y1_frac) if y1 <= y0 + 20: y0 = int(H * 0.5) y1 = int(H * 0.95) band_h = max(10, (y1 - y0) // max(1, bands)) per_band = [] for bi in range(bands): ya = y0 + bi * band_h yb = min(y1, ya + band_h) if yb <= ya + 5: continue chao_band = (m_chao[ya:yb, :] > 0).astype(np.float32) cana_band = (m_cana[ya:yb, :] > 0).astype(np.float32) ignore_band = (m_ignore[ya:yb, :] > 0).astype(np.float32) obst_band = (m_obst[ya:yb, :] > 0).astype(np.float32) score_chao = smooth_1d(np.mean(chao_band, axis=0), smooth_k) score_cana = smooth_1d(np.mean(cana_band, axis=0), smooth_k) ignore_ratio = float(np.mean(ignore_band)) obst_ratio = float(np.mean(obst_band)) bad_band = (ignore_ratio > bad_ignore_thr) or (obst_ratio > bad_obst_thr) # walls (secundário) left_peak = float(np.max(score_cana[:x_center])) if x_center > 5 else 0.0 right_peak = float(np.max(score_cana[x_center:])) if (W - x_center) > 5 else 0.0 has_left = left_peak >= cane_peak_min has_right = right_peak >= cane_peak_min walls = int(has_left) + int(has_right) # corredor por banda (vale) corr_ok, corr_info = band_has_corridor_valley( score_cana, score_chao, cane_thr=cane_peak_min, floor_min_ratio=floor_min_ratio, valley_min_width_frac=valley_min_width_frac, peaks_min_sep_frac=peaks_min_sep_frac, wall_run_thr=wall_run_thr, wall_min_run_frac=wall_min_run_frac, wall_side_margin_frac=wall_side_margin_frac, wall_min_peak=wall_min_peak, valley_cane_max_ratio=valley_cane_max_ratio ) ci = corr_info if isinstance(corr_info, dict) else {} ok_left_wall = bool(ci.get("ok_left_wall", False)) ok_right_wall = bool(ci.get("ok_right_wall", False)) has_thick_wall = ok_left_wall or ok_right_wall per_band.append({ "bi": bi, "bad": bad_band, "walls": walls, "ignore_ratio": ignore_ratio, "obst_ratio": obst_ratio, "corr_ok": bool(corr_ok), "corr_info": corr_info if isinstance(corr_info, dict) else {}, "thick_wall": bool(has_thick_wall), }) valid = [b for b in per_band if not b["bad"]] n_valid = len(valid) min_valid = max(3, int(bands * min_valid_bands_ratio)) if n_valid < min_valid: return False, {"reason": "few_valid_bands", "n_valid": n_valid, "min_valid": min_valid} # ordena por bi (topo->base) valid_sorted = sorted(valid, key=lambda b: b["bi"]) corr_vec = [1 if b["corr_ok"] else 0 for b in valid_sorted] wall_vec = [1 if b.get("thick_wall") else 0 for b in valid_sorted] N = len(corr_vec) n_corr = sum(corr_vec) corr_ratio_total = n_corr / max(1, N) # topo/base (metade) mid = N / 2.0 top_idx = [i for i in range(N) if i < mid] bot_idx = [i for i in range(N) if i >= mid] if len(top_idx) == 0 or len(bot_idx) == 0: half = max(1, N // 2) top_idx = list(range(half)) bot_idx = list(range(half, N)) tCorr_ratio = sum(corr_vec[i] for i in top_idx) / max(1, len(top_idx)) bCorr_ratio = sum(corr_vec[i] for i in bot_idx) / max(1, len(bot_idx)) tWall_ratio = sum(wall_vec[i] for i in top_idx) / max(1, len(top_idx)) bWall_ratio = sum(wall_vec[i] for i in bot_idx) / max(1, len(bot_idx)) # fronteiras (transição) open_run_bottom = _run_length_from_bottom(corr_vec, 0) open_run_top = 0 for v in corr_vec: if v == 0: open_run_top += 1 else: break # ---------- 1) modo estrito ---------- ok_total = (corr_ratio_total >= min_corr_ratio_total) ok_top = (tCorr_ratio >= min_top_corr_ratio) ok_bot = (bCorr_ratio >= min_bot_corr_ratio) ok_no_entering = (open_run_bottom <= max_open_run_bottom) ok_no_exiting = (open_run_top <= max_open_run_top) strict_ok = (ok_total and ok_top and ok_bot and ok_no_entering and ok_no_exiting) # ---------- 2) modo ponte ---------- # regra: primeiro 1 tem que estar “perto do topo”, último 1 “perto do fundo” e max_gap <= thr bridge_ok, bridge_dbg = corridor_bridge_ok(corr_vec, max_gap_run=bridge_max_gap_run) # Ajusta os critérios topo/fundo com margens (pra não ficar dependente de ser idx<=1 etc) # Ex.: se o 1 começa até bridge_top_margin e termina até bridge_bot_margin do fundo, ok. if bridge_ok: idx_first = bridge_dbg.get("idx_first", 999) idx_last = bridge_dbg.get("idx_last", -999) bridge_ok = (idx_first <= bridge_top_margin) and (idx_last >= (N - 1 - bridge_bot_margin)) # consistência de centro (só nas bandas OK) ok_bands = [b for b in valid_sorted if b["corr_ok"] and ("center_frac" in b["corr_info"])] center_ok, center_dbg = corridor_center_consistent(ok_bands, max_center_jump=max_center_jump) # robusto passa se: ponte + centro consistente + não tem “cara forte” de entering/exiting # (aqui a gente só penaliza se tá MUITO aberto em cima/baixo) robust_trans_ok = (open_run_bottom <= max_open_run_bottom + 4) and (open_run_top <= max_open_run_top + 4) bridge_mode_ok = (bridge_ok and center_ok and robust_trans_ok) # ---------- 3) modo ENDWALL (uma ponta clara + parede grossa na outra) ---------- top_clear = (tCorr_ratio >= min_end_corr_ratio) bot_clear = (bCorr_ratio >= min_end_corr_ratio) top_has_wall = (tWall_ratio >= min_end_thickwall_ratio) bot_has_wall = (bWall_ratio >= min_end_thickwall_ratio) endwall_ok = ((top_clear and bot_has_wall) or (bot_clear and top_has_wall)) # transição relaxada para endwall (permite base/topo meio falhando) endwall_trans_ok = (open_run_bottom <= max_open_run_relaxed) and (open_run_top <= max_open_run_relaxed) endwall_mode_ok = bool(endwall_ok and endwall_trans_ok) # ---------- decisão final ---------- is_in = bool(strict_ok or bridge_mode_ok or endwall_mode_ok) mode = "NO" if strict_ok: mode = "STRICT" elif bridge_mode_ok: mode = "BRIDGE" elif endwall_mode_ok: mode = "ENDWALL" # ---------- CONF 0..100 ---------- # forças total_strength = _clip01(corr_ratio_total / max(1e-6, min_corr_ratio_total)) top_strength = _clip01(tCorr_ratio / max(1e-6, min_top_corr_ratio)) bot_strength = _clip01(bCorr_ratio / max(1e-6, min_bot_corr_ratio)) quality = _clip01(n_valid / max(1, bands)) # ponte fortalece quando strict falha max_gap = float(bridge_dbg.get("max_gap", bridge_max_gap_run + 1)) bridge_strength = _clip01(1.0 - (max_gap / max(1.0, bridge_max_gap_run))) if bridge_ok else 0.0 center_strength = _clip01(1.0 - (float(center_dbg.get("max_center_jump", max_center_jump + 1.0)) / max(1e-6, max_center_jump))) if center_ok else 0.0 bridge_combo = 0.6 * bridge_strength + 0.4 * center_strength # transição: quanto mais rabo aberto, mais parece entering/exiting enter_pen = _clip01(open_run_bottom / max(1, max_open_run_bottom + 1)) exit_pen = _clip01(open_run_top / max(1, max_open_run_top + 1)) trans_pen = 0.5 * (enter_pen + exit_pen) # 0 bom, 1 ruim # score base # - se strict tá forte: depende de total/top/bot # - se strict tá fraco: ponte entra pra salvar score = ( 0.35 * total_strength + 0.20 * min(top_strength, bot_strength) + 0.20 * (1.0 - trans_pen) + 0.15 * quality + 0.10 * bridge_combo ) # gate: amarra no boolean final, mas sem matar quando é “modo ponte” gate = 1.0 if strict_ok: gate *= 1.0 elif bridge_mode_ok: gate *= 0.55 elif endwall_mode_ok: gate *= 0.50 # levemente abaixo do bridge (ajustável) else: gate *= 0.18 # penaliza se parece MUITO entering/exiting if (open_run_bottom > (max_open_run_bottom + 6)) or (open_run_top > (max_open_run_top + 6)): gate *= 0.35 in_conf = 100.0 * _clip01(score) * _clip01(gate) cap = 1.0 if mode == "ENDWALL": cap = 0.85 elif mode == "BRIDGE": cap = 0.90 in_conf = 100.0 * _clip01(score) * _clip01(gate) * cap # ---------- métricas extras ---------- centers = [] widths = [] reasons = {} for b in valid_sorted: ci = b.get("corr_info", {}) if isinstance(ci, dict): if "center_frac" in ci: centers.append(float(ci["center_frac"])) if "width_frac" in ci: widths.append(float(ci["width_frac"])) r = ci.get("reason", "OK" if b.get("corr_ok") else "NO_REASON") else: r = "NO_INFO" reasons[r] = reasons.get(r, 0) + 1 center_mean = float(np.mean(centers)) if len(centers) else None width_mean = float(np.mean(widths)) if len(widths) else None return is_in, { "n_valid": n_valid, "corr_ratio_total": round(corr_ratio_total, 3), "tCorr_ratio": round(tCorr_ratio, 3), "bCorr_ratio": round(bCorr_ratio, 3), "open_run_bottom": int(open_run_bottom), "open_run_top": int(open_run_top), "bridge_ok": bool(bridge_ok), "bridge_mode_ok": bool(bridge_mode_ok), "bridge_dbg": bridge_dbg, "center_ok": bool(center_ok), "center_dbg": center_dbg, "center_mean_frac": None if center_mean is None else round(center_mean, 3), "width_mean_frac": None if width_mean is None else round(width_mean, 3), "strict_ok": bool(strict_ok), "ok_total": bool(ok_total), "ok_top": bool(ok_top), "ok_bot": bool(ok_bot), "ok_no_entering": bool(ok_no_entering), "ok_no_exiting": bool(ok_no_exiting), "in_conf": round(in_conf, 2), "conf_dbg": { "total_strength": round(float(total_strength), 3), "top_strength": round(float(top_strength), 3), "bot_strength": round(float(bot_strength), 3), "bridge_combo": round(float(bridge_combo), 3), "trans_pen": round(float(trans_pen), 3), "quality": round(float(quality), 3), "gate": round(float(gate), 3), "score": round(float(score), 3), }, "reasons": reasons, "mode": mode, "tWall_ratio": round(tWall_ratio, 3), "bWall_ratio": round(bWall_ratio, 3), "top_clear": bool(top_clear), "bot_clear": bool(bot_clear), "top_has_wall": bool(top_has_wall), "bot_has_wall": bool(bot_has_wall), "endwall_ok": bool(endwall_ok), "endwall_mode_ok": bool(endwall_mode_ok), } def main(): ap = argparse.ArgumentParser() ap.add_argument("--img", default=None, help="Path de 1 máscara PNG (vermelho=chão, verde=cana, azul=obstáculo)") ap.add_argument("--dir", default=None, help="Pasta com várias máscaras (png/jpg). Usa A/D para navegar.") ap.add_argument("--start", type=int, default=0, help="índice inicial ao abrir uma pasta") ap.add_argument("--tol", type=int, default=40, help="tolerância de cor (0-255)") ap.add_argument("--bands", type=int, default=15, help="quantidade de bandas horizontais a serem analisadas") ap.add_argument("--y0_frac", type=float, default=0.1, help="percentual inicial para ROI vertical") ap.add_argument("--y1_frac", type=float, default=0.9, help="percentual final para ROI vertical") ap.add_argument("--smooth_k", type=int, default=51, help="suavização 1D por coluna") ap.add_argument("--min_w_frac", type=float, default=0.25, help="largura mínima do corredor (fração da imagem)") ap.add_argument("--max_w_frac", type=float, default=1.0, help="largura máxima do corredor (fração da imagem)") ap.add_argument("--debug", action="store_true", help="mostra debug_bands") args = ap.parse_args() # monta lista de imagens files = [] if args.dir: files = list_images(args.dir) if not files: raise FileNotFoundError(f"Nenhuma imagem encontrada em: {args.dir}") elif args.img: files = [Path(args.img)] else: raise ValueError("Use --img ou --dir") idx = int(np.clip(args.start, 0, len(files) - 1)) def process_one(path: Path): img = cv2.imread(str(path), cv2.IMREAD_COLOR) if img is None: return None, None, 0.0, {"reason": "falha ao abrir"} pts, conf, extras = detect_corridor_trapezoid( img, bands=args.bands, y0_frac=args.y0_frac, y1_frac=args.y1_frac, smooth_k=args.smooth_k, min_w_frac=args.min_w_frac, max_w_frac=args.max_w_frac, tol_color=args.tol, debug=args.debug, ) vis = img.copy() if pts is not None: vis = draw_trapezoid(vis, pts, conf) else: cv2.putText(vis, "SEM CORREDOR", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2, cv2.LINE_AA) # overlay com nome e índice label = f"[{idx+1}/{len(files)}] {path.name} conf={conf:.2f}" cv2.putText(vis, label, (20, vis.shape[0]-20), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2, cv2.LINE_AA) dbg = None if args.debug and extras is not None and pts is not None: params = extras.get("dbg_params", {"y0_frac": args.y0_frac, "y1_frac": args.y1_frac, "bands": args.bands}) dbg = draw_band_debug(img, extras, params["y0_frac"], params["y1_frac"], params["bands"]) #regime, regm = detect_corridor_regime( # img, # bands=args.bands, # y0_frac=args.y0_frac, # y1_frac=args.y1_frac, # tol_color=args.tol, # smooth_k=args.smooth_k, #) #print(f"REGIME={regime} | n0={regm.get('n0')} n1={regm.get('n1')} n2={regm.get('n2')} " # f"| top={regm.get('top')} bot={regm.get('bot')} " # f"| floor={regm.get('mean_floor',0):.2f} cane={regm.get('mean_cane',0):.2f}") is_open, m = is_open_field(img, bands=args.bands, y0_frac=args.y0_frac, y1_frac=args.y1_frac, tol_color=args.tol, smooth_k=args.smooth_k) print(f"OPEN_FIELD = {is_open} | conf={m.get('open_conf')} | n0={m.get('n0')} n1={m.get('n1')} n2={m.get('n2')} " f"| floor={m.get('mean_floor',0):.2f} cane={m.get('mean_cane',0):.2f} " f"| t2={m.get('t2')} veto={m.get('top_has_corridor')} " f"| conf_dbg={m.get('conf_dbg')}") is_ent, m = is_entering_corridor(img, bands=args.bands, y0_frac=args.y0_frac, y1_frac=args.y1_frac, tol_color=args.tol, smooth_k=args.smooth_k) print(f"ENTERING = {is_ent} | conf={m.get('enter_conf')} | prog={m.get('enter_progress')} " f"| corr_total={m.get('corr_ratio_total')} open_run_bot={m.get('open_run_bottom')} " f"| top={m.get('top')} bot={m.get('bot')} " f"| ok_top_corr={m.get('ok_top_corr')} ok_bot_open={m.get('ok_bot_open_by_frontier')} " f"| ok_bot_not_full={m.get('ok_bot_not_full_corridor')} ok_bot_no1={m.get('ok_bot_no_onewall')} " f"| veto_in={m.get('veto_in_corridor')} veto_onewall={m.get('veto_bot_onewall_consistent')} " f"| conf_dbg={m.get('conf_dbg')}") is_exit, m = is_exiting_corridor(img, bands=args.bands, y0_frac=args.y0_frac, y1_frac=args.y1_frac, tol_color=args.tol, smooth_k=args.smooth_k) print(f"EXITING = {is_exit} | conf={m.get('exit_conf')} | prog={m.get('exit_progress')} " f"| corr_total={m.get('corr_ratio_total')} corr_run_bot={m.get('corr_run_bottom')} " f"| top={m.get('top')} bot={m.get('bot')} " f"| ok_top_no_corr={m.get('ok_top_no_corr')} ok_top_open={m.get('ok_top_open')} " f"| ok_top_no1={m.get('ok_top_no_onewall')} ok_bot_corr={m.get('ok_bot_corr_by_frontier')} " f"| ok_bot_not_empty={m.get('ok_bot_not_empty_corr')} veto_in={m.get('veto_in_corridor')} " f"| conf_dbg={m.get('conf_dbg')}") is_in, m = is_in_corridor(img, bands=args.bands, y0_frac=args.y0_frac, y1_frac=args.y1_frac, tol_color=args.tol, smooth_k=args.smooth_k) bd = m.get("bridge_dbg", {}) cd = m.get("center_dbg", {}) print(f"IN_CORRIDOR= {is_in} | conf={m.get('in_conf')} " f"| corr={m.get('corr_ratio_total')} topCorr={m.get('tCorr_ratio')} botCorr={m.get('bCorr_ratio')} " f"| topWall={m.get('tWall_ratio'):.3f} botWall={m.get('bWall_ratio'):.3f}" f"| openBot={m.get('open_run_bottom')} openTop={m.get('open_run_top')} " f"| strict={m.get('strict_ok')} bridgeMode={m.get('bridge_mode_ok')} bridge={m.get('bridge_ok')} " f"| maxGap={bd.get('max_gap')} maxJump={cd.get('max_center_jump')} " f"| center={m.get('center_mean_frac')} width={m.get('width_mean_frac')} " f"| conf_dbg={m.get('conf_dbg')}") # se quiser ver estatística de reason print("IN_CORRIDOR reasons:", m.get("reasons")) return img, vis, conf, extras, dbg # loop interativo cv2.namedWindow("corridor_trapezoid", cv2.WINDOW_NORMAL) cv2.namedWindow("mask", cv2.WINDOW_NORMAL) if args.debug: cv2.namedWindow("debug_bands", cv2.WINDOW_NORMAL) while True: path = files[idx] img, vis, conf, extras, dbg = process_one(path) if img is None: # se falhou abrir, pula print(f"[ERRO] Não abriu: {path}") else: print(f"[{idx+1}/{len(files)}] {path} -> conf={conf:.3f} | {extras.get('bands_ok','?')}/{extras.get('bands_total','?')}") cv2.imshow("mask", img) cv2.imshow("corridor_trapezoid", vis) if args.debug and dbg is not None: cv2.imshow("debug_bands", dbg) key = cv2.waitKey(0) & 0xFF # sair if key in (27, ord('q'), ord('Q')): # ESC / Q break # próximo if key in (ord('d'), ord('D'), 83): # D ou seta direita (83 geralmente no Windows) idx = min(len(files) - 1, idx + 1) continue # anterior if key in (ord('a'), ord('A'), 81): # A ou seta esquerda (81 geralmente no Windows) idx = max(0, idx - 1) continue # recomputar (mesma imagem) if key in (ord('r'), ord('R')): continue cv2.destroyAllWindows() if __name__ == "__main__": main()