agrobot_base/Python/OAK/detect_corridor.py

2211 lines
81 KiB
Python

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