1200 lines
49 KiB
Python
1200 lines
49 KiB
Python
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import math
|
||
|
|
import unicodedata
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any, Dict, List, Optional, Tuple
|
||
|
|
|
||
|
|
import cv2
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Dataset Alignment Browser V2
|
||
|
|
# ------------------------------------------------------------
|
||
|
|
# Objetivo:
|
||
|
|
# Navegar no dataset multiespectral e comparar:
|
||
|
|
# 1) space=final -> tensor final usado no treino/inferencia
|
||
|
|
# 2) space=native -> dados decodificados nativos, antes da homografia/crop/fusao
|
||
|
|
#
|
||
|
|
# Tambem testa alinhamento dinamico por bordas com prioridades:
|
||
|
|
# - global : usa a imagem toda
|
||
|
|
# - largest_blob : usa o maior blob de bordas fortes
|
||
|
|
# - gt_target : usa mascara GT de cana/erva, se existir
|
||
|
|
#
|
||
|
|
# Requisitos:
|
||
|
|
# - Colocar este arquivo no mesmo projeto onde existe utils/audit_dataset_manual.py
|
||
|
|
# - Rodar a partir da raiz do projeto, por exemplo:
|
||
|
|
# python -m utils.dataset_alignment_browser_v2 --input_path .\dataset\original\group\ --groups-except chao --space native
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
_AUDIT_IMPORT_ERROR = None
|
||
|
|
try:
|
||
|
|
from utils.audit_dataset_manual import (
|
||
|
|
find_dataset_roots,
|
||
|
|
list_meta_files,
|
||
|
|
load_multispec_tensor,
|
||
|
|
resolve_mask_path,
|
||
|
|
load_mask,
|
||
|
|
parse_csv_set,
|
||
|
|
load_json,
|
||
|
|
resolve_camera_payloads,
|
||
|
|
resolve_module_params_path,
|
||
|
|
get_raw_processor_core,
|
||
|
|
)
|
||
|
|
except Exception as e1:
|
||
|
|
try:
|
||
|
|
from audit_dataset_manual import (
|
||
|
|
find_dataset_roots,
|
||
|
|
list_meta_files,
|
||
|
|
load_multispec_tensor,
|
||
|
|
resolve_mask_path,
|
||
|
|
load_mask,
|
||
|
|
parse_csv_set,
|
||
|
|
load_json,
|
||
|
|
resolve_camera_payloads,
|
||
|
|
resolve_module_params_path,
|
||
|
|
get_raw_processor_core,
|
||
|
|
)
|
||
|
|
except Exception as e2:
|
||
|
|
_AUDIT_IMPORT_ERROR = (e1, e2)
|
||
|
|
find_dataset_roots = None
|
||
|
|
list_meta_files = None
|
||
|
|
load_multispec_tensor = None
|
||
|
|
resolve_mask_path = None
|
||
|
|
load_mask = None
|
||
|
|
parse_csv_set = None
|
||
|
|
load_json = None
|
||
|
|
resolve_camera_payloads = None
|
||
|
|
resolve_module_params_path = None
|
||
|
|
get_raw_processor_core = None
|
||
|
|
|
||
|
|
EPS = 1e-6
|
||
|
|
IGNORE_INDEX = 255
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Utilidades gerais
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
|
||
|
|
def ensure_imports_ok():
|
||
|
|
if find_dataset_roots is None:
|
||
|
|
msg = (
|
||
|
|
"Nao consegui importar funcoes do audit_dataset_manual.py.\n"
|
||
|
|
"Coloque este script no mesmo projeto do auditor e rode a partir da raiz do projeto.\n"
|
||
|
|
)
|
||
|
|
if _AUDIT_IMPORT_ERROR:
|
||
|
|
msg += f"\nImport error 1: {_AUDIT_IMPORT_ERROR[0]}\nImport error 2: {_AUDIT_IMPORT_ERROR[1]}"
|
||
|
|
raise RuntimeError(msg)
|
||
|
|
|
||
|
|
|
||
|
|
def cv_text(text: Any) -> str:
|
||
|
|
s = str(text)
|
||
|
|
s = unicodedata.normalize("NFKD", s)
|
||
|
|
s = s.encode("ascii", "ignore").decode("ascii")
|
||
|
|
return s
|
||
|
|
|
||
|
|
|
||
|
|
def normalize_to_u8(x: np.ndarray, p_low: float = 1.0, p_high: float = 99.0) -> np.ndarray:
|
||
|
|
arr = x.astype(np.float32, copy=False)
|
||
|
|
finite = np.isfinite(arr)
|
||
|
|
if not np.any(finite):
|
||
|
|
return np.zeros(arr.shape[:2], dtype=np.uint8)
|
||
|
|
|
||
|
|
vals = arr[finite]
|
||
|
|
lo = float(np.percentile(vals, p_low))
|
||
|
|
hi = float(np.percentile(vals, p_high))
|
||
|
|
if hi <= lo + EPS:
|
||
|
|
hi = lo + 1.0
|
||
|
|
|
||
|
|
y = (arr - lo) / (hi - lo)
|
||
|
|
y = np.clip(y, 0.0, 1.0)
|
||
|
|
return (y * 255.0).astype(np.uint8)
|
||
|
|
|
||
|
|
|
||
|
|
def float01_to_u8(x: np.ndarray) -> np.ndarray:
|
||
|
|
return np.clip(x.astype(np.float32) * 255.0, 0, 255).astype(np.uint8)
|
||
|
|
|
||
|
|
|
||
|
|
def rgb_hwc_to_bgr(rgb: np.ndarray, stretch: bool = False) -> np.ndarray:
|
||
|
|
rgb = np.asarray(rgb, dtype=np.float32)
|
||
|
|
if stretch:
|
||
|
|
chans = [normalize_to_u8(rgb[:, :, i]) for i in range(3)]
|
||
|
|
rgb_u8 = np.dstack(chans)
|
||
|
|
else:
|
||
|
|
rgb_u8 = float01_to_u8(rgb)
|
||
|
|
return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
|
||
|
|
|
||
|
|
|
||
|
|
def rgb_from_tensor(tensor: np.ndarray, stretch: bool = False) -> np.ndarray:
|
||
|
|
rgb = np.transpose(tensor[:3], (1, 2, 0)).astype(np.float32)
|
||
|
|
return rgb_hwc_to_bgr(rgb, stretch=stretch)
|
||
|
|
|
||
|
|
|
||
|
|
def gray_from_rgb_hwc(rgb: np.ndarray) -> np.ndarray:
|
||
|
|
rgb = np.asarray(rgb, dtype=np.float32)
|
||
|
|
return (0.299 * rgb[:, :, 0] + 0.587 * rgb[:, :, 1] + 0.114 * rgb[:, :, 2]).astype(np.float32)
|
||
|
|
|
||
|
|
|
||
|
|
def gray_from_tensor_rgb(tensor: np.ndarray) -> np.ndarray:
|
||
|
|
r, g, b = [tensor[i].astype(np.float32, copy=False) for i in range(3)]
|
||
|
|
return (0.299 * r + 0.587 * g + 0.114 * b).astype(np.float32)
|
||
|
|
|
||
|
|
|
||
|
|
def resize_to(img: np.ndarray, hw: Tuple[int, int], interp: int = cv2.INTER_LINEAR) -> np.ndarray:
|
||
|
|
h, w = int(hw[0]), int(hw[1])
|
||
|
|
if img.shape[:2] == (h, w):
|
||
|
|
return img
|
||
|
|
return cv2.resize(img, (w, h), interpolation=interp)
|
||
|
|
|
||
|
|
|
||
|
|
def gradient_mag(x: np.ndarray) -> np.ndarray:
|
||
|
|
u8 = normalize_to_u8(x)
|
||
|
|
gx = cv2.Sobel(u8, cv2.CV_32F, 1, 0, ksize=3)
|
||
|
|
gy = cv2.Sobel(u8, cv2.CV_32F, 0, 1, ksize=3)
|
||
|
|
return cv2.magnitude(gx, gy).astype(np.float32)
|
||
|
|
|
||
|
|
|
||
|
|
def edge_binary(x: np.ndarray, low: int = 60, high: int = 140) -> np.ndarray:
|
||
|
|
return cv2.Canny(normalize_to_u8(x), low, high)
|
||
|
|
|
||
|
|
|
||
|
|
def colorize_gray(x: np.ndarray, cmap: int = cv2.COLORMAP_VIRIDIS) -> np.ndarray:
|
||
|
|
return cv2.applyColorMap(normalize_to_u8(x), cmap)
|
||
|
|
|
||
|
|
|
||
|
|
def put_label(img: np.ndarray, title: str, subtitle: str = "") -> np.ndarray:
|
||
|
|
out = img.copy()
|
||
|
|
title = cv_text(title)
|
||
|
|
subtitle = cv_text(subtitle)
|
||
|
|
hbox = 58 if subtitle else 36
|
||
|
|
cv2.rectangle(out, (0, 0), (out.shape[1], hbox), (0, 0, 0), -1)
|
||
|
|
cv2.putText(out, title, (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.62, (0, 255, 255), 2, cv2.LINE_AA)
|
||
|
|
if subtitle:
|
||
|
|
cv2.putText(out, subtitle[:165], (10, 48), cv2.FONT_HERSHEY_SIMPLEX, 0.43, (255, 255, 255), 1, cv2.LINE_AA)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def resize_keep(img: np.ndarray, target_w: int) -> np.ndarray:
|
||
|
|
scale = float(target_w) / float(img.shape[1])
|
||
|
|
target_h = max(1, int(img.shape[0] * scale))
|
||
|
|
return cv2.resize(img, (target_w, target_h), interpolation=cv2.INTER_AREA)
|
||
|
|
|
||
|
|
|
||
|
|
def make_grid(panels: List[Tuple[str, np.ndarray, str]], panel_w: int = 410, cols: int = 3) -> np.ndarray:
|
||
|
|
rendered: List[np.ndarray] = []
|
||
|
|
for title, img, subtitle in panels:
|
||
|
|
if img.ndim == 2:
|
||
|
|
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
||
|
|
small = resize_keep(img, panel_w)
|
||
|
|
rendered.append(put_label(small, title, subtitle))
|
||
|
|
|
||
|
|
if not rendered:
|
||
|
|
return np.zeros((300, 600, 3), dtype=np.uint8)
|
||
|
|
|
||
|
|
max_h = max(x.shape[0] for x in rendered)
|
||
|
|
padded: List[np.ndarray] = []
|
||
|
|
for im in rendered:
|
||
|
|
if im.shape[0] < max_h:
|
||
|
|
pad = np.zeros((max_h - im.shape[0], im.shape[1], 3), dtype=np.uint8)
|
||
|
|
im = np.vstack([im, pad])
|
||
|
|
padded.append(im)
|
||
|
|
|
||
|
|
gap = 10
|
||
|
|
gap_w = np.full((max_h, gap, 3), 24, dtype=np.uint8)
|
||
|
|
rows: List[np.ndarray] = []
|
||
|
|
filler = np.zeros_like(padded[0])
|
||
|
|
|
||
|
|
for i in range(0, len(padded), cols):
|
||
|
|
items = padded[i:i + cols]
|
||
|
|
while len(items) < cols:
|
||
|
|
items.append(filler.copy())
|
||
|
|
row = items[0]
|
||
|
|
for j in range(1, cols):
|
||
|
|
row = np.hstack([row, gap_w, items[j]])
|
||
|
|
rows.append(row)
|
||
|
|
|
||
|
|
gap_h = np.full((gap, rows[0].shape[1], 3), 24, dtype=np.uint8)
|
||
|
|
canvas = rows[0]
|
||
|
|
for r in rows[1:]:
|
||
|
|
canvas = np.vstack([canvas, gap_h, r])
|
||
|
|
return canvas
|
||
|
|
|
||
|
|
|
||
|
|
def make_info_panel(lines: List[str], size: Tuple[int, int] = (900, 280)) -> np.ndarray:
|
||
|
|
w, h = size
|
||
|
|
img = np.zeros((h, w, 3), dtype=np.uint8)
|
||
|
|
cv2.rectangle(img, (0, 0), (w - 1, h - 1), (70, 70, 70), 1)
|
||
|
|
y = 28
|
||
|
|
for i, line in enumerate(lines):
|
||
|
|
color = (0, 255, 255) if i == 0 else (235, 235, 235)
|
||
|
|
cv2.putText(img, cv_text(line[:145]), (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.52, color, 1, cv2.LINE_AA)
|
||
|
|
y += 23
|
||
|
|
if y > h - 12:
|
||
|
|
break
|
||
|
|
return img
|
||
|
|
|
||
|
|
|
||
|
|
def falsecolor_overlay(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
||
|
|
"""
|
||
|
|
Verde=A, Magenta=B.
|
||
|
|
Onde casa, tende a ficar claro/cinza/branco. Onde desalinha, aparecem franjas.
|
||
|
|
"""
|
||
|
|
au8 = normalize_to_u8(a)
|
||
|
|
bu8 = normalize_to_u8(b)
|
||
|
|
out = np.zeros((au8.shape[0], au8.shape[1], 3), dtype=np.uint8)
|
||
|
|
out[:, :, 1] = au8
|
||
|
|
out[:, :, 0] = bu8
|
||
|
|
out[:, :, 2] = bu8
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def alpha_blend(base_bgr: np.ndarray, overlay_gray: np.ndarray, alpha: float = 0.38,
|
||
|
|
cmap: int = cv2.COLORMAP_TURBO) -> np.ndarray:
|
||
|
|
cm = colorize_gray(overlay_gray, cmap)
|
||
|
|
cm = resize_to(cm, base_bgr.shape[:2])
|
||
|
|
return cv2.addWeighted(base_bgr, 1.0 - alpha, cm, alpha, 0.0)
|
||
|
|
|
||
|
|
|
||
|
|
def edge_overlay(rgb_gray: np.ndarray, re: np.ndarray, nir: np.ndarray) -> np.ndarray:
|
||
|
|
e_rgb = normalize_to_u8(gradient_mag(rgb_gray), 5, 99)
|
||
|
|
e_re = normalize_to_u8(gradient_mag(re), 5, 99)
|
||
|
|
e_nir = normalize_to_u8(gradient_mag(nir), 5, 99)
|
||
|
|
out = np.zeros((e_rgb.shape[0], e_rgb.shape[1], 3), dtype=np.uint8)
|
||
|
|
out[:, :, 1] = e_rgb
|
||
|
|
out[:, :, 2] = e_re
|
||
|
|
out[:, :, 0] = e_nir
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def draw_edges_on_rgb(rgb_bgr: np.ndarray, img: np.ndarray, color: Tuple[int, int, int]) -> np.ndarray:
|
||
|
|
out = rgb_bgr.copy()
|
||
|
|
img = resize_to(img, rgb_bgr.shape[:2])
|
||
|
|
ed = edge_binary(img)
|
||
|
|
out[ed > 0] = color
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def mask_overlay(rgb_bgr: np.ndarray, mask: Optional[np.ndarray]) -> np.ndarray:
|
||
|
|
if mask is None:
|
||
|
|
return rgb_bgr.copy()
|
||
|
|
|
||
|
|
mask = resize_to(mask.astype(np.int32), rgb_bgr.shape[:2], interp=cv2.INTER_NEAREST)
|
||
|
|
out = rgb_bgr.copy()
|
||
|
|
color_mask = np.zeros_like(out)
|
||
|
|
palette = {
|
||
|
|
0: (0, 0, 128),
|
||
|
|
1: (128, 0, 0),
|
||
|
|
2: (0, 128, 0),
|
||
|
|
255: (0, 0, 0),
|
||
|
|
}
|
||
|
|
for cls_id in np.unique(mask):
|
||
|
|
color_mask[mask == int(cls_id)] = palette.get(int(cls_id), (100, 100, 100))
|
||
|
|
return cv2.addWeighted(out, 0.70, color_mask, 0.30, 0)
|
||
|
|
|
||
|
|
|
||
|
|
def draw_roi(img: np.ndarray, roi_mask: Optional[np.ndarray], bbox: Optional[Tuple[int, int, int, int]], title: str = "") -> np.ndarray:
|
||
|
|
out = img.copy()
|
||
|
|
if roi_mask is not None:
|
||
|
|
mask = resize_to(roi_mask.astype(np.uint8), out.shape[:2], interp=cv2.INTER_NEAREST)
|
||
|
|
tint = np.zeros_like(out)
|
||
|
|
tint[:, :, 1] = 255
|
||
|
|
out = np.where(mask[:, :, None] > 0, cv2.addWeighted(out, 0.55, tint, 0.45, 0), out)
|
||
|
|
if bbox is not None:
|
||
|
|
x0, y0, x1, y1 = bbox
|
||
|
|
cv2.rectangle(out, (x0, y0), (x1, y1), (0, 255, 255), 2)
|
||
|
|
if title:
|
||
|
|
cv2.putText(out, cv_text(title), (10, out.shape[0] - 12), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 255), 1, cv2.LINE_AA)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def draw_crop_box(img: np.ndarray, crop_box: Optional[Tuple[int, int, int, int]], label: str = "crop") -> np.ndarray:
|
||
|
|
out = img.copy()
|
||
|
|
if crop_box is None:
|
||
|
|
return out
|
||
|
|
x0, y0, x1, y1 = [int(v) for v in crop_box]
|
||
|
|
cv2.rectangle(out, (x0, y0), (x1, y1), (0, 255, 255), 2)
|
||
|
|
cv2.putText(out, cv_text(label), (x0 + 6, max(22, y0 + 22)), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 255), 1, cv2.LINE_AA)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Registro por bordas / ROI
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class AlignResult:
|
||
|
|
method: str
|
||
|
|
priority: str
|
||
|
|
accepted: bool
|
||
|
|
warp: np.ndarray
|
||
|
|
used_inverse_map: bool
|
||
|
|
roi_mask: Optional[np.ndarray]
|
||
|
|
roi_bbox: Optional[Tuple[int, int, int, int]]
|
||
|
|
phase_dx: float = 0.0
|
||
|
|
phase_dy: float = 0.0
|
||
|
|
phase_response: float = 0.0
|
||
|
|
edge_corr_before: float = 0.0
|
||
|
|
edge_corr_after: float = 0.0
|
||
|
|
roi_corr_before: float = 0.0
|
||
|
|
roi_corr_after: float = 0.0
|
||
|
|
translation_px: float = 0.0
|
||
|
|
rotation_deg: float = 0.0
|
||
|
|
note: str = ""
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
def edge_corr(a: np.ndarray, b: np.ndarray, roi_mask: Optional[np.ndarray] = None) -> float:
|
||
|
|
ga = gradient_mag(a)
|
||
|
|
gb = gradient_mag(b)
|
||
|
|
|
||
|
|
if roi_mask is not None:
|
||
|
|
m = resize_to(roi_mask.astype(np.uint8), ga.shape[:2], interp=cv2.INTER_NEAREST) > 0
|
||
|
|
if np.count_nonzero(m) < 32:
|
||
|
|
return 0.0
|
||
|
|
va = ga[m].reshape(-1)
|
||
|
|
vb = gb[m].reshape(-1)
|
||
|
|
else:
|
||
|
|
va = ga.reshape(-1)
|
||
|
|
vb = gb.reshape(-1)
|
||
|
|
|
||
|
|
if va.size == 0 or vb.size == 0 or np.std(va) < EPS or np.std(vb) < EPS:
|
||
|
|
return 0.0
|
||
|
|
return float(np.corrcoef(va, vb)[0, 1])
|
||
|
|
|
||
|
|
|
||
|
|
def bbox_from_mask(mask: np.ndarray, pad: int = 8) -> Optional[Tuple[int, int, int, int]]:
|
||
|
|
ys, xs = np.where(mask > 0)
|
||
|
|
if xs.size == 0 or ys.size == 0:
|
||
|
|
return None
|
||
|
|
h, w = mask.shape[:2]
|
||
|
|
x0 = max(0, int(xs.min()) - pad)
|
||
|
|
y0 = max(0, int(ys.min()) - pad)
|
||
|
|
x1 = min(w, int(xs.max()) + 1 + pad)
|
||
|
|
y1 = min(h, int(ys.max()) + 1 + pad)
|
||
|
|
if x1 <= x0 or y1 <= y0:
|
||
|
|
return None
|
||
|
|
return x0, y0, x1, y1
|
||
|
|
|
||
|
|
|
||
|
|
def largest_edge_blob_mask(ref: np.ndarray, tgt: np.ndarray, min_area_frac: float = 0.003,
|
||
|
|
dilate_iter: int = 5) -> Tuple[Optional[np.ndarray], Optional[Tuple[int, int, int, int]], str]:
|
||
|
|
ref_g = gradient_mag(ref)
|
||
|
|
tgt_g = gradient_mag(tgt)
|
||
|
|
combo = np.maximum(normalize_to_u8(ref_g, 70, 99.5), normalize_to_u8(tgt_g, 70, 99.5))
|
||
|
|
|
||
|
|
# threshold robusto por percentil: pega bordas fortes, nao toda a palhada fina.
|
||
|
|
th = max(25, int(np.percentile(combo, 88)))
|
||
|
|
strong = (combo >= th).astype(np.uint8) * 255
|
||
|
|
|
||
|
|
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
||
|
|
strong = cv2.morphologyEx(strong, cv2.MORPH_CLOSE, k, iterations=2)
|
||
|
|
strong = cv2.dilate(strong, k, iterations=max(1, int(dilate_iter)))
|
||
|
|
|
||
|
|
n, labels, stats, _cent = cv2.connectedComponentsWithStats(strong, connectivity=8)
|
||
|
|
if n <= 1:
|
||
|
|
return None, None, "sem componentes"
|
||
|
|
|
||
|
|
h, w = strong.shape[:2]
|
||
|
|
min_area = int(float(min_area_frac) * h * w)
|
||
|
|
|
||
|
|
best_id = None
|
||
|
|
best_score = -1.0
|
||
|
|
for cid in range(1, n):
|
||
|
|
x, y, bw, bh, area = stats[cid]
|
||
|
|
if area < min_area:
|
||
|
|
continue
|
||
|
|
# Favorece area, mas tambem energia de borda dentro do blob.
|
||
|
|
m = labels == cid
|
||
|
|
energy = float(np.mean(combo[m])) if np.any(m) else 0.0
|
||
|
|
score = float(area) * (1.0 + energy / 255.0)
|
||
|
|
if score > best_score:
|
||
|
|
best_score = score
|
||
|
|
best_id = cid
|
||
|
|
|
||
|
|
if best_id is None:
|
||
|
|
return None, None, f"sem blob >= {min_area}px"
|
||
|
|
|
||
|
|
mask = (labels == best_id).astype(np.uint8)
|
||
|
|
bbox = bbox_from_mask(mask, pad=10)
|
||
|
|
area = int(stats[best_id, cv2.CC_STAT_AREA])
|
||
|
|
return mask, bbox, f"largest_blob id={best_id} area={area} score={best_score:.1f}"
|
||
|
|
|
||
|
|
|
||
|
|
def gt_target_mask(mask: Optional[np.ndarray], target_classes: List[int], hw: Tuple[int, int]) -> Tuple[Optional[np.ndarray], Optional[Tuple[int, int, int, int]], str]:
|
||
|
|
if mask is None:
|
||
|
|
return None, None, "sem GT mask"
|
||
|
|
m = resize_to(mask.astype(np.int32), hw, interp=cv2.INTER_NEAREST)
|
||
|
|
out = np.zeros(hw, dtype=np.uint8)
|
||
|
|
for cls_id in target_classes:
|
||
|
|
out[m == int(cls_id)] = 1
|
||
|
|
if np.count_nonzero(out) < 32:
|
||
|
|
return None, None, f"GT target vazio classes={target_classes}"
|
||
|
|
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
||
|
|
out = cv2.dilate(out, k, iterations=3)
|
||
|
|
return out, bbox_from_mask(out, pad=10), f"GT target classes={target_classes} area={int(np.count_nonzero(out))}"
|
||
|
|
|
||
|
|
|
||
|
|
def make_alignment_roi(ref: np.ndarray, tgt: np.ndarray, priority: str,
|
||
|
|
mask: Optional[np.ndarray], target_classes: List[int]) -> Tuple[Optional[np.ndarray], Optional[Tuple[int, int, int, int]], str]:
|
||
|
|
priority = str(priority).lower()
|
||
|
|
hw = ref.shape[:2]
|
||
|
|
if priority == "global":
|
||
|
|
return None, None, "global/full frame"
|
||
|
|
if priority == "largest_blob":
|
||
|
|
return largest_edge_blob_mask(ref, tgt)
|
||
|
|
if priority == "gt_target":
|
||
|
|
return gt_target_mask(mask, target_classes, hw)
|
||
|
|
return None, None, f"priority desconhecida: {priority}"
|
||
|
|
|
||
|
|
|
||
|
|
def crop_by_bbox(a: np.ndarray, bbox: Optional[Tuple[int, int, int, int]]) -> np.ndarray:
|
||
|
|
if bbox is None:
|
||
|
|
return a
|
||
|
|
x0, y0, x1, y1 = bbox
|
||
|
|
return a[y0:y1, x0:x1]
|
||
|
|
|
||
|
|
|
||
|
|
def estimate_phase_shift(
|
||
|
|
ref: np.ndarray,
|
||
|
|
tgt: np.ndarray,
|
||
|
|
bbox: Optional[Tuple[int, int, int, int]] = None,
|
||
|
|
roi_mask: Optional[np.ndarray] = None,
|
||
|
|
) -> Tuple[float, float, float]:
|
||
|
|
rr = crop_by_bbox(gradient_mag(ref), bbox)
|
||
|
|
tt = crop_by_bbox(gradient_mag(tgt), bbox)
|
||
|
|
|
||
|
|
if roi_mask is not None:
|
||
|
|
m = resize_to(roi_mask.astype(np.uint8), ref.shape[:2], interp=cv2.INTER_NEAREST)
|
||
|
|
m = crop_by_bbox(m, bbox)
|
||
|
|
if m.shape[:2] == rr.shape[:2] and np.count_nonzero(m) >= 32:
|
||
|
|
# Usa a mascara real da ROI, nao apenas o bbox. Isso faz gt_target/largest_blob
|
||
|
|
# puxarem a estimativa para o objeto dominante em vez da textura do retangulo inteiro.
|
||
|
|
mf = (m > 0).astype(np.float32)
|
||
|
|
rr = rr * mf
|
||
|
|
tt = tt * mf
|
||
|
|
|
||
|
|
rr = normalize_to_u8(rr).astype(np.float32)
|
||
|
|
tt = normalize_to_u8(tt).astype(np.float32)
|
||
|
|
try:
|
||
|
|
(dx, dy), resp = cv2.phaseCorrelate(rr, tt)
|
||
|
|
return float(dx), float(dy), float(resp)
|
||
|
|
except Exception:
|
||
|
|
return 0.0, 0.0, 0.0
|
||
|
|
|
||
|
|
|
||
|
|
def warp_from_phase(dx: float, dy: float) -> np.ndarray:
|
||
|
|
# Para alinhar tgt ao ref, desloca pelo negativo do shift estimado.
|
||
|
|
return np.array([[1.0, 0.0, -dx], [0.0, 1.0, -dy]], dtype=np.float32)
|
||
|
|
|
||
|
|
|
||
|
|
def warp_2x3_to_3x3(W: np.ndarray) -> np.ndarray:
|
||
|
|
H = np.eye(3, dtype=np.float32)
|
||
|
|
H[:2, :] = W.astype(np.float32)
|
||
|
|
return H
|
||
|
|
|
||
|
|
|
||
|
|
def warp_3x3_to_2x3(H: np.ndarray) -> np.ndarray:
|
||
|
|
return H[:2, :].astype(np.float32)
|
||
|
|
|
||
|
|
|
||
|
|
def local_warp_to_full(W_local: np.ndarray, bbox: Optional[Tuple[int, int, int, int]]) -> np.ndarray:
|
||
|
|
if bbox is None:
|
||
|
|
return W_local.astype(np.float32)
|
||
|
|
x0, y0, _x1, _y1 = bbox
|
||
|
|
T_full_to_local = np.array([[1, 0, -x0], [0, 1, -y0], [0, 0, 1]], dtype=np.float32)
|
||
|
|
T_local_to_full = np.array([[1, 0, x0], [0, 1, y0], [0, 0, 1]], dtype=np.float32)
|
||
|
|
H_local = warp_2x3_to_3x3(W_local)
|
||
|
|
H_full = T_local_to_full @ H_local @ T_full_to_local
|
||
|
|
return warp_3x3_to_2x3(H_full)
|
||
|
|
|
||
|
|
|
||
|
|
def try_ecc_alignment(ref: np.ndarray, tgt: np.ndarray, method: str,
|
||
|
|
bbox: Optional[Tuple[int, int, int, int]] = None,
|
||
|
|
max_iter: int = 60, eps: float = 1e-5) -> Tuple[np.ndarray, bool, str]:
|
||
|
|
ref_crop = crop_by_bbox(ref, bbox)
|
||
|
|
tgt_crop = crop_by_bbox(tgt, bbox)
|
||
|
|
|
||
|
|
ref_img = normalize_to_u8(gradient_mag(ref_crop)).astype(np.float32) / 255.0
|
||
|
|
tgt_img = normalize_to_u8(gradient_mag(tgt_crop)).astype(np.float32) / 255.0
|
||
|
|
|
||
|
|
if ref_img.shape[0] < 30 or ref_img.shape[1] < 30:
|
||
|
|
return np.eye(2, 3, dtype=np.float32), False, "ECC ROI pequena"
|
||
|
|
|
||
|
|
if method == "ecc_translation":
|
||
|
|
motion = cv2.MOTION_TRANSLATION
|
||
|
|
elif method == "ecc_euclidean":
|
||
|
|
motion = cv2.MOTION_EUCLIDEAN
|
||
|
|
elif method == "ecc_affine":
|
||
|
|
motion = cv2.MOTION_AFFINE
|
||
|
|
else:
|
||
|
|
raise ValueError(f"Metodo ECC invalido: {method}")
|
||
|
|
|
||
|
|
warp = np.eye(2, 3, dtype=np.float32)
|
||
|
|
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, max_iter, eps)
|
||
|
|
try:
|
||
|
|
cc, W_local = cv2.findTransformECC(ref_img, tgt_img, warp, motion, criteria)
|
||
|
|
W_full = local_warp_to_full(W_local.astype(np.float32), bbox)
|
||
|
|
return W_full, True, f"ECC cc={cc:.4f}"
|
||
|
|
except cv2.error as e:
|
||
|
|
return np.eye(2, 3, dtype=np.float32), False, f"ECC falhou: {str(e)[:90]}"
|
||
|
|
|
||
|
|
|
||
|
|
def extract_warp_metrics(warp: np.ndarray) -> Tuple[float, float]:
|
||
|
|
tx = float(warp[0, 2])
|
||
|
|
ty = float(warp[1, 2])
|
||
|
|
translation = math.hypot(tx, ty)
|
||
|
|
a = float(warp[0, 0])
|
||
|
|
b = float(warp[0, 1])
|
||
|
|
rot = -math.degrees(math.atan2(b, a))
|
||
|
|
return translation, rot
|
||
|
|
|
||
|
|
|
||
|
|
def apply_warp(img: np.ndarray, warp: np.ndarray, inverse_map: bool = False,
|
||
|
|
border_mode: int = cv2.BORDER_REFLECT101) -> np.ndarray:
|
||
|
|
flags = cv2.INTER_LINEAR
|
||
|
|
if inverse_map:
|
||
|
|
flags |= cv2.WARP_INVERSE_MAP
|
||
|
|
return cv2.warpAffine(
|
||
|
|
img.astype(np.float32),
|
||
|
|
warp.astype(np.float32),
|
||
|
|
(img.shape[1], img.shape[0]),
|
||
|
|
flags=flags,
|
||
|
|
borderMode=border_mode,
|
||
|
|
borderValue=0.0,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def estimate_alignment(ref: np.ndarray, tgt: np.ndarray, method: str, priority: str,
|
||
|
|
mask: Optional[np.ndarray], target_classes: List[int],
|
||
|
|
max_shift_px: float, max_rotation_deg: float,
|
||
|
|
min_improve_corr: float = -0.005) -> AlignResult:
|
||
|
|
roi_mask, roi_bbox, roi_note = make_alignment_roi(ref, tgt, priority, mask, target_classes)
|
||
|
|
|
||
|
|
corr_before = edge_corr(ref, tgt)
|
||
|
|
roi_corr_before = edge_corr(ref, tgt, roi_mask)
|
||
|
|
pdx, pdy, presp = estimate_phase_shift(ref, tgt, roi_bbox, roi_mask)
|
||
|
|
|
||
|
|
if method == "none":
|
||
|
|
return AlignResult(
|
||
|
|
method=method,
|
||
|
|
priority=priority,
|
||
|
|
accepted=False,
|
||
|
|
warp=np.eye(2, 3, dtype=np.float32),
|
||
|
|
used_inverse_map=False,
|
||
|
|
roi_mask=roi_mask,
|
||
|
|
roi_bbox=roi_bbox,
|
||
|
|
phase_dx=pdx,
|
||
|
|
phase_dy=pdy,
|
||
|
|
phase_response=presp,
|
||
|
|
edge_corr_before=corr_before,
|
||
|
|
edge_corr_after=corr_before,
|
||
|
|
roi_corr_before=roi_corr_before,
|
||
|
|
roi_corr_after=roi_corr_before,
|
||
|
|
note=f"sem correcao | {roi_note}",
|
||
|
|
)
|
||
|
|
|
||
|
|
if method == "phase":
|
||
|
|
warp = warp_from_phase(pdx, pdy)
|
||
|
|
corrected = apply_warp(tgt, warp, inverse_map=False)
|
||
|
|
corr_after = edge_corr(ref, corrected)
|
||
|
|
roi_corr_after = edge_corr(ref, corrected, roi_mask)
|
||
|
|
trans, rot = extract_warp_metrics(warp)
|
||
|
|
accepted = trans <= max_shift_px and (corr_after >= corr_before + min_improve_corr or roi_corr_after >= roi_corr_before + min_improve_corr)
|
||
|
|
note = f"phase resp={presp:.3f} | {roi_note}"
|
||
|
|
if not accepted:
|
||
|
|
# Mantem o warp proposto mesmo rejeitado. Assim o browser consegue mostrar
|
||
|
|
# o modo PROPOSTO/forcado para diagnostico visual.
|
||
|
|
note += " | rejeitado"
|
||
|
|
return AlignResult(
|
||
|
|
method=method,
|
||
|
|
priority=priority,
|
||
|
|
accepted=accepted,
|
||
|
|
warp=warp,
|
||
|
|
used_inverse_map=False,
|
||
|
|
roi_mask=roi_mask,
|
||
|
|
roi_bbox=roi_bbox,
|
||
|
|
phase_dx=pdx,
|
||
|
|
phase_dy=pdy,
|
||
|
|
phase_response=presp,
|
||
|
|
edge_corr_before=corr_before,
|
||
|
|
edge_corr_after=corr_after,
|
||
|
|
roi_corr_before=roi_corr_before,
|
||
|
|
roi_corr_after=roi_corr_after,
|
||
|
|
translation_px=trans,
|
||
|
|
rotation_deg=rot,
|
||
|
|
note=note,
|
||
|
|
)
|
||
|
|
|
||
|
|
warp, ok, note = try_ecc_alignment(ref, tgt, method, bbox=roi_bbox)
|
||
|
|
if not ok:
|
||
|
|
return AlignResult(
|
||
|
|
method=method,
|
||
|
|
priority=priority,
|
||
|
|
accepted=False,
|
||
|
|
warp=np.eye(2, 3, dtype=np.float32),
|
||
|
|
used_inverse_map=True,
|
||
|
|
roi_mask=roi_mask,
|
||
|
|
roi_bbox=roi_bbox,
|
||
|
|
phase_dx=pdx,
|
||
|
|
phase_dy=pdy,
|
||
|
|
phase_response=presp,
|
||
|
|
edge_corr_before=corr_before,
|
||
|
|
edge_corr_after=corr_before,
|
||
|
|
roi_corr_before=roi_corr_before,
|
||
|
|
roi_corr_after=roi_corr_before,
|
||
|
|
note=f"{note} | {roi_note}",
|
||
|
|
)
|
||
|
|
|
||
|
|
corrected = apply_warp(tgt, warp, inverse_map=True)
|
||
|
|
corr_after = edge_corr(ref, corrected)
|
||
|
|
roi_corr_after = edge_corr(ref, corrected, roi_mask)
|
||
|
|
trans, rot = extract_warp_metrics(warp)
|
||
|
|
accepted = (
|
||
|
|
trans <= max_shift_px
|
||
|
|
and abs(rot) <= max_rotation_deg
|
||
|
|
and (corr_after >= corr_before + min_improve_corr or roi_corr_after >= roi_corr_before + min_improve_corr)
|
||
|
|
)
|
||
|
|
|
||
|
|
if not accepted:
|
||
|
|
# Mantem o warp proposto mesmo rejeitado. Assim o browser consegue mostrar
|
||
|
|
# o modo PROPOSTO/forcado para diagnostico visual.
|
||
|
|
note += " | rejeitado por limite/correlacao"
|
||
|
|
|
||
|
|
return AlignResult(
|
||
|
|
method=method,
|
||
|
|
priority=priority,
|
||
|
|
accepted=accepted,
|
||
|
|
warp=warp,
|
||
|
|
used_inverse_map=True,
|
||
|
|
roi_mask=roi_mask,
|
||
|
|
roi_bbox=roi_bbox,
|
||
|
|
phase_dx=pdx,
|
||
|
|
phase_dy=pdy,
|
||
|
|
phase_response=presp,
|
||
|
|
edge_corr_before=corr_before,
|
||
|
|
edge_corr_after=corr_after,
|
||
|
|
roi_corr_before=roi_corr_before,
|
||
|
|
roi_corr_after=roi_corr_after,
|
||
|
|
translation_px=trans,
|
||
|
|
rotation_deg=rot,
|
||
|
|
note=f"{note} | {roi_note}",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Leitura dos dados: final e native
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class SampleEntry:
|
||
|
|
dataset_root: Path
|
||
|
|
meta_path: Path
|
||
|
|
sample_name: str
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class FusionDebug:
|
||
|
|
available: bool
|
||
|
|
warped_re: Optional[np.ndarray] = None
|
||
|
|
warped_nir: Optional[np.ndarray] = None
|
||
|
|
valid_re: Optional[np.ndarray] = None
|
||
|
|
valid_nir: Optional[np.ndarray] = None
|
||
|
|
common_mask: Optional[np.ndarray] = None
|
||
|
|
crop_box: Optional[Tuple[int, int, int, int]] = None
|
||
|
|
note: str = ""
|
||
|
|
|
||
|
|
|
||
|
|
class NativeDecoder:
|
||
|
|
def __init__(self):
|
||
|
|
self.core_cache: Dict[Any, Any] = {}
|
||
|
|
|
||
|
|
def load_native(self, entry: SampleEntry) -> Dict[str, Any]:
|
||
|
|
meta = load_json(entry.meta_path)
|
||
|
|
saved_dtypes = meta.get("saved_payload_dtypes", {}) or {}
|
||
|
|
saved_shapes = meta.get("saved_payload_shapes", {}) or {}
|
||
|
|
cam_paths = resolve_camera_payloads(entry.meta_path, entry.dataset_root, meta)
|
||
|
|
|
||
|
|
frame: Dict[str, np.ndarray] = {}
|
||
|
|
for cam_id, payload_path in cam_paths.items():
|
||
|
|
saved_dtype = saved_dtypes.get(cam_id)
|
||
|
|
saved_shape = saved_shapes.get(cam_id)
|
||
|
|
if saved_dtype is None or saved_shape is None:
|
||
|
|
raise RuntimeError(f"Faltam dtype/shape para {cam_id} em {entry.meta_path.name}")
|
||
|
|
arr = np.fromfile(str(payload_path), dtype=np.dtype(saved_dtype)).reshape(tuple(saved_shape))
|
||
|
|
frame[cam_id] = arr
|
||
|
|
|
||
|
|
sensor_width = int(meta.get("sensor_width", 1280))
|
||
|
|
sensor_height = int(meta.get("sensor_height", 800))
|
||
|
|
bayer = meta.get("bayer_pattern", "RGGB")
|
||
|
|
calib_path = resolve_module_params_path(entry.meta_path, entry.dataset_root, meta)
|
||
|
|
|
||
|
|
core = get_raw_processor_core(
|
||
|
|
core_cache=self.core_cache,
|
||
|
|
sensor_width=sensor_width,
|
||
|
|
sensor_height=sensor_height,
|
||
|
|
bayer=bayer,
|
||
|
|
calib_path=calib_path,
|
||
|
|
)
|
||
|
|
|
||
|
|
stream_meta = meta.get("stream_meta", {}) or {}
|
||
|
|
processing_meta = dict(stream_meta)
|
||
|
|
|
||
|
|
if meta.get("actual_camera_controls") is not None:
|
||
|
|
processing_meta["actual_camera_controls"] = meta.get("actual_camera_controls")
|
||
|
|
if meta.get("startup_camera_controls") is not None:
|
||
|
|
processing_meta["startup_camera_controls"] = meta.get("startup_camera_controls")
|
||
|
|
if meta.get("radiometric_last_result") is not None:
|
||
|
|
processing_meta["radiometric_last_result"] = meta.get("radiometric_last_result")
|
||
|
|
|
||
|
|
decoded = core.decode_stream_cameras(frame, processing_meta)
|
||
|
|
return {
|
||
|
|
"meta": meta,
|
||
|
|
"processing_meta": processing_meta,
|
||
|
|
"decoded": decoded,
|
||
|
|
"core": core,
|
||
|
|
"calib_path": calib_path,
|
||
|
|
"source": "native_decoded_before_fusion",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def role_to_images(decoded: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict[str, str]]:
|
||
|
|
rgb = None
|
||
|
|
re = None
|
||
|
|
nir = None
|
||
|
|
role_cam = {}
|
||
|
|
for cam_id, item in decoded.items():
|
||
|
|
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
|
||
|
|
if role:
|
||
|
|
role_cam[role] = str(cam_id)
|
||
|
|
img = item.get("image")
|
||
|
|
if role == "rgb":
|
||
|
|
rgb = img
|
||
|
|
elif role == "re":
|
||
|
|
re = img
|
||
|
|
elif role == "nir":
|
||
|
|
nir = img
|
||
|
|
if rgb is None or re is None or nir is None:
|
||
|
|
raise RuntimeError(f"Decoded sem rgb/re/nir completos. roles={role_cam}")
|
||
|
|
if rgb.ndim != 3 or rgb.shape[2] != 3:
|
||
|
|
raise RuntimeError(f"RGB nativo invalido: shape={rgb.shape}")
|
||
|
|
if re.ndim != 2 or nir.ndim != 2:
|
||
|
|
raise RuntimeError(f"RE/NIR nativos invalidos: re={re.shape} nir={nir.shape}")
|
||
|
|
return rgb.astype(np.float32), re.astype(np.float32), nir.astype(np.float32), role_cam
|
||
|
|
|
||
|
|
|
||
|
|
def compute_current_fusion_debug(core: Any, decoded: Dict[str, Any], processing_meta: Dict[str, Any]) -> FusionDebug:
|
||
|
|
try:
|
||
|
|
rgb, re, nir, _role_cam = role_to_images(decoded)
|
||
|
|
ref_h, ref_w = rgb.shape[:2]
|
||
|
|
ref_shape = (ref_h, ref_w)
|
||
|
|
|
||
|
|
warped_re, valid_re = core._warp_with_valid_mask(re, "re", ref_shape, processing_meta)
|
||
|
|
warped_nir, valid_nir = core._warp_with_valid_mask(nir, "nir", ref_shape, processing_meta)
|
||
|
|
|
||
|
|
common = np.ones((ref_h, ref_w), dtype=np.uint8)
|
||
|
|
common = np.logical_and(common > 0, valid_re > 0)
|
||
|
|
common = np.logical_and(common > 0, valid_nir > 0).astype(np.uint8)
|
||
|
|
|
||
|
|
crop_box = core._compute_common_crop_box([np.ones((ref_h, ref_w), dtype=np.uint8), valid_re, valid_nir])
|
||
|
|
if crop_box is not None:
|
||
|
|
crop_box = tuple(int(v) for v in crop_box)
|
||
|
|
|
||
|
|
return FusionDebug(
|
||
|
|
available=True,
|
||
|
|
warped_re=warped_re.astype(np.float32),
|
||
|
|
warped_nir=warped_nir.astype(np.float32),
|
||
|
|
valid_re=valid_re.astype(np.uint8),
|
||
|
|
valid_nir=valid_nir.astype(np.uint8),
|
||
|
|
common_mask=common.astype(np.uint8),
|
||
|
|
crop_box=crop_box,
|
||
|
|
note="homografia atual aplicada so para debug",
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
return FusionDebug(available=False, note=f"fusion_debug indisponivel: {str(e)[:160]}")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# Browser
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class BrowserState:
|
||
|
|
space: str = "final"
|
||
|
|
method: str = "phase"
|
||
|
|
priority: str = "global"
|
||
|
|
show_corrected: bool = False
|
||
|
|
force_apply: bool = False
|
||
|
|
show_edges: bool = True
|
||
|
|
show_mask: bool = True
|
||
|
|
show_crop_debug: bool = True
|
||
|
|
panel_w: int = 410
|
||
|
|
|
||
|
|
|
||
|
|
class DatasetAlignmentBrowserV2:
|
||
|
|
def __init__(self, args: argparse.Namespace):
|
||
|
|
ensure_imports_ok()
|
||
|
|
self.args = args
|
||
|
|
self.state = BrowserState(
|
||
|
|
space=args.space,
|
||
|
|
method=args.method,
|
||
|
|
priority=args.priority,
|
||
|
|
panel_w=args.panel_w,
|
||
|
|
show_crop_debug=not args.hide_crop_debug,
|
||
|
|
)
|
||
|
|
self.final_core_cache: Dict[Any, Any] = {}
|
||
|
|
self.native_decoder = NativeDecoder()
|
||
|
|
self.entries = self._build_entries(args.input_path, args.groups_except)
|
||
|
|
if not self.entries:
|
||
|
|
raise RuntimeError("Nenhuma amostra encontrada para navegar.")
|
||
|
|
self.index = max(0, min(args.start_index, len(self.entries) - 1))
|
||
|
|
self.window_name = "Dataset Alignment Browser V2"
|
||
|
|
self.save_dir = Path(args.save_dir) if args.save_dir else Path("alignment_browser_v2_out")
|
||
|
|
self.save_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
self.target_classes = [int(x) for x in str(args.target_classes).split(",") if x.strip()]
|
||
|
|
|
||
|
|
def _build_entries(self, input_path: str, groups_except: str) -> List[SampleEntry]:
|
||
|
|
roots = find_dataset_roots(Path(input_path))
|
||
|
|
skip = parse_csv_set(groups_except) if groups_except and parse_csv_set else set()
|
||
|
|
entries: List[SampleEntry] = []
|
||
|
|
for root in roots:
|
||
|
|
if root.name.lower() in skip:
|
||
|
|
continue
|
||
|
|
for meta_path in list_meta_files(root):
|
||
|
|
entries.append(SampleEntry(root, meta_path, f"{root.name}__{meta_path.stem}"))
|
||
|
|
return entries
|
||
|
|
|
||
|
|
def _load_mask_for_entry(self, entry: SampleEntry, hw: Tuple[int, int]) -> Optional[np.ndarray]:
|
||
|
|
try:
|
||
|
|
mask_path = resolve_mask_path(entry.dataset_root, entry.meta_path)
|
||
|
|
if mask_path is None:
|
||
|
|
return None
|
||
|
|
return load_mask(mask_path, hw, IGNORE_INDEX)
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def load_sample_final(self, entry: SampleEntry) -> Dict[str, Any]:
|
||
|
|
tensor, meta, _source_payload = load_multispec_tensor(entry.meta_path, entry.dataset_root, self.final_core_cache)
|
||
|
|
h, w = tensor.shape[1], tensor.shape[2]
|
||
|
|
mask = self._load_mask_for_entry(entry, (h, w))
|
||
|
|
|
||
|
|
rgb_bgr = rgb_from_tensor(tensor, stretch=False)
|
||
|
|
rgb_stretch_bgr = rgb_from_tensor(tensor, stretch=True)
|
||
|
|
rgb_gray = gray_from_tensor_rgb(tensor)
|
||
|
|
re = tensor[3].astype(np.float32, copy=False)
|
||
|
|
nir = tensor[4].astype(np.float32, copy=False)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"space": "final",
|
||
|
|
"entry": entry,
|
||
|
|
"meta": meta,
|
||
|
|
"mask": mask,
|
||
|
|
"rgb_bgr": rgb_bgr,
|
||
|
|
"rgb_stretch_bgr": rgb_stretch_bgr,
|
||
|
|
"rgb_gray": rgb_gray,
|
||
|
|
"re": re,
|
||
|
|
"nir": nir,
|
||
|
|
"native_note": "tensor final: pos homografia/crop/resize/flat/radnorm conforme pipeline",
|
||
|
|
"fusion_debug": None,
|
||
|
|
}
|
||
|
|
|
||
|
|
def load_sample_native(self, entry: SampleEntry) -> Dict[str, Any]:
|
||
|
|
native = self.native_decoder.load_native(entry)
|
||
|
|
rgb_hwc, re_native, nir_native, role_cam = role_to_images(native["decoded"])
|
||
|
|
|
||
|
|
# Para comparacao visual sem homografia, redimensiona RE/NIR para shape do RGB.
|
||
|
|
# Isso e apenas resize escalar, nao corrige paralaxe nem homografia.
|
||
|
|
ref_h, ref_w = rgb_hwc.shape[:2]
|
||
|
|
re_cmp = resize_to(re_native, (ref_h, ref_w))
|
||
|
|
nir_cmp = resize_to(nir_native, (ref_h, ref_w))
|
||
|
|
|
||
|
|
rgb_bgr = rgb_hwc_to_bgr(rgb_hwc, stretch=False)
|
||
|
|
rgb_stretch_bgr = rgb_hwc_to_bgr(rgb_hwc, stretch=True)
|
||
|
|
rgb_gray = gray_from_rgb_hwc(rgb_hwc)
|
||
|
|
|
||
|
|
mask = self._load_mask_for_entry(entry, (ref_h, ref_w))
|
||
|
|
fusion_debug = compute_current_fusion_debug(native["core"], native["decoded"], native["processing_meta"])
|
||
|
|
|
||
|
|
return {
|
||
|
|
"space": "native",
|
||
|
|
"entry": entry,
|
||
|
|
"meta": native["meta"],
|
||
|
|
"mask": mask,
|
||
|
|
"rgb_bgr": rgb_bgr,
|
||
|
|
"rgb_stretch_bgr": rgb_stretch_bgr,
|
||
|
|
"rgb_gray": rgb_gray,
|
||
|
|
"re": re_cmp.astype(np.float32),
|
||
|
|
"nir": nir_cmp.astype(np.float32),
|
||
|
|
"re_native_original": re_native.astype(np.float32),
|
||
|
|
"nir_native_original": nir_native.astype(np.float32),
|
||
|
|
"rgb_native_original": rgb_hwc.astype(np.float32),
|
||
|
|
"role_cam": role_cam,
|
||
|
|
"native_note": f"native decoded antes da fusao | RGB={rgb_hwc.shape[:2]} RE={re_native.shape} NIR={nir_native.shape} | overlay usa resize simples para RGB",
|
||
|
|
"fusion_debug": fusion_debug,
|
||
|
|
}
|
||
|
|
|
||
|
|
def load_current_sample(self) -> Dict[str, Any]:
|
||
|
|
entry = self.entries[self.index]
|
||
|
|
if self.state.space == "native":
|
||
|
|
sample = self.load_sample_native(entry)
|
||
|
|
elif self.state.space == "final":
|
||
|
|
sample = self.load_sample_final(entry)
|
||
|
|
else:
|
||
|
|
# fallback defensivo
|
||
|
|
sample = self.load_sample_final(entry)
|
||
|
|
|
||
|
|
re_align = estimate_alignment(
|
||
|
|
sample["rgb_gray"], sample["re"], self.state.method, self.state.priority,
|
||
|
|
sample["mask"] if self.state.show_mask else None, self.target_classes,
|
||
|
|
max_shift_px=self.args.max_shift_px,
|
||
|
|
max_rotation_deg=self.args.max_rotation_deg,
|
||
|
|
min_improve_corr=self.args.min_improve_corr,
|
||
|
|
)
|
||
|
|
nir_align = estimate_alignment(
|
||
|
|
sample["rgb_gray"], sample["nir"], self.state.method, self.state.priority,
|
||
|
|
sample["mask"] if self.state.show_mask else None, self.target_classes,
|
||
|
|
max_shift_px=self.args.max_shift_px,
|
||
|
|
max_rotation_deg=self.args.max_rotation_deg,
|
||
|
|
min_improve_corr=self.args.min_improve_corr,
|
||
|
|
)
|
||
|
|
|
||
|
|
apply_re = bool(re_align.accepted or self.state.force_apply)
|
||
|
|
apply_nir = bool(nir_align.accepted or self.state.force_apply)
|
||
|
|
re_corr = apply_warp(sample["re"], re_align.warp, inverse_map=re_align.used_inverse_map) if apply_re else sample["re"]
|
||
|
|
nir_corr = apply_warp(sample["nir"], nir_align.warp, inverse_map=nir_align.used_inverse_map) if apply_nir else sample["nir"]
|
||
|
|
|
||
|
|
sample["re_align"] = re_align
|
||
|
|
sample["nir_align"] = nir_align
|
||
|
|
sample["re_corr"] = re_corr
|
||
|
|
sample["nir_corr"] = nir_corr
|
||
|
|
return sample
|
||
|
|
|
||
|
|
def build_panels(self, sample: Dict[str, Any]) -> np.ndarray:
|
||
|
|
entry = sample["entry"]
|
||
|
|
idx_txt = f"[{self.index + 1}/{len(self.entries)}] {entry.sample_name}"
|
||
|
|
space_txt = self.state.space.upper()
|
||
|
|
if self.state.show_corrected and self.state.force_apply:
|
||
|
|
mode_txt = "PROPOSTO"
|
||
|
|
elif self.state.show_corrected:
|
||
|
|
mode_txt = "CORRIGIDO"
|
||
|
|
else:
|
||
|
|
mode_txt = "BRUTO"
|
||
|
|
|
||
|
|
rgb_bgr = sample["rgb_bgr"]
|
||
|
|
rgb_stretch = sample["rgb_stretch_bgr"]
|
||
|
|
rgb_gray = sample["rgb_gray"]
|
||
|
|
re_raw = sample["re"]
|
||
|
|
nir_raw = sample["nir"]
|
||
|
|
re = sample["re_corr"] if self.state.show_corrected else re_raw
|
||
|
|
nir = sample["nir_corr"] if self.state.show_corrected else nir_raw
|
||
|
|
mask = sample["mask"] if self.state.show_mask else None
|
||
|
|
re_align: AlignResult = sample["re_align"]
|
||
|
|
nir_align: AlignResult = sample["nir_align"]
|
||
|
|
|
||
|
|
panels: List[Tuple[str, np.ndarray, str]] = []
|
||
|
|
|
||
|
|
info_lines = [
|
||
|
|
idx_txt,
|
||
|
|
f"space={space_txt} | exibicao={mode_txt} | metodo={self.state.method} | prioridade={self.state.priority}",
|
||
|
|
"teclas: A/D prev/next | Up/Down +/-10 | X space | C raw/corr | F force/proposto | M metodo | P prioridade | V crop | E edges | K mask | S save | Q sair",
|
||
|
|
sample.get("native_note", ""),
|
||
|
|
f"RE global before/after={re_align.edge_corr_before:.4f}/{re_align.edge_corr_after:.4f} | ROI before/after={re_align.roi_corr_before:.4f}/{re_align.roi_corr_after:.4f}",
|
||
|
|
f"RE phase=({re_align.phase_dx:.2f},{re_align.phase_dy:.2f}) resp={re_align.phase_response:.3f} | accepted={re_align.accepted} | force={self.state.force_apply} | trans={re_align.translation_px:.2f}px rot={re_align.rotation_deg:.2f}deg",
|
||
|
|
f"NIR global before/after={nir_align.edge_corr_before:.4f}/{nir_align.edge_corr_after:.4f} | ROI before/after={nir_align.roi_corr_before:.4f}/{nir_align.roi_corr_after:.4f}",
|
||
|
|
f"NIR phase=({nir_align.phase_dx:.2f},{nir_align.phase_dy:.2f}) resp={nir_align.phase_response:.3f} | accepted={nir_align.accepted} | force={self.state.force_apply} | trans={nir_align.translation_px:.2f}px rot={nir_align.rotation_deg:.2f}deg",
|
||
|
|
f"RE note: {re_align.note}",
|
||
|
|
f"NIR note: {nir_align.note}",
|
||
|
|
]
|
||
|
|
info_panel = make_info_panel(info_lines, size=(950, 305))
|
||
|
|
|
||
|
|
mask_on_rgb = mask_overlay(rgb_bgr, mask)
|
||
|
|
roi_re_panel = draw_roi(rgb_bgr, re_align.roi_mask, re_align.roi_bbox, "ROI RE")
|
||
|
|
roi_nir_panel = draw_roi(rgb_bgr, nir_align.roi_mask, nir_align.roi_bbox, "ROI NIR")
|
||
|
|
|
||
|
|
panels.extend([
|
||
|
|
("RGB", rgb_bgr, idx_txt),
|
||
|
|
("RGB stretch", rgb_stretch, "visual somente"),
|
||
|
|
("Mask overlay", mask_on_rgb, "GT over RGB" if mask is not None else "sem mascara"),
|
||
|
|
("RE bruto", colorize_gray(re_raw), f"space={space_txt}"),
|
||
|
|
("NIR bruto", colorize_gray(nir_raw), f"space={space_txt}"),
|
||
|
|
("Info", info_panel, "metricas de alinhamento"),
|
||
|
|
("ROI usado RE", roi_re_panel, f"priority={self.state.priority}"),
|
||
|
|
("ROI usado NIR", roi_nir_panel, f"priority={self.state.priority}"),
|
||
|
|
(f"RE exibido {mode_txt}", colorize_gray(re), "bruto ou corrigido"),
|
||
|
|
])
|
||
|
|
|
||
|
|
panels.extend([
|
||
|
|
(f"RGBgray vs RE ({mode_txt})", falsecolor_overlay(rgb_gray, re), "verde=RGBgray magenta=RE"),
|
||
|
|
(f"RGBgray vs NIR ({mode_txt})", falsecolor_overlay(rgb_gray, nir), "verde=RGBgray magenta=NIR"),
|
||
|
|
(f"RE vs NIR ({mode_txt})", falsecolor_overlay(re, nir), "verde=RE magenta=NIR"),
|
||
|
|
(f"RGB + RE tint ({mode_txt})", alpha_blend(rgb_bgr, re, alpha=0.38, cmap=cv2.COLORMAP_INFERNO), "RGB com RE"),
|
||
|
|
(f"RGB + NIR tint ({mode_txt})", alpha_blend(rgb_bgr, nir, alpha=0.38, cmap=cv2.COLORMAP_VIRIDIS), "RGB com NIR"),
|
||
|
|
(f"RGB + RE edges ({mode_txt})", draw_edges_on_rgb(rgb_bgr, re, (0, 0, 255)), "bordas RE sobre RGB"),
|
||
|
|
])
|
||
|
|
|
||
|
|
if self.state.show_edges:
|
||
|
|
panels.extend([
|
||
|
|
("Edge overlay bruto", edge_overlay(rgb_gray, re_raw, nir_raw), "G=RGB | R=RE | B=NIR"),
|
||
|
|
(f"Edge overlay {mode_txt}", edge_overlay(rgb_gray, re, nir), "G=RGB | R=RE | B=NIR"),
|
||
|
|
(f"RGB + NIR edges ({mode_txt})", draw_edges_on_rgb(rgb_bgr, nir, (255, 255, 0)), "bordas NIR sobre RGB"),
|
||
|
|
])
|
||
|
|
|
||
|
|
# Debug de homografia/crop atual no modo native.
|
||
|
|
fusion_debug: Optional[FusionDebug] = sample.get("fusion_debug")
|
||
|
|
if self.state.show_crop_debug and sample.get("space") == "native":
|
||
|
|
if fusion_debug and fusion_debug.available:
|
||
|
|
warped_re = resize_to(fusion_debug.warped_re, rgb_bgr.shape[:2])
|
||
|
|
warped_nir = resize_to(fusion_debug.warped_nir, rgb_bgr.shape[:2])
|
||
|
|
common = fusion_debug.common_mask.astype(np.uint8) * 255
|
||
|
|
common_bgr = cv2.cvtColor(common, cv2.COLOR_GRAY2BGR)
|
||
|
|
crop_rgb = draw_crop_box(rgb_bgr, fusion_debug.crop_box, "crop comum atual")
|
||
|
|
panels.extend([
|
||
|
|
("Debug homografia atual RE", falsecolor_overlay(rgb_gray, warped_re), "verde=RGBgray magenta=RE warp atual"),
|
||
|
|
("Debug homografia atual NIR", falsecolor_overlay(rgb_gray, warped_nir), "verde=RGBgray magenta=NIR warp atual"),
|
||
|
|
("Mascara valida comum", common_bgr, f"crop={fusion_debug.crop_box}"),
|
||
|
|
("Crop comum atual", crop_rgb, "area que vira tensor final"),
|
||
|
|
("Warp atual RE vs NIR", falsecolor_overlay(warped_re, warped_nir), "verde=RE magenta=NIR apos H atual"),
|
||
|
|
("Edge H atual", edge_overlay(rgb_gray, warped_re, warped_nir), "G=RGB | R=RE | B=NIR"),
|
||
|
|
])
|
||
|
|
else:
|
||
|
|
note = fusion_debug.note if fusion_debug else "sem fusion debug"
|
||
|
|
panels.append(("Crop debug indisponivel", make_info_panel([note], size=(900, 260)), ""))
|
||
|
|
|
||
|
|
canvas = make_grid(panels, panel_w=self.state.panel_w, cols=3)
|
||
|
|
footer_h = 34
|
||
|
|
footer = np.full((footer_h, canvas.shape[1], 3), 18, dtype=np.uint8)
|
||
|
|
footer_text = (
|
||
|
|
f"sample {self.index+1}/{len(self.entries)} | space={space_txt} | exibicao={mode_txt} | force={self.state.force_apply} | metodo={self.state.method} | prioridade={self.state.priority} | "
|
||
|
|
f"RE g={re_align.edge_corr_before:.3f}->{re_align.edge_corr_after:.3f} roi={re_align.roi_corr_before:.3f}->{re_align.roi_corr_after:.3f} | "
|
||
|
|
f"NIR g={nir_align.edge_corr_before:.3f}->{nir_align.edge_corr_after:.3f} roi={nir_align.roi_corr_before:.3f}->{nir_align.roi_corr_after:.3f}"
|
||
|
|
)
|
||
|
|
cv2.putText(footer, cv_text(footer_text[:260]), (10, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (220, 220, 220), 1, cv2.LINE_AA)
|
||
|
|
return np.vstack([canvas, footer])
|
||
|
|
|
||
|
|
def save_current(self, canvas: np.ndarray, entry: SampleEntry):
|
||
|
|
mode_name = 'proposto' if (self.state.show_corrected and self.state.force_apply) else ('corr' if self.state.show_corrected else 'raw')
|
||
|
|
fname = f"{entry.sample_name}__space-{self.state.space}__{self.state.method}__{self.state.priority}__{mode_name}.png"
|
||
|
|
out_path = self.save_dir / fname
|
||
|
|
cv2.imwrite(str(out_path), canvas)
|
||
|
|
print(f"[OK] painel salvo: {out_path}")
|
||
|
|
|
||
|
|
def next_method(self):
|
||
|
|
methods = ["phase", "ecc_translation", "ecc_euclidean", "ecc_affine", "none"]
|
||
|
|
cur = methods.index(self.state.method) if self.state.method in methods else 0
|
||
|
|
self.state.method = methods[(cur + 1) % len(methods)]
|
||
|
|
|
||
|
|
def next_priority(self):
|
||
|
|
priorities = ["global", "largest_blob", "gt_target"]
|
||
|
|
cur = priorities.index(self.state.priority) if self.state.priority in priorities else 0
|
||
|
|
self.state.priority = priorities[(cur + 1) % len(priorities)]
|
||
|
|
|
||
|
|
def next_space(self):
|
||
|
|
spaces = ["final", "native"]
|
||
|
|
cur = spaces.index(self.state.space) if self.state.space in spaces else 0
|
||
|
|
self.state.space = spaces[(cur + 1) % len(spaces)]
|
||
|
|
|
||
|
|
def run(self):
|
||
|
|
cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL)
|
||
|
|
cv2.resizeWindow(self.window_name, 1640, 980)
|
||
|
|
|
||
|
|
while True:
|
||
|
|
entry = self.entries[self.index]
|
||
|
|
try:
|
||
|
|
sample = self.load_current_sample()
|
||
|
|
canvas = self.build_panels(sample)
|
||
|
|
except Exception as e:
|
||
|
|
canvas = make_info_panel([
|
||
|
|
f"Erro ao carregar sample {self.index+1}/{len(self.entries)}",
|
||
|
|
str(entry.meta_path),
|
||
|
|
str(e),
|
||
|
|
"Use A/D para navegar, Q para sair.",
|
||
|
|
], size=(1100, 420))
|
||
|
|
print(f"[ERRO] {entry.sample_name}: {e}")
|
||
|
|
|
||
|
|
cv2.imshow(self.window_name, canvas)
|
||
|
|
key = cv2.waitKeyEx(0)
|
||
|
|
|
||
|
|
if key in (27, ord('q'), ord('Q')):
|
||
|
|
break
|
||
|
|
elif key in (ord('d'), ord('D'), 2555904):
|
||
|
|
self.index = min(self.index + 1, len(self.entries) - 1)
|
||
|
|
elif key in (ord('a'), ord('A'), 2424832):
|
||
|
|
self.index = max(self.index - 1, 0)
|
||
|
|
elif key == 2490368:
|
||
|
|
self.index = max(self.index - 10, 0)
|
||
|
|
elif key == 2621440:
|
||
|
|
self.index = min(self.index + 10, len(self.entries) - 1)
|
||
|
|
elif key in (ord('c'), ord('C')):
|
||
|
|
self.state.show_corrected = not self.state.show_corrected
|
||
|
|
elif key in (ord('f'), ord('F')):
|
||
|
|
self.state.force_apply = not self.state.force_apply
|
||
|
|
if self.state.force_apply:
|
||
|
|
self.state.show_corrected = True
|
||
|
|
elif key in (ord('x'), ord('X')):
|
||
|
|
self.next_space()
|
||
|
|
elif key in (ord('m'), ord('M')):
|
||
|
|
self.next_method()
|
||
|
|
elif key in (ord('p'), ord('P')):
|
||
|
|
self.next_priority()
|
||
|
|
elif key in (ord('e'), ord('E')):
|
||
|
|
self.state.show_edges = not self.state.show_edges
|
||
|
|
elif key in (ord('k'), ord('K')):
|
||
|
|
self.state.show_mask = not self.state.show_mask
|
||
|
|
elif key in (ord('v'), ord('V')):
|
||
|
|
self.state.show_crop_debug = not self.state.show_crop_debug
|
||
|
|
elif key in (ord('s'), ord('S')):
|
||
|
|
self.save_current(canvas, entry)
|
||
|
|
elif key in (ord('h'), ord('H')):
|
||
|
|
print("\n=== HELP V2 ===")
|
||
|
|
print("A / Left : amostra anterior")
|
||
|
|
print("D / Right : proxima amostra")
|
||
|
|
print("Up / Down : pula -10 / +10")
|
||
|
|
print("X : alterna space final/native")
|
||
|
|
print("C : alterna bruto/corrigido")
|
||
|
|
print("F : forca aplicar warp proposto mesmo se rejeitado")
|
||
|
|
print("M : alterna metodo phase/ecc_translation/ecc_euclidean/ecc_affine/none")
|
||
|
|
print("P : alterna prioridade global/largest_blob/gt_target")
|
||
|
|
print("V : mostra/esconde debug de homografia/crop atual")
|
||
|
|
print("E : alterna paineis de borda")
|
||
|
|
print("K : mostra/esconde mascara")
|
||
|
|
print("S : salva painel atual")
|
||
|
|
print("Q / Esc : sair")
|
||
|
|
print("==============\n")
|
||
|
|
|
||
|
|
cv2.destroyAllWindows()
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================
|
||
|
|
# CLI
|
||
|
|
# ============================================================
|
||
|
|
|
||
|
|
|
||
|
|
def build_argparser() -> argparse.ArgumentParser:
|
||
|
|
ap = argparse.ArgumentParser(
|
||
|
|
description="Navegador visual V2 para comparar alinhamento nativo/final RGB/RE/NIR e testar correcao por bordas."
|
||
|
|
)
|
||
|
|
ap.add_argument("--input_path", type=str, required=True, help="Raiz do dataset ou super-root com grupos.")
|
||
|
|
ap.add_argument("--groups-except", type=str, default="", help="Ex: chao ou chao,chao_cana")
|
||
|
|
ap.add_argument("--start-index", type=int, default=0, help="Indice inicial para navegacao.")
|
||
|
|
ap.add_argument("--space", type=str, default="native", choices=["native", "final"], help="Espaco inicial de visualizacao.")
|
||
|
|
ap.add_argument("--method", type=str, default="phase", choices=["phase", "ecc_translation", "ecc_euclidean", "ecc_affine", "none"], help="Metodo inicial de correcao dinamica.")
|
||
|
|
ap.add_argument("--priority", type=str, default="global", choices=["global", "largest_blob", "gt_target"], help="Prioridade inicial para ROI do alinhamento.")
|
||
|
|
ap.add_argument("--target-classes", type=str, default="1,2", help="Classes usadas no modo gt_target. Padrao: 1,2 = cana,erva")
|
||
|
|
ap.add_argument("--panel-w", type=int, default=410, help="Largura de cada painel no grid.")
|
||
|
|
ap.add_argument("--max-shift-px", type=float, default=35.0, help="Limite de translacao aceito para a correcao dinamica.")
|
||
|
|
ap.add_argument("--max-rotation-deg", type=float, default=3.0, help="Limite de rotacao aceito para ECC euclidean/affine.")
|
||
|
|
ap.add_argument("--min-improve-corr", type=float, default=-0.003, help="Melhoria minima aceitavel na correlacao. Negativo leve permite correcao equivalente.")
|
||
|
|
ap.add_argument("--hide-crop-debug", action="store_true", help="Esconde paineis de debug da homografia/crop atual no modo native.")
|
||
|
|
ap.add_argument("--save-dir", type=str, default="alignment_browser_v2_out", help="Pasta para salvar paineis com tecla S.")
|
||
|
|
return ap
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
args = build_argparser().parse_args()
|
||
|
|
browser = DatasetAlignmentBrowserV2(args)
|
||
|
|
browser.run()
|