diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/EXEMPLO_ESTRUTURA/labelmap.txt.COLOQUE_AQUI b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/EXEMPLO_ESTRUTURA/labelmap.txt.COLOQUE_AQUI new file mode 100644 index 000000000..313ee5b9d --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/EXEMPLO_ESTRUTURA/labelmap.txt.COLOQUE_AQUI @@ -0,0 +1 @@ +Coloque seu labelmap.txt real ao lado do EXE. diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.py b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.py new file mode 100644 index 000000000..e376cba04 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.py @@ -0,0 +1,1449 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MaskReviewer.py +=============== + +Ferramenta standalone para revisão humana de máscaras sem inferência/modelo. + +Estrutura preferida ao lado do EXE: + MaskReviewer.exe + labelmap.txt + group/ + chao/ + previews/ + masks/ + predictions/ + final_masks/ + review_order.csv + chao_cana/ + chao_cana_erva/ + chao_erva/ + ... + +Se "group" e/ou "labelmap.txt" não forem encontrados automaticamente, +abre seletor do Windows para o usuário apontá-los. + +Decisões da sessão: + group/review_choices.csv + +Controles principais: + 1 = manter máscara HUMANA + 2 = escolher máscara do MODELO + 3 = nenhuma serve, marcar REMAP + E = editar + A / D = anterior / próxima + S = pular + V = mostrar/ocultar a final_mask salva + X = apagar decisão atual + Q / ESC = sair + +Overlay: + Trackbar "Overlay %" controla a mesma opacidade nos dois lados. + +Editor: + Ao pressionar E: + 1 = editar HUMANA como base + 2 = editar MODELO como base + 3 = começar de CHÃO, visualmente transparente + + Mouse esquerdo = adiciona ponto + Mouse direito = remove último ponto aberto + ENTER = fecha/preenche polígono com a classe selecionada + C = próxima classe + 0..9 = seleciona ID de classe diretamente + Z = desfaz último polígono confirmado + R = restaura máscara base + [ / ] = diminui / aumenta o pincel + + / - ou roda do mouse = zoom + Setas ou arrasto do botão do meio = pan + F = fit/visão completa + G = centraliza no último ponto sob o cursor + M = liga/desliga o mini mapa + Clique no mini mapa = centraliza a visão + Arrasto com o botão do meio sobre o mini mapa = move o mini mapa + BACKSPACE / DEL = limpa pontos abertos + S = salva edição em final_masks + ESC / Q = cancela edição + +O programa NUNCA altera o dataset bruto. +""" + +from __future__ import annotations + +import argparse +import csv +import json +import shutil +import sys +import time +import traceback +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import cv2 +import numpy as np + + +APP_NAME = "MaskReviewer" +SETTINGS_NAME = "MaskReviewer.settings.json" +CHOICES_NAME = "review_choices.csv" + + +# ============================================================ +# Estruturas +# ============================================================ + +@dataclass +class ReviewItem: + group: str + base: str + preview: Path + human_mask: Path + model_mask: Path + final_mask: Path + rank: Optional[int] = None + suspicion_pct: Optional[float] = None + reasons: str = "" + + +# ============================================================ +# Paths / dialogs +# ============================================================ + +def app_dir() -> Path: + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve().parent + return Path(__file__).resolve().parent + + +def ensure_dir(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + + +def load_settings(path: Path) -> dict: + try: + if path.is_file(): + with path.open("r", encoding="utf-8") as f: + js = json.load(f) + return js if isinstance(js, dict) else {} + except Exception: + pass + return {} + + +def save_settings(path: Path, settings: dict) -> None: + try: + with path.open("w", encoding="utf-8") as f: + json.dump(settings, f, ensure_ascii=False, indent=2) + except Exception: + # Preferimos não impedir a revisão por uma falha de persistência. + pass + + +def _tk_root(): + import tkinter as tk + root = tk.Tk() + root.withdraw() + root.attributes("-topmost", True) + return root + + +def choose_group_dialog(initial: Optional[Path] = None) -> Optional[Path]: + from tkinter import filedialog + root = _tk_root() + try: + selected = filedialog.askdirectory( + parent=root, + title="Selecione a pasta GROUP da revisão", + initialdir=str(initial) if initial and initial.exists() else str(app_dir()), + mustexist=True, + ) + return Path(selected).resolve() if selected else None + finally: + root.destroy() + + +def choose_labelmap_dialog(initial: Optional[Path] = None) -> Optional[Path]: + from tkinter import filedialog + root = _tk_root() + try: + selected = filedialog.askopenfilename( + parent=root, + title="Selecione o labelmap.txt", + initialdir=str(initial) if initial and initial.exists() else str(app_dir()), + filetypes=[("Labelmap", "labelmap.txt"), ("Arquivos TXT", "*.txt"), ("Todos", "*.*")], + ) + return Path(selected).resolve() if selected else None + finally: + root.destroy() + + +def show_error_dialog(message: str) -> None: + try: + from tkinter import messagebox + root = _tk_root() + try: + messagebox.showerror(APP_NAME, message, parent=root) + finally: + root.destroy() + except Exception: + print(message) + + +def resolve_group_root(cli_group: Optional[str], settings: dict) -> Path: + base = app_dir() + + candidates: List[Path] = [] + if cli_group: + candidates.append(Path(cli_group)) + saved = settings.get("group_root") + if saved: + candidates.append(Path(str(saved))) + + candidates += [ + base / "group", + base / "dataset" / "revisao" / "group", + base / "revisao" / "group", + ] + + for c in candidates: + try: + p = c.expanduser().resolve() + if p.is_dir(): + return p + except Exception: + continue + + selected = choose_group_dialog(base) + if selected is None: + raise RuntimeError("Nenhuma pasta group foi selecionada.") + return selected + + +def resolve_labelmap(cli_labelmap: Optional[str], group_root: Path, settings: dict) -> Path: + base = app_dir() + + candidates: List[Path] = [] + if cli_labelmap: + candidates.append(Path(cli_labelmap)) + saved = settings.get("labelmap") + if saved: + candidates.append(Path(str(saved))) + + # Pacote portátil: EXE + labelmap.txt + group/ + candidates.append(base / "labelmap.txt") + + # Estrutura do projeto: dataset/revisao/group -> dataset/labelmap.txt + candidates += [ + group_root.parent / "labelmap.txt", + group_root.parent.parent / "labelmap.txt", + base / "dataset" / "labelmap.txt", + ] + + for c in candidates: + try: + p = c.expanduser().resolve() + if p.is_file(): + return p + except Exception: + continue + + selected = choose_labelmap_dialog(group_root.parent) + if selected is None: + raise RuntimeError("Nenhum labelmap.txt foi selecionado.") + return selected + + +# ============================================================ +# CSV / coleta +# ============================================================ + +def read_csv_rows(path: Path) -> List[dict]: + if not path.is_file(): + return [] + with path.open("r", newline="", encoding="utf-8-sig") as f: + return list(csv.DictReader(f)) + + +def write_choices(path: Path, choices: Dict[Tuple[str, str], dict]) -> None: + ensure_dir(path.parent) + fieldnames = [ + "group", + "base", + "choice", + "source_mask", + "edit_base", + "final_mask", + "reviewed_at", + "suspicion_pct", + "review_reasons", + ] + rows = sorted(choices.values(), key=lambda r: (str(r["group"]), str(r["base"]))) + with path.open("w", newline="", encoding="utf-8-sig") as f: + w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore") + w.writeheader() + w.writerows(rows) + + +def load_choices(path: Path) -> Dict[Tuple[str, str], dict]: + out: Dict[Tuple[str, str], dict] = {} + for row in read_csv_rows(path): + key = (str(row.get("group", "")), str(row.get("base", ""))) + if all(key): + out[key] = row + return out + + +def load_order(group_dir: Path) -> Dict[str, dict]: + path = group_dir / "review_order.csv" + out: Dict[str, dict] = {} + for row in read_csv_rows(path): + base = str(row.get("base", "")) + if base: + out[base] = row + return out + + +def collect_items(group_root: Path, groups: Optional[List[str]]) -> List[ReviewItem]: + if not group_root.is_dir(): + raise FileNotFoundError(f"Pasta GROUP não encontrada: {group_root}") + + allowed = set(groups or []) + items: List[ReviewItem] = [] + + for group_dir in sorted(p for p in group_root.iterdir() if p.is_dir()): + group = group_dir.name + if allowed and group not in allowed: + continue + + previews_dir = group_dir / "previews" + masks_dir = group_dir / "masks" + preds_dir = group_dir / "predictions" + final_dir = group_dir / "final_masks" + ensure_dir(final_dir) + + if not masks_dir.is_dir() or not preds_dir.is_dir() or not previews_dir.is_dir(): + continue + + order = load_order(group_dir) + + # O auditor standalone normaliza tudo para PNG com mesmo basename. + bases = sorted( + p.stem + for p in masks_dir.glob("*.png") + if (preds_dir / p.name).is_file() and (previews_dir / p.name).is_file() + ) + + def sort_key(base: str): + row = order.get(base, {}) + try: + rank = int(float(row.get("review_rank_group", ""))) + except Exception: + rank = 10**9 + return (rank, base) + + bases.sort(key=sort_key) + + for base in bases: + row = order.get(base, {}) + try: + rank = int(float(row.get("review_rank_group", ""))) + except Exception: + rank = None + try: + suspicion = float(row.get("suspicion_pct", "")) + except Exception: + suspicion = None + + items.append( + ReviewItem( + group=group, + base=base, + preview=previews_dir / f"{base}.png", + human_mask=masks_dir / f"{base}.png", + model_mask=preds_dir / f"{base}.png", + final_mask=final_dir / f"{base}.png", + rank=rank, + suspicion_pct=suspicion, + reasons=str(row.get("review_reasons", "")), + ) + ) + + return items + + +# ============================================================ +# Imagens +# ============================================================ + +def imread_required(path: Path) -> np.ndarray: + """ + Leitura robusta de imagens, especialmente para Windows + PyInstaller. + + 1) Tenta cv2.imread normalmente. + 2) Se falhar, lê os bytes com NumPy e usa cv2.imdecode. + 3) Repete algumas vezes para tolerar falhas transitórias de I/O. + + Não altera nem regrava a imagem. + """ + path = Path(path) + + if not path.is_file(): + raise RuntimeError(f"Arquivo nao existe: {path}") + + errors = [] + + for attempt in range(3): + try: + img = cv2.imread(str(path), cv2.IMREAD_COLOR) + if img is not None and img.size > 0: + return img + errors.append(f"tentativa {attempt + 1}: cv2.imread retornou None") + except Exception as exc: + errors.append(f"tentativa {attempt + 1}: cv2.imread: {exc}") + + try: + raw = np.fromfile(str(path), dtype=np.uint8) + if raw.size > 0: + img = cv2.imdecode(raw, cv2.IMREAD_COLOR) + if img is not None and img.size > 0: + return img + errors.append(f"tentativa {attempt + 1}: cv2.imdecode retornou None") + except Exception as exc: + errors.append(f"tentativa {attempt + 1}: cv2.imdecode: {exc}") + + time.sleep(0.05 * (attempt + 1)) + + try: + file_size = path.stat().st_size + except Exception: + file_size = -1 + + raise RuntimeError( + "Falha ao abrir imagem apos leitura robusta:\n" + f"{path}\n" + f"Tamanho do arquivo: {file_size} bytes\n" + + "\n".join(errors) + ) + + +def fit_to(img: np.ndarray, wh: Tuple[int, int], interpolation: int) -> np.ndarray: + w, h = wh + if img.shape[1] == w and img.shape[0] == h: + return img + return cv2.resize(img, (w, h), interpolation=interpolation) + + +def add_label(img: np.ndarray, title: str, subtitle: str = "") -> np.ndarray: + out = img.copy() + h, w = out.shape[:2] + bar_h = max(52, int(h * 0.085)) + overlay = out.copy() + cv2.rectangle(overlay, (0, 0), (w, bar_h), (0, 0, 0), -1) + out = cv2.addWeighted(overlay, 0.72, out, 0.28, 0) + cv2.putText(out, title, (14, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 2, cv2.LINE_AA) + if subtitle: + cv2.putText(out, subtitle, (14, 46), cv2.FONT_HERSHEY_SIMPLEX, 0.46, (220, 220, 220), 1, cv2.LINE_AA) + return out + + +def now_iso() -> str: + return datetime.now().astimezone().isoformat(timespec="seconds") + + +# ============================================================ +# Labelmap +# ============================================================ + +FALLBACK_CLASSES_RGB = { + 0: ("chao", (85, 85, 85)), + 1: ("cana", (0, 190, 0)), + 2: ("erva", (230, 55, 55)), +} + + +def load_labelmap_classes(path: Path) -> Dict[int, Tuple[str, Tuple[int, int, int]]]: + classes: Dict[int, Tuple[str, Tuple[int, int, int]]] = {} + + if not path.is_file(): + raise FileNotFoundError(path) + + next_id = 0 + with path.open("r", encoding="utf-8") as f: + for raw_line in f: + s = raw_line.strip() + if not s or s.startswith("#"): + continue + + cid = None + name = None + color = None + + if ":" in s and not s.split(":", 1)[0].strip().isdigit(): + name_part, rest = s.split(":", 1) + name = name_part.strip() + color_txt = rest.split("::", 1)[0].strip().strip(":") + parts = [p.strip() for p in color_txt.split(",") if p.strip()] + if len(parts) >= 3: + try: + color = tuple(int(float(x)) for x in parts[:3]) + except Exception: + color = None + else: + parts = s.replace(",", " ").replace(":", " ").split() + if len(parts) >= 2 and parts[0].isdigit(): + cid = int(parts[0]) + name = parts[1] + if len(parts) >= 5: + try: + color = tuple(int(float(x)) for x in parts[2:5]) + except Exception: + color = None + elif parts: + name = parts[0] + + if not name: + continue + + lname = name.lower() + if lname in ("ignore", "void", "background_ignore"): + continue + + if cid is None: + cid = next_id + next_id = max(next_id, cid + 1) + + if color is None: + color = FALLBACK_CLASSES_RGB.get(cid, (name, (255, 255, 255)))[1] + + classes[int(cid)] = (str(name), tuple(map(int, color))) + + if not classes: + raise RuntimeError(f"Labelmap não contém classes utilizáveis: {path}") + + return classes + + +def class_color_bgr(classes, cid: int) -> Tuple[int, int, int]: + _name, rgb = classes[cid] + return int(rgb[2]), int(rgb[1]), int(rgb[0]) + + +def find_chao_id(classes) -> int: + for cid, (name, _rgb) in classes.items(): + if str(name).strip().lower() in ("chao", "chão", "ground", "solo"): + return int(cid) + return min(classes.keys()) + + +def colored_mask_to_ids(mask_bgr: np.ndarray, classes) -> np.ndarray: + h, w = mask_bgr.shape[:2] + ids = np.full((h, w), 255, dtype=np.uint8) + + for cid in classes: + bgr = np.array(class_color_bgr(classes, cid), dtype=np.uint8) + match = np.all(mask_bgr == bgr[None, None, :], axis=2) + ids[match] = int(cid) + + unmatched = int((ids == 255).sum()) + if unmatched: + raise RuntimeError( + f"A máscara possui {unmatched} pixels com cores que não existem no labelmap. " + "Confira se o labelmap.txt selecionado pertence a este dataset." + ) + return ids + + +def ids_to_colored_mask(ids: np.ndarray, classes) -> np.ndarray: + h, w = ids.shape[:2] + out = np.zeros((h, w, 3), dtype=np.uint8) + for cid in classes: + out[ids == cid] = class_color_bgr(classes, cid) + return out + + +# ============================================================ +# Painel principal +# ============================================================ + +def compose_panel( + item: ReviewItem, + choice: Optional[str], + index: int, + total: int, + alpha: float, + max_width: int, +) -> np.ndarray: + preview = imread_required(item.preview) + human = imread_required(item.human_mask) + model = imread_required(item.model_mask) + + h, w = preview.shape[:2] + human = fit_to(human, (w, h), cv2.INTER_NEAREST) + model = fit_to(model, (w, h), cv2.INTER_NEAREST) + + overlay_human = cv2.addWeighted(preview, 1.0 - alpha, human, alpha, 0.0) + overlay_model = cv2.addWeighted(preview, 1.0 - alpha, model, alpha, 0.0) + + sus = f"{item.suspicion_pct:.1f}/100" if item.suspicion_pct is not None else "n/a" + rank = str(item.rank) if item.rank is not None else "n/a" + common = f"{item.group}/{item.base} | suspeita={sus} | rank={rank}" + + tl = add_label(human, "1 MASCARA HUMANA", common) + tr = add_label(model, "2 MASCARA MODELO", common) + bl = add_label(overlay_human, "OVERLAY HUMANA", f"alpha={alpha:.2f}") + br = add_label(overlay_model, "OVERLAY MODELO", f"alpha={alpha:.2f}") + + top = np.hstack([tl, tr]) + bottom = np.hstack([bl, br]) + canvas = np.vstack([top, bottom]) + + footer_h = 92 + footer = np.zeros((footer_h, canvas.shape[1], 3), dtype=np.uint8) + status = choice or "NAO REVISADO" + final_exists = item.final_mask.is_file() + final_status = f"FINAL_MASK={'SIM' if final_exists else 'NAO'}" + line1 = f"[{index + 1}/{total}] escolha atual: {status} | {final_status}" + line2 = "1=humana 2=modelo 3=REMAP E=editar V=ver final A/D=navegar S=pular X=apagar Q=sair" + line3 = "Se a imagem ja foi revisada/ editada, aperte V para ver exatamente o PNG salvo em final_masks." + cv2.putText(footer, line1, (14, 27), cv2.FONT_HERSHEY_SIMPLEX, 0.60, (255, 255, 255), 2, cv2.LINE_AA) + cv2.putText(footer, line2, (14, 57), cv2.FONT_HERSHEY_SIMPLEX, 0.48, (210, 210, 210), 1, cv2.LINE_AA) + cv2.putText(footer, line3, (14, 84), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (170, 230, 170), 1, cv2.LINE_AA) + canvas = np.vstack([canvas, footer]) + + if max_width > 0 and canvas.shape[1] > max_width: + scale = max_width / float(canvas.shape[1]) + canvas = cv2.resize( + canvas, + (max_width, max(1, int(round(canvas.shape[0] * scale)))), + interpolation=cv2.INTER_AREA, + ) + + return canvas + + +def compose_final_panel( + item: ReviewItem, + choice: Optional[str], + alpha: float, + max_width: int, +) -> np.ndarray: + preview = imread_required(item.preview) + h, w = preview.shape[:2] + + if item.final_mask.is_file(): + final_mask = fit_to(imread_required(item.final_mask), (w, h), cv2.INTER_NEAREST) + final_overlay = cv2.addWeighted(preview, 1.0 - alpha, final_mask, alpha, 0.0) + left = add_label(final_mask, "FINAL_MASK SALVA", f"{item.group}/{item.base}") + right = add_label(final_overlay, "OVERLAY FINAL", f"choice={choice or 'NAO REVISADO'} | alpha={alpha:.2f}") + else: + blank = preview.copy() + dark = np.zeros_like(blank) + blank = cv2.addWeighted(blank, 0.35, dark, 0.65, 0.0) + cv2.putText(blank, "SEM FINAL_MASK SALVA", (30, max(60, h // 2 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2, cv2.LINE_AA) + cv2.putText(blank, "Escolha 1, 2 ou edite antes de visualizar.", (30, max(95, h // 2 + 28)), cv2.FONT_HERSHEY_SIMPLEX, 0.62, (210, 210, 210), 1, cv2.LINE_AA) + left = add_label(blank, "FINAL_MASK SALVA", f"{item.group}/{item.base}") + right = add_label(blank, "OVERLAY FINAL", f"choice={choice or 'NAO REVISADO'}") + + canvas = np.hstack([left, right]) + footer_h = 56 + footer = np.zeros((footer_h, canvas.shape[1], 3), dtype=np.uint8) + cv2.putText(footer, "Janela auxiliar da mascara final | V = mostrar/ocultar | mostra exatamente o arquivo salvo em final_masks", (14, 34), cv2.FONT_HERSHEY_SIMPLEX, 0.48, (230, 230, 230), 1, cv2.LINE_AA) + canvas = np.vstack([canvas, footer]) + + if max_width > 0 and canvas.shape[1] > max_width: + scale = max_width / float(canvas.shape[1]) + canvas = cv2.resize( + canvas, + (max_width, max(1, int(round(canvas.shape[0] * scale)))), + interpolation=cv2.INTER_AREA, + ) + + return canvas + + +# ============================================================ +# Decisões +# ============================================================ + +def record_choice( + item: ReviewItem, + choice_name: str, + source: Optional[Path], + choices: Dict[Tuple[str, str], dict], + choices_csv: Path, + edit_base: str = "", +) -> None: + key = (item.group, item.base) + + if choice_name in {"HUMAN", "MODEL"}: + if source is None or not source.is_file(): + raise FileNotFoundError(f"Máscara fonte ausente: {source}") + ensure_dir(item.final_mask.parent) + shutil.copy2(source, item.final_mask) + final_path = str(item.final_mask) + source_path = str(source) + + elif choice_name == "EDITED": + if source is None or not source.is_file(): + raise FileNotFoundError(f"Máscara editada ausente: {source}") + ensure_dir(item.final_mask.parent) + if source.resolve() != item.final_mask.resolve(): + shutil.copy2(source, item.final_mask) + final_path = str(item.final_mask) + source_path = str(source) + + elif choice_name == "REMAP": + if item.final_mask.exists(): + item.final_mask.unlink() + final_path = "" + source_path = "" + + else: + raise ValueError(choice_name) + + choices[key] = { + "group": item.group, + "base": item.base, + "choice": choice_name, + "source_mask": source_path, + "edit_base": edit_base, + "final_mask": final_path, + "reviewed_at": now_iso(), + "suspicion_pct": "" if item.suspicion_pct is None else f"{item.suspicion_pct:.6f}", + "review_reasons": item.reasons, + } + write_choices(choices_csv, choices) + + +def remove_choice( + item: ReviewItem, + choices: Dict[Tuple[str, str], dict], + choices_csv: Path, +) -> None: + key = (item.group, item.base) + choices.pop(key, None) + if item.final_mask.exists(): + item.final_mask.unlink() + write_choices(choices_csv, choices) + + +# ============================================================ +# Editor +# ============================================================ + +def overlay_ids( + preview: np.ndarray, + ids: np.ndarray, + classes, + alpha: float, + transparent_class_id: Optional[int] = None, +) -> np.ndarray: + mask = ids_to_colored_mask(ids, classes) + + if transparent_class_id is None: + return cv2.addWeighted(preview, 1.0 - alpha, mask, alpha, 0.0) + + out = preview.copy() + region = ids != transparent_class_id + if np.any(region): + blended = cv2.addWeighted(preview, 1.0 - alpha, mask, alpha, 0.0) + out[region] = blended[region] + return out + + +def choose_edit_base(window: str, panel: np.ndarray) -> Optional[str]: + prompt = panel.copy() + h, w = prompt.shape[:2] + bar_h = 108 + overlay = prompt.copy() + cv2.rectangle(overlay, (0, 0), (w, bar_h), (0, 0, 0), -1) + prompt = cv2.addWeighted(overlay, 0.82, prompt, 0.18, 0) + + cv2.putText( + prompt, "EDITOR: escolha a base", + (18, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.85, + (255, 255, 255), 2, cv2.LINE_AA, + ) + cv2.putText( + prompt, "1=HUMANA 2=MODELO 3=CHAO/VAZIO VISUALMENTE ESC=cancelar", + (18, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.62, + (80, 255, 120), 2, cv2.LINE_AA, + ) + + cv2.imshow(window, prompt) + + while True: + k = cv2.waitKeyEx(0) + if k == ord("1"): + return "HUMAN" + if k == ord("2"): + return "MODEL" + if k == ord("3"): + return "GROUND" + if k in (27, ord("q"), ord("Q")): + return None + + +def run_polygon_editor( + window: str, + item: ReviewItem, + base_choice: str, + classes, + alpha_state: dict, +) -> Optional[Tuple[np.ndarray, str]]: + preview = imread_required(item.preview) + human_bgr = fit_to(imread_required(item.human_mask), (preview.shape[1], preview.shape[0]), cv2.INTER_NEAREST) + model_bgr = fit_to(imread_required(item.model_mask), (preview.shape[1], preview.shape[0]), cv2.INTER_NEAREST) + + chao_id = find_chao_id(classes) + + if base_choice == "HUMAN": + base_ids = colored_mask_to_ids(human_bgr, classes) + transparent_class_id = None + elif base_choice == "MODEL": + base_ids = colored_mask_to_ids(model_bgr, classes) + transparent_class_id = None + elif base_choice == "GROUND": + base_ids = np.full(preview.shape[:2], chao_id, dtype=np.uint8) + transparent_class_id = chao_id + else: + raise ValueError(base_choice) + + work_ids = base_ids.copy() + history: List[np.ndarray] = [] + points: List[Tuple[int, int]] = [] + + class_ids = sorted(classes.keys()) + selected_pos = class_ids.index(chao_id) if chao_id in class_ids else 0 + selected_cid = class_ids[selected_pos] + + tool_mode = "POLY" + brush_radius = 12 + brush_radius_min = 1 + brush_radius_max = 128 + + zoom = 1.0 + zoom_min = 1.0 + zoom_max = 12.0 + zoom_step = 1.25 + view_x = 0.0 + view_y = 0.0 + + img_h, img_w = preview.shape[:2] + header_h = 92 + footer_h = 96 + canvas_w = img_w + canvas_h = img_h + + is_panning = False + pan_anchor = (0, 0) + pan_anchor_view = (0.0, 0.0) + is_painting = False + last_paint_point: Optional[Tuple[int, int]] = None + last_mouse_img = (img_w // 2, img_h // 2) + + minimap_visible = True + minimap_rect = None + is_dragging_minimap = False + minimap_drag_offset = (0, 0) + + def minimap_size() -> Tuple[int, int]: + mm_max_w = 220 + mm_max_h = 150 + scale = min(mm_max_w / img_w, mm_max_h / img_h) + mm_w = max(1, int(round(img_w * scale))) + mm_h = max(1, int(round(img_h * scale))) + return mm_w, mm_h + + mm_w, mm_h = minimap_size() + minimap_x = max(10, canvas_w - mm_w - 10) + minimap_y = 10 + + def clamp_minimap() -> None: + nonlocal minimap_x, minimap_y + minimap_x = int(min(max(0, minimap_x), max(0, canvas_w - mm_w))) + minimap_y = int(min(max(0, minimap_y), max(0, canvas_h - mm_h))) + + def inside_minimap_display(x: int, y: int) -> bool: + if not minimap_visible or minimap_rect is None: + return False + x0, y0, x1, y1 = minimap_rect + return x0 <= x < x1 and y0 <= y < y1 + + def max_offsets(scale: float) -> Tuple[float, float]: + vw = min(img_w, img_w / scale) + vh = min(img_h, img_h / scale) + return max(0.0, img_w - vw), max(0.0, img_h - vh) + + def clamp_view() -> None: + nonlocal view_x, view_y + mx, my = max_offsets(zoom) + view_x = min(max(0.0, view_x), mx) + view_y = min(max(0.0, view_y), my) + + def screen_to_image(x: int, y: int) -> Optional[Tuple[int, int]]: + yy = float(y - header_h) + xx = float(x) + if xx < 0 or yy < 0 or xx >= canvas_w or yy >= canvas_h: + return None + ix = int(np.floor(view_x + xx / zoom)) + iy = int(np.floor(view_y + yy / zoom)) + ix = min(max(ix, 0), img_w - 1) + iy = min(max(iy, 0), img_h - 1) + return ix, iy + + def set_zoom(new_zoom: float, anchor_screen: Optional[Tuple[int, int]] = None) -> None: + nonlocal zoom, view_x, view_y + old_zoom = zoom + new_zoom = min(max(float(new_zoom), zoom_min), zoom_max) + if abs(new_zoom - old_zoom) < 1e-9: + return + if anchor_screen is None: + ax = canvas_w / 2.0 + ay = canvas_h / 2.0 + else: + ax = float(anchor_screen[0]) + ay = float(anchor_screen[1] - header_h) + ax = min(max(ax, 0.0), float(canvas_w - 1)) + ay = min(max(ay, 0.0), float(canvas_h - 1)) + anchor_ix = view_x + ax / old_zoom + anchor_iy = view_y + ay / old_zoom + zoom = new_zoom + view_x = anchor_ix - ax / zoom + view_y = anchor_iy - ay / zoom + clamp_view() + + def fit_view() -> None: + nonlocal zoom, view_x, view_y + zoom = 1.0 + view_x = 0.0 + view_y = 0.0 + + def center_on(ix: int, iy: int) -> None: + nonlocal view_x, view_y + view_x = float(ix) - (canvas_w / zoom) / 2.0 + view_y = float(iy) - (canvas_h / zoom) / 2.0 + clamp_view() + + def center_on_from_minimap(display_x: int, display_y: int) -> None: + if mm_w <= 1 or mm_h <= 1: + return + rel_x = (display_x - minimap_x) / float(mm_w) + rel_y = (display_y - minimap_y) / float(mm_h) + rel_x = min(max(rel_x, 0.0), 1.0) + rel_y = min(max(rel_y, 0.0), 1.0) + ix = int(round(rel_x * (img_w - 1))) + iy = int(round(rel_y * (img_h - 1))) + center_on(ix, iy) + + def pan(dx_px: float, dy_px: float) -> None: + nonlocal view_x, view_y + view_x += dx_px / zoom + view_y += dy_px / zoom + clamp_view() + + def push_history() -> None: + history.append(work_ids.copy()) + if len(history) > 100: + del history[0] + + def apply_circle(mask: np.ndarray, ix: int, iy: int, radius: int, value: int) -> None: + cv2.circle(mask, (int(ix), int(iy)), int(radius), int(value), thickness=-1) + + def apply_from_base(mask: np.ndarray, ix: int, iy: int, radius: int) -> None: + yy, xx = np.ogrid[:img_h, :img_w] + circle = (xx - int(ix)) ** 2 + (yy - int(iy)) ** 2 <= int(radius) ** 2 + mask[circle] = base_ids[circle] + + def paint_line(mask: np.ndarray, p0: Tuple[int, int], p1: Tuple[int, int], radius: int, value: Optional[int], restore_from_base: bool) -> None: + x0, y0 = p0 + x1, y1 = p1 + dist = max(abs(x1 - x0), abs(y1 - y0), 1) + for t in range(dist + 1): + a = t / dist + x = int(round(x0 + (x1 - x0) * a)) + y = int(round(y0 + (y1 - y0) * a)) + if restore_from_base: + apply_from_base(mask, x, y, radius) + else: + assert value is not None + apply_circle(mask, x, y, radius, value) + + def begin_stroke(ix: int, iy: int) -> None: + nonlocal is_painting, last_paint_point + push_history() + is_painting = True + last_paint_point = (ix, iy) + if tool_mode == "BRUSH": + apply_circle(work_ids, ix, iy, brush_radius, selected_cid) + elif tool_mode == "ERASER": + apply_from_base(work_ids, ix, iy, brush_radius) + + def continue_stroke(ix: int, iy: int) -> None: + nonlocal last_paint_point + if not is_painting or last_paint_point is None: + return + if tool_mode == "BRUSH": + paint_line(work_ids, last_paint_point, (ix, iy), brush_radius, selected_cid, restore_from_base=False) + elif tool_mode == "ERASER": + paint_line(work_ids, last_paint_point, (ix, iy), brush_radius, None, restore_from_base=True) + last_paint_point = (ix, iy) + + def end_stroke() -> None: + nonlocal is_painting, last_paint_point + is_painting = False + last_paint_point = None + + def flood_component(ix: int, iy: int, new_cid: int) -> None: + old_cid = int(work_ids[iy, ix]) + if old_cid == int(new_cid): + return + region = (work_ids == old_cid).astype(np.uint8) + flood = np.zeros((img_h + 2, img_w + 2), dtype=np.uint8) + flags = 4 | cv2.FLOODFILL_MASK_ONLY | (255 << 8) + region_copy = region.copy() + cv2.floodFill(region_copy, flood, (int(ix), int(iy)), 0, flags=flags) + selected = flood[1:-1, 1:-1] != 0 + if not np.any(selected): + return + push_history() + work_ids[selected] = int(new_cid) + + def draw_minimap(target: np.ndarray) -> None: + nonlocal minimap_rect + if not minimap_visible: + minimap_rect = None + return + thumb = cv2.resize(preview, (mm_w, mm_h), interpolation=cv2.INTER_AREA) + if mm_w <= 6 or mm_h <= 6: + minimap_rect = None + return + clamp_minimap() + x0 = int(minimap_x) + y0 = int(minimap_y) + x1 = x0 + mm_w + y1 = y0 + mm_h + minimap_rect = (x0, y0, x1, y1) + overlay = target.copy() + cv2.rectangle(overlay, (x0 - 3, y0 - 3), (x1 + 3, y1 + 20), (0, 0, 0), -1) + cv2.addWeighted(overlay, 0.55, target, 0.45, 0, target) + target[y0:y1, x0:x1] = thumb + vw = img_w / zoom + vh = img_h / zoom + rx0 = x0 + int(round((view_x / img_w) * mm_w)) + ry0 = y0 + int(round((view_y / img_h) * mm_h)) + rx1 = x0 + int(round(((view_x + vw) / img_w) * mm_w)) + ry1 = y0 + int(round(((view_y + vh) / img_h) * mm_h)) + rx1 = min(max(rx1, rx0 + 1), x1) + ry1 = min(max(ry1, ry0 + 1), y1) + cv2.rectangle(target, (x0 - 1, y0 - 1), (x1 + 1, y1 + 1), (255, 255, 255), 1) + cv2.rectangle(target, (rx0, ry0), (rx1, ry1), (0, 255, 255), 2) + cv2.putText(target, "mini mapa [M]", (x0, min(target.shape[0] - 5, y1 + 16)), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (230, 230, 230), 1, cv2.LINE_AA) + + def current_view_overlay() -> np.ndarray: + alpha = float(alpha_state["value"]) / 100.0 + base_overlay = overlay_ids(preview, work_ids, classes, alpha=alpha, transparent_class_id=transparent_class_id) + if tool_mode in {"BRUSH", "ERASER"}: + cx, cy = last_mouse_img + color = class_color_bgr(classes, selected_cid) if tool_mode == "BRUSH" else (255, 255, 255) + cv2.circle(base_overlay, (int(cx), int(cy)), int(brush_radius), color, 2, cv2.LINE_AA) + view_w = max(1, int(round(canvas_w / zoom))) + view_h = max(1, int(round(canvas_h / zoom))) + x0 = int(round(view_x)) + y0 = int(round(view_y)) + x1 = min(img_w, x0 + view_w) + y1 = min(img_h, y0 + view_h) + crop = base_overlay[y0:y1, x0:x1] + display = cv2.resize(crop, (canvas_w, canvas_h), interpolation=cv2.INTER_NEAREST) + return display + + def on_mouse(event, x, y, flags, param): + nonlocal points, tool_mode, is_panning, pan_anchor, pan_anchor_view, last_mouse_img, view_x, view_y + nonlocal is_dragging_minimap, minimap_drag_offset, minimap_x, minimap_y + display_y = y - header_h + over_minimap = inside_minimap_display(x, display_y) + pos = screen_to_image(x, y) + if pos is not None: + last_mouse_img = pos + ix, iy = pos + else: + ix = iy = None + if event == cv2.EVENT_MOUSEWHEEL: + if over_minimap: + return + direction = 1 if flags > 0 else -1 + if direction > 0: + set_zoom(zoom * zoom_step, (x, y)) + else: + set_zoom(zoom / zoom_step, (x, y)) + return + if event == cv2.EVENT_MBUTTONDOWN: + if over_minimap: + is_dragging_minimap = True + minimap_drag_offset = (x - minimap_x, display_y - minimap_y) + else: + is_panning = True + pan_anchor = (x, y) + pan_anchor_view = (view_x, view_y) + return + if event == cv2.EVENT_MBUTTONUP: + is_panning = False + is_dragging_minimap = False + return + if event == cv2.EVENT_MOUSEMOVE and is_dragging_minimap: + minimap_x = x - minimap_drag_offset[0] + minimap_y = display_y - minimap_drag_offset[1] + clamp_minimap() + return + if event == cv2.EVENT_MOUSEMOVE and is_panning: + dx = pan_anchor[0] - x + dy = pan_anchor[1] - y + view_x = pan_anchor_view[0] + dx / zoom + view_y = pan_anchor_view[1] + dy / zoom + clamp_view() + return + if over_minimap: + if event == cv2.EVENT_LBUTTONDOWN: + center_on_from_minimap(x, display_y) + if event == cv2.EVENT_LBUTTONUP: + end_stroke() + return + if pos is None: + if event == cv2.EVENT_LBUTTONUP: + end_stroke() + return + if tool_mode == "POLY": + if event == cv2.EVENT_LBUTTONDOWN: + points.append((ix, iy)) + elif event == cv2.EVENT_RBUTTONDOWN and points: + points.pop() + elif tool_mode == "BUCKET": + if event == cv2.EVENT_LBUTTONDOWN: + points = [] + flood_component(ix, iy, selected_cid) + elif tool_mode in {"BRUSH", "ERASER"}: + if event == cv2.EVENT_LBUTTONDOWN: + points = [] + begin_stroke(ix, iy) + elif event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_LBUTTON): + continue_stroke(ix, iy) + elif event == cv2.EVENT_LBUTTONUP: + continue_stroke(ix, iy) + end_stroke() + + cv2.setMouseCallback(window, on_mouse) + + def render() -> np.ndarray: + alpha = float(alpha_state["value"]) / 100.0 + display = current_view_overlay() + if points: + draw_color = class_color_bgr(classes, selected_cid) + visible_points = [] + for px, py in points: + sx = int(round((float(px) - view_x) * zoom)) + sy = int(round((float(py) - view_y) * zoom)) + if -50 <= sx <= canvas_w + 50 and -50 <= sy <= canvas_h + 50: + visible_points.append((sx, sy)) + for sx, sy in visible_points: + cv2.circle(display, (sx, sy), 4, draw_color, -1, cv2.LINE_AA) + cv2.circle(display, (sx, sy), 6, (255, 255, 255), 1, cv2.LINE_AA) + if len(visible_points) >= 2: + pts = np.array(visible_points, dtype=np.int32) + cv2.polylines(display, [pts], False, draw_color, 2, cv2.LINE_AA) + draw_minimap(display) + header = np.zeros((header_h, canvas_w, 3), dtype=np.uint8) + footer = np.zeros((footer_h, canvas_w, 3), dtype=np.uint8) + cname, _rgb = classes[selected_cid] + bgr = class_color_bgr(classes, selected_cid) + minimap_txt = "ON" if minimap_visible else "OFF" + cv2.putText(header, f"EDITANDO {item.group}/{item.base} | base={base_choice} | tool={tool_mode}", (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.62, (245, 245, 245), 2, cv2.LINE_AA) + cv2.rectangle(header, (12, 48), (42, 78), bgr, -1) + cv2.rectangle(header, (12, 48), (42, 78), (255, 255, 255), 1) + cv2.putText(header, f"classe {selected_cid}: {cname} | brush={brush_radius}px | alpha={alpha:.2f} | zoom={zoom:.2f}x | minimapa={minimap_txt}", (54, 72), cv2.FONT_HERSHEY_SIMPLEX, 0.48, (220, 220, 220), 1, cv2.LINE_AA) + cv2.putText(footer, "P=poly B=balde N=pincel E=borracha | Esq=usar Dir=remove ponto | ENTER=preenche poligono", (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.44, (230, 230, 230), 1, cv2.LINE_AA) + cv2.putText(footer, "0..9=id C=proxima classe [ / ] pincel + / - ou roda=zoom setas=pan meio=pan | meio no minimapa=arrasta", (12, 58), cv2.FONT_HERSHEY_SIMPLEX, 0.44, (230, 230, 230), 1, cv2.LINE_AA) + cv2.putText(footer, "F=fit G=cursor M=liga/desliga minimapa | clique no minimapa=centraliza | Z=undo R=reset S=SALVA ESC/Q=cancela", (12, 86), cv2.FONT_HERSHEY_SIMPLEX, 0.44, (80, 255, 120), 1, cv2.LINE_AA) + return np.vstack([header, display, footer]) + + try: + while True: + cv2.imshow(window, render()) + k = cv2.waitKeyEx(30) + if k == -1: + continue + if k in (27, ord("q"), ord("Q")): + return None + if k in (ord("s"), ord("S")): + return work_ids.copy(), base_choice + if k in (ord("c"), ord("C")): + selected_pos = (selected_pos + 1) % len(class_ids) + selected_cid = class_ids[selected_pos] + continue + if ord("0") <= k <= ord("9"): + cid = int(chr(k)) + if cid in classes: + selected_cid = cid + selected_pos = class_ids.index(cid) + continue + if k in (ord("p"), ord("P")): + tool_mode = "POLY" + continue + if k in (ord("b"), ord("B")): + tool_mode = "BUCKET" + points = [] + end_stroke() + continue + if k in (ord("n"), ord("N")): + tool_mode = "BRUSH" + points = [] + end_stroke() + continue + if k in (ord("e"), ord("E")): + tool_mode = "ERASER" + points = [] + end_stroke() + continue + if k in (10, 13): + if tool_mode == "POLY" and len(points) >= 3: + push_history() + poly = np.array(points, dtype=np.int32) + cv2.fillPoly(work_ids, [poly], int(selected_cid)) + points = [] + continue + if k in (ord("z"), ord("Z")): + if history: + work_ids = history.pop() + points = [] + end_stroke() + continue + if k in (ord("r"), ord("R")): + work_ids = base_ids.copy() + history = [] + points = [] + end_stroke() + fit_view() + tool_mode = "POLY" + continue + if k in (8, 127, 3014656): + points = [] + end_stroke() + continue + if k in (ord("f"), ord("F")): + fit_view() + continue + if k in (ord("g"), ord("G")): + center_on(*last_mouse_img) + continue + if k in (ord("m"), ord("M")): + minimap_visible = not minimap_visible + continue + if k == ord("["): + brush_radius = max(brush_radius_min, brush_radius - 1) + continue + if k == ord("]"): + brush_radius = min(brush_radius_max, brush_radius + 1) + continue + if k in (ord("+"), ord("="), 171): + set_zoom(zoom * zoom_step) + continue + if k in (ord("-"), ord("_"), 173): + set_zoom(zoom / zoom_step) + continue + if k in (2424832, 81): + pan(-80, 0) + continue + if k in (2555904, 83): + pan(80, 0) + continue + if k == 2490368: + pan(0, -80) + continue + if k == 2621440: + pan(0, 80) + continue + finally: + cv2.setMouseCallback(window, lambda *args: None) + + +# ============================================================ +# Main +# ============================================================ + +def run_main() -> None: + parser = argparse.ArgumentParser(description="MaskReviewer standalone") + parser.add_argument("--group", default=None, help="Pasta group. Se ausente, procura ao lado do EXE ou abre seletor.") + parser.add_argument("--labelmap", default=None, help="labelmap.txt. Se ausente, procura automaticamente ou abre seletor.") + parser.add_argument("--groups", nargs="*", default=None, help="Opcional: limita grupos") + parser.add_argument("--alpha", type=float, default=0.45, help="Overlay inicial 0..1") + parser.add_argument("--max_width", type=int, default=1800) + parser.add_argument("--start_idx", type=int, default=None) + args = parser.parse_args() + + if not 0.0 <= args.alpha <= 1.0: + raise RuntimeError("--alpha deve ficar entre 0 e 1") + + base = app_dir() + settings_path = base / SETTINGS_NAME + settings = load_settings(settings_path) + + group_root = resolve_group_root(args.group, settings) + labelmap_path = resolve_labelmap(args.labelmap, group_root, settings) + + settings["group_root"] = str(group_root) + settings["labelmap"] = str(labelmap_path) + save_settings(settings_path, settings) + + classes = load_labelmap_classes(labelmap_path) + items = collect_items(group_root, args.groups) + if not items: + raise RuntimeError( + "Nenhum trio preview/mask/prediction foi encontrado.\n\n" + f"GROUP selecionado:\n{group_root}\n\n" + "Estrutura esperada por grupo:\n" + "previews/.png\nmasks/.png\npredictions/.png" + ) + + choices_csv = group_root / CHOICES_NAME + choices = load_choices(choices_csv) + + if args.start_idx is not None: + idx = max(0, min(len(items) - 1, int(args.start_idx))) + else: + idx = 0 + for i, item in enumerate(items): + if (item.group, item.base) not in choices: + idx = i + break + else: + idx = 0 + + alpha_state = {"value": int(round(args.alpha * 100.0))} + window = APP_NAME + final_window = f"{APP_NAME} - FINAL" + show_final_window = False + + def on_alpha(v): + alpha_state["value"] = max(0, min(100, int(v))) + + cv2.namedWindow(window, cv2.WINDOW_NORMAL) + cv2.createTrackbar("Overlay %", window, alpha_state["value"], 100, on_alpha) + + while True: + item = items[idx] + current = choices.get((item.group, item.base), {}).get("choice") + + alpha = float(alpha_state["value"]) / 100.0 + canvas = compose_panel( + item=item, + choice=current, + index=idx, + total=len(items), + alpha=alpha, + max_width=args.max_width, + ) + cv2.imshow(window, canvas) + + if show_final_window: + final_canvas = compose_final_panel(item=item, choice=current, alpha=alpha, max_width=args.max_width) + cv2.namedWindow(final_window, cv2.WINDOW_NORMAL) + cv2.imshow(final_window, final_canvas) + else: + try: + cv2.destroyWindow(final_window) + except Exception: + pass + + k = cv2.waitKeyEx(30) + if k == -1: + continue + + if k in (ord("q"), ord("Q"), 27): + break + + if k in (ord("v"), ord("V")): + show_final_window = not show_final_window + continue + + if k == ord("1"): + record_choice(item, "HUMAN", item.human_mask, choices, choices_csv) + idx = (idx + 1) % len(items) + continue + + if k == ord("2"): + record_choice(item, "MODEL", item.model_mask, choices, choices_csv) + idx = (idx + 1) % len(items) + continue + + if k == ord("3"): + record_choice(item, "REMAP", None, choices, choices_csv) + idx = (idx + 1) % len(items) + continue + + if k in (ord("e"), ord("E")): + base_choice = choose_edit_base(window, canvas) + if base_choice is None: + continue + edited = run_polygon_editor( + window=window, + item=item, + base_choice=base_choice, + classes=classes, + alpha_state=alpha_state, + ) + if edited is not None: + edited_ids, edit_base = edited + edited_bgr = ids_to_colored_mask(edited_ids, classes) + ensure_dir(item.final_mask.parent) + if not cv2.imwrite(str(item.final_mask), edited_bgr): + raise RuntimeError(f"Falha ao salvar máscara editada: {item.final_mask}") + record_choice(item, "EDITED", item.final_mask, choices, choices_csv, edit_base=edit_base) + idx = (idx + 1) % len(items) + continue + + if k in (ord("x"), ord("X")): + remove_choice(item, choices, choices_csv) + continue + + if k in (ord("s"), ord("S")): + idx = (idx + 1) % len(items) + continue + + if k in (ord("d"), ord("D"), 83, 2555904): + idx = (idx + 1) % len(items) + continue + + if k in (ord("a"), ord("A"), 81, 2424832): + idx = (idx - 1 + len(items)) % len(items) + continue + + try: + cv2.destroyWindow(final_window) + except Exception: + pass + cv2.destroyAllWindows() + + +def main() -> None: + try: + run_main() + except Exception as exc: + # Em build --windowed não existe console. Gravamos log ao lado do EXE. + log_path = app_dir() / "MaskReviewer_error.log" + try: + log_path.write_text(traceback.format_exc(), encoding="utf-8") + except Exception: + pass + + show_error_dialog( + f"O MaskReviewer encontrou um erro:\n\n{exc}\n\n" + f"Detalhes foram gravados em:\n{log_path}" + ) + raise + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.settings.json b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.settings.json new file mode 100644 index 000000000..785f2553a --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.settings.json @@ -0,0 +1,4 @@ +{ + "group_root": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\dist\\MaskReviewer\\group", + "labelmap": "C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\dist\\MaskReviewer\\labelmap.txt" +} \ No newline at end of file diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.spec b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.spec new file mode 100644 index 000000000..b3e3eb0f2 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/MaskReviewer.spec @@ -0,0 +1,51 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_all + +datas = [] +binaries = [] +hiddenimports = [] +tmp_ret = collect_all('cv2') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] + + +a = Analysis( + ['MaskReviewer.py'], + pathex=[], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='MaskReviewer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='MaskReviewer', +) diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/README_MASKREVIEWER.txt b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/README_MASKREVIEWER.txt new file mode 100644 index 000000000..529818576 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/README_MASKREVIEWER.txt @@ -0,0 +1,110 @@ +MASKREVIEWER - PACOTE WINDOWS +================================ + +OBJETIVO +-------- +Revisar, comparar e eventualmente editar máscaras já separadas pelo auditor. +Não carrega modelo, PyTorch, CUDA ou checkpoint. + +PACOTE PARA O ANOTADOR +---------------------- +A estrutura recomendada é: + +MaskReviewer\ + MaskReviewer.exe + _internal\ + labelmap.txt + group\ + chao\ + previews\ + masks\ + predictions\ + final_masks\ + review_order.csv + chao_cana\ + chao_cana_erva\ + chao_erva\ + ... + +Ao abrir MaskReviewer.exe: + +1. Se "group" estiver ao lado do EXE, ele usa automaticamente. +2. Se "labelmap.txt" estiver ao lado do EXE, ele usa automaticamente. +3. Se algum deles não existir, abre o seletor do Windows. +4. Os caminhos escolhidos ficam lembrados em MaskReviewer.settings.json. +5. O estado do lote fica em group\review_choices.csv. +6. As escolhas finais ficam em cada: + group\\final_masks\ + +NOVO LOTE +--------- +Para mandar uma nova revisão à mesma pessoa: + +1. Ela mantém: + MaskReviewer.exe + _internal\ + labelmap.txt + +2. Substitui a pasta: + group\ + +3. Abre o mesmo EXE. + +Como review_choices.csv fica DENTRO de group\, a sessão nova começa limpa junto +com a nova pasta group. + +CONTROLES +--------- +Painel 2x2: + 1 = máscara humana + 2 = máscara do modelo + 3 = REMAP + E = editar + A/D = anterior/próxima + S = pular + X = apagar decisão + Q/ESC = sair + +A barra "Overlay %" controla a opacidade dos dois overlays ao mesmo tempo. + +Editor: + E e depois: + 1 = humana como base + 2 = modelo como base + 3 = começar de chão + + Mouse esquerdo = adiciona ponto + Mouse direito = remove último ponto + ENTER = preenche polígono + C = próxima classe + 0..9 = ID de classe + Z = desfazer + R = restaurar base + BACKSPACE/DEL = limpar pontos abertos + S = salvar + ESC/Q = cancelar edição + +COMO GERAR O EXE +---------------- +Execute com duplo clique: + + build_windows.bat + +Recomendado: + Windows 10/11 + Python 3.11 ou 3.12 instalado SOMENTE na máquina de build + +O script cria: + dist\MaskReviewer\MaskReviewer.exe + +Depois do build, a máquina do anotador NÃO precisa ter Python. + +POR QUE --ONEDIR? +----------------- +O build usa PyInstaller --onedir porque OpenCV/Numpy ficam mais confiáveis, +iniciam mais rápido e são mais fáceis de diagnosticar do que um EXE --onefile. + +IMPORTANTE +---------- +O MaskReviewer nunca altera o dataset bruto. +Ele só escreve final_masks e review_choices.csv dentro do lote enviado. diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/Analysis-00.toc b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/Analysis-00.toc new file mode 100644 index 000000000..5192afbae --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/Analysis-00.toc @@ -0,0 +1,4402 @@ +(['C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\MaskReviewer.py'], + ['C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package'], + ['cv2', + 'cv2.config', + 'cv2.config-3', + 'cv2.cv2', + 'cv2.data', + 'cv2.load_config_py2', + 'cv2.load_config_py3', + 'cv2.mat_wrapper', + 'cv2.misc', + 'cv2.misc.version', + 'cv2.typing', + 'cv2.utils', + 'cv2.version'], + [('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_pyinstaller', + 0), + ('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks', + -1000), + ('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\_pyinstaller_hooks_contrib', + -1000)], + {}, + [], + [], + False, + {}, + 0, + [('cv2\\opencv_videoio_ffmpeg500_64.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg500_64.dll', + 'BINARY')], + [('cv2\\Error\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\Error\\__init__.pyi', + 'DATA'), + ('cv2\\LICENSE-3RD-PARTY.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\LICENSE-3RD-PARTY.txt', + 'DATA'), + ('cv2\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\LICENSE.txt', + 'DATA'), + ('cv2\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\__init__.py', + 'DATA'), + ('cv2\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\__init__.pyi', + 'DATA'), + ('cv2\\aruco\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\aruco\\__init__.pyi', + 'DATA'), + ('cv2\\barcode\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\barcode\\__init__.pyi', + 'DATA'), + ('cv2\\ccm\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ccm\\__init__.pyi', + 'DATA'), + ('cv2\\config-3.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config-3.py', + 'DATA'), + ('cv2\\config.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config.py', + 'DATA'), + ('cv2\\cuda\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\cuda\\__init__.pyi', + 'DATA'), + ('cv2\\data\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\data\\__init__.py', + 'DATA'), + ('cv2\\detail\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\detail\\__init__.pyi', + 'DATA'), + ('cv2\\dnn\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\dnn\\__init__.pyi', + 'DATA'), + ('cv2\\fisheye\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\fisheye\\__init__.pyi', + 'DATA'), + ('cv2\\flann\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\flann\\__init__.pyi', + 'DATA'), + ('cv2\\instr\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\instr\\__init__.pyi', + 'DATA'), + ('cv2\\ipp\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ipp\\__init__.pyi', + 'DATA'), + ('cv2\\load_config_py2.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py2.py', + 'DATA'), + ('cv2\\load_config_py3.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py3.py', + 'DATA'), + ('cv2\\mat_wrapper\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\mat_wrapper\\__init__.py', + 'DATA'), + ('cv2\\mcc\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\mcc\\__init__.pyi', + 'DATA'), + ('cv2\\misc\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\__init__.py', + 'DATA'), + ('cv2\\misc\\version.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\version.py', + 'DATA'), + ('cv2\\ocl\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ocl\\__init__.pyi', + 'DATA'), + ('cv2\\ogl\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ogl\\__init__.pyi', + 'DATA'), + ('cv2\\opencv_videoio_ffmpeg500_64.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg500_64.dll', + 'DATA'), + ('cv2\\parallel\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\parallel\\__init__.pyi', + 'DATA'), + ('cv2\\py.typed', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\py.typed', + 'DATA'), + ('cv2\\samples\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\samples\\__init__.pyi', + 'DATA'), + ('cv2\\segmentation\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\segmentation\\__init__.pyi', + 'DATA'), + ('cv2\\typing\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\typing\\__init__.py', + 'DATA'), + ('cv2\\utils\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\__init__.py', + 'DATA'), + ('cv2\\utils\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\fs\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\fs\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\logging\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\logging\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\nested\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\nested\\__init__.pyi', + 'DATA'), + ('cv2\\version.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\version.py', + 'DATA'), + ('cv2\\videoio_registry\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\videoio_registry\\__init__.pyi', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\INSTALLER', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\INSTALLER', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\LICENSE-3RD-PARTY.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\LICENSE-3RD-PARTY.txt', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\LICENSE.txt', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\METADATA', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\METADATA', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\RECORD', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\RECORD', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\REQUESTED', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\REQUESTED', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\WHEEL', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\WHEEL', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\top_level.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\top_level.txt', + 'DATA')], + '3.12.2 (tags/v3.12.2:6abddd9, Feb 6 2024, 21:26:36) [MSC v.1937 64 bit ' + '(AMD64)]', + [('pyi_rth__tkinter', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py', + 'PYSOURCE'), + ('pyi_rth_inspect', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py', + 'PYSOURCE'), + ('pyi_rth_pkgutil', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py', + 'PYSOURCE'), + ('pyi_rth_multiprocessing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py', + 'PYSOURCE'), + ('MaskReviewer', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\MaskReviewer.py', + 'PYSOURCE')], + [('subprocess', 'C:\\Python312\\Lib\\subprocess.py', 'PYMODULE'), + ('selectors', 'C:\\Python312\\Lib\\selectors.py', 'PYMODULE'), + ('contextlib', 'C:\\Python312\\Lib\\contextlib.py', 'PYMODULE'), + ('threading', 'C:\\Python312\\Lib\\threading.py', 'PYMODULE'), + ('_threading_local', 'C:\\Python312\\Lib\\_threading_local.py', 'PYMODULE'), + ('signal', 'C:\\Python312\\Lib\\signal.py', 'PYMODULE'), + ('multiprocessing.spawn', + 'C:\\Python312\\Lib\\multiprocessing\\spawn.py', + 'PYMODULE'), + ('multiprocessing.resource_tracker', + 'C:\\Python312\\Lib\\multiprocessing\\resource_tracker.py', + 'PYMODULE'), + ('multiprocessing.util', + 'C:\\Python312\\Lib\\multiprocessing\\util.py', + 'PYMODULE'), + ('multiprocessing.forkserver', + 'C:\\Python312\\Lib\\multiprocessing\\forkserver.py', + 'PYMODULE'), + ('multiprocessing.connection', + 'C:\\Python312\\Lib\\multiprocessing\\connection.py', + 'PYMODULE'), + ('multiprocessing.resource_sharer', + 'C:\\Python312\\Lib\\multiprocessing\\resource_sharer.py', + 'PYMODULE'), + ('xmlrpc.client', 'C:\\Python312\\Lib\\xmlrpc\\client.py', 'PYMODULE'), + ('xmlrpc', 'C:\\Python312\\Lib\\xmlrpc\\__init__.py', 'PYMODULE'), + ('gzip', 'C:\\Python312\\Lib\\gzip.py', 'PYMODULE'), + ('_compression', 'C:\\Python312\\Lib\\_compression.py', 'PYMODULE'), + ('xml.parsers.expat', + 'C:\\Python312\\Lib\\xml\\parsers\\expat.py', + 'PYMODULE'), + ('xml.parsers', 'C:\\Python312\\Lib\\xml\\parsers\\__init__.py', 'PYMODULE'), + ('xml', 'C:\\Python312\\Lib\\xml\\__init__.py', 'PYMODULE'), + ('xml.sax.expatreader', + 'C:\\Python312\\Lib\\xml\\sax\\expatreader.py', + 'PYMODULE'), + ('xml.sax.saxutils', 'C:\\Python312\\Lib\\xml\\sax\\saxutils.py', 'PYMODULE'), + ('urllib.request', 'C:\\Python312\\Lib\\urllib\\request.py', 'PYMODULE'), + ('fnmatch', 'C:\\Python312\\Lib\\fnmatch.py', 'PYMODULE'), + ('getpass', 'C:\\Python312\\Lib\\getpass.py', 'PYMODULE'), + ('nturl2path', 'C:\\Python312\\Lib\\nturl2path.py', 'PYMODULE'), + ('ftplib', 'C:\\Python312\\Lib\\ftplib.py', 'PYMODULE'), + ('netrc', 'C:\\Python312\\Lib\\netrc.py', 'PYMODULE'), + ('mimetypes', 'C:\\Python312\\Lib\\mimetypes.py', 'PYMODULE'), + ('getopt', 'C:\\Python312\\Lib\\getopt.py', 'PYMODULE'), + ('gettext', 'C:\\Python312\\Lib\\gettext.py', 'PYMODULE'), + ('copy', 'C:\\Python312\\Lib\\copy.py', 'PYMODULE'), + ('email.utils', 'C:\\Python312\\Lib\\email\\utils.py', 'PYMODULE'), + ('email.charset', 'C:\\Python312\\Lib\\email\\charset.py', 'PYMODULE'), + ('email.encoders', 'C:\\Python312\\Lib\\email\\encoders.py', 'PYMODULE'), + ('quopri', 'C:\\Python312\\Lib\\quopri.py', 'PYMODULE'), + ('email.errors', 'C:\\Python312\\Lib\\email\\errors.py', 'PYMODULE'), + ('email.quoprimime', 'C:\\Python312\\Lib\\email\\quoprimime.py', 'PYMODULE'), + ('email.base64mime', 'C:\\Python312\\Lib\\email\\base64mime.py', 'PYMODULE'), + ('email._parseaddr', 'C:\\Python312\\Lib\\email\\_parseaddr.py', 'PYMODULE'), + ('calendar', 'C:\\Python312\\Lib\\calendar.py', 'PYMODULE'), + ('random', 'C:\\Python312\\Lib\\random.py', 'PYMODULE'), + ('statistics', 'C:\\Python312\\Lib\\statistics.py', 'PYMODULE'), + ('fractions', 'C:\\Python312\\Lib\\fractions.py', 'PYMODULE'), + ('numbers', 'C:\\Python312\\Lib\\numbers.py', 'PYMODULE'), + ('http.cookiejar', 'C:\\Python312\\Lib\\http\\cookiejar.py', 'PYMODULE'), + ('http', 'C:\\Python312\\Lib\\http\\__init__.py', 'PYMODULE'), + ('urllib', 'C:\\Python312\\Lib\\urllib\\__init__.py', 'PYMODULE'), + ('ssl', 'C:\\Python312\\Lib\\ssl.py', 'PYMODULE'), + ('urllib.response', 'C:\\Python312\\Lib\\urllib\\response.py', 'PYMODULE'), + ('urllib.error', 'C:\\Python312\\Lib\\urllib\\error.py', 'PYMODULE'), + ('string', 'C:\\Python312\\Lib\\string.py', 'PYMODULE'), + ('hashlib', 'C:\\Python312\\Lib\\hashlib.py', 'PYMODULE'), + ('email', 'C:\\Python312\\Lib\\email\\__init__.py', 'PYMODULE'), + ('email.parser', 'C:\\Python312\\Lib\\email\\parser.py', 'PYMODULE'), + ('email._policybase', + 'C:\\Python312\\Lib\\email\\_policybase.py', + 'PYMODULE'), + ('email.feedparser', 'C:\\Python312\\Lib\\email\\feedparser.py', 'PYMODULE'), + ('email.message', 'C:\\Python312\\Lib\\email\\message.py', 'PYMODULE'), + ('email.policy', 'C:\\Python312\\Lib\\email\\policy.py', 'PYMODULE'), + ('email.contentmanager', + 'C:\\Python312\\Lib\\email\\contentmanager.py', + 'PYMODULE'), + ('email.headerregistry', + 'C:\\Python312\\Lib\\email\\headerregistry.py', + 'PYMODULE'), + ('email.iterators', 'C:\\Python312\\Lib\\email\\iterators.py', 'PYMODULE'), + ('email.generator', 'C:\\Python312\\Lib\\email\\generator.py', 'PYMODULE'), + ('email._encoded_words', + 'C:\\Python312\\Lib\\email\\_encoded_words.py', + 'PYMODULE'), + ('email._header_value_parser', + 'C:\\Python312\\Lib\\email\\_header_value_parser.py', + 'PYMODULE'), + ('email.header', 'C:\\Python312\\Lib\\email\\header.py', 'PYMODULE'), + ('bisect', 'C:\\Python312\\Lib\\bisect.py', 'PYMODULE'), + ('xml.sax', 'C:\\Python312\\Lib\\xml\\sax\\__init__.py', 'PYMODULE'), + ('xml.sax.handler', 'C:\\Python312\\Lib\\xml\\sax\\handler.py', 'PYMODULE'), + ('xml.sax._exceptions', + 'C:\\Python312\\Lib\\xml\\sax\\_exceptions.py', + 'PYMODULE'), + ('xml.sax.xmlreader', + 'C:\\Python312\\Lib\\xml\\sax\\xmlreader.py', + 'PYMODULE'), + ('urllib.parse', 'C:\\Python312\\Lib\\urllib\\parse.py', 'PYMODULE'), + ('ipaddress', 'C:\\Python312\\Lib\\ipaddress.py', 'PYMODULE'), + ('http.client', 'C:\\Python312\\Lib\\http\\client.py', 'PYMODULE'), + ('decimal', 'C:\\Python312\\Lib\\decimal.py', 'PYMODULE'), + ('_pydecimal', 'C:\\Python312\\Lib\\_pydecimal.py', 'PYMODULE'), + ('contextvars', 'C:\\Python312\\Lib\\contextvars.py', 'PYMODULE'), + ('base64', 'C:\\Python312\\Lib\\base64.py', 'PYMODULE'), + ('hmac', 'C:\\Python312\\Lib\\hmac.py', 'PYMODULE'), + ('struct', 'C:\\Python312\\Lib\\struct.py', 'PYMODULE'), + ('socket', 'C:\\Python312\\Lib\\socket.py', 'PYMODULE'), + ('tempfile', 'C:\\Python312\\Lib\\tempfile.py', 'PYMODULE'), + ('logging', 'C:\\Python312\\Lib\\logging\\__init__.py', 'PYMODULE'), + ('pickle', 'C:\\Python312\\Lib\\pickle.py', 'PYMODULE'), + ('pprint', 'C:\\Python312\\Lib\\pprint.py', 'PYMODULE'), + ('_compat_pickle', 'C:\\Python312\\Lib\\_compat_pickle.py', 'PYMODULE'), + ('multiprocessing.context', + 'C:\\Python312\\Lib\\multiprocessing\\context.py', + 'PYMODULE'), + ('multiprocessing.popen_spawn_win32', + 'C:\\Python312\\Lib\\multiprocessing\\popen_spawn_win32.py', + 'PYMODULE'), + ('multiprocessing.popen_forkserver', + 'C:\\Python312\\Lib\\multiprocessing\\popen_forkserver.py', + 'PYMODULE'), + ('multiprocessing.popen_spawn_posix', + 'C:\\Python312\\Lib\\multiprocessing\\popen_spawn_posix.py', + 'PYMODULE'), + ('multiprocessing.popen_fork', + 'C:\\Python312\\Lib\\multiprocessing\\popen_fork.py', + 'PYMODULE'), + ('multiprocessing.sharedctypes', + 'C:\\Python312\\Lib\\multiprocessing\\sharedctypes.py', + 'PYMODULE'), + ('multiprocessing.heap', + 'C:\\Python312\\Lib\\multiprocessing\\heap.py', + 'PYMODULE'), + ('ctypes', 'C:\\Python312\\Lib\\ctypes\\__init__.py', 'PYMODULE'), + ('ctypes._endian', 'C:\\Python312\\Lib\\ctypes\\_endian.py', 'PYMODULE'), + ('multiprocessing.pool', + 'C:\\Python312\\Lib\\multiprocessing\\pool.py', + 'PYMODULE'), + ('multiprocessing.dummy', + 'C:\\Python312\\Lib\\multiprocessing\\dummy\\__init__.py', + 'PYMODULE'), + ('multiprocessing.dummy.connection', + 'C:\\Python312\\Lib\\multiprocessing\\dummy\\connection.py', + 'PYMODULE'), + ('queue', 'C:\\Python312\\Lib\\queue.py', 'PYMODULE'), + ('multiprocessing.queues', + 'C:\\Python312\\Lib\\multiprocessing\\queues.py', + 'PYMODULE'), + ('multiprocessing.synchronize', + 'C:\\Python312\\Lib\\multiprocessing\\synchronize.py', + 'PYMODULE'), + ('multiprocessing.managers', + 'C:\\Python312\\Lib\\multiprocessing\\managers.py', + 'PYMODULE'), + ('multiprocessing.shared_memory', + 'C:\\Python312\\Lib\\multiprocessing\\shared_memory.py', + 'PYMODULE'), + ('secrets', 'C:\\Python312\\Lib\\secrets.py', 'PYMODULE'), + ('multiprocessing.reduction', + 'C:\\Python312\\Lib\\multiprocessing\\reduction.py', + 'PYMODULE'), + ('multiprocessing.process', + 'C:\\Python312\\Lib\\multiprocessing\\process.py', + 'PYMODULE'), + ('runpy', 'C:\\Python312\\Lib\\runpy.py', 'PYMODULE'), + ('pkgutil', 'C:\\Python312\\Lib\\pkgutil.py', 'PYMODULE'), + ('zipimport', 'C:\\Python312\\Lib\\zipimport.py', 'PYMODULE'), + ('importlib.readers', + 'C:\\Python312\\Lib\\importlib\\readers.py', + 'PYMODULE'), + ('importlib.resources.readers', + 'C:\\Python312\\Lib\\importlib\\resources\\readers.py', + 'PYMODULE'), + ('importlib.resources._itertools', + 'C:\\Python312\\Lib\\importlib\\resources\\_itertools.py', + 'PYMODULE'), + ('importlib.resources.abc', + 'C:\\Python312\\Lib\\importlib\\resources\\abc.py', + 'PYMODULE'), + ('importlib.resources', + 'C:\\Python312\\Lib\\importlib\\resources\\__init__.py', + 'PYMODULE'), + ('importlib.resources._legacy', + 'C:\\Python312\\Lib\\importlib\\resources\\_legacy.py', + 'PYMODULE'), + ('importlib.resources._common', + 'C:\\Python312\\Lib\\importlib\\resources\\_common.py', + 'PYMODULE'), + ('importlib.resources._adapters', + 'C:\\Python312\\Lib\\importlib\\resources\\_adapters.py', + 'PYMODULE'), + ('zipfile', 'C:\\Python312\\Lib\\zipfile\\__init__.py', 'PYMODULE'), + ('zipfile._path', + 'C:\\Python312\\Lib\\zipfile\\_path\\__init__.py', + 'PYMODULE'), + ('zipfile._path.glob', + 'C:\\Python312\\Lib\\zipfile\\_path\\glob.py', + 'PYMODULE'), + ('py_compile', 'C:\\Python312\\Lib\\py_compile.py', 'PYMODULE'), + ('importlib._bootstrap_external', + 'C:\\Python312\\Lib\\importlib\\_bootstrap_external.py', + 'PYMODULE'), + ('importlib.metadata', + 'C:\\Python312\\Lib\\importlib\\metadata\\__init__.py', + 'PYMODULE'), + ('importlib.abc', 'C:\\Python312\\Lib\\importlib\\abc.py', 'PYMODULE'), + ('importlib._abc', 'C:\\Python312\\Lib\\importlib\\_abc.py', 'PYMODULE'), + ('importlib._bootstrap', + 'C:\\Python312\\Lib\\importlib\\_bootstrap.py', + 'PYMODULE'), + ('importlib.metadata._itertools', + 'C:\\Python312\\Lib\\importlib\\metadata\\_itertools.py', + 'PYMODULE'), + ('importlib.metadata._functools', + 'C:\\Python312\\Lib\\importlib\\metadata\\_functools.py', + 'PYMODULE'), + ('importlib.metadata._collections', + 'C:\\Python312\\Lib\\importlib\\metadata\\_collections.py', + 'PYMODULE'), + ('importlib.metadata._meta', + 'C:\\Python312\\Lib\\importlib\\metadata\\_meta.py', + 'PYMODULE'), + ('importlib.metadata._adapters', + 'C:\\Python312\\Lib\\importlib\\metadata\\_adapters.py', + 'PYMODULE'), + ('importlib.metadata._text', + 'C:\\Python312\\Lib\\importlib\\metadata\\_text.py', + 'PYMODULE'), + ('textwrap', 'C:\\Python312\\Lib\\textwrap.py', 'PYMODULE'), + ('tokenize', 'C:\\Python312\\Lib\\tokenize.py', 'PYMODULE'), + ('token', 'C:\\Python312\\Lib\\token.py', 'PYMODULE'), + ('lzma', 'C:\\Python312\\Lib\\lzma.py', 'PYMODULE'), + ('bz2', 'C:\\Python312\\Lib\\bz2.py', 'PYMODULE'), + ('inspect', 'C:\\Python312\\Lib\\inspect.py', 'PYMODULE'), + ('dis', 'C:\\Python312\\Lib\\dis.py', 'PYMODULE'), + ('opcode', 'C:\\Python312\\Lib\\opcode.py', 'PYMODULE'), + ('ast', 'C:\\Python312\\Lib\\ast.py', 'PYMODULE'), + ('importlib', 'C:\\Python312\\Lib\\importlib\\__init__.py', 'PYMODULE'), + ('importlib.util', 'C:\\Python312\\Lib\\importlib\\util.py', 'PYMODULE'), + ('importlib.machinery', + 'C:\\Python312\\Lib\\importlib\\machinery.py', + 'PYMODULE'), + ('multiprocessing', + 'C:\\Python312\\Lib\\multiprocessing\\__init__.py', + 'PYMODULE'), + ('_py_abc', 'C:\\Python312\\Lib\\_py_abc.py', 'PYMODULE'), + ('tracemalloc', 'C:\\Python312\\Lib\\tracemalloc.py', 'PYMODULE'), + ('stringprep', 'C:\\Python312\\Lib\\stringprep.py', 'PYMODULE'), + ('tkinter.messagebox', + 'C:\\Python312\\Lib\\tkinter\\messagebox.py', + 'PYMODULE'), + ('tkinter.commondialog', + 'C:\\Python312\\Lib\\tkinter\\commondialog.py', + 'PYMODULE'), + ('tkinter.filedialog', + 'C:\\Python312\\Lib\\tkinter\\filedialog.py', + 'PYMODULE'), + ('tkinter.simpledialog', + 'C:\\Python312\\Lib\\tkinter\\simpledialog.py', + 'PYMODULE'), + ('tkinter.dialog', 'C:\\Python312\\Lib\\tkinter\\dialog.py', 'PYMODULE'), + ('tkinter', 'C:\\Python312\\Lib\\tkinter\\__init__.py', 'PYMODULE'), + ('tkinter.constants', + 'C:\\Python312\\Lib\\tkinter\\constants.py', + 'PYMODULE'), + ('numpy', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\__init__.py', + 'PYMODULE'), + ('numpy._core._exceptions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_exceptions.py', + 'PYMODULE'), + ('numpy._core._dtype_ctypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_dtype_ctypes.py', + 'PYMODULE'), + ('numpy._pytesttester', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_pytesttester.py', + 'PYMODULE'), + ('numpy.strings', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\strings\\__init__.py', + 'PYMODULE'), + ('numpy._core.strings', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\strings.py', + 'PYMODULE'), + ('numpy._core.umath', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\umath.py', + 'PYMODULE'), + ('numpy._core.overrides', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\overrides.py', + 'PYMODULE'), + ('numpy._utils._inspect', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\_inspect.py', + 'PYMODULE'), + ('numpy._utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\__init__.py', + 'PYMODULE'), + ('numpy._utils._conversions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\_conversions.py', + 'PYMODULE'), + ('numpy._core.multiarray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\multiarray.py', + 'PYMODULE'), + ('numpy.core', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\__init__.py', + 'PYMODULE'), + ('numpy.core._utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\_utils.py', + 'PYMODULE'), + ('numpy.char', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\char\\__init__.py', + 'PYMODULE'), + ('numpy._core.defchararray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\defchararray.py', + 'PYMODULE'), + ('numpy._core.numerictypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\numerictypes.py', + 'PYMODULE'), + ('numpy._core._type_aliases', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_type_aliases.py', + 'PYMODULE'), + ('numpy._core._string_helpers', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_string_helpers.py', + 'PYMODULE'), + ('numpy._core.numeric', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\numeric.py', + 'PYMODULE'), + ('numpy._core.fromnumeric', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\fromnumeric.py', + 'PYMODULE'), + ('numpy._core._methods', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_methods.py', + 'PYMODULE'), + ('numpy._core.arrayprint', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\arrayprint.py', + 'PYMODULE'), + ('numpy._core._asarray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_asarray.py', + 'PYMODULE'), + ('numpy._core._ufunc_config', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_ufunc_config.py', + 'PYMODULE'), + ('numpy._core.shape_base', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\shape_base.py', + 'PYMODULE'), + ('numpy.rec', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\rec\\__init__.py', + 'PYMODULE'), + ('numpy._core.records', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\records.py', + 'PYMODULE'), + ('numpy.typing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\typing\\__init__.py', + 'PYMODULE'), + ('numpy._typing._add_docstring', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_add_docstring.py', + 'PYMODULE'), + ('numpy._typing._array_like', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_array_like.py', + 'PYMODULE'), + ('numpy._typing._shape', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_shape.py', + 'PYMODULE'), + ('numpy._typing._nested_sequence', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nested_sequence.py', + 'PYMODULE'), + ('numpy._typing._nbit_base', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nbit_base.py', + 'PYMODULE'), + ('numpy._typing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\__init__.py', + 'PYMODULE'), + ('numpy._typing._ufunc', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_ufunc.py', + 'PYMODULE'), + ('numpy._typing._scalars', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_scalars.py', + 'PYMODULE'), + ('numpy._typing._nbit', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nbit.py', + 'PYMODULE'), + ('numpy._typing._dtype_like', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_dtype_like.py', + 'PYMODULE'), + ('numpy._typing._char_codes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_char_codes.py', + 'PYMODULE'), + ('numpy.f2py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\__init__.py', + 'PYMODULE'), + ('numpy.f2py.diagnose', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\diagnose.py', + 'PYMODULE'), + ('numpy.f2py.f2py2e', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\f2py2e.py', + 'PYMODULE'), + ('numpy.f2py.crackfortran', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\crackfortran.py', + 'PYMODULE'), + ('platform', 'C:\\Python312\\Lib\\platform.py', 'PYMODULE'), + ('fileinput', 'C:\\Python312\\Lib\\fileinput.py', 'PYMODULE'), + ('numpy.f2py._backends', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\__init__.py', + 'PYMODULE'), + ('numpy.f2py._backends._meson', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\_meson.py', + 'PYMODULE'), + ('numpy.f2py._backends._backend', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\_backend.py', + 'PYMODULE'), + ('numpy.f2py.rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\rules.py', + 'PYMODULE'), + ('numpy.f2py.use_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\use_rules.py', + 'PYMODULE'), + ('numpy.f2py.f90mod_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\f90mod_rules.py', + 'PYMODULE'), + ('numpy.f2py.common_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\common_rules.py', + 'PYMODULE'), + ('numpy.f2py.func2subr', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\func2subr.py', + 'PYMODULE'), + ('numpy.f2py._isocbind', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_isocbind.py', + 'PYMODULE'), + ('numpy.f2py.auxfuncs', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\auxfuncs.py', + 'PYMODULE'), + ('numpy.f2py.symbolic', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\symbolic.py', + 'PYMODULE'), + ('numpy.f2py.cb_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\cb_rules.py', + 'PYMODULE'), + ('numpy.f2py.capi_maps', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\capi_maps.py', + 'PYMODULE'), + ('numpy.f2py.cfuncs', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\cfuncs.py', + 'PYMODULE'), + ('numpy.f2py.__version__', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\__version__.py', + 'PYMODULE'), + ('numpy.matlib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matlib.py', + 'PYMODULE'), + ('numpy.matrixlib.defmatrix', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matrixlib\\defmatrix.py', + 'PYMODULE'), + ('numpy.testing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\__init__.py', + 'PYMODULE'), + ('numpy.testing._private.utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\utils.py', + 'PYMODULE'), + ('doctest', 'C:\\Python312\\Lib\\doctest.py', 'PYMODULE'), + ('pdb', 'C:\\Python312\\Lib\\pdb.py', 'PYMODULE'), + ('pydoc', 'C:\\Python312\\Lib\\pydoc.py', 'PYMODULE'), + ('webbrowser', 'C:\\Python312\\Lib\\webbrowser.py', 'PYMODULE'), + ('http.server', 'C:\\Python312\\Lib\\http\\server.py', 'PYMODULE'), + ('socketserver', 'C:\\Python312\\Lib\\socketserver.py', 'PYMODULE'), + ('html', 'C:\\Python312\\Lib\\html\\__init__.py', 'PYMODULE'), + ('html.entities', 'C:\\Python312\\Lib\\html\\entities.py', 'PYMODULE'), + ('pydoc_data.topics', + 'C:\\Python312\\Lib\\pydoc_data\\topics.py', + 'PYMODULE'), + ('pydoc_data', 'C:\\Python312\\Lib\\pydoc_data\\__init__.py', 'PYMODULE'), + ('tty', 'C:\\Python312\\Lib\\tty.py', 'PYMODULE'), + ('shlex', 'C:\\Python312\\Lib\\shlex.py', 'PYMODULE'), + ('glob', 'C:\\Python312\\Lib\\glob.py', 'PYMODULE'), + ('code', 'C:\\Python312\\Lib\\code.py', 'PYMODULE'), + ('codeop', 'C:\\Python312\\Lib\\codeop.py', 'PYMODULE'), + ('bdb', 'C:\\Python312\\Lib\\bdb.py', 'PYMODULE'), + ('cmd', 'C:\\Python312\\Lib\\cmd.py', 'PYMODULE'), + ('difflib', 'C:\\Python312\\Lib\\difflib.py', 'PYMODULE'), + ('unittest.case', 'C:\\Python312\\Lib\\unittest\\case.py', 'PYMODULE'), + ('unittest._log', 'C:\\Python312\\Lib\\unittest\\_log.py', 'PYMODULE'), + ('unittest.util', 'C:\\Python312\\Lib\\unittest\\util.py', 'PYMODULE'), + ('unittest.result', 'C:\\Python312\\Lib\\unittest\\result.py', 'PYMODULE'), + ('sysconfig', 'C:\\Python312\\Lib\\sysconfig.py', 'PYMODULE'), + ('_aix_support', 'C:\\Python312\\Lib\\_aix_support.py', 'PYMODULE'), + ('concurrent.futures', + 'C:\\Python312\\Lib\\concurrent\\futures\\__init__.py', + 'PYMODULE'), + ('concurrent.futures.thread', + 'C:\\Python312\\Lib\\concurrent\\futures\\thread.py', + 'PYMODULE'), + ('concurrent.futures.process', + 'C:\\Python312\\Lib\\concurrent\\futures\\process.py', + 'PYMODULE'), + ('concurrent.futures._base', + 'C:\\Python312\\Lib\\concurrent\\futures\\_base.py', + 'PYMODULE'), + ('concurrent', 'C:\\Python312\\Lib\\concurrent\\__init__.py', 'PYMODULE'), + ('numpy.testing._private.extbuild', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\extbuild.py', + 'PYMODULE'), + ('numpy.testing.overrides', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\overrides.py', + 'PYMODULE'), + ('numpy.lib.recfunctions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\recfunctions.py', + 'PYMODULE'), + ('numpy.lib._iotools', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_iotools.py', + 'PYMODULE'), + ('numpy.ma.mrecords', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\mrecords.py', + 'PYMODULE'), + ('numpy.testing._private', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\__init__.py', + 'PYMODULE'), + ('unittest', 'C:\\Python312\\Lib\\unittest\\__init__.py', 'PYMODULE'), + ('unittest.async_case', + 'C:\\Python312\\Lib\\unittest\\async_case.py', + 'PYMODULE'), + ('asyncio', 'C:\\Python312\\Lib\\asyncio\\__init__.py', 'PYMODULE'), + ('asyncio.unix_events', + 'C:\\Python312\\Lib\\asyncio\\unix_events.py', + 'PYMODULE'), + ('asyncio.log', 'C:\\Python312\\Lib\\asyncio\\log.py', 'PYMODULE'), + ('asyncio.windows_events', + 'C:\\Python312\\Lib\\asyncio\\windows_events.py', + 'PYMODULE'), + ('asyncio.windows_utils', + 'C:\\Python312\\Lib\\asyncio\\windows_utils.py', + 'PYMODULE'), + ('asyncio.selector_events', + 'C:\\Python312\\Lib\\asyncio\\selector_events.py', + 'PYMODULE'), + ('asyncio.proactor_events', + 'C:\\Python312\\Lib\\asyncio\\proactor_events.py', + 'PYMODULE'), + ('asyncio.base_subprocess', + 'C:\\Python312\\Lib\\asyncio\\base_subprocess.py', + 'PYMODULE'), + ('asyncio.threads', 'C:\\Python312\\Lib\\asyncio\\threads.py', 'PYMODULE'), + ('asyncio.taskgroups', + 'C:\\Python312\\Lib\\asyncio\\taskgroups.py', + 'PYMODULE'), + ('asyncio.subprocess', + 'C:\\Python312\\Lib\\asyncio\\subprocess.py', + 'PYMODULE'), + ('asyncio.streams', 'C:\\Python312\\Lib\\asyncio\\streams.py', 'PYMODULE'), + ('asyncio.queues', 'C:\\Python312\\Lib\\asyncio\\queues.py', 'PYMODULE'), + ('asyncio.runners', 'C:\\Python312\\Lib\\asyncio\\runners.py', 'PYMODULE'), + ('asyncio.base_events', + 'C:\\Python312\\Lib\\asyncio\\base_events.py', + 'PYMODULE'), + ('asyncio.trsock', 'C:\\Python312\\Lib\\asyncio\\trsock.py', 'PYMODULE'), + ('asyncio.staggered', + 'C:\\Python312\\Lib\\asyncio\\staggered.py', + 'PYMODULE'), + ('asyncio.timeouts', 'C:\\Python312\\Lib\\asyncio\\timeouts.py', 'PYMODULE'), + ('asyncio.tasks', 'C:\\Python312\\Lib\\asyncio\\tasks.py', 'PYMODULE'), + ('asyncio.base_tasks', + 'C:\\Python312\\Lib\\asyncio\\base_tasks.py', + 'PYMODULE'), + ('asyncio.locks', 'C:\\Python312\\Lib\\asyncio\\locks.py', 'PYMODULE'), + ('asyncio.mixins', 'C:\\Python312\\Lib\\asyncio\\mixins.py', 'PYMODULE'), + ('asyncio.sslproto', 'C:\\Python312\\Lib\\asyncio\\sslproto.py', 'PYMODULE'), + ('asyncio.transports', + 'C:\\Python312\\Lib\\asyncio\\transports.py', + 'PYMODULE'), + ('asyncio.protocols', + 'C:\\Python312\\Lib\\asyncio\\protocols.py', + 'PYMODULE'), + ('asyncio.futures', 'C:\\Python312\\Lib\\asyncio\\futures.py', 'PYMODULE'), + ('asyncio.base_futures', + 'C:\\Python312\\Lib\\asyncio\\base_futures.py', + 'PYMODULE'), + ('asyncio.exceptions', + 'C:\\Python312\\Lib\\asyncio\\exceptions.py', + 'PYMODULE'), + ('asyncio.events', 'C:\\Python312\\Lib\\asyncio\\events.py', 'PYMODULE'), + ('asyncio.format_helpers', + 'C:\\Python312\\Lib\\asyncio\\format_helpers.py', + 'PYMODULE'), + ('asyncio.coroutines', + 'C:\\Python312\\Lib\\asyncio\\coroutines.py', + 'PYMODULE'), + ('asyncio.constants', + 'C:\\Python312\\Lib\\asyncio\\constants.py', + 'PYMODULE'), + ('unittest.signals', 'C:\\Python312\\Lib\\unittest\\signals.py', 'PYMODULE'), + ('unittest.main', 'C:\\Python312\\Lib\\unittest\\main.py', 'PYMODULE'), + ('unittest.runner', 'C:\\Python312\\Lib\\unittest\\runner.py', 'PYMODULE'), + ('unittest.loader', 'C:\\Python312\\Lib\\unittest\\loader.py', 'PYMODULE'), + ('unittest.suite', 'C:\\Python312\\Lib\\unittest\\suite.py', 'PYMODULE'), + ('numpy.exceptions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\exceptions.py', + 'PYMODULE'), + ('numpy.ctypeslib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ctypeslib\\__init__.py', + 'PYMODULE'), + ('numpy.ctypeslib._ctypeslib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ctypeslib\\_ctypeslib.py', + 'PYMODULE'), + ('numpy._core._internal', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_internal.py', + 'PYMODULE'), + ('numpy.ma', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\__init__.py', + 'PYMODULE'), + ('numpy.ma.extras', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\extras.py', + 'PYMODULE'), + ('numpy.lib.array_utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\array_utils.py', + 'PYMODULE'), + ('numpy.lib._array_utils_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_array_utils_impl.py', + 'PYMODULE'), + ('numpy.ma.core', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\core.py', + 'PYMODULE'), + ('numpy.polynomial', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\__init__.py', + 'PYMODULE'), + ('numpy.polynomial._polybase', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\_polybase.py', + 'PYMODULE'), + ('numpy.polynomial.polynomial', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\polynomial.py', + 'PYMODULE'), + ('numpy.linalg._linalg', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\_linalg.py', + 'PYMODULE'), + ('numpy.polynomial.legendre', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\legendre.py', + 'PYMODULE'), + ('numpy.polynomial.laguerre', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\laguerre.py', + 'PYMODULE'), + ('numpy.polynomial.hermite_e', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\hermite_e.py', + 'PYMODULE'), + ('numpy.polynomial.hermite', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\hermite.py', + 'PYMODULE'), + ('numpy.polynomial.chebyshev', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\chebyshev.py', + 'PYMODULE'), + ('numpy.polynomial.polyutils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\polyutils.py', + 'PYMODULE'), + ('numpy.random', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\__init__.py', + 'PYMODULE'), + ('numpy.random._pickle', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_pickle.py', + 'PYMODULE'), + ('numpy.fft', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\__init__.py', + 'PYMODULE'), + ('numpy.fft._pocketfft', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_pocketfft.py', + 'PYMODULE'), + ('numpy.fft._helper', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_helper.py', + 'PYMODULE'), + ('numpy.linalg', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\__init__.py', + 'PYMODULE'), + ('numpy._array_api_info', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_array_api_info.py', + 'PYMODULE'), + ('numpy.lib._utils_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_utils_impl.py', + 'PYMODULE'), + ('numpy.lib._ufunclike_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_ufunclike_impl.py', + 'PYMODULE'), + ('numpy.lib._type_check_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_type_check_impl.py', + 'PYMODULE'), + ('numpy._core.getlimits', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\getlimits.py', + 'PYMODULE'), + ('numpy.lib._twodim_base_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_twodim_base_impl.py', + 'PYMODULE'), + ('numpy.lib._stride_tricks_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py', + 'PYMODULE'), + ('numpy.lib._shape_base_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_shape_base_impl.py', + 'PYMODULE'), + ('numpy.lib._polynomial_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_polynomial_impl.py', + 'PYMODULE'), + ('numpy.lib._npyio_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_npyio_impl.py', + 'PYMODULE'), + ('numpy.lib._format_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_format_impl.py', + 'PYMODULE'), + ('numpy.lib._datasource', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_datasource.py', + 'PYMODULE'), + ('numpy.lib.format', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\format.py', + 'PYMODULE'), + ('numpy.lib._nanfunctions_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_nanfunctions_impl.py', + 'PYMODULE'), + ('numpy.lib._index_tricks_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_index_tricks_impl.py', + 'PYMODULE'), + ('numpy.lib._histograms_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_histograms_impl.py', + 'PYMODULE'), + ('numpy.lib._function_base_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_function_base_impl.py', + 'PYMODULE'), + ('numpy.lib._arraysetops_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arraysetops_impl.py', + 'PYMODULE'), + ('numpy.lib._arraypad_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arraypad_impl.py', + 'PYMODULE'), + ('numpy.lib.scimath', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\scimath.py', + 'PYMODULE'), + ('numpy.lib._scimath_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_scimath_impl.py', + 'PYMODULE'), + ('numpy.matrixlib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matrixlib\\__init__.py', + 'PYMODULE'), + ('numpy.lib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\__init__.py', + 'PYMODULE'), + ('numpy.lib.stride_tricks', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\stride_tricks.py', + 'PYMODULE'), + ('numpy.lib.npyio', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\npyio.py', + 'PYMODULE'), + ('numpy.lib.mixins', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\mixins.py', + 'PYMODULE'), + ('numpy.lib.introspect', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\introspect.py', + 'PYMODULE'), + ('numpy.lib._version', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_version.py', + 'PYMODULE'), + ('numpy.lib._arrayterator_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arrayterator_impl.py', + 'PYMODULE'), + ('numpy._core.function_base', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\function_base.py', + 'PYMODULE'), + ('numpy._core.printoptions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\printoptions.py', + 'PYMODULE'), + ('numpy._core.memmap', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\memmap.py', + 'PYMODULE'), + ('numpy._core', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\__init__.py', + 'PYMODULE'), + ('numpy._core._dtype', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_dtype.py', + 'PYMODULE'), + ('numpy._core._add_newdocs_scalars', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_add_newdocs_scalars.py', + 'PYMODULE'), + ('numpy._core._add_newdocs', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_add_newdocs.py', + 'PYMODULE'), + ('numpy._core.einsumfunc', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\einsumfunc.py', + 'PYMODULE'), + ('numpy.__config__', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\__config__.py', + 'PYMODULE'), + ('numpy._distributor_init', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_distributor_init.py', + 'PYMODULE'), + ('numpy._globals', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_globals.py', + 'PYMODULE'), + ('numpy.dtypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\dtypes.py', + 'PYMODULE'), + ('numpy._expired_attrs_2_0', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_expired_attrs_2_0.py', + 'PYMODULE'), + ('numpy.version', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\version.py', + 'PYMODULE'), + ('numpy.core.multiarray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\multiarray.py', + 'PYMODULE'), + ('typing', 'C:\\Python312\\Lib\\typing.py', 'PYMODULE'), + ('pathlib', 'C:\\Python312\\Lib\\pathlib.py', 'PYMODULE'), + ('datetime', 'C:\\Python312\\Lib\\datetime.py', 'PYMODULE'), + ('_pydatetime', 'C:\\Python312\\Lib\\_pydatetime.py', 'PYMODULE'), + ('_strptime', 'C:\\Python312\\Lib\\_strptime.py', 'PYMODULE'), + ('dataclasses', 'C:\\Python312\\Lib\\dataclasses.py', 'PYMODULE'), + ('shutil', 'C:\\Python312\\Lib\\shutil.py', 'PYMODULE'), + ('tarfile', 'C:\\Python312\\Lib\\tarfile.py', 'PYMODULE'), + ('json', 'C:\\Python312\\Lib\\json\\__init__.py', 'PYMODULE'), + ('json.encoder', 'C:\\Python312\\Lib\\json\\encoder.py', 'PYMODULE'), + ('json.decoder', 'C:\\Python312\\Lib\\json\\decoder.py', 'PYMODULE'), + ('json.scanner', 'C:\\Python312\\Lib\\json\\scanner.py', 'PYMODULE'), + ('csv', 'C:\\Python312\\Lib\\csv.py', 'PYMODULE'), + ('argparse', 'C:\\Python312\\Lib\\argparse.py', 'PYMODULE'), + ('__future__', 'C:\\Python312\\Lib\\__future__.py', 'PYMODULE')], + [('cv2\\opencv_videoio_ffmpeg500_64.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg500_64.dll', + 'BINARY'), + ('python312.dll', 'C:\\Python312\\python312.dll', 'BINARY'), + ('numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll', + 'BINARY'), + ('numpy.libs\\libscipy_openblas64_-327b2e0bcffce2882e0dc04cdeb4eaa6.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy.libs\\libscipy_openblas64_-327b2e0bcffce2882e0dc04cdeb4eaa6.dll', + 'BINARY'), + ('select.pyd', 'C:\\Python312\\DLLs\\select.pyd', 'EXTENSION'), + ('_multiprocessing.pyd', + 'C:\\Python312\\DLLs\\_multiprocessing.pyd', + 'EXTENSION'), + ('pyexpat.pyd', 'C:\\Python312\\DLLs\\pyexpat.pyd', 'EXTENSION'), + ('_ssl.pyd', 'C:\\Python312\\DLLs\\_ssl.pyd', 'EXTENSION'), + ('_hashlib.pyd', 'C:\\Python312\\DLLs\\_hashlib.pyd', 'EXTENSION'), + ('unicodedata.pyd', 'C:\\Python312\\DLLs\\unicodedata.pyd', 'EXTENSION'), + ('_decimal.pyd', 'C:\\Python312\\DLLs\\_decimal.pyd', 'EXTENSION'), + ('_socket.pyd', 'C:\\Python312\\DLLs\\_socket.pyd', 'EXTENSION'), + ('_ctypes.pyd', 'C:\\Python312\\DLLs\\_ctypes.pyd', 'EXTENSION'), + ('_queue.pyd', 'C:\\Python312\\DLLs\\_queue.pyd', 'EXTENSION'), + ('_lzma.pyd', 'C:\\Python312\\DLLs\\_lzma.pyd', 'EXTENSION'), + ('_bz2.pyd', 'C:\\Python312\\DLLs\\_bz2.pyd', 'EXTENSION'), + ('cv2\\cv2.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\cv2.pyd', + 'EXTENSION'), + ('_tkinter.pyd', 'C:\\Python312\\DLLs\\_tkinter.pyd', 'EXTENSION'), + ('numpy\\_core\\_multiarray_tests.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_multiarray_tests.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\_core\\_multiarray_umath.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_multiarray_umath.cp312-win_amd64.pyd', + 'EXTENSION'), + ('_wmi.pyd', 'C:\\Python312\\DLLs\\_wmi.pyd', 'EXTENSION'), + ('numpy\\linalg\\_umath_linalg.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\_umath_linalg.cp312-win_amd64.pyd', + 'EXTENSION'), + ('_overlapped.pyd', 'C:\\Python312\\DLLs\\_overlapped.pyd', 'EXTENSION'), + ('_asyncio.pyd', 'C:\\Python312\\DLLs\\_asyncio.pyd', 'EXTENSION'), + ('numpy\\random\\mtrand.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\mtrand.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\bit_generator.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\bit_generator.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_sfc64.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_sfc64.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_philox.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_philox.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_pcg64.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_pcg64.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_mt19937.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_mt19937.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_generator.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_generator.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_common.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_common.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_bounded_integers.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_bounded_integers.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\fft\\_pocketfft_umath.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_pocketfft_umath.cp312-win_amd64.pyd', + 'EXTENSION'), + ('api-ms-win-crt-string-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-string-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-process-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-process-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-stdio-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-stdio-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-math-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-math-l1-1-0.dll', + 'BINARY'), + ('VCRUNTIME140.dll', 'C:\\Python312\\VCRUNTIME140.dll', 'BINARY'), + ('api-ms-win-crt-conio-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-conio-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-convert-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-convert-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-environment-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-environment-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-time-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-time-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-filesystem-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-filesystem-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-heap-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-heap-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-locale-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-locale-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-runtime-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-runtime-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-utility-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-utility-l1-1-0.dll', + 'BINARY'), + ('VCRUNTIME140_1.dll', 'C:\\Python312\\VCRUNTIME140_1.dll', 'BINARY'), + ('api-ms-win-crt-private-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-private-l1-1-0.dll', + 'BINARY'), + ('libssl-3.dll', 'C:\\Python312\\DLLs\\libssl-3.dll', 'BINARY'), + ('libcrypto-3.dll', 'C:\\Python312\\DLLs\\libcrypto-3.dll', 'BINARY'), + ('libffi-8.dll', 'C:\\Python312\\DLLs\\libffi-8.dll', 'BINARY'), + ('python3.dll', 'C:\\Python312\\python3.dll', 'BINARY'), + ('tcl86t.dll', 'C:\\Python312\\DLLs\\tcl86t.dll', 'BINARY'), + ('tk86t.dll', 'C:\\Python312\\DLLs\\tk86t.dll', 'BINARY'), + ('ucrtbase.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\ucrtbase.dll', + 'BINARY'), + ('zlib1.dll', 'C:\\Python312\\DLLs\\zlib1.dll', 'BINARY'), + ('api-ms-win-core-file-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-file-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-sysinfo-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-sysinfo-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-file-l1-2-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-file-l1-2-0.dll', + 'BINARY'), + ('api-ms-win-core-processthreads-l1-1-1.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-processthreads-l1-1-1.dll', + 'BINARY'), + ('api-ms-win-core-processenvironment-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-processenvironment-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-libraryloader-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-libraryloader-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-datetime-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-datetime-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-memory-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-memory-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-synch-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-synch-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-debug-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-debug-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-rtlsupport-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-rtlsupport-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-synch-l1-2-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-synch-l1-2-0.dll', + 'BINARY'), + ('api-ms-win-core-string-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-string-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-interlocked-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-interlocked-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-util-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-util-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-processthreads-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-processthreads-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-file-l2-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-file-l2-1-0.dll', + 'BINARY'), + ('api-ms-win-core-profile-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-profile-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-heap-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-heap-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-console-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-console-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-handle-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-handle-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-timezone-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-timezone-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-namedpipe-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-namedpipe-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-errorhandling-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-errorhandling-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-fibers-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-fibers-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-localization-l1-2-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-localization-l1-2-0.dll', + 'BINARY')], + [], + [], + [('cv2\\Error\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\Error\\__init__.pyi', + 'DATA'), + ('cv2\\LICENSE-3RD-PARTY.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\LICENSE-3RD-PARTY.txt', + 'DATA'), + ('cv2\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\LICENSE.txt', + 'DATA'), + ('cv2\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\__init__.py', + 'DATA'), + ('cv2\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\__init__.pyi', + 'DATA'), + ('cv2\\aruco\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\aruco\\__init__.pyi', + 'DATA'), + ('cv2\\barcode\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\barcode\\__init__.pyi', + 'DATA'), + ('cv2\\ccm\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ccm\\__init__.pyi', + 'DATA'), + ('cv2\\config-3.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config-3.py', + 'DATA'), + ('cv2\\config.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config.py', + 'DATA'), + ('cv2\\cuda\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\cuda\\__init__.pyi', + 'DATA'), + ('cv2\\data\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\data\\__init__.py', + 'DATA'), + ('cv2\\detail\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\detail\\__init__.pyi', + 'DATA'), + ('cv2\\dnn\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\dnn\\__init__.pyi', + 'DATA'), + ('cv2\\fisheye\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\fisheye\\__init__.pyi', + 'DATA'), + ('cv2\\flann\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\flann\\__init__.pyi', + 'DATA'), + ('cv2\\instr\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\instr\\__init__.pyi', + 'DATA'), + ('cv2\\ipp\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ipp\\__init__.pyi', + 'DATA'), + ('cv2\\load_config_py2.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py2.py', + 'DATA'), + ('cv2\\load_config_py3.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py3.py', + 'DATA'), + ('cv2\\mat_wrapper\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\mat_wrapper\\__init__.py', + 'DATA'), + ('cv2\\mcc\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\mcc\\__init__.pyi', + 'DATA'), + ('cv2\\misc\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\__init__.py', + 'DATA'), + ('cv2\\misc\\version.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\version.py', + 'DATA'), + ('cv2\\ocl\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ocl\\__init__.pyi', + 'DATA'), + ('cv2\\ogl\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ogl\\__init__.pyi', + 'DATA'), + ('cv2\\parallel\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\parallel\\__init__.pyi', + 'DATA'), + ('cv2\\py.typed', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\py.typed', + 'DATA'), + ('cv2\\samples\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\samples\\__init__.pyi', + 'DATA'), + ('cv2\\segmentation\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\segmentation\\__init__.pyi', + 'DATA'), + ('cv2\\typing\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\typing\\__init__.py', + 'DATA'), + ('cv2\\utils\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\__init__.py', + 'DATA'), + ('cv2\\utils\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\fs\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\fs\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\logging\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\logging\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\nested\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\nested\\__init__.pyi', + 'DATA'), + ('cv2\\version.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\version.py', + 'DATA'), + ('cv2\\videoio_registry\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\videoio_registry\\__init__.pyi', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\INSTALLER', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\INSTALLER', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\LICENSE-3RD-PARTY.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\LICENSE-3RD-PARTY.txt', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\LICENSE.txt', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\METADATA', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\METADATA', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\RECORD', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\RECORD', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\REQUESTED', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\REQUESTED', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\WHEEL', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\WHEEL', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\top_level.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\top_level.txt', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\London', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\London', + 'DATA'), + ('_tcl_data\\encoding\\gb2312-raw.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb2312-raw.enc', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\HST10', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\HST10', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Johns', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Johns', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Port-au-Prince', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Port-au-Prince', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tomsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tomsk', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Tell_City', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Tell_City', + 'DATA'), + ('_tcl_data\\msgs\\ru.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ru.msg', + 'DATA'), + ('_tcl_data\\tzdata\\UCT', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\UCT', 'DATA'), + ('_tcl_data\\tzdata\\America\\Anchorage', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Anchorage', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\DumontDUrville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\DumontDUrville', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Canary', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Canary', + 'DATA'), + ('_tcl_data\\msgs\\en_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santa_Isabel', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santa_Isabel', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Nairobi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Nairobi', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Windhoek', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Windhoek', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Ensenada', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Ensenada', + 'DATA'), + ('_tk_data\\images\\logoMed.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logoMed.gif', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Dar_es_Salaam', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Dar_es_Salaam', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Knox_IN', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Knox_IN', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Barbados', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Barbados', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Bucharest', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Bucharest', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Chita', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Chita', + 'DATA'), + ('_tcl_data\\msgs\\th.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\th.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Barthelemy', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Barthelemy', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Port_of_Spain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Port_of_Spain', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Mountain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Mountain', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Brazzaville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Brazzaville', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Luanda', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Luanda', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Maceio', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Maceio', + 'DATA'), + ('_tk_data\\msgs\\it.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\it.msg', 'DATA'), + ('_tcl_data\\msgs\\es_sv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_sv.msg', + 'DATA'), + ('_tcl_data\\http1.0\\http.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\http1.0\\http.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Atyrau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Atyrau', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Cape_Verde', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Cape_Verde', + 'DATA'), + ('_tk_data\\pkgIndex.tcl', 'C:\\Python312\\tcl\\tk8.6\\pkgIndex.tcl', 'DATA'), + ('_tcl_data\\encoding\\euc-cn.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\euc-cn.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Jan_Mayen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Jan_Mayen', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Godthab', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Godthab', + 'DATA'), + ('_tcl_data\\msgs\\en_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_be.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Wake', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Wake', + 'DATA'), + ('_tcl_data\\msgs\\sr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sr.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Urumqi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Urumqi', + 'DATA'), + ('_tk_data\\ttk\\xpTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\xpTheme.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Swift_Current', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Swift_Current', + 'DATA'), + ('_tcl_data\\msgs\\en_ca.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_ca.msg', + 'DATA'), + ('_tcl_data\\msgs\\it_ch.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\it_ch.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+12', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+12', + 'DATA'), + ('_tcl_data\\msgs\\fa_ir.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fa_ir.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Saskatchewan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Saskatchewan', + 'DATA'), + ('_tk_data\\msgbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\msgbox.tcl', 'DATA'), + ('_tcl_data\\msgs\\ms_my.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ms_my.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Port_Moresby', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Port_Moresby', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Gibraltar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Gibraltar', + 'DATA'), + ('_tcl_data\\msgs\\en_zw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_zw.msg', + 'DATA'), + ('_tcl_data\\encoding\\iso2022-jp.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso2022-jp.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Volgograd', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Volgograd', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Casablanca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Casablanca', + 'DATA'), + ('_tk_data\\images\\logo.eps', + 'C:\\Python312\\tcl\\tk8.6\\images\\logo.eps', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Indiana-Starke', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Indiana-Starke', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Moscow', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Moscow', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Vincent', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Vincent', + 'DATA'), + ('_tcl_data\\encoding\\cp1257.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1257.enc', + 'DATA'), + ('tcl8\\8.6\\http-2.9.8.tm', + 'C:\\Python312\\tcl\\tcl8\\8.6\\http-2.9.8.tm', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Arizona', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Arizona', + 'DATA'), + ('_tcl_data\\encoding\\macDingbats.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macDingbats.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Maputo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Maputo', + 'DATA'), + ('_tcl_data\\tzdata\\ROK', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\ROK', 'DATA'), + ('_tcl_data\\msgs\\ar_jo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_jo.msg', + 'DATA'), + ('tcl8\\8.4\\platform-1.0.19.tm', + 'C:\\Python312\\tcl\\tcl8\\8.4\\platform-1.0.19.tm', + 'DATA'), + ('_tcl_data\\msgs\\nn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nn.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cancun', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cancun', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Irkutsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Irkutsk', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Regina', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Regina', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Ljubljana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Ljubljana', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Manila', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Manila', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Aqtobe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Aqtobe', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Noronha', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Noronha', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Johannesburg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Johannesburg', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Kinshasa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Kinshasa', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Libreville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Libreville', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yangon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yangon', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Nouakchott', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Nouakchott', + 'DATA'), + ('_tcl_data\\tzdata\\Iceland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Iceland', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-3.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-3.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-1.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-1.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Sitka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Sitka', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Punta_Arenas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Punta_Arenas', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Stanley', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Stanley', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Tegucigalpa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Tegucigalpa', + 'DATA'), + ('_tcl_data\\encoding\\cp775.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp775.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Christmas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Christmas', + 'DATA'), + ('_tcl_data\\tm.tcl', 'C:\\Python312\\tcl\\tcl8.6\\tm.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Catamarca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Catamarca', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Sao_Paulo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Sao_Paulo', + 'DATA'), + ('_tcl_data\\msgs\\hr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hr.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Jakarta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Jakarta', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Fortaleza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Fortaleza', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Marigot', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Marigot', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Antananarivo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Antananarivo', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Abidjan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Abidjan', + 'DATA'), + ('_tcl_data\\msgs\\ta_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ta_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-2', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-2', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Riyadh', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Riyadh', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\ComodRivadavia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\ComodRivadavia', + 'DATA'), + ('_tcl_data\\msgs\\es_mx.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_mx.msg', + 'DATA'), + ('_tcl_data\\msgs\\pt_br.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\pt_br.msg', + 'DATA'), + ('_tk_data\\msgs\\fi.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\fi.msg', 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Asmera', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Asmera', + 'DATA'), + ('_tk_data\\bgerror.tcl', 'C:\\Python312\\tcl\\tk8.6\\bgerror.tcl', 'DATA'), + ('_tcl_data\\msgs\\en_hk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_hk.msg', + 'DATA'), + ('_tcl_data\\msgs\\sl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sl.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Currie', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Currie', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Pyongyang', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Pyongyang', + 'DATA'), + ('_tcl_data\\tzdata\\Universal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Universal', + 'DATA'), + ('_tk_data\\megawidget.tcl', + 'C:\\Python312\\tcl\\tk8.6\\megawidget.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Araguaina', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Araguaina', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Isle_of_Man', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Isle_of_Man', + 'DATA'), + ('_tk_data\\iconlist.tcl', 'C:\\Python312\\tcl\\tk8.6\\iconlist.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Australia\\South', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\South', + 'DATA'), + ('_tcl_data\\msgs\\af_za.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\af_za.msg', + 'DATA'), + ('_tcl_data\\msgs\\sv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sv.msg', + 'DATA'), + ('_tk_data\\comdlg.tcl', 'C:\\Python312\\tcl\\tk8.6\\comdlg.tcl', 'DATA'), + ('_tcl_data\\tzdata\\GB', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GB', 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Douala', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Douala', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Managua', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Managua', + 'DATA'), + ('_tcl_data\\encoding\\cp949.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp949.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Chungking', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Chungking', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Broken_Hill', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Broken_Hill', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Grand_Turk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Grand_Turk', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Macquarie', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Macquarie', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Atlantic', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Atlantic', + 'DATA'), + ('_tcl_data\\msgs\\is.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\is.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kiev', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kiev', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Simferopol', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Simferopol', + 'DATA'), + ('_tcl_data\\msgs\\tr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\tr.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Whitehorse', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Whitehorse', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Yap', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Yap', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santo_Domingo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santo_Domingo', + 'DATA'), + ('_tcl_data\\tzdata\\Singapore', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Singapore', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Lucia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Lucia', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Muscat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Muscat', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\San_Juan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\San_Juan', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guatemala', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guatemala', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\DeNoronha', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\DeNoronha', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Dawson_Creek', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Dawson_Creek', + 'DATA'), + ('_tk_data\\images\\pwrdLogo75.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo75.gif', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Dominica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Dominica', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-9.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-9.enc', + 'DATA'), + ('_tk_data\\msgs\\pl.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\pl.msg', 'DATA'), + ('_tcl_data\\msgs\\ro.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ro.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Almaty', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Almaty', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Porto-Novo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Porto-Novo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Resolute', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Resolute', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Asuncion', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Asuncion', + 'DATA'), + ('_tcl_data\\word.tcl', 'C:\\Python312\\tcl\\tcl8.6\\word.tcl', 'DATA'), + ('_tcl_data\\tzdata\\MST7MDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\MST7MDT', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Coral_Harbour', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Coral_Harbour', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+10', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+10', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Hong_Kong', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Hong_Kong', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Azores', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Azores', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Palmer', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Palmer', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Magadan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Magadan', + 'DATA'), + ('_tcl_data\\msgs\\en_ie.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_ie.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Mexico_City', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Mexico_City', + 'DATA'), + ('_tcl_data\\encoding\\cp869.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp869.enc', + 'DATA'), + ('_tcl_data\\encoding\\euc-kr.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\euc-kr.enc', + 'DATA'), + ('_tk_data\\msgs\\hu.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\hu.msg', 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT0', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Bahrain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Bahrain', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Belgrade', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Belgrade', + 'DATA'), + ('_tk_data\\ttk\\spinbox.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\spinbox.tcl', + 'DATA'), + ('_tk_data\\button.tcl', 'C:\\Python312\\tcl\\tk8.6\\button.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Tijuana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Tijuana', + 'DATA'), + ('_tcl_data\\msgs\\en_au.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_au.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Brisbane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Brisbane', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+5', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+5', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\West', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\West', + 'DATA'), + ('_tcl_data\\msgs\\zh_tw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_tw.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Ouagadougou', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Ouagadougou', + 'DATA'), + ('_tcl_data\\tzdata\\W-SU', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\W-SU', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kosrae', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kosrae', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Chatham', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Chatham', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Fakaofo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Fakaofo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Shiprock', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Shiprock', + 'DATA'), + ('_tcl_data\\msgs\\af.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\af.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Dublin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Dublin', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Budapest', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Budapest', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Mauritius', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Mauritius', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Juneau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Juneau', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-7.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-7.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Tarawa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Tarawa', + 'DATA'), + ('_tk_data\\ttk\\utils.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\utils.tcl', + 'DATA'), + ('_tcl_data\\msgs\\uk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\uk.msg', + 'DATA'), + ('_tcl_data\\tzdata\\GMT+0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT+0', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Mawson', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Mawson', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Hermosillo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Hermosillo', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+6', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+6', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kamchatka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kamchatka', + 'DATA'), + ('_tcl_data\\encoding\\cp857.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp857.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Jamaica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Jamaica', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Athens', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Athens', + 'DATA'), + ('_tcl_data\\msgs\\id_id.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\id_id.msg', + 'DATA'), + ('_tk_data\\optMenu.tcl', 'C:\\Python312\\tcl\\tk8.6\\optMenu.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ho_Chi_Minh', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ho_Chi_Minh', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Samara', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Samara', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lubumbashi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lubumbashi', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Thomas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Thomas', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Porto_Acre', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Porto_Acre', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Winamac', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Winamac', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\UTC', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\UTC', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Freetown', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Freetown', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Bougainville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Bougainville', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Famagusta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Famagusta', + 'DATA'), + ('_tcl_data\\msgs\\es.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es.msg', + 'DATA'), + ('_tk_data\\license.terms', + 'C:\\Python312\\tcl\\tk8.6\\license.terms', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Gaza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Gaza', + 'DATA'), + ('_tk_data\\images\\logo100.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logo100.gif', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-7', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-7', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tashkent', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tashkent', + 'DATA'), + ('_tcl_data\\msgs\\en_gb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_gb.msg', + 'DATA'), + ('_tcl_data\\msgs\\ta.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ta.msg', + 'DATA'), + ('_tk_data\\msgs\\zh_cn.msg', + 'C:\\Python312\\tcl\\tk8.6\\msgs\\zh_cn.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Kampala', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Kampala', + 'DATA'), + ('_tk_data\\ttk\\combobox.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\combobox.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Makassar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Makassar', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vatican', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vatican', + 'DATA'), + ('_tcl_data\\msgs\\es_cr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_cr.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Faroe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Faroe', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Aqtau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Aqtau', + 'DATA'), + ('_tcl_data\\init.tcl', 'C:\\Python312\\tcl\\tcl8.6\\init.tcl', 'DATA'), + ('_tcl_data\\encoding\\cns11643.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cns11643.enc', + 'DATA'), + ('_tk_data\\ttk\\ttk.tcl', 'C:\\Python312\\tcl\\tk8.6\\ttk\\ttk.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Queensland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Queensland', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\LHI', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\LHI', + 'DATA'), + ('_tcl_data\\msgs\\fi.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fi.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Troll', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Troll', + 'DATA'), + ('_tcl_data\\tzdata\\Jamaica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Jamaica', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Comoro', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Comoro', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Reykjavik', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Reykjavik', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Qyzylorda', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Qyzylorda', + 'DATA'), + ('_tcl_data\\msgs\\ar_sy.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_sy.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Shanghai', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Shanghai', + 'DATA'), + ('_tk_data\\ttk\\cursors.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\cursors.tcl', + 'DATA'), + ('_tk_data\\ttk\\notebook.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\notebook.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yekaterinburg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yekaterinburg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Galapagos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Galapagos', + 'DATA'), + ('_tcl_data\\msgs\\kw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kw.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_co.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_co.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Anguilla', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Anguilla', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Martinique', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Martinique', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Ojinaga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Ojinaga', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Petersburg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Petersburg', + 'DATA'), + ('_tcl_data\\msgs\\es_ve.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ve.msg', + 'DATA'), + ('_tk_data\\scrlbar.tcl', 'C:\\Python312\\tcl\\tk8.6\\scrlbar.tcl', 'DATA'), + ('_tk_data\\ttk\\fonts.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\fonts.tcl', + 'DATA'), + ('_tk_data\\images\\pwrdLogo200.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo200.gif', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kathmandu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kathmandu', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Nicosia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Nicosia', + 'DATA'), + ('_tcl_data\\msgs\\mk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mk.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Luxembourg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Luxembourg', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Blantyre', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Blantyre', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+7', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+7', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Khandyga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Khandyga', + 'DATA'), + ('_tcl_data\\encoding\\shiftjis.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\shiftjis.enc', + 'DATA'), + ('_tcl_data\\msgs\\fo_fo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fo_fo.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\La_Rioja', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\La_Rioja', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lome', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Algiers', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Algiers', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Bratislava', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Bratislava', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Monaco', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Monaco', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Eastern', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Eastern', + 'DATA'), + ('_tcl_data\\msgs\\sq.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sq.msg', + 'DATA'), + ('_tk_data\\msgs\\eo.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\eo.msg', 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Copenhagen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Copenhagen', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Baku', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Baku', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Antigua', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Antigua', + 'DATA'), + ('_tcl_data\\encoding\\cp936.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp936.enc', + 'DATA'), + ('_tcl_data\\msgs\\be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\be.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Chongqing', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Chongqing', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Andorra', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Andorra', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Djibouti', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Djibouti', + 'DATA'), + ('_tcl_data\\msgs\\bn_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\bn_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\St_Helena', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\St_Helena', + 'DATA'), + ('_tcl_data\\msgs\\en_nz.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_nz.msg', + 'DATA'), + ('_tcl_data\\msgs\\eu_es.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\eu_es.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Amman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Amman', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\PST8PDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\PST8PDT', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Harbin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Harbin', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\Greenwich', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\Greenwich', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Creston', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Creston', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Inuvik', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Inuvik', + 'DATA'), + ('_tcl_data\\msgs\\kok_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kok_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Tahiti', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Tahiti', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Maldives', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Maldives', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Malabo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Malabo', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Sydney', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Sydney', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Chicago', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Chicago', + 'DATA'), + ('_tcl_data\\msgs\\ar.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Phoenix', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Phoenix', + 'DATA'), + ('_tcl_data\\msgs\\es_py.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_py.msg', + 'DATA'), + ('_tcl_data\\msgs\\hu.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hu.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Detroit', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Detroit', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Belize', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Belize', + 'DATA'), + ('_tk_data\\images\\tai-ku.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\tai-ku.gif', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Palau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Palau', + 'DATA'), + ('_tcl_data\\parray.tcl', 'C:\\Python312\\tcl\\tcl8.6\\parray.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Eucla', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Eucla', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kashgar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kashgar', + 'DATA'), + ('_tcl_data\\msgs\\es_pe.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_pe.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Fiji', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Fiji', + 'DATA'), + ('_tcl_data\\tzdata\\GMT-0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT-0', + 'DATA'), + ('_tcl_data\\encoding\\symbol.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\symbol.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Davis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Davis', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Kentucky\\Monticello', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Kentucky\\Monticello', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Ushuaia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Ushuaia', + 'DATA'), + ('_tcl_data\\encoding\\cp1253.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1253.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp862.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp862.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-10.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-10.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Eastern', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Eastern', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-15.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-15.enc', + 'DATA'), + ('_tcl_data\\tzdata\\EST5EDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\EST5EDT', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Zagreb', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Zagreb', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Cordoba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Cordoba', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Juba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Juba', + 'DATA'), + ('_tcl_data\\encoding\\cp874.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp874.enc', + 'DATA'), + ('_tcl_data\\msgs\\zh_cn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_cn.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Rome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Rome', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Niamey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Niamey', + 'DATA'), + ('_tk_data\\icons.tcl', 'C:\\Python312\\tcl\\tk8.6\\icons.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Thunder_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Thunder_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Johnston', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Johnston', + 'DATA'), + ('_tcl_data\\encoding\\cp932.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp932.enc', + 'DATA'), + ('_tcl_data\\package.tcl', 'C:\\Python312\\tcl\\tcl8.6\\package.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Lima', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Lima', + 'DATA'), + ('_tcl_data\\encoding\\macCroatian.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macCroatian.enc', + 'DATA'), + ('_tcl_data\\msgs\\fr_ca.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr_ca.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Yancowinna', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Yancowinna', + 'DATA'), + ('_tcl_data\\tzdata\\MET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\MET', 'DATA'), + ('_tcl_data\\msgs\\bg.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\bg.msg', + 'DATA'), + ('_tcl_data\\msgs\\ko.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ko.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Hebron', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Hebron', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-2.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-2.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Ponape', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Ponape', + 'DATA'), + ('_tk_data\\ttk\\altTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\altTheme.tcl', + 'DATA'), + ('_tcl_data\\opt0.4\\optparse.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\opt0.4\\optparse.tcl', + 'DATA'), + ('_tk_data\\msgs\\en_gb.msg', + 'C:\\Python312\\tcl\\tk8.6\\msgs\\en_gb.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Lord_Howe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Lord_Howe', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vaduz', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vaduz', + 'DATA'), + ('_tcl_data\\tzdata\\US\\East-Indiana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\East-Indiana', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\South_Georgia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\South_Georgia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Puerto_Rico', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Puerto_Rico', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Khartoum', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Khartoum', + 'DATA'), + ('_tcl_data\\tzdata\\Israel', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Israel', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Jujuy', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Jujuy', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Denver', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Denver', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Porto_Velho', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Porto_Velho', + 'DATA'), + ('_tcl_data\\msgs\\es_pr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_pr.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Truk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Truk', + 'DATA'), + ('_tk_data\\mkpsenc.tcl', 'C:\\Python312\\tcl\\tk8.6\\mkpsenc.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Bogota', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Bogota', + 'DATA'), + ('_tk_data\\safetk.tcl', 'C:\\Python312\\tcl\\tk8.6\\safetk.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Bahia_Banderas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Bahia_Banderas', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Pohnpei', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Pohnpei', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\North', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\North', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Chagos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Chagos', + 'DATA'), + ('_tk_data\\clrpick.tcl', 'C:\\Python312\\tcl\\tk8.6\\clrpick.tcl', 'DATA'), + ('_tk_data\\ttk\\button.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\button.tcl', + 'DATA'), + ('_tk_data\\ttk\\panedwindow.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\panedwindow.tcl', + 'DATA'), + ('_tcl_data\\msgs\\zh.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh.msg', + 'DATA'), + ('_tcl_data\\encoding\\cp865.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp865.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Reunion', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Reunion', + 'DATA'), + ('_tk_data\\choosedir.tcl', + 'C:\\Python312\\tcl\\tk8.6\\choosedir.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Lisbon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Lisbon', + 'DATA'), + ('_tk_data\\dialog.tcl', 'C:\\Python312\\tcl\\tk8.6\\dialog.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Brunei', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Brunei', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Brussels', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Brussels', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Grenada', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Grenada', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kirov', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kirov', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Madeira', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Madeira', + 'DATA'), + ('_tcl_data\\tzdata\\America\\North_Dakota\\New_Salem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\North_Dakota\\New_Salem', + 'DATA'), + ('tcl8\\8.4\\platform\\shell-1.1.4.tm', + 'C:\\Python312\\tcl\\tcl8\\8.4\\platform\\shell-1.1.4.tm', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kiritimati', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kiritimati', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Marengo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Marengo', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Sakhalin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Sakhalin', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\MST7', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\MST7', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Tongatapu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Tongatapu', + 'DATA'), + ('_tk_data\\msgs\\ru.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\ru.msg', 'DATA'), + ('_tcl_data\\msgs\\mt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mt.msg', + 'DATA'), + ('_tcl_data\\encoding\\gb1988.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb1988.enc', + 'DATA'), + ('_tcl_data\\msgs\\te_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\te_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tehran', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tehran', + 'DATA'), + ('_tk_data\\ttk\\defaults.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\defaults.tcl', + 'DATA'), + ('_tcl_data\\msgs\\es_gt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_gt.msg', + 'DATA'), + ('_tcl_data\\tzdata\\CET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\CET', 'DATA'), + ('_tcl_data\\tzdata\\GB-Eire', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GB-Eire', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Guadalcanal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Guadalcanal', + 'DATA'), + ('_tk_data\\ttk\\sizegrip.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\sizegrip.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Qostanay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Qostanay', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Jujuy', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Jujuy', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Easter', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Easter', + 'DATA'), + ('_tcl_data\\msgs\\lv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\lv.msg', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-6.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-6.enc', + 'DATA'), + ('_tcl_data\\tzdata\\GMT0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT0', + 'DATA'), + ('_tcl_data\\encoding\\cp737.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp737.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-16.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-16.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Cuba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Cuba', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+1', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+1', + 'DATA'), + ('_tcl_data\\msgs\\es_pa.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_pa.msg', + 'DATA'), + ('_tk_data\\ttk\\entry.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\entry.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ust-Nera', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ust-Nera', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-1', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-1', + 'DATA'), + ('_tcl_data\\encoding\\cp855.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp855.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Guernsey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Guernsey', + 'DATA'), + ('_tcl_data\\tclIndex', 'C:\\Python312\\tcl\\tcl8.6\\tclIndex', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Omsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Omsk', + 'DATA'), + ('_tcl_data\\msgs\\eu.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\eu.msg', + 'DATA'), + ('_tcl_data\\msgs\\sk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sk.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Dawson', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Dawson', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Buenos_Aires', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Buenos_Aires', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Rainy_River', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rainy_River', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Caracas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Caracas', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Wallis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Wallis', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Seoul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Seoul', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kanton', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kanton', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Tasmania', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Tasmania', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Vancouver', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Vancouver', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nassau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nassau', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guadeloupe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guadeloupe', + 'DATA'), + ('_tcl_data\\msgs\\en_sg.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_sg.msg', + 'DATA'), + ('_tcl_data\\encoding\\cp860.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp860.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Los_Angeles', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Los_Angeles', + 'DATA'), + ('_tcl_data\\tzdata\\Egypt', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Egypt', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lagos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lagos', + 'DATA'), + ('_tcl_data\\encoding\\iso2022-kr.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso2022-kr.enc', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Pacific', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Pacific', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Enderbury', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Enderbury', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Chuuk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Chuuk', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-13', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-13', + 'DATA'), + ('_tcl_data\\msgs\\es_bo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_bo.msg', + 'DATA'), + ('_tk_data\\ttk\\menubutton.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\menubutton.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Catamarca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Catamarca', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Dakar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Dakar', + 'DATA'), + ('_tcl_data\\tzdata\\America\\North_Dakota\\Center', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\North_Dakota\\Center', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Boa_Vista', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Boa_Vista', + 'DATA'), + ('_tcl_data\\tzdata\\America\\El_Salvador', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\El_Salvador', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Madrid', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Madrid', + 'DATA'), + ('_tcl_data\\msgs\\te.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\te.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Efate', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Efate', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-0', + 'DATA'), + ('_tcl_data\\tzdata\\America\\New_York', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\New_York', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-3', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-3', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Phnom_Penh', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Phnom_Penh', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Mendoza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Mendoza', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Salta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Salta', + 'DATA'), + ('_tcl_data\\msgs\\en_ph.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_ph.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ashkhabad', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ashkhabad', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\AST4', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\AST4', + 'DATA'), + ('_tcl_data\\msgs\\sw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sw.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Mayotte', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Mayotte', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Menominee', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Menominee', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Fort_Nelson', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Fort_Nelson', + 'DATA'), + ('_tcl_data\\msgs\\de_at.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\de_at.msg', + 'DATA'), + ('_tcl_data\\tzdata\\PST8PDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\PST8PDT', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\El_Aaiun', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\El_Aaiun', + 'DATA'), + ('_tcl_data\\encoding\\koi8-u.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\koi8-u.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Astrakhan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Astrakhan', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kaliningrad', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kaliningrad', + 'DATA'), + ('_tcl_data\\encoding\\ksc5601.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\ksc5601.enc', + 'DATA'), + ('_tcl_data\\encoding\\macCyrillic.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macCyrillic.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cordoba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cordoba', + 'DATA'), + ('_tcl_data\\msgs\\es_ec.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ec.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kwajalein', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kwajalein', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Amsterdam', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Amsterdam', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Adak', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Adak', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vilnius', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vilnius', + 'DATA'), + ('_tcl_data\\tzdata\\Turkey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Turkey', + 'DATA'), + ('_tcl_data\\tzdata\\Hongkong', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Hongkong', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Montreal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Montreal', + 'DATA'), + ('_tk_data\\listbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\listbox.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dhaka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dhaka', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Yakutat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Yakutat', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Ceuta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Ceuta', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\CST6', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\CST6', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Vladivostok', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Vladivostok', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Chisinau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Chisinau', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Pacific', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Pacific', + 'DATA'), + ('_tcl_data\\msgs\\es_cl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_cl.msg', + 'DATA'), + ('_tcl_data\\encoding\\jis0212.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\jis0212.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Norfolk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Norfolk', + 'DATA'), + ('_tcl_data\\msgs\\gv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gv.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Tirane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Tirane', + 'DATA'), + ('_tcl_data\\http1.0\\pkgIndex.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\http1.0\\pkgIndex.tcl', + 'DATA'), + ('tcl8\\8.5\\msgcat-1.6.1.tm', + 'C:\\Python312\\tcl\\tcl8\\8.5\\msgcat-1.6.1.tm', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kyiv', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kyiv', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Mogadishu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Mogadishu', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\Universal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\Universal', + 'DATA'), + ('_tk_data\\obsolete.tcl', 'C:\\Python312\\tcl\\tk8.6\\obsolete.tcl', 'DATA'), + ('_tk_data\\ttk\\vistaTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\vistaTheme.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bissau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bissau', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cayman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cayman', + 'DATA'), + ('_tcl_data\\msgs\\el.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\el.msg', + 'DATA'), + ('_tcl_data\\encoding\\macCentEuro.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macCentEuro.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Vevay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Vevay', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Rothera', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Rothera', + 'DATA'), + ('_tcl_data\\msgs\\es_ni.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ni.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\South_Pole', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\South_Pole', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Buenos_Aires', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Buenos_Aires', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Cocos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Cocos', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Miquelon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Miquelon', + 'DATA'), + ('_tcl_data\\tzdata\\Libya', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Libya', + 'DATA'), + ('_tcl_data\\msgs\\fa.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fa.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Kerguelen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Kerguelen', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ashgabat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ashgabat', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Baghdad', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Baghdad', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Bermuda', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Bermuda', + 'DATA'), + ('_tcl_data\\tzdata\\ROC', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\ROC', 'DATA'), + ('_tcl_data\\msgs\\he.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\he.msg', + 'DATA'), + ('_tk_data\\msgs\\de.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\de.msg', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Karachi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Karachi', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ulaanbaatar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ulaanbaatar', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kuwait', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kuwait', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Fort_Wayne', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Fort_Wayne', + 'DATA'), + ('_tcl_data\\encoding\\dingbats.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\dingbats.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Mahe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Mahe', + 'DATA'), + ('_tk_data\\panedwindow.tcl', + 'C:\\Python312\\tcl\\tk8.6\\panedwindow.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Mexico\\BajaNorte', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Mexico\\BajaNorte', + 'DATA'), + ('_tk_data\\console.tcl', 'C:\\Python312\\tcl\\tk8.6\\console.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Rosario', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rosario', + 'DATA'), + ('_tcl_data\\encoding\\gb12345.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb12345.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Jayapura', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Jayapura', + 'DATA'), + ('_tcl_data\\msgs\\ga_ie.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ga_ie.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Thule', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Thule', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Thimbu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Thimbu', + 'DATA'), + ('_tcl_data\\msgs\\es_hn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_hn.msg', + 'DATA'), + ('_tcl_data\\msgs\\nb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nb.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Eirunepe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Eirunepe', + 'DATA'), + ('_tcl_data\\tzdata\\NZ-CHAT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\NZ-CHAT', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Winnipeg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Winnipeg', + 'DATA'), + ('_tcl_data\\tzdata\\EET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\EET', 'DATA'), + ('_tcl_data\\opt0.4\\pkgIndex.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\opt0.4\\pkgIndex.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Arctic\\Longyearbyen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Arctic\\Longyearbyen', + 'DATA'), + ('_tk_data\\focus.tcl', 'C:\\Python312\\tcl\\tk8.6\\focus.tcl', 'DATA'), + ('_tk_data\\tclIndex', 'C:\\Python312\\tcl\\tk8.6\\tclIndex', 'DATA'), + ('_tcl_data\\tzdata\\Poland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Poland', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Pago_Pago', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Pago_Pago', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tokyo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tokyo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Pangnirtung', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Pangnirtung', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cuiaba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cuiaba', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Midway', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Midway', + 'DATA'), + ('_tcl_data\\msgs\\cs.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\cs.msg', + 'DATA'), + ('_tcl_data\\encoding\\macThai.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macThai.enc', + 'DATA'), + ('_tk_data\\palette.tcl', 'C:\\Python312\\tcl\\tk8.6\\palette.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Maseru', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Maseru', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+8', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+8', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Marquesas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Marquesas', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Qatar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Qatar', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Syowa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Syowa', + 'DATA'), + ('_tcl_data\\msgs\\sh.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sh.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Scoresbysund', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Scoresbysund', + 'DATA'), + ('_tcl_data\\msgs\\pt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\pt.msg', + 'DATA'), + ('_tcl_data\\encoding\\tis-620.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\tis-620.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Beirut', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Beirut', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dili', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dili', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Thimphu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Thimphu', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Calcutta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Calcutta', + 'DATA'), + ('_tcl_data\\msgs\\kw_gb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kw_gb.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Sofia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Sofia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\San_Luis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\San_Luis', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dubai', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dubai', + 'DATA'), + ('_tcl_data\\msgs\\ko_kr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ko_kr.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Metlakatla', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Metlakatla', + 'DATA'), + ('_tk_data\\ttk\\classicTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\classicTheme.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Taipei', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Taipei', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vienna', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vienna', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nipigon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nipigon', + 'DATA'), + ('_tk_data\\ttk\\scrollbar.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\scrollbar.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cayenne', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cayenne', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Goose_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Goose_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+9', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+9', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Saigon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Saigon', + 'DATA'), + ('_tcl_data\\msgs\\en_za.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_za.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Barnaul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Barnaul', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Jerusalem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Jerusalem', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kuala_Lumpur', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kuala_Lumpur', + 'DATA'), + ('_tcl_data\\encoding\\cp1255.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1255.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Iran', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Iran', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Virgin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Virgin', + 'DATA'), + ('_tcl_data\\encoding\\cp866.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp866.enc', + 'DATA'), + ('_tcl_data\\msgs\\fo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fo.msg', + 'DATA'), + ('_tcl_data\\encoding\\cp863.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp863.enc', + 'DATA'), + ('_tk_data\\images\\pwrdLogo.eps', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo.eps', + 'DATA'), + ('_tk_data\\images\\pwrdLogo175.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo175.gif', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-6', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-6', + 'DATA'), + ('_tcl_data\\tzdata\\Greenwich', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Greenwich', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Rio_Branco', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rio_Branco', + 'DATA'), + ('_tcl_data\\msgs\\gl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gl.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Krasnoyarsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Krasnoyarsk', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\YST9YDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\YST9YDT', + 'DATA'), + ('_tcl_data\\msgs\\id.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\id.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Mexico\\General', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Mexico\\General', + 'DATA'), + ('tcl8\\8.5\\tcltest-2.5.5.tm', + 'C:\\Python312\\tcl\\tcl8\\8.5\\tcltest-2.5.5.tm', + 'DATA'), + ('_tcl_data\\encoding\\macTurkish.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macTurkish.enc', + 'DATA'), + ('_tcl_data\\msgs\\fr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Costa_Rica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Costa_Rica', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Montserrat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Montserrat', + 'DATA'), + ('_tk_data\\ttk\\progress.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\progress.tcl', + 'DATA'), + ('_tcl_data\\msgs\\kok.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kok.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+2', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+2', + 'DATA'), + ('_tk_data\\images\\README', + 'C:\\Python312\\tcl\\tk8.6\\images\\README', + 'DATA'), + ('_tcl_data\\tzdata\\Zulu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Zulu', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Ulyanovsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Ulyanovsk', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Paramaribo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Paramaribo', + 'DATA'), + ('_tcl_data\\msgs\\ms.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ms.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Central', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Central', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Choibalsan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Choibalsan', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dacca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dacca', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\Zulu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\Zulu', + 'DATA'), + ('_tk_data\\ttk\\aquaTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\aquaTheme.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tbilisi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tbilisi', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\NSW', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\NSW', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kuching', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kuching', + 'DATA'), + ('_tcl_data\\msgs\\da.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\da.msg', + 'DATA'), + ('_tcl_data\\msgs\\nl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nl.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Macao', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Macao', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Montevideo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Montevideo', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\CST6CDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\CST6CDT', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Helsinki', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Helsinki', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guayaquil', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guayaquil', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Kitts', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Kitts', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Knox', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Knox', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\EST5EDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\EST5EDT', + 'DATA'), + ('_tk_data\\images\\logoLarge.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logoLarge.gif', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Tucuman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Tucuman', + 'DATA'), + ('_tcl_data\\tzdata\\Eire', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Eire', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Timbuktu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Timbuktu', + 'DATA'), + ('_tcl_data\\msgs\\fr_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr_be.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Japan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Japan', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Louisville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Louisville', + 'DATA'), + ('_tcl_data\\msgs\\vi.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\vi.msg', + 'DATA'), + ('_tcl_data\\encoding\\macRoman.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macRoman.enc', + 'DATA'), + ('_tk_data\\ttk\\clamTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\clamTheme.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-12', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-12', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Manaus', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Manaus', + 'DATA'), + ('_tcl_data\\encoding\\cp1252.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1252.enc', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\PST8', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\PST8', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Damascus', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Damascus', + 'DATA'), + ('_tcl_data\\encoding\\cp852.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp852.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-13.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-13.enc', + 'DATA'), + ('_tcl_data\\msgs\\pl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\pl.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Zurich', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Zurich', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Accra', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Accra', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Central', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Central', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\EST5', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\EST5', + 'DATA'), + ('_tcl_data\\encoding\\cp950.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp950.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Riga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Riga', + 'DATA'), + ('_tcl_data\\msgs\\hi.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hi.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-9', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-9', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Asmara', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Asmara', + 'DATA'), + ('_tcl_data\\encoding\\macIceland.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macIceland.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Aden', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Aden', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santiago', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santiago', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Noumea', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Noumea', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bujumbura', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bujumbura', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\UCT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\UCT', + 'DATA'), + ('_tcl_data\\encoding\\iso2022.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso2022.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\East', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\East', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indianapolis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indianapolis', + 'DATA'), + ('_tcl_data\\auto.tcl', 'C:\\Python312\\tcl\\tcl8.6\\auto.tcl', 'DATA'), + ('_tcl_data\\encoding\\iso8859-4.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-4.enc', + 'DATA'), + ('_tk_data\\msgs\\cs.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\cs.msg', 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Auckland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Auckland', + 'DATA'), + ('_tk_data\\msgs\\el.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\el.msg', 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Malta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Malta', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Cairo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Cairo', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Bishkek', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Bishkek', + 'DATA'), + ('_tcl_data\\encoding\\cp1254.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1254.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Edmonton', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Edmonton', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lusaka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lusaka', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Yukon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Yukon', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\YST9', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\YST9', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Srednekolymsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Srednekolymsk', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Nauru', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Nauru', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Nicosia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Nicosia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Bahia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Bahia', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Addis_Ababa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Addis_Ababa', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\West', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\West', + 'DATA'), + ('_tcl_data\\msgs\\es_do.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_do.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Honolulu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Honolulu', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Toronto', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Toronto', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Majuro', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Majuro', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Minsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Minsk', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Sao_Tome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Sao_Tome', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Warsaw', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Warsaw', + 'DATA'), + ('_tcl_data\\encoding\\jis0201.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\jis0201.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Anadyr', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Anadyr', + 'DATA'), + ('_tcl_data\\msgs\\ar_lb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_lb.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Apia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Apia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Aruba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Aruba', + 'DATA'), + ('_tcl_data\\tzdata\\GMT', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT', 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Funafuti', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Funafuti', + 'DATA'), + ('_tcl_data\\encoding\\big5.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\big5.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Oslo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Oslo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Curacao', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Curacao', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Recife', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Recife', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nuuk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nuuk', + 'DATA'), + ('_tcl_data\\msgs\\bn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\bn.msg', + 'DATA'), + ('_tk_data\\tkfbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\tkfbox.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\Danmarkshavn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Danmarkshavn', + 'DATA'), + ('_tcl_data\\msgs\\ru_ua.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ru_ua.msg', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-5.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-5.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ulan_Bator', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ulan_Bator', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\McMurdo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\McMurdo', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\AST4ADT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\AST4ADT', + 'DATA'), + ('_tk_data\\msgs\\da.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\da.msg', 'DATA'), + ('_tcl_data\\msgs\\fa_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fa_in.msg', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-11.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-11.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Lower_Princes', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Lower_Princes', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Moncton', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Moncton', + 'DATA'), + ('_tcl_data\\msgs\\de_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\de_be.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Atikokan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Atikokan', + 'DATA'), + ('_tcl_data\\encoding\\macRomania.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macRomania.enc', + 'DATA'), + ('_tk_data\\msgs\\es.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\es.msg', 'DATA'), + ('_tk_data\\spinbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\spinbox.tcl', 'DATA'), + ('_tcl_data\\tzdata\\America\\North_Dakota\\Beulah', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\North_Dakota\\Beulah', + 'DATA'), + ('_tcl_data\\clock.tcl', 'C:\\Python312\\tcl\\tcl8.6\\clock.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+4', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+4', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Pitcairn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Pitcairn', + 'DATA'), + ('_tcl_data\\msgs\\gv_gb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gv_gb.msg', + 'DATA'), + ('_tcl_data\\encoding\\cp1256.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1256.enc', + 'DATA'), + ('_tk_data\\tearoff.tcl', 'C:\\Python312\\tcl\\tk8.6\\tearoff.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Casey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Casey', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Boise', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Boise', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Havana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Havana', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Niue', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Niue', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Mendoza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Mendoza', + 'DATA'), + ('_tcl_data\\tzdata\\UTC', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\UTC', 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Gambier', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Gambier', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bangui', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bangui', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-8', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-8', + 'DATA'), + ('_tcl_data\\encoding\\cp1251.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1251.enc', + 'DATA'), + ('_tk_data\\xmfbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\xmfbox.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Mariehamn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Mariehamn', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Rangoon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Rangoon', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Jersey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Jersey', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+0', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Mountain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Mountain', + 'DATA'), + ('_tk_data\\unsupported.tcl', + 'C:\\Python312\\tcl\\tk8.6\\unsupported.tcl', + 'DATA'), + ('_tk_data\\msgs\\nl.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\nl.msg', 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Prague', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Prague', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Atka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Atka', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Rankin_Inlet', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rankin_Inlet', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Conakry', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Conakry', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Tallinn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Tallinn', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-5', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-5', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Mazatlan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Mazatlan', + 'DATA'), + ('_tcl_data\\msgs\\ga.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ga.msg', + 'DATA'), + ('_tk_data\\tk.tcl', 'C:\\Python312\\tcl\\tk8.6\\tk.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kabul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kabul', + 'DATA'), + ('_tcl_data\\msgs\\mr_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mr_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cambridge_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cambridge_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Michigan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Michigan', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Adelaide', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Adelaide', + 'DATA'), + ('_tcl_data\\encoding\\koi8-r.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\koi8-r.enc', + 'DATA'), + ('_tcl_data\\msgs\\zh_sg.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_sg.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\ACT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\ACT', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nome', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Paris', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Paris', + 'DATA'), + ('_tcl_data\\history.tcl', 'C:\\Python312\\tcl\\tcl8.6\\history.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Victoria', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Victoria', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-8.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-8.enc', + 'DATA'), + ('_tk_data\\fontchooser.tcl', + 'C:\\Python312\\tcl\\tk8.6\\fontchooser.tcl', + 'DATA'), + ('_tcl_data\\msgs\\mr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mr.msg', + 'DATA'), + ('_tcl_data\\msgs\\eo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\eo.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Kwajalein', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Kwajalein', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santarem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santarem', + 'DATA'), + ('_tcl_data\\tzdata\\WET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\WET', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Macau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Macau', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Faeroe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Faeroe', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guyana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guyana', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Colombo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Colombo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Blanc-Sablon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Blanc-Sablon', + 'DATA'), + ('_tcl_data\\encoding\\cp437.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp437.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp850.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp850.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Tiraspol', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Tiraspol', + 'DATA'), + ('_tcl_data\\msgs\\hi_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hi_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Indianapolis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Indianapolis', + 'DATA'), + ('_tk_data\\entry.tcl', 'C:\\Python312\\tcl\\tk8.6\\entry.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Kigali', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Kigali', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Tortola', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Tortola', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dushanbe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dushanbe', + 'DATA'), + ('_tcl_data\\msgs\\nl_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nl_be.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tel_Aviv', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tel_Aviv', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ujung_Pandang', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ujung_Pandang', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Aleutian', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Aleutian', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Gaborone', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Gaborone', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Tunis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Tunis', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Sarajevo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Sarajevo', + 'DATA'), + ('_tcl_data\\msgs\\lt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\lt.msg', + 'DATA'), + ('_tcl_data\\encoding\\cp864.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp864.enc', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Yellowknife', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Yellowknife', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Hawaii', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Hawaii', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Iqaluit', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Iqaluit', + 'DATA'), + ('_tcl_data\\msgs\\en_bw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_bw.msg', + 'DATA'), + ('_tk_data\\ttk\\treeview.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\treeview.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Ndjamena', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Ndjamena', + 'DATA'), + ('_tcl_data\\tzdata\\CST6CDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\CST6CDT', + 'DATA'), + ('_tcl_data\\tzdata\\PRC', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\PRC', 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Mbabane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Mbabane', + 'DATA'), + ('_tcl_data\\msgs\\it.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\it.msg', + 'DATA'), + ('_tcl_data\\msgs\\ar_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_in.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Canberra', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Canberra', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Perth', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Perth', + 'DATA'), + ('_tcl_data\\msgs\\kl_gl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kl_gl.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Campo_Grande', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Campo_Grande', + 'DATA'), + ('_tcl_data\\encoding\\ascii.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\ascii.enc', + 'DATA'), + ('_tk_data\\msgs\\en.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\en.msg', 'DATA'), + ('_tk_data\\msgs\\pt.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\pt.msg', 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Vincennes', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Vincennes', + 'DATA'), + ('_tk_data\\msgs\\fr.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\fr.msg', 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Banjul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Banjul', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-10', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-10', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Guam', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Guam', + 'DATA'), + ('_tcl_data\\tzdata\\NZ', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\NZ', 'DATA'), + ('_tk_data\\text.tcl', 'C:\\Python312\\tcl\\tk8.6\\text.tcl', 'DATA'), + ('_tcl_data\\msgs\\es_uy.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_uy.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Samoa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Samoa', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Berlin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Berlin', + 'DATA'), + ('_tcl_data\\tzdata\\America\\La_Paz', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\La_Paz', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Busingen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Busingen', + 'DATA'), + ('_tcl_data\\tzdata\\Mexico\\BajaSur', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Mexico\\BajaSur', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Melbourne', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Melbourne', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Podgorica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Podgorica', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Chihuahua', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Chihuahua', + 'DATA'), + ('_tcl_data\\encoding\\gb2312.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb2312.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Vostok', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Vostok', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Rio_Gallegos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Rio_Gallegos', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-14.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-14.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Newfoundland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Newfoundland', + 'DATA'), + ('_tcl_data\\msgs\\gl_es.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gl_es.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Belem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Belem', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Kralendijk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Kralendijk', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Harare', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Harare', + 'DATA'), + ('_tcl_data\\encoding\\jis0208.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\jis0208.enc', + 'DATA'), + ('_tk_data\\msgs\\sv.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\sv.msg', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kolkata', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kolkata', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Uzhgorod', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Uzhgorod', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Stockholm', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Stockholm', + 'DATA'), + ('_tcl_data\\tzdata\\Chile\\EasterIsland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Chile\\EasterIsland', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Novokuznetsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Novokuznetsk', + 'DATA'), + ('_tcl_data\\msgs\\de.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\de.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Halifax', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Halifax', + 'DATA'), + ('_tcl_data\\msgs\\es_ar.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ar.msg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Glace_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Glace_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Matamoros', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Matamoros', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Monterrey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Monterrey', + 'DATA'), + ('_tk_data\\ttk\\winTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\winTheme.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Alaska', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Alaska', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\Acre', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\Acre', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Oral', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Oral', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-14', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-14', + 'DATA'), + ('_tcl_data\\encoding\\cp1250.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1250.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-11', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-11', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Merida', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Merida', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Belfast', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Belfast', + 'DATA'), + ('_tcl_data\\msgs\\ja.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ja.msg', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+3', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+3', + 'DATA'), + ('_tcl_data\\msgs\\kl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kl.msg', + 'DATA'), + ('_tk_data\\images\\pwrdLogo150.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo150.gif', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Kentucky\\Louisville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Kentucky\\Louisville', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Tripoli', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Tripoli', + 'DATA'), + ('_tk_data\\images\\logo64.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logo64.gif', + 'DATA'), + ('_tcl_data\\tzdata\\Navajo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Navajo', + 'DATA'), + ('_tk_data\\images\\pwrdLogo100.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo100.gif', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Pontianak', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Pontianak', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Samoa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Samoa', + 'DATA'), + ('_tcl_data\\tzdata\\EST', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\EST', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yerevan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yerevan', + 'DATA'), + ('_tk_data\\menu.tcl', 'C:\\Python312\\tcl\\tk8.6\\menu.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Skopje', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Skopje', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Lindeman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Lindeman', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Rarotonga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Rarotonga', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Samarkand', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Samarkand', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Monrovia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Monrovia', + 'DATA'), + ('_tcl_data\\safe.tcl', 'C:\\Python312\\tcl\\tcl8.6\\safe.tcl', 'DATA'), + ('_tk_data\\scale.tcl', 'C:\\Python312\\tcl\\tk8.6\\scale.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Vientiane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Vientiane', + 'DATA'), + ('_tcl_data\\encoding\\macJapan.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macJapan.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Bangkok', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Bangkok', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+11', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+11', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bamako', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bamako', + 'DATA'), + ('_tcl_data\\encoding\\macGreek.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macGreek.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-4', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-4', + 'DATA'), + ('_tcl_data\\tzdata\\HST', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\HST', 'DATA'), + ('_tcl_data\\tzdata\\America\\Panama', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Panama', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Saratov', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Saratov', + 'DATA'), + ('_tcl_data\\tzdata\\Portugal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Portugal', + 'DATA'), + ('_tcl_data\\encoding\\euc-jp.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\euc-jp.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Saipan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Saipan', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Istanbul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Istanbul', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Istanbul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Istanbul', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Zaporozhye', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Zaporozhye', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Darwin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Darwin', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\San_Marino', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\San_Marino', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Novosibirsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Novosibirsk', + 'DATA'), + ('_tcl_data\\msgs\\ca.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ca.msg', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\MST7MDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\MST7MDT', + 'DATA'), + ('_tcl_data\\tzdata\\Chile\\Continental', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Chile\\Continental', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Hovd', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Hovd', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Katmandu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Katmandu', + 'DATA'), + ('_tcl_data\\msgs\\zh_hk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_hk.msg', + 'DATA'), + ('_tk_data\\ttk\\scale.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\scale.tcl', + 'DATA'), + ('_tcl_data\\tzdata\\MST', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\MST', 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Singapore', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Singapore', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yakutsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yakutsk', + 'DATA'), + ('_tcl_data\\msgs\\fr_ch.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr_ch.msg', + 'DATA'), + ('_tcl_data\\encoding\\macUkraine.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macUkraine.enc', + 'DATA'), + ('_tcl_data\\encoding\\ebcdic.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\ebcdic.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp861.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp861.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1258.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1258.enc', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Hobart', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Hobart', + 'DATA'), + ('_tcl_data\\msgs\\et.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\et.msg', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\METADATA', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\METADATA', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\DELVEWHEEL', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\DELVEWHEEL', + 'DATA'), + ('numpy-2.5.2.dist-info\\REQUESTED', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\REQUESTED', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\INSTALLER', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\INSTALLER', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\WHEEL', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\WHEEL', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\ma\\LICENSE', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\ma\\LICENSE', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE', + 'DATA'), + ('numpy-2.5.2.dist-info\\RECORD', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\RECORD', + 'DATA'), + ('numpy-2.5.2.dist-info\\entry_points.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\entry_points.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md', + 'DATA'), + ('base_library.zip', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\base_library.zip', + 'DATA')], + [('cv2.version', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\version.py', + 'PYMODULE'), + ('cv2.utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\__init__.py', + 'PYMODULE'), + ('cv2.typing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\typing\\__init__.py', + 'PYMODULE'), + ('cv2.dnn', '-', 'PYMODULE'), + ('cv2.misc.version', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\version.py', + 'PYMODULE'), + ('cv2.misc', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\__init__.py', + 'PYMODULE'), + ('cv2.mat_wrapper', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\mat_wrapper\\__init__.py', + 'PYMODULE'), + ('cv2.load_config_py3', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py3.py', + 'PYMODULE'), + ('cv2.load_config_py2', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py2.py', + 'PYMODULE'), + ('cv2.data', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\data\\__init__.py', + 'PYMODULE'), + ('cv2.config-3', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config-3.py', + 'PYMODULE'), + ('cv2.config', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config.py', + 'PYMODULE'), + ('enum', 'C:\\Python312\\Lib\\enum.py', 'PYMODULE'), + ('os', 'C:\\Python312\\Lib\\os.py', 'PYMODULE'), + ('io', 'C:\\Python312\\Lib\\io.py', 'PYMODULE'), + ('posixpath', 'C:\\Python312\\Lib\\posixpath.py', 'PYMODULE'), + ('genericpath', 'C:\\Python312\\Lib\\genericpath.py', 'PYMODULE'), + ('re._parser', 'C:\\Python312\\Lib\\re\\_parser.py', 'PYMODULE'), + ('re._constants', 'C:\\Python312\\Lib\\re\\_constants.py', 'PYMODULE'), + ('re._compiler', 'C:\\Python312\\Lib\\re\\_compiler.py', 'PYMODULE'), + ('re._casefix', 'C:\\Python312\\Lib\\re\\_casefix.py', 'PYMODULE'), + ('re', 'C:\\Python312\\Lib\\re\\__init__.py', 'PYMODULE'), + ('abc', 'C:\\Python312\\Lib\\abc.py', 'PYMODULE'), + ('copyreg', 'C:\\Python312\\Lib\\copyreg.py', 'PYMODULE'), + ('warnings', 'C:\\Python312\\Lib\\warnings.py', 'PYMODULE'), + ('types', 'C:\\Python312\\Lib\\types.py', 'PYMODULE'), + ('linecache', 'C:\\Python312\\Lib\\linecache.py', 'PYMODULE'), + ('encodings.zlib_codec', + 'C:\\Python312\\Lib\\encodings\\zlib_codec.py', + 'PYMODULE'), + ('encodings.uu_codec', + 'C:\\Python312\\Lib\\encodings\\uu_codec.py', + 'PYMODULE'), + ('encodings.utf_8_sig', + 'C:\\Python312\\Lib\\encodings\\utf_8_sig.py', + 'PYMODULE'), + ('encodings.utf_8', 'C:\\Python312\\Lib\\encodings\\utf_8.py', 'PYMODULE'), + ('encodings.utf_7', 'C:\\Python312\\Lib\\encodings\\utf_7.py', 'PYMODULE'), + ('encodings.utf_32_le', + 'C:\\Python312\\Lib\\encodings\\utf_32_le.py', + 'PYMODULE'), + ('encodings.utf_32_be', + 'C:\\Python312\\Lib\\encodings\\utf_32_be.py', + 'PYMODULE'), + ('encodings.utf_32', 'C:\\Python312\\Lib\\encodings\\utf_32.py', 'PYMODULE'), + ('encodings.utf_16_le', + 'C:\\Python312\\Lib\\encodings\\utf_16_le.py', + 'PYMODULE'), + ('encodings.utf_16_be', + 'C:\\Python312\\Lib\\encodings\\utf_16_be.py', + 'PYMODULE'), + ('encodings.utf_16', 'C:\\Python312\\Lib\\encodings\\utf_16.py', 'PYMODULE'), + ('encodings.unicode_escape', + 'C:\\Python312\\Lib\\encodings\\unicode_escape.py', + 'PYMODULE'), + ('encodings.undefined', + 'C:\\Python312\\Lib\\encodings\\undefined.py', + 'PYMODULE'), + ('encodings.tis_620', + 'C:\\Python312\\Lib\\encodings\\tis_620.py', + 'PYMODULE'), + ('encodings.shift_jisx0213', + 'C:\\Python312\\Lib\\encodings\\shift_jisx0213.py', + 'PYMODULE'), + ('encodings.shift_jis_2004', + 'C:\\Python312\\Lib\\encodings\\shift_jis_2004.py', + 'PYMODULE'), + ('encodings.shift_jis', + 'C:\\Python312\\Lib\\encodings\\shift_jis.py', + 'PYMODULE'), + ('encodings.rot_13', 'C:\\Python312\\Lib\\encodings\\rot_13.py', 'PYMODULE'), + ('encodings.raw_unicode_escape', + 'C:\\Python312\\Lib\\encodings\\raw_unicode_escape.py', + 'PYMODULE'), + ('encodings.quopri_codec', + 'C:\\Python312\\Lib\\encodings\\quopri_codec.py', + 'PYMODULE'), + ('encodings.punycode', + 'C:\\Python312\\Lib\\encodings\\punycode.py', + 'PYMODULE'), + ('encodings.ptcp154', + 'C:\\Python312\\Lib\\encodings\\ptcp154.py', + 'PYMODULE'), + ('encodings.palmos', 'C:\\Python312\\Lib\\encodings\\palmos.py', 'PYMODULE'), + ('encodings.oem', 'C:\\Python312\\Lib\\encodings\\oem.py', 'PYMODULE'), + ('encodings.mbcs', 'C:\\Python312\\Lib\\encodings\\mbcs.py', 'PYMODULE'), + ('encodings.mac_turkish', + 'C:\\Python312\\Lib\\encodings\\mac_turkish.py', + 'PYMODULE'), + ('encodings.mac_romanian', + 'C:\\Python312\\Lib\\encodings\\mac_romanian.py', + 'PYMODULE'), + ('encodings.mac_roman', + 'C:\\Python312\\Lib\\encodings\\mac_roman.py', + 'PYMODULE'), + ('encodings.mac_latin2', + 'C:\\Python312\\Lib\\encodings\\mac_latin2.py', + 'PYMODULE'), + ('encodings.mac_iceland', + 'C:\\Python312\\Lib\\encodings\\mac_iceland.py', + 'PYMODULE'), + ('encodings.mac_greek', + 'C:\\Python312\\Lib\\encodings\\mac_greek.py', + 'PYMODULE'), + ('encodings.mac_farsi', + 'C:\\Python312\\Lib\\encodings\\mac_farsi.py', + 'PYMODULE'), + ('encodings.mac_cyrillic', + 'C:\\Python312\\Lib\\encodings\\mac_cyrillic.py', + 'PYMODULE'), + ('encodings.mac_croatian', + 'C:\\Python312\\Lib\\encodings\\mac_croatian.py', + 'PYMODULE'), + ('encodings.mac_arabic', + 'C:\\Python312\\Lib\\encodings\\mac_arabic.py', + 'PYMODULE'), + ('encodings.latin_1', + 'C:\\Python312\\Lib\\encodings\\latin_1.py', + 'PYMODULE'), + ('encodings.kz1048', 'C:\\Python312\\Lib\\encodings\\kz1048.py', 'PYMODULE'), + ('encodings.koi8_u', 'C:\\Python312\\Lib\\encodings\\koi8_u.py', 'PYMODULE'), + ('encodings.koi8_t', 'C:\\Python312\\Lib\\encodings\\koi8_t.py', 'PYMODULE'), + ('encodings.koi8_r', 'C:\\Python312\\Lib\\encodings\\koi8_r.py', 'PYMODULE'), + ('encodings.johab', 'C:\\Python312\\Lib\\encodings\\johab.py', 'PYMODULE'), + ('encodings.iso8859_9', + 'C:\\Python312\\Lib\\encodings\\iso8859_9.py', + 'PYMODULE'), + ('encodings.iso8859_8', + 'C:\\Python312\\Lib\\encodings\\iso8859_8.py', + 'PYMODULE'), + ('encodings.iso8859_7', + 'C:\\Python312\\Lib\\encodings\\iso8859_7.py', + 'PYMODULE'), + ('encodings.iso8859_6', + 'C:\\Python312\\Lib\\encodings\\iso8859_6.py', + 'PYMODULE'), + ('encodings.iso8859_5', + 'C:\\Python312\\Lib\\encodings\\iso8859_5.py', + 'PYMODULE'), + ('encodings.iso8859_4', + 'C:\\Python312\\Lib\\encodings\\iso8859_4.py', + 'PYMODULE'), + ('encodings.iso8859_3', + 'C:\\Python312\\Lib\\encodings\\iso8859_3.py', + 'PYMODULE'), + ('encodings.iso8859_2', + 'C:\\Python312\\Lib\\encodings\\iso8859_2.py', + 'PYMODULE'), + ('encodings.iso8859_16', + 'C:\\Python312\\Lib\\encodings\\iso8859_16.py', + 'PYMODULE'), + ('encodings.iso8859_15', + 'C:\\Python312\\Lib\\encodings\\iso8859_15.py', + 'PYMODULE'), + ('encodings.iso8859_14', + 'C:\\Python312\\Lib\\encodings\\iso8859_14.py', + 'PYMODULE'), + ('encodings.iso8859_13', + 'C:\\Python312\\Lib\\encodings\\iso8859_13.py', + 'PYMODULE'), + ('encodings.iso8859_11', + 'C:\\Python312\\Lib\\encodings\\iso8859_11.py', + 'PYMODULE'), + ('encodings.iso8859_10', + 'C:\\Python312\\Lib\\encodings\\iso8859_10.py', + 'PYMODULE'), + ('encodings.iso8859_1', + 'C:\\Python312\\Lib\\encodings\\iso8859_1.py', + 'PYMODULE'), + ('encodings.iso2022_kr', + 'C:\\Python312\\Lib\\encodings\\iso2022_kr.py', + 'PYMODULE'), + ('encodings.iso2022_jp_ext', + 'C:\\Python312\\Lib\\encodings\\iso2022_jp_ext.py', + 'PYMODULE'), + ('encodings.iso2022_jp_3', + 'C:\\Python312\\Lib\\encodings\\iso2022_jp_3.py', + 'PYMODULE'), + ('encodings.iso2022_jp_2004', + 'C:\\Python312\\Lib\\encodings\\iso2022_jp_2004.py', + 'PYMODULE'), + ('encodings.iso2022_jp_2', + 'C:\\Python312\\Lib\\encodings\\iso2022_jp_2.py', + 'PYMODULE'), + ('encodings.iso2022_jp_1', + 'C:\\Python312\\Lib\\encodings\\iso2022_jp_1.py', + 'PYMODULE'), + ('encodings.iso2022_jp', + 'C:\\Python312\\Lib\\encodings\\iso2022_jp.py', + 'PYMODULE'), + ('encodings.idna', 'C:\\Python312\\Lib\\encodings\\idna.py', 'PYMODULE'), + ('encodings.hz', 'C:\\Python312\\Lib\\encodings\\hz.py', 'PYMODULE'), + ('encodings.hp_roman8', + 'C:\\Python312\\Lib\\encodings\\hp_roman8.py', + 'PYMODULE'), + ('encodings.hex_codec', + 'C:\\Python312\\Lib\\encodings\\hex_codec.py', + 'PYMODULE'), + ('encodings.gbk', 'C:\\Python312\\Lib\\encodings\\gbk.py', 'PYMODULE'), + ('encodings.gb2312', 'C:\\Python312\\Lib\\encodings\\gb2312.py', 'PYMODULE'), + ('encodings.gb18030', + 'C:\\Python312\\Lib\\encodings\\gb18030.py', + 'PYMODULE'), + ('encodings.euc_kr', 'C:\\Python312\\Lib\\encodings\\euc_kr.py', 'PYMODULE'), + ('encodings.euc_jp', 'C:\\Python312\\Lib\\encodings\\euc_jp.py', 'PYMODULE'), + ('encodings.euc_jisx0213', + 'C:\\Python312\\Lib\\encodings\\euc_jisx0213.py', + 'PYMODULE'), + ('encodings.euc_jis_2004', + 'C:\\Python312\\Lib\\encodings\\euc_jis_2004.py', + 'PYMODULE'), + ('encodings.cp950', 'C:\\Python312\\Lib\\encodings\\cp950.py', 'PYMODULE'), + ('encodings.cp949', 'C:\\Python312\\Lib\\encodings\\cp949.py', 'PYMODULE'), + ('encodings.cp932', 'C:\\Python312\\Lib\\encodings\\cp932.py', 'PYMODULE'), + ('encodings.cp875', 'C:\\Python312\\Lib\\encodings\\cp875.py', 'PYMODULE'), + ('encodings.cp874', 'C:\\Python312\\Lib\\encodings\\cp874.py', 'PYMODULE'), + ('encodings.cp869', 'C:\\Python312\\Lib\\encodings\\cp869.py', 'PYMODULE'), + ('encodings.cp866', 'C:\\Python312\\Lib\\encodings\\cp866.py', 'PYMODULE'), + ('encodings.cp865', 'C:\\Python312\\Lib\\encodings\\cp865.py', 'PYMODULE'), + ('encodings.cp864', 'C:\\Python312\\Lib\\encodings\\cp864.py', 'PYMODULE'), + ('encodings.cp863', 'C:\\Python312\\Lib\\encodings\\cp863.py', 'PYMODULE'), + ('encodings.cp862', 'C:\\Python312\\Lib\\encodings\\cp862.py', 'PYMODULE'), + ('encodings.cp861', 'C:\\Python312\\Lib\\encodings\\cp861.py', 'PYMODULE'), + ('encodings.cp860', 'C:\\Python312\\Lib\\encodings\\cp860.py', 'PYMODULE'), + ('encodings.cp858', 'C:\\Python312\\Lib\\encodings\\cp858.py', 'PYMODULE'), + ('encodings.cp857', 'C:\\Python312\\Lib\\encodings\\cp857.py', 'PYMODULE'), + ('encodings.cp856', 'C:\\Python312\\Lib\\encodings\\cp856.py', 'PYMODULE'), + ('encodings.cp855', 'C:\\Python312\\Lib\\encodings\\cp855.py', 'PYMODULE'), + ('encodings.cp852', 'C:\\Python312\\Lib\\encodings\\cp852.py', 'PYMODULE'), + ('encodings.cp850', 'C:\\Python312\\Lib\\encodings\\cp850.py', 'PYMODULE'), + ('encodings.cp775', 'C:\\Python312\\Lib\\encodings\\cp775.py', 'PYMODULE'), + ('encodings.cp737', 'C:\\Python312\\Lib\\encodings\\cp737.py', 'PYMODULE'), + ('encodings.cp720', 'C:\\Python312\\Lib\\encodings\\cp720.py', 'PYMODULE'), + ('encodings.cp500', 'C:\\Python312\\Lib\\encodings\\cp500.py', 'PYMODULE'), + ('encodings.cp437', 'C:\\Python312\\Lib\\encodings\\cp437.py', 'PYMODULE'), + ('encodings.cp424', 'C:\\Python312\\Lib\\encodings\\cp424.py', 'PYMODULE'), + ('encodings.cp273', 'C:\\Python312\\Lib\\encodings\\cp273.py', 'PYMODULE'), + ('encodings.cp1258', 'C:\\Python312\\Lib\\encodings\\cp1258.py', 'PYMODULE'), + ('encodings.cp1257', 'C:\\Python312\\Lib\\encodings\\cp1257.py', 'PYMODULE'), + ('encodings.cp1256', 'C:\\Python312\\Lib\\encodings\\cp1256.py', 'PYMODULE'), + ('encodings.cp1255', 'C:\\Python312\\Lib\\encodings\\cp1255.py', 'PYMODULE'), + ('encodings.cp1254', 'C:\\Python312\\Lib\\encodings\\cp1254.py', 'PYMODULE'), + ('encodings.cp1253', 'C:\\Python312\\Lib\\encodings\\cp1253.py', 'PYMODULE'), + ('encodings.cp1252', 'C:\\Python312\\Lib\\encodings\\cp1252.py', 'PYMODULE'), + ('encodings.cp1251', 'C:\\Python312\\Lib\\encodings\\cp1251.py', 'PYMODULE'), + ('encodings.cp1250', 'C:\\Python312\\Lib\\encodings\\cp1250.py', 'PYMODULE'), + ('encodings.cp1140', 'C:\\Python312\\Lib\\encodings\\cp1140.py', 'PYMODULE'), + ('encodings.cp1125', 'C:\\Python312\\Lib\\encodings\\cp1125.py', 'PYMODULE'), + ('encodings.cp1026', 'C:\\Python312\\Lib\\encodings\\cp1026.py', 'PYMODULE'), + ('encodings.cp1006', 'C:\\Python312\\Lib\\encodings\\cp1006.py', 'PYMODULE'), + ('encodings.cp037', 'C:\\Python312\\Lib\\encodings\\cp037.py', 'PYMODULE'), + ('encodings.charmap', + 'C:\\Python312\\Lib\\encodings\\charmap.py', + 'PYMODULE'), + ('encodings.bz2_codec', + 'C:\\Python312\\Lib\\encodings\\bz2_codec.py', + 'PYMODULE'), + ('encodings.big5hkscs', + 'C:\\Python312\\Lib\\encodings\\big5hkscs.py', + 'PYMODULE'), + ('encodings.big5', 'C:\\Python312\\Lib\\encodings\\big5.py', 'PYMODULE'), + ('encodings.base64_codec', + 'C:\\Python312\\Lib\\encodings\\base64_codec.py', + 'PYMODULE'), + ('encodings.ascii', 'C:\\Python312\\Lib\\encodings\\ascii.py', 'PYMODULE'), + ('encodings.aliases', + 'C:\\Python312\\Lib\\encodings\\aliases.py', + 'PYMODULE'), + ('encodings', 'C:\\Python312\\Lib\\encodings\\__init__.py', 'PYMODULE'), + ('stat', 'C:\\Python312\\Lib\\stat.py', 'PYMODULE'), + ('keyword', 'C:\\Python312\\Lib\\keyword.py', 'PYMODULE'), + ('sre_parse', 'C:\\Python312\\Lib\\sre_parse.py', 'PYMODULE'), + ('functools', 'C:\\Python312\\Lib\\functools.py', 'PYMODULE'), + ('sre_compile', 'C:\\Python312\\Lib\\sre_compile.py', 'PYMODULE'), + ('collections.abc', 'C:\\Python312\\Lib\\collections\\abc.py', 'PYMODULE'), + ('collections', 'C:\\Python312\\Lib\\collections\\__init__.py', 'PYMODULE'), + ('heapq', 'C:\\Python312\\Lib\\heapq.py', 'PYMODULE'), + ('sre_constants', 'C:\\Python312\\Lib\\sre_constants.py', 'PYMODULE'), + ('codecs', 'C:\\Python312\\Lib\\codecs.py', 'PYMODULE'), + ('weakref', 'C:\\Python312\\Lib\\weakref.py', 'PYMODULE'), + ('operator', 'C:\\Python312\\Lib\\operator.py', 'PYMODULE'), + ('reprlib', 'C:\\Python312\\Lib\\reprlib.py', 'PYMODULE'), + ('ntpath', 'C:\\Python312\\Lib\\ntpath.py', 'PYMODULE'), + ('_weakrefset', 'C:\\Python312\\Lib\\_weakrefset.py', 'PYMODULE'), + ('_collections_abc', 'C:\\Python312\\Lib\\_collections_abc.py', 'PYMODULE'), + ('locale', 'C:\\Python312\\Lib\\locale.py', 'PYMODULE'), + ('cv2', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\__init__.py', + 'PYMODULE'), + ('traceback', 'C:\\Python312\\Lib\\traceback.py', 'PYMODULE')]) diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/COLLECT-00.toc b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/COLLECT-00.toc new file mode 100644 index 000000000..dce7bd496 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/COLLECT-00.toc @@ -0,0 +1,3088 @@ +([('MaskReviewer.exe', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\MaskReviewer.exe', + 'EXECUTABLE'), + ('VCRUNTIME140.dll', 'C:\\Python312\\VCRUNTIME140.dll', 'BINARY'), + ('VCRUNTIME140_1.dll', 'C:\\Python312\\VCRUNTIME140_1.dll', 'BINARY'), + ('_asyncio.pyd', 'C:\\Python312\\DLLs\\_asyncio.pyd', 'EXTENSION'), + ('_bz2.pyd', 'C:\\Python312\\DLLs\\_bz2.pyd', 'EXTENSION'), + ('_ctypes.pyd', 'C:\\Python312\\DLLs\\_ctypes.pyd', 'EXTENSION'), + ('_decimal.pyd', 'C:\\Python312\\DLLs\\_decimal.pyd', 'EXTENSION'), + ('_hashlib.pyd', 'C:\\Python312\\DLLs\\_hashlib.pyd', 'EXTENSION'), + ('_lzma.pyd', 'C:\\Python312\\DLLs\\_lzma.pyd', 'EXTENSION'), + ('_multiprocessing.pyd', + 'C:\\Python312\\DLLs\\_multiprocessing.pyd', + 'EXTENSION'), + ('_overlapped.pyd', 'C:\\Python312\\DLLs\\_overlapped.pyd', 'EXTENSION'), + ('_queue.pyd', 'C:\\Python312\\DLLs\\_queue.pyd', 'EXTENSION'), + ('_socket.pyd', 'C:\\Python312\\DLLs\\_socket.pyd', 'EXTENSION'), + ('_ssl.pyd', 'C:\\Python312\\DLLs\\_ssl.pyd', 'EXTENSION'), + ('_tcl_data\\auto.tcl', 'C:\\Python312\\tcl\\tcl8.6\\auto.tcl', 'DATA'), + ('_tcl_data\\clock.tcl', 'C:\\Python312\\tcl\\tcl8.6\\clock.tcl', 'DATA'), + ('_tcl_data\\encoding\\ascii.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\ascii.enc', + 'DATA'), + ('_tcl_data\\encoding\\big5.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\big5.enc', + 'DATA'), + ('_tcl_data\\encoding\\cns11643.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cns11643.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1250.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1250.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1251.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1251.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1252.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1252.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1253.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1253.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1254.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1254.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1255.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1255.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1256.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1256.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1257.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1257.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp1258.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp1258.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp437.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp437.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp737.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp737.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp775.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp775.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp850.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp850.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp852.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp852.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp855.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp855.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp857.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp857.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp860.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp860.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp861.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp861.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp862.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp862.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp863.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp863.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp864.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp864.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp865.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp865.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp866.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp866.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp869.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp869.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp874.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp874.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp932.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp932.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp936.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp936.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp949.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp949.enc', + 'DATA'), + ('_tcl_data\\encoding\\cp950.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\cp950.enc', + 'DATA'), + ('_tcl_data\\encoding\\dingbats.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\dingbats.enc', + 'DATA'), + ('_tcl_data\\encoding\\ebcdic.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\ebcdic.enc', + 'DATA'), + ('_tcl_data\\encoding\\euc-cn.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\euc-cn.enc', + 'DATA'), + ('_tcl_data\\encoding\\euc-jp.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\euc-jp.enc', + 'DATA'), + ('_tcl_data\\encoding\\euc-kr.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\euc-kr.enc', + 'DATA'), + ('_tcl_data\\encoding\\gb12345.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb12345.enc', + 'DATA'), + ('_tcl_data\\encoding\\gb1988.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb1988.enc', + 'DATA'), + ('_tcl_data\\encoding\\gb2312-raw.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb2312-raw.enc', + 'DATA'), + ('_tcl_data\\encoding\\gb2312.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\gb2312.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso2022-jp.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso2022-jp.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso2022-kr.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso2022-kr.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso2022.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso2022.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-1.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-1.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-10.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-10.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-11.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-11.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-13.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-13.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-14.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-14.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-15.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-15.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-16.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-16.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-2.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-2.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-3.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-3.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-4.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-4.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-5.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-5.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-6.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-6.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-7.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-7.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-8.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-8.enc', + 'DATA'), + ('_tcl_data\\encoding\\iso8859-9.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\iso8859-9.enc', + 'DATA'), + ('_tcl_data\\encoding\\jis0201.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\jis0201.enc', + 'DATA'), + ('_tcl_data\\encoding\\jis0208.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\jis0208.enc', + 'DATA'), + ('_tcl_data\\encoding\\jis0212.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\jis0212.enc', + 'DATA'), + ('_tcl_data\\encoding\\koi8-r.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\koi8-r.enc', + 'DATA'), + ('_tcl_data\\encoding\\koi8-u.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\koi8-u.enc', + 'DATA'), + ('_tcl_data\\encoding\\ksc5601.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\ksc5601.enc', + 'DATA'), + ('_tcl_data\\encoding\\macCentEuro.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macCentEuro.enc', + 'DATA'), + ('_tcl_data\\encoding\\macCroatian.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macCroatian.enc', + 'DATA'), + ('_tcl_data\\encoding\\macCyrillic.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macCyrillic.enc', + 'DATA'), + ('_tcl_data\\encoding\\macDingbats.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macDingbats.enc', + 'DATA'), + ('_tcl_data\\encoding\\macGreek.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macGreek.enc', + 'DATA'), + ('_tcl_data\\encoding\\macIceland.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macIceland.enc', + 'DATA'), + ('_tcl_data\\encoding\\macJapan.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macJapan.enc', + 'DATA'), + ('_tcl_data\\encoding\\macRoman.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macRoman.enc', + 'DATA'), + ('_tcl_data\\encoding\\macRomania.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macRomania.enc', + 'DATA'), + ('_tcl_data\\encoding\\macThai.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macThai.enc', + 'DATA'), + ('_tcl_data\\encoding\\macTurkish.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macTurkish.enc', + 'DATA'), + ('_tcl_data\\encoding\\macUkraine.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\macUkraine.enc', + 'DATA'), + ('_tcl_data\\encoding\\shiftjis.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\shiftjis.enc', + 'DATA'), + ('_tcl_data\\encoding\\symbol.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\symbol.enc', + 'DATA'), + ('_tcl_data\\encoding\\tis-620.enc', + 'C:\\Python312\\tcl\\tcl8.6\\encoding\\tis-620.enc', + 'DATA'), + ('_tcl_data\\history.tcl', 'C:\\Python312\\tcl\\tcl8.6\\history.tcl', 'DATA'), + ('_tcl_data\\http1.0\\http.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\http1.0\\http.tcl', + 'DATA'), + ('_tcl_data\\http1.0\\pkgIndex.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\http1.0\\pkgIndex.tcl', + 'DATA'), + ('_tcl_data\\init.tcl', 'C:\\Python312\\tcl\\tcl8.6\\init.tcl', 'DATA'), + ('_tcl_data\\msgs\\af.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\af.msg', + 'DATA'), + ('_tcl_data\\msgs\\af_za.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\af_za.msg', + 'DATA'), + ('_tcl_data\\msgs\\ar.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar.msg', + 'DATA'), + ('_tcl_data\\msgs\\ar_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\ar_jo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_jo.msg', + 'DATA'), + ('_tcl_data\\msgs\\ar_lb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_lb.msg', + 'DATA'), + ('_tcl_data\\msgs\\ar_sy.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ar_sy.msg', + 'DATA'), + ('_tcl_data\\msgs\\be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\be.msg', + 'DATA'), + ('_tcl_data\\msgs\\bg.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\bg.msg', + 'DATA'), + ('_tcl_data\\msgs\\bn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\bn.msg', + 'DATA'), + ('_tcl_data\\msgs\\bn_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\bn_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\ca.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ca.msg', + 'DATA'), + ('_tcl_data\\msgs\\cs.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\cs.msg', + 'DATA'), + ('_tcl_data\\msgs\\da.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\da.msg', + 'DATA'), + ('_tcl_data\\msgs\\de.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\de.msg', + 'DATA'), + ('_tcl_data\\msgs\\de_at.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\de_at.msg', + 'DATA'), + ('_tcl_data\\msgs\\de_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\de_be.msg', + 'DATA'), + ('_tcl_data\\msgs\\el.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\el.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_au.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_au.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_be.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_bw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_bw.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_ca.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_ca.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_gb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_gb.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_hk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_hk.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_ie.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_ie.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_nz.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_nz.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_ph.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_ph.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_sg.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_sg.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_za.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_za.msg', + 'DATA'), + ('_tcl_data\\msgs\\en_zw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\en_zw.msg', + 'DATA'), + ('_tcl_data\\msgs\\eo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\eo.msg', + 'DATA'), + ('_tcl_data\\msgs\\es.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_ar.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ar.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_bo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_bo.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_cl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_cl.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_co.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_co.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_cr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_cr.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_do.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_do.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_ec.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ec.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_gt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_gt.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_hn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_hn.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_mx.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_mx.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_ni.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ni.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_pa.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_pa.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_pe.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_pe.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_pr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_pr.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_py.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_py.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_sv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_sv.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_uy.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_uy.msg', + 'DATA'), + ('_tcl_data\\msgs\\es_ve.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\es_ve.msg', + 'DATA'), + ('_tcl_data\\msgs\\et.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\et.msg', + 'DATA'), + ('_tcl_data\\msgs\\eu.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\eu.msg', + 'DATA'), + ('_tcl_data\\msgs\\eu_es.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\eu_es.msg', + 'DATA'), + ('_tcl_data\\msgs\\fa.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fa.msg', + 'DATA'), + ('_tcl_data\\msgs\\fa_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fa_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\fa_ir.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fa_ir.msg', + 'DATA'), + ('_tcl_data\\msgs\\fi.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fi.msg', + 'DATA'), + ('_tcl_data\\msgs\\fo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fo.msg', + 'DATA'), + ('_tcl_data\\msgs\\fo_fo.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fo_fo.msg', + 'DATA'), + ('_tcl_data\\msgs\\fr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr.msg', + 'DATA'), + ('_tcl_data\\msgs\\fr_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr_be.msg', + 'DATA'), + ('_tcl_data\\msgs\\fr_ca.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr_ca.msg', + 'DATA'), + ('_tcl_data\\msgs\\fr_ch.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\fr_ch.msg', + 'DATA'), + ('_tcl_data\\msgs\\ga.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ga.msg', + 'DATA'), + ('_tcl_data\\msgs\\ga_ie.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ga_ie.msg', + 'DATA'), + ('_tcl_data\\msgs\\gl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gl.msg', + 'DATA'), + ('_tcl_data\\msgs\\gl_es.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gl_es.msg', + 'DATA'), + ('_tcl_data\\msgs\\gv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gv.msg', + 'DATA'), + ('_tcl_data\\msgs\\gv_gb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\gv_gb.msg', + 'DATA'), + ('_tcl_data\\msgs\\he.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\he.msg', + 'DATA'), + ('_tcl_data\\msgs\\hi.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hi.msg', + 'DATA'), + ('_tcl_data\\msgs\\hi_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hi_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\hr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hr.msg', + 'DATA'), + ('_tcl_data\\msgs\\hu.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\hu.msg', + 'DATA'), + ('_tcl_data\\msgs\\id.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\id.msg', + 'DATA'), + ('_tcl_data\\msgs\\id_id.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\id_id.msg', + 'DATA'), + ('_tcl_data\\msgs\\is.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\is.msg', + 'DATA'), + ('_tcl_data\\msgs\\it.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\it.msg', + 'DATA'), + ('_tcl_data\\msgs\\it_ch.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\it_ch.msg', + 'DATA'), + ('_tcl_data\\msgs\\ja.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ja.msg', + 'DATA'), + ('_tcl_data\\msgs\\kl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kl.msg', + 'DATA'), + ('_tcl_data\\msgs\\kl_gl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kl_gl.msg', + 'DATA'), + ('_tcl_data\\msgs\\ko.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ko.msg', + 'DATA'), + ('_tcl_data\\msgs\\ko_kr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ko_kr.msg', + 'DATA'), + ('_tcl_data\\msgs\\kok.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kok.msg', + 'DATA'), + ('_tcl_data\\msgs\\kok_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kok_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\kw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kw.msg', + 'DATA'), + ('_tcl_data\\msgs\\kw_gb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\kw_gb.msg', + 'DATA'), + ('_tcl_data\\msgs\\lt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\lt.msg', + 'DATA'), + ('_tcl_data\\msgs\\lv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\lv.msg', + 'DATA'), + ('_tcl_data\\msgs\\mk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mk.msg', + 'DATA'), + ('_tcl_data\\msgs\\mr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mr.msg', + 'DATA'), + ('_tcl_data\\msgs\\mr_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mr_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\ms.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ms.msg', + 'DATA'), + ('_tcl_data\\msgs\\ms_my.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ms_my.msg', + 'DATA'), + ('_tcl_data\\msgs\\mt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\mt.msg', + 'DATA'), + ('_tcl_data\\msgs\\nb.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nb.msg', + 'DATA'), + ('_tcl_data\\msgs\\nl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nl.msg', + 'DATA'), + ('_tcl_data\\msgs\\nl_be.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nl_be.msg', + 'DATA'), + ('_tcl_data\\msgs\\nn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\nn.msg', + 'DATA'), + ('_tcl_data\\msgs\\pl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\pl.msg', + 'DATA'), + ('_tcl_data\\msgs\\pt.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\pt.msg', + 'DATA'), + ('_tcl_data\\msgs\\pt_br.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\pt_br.msg', + 'DATA'), + ('_tcl_data\\msgs\\ro.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ro.msg', + 'DATA'), + ('_tcl_data\\msgs\\ru.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ru.msg', + 'DATA'), + ('_tcl_data\\msgs\\ru_ua.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ru_ua.msg', + 'DATA'), + ('_tcl_data\\msgs\\sh.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sh.msg', + 'DATA'), + ('_tcl_data\\msgs\\sk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sk.msg', + 'DATA'), + ('_tcl_data\\msgs\\sl.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sl.msg', + 'DATA'), + ('_tcl_data\\msgs\\sq.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sq.msg', + 'DATA'), + ('_tcl_data\\msgs\\sr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sr.msg', + 'DATA'), + ('_tcl_data\\msgs\\sv.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sv.msg', + 'DATA'), + ('_tcl_data\\msgs\\sw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\sw.msg', + 'DATA'), + ('_tcl_data\\msgs\\ta.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ta.msg', + 'DATA'), + ('_tcl_data\\msgs\\ta_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\ta_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\te.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\te.msg', + 'DATA'), + ('_tcl_data\\msgs\\te_in.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\te_in.msg', + 'DATA'), + ('_tcl_data\\msgs\\th.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\th.msg', + 'DATA'), + ('_tcl_data\\msgs\\tr.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\tr.msg', + 'DATA'), + ('_tcl_data\\msgs\\uk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\uk.msg', + 'DATA'), + ('_tcl_data\\msgs\\vi.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\vi.msg', + 'DATA'), + ('_tcl_data\\msgs\\zh.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh.msg', + 'DATA'), + ('_tcl_data\\msgs\\zh_cn.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_cn.msg', + 'DATA'), + ('_tcl_data\\msgs\\zh_hk.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_hk.msg', + 'DATA'), + ('_tcl_data\\msgs\\zh_sg.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_sg.msg', + 'DATA'), + ('_tcl_data\\msgs\\zh_tw.msg', + 'C:\\Python312\\tcl\\tcl8.6\\msgs\\zh_tw.msg', + 'DATA'), + ('_tcl_data\\opt0.4\\optparse.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\opt0.4\\optparse.tcl', + 'DATA'), + ('_tcl_data\\opt0.4\\pkgIndex.tcl', + 'C:\\Python312\\tcl\\tcl8.6\\opt0.4\\pkgIndex.tcl', + 'DATA'), + ('_tcl_data\\package.tcl', 'C:\\Python312\\tcl\\tcl8.6\\package.tcl', 'DATA'), + ('_tcl_data\\parray.tcl', 'C:\\Python312\\tcl\\tcl8.6\\parray.tcl', 'DATA'), + ('_tcl_data\\safe.tcl', 'C:\\Python312\\tcl\\tcl8.6\\safe.tcl', 'DATA'), + ('_tcl_data\\tclIndex', 'C:\\Python312\\tcl\\tcl8.6\\tclIndex', 'DATA'), + ('_tcl_data\\tm.tcl', 'C:\\Python312\\tcl\\tcl8.6\\tm.tcl', 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Abidjan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Abidjan', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Accra', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Accra', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Addis_Ababa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Addis_Ababa', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Algiers', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Algiers', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Asmara', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Asmara', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Asmera', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Asmera', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bamako', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bamako', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bangui', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bangui', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Banjul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Banjul', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bissau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bissau', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Blantyre', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Blantyre', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Brazzaville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Brazzaville', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Bujumbura', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Bujumbura', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Cairo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Cairo', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Casablanca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Casablanca', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Ceuta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Ceuta', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Conakry', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Conakry', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Dakar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Dakar', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Dar_es_Salaam', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Dar_es_Salaam', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Djibouti', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Djibouti', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Douala', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Douala', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\El_Aaiun', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\El_Aaiun', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Freetown', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Freetown', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Gaborone', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Gaborone', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Harare', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Harare', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Johannesburg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Johannesburg', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Juba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Juba', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Kampala', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Kampala', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Khartoum', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Khartoum', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Kigali', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Kigali', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Kinshasa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Kinshasa', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lagos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lagos', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Libreville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Libreville', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lome', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Luanda', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Luanda', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lubumbashi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lubumbashi', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Lusaka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Lusaka', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Malabo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Malabo', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Maputo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Maputo', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Maseru', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Maseru', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Mbabane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Mbabane', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Mogadishu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Mogadishu', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Monrovia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Monrovia', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Nairobi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Nairobi', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Ndjamena', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Ndjamena', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Niamey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Niamey', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Nouakchott', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Nouakchott', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Ouagadougou', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Ouagadougou', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Porto-Novo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Porto-Novo', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Sao_Tome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Sao_Tome', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Timbuktu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Timbuktu', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Tripoli', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Tripoli', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Tunis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Tunis', + 'DATA'), + ('_tcl_data\\tzdata\\Africa\\Windhoek', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Africa\\Windhoek', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Adak', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Adak', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Anchorage', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Anchorage', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Anguilla', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Anguilla', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Antigua', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Antigua', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Araguaina', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Araguaina', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Buenos_Aires', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Buenos_Aires', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Catamarca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Catamarca', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\ComodRivadavia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\ComodRivadavia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Cordoba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Cordoba', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Jujuy', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Jujuy', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\La_Rioja', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\La_Rioja', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Mendoza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Mendoza', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Rio_Gallegos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Rio_Gallegos', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Salta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Salta', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\San_Juan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\San_Juan', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\San_Luis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\San_Luis', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Tucuman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Tucuman', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Argentina\\Ushuaia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Argentina\\Ushuaia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Aruba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Aruba', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Asuncion', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Asuncion', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Atikokan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Atikokan', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Atka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Atka', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Bahia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Bahia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Bahia_Banderas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Bahia_Banderas', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Barbados', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Barbados', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Belem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Belem', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Belize', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Belize', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Blanc-Sablon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Blanc-Sablon', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Boa_Vista', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Boa_Vista', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Bogota', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Bogota', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Boise', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Boise', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Buenos_Aires', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Buenos_Aires', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cambridge_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cambridge_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Campo_Grande', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Campo_Grande', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cancun', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cancun', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Caracas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Caracas', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Catamarca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Catamarca', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cayenne', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cayenne', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cayman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cayman', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Chicago', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Chicago', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Chihuahua', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Chihuahua', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Coral_Harbour', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Coral_Harbour', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cordoba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cordoba', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Costa_Rica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Costa_Rica', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Creston', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Creston', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Cuiaba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Cuiaba', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Curacao', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Curacao', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Danmarkshavn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Danmarkshavn', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Dawson', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Dawson', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Dawson_Creek', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Dawson_Creek', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Denver', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Denver', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Detroit', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Detroit', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Dominica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Dominica', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Edmonton', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Edmonton', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Eirunepe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Eirunepe', + 'DATA'), + ('_tcl_data\\tzdata\\America\\El_Salvador', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\El_Salvador', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Ensenada', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Ensenada', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Fort_Nelson', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Fort_Nelson', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Fort_Wayne', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Fort_Wayne', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Fortaleza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Fortaleza', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Glace_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Glace_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Godthab', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Godthab', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Goose_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Goose_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Grand_Turk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Grand_Turk', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Grenada', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Grenada', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guadeloupe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guadeloupe', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guatemala', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guatemala', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guayaquil', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guayaquil', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Guyana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Guyana', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Halifax', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Halifax', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Havana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Havana', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Hermosillo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Hermosillo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Indianapolis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Indianapolis', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Knox', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Knox', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Marengo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Marengo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Petersburg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Petersburg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Tell_City', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Tell_City', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Vevay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Vevay', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Vincennes', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Vincennes', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indiana\\Winamac', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indiana\\Winamac', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Indianapolis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Indianapolis', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Inuvik', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Inuvik', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Iqaluit', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Iqaluit', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Jamaica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Jamaica', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Jujuy', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Jujuy', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Juneau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Juneau', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Kentucky\\Louisville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Kentucky\\Louisville', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Kentucky\\Monticello', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Kentucky\\Monticello', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Knox_IN', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Knox_IN', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Kralendijk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Kralendijk', + 'DATA'), + ('_tcl_data\\tzdata\\America\\La_Paz', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\La_Paz', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Lima', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Lima', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Los_Angeles', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Los_Angeles', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Louisville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Louisville', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Lower_Princes', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Lower_Princes', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Maceio', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Maceio', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Managua', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Managua', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Manaus', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Manaus', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Marigot', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Marigot', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Martinique', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Martinique', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Matamoros', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Matamoros', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Mazatlan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Mazatlan', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Mendoza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Mendoza', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Menominee', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Menominee', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Merida', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Merida', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Metlakatla', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Metlakatla', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Mexico_City', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Mexico_City', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Miquelon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Miquelon', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Moncton', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Moncton', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Monterrey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Monterrey', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Montevideo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Montevideo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Montreal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Montreal', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Montserrat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Montserrat', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nassau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nassau', + 'DATA'), + ('_tcl_data\\tzdata\\America\\New_York', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\New_York', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nipigon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nipigon', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nome', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Noronha', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Noronha', + 'DATA'), + ('_tcl_data\\tzdata\\America\\North_Dakota\\Beulah', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\North_Dakota\\Beulah', + 'DATA'), + ('_tcl_data\\tzdata\\America\\North_Dakota\\Center', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\North_Dakota\\Center', + 'DATA'), + ('_tcl_data\\tzdata\\America\\North_Dakota\\New_Salem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\North_Dakota\\New_Salem', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Nuuk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Nuuk', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Ojinaga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Ojinaga', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Panama', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Panama', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Pangnirtung', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Pangnirtung', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Paramaribo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Paramaribo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Phoenix', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Phoenix', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Port-au-Prince', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Port-au-Prince', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Port_of_Spain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Port_of_Spain', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Porto_Acre', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Porto_Acre', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Porto_Velho', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Porto_Velho', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Puerto_Rico', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Puerto_Rico', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Punta_Arenas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Punta_Arenas', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Rainy_River', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rainy_River', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Rankin_Inlet', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rankin_Inlet', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Recife', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Recife', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Regina', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Regina', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Resolute', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Resolute', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Rio_Branco', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rio_Branco', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Rosario', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Rosario', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santa_Isabel', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santa_Isabel', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santarem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santarem', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santiago', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santiago', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Santo_Domingo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Santo_Domingo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Sao_Paulo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Sao_Paulo', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Scoresbysund', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Scoresbysund', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Shiprock', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Shiprock', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Sitka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Sitka', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Barthelemy', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Barthelemy', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Johns', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Johns', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Kitts', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Kitts', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Lucia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Lucia', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Thomas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Thomas', + 'DATA'), + ('_tcl_data\\tzdata\\America\\St_Vincent', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\St_Vincent', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Swift_Current', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Swift_Current', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Tegucigalpa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Tegucigalpa', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Thule', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Thule', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Thunder_Bay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Thunder_Bay', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Tijuana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Tijuana', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Toronto', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Toronto', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Tortola', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Tortola', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Vancouver', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Vancouver', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Virgin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Virgin', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Whitehorse', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Whitehorse', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Winnipeg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Winnipeg', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Yakutat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Yakutat', + 'DATA'), + ('_tcl_data\\tzdata\\America\\Yellowknife', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\America\\Yellowknife', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Casey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Casey', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Davis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Davis', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\DumontDUrville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\DumontDUrville', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Macquarie', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Macquarie', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Mawson', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Mawson', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\McMurdo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\McMurdo', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Palmer', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Palmer', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Rothera', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Rothera', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\South_Pole', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\South_Pole', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Syowa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Syowa', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Troll', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Troll', + 'DATA'), + ('_tcl_data\\tzdata\\Antarctica\\Vostok', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Antarctica\\Vostok', + 'DATA'), + ('_tcl_data\\tzdata\\Arctic\\Longyearbyen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Arctic\\Longyearbyen', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Aden', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Aden', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Almaty', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Almaty', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Amman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Amman', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Anadyr', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Anadyr', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Aqtau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Aqtau', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Aqtobe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Aqtobe', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ashgabat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ashgabat', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ashkhabad', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ashkhabad', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Atyrau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Atyrau', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Baghdad', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Baghdad', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Bahrain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Bahrain', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Baku', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Baku', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Bangkok', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Bangkok', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Barnaul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Barnaul', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Beirut', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Beirut', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Bishkek', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Bishkek', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Brunei', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Brunei', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Calcutta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Calcutta', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Chita', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Chita', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Choibalsan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Choibalsan', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Chongqing', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Chongqing', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Chungking', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Chungking', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Colombo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Colombo', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dacca', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dacca', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Damascus', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Damascus', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dhaka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dhaka', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dili', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dili', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dubai', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dubai', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Dushanbe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Dushanbe', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Famagusta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Famagusta', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Gaza', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Gaza', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Harbin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Harbin', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Hebron', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Hebron', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ho_Chi_Minh', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ho_Chi_Minh', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Hong_Kong', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Hong_Kong', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Hovd', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Hovd', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Irkutsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Irkutsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Istanbul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Istanbul', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Jakarta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Jakarta', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Jayapura', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Jayapura', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Jerusalem', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Jerusalem', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kabul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kabul', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kamchatka', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kamchatka', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Karachi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Karachi', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kashgar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kashgar', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kathmandu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kathmandu', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Katmandu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Katmandu', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Khandyga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Khandyga', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kolkata', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kolkata', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Krasnoyarsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Krasnoyarsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kuala_Lumpur', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kuala_Lumpur', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kuching', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kuching', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Kuwait', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Kuwait', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Macao', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Macao', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Macau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Macau', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Magadan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Magadan', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Makassar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Makassar', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Manila', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Manila', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Muscat', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Muscat', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Nicosia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Nicosia', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Novokuznetsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Novokuznetsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Novosibirsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Novosibirsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Omsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Omsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Oral', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Oral', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Phnom_Penh', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Phnom_Penh', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Pontianak', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Pontianak', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Pyongyang', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Pyongyang', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Qatar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Qatar', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Qostanay', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Qostanay', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Qyzylorda', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Qyzylorda', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Rangoon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Rangoon', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Riyadh', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Riyadh', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Saigon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Saigon', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Sakhalin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Sakhalin', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Samarkand', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Samarkand', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Seoul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Seoul', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Shanghai', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Shanghai', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Singapore', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Singapore', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Srednekolymsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Srednekolymsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Taipei', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Taipei', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tashkent', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tashkent', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tbilisi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tbilisi', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tehran', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tehran', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tel_Aviv', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tel_Aviv', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Thimbu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Thimbu', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Thimphu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Thimphu', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tokyo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tokyo', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Tomsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Tomsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ujung_Pandang', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ujung_Pandang', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ulaanbaatar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ulaanbaatar', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ulan_Bator', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ulan_Bator', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Urumqi', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Urumqi', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Ust-Nera', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Ust-Nera', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Vientiane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Vientiane', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Vladivostok', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Vladivostok', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yakutsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yakutsk', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yangon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yangon', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yekaterinburg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yekaterinburg', + 'DATA'), + ('_tcl_data\\tzdata\\Asia\\Yerevan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Asia\\Yerevan', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Azores', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Azores', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Bermuda', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Bermuda', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Canary', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Canary', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Cape_Verde', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Cape_Verde', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Faeroe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Faeroe', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Faroe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Faroe', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Jan_Mayen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Jan_Mayen', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Madeira', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Madeira', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Reykjavik', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Reykjavik', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\South_Georgia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\South_Georgia', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\St_Helena', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\St_Helena', + 'DATA'), + ('_tcl_data\\tzdata\\Atlantic\\Stanley', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Atlantic\\Stanley', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\ACT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\ACT', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Adelaide', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Adelaide', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Brisbane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Brisbane', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Broken_Hill', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Broken_Hill', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Canberra', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Canberra', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Currie', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Currie', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Darwin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Darwin', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Eucla', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Eucla', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Hobart', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Hobart', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\LHI', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\LHI', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Lindeman', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Lindeman', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Lord_Howe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Lord_Howe', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Melbourne', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Melbourne', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\NSW', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\NSW', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\North', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\North', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Perth', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Perth', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Queensland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Queensland', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\South', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\South', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Sydney', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Sydney', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Tasmania', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Tasmania', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Victoria', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Victoria', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\West', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\West', + 'DATA'), + ('_tcl_data\\tzdata\\Australia\\Yancowinna', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Australia\\Yancowinna', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\Acre', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\Acre', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\DeNoronha', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\DeNoronha', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\East', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\East', + 'DATA'), + ('_tcl_data\\tzdata\\Brazil\\West', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Brazil\\West', + 'DATA'), + ('_tcl_data\\tzdata\\CET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\CET', 'DATA'), + ('_tcl_data\\tzdata\\CST6CDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\CST6CDT', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Atlantic', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Atlantic', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Central', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Central', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Eastern', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Eastern', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Mountain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Mountain', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Newfoundland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Newfoundland', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Pacific', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Pacific', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Saskatchewan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Saskatchewan', + 'DATA'), + ('_tcl_data\\tzdata\\Canada\\Yukon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Canada\\Yukon', + 'DATA'), + ('_tcl_data\\tzdata\\Chile\\Continental', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Chile\\Continental', + 'DATA'), + ('_tcl_data\\tzdata\\Chile\\EasterIsland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Chile\\EasterIsland', + 'DATA'), + ('_tcl_data\\tzdata\\Cuba', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Cuba', + 'DATA'), + ('_tcl_data\\tzdata\\EET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\EET', 'DATA'), + ('_tcl_data\\tzdata\\EST', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\EST', 'DATA'), + ('_tcl_data\\tzdata\\EST5EDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\EST5EDT', + 'DATA'), + ('_tcl_data\\tzdata\\Egypt', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Egypt', + 'DATA'), + ('_tcl_data\\tzdata\\Eire', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Eire', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+0', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+1', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+1', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+10', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+10', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+11', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+11', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+12', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+12', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+2', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+2', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+3', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+3', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+4', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+4', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+5', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+5', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+6', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+6', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+7', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+7', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+8', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+8', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT+9', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT+9', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-0', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-1', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-1', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-10', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-10', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-11', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-11', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-12', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-12', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-13', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-13', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-14', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-14', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-2', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-2', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-3', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-3', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-4', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-4', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-5', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-5', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-6', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-6', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-7', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-7', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-8', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-8', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT-9', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT-9', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\GMT0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\GMT0', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\Greenwich', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\Greenwich', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\UCT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\UCT', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\UTC', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\UTC', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\Universal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\Universal', + 'DATA'), + ('_tcl_data\\tzdata\\Etc\\Zulu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Etc\\Zulu', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Amsterdam', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Amsterdam', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Andorra', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Andorra', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Astrakhan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Astrakhan', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Athens', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Athens', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Belfast', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Belfast', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Belgrade', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Belgrade', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Berlin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Berlin', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Bratislava', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Bratislava', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Brussels', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Brussels', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Bucharest', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Bucharest', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Budapest', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Budapest', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Busingen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Busingen', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Chisinau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Chisinau', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Copenhagen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Copenhagen', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Dublin', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Dublin', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Gibraltar', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Gibraltar', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Guernsey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Guernsey', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Helsinki', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Helsinki', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Isle_of_Man', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Isle_of_Man', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Istanbul', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Istanbul', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Jersey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Jersey', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kaliningrad', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kaliningrad', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kiev', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kiev', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kirov', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kirov', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Kyiv', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Kyiv', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Lisbon', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Lisbon', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Ljubljana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Ljubljana', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\London', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\London', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Luxembourg', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Luxembourg', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Madrid', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Madrid', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Malta', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Malta', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Mariehamn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Mariehamn', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Minsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Minsk', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Monaco', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Monaco', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Moscow', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Moscow', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Nicosia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Nicosia', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Oslo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Oslo', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Paris', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Paris', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Podgorica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Podgorica', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Prague', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Prague', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Riga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Riga', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Rome', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Rome', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Samara', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Samara', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\San_Marino', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\San_Marino', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Sarajevo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Sarajevo', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Saratov', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Saratov', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Simferopol', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Simferopol', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Skopje', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Skopje', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Sofia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Sofia', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Stockholm', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Stockholm', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Tallinn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Tallinn', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Tirane', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Tirane', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Tiraspol', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Tiraspol', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Ulyanovsk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Ulyanovsk', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Uzhgorod', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Uzhgorod', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vaduz', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vaduz', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vatican', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vatican', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vienna', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vienna', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Vilnius', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Vilnius', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Volgograd', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Volgograd', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Warsaw', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Warsaw', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Zagreb', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Zagreb', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Zaporozhye', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Zaporozhye', + 'DATA'), + ('_tcl_data\\tzdata\\Europe\\Zurich', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Europe\\Zurich', + 'DATA'), + ('_tcl_data\\tzdata\\GB', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GB', 'DATA'), + ('_tcl_data\\tzdata\\GB-Eire', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GB-Eire', + 'DATA'), + ('_tcl_data\\tzdata\\GMT', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT', 'DATA'), + ('_tcl_data\\tzdata\\GMT+0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT+0', + 'DATA'), + ('_tcl_data\\tzdata\\GMT-0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT-0', + 'DATA'), + ('_tcl_data\\tzdata\\GMT0', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\GMT0', + 'DATA'), + ('_tcl_data\\tzdata\\Greenwich', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Greenwich', + 'DATA'), + ('_tcl_data\\tzdata\\HST', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\HST', 'DATA'), + ('_tcl_data\\tzdata\\Hongkong', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Hongkong', + 'DATA'), + ('_tcl_data\\tzdata\\Iceland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Iceland', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Antananarivo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Antananarivo', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Chagos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Chagos', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Christmas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Christmas', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Cocos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Cocos', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Comoro', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Comoro', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Kerguelen', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Kerguelen', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Mahe', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Mahe', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Maldives', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Maldives', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Mauritius', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Mauritius', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Mayotte', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Mayotte', + 'DATA'), + ('_tcl_data\\tzdata\\Indian\\Reunion', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Indian\\Reunion', + 'DATA'), + ('_tcl_data\\tzdata\\Iran', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Iran', + 'DATA'), + ('_tcl_data\\tzdata\\Israel', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Israel', + 'DATA'), + ('_tcl_data\\tzdata\\Jamaica', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Jamaica', + 'DATA'), + ('_tcl_data\\tzdata\\Japan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Japan', + 'DATA'), + ('_tcl_data\\tzdata\\Kwajalein', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Kwajalein', + 'DATA'), + ('_tcl_data\\tzdata\\Libya', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Libya', + 'DATA'), + ('_tcl_data\\tzdata\\MET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\MET', 'DATA'), + ('_tcl_data\\tzdata\\MST', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\MST', 'DATA'), + ('_tcl_data\\tzdata\\MST7MDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\MST7MDT', + 'DATA'), + ('_tcl_data\\tzdata\\Mexico\\BajaNorte', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Mexico\\BajaNorte', + 'DATA'), + ('_tcl_data\\tzdata\\Mexico\\BajaSur', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Mexico\\BajaSur', + 'DATA'), + ('_tcl_data\\tzdata\\Mexico\\General', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Mexico\\General', + 'DATA'), + ('_tcl_data\\tzdata\\NZ', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\NZ', 'DATA'), + ('_tcl_data\\tzdata\\NZ-CHAT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\NZ-CHAT', + 'DATA'), + ('_tcl_data\\tzdata\\Navajo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Navajo', + 'DATA'), + ('_tcl_data\\tzdata\\PRC', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\PRC', 'DATA'), + ('_tcl_data\\tzdata\\PST8PDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\PST8PDT', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Apia', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Apia', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Auckland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Auckland', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Bougainville', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Bougainville', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Chatham', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Chatham', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Chuuk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Chuuk', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Easter', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Easter', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Efate', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Efate', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Enderbury', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Enderbury', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Fakaofo', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Fakaofo', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Fiji', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Fiji', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Funafuti', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Funafuti', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Galapagos', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Galapagos', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Gambier', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Gambier', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Guadalcanal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Guadalcanal', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Guam', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Guam', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Honolulu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Honolulu', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Johnston', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Johnston', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kanton', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kanton', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kiritimati', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kiritimati', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kosrae', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kosrae', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Kwajalein', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Kwajalein', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Majuro', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Majuro', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Marquesas', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Marquesas', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Midway', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Midway', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Nauru', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Nauru', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Niue', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Niue', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Norfolk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Norfolk', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Noumea', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Noumea', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Pago_Pago', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Pago_Pago', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Palau', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Palau', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Pitcairn', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Pitcairn', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Pohnpei', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Pohnpei', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Ponape', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Ponape', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Port_Moresby', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Port_Moresby', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Rarotonga', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Rarotonga', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Saipan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Saipan', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Samoa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Samoa', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Tahiti', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Tahiti', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Tarawa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Tarawa', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Tongatapu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Tongatapu', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Truk', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Truk', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Wake', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Wake', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Wallis', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Wallis', + 'DATA'), + ('_tcl_data\\tzdata\\Pacific\\Yap', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Pacific\\Yap', + 'DATA'), + ('_tcl_data\\tzdata\\Poland', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Poland', + 'DATA'), + ('_tcl_data\\tzdata\\Portugal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Portugal', + 'DATA'), + ('_tcl_data\\tzdata\\ROC', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\ROC', 'DATA'), + ('_tcl_data\\tzdata\\ROK', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\ROK', 'DATA'), + ('_tcl_data\\tzdata\\Singapore', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Singapore', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\AST4', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\AST4', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\AST4ADT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\AST4ADT', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\CST6', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\CST6', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\CST6CDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\CST6CDT', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\EST5', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\EST5', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\EST5EDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\EST5EDT', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\HST10', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\HST10', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\MST7', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\MST7', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\MST7MDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\MST7MDT', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\PST8', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\PST8', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\PST8PDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\PST8PDT', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\YST9', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\YST9', + 'DATA'), + ('_tcl_data\\tzdata\\SystemV\\YST9YDT', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\SystemV\\YST9YDT', + 'DATA'), + ('_tcl_data\\tzdata\\Turkey', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Turkey', + 'DATA'), + ('_tcl_data\\tzdata\\UCT', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\UCT', 'DATA'), + ('_tcl_data\\tzdata\\US\\Alaska', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Alaska', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Aleutian', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Aleutian', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Arizona', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Arizona', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Central', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Central', + 'DATA'), + ('_tcl_data\\tzdata\\US\\East-Indiana', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\East-Indiana', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Eastern', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Eastern', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Hawaii', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Hawaii', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Indiana-Starke', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Indiana-Starke', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Michigan', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Michigan', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Mountain', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Mountain', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Pacific', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Pacific', + 'DATA'), + ('_tcl_data\\tzdata\\US\\Samoa', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\US\\Samoa', + 'DATA'), + ('_tcl_data\\tzdata\\UTC', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\UTC', 'DATA'), + ('_tcl_data\\tzdata\\Universal', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Universal', + 'DATA'), + ('_tcl_data\\tzdata\\W-SU', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\W-SU', + 'DATA'), + ('_tcl_data\\tzdata\\WET', 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\WET', 'DATA'), + ('_tcl_data\\tzdata\\Zulu', + 'C:\\Python312\\tcl\\tcl8.6\\tzdata\\Zulu', + 'DATA'), + ('_tcl_data\\word.tcl', 'C:\\Python312\\tcl\\tcl8.6\\word.tcl', 'DATA'), + ('_tk_data\\bgerror.tcl', 'C:\\Python312\\tcl\\tk8.6\\bgerror.tcl', 'DATA'), + ('_tk_data\\button.tcl', 'C:\\Python312\\tcl\\tk8.6\\button.tcl', 'DATA'), + ('_tk_data\\choosedir.tcl', + 'C:\\Python312\\tcl\\tk8.6\\choosedir.tcl', + 'DATA'), + ('_tk_data\\clrpick.tcl', 'C:\\Python312\\tcl\\tk8.6\\clrpick.tcl', 'DATA'), + ('_tk_data\\comdlg.tcl', 'C:\\Python312\\tcl\\tk8.6\\comdlg.tcl', 'DATA'), + ('_tk_data\\console.tcl', 'C:\\Python312\\tcl\\tk8.6\\console.tcl', 'DATA'), + ('_tk_data\\dialog.tcl', 'C:\\Python312\\tcl\\tk8.6\\dialog.tcl', 'DATA'), + ('_tk_data\\entry.tcl', 'C:\\Python312\\tcl\\tk8.6\\entry.tcl', 'DATA'), + ('_tk_data\\focus.tcl', 'C:\\Python312\\tcl\\tk8.6\\focus.tcl', 'DATA'), + ('_tk_data\\fontchooser.tcl', + 'C:\\Python312\\tcl\\tk8.6\\fontchooser.tcl', + 'DATA'), + ('_tk_data\\iconlist.tcl', 'C:\\Python312\\tcl\\tk8.6\\iconlist.tcl', 'DATA'), + ('_tk_data\\icons.tcl', 'C:\\Python312\\tcl\\tk8.6\\icons.tcl', 'DATA'), + ('_tk_data\\images\\README', + 'C:\\Python312\\tcl\\tk8.6\\images\\README', + 'DATA'), + ('_tk_data\\images\\logo.eps', + 'C:\\Python312\\tcl\\tk8.6\\images\\logo.eps', + 'DATA'), + ('_tk_data\\images\\logo100.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logo100.gif', + 'DATA'), + ('_tk_data\\images\\logo64.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logo64.gif', + 'DATA'), + ('_tk_data\\images\\logoLarge.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logoLarge.gif', + 'DATA'), + ('_tk_data\\images\\logoMed.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\logoMed.gif', + 'DATA'), + ('_tk_data\\images\\pwrdLogo.eps', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo.eps', + 'DATA'), + ('_tk_data\\images\\pwrdLogo100.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo100.gif', + 'DATA'), + ('_tk_data\\images\\pwrdLogo150.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo150.gif', + 'DATA'), + ('_tk_data\\images\\pwrdLogo175.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo175.gif', + 'DATA'), + ('_tk_data\\images\\pwrdLogo200.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo200.gif', + 'DATA'), + ('_tk_data\\images\\pwrdLogo75.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\pwrdLogo75.gif', + 'DATA'), + ('_tk_data\\images\\tai-ku.gif', + 'C:\\Python312\\tcl\\tk8.6\\images\\tai-ku.gif', + 'DATA'), + ('_tk_data\\license.terms', + 'C:\\Python312\\tcl\\tk8.6\\license.terms', + 'DATA'), + ('_tk_data\\listbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\listbox.tcl', 'DATA'), + ('_tk_data\\megawidget.tcl', + 'C:\\Python312\\tcl\\tk8.6\\megawidget.tcl', + 'DATA'), + ('_tk_data\\menu.tcl', 'C:\\Python312\\tcl\\tk8.6\\menu.tcl', 'DATA'), + ('_tk_data\\mkpsenc.tcl', 'C:\\Python312\\tcl\\tk8.6\\mkpsenc.tcl', 'DATA'), + ('_tk_data\\msgbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\msgbox.tcl', 'DATA'), + ('_tk_data\\msgs\\cs.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\cs.msg', 'DATA'), + ('_tk_data\\msgs\\da.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\da.msg', 'DATA'), + ('_tk_data\\msgs\\de.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\de.msg', 'DATA'), + ('_tk_data\\msgs\\el.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\el.msg', 'DATA'), + ('_tk_data\\msgs\\en.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\en.msg', 'DATA'), + ('_tk_data\\msgs\\en_gb.msg', + 'C:\\Python312\\tcl\\tk8.6\\msgs\\en_gb.msg', + 'DATA'), + ('_tk_data\\msgs\\eo.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\eo.msg', 'DATA'), + ('_tk_data\\msgs\\es.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\es.msg', 'DATA'), + ('_tk_data\\msgs\\fi.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\fi.msg', 'DATA'), + ('_tk_data\\msgs\\fr.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\fr.msg', 'DATA'), + ('_tk_data\\msgs\\hu.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\hu.msg', 'DATA'), + ('_tk_data\\msgs\\it.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\it.msg', 'DATA'), + ('_tk_data\\msgs\\nl.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\nl.msg', 'DATA'), + ('_tk_data\\msgs\\pl.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\pl.msg', 'DATA'), + ('_tk_data\\msgs\\pt.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\pt.msg', 'DATA'), + ('_tk_data\\msgs\\ru.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\ru.msg', 'DATA'), + ('_tk_data\\msgs\\sv.msg', 'C:\\Python312\\tcl\\tk8.6\\msgs\\sv.msg', 'DATA'), + ('_tk_data\\msgs\\zh_cn.msg', + 'C:\\Python312\\tcl\\tk8.6\\msgs\\zh_cn.msg', + 'DATA'), + ('_tk_data\\obsolete.tcl', 'C:\\Python312\\tcl\\tk8.6\\obsolete.tcl', 'DATA'), + ('_tk_data\\optMenu.tcl', 'C:\\Python312\\tcl\\tk8.6\\optMenu.tcl', 'DATA'), + ('_tk_data\\palette.tcl', 'C:\\Python312\\tcl\\tk8.6\\palette.tcl', 'DATA'), + ('_tk_data\\panedwindow.tcl', + 'C:\\Python312\\tcl\\tk8.6\\panedwindow.tcl', + 'DATA'), + ('_tk_data\\pkgIndex.tcl', 'C:\\Python312\\tcl\\tk8.6\\pkgIndex.tcl', 'DATA'), + ('_tk_data\\safetk.tcl', 'C:\\Python312\\tcl\\tk8.6\\safetk.tcl', 'DATA'), + ('_tk_data\\scale.tcl', 'C:\\Python312\\tcl\\tk8.6\\scale.tcl', 'DATA'), + ('_tk_data\\scrlbar.tcl', 'C:\\Python312\\tcl\\tk8.6\\scrlbar.tcl', 'DATA'), + ('_tk_data\\spinbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\spinbox.tcl', 'DATA'), + ('_tk_data\\tclIndex', 'C:\\Python312\\tcl\\tk8.6\\tclIndex', 'DATA'), + ('_tk_data\\tearoff.tcl', 'C:\\Python312\\tcl\\tk8.6\\tearoff.tcl', 'DATA'), + ('_tk_data\\text.tcl', 'C:\\Python312\\tcl\\tk8.6\\text.tcl', 'DATA'), + ('_tk_data\\tk.tcl', 'C:\\Python312\\tcl\\tk8.6\\tk.tcl', 'DATA'), + ('_tk_data\\tkfbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\tkfbox.tcl', 'DATA'), + ('_tk_data\\ttk\\altTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\altTheme.tcl', + 'DATA'), + ('_tk_data\\ttk\\aquaTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\aquaTheme.tcl', + 'DATA'), + ('_tk_data\\ttk\\button.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\button.tcl', + 'DATA'), + ('_tk_data\\ttk\\clamTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\clamTheme.tcl', + 'DATA'), + ('_tk_data\\ttk\\classicTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\classicTheme.tcl', + 'DATA'), + ('_tk_data\\ttk\\combobox.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\combobox.tcl', + 'DATA'), + ('_tk_data\\ttk\\cursors.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\cursors.tcl', + 'DATA'), + ('_tk_data\\ttk\\defaults.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\defaults.tcl', + 'DATA'), + ('_tk_data\\ttk\\entry.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\entry.tcl', + 'DATA'), + ('_tk_data\\ttk\\fonts.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\fonts.tcl', + 'DATA'), + ('_tk_data\\ttk\\menubutton.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\menubutton.tcl', + 'DATA'), + ('_tk_data\\ttk\\notebook.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\notebook.tcl', + 'DATA'), + ('_tk_data\\ttk\\panedwindow.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\panedwindow.tcl', + 'DATA'), + ('_tk_data\\ttk\\progress.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\progress.tcl', + 'DATA'), + ('_tk_data\\ttk\\scale.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\scale.tcl', + 'DATA'), + ('_tk_data\\ttk\\scrollbar.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\scrollbar.tcl', + 'DATA'), + ('_tk_data\\ttk\\sizegrip.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\sizegrip.tcl', + 'DATA'), + ('_tk_data\\ttk\\spinbox.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\spinbox.tcl', + 'DATA'), + ('_tk_data\\ttk\\treeview.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\treeview.tcl', + 'DATA'), + ('_tk_data\\ttk\\ttk.tcl', 'C:\\Python312\\tcl\\tk8.6\\ttk\\ttk.tcl', 'DATA'), + ('_tk_data\\ttk\\utils.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\utils.tcl', + 'DATA'), + ('_tk_data\\ttk\\vistaTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\vistaTheme.tcl', + 'DATA'), + ('_tk_data\\ttk\\winTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\winTheme.tcl', + 'DATA'), + ('_tk_data\\ttk\\xpTheme.tcl', + 'C:\\Python312\\tcl\\tk8.6\\ttk\\xpTheme.tcl', + 'DATA'), + ('_tk_data\\unsupported.tcl', + 'C:\\Python312\\tcl\\tk8.6\\unsupported.tcl', + 'DATA'), + ('_tk_data\\xmfbox.tcl', 'C:\\Python312\\tcl\\tk8.6\\xmfbox.tcl', 'DATA'), + ('_tkinter.pyd', 'C:\\Python312\\DLLs\\_tkinter.pyd', 'EXTENSION'), + ('_wmi.pyd', 'C:\\Python312\\DLLs\\_wmi.pyd', 'EXTENSION'), + ('api-ms-win-core-console-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-console-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-datetime-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-datetime-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-debug-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-debug-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-errorhandling-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-errorhandling-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-fibers-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-fibers-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-file-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-file-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-file-l1-2-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-file-l1-2-0.dll', + 'BINARY'), + ('api-ms-win-core-file-l2-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-file-l2-1-0.dll', + 'BINARY'), + ('api-ms-win-core-handle-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-handle-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-heap-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-heap-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-interlocked-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-interlocked-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-libraryloader-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-libraryloader-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-localization-l1-2-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-localization-l1-2-0.dll', + 'BINARY'), + ('api-ms-win-core-memory-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-memory-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-namedpipe-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-namedpipe-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-processenvironment-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-processenvironment-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-processthreads-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-processthreads-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-processthreads-l1-1-1.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-processthreads-l1-1-1.dll', + 'BINARY'), + ('api-ms-win-core-profile-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-profile-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-rtlsupport-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-rtlsupport-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-string-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-string-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-synch-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-synch-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-synch-l1-2-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-synch-l1-2-0.dll', + 'BINARY'), + ('api-ms-win-core-sysinfo-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-sysinfo-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-timezone-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-timezone-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-core-util-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-core-util-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-conio-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-conio-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-convert-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-convert-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-environment-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-environment-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-filesystem-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-filesystem-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-heap-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-heap-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-locale-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-locale-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-math-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-math-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-private-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-private-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-process-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-process-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-runtime-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-runtime-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-stdio-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-stdio-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-string-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-string-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-time-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-time-l1-1-0.dll', + 'BINARY'), + ('api-ms-win-crt-utility-l1-1-0.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\api-ms-win-crt-utility-l1-1-0.dll', + 'BINARY'), + ('base_library.zip', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\base_library.zip', + 'DATA'), + ('cv2\\Error\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\Error\\__init__.pyi', + 'DATA'), + ('cv2\\LICENSE-3RD-PARTY.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\LICENSE-3RD-PARTY.txt', + 'DATA'), + ('cv2\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\LICENSE.txt', + 'DATA'), + ('cv2\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\__init__.py', + 'DATA'), + ('cv2\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\__init__.pyi', + 'DATA'), + ('cv2\\aruco\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\aruco\\__init__.pyi', + 'DATA'), + ('cv2\\barcode\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\barcode\\__init__.pyi', + 'DATA'), + ('cv2\\ccm\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ccm\\__init__.pyi', + 'DATA'), + ('cv2\\config-3.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config-3.py', + 'DATA'), + ('cv2\\config.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\config.py', + 'DATA'), + ('cv2\\cuda\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\cuda\\__init__.pyi', + 'DATA'), + ('cv2\\cv2.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\cv2.pyd', + 'EXTENSION'), + ('cv2\\data\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\data\\__init__.py', + 'DATA'), + ('cv2\\detail\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\detail\\__init__.pyi', + 'DATA'), + ('cv2\\dnn\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\dnn\\__init__.pyi', + 'DATA'), + ('cv2\\fisheye\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\fisheye\\__init__.pyi', + 'DATA'), + ('cv2\\flann\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\flann\\__init__.pyi', + 'DATA'), + ('cv2\\instr\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\instr\\__init__.pyi', + 'DATA'), + ('cv2\\ipp\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ipp\\__init__.pyi', + 'DATA'), + ('cv2\\load_config_py2.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py2.py', + 'DATA'), + ('cv2\\load_config_py3.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\load_config_py3.py', + 'DATA'), + ('cv2\\mat_wrapper\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\mat_wrapper\\__init__.py', + 'DATA'), + ('cv2\\mcc\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\mcc\\__init__.pyi', + 'DATA'), + ('cv2\\misc\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\__init__.py', + 'DATA'), + ('cv2\\misc\\version.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\misc\\version.py', + 'DATA'), + ('cv2\\ocl\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ocl\\__init__.pyi', + 'DATA'), + ('cv2\\ogl\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\ogl\\__init__.pyi', + 'DATA'), + ('cv2\\opencv_videoio_ffmpeg500_64.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg500_64.dll', + 'BINARY'), + ('cv2\\parallel\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\parallel\\__init__.pyi', + 'DATA'), + ('cv2\\py.typed', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\py.typed', + 'DATA'), + ('cv2\\samples\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\samples\\__init__.pyi', + 'DATA'), + ('cv2\\segmentation\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\segmentation\\__init__.pyi', + 'DATA'), + ('cv2\\typing\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\typing\\__init__.py', + 'DATA'), + ('cv2\\utils\\__init__.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\__init__.py', + 'DATA'), + ('cv2\\utils\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\fs\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\fs\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\logging\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\logging\\__init__.pyi', + 'DATA'), + ('cv2\\utils\\nested\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\utils\\nested\\__init__.pyi', + 'DATA'), + ('cv2\\version.py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\version.py', + 'DATA'), + ('cv2\\videoio_registry\\__init__.pyi', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\cv2\\videoio_registry\\__init__.pyi', + 'DATA'), + ('libcrypto-3.dll', 'C:\\Python312\\DLLs\\libcrypto-3.dll', 'BINARY'), + ('libffi-8.dll', 'C:\\Python312\\DLLs\\libffi-8.dll', 'BINARY'), + ('libssl-3.dll', 'C:\\Python312\\DLLs\\libssl-3.dll', 'BINARY'), + ('numpy-2.5.2.dist-info\\DELVEWHEEL', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\DELVEWHEEL', + 'DATA'), + ('numpy-2.5.2.dist-info\\INSTALLER', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\INSTALLER', + 'DATA'), + ('numpy-2.5.2.dist-info\\METADATA', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\METADATA', + 'DATA'), + ('numpy-2.5.2.dist-info\\RECORD', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\RECORD', + 'DATA'), + ('numpy-2.5.2.dist-info\\REQUESTED', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\REQUESTED', + 'DATA'), + ('numpy-2.5.2.dist-info\\WHEEL', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\WHEEL', + 'DATA'), + ('numpy-2.5.2.dist-info\\entry_points.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\entry_points.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\ma\\LICENSE', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\ma\\LICENSE', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md', + 'DATA'), + ('numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy-2.5.2.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md', + 'DATA'), + ('numpy.libs\\libscipy_openblas64_-327b2e0bcffce2882e0dc04cdeb4eaa6.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy.libs\\libscipy_openblas64_-327b2e0bcffce2882e0dc04cdeb4eaa6.dll', + 'BINARY'), + ('numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll', + 'BINARY'), + ('numpy\\_core\\_multiarray_tests.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_multiarray_tests.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\_core\\_multiarray_umath.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_multiarray_umath.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\fft\\_pocketfft_umath.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_pocketfft_umath.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\linalg\\_umath_linalg.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\_umath_linalg.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_bounded_integers.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_bounded_integers.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_common.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_common.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_generator.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_generator.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_mt19937.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_mt19937.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_pcg64.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_pcg64.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_philox.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_philox.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\_sfc64.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_sfc64.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\bit_generator.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\bit_generator.cp312-win_amd64.pyd', + 'EXTENSION'), + ('numpy\\random\\mtrand.cp312-win_amd64.pyd', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\mtrand.cp312-win_amd64.pyd', + 'EXTENSION'), + ('opencv_python-5.0.0.93.dist-info\\INSTALLER', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\INSTALLER', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\LICENSE-3RD-PARTY.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\LICENSE-3RD-PARTY.txt', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\LICENSE.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\LICENSE.txt', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\METADATA', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\METADATA', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\RECORD', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\RECORD', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\REQUESTED', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\REQUESTED', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\WHEEL', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\WHEEL', + 'DATA'), + ('opencv_python-5.0.0.93.dist-info\\top_level.txt', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\opencv_python-5.0.0.93.dist-info\\top_level.txt', + 'DATA'), + ('pyexpat.pyd', 'C:\\Python312\\DLLs\\pyexpat.pyd', 'EXTENSION'), + ('python3.dll', 'C:\\Python312\\python3.dll', 'BINARY'), + ('python312.dll', 'C:\\Python312\\python312.dll', 'BINARY'), + ('select.pyd', 'C:\\Python312\\DLLs\\select.pyd', 'EXTENSION'), + ('tcl86t.dll', 'C:\\Python312\\DLLs\\tcl86t.dll', 'BINARY'), + ('tcl8\\8.4\\platform-1.0.19.tm', + 'C:\\Python312\\tcl\\tcl8\\8.4\\platform-1.0.19.tm', + 'DATA'), + ('tcl8\\8.4\\platform\\shell-1.1.4.tm', + 'C:\\Python312\\tcl\\tcl8\\8.4\\platform\\shell-1.1.4.tm', + 'DATA'), + ('tcl8\\8.5\\msgcat-1.6.1.tm', + 'C:\\Python312\\tcl\\tcl8\\8.5\\msgcat-1.6.1.tm', + 'DATA'), + ('tcl8\\8.5\\tcltest-2.5.5.tm', + 'C:\\Python312\\tcl\\tcl8\\8.5\\tcltest-2.5.5.tm', + 'DATA'), + ('tcl8\\8.6\\http-2.9.8.tm', + 'C:\\Python312\\tcl\\tcl8\\8.6\\http-2.9.8.tm', + 'DATA'), + ('tk86t.dll', 'C:\\Python312\\DLLs\\tk86t.dll', 'BINARY'), + ('ucrtbase.dll', + 'C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance ' + 'Toolkit\\ucrtbase.dll', + 'BINARY'), + ('unicodedata.pyd', 'C:\\Python312\\DLLs\\unicodedata.pyd', 'EXTENSION'), + ('zlib1.dll', 'C:\\Python312\\DLLs\\zlib1.dll', 'BINARY')],) diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/EXE-00.toc b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/EXE-00.toc new file mode 100644 index 000000000..fa97f9ab4 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/EXE-00.toc @@ -0,0 +1,77 @@ +('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\MaskReviewer.exe', + False, + False, + True, + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-windowed.ico', + None, + False, + False, + b'\n\n \n \n \n \n \n \n \n ' + b'\n <' + b'application>\n \n \n ' + b' \n \n \n \n <' + b'/compatibility>\n ' + b'\n \n true\n \n \n \n \n \n \n \n', + True, + False, + None, + None, + None, + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\MaskReviewer.pkg', + [('pyi-contents-directory _internal', '', 'OPTION'), + ('PYZ-00.pyz', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\PYZ-00.pyz', + 'PYZ'), + ('struct', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\struct.pyc', + 'PYMODULE'), + ('pyimod01_archive', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod01_archive.pyc', + 'PYMODULE'), + ('pyimod02_importers', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod02_importers.pyc', + 'PYMODULE'), + ('pyimod03_ctypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod03_ctypes.pyc', + 'PYMODULE'), + ('pyimod04_pywin32', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod04_pywin32.pyc', + 'PYMODULE'), + ('pyiboot01_bootstrap', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py', + 'PYSOURCE'), + ('pyi_rth__tkinter', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py', + 'PYSOURCE'), + ('pyi_rth_inspect', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py', + 'PYSOURCE'), + ('pyi_rth_pkgutil', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py', + 'PYSOURCE'), + ('pyi_rth_multiprocessing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py', + 'PYSOURCE'), + ('MaskReviewer', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\MaskReviewer.py', + 'PYSOURCE')], + [], + False, + False, + 1787944409, + [('runw.exe', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\runw.exe', + 'EXECUTABLE')], + 'C:\\Python312\\python312.dll') diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/MaskReviewer.exe b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/MaskReviewer.exe new file mode 100644 index 000000000..be22ca00d Binary files /dev/null and b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/MaskReviewer.exe differ diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/MaskReviewer.pkg b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/MaskReviewer.pkg new file mode 100644 index 000000000..e78bb5969 Binary files /dev/null and b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/MaskReviewer.pkg differ diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PKG-00.toc b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PKG-00.toc new file mode 100644 index 000000000..4ae6328d5 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PKG-00.toc @@ -0,0 +1,55 @@ +('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\MaskReviewer.pkg', + {'BINARY': True, + 'DATA': True, + 'EXECUTABLE': True, + 'EXTENSION': True, + 'PYMODULE': True, + 'PYSOURCE': True, + 'PYZ': False, + 'SPLASH': True, + 'SYMLINK': False}, + [('pyi-contents-directory _internal', '', 'OPTION'), + ('PYZ-00.pyz', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\PYZ-00.pyz', + 'PYZ'), + ('struct', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\struct.pyc', + 'PYMODULE'), + ('pyimod01_archive', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod01_archive.pyc', + 'PYMODULE'), + ('pyimod02_importers', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod02_importers.pyc', + 'PYMODULE'), + ('pyimod03_ctypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod03_ctypes.pyc', + 'PYMODULE'), + ('pyimod04_pywin32', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\localpycs\\pyimod04_pywin32.pyc', + 'PYMODULE'), + ('pyiboot01_bootstrap', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py', + 'PYSOURCE'), + ('pyi_rth__tkinter', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py', + 'PYSOURCE'), + ('pyi_rth_inspect', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py', + 'PYSOURCE'), + ('pyi_rth_pkgutil', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py', + 'PYSOURCE'), + ('pyi_rth_multiprocessing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py', + 'PYSOURCE'), + ('MaskReviewer', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\MaskReviewer.py', + 'PYSOURCE')], + 'python312.dll', + True, + False, + False, + [], + None, + None, + None) diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PYZ-00.pyz b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PYZ-00.pyz new file mode 100644 index 000000000..9e91e7584 Binary files /dev/null and b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PYZ-00.pyz differ diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PYZ-00.toc b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PYZ-00.toc new file mode 100644 index 000000000..26ac02e3a --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/PYZ-00.toc @@ -0,0 +1,815 @@ +('C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\build\\MaskReviewer\\PYZ-00.pyz', + [('__future__', 'C:\\Python312\\Lib\\__future__.py', 'PYMODULE'), + ('_aix_support', 'C:\\Python312\\Lib\\_aix_support.py', 'PYMODULE'), + ('_compat_pickle', 'C:\\Python312\\Lib\\_compat_pickle.py', 'PYMODULE'), + ('_compression', 'C:\\Python312\\Lib\\_compression.py', 'PYMODULE'), + ('_py_abc', 'C:\\Python312\\Lib\\_py_abc.py', 'PYMODULE'), + ('_pydatetime', 'C:\\Python312\\Lib\\_pydatetime.py', 'PYMODULE'), + ('_pydecimal', 'C:\\Python312\\Lib\\_pydecimal.py', 'PYMODULE'), + ('_strptime', 'C:\\Python312\\Lib\\_strptime.py', 'PYMODULE'), + ('_threading_local', 'C:\\Python312\\Lib\\_threading_local.py', 'PYMODULE'), + ('argparse', 'C:\\Python312\\Lib\\argparse.py', 'PYMODULE'), + ('ast', 'C:\\Python312\\Lib\\ast.py', 'PYMODULE'), + ('asyncio', 'C:\\Python312\\Lib\\asyncio\\__init__.py', 'PYMODULE'), + ('asyncio.base_events', + 'C:\\Python312\\Lib\\asyncio\\base_events.py', + 'PYMODULE'), + ('asyncio.base_futures', + 'C:\\Python312\\Lib\\asyncio\\base_futures.py', + 'PYMODULE'), + ('asyncio.base_subprocess', + 'C:\\Python312\\Lib\\asyncio\\base_subprocess.py', + 'PYMODULE'), + ('asyncio.base_tasks', + 'C:\\Python312\\Lib\\asyncio\\base_tasks.py', + 'PYMODULE'), + ('asyncio.constants', + 'C:\\Python312\\Lib\\asyncio\\constants.py', + 'PYMODULE'), + ('asyncio.coroutines', + 'C:\\Python312\\Lib\\asyncio\\coroutines.py', + 'PYMODULE'), + ('asyncio.events', 'C:\\Python312\\Lib\\asyncio\\events.py', 'PYMODULE'), + ('asyncio.exceptions', + 'C:\\Python312\\Lib\\asyncio\\exceptions.py', + 'PYMODULE'), + ('asyncio.format_helpers', + 'C:\\Python312\\Lib\\asyncio\\format_helpers.py', + 'PYMODULE'), + ('asyncio.futures', 'C:\\Python312\\Lib\\asyncio\\futures.py', 'PYMODULE'), + ('asyncio.locks', 'C:\\Python312\\Lib\\asyncio\\locks.py', 'PYMODULE'), + ('asyncio.log', 'C:\\Python312\\Lib\\asyncio\\log.py', 'PYMODULE'), + ('asyncio.mixins', 'C:\\Python312\\Lib\\asyncio\\mixins.py', 'PYMODULE'), + ('asyncio.proactor_events', + 'C:\\Python312\\Lib\\asyncio\\proactor_events.py', + 'PYMODULE'), + ('asyncio.protocols', + 'C:\\Python312\\Lib\\asyncio\\protocols.py', + 'PYMODULE'), + ('asyncio.queues', 'C:\\Python312\\Lib\\asyncio\\queues.py', 'PYMODULE'), + ('asyncio.runners', 'C:\\Python312\\Lib\\asyncio\\runners.py', 'PYMODULE'), + ('asyncio.selector_events', + 'C:\\Python312\\Lib\\asyncio\\selector_events.py', + 'PYMODULE'), + ('asyncio.sslproto', 'C:\\Python312\\Lib\\asyncio\\sslproto.py', 'PYMODULE'), + ('asyncio.staggered', + 'C:\\Python312\\Lib\\asyncio\\staggered.py', + 'PYMODULE'), + ('asyncio.streams', 'C:\\Python312\\Lib\\asyncio\\streams.py', 'PYMODULE'), + ('asyncio.subprocess', + 'C:\\Python312\\Lib\\asyncio\\subprocess.py', + 'PYMODULE'), + ('asyncio.taskgroups', + 'C:\\Python312\\Lib\\asyncio\\taskgroups.py', + 'PYMODULE'), + ('asyncio.tasks', 'C:\\Python312\\Lib\\asyncio\\tasks.py', 'PYMODULE'), + ('asyncio.threads', 'C:\\Python312\\Lib\\asyncio\\threads.py', 'PYMODULE'), + ('asyncio.timeouts', 'C:\\Python312\\Lib\\asyncio\\timeouts.py', 'PYMODULE'), + ('asyncio.transports', + 'C:\\Python312\\Lib\\asyncio\\transports.py', + 'PYMODULE'), + ('asyncio.trsock', 'C:\\Python312\\Lib\\asyncio\\trsock.py', 'PYMODULE'), + ('asyncio.unix_events', + 'C:\\Python312\\Lib\\asyncio\\unix_events.py', + 'PYMODULE'), + ('asyncio.windows_events', + 'C:\\Python312\\Lib\\asyncio\\windows_events.py', + 'PYMODULE'), + ('asyncio.windows_utils', + 'C:\\Python312\\Lib\\asyncio\\windows_utils.py', + 'PYMODULE'), + ('base64', 'C:\\Python312\\Lib\\base64.py', 'PYMODULE'), + ('bdb', 'C:\\Python312\\Lib\\bdb.py', 'PYMODULE'), + ('bisect', 'C:\\Python312\\Lib\\bisect.py', 'PYMODULE'), + ('bz2', 'C:\\Python312\\Lib\\bz2.py', 'PYMODULE'), + ('calendar', 'C:\\Python312\\Lib\\calendar.py', 'PYMODULE'), + ('cmd', 'C:\\Python312\\Lib\\cmd.py', 'PYMODULE'), + ('code', 'C:\\Python312\\Lib\\code.py', 'PYMODULE'), + ('codeop', 'C:\\Python312\\Lib\\codeop.py', 'PYMODULE'), + ('concurrent', 'C:\\Python312\\Lib\\concurrent\\__init__.py', 'PYMODULE'), + ('concurrent.futures', + 'C:\\Python312\\Lib\\concurrent\\futures\\__init__.py', + 'PYMODULE'), + ('concurrent.futures._base', + 'C:\\Python312\\Lib\\concurrent\\futures\\_base.py', + 'PYMODULE'), + ('concurrent.futures.process', + 'C:\\Python312\\Lib\\concurrent\\futures\\process.py', + 'PYMODULE'), + ('concurrent.futures.thread', + 'C:\\Python312\\Lib\\concurrent\\futures\\thread.py', + 'PYMODULE'), + ('contextlib', 'C:\\Python312\\Lib\\contextlib.py', 'PYMODULE'), + ('contextvars', 'C:\\Python312\\Lib\\contextvars.py', 'PYMODULE'), + ('copy', 'C:\\Python312\\Lib\\copy.py', 'PYMODULE'), + ('csv', 'C:\\Python312\\Lib\\csv.py', 'PYMODULE'), + ('ctypes', 'C:\\Python312\\Lib\\ctypes\\__init__.py', 'PYMODULE'), + ('ctypes._endian', 'C:\\Python312\\Lib\\ctypes\\_endian.py', 'PYMODULE'), + ('dataclasses', 'C:\\Python312\\Lib\\dataclasses.py', 'PYMODULE'), + ('datetime', 'C:\\Python312\\Lib\\datetime.py', 'PYMODULE'), + ('decimal', 'C:\\Python312\\Lib\\decimal.py', 'PYMODULE'), + ('difflib', 'C:\\Python312\\Lib\\difflib.py', 'PYMODULE'), + ('dis', 'C:\\Python312\\Lib\\dis.py', 'PYMODULE'), + ('doctest', 'C:\\Python312\\Lib\\doctest.py', 'PYMODULE'), + ('email', 'C:\\Python312\\Lib\\email\\__init__.py', 'PYMODULE'), + ('email._encoded_words', + 'C:\\Python312\\Lib\\email\\_encoded_words.py', + 'PYMODULE'), + ('email._header_value_parser', + 'C:\\Python312\\Lib\\email\\_header_value_parser.py', + 'PYMODULE'), + ('email._parseaddr', 'C:\\Python312\\Lib\\email\\_parseaddr.py', 'PYMODULE'), + ('email._policybase', + 'C:\\Python312\\Lib\\email\\_policybase.py', + 'PYMODULE'), + ('email.base64mime', 'C:\\Python312\\Lib\\email\\base64mime.py', 'PYMODULE'), + ('email.charset', 'C:\\Python312\\Lib\\email\\charset.py', 'PYMODULE'), + ('email.contentmanager', + 'C:\\Python312\\Lib\\email\\contentmanager.py', + 'PYMODULE'), + ('email.encoders', 'C:\\Python312\\Lib\\email\\encoders.py', 'PYMODULE'), + ('email.errors', 'C:\\Python312\\Lib\\email\\errors.py', 'PYMODULE'), + ('email.feedparser', 'C:\\Python312\\Lib\\email\\feedparser.py', 'PYMODULE'), + ('email.generator', 'C:\\Python312\\Lib\\email\\generator.py', 'PYMODULE'), + ('email.header', 'C:\\Python312\\Lib\\email\\header.py', 'PYMODULE'), + ('email.headerregistry', + 'C:\\Python312\\Lib\\email\\headerregistry.py', + 'PYMODULE'), + ('email.iterators', 'C:\\Python312\\Lib\\email\\iterators.py', 'PYMODULE'), + ('email.message', 'C:\\Python312\\Lib\\email\\message.py', 'PYMODULE'), + ('email.parser', 'C:\\Python312\\Lib\\email\\parser.py', 'PYMODULE'), + ('email.policy', 'C:\\Python312\\Lib\\email\\policy.py', 'PYMODULE'), + ('email.quoprimime', 'C:\\Python312\\Lib\\email\\quoprimime.py', 'PYMODULE'), + ('email.utils', 'C:\\Python312\\Lib\\email\\utils.py', 'PYMODULE'), + ('fileinput', 'C:\\Python312\\Lib\\fileinput.py', 'PYMODULE'), + ('fnmatch', 'C:\\Python312\\Lib\\fnmatch.py', 'PYMODULE'), + ('fractions', 'C:\\Python312\\Lib\\fractions.py', 'PYMODULE'), + ('ftplib', 'C:\\Python312\\Lib\\ftplib.py', 'PYMODULE'), + ('getopt', 'C:\\Python312\\Lib\\getopt.py', 'PYMODULE'), + ('getpass', 'C:\\Python312\\Lib\\getpass.py', 'PYMODULE'), + ('gettext', 'C:\\Python312\\Lib\\gettext.py', 'PYMODULE'), + ('glob', 'C:\\Python312\\Lib\\glob.py', 'PYMODULE'), + ('gzip', 'C:\\Python312\\Lib\\gzip.py', 'PYMODULE'), + ('hashlib', 'C:\\Python312\\Lib\\hashlib.py', 'PYMODULE'), + ('hmac', 'C:\\Python312\\Lib\\hmac.py', 'PYMODULE'), + ('html', 'C:\\Python312\\Lib\\html\\__init__.py', 'PYMODULE'), + ('html.entities', 'C:\\Python312\\Lib\\html\\entities.py', 'PYMODULE'), + ('http', 'C:\\Python312\\Lib\\http\\__init__.py', 'PYMODULE'), + ('http.client', 'C:\\Python312\\Lib\\http\\client.py', 'PYMODULE'), + ('http.cookiejar', 'C:\\Python312\\Lib\\http\\cookiejar.py', 'PYMODULE'), + ('http.server', 'C:\\Python312\\Lib\\http\\server.py', 'PYMODULE'), + ('importlib', 'C:\\Python312\\Lib\\importlib\\__init__.py', 'PYMODULE'), + ('importlib._abc', 'C:\\Python312\\Lib\\importlib\\_abc.py', 'PYMODULE'), + ('importlib._bootstrap', + 'C:\\Python312\\Lib\\importlib\\_bootstrap.py', + 'PYMODULE'), + ('importlib._bootstrap_external', + 'C:\\Python312\\Lib\\importlib\\_bootstrap_external.py', + 'PYMODULE'), + ('importlib.abc', 'C:\\Python312\\Lib\\importlib\\abc.py', 'PYMODULE'), + ('importlib.machinery', + 'C:\\Python312\\Lib\\importlib\\machinery.py', + 'PYMODULE'), + ('importlib.metadata', + 'C:\\Python312\\Lib\\importlib\\metadata\\__init__.py', + 'PYMODULE'), + ('importlib.metadata._adapters', + 'C:\\Python312\\Lib\\importlib\\metadata\\_adapters.py', + 'PYMODULE'), + ('importlib.metadata._collections', + 'C:\\Python312\\Lib\\importlib\\metadata\\_collections.py', + 'PYMODULE'), + ('importlib.metadata._functools', + 'C:\\Python312\\Lib\\importlib\\metadata\\_functools.py', + 'PYMODULE'), + ('importlib.metadata._itertools', + 'C:\\Python312\\Lib\\importlib\\metadata\\_itertools.py', + 'PYMODULE'), + ('importlib.metadata._meta', + 'C:\\Python312\\Lib\\importlib\\metadata\\_meta.py', + 'PYMODULE'), + ('importlib.metadata._text', + 'C:\\Python312\\Lib\\importlib\\metadata\\_text.py', + 'PYMODULE'), + ('importlib.readers', + 'C:\\Python312\\Lib\\importlib\\readers.py', + 'PYMODULE'), + ('importlib.resources', + 'C:\\Python312\\Lib\\importlib\\resources\\__init__.py', + 'PYMODULE'), + ('importlib.resources._adapters', + 'C:\\Python312\\Lib\\importlib\\resources\\_adapters.py', + 'PYMODULE'), + ('importlib.resources._common', + 'C:\\Python312\\Lib\\importlib\\resources\\_common.py', + 'PYMODULE'), + ('importlib.resources._itertools', + 'C:\\Python312\\Lib\\importlib\\resources\\_itertools.py', + 'PYMODULE'), + ('importlib.resources._legacy', + 'C:\\Python312\\Lib\\importlib\\resources\\_legacy.py', + 'PYMODULE'), + ('importlib.resources.abc', + 'C:\\Python312\\Lib\\importlib\\resources\\abc.py', + 'PYMODULE'), + ('importlib.resources.readers', + 'C:\\Python312\\Lib\\importlib\\resources\\readers.py', + 'PYMODULE'), + ('importlib.util', 'C:\\Python312\\Lib\\importlib\\util.py', 'PYMODULE'), + ('inspect', 'C:\\Python312\\Lib\\inspect.py', 'PYMODULE'), + ('ipaddress', 'C:\\Python312\\Lib\\ipaddress.py', 'PYMODULE'), + ('json', 'C:\\Python312\\Lib\\json\\__init__.py', 'PYMODULE'), + ('json.decoder', 'C:\\Python312\\Lib\\json\\decoder.py', 'PYMODULE'), + ('json.encoder', 'C:\\Python312\\Lib\\json\\encoder.py', 'PYMODULE'), + ('json.scanner', 'C:\\Python312\\Lib\\json\\scanner.py', 'PYMODULE'), + ('logging', 'C:\\Python312\\Lib\\logging\\__init__.py', 'PYMODULE'), + ('lzma', 'C:\\Python312\\Lib\\lzma.py', 'PYMODULE'), + ('mimetypes', 'C:\\Python312\\Lib\\mimetypes.py', 'PYMODULE'), + ('multiprocessing', + 'C:\\Python312\\Lib\\multiprocessing\\__init__.py', + 'PYMODULE'), + ('multiprocessing.connection', + 'C:\\Python312\\Lib\\multiprocessing\\connection.py', + 'PYMODULE'), + ('multiprocessing.context', + 'C:\\Python312\\Lib\\multiprocessing\\context.py', + 'PYMODULE'), + ('multiprocessing.dummy', + 'C:\\Python312\\Lib\\multiprocessing\\dummy\\__init__.py', + 'PYMODULE'), + ('multiprocessing.dummy.connection', + 'C:\\Python312\\Lib\\multiprocessing\\dummy\\connection.py', + 'PYMODULE'), + ('multiprocessing.forkserver', + 'C:\\Python312\\Lib\\multiprocessing\\forkserver.py', + 'PYMODULE'), + ('multiprocessing.heap', + 'C:\\Python312\\Lib\\multiprocessing\\heap.py', + 'PYMODULE'), + ('multiprocessing.managers', + 'C:\\Python312\\Lib\\multiprocessing\\managers.py', + 'PYMODULE'), + ('multiprocessing.pool', + 'C:\\Python312\\Lib\\multiprocessing\\pool.py', + 'PYMODULE'), + ('multiprocessing.popen_fork', + 'C:\\Python312\\Lib\\multiprocessing\\popen_fork.py', + 'PYMODULE'), + ('multiprocessing.popen_forkserver', + 'C:\\Python312\\Lib\\multiprocessing\\popen_forkserver.py', + 'PYMODULE'), + ('multiprocessing.popen_spawn_posix', + 'C:\\Python312\\Lib\\multiprocessing\\popen_spawn_posix.py', + 'PYMODULE'), + ('multiprocessing.popen_spawn_win32', + 'C:\\Python312\\Lib\\multiprocessing\\popen_spawn_win32.py', + 'PYMODULE'), + ('multiprocessing.process', + 'C:\\Python312\\Lib\\multiprocessing\\process.py', + 'PYMODULE'), + ('multiprocessing.queues', + 'C:\\Python312\\Lib\\multiprocessing\\queues.py', + 'PYMODULE'), + ('multiprocessing.reduction', + 'C:\\Python312\\Lib\\multiprocessing\\reduction.py', + 'PYMODULE'), + ('multiprocessing.resource_sharer', + 'C:\\Python312\\Lib\\multiprocessing\\resource_sharer.py', + 'PYMODULE'), + ('multiprocessing.resource_tracker', + 'C:\\Python312\\Lib\\multiprocessing\\resource_tracker.py', + 'PYMODULE'), + ('multiprocessing.shared_memory', + 'C:\\Python312\\Lib\\multiprocessing\\shared_memory.py', + 'PYMODULE'), + ('multiprocessing.sharedctypes', + 'C:\\Python312\\Lib\\multiprocessing\\sharedctypes.py', + 'PYMODULE'), + ('multiprocessing.spawn', + 'C:\\Python312\\Lib\\multiprocessing\\spawn.py', + 'PYMODULE'), + ('multiprocessing.synchronize', + 'C:\\Python312\\Lib\\multiprocessing\\synchronize.py', + 'PYMODULE'), + ('multiprocessing.util', + 'C:\\Python312\\Lib\\multiprocessing\\util.py', + 'PYMODULE'), + ('netrc', 'C:\\Python312\\Lib\\netrc.py', 'PYMODULE'), + ('nturl2path', 'C:\\Python312\\Lib\\nturl2path.py', 'PYMODULE'), + ('numbers', 'C:\\Python312\\Lib\\numbers.py', 'PYMODULE'), + ('numpy', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\__init__.py', + 'PYMODULE'), + ('numpy.__config__', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\__config__.py', + 'PYMODULE'), + ('numpy._array_api_info', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_array_api_info.py', + 'PYMODULE'), + ('numpy._core', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\__init__.py', + 'PYMODULE'), + ('numpy._core._add_newdocs', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_add_newdocs.py', + 'PYMODULE'), + ('numpy._core._add_newdocs_scalars', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_add_newdocs_scalars.py', + 'PYMODULE'), + ('numpy._core._asarray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_asarray.py', + 'PYMODULE'), + ('numpy._core._dtype', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_dtype.py', + 'PYMODULE'), + ('numpy._core._dtype_ctypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_dtype_ctypes.py', + 'PYMODULE'), + ('numpy._core._exceptions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_exceptions.py', + 'PYMODULE'), + ('numpy._core._internal', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_internal.py', + 'PYMODULE'), + ('numpy._core._methods', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_methods.py', + 'PYMODULE'), + ('numpy._core._string_helpers', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_string_helpers.py', + 'PYMODULE'), + ('numpy._core._type_aliases', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_type_aliases.py', + 'PYMODULE'), + ('numpy._core._ufunc_config', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\_ufunc_config.py', + 'PYMODULE'), + ('numpy._core.arrayprint', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\arrayprint.py', + 'PYMODULE'), + ('numpy._core.defchararray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\defchararray.py', + 'PYMODULE'), + ('numpy._core.einsumfunc', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\einsumfunc.py', + 'PYMODULE'), + ('numpy._core.fromnumeric', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\fromnumeric.py', + 'PYMODULE'), + ('numpy._core.function_base', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\function_base.py', + 'PYMODULE'), + ('numpy._core.getlimits', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\getlimits.py', + 'PYMODULE'), + ('numpy._core.memmap', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\memmap.py', + 'PYMODULE'), + ('numpy._core.multiarray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\multiarray.py', + 'PYMODULE'), + ('numpy._core.numeric', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\numeric.py', + 'PYMODULE'), + ('numpy._core.numerictypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\numerictypes.py', + 'PYMODULE'), + ('numpy._core.overrides', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\overrides.py', + 'PYMODULE'), + ('numpy._core.printoptions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\printoptions.py', + 'PYMODULE'), + ('numpy._core.records', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\records.py', + 'PYMODULE'), + ('numpy._core.shape_base', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\shape_base.py', + 'PYMODULE'), + ('numpy._core.strings', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\strings.py', + 'PYMODULE'), + ('numpy._core.umath', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_core\\umath.py', + 'PYMODULE'), + ('numpy._distributor_init', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_distributor_init.py', + 'PYMODULE'), + ('numpy._expired_attrs_2_0', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_expired_attrs_2_0.py', + 'PYMODULE'), + ('numpy._globals', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_globals.py', + 'PYMODULE'), + ('numpy._pytesttester', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_pytesttester.py', + 'PYMODULE'), + ('numpy._typing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\__init__.py', + 'PYMODULE'), + ('numpy._typing._add_docstring', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_add_docstring.py', + 'PYMODULE'), + ('numpy._typing._array_like', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_array_like.py', + 'PYMODULE'), + ('numpy._typing._char_codes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_char_codes.py', + 'PYMODULE'), + ('numpy._typing._dtype_like', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_dtype_like.py', + 'PYMODULE'), + ('numpy._typing._nbit', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nbit.py', + 'PYMODULE'), + ('numpy._typing._nbit_base', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nbit_base.py', + 'PYMODULE'), + ('numpy._typing._nested_sequence', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_nested_sequence.py', + 'PYMODULE'), + ('numpy._typing._scalars', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_scalars.py', + 'PYMODULE'), + ('numpy._typing._shape', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_shape.py', + 'PYMODULE'), + ('numpy._typing._ufunc', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_typing\\_ufunc.py', + 'PYMODULE'), + ('numpy._utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\__init__.py', + 'PYMODULE'), + ('numpy._utils._conversions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\_conversions.py', + 'PYMODULE'), + ('numpy._utils._inspect', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\_utils\\_inspect.py', + 'PYMODULE'), + ('numpy.char', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\char\\__init__.py', + 'PYMODULE'), + ('numpy.core', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\__init__.py', + 'PYMODULE'), + ('numpy.core._utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\_utils.py', + 'PYMODULE'), + ('numpy.core.multiarray', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\core\\multiarray.py', + 'PYMODULE'), + ('numpy.ctypeslib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ctypeslib\\__init__.py', + 'PYMODULE'), + ('numpy.ctypeslib._ctypeslib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ctypeslib\\_ctypeslib.py', + 'PYMODULE'), + ('numpy.dtypes', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\dtypes.py', + 'PYMODULE'), + ('numpy.exceptions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\exceptions.py', + 'PYMODULE'), + ('numpy.f2py', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\__init__.py', + 'PYMODULE'), + ('numpy.f2py.__version__', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\__version__.py', + 'PYMODULE'), + ('numpy.f2py._backends', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\__init__.py', + 'PYMODULE'), + ('numpy.f2py._backends._backend', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\_backend.py', + 'PYMODULE'), + ('numpy.f2py._backends._meson', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_backends\\_meson.py', + 'PYMODULE'), + ('numpy.f2py._isocbind', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\_isocbind.py', + 'PYMODULE'), + ('numpy.f2py.auxfuncs', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\auxfuncs.py', + 'PYMODULE'), + ('numpy.f2py.capi_maps', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\capi_maps.py', + 'PYMODULE'), + ('numpy.f2py.cb_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\cb_rules.py', + 'PYMODULE'), + ('numpy.f2py.cfuncs', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\cfuncs.py', + 'PYMODULE'), + ('numpy.f2py.common_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\common_rules.py', + 'PYMODULE'), + ('numpy.f2py.crackfortran', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\crackfortran.py', + 'PYMODULE'), + ('numpy.f2py.diagnose', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\diagnose.py', + 'PYMODULE'), + ('numpy.f2py.f2py2e', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\f2py2e.py', + 'PYMODULE'), + ('numpy.f2py.f90mod_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\f90mod_rules.py', + 'PYMODULE'), + ('numpy.f2py.func2subr', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\func2subr.py', + 'PYMODULE'), + ('numpy.f2py.rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\rules.py', + 'PYMODULE'), + ('numpy.f2py.symbolic', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\symbolic.py', + 'PYMODULE'), + ('numpy.f2py.use_rules', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\f2py\\use_rules.py', + 'PYMODULE'), + ('numpy.fft', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\__init__.py', + 'PYMODULE'), + ('numpy.fft._helper', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_helper.py', + 'PYMODULE'), + ('numpy.fft._pocketfft', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\fft\\_pocketfft.py', + 'PYMODULE'), + ('numpy.lib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\__init__.py', + 'PYMODULE'), + ('numpy.lib._array_utils_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_array_utils_impl.py', + 'PYMODULE'), + ('numpy.lib._arraypad_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arraypad_impl.py', + 'PYMODULE'), + ('numpy.lib._arraysetops_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arraysetops_impl.py', + 'PYMODULE'), + ('numpy.lib._arrayterator_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_arrayterator_impl.py', + 'PYMODULE'), + ('numpy.lib._datasource', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_datasource.py', + 'PYMODULE'), + ('numpy.lib._format_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_format_impl.py', + 'PYMODULE'), + ('numpy.lib._function_base_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_function_base_impl.py', + 'PYMODULE'), + ('numpy.lib._histograms_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_histograms_impl.py', + 'PYMODULE'), + ('numpy.lib._index_tricks_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_index_tricks_impl.py', + 'PYMODULE'), + ('numpy.lib._iotools', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_iotools.py', + 'PYMODULE'), + ('numpy.lib._nanfunctions_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_nanfunctions_impl.py', + 'PYMODULE'), + ('numpy.lib._npyio_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_npyio_impl.py', + 'PYMODULE'), + ('numpy.lib._polynomial_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_polynomial_impl.py', + 'PYMODULE'), + ('numpy.lib._scimath_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_scimath_impl.py', + 'PYMODULE'), + ('numpy.lib._shape_base_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_shape_base_impl.py', + 'PYMODULE'), + ('numpy.lib._stride_tricks_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py', + 'PYMODULE'), + ('numpy.lib._twodim_base_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_twodim_base_impl.py', + 'PYMODULE'), + ('numpy.lib._type_check_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_type_check_impl.py', + 'PYMODULE'), + ('numpy.lib._ufunclike_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_ufunclike_impl.py', + 'PYMODULE'), + ('numpy.lib._utils_impl', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_utils_impl.py', + 'PYMODULE'), + ('numpy.lib._version', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\_version.py', + 'PYMODULE'), + ('numpy.lib.array_utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\array_utils.py', + 'PYMODULE'), + ('numpy.lib.format', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\format.py', + 'PYMODULE'), + ('numpy.lib.introspect', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\introspect.py', + 'PYMODULE'), + ('numpy.lib.mixins', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\mixins.py', + 'PYMODULE'), + ('numpy.lib.npyio', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\npyio.py', + 'PYMODULE'), + ('numpy.lib.recfunctions', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\recfunctions.py', + 'PYMODULE'), + ('numpy.lib.scimath', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\scimath.py', + 'PYMODULE'), + ('numpy.lib.stride_tricks', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\lib\\stride_tricks.py', + 'PYMODULE'), + ('numpy.linalg', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\__init__.py', + 'PYMODULE'), + ('numpy.linalg._linalg', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\linalg\\_linalg.py', + 'PYMODULE'), + ('numpy.ma', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\__init__.py', + 'PYMODULE'), + ('numpy.ma.core', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\core.py', + 'PYMODULE'), + ('numpy.ma.extras', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\extras.py', + 'PYMODULE'), + ('numpy.ma.mrecords', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\ma\\mrecords.py', + 'PYMODULE'), + ('numpy.matlib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matlib.py', + 'PYMODULE'), + ('numpy.matrixlib', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matrixlib\\__init__.py', + 'PYMODULE'), + ('numpy.matrixlib.defmatrix', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\matrixlib\\defmatrix.py', + 'PYMODULE'), + ('numpy.polynomial', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\__init__.py', + 'PYMODULE'), + ('numpy.polynomial._polybase', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\_polybase.py', + 'PYMODULE'), + ('numpy.polynomial.chebyshev', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\chebyshev.py', + 'PYMODULE'), + ('numpy.polynomial.hermite', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\hermite.py', + 'PYMODULE'), + ('numpy.polynomial.hermite_e', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\hermite_e.py', + 'PYMODULE'), + ('numpy.polynomial.laguerre', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\laguerre.py', + 'PYMODULE'), + ('numpy.polynomial.legendre', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\legendre.py', + 'PYMODULE'), + ('numpy.polynomial.polynomial', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\polynomial.py', + 'PYMODULE'), + ('numpy.polynomial.polyutils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\polynomial\\polyutils.py', + 'PYMODULE'), + ('numpy.random', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\__init__.py', + 'PYMODULE'), + ('numpy.random._pickle', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\random\\_pickle.py', + 'PYMODULE'), + ('numpy.rec', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\rec\\__init__.py', + 'PYMODULE'), + ('numpy.strings', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\strings\\__init__.py', + 'PYMODULE'), + ('numpy.testing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\__init__.py', + 'PYMODULE'), + ('numpy.testing._private', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\__init__.py', + 'PYMODULE'), + ('numpy.testing._private.extbuild', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\extbuild.py', + 'PYMODULE'), + ('numpy.testing._private.utils', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\_private\\utils.py', + 'PYMODULE'), + ('numpy.testing.overrides', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\testing\\overrides.py', + 'PYMODULE'), + ('numpy.typing', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\typing\\__init__.py', + 'PYMODULE'), + ('numpy.version', + 'C:\\ZendionInc\\agrobot_base\\Python\\OAK\\datasets\\oak-fcc-3\\MaskReviewer_package\\.build_venv\\Lib\\site-packages\\numpy\\version.py', + 'PYMODULE'), + ('opcode', 'C:\\Python312\\Lib\\opcode.py', 'PYMODULE'), + ('pathlib', 'C:\\Python312\\Lib\\pathlib.py', 'PYMODULE'), + ('pdb', 'C:\\Python312\\Lib\\pdb.py', 'PYMODULE'), + ('pickle', 'C:\\Python312\\Lib\\pickle.py', 'PYMODULE'), + ('pkgutil', 'C:\\Python312\\Lib\\pkgutil.py', 'PYMODULE'), + ('platform', 'C:\\Python312\\Lib\\platform.py', 'PYMODULE'), + ('pprint', 'C:\\Python312\\Lib\\pprint.py', 'PYMODULE'), + ('py_compile', 'C:\\Python312\\Lib\\py_compile.py', 'PYMODULE'), + ('pydoc', 'C:\\Python312\\Lib\\pydoc.py', 'PYMODULE'), + ('pydoc_data', 'C:\\Python312\\Lib\\pydoc_data\\__init__.py', 'PYMODULE'), + ('pydoc_data.topics', + 'C:\\Python312\\Lib\\pydoc_data\\topics.py', + 'PYMODULE'), + ('queue', 'C:\\Python312\\Lib\\queue.py', 'PYMODULE'), + ('quopri', 'C:\\Python312\\Lib\\quopri.py', 'PYMODULE'), + ('random', 'C:\\Python312\\Lib\\random.py', 'PYMODULE'), + ('runpy', 'C:\\Python312\\Lib\\runpy.py', 'PYMODULE'), + ('secrets', 'C:\\Python312\\Lib\\secrets.py', 'PYMODULE'), + ('selectors', 'C:\\Python312\\Lib\\selectors.py', 'PYMODULE'), + ('shlex', 'C:\\Python312\\Lib\\shlex.py', 'PYMODULE'), + ('shutil', 'C:\\Python312\\Lib\\shutil.py', 'PYMODULE'), + ('signal', 'C:\\Python312\\Lib\\signal.py', 'PYMODULE'), + ('socket', 'C:\\Python312\\Lib\\socket.py', 'PYMODULE'), + ('socketserver', 'C:\\Python312\\Lib\\socketserver.py', 'PYMODULE'), + ('ssl', 'C:\\Python312\\Lib\\ssl.py', 'PYMODULE'), + ('statistics', 'C:\\Python312\\Lib\\statistics.py', 'PYMODULE'), + ('string', 'C:\\Python312\\Lib\\string.py', 'PYMODULE'), + ('stringprep', 'C:\\Python312\\Lib\\stringprep.py', 'PYMODULE'), + ('subprocess', 'C:\\Python312\\Lib\\subprocess.py', 'PYMODULE'), + ('sysconfig', 'C:\\Python312\\Lib\\sysconfig.py', 'PYMODULE'), + ('tarfile', 'C:\\Python312\\Lib\\tarfile.py', 'PYMODULE'), + ('tempfile', 'C:\\Python312\\Lib\\tempfile.py', 'PYMODULE'), + ('textwrap', 'C:\\Python312\\Lib\\textwrap.py', 'PYMODULE'), + ('threading', 'C:\\Python312\\Lib\\threading.py', 'PYMODULE'), + ('tkinter', 'C:\\Python312\\Lib\\tkinter\\__init__.py', 'PYMODULE'), + ('tkinter.commondialog', + 'C:\\Python312\\Lib\\tkinter\\commondialog.py', + 'PYMODULE'), + ('tkinter.constants', + 'C:\\Python312\\Lib\\tkinter\\constants.py', + 'PYMODULE'), + ('tkinter.dialog', 'C:\\Python312\\Lib\\tkinter\\dialog.py', 'PYMODULE'), + ('tkinter.filedialog', + 'C:\\Python312\\Lib\\tkinter\\filedialog.py', + 'PYMODULE'), + ('tkinter.messagebox', + 'C:\\Python312\\Lib\\tkinter\\messagebox.py', + 'PYMODULE'), + ('tkinter.simpledialog', + 'C:\\Python312\\Lib\\tkinter\\simpledialog.py', + 'PYMODULE'), + ('token', 'C:\\Python312\\Lib\\token.py', 'PYMODULE'), + ('tokenize', 'C:\\Python312\\Lib\\tokenize.py', 'PYMODULE'), + ('tracemalloc', 'C:\\Python312\\Lib\\tracemalloc.py', 'PYMODULE'), + ('tty', 'C:\\Python312\\Lib\\tty.py', 'PYMODULE'), + ('typing', 'C:\\Python312\\Lib\\typing.py', 'PYMODULE'), + ('unittest', 'C:\\Python312\\Lib\\unittest\\__init__.py', 'PYMODULE'), + ('unittest._log', 'C:\\Python312\\Lib\\unittest\\_log.py', 'PYMODULE'), + ('unittest.async_case', + 'C:\\Python312\\Lib\\unittest\\async_case.py', + 'PYMODULE'), + ('unittest.case', 'C:\\Python312\\Lib\\unittest\\case.py', 'PYMODULE'), + ('unittest.loader', 'C:\\Python312\\Lib\\unittest\\loader.py', 'PYMODULE'), + ('unittest.main', 'C:\\Python312\\Lib\\unittest\\main.py', 'PYMODULE'), + ('unittest.result', 'C:\\Python312\\Lib\\unittest\\result.py', 'PYMODULE'), + ('unittest.runner', 'C:\\Python312\\Lib\\unittest\\runner.py', 'PYMODULE'), + ('unittest.signals', 'C:\\Python312\\Lib\\unittest\\signals.py', 'PYMODULE'), + ('unittest.suite', 'C:\\Python312\\Lib\\unittest\\suite.py', 'PYMODULE'), + ('unittest.util', 'C:\\Python312\\Lib\\unittest\\util.py', 'PYMODULE'), + ('urllib', 'C:\\Python312\\Lib\\urllib\\__init__.py', 'PYMODULE'), + ('urllib.error', 'C:\\Python312\\Lib\\urllib\\error.py', 'PYMODULE'), + ('urllib.parse', 'C:\\Python312\\Lib\\urllib\\parse.py', 'PYMODULE'), + ('urllib.request', 'C:\\Python312\\Lib\\urllib\\request.py', 'PYMODULE'), + ('urllib.response', 'C:\\Python312\\Lib\\urllib\\response.py', 'PYMODULE'), + ('webbrowser', 'C:\\Python312\\Lib\\webbrowser.py', 'PYMODULE'), + ('xml', 'C:\\Python312\\Lib\\xml\\__init__.py', 'PYMODULE'), + ('xml.parsers', 'C:\\Python312\\Lib\\xml\\parsers\\__init__.py', 'PYMODULE'), + ('xml.parsers.expat', + 'C:\\Python312\\Lib\\xml\\parsers\\expat.py', + 'PYMODULE'), + ('xml.sax', 'C:\\Python312\\Lib\\xml\\sax\\__init__.py', 'PYMODULE'), + ('xml.sax._exceptions', + 'C:\\Python312\\Lib\\xml\\sax\\_exceptions.py', + 'PYMODULE'), + ('xml.sax.expatreader', + 'C:\\Python312\\Lib\\xml\\sax\\expatreader.py', + 'PYMODULE'), + ('xml.sax.handler', 'C:\\Python312\\Lib\\xml\\sax\\handler.py', 'PYMODULE'), + ('xml.sax.saxutils', 'C:\\Python312\\Lib\\xml\\sax\\saxutils.py', 'PYMODULE'), + ('xml.sax.xmlreader', + 'C:\\Python312\\Lib\\xml\\sax\\xmlreader.py', + 'PYMODULE'), + ('xmlrpc', 'C:\\Python312\\Lib\\xmlrpc\\__init__.py', 'PYMODULE'), + ('xmlrpc.client', 'C:\\Python312\\Lib\\xmlrpc\\client.py', 'PYMODULE'), + ('zipfile', 'C:\\Python312\\Lib\\zipfile\\__init__.py', 'PYMODULE'), + ('zipfile._path', + 'C:\\Python312\\Lib\\zipfile\\_path\\__init__.py', + 'PYMODULE'), + ('zipfile._path.glob', + 'C:\\Python312\\Lib\\zipfile\\_path\\glob.py', + 'PYMODULE'), + ('zipimport', 'C:\\Python312\\Lib\\zipimport.py', 'PYMODULE')]) diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/base_library.zip b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/base_library.zip new file mode 100644 index 000000000..791caf2e4 Binary files /dev/null and b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/base_library.zip differ diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/warn-MaskReviewer.txt b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/warn-MaskReviewer.txt new file mode 100644 index 000000000..8ed17f335 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/warn-MaskReviewer.txt @@ -0,0 +1,223 @@ + +This file lists modules PyInstaller was not able to find. This does not +necessarily mean these modules are required for running your program. Both +Python's standard library and 3rd-party Python packages often conditionally +import optional modules, some of which may be available only on certain +platforms. + +Types of import: +* top-level: imported at the top-level - look at these first +* conditional: imported within an if-statement +* delayed: imported within a function +* optional: imported within a try-except-statement + +IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for + tracking down the missing module yourself. Thanks! + +missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), http.server (delayed, optional), netrc (delayed, conditional), getpass (delayed) +missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional) +missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed) +missing module named fcntl - imported by subprocess (optional) +missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional) +missing module named _scproxy - imported by urllib.request (conditional) +missing module named termios - imported by tty (top-level), getpass (optional) +missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level) +missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level) +missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level) +missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level) +missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level) +missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level) +missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional) +missing module named resource - imported by posix (top-level) +excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level) +missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level) +missing module named pyimod02_importers - imported by C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed) +missing module named _dummy_thread - imported by numpy._core.arrayprint (optional) +missing module named typing_extensions - imported by numpy._typing._nested_sequence (conditional), numpy.random.bit_generator (top-level) +missing module named charset_normalizer - imported by numpy.f2py.crackfortran (optional) +missing module named vms_lib - imported by platform (delayed, optional) +missing module named 'java.lang' - imported by platform (delayed, optional) +missing module named java - imported by platform (delayed) +missing module named _winreg - imported by platform (delayed, optional) +missing module named psutil - imported by numpy.testing._private.utils (delayed, optional) +missing module named readline - imported by cmd (delayed, conditional, optional), code (delayed, conditional, optional), pdb (delayed, optional) +missing module named win32pdh - imported by numpy.testing._private.utils (delayed, conditional) +missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional) +missing module named _typeshed - imported by numpy.random._common (top-level), numpy.random.bit_generator (top-level) +missing module named threadpoolctl - imported by numpy.lib._utils_impl (delayed, optional) +missing module named numpy._core.zeros - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.void - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.vecmat - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.vecdot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.ushort - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.unsignedinteger - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.ulonglong - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.ulong - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.uintp - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.uintc - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.uint64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.uint32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.uint16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.uint - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.ubyte - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.trunc - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.true_divide - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._function_base_impl (top-level), numpy (conditional) +missing module named numpy._core.trace - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.timedelta64 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.tensordot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.tanh - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.tan - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.swapaxes - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.sum - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.subtract - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.str_ - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.square - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.sqrt - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level) +missing module named numpy._core.spacing - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.sort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.sinh - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.single - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.signedinteger - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.signbit - imported by numpy._core (conditional), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.sign - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.short - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.rint - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.right_shift - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.remainder - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.reciprocal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level) +missing module named numpy._core.radians - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.rad2deg - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.prod - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.power - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.positive - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.pi - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.outer - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.ones - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional) +missing module named numpy._core.object_ - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.number - imported by numpy._core (conditional), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.not_equal - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.nextafter - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.newaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.negative - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.ndarray - imported by numpy._core (top-level), numpy.lib._utils_impl (top-level), numpy (conditional), numpy.testing._private.utils (top-level) +missing module named numpy._core.multiply - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.moveaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.modf - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.mod - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.minimum - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.maximum - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.matvec - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.matrix_transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.matmul - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.longlong - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.longdouble - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.long - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.logical_xor - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.logical_or - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.logical_not - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.logical_and - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.logaddexp2 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.logaddexp - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.log10 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.log2 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.log1p - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.log - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.linspace - imported by numpy._core (top-level), numpy.lib._index_tricks_impl (top-level), numpy (conditional) +missing module named numpy._core.less_equal - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.less - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.left_shift - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.ldexp - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.lcm - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.isscalar - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.isnan - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.isfinite - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.intp - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy._array_api_info (top-level), numpy.testing._private.utils (top-level) +missing module named numpy._core.integer - imported by numpy._core (conditional), numpy (conditional), numpy.fft._helper (top-level) +missing module named numpy._core.intc - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.int64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.int32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.int16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.int8 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.inf - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.inexact - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.iinfo - imported by numpy._core (top-level), numpy.lib._twodim_base_impl (top-level), numpy (conditional) +missing module named numpy._core.hypot - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.hstack - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional) +missing module named numpy._core.heaviside - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.half - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.greater_equal - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.greater - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.gcd - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.frompyfunc - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.frexp - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.fmod - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.fmin - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.fmax - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.floor_divide - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.floor - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.floating - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.float_power - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.float32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level), numpy.testing._private.utils (top-level) +missing module named numpy._core.float16 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.finfo - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional) +missing module named numpy._core.fabs - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.expm1 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.exp2 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.exp - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.euler_gamma - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.errstate - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.equal - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.empty_like - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level) +missing module named numpy._core.empty - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._helper (top-level), numpy.testing._private.utils (top-level) +missing module named numpy._core.e - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.double - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.dot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional) +missing module named numpy._core.divmod - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.divide - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.diagonal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.degrees - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.deg2rad - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.datetime64 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.csingle - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.cross - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.count_nonzero - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.cosh - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.cos - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.copysign - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.conjugate - imported by numpy._core (conditional), numpy (conditional), numpy.fft._pocketfft (top-level) +missing module named numpy._core.conj - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.complexfloating - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.complex64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level) +missing module named numpy._core.clongdouble - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.character - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.ceil - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.cdouble - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.cbrt - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.bytes_ - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.byte - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.bool_ - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.bitwise_xor - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.bitwise_or - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.bitwise_count - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.bitwise_and - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.atleast_3d - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional) +missing module named numpy._core.atleast_2d - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.atleast_1d - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional) +missing module named numpy._core.asarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._array_utils_impl (top-level), numpy (conditional), numpy.fft._helper (top-level), numpy.fft._pocketfft (top-level) +missing module named numpy._core.asanyarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.array - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional), numpy.testing._private.utils (top-level) +missing module named numpy._core.argsort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.arctanh - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.arctan2 - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.arctan - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.arcsinh - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.arcsin - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.arccosh - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.arccos - imported by numpy._core (conditional), numpy (conditional) +missing module named numpy._core.amin - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.amax - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named numpy._core.all - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.testing._private.utils (delayed) +missing module named numpy._core.add - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional) +missing module named yaml - imported by numpy.__config__ (delayed) +missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional) diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/xref-MaskReviewer.html b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/xref-MaskReviewer.html new file mode 100644 index 000000000..b7507c528 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build/MaskReviewer/xref-MaskReviewer.html @@ -0,0 +1,18920 @@ + + + + + modulegraph cross reference for MaskReviewer.py, pyi_rth__tkinter.py, pyi_rth_inspect.py, pyi_rth_multiprocessing.py, pyi_rth_pkgutil.py + + + +

modulegraph cross reference for MaskReviewer.py, pyi_rth__tkinter.py, pyi_rth_inspect.py, pyi_rth_multiprocessing.py, pyi_rth_pkgutil.py

+ +
+ + MaskReviewer.py +Script
+imports: + __future__ + • _collections_abc + • _weakrefset + • abc + • argparse + • codecs + • collections + • collections.abc + • copyreg + • csv + • cv2 + • cv2.config + • cv2.config-3 + • cv2.cv2 + • cv2.data + • cv2.load_config_py2 + • cv2.load_config_py3 + • cv2.mat_wrapper + • cv2.misc + • cv2.misc.version + • cv2.typing + • cv2.utils + • cv2.version + • dataclasses + • datetime + • encodings + • encodings.aliases + • encodings.ascii + • encodings.base64_codec + • encodings.big5 + • encodings.big5hkscs + • encodings.bz2_codec + • encodings.charmap + • encodings.cp037 + • encodings.cp1006 + • encodings.cp1026 + • encodings.cp1125 + • encodings.cp1140 + • encodings.cp1250 + • encodings.cp1251 + • encodings.cp1252 + • encodings.cp1253 + • encodings.cp1254 + • encodings.cp1255 + • encodings.cp1256 + • encodings.cp1257 + • encodings.cp1258 + • encodings.cp273 + • encodings.cp424 + • encodings.cp437 + • encodings.cp500 + • encodings.cp720 + • encodings.cp737 + • encodings.cp775 + • encodings.cp850 + • encodings.cp852 + • encodings.cp855 + • encodings.cp856 + • encodings.cp857 + • encodings.cp858 + • encodings.cp860 + • encodings.cp861 + • encodings.cp862 + • encodings.cp863 + • encodings.cp864 + • encodings.cp865 + • encodings.cp866 + • encodings.cp869 + • encodings.cp874 + • encodings.cp875 + • encodings.cp932 + • encodings.cp949 + • encodings.cp950 + • encodings.euc_jis_2004 + • encodings.euc_jisx0213 + • encodings.euc_jp + • encodings.euc_kr + • encodings.gb18030 + • encodings.gb2312 + • encodings.gbk + • encodings.hex_codec + • encodings.hp_roman8 + • encodings.hz + • encodings.idna + • encodings.iso2022_jp + • encodings.iso2022_jp_1 + • encodings.iso2022_jp_2 + • encodings.iso2022_jp_2004 + • encodings.iso2022_jp_3 + • encodings.iso2022_jp_ext + • encodings.iso2022_kr + • encodings.iso8859_1 + • encodings.iso8859_10 + • encodings.iso8859_11 + • encodings.iso8859_13 + • encodings.iso8859_14 + • encodings.iso8859_15 + • encodings.iso8859_16 + • encodings.iso8859_2 + • encodings.iso8859_3 + • encodings.iso8859_4 + • encodings.iso8859_5 + • encodings.iso8859_6 + • encodings.iso8859_7 + • encodings.iso8859_8 + • encodings.iso8859_9 + • encodings.johab + • encodings.koi8_r + • encodings.koi8_t + • encodings.koi8_u + • encodings.kz1048 + • encodings.latin_1 + • encodings.mac_arabic + • encodings.mac_croatian + • encodings.mac_cyrillic + • encodings.mac_farsi + • encodings.mac_greek + • encodings.mac_iceland + • encodings.mac_latin2 + • encodings.mac_roman + • encodings.mac_romanian + • encodings.mac_turkish + • encodings.mbcs + • encodings.oem + • encodings.palmos + • encodings.ptcp154 + • encodings.punycode + • encodings.quopri_codec + • encodings.raw_unicode_escape + • encodings.rot_13 + • encodings.shift_jis + • encodings.shift_jis_2004 + • encodings.shift_jisx0213 + • encodings.tis_620 + • encodings.undefined + • encodings.unicode_escape + • encodings.utf_16 + • encodings.utf_16_be + • encodings.utf_16_le + • encodings.utf_32 + • encodings.utf_32_be + • encodings.utf_32_le + • encodings.utf_7 + • encodings.utf_8 + • encodings.utf_8_sig + • encodings.uu_codec + • encodings.zlib_codec + • enum + • functools + • genericpath + • heapq + • io + • json + • keyword + • linecache + • locale + • ntpath + • numpy + • operator + • os + • pathlib + • posixpath + • pyi_rth__tkinter.py + • pyi_rth_inspect.py + • pyi_rth_multiprocessing.py + • pyi_rth_pkgutil.py + • re + • re._casefix + • re._compiler + • re._constants + • re._parser + • reprlib + • shutil + • sre_compile + • sre_constants + • sre_parse + • stat + • sys + • time + • tkinter + • tkinter.filedialog + • tkinter.messagebox + • traceback + • types + • typing + • warnings + • weakref + +
+ +
+ +
+ + pyi_rth__tkinter.py +Script
+imports: + os + • sys + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + pyi_rth_inspect.py +Script
+imports: + inspect + • os + • sys + • zipfile + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + pyi_rth_multiprocessing.py +Script
+imports: + multiprocessing + • multiprocessing.spawn + • subprocess + • sys + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + pyi_rth_pkgutil.py +Script
+imports: + pkgutil + • pyimod02_importers + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + 'java.lang' +MissingModule
+imported by: + platform + +
+ +
+ +
+ + __future__ +SourceModule
+imported by: + MaskReviewer.py + • codeop + • doctest + • pydoc + +
+ +
+ +
+ + _abc (builtin module)
+imported by: + abc + +
+ +
+ +
+ + _aix_support +SourceModule
+imports: + contextlib + • os + • subprocess + • sys + • sysconfig + +
+
+imported by: + sysconfig + +
+ +
+ +
+ + _ast (builtin module)
+imported by: + ast + +
+ +
+ +
+ + _asyncio C:\Python312\DLLs\_asyncio.pyd
+imported by: + asyncio.events + • asyncio.futures + • asyncio.tasks + +
+ +
+ +
+ + _bisect (builtin module)
+imported by: + bisect + +
+ +
+ +
+ + _blake2 (builtin module)
+imported by: + hashlib + +
+ +
+ +
+ + _bz2 C:\Python312\DLLs\_bz2.pyd
+imported by: + bz2 + +
+ +
+ +
+ + _codecs (builtin module)
+imported by: + codecs + +
+ +
+ +
+ + _codecs_cn (builtin module)
+imported by: + encodings.gb18030 + • encodings.gb2312 + • encodings.gbk + • encodings.hz + +
+ +
+ +
+ + _codecs_hk (builtin module)
+imported by: + encodings.big5hkscs + +
+ +
+ +
+ + _codecs_iso2022 (builtin module) + +
+ +
+ + _codecs_jp (builtin module) + +
+ +
+ + _codecs_kr (builtin module)
+imported by: + encodings.cp949 + • encodings.euc_kr + • encodings.johab + +
+ +
+ +
+ + _codecs_tw (builtin module)
+imported by: + encodings.big5 + • encodings.cp950 + +
+ +
+ +
+ + _collections (builtin module)
+imported by: + collections + • threading + +
+ +
+ +
+ + _collections_abc +SourceModule
+imports: + abc + • sys + • warnings + +
+
+imported by: + MaskReviewer.py + • collections + • collections.abc + • contextlib + • locale + • os + • pathlib + • random + • types + • weakref + +
+ +
+ +
+ + _compat_pickle +SourceModule
+imported by: + _pickle + • pickle + +
+ +
+ +
+ + _compression +SourceModule
+imports: + io + • sys + +
+
+imported by: + bz2 + • gzip + • lzma + +
+ +
+ +
+ + _contextvars (builtin module)
+imported by: + contextvars + +
+ +
+ +
+ + _csv (builtin module)
+imported by: + csv + +
+ +
+ +
+ + _ctypes C:\Python312\DLLs\_ctypes.pyd
+imported by: + ctypes + • numpy._core._dtype_ctypes + +
+ +
+ +
+ + _datetime (builtin module)
+imports: + _strptime + • time + +
+
+imported by: + datetime + +
+ +
+ +
+ + _decimal C:\Python312\DLLs\_decimal.pyd
+imported by: + decimal + +
+ +
+ +
+ + _dummy_thread +MissingModule
+imported by: + numpy._core.arrayprint + +
+ +
+ +
+ + _frozen_importlib +ExcludedModule
+imported by: + importlib + • importlib.abc + • zipimport + +
+ +
+ +
+ + _frozen_importlib_external +MissingModule
+imported by: + importlib + • importlib._bootstrap + • importlib.abc + • zipimport + +
+ +
+ +
+ + _functools (builtin module)
+imported by: + functools + +
+ +
+ +
+ + _hashlib C:\Python312\DLLs\_hashlib.pyd
+imported by: + hashlib + • hmac + +
+ +
+ +
+ + _heapq (builtin module)
+imported by: + heapq + +
+ +
+ +
+ + _imp (builtin module)
+imported by: + importlib + • importlib._bootstrap_external + • importlib.util + • sysconfig + • zipimport + +
+ +
+ +
+ + _io (builtin module)
+imported by: + importlib._bootstrap_external + • io + • zipimport + +
+ +
+ +
+ + _json (builtin module)
+imports: + json.decoder + +
+
+imported by: + json.decoder + • json.encoder + • json.scanner + +
+ +
+ +
+ + _locale (builtin module)
+imported by: + locale + +
+ +
+ +
+ + _lzma C:\Python312\DLLs\_lzma.pyd
+imported by: + lzma + +
+ +
+ +
+ + _md5 (builtin module)
+imported by: + hashlib + +
+ +
+ +
+ + _multibytecodec (builtin module) + +
+ +
+ + _multiprocessing C:\Python312\DLLs\_multiprocessing.pyd + +
+ +
+ + _opcode (builtin module)
+imported by: + opcode + +
+ +
+ +
+ + _operator (builtin module)
+imported by: + hmac + • operator + +
+ +
+ +
+ + _overlapped C:\Python312\DLLs\_overlapped.pyd
+imported by: + asyncio.windows_events + +
+ +
+ +
+ + _pickle (builtin module)
+imports: + _compat_pickle + • codecs + • copyreg + +
+
+imported by: + pickle + +
+ +
+ +
+ + _posixshmem +MissingModule + +
+ +
+ + _posixsubprocess +MissingModule
+imports: + gc + +
+
+imported by: + multiprocessing.util + • subprocess + +
+ +
+ +
+ + _py_abc +SourceModule
+imports: + _weakrefset + +
+
+imported by: + abc + +
+ +
+ +
+ + _pydatetime +SourceModule
+imports: + _strptime + • math + • operator + • sys + • time + • warnings + +
+
+imported by: + datetime + +
+ +
+ +
+ + _pydecimal +SourceModule
+imports: + collections + • contextvars + • itertools + • locale + • math + • numbers + • re + • sys + +
+
+imported by: + decimal + +
+ +
+ +
+ + _queue C:\Python312\DLLs\_queue.pyd
+imported by: + queue + +
+ +
+ +
+ + _random (builtin module)
+imported by: + random + +
+ +
+ +
+ + _scproxy +MissingModule
+imported by: + urllib.request + +
+ +
+ +
+ + _sha1 (builtin module)
+imported by: + hashlib + +
+ +
+ +
+ + _sha2 (builtin module)
+imported by: + hashlib + • random + +
+ +
+ +
+ + _sha3 (builtin module)
+imported by: + hashlib + +
+ +
+ +
+ + _signal (builtin module)
+imported by: + signal + +
+ +
+ +
+ + _socket C:\Python312\DLLs\_socket.pyd
+imported by: + socket + +
+ +
+ +
+ + _sre (builtin module)
+imports: + copy + • re + +
+
+imported by: + re + • re._compiler + • re._constants + +
+ +
+ +
+ + _ssl C:\Python312\DLLs\_ssl.pyd
+imports: + socket + +
+
+imported by: + ssl + +
+ +
+ +
+ + _stat (builtin module)
+imported by: + stat + +
+ +
+ +
+ + _statistics (builtin module)
+imported by: + statistics + +
+ +
+ +
+ + _string (builtin module)
+imported by: + string + +
+ +
+ +
+ + _strptime +SourceModule
+imports: + _thread + • calendar + • datetime + • locale + • re + • time + +
+
+imported by: + _datetime + • _pydatetime + • time + +
+ +
+ +
+ + _struct (builtin module)
+imported by: + struct + +
+ +
+ +
+ + _thread (builtin module)
+imported by: + _strptime + • dataclasses + • functools + • numpy._core.arrayprint + • reprlib + • tempfile + • threading + +
+ +
+ +
+ + _threading_local +SourceModule
+imports: + contextlib + • threading + • weakref + +
+
+imported by: + threading + +
+ +
+ +
+ + _tkinter C:\Python312\DLLs\_tkinter.pyd
+imported by: + tkinter + +
+ +
+ +
+ + _tokenize (builtin module)
+imported by: + tokenize + +
+ +
+ +
+ + _tracemalloc (builtin module)
+imported by: + tracemalloc + +
+ +
+ +
+ + _typeshed +MissingModule + +
+ +
+ + _typing (builtin module)
+imported by: + typing + +
+ +
+ +
+ + _warnings (builtin module)
+imported by: + importlib._bootstrap_external + • warnings + • zipimport + +
+ +
+ +
+ + _weakref (builtin module)
+imported by: + _weakrefset + • collections + • weakref + • xml.sax.expatreader + +
+ +
+ +
+ + _weakrefset +SourceModule
+imports: + _weakref + • types + +
+
+imported by: + MaskReviewer.py + • _py_abc + • multiprocessing.process + • threading + • weakref + +
+ +
+ +
+ + _winapi (builtin module) + +
+ +
+ + _winreg +MissingModule
+imported by: + platform + +
+ +
+ +
+ + _wmi C:\Python312\DLLs\_wmi.pyd
+imported by: + platform + +
+ +
+ +
+ + abc +SourceModule
+imports: + _abc + • _py_abc + +
+ + +
+ +
+ + argparse +SourceModule
+imports: + copy + • gettext + • os + • re + • shutil + • sys + • textwrap + • warnings + +
+
+imported by: + MaskReviewer.py + • ast + • calendar + • code + • dis + • doctest + • gzip + • http.server + • inspect + • numpy.f2py.f2py2e + • py_compile + • tarfile + • tokenize + • unittest.main + • zipfile + +
+ +
+ +
+ + array (builtin module) + +
+ +
+ + ast +SourceModule
+imports: + _ast + • argparse + • collections + • contextlib + • enum + • inspect + • re + • sys + • warnings + +
+ + +
+ +
+ + asyncio +Package + + +
+ +
+ + asyncio.DefaultEventLoopPolicy +MissingModule
+imported by: + asyncio + • asyncio.events + +
+ +
+ +
+ + asyncio.base_events +SourceModule
+imports: + asyncio + • asyncio.constants + • asyncio.coroutines + • asyncio.events + • asyncio.exceptions + • asyncio.futures + • asyncio.log + • asyncio.protocols + • asyncio.sslproto + • asyncio.staggered + • asyncio.tasks + • asyncio.transports + • asyncio.trsock + • collections + • collections.abc + • concurrent.futures + • errno + • functools + • heapq + • itertools + • os + • socket + • ssl + • stat + • subprocess + • sys + • threading + • time + • traceback + • warnings + • weakref + +
+ + +
+ +
+ + asyncio.base_futures +SourceModule
+imports: + asyncio + • asyncio.format_helpers + • reprlib + +
+
+imported by: + asyncio + • asyncio.base_tasks + • asyncio.futures + +
+ +
+ +
+ + asyncio.base_subprocess +SourceModule
+imports: + asyncio + • asyncio.log + • asyncio.protocols + • asyncio.transports + • collections + • subprocess + • warnings + +
+
+imported by: + asyncio + • asyncio.unix_events + • asyncio.windows_events + +
+ +
+ +
+ + asyncio.base_tasks +SourceModule
+imports: + asyncio + • asyncio.base_futures + • asyncio.coroutines + • linecache + • reprlib + • traceback + +
+
+imported by: + asyncio + • asyncio.tasks + +
+ +
+ +
+ + asyncio.constants +SourceModule
+imports: + asyncio + • enum + +
+ + +
+ +
+ + asyncio.coroutines +SourceModule
+imports: + asyncio + • collections.abc + • inspect + • os + • sys + • types + +
+ + +
+ +
+ + asyncio.events +SourceModule
+imports: + _asyncio + • asyncio + • asyncio.DefaultEventLoopPolicy + • asyncio.format_helpers + • contextvars + • os + • signal + • socket + • subprocess + • sys + • threading + • warnings + +
+ + +
+ +
+ + asyncio.exceptions +SourceModule
+imports: + asyncio + +
+ + +
+ +
+ + asyncio.format_helpers +SourceModule
+imports: + asyncio + • asyncio.constants + • functools + • inspect + • reprlib + • sys + • traceback + +
+
+imported by: + asyncio + • asyncio.base_futures + • asyncio.events + • asyncio.futures + • asyncio.streams + +
+ +
+ +
+ + asyncio.futures +SourceModule + + +
+ +
+ + asyncio.locks +SourceModule
+imports: + asyncio + • asyncio.exceptions + • asyncio.mixins + • collections + • enum + +
+
+imported by: + asyncio + • asyncio.queues + • asyncio.staggered + +
+ +
+ +
+ + asyncio.log +SourceModule
+imports: + asyncio + • logging + +
+ + +
+ +
+ + asyncio.mixins +SourceModule
+imports: + asyncio + • asyncio.events + • threading + +
+
+imported by: + asyncio + • asyncio.locks + • asyncio.queues + +
+ +
+ +
+ + asyncio.proactor_events +SourceModule +
+imported by: + asyncio + • asyncio.windows_events + +
+ +
+ +
+ + asyncio.protocols +SourceModule
+imports: + asyncio + +
+ + +
+ +
+ + asyncio.queues +SourceModule
+imports: + asyncio + • asyncio.locks + • asyncio.mixins + • collections + • heapq + • types + +
+
+imported by: + asyncio + • asyncio.tasks + +
+ +
+ +
+ + asyncio.runners +SourceModule
+imports: + asyncio + • asyncio.constants + • asyncio.coroutines + • asyncio.events + • asyncio.exceptions + • asyncio.tasks + • contextvars + • enum + • functools + • signal + • threading + +
+
+imported by: + asyncio + +
+ +
+ +
+ + asyncio.selector_events +SourceModule
+imports: + asyncio + • asyncio.base_events + • asyncio.constants + • asyncio.events + • asyncio.futures + • asyncio.log + • asyncio.protocols + • asyncio.sslproto + • asyncio.transports + • asyncio.trsock + • collections + • errno + • functools + • itertools + • os + • selectors + • socket + • ssl + • warnings + • weakref + +
+
+imported by: + asyncio + • asyncio.unix_events + • asyncio.windows_events + +
+ +
+ +
+ + asyncio.sslproto +SourceModule
+imports: + asyncio + • asyncio.constants + • asyncio.exceptions + • asyncio.log + • asyncio.protocols + • asyncio.transports + • collections + • enum + • ssl + • warnings + +
+ + +
+ +
+ + asyncio.staggered +SourceModule
+imports: + asyncio + • asyncio.events + • asyncio.exceptions + • asyncio.locks + • asyncio.tasks + • contextlib + • typing + +
+
+imported by: + asyncio + • asyncio.base_events + +
+ +
+ +
+ + asyncio.streams +SourceModule +
+imported by: + asyncio + • asyncio.subprocess + +
+ +
+ +
+ + asyncio.subprocess +SourceModule
+imports: + asyncio + • asyncio.events + • asyncio.log + • asyncio.protocols + • asyncio.streams + • asyncio.tasks + • subprocess + +
+
+imported by: + asyncio + +
+ +
+ +
+ + asyncio.taskgroups +SourceModule
+imports: + asyncio + • asyncio.events + • asyncio.exceptions + • asyncio.tasks + +
+
+imported by: + asyncio + +
+ +
+ +
+ + asyncio.tasks +SourceModule + + +
+ +
+ + asyncio.threads +SourceModule
+imports: + asyncio + • asyncio.events + • contextvars + • functools + +
+
+imported by: + asyncio + +
+ +
+ +
+ + asyncio.timeouts +SourceModule
+imports: + asyncio + • asyncio.events + • asyncio.exceptions + • asyncio.tasks + • enum + • types + • typing + +
+
+imported by: + asyncio + • asyncio.tasks + +
+ +
+ +
+ + asyncio.transports +SourceModule
+imports: + asyncio + +
+ + +
+ +
+ + asyncio.trsock +SourceModule
+imports: + asyncio + • socket + +
+ + +
+ +
+ + asyncio.unix_events +SourceModule +
+imported by: + asyncio + +
+ +
+ +
+ + asyncio.windows_events +SourceModule +
+imported by: + asyncio + +
+ +
+ +
+ + asyncio.windows_utils +SourceModule
+imports: + _winapi + • asyncio + • itertools + • msvcrt + • os + • subprocess + • sys + • tempfile + • warnings + +
+
+imported by: + asyncio + • asyncio.windows_events + +
+ +
+ +
+ + atexit (builtin module)
+imported by: + logging + • multiprocessing.util + • weakref + +
+ +
+ +
+ + base64 +SourceModule
+imports: + binascii + • getopt + • re + • struct + • sys + +
+ + +
+ +
+ + bdb +SourceModule
+imports: + fnmatch + • inspect + • linecache + • os + • reprlib + • sys + +
+
+imported by: + pdb + +
+ +
+ +
+ + binascii (builtin module) + +
+ +
+ + bisect +SourceModule
+imports: + _bisect + +
+
+imported by: + multiprocessing.heap + • random + • statistics + • urllib.request + +
+ +
+ +
+ + builtins (builtin module)
+imported by: + bz2 + • codecs + • doctest + • enum + • gettext + • gzip + • inspect + • locale + • lzma + • numpy._core.numeric + • numpy._core.numerictypes + • numpy.lib._function_base_impl + • numpy.ma.core + • numpy.ma.mrecords + • numpy.random._generator + • numpy.random.mtrand + • operator + • pydoc + • reprlib + • subprocess + • tarfile + • tokenize + • warnings + +
+ +
+ +
+ + bz2 +SourceModule
+imports: + _bz2 + • _compression + • builtins + • io + • os + +
+
+imported by: + encodings.bz2_codec + • fileinput + • numpy.lib._datasource + • shutil + • tarfile + • zipfile + +
+ +
+ +
+ + calendar +SourceModule
+imports: + argparse + • datetime + • enum + • itertools + • locale + • sys + • warnings + +
+
+imported by: + _strptime + • email._parseaddr + • http.cookiejar + • ssl + +
+ +
+ +
+ + charset_normalizer +MissingModule
+imported by: + numpy.f2py.crackfortran + +
+ +
+ +
+ + cmd +SourceModule
+imports: + readline + • string + • sys + +
+
+imported by: + pdb + +
+ +
+ +
+ + code +SourceModule
+imports: + argparse + • codeop + • readline + • sys + • traceback + +
+
+imported by: + pdb + +
+ +
+ +
+ + codecs +SourceModule
+imports: + _codecs + • builtins + • encodings + • sys + +
+
+imported by: + MaskReviewer.py + • _pickle + • encodings + • encodings.ascii + • encodings.base64_codec + • encodings.big5 + • encodings.big5hkscs + • encodings.bz2_codec + • encodings.charmap + • encodings.cp037 + • encodings.cp1006 + • encodings.cp1026 + • encodings.cp1125 + • encodings.cp1140 + • encodings.cp1250 + • encodings.cp1251 + • encodings.cp1252 + • encodings.cp1253 + • encodings.cp1254 + • encodings.cp1255 + • encodings.cp1256 + • encodings.cp1257 + • encodings.cp1258 + • encodings.cp273 + • encodings.cp424 + • encodings.cp437 + • encodings.cp500 + • encodings.cp720 + • encodings.cp737 + • encodings.cp775 + • encodings.cp850 + • encodings.cp852 + • encodings.cp855 + • encodings.cp856 + • encodings.cp857 + • encodings.cp858 + • encodings.cp860 + • encodings.cp861 + • encodings.cp862 + • encodings.cp863 + • encodings.cp864 + • encodings.cp865 + • encodings.cp866 + • encodings.cp869 + • encodings.cp874 + • encodings.cp875 + • encodings.cp932 + • encodings.cp949 + • encodings.cp950 + • encodings.euc_jis_2004 + • encodings.euc_jisx0213 + • encodings.euc_jp + • encodings.euc_kr + • encodings.gb18030 + • encodings.gb2312 + • encodings.gbk + • encodings.hex_codec + • encodings.hp_roman8 + • encodings.hz + • encodings.idna + • encodings.iso2022_jp + • encodings.iso2022_jp_1 + • encodings.iso2022_jp_2 + • encodings.iso2022_jp_2004 + • encodings.iso2022_jp_3 + • encodings.iso2022_jp_ext + • encodings.iso2022_kr + • encodings.iso8859_1 + • encodings.iso8859_10 + • encodings.iso8859_11 + • encodings.iso8859_13 + • encodings.iso8859_14 + • encodings.iso8859_15 + • encodings.iso8859_16 + • encodings.iso8859_2 + • encodings.iso8859_3 + • encodings.iso8859_4 + • encodings.iso8859_5 + • encodings.iso8859_6 + • encodings.iso8859_7 + • encodings.iso8859_8 + • encodings.iso8859_9 + • encodings.johab + • encodings.koi8_r + • encodings.koi8_t + • encodings.koi8_u + • encodings.kz1048 + • encodings.latin_1 + • encodings.mac_arabic + • encodings.mac_croatian + • encodings.mac_cyrillic + • encodings.mac_farsi + • encodings.mac_greek + • encodings.mac_iceland + • encodings.mac_latin2 + • encodings.mac_roman + • encodings.mac_romanian + • encodings.mac_turkish + • encodings.mbcs + • encodings.oem + • encodings.palmos + • encodings.ptcp154 + • encodings.punycode + • encodings.quopri_codec + • encodings.raw_unicode_escape + • encodings.rot_13 + • encodings.shift_jis + • encodings.shift_jis_2004 + • encodings.shift_jisx0213 + • encodings.tis_620 + • encodings.undefined + • encodings.unicode_escape + • encodings.utf_16 + • encodings.utf_16_be + • encodings.utf_16_le + • encodings.utf_32 + • encodings.utf_32_be + • encodings.utf_32_le + • encodings.utf_7 + • encodings.utf_8 + • encodings.utf_8_sig + • encodings.uu_codec + • encodings.zlib_codec + • json + • numpy.f2py.crackfortran + • pickle + • tokenize + • xml.sax.saxutils + +
+ +
+ +
+ + codeop +SourceModule
+imports: + __future__ + • warnings + +
+
+imported by: + code + +
+ +
+ +
+ + collections +Package
+imports: + _collections + • _collections_abc + • _weakref + • copy + • heapq + • itertools + • keyword + • operator + • reprlib + • sys + +
+ + +
+ +
+ + collections.abc +SourceModule
+imports: + _collections_abc + • collections + +
+ + +
+ +
+ + concurrent +Package
+imported by: + concurrent.futures + +
+ +
+ +
+ + concurrent.futures +Package + + +
+ +
+ + concurrent.futures._base +SourceModule
+imports: + collections + • concurrent.futures + • logging + • threading + • time + • types + +
+ + +
+ +
+ + concurrent.futures.process +SourceModule +
+imported by: + concurrent.futures + +
+ +
+ +
+ + concurrent.futures.thread +SourceModule
+imports: + concurrent.futures + • concurrent.futures._base + • itertools + • os + • queue + • threading + • types + • weakref + +
+
+imported by: + concurrent.futures + +
+ +
+ +
+ + contextlib +SourceModule
+imports: + _collections_abc + • abc + • collections + • functools + • os + • sys + • types + +
+ + +
+ +
+ + contextvars +SourceModule
+imports: + _contextvars + +
+ + +
+ +
+ + copy +SourceModule
+imports: + copyreg + • types + • weakref + +
+ + +
+ +
+ + copyreg +SourceModule
+imports: + functools + • operator + +
+
+imported by: + MaskReviewer.py + • _pickle + • copy + • multiprocessing.reduction + • numpy._core + • pickle + • re + • typing + +
+ +
+ +
+ + csv +SourceModule
+imports: + _csv + • io + • re + • types + +
+
+imported by: + MaskReviewer.py + • importlib.metadata + +
+ +
+ +
+ + ctypes +Package
+imports: + _ctypes + • ctypes._endian + • nt + • os + • struct + • sys + • types + +
+ + +
+ +
+ + ctypes._endian +SourceModule
+imports: + ctypes + • sys + +
+
+imported by: + ctypes + +
+ +
+ +
+ + cv2 +Package
+imports: + copy + • cv2 + • cv2.config + • cv2.config-3 + • cv2.cv2 + • cv2.data + • cv2.load_config_py3 + • cv2.mat_wrapper + • cv2.misc + • cv2.misc.version + • cv2.typing + • cv2.utils + • cv2.version + • importlib + • numpy + • numpy.core.multiarray + • os + • platform + • sys + +
+
+imported by: + MaskReviewer.py + • cv2 + • cv2.config + • cv2.config-3 + • cv2.cv2 + • cv2.data + • cv2.dnn + • cv2.load_config_py2 + • cv2.load_config_py3 + • cv2.mat_wrapper + • cv2.misc + • cv2.misc.version + • cv2.typing + • cv2.utils + • cv2.version + +
+ +
+ +
+ + cv2.config +SourceModule
+imports: + cv2 + • os + +
+
+imported by: + MaskReviewer.py + • cv2 + +
+ +
+ +
+ + cv2.config-3 +SourceModule
+imports: + cv2 + • cv2.version + +
+
+imported by: + MaskReviewer.py + • cv2 + +
+ +
+ +
+ + cv2.cv2 C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\cv2\cv2.pyd
+imports: + cv2 + +
+
+imported by: + MaskReviewer.py + • cv2 + +
+ +
+ +
+ + cv2.data +Package
+imports: + cv2 + • os + +
+
+imported by: + MaskReviewer.py + • cv2 + +
+ +
+ +
+ + cv2.dnn +NamespacePackage
+imports: + cv2 + +
+
+imported by: + cv2.typing + +
+ +
+ +
+ + cv2.load_config_py2 +SourceModule
+imports: + cv2 + • sys + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + cv2.load_config_py3 +SourceModule
+imports: + cv2 + • os + • sys + +
+
+imported by: + MaskReviewer.py + • cv2 + +
+ +
+ +
+ + cv2.mat_wrapper +Package
+imports: + cv2 + • numpy + • typing + +
+
+imported by: + MaskReviewer.py + • cv2 + • cv2.typing + +
+ +
+ +
+ + cv2.misc +Package
+imports: + cv2 + • cv2.misc.version + +
+
+imported by: + MaskReviewer.py + • cv2 + • cv2.misc.version + +
+ +
+ +
+ + cv2.misc.version +SourceModule
+imports: + cv2 + • cv2.misc + +
+
+imported by: + MaskReviewer.py + • cv2 + • cv2.misc + +
+ +
+ +
+ + cv2.typing +Package
+imports: + cv2 + • cv2.dnn + • cv2.mat_wrapper + • numpy + • typing + +
+
+imported by: + MaskReviewer.py + • cv2 + +
+ +
+ +
+ + cv2.utils +Package
+imports: + collections + • cv2 + +
+
+imported by: + MaskReviewer.py + • cv2 + +
+ +
+ +
+ + cv2.version +SourceModule
+imports: + cv2 + +
+
+imported by: + MaskReviewer.py + • cv2 + • cv2.config-3 + +
+ +
+ +
+ + dataclasses +SourceModule
+imports: + _thread + • abc + • copy + • functools + • inspect + • itertools + • keyword + • re + • sys + • types + +
+
+imported by: + MaskReviewer.py + • pprint + +
+ +
+ +
+ + datetime +SourceModule
+imports: + _datetime + • _pydatetime + • time + +
+
+imported by: + MaskReviewer.py + • _strptime + • calendar + • email.utils + • http.cookiejar + • http.server + • numpy.ma.core + • xmlrpc.client + +
+ +
+ +
+ + decimal +SourceModule
+imports: + _decimal + • _pydecimal + +
+
+imported by: + fractions + • statistics + • xmlrpc.client + +
+ +
+ +
+ + difflib +SourceModule
+imports: + collections + • difflib + • heapq + • re + • types + +
+
+imported by: + difflib + • doctest + • numpy.testing._private.utils + • unittest.case + +
+ +
+ +
+ + dis +SourceModule
+imports: + argparse + • collections + • io + • opcode + • sys + • types + +
+
+imported by: + inspect + • pdb + +
+ +
+ +
+ + doctest +SourceModule
+imports: + __future__ + • argparse + • builtins + • collections + • difflib + • inspect + • io + • linecache + • os + • pdb + • re + • sys + • traceback + • unittest + +
+
+imported by: + numpy.testing._private.utils + +
+ +
+ +
+ + email +Package + + +
+ +
+ + email._encoded_words +SourceModule
+imports: + base64 + • binascii + • email + • email.errors + • functools + • re + • string + +
+
+imported by: + email._header_value_parser + • email.message + +
+ +
+ +
+ + email._header_value_parser +SourceModule
+imports: + email + • email._encoded_words + • email.errors + • email.utils + • operator + • re + • string + • sys + • urllib + +
+
+imported by: + email + • email.headerregistry + +
+ +
+ +
+ + email._parseaddr +SourceModule
+imports: + calendar + • email + • time + +
+
+imported by: + email.utils + +
+ +
+ +
+ + email._policybase +SourceModule
+imports: + abc + • email + • email.charset + • email.header + • email.utils + +
+
+imported by: + email.feedparser + • email.message + • email.parser + • email.policy + +
+ +
+ +
+ + email.base64mime +SourceModule
+imports: + base64 + • binascii + • email + +
+
+imported by: + email.charset + • email.header + +
+ +
+ +
+ + email.charset +SourceModule
+imports: + email + • email.base64mime + • email.encoders + • email.errors + • email.quoprimime + • functools + +
+
+imported by: + email + • email._policybase + • email.contentmanager + • email.header + • email.message + • email.utils + +
+ +
+ +
+ + email.contentmanager +SourceModule
+imports: + binascii + • email + • email.charset + • email.errors + • email.message + • email.quoprimime + +
+
+imported by: + email.policy + +
+ +
+ +
+ + email.encoders +SourceModule
+imports: + base64 + • email + • quopri + +
+
+imported by: + email.charset + +
+ +
+ +
+ + email.errors +SourceModule
+imports: + email + +
+ + +
+ +
+ + email.feedparser +SourceModule
+imports: + collections + • email + • email._policybase + • email.errors + • email.message + • io + • re + +
+
+imported by: + email.parser + +
+ +
+ +
+ + email.generator +SourceModule
+imports: + copy + • email + • email.utils + • io + • random + • re + • sys + • time + +
+
+imported by: + email.message + +
+ +
+ +
+ + email.header +SourceModule
+imports: + binascii + • email + • email.base64mime + • email.charset + • email.errors + • email.quoprimime + • re + +
+
+imported by: + email + • email._policybase + +
+ +
+ +
+ + email.headerregistry +SourceModule
+imports: + email + • email._header_value_parser + • email.errors + • email.utils + • types + +
+
+imported by: + email.policy + +
+ +
+ +
+ + email.iterators +SourceModule
+imports: + email + • io + • sys + +
+
+imported by: + email.message + +
+ +
+ +
+ + email.message +SourceModule
+imports: + binascii + • email + • email._encoded_words + • email._policybase + • email.charset + • email.errors + • email.generator + • email.iterators + • email.policy + • email.utils + • io + • quopri + • re + +
+ + +
+ +
+ + email.parser +SourceModule
+imports: + email + • email._policybase + • email.feedparser + • io + +
+
+imported by: + email + • http.client + +
+ +
+ +
+ + email.policy +SourceModule
+imports: + email + • email._policybase + • email.contentmanager + • email.headerregistry + • email.message + • email.utils + • re + • sys + +
+
+imported by: + email.message + +
+ +
+ +
+ + email.quoprimime +SourceModule
+imports: + email + • re + • string + +
+
+imported by: + email.charset + • email.contentmanager + • email.header + +
+ +
+ +
+ + email.utils +SourceModule
+imports: + datetime + • email + • email._parseaddr + • email.charset + • os + • random + • re + • socket + • time + • urllib.parse + • warnings + +
+ + +
+ +
+ + encodings +Package
+imports: + _winapi + • codecs + • encodings + • encodings.aliases + • encodings.ascii + • encodings.base64_codec + • encodings.big5 + • encodings.big5hkscs + • encodings.bz2_codec + • encodings.charmap + • encodings.cp037 + • encodings.cp1006 + • encodings.cp1026 + • encodings.cp1125 + • encodings.cp1140 + • encodings.cp1250 + • encodings.cp1251 + • encodings.cp1252 + • encodings.cp1253 + • encodings.cp1254 + • encodings.cp1255 + • encodings.cp1256 + • encodings.cp1257 + • encodings.cp1258 + • encodings.cp273 + • encodings.cp424 + • encodings.cp437 + • encodings.cp500 + • encodings.cp720 + • encodings.cp737 + • encodings.cp775 + • encodings.cp850 + • encodings.cp852 + • encodings.cp855 + • encodings.cp856 + • encodings.cp857 + • encodings.cp858 + • encodings.cp860 + • encodings.cp861 + • encodings.cp862 + • encodings.cp863 + • encodings.cp864 + • encodings.cp865 + • encodings.cp866 + • encodings.cp869 + • encodings.cp874 + • encodings.cp875 + • encodings.cp932 + • encodings.cp949 + • encodings.cp950 + • encodings.euc_jis_2004 + • encodings.euc_jisx0213 + • encodings.euc_jp + • encodings.euc_kr + • encodings.gb18030 + • encodings.gb2312 + • encodings.gbk + • encodings.hex_codec + • encodings.hp_roman8 + • encodings.hz + • encodings.idna + • encodings.iso2022_jp + • encodings.iso2022_jp_1 + • encodings.iso2022_jp_2 + • encodings.iso2022_jp_2004 + • encodings.iso2022_jp_3 + • encodings.iso2022_jp_ext + • encodings.iso2022_kr + • encodings.iso8859_1 + • encodings.iso8859_10 + • encodings.iso8859_11 + • encodings.iso8859_13 + • encodings.iso8859_14 + • encodings.iso8859_15 + • encodings.iso8859_16 + • encodings.iso8859_2 + • encodings.iso8859_3 + • encodings.iso8859_4 + • encodings.iso8859_5 + • encodings.iso8859_6 + • encodings.iso8859_7 + • encodings.iso8859_8 + • encodings.iso8859_9 + • encodings.johab + • encodings.koi8_r + • encodings.koi8_t + • encodings.koi8_u + • encodings.kz1048 + • encodings.latin_1 + • encodings.mac_arabic + • encodings.mac_croatian + • encodings.mac_cyrillic + • encodings.mac_farsi + • encodings.mac_greek + • encodings.mac_iceland + • encodings.mac_latin2 + • encodings.mac_roman + • encodings.mac_romanian + • encodings.mac_turkish + • encodings.mbcs + • encodings.oem + • encodings.palmos + • encodings.ptcp154 + • encodings.punycode + • encodings.quopri_codec + • encodings.raw_unicode_escape + • encodings.rot_13 + • encodings.shift_jis + • encodings.shift_jis_2004 + • encodings.shift_jisx0213 + • encodings.tis_620 + • encodings.undefined + • encodings.unicode_escape + • encodings.utf_16 + • encodings.utf_16_be + • encodings.utf_16_le + • encodings.utf_32 + • encodings.utf_32_be + • encodings.utf_32_le + • encodings.utf_7 + • encodings.utf_8 + • encodings.utf_8_sig + • encodings.uu_codec + • encodings.zlib_codec + • sys + +
+
+imported by: + MaskReviewer.py + • codecs + • encodings + • encodings.aliases + • encodings.ascii + • encodings.base64_codec + • encodings.big5 + • encodings.big5hkscs + • encodings.bz2_codec + • encodings.charmap + • encodings.cp037 + • encodings.cp1006 + • encodings.cp1026 + • encodings.cp1125 + • encodings.cp1140 + • encodings.cp1250 + • encodings.cp1251 + • encodings.cp1252 + • encodings.cp1253 + • encodings.cp1254 + • encodings.cp1255 + • encodings.cp1256 + • encodings.cp1257 + • encodings.cp1258 + • encodings.cp273 + • encodings.cp424 + • encodings.cp437 + • encodings.cp500 + • encodings.cp720 + • encodings.cp737 + • encodings.cp775 + • encodings.cp850 + • encodings.cp852 + • encodings.cp855 + • encodings.cp856 + • encodings.cp857 + • encodings.cp858 + • encodings.cp860 + • encodings.cp861 + • encodings.cp862 + • encodings.cp863 + • encodings.cp864 + • encodings.cp865 + • encodings.cp866 + • encodings.cp869 + • encodings.cp874 + • encodings.cp875 + • encodings.cp932 + • encodings.cp949 + • encodings.cp950 + • encodings.euc_jis_2004 + • encodings.euc_jisx0213 + • encodings.euc_jp + • encodings.euc_kr + • encodings.gb18030 + • encodings.gb2312 + • encodings.gbk + • encodings.hex_codec + • encodings.hp_roman8 + • encodings.hz + • encodings.idna + • encodings.iso2022_jp + • encodings.iso2022_jp_1 + • encodings.iso2022_jp_2 + • encodings.iso2022_jp_2004 + • encodings.iso2022_jp_3 + • encodings.iso2022_jp_ext + • encodings.iso2022_kr + • encodings.iso8859_1 + • encodings.iso8859_10 + • encodings.iso8859_11 + • encodings.iso8859_13 + • encodings.iso8859_14 + • encodings.iso8859_15 + • encodings.iso8859_16 + • encodings.iso8859_2 + • encodings.iso8859_3 + • encodings.iso8859_4 + • encodings.iso8859_5 + • encodings.iso8859_6 + • encodings.iso8859_7 + • encodings.iso8859_8 + • encodings.iso8859_9 + • encodings.johab + • encodings.koi8_r + • encodings.koi8_t + • encodings.koi8_u + • encodings.kz1048 + • encodings.latin_1 + • encodings.mac_arabic + • encodings.mac_croatian + • encodings.mac_cyrillic + • encodings.mac_farsi + • encodings.mac_greek + • encodings.mac_iceland + • encodings.mac_latin2 + • encodings.mac_roman + • encodings.mac_romanian + • encodings.mac_turkish + • encodings.mbcs + • encodings.oem + • encodings.palmos + • encodings.ptcp154 + • encodings.punycode + • encodings.quopri_codec + • encodings.raw_unicode_escape + • encodings.rot_13 + • encodings.shift_jis + • encodings.shift_jis_2004 + • encodings.shift_jisx0213 + • encodings.tis_620 + • encodings.undefined + • encodings.unicode_escape + • encodings.utf_16 + • encodings.utf_16_be + • encodings.utf_16_le + • encodings.utf_32 + • encodings.utf_32_be + • encodings.utf_32_le + • encodings.utf_7 + • encodings.utf_8 + • encodings.utf_8_sig + • encodings.uu_codec + • encodings.zlib_codec + • locale + +
+ +
+ +
+ + encodings.aliases +SourceModule
+imports: + encodings + +
+
+imported by: + MaskReviewer.py + • encodings + • locale + +
+ +
+ +
+ + encodings.ascii +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.base64_codec +SourceModule
+imports: + base64 + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.big5 +SourceModule
+imports: + _codecs_tw + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.big5hkscs +SourceModule
+imports: + _codecs_hk + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.bz2_codec +SourceModule
+imports: + bz2 + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.charmap +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp037 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1006 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1026 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1125 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1140 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1250 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1251 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1252 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1253 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1254 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1255 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1256 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1257 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp1258 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp273 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp424 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp437 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp500 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp720 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp737 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp775 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp850 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp852 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp855 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp856 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp857 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp858 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp860 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp861 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp862 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp863 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp864 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp865 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp866 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp869 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp874 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp875 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp932 +SourceModule
+imports: + _codecs_jp + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp949 +SourceModule
+imports: + _codecs_kr + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.cp950 +SourceModule
+imports: + _codecs_tw + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.euc_jis_2004 +SourceModule
+imports: + _codecs_jp + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.euc_jisx0213 +SourceModule
+imports: + _codecs_jp + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.euc_jp +SourceModule
+imports: + _codecs_jp + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.euc_kr +SourceModule
+imports: + _codecs_kr + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.gb18030 +SourceModule
+imports: + _codecs_cn + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.gb2312 +SourceModule
+imports: + _codecs_cn + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.gbk +SourceModule
+imports: + _codecs_cn + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.hex_codec +SourceModule
+imports: + binascii + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.hp_roman8 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.hz +SourceModule
+imports: + _codecs_cn + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.idna +SourceModule
+imports: + codecs + • encodings + • re + • stringprep + • unicodedata + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso2022_jp +SourceModule
+imports: + _codecs_iso2022 + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso2022_jp_1 +SourceModule
+imports: + _codecs_iso2022 + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso2022_jp_2 +SourceModule
+imports: + _codecs_iso2022 + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso2022_jp_2004 +SourceModule
+imports: + _codecs_iso2022 + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso2022_jp_3 +SourceModule
+imports: + _codecs_iso2022 + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso2022_jp_ext +SourceModule
+imports: + _codecs_iso2022 + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso2022_kr +SourceModule
+imports: + _codecs_iso2022 + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_1 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_10 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_11 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_13 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_14 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_15 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_16 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_2 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_3 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_4 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_5 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_6 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_7 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_8 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.iso8859_9 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.johab +SourceModule
+imports: + _codecs_kr + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.koi8_r +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.koi8_t +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.koi8_u +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.kz1048 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.latin_1 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_arabic +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_croatian +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_cyrillic +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_farsi +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_greek +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_iceland +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_latin2 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_roman +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_romanian +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mac_turkish +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.mbcs +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.oem +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.palmos +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.ptcp154 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.punycode +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.quopri_codec +SourceModule
+imports: + codecs + • encodings + • io + • quopri + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.raw_unicode_escape +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.rot_13 +SourceModule
+imports: + codecs + • encodings + • sys + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.shift_jis +SourceModule
+imports: + _codecs_jp + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.shift_jis_2004 +SourceModule
+imports: + _codecs_jp + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.shift_jisx0213 +SourceModule
+imports: + _codecs_jp + • _multibytecodec + • codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.tis_620 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.undefined +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.unicode_escape +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_16 +SourceModule
+imports: + codecs + • encodings + • sys + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_16_be +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_16_le +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_32 +SourceModule
+imports: + codecs + • encodings + • sys + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_32_be +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_32_le +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_7 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_8 +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.utf_8_sig +SourceModule
+imports: + codecs + • encodings + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.uu_codec +SourceModule
+imports: + binascii + • codecs + • encodings + • io + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + encodings.zlib_codec +SourceModule
+imports: + codecs + • encodings + • zlib + +
+
+imported by: + MaskReviewer.py + • encodings + +
+ +
+ +
+ + enum +SourceModule
+imports: + builtins + • functools + • operator + • sys + • types + • warnings + +
+
+imported by: + MaskReviewer.py + • ast + • asyncio.constants + • asyncio.locks + • asyncio.runners + • asyncio.sslproto + • asyncio.timeouts + • calendar + • http + • inspect + • numpy.__config__ + • numpy._globals + • numpy.f2py.symbolic + • py_compile + • re + • signal + • socket + • ssl + • tkinter + +
+ +
+ +
+ + errno (builtin module) + +
+ +
+ + fcntl +MissingModule
+imported by: + subprocess + +
+ +
+ +
+ + fileinput +SourceModule
+imports: + bz2 + • getopt + • gzip + • io + • os + • sys + • types + • warnings + +
+
+imported by: + numpy.f2py.crackfortran + +
+ +
+ +
+ + fnmatch +SourceModule
+imports: + functools + • os + • posixpath + • re + +
+
+imported by: + bdb + • glob + • pathlib + • shutil + • tkinter.filedialog + • tracemalloc + • unittest.loader + • urllib.request + +
+ +
+ +
+ + fractions +SourceModule
+imports: + decimal + • functools + • math + • numbers + • operator + • re + • sys + +
+
+imported by: + statistics + +
+ +
+ +
+ + ftplib +SourceModule
+imports: + netrc + • re + • socket + • ssl + • sys + +
+
+imported by: + urllib.request + +
+ +
+ +
+ + functools +SourceModule
+imports: + _functools + • _thread + • abc + • collections + • reprlib + • types + • typing + • weakref + +
+
+imported by: + MaskReviewer.py + • asyncio.base_events + • asyncio.format_helpers + • asyncio.runners + • asyncio.selector_events + • asyncio.tasks + • asyncio.threads + • concurrent.futures.process + • contextlib + • copyreg + • dataclasses + • email._encoded_words + • email.charset + • enum + • fnmatch + • fractions + • importlib.metadata + • importlib.metadata._adapters + • importlib.metadata._functools + • importlib.resources._common + • importlib.resources._legacy + • inspect + • ipaddress + • linecache + • locale + • multiprocessing.reduction + • multiprocessing.shared_memory + • numpy._core._ufunc_config + • numpy._core.arrayprint + • numpy._core.defchararray + • numpy._core.einsumfunc + • numpy._core.fromnumeric + • numpy._core.function_base + • numpy._core.getlimits + • numpy._core.multiarray + • numpy._core.numeric + • numpy._core.overrides + • numpy._core.shape_base + • numpy._core.strings + • numpy._utils + • numpy.f2py.auxfuncs + • numpy.fft._pocketfft + • numpy.lib._arraysetops_impl + • numpy.lib._arrayterator_impl + • numpy.lib._function_base_impl + • numpy.lib._histograms_impl + • numpy.lib._index_tricks_impl + • numpy.lib._nanfunctions_impl + • numpy.lib._npyio_impl + • numpy.lib._polynomial_impl + • numpy.lib._shape_base_impl + • numpy.lib._twodim_base_impl + • numpy.lib._type_check_impl + • numpy.linalg._linalg + • numpy.ma.core + • numpy.ma.extras + • numpy.polynomial.polyutils + • numpy.testing._private.utils + • operator + • pathlib + • pdb + • pickle + • pkgutil + • platform + • re + • statistics + • tempfile + • threading + • tokenize + • tracemalloc + • types + • typing + • unittest.case + • unittest.loader + • unittest.result + • unittest.signals + • urllib.parse + +
+ +
+ +
+ + gc (builtin module)
+imports: + time + +
+
+imported by: + _posixsubprocess + • numpy.testing._private.utils + • weakref + +
+ +
+ +
+ + genericpath +SourceModule
+imports: + os + • stat + +
+
+imported by: + MaskReviewer.py + • ntpath + • posixpath + +
+ +
+ +
+ + getopt +SourceModule
+imports: + gettext + • os + • sys + +
+
+imported by: + base64 + • fileinput + • mimetypes + • pdb + • pydoc + • quopri + • webbrowser + +
+ +
+ +
+ + getpass +SourceModule
+imports: + contextlib + • io + • msvcrt + • os + • pwd + • sys + • termios + • warnings + +
+
+imported by: + urllib.request + +
+ +
+ +
+ + gettext +SourceModule
+imports: + builtins + • copy + • errno + • locale + • operator + • os + • re + • struct + • sys + • warnings + +
+
+imported by: + argparse + • getopt + +
+ +
+ +
+ + glob +SourceModule
+imports: + contextlib + • fnmatch + • itertools + • os + • re + • stat + • sys + +
+
+imported by: + pdb + +
+ +
+ +
+ + grp +MissingModule
+imported by: + pathlib + • shutil + • subprocess + • tarfile + +
+ +
+ +
+ + gzip +SourceModule
+imports: + _compression + • argparse + • builtins + • errno + • io + • os + • struct + • sys + • time + • warnings + • zlib + +
+
+imported by: + fileinput + • numpy.lib._datasource + • tarfile + • xmlrpc.client + +
+ +
+ +
+ + hashlib +SourceModule
+imports: + _blake2 + • _hashlib + • _md5 + • _sha1 + • _sha2 + • _sha3 + • logging + +
+
+imported by: + hmac + • random + • urllib.request + +
+ +
+ +
+ + heapq +SourceModule
+imports: + _heapq + +
+
+imported by: + MaskReviewer.py + • asyncio.base_events + • asyncio.queues + • collections + • difflib + • queue + +
+ +
+ +
+ + hmac +SourceModule
+imports: + _hashlib + • _operator + • hashlib + • warnings + +
+
+imported by: + multiprocessing.connection + • secrets + +
+ +
+ +
+ + html +Package
+imports: + html.entities + • re + +
+
+imported by: + html.entities + • http.server + +
+ +
+ +
+ + html.entities +SourceModule
+imports: + html + +
+
+imported by: + html + +
+ +
+ +
+ + http +Package
+imports: + enum + +
+
+imported by: + http.client + • http.cookiejar + • http.server + +
+ +
+ +
+ + http.client +SourceModule
+imports: + collections.abc + • email.message + • email.parser + • errno + • http + • io + • re + • socket + • ssl + • sys + • urllib.parse + +
+
+imported by: + http.cookiejar + • http.server + • urllib.request + • xmlrpc.client + +
+ +
+ +
+ + http.cookiejar +SourceModule
+imports: + calendar + • copy + • datetime + • http + • http.client + • io + • logging + • os + • re + • threading + • time + • traceback + • urllib.parse + • urllib.request + • warnings + +
+
+imported by: + urllib.request + +
+ +
+ +
+ + http.server +SourceModule
+imports: + argparse + • base64 + • binascii + • contextlib + • copy + • datetime + • email.utils + • html + • http + • http.client + • io + • itertools + • mimetypes + • os + • posixpath + • pwd + • select + • shutil + • socket + • socketserver + • subprocess + • sys + • time + • urllib.parse + +
+
+imported by: + pydoc + +
+ +
+ +
+ + importlib +Package + + +
+ +
+ + importlib._abc +SourceModule
+imports: + abc + • importlib + • importlib._bootstrap + +
+
+imported by: + importlib.abc + • importlib.util + +
+ +
+ +
+ + importlib._bootstrap +SourceModule
+imports: + _frozen_importlib_external + • importlib + +
+
+imported by: + importlib + • importlib._abc + • importlib.machinery + • importlib.util + • pydoc + +
+ +
+ +
+ + importlib._bootstrap_external +SourceModule
+imports: + _imp + • _io + • _warnings + • importlib + • importlib.metadata + • importlib.readers + • marshal + • nt + • posix + • sys + • tokenize + • winreg + +
+
+imported by: + importlib + • importlib.abc + • importlib.machinery + • importlib.util + • py_compile + • pydoc + +
+ +
+ +
+ + importlib.abc +SourceModule +
+imported by: + importlib.metadata + +
+ +
+ +
+ + importlib.machinery +SourceModule +
+imported by: + importlib.abc + • inspect + • pkgutil + • py_compile + • pydoc + • runpy + +
+ +
+ +
+ + importlib.metadata +Package + + +
+ +
+ + importlib.metadata._adapters +SourceModule
+imports: + email.message + • functools + • importlib.metadata + • importlib.metadata._text + • re + • textwrap + • warnings + +
+
+imported by: + importlib.metadata + +
+ +
+ +
+ + importlib.metadata._collections +SourceModule
+imports: + collections + • importlib.metadata + +
+
+imported by: + importlib.metadata + +
+ +
+ +
+ + importlib.metadata._functools +SourceModule
+imports: + functools + • importlib.metadata + • types + +
+
+imported by: + importlib.metadata + • importlib.metadata._text + +
+ +
+ +
+ + importlib.metadata._itertools +SourceModule
+imports: + importlib.metadata + • itertools + +
+
+imported by: + importlib.metadata + +
+ +
+ +
+ + importlib.metadata._meta +SourceModule
+imports: + importlib.metadata + • typing + +
+
+imported by: + importlib.metadata + +
+ +
+ +
+ + importlib.metadata._text +SourceModule +
+imported by: + importlib.metadata._adapters + +
+ +
+ +
+ + importlib.readers +SourceModule
+imports: + importlib + • importlib.resources.readers + +
+
+imported by: + importlib._bootstrap_external + • zipimport + +
+ +
+ +
+ + importlib.resources +Package + + +
+ +
+ + importlib.resources._adapters +SourceModule
+imports: + contextlib + • importlib.resources + • importlib.resources.abc + • io + +
+
+imported by: + importlib.resources._common + +
+ +
+ +
+ + importlib.resources._common +SourceModule
+imports: + contextlib + • functools + • importlib + • importlib.resources + • importlib.resources._adapters + • importlib.resources.abc + • inspect + • itertools + • os + • pathlib + • tempfile + • types + • typing + • warnings + +
+ + +
+ +
+ + importlib.resources._itertools +SourceModule
+imports: + importlib.resources + +
+
+imported by: + importlib.resources.readers + +
+ +
+ +
+ + importlib.resources._legacy +SourceModule
+imports: + functools + • importlib.resources + • importlib.resources._common + • os + • pathlib + • types + • typing + • warnings + +
+
+imported by: + importlib.resources + +
+ +
+ +
+ + importlib.resources.abc +SourceModule
+imports: + abc + • importlib.resources + • io + • itertools + • os + • pathlib + • typing + +
+ + +
+ +
+ + importlib.resources.readers +SourceModule +
+imported by: + importlib.readers + +
+ +
+ +
+ + importlib.util +SourceModule
+imports: + _imp + • importlib + • importlib._abc + • importlib._bootstrap + • importlib._bootstrap_external + • sys + • types + +
+
+imported by: + numpy.testing._private.extbuild + • numpy.testing._private.utils + • pkgutil + • py_compile + • pydoc + • runpy + • zipfile + +
+ +
+ +
+ + inspect +SourceModule
+imports: + abc + • argparse + • ast + • builtins + • collections + • collections.abc + • dis + • enum + • functools + • importlib + • importlib.machinery + • itertools + • keyword + • linecache + • operator + • os + • re + • sys + • token + • tokenize + • types + +
+ + +
+ +
+ + io +SourceModule
+imports: + _io + • abc + +
+ + +
+ +
+ + ipaddress +SourceModule
+imports: + functools + • re + +
+
+imported by: + urllib.parse + +
+ +
+ +
+ + itertools (builtin module) + +
+ +
+ + java +MissingModule
+imported by: + platform + +
+ +
+ +
+ + json +Package
+imports: + codecs + • json.decoder + • json.encoder + • json.scanner + +
+ + +
+ +
+ + json.decoder +SourceModule
+imports: + _json + • json + • json.scanner + • re + +
+
+imported by: + _json + • json + +
+ +
+ +
+ + json.encoder +SourceModule
+imports: + _json + • json + • re + +
+
+imported by: + json + +
+ +
+ +
+ + json.scanner +SourceModule
+imports: + _json + • json + • re + +
+
+imported by: + json + • json.decoder + +
+ +
+ +
+ + keyword +SourceModule
+imported by: + MaskReviewer.py + • collections + • dataclasses + • inspect + +
+ +
+ +
+ + linecache +SourceModule
+imports: + functools + • os + • sys + • tokenize + +
+
+imported by: + MaskReviewer.py + • asyncio.base_tasks + • bdb + • doctest + • inspect + • pdb + • traceback + • tracemalloc + • warnings + +
+ +
+ +
+ + locale +SourceModule
+imports: + _collections_abc + • _locale + • builtins + • encodings + • encodings.aliases + • functools + • os + • re + • sys + • warnings + +
+
+imported by: + MaskReviewer.py + • _pydecimal + • _strptime + • calendar + • gettext + • subprocess + • tkinter.filedialog + +
+ +
+ +
+ + logging +Package
+imports: + atexit + • collections.abc + • io + • os + • pickle + • re + • string + • sys + • threading + • time + • traceback + • types + • warnings + • weakref + +
+ + +
+ +
+ + lzma +SourceModule
+imports: + _compression + • _lzma + • builtins + • io + • os + +
+
+imported by: + numpy.lib._datasource + • shutil + • tarfile + • zipfile + +
+ +
+ +
+ + marshal (builtin module)
+imported by: + importlib._bootstrap_external + • pkgutil + • zipimport + +
+ +
+ +
+ + math (builtin module) + +
+ +
+ + mimetypes +SourceModule
+imports: + _winapi + • getopt + • os + • posixpath + • sys + • urllib.parse + • winreg + +
+
+imported by: + http.server + • urllib.request + +
+ +
+ +
+ + mmap (builtin module) + +
+ +
+ + msvcrt (builtin module) + +
+ +
+ + multiprocessing +Package + + +
+ +
+ + multiprocessing.AuthenticationError +MissingModule
+imported by: + multiprocessing + • multiprocessing.connection + +
+ +
+ +
+ + multiprocessing.BufferTooShort +MissingModule
+imported by: + multiprocessing + • multiprocessing.connection + +
+ +
+ +
+ + multiprocessing.TimeoutError +MissingModule
+imported by: + multiprocessing + • multiprocessing.pool + +
+ +
+ +
+ + multiprocessing.connection +SourceModule + + +
+ +
+ + multiprocessing.context +SourceModule + + +
+ +
+ + multiprocessing.dummy +Package
+imports: + array + • multiprocessing + • multiprocessing.dummy.connection + • multiprocessing.pool + • queue + • sys + • threading + • weakref + +
+ + +
+ +
+ + multiprocessing.dummy.connection +SourceModule
+imports: + multiprocessing.dummy + • queue + +
+
+imported by: + multiprocessing.dummy + +
+ +
+ +
+ + multiprocessing.forkserver +SourceModule + + +
+ +
+ + multiprocessing.get_context +MissingModule + +
+ +
+ + multiprocessing.get_start_method +MissingModule
+imported by: + multiprocessing + • multiprocessing.spawn + +
+ +
+ +
+ + multiprocessing.heap +SourceModule
+imports: + _winapi + • bisect + • collections + • mmap + • multiprocessing + • multiprocessing.context + • multiprocessing.util + • os + • sys + • tempfile + • threading + +
+ + +
+ +
+ + multiprocessing.managers +SourceModule +
+imported by: + multiprocessing.context + +
+ +
+ +
+ + multiprocessing.pool +SourceModule + + +
+ +
+ + multiprocessing.popen_fork +SourceModule
+imports: + multiprocessing + • multiprocessing.connection + • multiprocessing.util + • os + • signal + +
+ + +
+ +
+ + multiprocessing.popen_forkserver +SourceModule +
+imported by: + multiprocessing.context + +
+ +
+ +
+ + multiprocessing.popen_spawn_posix +SourceModule +
+imported by: + multiprocessing.context + +
+ +
+ +
+ + multiprocessing.popen_spawn_win32 +SourceModule
+imports: + _winapi + • msvcrt + • multiprocessing + • multiprocessing.context + • multiprocessing.spawn + • multiprocessing.util + • os + • signal + • sys + +
+
+imported by: + multiprocessing.context + +
+ +
+ +
+ + multiprocessing.process +SourceModule + + +
+ +
+ + multiprocessing.queues +SourceModule + + +
+ +
+ + multiprocessing.reduction +SourceModule
+imports: + _winapi + • abc + • array + • copyreg + • functools + • io + • multiprocessing + • multiprocessing.context + • multiprocessing.resource_sharer + • os + • pickle + • socket + • sys + +
+
+imported by: + multiprocessing + • multiprocessing.context + +
+ +
+ +
+ + multiprocessing.resource_sharer +SourceModule + + +
+ +
+ + multiprocessing.resource_tracker +SourceModule
+imports: + _multiprocessing + • _posixshmem + • multiprocessing + • multiprocessing.spawn + • multiprocessing.util + • os + • signal + • sys + • threading + • warnings + +
+ + +
+ +
+ + multiprocessing.set_start_method +MissingModule
+imported by: + multiprocessing + • multiprocessing.spawn + +
+ +
+ +
+ + multiprocessing.shared_memory +SourceModule
+imports: + _posixshmem + • _winapi + • errno + • functools + • mmap + • multiprocessing + • multiprocessing.resource_tracker + • os + • secrets + • struct + • types + +
+
+imported by: + multiprocessing + • multiprocessing.managers + +
+ +
+ +
+ + multiprocessing.sharedctypes +SourceModule +
+imported by: + multiprocessing.context + +
+ +
+ +
+ + multiprocessing.spawn +SourceModule + + +
+ +
+ + multiprocessing.synchronize +SourceModule + + +
+ +
+ + multiprocessing.util +SourceModule + + +
+ +
+ + netrc +SourceModule
+imports: + os + • pwd + • stat + +
+
+imported by: + ftplib + +
+ +
+ +
+ + nt (builtin module)
+imported by: + ctypes + • importlib._bootstrap_external + • ntpath + • os + • shutil + +
+ +
+ +
+ + ntpath +SourceModule
+imports: + _winapi + • genericpath + • nt + • os + • stat + • string + • sys + +
+
+imported by: + MaskReviewer.py + • os + • os.path + • pathlib + +
+ +
+ +
+ + nturl2path +SourceModule
+imports: + string + • urllib.parse + +
+
+imported by: + urllib.request + +
+ +
+ +
+ + numbers +SourceModule
+imports: + abc + +
+ + +
+ +
+ + numpy +Package
+imports: + numpy + • numpy.__config__ + • numpy._array_api_info + • numpy._core + • numpy._core._dtype_ctypes + • numpy._core._exceptions + • numpy._core._multiarray_tests + • numpy._core.add + • numpy._core.all + • numpy._core.amax + • numpy._core.amin + • numpy._core.arccos + • numpy._core.arccosh + • numpy._core.arcsin + • numpy._core.arcsinh + • numpy._core.arctan + • numpy._core.arctan2 + • numpy._core.arctanh + • numpy._core.argsort + • numpy._core.array + • numpy._core.asanyarray + • numpy._core.asarray + • numpy._core.atleast_1d + • numpy._core.atleast_2d + • numpy._core.atleast_3d + • numpy._core.bitwise_and + • numpy._core.bitwise_count + • numpy._core.bitwise_or + • numpy._core.bitwise_xor + • numpy._core.bool_ + • numpy._core.byte + • numpy._core.bytes_ + • numpy._core.cbrt + • numpy._core.cdouble + • numpy._core.ceil + • numpy._core.character + • numpy._core.clongdouble + • numpy._core.complex64 + • numpy._core.complexfloating + • numpy._core.conj + • numpy._core.conjugate + • numpy._core.copysign + • numpy._core.cos + • numpy._core.cosh + • numpy._core.count_nonzero + • numpy._core.cross + • numpy._core.csingle + • numpy._core.datetime64 + • numpy._core.deg2rad + • numpy._core.degrees + • numpy._core.diagonal + • numpy._core.divide + • numpy._core.divmod + • numpy._core.dot + • numpy._core.double + • numpy._core.e + • numpy._core.empty + • numpy._core.empty_like + • numpy._core.equal + • numpy._core.errstate + • numpy._core.euler_gamma + • numpy._core.exp + • numpy._core.exp2 + • numpy._core.expm1 + • numpy._core.fabs + • numpy._core.finfo + • numpy._core.float16 + • numpy._core.float32 + • numpy._core.float_power + • numpy._core.floating + • numpy._core.floor + • numpy._core.floor_divide + • numpy._core.fmax + • numpy._core.fmin + • numpy._core.fmod + • numpy._core.frexp + • numpy._core.frompyfunc + • numpy._core.gcd + • numpy._core.greater + • numpy._core.greater_equal + • numpy._core.half + • numpy._core.heaviside + • numpy._core.hstack + • numpy._core.hypot + • numpy._core.iinfo + • numpy._core.inexact + • numpy._core.inf + • numpy._core.int16 + • numpy._core.int32 + • numpy._core.int64 + • numpy._core.int8 + • numpy._core.intc + • numpy._core.integer + • numpy._core.intp + • numpy._core.isfinite + • numpy._core.isnan + • numpy._core.isscalar + • numpy._core.lcm + • numpy._core.ldexp + • numpy._core.left_shift + • numpy._core.less + • numpy._core.less_equal + • numpy._core.linspace + • numpy._core.log + • numpy._core.log10 + • numpy._core.log1p + • numpy._core.log2 + • numpy._core.logaddexp + • numpy._core.logaddexp2 + • numpy._core.logical_and + • numpy._core.logical_not + • numpy._core.logical_or + • numpy._core.logical_xor + • numpy._core.long + • numpy._core.longdouble + • numpy._core.longlong + • numpy._core.matmul + • numpy._core.matrix_transpose + • numpy._core.matvec + • numpy._core.maximum + • numpy._core.memmap + • numpy._core.minimum + • numpy._core.mod + • numpy._core.modf + • numpy._core.moveaxis + • numpy._core.multiply + • numpy._core.ndarray + • numpy._core.negative + • numpy._core.newaxis + • numpy._core.nextafter + • numpy._core.not_equal + • numpy._core.number + • numpy._core.object_ + • numpy._core.ones + • numpy._core.outer + • numpy._core.pi + • numpy._core.positive + • numpy._core.power + • numpy._core.printoptions + • numpy._core.prod + • numpy._core.rad2deg + • numpy._core.radians + • numpy._core.reciprocal + • numpy._core.remainder + • numpy._core.right_shift + • numpy._core.rint + • numpy._core.short + • numpy._core.sign + • numpy._core.signbit + • numpy._core.signedinteger + • numpy._core.single + • numpy._core.sinh + • numpy._core.sort + • numpy._core.spacing + • numpy._core.sqrt + • numpy._core.square + • numpy._core.str_ + • numpy._core.subtract + • numpy._core.sum + • numpy._core.swapaxes + • numpy._core.tan + • numpy._core.tanh + • numpy._core.tensordot + • numpy._core.timedelta64 + • numpy._core.trace + • numpy._core.transpose + • numpy._core.true_divide + • numpy._core.trunc + • numpy._core.ubyte + • numpy._core.uint + • numpy._core.uint16 + • numpy._core.uint32 + • numpy._core.uint64 + • numpy._core.uintc + • numpy._core.uintp + • numpy._core.ulong + • numpy._core.ulonglong + • numpy._core.unsignedinteger + • numpy._core.ushort + • numpy._core.vecdot + • numpy._core.vecmat + • numpy._core.void + • numpy._core.zeros + • numpy._distributor_init + • numpy._distributor_init_local + • numpy._expired_attrs_2_0 + • numpy._globals + • numpy._pytesttester + • numpy.char + • numpy.core + • numpy.ctypeslib + • numpy.dtypes + • numpy.exceptions + • numpy.f2py + • numpy.fft + • numpy.lib + • numpy.lib._arraypad_impl + • numpy.lib._arraysetops_impl + • numpy.lib._function_base_impl + • numpy.lib._histograms_impl + • numpy.lib._index_tricks_impl + • numpy.lib._nanfunctions_impl + • numpy.lib._npyio_impl + • numpy.lib._polynomial_impl + • numpy.lib._shape_base_impl + • numpy.lib._stride_tricks_impl + • numpy.lib._twodim_base_impl + • numpy.lib._type_check_impl + • numpy.lib._ufunclike_impl + • numpy.lib._utils_impl + • numpy.lib.scimath + • numpy.linalg + • numpy.ma + • numpy.matlib + • numpy.matrixlib + • numpy.polynomial + • numpy.random + • numpy.rec + • numpy.strings + • numpy.testing + • numpy.typing + • numpy.version + • os + • pathlib + • sys + • warnings + +
+
+imported by: + MaskReviewer.py + • cv2 + • cv2.mat_wrapper + • cv2.typing + • numpy + • numpy.__config__ + • numpy._array_api_info + • numpy._core + • numpy._core._dtype + • numpy._core._dtype_ctypes + • numpy._core._internal + • numpy._core._methods + • numpy._core.arrayprint + • numpy._core.defchararray + • numpy._core.fromnumeric + • numpy._core.function_base + • numpy._core.memmap + • numpy._core.numeric + • numpy._core.strings + • numpy._core.umath + • numpy._distributor_init + • numpy._expired_attrs_2_0 + • numpy._globals + • numpy._pytesttester + • numpy._typing + • numpy._typing._array_like + • numpy._typing._dtype_like + • numpy._typing._scalars + • numpy._typing._ufunc + • numpy._utils + • numpy.char + • numpy.core + • numpy.ctypeslib + • numpy.ctypeslib._ctypeslib + • numpy.dtypes + • numpy.exceptions + • numpy.f2py + • numpy.f2py.diagnose + • numpy.f2py.f90mod_rules + • numpy.fft + • numpy.lib + • numpy.lib._arraypad_impl + • numpy.lib._arraysetops_impl + • numpy.lib._format_impl + • numpy.lib._function_base_impl + • numpy.lib._histograms_impl + • numpy.lib._index_tricks_impl + • numpy.lib._iotools + • numpy.lib._nanfunctions_impl + • numpy.lib._npyio_impl + • numpy.lib._shape_base_impl + • numpy.lib._stride_tricks_impl + • numpy.lib._twodim_base_impl + • numpy.lib._utils_impl + • numpy.lib.recfunctions + • numpy.linalg + • numpy.linalg._linalg + • numpy.linalg._umath_linalg + • numpy.ma + • numpy.ma.core + • numpy.ma.extras + • numpy.ma.mrecords + • numpy.matlib + • numpy.matrixlib + • numpy.polynomial + • numpy.polynomial._polybase + • numpy.polynomial.chebyshev + • numpy.polynomial.hermite + • numpy.polynomial.hermite_e + • numpy.polynomial.laguerre + • numpy.polynomial.legendre + • numpy.polynomial.polynomial + • numpy.polynomial.polyutils + • numpy.random + • numpy.random._common + • numpy.random._generator + • numpy.random._mt19937 + • numpy.random._philox + • numpy.random._sfc64 + • numpy.random.bit_generator + • numpy.random.mtrand + • numpy.rec + • numpy.strings + • numpy.testing + • numpy.testing._private.utils + • numpy.testing.overrides + • numpy.typing + • numpy.version + +
+ +
+ +
+ + numpy.__config__ +SourceModule
+imports: + enum + • json + • numpy + • numpy._core._multiarray_umath + • warnings + • yaml + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy._array_api_info +SourceModule +
+imported by: + numpy + +
+ +
+ +
+ + numpy._core +Package
+imports: + copyreg + • numpy + • numpy._core + • numpy._core._add_newdocs + • numpy._core._add_newdocs_scalars + • numpy._core._asarray + • numpy._core._dtype + • numpy._core._dtype_ctypes + • numpy._core._internal + • numpy._core._methods + • numpy._core._multiarray_umath + • numpy._core.add + • numpy._core.all + • numpy._core.amax + • numpy._core.amin + • numpy._core.arccos + • numpy._core.arccosh + • numpy._core.arcsin + • numpy._core.arcsinh + • numpy._core.arctan + • numpy._core.arctan2 + • numpy._core.arctanh + • numpy._core.argsort + • numpy._core.array + • numpy._core.arrayprint + • numpy._core.asanyarray + • numpy._core.asarray + • numpy._core.atleast_1d + • numpy._core.atleast_2d + • numpy._core.atleast_3d + • numpy._core.bitwise_and + • numpy._core.bitwise_count + • numpy._core.bitwise_or + • numpy._core.bitwise_xor + • numpy._core.bool_ + • numpy._core.byte + • numpy._core.bytes_ + • numpy._core.cbrt + • numpy._core.cdouble + • numpy._core.ceil + • numpy._core.character + • numpy._core.clongdouble + • numpy._core.complex64 + • numpy._core.complexfloating + • numpy._core.conj + • numpy._core.conjugate + • numpy._core.copysign + • numpy._core.cos + • numpy._core.cosh + • numpy._core.count_nonzero + • numpy._core.cross + • numpy._core.csingle + • numpy._core.datetime64 + • numpy._core.deg2rad + • numpy._core.degrees + • numpy._core.diagonal + • numpy._core.divide + • numpy._core.divmod + • numpy._core.dot + • numpy._core.double + • numpy._core.e + • numpy._core.einsumfunc + • numpy._core.empty + • numpy._core.empty_like + • numpy._core.equal + • numpy._core.errstate + • numpy._core.euler_gamma + • numpy._core.exp + • numpy._core.exp2 + • numpy._core.expm1 + • numpy._core.fabs + • numpy._core.finfo + • numpy._core.float16 + • numpy._core.float32 + • numpy._core.float_power + • numpy._core.floating + • numpy._core.floor + • numpy._core.floor_divide + • numpy._core.fmax + • numpy._core.fmin + • numpy._core.fmod + • numpy._core.frexp + • numpy._core.fromnumeric + • numpy._core.frompyfunc + • numpy._core.function_base + • numpy._core.gcd + • numpy._core.getlimits + • numpy._core.greater + • numpy._core.greater_equal + • numpy._core.half + • numpy._core.heaviside + • numpy._core.hstack + • numpy._core.hypot + • numpy._core.iinfo + • numpy._core.inexact + • numpy._core.inf + • numpy._core.int16 + • numpy._core.int32 + • numpy._core.int64 + • numpy._core.int8 + • numpy._core.intc + • numpy._core.integer + • numpy._core.intp + • numpy._core.isfinite + • numpy._core.isnan + • numpy._core.isscalar + • numpy._core.lcm + • numpy._core.ldexp + • numpy._core.left_shift + • numpy._core.less + • numpy._core.less_equal + • numpy._core.linspace + • numpy._core.log + • numpy._core.log10 + • numpy._core.log1p + • numpy._core.log2 + • numpy._core.logaddexp + • numpy._core.logaddexp2 + • numpy._core.logical_and + • numpy._core.logical_not + • numpy._core.logical_or + • numpy._core.logical_xor + • numpy._core.long + • numpy._core.longdouble + • numpy._core.longlong + • numpy._core.matmul + • numpy._core.matrix_transpose + • numpy._core.matvec + • numpy._core.maximum + • numpy._core.memmap + • numpy._core.minimum + • numpy._core.mod + • numpy._core.modf + • numpy._core.moveaxis + • numpy._core.multiarray + • numpy._core.multiply + • numpy._core.ndarray + • numpy._core.negative + • numpy._core.newaxis + • numpy._core.nextafter + • numpy._core.not_equal + • numpy._core.number + • numpy._core.numeric + • numpy._core.numerictypes + • numpy._core.object_ + • numpy._core.ones + • numpy._core.outer + • numpy._core.overrides + • numpy._core.pi + • numpy._core.positive + • numpy._core.power + • numpy._core.prod + • numpy._core.rad2deg + • numpy._core.radians + • numpy._core.reciprocal + • numpy._core.records + • numpy._core.remainder + • numpy._core.right_shift + • numpy._core.rint + • numpy._core.shape_base + • numpy._core.short + • numpy._core.sign + • numpy._core.signbit + • numpy._core.signedinteger + • numpy._core.single + • numpy._core.sinh + • numpy._core.sort + • numpy._core.spacing + • numpy._core.sqrt + • numpy._core.square + • numpy._core.str_ + • numpy._core.subtract + • numpy._core.sum + • numpy._core.swapaxes + • numpy._core.tan + • numpy._core.tanh + • numpy._core.tensordot + • numpy._core.timedelta64 + • numpy._core.trace + • numpy._core.transpose + • numpy._core.true_divide + • numpy._core.trunc + • numpy._core.ubyte + • numpy._core.uint + • numpy._core.uint16 + • numpy._core.uint32 + • numpy._core.uint64 + • numpy._core.uintc + • numpy._core.uintp + • numpy._core.ulong + • numpy._core.ulonglong + • numpy._core.umath + • numpy._core.unsignedinteger + • numpy._core.ushort + • numpy._core.vecdot + • numpy._core.vecmat + • numpy._core.void + • numpy._core.zeros + • numpy._pytesttester + • numpy.version + • os + • sys + +
+
+imported by: + numpy + • numpy._array_api_info + • numpy._core + • numpy._core._add_newdocs + • numpy._core._add_newdocs_scalars + • numpy._core._asarray + • numpy._core._dtype + • numpy._core._dtype_ctypes + • numpy._core._exceptions + • numpy._core._internal + • numpy._core._methods + • numpy._core._multiarray_tests + • numpy._core._multiarray_umath + • numpy._core._string_helpers + • numpy._core._type_aliases + • numpy._core._ufunc_config + • numpy._core.arrayprint + • numpy._core.defchararray + • numpy._core.einsumfunc + • numpy._core.fromnumeric + • numpy._core.function_base + • numpy._core.getlimits + • numpy._core.memmap + • numpy._core.multiarray + • numpy._core.numeric + • numpy._core.numerictypes + • numpy._core.overrides + • numpy._core.printoptions + • numpy._core.records + • numpy._core.shape_base + • numpy._core.strings + • numpy._core.umath + • numpy.core + • numpy.core.multiarray + • numpy.fft._helper + • numpy.fft._pocketfft + • numpy.lib._array_utils_impl + • numpy.lib._arraysetops_impl + • numpy.lib._function_base_impl + • numpy.lib._histograms_impl + • numpy.lib._index_tricks_impl + • numpy.lib._nanfunctions_impl + • numpy.lib._npyio_impl + • numpy.lib._polynomial_impl + • numpy.lib._shape_base_impl + • numpy.lib._twodim_base_impl + • numpy.lib._type_check_impl + • numpy.lib._utils_impl + • numpy.lib.mixins + • numpy.linalg._linalg + • numpy.ma.core + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core._add_newdocs +SourceModule +
+imported by: + numpy._core + +
+ +
+ +
+ + numpy._core._add_newdocs_scalars +SourceModule
+imports: + numpy._core + • numpy._core.function_base + • numpy._core.numerictypes + • os + • sys + +
+
+imported by: + numpy._core + +
+ +
+ +
+ + numpy._core._asarray +SourceModule +
+imported by: + numpy._core + • numpy._core.numeric + +
+ +
+ +
+ + numpy._core._dtype +SourceModule
+imports: + numpy + • numpy._core + +
+
+imported by: + numpy._core + +
+ +
+ +
+ + numpy._core._dtype_ctypes +SourceModule
+imports: + _ctypes + • ctypes + • numpy + • numpy._core + +
+
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core._exceptions +SourceModule
+imports: + numpy._core + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy._core._internal +SourceModule
+imports: + ast + • ctypes + • inspect + • math + • numpy + • numpy._core + • numpy._core._multiarray_umath + • numpy._core.multiarray + • numpy.exceptions + • re + • sys + • warnings + +
+
+imported by: + numpy._core + • numpy._globals + • numpy.ctypeslib._ctypeslib + +
+ +
+ +
+ + numpy._core._methods +SourceModule +
+imported by: + numpy._core + • numpy._core.fromnumeric + +
+ +
+ +
+ + numpy._core._multiarray_tests C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\_core\_multiarray_tests.cp312-win_amd64.pyd
+imports: + numpy._core + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy._core._multiarray_umath C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\_core\_multiarray_umath.cp312-win_amd64.pyd
+imports: + numpy._core + +
+ + +
+ +
+ + numpy._core._string_helpers +SourceModule
+imports: + numpy._core + +
+
+imported by: + numpy._core.numerictypes + +
+ +
+ +
+ + numpy._core._type_aliases +SourceModule
+imports: + numpy._core + • numpy._core.multiarray + +
+
+imported by: + numpy._core.numerictypes + +
+ +
+ +
+ + numpy._core._ufunc_config +SourceModule
+imports: + functools + • numpy._core + • numpy._core.umath + • numpy._utils + +
+
+imported by: + numpy._core.numeric + +
+ +
+ +
+ + numpy._core.add +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.all +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core.amax +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.amin +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.arccos +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.arccosh +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.arcsin +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.arcsinh +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.arctan +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.arctan2 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.arctanh +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.argsort +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.array +MissingModule + +
+ +
+ + numpy._core.arrayprint +SourceModule +
+imported by: + numpy._core + • numpy._core.numeric + • numpy._core.records + +
+ +
+ +
+ + numpy._core.asanyarray +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.asarray +MissingModule + +
+ +
+ + numpy._core.atleast_1d +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._polynomial_impl + +
+ +
+ +
+ + numpy._core.atleast_2d +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.atleast_3d +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._shape_base_impl + +
+ +
+ +
+ + numpy._core.bitwise_and +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.bitwise_count +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.bitwise_or +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.bitwise_xor +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.bool_ +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.byte +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.bytes_ +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.cbrt +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.cdouble +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.ceil +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.character +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.clongdouble +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.complex64 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.complexfloating +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.conj +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.conjugate +MissingModule
+imported by: + numpy + • numpy._core + • numpy.fft._pocketfft + +
+ +
+ +
+ + numpy._core.copysign +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.cos +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.cosh +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.count_nonzero +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.cross +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.csingle +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.datetime64 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.defchararray +SourceModule +
+imported by: + numpy.char + +
+ +
+ +
+ + numpy._core.deg2rad +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.degrees +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.diagonal +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.divide +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.divmod +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.dot +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._polynomial_impl + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.double +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.e +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.einsumfunc +SourceModule +
+imported by: + numpy._core + +
+ +
+ +
+ + numpy._core.empty +MissingModule + +
+ +
+ + numpy._core.empty_like +MissingModule
+imported by: + numpy + • numpy._core + • numpy.fft._pocketfft + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.equal +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.errstate +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core.euler_gamma +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.exp +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.exp2 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.expm1 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.fabs +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.finfo +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._polynomial_impl + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.float16 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.float32 +MissingModule + +
+ +
+ + numpy._core.float_power +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.floating +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.floor +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.floor_divide +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.fmax +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.fmin +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.fmod +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.frexp +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.fromnumeric +SourceModule + + +
+ +
+ + numpy._core.frompyfunc +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.function_base +SourceModule + + +
+ +
+ + numpy._core.gcd +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.getlimits +SourceModule +
+imported by: + numpy._core + • numpy.lib._type_check_impl + +
+ +
+ +
+ + numpy._core.greater +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.greater_equal +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.half +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.heaviside +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.hstack +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._polynomial_impl + +
+ +
+ +
+ + numpy._core.hypot +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.iinfo +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._twodim_base_impl + +
+ +
+ +
+ + numpy._core.inexact +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.inf +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core.int16 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.int32 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.int64 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.int8 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.intc +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.integer +MissingModule
+imported by: + numpy + • numpy._core + • numpy.fft._helper + +
+ +
+ +
+ + numpy._core.intp +MissingModule + +
+ +
+ + numpy._core.isfinite +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.isnan +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core.isscalar +MissingModule + +
+ +
+ + numpy._core.lcm +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.ldexp +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.left_shift +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.less +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.less_equal +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.linspace +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._index_tricks_impl + +
+ +
+ +
+ + numpy._core.log +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.log10 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.log1p +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.log2 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.logaddexp +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.logaddexp2 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.logical_and +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.logical_not +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.logical_or +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.logical_xor +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.long +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.longdouble +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.longlong +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.matmul +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.matrix_transpose +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.matvec +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.maximum +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.memmap +SourceModule
+imports: + contextlib + • mmap + • numpy + • numpy._core + • numpy._core.numeric + • numpy._utils + • operator + • os.path + +
+
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.minimum +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.mod +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.modf +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.moveaxis +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.multiarray +SourceModule + + +
+ +
+ + numpy._core.multiply +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.ndarray +MissingModule + +
+ +
+ + numpy._core.negative +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.newaxis +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.nextafter +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.not_equal +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.number +MissingModule
+imported by: + numpy + • numpy._core + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core.numeric +SourceModule + + +
+ +
+ + numpy._core.numerictypes +SourceModule + + +
+ +
+ + numpy._core.object_ +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core.ones +MissingModule
+imported by: + numpy + • numpy._core + • numpy.lib._polynomial_impl + +
+ +
+ +
+ + numpy._core.outer +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.overrides +SourceModule + + +
+ +
+ + numpy._core.pi +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.positive +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.power +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.printoptions +SourceModule
+imports: + contextvars + • numpy._core + • sys + +
+
+imported by: + numpy + • numpy._core.arrayprint + +
+ +
+ +
+ + numpy._core.prod +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.rad2deg +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.radians +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.reciprocal +MissingModule
+imported by: + numpy + • numpy._core + • numpy.fft._pocketfft + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.records +SourceModule +
+imported by: + numpy._core + • numpy.rec + +
+ +
+ +
+ + numpy._core.remainder +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.right_shift +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.rint +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.shape_base +SourceModule + + +
+ +
+ + numpy._core.short +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.sign +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.signbit +MissingModule
+imported by: + numpy + • numpy._core + • numpy.testing._private.utils + +
+ +
+ +
+ + numpy._core.signedinteger +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.single +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.sinh +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.sort +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.spacing +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.sqrt +MissingModule
+imported by: + numpy + • numpy._core + • numpy.fft._pocketfft + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.square +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.str_ +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.strings +SourceModule
+imports: + functools + • numpy + • numpy._core + • numpy._core.multiarray + • numpy._core.overrides + • numpy._core.umath + • sys + +
+
+imported by: + numpy._core.defchararray + • numpy.strings + +
+ +
+ +
+ + numpy._core.subtract +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.sum +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.swapaxes +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.tan +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.tanh +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.tensordot +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.timedelta64 +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.trace +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.transpose +MissingModule + +
+ +
+ + numpy._core.true_divide +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.trunc +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.ubyte +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.uint +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.uint16 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.uint32 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.uint64 +MissingModule
+imported by: + numpy + • numpy._array_api_info + • numpy._core + +
+ +
+ +
+ + numpy._core.uintc +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.uintp +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.ulong +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.ulonglong +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.umath +SourceModule
+imports: + numpy + • numpy._core + • numpy._core._multiarray_umath + +
+ + +
+ +
+ + numpy._core.unsignedinteger +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.ushort +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.vecdot +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._core.vecmat +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.void +MissingModule
+imported by: + numpy + • numpy._core + +
+ +
+ +
+ + numpy._core.zeros +MissingModule
+imported by: + numpy + • numpy._core + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._distributor_init +SourceModule
+imports: + numpy + • numpy._distributor_init_local + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy._distributor_init_local +MissingModule
+imported by: + numpy + • numpy._distributor_init + +
+ +
+ +
+ + numpy._expired_attrs_2_0 +SourceModule
+imports: + numpy + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy._globals +SourceModule
+imports: + enum + • numpy + • numpy._core._internal + • numpy._utils + +
+
+imported by: + numpy + • numpy._core._methods + • numpy.linalg._linalg + +
+ +
+ +
+ + numpy._pytesttester +SourceModule
+imports: + numpy + • os + • sys + +
+
+imported by: + numpy + • numpy._core + • numpy.f2py + • numpy.fft + • numpy.lib + • numpy.linalg + • numpy.ma + • numpy.matrixlib + • numpy.polynomial + • numpy.random + • numpy.testing + • numpy.typing + +
+ +
+ +
+ + numpy._typing +Package + + +
+ +
+ + numpy._typing._add_docstring +SourceModule
+imports: + numpy._typing + • numpy._typing._array_like + • re + • textwrap + +
+
+imported by: + numpy.typing + +
+ +
+ +
+ + numpy._typing._array_like +SourceModule +
+imported by: + numpy._typing + • numpy._typing._add_docstring + +
+ +
+ +
+ + numpy._typing._char_codes +SourceModule
+imports: + numpy._typing + • typing + +
+
+imported by: + numpy._typing + • numpy._typing._dtype_like + +
+ +
+ +
+ + numpy._typing._dtype_like +SourceModule
+imports: + collections.abc + • numpy + • numpy._typing + • numpy._typing._char_codes + • typing + +
+
+imported by: + numpy._typing + +
+ +
+ +
+ + numpy._typing._nbit +SourceModule
+imports: + numpy._typing + • numpy._typing._nbit_base + +
+
+imported by: + numpy._typing + +
+ +
+ +
+ + numpy._typing._nbit_base +SourceModule
+imports: + numpy._typing + • numpy._utils + • typing + +
+ + +
+ +
+ + numpy._typing._nested_sequence +SourceModule
+imports: + collections.abc + • numpy._typing + • typing + • typing_extensions + +
+
+imported by: + numpy._typing + • numpy._typing._array_like + +
+ +
+ +
+ + numpy._typing._scalars +SourceModule
+imports: + numpy + • numpy._typing + • typing + +
+
+imported by: + numpy._typing + +
+ +
+ +
+ + numpy._typing._shape +SourceModule
+imports: + collections.abc + • numpy._typing + • typing + +
+
+imported by: + numpy._typing + • numpy._typing._array_like + +
+ +
+ +
+ + numpy._typing._ufunc +SourceModule
+imports: + numpy + • numpy._typing + +
+
+imported by: + numpy._typing + • numpy.linalg._umath_linalg + +
+ +
+ +
+ + numpy._utils +Package
+imports: + functools + • numpy + • numpy._utils._conversions + • warnings + +
+ + +
+ +
+ + numpy._utils._conversions +SourceModule
+imports: + numpy._utils + +
+
+imported by: + numpy._utils + +
+ +
+ +
+ + numpy._utils._inspect +SourceModule
+imports: + numpy._utils + • types + +
+
+imported by: + numpy._core.overrides + +
+ +
+ +
+ + numpy.char +Package
+imports: + numpy + • numpy._core.defchararray + • warnings + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy.core +Package
+imports: + numpy + • numpy._core + • numpy.core._utils + +
+
+imported by: + numpy + • numpy.core._utils + • numpy.core.multiarray + +
+ +
+ +
+ + numpy.core._utils +SourceModule
+imports: + numpy.core + • warnings + +
+
+imported by: + numpy.core + • numpy.core.multiarray + +
+ +
+ +
+ + numpy.core.multiarray +SourceModule +
+imported by: + cv2 + +
+ +
+ +
+ + numpy.ctypeslib +Package
+imports: + numpy + • numpy.ctypeslib._ctypeslib + +
+
+imported by: + numpy + • numpy.ctypeslib._ctypeslib + +
+ +
+ +
+ + numpy.ctypeslib._ctypeslib +SourceModule
+imports: + ctypes + • numpy + • numpy._core._internal + • numpy._core.multiarray + • numpy._utils + • numpy.ctypeslib + • os + • sys + • sysconfig + +
+
+imported by: + numpy.ctypeslib + +
+ +
+ +
+ + numpy.dtypes +SourceModule
+imports: + numpy + • numpy._core._multiarray_umath + • numpy.dtypes + +
+
+imported by: + numpy + • numpy._typing._array_like + • numpy.dtypes + +
+ +
+ +
+ + numpy.exceptions +SourceModule
+imports: + numpy + +
+ + +
+ +
+ + numpy.f2py +Package + + +
+ +
+ + numpy.f2py.__version__ +SourceModule
+imports: + numpy.f2py + • numpy.version + +
+ + +
+ +
+ + numpy.f2py._backends +Package
+imports: + numpy.f2py + • numpy.f2py._backends._meson + +
+ + +
+ +
+ + numpy.f2py._backends._backend +SourceModule
+imports: + abc + • numpy.f2py._backends + +
+
+imported by: + numpy.f2py._backends._meson + +
+ +
+ +
+ + numpy.f2py._backends._meson +SourceModule
+imports: + errno + • itertools + • numpy.f2py._backends + • numpy.f2py._backends._backend + • os + • pathlib + • re + • shutil + • string + • subprocess + • sys + +
+
+imported by: + numpy.f2py._backends + +
+ +
+ +
+ + numpy.f2py._isocbind +SourceModule
+imports: + numpy.f2py + +
+
+imported by: + numpy.f2py.capi_maps + • numpy.f2py.func2subr + +
+ +
+ +
+ + numpy.f2py.auxfuncs +SourceModule
+imports: + functools + • numpy.f2py + • numpy.f2py.__version__ + • numpy.f2py.capi_maps + • numpy.f2py.cfuncs + • pprint + • re + • sys + • types + +
+ + +
+ +
+ + numpy.f2py.capi_maps +SourceModule + + +
+ +
+ + numpy.f2py.cb_rules +SourceModule +
+imported by: + numpy.f2py + • numpy.f2py.capi_maps + • numpy.f2py.f2py2e + +
+ +
+ +
+ + numpy.f2py.cfuncs +SourceModule
+imports: + copy + • numpy.f2py + • numpy.f2py.__version__ + • numpy.f2py.capi_maps + • sys + +
+ + +
+ +
+ + numpy.f2py.common_rules +SourceModule +
+imported by: + numpy.f2py + • numpy.f2py.rules + +
+ +
+ +
+ + numpy.f2py.crackfortran +SourceModule
+imports: + charset_normalizer + • codecs + • copy + • fileinput + • numpy.f2py + • numpy.f2py.__version__ + • numpy.f2py.auxfuncs + • numpy.f2py.symbolic + • os + • pathlib + • platform + • re + • string + • sys + +
+ + +
+ +
+ + numpy.f2py.diagnose +SourceModule
+imports: + numpy + • numpy.f2py + • numpy.f2py.f2py2e + • os + • sys + • tempfile + +
+
+imported by: + numpy.f2py + +
+ +
+ +
+ + numpy.f2py.f2py2e +SourceModule +
+imported by: + numpy.f2py + • numpy.f2py.diagnose + +
+ +
+ +
+ + numpy.f2py.f90mod_rules +SourceModule +
+imported by: + numpy.f2py + • numpy.f2py.f2py2e + • numpy.f2py.rules + +
+ +
+ +
+ + numpy.f2py.func2subr +SourceModule
+imports: + copy + • numpy.f2py + • numpy.f2py._isocbind + • numpy.f2py.auxfuncs + +
+ + +
+ +
+ + numpy.f2py.rules +SourceModule +
+imported by: + numpy.f2py + • numpy.f2py.f2py2e + • numpy.f2py.f90mod_rules + +
+ +
+ +
+ + numpy.f2py.symbolic +SourceModule
+imports: + enum + • math + • numpy.f2py + • re + • warnings + +
+
+imported by: + numpy.f2py + • numpy.f2py.crackfortran + +
+ +
+ +
+ + numpy.f2py.use_rules +SourceModule
+imports: + numpy.f2py + • numpy.f2py.auxfuncs + +
+
+imported by: + numpy.f2py + • numpy.f2py.rules + +
+ +
+ +
+ + numpy.fft +Package + + +
+ +
+ + numpy.fft._helper +SourceModule +
+imported by: + numpy.fft + +
+ +
+ +
+ + numpy.fft._pocketfft +SourceModule +
+imported by: + numpy.fft + +
+ +
+ +
+ + numpy.fft._pocketfft_umath C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\fft\_pocketfft_umath.cp312-win_amd64.pyd
+imports: + numpy.fft + +
+
+imported by: + numpy.fft + • numpy.fft._pocketfft + +
+ +
+ +
+ + numpy.lib +Package + + +
+ +
+ + numpy.lib._array_utils_impl +SourceModule + + +
+ +
+ + numpy.lib._arraypad_impl +SourceModule
+imports: + numpy + • numpy._core.overrides + • numpy.lib + • numpy.lib._index_tricks_impl + • typing + +
+
+imported by: + numpy + • numpy.lib + +
+ +
+ +
+ + numpy.lib._arraysetops_impl +SourceModule +
+imported by: + numpy + • numpy.lib + +
+ +
+ +
+ + numpy.lib._arrayterator_impl +SourceModule
+imports: + functools + • numpy.lib + • operator + +
+
+imported by: + numpy.lib + +
+ +
+ +
+ + numpy.lib._datasource +SourceModule
+imports: + bz2 + • gzip + • lzma + • numpy._utils + • numpy.lib + • os + • shutil + • tempfile + • urllib.error + • urllib.parse + • urllib.request + +
+
+imported by: + numpy.lib._npyio_impl + +
+ +
+ +
+ + numpy.lib._format_impl +SourceModule
+imports: + ast + • io + • numpy + • numpy._utils + • numpy.lib + • numpy.lib._utils_impl + • os + • pickle + • struct + • tokenize + • warnings + +
+
+imported by: + numpy.lib._npyio_impl + • numpy.lib.format + +
+ +
+ +
+ + numpy.lib._function_base_impl +SourceModule + + +
+ +
+ + numpy.lib._histograms_impl +SourceModule
+imports: + contextlib + • functools + • numpy + • numpy._core + • numpy._core.overrides + • numpy.lib + • operator + • warnings + +
+
+imported by: + numpy + • numpy.lib + • numpy.lib._function_base_impl + +
+ +
+ +
+ + numpy.lib._index_tricks_impl +SourceModule + + +
+ +
+ + numpy.lib._iotools +SourceModule
+imports: + itertools + • numpy + • numpy._core.numeric + • numpy._utils + • numpy.lib + +
+
+imported by: + numpy.lib._npyio_impl + • numpy.lib.recfunctions + +
+ +
+ +
+ + numpy.lib._nanfunctions_impl +SourceModule +
+imported by: + numpy + • numpy.lib + +
+ +
+ +
+ + numpy.lib._npyio_impl +SourceModule +
+imported by: + numpy + • numpy.lib + • numpy.lib.npyio + +
+ +
+ +
+ + numpy.lib._polynomial_impl +SourceModule +
+imported by: + numpy + • numpy.lib + +
+ +
+ +
+ + numpy.lib._scimath_impl +SourceModule +
+imported by: + numpy.lib.scimath + +
+ +
+ +
+ + numpy.lib._shape_base_impl +SourceModule +
+imported by: + numpy + • numpy.lib + +
+ +
+ +
+ + numpy.lib._stride_tricks_impl +SourceModule + + +
+ +
+ + numpy.lib._twodim_base_impl +SourceModule + + +
+ +
+ + numpy.lib._type_check_impl +SourceModule +
+imported by: + numpy + • numpy.lib + • numpy.lib._polynomial_impl + • numpy.lib._scimath_impl + +
+ +
+ +
+ + numpy.lib._ufunclike_impl +SourceModule
+imports: + numpy._core.numeric + • numpy._core.overrides + • numpy.lib + • warnings + +
+
+imported by: + numpy + • numpy.lib + • numpy.lib._type_check_impl + +
+ +
+ +
+ + numpy.lib._utils_impl +SourceModule
+imports: + inspect + • numpy + • numpy._core + • numpy._core._multiarray_umath + • numpy._core.ndarray + • numpy._utils + • numpy.lib + • os + • platform + • pprint + • pydoc + • sys + • threadpoolctl + • types + +
+
+imported by: + numpy + • numpy.lib + • numpy.lib._format_impl + +
+ +
+ +
+ + numpy.lib._version +SourceModule
+imports: + numpy.lib + • re + +
+
+imported by: + numpy.lib + +
+ +
+ +
+ + numpy.lib.array_utils +SourceModule
+imports: + numpy.lib + • numpy.lib._array_utils_impl + +
+ + +
+ +
+ + numpy.lib.format +SourceModule
+imports: + numpy.lib + • numpy.lib._format_impl + +
+
+imported by: + numpy.lib + • numpy.lib._npyio_impl + +
+ +
+ +
+ + numpy.lib.introspect +SourceModule
+imports: + numpy._core._multiarray_umath + • numpy.lib + • re + +
+
+imported by: + numpy.lib + +
+ +
+ +
+ + numpy.lib.mixins +SourceModule
+imports: + numpy._core + • numpy._core.umath + • numpy.lib + +
+
+imported by: + numpy.lib + +
+ +
+ +
+ + numpy.lib.npyio +SourceModule
+imports: + numpy.lib + • numpy.lib._npyio_impl + +
+
+imported by: + numpy.lib + +
+ +
+ +
+ + numpy.lib.recfunctions +SourceModule
+imports: + itertools + • numpy + • numpy._core.overrides + • numpy.lib + • numpy.lib._iotools + • numpy.ma + • numpy.ma.mrecords + +
+
+imported by: + numpy.lib + • numpy.testing.overrides + +
+ +
+ +
+ + numpy.lib.scimath +SourceModule
+imports: + numpy.lib + • numpy.lib._scimath_impl + +
+
+imported by: + numpy + • numpy.lib + +
+ +
+ +
+ + numpy.lib.stride_tricks +SourceModule +
+imported by: + numpy.lib + +
+ +
+ +
+ + numpy.linalg +Package + + +
+ +
+ + numpy.linalg._linalg +SourceModule
+imports: + functools + • numpy + • numpy._core + • numpy._core.add + • numpy._core.all + • numpy._core.amax + • numpy._core.amin + • numpy._core.argsort + • numpy._core.array + • numpy._core.asanyarray + • numpy._core.asarray + • numpy._core.atleast_2d + • numpy._core.cdouble + • numpy._core.complexfloating + • numpy._core.count_nonzero + • numpy._core.cross + • numpy._core.csingle + • numpy._core.diagonal + • numpy._core.divide + • numpy._core.dot + • numpy._core.double + • numpy._core.empty + • numpy._core.empty_like + • numpy._core.errstate + • numpy._core.finfo + • numpy._core.inexact + • numpy._core.inf + • numpy._core.intc + • numpy._core.intp + • numpy._core.isfinite + • numpy._core.isnan + • numpy._core.matmul + • numpy._core.matrix_transpose + • numpy._core.moveaxis + • numpy._core.multiply + • numpy._core.newaxis + • numpy._core.object_ + • numpy._core.outer + • numpy._core.overrides + • numpy._core.prod + • numpy._core.reciprocal + • numpy._core.single + • numpy._core.sort + • numpy._core.sqrt + • numpy._core.sum + • numpy._core.swapaxes + • numpy._core.tensordot + • numpy._core.trace + • numpy._core.transpose + • numpy._core.vecdot + • numpy._core.zeros + • numpy._globals + • numpy._typing + • numpy._utils + • numpy.lib._twodim_base_impl + • numpy.lib.array_utils + • numpy.linalg + • numpy.linalg._umath_linalg + • operator + • typing + • warnings + +
+ + +
+ +
+ + numpy.linalg._umath_linalg C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\linalg\_umath_linalg.cp312-win_amd64.pyd
+imports: + numpy + • numpy._typing._ufunc + • numpy.linalg + • typing + +
+ + +
+ +
+ + numpy.ma +Package
+imports: + numpy + • numpy._pytesttester + • numpy.ma + • numpy.ma.core + • numpy.ma.extras + +
+ + +
+ +
+ + numpy.ma.core +SourceModule
+imports: + builtins + • copy + • datetime + • functools + • inspect + • numpy + • numpy._core + • numpy._core.multiarray + • numpy._core.numeric + • numpy._core.numerictypes + • numpy._core.umath + • numpy._utils + • numpy.ma + • operator + • re + • textwrap + • warnings + +
+
+imported by: + numpy.ma + • numpy.ma.extras + +
+ +
+ +
+ + numpy.ma.extras +SourceModule +
+imported by: + numpy.ma + +
+ +
+ +
+ + numpy.ma.mrecords +SourceModule
+imports: + builtins + • numpy + • numpy.ma + • warnings + +
+
+imported by: + numpy.lib.recfunctions + +
+ +
+ +
+ + numpy.matlib +SourceModule
+imports: + numpy + • numpy.matrixlib.defmatrix + • warnings + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy.matrixlib +Package + + +
+ +
+ + numpy.matrixlib.defmatrix +SourceModule
+imports: + ast + • numpy._core.numeric + • numpy._utils + • numpy.linalg + • numpy.matrixlib + • sys + • warnings + +
+
+imported by: + numpy.lib._shape_base_impl + • numpy.matlib + • numpy.matrixlib + +
+ +
+ +
+ + numpy.polynomial +Package + + +
+ +
+ + numpy.polynomial._polybase +SourceModule
+imports: + abc + • collections.abc + • numbers + • numpy + • numpy.polynomial + • numpy.polynomial.polyutils + • os + +
+ + +
+ +
+ + numpy.polynomial.chebyshev +SourceModule +
+imported by: + numpy.polynomial + +
+ +
+ +
+ + numpy.polynomial.hermite +SourceModule +
+imported by: + numpy.polynomial + +
+ +
+ +
+ + numpy.polynomial.hermite_e +SourceModule +
+imported by: + numpy.polynomial + +
+ +
+ +
+ + numpy.polynomial.laguerre +SourceModule +
+imported by: + numpy.polynomial + +
+ +
+ +
+ + numpy.polynomial.legendre +SourceModule +
+imported by: + numpy.polynomial + +
+ +
+ +
+ + numpy.polynomial.polynomial +SourceModule + + +
+ +
+ + numpy.polynomial.polyutils +SourceModule
+imports: + functools + • numpy + • numpy._core.multiarray + • numpy.polynomial + • operator + • warnings + +
+ + +
+ +
+ + numpy.random +Package + + +
+ +
+ + numpy.random._bounded_integers C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\_bounded_integers.cp312-win_amd64.pyd
+imports: + numpy.random + +
+
+imported by: + numpy.random + +
+ +
+ +
+ + numpy.random._common C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\_common.cp312-win_amd64.pyd
+imports: + _typeshed + • collections.abc + • numpy + • numpy.random + • typing + +
+
+imported by: + numpy.random + +
+ +
+ +
+ + numpy.random._generator C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\_generator.cp312-win_amd64.pyd +
+imported by: + numpy.random + • numpy.random._pickle + +
+ +
+ +
+ + numpy.random._mt19937 C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\_mt19937.cp312-win_amd64.pyd
+imports: + numpy + • numpy._typing + • numpy.random + • numpy.random.bit_generator + • numpy.typing + • typing + +
+
+imported by: + numpy.random + • numpy.random._pickle + +
+ +
+ +
+ + numpy.random._pcg64 C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\_pcg64.cp312-win_amd64.pyd
+imports: + numpy._typing + • numpy.random + • numpy.random.bit_generator + • typing + +
+
+imported by: + numpy.random + • numpy.random._pickle + +
+ +
+ +
+ + numpy.random._philox C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\_philox.cp312-win_amd64.pyd
+imports: + numpy + • numpy._typing + • numpy.random + • numpy.random.bit_generator + • numpy.typing + • typing + +
+
+imported by: + numpy.random + • numpy.random._pickle + +
+ +
+ +
+ + numpy.random._pickle +SourceModule +
+imported by: + numpy.random + +
+ +
+ +
+ + numpy.random._sfc64 C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\_sfc64.cp312-win_amd64.pyd
+imports: + numpy + • numpy._typing + • numpy.random + • numpy.random.bit_generator + • typing + +
+
+imported by: + numpy.random + • numpy.random._pickle + +
+ +
+ +
+ + numpy.random.bit_generator C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\bit_generator.cp312-win_amd64.pyd
+imports: + _typeshed + • abc + • collections.abc + • numpy + • numpy._typing + • numpy.random + • threading + • typing + • typing_extensions + +
+ + +
+ +
+ + numpy.random.mtrand C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\MaskReviewer_package\.build_venv\Lib\site-packages\numpy\random\mtrand.cp312-win_amd64.pyd
+imports: + builtins + • collections.abc + • numpy + • numpy._typing + • numpy.random + • numpy.random.bit_generator + • typing + +
+ + +
+ +
+ + numpy.rec +Package
+imports: + numpy + • numpy._core.records + +
+
+imported by: + numpy + +
+ +
+ +
+ + numpy.strings +Package
+imports: + numpy + • numpy._core.strings + +
+
+imported by: + numpy + • numpy._core.defchararray + +
+ +
+ +
+ + numpy.testing +Package +
+imported by: + numpy + • numpy.testing + • numpy.testing._private + • numpy.testing.overrides + +
+ +
+ +
+ + numpy.testing._private +Package + + +
+ +
+ + numpy.testing._private.extbuild +SourceModule
+imports: + importlib.util + • numpy.testing._private + • numpy.testing._private.utils + • os + • pathlib + • sys + • sysconfig + • textwrap + +
+
+imported by: + numpy.testing + • numpy.testing._private + +
+ +
+ +
+ + numpy.testing._private.utils +SourceModule + + +
+ +
+ + numpy.testing.overrides +SourceModule +
+imported by: + numpy.testing + +
+ +
+ +
+ + numpy.typing +Package +
+imported by: + numpy + • numpy.random._mt19937 + • numpy.random._philox + +
+ +
+ +
+ + numpy.version +SourceModule
+imports: + numpy + +
+
+imported by: + numpy + • numpy._core + • numpy.f2py.__version__ + +
+ +
+ +
+ + opcode +SourceModule
+imports: + _opcode + +
+
+imported by: + dis + +
+ +
+ +
+ + operator +SourceModule
+imports: + _operator + • builtins + • functools + +
+ + +
+ +
+ + os +SourceModule
+imports: + _collections_abc + • abc + • io + • nt + • ntpath + • os.path + • posix + • posixpath + • stat + • subprocess + • sys + • warnings + +
+
+imported by: + MaskReviewer.py + • _aix_support + • argparse + • asyncio.base_events + • asyncio.coroutines + • asyncio.events + • asyncio.proactor_events + • asyncio.selector_events + • asyncio.unix_events + • asyncio.windows_utils + • bdb + • bz2 + • concurrent.futures.process + • concurrent.futures.thread + • contextlib + • ctypes + • cv2 + • cv2.config + • cv2.data + • cv2.load_config_py3 + • doctest + • email.utils + • fileinput + • fnmatch + • genericpath + • getopt + • getpass + • gettext + • glob + • gzip + • http.cookiejar + • http.server + • importlib.metadata + • importlib.resources._common + • importlib.resources._legacy + • importlib.resources.abc + • inspect + • linecache + • locale + • logging + • lzma + • mimetypes + • multiprocessing.connection + • multiprocessing.context + • multiprocessing.forkserver + • multiprocessing.heap + • multiprocessing.managers + • multiprocessing.pool + • multiprocessing.popen_fork + • multiprocessing.popen_forkserver + • multiprocessing.popen_spawn_posix + • multiprocessing.popen_spawn_win32 + • multiprocessing.process + • multiprocessing.queues + • multiprocessing.reduction + • multiprocessing.resource_sharer + • multiprocessing.resource_tracker + • multiprocessing.shared_memory + • multiprocessing.spawn + • multiprocessing.util + • netrc + • ntpath + • numpy + • numpy._core + • numpy._core._add_newdocs_scalars + • numpy._core._methods + • numpy._core.records + • numpy._pytesttester + • numpy.ctypeslib._ctypeslib + • numpy.f2py + • numpy.f2py._backends._meson + • numpy.f2py.capi_maps + • numpy.f2py.crackfortran + • numpy.f2py.diagnose + • numpy.f2py.f2py2e + • numpy.f2py.rules + • numpy.lib._datasource + • numpy.lib._format_impl + • numpy.lib._npyio_impl + • numpy.lib._twodim_base_impl + • numpy.lib._utils_impl + • numpy.polynomial._polybase + • numpy.testing._private.extbuild + • numpy.testing._private.utils + • os.path + • pathlib + • pdb + • pkgutil + • platform + • posixpath + • py_compile + • pydoc + • pyi_rth__tkinter.py + • pyi_rth_inspect.py + • random + • runpy + • shlex + • shutil + • socket + • socketserver + • ssl + • subprocess + • sysconfig + • tarfile + • tempfile + • threading + • tkinter + • tkinter.filedialog + • unittest.loader + • unittest.main + • urllib.request + • webbrowser + • xml.sax + • xml.sax.saxutils + • zipfile + +
+ +
+ +
+ + os.path +AliasNode
+imports: + ntpath + • os + +
+
+imported by: + numpy._core.memmap + • os + • pkgutil + • py_compile + • sysconfig + • tracemalloc + • unittest.util + +
+ +
+ +
+ + pathlib +SourceModule
+imports: + _collections_abc + • errno + • fnmatch + • functools + • grp + • io + • ntpath + • os + • posixpath + • pwd + • re + • stat + • sys + • urllib.parse + • warnings + +
+ + +
+ +
+ + pdb +SourceModule
+imports: + bdb + • cmd + • code + • dis + • functools + • getopt + • glob + • inspect + • io + • linecache + • os + • pdb + • pprint + • pydoc + • re + • readline + • runpy + • shlex + • signal + • sys + • token + • tokenize + • traceback + • typing + +
+
+imported by: + doctest + • pdb + +
+ +
+ +
+ + pickle +SourceModule
+imports: + _compat_pickle + • _pickle + • codecs + • copyreg + • functools + • io + • itertools + • pprint + • re + • struct + • sys + • types + +
+ + +
+ +
+ + pkgutil +SourceModule
+imports: + collections + • functools + • importlib + • importlib.machinery + • importlib.util + • inspect + • marshal + • os + • os.path + • re + • sys + • types + • warnings + • zipimport + +
+
+imported by: + pydoc + • pyi_rth_pkgutil.py + • runpy + +
+ +
+ +
+ + platform +SourceModule
+imports: + 'java.lang' + • _winreg + • _wmi + • collections + • functools + • itertools + • java + • os + • re + • socket + • struct + • subprocess + • sys + • vms_lib + • winreg + +
+ + +
+ +
+ + posix +MissingModule
+imports: + resource + +
+
+imported by: + importlib._bootstrap_external + • os + • posixpath + • shutil + +
+ +
+ +
+ + posixpath +SourceModule
+imports: + genericpath + • os + • posix + • pwd + • re + • stat + • sys + +
+
+imported by: + MaskReviewer.py + • fnmatch + • http.server + • importlib.metadata + • mimetypes + • os + • pathlib + • zipfile._path + +
+ +
+ +
+ + pprint +SourceModule
+imports: + collections + • dataclasses + • io + • re + • sys + • types + +
+ + +
+ +
+ + psutil +MissingModule
+imported by: + numpy.testing._private.utils + +
+ +
+ +
+ + pwd +MissingModule
+imported by: + getpass + • http.server + • netrc + • pathlib + • posixpath + • shutil + • subprocess + • tarfile + +
+ +
+ +
+ + py_compile +SourceModule
+imports: + argparse + • enum + • importlib._bootstrap_external + • importlib.machinery + • importlib.util + • os + • os.path + • sys + • traceback + +
+
+imported by: + zipfile + +
+ +
+ +
+ + pydoc +SourceModule
+imports: + __future__ + • builtins + • collections + • email.message + • getopt + • http.server + • importlib._bootstrap + • importlib._bootstrap_external + • importlib.machinery + • importlib.util + • inspect + • io + • os + • pkgutil + • platform + • pydoc_data.topics + • re + • reprlib + • select + • subprocess + • sys + • sysconfig + • tempfile + • textwrap + • threading + • time + • tokenize + • traceback + • tty + • urllib.parse + • warnings + • webbrowser + +
+
+imported by: + numpy.lib._utils_impl + • pdb + +
+ +
+ +
+ + pydoc_data +Package
+imported by: + pydoc_data.topics + +
+ +
+ +
+ + pydoc_data.topics +SourceModule
+imports: + pydoc_data + +
+
+imported by: + pydoc + +
+ +
+ +
+ + pyexpat C:\Python312\DLLs\pyexpat.pyd
+imported by: + xml.parsers.expat + +
+ +
+ +
+ + pyimod02_importers +MissingModule
+imported by: + pyi_rth_pkgutil.py + +
+ +
+ +
+ + queue +SourceModule
+imports: + _queue + • collections + • heapq + • threading + • time + • types + +
+ + +
+ +
+ + quopri +SourceModule
+imports: + binascii + • getopt + • io + • sys + +
+
+imported by: + email.encoders + • email.message + • encodings.quopri_codec + +
+ +
+ +
+ + random +SourceModule
+imports: + _collections_abc + • _random + • _sha2 + • bisect + • hashlib + • itertools + • math + • operator + • os + • statistics + • time + • warnings + +
+
+imported by: + email.generator + • email.utils + • secrets + • statistics + • tempfile + +
+ +
+ +
+ + re +Package
+imports: + _sre + • copyreg + • enum + • functools + • re + • re._compiler + • re._constants + • re._parser + • warnings + +
+
+imported by: + MaskReviewer.py + • _pydecimal + • _sre + • _strptime + • argparse + • ast + • base64 + • csv + • dataclasses + • difflib + • doctest + • email._encoded_words + • email._header_value_parser + • email.feedparser + • email.generator + • email.header + • email.message + • email.policy + • email.quoprimime + • email.utils + • encodings.idna + • fnmatch + • fractions + • ftplib + • gettext + • glob + • html + • http.client + • http.cookiejar + • importlib.metadata + • importlib.metadata._adapters + • importlib.metadata._text + • inspect + • ipaddress + • json.decoder + • json.encoder + • json.scanner + • locale + • logging + • numpy._core._internal + • numpy._typing._add_docstring + • numpy.f2py._backends._meson + • numpy.f2py.auxfuncs + • numpy.f2py.capi_maps + • numpy.f2py.crackfortran + • numpy.f2py.f2py2e + • numpy.f2py.symbolic + • numpy.lib._function_base_impl + • numpy.lib._npyio_impl + • numpy.lib._polynomial_impl + • numpy.lib._version + • numpy.lib.introspect + • numpy.ma.core + • numpy.testing._private.utils + • pathlib + • pdb + • pickle + • pkgutil + • platform + • posixpath + • pprint + • pydoc + • re + • re._casefix + • re._compiler + • re._constants + • re._parser + • shlex + • sre_compile + • sre_constants + • sre_parse + • string + • sysconfig + • tarfile + • textwrap + • tkinter + • tokenize + • typing + • unittest.case + • unittest.loader + • urllib.parse + • urllib.request + • warnings + • zipfile._path + • zipfile._path.glob + +
+ +
+ +
+ + re._casefix +SourceModule
+imports: + re + +
+
+imported by: + MaskReviewer.py + • re._compiler + +
+ +
+ +
+ + re._compiler +SourceModule
+imports: + _sre + • re + • re._casefix + • re._constants + • re._parser + • sys + +
+
+imported by: + MaskReviewer.py + • re + • sre_compile + +
+ +
+ +
+ + re._constants +SourceModule
+imports: + _sre + • re + +
+
+imported by: + MaskReviewer.py + • re + • re._compiler + • re._parser + • sre_constants + +
+ +
+ +
+ + re._parser +SourceModule
+imports: + re + • re._constants + • unicodedata + • warnings + +
+
+imported by: + MaskReviewer.py + • re + • re._compiler + • sre_parse + +
+ +
+ +
+ + readline +MissingModule
+imported by: + cmd + • code + • pdb + +
+ +
+ +
+ + reprlib +SourceModule
+imports: + _thread + • builtins + • itertools + +
+
+imported by: + MaskReviewer.py + • asyncio.base_futures + • asyncio.base_tasks + • asyncio.format_helpers + • bdb + • collections + • functools + • pydoc + +
+ +
+ +
+ + resource +MissingModule
+imported by: + posix + +
+ +
+ +
+ + runpy +SourceModule
+imports: + importlib.machinery + • importlib.util + • io + • os + • pkgutil + • sys + • warnings + +
+
+imported by: + multiprocessing.spawn + • pdb + +
+ +
+ +
+ + secrets +SourceModule
+imports: + base64 + • hmac + • random + +
+
+imported by: + multiprocessing.shared_memory + +
+ +
+ +
+ + select C:\Python312\DLLs\select.pyd
+imported by: + http.server + • pydoc + • selectors + • subprocess + +
+ +
+ +
+ + selectors +SourceModule
+imports: + abc + • collections + • collections.abc + • math + • select + • sys + +
+ + +
+ +
+ + shlex +SourceModule
+imports: + collections + • io + • os + • re + • sys + +
+
+imported by: + pdb + • webbrowser + +
+ +
+ +
+ + shutil +SourceModule
+imports: + _winapi + • bz2 + • collections + • errno + • fnmatch + • grp + • lzma + • nt + • os + • posix + • pwd + • stat + • sys + • tarfile + • warnings + • zipfile + • zlib + +
+ + +
+ +
+ + signal +SourceModule
+imports: + _signal + • enum + +
+ + +
+ +
+ + socket +SourceModule
+imports: + _socket + • array + • enum + • errno + • io + • os + • selectors + • sys + +
+ + +
+ +
+ + socketserver +SourceModule
+imports: + io + • os + • selectors + • socket + • sys + • threading + • time + • traceback + +
+
+imported by: + http.server + +
+ +
+ +
+ + sre_compile +SourceModule
+imports: + re + • re._compiler + • warnings + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + sre_constants +SourceModule
+imports: + re + • re._constants + • warnings + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + sre_parse +SourceModule
+imports: + re + • re._parser + • warnings + +
+
+imported by: + MaskReviewer.py + +
+ +
+ +
+ + ssl +SourceModule
+imports: + _ssl + • base64 + • calendar + • collections + • enum + • errno + • os + • socket + • sys + • time + • warnings + +
+ + +
+ +
+ + stat +SourceModule
+imports: + _stat + +
+
+imported by: + MaskReviewer.py + • asyncio.base_events + • asyncio.unix_events + • genericpath + • glob + • netrc + • ntpath + • os + • pathlib + • posixpath + • shutil + • tarfile + • zipfile + +
+ +
+ +
+ + statistics +SourceModule
+imports: + _statistics + • bisect + • collections + • decimal + • fractions + • functools + • itertools + • math + • numbers + • operator + • random + • sys + +
+
+imported by: + random + +
+ +
+ +
+ + string +SourceModule
+imports: + _string + • collections + • re + +
+ + +
+ +
+ + stringprep +SourceModule
+imports: + unicodedata + +
+
+imported by: + encodings.idna + +
+ +
+ +
+ + struct +SourceModule
+imports: + _struct + +
+ + +
+ +
+ + subprocess +SourceModule
+imports: + _posixsubprocess + • _winapi + • builtins + • contextlib + • errno + • fcntl + • grp + • io + • locale + • msvcrt + • os + • pwd + • select + • selectors + • signal + • sys + • threading + • time + • types + • warnings + +
+ + +
+ +
+ + sys (builtin module)
+imported by: + MaskReviewer.py + • _aix_support + • _collections_abc + • _compression + • _pydatetime + • _pydecimal + • argparse + • ast + • asyncio + • asyncio.base_events + • asyncio.coroutines + • asyncio.events + • asyncio.format_helpers + • asyncio.futures + • asyncio.streams + • asyncio.unix_events + • asyncio.windows_events + • asyncio.windows_utils + • base64 + • bdb + • calendar + • cmd + • code + • codecs + • collections + • concurrent.futures.process + • contextlib + • ctypes + • ctypes._endian + • cv2 + • cv2.load_config_py2 + • cv2.load_config_py3 + • dataclasses + • dis + • doctest + • email._header_value_parser + • email.generator + • email.iterators + • email.policy + • encodings + • encodings.rot_13 + • encodings.utf_16 + • encodings.utf_32 + • enum + • fileinput + • fractions + • ftplib + • getopt + • getpass + • gettext + • glob + • gzip + • http.client + • http.server + • importlib + • importlib._bootstrap_external + • importlib.metadata + • importlib.util + • inspect + • linecache + • locale + • logging + • mimetypes + • multiprocessing + • multiprocessing.connection + • multiprocessing.context + • multiprocessing.dummy + • multiprocessing.forkserver + • multiprocessing.heap + • multiprocessing.managers + • multiprocessing.popen_spawn_win32 + • multiprocessing.process + • multiprocessing.queues + • multiprocessing.reduction + • multiprocessing.resource_sharer + • multiprocessing.resource_tracker + • multiprocessing.spawn + • multiprocessing.synchronize + • multiprocessing.util + • ntpath + • numpy + • numpy._core + • numpy._core._add_newdocs_scalars + • numpy._core._internal + • numpy._core.arrayprint + • numpy._core.numeric + • numpy._core.printoptions + • numpy._core.strings + • numpy._pytesttester + • numpy.ctypeslib._ctypeslib + • numpy.f2py + • numpy.f2py._backends._meson + • numpy.f2py.auxfuncs + • numpy.f2py.cfuncs + • numpy.f2py.crackfortran + • numpy.f2py.diagnose + • numpy.f2py.f2py2e + • numpy.f2py.rules + • numpy.lib._index_tricks_impl + • numpy.lib._utils_impl + • numpy.matrixlib.defmatrix + • numpy.testing._private.extbuild + • numpy.testing._private.utils + • os + • pathlib + • pdb + • pickle + • pkgutil + • platform + • posixpath + • pprint + • py_compile + • pydoc + • pyi_rth__tkinter.py + • pyi_rth_inspect.py + • pyi_rth_multiprocessing.py + • quopri + • re._compiler + • runpy + • selectors + • shlex + • shutil + • socket + • socketserver + • ssl + • statistics + • subprocess + • sysconfig + • tarfile + • tempfile + • threading + • tkinter + • tokenize + • traceback + • types + • typing + • unittest.case + • unittest.loader + • unittest.main + • unittest.result + • unittest.runner + • unittest.suite + • urllib.request + • warnings + • weakref + • webbrowser + • xml.parsers.expat + • xml.sax + • xml.sax.saxutils + • xmlrpc.client + • zipfile + • zipimport + +
+ +
+ +
+ + sysconfig +SourceModule
+imports: + _aix_support + • _imp + • os + • os.path + • pprint + • re + • sys + • threading + • types + • warnings + +
+ + +
+ +
+ + tarfile +SourceModule
+imports: + argparse + • builtins + • bz2 + • copy + • grp + • gzip + • io + • lzma + • os + • pwd + • re + • shutil + • stat + • struct + • sys + • time + • warnings + • zlib + +
+
+imported by: + shutil + +
+ +
+ +
+ + tempfile +SourceModule
+imports: + _thread + • errno + • functools + • io + • os + • random + • shutil + • sys + • types + • warnings + • weakref + +
+ + +
+ +
+ + termios +MissingModule
+imported by: + getpass + • tty + +
+ +
+ +
+ + textwrap +SourceModule
+imports: + re + +
+ + +
+ +
+ + threading +SourceModule
+imports: + _collections + • _thread + • _threading_local + • _weakrefset + • collections + • functools + • itertools + • os + • sys + • time + • traceback + • warnings + +
+ + +
+ +
+ + threadpoolctl +MissingModule
+imported by: + numpy.lib._utils_impl + +
+ +
+ +
+ + time (builtin module)
+imports: + _strptime + +
+ + +
+ +
+ + tkinter +Package
+imports: + _tkinter + • collections + • enum + • os + • re + • sys + • tkinter.commondialog + • tkinter.constants + • tkinter.filedialog + • tkinter.messagebox + • traceback + • types + +
+ + +
+ +
+ + tkinter.commondialog +SourceModule
+imports: + tkinter + +
+
+imported by: + tkinter + • tkinter.filedialog + • tkinter.messagebox + +
+ +
+ +
+ + tkinter.constants +SourceModule
+imports: + tkinter + +
+
+imported by: + tkinter + +
+ +
+ +
+ + tkinter.dialog +SourceModule
+imports: + tkinter + +
+
+imported by: + tkinter.filedialog + +
+ +
+ +
+ + tkinter.filedialog +SourceModule
+imports: + fnmatch + • locale + • os + • tkinter + • tkinter.commondialog + • tkinter.dialog + • tkinter.simpledialog + +
+
+imported by: + MaskReviewer.py + • tkinter + +
+ +
+ +
+ + tkinter.messagebox +SourceModule
+imports: + tkinter + • tkinter.commondialog + +
+
+imported by: + MaskReviewer.py + • tkinter + • tkinter.simpledialog + +
+ +
+ +
+ + tkinter.simpledialog +SourceModule
+imports: + tkinter + • tkinter.messagebox + +
+
+imported by: + tkinter.filedialog + +
+ +
+ +
+ + token +SourceModule
+imported by: + inspect + • pdb + • tokenize + +
+ +
+ +
+ + tokenize +SourceModule
+imports: + _tokenize + • argparse + • builtins + • codecs + • collections + • functools + • io + • itertools + • re + • sys + • token + +
+
+imported by: + importlib._bootstrap_external + • inspect + • linecache + • numpy.lib._format_impl + • pdb + • pydoc + +
+ +
+ +
+ + traceback +SourceModule
+imports: + ast + • collections.abc + • contextlib + • itertools + • linecache + • sys + • textwrap + • unicodedata + +
+ + +
+ +
+ + tracemalloc +SourceModule
+imports: + _tracemalloc + • collections.abc + • fnmatch + • functools + • linecache + • os.path + • pickle + +
+
+imported by: + warnings + +
+ +
+ +
+ + tty +SourceModule
+imports: + termios + +
+
+imported by: + pydoc + +
+ +
+ +
+ + types +SourceModule
+imports: + _collections_abc + • functools + • sys + +
+ + +
+ +
+ + typing +SourceModule
+imports: + _typing + • abc + • collections + • collections.abc + • contextlib + • copyreg + • functools + • inspect + • operator + • re + • sys + • types + • warnings + +
+ + +
+ +
+ + typing_extensions +MissingModule + +
+ +
+ + unicodedata C:\Python312\DLLs\unicodedata.pyd
+imported by: + encodings.idna + • re._parser + • stringprep + • traceback + • urllib.parse + +
+ +
+ +
+ + unittest +Package + + +
+ +
+ + unittest._log +SourceModule
+imports: + collections + • logging + • unittest + • unittest.case + +
+
+imported by: + unittest.case + +
+ +
+ +
+ + unittest.async_case +SourceModule
+imports: + asyncio + • contextvars + • inspect + • unittest + • unittest.case + • warnings + +
+
+imported by: + unittest + +
+ +
+ +
+ + unittest.case +SourceModule
+imports: + collections + • contextlib + • difflib + • functools + • pprint + • re + • sys + • time + • traceback + • types + • unittest + • unittest._log + • unittest.result + • unittest.util + • warnings + +
+ + +
+ +
+ + unittest.loader +SourceModule
+imports: + fnmatch + • functools + • os + • re + • sys + • traceback + • types + • unittest + • unittest.case + • unittest.suite + • unittest.util + • warnings + +
+
+imported by: + unittest + • unittest.main + +
+ +
+ +
+ + unittest.main +SourceModule
+imports: + argparse + • os + • sys + • unittest + • unittest.loader + • unittest.runner + • unittest.signals + • warnings + +
+
+imported by: + unittest + +
+ +
+ +
+ + unittest.result +SourceModule
+imports: + functools + • io + • sys + • traceback + • unittest + • unittest.util + +
+
+imported by: + unittest + • unittest.case + • unittest.runner + +
+ +
+ +
+ + unittest.runner +SourceModule
+imports: + sys + • time + • unittest + • unittest.case + • unittest.result + • unittest.signals + • warnings + +
+
+imported by: + unittest + • unittest.main + +
+ +
+ +
+ + unittest.signals +SourceModule
+imports: + functools + • signal + • unittest + • weakref + +
+
+imported by: + unittest + • unittest.main + • unittest.runner + +
+ +
+ +
+ + unittest.suite +SourceModule
+imports: + sys + • unittest + • unittest.case + • unittest.util + +
+
+imported by: + unittest + • unittest.loader + +
+ +
+ +
+ + unittest.util +SourceModule
+imports: + collections + • os.path + • unittest + +
+
+imported by: + unittest + • unittest.case + • unittest.loader + • unittest.result + • unittest.suite + +
+ +
+ +
+ + urllib +Package + +
+ +
+ + urllib.error +SourceModule
+imports: + io + • urllib + • urllib.response + +
+
+imported by: + numpy.lib._datasource + • urllib.request + +
+ +
+ +
+ + urllib.parse +SourceModule
+imports: + collections + • functools + • ipaddress + • math + • re + • types + • unicodedata + • urllib + • warnings + +
+
+imported by: + email.utils + • http.client + • http.cookiejar + • http.server + • mimetypes + • nturl2path + • numpy.lib._datasource + • pathlib + • pydoc + • urllib.request + • xml.sax.saxutils + • xmlrpc.client + +
+ +
+ +
+ + urllib.request +SourceModule
+imports: + _scproxy + • base64 + • bisect + • contextlib + • email + • email.utils + • fnmatch + • ftplib + • getpass + • hashlib + • http.client + • http.cookiejar + • io + • mimetypes + • nturl2path + • os + • re + • socket + • ssl + • string + • sys + • tempfile + • time + • urllib + • urllib.error + • urllib.parse + • urllib.response + • warnings + • winreg + +
+
+imported by: + http.cookiejar + • numpy.lib._datasource + • xml.sax.saxutils + +
+ +
+ +
+ + urllib.response +SourceModule
+imports: + tempfile + • urllib + +
+
+imported by: + urllib.error + • urllib.request + +
+ +
+ +
+ + vms_lib +MissingModule
+imported by: + platform + +
+ +
+ +
+ + warnings +SourceModule
+imports: + _warnings + • builtins + • linecache + • re + • sys + • traceback + • tracemalloc + +
+
+imported by: + MaskReviewer.py + • _collections_abc + • _pydatetime + • argparse + • ast + • asyncio.base_events + • asyncio.base_subprocess + • asyncio.events + • asyncio.proactor_events + • asyncio.selector_events + • asyncio.sslproto + • asyncio.streams + • asyncio.tasks + • asyncio.unix_events + • asyncio.windows_utils + • calendar + • codeop + • email.utils + • enum + • fileinput + • getpass + • gettext + • gzip + • hmac + • http.cookiejar + • importlib + • importlib.abc + • importlib.metadata + • importlib.metadata._adapters + • importlib.resources._common + • importlib.resources._legacy + • locale + • logging + • multiprocessing.forkserver + • multiprocessing.pool + • multiprocessing.resource_tracker + • numpy + • numpy.__config__ + • numpy._core._internal + • numpy._core._methods + • numpy._core.arrayprint + • numpy._core.function_base + • numpy._core.numeric + • numpy._core.records + • numpy._utils + • numpy.char + • numpy.core._utils + • numpy.f2py + • numpy.f2py.symbolic + • numpy.fft._pocketfft + • numpy.lib + • numpy.lib._format_impl + • numpy.lib._function_base_impl + • numpy.lib._histograms_impl + • numpy.lib._nanfunctions_impl + • numpy.lib._npyio_impl + • numpy.lib._polynomial_impl + • numpy.lib._twodim_base_impl + • numpy.lib._type_check_impl + • numpy.lib._ufunclike_impl + • numpy.linalg._linalg + • numpy.ma.core + • numpy.ma.extras + • numpy.ma.mrecords + • numpy.matlib + • numpy.matrixlib.defmatrix + • numpy.polynomial.polyutils + • numpy.testing._private.utils + • numpy.typing + • os + • pathlib + • pkgutil + • pydoc + • random + • re + • re._parser + • runpy + • shutil + • sre_compile + • sre_constants + • sre_parse + • ssl + • subprocess + • sysconfig + • tarfile + • tempfile + • threading + • typing + • unittest.async_case + • unittest.case + • unittest.loader + • unittest.main + • unittest.runner + • urllib.parse + • urllib.request + • webbrowser + • zipfile + +
+ +
+ +
+ + weakref +SourceModule
+imports: + _collections_abc + • _weakref + • _weakrefset + • atexit + • copy + • gc + • itertools + • sys + +
+ + +
+ +
+ + webbrowser +SourceModule
+imports: + copy + • getopt + • os + • shlex + • shutil + • subprocess + • sys + • threading + • warnings + +
+
+imported by: + pydoc + +
+ +
+ +
+ + win32pdh +MissingModule
+imported by: + numpy.testing._private.utils + +
+ +
+ +
+ + winreg (builtin module)
+imported by: + importlib._bootstrap_external + • mimetypes + • platform + • urllib.request + +
+ +
+ +
+ + xml +Package
+imports: + xml.sax.expatreader + • xml.sax.xmlreader + +
+
+imported by: + xml.parsers + • xml.sax + +
+ +
+ +
+ + xml.parsers +Package
+imports: + xml + • xml.parsers.expat + +
+
+imported by: + xml.parsers.expat + • xml.sax.expatreader + • xmlrpc.client + +
+ +
+ +
+ + xml.parsers.expat +SourceModule
+imports: + pyexpat + • sys + • xml.parsers + +
+
+imported by: + xml.parsers + • xml.sax.expatreader + • xmlrpc.client + +
+ +
+ +
+ + xml.sax +Package
+imports: + io + • os + • sys + • xml + • xml.sax + • xml.sax._exceptions + • xml.sax.expatreader + • xml.sax.handler + • xml.sax.saxutils + • xml.sax.xmlreader + +
+ + +
+ +
+ + xml.sax._exceptions +SourceModule
+imports: + xml.sax + +
+
+imported by: + xml.sax + • xml.sax.expatreader + • xml.sax.xmlreader + +
+ +
+ +
+ + xml.sax.expatreader +SourceModule +
+imported by: + xml + • xml.sax + +
+ +
+ +
+ + xml.sax.handler +SourceModule
+imports: + xml.sax + +
+
+imported by: + xml.sax + • xml.sax.expatreader + • xml.sax.saxutils + • xml.sax.xmlreader + +
+ +
+ +
+ + xml.sax.saxutils +SourceModule
+imports: + codecs + • io + • os + • sys + • urllib.parse + • urllib.request + • xml.sax + • xml.sax.handler + • xml.sax.xmlreader + +
+
+imported by: + xml.sax + • xml.sax.expatreader + • xml.sax.xmlreader + +
+ +
+ +
+ + xml.sax.xmlreader +SourceModule
+imports: + xml.sax + • xml.sax._exceptions + • xml.sax.handler + • xml.sax.saxutils + +
+
+imported by: + xml + • xml.sax + • xml.sax.expatreader + • xml.sax.saxutils + +
+ +
+ +
+ + xmlrpc +Package
+imported by: + xmlrpc.client + +
+ +
+ +
+ + xmlrpc.client +SourceModule
+imports: + base64 + • datetime + • decimal + • errno + • gzip + • http.client + • io + • sys + • time + • urllib.parse + • xml.parsers + • xml.parsers.expat + • xmlrpc + +
+
+imported by: + multiprocessing.connection + +
+ +
+ +
+ + yaml +MissingModule
+imported by: + numpy.__config__ + +
+ +
+ +
+ + zipfile +Package
+imports: + argparse + • binascii + • bz2 + • importlib.util + • io + • lzma + • os + • py_compile + • shutil + • stat + • struct + • sys + • threading + • time + • warnings + • zipfile._path + • zlib + +
+ + +
+ +
+ + zipfile._path +Package
+imports: + contextlib + • io + • itertools + • pathlib + • posixpath + • re + • zipfile + • zipfile._path.glob + +
+
+imported by: + zipfile + • zipfile._path.glob + +
+ +
+ +
+ + zipfile._path.glob +SourceModule
+imports: + re + • zipfile._path + +
+
+imported by: + zipfile._path + +
+ +
+ +
+ + zipimport +SourceModule
+imports: + _frozen_importlib + • _frozen_importlib_external + • _imp + • _io + • _warnings + • importlib.readers + • marshal + • sys + • time + • zlib + +
+
+imported by: + pkgutil + +
+ +
+ +
+ + zlib (builtin module)
+imported by: + encodings.zlib_codec + • gzip + • shutil + • tarfile + • zipfile + • zipimport + +
+ +
+ + + diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build_windows.bat b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build_windows.bat new file mode 100644 index 000000000..69112babf --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/build_windows.bat @@ -0,0 +1,81 @@ +@echo off +setlocal +cd /d "%~dp0" + +echo ============================================================ +echo BUILD MASKREVIEWER WINDOWS +echo ============================================================ +echo. + +where py >nul 2>nul +if %errorlevel%==0 ( + set PY=py +) else ( + set PY=python +) + +%PY% --version +if errorlevel 1 ( + echo. + echo [ERRO] Python nao encontrado. + echo Instale Python 3.11 ou 3.12 para Windows e tente novamente. + pause + exit /b 1 +) + +if not exist ".build_venv\Scripts\python.exe" ( + echo [1/4] Criando ambiente de build... + %PY% -m venv .build_venv + if errorlevel 1 goto :erro +) + +call ".build_venv\Scripts\activate.bat" + +echo [2/4] Instalando dependencias de build... +python -m pip install --upgrade pip +python -m pip install pyinstaller numpy opencv-python +if errorlevel 1 goto :erro + +echo [3/4] Limpando builds antigos... +if exist build rmdir /s /q build +if exist dist rmdir /s /q dist +if exist MaskReviewer.spec del /q MaskReviewer.spec + +echo [4/4] Gerando EXE... +python -m PyInstaller ^ + --noconfirm ^ + --clean ^ + --onedir ^ + --windowed ^ + --name MaskReviewer ^ + --collect-all cv2 ^ + MaskReviewer.py + +if errorlevel 1 goto :erro + +echo. +echo ============================================================ +echo BUILD CONCLUIDO +echo ============================================================ +echo Pasta gerada: +echo %CD%\dist\MaskReviewer +echo. +echo Coloque ao lado do MaskReviewer.exe: +echo labelmap.txt +echo group\ +echo. +echo Estrutura final recomendada: +echo MaskReviewer\ +echo MaskReviewer.exe +echo _internal\ +echo labelmap.txt +echo group\ +echo. +pause +exit /b 0 + +:erro +echo. +echo [ERRO] Falha no build. +pause +exit /b 1 diff --git a/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/executar_sem_exe.bat b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/executar_sem_exe.bat new file mode 100644 index 000000000..1c2f6fc31 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/MaskReviewer_package/executar_sem_exe.bat @@ -0,0 +1,19 @@ +@echo off +setlocal +cd /d "%~dp0" + +where py >nul 2>nul +if %errorlevel%==0 ( + set PY=py +) else ( + set PY=python +) + +%PY% -c "import cv2, numpy" >nul 2>nul +if errorlevel 1 ( + echo Instalando numpy e opencv-python... + %PY% -m pip install numpy opencv-python +) + +%PY% MaskReviewer.py +if errorlevel 1 pause diff --git a/Python/OAK/datasets/oak-fcc-3/_10_export_onnx.py b/Python/OAK/datasets/oak-fcc-3/_10_export_onnx.py index d668adce8..f0de57b22 100644 --- a/Python/OAK/datasets/oak-fcc-3/_10_export_onnx.py +++ b/Python/OAK/datasets/oak-fcc-3/_10_export_onnx.py @@ -8,7 +8,8 @@ Exporta o checkpoint PyTorch do SegFormer Multi-Head OAK-FCC-3 para ONNX. Exemplo: -python _10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.pt --out backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.onnx --include-norm --postprocess argmax_fullres +python .\_10_export_onnx.py --config config.json --checkpoint backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.pt --out backup/segformer_b1/2026_08_17/stacked_raw5_ndvi_ndre/best_score.onnx --include-norm --postprocess argmax_fullres +python .\_10_export_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --opset 17 --device cuda --include-norm --postprocess argmax_fullres """ from __future__ import annotations diff --git a/Python/OAK/datasets/oak-fcc-3/_11_validate_onnx.py b/Python/OAK/datasets/oak-fcc-3/_11_validate_onnx.py index e376f6ae8..4ca57cd2d 100644 --- a/Python/OAK/datasets/oak-fcc-3/_11_validate_onnx.py +++ b/Python/OAK/datasets/oak-fcc-3/_11_validate_onnx.py @@ -13,6 +13,7 @@ para o SegFormer OAK-FCC-3 Multi-Head. Exemplo: python _11_validate_onnx.py --config config.json --max_samples 20 --device cuda --onnx_provider cuda --torch_no_amp +python .\_11_validate_onnx.py --config .\config.json --train-script .\_8_train_multihead_v2.py --max_samples 50 --device cuda --onnx_provider cuda --torch_no_amp Para validar o ONNX com saída já redimensionada: diff --git a/Python/OAK/datasets/oak-fcc-3/_12_benchmark_onnx_v2.py b/Python/OAK/datasets/oak-fcc-3/_12_benchmark_onnx_v2.py new file mode 100644 index 000000000..dabe02c15 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/_12_benchmark_onnx_v2.py @@ -0,0 +1,1191 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +_12_benchmark_onnx.py + +Benchmark PyTorch vs ONNX Runtime para SegFormer OAK-FCC-3 Multi-Head. + +Mede: + - PyTorch FP32 + - PyTorch AMP/FP16 + - ONNX Runtime CUDA ou CPU + +Exemplos: + +Benchmark ONNX cru 160x256: + +python _12_benchmark_onnx_v2.py --config config.json --train-script .\_8_train_multihead_v2.py --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider cuda + +Benchmark ONNX resized 640x1024: + +python _12_benchmark_onnx_v2.py --config config.json --train-script .\_8_train_multihead_v2.py --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider cuda + + +TensorRT +python .\_12_benchmark_onnx_v2.py --config .\config.json --train-script .\_8_train_multihead_v2.py --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider tensorrt --skip_torch_fp32 --skip_torch_amp + +""" + +from __future__ import annotations + +import gc +import csv +import json +import time +import argparse +import importlib.util +import inspect +from pathlib import Path +from typing import Optional, List, Dict, Sequence, Tuple + +import cv2 +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + + +SOURCE_CHANNEL_ORDER = ["R", "G", "B", "RE", "NIR"] +DERIVED_CHANNEL_ORDER = ["NDVI", "NDRE"] +SUPPORTED_INPUT_CHANNELS = SOURCE_CHANNEL_ORDER + DERIVED_CHANNEL_ORDER + + +# ============================================================ +# Utils +# ============================================================ + +def load_json(path: str | Path) -> dict: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def save_json(path: str | Path, data: dict): + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + +def save_csv(path: str | Path, rows: List[dict]): + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + + if not rows: + return + + keys = list(rows[0].keys()) + + with path.open("w", newline="", encoding="utf-8") as f: + w = csv.DictWriter(f, fieldnames=keys) + w.writeheader() + w.writerows(rows) + + +def resolve_path(path_like: Optional[str], base: Optional[Path] = None) -> Optional[Path]: + if path_like is None: + return None + + p = Path(path_like) + if p.is_absolute(): + return p + + if base is None: + base = Path.cwd() + + return (base / p).resolve() + + +def import_train_module(train_script_path: str | Path): + train_script_path = Path(train_script_path) + + if not train_script_path.exists(): + raise FileNotFoundError(f"Script de treino não encontrado: {train_script_path}") + + spec = importlib.util.spec_from_file_location( + "train_multihead_module", + str(train_script_path.resolve()) + ) + + if spec is None or spec.loader is None: + raise RuntimeError(f"Não consegui importar o script: {train_script_path}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def synchronize_if_cuda(device: torch.device): + if device.type == "cuda": + torch.cuda.synchronize() + + +def clear_cuda(): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.synchronize() + + +def percentile(values: List[float], p: float) -> float: + if not values: + return 0.0 + return float(np.percentile(np.asarray(values, dtype=np.float64), p)) + + +def summarize_times(times_ms: List[float]) -> dict: + arr = np.asarray(times_ms, dtype=np.float64) + + if arr.size == 0: + return { + "n": 0, + "mean_ms": 0.0, + "median_ms": 0.0, + "min_ms": 0.0, + "max_ms": 0.0, + "p95_ms": 0.0, + "p99_ms": 0.0, + "fps_mean": 0.0, + "fps_p95_latency": 0.0, + } + + mean_ms = float(arr.mean()) + p95_ms = float(np.percentile(arr, 95)) + p99_ms = float(np.percentile(arr, 99)) + + return { + "n": int(arr.size), + "mean_ms": mean_ms, + "median_ms": float(np.median(arr)), + "min_ms": float(arr.min()), + "max_ms": float(arr.max()), + "p95_ms": p95_ms, + "p99_ms": p99_ms, + "fps_mean": float(1000.0 / mean_ms) if mean_ms > 0 else 0.0, + "fps_p95_latency": float(1000.0 / p95_ms) if p95_ms > 0 else 0.0, + } + + +def experiment_tag(config: dict, channels: int) -> str: + explicit = str(config.get("stats_source_tag", "")).strip() + if explicit: + return explicit + return f"{config.get('fusion_mode', 'stacked')}_raw{channels}" + + +def load_export_metadata(onnx_path: Path) -> Tuple[dict, Optional[Path]]: + meta_path = onnx_path.with_suffix(".export_meta.json") + if meta_path.is_file(): + return load_json(meta_path), meta_path + return {}, None + + +def resolve_model_artifact_paths( + args, + config: dict, + config_dir: Path, + channels: int, + require_onnx: bool = True, +) -> Tuple[Path, Path, str]: + """ + Resolve checkpoint e ONNX. + + Se --checkpoint ou --onnx forem informados, usa os caminhos informados. + Se ficarem vazios, monta a partir do config: + + backup/{modelo}/{model_name}/{stats_source_tag}/{ckpt_name}.pt + backup/{modelo}/{model_name}/{stats_source_tag}/{ckpt_name}.onnx + + ckpt_name vem de: + config["ckpt_test"] ou "best_score" + """ + model = config.get("modelo", "segformer_b1") + model_name = config.get("model_name", "target_teached") + ckpt_name = config.get("ckpt_test", "best_score") + base_dir = config_dir / "backup" / model / model_name / experiment_tag(config, channels) + + ckpt_filename = str(ckpt_name) + if not ckpt_filename.lower().endswith(".pt"): + ckpt_filename += ".pt" + onnx_filename = f"{Path(str(ckpt_name)).stem}.onnx" + + if args.checkpoint: + checkpoint_path = resolve_path(args.checkpoint, config_dir) + else: + checkpoint_path = base_dir / ckpt_filename + + if args.onnx: + onnx_path = resolve_path(args.onnx, config_dir) + else: + onnx_path = base_dir / onnx_filename + + if checkpoint_path is None or not checkpoint_path.is_file(): + raise FileNotFoundError( + f"Checkpoint não encontrado: {checkpoint_path}\n" + f"Dica: informe --checkpoint ou ajuste config['ckpt_test']." + ) + + if require_onnx and (onnx_path is None or not onnx_path.is_file()): + raise FileNotFoundError( + f"ONNX não encontrado: {onnx_path}\n" + f"Dica: exporte o checkpoint, informe --onnx, ou use --skip_onnx para benchmark só PyTorch." + ) + + # Mesmo em --skip_onnx retornamos o caminho esperado, ainda que o arquivo não exista. + # Isso mantém nomes de relatório e diretório do experimento previsíveis. + return checkpoint_path.resolve(), onnx_path.resolve(), str(ckpt_name) + + +# ============================================================ +# Dataset / normalização +# ============================================================ + +def collect_tensor_samples(root: Path, max_samples: int = 50, start_idx: int = 0) -> List[Path]: + tensor_paths: List[Path] = [] + + direct = root / "tensors" + if direct.is_dir(): + tensor_paths.extend(sorted(direct.glob("*.npy"))) + + group_root = root / "group" + if group_root.is_dir(): + for gdir in sorted(group_root.iterdir()): + tdir = gdir / "tensors" + if tdir.is_dir(): + tensor_paths.extend(sorted(tdir.glob("*.npy"))) + + if not tensor_paths: + tensor_paths.extend(sorted(root.glob("**/tensors/*.npy"))) + + if not tensor_paths: + raise RuntimeError(f"Nenhum tensor .npy encontrado em: {root}") + + start_idx = max(0, int(start_idx)) + selected = tensor_paths[start_idx:] + + if max_samples > 0: + selected = selected[:int(max_samples)] + + return selected + + +def load_tensor( + path: Path, + requested_channels: Sequence[str], + saved_channel_names: Optional[Sequence[str]] = None, +) -> np.ndarray: + arr = np.load(str(path)).astype(np.float32) + + if arr.ndim != 3: + raise RuntimeError(f"Tensor inválido {path}: shape={arr.shape}, esperado 3D") + + if 1 <= arr.shape[0] <= len(SUPPORTED_INPUT_CHANNELS) and arr.shape[1] > 8 and arr.shape[2] > 8: + chw = arr + elif 1 <= arr.shape[-1] <= len(SUPPORTED_INPUT_CHANNELS) and arr.shape[0] > 8 and arr.shape[1] > 8: + chw = np.transpose(arr, (2, 0, 1)) + else: + raise RuntimeError(f"Tensor com layout inesperado: {path} shape={arr.shape}") + + requested = [str(x).strip().upper() for x in requested_channels] + saved = [str(x).strip().upper() for x in (saved_channel_names or [])] + + if saved: + if len(saved) != chw.shape[0] or len(set(saved)) != len(saved): + raise RuntimeError( + f"Meta/tensor incompatíveis em {path}: channels={saved}, shape={chw.shape}" + ) + missing = [name for name in requested if name not in saved] + if missing: + raise RuntimeError(f"Tensor {path} não contém {missing}. Disponíveis={saved}") + indices = [saved.index(name) for name in requested] + elif chw.shape[0] == len(requested): + indices = list(range(len(requested))) + print(f"[WARN] {path.name}: sem meta.channels; assumindo ordem {requested}") + elif chw.shape[0] == len(SOURCE_CHANNEL_ORDER) and all(x in SOURCE_CHANNEL_ORDER for x in requested): + indices = [SOURCE_CHANNEL_ORDER.index(name) for name in requested] + print(f"[WARN] {path.name}: Raw5 legado; usando ordem {SOURCE_CHANNEL_ORDER}") + else: + raise RuntimeError( + f"Não é seguro inferir canais de {path}: shape={chw.shape}, pedidos={requested}" + ) + + chw = chw[indices, :, :] + + finite = np.isfinite(chw) + if finite.any(): + mx = float(np.nanmax(chw[finite])) + if mx > 2.0 and mx <= 255.0: + chw = chw / 255.0 + elif mx > 255.0: + chw = chw / 65535.0 + + chw = np.nan_to_num(chw, nan=0.0, posinf=1.0, neginf=-1.0) + return np.ascontiguousarray(chw, dtype=np.float32) + + +def load_tensor_channel_names(tensor_path: Path) -> List[str]: + meta_path = tensor_path.parent.parent / "metas" / f"{tensor_path.stem}.json" + if not meta_path.is_file(): + return [] + meta = load_json(meta_path) + return list(meta.get("channels") or meta.get("input_channels") or []) + + +def find_norm_stats(config: dict, config_dir: Path, save_dir: Path, explicit: Optional[str]) -> Optional[Path]: + if explicit: + return resolve_path(explicit, config_dir) + + W, H = config.get("resolucao", [1024, 640]) + dataset_path = config_dir / "dataset" + + candidates = [ + save_dir / "norm_stats.json", + dataset_path / f"{int(W)}x{int(H)}" / "group" / "norm_stats.json", + config_dir / "backup" / config.get("modelo", "segformer_b1") / config.get("model_name", "test") / config.get("stats_source_tag", "stacked_raw5") / "norm_stats.json", + ] + + for p in candidates: + if p.is_file(): + return p + + return candidates[0] + + +def load_norm_stats( + path: Optional[Path], + channel_names: Sequence[str], +) -> Tuple[Optional[List[float]], Optional[List[float]], Optional[str]]: + if path is None or not path.is_file(): + raise FileNotFoundError( + f"norm_stats não encontrado: {path}. Benchmark exige os stats do treinamento." + ) + + js = load_json(path) + mean = js.get("mean") + std = js.get("std") + names = [str(x).strip().upper() for x in js.get("channels", [])] + requested = [str(x).strip().upper() for x in channel_names] + + if mean is None or std is None: + raise RuntimeError(f"norm_stats inválido, faltando mean/std: {path}") + + if len(mean) != len(std): + raise RuntimeError(f"norm_stats inválido: mean={len(mean)} std={len(std)}") + + if names: + if len(names) != len(mean) or len(set(names)) != len(names): + raise RuntimeError(f"norm_stats channels inválido: {names}") + missing = [name for name in requested if name not in names] + if missing: + raise RuntimeError( + f"norm_stats não contém {missing}. Disponíveis={names}, pedidos={requested}" + ) + indices = [names.index(name) for name in requested] + elif len(mean) == len(requested): + indices = list(range(len(requested))) + print(f"[NORM][WARN] stats sem nomes; assumindo ordem {requested}") + elif len(mean) == len(SOURCE_CHANNEL_ORDER) and all(x in SOURCE_CHANNEL_ORDER for x in requested): + indices = [SOURCE_CHANNEL_ORDER.index(name) for name in requested] + print(f"[NORM][WARN] stats Raw5 legados; usando ordem {SOURCE_CHANNEL_ORDER}") + else: + raise RuntimeError( + f"Não é seguro mapear norm_stats sem nomes: mean={len(mean)}, pedidos={requested}" + ) + + mean_sel = [float(mean[i]) for i in indices] + std_sel = [float(std[i]) for i in indices] + if not np.all(np.isfinite(mean_sel)) or not np.all(np.isfinite(std_sel)): + raise RuntimeError(f"norm_stats possui NaN/Inf: mean={mean_sel} std={std_sel}") + if any(x <= 0.0 for x in std_sel): + raise RuntimeError(f"norm_stats possui std inválido: {std_sel}") + + print(f"[NORM] usando {path}") + print(f"[NORM] channels={requested}") + print(f"[NORM] mean={mean_sel}") + print(f"[NORM] std ={std_sel}") + + return mean_sel, std_sel, str(path) + + +def normalize_numpy_chw(chw: np.ndarray, mean: Optional[List[float]], std: Optional[List[float]]) -> np.ndarray: + if mean is None or std is None: + return chw.astype(np.float32) + + mean_np = np.asarray(mean, dtype=np.float32).reshape(-1, 1, 1) + std_np = np.asarray(std, dtype=np.float32).reshape(-1, 1, 1) + std_np = np.clip(std_np, 1e-6, None) + + return ((chw.astype(np.float32) - mean_np) / std_np).astype(np.float32) + + +def load_inputs_as_numpy( + samples: List[Path], + input_channel_names: Sequence[str], + mean: Optional[List[float]], + std: Optional[List[float]], + target_hw: Tuple[int, int], + normalize_input: bool = True, +) -> List[np.ndarray]: + H, W = target_hw + xs = [] + + for p in samples: + chw01 = load_tensor( + p, + requested_channels=input_channel_names, + saved_channel_names=load_tensor_channel_names(p), + ) + + if chw01.shape[-2:] != (H, W): + hwc = np.transpose(chw01, (1, 2, 0)) + hwc = cv2.resize(hwc, (W, H), interpolation=cv2.INTER_LINEAR) + chw01 = np.transpose(hwc, (2, 0, 1)).astype(np.float32) + + if normalize_input: + chw = normalize_numpy_chw(chw01, mean=mean, std=std) + else: + chw = chw01.astype(np.float32, copy=False) + + x = np.expand_dims(chw, axis=0).astype(np.float32) + xs.append(x) + + return xs + + +# ============================================================ +# PyTorch +# ============================================================ + +class TorchTupleWrapper(nn.Module): + def __init__( + self, + model: nn.Module, + output_heads: List[str], + postprocess: str = "none", + include_norm: bool = False, + norm_mean: Optional[Sequence[float]] = None, + norm_std: Optional[Sequence[float]] = None, + ): + super().__init__() + self.model = model + self.output_heads = list(output_heads) + self.postprocess = str(postprocess).lower() + self.include_norm = bool(include_norm) + if self.postprocess not in ("none", "resize_logits", "argmax_lowres", "argmax_fullres"): + raise RuntimeError(f"postprocess inválido: {self.postprocess}") + if self.include_norm: + if norm_mean is None or norm_std is None: + raise RuntimeError("include_norm=True requer norm_mean/norm_std") + self.register_buffer( + "norm_mean", + torch.tensor(norm_mean, dtype=torch.float32).view(1, -1, 1, 1), + ) + self.register_buffer( + "norm_std", + torch.tensor(norm_std, dtype=torch.float32).view(1, -1, 1, 1), + ) + else: + self.register_buffer("norm_mean", torch.empty(0)) + self.register_buffer("norm_std", torch.empty(0)) + + def forward(self, pixel_values: torch.Tensor): + x = pixel_values + if self.include_norm: + x = (x - self.norm_mean) / torch.clamp(self.norm_std, min=1e-6) + outputs = self.model(pixel_values=x) + input_hw = pixel_values.shape[-2:] + result = [] + + for head in self.output_heads: + value = outputs[head] + if self.postprocess in ("resize_logits", "argmax_fullres"): + value = F.interpolate( + value, + size=input_hw, + mode="bilinear", + align_corners=False, + ) + if self.postprocess in ("argmax_lowres", "argmax_fullres"): + value = torch.argmax(value, dim=1).to(torch.uint8) + result.append(value) + + return tuple(result) + + +@torch.inference_mode() +def benchmark_torch( + model: nn.Module, + inputs_np: List[np.ndarray], + device: torch.device, + warmup: int, + repeat: int, + amp: bool, + label: str, +) -> Tuple[dict, List[dict]]: + model.eval() + + times = [] + rows = [] + + # Precarrega tensors na GPU para medir só inferência do modelo. + inputs_t = [ + torch.from_numpy(x).to(device, non_blocking=True) + for x in inputs_np + ] + + if device.type == "cuda": + torch.cuda.synchronize() + + print(f"\n[BENCH] {label} | warmup={warmup} repeat={repeat}") + + # Warmup + for i in range(max(0, warmup)): + x = inputs_t[i % len(inputs_t)] + with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=amp and device.type == "cuda"): + _ = model(x) + + synchronize_if_cuda(device) + + # Medição + total_iter = len(inputs_t) * max(1, repeat) + idx = 0 + + for r in range(max(1, repeat)): + for sample_idx, x in enumerate(inputs_t): + synchronize_if_cuda(device) + t0 = time.perf_counter() + + with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=amp and device.type == "cuda"): + _ = model(x) + + synchronize_if_cuda(device) + dt_ms = (time.perf_counter() - t0) * 1000.0 + + times.append(dt_ms) + rows.append({ + "engine": label, + "repeat": r, + "sample_idx": sample_idx, + "iter_idx": idx, + "latency_ms": dt_ms, + }) + + idx += 1 + + if idx % 25 == 0 or idx == total_iter: + print(f" {idx:04d}/{total_iter:04d} | last={dt_ms:.2f}ms") + + summary = summarize_times(times) + summary["engine"] = label + summary["timing_scope"] = "model_forward_and_export_postprocess_device_resident" + + return summary, rows + + +# ============================================================ +# ONNX Runtime +# ============================================================ + +def create_onnx_session( + onnx_path: Path, + provider: str, + trt_home: Optional[str] = None, + trt_fp16: bool = True, +): + import os + + try: + import onnxruntime as ort + except ImportError: + raise ImportError( + "onnxruntime não está instalado. Instale com:\n" + " pip install onnxruntime-gpu\n" + "ou CPU:\n" + " pip install onnxruntime" + ) + + available = ort.get_available_providers() + print(f"[ONNX] providers disponíveis: {available}") + + provider = provider.lower() + + if provider == "tensorrt": + trt_home = trt_home or os.environ.get("TRT_HOME", r"C:\dev\TensorRT-10.10.0.31") + dll_dirs = [ + os.path.join(trt_home, "lib"), + os.path.join(trt_home, "bin"), + ] + cuda_home = os.environ.get("CUDA_PATH") + if cuda_home: + dll_dirs.append(os.path.join(cuda_home, "bin")) + dll_dirs.append(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\bin") + + for dll_dir in dll_dirs: + if os.path.isdir(dll_dir): + try: + os.add_dll_directory(dll_dir) + print(f"[DLL] add_dll_directory: {dll_dir}") + except Exception as e: + print(f"[DLL][WARN] falha em {dll_dir}: {e}") + + sess_options = ort.SessionOptions() + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + + if provider == "cuda": + providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] + + elif provider == "cpu": + providers = ["CPUExecutionProvider"] + + elif provider == "tensorrt": + cache_dir = onnx_path.parent / "trt_cache" + cache_dir.mkdir(parents=True, exist_ok=True) + + trt_options = { + "device_id": 0, + + # FP16: o ponto principal do nosso teste. + "trt_fp16_enable": bool(trt_fp16), + + # Cache: evita rebuild do engine a cada execução. + "trt_engine_cache_enable": True, + "trt_engine_cache_path": str(cache_dir), + + # Timing cache ajuda a acelerar builds futuros. + "trt_timing_cache_enable": True, + "trt_timing_cache_path": str(cache_dir), + + # Workspace. 4GB é razoável para RTX 3070, ajuste se faltar VRAM. + "trt_max_workspace_size": 4 * 1024 * 1024 * 1024, + } + + providers = [ + ("TensorrtExecutionProvider", trt_options), + "CUDAExecutionProvider", + "CPUExecutionProvider", + ] + + else: + providers = [provider] + + # Checagem de disponibilidade, lidando com provider tuple. + requested_names = [ + p[0] if isinstance(p, tuple) else p + for p in providers + ] + + providers_ok = [ + p for p in providers + if (p[0] if isinstance(p, tuple) else p) in available + ] + + if not providers_ok: + raise RuntimeError( + f"Nenhum provider solicitado está disponível. " + f"Solicitado={requested_names}, disponível={available}" + ) + + session = ort.InferenceSession( + str(onnx_path), + sess_options=sess_options, + providers=providers_ok, + ) + + print(f"[ONNX] usando providers: {session.get_providers()}") + + active_providers = session.get_providers() + + if provider == "tensorrt" and "TensorrtExecutionProvider" not in active_providers: + raise RuntimeError( + "TensorRTExecutionProvider foi solicitado, mas não ficou ativo. " + f"Providers ativos: {active_providers}. " + "Provável causa: TensorRT não instalado, DLLs fora do PATH, " + "ou versão incompatível com onnxruntime-gpu." + ) + + if provider == "cuda" and "CUDAExecutionProvider" not in active_providers: + raise RuntimeError( + "CUDAExecutionProvider foi solicitado, mas não ficou ativo. " + f"Providers ativos: {active_providers}." + ) + + return session + + +def benchmark_onnx( + session, + inputs_np: List[np.ndarray], + warmup: int, + repeat: int, + label: str, +) -> Tuple[dict, List[dict]]: + input_name = session.get_inputs()[0].name + + times = [] + rows = [] + + print(f"\n[BENCH] {label} | warmup={warmup} repeat={repeat}") + + # Warmup + for i in range(max(0, warmup)): + x = inputs_np[i % len(inputs_np)] + _ = session.run(None, {input_name: x}) + + # Medição + total_iter = len(inputs_np) * max(1, repeat) + idx = 0 + + for r in range(max(1, repeat)): + for sample_idx, x in enumerate(inputs_np): + t0 = time.perf_counter() + _ = session.run(None, {input_name: x}) + dt_ms = (time.perf_counter() - t0) * 1000.0 + + times.append(dt_ms) + rows.append({ + "engine": label, + "repeat": r, + "sample_idx": sample_idx, + "iter_idx": idx, + "latency_ms": dt_ms, + }) + + idx += 1 + + if idx % 25 == 0 or idx == total_iter: + print(f" {idx:04d}/{total_iter:04d} | last={dt_ms:.2f}ms") + + summary = summarize_times(times) + summary["engine"] = label + summary["timing_scope"] = "onnx_session_run_host_input_and_outputs" + + return summary, rows + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument("--config", default="config.json") + parser.add_argument("--checkpoint", default="") + parser.add_argument("--onnx", default="") + parser.add_argument("--train-script", default="_8_train_multihead_v2.py") + parser.add_argument("--labelmap", default="dataset/labelmap.txt") + + parser.add_argument("--split_folder", default="val", choices=["train", "val", "test"]) + parser.add_argument("--root_override", default=None) + parser.add_argument("--norm_stats", default=None) + + parser.add_argument("--max_samples", type=int, default=50) + parser.add_argument("--start_idx", type=int, default=0) + parser.add_argument("--warmup", type=int, default=10) + parser.add_argument("--repeat", type=int, default=5) + + parser.add_argument("--device", default="cuda", choices=["cuda", "cpu"]) + parser.add_argument("--onnx_provider", default="cuda", choices=["cuda", "cpu", "tensorrt"]) + parser.add_argument("--trt_home", default=None) + parser.add_argument("--trt_no_fp16", action="store_true") + + parser.add_argument("--skip_torch_fp32", action="store_true") + parser.add_argument("--skip_torch_amp", action="store_true") + parser.add_argument("--skip_onnx", action="store_true") + + parser.add_argument( + "--onnx_has_norm", + action="store_true", + help="Força ONNX com normalização interna; normalmente detectado pelo export_meta.", + ) + parser.add_argument( + "--onnx_no_norm", + action="store_true", + help="Força ONNX sem normalização interna; normalmente detectado pelo export_meta.", + ) + + parser.add_argument("--out_dir", default=None) + + args = parser.parse_args() + + if args.skip_torch_fp32 and args.skip_torch_amp and args.skip_onnx: + raise RuntimeError("Todos os engines foram desativados; não há o que medir.") + + config_path = resolve_path(args.config, Path.cwd()) + + if config_path is None or not config_path.is_file(): + raise FileNotFoundError(f"Config não encontrado: {config_path}") + config_dir = config_path.parent + config = load_json(config_path) + + train_script_path = resolve_path(args.train_script, config_dir) + labelmap_path = resolve_path(args.labelmap, config_dir) + if train_script_path is None or not train_script_path.is_file(): + raise FileNotFoundError(f"Train script não encontrado: {train_script_path}") + if labelmap_path is None or not labelmap_path.is_file(): + raise FileNotFoundError(f"Labelmap não encontrado: {labelmap_path}") + + train_mod = import_train_module(train_script_path) + trainer_version = str(getattr(train_mod, "TRAINER_VERSION", "legacy/unknown")) + print(f"[TRAIN MODULE] {train_script_path.name} | trainer_version={trainer_version}") + + W, H = config.get("resolucao", [1024, 640]) + W = int(W) + H = int(H) + + backbone = config.get("backbone", "nvidia/mit-b1") + input_channel_names = train_mod.get_input_channel_names(config) + input_channel_indices = train_mod.get_input_channel_indices(config) + channels = len(input_channel_names) + + checkpoint_path, onnx_path, ckpt_name = resolve_model_artifact_paths( + args=args, + config=config, + config_dir=config_dir, + channels=channels, + require_onnx=not args.skip_onnx, + ) + + export_meta, export_meta_path = load_export_metadata(onnx_path) if onnx_path.is_file() else ({}, None) + exported_names = [ + str(x).strip().upper() + for x in export_meta.get("input_channel_names", []) + ] + if exported_names and exported_names != input_channel_names: + raise RuntimeError( + f"ONNX incompatível: canais exportados={exported_names}, " + f"config atual={input_channel_names}." + ) + if args.onnx_has_norm and args.onnx_no_norm: + raise RuntimeError("Use apenas uma opção: --onnx_has_norm ou --onnx_no_norm.") + input_contract = export_meta.get("input_contract", {}) or {} + detected_onnx_has_norm = bool( + input_contract.get("normalization_embedded", export_meta.get("include_norm", False)) + ) + onnx_has_norm = ( + True if args.onnx_has_norm + else False if args.onnx_no_norm + else detected_onnx_has_norm + ) + exported_postprocess = str(export_meta.get("postprocess", "none")).lower() + if exported_postprocess == "none" and bool(export_meta.get("resize_to_input", False)): + exported_postprocess = "resize_logits" + if exported_postprocess not in ("none", "resize_logits", "argmax_lowres", "argmax_fullres"): + raise RuntimeError(f"postprocess inválido no export_meta: {exported_postprocess}") + + semantic_id2label, semantic_label2id, ignore_from_labelmap = train_mod.load_labelmap( + str(labelmap_path) + ) + + heads_config = train_mod.build_heads_config( + config, + ignore_index=int(ignore_from_labelmap) + ) + + heads_config["semantic"]["num_classes"] = int(len(semantic_id2label)) + heads_config["semantic"]["ignore_index"] = int(ignore_from_labelmap) + + output_heads = list(heads_config.keys()) + exported_heads = list(export_meta.get("heads", []) or []) + if exported_heads and exported_heads != output_heads: + raise RuntimeError( + f"Heads do ONNX={exported_heads} diferem do config/modelo={output_heads}." + ) + + save_dir = ( + config_dir + / "backup" + / config.get("modelo", "segformer_b1") + / config.get("model_name", "test") + / experiment_tag(config, channels) + ) + + norm_stats_path = find_norm_stats( + config=config, + config_dir=config_dir, + save_dir=save_dir, + explicit=args.norm_stats, + ) + + mean, std, norm_stats_used = load_norm_stats( + norm_stats_path, + channel_names=input_channel_names, + ) + + if args.root_override: + root = resolve_path(args.root_override, Path.cwd()) + else: + root = (config_dir / "dataset" / "split" / args.split_folder).resolve() + + if root is None or not root.is_dir(): + raise FileNotFoundError(f"Root de dados não encontrado: {root}") + + samples = collect_tensor_samples( + root=root, + max_samples=args.max_samples, + start_idx=args.start_idx, + ) + + if args.out_dir: + out_dir = resolve_path(args.out_dir, config_dir) + else: + out_dir = onnx_path.parent / "benchmarks" + + assert out_dir is not None + out_dir.mkdir(parents=True, exist_ok=True) + + use_cuda = args.device == "cuda" and torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + if args.device == "cuda" and not torch.cuda.is_available(): + print("[WARN] CUDA indisponível. Usando CPU no PyTorch.") + + print("==========================================") + print("Benchmark PyTorch vs ONNX") + print(f"Config : {config_path}") + print(f"Checkpoint : {checkpoint_path}") + print(f"ONNX : {onnx_path}") + print(f"Export meta : {export_meta_path or 'ausente (modo legado)'}") + print(f"Root : {root}") + print(f"Samples : {len(samples)}") + print(f"Warmup : {args.warmup}") + print(f"Repeat : {args.repeat}") + print(f"Backbone : {backbone}") + print(f"Input shape : [1, {channels}, {H}, {W}]") + print(f"Channels : {input_channel_names} idx={input_channel_indices}") + print(f"Heads : {output_heads}") + print(f"Device : {device}") + print(f"ONNX provider: {args.onnx_provider}") + print(f"ONNX has norm: {onnx_has_norm}") + print(f"Postprocess : {exported_postprocess}") + print(f"Out dir : {out_dir}") + print("==========================================") + print( + "[TIMING] PyTorch mede forward com entrada residente no device; " + "ONNX session.run inclui cópia da entrada/saídas do ORT. " + "Pré-processamento e leitura de disco ficam fora das duas medições." + ) + + run_torch_fp32 = not args.skip_torch_fp32 + run_torch_amp = not args.skip_torch_amp and device.type == "cuda" + if not args.skip_torch_amp and device.type != "cuda": + print("[WARN] PyTorch AMP/FP16 exige CUDA; medição AMP será pulada.") + need_torch = run_torch_fp32 or run_torch_amp + if not need_torch and args.skip_onnx: + raise RuntimeError("Nenhum engine executável restou para o benchmark.") + need_normalized_inputs = ( + (need_torch or not args.skip_onnx) and not onnx_has_norm + ) + + inputs_torch_np: List[np.ndarray] = [] + if need_normalized_inputs: + print("\n[DATA] Carregando inputs normalizados...") + inputs_torch_np = load_inputs_as_numpy( + samples=samples, + input_channel_names=input_channel_names, + mean=mean, + std=std, + target_hw=(H, W), + normalize_input=True, + ) + + if (need_torch or not args.skip_onnx) and onnx_has_norm: + print("[DATA] Carregando inputs crus para ONNX com normalização embutida...") + inputs_onnx_np = load_inputs_as_numpy( + samples=samples, + input_channel_names=input_channel_names, + mean=mean, + std=std, + target_hw=(H, W), + normalize_input=False, + ) + else: + inputs_onnx_np = inputs_torch_np + if need_torch and onnx_has_norm: + inputs_torch_np = inputs_onnx_np + print(f"[DATA] Amostras preparadas: {len(samples)}") + + summaries = [] + all_rows = [] + + # ======================================================== + # PyTorch + # ======================================================== + if need_torch: + print("\n[MODEL] Montando PyTorch...") + build_kwargs = dict( + backbone=backbone, + input_channel_names=input_channel_names, + heads_config=heads_config, + semantic_id2label=semantic_id2label, + semantic_label2id=semantic_label2id, + ) + try: + if "config" in inspect.signature(train_mod.build_model).parameters: + build_kwargs["config"] = config + except (TypeError, ValueError): + pass + model = train_mod.build_model(**build_kwargs) + + ckpt = train_mod.load_checkpoint( + str(checkpoint_path), + model, + map_location="cpu", + ) + + saved_contract = ckpt.get("model_contract", {}) or {} + print( + "[MODEL CONTRACT] " + f"decoder={saved_contract.get('decoder_mode', 'legacy')} | " + f"spectral_init={saved_contract.get('spectral_input_init', 'legacy')} | " + f"channels={saved_contract.get('input_channel_names', [])}" + ) + saved_names = [str(x).strip().upper() for x in saved_contract.get("input_channel_names", [])] + legacy_names = SOURCE_CHANNEL_ORDER[:channels] + if not saved_names and input_channel_names != legacy_names: + raise RuntimeError( + "Checkpoint sem contrato nominal não pode ser usado com " + f"input_channels={input_channel_names}." + ) + model.to(device) + model.eval() + + torch_model = TorchTupleWrapper( + model=model, + output_heads=output_heads, + postprocess=exported_postprocess, + include_norm=onnx_has_norm, + norm_mean=mean, + norm_std=std, + ).to(device) + torch_model.eval() + + clear_cuda() + + if run_torch_fp32: + summary, rows = benchmark_torch( + model=torch_model, + inputs_np=inputs_torch_np, + device=device, + warmup=args.warmup, + repeat=args.repeat, + amp=False, + label="torch_fp32", + ) + summaries.append(summary) + all_rows.extend(rows) + + clear_cuda() + + if run_torch_amp: + summary, rows = benchmark_torch( + model=torch_model, + inputs_np=inputs_torch_np, + device=device, + warmup=args.warmup, + repeat=args.repeat, + amp=True, + label="torch_amp_fp16", + ) + summaries.append(summary) + all_rows.extend(rows) + + del torch_model + del model + clear_cuda() + + # ======================================================== + # ONNX + # ======================================================== + if not args.skip_onnx: + print("\n[ONNX] Carregando sessão...") + session = create_onnx_session( + onnx_path=onnx_path, + provider=args.onnx_provider, + trt_home=args.trt_home, + trt_fp16=not args.trt_no_fp16, + ) + + onnx_input = session.get_inputs()[0] + onnx_input_shape = onnx_input.shape + if len(onnx_input_shape) != 4: + raise RuntimeError( + f"Entrada ONNX inesperada: {onnx_input.name} shape={onnx_input_shape}" + ) + onnx_channels = onnx_input_shape[1] + if isinstance(onnx_channels, int) and onnx_channels != channels: + raise RuntimeError( + f"ONNX espera {onnx_channels} canais, config possui {channels}: " + f"{input_channel_names}" + ) + print(f"[ONNX] input={onnx_input.name} shape={onnx_input_shape}") + + summary, rows = benchmark_onnx( + session=session, + inputs_np=inputs_onnx_np, + warmup=args.warmup, + repeat=args.repeat, + label=f"onnx_{args.onnx_provider}", + ) + summaries.append(summary) + all_rows.extend(rows) + + # ======================================================== + # Relatório + # ======================================================== + print("\n========== RESUMO ==========") + + for s in summaries: + print( + f"{s['engine']:<16} " + f"n={s['n']:<4} " + f"mean={s['mean_ms']:.3f}ms " + f"median={s['median_ms']:.3f}ms " + f"p95={s['p95_ms']:.3f}ms " + f"p99={s['p99_ms']:.3f}ms " + f"fps_mean={s['fps_mean']:.2f} " + f"fps_p95={s['fps_p95_latency']:.2f}" + ) + + base_name = f"{onnx_path.stem}_{args.onnx_provider}" + report_json = out_dir / f"{base_name}_benchmark_report.json" + report_csv = out_dir / f"{base_name}_benchmark_rows.csv" + + report = { + "config": str(config_path), + "train_script": str(train_script_path), + "trainer_version": trainer_version, + "checkpoint": str(checkpoint_path), + "ckpt_name": ckpt_name, + "onnx": str(onnx_path), + "export_meta": str(export_meta_path) if export_meta_path is not None else None, + "onnx_has_norm": bool(onnx_has_norm), + "onnx_postprocess": exported_postprocess, + "root": str(root), + "samples": len(samples), + "warmup": int(args.warmup), + "repeat": int(args.repeat), + "input_shape": [1, channels, H, W], + "input_channel_names": input_channel_names, + "input_channel_indices": input_channel_indices, + "heads": output_heads, + "norm_stats_used": norm_stats_used, + "onnx_provider": args.onnx_provider, + "trt_home": args.trt_home, + "trt_fp16": bool(not args.trt_no_fp16), + "device": str(device), + "dataset_preprocessing_in_timing": False, + "normalization_in_timing": bool(onnx_has_norm), + "summaries": summaries, + } + + save_json(report_json, report) + save_csv(report_csv, all_rows) + + print(f"\n[OK] JSON salvo em: {report_json}") + print(f"[OK] CSV salvo em : {report_csv}") + print("\nBenchmark finalizado.") + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/_3_select_training_samples.py b/Python/OAK/datasets/oak-fcc-3/_3_select_training_samples.py index 0045791b1..0bd9bb9d5 100644 --- a/Python/OAK/datasets/oak-fcc-3/_3_select_training_samples.py +++ b/Python/OAK/datasets/oak-fcc-3/_3_select_training_samples.py @@ -2,675 +2,780 @@ # -*- coding: utf-8 -*- """ -_3_select_training_samples.py +Seleção agrícola inteligente antes do normalize. -Seleciona amostras prontas em dataset/brutas/group e copia para -dataset/original/group com balanceamento por classe. +Princípios: +- máscara é a fonte da verdade para as classes presentes; +- pasta de origem é apenas uma indicação manual; +- remove duplicatas/quase-duplicatas; +- quando há limite, seleciona coreset por diversidade + relevância agrícola; +- preserva preview/mask/meta/3 bins; +- gera manifest, audit e summary. -Estrutura de entrada: +Classes padrão: 0=chao, 1=cana, 2=erva, 255=ignore. -dataset/brutas/group/ - / - previews/ - masks/ - metas/ - bins/ +python .\_3_select_training_samples.py --src-root .\dataset\brutas\group --dst-root .\dataset\original\group --copy-all chao_cana chao_erva chao_cana_erva cana erva cana_erva --group-limit chao=500 --selection-mode diverse --presence-min-pixels 100 --seed 42 --clear-dst -Estrutura de saída: - -dataset/original/group/ - / - previews/ - masks/ - metas/ - bins/ - -Cada amostra válida precisa ter: - previews/.png - masks/.png - metas/.json - bins/_CAM_A.bin - bins/_CAM_B.bin - bins/_CAM_C.bin - -Também aceita formato antigo: - bins/_cam0.bin - bins/_cam1.bin - bins/_cam2.bin - -Exemplo: - -python _4_select_training_samples.py ^ - --src-root dataset/brutas/group ^ - --dst-root dataset/original/group ^ - --copy-all chao_cana chao_erva chao_cana_erva ^ - --max-ratio 2.0 ^ - --seed 42 """ from __future__ import annotations import argparse import csv +import hashlib import json +import math import random import re import shutil -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from statistics import median -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Sequence, Tuple +import cv2 +import numpy as np + +from helpers import ( + carregar_labelmap_completo, + converter_mask_rgb_para_ids, + _infer_ignore_id, +) PREVIEW_EXTS = (".png", ".jpg", ".jpeg") - -BIN_RE_OAK = re.compile( - r"^(?P.+)_(?PCAM_[ABC])(?P.*)\.bin$", - re.IGNORECASE, -) - -BIN_RE_OLD = re.compile( - r"^(?P.+)_cam(?P\d+)(?P.*)\.bin$", - re.IGNORECASE, -) - -CAM_ORDER = { - "CAM_A": 0, - "CAM_B": 1, - "CAM_C": 2, - "cam0": 0, - "cam1": 1, - "cam2": 2, +CLASS_IDS = (0, 1, 2) +GROUP_BY_SET = { + frozenset({0}): "chao", + frozenset({1}): "cana", + frozenset({2}): "erva", + frozenset({0, 1}): "chao_cana", + frozenset({0, 2}): "chao_erva", + frozenset({1, 2}): "cana_erva", + frozenset({0, 1, 2}): "chao_cana_erva", } +BIN_RE_OAK = re.compile(r"^(?P.+)_(?PCAM_[ABC])(?P.*)\.bin$", re.I) +BIN_RE_OLD = re.compile(r"^(?P.+)_cam(?P\d+)(?P.*)\.bin$", re.I) +CAM_ORDER = {"CAM_A": 0, "CAM_B": 1, "CAM_C": 2, "cam0": 0, "cam1": 1, "cam2": 2} + @dataclass -class SampleBundle: - group: str +class Sample: + source_group: str base: str preview_path: Path mask_path: Path meta_path: Path bin_paths: List[Path] + actual_group: str = "" + present_ids: Tuple[int, ...] = field(default_factory=tuple) + frac_chao: float = 0.0 + frac_cana: float = 0.0 + frac_erva: float = 0.0 + comp_cana: int = 0 + comp_erva: int = 0 + largest_cana: float = 0.0 + largest_erva: float = 0.0 + weed_near_cane: float = 0.0 + weed_touch_cane: float = 0.0 + brightness: float = 0.0 + contrast: float = 0.0 + saturation: float = 0.0 + dhash: int = 0 + content_hash: str = "" + feature: Optional[np.ndarray] = None + agri_score: float = 0.0 + diversity_score: float = 0.0 + duplicate_of: str = "" + selected: bool = False + reason: str = "" -def ensure_dir(path: Path) -> None: - path.mkdir(parents=True, exist_ok=True) + +def ensure_dir(p: Path): + p.mkdir(parents=True, exist_ok=True) def parse_bin_name(path: Path): - name = path.name - - m = BIN_RE_OAK.match(name) + m = BIN_RE_OAK.match(path.name) if m: cam = m.group("cam").upper() - - # Exemplo: - # 20_PosProcessamento_CAM_A (2).bin - # prefixo = 20_PosProcessamento - # sufixo = " (2)" - # base = 20_PosProcessamento (2) - base = m.group("prefixo") + m.group("sufixo") - - return base, cam, CAM_ORDER.get(cam, 99) - - m = BIN_RE_OLD.match(name) + return m.group("prefixo") + m.group("sufixo"), cam, CAM_ORDER.get(cam, 99) + m = BIN_RE_OLD.match(path.name) if m: cam = f"cam{int(m.group('cam'))}" - base = m.group("prefixo") + m.group("sufixo") - - return base, cam, CAM_ORDER.get(cam, 99) - + return m.group("prefixo") + m.group("sufixo"), cam, CAM_ORDER.get(cam, 99) return None -def find_preview_files(previews_dir: Path) -> List[Path]: - files = [] - - if not previews_dir.is_dir(): - return files - - for p in sorted(previews_dir.iterdir()): - if p.is_file() and p.suffix.lower() in PREVIEW_EXTS: - files.append(p) - - return files - - -def find_bins_for_base(bins_dir: Path, base: str) -> List[Path]: +def find_bins(bins_dir: Path, base: str) -> List[Path]: found = [] - if not bins_dir.is_dir(): - return found - + return [] for p in bins_dir.glob("*.bin"): parsed = parse_bin_name(p) - if parsed is None: - continue - - bin_base, _cam, order = parsed - - if bin_base != base: - continue - - found.append((order, p)) - - found.sort(key=lambda x: x[0]) - return [p for _, p in found] + if parsed and parsed[0] == base: + found.append((parsed[2], p)) + return [p for _, p in sorted(found, key=lambda x: x[0])] -def has_required_cameras( - bin_paths: List[Path], - required_cameras: List[str], -) -> bool: - if not required_cameras: - return len(bin_paths) > 0 - - counts = {cam.upper(): 0 for cam in required_cameras} - +def has_required_cameras(bin_paths: Sequence[Path], required: Sequence[str]) -> bool: + if not required: + return bool(bin_paths) + seen = {x.upper(): 0 for x in required} for p in bin_paths: parsed = parse_bin_name(p) - if parsed is None: + if not parsed: continue - - _base, cam, _order = parsed - - if cam.lower() == "cam0": - normalizada = "CAM_A" - elif cam.lower() == "cam1": - normalizada = "CAM_B" - elif cam.lower() == "cam2": - normalizada = "CAM_C" - else: - normalizada = cam.upper() - - if normalizada in counts: - counts[normalizada] += 1 - - return all(quantidade == 1 for quantidade in counts.values()) + cam = parsed[1] + cam = {"cam0": "CAM_A", "cam1": "CAM_B", "cam2": "CAM_C"}.get(cam.lower(), cam.upper()) + if cam in seen: + seen[cam] += 1 + return all(v == 1 for v in seen.values()) -def collect_valid_samples_from_group( - group_dir: Path, - required_cameras: List[str], -) -> List[SampleBundle]: - group = group_dir.name - - previews_dir = group_dir / "previews" - masks_dir = group_dir / "masks" - metas_dir = group_dir / "metas" - bins_dir = group_dir / "bins" - - samples = [] - - if not previews_dir.is_dir(): - return samples - - for preview_path in find_preview_files(previews_dir): - base = preview_path.stem - - mask_path = masks_dir / f"{base}.png" - meta_path = metas_dir / f"{base}.json" - bin_paths = find_bins_for_base(bins_dir, base) - - if not mask_path.exists(): +def collect_samples(src_root: Path, required: Sequence[str]) -> List[Sample]: + out = [] + for gdir in sorted(src_root.iterdir()): + if not gdir.is_dir(): continue - - if not meta_path.exists(): + pdir, mdir, jdir, bdir = gdir / "previews", gdir / "masks", gdir / "metas", gdir / "bins" + if not pdir.is_dir(): continue - - if not bin_paths: - continue - - if not has_required_cameras(bin_paths, required_cameras): - continue - - samples.append( - SampleBundle( - group=group, - base=base, - preview_path=preview_path, - mask_path=mask_path, - meta_path=meta_path, - bin_paths=bin_paths, - ) - ) - - return samples + for preview in sorted(pdir.iterdir()): + if not preview.is_file() or preview.suffix.lower() not in PREVIEW_EXTS: + continue + base = preview.stem + mask = mdir / f"{base}.png" + meta = jdir / f"{base}.json" + bins = find_bins(bdir, base) + if not mask.is_file() or not meta.is_file() or not bins: + continue + if not has_required_cameras(bins, required): + continue + out.append(Sample(gdir.name, base, preview, mask, meta, bins)) + return out -def collect_all_groups( - src_root: Path, - required_cameras: List[str], -) -> Dict[str, List[SampleBundle]]: - groups = {} +def read_mask( + path: Path, + cor_para_id: dict, + ignore_id: int, +) -> np.ndarray: + """ + Lê a máscara bruta usando o MESMO contrato do normalize. - for group_dir in sorted(src_root.iterdir()): - if not group_dir.is_dir(): - continue + Aceita: + 1) id-map grayscale já em 0/1/2/ignore; + 2) máscara RGB colorida definida pelo dataset/labelmap.txt. - valid = collect_valid_samples_from_group(group_dir, required_cameras) - groups[group_dir.name] = valid + Nas máscaras coloridas: + OpenCV lê BGR -> convertemos para RGB -> helpers.converter_mask_rgb_para_ids. + """ + raw = cv2.imread(str(path), cv2.IMREAD_UNCHANGED) - return groups + if raw is None: + raise RuntimeError(f"falha ao ler máscara: {path}") + allowed = set(CLASS_IDS) | {int(ignore_id)} -def compute_reference_count( - all_samples: Dict[str, List[SampleBundle]], - copy_all: List[str], - fallback_mode: str, -) -> int: - counts = [] + # Caso já seja uma máscara de IDs grayscale. + if raw.ndim == 2: + unique = set(int(v) for v in np.unique(raw).tolist()) + if unique.issubset(allowed): + return np.asarray(raw) - for group in copy_all: - n = len(all_samples.get(group, [])) - if n > 0: - counts.append(n) + # Algumas máscaras podem estar salvas em 3 canais, mas já conter IDs iguais + # nos três canais. Aceitamos esse caso também. + if raw.ndim == 3 and raw.shape[2] >= 3: + c0 = raw[..., 0] + c1 = raw[..., 1] + c2 = raw[..., 2] - if counts: - return int(round(median(counts))) + if np.array_equal(c0, c1) and np.array_equal(c0, c2): + unique = set(int(v) for v in np.unique(c0).tolist()) + if unique.issubset(allowed): + return np.asarray(c0) - # Fallback caso não tenha copy_all válido - all_counts = [len(v) for v in all_samples.values() if len(v) > 0] + # Contrato normal do dataset bruto: máscara COLORIDA segundo labelmap.txt. + bgr = cv2.imread(str(path), cv2.IMREAD_COLOR) - if not all_counts: - return 0 + if bgr is None: + raise RuntimeError(f"falha ao abrir máscara RGB: {path}") - if fallback_mode == "min": - return min(all_counts) + rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) - if fallback_mode == "median": - return int(round(median(all_counts))) - - if fallback_mode == "mean": - return int(round(sum(all_counts) / len(all_counts))) - - return min(all_counts) - - -def select_samples( - all_samples: Dict[str, List[SampleBundle]], - copy_all: List[str], - max_ratio: float, - target_per_other: Optional[int], - max_per_class: Optional[int], - seed: int, - fallback_mode: str, -) -> Dict[str, List[SampleBundle]]: - rng = random.Random(seed) - - copy_all_set = set(copy_all) - - reference_count = compute_reference_count( - all_samples=all_samples, - copy_all=copy_all, - fallback_mode=fallback_mode, + ids = converter_mask_rgb_para_ids( + rgb, + cor_para_id, + int(ignore_id), ) - if target_per_other is not None: - other_limit = int(target_per_other) - else: - other_limit = int(round(reference_count * float(max_ratio))) + ids = np.asarray(ids) - if other_limit < 1: - other_limit = 1 + if ids.ndim != 2: + raise RuntimeError( + f"conversão RGB->IDs retornou shape inválido {ids.shape}: {path}" + ) - selected = {} + unique = set(int(v) for v in np.unique(ids).tolist()) + unknown = sorted(unique - allowed) - for group, samples in all_samples.items(): - samples_sorted = list(samples) - rng.shuffle(samples_sorted) + if unknown: + raise RuntimeError( + f"IDs desconhecidos após converter labelmap: {unknown} em {path}" + ) - if group in copy_all_set: - chosen = samples_sorted - else: - limit = other_limit - - if max_per_class is not None: - limit = min(limit, int(max_per_class)) - - chosen = samples_sorted[: min(len(samples_sorted), limit)] - - # Ordena por nome depois do sorteio, só para copiar organizado - chosen.sort(key=lambda s: s.base) - selected[group] = chosen - - return selected + return ids -def dest_bin_name(new_base: str, src_bin: Path) -> str: - parsed = parse_bin_name(src_bin) +def infer_group(mask: np.ndarray, ignore_ids: Sequence[int], min_pixels: int, min_frac: float): + allowed = set(CLASS_IDS) | set(ignore_ids) + unknown = sorted(set(int(v) for v in np.unique(mask)) - allowed) + if unknown: + raise RuntimeError(f"IDs desconhecidos na máscara: {unknown}") - if parsed is None: - return src_bin.name + present = [] + for cid in CLASS_IDS: + count = int(np.count_nonzero(mask == cid)) + frac = count / max(mask.size, 1) + if count >= max(1, min_pixels) and frac >= min_frac: + present.append(cid) - _old_base, cam, _order = parsed - - if cam.upper().startswith("CAM_"): - return f"{new_base}_{cam.upper()}.bin" - - return f"{new_base}_{cam}.bin" + group = GROUP_BY_SET.get(frozenset(present)) + if group is None: + raise RuntimeError(f"combinação de classes não mapeada: {present}") + return group, tuple(present) -def unique_base_for_dest(dst_group_dir: Path, base: str, preview_ext: str) -> str: - previews_dir = dst_group_dir / "previews" - masks_dir = dst_group_dir / "masks" - metas_dir = dst_group_dir / "metas" - bins_dir = dst_group_dir / "bins" +def comp_stats(binary: np.ndarray): + n, _, stats, _ = cv2.connectedComponentsWithStats(binary.astype(np.uint8), 8) + if n <= 1: + return 0, 0.0 + areas = stats[1:, cv2.CC_STAT_AREA] + return int(len(areas)), float(np.max(areas) / max(binary.size, 1)) - def occupied(candidate: str) -> bool: - if (previews_dir / f"{candidate}{preview_ext}").exists(): - return True - if (masks_dir / f"{candidate}.png").exists(): - return True - if (metas_dir / f"{candidate}.json").exists(): - return True - for suffix in ("CAM_A", "CAM_B", "CAM_C", "cam0", "cam1", "cam2"): - if (bins_dir / f"{candidate}_{suffix}.bin").exists(): - return True +def weed_cane_relation(mask: np.ndarray, near_frac: float): + cane = mask == 1 + weed = mask == 2 + if not np.any(cane) or not np.any(weed): + return 0.0, 0.0 - return False + radius = max(1, int(round(min(mask.shape[:2]) * near_frac))) + dist = cv2.distanceTransform((~cane).astype(np.uint8), cv2.DIST_L2, 3) + near = float(np.mean(dist[weed] <= radius)) - if not occupied(base): - return base + dil = cv2.dilate(cane.astype(np.uint8), np.ones((3, 3), np.uint8), iterations=1) > 0 + touch = float(np.mean(dil[weed])) if np.any(weed) else 0.0 + return near, touch + +def grid_fraction(binary: np.ndarray, rows=4, cols=6): + h, w = binary.shape[:2] + vals = [] + for gy in range(rows): + y0, y1 = int(gy*h/rows), int((gy+1)*h/rows) + for gx in range(cols): + x0, x1 = int(gx*w/cols), int((gx+1)*w/cols) + tile = binary[y0:y1, x0:x1] + vals.append(float(np.mean(tile)) if tile.size else 0.0) + return np.asarray(vals, np.float32) + + +def dhash64(img: np.ndarray) -> int: + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + small = cv2.resize(gray, (9, 8), interpolation=cv2.INTER_AREA) + diff = small[:, 1:] > small[:, :-1] + v = 0 + for bit in diff.flatten(): + v = (v << 1) | int(bool(bit)) + return v + + +def hist_features(img: np.ndarray): + hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + parts = [] + for ch, bins, hi in ((0, 12, 180), (1, 8, 256), (2, 8, 256)): + h = cv2.calcHist([hsv], [ch], None, [bins], [0, hi]).flatten().astype(np.float32) + if h.sum() > 0: + h /= h.sum() + parts.append(h) + return np.concatenate(parts) + + +def agricultural_score(s: Sample) -> float: + has_chao, has_cana, has_erva = 0 in s.present_ids, 1 in s.present_ids, 2 in s.present_ids + score = 0.0 + if has_erva: score += 0.20 + if has_cana: score += 0.12 + if has_chao and (has_cana or has_erva): score += 0.08 + if has_cana and has_erva: score += 0.28 + score += 0.18 * s.weed_near_cane + score += 0.10 * s.weed_touch_cane + return float(min(1.0, score)) + + +def analyze( + s: Sample, + trust_folder: bool, + ignore_ids: Sequence[int], + min_pixels: int, + min_frac: float, + near_frac: float, + cor_para_id: dict, + ignore_id: int, +): + mask = read_mask( + s.mask_path, + cor_para_id=cor_para_id, + ignore_id=ignore_id, + ) + inferred, present = infer_group(mask, ignore_ids, min_pixels, min_frac) + s.actual_group = s.source_group if trust_folder else inferred + s.present_ids = present + s.frac_chao = float(np.mean(mask == 0)) + s.frac_cana = float(np.mean(mask == 1)) + s.frac_erva = float(np.mean(mask == 2)) + s.comp_cana, s.largest_cana = comp_stats(mask == 1) + s.comp_erva, s.largest_erva = comp_stats(mask == 2) + s.weed_near_cane, s.weed_touch_cane = weed_cane_relation(mask, near_frac) + + img = cv2.imread(str(s.preview_path), cv2.IMREAD_COLOR) + if img is None: + raise RuntimeError(f"falha ao ler preview: {s.preview_path}") + hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + s.brightness = float(np.mean(hsv[..., 2]) / 255.0) + s.saturation = float(np.mean(hsv[..., 1]) / 255.0) + s.contrast = float(np.std(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) / 255.0) + s.dhash = dhash64(img) + + img_small = cv2.resize(img, (64, 40), interpolation=cv2.INTER_AREA) + mask_small = cv2.resize(mask.astype(np.uint8), (64, 40), interpolation=cv2.INTER_NEAREST) + h = hashlib.sha1(); h.update(img_small.tobytes()); h.update(mask_small.tobytes()) + s.content_hash = h.hexdigest() + + low = cv2.resize(img, (8, 5), interpolation=cv2.INTER_AREA).astype(np.float32).flatten() / 255.0 + s.feature = np.concatenate([ + np.asarray([ + s.frac_chao, s.frac_cana, s.frac_erva, + math.log1p(s.comp_cana)/6.0, math.log1p(s.comp_erva)/6.0, + s.largest_cana, s.largest_erva, + s.weed_near_cane, s.weed_touch_cane, + s.brightness, s.contrast, s.saturation, + ], np.float32), + grid_fraction(mask == 1), + grid_fraction(mask == 2), + hist_features(img), + low, + ]).astype(np.float32) + s.agri_score = agricultural_score(s) + + +def hamming(a: int, b: int) -> int: + return (a ^ b).bit_count() + + +def deduplicate(samples: Sequence[Sample], max_hamming: int, max_mask_l1: float): + ordered = sorted(samples, key=lambda s: (-s.agri_score, s.base)) + kept, dups = [], [] + exact = {} + for s in ordered: + key = (s.actual_group, s.content_hash) + if key in exact: + s.duplicate_of = exact[key].base; s.reason = "duplicate_exact"; dups.append(s); continue + sf = np.asarray([s.frac_chao, s.frac_cana, s.frac_erva]) + rep = None + for k in kept: + if k.actual_group != s.actual_group or hamming(k.dhash, s.dhash) > max_hamming: + continue + kf = np.asarray([k.frac_chao, k.frac_cana, k.frac_erva]) + if float(np.sum(np.abs(kf - sf))) <= max_mask_l1: + rep = k; break + if rep is not None: + s.duplicate_of = rep.base; s.reason = "duplicate_near"; dups.append(s); continue + kept.append(s); exact[key] = s + return kept, dups + + +def robust_z(x: np.ndarray): + med = np.median(x, axis=0) + q25, q75 = np.percentile(x, 25, axis=0), np.percentile(x, 75, axis=0) + scale = q75 - q25 + std = np.std(x, axis=0) + scale = np.where(scale > 1e-6, scale, std) + scale = np.where(scale > 1e-6, scale, 1.0) + return np.clip((x - med) / scale, -8, 8).astype(np.float32) + + +def select_diverse(samples: Sequence[Sample], k: int, seed: int, agri_weight: float): + items = list(samples) + if k >= len(items): + for s in items: s.reason = s.reason or "all"; s.diversity_score = 1.0 + return items + + x = robust_z(np.stack([s.feature for s in items])) + agri = np.asarray([s.agri_score for s in items], np.float32) + rng = np.random.default_rng(seed) + jitter = rng.uniform(0, 1e-6, len(items)).astype(np.float32) + + first = int(np.argmax(agri + jitter)) + chosen = [first] + used = np.zeros(len(items), bool); used[first] = True + d = np.sum((x - x[first])**2, axis=1) + + while len(chosen) < k: + dist = np.sqrt(np.maximum(d, 0)) + priority = dist * (1.0 + agri_weight * agri) + priority[used] = -1 + idx = int(np.argmax(priority + jitter)) + chosen.append(idx); used[idx] = True + d = np.minimum(d, np.sum((x - x[idx])**2, axis=1)) + + out = [] + for idx in chosen: + s = items[idx]; s.reason = "diverse_coreset"; s.diversity_score = 1.0; out.append(s) + return out + + +def parse_group_limits(values: Sequence[str]) -> Dict[str, int]: + out = {} + for v in values: + if "=" not in v: + raise ValueError(f"--group-limit inválido: {v}; use grupo=N") + g, n = v.split("=", 1); out[g.strip()] = int(n) + return out + + +def group_by_actual(samples: Sequence[Sample]): + out: Dict[str, List[Sample]] = {} + for s in samples: out.setdefault(s.actual_group, []).append(s) + return out + + +def reference_count(by_group, copy_all, fallback): + vals = [len(by_group[g]) for g in copy_all if g in by_group and by_group[g]] + if vals: return int(round(median(vals))) + vals = [len(v) for v in by_group.values() if v] + if not vals: return 0 + if fallback == "median": return int(round(median(vals))) + if fallback == "mean": return int(round(sum(vals)/len(vals))) + return min(vals) + + +def resolve_limit(group, n, copy_all, group_limits, ref, max_ratio, target_other, max_per_class): + if group in group_limits: return min(n, max(0, group_limits[group])) + if group in set(copy_all): return n + lim = target_other if target_other is not None else int(round(ref * max_ratio)) + if max_per_class is not None: lim = min(lim, max_per_class) + return min(n, max(0, lim)) + + +def dest_bin_name(base: str, src: Path): + parsed = parse_bin_name(src) + if not parsed: return src.name + cam = parsed[1] + return f"{base}_{cam.upper() if cam.upper().startswith('CAM_') else cam}.bin" + + +def unique_base(dst_group: Path, base: str, ext: str): + def occupied(c): + if (dst_group/"previews"/f"{c}{ext}").exists() or (dst_group/"masks"/f"{c}.png").exists() or (dst_group/"metas"/f"{c}.json").exists(): return True + return any((dst_group/"bins"/f"{c}_{s}.bin").exists() for s in ("CAM_A","CAM_B","CAM_C","cam0","cam1","cam2")) + if not occupied(base): return base i = 1 - while True: - candidate = f"{base}_{i:03d}" - if not occupied(candidate): - return candidate - i += 1 + while occupied(f"{base}_{i:03d}"): i += 1 + return f"{base}_{i:03d}" -def copy_sample( - sample: SampleBundle, - dst_root: Path, - overwrite: bool, -) -> dict: - dst_group_dir = dst_root / sample.group - - dst_previews = dst_group_dir / "previews" - dst_masks = dst_group_dir / "masks" - dst_metas = dst_group_dir / "metas" - dst_bins = dst_group_dir / "bins" - - for d in (dst_previews, dst_masks, dst_metas, dst_bins): - ensure_dir(d) - - preview_ext = sample.preview_path.suffix.lower() - - if overwrite: - new_base = sample.base - else: - new_base = unique_base_for_dest(dst_group_dir, sample.base, preview_ext) - - dst_preview = dst_previews / f"{new_base}{preview_ext}" - dst_mask = dst_masks / f"{new_base}.png" - dst_meta = dst_metas / f"{new_base}.json" - - shutil.copy2(sample.preview_path, dst_preview) - shutil.copy2(sample.mask_path, dst_mask) - shutil.copy2(sample.meta_path, dst_meta) - - dst_bins_paths = [] - - for src_bin in sample.bin_paths: - dst_bin = dst_bins / dest_bin_name(new_base, src_bin) - shutil.copy2(src_bin, dst_bin) - dst_bins_paths.append(dst_bin) - +def copy_sample(s: Sample, dst_root: Path, overwrite: bool): + g = dst_root / s.actual_group + for d in (g/"previews", g/"masks", g/"metas", g/"bins"): ensure_dir(d) + ext = s.preview_path.suffix.lower() + base = s.base if overwrite else unique_base(g, s.base, ext) + dp, dm, dj = g/"previews"/f"{base}{ext}", g/"masks"/f"{base}.png", g/"metas"/f"{base}.json" + shutil.copy2(s.preview_path, dp); shutil.copy2(s.mask_path, dm); shutil.copy2(s.meta_path, dj) + dbins = [] + for b in s.bin_paths: + d = g/"bins"/dest_bin_name(base, b); shutil.copy2(b, d); dbins.append(d) return { - "group": sample.group, - "src_base": sample.base, - "dst_base": new_base, - "src_preview": str(sample.preview_path), - "src_mask": str(sample.mask_path), - "src_meta": str(sample.meta_path), - "src_bins": json.dumps([str(p) for p in sample.bin_paths], ensure_ascii=False), - "dst_preview": str(dst_preview), - "dst_mask": str(dst_mask), - "dst_meta": str(dst_meta), - "dst_bins": json.dumps([str(p) for p in dst_bins_paths], ensure_ascii=False), - "bin_count": len(dst_bins_paths), + "source_group": s.source_group, "actual_group": s.actual_group, + "src_base": s.base, "dst_base": base, + "src_preview": str(s.preview_path), "src_mask": str(s.mask_path), "src_meta": str(s.meta_path), + "src_bins": json.dumps([str(x) for x in s.bin_paths], ensure_ascii=False), + "dst_preview": str(dp), "dst_mask": str(dm), "dst_meta": str(dj), + "dst_bins": json.dumps([str(x) for x in dbins], ensure_ascii=False), + "frac_chao": s.frac_chao, "frac_cana": s.frac_cana, "frac_erva": s.frac_erva, + "agricultural_score": s.agri_score, "selection_reason": s.reason, } -def write_manifest(path: Path, rows: List[dict]) -> None: +def write_csv(path: Path, rows: List[dict]): + if not rows: return ensure_dir(path.parent) - - fieldnames = [ - "group", - "src_base", - "dst_base", - "src_preview", - "src_mask", - "src_meta", - "src_bins", - "dst_preview", - "dst_mask", - "dst_meta", - "dst_bins", - "bin_count", - ] - with path.open("w", newline="", encoding="utf-8") as f: - w = csv.DictWriter(f, fieldnames=fieldnames) - w.writeheader() - w.writerows(rows) + w = csv.DictWriter(f, fieldnames=list(rows[0].keys()), extrasaction="ignore"); w.writeheader(); w.writerows(rows) -def write_summary(path: Path, summary: dict) -> None: - ensure_dir(path.parent) - - with path.open("w", encoding="utf-8") as f: - json.dump(summary, f, ensure_ascii=False, indent=2) - - -def maybe_clear_dest(dst_root: Path) -> None: - if dst_root.exists(): - shutil.rmtree(dst_root) - ensure_dir(dst_root) +def audit_row(s: Sample): + return { + "source_group": s.source_group, "actual_group": s.actual_group, + "group_mismatch": s.source_group != s.actual_group, + "base": s.base, "selected": s.selected, "reason": s.reason, + "duplicate_of": s.duplicate_of, + "present_ids": ",".join(map(str, s.present_ids)), + "frac_chao": s.frac_chao, "frac_cana": s.frac_cana, "frac_erva": s.frac_erva, + "comp_cana": s.comp_cana, "comp_erva": s.comp_erva, + "largest_cana": s.largest_cana, "largest_erva": s.largest_erva, + "weed_near_cane": s.weed_near_cane, "weed_touch_cane": s.weed_touch_cane, + "brightness": s.brightness, "contrast": s.contrast, "saturation": s.saturation, + "agri_score": s.agri_score, "dhash": f"{s.dhash:016x}", "content_hash": s.content_hash, + "preview": str(s.preview_path), "mask": str(s.mask_path), "meta": str(s.meta_path), + } def main(): - parser = argparse.ArgumentParser( - description="Seleciona amostras prontas de dataset/brutas/group para dataset/original/group com balanceamento." - ) - - parser.add_argument( - "--src-root", - default="dataset/brutas/group", - help="Raiz dos grupos preparados.", - ) - - parser.add_argument( - "--dst-root", - default="dataset/original/group", - help="Raiz de saída dos grupos selecionados.", - ) - - parser.add_argument( - "--copy-all", - nargs="*", - default=[], - help="Classes/grupos que serão copiados integralmente.", - ) - - parser.add_argument( - "--max-ratio", - type=float, - default=2.0, - help="Para classes não listadas em --copy-all, copia no máximo reference_count * max_ratio.", - ) - - parser.add_argument( - "--target-per-other", - type=int, - default=None, - help="Se definido, ignora --max-ratio e usa este limite para classes fora de --copy-all.", - ) - - parser.add_argument( - "--max-per-class", - type=int, - default=None, - help="Limite absoluto por classe para classes fora de --copy-all.", - ) - - parser.add_argument( - "--required-cameras", - nargs="*", - default=["CAM_A", "CAM_B", "CAM_C"], - help="Câmeras obrigatórias nos bins. Use vazio para aceitar qualquer bin.", - ) - - parser.add_argument( - "--seed", - type=int, - default=42, - help="Seed para seleção aleatória reprodutível.", - ) - - parser.add_argument( - "--fallback-mode", - choices=["min", "median", "mean"], - default="min", - help="Referência caso nenhum grupo de --copy-all tenha amostras.", - ) - - parser.add_argument( - "--manifest", + ap = argparse.ArgumentParser(description="Seleção agrícola inteligente por máscara + diversidade + relevância operacional") + ap.add_argument("--src-root", default="dataset/brutas/group") + ap.add_argument("--dst-root", default="dataset/original/group") + ap.add_argument( + "--labelmap", default="", - help="CSV de manifesto. Default: /selection_manifest.csv", + help=( + "Caminho do labelmap.txt. " + "Default: tenta /labelmap.txt a partir de --src-root." + ), ) + ap.add_argument("--copy-all", nargs="*", default=[]) + ap.add_argument("--group-limit", nargs="*", default=[], metavar="GRUPO=N") + ap.add_argument("--selection-mode", choices=["diverse", "random"], default="diverse") + ap.add_argument("--agricultural-weight", type=float, default=0.75) + ap.add_argument("--max-ratio", type=float, default=2.0) + ap.add_argument("--target-per-other", type=int, default=None) + ap.add_argument("--max-per-class", type=int, default=None) + ap.add_argument("--required-cameras", nargs="*", default=["CAM_A","CAM_B","CAM_C"]) + ap.add_argument("--seed", type=int, default=42) + ap.add_argument("--fallback-mode", choices=["min","median","mean"], default="min") + ap.add_argument("--trust-folder-group", action="store_true") + ap.add_argument("--presence-min-pixels", type=int, default=1) + ap.add_argument("--presence-min-fraction", type=float, default=0.0) + ap.add_argument("--ignore-ids", nargs="*", type=int, default=[255]) + ap.add_argument("--near-radius-frac", type=float, default=0.02) + ap.add_argument("--no-dedup", action="store_true") + ap.add_argument("--dedup-hamming", type=int, default=3) + ap.add_argument("--dedup-mask-l1", type=float, default=0.03) + ap.add_argument("--manifest", default="") + ap.add_argument("--audit", default="") + ap.add_argument("--summary", default="") + ap.add_argument("--dry-run", action="store_true") + ap.add_argument("--clear-dst", action="store_true") + ap.add_argument("--overwrite", action="store_true") + args = ap.parse_args() - parser.add_argument( - "--summary", - default="", - help="JSON de resumo. Default: /selection_summary.json", - ) - - parser.add_argument( - "--dry-run", - action="store_true", - help="Só mostra o plano, não copia nada.", - ) - - parser.add_argument( - "--clear-dst", - action="store_true", - help="Apaga dst-root antes de copiar.", - ) - - parser.add_argument( - "--overwrite", - action="store_true", - help="Sobrescreve nomes existentes no destino. Sem isso, deduplica com _001, _002...", - ) - - args = parser.parse_args() - - src_root = Path(args.src_root) - dst_root = Path(args.dst_root) - - if not src_root.is_dir(): - raise SystemExit(f"[ERRO] src-root não encontrado: {src_root}") - + src, dst = Path(args.src_root), Path(args.dst_root) + if not src.is_dir(): raise SystemExit(f"[ERRO] src-root não encontrado: {src}") if args.clear_dst and not args.dry_run: - maybe_clear_dest(dst_root) + if dst.exists(): shutil.rmtree(dst) + ensure_dir(dst) - all_samples = collect_all_groups( - src_root=src_root, - required_cameras=args.required_cameras, - ) + limits = parse_group_limits(args.group_limit) - selected = select_samples( - all_samples=all_samples, - copy_all=args.copy_all, - max_ratio=args.max_ratio, - target_per_other=args.target_per_other, - max_per_class=args.max_per_class, - seed=args.seed, - fallback_mode=args.fallback_mode, - ) + # -------------------------------------------------------- + # Labelmap: mesmo contrato usado pelo normalize + # src típico: dataset/brutas/group -> dataset/labelmap.txt + # -------------------------------------------------------- + if args.labelmap: + labelmap_path = Path(args.labelmap) + else: + # dataset/brutas/group -> parents[1] == dataset + try: + dataset_root = src.parents[1] + except Exception: + dataset_root = Path("dataset") - reference_count = compute_reference_count( - all_samples=all_samples, - copy_all=args.copy_all, - fallback_mode=args.fallback_mode, + labelmap_path = dataset_root / "labelmap.txt" + + if not labelmap_path.is_file(): + fallback = Path("dataset") / "labelmap.txt" + if fallback.is_file(): + labelmap_path = fallback + + if not labelmap_path.is_file(): + raise SystemExit( + f"[ERRO] labelmap não encontrado: {labelmap_path}\n" + "Passe explicitamente --labelmap caminho/labelmap.txt" + ) + + try: + cor_para_id, _, classes_labelmap, ignore_rgb = carregar_labelmap_completo( + str(labelmap_path) + ) + ignore_id = int(_infer_ignore_id(ignore_rgb, 255)) + except Exception as exc: + raise SystemExit( + f"[ERRO] Falha ao carregar labelmap {labelmap_path}: {exc}" + ) + + # O ignore detectado no labelmap entra sempre na lista permitida. + effective_ignore_ids = sorted( + set(int(x) for x in args.ignore_ids) | {ignore_id} ) print("==============================================") - print("Seleção de amostras para treinamento") - print(f"Origem : {src_root}") - print(f"Destino : {dst_root}") - print(f"Copy all : {args.copy_all}") - print(f"Reference : {reference_count}") - print(f"Max ratio : {args.max_ratio}") - print(f"Target other : {args.target_per_other}") - print(f"Seed : {args.seed}") - print(f"Dry-run : {args.dry_run}") + print("SELETOR AGRÍCOLA INTELIGENTE") + print(f"Origem : {src}") + print(f"Destino : {dst}") + print(f"Modo : {args.selection_mode}") + print(f"Copy all : {args.copy_all}") + print(f"Group limits : {limits}") + print(f"Máscara é verdade : {not args.trust_folder_group}") + print(f"Dedup : {not args.no_dedup}") + print(f"Labelmap : {labelmap_path}") + print(f"Ignore ID : {ignore_id}") + print(f"Classes labelmap : {classes_labelmap}") print("==============================================") - print("\nPlano por grupo:") - for group in sorted(all_samples.keys()): - total_valid = len(all_samples[group]) - total_selected = len(selected.get(group, [])) - mode = "ALL" if group in set(args.copy_all) else "BALANCED" - print(f" {group:24s} validos={total_valid:5d} selecionados={total_selected:5d} modo={mode}") + samples = collect_samples(src, args.required_cameras) + print(f"[1/4] bundles válidos: {len(samples)}") + analyzed, rejected = [], [] + for i, s in enumerate(samples, 1): + try: + analyze( + s, + args.trust_folder_group, + effective_ignore_ids, + args.presence_min_pixels, + args.presence_min_fraction, + args.near_radius_frac, + cor_para_id, + ignore_id, + ) + analyzed.append(s) + + except Exception as e: + error_text = str(e) + rejected.append({ + "source_group": s.source_group, + "base": s.base, + "mask": str(s.mask_path), + "error": error_text, + }) + + # Mostra cedo os primeiros erros para não esperar milhares de samples + # antes de perceber um problema sistêmico. + if len(rejected) <= 10: + print( + f"[REJECT {len(rejected):02d}] " + f"{s.source_group}/{s.base}: {error_text}" + ) + + if i % 250 == 0 or i == len(samples): + print( + f" auditados={i}/{len(samples)} " + f"ok={len(analyzed)} rejeitados={len(rejected)}" + ) + + mismatches = [s for s in analyzed if s.source_group != s.actual_group] + print( + f"[2/4] auditados={len(analyzed)} " + f"mismatch_pasta_mascara={len(mismatches)} " + f"rejeitados={len(rejected)}" + ) + + if rejected: + error_counts: Dict[str, int] = {} + for item in rejected: + # Agrupa pela mensagem completa; para nosso volume já é suficiente + # e deixa o erro sistêmico imediatamente visível. + key = str(item.get("error", "erro_desconhecido")) + error_counts[key] = error_counts.get(key, 0) + 1 + + print("\nPrincipais motivos de rejeição:") + for error, count in sorted( + error_counts.items(), + key=lambda kv: (-kv[1], kv[0]), + )[:10]: + print(f" {count:5d}x {error}") + + if not analyzed: + raise SystemExit( + "[ERRO] Nenhuma amostra passou pela auditoria. " + "Veja os [REJECT] acima antes de continuar." + ) + + if args.no_dedup: + unique, dups = analyzed, [] + else: + unique, dups = deduplicate(analyzed, args.dedup_hamming, args.dedup_mask_l1) + print(f"[3/4] únicos={len(unique)} duplicatas/quase={len(dups)}") + + by_group = group_by_actual(unique) + ref = reference_count(by_group, args.copy_all, args.fallback_mode) + selected_by_group = {} + + print("\nPlano por grupo REAL (máscara):") + for group in sorted(by_group): + items = by_group[group] + lim = resolve_limit(group, len(items), args.copy_all, limits, ref, args.max_ratio, args.target_per_other, args.max_per_class) + if lim >= len(items): + chosen = list(items) + for s in chosen: s.reason = s.reason or "all"; s.diversity_score = 1.0 + elif args.selection_mode == "diverse": + chosen = select_diverse(items, lim, args.seed, args.agricultural_weight) + else: + rng = random.Random(args.seed); chosen = list(items); rng.shuffle(chosen); chosen = chosen[:lim] + for s in chosen: s.reason = "random" + for s in chosen: s.selected = True + selected_by_group[group] = chosen + print(f" {group:24s} únicos={len(items):5d} selecionados={len(chosen):5d} modo={'ALL' if lim >= len(items) else args.selection_mode.upper()}") + + total_selected = sum(len(v) for v in selected_by_group.values()) manifest_rows = [] if not args.dry_run: - for group in sorted(selected.keys()): - for sample in selected[group]: - row = copy_sample( - sample=sample, - dst_root=dst_root, - overwrite=args.overwrite, - ) - manifest_rows.append(row) + for group in sorted(selected_by_group): + for s in selected_by_group[group]: + manifest_rows.append(copy_sample(s, dst, args.overwrite)) - manifest_path = Path(args.manifest) if args.manifest else dst_root / "selection_manifest.csv" - summary_path = Path(args.summary) if args.summary else dst_root / "selection_summary.json" + manifest = Path(args.manifest) if args.manifest else dst / "selection_manifest.csv" + audit = Path(args.audit) if args.audit else dst / "selection_audit.csv" + summary = Path(args.summary) if args.summary else dst / "selection_summary.json" + write_csv(manifest, manifest_rows) + write_csv(audit, [audit_row(s) for s in sorted(analyzed, key=lambda x:(x.actual_group,x.source_group,x.base))]) - write_manifest(manifest_path, manifest_rows) - - summary = { - "src_root": str(src_root), - "dst_root": str(dst_root), - "copy_all": args.copy_all, - "max_ratio": args.max_ratio, - "target_per_other": args.target_per_other, - "max_per_class": args.max_per_class, - "required_cameras": args.required_cameras, - "seed": args.seed, - "reference_count": reference_count, - "groups": { - group: { - "valid": len(all_samples[group]), - "selected": len(selected.get(group, [])), - "mode": "all" if group in set(args.copy_all) else "balanced", - } - for group in sorted(all_samples.keys()) - }, - "total_selected": sum(len(v) for v in selected.values()), + data = { + "src_root": str(src), "dst_root": str(dst), "selection_mode": args.selection_mode, + "copy_all": args.copy_all, "group_limits": limits, "seed": args.seed, + "labelmap": str(labelmap_path), + "ignore_id": ignore_id, + "effective_ignore_ids": effective_ignore_ids, + "reclassify_by_mask": not args.trust_folder_group, + "dedup_enabled": not args.no_dedup, + "total_collected": len(samples), "total_analyzed": len(analyzed), + "total_rejected": len(rejected), "total_group_mismatches": len(mismatches), + "total_unique_after_dedup": len(unique), "total_duplicates": len(dups), + "total_selected": total_selected, + "groups": {g:{"unique_available":len(by_group[g]),"selected":len(selected_by_group[g])} for g in sorted(by_group)}, + "folder_mask_mismatches": [{"base":s.base,"source_group":s.source_group,"actual_group":s.actual_group,"frac_chao":s.frac_chao,"frac_cana":s.frac_cana,"frac_erva":s.frac_erva} for s in mismatches], + "rejected": rejected, } - - write_summary(summary_path, summary) - - print("\n[OK] Cópia concluída.") - print(f"Manifest : {manifest_path}") - print(f"Summary : {summary_path}") - + ensure_dir(summary.parent) + summary.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"\n[OK] Manifest: {manifest}\n[OK] Audit : {audit}\n[OK] Summary : {summary}") else: print("\n[DRY-RUN] Nada foi copiado.") - print("\nResumo final:") - print(f"Total válido : {sum(len(v) for v in all_samples.values())}") - print(f"Total selecionado: {sum(len(v) for v in selected.values())}") + print("\n==============================================") + print(f"Válidos coletados : {len(samples)}") + print(f"Auditados OK : {len(analyzed)}") + print(f"Rejeitados : {len(rejected)}") + print(f"Mismatch pasta x máscara : {len(mismatches)}") + print(f"Duplicatas/quase : {len(dups)}") + print(f"Únicos após dedup : {len(unique)}") + print(f"Selecionados : {total_selected}") + print("==============================================") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/Python/OAK/datasets/oak-fcc-3/_7_split.py b/Python/OAK/datasets/oak-fcc-3/_7_split.py index 648a2a930..efb328082 100644 --- a/Python/OAK/datasets/oak-fcc-3/_7_split.py +++ b/Python/OAK/datasets/oak-fcc-3/_7_split.py @@ -1,23 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + """ -_7_split_multi_source.py +_7_split_agri_teacher_v2.py -Split estratificado por grupo/família para OAK-FCC-3, com suporte a múltiplas -raízes de dataset e proteção contra vazamento de dados sintéticos/augmentados. +Split agrícola inteligente para OAK-FCC-3 / Teacher V2. -Por que este script existe? ---------------------------- -No fluxo atual temos, por exemplo: +Objetivos: +- VAL/TEST 100% reais. +- Sintéticos/augment/copy-paste somente no TRAIN. +- Máscara semântica é a fonte da verdade para o grupo real. +- Proteção por família para impedir leakage de augmentações. +- Agrupamento conservador de famílias visualmente quase idênticas. +- Estratificação por problema agrícola, não apenas pelo nome da pasta: + * grupo semântico real + * quantidade de erva + * proximidade/contato cana x erva +- Seleção DIVERSA das famílias de VAL/TEST (coreset/farthest-point), + para que a "prova" cubra o espaço de situações em vez de ser um random puro. +- Descritores usam máscara + preview +, opcionalmente, tensor multiespectral + já normalizado/preparado pelo pipeline. +- Gera auditoria completa para sabermos exatamente por que cada família caiu + em TRAIN/VAL/TEST. - dataset/1024x640/group -> dados reais normalizados - dataset/copypaste/group -> dados sintéticos copy/paste pós-normalização - -A validação precisa continuar 100% real. Então este script: - - usa dados reais para decidir o split por família - - manda aug/copy-paste/sintéticos somente para TRAIN - - opcionalmente só inclui sintéticos cuja família real caiu no TRAIN - - copia tensores, máscaras, masks auxiliares, metas, previews e visuals +A ideia pedagógica: + TRAIN = sala de aula ampla e diversa. + VAL = prova real, representativa e cobrindo os pontos difíceis. + TEST = prova final opcional, também 100% real. Estrutura esperada: //tensors/*.npy @@ -28,34 +37,41 @@ Estrutura esperada: //previews/*.png //visuals/*.png -Uso recomendado para real + copy/paste: ---------------------------------------- -python _7_split_multi_source.py ^ - --src-roots dataset/1024x640/group,dataset/copypaste/group ^ + +Teste +python .\_7_split.py --train 0.85 --val 0.15 --test 0 --strategy agri_diverse --merge-near-duplicate-families --dry-run +python .\_7_split.py --train 0.85 --val 0.15 --test 0 --strategy agri_diverse --merge-near-duplicate-families --use-tensor-features --dry-run + +Exemplo recomendado, somente dados reais: +python _7_split.py --train 0.85 --val 0.15 --test 0 --strategy agri_diverse --use-tensor-features --merge-near-duplicate-families --clear-dst + +Real + copy/paste: +python _7_split.py ^ + --src-roots dataset/1280x800/group,dataset/copypaste/group ^ --train-only-roots dataset/copypaste/group ^ - --train 0.7 --val 0.3 --test 0.0 ^ + --train 0.85 --val 0.15 --test 0 ^ + --strategy agri_diverse ^ --synthetic-train-only ^ --synthetic-respect-family-split ^ --clear-dst - -Com isso: - - dados reais vão para train/val conforme split - - copy/paste vai apenas para train - - copy/paste derivado de família que caiu em val/test é ignorado por padrão """ from __future__ import annotations -import os -import re +import argparse import csv import json -import shutil +import math +import os import random -import argparse -from dataclasses import dataclass +import re +import shutil +from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, List, Optional, Tuple, Any +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple + +import cv2 +import numpy as np # ============================================================ @@ -72,92 +88,88 @@ RESOLUCAO = tuple(CONFIG.get("resolucao", [1024, 640])) MULTI_HEAD = bool(CONFIG.get("multi_head", False)) TENSOR_EXT = ".npy" -MASK_NPY_SUFFIX = ".npy" +MASK_SUFFIX = ".npy" AUX_MASK_DIRS = [ "masks_vegetation", "masks_cana", ] -COPY_OPTIONAL_DIRS = [ - "metas", - "previews", - "visuals", -] +CLASS_ID_TO_NAME = { + 0: "chao", + 1: "cana", + 2: "erva", +} -IGNORE_FILENAMES = { - "normalize_manifest.csv", - "copy_paste_manifest.csv", - "copy_paste_rejected.csv", - "copy_paste_summary.json", - "audit_summary.json", - "audit_health.json", +GROUP_BY_CLASS_SET = { + frozenset({0}): "chao", + frozenset({1}): "cana", + frozenset({2}): "erva", + frozenset({0, 1}): "chao_cana", + frozenset({0, 2}): "chao_erva", + frozenset({1, 2}): "cana_erva", + frozenset({0, 1, 2}): "chao_cana_erva", } # ============================================================ -# Regex de família/origem +# Regex origem/família # ============================================================ RE_ORIGINAL_PREFIX = re.compile(r"^original_(.+)$", re.IGNORECASE) -# augmented_abc_augx_00 -> family abc RE_AUGMENTED_FAMILY = re.compile( r"^augmented_(.+?)(?:_aug[a-zA-Z0-9]*_\d+)?$", re.IGNORECASE, ) -RE_AUG_SUFFIX = re.compile(r"_aug[a-zA-Z0-9]*_\d+$", re.IGNORECASE) -# copy-paste: abc_cp_00 -> family abc -RE_COPYPASTE_SUFFIX = re.compile(r"(.+)_cp_\d+$", re.IGNORECASE) +RE_AUG_SUFFIX = re.compile( + r"_aug[a-zA-Z0-9]*_\d+$", + re.IGNORECASE, +) -# grupos copy/paste: chao_cana_copypaste -> split_group chao_cana -RE_GROUP_COPYPASTE_SUFFIX = re.compile(r"(.+)_copypaste$", re.IGNORECASE) +RE_COPYPASTE_SUFFIX = re.compile( + r"(.+)_cp_\d+$", + re.IGNORECASE, +) + +RE_GROUP_COPYPASTE_SUFFIX = re.compile( + r"(.+)_copypaste$", + re.IGNORECASE, +) # ============================================================ # Utilidades # ============================================================ -def garantir(p: str | Path) -> None: - os.makedirs(str(p), exist_ok=True) +def garantir(path: str | Path) -> None: + os.makedirs(str(path), exist_ok=True) -def limpar_dir(p: str | Path) -> None: - if os.path.isdir(str(p)): - shutil.rmtree(str(p)) - garantir(p) +def limpar_dir(path: str | Path) -> None: + if os.path.isdir(str(path)): + shutil.rmtree(str(path)) + garantir(path) -def norm_path(p: str | Path) -> str: - return os.path.normcase(os.path.abspath(str(p))) +def norm_path(path: str | Path) -> str: + return os.path.normcase(os.path.abspath(str(path))) -def parse_csv_list(s: Optional[str]) -> List[str]: - if not s: +def parse_csv_list(value: Optional[str]) -> List[str]: + if not value: return [] - return [x.strip() for x in str(s).split(",") if x.strip()] + return [x.strip() for x in str(value).split(",") if x.strip()] -def parse_map(s: str, value_type=int) -> Dict[str, Any]: - out: Dict[str, Any] = {} - if not s: - return out - for item in s.split(","): - item = item.strip() - if not item: - continue - if ":" not in item: - raise ValueError(f"Item de mapa inválido: {item}. Use grupo:valor") - k, v = item.split(":", 1) - out[k.strip()] = value_type(v.strip()) - return out - - -def load_json_safe(path: str | Path) -> Dict[str, Any]: +def load_json_safe(path: Optional[str | Path]) -> Dict[str, Any]: + if not path: + return {} try: with open(path, "r", encoding="utf-8") as f: - return json.load(f) + data = json.load(f) + return data if isinstance(data, dict) else {} except Exception: return {} @@ -170,34 +182,36 @@ def save_json(path: str | Path, data: Dict[str, Any]) -> None: def lista_grupos(root: str | Path) -> List[str]: root = str(root) + if not os.path.isdir(root): return [] - out = [] - for g in sorted(os.listdir(root)): - gdir = os.path.join(root, g) - if not os.path.isdir(gdir): + result = [] + + for group in sorted(os.listdir(root)): + group_dir = os.path.join(root, group) + + if not os.path.isdir(group_dir): continue + if ( - os.path.isdir(os.path.join(gdir, "tensors")) - and os.path.isdir(os.path.join(gdir, "masks")) + os.path.isdir(os.path.join(group_dir, "tensors")) + and os.path.isdir(os.path.join(group_dir, "masks")) ): - out.append(g) - return out + result.append(group) + + return result def listar_tensors(tensor_dir: str | Path) -> List[str]: if not os.path.isdir(str(tensor_dir)): return [] - return sorted([ - f for f in os.listdir(str(tensor_dir)) + + return sorted( + f + for f in os.listdir(str(tensor_dir)) if f.lower().endswith(TENSOR_EXT) - ]) - - -def mask_npy_from_tensor_name(tensor_name: str) -> str: - base, _ = os.path.splitext(tensor_name) - return base + MASK_NPY_SUFFIX + ) def base_no_ext(filename: str) -> str: @@ -205,49 +219,34 @@ def base_no_ext(filename: str) -> str: def normalize_group_for_split(group_name: str) -> str: - """ - chao_cana_copypaste -> chao_cana - outros ficam iguais. - """ m = RE_GROUP_COPYPASTE_SUFFIX.match(group_name) if m: return m.group(1) return group_name -def output_group_name(source_group: str, split_group: str, is_synthetic: bool, mode: str) -> str: - """ - mode: - source: mantém nome original do grupo da fonte - base: usa grupo normalizado do split - suffix: sintético vai para _synthetic, real fica base - """ +def output_group_name( + source_group: str, + actual_group: str, + is_synthetic: bool, + mode: str, +) -> str: if mode == "source": return source_group + if mode == "base": - return split_group + return actual_group + if mode == "suffix": - if is_synthetic: - return f"{split_group}_synthetic" - return split_group - return source_group + return f"{actual_group}_synthetic" if is_synthetic else actual_group + + return actual_group -def classify_source_and_family(filename_no_ext: str, meta: Optional[Dict[str, Any]] = None) -> Tuple[str, str, bool]: - """ - Retorna: - source: original | augmented | copypaste | synthetic | unknown - family: id da família para impedir vazamento - is_synthetic: True para aug/copy/synthetic - - Regras: - original_abc -> original, abc - augmented_abc_aug_00 -> augmented, abc - abc_aug_00 -> augmented, abc - abc_cp_00 -> copypaste, abc - meta.synthetic == true -> synthetic/copypaste, tenta family pelo receiver/base - abc -> unknown, abc, tratado como original real - """ +def classify_source_and_family( + filename_no_ext: str, + meta: Optional[Dict[str, Any]] = None, +) -> Tuple[str, str, bool]: meta = meta or {} m = RE_ORIGINAL_PREFIX.match(filename_no_ext) @@ -259,20 +258,19 @@ def classify_source_and_family(filename_no_ext: str, meta: Optional[Dict[str, An return "augmented", m.group(1), True if RE_AUG_SUFFIX.search(filename_no_ext): - fam = RE_AUG_SUFFIX.sub("", filename_no_ext) - return "augmented", fam, True + family = RE_AUG_SUFFIX.sub("", filename_no_ext) + return "augmented", family, True m = RE_COPYPASTE_SUFFIX.match(filename_no_ext) if m: return "copypaste", m.group(1), True - # Meta do copy/paste v1/v2. cp = meta.get("copy_paste_augmentation") if isinstance(cp, dict): - recv = cp.get("receiver") - if isinstance(recv, dict): - fam = str(recv.get("base") or filename_no_ext) - return "copypaste", fam, True + receiver = cp.get("receiver") + if isinstance(receiver, dict): + family = str(receiver.get("base") or filename_no_ext) + return "copypaste", family, True return "copypaste", filename_no_ext, True if bool(meta.get("synthetic", False)): @@ -281,6 +279,69 @@ def classify_source_and_family(filename_no_ext: str, meta: Optional[Dict[str, An return "unknown", filename_no_ext, False +def read_mask(path: str | Path) -> np.ndarray: + mask = np.load(str(path), mmap_mode="r") + arr = np.asarray(mask) + + if arr.ndim > 2: + arr = np.squeeze(arr) + + if arr.ndim != 2: + raise RuntimeError( + f"Máscara semântica deve ser HxW; veio {arr.shape}: {path}" + ) + + return arr + + +def infer_actual_group( + mask: np.ndarray, + presence_min_pixels: int, + presence_min_fraction: float, + ignore_ids: Sequence[int], +) -> Tuple[str, Tuple[int, ...]]: + ignore_set = set(int(x) for x in ignore_ids) + + unique_ids = set(int(x) for x in np.unique(mask).tolist()) + + unknown = sorted( + x + for x in unique_ids + if x not in CLASS_ID_TO_NAME and x not in ignore_set + ) + + if unknown: + raise RuntimeError( + f"Máscara possui IDs desconhecidos: {unknown}" + ) + + present = [] + n = int(mask.size) + + for class_id in sorted(CLASS_ID_TO_NAME): + count = int(np.count_nonzero(mask == class_id)) + frac = count / max(n, 1) + + if ( + count >= max(1, int(presence_min_pixels)) + and frac >= float(presence_min_fraction) + ): + present.append(class_id) + + group = GROUP_BY_CLASS_SET.get(frozenset(present)) + + if group is None: + raise RuntimeError( + f"Combinação de classes não mapeável: {present}" + ) + + return group, tuple(present) + + +# ============================================================ +# Dataclasses +# ============================================================ + @dataclass class Sample: src_root: str @@ -288,14 +349,13 @@ class Sample: source_root_train_only: bool source_group: str - split_group: str - output_group_default: str + split_group_folder: str tensor_path: str tensor_name: str base: str - mask_path: str + source: str family: str is_synthetic: bool @@ -304,26 +364,77 @@ class Sample: preview_path: Optional[str] visual_path: Optional[str] + actual_group: str = "" + present_ids: Tuple[int, ...] = field(default_factory=tuple) + + frac_chao: float = 0.0 + frac_cana: float = 0.0 + frac_erva: float = 0.0 + frac_ignore: float = 0.0 + + comp_cana: int = 0 + comp_erva: int = 0 + largest_cana_frac: float = 0.0 + largest_erva_frac: float = 0.0 + + weed_near_cane_frac: float = 0.0 + weed_contact_cane_frac: float = 0.0 + + brightness: float = 0.0 + contrast: float = 0.0 + saturation: float = 0.0 + dhash64: int = 0 + + feature_vector: Optional[np.ndarray] = None + stratum: str = "" + agri_risk: float = 0.0 + audit_error: str = "" + + +@dataclass +class FamilyUnit: + actual_group: str + family: str + leakage_id: str + samples: List[Sample] + + feature_vector: np.ndarray + stratum: str + agri_risk: float + + dhash64: int + frac_chao: float + frac_cana: float + frac_erva: float + + assigned_split: Optional[str] = None + assignment_reason: str = "" + # ============================================================ -# Coleta de amostras +# Coleta # ============================================================ def collect_samples_from_root( src_root: str | Path, train_only_roots_norm: set[str], - label: str = "", - groups_filter: Optional[set[str]] = None, + label: str, + groups_filter: Optional[set[str]], ) -> List[Sample]: src_root = str(src_root) - src_root_norm = norm_path(src_root) - root_train_only = src_root_norm in train_only_roots_norm - root_label = label or os.path.basename(os.path.normpath(src_root)) or src_root + root_norm = norm_path(src_root) + root_train_only = root_norm in train_only_roots_norm samples: List[Sample] = [] for group_name in lista_grupos(src_root): - if groups_filter and group_name not in groups_filter and normalize_group_for_split(group_name) not in groups_filter: + split_group_folder = normalize_group_for_split(group_name) + + if ( + groups_filter + and group_name not in groups_filter + and split_group_folder not in groups_filter + ): continue group_dir = os.path.join(src_root, group_name) @@ -333,333 +444,1532 @@ def collect_samples_from_root( preview_dir = os.path.join(group_dir, "previews") visual_dir = os.path.join(group_dir, "visuals") - split_group = normalize_group_for_split(group_name) - for tensor_name in listar_tensors(tensor_dir): base = base_no_ext(tensor_name) - mask_name = mask_npy_from_tensor_name(tensor_name) - tensor_path = os.path.join(tensor_dir, tensor_name) - mask_path = os.path.join(mask_dir, mask_name) - if not os.path.exists(mask_path): + tensor_path = os.path.join(tensor_dir, tensor_name) + mask_path = os.path.join(mask_dir, base + MASK_SUFFIX) + + if not os.path.isfile(mask_path): continue meta_path = os.path.join(meta_dir, base + ".json") - if not os.path.exists(meta_path): + if not os.path.isfile(meta_path): meta_path = None - meta = load_json_safe(meta_path) if meta_path else {} - source, family, is_synthetic = classify_source_and_family(base, meta) + meta = load_json_safe(meta_path) + source, family, is_synthetic = classify_source_and_family( + base, + meta, + ) preview_path = None for ext in (".png", ".jpg", ".jpeg"): - cand = os.path.join(preview_dir, base + ext) - if os.path.exists(cand): - preview_path = cand + candidate = os.path.join(preview_dir, base + ext) + if os.path.isfile(candidate): + preview_path = candidate break visual_path = None for ext in (".png", ".jpg", ".jpeg"): - cand = os.path.join(visual_dir, base + "_debug" + ext) - if os.path.exists(cand): - visual_path = cand - break - cand2 = os.path.join(visual_dir, base + ext) - if os.path.exists(cand2): - visual_path = cand2 + candidate = os.path.join( + visual_dir, + base + "_debug" + ext, + ) + if os.path.isfile(candidate): + visual_path = candidate break - samples.append(Sample( - src_root=src_root, - source_root_label=root_label, - source_root_train_only=root_train_only, + candidate = os.path.join( + visual_dir, + base + ext, + ) + if os.path.isfile(candidate): + visual_path = candidate + break - source_group=group_name, - split_group=split_group, - output_group_default=group_name, - - tensor_path=tensor_path, - tensor_name=tensor_name, - base=base, - - mask_path=mask_path, - source=source, - family=family, - is_synthetic=is_synthetic, - - meta_path=meta_path, - preview_path=preview_path, - visual_path=visual_path, - )) + samples.append( + Sample( + src_root=src_root, + source_root_label=label, + source_root_train_only=root_train_only, + source_group=group_name, + split_group_folder=split_group_folder, + tensor_path=tensor_path, + tensor_name=tensor_name, + base=base, + mask_path=mask_path, + source=source, + family=family, + is_synthetic=is_synthetic, + meta_path=meta_path, + preview_path=preview_path, + visual_path=visual_path, + ) + ) return samples def collect_all_samples( - src_roots: List[str], - train_only_roots: List[str], - groups: Optional[List[str]] = None, + src_roots: Sequence[str], + train_only_roots: Sequence[str], + groups: Optional[Sequence[str]], ) -> List[Sample]: - train_only_norm = {norm_path(p) for p in train_only_roots} + train_only_norm = { + norm_path(x) + for x in train_only_roots + } + groups_filter = set(groups) if groups else None - all_samples: List[Sample] = [] + result = [] + for i, root in enumerate(src_roots): - label = f"root{i}" - samples = collect_samples_from_root( - src_root=root, - train_only_roots_norm=train_only_norm, - label=label, - groups_filter=groups_filter, + result.extend( + collect_samples_from_root( + src_root=root, + train_only_roots_norm=train_only_norm, + label=f"root{i}", + groups_filter=groups_filter, + ) ) - all_samples.extend(samples) - return all_samples + return result # ============================================================ -# Split por família real +# Features agrícolas # ============================================================ -def allocate_counts(n: int, p_train: float, p_val: float, p_test: float, min_train: int, min_val: int, min_test: int) -> Tuple[int, int, int]: - n_train = int(round(n * p_train)) - n_val = int(round(n * p_val)) - n_test = n - n_train - n_val +def connected_component_stats(binary: np.ndarray) -> Tuple[int, float]: + binary = np.ascontiguousarray( + binary.astype(np.uint8) + ) - if n_test < 0: - excesso = -n_test - take_train = min(excesso, max(0, n_train)) - n_train -= take_train - excesso -= take_train - if excesso > 0: - take_val = min(excesso, max(0, n_val)) - n_val -= take_val - excesso -= take_val - n_test = 0 + n, _labels, stats, _centroids = cv2.connectedComponentsWithStats( + binary, + connectivity=8, + ) - min_sum = min_train + min_val + min_test + if n <= 1: + return 0, 0.0 - if n >= min_sum: - n_train = max(n_train, min_train) - n_val = max(n_val, min_val) - n_test = max(n_test, min_test) + areas = stats[1:, cv2.CC_STAT_AREA].astype(np.float64) - total = n_train + n_val + n_test - while total > n: - if n_test > min_test: - n_test -= 1 - elif n_val > min_val: - n_val -= 1 - elif n_train > min_train: - n_train -= 1 + return ( + int(len(areas)), + float(np.max(areas) / max(binary.size, 1)), + ) + + +def weed_cane_relation( + mask_small: np.ndarray, + near_radius_frac: float, +) -> Tuple[float, float]: + cane = mask_small == 1 + weed = mask_small == 2 + + if not np.any(cane) or not np.any(weed): + return 0.0, 0.0 + + h, w = mask_small.shape + radius = max( + 1, + int(round(min(h, w) * float(near_radius_frac))), + ) + + dist = cv2.distanceTransform( + (~cane).astype(np.uint8), + cv2.DIST_L2, + 3, + ) + + weed_dist = dist[weed] + + near_frac = ( + float(np.mean(weed_dist <= radius)) + if weed_dist.size + else 0.0 + ) + + kernel = np.ones((3, 3), np.uint8) + cane_dilated = cv2.dilate( + cane.astype(np.uint8), + kernel, + iterations=1, + ) > 0 + + weed_count = int(np.count_nonzero(weed)) + contact = int(np.count_nonzero(weed & cane_dilated)) + + contact_frac = contact / max(weed_count, 1) + + return float(near_frac), float(contact_frac) + + +def grid_fraction( + binary: np.ndarray, + rows: int = 4, + cols: int = 6, +) -> np.ndarray: + h, w = binary.shape + result = [] + + for gy in range(rows): + y0 = int(round(gy * h / rows)) + y1 = int(round((gy + 1) * h / rows)) + + for gx in range(cols): + x0 = int(round(gx * w / cols)) + x1 = int(round((gx + 1) * w / cols)) + + tile = binary[y0:y1, x0:x1] + + result.append( + float(np.mean(tile)) + if tile.size + else 0.0 + ) + + return np.asarray( + result, + dtype=np.float32, + ) + + +def dhash64(img_bgr: np.ndarray) -> int: + gray = cv2.cvtColor( + img_bgr, + cv2.COLOR_BGR2GRAY, + ) + + small = cv2.resize( + gray, + (9, 8), + interpolation=cv2.INTER_AREA, + ) + + diff = small[:, 1:] > small[:, :-1] + + value = 0 + for bit in diff.flatten(): + value = (value << 1) | int(bool(bit)) + + return int(value) + + +def preview_features( + preview_path: Optional[str], +) -> Tuple[np.ndarray, float, float, float, int]: + if not preview_path: + return ( + np.zeros(35, dtype=np.float32), + 0.0, + 0.0, + 0.0, + 0, + ) + + img = cv2.imread( + preview_path, + cv2.IMREAD_COLOR, + ) + + if img is None: + return ( + np.zeros(35, dtype=np.float32), + 0.0, + 0.0, + 0.0, + 0, + ) + + hsv = cv2.cvtColor( + img, + cv2.COLOR_BGR2HSV, + ) + + brightness = float( + np.mean(hsv[..., 2]) / 255.0 + ) + + saturation = float( + np.mean(hsv[..., 1]) / 255.0 + ) + + gray = cv2.cvtColor( + img, + cv2.COLOR_BGR2GRAY, + ) + + contrast = float( + np.std(gray) / 255.0 + ) + + hist_h = cv2.calcHist( + [hsv], + [0], + None, + [12], + [0, 180], + ).flatten() + + hist_s = cv2.calcHist( + [hsv], + [1], + None, + [8], + [0, 256], + ).flatten() + + hist_v = cv2.calcHist( + [hsv], + [2], + None, + [8], + [0, 256], + ).flatten() + + hist = np.concatenate([ + hist_h, + hist_s, + hist_v, + ]).astype(np.float32) + + hist_sum = float(hist.sum()) + if hist_sum > 0: + hist /= hist_sum + + low = cv2.resize( + gray, + (7, 1), + interpolation=cv2.INTER_AREA, + ).astype(np.float32).flatten() / 255.0 + + feat = np.concatenate([ + hist, + low, + ]).astype(np.float32) + + return ( + feat, + brightness, + contrast, + saturation, + dhash64(img), + ) + + +def tensor_summary_features( + tensor_path: str, + mask: np.ndarray, + enabled: bool, + stride: int, +) -> np.ndarray: + """ + Features multiespectrais baratas. + + Usa mmap + subamostragem espacial. + Para cada canal disponível: + mean, std, p10, p50, p90 + + Para os 5 primeiros canais físicos, acrescenta: + média na CANA e média na ERVA + + Também calcula NDVI/NDRE aproximados a partir de: + R=0, RE=3, NIR=4 + quando C>=5. + + Não depende de o treino usar 5 ou 7 canais. + """ + if not enabled: + return np.zeros(49, dtype=np.float32) + + try: + arr = np.load( + tensor_path, + mmap_mode="r", + ) + + x = np.asarray(arr) + + if x.ndim == 4 and x.shape[0] == 1: + x = x[0] + + if x.ndim != 3: + raise RuntimeError( + f"tensor não CHW: {x.shape}" + ) + + c, h, w = x.shape + + step = max(1, int(stride)) + + ys = np.arange( + 0, + h, + step, + dtype=np.int64, + ) + + xs = np.arange( + 0, + w, + step, + dtype=np.int64, + ) + + x_small = np.asarray( + x[:, ys[:, None], xs[None, :]], + dtype=np.float32, + ) + + mask_small = cv2.resize( + mask.astype(np.uint8), + (len(xs), len(ys)), + interpolation=cv2.INTER_NEAREST, + ) + + feats = [] + + max_channels = min(c, 7) + + for ch in range(7): + if ch < max_channels: + values = x_small[ch].reshape(-1) + values = values[np.isfinite(values)] + + if values.size: + feats.extend([ + float(np.mean(values)), + float(np.std(values)), + float(np.percentile(values, 10)), + float(np.percentile(values, 50)), + float(np.percentile(values, 90)), + ]) + else: + feats.extend([0.0] * 5) else: - break - total = n_train + n_val + n_test + feats.extend([0.0] * 5) - while total < n: - if n_train - min_train <= n_val - min_val: - n_train += 1 - else: - n_val += 1 - total = n_train + n_val + n_test - else: - n_train = min(n, max(1, min_train)) - resto = n - n_train - n_val = max(0, min(resto, min_val)) - n_test = max(0, resto - n_val) + # médias por classe dos 5 canais físicos + for class_id in (1, 2): + class_mask = mask_small == class_id - diff = n - (n_train + n_val + n_test) - if diff != 0: - if diff > 0: - take = min(diff, n - n_train) - n_train += take - diff -= take - if diff > 0: - n_val += diff + for ch in range(5): + if ch < c and np.any(class_mask): + vals = x_small[ch][class_mask] + vals = vals[np.isfinite(vals)] + feats.append( + float(np.mean(vals)) + if vals.size + else 0.0 + ) + else: + feats.append(0.0) + + # NDVI / NDRE globais do Raw5 físico. + if c >= 5: + r = x_small[0] + re = x_small[3] + nir = x_small[4] + + def nd(a, b): + den = a + b + out = np.zeros_like(a, dtype=np.float32) + np.divide( + a - b, + den, + out=out, + where=np.abs(den) > 1e-6, + ) + return np.nan_to_num( + out, + nan=0.0, + posinf=1.0, + neginf=-1.0, + ) + + ndvi = nd(nir, r) + ndre = nd(nir, re) + + feats.extend([ + float(np.mean(ndvi)), + float(np.std(ndvi)), + float(np.mean(ndre)), + float(np.std(ndre)), + ]) else: - diff = -diff - take = min(diff, n_test) - n_test -= take - diff -= take - if diff > 0: - n_val -= diff + feats.extend([0.0] * 4) - return n_train, n_val, n_test + # 35 + 10 + 4 = 49 + return np.asarray( + feats, + dtype=np.float32, + ) + + except Exception: + return np.zeros( + 49, + dtype=np.float32, + ) -def build_family_split( - samples: List[Sample], +def target_size_bin( + frac_erva: float, + tiny: float, + small: float, + medium: float, +) -> str: + if frac_erva <= 0.0: + return "none" + if frac_erva < tiny: + return "tiny" + if frac_erva < small: + return "small" + if frac_erva < medium: + return "medium" + return "large" + + +def compute_agri_risk( + frac_cana: float, + frac_erva: float, + weed_near_cane_frac: float, + weed_contact_cane_frac: float, +) -> float: + """ + Risco agrícola relativo. + + Não é score de qualidade. + É somente uma forma de marcar casos que são "prova difícil": + - cana + erva simultâneas; + - erva perto/encostada na cana; + - target pequeno em presença de cana. + """ + has_cana = frac_cana > 0.0 + has_erva = frac_erva > 0.0 + + score = 0.0 + + if has_cana and has_erva: + score += 0.35 + + score += 0.30 * float(weed_near_cane_frac) + score += 0.20 * float(weed_contact_cane_frac) + + if has_cana and has_erva and frac_erva < 0.02: + score += 0.15 + + return float( + max(0.0, min(score, 1.0)) + ) + + +def analyze_sample( + sample: Sample, + args, +) -> None: + mask = read_mask( + sample.mask_path + ) + + actual_group, present_ids = infer_actual_group( + mask=mask, + presence_min_pixels=args.presence_min_pixels, + presence_min_fraction=args.presence_min_fraction, + ignore_ids=args.ignore_ids, + ) + + sample.actual_group = actual_group + sample.present_ids = present_ids + + sample.frac_chao = float( + np.mean(mask == 0) + ) + sample.frac_cana = float( + np.mean(mask == 1) + ) + sample.frac_erva = float( + np.mean(mask == 2) + ) + + sample.frac_ignore = float( + sum( + np.mean(mask == int(i)) + for i in args.ignore_ids + ) + ) + + # Para morfologia, reduz bastante. A feature é relativa, não pixel-exata. + small_w = min(320, mask.shape[1]) + small_h = max( + 1, + int(round( + mask.shape[0] + * small_w + / max(mask.shape[1], 1) + )), + ) + + mask_small = cv2.resize( + mask.astype(np.uint8), + (small_w, small_h), + interpolation=cv2.INTER_NEAREST, + ) + + ( + sample.comp_cana, + sample.largest_cana_frac, + ) = connected_component_stats( + mask_small == 1 + ) + + ( + sample.comp_erva, + sample.largest_erva_frac, + ) = connected_component_stats( + mask_small == 2 + ) + + ( + sample.weed_near_cane_frac, + sample.weed_contact_cane_frac, + ) = weed_cane_relation( + mask_small, + args.near_radius_frac, + ) + + ( + visual_feat, + sample.brightness, + sample.contrast, + sample.saturation, + sample.dhash64, + ) = preview_features( + sample.preview_path + ) + + tensor_feat = tensor_summary_features( + tensor_path=sample.tensor_path, + mask=mask, + enabled=bool(args.use_tensor_features), + stride=args.tensor_feature_stride, + ) + + cana_grid = grid_fraction( + mask_small == 1, + ) + + erva_grid = grid_fraction( + mask_small == 2, + ) + + size_bin = target_size_bin( + sample.frac_erva, + args.target_tiny_frac, + args.target_small_frac, + args.target_medium_frac, + ) + + if ( + sample.frac_cana > 0 + and sample.frac_erva > 0 + and sample.weed_near_cane_frac >= args.close_weed_cane_threshold + ): + proximity_bin = "close" + elif sample.frac_cana > 0 and sample.frac_erva > 0: + proximity_bin = "separate" + else: + proximity_bin = "na" + + sample.stratum = ( + f"{sample.actual_group}" + f"|target={size_bin}" + f"|caneweed={proximity_bin}" + ) + + sample.agri_risk = compute_agri_risk( + frac_cana=sample.frac_cana, + frac_erva=sample.frac_erva, + weed_near_cane_frac=sample.weed_near_cane_frac, + weed_contact_cane_frac=sample.weed_contact_cane_frac, + ) + + scalar_feat = np.asarray([ + sample.frac_chao, + sample.frac_cana, + sample.frac_erva, + sample.frac_ignore, + math.log1p(sample.comp_cana) / 6.0, + math.log1p(sample.comp_erva) / 6.0, + sample.largest_cana_frac, + sample.largest_erva_frac, + sample.weed_near_cane_frac, + sample.weed_contact_cane_frac, + sample.brightness, + sample.contrast, + sample.saturation, + sample.agri_risk, + ], dtype=np.float32) + + sample.feature_vector = np.concatenate([ + scalar_feat, + cana_grid, + erva_grid, + visual_feat, + tensor_feat, + ]).astype(np.float32) + + +# ============================================================ +# Família / near-duplicate leakage +# ============================================================ + +def hamming64(a: int, b: int) -> int: + return int( + (int(a) ^ int(b)).bit_count() + ) + + +class UnionFind: + def __init__(self, n: int): + self.parent = list(range(n)) + self.rank = [0] * n + + def find(self, x: int) -> int: + while self.parent[x] != x: + self.parent[x] = self.parent[ + self.parent[x] + ] + x = self.parent[x] + return x + + def union(self, a: int, b: int) -> None: + ra = self.find(a) + rb = self.find(b) + + if ra == rb: + return + + if self.rank[ra] < self.rank[rb]: + ra, rb = rb, ra + + self.parent[rb] = ra + + if self.rank[ra] == self.rank[rb]: + self.rank[ra] += 1 + + +def build_real_family_units( + samples: Sequence[Sample], +) -> List[FamilyUnit]: + grouped: Dict[Tuple[str, str], List[Sample]] = {} + + for s in samples: + if s.is_synthetic: + continue + if s.source_root_train_only: + continue + + grouped.setdefault( + (s.actual_group, s.family), + [], + ).append(s) + + units = [] + + for (group, family), members in grouped.items(): + vectors = np.stack([ + s.feature_vector + for s in members + ], axis=0) + + representative = max( + members, + key=lambda s: s.agri_risk, + ) + + stratum_counts: Dict[str, int] = {} + for s in members: + stratum_counts[s.stratum] = ( + stratum_counts.get(s.stratum, 0) + 1 + ) + + stratum = max( + stratum_counts.items(), + key=lambda kv: (kv[1], kv[0]), + )[0] + + units.append( + FamilyUnit( + actual_group=group, + family=family, + leakage_id=family, + samples=list(members), + feature_vector=np.mean( + vectors, + axis=0, + ).astype(np.float32), + stratum=stratum, + agri_risk=float( + max(s.agri_risk for s in members) + ), + dhash64=representative.dhash64, + frac_chao=float( + np.mean([s.frac_chao for s in members]) + ), + frac_cana=float( + np.mean([s.frac_cana for s in members]) + ), + frac_erva=float( + np.mean([s.frac_erva for s in members]) + ), + ) + ) + + return units + + +def merge_near_duplicate_family_units( + units: Sequence[FamilyUnit], + enabled: bool, + max_hamming: int, + mask_l1: float, +) -> Tuple[List[FamilyUnit], Dict[Tuple[str, str], str], int]: + """ + Une famílias reais muito semelhantes para que não possam cair uma no + TRAIN e outra no VAL. + + É propositalmente conservador. + Só compara famílias do mesmo grupo real. + """ + units = list(units) + + if not enabled or len(units) <= 1: + mapping = { + (u.actual_group, u.family): u.leakage_id + for u in units + } + return units, mapping, 0 + + result_units = [] + family_to_leakage: Dict[Tuple[str, str], str] = {} + merged_count = 0 + + by_group: Dict[str, List[FamilyUnit]] = {} + + for u in units: + by_group.setdefault( + u.actual_group, + [], + ).append(u) + + for group, group_units in by_group.items(): + n = len(group_units) + uf = UnionFind(n) + + # bucket conservador por frações de classes para evitar O(n²) total + buckets: Dict[Tuple[int, int, int], List[int]] = {} + + for i, u in enumerate(group_units): + key = ( + int(round(u.frac_chao / 0.02)), + int(round(u.frac_cana / 0.02)), + int(round(u.frac_erva / 0.02)), + ) + buckets.setdefault(key, []).append(i) + + for key, indices in buckets.items(): + # compara bucket e vizinhos imediatos de composição + candidate_indices = [] + + a0, a1, a2 = key + + for d0 in (-1, 0, 1): + for d1 in (-1, 0, 1): + for d2 in (-1, 0, 1): + candidate_indices.extend( + buckets.get( + (a0 + d0, a1 + d1, a2 + d2), + [], + ) + ) + + candidate_indices = sorted( + set(candidate_indices) + ) + + for i in indices: + ui = group_units[i] + + for j in candidate_indices: + if j <= i: + continue + + uj = group_units[j] + + if ( + hamming64( + ui.dhash64, + uj.dhash64, + ) + > int(max_hamming) + ): + continue + + l1 = ( + abs(ui.frac_chao - uj.frac_chao) + + abs(ui.frac_cana - uj.frac_cana) + + abs(ui.frac_erva - uj.frac_erva) + ) + + if l1 > float(mask_l1): + continue + + uf.union(i, j) + + clusters: Dict[int, List[FamilyUnit]] = {} + + for i, u in enumerate(group_units): + clusters.setdefault( + uf.find(i), + [], + ).append(u) + + for cluster_index, members in enumerate(clusters.values()): + if len(members) > 1: + merged_count += len(members) - 1 + + leakage_id = ( + members[0].family + if len(members) == 1 + else f"near_{group}_{cluster_index:05d}" + ) + + all_samples = [] + for m in members: + all_samples.extend( + m.samples + ) + family_to_leakage[ + (group, m.family) + ] = leakage_id + + vectors = np.stack([ + m.feature_vector + for m in members + ], axis=0) + + hardest = max( + members, + key=lambda m: m.agri_risk, + ) + + stratum_counts: Dict[str, int] = {} + for m in members: + stratum_counts[m.stratum] = ( + stratum_counts.get(m.stratum, 0) + 1 + ) + + stratum = max( + stratum_counts.items(), + key=lambda kv: (kv[1], kv[0]), + )[0] + + result_units.append( + FamilyUnit( + actual_group=group, + family=members[0].family, + leakage_id=leakage_id, + samples=all_samples, + feature_vector=np.mean( + vectors, + axis=0, + ).astype(np.float32), + stratum=stratum, + agri_risk=float( + max(m.agri_risk for m in members) + ), + dhash64=hardest.dhash64, + frac_chao=float( + np.mean([m.frac_chao for m in members]) + ), + frac_cana=float( + np.mean([m.frac_cana for m in members]) + ), + frac_erva=float( + np.mean([m.frac_erva for m in members]) + ), + ) + ) + + return ( + result_units, + family_to_leakage, + merged_count, + ) + + +# ============================================================ +# Seleção diversa de VAL/TEST +# ============================================================ + +def robust_standardize( + features: np.ndarray, +) -> np.ndarray: + x = np.asarray( + features, + dtype=np.float32, + ) + + med = np.median( + x, + axis=0, + ) + + q25 = np.percentile( + x, + 25, + axis=0, + ) + + q75 = np.percentile( + x, + 75, + axis=0, + ) + + scale = q75 - q25 + std = np.std( + x, + axis=0, + ) + + scale = np.where( + scale > 1e-6, + scale, + std, + ) + + scale = np.where( + scale > 1e-6, + scale, + 1.0, + ) + + z = (x - med) / scale + + return np.clip( + z, + -8.0, + 8.0, + ).astype(np.float32) + + +def diverse_order( + units: Sequence[FamilyUnit], + seed: int, +) -> List[int]: + """ + Ordem k-center: + começa pela unidade mais próxima do centro do estrato + e depois vai pegando a mais distante do conjunto já coberto. + + Isso produz uma prova representativa + ampla, não somente casos extremos. + """ + units = list(units) + + if len(units) <= 1: + return list(range(len(units))) + + x = np.stack([ + u.feature_vector + for u in units + ], axis=0) + + z = robust_standardize(x) + + centroid = np.mean( + z, + axis=0, + ) + + dist_center = np.sum( + (z - centroid) ** 2, + axis=1, + ) + + rng = np.random.default_rng( + int(seed) + ) + + jitter = rng.uniform( + 0.0, + 1e-8, + size=len(units), + ) + + first = int( + np.argmin( + dist_center + jitter + ) + ) + + order = [first] + + selected = np.zeros( + len(units), + dtype=bool, + ) + selected[first] = True + + delta = z - z[first] + + min_dist_sq = np.sum( + delta * delta, + axis=1, + ) + + min_dist_sq[first] = 0.0 + + while len(order) < len(units): + score = min_dist_sq.copy() + score[selected] = -1.0 + + idx = int( + np.argmax( + score + jitter + ) + ) + + order.append(idx) + selected[idx] = True + + delta = z - z[idx] + + dist_sq = np.sum( + delta * delta, + axis=1, + ) + + min_dist_sq = np.minimum( + min_dist_sq, + dist_sq, + ) + + return order + + +def allocate_stratum_counts( + n: int, + p_train: float, + p_val: float, + p_test: float, +) -> Tuple[int, int, int]: + if n <= 0: + return 0, 0, 0 + + n_val = int(round( + n * p_val + )) + + n_test = int(round( + n * p_test + )) + + # Garante pelo menos uma família de val em estratos minimamente grandes. + if p_val > 0 and n >= 5: + n_val = max( + 1, + n_val, + ) + + if p_test > 0 and n >= 10: + n_test = max( + 1, + n_test, + ) + + while n_val + n_test >= n: + if n_test > 0: + n_test -= 1 + elif n_val > 0: + n_val -= 1 + else: + break + + n_train = n - n_val - n_test + + return ( + n_train, + n_val, + n_test, + ) + + +def assign_agri_diverse_split( + units: Sequence[FamilyUnit], p_train: float, p_val: float, p_test: float, seed: int, - mins: Dict[str, int], - caps_map: Optional[Dict[str, int]] = None, -) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, int]]]: +) -> Dict[Tuple[str, str], str]: """ - Usa apenas amostras reais/originais para decidir a partição das famílias. + Estratifica por "situação agrícola" e escolhe VAL/TEST por diversidade. - Retorna: - family_split[split_group][family] = train|val|test - group_summary[split_group] = contagens de famílias + Dentro de cada estrato: + - escolhe representantes diversos para VAL; + - escolhe representantes ainda não usados para TEST; + - restante vai para TRAIN. """ - caps_map = caps_map or {} + strata: Dict[str, List[FamilyUnit]] = {} - families_by_group: Dict[str, set[str]] = {} + for u in units: + strata.setdefault( + u.stratum, + [], + ).append(u) - for s in samples: - if s.source_root_train_only: - continue - if s.is_synthetic: - continue - if s.source in ("augmented", "copypaste", "synthetic"): - continue + assignment: Dict[Tuple[str, str], str] = {} - families_by_group.setdefault(s.split_group, set()).add(s.family) + for stratum_index, stratum in enumerate(sorted(strata)): + members = strata[stratum] - family_split: Dict[str, Dict[str, str]] = {} - group_summary: Dict[str, Dict[str, int]] = {} - - for group_name in sorted(families_by_group.keys()): - fams = sorted(families_by_group[group_name]) - rng = random.Random(seed) - rng.shuffle(fams) - - total_familias = len(fams) - if total_familias == 0: - group_summary[group_name] = {"familias": 0, "train_families": 0, "val_families": 0, "test_families": 0} - continue - - n_tr, n_va, n_te = allocate_counts( - total_familias, + n_train, n_val, n_test = allocate_stratum_counts( + len(members), p_train, p_val, p_test, - mins["train"], - mins["val"], - mins["test"], ) - fam_train = set(fams[:n_tr]) - fam_val = set(fams[n_tr:n_tr + n_va]) - fam_test = set(fams[n_tr + n_va:n_tr + n_va + n_te]) + order = diverse_order( + members, + seed=seed + stratum_index * 9973, + ) - if group_name in caps_map: - cap = int(caps_map[group_name]) - if len(fam_train) > cap: - fam_list = list(fam_train) - rng.shuffle(fam_list) - kept = set(fam_list[:cap]) - dropped = set(fam_list[cap:]) - fam_train = kept - print( - f"[{group_name}] cap-train-families={cap} → " - f"mantidas {len(kept)} famílias, descartadas {len(dropped)} do TRAIN" + # Distribui as posições mais representativas/diversas para a prova. + val_idx = set( + order[:n_val] + ) + + test_idx = set( + order[n_val:n_val + n_test] + ) + + for i, unit in enumerate(members): + if i in val_idx: + split = "val" + reason = "agri_diverse_val" + elif i in test_idx: + split = "test" + reason = "agri_diverse_test" + else: + split = "train" + reason = "agri_diverse_train" + + unit.assigned_split = split + unit.assignment_reason = reason + + assignment[ + (unit.actual_group, unit.leakage_id) + ] = split + + return assignment + + +def assign_random_split( + units: Sequence[FamilyUnit], + p_train: float, + p_val: float, + p_test: float, + seed: int, +) -> Dict[Tuple[str, str], str]: + by_group: Dict[str, List[FamilyUnit]] = {} + + for u in units: + by_group.setdefault( + u.actual_group, + [], + ).append(u) + + assignment = {} + + for group_index, group in enumerate(sorted(by_group)): + members = list( + by_group[group] + ) + + rng = random.Random( + seed + group_index * 1009 + ) + rng.shuffle(members) + + n = len(members) + + n_train = int(round( + n * p_train + )) + + n_val = int(round( + n * p_val + )) + + n_test = ( + n + - n_train + - n_val + ) + + if n_test < 0: + n_test = 0 + n_train = max( + 0, + n - n_val, + ) + + for i, unit in enumerate(members): + if i < n_train: + split = "train" + elif i < n_train + n_val: + split = "val" + else: + split = "test" + + unit.assigned_split = split + unit.assignment_reason = "random_group" + + assignment[ + (unit.actual_group, unit.leakage_id) + ] = split + + return assignment + + +# ============================================================ +# Aplicar split aos samples +# ============================================================ + +def decide_sample_split( + sample: Sample, + family_to_leakage: Dict[Tuple[str, str], str], + assignment: Dict[Tuple[str, str], str], + args, +) -> Tuple[Optional[str], str]: + # Para dados reais, a máscara define o grupo/família de leakage. + # Para sintéticos/copy-paste, a família deve respeitar o split da + # FAMÍLIA REAL DE ORIGEM/receiver. O grupo sintético pode mudar depois + # do copy-paste (ex.: chao_cana -> chao_cana_erva), então usar + # actual_group aqui faria o filho perder o vínculo com o pai real. + family_lookup_group = ( + sample.actual_group + if not sample.is_synthetic + else normalize_group_for_split(sample.source_group) + ) + + leakage_id = family_to_leakage.get( + (family_lookup_group, sample.family), + sample.family, + ) + + assigned = assignment.get( + (family_lookup_group, leakage_id) + ) + + is_train_only_candidate = ( + sample.source_root_train_only + or ( + bool(args.synthetic_train_only) + and sample.is_synthetic + ) + or sample.source in ( + "augmented", + "copypaste", + "synthetic", + ) + ) + + if is_train_only_candidate: + if bool(args.synthetic_respect_family_split): + if assigned is None: + if bool(args.allow_orphan_synthetic_train): + return ( + "train", + "train_only_orphan_allowed", + ) + + return ( + None, + "skip_train_only_orphan_no_real_family", ) - group_map: Dict[str, str] = {} - for f in fam_train: - group_map[f] = "train" - for f in fam_val: - group_map[f] = "val" - for f in fam_test: - group_map[f] = "test" + if assigned != "train": + return ( + None, + f"skip_train_only_family_assigned_{assigned}", + ) - family_split[group_name] = group_map - group_summary[group_name] = { - "familias": total_familias, - "train_families": len(fam_train), - "val_families": len(fam_val), - "test_families": len(fam_test), - } + return ( + "train", + "train_only_family_train", + ) - return family_split, group_summary + return ( + "train", + "train_only_forced", + ) + + if assigned is None: + return ( + None, + "skip_real_no_family_assignment", + ) + + return ( + assigned, + f"real_assigned_{assigned}", + ) # ============================================================ # Cópia # ============================================================ -def copy_optional_file(src_path: Optional[str], dst_dir: str, dst_base: str, suffix: str = "") -> Optional[str]: - if not src_path or not os.path.exists(src_path): - return None - - garantir(dst_dir) - ext = os.path.splitext(src_path)[1] - dst = os.path.join(dst_dir, dst_base + suffix + ext) - shutil.copy2(src_path, dst) - return dst - - -def copy_named_optional(src_dir: str, dst_dir: str, base: str, ext: str) -> Optional[str]: - src = os.path.join(src_dir, base + ext) - if not os.path.exists(src): - return None - garantir(dst_dir) - dst = os.path.join(dst_dir, base + ext) - shutil.copy2(src, dst) - return dst - - def copiar_sample( - s: Sample, + sample: Sample, dst_root: str, split_name: str, out_group_name: str, - copy_meta_preview: bool = True, - copy_visuals: bool = True, + copy_meta_preview: bool, + copy_visuals: bool, ) -> Optional[Dict[str, Any]]: - dst_group_dir = os.path.join(dst_root, split_name, "group", out_group_name) + dst_group_dir = os.path.join( + dst_root, + split_name, + "group", + out_group_name, + ) - dst_tensor_dir = os.path.join(dst_group_dir, "tensors") - dst_mask_dir = os.path.join(dst_group_dir, "masks") - dst_meta_dir = os.path.join(dst_group_dir, "metas") - dst_preview_dir = os.path.join(dst_group_dir, "previews") - dst_visual_dir = os.path.join(dst_group_dir, "visuals") + dst_tensor_dir = os.path.join( + dst_group_dir, + "tensors", + ) - garantir(dst_tensor_dir) - garantir(dst_mask_dir) + dst_mask_dir = os.path.join( + dst_group_dir, + "masks", + ) - tensor_dst = os.path.join(dst_tensor_dir, s.tensor_name) - mask_dst = os.path.join(dst_mask_dir, os.path.basename(s.mask_path)) + garantir( + dst_tensor_dir + ) + garantir( + dst_mask_dir + ) - if not (os.path.exists(s.tensor_path) and os.path.exists(s.mask_path)): + if not ( + os.path.isfile(sample.tensor_path) + and os.path.isfile(sample.mask_path) + ): return None - shutil.copy2(s.tensor_path, tensor_dst) - shutil.copy2(s.mask_path, mask_dst) + tensor_dst = os.path.join( + dst_tensor_dir, + sample.tensor_name, + ) - base = s.base + mask_dst = os.path.join( + dst_mask_dir, + os.path.basename(sample.mask_path), + ) + + shutil.copy2( + sample.tensor_path, + tensor_dst, + ) + + shutil.copy2( + sample.mask_path, + mask_dst, + ) + + base = sample.base + + mask_png_src = os.path.join( + os.path.dirname(sample.mask_path), + base + ".png", + ) - # mask debug PNG opcional - mask_png_src = os.path.join(os.path.dirname(s.mask_path), base + ".png") mask_png_dst = None - if os.path.exists(mask_png_src): - mask_png_dst = os.path.join(dst_mask_dir, base + ".png") - shutil.copy2(mask_png_src, mask_png_dst) - # aux masks + if os.path.isfile(mask_png_src): + mask_png_dst = os.path.join( + dst_mask_dir, + base + ".png", + ) + + shutil.copy2( + mask_png_src, + mask_png_dst, + ) + aux_masks: Dict[str, Dict[str, Optional[str]]] = {} - src_group_dir = os.path.join(s.src_root, s.source_group) + + src_group_dir = os.path.join( + sample.src_root, + sample.source_group, + ) for aux_dir in AUX_MASK_DIRS: - aux_src_dir = os.path.join(src_group_dir, aux_dir) - aux_dst_dir = os.path.join(dst_group_dir, aux_dir) + aux_src_dir = os.path.join( + src_group_dir, + aux_dir, + ) - aux_npy_src = os.path.join(aux_src_dir, base + ".npy") - aux_png_src = os.path.join(aux_src_dir, base + ".png") + aux_dst_dir = os.path.join( + dst_group_dir, + aux_dir, + ) + + aux_npy_src = os.path.join( + aux_src_dir, + base + ".npy", + ) + + aux_png_src = os.path.join( + aux_src_dir, + base + ".png", + ) aux_npy_dst = None aux_png_dst = None - if os.path.exists(aux_npy_src): - garantir(aux_dst_dir) - aux_npy_dst = os.path.join(aux_dst_dir, base + ".npy") - shutil.copy2(aux_npy_src, aux_npy_dst) + if os.path.isfile(aux_npy_src): + garantir( + aux_dst_dir + ) - if os.path.exists(aux_png_src): - aux_png_dst = os.path.join(aux_dst_dir, base + ".png") - shutil.copy2(aux_png_src, aux_png_dst) + aux_npy_dst = os.path.join( + aux_dst_dir, + base + ".npy", + ) + + shutil.copy2( + aux_npy_src, + aux_npy_dst, + ) + + if os.path.isfile(aux_png_src): + aux_png_dst = os.path.join( + aux_dst_dir, + base + ".png", + ) + + shutil.copy2( + aux_png_src, + aux_png_dst, + ) aux_masks[aux_dir] = { "npy": aux_npy_dst, @@ -668,8 +1978,8 @@ def copiar_sample( if MULTI_HEAD and aux_npy_dst is None: raise RuntimeError( - f"multi_head=true, mas máscara auxiliar ausente: " - f"{aux_dir}/{base}.npy em {src_group_dir}" + f"multi_head=true e máscara auxiliar ausente: " + f"{aux_dir}/{base}.npy" ) meta_dst = None @@ -677,146 +1987,215 @@ def copiar_sample( visual_dst = None if copy_meta_preview: - if s.meta_path and os.path.exists(s.meta_path): - garantir(dst_meta_dir) - meta_dst = os.path.join(dst_meta_dir, base + ".json") - shutil.copy2(s.meta_path, meta_dst) + if ( + sample.meta_path + and os.path.isfile(sample.meta_path) + ): + dst_meta_dir = os.path.join( + dst_group_dir, + "metas", + ) - if s.preview_path and os.path.exists(s.preview_path): - garantir(dst_preview_dir) - preview_dst = os.path.join(dst_preview_dir, base + os.path.splitext(s.preview_path)[1]) - shutil.copy2(s.preview_path, preview_dst) + garantir( + dst_meta_dir + ) - if copy_visuals and s.visual_path and os.path.exists(s.visual_path): - garantir(dst_visual_dir) - visual_dst = os.path.join(dst_visual_dir, os.path.basename(s.visual_path)) - shutil.copy2(s.visual_path, visual_dst) + meta_dst = os.path.join( + dst_meta_dir, + base + ".json", + ) + + shutil.copy2( + sample.meta_path, + meta_dst, + ) + + if ( + sample.preview_path + and os.path.isfile(sample.preview_path) + ): + dst_preview_dir = os.path.join( + dst_group_dir, + "previews", + ) + + garantir( + dst_preview_dir + ) + + ext = os.path.splitext( + sample.preview_path + )[1] + + preview_dst = os.path.join( + dst_preview_dir, + base + ext, + ) + + shutil.copy2( + sample.preview_path, + preview_dst, + ) + + if ( + copy_visuals + and sample.visual_path + and os.path.isfile(sample.visual_path) + ): + dst_visual_dir = os.path.join( + dst_group_dir, + "visuals", + ) + + garantir( + dst_visual_dir + ) + + visual_dst = os.path.join( + dst_visual_dir, + os.path.basename(sample.visual_path), + ) + + shutil.copy2( + sample.visual_path, + visual_dst, + ) return { "split": split_name, "group": out_group_name, - "source_group": s.source_group, - "split_group": s.split_group, + "source_group": sample.source_group, + "actual_group": sample.actual_group, "base": base, - "family": s.family, - "source": s.source, - "is_synthetic": int(bool(s.is_synthetic)), - "source_root": s.src_root, - "source_root_label": s.source_root_label, - "source_root_train_only": int(bool(s.source_root_train_only)), - + "family": sample.family, + "source": sample.source, + "is_synthetic": int(sample.is_synthetic), + "stratum": sample.stratum, + "agri_risk": sample.agri_risk, + "frac_chao": sample.frac_chao, + "frac_cana": sample.frac_cana, + "frac_erva": sample.frac_erva, + "weed_near_cane_frac": sample.weed_near_cane_frac, + "weed_contact_cane_frac": sample.weed_contact_cane_frac, + "source_root": sample.src_root, "tensor": tensor_dst, "mask_npy": mask_dst, "mask_png": mask_png_dst, - - "mask_vegetation_npy": aux_masks.get("masks_vegetation", {}).get("npy"), - "mask_vegetation_png": aux_masks.get("masks_vegetation", {}).get("png"), - - "mask_cana_npy": aux_masks.get("masks_cana", {}).get("npy"), - "mask_cana_png": aux_masks.get("masks_cana", {}).get("png"), - + "mask_vegetation_npy": aux_masks.get( + "masks_vegetation", + {}, + ).get("npy"), + "mask_cana_npy": aux_masks.get( + "masks_cana", + {}, + ).get("npy"), "meta": meta_dst, "preview": preview_dst, "visual_debug": visual_dst, } -def decide_sample_split( - s: Sample, - family_split: Dict[str, Dict[str, str]], - args, -) -> Tuple[Optional[str], str]: - """ - Retorna (split, reason) - split None = ignorado - """ - group_map = family_split.get(s.split_group, {}) - assigned = group_map.get(s.family) +# ============================================================ +# Relatórios +# ============================================================ - is_train_only_candidate = ( - s.source_root_train_only - or (bool(args.synthetic_train_only) and s.is_synthetic) - or s.source in ("augmented", "copypaste", "synthetic") +def write_csv( + path: str | Path, + rows: Sequence[Dict[str, Any]], + fieldnames: Sequence[str], +) -> None: + garantir( + os.path.dirname(str(path)) ) - if is_train_only_candidate: - if bool(args.synthetic_respect_family_split): - if assigned is None: - if bool(args.allow_orphan_synthetic_train): - return "train", "train_only_orphan_allowed" - return None, "skip_train_only_orphan_no_real_family" + with open( + path, + "w", + newline="", + encoding="utf-8", + ) as f: + writer = csv.DictWriter( + f, + fieldnames=list(fieldnames), + extrasaction="ignore", + ) - if assigned != "train": - return None, f"skip_train_only_family_assigned_{assigned}" - - return "train", "train_only_family_train" - - return "train", "train_only_forced" - - # real/original - if assigned is None: - return None, "skip_real_no_family_assignment" - - return assigned, f"real_assigned_{assigned}" + writer.writeheader() + writer.writerows(rows) -# ============================================================ -# Manifestos -# ============================================================ - -def write_manifest(path: str, rows: List[Dict[str, Any]]) -> None: - garantir(os.path.dirname(path)) - - fieldnames = [ - "split", - "group", - "source_group", - "split_group", - "base", - "family", - "source", - "is_synthetic", - "source_root", - "source_root_label", - "source_root_train_only", - - "tensor", - "mask_npy", - "mask_png", - - "mask_vegetation_npy", - "mask_vegetation_png", - - "mask_cana_npy", - "mask_cana_png", - - "meta", - "preview", - "visual_debug", - ] - - with open(path, "w", newline="", encoding="utf-8") as f: - w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore") - w.writeheader() - w.writerows(rows) +def sample_audit_row( + sample: Sample, + split_name: Optional[str], + reason: str, +) -> Dict[str, Any]: + return { + "source_group": sample.source_group, + "actual_group": sample.actual_group, + "folder_mask_mismatch": ( + normalize_group_for_split(sample.source_group) + != sample.actual_group + ), + "base": sample.base, + "family": sample.family, + "source": sample.source, + "is_synthetic": int(sample.is_synthetic), + "split": split_name or "", + "reason": reason, + "stratum": sample.stratum, + "agri_risk": sample.agri_risk, + "present_ids": ",".join( + map(str, sample.present_ids) + ), + "frac_chao": sample.frac_chao, + "frac_cana": sample.frac_cana, + "frac_erva": sample.frac_erva, + "frac_ignore": sample.frac_ignore, + "comp_cana": sample.comp_cana, + "comp_erva": sample.comp_erva, + "largest_cana_frac": sample.largest_cana_frac, + "largest_erva_frac": sample.largest_erva_frac, + "weed_near_cane_frac": sample.weed_near_cane_frac, + "weed_contact_cane_frac": sample.weed_contact_cane_frac, + "brightness": sample.brightness, + "contrast": sample.contrast, + "saturation": sample.saturation, + "dhash64": f"{sample.dhash64:016x}", + "tensor": sample.tensor_path, + "mask": sample.mask_path, + "preview": sample.preview_path or "", + "audit_error": sample.audit_error, + } -def write_skipped(path: str, rows: List[Dict[str, Any]]) -> None: - garantir(os.path.dirname(path)) - fieldnames = [ - "source_root", - "source_group", - "split_group", - "base", - "family", - "source", - "is_synthetic", - "reason", - ] - with open(path, "w", newline="", encoding="utf-8") as f: - w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore") - w.writeheader() - w.writerows(rows) +def family_audit_rows( + units: Sequence[FamilyUnit], +) -> List[Dict[str, Any]]: + rows = [] + + for u in sorted( + units, + key=lambda x: ( + x.actual_group, + x.stratum, + x.leakage_id, + ), + ): + rows.append({ + "actual_group": u.actual_group, + "family": u.family, + "leakage_id": u.leakage_id, + "samples_in_unit": len(u.samples), + "stratum": u.stratum, + "agri_risk": u.agri_risk, + "frac_chao": u.frac_chao, + "frac_cana": u.frac_cana, + "frac_erva": u.frac_erva, + "assigned_split": u.assigned_split or "", + "assignment_reason": u.assignment_reason, + }) + + return rows # ============================================================ @@ -825,208 +2204,384 @@ def write_skipped(path: str, rows: List[Dict[str, Any]]) -> None: def main() -> None: ap = argparse.ArgumentParser( - description="Split multi-source OAK-FCC-3 sem vazamento, com sintéticos/aug apenas no train." + description=( + "Split agrícola Teacher V2: família, máscara, diversidade e " + "prova real sem leakage." + ) ) - ap.add_argument("--train", type=float, default=0.70) - ap.add_argument("--val", type=float, default=0.29) - ap.add_argument("--test", type=float, default=0.01) - ap.add_argument("--seed", type=int, default=42) + ap.add_argument( + "--train", + type=float, + default=0.85, + ) - ap.add_argument("--min-train", type=int, default=1) - ap.add_argument("--min-val", type=int, default=1) - ap.add_argument("--min-test", type=int, default=0) + ap.add_argument( + "--val", + type=float, + default=0.15, + ) + + ap.add_argument( + "--test", + type=float, + default=0.0, + ) + + ap.add_argument( + "--seed", + type=int, + default=42, + ) + + ap.add_argument( + "--strategy", + choices=[ + "agri_diverse", + "random", + ], + default="agri_diverse", + ) ap.add_argument( "--resolucao", - type=str, default=None, - help="Sobrescreve resolução no formato WxH. Ex: 1024x640.", + help="WxH, ex. 1280x800", ) ap.add_argument( "--src-root", - type=str, default=None, - help="Compatibilidade: uma raiz normalizada. Default: dataset//group", ) ap.add_argument( "--src-roots", - type=str, default="", - help="Lista de raízes separadas por vírgula. Ex: dataset/1024x640/group,dataset/copypaste/group", ) ap.add_argument( "--train-only-roots", - type=str, default="", - help="Raízes que devem entrar somente no TRAIN. Ex: dataset/copypaste/group", ) ap.add_argument( "--dst-root", - type=str, default="dataset/split", - help="Raiz do split.", ) ap.add_argument( "--groups", - type=str, default=None, - help="Lista de grupos/split_groups separados por vírgula.", - ) - - ap.add_argument( - "--cap-train-families", - type=str, - default="", - help="Mapa 'grupo:cap,...' para limitar famílias reais no TRAIN. Ex: 'chao:50'", ) ap.add_argument( "--clear-dst", action="store_true", - help="Apaga dst-root antes de copiar.", ) ap.add_argument( "--no-meta-preview", action="store_true", - help="Não copia metas/previews para o split.", ) ap.add_argument( "--no-visuals", action="store_true", - help="Não copia pasta visuals/debug.", ) + ap.add_argument( + "--output-group-mode", + choices=[ + "source", + "base", + "suffix", + ], + default="base", + help=( + "base é recomendado: o grupo final segue a máscara real." + ), + ) + + # máscara + ap.add_argument( + "--presence-min-pixels", + type=int, + default=1, + ) + + ap.add_argument( + "--presence-min-fraction", + type=float, + default=0.0, + ) + + ap.add_argument( + "--ignore-ids", + nargs="*", + type=int, + default=[255], + ) + + # Teacher/agri strata + ap.add_argument( + "--target-tiny-frac", + type=float, + default=0.002, + ) + + ap.add_argument( + "--target-small-frac", + type=float, + default=0.010, + ) + + ap.add_argument( + "--target-medium-frac", + type=float, + default=0.050, + ) + + ap.add_argument( + "--near-radius-frac", + type=float, + default=0.02, + ) + + ap.add_argument( + "--close-weed-cane-threshold", + type=float, + default=0.25, + ) + + # features + ap.add_argument( + "--use-tensor-features", + action="store_true", + default=False, + help=( + "Inclui diversidade espectral do tensor no split. " + "Recomendado após validar custo no dry-run." + ), + ) + + ap.add_argument( + "--tensor-feature-stride", + type=int, + default=16, + help=( + "Subamostragem espacial para features do tensor. " + "16 é leve mesmo em 1280x800." + ), + ) + + # near duplicates + ap.add_argument( + "--merge-near-duplicate-families", + action="store_true", + default=False, + help=( + "Une famílias reais quase idênticas para impedir leakage." + ), + ) + + ap.add_argument( + "--near-dup-hamming", + type=int, + default=2, + ) + + ap.add_argument( + "--near-dup-mask-l1", + type=float, + default=0.02, + ) + + # synthetic ap.add_argument( "--synthetic-train-only", action="store_true", default=True, - help="Força samples synthetic/augmented/copypaste para TRAIN apenas.", ) ap.add_argument( "--allow-synthetic-val", dest="synthetic_train_only", action="store_false", - help="Permite sintéticos no val/test. Não recomendado.", ) ap.add_argument( "--synthetic-respect-family-split", action="store_true", default=True, - help="Só inclui sintético no TRAIN se a família real correspondente caiu no TRAIN.", ) ap.add_argument( "--no-synthetic-respect-family-split", dest="synthetic_respect_family_split", action="store_false", - help="Inclui sintéticos no TRAIN mesmo sem checar a família real. Mais arriscado.", ) ap.add_argument( "--allow-orphan-synthetic-train", action="store_true", default=False, - help="Permite sintético no TRAIN mesmo sem família real encontrada.", - ) - - ap.add_argument( - "--output-group-mode", - choices=["source", "base", "suffix"], - default="source", - help="Nome do grupo de saída. source mantém chao_cana_copypaste; base junta em chao_cana; suffix cria _synthetic.", ) + # relatórios ap.add_argument( "--manifest", - type=str, default="", - help="CSV de manifesto. Default: /split_manifest.csv", + ) + + ap.add_argument( + "--audit", + default="", + ) + + ap.add_argument( + "--families", + default="", ) ap.add_argument( "--summary", - type=str, default="", - help="JSON de resumo. Default: /split_summary.json", ) ap.add_argument( "--skipped", - type=str, default="", - help="CSV de samples ignorados. Default: /split_skipped.csv", + ) + + ap.add_argument( + "--dry-run", + action="store_true", + help="Audita e decide split, mas não copia arquivos.", ) args = ap.parse_args() + # resolução if args.resolucao: try: w, h = args.resolucao.lower().split("x") - resolucao = (int(w), int(h)) + resolution = ( + int(w), + int(h), + ) except Exception: - resolucao = RESOLUCAO + raise SystemExit( + f"[ERRO] resolução inválida: {args.resolucao}" + ) else: - resolucao = RESOLUCAO + resolution = RESOLUCAO - default_src = os.path.join("dataset", f"{resolucao[0]}x{resolucao[1]}", "group") + default_src = os.path.join( + "dataset", + f"{resolution[0]}x{resolution[1]}", + "group", + ) + + src_roots = parse_csv_list( + args.src_roots + ) - src_roots = parse_csv_list(args.src_roots) if args.src_root: - src_roots.insert(0, args.src_root) - if not src_roots: - src_roots = [default_src] + src_roots.insert( + 0, + args.src_root, + ) + + if not src_roots: + src_roots = [ + default_src + ] - # Remove duplicatas preservando ordem. seen = set() - src_roots_unique = [] - for r in src_roots: - nr = norm_path(r) + unique_src_roots = [] + + for root in src_roots: + nr = norm_path(root) + if nr not in seen: seen.add(nr) - src_roots_unique.append(r) - src_roots = src_roots_unique + unique_src_roots.append(root) - train_only_roots = parse_csv_list(args.train_only_roots) + src_roots = unique_src_roots - for r in src_roots: - if not os.path.isdir(r): - raise SystemExit(f"[ERRO] src-root não encontrado: {r}") + train_only_roots = parse_csv_list( + args.train_only_roots + ) - dst_root = args.dst_root + for root in src_roots: + if not os.path.isdir(root): + raise SystemExit( + f"[ERRO] src-root não encontrado: {root}" + ) + + soma = ( + args.train + + args.val + + args.test + ) - soma = args.train + args.val + args.test if soma <= 0: - raise ValueError("Soma de proporções deve ser > 0.") + raise SystemExit( + "[ERRO] soma train+val+test deve ser > 0" + ) p_train = args.train / soma p_val = args.val / soma p_test = args.test / soma - mins = { - "train": max(0, args.min_train), - "val": max(0, args.min_val), - "test": max(0, args.min_test), - } + groups = ( + parse_csv_list(args.groups) + if args.groups + else None + ) - groups = parse_csv_list(args.groups) if args.groups else None - caps_map = parse_map(args.cap_train_families, int) + dst_root = args.dst_root - if args.clear_dst: - print(f"[INFO] Limpando destino: {dst_root}") - limpar_dir(dst_root) + if ( + args.clear_dst + and not args.dry_run + ): + print( + f"[INFO] Limpando destino: {dst_root}" + ) + limpar_dir( + dst_root + ) else: - garantir(dst_root) + garantir( + dst_root + ) + print("==========================================") + print("SPLIT AGRÍCOLA TEACHER V2") + print(f"Resolution : {resolution[0]}x{resolution[1]}") + print(f"Strategy : {args.strategy}") + print( + f"Split : train={p_train:.3f} " + f"val={p_val:.3f} test={p_test:.3f}" + ) + print(f"Tensor feat: {args.use_tensor_features}") + print( + f"Near-dups : {args.merge_near_duplicate_families}" + ) + print(f"Dry-run : {args.dry_run}") + print("SRC:") + for root in src_roots: + marker = ( + " [TRAIN_ONLY]" + if norm_path(root) + in {norm_path(x) for x in train_only_roots} + else "" + ) + print(f" - {root}{marker}") + print("==========================================") + + print("[1/6] Coletando samples...") samples = collect_all_samples( src_roots=src_roots, train_only_roots=train_only_roots, @@ -1034,41 +2589,135 @@ def main() -> None: ) if not samples: - print("[WARN] Nenhuma amostra encontrada.") - return - - family_split, family_summary = build_family_split( - samples=samples, - p_train=p_train, - p_val=p_val, - p_test=p_test, - seed=args.seed, - mins=mins, - caps_map=caps_map, - ) - - print("==========================================") - print("Split OAK-FCC-3 Multi-source") - print("SRC_ROOTS:") - for r in src_roots: - marker = " [TRAIN_ONLY]" if norm_path(r) in {norm_path(x) for x in train_only_roots} else "" - print(f" - {r}{marker}") - print(f"DST : {dst_root}") - print(f"Split : train={p_train:.3f}, val={p_val:.3f}, test={p_test:.3f}") - print(f"Mínimos : train={mins['train']} val={mins['val']} test={mins['test']}") - print(f"Seed : {args.seed}") - print(f"Synthetic : train_only={args.synthetic_train_only} respect_family={args.synthetic_respect_family_split}") - print("==========================================") - - print("\nFamílias reais por split_group:") - for g, info in family_summary.items(): - print( - f"[{g}] famílias={info['familias']} → " - f"train={info['train_families']}, val={info['val_families']}, test={info['test_families']}" + raise SystemExit( + "[ERRO] Nenhum sample encontrado." ) - all_rows: List[Dict[str, Any]] = [] - skipped_rows: List[Dict[str, Any]] = [] + print( + f"[OK] samples encontrados: {len(samples)}" + ) + + print("[2/6] Auditando máscaras + features agrícolas...") + analyzed: List[Sample] = [] + rejected: List[Sample] = [] + + for i, sample in enumerate( + samples, + start=1, + ): + try: + analyze_sample( + sample, + args, + ) + analyzed.append( + sample + ) + except Exception as exc: + sample.audit_error = str(exc) + rejected.append( + sample + ) + + if ( + i % 100 == 0 + or i == len(samples) + ): + print( + f" {i}/{len(samples)} " + f"ok={len(analyzed)} " + f"rejeitados={len(rejected)}" + ) + + real_analyzed = [ + s + for s in analyzed + if not s.is_synthetic + and not s.source_root_train_only + ] + + mismatches = [ + s + for s in real_analyzed + if normalize_group_for_split( + s.source_group + ) != s.actual_group + ] + + print( + f"[OK] reais auditados={len(real_analyzed)} " + f"mismatch pasta/máscara={len(mismatches)}" + ) + + print("[3/6] Construindo famílias reais...") + base_units = build_real_family_units( + real_analyzed + ) + + ( + units, + family_to_leakage, + merged_count, + ) = merge_near_duplicate_family_units( + units=base_units, + enabled=args.merge_near_duplicate_families, + max_hamming=args.near_dup_hamming, + mask_l1=args.near_dup_mask_l1, + ) + + # Garante mapping mesmo sem merge. + for unit in units: + for sample in unit.samples: + family_to_leakage.setdefault( + (sample.actual_group, sample.family), + unit.leakage_id, + ) + + print( + f"[OK] famílias base={len(base_units)} " + f"unidades leakage={len(units)} " + f"famílias mescladas={merged_count}" + ) + + print("[4/6] Montando a prova...") + if args.strategy == "agri_diverse": + assignment = assign_agri_diverse_split( + units=units, + p_train=p_train, + p_val=p_val, + p_test=p_test, + seed=args.seed, + ) + else: + assignment = assign_random_split( + units=units, + p_train=p_train, + p_val=p_val, + p_test=p_test, + seed=args.seed, + ) + + family_counts = { + "train": 0, + "val": 0, + "test": 0, + } + + for unit in units: + if unit.assigned_split: + family_counts[ + unit.assigned_split + ] += 1 + + print( + f"[OK] famílias/unidades → " + f"train={family_counts['train']} " + f"val={family_counts['val']} " + f"test={family_counts['test']}" + ) + + print("[5/6] Aplicando split aos samples...") + decisions: Dict[int, Tuple[Optional[str], str]] = {} totals = { "train": 0, @@ -1081,121 +2730,306 @@ def main() -> None: "real_test": 0, } - by_group: Dict[str, Dict[str, int]] = {} + stratum_counts: Dict[str, Dict[str, int]] = {} - for s in samples: - split_name, reason = decide_sample_split(s, family_split, args) + for sample in analyzed: + split_name, reason = decide_sample_split( + sample=sample, + family_to_leakage=family_to_leakage, + assignment=assignment, + args=args, + ) + + decisions[id(sample)] = ( + split_name, + reason, + ) if split_name is None: totals["skipped"] += 1 - skipped_rows.append({ - "source_root": s.src_root, - "source_group": s.source_group, - "split_group": s.split_group, - "base": s.base, - "family": s.family, - "source": s.source, - "is_synthetic": int(s.is_synthetic), - "reason": reason, - }) continue - out_group = output_group_name( - source_group=s.source_group, - split_group=s.split_group, - is_synthetic=s.is_synthetic, - mode=args.output_group_mode, - ) - - row = copiar_sample( - s=s, - dst_root=dst_root, - split_name=split_name, - out_group_name=out_group, - copy_meta_preview=not args.no_meta_preview, - copy_visuals=not args.no_visuals, - ) - - if row is None: - totals["skipped"] += 1 - skipped_rows.append({ - "source_root": s.src_root, - "source_group": s.source_group, - "split_group": s.split_group, - "base": s.base, - "family": s.family, - "source": s.source, - "is_synthetic": int(s.is_synthetic), - "reason": "copy_failed_missing_tensor_or_mask", - }) - continue - - all_rows.append(row) - totals[split_name] += 1 - if split_name == "train" and s.is_synthetic: + + if ( + split_name == "train" + and sample.is_synthetic + ): totals["synthetic_train"] += 1 + elif split_name == "train": totals["real_train"] += 1 + elif split_name == "val": totals["real_val"] += 1 + elif split_name == "test": totals["real_test"] += 1 - gsum = by_group.setdefault(out_group, {"train": 0, "val": 0, "test": 0, "synthetic_train": 0, "skipped": 0}) - gsum[split_name] = gsum.get(split_name, 0) + 1 - if split_name == "train" and s.is_synthetic: - gsum["synthetic_train"] += 1 + ss = stratum_counts.setdefault( + sample.stratum, + { + "train": 0, + "val": 0, + "test": 0, + }, + ) - manifest_path = args.manifest or os.path.join(dst_root, "split_manifest.csv") - summary_path = args.summary or os.path.join(dst_root, "split_summary.json") - skipped_path = args.skipped or os.path.join(dst_root, "split_skipped.csv") + ss[split_name] += 1 - write_manifest(manifest_path, all_rows) - write_skipped(skipped_path, skipped_rows) + print( + f"[OK] samples → " + f"train={totals['train']} " + f"val={totals['val']} " + f"test={totals['test']} " + f"skipped={totals['skipped']}" + ) + + print("[6/6] Copiando / relatórios...") + manifest_rows = [] + + if not args.dry_run: + for sample in analyzed: + split_name, reason = decisions[ + id(sample) + ] + + if split_name is None: + continue + + out_group = output_group_name( + source_group=sample.source_group, + actual_group=sample.actual_group, + is_synthetic=sample.is_synthetic, + mode=args.output_group_mode, + ) + + row = copiar_sample( + sample=sample, + dst_root=dst_root, + split_name=split_name, + out_group_name=out_group, + copy_meta_preview=not args.no_meta_preview, + copy_visuals=not args.no_visuals, + ) + + if row is not None: + row["decision_reason"] = reason + manifest_rows.append( + row + ) + + manifest_path = ( + args.manifest + or os.path.join( + dst_root, + "split_manifest.csv", + ) + ) + + audit_path = ( + args.audit + or os.path.join( + dst_root, + "split_audit.csv", + ) + ) + + families_path = ( + args.families + or os.path.join( + dst_root, + "split_families.csv", + ) + ) + + skipped_path = ( + args.skipped + or os.path.join( + dst_root, + "split_skipped.csv", + ) + ) + + summary_path = ( + args.summary + or os.path.join( + dst_root, + "split_summary.json", + ) + ) + + audit_rows = [] + + skipped_rows = [] + + for sample in analyzed: + split_name, reason = decisions[ + id(sample) + ] + + row = sample_audit_row( + sample, + split_name, + reason, + ) + + audit_rows.append( + row + ) + + if split_name is None: + skipped_rows.append( + row + ) + + for sample in rejected: + audit_rows.append( + sample_audit_row( + sample, + None, + "audit_rejected", + ) + ) + + if audit_rows: + write_csv( + audit_path, + audit_rows, + list(audit_rows[0].keys()), + ) + + family_rows = family_audit_rows( + units + ) + + if family_rows: + write_csv( + families_path, + family_rows, + list(family_rows[0].keys()), + ) + + if skipped_rows: + write_csv( + skipped_path, + skipped_rows, + list(skipped_rows[0].keys()), + ) + + if ( + not args.dry_run + and manifest_rows + ): + write_csv( + manifest_path, + manifest_rows, + list(manifest_rows[0].keys()), + ) summary = { - "schema": "oak_fcc3_multi_source_split_v1", + "schema": "oak_fcc3_agri_teacher_v2_split_v1", + "resolution": list(resolution), + "strategy": args.strategy, "src_roots": src_roots, "train_only_roots": train_only_roots, "dst_root": dst_root, - "resolution": list(resolucao), "proportions": { "train": p_train, "val": p_val, "test": p_test, }, - "mins": mins, - "seed": args.seed, - "multi_head": MULTI_HEAD, "options": { + "use_tensor_features": args.use_tensor_features, + "tensor_feature_stride": args.tensor_feature_stride, + "merge_near_duplicate_families": args.merge_near_duplicate_families, + "near_dup_hamming": args.near_dup_hamming, + "near_dup_mask_l1": args.near_dup_mask_l1, "synthetic_train_only": args.synthetic_train_only, "synthetic_respect_family_split": args.synthetic_respect_family_split, - "allow_orphan_synthetic_train": args.allow_orphan_synthetic_train, "output_group_mode": args.output_group_mode, }, - "family_summary": family_summary, - "groups": by_group, - "total": totals, - "samples_seen": len(samples), - "manifest": manifest_path, - "skipped": skipped_path, + "teacher_bins": { + "target_tiny_frac": args.target_tiny_frac, + "target_small_frac": args.target_small_frac, + "target_medium_frac": args.target_medium_frac, + "near_radius_frac": args.near_radius_frac, + "close_weed_cane_threshold": args.close_weed_cane_threshold, + }, + "counts": { + "samples_seen": len(samples), + "samples_analyzed": len(analyzed), + "samples_rejected": len(rejected), + "real_samples": len(real_analyzed), + "folder_mask_mismatches": len(mismatches), + "base_families": len(base_units), + "leakage_units": len(units), + "near_duplicate_families_merged": merged_count, + "family_split": family_counts, + "samples_split": totals, + }, + "strata": stratum_counts, + "paths": { + "manifest": ( + manifest_path + if not args.dry_run + else None + ), + "audit": audit_path, + "families": families_path, + "skipped": skipped_path, + }, + "dry_run": bool(args.dry_run), } - save_json(summary_path, summary) + save_json( + summary_path, + summary, + ) - print("\nResumo global:") - print(f" train total: {totals['train']}") - print(f" real train: {totals['real_train']}") - print(f" synthetic train: {totals['synthetic_train']}") - print(f" val real: {totals['real_val']}") - print(f" test real: {totals['real_test']}") - print(f" skipped: {totals['skipped']}") - print(f"\nManifest: {manifest_path}") - print(f"Skipped : {skipped_path}") - print(f"Summary : {summary_path}") + print("==========================================") + print("RESULTADO") + print( + f"Samples reais : {len(real_analyzed)}" + ) + print( + f"Mismatch pasta/GT : {len(mismatches)}" + ) + print( + f"Famílias base : {len(base_units)}" + ) + print( + f"Unidades leakage : {len(units)}" + ) + print( + f"Near-dups mescladas: {merged_count}" + ) + print( + f"TRAIN : {totals['train']} " + f"(real={totals['real_train']} synthetic={totals['synthetic_train']})" + ) + print( + f"VAL real : {totals['real_val']}" + ) + print( + f"TEST real : {totals['real_test']}" + ) + print( + f"Skipped : {totals['skipped']}" + ) + print(f"Audit : {audit_path}") + print(f"Families : {families_path}") + print(f"Summary : {summary_path}") - print("\n✅ Split multi-source sem vazamento concluído!") + if not args.dry_run: + print(f"Manifest : {manifest_path}") + + print("==========================================") + + if args.dry_run: + print("✅ DRY-RUN concluído. Nenhum dataset foi copiado.") + else: + print("✅ Split agrícola Teacher V2 concluído.") if __name__ == "__main__": diff --git a/Python/OAK/datasets/oak-fcc-3/_8_train_multihead_v2.py b/Python/OAK/datasets/oak-fcc-3/_8_train_multihead_v2.py new file mode 100644 index 000000000..ef34ac788 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/_8_train_multihead_v2.py @@ -0,0 +1,3038 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +_8_train_multihead_v2.py + +Agri Teacher V2 para SegFormer multiespectral OAK-FCC-3. + +Objetivo operacional: distinguir chão/cana/erva e maximizar a decisão +pulverizável (vegetação não-cana), com proteção explícita da cana. + +Recursos principais: +- Raw3/4/5/7 por nomes de canais, incluindo NDVI/NDRE; +- augmentation multiespectral sincronizado e fisicamente coerente; +- sampler agrícola de batches diversos; +- decoder separado ou decoder compartilhado + classifiers leves; +- inicialização multiespectral configurável; +- CE + Dice por imagem, boundary/OHEM opcionais e safety loss; +- AdamW com LR discriminativo, no-decay em norm/bias; +- warmup + poly/cosine por optimizer step; +- AMP, gradient accumulation correto e gradient clipping; +- métricas agrícolas: target precision/recall/F1, cana safety, spray rate, + weed miss rate, macro IoU, threshold sweep, ECE e cenários por área; +- checkpoint/resume completo com optimizer, scheduler, scaler, RNG e global step. + +Mantém as funções públicas usadas por _10_export_onnx.py e _11_validate_onnx.py. +""" + +from __future__ import annotations + +import os +os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:128") + +import csv +import copy +import json +import time +import math +import argparse +import random +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Any + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.data import Dataset, DataLoader, Sampler, WeightedRandomSampler +from torch.amp import autocast, GradScaler + +from transformers import SegformerForSemanticSegmentation + + +# ============================================================ +# Seed / util +# ============================================================ + +def set_seed(seed: int = 42): + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + +def ensure_dir(path: Path): + path.mkdir(parents=True, exist_ok=True) + + +def load_json(path: str | Path) -> dict: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def save_json(path: Path, data: dict): + ensure_dir(path.parent) + with path.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + +def safe_float(x, default=0.0) -> float: + try: + return float(x) + except Exception: + return float(default) + + +# ============================================================ +# Labelmap +# ============================================================ + +def load_labelmap(labelmap_path: str): + """ + Tenta usar helpers.carregar_labelmap_completo. + Fallback simples para labelmap com uma classe por linha. + """ + try: + from helpers import carregar_labelmap_completo, _infer_ignore_id + + _cor_para_id, _colormap_rgb, id_para_nome, ignore_rgb = carregar_labelmap_completo(labelmap_path) + ignore_id = _infer_ignore_id(ignore_rgb, 255) + + id2label = {int(k): str(v) for k, v in id_para_nome.items()} + label2id = {v.lower(): k for k, v in id2label.items()} + + return id2label, label2id, int(ignore_id) + + except Exception as e: + print(f"[WARN] Não consegui usar helpers.carregar_labelmap_completo: {e}") + print("[WARN] Usando parser simples: uma classe por linha.") + + id2label = {} + with open(labelmap_path, "r", encoding="utf-8") as f: + for line in f: + s = line.strip() + if not s or s.startswith("#"): + continue + + parts = s.replace(",", " ").split() + + if len(parts) >= 2 and parts[0].isdigit(): + cid = int(parts[0]) + name = parts[1] + else: + cid = len(id2label) + name = parts[0] + + if name.lower() in ("ignore", "void", "background_ignore"): + continue + + id2label[cid] = name + + label2id = {v.lower(): k for k, v in id2label.items()} + return id2label, label2id, 255 + + +# ============================================================ +# Head config +# ============================================================ + +DEFAULT_HEADS = { + "semantic": { + "enabled": True, + "type": "multiclass", + "num_classes": 3, + "mask_dir": "masks", + "classes": { + "chao": 0, + "cana": 1, + "erva": 2, + }, + "ignore_index": 255, + "loss_weight": 0.10, + }, + "vegetation": { + "enabled": True, + "type": "binary", + "num_classes": 2, + "mask_dir": "masks_vegetation", + "classes": { + "background": 0, + "vegetation": 1, + }, + "ignore_index": 255, + "loss_weight": 0.20, + }, + "cana": { + "enabled": True, + "type": "binary", + "num_classes": 2, + "mask_dir": "masks_cana", + "classes": { + "not_cana": 0, + "cana": 1, + }, + "ignore_index": 255, + "loss_weight": 0.25, + }, + "target": { + "enabled": True, + "type": "binary", + "num_classes": 2, + "mask_dir": "__derived_target__", + "classes": { + "background": 0, + "target": 1, + }, + "ignore_index": 255, + "loss_weight": 0.45, + "derived_from": ["vegetation", "cana"], + }, +} + + +def merge_dict(dst: dict, src: dict) -> dict: + out = copy.deepcopy(dst) + + def rec(a, b): + for k, v in b.items(): + if isinstance(v, dict) and isinstance(a.get(k), dict): + rec(a[k], v) + else: + a[k] = v + + if isinstance(src, dict): + rec(out, src) + + return out + + +def build_heads_config(config: dict, ignore_index: int) -> Dict[str, dict]: + cfg = merge_dict(DEFAULT_HEADS, config.get("heads", {}) or {}) + + # Compatibilidade com configs antigas. + if not bool(config.get("multi_head", True)): + print("[WARN] config.multi_head não está true. Este script vai treinar multi-head mesmo assim.") + + for name, hcfg in cfg.items(): + hcfg.setdefault("enabled", True) + hcfg.setdefault("ignore_index", ignore_index) + hcfg["num_classes"] = int(hcfg.get("num_classes", 2)) + hcfg["loss_weight"] = float(hcfg.get("loss_weight", 1.0)) + hcfg["mask_dir"] = str(hcfg.get("mask_dir", "masks")) + + active = {k: v for k, v in cfg.items() if bool(v.get("enabled", True))} + + if "semantic" not in active: + raise RuntimeError("A head 'semantic' deve estar habilitada neste contrato.") + if "vegetation" not in active: + raise RuntimeError("A head 'vegetation' deve estar habilitada neste contrato.") + if "cana" not in active: + raise RuntimeError("A head 'cana' deve estar habilitada neste contrato.") + + total_w = sum(float(v.get("loss_weight", 0.0)) for v in active.values()) + if total_w <= 0: + raise RuntimeError("Soma de loss_weight das heads deve ser > 0.") + + # Normaliza os pesos para não bagunçar magnitude da loss. + for v in active.values(): + v["loss_weight_norm"] = float(v.get("loss_weight", 0.0)) / total_w + + return active + + + +# ============================================================ +# Training V2 defaults / helpers +# ============================================================ + +TRAINER_VERSION = "agri_teacher_v2.2" +SOURCE_CHANNEL_ORDER = ["R", "G", "B", "RE", "NIR"] +DERIVED_CHANNEL_ORDER = ["NDVI", "NDRE"] +SUPPORTED_INPUT_CHANNELS = SOURCE_CHANNEL_ORDER + DERIVED_CHANNEL_ORDER + +DEFAULT_TRAINING_V2 = { + "model": { + # separate = 1 decoder SegFormer completo por head (legado) + # shared_light = 1 decoder compartilhado + classifiers 1x1 leves + "decoder_mode": "shared_light", + # mean_rgb | zero_extra | scaled_mean + "spectral_input_init": "zero_extra", + }, + "data": { + # Valida conteúdo real de tensors/masks .npy antes do treino. + # Arquivos truncados podem existir e ainda assim passar pelo split por existência. + "validate_npy_content": True, + "skip_corrupt_samples": True, + # Mesmo com skip=true, aborta se a corrupção deixar de ser pontual. + "max_corrupt_fraction": 0.005, + }, + "augmentation": { + "enabled": True, + "horizontal_flip_p": 0.50, + "vertical_flip_p": 0.00, + "affine_p": 0.70, + "rotate_deg": 5.0, + "scale_min": 0.90, + "scale_max": 1.10, + "translate_frac": 0.04, + "crop_p": 0.45, + "crop_scale_min": 0.70, + "crop_scale_max": 1.00, + "crop_focus_target_p": 0.55, + "crop_focus_cana_p": 0.25, + "global_gain_p": 0.35, + "global_gain_min": 0.92, + "global_gain_max": 1.08, + "band_gain_p": 0.25, + "band_gain_min": 0.96, + "band_gain_max": 1.04, + "rgb_gamma_p": 0.20, + "rgb_gamma_min": 0.94, + "rgb_gamma_max": 1.06, + "noise_p": 0.20, + "noise_sigma_min": 0.001, + "noise_sigma_max": 0.008, + "blur_p": 0.12, + "blur_kernel": 3, + "sensor_channel_dropout_p": 0.00, + "clip_physical": True, + }, + "sampler": { + # random | diverse | weighted + "mode": "diverse", + "samples_per_epoch": 0, + "tiny_target_pct": 0.005, + "small_target_pct": 0.02, + "medium_target_pct": 0.10, + }, + "class_weighting": { + # log_inverse = comportamento robusto semelhante ao legado, agora com dataset inteiro + # sqrt_inverse = inverso de frequência suavizado + "method": "log_inverse", + "log_offset": 1.02, + "power": 0.50, + "min_weight": 0.25, + "max_weight": 4.00, + }, + "loss": { + "dice_reduction": "per_image", + "dice_smooth": 1.0, + "boundary_weight": 0.00, + "ohem_ratio": 0.00, + "safety": { + "enabled": True, + "weight": 0.08, + "cana_weight": 1.00, + "ground_weight": 0.20, + }, + }, + "optimizer": { + "encoder_lr": None, # None => usa --lr + "patch_lr_mult": 2.0, + "heads_lr_mult": 5.0, + "weight_decay": None, # None => usa --wd + "no_decay_bias": True, + "no_decay_norm": True, + "betas": [0.9, 0.999], + "eps": 1e-8, + }, + "scheduler": { + # poly | cosine | constant + "mode": "poly", + "warmup_ratio": 0.05, + "warmup_start_factor": 0.10, + "poly_power": 1.0, + "min_lr_ratio": 0.02, + }, + "optimization": { + "grad_clip_norm": 1.0, + "matmul_precision": "high", + "cudnn_benchmark": True, + "persistent_workers": True, + "prefetch_factor": 2, + }, + "target_distillation": { + # Compatível com config.target_distillation legado. + "enabled": False, + "mode": "cross_head", + "start_epoch": 8, + "rampup_epochs": 12, + "hard_weight": 0.75, + "distill_weight": 0.25, + "teacher_confidence_min": 0.60, + "detach_teacher": True, + "w_sem_erva": 0.45, + "w_veg_not_cana": 0.35, + "w_veg_suppressed": 0.20, + "cana_suppression_power": 1.5, + }, + "metrics": { + # Sweep mais amplo para separar qualidade do modelo de calibração do threshold. + "target_thresholds": [ + 0.30, + 0.35, + 0.40, + 0.45, + 0.50, + 0.55, + 0.60, + 0.65, + 0.70, + 0.75, + 0.80, + 0.85, + 0.90, + 0.95, + ], + "ece_bins": 15, + "scenario_metrics": True, + "group_metrics": True, + # No train mantemos as métricas básicas, mas pulamos sweep/ECE/cenários + # pesados. A validação continua completa. Não altera gradientes. + "rich_train_metrics": False, + "operational_threshold": { + # Restrições são usadas somente para recomendar threshold e salvar + # best_operational. Não alteram a loss nem o score principal. + "max_cana_spray_rate": 0.02, + "max_ground_spray_rate": 0.03, + "max_weed_miss_rate": 0.20, + "score_weights": { + "target_iou": 0.35, + "target_f1": 0.20, + "cana_safety": 0.25, + "ground_safety": 0.10, + "weed_recall": 0.10 + } + }, + }, + "selection_score": { + "target_iou": 0.40, + "cana_iou": 0.20, + "target_f1": 0.10, + "vegetation_miou": 0.10, + "semantic_miou": 0.05, + "cana_safety": 0.15, + }, + "checkpoint": { + "early_stop_min_delta": 5e-4, + "save_best_safety": True, + # Mantém duas réguas extras: a régua histórica do trainer antigo e + # uma régua operacional que escolhe threshold sob restrições agrícolas. + "save_best_legacy": True, + "save_best_operational": True, + }, +} + +_LAST_BUILD_CONFIG: Optional[dict] = None + + +def get_training_v2_config(config: dict) -> dict: + cfg = merge_dict(DEFAULT_TRAINING_V2, config.get("training_v2", {}) or {}) + + # Compatibilidade com target_distillation no nível raiz. + # Se training_v2.target_distillation foi declarado explicitamente, ele manda. + if ( + "target_distillation" not in (config.get("training_v2", {}) or {}) + and isinstance(config.get("target_distillation"), dict) + ): + cfg["target_distillation"] = merge_dict( + cfg.get("target_distillation", {}), + config.get("target_distillation", {}), + ) + + return cfg + + +def seed_worker(worker_id: int): + seed = torch.initial_seed() % (2**32) + np.random.seed(seed) + random.seed(seed) + + +def capture_rng_state() -> dict: + state = { + "python": random.getstate(), + "numpy": np.random.get_state(), + "torch": torch.get_rng_state(), + } + if torch.cuda.is_available(): + state["cuda"] = torch.cuda.get_rng_state_all() + return state + + +def restore_rng_state(state: Optional[dict]): + if not state: + return + try: + if "python" in state: + random.setstate(state["python"]) + if "numpy" in state: + np.random.set_state(state["numpy"]) + if "torch" in state: + torch.set_rng_state(state["torch"]) + if torch.cuda.is_available() and "cuda" in state: + torch.cuda.set_rng_state_all(state["cuda"]) + except Exception as exc: + print(f"[WARN] Não consegui restaurar RNG state: {exc}") + + +# ============================================================ +# Channels +# ============================================================ + +def get_input_channel_names(config: dict) -> List[str]: + if "input_channels" in config: + configured = config["input_channels"] + if isinstance(configured, str): + names = [str(c).strip().upper() for c in configured.split(",") if str(c).strip()] + else: + names = [str(c).strip().upper() for c in configured if str(c).strip()] + else: + n = int(config.get("channels", 5)) + if n < 1 or n > len(SOURCE_CHANNEL_ORDER): + raise RuntimeError( + "Config antiga sem input_channels só suporta channels entre 1 e 5. " + "Para NDVI/NDRE, declare input_channels explicitamente." + ) + names = SOURCE_CHANNEL_ORDER[:n] + + if not names: + raise RuntimeError("input_channels não pode ser vazio.") + duplicates = sorted({name for name in names if names.count(name) > 1}) + if duplicates: + raise RuntimeError(f"Canais duplicados em input_channels: {duplicates}") + invalid = [c for c in names if c not in SUPPORTED_INPUT_CHANNELS] + if invalid: + raise RuntimeError(f"Canais inválidos em input_channels: {invalid}. Suportados={SUPPORTED_INPUT_CHANNELS}") + + configured_count = config.get("channels") + if configured_count is not None and int(configured_count) != len(names): + raise RuntimeError( + f"config.channels={configured_count} incompatível com input_channels={names} ({len(names)})." + ) + return names + + +def get_input_channel_indices(config: dict) -> List[int]: + return list(range(len(get_input_channel_names(config)))) + + +# ============================================================ +# Multispectral augmentation +# ============================================================ + +class MultiSpectralAugmenter: + """Augmentation sincronizado para imagem multiespectral + todas as masks.""" + + def __init__(self, channel_names: List[str], cfg: dict, derived_cfg: Optional[dict] = None): + self.channel_names = [str(x).upper() for x in channel_names] + self.cfg = cfg or {} + self.derived_cfg = derived_cfg or {} + self._warned_derived_missing = False + + def _rand(self): + return random.random() + + def _uniform(self, a, b): + return random.uniform(float(a), float(b)) + + def _apply_crop(self, x: torch.Tensor, masks: Dict[str, torch.Tensor]): + cfg = self.cfg + if self._rand() >= float(cfg.get("crop_p", 0.0)): + return x, masks + + h, w = x.shape[-2:] + scale = self._uniform(cfg.get("crop_scale_min", 0.70), cfg.get("crop_scale_max", 1.0)) + crop_h = max(16, min(h, int(round(h * scale)))) + crop_w = max(16, min(w, int(round(w * scale)))) + + focus_xy = None + r = self._rand() + p_target = float(cfg.get("crop_focus_target_p", 0.55)) + p_cana = float(cfg.get("crop_focus_cana_p", 0.25)) + + focus_mask = None + if r < p_target and "target" in masks: + focus_mask = masks["target"] == 1 + elif r < p_target + p_cana and "cana" in masks: + focus_mask = masks["cana"] == 1 + + if focus_mask is not None and bool(focus_mask.any()): + coords = torch.nonzero(focus_mask, as_tuple=False) + pos = coords[random.randrange(coords.shape[0])] + cy, cx = int(pos[0]), int(pos[1]) + focus_xy = (cx, cy) + + if focus_xy is None: + cx = random.randint(0, max(0, w - 1)) + cy = random.randint(0, max(0, h - 1)) + else: + cx, cy = focus_xy + + x0 = max(0, min(w - crop_w, cx - crop_w // 2)) + y0 = max(0, min(h - crop_h, cy - crop_h // 2)) + x1, y1 = x0 + crop_w, y0 + crop_h + + x_crop = x[:, y0:y1, x0:x1].unsqueeze(0) + x = F.interpolate(x_crop, size=(h, w), mode="bilinear", align_corners=False).squeeze(0) + + out_masks = {} + for name, m in masks.items(): + mc = m[y0:y1, x0:x1].unsqueeze(0).unsqueeze(0).float() + out_masks[name] = F.interpolate(mc, size=(h, w), mode="nearest").squeeze(0).squeeze(0).long() + return x, out_masks + + def _apply_affine(self, x: torch.Tensor, masks: Dict[str, torch.Tensor]): + cfg = self.cfg + if self._rand() >= float(cfg.get("affine_p", 0.0)): + return x, masks + + h, w = x.shape[-2:] + angle = math.radians(self._uniform(-cfg.get("rotate_deg", 5.0), cfg.get("rotate_deg", 5.0))) + scale = self._uniform(cfg.get("scale_min", 0.90), cfg.get("scale_max", 1.10)) + trans = float(cfg.get("translate_frac", 0.04)) + tx = self._uniform(-trans, trans) * 2.0 + ty = self._uniform(-trans, trans) * 2.0 + + c = math.cos(angle) / max(scale, 1e-6) + s = math.sin(angle) / max(scale, 1e-6) + theta = x.new_tensor([[c, -s, tx], [s, c, ty]]).unsqueeze(0) + grid = F.affine_grid(theta, size=(1, x.shape[0], h, w), align_corners=False) + + x = F.grid_sample( + x.unsqueeze(0), grid, mode="bilinear", padding_mode="reflection", align_corners=False + ).squeeze(0) + + valid = ((grid[..., 0].abs() <= 1.0) & (grid[..., 1].abs() <= 1.0)).squeeze(0) + out_masks = {} + for name, m in masks.items(): + mi = F.grid_sample( + m.unsqueeze(0).unsqueeze(0).float(), + grid, + mode="nearest", + padding_mode="zeros", + align_corners=False, + ).squeeze(0).squeeze(0).long() + ignore_index = 255 + mi[~valid] = ignore_index + out_masks[name] = mi + return x, out_masks + + def _can_recompute_derived(self) -> bool: + names = set(self.channel_names) + if "NDVI" in names and not {"NIR", "R"}.issubset(names): + return False + if "NDRE" in names and not {"NIR", "RE"}.issubset(names): + return False + return True + + def _recompute_derived(self, x: torch.Tensor): + idx = {name: i for i, name in enumerate(self.channel_names)} + eps = float(self.derived_cfg.get("epsilon", 1e-6)) + clip_min = float(self.derived_cfg.get("clip_min", -1.0)) + clip_max = float(self.derived_cfg.get("clip_max", 1.0)) + + def nd(a, b): + denom = a + b + out = torch.zeros_like(a) + valid = denom.abs() > eps + out[valid] = (a[valid] - b[valid]) / denom[valid] + return out.clamp(clip_min, clip_max) + + if "NDVI" in idx and "NIR" in idx and "R" in idx: + x[idx["NDVI"]] = nd(x[idx["NIR"]], x[idx["R"]]) + if "NDRE" in idx and "NIR" in idx and "RE" in idx: + x[idx["NDRE"]] = nd(x[idx["NIR"]], x[idx["RE"]]) + return x + + def _blur(self, x: torch.Tensor, k: int): + k = int(k) + if k < 3: + return x + if k % 2 == 0: + k += 1 + return F.avg_pool2d(x.unsqueeze(0), kernel_size=k, stride=1, padding=k // 2).squeeze(0) + + def _apply_radiometric(self, x: torch.Tensor): + cfg = self.cfg + idx = {name: i for i, name in enumerate(self.channel_names)} + physical_idx = [idx[n] for n in SOURCE_CHANNEL_ORDER if n in idx] + has_derived = any(n in idx for n in DERIVED_CHANNEL_ORDER) + can_recompute = self._can_recompute_derived() + + if has_derived and not can_recompute and not self._warned_derived_missing: + print( + "[AUG][WARN] Há NDVI/NDRE sem todas as bandas físicas necessárias. " + "Jitters espectrais independentes serão pulados para preservar coerência." + ) + self._warned_derived_missing = True + + # Ganho global é seguro para índices de diferença normalizada se aplicado + # igualmente às bandas físicas relevantes. + if physical_idx and self._rand() < float(cfg.get("global_gain_p", 0.0)): + gain = self._uniform(cfg.get("global_gain_min", 0.92), cfg.get("global_gain_max", 1.08)) + x[physical_idx] *= gain + + if physical_idx and (not has_derived or can_recompute): + if self._rand() < float(cfg.get("band_gain_p", 0.0)): + lo = float(cfg.get("band_gain_min", 0.96)) + hi = float(cfg.get("band_gain_max", 1.04)) + for i in physical_idx: + x[i] *= self._uniform(lo, hi) + + if self._rand() < float(cfg.get("rgb_gamma_p", 0.0)): + gamma = self._uniform(cfg.get("rgb_gamma_min", 0.94), cfg.get("rgb_gamma_max", 1.06)) + for name in ("R", "G", "B"): + if name in idx: + x[idx[name]] = torch.clamp(x[idx[name]], min=0.0).pow(gamma) + + if self._rand() < float(cfg.get("noise_p", 0.0)): + sigma = self._uniform(cfg.get("noise_sigma_min", 0.001), cfg.get("noise_sigma_max", 0.008)) + noise = torch.randn_like(x[physical_idx]) * float(sigma) + x[physical_idx] += noise + + p_drop = float(cfg.get("sensor_channel_dropout_p", 0.0)) + if p_drop > 0 and self._rand() < p_drop: + candidates = [i for i in physical_idx if self.channel_names[i] not in ("R", "G", "B")] + if candidates: + x[random.choice(candidates)] = 0.0 + + if self._rand() < float(cfg.get("blur_p", 0.0)): + x = self._blur(x, int(cfg.get("blur_kernel", 3))) + + if bool(cfg.get("clip_physical", True)): + for i in physical_idx: + x[i].clamp_(0.0, 1.0) + + if has_derived and can_recompute: + x = self._recompute_derived(x) + + return x + + def __call__(self, x: torch.Tensor, masks: Dict[str, torch.Tensor]): + if not bool(self.cfg.get("enabled", True)): + return x, masks + + x, masks = self._apply_crop(x, masks) + x, masks = self._apply_affine(x, masks) + + if self._rand() < float(self.cfg.get("horizontal_flip_p", 0.0)): + x = torch.flip(x, dims=[2]) + masks = {k: torch.flip(v, dims=[1]) for k, v in masks.items()} + + if self._rand() < float(self.cfg.get("vertical_flip_p", 0.0)): + x = torch.flip(x, dims=[1]) + masks = {k: torch.flip(v, dims=[0]) for k, v in masks.items()} + + x = self._apply_radiometric(x) + return x.contiguous(), {k: v.contiguous() for k, v in masks.items()} + + +def inspect_npy_integrity(path: str | Path, expected_ndim: Optional[int] = None) -> dict: + """ + Valida header + tamanho do payload de um .npy sem criar memory-map. + + Isso evita esgotar recursos de mapeamento do Windows durante auditorias de + milhares de arquivos e ainda detecta truncamento real do payload. + """ + p = Path(path) + if not p.exists(): + raise FileNotFoundError(str(p)) + + file_size = int(p.stat().st_size) + with p.open("rb") as f: + version = np.lib.format.read_magic(f) + if version == (1, 0): + shape, fortran_order, dtype = np.lib.format.read_array_header_1_0(f) + elif version in ((2, 0), (3, 0)): + # v2/v3 usam header longo. Em caso de implementação NumPy antiga, + # o fallback abaixo ainda faz uma leitura convencional sem mmap. + try: + shape, fortran_order, dtype = np.lib.format.read_array_header_2_0(f) + except Exception: + arr = np.load(str(p), allow_pickle=False) + shape, dtype = tuple(arr.shape), arr.dtype + fortran_order = bool(arr.flags.f_contiguous and not arr.flags.c_contiguous) + if expected_ndim is not None and arr.ndim != int(expected_ndim): + raise ValueError(f"ndim={arr.ndim}, esperado={expected_ndim}, shape={arr.shape}") + return {"shape": tuple(shape), "dtype": str(dtype), "fortran_order": fortran_order, "file_size": file_size} + else: + arr = np.load(str(p), allow_pickle=False) + shape, dtype = tuple(arr.shape), arr.dtype + fortran_order = bool(arr.flags.f_contiguous and not arr.flags.c_contiguous) + if expected_ndim is not None and arr.ndim != int(expected_ndim): + raise ValueError(f"ndim={arr.ndim}, esperado={expected_ndim}, shape={arr.shape}") + return {"shape": tuple(shape), "dtype": str(dtype), "fortran_order": fortran_order, "file_size": file_size} + + shape = tuple(int(v) for v in shape) + dtype = np.dtype(dtype) + data_offset = int(f.tell()) + + if expected_ndim is not None and len(shape) != int(expected_ndim): + raise ValueError(f"ndim={len(shape)}, esperado={expected_ndim}, shape={shape}") + if any(v <= 0 for v in shape): + raise ValueError(f"shape inválido={shape}") + if dtype.hasobject: + # Dataset numérico não deveria usar object. Leitura convencional para + # produzir diagnóstico explícito em vez de confiar no tamanho bruto. + arr = np.load(str(p), allow_pickle=False) + return {"shape": tuple(arr.shape), "dtype": str(arr.dtype), "fortran_order": fortran_order, "file_size": file_size} + + expected_payload = int(np.prod(shape, dtype=np.int64)) * int(dtype.itemsize) + actual_payload = max(0, file_size - data_offset) + if actual_payload < expected_payload: + raise ValueError( + f"payload truncado: shape={shape} dtype={dtype} " + f"esperado={expected_payload} bytes, encontrado={actual_payload} bytes" + ) + + return { + "shape": shape, + "dtype": str(dtype), + "fortran_order": bool(fortran_order), + "file_size": file_size, + "payload_bytes": actual_payload, + "expected_payload_bytes": expected_payload, + } + + +# ============================================================ +# Dataset OAK-FCC-3 multi-head +# ============================================================ + +class OakFcc3TensorMultiHeadDataset(Dataset): + def __init__( + self, + root: str | Path, + heads_config: Dict[str, dict], + channels: int = 5, + input_channel_names: Optional[List[str]] = None, + strict_channels: bool = False, + resize_hw: Optional[Tuple[int, int]] = None, + augment: bool = False, + augmentation_config: Optional[dict] = None, + derived_config: Optional[dict] = None, + data_validation_config: Optional[dict] = None, + ): + self.root = Path(root) + self.heads_config = heads_config + self.channels = int(channels) + self.input_channel_names = list(input_channel_names or SOURCE_CHANNEL_ORDER[:self.channels]) + self.strict_channels = bool(strict_channels) + self.resize_hw = resize_hw + self.augment = bool(augment) + self.augmenter = MultiSpectralAugmenter( + self.input_channel_names, + augmentation_config or {}, + derived_cfg=derived_config or {}, + ) if self.augment else None + self.data_validation_config = data_validation_config or {} + + self.samples = self._collect_samples() + if not self.samples: + raise RuntimeError(f"Nenhuma amostra encontrada em: {self.root}") + if bool(self.data_validation_config.get("validate_npy_content", True)): + self._validate_and_filter_npy_samples() + self._validate_tensor_contract() + self._add_sample_class_stats() + + def _collect_samples(self): + samples = [] + group_root = self.root / "group" + if not group_root.is_dir(): + raise RuntimeError(f"Pasta group não encontrada em: {self.root}") + + for group_dir in sorted(group_root.iterdir()): + if not group_dir.is_dir(): + continue + tensors_dir = group_dir / "tensors" + metas_dir = group_dir / "metas" + previews_dir = group_dir / "previews" + if not tensors_dir.is_dir(): + continue + + for tensor_path in sorted(tensors_dir.glob("*.npy")): + base = tensor_path.stem + masks, missing = {}, [] + for head_name, hcfg in self.heads_config.items(): + mask_dir_name = str(hcfg.get("mask_dir")) + if mask_dir_name == "__derived_target__" or bool(hcfg.get("derived", False)): + masks[head_name] = None + continue + mask_path = group_dir / mask_dir_name / f"{base}.npy" + if not mask_path.exists(): + missing.append(f"{head_name}:{mask_path}") + else: + masks[head_name] = mask_path + if missing: + print(f"[WARN] Pulando {tensor_path}, masks ausentes: {missing}") + continue + + meta_path = metas_dir / f"{base}.json" + preview_path = previews_dir / f"{base}.png" + tensor_channels = None + if meta_path.exists(): + meta = load_json(meta_path) + meta_channels = meta.get("channels") + if isinstance(meta_channels, list) and meta_channels: + tensor_channels = [str(c).strip().upper() for c in meta_channels] + + samples.append({ + "group": group_dir.name, + "base": base, + "tensor": tensor_path, + "masks": masks, + "meta": meta_path if meta_path.exists() else None, + "preview": preview_path if preview_path.exists() else None, + "tensor_channels": tensor_channels, + }) + return samples + + def _resolve_tensor_channel_indices(self, sample: dict, tensor_c: int) -> List[int]: + saved_names = sample.get("tensor_channels") + if saved_names: + if len(saved_names) != tensor_c: + raise RuntimeError( + f"Meta/tensor incompatíveis em {sample['tensor']}: meta.channels={saved_names}, tensor C={tensor_c}." + ) + if len(set(saved_names)) != len(saved_names): + raise RuntimeError(f"Meta possui canais duplicados em {sample['tensor']}: {saved_names}") + missing = [name for name in self.input_channel_names if name not in saved_names] + if missing: + raise RuntimeError( + f"Tensor {sample['tensor']} não possui canais {missing}. Salvos={saved_names}, pedidos={self.input_channel_names}." + ) + return [saved_names.index(name) for name in self.input_channel_names] + + if tensor_c == self.channels: + return list(range(self.channels)) + if tensor_c == len(SOURCE_CHANNEL_ORDER) and all(name in SOURCE_CHANNEL_ORDER for name in self.input_channel_names): + return [SOURCE_CHANNEL_ORDER.index(name) for name in self.input_channel_names] + raise RuntimeError( + f"Não foi possível inferir canais de {sample['tensor']}: tensor C={tensor_c}, " + f"pedidos={self.input_channel_names}, meta.channels ausente." + ) + + def _validate_and_filter_npy_samples(self): + """ + Audita .npy por header + tamanho de payload, sem mmap. + + A versão anterior usava milhares de memory-maps e podia gerar WinError 8 + no Windows por falta de recursos de mapeamento, classificando um arquivo + saudável como "corrupt". Aqui só tratamos como corrupção problemas + estruturais reais do arquivo. + """ + cfg = self.data_validation_config or {} + skip_corrupt = bool(cfg.get("skip_corrupt_samples", True)) + max_corrupt_fraction = max(0.0, float(cfg.get("max_corrupt_fraction", 0.005))) + + original_count = len(self.samples) + good_samples = [] + bad_samples = [] + + for s in self.samples: + failures = [] + try: + info = inspect_npy_integrity(s["tensor"], expected_ndim=3) + shape = tuple(info["shape"]) + if int(shape[0]) <= 0 or int(shape[1]) <= 0 or int(shape[2]) <= 0: + failures.append(f"tensor:invalid_shape={shape}") + except Exception as exc: + failures.append(f"tensor:{type(exc).__name__}:{exc} | path={s['tensor']}") + + for head_name, path in (s.get("masks") or {}).items(): + if path is None: + continue + try: + info = inspect_npy_integrity(path, expected_ndim=2) + shape = tuple(info["shape"]) + if int(shape[0]) <= 0 or int(shape[1]) <= 0: + failures.append(f"{head_name}:invalid_shape={shape} | path={path}") + except Exception as exc: + failures.append(f"{head_name}:{type(exc).__name__}:{exc} | path={path}") + + if failures: + bad_samples.append({ + "group": s.get("group"), + "base": s.get("base"), + "tensor": str(s.get("tensor")), + "failures": failures, + }) + else: + good_samples.append(s) + + if not bad_samples: + print(f"[DATA][INTEGRITY] root={self.root} OK | samples={original_count}") + return + + print( + f"[DATA][CORRUPT][WARN] root={self.root} " + f"corrupt={len(bad_samples)}/{original_count}" + ) + for item in bad_samples[:50]: + print( + f"[DATA][CORRUPT] group={item['group']} base={item['base']} " + f"tensor={item['tensor']}" + ) + for failure in item["failures"]: + print(f" - {failure}") + if len(bad_samples) > 50: + print(f"[DATA][CORRUPT] ... +{len(bad_samples)-50} amostras não exibidas") + + corrupt_fraction = len(bad_samples) / max(1, original_count) + if corrupt_fraction > max_corrupt_fraction: + raise RuntimeError( + "Corrupção de dataset acima do limite de segurança: " + f"{len(bad_samples)}/{original_count} ({corrupt_fraction*100:.3f}%) > " + f"{max_corrupt_fraction*100:.3f}%. Corrija o dataset antes de treinar." + ) + + if not skip_corrupt: + first = bad_samples[0] + raise RuntimeError( + "Amostra .npy corrompida encontrada e skip_corrupt_samples=false: " + f"group={first['group']} base={first['base']} failures={first['failures']}" + ) + + self.samples = good_samples + print( + f"[DATA][CORRUPT][SKIP] root={self.root} removidas={len(bad_samples)} " + f"restantes={len(self.samples)} | limite={max_corrupt_fraction*100:.3f}%" + ) + if not self.samples: + raise RuntimeError(f"Todas as amostras foram removidas por corrupção em: {self.root}") + + def _validate_tensor_contract(self): + """ + Valida o contrato EFETIVO dos tensores. + + Importante: + - metas novos podem trazer meta.channels explicitamente; + - metas legados podem não trazer essa lista; + - quando C == self.channels, _resolve_tensor_channel_indices() já define + que a ordem implícita é exatamente self.input_channel_names. + + Portanto, strict_channels deve comparar o contrato resolvido, e não + simplesmente diferenciar "meta com channels" de "meta sem channels". + """ + contracts = set() + spatial = set() + missing_channel_meta = 0 + + for sample in self.samples: + info = inspect_npy_integrity(sample["tensor"], expected_ndim=3) + shape = tuple(info["shape"]) + tensor_c = int(shape[0]) + indices = self._resolve_tensor_channel_indices(sample, tensor_c) + if len(indices) != self.channels: + raise RuntimeError( + f"Contrato de canais inválido em {sample['tensor']}: indices={indices}" + ) + + saved_names = sample.get("tensor_channels") + if saved_names: + # Contrato nominal explícito no meta, na ordem efetivamente usada. + effective_names = tuple(saved_names[i] for i in indices) + else: + # Contrato legado implícito já validado por + # _resolve_tensor_channel_indices(). + missing_channel_meta += 1 + effective_names = tuple(self.input_channel_names) + + contracts.add((effective_names, len(indices))) + spatial.add(tuple(shape[-2:])) + + if self.strict_channels and len(contracts) > 1: + raise RuntimeError( + f"Contratos de tensor efetivos diferentes no mesmo split: " + f"{sorted(contracts, key=str)}" + ) + + if missing_channel_meta > 0: + print( + f"[DATA][CHANNELS][WARN] root={self.root} " + f"amostras_sem_meta_channels={missing_channel_meta}/{len(self.samples)} | " + f"assumindo ordem={self.input_channel_names} porque C={self.channels}." + ) + + print( + f"[DATA][CHANNELS] root={self.root} pedidos={self.input_channel_names} " + f"contratos_efetivos={sorted(contracts, key=str)}" + ) + print( + f"[DATA][SPATIAL] root={self.root} shapes={sorted(spatial)} " + f"resize_hw={self.resize_hw}" + ) + + def __len__(self): + return len(self.samples) + + def _resize_tensor(self, x: torch.Tensor): + if self.resize_hw is None: + return x + h, w = self.resize_hw + if x.shape[-2:] != (h, w): + x = F.interpolate(x.unsqueeze(0), size=(h, w), mode="bilinear", align_corners=False).squeeze(0) + return x + + def _resize_mask(self, y: torch.Tensor): + if self.resize_hw is None: + return y + h, w = self.resize_hw + if y.shape[-2:] != (h, w): + y = F.interpolate(y.unsqueeze(0).unsqueeze(0).float(), size=(h, w), mode="nearest").squeeze().long() + return y + + def _load_masks_for_sample(self, s: dict, do_resize: bool = True) -> Dict[str, torch.Tensor]: + masks = {} + for head_name, path in s["masks"].items(): + if path is None: + continue + y = np.load(str(path)).astype(np.int64) + yt = torch.from_numpy(np.ascontiguousarray(y)).long() + if do_resize: + yt = self._resize_mask(yt) + masks[head_name] = yt + + if "target" in self.heads_config: + if "vegetation" not in masks or "cana" not in masks: + raise RuntimeError("Head target requer masks vegetation e cana.") + ignore_index = int(self.heads_config["target"].get("ignore_index", 255)) + veg, cana = masks["vegetation"], masks["cana"] + valid = (veg != ignore_index) & (cana != ignore_index) + target = torch.zeros_like(veg, dtype=torch.long) + target[(veg == 1) & (cana == 0)] = 1 + target[~valid] = ignore_index + masks["target"] = target + return masks + + def load_mask_numpy(self, sample_idx: int, head_name: str) -> np.ndarray: + s = self.samples[int(sample_idx)] + if head_name == "target": + masks = self._load_masks_for_sample(s, do_resize=False) + return masks["target"].numpy() + path = s["masks"].get(head_name) + if path is None: + raise RuntimeError(f"Mask física ausente para head={head_name}") + return np.load(str(path)).astype(np.int64) + + def __getitem__(self, idx): + s = self.samples[idx] + x = np.load(str(s["tensor"])).astype(np.float32) + if x.ndim != 3: + raise RuntimeError(f"Tensor inválido {s['tensor']}: shape={x.shape}") + indices = self._resolve_tensor_channel_indices(s, int(x.shape[0])) + x = x[indices, :, :] + xt = self._resize_tensor(torch.from_numpy(np.ascontiguousarray(x)).float()) + masks = self._load_masks_for_sample(s, do_resize=True) + + if self.augmenter is not None: + xt, masks = self.augmenter(xt, masks) + + return {"image": xt, "masks": masks, "group": s["group"], "base": s["base"]} + + def _add_sample_class_stats(self): + sem_cfg = self.heads_config.get("semantic", {}) + classes = sem_cfg.get("classes", {}) or {} + chao_id = int(classes.get("chao", 0)) + cana_id = int(classes.get("cana", 1)) + erva_id = int(classes.get("erva", 2)) + ignore = int(sem_cfg.get("ignore_index", 255)) + + for s in self.samples: + stats = { + "pixels_total": 0, "pixels_chao": 0, "pixels_cana": 0, "pixels_erva": 0, + "pixels_vegetation": 0, "pixels_target": 0, + "pct_cana": 0.0, "pct_erva": 0.0, "pct_target": 0.0, + "has_cana": False, "has_erva": False, "has_target": False, + } + sem_path = s["masks"].get("semantic") + veg_path = s["masks"].get("vegetation") + cana_path = s["masks"].get("cana") + + if sem_path is not None and Path(sem_path).exists(): + sem = np.load(str(sem_path)).astype(np.int64) + valid = sem != ignore + total = int(valid.sum()) + stats["pixels_total"] = total + if total > 0: + stats["pixels_chao"] = int(((sem == chao_id) & valid).sum()) + stats["pixels_cana"] = int(((sem == cana_id) & valid).sum()) + stats["pixels_erva"] = int(((sem == erva_id) & valid).sum()) + stats["pct_cana"] = stats["pixels_cana"] / total + stats["pct_erva"] = stats["pixels_erva"] / total + + if veg_path is not None and cana_path is not None: + veg = np.load(str(veg_path)).astype(np.int64) + cana = np.load(str(cana_path)).astype(np.int64) + valid = (veg != ignore) & (cana != ignore) + total = int(valid.sum()) + if total > 0: + target = (veg == 1) & (cana == 0) & valid + stats["pixels_vegetation"] = int(((veg == 1) & valid).sum()) + stats["pixels_target"] = int(target.sum()) + stats["pct_target"] = stats["pixels_target"] / total + + stats["has_cana"] = stats["pixels_cana"] > 0 + stats["has_erva"] = stats["pixels_erva"] > 0 + stats["has_target"] = stats["pixels_target"] > 0 + s["class_stats"] = stats + + +def collate_fn(batch): + imgs = torch.stack([b["image"] for b in batch], dim=0) + head_names = list(batch[0]["masks"].keys()) + masks = {h: torch.stack([b["masks"][h] for b in batch], dim=0) for h in head_names} + meta = {"group": [b["group"] for b in batch], "base": [b["base"] for b in batch]} + return imgs, masks, meta + + +# ============================================================ +# Diverse agricultural batch sampler +# ============================================================ + +class DiverseAgriculturalBatchSampler(Sampler[List[int]]): + """Monta batches com target, cana, mistos e negativos sempre que possível.""" + + def __init__(self, ds: OakFcc3TensorMultiHeadDataset, batch_size: int, cfg: dict, seed: int = 42): + self.ds = ds + self.batch_size = int(batch_size) + self.cfg = cfg or {} + self.seed = int(seed) + self.epoch = 0 + self.samples_per_epoch = int(self.cfg.get("samples_per_epoch", 0) or 0) or len(ds) + self.num_batches = max(1, math.ceil(self.samples_per_epoch / max(1, self.batch_size))) + self.pools = self._build_pools() + self._print_summary() + + def _build_pools(self): + tiny = float(self.cfg.get("tiny_target_pct", 0.005)) + small = float(self.cfg.get("small_target_pct", 0.02)) + medium = float(self.cfg.get("medium_target_pct", 0.10)) + pools = {k: [] for k in [ + "mixed", "target_only", "cana_only", "ground", + "target_tiny", "target_small", "target_medium", "target_large", "any" + ]} + for i, s in enumerate(self.ds.samples): + st = s.get("class_stats", {}) + has_t = bool(st.get("has_target", False)) + has_c = bool(st.get("has_cana", False)) + pct = float(st.get("pct_target", 0.0)) + pools["any"].append(i) + if has_t and has_c: + pools["mixed"].append(i) + elif has_t: + pools["target_only"].append(i) + elif has_c: + pools["cana_only"].append(i) + else: + pools["ground"].append(i) + if has_t: + if pct <= tiny: + pools["target_tiny"].append(i) + elif pct <= small: + pools["target_small"].append(i) + elif pct <= medium: + pools["target_medium"].append(i) + else: + pools["target_large"].append(i) + return pools + + def _print_summary(self): + txt = " | ".join(f"{k}={len(v)}" for k, v in self.pools.items() if k != "any") + print(f"[SAMPLER:DIVERSE] batches={self.num_batches} batch={self.batch_size} | {txt}") + + def set_epoch(self, epoch: int): + self.epoch = int(epoch) + + def __len__(self): + return self.num_batches + + def __iter__(self): + rng = random.Random(self.seed + self.epoch * 100003) + queues = {} + pos = {} + for k, vals in self.pools.items(): + q = list(vals) + rng.shuffle(q) + queues[k] = q + pos[k] = 0 + + def draw(pool_name: str, used: set): + q = queues.get(pool_name) or [] + if not q: + return None + for _ in range(max(1, len(q) * 2)): + if pos[pool_name] >= len(q): + rng.shuffle(q) + pos[pool_name] = 0 + idx = q[pos[pool_name]] + pos[pool_name] += 1 + if idx not in used: + return idx + return None + + pattern = [ + "mixed", "target_tiny", "cana_only", "ground", + "target_small", "target_only", "target_medium", "target_large", + ] + + for b in range(self.num_batches): + used, batch = set(), [] + offset = b % len(pattern) + desired = pattern[offset:] + pattern[:offset] + for pool_name in desired: + if len(batch) >= self.batch_size: + break + idx = draw(pool_name, used) + if idx is not None: + batch.append(idx) + used.add(idx) + + while len(batch) < self.batch_size: + idx = draw("any", used) + if idx is None: + idx = rng.randrange(len(self.ds)) + batch.append(idx) + used.add(idx) + yield batch + + +def build_sample_weights(ds, mode="target_focus"): + weights = [] + for s in ds.samples: + st = s.get("class_stats", {}) + has_cana = bool(st.get("has_cana", False)) + has_target = bool(st.get("has_target", False)) + pct_target = float(st.get("pct_target", 0.0)) + w = 1.0 + if not has_cana and not has_target: + w *= 0.55 + if has_cana: + w *= 1.4 + if has_target: + w *= 2.4 + w *= 1.0 + min(2.0, 30.0 * pct_target) + weights.append(max(0.20, min(w, 8.0))) + return torch.tensor(weights, dtype=torch.double) + + +# ============================================================ +# Normalization +# ============================================================ + +class FixedNormalizer(nn.Module): + def __init__(self, mean: List[float], std: List[float]): + super().__init__() + self.register_buffer("mean", torch.tensor(mean, dtype=torch.float32).view(1, -1, 1, 1)) + self.register_buffer("std", torch.clamp(torch.tensor(std, dtype=torch.float32).view(1, -1, 1, 1), min=1e-6)) + def forward(self, x): + return (x - self.mean) / self.std + + +def normalize_per_batch(x: torch.Tensor, eps: float = 1e-6): + mean = x.mean(dim=(0, 2, 3), keepdim=True) + std = x.std(dim=(0, 2, 3), keepdim=True).clamp_min(eps) + return (x - mean) / std + + +def build_normalizer(config: dict, args, device: torch.device): + input_channel_names = get_input_channel_names(config) + channels = len(input_channel_names) + model_family = config.get("modelo", "segformer") + model_name = config.get("model_name", "model") + stats_source_tag = config.get("stats_source_tag", f"stacked_raw{channels}") + candidates = [] + if args.norm_stats: + candidates.append(Path(args.norm_stats)) + candidates.append(Path("dataset") / f"{config['resolucao'][0]}x{config['resolucao'][1]}" / "group" / "norm_stats.json") + candidates.append(Path("backup") / model_family / model_name / stats_source_tag / "norm_stats.json") + + for p in candidates: + if not p.exists(): + continue + stats = load_json(p) + mean, std = stats.get("mean", []), stats.get("std", []) + raw_channels = stats.get("channels", []) + stat_channels = [str(c).strip().upper() for c in raw_channels] if isinstance(raw_channels, list) else [] + if len(mean) != len(std): + raise RuntimeError(f"norm_stats incompatível: {p} | mean={len(mean)} std={len(std)}") + if stat_channels: + if len(stat_channels) != len(mean) or len(set(stat_channels)) != len(stat_channels): + raise RuntimeError(f"norm_stats channels inválidos: {p} | {stat_channels}") + missing = [n for n in input_channel_names if n not in stat_channels] + if missing: + raise RuntimeError(f"norm_stats {p} não possui {missing}. Disponíveis={stat_channels}") + ids = [stat_channels.index(n) for n in input_channel_names] + mean, std = [mean[i] for i in ids], [std[i] for i in ids] + selected = [stat_channels[i] for i in ids] + elif len(mean) == channels: + selected = list(input_channel_names) + elif len(mean) == len(SOURCE_CHANNEL_ORDER) and all(n in SOURCE_CHANNEL_ORDER for n in input_channel_names): + ids = [SOURCE_CHANNEL_ORDER.index(n) for n in input_channel_names] + mean, std = [mean[i] for i in ids], [std[i] for i in ids] + selected = list(input_channel_names) + else: + raise RuntimeError(f"norm_stats sem nomes incompatível: {p} mean/std={len(mean)} pedidos={input_channel_names}") + + ma, sa = np.asarray(mean, np.float64), np.asarray(std, np.float64) + if not np.isfinite(ma).all() or not np.isfinite(sa).all() or np.any(sa <= 0): + raise RuntimeError(f"norm_stats contém valores inválidos: {p}") + print(f"[NORM] usando stats fixos: {p}") + print(f"[NORM] selected_channels={selected}") + return FixedNormalizer(mean, std).to(device), str(p) + + if bool(getattr(args, "allow_missing_norm_stats", False)): + print("[NORM][WARN] norm_stats ausente; usando normalize_per_batch.") + return None, None + raise RuntimeError( + "norm_stats não encontrado. Use --norm_stats ou --allow-missing-norm-stats conscientemente. " + f"Candidatos={[str(p) for p in candidates]}" + ) + + +# ============================================================ +# Model: separate decoders or shared-light decoder +# ============================================================ + +def patch_segformer_encoder_input_channels(segformer_encoder: nn.Module, input_channel_names: List[str], init_mode: str = "zero_extra"): + names = [str(c).strip().upper() for c in input_channel_names] + if names == ["R", "G", "B"]: + return segformer_encoder + + # Transformers <= versões usadas hoje no projeto: .encoder.patch_embeddings[0].proj + # Transformers recentes: .stages[0].patch_embeddings.proj + if hasattr(segformer_encoder, "encoder") and hasattr(segformer_encoder.encoder, "patch_embeddings"): + patch_owner = segformer_encoder.encoder.patch_embeddings[0] + elif hasattr(segformer_encoder, "stages") and len(segformer_encoder.stages) > 0: + patch_owner = segformer_encoder.stages[0].patch_embeddings + else: + raise RuntimeError("Não encontrei o primeiro patch embedding do SegFormer nesta versão do Transformers.") + + proj = patch_owner.proj + old_weight = proj.weight.data.clone() + old_bias = proj.bias.data.clone() if proj.bias is not None else None + new_proj = nn.Conv2d( + in_channels=len(names), out_channels=proj.out_channels, + kernel_size=proj.kernel_size, stride=proj.stride, padding=proj.padding, + dilation=proj.dilation, groups=proj.groups, bias=proj.bias is not None, + padding_mode=proj.padding_mode, + ) + + mode = str(init_mode or "zero_extra").lower() + with torch.no_grad(): + pretrained = {"R": 0, "G": 1, "B": 2} + mean_rgb = old_weight[:, :3].mean(dim=1) + has_rgb = any(n in pretrained for n in names) + for dst, name in enumerate(names): + if name in pretrained: + new_proj.weight[:, dst].copy_(old_weight[:, pretrained[name]]) + elif mode == "mean_rgb": + new_proj.weight[:, dst].copy_(mean_rgb) + elif mode == "scaled_mean": + new_proj.weight[:, dst].copy_(mean_rgb * (3.0 / max(3.0, float(len(names))))) + elif mode == "zero_extra" and has_rgb: + new_proj.weight[:, dst].zero_() + else: + new_proj.weight[:, dst].copy_(mean_rgb) + if old_bias is not None: + new_proj.bias.copy_(old_bias) + + patch_owner.proj = new_proj + print(f"[MODEL] input patch RGB(3) -> {names} | init={mode}") + return segformer_encoder + + +def replace_segformer_decode_classifier(decode_head: nn.Module, num_classes: int): + old = getattr(decode_head, "classifier", None) + if not isinstance(old, nn.Conv2d): + raise RuntimeError(f"decode_head.classifier inesperado: {type(old)}") + new = nn.Conv2d(old.in_channels, int(num_classes), kernel_size=old.kernel_size, stride=old.stride, + padding=old.padding, dilation=old.dilation, groups=old.groups, + bias=old.bias is not None, padding_mode=old.padding_mode) + nn.init.xavier_uniform_(new.weight) + if new.bias is not None: + nn.init.zeros_(new.bias) + decode_head.classifier = new + return decode_head + + +class MultiHeadSegFormer(nn.Module): + def __init__(self, backbone: str, input_channel_names: List[str], heads_config: Dict[str, dict], + semantic_id2label: Dict[int, str], semantic_label2id: Dict[str, int], + model_cfg: Optional[dict] = None): + super().__init__() + model_cfg = model_cfg or {} + semantic_classes = int(heads_config["semantic"].get("num_classes", len(semantic_id2label))) + names = [str(c).strip().upper() for c in input_channel_names] + base = SegformerForSemanticSegmentation.from_pretrained( + backbone, num_labels=semantic_classes, + id2label={int(k): str(v) for k, v in semantic_id2label.items()}, + label2id={str(k): int(v) for k, v in semantic_label2id.items()}, + ignore_mismatched_sizes=True, + ) + self.decoder_mode = str(model_cfg.get("decoder_mode", "shared_light")).lower() + self.spectral_input_init = str(model_cfg.get("spectral_input_init", "zero_extra")).lower() + patch_segformer_encoder_input_channels(base.segformer, names, self.spectral_input_init) + base.config.num_channels = len(names) + base.config.input_channel_names = list(names) + base.config.agri_decoder_mode = self.decoder_mode + base.config.agri_trainer_version = TRAINER_VERSION + base.config.agri_spectral_input_init = self.spectral_input_init + + self.segformer = base.segformer + self.heads_config = heads_config + + if self.decoder_mode == "separate": + self.decode_heads = nn.ModuleDict() + for head_name, hcfg in heads_config.items(): + h = copy.deepcopy(base.decode_head) + self.decode_heads[head_name] = replace_segformer_decode_classifier(h, int(hcfg["num_classes"])) + self.shared_decode_head = None + self.classifiers = None + + elif self.decoder_mode == "shared_light": + self.decode_heads = None + self.shared_decode_head = copy.deepcopy(base.decode_head) + old_classifier = self.shared_decode_head.classifier + if not isinstance(old_classifier, nn.Conv2d): + raise RuntimeError("SegFormer decode_head.classifier não é Conv2d.") + feature_channels = int(old_classifier.in_channels) + self.shared_decode_head.classifier = nn.Identity() + self.classifiers = nn.ModuleDict() + for head_name, hcfg in heads_config.items(): + clf = nn.Conv2d(feature_channels, int(hcfg["num_classes"]), kernel_size=1, bias=True) + nn.init.xavier_uniform_(clf.weight) + nn.init.zeros_(clf.bias) + self.classifiers[head_name] = clf + else: + raise RuntimeError(f"training_v2.model.decoder_mode inválido: {self.decoder_mode}") + + self.config = base.config + print(f"[MODEL] decoder_mode={self.decoder_mode}") + + def forward(self, pixel_values: torch.Tensor) -> Dict[str, torch.Tensor]: + outputs = self.segformer(pixel_values=pixel_values, output_hidden_states=True, return_dict=True) + hidden_states = outputs.hidden_states + if self.decoder_mode == "separate": + return {name: head(hidden_states) for name, head in self.decode_heads.items()} + features = self.shared_decode_head(hidden_states) + return {name: clf(features) for name, clf in self.classifiers.items()} + + +def build_model(backbone: str, input_channel_names: List[str], heads_config: Dict[str, dict], + semantic_id2label: Dict[int, str], semantic_label2id: Dict[str, int], config: Optional[dict] = None): + global _LAST_BUILD_CONFIG + effective_config = config or _LAST_BUILD_CONFIG or {} + tcfg = get_training_v2_config(effective_config) + return MultiHeadSegFormer( + backbone=backbone, + input_channel_names=input_channel_names, + heads_config=heads_config, + semantic_id2label=semantic_id2label, + semantic_label2id=semantic_label2id, + model_cfg=tcfg.get("model", {}), + ) + + +# Override build_heads_config only to keep export/validate aware of the config +_build_heads_config_base = build_heads_config + +def build_heads_config(config: dict, ignore_index: int) -> Dict[str, dict]: + global _LAST_BUILD_CONFIG + _LAST_BUILD_CONFIG = copy.deepcopy(config) + return _build_heads_config_base(config, ignore_index) + + +# ============================================================ +# Class weighting / optimizer / scheduler +# ============================================================ + +def estimate_head_class_weights(ds: OakFcc3TensorMultiHeadDataset, head_name: str, num_classes: int, + ignore_index: int, method_cfg: dict): + counts = np.zeros(num_classes, dtype=np.float64) + for i in range(len(ds)): + m = ds.load_mask_numpy(i, head_name).reshape(-1) + m = m[(m != ignore_index) & (m >= 0) & (m < num_classes)] + if m.size: + counts += np.bincount(m, minlength=num_classes)[:num_classes] + freq = counts / max(counts.sum(), 1.0) + freq = np.clip(freq, 1e-12, 1.0) + method = str(method_cfg.get("method", "log_inverse")).lower() + if method == "sqrt_inverse": + power = float(method_cfg.get("power", 0.5)) + weights = np.power(1.0 / freq, power) + elif method == "none": + weights = np.ones_like(freq) + else: + offset = float(method_cfg.get("log_offset", 1.02)) + weights = 1.0 / np.log(offset + freq) + weights = weights / max(weights.mean(), 1e-12) + weights = np.clip(weights, float(method_cfg.get("min_weight", 0.25)), float(method_cfg.get("max_weight", 4.0))) + weights = weights / max(weights.mean(), 1e-12) + return torch.tensor(weights, dtype=torch.float32), counts + + +def parse_class_weights_string(s: str) -> Dict[str, List[float]]: + out, txt = {}, str(s).strip() + if not txt: + return out + if "=" not in txt: + out["__single__"] = [float(x) for x in txt.split(",") if x.strip()] + return out + for block in txt.split(";"): + if not block.strip(): + continue + k, v = block.split("=", 1) + out[k.strip()] = [float(x) for x in v.split(",") if x.strip()] + return out + + +def build_criterions(ds_train, heads_config, class_weights_mode, device, seed, weighting_cfg: Optional[dict] = None): + criterions, debug = {}, {} + weighting_cfg = weighting_cfg or {} + parsed_manual = parse_class_weights_string(class_weights_mode) if str(class_weights_mode).lower() not in ("auto", "none") else {} + for head_name, hcfg in heads_config.items(): + ncls, ignore = int(hcfg["num_classes"]), int(hcfg.get("ignore_index", 255)) + weights, counts = None, None + mode = str(class_weights_mode).lower() + if mode == "auto": + w, counts = estimate_head_class_weights(ds_train, head_name, ncls, ignore, weighting_cfg) + weights = w.to(device) + elif mode != "none": + vals = parsed_manual.get(head_name, parsed_manual.get("__single__") if head_name == "semantic" else None) + if vals is not None: + if len(vals) != ncls: + raise RuntimeError(f"Pesos de {head_name} precisam ter {ncls} valores: {vals}") + weights = torch.tensor(vals, dtype=torch.float32, device=device) + criterions[head_name] = nn.CrossEntropyLoss(weight=weights, ignore_index=ignore) + debug[head_name] = { + "weights": weights.detach().cpu().tolist() if weights is not None else None, + "counts": counts.astype(np.int64).tolist() if counts is not None else None, + } + print(f"[LOSS:{head_name}] counts={debug[head_name]['counts']} weights={debug[head_name]['weights']}") + return criterions, debug + + +def _normalization_parameter_names(model: nn.Module) -> set: + norm_types = (nn.LayerNorm, nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.GroupNorm, nn.InstanceNorm1d, nn.InstanceNorm2d, nn.InstanceNorm3d) + names = set() + for module_name, module in model.named_modules(): + if isinstance(module, norm_types): + for pn, _ in module.named_parameters(recurse=False): + names.add(f"{module_name}.{pn}" if module_name else pn) + return names + + +def build_optimizer(model: nn.Module, base_lr: float, base_wd: float, cfg: dict): + encoder_lr = float(cfg.get("encoder_lr") if cfg.get("encoder_lr") is not None else base_lr) + wd = float(cfg.get("weight_decay") if cfg.get("weight_decay") is not None else base_wd) + patch_mult = float(cfg.get("patch_lr_mult", 2.0)) + heads_mult = float(cfg.get("heads_lr_mult", 5.0)) + norm_names = _normalization_parameter_names(model) + groups = {} + + def group_name(name: str): + is_patch = ( + name.startswith("segformer.encoder.patch_embeddings.0") or + name.startswith("segformer.stages.0.patch_embeddings") + ) + is_encoder = name.startswith("segformer.") + if is_patch: + section, lr = "patch", encoder_lr * patch_mult + elif is_encoder: + section, lr = "encoder", encoder_lr + else: + section, lr = "heads", encoder_lr * heads_mult + no_decay = ( + (bool(cfg.get("no_decay_bias", True)) and name.endswith(".bias")) or + (bool(cfg.get("no_decay_norm", True)) and name in norm_names) + ) + return section, lr, (0.0 if no_decay else wd) + + for name, p in model.named_parameters(): + if not p.requires_grad: + continue + section, lr, group_wd = group_name(name) + key = (section, lr, group_wd) + groups.setdefault(key, []).append(p) + + param_groups = [] + for (section, lr, group_wd), params in groups.items(): + param_groups.append({"params": params, "lr": lr, "weight_decay": group_wd, "group_name": f"{section}_{'nodecay' if group_wd == 0 else 'decay'}"}) + print(f"[OPT] {param_groups[-1]['group_name']}: params={sum(p.numel() for p in params):,} lr={lr:.3e} wd={group_wd:.3e}") + + betas = cfg.get("betas", [0.9, 0.999]) + return torch.optim.AdamW(param_groups, lr=encoder_lr, betas=(float(betas[0]), float(betas[1])), eps=float(cfg.get("eps", 1e-8))) + + +class WarmupDecayScheduler: + """Step scheduler determinístico, salvo integralmente no checkpoint.""" + def __init__(self, optimizer, total_steps: int, cfg: dict): + self.optimizer = optimizer + self.total_steps = max(1, int(total_steps)) + self.cfg = copy.deepcopy(cfg or {}) + self.base_lrs = [float(g["lr"]) for g in optimizer.param_groups] + self.step_num = 0 + self._apply(self.step_num) + + def _factor(self, step: int) -> float: + mode = str(self.cfg.get("mode", "poly")).lower() + warmup = int(round(self.total_steps * float(self.cfg.get("warmup_ratio", 0.05)))) + warmup = max(0, min(warmup, self.total_steps - 1)) + start = float(self.cfg.get("warmup_start_factor", 0.10)) + min_ratio = float(self.cfg.get("min_lr_ratio", 0.02)) + if warmup > 0 and step < warmup: + if warmup == 1: + return 1.0 + t = step / float(warmup - 1) + return start + (1.0 - start) * t + if mode == "constant": + return 1.0 + decay_steps = max(1, self.total_steps - warmup) + t = min(1.0, max(0.0, (step - warmup) / float(max(1, decay_steps - 1)))) + if mode == "cosine": + raw = 0.5 * (1.0 + math.cos(math.pi * t)) + else: + raw = (1.0 - t) ** float(self.cfg.get("poly_power", 1.0)) + return min_ratio + (1.0 - min_ratio) * raw + + def _apply(self, step: int): + f = self._factor(step) + for g, base in zip(self.optimizer.param_groups, self.base_lrs): + g["lr"] = float(base * f) + + def step(self): + self.step_num = min(self.total_steps, self.step_num + 1) + self._apply(self.step_num) + + def state_dict(self): + return {"step_num": self.step_num, "total_steps": self.total_steps, "base_lrs": self.base_lrs, "cfg": self.cfg} + + def load_state_dict(self, state: dict): + self.step_num = int(state.get("step_num", 0)) + saved_total = int(state.get("total_steps", self.total_steps)) + if saved_total != self.total_steps: + print(f"[SCHED][WARN] total_steps checkpoint={saved_total}, atual={self.total_steps}; mantendo atual.") + self._apply(self.step_num) + + def get_last_lr(self): + return [float(g["lr"]) for g in self.optimizer.param_groups] + + +# ============================================================ +# Metrics / losses +# ============================================================ + +@torch.no_grad() +def update_confusion_matrix(cm, preds, labels, num_classes, ignore_index=255): + preds, labels = preds.reshape(-1), labels.reshape(-1) + valid = labels != ignore_index + preds, labels = preds[valid], labels[valid] + valid2 = (labels >= 0) & (labels < num_classes) & (preds >= 0) & (preds < num_classes) + preds, labels = preds[valid2], labels[valid2] + if labels.numel() == 0: + return + idx = labels * num_classes + preds + cm += torch.bincount(idx, minlength=num_classes * num_classes).view(num_classes, num_classes) + + +@torch.no_grad() +def compute_iou_from_cm(cm, eps=1e-6): + cm = cm.float(); tp = torch.diag(cm); fp = cm.sum(0) - tp; fn = cm.sum(1) - tp + iou = (tp / (tp + fp + fn + eps)).cpu().tolist() + return float(np.mean(iou)), iou + + +@torch.no_grad() +def compute_pixel_acc_from_cm(cm, eps=1e-6): + cm = cm.float() + return float(torch.diag(cm).sum() / (cm.sum() + eps)) + + +def binary_stats_from_cm(cm, eps=1e-9): + c = cm.detach().float().cpu() + if c.numel() != 4: + return {} + tn, fp, fn, tp = [float(x) for x in c.reshape(-1)] + precision = tp / (tp + fp + eps) + recall = tp / (tp + fn + eps) + specificity = tn / (tn + fp + eps) + f1 = 2 * precision * recall / (precision + recall + eps) + return {"precision": precision, "recall": recall, "specificity": specificity, "f1": f1, + "tp": tp, "fp": fp, "fn": fn, "tn": tn} + + +def dice_loss(logits, target, num_classes, ignore_index=255, smooth=1.0, reduction_mode="per_image"): + probs = torch.softmax(logits, dim=1) + valid = target != ignore_index + if not bool(valid.any()): + return logits.new_tensor(0.0) + tc = target.clone(); tc[~valid] = 0 + onehot = F.one_hot(tc.long(), num_classes=num_classes).permute(0, 3, 1, 2).float() + vf = valid.unsqueeze(1).float(); probs = probs * vf; onehot = onehot * vf + if str(reduction_mode).lower() == "global": + dims = (0, 2, 3) + inter = (probs * onehot).sum(dims); den = probs.sum(dims) + onehot.sum(dims) + else: + dims = (2, 3) + inter = (probs * onehot).sum(dims); den = probs.sum(dims) + onehot.sum(dims) + dice = (2.0 * inter + smooth) / (den + smooth) + return 1.0 - dice.mean() + + +def _boundary_mask(target: torch.Tensor, ignore_index: int): + valid = target != ignore_index + t = target.clone(); t[~valid] = 0 + b = torch.zeros_like(valid) + b[:, 1:] |= valid[:, 1:] & valid[:, :-1] & (t[:, 1:] != t[:, :-1]) + b[:, :-1] |= valid[:, :-1] & valid[:, 1:] & (t[:, :-1] != t[:, 1:]) + b[:, :, 1:] |= valid[:, :, 1:] & valid[:, :, :-1] & (t[:, :, 1:] != t[:, :, :-1]) + b[:, :, :-1] |= valid[:, :, :-1] & valid[:, :, 1:] & (t[:, :, :-1] != t[:, :, 1:]) + return b + + +def get_target_distill_ramp_factor(config: dict, epoch: Optional[int]) -> float: + cfg = get_training_v2_config(config).get("target_distillation", {}) + if not bool(cfg.get("enabled", False)): + return 0.0 + if epoch is None: + return 1.0 + start, ramp = int(cfg.get("start_epoch", 8)), int(cfg.get("rampup_epochs", 12)) + if epoch < start: + return 0.0 + if ramp <= 0: + return 1.0 + t = max(0.0, min(1.0, (epoch - start + 1) / float(ramp))) + return float(t * t * (3.0 - 2.0 * t)) + + +def build_target_teacher_from_heads(logits_by_head, target_shape, config, erva_id=2): + cfg = get_training_v2_config(config).get("target_distillation", {}) + if not bool(cfg.get("enabled", False)) or str(cfg.get("mode", "cross_head")) != "cross_head": + return None + if any(k not in logits_by_head for k in ("semantic", "vegetation", "cana")): + return None + def rs(x): + return F.interpolate(x, size=target_shape, mode="bilinear", align_corners=False) if x.shape[-2:] != target_shape else x + sem, veg, cana = map(rs, (logits_by_head["semantic"], logits_by_head["vegetation"], logits_by_head["cana"])) + ps, pv, pc = torch.softmax(sem, 1), torch.softmax(veg, 1), torch.softmax(cana, 1) + not_cana = (1.0 - pc[:, 1]).clamp(0, 1) + teacher = ( + float(cfg.get("w_sem_erva", 0.45)) * ps[:, int(erva_id)] + + float(cfg.get("w_veg_not_cana", 0.35)) * pv[:, 1] * not_cana + + float(cfg.get("w_veg_suppressed", 0.20)) * pv[:, 1] * not_cana.pow(float(cfg.get("cana_suppression_power", 1.5))) + ).clamp(0, 1) + if bool(cfg.get("detach_teacher", True)): + teacher = teacher.detach() + return teacher + + +def compute_losses_for_batch(logits_by_head, masks_by_head, heads_config, criterions, + dice_weight=0.30, config=None, epoch=None): + cfg = config or {} + tv2 = get_training_v2_config(cfg) + lcfg = tv2.get("loss", {}) + total, parts = None, {} + resized_logits = {} + + for head_name, logits in logits_by_head.items(): + target = masks_by_head[head_name] + hcfg = heads_config[head_name] + ncls, ignore = int(hcfg["num_classes"]), int(hcfg.get("ignore_index", 255)) + hw = float(hcfg.get("loss_weight_norm", 1.0)) + if logits.shape[-2:] != target.shape[-2:]: + logits = F.interpolate(logits, size=target.shape[-2:], mode="bilinear", align_corners=False) + resized_logits[head_name] = logits + ce = criterions[head_name](logits, target) + dl = dice_loss(logits, target, ncls, ignore, float(lcfg.get("dice_smooth", 1.0)), str(lcfg.get("dice_reduction", "per_image"))) + head_loss = (1.0 - dice_weight) * ce + dice_weight * dl + + bw = float(lcfg.get("boundary_weight", 0.0)) + if bw > 0: + boundary = _boundary_mask(target, ignore) + if bool(boundary.any()): + cem = F.cross_entropy(logits, target, weight=criterions[head_name].weight, ignore_index=ignore, reduction="none") + bl = cem[boundary].mean() + head_loss = head_loss + bw * bl + parts[f"{head_name}_boundary"] = bl.detach() + + ohem_ratio = float(lcfg.get("ohem_ratio", 0.0)) + if 0 < ohem_ratio < 1: + cem = F.cross_entropy(logits, target, weight=criterions[head_name].weight, ignore_index=ignore, reduction="none") + valid = target != ignore + vals = cem[valid] + if vals.numel() > 0: + k = max(1, int(vals.numel() * ohem_ratio)) + ohem = torch.topk(vals, k=k, largest=True).values.mean() + head_loss = 0.8 * head_loss + 0.2 * ohem + parts[f"{head_name}_ohem"] = ohem.detach() + + if head_name == "target": + ramp = get_target_distill_ramp_factor(cfg, epoch) + dcfg = tv2.get("target_distillation", {}) + if ramp > 0: + teacher = build_target_teacher_from_heads(logits_by_head, target.shape[-2:], cfg, + erva_id=int(cfg.get("heads", {}).get("semantic", {}).get("classes", {}).get("erva", 2))) + if teacher is not None: + binary_logits = logits[:, 1] - logits[:, 0] + valid = target != ignore + conf = torch.maximum(teacher, 1.0 - teacher) + valid &= conf >= float(dcfg.get("teacher_confidence_min", 0.60)) + if bool(valid.any()): + distill = F.binary_cross_entropy_with_logits(binary_logits[valid], teacher[valid]) + hard_w = float(dcfg.get("hard_weight", 0.75)) + dist_w = float(dcfg.get("distill_weight", 0.25)) * ramp + denom = max(1e-6, hard_w + dist_w) + head_loss = (hard_w / denom) * head_loss + (dist_w / denom) * distill + parts["target_distill"] = distill.detach() + parts["target_distill_ramp"] = logits.new_tensor(ramp).detach() + + weighted = hw * head_loss + total = weighted if total is None else total + weighted + parts[f"{head_name}_loss"] = head_loss.detach() + parts[f"{head_name}_ce"] = ce.detach() + parts[f"{head_name}_dice"] = dl.detach() + + # Domain safety loss: target em cana custa mais que target em chão. + scfg = lcfg.get("safety", {}) or {} + if bool(scfg.get("enabled", True)) and "target" in resized_logits: + logits = resized_logits["target"] + binary = logits[:, 1] - logits[:, 0] + cana_gt = masks_by_head.get("cana") + sem_gt = masks_by_head.get("semantic") + safety_terms = [] + if cana_gt is not None: + valid_cana = cana_gt == 1 + if bool(valid_cana.any()): + lc = F.binary_cross_entropy_with_logits(binary[valid_cana], torch.zeros_like(binary[valid_cana])) + safety_terms.append(float(scfg.get("cana_weight", 1.0)) * lc) + parts["safety_cana"] = lc.detach() + if sem_gt is not None: + chao_id = int(heads_config.get("semantic", {}).get("classes", {}).get("chao", 0)) + valid_ground = sem_gt == chao_id + if bool(valid_ground.any()): + lg = F.binary_cross_entropy_with_logits(binary[valid_ground], torch.zeros_like(binary[valid_ground])) + safety_terms.append(float(scfg.get("ground_weight", 0.20)) * lg) + parts["safety_ground"] = lg.detach() + if safety_terms: + safety = sum(safety_terms) / max(1.0, sum([float(scfg.get("cana_weight", 1.0)), float(scfg.get("ground_weight", 0.20))])) + total = total + float(scfg.get("weight", 0.08)) * safety + parts["safety_loss"] = safety.detach() + + return total, parts + + +def resize_logits_to_target_if_needed(logits, target): + return F.interpolate(logits, size=target.shape[-2:], mode="bilinear", align_corners=False) if logits.shape[-2:] != target.shape[-2:] else logits + + +class AgriculturalMetricsMeter: + def __init__( + self, + thresholds: List[float], + ece_bins: int, + device: torch.device, + rich: bool = True, + scenario_metrics: bool = True, + group_metrics: bool = True, + operational_cfg: Optional[dict] = None, + ): + thresholds = [float(t) for t in thresholds] + if 0.5 not in thresholds: + thresholds.append(0.5) + self.thresholds = sorted(set(thresholds)) + self.ece_bins = max(2, int(ece_bins)) + self.device = device + self.rich = bool(rich) + self.scenario_metrics = bool(scenario_metrics) and self.rich + self.group_metrics = bool(group_metrics) and self.rich + self.operational_cfg = operational_cfg or {} + + self.cana_total = self.cana_sprayed = 0 + self.ground_total = self.ground_sprayed = 0 + self.target_total = self.target_missed = 0 + self.valid_total = self.predicted_target_total = 0 + + self.macro_target_iou_sum = 0.0 + self.macro_target_iou_count = 0 + + # Estatísticas por threshold. Cana/chão usam os mesmos GTs globais. + self.threshold_counts = { + t: { + "tp": 0, "fp": 0, "fn": 0, "tn": 0, + "cana_sprayed": 0, + "ground_sprayed": 0, + "predicted_target": 0, + } + for t in self.thresholds + } + + # Calibração. Acumuladores em float64; entradas são convertidas para + # float32 antes de somar, evitando overflow FP16 com --amp_val. + self.ece_count = np.zeros(self.ece_bins, np.float64) + self.ece_conf = np.zeros(self.ece_bins, np.float64) + self.ece_acc = np.zeros(self.ece_bins, np.float64) + self.brier_sum = 0.0 + self.nll_sum = 0.0 + self.calibration_count = 0 + + self.scenario = { + k: torch.zeros((2, 2), dtype=torch.int64, device=device) + for k in ["none", "tiny", "small", "medium", "large"] + } + self.group_stats: Dict[str, dict] = {} + + # Métricas por frame, úteis porque uma única mancha errada de target + # sobre cana já pode ser operacionalmente relevante. + self.frames_total = 0 + self.frames_with_target = 0 + self.frames_target_detected = 0 + self.frames_target_completely_missed = 0 + self.frames_with_cana = 0 + self.frames_cana_clean = 0 + self.frames_with_ground = 0 + self.frames_ground_clean = 0 + + # Consistência entre target direta e target composta + # vegetation==1 AND cana==0. + self.composed_valid = 0 + self.composed_disagree = 0 + self.composed_intersection = 0 + self.composed_union = 0 + + @staticmethod + def _empty_group_stats() -> dict: + return { + "frames": 0, + "tp": 0, "fp": 0, "fn": 0, "tn": 0, + "cana_total": 0, "cana_sprayed": 0, + "ground_total": 0, "ground_sprayed": 0, + "target_total": 0, "target_missed": 0, + } + + @staticmethod + def _binary_metrics(tp: int, fp: int, fn: int, tn: int = 0) -> dict: + eps = 1e-9 + precision = tp / (tp + fp + eps) + recall = tp / (tp + fn + eps) + f1 = 2 * precision * recall / (precision + recall + eps) + iou = tp / (tp + fp + fn + eps) + specificity = tn / (tn + fp + eps) + return { + "precision": precision, + "recall": recall, + "f1": f1, + "iou": iou, + "specificity": specificity, + "tp": int(tp), "fp": int(fp), "fn": int(fn), "tn": int(tn), + } + + @torch.no_grad() + def update(self, target_logits, masks, heads_config, meta: Optional[dict] = None, composed_pred: Optional[torch.Tensor] = None): + gt = masks.get("target") + if gt is None: + return + + ignore = int(heads_config["target"].get("ignore_index", 255)) + logits = resize_logits_to_target_if_needed(target_logits, gt).float() + prob = torch.softmax(logits, dim=1)[:, 1].float() + pred = (prob >= 0.5).long() + valid = gt != ignore + + cana = masks.get("cana") + sem = masks.get("semantic") + chao_id = int(heads_config.get("semantic", {}).get("classes", {}).get("chao", 0)) + + cana_mask = ((cana == 1) & valid) if cana is not None else torch.zeros_like(valid) + ground_mask = ((sem == chao_id) & valid) if sem is not None else torch.zeros_like(valid) + target_mask = (gt == 1) & valid + + self.cana_total += int(cana_mask.sum()) + self.cana_sprayed += int(((pred == 1) & cana_mask).sum()) + self.ground_total += int(ground_mask.sum()) + self.ground_sprayed += int(((pred == 1) & ground_mask).sum()) + self.target_total += int(target_mask.sum()) + self.target_missed += int(((pred == 0) & target_mask).sum()) + self.valid_total += int(valid.sum()) + self.predicted_target_total += int(((pred == 1) & valid).sum()) + + if composed_pred is not None: + cp = composed_pred + if cp.shape[-2:] != gt.shape[-2:]: + cp = F.interpolate(cp.unsqueeze(1).float(), size=gt.shape[-2:], mode="nearest").squeeze(1).long() + dv = valid + direct = pred[dv] + comp = cp[dv] + self.composed_valid += int(direct.numel()) + self.composed_disagree += int((direct != comp).sum()) + self.composed_intersection += int(((direct == 1) & (comp == 1)).sum()) + self.composed_union += int(((direct == 1) | (comp == 1)).sum()) + + groups = [] + if isinstance(meta, dict): + groups = list(meta.get("group", []) or []) + + # Métricas frame-a-frame, cenários e grupos. + for b in range(gt.shape[0]): + vb = valid[b] + if not bool(vb.any()): + continue + gb = gt[b][vb] + pb = pred[b][vb] + tp = int(((gb == 1) & (pb == 1)).sum()) + fp = int(((gb == 0) & (pb == 1)).sum()) + fn = int(((gb == 1) & (pb == 0)).sum()) + tn = int(((gb == 0) & (pb == 0)).sum()) + + self.frames_total += 1 + has_target = bool((gb == 1).any()) + if has_target: + self.frames_with_target += 1 + if tp > 0: + self.frames_target_detected += 1 + else: + self.frames_target_completely_missed += 1 + + if cana is not None: + cb = cana_mask[b] + if bool(cb.any()): + self.frames_with_cana += 1 + if not bool(((pred[b] == 1) & cb).any()): + self.frames_cana_clean += 1 + + if sem is not None: + grb = ground_mask[b] + if bool(grb.any()): + self.frames_with_ground += 1 + if not bool(((pred[b] == 1) & grb).any()): + self.frames_ground_clean += 1 + + denom = tp + fp + fn + if denom > 0: + self.macro_target_iou_sum += tp / denom + self.macro_target_iou_count += 1 + + if self.scenario_metrics: + pct = float((gb == 1).float().mean()) + if pct == 0: + bucket = "none" + elif pct <= 0.005: + bucket = "tiny" + elif pct <= 0.02: + bucket = "small" + elif pct <= 0.10: + bucket = "medium" + else: + bucket = "large" + update_confusion_matrix(self.scenario[bucket], pb, gb, 2, ignore_index=255) + + if self.group_metrics: + group = str(groups[b] if b < len(groups) else "unknown") + gs = self.group_stats.setdefault(group, self._empty_group_stats()) + gs["frames"] += 1 + gs["tp"] += tp; gs["fp"] += fp; gs["fn"] += fn; gs["tn"] += tn + if cana is not None: + cb = cana_mask[b] + gs["cana_total"] += int(cb.sum()) + gs["cana_sprayed"] += int(((pred[b] == 1) & cb).sum()) + if sem is not None: + grb = ground_mask[b] + gs["ground_total"] += int(grb.sum()) + gs["ground_sprayed"] += int(((pred[b] == 1) & grb).sum()) + tb = target_mask[b] + gs["target_total"] += int(tb.sum()) + gs["target_missed"] += int(((pred[b] == 0) & tb).sum()) + + if not self.rich: + return + + pv = prob[valid].float() + gv = gt[valid].long() + cv = cana_mask[valid] + grv = ground_mask[valid] + + for t in self.thresholds: + pp = pv >= float(t) + st = self.threshold_counts[t] + st["tp"] += int(((pp == 1) & (gv == 1)).sum()) + st["fp"] += int(((pp == 1) & (gv == 0)).sum()) + st["fn"] += int(((pp == 0) & (gv == 1)).sum()) + st["tn"] += int(((pp == 0) & (gv == 0)).sum()) + st["predicted_target"] += int(pp.sum()) + if cana is not None: + st["cana_sprayed"] += int((pp & cv).sum()) + if sem is not None: + st["ground_sprayed"] += int((pp & grv).sum()) + + # ECE em FP32, evitando overflow de soma em FP16 durante amp_val. + conf = torch.maximum(pv, 1.0 - pv).float() + correct = ((pv >= 0.5).long() == gv).float() + bins = torch.clamp((conf * self.ece_bins).long(), 0, self.ece_bins - 1) + for i in range(self.ece_bins): + m = bins == i + if bool(m.any()): + n = int(m.sum()) + self.ece_count[i] += n + self.ece_conf[i] += float(conf[m].float().sum().cpu()) + self.ece_acc[i] += float(correct[m].float().sum().cpu()) + + gvf = gv.float() + self.brier_sum += float(torch.sum((pv - gvf) ** 2).cpu()) + pclip = torch.clamp(pv, 1e-6, 1.0 - 1e-6) + self.nll_sum += float(torch.sum(-(gvf * torch.log(pclip) + (1.0 - gvf) * torch.log(1.0 - pclip))).cpu()) + self.calibration_count += int(pv.numel()) + + def _operational_score(self, row: dict) -> float: + cfg = self.operational_cfg or {} + w = cfg.get("score_weights", {}) or {} + vals = { + "target_iou": float(row.get("iou", 0.0)), + "target_f1": float(row.get("f1", 0.0)), + "cana_safety": float(row.get("cana_safety", 0.0)), + "ground_safety": 1.0 - float(row.get("ground_spray_rate", 1.0)), + "weed_recall": 1.0 - float(row.get("weed_miss_rate", 1.0)), + } + defaults = {"target_iou":0.35, "target_f1":0.20, "cana_safety":0.25, "ground_safety":0.10, "weed_recall":0.10} + total_w = 0.0 + score = 0.0 + for k, dv in defaults.items(): + wk = float(w.get(k, dv)) + total_w += wk + score += wk * vals[k] + return score / max(total_w, 1e-9) + + def compute(self): + eps = 1e-9 + out = { + "cana_spray_rate": self.cana_sprayed / max(1, self.cana_total), + "cana_safety": 1.0 - self.cana_sprayed / max(1, self.cana_total), + "ground_spray_rate": self.ground_sprayed / max(1, self.ground_total), + "ground_safety": 1.0 - self.ground_sprayed / max(1, self.ground_total), + "weed_miss_rate": self.target_missed / max(1, self.target_total), + "weed_recall": 1.0 - self.target_missed / max(1, self.target_total), + "predicted_target_rate": self.predicted_target_total / max(1, self.valid_total), + "macro_target_iou": self.macro_target_iou_sum / max(1, self.macro_target_iou_count), + "macro_target_iou_positive_union": self.macro_target_iou_sum / max(1, self.macro_target_iou_count), + "frame_target_detection_rate": self.frames_target_detected / max(1, self.frames_with_target), + "frame_target_complete_miss_rate": self.frames_target_completely_missed / max(1, self.frames_with_target), + "frame_cana_clean_rate": self.frames_cana_clean / max(1, self.frames_with_cana), + "frame_ground_clean_rate": self.frames_ground_clean / max(1, self.frames_with_ground), + "target_composed_disagreement_rate": self.composed_disagree / max(1, self.composed_valid), + "target_composed_agreement": 1.0 - self.composed_disagree / max(1, self.composed_valid), + "target_composed_iou": self.composed_intersection / max(1, self.composed_union), + "counts": { + "cana_total": self.cana_total, "cana_sprayed": self.cana_sprayed, + "ground_total": self.ground_total, "ground_sprayed": self.ground_sprayed, + "target_total": self.target_total, "target_missed": self.target_missed, + "valid_total": self.valid_total, "predicted_target_total": self.predicted_target_total, + "frames_total": self.frames_total, "frames_with_target": self.frames_with_target, + "frames_with_cana": self.frames_with_cana, "frames_with_ground": self.frames_with_ground, + }, + } + + sweep = {} + best_f1 = {"threshold": 0.5, "f1": -1.0, "iou": -1.0} + + # Contrato: + # best_operational_threshold só existe quando há um threshold SAFE. + best_operational = { + "threshold": None, + "operational_score": -1.0, + "constraints_met": False, + } + feasible_rows = [] + + if self.rich: + max_cana = float(self.operational_cfg.get("max_cana_spray_rate", 0.02)) + max_ground = float(self.operational_cfg.get("max_ground_spray_rate", 0.03)) + max_miss = float(self.operational_cfg.get("max_weed_miss_rate", 0.20)) + + for t, st in self.threshold_counts.items(): + bm = self._binary_metrics(st["tp"], st["fp"], st["fn"], st["tn"]) + cana_spray = st["cana_sprayed"] / max(1, self.cana_total) + ground_spray = st["ground_sprayed"] / max(1, self.ground_total) + weed_miss = st["fn"] / max(1, self.target_total) + row = { + **bm, + "threshold": float(t), + "cana_spray_rate": cana_spray, + "cana_safety": 1.0 - cana_spray, + "ground_spray_rate": ground_spray, + "ground_safety": 1.0 - ground_spray, + "weed_miss_rate": weed_miss, + "weed_recall": 1.0 - weed_miss, + "predicted_target_rate": st["predicted_target"] / max(1, self.valid_total), + } + has_required_domains = ( + self.cana_total > 0 + and self.ground_total > 0 + and self.target_total > 0 + ) + row["constraints_met"] = bool( + has_required_domains and + cana_spray <= max_cana and ground_spray <= max_ground and weed_miss <= max_miss + ) + row["operational_score"] = self._operational_score(row) + sweep[str(t)] = row + if row["constraints_met"]: + feasible_rows.append(row) + if row["f1"] > best_f1["f1"]: + best_f1 = {"threshold": float(t), "f1": row["f1"], "iou": row["iou"]} + + if feasible_rows: + chosen = max( + feasible_rows, + key=lambda r: r["operational_score"], + ) + best_operational = dict(chosen) + best_operational["constraints_met"] = True + + out["threshold_sweep"] = sweep + out["best_threshold"] = best_f1 + out["best_operational_threshold"] = best_operational + + total = self.ece_count.sum() + ece = 0.0 + if self.rich and total > 0: + for n, cs, acs in zip(self.ece_count, self.ece_conf, self.ece_acc): + if n > 0: + ece += (n / total) * abs((acs / n) - (cs / n)) + out["target_ece"] = float(ece) if self.rich else None + out["target_brier"] = (self.brier_sum / max(1, self.calibration_count)) if self.rich else None + out["target_nll"] = (self.nll_sum / max(1, self.calibration_count)) if self.rich else None + + out["scenario_target"] = {} + if self.scenario_metrics: + for name, cm in self.scenario.items(): + miou, ious = compute_iou_from_cm(cm) + bs = binary_stats_from_cm(cm) + out["scenario_target"][name] = { + "miou": miou, + "iou_target": ious[1], + **bs, + "cm": cm.detach().cpu().tolist(), + } + + out["group_target"] = {} + if self.group_metrics: + for group, gs in sorted(self.group_stats.items()): + bm = self._binary_metrics(gs["tp"], gs["fp"], gs["fn"], gs["tn"]) + out["group_target"][group] = { + **bm, + "frames": int(gs["frames"]), + "cana_spray_rate": gs["cana_sprayed"] / max(1, gs["cana_total"]), + "cana_safety": 1.0 - gs["cana_sprayed"] / max(1, gs["cana_total"]), + "ground_spray_rate": gs["ground_sprayed"] / max(1, gs["ground_total"]), + "weed_miss_rate": gs["target_missed"] / max(1, gs["target_total"]), + "counts": dict(gs), + } + return out + + +@torch.no_grad() +def update_operational_target_cm(cm_target, pred_veg, pred_cana, gt_veg, gt_cana, ignore_index=255): + valid = (gt_veg != ignore_index) & (gt_cana != ignore_index) + gt = ((gt_veg == 1) & (gt_cana == 0)).long()[valid] + pred = ((pred_veg == 1) & (pred_cana == 0)).long()[valid] + if gt.numel(): + cm_target += torch.bincount(gt.reshape(-1) * 2 + pred.reshape(-1), minlength=4).view(2, 2) + + +# ============================================================ +# Train / Val +# ============================================================ + +def run_one_epoch(model, loader, optimizer, device, heads_config, criterions, amp, scaler, train, + grad_accum=1, normalizer=None, dice_weight=0.30, config=None, epoch=None, + scheduler=None, grad_clip_norm=1.0, global_step=0): + model.train(train) + total_loss, n_batches, loss_sums = 0.0, 0, {} + cms = {name: torch.zeros((int(h["num_classes"]), int(h["num_classes"])), dtype=torch.int64, device=device) for name, h in heads_config.items()} + cm_op = torch.zeros((2, 2), dtype=torch.int64, device=device) + tv2 = get_training_v2_config(config or {}) + mm = tv2.get("metrics", {}) + rich_metrics = (not train) or bool(mm.get("rich_train_metrics", False)) + agri = AgriculturalMetricsMeter( + mm.get("target_thresholds", [0.3,0.4,0.5,0.6,0.7]), + mm.get("ece_bins", 12), + device, + rich=rich_metrics, + scenario_metrics=bool(mm.get("scenario_metrics", True)), + group_metrics=bool(mm.get("group_metrics", True)), + operational_cfg=mm.get("operational_threshold", {}) or {}, + ) + grad_norm_sum = grad_norm_max = 0.0; grad_norm_count = 0; optimizer_steps = 0; skipped_steps = 0 + t0 = time.time() + + if train and optimizer is not None: + optimizer.zero_grad(set_to_none=True) + + with torch.set_grad_enabled(train): + total_loader_steps = len(loader) + for step, (imgs, masks, _meta) in enumerate(loader): + imgs = imgs.to(device, non_blocking=True) + masks = {k: v.to(device, non_blocking=True) for k, v in masks.items()} + imgs = normalizer(imgs) if normalizer is not None else normalize_per_batch(imgs) + + window_start = (step // max(1, grad_accum)) * max(1, grad_accum) + window_end = min(window_start + max(1, grad_accum), total_loader_steps) + accum_divisor = max(1, window_end - window_start) + + with autocast(device_type="cuda", enabled=amp and device.type == "cuda"): + logits_by_head = model(pixel_values=imgs) + loss_raw, loss_parts = compute_losses_for_batch( + logits_by_head, masks, heads_config, criterions, + dice_weight=dice_weight, config=config, epoch=epoch, + ) + loss_backward = loss_raw / accum_divisor if train else loss_raw + + if train and optimizer is not None: + if amp and scaler is not None and device.type == "cuda": + scaler.scale(loss_backward).backward() + else: + loss_backward.backward() + + boundary = ((step + 1) % max(1, grad_accum) == 0) or ((step + 1) == total_loader_steps) + if boundary: + if amp and scaler is not None and device.type == "cuda": + scaler.unscale_(optimizer) + gn = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=float(grad_clip_norm)) if grad_clip_norm and grad_clip_norm > 0 else torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=float("inf")) + gn_val = float(gn.detach().cpu()) if torch.is_tensor(gn) else float(gn) + grad_norm_sum += gn_val; grad_norm_max = max(grad_norm_max, gn_val); grad_norm_count += 1 + + did_step = True + if amp and scaler is not None and device.type == "cuda": + before = float(scaler.get_scale()) + scaler.step(optimizer); scaler.update() + after = float(scaler.get_scale()) + did_step = after >= before + else: + optimizer.step() + + optimizer.zero_grad(set_to_none=True) + if did_step: + optimizer_steps += 1; global_step += 1 + if scheduler is not None: + scheduler.step() + else: + skipped_steps += 1 + + total_loss += float(loss_raw.detach().item()); n_batches += 1 + for k, v in loss_parts.items(): + loss_sums[k] = loss_sums.get(k, 0.0) + float(v.item()) + + with torch.no_grad(): + preds = {} + for head_name, logits in logits_by_head.items(): + if head_name not in masks: + continue + lm = resize_logits_to_target_if_needed(logits, masks[head_name]) + pr = torch.argmax(lm, dim=1); preds[head_name] = pr + update_confusion_matrix(cms[head_name], pr, masks[head_name], int(heads_config[head_name]["num_classes"]), int(heads_config[head_name].get("ignore_index", 255))) + composed_pred = None + if "vegetation" in preds and "cana" in preds: + update_operational_target_cm(cm_op, preds["vegetation"], preds["cana"], masks["vegetation"], masks["cana"], int(heads_config["vegetation"].get("ignore_index", 255))) + composed_pred = ((preds["vegetation"] == 1) & (preds["cana"] == 0)).long() + if "target" in logits_by_head: + agri.update(logits_by_head["target"], masks, heads_config, meta=_meta, composed_pred=composed_pred) + + metrics = { + "loss": total_loss / max(1, n_batches), "time_s": time.time() - t0, + "heads": {}, "loss_parts": {k: v/max(1,n_batches) for k,v in loss_sums.items()}, + "optimization": {"optimizer_steps": optimizer_steps, "skipped_steps": skipped_steps, "global_step": global_step, + "grad_norm_mean": grad_norm_sum/max(1,grad_norm_count), "grad_norm_max": grad_norm_max}, + } + for name, cm in cms.items(): + miou, ious = compute_iou_from_cm(cm); acc = compute_pixel_acc_from_cm(cm) + hm = {"miou": miou, "iou_per_class": ious, "acc": acc, "cm": cm.detach().cpu().tolist()} + if cm.shape == (2,2): + hm.update(binary_stats_from_cm(cm)) + metrics["heads"][name] = hm + op_miou, op_iou = compute_iou_from_cm(cm_op) + metrics["operational_target"] = {"miou": op_miou, "iou_background": op_iou[0], "iou_target": op_iou[1], "acc": compute_pixel_acc_from_cm(cm_op), "cm": cm_op.detach().cpu().tolist(), **binary_stats_from_cm(cm_op)} + metrics["agricultural"] = agri.compute() + + # Confusões semanticamente importantes para campo: cana confundida com + # erva é muito mais crítica que uma simples queda de mIoU. + if "semantic" in cms: + scm = cms["semantic"].detach().float().cpu() + classes = heads_config.get("semantic", {}).get("classes", {}) or {} + cid = int(classes.get("cana", 1)); eid = int(classes.get("erva", 2)); gid = int(classes.get("chao", 0)) + def _row_rate(src, dst): + if src < 0 or dst < 0 or src >= scm.shape[0] or dst >= scm.shape[1]: + return 0.0 + den = float(scm[src, :].sum()) + return float(scm[src, dst]) / max(den, 1e-9) + metrics["agricultural"].update({ + "semantic_cana_as_erva_rate": _row_rate(cid, eid), + "semantic_erva_as_cana_rate": _row_rate(eid, cid), + "semantic_cana_as_ground_rate": _row_rate(cid, gid), + "semantic_erva_as_ground_rate": _row_rate(eid, gid), + }) + return metrics + + +# ============================================================ +# Selection / checkpoints / logs +# ============================================================ + +def compute_selection_score(va: dict, config: Optional[dict] = None) -> dict: + heads, ag = va.get("heads", {}), va.get("agricultural", {}) + target_vals = heads.get("target", {}).get("iou_per_class", []) or [] + cana_vals = heads.get("cana", {}).get("iou_per_class", []) or [] + target_iou = safe_float(target_vals[1] if len(target_vals) > 1 else va.get("operational_target", {}).get("iou_target"), 0.0) + cana_iou = safe_float(cana_vals[1] if len(cana_vals) > 1 else 0.0, 0.0) + target_f1 = safe_float(heads.get("target", {}).get("f1"), 0.0) + veg_miou = safe_float(heads.get("vegetation", {}).get("miou"), 0.0) + sem_miou = safe_float(heads.get("semantic", {}).get("miou"), 0.0) + cana_safety = safe_float(ag.get("cana_safety"), 0.0) + + weights = get_training_v2_config(config or {}).get("selection_score", {}) + score = ( + float(weights.get("target_iou", 0.40)) * target_iou + + float(weights.get("cana_iou", 0.20)) * cana_iou + + float(weights.get("target_f1", 0.10)) * target_f1 + + float(weights.get("vegetation_miou", 0.10)) * veg_miou + + float(weights.get("semantic_miou", 0.05)) * sem_miou + + float(weights.get("cana_safety", 0.15)) * cana_safety + ) + + # Régua histórica exata do trainer anterior. Isso NÃO torna datasets, + # resolução ou preprocessamento diferentes diretamente comparáveis, mas + # permite olhar o mesmo score quando fizermos um A/B controlado. + legacy_score = ( + 0.50 * target_iou + + 0.25 * cana_iou + + 0.15 * veg_miou + + 0.10 * sem_miou + ) + + best_op = ag.get("best_operational_threshold", {}) or {} + operational_score = safe_float(best_op.get("operational_score"), -1.0) + + return { + "score": float(score), + "legacy_score": float(legacy_score), + "operational_score": float(operational_score), + "operational_threshold": best_op.get("threshold"), + "operational_constraints_met": bool(best_op.get("constraints_met", False)), + "target_iou": target_iou, + "target_head_iou": target_iou, + "operational_target_iou": safe_float(va.get("operational_target", {}).get("iou_target"), 0.0), + "cana_head_iou": cana_iou, + "target_f1": target_f1, + "vegetation_miou": veg_miou, + "semantic_miou": sem_miou, + "cana_safety": cana_safety, + "cana_spray_rate": safe_float(ag.get("cana_spray_rate"), 1.0), + } + + +def atomic_torch_save(obj, path: Path): + ensure_dir(path.parent) + tmp = path.with_suffix(path.suffix + ".tmp") + torch.save(obj, tmp) + os.replace(tmp, path) + + +def save_checkpoint(path, model, optimizer=None, scaler=None, epoch=0, best=None, extra=None, + scheduler=None, epochs_without_improve=0, global_step=0): + cfg = getattr(model, "config", None) + ckpt = { + "epoch": int(epoch), "model": model.state_dict(), "best": best or {}, + "trainer_version": TRAINER_VERSION, + "model_contract": { + "num_channels": int(getattr(cfg, "num_channels", 0) or 0), + "input_channel_names": list(getattr(cfg, "input_channel_names", []) or []), + "decoder_mode": str(getattr(cfg, "agri_decoder_mode", "separate")), + "spectral_input_init": str(getattr(cfg, "agri_spectral_input_init", "mean_rgb")), + }, + "epochs_without_improve": int(epochs_without_improve), + "global_step": int(global_step), + "rng_state": capture_rng_state(), + } + if optimizer is not None: ckpt["optimizer"] = optimizer.state_dict() + if scheduler is not None: ckpt["scheduler"] = scheduler.state_dict() + if scaler is not None: ckpt["scaler"] = scaler.state_dict() + if extra: ckpt["extra"] = extra + atomic_torch_save(ckpt, Path(path)) + + +def load_checkpoint(path, model, optimizer=None, scaler=None, map_location="cpu", scheduler=None, restore_rng=False): + ckpt = torch.load(path, map_location=map_location, weights_only=False) + saved = ckpt.get("model_contract", {}) or {}; cfg = getattr(model, "config", None) + cur_n = int(getattr(cfg, "num_channels", 0) or 0); cur_names = list(getattr(cfg, "input_channel_names", []) or []) + if int(saved.get("num_channels", 0) or 0) not in (0, cur_n): + raise RuntimeError(f"Checkpoint canais incompatíveis: saved={saved.get('num_channels')} atual={cur_n}") + if saved.get("input_channel_names") and cur_names and list(saved["input_channel_names"]) != cur_names: + raise RuntimeError(f"Checkpoint canais nominais incompatíveis: saved={saved['input_channel_names']} atual={cur_names}") + saved_decoder = str(saved.get("decoder_mode", "")) + cur_decoder = str(getattr(cfg, "agri_decoder_mode", "")) + if saved_decoder and cur_decoder and saved_decoder != cur_decoder: + raise RuntimeError(f"Checkpoint decoder_mode={saved_decoder}, modelo atual={cur_decoder}") + model.load_state_dict(ckpt["model"], strict=True) + if optimizer is not None and "optimizer" in ckpt: + optimizer.load_state_dict(ckpt["optimizer"]) + if scheduler is not None and "scheduler" in ckpt: + scheduler.load_state_dict(ckpt["scheduler"]) + if scaler is not None and "scaler" in ckpt: + scaler.load_state_dict(ckpt["scaler"]) + if restore_rng: + restore_rng_state(ckpt.get("rng_state")) + return ckpt + + +def append_train_log(path: Path, row: dict): + ensure_dir(path.parent); exists = path.exists() + with path.open("a", newline="", encoding="utf-8") as f: + w = csv.DictWriter(f, fieldnames=list(row.keys())) + if not exists: w.writeheader() + w.writerow(row) + + +def append_jsonl(path: Path, obj: dict): + ensure_dir(path.parent) + with path.open("a", encoding="utf-8") as f: + f.write(json.dumps(obj, ensure_ascii=False, default=str) + "\n") + + +def flatten_epoch_log(epoch, lr, tr, va, best, score_now: Optional[dict] = None): + score_now = score_now or {} + row = { + "epoch": epoch, "lr": lr, + "train_loss": tr["loss"], "val_loss": va["loss"], + "score": score_now.get("score"), + "legacy_score": score_now.get("legacy_score"), + "operational_score": score_now.get("operational_score"), + "operational_threshold": score_now.get("operational_threshold"), + "operational_constraints_met": score_now.get("operational_constraints_met"), + "best_score": best.get("score", -1), + "best_legacy_score": best.get("legacy_score", -1), + "best_operational_score": best.get("operational_score", -1), + "best_target_iou": best.get("target_iou", -1), + "best_cana_head_iou": best.get("cana_head_iou", -1), + "best_semantic_miou": best.get("semantic_miou", -1), + "best_cana_safety": best.get("cana_safety", -1), + } + for prefix, obj in (("train", tr), ("val", va)): + for hn, hm in obj.get("heads", {}).items(): + row[f"{prefix}_{hn}_miou"] = hm.get("miou") + row[f"{prefix}_{hn}_acc"] = hm.get("acc") + for k in ("precision", "recall", "f1", "specificity"): + if k in hm: + row[f"{prefix}_{hn}_{k}"] = hm[k] + for i, v in enumerate(hm.get("iou_per_class", []) or []): + row[f"{prefix}_{hn}_iou_{i}"] = v + + op = obj.get("operational_target", {}) or {} + row[f"{prefix}_op_target_iou"] = op.get("iou_target") + row[f"{prefix}_op_target_f1"] = op.get("f1") + + ag = obj.get("agricultural", {}) or {} + for k in ( + "cana_spray_rate", "cana_safety", "ground_spray_rate", "ground_safety", + "weed_miss_rate", "weed_recall", "macro_target_iou", + "frame_target_detection_rate", "frame_target_complete_miss_rate", + "frame_cana_clean_rate", "frame_ground_clean_rate", + "target_composed_disagreement_rate", "target_composed_agreement", "target_composed_iou", + "semantic_cana_as_erva_rate", "semantic_erva_as_cana_rate", + "semantic_cana_as_ground_rate", "semantic_erva_as_ground_rate", + "target_ece", "target_brier", "target_nll", + ): + row[f"{prefix}_{k}"] = ag.get(k) + + bt = ag.get("best_threshold", {}) or {} + row[f"{prefix}_best_threshold"] = bt.get("threshold") + row[f"{prefix}_best_threshold_f1"] = bt.get("f1") + row[f"{prefix}_best_threshold_iou"] = bt.get("iou") + + bop = ag.get("best_operational_threshold", {}) or {} + row[f"{prefix}_best_op_threshold"] = bop.get("threshold") + row[f"{prefix}_best_op_score"] = bop.get("operational_score") + row[f"{prefix}_best_op_constraints_met"] = bop.get("constraints_met") + row[f"{prefix}_best_op_spray_cana"] = bop.get("cana_spray_rate") + row[f"{prefix}_best_op_spray_ground"] = bop.get("ground_spray_rate") + row[f"{prefix}_best_op_weed_miss"] = bop.get("weed_miss_rate") + + # Sweep completo em CSV para análise posterior sem precisar parsear JSONL. + for thr, tm in sorted((ag.get("threshold_sweep", {}) or {}).items(), key=lambda kv: float(kv[0])): + tag = str(thr).replace(".", "p") + for k in ("iou", "f1", "precision", "recall", "cana_spray_rate", "ground_spray_rate", "weed_miss_rate", "operational_score", "constraints_met"): + row[f"{prefix}_thr_{tag}_{k}"] = tm.get(k) + + for group, gm in sorted((ag.get("group_target", {}) or {}).items()): + gtag = str(group).replace(" ", "_") + for k in ("iou", "f1", "precision", "recall", "cana_spray_rate", "ground_spray_rate", "weed_miss_rate"): + row[f"{prefix}_group_{gtag}_{k}"] = gm.get(k) + + for scen, sm in sorted((ag.get("scenario_target", {}) or {}).items()): + for k in ("iou_target", "f1", "precision", "recall"): + row[f"{prefix}_scenario_{scen}_{k}"] = sm.get(k) + + optm = obj.get("optimization", {}) or {} + for k in ("global_step", "optimizer_steps", "skipped_steps", "grad_norm_mean", "grad_norm_max"): + row[f"{prefix}_{k}"] = optm.get(k) + for k, v in obj.get("loss_parts", {}).items(): + row[f"{prefix}_{k}"] = v + return row + + +def pretty_iou(names: Dict[int, str], vals: List[float]): + return " | ".join(f"{names.get(i,i)}:{v:.3f}" for i,v in enumerate(vals)) + + +def binary_iou_text(head_name, vals): + names = {"vegetation": {0:"bg",1:"veg"}, "cana": {0:"not_cana",1:"cana"}, "target": {0:"bg",1:"target"}}.get(head_name,{0:"0",1:"1"}) + return pretty_iou(names, vals) + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--config", default="config.json") + parser.add_argument("--epochs", type=int, default=80) + parser.add_argument("--batch", type=int, default=1) + parser.add_argument("--lr", type=float, default=3e-5) + parser.add_argument("--wd", type=float, default=0.01) + parser.add_argument("--num_workers", type=int, default=2) + parser.add_argument("--amp", action="store_true") + parser.add_argument("--amp_val", action="store_true") + parser.add_argument("--grad_accum", type=int, default=4) + parser.add_argument("--grad_ckpt", action="store_true") + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--ignore_index", type=int, default=None) + parser.add_argument("--class_weights", default="auto") + parser.add_argument("--dice_weight", type=float, default=0.30) + parser.add_argument("--norm_stats", default=None) + parser.add_argument("--allow-missing-norm-stats", dest="allow_missing_norm_stats", action="store_true") + parser.add_argument("--src-root", default="dataset/split") + parser.add_argument("--save-every", type=int, default=10) + parser.add_argument("--resume", action="store_true") + parser.add_argument("--resume-ckpt", default=None) + parser.add_argument("--early-stop", type=int, default=25) + # Legado + overrides V2 + parser.add_argument("--balanced_sampler", action="store_true") + parser.add_argument("--samples_per_epoch", type=int, default=0) + parser.add_argument("--sampler", choices=["random","diverse","weighted"], default=None) + parser.add_argument("--decoder-mode", choices=["separate","shared_light"], default=None) + parser.add_argument("--spectral-init", choices=["mean_rgb","zero_extra","scaled_mean"], default=None) + parser.add_argument("--no-augment", action="store_true") + parser.add_argument("--grad-clip", type=float, default=None) + args = parser.parse_args() + + set_seed(args.seed) + config = load_json(args.config) + tv2 = get_training_v2_config(config) + if args.decoder_mode: tv2["model"]["decoder_mode"] = args.decoder_mode + if args.spectral_init: tv2["model"]["spectral_input_init"] = args.spectral_init + if args.no_augment: tv2["augmentation"]["enabled"] = False + if args.sampler: tv2["sampler"]["mode"] = args.sampler + if args.balanced_sampler: tv2["sampler"]["mode"] = "weighted" + if args.samples_per_epoch > 0: tv2["sampler"]["samples_per_epoch"] = args.samples_per_epoch + if args.grad_clip is not None: tv2["optimization"]["grad_clip_norm"] = args.grad_clip + config["training_v2"] = tv2 + + optcfg = tv2.get("optimization", {}) + try: torch.set_float32_matmul_precision(str(optcfg.get("matmul_precision", "high"))) + except Exception: pass + if torch.backends.cudnn.is_available(): + torch.backends.cudnn.benchmark = bool(optcfg.get("cudnn_benchmark", True)) + + W, H = config["resolucao"] + backbone = config.get("backbone", "nvidia/mit-b1") + input_channel_names = get_input_channel_names(config); channels = len(input_channel_names) + if config.get("fusion_mode", "stacked") != "stacked": raise RuntimeError("Este script exige fusion_mode='stacked'.") + model_family = config.get("modelo", "segformer"); model_name = config.get("model_name", "test") + stats_source_tag = config.get("stats_source_tag", f"stacked_raw{channels}") + save_dir = Path("backup") / model_family / model_name / stats_source_tag; ensure_dir(save_dir) + + labelmap_path = Path("dataset") / "labelmap.txt" + semantic_id2label, semantic_label2id, ignore_from_labelmap = load_labelmap(str(labelmap_path)) + ignore_index = int(args.ignore_index if args.ignore_index is not None else ignore_from_labelmap) + heads_config = build_heads_config(config, ignore_index) + heads_config["semantic"]["num_classes"] = len(semantic_id2label); heads_config["semantic"]["ignore_index"] = ignore_index + + print("="*70) + print(f"Agri Teacher V2 | {TRAINER_VERSION}") + print(f"Backbone={backbone} | resolution={W}x{H} | channels={input_channel_names}") + print(f"decoder={tv2['model']['decoder_mode']} | spectral_init={tv2['model']['spectral_input_init']}") + print(f"augmentation={tv2['augmentation']['enabled']} | sampler={tv2['sampler']['mode']}") + print(f"save_dir={save_dir}") + for name,h in heads_config.items(): print(f"HEAD {name}: classes={h['num_classes']} weight={h['loss_weight']} norm={h['loss_weight_norm']:.3f}") + print("="*70) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu"); print(f"Device={device}") + train_root, val_root = Path(args.src_root)/"train", Path(args.src_root)/"val" + resize_hw = (H, W) + ds_train = OakFcc3TensorMultiHeadDataset( + train_root, heads_config, channels, input_channel_names, True, resize_hw, + augment=bool(tv2["augmentation"].get("enabled", True)), augmentation_config=tv2["augmentation"], + derived_config=config.get("derived_channels", {}) or {}, + data_validation_config=tv2.get("data", {}) or {}, + ) + ds_val = OakFcc3TensorMultiHeadDataset( + val_root, heads_config, channels, input_channel_names, True, resize_hw, + augment=False, augmentation_config=None, derived_config=config.get("derived_channels", {}) or {}, + data_validation_config=tv2.get("data", {}) or {}, + ) + print(f"[DATA] train={len(ds_train)} val={len(ds_val)}") + + sampler_mode = str(tv2["sampler"].get("mode", "diverse")).lower() + common_dl = dict(num_workers=args.num_workers, pin_memory=True, collate_fn=collate_fn, worker_init_fn=seed_worker) + if args.num_workers > 0: + common_dl["persistent_workers"] = bool(optcfg.get("persistent_workers", True)) + common_dl["prefetch_factor"] = int(optcfg.get("prefetch_factor", 2)) + + diverse_batch_sampler = None + if sampler_mode == "diverse": + diverse_batch_sampler = DiverseAgriculturalBatchSampler(ds_train, args.batch, tv2["sampler"], args.seed) + dl_train = DataLoader(ds_train, batch_sampler=diverse_batch_sampler, **common_dl) + elif sampler_mode == "weighted": + weights = build_sample_weights(ds_train) + ns = int(tv2["sampler"].get("samples_per_epoch", 0) or 0) or len(ds_train) + sampler = WeightedRandomSampler(weights, num_samples=ns, replacement=True) + dl_train = DataLoader(ds_train, batch_size=args.batch, sampler=sampler, shuffle=False, drop_last=False, **common_dl) + print(f"[SAMPLER:WEIGHTED] min={float(weights.min()):.3f} max={float(weights.max()):.3f} mean={float(weights.mean()):.3f}") + else: + dl_train = DataLoader(ds_train, batch_size=args.batch, shuffle=True, drop_last=False, **common_dl) + + val_workers = max(0, args.num_workers // 2) + dl_val_kwargs = dict(batch_size=1, shuffle=False, num_workers=val_workers, pin_memory=True, collate_fn=collate_fn, worker_init_fn=seed_worker, drop_last=False) + if val_workers > 0: + dl_val_kwargs["persistent_workers"] = bool(optcfg.get("persistent_workers", True)); dl_val_kwargs["prefetch_factor"] = int(optcfg.get("prefetch_factor", 2)) + dl_val = DataLoader(ds_val, **dl_val_kwargs) + + normalizer, norm_stats_path = build_normalizer(config, args, device) + experiment_stats_path = None + if normalizer is not None: + source_stats = load_json(norm_stats_path) if norm_stats_path else {} + selected = {"channels": input_channel_names, "mean": normalizer.mean.detach().cpu().view(-1).tolist(), + "std": normalizer.std.detach().cpu().view(-1).tolist(), "pixels": source_stats.get("pixels"), + "source_norm_stats": norm_stats_path, "stats_source_tag": stats_source_tag} + experiment_stats_path = save_dir/"norm_stats.json"; save_json(experiment_stats_path, selected) + + model = build_model(backbone, input_channel_names, heads_config, semantic_id2label, semantic_label2id, config=config) + if args.grad_ckpt: + try: model.segformer.gradient_checkpointing_enable(); print("[MODEL] gradient checkpointing enabled") + except Exception as e: print(f"[WARN] gradient checkpointing: {e}") + model.to(device) + + criterions, weight_debug = build_criterions(ds_train, heads_config, args.class_weights, device, args.seed, tv2.get("class_weighting", {})) + optimizer = build_optimizer(model, args.lr, args.wd, tv2.get("optimizer", {})) + optimizer_steps_per_epoch = max(1, math.ceil(len(dl_train) / max(1, args.grad_accum))) + total_optimizer_steps = optimizer_steps_per_epoch * args.epochs + scheduler = WarmupDecayScheduler(optimizer, total_optimizer_steps, tv2.get("scheduler", {})) + scaler = GradScaler(enabled=args.amp and device.type == "cuda") + + snapshot = { + "trainer_version": TRAINER_VERSION, "config": config, "args": vars(args), "training_v2": tv2, + "semantic_id2label": semantic_id2label, "heads_config": heads_config, "ignore_index": ignore_index, + "norm_stats_path": norm_stats_path, "experiment_norm_stats_path": str(experiment_stats_path) if experiment_stats_path else None, + "class_weight_debug": weight_debug, "input_channel_names": input_channel_names, + "tensor_contract": "normalized_v2_final_channel_order", "stats_source_tag": stats_source_tag, + "optimizer_steps_per_epoch": optimizer_steps_per_epoch, "total_optimizer_steps": total_optimizer_steps, + } + snapshot_default = save_dir/"train_config_snapshot.json" + snapshot_path = snapshot_default if not snapshot_default.exists() else save_dir/f"train_config_snapshot_{TRAINER_VERSION}.json" + save_json(snapshot_path, snapshot) + + last_path=save_dir/"last.pt"; best_score_path=save_dir/"best_score.pt"; best_target_path=save_dir/"best_target.pt" + best_cana_path=save_dir/"best_cana_head.pt"; best_sem_path=save_dir/"best_semantic_miou.pt"; best_safety_path=save_dir/"best_cana_safety.pt" + best_legacy_path=save_dir/"best_legacy_score.pt"; best_operational_path=save_dir/"best_operational.pt" + + # Se o diretório já contém logs V2.0, não misturamos headers CSV diferentes. + legacy_train_log = save_dir/"train_log.csv" + legacy_jsonl = save_dir/"train_metrics.jsonl" + train_log_path = legacy_train_log if not legacy_train_log.exists() else save_dir/f"train_log_{TRAINER_VERSION}.csv" + jsonl_path = legacy_jsonl if not legacy_jsonl.exists() else save_dir/f"train_metrics_{TRAINER_VERSION}.jsonl" + + start_epoch=1; global_step=0; epochs_without_improve=0 + best={ + "score":-1.0, "legacy_score":-1.0, "operational_score":-1.0, + "target_iou":-1.0, "cana_head_iou":-1.0, "semantic_miou":-1.0, + "vegetation_miou":-1.0, "cana_safety":-1.0 + } + resume_path = Path(args.resume_ckpt) if args.resume_ckpt else last_path + if args.resume and resume_path.exists(): + ckpt = load_checkpoint(resume_path, model, optimizer, scaler, "cpu", scheduler=scheduler, restore_rng=True) + start_epoch=int(ckpt.get("epoch",0))+1; best=ckpt.get("best",best); global_step=int(ckpt.get("global_step",0)); epochs_without_improve=int(ckpt.get("epochs_without_improve",0)) + print(f"[RESUME] {resume_path} -> epoch={start_epoch} global_step={global_step}") + + min_delta = float(tv2.get("checkpoint",{}).get("early_stop_min_delta",5e-4)) + best_score_for_es = float(best.get("score", -1.0)) + + for epoch in range(start_epoch, args.epochs+1): + if diverse_batch_sampler is not None: diverse_batch_sampler.set_epoch(epoch) + lr_now = [float(g["lr"]) for g in optimizer.param_groups] + print(f"\n==== Epoch {epoch}/{args.epochs} | lr={[f'{x:.2e}' for x in lr_now]} ====") + if device.type=="cuda": torch.cuda.reset_peak_memory_stats() + + tr = run_one_epoch(model, dl_train, optimizer, device, heads_config, criterions, args.amp, scaler, True, + max(1,args.grad_accum), normalizer, args.dice_weight, config, epoch, + scheduler=scheduler, grad_clip_norm=float(tv2["optimization"].get("grad_clip_norm",1.0)), global_step=global_step) + global_step = int(tr.get("optimization",{}).get("global_step",global_step)) + va = run_one_epoch(model, dl_val, None, device, heads_config, criterions, args.amp_val, None, False, + 1, normalizer, args.dice_weight, config, epoch, scheduler=None, grad_clip_norm=0, global_step=global_step) + score_now = compute_selection_score(va, config) + + th = va["heads"].get("target",{}); ag = va.get("agricultural",{}) + tr_th = tr["heads"].get("target", {}) + print( + f"TRAIN loss={tr['loss']:.4f} " + f"sem_mIoU={tr['heads']['semantic']['miou']:.4f} " + f"cana_mIoU={tr['heads']['cana']['miou']:.4f} " + f"targetIoU={tr_th.get('iou_per_class',[0,0])[1]:.4f} " + f"t={tr['time_s']:.1f}s" + ) + print( + f"VAL loss={va['loss']:.4f} " + f"sem_mIoU={va['heads']['semantic']['miou']:.4f} " + f"canaIoU={va['heads']['cana']['iou_per_class'][1]:.4f} " + f"targetIoU={th.get('iou_per_class',[0,0])[1]:.4f} " + f"P={th.get('precision',0):.4f} R={th.get('recall',0):.4f} F1={th.get('f1',0):.4f} " + f"score={score_now['score']:.4f}" + ) + print( + f"LEGACY score={score_now.get('legacy_score',0):.4f} " + f"opTargetIoU={score_now.get('operational_target_iou',0):.4f}" + ) + print( + f"AGRI@0.50 cana_safety={ag.get('cana_safety',0):.5f} " + f"spray_cana={100*ag.get('cana_spray_rate',0):.3f}% " + f"spray_ground={100*ag.get('ground_spray_rate',0):.3f}% " + f"weed_miss={100*ag.get('weed_miss_rate',0):.3f}% " + f"macroIoU={ag.get('macro_target_iou',0):.4f}" + ) + bt = ag.get("best_threshold", {}) or {} + print(f"THR-F1 best={bt.get('threshold')} F1={bt.get('f1',0):.4f} IoU={bt.get('iou',0):.4f}") + bop = ag.get("best_operational_threshold", {}) or {} + print( + f"THR-OP best={bop.get('threshold')} score={bop.get('operational_score',0):.4f} " + f"safe={'YES' if bop.get('constraints_met',False) else 'NO'} " + f"spray_cana={100*safe_float(bop.get('cana_spray_rate'),0):.3f}% " + f"spray_ground={100*safe_float(bop.get('ground_spray_rate'),0):.3f}% " + f"weed_miss={100*safe_float(bop.get('weed_miss_rate'),0):.3f}%" + ) + print( + f"CAL ECE={safe_float(ag.get('target_ece'),0):.4f} " + f"Brier={safe_float(ag.get('target_brier'),0):.5f} " + f"NLL={safe_float(ag.get('target_nll'),0):.5f}" + ) + print( + f"SAFE cana->erva={100*ag.get('semantic_cana_as_erva_rate',0):.3f}% " + f"erva->cana={100*ag.get('semantic_erva_as_cana_rate',0):.3f}% " + f"target/composed disagree={100*ag.get('target_composed_disagreement_rate',0):.3f}% " + f"IoU={ag.get('target_composed_iou',0):.4f}" + ) + print( + f"FRAME target_detect={100*ag.get('frame_target_detection_rate',0):.2f}% " + f"complete_miss={100*ag.get('frame_target_complete_miss_rate',0):.2f}% " + f"cana_clean={100*ag.get('frame_cana_clean_rate',0):.2f}% " + f"ground_clean={100*ag.get('frame_ground_clean_rate',0):.2f}%" + ) + + sweep = ag.get("threshold_sweep", {}) or {} + if sweep: + print("THR-SWEEP:") + for tkey, tm in sorted(sweep.items(), key=lambda kv: float(kv[0])): + flag = "OK" if tm.get("constraints_met", False) else "--" + print( + f" t={float(tkey):.2f} IoU={tm.get('iou',0):.4f} F1={tm.get('f1',0):.4f} " + f"P={tm.get('precision',0):.4f} R={tm.get('recall',0):.4f} " + f"cana={100*tm.get('cana_spray_rate',0):.2f}% " + f"ground={100*tm.get('ground_spray_rate',0):.2f}% " + f"miss={100*tm.get('weed_miss_rate',0):.2f}% op={tm.get('operational_score',0):.4f} {flag}" + ) + + scen = ag.get("scenario_target", {}) or {} + if scen: + parts = [] + for name in ("none", "tiny", "small", "medium", "large"): + sm = scen.get(name, {}) or {} + if name == "none": + parts.append(f"none FP={100*(1-safe_float(sm.get('specificity'),1)):.2f}%") + else: + parts.append(f"{name} IoU={sm.get('iou_target',0):.3f}/R={sm.get('recall',0):.3f}") + print("SCEN " + " | ".join(parts)) + + groups = ag.get("group_target", {}) or {} + if groups: + print("GROUP:") + for gname, gm in sorted(groups.items()): + print( + f" {gname}: IoU={gm.get('iou',0):.4f} F1={gm.get('f1',0):.4f} " + f"R={gm.get('recall',0):.4f} cana={100*gm.get('cana_spray_rate',0):.2f}% " + f"ground={100*gm.get('ground_spray_rate',0):.2f}% miss={100*gm.get('weed_miss_rate',0):.2f}%" + ) + + print("IoU semantic:", pretty_iou(semantic_id2label, va["heads"]["semantic"]["iou_per_class"])) + print("IoU vegetation:", binary_iou_text("vegetation", va["heads"]["vegetation"]["iou_per_class"])) + print("IoU cana:", binary_iou_text("cana", va["heads"]["cana"]["iou_per_class"])) + print("IoU target:", binary_iou_text("target", va["heads"]["target"]["iou_per_class"])) + if device.type=="cuda": + print(f"GPU peak={torch.cuda.max_memory_allocated()/1024**3:.2f} GiB") + + old = copy.deepcopy(best) + op_score = safe_float( + score_now.get("operational_score"), + -1.0, + ) + op_safe = bool( + score_now.get("operational_constraints_met", False) + ) + best["score"] = max(float(best.get("score",-1)), score_now["score"]) + best["legacy_score"] = max(float(best.get("legacy_score",-1)), score_now.get("legacy_score",-1)) + # O histórico "best operational" só aceita epochs SAFE. + if op_safe: + best["operational_score"] = max(float(best.get("operational_score", -1.0)), op_score) + best["target_iou"] = max(float(best.get("target_iou",-1)), score_now["target_iou"]) + best["cana_head_iou"] = max(float(best.get("cana_head_iou",-1)), score_now["cana_head_iou"]) + best["semantic_miou"] = max(float(best.get("semantic_miou",-1)), score_now["semantic_miou"]) + best["vegetation_miou"] = max(float(best.get("vegetation_miou",-1)), score_now["vegetation_miou"]) + best["cana_safety"] = max(float(best.get("cana_safety",-1)), score_now["cana_safety"]) + + extra={"train":tr,"val":va,"score_now":score_now,"lr_groups":[float(g['lr']) for g in optimizer.param_groups]} + if score_now["score"] > float(old.get("score",-1)): + save_checkpoint(best_score_path,model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step); print(f"[BEST SCORE] {score_now['score']:.4f}") + if bool(tv2.get("checkpoint",{}).get("save_best_legacy",True)) and score_now.get("legacy_score",-1) > float(old.get("legacy_score",-1)): + save_checkpoint(best_legacy_path,model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step); print(f"[BEST LEGACY] {score_now.get('legacy_score',-1):.4f}") + if (bool(tv2.get("checkpoint", {}).get("save_best_operational", True)) and op_safe and op_score > float(old.get("operational_score", -1.0))): + save_checkpoint(best_operational_path, model, optimizer, scaler, epoch, best, extra, scheduler, epochs_without_improve, global_step); print(f"[BEST OPERATIONAL SAFE] {op_score:.4f} @thr={score_now.get('operational_threshold')}") + if score_now["target_iou"] > float(old.get("target_iou",-1)): + save_checkpoint(best_target_path,model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step); print(f"[BEST TARGET] {score_now['target_iou']:.4f}") + if score_now["cana_head_iou"] > float(old.get("cana_head_iou",-1)): + save_checkpoint(best_cana_path,model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step); print(f"[BEST CANA] {score_now['cana_head_iou']:.4f}") + if score_now["semantic_miou"] > float(old.get("semantic_miou",-1)): + save_checkpoint(best_sem_path,model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step); print(f"[BEST SEM] {score_now['semantic_miou']:.4f}") + if bool(tv2.get("checkpoint",{}).get("save_best_safety",True)) and score_now["cana_safety"] > float(old.get("cana_safety",-1)): + save_checkpoint(best_safety_path,model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step); print(f"[BEST SAFETY] {score_now['cana_safety']:.5f}") + + # Early stop is driven only by the principal agricultural score. + if score_now["score"] > best_score_for_es + min_delta: + best_score_for_es = score_now["score"]; epochs_without_improve = 0 + else: + epochs_without_improve += 1 + + save_checkpoint(last_path,model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step) + if args.save_every>0 and epoch%args.save_every==0: + save_checkpoint(save_dir/f"epoch_{epoch:04d}.pt",model,optimizer,scaler,epoch,best,extra,scheduler,epochs_without_improve,global_step) + + append_train_log(train_log_path, flatten_epoch_log(epoch, lr_now[0], tr, va, best, score_now)) + append_jsonl(jsonl_path, {"epoch":epoch,"train":tr,"val":va,"score_now":score_now,"best":best}) + + if args.early_stop>0 and epochs_without_improve>=args.early_stop: + print(f"[EARLY STOP] {epochs_without_improve} épocas sem ganho de score >= {min_delta}.") + break + + print("\nTreino finalizado.") + print(json.dumps(best, indent=2, ensure_ascii=False)) + print(f"Save dir: {save_dir}") + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/_9_review_dataset_multihead_v2.py b/Python/OAK/datasets/oak-fcc-3/_9_review_dataset_multihead_v2.py new file mode 100644 index 000000000..069c1339c --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/_9_review_dataset_multihead_v2.py @@ -0,0 +1,1478 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +r""" +_9_review_dataset_multihead_v2.py + +Auditoria/model-mining do dataset usando o mesmo contrato de inferência do +_9_test_multihead_v2.py. + +Objetivo +-------- +Rodar um checkpoint sobre TODO o dataset normalizado (ex.: dataset/960x600/group), +medir onde GT e modelo discordam e separar automaticamente os casos que merecem +revisão humana. + +IMPORTANTE: discordância modelo x GT NÃO é tratada automaticamente como erro do +modelo. Em um dataset com rótulos imperfeitos, um conflito de alta confiança pode +ser justamente um forte candidato a máscara incorreta. + +Saída de revisão (quando não usa --report_only): + + dataset/revisao/group// + previews/.png + masks/.png # GT semantic colorido + predictions/.png # predição semantic colorida + panels/.png # opcional, --save_panels + final_masks/.png # NÃO é tocado pelo auditor + review_order.csv + +Modos de exportação: + --export_mode flagged + Exporta casos acionados pelos critérios técnicos. + --export_mode score --min_suspicion_pct 80 + Exporta casos com review_score*100 >= 80. + --export_mode all + Exporta todas as amostras. + +suspicion_pct = review_score*100 é uma escala heurística de ranking, +não uma probabilidade estatística de a máscara estar errada. + +Relatórios por execução/checkpoint: + + dataset/revisao/reports// + review_report.csv # TODAS as amostras + review_candidates.csv # somente selecionadas + review_summary.json + confusion_semantic.csv + confusion_vegetation.csv + confusion_cana.csv + confusion_target.csv + +Exemplos +-------- + +1) Gerar revisão física dos casos suspeitos no dataset 960x600: + +python .\_9_review_dataset_multihead_v2.py ^ + --config .\config.json ^ + --root dataset\960x600\group ^ + --ckpt backup\segformer_b1\2026_08_27_linear_demosaic\stacked_raw5\best_score.pt ^ + --target_threshold auto ^ + --export_mode score --min_suspicion_pct 80 + --clear_review + +2) Somente relatório, sem copiar previews/masks/predictions: + +python .\_9_review_dataset_multihead_v2.py ^ + --config .\config.json ^ + --root dataset\960x600\group ^ + --ckpt backup\...\best_cana_head.pt ^ + --report_only ^ + --run_name best_cana + +3) Forçar os 100 casos mais suspeitos de cada grupo para revisão: + +python .\_9_review_dataset_multihead_v2.py ^ + --config .\config.json ^ + --root dataset\960x600\group ^ + --ckpt backup\...\best_score.pt ^ + --top_k_per_group 100 ^ + --clear_review + +4) Mesmo auditor usando ONNX Runtime + TensorRT (logits): + +python .\_9_review_dataset_multihead_v2.py ^ + --config .\config.json ^ + --root dataset\960x600\group ^ + --onnx backup\...\best_score_audit.onnx ^ + --onnx_provider tensorrt ^ + --target_threshold auto ^ + --export_mode flagged ^ + --clear_review + +IMPORTANTE para ONNX: +- O auditor precisa de LOGITS/probabilidades para medir alta confiança. +- Exporte com --postprocess none ou --postprocess resize_logits. +- ONNX mask-only (argmax_lowres/argmax_fullres) é recusado de propósito. +- Normalização embutida é detectada pelo .export_meta.json para evitar dupla normalização. +""" + +from __future__ import annotations + +import argparse +import csv +import importlib.util +import json +import math +import os +import shutil +import sys +import time +from collections import defaultdict +from pathlib import Path +from typing import Dict, Iterable, List, Optional, Sequence, Tuple + +import cv2 +import numpy as np +import torch + + +# ============================================================ +# Utilidades gerais +# ============================================================ + + +def load_json(path: Path) -> dict: + with path.open("r", encoding="utf-8") as f: + return json.load(f) + + +def ensure_dir(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + + +def resolve_path(path_like: Optional[str], base: Optional[Path] = None) -> Optional[Path]: + if path_like is None: + return None + p = Path(path_like) + if p.is_absolute(): + return p + return ((base or Path.cwd()) / p).resolve() + + +def safe_float(v, default: float = 0.0) -> float: + try: + x = float(v) + return x if math.isfinite(x) else default + except Exception: + return default + + +def pct(x: float) -> float: + return float(x) * 100.0 + + +def clear_dir(path: Path) -> None: + if path.exists(): + shutil.rmtree(path) + path.mkdir(parents=True, exist_ok=True) + + +def clear_generated_review(review_group_root: Path) -> None: + """Limpa somente artefatos regeneráveis e PRESERVA final_masks/.""" + if not review_group_root.exists(): + review_group_root.mkdir(parents=True, exist_ok=True) + return + + for group_dir in review_group_root.iterdir(): + if not group_dir.is_dir(): + continue + + for name in ("previews", "masks", "predictions", "panels"): + p = group_dir / name + if p.exists(): + shutil.rmtree(p) + + order_csv = group_dir / "review_order.csv" + if order_csv.exists(): + order_csv.unlink() + + +def load_test_module(script_path: Path): + if not script_path.is_file(): + raise FileNotFoundError( + f"Script base não encontrado: {script_path}\n" + "Coloque este arquivo ao lado de _9_test_multihead_v2.py ou informe --test_script." + ) + spec = importlib.util.spec_from_file_location("agri_test_multihead_v2", str(script_path)) + if spec is None or spec.loader is None: + raise RuntimeError(f"Não consegui carregar módulo de teste: {script_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + + +def load_onnx_export_meta(onnx_path: Path) -> Tuple[dict, Optional[Path]]: + """Carrega o sidecar gerado por _10_export_onnx.py, quando existir.""" + candidates = [ + onnx_path.with_suffix(".export_meta.json"), + onnx_path.parent / f"{onnx_path.stem}.export_meta.json", + ] + for p in candidates: + if p.is_file(): + try: + return load_json(p), p + except Exception as exc: + raise RuntimeError(f"Falha ao ler metadata ONNX: {p}: {exc}") from exc + return {}, None + + +def onnx_normalization_embedded(meta: dict, mode: str = "auto") -> bool: + mode = str(mode or "auto").strip().lower() + if mode == "embedded": + return True + if mode == "external": + return False + + contract = meta.get("input_contract", {}) if isinstance(meta, dict) else {} + if isinstance(contract, dict) and "normalization_embedded" in contract: + return bool(contract.get("normalization_embedded")) + if isinstance(meta, dict) and "include_norm" in meta: + return bool(meta.get("include_norm")) + return False + + +def _extract_operational_threshold_from_ckpt_dict(ckpt: dict) -> Optional[float]: + if not isinstance(ckpt, dict): + return None + + candidates = [] + best = ckpt.get("best", {}) or {} + if isinstance(best, dict): + candidates.extend([ + best.get("operational_threshold"), + best.get("best_operational_threshold"), + ]) + + extra = ckpt.get("extra", {}) or {} + if isinstance(extra, dict): + score_now = extra.get("score_now", {}) or {} + if isinstance(score_now, dict): + candidates.append(score_now.get("operational_threshold")) + val = extra.get("val", {}) or {} + if isinstance(val, dict): + agri = val.get("agricultural", {}) or {} + if isinstance(agri, dict): + bop = agri.get("best_operational_threshold", {}) or {} + if isinstance(bop, dict): + candidates.append(bop.get("threshold")) + + for value in candidates: + try: + if value is not None: + v = float(value) + if 0.0 <= v <= 1.0: + return v + except Exception: + pass + return None + + +def resolve_onnx_operational_threshold(meta: dict, meta_path: Optional[Path], onnx_path: Path) -> Tuple[Optional[float], str]: + """ + Resolve o threshold operacional sem precisar executar PyTorch. + + Ordem: + 1) metadata ONNX explícita; + 2) checkpoint_best do sidecar; + 3) checkpoint .pt referenciado no sidecar, carregado apenas para ler metadata. + """ + if isinstance(meta, dict): + direct_candidates = [ + meta.get("operational_threshold"), + meta.get("checkpoint_operational_threshold"), + ] + best = meta.get("checkpoint_best", {}) or {} + if isinstance(best, dict): + direct_candidates.extend([ + best.get("operational_threshold"), + best.get("best_operational_threshold"), + ]) + for value in direct_candidates: + try: + if value is not None: + v = float(value) + if 0.0 <= v <= 1.0: + return v, "onnx_export_meta" + except Exception: + pass + + ckpt_txt = meta.get("checkpoint") + if ckpt_txt: + raw = Path(str(ckpt_txt)) + candidates = [raw] + if not raw.is_absolute(): + if meta_path is not None: + candidates.append((meta_path.parent / raw).resolve()) + candidates.append((onnx_path.parent / raw).resolve()) + candidates.append((Path.cwd() / raw).resolve()) + ckpt_path = next((p for p in candidates if p.is_file()), None) + if ckpt_path is not None: + try: + # Carrega apenas para metadata. Nenhum modelo PyTorch é construído/executado. + ckpt = torch.load(str(ckpt_path), map_location="cpu", weights_only=False) + thr = _extract_operational_threshold_from_ckpt_dict(ckpt) + if thr is not None: + return thr, f"checkpoint_metadata:{ckpt_path}" + except Exception as exc: + print(f"[ONNX][WARN] não consegui ler threshold do checkpoint do sidecar: {exc}") + + return None, "none" + + +def validate_onnx_for_audit(tester, meta: dict, onnx_path: Path) -> None: + """ + Auditoria precisa de probabilidades reais. Portanto, o ONNX deve devolver logits. + Modelos exportados apenas com argmax/mask não servem para high-confidence mining. + """ + postprocess = str((meta or {}).get("postprocess", "")).strip().lower() + output_names = [str(x) for x in getattr(tester, "output_names", [])] + + mask_only_by_meta = postprocess in ("argmax_lowres", "argmax_fullres") + mask_only_by_name = bool(output_names) and all( + name.lower().endswith("_mask") or "mask" in name.lower() + for name in output_names + ) + + if mask_only_by_meta or mask_only_by_name: + raise RuntimeError( + "Este ONNX devolve máscaras/argmax, mas o auditor precisa de LOGITS para " + "calcular probabilidades, high-confidence conflict e uncertainty.\n" + f"ONNX: {onnx_path}\n" + f"postprocess={postprocess or 'desconhecido'} outputs={output_names}\n" + "Exporte um ONNX de auditoria com --postprocess none (recomendado) " + "ou --postprocess resize_logits." + ) + + missing = [] + for required in ("semantic", "vegetation", "cana"): + if not any(required in name.lower() for name in output_names): + missing.append(required) + if missing: + raise RuntimeError( + f"ONNX não expõe todas as heads necessárias para auditoria: faltando={missing}, " + f"outputs={output_names}" + ) + + +def write_csv(path: Path, rows: Sequence[dict], fieldnames: Optional[Sequence[str]] = None) -> None: + ensure_dir(path.parent) + if fieldnames is None: + keys = [] + seen = set() + for row in rows: + for key in row.keys(): + if key not in seen: + seen.add(key) + keys.append(key) + fieldnames = keys + + with path.open("w", newline="", encoding="utf-8-sig") as f: + writer = csv.DictWriter(f, fieldnames=list(fieldnames), extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow(row) + + +def write_cm_csv(path: Path, cm: np.ndarray, labels: Sequence[str]) -> None: + rows = [] + for gi, gt_name in enumerate(labels): + row = {"gt\\pred": gt_name} + for pi, pred_name in enumerate(labels): + row[pred_name] = int(cm[gi, pi]) + rows.append(row) + write_csv(path, rows, ["gt\\pred", *labels]) + + +# ============================================================ +# Métricas +# ============================================================ + + +def resize_ids(mask: np.ndarray, hw: Tuple[int, int]) -> np.ndarray: + h, w = hw + if mask.shape[:2] == (h, w): + return mask + return cv2.resize(mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) + + +def class_metrics_from_cm(cm: np.ndarray, labels: Sequence[str]) -> dict: + cm = cm.astype(np.float64) + tp = np.diag(cm) + support = cm.sum(axis=1) + pred_count = cm.sum(axis=0) + fp = pred_count - tp + fn = support - tp + + precision = np.divide(tp, tp + fp, out=np.zeros_like(tp), where=(tp + fp) > 0) + recall = np.divide(tp, tp + fn, out=np.zeros_like(tp), where=(tp + fn) > 0) + f1 = np.divide(2 * precision * recall, precision + recall, out=np.zeros_like(tp), where=(precision + recall) > 0) + iou = np.divide(tp, tp + fp + fn, out=np.zeros_like(tp), where=(tp + fp + fn) > 0) + + total = max(float(cm.sum()), 1.0) + out = { + "accuracy": float(tp.sum() / total), + "miou": float(np.mean(iou)) if len(iou) else 0.0, + "classes": {}, + } + for i, name in enumerate(labels): + out["classes"][str(name)] = { + "support_pixels": int(support[i]), + "pred_pixels": int(pred_count[i]), + "precision": float(precision[i]), + "recall": float(recall[i]), + "f1": float(f1[i]), + "iou": float(iou[i]), + } + return out + + +def directional_rate(cm: np.ndarray, gt_idx: int, pred_idx: int) -> float: + den = int(cm[gt_idx].sum()) + if den <= 0: + return 0.0 + return float(cm[gt_idx, pred_idx] / den) + + +def binary_target_stats(pred: np.ndarray, gt: np.ndarray, ignore_id: int) -> dict: + valid = gt != ignore_id + gt1 = (gt == 1) & valid + pr1 = (pred == 1) & valid + tp = int((gt1 & pr1).sum()) + fp = int((~gt1 & pr1 & valid).sum()) + fn = int((gt1 & ~pr1).sum()) + tn = int((~gt1 & ~pr1 & valid).sum()) + + precision = tp / max(tp + fp, 1) + recall = tp / max(tp + fn, 1) + f1 = 2 * precision * recall / max(precision + recall, 1e-12) + iou = tp / max(tp + fp + fn, 1) + return { + "tp": tp, + "fp": fp, + "fn": fn, + "tn": tn, + "precision": float(precision), + "recall": float(recall), + "f1": float(f1), + "iou": float(iou), + } + + +def semantic_sample_signals( + pred: np.ndarray, + probs: np.ndarray, + gt: np.ndarray, + ignore_id: int, + high_confidence: float, + uncertainty_margin: float, +) -> dict: + if pred.shape != gt.shape: + pred = resize_ids(pred, gt.shape[:2]) + + valid = gt != ignore_id + valid_n = int(valid.sum()) + if valid_n <= 0: + return { + "valid_pixels": 0, + "wrong_pixels": 0, + "highconf_wrong_pixels": 0, + "uncertain_pixels": 0, + "gt_chao_pixels": 0, + "gt_cana_pixels": 0, + "gt_erva_pixels": 0, + "hc_gt_chao_pred_cana_pixels": 0, + "hc_gt_chao_pred_erva_pixels": 0, + "hc_gt_cana_pred_erva_pixels": 0, + "hc_gt_erva_pred_cana_pixels": 0, + "semantic_disagree_frac": 0.0, + "highconf_disagree_frac": 0.0, + "uncertain_frac": 0.0, + "mean_conf_correct": 0.0, + "mean_conf_wrong": 0.0, + "gt_chao_pred_cana_frac": 0.0, + "gt_chao_pred_erva_frac": 0.0, + "gt_cana_pred_erva_frac": 0.0, + "gt_erva_pred_cana_frac": 0.0, + "hc_gt_chao_pred_cana_frac": 0.0, + "hc_gt_chao_pred_erva_frac": 0.0, + "hc_gt_cana_pred_erva_frac": 0.0, + "hc_gt_erva_pred_cana_frac": 0.0, + } + + top1 = np.max(probs, axis=0) + if probs.shape[0] >= 2: + part = np.partition(probs, kth=probs.shape[0] - 2, axis=0) + top2 = part[-2] + else: + top2 = np.zeros_like(top1) + margin = top1 - top2 + + wrong = valid & (pred != gt) + correct = valid & (pred == gt) + highconf_wrong = wrong & (top1 >= high_confidence) + uncertain = valid & (margin <= uncertainty_margin) + + def frac(mask: np.ndarray) -> float: + return float(mask.sum() / valid_n) + + def conf_mean(mask: np.ndarray) -> float: + n = int(mask.sum()) + return float(top1[mask].mean()) if n else 0.0 + + g0p1 = valid & (gt == 0) & (pred == 1) + g0p2 = valid & (gt == 0) & (pred == 2) + g1p2 = valid & (gt == 1) & (pred == 2) + g2p1 = valid & (gt == 2) & (pred == 1) + + return { + "valid_pixels": valid_n, + "wrong_pixels": int(wrong.sum()), + "highconf_wrong_pixels": int(highconf_wrong.sum()), + "uncertain_pixels": int(uncertain.sum()), + "gt_chao_pixels": int((valid & (gt == 0)).sum()), + "gt_cana_pixels": int((valid & (gt == 1)).sum()), + "gt_erva_pixels": int((valid & (gt == 2)).sum()), + "hc_gt_chao_pred_cana_pixels": int((g0p1 & (top1 >= high_confidence)).sum()), + "hc_gt_chao_pred_erva_pixels": int((g0p2 & (top1 >= high_confidence)).sum()), + "hc_gt_cana_pred_erva_pixels": int((g1p2 & (top1 >= high_confidence)).sum()), + "hc_gt_erva_pred_cana_pixels": int((g2p1 & (top1 >= high_confidence)).sum()), + "semantic_disagree_frac": frac(wrong), + "highconf_disagree_frac": frac(highconf_wrong), + "uncertain_frac": frac(uncertain), + "mean_conf_correct": conf_mean(correct), + "mean_conf_wrong": conf_mean(wrong), + "gt_chao_pred_cana_frac": frac(g0p1), + "gt_chao_pred_erva_frac": frac(g0p2), + "gt_cana_pred_erva_frac": frac(g1p2), + "gt_erva_pred_cana_frac": frac(g2p1), + "hc_gt_chao_pred_cana_frac": frac(g0p1 & (top1 >= high_confidence)), + "hc_gt_chao_pred_erva_frac": frac(g0p2 & (top1 >= high_confidence)), + "hc_gt_cana_pred_erva_frac": frac(g1p2 & (top1 >= high_confidence)), + "hc_gt_erva_pred_cana_frac": frac(g2p1 & (top1 >= high_confidence)), + } + + +def review_score(signals: dict, target_disagree_frac: float) -> float: + """ + Score apenas para RANKING, não é probabilidade de máscara errada. + + Dá mais peso para conflito de alta confiança que para simples incerteza. + Cada termo é saturado numa escala prática para evitar que um único tipo de + erro domine completamente a ordenação. + """ + sem = min(safe_float(signals.get("semantic_disagree_frac")) / 0.20, 1.0) + hc = min(safe_float(signals.get("highconf_disagree_frac")) / 0.05, 1.0) + cross = min( + ( + safe_float(signals.get("hc_gt_cana_pred_erva_frac")) + + safe_float(signals.get("hc_gt_erva_pred_cana_frac")) + ) / 0.02, + 1.0, + ) + tgt = min(safe_float(target_disagree_frac) / 0.15, 1.0) + unc = min(safe_float(signals.get("uncertain_frac")) / 0.25, 1.0) + return float(0.30 * sem + 0.40 * hc + 0.15 * cross + 0.10 * tgt + 0.05 * unc) + + +def build_reasons( + row: dict, + min_disagree_frac: float, + min_highconf_disagree_frac: float, + min_cross_frac: float, + min_target_disagree_frac: float, + min_conflict_pixels: int, +) -> List[str]: + reasons: List[str] = [] + valid_pixels = max(int(row.get("valid_pixels", 0)), 1) + hc_pixels = int(round(float(row.get("highconf_disagree_frac", 0.0)) * valid_pixels)) + + if float(row.get("semantic_disagree_frac", 0.0)) >= min_disagree_frac: + reasons.append("SEMANTIC_DISAGREE") + + if ( + float(row.get("highconf_disagree_frac", 0.0)) >= min_highconf_disagree_frac + and hc_pixels >= min_conflict_pixels + ): + reasons.append("HIGH_CONF_GT_CONFLICT") + + if ( + float(row.get("hc_gt_chao_pred_cana_frac", 0.0)) + + float(row.get("hc_gt_chao_pred_erva_frac", 0.0)) + >= min_highconf_disagree_frac + ): + reasons.append("GT_CHAO_PRED_VEGETATION") + + if ( + float(row.get("hc_gt_cana_pred_erva_frac", 0.0)) + + float(row.get("hc_gt_erva_pred_cana_frac", 0.0)) + >= min_cross_frac + ): + reasons.append("CANA_ERVA_CROSS") + + if float(row.get("target_disagree_frac", 0.0)) >= min_target_disagree_frac: + reasons.append("TARGET_DISAGREE") + + return reasons + + +# ============================================================ +# Visualização/copiar casos de revisão +# ============================================================ + + +def load_preview_rgb(sample, chw: np.ndarray, base_module, input_channel_names: Sequence[str]) -> np.ndarray: + if getattr(sample, "preview_path", None) is not None and sample.preview_path.is_file(): + bgr = cv2.imread(str(sample.preview_path), cv2.IMREAD_COLOR) + if bgr is not None: + return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + return base_module.tensor_to_preview_rgb(chw, input_channel_names) + + +def save_review_item( + row: dict, + sample, + chw: np.ndarray, + gt_sem: np.ndarray, + pred_sem: np.ndarray, + review_group_root: Path, + base_module, + semantic_cmap: Dict[int, Tuple[int, int, int]], + ignore_id: int, + input_channel_names: Sequence[str], + save_panels: bool, +) -> None: + group_dir = review_group_root / sample.group + previews_dir = group_dir / "previews" + masks_dir = group_dir / "masks" + predictions_dir = group_dir / "predictions" + panels_dir = group_dir / "panels" + + ensure_dir(previews_dir) + ensure_dir(masks_dir) + ensure_dir(predictions_dir) + if save_panels: + ensure_dir(panels_dir) + + # Revisão humana usa somente PNG. Mesmo se o preview fonte for JPG, + # a saída é normalizada para .png. + preview_rgb = load_preview_rgb(sample, chw, base_module, input_channel_names) + cv2.imwrite( + str(previews_dir / f"{sample.base}.png"), + cv2.cvtColor(preview_rgb, cv2.COLOR_RGB2BGR), + ) + + gt_rgb = base_module.ids_to_rgb(gt_sem, semantic_cmap, ignore_id) + pred_rgb = base_module.ids_to_rgb(pred_sem, semantic_cmap, ignore_id) + + cv2.imwrite( + str(masks_dir / f"{sample.base}.png"), + cv2.cvtColor(gt_rgb, cv2.COLOR_RGB2BGR), + ) + cv2.imwrite( + str(predictions_dir / f"{sample.base}.png"), + cv2.cvtColor(pred_rgb, cv2.COLOR_RGB2BGR), + ) + + if save_panels: + # Normaliza tamanho do preview para GT/pred. + h, w = gt_sem.shape[:2] + if preview_rgb.shape[:2] != (h, w): + preview_rgb = cv2.resize(preview_rgb, (w, h), interpolation=cv2.INTER_AREA) + + diff = np.zeros_like(gt_rgb) + valid = gt_sem != ignore_id + bad = valid & (gt_sem != pred_sem) + diff[bad] = (255, 40, 40) + + overlay_gt = base_module.overlay_rgb(preview_rgb, gt_rgb, 0.45) + overlay_pred = base_module.overlay_rgb(preview_rgb, pred_rgb, 0.45) + panels = [ + ("Preview", preview_rgb, sample.base), + ("GT semantic", overlay_gt, f"grupo={sample.group}"), + ("Pred semantic", overlay_pred, f"score={float(row['review_score']):.3f}"), + ("GT != Pred", diff, str(row.get("review_reasons", ""))), + ] + panel = base_module.compose_grid(panels, cols=2, max_width=1600) + cv2.imwrite(str(panels_dir / f"{sample.base}.png"), cv2.cvtColor(panel, cv2.COLOR_RGB2BGR)) + + +# ============================================================ +# Main +# ============================================================ + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Audita masks usando um checkpoint multi-head e separa casos suspeitos para revisão humana." + ) + parser.add_argument("--config", default="config.json") + parser.add_argument("--root", default=None, help="Default: dataset//group conforme config.resolucao") + parser.add_argument("--ckpt", default=None, help="Checkpoint .pt. Se --onnx for usado, não informe --ckpt.") + parser.add_argument("--onnx", default=None, help="Modelo ONNX alternativo ao .pt para auditoria acelerada.") + parser.add_argument( + "--onnx_provider", + default="tensorrt", + choices=["tensorrt", "cuda", "cpu"], + help="Provider do ONNX Runtime. Default: tensorrt.", + ) + parser.add_argument( + "--onnx_norm", + default="auto", + choices=["auto", "external", "embedded"], + help="auto usa .export_meta.json; external aplica norm_stats fora; embedded assume normalização dentro do ONNX.", + ) + parser.add_argument("--trt_home", default=None, help="Pasta do TensorRT. Opcional; usa TRT_HOME/default do test script.") + parser.add_argument("--trt_no_fp16", action="store_true", help="Desliga FP16 no TensorRT.") + parser.add_argument("--onnx_warmup", type=int, default=3, help="Warmups antes de medir auditoria ONNX/TRT. Default=3.") + parser.add_argument("--norm_stats", default=None) + parser.add_argument("--channels", type=int, default=None) + parser.add_argument("--resize_w", type=int, default=None) + parser.add_argument("--resize_h", type=int, default=None) + parser.add_argument("--ignore_id", type=int, default=None) + parser.add_argument("--no_amp", action="store_true") + parser.add_argument("--target_threshold", default="auto", help="Número ou auto") + parser.add_argument("--test_script", default=None, help="Default: _9_test_multihead_v2.py ao lado deste script") + + parser.add_argument("--out_root", default="dataset/revisao") + parser.add_argument("--run_name", default=None, help="Nome do relatório. Default: stem do checkpoint") + parser.add_argument("--report_only", action="store_true", help="Não copia casos para dataset/revisao/group") + parser.add_argument( + "--export_mode", + choices=["flagged", "score", "all"], + default="flagged", + help="flagged=critérios técnicos; score=score >= min_suspicion_pct; all=todas.", + ) + parser.add_argument( + "--min_suspicion_pct", + type=float, + default=80.0, + help="Usado com --export_mode score. Escala heurística 0..100, não probabilidade.", + ) + parser.add_argument( + "--clear_review", + action="store_true", + help="Limpa previews/masks/predictions/panels e PRESERVA final_masks.", + ) + parser.add_argument( + "--clear_review_all", + action="store_true", + help="PERIGOSO: apaga toda a árvore group, inclusive final_masks.", + ) + parser.add_argument("--save_panels", action="store_true", help="Salva painel preview/GT/pred/diff dos candidatos") + + # Critérios de revisão. São deliberadamente conservadores. + parser.add_argument("--high_confidence", type=float, default=0.85) + parser.add_argument("--uncertainty_margin", type=float, default=0.12) + parser.add_argument("--min_disagree_frac", type=float, default=0.05, + help="Flag se >=5%% dos pixels semantic discordarem do GT") + parser.add_argument("--min_highconf_disagree_frac", type=float, default=0.002, + help="Flag se >=0.2%% dos pixels divergirem com alta confiança") + parser.add_argument("--min_cross_frac", type=float, default=0.001, + help="Flag para cana<->erva de alta confiança") + parser.add_argument("--min_target_disagree_frac", type=float, default=0.05) + parser.add_argument("--min_conflict_pixels", type=int, default=200) + parser.add_argument("--min_review_score", type=float, default=0.0, + help="Opcional: também seleciona por score de ranking. 0 desliga.") + parser.add_argument("--top_k_per_group", type=int, default=0, + help="Se >0, ignora flags como filtro final e pega os K scores maiores de cada grupo") + parser.add_argument("--max_review_per_group", type=int, default=0, + help="Cap dos candidatos por grupo após filtros. 0=sem limite") + parser.add_argument("--max_samples", type=int, default=0, help="Debug. 0=todos") + args = parser.parse_args() + + if not (0.0 <= float(args.min_suspicion_pct) <= 100.0): + parser.error("--min_suspicion_pct deve ficar entre 0 e 100.") + if args.clear_review and args.clear_review_all: + parser.error("Use apenas um entre --clear_review e --clear_review_all.") + if args.ckpt and args.onnx: + parser.error("Use apenas um backend por execução: --ckpt OU --onnx.") + if args.onnx_warmup < 0: + parser.error("--onnx_warmup deve ser >= 0.") + + script_here = Path(__file__).resolve() + test_script = resolve_path(args.test_script, Path.cwd()) if args.test_script else script_here.with_name("_9_test_multihead_v2.py") + base = load_test_module(test_script) + + config_path = resolve_path(args.config, Path.cwd()) + if config_path is None or not config_path.is_file(): + raise FileNotFoundError(f"Config não encontrado: {config_path}") + config_dir = config_path.parent + config = load_json(config_path) + config["runtime_mode"] = "all" + + input_channel_names = base.get_input_channel_names(config, args.channels) + channels = len(input_channel_names) + + res = config.get("resolucao", [960, 600]) + default_w, default_h = int(res[0]), int(res[1]) + target_w = int(args.resize_w or default_w) + target_h = int(args.resize_h or default_h) + + dataset_path = config_dir / "dataset" + labelmap_path = dataset_path / "labelmap.txt" + semantic_id2label, semantic_label2id, labelmap_ignore_id, loaded_cmap = base.load_labelmap(labelmap_path) + ignore_id = int(args.ignore_id if args.ignore_id is not None else labelmap_ignore_id) + + heads_config = base.build_heads_config(config, ignore_index=ignore_id) + heads_config["semantic"]["num_classes"] = int(len(semantic_id2label)) + heads_config["semantic"]["ignore_index"] = ignore_id + + semantic_cmap = dict(base.SEMANTIC_COLORS_RGB) + semantic_cmap.update({int(k): tuple(map(int, v)) for k, v in loaded_cmap.items()}) + + save_dir = base.infer_save_dir(config, config_dir, channels) + + backend = "onnx" if args.onnx else "pytorch" + ckpt_path: Optional[Path] = None + onnx_path: Optional[Path] = None + onnx_meta: dict = {} + onnx_meta_path: Optional[Path] = None + + if backend == "onnx": + onnx_path = resolve_path(args.onnx, Path.cwd()) + if onnx_path is None or not onnx_path.is_file(): + raise FileNotFoundError(f"ONNX não encontrado: {onnx_path}") + onnx_meta, onnx_meta_path = load_onnx_export_meta(onnx_path) + model_path = onnx_path + else: + ckpt_path = base.find_checkpoint(save_dir, args.ckpt) + model_path = ckpt_path + + if args.norm_stats: + norm_stats_path = resolve_path(args.norm_stats, Path.cwd()) + else: + candidates = [ + save_dir / "norm_stats.json", + dataset_path / f"{default_w}x{default_h}" / "group" / "norm_stats.json", + ] + norm_stats_path = next((p for p in candidates if p.is_file()), candidates[0]) + + mean, std = base.load_norm_stats(norm_stats_path, channel_names=input_channel_names) + if mean is None or std is None: + raise FileNotFoundError( + f"norm_stats obrigatório para auditoria comparável ao treino: {norm_stats_path}" + ) + + root = resolve_path(args.root, Path.cwd()) if args.root else (dataset_path / f"{default_w}x{default_h}" / "group").resolve() + if root is None or not root.is_dir(): + raise FileNotFoundError(f"Root não encontrado: {root}") + + samples, has_gt = base.collect_samples(root, heads_config=heads_config, require_masks=True) + if not has_gt: + raise RuntimeError("Dataset não possui GT. Este script precisa das masks para medir conflito.") + if args.max_samples > 0: + samples = samples[: args.max_samples] + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + threshold_from_model: Optional[float] = None + threshold_source = "none" + onnx_norm_embedded = False + + if backend == "onnx": + if not hasattr(base, "OnnxMultiHeadTester"): + raise RuntimeError( + "Seu _9_test_multihead_v2.py não possui OnnxMultiHeadTester. " + "Use a versão atualizada do script de teste." + ) + + onnx_norm_embedded = onnx_normalization_embedded(onnx_meta, args.onnx_norm) + if args.onnx_norm == "auto" and not onnx_meta: + print( + "[ONNX][WARN] .export_meta.json não encontrado. " + "Assumindo normalização EXTERNA via norm_stats. " + "Use --onnx_norm embedded se o grafo já incluir normalização." + ) + + tester = base.OnnxMultiHeadTester( + config=config, + onnx_path=onnx_path, + provider=args.onnx_provider, + input_channel_names=input_channel_names, + heads_config=heads_config, + mean=None if onnx_norm_embedded else mean, + std=None if onnx_norm_embedded else std, + trt_home=args.trt_home, + trt_fp16=not args.trt_no_fp16, + ) + validate_onnx_for_audit(tester, onnx_meta, onnx_path) + threshold_from_model, threshold_source = resolve_onnx_operational_threshold( + onnx_meta, onnx_meta_path, onnx_path + ) + else: + tester = base.MultiHeadTester( + config=config, + ckpt_path=ckpt_path, + device=device, + input_channel_names=input_channel_names, + heads_config=heads_config, + semantic_id2label=semantic_id2label, + semantic_label2id=semantic_label2id, + mean=mean, + std=std, + use_amp=not args.no_amp, + ) + threshold_from_model = getattr(tester, "ckpt_operational_threshold", None) + threshold_source = "checkpoint" if threshold_from_model is not None else "none" + + threshold_arg = str(args.target_threshold).strip().lower() + if threshold_arg == "auto": + if threshold_from_model is not None: + tester.target_threshold = float(threshold_from_model) + print(f"[TARGET] auto -> {tester.target_threshold:.3f} ({threshold_source})") + else: + tester.target_threshold = 0.5 + print("[TARGET][WARN] threshold operacional não encontrado; usando 0.5") + else: + tester.target_threshold = float(threshold_arg) + threshold_source = "cli" + if not 0.0 <= tester.target_threshold <= 1.0: + raise RuntimeError("--target_threshold deve ficar entre 0 e 1") + + module_params_config = config.get("module_params_json") + if module_params_config: + p = Path(str(module_params_config)) + if not p.is_absolute(): + p = config_dir / p + module_params_config = str(p.resolve()) + + out_root = resolve_path(args.out_root, Path.cwd()) + if out_root is None: + raise RuntimeError("out_root inválido") + review_group_root = out_root / "group" + run_name = args.run_name or (model_path.stem + (f"_{args.onnx_provider}" if backend == "onnx" else "")) + report_dir = out_root / "reports" / run_name + ensure_dir(report_dir) + + if not args.report_only: + if args.clear_review_all: + print(f"[WARN] Limpando revisão COMPLETA, inclusive final_masks: {review_group_root}") + clear_dir(review_group_root) + elif args.clear_review: + print(f"[INFO] Limpando artefatos regeneráveis e PRESERVANDO final_masks: {review_group_root}") + clear_generated_review(review_group_root) + + # Confusion matrices globais. + cms_total = { + name: np.zeros((int(hcfg["num_classes"]), int(hcfg["num_classes"])), dtype=np.int64) + for name, hcfg in heads_config.items() + } + + rows: List[dict] = [] + cache_for_copy: Dict[Tuple[str, str], tuple] = {} + + n = len(samples) + print("=" * 72) + print("DATASET REVIEW / MODEL MINING") + print(f"Root : {root}") + print(f"Backend : {backend}") + print(f"Model : {model_path}") + if backend == "onnx": + print(f"Provider : {args.onnx_provider}") + print(f"Norm ONNX : {'embedded' if onnx_norm_embedded else 'external'}") + print(f"Export meta : {onnx_meta_path}") + print(f"Outputs : {getattr(tester, 'output_names', [])}") + print(f"Samples : {n}") + print(f"Resolution : {target_w}x{target_h}") + print(f"Threshold : {tester.target_threshold:.3f}") + print(f"High conf : {args.high_confidence:.3f}") + print(f"Report only : {args.report_only}") + print(f"Export mode : {args.export_mode}") + if args.export_mode == "score": + print(f"Suspicion >=: {args.min_suspicion_pct:.1f}/100") + print("=" * 72) + + warmup_seconds = 0.0 + if backend == "onnx" and args.onnx_warmup > 0 and samples: + print(f"[ONNX] warmup={args.onnx_warmup} (fora das métricas de tempo)") + warm_sample = samples[0] + warm_chw = base.load_sample_tensor( + warm_sample, + requested_channels=input_channel_names, + derived_config=config.get("derived_channels", {}) or {}, + target_size=(target_w, target_h), + module_params_path=module_params_config, + ) + if (warm_chw.shape[2], warm_chw.shape[1]) != (target_w, target_h): + hwc = np.transpose(warm_chw, (1, 2, 0)) + hwc = cv2.resize(hwc, (target_w, target_h), interpolation=cv2.INTER_LINEAR) + warm_chw = np.transpose(hwc, (2, 0, 1)).astype(np.float32) + tw = time.perf_counter() + for _ in range(int(args.onnx_warmup)): + tester.infer(warm_chw) + warmup_seconds = time.perf_counter() - tw + print(f"[ONNX] warmup concluído em {warmup_seconds:.2f}s") + + t0_all = time.perf_counter() + infer_ms_sum = 0.0 + infer_total_ms_sum = 0.0 + + for i, sample in enumerate(samples, 1): + chw = base.load_sample_tensor( + sample, + requested_channels=input_channel_names, + derived_config=config.get("derived_channels", {}) or {}, + target_size=(target_w, target_h), + module_params_path=module_params_config, + ) + + if (chw.shape[2], chw.shape[1]) != (target_w, target_h): + hwc = np.transpose(chw, (1, 2, 0)) + hwc = cv2.resize(hwc, (target_w, target_h), interpolation=cv2.INTER_LINEAR) + chw = np.transpose(hwc, (2, 0, 1)).astype(np.float32) + + t_infer_wall0 = time.perf_counter() + preds, probs, t_inf = tester.infer(chw) + t_inf_total = (time.perf_counter() - t_infer_wall0) * 1000.0 + infer_ms_sum += t_inf + infer_total_ms_sum += t_inf_total + + gt_masks = {name: base.load_mask(path) for name, path in sample.masks.items()} + pred_sem = preds.get("semantic") + prob_sem = probs.get("semantic") + gt_sem = gt_masks.get("semantic") + if pred_sem is None or prob_sem is None or gt_sem is None: + raise RuntimeError(f"Amostra sem semantic necessário: {sample.base}") + + gt_sem = resize_ids(gt_sem, pred_sem.shape[:2]) + gt_masks["semantic"] = gt_sem + + for head_name, pred in preds.items(): + gt = gt_masks.get(head_name) + if gt is None: + continue + gt = resize_ids(gt, pred.shape[:2]) + gt_masks[head_name] = gt + cms_total[head_name] += base.confusion_matrix_np( + pred, + gt, + int(heads_config[head_name]["num_classes"]), + ignore_id, + ) + + # GT target derivado, exatamente como no teste/treino. + gt_target = gt_masks.get("target") + if gt_target is None and gt_masks.get("vegetation") is not None and gt_masks.get("cana") is not None: + gt_v = resize_ids(gt_masks["vegetation"], pred_sem.shape[:2]) + gt_c = resize_ids(gt_masks["cana"], pred_sem.shape[:2]) + gt_target = base.operational_target_mask(gt_v, gt_c, ignore_id=ignore_id) + + pred_target = preds.get("target") + if pred_target is None and preds.get("vegetation") is not None and preds.get("cana") is not None: + pred_target = base.operational_target_mask( + preds["vegetation"], preds["cana"], ignore_id=ignore_id + ) + + target_stats = None + target_disagree_frac = 0.0 + if gt_target is not None and pred_target is not None: + gt_target = resize_ids(gt_target, pred_target.shape[:2]) + target_stats = binary_target_stats(pred_target, gt_target, ignore_id) + valid_t = gt_target != ignore_id + den_t = int(valid_t.sum()) + if den_t > 0: + target_disagree_frac = float(((pred_target != gt_target) & valid_t).sum() / den_t) + if "target" in cms_total: + cms_total["target"] += base.confusion_matrix_np( + pred_target, gt_target, 2, ignore_id + ) + + signals = semantic_sample_signals( + pred=pred_sem, + probs=prob_sem, + gt=gt_sem, + ignore_id=ignore_id, + high_confidence=args.high_confidence, + uncertainty_margin=args.uncertainty_margin, + ) + + cm_sem_sample = base.confusion_matrix_np(pred_sem, gt_sem, len(semantic_id2label), ignore_id) + iou_sem, miou_sem, acc_sem = base.metrics_from_cm(cm_sem_sample) + + score = review_score(signals, target_disagree_frac) + + row = { + "index": i - 1, + "group": sample.group, + "base": sample.base, + "tensor_path": str(sample.tensor_path or ""), + "preview_path": str(sample.preview_path or ""), + "semantic_mask_path": str(sample.masks.get("semantic") or ""), + "inference_backend": backend, + "inference_provider": args.onnx_provider if backend == "onnx" else str(device), + "inference_ms": float(t_inf), + "inference_total_ms": float(t_inf_total), + "target_threshold": float(tester.target_threshold), + "review_score": score, + "suspicion_pct": float(score * 100.0), + "semantic_miou": float(miou_sem), + "semantic_acc": float(acc_sem), + "iou_chao": float(iou_sem[0]) if len(iou_sem) > 0 else 0.0, + "iou_cana": float(iou_sem[1]) if len(iou_sem) > 1 else 0.0, + "iou_erva": float(iou_sem[2]) if len(iou_sem) > 2 else 0.0, + **signals, + "target_disagree_frac": float(target_disagree_frac), + "target_iou": float(target_stats["iou"]) if target_stats else 0.0, + "target_f1": float(target_stats["f1"]) if target_stats else 0.0, + "target_precision": float(target_stats["precision"]) if target_stats else 0.0, + "target_recall": float(target_stats["recall"]) if target_stats else 0.0, + } + + reasons = build_reasons( + row, + min_disagree_frac=args.min_disagree_frac, + min_highconf_disagree_frac=args.min_highconf_disagree_frac, + min_cross_frac=args.min_cross_frac, + min_target_disagree_frac=args.min_target_disagree_frac, + min_conflict_pixels=args.min_conflict_pixels, + ) + if args.min_review_score > 0 and score >= args.min_review_score: + reasons.append("REVIEW_SCORE") + row["review_reasons"] = ";".join(dict.fromkeys(reasons)) + row["flagged"] = int(bool(reasons)) + rows.append(row) + + # Cache somente do que já pode ser exportado. Em --export_mode all, + # não guardamos o dataset inteiro em RAM; a cópia refaz a inferência. + cache_now = False + if not args.report_only and args.top_k_per_group <= 0: + if args.export_mode == "flagged": + cache_now = bool(reasons) + elif args.export_mode == "score": + cache_now = (score * 100.0) >= float(args.min_suspicion_pct) + + if cache_now: + cache_for_copy[(sample.group, sample.base)] = (sample, chw, gt_sem, pred_sem) + + if i % 50 == 0 or i == n: + flagged_now = sum(int(r["flagged"]) for r in rows) + print( + f"[{i:5d}/{n}] flagged={flagged_now:4d} " + f"avg_inf={infer_ms_sum / i:.1f}ms " + f"avg_infer_total={infer_total_ms_sum / i:.1f}ms " + f"last_score={score:.3f} {sample.group}/{sample.base}" + ) + + elapsed = time.perf_counter() - t0_all + + # ======================================================== + # Seleção final para revisão física + # ======================================================== + by_group: Dict[str, List[dict]] = defaultdict(list) + for row in rows: + by_group[str(row["group"])].append(row) + + selected_rows: List[dict] = [] + for group, grows in sorted(by_group.items()): + ordered = sorted(grows, key=lambda r: float(r["review_score"]), reverse=True) + + if args.top_k_per_group > 0: + # top_k tem precedência sobre export_mode. + chosen = ordered[: args.top_k_per_group] + for r in chosen: + if not r["review_reasons"]: + r["review_reasons"] = "TOP_K" + r["flagged"] = 1 + elif args.export_mode == "all": + chosen = ordered + for r in chosen: + if not r["review_reasons"]: + r["review_reasons"] = "EXPORT_ALL" + elif args.export_mode == "score": + threshold_score = float(args.min_suspicion_pct) / 100.0 + chosen = [r for r in ordered if float(r["review_score"]) >= threshold_score] + for r in chosen: + if not r["review_reasons"]: + r["review_reasons"] = "SUSPICION_SCORE" + else: + chosen = [r for r in ordered if int(r["flagged"]) == 1] + + if args.max_review_per_group > 0: + chosen = chosen[: args.max_review_per_group] + + for rank, r in enumerate(chosen, 1): + r["review_rank_group"] = rank + selected_rows.extend(chosen) + + selected_keys = {(r["group"], r["base"]) for r in selected_rows} + + # Copia apenas depois de finalizar ranking/caps. + if not args.report_only: + print(f"\n[COPY] candidatos finais: {len(selected_rows)}") + sample_by_key = {(s.group, s.base): s for s in samples} + + for j, row in enumerate(selected_rows, 1): + key = (str(row["group"]), str(row["base"])) + sample = sample_by_key[key] + + cached = cache_for_copy.get((sample.group, sample.base)) + if cached is not None and cached[0].group == sample.group: + _, chw, gt_sem, pred_sem = cached + else: + chw = base.load_sample_tensor( + sample, + requested_channels=input_channel_names, + derived_config=config.get("derived_channels", {}) or {}, + target_size=(target_w, target_h), + module_params_path=module_params_config, + ) + if (chw.shape[2], chw.shape[1]) != (target_w, target_h): + hwc = np.transpose(chw, (1, 2, 0)) + hwc = cv2.resize(hwc, (target_w, target_h), interpolation=cv2.INTER_LINEAR) + chw = np.transpose(hwc, (2, 0, 1)).astype(np.float32) + preds, _, _ = tester.infer(chw) + pred_sem = preds["semantic"] + gt_sem = resize_ids(base.load_mask(sample.masks["semantic"]), pred_sem.shape[:2]) + + save_review_item( + row=row, + sample=sample, + chw=chw, + gt_sem=gt_sem, + pred_sem=pred_sem, + review_group_root=review_group_root, + base_module=base, + semantic_cmap=semantic_cmap, + ignore_id=ignore_id, + input_channel_names=input_channel_names, + save_panels=args.save_panels, + ) + + if j % 50 == 0 or j == len(selected_rows): + print(f" copiados {j}/{len(selected_rows)}") + + # Ordem de revisão por grupo. + selected_by_group: Dict[str, List[dict]] = defaultdict(list) + for r in selected_rows: + selected_by_group[str(r["group"])].append(r) + for group, grows in selected_by_group.items(): + order_path = review_group_root / group / "review_order.csv" + write_csv( + order_path, + sorted(grows, key=lambda r: int(r.get("review_rank_group", 999999))), + ) + + # ======================================================== + # Relatórios globais + # ======================================================== + report_path = report_dir / "review_report.csv" + candidates_path = report_dir / "review_candidates.csv" + write_csv(report_path, rows) + write_csv(candidates_path, selected_rows) + + semantic_labels = [semantic_id2label[i] for i in sorted(semantic_id2label)] + head_labels = { + "semantic": semantic_labels, + "vegetation": ["background", "vegetation"], + "cana": ["not_cana", "cana"], + "target": ["background", "target"], + } + + metrics_global = {} + for head_name, cm in cms_total.items(): + if cm.sum() <= 0: + continue + labels = head_labels.get(head_name, [str(i) for i in range(cm.shape[0])]) + metrics_global[head_name] = class_metrics_from_cm(cm, labels) + write_cm_csv(report_dir / f"confusion_{head_name}.csv", cm, labels) + + cm_sem = cms_total.get("semantic") + directional = {} + if cm_sem is not None and cm_sem.shape[0] >= 3 and cm_sem.sum() > 0: + directional = { + "chao_to_cana": directional_rate(cm_sem, 0, 1), + "chao_to_erva": directional_rate(cm_sem, 0, 2), + "cana_to_chao": directional_rate(cm_sem, 1, 0), + "cana_to_erva": directional_rate(cm_sem, 1, 2), + "erva_to_chao": directional_rate(cm_sem, 2, 0), + "erva_to_cana": directional_rate(cm_sem, 2, 1), + } + + # Estatísticas por grupo. + group_summary = {} + selected_set = {(str(r["group"]), str(r["base"])) for r in selected_rows} + for group, grows in sorted(by_group.items()): + scores = np.array([float(r["review_score"]) for r in grows], dtype=np.float64) + sem_dis = np.array([float(r["semantic_disagree_frac"]) for r in grows], dtype=np.float64) + hc_dis = np.array([float(r["highconf_disagree_frac"]) for r in grows], dtype=np.float64) + tgt_dis = np.array([float(r["target_disagree_frac"]) for r in grows], dtype=np.float64) + selected_n = sum((group, str(r["base"])) in selected_set for r in grows) + group_summary[group] = { + "samples": len(grows), + "selected_for_review": int(selected_n), + "selected_pct": float(100.0 * selected_n / max(len(grows), 1)), + "review_score_mean": float(scores.mean()) if len(scores) else 0.0, + "review_score_p95": float(np.percentile(scores, 95)) if len(scores) else 0.0, + "semantic_disagree_mean": float(sem_dis.mean()) if len(sem_dis) else 0.0, + "highconf_disagree_mean": float(hc_dis.mean()) if len(hc_dis) else 0.0, + "target_disagree_mean": float(tgt_dis.mean()) if len(tgt_dis) else 0.0, + } + + all_scores = np.array([float(r["review_score"]) for r in rows], dtype=np.float64) + all_sem_dis = np.array([float(r["semantic_disagree_frac"]) for r in rows], dtype=np.float64) + all_hc = np.array([float(r["highconf_disagree_frac"]) for r in rows], dtype=np.float64) + all_unc = np.array([float(r["uncertain_frac"]) for r in rows], dtype=np.float64) + + correct_pixels_total = sum(max(int(r.get("valid_pixels", 0)) - int(r.get("wrong_pixels", 0)), 0) for r in rows) + wrong_pixels_total = sum(int(r.get("wrong_pixels", 0)) for r in rows) + mean_conf_correct_global = ( + sum(float(r.get("mean_conf_correct", 0.0)) * max(int(r.get("valid_pixels", 0)) - int(r.get("wrong_pixels", 0)), 0) for r in rows) + / max(correct_pixels_total, 1) + ) + mean_conf_wrong_global = ( + sum(float(r.get("mean_conf_wrong", 0.0)) * int(r.get("wrong_pixels", 0)) for r in rows) + / max(wrong_pixels_total, 1) + ) + + gt_chao_total = sum(int(r.get("gt_chao_pixels", 0)) for r in rows) + gt_cana_total = sum(int(r.get("gt_cana_pixels", 0)) for r in rows) + gt_erva_total = sum(int(r.get("gt_erva_pixels", 0)) for r in rows) + highconf_directional = { + "chao_to_cana": sum(int(r.get("hc_gt_chao_pred_cana_pixels", 0)) for r in rows) / max(gt_chao_total, 1), + "chao_to_erva": sum(int(r.get("hc_gt_chao_pred_erva_pixels", 0)) for r in rows) / max(gt_chao_total, 1), + "cana_to_erva": sum(int(r.get("hc_gt_cana_pred_erva_pixels", 0)) for r in rows) / max(gt_cana_total, 1), + "erva_to_cana": sum(int(r.get("hc_gt_erva_pred_cana_pixels", 0)) for r in rows) / max(gt_erva_total, 1), + } + + summary = { + "schema": "oak_fcc3_dataset_review_v1", + "note": ( + "Model-vs-GT disagreement is a review signal, not automatic proof of model error. " + "High-confidence disagreement can indicate label noise or a systematic model mistake." + ), + "run_name": run_name, + "config": str(config_path), + "root": str(root), + "backend": backend, + "model_path": str(model_path), + "checkpoint": str(ckpt_path) if ckpt_path is not None else None, + "onnx": str(onnx_path) if onnx_path is not None else None, + "onnx_provider": args.onnx_provider if backend == "onnx" else None, + "onnx_active_providers": list(getattr(getattr(tester, "session", None), "get_providers", lambda: [])()) if backend == "onnx" else None, + "onnx_export_meta": str(onnx_meta_path) if onnx_meta_path is not None else None, + "onnx_normalization_embedded": bool(onnx_norm_embedded) if backend == "onnx" else None, + "target_threshold_source": threshold_source, + "norm_stats": str(norm_stats_path), + "resolution": [target_w, target_h], + "input_channels": list(input_channel_names), + "target_threshold": float(tester.target_threshold), + "samples": len(rows), + "selected_for_review": len(selected_rows), + "selected_pct": float(100.0 * len(selected_rows) / max(len(rows), 1)), + "export": { + "mode": str(args.export_mode), + "min_suspicion_pct": float(args.min_suspicion_pct), + "report_only": bool(args.report_only), + "top_k_per_group": int(args.top_k_per_group), + "max_review_per_group": int(args.max_review_per_group), + "png_only": True, + }, + "elapsed_seconds": float(elapsed), + "warmup_seconds_excluded": float(warmup_seconds), + "mean_inference_ms": float(infer_ms_sum / max(len(rows), 1)), + "mean_inference_total_ms": float(infer_total_ms_sum / max(len(rows), 1)), + "inference_timing_scope": ( + "onnx_session_run" if backend == "onnx" else "pytorch_tester_reported" + ), + "review_thresholds": { + "high_confidence": args.high_confidence, + "uncertainty_margin": args.uncertainty_margin, + "min_disagree_frac": args.min_disagree_frac, + "min_highconf_disagree_frac": args.min_highconf_disagree_frac, + "min_cross_frac": args.min_cross_frac, + "min_target_disagree_frac": args.min_target_disagree_frac, + "min_conflict_pixels": args.min_conflict_pixels, + "min_review_score": args.min_review_score, + "top_k_per_group": args.top_k_per_group, + "max_review_per_group": args.max_review_per_group, + }, + "review_score": { + "mean": float(all_scores.mean()) if len(all_scores) else 0.0, + "median": float(np.median(all_scores)) if len(all_scores) else 0.0, + "p95": float(np.percentile(all_scores, 95)) if len(all_scores) else 0.0, + "max": float(all_scores.max()) if len(all_scores) else 0.0, + }, + "annotation_health_signals": { + "semantic_disagree_mean": float(all_sem_dis.mean()) if len(all_sem_dis) else 0.0, + "semantic_disagree_p95": float(np.percentile(all_sem_dis, 95)) if len(all_sem_dis) else 0.0, + "highconf_disagree_mean": float(all_hc.mean()) if len(all_hc) else 0.0, + "highconf_disagree_p95": float(np.percentile(all_hc, 95)) if len(all_hc) else 0.0, + "uncertain_mean": float(all_unc.mean()) if len(all_unc) else 0.0, + "mean_model_confidence_on_gt_agreement": float(mean_conf_correct_global), + "mean_model_confidence_on_gt_disagreement": float(mean_conf_wrong_global), + }, + "directional_semantic_confusion": directional, + "high_confidence_directional_conflict": highconf_directional, + "metrics_global": metrics_global, + "groups": group_summary, + "paths": { + "review_report": str(report_path), + "review_candidates": str(candidates_path), + "review_group_root": str(review_group_root), + }, + } + + summary_path = report_dir / "review_summary.json" + with summary_path.open("w", encoding="utf-8") as f: + json.dump(summary, f, ensure_ascii=False, indent=2) + + # README curto para não esquecer o significado do score. + readme = report_dir / "README_REVIEW.txt" + readme.write_text( + "\n".join([ + "DATASET REVIEW / MODEL MINING", + "", + "IMPORTANTE:", + "- Discordancia modelo x GT nao prova que o modelo errou.", + "- Conflito de alta confianca e um candidato forte para revisao humana.", + "- review_score serve apenas para RANKING de suspeitos.", + "- suspicion_pct = review_score*100 e NAO e probabilidade de mascara errada.", + f"- export_mode={args.export_mode} min_suspicion_pct={args.min_suspicion_pct:.1f}", + "- Exportacao fisica usa somente PNG.", + "- --clear_review PRESERVA final_masks; --clear_review_all apaga tudo.", + "- Para comparar checkpoints, use --report_only e run_name diferentes.", + "", + f"Backend: {backend}", + f"Model: {model_path}", + f"Provider: {args.onnx_provider if backend == 'onnx' else device}", + f"Root: {root}", + f"Samples: {len(rows)}", + f"Selecionados: {len(selected_rows)}", + ]), + encoding="utf-8", + ) + + print("\n" + "=" * 72) + print("REVISÃO CONCLUÍDA") + print(f"Samples : {len(rows)}") + print(f"Selecionados p/ revisão : {len(selected_rows)} ({100.0 * len(selected_rows) / max(len(rows), 1):.2f}%)") + print(f"Export mode : {args.export_mode}") + if args.export_mode == "score": + print(f"Suspicion mínima : {args.min_suspicion_pct:.1f}/100") + print(f"Tempo total : {elapsed:.1f}s") + print(f"Backend : {backend}") + print(f"Inferência backend média : {infer_ms_sum / max(len(rows), 1):.1f}ms") + print(f"Inferência total média : {infer_total_ms_sum / max(len(rows), 1):.1f}ms") + if backend == "onnx": + print(f"ONNX provider : {args.onnx_provider}") + print(f"Warmup excluído : {warmup_seconds:.2f}s") + print(f"Report : {report_path}") + print(f"Candidates : {candidates_path}") + print(f"Summary : {summary_path}") + if not args.report_only: + print(f"Revisão física : {review_group_root}") + + if directional: + print("\nConfusão semântica direcional global:") + print(f" chão -> cana : {100.0 * directional['chao_to_cana']:.3f}%") + print(f" chão -> erva : {100.0 * directional['chao_to_erva']:.3f}%") + print(f" cana -> chão : {100.0 * directional['cana_to_chao']:.3f}%") + print(f" cana -> erva : {100.0 * directional['cana_to_erva']:.3f}%") + print(f" erva -> chão : {100.0 * directional['erva_to_chao']:.3f}%") + print(f" erva -> cana : {100.0 * directional['erva_to_cana']:.3f}%") + + sem_global = metrics_global.get("semantic") + if sem_global: + print(f"\nSemantic global: acc={sem_global['accuracy']:.4f} mIoU={sem_global['miou']:.4f}") + target_global = metrics_global.get("target") + if target_global: + print(f"Target global : acc={target_global['accuracy']:.4f} mIoU={target_global['miou']:.4f}") + + print("=" * 72) + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/_9_test_multihead.py b/Python/OAK/datasets/oak-fcc-3/_9_test_multihead.py index 4ad0a6fc3..8927b1bc7 100644 --- a/Python/OAK/datasets/oak-fcc-3/_9_test_multihead.py +++ b/Python/OAK/datasets/oak-fcc-3/_9_test_multihead.py @@ -25,10 +25,7 @@ Mostra: Exemplo: -python .\\_9_test_infer_multihead.py ^ - --config config.json ^ - --split_folder val ^ - --ckpt backup\segformer_b1\test_multi\stacked_raw5_multihead\best_score.pt +python .\\_9_test_multihead.py --config config.json --split_folder val --ckpt backup\segformer_b1\test_multi\stacked_raw5_multihead\best_score.pt Controles: D / seta direita : próxima amostra @@ -634,7 +631,20 @@ def collect_samples(root: Path, heads_config: Dict[str, dict], require_masks: bo missing = [] for head_name, hcfg in heads_config.items(): - mask_dir = group_dir / str(hcfg.get("mask_dir", "masks")) + mask_dir_name = str(hcfg.get("mask_dir", "masks")) + is_derived = ( + bool(hcfg.get("derived", False)) + or mask_dir_name == "__derived_target__" + ) + + # Heads derivadas não possuem máscara física. + # O GT target será construído posteriormente a partir de: + # vegetation == 1 AND cana == 0. + if is_derived: + masks[head_name] = None + continue + + mask_dir = group_dir / mask_dir_name mask_path = mask_dir / f"{base}.npy" if mask_path.exists(): @@ -2002,14 +2012,25 @@ def main(): sample_metrics[head_name] = {"iou": iou, "miou": miou, "acc": acc} metric_lines.append(f"{head_name}: mIoU={miou:.3f} acc={acc:.3f}") - if gt_target is not None: - cm_t = confusion_matrix_np(pred_target, gt_target, 2, ignore_id) + if gt_target is not None and pred_target_op is not None: + cm_t = confusion_matrix_np(pred_target_op, gt_target, 2, ignore_id) iou_t, miou_t, acc_t = metrics_from_cm(cm_t) - sample_metrics["target_op"] = {"iou": iou_t, "miou": miou_t, "acc": acc_t} - metric_lines.append(f"target_op: IoU_alvo={iou_t[1]:.3f} acc={acc_t:.3f}") + + sample_metrics["target_op"] = { + "iou": iou_t, + "miou": miou_t, + "acc": acc_t, + } + + metric_lines.append( + f"target_op: IoU_alvo={iou_t[1]:.3f} acc={acc_t:.3f}" + ) + if "target" in sample_metrics: iou_head = sample_metrics["target"]["iou"] - metric_lines.append(f"target_head: IoU_alvo={iou_head[1]:.3f}") + metric_lines.append( + f"target_head: IoU_alvo={iou_head[1]:.3f}" + ) if idx not in visited: for head_name, pred in preds.items(): diff --git a/Python/OAK/datasets/oak-fcc-3/_9_test_multihead_v2.py b/Python/OAK/datasets/oak-fcc-3/_9_test_multihead_v2.py new file mode 100644 index 000000000..2f1a526d5 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/_9_test_multihead_v2.py @@ -0,0 +1,2406 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +r""" +_9_test_multihead_v2_1.py + +Teste/visualização do SegFormer OAK-FCC-3 Multi-Head. + +Contrato esperado após normalize + split: + + dataset/split/val/group// + tensors/.npy # CHW float32 conforme meta/config + masks/.npy # semantic: 0=chao, 1=cana, 2=erva, 255=ignore + masks_vegetation/.npy # vegetation: 0=background, 1=vegetation, 255=ignore + masks_cana/.npy # cana: 0=not_cana, 1=cana, 255=ignore + metas/.json + previews/.png + +Mostra: + - RGB preview do tensor + - GT/pred/overlay semantic + - GT/pred/overlay vegetation + - GT/pred/overlay cana + - alvo operacional = vegetation == 1 AND cana == 0 + - mapas de confiança das heads binárias + +Exemplo: + +python .\\_9_test_multihead.py --config config.json --split_folder val --ckpt backup\segformer_b1\test_multi\stacked_raw5_multihead\best_score.pt + +Controles: + D / seta direita : próxima amostra + A / seta esquerda: amostra anterior + S : salvar painel atual em --out_dir + SPACE : alterna modo compacto/detalhado + Q / ESC : sair +""" + +from __future__ import annotations + +import argparse +import copy +import json +import time +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Tuple + +import cv2 +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers import SegformerForSemanticSegmentation + +from core.raw_processor_core import RawProcessorCore + + +# ============================================================ +# Config default +# ============================================================ + +DEFAULT_HEADS = { + "semantic": { + "enabled": True, + "type": "multiclass", + "num_classes": 3, + "mask_dir": "masks", + "classes": {"chao": 0, "cana": 1, "erva": 2}, + "ignore_index": 255, + }, + "vegetation": { + "enabled": True, + "type": "binary", + "num_classes": 2, + "mask_dir": "masks_vegetation", + "classes": {"background": 0, "vegetation": 1}, + "ignore_index": 255, + }, + "cana": { + "enabled": True, + "type": "binary", + "num_classes": 2, + "mask_dir": "masks_cana", + "classes": {"not_cana": 0, "cana": 1}, + "ignore_index": 255, + }, + "target": { + "enabled": True, + "type": "binary", + "num_classes": 2, + "mask_dir": "__derived_target__", + "classes": { + "background": 0, + "target": 1, + }, + "ignore_index": 255, + "derived_from": ["vegetation", "cana"], + }, +} + +SEMANTIC_COLORS_RGB = { + 0: (85, 85, 85), # chao + 1: (0, 190, 0), # cana + 2: (230, 55, 55), # erva +} + +BINARY_COLORS_RGB = { + 0: (30, 30, 30), + 1: (0, 220, 80), +} + +CANA_COLORS_RGB = { + 0: (30, 30, 30), + 1: (40, 210, 255), +} + +TARGET_COLORS_RGB = { + 0: (30, 30, 30), + 1: (255, 70, 30), +} + + +@dataclass +class SampleItem: + group: str + base: str + tensor_path: Optional[Path] + masks: Dict[str, Optional[Path]] + meta_path: Optional[Path] = None + preview_path: Optional[Path] = None + + # novo + source_kind: str = "tensor" # "tensor" ou "raw_native_multi" + raw_group: Optional[dict] = None + + +# ============================================================ +# Util +# ============================================================ + +def load_json(path: str | Path) -> dict: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def ensure_dir(path: Path): + path.mkdir(parents=True, exist_ok=True) + + +def resolve_path(path_like: Optional[str], base: Optional[Path] = None) -> Optional[Path]: + if path_like is None: + return None + p = Path(path_like) + if p.is_absolute(): + return p + if base is None: + base = Path.cwd() + return (base / p).resolve() + + +def merge_dict(dst: dict, src: dict) -> dict: + out = copy.deepcopy(dst) + + def rec(a, b): + for k, v in b.items(): + if isinstance(v, dict) and isinstance(a.get(k), dict): + rec(a[k], v) + else: + a[k] = v + + if isinstance(src, dict): + rec(out, src) + return out + + +def build_heads_config(config: dict, ignore_index: int) -> Dict[str, dict]: + cfg = merge_dict(DEFAULT_HEADS, config.get("heads", {}) or {}) + active = {} + for name, hcfg in cfg.items(): + if not bool(hcfg.get("enabled", True)): + continue + hcfg.setdefault("ignore_index", ignore_index) + hcfg["ignore_index"] = int(hcfg.get("ignore_index", ignore_index)) + hcfg["num_classes"] = int(hcfg.get("num_classes", 2)) + hcfg["mask_dir"] = str(hcfg.get("mask_dir", "masks")) + active[name] = hcfg + + for required in ("semantic", "vegetation", "cana"): + if required not in active: + raise RuntimeError(f"Head obrigatória ausente no config: {required}") + + return active + + +def load_labelmap(labelmap_path: Path) -> Tuple[Dict[int, str], Dict[str, int], int, Dict[int, Tuple[int, int, int]]]: + try: + from helpers import carregar_labelmap_completo, _infer_ignore_id + + _cor_para_id, _colormap_rgb, id_para_nome, ignore_rgb = carregar_labelmap_completo(str(labelmap_path)) + ignore_id = int(_infer_ignore_id(ignore_rgb, 255)) + + id2label = { + int(k): str(v) + for k, v in id_para_nome.items() + if str(v).lower() not in ("ignore", "void", "background_ignore") + } + label2id = {v.lower(): k for k, v in id2label.items()} + + colormap_rgb = {} + if isinstance(_colormap_rgb, dict): + for k, v in _colormap_rgb.items(): + ik = int(k) + if ik in id2label: + colormap_rgb[ik] = tuple(map(int, v[:3])) + elif isinstance(_colormap_rgb, (list, tuple)): + for ik, v in enumerate(_colormap_rgb): + if ik in id2label: + colormap_rgb[ik] = tuple(map(int, v[:3])) + + return id2label, label2id, ignore_id, colormap_rgb + + except Exception as e: + print(f"[WARN] Não consegui usar helpers.carregar_labelmap_completo: {e}") + print("[WARN] Usando parser simples do labelmap.") + + id2label: Dict[int, str] = {} + colormap_rgb: Dict[int, Tuple[int, int, int]] = {} + ignore_id = 255 + next_id = 0 + + with labelmap_path.open("r", encoding="utf-8") as f: + for raw_line in f: + s = raw_line.strip() + if not s or s.startswith("#"): + continue + + name = None + color = None + cid = None + + if ":" in s and not s.split(":", 1)[0].strip().isdigit(): + name_part, rest = s.split(":", 1) + name = name_part.strip() + color_txt = rest.split("::", 1)[0].strip().strip(":") + rgb_parts = [p.strip() for p in color_txt.split(",") if p.strip()] + if len(rgb_parts) >= 3: + color = tuple(int(float(p)) for p in rgb_parts[:3]) + else: + parts = s.replace(",", " ").replace(":", " ").split() + if len(parts) >= 2 and parts[0].isdigit(): + cid = int(parts[0]) + name = parts[1] + if len(parts) >= 5: + color = tuple(int(float(p)) for p in parts[2:5]) + elif len(parts) >= 1: + name = parts[0] + + if not name: + continue + + if name.lower() in ("ignore", "void", "background_ignore"): + ignore_id = 255 + continue + + if cid is None: + cid = next_id + next_id = max(next_id, cid + 1) + + id2label[int(cid)] = str(name) + if color is not None: + colormap_rgb[int(cid)] = color + + if not id2label: + id2label = {0: "chao", 1: "cana", 2: "erva"} + + label2id = {v.lower(): k for k, v in id2label.items()} + for cid in id2label: + colormap_rgb.setdefault(cid, SEMANTIC_COLORS_RGB.get(cid, (255, 255, 255))) + + return id2label, label2id, int(ignore_id), colormap_rgb + + +def find_raw_dataset_layout_root(path: Path) -> Optional[Path]: + """ + Detecta layout bruto: + root/metas + root/previews + root/bins + root/masks opcional + + Aceita root, root/metas, root/bins, root/previews ou arquivo dentro deles. + """ + p = path.resolve() + candidates = [] + + if p.is_file(): + candidates.append(p.parent) + candidates.append(p.parent.parent) + else: + candidates.append(p) + candidates.append(p.parent) + + for c in candidates: + if not c: + continue + + if c.name.lower() in ("metas", "metadata", "jsons", "previews", "bins", "masks"): + root = c.parent + else: + root = c + + if (root / "metas").is_dir() and (root / "previews").is_dir() and (root / "bins").is_dir(): + return root + + return None + + +def resolve_raw_sibling_file(root: Path, subdir: str, stem: str, exts: Tuple[str, ...]) -> Optional[Path]: + folder = root / subdir + if not folder.is_dir(): + return None + + for ext in exts: + p = folder / f"{stem}{ext}" + if p.exists(): + return p + + return None + + +def resolve_raw_capture_group_from_json(json_path: Path, dataset_root: Path) -> dict: + """ + Resolve uma captura bruta: + metas/.json + previews/.png + bins/ + masks/ opcional + """ + json_path = json_path.resolve() + meta = load_json(json_path) + base_name = json_path.stem + bins_dir = dataset_root / "bins" + + preview_path = resolve_raw_sibling_file( + dataset_root, + "previews", + base_name, + (".png", ".jpg", ".jpeg"), + ) + + mask_path = resolve_raw_sibling_file( + dataset_root, + "masks", + base_name, + (".npy", ".png", ".tif", ".tiff"), + ) + + group = { + "json": json_path, + "png": preview_path, + "mask": mask_path, + "final_raw": None, + "cameras": {}, + "dataset_root": dataset_root, + } + + if "saved_payload_paths" in meta: + for cam_id, fname in meta["saved_payload_paths"].items(): + fname_path = Path(fname) + + candidates = [ + bins_dir / fname_path.name, + json_path.parent / fname, + dataset_root / fname, + bins_dir / f"{base_name}_{cam_id}.bin", + bins_dir / f"{base_name}_{cam_id}.raw", + bins_dir / f"{base_name}_{cam_id.lower()}.bin", + bins_dir / f"{base_name}_{cam_id.lower()}.raw", + ] + + found = next((c for c in candidates if c.exists()), None) + if found is not None: + group["cameras"][cam_id] = found + else: + print(f"[WARN] bin não encontrado para {cam_id}: {fname}") + + return group + + +def build_multispec_from_raw_native_multi_for_infer( + group: dict, + meta: dict, + target_size: Optional[Tuple[int, int]] = None, + module_params_path: Optional[str] = None, +) -> Tuple[np.ndarray, dict]: + """ + Gera tensor MULTISPEC [R,G,B,RE,NIR] em CHW float32 0..1 + a partir dos bins RAW_BRUTO salvos. + """ + if meta.get("saved_payload_type") != "raw_native_multi": + raise RuntimeError( + f"Captura bruta não suportada aqui: saved_payload_type={meta.get('saved_payload_type')}" + ) + + stream_meta = meta.get("stream_meta", {}) or {} + saved_dtypes = meta.get("saved_payload_dtypes", {}) or {} + saved_shapes = meta.get("saved_payload_shapes", {}) or {} + + frame = {} + + for cam_id, path in group["cameras"].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} no JSON: {group['json']}") + + arr = np.fromfile(str(path), dtype=np.dtype(saved_dtype)).reshape(tuple(saved_shape)) + frame[cam_id] = arr + + if not frame: + raise RuntimeError(f"Nenhum bin de câmera encontrado para: {group['json']}") + + sensor_width = int(meta.get("sensor_width", 1280)) + sensor_height = int(meta.get("sensor_height", 800)) + bayer = meta.get("bayer_pattern", "BGGR") + + json_dir = Path(group["json"]).parent + calib_path = None + for candidate in ( + module_params_path, + meta.get("camera_params_json"), + "calibration/module_params.json", + ): + if not candidate: + continue + p = Path(str(candidate)) + for resolved in (p, Path.cwd() / p, json_dir / p): + if resolved.is_file(): + calib_path = str(resolved.resolve()) + break + if calib_path is not None: + break + + if calib_path is None: + raise RuntimeError( + "module_params.json não encontrado. A inferência RAW deve usar a mesma " + "calibração do normalize; confira config.module_params_json." + ) + + core = RawProcessorCore( + sensor_width=sensor_width, + sensor_height=sensor_height, + bayer_pattern=bayer, + calibration_json_path=calib_path, + ) + + processing_meta = dict(stream_meta) + processing_meta["frame_type"] = "RAW_BRUTO" + + 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") + + tensor = core.build_infer_tensor_from_stream( + frame=frame, + meta=processing_meta, + channels_expected=5, + target_size=target_size, + ) + + if tensor is None: + raise RuntimeError(f"RawProcessorCore retornou tensor None para: {group['json']}") + + tensor = np.asarray(tensor, dtype=np.float32) + tensor = np.nan_to_num(tensor, nan=0.0, posinf=1.0, neginf=0.0) + + processing_info = { + "fusion_result": getattr(core, "last_fusion_result", None), + "fusion_config_used": getattr(core, "fusion_config", None), + "radiometric_normalization_result": getattr(core, "last_radiometric_normalization_result", None), + "patch_normalization_result": getattr(core, "last_patch_normalization_result", None), + "frame_quality": getattr(core, "last_frame_quality_result", None), + } + + return tensor, processing_info + + +SOURCE_CHANNEL_ORDER = ["R", "G", "B", "RE", "NIR"] +DERIVED_CHANNEL_ORDER = ["NDVI", "NDRE"] +SUPPORTED_INPUT_CHANNELS = SOURCE_CHANNEL_ORDER + DERIVED_CHANNEL_ORDER + +def get_input_channel_names(config: dict, channels_override: Optional[int] = None) -> List[str]: + if "input_channels" in config: + configured = config["input_channels"] + if isinstance(configured, str): + names = [str(c).strip().upper() for c in configured.split(",") if str(c).strip()] + else: + names = [str(c).strip().upper() for c in configured if str(c).strip()] + else: + n = int(channels_override or config.get("channels", 5)) + if n < 1 or n > len(SOURCE_CHANNEL_ORDER): + raise RuntimeError( + "Config sem input_channels só suporta channels entre 1 e 5. " + "Para NDVI/NDRE, declare input_channels explicitamente." + ) + names = SOURCE_CHANNEL_ORDER[:n] + + if not names: + raise RuntimeError("input_channels não pode ser vazio.") + + duplicates = sorted({name for name in names if names.count(name) > 1}) + if duplicates: + raise RuntimeError(f"Canais duplicados em input_channels: {duplicates}") + + invalid = [c for c in names if c not in SUPPORTED_INPUT_CHANNELS] + if invalid: + raise RuntimeError( + f"Canais inválidos em input_channels: {invalid}. " + f"Suportados: {SUPPORTED_INPUT_CHANNELS}" + ) + + expected_count = int(channels_override) if channels_override is not None else config.get("channels") + if expected_count is not None and int(expected_count) != len(names): + raise RuntimeError( + "channels incompatível com input_channels: " + f"channels={expected_count}, input_channels={names} ({len(names)} canais)." + ) + + return names + + +def get_input_channel_indices(config: dict, channels_override: Optional[int] = None) -> List[int]: + """Indices do tensor normalizado v2, já salvo na ordem final.""" + return list(range(len(get_input_channel_names(config, channels_override)))) + + +def build_model_input_tensor( + source_tensor: np.ndarray, + requested_channels: Sequence[str], + derived_config: Optional[dict] = None, +) -> np.ndarray: + """Replica exatamente a derivação de canais usada pelo normalize.""" + derived_config = derived_config or {} + eps = float(derived_config.get("epsilon", 1e-6)) + clip_min = float(derived_config.get("clip_min", -1.0)) + clip_max = float(derived_config.get("clip_max", 1.0)) + + if not np.isfinite(eps) or eps <= 0.0: + raise RuntimeError(f"derived_channels.epsilon inválido: {eps}") + if not np.isfinite(clip_min) or not np.isfinite(clip_max) or clip_min >= clip_max: + raise RuntimeError( + f"Faixa derived_channels inválida: clip_min={clip_min}, clip_max={clip_max}" + ) + + source = np.nan_to_num( + np.asarray(source_tensor, dtype=np.float32), + nan=0.0, + posinf=1.0, + neginf=0.0, + ) + if source.ndim != 3 or source.shape[0] != len(SOURCE_CHANNEL_ORDER): + raise RuntimeError( + f"Tensor fonte inválido: esperado {SOURCE_CHANNEL_ORDER}, shape={source.shape}" + ) + + available = { + name: source[index] + for index, name in enumerate(SOURCE_CHANNEL_ORDER) + } + + def normalized_difference(a: np.ndarray, b: np.ndarray) -> np.ndarray: + denominator = a + b + result = np.zeros_like(a, dtype=np.float32) + np.divide( + a - b, + denominator, + out=result, + where=np.abs(denominator) > eps, + ) + return np.clip(result, clip_min, clip_max).astype(np.float32, copy=False) + + if "NDVI" in requested_channels: + available["NDVI"] = normalized_difference(available["NIR"], available["R"]) + if "NDRE" in requested_channels: + available["NDRE"] = normalized_difference(available["NIR"], available["RE"]) + + return np.ascontiguousarray( + np.stack([available[str(name).upper()] for name in requested_channels], axis=0), + dtype=np.float32, + ) + + +# ============================================================ +# Dataset +# ============================================================ + +def collect_samples(root: Path, heads_config: Dict[str, dict], require_masks: bool = False) -> Tuple[List[SampleItem], bool]: + tensor_paths: List[Path] = [] + + direct = root / "tensors" + if direct.is_dir(): + tensor_paths.extend(sorted(direct.glob("*.npy"))) + + group_root = root / "group" + if group_root.is_dir(): + for gdir in sorted(group_root.iterdir()): + tdir = gdir / "tensors" + if tdir.is_dir(): + tensor_paths.extend(sorted(tdir.glob("*.npy"))) + + if not tensor_paths: + tensor_paths.extend(sorted(root.glob("**/tensors/*.npy"))) + + # ============================================================ + # MODO 1: dataset já normalizado com tensors/*.npy + # ============================================================ + if tensor_paths: + samples: List[SampleItem] = [] + has_any_gt = False + + for tp in tensor_paths: + base = tp.stem + group_name = tp.parent.parent.name if tp.parent.name == "tensors" else "default" + group_dir = tp.parent.parent if tp.parent.name == "tensors" else tp.parent + + masks: Dict[str, Optional[Path]] = {} + missing = [] + + for head_name, hcfg in heads_config.items(): + mask_dir_name = str(hcfg.get("mask_dir", "masks")) + is_derived = ( + bool(hcfg.get("derived", False)) + or mask_dir_name == "__derived_target__" + ) + + # Heads derivadas não possuem máscara física. + # O GT target será construído posteriormente a partir de: + # vegetation == 1 AND cana == 0. + if is_derived: + masks[head_name] = None + continue + + mask_dir = group_dir / mask_dir_name + mask_path = mask_dir / f"{base}.npy" + + if mask_path.exists(): + masks[head_name] = mask_path + has_any_gt = True + else: + masks[head_name] = None + missing.append(f"{head_name}:{mask_path}") + + if require_masks and missing: + raise RuntimeError(f"Masks ausentes para {tp}: {missing}") + + meta_path = group_dir / "metas" / f"{base}.json" + preview_path = group_dir / "previews" / f"{base}.png" + + samples.append(SampleItem( + group=group_name, + base=base, + tensor_path=tp, + masks=masks, + meta_path=meta_path if meta_path.exists() else None, + preview_path=preview_path if preview_path.exists() else None, + source_kind="tensor", + raw_group=None, + )) + + return samples, has_any_gt + + # ============================================================ + # MODO 2: dataset bruto com bins/metas/previews/masks + # ============================================================ + raw_root = find_raw_dataset_layout_root(root) + + if raw_root is None: + raise RuntimeError( + f"Nenhum tensor .npy encontrado e também não detectei layout bruto " + f"com bins/metas/previews em: {root}" + ) + + meta_paths = sorted((raw_root / "metas").glob("*.json")) + if not meta_paths: + raise RuntimeError(f"Nenhum meta .json encontrado em: {raw_root / 'metas'}") + + samples: List[SampleItem] = [] + has_any_gt = False + + for mp in meta_paths: + base = mp.stem + meta = load_json(mp) + + if meta.get("saved_payload_type") != "raw_native_multi": + print(f"[WARN] pulando {mp.name}: saved_payload_type={meta.get('saved_payload_type')}") + continue + + raw_group = resolve_raw_capture_group_from_json(mp, raw_root) + + masks: Dict[str, Optional[Path]] = {} + + # Tenta masks específicas multi-head primeiro. + for head_name, hcfg in heads_config.items(): + mask_dir = raw_root / str(hcfg.get("mask_dir", "masks")) + mask_path = None + + mask_dir_name = str(hcfg.get("mask_dir", "masks")) + + if mask_dir_name == "__derived_target__" or bool(hcfg.get("derived", False)): + masks[head_name] = None + continue + + for ext in (".npy", ".png", ".tif", ".tiff"): + p = mask_dir / f"{base}{ext}" + if p.exists(): + mask_path = p + break + + masks[head_name] = mask_path + if mask_path is not None: + has_any_gt = True + + # Se só existir masks/.png ou .npy semantic, conecta na head semantic. + if masks.get("semantic") is None: + semantic_mask = resolve_raw_sibling_file( + raw_root, + "masks", + base, + (".npy", ".png", ".tif", ".tiff"), + ) + if semantic_mask is not None: + masks["semantic"] = semantic_mask + has_any_gt = True + + if require_masks and not any(p is not None for p in masks.values()): + raise RuntimeError(f"Mask ausente para captura bruta: {mp}") + + preview_path = resolve_raw_sibling_file( + raw_root, + "previews", + base, + (".png", ".jpg", ".jpeg"), + ) + + samples.append(SampleItem( + group=raw_root.name, + base=base, + tensor_path=None, + masks=masks, + meta_path=mp, + preview_path=preview_path, + source_kind="raw_native_multi", + raw_group=raw_group, + )) + + if not samples: + raise RuntimeError(f"Nenhuma captura raw_native_multi válida encontrada em: {raw_root}") + + return samples, has_any_gt + + +def load_tensor( + path: Path, + requested_channels: Sequence[str], + saved_channel_names: Optional[Sequence[str]] = None, +) -> np.ndarray: + arr = np.load(str(path)).astype(np.float32) + + if arr.ndim != 3: + raise RuntimeError(f"Tensor inválido {path}: shape={arr.shape}, esperado 3D") + + # Normaliza para CHW. Os tensores atuais podem ter Raw5 + NDVI + NDRE. + if 1 <= arr.shape[0] <= len(SUPPORTED_INPUT_CHANNELS) and arr.shape[1] > 8 and arr.shape[2] > 8: + chw = arr + elif 1 <= arr.shape[-1] <= len(SUPPORTED_INPUT_CHANNELS) and arr.shape[0] > 8 and arr.shape[1] > 8: + chw = np.transpose(arr, (2, 0, 1)) + else: + raise RuntimeError(f"Tensor com layout inesperado: {path} shape={arr.shape}") + + requested = [str(x).strip().upper() for x in requested_channels] + saved = [str(x).strip().upper() for x in (saved_channel_names or [])] + + if saved: + if len(saved) != chw.shape[0]: + raise RuntimeError( + f"Meta/tensor incompatíveis em {path}: meta channels={saved}, shape={chw.shape}" + ) + if len(set(saved)) != len(saved): + raise RuntimeError(f"Meta possui canais duplicados em {path}: {saved}") + missing = [name for name in requested if name not in saved] + if missing: + raise RuntimeError( + f"Tensor {path} não contém canais pedidos {missing}. Disponíveis={saved}" + ) + indices = [saved.index(name) for name in requested] + elif chw.shape[0] == len(requested): + # Compatibilidade com tensores antigos sem metadata de canais. + indices = list(range(len(requested))) + print(f"[WARN] {path.name}: sem channels no meta; assumindo ordem do config {requested}") + elif chw.shape[0] == len(SOURCE_CHANNEL_ORDER) and all(name in SOURCE_CHANNEL_ORDER for name in requested): + indices = [SOURCE_CHANNEL_ORDER.index(name) for name in requested] + print(f"[WARN] {path.name}: tensor Raw5 legado; usando ordem física {SOURCE_CHANNEL_ORDER}") + else: + raise RuntimeError( + f"Não é seguro inferir a ordem dos canais de {path}: shape={chw.shape}, " + f"pedidos={requested}. Garanta metas/.json com o campo channels." + ) + + chw = chw[indices, :, :] + + finite = np.isfinite(chw) + if finite.any(): + mx = float(np.nanmax(chw[finite])) + if mx > 2.0 and mx <= 255.0: + chw = chw / 255.0 + elif mx > 255.0: + chw = chw / 65535.0 + + # Não aplicar clip 0..1: NDVI/NDRE possuem faixa nominal -1..1. + chw = np.nan_to_num(chw, nan=0.0, posinf=1.0, neginf=-1.0) + return np.ascontiguousarray(chw, dtype=np.float32) + + +def load_sample_tensor( + sample: SampleItem, + requested_channels: Sequence[str], + derived_config: Optional[dict] = None, + target_size: Optional[Tuple[int, int]] = None, + module_params_path: Optional[str] = None, +) -> np.ndarray: + """ + Carrega tensor de uma amostra. + - Se for tensor pronto: lê .npy. + - Se for RAW_BRUTO: gera MULTISPEC em tempo real a partir dos bins. + """ + if sample.source_kind == "tensor": + if sample.tensor_path is None: + raise RuntimeError(f"Sample tensor sem tensor_path: {sample.base}") + meta = load_json(sample.meta_path) if sample.meta_path is not None else {} + saved_names = meta.get("channels") or meta.get("input_channels") or [] + return load_tensor(sample.tensor_path, requested_channels, saved_names) + + if sample.source_kind == "raw_native_multi": + if sample.raw_group is None or sample.meta_path is None: + raise RuntimeError(f"Sample raw sem raw_group/meta_path: {sample.base}") + + meta = load_json(sample.meta_path) + tensor, _processing_info = build_multispec_from_raw_native_multi_for_infer( + sample.raw_group, + meta, + target_size=target_size, + module_params_path=module_params_path, + ) + + if tensor.ndim != 3: + raise RuntimeError(f"Tensor RAW gerado inválido: {sample.base} shape={tensor.shape}") + + return build_model_input_tensor(tensor, requested_channels, derived_config) + + raise RuntimeError(f"source_kind desconhecido: {sample.source_kind}") + + +def load_mask(path: Optional[Path]) -> Optional[np.ndarray]: + if path is None: + return None + if path.suffix.lower() == ".npy": + mask = np.load(str(path)) + else: + mask = cv2.imread(str(path), cv2.IMREAD_UNCHANGED) + if mask is None: + raise RuntimeError(f"Falha ao ler mask: {path}") + if mask.ndim == 3: + mask = mask[:, :, 0] + return mask.astype(np.int64) + + +# ============================================================ +# Normalização +# ============================================================ + +def load_norm_stats( + path: Optional[Path], + channel_names: Sequence[str], +) -> Tuple[Optional[List[float]], Optional[List[float]]]: + if path is None or not path.is_file(): + if path is not None: + print(f"[NORM] não encontrei norm_stats em {path}. Usando tensor 0..1 sem padronização.") + return None, None + + js = load_json(path) + mean = js.get("mean", None) + std = js.get("std", None) + names = [str(x).strip().upper() for x in js.get("channels", [])] + requested = [str(x).strip().upper() for x in channel_names] + + if mean is None or std is None: + raise RuntimeError(f"norm_stats inválido, faltando mean/std: {path}") + if len(mean) != len(std): + raise RuntimeError(f"norm_stats inválido: mean={len(mean)} std={len(std)} em {path}") + + if names: + if len(names) != len(mean): + raise RuntimeError( + f"norm_stats incompatível: channels={len(names)} mean={len(mean)} em {path}" + ) + if len(set(names)) != len(names): + raise RuntimeError(f"norm_stats possui canais duplicados: {names}") + missing = [name for name in requested if name not in names] + if missing: + raise RuntimeError( + f"norm_stats não contém canais pedidos {missing}. Disponíveis={names}" + ) + indices = [names.index(name) for name in requested] + elif len(mean) == len(requested): + indices = list(range(len(requested))) + print(f"[NORM][WARN] stats sem nomes; assumindo ordem do config {requested}") + elif len(mean) == len(SOURCE_CHANNEL_ORDER) and all(name in SOURCE_CHANNEL_ORDER for name in requested): + indices = [SOURCE_CHANNEL_ORDER.index(name) for name in requested] + print(f"[NORM][WARN] stats Raw5 legados; usando ordem {SOURCE_CHANNEL_ORDER}") + else: + raise RuntimeError( + f"Não é seguro mapear norm_stats sem nomes: mean={len(mean)}, pedidos={requested}" + ) + + mean = [float(mean[i]) for i in indices] + std = [float(std[i]) for i in indices] + if not np.all(np.isfinite(mean)) or not np.all(np.isfinite(std)) or any(x <= 0.0 for x in std): + raise RuntimeError(f"norm_stats contém valores inválidos: mean={mean} std={std}") + + print(f"[NORM] usando {path}") + print(f"[NORM] channels={requested}") + print(f"[NORM] mean={mean}") + print(f"[NORM] std ={std}") + return mean, std + + +# ============================================================ +# Modelo multi-head, igual ao treino +# ============================================================ + +def patch_segformer_encoder_input_channels( + segformer_encoder: nn.Module, + input_channel_names: Sequence[str], + init_mode: str = "zero_extra", +): + """Replica a inicialização espectral do Agri Teacher V2/V2.1.""" + names = [str(c).strip().upper() for c in input_channel_names] + if names == ["R", "G", "B"]: + return segformer_encoder + + # Compatibilidade entre versões do transformers. + if hasattr(segformer_encoder, "encoder") and hasattr(segformer_encoder.encoder, "patch_embeddings"): + patch_owner = segformer_encoder.encoder.patch_embeddings[0] + elif hasattr(segformer_encoder, "stages") and len(segformer_encoder.stages) > 0: + patch_owner = segformer_encoder.stages[0].patch_embeddings + else: + raise RuntimeError("Não encontrei o primeiro patch embedding do SegFormer nesta versão do Transformers.") + + proj = patch_owner.proj + old_weight = proj.weight.data.clone() + old_bias = proj.bias.data.clone() if proj.bias is not None else None + + new_proj = nn.Conv2d( + in_channels=len(names), + out_channels=proj.out_channels, + kernel_size=proj.kernel_size, + stride=proj.stride, + padding=proj.padding, + dilation=proj.dilation, + groups=proj.groups, + bias=proj.bias is not None, + padding_mode=proj.padding_mode, + ) + + mode = str(init_mode or "zero_extra").lower() + with torch.no_grad(): + if old_weight.shape[1] < 3: + raise RuntimeError(f"Patch embedding pré-treinado inesperado: weight={tuple(old_weight.shape)}") + + pretrained = {"R": 0, "G": 1, "B": 2} + mean_rgb = old_weight[:, :3].mean(dim=1) + has_rgb = any(n in pretrained for n in names) + + for dst, name in enumerate(names): + if name in pretrained: + new_proj.weight[:, dst].copy_(old_weight[:, pretrained[name]]) + elif mode == "mean_rgb": + new_proj.weight[:, dst].copy_(mean_rgb) + elif mode == "scaled_mean": + new_proj.weight[:, dst].copy_(mean_rgb * (3.0 / max(3.0, float(len(names))))) + elif mode == "zero_extra" and has_rgb: + new_proj.weight[:, dst].zero_() + else: + new_proj.weight[:, dst].copy_(mean_rgb) + + if old_bias is not None: + new_proj.bias.copy_(old_bias) + + patch_owner.proj = new_proj + print(f"[MODEL] input patch RGB(3) -> {names} | init={mode}") + return segformer_encoder + + +def replace_segformer_decode_classifier(decode_head: nn.Module, num_classes: int): + old = getattr(decode_head, "classifier", None) + if not isinstance(old, nn.Conv2d): + raise RuntimeError(f"decode_head.classifier inesperado: {type(old)}") + + new = nn.Conv2d( + old.in_channels, + int(num_classes), + kernel_size=old.kernel_size, + stride=old.stride, + padding=old.padding, + dilation=old.dilation, + groups=old.groups, + bias=old.bias is not None, + padding_mode=old.padding_mode, + ) + nn.init.xavier_uniform_(new.weight) + if new.bias is not None: + nn.init.zeros_(new.bias) + decode_head.classifier = new + return decode_head + + +class MultiHeadSegFormer(nn.Module): + """Arquitetura compatível com o trainer antigo e com Agri Teacher V2/V2.1.""" + def __init__( + self, + backbone: str, + input_channel_names: Sequence[str], + heads_config: Dict[str, dict], + semantic_id2label: Dict[int, str], + semantic_label2id: Dict[str, int], + model_cfg: Optional[dict] = None, + ): + super().__init__() + model_cfg = model_cfg or {} + + semantic_classes = int(heads_config["semantic"].get("num_classes", len(semantic_id2label))) + names = [str(c).strip().upper() for c in input_channel_names] + + base = SegformerForSemanticSegmentation.from_pretrained( + backbone, + num_labels=semantic_classes, + id2label={int(k): str(v) for k, v in semantic_id2label.items()}, + label2id={str(k): int(v) for k, v in semantic_label2id.items()}, + ignore_mismatched_sizes=True, + ) + + self.decoder_mode = str(model_cfg.get("decoder_mode", "separate")).lower() + self.spectral_input_init = str(model_cfg.get("spectral_input_init", "mean_rgb")).lower() + + patch_segformer_encoder_input_channels( + base.segformer, + names, + init_mode=self.spectral_input_init, + ) + + base.config.num_channels = len(names) + base.config.input_channel_names = list(names) + base.config.agri_decoder_mode = self.decoder_mode + base.config.agri_spectral_input_init = self.spectral_input_init + + self.segformer = base.segformer + self.heads_config = heads_config + + if self.decoder_mode == "separate": + self.decode_heads = nn.ModuleDict() + for head_name, hcfg in heads_config.items(): + h = copy.deepcopy(base.decode_head) + self.decode_heads[head_name] = replace_segformer_decode_classifier( + h, int(hcfg["num_classes"]) + ) + self.shared_decode_head = None + self.classifiers = None + + elif self.decoder_mode == "shared_light": + self.decode_heads = None + self.shared_decode_head = copy.deepcopy(base.decode_head) + old_classifier = self.shared_decode_head.classifier + if not isinstance(old_classifier, nn.Conv2d): + raise RuntimeError("SegFormer decode_head.classifier não é Conv2d.") + + feature_channels = int(old_classifier.in_channels) + self.shared_decode_head.classifier = nn.Identity() + self.classifiers = nn.ModuleDict() + + for head_name, hcfg in heads_config.items(): + clf = nn.Conv2d( + feature_channels, + int(hcfg["num_classes"]), + kernel_size=1, + bias=True, + ) + nn.init.xavier_uniform_(clf.weight) + nn.init.zeros_(clf.bias) + self.classifiers[head_name] = clf + else: + raise RuntimeError( + f"training_v2.model.decoder_mode inválido: {self.decoder_mode}" + ) + + self.config = base.config + print(f"[MODEL] decoder_mode={self.decoder_mode}") + + def forward( + self, + pixel_values: torch.Tensor, + head_names: Optional[Sequence[str]] = None, + ) -> Dict[str, torch.Tensor]: + outputs = self.segformer( + pixel_values=pixel_values, + output_hidden_states=True, + return_dict=True, + ) + hidden_states = outputs.hidden_states + + if self.decoder_mode == "separate": + available = list(self.decode_heads.keys()) + selected = available if head_names is None else [h for h in head_names if h in self.decode_heads] + return {name: self.decode_heads[name](hidden_states) for name in selected} + + # shared_light: calcula o decoder compartilhado uma vez e só executa os classifiers pedidos. + features = self.shared_decode_head(hidden_states) + available = list(self.classifiers.keys()) + selected = available if head_names is None else [h for h in head_names if h in self.classifiers] + return {name: self.classifiers[name](features) for name in selected} + + +class MultiHeadTester: + def __init__( + self, + config: dict, + ckpt_path: Path, + device: torch.device, + input_channel_names: Sequence[str], + heads_config: Dict[str, dict], + semantic_id2label: Dict[int, str], + semantic_label2id: Dict[str, int], + mean: Optional[Sequence[float]], + std: Optional[Sequence[float]], + use_amp: bool = True, + ): + self.config = config + self.ckpt_path = ckpt_path + self.device = device + self.input_channel_names = [str(c).strip().upper() for c in input_channel_names] + self.channels = len(self.input_channel_names) + channels = self.channels + self.heads_config = heads_config + self.use_amp = use_amp and device.type == "cuda" + self.runtime_mode = str(config.get("runtime_mode", "all")).lower() + self.target_threshold = 0.5 + self.ckpt_operational_threshold = None + + self.mean = None if mean is None else torch.tensor(mean, dtype=torch.float32).view(1, channels, 1, 1).to(device) + self.std = None if std is None else torch.tensor(std, dtype=torch.float32).view(1, channels, 1, 1).to(device) + + backbone = config.get("backbone", config.get("pretrained_model", "nvidia/mit-b1")) + print(f"[MODEL] backbone={backbone}") + print(f"[MODEL] ckpt={ckpt_path}") + + training_v2 = config.get("training_v2", {}) or {} + model_cfg = training_v2.get("model", {}) or {} + print(f"[MODEL] config decoder_mode={model_cfg.get('decoder_mode', 'separate')} | spectral_init={model_cfg.get('spectral_input_init', 'mean_rgb')}") + + self.model = MultiHeadSegFormer( + backbone=backbone, + input_channel_names=self.input_channel_names, + heads_config=heads_config, + semantic_id2label=semantic_id2label, + semantic_label2id=semantic_label2id, + model_cfg=model_cfg, + ) + + self._load_checkpoint(ckpt_path) + self.model.to(device) + self.model.eval() + + def _load_checkpoint(self, ckpt_path: Path): + ckpt = torch.load(str(ckpt_path), map_location="cpu", weights_only=False) + + saved_contract = ckpt.get("model_contract", {}) if isinstance(ckpt, dict) else {} + saved_contract = saved_contract or {} + saved_channels = int(saved_contract.get("num_channels", 0) or 0) + saved_names = [str(x).strip().upper() for x in saved_contract.get("input_channel_names", [])] + legacy_default_names = SOURCE_CHANNEL_ORDER[:self.channels] + if not saved_names and self.input_channel_names != legacy_default_names: + raise RuntimeError( + "Checkpoint antigo sem model_contract.input_channel_names não pode ser " + f"validado para a ordem atual {self.input_channel_names}. Reexporte/treine com " + "o train_multihead atualizado ou use um checkpoint nominal compatível." + ) + if not saved_names: + print( + f"[MODEL][WARN] checkpoint legado sem nomes; assumindo {legacy_default_names}" + ) + if saved_channels and saved_channels != self.channels: + raise RuntimeError( + f"Checkpoint incompatível: salvou {saved_channels} canais, " + f"config atual pede {self.channels}." + ) + if saved_names and saved_names != self.input_channel_names: + raise RuntimeError( + f"Checkpoint incompatível: canais salvos={saved_names}, " + f"config atual={self.input_channel_names}." + ) + + saved_decoder = str(saved_contract.get("decoder_mode", "") or "").lower() + current_decoder = str(getattr(self.model.config, "agri_decoder_mode", "") or "").lower() + if saved_decoder and current_decoder and saved_decoder != current_decoder: + raise RuntimeError( + f"Checkpoint decoder_mode={saved_decoder}, mas config/teste criou decoder_mode={current_decoder}. " + "Use o mesmo training_v2.model.decoder_mode do treinamento." + ) + + saved_spectral = str(saved_contract.get("spectral_input_init", "") or "").lower() + current_spectral = str(getattr(self.model.config, "agri_spectral_input_init", "") or "").lower() + if saved_spectral and current_spectral and saved_spectral != current_spectral: + raise RuntimeError( + f"Checkpoint spectral_input_init={saved_spectral}, mas config/teste usa {current_spectral}." + ) + + extra = ckpt.get("extra", {}) if isinstance(ckpt, dict) else {} + score_now = extra.get("score_now", {}) if isinstance(extra, dict) else {} + try: + op_thr = score_now.get("operational_threshold") + if op_thr is not None: + self.ckpt_operational_threshold = float(op_thr) + except Exception: + self.ckpt_operational_threshold = None + + if isinstance(ckpt, dict): + for key in ("model", "model_state", "model_state_dict", "state_dict"): + if key in ckpt and isinstance(ckpt[key], dict): + state = ckpt[key] + break + else: + state = ckpt + else: + raise RuntimeError(f"Checkpoint em formato inesperado: {type(ckpt)}") + + clean = {} + for k, v in state.items(): + nk = k + for prefix in ("module.", "model."): + if nk.startswith(prefix): + nk = nk[len(prefix):] + clean[nk] = v + + try: + self.model.load_state_dict(clean, strict=True) + except RuntimeError as exc: + raise RuntimeError( + "Checkpoint incompatível com a arquitetura/canais atuais. " + "Use o mesmo input_channels e heads empregados no treinamento.\n" + f"Detalhe original: {exc}" + ) from exc + print("[MODEL] checkpoint carregado com strict=True") + + def _normalize(self, x: torch.Tensor) -> torch.Tensor: + if self.mean is not None and self.std is not None: + return (x - self.mean) / torch.clamp(self.std, min=1e-6) + return x + + @torch.inference_mode() + def infer(self, chw_01: np.ndarray) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], float]: + x = torch.from_numpy(chw_01).unsqueeze(0).to(self.device, non_blocking=True) + x = self._normalize(x) + + h, w = int(chw_01.shape[1]), int(chw_01.shape[2]) + + if self.device.type == "cuda": + torch.cuda.synchronize() + t0 = time.perf_counter() + + with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=self.use_amp): + head_names = None + if self.runtime_mode in ("target_direct", "target_head"): + head_names = ["target"] + elif self.runtime_mode in ("operational", "target_op"): + head_names = ["vegetation", "cana"] + + logits_by_head = self.model(pixel_values=x, head_names=head_names) + + preds = {} + probs = {} + + for head_name, logits in logits_by_head.items(): + logits = F.interpolate(logits, size=(h, w), mode="bilinear", align_corners=False) + prob = torch.softmax(logits, dim=1)[0] + if head_name == "target" and prob.shape[0] > 1: + pred = (prob[1] >= float(self.target_threshold)).to(torch.uint8) + else: + pred = torch.argmax(prob, dim=0).to(torch.uint8) + preds[head_name] = pred.detach().cpu().numpy().astype(np.uint8) + probs[head_name] = prob.detach().cpu().numpy().astype(np.float32) + + if self.device.type == "cuda": + torch.cuda.synchronize() + t_ms = (time.perf_counter() - t0) * 1000.0 + + return preds, probs, t_ms + + +class OnnxMultiHeadTester: + def __init__( + self, + config: dict, + onnx_path: Path, + provider: str, + input_channel_names: Sequence[str], + heads_config: Dict[str, dict], + mean: Optional[Sequence[float]], + std: Optional[Sequence[float]], + trt_home: Optional[str] = None, + trt_fp16: bool = True, + ): + self.config = config + self.onnx_path = onnx_path + self.provider = provider.lower() + self.input_channel_names = [str(x).strip().upper() for x in input_channel_names] + self.channels = len(self.input_channel_names) + channels = self.channels + self.heads_config = heads_config + self.runtime_mode = str(config.get("runtime_mode", "all")).lower() + self.target_threshold = 0.5 + + export_meta_path = onnx_path.with_suffix(".export_meta.json") + export_meta = load_json(export_meta_path) if export_meta_path.is_file() else {} + exported_names = [ + str(x).strip().upper() + for x in export_meta.get("input_channel_names", []) + ] + if exported_names and exported_names != self.input_channel_names: + raise RuntimeError( + f"ONNX incompatível: canais exportados={exported_names}, " + f"config atual={self.input_channel_names}." + ) + + postprocess = str(export_meta.get("postprocess", "none")).lower() + if postprocess not in ("none", "resize_logits"): + raise RuntimeError( + f"Este comparador espera logits, mas o ONNX usa postprocess={postprocess}. " + "Exporte com --postprocess none ou resize_logits." + ) + + input_contract = export_meta.get("input_contract", {}) or {} + self.normalization_embedded = bool( + input_contract.get( + "normalization_embedded", + export_meta.get("include_norm", False), + ) + ) + if self.normalization_embedded: + self.mean = None + self.std = None + print("[ONNX_MODEL] normalização já embutida no grafo") + else: + self.mean = None if mean is None else np.asarray(mean, dtype=np.float32).reshape(1, channels, 1, 1) + self.std = None if std is None else np.asarray(std, dtype=np.float32).reshape(1, channels, 1, 1) + + print(f"[ONNX_MODEL] onnx={onnx_path}") + print(f"[ONNX_MODEL] provider={provider}") + + self.session = self._create_session( + onnx_path=onnx_path, + provider=provider, + trt_home=trt_home, + trt_fp16=trt_fp16, + ) + + self.input_name = self.session.get_inputs()[0].name + input_shape = self.session.get_inputs()[0].shape + if len(input_shape) != 4: + raise RuntimeError(f"Entrada ONNX inesperada: nome={self.input_name} shape={input_shape}") + onnx_channels = input_shape[1] + if isinstance(onnx_channels, int) and onnx_channels != self.channels: + raise RuntimeError( + f"ONNX incompatível: entrada possui {onnx_channels} canais, " + f"config atual pede {self.channels}." + ) + self.output_names = [o.name for o in self.session.get_outputs()] + print(f"[ONNX_MODEL] input={self.input_name} shape={input_shape}") + print(f"[ONNX_MODEL] outputs={self.output_names}") + + def _create_session( + self, + onnx_path: Path, + provider: str, + trt_home: Optional[str], + trt_fp16: bool, + ): + try: + import onnxruntime as ort + except ImportError: + raise ImportError( + "onnxruntime não está instalado. Use:\n" + " pip install onnxruntime-gpu" + ) + + provider = provider.lower() + + if provider == "tensorrt": + trt_home = trt_home or os.environ.get("TRT_HOME", r"C:\dev\TensorRT-10.10.0.31") + + dll_dirs = [ + os.path.join(trt_home, "lib"), + os.path.join(trt_home, "bin"), + ] + + cuda_home = os.environ.get("CUDA_PATH") + if cuda_home: + dll_dirs.append(os.path.join(cuda_home, "bin")) + + # fallback comum que vocês estão usando + dll_dirs.append(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.3\bin") + dll_dirs.append(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\bin") + + for dll_dir in dll_dirs: + if os.path.isdir(dll_dir): + try: + os.add_dll_directory(dll_dir) + print(f"[DLL] add_dll_directory: {dll_dir}") + except Exception as e: + print(f"[DLL][WARN] falha em {dll_dir}: {e}") + + available = ort.get_available_providers() + print(f"[ONNX] providers disponíveis: {available}") + + sess_options = ort.SessionOptions() + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + + if provider == "cpu": + providers = ["CPUExecutionProvider"] + + elif provider == "cuda": + providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] + + elif provider == "tensorrt": + cache_dir = onnx_path.parent / "trt_cache" + cache_dir.mkdir(parents=True, exist_ok=True) + + trt_options = { + "device_id": 0, + "trt_fp16_enable": bool(trt_fp16), + "trt_engine_cache_enable": True, + "trt_engine_cache_path": str(cache_dir), + "trt_timing_cache_enable": True, + "trt_timing_cache_path": str(cache_dir), + "trt_max_workspace_size": 4 * 1024 * 1024 * 1024, + } + + providers = [ + ("TensorrtExecutionProvider", trt_options), + "CUDAExecutionProvider", + "CPUExecutionProvider", + ] + + else: + raise RuntimeError(f"Provider ONNX inválido: {provider}") + + providers_ok = [ + p for p in providers + if (p[0] if isinstance(p, tuple) else p) in available + ] + + if not providers_ok: + raise RuntimeError(f"Nenhum provider ONNX disponível. Pedido={providers}, disponíveis={available}") + + session = ort.InferenceSession( + str(onnx_path), + sess_options=sess_options, + providers=providers_ok, + ) + + active = session.get_providers() + print(f"[ONNX] usando providers: {active}") + + if provider == "tensorrt" and "TensorrtExecutionProvider" not in active: + raise RuntimeError(f"TensorRT solicitado, mas não ficou ativo. Providers ativos: {active}") + + if provider == "cuda" and "CUDAExecutionProvider" not in active: + raise RuntimeError(f"CUDA solicitado, mas não ficou ativo. Providers ativos: {active}") + + return session + + def _normalize(self, x: np.ndarray) -> np.ndarray: + if self.mean is not None and self.std is not None: + return ((x - self.mean[0]) / np.clip(self.std[0], 1e-6, None)).astype(np.float32) + return x.astype(np.float32) + + def _selected_heads(self) -> Optional[List[str]]: + if self.runtime_mode in ("target_direct", "target_head"): + return ["target"] + if self.runtime_mode in ("operational", "target_op"): + return ["vegetation", "cana"] + return None + + @staticmethod + def _softmax_np(logits: np.ndarray, axis: int = 1) -> np.ndarray: + x = logits.astype(np.float32) + x = x - np.max(x, axis=axis, keepdims=True) + e = np.exp(x) + return e / np.clip(np.sum(e, axis=axis, keepdims=True), 1e-12, None) + + @staticmethod + def _resize_logits_nchw(logits: np.ndarray, target_hw: Tuple[int, int]) -> np.ndarray: + n, c, h, w = logits.shape + th, tw = target_hw + + if (h, w) == (th, tw): + return logits + + out = np.empty((n, c, th, tw), dtype=np.float32) + for bi in range(n): + for ci in range(c): + out[bi, ci] = cv2.resize( + logits[bi, ci].astype(np.float32), + (tw, th), + interpolation=cv2.INTER_LINEAR, + ) + return out + + def _map_outputs(self, outputs: List[np.ndarray]) -> Dict[str, np.ndarray]: + raw = { + name: arr.astype(np.float32) + for name, arr in zip(self.output_names, outputs) + } + + mapped = {} + for head in self.heads_config.keys(): + candidates = [ + head, + f"{head}_logits", + f"output_{head}", + ] + + found = None + for c in candidates: + if c in raw: + found = c + break + + if found is not None: + mapped[head] = raw[found] + + return mapped + + def infer(self, chw_01: np.ndarray) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], float]: + h, w = int(chw_01.shape[1]), int(chw_01.shape[2]) + + x = self._normalize(chw_01) + x = np.expand_dims(x, axis=0).astype(np.float32) + + t0 = time.perf_counter() + outputs = self.session.run(None, {self.input_name: x}) + t_ms = (time.perf_counter() - t0) * 1000.0 + + logits_by_head = self._map_outputs(outputs) + + selected = self._selected_heads() + if selected is not None: + logits_by_head = { + hname: logits + for hname, logits in logits_by_head.items() + if hname in selected + } + + preds = {} + probs = {} + + for head_name, logits in logits_by_head.items(): + logits = self._resize_logits_nchw(logits, (h, w)) + prob = self._softmax_np(logits, axis=1)[0] + if head_name == "target" and prob.shape[0] > 1: + pred = (prob[1] >= float(self.target_threshold)).astype(np.uint8) + else: + pred = np.argmax(prob, axis=0).astype(np.uint8) + + preds[head_name] = pred + probs[head_name] = prob.astype(np.float32) + + return preds, probs, t_ms + + +# ============================================================ +# Checkpoints / paths +# ============================================================ + +def infer_experiment_tag(config: dict, channels: int) -> str: + explicit = str(config.get("stats_source_tag", "")).strip() + if explicit: + return explicit + fusion_mode = str(config.get("fusion_mode", "stacked")).strip() + return f"{fusion_mode}_raw{channels}" + + +def infer_save_dir(config: dict, config_dir: Path, channels: int) -> Path: + model_name = config.get("model_name", "test_multi") + modelo_folder = config.get("modelo", "segformer_b1") + exp_tag = infer_experiment_tag(config, channels) + return (config_dir / "backup" / modelo_folder / model_name / exp_tag).resolve() + + +def find_checkpoint(save_dir: Path, preferred: Optional[str] = None) -> Path: + if preferred is not None: + ckpt = Path(preferred) + if not ckpt.is_absolute(): + ckpt_cwd = (Path.cwd() / ckpt).resolve() + ckpt_save = (save_dir / ckpt).resolve() + ckpt = ckpt_cwd if ckpt_cwd.is_file() else ckpt_save + if not ckpt.is_file(): + raise FileNotFoundError(f"Checkpoint não encontrado: {ckpt}") + return ckpt + + candidates = [ + save_dir / "best_score.pt", + save_dir / "best_operational.pt", + save_dir / "best_legacy_score.pt", + save_dir / "best_target.pt", + save_dir / "best_cana_head.pt", + save_dir / "best_semantic_miou.pt", + save_dir / "last.pt", + ] + for c in candidates: + if c.is_file(): + return c + + raise FileNotFoundError("Nenhum checkpoint encontrado. Procurei:\n" + "\n".join(str(c) for c in candidates)) + + +def find_onnx_model(save_dir: Path, ckpt_path: Path, preferred: Optional[str] = None) -> Path: + """ + Resolve o .onnx. + + Se preferred for informado, usa ele. + Caso contrário, usa o mesmo stem do checkpoint: + best_score.pt -> best_score.onnx + """ + if preferred: + p = Path(preferred) + if not p.is_absolute(): + p_cwd = (Path.cwd() / p).resolve() + p_save = (save_dir / p).resolve() + p = p_cwd if p_cwd.is_file() else p_save + + if not p.is_file(): + raise FileNotFoundError(f"ONNX não encontrado: {p}") + + return p.resolve() + + p = ckpt_path.with_suffix(".onnx") + + if not p.is_file(): + alt = save_dir / f"{ckpt_path.stem}.onnx" + p = alt + + if not p.is_file(): + raise FileNotFoundError( + f"ONNX não encontrado para checkpoint {ckpt_path.name}. Procurei:\n" + f" {ckpt_path.with_suffix('.onnx')}\n" + f" {save_dir / (ckpt_path.stem + '.onnx')}\n" + f"Informe manualmente com --onnx." + ) + + return p.resolve() + + +# ============================================================ +# Visualização +# ============================================================ + +def tensor_to_preview_rgb( + chw: np.ndarray, + channel_names: Sequence[str], + gamma: float = 0.85, +) -> np.ndarray: + c, h, w = chw.shape + names = [str(x).strip().upper() for x in channel_names] + if all(name in names for name in ("R", "G", "B")): + indices = [names.index(name) for name in ("R", "G", "B")] + rgb = np.transpose(chw[indices], (1, 2, 0)).copy() + elif c >= 3: + rgb = np.transpose(chw[:3], (1, 2, 0)).copy() + else: + one = chw[0] + rgb = np.stack([one, one, one], axis=-1) + + rgb = np.nan_to_num(rgb, nan=0.0, posinf=1.0, neginf=0.0) + lo = np.percentile(rgb, 1.0) + hi = np.percentile(rgb, 99.0) + if hi > lo: + rgb = (rgb - lo) / (hi - lo) + rgb = np.clip(rgb, 0.0, 1.0) + if gamma and gamma > 0: + rgb = np.power(rgb, gamma) + return (rgb * 255.0).astype(np.uint8) + + +def ids_to_rgb(mask: np.ndarray, colormap_rgb: Dict[int, Tuple[int, int, int]], ignore_id: int = 255) -> np.ndarray: + h, w = mask.shape[:2] + out = np.zeros((h, w, 3), dtype=np.uint8) + for cid, color in colormap_rgb.items(): + out[mask == cid] = color + out[mask == ignore_id] = (0, 0, 0) + return out + + +def prob_to_heat_rgb(prob01: np.ndarray) -> np.ndarray: + p = np.clip(prob01, 0.0, 1.0) + u8 = (p * 255.0).astype(np.uint8) + bgr = cv2.applyColorMap(u8, cv2.COLORMAP_TURBO) + return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + + +def overlay_rgb(base_rgb: np.ndarray, mask_rgb: np.ndarray, alpha: float) -> np.ndarray: + return cv2.addWeighted(base_rgb, 1.0 - alpha, mask_rgb, alpha, 0.0) + + +def put_label(img_rgb: np.ndarray, title: str, subtitle: str = "") -> np.ndarray: + out = img_rgb.copy() + cv2.putText(out, title, (10, 26), cv2.FONT_HERSHEY_SIMPLEX, 0.68, (0, 0, 0), 4, cv2.LINE_AA) + cv2.putText(out, title, (10, 26), cv2.FONT_HERSHEY_SIMPLEX, 0.68, (255, 255, 255), 2, cv2.LINE_AA) + if subtitle: + cv2.putText(out, subtitle, (10, 52), cv2.FONT_HERSHEY_SIMPLEX, 0.48, (0, 0, 0), 3, cv2.LINE_AA) + cv2.putText(out, subtitle, (10, 52), cv2.FONT_HERSHEY_SIMPLEX, 0.48, (0, 255, 90), 1, cv2.LINE_AA) + return out + + +def resize_panel(img: np.ndarray, size: Tuple[int, int]) -> np.ndarray: + w, h = size + if img.shape[1] == w and img.shape[0] == h: + return img + return cv2.resize(img, (w, h), interpolation=cv2.INTER_NEAREST) + + +def compose_grid(panels: List[Tuple[str, np.ndarray, str]], cols: int = 3, max_width: int = 1800) -> np.ndarray: + if not panels: + return np.zeros((480, 640, 3), dtype=np.uint8) + + base_h, base_w = panels[0][1].shape[:2] + labeled = [] + for title, img, subtitle in panels: + img = resize_panel(img, (base_w, base_h)) + labeled.append(put_label(img, title, subtitle)) + + rows = [] + blank = np.zeros_like(labeled[0]) + for i in range(0, len(labeled), cols): + row_imgs = labeled[i:i + cols] + while len(row_imgs) < cols: + row_imgs.append(blank.copy()) + rows.append(np.hstack(row_imgs)) + + canvas = np.vstack(rows) + + if canvas.shape[1] > max_width: + scale = max_width / canvas.shape[1] + canvas = cv2.resize(canvas, (int(canvas.shape[1] * scale), int(canvas.shape[0] * scale)), interpolation=cv2.INTER_AREA) + + return canvas + + +def class_percent(mask: np.ndarray, class_id: int, ignore_id: int = 255) -> float: + valid = mask != ignore_id + den = int(valid.sum()) + if den <= 0: + return 0.0 + return float(((mask == class_id) & valid).sum() * 100.0 / den) + + +def compare_pred_equal_percent(a: Optional[np.ndarray], b: Optional[np.ndarray]) -> Optional[float]: + if a is None or b is None: + return None + + if a.shape != b.shape: + b = cv2.resize( + b.astype(np.uint8), + (a.shape[1], a.shape[0]), + interpolation=cv2.INTER_NEAREST, + ) + + return float(np.mean(a == b) * 100.0) + + +def diff_mask_rgb(a: Optional[np.ndarray], b: Optional[np.ndarray]) -> Optional[np.ndarray]: + if a is None or b is None: + return None + + if a.shape != b.shape: + b = cv2.resize( + b.astype(np.uint8), + (a.shape[1], a.shape[0]), + interpolation=cv2.INTER_NEAREST, + ) + + diff = (a != b).astype(np.uint8) * 255 + rgb = np.zeros((diff.shape[0], diff.shape[1], 3), dtype=np.uint8) + rgb[:, :, 0] = diff # vermelho em RGB + return rgb + + +# ============================================================ +# Métricas numpy +# ============================================================ + +def confusion_matrix_np(pred: np.ndarray, gt: np.ndarray, num_classes: int, ignore_id: int) -> np.ndarray: + if pred.shape != gt.shape: + pred = cv2.resize(pred.astype(np.uint8), (gt.shape[1], gt.shape[0]), interpolation=cv2.INTER_NEAREST) + + valid = gt != ignore_id + valid &= gt >= 0 + valid &= gt < num_classes + gt_v = gt[valid].astype(np.int64) + pred_v = pred[valid].astype(np.int64) + pred_v = np.clip(pred_v, 0, num_classes - 1) + + cm = np.bincount(num_classes * gt_v + pred_v, minlength=num_classes * num_classes) + return cm.reshape(num_classes, num_classes).astype(np.int64) + + +def metrics_from_cm(cm: np.ndarray) -> Tuple[np.ndarray, float, float]: + tp = np.diag(cm).astype(np.float64) + fp = cm.sum(axis=0).astype(np.float64) - tp + fn = cm.sum(axis=1).astype(np.float64) - tp + denom = tp + fp + fn + iou = np.divide(tp, denom, out=np.zeros_like(tp), where=denom > 0) + miou = float(np.mean(iou)) if len(iou) else 0.0 + acc = float(tp.sum() / max(cm.sum(), 1)) + return iou, miou, acc + + +def operational_target_mask(veg_mask: np.ndarray, cana_mask: np.ndarray, ignore_id: int = 255) -> np.ndarray: + out = np.zeros_like(veg_mask, dtype=np.uint8) + ignore = (veg_mask == ignore_id) | (cana_mask == ignore_id) + out[(veg_mask == 1) & (cana_mask == 0)] = 1 + out[ignore] = ignore_id + return out + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--config", default="config.json") + parser.add_argument("--split_folder", default="val", choices=["train", "val", "test"]) + parser.add_argument("--root_override", default=None) + parser.add_argument("--test_folder", default=None) + parser.add_argument("--ckpt", default=None) + parser.add_argument("--norm_stats", default=None) + parser.add_argument( + "--allow_missing_norm_stats", + action="store_true", + help="Permite inferência sem padronização (não recomendado).", + ) + parser.add_argument("--channels", type=int, default=None) + parser.add_argument("--resize_w", type=int, default=None) + parser.add_argument("--resize_h", type=int, default=None) + parser.add_argument("--alpha", type=float, default=0.45) + parser.add_argument("--ignore_id", type=int, default=None) + parser.add_argument("--no_amp", action="store_true") + parser.add_argument("--require_masks", action="store_true") + parser.add_argument("--out_dir", default="outputs_test_multihead") + parser.add_argument("--start_idx", type=int, default=0) + parser.add_argument("--max_width", type=int, default=1800) + parser.add_argument("--runtime_mode", default="all", choices=["all", "target_direct", "operational"]) + parser.add_argument( + "--target_threshold", + default="0.5", + help="Threshold da head target direta. Use número (ex. 0.7) ou 'auto' para usar o operational_threshold salvo no checkpoint.", + ) + parser.add_argument("--onnx", default="") + parser.add_argument("--onnx_provider", default="", choices=["", "cpu", "cuda", "tensorrt"]) + parser.add_argument("--trt_home", default=None) + parser.add_argument("--trt_no_fp16", action="store_true") + args = parser.parse_args() + + config_path = resolve_path(args.config, Path.cwd()) + if config_path is None or not config_path.is_file(): + raise FileNotFoundError(f"Config não encontrado: {config_path}") + + config_dir = config_path.parent + config = load_json(config_path) + config["runtime_mode"] = args.runtime_mode + + input_channel_names = get_input_channel_names(config, args.channels) + input_channel_indices = get_input_channel_indices(config, args.channels) + channels = len(input_channel_names) + + print(f"Input channels: {input_channel_names} idx={input_channel_indices}") + res = config.get("resolucao", [1024, 640]) + default_w, default_h = int(res[0]), int(res[1]) + target_w = int(args.resize_w or default_w) + target_h = int(args.resize_h or default_h) + module_params_config = config.get("module_params_json") + if module_params_config: + module_params_candidate = Path(str(module_params_config)) + if not module_params_candidate.is_absolute(): + module_params_candidate = config_dir / module_params_candidate + module_params_config = str(module_params_candidate.resolve()) + + dataset_path = config_dir / "dataset" + labelmap_path = dataset_path / "labelmap.txt" + semantic_id2label, semantic_label2id, labelmap_ignore_id, loaded_colormap_rgb = load_labelmap(labelmap_path) + + ignore_id = int(args.ignore_id if args.ignore_id is not None else labelmap_ignore_id) + heads_config = build_heads_config(config, ignore_index=ignore_id) + heads_config["semantic"]["num_classes"] = int(len(semantic_id2label)) + heads_config["semantic"]["ignore_index"] = int(ignore_id) + + semantic_cmap = dict(SEMANTIC_COLORS_RGB) + semantic_cmap.update({int(k): tuple(map(int, v)) for k, v in loaded_colormap_rgb.items()}) + + save_dir = infer_save_dir(config, config_dir, channels) + ckpt_path = find_checkpoint(save_dir, args.ckpt) + + if args.norm_stats is not None: + norm_stats_path = resolve_path(args.norm_stats, Path.cwd()) + else: + # O treino multihead usa stats do dataset normalizado, mas também tentamos alguns fallbacks. + candidates = [ + save_dir / "norm_stats.json", + dataset_path / f"{default_w}x{default_h}" / "group" / "norm_stats.json", + config_dir / "backup" / config.get("modelo", "segformer_b1") / config.get("model_name", "test_multi") / config.get("stats_source_tag", "stacked_raw5") / "norm_stats.json", + ] + norm_stats_path = next((p for p in candidates if p.is_file()), candidates[0]) + + mean, std = load_norm_stats(norm_stats_path, channel_names=input_channel_names) + if (mean is None or std is None) and not args.allow_missing_norm_stats: + raise FileNotFoundError( + f"norm_stats não encontrado em {norm_stats_path}. A inferência deve usar os " + "mesmos stats do treinamento. Informe --norm_stats ou, conscientemente, " + "use --allow_missing_norm_stats." + ) + + if args.test_folder is not None: + root = resolve_path(args.test_folder, Path.cwd()) + elif args.root_override is not None: + root = resolve_path(args.root_override, Path.cwd()) + else: + root = (dataset_path / "split" / args.split_folder).resolve() + + if root is None or not root.is_dir(): + raise FileNotFoundError(f"Root de dados não encontrado: {root}") + + samples, has_gt = collect_samples(root, heads_config=heads_config, require_masks=args.require_masks) + n = len(samples) + + print("==========================================") + print("Teste SegFormer OAK-FCC-3 Multi-Head") + print(f"Root : {root}") + print(f"Samples : {n}") + print(f"GT : {'sim' if has_gt else 'não'}") + print(f"Resolution : {target_w}x{target_h}") + print(f"Channels : {channels}") + print(f"Semantic : {semantic_id2label}") + print(f"Ignore index: {ignore_id}") + print("Heads:") + for name, hcfg in heads_config.items(): + print(f" - {name}: classes={hcfg['num_classes']} mask_dir={hcfg['mask_dir']}") + print("==========================================") + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Device: {device}") + + tester = MultiHeadTester( + config=config, + ckpt_path=ckpt_path, + device=device, + input_channel_names=input_channel_names, + heads_config=heads_config, + semantic_id2label=semantic_id2label, + semantic_label2id=semantic_label2id, + mean=mean, + std=std, + use_amp=not args.no_amp, + ) + + target_threshold_arg = str(args.target_threshold).strip().lower() + if target_threshold_arg == "auto": + if tester.ckpt_operational_threshold is None: + tester.target_threshold = 0.5 + print("[TARGET][WARN] checkpoint não possui operational_threshold; usando 0.5") + else: + tester.target_threshold = float(tester.ckpt_operational_threshold) + print(f"[TARGET] threshold=auto -> {tester.target_threshold:.3f} (salvo no checkpoint)") + else: + try: + tester.target_threshold = float(target_threshold_arg) + except ValueError as exc: + raise RuntimeError(f"--target_threshold inválido: {args.target_threshold}") from exc + if not (0.0 <= tester.target_threshold <= 1.0): + raise RuntimeError("--target_threshold deve ficar entre 0 e 1.") + print(f"[TARGET] threshold direto={tester.target_threshold:.3f}") + + onnx_tester = None + onnx_path = None + + if args.onnx_provider: + onnx_path = find_onnx_model( + save_dir=save_dir, + ckpt_path=ckpt_path, + preferred=args.onnx, + ) + + onnx_tester = OnnxMultiHeadTester( + config=config, + onnx_path=onnx_path, + provider=args.onnx_provider, + input_channel_names=input_channel_names, + heads_config=heads_config, + mean=mean, + std=std, + trt_home=args.trt_home, + trt_fp16=not args.trt_no_fp16, + ) + onnx_tester.target_threshold = float(tester.target_threshold) + + if onnx_tester is not None: + print(f"ONNX : {onnx_path}") + print(f"ONNX provider: {args.onnx_provider}") + + out_dir = Path(args.out_dir) + ensure_dir(out_dir) + + idx = max(0, min(args.start_idx, n - 1)) + detailed = False + + cms_total = { + name: np.zeros((int(hcfg["num_classes"]), int(hcfg["num_classes"])), dtype=np.int64) + for name, hcfg in heads_config.items() + } + cm_target_total = np.zeros((2, 2), dtype=np.int64) + visited = set() + + win_name = "OAK-FCC-3 MultiHead Test | D/A navega | S salva | SPACE detalhado | Q sai" + cv2.namedWindow(win_name, cv2.WINDOW_NORMAL) + win_name_onnx = None + if onnx_tester is not None: + win_name_onnx = "ONNX/TensorRT MultiHead Test | comparação visual" + cv2.namedWindow(win_name_onnx, cv2.WINDOW_NORMAL) + + while True: + sample = samples[idx] + chw = load_sample_tensor( + sample, + requested_channels=input_channel_names, + derived_config=config.get("derived_channels", {}) or {}, + target_size=(target_w, target_h), + module_params_path=module_params_config, + ) + + if (chw.shape[2], chw.shape[1]) != (target_w, target_h): + hwc = np.transpose(chw, (1, 2, 0)) + hwc = cv2.resize(hwc, (target_w, target_h), interpolation=cv2.INTER_LINEAR) + chw = np.transpose(hwc, (2, 0, 1)).astype(np.float32) + + preds, probs, t_inf = tester.infer(chw) + preview_rgb = tensor_to_preview_rgb(chw, input_channel_names) + + onnx_preds = None + onnx_probs = None + t_onnx = None + + if onnx_tester is not None: + onnx_preds, onnx_probs, t_onnx = onnx_tester.infer(chw) + + first_pred = next(iter(preds.values())) + pred_h, pred_w = first_pred.shape[:2] + + pred_sem = preds.get("semantic") + pred_veg = preds.get("vegetation") + pred_cana = preds.get("cana") + + # Target operacional antigo: vegetation AND not cana + pred_target_op = None + if pred_veg is not None and pred_cana is not None: + pred_target_op = operational_target_mask(pred_veg, pred_cana, ignore_id=ignore_id) + + # Target direta nova: saída da 4ª cabeça + pred_target_head = preds.get("target") + + # Fallback visual: se não tiver head target, mostra operacional + pred_target = pred_target_head if pred_target_head is not None else pred_target_op + + prob_veg = probs["vegetation"][1] if "vegetation" in probs and probs["vegetation"].shape[0] > 1 else None + prob_cana = probs["cana"][1] if "cana" in probs and probs["cana"].shape[0] > 1 else None + + prob_target_op = None + if prob_veg is not None and prob_cana is not None: + prob_target_op = np.clip(prob_veg * (1.0 - prob_cana), 0.0, 1.0) + + prob_target_head = None + if "target" in probs: + prob_target_head = probs["target"][1] if probs["target"].shape[0] > 1 else probs["target"][0] + + prob_target = prob_target_head if prob_target_head is not None else prob_target_op + + gt_masks = {name: load_mask(path) for name, path in sample.masks.items()} + for name, gt in list(gt_masks.items()): + if gt is not None and gt.shape != (pred_h, pred_w): + gt_masks[name] = cv2.resize( + gt.astype(np.uint8), + (pred_w, pred_h), + interpolation=cv2.INTER_NEAREST, + ) + + gt_target = None + if gt_masks.get("target") is not None: + gt_target = gt_masks["target"] + elif gt_masks.get("vegetation") is not None and gt_masks.get("cana") is not None: + gt_target = operational_target_mask( + gt_masks["vegetation"], + gt_masks["cana"], + ignore_id=ignore_id, + ) + gt_masks["target"] = gt_target + + # Métricas da amostra. + metric_lines = [] + sample_metrics = {} + for head_name, pred in preds.items(): + gt = gt_masks.get(head_name) + if gt is not None: + cm = confusion_matrix_np(pred, gt, int(heads_config[head_name]["num_classes"]), ignore_id) + iou, miou, acc = metrics_from_cm(cm) + sample_metrics[head_name] = {"iou": iou, "miou": miou, "acc": acc} + metric_lines.append(f"{head_name}: mIoU={miou:.3f} acc={acc:.3f}") + + if gt_target is not None and pred_target_op is not None: + cm_t = confusion_matrix_np(pred_target_op, gt_target, 2, ignore_id) + iou_t, miou_t, acc_t = metrics_from_cm(cm_t) + + sample_metrics["target_op"] = { + "iou": iou_t, + "miou": miou_t, + "acc": acc_t, + } + + metric_lines.append( + f"target_op: IoU_alvo={iou_t[1]:.3f} acc={acc_t:.3f}" + ) + + if "target" in sample_metrics: + iou_head = sample_metrics["target"]["iou"] + metric_lines.append( + f"target_head: IoU_alvo={iou_head[1]:.3f}" + ) + + if idx not in visited: + for head_name, pred in preds.items(): + gt = gt_masks.get(head_name) + if gt is not None: + cms_total[head_name] += confusion_matrix_np(pred, gt, int(heads_config[head_name]["num_classes"]), ignore_id) + if gt_target is not None and pred_target_op is not None: + cm_target_total += confusion_matrix_np(pred_target_op, gt_target, 2, ignore_id) + visited.add(idx) + + # Visuals. + panels: List[Tuple[str, np.ndarray, str]] = [] + if pred_sem is not None: + pred_sem_rgb = ids_to_rgb(pred_sem, semantic_cmap, ignore_id) + if gt_masks.get("semantic") is not None: + panels.append(("GT semantic", ids_to_rgb(gt_masks["semantic"], semantic_cmap, ignore_id), "chao/cana/erva")) + panels.append(("Pred semantic", pred_sem_rgb, f"erva={class_percent(pred_sem, 2, ignore_id):.1f}% cana={class_percent(pred_sem, 1, ignore_id):.1f}%")) + panels.append(("Overlay semantic", overlay_rgb(preview_rgb, pred_sem_rgb, args.alpha), "")) + + if pred_veg is not None: + pred_veg_rgb = ids_to_rgb(pred_veg, BINARY_COLORS_RGB, ignore_id) + if gt_masks.get("vegetation") is not None: + panels.append(("GT vegetation", ids_to_rgb(gt_masks["vegetation"], BINARY_COLORS_RGB, ignore_id), "0=fundo 1=veg")) + panels.append(("Pred vegetation", pred_veg_rgb, f"veg={class_percent(pred_veg, 1, ignore_id):.1f}%")) + if prob_veg is not None: + panels.append(("P vegetation", prob_to_heat_rgb(prob_veg), f"mean={float(prob_veg.mean()):.3f}")) + + if pred_cana is not None: + pred_cana_rgb = ids_to_rgb(pred_cana, CANA_COLORS_RGB, ignore_id) + if gt_masks.get("cana") is not None: + panels.append(("GT cana", ids_to_rgb(gt_masks["cana"], CANA_COLORS_RGB, ignore_id), "0=not_cana 1=cana")) + panels.append(("Pred cana", pred_cana_rgb, f"cana={class_percent(pred_cana, 1, ignore_id):.1f}%")) + if prob_cana is not None: + panels.append(("P cana", prob_to_heat_rgb(prob_cana), f"mean={float(prob_cana.mean()):.3f}")) + + if detailed: + if gt_target is not None: + panels.append(("GT target", ids_to_rgb(gt_target, TARGET_COLORS_RGB, ignore_id), "derivado")) + + if pred_target_head is not None: + panels.append(("Pred target HEAD", ids_to_rgb(pred_target_head, TARGET_COLORS_RGB, ignore_id), f"thr={tester.target_threshold:.2f} alvo={class_percent(pred_target_head, 1, ignore_id):.1f}%")) + if prob_target_head is not None: + panels.append(("P target HEAD", prob_to_heat_rgb(prob_target_head), f"mean={float(prob_target_head.mean()):.3f}")) + + if pred_target_op is not None: + panels.append(("Pred target OP", ids_to_rgb(pred_target_op, TARGET_COLORS_RGB, ignore_id), f"alvo={class_percent(pred_target_op, 1, ignore_id):.1f}%")) + if prob_target_op is not None: + panels.append(("P target OP", prob_to_heat_rgb(prob_target_op), f"mean={float(prob_target_op.mean()):.3f}")) + else: + if pred_target_head is not None: + rgb = ids_to_rgb(pred_target_head, TARGET_COLORS_RGB, ignore_id) + panels.append(("Pred target HEAD", rgb, f"thr={tester.target_threshold:.2f} alvo={class_percent(pred_target_head, 1, ignore_id):.1f}%")) + panels.append(("Overlay target HEAD", overlay_rgb(preview_rgb, rgb, args.alpha), "head direta")) + elif pred_target_op is not None: + rgb = ids_to_rgb(pred_target_op, TARGET_COLORS_RGB, ignore_id) + panels.append(("Pred target OP", rgb, f"alvo={class_percent(pred_target_op, 1, ignore_id):.1f}%")) + panels.append(("Overlay target OP", overlay_rgb(preview_rgb, rgb, args.alpha), "veg & !cana")) + + canvas = compose_grid(panels, cols=3, max_width=args.max_width) + + header_h = 78 + header = np.zeros((header_h, canvas.shape[1], 3), dtype=np.uint8) + header[:] = (25, 25, 25) + source_name = sample.tensor_path.name if sample.tensor_path is not None else f"{sample.base}.json [{sample.source_kind}]" + h1 = f"idx {idx + 1}/{n} | {source_name} | inf={t_inf:.1f}ms | {'detalhado' if detailed else 'compacto'}" + h2 = " | ".join(metric_lines[:3]) if metric_lines else "sem GT" + cv2.putText(header, h1, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (235, 235, 235), 1, cv2.LINE_AA) + cv2.putText(header, h2, (12, 58), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (0, 255, 120), 1, cv2.LINE_AA) + canvas = np.vstack([header, canvas]) + + onnx_canvas = None + if onnx_tester is not None and onnx_preds is not None and onnx_probs is not None: + onnx_pred_sem = onnx_preds.get("semantic") + onnx_pred_veg = onnx_preds.get("vegetation") + onnx_pred_cana = onnx_preds.get("cana") + + onnx_pred_target_op = None + if onnx_pred_veg is not None and onnx_pred_cana is not None: + onnx_pred_target_op = operational_target_mask( + onnx_pred_veg, + onnx_pred_cana, + ignore_id=ignore_id, + ) + + onnx_pred_target_head = onnx_preds.get("target") + onnx_pred_target = onnx_pred_target_head if onnx_pred_target_head is not None else onnx_pred_target_op + + onnx_prob_veg = onnx_probs["vegetation"][1] if "vegetation" in onnx_probs and onnx_probs["vegetation"].shape[0] > 1 else None + onnx_prob_cana = onnx_probs["cana"][1] if "cana" in onnx_probs and onnx_probs["cana"].shape[0] > 1 else None + + onnx_prob_target_op = None + if onnx_prob_veg is not None and onnx_prob_cana is not None: + onnx_prob_target_op = np.clip(onnx_prob_veg * (1.0 - onnx_prob_cana), 0.0, 1.0) + + onnx_prob_target_head = None + if "target" in onnx_probs: + onnx_prob_target_head = onnx_probs["target"][1] if onnx_probs["target"].shape[0] > 1 else onnx_probs["target"][0] + + onnx_prob_target = onnx_prob_target_head if onnx_prob_target_head is not None else onnx_prob_target_op + + eq_sem = compare_pred_equal_percent(pred_sem, onnx_pred_sem) + eq_veg = compare_pred_equal_percent(pred_veg, onnx_pred_veg) + eq_cana = compare_pred_equal_percent(pred_cana, onnx_pred_cana) + eq_target = compare_pred_equal_percent(pred_target, onnx_pred_target) + + onnx_panels: List[Tuple[str, np.ndarray, str]] = [] + + if onnx_pred_sem is not None: + onnx_sem_rgb = ids_to_rgb(onnx_pred_sem, semantic_cmap, ignore_id) + onnx_panels.append(( + "ONNX semantic", + onnx_sem_rgb, + "" if eq_sem is None else f"igual PT={eq_sem:.3f}%" + )) + onnx_panels.append(( + "ONNX overlay semantic", + overlay_rgb(preview_rgb, onnx_sem_rgb, args.alpha), + "" + )) + + d = diff_mask_rgb(pred_sem, onnx_pred_sem) + if d is not None: + onnx_panels.append(("Diff semantic", d, "vermelho=diferente")) + + if onnx_pred_veg is not None: + onnx_veg_rgb = ids_to_rgb(onnx_pred_veg, BINARY_COLORS_RGB, ignore_id) + onnx_panels.append(( + "ONNX vegetation", + onnx_veg_rgb, + "" if eq_veg is None else f"igual PT={eq_veg:.3f}%" + )) + + if onnx_prob_veg is not None: + onnx_panels.append(( + "ONNX P vegetation", + prob_to_heat_rgb(onnx_prob_veg), + f"mean={float(onnx_prob_veg.mean()):.3f}" + )) + + if onnx_pred_cana is not None: + onnx_cana_rgb = ids_to_rgb(onnx_pred_cana, CANA_COLORS_RGB, ignore_id) + onnx_panels.append(( + "ONNX cana", + onnx_cana_rgb, + "" if eq_cana is None else f"igual PT={eq_cana:.3f}%" + )) + + if onnx_prob_cana is not None: + onnx_panels.append(( + "ONNX P cana", + prob_to_heat_rgb(onnx_prob_cana), + f"mean={float(onnx_prob_cana.mean()):.3f}" + )) + + if onnx_pred_target is not None: + onnx_target_rgb = ids_to_rgb(onnx_pred_target, TARGET_COLORS_RGB, ignore_id) + title = "ONNX target HEAD" if onnx_pred_target_head is not None else "ONNX target OP" + + onnx_panels.append(( + title, + onnx_target_rgb, + "" if eq_target is None else f"igual PT={eq_target:.3f}%" + )) + onnx_panels.append(( + "ONNX overlay target", + overlay_rgb(preview_rgb, onnx_target_rgb, args.alpha), + f"inf={t_onnx:.1f}ms" + )) + + if onnx_prob_target is not None: + onnx_panels.append(( + "ONNX P target", + prob_to_heat_rgb(onnx_prob_target), + f"mean={float(onnx_prob_target.mean()):.3f}" + )) + + d = diff_mask_rgb(pred_target, onnx_pred_target) + if d is not None: + onnx_panels.append(("Diff target", d, "vermelho=diferente")) + + onnx_canvas = compose_grid(onnx_panels, cols=3, max_width=args.max_width) + + header_h_onnx = 78 + header_onnx = np.zeros((header_h_onnx, onnx_canvas.shape[1], 3), dtype=np.uint8) + header_onnx[:] = (18, 18, 35) + + h1_onnx = f"ONNX {args.onnx_provider} | {source_name} | inf={t_onnx:.1f}ms" + h2_parts = [] + if eq_sem is not None: + h2_parts.append(f"sem={eq_sem:.3f}%") + if eq_veg is not None: + h2_parts.append(f"veg={eq_veg:.3f}%") + if eq_cana is not None: + h2_parts.append(f"cana={eq_cana:.3f}%") + if eq_target is not None: + h2_parts.append(f"target={eq_target:.3f}%") + h2_onnx = "igual PyTorch: " + " | ".join(h2_parts) if h2_parts else "comparação indisponível" + + cv2.putText(header_onnx, h1_onnx, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (235, 235, 235), 1, cv2.LINE_AA) + cv2.putText(header_onnx, h2_onnx, (12, 58), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (0, 255, 120), 1, cv2.LINE_AA) + + onnx_canvas = np.vstack([header_onnx, onnx_canvas]) + + cv2.imshow(win_name, cv2.cvtColor(canvas, cv2.COLOR_RGB2BGR)) + if onnx_canvas is not None and win_name_onnx is not None: + cv2.imshow(win_name_onnx, cv2.cvtColor(onnx_canvas, cv2.COLOR_RGB2BGR)) + + k = cv2.waitKey(0) & 0xFF + + if k in (ord("q"), ord("Q"), 27): + break + elif k in (ord("d"), ord("D"), 83): + idx = (idx + 1) % n + elif k in (ord("a"), ord("A"), 81): + idx = (idx - 1 + n) % n + elif k == ord(" "): + detailed = not detailed + elif k in (ord("s"), ord("S")): + out_path = out_dir / f"multihead_pytorch_{idx:05d}_{sample.base}.png" + cv2.imwrite(str(out_path), cv2.cvtColor(canvas, cv2.COLOR_RGB2BGR)) + print(f"[SAVE] {out_path}") + + if onnx_canvas is not None: + out_path_onnx = out_dir / f"multihead_onnx_{args.onnx_provider}_{idx:05d}_{sample.base}.png" + cv2.imwrite(str(out_path_onnx), cv2.cvtColor(onnx_canvas, cv2.COLOR_RGB2BGR)) + print(f"[SAVE] {out_path_onnx}") + + cv2.destroyAllWindows() + + if visited: + print("\n========== RESUMO DOS SAMPLES VISITADOS ==========") + print(f"visitados={len(visited)}/{n}") + for head_name, cm in cms_total.items(): + if cm.sum() <= 0: + continue + iou, miou, acc = metrics_from_cm(cm) + print(f"\n[{head_name}] acc={acc:.4f} mIoU={miou:.4f}") + for i, v in enumerate(iou): + print(f" IoU {i} = {v:.4f}") + + if cm_target_total.sum() > 0: + iou, miou, acc = metrics_from_cm(cm_target_total) + print(f"\n[target operacional] acc={acc:.4f} mIoU={miou:.4f}") + print(f" IoU background = {iou[0]:.4f}") + print(f" IoU alvo = {iou[1]:.4f}") + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/config.json b/Python/OAK/datasets/oak-fcc-3/config.json index 8eeb400e9..574570075 100644 --- a/Python/OAK/datasets/oak-fcc-3/config.json +++ b/Python/OAK/datasets/oak-fcc-3/config.json @@ -1,18 +1,18 @@ { "camera": "oak-fcc-3", "modelo": "segformer_b1", - "model_name": "2026_08_18", + "model_name": "2026_08_28_linear_demosaic", "main_class_name": "erva", "es_classes": "", "model_to_use": "geral", "raw_size": [1280, 800], - "resolucao": [640, 400], + "resolucao": [960, 600], "roi_inicio": 0.0, "roi_tamanho": 1.0, "shaves": 3, "source_channels": ["R", "G", "B", "RE", "NIR"], - "channels": 7, - "input_channels": ["R", "G", "B", "RE", "NIR", "NDVI", "NDRE"], + "channels": 5, + "input_channels": ["R", "G", "B", "RE", "NIR"], "derived_channels": { "epsilon": 1e-6, "clip_min": -1.0, @@ -20,7 +20,7 @@ }, "backbone": "nvidia/mit-b1", "fusion_mode": "stacked", - "stats_source_tag": "stacked_raw5_ndvi_ndre", + "stats_source_tag": "stacked_raw5", "module_params_json": "calibration/module_params.json", "ckpt_test": "best_score", "multi_head": true, @@ -41,7 +41,7 @@ "mask_dir": "masks_vegetation", "classes": {"background": 0, "vegetation": 1}, "ignore_index": 255, - "loss_weight": 0.20 + "loss_weight": 0.15 }, "cana": { "enabled": true, @@ -50,7 +50,7 @@ "mask_dir": "masks_cana", "classes": {"not_cana": 0, "cana": 1}, "ignore_index": 255, - "loss_weight": 0.25 + "loss_weight": 0.35 }, "target": { "enabled": true, @@ -59,23 +59,158 @@ "mask_dir": "__derived_target__", "classes": {"background": 0, "target": 1}, "ignore_index": 255, - "loss_weight": 0.35, + "loss_weight": 0.30, "derived": true } }, - "target_distillation": { - "enabled": true, - "hard_weight": 0.85, - "distill_weight": 0.15, - "rampup_enabled": true, - "start_epoch": 8, - "rampup_epochs": 12, - "w_sem_erva": 0.45, - "w_veg_not_cana": 0.35, - "w_veg_suppressed": 0.20, - "cana_suppression_power": 1.5, - "teacher_min": 0.0, - "teacher_max": 1.0, - "detach_teacher": true + "training_v2": { + "model": { + "decoder_mode": "shared_light", + "spectral_input_init": "zero_extra" + }, + "augmentation": { + "enabled": true, + "horizontal_flip_p": 0.5, + "vertical_flip_p": 0.0, + "affine_p": 0.7, + "rotate_deg": 5.0, + "scale_min": 0.9, + "scale_max": 1.1, + "translate_frac": 0.04, + "crop_p": 0.45, + "crop_scale_min": 0.7, + "crop_scale_max": 1.0, + "crop_focus_target_p": 0.55, + "crop_focus_cana_p": 0.25, + "global_gain_p": 0.35, + "global_gain_min": 0.92, + "global_gain_max": 1.08, + "band_gain_p": 0.25, + "band_gain_min": 0.96, + "band_gain_max": 1.04, + "rgb_gamma_p": 0.2, + "rgb_gamma_min": 0.94, + "rgb_gamma_max": 1.06, + "noise_p": 0.2, + "noise_sigma_min": 0.001, + "noise_sigma_max": 0.008, + "blur_p": 0.12, + "blur_kernel": 3, + "sensor_channel_dropout_p": 0.0, + "clip_physical": true + }, + "sampler": { + "mode": "diverse", + "samples_per_epoch": 0, + "tiny_target_pct": 0.005, + "small_target_pct": 0.02, + "medium_target_pct": 0.1 + }, + "class_weighting": { + "method": "log_inverse", + "log_offset": 1.02, + "power": 0.5, + "min_weight": 0.25, + "max_weight": 4.0 + }, + "loss": { + "dice_reduction": "per_image", + "dice_smooth": 1.0, + "boundary_weight": 0.0, + "ohem_ratio": 0.0, + "safety": { + "enabled": true, + "weight": 0.08, + "cana_weight": 1.0, + "ground_weight": 0.2 + } + }, + "optimizer": { + "encoder_lr": null, + "patch_lr_mult": 2.0, + "heads_lr_mult": 5.0, + "weight_decay": null, + "no_decay_bias": true, + "no_decay_norm": true, + "betas": [ + 0.9, + 0.999 + ], + "eps": 1e-08 + }, + "scheduler": { + "mode": "poly", + "warmup_ratio": 0.05, + "warmup_start_factor": 0.1, + "poly_power": 1.0, + "min_lr_ratio": 0.02 + }, + "optimization": { + "grad_clip_norm": 1.0, + "matmul_precision": "high", + "cudnn_benchmark": true, + "persistent_workers": true, + "prefetch_factor": 2 + }, + "target_distillation": { + "enabled": false, + "mode": "cross_head", + "start_epoch": 8, + "rampup_epochs": 12, + "hard_weight": 0.75, + "distill_weight": 0.25, + "teacher_confidence_min": 0.6, + "detach_teacher": true, + "w_sem_erva": 0.45, + "w_veg_not_cana": 0.35, + "w_veg_suppressed": 0.2, + "cana_suppression_power": 1.5 + }, + "metrics": { + "target_thresholds": [ + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9 + ], + "ece_bins": 15, + "scenario_metrics": true, + "group_metrics": true, + "rich_train_metrics": false, + "operational_threshold": { + "max_cana_spray_rate": 0.02, + "max_ground_spray_rate": 0.03, + "max_weed_miss_rate": 0.2, + "score_weights": { + "target_iou": 0.35, + "target_f1": 0.2, + "cana_safety": 0.25, + "ground_safety": 0.1, + "weed_recall": 0.1 + } + } + }, + "selection_score": { + "target_iou": 0.4, + "cana_iou": 0.2, + "target_f1": 0.1, + "vegetation_miou": 0.1, + "semantic_miou": 0.05, + "cana_safety": 0.15 + }, + "checkpoint": { + "early_stop_min_delta": 0.0005, + "save_best_safety": true, + "save_best_legacy": true, + "save_best_operational": true + }, + "data": { + "validate_npy_content": true, + "skip_corrupt_samples": true, + "max_corrupt_fraction": 0.005 + } } } \ No newline at end of file diff --git a/Python/OAK/datasets/oak-fcc-3/core/benchmark.py b/Python/OAK/datasets/oak-fcc-3/core/benchmark.py deleted file mode 100644 index 755895987..000000000 --- a/Python/OAK/datasets/oak-fcc-3/core/benchmark.py +++ /dev/null @@ -1,620 +0,0 @@ -import argparse -import json -import sys -import time -from pathlib import Path - -import cv2 -import numpy as np - -try: - import torch -except Exception: - torch = None - - -# ============================================================ -# Ajuste de import local -# ============================================================ - -THIS_FILE = Path(__file__).resolve() - -# Esperado: -# .../Python/Scripts/workers/camera_worker/oak_fcc3_core/benchmark_raw_bruto_scientific.py -WORKERS_DIR = THIS_FILE.parents[2] - -if str(WORKERS_DIR) not in sys.path: - sys.path.insert(0, str(WORKERS_DIR)) - -from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client - -try: - from camera_worker.oak_fcc3_core.segformer_service import MultiSpecSegformerService -except Exception: - MultiSpecSegformerService = None - - -# ============================================================ -# Utils -# ============================================================ - -def now_ms(): - return time.perf_counter() * 1000.0 - - -def mean(xs): - return float(np.mean(xs)) if xs else 0.0 - - -def p95(xs): - return float(np.percentile(xs, 95)) if xs else 0.0 - - -def maxv(xs): - return float(np.max(xs)) if xs else 0.0 - - -def last_mean(xs, n=30): - return float(np.mean(xs[-n:])) if xs else 0.0 - - -def last_max(xs, n=30): - return float(np.max(xs[-n:])) if xs else 0.0 - - -def load_json_if_exists(path): - if not path: - return None - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - - -def parse_float_list(s, expected=5, default=None): - if s is None: - return default - - vals = [float(x.strip()) for x in str(s).split(",") if x.strip() != ""] - - if len(vals) != expected: - raise ValueError(f"Esperado {expected} valores, veio {len(vals)}: {s}") - - return np.array(vals, dtype=np.float32) - - -def extract_mean_std_from_model_config(cfg): - if not isinstance(cfg, dict): - return None, None - - mean_cfg = cfg.get("mean") or cfg.get("channel_mean") or cfg.get("norm_mean") - std_cfg = cfg.get("std") or cfg.get("channel_std") or cfg.get("norm_std") - - norm = cfg.get("normalization") or cfg.get("norm") or {} - - if mean_cfg is None and isinstance(norm, dict): - mean_cfg = norm.get("mean") or norm.get("channel_mean") - - if std_cfg is None and isinstance(norm, dict): - std_cfg = norm.get("std") or norm.get("channel_std") - - if mean_cfg is None or std_cfg is None: - return None, None - - mean_arr = np.array(mean_cfg, dtype=np.float32) - std_arr = np.array(std_cfg, dtype=np.float32) - - if mean_arr.size != 5 or std_arr.size != 5: - return None, None - - return mean_arr, std_arr - - -def tensor_stats(tensor): - names = ["R", "G", "B", "RE", "NIR"] - out = {} - - for i, name in enumerate(names): - ch = tensor[i].astype(np.float32) - out[name] = { - "min": float(np.min(ch)), - "p01": float(np.percentile(ch, 1)), - "p50": float(np.percentile(ch, 50)), - "p99": float(np.percentile(ch, 99)), - "max": float(np.max(ch)), - "mean": float(np.mean(ch)), - "std": float(np.std(ch)), - } - - return out - - -def print_tensor_stats(label, tensor): - print("============================================") - print(f"[{label}] TENSOR") - print(f"shape={tensor.shape} dtype={tensor.dtype}") - - stats = tensor_stats(tensor) - for ch, s in stats.items(): - print( - f"{ch:>3} | " - f"min={s['min']:.4f} " - f"p01={s['p01']:.4f} " - f"p50={s['p50']:.4f} " - f"p99={s['p99']:.4f} " - f"max={s['max']:.4f} " - f"mean={s['mean']:.4f} " - f"std={s['std']:.4f}" - ) - - print("============================================") - - -def to_u8_01(arr): - arr = np.asarray(arr, dtype=np.float32) - arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=0.0) - arr = np.clip(arr, 0.0, 1.0) - return (arr * 255.0).astype(np.uint8) - - -def make_panel(tensor): - rgb = np.stack([tensor[0], tensor[1], tensor[2]], axis=2) - rgb_bgr = cv2.cvtColor(to_u8_01(rgb), cv2.COLOR_RGB2BGR) - - re_bgr = cv2.cvtColor(to_u8_01(tensor[3]), cv2.COLOR_GRAY2BGR) - nir_bgr = cv2.cvtColor(to_u8_01(tensor[4]), cv2.COLOR_GRAY2BGR) - - false_rgb = np.stack( - [ - tensor[4], # visual R = NIR - tensor[3], # visual G = RE - tensor[0], # visual B = R real - ], - axis=2, - ) - false_bgr = cv2.cvtColor(to_u8_01(false_rgb), cv2.COLOR_RGB2BGR) - - def title(img, text): - h, w = img.shape[:2] - bar_h = 34 - bar = np.zeros((bar_h, w, 3), dtype=np.uint8) - cv2.putText( - bar, - text, - (10, 24), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (255, 255, 255), - 2, - cv2.LINE_AA, - ) - return np.vstack([bar, img]) - - rgb_bgr = title(rgb_bgr, "RGB") - re_bgr = title(re_bgr, "RE") - nir_bgr = title(nir_bgr, "NIR") - false_bgr = title(false_bgr, "Falso color NIR/RE/R") - - top = np.hstack([rgb_bgr, re_bgr]) - bottom = np.hstack([nir_bgr, false_bgr]) - - return np.vstack([top, bottom]) - - -# ============================================================ -# Benchmark processor -# ============================================================ - -class ScientificBenchmark: - def __init__(self, args): - self.args = args - self.target_size = (int(args.width), int(args.height)) - - self.model_cfg = load_json_if_exists(args.model_config_json) - mean_cfg, std_cfg = extract_mean_std_from_model_config(self.model_cfg) - - if args.model_mean is not None: - self.mean = parse_float_list(args.model_mean, expected=5) - elif mean_cfg is not None: - self.mean = mean_cfg - else: - self.mean = np.array([0.5, 0.5, 0.5, 0.5, 0.5], dtype=np.float32) - - if args.model_std is not None: - self.std = parse_float_list(args.model_std, expected=5) - elif std_cfg is not None: - self.std = std_cfg - else: - self.std = np.array([0.25, 0.25, 0.25, 0.25, 0.25], dtype=np.float32) - - self.mean_chw = self.mean[:, None, None].astype(np.float32) - self.std_chw = self.std[:, None, None].astype(np.float32) - - self.device = None - if torch is not None and torch.cuda.is_available(): - self.device = torch.device("cuda") - elif torch is not None: - self.device = torch.device("cpu") - - self.model_svc = None - - if args.run_model: - if MultiSpecSegformerService is None: - raise RuntimeError("MultiSpecSegformerService não pôde ser importado.") - - if not isinstance(self.model_cfg, dict): - raise RuntimeError("--run_model requer --model_config_json válido.") - - self.model_svc = MultiSpecSegformerService( - model_config=self.model_cfg, - mostrar_log=print, - ) - - dummy = np.zeros((5, args.height, args.width), dtype=np.float32) - - for _ in range(max(0, int(args.warmup_model))): - self.model_svc.infer_tensor_fast(dummy, keep_probs=False) - - print(f"[BENCH] Warmup modelo concluído: {args.warmup_model}x") - - def process_once(self, client): - """ - Mede uma iteração completa do fluxo científico. - """ - times = { - "capture_ms": 0.0, - "decode_ms": 0.0, - "controller_ms": 0.0, - "fuse_total_ms": 0.0, - "fuse_dark_ms": 0.0, - "fuse_radnorm_ms": 0.0, - "fuse_flat_ms": 0.0, - "fuse_prepare_ms": 0.0, - "fuse_warp_ms": 0.0, - "fuse_crop_resize_ms": 0.0, - "fuse_concat_ms": 0.0, - "model_norm_ms": 0.0, - "torch_ms": 0.0, - "infer_ms": 0.0, - "total_ms": 0.0, - "sync_dt_ms": 0.0, - } - - t_total0 = now_ms() - - # ======================================================== - # 1. Captura RAW_BRUTO - # ======================================================== - t0 = now_ms() - raw_frame, raw_meta = client.get_next_raw_frame(timeout=self.args.timeout) - times["capture_ms"] = now_ms() - t0 - - #cp = raw_meta.get("capture_perf", {}) - #print( - # "[CAP_ASYNC] " - # f"get_wait={cp.get('async_get_wait_ms',0):.2f}ms " - # f"age={cp.get('async_packet_age_ms',0):.2f}ms " - # f"seq={cp.get('async_packet_seq')} " - # f"thread_wait={cp.get('wait_total_ms',0):.2f}ms " - # f"sleep={cp.get('sleep_ms',0):.2f}ms/{cp.get('sleep_count',0)} " - # f"drain={cp.get('drain_total_ms',0):.2f}ms " - # f"copy={cp.get('drain_frombuffer_copy_ms',0):.2f}ms " - # f"status={cp.get('async_status',{})}" - #) - - times["sync_dt_ms"] = float(raw_meta.get("sync_dt_ms", 0.0) or 0.0) - - # ======================================================== - # 2. Decode RAW10 packed -> float científico por câmera - # ======================================================== - t0 = now_ms() - decoded = client.decode_stream_cameras(raw_frame, raw_meta) - times["decode_ms"] = now_ms() - t0 - - # ======================================================== - # 3. RadiometricController update, se ativo - # Isso NÃO é a radiometric_normalization do tensor. - # É o controlador de exposição/ganho. - # ======================================================== - t0 = now_ms() - client.update_radiometry(decoded, raw_meta) - times["controller_ms"] = now_ms() - t0 - - # ======================================================== - # 4. Fusão científica no RawProcessorCore - # dark + radnorm + flat + homografia + crop/resize + concat - # ======================================================== - t0 = now_ms() - tensor = client.build_infer_tensor_from_decoded( - decoded=decoded, - meta=raw_meta, - channels_expected=5, - target_size=self.target_size, - ) - times["fuse_total_ms"] = now_ms() - t0 - - # Pega detalhamento interno do core - try: - perf = (client.core.last_fusion_result or {}).get("perf", {}) or {} - times["fuse_dark_ms"] = float(perf.get("dark_ms", 0.0) or 0.0) - times["fuse_radnorm_ms"] = float(perf.get("radnorm_ms", 0.0) or 0.0) - times["fuse_flat_ms"] = float(perf.get("flat_ms", 0.0) or 0.0) - times["fuse_prepare_ms"] = float(perf.get("prepare_ms", 0.0) or 0.0) - times["fuse_warp_ms"] = float(perf.get("warp_total_ms", 0.0) or 0.0) - times["fuse_crop_resize_ms"] = float(perf.get("crop_resize_ms", 0.0) or 0.0) - times["fuse_concat_ms"] = float(perf.get("concat_ms", 0.0) or 0.0) - except Exception: - pass - - # ======================================================== - # 5. Normalização do modelo, opcional - # ======================================================== - if self.args.simulate_model_norm: - t0 = now_ms() - tensor = (tensor - self.mean_chw) / np.maximum(self.std_chw, 1e-6) - tensor = np.ascontiguousarray(tensor, dtype=np.float32) - times["model_norm_ms"] = now_ms() - t0 - - # ======================================================== - # 6. Transferência para torch/cuda, opcional - # ======================================================== - if self.args.to_torch: - if torch is None: - raise RuntimeError("--to_torch requer torch instalado.") - - t0 = now_ms() - x = torch.from_numpy(tensor).unsqueeze(0).to(self.device, non_blocking=True) - - if self.device is not None and self.device.type == "cuda": - torch.cuda.synchronize() - - times["torch_ms"] = now_ms() - t0 - - # ======================================================== - # 7. Inferência real, opcional - # ======================================================== - pred = None - if self.args.run_model: - t0 = now_ms() - pred = self.model_svc.infer_tensor_fast(tensor, keep_probs=False) - times["infer_ms"] = now_ms() - t0 - - times["total_ms"] = now_ms() - t_total0 - - return tensor, pred, raw_frame, raw_meta, decoded, times - - -# ============================================================ -# Main -# ============================================================ - -def main(): - ap = argparse.ArgumentParser( - description="Benchmark científico OAK-FCC-3 RAW_BRUTO -> tensor multiespectral final." - ) - - ap.add_argument( - "--module_params", - default=r"C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\calibration\module_params.json", - help="Caminho do module_params.json.", - ) - - ap.add_argument("--width", type=int, default=1024, help="Largura final do tensor.") - ap.add_argument("--height", type=int, default=640, help="Altura final do tensor.") - ap.add_argument("--fps", type=float, default=30.0) - ap.add_argument("--seconds", type=float, default=20.0) - ap.add_argument("--timeout", type=float, default=3.0) - ap.add_argument("--warmup", type=int, default=5) - ap.add_argument("--mx_id", default=None) - ap.add_argument("--sync_tolerance_ms", type=float, default=25.0) - ap.add_argument("--buffer_size", type=int, default=8) - - ap.add_argument("--simulate_model_norm", action="store_true") - ap.add_argument("--model_mean", default=None) - ap.add_argument("--model_std", default=None) - ap.add_argument("--to_torch", action="store_true") - - ap.add_argument("--run_model", action="store_true") - ap.add_argument("--model_config_json", default=None) - ap.add_argument("--warmup_model", type=int, default=3) - - ap.add_argument("--save_debug", action="store_true") - ap.add_argument("--debug_dir", default="raw_bruto_scientific_benchmark") - ap.add_argument("--show", action="store_true") - ap.add_argument("--display_scale", type=float, default=0.65) - - args = ap.parse_args() - - bench = ScientificBenchmark(args) - - client = OakFcc3Client( - width=args.width, - height=args.height, - fps=args.fps, - frame_type="RAW_BRUTO", - output_dtype="uint8", - capture_mode="TRIPLE", - raw_policy="require_triple", - module_calibration_json=args.module_params, - sync_tolerance_ms=args.sync_tolerance_ms, - buffer_size=args.buffer_size, - mx_id=args.mx_id, - ) - - client.core.warmup_numba_raw10_decode() - - samples = { - "capture_ms": [], - "decode_ms": [], - "controller_ms": [], - "fuse_total_ms": [], - "fuse_dark_ms": [], - "fuse_radnorm_ms": [], - "fuse_flat_ms": [], - "fuse_prepare_ms": [], - "fuse_warp_ms": [], - "fuse_crop_resize_ms": [], - "fuse_concat_ms": [], - "model_norm_ms": [], - "torch_ms": [], - "infer_ms": [], - "total_ms": [], - "sync_dt_ms": [], - } - - n_frames = 0 - t_start = time.perf_counter() - t_last_log = t_start - - last_tensor = None - last_meta = None - - try: - client.start(print_debug=True) - - # Warmup de câmera/controlador/filas - print(f"[BENCH] Warmup frames: {args.warmup}") - for _ in range(max(0, int(args.warmup))): - try: - bench.process_once(client) - except Exception as e: - print(f"[WARN] warmup falhou: {type(e).__name__}: {e}") - time.sleep(0.02) - - print("============================================") - print("[BENCH] Iniciando benchmark científico RAW_BRUTO") - print(f"target tensor : (5,{args.height},{args.width})") - print(f"duration : {args.seconds}s") - print("============================================") - - while True: - now = time.perf_counter() - elapsed = now - t_start - - if elapsed >= args.seconds: - break - - try: - tensor, pred, raw_frame, raw_meta, decoded, times = bench.process_once(client) - except TimeoutError as e: - print(f"[TIMEOUT] {e}") - continue - - n_frames += 1 - last_tensor = tensor - last_meta = raw_meta - - for k in samples: - samples[k].append(float(times.get(k, 0.0) or 0.0)) - - if now - t_last_log >= 1.0: - elapsed = now - t_start - fps = n_frames / max(elapsed, 1e-6) - - print( - "[RAW_SCI_PERF] " - f"elapsed={elapsed:.1f}s " - f"frames={n_frames} " - f"fps={fps:.2f} " - f"sync={last_mean(samples['sync_dt_ms']):.2f}ms " - f"capture={last_mean(samples['capture_ms']):.2f}ms " - f"decode={last_mean(samples['decode_ms']):.2f}ms " - f"controller={last_mean(samples['controller_ms']):.2f}ms " - f"fuse={last_mean(samples['fuse_total_ms']):.2f}ms " - f"radnorm={last_mean(samples['fuse_radnorm_ms']):.2f}ms " - f"flat={last_mean(samples['fuse_flat_ms']):.2f}ms " - f"warp={last_mean(samples['fuse_warp_ms']):.2f}ms " - f"crop_resize={last_mean(samples['fuse_crop_resize_ms']):.2f}ms " - f"concat={last_mean(samples['fuse_concat_ms']):.2f}ms " - f"model_norm={last_mean(samples['model_norm_ms']):.2f}ms " - f"torch={last_mean(samples['torch_ms']):.2f}ms " - f"infer={last_mean(samples['infer_ms']):.2f}ms " - f"total={last_mean(samples['total_ms']):.2f}ms " - f"tensor_shape={tuple(tensor.shape)}" - ) - - t_last_log = now - - elapsed_total = time.perf_counter() - t_start - fps_total = n_frames / max(elapsed_total, 1e-6) - - print("============================================") - print("RESULTADO FINAL RAW_BRUTO CIENTÍFICO") - print(f"elapsed : {elapsed_total:.2f}s") - print(f"frames : {n_frames}") - print(f"fps : {fps_total:.2f}") - print("--------------------------------------------") - - def print_metric(name): - xs = samples[name] - print( - f"{name:18s} " - f"mean={mean(xs):8.2f}ms " - f"p95={p95(xs):8.2f}ms " - f"max={maxv(xs):8.2f}ms" - ) - - for name in [ - "sync_dt_ms", - "capture_ms", - "decode_ms", - "controller_ms", - "fuse_total_ms", - "fuse_dark_ms", - "fuse_radnorm_ms", - "fuse_flat_ms", - "fuse_prepare_ms", - "fuse_warp_ms", - "fuse_crop_resize_ms", - "fuse_concat_ms", - "model_norm_ms", - "torch_ms", - "infer_ms", - "total_ms", - ]: - print_metric(name) - - print("============================================") - - if last_tensor is not None: - print_tensor_stats("LAST RAW_BRUTO SCI", last_tensor) - - if args.save_debug: - debug_dir = Path(args.debug_dir) - debug_dir.mkdir(parents=True, exist_ok=True) - - np.save(str(debug_dir / "last_tensor.npy"), last_tensor) - - stats_path = debug_dir / "last_tensor_stats.json" - with open(stats_path, "w", encoding="utf-8") as f: - json.dump(tensor_stats(last_tensor), f, indent=2, ensure_ascii=False) - - panel = make_panel(last_tensor) - cv2.imwrite(str(debug_dir / "last_tensor_panel.png"), panel) - - if last_meta is not None: - with open(debug_dir / "last_meta.json", "w", encoding="utf-8") as f: - json.dump(last_meta, f, indent=2, ensure_ascii=False) - - print(f"[SAVE] Debug salvo em: {debug_dir}") - - if args.show: - panel = make_panel(last_tensor) - - if args.display_scale and abs(args.display_scale - 1.0) > 1e-6: - new_w = max(1, int(panel.shape[1] * args.display_scale)) - new_h = max(1, int(panel.shape[0] * args.display_scale)) - panel = cv2.resize(panel, (new_w, new_h), interpolation=cv2.INTER_AREA) - - cv2.imshow("RAW_BRUTO scientific tensor", panel) - print("[INFO] Pressione qualquer tecla para fechar.") - cv2.waitKey(0) - cv2.destroyAllWindows() - - finally: - try: - client.stop() - except Exception: - pass - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/Python/OAK/datasets/oak-fcc-3/core/benchmark_preview.py b/Python/OAK/datasets/oak-fcc-3/core/benchmark_preview.py deleted file mode 100644 index 407a3bbbe..000000000 --- a/Python/OAK/datasets/oak-fcc-3/core/benchmark_preview.py +++ /dev/null @@ -1,645 +0,0 @@ -import argparse -import json -import sys -import time -import threading -from pathlib import Path - -import cv2 -import numpy as np - -# ============================================================ -# Ajuste de import local -# ============================================================ -THIS_FILE = Path(__file__).resolve() -WORKERS_DIR = THIS_FILE.parents[2] -if str(WORKERS_DIR) not in sys.path: - sys.path.insert(0, str(WORKERS_DIR)) - -from camera_worker.oak_fcc3_core.oak_fcc3_client import OakFcc3Client - -try: - from camera_worker.oak_fcc3_core.segformer_service import MultiSpecSegformerService -except Exception: - MultiSpecSegformerService = None - - -# ============================================================ -# FPS / utilidades -# ============================================================ -class FpsMeter: - def __init__(self, alpha=0.15): - self.alpha = float(alpha) - self.last_ts = None - self.fps = 0.0 - - def tick(self): - now = time.perf_counter() - if self.last_ts is not None: - dt = now - self.last_ts - if dt > 1e-9: - inst = 1.0 / dt - self.fps = inst if self.fps <= 0 else (1.0 - self.alpha) * self.fps + self.alpha * inst - self.last_ts = now - return self.fps - - -def load_json_if_exists(path): - if not path: - return None - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - - -def init_model_service(args): - """ - Mesmo contrato do benchmark científico: - --run_model exige --model_config_json - MultiSpecSegformerService(model_config=cfg).infer_tensor_fast(tensor, keep_probs=False) - """ - if not args.run_model: - return None - - if MultiSpecSegformerService is None: - raise RuntimeError("MultiSpecSegformerService não pôde ser importado.") - - model_cfg = load_json_if_exists(args.model_config_json) - if not isinstance(model_cfg, dict): - raise RuntimeError("--run_model requer --model_config_json válido.") - - svc = MultiSpecSegformerService( - model_config=model_cfg, - mostrar_log=print, - ) - - dummy = np.zeros((5, int(args.height), int(args.width)), dtype=np.float32) - for _ in range(max(0, int(args.warmup_model))): - svc.infer_tensor_fast(dummy, keep_probs=False) - - print(f"[MODEL] Warmup concluído: {args.warmup_model}x") - return svc - - -def to_u8_01(arr, auto_level=False): - arr = np.asarray(arr, dtype=np.float32) - arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=0.0) - - if auto_level: - p1 = float(np.percentile(arr, 1)) - p99 = float(np.percentile(arr, 99)) - den = max(p99 - p1, 1e-6) - arr = (arr - p1) / den - - arr = np.clip(arr, 0.0, 1.0) - return (arr * 255.0).astype(np.uint8) - - -def ensure_bgr(img, auto_level=False): - img = np.asarray(img) - - if img.ndim == 2: - g = to_u8_01(img, auto_level=auto_level) - return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR) - - if img.ndim == 3 and img.shape[2] == 3: - u8 = to_u8_01(img, auto_level=auto_level) - # decoded/tensor RGB vem em RGB; OpenCV mostra BGR - return cv2.cvtColor(u8, cv2.COLOR_RGB2BGR) - - raise RuntimeError(f"Imagem inválida para visualização: shape={img.shape}") - - -def tensor_rgb_to_bgr(tensor, auto_level=False): - rgb = np.stack([tensor[0], tensor[1], tensor[2]], axis=2) - return ensure_bgr(rgb, auto_level=auto_level) - - -def tensor_channel_to_bgr(tensor, idx, auto_level=False): - return ensure_bgr(tensor[idx], auto_level=auto_level) - - -def add_title(img, title, color=(255, 255, 255)): - out = img.copy() - h, w = out.shape[:2] - bar_h = 34 - bar = np.zeros((bar_h, w, 3), dtype=np.uint8) - cv2.putText(bar, str(title), (10, 23), cv2.FONT_HERSHEY_SIMPLEX, 0.62, color, 2, cv2.LINE_AA) - return np.vstack([bar, out]) - - -def add_hud(panel, lines): - out = panel.copy() - x, y = 12, 45 - for line in lines: - cv2.putText(out, line, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.62, (0, 255, 255), 2, cv2.LINE_AA) - y += 24 - return out - - -def resize_tile(img, tile_w, tile_h): - return cv2.resize(img, (int(tile_w), int(tile_h)), interpolation=cv2.INTER_AREA) - - -def get_cam_by_role(decoded, role): - role = str(role).lower() - for cam_id, item in decoded.items(): - r = str(item.get("role") or item.get("meta", {}).get("role") or "").lower() - if r == role: - return cam_id - return None - - -def decoded_raw_tiles(decoded, tile_w, tile_h, auto_level=False): - """ - Retorna tiles RAW/decoded para RGB, RE, NIR antes da fusão final. - Aqui 'RAW_BRUTO' significa o conteúdo decodificado vindo das câmeras, ainda no espaço nativo. - """ - tiles = {} - - rgb_id = get_cam_by_role(decoded, "rgb") - re_id = get_cam_by_role(decoded, "re") - nir_id = get_cam_by_role(decoded, "nir") - - if rgb_id is not None: - img = decoded[rgb_id]["image"] - tiles["rgb"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h) - else: - tiles["rgb"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8) - - if re_id is not None: - img = decoded[re_id]["image"] - tiles["re"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h) - else: - tiles["re"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8) - - if nir_id is not None: - img = decoded[nir_id]["image"] - tiles["nir"] = resize_tile(ensure_bgr(img, auto_level=auto_level), tile_w, tile_h) - else: - tiles["nir"] = np.zeros((tile_h, tile_w, 3), dtype=np.uint8) - - return tiles - - -def tensor_tiles(tensor, tile_w, tile_h, auto_level=False): - return { - "rgb": resize_tile(tensor_rgb_to_bgr(tensor, auto_level=auto_level), tile_w, tile_h), - "re": resize_tile(tensor_channel_to_bgr(tensor, 3, auto_level=auto_level), tile_w, tile_h), - "nir": resize_tile(tensor_channel_to_bgr(tensor, 4, auto_level=auto_level), tile_w, tile_h), - } - - -def colorize_label_map(label_map, num_classes=None): - label = np.asarray(label_map) - if label.ndim == 3: - label = np.argmax(label, axis=0) - label = label.astype(np.int32) - - if num_classes is None: - num_classes = int(max(1, label.max() + 1)) - - # Paleta simples e estável. BGR. - palette = np.array([ - [40, 40, 40], - [60, 180, 60], - [60, 60, 220], - [220, 180, 60], - [180, 60, 180], - [180, 180, 60], - [60, 180, 180], - [220, 220, 220], - ], dtype=np.uint8) - - out = palette[label % len(palette)] - return out - - -def try_extract_prediction_tiles(pred, target_w, target_h): - """ - Tentativa genérica. Adapte aqui se o benchmark tiver nomes específicos das cabeças. - Retorna até 3 tiles BGR: semântica/head0/head1. - """ - if pred is None: - blank = np.zeros((target_h, target_w, 3), dtype=np.uint8) - return [blank, blank.copy(), blank.copy()], ["Pred vazio", "Head 1", "Head 2"] - - candidates = [] - names = [] - - if isinstance(pred, dict): - # nomes comuns - for key in ("mask", "pred_mask", "class_map", "semantic", "semantic_mask", "segmentation"): - if key in pred: - candidates.append(pred[key]) - names.append(key) - - heads = pred.get("heads") or pred.get("head_outputs") or pred.get("predictions") - if isinstance(heads, dict): - for k, v in heads.items(): - candidates.append(v) - names.append(str(k)) - elif isinstance(heads, (list, tuple)): - for i, v in enumerate(heads): - candidates.append(v) - names.append(f"head_{i}") - else: - candidates.append(pred) - names.append("prediction") - - tiles = [] - out_names = [] - - for name, arr in zip(names, candidates): - arr = np.asarray(arr) - - # remove batch se existir - if arr.ndim == 4 and arr.shape[0] == 1: - arr = arr[0] - - if arr.ndim == 3: - # CHW logits/probs ou HWC RGB/probs - if arr.shape[0] <= 32: - vis = colorize_label_map(np.argmax(arr, axis=0), num_classes=arr.shape[0]) - elif arr.shape[2] in (1, 3): - vis = ensure_bgr(arr[:, :, 0] if arr.shape[2] == 1 else arr, auto_level=True) - else: - vis = ensure_bgr(np.max(arr, axis=2), auto_level=True) - elif arr.ndim == 2: - # se parecer label map, colore; se parecer float, cinza auto-level - if np.issubdtype(arr.dtype, np.integer) and int(np.max(arr)) <= 64: - vis = colorize_label_map(arr) - else: - vis = ensure_bgr(arr, auto_level=True) - elif arr.ndim == 1: - # vetor de classe/score: desenha texto - vis = np.zeros((target_h, target_w, 3), dtype=np.uint8) - txt = np.array2string(arr[:8], precision=2, separator=", ") - cv2.putText(vis, txt[:80], (10, target_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA) - else: - continue - - vis = resize_tile(vis, target_w, target_h) - tiles.append(vis) - out_names.append(name) - - if len(tiles) >= 3: - break - - while len(tiles) < 3: - tiles.append(np.zeros((target_h, target_w, 3), dtype=np.uint8)) - out_names.append(f"pred_{len(tiles)}") - - return tiles[:3], out_names[:3] - - -def try_run_model(model_svc, tensor): - """ - Inferência real, igual ao benchmark científico. - Mantém esta função isolada para adaptar fácil caso o retorno do modelo mude. - """ - if model_svc is None: - return None, "model_svc_none" - - if hasattr(model_svc, "infer_tensor_fast"): - return model_svc.infer_tensor_fast(tensor, keep_probs=False), None - - if hasattr(model_svc, "infer"): - return model_svc.infer(tensor), None - - return None, "model_svc_sem_infer" - - -def build_grid(raw_tiles, final_tiles, pred_tiles=None, pred_names=None, tile_w=420, tile_h=260): - rows = [] - row_defs = [ - ("RGB", "rgb"), - ("RE", "re"), - ("NIR", "nir"), - ] - - for i, (label, key) in enumerate(row_defs): - left = add_title(raw_tiles[key], f"RAW_BRUTO decoded {label}") - mid = add_title(final_tiles[key], f"Tensor final {label}") - - cells = [left, mid] - - if pred_tiles is not None: - name = pred_names[i] if pred_names and i < len(pred_names) else f"Pred {i}" - cells.append(add_title(pred_tiles[i], name)) - - # iguala altura após título - h_min = min(c.shape[0] for c in cells) - norm = [cv2.resize(c, (tile_w, h_min), interpolation=cv2.INTER_AREA) if c.shape[1] != tile_w or c.shape[0] != h_min else c for c in cells] - rows.append(np.hstack(norm)) - - return np.vstack(rows) - - -def get_core_perf(client): - for attr in ("raw_processor", "processor", "core", "raw_processor_core"): - obj = getattr(client, attr, None) - if obj is not None and getattr(obj, "last_fusion_result", None) is not None: - return obj.last_fusion_result.get("perf", {}) or {} - return {} - - - -# ============================================================ -# Estado compartilhado / workers assíncronos -# ============================================================ -class SharedState: - def __init__(self): - self.lock = threading.Lock() - self.running = True - self.latest_decoded = None - self.latest_meta = None - self.latest_tensor = None - self.latest_perf = {} - self.latest_pred = None - self.latest_model_warn = None - self.latest_model_ms = 0.0 - self.latest_error = None - self.tensor_fps = FpsMeter() - self.model_fps = FpsMeter() - self.preview_fps = FpsMeter() - self.tensor_seq = 0 - self.model_seq = 0 - - def stop(self): - with self.lock: - self.running = False - - def is_running(self): - with self.lock: - return bool(self.running) - - -def tensor_worker(client, state, args): - """ - Roda no talo: captura RAW_BRUTO, monta tensor final e atualiza cache. - Não depende do FPS da janela. - """ - while state.is_running(): - try: - frame, meta, decoded = client.get_next_decoded(timeout=args.timeout) - if not isinstance(frame, dict): - raise RuntimeError(f"RAW_BRUTO esperado como dict. Veio {type(frame)}") - - tensor = client.build_infer_tensor_from_decoded( - decoded=decoded, - meta=meta, - channels_expected=5, - target_size=(args.width, args.height), - ) - tensor = np.ascontiguousarray(tensor.astype(np.float32, copy=False)) - perf = get_core_perf(client) - fps = state.tensor_fps.tick() - - with state.lock: - state.latest_decoded = decoded - state.latest_meta = meta - state.latest_tensor = tensor - state.latest_perf = dict(perf or {}) - state.tensor_seq += 1 - state.latest_error = None - - except Exception as e: - with state.lock: - state.latest_error = f"tensor_worker: {type(e).__name__}: {e}" - time.sleep(0.02) - - -def model_worker(model_svc, state, args): - """ - Opcional: roda inferência no último tensor disponível. - Não bloqueia o worker de tensor nem a janela. - """ - last_seq = -1 - - while state.is_running(): - with state.lock: - tensor = None if state.latest_tensor is None else state.latest_tensor.copy() - seq = state.tensor_seq - - if tensor is None or seq == last_seq: - time.sleep(0.005) - continue - - last_seq = seq - - try: - t0_model = time.perf_counter() - pred, warn = try_run_model(model_svc, tensor) - model_ms = (time.perf_counter() - t0_model) * 1000.0 - if warn is None: - state.model_fps.tick() - - with state.lock: - state.latest_pred = pred - state.latest_model_warn = warn - state.latest_model_ms = float(model_ms) - state.model_seq += 1 - - except Exception as e: - with state.lock: - state.latest_model_warn = f"model_worker: {type(e).__name__}: {e}" - time.sleep(0.02) - - -def snapshot_state(state): - """ - Copia referências do cache para desenhar. A janela só roda na cadência do preview. - """ - with state.lock: - return { - "decoded": state.latest_decoded, - "meta": state.latest_meta, - "tensor": state.latest_tensor, - "perf": dict(state.latest_perf or {}), - "pred": state.latest_pred, - "model_warn": state.latest_model_warn, - "model_ms": state.latest_model_ms, - "error": state.latest_error, - "tensor_fps": state.tensor_fps.fps, - "model_fps": state.model_fps.fps, - "tensor_seq": state.tensor_seq, - "model_seq": state.model_seq, - } - - -# ============================================================ -# Main loop assíncrono -# ============================================================ -def main(): - ap = argparse.ArgumentParser(description="Preview assíncrono RAW_BRUTO decoded vs tensor final multispectral.") - ap.add_argument("--module_params", default=r"C:\ZendionInc\agrobot_base\Python\OAK\datasets\oak-fcc-3\calibration\module_params.json") - ap.add_argument("--width", type=int, default=1024, help="Largura final do tensor.") - ap.add_argument("--height", type=int, default=640, help="Altura final do tensor.") - ap.add_argument("--fps", type=float, default=40.0, help="FPS alvo da câmera.") - ap.add_argument("--preview_fps", type=float, default=5.0, help="FPS da janela OpenCV apenas.") - ap.add_argument("--timeout", type=float, default=2.0) - ap.add_argument("--warmup", type=int, default=5) - ap.add_argument("--mx_id", default=None) - ap.add_argument("--display_scale", type=float, default=0.75) - ap.add_argument("--tile_w", type=int, default=420) - ap.add_argument("--tile_h", type=int, default=260) - ap.add_argument("--auto_level_raw", action="store_true") - ap.add_argument("--auto_level_tensor", action="store_true") - ap.add_argument("--run_model", action="store_true", help="Roda inferência em thread separada usando o último tensor cacheado.") - ap.add_argument("--model_config_json", default=None, help="JSON de configuração do modelo SegFormer, igual ao benchmark.") - ap.add_argument("--warmup_model", type=int, default=3, help="Número de inferências dummy para aquecer o modelo.") - ap.add_argument("--save_last", default=None) - args = ap.parse_args() - - client = OakFcc3Client( - width=args.width, - height=args.height, - fps=args.fps, - frame_type="RAW_BRUTO", - capture_mode="TRIPLE", - raw_policy="require_triple", - module_calibration_json=args.module_params, - mx_id=args.mx_id, - ) - - model_svc = init_model_service(args) if args.run_model else None - - state = SharedState() - last_panel = None - last_warn_ts = 0.0 - last_seq_drawn = -1 - - try: - client.start(print_debug=True) - - for _ in range(max(0, int(args.warmup))): - try: - client.get_next_decoded(timeout=args.timeout) - except Exception: - pass - time.sleep(0.03) - - tw = threading.Thread(target=tensor_worker, args=(client, state, args), daemon=True) - tw.start() - - mw = None - if args.run_model: - mw = threading.Thread(target=model_worker, args=(model_svc, state, args), daemon=True) - mw.start() - - min_period = 1.0 / max(float(args.preview_fps), 0.1) - next_draw_ts = 0.0 - - print("[INFO] Preview assíncrono iniciado. Pressione Q ou ESC para sair.") - print("[INFO] Tensor FPS = geração real do tensor. Preview FPS = janela. Model FPS = inferência, se habilitada.") - - while True: - now = time.perf_counter() - if now < next_draw_ts: - time.sleep(min(0.005, next_draw_ts - now)) - key = cv2.waitKey(1) & 0xFF - if key in (27, ord('q'), ord('Q')): - break - continue - - next_draw_ts = now + min_period - snap = snapshot_state(state) - - if snap["tensor"] is None or snap["decoded"] is None: - blank = np.zeros((360, 900, 3), dtype=np.uint8) - cv2.putText(blank, "Aguardando primeiro tensor...", (30, 180), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2, cv2.LINE_AA) - cv2.imshow("OAK RAW_BRUTO vs Tensor Final", blank) - key = cv2.waitKey(1) & 0xFF - if key in (27, ord('q'), ord('Q')): - break - continue - - # Desenha só usando o cache. Não captura nem monta tensor aqui. - t_draw0 = time.perf_counter() - raw_tiles = decoded_raw_tiles( - snap["decoded"], - tile_w=args.tile_w, - tile_h=args.tile_h, - auto_level=args.auto_level_raw, - ) - final_tiles = tensor_tiles( - snap["tensor"], - tile_w=args.tile_w, - tile_h=args.tile_h, - auto_level=args.auto_level_tensor, - ) - - pred_tiles = None - pred_names = None - if args.run_model: - pred_tiles, pred_names = try_extract_prediction_tiles( - snap["pred"], - target_w=args.tile_w, - target_h=args.tile_h, - ) - if snap["model_warn"]: - tnow = time.time() - if tnow - last_warn_ts > 2.0: - print(f"[WARN][MODEL] {snap['model_warn']}") - last_warn_ts = tnow - - panel = build_grid( - raw_tiles=raw_tiles, - final_tiles=final_tiles, - pred_tiles=pred_tiles, - pred_names=pred_names, - tile_w=args.tile_w, - tile_h=args.tile_h, - ) - - preview_fps = state.preview_fps.tick() - draw_ms = (time.perf_counter() - t_draw0) * 1000.0 - perf = snap["perf"] - meta = snap["meta"] or {} - tensor_seq = int(snap["tensor_seq"]) - dropped_for_preview = max(0, tensor_seq - last_seq_drawn - 1) if last_seq_drawn >= 0 else 0 - last_seq_drawn = tensor_seq - - hud = [ - f"Preview FPS: {preview_fps:.1f} | Tensor FPS: {snap['tensor_fps']:.1f} | Model FPS: {snap['model_fps']:.1f} | infer={snap.get('model_ms', 0.0):.1f}ms", - f"draw={draw_ms:.1f}ms flat={perf.get('flat_ms', 0):.1f} warp={perf.get('warp_total_ms', 0):.1f} crop={perf.get('crop_resize_ms', 0):.1f} fuse={perf.get('total_ms', 0):.1f}", - f"seq={tensor_seq} skipped_preview={dropped_for_preview} frame_type={meta.get('frame_type')} run_model={args.run_model}", - ] - if snap["error"]: - hud.append(str(snap["error"])[:120]) - - panel = add_hud(panel, hud) - last_panel = panel - - disp = panel - if args.display_scale and abs(args.display_scale - 1.0) > 1e-6: - disp = cv2.resize( - disp, - (max(1, int(disp.shape[1] * args.display_scale)), max(1, int(disp.shape[0] * args.display_scale))), - interpolation=cv2.INTER_AREA, - ) - - cv2.imshow("OAK RAW_BRUTO vs Tensor Final", disp) - key = cv2.waitKey(1) & 0xFF - if key in (27, ord('q'), ord('Q')): - break - - finally: - state.stop() - time.sleep(0.05) - - if args.save_last and last_panel is not None: - out_path = Path(args.save_last) - out_path.parent.mkdir(parents=True, exist_ok=True) - cv2.imwrite(str(out_path), last_panel) - print(f"[SAVE] {out_path}") - - try: - client.stop() - except Exception: - pass - cv2.destroyAllWindows() - - -if __name__ == "__main__": - main() diff --git a/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py b/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py index 3cb60e76c..ba9dc5406 100644 --- a/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py +++ b/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_client.py @@ -46,6 +46,7 @@ class OakFcc3Client: mx_id=None, imu_modo="rotation_vector", imu_freq_hz=200, + evaluate_quality=True, **kwargs, ): self.width = width @@ -63,6 +64,16 @@ class OakFcc3Client: self.imu_modo = str(imu_modo).strip().lower() self.imu_freq_hz = int(imu_freq_hz) + # Auditoria radiométrica completa do Raw5. + # + # True mantém o comportamento histórico e é útil para captura científica, + # normalize/auditoria e ferramentas offline. + # + # No runtime em tempo real deve ficar False: evaluate_frame_quality() + # calcula estatísticas/percentis pesados e não faz parte da montagem + # necessária para a inferência. + self.evaluate_quality = bool(evaluate_quality) + self.mx_id = str(mx_id) if mx_id else None self.svc = OakFcc3Service( @@ -327,8 +338,33 @@ class OakFcc3Client: target_size=target_size, ) - def build_infer_tensor_from_decoded(self, decoded, meta, channels_expected, target_size=None): + def build_infer_tensor_from_decoded( + self, + decoded, + meta, + channels_expected, + target_size=None, + evaluate_quality=None, + ): + """ + Monta o Raw5 físico a partir das câmeras já decodificadas. + + evaluate_quality: + - None -> usa self.evaluate_quality + - True -> executa evaluate_frame_quality() e atualiza + core.last_frame_quality_result + - False -> não executa a auditoria pesada e limpa + core.last_frame_quality_result + + A flag altera somente a auditoria de qualidade. Não altera decode, + radiometria, flat-field, homografia, crop/resize ou patch normalization. + """ channels_expected = self._validate_physical_channel_count(channels_expected) + + if evaluate_quality is None: + evaluate_quality = self.evaluate_quality + evaluate_quality = bool(evaluate_quality) + tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected) tensor = self.core.resize_tensor_chw(tensor, target_size=target_size) @@ -338,7 +374,12 @@ class OakFcc3Client: if bool(patch_cfg.get("enabled", False)): tensor = self.core.apply_patch_normalization_to_tensor(tensor) - self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor) + if evaluate_quality: + self.core.last_frame_quality_result = self.core.evaluate_frame_quality(tensor) + else: + # Evita deixar um resultado antigo parecer referente ao frame atual. + self.core.last_frame_quality_result = None + return tensor def decode_stream_cameras(self, frame, meta): @@ -382,12 +423,19 @@ class OakFcc3Client: tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1)) return np.ascontiguousarray(tensor.astype(np.float32, copy=False)) - def build_multispec_tensor(self, decoded, meta=None, target_size=None): + def build_multispec_tensor( + self, + decoded, + meta=None, + target_size=None, + evaluate_quality=None, + ): tensor = self.build_infer_tensor_from_decoded( decoded=decoded, meta=meta, channels_expected=5, target_size=target_size, + evaluate_quality=evaluate_quality, ) return np.ascontiguousarray(tensor.astype(np.float32, copy=False)) diff --git a/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_manager.py b/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_manager.py index 4544dcebf..ad04a1a47 100644 --- a/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_manager.py +++ b/Python/OAK/datasets/oak-fcc-3/core/oak_fcc3_manager.py @@ -9,6 +9,8 @@ import cv2 import depthai as dai import numpy as np +from itertools import product + class OakFcc3Manager: """ @@ -43,6 +45,8 @@ class OakFcc3Manager: raw_policy="allow_single", roles=None, sync_mode="best", + hardware_sync_enabled=True, + frame_sync_master="CAM_A", sync_tolerance_ms=12.0, buffer_size=8, only_camera=None, @@ -52,6 +56,9 @@ class OakFcc3Manager: imu_modo="rotation_vector", imu_freq_hz=200, ): + self.hardware_sync_enabled = hardware_sync_enabled + self.frame_sync_master = frame_sync_master + self.fps = fps # Para compatibilidade, mantemos width/height. @@ -432,6 +439,49 @@ class OakFcc3Manager: # Pipeline creation, MULTISPEC aligned on OAK # ============================================================ + def _aplicar_frame_sync(self, cam, cam_id): + cam_id_normalizado = str(cam_id).strip().upper() + master = str(self.frame_sync_master).strip().upper() + + if not self.hardware_sync_enabled: + print( + f"[OAK FSYNC] cam={cam_id_normalizado} " + f"habilitado=False modo=DISABLED" + ) + return + + cameras_validas = {"CAM_A", "CAM_B", "CAM_C"} + + if master not in cameras_validas: + raise ValueError( + f"frame_sync_master inválido: {self.frame_sync_master}. " + f"Esperado: CAM_A, CAM_B ou CAM_C." + ) + + if cam_id_normalizado not in cameras_validas: + print( + f"[OAK FSYNC] Câmera ignorada: " + f"cam_id={cam_id_normalizado}" + ) + return + + if cam_id_normalizado == master: + modo = dai.CameraControl.FrameSyncMode.OUTPUT + nome_modo = "OUTPUT" + else: + modo = dai.CameraControl.FrameSyncMode.INPUT + nome_modo = "INPUT" + + cam.initialControl.setFrameSyncMode(modo) + + print( + f"[OAK FSYNC] " + f"cam={cam_id_normalizado} " + f"master={master} " + f"habilitado=True " + f"modo={nome_modo}" + ) + def _create_color_camera_multispec(self, socket): cam = self.pipeline.create(dai.node.ColorCamera) cam.setBoardSocket(socket) @@ -591,6 +641,7 @@ class OakFcc3Manager: raise RuntimeError(f"Role não suportada no MULTISPEC: cam_id={cam_id}, role={role}") self.apply_initial_camera_controls_to_node(cam, cam_id) + self._aplicar_frame_sync(cam, cam_id) xin_ctrl = self.pipeline.create(dai.node.XLinkIn) xin_ctrl.setStreamName(f"{cam_id}_ctrl") @@ -871,6 +922,7 @@ class OakFcc3Manager: ) self.apply_initial_camera_controls_to_node(cam, socket_name) + self._aplicar_frame_sync(cam, socket_name) xin_ctrl = self.pipeline.create(dai.node.XLinkIn) xin_ctrl.setStreamName(f"{socket_name}_ctrl") @@ -1241,6 +1293,41 @@ class OakFcc3Manager: f"Tolerância atual={self.sync_tolerance_ms} ms." ) + def _encontrar_melhor_tripleta(self, required_cam_ids): + listas = [ + list(self.buffers[cam_id]) + for cam_id in required_cam_ids + ] + + melhor_selecao = None + melhor_score = None + + for combinacao in product(*listas): + timestamps = [ + item["timestamp"] + for item in combinacao + ] + + menor_ts = min(timestamps) + maior_ts = max(timestamps) + spread_ms = (maior_ts - menor_ts) * 1000.0 + + # Primeiro prioriza menor dispersão. + # Em empate, prefere o pacote mais recente. + score = ( + spread_ms, + -menor_ts, + ) + + if melhor_score is None or score < melhor_score: + melhor_score = score + melhor_selecao = { + cam_id: item + for cam_id, item in zip(required_cam_ids, combinacao) + } + + return melhor_selecao + def _extract_frame_controls(self, msg): controls = { @@ -1330,9 +1417,19 @@ class OakFcc3Manager: t0_ts = self._cap_now_ms() try: - ts = msg.getTimestamp().total_seconds() + ts_start = msg.getTimestampDevice( + dai.CameraExposureOffset.START + ).total_seconds() + + ts_end = msg.getTimestampDevice( + dai.CameraExposureOffset.END + ).total_seconds() + except Exception: - ts = time.time() + ts_start = msg.getTimestamp().total_seconds() + ts_end = ts_start + + ts = ts_start if perf is not None: perf["drain_get_timestamp_ms"] += self._cap_now_ms() - t0_ts @@ -1395,7 +1492,9 @@ class OakFcc3Manager: self.buffers[cam_id].append({ "frame": frame, - "timestamp": ts, + "timestamp": ts_start, + "timestamp_start": ts_start, + "timestamp_end": ts_end, "controls": frame_controls, }) @@ -1420,34 +1519,25 @@ class OakFcc3Manager: def _try_get_synced_packet(self, perf=None): required_cam_ids = self._get_required_cam_ids() + if not required_cam_ids: + if perf is not None: + perf["wait_reason"] = "no_required_cameras" + return None + + # Todas as câmeras precisam ter pelo menos um frame disponível. for cam_id in required_cam_ids: if cam_id not in self.buffers or len(self.buffers[cam_id]) == 0: if perf is not None: perf["wait_reason"] = f"empty_buffer:{cam_id}" return None - ref_cam_id = min(required_cam_ids, key=lambda cid: len(self.buffers[cid])) - ref_item = self.buffers[ref_cam_id][0] - ref_ts = ref_item["timestamp"] + # Procura a melhor combinação entre todos os frames disponíveis. + selected = self._encontrar_melhor_tripleta(required_cam_ids) - selected = {} - - for cam_id in required_cam_ids: - best_item = None - best_dt = None - - for item in self.buffers[cam_id]: - dt = abs(item["timestamp"] - ref_ts) - if best_dt is None or dt < best_dt: - best_dt = dt - best_item = item - - if best_item is None: - if perf is not None: - perf["wait_reason"] = f"no_best_item:{cam_id}" - return None - - selected[cam_id] = best_item + if not selected or len(selected) != len(required_cam_ids): + if perf is not None: + perf["wait_reason"] = "no_valid_selection" + return None timestamps = { cam_id: item["timestamp"] @@ -1459,44 +1549,80 @@ class OakFcc3Manager: for cam_id, item in selected.items() } + ts_values = list(timestamps.values()) + + sync_dt_ms = ( + (max(ts_values) - min(ts_values)) * 1000.0 + if len(ts_values) >= 2 + else 0.0 + ) + + sync_ok = sync_dt_ms <= self.sync_tolerance_ms + if perf is not None: - perf["selected_ts_by_cam"] = {cam_id: float(ts) for cam_id, ts in timestamps.items()} + perf["selected_ts_by_cam"] = { + cam_id: float(ts) + for cam_id, ts in timestamps.items() + } + perf["selected_seq_by_cam"] = { cam_id: item.get("controls", {}).get("sequence_num") for cam_id, item in selected.items() } - ts_values = list(timestamps.values()) - sync_dt_ms = (max(ts_values) - min(ts_values)) * 1000.0 if len(ts_values) >= 2 else 0.0 - sync_ok = sync_dt_ms <= self.sync_tolerance_ms + perf["sync_dt_ms"] = float(sync_dt_ms) + perf["sync_ok"] = bool(sync_ok) + + modo_sync = str(self.sync_mode).strip().lower() + + # No modo estrito, nunca entrega uma tripleta fora da tolerância. + if not sync_ok and modo_sync == "strict": + #Remove o frame globalmente mais antigo entre as cabeças + #dos buffers. Frames futuros somente estarão mais distantes + #desse frame, então ele não conseguirá formar uma combinação + #melhor posteriormente. + oldest_cam_id = min( + required_cam_ids, + key=lambda cam_id: self.buffers[cam_id][0]["timestamp"] + ) + + dropped_item = self.buffers[oldest_cam_id].popleft() - if not sync_ok and self.sync_mode == "strict": - oldest_cam_id = min(timestamps, key=timestamps.get) - if len(self.buffers[oldest_cam_id]) > 0: - self.buffers[oldest_cam_id].popleft() if perf is not None: perf["wait_reason"] = f"strict_drop_oldest:{oldest_cam_id}" - perf["sync_dt_ms"] = float(sync_dt_ms) - perf["sync_ok"] = False + perf["dropped_timestamp"] = float(dropped_item["timestamp"]) + return None + # Em best/best_effort, entrega a melhor combinação disponível, + # mesmo quando estiver fora da tolerância. frames = { cam_id: item["frame"] for cam_id, item in selected.items() } + # Consome todos os frames anteriores e o próprio frame selecionado. for cam_id, used_item in selected.items(): - while len(self.buffers[cam_id]) > 0: + while self.buffers[cam_id]: item = self.buffers[cam_id].popleft() + if item is used_item: break if perf is not None: - perf["wait_reason"] = "synced_selected" - perf["sync_dt_ms"] = float(sync_dt_ms) - perf["sync_ok"] = bool(sync_ok) + perf["wait_reason"] = ( + "synced_selected" + if sync_ok + else "best_effort_selected_outside_tolerance" + ) - return frames, timestamps, sync_dt_ms, sync_ok, frame_controls + return ( + frames, + timestamps, + sync_dt_ms, + sync_ok, + frame_controls, + ) def _get_available_cam_ids_ordered(self): role_order = ["rgb", "re", "nir"] diff --git a/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py b/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py index c105e7b8f..3a2ba9d24 100644 --- a/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py +++ b/Python/OAK/datasets/oak-fcc-3/core/raw_processor_core.py @@ -1,9 +1,42 @@ +""" +RawProcessorCore - Production +============================= + +Canonical multispectral preprocessing core for training and inference. + +Product contract: +- CAM_A RGB: OV9782 1280x800 OR AR0234 1920x1200 +- CAM_B RE : OV9282 1280x800 +- CAM_C NIR: OV9282 1280x800 +- Physical output tensor: [R, G, B, RE, NIR] + +Production revision highlights: +- mixed-resolution homography with independent source/reference spaces; +- OpenCV pixel-center-aware geometry scaling; +- native RE/NIR warp without pre-resizing to RGB; +- strict validation of final module_params and per-frame hardware metadata; +- product RAW_BRUTO fail-closed input contract; +- radiometric frame-control fallbacks for saved datasets; +- single-resample fusion directly to requested tensor size; +- intrinsics contract loading/validation with runtime undistort gated off; +- faster Flat-Field saturation guard using fused Numba kernel; +- lower-allocation in-place Flat-Field path; +- faster frame-quality statistics while retaining exact critical percentages. + +This module intentionally keeps legacy behavior available when a non-product +module_params is loaded. The strict fail-closed behavior is enabled only for +module_params generated by the production assembler. +""" + +RAW_PROCESSOR_CORE_VERSION = "production_v1_2026_08_24" + import json import os import time import cv2 import numpy as np import math +from copy import deepcopy from typing import Optional @@ -215,6 +248,97 @@ if _HAS_NUMBA: out[y, x] = v + @_numba.njit(cache=True, fastmath=True, parallel=True) + def _apply_flat_gain_guard_numba( + base, + gain, + saturation_mask, + out, + height, + width, + strength, + gain_min_runtime, + gain_max_runtime, + sat_soft_start, + sat_hard, + clip_output, + mode_code, + ): + """ + Flat-field com saturation guard em uma única passada. + + mode_code: + 1 = fade_strength + 2 = skip + 0 = simple + """ + denom = sat_hard - sat_soft_start + if denom < 1e-6: + denom = 1e-6 + + for y in _numba.prange(height): + for x in range(width): + v = float(base[y, x]) + g = float(gain[y, x]) + + if mode_code == 1: + t = (v - sat_soft_start) / denom + + if t < 0.0: + t = 0.0 + elif t > 1.0: + t = 1.0 + + local_strength = 1.0 - t + + if saturation_mask[y, x]: + local_strength = 0.0 + + gain_eff = 1.0 + ( + strength + * local_strength + * (g - 1.0) + ) + + if gain_eff < gain_min_runtime: + gain_eff = gain_min_runtime + elif gain_eff > gain_max_runtime: + gain_eff = gain_max_runtime + + value = v * gain_eff + + elif mode_code == 2: + if saturation_mask[y, x]: + value = v + else: + gain_eff = 1.0 + strength * (g - 1.0) + + if gain_eff < gain_min_runtime: + gain_eff = gain_min_runtime + elif gain_eff > gain_max_runtime: + gain_eff = gain_max_runtime + + value = v * gain_eff + + else: + gain_eff = 1.0 + strength * (g - 1.0) + + if gain_eff < gain_min_runtime: + gain_eff = gain_min_runtime + elif gain_eff > gain_max_runtime: + gain_eff = gain_max_runtime + + value = v * gain_eff + + if clip_output: + if value < 0.0: + value = 0.0 + elif value > 1.0: + value = 1.0 + + out[y, x] = value + + @_numba.njit(cache=True, fastmath=True, parallel=True) def _apply_tensor_flat_gain_chw_numba(tensor, gain, channels, height, width, clip_output): for c in _numba.prange(channels): @@ -389,6 +513,7 @@ class RawProcessorCore: "save_debug": True, } self.last_radiometric_normalization_result = None + self.last_flatfield_result = None self.radiometric_config = {} self.patch_normalization_config = { @@ -440,37 +565,160 @@ class RawProcessorCore: self.last_fusion_result = None self.camera_settings = {} + # Contrato de produto carregado pelo assembler final. + self.strict_product_contract = False + self.module_params_schema = None + self.sensor_size_by_role = {} + self.camera_hardware = {} + self.intrinsics_config = { + "enabled": False, + "calibration_space": "native_stream_no_external_undistort", + "cameras": {}, + "runtime_undistort": {"enabled": False}, + } + self.calibration_provenance = {} + self.assembly_metadata = {} + self._last_frame_controls_source = None + self._flatfield_runtime_cache = {} self.last_decode_perf = {} self._last_decode_perf_log_ts = 0.0 self.last_rgb_enhancement_result = None + # QC: percentis são diagnóstico, não parte do tensor. + # Mantemos saturação/dark/range exatos e limitamos apenas estatísticas + # descritivas para não gastar dezenas/centenas de ms por frame. + self.quality_stats_max_samples = 65536 + if calibration_json_path: self.load_config_json(calibration_json_path) def warmup_numba_raw10_decode(self): + """ + Warmup sensor-aware. + + Numba especializa por dtype/layout, não por shape, mas usar os tamanhos + reais também aquece alocações e o caminho OpenCV do RGB correto. + """ if not _HAS_NUMBA: return {"ok": False, "reason": "numba_not_available"} - w, h = 1280, 800 - packed_w = int(np.ceil(w * 10 / 8)) - dummy = np.zeros((h, packed_w), dtype=np.uint8) + sizes = getattr(self, "sensor_size_by_role", {}) or {} - _ = self._raw10_mono_to_float01_aggressive(dummy, w, h, 10) - _ = self._raw10_rgb_bayer_planes_to_rgb_float01_aggressive(dummy, w, h, "RGGB", 10) - _ = self._raw10_to_raw16_aggressive(dummy, w, h) + rgb_size = sizes.get("rgb") or [ + int(self.sensor_width), + int(self.sensor_height), + ] + re_size = sizes.get("re") or [1280, 800] - raw16 = self._raw10_to_raw16_aggressive(dummy, w, h) - rgb16 = cv2.cvtColor(raw16, cv2.COLOR_BayerRG2RGB) + rgb_w, rgb_h = int(rgb_size[0]), int(rgb_size[1]) + spec_w, spec_h = int(re_size[0]), int(re_size[1]) + + rgb_packed_w = self._raw10_expected_packed_width_fast(rgb_w) + spec_packed_w = self._raw10_expected_packed_width_fast(spec_w) + + dummy_rgb = np.zeros((rgb_h, rgb_packed_w), dtype=np.uint8) + dummy_spec = np.zeros((spec_h, spec_packed_w), dtype=np.uint8) + + _ = self._raw10_mono_to_float01_aggressive( + dummy_spec, + spec_w, + spec_h, + 10, + ) + + _ = self._raw10_rgb_bayer_planes_to_rgb_float01_aggressive( + dummy_rgb, + rgb_w, + rgb_h, + self.bayer_pattern, + 10, + ) + + raw16 = self._raw10_to_raw16_aggressive( + dummy_rgb, + rgb_w, + rgb_h, + ) + + cv_code, _ = self._get_bayer_cv2_code( + bayer_pattern=self.bayer_pattern, + algorithm=str( + (self.rgb_processing_config or {}).get( + "demosaic_algorithm", + "ea", + ) + ), + ) + + rgb16 = cv2.cvtColor(raw16, cv_code) _ = self._rgb16_to_float32_gain_clip_aggressive(rgb16, 10) - dummy_tensor = np.zeros((5, 640, 1024), dtype=np.float32) - dummy_gain = np.ones((5, 640, 1024), dtype=np.float32) - _apply_tensor_flat_gain_chw_numba(dummy_tensor, dummy_gain, 5, 640, 1024, True) + # Warmup do flat final/Numba com target real, se houver. + target = (self.fusion_config or {}).get("target_size") or [ + max(1, rgb_w // 2), + max(1, rgb_h // 2), + ] + tw, th = int(target[0]), int(target[1]) - return {"ok": True, "backend": "numba", "shape": [h, packed_w]} + dummy_tensor = np.zeros((5, th, tw), dtype=np.float32) + dummy_gain = np.ones((5, th, tw), dtype=np.float32) + + _apply_tensor_flat_gain_chw_numba( + dummy_tensor, + dummy_gain, + 5, + th, + tw, + True, + ) + + # Warmup do kernel fused do Flat-Field com saturation guard. + kh = min(th, 64) + kw = min(tw, 64) + + dummy_base = np.zeros( + (kh, kw), + dtype=np.float32, + ) + dummy_gain_ch = np.ones( + (kh, kw), + dtype=np.float32, + ) + dummy_mask = np.zeros( + (kh, kw), + dtype=np.bool_, + ) + dummy_out = np.empty( + (kh, kw), + dtype=np.float32, + ) + + _apply_flat_gain_guard_numba( + dummy_base, + dummy_gain_ch, + dummy_mask, + dummy_out, + kh, + kw, + 1.0, + 0.75, + 1.35, + 0.88, + 0.97, + True, + 1, + ) + + return { + "ok": True, + "backend": "numba", + "rgb_native": [rgb_w, rgb_h], + "spectral_native": [spec_w, spec_h], + "target_size": [tw, th], + } def unpack_raw10_packed( self, @@ -910,8 +1158,19 @@ class RawProcessorCore: output_dtype: str = "float32", bit_depth: int = 10, ) -> np.ndarray: + """ + RAW Bayer uint16 -> RGB CHW usando exatamente o rgb_processing atual. + + Suporta: + linear_demosaic + linear_demosaic_half + bayer_planes + """ rgb_mode = str( - (getattr(self, "rgb_processing_config", {}) or {}).get("mode", "linear_demosaic") + (getattr(self, "rgb_processing_config", {}) or {}).get( + "mode", + "linear_demosaic", + ) ).lower() if rgb_mode in ("linear_demosaic", "demosaic", "full_res"): @@ -920,6 +1179,23 @@ class RawProcessorCore: bit_depth=bit_depth, ) + elif rgb_mode in ( + "linear_demosaic_half", + "demosaic_half", + "full_demosaic_half", + ): + r, g, b = self.demosaic_raw16_to_rgb_linear( + raw16, + bit_depth=bit_depth, + ) + + h, w = r.shape[:2] + size = (w // 2, h // 2) + + r = cv2.resize(r, size, interpolation=cv2.INTER_AREA) + g = cv2.resize(g, size, interpolation=cv2.INTER_AREA) + b = cv2.resize(b, size, interpolation=cv2.INTER_AREA) + elif rgb_mode in ("bayer_planes", "bayer", "half_res"): r, g, b = self.bayer_planes_to_rgb_linear( raw16, @@ -927,7 +1203,9 @@ class RawProcessorCore: ) else: - raise ValueError(f"rgb_processing.mode inválido: {rgb_mode}") + raise ValueError( + f"rgb_processing.mode inválido: {rgb_mode}" + ) rgb_cal = getattr(self, "rgb_calibration", {}) or {} if rgb_cal.get("enabled", False): @@ -936,30 +1214,58 @@ class RawProcessorCore: g = g * float(gains.get("G", 1.0)) b = b * float(gains.get("B", 1.0)) - rgb_hwc = np.stack([r, g, b], axis=2).astype(np.float32) - np.clip(rgb_hwc, 0.0, 1.0, out=rgb_hwc) + rgb_hwc = np.stack( + [r, g, b], + axis=2, + ).astype(np.float32) + + np.clip( + rgb_hwc, + 0.0, + 1.0, + out=rgb_hwc, + ) - # Pós-processamento RGB opcional. - # Aplicado aqui para o caminho de treino/offline que chama build_training_rgb(). rgb_hwc = self.apply_rgb_enhancement_to_hwc_float01( rgb_hwc, stage="build_training_rgb.after_decode", source_kind="raw", ) - chw = np.transpose(rgb_hwc, (2, 0, 1)).astype(np.float32, copy=False) - np.clip(chw, 0.0, 1.0, out=chw) + chw = np.transpose( + rgb_hwc, + (2, 0, 1), + ).astype(np.float32, copy=False) + + np.clip( + chw, + 0.0, + 1.0, + out=chw, + ) if output_dtype == "float32": return chw if output_dtype == "uint8": - return (chw * 255.0).clip(0, 255).astype(np.uint8) + return ( + chw * 255.0 + ).clip( + 0, + 255, + ).astype(np.uint8) if output_dtype == "uint16": - return (chw * 65535.0).clip(0, 65535).astype(np.uint16) + return ( + chw * 65535.0 + ).clip( + 0, + 65535, + ).astype(np.uint16) - raise ValueError(f"output_dtype não suportado: {output_dtype}") + raise ValueError( + f"output_dtype não suportado: {output_dtype}" + ) def _channel_names_from_decoded(self, decoded): names = ["R", "G", "B"] @@ -1049,38 +1355,106 @@ class RawProcessorCore: return decoded - def build_multispectral_tensor(self, bins_data, bins_meta, target_size=None): - decoded = self.decode_bins_cameras(bins_data, bins_meta) + def build_multispectral_tensor( + self, + bins_data, + bins_meta, + target_size=None, + capture_meta=None, + ): + """ + Caminho offline/legado para dados já carregados. - rgb_cam_id = self._find_cam_by_role(decoded, "rgb") + Em module_params de produção, radiometric_normalization é obrigatória. + Portanto capture_meta deve acompanhar o sample; caso contrário este método + recusará gerar um tensor silenciosamente diferente do runtime. + + Para datasets RAW_BRUTO novos, prefira build_infer_tensor_from_stream(). + """ + decoded = self.decode_bins_cameras( + bins_data, + bins_meta, + ) + + rgb_cam_id = self._find_cam_by_role( + decoded, + "rgb", + ) if rgb_cam_id is None: raise RuntimeError("RGB obrigatório") - channel_names = self._channel_names_from_decoded(decoded) + channel_names = self._channel_names_from_decoded( + decoded + ) + + if ( + getattr(self, "strict_product_contract", False) + and bool( + (self.radiometric_normalization_config or {}).get( + "enabled", + False, + ) + ) + and not isinstance(capture_meta, dict) + ): + raise RuntimeError( + "build_multispectral_tensor() em modo produto exige capture_meta " + "com controles reais da captura. Prefira build_infer_tensor_from_stream()." + ) tensor = self.fuse_multispec_cameras( decoded, - meta=None, + meta=capture_meta, channels_expected=len(channel_names), + target_size=target_size, ) - tensor = self.resize_tensor_chw(tensor, target_size=target_size) + if bool( + (self.patch_normalization_config or {}).get( + "enabled", + False, + ) + ): + tensor = self.apply_patch_normalization_to_tensor( + tensor + ) - if bool((self.patch_normalization_config or {}).get("enabled", False)): - tensor = self.apply_patch_normalization_to_tensor(tensor) - - self.last_frame_quality_result = self.evaluate_frame_quality(tensor) + self.last_frame_quality_result = self.evaluate_frame_quality( + tensor + ) return tensor, channel_names def build_infer_tensor_from_stream(self, frame, meta, channels_expected, target_size=None): channels_expected = self._validate_physical_channel_count(channels_expected) - frame_type = meta.get("frame_type") + stream_meta = ( + meta.get("stream_meta", {}) + if isinstance(meta, dict) + else {} + ) or {} + frame_type = ( + meta.get("frame_type") + if isinstance(meta, dict) + else None + ) or stream_meta.get("frame_type") + + if ( + getattr(self, "strict_product_contract", False) + and frame_type != "RAW_BRUTO" + ): + raise RuntimeError( + "module_params de produção aceita somente frame_type='RAW_BRUTO' " + "neste core. RGB/MULTISPEC pré-processado contornaria calibrações." + ) if frame_type == "RAW_BRUTO": decoded = self.decode_stream_cameras(frame, meta) - tensor = self.fuse_multispec_cameras(decoded, meta, channels_expected) - tensor = self.resize_tensor_chw(tensor, target_size=target_size) + tensor = self.fuse_multispec_cameras( + decoded, + meta, + channels_expected, + target_size=target_size, + ) # Sem cartões neste modo novo. # patch_normalization deve ficar desligado no JSON. @@ -1125,42 +1499,92 @@ class RawProcessorCore: if not isinstance(frame, dict): raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi") - camera_frames = meta.get("camera_frames", {}) or {} - camera_info = meta.get("camera_info", {}) or {} + if not isinstance(meta, dict): + raise RuntimeError("RAW_BRUTO exige meta dict.") + + self._validate_stream_camera_contract(frame, meta) + + stream_meta = ( + meta.get("stream_meta", {}) + if isinstance(meta.get("stream_meta"), dict) + else {} + ) + camera_frames = ( + meta.get("camera_frames", {}) + or stream_meta.get("camera_frames", {}) + or {} + ) + camera_info = ( + meta.get("camera_info", {}) + or stream_meta.get("camera_info", {}) + or {} + ) decoded = {} rgb_mode = str( - (getattr(self, "rgb_processing_config", {}) or {}).get("mode", "linear_demosaic") + (getattr(self, "rgb_processing_config", {}) or {}).get( + "mode", + "linear_demosaic", + ) ).lower() for cam_id, data in frame.items(): - cam_meta = camera_frames.get(cam_id) or camera_info.get(cam_id) or {} + cam_meta = {} + if isinstance(camera_info.get(cam_id), dict): + cam_meta.update(camera_info.get(cam_id) or {}) + if isinstance(camera_frames.get(cam_id), dict): + cam_meta.update(camera_frames.get(cam_id) or {}) role = str(cam_meta.get("role", "")).lower() if not role: - raise RuntimeError(f"Meta da câmera {cam_id} sem role. Esperado role='rgb', 'nir' ou 're'.") + raise RuntimeError( + f"Meta da câmera {cam_id} sem role. " + "Esperado role='rgb', 'nir' ou 're'." + ) bit_depth = int(cam_meta.get("bit_depth", 10)) raw_format = str(cam_meta.get("raw_format", "")).upper() - packed = bool(cam_meta.get("packed", False)) - is_raw10 = raw_format == "RAW10_PACKED" or packed or bit_depth == 10 + packed_flag = bool(cam_meta.get("packed", False)) if role == "rgb": - if is_raw10 and data.ndim == 2: - sensor_w = int(cam_meta.get("width", self.sensor_width)) - sensor_h = int(cam_meta.get("height", self.sensor_height)) + sensor_w = int(cam_meta.get("width", self.sensor_width)) + sensor_h = int(cam_meta.get("height", self.sensor_height)) - bayer = ( - cam_meta.get("bayer_pattern") - or cam_meta.get("bayer") - or self.bayer_pattern - or "RGGB" + bayer = str( + cam_meta.get("bayer_pattern") + or cam_meta.get("bayer") + or self.bayer_pattern + or "RGGB" + ).upper() + + arr = np.asarray(data) + + expected_packed_w = self._raw10_expected_packed_width_fast(sensor_w) + packed_width_meta = int(cam_meta.get("packed_width", 0) or 0) + + looks_like_raw10_packed = ( + arr.ndim == 2 + and arr.dtype == np.uint8 + and arr.shape[0] == sensor_h + and ( + raw_format == "RAW10_PACKED" + or packed_flag + or ( + packed_width_meta > 0 + and arr.shape[1] >= packed_width_meta + ) + or ( + bit_depth == 10 + and arr.shape[1] >= expected_packed_w + ) ) + ) + if looks_like_raw10_packed: if rgb_mode in ("bayer_planes", "bayer", "half_res"): rgb_hwc = self._raw10_rgb_bayer_planes_to_rgb_float01_aggressive( - data, + arr, width=sensor_w, height=sensor_h, bayer_pattern=bayer, @@ -1176,7 +1600,7 @@ class RawProcessorCore: "full_demosaic_half", ): rgb_hwc = self._raw10_rgb_linear_demosaic_to_rgb_float01_fast( - data, + arr, width=sensor_w, height=sensor_h, bayer_pattern=bayer, @@ -1184,10 +1608,12 @@ class RawProcessorCore: ) else: - raise ValueError(f"rgb_processing.mode inválido: {rgb_mode}") + raise ValueError( + f"rgb_processing.mode inválido: {rgb_mode}" + ) - # Pós-processamento RGB opcional. - # Este é o caminho principal de inferência RAW_BRUTO. + # Em produto enhancement é validado como OFF. + # Em modo legado permanece compatível com os experimentos antigos. rgb_hwc = self.apply_rgb_enhancement_to_hwc_float01( rgb_hwc, stage="decode_stream_cameras.after_raw_decode", @@ -1201,18 +1627,50 @@ class RawProcessorCore: "meta": cam_meta, } + elif arr.ndim == 2 and arr.dtype == np.uint16: + # RAW Bayer já desempacotado. Útil em fluxo offline/diagnóstico. + old_bayer = self.bayer_pattern + try: + self.bayer_pattern = bayer + rgb_chw = self.build_training_rgb( + arr, + output_dtype="float32", + bit_depth=bit_depth, + ) + finally: + self.bayer_pattern = old_bayer + + rgb_hwc = np.transpose( + rgb_chw, + (1, 2, 0), + ).astype(np.float32, copy=False) + + decoded[cam_id] = { + "name": "RGB", + "role": "rgb", + "image": rgb_hwc, + "meta": cam_meta, + } + else: - # Caso preview/processado antigo: BGR HWC uint8. - if data.ndim != 3 or data.shape[2] != 3: - raise RuntimeError(f"{cam_id} RGB inválida: shape={data.shape}") + # Preview/processado antigo: BGR HWC. + if arr.ndim != 3 or arr.shape[2] != 3: + raise RuntimeError( + f"{cam_id} RGB inválida: shape={arr.shape}, " + f"dtype={arr.dtype}" + ) - rgb = data[:, :, ::-1].astype(np.float32) / 255.0 + if arr.dtype == np.uint8: + rgb = arr[:, :, ::-1].astype(np.float32) / 255.0 + elif arr.dtype == np.uint16: + rgb = arr[:, :, ::-1].astype(np.float32) / 65535.0 + else: + rgb = arr[:, :, ::-1].astype(np.float32) + if np.nanmax(rgb) > 1.5: + rgb /= 255.0 - rgb = np.clip(rgb, 0.0, 1.0) + np.clip(rgb, 0.0, 1.0, out=rgb) - # Por padrão não mexe em preview/RGB já processado. - # Se quiser aplicar também neste caminho, use: - # rgb_processing.enhancement.apply_to_preview_input=true rgb = self.apply_rgb_enhancement_to_hwc_float01( rgb, stage="decode_stream_cameras.preview_input", @@ -1230,7 +1688,10 @@ class RawProcessorCore: decoded[cam_id] = { "name": "RE", "role": "re", - "image": self._decode_spectral_frame_to_float01(data, cam_meta), + "image": self._decode_spectral_frame_to_float01( + data, + cam_meta, + ), "meta": cam_meta, } @@ -1238,12 +1699,17 @@ class RawProcessorCore: decoded[cam_id] = { "name": "NIR", "role": "nir", - "image": self._decode_spectral_frame_to_float01(data, cam_meta), + "image": self._decode_spectral_frame_to_float01( + data, + cam_meta, + ), "meta": cam_meta, } else: - raise RuntimeError(f"Role não suportada em {cam_id}: {role}") + raise RuntimeError( + f"Role não suportada em {cam_id}: {role}" + ) return decoded @@ -1313,7 +1779,13 @@ class RawProcessorCore: return np.clip(arr01, 0.0, 1.0) - def fuse_multispec_cameras(self, decoded, meta, channels_expected): + def fuse_multispec_cameras( + self, + decoded, + meta, + channels_expected, + target_size=None, + ): t_total0 = time.perf_counter() t0 = time.perf_counter() @@ -1344,6 +1816,7 @@ class RawProcessorCore: decoded, meta, channels_expected, + target_size_override=target_size, ) t_flat_ms = float(t_flat_native_ms + float(direct_perf.get("final_flat_ms", 0.0))) @@ -1465,13 +1938,55 @@ class RawProcessorCore: ) def _warp_with_valid_mask(self, img, role, ref_shape, meta): - ref_h, ref_w = ref_shape + """ + Legacy spatial path mantido correto para mixed-resolution. - if img.shape[:2] != (ref_h, ref_w): - img = cv2.resize(img, (ref_w, ref_h), interpolation=cv2.INTER_LINEAR) + Em homography, RE/NIR permanecem no tamanho nativo até o warp. + Nos modos identity/manual antigos, resize para a referência continua + permitido apenas por compatibilidade. + """ + ref_h, ref_w = int(ref_shape[0]), int(ref_shape[1]) cfg = self.fusion_config - mode = cfg.get("alignment_mode", "identity") + mode = str(cfg.get("alignment_mode", "identity")).lower() + + if mode == "homography": + H = self._direct_fusion_get_role_homography_fast( + role, + meta, + (ref_h, ref_w), + source_size=img.shape[:2], + ) + + source_mask = np.ones(img.shape[:2], dtype=np.uint8) * 255 + + warped = cv2.warpPerspective( + img, + H, + (ref_w, ref_h), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0, + ) + + warped_mask = cv2.warpPerspective( + source_mask, + H, + (ref_w, ref_h), + flags=cv2.INTER_NEAREST, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0, + ) + + return warped, (warped_mask > 0).astype(np.uint8) + + # Modos legados operavam em uma grade comum. + if img.shape[:2] != (ref_h, ref_w): + img = cv2.resize( + img, + (ref_w, ref_h), + interpolation=cv2.INTER_LINEAR, + ) mask = np.ones((ref_h, ref_w), dtype=np.uint8) * 255 @@ -1496,36 +2011,10 @@ class RawProcessorCore: warped = self._affine_image(img, dx, dy, theta_deg) warped_mask = self._affine_image(mask, dx, dy, theta_deg) - elif mode == "homography": - H, calib_size, profile_name = self._resolve_homography_entry_for_role(role) - H = self._scale_homography_to_runtime( - H, - calib_size=calib_size, - runtime_size=(ref_w, ref_h), - ) - - if H.shape != (3, 3): - raise RuntimeError(f"Homografia inválida para {role}: shape={H.shape}") - - warped = cv2.warpPerspective( - img, H, (ref_w, ref_h), - flags=cv2.INTER_LINEAR, - borderMode=cv2.BORDER_CONSTANT, - borderValue=0 - ) - - warped_mask = cv2.warpPerspective( - mask, H, (ref_w, ref_h), - flags=cv2.INTER_NEAREST, - borderMode=cv2.BORDER_CONSTANT, - borderValue=0 - ) - else: raise RuntimeError(f"alignment_mode inválido: {mode}") - warped_mask = (warped_mask > 0).astype(np.uint8) - return warped, warped_mask + return warped, (warped_mask > 0).astype(np.uint8) def _compute_common_crop_box(self, masks): if not masks: @@ -1599,56 +2088,157 @@ class RawProcessorCore: return np.stack(chans, axis=0) - def _scale_homography_to_runtime(self, H, calib_size, runtime_size): + def _scale_homography_to_runtime( + self, + H, + calib_size=None, + runtime_size=None, + *, + calib_source_size=None, + calib_reference_size=None, + runtime_source_size=None, + runtime_reference_size=None, + ): """ - Ajusta uma homografia calculada em calib_size para ser aplicada em runtime_size. + Escala H entre dois espaços independentes usando convenção de centro + de pixel compatível com cv2.resize: - H original: - ponto_spec_calib -> ponto_rgb_calib + x_dst = (x_src + 0.5) * scale - 0.5 - H runtime: - ponto_spec_runtime -> ponto_rgb_runtime + Caso geral: + H_calib : source_calib -> reference_calib + H_runtime : source_runtime -> reference_runtime + + H_runtime = A_dst @ H_calib @ inv(A_src) + + Chamadas antigas com calib_size/runtime_size seguem compatíveis. """ if H is None: return None - H = np.asarray(H, dtype=np.float32) + H = np.asarray( + H, + dtype=np.float64, + ) - if calib_size is None: - return H + if H.shape != (3, 3): + raise RuntimeError( + f"Homografia inválida: shape={H.shape}" + ) - calib_w, calib_h = calib_size - runtime_w, runtime_h = runtime_size + if calib_source_size is None: + calib_source_size = calib_size - calib_w = float(calib_w) - calib_h = float(calib_h) - runtime_w = float(runtime_w) - runtime_h = float(runtime_h) + if calib_reference_size is None: + calib_reference_size = calib_size - if calib_w <= 0 or calib_h <= 0: - return H + if runtime_source_size is None: + runtime_source_size = runtime_size - sx = runtime_w / calib_w - sy = runtime_h / calib_h + if runtime_reference_size is None: + runtime_reference_size = runtime_size - S = np.array([ - [sx, 0.0, 0.0], - [0.0, sy, 0.0], - [0.0, 0.0, 1.0], - ], dtype=np.float32) + if ( + calib_source_size is None + or calib_reference_size is None + or runtime_source_size is None + or runtime_reference_size is None + ): + out = H.copy() - S_inv = np.array([ - [1.0 / sx, 0.0, 0.0], - [0.0, 1.0 / sy, 0.0], - [0.0, 0.0, 1.0], - ], dtype=np.float32) + if abs( + float( + out[2, 2] + ) + ) > 1e-12: + out /= out[2, 2] - H_runtime = S @ H @ S_inv + return out.astype( + np.float32 + ) - if abs(H_runtime[2, 2]) > 1e-9: - H_runtime = H_runtime / H_runtime[2, 2] + def resize_matrix( + src_size, + dst_size, + ): + src_w, src_h = [ + float(v) + for v in src_size + ] + dst_w, dst_h = [ + float(v) + for v in dst_size + ] - return H_runtime.astype(np.float32) + if ( + src_w <= 0 + or src_h <= 0 + or dst_w <= 0 + or dst_h <= 0 + ): + raise RuntimeError( + f"Tamanho inválido: " + f"src={src_size}, dst={dst_size}" + ) + + sx = dst_w / src_w + sy = dst_h / src_h + + tx = ( + 0.5 * sx + - 0.5 + ) + ty = ( + 0.5 * sy + - 0.5 + ) + + return np.array( + [ + [sx, 0.0, tx], + [0.0, sy, ty], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + + A_src = resize_matrix( + calib_source_size, + runtime_source_size, + ) + + A_dst = resize_matrix( + calib_reference_size, + runtime_reference_size, + ) + + H_runtime = ( + A_dst + @ H + @ np.linalg.inv( + A_src + ) + ) + + if abs( + float( + H_runtime[2, 2] + ) + ) > 1e-12: + H_runtime /= H_runtime[2, 2] + + if not np.all( + np.isfinite( + H_runtime + ) + ): + raise RuntimeError( + "Homografia runtime contém valores não finitos." + ) + + return H_runtime.astype( + np.float32 + ) def apply_patch_normalization_to_tensor(self, tensor: np.ndarray) -> np.ndarray: @@ -1941,36 +2531,114 @@ class RawProcessorCore: def _array01_stats(self, arr: np.ndarray) -> dict: """ - Estatísticas compactas para debug radiométrico de arrays float. - Mantém tudo serializável em JSON e leve o bastante para log por frame. + Estatísticas compactas para debug/quality. + + Performance: + - sat/dark/over/under, min/max e finitude são EXATOS; + - percentis, mean e std usam amostragem determinística quando o array + ultrapassa quality_stats_max_samples. + + Essa amostragem não altera o tensor nem decisões de saturação/range. """ if arr is None: return {} - vals = np.asarray(arr, dtype=np.float32).reshape(-1) + vals = np.asarray( + arr, + dtype=np.float32, + ).reshape(-1) + if vals.size <= 0: return {} - finite = vals[np.isfinite(vals)] - if finite.size <= 0: - return {"count": int(vals.size), "finite_count": 0} + finite_mask = np.isfinite(vals) + finite_count = int( + np.count_nonzero(finite_mask) + ) + + if finite_count <= 0: + return { + "count": int(vals.size), + "finite_count": 0, + } + + if finite_count == vals.size: + finite = vals + else: + finite = vals[finite_mask] + + n = int(finite.size) + + max_samples = int( + getattr( + self, + "quality_stats_max_samples", + 65536, + ) + or 65536 + ) + + if max_samples <= 0: + max_samples = 65536 + + if n > max_samples: + stride = int( + math.ceil( + n / float(max_samples) + ) + ) + sample = finite[::stride] + else: + stride = 1 + sample = finite + + # Percentis em uma única chamada. + p01, p05, p50, p95, p99 = np.percentile( + sample, + [1, 5, 50, 95, 99], + ) + + sat_count = int( + np.count_nonzero( + finite >= 0.98 + ) + ) + dark_count = int( + np.count_nonzero( + finite <= 0.02 + ) + ) + over_count = int( + np.count_nonzero( + finite > 1.0 + ) + ) + under_count = int( + np.count_nonzero( + finite < 0.0 + ) + ) + + inv_n = 100.0 / float(n) return { "count": int(vals.size), - "finite_count": int(finite.size), + "finite_count": finite_count, + "sample_count": int(sample.size), + "sample_stride": int(stride), "min": float(np.min(finite)), - "p01": float(np.percentile(finite, 1)), - "p05": float(np.percentile(finite, 5)), - "p50": float(np.percentile(finite, 50)), - "p95": float(np.percentile(finite, 95)), - "p99": float(np.percentile(finite, 99)), + "p01": float(p01), + "p05": float(p05), + "p50": float(p50), + "p95": float(p95), + "p99": float(p99), "max": float(np.max(finite)), - "mean": float(np.mean(finite)), - "std": float(np.std(finite)), - "sat_pct": float((finite >= 0.98).mean() * 100.0), - "dark_pct": float((finite <= 0.02).mean() * 100.0), - "over_1_pct": float((finite > 1.0).mean() * 100.0), - "under_0_pct": float((finite < 0.0).mean() * 100.0), + "mean": float(np.mean(sample)), + "std": float(np.std(sample)), + "sat_pct": float(sat_count * inv_n), + "dark_pct": float(dark_count * inv_n), + "over_1_pct": float(over_count * inv_n), + "under_0_pct": float(under_count * inv_n), } def _tensor_channel_stats(self, tensor: np.ndarray, channel_names: list) -> dict: @@ -2503,6 +3171,107 @@ class RawProcessorCore: f"patch_warning:{warning_text}", ) + # ======================================================== + # Contrato obrigatório de calibração + # ======================================================== + + rad_norm_cfg = self.radiometric_normalization_config or {} + if bool(rad_norm_cfg.get("enabled", False)): + rad_result = self.last_radiometric_normalization_result or {} + + if not isinstance(rad_result, dict) or not bool(rad_result.get("applied", False)): + mark("bad", "radiometric_normalization_required_but_not_applied") + else: + roles_done = set((rad_result.get("by_role", {}) or {}).keys()) + missing_roles = [ + role + for role in ("rgb", "re", "nir") + if role not in roles_done + ] + + if missing_roles: + mark( + "bad", + "radiometric_normalization_missing_roles:" + + ",".join(missing_roles), + ) + + rad_warnings = [ + str(x) + for x in (rad_result.get("warnings", []) or []) + ] + + hard_rad_tokens = ( + "missing_frame_controls", + "invalid_factor", + "unsupported_method", + ) + + for warning in rad_warnings: + if any(token in warning for token in hard_rad_tokens): + mark("bad", f"radiometric_normalization:{warning}") + + quality["metrics"]["radiometric_normalization"] = rad_result + + flat_cfg = self.flatfield_config or {} + if bool(flat_cfg.get("enabled", False)): + if not bool(getattr(self, "flatfield_loaded", False)): + mark("bad", "flatfield_required_but_not_loaded") + + loaded_channels = sorted( + str(x) + for x in (getattr(self, "flatfield_maps", {}) or {}).keys() + ) + quality["metrics"]["flatfield_loaded_channels"] = loaded_channels + + required_flat_channels = {"R", "G", "B", "RE", "NIR"} + if not required_flat_channels.issubset(set(loaded_channels)): + mark( + "bad", + "flatfield_missing_channels:" + + ",".join( + sorted(required_flat_channels - set(loaded_channels)) + ), + ) + + flat_result = self.last_flatfield_result or {} + quality["metrics"]["flatfield_result"] = flat_result + + if ( + str(flat_cfg.get("apply_space", "native_camera_space")).lower() + == "native_camera_space" + and ( + not isinstance(flat_result, dict) + or not bool(flat_result.get("applied", False)) + ) + ): + mark( + "bad", + "flatfield_required_but_not_applied", + ) + + if getattr(self, "strict_product_contract", False): + fusion_result = self.last_fusion_result or {} + profiles_used = fusion_result.get("homography_profiles_used", {}) or {} + + for role in ("re", "nir"): + if role not in profiles_used: + mark("bad", f"homography_not_applied:{role}") + + intr = self.intrinsics_config or {} + if bool((intr.get("runtime_undistort", {}) or {}).get("enabled", False)): + mark("bad", "runtime_undistort_enabled_but_not_supported") + + quality["metrics"]["product_contract"] = { + "strict": True, + "frame_controls_source": getattr( + self, + "_last_frame_controls_source", + None, + ), + "homography_profiles_used": profiles_used, + } + # ======================================================== # Resultado final # ======================================================== @@ -2861,53 +3630,725 @@ class RawProcessorCore: ) + def _validate_product_runtime_contract(self, data: dict): + """ + Validação fail-closed do module_params montado pelo assembler de produção. + + Arquivos legados continuam aceitos fora do strict_product_contract. + """ + if not getattr(self, "strict_product_contract", False): + return True + + if self.module_params_schema != "multispec_module_params_v3": + raise RuntimeError( + f"Schema de module_params inválido: {self.module_params_schema!r}" + ) + + if str(data.get("frame_type", "RAW_BRUTO")).upper() != "RAW_BRUTO": + raise RuntimeError( + f"Produto exige frame_type='RAW_BRUTO', veio {data.get('frame_type')!r}." + ) + + if self.bayer_pattern not in ("RGGB", "BGGR", "GRBG", "GBRG"): + raise RuntimeError( + f"Bayer inválido no module_params: {self.bayer_pattern!r}" + ) + + if not isinstance(self.sensor_size_by_role, dict): + raise RuntimeError("sensor_size_by_role ausente/inválido.") + + if not isinstance(self.camera_hardware, dict): + raise RuntimeError("camera_hardware ausente/inválido.") + + for role in ("rgb", "re", "nir"): + size = self.sensor_size_by_role.get(role) + hw = self.camera_hardware.get(role) + + if not ( + isinstance(size, (list, tuple)) + and len(size) == 2 + and int(size[0]) > 0 + and int(size[1]) > 0 + ): + raise RuntimeError( + f"sensor_size_by_role.{role} inválido: {size}" + ) + + if not isinstance(hw, dict): + raise RuntimeError( + f"camera_hardware.{role} ausente." + ) + + hw_size = hw.get("size") + if hw_size is not None: + hw_size = [ + int(hw_size[0]), + int(hw_size[1]), + ] + expected_size = [ + int(size[0]), + int(size[1]), + ] + + if hw_size != expected_size: + raise RuntimeError( + f"Hardware/size divergente em {role}: " + f"hw={hw_size}, size={expected_size}" + ) + + rgb_size = [ + int(self.sensor_width), + int(self.sensor_height), + ] + expected_rgb = [ + int(x) + for x in self.sensor_size_by_role["rgb"] + ] + + if rgb_size != expected_rgb: + raise RuntimeError( + f"sensor_width/height root {rgb_size} != RGB {expected_rgb}" + ) + + rgb_mode = str( + (self.rgb_processing_config or {}).get( + "mode", + "", + ) + ).lower() + + allowed_rgb_modes = { + "linear_demosaic", + "demosaic", + "full_res", + "linear_demosaic_half", + "demosaic_half", + "full_demosaic_half", + "bayer_planes", + "bayer", + "half_res", + } + + if rgb_mode not in allowed_rgb_modes: + raise RuntimeError( + f"rgb_processing.mode inválido no produto: {rgb_mode!r}" + ) + + if ( + str( + (self.fusion_config or {}).get( + "alignment_mode", + "", + ) + ).lower() + != "homography" + ): + raise RuntimeError( + "Produto exige fusion_config.alignment_mode='homography'." + ) + + if not bool( + (self.flatfield_config or {}).get( + "enabled", + False, + ) + ): + raise RuntimeError( + "Produto exige flatfield_config.enabled=true." + ) + + flat_space = str( + (self.flatfield_config or {}).get( + "apply_space", + "native_camera_space", + ) + ).lower() + + if flat_space != "native_camera_space": + raise RuntimeError( + f"Produto exige Flat-Field no espaço nativo; " + f"apply_space={flat_space!r}" + ) + + if bool( + (self.flatfield_config or {}).get( + "subtract_dark", + False, + ) + ): + raise RuntimeError( + "Produto atual exige flatfield_config.subtract_dark=false." + ) + + if not self.flatfield_loaded: + raise RuntimeError( + "Flat-field produto não foi carregado integralmente." + ) + + rad_norm = self.radiometric_normalization_config or {} + + if not bool( + rad_norm.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "Produto exige radiometric_normalization.enabled=true." + ) + + if ( + str( + rad_norm.get( + "method", + "", + ) + ).lower() + != "oak_ae_frame_controls_v1" + ): + raise RuntimeError( + f"Método radiométrico inválido no produto: " + f"{rad_norm.get('method')!r}" + ) + + factor_model = str( + rad_norm.get( + "factor_model", + "exposure_time_us_x_iso", + ) + ).lower() + + if factor_model != "exposure_time_us_x_iso": + raise RuntimeError( + f"factor_model radiométrico inválido: {factor_model!r}" + ) + + apply_stage = str( + rad_norm.get( + "apply_stage", + "after_dark_before_flat_gain", + ) + ).lower() + + if apply_stage != "after_dark_before_flat_gain": + raise RuntimeError( + f"apply_stage radiométrico incompatível: {apply_stage!r}" + ) + + refs = rad_norm.get( + "reference_controls", + {}, + ) or {} + + for role in ("rgb", "re", "nir"): + ref = refs.get(role) + if not isinstance(ref, dict): + raise RuntimeError( + f"radiometric_normalization sem reference_controls.{role}" + ) + + try: + exp = float( + ref.get( + "exposure_time_us", + 0, + ) + ) + iso = float( + ref.get( + "sensitivity_iso", + 0, + ) + ) + except Exception as exc: + raise RuntimeError( + f"reference_controls inválido em {role}: {ref}" + ) from exc + + if exp <= 0 or iso <= 0: + raise RuntimeError( + f"reference_controls inválido em {role}: {ref}" + ) + + if bool( + (self.radiometric_config or {}).get( + "enabled", + False, + ) + ): + raise RuntimeError( + "radiometric_config controller deve estar desligado no produto." + ) + + if bool( + (self.patch_normalization_config or {}).get( + "enabled", + False, + ) + ): + raise RuntimeError( + "patch_normalization deve estar desligado no produto." + ) + + if bool( + (self.rgb_calibration or {}).get( + "enabled", + False, + ) + ): + raise RuntimeError( + "rgb_calibration manual deve estar desligado no produto." + ) + + intr = self.intrinsics_config or {} + + if not bool( + intr.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "Produto exige intrinsics_config.enabled=true." + ) + + und_cfg = ( + intr.get( + "runtime_undistort", + {}, + ) + or {} + ) + + if bool( + und_cfg.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "runtime_undistort=true ainda não é permitido neste core. " + "Recalibre Homography no espaço undistorted e implemente esse " + "estágio antes de ativar." + ) + + intr_space = str( + intr.get( + "calibration_space", + "native_stream_no_external_undistort", + ) + ) + + for role in ("re", "nir"): + entry = self._resolve_homography_geometry_entry_for_role( + role + ) + + hom_space = str( + entry.get( + "coordinate_space", + ) + or "" + ) + + if ( + hom_space + and intr_space + and hom_space != intr_space + ): + raise RuntimeError( + f"Domínio geométrico divergente em {role}: " + f"intrinsics={intr_space}, homography={hom_space}" + ) + + cameras = ( + intr.get( + "cameras", + {}, + ) + or {} + ) + + for role in ("rgb", "re", "nir"): + cam = cameras.get(role) + + if not isinstance(cam, dict): + raise RuntimeError( + f"intrinsics_config.cameras.{role} ausente." + ) + + image_size = cam.get( + "image_size" + ) + expected = [ + int(x) + for x in self.sensor_size_by_role[role] + ] + + if not ( + isinstance(image_size, (list, tuple)) + and len(image_size) == 2 + and [ + int(image_size[0]), + int(image_size[1]), + ] + == expected + ): + raise RuntimeError( + f"Intrinsics size divergente em {role}: " + f"{image_size} != {expected}" + ) + + K = np.asarray( + cam.get( + "camera_matrix" + ), + dtype=np.float64, + ) + + D = np.asarray( + cam.get( + "dist_coeffs" + ), + dtype=np.float64, + ).reshape(-1) + + if ( + K.shape != (3, 3) + or not np.all( + np.isfinite(K) + ) + ): + raise RuntimeError( + f"Intrinsics K inválida em {role}." + ) + + if ( + D.size < 4 + or not np.all( + np.isfinite(D) + ) + ): + raise RuntimeError( + f"Intrinsics D inválida em {role}." + ) + + # Enhancement é não linear e, no caminho legado atual, acontece antes + # da normalização radiométrica. Produto calibrado não pode ativá-lo. + enh = self._get_rgb_enhancement_config() + + if bool( + enh.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "rgb_processing.enhancement deve ficar disabled no pipeline " + "calibrado de produto." + ) + + for role in ("rgb", "re", "nir"): + if role not in ( + self.camera_settings + or {} + ): + raise RuntimeError( + f"camera_settings sem role {role}." + ) + + return True + + + def _validate_stream_camera_contract(self, frame: dict, meta: dict): + """ + Confere por frame se o RAW recebido pertence ao hardware homologado. + + Em modo produto: + - exige CAM_A/CAM_B/CAM_C por role; + - exige resolução nativa correta; + - exige RAW10 packed uint8 2D; + - exige sensor/socket coerentes quando informados; + - exige Bayer RGB coerente. + """ + if not getattr(self, "strict_product_contract", False): + return True + + if not isinstance(frame, dict) or not isinstance(meta, dict): + raise RuntimeError("Contrato stream inválido para produto.") + + stream_meta = ( + meta.get("stream_meta", {}) + if isinstance(meta.get("stream_meta"), dict) + else {} + ) + camera_frames = ( + meta.get("camera_frames", {}) + or stream_meta.get("camera_frames", {}) + or {} + ) + camera_info = ( + meta.get("camera_info", {}) + or stream_meta.get("camera_info", {}) + or {} + ) + + seen_roles = {} + + for cam_id, data in frame.items(): + cam_meta = {} + + if isinstance(camera_info.get(cam_id), dict): + cam_meta.update(camera_info.get(cam_id) or {}) + + if isinstance(camera_frames.get(cam_id), dict): + cam_meta.update(camera_frames.get(cam_id) or {}) + + role = str(cam_meta.get("role", "")).lower() + + if role not in ("rgb", "re", "nir"): + raise RuntimeError( + f"{cam_id}: role inválida/ausente no stream: {role!r}" + ) + + if role in seen_roles: + raise RuntimeError( + f"Role duplicada no stream: {role} " + f"em {seen_roles[role]} e {cam_id}" + ) + + seen_roles[role] = cam_id + + expected_size = self.sensor_size_by_role.get(role) + if not expected_size: + raise RuntimeError(f"MP sem sensor_size_by_role.{role}") + + width = cam_meta.get("width") + height = cam_meta.get("height") + + if width is None or height is None: + raise RuntimeError( + f"{cam_id}/{role}: metadata sem width/height nativos." + ) + + got = [int(width), int(height)] + expected = [int(expected_size[0]), int(expected_size[1])] + + if got != expected: + raise RuntimeError( + f"{cam_id}/{role}: resolução nativa {got} " + f"!= homologada {expected}" + ) + + arr = np.asarray(data) + expected_packed_width = self._raw10_expected_packed_width_fast( + expected[0] + ) + + if arr.ndim != 2 or arr.dtype != np.uint8: + raise RuntimeError( + f"{cam_id}/{role}: produto RAW_BRUTO exige " + f"RAW10 packed uint8 2D; shape={arr.shape}, dtype={arr.dtype}" + ) + + if arr.shape[0] != expected[1]: + raise RuntimeError( + f"{cam_id}/{role}: altura packed={arr.shape[0]} " + f"!= nativa={expected[1]}" + ) + + if arr.shape[1] < expected_packed_width: + raise RuntimeError( + f"{cam_id}/{role}: packed_width={arr.shape[1]} " + f"< mínimo RAW10={expected_packed_width}" + ) + + padding = int(arr.shape[1] - expected_packed_width) + if padding > 64: + raise RuntimeError( + f"{cam_id}/{role}: padding RAW10 excessivo={padding}" + ) + + bit_depth = int(cam_meta.get("bit_depth", 10)) + if bit_depth != 10: + raise RuntimeError( + f"{cam_id}/{role}: produto espera bit_depth=10, " + f"veio {bit_depth}" + ) + + expected_hw = self.camera_hardware.get(role, {}) or {} + expected_sensor = str( + expected_hw.get("sensor", "") or "" + ).upper() + + actual_sensor = str( + cam_meta.get("sensor") + or cam_meta.get("sensor_name") + or "" + ).upper() + + if ( + expected_sensor + and actual_sensor + and actual_sensor != expected_sensor + ): + raise RuntimeError( + f"{cam_id}/{role}: sensor={actual_sensor} " + f"!= homologado={expected_sensor}" + ) + + expected_socket = str( + expected_hw.get("socket", "") or "" + ) + + if ( + expected_socket + and str(cam_id).upper() != expected_socket.upper() + ): + raise RuntimeError( + f"{role}: cam_id={cam_id} " + f"!= socket homologado={expected_socket}" + ) + + if role == "rgb": + actual_bayer = str( + cam_meta.get("bayer_pattern") + or cam_meta.get("bayer") + or self.bayer_pattern + ).upper() + + if actual_bayer != self.bayer_pattern: + raise RuntimeError( + f"RGB Bayer do frame={actual_bayer} " + f"!= MP={self.bayer_pattern}" + ) + + missing = [ + role + for role in ("rgb", "re", "nir") + if role not in seen_roles + ] + + if missing: + raise RuntimeError( + f"Frame produto incompleto. Faltam roles: {missing}" + ) + + return True + def load_config_json(self, path: str): if not path or not os.path.isfile(path): - raise FileNotFoundError(f"Arquivo de calibração não encontrado: {path}") + raise FileNotFoundError( + f"Arquivo de calibração não encontrado: {path}" + ) with open(path, "r", encoding="utf-8") as f: data = json.load(f) - self.bayer_pattern = data.get("bayer_pattern", self.bayer_pattern) + if not isinstance(data, dict): + raise RuntimeError("module_params root deve ser dict/object.") + + self.calibration_json_path = path + self.calibration_base_dir = os.path.dirname(os.path.abspath(path)) + + self.strict_product_contract = bool( + isinstance(data.get("assembly_metadata"), dict) + and isinstance(data.get("camera_hardware"), dict) + ) + + self.module_params_schema = data.get("schema") + self.assembly_metadata = deepcopy(data.get("assembly_metadata", {}) or {}) + self.calibration_provenance = deepcopy( + data.get("calibration_provenance", {}) or {} + ) + self.camera_hardware = deepcopy(data.get("camera_hardware", {}) or {}) + self.sensor_size_by_role = deepcopy( + data.get("sensor_size_by_role", {}) or {} + ) + self.intrinsics_config = deepcopy(data.get("intrinsics_config", {}) or {}) + + if data.get("sensor_width") is not None: + self.sensor_width = int(data["sensor_width"]) + + if data.get("sensor_height") is not None: + self.sensor_height = int(data["sensor_height"]) + + self.bayer_pattern = str( + data.get("bayer_pattern", self.bayer_pattern) + ).upper() rgb_proc = data.get("rgb_processing") if isinstance(rgb_proc, dict): - self.rgb_processing_config = self._merge_config(self.rgb_processing_config, rgb_proc) + self.rgb_processing_config = self._merge_config( + self.rgb_processing_config, + rgb_proc, + ) fusion = data.get("fusion_config") if isinstance(fusion, dict): - self.fusion_config = self._merge_config(self.fusion_config, fusion) + self.fusion_config = self._merge_config( + self.fusion_config, + fusion, + ) + elif self.strict_product_contract: + raise RuntimeError("module_params produto sem fusion_config.") else: print("[WARN] JSON sem fusion_config. Mantendo config padrão.") rgb_cal = data.get("rgb_calibration") if isinstance(rgb_cal, dict): - self.rgb_calibration = self._merge_config(self.rgb_calibration, rgb_cal) + self.rgb_calibration = self._merge_config( + self.rgb_calibration, + rgb_cal, + ) + + radiometric = data.get("radiometric_config") + if isinstance(radiometric, dict): + self.radiometric_config = self._merge_config( + self.radiometric_config, + radiometric, + ) + + rad_norm = data.get("radiometric_normalization") + if isinstance(rad_norm, dict): + self.radiometric_normalization_config = self._merge_config( + self.radiometric_normalization_config, + rad_norm, + ) + + patch_norm = data.get("patch_normalization") + if isinstance(patch_norm, dict): + self.patch_normalization_config = self._merge_config( + self.patch_normalization_config, + patch_norm, + ) + + cam_set = data.get("camera_settings") + if isinstance(cam_set, dict): + self.camera_settings = self._merge_config( + self.camera_settings, + cam_set, + ) flatfield = data.get("flatfield_config") if isinstance(flatfield, dict): - self.flatfield_config = self._merge_config(self.flatfield_config, flatfield) - self.load_flatfield_maps() + self.flatfield_config = self._merge_config( + self.flatfield_config, + flatfield, + ) else: self.flatfield_config["enabled"] = False self.flatfield_maps = {} self.flatfield_loaded = False - radiometric = data.get("radiometric_config") - if isinstance(radiometric, dict): - self.radiometric_config = self._merge_config(self.radiometric_config, radiometric) + self.clear_direct_fusion_geometry_cache() + self._flatfield_runtime_cache = {} - rad_norm_config = data.get("radiometric_normalization") - if isinstance(rad_norm_config, dict): - self.radiometric_normalization_config = self._merge_config(self.radiometric_normalization_config, rad_norm_config) + if self.flatfield_config.get("enabled", False): + self.load_flatfield_maps() - patch_norm = data.get("patch_normalization") - if isinstance(patch_norm, dict): - self.patch_normalization_config = self._merge_config(self.patch_normalization_config, patch_norm) - - cam_set = data.get("camera_settings") - if isinstance(cam_set, dict): - self.camera_settings = self._merge_config(self.camera_settings, cam_set) + self._validate_product_runtime_contract(data) + return data def _merge_config(self, default_cfg: dict, loaded_cfg: dict) -> dict: cfg = json.loads(json.dumps(default_cfg)) @@ -2961,55 +4402,110 @@ class RawProcessorCore: npz_file = cfg.get("npz_file") if not npz_file: - print("[WARN] flatfield_config habilitado, mas sem npz_file.") + msg = "flatfield_config habilitado, mas sem npz_file." self.flatfield_maps = {} self.flatfield_loaded = False + + if getattr(self, "strict_product_contract", False): + raise RuntimeError(msg) + + print(f"[WARN] {msg}") return False npz_path = self._resolve_calibration_path(npz_file) if not os.path.isfile(npz_path): - print(f"[WARN] Arquivo flat-field não encontrado: {npz_file} -> {npz_path}") + msg = f"Arquivo flat-field não encontrado: {npz_file} -> {npz_path}" self.flatfield_maps = {} self.flatfield_loaded = False - return False - data = np.load(npz_path) + if getattr(self, "strict_product_contract", False): + raise FileNotFoundError(msg) + + print(f"[WARN] {msg}") + return False maps = {} channel_maps = cfg.get("channel_maps", {}) or {} channels = cfg.get("channels", ["R", "G", "B", "RE", "NIR"]) - for ch in channels: - ch = str(ch).upper() - ch_cfg = channel_maps.get(ch, {}) or {} + with np.load(npz_path, allow_pickle=False) as data: + for ch in channels: + ch = str(ch).upper() + ch_cfg = channel_maps.get(ch, {}) or {} - gain_key = ch_cfg.get("gain_key", f"gain_{ch}") - dark_key = ch_cfg.get("dark_median_key", f"dark_median_{ch}") + if isinstance(ch_cfg, str): + ch_cfg = {"gain_key": ch_cfg} - if gain_key not in data: - print(f"[WARN] Flat-field sem chave {gain_key} para canal {ch}.") - continue + gain_key = ch_cfg.get("gain_key", f"gain_{ch}") + dark_key = ch_cfg.get("dark_median_key", f"dark_median_{ch}") - entry = { - "gain": data[gain_key].astype(np.float32), - "gain_key": gain_key, - } + if gain_key not in data: + if getattr(self, "strict_product_contract", False): + raise RuntimeError( + f"Flat-field sem chave obrigatória {gain_key} para {ch}." + ) - if dark_key and dark_key in data: - entry["dark"] = data[dark_key].astype(np.float32) - entry["dark_key"] = dark_key + print(f"[WARN] Flat-field sem chave {gain_key} para canal {ch}.") + continue - maps[ch] = entry + gain = np.asarray(data[gain_key], dtype=np.float32) + + if gain.ndim != 2 or gain.size == 0: + raise RuntimeError( + f"Flat-field {ch}/{gain_key} shape inválido: {gain.shape}" + ) + + if not np.all(np.isfinite(gain)): + raise RuntimeError( + f"Flat-field {ch}/{gain_key} contém NaN/Inf." + ) + + if np.any(gain <= 0): + raise RuntimeError( + f"Flat-field {ch}/{gain_key} contém ganho <= 0." + ) + + entry = { + "gain": gain, + "gain_key": gain_key, + } + + if dark_key and dark_key in data: + dark = np.asarray(data[dark_key], dtype=np.float32) + if dark.ndim != 2 or not np.all(np.isfinite(dark)): + raise RuntimeError( + f"Dark map {ch}/{dark_key} inválido." + ) + + entry["dark"] = dark + entry["dark_key"] = dark_key + + maps[ch] = entry + + required = {"R", "G", "B", "RE", "NIR"} self.flatfield_maps = maps - self.flatfield_loaded = len(maps) > 0 + self.flatfield_loaded = required.issubset(set(maps)) + + if getattr(self, "strict_product_contract", False) and not self.flatfield_loaded: + missing = sorted(required - set(maps)) + raise RuntimeError( + f"Flat-field produto incompleto. Faltam canais: {missing}" + ) if self.flatfield_loaded: - print(f"[OK] Flat-field carregado: {npz_path} | canais={list(maps.keys())}") + print( + f"[OK] Flat-field carregado: {npz_path} | " + f"canais={list(maps.keys())}" + ) else: - print(f"[WARN] Flat-field habilitado, mas nenhum mapa foi carregado: {npz_path}") + print( + f"[WARN] Flat-field parcial: {npz_path} | " + f"canais={list(maps.keys())}" + ) + self._flatfield_runtime_cache = {} return self.flatfield_loaded def apply_dark_to_decoded(self, decoded: dict) -> dict: @@ -3077,50 +4573,97 @@ class RawProcessorCore: def apply_flat_gain_to_decoded(self, decoded: dict) -> dict: cfg = self.flatfield_config or {} + result = { + "enabled": bool(cfg.get("enabled", False)), + "applied": False, + "by_role": {}, + "warnings": [], + } + self.last_flatfield_result = result + if not cfg.get("enabled", False): + result["warnings"].append("flatfield_disabled") return decoded if not self.flatfield_loaded: self.load_flatfield_maps() if not self.flatfield_loaded: + result["warnings"].append("flatfield_maps_not_loaded") return decoded clip_output = bool(cfg.get("clip_output", True)) corrected = {} - # Guarda anti-roxo / anti-artefato em saturação - sat_guard_enabled = bool(cfg.get("saturation_guard_enabled", True)) - sat_mode = str(cfg.get("saturation_guard_mode", "fade_strength")).lower() - sat_threshold = float(cfg.get("saturation_guard_threshold", 0.97)) + sat_guard_enabled = bool( + cfg.get( + "saturation_guard_enabled", + True, + ) + ) + sat_mode = str( + cfg.get( + "saturation_guard_mode", + "fade_strength", + ) + ).lower() + sat_threshold = float( + cfg.get( + "saturation_guard_threshold", + 0.97, + ) + ) for cam_id, item in decoded.items(): - role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower() + role = str( + item.get("role") + or item.get("meta", {}).get("role") + or "" + ).lower() + img = item.get("image") if img is None: corrected[cam_id] = item + result["warnings"].append( + f"{cam_id}:missing_image" + ) continue new_item = dict(item) - new_meta = dict(item.get("meta", {}) or {}) + new_meta = dict( + item.get("meta", {}) + or {} + ) if role == "rgb": - if img.ndim != 3 or img.shape[2] < 3: + if ( + img.ndim != 3 + or img.shape[2] < 3 + ): corrected[cam_id] = item + result["warnings"].append( + f"{cam_id}:invalid_rgb_shape:{img.shape}" + ) continue - out = img.astype(np.float32).copy() + out, reused = self._radiometric_get_writable_float32_image( + img + ) - # Máscara comum RGB: - # se qualquer canal estiver perto de saturar, tratamos os 3 canais juntos. - # Isso evita R/G/B receberem correções diferentes e criarem magenta/roxo. - saturation_mask = None + # Máscara comum sem criar rgb_max float32 full-res. if sat_guard_enabled: - rgb_max = np.max(out[:, :, :3], axis=2) - saturation_mask = rgb_max >= sat_threshold + saturation_mask = ( + (out[:, :, 0] >= sat_threshold) + | (out[:, :, 1] >= sat_threshold) + | (out[:, :, 2] >= sat_threshold) + ) + else: + saturation_mask = None - for idx, ch in enumerate(("R", "G", "B")): + for idx, ch in enumerate( + ("R", "G", "B") + ): out[:, :, idx] = self._apply_flat_gain_single_channel( out[:, :, idx], ch, @@ -3130,29 +4673,61 @@ class RawProcessorCore: new_item["image"] = out + result["by_role"]["rgb"] = { + "camera_id": cam_id, + "channels": ["R", "G", "B"], + "applied": True, + "inplace_reused_input": bool(reused), + "shape": list(out.shape), + } + elif role in ("re", "nir"): - ch = "RE" if role == "re" else "NIR" + ch = ( + "RE" + if role == "re" + else "NIR" + ) + + img_f, reused = self._radiometric_get_writable_float32_image( + img + ) - # Para RE/NIR a guarda pode ser por canal mesmo. - # Não existe cor roxa aqui, mas ainda evita mexer em pixels clipados. - saturation_mask = None - img_f = img.astype(np.float32) if sat_guard_enabled: - saturation_mask = img_f >= sat_threshold + saturation_mask = ( + img_f >= sat_threshold + ) + else: + saturation_mask = None - new_item["image"] = self._apply_flat_gain_single_channel( + out = self._apply_flat_gain_single_channel( img_f, ch, clip_output=clip_output, saturation_mask=saturation_mask, ) + new_item["image"] = out + + result["by_role"][role] = { + "camera_id": cam_id, + "channels": [ch], + "applied": True, + "inplace_reused_input": bool(reused), + "shape": list(out.shape), + } + else: corrected[cam_id] = item + result["warnings"].append( + f"{cam_id}:unsupported_role:{role}" + ) continue new_meta["flatfield_applied"] = True - new_meta["flatfield_map_type"] = cfg.get("map_type", "gain") + new_meta["flatfield_map_type"] = cfg.get( + "map_type", + "gain", + ) new_meta["flatfield_saturation_guard"] = { "enabled": sat_guard_enabled, "mode": sat_mode, @@ -3162,6 +4737,45 @@ class RawProcessorCore: new_item["meta"] = new_meta corrected[cam_id] = new_item + required_roles = { + "rgb", + "re", + "nir", + } + applied_roles = { + role + for role, info in result["by_role"].items() + if isinstance(info, dict) + and bool(info.get("applied", False)) + } + + result["applied"] = ( + required_roles.issubset( + applied_roles + ) + ) + result["applied_roles"] = sorted( + applied_roles + ) + result["missing_roles"] = sorted( + required_roles + - applied_roles + ) + + if ( + getattr( + self, + "strict_product_contract", + False, + ) + and not result["applied"] + ): + raise RuntimeError( + "Flat-Field obrigatório não foi aplicado em todas as roles: " + f"missing={result['missing_roles']}" + ) + + self.last_flatfield_result = result return corrected def _subtract_dark_single_channel( @@ -3289,7 +4903,93 @@ class RawProcessorCore: saturation_mask = base >= sat_threshold # ============================================================ - # Calcula ganho efetivo + # Fast path Numba: saturation guard + gain + clip em uma passada. + # ============================================================ + if ( + _HAS_NUMBA + and sat_guard_enabled + and saturation_mask is not None + and sat_mode in ("fade_strength", "skip") + ): + base_c = base + if not base_c.flags.c_contiguous: + base_c = np.ascontiguousarray(base_c) + + gain_c = gain.astype(np.float32, copy=False) + if not gain_c.flags.c_contiguous: + gain_c = np.ascontiguousarray(gain_c) + + mask_c = np.asarray( + saturation_mask, + dtype=np.bool_, + ) + if not mask_c.flags.c_contiguous: + mask_c = np.ascontiguousarray(mask_c) + + if mask_c.shape != base_c.shape: + raise RuntimeError( + f"saturation_mask shape inválido: " + f"{mask_c.shape} != {base_c.shape}" + ) + + mode_code = 1 if sat_mode == "fade_strength" else 2 + + out = np.empty( + base_c.shape, + dtype=np.float32, + ) + + t_numba0 = time.perf_counter() + + _apply_flat_gain_guard_numba( + base_c, + gain_c, + mask_c, + out, + int(base_c.shape[0]), + int(base_c.shape[1]), + float(strength), + float(gain_min_runtime), + float(gain_max_runtime), + float(sat_soft_start), + float(sat_hard), + bool(clip_output), + int(mode_code), + ) + + t_total_ms = ( + time.perf_counter() + - t_total0 + ) * 1000.0 + + if not hasattr( + self, + "_last_flat_ch_perf_log_ts", + ): + self._last_flat_ch_perf_log_ts = 0.0 + + now = time.time() + + if ( + now + - self._last_flat_ch_perf_log_ts + >= 1.0 + ): + self._last_flat_ch_perf_log_ts = now + + print( + "[PERF][FLAT_CH_NUMBA] " + f"ch={ch} " + f"shape={base_c.shape} " + f"total={t_total_ms:.2f}ms " + f"mode={sat_mode} " + f"strength={strength:.2f}" + ) + + return out + + # ============================================================ + # Fallback NumPy: calcula ganho efetivo. # ============================================================ t0 = time.perf_counter() @@ -3382,8 +5082,14 @@ class RawProcessorCore: result["warnings"].append("missing_frame_controls") self.last_radiometric_normalization_result = result - if str(cfg.get("missing_controls_policy", "skip")).lower() == "raise": - raise RuntimeError("radiometric_normalization ativo, mas meta.frame_controls ausente.") + if ( + getattr(self, "strict_product_contract", False) + or str(cfg.get("missing_controls_policy", "skip")).lower() == "raise" + ): + raise RuntimeError( + "radiometric_normalization ativo, mas controles reais " + "da captura estão ausentes." + ) return decoded @@ -3422,8 +5128,14 @@ class RawProcessorCore: f"{role}:invalid_factor actual={actual_factor:.6g} ref={ref_factor:.6g}" ) - if str(cfg.get("invalid_controls_policy", "skip")).lower() == "raise": - raise RuntimeError(f"Controles radiométricos inválidos para role={role}: {actual_ctrl}") + if ( + getattr(self, "strict_product_contract", False) + or str(cfg.get("invalid_controls_policy", "skip")).lower() == "raise" + ): + raise RuntimeError( + f"Controles radiométricos inválidos para role={role}: " + f"{actual_ctrl}" + ) continue @@ -3487,53 +5199,87 @@ class RawProcessorCore: self.last_radiometric_normalization_result = result return normalized - def _extract_frame_controls_from_meta_by_role(self, meta: dict | None) -> tuple[dict, dict]: + def _extract_frame_controls_from_meta_by_role( + self, + meta: dict | None, + ) -> tuple[dict, dict]: """ - Retorna: - controls_by_role = { - "rgb": {...}, - "re": {...}, - "nir": {...} - } + Resolve controles reais da captura com compatibilidade histórica. - controls_by_cam = { - "CAM_A": {...}, - "CAM_B": {...}, - "CAM_C": {...} - } + Prioridade: + 1. frame_controls + 2. actual_camera_controls + 3. camera_controls + 4. startup_camera_controls - Fonte principal: - meta["frame_controls"] - - Também aceita: - meta["stream_meta"]["frame_controls"] + Aceita chaves CAM_A/B/C ou rgb/re/nir. """ if not isinstance(meta, dict): return {}, {} - stream_meta = meta.get("stream_meta") if isinstance(meta.get("stream_meta"), dict) else None + stream_meta = ( + meta.get("stream_meta") + if isinstance(meta.get("stream_meta"), dict) + else {} + ) - frame_controls = meta.get("frame_controls") - if not isinstance(frame_controls, dict) and stream_meta is not None: - frame_controls = stream_meta.get("frame_controls") + controls = None + source_key = None - if not isinstance(frame_controls, dict) or not frame_controls: + for key in ( + "frame_controls", + "actual_camera_controls", + "camera_controls", + "startup_camera_controls", + ): + candidate = meta.get(key) + + if not isinstance(candidate, dict) and stream_meta: + candidate = stream_meta.get(key) + + if isinstance(candidate, dict) and candidate: + controls = candidate + source_key = key + break + + if not isinstance(controls, dict) or not controls: return {}, {} camera_info = meta.get("camera_info") - if not isinstance(camera_info, dict) and stream_meta is not None: + if not isinstance(camera_info, dict): camera_info = stream_meta.get("camera_info") camera_info = camera_info if isinstance(camera_info, dict) else {} controls_by_cam = {} controls_by_role = {} + role_to_cam = {} - for cam_id, ctrl in frame_controls.items(): + for cam_id, info in camera_info.items(): + if not isinstance(info, dict): + continue + role = str(info.get("role", "")).lower() + if role in ("rgb", "re", "nir"): + role_to_cam[role] = str(cam_id) + + for key, ctrl in controls.items(): if not isinstance(ctrl, dict): continue - cam_id = str(cam_id) + key_str = str(key) + key_lower = key_str.lower() + + if key_lower in ("rgb", "re", "nir"): + role = key_lower + cam_id = role_to_cam.get(role) + + if cam_id: + controls_by_cam[cam_id] = dict(ctrl) + + controls_by_role[role] = dict(ctrl) + continue + + cam_id = key_str controls_by_cam[cam_id] = dict(ctrl) role = str( @@ -3541,12 +5287,12 @@ class RawProcessorCore: ).lower() if not role: - # fallback defensivo caso algum meta antigo venha sem camera_info role = self._role_from_cam_id_fallback(cam_id) if role: controls_by_role[role] = dict(ctrl) + self._last_frame_controls_source = source_key return controls_by_role, controls_by_cam def _role_from_cam_id_fallback(self, cam_id: str) -> str: @@ -4256,17 +6002,53 @@ class RawProcessorCore: - def _direct_fusion_get_target_size_fast(self, ref_size): + def _direct_fusion_get_target_size_fast( + self, + ref_size, + target_size_override=None, + ): """ - Resolve target_size final como (target_w, target_h). - Usa fusion_config.target_size se existir, senão usa tamanho de referência. + Resolve o target final como (W,H). + + Prioridade: + 1. target_size_override do caller (treino/inferência); + 2. fusion_config.target_size; + 3. tamanho RGB/ref. + + Isso evita fuse -> resize extra no caminho canônico RAW_BRUTO. """ ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) - fusion = getattr(self, "fusion_config", {}) or {} - target_size = fusion.get("target_size", None) - if isinstance(target_size, (list, tuple)) and len(target_size) == 2: - return int(target_size[0]), int(target_size[1]) + if ( + isinstance(target_size_override, (list, tuple)) + and len(target_size_override) == 2 + ): + tw = int(target_size_override[0]) + th = int(target_size_override[1]) + + if tw <= 0 or th <= 0: + raise RuntimeError( + f"target_size_override inválido: {target_size_override}" + ) + + return tw, th + + fusion = getattr(self, "fusion_config", {}) or {} + target_size = fusion.get("target_size") + + if ( + isinstance(target_size, (list, tuple)) + and len(target_size) == 2 + ): + tw = int(target_size[0]) + th = int(target_size[1]) + + if tw <= 0 or th <= 0: + raise RuntimeError( + f"fusion_config.target_size inválido: {target_size}" + ) + + return tw, th return int(ref_w), int(ref_h) @@ -4285,48 +6067,85 @@ class RawProcessorCore: return (0, 0, ref_w, ref_h), False - def _direct_fusion_crop_to_target_matrix_fast(self, crop_box, target_size): + def _direct_fusion_crop_to_target_matrix_fast( + self, + crop_box, + target_size, + ): """ - Matriz C que leva coordenadas do espaço RGB/ref para a saída final. + Matriz C: RGB/reference -> target final. - crop_box está no espaço da imagem de referência: - x0,y0,x1,y1 + Replica crop + cv2.resize com convenção de centro de pixel: - Queremos: - x=x0 -> 0 - x=x1 -> target_w - y=y0 -> 0 - y=y1 -> target_h - - Retorna C_ref_to_target. + x_crop = x_ref - x0 + x_target = (x_crop + 0.5) * sx - 0.5 """ - x0, y0, x1, y1 = [float(v) for v in crop_box] - target_w, target_h = int(target_size[0]), int(target_size[1]) + x0, y0, x1, y1 = [ + float(v) + for v in crop_box + ] - crop_w = max(1.0, x1 - x0) - crop_h = max(1.0, y1 - y0) + target_w, target_h = [ + int(v) + for v in target_size + ] - sx = float(target_w) / crop_w - sy = float(target_h) / crop_h + crop_w = max( + 1.0, + x1 - x0, + ) + crop_h = max( + 1.0, + y1 - y0, + ) - C = np.array( + sx = ( + float(target_w) + / crop_w + ) + sy = ( + float(target_h) + / crop_h + ) + + tx = ( + -x0 * sx + + 0.5 * sx + - 0.5 + ) + ty = ( + -y0 * sy + + 0.5 * sy + - 0.5 + ) + + return np.array( [ - [sx, 0.0, -x0 * sx], - [0.0, sy, -y0 * sy], + [sx, 0.0, tx], + [0.0, sy, ty], [0.0, 0.0, 1.0], ], dtype=np.float32, ) - return C - - def _direct_fusion_scale_homography_for_ref_fast(self, H, meta, ref_size): + def _direct_fusion_scale_homography_for_ref_fast( + self, + H, + meta, + ref_size, + ): """ - Escala a homografia calibrada para o runtime da referência RGB. + Helper legado sem informação da source. - Usa a própria função existente _scale_homography_to_runtime() se existir. - Isso mantém compatibilidade com o contrato atual do core. + Em produto mixed-resolution ele é ambíguo e portanto proibido. + Use _direct_fusion_get_role_homography_fast(role, ..., source_size=...). """ + if getattr(self, "strict_product_contract", False): + raise RuntimeError( + "_direct_fusion_scale_homography_for_ref_fast() não pode ser usado " + "no produto mixed-resolution porque não conhece source_size." + ) + fusion = getattr(self, "fusion_config", {}) or {} ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) @@ -4337,118 +6156,137 @@ class RawProcessorCore: or None ) - H = np.asarray(H, dtype=np.float32) - - if hasattr(self, "_scale_homography_to_runtime"): - try: - return self._scale_homography_to_runtime( - H, - calib_size=calib_size, - runtime_size=(ref_w, ref_h), - ).astype(np.float32) - except TypeError: - try: - return self._scale_homography_to_runtime( - H, - calib_size, - (ref_w, ref_h), - ).astype(np.float32) - except Exception: - pass - except Exception: - pass - - # Fallback local. - if calib_size is None: - if abs(float(H[2, 2])) > 1e-9: - H = H / H[2, 2] - return H.astype(np.float32) - - calib_w, calib_h = float(calib_size[0]), float(calib_size[1]) - if calib_w <= 0 or calib_h <= 0: - return H.astype(np.float32) - - sx = float(ref_w) / calib_w - sy = float(ref_h) / calib_h - - S = np.array([[sx, 0.0, 0.0], [0.0, sy, 0.0], [0.0, 0.0, 1.0]], dtype=np.float32) - S_inv = np.array([[1.0 / sx, 0.0, 0.0], [0.0, 1.0 / sy, 0.0], [0.0, 0.0, 1.0]], dtype=np.float32) - - H_runtime = S @ H @ S_inv - if abs(float(H_runtime[2, 2])) > 1e-9: - H_runtime = H_runtime / H_runtime[2, 2] - - return H_runtime.astype(np.float32) - - def _direct_fusion_get_role_homography_fast(self, role, meta, ref_size): - """ - Retorna H_role_to_rgb escalada para o espaço da referência RGB. - - Suporta: - - contrato antigo: fusion_config.homographies.re_to_rgb/nir_to_rgb - - contrato novo: fusion_config.homography_profiles..homographies.* - """ - role = str(role).lower() - - H, calib_size, profile_name = self._resolve_homography_entry_for_role(role) - - ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) - - H_scaled = self._scale_homography_to_runtime( + return self._scale_homography_to_runtime( H, calib_size=calib_size, runtime_size=(ref_w, ref_h), + ).astype(np.float32) + + def _direct_fusion_get_role_homography_fast( + self, + role, + meta, + ref_size, + source_size=None, + ): + """ + Retorna H_role_to_rgb no espaço REAL de runtime. + + ref_size: + (ref_h, ref_w) da imagem RGB já decodificada. + + source_size: + (src_h, src_w) da banda RE/NIR REAL, sem resize prévio. + """ + role = str(role).lower() + entry = self._resolve_homography_geometry_entry_for_role(role) + + ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) + + if source_size is None: + src_w, src_h = entry["source_calibration_size"] + else: + src_h, src_w = int(source_size[0]), int(source_size[1]) + + H_scaled = self._scale_homography_to_runtime( + entry["H"], + calib_source_size=entry["source_calibration_size"], + calib_reference_size=entry["reference_calibration_size"], + runtime_source_size=(int(src_w), int(src_h)), + runtime_reference_size=(int(ref_w), int(ref_h)), ) if H_scaled is None or H_scaled.shape != (3, 3): raise RuntimeError( - f"Homografia inválida para role={role}, profile={profile_name}: " + f"Homografia inválida para role={role}, " + f"profile={entry['profile_name']}: " f"shape={None if H_scaled is None else H_scaled.shape}" ) return H_scaled.astype(np.float32) - def _direct_fusion_resize_spec_to_ref_if_needed_fast(self, img, ref_size): + def _direct_fusion_resize_spec_to_ref_if_needed_fast( + self, + img, + ref_size, + ): """ - Mantém compatibilidade com o fluxo atual: - se RE/NIR não estão no mesmo shape do RGB de referência, redimensiona para ref. + Compatibilidade legado. + + No pipeline de produto mixed-resolution, pré-resize da banda antes da + homografia é proibido. O caminho ativo não chama mais este método. + + Em MP legado, preserva o comportamento histórico. """ ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) if img.shape[:2] == (ref_h, ref_w): return img + if getattr(self, "strict_product_contract", False): + raise RuntimeError( + "Pré-resize de RE/NIR para o tamanho RGB é inválido no " + "contrato mixed-resolution de produto." + ) + return cv2.resize( img.astype(np.float32, copy=False), (ref_w, ref_h), interpolation=cv2.INTER_LINEAR, ) - def _direct_fusion_compute_valid_masks_fast(self, decoded, role_to_cam, ref_size, meta): + def _direct_fusion_compute_valid_masks_fast( + self, + decoded, + role_to_cam, + ref_size, + meta, + ): """ - Calcula máscaras válidas no espaço RGB/ref para crop comum. - Usa warpPerspective apenas em máscara uint8, que costuma ser barato. + Máscaras válidas no espaço RGB/ref para mixed-resolution. + + A máscara de cada banda nasce no tamanho REAL da própria source. """ ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) - valid_masks = [np.ones((ref_h, ref_w), dtype=np.uint8)] - - base = np.ones((ref_h, ref_w), dtype=np.uint8) * 255 + valid_masks = [ + np.ones((ref_h, ref_w), dtype=np.uint8) + ] for role in ("re", "nir"): - if role not in role_to_cam: + cam_id = role_to_cam.get(role) + if cam_id is None: continue - H = self._direct_fusion_get_role_homography_fast(role, meta, ref_size) + img = decoded[cam_id]["image"] + + H = self._direct_fusion_get_role_homography_fast( + role, + meta, + ref_size, + source_size=img.shape[:2], + ) + + source_mask = ( + np.ones( + img.shape[:2], + dtype=np.uint8, + ) + * 255 + ) + mask = cv2.warpPerspective( - base, + source_mask, H, (ref_w, ref_h), flags=cv2.INTER_NEAREST, borderMode=cv2.BORDER_CONSTANT, borderValue=0, ) - valid_masks.append(mask) + + valid_masks.append( + (mask > 0).astype(np.uint8) + ) return valid_masks @@ -4475,23 +6313,34 @@ class RawProcessorCore: tensor[1] = rgb_out[:, :, 1] tensor[2] = rgb_out[:, :, 2] - def _direct_fusion_write_spec_fast(self, tensor, channel_index, img, role, C_ref_to_target, ref_size, target_size, meta): + def _direct_fusion_write_spec_fast( + self, + tensor, + channel_index, + img, + role, + C_ref_to_target, + ref_size, + target_size, + meta, + ): """ - Escreve RE ou NIR direto no tensor final, compondo: - M = C_ref_to_target @ H_role_to_rgb + RE/NIR native -> target final em um único warp. - img é primeiro redimensionada para ref_size se necessário, para manter o mesmo - comportamento geométrico do fluxo atual. + M = C_ref_to_target @ H_runtime(source_real -> RGB/ref_real) """ target_w, target_h = int(target_size[0]), int(target_size[1]) - img_ref = self._direct_fusion_resize_spec_to_ref_if_needed_fast(img, ref_size) - - H_role_to_rgb = self._direct_fusion_get_role_homography_fast(role, meta, ref_size) + H_role_to_rgb = self._direct_fusion_get_role_homography_fast( + role, + meta, + ref_size, + source_size=img.shape[:2], + ) M_role_to_target = (C_ref_to_target @ H_role_to_rgb).astype(np.float32) out = cv2.warpPerspective( - img_ref.astype(np.float32, copy=False), + img.astype(np.float32, copy=False), M_role_to_target, (target_w, target_h), flags=cv2.INTER_LINEAR, @@ -4501,9 +6350,18 @@ class RawProcessorCore: tensor[int(channel_index)] = out - def _fuse_multispec_direct_to_target_fast(self, decoded, meta, channels_expected): + def _fuse_multispec_direct_to_target_fast( + self, + decoded, + meta, + channels_expected, + target_size_override=None, + ): """ Fusão direta otimizada com cache de geometria fixa. + + target_size_override permite ir diretamente ao tamanho final pedido + pelo treino/inferência, evitando um segundo resize posterior. """ channels_expected = self._validate_physical_channel_count(channels_expected) t0 = time.perf_counter() @@ -4518,7 +6376,10 @@ class RawProcessorCore: ref_h, ref_w = rgb.shape[:2] ref_size = (int(ref_h), int(ref_w)) - target_size = self._direct_fusion_get_target_size_fast(ref_size) + target_size = self._direct_fusion_get_target_size_fast( + ref_size, + target_size_override=target_size_override, + ) target_w, target_h = int(target_size[0]), int(target_size[1]) role_to_cam = { @@ -4747,39 +6608,49 @@ class RawProcessorCore: - def _direct_fusion_get_geometry_cache_key_fast(self, ref_size, target_size, role_to_cam): + def _direct_fusion_get_geometry_cache_key_fast( + self, + decoded, + ref_size, + target_size, + role_to_cam, + ): """ - Chave simples e estável para cache da geometria. - - Considera: - - tamanho do RGB/ref - - target final - - roles presentes - - crop/resize - - homografia efetivamente selecionada por perfil - - calibration_size efetivo por role + Mixed-resolution exige que o tamanho REAL de cada source faça parte + da chave, pois H_runtime depende de source_size e reference_size. """ ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) target_w, target_h = int(target_size[0]), int(target_size[1]) fusion = getattr(self, "fusion_config", {}) or {} - roles = tuple(sorted([str(r).lower() for r in role_to_cam.keys()])) + roles = tuple(sorted(str(r).lower() for r in role_to_cam.keys())) + + role_shapes = [] + for role in roles: + cam_id = role_to_cam.get(role) + if cam_id is None: + continue + img = decoded[cam_id].get("image") + if img is None: + continue + role_shapes.append((role, tuple(int(v) for v in img.shape[:2]))) def h_sig_for_role(role): role = str(role).lower() - if role not in role_to_cam: return None try: - H, calib_size, profile_name = self._resolve_homography_entry_for_role(role) + e = self._resolve_homography_geometry_entry_for_role(role) except Exception: return None - arr = np.asarray(H, dtype=np.float32).reshape(-1) + arr = np.asarray(e["H"], dtype=np.float32).reshape(-1) return ( - str(profile_name), - tuple(calib_size or []), + str(e["profile_name"]), + tuple(e["source_calibration_size"] or []), + tuple(e["reference_calibration_size"] or []), + str(e.get("coordinate_space") or ""), tuple(np.round(arr, 8).tolist()), ) @@ -4789,6 +6660,7 @@ class RawProcessorCore: target_w, target_h, roles, + tuple(sorted(role_shapes)), bool(fusion.get("crop_valid_common", False)), bool(fusion.get("resize_after_crop", False)), h_sig_for_role("re"), @@ -4807,26 +6679,31 @@ class RawProcessorCore: self._direct_fusion_remap_cache_hits = 0 self._direct_fusion_remap_cache_misses = 0 - def _direct_fusion_get_geometry_cached_fast(self, decoded, role_to_cam, ref_size, target_size, meta): + def _direct_fusion_get_geometry_cached_fast( + self, + decoded, + role_to_cam, + ref_size, + target_size, + meta, + ): """ - Retorna geometria cacheada para a fusão direta. + Geometria fixa cacheada. - Saída: - geom = { - key, - crop_box, - crop_applied, - C_ref_to_target, - H_role_to_rgb: {re,nir}, - M_role_to_target: {re,nir}, - valid_masks, # opcional/debug - prepare_cache_hit, - } + H_role_to_rgb sempre mapeia: + banda no tamanho REAL atual -> RGB/ref atual. + + Nenhum resize de RE/NIR acontece antes da homografia. """ if not hasattr(self, "_direct_fusion_geometry_cache"): self.clear_direct_fusion_geometry_cache() - key = self._direct_fusion_get_geometry_cache_key_fast(ref_size, target_size, role_to_cam) + key = self._direct_fusion_get_geometry_cache_key_fast( + decoded, + ref_size, + target_size, + role_to_cam, + ) cache = self._direct_fusion_geometry_cache if key in cache: @@ -4841,34 +6718,46 @@ class RawProcessorCore: ref_h, ref_w = int(ref_size[0]), int(ref_size[1]) - # ------------------------------------------------------------ - # Homografias escaladas para runtime. - # ------------------------------------------------------------ H_role_to_rgb = {} homography_profiles_used = {} - for role in ("re", "nir"): - if role in role_to_cam: - H_raw, calib_size, profile_name = self._resolve_homography_entry_for_role(role) - homography_profiles_used[role] = { - "profile": profile_name, - "calib_size": list(calib_size) if calib_size is not None else None, - } - H_role_to_rgb[role] = self._direct_fusion_get_role_homography_fast(role, meta, ref_size) - - # ------------------------------------------------------------ - # Máscaras válidas e crop comum. - # Essa era uma das partes caras e totalmente fixa. - # ------------------------------------------------------------ - valid_masks = [np.ones((ref_h, ref_w), dtype=np.uint8)] - base = np.ones((ref_h, ref_w), dtype=np.uint8) * 255 + source_shapes = {} for role in ("re", "nir"): - if role not in H_role_to_rgb: + if role not in role_to_cam: continue + cam_id = role_to_cam[role] + img = decoded[cam_id]["image"] + src_shape = tuple(int(v) for v in img.shape[:2]) + source_shapes[role] = src_shape + + entry = self._resolve_homography_geometry_entry_for_role(role) + + homography_profiles_used[role] = { + "profile": entry["profile_name"], + "source_calibration_size": list(entry["source_calibration_size"]), + "reference_calibration_size": list(entry["reference_calibration_size"]), + "coordinate_space": entry.get("coordinate_space"), + "runtime_source_shape": list(src_shape), + "runtime_reference_shape": [ref_h, ref_w], + } + + H_role_to_rgb[role] = self._direct_fusion_get_role_homography_fast( + role, + meta, + ref_size, + source_size=src_shape, + ) + + valid_masks = [np.ones((ref_h, ref_w), dtype=np.uint8)] + + for role, H in H_role_to_rgb.items(): + src_h, src_w = source_shapes[role] + source_mask = np.ones((src_h, src_w), dtype=np.uint8) * 255 + mask = cv2.warpPerspective( - base, - H_role_to_rgb[role], + source_mask, + H, (ref_w, ref_h), flags=cv2.INTER_NEAREST, borderMode=cv2.BORDER_CONSTANT, @@ -4876,19 +6765,20 @@ class RawProcessorCore: ) valid_masks.append(mask) - crop_box, crop_applied = self._direct_fusion_get_crop_box_fast(valid_masks, ref_size) + crop_box, crop_applied = self._direct_fusion_get_crop_box_fast( + valid_masks, + ref_size, + ) - # ------------------------------------------------------------ - # Matriz crop RGB/ref -> target final. - # ------------------------------------------------------------ - C_ref_to_target = self._direct_fusion_crop_to_target_matrix_fast(crop_box, target_size) + C_ref_to_target = self._direct_fusion_crop_to_target_matrix_fast( + crop_box, + target_size, + ) - # ------------------------------------------------------------ - # Matrizes compostas role original/ref -> target. - # ------------------------------------------------------------ - M_role_to_target = {} - for role, H in H_role_to_rgb.items(): - M_role_to_target[role] = (C_ref_to_target @ H).astype(np.float32) + M_role_to_target = { + role: (C_ref_to_target @ H).astype(np.float32) + for role, H in H_role_to_rgb.items() + } geom = { "key": key, @@ -4897,6 +6787,7 @@ class RawProcessorCore: "C_ref_to_target": C_ref_to_target.astype(np.float32), "H_role_to_rgb": H_role_to_rgb, "M_role_to_target": M_role_to_target, + "source_shapes": source_shapes, "valid_masks": valid_masks, "prepare_cache_hit": False, "cache_hits": int(self._direct_fusion_geometry_cache_hits), @@ -4904,28 +6795,33 @@ class RawProcessorCore: "homography_profiles_used": homography_profiles_used, } - # Cache pequeno: normalmente só uma geometria. Se mudar resolução/config, - # evita crescimento infinito. if len(cache) > 4: cache.clear() cache[key] = geom return geom - def _direct_fusion_write_spec_cached_fast(self, tensor, channel_index, img, role, geom, ref_size, target_size): + def _direct_fusion_write_spec_cached_fast( + self, + tensor, + channel_index, + img, + role, + geom, + ref_size, + target_size, + ): """ - Escreve RE/NIR usando matriz composta cacheada. + RE/NIR native -> target final em um único warp. """ target_w, target_h = int(target_size[0]), int(target_size[1]) - img_ref = self._direct_fusion_resize_spec_to_ref_if_needed_fast(img, ref_size) - M_role_to_target = geom["M_role_to_target"].get(str(role).lower()) if M_role_to_target is None: raise RuntimeError(f"Matriz composta ausente para role={role}") out = cv2.warpPerspective( - img_ref.astype(np.float32, copy=False), + img.astype(np.float32, copy=False), M_role_to_target, (target_w, target_h), flags=cv2.INTER_LINEAR, @@ -5046,7 +6942,10 @@ class RawProcessorCore: target_size: tuple, ): """ - Retorna mapas de remap cacheados para RGB, RE e NIR. + Retorna remaps cacheados. + + M_role_to_target já nasce no espaço da source REAL. + Não existe resize/compensação posterior. """ if not hasattr(self, "_direct_fusion_remap_cache"): self._direct_fusion_remap_cache = {} @@ -5083,25 +6982,16 @@ class RawProcessorCore: "cache_misses": int(self._direct_fusion_remap_cache_misses), } - # ------------------------------------------------------------ - # RGB: matriz C_ref_to_target leva RGB/ref -> target. - # ------------------------------------------------------------ rgb_cam_id = role_to_cam.get("rgb") if rgb_cam_id is not None: rgb_img = decoded[rgb_cam_id]["image"] - C_ref_to_target = geom["C_ref_to_target"] - remap["maps"]["rgb"] = self._direct_fusion_build_remap_from_src_to_dst_fast( - M_src_to_dst=C_ref_to_target, + M_src_to_dst=geom["C_ref_to_target"], src_shape=rgb_img.shape[:2], dst_size=(target_w, target_h), ref_shape_for_scaled_src=None, ) - # ------------------------------------------------------------ - # RE/NIR: matriz composta M_role_to_target leva role/ref -> target. - # Se a imagem fonte não tiver o mesmo tamanho do ref, escalamos o mapa. - # ------------------------------------------------------------ for role in ("re", "nir"): cam_id = role_to_cam.get(role) if cam_id is None: @@ -5117,7 +7007,7 @@ class RawProcessorCore: M_src_to_dst=M_role_to_target, src_shape=img.shape[:2], dst_size=(target_w, target_h), - ref_shape_for_scaled_src=ref_size, + ref_shape_for_scaled_src=None, ) if len(cache) > 4: @@ -5199,30 +7089,24 @@ class RawProcessorCore: return "default" - def _resolve_homography_entry_for_role(self, role: str): + def _resolve_homography_geometry_entry_for_role(self, role: str) -> dict: """ - Resolve a homografia no contrato novo ou antigo. + Resolve H preservando os espaços de calibração de origem e referência. Contrato novo: - fusion_config.homography_profiles..homographies._to_rgb - - Contrato antigo: - fusion_config.homographies._to_rgb - - Retorna: - H, calib_size, profile_name + fusion_config.homography_profiles. + .reference_size + .source_size_by_role[re|nir] + .homographies._to_rgb """ role = str(role).lower() + if role not in ("re", "nir"): + raise ValueError(f"Role espectral inválida: {role}") + fusion = getattr(self, "fusion_config", {}) or {} - key = f"{role}_to_rgb" - selected_profile = self._resolve_homography_profile_name_for_role(role) - # ------------------------------------------------------------ - # Futuro: auto por profundidade. - # Por enquanto, cai em media/default de forma explícita. - # ------------------------------------------------------------ if selected_profile == "auto": profiles = fusion.get("homography_profiles", {}) or {} if "media" in profiles: @@ -5232,15 +7116,12 @@ class RawProcessorCore: else: selected_profile = "" - # ------------------------------------------------------------ - # Contrato novo: homography_profiles - # ------------------------------------------------------------ profiles = fusion.get("homography_profiles", {}) or {} + if isinstance(profiles, dict) and selected_profile: profile = profiles.get(selected_profile) if profile is None: - # tolera nomes com caixa diferente for name, item in profiles.items(): if str(name).lower() == selected_profile: profile = item @@ -5248,45 +7129,109 @@ class RawProcessorCore: break if isinstance(profile, dict): - profile_homographies = profile.get("homographies", {}) or {} - H = profile_homographies.get(key) - + homographies = profile.get("homographies", {}) or {} + H = homographies.get(key) if H is None: - # fallback curto: "re" ou "nir" - H = profile_homographies.get(role) + H = homographies.get(role) if H is not None: - calib_size = ( - profile.get("homography_calibration_size") + reference_size = ( + profile.get("reference_size") + or profile.get("homography_calibration_size") or profile.get("calibration_size") or fusion.get("homography_calibration_size") or fusion.get("calibration_size") - or None ) - return H, calib_size, selected_profile - # ------------------------------------------------------------ - # Contrato antigo: homographies direto - # ------------------------------------------------------------ + source_sizes = profile.get("source_size_by_role", {}) or {} + source_size = ( + source_sizes.get(role) + or profile.get("source_size") + or reference_size + ) + + if reference_size is None or source_size is None: + raise RuntimeError( + f"Perfil {selected_profile!r} sem tamanhos geométricos " + f"suficientes para role={role}." + ) + + if len(reference_size) != 2 or len(source_size) != 2: + raise RuntimeError( + f"Tamanhos inválidos no perfil {selected_profile!r}: " + f"source={source_size}, reference={reference_size}" + ) + + coordinate_space = ( + profile.get("coordinate_space") + or fusion.get("coordinate_space") + or "native_stream_no_external_undistort" + ) + + H_arr = np.asarray(H, dtype=np.float32) + if H_arr.shape != (3, 3) or not np.all(np.isfinite(H_arr)): + raise RuntimeError( + f"Homografia inválida no perfil={selected_profile} role={role}" + ) + + return { + "H": H_arr, + "source_calibration_size": [ + int(source_size[0]), + int(source_size[1]), + ], + "reference_calibration_size": [ + int(reference_size[0]), + int(reference_size[1]), + ], + "profile_name": selected_profile, + "coordinate_space": str(coordinate_space), + } + + # Compatibilidade legado, onde as duas grades tinham o mesmo tamanho. homographies = fusion.get("homographies", {}) or {} H = homographies.get(key) - if H is None: H = homographies.get(role) if H is not None: - calib_size = ( + common_size = ( fusion.get("homography_calibration_size") or fusion.get("calibration_size") - or None + or [self.sensor_width, self.sensor_height] ) - return H, calib_size, "legacy" + + H_arr = np.asarray(H, dtype=np.float32) + if H_arr.shape != (3, 3): + raise RuntimeError(f"Homografia legacy inválida para {role}: {H_arr.shape}") + + return { + "H": H_arr, + "source_calibration_size": [int(common_size[0]), int(common_size[1])], + "reference_calibration_size": [int(common_size[0]), int(common_size[1])], + "profile_name": "legacy", + "coordinate_space": "legacy_common_pixel_space", + } raise RuntimeError( - f"Homografia ausente para role={role}. " - f"Procurei profile='{selected_profile}' em " - f"fusion_config.homography_profiles.*.homographies.{key} " - f"e fallback fusion_config.homographies.{key}" + f"Homografia ausente para role={role}; profile={selected_profile!r}." + ) + + def _resolve_homography_entry_for_role(self, role: str): + """ + Compatibilidade com callers antigos. + + Retorna: + H, reference_calibration_size, profile_name + + A geometria mixed-resolution usa internamente + _resolve_homography_geometry_entry_for_role(). + """ + entry = self._resolve_homography_geometry_entry_for_role(role) + return ( + entry["H"], + entry["reference_calibration_size"], + entry["profile_name"], ) diff --git a/Python/OAK/datasets/oak-fcc-3/utils/_1_focus_calibration_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/_1_focus_calibration_tool.py new file mode 100644 index 000000000..516328ade --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/utils/_1_focus_calibration_tool.py @@ -0,0 +1,3990 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +focus_calibration_production.py +=============================== + +Ferramenta FINAL de produção/QC para ajuste manual de foco do módulo OAK-FFC-3P. + +Topologia oficial do produto +---------------------------- + CAM_A = RGB = OV9782 (1280x800) OU AR0234 (1920x1200) + CAM_B = RE = OV9282 (1280x800) + CAM_C = NIR = OV9282 (1280x800) + +Objetivos +--------- +- Detectar e validar o hardware antes de permitir a calibração. +- Suportar OV9782 e AR0234 na CAM_A. +- Mostrar RGB + RE + NIR simultaneamente. +- Deixar AE estabilizar e depois congelar exposição/ISO. +- Medir foco SOMENTE quando chega frame novo. +- Auxiliar o operador a atravessar e reencontrar o pico de foco. +- Exigir estabilidade próxima ao pico antes do ACCEPT. +- Medir centro + 4 regiões periféricas para detectar tilt/decentering. +- Salvar evidência auditável por sessão. +- Nunca alterar module_params nem qualquer calibração de runtime. +- Nunca sobrescrever um resultado ativo aprovado com uma sessão FAIL/CANCELLED. + +Natureza do artefato +-------------------- +Foco é um ajuste FÍSICO da lente. Portanto: + runtime_effect = "none" + module_params_fragment = null + +O JSON produzido por esta ferramenta é um relatório de QC/rastreabilidade. +O assembler final do module_params pode ignorá-lo. + +Target recomendado +------------------ +Para o modo de produção, use um alvo plano com DETALHE FINO distribuído por +todo o FOV, perpendicular ao eixo óptico. Ex.: chart de foco / Siemens stars / +checkerboard denso / textura impressa de alta frequência. + +Evite avaliar FIELD QA em cena natural com regiões sem textura. A ferramenta +bloqueia acceptance se alguma das 5 zonas não tiver textura suficiente. + +Fluxo +----- +1) PRE-FLIGHT + - target de foco preenche o FOV + - AE estabiliza + - valida saturação, escuridão e textura + - ENTER congela EXP/ISO das 3 câmeras + +2) FOCUS RGB + - gire a lente atravessando o pico + - volte para >= limiar do melhor score + - score deve estabilizar por N frames NOVOS + - FIELD QA centro + cantos precisa passar + - A solicita acceptance + +3) FOCUS RE + +4) FOCUS NIR + +5) Resultado + - PASS: salva candidate + promove relatório QC ativo + - FAIL/CANCEL: candidate preservado, ativo anterior mantido + +Teclas durante ajuste +--------------------- +1 / 2 / 3 -> seleciona RGB / RE / NIR +M -> troca métrica principal +D -> informa sentido atual (rosqueando/desrosqueando) +C -> centraliza ROI principal +L -> trava/destrava ROI +E -> equalize apenas para diagnóstico; ao mudar, reseta histórico +R -> reseta sweep/score da câmera ativa +A -> tenta ACCEPT da câmera ativa +S -> snapshot de diagnóstico +Q / ESC -> cancela sessão + +IMPORTANTE SOBRE ACCEPTANCE +--------------------------- +O "best" é relativo à sessão. Para evitar aceitar um foco ruim sem ter +atravessado o pico, o modo de produção exige: + - ter observado um best; + - ter caído X% abaixo desse best APÓS o pico (peak bracket); + - ter retornado para perto do best; + - manter estabilidade por N frames novos; + - field QA aprovado. + +Há um override de engenharia: + --engineering-override +que permite aceitar sem bracket/field QA, mas isso fica registrado no JSON +e o resultado não é considerado homologação normal. + +Saídas padrão +------------- +calibration/focus_candidates// + report.json + PASS.txt / FAIL.txt / CANCELLED.txt + snapshots/ + rgb_accepted_full.png + rgb_accepted_board.png + ... + re_... + nir_... + +Se PASS: + calibration/focus_qc_active.json + +Nenhum NPZ é gerado. +Nenhuma chave do module_params é alterada. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import time +from collections import deque +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional, Tuple, List, Any + +import cv2 +import depthai as dai +import numpy as np + + +# ============================================================ +# Contrato do produto +# ============================================================ + +ROLES = ("rgb", "re", "nir") +METHODS = ("tenengrad_norm", "laplacian_norm", "tenengrad", "laplacian", "brenner") + +PRODUCT_TOPOLOGY = { + "rgb": { + "socket": "CAM_A", + "allowed_sensors": ("OV9782", "AR0234"), + }, + "re": { + "socket": "CAM_B", + "allowed_sensors": ("OV9282",), + }, + "nir": { + "socket": "CAM_C", + "allowed_sensors": ("OV9282",), + }, +} + +RGB_SENSOR_MODES = { + "OV9782": { + "resolution_enum": "THE_800_P", + "width": 1280, + "height": 800, + }, + "AR0234": { + "resolution_enum": "THE_1200_P", + "width": 1920, + "height": 1200, + }, +} + +MONO_SENSOR_MODES = { + "OV9282": { + "resolution_enum": "THE_800_P", + "width": 1280, + "height": 800, + } +} + +SCHEMA = "multispec_focus_qc_v3" + + +# ============================================================ +# Limiares de produção +# ============================================================ + +DEFAULT_QA = { + # Preflight imagem + "preflight_sat_warning_pct": 0.20, + "preflight_sat_bad_pct": 1.00, + "preflight_dark_warning_pct": 5.00, + "preflight_dark_bad_pct": 15.00, + + # Textura mínima usando std no ROI/zone em escala 0..1 + "min_texture_std": 0.035, + "min_texture_std_warning": 0.050, + + # Bracket do pico + "peak_drop_required_pct": 4.0, + + # Se um novo best superar o anterior só por menos que este valor, + # tratamos como refinamento/ruído e preservamos o bracket já observado. + # Um salto maior significa que achamos uma região de foco nova e exige + # atravessar o pico novamente. + "new_best_rebracket_pct": 0.020, + + # Retorno ao pico + "accept_near_best_ratio": 0.985, + + # Estabilidade do score no lock + "accept_stable_frames": 12, + "accept_stable_cv": 0.018, + "accept_stable_range_pct": 0.050, + + # Field QA é agregado sobre vários frames NOVOS. + "field_stable_frames": 8, + + # Field QA, razão do sharpness normalizado da zona / centro + "field_corner_ratio_warning": 0.72, + "field_corner_ratio_bad": 0.58, + + # Assimetria entre cantos opostos / extremos + "field_spread_warning": 0.35, + "field_spread_bad": 0.50, + + # Controle manual confirmado + "exposure_rel_tolerance": 0.01, + "exposure_abs_tolerance_us": 20, + "iso_tolerance": 5, + + # FPS + "fps_min_ratio": 0.65, +} + + +# ============================================================ +# Helpers +# ============================================================ + +def now_str() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def file_stamp() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f") + + +def ensure_dir(path: str | Path): + Path(path).mkdir(parents=True, exist_ok=True) + + +def save_json_atomic(path: str | Path, data: dict): + p = Path(path) + ensure_dir(p.parent) + + tmp = p.with_suffix(p.suffix + ".tmp") + with tmp.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.flush() + os.fsync(f.fileno()) + + os.replace(tmp, p) + + +def sha256_file(path: str | Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + while True: + b = f.read(1024 * 1024) + if not b: + break + h.update(b) + return h.hexdigest() + + +def safe_int(v, default=None): + try: + if v is None: + return default + return int(v) + except Exception: + return default + + +def safe_float(v, default=None): + try: + if v is None: + return default + return float(v) + except Exception: + return default + + +def status_rank(s: str) -> int: + return {"good": 0, "warning": 1, "bad": 2}.get(str(s).lower(), 2) + + +def merge_status(a: str, b: str) -> str: + return a if status_rank(a) >= status_rank(b) else b + + +def overlay_hud( + img, + lines, + x=12, + y=24, + font_scale=0.50, + line_step=20, + color=(255, 255, 255), + shadow=True, +): + yy = int(y) + h = img.shape[0] + + for line in lines: + if yy > h - 6: + break + + text = str(line) + + if shadow: + cv2.putText( + img, + text, + (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, + (0, 0, 0), + 3, + cv2.LINE_AA, + ) + + cv2.putText( + img, + text, + (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, + color, + 1, + cv2.LINE_AA, + ) + + yy += int(line_step) + + +def build_empty_panel(shape_hw, title, subtitle="sem frame"): + h, w = shape_hw + img = np.zeros((h, w, 3), dtype=np.uint8) + overlay_hud( + img, + [title, subtitle], + x=20, + y=46, + font_scale=0.72, + line_step=30, + ) + return img + + +def socket_name(socket) -> str: + name = getattr(socket, "name", None) + if name: + return str(name) + + s = str(socket) + for candidate in ("CAM_A", "CAM_B", "CAM_C", "CAM_D"): + if candidate in s: + return candidate + + return s + + +def get_socket(name: str): + name = str(name).upper().strip() + + mapping = { + "CAM_A": dai.CameraBoardSocket.CAM_A, + "CAM_B": dai.CameraBoardSocket.CAM_B, + "CAM_C": dai.CameraBoardSocket.CAM_C, + } + + if name not in mapping: + raise ValueError(f"Socket inválido: {name}") + + return mapping[name] + + +def supported_type_strings(feature) -> List[str]: + return [ + str(v).upper() + for v in (getattr(feature, "supportedTypes", []) or []) + ] + + +def feature_is_color(feature) -> bool: + types = supported_type_strings(feature) + sensor = str(getattr(feature, "sensorName", "") or "").upper() + + if any("COLOR" in t for t in types): + return True + + if any("MONO" in t for t in types): + return False + + return sensor in {"OV9782", "AR0234"} + + +def feature_is_mono(feature) -> bool: + types = supported_type_strings(feature) + + if any("MONO" in t for t in types): + return True + + if any("COLOR" in t for t in types): + return False + + return not feature_is_color(feature) + + +def resize_panel(img: np.ndarray, target_hw: Tuple[int, int]) -> np.ndarray: + th, tw = target_hw + return cv2.resize(img, (tw, th), interpolation=cv2.INTER_AREA) + + +def to_gray01(img_bgr: np.ndarray) -> Optional[np.ndarray]: + if img_bgr is None: + return None + + if img_bgr.ndim == 2: + gray = img_bgr + + elif img_bgr.ndim == 3 and img_bgr.shape[2] == 1: + gray = img_bgr[:, :, 0] + + else: + gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) + + return np.clip(gray.astype(np.float32) / 255.0, 0.0, 1.0) + + +def default_roi_for_shape(shape_hw, frac=0.36): + h, w = shape_hw + + rw = max(16, int(round(w * frac))) + rh = max(16, int(round(h * frac))) + + x0 = (w - rw) // 2 + y0 = (h - rh) // 2 + + return (x0, y0, x0 + rw, y0 + rh) + + +def sanitize_roi(rect, shape_hw): + if rect is None: + return None + + h, w = shape_hw + x0, y0, x1, y1 = rect + + x0, x1 = sorted((int(x0), int(x1))) + y0, y1 = sorted((int(y0), int(y1))) + + x0 = max(0, min(w - 1, x0)) + x1 = max(1, min(w, x1)) + y0 = max(0, min(h - 1, y0)) + y1 = max(1, min(h, y1)) + + if x1 - x0 < 12 or y1 - y0 < 12: + return None + + return (x0, y0, x1, y1) + + +def crop_rect(img, rect): + if img is None: + return None + + r = sanitize_roi(rect, img.shape[:2]) + if r is None: + return None + + x0, y0, x1, y1 = r + return img[y0:y1, x0:x1] + + +def rect_from_norm(shape_hw, x0, y0, x1, y1): + h, w = shape_hw + return ( + int(round(x0 * w)), + int(round(y0 * h)), + int(round(x1 * w)), + int(round(y1 * h)), + ) + + +# ============================================================ +# Descoberta e contrato de hardware +# ============================================================ + +@dataclass +class SensorSpec: + role: str + socket_name: str + sensor_name: str + feature_width: int + feature_height: int + configured_width: int + configured_height: int + resolution_name: str + stream_name: str + control_name: str + is_color: bool + source: str + + +def discover_cameras(mx_id: Optional[str]): + device_info = dai.DeviceInfo(mx_id) if mx_id else None + + ctx = dai.Device(device_info) if device_info is not None else dai.Device() + + with ctx as device: + features = list(device.getConnectedCameraFeatures()) + + actual_mx = None + for attr in ("getMxId", "getDeviceId"): + fn = getattr(device, attr, None) + if callable(fn): + try: + v = fn() + if v: + actual_mx = str(v) + break + except Exception: + pass + + usb_speed = None + try: + usb_speed = str(device.getUsbSpeed()) + except Exception: + pass + + rows = [] + + for f in features: + rows.append({ + "socket_obj": f.socket, + "socket_name": socket_name(f.socket), + "sensor_name": str(getattr(f, "sensorName", "") or "").upper(), + "width": int(getattr(f, "width", 0) or 0), + "height": int(getattr(f, "height", 0) or 0), + "supported_types": supported_type_strings(f), + "is_color": feature_is_color(f), + "is_mono": feature_is_mono(f), + "has_autofocus_ic": int(getattr(f, "hasAutofocusIC", 0) or 0), + }) + + return rows, actual_mx, usb_speed + + +def enum_if_exists(enum_cls, name: str): + return getattr(enum_cls, name, None) + + +def product_sensor_mode(sensor_name: str, is_color: bool): + sensor = str(sensor_name).upper() + + table = RGB_SENSOR_MODES if is_color else MONO_SENSOR_MODES + + if sensor not in table: + raise RuntimeError( + f"Sensor não homologado para esta role: {sensor}" + ) + + return table[sensor] + + +def validate_product_topology(camera_rows: list[dict], relaxed=False): + by_socket = { + row["socket_name"]: row + for row in camera_rows + } + + errors = [] + + for role in ROLES: + contract = PRODUCT_TOPOLOGY[role] + sock = contract["socket"] + + row = by_socket.get(sock) + + if row is None: + errors.append(f"{role.upper()}: {sock} ausente") + continue + + sensor = row["sensor_name"].upper() + + if sensor not in contract["allowed_sensors"]: + errors.append( + f"{role.upper()}: {sock} sensor={sensor}, " + f"permitidos={contract['allowed_sensors']}" + ) + + if role == "rgb" and not row["is_color"]: + errors.append(f"RGB: {sock}/{sensor} não anunciado como COLOR") + + if role in ("re", "nir") and not row["is_mono"]: + errors.append(f"{role.upper()}: {sock}/{sensor} não anunciado como MONO") + + if errors and not relaxed: + raise RuntimeError( + "Topologia do módulo não corresponde ao produto:\n - " + + "\n - ".join(errors) + ) + + return { + "valid": len(errors) == 0, + "relaxed": bool(relaxed), + "errors": errors, + } + + +def build_specs(camera_rows) -> Dict[str, SensorSpec]: + by_socket = { + row["socket_name"]: row + for row in camera_rows + } + + specs = {} + + for role in ROLES: + socket = PRODUCT_TOPOLOGY[role]["socket"] + row = by_socket.get(socket) + + if row is None: + continue + + is_color = role == "rgb" + + mode = product_sensor_mode( + row["sensor_name"], + is_color=is_color, + ) + + # A dimensão anunciada pelo DepthAI deve ser coerente quando disponível. + if row["width"] and row["height"]: + if (row["width"], row["height"]) != (mode["width"], mode["height"]): + raise RuntimeError( + f"{socket}/{row['sensor_name']}: dimensão anunciada " + f"{row['width']}x{row['height']} != homologada " + f"{mode['width']}x{mode['height']}" + ) + + specs[role] = SensorSpec( + role=role, + socket_name=socket, + sensor_name=row["sensor_name"], + feature_width=row["width"], + feature_height=row["height"], + configured_width=mode["width"], + configured_height=mode["height"], + resolution_name=mode["resolution_enum"], + stream_name=f"focus_{role}", + control_name=f"focus_ctrl_{role}", + is_color=is_color, + source="ISP" if is_color else "MONO_OUT", + ) + + missing = [r for r in ROLES if r not in specs] + if missing: + raise RuntimeError(f"Não foi possível montar specs das roles: {missing}") + + return specs + + +# ============================================================ +# Pipeline e controles +# ============================================================ + +def try_set_initial_isp_controls(cam, args) -> dict: + """ + ISP determinístico na RGB. + + Esses parâmetros afetam diretamente energia de borda, portanto a ferramenta + tenta fixá-los. Se a versão do DepthAI não expuser algum controle, por + padrão a calibração aborta. --allow-isp-defaults relaxa isso. + """ + result = { + "sharpness": None, + "luma_denoise": None, + "chroma_denoise": None, + "errors": [], + } + + operations = ( + ("sharpness", "setSharpness", int(args.isp_sharpness)), + ("luma_denoise", "setLumaDenoise", int(args.isp_luma_denoise)), + ("chroma_denoise", "setChromaDenoise", int(args.isp_chroma_denoise)), + ) + + for key, method_name, value in operations: + fn = getattr(cam.initialControl, method_name, None) + + if not callable(fn): + result["errors"].append(f"{method_name}:unsupported") + continue + + try: + fn(value) + result[key] = value + except Exception as exc: + result["errors"].append(f"{method_name}:{exc}") + + if result["errors"] and not args.allow_isp_defaults: + raise RuntimeError( + "Não foi possível fixar controles ISP necessários para repetibilidade: " + + "; ".join(result["errors"]) + + ". Use --allow-isp-defaults somente como override de engenharia." + ) + + return result + + +def build_pipeline(specs: Dict[str, SensorSpec], args): + pipeline = dai.Pipeline() + + isp_settings = None + + for role in ROLES: + spec = specs[role] + socket = get_socket(spec.socket_name) + + if spec.is_color: + cam = pipeline.createColorCamera() + cam.setBoardSocket(socket) + + enum_value = enum_if_exists( + dai.ColorCameraProperties.SensorResolution, + spec.resolution_name, + ) + + if enum_value is None: + raise RuntimeError( + f"DepthAI sem ColorCamera {spec.resolution_name}" + ) + + cam.setResolution(enum_value) + cam.setFps(float(args.fps)) + cam.setInterleaved(False) + + try: + cam.setColorOrder( + dai.ColorCameraProperties.ColorOrder.BGR + ) + except Exception: + pass + + # AE/AWB ativos somente para preflight. + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + + try: + cam.initialControl.setAutoWhiteBalanceLock(False) + except Exception: + pass + + isp_settings = try_set_initial_isp_controls(cam, args) + + output = cam.isp + + else: + cam = pipeline.createMonoCamera() + cam.setBoardSocket(socket) + + enum_value = enum_if_exists( + dai.MonoCameraProperties.SensorResolution, + spec.resolution_name, + ) + + if enum_value is None: + raise RuntimeError( + f"DepthAI sem MonoCamera {spec.resolution_name}" + ) + + cam.setResolution(enum_value) + cam.setFps(float(args.fps)) + + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + + output = cam.out + + xout = pipeline.createXLinkOut() + xout.setStreamName(spec.stream_name) + output.link(xout.input) + + xin = pipeline.createXLinkIn() + xin.setStreamName(spec.control_name) + + if not hasattr(cam, "inputControl"): + raise RuntimeError( + f"{spec.socket_name}/{spec.sensor_name} sem inputControl" + ) + + xin.out.link(cam.inputControl) + + return pipeline, isp_settings + + +@dataclass +class LockedControl: + role: str + exposure_time_us: int + sensitivity_iso: int + source: str + + +def packet_controls(packet) -> dict: + exp_us = None + iso = None + color_temp = None + seq = None + timestamp = None + + try: + exp = packet.getExposureTime() + if hasattr(exp, "total_seconds"): + exp_us = int(round(exp.total_seconds() * 1_000_000)) + else: + exp_us = int(exp) + except Exception: + pass + + try: + iso = int(packet.getSensitivity()) + except Exception: + pass + + try: + color_temp = int(packet.getColorTemperature()) + except Exception: + pass + + try: + seq = int(packet.getSequenceNum()) + except Exception: + pass + + for method in ("getTimestampDevice", "getTimestamp"): + fn = getattr(packet, method, None) + if callable(fn): + try: + ts = fn() + if hasattr(ts, "total_seconds"): + timestamp = float(ts.total_seconds()) + else: + timestamp = float(ts) + break + except Exception: + pass + + return { + "exposure_time_us": exp_us, + "sensitivity_iso": iso, + "color_temperature_k": color_temp, + "sequence_num": seq, + "timestamp_s": timestamp, + } + + +def send_manual_control(queue, lock: LockedControl, rgb=False): + ctrl = dai.CameraControl() + + ctrl.setManualExposure( + int(lock.exposure_time_us), + int(lock.sensitivity_iso), + ) + + if rgb: + try: + ctrl.setAutoWhiteBalanceLock(True) + except Exception: + pass + + queue.send(ctrl) + + +def controls_match(actual: dict, target: LockedControl, qa: dict): + exp = safe_int(actual.get("exposure_time_us"), None) + iso = safe_int(actual.get("sensitivity_iso"), None) + + if exp is None or iso is None: + return False + + exp_tol = max( + int(qa["exposure_abs_tolerance_us"]), + int(round(target.exposure_time_us * qa["exposure_rel_tolerance"])), + ) + + if abs(exp - target.exposure_time_us) > exp_tol: + return False + + if abs(iso - target.sensitivity_iso) > int(qa["iso_tolerance"]): + return False + + return True + + +# ============================================================ +# Métricas de foco +# ============================================================ + +def preprocess_gray(gray01: np.ndarray, equalize=False): + g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8) + + if equalize: + g = cv2.equalizeHist(g) + + return g + + +def raw_focus_metrics_from_gray(gray01: np.ndarray, equalize=False): + if gray01 is None or gray01.size < 64: + return { + "valid": False, + "laplacian": 0.0, + "tenengrad": 0.0, + "brenner": 0.0, + "laplacian_norm": 0.0, + "tenengrad_norm": 0.0, + "mean": 0.0, + "std": 0.0, + "p95": 0.0, + "pct_saturated": 0.0, + "pct_dark": 0.0, + "pixels": 0, + } + + arr01 = np.clip(gray01.astype(np.float32), 0.0, 1.0) + g = preprocess_gray(arr01, equalize=equalize) + + lap = cv2.Laplacian(g, cv2.CV_64F, ksize=3) + lap_var = float(lap.var()) + + sx = cv2.Sobel(g, cv2.CV_64F, 1, 0, ksize=3) + sy = cv2.Sobel(g, cv2.CV_64F, 0, 1, ksize=3) + ten = float(np.mean(sx * sx + sy * sy)) + + arr_u8 = g.astype(np.float32) + + if arr_u8.shape[1] >= 3: + diff = arr_u8[:, 2:] - arr_u8[:, :-2] + brenner = float(np.mean(diff * diff)) + else: + brenner = 0.0 + + std01 = float(arr01.std()) + + # Normalização por contraste local. + # Evita que uma zona ganhe score só porque o desenho impresso é mais contrastado. + variance_u8 = max(float(np.var(arr_u8)), 1.0) + + return { + "valid": True, + "laplacian": lap_var, + "tenengrad": ten, + "brenner": brenner, + "laplacian_norm": float(lap_var / variance_u8), + "tenengrad_norm": float(ten / variance_u8), + "mean": float(arr01.mean()), + "std": std01, + "p95": float(np.percentile(arr01, 95)), + "pct_saturated": float((arr01 >= 0.98).mean() * 100.0), + "pct_dark": float((arr01 <= 0.02).mean() * 100.0), + "pixels": int(arr01.size), + } + + +def compute_focus_metrics(img_bgr, roi_rect, equalize=False): + gray = to_gray01(img_bgr) + + if gray is None: + return raw_focus_metrics_from_gray(None) + + roi = crop_rect(gray, roi_rect) + + return raw_focus_metrics_from_gray( + roi, + equalize=equalize, + ) + + +def metric_value(metrics: dict, method: str) -> float: + return float(metrics.get(method, 0.0) or 0.0) + + +def smoothed_score(history, window: int) -> float: + if not history: + return 0.0 + + vals = [ + float(x["score"]) + for x in list(history)[-max(1, int(window)):] + ] + + return float(np.mean(vals)) + + +def stable_stats(history, frames_count: int) -> dict: + n = int(frames_count) + + if len(history) < n: + return { + "ready": False, + "count": len(history), + "needed": n, + "mean": 0.0, + "std": 0.0, + "cv": float("inf"), + "range_pct": float("inf"), + } + + vals = np.array( + [float(x["smooth"]) for x in list(history)[-n:]], + dtype=np.float64, + ) + + mean = float(np.mean(vals)) + std = float(np.std(vals)) + + cv = std / max(abs(mean), 1e-12) + range_pct = float((np.max(vals) - np.min(vals)) / max(abs(mean), 1e-12)) + + return { + "ready": True, + "count": n, + "needed": n, + "mean": mean, + "std": std, + "cv": float(cv), + "range_pct": range_pct, + } + + +# ============================================================ +# Sweep / bracket do pico +# ============================================================ + +@dataclass +class SweepState: + best_score: float = 0.0 + best_raw_score: float = 0.0 + best_timestamp: Optional[str] = None + best_seq: Optional[int] = None + best_metrics: Optional[dict] = None + best_roi: Optional[List[int]] = None + + peak_drop_observed: bool = False + lowest_ratio_after_best: float = 1.0 + + frames_scored: int = 0 + + +def update_sweep( + sweep: SweepState, + score: float, + smooth: float, + metrics: dict, + roi, + sequence_num, + drop_required_pct: float, + new_best_rebracket_pct: float, +): + sweep.frames_scored += 1 + + if smooth > sweep.best_score: + previous_best = float(sweep.best_score) + had_bracket = bool(sweep.peak_drop_observed) + previous_lowest_ratio = float(sweep.lowest_ratio_after_best) + + improvement = ( + float("inf") + if previous_best <= 0 + else (float(smooth) / previous_best) - 1.0 + ) + + sweep.best_score = float(smooth) + sweep.best_raw_score = float(score) + sweep.best_timestamp = now_str() + sweep.best_seq = safe_int(sequence_num, None) + sweep.best_metrics = dict(metrics) + sweep.best_roi = list(map(int, roi)) if roi else None + + # Primeiro best obviamente não tem bracket. + if previous_best <= 0: + sweep.lowest_ratio_after_best = 1.0 + sweep.peak_drop_observed = False + + # Pequeno refinamento do mesmo pico: preserva bracket já demonstrado. + elif ( + had_bracket + and improvement <= float(new_best_rebracket_pct) + ): + sweep.lowest_ratio_after_best = previous_lowest_ratio + sweep.peak_drop_observed = True + + # Novo patamar significativamente melhor: precisa bracketar de novo. + else: + sweep.lowest_ratio_after_best = 1.0 + sweep.peak_drop_observed = False + + return + + if sweep.best_score <= 0: + return + + ratio = float(smooth / sweep.best_score) + sweep.lowest_ratio_after_best = min( + sweep.lowest_ratio_after_best, + ratio, + ) + + required_ratio = 1.0 - float(drop_required_pct) / 100.0 + + if ratio <= required_ratio: + sweep.peak_drop_observed = True + + +def analyze_trend( + history, + sweep: SweepState, + direction_name: str, + drop_warn_pct=3.0, +): + if len(history) < 6: + return { + "status": "coletando", + "instruction": "gire devagar", + "pct_of_best": 0.0, + "delta": 0.0, + } + + recent = [ + float(x["smooth"]) + for x in list(history)[-5:] + ] + + if len(history) >= 12: + old = [ + float(x["smooth"]) + for x in list(history)[-12:-7] + ] + else: + old = [ + float(x["smooth"]) + for x in list(history)[:5] + ] + + recent_mean = float(np.mean(recent)) + old_mean = float(np.mean(old)) + delta = recent_mean - old_mean + + best = float(sweep.best_score) + + ratio = 0.0 if best <= 0 else recent_mean / best + drop_pct = 100.0 * (1.0 - ratio) + + if best > 0 and drop_pct >= float(drop_warn_pct): + return { + "status": "passou_do_pico", + "instruction": f"volte: contrario de {direction_name}", + "pct_of_best": ratio * 100.0, + "delta": delta, + } + + eps = max(best * 0.002, 1e-6) + + if delta > eps: + status = "melhorando" + instruction = f"continue {direction_name}" + + elif delta < -eps: + status = "piorando" + instruction = f"inverta: contrario de {direction_name}" + + else: + status = "estavel" + instruction = "ajuste fino" + + return { + "status": status, + "instruction": instruction, + "pct_of_best": ratio * 100.0, + "delta": delta, + } + + +# ============================================================ +# FIELD QA +# ============================================================ + +FIELD_ZONES_NORM = { + "center": (0.34, 0.34, 0.66, 0.66), + + # Evita os extremos absolutos do FOV. + "top_left": (0.08, 0.08, 0.30, 0.30), + "top_right": (0.70, 0.08, 0.92, 0.30), + "bottom_left": (0.08, 0.70, 0.30, 0.92), + "bottom_right": (0.70, 0.70, 0.92, 0.92), +} + + +def compute_field_qa( + img_bgr: np.ndarray, + qa: dict, + equalize=False, +): + gray = to_gray01(img_bgr) + + if gray is None: + return { + "status": "bad", + "reasons": ["sem_frame"], + "zones": {}, + } + + zones = {} + + for name, nrect in FIELD_ZONES_NORM.items(): + rect = rect_from_norm( + gray.shape[:2], + *nrect, + ) + + roi = crop_rect(gray, rect) + + metrics = raw_focus_metrics_from_gray( + roi, + equalize=equalize, + ) + + texture = float(metrics.get("std", 0.0)) + sharp = float(metrics.get("tenengrad_norm", 0.0)) + + zones[name] = { + "rect": list(rect), + "texture_std": texture, + "sharpness": sharp, + "metrics": metrics, + } + + status = "good" + reasons = [] + + # Textura suficiente em todas as zonas. + for name, item in zones.items(): + texture = item["texture_std"] + + if texture < qa["min_texture_std"]: + status = merge_status(status, "bad") + reasons.append( + f"{name}:insufficient_texture:{texture:.4f}" + ) + + elif texture < qa["min_texture_std_warning"]: + status = merge_status(status, "warning") + reasons.append( + f"{name}:low_texture:{texture:.4f}" + ) + + center = max( + float(zones["center"]["sharpness"]), + 1e-9, + ) + + corner_names = ( + "top_left", + "top_right", + "bottom_left", + "bottom_right", + ) + + corner_ratios = {} + + for name in corner_names: + ratio = float( + zones[name]["sharpness"] / center + ) + + corner_ratios[name] = ratio + zones[name]["ratio_to_center"] = ratio + + if ratio < qa["field_corner_ratio_bad"]: + status = merge_status(status, "bad") + reasons.append( + f"{name}:corner_ratio_bad:{ratio:.3f}" + ) + + elif ratio < qa["field_corner_ratio_warning"]: + status = merge_status(status, "warning") + reasons.append( + f"{name}:corner_ratio_warn:{ratio:.3f}" + ) + + vals = np.array( + [corner_ratios[x] for x in corner_names], + dtype=np.float64, + ) + + spread = float( + (np.max(vals) - np.min(vals)) + / max(float(np.median(vals)), 1e-9) + ) + + if spread > qa["field_spread_bad"]: + status = merge_status(status, "bad") + reasons.append( + f"corner_spread_bad:{spread:.3f}" + ) + + elif spread > qa["field_spread_warning"]: + status = merge_status(status, "warning") + reasons.append( + f"corner_spread_warn:{spread:.3f}" + ) + + return { + "status": status, + "reasons": reasons, + "zones": zones, + "corner_ratios": corner_ratios, + "corner_spread": spread, + "samples": 1, + } + + +def aggregate_field_qa( + reports, + qa: dict, +): + """ + Agrega FIELD QA por mediana sobre vários frames NOVOS. + + Isso evita aprovar/reprovar tilt/decentering por ruído de um frame isolado. + """ + reports = list(reports or []) + + needed = int(qa["field_stable_frames"]) + + if len(reports) < needed: + return { + "status": "collecting", + "reasons": [ + f"field_samples:{len(reports)}/{needed}" + ], + "zones": {}, + "corner_ratios": {}, + "corner_spread": None, + "samples": len(reports), + "needed": needed, + } + + recent = reports[-needed:] + + zone_names = tuple(FIELD_ZONES_NORM.keys()) + zones = {} + + for name in zone_names: + textures = [] + sharpness = [] + ratios = [] + + for report in recent: + item = report.get("zones", {}).get(name, {}) + + if item.get("texture_std") is not None: + textures.append( + float(item["texture_std"]) + ) + + if item.get("sharpness") is not None: + sharpness.append( + float(item["sharpness"]) + ) + + if item.get("ratio_to_center") is not None: + ratios.append( + float(item["ratio_to_center"]) + ) + + zones[name] = { + "texture_std": ( + float(np.median(textures)) + if textures + else 0.0 + ), + "sharpness": ( + float(np.median(sharpness)) + if sharpness + else 0.0 + ), + } + + if name == "center": + zones[name]["ratio_to_center"] = 1.0 + elif ratios: + zones[name]["ratio_to_center"] = float( + np.median(ratios) + ) + + status = "good" + reasons = [] + + for name, item in zones.items(): + texture = float( + item.get("texture_std", 0.0) + ) + + if texture < qa["min_texture_std"]: + status = merge_status(status, "bad") + reasons.append( + f"{name}:insufficient_texture:{texture:.4f}" + ) + + elif texture < qa["min_texture_std_warning"]: + status = merge_status(status, "warning") + reasons.append( + f"{name}:low_texture:{texture:.4f}" + ) + + corner_names = ( + "top_left", + "top_right", + "bottom_left", + "bottom_right", + ) + + corner_ratios = {} + + for name in corner_names: + ratio = float( + zones[name].get("ratio_to_center", 0.0) + ) + + corner_ratios[name] = ratio + + if ratio < qa["field_corner_ratio_bad"]: + status = merge_status(status, "bad") + reasons.append( + f"{name}:corner_ratio_bad:{ratio:.3f}" + ) + + elif ratio < qa["field_corner_ratio_warning"]: + status = merge_status(status, "warning") + reasons.append( + f"{name}:corner_ratio_warn:{ratio:.3f}" + ) + + vals = np.array( + list(corner_ratios.values()), + dtype=np.float64, + ) + + spread = float( + (np.max(vals) - np.min(vals)) + / max(float(np.median(vals)), 1e-9) + ) + + if spread > qa["field_spread_bad"]: + status = merge_status(status, "bad") + reasons.append( + f"corner_spread_bad:{spread:.3f}" + ) + + elif spread > qa["field_spread_warning"]: + status = merge_status(status, "warning") + reasons.append( + f"corner_spread_warn:{spread:.3f}" + ) + + return { + "status": status, + "reasons": reasons, + "zones": zones, + "corner_ratios": corner_ratios, + "corner_spread": spread, + "samples": needed, + "needed": needed, + } + + +# ============================================================ +# Preflight +# ============================================================ + +def image_preflight(img_bgr, qa: dict): + gray = to_gray01(img_bgr) + + if gray is None: + return { + "status": "bad", + "reasons": ["sem_frame"], + } + + arr = gray.reshape(-1) + + mean = float(np.mean(arr)) + std = float(np.std(arr)) + p95 = float(np.percentile(arr, 95)) + sat_pct = float((arr >= 0.98).mean() * 100.0) + dark_pct = float((arr <= 0.02).mean() * 100.0) + + status = "good" + reasons = [] + + if sat_pct > qa["preflight_sat_bad_pct"]: + status = "bad" + reasons.append(f"sat_bad:{sat_pct:.2f}%") + + elif sat_pct > qa["preflight_sat_warning_pct"]: + status = merge_status(status, "warning") + reasons.append(f"sat_warn:{sat_pct:.2f}%") + + if dark_pct > qa["preflight_dark_bad_pct"]: + status = merge_status(status, "bad") + reasons.append(f"dark_bad:{dark_pct:.2f}%") + + elif dark_pct > qa["preflight_dark_warning_pct"]: + status = merge_status(status, "warning") + reasons.append(f"dark_warn:{dark_pct:.2f}%") + + # Full frame precisa ter alguma textura. + if std < qa["min_texture_std"]: + status = merge_status(status, "bad") + reasons.append(f"texture_bad:{std:.4f}") + + elif std < qa["min_texture_std_warning"]: + status = merge_status(status, "warning") + reasons.append(f"texture_warn:{std:.4f}") + + return { + "status": status, + "reasons": reasons, + "mean": mean, + "std": std, + "p95": p95, + "sat_pct": sat_pct, + "dark_pct": dark_pct, + } + + +def merge_preflight_by_role(frames, qa): + result = { + "status": "good", + "roles": {}, + "reasons": [], + } + + for role in ROLES: + r = image_preflight(frames.get(role), qa) + result["roles"][role] = r + result["status"] = merge_status( + result["status"], + r["status"], + ) + result["reasons"].extend( + [f"{role}:{x}" for x in r.get("reasons", [])] + ) + + return result + + +# ============================================================ +# Estado de runtime +# ============================================================ + +class FocusRuntime: + def __init__( + self, + device, + specs: Dict[str, SensorSpec], + args, + qa, + ): + self.device = device + self.specs = specs + self.args = args + self.qa = qa + + self.output_queues = { + role: device.getOutputQueue( + name=spec.stream_name, + maxSize=2, + blocking=False, + ) + for role, spec in specs.items() + } + + self.control_queues = { + role: device.getInputQueue( + spec.control_name + ) + for role, spec in specs.items() + } + + self.frames = { + role: None + for role in ROLES + } + + self.controls = { + role: {} + for role in ROLES + } + + self.last_seq = { + role: None + for role in ROLES + } + + self.frame_counts = { + role: 0 + for role in ROLES + } + + self.fps = { + role: 0.0 + for role in ROLES + } + + self._fps_count = { + role: 0 + for role in ROLES + } + + self._fps_t0 = { + role: time.time() + for role in ROLES + } + + def poll(self) -> set[str]: + """ + Retorna as roles que receberam FRAME NOVO nesta iteração. + + Esta informação é usada pelo score. O mesmo frame NUNCA é adicionado + duas vezes ao histórico de foco. + """ + fresh = set() + + for role in ROLES: + packet = self.output_queues[role].tryGet() + + if packet is None: + continue + + ctrl = packet_controls(packet) + seq = ctrl.get("sequence_num") + + if seq is not None and seq == self.last_seq[role]: + continue + + try: + img = packet.getCvFrame() + except Exception as exc: + raise RuntimeError( + f"{role}: getCvFrame falhou: {exc}" + ) + + if img is None: + continue + + if img.ndim == 2: + img = cv2.cvtColor( + img, + cv2.COLOR_GRAY2BGR, + ) + + self.frames[role] = img + self.controls[role] = ctrl + self.last_seq[role] = seq + self.frame_counts[role] += 1 + + fresh.add(role) + + self._fps_count[role] += 1 + + dt = time.time() - self._fps_t0[role] + + if dt >= 1.0: + self.fps[role] = ( + self._fps_count[role] / dt + ) + self._fps_count[role] = 0 + self._fps_t0[role] = time.time() + + return fresh + + def wait_all(self, timeout=8.0): + t0 = time.time() + + while time.time() - t0 < timeout: + self.poll() + + if all( + self.frames[r] is not None + for r in ROLES + ): + return + + time.sleep(0.003) + + missing = [ + r for r in ROLES + if self.frames[r] is None + ] + + raise TimeoutError( + f"Timeout aguardando câmeras: {missing}" + ) + + def send_manual(self, locked): + for role in ROLES: + send_manual_control( + self.control_queues[role], + locked[role], + rgb=(role == "rgb"), + ) + + def verify_manual(self, locked, settle_frames): + matched = { + r: 0 + for r in ROLES + } + + observed_seq = dict(self.last_seq) + t0 = time.time() + + while time.time() - t0 < 10.0: + fresh = self.poll() + + for role in fresh: + seq = self.last_seq[role] + + if seq == observed_seq.get(role): + continue + + observed_seq[role] = seq + + if controls_match( + self.controls[role], + locked[role], + self.qa, + ): + matched[role] += 1 + + else: + matched[role] = 0 + + if all( + matched[r] >= int(settle_frames) + for r in ROLES + ): + return + + time.sleep(0.002) + + raise RuntimeError( + "Não foi possível confirmar EXP/ISO manual nas três câmeras. " + f"matched={matched}" + ) + + +# ============================================================ +# Seleção de controles +# ============================================================ + +def manual_override_from_args(args, role: str): + exp = getattr(args, f"{role}_exp_us") + iso = getattr(args, f"{role}_iso") + + if exp is None and iso is None: + return None + + if exp is None or iso is None: + raise ValueError( + f"Informe ambos --{role}-exp-us e --{role}-iso" + ) + + return LockedControl( + role=role, + exposure_time_us=int(exp), + sensitivity_iso=int(iso), + source="cli_manual", + ) + + +def sample_ae_controls( + runtime: FocusRuntime, + args, +): + locked = {} + + for role in ROLES: + override = manual_override_from_args( + args, + role, + ) + + if override is not None: + locked[role] = override + continue + + exp_values = [] + iso_values = [] + + t0 = time.time() + + while time.time() - t0 < 0.8: + fresh = runtime.poll() + + if role not in fresh: + time.sleep(0.003) + continue + + c = runtime.controls[role] + + exp = safe_int( + c.get("exposure_time_us"), + None, + ) + + iso = safe_int( + c.get("sensitivity_iso"), + None, + ) + + if exp is not None and exp > 0: + exp_values.append(exp) + + if iso is not None and iso > 0: + iso_values.append(iso) + + if not exp_values or not iso_values: + raise RuntimeError( + f"Sem metadados EXP/ISO suficientes para {role.upper()}" + ) + + locked[role] = LockedControl( + role=role, + exposure_time_us=int( + round(np.median(exp_values)) + ), + sensitivity_iso=int( + round(np.median(iso_values)) + ), + source="ae_snapshot", + ) + + return locked + + +# ============================================================ +# Desenho +# ============================================================ + +def draw_roi( + panel, + roi_src, + src_shape_hw, + color=(0, 255, 255), + label="FOCUS ROI", + thickness=2, +): + if roi_src is None: + return + + ph, pw = panel.shape[:2] + sh, sw = src_shape_hw + + x0, y0, x1, y1 = roi_src + + px0 = int(x0 * pw / max(sw, 1)) + px1 = int(x1 * pw / max(sw, 1)) + py0 = int(y0 * ph / max(sh, 1)) + py1 = int(y1 * ph / max(sh, 1)) + + cv2.rectangle( + panel, + (px0, py0), + (px1, py1), + color, + thickness, + ) + + cv2.putText( + panel, + label, + (px0 + 4, max(18, py0 - 5)), + cv2.FONT_HERSHEY_SIMPLEX, + 0.40, + color, + 1, + cv2.LINE_AA, + ) + + +def draw_field_zones(panel, src_shape_hw, field_qa=None): + color_good = (0, 220, 0) + color_warn = (0, 200, 255) + color_bad = (0, 0, 255) + + for name, nrect in FIELD_ZONES_NORM.items(): + rect = rect_from_norm( + src_shape_hw, + *nrect, + ) + + color = (150, 150, 150) + label = name.upper() + + if field_qa: + item = field_qa.get("zones", {}).get(name, {}) + ratio = item.get("ratio_to_center") + + if name == "center": + color = color_good + label += " 1.00" + + elif ratio is not None: + if ratio < DEFAULT_QA["field_corner_ratio_bad"]: + color = color_bad + + elif ratio < DEFAULT_QA["field_corner_ratio_warning"]: + color = color_warn + + else: + color = color_good + + label += f" {ratio:.2f}" + + draw_roi( + panel, + rect, + src_shape_hw, + color=color, + label=label, + thickness=1, + ) + + +def draw_history_graph( + panel, + history, + x, + y, + w, + h, + best_score, +): + cv2.rectangle( + panel, + (x, y), + (x + w, y + h), + (35, 35, 35), + -1, + ) + + cv2.rectangle( + panel, + (x, y), + (x + w, y + h), + (110, 110, 110), + 1, + ) + + if len(history) < 2: + return + + vals = np.array( + [ + float(item["smooth"]) + for item in history + ], + dtype=np.float32, + ) + + vals = vals[-max(2, w):] + + max_val = max( + float(np.max(vals)), + float(best_score), + 1e-6, + ) + + min_val = min( + float(np.min(vals)), + max_val * 0.88, + ) + + span = max( + max_val - min_val, + 1e-6, + ) + + pts = [] + + for i, v in enumerate(vals): + px = x + int( + i / max(1, len(vals) - 1) + * (w - 1) + ) + + py = y + h - 1 - int( + (float(v) - min_val) + / span + * (h - 1) + ) + + pts.append((px, py)) + + for a, b in zip(pts[:-1], pts[1:]): + cv2.line( + panel, + a, + b, + (255, 255, 255), + 2, + cv2.LINE_AA, + ) + + +def build_board( + runtime: FocusRuntime, + specs, + selected_role, + roi_rects, + state, + field_qa_by_role, + args, + qa, + status_message="", +): + ph = int(args.panel_height) + pw = int(args.panel_width) + + panels = {} + + for role in ROLES: + img = runtime.frames[role] + spec = specs[role] + + if img is None: + panels[role] = build_empty_panel( + (ph, pw), + role.upper(), + ) + continue + + view = img.copy() + + if role == "re": + gray = cv2.cvtColor( + view, + cv2.COLOR_BGR2GRAY, + ) + z = np.zeros_like(gray) + view = np.dstack([z, z, gray]) + + elif role == "nir": + gray = cv2.cvtColor( + view, + cv2.COLOR_BGR2GRAY, + ) + z = np.zeros_like(gray) + view = np.dstack([gray, gray, z]) + + panel = resize_panel( + view, + (ph, pw), + ) + + active = role == selected_role + + draw_roi( + panel, + roi_rects[role], + img.shape[:2], + color=(0, 255, 255) if active else (0, 170, 255), + label="FOCUS ROI", + thickness=2 if active else 1, + ) + + draw_field_zones( + panel, + img.shape[:2], + field_qa_by_role.get(role), + ) + + c = runtime.controls[role] + + overlay_hud( + panel, + [ + f"{role.upper()} | {spec.socket_name} | {spec.sensor_name}", + f"{img.shape[1]}x{img.shape[0]} | {'ATIVA' if active else ''}", + f"EXP={c.get('exposure_time_us')}us ISO={c.get('sensitivity_iso')}", + f"FPS={runtime.fps[role]:.1f}", + ], + x=10, + y=20, + font_scale=0.40, + line_step=17, + ) + + panels[role] = panel + + data = np.zeros((ph, pw, 3), dtype=np.uint8) + + role_state = state[selected_role] + sweep = role_state["sweep"] + history = role_state["history"] + + best = float(sweep.best_score) + current = float(role_state.get("smooth", 0.0)) + ratio = 0.0 if best <= 0 else current / best + + stable = role_state.get("stable", {}) + field = field_qa_by_role.get(selected_role) or {} + + lines = [ + "FOCUS CALIBRATION - PRODUCTION", + f"ativa={selected_role.upper()} | metodo={args.method}", + f"score={role_state.get('score', 0.0):.3f}", + f"smooth={current:.3f}", + f"best={best:.3f} | atual/best={ratio*100:.2f}%", + f"frames NOVOS medidos={sweep.frames_scored}", + "", + f"peak_bracket={'SIM' if sweep.peak_drop_observed else 'NAO'}", + f"stable={'SIM' if stable.get('pass') else 'NAO'} " + f"CV={stable.get('cv', 0)*100:.2f}% " + f"range={stable.get('range_pct', 0)*100:.2f}%", + f"FIELD QA={field.get('status', 'aguardando').upper()}", + f"camera accepted={'SIM' if role_state.get('accepted') else 'NAO'}", + "", + f"trend={role_state.get('trend', {}).get('status', 'coletando')}", + f"acao={role_state.get('trend', {}).get('instruction', 'gire devagar')}", + f"sentido={role_state.get('direction_name')}", + "", + "A tenta ACCEPT | R reset sweep", + "1/2/3 câmera | M métrica | D sentido", + "mouse ROI | C centraliza | L trava", + "E equalize | S snapshot | Q cancela", + ] + + overlay_hud( + data, + lines, + x=14, + y=22, + font_scale=0.40, + line_step=17, + ) + + draw_history_graph( + data, + history, + x=16, + y=max(260, ph - 130), + w=max(100, pw - 32), + h=90, + best_score=best, + ) + + board = np.vstack([ + np.hstack([ + panels["rgb"], + panels["re"], + ]), + np.hstack([ + panels["nir"], + data, + ]), + ]) + + if status_message: + cv2.putText( + board, + status_message, + (18, board.shape[0] - 14), + cv2.FONT_HERSHEY_SIMPLEX, + 0.55, + (0, 255, 0), + 2, + cv2.LINE_AA, + ) + + return board + + +# ============================================================ +# Reset e acceptance +# ============================================================ + +def new_role_state(args): + return { + "history": deque( + maxlen=int(args.history) + ), + "sweep": SweepState(), + "score": 0.0, + "smooth": 0.0, + "metrics": None, + "trend": {}, + "stable": {}, + "accepted": False, + "accepted_at": None, + "acceptance": None, + "direction_idx": 0, + "direction_name": "rosqueando", + "last_scored_seq": None, + } + + +def reset_role_state( + state, + role, + args, +): + accepted = state[role].get("accepted", False) + + state[role] = new_role_state(args) + + # Mudar metodologia/ROI invalida o acceptance anterior dessa role. + if accepted: + state[role]["accepted"] = False + + +def evaluate_stability( + history, + sweep, + qa, +): + stats = stable_stats( + history, + qa["accept_stable_frames"], + ) + + if not stats["ready"]: + stats["pass"] = False + return stats + + near_best = ( + stats["mean"] + >= sweep.best_score + * qa["accept_near_best_ratio"] + ) + + cv_ok = ( + stats["cv"] + <= qa["accept_stable_cv"] + ) + + range_ok = ( + stats["range_pct"] + <= qa["accept_stable_range_pct"] + ) + + stats["near_best"] = bool(near_best) + stats["cv_ok"] = bool(cv_ok) + stats["range_ok"] = bool(range_ok) + stats["pass"] = bool( + near_best and cv_ok and range_ok + ) + + return stats + + +def attempt_accept( + role, + state, + field_qa_by_role, + runtime, + roi_rects, + args, + qa, +): + s = state[role] + sweep = s["sweep"] + stable = s.get("stable") or {} + field = field_qa_by_role.get(role) or {} + + reasons = [] + + if sweep.frames_scored < max(20, qa["accept_stable_frames"]): + reasons.append( + f"poucos_frames:{sweep.frames_scored}" + ) + + if not sweep.peak_drop_observed: + reasons.append("peak_nao_bracketado") + + if not stable.get("pass"): + reasons.append("score_nao_estavel_perto_do_best") + + field_status = field.get("status") + + if field_status == "bad": + reasons.append("field_qa_bad") + + elif ( + field_status == "warning" + and not args.allow_warning_field + ): + reasons.append("field_qa_warning") + + if field_status not in ("good", "warning"): + reasons.append("field_qa_ausente") + + engineering_override_used = False + + if reasons and args.engineering_override: + engineering_override_used = True + reasons_for_record = list(reasons) + reasons = [] + else: + reasons_for_record = list(reasons) + + accepted = len(reasons) == 0 + + result = { + "accepted": accepted, + "requested_at": now_str(), + "engineering_override_used": engineering_override_used, + "blocked_reasons": reasons_for_record, + "method": args.method, + "equalize": bool(args.equalize_runtime), + "best_score": float(sweep.best_score), + "accepted_score": float(s.get("smooth", 0.0)), + "accepted_ratio_to_best": ( + 0.0 + if sweep.best_score <= 0 + else float( + s.get("smooth", 0.0) + / sweep.best_score + ) + ), + "frames_scored": int(sweep.frames_scored), + "peak_drop_observed": bool( + sweep.peak_drop_observed + ), + "lowest_ratio_after_best": float( + sweep.lowest_ratio_after_best + ), + "stability": dict(stable), + "field_qa": field, + "roi": ( + list(map(int, roi_rects[role])) + if roi_rects[role] is not None + else None + ), + "frame_controls": dict( + runtime.controls[role] + ), + "sequence_num": safe_int( + runtime.last_seq[role], + None, + ), + } + + if accepted: + s["accepted"] = True + s["accepted_at"] = now_str() + s["acceptance"] = result + + return result + + +# ============================================================ +# Snapshots +# ============================================================ + +def save_role_snapshot( + role, + runtime, + board, + out_dir, + label, +): + ensure_dir(out_dir) + + stamp = file_stamp() + + frame_path = Path(out_dir) / ( + f"{role}_{label}_full_{stamp}.png" + ) + + board_path = Path(out_dir) / ( + f"{role}_{label}_board_{stamp}.png" + ) + + img = runtime.frames.get(role) + + if img is not None: + cv2.imwrite( + str(frame_path), + img, + ) + + if board is not None: + cv2.imwrite( + str(board_path), + board, + ) + + return { + "frame_png": ( + str(frame_path) + if frame_path.exists() + else None + ), + "board_png": ( + str(board_path) + if board_path.exists() + else None + ), + } + + +# ============================================================ +# Preflight UI +# ============================================================ + +def run_preflight( + runtime: FocusRuntime, + specs, + args, + qa, +): + runtime.wait_all() + + print("") + print("[PRE-FLIGHT] Use alvo de foco plano, detalhado e preenchendo o FOV.") + print("[PRE-FLIGHT] ENTER congela exposição/ISO quando aprovado.") + + last_report = None + + while True: + runtime.poll() + + last_report = merge_preflight_by_role( + runtime.frames, + qa, + ) + + ph = int(args.panel_height) + pw = int(args.panel_width) + + panels = [] + + for role in ROLES: + img = runtime.frames[role] + + if img is None: + panel = build_empty_panel( + (ph, pw), + role.upper(), + ) + else: + panel = resize_panel( + img, + (ph, pw), + ) + + r = last_report["roles"][role] + + overlay_hud( + panel, + [ + f"{role.upper()} | {specs[role].sensor_name}", + f"PREFLIGHT={r['status'].upper()}", + f"std={r.get('std', 0):.3f}", + f"sat={r.get('sat_pct', 0):.2f}% dark={r.get('dark_pct', 0):.2f}%", + f"EXP={runtime.controls[role].get('exposure_time_us')}us " + f"ISO={runtime.controls[role].get('sensitivity_iso')}", + ], + x=10, + y=22, + font_scale=0.42, + line_step=18, + ) + + panels.append(panel) + + data = np.zeros( + (ph, pw, 3), + dtype=np.uint8, + ) + + lines = [ + "FOCUS QC - PRE-FLIGHT", + "", + f"STATUS GLOBAL: {last_report['status'].upper()}", + "", + "Use chart DETALHADO em todo o campo.", + "Módulo perpendicular ao alvo.", + "Evite reflexos e saturação.", + "", + "ENTER = congelar EXP/ISO", + "Q/ESC = cancelar", + ] + + for reason in last_report["reasons"][:8]: + lines.append(f"! {reason}") + + overlay_hud( + data, + lines, + x=14, + y=30, + font_scale=0.45, + line_step=20, + ) + + board = np.vstack([ + np.hstack([ + panels[0], + panels[1], + ]), + np.hstack([ + panels[2], + data, + ]), + ]) + + cv2.imshow( + "Focus Calibration - Production", + board, + ) + + k = cv2.waitKey(1) & 0xFF + + if k in (ord("q"), ord("Q"), 27): + raise KeyboardInterrupt( + "Cancelado no preflight." + ) + + if k in (13, 10): + status = last_report["status"] + + allowed = ( + status == "good" + or ( + status == "warning" + and args.allow_warning_preflight + ) + or args.engineering_override + ) + + if not allowed: + print( + f"[BLOCK] PREFLIGHT={status.upper()}. " + "Corrija alvo/luz antes de continuar." + ) + continue + + locked = sample_ae_controls( + runtime, + args, + ) + + runtime.send_manual(locked) + + runtime.verify_manual( + locked, + settle_frames=args.lock_settle_frames, + ) + + return last_report, locked + + time.sleep(0.002) + + +# ============================================================ +# Report +# ============================================================ + +def state_to_report(state): + out = {} + + for role in ROLES: + s = state[role] + sweep = s["sweep"] + + out[role] = { + "accepted": bool(s.get("accepted")), + "accepted_at": s.get("accepted_at"), + "acceptance": s.get("acceptance"), + "sweep": asdict(sweep), + } + + return out + + +def build_base_report( + args, + camera_rows, + specs, + actual_mx, + usb_speed, + topology_report, + isp_settings, + qa, + candidate_dir, +): + return { + "schema": SCHEMA, + "created_at": now_str(), + "status": "running", + "runtime_effect": "none", + "module_params_fragment": None, + "module_params_merge_required": False, + "purpose": ( + "physical_optical_focus_qc_only" + ), + "depthai_version": getattr( + dai, + "__version__", + "unknown", + ), + "device_mx_id": actual_mx, + "usb_speed": usb_speed, + "topology": topology_report, + "camera_inventory": [ + { + k: v + for k, v in row.items() + if k != "socket_obj" + } + for row in camera_rows + ], + "resolved_setup": { + role: asdict(spec) + for role, spec in specs.items() + }, + "isp_measurement_settings": isp_settings, + "measurement": { + "method_initial": args.method, + "fps_requested": args.fps, + "panel_size": [ + args.panel_width, + args.panel_height, + ], + "smooth_window": args.smooth_window, + "history": args.history, + "equalize_initial": bool( + args.equalize + ), + "target_requirement": ( + "flat_high_detail_target_across_entire_fov" + ), + }, + "qa_thresholds": qa, + "policy": { + "allow_warning_preflight": bool( + args.allow_warning_preflight + ), + "allow_warning_field": bool( + args.allow_warning_field + ), + "engineering_override": bool( + args.engineering_override + ), + "allow_isp_defaults": bool( + args.allow_isp_defaults + ), + }, + "traceability": { + "module_id": args.module_id or None, + "operator": args.operator or None, + "target_distance_mm": args.target_distance_mm, + "lens_by_role": { + "rgb": args.lens_rgb or None, + "re": args.lens_re or None, + "nir": args.lens_nir or None, + }, + }, + "notes": args.notes or "", + "candidate_dir": str(candidate_dir), + "preflight": None, + "locked_controls": None, + "results_by_role": {}, + "snapshots": [], + "promoted": False, + } + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser( + description=( + "Ferramenta FINAL de produção para ajuste/QC de foco " + "OV9782 ou AR0234 + 2x OV9282." + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # Hardware + parser.add_argument( + "--mx-id", + default=None, + ) + + parser.add_argument( + "--fps", + type=float, + default=20.0, + ) + + parser.add_argument( + "--relaxed-topology", + action="store_true", + help=( + "Override de engenharia. Produto normal exige " + "CAM_A=RGB, CAM_B=RE, CAM_C=NIR." + ), + ) + + # ISP determinístico RGB + parser.add_argument( + "--isp-sharpness", + type=int, + default=0, + ) + + parser.add_argument( + "--isp-luma-denoise", + type=int, + default=0, + ) + + parser.add_argument( + "--isp-chroma-denoise", + type=int, + default=0, + ) + + parser.add_argument( + "--allow-isp-defaults", + action="store_true", + help=( + "Permite continuar caso esta versão do DepthAI " + "não exponha sharpness/denoise no initialControl." + ), + ) + + # UI / métrica + parser.add_argument( + "--panel-width", + type=int, + default=640, + ) + + parser.add_argument( + "--panel-height", + type=int, + default=400, + ) + + parser.add_argument( + "--method", + default="tenengrad_norm", + choices=list(METHODS), + ) + + parser.add_argument( + "--history", + type=int, + default=320, + ) + + parser.add_argument( + "--smooth-window", + type=int, + default=5, + ) + + parser.add_argument( + "--drop-warn-pct", + type=float, + default=3.0, + ) + + parser.add_argument( + "--equalize", + action="store_true", + ) + + parser.add_argument( + "--lock-settle-frames", + type=int, + default=8, + ) + + # Controles manuais opcionais + for role in ROLES: + parser.add_argument( + f"--{role}-exp-us", + type=int, + default=None, + ) + + parser.add_argument( + f"--{role}-iso", + type=int, + default=None, + ) + + # Política + parser.add_argument( + "--allow-warning-preflight", + action="store_true", + ) + + parser.add_argument( + "--allow-warning-field", + action="store_true", + help=( + "Permite aceitar FIELD QA=WARNING. Por padrão produto exige GOOD." + ), + ) + + parser.add_argument( + "--engineering-override", + action="store_true", + help=( + "Permite acceptance mesmo sem todos os critérios. " + "Fica explicitamente registrado e não representa fluxo normal." + ), + ) + + # Output + parser.add_argument( + "--candidate-root", + default="calibration/focus_candidates", + ) + + parser.add_argument( + "--active-json", + default="calibration/focus_qc_active.json", + ) + + parser.add_argument( + "--module-id", + default="", + help="Identificador/NS do módulo óptico, se houver.", + ) + + parser.add_argument( + "--operator", + default="", + help="Operador/técnico responsável pela calibração.", + ) + + parser.add_argument( + "--target-distance-mm", + type=float, + default=None, + help="Distância lente-alvo usada no QC de foco.", + ) + + parser.add_argument( + "--lens-rgb", + default="", + help="PN/ID/lote da lente RGB.", + ) + + parser.add_argument( + "--lens-re", + default="", + help="PN/ID/lote da lente RE.", + ) + + parser.add_argument( + "--lens-nir", + default="", + help="PN/ID/lote da lente NIR.", + ) + + parser.add_argument( + "--notes", + default="", + ) + + args = parser.parse_args() + + # Campo mutável apenas em runtime para E/M. + args.equalize_runtime = bool(args.equalize) + + qa = dict(DEFAULT_QA) + + sid = file_stamp() + candidate_dir = ( + Path(args.candidate_root) + / sid + ) + snapshot_dir = ( + candidate_dir + / "snapshots" + ) + candidate_report = ( + candidate_dir + / "report.json" + ) + + ensure_dir(snapshot_dir) + + camera_rows = [] + specs = {} + actual_mx = None + usb_speed = None + report = None + device_ctx = None + state = None + + cv2.namedWindow( + "Focus Calibration - Production", + cv2.WINDOW_NORMAL, + ) + + try: + # ---------------------------------------------------- + # Descoberta + # ---------------------------------------------------- + + ( + camera_rows, + actual_mx, + usb_speed, + ) = discover_cameras( + args.mx_id + ) + + topology_report = validate_product_topology( + camera_rows, + relaxed=args.relaxed_topology, + ) + + specs = build_specs( + camera_rows + ) + + print("=" * 82) + print("FOCUS CALIBRATION - PRODUCTION") + print(f"DepthAI : {getattr(dai, '__version__', 'unknown')}") + print(f"MX ID : {actual_mx}") + print(f"USB : {usb_speed}") + print("-" * 82) + + for role in ROLES: + spec = specs[role] + + print( + f"{spec.socket_name} -> " + f"{role.upper():3s} | " + f"{spec.sensor_name:8s} | " + f"{spec.configured_width}x{spec.configured_height} | " + f"{spec.resolution_name} | " + f"{spec.source}" + ) + + print("=" * 82) + + # ---------------------------------------------------- + # Pipeline + # ---------------------------------------------------- + + pipeline, isp_settings = build_pipeline( + specs, + args, + ) + + device_info = ( + dai.DeviceInfo(actual_mx) + if actual_mx + else None + ) + + device_ctx = ( + dai.Device( + pipeline, + device_info, + ) + if device_info is not None + else dai.Device(pipeline) + ) + + report = build_base_report( + args, + camera_rows, + specs, + actual_mx, + usb_speed, + topology_report, + isp_settings, + qa, + candidate_dir, + ) + + with device_ctx as device: + runtime = FocusRuntime( + device, + specs, + args, + qa, + ) + + # ------------------------------------------------ + # Preflight + lock + # ------------------------------------------------ + + preflight, locked = run_preflight( + runtime, + specs, + args, + qa, + ) + + report["preflight"] = preflight + + report["locked_controls"] = { + role: asdict(locked[role]) + for role in ROLES + } + + print("[LOCK] Exposição congelada:") + for role in ROLES: + c = locked[role] + print( + f" {role.upper()}: " + f"{c.exposure_time_us}us " + f"ISO={c.sensitivity_iso}" + ) + + # ------------------------------------------------ + # Estado de foco + # ------------------------------------------------ + + state = { + role: new_role_state(args) + for role in ROLES + } + + roi_rects = { + role: None + for role in ROLES + } + + roi_locked = False + selected_role = "rgb" + + field_qa_by_role = { + role: None + for role in ROLES + } + + field_history_by_role = { + role: deque( + maxlen=max( + 16, + int(qa["field_stable_frames"]) * 2, + ) + ) + for role in ROLES + } + + last_board = None + last_message = "" + last_message_t = 0.0 + + panel_rects = { + "rgb": None, + "re": None, + "nir": None, + "data": None, + } + + # ------------------------------------------------ + # Mouse ROI + # ------------------------------------------------ + + dragging = False + drag_start = None + + def message(text): + nonlocal last_message, last_message_t + last_message = str(text) + last_message_t = time.time() + + def inside(rect, x, y): + if rect is None: + return False + + x0, y0, x1, y1 = rect + return ( + x0 <= x < x1 + and y0 <= y < y1 + ) + + def display_to_source( + role, + x, + y, + ): + rect = panel_rects.get(role) + img = runtime.frames.get(role) + + if rect is None or img is None: + return None + + x0, y0, x1, y1 = rect + pw = max(1, x1 - x0) + ph = max(1, y1 - y0) + + sh, sw = img.shape[:2] + + lx = max( + 0, + min( + pw - 1, + int(x - x0), + ), + ) + + ly = max( + 0, + min( + ph - 1, + int(y - y0), + ), + ) + + sx = int(lx * sw / pw) + sy = int(ly * sh / ph) + + return ( + max(0, min(sw - 1, sx)), + max(0, min(sh - 1, sy)), + ) + + def on_mouse( + event, + x, + y, + flags, + param, + ): + nonlocal dragging, drag_start + + if roi_locked: + return + + rect = panel_rects.get( + selected_role + ) + + if ( + rect is None + or not inside(rect, x, y) + ): + return + + pt = display_to_source( + selected_role, + x, + y, + ) + + if pt is None: + return + + if event == cv2.EVENT_LBUTTONDOWN: + dragging = True + drag_start = pt + + elif ( + event == cv2.EVENT_MOUSEMOVE + and dragging + and drag_start is not None + ): + x0, y0 = drag_start + x1, y1 = pt + + roi_rects[selected_role] = ( + x0, + y0, + x1, + y1, + ) + + elif ( + event == cv2.EVENT_LBUTTONUP + and dragging + and drag_start is not None + ): + x0, y0 = drag_start + x1, y1 = pt + + img = runtime.frames[ + selected_role + ] + + r = sanitize_roi( + ( + x0, + y0, + x1, + y1, + ), + img.shape[:2], + ) + + dragging = False + drag_start = None + + if r is not None: + roi_rects[selected_role] = r + + reset_role_state( + state, + selected_role, + args, + ) + field_history_by_role[selected_role].clear() + field_qa_by_role[selected_role] = None + + message( + f"ROI alterada: " + f"{selected_role.upper()} | sweep resetado" + ) + + cv2.setMouseCallback( + "Focus Calibration - Production", + on_mouse, + ) + + # ------------------------------------------------ + # Loop principal + # ------------------------------------------------ + + print("") + print("[FOCUS] Ajuste RGB, depois RE e NIR.") + print("[FOCUS] atravesse o pico, volte ao melhor e pressione A.") + + while True: + fresh_roles = runtime.poll() + + # Inicializa ROIs quando os frames existem. + for role in ROLES: + img = runtime.frames[role] + + if ( + img is not None + and roi_rects[role] is None + ): + roi_rects[role] = ( + default_roi_for_shape( + img.shape[:2], + frac=0.36, + ) + ) + + # ------------------------------------------------ + # SCORE SOMENTE EM FRAME NOVO + # ------------------------------------------------ + + if ( + selected_role in fresh_roles + and runtime.frames[selected_role] is not None + and roi_rects[selected_role] is not None + ): + s = state[selected_role] + + seq = runtime.last_seq[ + selected_role + ] + + # Defesa extra contra duplicate score. + if ( + seq is None + or seq != s["last_scored_seq"] + ): + img = runtime.frames[ + selected_role + ] + + metrics = compute_focus_metrics( + img, + roi_rects[selected_role], + equalize=args.equalize_runtime, + ) + + score = metric_value( + metrics, + args.method, + ) + + hist = s["history"] + + hist.append({ + "timestamp": now_str(), + "sequence_num": safe_int( + seq, + None, + ), + "score": float(score), + "smooth": float(score), + }) + + smooth = smoothed_score( + hist, + args.smooth_window, + ) + + hist[-1]["smooth"] = float( + smooth + ) + + update_sweep( + s["sweep"], + score=float(score), + smooth=float(smooth), + metrics=metrics, + roi=roi_rects[selected_role], + sequence_num=seq, + drop_required_pct=qa[ + "peak_drop_required_pct" + ], + new_best_rebracket_pct=qa[ + "new_best_rebracket_pct" + ], + ) + + s["score"] = float(score) + s["smooth"] = float(smooth) + s["metrics"] = metrics + + s["trend"] = analyze_trend( + hist, + s["sweep"], + s["direction_name"], + drop_warn_pct=args.drop_warn_pct, + ) + + s["stable"] = evaluate_stability( + hist, + s["sweep"], + qa, + ) + + # FIELD QA entra no histórico apenas em frame novo. + field_history_by_role[ + selected_role + ].append( + compute_field_qa( + img, + qa, + equalize=args.equalize_runtime, + ) + ) + + field_qa_by_role[ + selected_role + ] = aggregate_field_qa( + field_history_by_role[ + selected_role + ], + qa, + ) + + s["last_scored_seq"] = seq + + # Atualiza FIELD QA das outras câmeras somente com frames novos, + # sem alimentar o histórico do score principal. + for role in ( + fresh_roles + - {selected_role} + ): + img = runtime.frames[role] + + if img is not None: + field_history_by_role[ + role + ].append( + compute_field_qa( + img, + qa, + equalize=args.equalize_runtime, + ) + ) + + field_qa_by_role[ + role + ] = aggregate_field_qa( + field_history_by_role[ + role + ], + qa, + ) + + # ------------------------------------------------ + # FPS watchdog + # ------------------------------------------------ + + fps_warn = [] + + for role in ROLES: + fps = runtime.fps[role] + + if ( + fps > 0 + and fps + < args.fps + * qa["fps_min_ratio"] + ): + fps_warn.append( + f"{role.upper()} FPS baixo={fps:.1f}" + ) + + if fps_warn: + message( + " | ".join(fps_warn) + ) + + # ------------------------------------------------ + # Board + # ------------------------------------------------ + + ph = int(args.panel_height) + pw = int(args.panel_width) + + panel_rects["rgb"] = ( + 0, + 0, + pw, + ph, + ) + + panel_rects["re"] = ( + pw, + 0, + pw * 2, + ph, + ) + + panel_rects["nir"] = ( + 0, + ph, + pw, + ph * 2, + ) + + panel_rects["data"] = ( + pw, + ph, + pw * 2, + ph * 2, + ) + + msg = ( + last_message + if ( + last_message + and time.time() + - last_message_t + < 3.0 + ) + else "" + ) + + last_board = build_board( + runtime, + specs, + selected_role, + roi_rects, + state, + field_qa_by_role, + args, + qa, + status_message=msg, + ) + + cv2.imshow( + "Focus Calibration - Production", + last_board, + ) + + # ------------------------------------------------ + # Teclas + # ------------------------------------------------ + + k = cv2.waitKey(1) & 0xFF + + if k in ( + ord("q"), + ord("Q"), + 27, + ): + raise KeyboardInterrupt( + "Sessão de foco cancelada." + ) + + elif k == ord("1"): + selected_role = "rgb" + message("Selecionada RGB") + + elif k == ord("2"): + selected_role = "re" + message("Selecionada RE") + + elif k == ord("3"): + selected_role = "nir" + message("Selecionada NIR") + + elif k in ( + ord("m"), + ord("M"), + ): + idx = METHODS.index( + args.method + ) + + args.method = METHODS[ + (idx + 1) + % len(METHODS) + ] + + # Score mudou de domínio: reset de todos os sweeps. + for role in ROLES: + reset_role_state( + state, + role, + args, + ) + field_history_by_role[role].clear() + field_qa_by_role[role] = None + + message( + f"Métrica -> {args.method} | sweeps resetados" + ) + + elif k in ( + ord("d"), + ord("D"), + ): + s = state[selected_role] + + s["direction_idx"] = ( + 1 - s["direction_idx"] + ) + + s["direction_name"] = ( + "rosqueando" + if s["direction_idx"] == 0 + else "desrosqueando" + ) + + message( + f"Sentido -> {s['direction_name']}" + ) + + elif k in ( + ord("e"), + ord("E"), + ): + args.equalize_runtime = ( + not args.equalize_runtime + ) + + for role in ROLES: + reset_role_state( + state, + role, + args, + ) + field_history_by_role[role].clear() + field_qa_by_role[role] = None + + message( + f"Equalize -> " + f"{'ON' if args.equalize_runtime else 'OFF'} | " + "sweeps resetados" + ) + + elif k in ( + ord("l"), + ord("L"), + ): + roi_locked = not roi_locked + + message( + f"ROI -> " + f"{'travada' if roi_locked else 'editavel'}" + ) + + elif k in ( + ord("c"), + ord("C"), + ): + img = runtime.frames[ + selected_role + ] + + if img is not None: + roi_rects[selected_role] = ( + default_roi_for_shape( + img.shape[:2], + frac=0.36, + ) + ) + + reset_role_state( + state, + selected_role, + args, + ) + field_history_by_role[selected_role].clear() + field_qa_by_role[selected_role] = None + + message( + f"ROI centralizada {selected_role.upper()} | " + "sweep resetado" + ) + + elif k in ( + ord("r"), + ord("R"), + ): + reset_role_state( + state, + selected_role, + args, + ) + field_history_by_role[selected_role].clear() + field_qa_by_role[selected_role] = None + + message( + f"Sweep resetado: {selected_role.upper()}" + ) + + elif k in ( + ord("s"), + ord("S"), + ): + paths = save_role_snapshot( + selected_role, + runtime, + last_board, + snapshot_dir, + "diagnostic", + ) + + report["snapshots"].append({ + "timestamp": now_str(), + "role": selected_role, + "type": "diagnostic", + "paths": paths, + }) + + message( + f"Snapshot salvo: {selected_role.upper()}" + ) + + elif k in ( + ord("a"), + ord("A"), + ): + acceptance = attempt_accept( + selected_role, + state, + field_qa_by_role, + runtime, + roi_rects, + args, + qa, + ) + + if acceptance["accepted"]: + paths = save_role_snapshot( + selected_role, + runtime, + last_board, + snapshot_dir, + "accepted", + ) + + acceptance[ + "snapshot_paths" + ] = paths + + state[selected_role][ + "acceptance" + ] = acceptance + + message( + f"{selected_role.upper()} ACCEPTED ✅" + ) + + print( + f"[ACCEPT] {selected_role.upper()} " + f"ratio={acceptance['accepted_ratio_to_best']*100:.2f}% " + f"field={acceptance['field_qa']['status']}" + ) + + # Avança automaticamente para próxima não aceita. + pending = [ + r for r in ROLES + if not state[r]["accepted"] + ] + + if pending: + selected_role = pending[0] + + else: + break + + else: + message( + "ACCEPT bloqueado: " + + ", ".join( + acceptance[ + "blocked_reasons" + ] + ) + ) + + time.sleep(0.001) + + # ------------------------------------------------ + # Final PASS + # ------------------------------------------------ + + all_accepted = all( + state[r]["accepted"] + for r in ROLES + ) + + if not all_accepted: + raise RuntimeError( + "Loop finalizou sem todas as câmeras aceitas." + ) + + report["results_by_role"] = ( + state_to_report(state) + ) + + report["finished_at"] = now_str() + report["status"] = "pass" + + # Se engineering override foi usado em qualquer role, + # continua PASS operacional, mas fica marcado como não-homologação normal. + override_roles = [ + role + for role in ROLES + if ( + state[role] + .get("acceptance", {}) + .get( + "engineering_override_used", + False, + ) + ) + ] + + report["engineering_override_roles"] = ( + override_roles + ) + + report["normal_production_homologation"] = ( + len(override_roles) == 0 + ) + + # Override de engenharia pode concluir a sessão para diagnóstico, + # mas NUNCA substitui o último QC homologado. + report["promoted"] = bool( + report["normal_production_homologation"] + ) + + if report["promoted"]: + report["promoted_at"] = now_str() + + save_json_atomic( + candidate_report, + report, + ) + + marker_name = ( + "PASS.txt" + if report["normal_production_homologation"] + else "PASS_ENGINEERING_OVERRIDE.txt" + ) + + ( + candidate_dir + / marker_name + ).write_text( + ( + f"status=pass\n" + f"created_at={report['created_at']}\n" + f"finished_at={report['finished_at']}\n" + f"normal_production_homologation=" + f"{report['normal_production_homologation']}\n" + f"promoted={report['promoted']}\n" + ), + encoding="utf-8", + ) + + if report["promoted"]: + active = Path(args.active_json) + + if active.exists(): + backup = active.with_name( + active.stem + + f".backup_{sid}" + + active.suffix + ) + + shutil.copy2( + active, + backup, + ) + + save_json_atomic( + active, + report, + ) + + print("") + print("=" * 82) + + if report["promoted"]: + print("[PASS] FOCUS QC APROVADO E HOMOLOGADO") + print(f"[ACTIVE QC] {args.active_json}") + else: + print("[PASS OVERRIDE] Sessão concluída somente para engenharia") + print("[SAFE] QC ativo homologado anterior foi preservado.") + + print(f"[CANDIDATE] {candidate_dir}") + print("[RUNTIME] Nenhum parâmetro do module_params foi alterado.") + print("=" * 82) + + final = np.zeros( + (720, 1280, 3), + dtype=np.uint8, + ) + + lines = [ + "FOCUS QC - PASS", + "", + f"RGB: {specs['rgb'].sensor_name} ACCEPTED", + f"RE : {specs['re'].sensor_name} ACCEPTED", + f"NIR: {specs['nir'].sensor_name} ACCEPTED", + "", + f"Homologacao normal: " + f"{'SIM' if report['normal_production_homologation'] else 'NAO - override usado'}", + "", + "Nenhum parametro de runtime foi modificado.", + ( + f"Relatorio ativo: {args.active_json}" + if report["promoted"] + else f"Candidate: {candidate_report}" + ), + "", + "Qualquer tecla fecha.", + ] + + overlay_hud( + final, + lines, + x=46, + y=70, + font_scale=0.72, + line_step=34, + ) + + cv2.imshow( + "Focus Calibration - Production", + final, + ) + + cv2.waitKey(0) + + except KeyboardInterrupt as exc: + if report is None: + report = { + "schema": SCHEMA, + "created_at": now_str(), + "runtime_effect": "none", + "module_params_fragment": None, + } + + if state is not None: + report["results_by_role"] = state_to_report(state) + + report["status"] = "cancelled" + report["finished_at"] = now_str() + report["error"] = str(exc) + report["promoted"] = False + + try: + save_json_atomic( + candidate_report, + report, + ) + + ( + candidate_dir + / "CANCELLED.txt" + ).write_text( + ( + f"status=cancelled\n" + f"reason={exc}\n" + ), + encoding="utf-8", + ) + except Exception: + pass + + print(f"[CANCELADO] {exc}") + print("[SAFE] QC ativo anterior preservado.") + + except Exception as exc: + if report is None: + report = { + "schema": SCHEMA, + "created_at": now_str(), + "runtime_effect": "none", + "module_params_fragment": None, + } + + if state is not None: + report["results_by_role"] = state_to_report(state) + + report["status"] = "error" + report["finished_at"] = now_str() + report["error"] = ( + f"{type(exc).__name__}: {exc}" + ) + report["promoted"] = False + + try: + save_json_atomic( + candidate_report, + report, + ) + + ( + candidate_dir + / "FAIL.txt" + ).write_text( + ( + f"status=error\n" + f"error={type(exc).__name__}: {exc}\n" + ), + encoding="utf-8", + ) + except Exception: + pass + + print("") + print("=" * 82) + print("[ERRO] FOCUS QC ABORTADO") + print(f"{type(exc).__name__}: {exc}") + print("[SAFE] QC ativo anterior preservado.") + print("=" * 82) + + raise + + finally: + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/_2_intrinsics_calibration_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/_2_intrinsics_calibration_tool.py new file mode 100644 index 000000000..37c64d52a --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/utils/_2_intrinsics_calibration_tool.py @@ -0,0 +1,2088 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +intrinsics_calibration_production.py + +Calibração intrínseca + distorção de lente, nível produto, para: + + CAM_A = RGB = OV9782 1280x800 OU AR0234 1920x1200 + CAM_B = RE = OV9282 1280x800 + CAM_C = NIR = OV9282 1280x800 + +Domínio: + RGB = ISP full-resolution + RE/NIR = MONO full-resolution + sem resize + sem undistort externo + +Gera: + calibration/intrinsics_calibration_v1.json + +O artefato contém: + module_params_fragment.intrinsics_config + +IMPORTANTE: + runtime_undistort.enabled = false por padrão. + Se no futuro ativarmos undistort no runtime, a homografia precisa ser + recalibrada no mesmo espaço geométrico undistorted. + +Fluxo: + 1) Mostre o ChArUco nas 3 câmeras. + 2) ENTER trava EXP/ISO. + 3) G captura views variadas. + 4) Varie X/Y, distância, yaw, pitch e roll. + 5) Ideal: 18-25 views. + 6) A calibra e executa acceptance. + 7) PASS promove o JSON ativo. + +Teclas: + G captura view + A calibra/avalia + V limpa views + S snapshot + 1/2/3 seleciona câmera do preview + U liga/desliga preview undistorted após avaliação + Q/ESC cancela +""" +from __future__ import annotations + +import argparse +import json +import math +import os +import shutil +import time +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +from typing import Optional + +import cv2 +import depthai as dai +import numpy as np + + +ROLES = ("rgb", "re", "nir") +SCHEMA = "multispec_intrinsics_calibration_v1" +CALIBRATION_SPACE = "native_stream_no_external_undistort" + +TOPOLOGY = { + "rgb": ("CAM_A", ("OV9782", "AR0234")), + "re": ("CAM_B", ("OV9282",)), + "nir": ("CAM_C", ("OV9282",)), +} + +RGB_MODES = { + "OV9782": ("THE_800_P", 1280, 800), + "AR0234": ("THE_1200_P", 1920, 1200), +} +MONO_MODES = { + "OV9282": ("THE_800_P", 1280, 800), +} + +QA = { + "min_markers": 4, + "min_corners": 10, + "min_views": 14, + "recommended_views": 20, + "min_retained_views": 12, + "min_unique_ids": 24, + + "duplicate_center_norm": 0.035, + "duplicate_area_norm": 0.055, + + "coverage_span_x_warn": 0.72, + "coverage_span_x_bad": 0.60, + "coverage_span_y_warn": 0.66, + "coverage_span_y_bad": 0.54, + "coverage_hull_warn": 0.36, + "coverage_hull_bad": 0.25, + + "area_range_warn": 0.12, + "area_range_bad": 0.07, + "center_std_x_warn": 0.18, + "center_std_x_bad": 0.11, + "center_std_y_warn": 0.16, + "center_std_y_bad": 0.10, + + "rms_warn": 0.85, + "rms_bad": 1.40, + "fit_med_warn": 0.55, + "fit_med_bad": 0.90, + "fit_p95_warn": 1.20, + "fit_p95_bad": 2.00, + + "cv_med_warn": 0.85, + "cv_med_bad": 1.35, + "cv_p95_warn": 1.80, + "cv_p95_bad": 3.00, + "cv_view_med_warn": 1.60, + "cv_view_med_bad": 2.60, + + "reject_view_med": 1.80, + "reject_view_p95": 3.20, + "max_reject_rounds": 3, + + "fx_fy_ratio_warn": 1.08, + "fx_fy_ratio_bad": 1.18, + "principal_margin_frac": 0.12, + "focal_min_frac_w": 0.18, + "focal_max_frac_w": 8.0, + + "brown_coeff_warn": 1.5, + "brown_coeff_bad": 5.0, + "rational_coeff_warn": 5.0, + "rational_coeff_bad": 20.0, + + "rational_min_cv_improvement": 0.10, + + "edge_shift_warn_px": 8.0, + "edge_shift_high_px": 20.0, + + "exp_rel_tol": 0.01, + "exp_abs_tol_us": 20, + "iso_tol": 5, +} + + +def now_str(): + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def stamp(): + return datetime.now().strftime("%Y%m%d_%H%M%S_%f") + + +def ensure_dir(p): + Path(p).mkdir(parents=True, exist_ok=True) + + +def save_json_atomic(path, data): + p = Path(path) + ensure_dir(p.parent) + tmp = p.with_suffix(p.suffix + ".tmp") + with tmp.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, p) + + +def status_rank(s): + return {"good": 0, "warning": 1, "bad": 2}.get(str(s), 2) + + +def merge_status(a, b): + return a if status_rank(a) >= status_rank(b) else b + + +def safe_int(v, default=None): + try: + return int(v) if v is not None else default + except Exception: + return default + + +def overlay(img, lines, x=12, y=24, scale=0.45, step=19): + yy = y + for line in lines: + if yy > img.shape[0] - 6: + break + s = str(line) + cv2.putText(img, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, + scale, (0, 0, 0), 3, cv2.LINE_AA) + cv2.putText(img, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, + scale, (255, 255, 255), 1, cv2.LINE_AA) + yy += step + + +def socket_name(sock): + name = getattr(sock, "name", None) + if name: + return str(name) + text = str(sock) + for x in ("CAM_A", "CAM_B", "CAM_C", "CAM_D"): + if x in text: + return x + return text + + +def get_socket(name): + return { + "CAM_A": dai.CameraBoardSocket.CAM_A, + "CAM_B": dai.CameraBoardSocket.CAM_B, + "CAM_C": dai.CameraBoardSocket.CAM_C, + }[name] + + +def feature_types(f): + return [str(x).upper() for x in (getattr(f, "supportedTypes", []) or [])] + + +def feature_is_color(f): + types = feature_types(f) + sensor = str(getattr(f, "sensorName", "") or "").upper() + if any("COLOR" in x for x in types): + return True + if any("MONO" in x for x in types): + return False + return sensor in {"OV9782", "AR0234"} + + +def feature_is_mono(f): + types = feature_types(f) + if any("MONO" in x for x in types): + return True + if any("COLOR" in x for x in types): + return False + return not feature_is_color(f) + + +@dataclass +class CameraSpec: + role: str + socket: str + sensor: str + width: int + height: int + resolution_name: str + stream: str + control: str + is_color: bool + source: str + + +def discover(mx_id: Optional[str]): + info = dai.DeviceInfo(mx_id) if mx_id else None + ctx = dai.Device(info) if info is not None else dai.Device() + + with ctx as dev: + actual_mx = None + for method in ("getMxId", "getDeviceId"): + fn = getattr(dev, method, None) + if callable(fn): + try: + actual_mx = str(fn()) + if actual_mx: + break + except Exception: + pass + + try: + usb = str(dev.getUsbSpeed()) + except Exception: + usb = None + + rows = [] + for f in dev.getConnectedCameraFeatures(): + rows.append({ + "socket": socket_name(f.socket), + "sensor": str(f.sensorName or "").upper(), + "width": int(getattr(f, "width", 0) or 0), + "height": int(getattr(f, "height", 0) or 0), + "is_color": feature_is_color(f), + "is_mono": feature_is_mono(f), + "supported_types": feature_types(f), + }) + + return rows, actual_mx, usb + + +def validate_specs(rows): + by_socket = {r["socket"]: r for r in rows} + specs = {} + errors = [] + + for role in ROLES: + sock, allowed = TOPOLOGY[role] + row = by_socket.get(sock) + + if row is None: + errors.append(f"{role}: {sock} ausente") + continue + + sensor = row["sensor"] + if sensor not in allowed: + errors.append(f"{role}: {sock}={sensor}, esperado={allowed}") + continue + + if role == "rgb": + mode, w, h = RGB_MODES[sensor] + if not row["is_color"]: + errors.append(f"{sock}/{sensor} não COLOR") + else: + mode, w, h = MONO_MODES[sensor] + if not row["is_mono"]: + errors.append(f"{sock}/{sensor} não MONO") + + if row["width"] and row["height"]: + if (row["width"], row["height"]) != (w, h): + errors.append( + f"{sock}/{sensor}: {row['width']}x{row['height']} != {w}x{h}" + ) + + specs[role] = CameraSpec( + role=role, + socket=sock, + sensor=sensor, + width=w, + height=h, + resolution_name=mode, + stream=f"intr_{role}", + control=f"intr_ctrl_{role}", + is_color=(role == "rgb"), + source="ISP" if role == "rgb" else "MONO_OUT", + ) + + if errors: + raise RuntimeError("Topologia inválida:\n - " + "\n - ".join(errors)) + + return specs + + +def hardware_signature(specs): + return { + role: { + "socket": s.socket, + "sensor": s.sensor, + "size": [s.width, s.height], + "stream_source": s.source, + } + for role, s in specs.items() + } + + +def build_pipeline(specs, fps, sharpness, luma, chroma): + p = dai.Pipeline() + isp = {"sharpness": None, "luma_denoise": None, "chroma_denoise": None, + "errors": []} + + for role in ROLES: + s = specs[role] + sock = get_socket(s.socket) + + if s.is_color: + cam = p.createColorCamera() + cam.setBoardSocket(sock) + enum = getattr(dai.ColorCameraProperties.SensorResolution, + s.resolution_name, None) + if enum is None: + raise RuntimeError(f"DepthAI sem {s.resolution_name}") + cam.setResolution(enum) + cam.setFps(float(fps)) + cam.setInterleaved(False) + try: + cam.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR) + cam.setIspScale(1, 1) + except Exception: + pass + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + try: + cam.initialControl.setAutoWhiteBalanceLock(False) + except Exception: + pass + + for key, method, value in ( + ("sharpness", "setSharpness", sharpness), + ("luma_denoise", "setLumaDenoise", luma), + ("chroma_denoise", "setChromaDenoise", chroma), + ): + fn = getattr(cam.initialControl, method, None) + if callable(fn): + try: + fn(int(value)) + isp[key] = int(value) + except Exception as exc: + isp["errors"].append(f"{method}:{exc}") + else: + isp["errors"].append(f"{method}:unsupported") + + output = cam.isp + else: + cam = p.createMonoCamera() + cam.setBoardSocket(sock) + enum = getattr(dai.MonoCameraProperties.SensorResolution, + s.resolution_name, None) + if enum is None: + raise RuntimeError(f"DepthAI sem {s.resolution_name}") + cam.setResolution(enum) + cam.setFps(float(fps)) + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + output = cam.out + + xo = p.createXLinkOut() + xo.setStreamName(s.stream) + output.link(xo.input) + + xi = p.createXLinkIn() + xi.setStreamName(s.control) + xi.out.link(cam.inputControl) + + return p, isp + + +@dataclass +class LockedControl: + role: str + exposure_time_us: int + sensitivity_iso: int + + +def packet_controls(pkt): + exp = iso = seq = None + try: + v = pkt.getExposureTime() + exp = int(round(v.total_seconds() * 1e6)) if hasattr(v, "total_seconds") else int(v) + except Exception: + pass + try: + iso = int(pkt.getSensitivity()) + except Exception: + pass + try: + seq = int(pkt.getSequenceNum()) + except Exception: + pass + return { + "exposure_time_us": exp, + "sensitivity_iso": iso, + "sequence_num": seq, + } + + +def controls_match(actual, target): + exp = safe_int(actual.get("exposure_time_us")) + iso = safe_int(actual.get("sensitivity_iso")) + if exp is None or iso is None: + return False + exp_tol = max( + QA["exp_abs_tol_us"], + int(round(target.exposure_time_us * QA["exp_rel_tol"])), + ) + return ( + abs(exp - target.exposure_time_us) <= exp_tol + and abs(iso - target.sensitivity_iso) <= QA["iso_tol"] + ) + + +class Runtime: + def __init__(self, dev, specs): + self.specs = specs + self.oq = { + r: dev.getOutputQueue(name=specs[r].stream, maxSize=3, blocking=False) + for r in ROLES + } + self.cq = { + r: dev.getInputQueue(specs[r].control) + for r in ROLES + } + self.frames = {r: None for r in ROLES} + self.ctrl = {r: {} for r in ROLES} + self.seq = {r: None for r in ROLES} + + def poll(self): + fresh = set() + for r in ROLES: + pkt = self.oq[r].tryGet() + if pkt is None: + continue + + c = packet_controls(pkt) + seq = c["sequence_num"] + + if seq is not None and seq == self.seq[r]: + continue + + img = pkt.getCvFrame() + if img is None: + continue + + if img.shape[:2] != (self.specs[r].height, self.specs[r].width): + raise RuntimeError( + f"{r}: shape={img.shape[:2]}, esperado=" + f"{(self.specs[r].height, self.specs[r].width)}" + ) + + self.frames[r] = np.ascontiguousarray(img) + self.ctrl[r] = c + self.seq[r] = seq + fresh.add(r) + + return fresh + + def wait_all(self, timeout=8.0): + t0 = time.time() + while time.time() - t0 < timeout: + self.poll() + if all(self.frames[r] is not None for r in ROLES): + return + time.sleep(0.003) + raise TimeoutError("Timeout aguardando câmeras.") + + def fresh_triplet(self, timeout=3.0): + start = dict(self.seq) + frames, ctrls = {}, {} + t0 = time.time() + + while time.time() - t0 < timeout: + fresh = self.poll() + for r in fresh: + if r not in frames and self.seq[r] != start.get(r): + frames[r] = self.frames[r].copy() + ctrls[r] = dict(self.ctrl[r]) + + if len(frames) == 3: + return {"frames": frames, "controls": ctrls} + + time.sleep(0.001) + + raise TimeoutError("Triplet incompleto.") + + def send_manual(self, locked): + for r in ROLES: + c = dai.CameraControl() + c.setManualExposure( + int(locked[r].exposure_time_us), + int(locked[r].sensitivity_iso), + ) + if r == "rgb": + try: + c.setAutoWhiteBalanceLock(True) + except Exception: + pass + self.cq[r].send(c) + + def verify_manual(self, locked, settle_frames=8, timeout=10.0): + seen = dict(self.seq) + ok = {r: 0 for r in ROLES} + t0 = time.time() + + while time.time() - t0 < timeout: + fresh = self.poll() + for r in fresh: + if self.seq[r] == seen.get(r): + continue + seen[r] = self.seq[r] + ok[r] = ok[r] + 1 if controls_match(self.ctrl[r], locked[r]) else 0 + + if all(ok[r] >= settle_frames for r in ROLES): + return + + time.sleep(0.002) + + raise RuntimeError(f"EXP/ISO não estabilizou: {ok}") + + +def lock_current_ae(runtime): + locked = {} + for role in ROLES: + exps, isos = [], [] + t0 = time.time() + + while time.time() - t0 < 0.8: + fresh = runtime.poll() + if role not in fresh: + time.sleep(0.002) + continue + + e = safe_int(runtime.ctrl[role].get("exposure_time_us")) + i = safe_int(runtime.ctrl[role].get("sensitivity_iso")) + + if e and e > 0: + exps.append(e) + if i and i > 0: + isos.append(i) + + if not exps or not isos: + raise RuntimeError(f"Sem EXP/ISO em {role}") + + locked[role] = LockedControl( + role=role, + exposure_time_us=int(round(np.median(exps))), + sensitivity_iso=int(round(np.median(isos))), + ) + + runtime.send_manual(locked) + runtime.verify_manual(locked) + return locked + + +# ============================================================ +# ChArUco +# ============================================================ + +def require_aruco(): + if not hasattr(cv2, "aruco"): + raise RuntimeError("Use opencv-contrib-python; cv2.aruco não existe.") + + +def aruco_dictionary(name): + require_aruco() + if not hasattr(cv2.aruco, name): + raise ValueError(f"Dicionário inválido: {name}") + enum = getattr(cv2.aruco, name) + if hasattr(cv2.aruco, "getPredefinedDictionary"): + return cv2.aruco.getPredefinedDictionary(enum) + return cv2.aruco.Dictionary_get(enum) + + +def make_board(nx, ny, square, marker, dictionary): + if hasattr(cv2.aruco, "CharucoBoard"): + try: + return cv2.aruco.CharucoBoard( + (int(nx), int(ny)), float(square), float(marker), dictionary + ) + except Exception: + pass + + if hasattr(cv2.aruco, "CharucoBoard_create"): + return cv2.aruco.CharucoBoard_create( + int(nx), int(ny), float(square), float(marker), dictionary + ) + + raise RuntimeError("CharucoBoard incompatível com este OpenCV.") + + +def board_object_points(board): + if hasattr(board, "getChessboardCorners"): + pts = board.getChessboardCorners() + else: + pts = getattr(board, "chessboardCorners", None) + + if pts is None: + raise RuntimeError("Sem chessboard corners no board.") + + return np.asarray(pts, dtype=np.float32).reshape(-1, 3) + + +def detector_params(): + if hasattr(cv2.aruco, "DetectorParameters"): + p = cv2.aruco.DetectorParameters() + else: + p = cv2.aruco.DetectorParameters_create() + + try: + p.cornerRefinementMethod = getattr(cv2.aruco, "CORNER_REFINE_SUBPIX", 1) + p.cornerRefinementWinSize = 5 + p.cornerRefinementMaxIterations = 40 + p.cornerRefinementMinAccuracy = 0.01 + except Exception: + pass + + return p + + +def detect_charuco(img, board, dictionary, params): + gray = img if img.ndim == 2 else cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + if hasattr(cv2.aruco, "ArucoDetector"): + det = cv2.aruco.ArucoDetector(dictionary, params) + mc, mi, _ = det.detectMarkers(gray) + else: + mc, mi, _ = cv2.aruco.detectMarkers(gray, dictionary, parameters=params) + + markers = 0 if mi is None else len(mi) + if markers == 0: + return {"markers": 0, "corners": 0, "points": {}} + + cc = ci = None + + if hasattr(cv2.aruco, "CharucoDetector"): + try: + cd = cv2.aruco.CharucoDetector(board) + out = cd.detectBoard( + image=gray, + markerCorners=mc, + markerIds=mi, + ) + cc, ci = out[0], out[1] + except Exception: + pass + + if cc is None and hasattr(cv2.aruco, "interpolateCornersCharuco"): + ret, cc, ci = cv2.aruco.interpolateCornersCharuco( + markerCorners=mc, + markerIds=mi, + image=gray, + board=board, + ) + if ret is None or ret <= 0: + cc = ci = None + + if cc is None or ci is None: + return {"markers": markers, "corners": 0, "points": {}} + + pts = np.asarray(cc, dtype=np.float32).reshape(-1, 2) + ids = np.asarray(ci).reshape(-1) + + try: + cv2.cornerSubPix( + gray, + pts.reshape(-1, 1, 2), + (4, 4), + (-1, -1), + (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 40, 0.01), + ) + except Exception: + pass + + return { + "markers": int(markers), + "corners": int(len(ids)), + "points": { + int(cid): [float(pt[0]), float(pt[1])] + for cid, pt in zip(ids, pts) + }, + } + + +def detect_triplet(triplet, board, dictionary, params): + out = {} + reasons = [] + + for role in ROLES: + d = detect_charuco(triplet["frames"][role], board, dictionary, params) + out[role] = d + + if d["markers"] < QA["min_markers"]: + reasons.append(f"{role}:markers={d['markers']}") + if d["corners"] < QA["min_corners"]: + reasons.append(f"{role}:corners={d['corners']}") + + return { + "valid": not reasons, + "reasons": reasons, + "detections": out, + } + + +# ============================================================ +# Pose/cobertura +# ============================================================ + +def pose_signature(points, shape_hw): + pts = np.asarray(list(points.values()), dtype=np.float32) + h, w = shape_hw + + x0, x1 = float(pts[:, 0].min()), float(pts[:, 0].max()) + y0, y1 = float(pts[:, 1].min()), float(pts[:, 1].max()) + + return { + "cx": ((x0 + x1) * 0.5) / w, + "cy": ((y0 + y1) * 0.5) / h, + "area": ((x1 - x0) * (y1 - y0)) / float(w * h), + } + + +def duplicate_pose(sig, views): + if not views: + return False + + # Rejeita somente se parecer repetida nas três câmeras. + repeated = 0 + + for role in ROLES: + s = sig[role] + role_dup = False + + for v in views: + p = v["pose"][role] + d = math.hypot(s["cx"] - p["cx"], s["cy"] - p["cy"]) + da = abs(s["area"] - p["area"]) + + if d < QA["duplicate_center_norm"] and da < QA["duplicate_area_norm"]: + role_dup = True + break + + repeated += int(role_dup) + + return repeated == 3 + + +def coverage(points, image_size): + pts = np.asarray(points, dtype=np.float32) + w, h = image_size + + if len(pts) < 3: + return {"span_x": 0.0, "span_y": 0.0, "hull": 0.0} + + hull = cv2.convexHull(pts.reshape(-1, 1, 2)) + + return { + "span_x": float((pts[:, 0].max() - pts[:, 0].min()) / w), + "span_y": float((pts[:, 1].max() - pts[:, 1].min()) / h), + "hull": float(cv2.contourArea(hull) / (w * h)), + } + + +def diversity(views, role): + if not views: + return {} + + cx = np.array([v["pose"][role]["cx"] for v in views], dtype=np.float64) + cy = np.array([v["pose"][role]["cy"] for v in views], dtype=np.float64) + area = np.array([v["pose"][role]["area"] for v in views], dtype=np.float64) + + return { + "center_std_x": float(cx.std()), + "center_std_y": float(cy.std()), + "area_min": float(area.min()), + "area_max": float(area.max()), + "area_range": float(area.max() - area.min()), + } + + +# ============================================================ +# Dataset / calibração +# ============================================================ + +def view_object_image(view, role, board_pts): + p = view["detections"][role]["points"] + ids = sorted(int(x) for x in p.keys()) + + obj = np.asarray([board_pts[i] for i in ids], dtype=np.float32).reshape(-1, 1, 3) + img = np.asarray([p[str(i)] if str(i) in p else p[i] for i in ids], + dtype=np.float32).reshape(-1, 1, 2) + + return ids, obj, img + + +def build_dataset(views, role, board_pts, retained=None): + allowed = None if retained is None else set(int(x) for x in retained) + objs, imgs, view_ids, charuco_ids = [], [], [], [] + + for v in views: + vid = int(v["view_id"]) + if allowed is not None and vid not in allowed: + continue + + ids, obj, img = view_object_image(v, role, board_pts) + if len(ids) < 4: + continue + + objs.append(obj) + imgs.append(img) + view_ids.append(vid) + charuco_ids.extend(ids) + + return { + "objects": objs, + "images": imgs, + "view_ids": view_ids, + "charuco_ids": charuco_ids, + } + + +def model_flags(name): + if name == "brown5": + return cv2.CALIB_FIX_K4 | cv2.CALIB_FIX_K5 | cv2.CALIB_FIX_K6 + if name == "rational8": + return cv2.CALIB_RATIONAL_MODEL + raise ValueError(name) + + +def calibrate_model(dataset, image_size, model_name): + if len(dataset["objects"]) < 3: + raise RuntimeError("Poucas views.") + + w, h = image_size + K0 = np.array([ + [float(w), 0.0, w / 2.0], + [0.0, float(w), h / 2.0], + [0.0, 0.0, 1.0], + ], dtype=np.float64) + + D0 = np.zeros((8 if model_name == "rational8" else 5, 1), dtype=np.float64) + flags = model_flags(model_name) + criteria = ( + cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, + 100, + 1e-10, + ) + + if hasattr(cv2, "calibrateCameraExtended"): + rms, K, D, rvecs, tvecs, std_i, _, per_view = cv2.calibrateCameraExtended( + dataset["objects"], + dataset["images"], + (w, h), + K0, + D0, + flags=flags, + criteria=criteria, + ) + else: + rms, K, D, rvecs, tvecs = cv2.calibrateCamera( + dataset["objects"], + dataset["images"], + (w, h), + K0, + D0, + flags=flags, + criteria=criteria, + ) + std_i = per_view = None + + model = { + "name": model_name, + "rms": float(rms), + "K": np.asarray(K, dtype=np.float64), + "D": np.asarray(D, dtype=np.float64).reshape(-1), + "rvecs": [np.asarray(x).reshape(3) for x in rvecs], + "tvecs": [np.asarray(x).reshape(3) for x in tvecs], + "std_intrinsics": None if std_i is None else np.asarray(std_i).reshape(-1), + "opencv_per_view": None if per_view is None else np.asarray(per_view).reshape(-1), + } + + return model + + +def reprojection_errors(model, dataset): + per_view = [] + all_e = [] + + for idx, (obj, img, rv, tv) in enumerate(zip( + dataset["objects"], dataset["images"], model["rvecs"], model["tvecs"] + )): + proj, _ = cv2.projectPoints(obj, rv, tv, model["K"], model["D"]) + obs = np.asarray(img).reshape(-1, 2) + pred = np.asarray(proj).reshape(-1, 2) + e = np.linalg.norm(obs - pred, axis=1) + + all_e.extend(e.tolist()) + per_view.append({ + "view_id": int(dataset["view_ids"][idx]), + "count": int(len(e)), + "mean_px": float(e.mean()), + "median_px": float(np.median(e)), + "p95_px": float(np.percentile(e, 95)), + "max_px": float(e.max()), + }) + + a = np.asarray(all_e, dtype=np.float64) + + return { + "per_view": per_view, + "overall": { + "count": int(a.size), + "mean_px": float(a.mean()) if a.size else None, + "median_px": float(np.median(a)) if a.size else None, + "p95_px": float(np.percentile(a, 95)) if a.size else None, + "max_px": float(a.max()) if a.size else None, + }, + } + + +def iterative_reject(views, role, board_pts, image_size, model_name): + retained = [int(v["view_id"]) for v in views] + rejected = [] + rounds = [] + + for round_idx in range(QA["max_reject_rounds"] + 1): + ds = build_dataset(views, role, board_pts, retained) + + if len(ds["objects"]) < QA["min_retained_views"]: + break + + model = calibrate_model(ds, image_size, model_name) + err = reprojection_errors(model, ds) + + bad = [ + x for x in err["per_view"] + if ( + x["median_px"] > QA["reject_view_med"] + or x["p95_px"] > QA["reject_view_p95"] + ) + ] + + rounds.append({ + "round": round_idx, + "retained_before": list(retained), + "per_view": err["per_view"], + "bad": [x["view_id"] for x in bad], + }) + + if not bad: + break + + worst = max(bad, key=lambda x: x["median_px"]) + + if len(retained) - 1 < QA["min_retained_views"]: + break + + retained.remove(worst["view_id"]) + rejected.append(worst["view_id"]) + + return { + "retained": retained, + "rejected": rejected, + "rounds": rounds, + } + + +def leave_one_out(views, role, board_pts, image_size, model_name, retained): + retained = sorted(set(int(x) for x in retained)) + reports = [] + all_e = [] + + if len(retained) < 6: + return {"valid": False, "views": [], "overall": {}} + + for held in retained: + train_ids = [x for x in retained if x != held] + train = build_dataset(views, role, board_pts, train_ids) + test = build_dataset(views, role, board_pts, [held]) + + if len(train["objects"]) < 5 or len(test["objects"]) != 1: + continue + + try: + model = calibrate_model(train, image_size, model_name) + obj = test["objects"][0] + img = test["images"][0] + + ok, rv, tv = cv2.solvePnP( + obj, img, model["K"], model["D"], + flags=cv2.SOLVEPNP_ITERATIVE, + ) + if not ok: + continue + + proj, _ = cv2.projectPoints(obj, rv, tv, model["K"], model["D"]) + obs = np.asarray(img).reshape(-1, 2) + pred = np.asarray(proj).reshape(-1, 2) + e = np.linalg.norm(obs - pred, axis=1) + + all_e.extend(e.tolist()) + reports.append({ + "held_out_view_id": int(held), + "count": int(len(e)), + "mean_px": float(e.mean()), + "median_px": float(np.median(e)), + "p95_px": float(np.percentile(e, 95)), + "max_px": float(e.max()), + }) + except Exception: + continue + + a = np.asarray(all_e, dtype=np.float64) + + return { + "valid": len(reports) == len(retained), + "views": reports, + "overall": { + "count": int(a.size), + "mean_px": float(a.mean()) if a.size else None, + "median_px": float(np.median(a)) if a.size else None, + "p95_px": float(np.percentile(a, 95)) if a.size else None, + "max_px": float(a.max()) if a.size else None, + }, + } + + +def distortion_shift(K, D, image_size): + w, h = image_size + xs = np.linspace(0, w - 1, 21) + ys = np.linspace(0, h - 1, 15) + + pts = np.asarray([[x, y] for y in ys for x in xs], + dtype=np.float32).reshape(-1, 1, 2) + + und = cv2.undistortPoints(pts, K, D, P=K).reshape(-1, 2) + orig = pts.reshape(-1, 2) + shift = np.linalg.norm(und - orig, axis=1) + + edge = ( + (orig[:, 0] < 0.15 * w) + | (orig[:, 0] > 0.85 * w) + | (orig[:, 1] < 0.15 * h) + | (orig[:, 1] > 0.85 * h) + ) + + e = shift[edge] + + return { + "median_px": float(np.median(shift)), + "p95_px": float(np.percentile(shift, 95)), + "max_px": float(shift.max()), + "edge_median_px": float(np.median(e)), + "edge_p95_px": float(np.percentile(e, 95)), + "edge_max_px": float(e.max()), + } + + +def undistort_products(K, D, image_size): + w, h = image_size + out = {} + + for alpha in (0.0, 0.5, 1.0): + newK, roi = cv2.getOptimalNewCameraMatrix( + K, D, (w, h), alpha, (w, h), centerPrincipalPoint=False + ) + + out[str(alpha)] = { + "alpha": alpha, + "new_camera_matrix": np.asarray(newK).tolist(), + "valid_roi": [int(x) for x in roi], + } + + return out + + +def sanity(model, image_size): + w, h = image_size + K, D = model["K"], model["D"] + + fx, fy = float(K[0, 0]), float(K[1, 1]) + cx, cy = float(K[0, 2]), float(K[1, 2]) + + ratio = max(fx / max(fy, 1e-9), fy / max(fx, 1e-9)) + margin_x = QA["principal_margin_frac"] * w + margin_y = QA["principal_margin_frac"] * h + + status = "good" + reasons = [] + + if not (QA["focal_min_frac_w"] * w <= fx <= QA["focal_max_frac_w"] * w): + status = "bad" + reasons.append(f"fx_implausible:{fx:.1f}") + + if not (QA["focal_min_frac_w"] * w <= fy <= QA["focal_max_frac_w"] * w): + status = "bad" + reasons.append(f"fy_implausible:{fy:.1f}") + + if ratio > QA["fx_fy_ratio_bad"]: + status = "bad" + reasons.append(f"fx_fy_ratio_bad:{ratio:.3f}") + elif ratio > QA["fx_fy_ratio_warn"]: + status = merge_status(status, "warning") + reasons.append(f"fx_fy_ratio_warn:{ratio:.3f}") + + if not (-margin_x <= cx <= w + margin_x and -margin_y <= cy <= h + margin_y): + status = "bad" + reasons.append(f"principal_point_bad:{cx:.1f},{cy:.1f}") + + max_abs = float(np.max(np.abs(D))) if len(D) else 0.0 + + return { + "status": status, + "reasons": reasons, + "fx": fx, + "fy": fy, + "cx": cx, + "cy": cy, + "fx_fy_ratio": ratio, + "max_abs_dist_coeff": max_abs, + } + + +def evaluate_model(views, role, board_pts, image_size, model_name): + rej = iterative_reject( + views, role, board_pts, image_size, model_name + ) + + ds = build_dataset( + views, role, board_pts, rej["retained"] + ) + + if len(ds["objects"]) < QA["min_retained_views"]: + return { + "status": "bad", + "reasons": ["retained_views_insufficient"], + "model_name": model_name, + } + + model = calibrate_model(ds, image_size, model_name) + fit = reprojection_errors(model, ds) + cv = leave_one_out( + views, role, board_pts, image_size, model_name, rej["retained"] + ) + + all_points = [] + for im in ds["images"]: + all_points.extend(np.asarray(im).reshape(-1, 2).tolist()) + + cov = coverage(all_points, image_size) + retained_views = [v for v in views if int(v["view_id"]) in set(rej["retained"])] + div = diversity(retained_views, role) + sane = sanity(model, image_size) + shift = distortion_shift(model["K"], model["D"], image_size) + und = undistort_products(model["K"], model["D"], image_size) + + status = sane["status"] + reasons = list(sane["reasons"]) + + def high(value, warn, bad, name): + nonlocal status + if value is None: + status = "bad" + reasons.append(f"{name}_missing") + elif value > bad: + status = "bad" + reasons.append(f"{name}_bad:{value:.4f}") + elif value > warn: + status = merge_status(status, "warning") + reasons.append(f"{name}_warn:{value:.4f}") + + def low(value, warn, bad, name): + nonlocal status + if value < bad: + status = "bad" + reasons.append(f"{name}_bad:{value:.4f}") + elif value < warn: + status = merge_status(status, "warning") + reasons.append(f"{name}_warn:{value:.4f}") + + high(model["rms"], QA["rms_warn"], QA["rms_bad"], "rms") + high(fit["overall"]["median_px"], QA["fit_med_warn"], QA["fit_med_bad"], "fit_med") + high(fit["overall"]["p95_px"], QA["fit_p95_warn"], QA["fit_p95_bad"], "fit_p95") + + if not cv.get("valid"): + status = "bad" + reasons.append("cross_validation_invalid") + else: + high(cv["overall"].get("median_px"), QA["cv_med_warn"], QA["cv_med_bad"], "cv_med") + high(cv["overall"].get("p95_px"), QA["cv_p95_warn"], QA["cv_p95_bad"], "cv_p95") + medians = [x["median_px"] for x in cv["views"]] + high(max(medians) if medians else None, + QA["cv_view_med_warn"], QA["cv_view_med_bad"], "cv_worst_view_med") + + low(cov["span_x"], QA["coverage_span_x_warn"], QA["coverage_span_x_bad"], "coverage_x") + low(cov["span_y"], QA["coverage_span_y_warn"], QA["coverage_span_y_bad"], "coverage_y") + low(cov["hull"], QA["coverage_hull_warn"], QA["coverage_hull_bad"], "coverage_hull") + low(div.get("area_range", 0), QA["area_range_warn"], QA["area_range_bad"], "area_range") + low(div.get("center_std_x", 0), QA["center_std_x_warn"], QA["center_std_x_bad"], "center_std_x") + low(div.get("center_std_y", 0), QA["center_std_y_warn"], QA["center_std_y_bad"], "center_std_y") + + max_abs = sane["max_abs_dist_coeff"] + if model_name == "brown5": + warn, bad = QA["brown_coeff_warn"], QA["brown_coeff_bad"] + else: + warn, bad = QA["rational_coeff_warn"], QA["rational_coeff_bad"] + + if max_abs > bad: + status = "bad" + reasons.append(f"dist_coeff_bad:{max_abs:.3f}") + elif max_abs > warn: + status = merge_status(status, "warning") + reasons.append(f"dist_coeff_warn:{max_abs:.3f}") + + unique_ids = sorted(set(ds["charuco_ids"])) + if len(unique_ids) < QA["min_unique_ids"]: + status = "bad" + reasons.append(f"unique_ids_bad:{len(unique_ids)}") + + return { + "status": status, + "reasons": reasons, + "model_name": model_name, + "image_size": list(image_size), + "camera_matrix": model["K"].tolist(), + "dist_coeffs": model["D"].tolist(), + "rms_px": model["rms"], + "std_intrinsics": None if model["std_intrinsics"] is None + else model["std_intrinsics"].tolist(), + "retained_view_ids": rej["retained"], + "rejected_view_ids": rej["rejected"], + "view_rejection": rej, + "fit_error": fit, + "cross_validation": cv, + "coverage": cov, + "diversity": div, + "unique_charuco_ids": len(unique_ids), + "sanity": sane, + "distortion_shift": shift, + "undistort_products": und, + } + + +def choose_model(brown, rational, requested): + if requested == "brown5": + return brown, {"selected": "brown5", "reason": "forced"} + if requested == "rational8": + return rational, {"selected": "rational8", "reason": "forced"} + + if rational["status"] == "bad": + return brown, {"selected": "brown5", "reason": "rational_bad"} + if brown["status"] == "bad": + return rational, {"selected": "rational8", "reason": "brown_bad"} + + b = brown["cross_validation"]["overall"].get("p95_px") + r = rational["cross_validation"]["overall"].get("p95_px") + + if b is None or r is None: + return brown, {"selected": "brown5", "reason": "prefer_simple"} + + improvement = (b - r) / max(b, 1e-9) + + if improvement >= QA["rational_min_cv_improvement"]: + return rational, { + "selected": "rational8", + "reason": "cv_materially_better", + "cv_p95_improvement_fraction": float(improvement), + } + + return brown, { + "selected": "brown5", + "reason": "prefer_simple", + "cv_p95_improvement_fraction": float(improvement), + } + + +def evaluate_all(views, board_pts, specs, requested): + cameras = {} + overall = "good" + reasons = [] + + for role in ROLES: + size = (specs[role].width, specs[role].height) + + brown = evaluate_model( + views, role, board_pts, size, "brown5" + ) + rational = evaluate_model( + views, role, board_pts, size, "rational8" + ) + + selected, selection = choose_model( + brown, rational, requested + ) + + edge = selected.get("distortion_shift", {}).get("edge_p95_px", 0.0) + + if edge >= QA["edge_shift_high_px"]: + rec = "strongly_consider_runtime_undistort" + elif edge >= QA["edge_shift_warn_px"]: + rec = "consider_runtime_undistort" + else: + rec = "runtime_undistort_optional" + + cameras[role] = { + "status": selected["status"], + "reasons": selected["reasons"], + "model_selection": selection, + "selected_model": selected, + "candidate_models": { + "brown5": brown, + "rational8": rational, + }, + "runtime_geometry_recommendation": rec, + } + + overall = merge_status(overall, selected["status"]) + reasons.extend([f"{role}:{x}" for x in selected["reasons"]]) + + return { + "status": overall, + "reasons": reasons, + "cameras": cameras, + } + + +def module_fragment(evaluation, specs): + cameras = {} + + for role in ROLES: + cam = evaluation["cameras"][role] + m = cam["selected_model"] + + cameras[role] = { + "socket": specs[role].socket, + "sensor": specs[role].sensor, + "stream_source": specs[role].source, + "image_size": [specs[role].width, specs[role].height], + "distortion_model": cam["model_selection"]["selected"], + "camera_matrix": m["camera_matrix"], + "dist_coeffs": m["dist_coeffs"], + "undistort_products": m["undistort_products"], + } + + return { + "intrinsics_config": { + "enabled": True, + "calibration_space": CALIBRATION_SPACE, + "cameras": cameras, + "runtime_undistort": { + "enabled": False, + "alpha": 0.0, + "interpolation": "linear", + "reason": ( + "Homography atual foi calibrada no espaço nativo distorcido. " + "Ative undistort somente após recalibrar a homografia no novo espaço." + ), + }, + } + } + + +# ============================================================ +# UI helpers +# ============================================================ + +def draw_points(panel, points, src_shape, color): + if not points: + return + + ph, pw = panel.shape[:2] + sh, sw = src_shape + + for idx, (cid, pt) in enumerate(sorted((int(k), v) for k, v in points.items())): + x = int(float(pt[0]) * pw / sw) + y = int(float(pt[1]) * ph / sh) + cv2.circle(panel, (x, y), 3, color, -1) + if idx < 40: + cv2.putText(panel, str(cid), (x + 3, y - 3), + cv2.FONT_HERSHEY_SIMPLEX, 0.3, color, 1, cv2.LINE_AA) + + +def save_view_preview(path, frames, det): + panels = [] + colors = {"rgb": (0, 255, 0), "re": (0, 255, 255), "nir": (255, 255, 0)} + + for role in ROLES: + img = frames[role] + bgr = img if img.ndim == 3 else cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + p = cv2.resize(bgr, (480, 300), interpolation=cv2.INTER_AREA) + draw_points(p, det["detections"][role]["points"], img.shape[:2], colors[role]) + overlay(p, [ + role.upper(), + f"markers={det['detections'][role]['markers']}", + f"corners={det['detections'][role]['corners']}", + ], x=8, y=18, scale=0.36, step=16) + panels.append(p) + + cv2.imwrite(str(path), np.hstack(panels)) + + +def undistorted(frame, cam_result, alpha): + m = cam_result["selected_model"] + K = np.asarray(m["camera_matrix"], dtype=np.float64) + D = np.asarray(m["dist_coeffs"], dtype=np.float64) + product = m["undistort_products"][str(float(alpha))] + newK = np.asarray(product["new_camera_matrix"], dtype=np.float64) + return cv2.undistort(frame, K, D, None, newK) + + +def build_ui(runtime, specs, views, last_det, evaluation, + selected_role, show_undist, args, msg=""): + pw, ph = args.panel_width, args.panel_height + panels = {} + colors = {"rgb": (0, 255, 0), "re": (0, 255, 255), "nir": (255, 255, 0)} + + for role in ROLES: + frame = runtime.frames[role] + + if show_undist and evaluation is not None and role == selected_role: + frame = undistorted( + frame, evaluation["cameras"][role], args.preview_alpha + ) + + bgr = frame if frame.ndim == 3 else cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR) + p = cv2.resize(bgr, (pw, ph), interpolation=cv2.INTER_AREA) + + if last_det is not None: + draw_points( + p, + last_det["detections"][role]["points"], + runtime.frames[role].shape[:2], + colors[role], + ) + + overlay(p, [ + f"{role.upper()} | {specs[role].sensor}", + f"{specs[role].width}x{specs[role].height}", + "UNDIST PREVIEW" if (show_undist and role == selected_role) else "NATIVE", + f"EXP={runtime.ctrl[role].get('exposure_time_us')}us " + f"ISO={runtime.ctrl[role].get('sensitivity_iso')}", + ], x=8, y=18, scale=0.36, step=16) + + panels[role] = p + + data = np.zeros((ph, pw, 3), dtype=np.uint8) + + lines = [ + "INTRINSICS CALIBRATION - PRODUCTION", + f"views={len(views)} / min={QA['min_views']} rec={QA['recommended_views']}", + f"preview={selected_role.upper()} undist={'ON' if show_undist else 'OFF'}", + "", + "G capture | A calibrate", + "V clear | S snapshot", + "1/2/3 select | U undist preview", + "Q/ESC cancel", + ] + + if evaluation is not None: + lines += ["", f"EVAL={evaluation['status'].upper()}"] + + for role in ROLES: + c = evaluation["cameras"][role] + m = c["selected_model"] + cvmed = m["cross_validation"]["overall"].get("median_px") + lines.append( + f"{role.upper()} {c['model_selection']['selected']} " + f"RMS={m['rms_px']:.3f}px " + f"CV={0.0 if cvmed is None else cvmed:.3f}px " + f"edge={m['distortion_shift']['edge_p95_px']:.1f}px" + ) + + overlay(data, lines, x=12, y=20, scale=0.39, step=17) + + board = np.vstack([ + np.hstack([panels["rgb"], panels["re"]]), + np.hstack([panels["nir"], data]), + ]) + + if msg: + cv2.putText( + board, msg, (16, board.shape[0] - 14), + cv2.FONT_HERSHEY_SIMPLEX, 0.52, (0, 255, 0), 2, cv2.LINE_AA, + ) + + return board + + +# ============================================================ +# Main +# ============================================================ + +def main(): + ap = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + ap.add_argument("--mx-id", default=None) + ap.add_argument("--fps", type=float, default=20.0) + + ap.add_argument("--isp-sharpness", type=int, default=0) + ap.add_argument("--isp-luma-denoise", type=int, default=0) + ap.add_argument("--isp-chroma-denoise", type=int, default=0) + + ap.add_argument("--charuco-dictionary", default="DICT_4X4_50") + ap.add_argument("--charuco-squares-x", type=int, default=13) + ap.add_argument("--charuco-squares-y", type=int, default=7) + ap.add_argument("--charuco-square-length", type=float, default=0.031) + ap.add_argument("--charuco-marker-length", type=float, default=0.023) + + ap.add_argument( + "--distortion-model", + default="auto", + choices=["auto", "brown5", "rational8"], + ) + ap.add_argument("--promote-warning", action="store_true") + + ap.add_argument("--panel-width", type=int, default=640) + ap.add_argument("--panel-height", type=int, default=400) + ap.add_argument("--preview-alpha", type=float, default=0.0, + choices=[0.0, 0.5, 1.0]) + + ap.add_argument( + "--candidate-root", + default="calibration/intrinsics_candidates", + ) + ap.add_argument( + "--active-json", + default="calibration/intrinsics_calibration_v1.json", + ) + + ap.add_argument("--module-id", default="") + ap.add_argument("--operator", default="") + ap.add_argument("--lens-rgb", default="") + ap.add_argument("--lens-re", default="") + ap.add_argument("--lens-nir", default="") + ap.add_argument("--notes", default="") + + args = ap.parse_args() + + sid = stamp() + candidate_dir = Path(args.candidate_root) / sid + views_dir = candidate_dir / "views" + snapshots_dir = candidate_dir / "snapshots" + report_path = candidate_dir / "report.json" + + ensure_dir(views_dir) + ensure_dir(snapshots_dir) + + dictionary = aruco_dictionary(args.charuco_dictionary) + board = make_board( + args.charuco_squares_x, + args.charuco_squares_y, + args.charuco_square_length, + args.charuco_marker_length, + dictionary, + ) + board_pts = board_object_points(board) + params = detector_params() + + views = [] + report = None + evaluation = None + last_det = None + + window = "Intrinsics Calibration - Production" + cv2.namedWindow(window, cv2.WINDOW_NORMAL) + + try: + rows, actual_mx, usb = discover(args.mx_id) + specs = validate_specs(rows) + + pipeline, isp = build_pipeline( + specs, + args.fps, + args.isp_sharpness, + args.isp_luma_denoise, + args.isp_chroma_denoise, + ) + + print("=" * 82) + print("INTRINSICS CALIBRATION - PRODUCTION") + print(f"MX ID: {actual_mx}") + print(f"USB : {usb}") + + for role in ROLES: + s = specs[role] + print( + f"{s.socket} -> {role.upper():3s} | {s.sensor:8s} | " + f"{s.width}x{s.height} | {s.source}" + ) + + print("=" * 82) + + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + "status": "running", + "promoted": False, + "depthai_version": getattr(dai, "__version__", "unknown"), + "opencv_version": cv2.__version__, + "device_mx_id": actual_mx, + "usb_speed": usb, + "hardware_signature": hardware_signature(specs), + "calibration_space": CALIBRATION_SPACE, + "charuco": { + "dictionary": args.charuco_dictionary, + "squares_x": args.charuco_squares_x, + "squares_y": args.charuco_squares_y, + "square_length_m": args.charuco_square_length, + "marker_length_m": args.charuco_marker_length, + }, + "distortion_model_policy": { + "requested": args.distortion_model, + "auto_rule": ( + "Prefer Brown5; choose Rational8 only if LOO-CV p95 " + "improves materially." + ), + }, + "qa_thresholds": QA, + "isp_measurement_settings": isp, + "traceability": { + "module_id": args.module_id or None, + "operator": args.operator or None, + "lens_by_role": { + "rgb": args.lens_rgb or None, + "re": args.lens_re or None, + "nir": args.lens_nir or None, + }, + }, + "notes": args.notes, + "locked_controls": None, + "views": [], + "evaluation": None, + "module_params_fragment": None, + } + + info = dai.DeviceInfo(actual_mx) if actual_mx else None + dev_ctx = dai.Device(pipeline, info) if info is not None else dai.Device(pipeline) + + with dev_ctx as dev: + runtime = Runtime(dev, specs) + runtime.wait_all() + + # Preflight + print("[PRE-FLIGHT] Mostre o ChArUco nas três câmeras.") + + while True: + runtime.poll() + + trip = {"frames": runtime.frames} + det = detect_triplet(trip, board, dictionary, params) + + panels = [] + + for role in ROLES: + img = runtime.frames[role] + bgr = img if img.ndim == 3 else cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + p = cv2.resize( + bgr, + (args.panel_width, args.panel_height), + interpolation=cv2.INTER_AREA, + ) + + d = det["detections"][role] + overlay(p, [ + f"{role.upper()} | {specs[role].sensor}", + f"markers={d['markers']} corners={d['corners']}", + f"EXP={runtime.ctrl[role].get('exposure_time_us')}us " + f"ISO={runtime.ctrl[role].get('sensitivity_iso')}", + ], x=8, y=18, scale=0.38, step=17) + + panels.append(p) + + data = np.zeros( + (args.panel_height, args.panel_width, 3), + dtype=np.uint8, + ) + + overlay(data, [ + "INTRINSICS PRE-FLIGHT", + "", + "GOOD" if det["valid"] else "BAD", + "", + "ENTER = lock EXP/ISO", + "Q/ESC = cancel", + "", + *[f"! {x}" for x in det["reasons"][:8]], + ], x=14, y=28, scale=0.43, step=20) + + board_ui = np.vstack([ + np.hstack([panels[0], panels[1]]), + np.hstack([panels[2], data]), + ]) + + cv2.imshow(window, board_ui) + k = cv2.waitKey(1) & 0xFF + + if k in (ord("q"), ord("Q"), 27): + raise KeyboardInterrupt("Cancelado no preflight.") + + if k in (13, 10): + if not det["valid"]: + print("[BLOCK] ChArUco ainda não GOOD nas 3 câmeras.") + continue + + locked = lock_current_ae(runtime) + report["locked_controls"] = { + r: asdict(locked[r]) for r in ROLES + } + save_json_atomic(report_path, report) + break + + time.sleep(0.002) + + # Capture loop + selected_role = "rgb" + show_undist = False + message = "" + message_t = 0.0 + + print( + f"[CAPTURE] mínimo={QA['min_views']}, ideal={QA['recommended_views']}. " + "Varie posição, distância, yaw, pitch e roll." + ) + + while True: + runtime.poll() + + msg = message if ( + message and time.time() - message_t < 4.0 + ) else "" + + ui = build_ui( + runtime, specs, views, last_det, evaluation, + selected_role, show_undist, args, msg + ) + + cv2.imshow(window, ui) + k = cv2.waitKey(1) & 0xFF + + if k in (ord("q"), ord("Q"), 27): + raise KeyboardInterrupt("Sessão cancelada.") + + if k == ord("1"): + selected_role = "rgb" + elif k == ord("2"): + selected_role = "re" + elif k == ord("3"): + selected_role = "nir" + + elif k in (ord("u"), ord("U")): + if evaluation is None: + message = "Use A primeiro para calibrar." + else: + show_undist = not show_undist + message = f"Undistort preview={show_undist}" + message_t = time.time() + + elif k in (ord("v"), ord("V")): + views.clear() + last_det = None + evaluation = None + report["views"] = [] + report["evaluation"] = None + save_json_atomic(report_path, report) + message = "Views limpas." + message_t = time.time() + + elif k in (ord("s"), ord("S")): + snap = snapshots_dir / f"board_{stamp()}.png" + cv2.imwrite(str(snap), ui) + message = f"Snapshot={snap.name}" + message_t = time.time() + + elif k in (ord("g"), ord("G")): + try: + trip = runtime.fresh_triplet() + det = detect_triplet( + trip, board, dictionary, params + ) + last_det = det + + if not det["valid"]: + message = "REJEITADO: " + " | ".join(det["reasons"]) + message_t = time.time() + continue + + pose = { + role: pose_signature( + det["detections"][role]["points"], + trip["frames"][role].shape[:2], + ) + for role in ROLES + } + + if duplicate_pose(pose, views): + message = ( + "REJEITADO: pose repetida. " + "Mude posição/distância/inclinação." + ) + message_t = time.time() + continue + + view_id = len(views) + 1 + + view = { + "view_id": view_id, + "captured_at": now_str(), + "pose": pose, + "controls": trip["controls"], + "detections": { + role: { + "markers": det["detections"][role]["markers"], + "corners": det["detections"][role]["corners"], + "points": { + str(cid): pt + for cid, pt in det["detections"][role]["points"].items() + }, + } + for role in ROLES + }, + } + + preview = views_dir / f"view_{view_id:03d}.png" + save_view_preview( + preview, trip["frames"], det + ) + view["preview_png"] = str(preview) + + views.append(view) + report["views"] = views + save_json_atomic(report_path, report) + + message = ( + f"VIEW #{view_id:03d} ACCEPTED | " + f"RGB={view['detections']['rgb']['corners']} " + f"RE={view['detections']['re']['corners']} " + f"NIR={view['detections']['nir']['corners']}" + ) + message_t = time.time() + print("[VIEW]", message) + + except Exception as exc: + message = f"Captura rejeitada: {exc}" + message_t = time.time() + + elif k in (ord("a"), ord("A")): + if len(views) < QA["min_views"]: + message = ( + f"Precisa >= {QA['min_views']} views. Atual={len(views)}" + ) + message_t = time.time() + continue + + evaluation = evaluate_all( + views, board_pts, specs, args.distortion_model + ) + + report["evaluation"] = evaluation + report["module_params_fragment"] = module_fragment( + evaluation, specs + ) + save_json_atomic(report_path, report) + + if evaluation["status"] == "bad": + message = ( + "EVAL BAD: " + + " | ".join(evaluation["reasons"][:5]) + ) + message_t = time.time() + print("[BAD]", *evaluation["reasons"], sep="\n ") + continue + + if ( + evaluation["status"] == "warning" + and not args.promote_warning + ): + message = ( + "EVAL WARNING. Capture mais/melhores views." + ) + message_t = time.time() + print("[WARNING]", *evaluation["reasons"], sep="\n ") + continue + + report["status"] = ( + "pass" + if evaluation["status"] == "good" + else "warning_promoted" + ) + report["finished_at"] = now_str() + report["promoted"] = True + report["promoted_at"] = now_str() + + active = Path(args.active_json) + + if active.exists(): + backup = active.with_name( + active.stem + ".backup_" + stamp() + active.suffix + ) + shutil.copy2(active, backup) + + save_json_atomic(active, report) + save_json_atomic(report_path, report) + + marker = candidate_dir / ( + "PASS.txt" + if evaluation["status"] == "good" + else "PASS_WARNING.txt" + ) + marker.write_text( + f"status={report['status']}\nactive_json={active}\n", + encoding="utf-8", + ) + + print("=" * 82) + print("[PASS] INTRINSICS PROMOTED") + + for role in ROLES: + c = evaluation["cameras"][role] + m = c["selected_model"] + print( + f"{role.upper()}: " + f"{c['model_selection']['selected']} | " + f"RMS={m['rms_px']:.3f}px | " + f"CVmed={m['cross_validation']['overall']['median_px']:.3f}px | " + f"edgeP95={m['distortion_shift']['edge_p95_px']:.1f}px | " + f"{c['runtime_geometry_recommendation']}" + ) + + print("[RUNTIME] runtime_undistort permanece false.") + print( + "[IMPORTANTE] Se ativarmos undistort, recalibrar Homography " + "no espaço undistorted." + ) + print(f"[ACTIVE] {active}") + print("=" * 82) + + final = np.zeros((720, 1280, 3), dtype=np.uint8) + lines = [ + "INTRINSICS CALIBRATION - PASS", + "", + ] + + for role in ROLES: + c = evaluation["cameras"][role] + m = c["selected_model"] + lines.append( + f"{role.upper()}: {c['model_selection']['selected']} | " + f"RMS={m['rms_px']:.3f}px | " + f"CV={m['cross_validation']['overall']['median_px']:.3f}px | " + f"edge={m['distortion_shift']['edge_p95_px']:.1f}px" + ) + + lines += [ + "", + "runtime_undistort = DISABLED", + f"Active: {active}", + "", + "Qualquer tecla fecha.", + ] + + overlay(final, lines, x=42, y=62, scale=0.64, step=31) + cv2.imshow(window, final) + cv2.waitKey(0) + break + + time.sleep(0.001) + + except KeyboardInterrupt as exc: + if report is None: + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + } + + report["status"] = "cancelled" + report["promoted"] = False + report["finished_at"] = now_str() + report["error"] = str(exc) + report["views"] = views + + try: + save_json_atomic(report_path, report) + (candidate_dir / "CANCELLED.txt").write_text( + f"reason={exc}\n", encoding="utf-8" + ) + except Exception: + pass + + print("[CANCELLED]", exc) + print("[SAFE] Intrinsics ativo anterior preservado.") + + except Exception as exc: + if report is None: + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + } + + report["status"] = "error" + report["promoted"] = False + report["finished_at"] = now_str() + report["error"] = f"{type(exc).__name__}: {exc}" + report["views"] = views + + try: + save_json_atomic(report_path, report) + (candidate_dir / "FAIL.txt").write_text( + f"error={type(exc).__name__}: {exc}\n", + encoding="utf-8", + ) + except Exception: + pass + + print("[ERROR]", type(exc).__name__, exc) + print("[SAFE] Intrinsics ativo anterior preservado.") + raise + + finally: + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/_3_flatfield_calibration_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/_3_flatfield_calibration_tool.py new file mode 100644 index 000000000..680844c1e --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/utils/_3_flatfield_calibration_tool.py @@ -0,0 +1,3089 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +flatfield_calibration_production.py +=================================== + +Calibrador de flat-field / dark-frame de PRODUÇÃO para o módulo OAK-FFC-3P. + +Setup suportado: + CAM_A = RGB = OV9782 (1280x800) OU AR0234 (1920x1200) + CAM_B = RE = OV9282 (1280x800) + CAM_C = NIR = OV9282 (1280x800) + +Princípios de produto: +---------------------- +1. Descobre e valida o hardware antes de iniciar. +2. Configura resolução por SENSOR, não por resolução global. +3. Captura RAW10 nativo de cada sensor. +4. Usa o mesmo RawProcessorCore da aplicação para obter R/G/B/RE/NIR. +5. Deixa AE estabilizar no WHITE e então CONGELA exposição/ISO. +6. WHITE, DARK e WHITE-VALIDATION usam exatamente os mesmos controles. +7. Não redimensiona dark para "caber": shape divergente é erro fatal. +8. Rejeita frames temporalmente anômalos. +9. Gera o gain a partir do campo suavizado antes da inversão. +10. Valida o flat numa segunda captura WHITE que não participou do cálculo. +11. Sempre salva um candidate auditável. +12. Só promove para "active" se o acceptance test passar. +13. Preserva calibração ativa anterior em caso de falha. +14. Gera metadados, hashes, previews e sugestão de flatfield_config. + +Dependências: +------------- + pip install depthai opencv-python numpy + + Projeto: + core/raw_processor_core.py + +Fluxo: +------ + A) WHITE PREFLIGHT + - alvo branco/cinza fosco uniforme + - AE estabiliza + - ENTER congela exposição/ISO de cada câmera + + B) WHITE A + - captura usada para construir o flat + + C) DARK + - lentes totalmente tampadas + - MESMOS controles do WHITE A + + D) WHITE B / VALIDATION + - destampar e voltar ao mesmo alvo + - MESMOS controles + - esta captura NÃO participa da construção + - mede ganho real de uniformidade antes/depois + +Saídas: +------- + calibration/flatfield_candidates// + flatfield_maps.npz + report.json + previews/ + PASS.txt ou FAIL.txt + + Se PASS: + calibration/flatfield_maps_v1.npz + calibration/flatfield_maps_v1.json + +Observação importante sobre Bayer: +---------------------------------- + --rgb-bayer auto usa: + OV9782 -> BGGR (configuração histórica do módulo deste projeto) + AR0234 -> GRBG (CFA nativo usual do AR0234) + + Para produto, o Bayer resolvido fica registrado no report.json. + Se o seu módulo AR0234 específico estiver montado com orientação/mirror + diferente, use --rgb-bayer RGGB|BGGR|GRBG|GBRG explicitamente. + +Exemplo: +-------- + python flatfield_calibration_production.py + + python flatfield_calibration_production.py ^ + --module-params calibration/module_params.json ^ + --frames 60 ^ + --validation-frames 30 + +Teclas: +------- + ENTER = avançar/iniciar etapa + Q/ESC = cancelar sem promover + +IMPORTANTE: +----------- +Este script é fail-closed. Um candidate pode ser salvo mesmo quando falha, +mas uma calibração FAIL não sobrescreve o arquivo ativo. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import shutil +import sys +import time +from collections import deque +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional, Tuple, List, Any + +import cv2 +import depthai as dai +import numpy as np + +from core.raw_processor_core import RawProcessorCore + + +# ============================================================ +# Constantes de produto +# ============================================================ + +ROLES = ("rgb", "re", "nir") +CHANNELS = ("R", "G", "B", "RE", "NIR") + +ROLE_TO_SOCKET = { + "rgb": "CAM_A", + "re": "CAM_B", + "nir": "CAM_C", +} + +EXPECTED_MONO_SENSOR = "OV9282" + +SUPPORTED_RGB = { + "OV9782": { + "width": 1280, + "height": 800, + "resolution_enum": "THE_800_P", + "default_bayer": "BGGR", + }, + "AR0234": { + "width": 1920, + "height": 1200, + "resolution_enum": "THE_1200_P", + "default_bayer": "GRBG", + }, +} + +SUPPORTED_MONO = { + "OV9282": { + "width": 1280, + "height": 800, + "resolution_enum": "THE_800_P", + }, +} + +BAYER_PATTERNS = ("RGGB", "BGGR", "GRBG", "GBRG") + +# Raw10 +RAW10_MAX = 1023.0 +RAW10_WHITE_SAT = 1018 +RAW10_DARK_FLOOR = 16 + +SCHEMA = "multispec_flatfield_production_v2" + + +# ============================================================ +# Thresholds de QA +# ============================================================ + +DEFAULT_THRESHOLDS = { + # WHITE ao vivo / raw por role + "white_sat_warning_pct": 0.10, + "white_sat_bad_pct": 0.50, + "white_dark_warning_pct": 1.00, + "white_dark_bad_pct": 4.00, + + # Estabilidade temporal da mediana global por role + "temporal_cv_warning": 0.005, # 0.5% + "temporal_cv_bad": 0.015, # 1.5% + + # Rejeição de frame individual em relação à mediana temporal + "frame_global_deviation_reject": 0.05, # 5% + + # DARK: vazamento de luz / tampa inadequada + "dark_p99_warning": 0.08, + "dark_p99_bad": 0.16, + + # Gain + "gain_p99_warning": 1.75, + "gain_p99_bad": 2.20, + "gain_max_warning": 2.20, + "gain_max_bad": 3.00, + "gain_min_warning": 0.55, + "gain_min_bad": 0.35, + + # Defeitos locais no sinal branco após remover shading suave + "local_dev_p99_warning": 0.12, + "local_dev_p99_bad": 0.20, + "local_outlier_pct_warning": 1.0, + "local_outlier_pct_bad": 3.0, + + # Acceptance WHITE B após flat + "corrected_grid_cv_warning": 0.055, + "corrected_grid_cv_bad": 0.085, + + # Só exige melhora relativa quando havia shading mensurável antes. + "min_before_grid_cv_for_improvement": 0.025, + "improvement_warning": 0.25, + "improvement_bad": 0.10, + + # Se o flat piorar a uniformidade de forma clara, falha. + "worsening_bad": -0.05, + + # Controle travado + "exposure_rel_tolerance": 0.01, + "exposure_abs_tolerance_us": 20, + "iso_tolerance": 5, +} + + +# ============================================================ +# Utilidades +# ============================================================ + +def now_str() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def session_id() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S") + + +def ensure_dir(path: str | Path): + Path(path).mkdir(parents=True, exist_ok=True) + + +def load_json(path: Optional[str]) -> dict: + if not path: + return {} + p = Path(path) + if not p.is_file(): + return {} + with p.open("r", encoding="utf-8") as f: + return json.load(f) + + +def save_json(path: str | Path, data: dict): + p = Path(path) + ensure_dir(p.parent) + tmp = p.with_suffix(p.suffix + ".tmp") + with tmp.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, p) + + +def sha256_file(path: str | Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + while True: + chunk = f.read(1024 * 1024) + if not chunk: + break + h.update(chunk) + return h.hexdigest() + + +def safe_float(v, default=None): + try: + if v is None: + return default + return float(v) + except Exception: + return default + + +def safe_int(v, default=None): + try: + if v is None: + return default + return int(v) + except Exception: + return default + + +def clamp(v, lo, hi): + return max(lo, min(hi, v)) + + +def finite_percentile(arr: np.ndarray, pct: float, default=0.0) -> float: + f = arr[np.isfinite(arr)] + if f.size == 0: + return float(default) + return float(np.percentile(f, pct)) + + +def robust_median(arr: np.ndarray, margin_frac=0.04) -> float: + a = arr.astype(np.float32) + h, w = a.shape[:2] + mx = int(w * margin_frac) + my = int(h * margin_frac) + if w - 2 * mx > 8 and h - 2 * my > 8: + a = a[my:h-my, mx:w-mx] + f = a[np.isfinite(a)] + if f.size == 0: + return 0.0 + return float(np.median(f)) + + +def robust_center_reference(arr: np.ndarray, center_frac=0.44) -> float: + """ + Referência do flat no centro óptico, robusta contra pixels ruins. + Mantém ganho ~1 no centro. + """ + a = arr.astype(np.float32) + h, w = a.shape[:2] + + cw = max(16, int(w * center_frac)) + ch = max(16, int(h * center_frac)) + + x0 = (w - cw) // 2 + y0 = (h - ch) // 2 + + core = a[y0:y0+ch, x0:x0+cw] + f = core[np.isfinite(core)] + + if f.size == 0: + f = a[np.isfinite(a)] + + if f.size == 0: + return 1.0 + + lo = np.percentile(f, 10) + hi = np.percentile(f, 90) + trimmed = f[(f >= lo) & (f <= hi)] + if trimmed.size: + f = trimmed + + v = float(np.median(f)) + return v if np.isfinite(v) and v > 1e-8 else 1.0 + + +def normalize_display(arr: np.ndarray, low=1.0, high=99.0) -> np.ndarray: + a = arr.astype(np.float32) + f = a[np.isfinite(a)] + if f.size == 0: + return np.zeros(a.shape[:2], dtype=np.uint8) + + lo = float(np.percentile(f, low)) + hi = float(np.percentile(f, high)) + + if hi <= lo: + hi = lo + 1e-6 + + out = (a - lo) / (hi - lo) + return np.clip(out * 255.0, 0, 255).astype(np.uint8) + + +def raw10_display(raw16: np.ndarray) -> np.ndarray: + u8 = np.clip(raw16.astype(np.float32) * (255.0 / RAW10_MAX), 0, 255).astype(np.uint8) + return u8 + + +def overlay_hud( + img, + lines, + x=12, + y=24, + font_scale=0.52, + line_step=21, + color=(255, 255, 255), +): + yy = int(y) + h = img.shape[0] + + for line in lines: + if yy > h - 6: + break + + s = str(line) + cv2.putText( + img, s, (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, (0, 0, 0), 3, cv2.LINE_AA, + ) + cv2.putText( + img, s, (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, color, 1, cv2.LINE_AA, + ) + yy += int(line_step) + + +def resize_panel(img: np.ndarray, width: int, height: int) -> np.ndarray: + return cv2.resize(img, (int(width), int(height)), interpolation=cv2.INTER_AREA) + + +def status_rank(status: str) -> int: + return {"good": 0, "warning": 1, "bad": 2}.get(str(status).lower(), 2) + + +def merge_status(a: str, b: str) -> str: + return a if status_rank(a) >= status_rank(b) else b + + +def get_timestamp_seconds(pkt) -> float: + for name in ("getTimestampDevice", "getTimestamp"): + fn = getattr(pkt, name, None) + if callable(fn): + try: + ts = fn() + if hasattr(ts, "total_seconds"): + return float(ts.total_seconds()) + return float(ts) + except Exception: + pass + return time.monotonic() + + +def packet_controls(pkt) -> dict: + exp_us = None + iso = None + color_temp = None + seq = None + + try: + exp = pkt.getExposureTime() + if hasattr(exp, "total_seconds"): + exp_us = int(round(exp.total_seconds() * 1_000_000)) + else: + exp_us = int(exp) + except Exception: + pass + + try: + iso = int(pkt.getSensitivity()) + except Exception: + pass + + try: + color_temp = int(pkt.getColorTemperature()) + except Exception: + pass + + try: + seq = int(pkt.getSequenceNum()) + except Exception: + pass + + return { + "exposure_time_us": exp_us, + "sensitivity_iso": iso, + "color_temperature_k": color_temp, + "sequence_num": seq, + "timestamp_s": get_timestamp_seconds(pkt), + } + + +# ============================================================ +# RAW10 pack/unpack +# ============================================================ + +def unpack_raw10(data, width: int, height: int, stride: Optional[int] = None) -> np.ndarray: + """ + Unpack RAW10 MIPI: 4 pixels / 5 bytes. + Retorna uint16 0..1023. + Vetorizado e com suporte a stride. + """ + width = int(width) + height = int(height) + + if width <= 0 or height <= 0: + raise ValueError(f"Dimensão RAW inválida: {width}x{height}") + + if width % 4 != 0: + raise ValueError(f"RAW10 width precisa ser múltiplo de 4: {width}") + + arr = np.asarray(data, dtype=np.uint8).reshape(-1) + + min_row_bytes = (width // 4) * 5 + + if stride is None or int(stride) <= 0: + stride = min_row_bytes + + stride = int(stride) + + expected = stride * height + if arr.size < expected: + raise ValueError( + f"RAW10 curto: bytes={arr.size}, esperado>={expected} " + f"({width}x{height}, stride={stride})" + ) + + rows = arr[:expected].reshape(height, stride) + payload = rows[:, :min_row_bytes] + + groups = payload.reshape(height, width // 4, 5) + + out = np.empty((height, width // 4, 4), dtype=np.uint16) + + out[:, :, 0] = (groups[:, :, 0].astype(np.uint16) << 2) | (groups[:, :, 4] & 0x03) + out[:, :, 1] = (groups[:, :, 1].astype(np.uint16) << 2) | ((groups[:, :, 4] >> 2) & 0x03) + out[:, :, 2] = (groups[:, :, 2].astype(np.uint16) << 2) | ((groups[:, :, 4] >> 4) & 0x03) + out[:, :, 3] = (groups[:, :, 3].astype(np.uint16) << 2) | ((groups[:, :, 4] >> 6) & 0x03) + + return np.ascontiguousarray(out.reshape(height, width)) + + +def pack_raw10(raw16: np.ndarray) -> np.ndarray: + """ + Reempacota uint16 0..1023 no formato esperado pelo RawProcessorCore: + shape=(H, W*5/4), uint8. + """ + raw = np.clip(np.rint(raw16), 0, 1023).astype(np.uint16) + + h, w = raw.shape[:2] + if w % 4 != 0: + raise ValueError(f"RAW10 pack requer width múltiplo de 4: {w}") + + px = raw.reshape(h, w // 4, 4) + + packed = np.empty((h, w // 4, 5), dtype=np.uint8) + + packed[:, :, 0] = (px[:, :, 0] >> 2).astype(np.uint8) + packed[:, :, 1] = (px[:, :, 1] >> 2).astype(np.uint8) + packed[:, :, 2] = (px[:, :, 2] >> 2).astype(np.uint8) + packed[:, :, 3] = (px[:, :, 3] >> 2).astype(np.uint8) + + low = ( + (px[:, :, 0] & 0x03) + | ((px[:, :, 1] & 0x03) << 2) + | ((px[:, :, 2] & 0x03) << 4) + | ((px[:, :, 3] & 0x03) << 6) + ) + + packed[:, :, 4] = low.astype(np.uint8) + + return np.ascontiguousarray(packed.reshape(h, (w // 4) * 5)) + + +# ============================================================ +# Hardware discovery +# ============================================================ + +@dataclass +class CameraSpec: + role: str + socket_name: str + sensor_name: str + width: int + height: int + resolution_enum: str + stream_name: str + control_name: str + is_color: bool + bayer_pattern: Optional[str] = None + + +def device_id_from_info(dev_info) -> Optional[str]: + for name in ("getMxId", "getDeviceId"): + try: + fn = getattr(dev_info, name, None) + if callable(fn): + v = fn() + if v: + return str(v) + except Exception: + pass + + for name in ("mxid", "deviceId"): + try: + v = getattr(dev_info, name, None) + if v: + return str(v) + except Exception: + pass + + return None + + +def select_device_info(mx_id: Optional[str]): + devices = dai.Device.getAllAvailableDevices() + + if not devices: + raise RuntimeError("Nenhum dispositivo OAK/DepthAI encontrado.") + + if not mx_id: + return devices[0] + + target = str(mx_id).strip() + + for info in devices: + if device_id_from_info(info) == target: + return info + + available = [device_id_from_info(x) for x in devices] + raise RuntimeError(f"MX ID {target!r} não encontrado. Disponíveis: {available}") + + +def discover_features(dev_info): + with dai.Device(dev_info) as dev: + rows = [] + + for f in dev.getConnectedCameraFeatures(): + rows.append({ + "socket_name": str(f.socket.name), + "sensor_name": str(f.sensorName or ""), + "width": int(getattr(f, "width", 0) or 0), + "height": int(getattr(f, "height", 0) or 0), + "supported_types": [str(x) for x in (getattr(f, "supportedTypes", []) or [])], + "has_autofocus_ic": int(getattr(f, "hasAutofocusIC", 0) or 0), + }) + + usb = None + try: + usb = str(dev.getUsbSpeed()) + except Exception: + pass + + return rows, usb + + +def get_socket(name: str): + name = str(name).upper() + + mapping = { + "CAM_A": dai.CameraBoardSocket.CAM_A, + "CAM_B": dai.CameraBoardSocket.CAM_B, + "CAM_C": dai.CameraBoardSocket.CAM_C, + } + + if name not in mapping: + raise ValueError(f"Socket não suportado: {name}") + + return mapping[name] + + +def resolve_bayer(sensor_name: str, arg_value: str, module_params: dict) -> str: + arg = str(arg_value or "auto").upper() + + if arg != "AUTO": + if arg not in BAYER_PATTERNS: + raise ValueError(f"Bayer inválido: {arg}") + return arg + + # 1) Procura configuração explícita no module_params. + candidates = [ + module_params.get("bayer_pattern"), + module_params.get("bayer"), + ] + + cam_a_cfg = ( + module_params.get("camera_info", {}).get("CAM_A", {}) + if isinstance(module_params.get("camera_info"), dict) + else {} + ) + + if isinstance(cam_a_cfg, dict): + candidates.extend([ + cam_a_cfg.get("bayer_pattern"), + cam_a_cfg.get("bayer"), + ]) + + for c in candidates: + if c and str(c).upper() in BAYER_PATTERNS: + return str(c).upper() + + # 2) Fallback sensor-aware. + sensor = str(sensor_name).upper() + if sensor in SUPPORTED_RGB: + return SUPPORTED_RGB[sensor]["default_bayer"] + + raise RuntimeError( + f"Não foi possível resolver Bayer automaticamente para {sensor_name}. " + f"Use --rgb-bayer explicitamente." + ) + + +def validate_and_build_specs( + features: list[dict], + rgb_bayer_arg: str, + module_params: dict, +) -> Dict[str, CameraSpec]: + by_socket = {x["socket_name"]: x for x in features} + + missing = [sock for sock in ("CAM_A", "CAM_B", "CAM_C") if sock not in by_socket] + if missing: + raise RuntimeError(f"Módulo incompleto. Sockets ausentes: {missing}") + + rgb_row = by_socket["CAM_A"] + re_row = by_socket["CAM_B"] + nir_row = by_socket["CAM_C"] + + rgb_sensor = rgb_row["sensor_name"].upper() + + if rgb_sensor not in SUPPORTED_RGB: + raise RuntimeError( + f"CAM_A deve ser OV9782 ou AR0234. Detectado: {rgb_row['sensor_name']}" + ) + + for role, row in (("RE", re_row), ("NIR", nir_row)): + if row["sensor_name"].upper() != EXPECTED_MONO_SENSOR: + raise RuntimeError( + f"{role} deve usar OV9282. Detectado em " + f"{'CAM_B' if role == 'RE' else 'CAM_C'}: {row['sensor_name']}" + ) + + rgb_cfg = SUPPORTED_RGB[rgb_sensor] + bayer = resolve_bayer(rgb_sensor, rgb_bayer_arg, module_params) + + specs = { + "rgb": CameraSpec( + role="rgb", + socket_name="CAM_A", + sensor_name=rgb_sensor, + width=rgb_cfg["width"], + height=rgb_cfg["height"], + resolution_enum=rgb_cfg["resolution_enum"], + stream_name="raw_rgb", + control_name="ctrl_rgb", + is_color=True, + bayer_pattern=bayer, + ), + "re": CameraSpec( + role="re", + socket_name="CAM_B", + sensor_name="OV9282", + width=1280, + height=800, + resolution_enum="THE_800_P", + stream_name="raw_re", + control_name="ctrl_re", + is_color=False, + ), + "nir": CameraSpec( + role="nir", + socket_name="CAM_C", + sensor_name="OV9282", + width=1280, + height=800, + resolution_enum="THE_800_P", + stream_name="raw_nir", + control_name="ctrl_nir", + is_color=False, + ), + } + + # Valida o que o device anunciou, sem aceitar silent mismatch. + for role, spec in specs.items(): + row = by_socket[spec.socket_name] + + if row["width"] and row["height"]: + if (row["width"], row["height"]) != (spec.width, spec.height): + # Alguns firmwares podem anunciar dimensão diferente da mode table. + # Produto: não adivinhamos. + raise RuntimeError( + f"{spec.socket_name}/{spec.sensor_name}: dimensão anunciada " + f"{row['width']}x{row['height']} diverge da esperada " + f"{spec.width}x{spec.height}." + ) + + return specs + + +# ============================================================ +# Pipeline DepthAI standalone +# ============================================================ + +def resolve_antibanding(mode: str): + mode = str(mode or "OFF").upper() + + enum_cls = dai.CameraControl.AntiBandingMode + + names = { + "OFF": "OFF", + "50": "MAINS_50_HZ", + "50HZ": "MAINS_50_HZ", + "60": "MAINS_60_HZ", + "60HZ": "MAINS_60_HZ", + } + + enum_name = names.get(mode) + if not enum_name: + raise ValueError(f"anti-banding inválido: {mode}") + + return getattr(enum_cls, enum_name) + + +def build_pipeline(specs: Dict[str, CameraSpec], fps: float, anti_banding: str): + pipeline = dai.Pipeline() + + anti_enum = resolve_antibanding(anti_banding) + + for role in ROLES: + spec = specs[role] + socket = get_socket(spec.socket_name) + + if spec.is_color: + cam = pipeline.createColorCamera() + cam.setBoardSocket(socket) + + enum_value = getattr( + dai.ColorCameraProperties.SensorResolution, + spec.resolution_enum, + None, + ) + + if enum_value is None: + raise RuntimeError( + f"DepthAI instalado não expõe ColorCameraProperties." + f"SensorResolution.{spec.resolution_enum}." + ) + + cam.setResolution(enum_value) + cam.setFps(float(fps)) + cam.setInterleaved(False) + + else: + cam = pipeline.create(dai.node.MonoCamera) + cam.setBoardSocket(socket) + + enum_value = getattr( + dai.MonoCameraProperties.SensorResolution, + spec.resolution_enum, + None, + ) + + if enum_value is None: + raise RuntimeError( + f"DepthAI instalado não expõe MonoCameraProperties." + f"SensorResolution.{spec.resolution_enum}." + ) + + cam.setResolution(enum_value) + cam.setFps(float(fps)) + + # AE inicia ativo para o preflight WHITE. + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + + try: + cam.initialControl.setAntiBandingMode(anti_enum) + except Exception: + pass + + if spec.is_color: + try: + cam.initialControl.setAutoWhiteBalanceLock(False) + except Exception: + pass + + # RAW obrigatório. + if not hasattr(cam, "raw"): + raise RuntimeError( + f"{spec.socket_name}/{spec.sensor_name} não expõe saída raw " + "nesta versão do DepthAI." + ) + + xout = pipeline.createXLinkOut() + xout.setStreamName(spec.stream_name) + cam.raw.link(xout.input) + + xin = pipeline.createXLinkIn() + xin.setStreamName(spec.control_name) + + if not hasattr(cam, "inputControl"): + raise RuntimeError( + f"{spec.socket_name}/{spec.sensor_name} sem inputControl." + ) + + xin.out.link(cam.inputControl) + + return pipeline + + +# ============================================================ +# Controle manual de exposição +# ============================================================ + +@dataclass +class LockedControl: + role: str + exposure_time_us: int + sensitivity_iso: int + source: str + + +def send_manual_control(queue, control: LockedControl, lock_awb=False): + ctrl = dai.CameraControl() + ctrl.setManualExposure( + int(control.exposure_time_us), + int(control.sensitivity_iso), + ) + + if lock_awb: + try: + ctrl.setAutoWhiteBalanceLock(True) + except Exception: + pass + + queue.send(ctrl) + + +def controls_match(actual: dict, target: LockedControl, th: dict) -> bool: + exp = safe_int(actual.get("exposure_time_us"), None) + iso = safe_int(actual.get("sensitivity_iso"), None) + + if exp is None or iso is None: + return False + + exp_tol = max( + int(th["exposure_abs_tolerance_us"]), + int(round(target.exposure_time_us * th["exposure_rel_tolerance"])), + ) + + if abs(exp - target.exposure_time_us) > exp_tol: + return False + + if abs(iso - target.sensitivity_iso) > int(th["iso_tolerance"]): + return False + + return True + + +# ============================================================ +# Estatísticas / QA +# ============================================================ + +def raw_stats(raw16: np.ndarray) -> dict: + a = raw16.astype(np.float32) / RAW10_MAX + + return { + "mean": float(np.mean(a)), + "std": float(np.std(a)), + "p01": float(np.percentile(a, 1)), + "p05": float(np.percentile(a, 5)), + "p50": float(np.percentile(a, 50)), + "p95": float(np.percentile(a, 95)), + "p99": float(np.percentile(a, 99)), + "sat_pct": float((raw16 >= RAW10_WHITE_SAT).mean() * 100.0), + "dark_pct": float((raw16 <= RAW10_DARK_FLOOR).mean() * 100.0), + } + + +def temporal_cv(values: List[float]) -> float: + if len(values) < 3: + return float("inf") + + arr = np.array(values, dtype=np.float64) + mean = float(np.mean(arr)) + + if abs(mean) <= 1e-12: + return float("inf") + + return float(np.std(arr) / abs(mean)) + + +def local_defect_stats( + img01: np.ndarray, + blur_frac: float = 0.18, + margin_frac: float = 0.04, + outlier_thr: float = 0.10, +) -> dict: + a = img01.astype(np.float32) + + h, w = a.shape[:2] + mx = int(w * margin_frac) + my = int(h * margin_frac) + + if w - 2 * mx > 32 and h - 2 * my > 32: + a = a[my:h-my, mx:w-mx] + + h, w = a.shape[:2] + + k = int(round(min(h, w) * blur_frac)) + k = max(31, k) + if k % 2 == 0: + k += 1 + + # Kernel OpenCV não pode exceder a imagem. + k = min(k, min(h, w) - (1 - min(h, w) % 2)) + if k < 3: + return {"valid": False} + + smooth = cv2.GaussianBlur( + a, (k, k), 0, + borderType=cv2.BORDER_REFLECT, + ) + + ratio = a / np.maximum(smooth, 1e-6) + resid = np.abs(ratio - 1.0) + + f = resid[np.isfinite(resid)] + if f.size == 0: + return {"valid": False} + + return { + "valid": True, + "blur_ksize": int(k), + "local_dev_p95": float(np.percentile(f, 95)), + "local_dev_p99": float(np.percentile(f, 99)), + "local_dev_max": float(np.max(f)), + "local_outlier_pct": float((f >= outlier_thr).mean() * 100.0), + "outlier_threshold": float(outlier_thr), + } + + +def grid_uniformity(img01: np.ndarray, grid_size=5, margin_frac=0.05) -> dict: + a = img01.astype(np.float32) + + h, w = a.shape[:2] + mx = int(w * margin_frac) + my = int(h * margin_frac) + + if w - 2 * mx > 16 and h - 2 * my > 16: + a = a[my:h-my, mx:w-mx] + + h, w = a.shape[:2] + + vals = [] + grid = [] + + for gy in range(grid_size): + row = [] + y0 = int(round(gy * h / grid_size)) + y1 = int(round((gy + 1) * h / grid_size)) + + for gx in range(grid_size): + x0 = int(round(gx * w / grid_size)) + x1 = int(round((gx + 1) * w / grid_size)) + + cell = a[y0:y1, x0:x1] + f = cell[np.isfinite(cell)] + + v = float(np.median(f)) if f.size else 0.0 + vals.append(v) + row.append(v) + + grid.append(row) + + v = np.array(vals, dtype=np.float64) + center = float(grid[grid_size // 2][grid_size // 2]) + med = float(np.median(v)) + + den = max(abs(med), 1e-9) + + return { + "grid": grid, + "median": med, + "center": center, + "min": float(np.min(v)), + "max": float(np.max(v)), + "grid_cv": float(np.std(v) / den), + "range_over_median": float((np.max(v) - np.min(v)) / den), + } + + +def evaluate_live_white( + latest_raw: Dict[str, np.ndarray], + temporal_medians: Dict[str, deque], + th: dict, +) -> dict: + report = { + "status": "good", + "roles": {}, + "reasons": [], + } + + for role in ROLES: + raw = latest_raw.get(role) + + if raw is None: + report["status"] = "bad" + report["roles"][role] = { + "status": "bad", + "reasons": ["sem_frame"], + } + continue + + st = raw_stats(raw) + cv = temporal_cv(list(temporal_medians[role])) + + status = "good" + reasons = [] + + if st["sat_pct"] > th["white_sat_bad_pct"]: + status = merge_status(status, "bad") + reasons.append(f"sat_bad:{st['sat_pct']:.3f}%") + elif st["sat_pct"] > th["white_sat_warning_pct"]: + status = merge_status(status, "warning") + reasons.append(f"sat_warn:{st['sat_pct']:.3f}%") + + if st["dark_pct"] > th["white_dark_bad_pct"]: + status = merge_status(status, "bad") + reasons.append(f"dark_bad:{st['dark_pct']:.3f}%") + elif st["dark_pct"] > th["white_dark_warning_pct"]: + status = merge_status(status, "warning") + reasons.append(f"dark_warn:{st['dark_pct']:.3f}%") + + if np.isfinite(cv): + if cv > th["temporal_cv_bad"]: + status = merge_status(status, "bad") + reasons.append(f"temporal_cv_bad:{cv:.4f}") + elif cv > th["temporal_cv_warning"]: + status = merge_status(status, "warning") + reasons.append(f"temporal_cv_warn:{cv:.4f}") + + report["roles"][role] = { + "status": status, + "stats": st, + "temporal_cv": None if not np.isfinite(cv) else cv, + "reasons": reasons, + } + + report["status"] = merge_status(report["status"], status) + report["reasons"].extend([f"{role}:{x}" for x in reasons]) + + return report + + +# ============================================================ +# Acumulador streaming +# ============================================================ + +class MeanAccumulator: + """ + Média por pixel com rejeição de frames baseada na mediana global. + Não guarda N frames full-res em RAM. + """ + + def __init__(self, shape: Tuple[int, int], reject_fraction: float): + self.shape = tuple(shape) + self.reject_fraction = float(reject_fraction) + + self.sum = np.zeros(self.shape, dtype=np.float64) + self.count = 0 + self.rejected = 0 + + self.global_medians = deque(maxlen=25) + self.accepted_medians = [] + self.frame_controls = [] + self.frame_meta = [] + + def try_add(self, raw16: np.ndarray, ctrl: dict, meta: dict) -> bool: + if raw16.shape != self.shape: + raise RuntimeError( + f"Shape mudou durante captura: esperado={self.shape}, recebido={raw16.shape}" + ) + + med = robust_median(raw16) + + if len(self.global_medians) >= 5: + ref = float(np.median(np.array(self.global_medians, dtype=np.float64))) + + if abs(ref) > 1e-9: + dev = abs(med - ref) / abs(ref) + + if dev > self.reject_fraction: + self.rejected += 1 + return False + + self.global_medians.append(med) + self.accepted_medians.append(float(med)) + + self.sum += raw16.astype(np.float64) + self.count += 1 + + self.frame_controls.append(dict(ctrl)) + self.frame_meta.append(dict(meta)) + + return True + + def mean(self) -> np.ndarray: + if self.count <= 0: + raise RuntimeError("Nenhum frame aceito no acumulador.") + return (self.sum / float(self.count)).astype(np.float32) + + def summary(self) -> dict: + vals = np.array(self.accepted_medians, dtype=np.float64) + + return { + "accepted": int(self.count), + "rejected": int(self.rejected), + "global_median_mean": float(np.mean(vals)) if vals.size else None, + "global_median_std": float(np.std(vals)) if vals.size else None, + "global_median_cv": ( + float(np.std(vals) / max(abs(np.mean(vals)), 1e-9)) + if vals.size else None + ), + } + + +# ============================================================ +# Contexto de aquisição +# ============================================================ + +class AcquisitionContext: + def __init__( + self, + device, + specs: Dict[str, CameraSpec], + thresholds: dict, + panel_width: int, + panel_height: int, + rgb_bayer: str, + ): + self.device = device + self.specs = specs + self.thresholds = thresholds + self.panel_width = int(panel_width) + self.panel_height = int(panel_height) + self.rgb_bayer = rgb_bayer + + self.raw_queues = { + role: device.getOutputQueue( + name=spec.stream_name, + maxSize=2, + blocking=False, + ) + for role, spec in specs.items() + } + + self.control_queues = { + role: device.getInputQueue(spec.control_name) + for role, spec in specs.items() + } + + self.latest_raw: Dict[str, Optional[np.ndarray]] = {r: None for r in ROLES} + self.latest_ctrl: Dict[str, dict] = {r: {} for r in ROLES} + self.latest_packet_meta: Dict[str, dict] = {r: {} for r in ROLES} + self.latest_seq: Dict[str, Optional[int]] = {r: None for r in ROLES} + + self.temporal_medians = { + r: deque(maxlen=20) + for r in ROLES + } + + self.last_ui_time = 0.0 + + def decode_packet(self, role: str, pkt): + spec = self.specs[role] + + # Produto: RAW10 obrigatório para estes três sensores. + raw_type = str(pkt.getType()).upper() + + if "RAW10" not in raw_type: + raise RuntimeError( + f"{role.upper()}/{spec.sensor_name}: esperado RAW10, recebido {pkt.getType()}" + ) + + width = int(pkt.getWidth()) + height = int(pkt.getHeight()) + + if (width, height) != (spec.width, spec.height): + raise RuntimeError( + f"{role.upper()}/{spec.sensor_name}: frame {width}x{height}, " + f"esperado {spec.width}x{spec.height}" + ) + + stride = None + try: + stride = int(pkt.getStride()) + except Exception: + pass + + raw16 = unpack_raw10( + pkt.getData(), + width=width, + height=height, + stride=stride, + ) + + ctrl = packet_controls(pkt) + + seq = ctrl.get("sequence_num") + meta = { + "role": role, + "socket": spec.socket_name, + "sensor": spec.sensor_name, + "width": width, + "height": height, + "stride": stride, + "raw_type": raw_type, + "sequence_num": seq, + "timestamp_s": ctrl.get("timestamp_s"), + } + + return raw16, ctrl, meta + + def poll(self): + updated = [] + + for role in ROLES: + pkt = self.raw_queues[role].tryGet() + if pkt is None: + continue + + raw16, ctrl, meta = self.decode_packet(role, pkt) + + seq = ctrl.get("sequence_num") + + if seq is not None and seq == self.latest_seq.get(role): + continue + + self.latest_seq[role] = seq + self.latest_raw[role] = raw16 + self.latest_ctrl[role] = ctrl + self.latest_packet_meta[role] = meta + self.temporal_medians[role].append(robust_median(raw16)) + + updated.append(role) + + return updated + + def wait_until_all_have_frames(self, timeout=8.0): + t0 = time.time() + + while time.time() - t0 < timeout: + self.poll() + + if all(self.latest_raw[r] is not None for r in ROLES): + return + + time.sleep(0.003) + + missing = [r for r in ROLES if self.latest_raw[r] is None] + raise TimeoutError(f"Timeout aguardando RAW das roles: {missing}") + + def send_locked_controls(self, locked: Dict[str, LockedControl]): + for role in ROLES: + send_manual_control( + self.control_queues[role], + locked[role], + lock_awb=(role == "rgb"), + ) + + def verify_locked_controls( + self, + locked: Dict[str, LockedControl], + settle_frames=10, + timeout=10.0, + ): + matched = {r: 0 for r in ROLES} + last_seq = dict(self.latest_seq) + + t0 = time.time() + + while time.time() - t0 < timeout: + self.poll() + + for role in ROLES: + seq = self.latest_seq.get(role) + if seq is None or seq == last_seq.get(role): + continue + + last_seq[role] = seq + + if controls_match( + self.latest_ctrl[role], + locked[role], + self.thresholds, + ): + matched[role] += 1 + else: + matched[role] = 0 + + self.show_board( + title="TRAVANDO CONTROLES", + lines=[ + "Verificando exposição/ISO manual...", + *[ + f"{r.upper()}: {matched[r]}/{settle_frames} " + f"target={locked[r].exposure_time_us}us ISO{locked[r].sensitivity_iso} " + f"actual={self.latest_ctrl[r].get('exposure_time_us')}us " + f"ISO{self.latest_ctrl[r].get('sensitivity_iso')}" + for r in ROLES + ], + "", + "Não altere iluminação nem cubra as lentes ainda.", + ], + ) + + if all(matched[r] >= settle_frames for r in ROLES): + return + + k = cv2.waitKey(1) & 0xFF + if k in (ord("q"), ord("Q"), 27): + raise KeyboardInterrupt("Cancelado durante lock de controles.") + + time.sleep(0.001) + + raise RuntimeError( + "Não foi possível confirmar controles manuais estáveis. " + f"matched={matched}" + ) + + def _rgb_raw_preview(self, raw16: np.ndarray) -> np.ndarray: + # Preview somente. A calibração usa RawProcessorCore. + code_map = { + "RGGB": cv2.COLOR_BayerRG2BGR, + "BGGR": cv2.COLOR_BayerBG2BGR, + "GRBG": cv2.COLOR_BayerGR2BGR, + "GBRG": cv2.COLOR_BayerGB2BGR, + } + + u8 = raw10_display(raw16) + + try: + bgr = cv2.cvtColor(u8, code_map[self.rgb_bayer]) + except Exception: + bgr = cv2.cvtColor(u8, cv2.COLOR_GRAY2BGR) + + return bgr + + def show_board( + self, + title: str, + lines: List[str], + progress: Optional[Dict[str, Tuple[int, int]]] = None, + quality: Optional[dict] = None, + ): + pw = self.panel_width + ph = self.panel_height + + panels = {} + + for role in ROLES: + raw = self.latest_raw.get(role) + spec = self.specs[role] + + if raw is None: + panel = np.zeros((ph, pw, 3), dtype=np.uint8) + overlay_hud(panel, [role.upper(), "aguardando frame"]) + panels[role] = panel + continue + + if role == "rgb": + view = self._rgb_raw_preview(raw) + else: + u8 = raw10_display(raw) + view = cv2.cvtColor(u8, cv2.COLOR_GRAY2BGR) + + panel = resize_panel(view, pw, ph) + + st = raw_stats(raw) + ctrl = self.latest_ctrl.get(role, {}) + + overlay_hud( + panel, + [ + f"{role.upper()} | {spec.socket_name} | {spec.sensor_name}", + f"RAW10 {spec.width}x{spec.height}", + f"p50={st['p50']:.3f} p99={st['p99']:.3f}", + f"sat={st['sat_pct']:.3f}% dark={st['dark_pct']:.3f}%", + f"EXP={ctrl.get('exposure_time_us')}us ISO={ctrl.get('sensitivity_iso')}", + ], + x=10, + y=22, + font_scale=0.46, + line_step=19, + ) + + panels[role] = panel + + data = np.zeros((ph, pw, 3), dtype=np.uint8) + + data_lines = [title, ""] + list(lines) + + if quality: + data_lines.append("") + data_lines.append(f"PREFLIGHT: {quality.get('status', 'unknown').upper()}") + + for role in ROLES: + item = quality.get("roles", {}).get(role, {}) + st = item.get("status", "unknown").upper() + cv = item.get("temporal_cv") + cv_txt = "n/a" if cv is None else f"{cv*100:.2f}%" + data_lines.append( + f"{role.upper()}: {st} temporal_cv={cv_txt}" + ) + + for reason in item.get("reasons", [])[:2]: + data_lines.append(f" ! {reason}") + + if progress: + data_lines.append("") + for role in ROLES: + a, b = progress.get(role, (0, 0)) + data_lines.append(f"{role.upper()}: {a}/{b}") + + overlay_hud( + data, + data_lines, + x=14, + y=24, + font_scale=0.44, + line_step=18, + ) + + board = np.vstack([ + np.hstack([panels["rgb"], panels["re"]]), + np.hstack([panels["nir"], data]), + ]) + + cv2.imshow("Flat Field Calibration - Production", board) + return board + + +# ============================================================ +# Preflight / locking +# ============================================================ + +def resolve_manual_override(args, role: str): + exp = getattr(args, f"{role}_exp_us") + iso = getattr(args, f"{role}_iso") + + if exp is None and iso is None: + return None + + if exp is None or iso is None: + raise ValueError( + f"Para override manual de {role}, informe ambos " + f"--{role}-exp-us e --{role}-iso." + ) + + return LockedControl( + role=role, + exposure_time_us=int(exp), + sensitivity_iso=int(iso), + source="cli_manual", + ) + + +def choose_locked_controls( + ctx: AcquisitionContext, + args, +) -> Dict[str, LockedControl]: + locked = {} + + for role in ROLES: + override = resolve_manual_override(args, role) + + if override is not None: + locked[role] = override + continue + + # Usa mediana das últimas leituras recentes observadas. + exp_values = [] + iso_values = [] + + # O estado latest contém a amostra mais recente. Para robustez, + # capturamos mais algumas amostras rapidamente. + t0 = time.time() + + while time.time() - t0 < 0.7: + ctx.poll() + + c = ctx.latest_ctrl.get(role, {}) + exp = safe_int(c.get("exposure_time_us"), None) + iso = safe_int(c.get("sensitivity_iso"), None) + + if exp is not None and exp > 0: + exp_values.append(exp) + if iso is not None and iso > 0: + iso_values.append(iso) + + time.sleep(0.01) + + if not exp_values or not iso_values: + raise RuntimeError( + f"Não foi possível ler exposição/ISO reais da câmera {role.upper()}." + ) + + locked[role] = LockedControl( + role=role, + exposure_time_us=int(round(np.median(exp_values))), + sensitivity_iso=int(round(np.median(iso_values))), + source="ae_snapshot", + ) + + return locked + + +def wait_white_preflight( + ctx: AcquisitionContext, + args, + thresholds: dict, +) -> Tuple[dict, Dict[str, LockedControl]]: + print("") + print("[ETAPA] WHITE PREFLIGHT") + print("Aponte para alvo branco/cinza fosco uniforme, preenchendo todo o FOV.") + print("A iluminação deve estar estável e sem reflexos.") + print("ENTER congela os controles quando o preflight estiver aprovado.") + + ctx.wait_until_all_have_frames() + + last_quality = None + + while True: + ctx.poll() + + last_quality = evaluate_live_white( + {r: ctx.latest_raw[r] for r in ROLES}, + ctx.temporal_medians, + thresholds, + ) + + ctx.show_board( + title="ETAPA 0/3 - WHITE PREFLIGHT", + lines=[ + "Alvo branco/cinza fosco uniforme.", + "Preencha TODO o campo de visão.", + "Aguarde AE estabilizar.", + "", + "ENTER = congelar EXP/ISO e iniciar", + "Q/ESC = cancelar", + ], + quality=last_quality, + ) + + k = cv2.waitKey(1) & 0xFF + + if k in (ord("q"), ord("Q"), 27): + raise KeyboardInterrupt("Cancelado no WHITE PREFLIGHT.") + + if k in (13, 10): + status = last_quality.get("status", "bad") + + allowed = ( + status == "good" + or (status == "warning" and args.allow_warning_preflight) + or args.force_preflight + ) + + if not allowed: + print( + f"[BLOCK] WHITE PREFLIGHT={status.upper()}. " + "Ajuste luz/alvo ou use override explícito somente se souber o que está fazendo." + ) + continue + + locked = choose_locked_controls(ctx, args) + + print("[LOCK] Controles escolhidos:") + for role in ROLES: + c = locked[role] + print( + f" {role.upper()}: {c.exposure_time_us} us | " + f"ISO {c.sensitivity_iso} | source={c.source}" + ) + + ctx.send_locked_controls(locked) + ctx.verify_locked_controls( + locked, + settle_frames=args.lock_settle_frames, + ) + + return last_quality, locked + + time.sleep(0.002) + + +# ============================================================ +# Captura de estágio +# ============================================================ + +def wait_stage_prompt( + ctx: AcquisitionContext, + title: str, + instructions: List[str], +): + while True: + ctx.poll() + + ctx.show_board( + title=title, + lines=instructions + [ + "", + "ENTER = iniciar captura", + "Q/ESC = cancelar", + ], + ) + + k = cv2.waitKey(1) & 0xFF + + if k in (ord("q"), ord("Q"), 27): + raise KeyboardInterrupt(f"Cancelado em {title}.") + + if k in (13, 10): + return + + time.sleep(0.002) + + +def capture_stage( + ctx: AcquisitionContext, + stage_name: str, + frames: int, + discard_frames: int, + locked: Dict[str, LockedControl], + thresholds: dict, +) -> Tuple[Dict[str, np.ndarray], dict]: + target = int(frames) + + accum = { + role: MeanAccumulator( + shape=(ctx.specs[role].height, ctx.specs[role].width), + reject_fraction=thresholds["frame_global_deviation_reject"], + ) + for role in ROLES + } + + discard_left = {role: int(discard_frames) for role in ROLES} + last_seq = dict(ctx.latest_seq) + + t0 = time.time() + + while any(accum[r].count < target for r in ROLES): + updated = ctx.poll() + + for role in updated: + if accum[role].count >= target: + continue + + seq = ctx.latest_seq.get(role) + if seq is not None and seq == last_seq.get(role): + continue + last_seq[role] = seq + + ctrl = ctx.latest_ctrl[role] + + if not controls_match(ctrl, locked[role], thresholds): + raise RuntimeError( + f"{stage_name}/{role}: controles divergiram do lock. " + f"target={asdict(locked[role])}, actual={ctrl}" + ) + + if discard_left[role] > 0: + discard_left[role] -= 1 + continue + + raw = ctx.latest_raw[role] + + accum[role].try_add( + raw, + ctrl=ctrl, + meta=ctx.latest_packet_meta[role], + ) + + progress = { + role: (accum[role].count, target) + for role in ROLES + } + + ctx.show_board( + title=f"CAPTURA - {stage_name.upper()}", + lines=[ + "Controles MANUAIS travados.", + "Não mova o módulo nem altere iluminação.", + f"Tempo: {time.time() - t0:.1f}s", + "", + "Frames temporalmente anômalos são rejeitados.", + "Q/ESC = cancelar", + ], + progress=progress, + ) + + k = cv2.waitKey(1) & 0xFF + + if k in (ord("q"), ord("Q"), 27): + raise KeyboardInterrupt(f"Captura {stage_name} cancelada.") + + time.sleep(0.001) + + means = { + role: accum[role].mean() + for role in ROLES + } + + report = { + role: accum[role].summary() + for role in ROLES + } + + return means, report + + +# ============================================================ +# Decode usando o RawProcessorCore da aplicação +# ============================================================ + +def disable_nonflat_processing(core: RawProcessorCore): + """ + Mantém SOMENTE o decode físico necessário para produzir R/G/B/RE/NIR. + + Durante a geração de um novo flat não podemos deixar uma calibração antiga + participar da própria medição. Por isso ficam forçados OFF: + - flat-field existente; + - normalização radiométrica; + - enhancement; + - ganhos RGB globais/calibração de cor. + + O único item herdável do module_params é a estratégia de DECODE RGB + (mode + demosaic_algorithm), pois o mapa precisa nascer no mesmo domínio + espacial em que será aplicado na aplicação. + """ + try: + core.flatfield_config["enabled"] = False + core.flatfield_config["npz_file"] = None + except Exception: + pass + + try: + core.flatfield_loaded = False + core.flatfield_maps = {} + except Exception: + pass + + for attr in ( + "radiometric_normalization_config", + "radiometric_config", + "patch_normalization_config", + ): + try: + cfg = getattr(core, attr, None) + if isinstance(cfg, dict): + cfg["enabled"] = False + except Exception: + pass + + try: + enh = core.rgb_processing_config.get("enhancement") + if isinstance(enh, dict): + enh["enabled"] = False + auto_stretch = enh.get("auto_stretch") + if isinstance(auto_stretch, dict): + auto_stretch["enabled"] = False + except Exception: + pass + + try: + core.rgb_calibration["enabled"] = False + gains = core.rgb_calibration.get("gains") + if isinstance(gains, dict): + gains.update({"R": 1.0, "G": 1.0, "B": 1.0}) + except Exception: + pass + + +def create_processing_core( + specs: Dict[str, CameraSpec], + module_params_path: Optional[str], +) -> RawProcessorCore: + """ + Cria um RawProcessorCore LIMPO. + + Regra importante: + NUNCA passa module_params como calibration_json_path aqui. O construtor do + RawProcessorCore pode carregar flatfield_config imediatamente, portanto um + flat antigo da OV9782 poderia contaminar ou até quebrar a calibração da + AR0234 antes de conseguirmos desligá-lo. + + Do module_params copiamos apenas: + rgb_processing.mode + rgb_processing.demosaic_algorithm + """ + rgb = specs["rgb"] + + core = RawProcessorCore( + sensor_width=rgb.width, + sensor_height=rgb.height, + bayer_pattern=rgb.bayer_pattern or "BGGR", + calibration_json_path=None, + ) + + module_cfg = load_json(module_params_path) + + rgb_cfg = module_cfg.get("rgb_processing") + if isinstance(rgb_cfg, dict): + mode = str( + rgb_cfg.get("mode", core.rgb_processing_config.get("mode", "linear_demosaic")) + ).strip().lower() + + allowed_modes = { + "linear_demosaic", + "linear_demosaic_half", + "bayer_planes", + } + + if mode not in allowed_modes: + raise RuntimeError( + "rgb_processing.mode do module_params não é seguro/compatível " + f"com este calibrador: {mode!r}. Permitidos: {sorted(allowed_modes)}" + ) + + algorithm = str( + rgb_cfg.get( + "demosaic_algorithm", + core.rgb_processing_config.get("demosaic_algorithm", "ea"), + ) + ).strip().lower() + + if algorithm not in {"ea", "bilinear"}: + raise RuntimeError( + "rgb_processing.demosaic_algorithm inválido no module_params: " + f"{algorithm!r}. Use 'ea' ou 'bilinear'." + ) + + core.rgb_processing_config["mode"] = mode + core.rgb_processing_config["demosaic_algorithm"] = algorithm + + # A identidade do sensor recém-descoberto sempre vence qualquer JSON antigo. + core.sensor_width = int(rgb.width) + core.sensor_height = int(rgb.height) + core.bayer_pattern = str(rgb.bayer_pattern or "BGGR").upper() + + disable_nonflat_processing(core) + return core + + +def build_decode_meta(specs: Dict[str, CameraSpec]) -> dict: + camera_info = {} + + for role in ROLES: + spec = specs[role] + + camera_info[spec.socket_name] = { + "id": spec.socket_name, + "role": role, + "sensor": spec.sensor_name, + "sensor_name": spec.sensor_name, + "width": int(spec.width), + "height": int(spec.height), + "bit_depth": 10, + "raw_format": "RAW10_PACKED", + "packed": True, + "bayer_pattern": spec.bayer_pattern if role == "rgb" else None, + } + + return { + "frame_type": "RAW_BRUTO", + "camera_info": camera_info, + "camera_frames": camera_info, + } + + +def decode_reference( + core: RawProcessorCore, + specs: Dict[str, CameraSpec], + raw_means: Dict[str, np.ndarray], +) -> dict: + frame = {} + + for role in ROLES: + spec = specs[role] + raw = raw_means[role] + + expected = (spec.height, spec.width) + if raw.shape != expected: + raise RuntimeError( + f"decode_reference/{role}: shape={raw.shape}, esperado={expected}" + ) + + frame[spec.socket_name] = pack_raw10(raw) + + decoded = core.decode_stream_cameras( + frame, + build_decode_meta(specs), + ) + + out = {} + + for cam_id, item in decoded.items(): + role = str(item.get("role", "")).lower() + img = item.get("image") + + if role == "rgb": + if img is None or img.ndim != 3 or img.shape[2] < 3: + raise RuntimeError(f"Decode RGB inválido: {None if img is None else img.shape}") + + out["R"] = img[:, :, 0].astype(np.float32).copy() + out["G"] = img[:, :, 1].astype(np.float32).copy() + out["B"] = img[:, :, 2].astype(np.float32).copy() + + elif role == "re": + if img is None or img.ndim != 2: + raise RuntimeError(f"Decode RE inválido: {None if img is None else img.shape}") + out["RE"] = img.astype(np.float32).copy() + + elif role == "nir": + if img is None or img.ndim != 2: + raise RuntimeError(f"Decode NIR inválido: {None if img is None else img.shape}") + out["NIR"] = img.astype(np.float32).copy() + + missing = [ch for ch in CHANNELS if ch not in out] + if missing: + raise RuntimeError(f"RawProcessorCore não produziu canais: {missing}") + + return out + + +# ============================================================ +# Construção do flat +# ============================================================ + +def gaussian_smooth_field(signal: np.ndarray, sigma_frac: float) -> Tuple[np.ndarray, float]: + """ + Suaviza o CAMPO luminoso antes de inverter. + sigma é proporcional à menor dimensão, portanto AR0234/OV9782 recebem + escalas comparáveis no FOV. + """ + a = signal.astype(np.float32) + + sigma = max(1.0, float(min(a.shape[:2])) * float(sigma_frac)) + + smooth = cv2.GaussianBlur( + a, + (0, 0), + sigmaX=sigma, + sigmaY=sigma, + borderType=cv2.BORDER_REFLECT, + ) + + return smooth.astype(np.float32), float(sigma) + + +def build_gain_maps( + white_a: dict, + dark: Optional[dict], + epsilon: float, + sigma_frac: float, + min_gain: float, + max_gain: float, +): + gain_maps = {} + flat_norm = {} + white_signal = {} + smooth_fields = {} + dark_used = {} + processing_meta = {} + + for ch in CHANNELS: + w = white_a[ch].astype(np.float32) + + if dark is not None: + d = dark[ch].astype(np.float32) + + if d.shape != w.shape: + raise RuntimeError( + f"{ch}: DARK shape {d.shape} != WHITE shape {w.shape}. " + "Calibração abortada; resize de dark é proibido." + ) + else: + d = np.zeros_like(w, dtype=np.float32) + + signal = w - d + signal = np.maximum(signal, float(epsilon)).astype(np.float32) + + smooth, sigma = gaussian_smooth_field(signal, sigma_frac=sigma_frac) + + reference = robust_center_reference(smooth) + gain_unclipped = reference / np.maximum(smooth, float(epsilon)) + + gain = np.clip( + gain_unclipped, + float(min_gain), + float(max_gain), + ).astype(np.float32) + + gain_maps[ch] = gain + flat_norm[ch] = (smooth / max(reference, epsilon)).astype(np.float32) + white_signal[ch] = signal + smooth_fields[ch] = smooth + dark_used[ch] = d + + processing_meta[ch] = { + "reference": float(reference), + "sigma_px": float(sigma), + "gain_unclipped_min": float(np.min(gain_unclipped)), + "gain_unclipped_max": float(np.max(gain_unclipped)), + "clipped_low_pct": float((gain_unclipped < min_gain).mean() * 100.0), + "clipped_high_pct": float((gain_unclipped > max_gain).mean() * 100.0), + } + + return { + "gain_maps": gain_maps, + "flat_norm": flat_norm, + "white_signal": white_signal, + "smooth_fields": smooth_fields, + "dark_used": dark_used, + "processing_meta": processing_meta, + } + + +# ============================================================ +# Acceptance test +# ============================================================ + +def evaluate_gain_channel(gain: np.ndarray, th: dict) -> dict: + f = gain[np.isfinite(gain)] + + if f.size == 0: + return { + "status": "bad", + "reasons": ["gain_invalid"], + } + + st = { + "min": float(np.min(f)), + "p01": float(np.percentile(f, 1)), + "p05": float(np.percentile(f, 5)), + "p50": float(np.percentile(f, 50)), + "p95": float(np.percentile(f, 95)), + "p99": float(np.percentile(f, 99)), + "max": float(np.max(f)), + "mean": float(np.mean(f)), + "std": float(np.std(f)), + } + + status = "good" + reasons = [] + + if st["p99"] > th["gain_p99_bad"]: + status = merge_status(status, "bad") + reasons.append(f"gain_p99_bad:{st['p99']:.3f}") + elif st["p99"] > th["gain_p99_warning"]: + status = merge_status(status, "warning") + reasons.append(f"gain_p99_warn:{st['p99']:.3f}") + + if st["max"] > th["gain_max_bad"]: + status = merge_status(status, "bad") + reasons.append(f"gain_max_bad:{st['max']:.3f}") + elif st["max"] > th["gain_max_warning"]: + status = merge_status(status, "warning") + reasons.append(f"gain_max_warn:{st['max']:.3f}") + + if st["min"] < th["gain_min_bad"]: + status = merge_status(status, "bad") + reasons.append(f"gain_min_bad:{st['min']:.3f}") + elif st["min"] < th["gain_min_warning"]: + status = merge_status(status, "warning") + reasons.append(f"gain_min_warn:{st['min']:.3f}") + + return { + "status": status, + "reasons": reasons, + "stats": st, + } + + +def evaluate_dark_channels(dark: dict, th: dict) -> dict: + result = { + "status": "good", + "channels": {}, + "reasons": [], + } + + for ch in CHANNELS: + a = dark[ch].astype(np.float32) + p99 = finite_percentile(a, 99, default=1.0) + + status = "good" + reasons = [] + + if p99 > th["dark_p99_bad"]: + status = "bad" + reasons.append(f"dark_p99_bad:{p99:.4f}") + elif p99 > th["dark_p99_warning"]: + status = "warning" + reasons.append(f"dark_p99_warn:{p99:.4f}") + + result["channels"][ch] = { + "status": status, + "p99": p99, + "reasons": reasons, + } + + result["status"] = merge_status(result["status"], status) + result["reasons"].extend([f"{ch}:{x}" for x in reasons]) + + return result + + +def acceptance_test( + gain_maps: dict, + white_b: dict, + dark: Optional[dict], + th: dict, +) -> dict: + result = { + "status": "good", + "channels": {}, + "reasons": [], + } + + for ch in CHANNELS: + wb = white_b[ch].astype(np.float32) + + if dark is not None: + d = dark[ch].astype(np.float32) + + if wb.shape != d.shape: + raise RuntimeError( + f"Acceptance {ch}: WHITE_B {wb.shape} != DARK {d.shape}" + ) + else: + d = np.zeros_like(wb) + + signal = np.maximum(wb - d, 1e-8) + corrected = signal * gain_maps[ch] + + before = grid_uniformity(signal) + after = grid_uniformity(corrected) + + before_cv = float(before["grid_cv"]) + after_cv = float(after["grid_cv"]) + + if before_cv > 1e-9: + improvement = 1.0 - (after_cv / before_cv) + else: + improvement = 0.0 + + local_after = local_defect_stats( + corrected, + outlier_thr=th["local_dev_p99_warning"], + ) + + gain_report = evaluate_gain_channel(gain_maps[ch], th) + + status = gain_report["status"] + reasons = list(gain_report["reasons"]) + + if after_cv > th["corrected_grid_cv_bad"]: + status = merge_status(status, "bad") + reasons.append(f"corrected_grid_cv_bad:{after_cv:.4f}") + elif after_cv > th["corrected_grid_cv_warning"]: + status = merge_status(status, "warning") + reasons.append(f"corrected_grid_cv_warn:{after_cv:.4f}") + + if before_cv >= th["min_before_grid_cv_for_improvement"]: + if improvement < th["worsening_bad"]: + status = merge_status(status, "bad") + reasons.append(f"flat_worsened:{improvement:.3f}") + elif improvement < th["improvement_bad"]: + status = merge_status(status, "bad") + reasons.append(f"improvement_bad:{improvement:.3f}") + elif improvement < th["improvement_warning"]: + status = merge_status(status, "warning") + reasons.append(f"improvement_warn:{improvement:.3f}") + + if local_after.get("valid"): + dev = float(local_after["local_dev_p99"]) + out_pct = float(local_after["local_outlier_pct"]) + + if dev > th["local_dev_p99_bad"]: + status = merge_status(status, "bad") + reasons.append(f"local_dev_bad:{dev:.3f}") + elif dev > th["local_dev_p99_warning"]: + status = merge_status(status, "warning") + reasons.append(f"local_dev_warn:{dev:.3f}") + + if out_pct > th["local_outlier_pct_bad"]: + status = merge_status(status, "bad") + reasons.append(f"local_outlier_bad:{out_pct:.2f}%") + elif out_pct > th["local_outlier_pct_warning"]: + status = merge_status(status, "warning") + reasons.append(f"local_outlier_warn:{out_pct:.2f}%") + + result["channels"][ch] = { + "status": status, + "reasons": reasons, + "before_uniformity": before, + "after_uniformity": after, + "grid_cv_before": before_cv, + "grid_cv_after": after_cv, + "improvement": float(improvement), + "local_after": local_after, + "gain": gain_report, + } + + result["status"] = merge_status(result["status"], status) + result["reasons"].extend([f"{ch}:{x}" for x in reasons]) + + return result + + +# ============================================================ +# Previews +# ============================================================ + +def gray_bgr(arr: np.ndarray) -> np.ndarray: + return cv2.cvtColor(normalize_display(arr), cv2.COLOR_GRAY2BGR) + + +def save_previews( + out_dir: Path, + gain_maps: dict, + white_signal_a: dict, + white_b: dict, + dark: Optional[dict], +): + ensure_dir(out_dir) + + for ch in CHANNELS: + gain = gain_maps[ch] + + wb = white_b[ch].astype(np.float32) + d = dark[ch].astype(np.float32) if dark is not None else np.zeros_like(wb) + validation_signal = np.maximum(wb - d, 1e-8) + corrected = validation_signal * gain + + cv2.imwrite(str(out_dir / f"{ch}_gain.png"), gray_bgr(gain)) + cv2.imwrite(str(out_dir / f"{ch}_white_A_signal.png"), gray_bgr(white_signal_a[ch])) + cv2.imwrite(str(out_dir / f"{ch}_white_B_before.png"), gray_bgr(validation_signal)) + cv2.imwrite(str(out_dir / f"{ch}_white_B_after.png"), gray_bgr(corrected)) + + # Comparativo lado a lado. + before = gray_bgr(validation_signal) + after = gray_bgr(corrected) + + h = min(before.shape[0], after.shape[0]) + w = min(before.shape[1], after.shape[1]) + + before = cv2.resize(before, (w, h)) + after = cv2.resize(after, (w, h)) + + overlay_hud(before, [f"{ch} BEFORE"], x=12, y=26, font_scale=0.65, line_step=24) + overlay_hud(after, [f"{ch} AFTER"], x=12, y=26, font_scale=0.65, line_step=24) + + side = np.hstack([before, after]) + cv2.imwrite(str(out_dir / f"{ch}_validation_compare.png"), side) + + +# ============================================================ +# Persistência / promoção +# ============================================================ + +def suggested_flatfield_config(active_npz: str) -> dict: + return { + "flatfield_config": { + "enabled": True, + "npz_file": str(active_npz).replace("\\", "/"), + "apply_before_fusion": True, + "apply_after_decode": True, + "apply_space": "native_camera_space", + "map_type": "gain", + "channels": list(CHANNELS), + "channel_maps": { + ch: f"gain_{ch}" + for ch in CHANNELS + }, + "subtract_dark": False, + "clip_output": True, + } + } + + +def save_candidate_npz( + path: Path, + gain_maps: dict, + flat_norm: dict, + white_a: dict, + white_b: dict, + dark: Optional[dict], + raw_white_a: dict, + raw_white_b: dict, + raw_dark: Optional[dict], +): + payload = {} + + for ch in CHANNELS: + payload[f"gain_{ch}"] = gain_maps[ch].astype(np.float32) + payload[f"flat_norm_{ch}"] = flat_norm[ch].astype(np.float32) + + # Novo nome correto. + payload[f"white_reference_{ch}"] = white_a[ch].astype(np.float32) + payload[f"validation_white_{ch}"] = white_b[ch].astype(np.float32) + + # Alias legado para ferramentas existentes. + payload[f"white_median_{ch}"] = white_a[ch].astype(np.float32) + + if dark is not None: + payload[f"dark_reference_{ch}"] = dark[ch].astype(np.float32) + payload[f"dark_median_{ch}"] = dark[ch].astype(np.float32) + + # Auditoria RAW nativa por câmera. + for role in ROLES: + payload[f"raw_white_A_{role}"] = raw_white_a[role].astype(np.float32) + payload[f"raw_white_B_{role}"] = raw_white_b[role].astype(np.float32) + + if raw_dark is not None: + payload[f"raw_dark_{role}"] = raw_dark[role].astype(np.float32) + + ensure_dir(path.parent) + tmp = path.with_suffix(path.suffix + ".tmp.npz") + np.savez_compressed(tmp, **payload) + os.replace(tmp, path) + + +def atomic_promote( + candidate_npz: Path, + candidate_report: Path, + active_npz: Path, + active_json: Path, +): + ensure_dir(active_npz.parent) + ensure_dir(active_json.parent) + + # Backup da calibração ativa anterior. + stamp = session_id() + + if active_npz.exists(): + backup = active_npz.with_name(active_npz.stem + f".backup_{stamp}" + active_npz.suffix) + shutil.copy2(active_npz, backup) + + if active_json.exists(): + backup = active_json.with_name(active_json.stem + f".backup_{stamp}" + active_json.suffix) + shutil.copy2(active_json, backup) + + tmp_npz = active_npz.with_suffix(active_npz.suffix + ".tmp") + tmp_json = active_json.with_suffix(active_json.suffix + ".tmp") + + shutil.copy2(candidate_npz, tmp_npz) + shutil.copy2(candidate_report, tmp_json) + + # Só troca depois que ambos foram copiados. + os.replace(tmp_npz, active_npz) + os.replace(tmp_json, active_json) + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser( + description=( + "Calibrador de flat-field/dark-frame de produção para " + "OAK-FFC-3P: OV9782 ou AR0234 + 2x OV9282." + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # Hardware + parser.add_argument("--mx-id", default=None) + parser.add_argument("--fps", type=float, default=20.0) + parser.add_argument( + "--anti-banding", + default="OFF", + choices=["OFF", "50", "60", "50HZ", "60HZ"], + ) + parser.add_argument( + "--rgb-bayer", + default="auto", + choices=["auto", "RGGB", "BGGR", "GRBG", "GBRG"], + ) + parser.add_argument( + "--module-params", + default="calibration/module_params.json", + help=( + "Lê SOMENTE rgb_processing.mode e demosaic_algorithm para manter " + "o mesmo domínio de decode da produção. O JSON completo NÃO é " + "carregado no RawProcessorCore durante a calibração, evitando que " + "flat/radiometric/enhancement antigos contaminem o novo flat." + ), + ) + + # Captura + parser.add_argument("--frames", type=int, default=60) + parser.add_argument("--validation-frames", type=int, default=30) + parser.add_argument("--dark-frames", type=int, default=60) + parser.add_argument("--discard-frames", type=int, default=12) + parser.add_argument("--lock-settle-frames", type=int, default=8) + + # Overrides manuais opcionais + for role in ROLES: + parser.add_argument(f"--{role}-exp-us", type=int, default=None) + parser.add_argument(f"--{role}-iso", type=int, default=None) + + # Processamento + parser.add_argument( + "--sigma-frac", + type=float, + default=0.025, + help="Sigma do shading gaussiano como fração da menor dimensão.", + ) + parser.add_argument("--min-gain", type=float, default=0.25) + parser.add_argument("--max-gain", type=float, default=4.0) + parser.add_argument("--epsilon", type=float, default=1e-6) + + # Política + parser.add_argument( + "--skip-dark", + action="store_true", + help=( + "Permite gerar candidate sem dark. Por padrão candidate sem dark " + "NÃO é promovido, a menos que --allow-no-dark-promote seja usado." + ), + ) + parser.add_argument("--allow-no-dark-promote", action="store_true") + parser.add_argument("--allow-warning-preflight", action="store_true") + parser.add_argument( + "--force-preflight", + action="store_true", + help="Override explícito de engenharia. Fica registrado no report.", + ) + parser.add_argument( + "--promote-warning", + action="store_true", + help="Permite promover acceptance WARNING. Nunca promove BAD.", + ) + parser.add_argument("--no-promote", action="store_true") + + # Output + parser.add_argument( + "--candidate-root", + default="calibration/flatfield_candidates", + ) + parser.add_argument( + "--active-npz", + default="calibration/flatfield_maps_v1.npz", + ) + parser.add_argument( + "--active-json", + default="calibration/flatfield_maps_v1.json", + ) + + # UI + parser.add_argument("--panel-width", type=int, default=640) + parser.add_argument("--panel-height", type=int, default=400) + + parser.add_argument("--notes", default="") + + args = parser.parse_args() + + if args.frames < 10 or args.validation_frames < 10 or args.dark_frames < 10: + raise ValueError("Produção: use pelo menos 10 frames por estágio.") + + if not (0.001 <= args.sigma_frac <= 0.20): + raise ValueError("--sigma-frac fora da faixa segura [0.001, 0.20].") + + thresholds = dict(DEFAULT_THRESHOLDS) + + sid = session_id() + candidate_dir = Path(args.candidate_root) / sid + preview_dir = candidate_dir / "previews" + candidate_npz = candidate_dir / "flatfield_maps.npz" + candidate_report_path = candidate_dir / "report.json" + + ensure_dir(candidate_dir) + ensure_dir(preview_dir) + + module_params = load_json(args.module_params) + + # -------------------------------------------------------- + # Descoberta + # -------------------------------------------------------- + + dev_info = select_device_info(args.mx_id) + mx_id = device_id_from_info(dev_info) + + features, usb_speed = discover_features(dev_info) + + specs = validate_and_build_specs( + features=features, + rgb_bayer_arg=args.rgb_bayer, + module_params=module_params, + ) + + rgb_spec = specs["rgb"] + + print("=" * 82) + print("FLAT-FIELD CALIBRATION - PRODUCTION") + print(f"DepthAI : {getattr(dai, '__version__', 'unknown')}") + print(f"MX ID : {mx_id}") + print(f"USB : {usb_speed}") + print("-" * 82) + + for role in ROLES: + s = specs[role] + extra = f" | Bayer={s.bayer_pattern}" if s.bayer_pattern else "" + print( + f"{s.socket_name} -> {role.upper():3s} | {s.sensor_name:8s} | " + f"{s.width}x{s.height} | {s.resolution_enum}{extra}" + ) + + print("=" * 82) + + # -------------------------------------------------------- + # Pipeline + # -------------------------------------------------------- + + pipeline = build_pipeline( + specs, + fps=args.fps, + anti_banding=args.anti_banding, + ) + + device = dai.Device(pipeline, dev_info) + + cv2.namedWindow( + "Flat Field Calibration - Production", + cv2.WINDOW_NORMAL, + ) + + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + "status": "running", + "promoted": False, + "depthai_version": getattr(dai, "__version__", "unknown"), + "device": { + "mx_id": mx_id, + "usb_speed": usb_speed, + }, + "hardware_inventory": features, + "resolved_setup": { + role: asdict(spec) + for role, spec in specs.items() + }, + "module_params_path": args.module_params, + "rgb_processing": None, + "capture": { + "fps": args.fps, + "anti_banding": args.anti_banding, + "frames": args.frames, + "dark_frames": args.dark_frames, + "validation_frames": args.validation_frames, + "discard_frames": args.discard_frames, + }, + "policy": { + "skip_dark": bool(args.skip_dark), + "allow_no_dark_promote": bool(args.allow_no_dark_promote), + "allow_warning_preflight": bool(args.allow_warning_preflight), + "force_preflight": bool(args.force_preflight), + "promote_warning": bool(args.promote_warning), + "no_promote": bool(args.no_promote), + }, + "thresholds": thresholds, + "notes": args.notes, + "stages": {}, + "outputs": { + "candidate_dir": str(candidate_dir), + "candidate_npz": str(candidate_npz), + "candidate_report": str(candidate_report_path), + "active_npz": args.active_npz, + "active_json": args.active_json, + }, + "suggested_module_params_patch": suggested_flatfield_config(args.active_npz), + } + + try: + ctx = AcquisitionContext( + device=device, + specs=specs, + thresholds=thresholds, + panel_width=args.panel_width, + panel_height=args.panel_height, + rgb_bayer=rgb_spec.bayer_pattern, + ) + + # ---------------------------------------------------- + # Core de processamento da própria aplicação + # ---------------------------------------------------- + + core = create_processing_core(specs, args.module_params) + + report["rgb_processing"] = { + "mode": str(core.rgb_processing_config.get("mode")), + "demosaic_algorithm": str(core.rgb_processing_config.get("demosaic_algorithm")), + "enhancement_forced_off": True, + "flatfield_forced_off": True, + "radiometric_normalization_forced_off": True, + } + + print( + "[CORE] rgb_processing.mode=" + f"{report['rgb_processing']['mode']}" + ) + + # ---------------------------------------------------- + # WHITE PREFLIGHT + lock + # ---------------------------------------------------- + + preflight_report, locked = wait_white_preflight( + ctx, + args, + thresholds, + ) + + report["white_preflight"] = preflight_report + report["locked_controls"] = { + role: asdict(ctrl) + for role, ctrl in locked.items() + } + + # ---------------------------------------------------- + # WHITE A + # ---------------------------------------------------- + + wait_stage_prompt( + ctx, + title="ETAPA 1/3 - WHITE A", + instructions=[ + "Mantenha o MESMO alvo do preflight.", + "Não mova módulo, lente ou iluminação.", + "Esta captura CONSTRÓI o flat.", + "EXP/ISO já estão travados.", + ], + ) + + raw_white_a, white_a_capture_report = capture_stage( + ctx=ctx, + stage_name="white_A", + frames=args.frames, + discard_frames=args.discard_frames, + locked=locked, + thresholds=thresholds, + ) + + report["stages"]["white_A"] = white_a_capture_report + + # ---------------------------------------------------- + # DARK + # ---------------------------------------------------- + + raw_dark = None + + if not args.skip_dark: + wait_stage_prompt( + ctx, + title="ETAPA 2/3 - DARK", + instructions=[ + "TAMPE COMPLETAMENTE as três lentes.", + "Não use fundo preto distante.", + "Não altere iluminação nem controles.", + "EXP/ISO permanecem idênticos ao WHITE.", + ], + ) + + raw_dark, dark_capture_report = capture_stage( + ctx=ctx, + stage_name="dark", + frames=args.dark_frames, + discard_frames=args.discard_frames, + locked=locked, + thresholds=thresholds, + ) + + report["stages"]["dark"] = dark_capture_report + else: + report["stages"]["dark"] = { + "skipped": True, + } + + # ---------------------------------------------------- + # WHITE B / acceptance independente + # ---------------------------------------------------- + + wait_stage_prompt( + ctx, + title="ETAPA 3/3 - WHITE B / VALIDATION", + instructions=[ + "DESTAMPE as lentes.", + "Volte exatamente ao mesmo alvo WHITE.", + "Esta captura NÃO participa do cálculo do flat.", + "Ela mede se a correção funciona de verdade.", + ], + ) + + raw_white_b, white_b_capture_report = capture_stage( + ctx=ctx, + stage_name="white_B_validation", + frames=args.validation_frames, + discard_frames=args.discard_frames, + locked=locked, + thresholds=thresholds, + ) + + report["stages"]["white_B_validation"] = white_b_capture_report + + # ---------------------------------------------------- + # Decode das referências pelo RawProcessorCore + # ---------------------------------------------------- + + processing_screen = np.zeros((720, 1280, 3), dtype=np.uint8) + overlay_hud( + processing_screen, + [ + "PROCESSANDO FLAT-FIELD...", + "", + f"RGB sensor: {rgb_spec.sensor_name}", + f"RGB Bayer : {rgb_spec.bayer_pattern}", + f"RGB mode : {report['rgb_processing']['mode']}", + "", + "Decodificando referências pelo RawProcessorCore.", + "Gerando shading field, gain maps e acceptance test.", + ], + x=40, + y=80, + font_scale=0.72, + line_step=31, + ) + cv2.imshow("Flat Field Calibration - Production", processing_screen) + cv2.waitKey(1) + + white_a = decode_reference(core, specs, raw_white_a) + white_b = decode_reference(core, specs, raw_white_b) + dark = decode_reference(core, specs, raw_dark) if raw_dark is not None else None + + # Garante shapes invariantes por canal. + for ch in CHANNELS: + if white_a[ch].shape != white_b[ch].shape: + raise RuntimeError( + f"{ch}: WHITE_A shape {white_a[ch].shape} != " + f"WHITE_B shape {white_b[ch].shape}" + ) + + if dark is not None and white_a[ch].shape != dark[ch].shape: + raise RuntimeError( + f"{ch}: WHITE_A shape {white_a[ch].shape} != " + f"DARK shape {dark[ch].shape}" + ) + + report["decoded_channel_shapes"] = { + ch: list(white_a[ch].shape) + for ch in CHANNELS + } + + # ---------------------------------------------------- + # Dark QA + # ---------------------------------------------------- + + dark_quality = None + + if dark is not None: + dark_quality = evaluate_dark_channels(dark, thresholds) + report["dark_quality"] = dark_quality + + # ---------------------------------------------------- + # Gain + # ---------------------------------------------------- + + built = build_gain_maps( + white_a=white_a, + dark=dark, + epsilon=args.epsilon, + sigma_frac=args.sigma_frac, + min_gain=args.min_gain, + max_gain=args.max_gain, + ) + + gain_maps = built["gain_maps"] + flat_norm = built["flat_norm"] + white_signal_a = built["white_signal"] + + report["gain_processing"] = { + "sigma_frac": args.sigma_frac, + "min_gain": args.min_gain, + "max_gain": args.max_gain, + "epsilon": args.epsilon, + "channels": built["processing_meta"], + } + + # ---------------------------------------------------- + # Acceptance + # ---------------------------------------------------- + + acceptance = acceptance_test( + gain_maps=gain_maps, + white_b=white_b, + dark=dark, + th=thresholds, + ) + + report["acceptance"] = acceptance + + # Dark BAD também torna calibração BAD. + final_status = acceptance["status"] + + if dark_quality is not None: + final_status = merge_status(final_status, dark_quality["status"]) + + if args.skip_dark and not args.allow_no_dark_promote: + final_status = merge_status(final_status, "warning") + report.setdefault("reasons", []).append( + "dark_skipped:promotion_blocked_without_allow_no_dark_promote" + ) + + report["status"] = final_status + report["finished_at"] = now_str() + + # ---------------------------------------------------- + # Candidate + # ---------------------------------------------------- + + save_candidate_npz( + path=candidate_npz, + gain_maps=gain_maps, + flat_norm=flat_norm, + white_a=white_a, + white_b=white_b, + dark=dark, + raw_white_a=raw_white_a, + raw_white_b=raw_white_b, + raw_dark=raw_dark, + ) + + save_previews( + out_dir=preview_dir, + gain_maps=gain_maps, + white_signal_a=white_signal_a, + white_b=white_b, + dark=dark, + ) + + report["candidate_sha256"] = sha256_file(candidate_npz) + + # Decide promoção ANTES de salvar report final. + promote_allowed = False + + if final_status == "good": + promote_allowed = True + + elif final_status == "warning" and args.promote_warning: + promote_allowed = True + + if args.skip_dark and not args.allow_no_dark_promote: + promote_allowed = False + + if args.no_promote: + promote_allowed = False + + report["promotion_decision"] = { + "allowed_by_quality": bool(promote_allowed), + "final_status": final_status, + "no_promote": bool(args.no_promote), + } + + save_json(candidate_report_path, report) + + # Marca candidate. + marker = candidate_dir / ( + "PASS.txt" if promote_allowed else "FAIL.txt" + ) + + marker.write_text( + ( + f"status={final_status}\n" + f"promote_allowed={promote_allowed}\n" + f"created_at={report['created_at']}\n" + f"finished_at={report['finished_at']}\n" + ), + encoding="utf-8", + ) + + # ---------------------------------------------------- + # Promoção atômica + # ---------------------------------------------------- + + if promote_allowed: + atomic_promote( + candidate_npz=candidate_npz, + candidate_report=candidate_report_path, + active_npz=Path(args.active_npz), + active_json=Path(args.active_json), + ) + + report["promoted"] = True + report["promoted_at"] = now_str() + + # Atualiza report candidate e active JSON com estado promovido. + save_json(candidate_report_path, report) + save_json(args.active_json, report) + + print("") + print("=" * 82) + print("[PASS] FLAT-FIELD APROVADO E PROMOVIDO") + print(f"[ACTIVE] NPZ : {args.active_npz}") + print(f"[ACTIVE] JSON: {args.active_json}") + print(f"[CANDIDATE] {candidate_dir}") + print("=" * 82) + + else: + print("") + print("=" * 82) + print("[FAIL/WARN] CANDIDATE NÃO FOI PROMOVIDO") + print(f"[STATUS] {final_status.upper()}") + print(f"[CANDIDATE] {candidate_dir}") + print("[SAFE] Calibração ativa anterior foi preservada.") + print("=" * 82) + + # ---------------------------------------------------- + # Resumo visual final + # ---------------------------------------------------- + + final_panel = np.zeros((720, 1280, 3), dtype=np.uint8) + + lines = [ + "FLAT-FIELD - RESULTADO FINAL", + "", + f"STATUS: {final_status.upper()}", + f"PROMOVIDO: {'SIM' if report.get('promoted') else 'NAO'}", + "", + f"RGB: {rgb_spec.sensor_name} {rgb_spec.width}x{rgb_spec.height}", + f"Bayer: {rgb_spec.bayer_pattern}", + f"RGB processing: {report['rgb_processing']['mode']}", + "", + ] + + for ch in CHANNELS: + a = acceptance["channels"][ch] + lines.append( + f"{ch}: {a['status'].upper()} | " + f"gridCV {a['grid_cv_before']:.4f}->{a['grid_cv_after']:.4f} | " + f"melhora={a['improvement']*100:.1f}%" + ) + + lines.extend([ + "", + f"Candidate: {candidate_dir}", + "", + "Qualquer tecla fecha.", + ]) + + overlay_hud( + final_panel, + lines, + x=34, + y=58, + font_scale=0.62, + line_step=27, + ) + + cv2.imshow("Flat Field Calibration - Production", final_panel) + cv2.waitKey(0) + + except KeyboardInterrupt as exc: + report["status"] = "cancelled" + report["finished_at"] = now_str() + report["error"] = str(exc) + + try: + save_json(candidate_report_path, report) + except Exception: + pass + + print(f"[CANCELADO] {exc}") + + except Exception as exc: + report["status"] = "error" + report["finished_at"] = now_str() + report["error"] = f"{type(exc).__name__}: {exc}" + + try: + save_json(candidate_report_path, report) + except Exception: + pass + + print("") + print("=" * 82) + print("[ERRO] CALIBRAÇÃO ABORTADA - NADA FOI PROMOVIDO") + print(f"{type(exc).__name__}: {exc}") + print(f"Report parcial: {candidate_report_path}") + print("=" * 82) + + raise + + finally: + try: + device.close() + except Exception: + pass + + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/_4_radiometric_config_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/_4_radiometric_config_tool.py new file mode 100644 index 000000000..380fe5d46 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/utils/_4_radiometric_config_tool.py @@ -0,0 +1,5357 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +radiometric_calibration_production.py +===================================== + +Calibrador FINAL de produção para a NORMALIZAÇÃO RADIOMÉTRICA do módulo +multiespectral OAK-FFC-3P. + +Topologia oficial +----------------- + CAM_A = RGB = OV9782 1280x800 + OU + AR0234 1920x1200 + + CAM_B = RE = OV9282 1280x800 + CAM_C = NIR = OV9282 1280x800 + +O que esta ferramenta calibra +----------------------------- +Esta ferramenta calibra o bloco que o runtime chama de: + + radiometric_normalization + +Contrato atual: + + method = oak_ae_frame_controls_v1 + factor_model = exposure_time_us_x_iso + iso_base = 100 + scale = reference_factor / actual_factor + +onde: + + actual_factor = + exposure_time_us * (sensitivity_iso / iso_base) + +Ela NÃO configura automaticamente o controlador dinâmico de exposição +`radiometric_config` e NÃO habilita `patch_normalization`. + +Esses dois blocos são políticas diferentes: + - radiometric_config: + controlador ativo de exposição em campo + - patch_normalization: + normalização pós-fusão por cartões/patches + +Por segurança de produto, ambos permanecem recomendados como DISABLED até +uma etapa específica de validação de campo decidir o contrário. + +Por que fazer uma calibração real? +---------------------------------- +Trocar OV9782 -> AR0234 muda sensibilidade, resposta e faixa operacional. +Copiar reference_controls antigos pode deslocar a escala dos canais que entra +no modelo e alterar relações espectrais. + +Esta ferramenta mede, por sensor: + 1. estabilidade temporal; + 2. resposta RAW vs exposição; + 3. linearidade da hipótese exposição x ISO; + 4. intercepto/offset efetivo; + 5. erro da normalização ao longo de um sweep; + 6. ponto de referência para um alvo radiométrico definido; + 7. validação independente no ponto calculado. + +Domínio de medição +------------------ +A medição é feita no RAW nativo da câmera, antes de: + - demosaic; + - enhancement; + - homografia; + - flat gain; + - resize; + - patch normalization. + +Isso casa com o propósito do bloco runtime: + apply_stage = after_dark_before_flat_gain + +IMPORTANTE: +----------- +A referência absoluta depende do contrato físico da bancada. + +Para repetir calibrações entre módulos, mantenha fixos: + - painel difuso/cinza usado; + - distância; + - geometria; + - fonte de luz; + - temperatura/tempo de aquecimento; + - intensidade da fonte. + +Se a bancada mudar, a referência radiométrica muda junto. + +Uso recomendado +--------------- +1. Coloque um painel cinza/difuso uniforme preenchendo o FOV. +2. Use iluminação de bancada estável e reproduzível. +3. Rode: + + python radiometric_calibration_production.py ^ + --rig-id RIG_MS_01 ^ + --target-id GRAY_PANEL_01 ^ + --illumination-id LED_CAL_01 + +4. Espere o preflight ficar GOOD. +5. ENTER inicia a calibração automática. +6. Não mexa mais em câmera, painel ou luz. +7. O script executa o sweep, regressão e validação. +8. PASS promove o artefato. + +Saídas +------ +calibration/radiometry_candidates// + report.json + sweep.csv + PASS.txt / FAIL.txt / CANCELLED.txt + response_rgb.png + response_re.png + response_nir.png + validation_rgb.png + validation_re.png + validation_nir.png + +Se PASS: + calibration/radiometry_calibration_v5.json + +O JSON ativo contém: + module_params_fragment.radiometric_normalization + +O assembler final continua responsável por montar module_params.json. + +Política fail-closed +-------------------- +- BAD nunca promove. +- WARNING só promove com --promote-warning. +- FAIL/CANCEL preserva o ativo anterior. +- sensor/resolução diferentes do produto abortam. +- controles manuais divergentes abortam. +- sweep insuficiente aborta. +- o script nunca reativa silenciosamente o controlador de campo. + +Teclas +------ +PRE-FLIGHT: + ENTER -> iniciar + Q/ESC -> cancelar + +RESULTADO: + qualquer tecla -> fechar +""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import math +import os +import shutil +import time +from collections import deque +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional, Tuple, List, Any + +import cv2 +import depthai as dai +import numpy as np + + +# ============================================================ +# Contrato do produto +# ============================================================ + +ROLES = ("rgb", "re", "nir") + +PRODUCT_TOPOLOGY = { + "rgb": { + "socket": "CAM_A", + "allowed_sensors": ("OV9782", "AR0234"), + }, + "re": { + "socket": "CAM_B", + "allowed_sensors": ("OV9282",), + }, + "nir": { + "socket": "CAM_C", + "allowed_sensors": ("OV9282",), + }, +} + +RGB_SENSOR_MODES = { + "OV9782": { + "resolution_enum": "THE_800_P", + "width": 1280, + "height": 800, + }, + "AR0234": { + "resolution_enum": "THE_1200_P", + "width": 1920, + "height": 1200, + }, +} + +MONO_SENSOR_MODES = { + "OV9282": { + "resolution_enum": "THE_800_P", + "width": 1280, + "height": 800, + }, +} + +SCHEMA = "multispec_radiometric_calibration_v5" +CALIBRATION_DOMAIN = "native_sensor_raw_linear" +NORMALIZATION_METHOD = "oak_ae_frame_controls_v1" +FACTOR_MODEL = "exposure_time_us_x_iso" +ISO_BASE = 100.0 + + +# ============================================================ +# QA de produção +# ============================================================ + +DEFAULT_QA = { + # Preflight em painel cinza + "preflight_p50_min": 0.12, + "preflight_p50_max": 0.78, + "preflight_sat_warning_pct": 0.10, + "preflight_sat_bad_pct": 0.50, + "preflight_dark_warning_pct": 2.0, + "preflight_dark_bad_pct": 8.0, + "preflight_temporal_cv_warning": 0.010, + "preflight_temporal_cv_bad": 0.025, + + # Sweep + "min_sweep_levels": 6, + "min_valid_levels": 5, + "valid_p50_min": 0.035, + "valid_p50_max": 0.88, + "valid_sat_max_pct": 0.20, + "level_temporal_cv_warning": 0.010, + "level_temporal_cv_bad": 0.025, + + # Regressão + "r2_warning": 0.995, + "r2_bad": 0.985, + "intercept_fraction_warning": 0.035, + "intercept_fraction_bad": 0.080, + + # Validação do modelo de normalização + "norm_median_rel_error_warning": 0.030, + "norm_median_rel_error_bad": 0.060, + "norm_p95_rel_error_warning": 0.050, + "norm_p95_rel_error_bad": 0.090, + + # Validação independente no reference point + "validation_target_rel_error_warning": 0.050, + "validation_target_rel_error_bad": 0.100, + "validation_temporal_cv_warning": 0.010, + "validation_temporal_cv_bad": 0.025, + "validation_sat_warning_pct": 0.10, + "validation_sat_bad_pct": 0.50, + + # Diferença entre target real escolhido e faixa observada + "reference_extrapolation_margin": 0.03, + + # Controles + "exposure_rel_tolerance": 0.012, + "exposure_abs_tolerance_us": 25, + "iso_tolerance": 5, + + # Sync visual, não é requisito rígido da regressão + "sync_warning_ms": 40.0, +} + + +DEFAULT_SWEEP_FACTORS = ( + 0.30, + 0.42, + 0.55, + 0.70, + 0.85, + 1.00, + 1.18, + 1.38, + 1.58, +) + + +# ============================================================ +# Helpers +# ============================================================ + +def now_str() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def session_stamp() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f") + + +def ensure_dir(path: str | Path): + Path(path).mkdir(parents=True, exist_ok=True) + + +def save_json_atomic(path: str | Path, data: dict): + p = Path(path) + ensure_dir(p.parent) + + tmp = p.with_suffix(p.suffix + ".tmp") + + with tmp.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.flush() + os.fsync(f.fileno()) + + os.replace(tmp, p) + + +def load_json(path: str | Path) -> dict: + p = Path(path) + + if not p.is_file(): + return {} + + with p.open("r", encoding="utf-8") as f: + return json.load(f) + + +def sha256_file(path: str | Path) -> str: + h = hashlib.sha256() + + with open(path, "rb") as f: + while True: + chunk = f.read(1024 * 1024) + if not chunk: + break + h.update(chunk) + + return h.hexdigest() + + +def safe_int(v, default=None): + try: + if v is None: + return default + return int(v) + except Exception: + return default + + +def safe_float(v, default=None): + try: + if v is None: + return default + return float(v) + except Exception: + return default + + +def clamp(v, lo, hi): + return max(lo, min(hi, v)) + + +def status_rank(s: str) -> int: + return { + "good": 0, + "warning": 1, + "bad": 2, + }.get(str(s).lower(), 2) + + +def merge_status(a: str, b: str) -> str: + return a if status_rank(a) >= status_rank(b) else b + + +def overlay_hud( + img, + lines, + x=12, + y=24, + font_scale=0.50, + line_step=20, + color=(255, 255, 255), +): + yy = int(y) + h = img.shape[0] + + for line in lines: + if yy > h - 6: + break + + text = str(line) + + cv2.putText( + img, + text, + (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, + (0, 0, 0), + 3, + cv2.LINE_AA, + ) + + cv2.putText( + img, + text, + (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, + color, + 1, + cv2.LINE_AA, + ) + + yy += int(line_step) + + +def socket_name(socket) -> str: + name = getattr(socket, "name", None) + + if name: + return str(name) + + text = str(socket) + + for candidate in ( + "CAM_A", + "CAM_B", + "CAM_C", + "CAM_D", + ): + if candidate in text: + return candidate + + return text + + +def get_socket(name: str): + mapping = { + "CAM_A": dai.CameraBoardSocket.CAM_A, + "CAM_B": dai.CameraBoardSocket.CAM_B, + "CAM_C": dai.CameraBoardSocket.CAM_C, + } + + key = str(name).upper() + + if key not in mapping: + raise ValueError( + f"Socket inválido: {name}" + ) + + return mapping[key] + + +def enum_if_exists(enum_cls, name: str): + return getattr( + enum_cls, + name, + None, + ) + + +def supported_type_strings(feature): + return [ + str(x).upper() + for x in ( + getattr(feature, "supportedTypes", []) + or [] + ) + ] + + +def feature_is_color(feature) -> bool: + types = supported_type_strings(feature) + sensor = str( + getattr(feature, "sensorName", "") + or "" + ).upper() + + if any("COLOR" in x for x in types): + return True + + if any("MONO" in x for x in types): + return False + + return sensor in { + "OV9782", + "AR0234", + } + + +def feature_is_mono(feature) -> bool: + types = supported_type_strings(feature) + + if any("MONO" in x for x in types): + return True + + if any("COLOR" in x for x in types): + return False + + return not feature_is_color(feature) + + +def resize_panel( + img, + width, + height, +): + return cv2.resize( + img, + ( + int(width), + int(height), + ), + interpolation=cv2.INTER_AREA, + ) + + +def robust_median(arr: np.ndarray) -> float: + f = np.asarray( + arr, + dtype=np.float32, + ) + + f = f[ + np.isfinite(f) + ] + + if f.size == 0: + return 0.0 + + return float( + np.median(f) + ) + + +def temporal_cv(values) -> float: + arr = np.asarray( + list(values), + dtype=np.float64, + ) + + if arr.size < 3: + return float("inf") + + mean = float( + np.mean(arr) + ) + + if abs(mean) < 1e-12: + return float("inf") + + return float( + np.std(arr) + / abs(mean) + ) + + +def central_roi( + img01: np.ndarray, + frac: float, +): + h, w = img01.shape[:2] + + frac = clamp( + float(frac), + 0.05, + 1.0, + ) + + rw = max( + 8, + int(round(w * frac)), + ) + + rh = max( + 8, + int(round(h * frac)), + ) + + x0 = (w - rw) // 2 + y0 = (h - rh) // 2 + + return img01[ + y0:y0 + rh, + x0:x0 + rw, + ], ( + x0, + y0, + x0 + rw, + y0 + rh, + ) + + +def image_stats( + img01: np.ndarray, + roi_frac: float, +): + roi, rect = central_roi( + img01, + roi_frac, + ) + + a = np.asarray( + roi, + dtype=np.float32, + ) + + f = a[ + np.isfinite(a) + ] + + if f.size == 0: + return { + "valid": False, + "roi_rect": list(rect), + } + + return { + "valid": True, + "roi_rect": list(rect), + "pixels": int(f.size), + "mean": float( + np.mean(f) + ), + "std": float( + np.std(f) + ), + "p01": float( + np.percentile(f, 1) + ), + "p05": float( + np.percentile(f, 5) + ), + "p50": float( + np.percentile(f, 50) + ), + "p95": float( + np.percentile(f, 95) + ), + "p99": float( + np.percentile(f, 99) + ), + "sat_pct": float( + (f >= 0.995).mean() + * 100.0 + ), + "dark_pct": float( + (f <= 0.010).mean() + * 100.0 + ), + } + + +def gray_preview( + img01, + roi_frac, +): + vis = np.clip( + img01 * 255.0, + 0, + 255, + ).astype(np.uint8) + + bgr = cv2.cvtColor( + vis, + cv2.COLOR_GRAY2BGR, + ) + + _, rect = central_roi( + img01, + roi_frac, + ) + + x0, y0, x1, y1 = rect + + cv2.rectangle( + bgr, + (x0, y0), + (x1, y1), + (0, 255, 255), + 2, + ) + + return bgr + + +# ============================================================ +# RAW decode +# ============================================================ + +def unpack_raw10( + data, + width: int, + height: int, + stride: Optional[int] = None, +) -> np.ndarray: + width = int(width) + height = int(height) + + if width % 4 != 0: + raise ValueError( + f"RAW10 width não múltiplo de 4: {width}" + ) + + arr = np.asarray( + data, + dtype=np.uint8, + ).reshape(-1) + + row_bytes = ( + width // 4 + ) * 5 + + if stride is None or int(stride) < row_bytes: + stride = row_bytes + + stride = int(stride) + expected = stride * height + + if arr.size < expected: + raise RuntimeError( + f"RAW10 curto: {arr.size} < {expected}" + ) + + rows = arr[ + :expected + ].reshape( + height, + stride, + ) + + payload = rows[ + :, + :row_bytes, + ] + + g = payload.reshape( + height, + width // 4, + 5, + ) + + out = np.empty( + ( + height, + width // 4, + 4, + ), + dtype=np.uint16, + ) + + out[:, :, 0] = ( + g[:, :, 0].astype(np.uint16) << 2 + ) | ( + g[:, :, 4] & 0x03 + ) + + out[:, :, 1] = ( + g[:, :, 1].astype(np.uint16) << 2 + ) | ( + (g[:, :, 4] >> 2) + & 0x03 + ) + + out[:, :, 2] = ( + g[:, :, 2].astype(np.uint16) << 2 + ) | ( + (g[:, :, 4] >> 4) + & 0x03 + ) + + out[:, :, 3] = ( + g[:, :, 3].astype(np.uint16) << 2 + ) | ( + (g[:, :, 4] >> 6) + & 0x03 + ) + + return np.ascontiguousarray( + out.reshape( + height, + width, + ) + ) + + +def unpack_raw8( + data, + width: int, + height: int, + stride: Optional[int] = None, +) -> np.ndarray: + arr = np.asarray( + data, + dtype=np.uint8, + ).reshape(-1) + + width = int(width) + height = int(height) + + if stride is None or int(stride) < width: + stride = width + + stride = int(stride) + expected = stride * height + + if arr.size < expected: + raise RuntimeError( + f"RAW8 curto: {arr.size} < {expected}" + ) + + rows = arr[ + :expected + ].reshape( + height, + stride, + ) + + return np.ascontiguousarray( + rows[:, :width] + ) + + +def decode_raw_packet( + packet, + spec, +): + width = int( + packet.getWidth() + ) + + height = int( + packet.getHeight() + ) + + if ( + width, + height, + ) != ( + spec.width, + spec.height, + ): + raise RuntimeError( + f"{spec.role}: frame " + f"{width}x{height} != " + f"{spec.width}x{spec.height}" + ) + + stride = None + + try: + stride = int( + packet.getStride() + ) + except Exception: + pass + + raw_type = str( + packet.getType() + ).upper() + + if "RAW10" in raw_type: + raw = unpack_raw10( + packet.getData(), + width, + height, + stride, + ) + + img01 = ( + raw.astype(np.float32) + / 1023.0 + ) + + bit_depth = 10 + + elif ( + "RAW8" in raw_type + or "GRAY8" in raw_type + ): + raw = unpack_raw8( + packet.getData(), + width, + height, + stride, + ) + + img01 = ( + raw.astype(np.float32) + / 255.0 + ) + + bit_depth = 8 + + else: + # Último fallback para MonoCamera.out-like payload, + # caso a API não nomeie RAW8 claramente. + data = np.asarray( + packet.getData(), + dtype=np.uint8, + ).reshape(-1) + + if data.size >= width * height: + raw = unpack_raw8( + data, + width, + height, + stride, + ) + + img01 = ( + raw.astype(np.float32) + / 255.0 + ) + + bit_depth = 8 + else: + raise RuntimeError( + f"{spec.role}: tipo RAW não suportado: " + f"{raw_type}" + ) + + return { + "image01": np.clip( + img01, + 0.0, + 1.0, + ), + "raw_type": raw_type, + "bit_depth": bit_depth, + "stride": stride, + } + + +# ============================================================ +# Hardware discovery +# ============================================================ + +@dataclass +class CameraSpec: + role: str + socket_name: str + sensor_name: str + width: int + height: int + resolution_name: str + stream_name: str + control_name: str + is_color: bool + + +def discover_cameras( + mx_id: Optional[str], +): + device_info = ( + dai.DeviceInfo(mx_id) + if mx_id + else None + ) + + ctx = ( + dai.Device(device_info) + if device_info is not None + else dai.Device() + ) + + with ctx as device: + actual_mx = None + + for method_name in ( + "getMxId", + "getDeviceId", + ): + fn = getattr( + device, + method_name, + None, + ) + + if callable(fn): + try: + value = fn() + + if value: + actual_mx = str( + value + ) + break + + except Exception: + pass + + usb_speed = None + + try: + usb_speed = str( + device.getUsbSpeed() + ) + except Exception: + pass + + rows = [] + + for feature in ( + device + .getConnectedCameraFeatures() + ): + rows.append({ + "socket_name": socket_name( + feature.socket + ), + "sensor_name": str( + feature.sensorName + or "" + ).upper(), + "width": int( + getattr( + feature, + "width", + 0, + ) or 0 + ), + "height": int( + getattr( + feature, + "height", + 0, + ) or 0 + ), + "supported_types": ( + supported_type_strings( + feature + ) + ), + "is_color": ( + feature_is_color( + feature + ) + ), + "is_mono": ( + feature_is_mono( + feature + ) + ), + }) + + return rows, actual_mx, usb_speed + + +def validate_product_topology( + camera_rows, +): + by_socket = { + row["socket_name"]: row + for row in camera_rows + } + + errors = [] + + for role in ROLES: + contract = ( + PRODUCT_TOPOLOGY[role] + ) + + socket = contract[ + "socket" + ] + + row = by_socket.get( + socket + ) + + if row is None: + errors.append( + f"{role}: {socket} ausente" + ) + continue + + sensor = row[ + "sensor_name" + ] + + if sensor not in contract[ + "allowed_sensors" + ]: + errors.append( + f"{role}: {socket} " + f"sensor={sensor}, " + f"esperado=" + f"{contract['allowed_sensors']}" + ) + + if ( + role == "rgb" + and not row["is_color"] + ): + errors.append( + f"RGB {socket}/{sensor} " + "não anunciado COLOR" + ) + + if ( + role in ("re", "nir") + and not row["is_mono"] + ): + errors.append( + f"{role.upper()} " + f"{socket}/{sensor} " + "não anunciado MONO" + ) + + if errors: + raise RuntimeError( + "Topologia inválida:\n - " + + "\n - ".join(errors) + ) + + return { + "valid": True, + "errors": [], + } + + +def build_specs( + camera_rows, +): + by_socket = { + row["socket_name"]: row + for row in camera_rows + } + + specs = {} + + for role in ROLES: + socket = ( + PRODUCT_TOPOLOGY[role] + ["socket"] + ) + + row = by_socket[ + socket + ] + + sensor = row[ + "sensor_name" + ] + + cfg = ( + RGB_SENSOR_MODES[sensor] + if role == "rgb" + else MONO_SENSOR_MODES[sensor] + ) + + if ( + row["width"] + and row["height"] + and ( + row["width"], + row["height"], + ) + != ( + cfg["width"], + cfg["height"], + ) + ): + raise RuntimeError( + f"{socket}/{sensor}: " + f"anunciado " + f"{row['width']}x{row['height']}, " + f"esperado " + f"{cfg['width']}x{cfg['height']}" + ) + + specs[role] = CameraSpec( + role=role, + socket_name=socket, + sensor_name=sensor, + width=int( + cfg["width"] + ), + height=int( + cfg["height"] + ), + resolution_name=( + cfg["resolution_enum"] + ), + stream_name=( + f"rad_raw_{role}" + ), + control_name=( + f"rad_ctrl_{role}" + ), + is_color=( + role == "rgb" + ), + ) + + return specs + + +def hardware_signature( + specs, +): + return { + role: { + "socket": specs[ + role + ].socket_name, + "sensor": specs[ + role + ].sensor_name, + "size": [ + specs[role].width, + specs[role].height, + ], + } + for role in ROLES + } + + +# ============================================================ +# Pipeline +# ============================================================ + +def build_pipeline( + specs, + args, +): + pipeline = dai.Pipeline() + + for role in ROLES: + spec = specs[role] + socket = get_socket( + spec.socket_name + ) + + if spec.is_color: + cam = ( + pipeline + .createColorCamera() + ) + + cam.setBoardSocket( + socket + ) + + enum_value = enum_if_exists( + dai.ColorCameraProperties + .SensorResolution, + spec.resolution_name, + ) + + if enum_value is None: + raise RuntimeError( + "DepthAI sem " + f"{spec.resolution_name}" + ) + + cam.setResolution( + enum_value + ) + + cam.setFps( + float(args.fps) + ) + + cam.setInterleaved( + False + ) + + else: + cam = ( + pipeline + .createMonoCamera() + ) + + cam.setBoardSocket( + socket + ) + + enum_value = enum_if_exists( + dai.MonoCameraProperties + .SensorResolution, + spec.resolution_name, + ) + + if enum_value is None: + raise RuntimeError( + "DepthAI sem " + f"{spec.resolution_name}" + ) + + cam.setResolution( + enum_value + ) + + cam.setFps( + float(args.fps) + ) + + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + + if not hasattr( + cam, + "raw", + ): + raise RuntimeError( + f"{spec.socket_name}/" + f"{spec.sensor_name} " + "não expõe saída raw." + ) + + xout = ( + pipeline + .createXLinkOut() + ) + + xout.setStreamName( + spec.stream_name + ) + + cam.raw.link( + xout.input + ) + + xin = ( + pipeline + .createXLinkIn() + ) + + xin.setStreamName( + spec.control_name + ) + + if not hasattr( + cam, + "inputControl", + ): + raise RuntimeError( + f"{spec.socket_name} " + "sem inputControl" + ) + + xin.out.link( + cam.inputControl + ) + + return pipeline + + +# ============================================================ +# Controles +# ============================================================ + +@dataclass +class LockedControl: + role: str + exposure_time_us: int + sensitivity_iso: int + source: str + + +def packet_controls( + packet, +): + exposure_us = None + iso = None + sequence_num = None + timestamp_s = None + + try: + exp = ( + packet + .getExposureTime() + ) + + if hasattr( + exp, + "total_seconds", + ): + exposure_us = int( + round( + exp.total_seconds() + * 1_000_000 + ) + ) + else: + exposure_us = int(exp) + + except Exception: + pass + + try: + iso = int( + packet.getSensitivity() + ) + except Exception: + pass + + try: + sequence_num = int( + packet.getSequenceNum() + ) + except Exception: + pass + + for method_name in ( + "getTimestampDevice", + "getTimestamp", + ): + fn = getattr( + packet, + method_name, + None, + ) + + if callable(fn): + try: + ts = fn() + + if hasattr( + ts, + "total_seconds", + ): + timestamp_s = float( + ts.total_seconds() + ) + else: + timestamp_s = float( + ts + ) + + break + + except Exception: + pass + + return { + "exposure_time_us": ( + exposure_us + ), + "sensitivity_iso": iso, + "sequence_num": ( + sequence_num + ), + "timestamp_s": ( + timestamp_s + ), + } + + +def controls_match( + actual, + target: LockedControl, + qa, +): + exp = safe_int( + actual.get( + "exposure_time_us" + ), + None, + ) + + iso = safe_int( + actual.get( + "sensitivity_iso" + ), + None, + ) + + if exp is None or iso is None: + return False + + exp_tol = max( + int( + qa[ + "exposure_abs_tolerance_us" + ] + ), + int( + round( + target.exposure_time_us + * qa[ + "exposure_rel_tolerance" + ] + ) + ), + ) + + return ( + abs( + exp + - target.exposure_time_us + ) + <= exp_tol + and abs( + iso + - target.sensitivity_iso + ) + <= int( + qa[ + "iso_tolerance" + ] + ) + ) + + +def send_manual_control( + queue, + lock: LockedControl, +): + ctrl = dai.CameraControl() + + ctrl.setManualExposure( + int( + lock.exposure_time_us + ), + int( + lock.sensitivity_iso + ), + ) + + queue.send( + ctrl + ) + + +# ============================================================ +# Runtime +# ============================================================ + +class RadiometricRuntime: + def __init__( + self, + device, + specs, + args, + qa, + ): + self.device = device + self.specs = specs + self.args = args + self.qa = qa + + self.out_q = { + role: ( + device + .getOutputQueue( + name=spec.stream_name, + maxSize=3, + blocking=False, + ) + ) + for role, spec in ( + specs.items() + ) + } + + self.ctrl_q = { + role: ( + device + .getInputQueue( + spec.control_name + ) + ) + for role, spec in ( + specs.items() + ) + } + + self.images = { + role: None + for role in ROLES + } + + self.controls = { + role: {} + for role in ROLES + } + + self.raw_meta = { + role: {} + for role in ROLES + } + + self.last_seq = { + role: None + for role in ROLES + } + + self.fps = { + role: 0.0 + for role in ROLES + } + + self._fps_count = { + role: 0 + for role in ROLES + } + + self._fps_t0 = { + role: time.time() + for role in ROLES + } + + self.temporal_p50 = { + role: deque( + maxlen=20 + ) + for role in ROLES + } + + def poll(self): + fresh = set() + + for role in ROLES: + packet = ( + self.out_q[role] + .tryGet() + ) + + if packet is None: + continue + + ctrl = packet_controls( + packet + ) + + seq = ctrl.get( + "sequence_num" + ) + + if ( + seq is not None + and seq + == self.last_seq[role] + ): + continue + + decoded = decode_raw_packet( + packet, + self.specs[role], + ) + + self.images[role] = ( + decoded["image01"] + ) + + self.controls[role] = ( + ctrl + ) + + self.raw_meta[role] = { + "raw_type": decoded[ + "raw_type" + ], + "bit_depth": decoded[ + "bit_depth" + ], + "stride": decoded[ + "stride" + ], + } + + self.last_seq[role] = seq + + st = image_stats( + self.images[role], + self.args.roi_frac, + ) + + if st.get("valid"): + self.temporal_p50[ + role + ].append( + st["p50"] + ) + + fresh.add(role) + + self._fps_count[ + role + ] += 1 + + dt = ( + time.time() + - self._fps_t0[role] + ) + + if dt >= 1.0: + self.fps[role] = ( + self._fps_count[ + role + ] + / dt + ) + + self._fps_count[ + role + ] = 0 + + self._fps_t0[ + role + ] = time.time() + + return fresh + + def wait_all( + self, + timeout=8.0, + ): + t0 = time.time() + + while ( + time.time() - t0 + < timeout + ): + self.poll() + + if all( + self.images[ + role + ] is not None + for role in ROLES + ): + return + + time.sleep( + 0.003 + ) + + missing = [ + role + for role in ROLES + if self.images[ + role + ] is None + ] + + raise TimeoutError( + f"Sem frames: {missing}" + ) + + def send_controls( + self, + controls_by_role, + ): + for role in ROLES: + send_manual_control( + self.ctrl_q[role], + controls_by_role[ + role + ], + ) + + def verify_controls( + self, + controls_by_role, + settle_frames=5, + timeout=10.0, + ): + matched = { + role: 0 + for role in ROLES + } + + seen_seq = dict( + self.last_seq + ) + + t0 = time.time() + + while ( + time.time() - t0 + < timeout + ): + fresh = self.poll() + + for role in fresh: + seq = ( + self.last_seq[ + role + ] + ) + + if ( + seq + == seen_seq.get( + role + ) + ): + continue + + seen_seq[ + role + ] = seq + + if controls_match( + self.controls[ + role + ], + controls_by_role[ + role + ], + self.qa, + ): + matched[ + role + ] += 1 + else: + matched[ + role + ] = 0 + + if all( + matched[role] + >= int( + settle_frames + ) + for role in ROLES + ): + return + + time.sleep( + 0.002 + ) + + raise RuntimeError( + "Controles manuais " + "não estabilizaram: " + f"{matched}" + ) + + def capture_level( + self, + frames_count, + controls_by_role, + ): + values = { + role: [] + for role in ROLES + } + + p95_values = { + role: [] + for role in ROLES + } + + sat_values = { + role: [] + for role in ROLES + } + + dark_values = { + role: [] + for role in ROLES + } + + latest_stats = { + role: None + for role in ROLES + } + + seen = dict( + self.last_seq + ) + + t0 = time.time() + + while any( + len( + values[role] + ) < int( + frames_count + ) + for role in ROLES + ): + fresh = self.poll() + + for role in fresh: + if ( + len(values[role]) + >= int( + frames_count + ) + ): + continue + + seq = self.last_seq[ + role + ] + + if seq == seen.get( + role + ): + continue + + seen[role] = seq + + if not controls_match( + self.controls[ + role + ], + controls_by_role[ + role + ], + self.qa, + ): + raise RuntimeError( + f"{role}: " + "controle mudou durante " + "captura" + ) + + st = image_stats( + self.images[ + role + ], + self.args.roi_frac, + ) + + if not st.get( + "valid" + ): + continue + + values[role].append( + float( + st["p50"] + ) + ) + + p95_values[ + role + ].append( + float( + st["p95"] + ) + ) + + sat_values[ + role + ].append( + float( + st["sat_pct"] + ) + ) + + dark_values[ + role + ].append( + float( + st["dark_pct"] + ) + ) + + latest_stats[ + role + ] = st + + if ( + time.time() - t0 + > 12.0 + ): + raise TimeoutError( + "Timeout capturando " + "nível do sweep." + ) + + time.sleep( + 0.001 + ) + + out = {} + + for role in ROLES: + arr = np.asarray( + values[role], + dtype=np.float64, + ) + + out[role] = { + "frames": int( + arr.size + ), + "p50": float( + np.median( + arr + ) + ), + "p50_mean": float( + np.mean( + arr + ) + ), + "p50_std": float( + np.std( + arr + ) + ), + "p50_temporal_cv": float( + np.std(arr) + / max( + abs( + np.mean( + arr + ) + ), + 1e-12, + ) + ), + "p95": float( + np.median( + np.asarray( + p95_values[ + role + ] + ) + ) + ), + "sat_pct": float( + np.median( + np.asarray( + sat_values[ + role + ] + ) + ) + ), + "dark_pct": float( + np.median( + np.asarray( + dark_values[ + role + ] + ) + ) + ), + "last_stats": ( + latest_stats[ + role + ] + ), + } + + return out + + +# ============================================================ +# Preflight +# ============================================================ + +def preflight_report( + runtime, + qa, +): + result = { + "status": "good", + "roles": {}, + "reasons": [], + } + + for role in ROLES: + img = runtime.images[ + role + ] + + if img is None: + result[ + "status" + ] = "bad" + + result[ + "roles" + ][role] = { + "status": "bad", + "reasons": [ + "sem_frame" + ], + } + + continue + + st = image_stats( + img, + runtime.args.roi_frac, + ) + + cv = temporal_cv( + runtime.temporal_p50[ + role + ] + ) + + status = "good" + reasons = [] + + p50 = st[ + "p50" + ] + + if ( + p50 + < qa[ + "preflight_p50_min" + ] + ): + status = "bad" + reasons.append( + f"p50_low:{p50:.3f}" + ) + + elif ( + p50 + > qa[ + "preflight_p50_max" + ] + ): + status = "bad" + reasons.append( + f"p50_high:{p50:.3f}" + ) + + if ( + st["sat_pct"] + > qa[ + "preflight_sat_bad_pct" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + "sat_bad:" + f"{st['sat_pct']:.3f}%" + ) + + elif ( + st["sat_pct"] + > qa[ + "preflight_sat_warning_pct" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + "sat_warn:" + f"{st['sat_pct']:.3f}%" + ) + + if ( + st["dark_pct"] + > qa[ + "preflight_dark_bad_pct" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + "dark_bad:" + f"{st['dark_pct']:.3f}%" + ) + + elif ( + st["dark_pct"] + > qa[ + "preflight_dark_warning_pct" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + "dark_warn:" + f"{st['dark_pct']:.3f}%" + ) + + if math.isfinite(cv): + if ( + cv + > qa[ + "preflight_temporal_cv_bad" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + f"temporal_cv_bad:{cv:.4f}" + ) + + elif ( + cv + > qa[ + "preflight_temporal_cv_warning" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + f"temporal_cv_warn:{cv:.4f}" + ) + + result[ + "roles" + ][role] = { + "status": status, + "stats": st, + "temporal_cv": ( + None + if not math.isfinite( + cv + ) + else float(cv) + ), + "controls": dict( + runtime.controls[ + role + ] + ), + "raw_meta": dict( + runtime.raw_meta[ + role + ] + ), + "reasons": reasons, + } + + result[ + "status" + ] = merge_status( + result[ + "status" + ], + status, + ) + + result[ + "reasons" + ].extend( + [ + f"{role}:{x}" + for x in reasons + ] + ) + + return result + + +def choose_base_equivalent_exposure( + runtime, + calibration_iso, +): + """ + Converte o ponto encontrado pelo AE para exposição equivalente no ISO + de calibração: + + factor = exp * ISO/100 + exp_cal = factor / (ISO_cal/100) + """ + out = {} + + for role in ROLES: + ctrl = runtime.controls[ + role + ] + + exp = safe_float( + ctrl.get( + "exposure_time_us" + ), + None, + ) + + iso = safe_float( + ctrl.get( + "sensitivity_iso" + ), + None, + ) + + if ( + exp is None + or iso is None + or exp <= 0 + or iso <= 0 + ): + raise RuntimeError( + f"Sem EXP/ISO válido " + f"para {role}" + ) + + factor = ( + exp + * ( + iso + / ISO_BASE + ) + ) + + cal_gain = ( + float( + calibration_iso + ) + / ISO_BASE + ) + + exp_equiv = ( + factor + / cal_gain + ) + + out[role] = { + "ae_exposure_time_us": ( + float(exp) + ), + "ae_sensitivity_iso": ( + float(iso) + ), + "ae_factor": ( + float(factor) + ), + "calibration_iso": int( + calibration_iso + ), + "equivalent_exposure_us": ( + float( + exp_equiv + ) + ), + } + + return out + + +# ============================================================ +# Sweep +# ============================================================ + +def parse_sweep_factors( + text: str, +): + if not text: + return list( + DEFAULT_SWEEP_FACTORS + ) + + vals = [] + + for token in ( + str(text) + .replace(";", ",") + .split(",") + ): + token = token.strip() + + if not token: + continue + + v = float(token) + + if v <= 0: + raise ValueError( + "Sweep factor deve ser > 0" + ) + + vals.append( + v + ) + + vals = sorted( + set( + round(v, 6) + for v in vals + ) + ) + + return vals + + +def build_sweep_plan( + base_equiv, + factors, + args, +): + """ + Cada nível possui exposição própria por role, mas o mesmo fator relativo + ao ponto de trabalho encontrado pelo AE. + """ + levels = [] + + max_frame_exp = int( + min( + float( + args.max_exposure_us + ), + ( + 1_000_000.0 + / max( + float( + args.fps + ), + 1.0, + ) + ) + * float( + args.frame_period_fraction + ), + ) + ) + + min_exp = int( + args.min_exposure_us + ) + + for factor in factors: + controls = {} + + for role in ROLES: + base = float( + base_equiv[ + role + ][ + "equivalent_exposure_us" + ] + ) + + exp = int( + round( + base + * float( + factor + ) + ) + ) + + exp = int( + clamp( + exp, + min_exp, + max_frame_exp, + ) + ) + + controls[role] = LockedControl( + role=role, + exposure_time_us=exp, + sensitivity_iso=int( + args.calibration_iso + ), + source=( + "radiometric_sweep" + ), + ) + + signature = tuple( + controls[ + role + ].exposure_time_us + for role in ROLES + ) + + if ( + levels + and tuple( + levels[-1][ + "controls" + ][role] + .exposure_time_us + for role in ROLES + ) + == signature + ): + continue + + levels.append({ + "factor": float( + factor + ), + "controls": controls, + }) + + return levels, { + "min_exposure_us": min_exp, + "max_exposure_us": max_frame_exp, + } + + +def show_progress_board( + runtime, + title, + lines, + args, +): + pw = int( + args.panel_width + ) + + ph = int( + args.panel_height + ) + + panels = {} + + for role in ROLES: + img = runtime.images[ + role + ] + + if img is None: + panel = np.zeros( + ( + ph, + pw, + 3, + ), + dtype=np.uint8, + ) + else: + view = gray_preview( + img, + args.roi_frac, + ) + + panel = resize_panel( + view, + pw, + ph, + ) + + st = ( + image_stats( + img, + args.roi_frac, + ) + if img is not None + else {} + ) + + ctrl = runtime.controls[ + role + ] + + overlay_hud( + panel, + [ + f"{role.upper()} | " + f"{runtime.specs[role].sensor_name}", + f"RAW " + f"{runtime.specs[role].width}x" + f"{runtime.specs[role].height}", + f"p50={st.get('p50', 0):.3f} " + f"p95={st.get('p95', 0):.3f}", + f"sat={st.get('sat_pct', 0):.3f}%", + f"EXP={ctrl.get('exposure_time_us')}us " + f"ISO={ctrl.get('sensitivity_iso')}", + ], + x=8, + y=18, + font_scale=0.39, + line_step=17, + ) + + panels[ + role + ] = panel + + data = np.zeros( + ( + ph, + pw, + 3, + ), + dtype=np.uint8, + ) + + overlay_hud( + data, + [ + title, + "", + *lines, + ], + x=14, + y=25, + font_scale=0.43, + line_step=19, + ) + + return np.vstack([ + np.hstack([ + panels["rgb"], + panels["re"], + ]), + np.hstack([ + panels["nir"], + data, + ]), + ]) + + +# ============================================================ +# Regression / calibration math +# ============================================================ + +def linear_fit( + x, + y, +): + x = np.asarray( + x, + dtype=np.float64, + ) + + y = np.asarray( + y, + dtype=np.float64, + ) + + if x.size < 2: + raise ValueError( + "Poucos pontos para regressão" + ) + + A = np.vstack([ + x, + np.ones_like(x), + ]).T + + slope, intercept = ( + np.linalg.lstsq( + A, + y, + rcond=None, + )[0] + ) + + pred = ( + slope * x + + intercept + ) + + resid = y - pred + + ss_res = float( + np.sum( + resid ** 2 + ) + ) + + ss_tot = float( + np.sum( + ( + y + - np.mean(y) + ) ** 2 + ) + ) + + r2 = ( + 1.0 + if ss_tot <= 1e-18 + else 1.0 + - ss_res + / ss_tot + ) + + return { + "slope": float( + slope + ), + "intercept": float( + intercept + ), + "r2": float( + r2 + ), + "prediction": pred, + "residual": resid, + } + + +def describe( + values, +): + arr = np.asarray( + list(values), + dtype=np.float64, + ) + + if arr.size == 0: + return { + "count": 0, + } + + return { + "count": int( + arr.size + ), + "min": float( + np.min(arr) + ), + "p05": float( + np.percentile( + arr, + 5, + ) + ), + "p50": float( + np.percentile( + arr, + 50, + ) + ), + "p95": float( + np.percentile( + arr, + 95, + ) + ), + "max": float( + np.max(arr) + ), + "mean": float( + np.mean(arr) + ), + "std": float( + np.std(arr) + ), + } + + +def analyze_role_sweep( + sweep_rows, + role, + target_p50, + args, + qa, +): + all_points = [] + + for row in sweep_rows: + r = row[ + "roles" + ][role] + + ctrl = row[ + "controls" + ][role] + + exposure = float( + ctrl[ + "exposure_time_us" + ] + ) + + iso = float( + ctrl[ + "sensitivity_iso" + ] + ) + + actual_factor = ( + exposure + * ( + iso + / ISO_BASE + ) + ) + + p50 = float( + r["p50"] + ) + + point = { + "level_index": int( + row[ + "level_index" + ] + ), + "sweep_factor": float( + row[ + "sweep_factor" + ] + ), + "exposure_time_us": exposure, + "sensitivity_iso": iso, + "actual_factor": actual_factor, + "p50": p50, + "p95": float( + r["p95"] + ), + "sat_pct": float( + r["sat_pct"] + ), + "dark_pct": float( + r["dark_pct"] + ), + "temporal_cv": float( + r[ + "p50_temporal_cv" + ] + ), + } + + valid = ( + p50 + >= qa[ + "valid_p50_min" + ] + and p50 + <= qa[ + "valid_p50_max" + ] + and point[ + "sat_pct" + ] + <= qa[ + "valid_sat_max_pct" + ] + and point[ + "temporal_cv" + ] + <= qa[ + "level_temporal_cv_bad" + ] + ) + + point["valid"] = bool( + valid + ) + + all_points.append( + point + ) + + valid_points = [ + p + for p in all_points + if p["valid"] + ] + + result = { + "status": "good", + "reasons": [], + "role": role, + "target_p50": float( + target_p50 + ), + "all_points": all_points, + "valid_points": valid_points, + } + + if ( + len(valid_points) + < qa[ + "min_valid_levels" + ] + ): + result[ + "status" + ] = "bad" + + result[ + "reasons" + ].append( + "valid_levels_bad:" + f"{len(valid_points)}" + ) + + return result + + x = np.asarray( + [ + p[ + "actual_factor" + ] + for p in valid_points + ], + dtype=np.float64, + ) + + y = np.asarray( + [ + p["p50"] + for p in valid_points + ], + dtype=np.float64, + ) + + fit = linear_fit( + x, + y, + ) + + slope = float( + fit["slope"] + ) + + intercept = float( + fit["intercept"] + ) + + if slope <= 0: + result[ + "status" + ] = "bad" + + result[ + "reasons" + ].append( + f"negative_slope:{slope}" + ) + + return result + + reference_factor = ( + float(target_p50) + - intercept + ) / slope + + x_min = float( + np.min(x) + ) + + x_max = float( + np.max(x) + ) + + extrap_margin = float( + qa[ + "reference_extrapolation_margin" + ] + ) + + if ( + reference_factor + < x_min + * ( + 1.0 + - extrap_margin + ) + or reference_factor + > x_max + * ( + 1.0 + + extrap_margin + ) + ): + result[ + "status" + ] = "bad" + + result[ + "reasons" + ].append( + "reference_outside_measured_range:" + f"{reference_factor:.1f}" + ) + + reference_factor = float( + clamp( + reference_factor, + x_min, + x_max, + ) + ) + + reference_iso = int( + args.calibration_iso + ) + + reference_exposure_us = ( + reference_factor + / ( + reference_iso + / ISO_BASE + ) + ) + + # Testa exatamente o modelo utilizado no runtime: + # normalized = measured * reference_factor / actual_factor + normalized_values = ( + y + * reference_factor + / x + ) + + ref_response = ( + slope + * reference_factor + + intercept + ) + + rel_errors = np.abs( + normalized_values + - ref_response + ) / max( + abs( + ref_response + ), + 1e-9, + ) + + intercept_fraction = ( + abs( + intercept + ) + / max( + abs( + ref_response + ), + 1e-9, + ) + ) + + status = result[ + "status" + ] + + reasons = list( + result[ + "reasons" + ] + ) + + r2 = float( + fit["r2"] + ) + + if r2 < qa[ + "r2_bad" + ]: + status = merge_status( + status, + "bad", + ) + + reasons.append( + f"r2_bad:{r2:.6f}" + ) + + elif r2 < qa[ + "r2_warning" + ]: + status = merge_status( + status, + "warning", + ) + + reasons.append( + f"r2_warn:{r2:.6f}" + ) + + if ( + intercept_fraction + > qa[ + "intercept_fraction_bad" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + "intercept_fraction_bad:" + f"{intercept_fraction:.4f}" + ) + + elif ( + intercept_fraction + > qa[ + "intercept_fraction_warning" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + "intercept_fraction_warn:" + f"{intercept_fraction:.4f}" + ) + + rel_med = float( + np.median( + rel_errors + ) + ) + + rel_p95 = float( + np.percentile( + rel_errors, + 95, + ) + ) + + if ( + rel_med + > qa[ + "norm_median_rel_error_bad" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + "norm_med_rel_bad:" + f"{rel_med:.4f}" + ) + + elif ( + rel_med + > qa[ + "norm_median_rel_error_warning" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + "norm_med_rel_warn:" + f"{rel_med:.4f}" + ) + + if ( + rel_p95 + > qa[ + "norm_p95_rel_error_bad" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + "norm_p95_rel_bad:" + f"{rel_p95:.4f}" + ) + + elif ( + rel_p95 + > qa[ + "norm_p95_rel_error_warning" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + "norm_p95_rel_warn:" + f"{rel_p95:.4f}" + ) + + measured_scales = ( + reference_factor + / x + ) + + result.update({ + "status": status, + "reasons": reasons, + "fit": { + "slope_per_factor": slope, + "intercept": intercept, + "r2": r2, + "intercept_fraction_of_reference_response": ( + float( + intercept_fraction + ) + ), + "reference_response_predicted": float( + ref_response + ), + }, + "reference": { + "reference_factor": float( + reference_factor + ), + "exposure_time_us": int( + round( + reference_exposure_us + ) + ), + "sensitivity_iso": int( + reference_iso + ), + "target_p50": float( + target_p50 + ), + }, + "normalization_model_validation": { + "normalized_values": [ + float(x) + for x in normalized_values + ], + "relative_errors": [ + float(x) + for x in rel_errors + ], + "median_relative_error": ( + rel_med + ), + "p95_relative_error": ( + rel_p95 + ), + "max_relative_error": float( + np.max( + rel_errors + ) + ), + }, + "validated_scale_range": { + "min": float( + np.min( + measured_scales + ) + ), + "max": float( + np.max( + measured_scales + ) + ), + }, + "observed_factor_range": { + "min": x_min, + "max": x_max, + }, + }) + + return result + + +# ============================================================ +# Validation at reference +# ============================================================ + +def evaluate_reference_validation( + role_analysis, + validation_stats, + qa, +): + role = role_analysis[ + "role" + ] + + target = float( + role_analysis[ + "target_p50" + ] + ) + + st = validation_stats[ + role + ] + + measured = float( + st["p50"] + ) + + target_rel_error = ( + abs( + measured + - target + ) + / max( + abs( + target + ), + 1e-9, + ) + ) + + temporal = float( + st[ + "p50_temporal_cv" + ] + ) + + sat = float( + st[ + "sat_pct" + ] + ) + + status = "good" + reasons = [] + + if ( + target_rel_error + > qa[ + "validation_target_rel_error_bad" + ] + ): + status = "bad" + + reasons.append( + "target_rel_bad:" + f"{target_rel_error:.4f}" + ) + + elif ( + target_rel_error + > qa[ + "validation_target_rel_error_warning" + ] + ): + status = "warning" + + reasons.append( + "target_rel_warn:" + f"{target_rel_error:.4f}" + ) + + if ( + temporal + > qa[ + "validation_temporal_cv_bad" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + "temporal_cv_bad:" + f"{temporal:.4f}" + ) + + elif ( + temporal + > qa[ + "validation_temporal_cv_warning" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + "temporal_cv_warn:" + f"{temporal:.4f}" + ) + + if ( + sat + > qa[ + "validation_sat_bad_pct" + ] + ): + status = merge_status( + status, + "bad", + ) + + reasons.append( + f"sat_bad:{sat:.4f}%" + ) + + elif ( + sat + > qa[ + "validation_sat_warning_pct" + ] + ): + status = merge_status( + status, + "warning", + ) + + reasons.append( + f"sat_warn:{sat:.4f}%" + ) + + return { + "status": status, + "reasons": reasons, + "target_p50": target, + "measured_p50": measured, + "target_relative_error": ( + float( + target_rel_error + ) + ), + "temporal_cv": temporal, + "sat_pct": sat, + "dark_pct": float( + st[ + "dark_pct" + ] + ), + "p95": float( + st["p95"] + ), + } + + +# ============================================================ +# Plots / CSV +# ============================================================ + +def save_sweep_csv( + path, + sweep_rows, +): + ensure_dir( + Path(path).parent + ) + + fields = [ + "level_index", + "sweep_factor", + "role", + "exposure_time_us", + "sensitivity_iso", + "p50", + "p95", + "sat_pct", + "dark_pct", + "p50_temporal_cv", + ] + + with open( + path, + "w", + newline="", + encoding="utf-8", + ) as f: + writer = csv.DictWriter( + f, + fieldnames=fields, + ) + + writer.writeheader() + + for row in sweep_rows: + for role in ROLES: + ctrl = row[ + "controls" + ][role] + + st = row[ + "roles" + ][role] + + writer.writerow({ + "level_index": ( + row[ + "level_index" + ] + ), + "sweep_factor": ( + row[ + "sweep_factor" + ] + ), + "role": role, + "exposure_time_us": ( + ctrl[ + "exposure_time_us" + ] + ), + "sensitivity_iso": ( + ctrl[ + "sensitivity_iso" + ] + ), + "p50": st[ + "p50" + ], + "p95": st[ + "p95" + ], + "sat_pct": st[ + "sat_pct" + ], + "dark_pct": st[ + "dark_pct" + ], + "p50_temporal_cv": ( + st[ + "p50_temporal_cv" + ] + ), + }) + + +def save_response_plot( + path, + analysis, + width=1000, + height=650, +): + canvas = np.zeros( + ( + height, + width, + 3, + ), + dtype=np.uint8, + ) + + canvas[:] = 25 + + margin_l = 90 + margin_r = 40 + margin_t = 70 + margin_b = 90 + + x0 = margin_l + y0 = height - margin_b + x1 = width - margin_r + y1 = margin_t + + cv2.rectangle( + canvas, + (x0, y1), + (x1, y0), + (180, 180, 180), + 1, + ) + + valid = analysis.get( + "valid_points", + [], + ) + + if not valid or "fit" not in analysis: + overlay_hud( + canvas, + [ + f"{analysis.get('role', '').upper()}", + "Sem modelo válido", + ], + x=40, + y=45, + font_scale=0.7, + line_step=28, + ) + + cv2.imwrite( + str(path), + canvas, + ) + + return + + xs = np.asarray( + [ + p[ + "actual_factor" + ] + for p in valid + ], + dtype=np.float64, + ) + + ys = np.asarray( + [ + p["p50"] + for p in valid + ], + dtype=np.float64, + ) + + xmin = float( + np.min(xs) + ) + + xmax = float( + np.max(xs) + ) + + ymin = 0.0 + + ymax = max( + 0.05, + float( + np.max(ys) + ) + * 1.10, + ) + + def px(x): + return int( + x0 + + ( + float(x) + - xmin + ) + / max( + xmax - xmin, + 1e-9, + ) + * ( + x1 - x0 + ) + ) + + def py(y): + return int( + y0 + - ( + float(y) + - ymin + ) + / max( + ymax - ymin, + 1e-9, + ) + * ( + y0 - y1 + ) + ) + + slope = analysis[ + "fit" + ][ + "slope_per_factor" + ] + + intercept = analysis[ + "fit" + ][ + "intercept" + ] + + line_x = np.linspace( + xmin, + xmax, + 120, + ) + + prev = None + + for xv in line_x: + yv = ( + slope + * xv + + intercept + ) + + pt = ( + px(xv), + py(yv), + ) + + if prev is not None: + cv2.line( + canvas, + prev, + pt, + (180, 180, 180), + 2, + cv2.LINE_AA, + ) + + prev = pt + + for p in analysis[ + "all_points" + ]: + color = ( + (0, 220, 0) + if p["valid"] + else (0, 0, 220) + ) + + cv2.circle( + canvas, + ( + px( + p[ + "actual_factor" + ] + ), + py( + p["p50"] + ), + ), + 6, + color, + -1, + ) + + ref = analysis[ + "reference" + ] + + ref_x = float( + ref[ + "reference_factor" + ] + ) + + cv2.line( + canvas, + ( + px(ref_x), + y1, + ), + ( + px(ref_x), + y0, + ), + (0, 255, 255), + 2, + ) + + overlay_hud( + canvas, + [ + f"{analysis['role'].upper()} RADIOMETRIC RESPONSE", + f"status={analysis['status'].upper()}", + f"R2={analysis['fit']['r2']:.6f}", + f"ref={ref['exposure_time_us']}us ISO={ref['sensitivity_iso']}", + f"norm med err=" + f"{analysis['normalization_model_validation']['median_relative_error']*100:.2f}%", + f"norm p95 err=" + f"{analysis['normalization_model_validation']['p95_relative_error']*100:.2f}%", + ], + x=30, + y=28, + font_scale=0.48, + line_step=21, + ) + + cv2.putText( + canvas, + "Exposure x ISO factor", + ( + width // 2 - 90, + height - 30, + ), + cv2.FONT_HERSHEY_SIMPLEX, + 0.55, + (220, 220, 220), + 1, + cv2.LINE_AA, + ) + + cv2.putText( + canvas, + "RAW p50", + ( + 18, + height // 2, + ), + cv2.FONT_HERSHEY_SIMPLEX, + 0.55, + (220, 220, 220), + 1, + cv2.LINE_AA, + ) + + cv2.imwrite( + str(path), + canvas, + ) + + +# ============================================================ +# Runtime fragment +# ============================================================ + +def build_normalization_fragment( + role_analyses, + args, +): + reference_controls = {} + + scale_limits = {} + + for role in ROLES: + ref = role_analyses[ + role + ][ + "reference" + ] + + reference_controls[ + role + ] = { + "exposure_time_us": int( + ref[ + "exposure_time_us" + ] + ), + "sensitivity_iso": int( + ref[ + "sensitivity_iso" + ] + ), + } + + scale_limits[ + role + ] = { + "min": float( + args.runtime_scale_min + ), + "max": float( + args.runtime_scale_max + ), + } + + scale_limits[ + "default" + ] = { + "min": float( + args.runtime_scale_min + ), + "max": float( + args.runtime_scale_max + ), + } + + return { + "radiometric_normalization": { + "enabled": True, + "method": NORMALIZATION_METHOD, + "apply_stage": ( + "after_dark_before_flat_gain" + ), + "control_source": ( + "stream_meta.frame_controls" + ), + "role_mapping_source": ( + "camera_info" + ), + "factor_model": FACTOR_MODEL, + "iso_base": ISO_BASE, + "reference_mode": "fixed", + "reference_controls": ( + reference_controls + ), + "scale_limits": ( + scale_limits + ), + "missing_controls_policy": ( + "skip" + ), + "invalid_controls_policy": ( + "skip" + ), + "clip_output": False, + "save_debug": True, + } + } + + +# ============================================================ +# Active artifact +# ============================================================ + +def promote_active( + active_path: Path, + report, +): + ensure_dir( + active_path.parent + ) + + if active_path.exists(): + old = load_json( + active_path + ) + + old_sig = old.get( + "hardware_signature" + ) + + new_sig = report.get( + "hardware_signature" + ) + + # Diferente de homografia, aqui trocar sensor RGB é justamente + # um motivo legítimo para recalibrar. Portanto não bloqueamos, + # apenas mantemos backup. + backup = active_path.with_name( + active_path.stem + + f".backup_{session_stamp()}" + + active_path.suffix + ) + + shutil.copy2( + active_path, + backup, + ) + + save_json_atomic( + active_path, + report, + ) + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser( + description=( + "Calibração radiométrica de produção para " + "OV9782/AR0234 + 2x OV9282." + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # Hardware + parser.add_argument( + "--mx-id", + default=None, + ) + + parser.add_argument( + "--fps", + type=float, + default=20.0, + ) + + # Bancada + parser.add_argument( + "--rig-id", + default="", + help="ID da bancada radiométrica.", + ) + + parser.add_argument( + "--target-id", + default="", + help="ID/lote do painel cinza/difuso.", + ) + + parser.add_argument( + "--illumination-id", + default="", + help="ID/configuração da iluminação controlada.", + ) + + parser.add_argument( + "--target-distance-mm", + type=float, + default=None, + ) + + parser.add_argument( + "--warmup-s", + type=float, + default=3.0, + ) + + # ROI + parser.add_argument( + "--roi-frac", + type=float, + default=0.50, + help=( + "Fração central usada para medição, " + "evitando bordas/vignette." + ), + ) + + # Sweep + parser.add_argument( + "--calibration-iso", + type=int, + default=100, + ) + + parser.add_argument( + "--sweep-factors", + default=",".join( + str(x) + for x in DEFAULT_SWEEP_FACTORS + ), + ) + + parser.add_argument( + "--frames-per-level", + type=int, + default=14, + ) + + parser.add_argument( + "--validation-frames", + type=int, + default=24, + ) + + parser.add_argument( + "--settle-frames", + type=int, + default=5, + ) + + parser.add_argument( + "--min-exposure-us", + type=int, + default=100, + ) + + parser.add_argument( + "--max-exposure-us", + type=int, + default=30000, + ) + + parser.add_argument( + "--frame-period-fraction", + type=float, + default=0.85, + help=( + "Limite de exposição como fração do período " + "de frame." + ), + ) + + # Target canônico por role + parser.add_argument( + "--target-p50-rgb", + type=float, + default=0.45, + ) + + parser.add_argument( + "--target-p50-re", + type=float, + default=0.45, + ) + + parser.add_argument( + "--target-p50-nir", + type=float, + default=0.45, + ) + + # Runtime scale policy, mantemos contrato atual amplo. + parser.add_argument( + "--runtime-scale-min", + type=float, + default=0.05, + ) + + parser.add_argument( + "--runtime-scale-max", + type=float, + default=3.0, + ) + + # Política + parser.add_argument( + "--promote-warning", + action="store_true", + ) + + # UI + parser.add_argument( + "--panel-width", + type=int, + default=640, + ) + + parser.add_argument( + "--panel-height", + type=int, + default=400, + ) + + # Output + parser.add_argument( + "--candidate-root", + default=( + "calibration/" + "radiometry_candidates" + ), + ) + + parser.add_argument( + "--active-json", + default=( + "calibration/" + "radiometry_calibration_v5.json" + ), + ) + + # Trace + parser.add_argument( + "--module-id", + default="", + ) + + parser.add_argument( + "--operator", + default="", + ) + + parser.add_argument( + "--notes", + default="", + ) + + args = parser.parse_args() + + if not ( + 0.10 + <= args.roi_frac + <= 0.95 + ): + raise ValueError( + "--roi-frac deve estar " + "entre 0.10 e 0.95" + ) + + if ( + args.calibration_iso + < 50 + ): + raise ValueError( + "--calibration-iso inválido" + ) + + if ( + args.runtime_scale_min + <= 0 + or args.runtime_scale_max + <= args.runtime_scale_min + ): + raise ValueError( + "Runtime scale limits inválidos" + ) + + qa = dict( + DEFAULT_QA + ) + + targets = { + "rgb": float( + args.target_p50_rgb + ), + "re": float( + args.target_p50_re + ), + "nir": float( + args.target_p50_nir + ), + } + + for role, value in targets.items(): + if not ( + 0.10 + <= value + <= 0.80 + ): + raise ValueError( + f"target-p50-{role} " + f"fora de [0.10,0.80]" + ) + + factors = parse_sweep_factors( + args.sweep_factors + ) + + if ( + len(factors) + < qa[ + "min_sweep_levels" + ] + ): + raise ValueError( + "Sweep com poucos fatores" + ) + + sid = session_stamp() + + candidate_dir = ( + Path( + args.candidate_root + ) + / sid + ) + + candidate_report_path = ( + candidate_dir + / "report.json" + ) + + sweep_csv_path = ( + candidate_dir + / "sweep.csv" + ) + + ensure_dir( + candidate_dir + ) + + camera_rows = [] + specs = {} + report = None + sweep_rows = [] + role_analyses = {} + validation_stats = None + + window_name = ( + "Radiometric Calibration - Production" + ) + + cv2.namedWindow( + window_name, + cv2.WINDOW_NORMAL, + ) + + try: + ( + camera_rows, + actual_mx, + usb_speed, + ) = discover_cameras( + args.mx_id + ) + + topology = ( + validate_product_topology( + camera_rows + ) + ) + + specs = build_specs( + camera_rows + ) + + print("=" * 88) + print( + "RADIOMETRIC CALIBRATION - PRODUCTION" + ) + print( + f"MX ID : {actual_mx}" + ) + print( + f"USB : {usb_speed}" + ) + print("-" * 88) + + for role in ROLES: + s = specs[ + role + ] + + print( + f"{s.socket_name} -> " + f"{role.upper():3s} | " + f"{s.sensor_name:8s} | " + f"{s.width}x{s.height}" + ) + + print("=" * 88) + + pipeline = build_pipeline( + specs, + args, + ) + + device_info = ( + dai.DeviceInfo( + actual_mx + ) + if actual_mx + else None + ) + + device_ctx = ( + dai.Device( + pipeline, + device_info, + ) + if device_info is not None + else dai.Device( + pipeline + ) + ) + + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + "status": "running", + "promoted": False, + "depthai_version": getattr( + dai, + "__version__", + "unknown", + ), + "opencv_version": ( + cv2.__version__ + ), + "device_mx_id": ( + actual_mx + ), + "usb_speed": usb_speed, + "topology": topology, + "camera_inventory": ( + camera_rows + ), + "hardware_signature": ( + hardware_signature( + specs + ) + ), + "calibration_domain": ( + CALIBRATION_DOMAIN + ), + "normalization_contract": { + "method": ( + NORMALIZATION_METHOD + ), + "factor_model": ( + FACTOR_MODEL + ), + "iso_base": ( + ISO_BASE + ), + "apply_stage": ( + "after_dark_before_flat_gain" + ), + }, + "bench_contract": { + "rig_id": ( + args.rig_id + or None + ), + "target_id": ( + args.target_id + or None + ), + "illumination_id": ( + args.illumination_id + or None + ), + "target_distance_mm": ( + args.target_distance_mm + ), + "roi_frac": float( + args.roi_frac + ), + "target_p50_by_role": ( + targets + ), + "operator_instruction": ( + "Painel difuso uniforme, " + "geometria e iluminação " + "reproduzíveis entre módulos." + ), + }, + "sweep_config": { + "calibration_iso": int( + args.calibration_iso + ), + "factors": ( + factors + ), + "frames_per_level": int( + args.frames_per_level + ), + "validation_frames": int( + args.validation_frames + ), + "min_exposure_us": int( + args.min_exposure_us + ), + "max_exposure_us": int( + args.max_exposure_us + ), + "frame_period_fraction": float( + args.frame_period_fraction + ), + }, + "qa_thresholds": qa, + "traceability": { + "module_id": ( + args.module_id + or None + ), + "operator": ( + args.operator + or None + ), + }, + "notes": ( + args.notes + or "" + ), + "preflight": None, + "base_equivalent_exposure": None, + "sweep_plan": None, + "sweep": [], + "analysis": {}, + "reference_validation": {}, + "module_params_fragment": None, + "runtime_policy_recommendation": { + "radiometric_config": { + "enabled": False, + "reason": ( + "Field AE controller is a " + "separate policy and is not " + "factory-calibrated here." + ), + }, + "patch_normalization": { + "enabled": False, + "reason": ( + "Patch normalization is a " + "separate post-fusion policy." + ), + }, + }, + "candidate_dir": str( + candidate_dir + ), + "active_json": ( + args.active_json + ), + } + + with device_ctx as device: + runtime = ( + RadiometricRuntime( + device, + specs, + args, + qa, + ) + ) + + runtime.wait_all() + + # Warm-up + warmup_end = ( + time.time() + + max( + 0.0, + float( + args.warmup_s + ), + ) + ) + + while ( + time.time() + < warmup_end + ): + runtime.poll() + + board = ( + show_progress_board( + runtime, + "WARM-UP", + [ + "Painel cinza/difuso " + "preenchendo o FOV.", + "Não altere luz, câmera " + "ou distância.", + "", + f"restante=" + f"{max(0.0, warmup_end-time.time()):.1f}s", + ], + args, + ) + ) + + cv2.imshow( + window_name, + board, + ) + + k = ( + cv2.waitKey(1) + & 0xFF + ) + + if k in ( + ord("q"), + ord("Q"), + 27, + ): + raise KeyboardInterrupt( + "Cancelado no warm-up." + ) + + # Preflight + print("") + print( + "[PRE-FLIGHT] " + "Aguarde GOOD e pressione ENTER." + ) + + last_preflight = None + + while True: + runtime.poll() + + last_preflight = ( + preflight_report( + runtime, + qa, + ) + ) + + lines = [ + "Painel uniforme e iluminação estável.", + "", + f"GLOBAL=" + f"{last_preflight['status'].upper()}", + "", + "ENTER = iniciar sweep", + "Q/ESC = cancelar", + ] + + for role in ROLES: + item = ( + last_preflight[ + "roles" + ][role] + ) + + st = item.get( + "stats", + {}, + ) + + lines.append( + f"{role.upper()}: " + f"{item['status'].upper()} " + f"p50={st.get('p50',0):.3f} " + f"cv=" + f"{(item.get('temporal_cv') or 0)*100:.2f}%" + ) + + board = ( + show_progress_board( + runtime, + "PRE-FLIGHT", + lines, + args, + ) + ) + + cv2.imshow( + window_name, + board, + ) + + k = ( + cv2.waitKey(1) + & 0xFF + ) + + if k in ( + ord("q"), + ord("Q"), + 27, + ): + raise KeyboardInterrupt( + "Cancelado no preflight." + ) + + if k in ( + 13, + 10, + ): + if ( + last_preflight[ + "status" + ] + != "good" + ): + print( + "[BLOCK] Preflight " + "ainda não está GOOD." + ) + continue + + break + + time.sleep( + 0.002 + ) + + report[ + "preflight" + ] = last_preflight + + base_equiv = ( + choose_base_equivalent_exposure( + runtime, + args.calibration_iso, + ) + ) + + report[ + "base_equivalent_exposure" + ] = base_equiv + + plan, plan_limits = ( + build_sweep_plan( + base_equiv, + factors, + args, + ) + ) + + if ( + len(plan) + < qa[ + "min_sweep_levels" + ] + ): + raise RuntimeError( + "Sweep ficou com poucos " + "níveis únicos após clamps. " + "Ajuste iluminação/fps." + ) + + report[ + "sweep_plan" + ] = { + "levels": [ + { + "factor": ( + level[ + "factor" + ] + ), + "controls": { + role: asdict( + level[ + "controls" + ][role] + ) + for role in ROLES + }, + } + for level in plan + ], + "limits": ( + plan_limits + ), + } + + save_json_atomic( + candidate_report_path, + report, + ) + + # Sweep automático + print("") + print( + f"[SWEEP] " + f"{len(plan)} níveis." + ) + + for idx, level in enumerate( + plan, + start=1, + ): + controls = level[ + "controls" + ] + + runtime.send_controls( + controls + ) + + runtime.verify_controls( + controls, + settle_frames=( + args.settle_frames + ), + ) + + stats = ( + runtime.capture_level( + args.frames_per_level, + controls, + ) + ) + + row = { + "level_index": idx, + "sweep_factor": ( + level[ + "factor" + ] + ), + "captured_at": ( + now_str() + ), + "controls": { + role: { + "exposure_time_us": ( + controls[ + role + ].exposure_time_us + ), + "sensitivity_iso": ( + controls[ + role + ].sensitivity_iso + ), + } + for role in ROLES + }, + "roles": stats, + } + + sweep_rows.append( + row + ) + + report[ + "sweep" + ] = sweep_rows + + save_json_atomic( + candidate_report_path, + report, + ) + + save_sweep_csv( + sweep_csv_path, + sweep_rows, + ) + + lines = [ + f"nível {idx}/{len(plan)}", + f"factor={level['factor']:.3f}", + "", + ] + + for role in ROLES: + lines.append( + f"{role.upper()}: " + f"exp=" + f"{controls[role].exposure_time_us}us " + f"p50=" + f"{stats[role]['p50']:.3f} " + f"sat=" + f"{stats[role]['sat_pct']:.3f}%" + ) + + board = ( + show_progress_board( + runtime, + "SWEEP RADIOMÉTRICO", + lines, + args, + ) + ) + + cv2.imshow( + window_name, + board, + ) + + cv2.waitKey(1) + + print( + "[LEVEL] " + + " | ".join( + [ + f"{role.upper()} " + f"{stats[role]['p50']:.3f}" + for role in ROLES + ] + ) + ) + + # Análise + overall_status = "good" + overall_reasons = [] + + for role in ROLES: + analysis = ( + analyze_role_sweep( + sweep_rows, + role, + targets[role], + args, + qa, + ) + ) + + role_analyses[ + role + ] = analysis + + overall_status = ( + merge_status( + overall_status, + analysis[ + "status" + ], + ) + ) + + overall_reasons.extend( + [ + f"{role}:{x}" + for x in analysis.get( + "reasons", + [], + ) + ] + ) + + save_response_plot( + candidate_dir + / ( + f"response_" + f"{role}.png" + ), + analysis, + ) + + report[ + "analysis" + ] = role_analyses + + # Não tenta reference validation se algum papel não conseguiu + # sequer gerar referência. + if any( + "reference" + not in role_analyses[ + role + ] + for role in ROLES + ): + report[ + "status" + ] = "bad" + + report[ + "finished_at" + ] = now_str() + + report[ + "evaluation_reasons" + ] = ( + overall_reasons + ) + + save_json_atomic( + candidate_report_path, + report, + ) + + raise RuntimeError( + "Não foi possível gerar " + "reference_controls para " + "todas as câmeras." + ) + + # Reference validation + reference_controls = { + role: LockedControl( + role=role, + exposure_time_us=int( + role_analyses[ + role + ][ + "reference" + ][ + "exposure_time_us" + ] + ), + sensitivity_iso=int( + role_analyses[ + role + ][ + "reference" + ][ + "sensitivity_iso" + ] + ), + source=( + "radiometric_reference_validation" + ), + ) + for role in ROLES + } + + runtime.send_controls( + reference_controls + ) + + runtime.verify_controls( + reference_controls, + settle_frames=( + args.settle_frames + ), + ) + + validation_stats = ( + runtime.capture_level( + args.validation_frames, + reference_controls, + ) + ) + + validation_report = {} + + for role in ROLES: + vr = ( + evaluate_reference_validation( + role_analyses[ + role + ], + validation_stats, + qa, + ) + ) + + validation_report[ + role + ] = vr + + overall_status = ( + merge_status( + overall_status, + vr[ + "status" + ], + ) + ) + + overall_reasons.extend( + [ + f"{role}:validation:{x}" + for x in vr[ + "reasons" + ] + ] + ) + + # Salva frame de evidência no ponto de referência. + if runtime.images[ + role + ] is not None: + vis = gray_preview( + runtime.images[ + role + ], + args.roi_frac, + ) + + overlay_hud( + vis, + [ + f"{role.upper()} REFERENCE VALIDATION", + f"p50=" + f"{vr['measured_p50']:.4f}", + f"target=" + f"{vr['target_p50']:.4f}", + f"status=" + f"{vr['status'].upper()}", + f"EXP=" + f"{reference_controls[role].exposure_time_us}us " + f"ISO=" + f"{reference_controls[role].sensitivity_iso}", + ], + x=12, + y=25, + font_scale=0.55, + line_step=23, + ) + + cv2.imwrite( + str( + candidate_dir + / ( + f"validation_" + f"{role}.png" + ) + ), + vis, + ) + + report[ + "reference_validation" + ] = validation_report + + fragment = ( + build_normalization_fragment( + role_analyses, + args, + ) + ) + + report[ + "module_params_fragment" + ] = fragment + + report[ + "evaluation_reasons" + ] = overall_reasons + + report[ + "status" + ] = overall_status + + report[ + "finished_at" + ] = now_str() + + promote = ( + overall_status + == "good" + or ( + overall_status + == "warning" + and args.promote_warning + ) + ) + + report[ + "promoted" + ] = bool( + promote + ) + + report[ + "promotion_policy" + ] = { + "promote_warning": bool( + args.promote_warning + ), + "allowed": bool( + promote + ), + } + + save_json_atomic( + candidate_report_path, + report, + ) + + marker_name = ( + "PASS.txt" + if promote + else "FAIL.txt" + ) + + ( + candidate_dir + / marker_name + ).write_text( + ( + f"status={overall_status}\n" + f"promoted={promote}\n" + f"session={sid}\n" + ), + encoding="utf-8", + ) + + if promote: + report[ + "promoted_at" + ] = now_str() + + promote_active( + Path( + args.active_json + ), + report, + ) + + report[ + "active_json_sha256" + ] = sha256_file( + args.active_json + ) + + save_json_atomic( + candidate_report_path, + report, + ) + + # Final UI + final = np.zeros( + ( + 760, + 1320, + 3, + ), + dtype=np.uint8, + ) + + lines = [ + "RADIOMETRIC CALIBRATION - RESULT", + "", + f"STATUS={overall_status.upper()}", + f"PROMOTED={'YES' if promote else 'NO'}", + "", + ] + + for role in ROLES: + a = role_analyses[ + role + ] + + v = validation_report[ + role + ] + + if "fit" in a: + lines.append( + f"{role.upper()}: " + f"ref=" + f"{a['reference']['exposure_time_us']}us " + f"ISO{a['reference']['sensitivity_iso']} | " + f"R2={a['fit']['r2']:.6f} | " + f"norm p95=" + f"{a['normalization_model_validation']['p95_relative_error']*100:.2f}% | " + f"val p50=" + f"{v['measured_p50']:.3f}" + ) + else: + lines.append( + f"{role.upper()}: INVALID" + ) + + lines.extend([ + "", + "Field radiometric controller: remains DISABLED", + "Patch normalization: remains DISABLED", + "", + f"Candidate: {candidate_dir}", + ( + f"Active: {args.active_json}" + if promote + else "Active calibration preserved" + ), + "", + "Any key closes.", + ]) + + overlay_hud( + final, + lines, + x=42, + y=62, + font_scale=0.62, + line_step=30, + ) + + cv2.imshow( + window_name, + final, + ) + + cv2.waitKey(0) + + if promote: + print("") + print("=" * 88) + print( + "[PASS] RADIOMETRIC " + "CALIBRATION PROMOTED" + ) + print( + f"[ACTIVE] {args.active_json}" + ) + print( + f"[CANDIDATE] {candidate_dir}" + ) + print("=" * 88) + + else: + print("") + print("=" * 88) + print( + "[NOT PROMOTED] " + f"status={overall_status.upper()}" + ) + print( + "[SAFE] Active calibration " + "preserved." + ) + print( + f"[CANDIDATE] {candidate_dir}" + ) + print("=" * 88) + + except KeyboardInterrupt as exc: + if report is None: + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + } + + report[ + "status" + ] = "cancelled" + + report[ + "finished_at" + ] = now_str() + + report[ + "promoted" + ] = False + + report[ + "error" + ] = str(exc) + + report[ + "sweep" + ] = sweep_rows + + try: + save_json_atomic( + candidate_report_path, + report, + ) + + ( + candidate_dir + / "CANCELLED.txt" + ).write_text( + f"reason={exc}\n", + encoding="utf-8", + ) + + except Exception: + pass + + print( + f"[CANCELADO] {exc}" + ) + + print( + "[SAFE] Calibração ativa " + "preservada." + ) + + except Exception as exc: + if report is None: + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + } + + # Preserva BAD já calculado, caso exista. + if report.get( + "status" + ) == "running": + report[ + "status" + ] = "error" + + report[ + "finished_at" + ] = now_str() + + report[ + "promoted" + ] = False + + report[ + "error" + ] = ( + f"{type(exc).__name__}: " + f"{exc}" + ) + + report[ + "sweep" + ] = sweep_rows + + try: + save_json_atomic( + candidate_report_path, + report, + ) + + ( + candidate_dir + / "FAIL.txt" + ).write_text( + ( + f"error=" + f"{type(exc).__name__}: " + f"{exc}\n" + ), + encoding="utf-8", + ) + + except Exception: + pass + + print("") + print("=" * 88) + print( + "[ERRO] RADIOMETRIC " + "CALIBRATION ABORTED" + ) + print( + f"{type(exc).__name__}: {exc}" + ) + print( + "[SAFE] Active calibration " + "preserved." + ) + print("=" * 88) + + raise + + finally: + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/_5_camera_startup_profile.py b/Python/OAK/datasets/oak-fcc-3/utils/_5_camera_startup_profile.py new file mode 100644 index 000000000..1dbfbfa52 --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/utils/_5_camera_startup_profile.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +camera_startup_profile_production.py +==================================== + +Gerador/auditor FINAL de produção para o bloco `camera_settings` +do módulo multiespectral. + +IMPORTANTE +---------- +Isto NÃO é uma calibração óptica/radiométrica nova. + +O script antigo "Sensor Calibration Tool" misturava: + - camera_settings; + - ajustes manuais AE/AWB/exposure/gain; + - ROIs de cana/erva/solo; + - guidance heurístico; + - rgb_calibration manual; + - snapshots/offline samples. + +Na arquitetura final de produto essas responsabilidades já foram separadas. + +Esta ferramenta preserva SOMENTE o que ainda é necessário ao runtime: + camera_settings + +e força explicitamente: + rgb_calibration.enabled = false + gains = 1,1,1 + +para impedir que ganhos RGB históricos/experimentais vazem para o tensor. + +Dependência +----------- +A ferramenta lê o artefato homologado da etapa radiométrica: + + calibration/radiometry_calibration_v5.json + +e reaproveita: + radiometric_normalization.reference_controls + +como valores de fallback do startup. + +Política normal de produto +-------------------------- + RGB: + AE = ON + AWB = ON + exposure_time_us = referência radiométrica (fallback) + analogue_gain = ISO_ref / 100 + colour_gains = [1.0, 1.0] + + RE: + AE = ON + AWB = OFF + exposure_time_us = referência radiométrica (fallback) + analogue_gain = ISO_ref / 100 + + NIR: + AE = ON + AWB = OFF + exposure_time_us = referência radiométrica (fallback) + analogue_gain = ISO_ref / 100 + +Por que manter os valores manuais se AE está ON? +------------------------------------------------- +No manager atual, com AE ligado, exposure/gain do JSON não são forçados +fisicamente. Eles ficam armazenados como estado/fallback para uma eventual +transição futura para manual. + +Assim: + - o campo continua adaptando exposição automaticamente; + - a normalização radiométrica usa os controles reais de cada frame; + - se AE for desabilitado em algum fluxo futuro, o fallback já é coerente + com a calibração radiométrica do módulo. + +Saída +----- + calibration/camera_startup_profile_v3.json + +Contém: + module_params_fragment.camera_settings + module_params_fragment.rgb_calibration + +O assembler final do module_params continua sendo a autoridade de merge. + +Fail-closed +----------- +- exige CAM_A = OV9782 ou AR0234; +- exige CAM_B/C = OV9282; +- exige artefato radiométrico promovido; +- exige hardware_signature compatível; +- exige reference_controls válidos para rgb/re/nir; +- não aceita ISO inválido; +- não ativa rgb_calibration; +- preserva backup do ativo anterior. + +Uso +--- + python camera_startup_profile_production.py + +Com rastreabilidade: + python camera_startup_profile_production.py ^ + --module-id MOD_MS_001 ^ + --operator Diego + +Somente auditoria, sem salvar: + python camera_startup_profile_production.py --check-only +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional, List + +import depthai as dai + + +SCHEMA = "multispec_camera_startup_profile_v3" +ISO_BASE = 100.0 + +ROLES = ("rgb", "re", "nir") + +PRODUCT_TOPOLOGY = { + "rgb": { + "socket": "CAM_A", + "allowed_sensors": ("OV9782", "AR0234"), + }, + "re": { + "socket": "CAM_B", + "allowed_sensors": ("OV9282",), + }, + "nir": { + "socket": "CAM_C", + "allowed_sensors": ("OV9282",), + }, +} + +SENSOR_NATIVE_SIZE = { + "OV9782": (1280, 800), + "AR0234": (1920, 1200), + "OV9282": (1280, 800), +} + + +def now_str() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def stamp() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f") + + +def ensure_dir(path: str | Path): + Path(path).mkdir(parents=True, exist_ok=True) + + +def load_json(path: str | Path) -> dict: + p = Path(path) + if not p.is_file(): + raise FileNotFoundError(f"Arquivo não encontrado: {p}") + + with p.open("r", encoding="utf-8") as f: + return json.load(f) + + +def save_json_atomic(path: str | Path, data: dict): + p = Path(path) + ensure_dir(p.parent) + + tmp = p.with_suffix(p.suffix + ".tmp") + + with tmp.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.flush() + os.fsync(f.fileno()) + + os.replace(tmp, p) + + +def socket_name(socket) -> str: + name = getattr(socket, "name", None) + if name: + return str(name) + + text = str(socket) + for candidate in ("CAM_A", "CAM_B", "CAM_C", "CAM_D"): + if candidate in text: + return candidate + + return text + + +def supported_types(feature) -> List[str]: + return [ + str(v).upper() + for v in (getattr(feature, "supportedTypes", []) or []) + ] + + +def feature_is_color(feature) -> bool: + types = supported_types(feature) + sensor = str(getattr(feature, "sensorName", "") or "").upper() + + if any("COLOR" in x for x in types): + return True + if any("MONO" in x for x in types): + return False + + return sensor in {"OV9782", "AR0234"} + + +def feature_is_mono(feature) -> bool: + types = supported_types(feature) + + if any("MONO" in x for x in types): + return True + if any("COLOR" in x for x in types): + return False + + return not feature_is_color(feature) + + +@dataclass +class CameraInfo: + role: str + socket: str + sensor: str + width: int + height: int + is_color: bool + is_mono: bool + + +def discover_hardware(mx_id: Optional[str]): + info = dai.DeviceInfo(mx_id) if mx_id else None + ctx = dai.Device(info) if info is not None else dai.Device() + + with ctx as device: + actual_mx = None + + for method_name in ("getMxId", "getDeviceId"): + fn = getattr(device, method_name, None) + if callable(fn): + try: + value = fn() + if value: + actual_mx = str(value) + break + except Exception: + pass + + usb_speed = None + try: + usb_speed = str(device.getUsbSpeed()) + except Exception: + pass + + rows = [] + + for f in device.getConnectedCameraFeatures(): + rows.append({ + "socket": socket_name(f.socket), + "sensor": str(getattr(f, "sensorName", "") or "").upper(), + "width": int(getattr(f, "width", 0) or 0), + "height": int(getattr(f, "height", 0) or 0), + "is_color": feature_is_color(f), + "is_mono": feature_is_mono(f), + "supported_types": supported_types(f), + }) + + return rows, actual_mx, usb_speed + + +def validate_and_resolve_hardware(rows: list[dict]) -> Dict[str, CameraInfo]: + by_socket = {row["socket"]: row for row in rows} + resolved = {} + errors = [] + + for role in ROLES: + contract = PRODUCT_TOPOLOGY[role] + sock = contract["socket"] + + row = by_socket.get(sock) + if row is None: + errors.append(f"{role.upper()}: {sock} ausente") + continue + + sensor = row["sensor"] + + if sensor not in contract["allowed_sensors"]: + errors.append( + f"{role.upper()}: {sock} sensor={sensor}, " + f"permitidos={contract['allowed_sensors']}" + ) + continue + + expected_w, expected_h = SENSOR_NATIVE_SIZE[sensor] + + if row["width"] and row["height"]: + if (row["width"], row["height"]) != (expected_w, expected_h): + errors.append( + f"{role.upper()}: {sensor} anunciou " + f"{row['width']}x{row['height']}, esperado " + f"{expected_w}x{expected_h}" + ) + + if role == "rgb" and not row["is_color"]: + errors.append(f"RGB: {sock}/{sensor} não anunciado COLOR") + + if role in ("re", "nir") and not row["is_mono"]: + errors.append(f"{role.upper()}: {sock}/{sensor} não anunciado MONO") + + resolved[role] = CameraInfo( + role=role, + socket=sock, + sensor=sensor, + width=expected_w, + height=expected_h, + is_color=bool(row["is_color"]), + is_mono=bool(row["is_mono"]), + ) + + if errors: + raise RuntimeError( + "Topologia de produto inválida:\n - " + + "\n - ".join(errors) + ) + + return resolved + + +def hardware_signature_from_resolved(resolved: Dict[str, CameraInfo]) -> dict: + return { + role: { + "socket": resolved[role].socket, + "sensor": resolved[role].sensor, + "size": [resolved[role].width, resolved[role].height], + } + for role in ROLES + } + + +def normalize_signature(sig: dict) -> dict: + """ + Aceita assinatura no formato dos calibradores de produção. + Retorna apenas role/socket/sensor/size para comparação robusta. + """ + out = {} + + for role in ROLES: + item = (sig or {}).get(role, {}) or {} + + socket = ( + item.get("socket") + or item.get("socket_name") + ) + + sensor = ( + item.get("sensor") + or item.get("sensor_name") + ) + + size = item.get("size") + + if size is None: + width = item.get("width") + height = item.get("height") + if width is not None and height is not None: + size = [int(width), int(height)] + + if socket is None or sensor is None or size is None: + raise RuntimeError( + f"hardware_signature incompleta para {role}: {item}" + ) + + out[role] = { + "socket": str(socket), + "sensor": str(sensor).upper(), + "size": [int(size[0]), int(size[1])], + } + + return out + + +def extract_radiometric_fragment(data: dict) -> dict: + """ + O artefato radiométrico de produção guarda: + module_params_fragment.radiometric_normalization + """ + fragment = data.get("module_params_fragment", {}) or {} + rn = fragment.get("radiometric_normalization") + + if not isinstance(rn, dict): + raise RuntimeError( + "Artefato radiométrico sem " + "module_params_fragment.radiometric_normalization" + ) + + if not rn.get("enabled", False): + raise RuntimeError( + "radiometric_normalization do artefato não está enabled=true" + ) + + return rn + + +def validate_radiometry_artifact( + data: dict, + current_signature: dict, +): + schema = str(data.get("schema", "")) + + if schema != "multispec_radiometric_calibration_v5": + raise RuntimeError( + f"Schema radiométrico inesperado: {schema!r}. " + "Esperado 'multispec_radiometric_calibration_v5'." + ) + + status = str(data.get("status", "")).lower() + + if status not in ("good", "warning"): + raise RuntimeError( + f"Radiometria não homologada para consumo: status={status!r}" + ) + + if not bool(data.get("promoted", False)): + raise RuntimeError( + "Artefato radiométrico não foi promovido." + ) + + artifact_sig = normalize_signature( + data.get("hardware_signature", {}) + ) + + if artifact_sig != current_signature: + raise RuntimeError( + "Hardware conectado não corresponde ao hardware da radiometria.\n" + f"Atual : {current_signature}\n" + f"Rad : {artifact_sig}" + ) + + rn = extract_radiometric_fragment(data) + + refs = rn.get("reference_controls", {}) or {} + + for role in ROLES: + ref = refs.get(role) + + if not isinstance(ref, dict): + raise RuntimeError( + f"reference_controls ausente para {role}" + ) + + exp = ref.get("exposure_time_us") + iso = ref.get("sensitivity_iso") + + if exp is None or float(exp) <= 0: + raise RuntimeError( + f"reference exposure inválida para {role}: {exp}" + ) + + if iso is None or float(iso) < 50: + raise RuntimeError( + f"reference ISO inválido para {role}: {iso}" + ) + + return rn + + +def iso_to_analogue_gain(iso: float) -> float: + """ + O manager atual representa analogue_gain aproximadamente como ISO/100. + """ + gain = float(iso) / ISO_BASE + return max(1.0, gain) + + +def build_camera_settings(reference_controls: dict) -> dict: + settings = {} + + for role in ROLES: + ref = reference_controls[role] + + exp = int(round(float(ref["exposure_time_us"]))) + iso = float(ref["sensitivity_iso"]) + gain = iso_to_analogue_gain(iso) + + if role == "rgb": + settings[role] = { + "ae_enable": True, + "awb_enable": True, + "exposure_time_us": exp, + "analogue_gain": gain, + "colour_gains": [1.0, 1.0], + } + else: + settings[role] = { + "ae_enable": True, + "awb_enable": False, + "exposure_time_us": exp, + "analogue_gain": gain, + "colour_gains": None, + } + + return settings + + +def build_rgb_calibration_policy() -> dict: + """ + Produto final mantém correção multiplicativa RGB desligada. + Se um dia ela for homologada, deve existir um calibrador separado e + dataset/runtime precisam usar o mesmo contrato. + """ + return { + "enabled": False, + "gains": { + "R": 1.0, + "G": 1.0, + "B": 1.0, + }, + "policy": "disabled_product_default", + "reason": ( + "No validated RGB tensor-gain calibration is currently active. " + "Historical manual gains are intentionally discarded." + ), + } + + +def compare_existing_module_params( + module_params_path: Optional[str], + generated_fragment: dict, +) -> dict: + if not module_params_path: + return { + "available": False, + "path": None, + } + + p = Path(module_params_path) + + if not p.is_file(): + return { + "available": False, + "path": str(p), + } + + current = load_json(p) + + current_camera = current.get("camera_settings") + current_rgb = current.get("rgb_calibration") + + generated_camera = generated_fragment["camera_settings"] + generated_rgb = generated_fragment["rgb_calibration"] + + return { + "available": True, + "path": str(p), + "schema": current.get("schema"), + "camera_settings_equal": current_camera == generated_camera, + "rgb_calibration_equal": current_rgb == generated_rgb, + "current_camera_settings": current_camera, + "generated_camera_settings": generated_camera, + "current_rgb_calibration": current_rgb, + "generated_rgb_calibration": generated_rgb, + } + + +def main(): + parser = argparse.ArgumentParser( + description=( + "Gerador/auditor de produção para camera_settings e " + "política rgb_calibration do módulo multiespectral." + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument( + "--mx-id", + default=None, + help="MX ID da OAK. Vazio usa o primeiro dispositivo disponível.", + ) + + parser.add_argument( + "--radiometry-json", + default="calibration/radiometry_calibration_v5.json", + help="Artefato radiométrico homologado.", + ) + + parser.add_argument( + "--out-json", + default="calibration/camera_startup_profile_v3.json", + help="Artefato ativo desta etapa.", + ) + + parser.add_argument( + "--compare-module-params", + default="calibration/module_params.json", + help="Opcional: audita diferenças contra module_params existente.", + ) + + parser.add_argument( + "--module-id", + default="", + ) + + parser.add_argument( + "--operator", + default="", + ) + + parser.add_argument( + "--notes", + default="", + ) + + parser.add_argument( + "--check-only", + action="store_true", + help="Valida e imprime o fragmento, mas não salva.", + ) + + args = parser.parse_args() + + rows, actual_mx, usb_speed = discover_hardware(args.mx_id) + resolved = validate_and_resolve_hardware(rows) + + current_signature = hardware_signature_from_resolved(resolved) + + radiometry = load_json(args.radiometry_json) + + rn = validate_radiometry_artifact( + radiometry, + current_signature, + ) + + reference_controls = rn["reference_controls"] + + camera_settings = build_camera_settings(reference_controls) + rgb_calibration = build_rgb_calibration_policy() + + module_params_fragment = { + "camera_settings": camera_settings, + "rgb_calibration": { + "enabled": False, + "gains": { + "R": 1.0, + "G": 1.0, + "B": 1.0, + }, + }, + } + + audit = compare_existing_module_params( + args.compare_module_params, + module_params_fragment, + ) + + payload = { + "schema": SCHEMA, + "created_at": now_str(), + "status": "good", + "promoted": not args.check_only, + "runtime_effect": ( + "startup_camera_policy_and_explicit_rgb_gain_disable" + ), + "device_mx_id": actual_mx, + "usb_speed": usb_speed, + "hardware_inventory": rows, + "hardware_signature": current_signature, + "source_radiometry": { + "path": str(args.radiometry_json), + "schema": radiometry.get("schema"), + "session_id": radiometry.get("session_id"), + "status": radiometry.get("status"), + "promoted": radiometry.get("promoted"), + "reference_controls": reference_controls, + }, + "startup_policy": { + "mode": "ae_with_calibrated_manual_fallback", + "rgb_awb": "auto_for_isp_preview_only", + "spectral_awb": "off_not_applicable", + "manual_fallback_source": ( + "radiometric_normalization.reference_controls" + ), + "note": ( + "With AE enabled, manager stores exposure/gain as fallback " + "and does not force them manually at startup." + ), + }, + "rgb_calibration_policy": rgb_calibration, + "module_params_fragment": module_params_fragment, + "existing_module_params_audit": audit, + "traceability": { + "module_id": args.module_id or None, + "operator": args.operator or None, + }, + "notes": args.notes or "", + } + + print("=" * 82) + print("CAMERA STARTUP PROFILE - PRODUCTION") + print(f"MX ID : {actual_mx}") + print(f"USB : {usb_speed}") + print("-" * 82) + + for role in ROLES: + c = resolved[role] + s = camera_settings[role] + + print( + f"{c.socket} -> {role.upper():3s} | " + f"{c.sensor:8s} | {c.width}x{c.height} | " + f"AE={s['ae_enable']} AWB={s['awb_enable']} | " + f"fallback={s['exposure_time_us']}us gain={s['analogue_gain']:.3f}" + ) + + print("-" * 82) + print("rgb_calibration: DISABLED | gains = 1.0 / 1.0 / 1.0") + + if audit.get("available"): + print( + "module_params audit: " + f"camera_settings_equal={audit['camera_settings_equal']} | " + f"rgb_calibration_equal={audit['rgb_calibration_equal']}" + ) + else: + print("module_params audit: arquivo não disponível") + + if args.check_only: + print("[CHECK-ONLY] Nenhum arquivo alterado.") + print("=" * 82) + return + + out = Path(args.out_json) + ensure_dir(out.parent) + + if out.exists(): + backup = out.with_name( + out.stem + + f".backup_{stamp()}" + + out.suffix + ) + shutil.copy2(out, backup) + + save_json_atomic(out, payload) + + print(f"[PASS] Artefato salvo: {out}") + print("[ASSEMBLER] Consumir module_params_fragment deste JSON.") + print("=" * 82) + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/_6_homography_calibration_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/_6_homography_calibration_tool.py new file mode 100644 index 000000000..064fde68f --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/utils/_6_homography_calibration_tool.py @@ -0,0 +1,4904 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +homography_calibration_production.py +==================================== + +Calibrador FINAL de produção para alinhamento geométrico RGB / RE / NIR +do módulo OAK-FFC-3P usando ChArUco. + +Topologia oficial +----------------- + CAM_A = RGB = OV9782 1280x800 + OU + AR0234 1920x1200 + + CAM_B = RE = OV9282 1280x800 + CAM_C = NIR = OV9282 1280x800 + +Princípio geométrico +-------------------- +A homografia é estimada DIRETAMENTE entre os espaços nativos: + + H_RE_to_RGB : coordenadas RE 1280x800 -> coordenadas RGB nativas + H_NIR_to_RGB : coordenadas NIR 1280x800 -> coordenadas RGB nativas + +Portanto: + - OV9782: destino RGB = 1280x800 + - AR0234: destino RGB = 1920x1200 + +NÃO redimensionamos RE/NIR para o tamanho da RGB antes de detectar pontos. +Isso seria geometricamente incorreto para uma calibração de produto. + +Domínio da calibração +--------------------- +As imagens usadas aqui são: + RGB -> saída ISP full-resolution + RE -> saída MONO full-resolution + NIR -> saída MONO full-resolution + +Nenhum undistort/remap externo é aplicado nesta ferramenta. + +O JSON registra explicitamente: + coordinate_space = "native_stream_no_external_undistort" + +Assim o assembler final do module_params sabe em qual espaço a matriz nasceu. +Se uma etapa posterior introduzir remap/undistort antes da homografia, o +assembler/runtime deve respeitar/compor os espaços corretamente. + +Por que ChArUco? +---------------- +O ChArUco fornece IDs comuns subpixel entre as três câmeras e reduz o erro +humano da seleção manual. Esta versão também: + - captura triplets frescos; + - valida coerência temporal; + - rejeita amostras repetidas; + - exige distribuição espacial pelo FOV; + - rejeita amostras incompatíveis com o mesmo plano físico; + - faz RANSAC; + - faz validação por AMOSTRA (leave-one-sample-out); + - calcula overlap útil; + - só promove perfil que passa no acceptance test. + +Perfis de altura/plano +---------------------- +Uma única homografia é válida para UM plano físico. + +Exemplos do projeto: + baixa -> plano mais distante / chão + media -> plano médio + alta -> plano mais próximo + +Execute uma sessão por perfil, mantendo o ChArUco NO MESMO PLANO durante +todas as amostras daquela sessão. + +Exemplos: +--------- + python homography_calibration_production.py ^ + --profile-name media ^ + --depth-cm 66 + + python homography_calibration_production.py ^ + --profile-name baixa ^ + --depth-cm 120 + + python homography_calibration_production.py ^ + --profile-name alta ^ + --depth-cm 36 + +Fluxo +----- +1. PRE-FLIGHT: + - ChArUco visível nas três câmeras + - AE estabiliza + - ENTER congela exposição/ISO + +2. CAPTURA: + - mova o ChArUco lateralmente DENTRO DO MESMO PLANO + - mantenha o tabuleiro coplanar ao plano calibrado + - G captura uma amostra tripla nova + - a ferramenta rejeita detecção ruim / repetida / dessincronizada + +3. Quando houver amostras suficientes e boa cobertura: + - A calcula e executa acceptance + +4. PASS: + - candidate auditável é salvo + - perfil é promovido para homography_calibration_v4.json + - outros perfis previamente aprovados são preservados + +5. FAIL/CANCEL: + - candidate é preservado + - calibração ativa anterior NÃO é alterada + +Teclas +------ + G captura nova amostra ChArUco + A calcula / tenta aprovar perfil + 2 overlay RGB + RE + 3 overlay RGB + NIR + TAB alterna RE/NIR + V limpa todas as amostras da sessão + S salva snapshot da tela + Q/ESC cancela + +Dependências +------------ + pip install depthai opencv-contrib-python numpy + +Artefato consumível pelo assembler +---------------------------------- +O JSON ativo contém um module_params_fragment com fusion_config/homography_profiles. +O assembler final continua sendo a autoridade que monta module_params. + +Schema: + multispec_homography_calibration_v4 +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import shutil +import time +from collections import deque +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional, Tuple, List, Any + +import cv2 +import depthai as dai +import numpy as np + + +# ============================================================ +# Contrato do produto +# ============================================================ + +ROLES = ("rgb", "re", "nir") +SPEC_ROLES = ("re", "nir") + +PRODUCT_TOPOLOGY = { + "rgb": { + "socket": "CAM_A", + "allowed_sensors": ("OV9782", "AR0234"), + }, + "re": { + "socket": "CAM_B", + "allowed_sensors": ("OV9282",), + }, + "nir": { + "socket": "CAM_C", + "allowed_sensors": ("OV9282",), + }, +} + +RGB_SENSOR_MODES = { + "OV9782": { + "resolution_enum": "THE_800_P", + "width": 1280, + "height": 800, + }, + "AR0234": { + "resolution_enum": "THE_1200_P", + "width": 1920, + "height": 1200, + }, +} + +MONO_SENSOR_MODES = { + "OV9282": { + "resolution_enum": "THE_800_P", + "width": 1280, + "height": 800, + }, +} + +SCHEMA = "multispec_homography_calibration_v4" +COORDINATE_SPACE = "native_stream_no_external_undistort" + + +# ============================================================ +# QA defaults +# ============================================================ + +DEFAULT_QA = { + # Captura + "sync_tolerance_ms": 35.0, + "min_markers_each": 4, + "min_charuco_corners_each": 8, + "min_common_corners_each_pair": 8, + + # Dataset + "min_samples": 8, + "recommended_samples": 12, + "min_total_pairs": 70, + "min_unique_charuco_ids": 18, + + # Repetição de pose + "duplicate_center_distance_norm": 0.025, + "duplicate_scale_delta": 0.045, + + # RANSAC + "ransac_reproj_threshold_px": 3.0, + "ransac_confidence": 0.999, + "ransac_max_iters": 10000, + + # Rejeição de amostra planar incompatível + "sample_reject_median_error_px": 4.0, + "max_sample_rejection_rounds": 2, + + # Acceptance in-sample final + "inlier_ratio_warning": 0.95, + "inlier_ratio_bad": 0.90, + + "fit_median_error_warning_px": 1.20, + "fit_median_error_bad_px": 2.00, + + "fit_p95_error_warning_px": 2.50, + "fit_p95_error_bad_px": 4.00, + + # Leave-one-sample-out + "cv_median_error_warning_px": 1.60, + "cv_median_error_bad_px": 2.60, + + "cv_p95_error_warning_px": 3.20, + "cv_p95_error_bad_px": 4.80, + + "cv_max_sample_median_warning_px": 2.60, + "cv_max_sample_median_bad_px": 4.00, + + # Cobertura no destino RGB + "coverage_span_x_warning": 0.60, + "coverage_span_x_bad": 0.48, + + "coverage_span_y_warning": 0.55, + "coverage_span_y_bad": 0.43, + + "coverage_hull_warning": 0.24, + "coverage_hull_bad": 0.15, + + # Overlap útil das imagens + "overlap_warning": 0.80, + "overlap_bad": 0.70, + + # Sanidade matricial + "condition_number_warning": 1.0e5, + "condition_number_bad": 1.0e7, + + # Exposição manual + "exposure_rel_tolerance": 0.01, + "exposure_abs_tolerance_us": 20, + "iso_tolerance": 5, +} + + +# ============================================================ +# Helpers +# ============================================================ + +def now_str() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def session_stamp() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f") + + +def ensure_dir(path: str | Path): + Path(path).mkdir(parents=True, exist_ok=True) + + +def save_json_atomic(path: str | Path, data: dict): + p = Path(path) + ensure_dir(p.parent) + + tmp = p.with_suffix(p.suffix + ".tmp") + + with tmp.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.flush() + os.fsync(f.fileno()) + + os.replace(tmp, p) + + +def load_json(path: str | Path) -> dict: + p = Path(path) + if not p.is_file(): + return {} + + with p.open("r", encoding="utf-8") as f: + return json.load(f) + + +def sha256_file(path: str | Path) -> str: + h = hashlib.sha256() + + with open(path, "rb") as f: + while True: + chunk = f.read(1024 * 1024) + if not chunk: + break + h.update(chunk) + + return h.hexdigest() + + +def safe_int(v, default=None): + try: + if v is None: + return default + return int(v) + except Exception: + return default + + +def safe_float(v, default=None): + try: + if v is None: + return default + return float(v) + except Exception: + return default + + +def status_rank(s: str) -> int: + return { + "good": 0, + "warning": 1, + "bad": 2, + }.get(str(s).lower(), 2) + + +def merge_status(a: str, b: str) -> str: + return a if status_rank(a) >= status_rank(b) else b + + +def overlay_hud( + img, + lines, + x=12, + y=24, + font_scale=0.50, + line_step=20, + color=(255, 255, 255), +): + yy = int(y) + h = img.shape[0] + + for line in lines: + if yy > h - 6: + break + + text = str(line) + + cv2.putText( + img, + text, + (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, + (0, 0, 0), + 3, + cv2.LINE_AA, + ) + + cv2.putText( + img, + text, + (int(x), yy), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, + color, + 1, + cv2.LINE_AA, + ) + + yy += int(line_step) + + +def socket_name(socket) -> str: + name = getattr(socket, "name", None) + if name: + return str(name) + + text = str(socket) + + for candidate in ("CAM_A", "CAM_B", "CAM_C", "CAM_D"): + if candidate in text: + return candidate + + return text + + +def get_socket(name: str): + mapping = { + "CAM_A": dai.CameraBoardSocket.CAM_A, + "CAM_B": dai.CameraBoardSocket.CAM_B, + "CAM_C": dai.CameraBoardSocket.CAM_C, + } + + key = str(name).upper() + + if key not in mapping: + raise ValueError(f"Socket inválido: {name}") + + return mapping[key] + + +def enum_if_exists(enum_cls, name: str): + return getattr(enum_cls, name, None) + + +def supported_type_strings(feature) -> List[str]: + return [ + str(v).upper() + for v in (getattr(feature, "supportedTypes", []) or []) + ] + + +def feature_is_color(feature) -> bool: + types = supported_type_strings(feature) + sensor = str(getattr(feature, "sensorName", "") or "").upper() + + if any("COLOR" in x for x in types): + return True + + if any("MONO" in x for x in types): + return False + + return sensor in {"OV9782", "AR0234"} + + +def feature_is_mono(feature) -> bool: + types = supported_type_strings(feature) + + if any("MONO" in x for x in types): + return True + + if any("COLOR" in x for x in types): + return False + + return not feature_is_color(feature) + + +def resize_panel(img: np.ndarray, width: int, height: int) -> np.ndarray: + return cv2.resize( + img, + (int(width), int(height)), + interpolation=cv2.INTER_AREA, + ) + + +def gray_bgr(gray_u8: np.ndarray) -> np.ndarray: + if gray_u8.ndim == 3: + return gray_u8 + + return cv2.cvtColor( + gray_u8, + cv2.COLOR_GRAY2BGR, + ) + + +def colorize_spec(gray_u8: np.ndarray, role: str) -> np.ndarray: + if gray_u8.ndim == 3: + gray_u8 = cv2.cvtColor( + gray_u8, + cv2.COLOR_BGR2GRAY, + ) + + z = np.zeros_like(gray_u8) + + if role == "re": + return np.dstack([z, z, gray_u8]) + + if role == "nir": + return np.dstack([gray_u8, gray_u8, z]) + + return gray_bgr(gray_u8) + + +def normalize_gray_u8(img) -> np.ndarray: + if img is None: + raise ValueError("Imagem None") + + if img.ndim == 3: + gray = cv2.cvtColor( + img, + cv2.COLOR_BGR2GRAY, + ) + else: + gray = img + + if gray.dtype == np.uint8: + return gray + + arr = gray.astype(np.float32) + finite = arr[np.isfinite(arr)] + + if finite.size == 0: + return np.zeros(arr.shape[:2], dtype=np.uint8) + + lo = float(np.percentile(finite, 1.0)) + hi = float(np.percentile(finite, 99.5)) + + if hi <= lo: + hi = lo + 1e-6 + + out = (arr - lo) / (hi - lo) + + return np.clip(out * 255.0, 0, 255).astype(np.uint8) + + +# ============================================================ +# Hardware +# ============================================================ + +@dataclass +class CameraSpec: + role: str + socket_name: str + sensor_name: str + width: int + height: int + resolution_name: str + stream_name: str + control_name: str + is_color: bool + source: str + + +def discover_cameras(mx_id: Optional[str]): + device_info = dai.DeviceInfo(mx_id) if mx_id else None + + ctx = ( + dai.Device(device_info) + if device_info is not None + else dai.Device() + ) + + with ctx as device: + actual_mx = None + + for method_name in ("getMxId", "getDeviceId"): + fn = getattr(device, method_name, None) + + if callable(fn): + try: + value = fn() + + if value: + actual_mx = str(value) + break + + except Exception: + pass + + usb_speed = None + + try: + usb_speed = str(device.getUsbSpeed()) + except Exception: + pass + + rows = [] + + for feature in device.getConnectedCameraFeatures(): + rows.append({ + "socket_name": socket_name(feature.socket), + "sensor_name": str(feature.sensorName or "").upper(), + "width": int(getattr(feature, "width", 0) or 0), + "height": int(getattr(feature, "height", 0) or 0), + "supported_types": supported_type_strings(feature), + "is_color": feature_is_color(feature), + "is_mono": feature_is_mono(feature), + "has_autofocus_ic": int( + getattr(feature, "hasAutofocusIC", 0) or 0 + ), + }) + + return rows, actual_mx, usb_speed + + +def validate_product_topology(camera_rows: list[dict]): + by_socket = { + row["socket_name"]: row + for row in camera_rows + } + + errors = [] + + for role in ROLES: + contract = PRODUCT_TOPOLOGY[role] + socket = contract["socket"] + + row = by_socket.get(socket) + + if row is None: + errors.append( + f"{role.upper()}: {socket} ausente" + ) + continue + + sensor = row["sensor_name"] + + if sensor not in contract["allowed_sensors"]: + errors.append( + f"{role.upper()}: {socket} sensor={sensor}, " + f"esperado={contract['allowed_sensors']}" + ) + + if role == "rgb" and not row["is_color"]: + errors.append( + f"RGB {socket}/{sensor} não anunciado COLOR" + ) + + if role in SPEC_ROLES and not row["is_mono"]: + errors.append( + f"{role.upper()} {socket}/{sensor} não anunciado MONO" + ) + + if errors: + raise RuntimeError( + "Topologia do produto inválida:\n - " + + "\n - ".join(errors) + ) + + return { + "valid": True, + "errors": [], + } + + +def build_specs(camera_rows: list[dict]) -> Dict[str, CameraSpec]: + by_socket = { + row["socket_name"]: row + for row in camera_rows + } + + specs = {} + + for role in ROLES: + socket = PRODUCT_TOPOLOGY[role]["socket"] + row = by_socket[socket] + sensor = row["sensor_name"] + + if role == "rgb": + cfg = RGB_SENSOR_MODES[sensor] + else: + cfg = MONO_SENSOR_MODES[sensor] + + if row["width"] and row["height"]: + if ( + row["width"], + row["height"], + ) != ( + cfg["width"], + cfg["height"], + ): + raise RuntimeError( + f"{socket}/{sensor}: dimensão anunciada " + f"{row['width']}x{row['height']} != " + f"{cfg['width']}x{cfg['height']}" + ) + + specs[role] = CameraSpec( + role=role, + socket_name=socket, + sensor_name=sensor, + width=int(cfg["width"]), + height=int(cfg["height"]), + resolution_name=cfg["resolution_enum"], + stream_name=f"hcal_{role}", + control_name=f"hcal_ctrl_{role}", + is_color=(role == "rgb"), + source="ISP" if role == "rgb" else "MONO_OUT", + ) + + return specs + + +def build_pipeline(specs: Dict[str, CameraSpec], args): + pipeline = dai.Pipeline() + + isp_settings = { + "sharpness": None, + "luma_denoise": None, + "chroma_denoise": None, + "errors": [], + } + + for role in ROLES: + spec = specs[role] + socket = get_socket(spec.socket_name) + + if spec.is_color: + cam = pipeline.createColorCamera() + cam.setBoardSocket(socket) + + enum_value = enum_if_exists( + dai.ColorCameraProperties.SensorResolution, + spec.resolution_name, + ) + + if enum_value is None: + raise RuntimeError( + f"DepthAI não expõe ColorCamera {spec.resolution_name}" + ) + + cam.setResolution(enum_value) + cam.setFps(float(args.fps)) + cam.setInterleaved(False) + + try: + cam.setColorOrder( + dai.ColorCameraProperties.ColorOrder.BGR + ) + except Exception: + pass + + try: + cam.setIspScale(1, 1) + except Exception: + pass + + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + + try: + cam.initialControl.setAutoWhiteBalanceLock(False) + except Exception: + pass + + for key, method_name, value in ( + ("sharpness", "setSharpness", int(args.isp_sharpness)), + ("luma_denoise", "setLumaDenoise", int(args.isp_luma_denoise)), + ("chroma_denoise", "setChromaDenoise", int(args.isp_chroma_denoise)), + ): + fn = getattr(cam.initialControl, method_name, None) + + if not callable(fn): + isp_settings["errors"].append( + f"{method_name}:unsupported" + ) + continue + + try: + fn(value) + isp_settings[key] = value + except Exception as exc: + isp_settings["errors"].append( + f"{method_name}:{exc}" + ) + + output = cam.isp + + else: + cam = pipeline.createMonoCamera() + cam.setBoardSocket(socket) + + enum_value = enum_if_exists( + dai.MonoCameraProperties.SensorResolution, + spec.resolution_name, + ) + + if enum_value is None: + raise RuntimeError( + f"DepthAI não expõe MonoCamera {spec.resolution_name}" + ) + + cam.setResolution(enum_value) + cam.setFps(float(args.fps)) + + try: + cam.initialControl.setAutoExposureEnable() + except Exception: + pass + + output = cam.out + + xout = pipeline.createXLinkOut() + xout.setStreamName(spec.stream_name) + output.link(xout.input) + + xin = pipeline.createXLinkIn() + xin.setStreamName(spec.control_name) + + if not hasattr(cam, "inputControl"): + raise RuntimeError( + f"{spec.socket_name}/{spec.sensor_name} sem inputControl" + ) + + xin.out.link(cam.inputControl) + + return pipeline, isp_settings + + +# ============================================================ +# Controles de exposição +# ============================================================ + +@dataclass +class LockedControl: + role: str + exposure_time_us: int + sensitivity_iso: int + source: str + + +def packet_controls(packet) -> dict: + exposure_us = None + iso = None + color_temperature = None + sequence_num = None + timestamp_s = None + + try: + exp = packet.getExposureTime() + + if hasattr(exp, "total_seconds"): + exposure_us = int( + round(exp.total_seconds() * 1_000_000) + ) + else: + exposure_us = int(exp) + + except Exception: + pass + + try: + iso = int(packet.getSensitivity()) + except Exception: + pass + + try: + color_temperature = int( + packet.getColorTemperature() + ) + except Exception: + pass + + try: + sequence_num = int( + packet.getSequenceNum() + ) + except Exception: + pass + + for method_name in ( + "getTimestampDevice", + "getTimestamp", + ): + fn = getattr(packet, method_name, None) + + if callable(fn): + try: + ts = fn() + + if hasattr(ts, "total_seconds"): + timestamp_s = float( + ts.total_seconds() + ) + else: + timestamp_s = float(ts) + + break + except Exception: + pass + + return { + "exposure_time_us": exposure_us, + "sensitivity_iso": iso, + "color_temperature_k": color_temperature, + "sequence_num": sequence_num, + "timestamp_s": timestamp_s, + } + + +def controls_match( + actual: dict, + target: LockedControl, + qa: dict, +) -> bool: + exp = safe_int( + actual.get("exposure_time_us"), + None, + ) + + iso = safe_int( + actual.get("sensitivity_iso"), + None, + ) + + if exp is None or iso is None: + return False + + exp_tol = max( + int(qa["exposure_abs_tolerance_us"]), + int( + round( + target.exposure_time_us + * qa["exposure_rel_tolerance"] + ) + ), + ) + + return ( + abs(exp - target.exposure_time_us) + <= exp_tol + and abs(iso - target.sensitivity_iso) + <= int(qa["iso_tolerance"]) + ) + + +def send_manual_control( + queue, + lock: LockedControl, + rgb=False, +): + ctrl = dai.CameraControl() + + ctrl.setManualExposure( + int(lock.exposure_time_us), + int(lock.sensitivity_iso), + ) + + if rgb: + try: + ctrl.setAutoWhiteBalanceLock(True) + except Exception: + pass + + queue.send(ctrl) + + +# ============================================================ +# Runtime de aquisição +# ============================================================ + +class AcquisitionRuntime: + def __init__( + self, + device, + specs: Dict[str, CameraSpec], + args, + qa, + ): + self.device = device + self.specs = specs + self.args = args + self.qa = qa + + self.output_queues = { + role: device.getOutputQueue( + name=spec.stream_name, + maxSize=3, + blocking=False, + ) + for role, spec in specs.items() + } + + self.control_queues = { + role: device.getInputQueue( + spec.control_name + ) + for role, spec in specs.items() + } + + self.frames = { + role: None + for role in ROLES + } + + self.controls = { + role: {} + for role in ROLES + } + + self.last_seq = { + role: None + for role in ROLES + } + + self.frame_counter = { + role: 0 + for role in ROLES + } + + self.fps = { + role: 0.0 + for role in ROLES + } + + self._fps_counter = { + role: 0 + for role in ROLES + } + + self._fps_t0 = { + role: time.time() + for role in ROLES + } + + def poll(self) -> set[str]: + fresh = set() + + for role in ROLES: + packet = self.output_queues[role].tryGet() + + if packet is None: + continue + + ctrl = packet_controls(packet) + seq = ctrl.get("sequence_num") + + if ( + seq is not None + and seq == self.last_seq[role] + ): + continue + + img = packet.getCvFrame() + + if img is None: + continue + + if img.ndim == 2: + img = np.ascontiguousarray(img) + else: + img = np.ascontiguousarray(img) + + expected = ( + self.specs[role].height, + self.specs[role].width, + ) + + if img.shape[:2] != expected: + raise RuntimeError( + f"{role.upper()}: frame {img.shape[:2]} != " + f"esperado {expected}" + ) + + self.frames[role] = img + self.controls[role] = ctrl + self.last_seq[role] = seq + self.frame_counter[role] += 1 + + fresh.add(role) + + self._fps_counter[role] += 1 + + dt = time.time() - self._fps_t0[role] + + if dt >= 1.0: + self.fps[role] = ( + self._fps_counter[role] + / dt + ) + + self._fps_counter[role] = 0 + self._fps_t0[role] = time.time() + + return fresh + + def wait_all(self, timeout=8.0): + t0 = time.time() + + while time.time() - t0 < timeout: + self.poll() + + if all( + self.frames[r] is not None + for r in ROLES + ): + return + + time.sleep(0.003) + + missing = [ + r + for r in ROLES + if self.frames[r] is None + ] + + raise TimeoutError( + f"Timeout aguardando câmeras: {missing}" + ) + + def send_manual(self, locked): + for role in ROLES: + send_manual_control( + self.control_queues[role], + locked[role], + rgb=(role == "rgb"), + ) + + def verify_manual( + self, + locked, + settle_frames=8, + timeout=10.0, + ): + matched = { + r: 0 + for r in ROLES + } + + seen = dict(self.last_seq) + t0 = time.time() + + while time.time() - t0 < timeout: + fresh = self.poll() + + for role in fresh: + seq = self.last_seq[role] + + if seq == seen.get(role): + continue + + seen[role] = seq + + if controls_match( + self.controls[role], + locked[role], + self.qa, + ): + matched[role] += 1 + else: + matched[role] = 0 + + if all( + matched[r] >= int(settle_frames) + for r in ROLES + ): + return + + time.sleep(0.002) + + raise RuntimeError( + "Não foi possível confirmar exposição manual: " + f"{matched}" + ) + + def fresh_triplet( + self, + timeout=3.0, + ): + """ + Aguarda um frame NOVO de cada role após o pedido. + Retorna snapshots e metadados. + """ + start_seq = dict(self.last_seq) + + captured = {} + captured_controls = {} + + t0 = time.time() + + while time.time() - t0 < timeout: + fresh = self.poll() + + for role in fresh: + seq = self.last_seq[role] + + if ( + role not in captured + and seq != start_seq.get(role) + ): + captured[role] = ( + self.frames[role].copy() + ) + captured_controls[role] = dict( + self.controls[role] + ) + + if len(captured) == 3: + break + + time.sleep(0.001) + + if len(captured) != 3: + missing = [ + r + for r in ROLES + if r not in captured + ] + + raise TimeoutError( + f"Triplet incompleto. Faltando: {missing}" + ) + + timestamps = [ + captured_controls[r].get("timestamp_s") + for r in ROLES + ] + + timestamps = [ + float(x) + for x in timestamps + if x is not None + ] + + if len(timestamps) == 3: + sync_dt_ms = ( + max(timestamps) - min(timestamps) + ) * 1000.0 + else: + sync_dt_ms = None + + return { + "frames": captured, + "controls": captured_controls, + "sync_dt_ms": sync_dt_ms, + } + + +# ============================================================ +# ChArUco compatibility layer +# ============================================================ + +def require_aruco(): + if not hasattr(cv2, "aruco"): + raise RuntimeError( + "cv2.aruco indisponível. " + "Instale opencv-contrib-python." + ) + + if not ( + hasattr(cv2.aruco, "CharucoBoard") + or hasattr(cv2.aruco, "CharucoBoard_create") + ): + raise RuntimeError( + "OpenCV sem CharucoBoard. " + "Use opencv-contrib-python." + ) + + +def get_aruco_dictionary(name: str): + require_aruco() + + if not hasattr(cv2.aruco, name): + available = sorted( + x + for x in dir(cv2.aruco) + if x.startswith("DICT_") + ) + + raise ValueError( + f"Dicionário ArUco inválido: {name}. " + f"Disponíveis: {available}" + ) + + dict_id = getattr(cv2.aruco, name) + + if hasattr( + cv2.aruco, + "getPredefinedDictionary", + ): + return cv2.aruco.getPredefinedDictionary( + dict_id + ) + + return cv2.aruco.Dictionary_get( + dict_id + ) + + +def create_charuco_board( + squares_x, + squares_y, + square_length, + marker_length, + dictionary, +): + require_aruco() + + if hasattr(cv2.aruco, "CharucoBoard"): + try: + return cv2.aruco.CharucoBoard( + ( + int(squares_x), + int(squares_y), + ), + float(square_length), + float(marker_length), + dictionary, + ) + except TypeError: + pass + + if hasattr( + cv2.aruco, + "CharucoBoard_create", + ): + return cv2.aruco.CharucoBoard_create( + int(squares_x), + int(squares_y), + float(square_length), + float(marker_length), + dictionary, + ) + + raise RuntimeError( + "API CharucoBoard incompatível." + ) + + +def create_detector_params(): + if hasattr( + cv2.aruco, + "DetectorParameters", + ): + params = cv2.aruco.DetectorParameters() + else: + params = cv2.aruco.DetectorParameters_create() + + # Conservador, prioriza robustez. + for name, value in ( + ("cornerRefinementMethod", getattr(cv2.aruco, "CORNER_REFINE_SUBPIX", 1)), + ("cornerRefinementWinSize", 5), + ("cornerRefinementMaxIterations", 40), + ("cornerRefinementMinAccuracy", 0.01), + ): + try: + setattr(params, name, value) + except Exception: + pass + + return params + + +def prepare_charuco_gray( + img, + equalize=False, + invert=False, +): + gray = normalize_gray_u8(img) + + if invert: + gray = 255 - gray + + if equalize: + try: + clahe = cv2.createCLAHE( + clipLimit=2.0, + tileGridSize=(8, 8), + ) + gray = clahe.apply(gray) + except Exception: + gray = cv2.equalizeHist(gray) + + return gray + + +def detect_markers_compat( + gray, + dictionary, + params, +): + if hasattr(cv2.aruco, "ArucoDetector"): + detector = cv2.aruco.ArucoDetector( + dictionary, + params, + ) + + return detector.detectMarkers( + gray + ) + + return cv2.aruco.detectMarkers( + gray, + dictionary, + parameters=params, + ) + + +def interpolate_charuco_compat( + gray, + board, + marker_corners, + marker_ids, +): + if marker_ids is None or len(marker_ids) == 0: + return None, None + + # API moderna. + # IMPORTANTE: em OpenCV recente os argumentos posicionais 2/3 são + # charucoCorners/charucoIds de saída, não markerCorners/markerIds. + # Portanto usamos nomes explícitos para não trocar a semântica. + if hasattr(cv2.aruco, "CharucoDetector"): + try: + detector = cv2.aruco.CharucoDetector( + board + ) + + result = detector.detectBoard( + image=gray, + markerCorners=marker_corners, + markerIds=marker_ids, + ) + + if ( + isinstance(result, tuple) + and len(result) >= 2 + ): + return result[0], result[1] + + except Exception: + # Fallback: deixa o próprio CharucoDetector detectar os markers. + try: + result = detector.detectBoard( + gray + ) + + if ( + isinstance(result, tuple) + and len(result) >= 2 + ): + return result[0], result[1] + + except Exception: + pass + + # API legacy estável + if hasattr( + cv2.aruco, + "interpolateCornersCharuco", + ): + ret, corners, ids = ( + cv2.aruco.interpolateCornersCharuco( + markerCorners=marker_corners, + markerIds=marker_ids, + image=gray, + board=board, + ) + ) + + if ret is None or ret <= 0: + return None, None + + return corners, ids + + raise RuntimeError( + "Nenhuma API ChArUco compatível encontrada." + ) + + +def refine_charuco_subpix( + gray, + corners, +): + if corners is None or len(corners) == 0: + return corners + + pts = np.asarray( + corners, + dtype=np.float32, + ).reshape(-1, 1, 2) + + try: + cv2.cornerSubPix( + gray, + pts, + (4, 4), + (-1, -1), + ( + cv2.TERM_CRITERIA_EPS + + cv2.TERM_CRITERIA_MAX_ITER, + 40, + 0.01, + ), + ) + except Exception: + pass + + return pts + + +def detect_charuco_variant( + img, + board, + dictionary, + params, + equalize, + invert, +): + gray = prepare_charuco_gray( + img, + equalize=equalize, + invert=invert, + ) + + marker_corners, marker_ids, rejected = ( + detect_markers_compat( + gray, + dictionary, + params, + ) + ) + + marker_count = ( + 0 + if marker_ids is None + else int(len(marker_ids)) + ) + + if marker_count == 0: + return { + "points": {}, + "markers": 0, + "corners": 0, + "equalize": equalize, + "invert": invert, + "gray": gray, + } + + charuco_corners, charuco_ids = ( + interpolate_charuco_compat( + gray, + board, + marker_corners, + marker_ids, + ) + ) + + if ( + charuco_corners is None + or charuco_ids is None + ): + return { + "points": {}, + "markers": marker_count, + "corners": 0, + "equalize": equalize, + "invert": invert, + "gray": gray, + } + + charuco_corners = ( + refine_charuco_subpix( + gray, + charuco_corners, + ) + ) + + ids_flat = np.asarray( + charuco_ids + ).reshape(-1) + + pts = np.asarray( + charuco_corners + ).reshape(-1, 2) + + point_by_id = { + int(cid): ( + float(pt[0]), + float(pt[1]), + ) + for cid, pt in zip( + ids_flat, + pts, + ) + } + + return { + "points": point_by_id, + "markers": marker_count, + "corners": len(point_by_id), + "equalize": bool(equalize), + "invert": bool(invert), + "gray": gray, + } + + +def detect_charuco_best( + img, + board, + dictionary, + params, + allow_invert=True, +): + """ + Tenta variantes de contraste e escolhe a que produz mais cantos. + As coordenadas continuam no frame nativo original. + """ + variants = [ + (False, False), + (True, False), + ] + + if allow_invert: + variants += [ + (False, True), + (True, True), + ] + + results = [ + detect_charuco_variant( + img, + board, + dictionary, + params, + equalize=eq, + invert=inv, + ) + for eq, inv in variants + ] + + results.sort( + key=lambda x: ( + x["corners"], + x["markers"], + ), + reverse=True, + ) + + return results[0] + + +# ============================================================ +# Sample +# ============================================================ + +@dataclass +class PoseSignature: + center_x_norm: float + center_y_norm: float + bbox_area_norm: float + + +def pose_signature( + rgb_points: dict, + rgb_shape_hw, +) -> PoseSignature: + if not rgb_points: + return PoseSignature( + 0.0, + 0.0, + 0.0, + ) + + pts = np.array( + list(rgb_points.values()), + dtype=np.float32, + ) + + h, w = rgb_shape_hw + + x0 = float(np.min(pts[:, 0])) + x1 = float(np.max(pts[:, 0])) + y0 = float(np.min(pts[:, 1])) + y1 = float(np.max(pts[:, 1])) + + cx = 0.5 * (x0 + x1) + cy = 0.5 * (y0 + y1) + + area = ( + max(0.0, x1 - x0) + * max(0.0, y1 - y0) + ) + + return PoseSignature( + center_x_norm=cx / max(float(w), 1.0), + center_y_norm=cy / max(float(h), 1.0), + bbox_area_norm=area / max( + float(w * h), + 1.0, + ), + ) + + +def is_duplicate_pose( + signature: PoseSignature, + prior_signatures: list[PoseSignature], + qa: dict, +): + for prior in prior_signatures: + dc = math.hypot( + signature.center_x_norm + - prior.center_x_norm, + signature.center_y_norm + - prior.center_y_norm, + ) + + da = abs( + signature.bbox_area_norm + - prior.bbox_area_norm + ) + + if ( + dc + < qa["duplicate_center_distance_norm"] + and da + < qa["duplicate_scale_delta"] + ): + return True, { + "center_distance_norm": dc, + "area_delta_norm": da, + } + + return False, None + + +def common_pairs( + rgb_points: dict, + spec_points: dict, +): + ids = sorted( + set(rgb_points.keys()) + & set(spec_points.keys()) + ) + + src = [ + spec_points[cid] + for cid in ids + ] + + dst = [ + rgb_points[cid] + for cid in ids + ] + + return ids, src, dst + + +# ============================================================ +# Homography math +# ============================================================ + +def find_homography_ransac( + src_pts, + dst_pts, + qa, +): + if ( + len(src_pts) < 4 + or len(dst_pts) < 4 + or len(src_pts) != len(dst_pts) + ): + return None, None + + src = np.asarray( + src_pts, + dtype=np.float32, + ) + + dst = np.asarray( + dst_pts, + dtype=np.float32, + ) + + H, mask = cv2.findHomography( + src, + dst, + method=cv2.RANSAC, + ransacReprojThreshold=float( + qa["ransac_reproj_threshold_px"] + ), + maxIters=int( + qa["ransac_max_iters"] + ), + confidence=float( + qa["ransac_confidence"] + ), + ) + + if H is None: + return None, mask + + H = np.asarray( + H, + dtype=np.float64, + ) + + if abs(H[2, 2]) > 1e-12: + H = H / H[2, 2] + + return H, mask + + +def project_points( + H, + src_pts, +): + src = np.asarray( + src_pts, + dtype=np.float32, + ).reshape(-1, 1, 2) + + out = cv2.perspectiveTransform( + src, + np.asarray(H, dtype=np.float64), + ) + + return out.reshape(-1, 2) + + +def reprojection_errors( + H, + src_pts, + dst_pts, +): + projected = project_points( + H, + src_pts, + ) + + dst = np.asarray( + dst_pts, + dtype=np.float32, + ) + + return np.linalg.norm( + projected - dst, + axis=1, + ) + + +def error_stats(errors): + e = np.asarray( + errors, + dtype=np.float64, + ) + + if e.size == 0: + return { + "count": 0, + "mean_px": None, + "median_px": None, + "p95_px": None, + "max_px": None, + } + + return { + "count": int(e.size), + "mean_px": float(np.mean(e)), + "median_px": float(np.median(e)), + "p95_px": float( + np.percentile(e, 95) + ), + "max_px": float(np.max(e)), + } + + +def coverage_stats( + dst_pts, + image_size_wh, +): + w, h = image_size_wh + + pts = np.asarray( + dst_pts, + dtype=np.float32, + ) + + if len(pts) < 3: + return { + "span_x": 0.0, + "span_y": 0.0, + "hull_area_fraction": 0.0, + } + + x0 = float(np.min(pts[:, 0])) + x1 = float(np.max(pts[:, 0])) + y0 = float(np.min(pts[:, 1])) + y1 = float(np.max(pts[:, 1])) + + hull = cv2.convexHull( + pts.reshape(-1, 1, 2) + ) + + hull_area = float( + cv2.contourArea(hull) + ) + + return { + "span_x": ( + (x1 - x0) + / max(float(w), 1.0) + ), + "span_y": ( + (y1 - y0) + / max(float(h), 1.0) + ), + "hull_area_fraction": ( + hull_area + / max(float(w * h), 1.0) + ), + "bbox": [ + x0, + y0, + x1, + y1, + ], + } + + +def overlap_fraction( + H, + source_size_wh, + destination_size_wh, +): + sw, sh = source_size_wh + dw, dh = destination_size_wh + + mask = np.ones( + (int(sh), int(sw)), + dtype=np.uint8, + ) + + warped = cv2.warpPerspective( + mask, + np.asarray(H, dtype=np.float64), + (int(dw), int(dh)), + flags=cv2.INTER_NEAREST, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0, + ) + + return float( + (warped > 0).mean() + ) + + +def homography_condition(H): + Hn = np.asarray( + H, + dtype=np.float64, + ) + + if abs(Hn[2, 2]) > 1e-12: + Hn = Hn / Hn[2, 2] + + try: + return float( + np.linalg.cond(Hn) + ) + except Exception: + return float("inf") + + +# ============================================================ +# Dataset assembly / sample rejection +# ============================================================ + +def role_pairs_from_samples( + samples, + role, + sample_ids_filter=None, +): + src = [] + dst = [] + sample_ids = [] + charuco_ids = [] + + allowed = ( + None + if sample_ids_filter is None + else set(sample_ids_filter) + ) + + for sample in samples: + sid = int(sample["sample_id"]) + + if ( + allowed is not None + and sid not in allowed + ): + continue + + pair = sample["pairs"][role] + + for cid, spt, dpt in zip( + pair["charuco_ids"], + pair["src_points"], + pair["dst_points"], + ): + src.append(spt) + dst.append(dpt) + sample_ids.append(sid) + charuco_ids.append(int(cid)) + + return { + "src": src, + "dst": dst, + "sample_ids": sample_ids, + "charuco_ids": charuco_ids, + } + + +def per_sample_residuals( + H, + pairs, +): + errors = reprojection_errors( + H, + pairs["src"], + pairs["dst"], + ) + + by_sample = {} + + for err, sid in zip( + errors, + pairs["sample_ids"], + ): + by_sample.setdefault( + int(sid), + [], + ).append(float(err)) + + result = {} + + for sid, values in by_sample.items(): + st = error_stats(values) + + result[int(sid)] = st + + return result + + +def reject_incompatible_samples( + samples, + role, + qa, +): + retained = [ + int(s["sample_id"]) + for s in samples + ] + + rejected = [] + + rounds = [] + + for round_idx in range( + int(qa["max_sample_rejection_rounds"]) + 1 + ): + pairs = role_pairs_from_samples( + samples, + role, + sample_ids_filter=retained, + ) + + if len(pairs["src"]) < 4: + break + + H, mask = find_homography_ransac( + pairs["src"], + pairs["dst"], + qa, + ) + + if H is None: + break + + residual = per_sample_residuals( + H, + pairs, + ) + + bad = [ + sid + for sid, st in residual.items() + if ( + st["median_px"] is not None + and st["median_px"] + > qa["sample_reject_median_error_px"] + ) + ] + + rounds.append({ + "round": round_idx, + "retained": list(retained), + "bad_candidates": list(bad), + "per_sample": residual, + }) + + if not bad: + break + + # Remove primeiro o pior, mantendo mínimo de amostras. + worst = max( + bad, + key=lambda sid: residual[sid]["median_px"], + ) + + if ( + len(retained) - 1 + < int(qa["min_samples"]) + ): + break + + retained.remove(worst) + rejected.append(worst) + + return { + "retained_sample_ids": retained, + "rejected_sample_ids": rejected, + "rounds": rounds, + } + + +# ============================================================ +# Leave-one-sample-out validation +# ============================================================ + +def leave_one_sample_out_cv( + samples, + role, + retained_sample_ids, + qa, +): + retained = list( + sorted(set(retained_sample_ids)) + ) + + sample_reports = [] + all_errors = [] + + if len(retained) < 5: + return { + "valid": False, + "reason": "menos_de_5_amostras", + "samples": [], + "overall": error_stats([]), + } + + for held_out in retained: + train_ids = [ + sid + for sid in retained + if sid != held_out + ] + + train = role_pairs_from_samples( + samples, + role, + train_ids, + ) + + test = role_pairs_from_samples( + samples, + role, + [held_out], + ) + + if ( + len(train["src"]) < 4 + or len(test["src"]) == 0 + ): + continue + + H, _ = find_homography_ransac( + train["src"], + train["dst"], + qa, + ) + + if H is None: + continue + + errors = reprojection_errors( + H, + test["src"], + test["dst"], + ) + + all_errors.extend( + [float(x) for x in errors] + ) + + sample_reports.append({ + "held_out_sample_id": int(held_out), + "train_samples": len(train_ids), + "test_points": len(test["src"]), + "error": error_stats(errors), + }) + + return { + "valid": len(sample_reports) == len(retained), + "samples": sample_reports, + "overall": error_stats(all_errors), + } + + +# ============================================================ +# Role model +# ============================================================ + +def build_role_model( + samples, + role, + specs, + qa, +): + rejection = reject_incompatible_samples( + samples, + role, + qa, + ) + + retained = rejection[ + "retained_sample_ids" + ] + + pairs = role_pairs_from_samples( + samples, + role, + retained, + ) + + H, inlier_mask = find_homography_ransac( + pairs["src"], + pairs["dst"], + qa, + ) + + if H is None: + return { + "status": "bad", + "reasons": [ + "findHomography_failed" + ], + "role": role, + "rejection": rejection, + } + + errors = reprojection_errors( + H, + pairs["src"], + pairs["dst"], + ) + + if inlier_mask is None: + inlier = np.ones( + len(errors), + dtype=bool, + ) + else: + inlier = ( + np.asarray(inlier_mask) + .reshape(-1) + .astype(bool) + ) + + inlier_errors = ( + errors[inlier] + if np.any(inlier) + else errors + ) + + fit = error_stats( + inlier_errors + ) + + inlier_ratio = float( + np.sum(inlier) + / max(len(inlier), 1) + ) + + unique_ids = sorted( + set(pairs["charuco_ids"]) + ) + + dst_size = ( + specs["rgb"].width, + specs["rgb"].height, + ) + + src_size = ( + specs[role].width, + specs[role].height, + ) + + coverage = coverage_stats( + pairs["dst"], + dst_size, + ) + + overlap = overlap_fraction( + H, + src_size, + dst_size, + ) + + condition = homography_condition(H) + + cv_report = leave_one_sample_out_cv( + samples, + role, + retained, + qa, + ) + + cv_overall = cv_report["overall"] + + cv_sample_medians = [ + float(x["error"]["median_px"]) + for x in cv_report["samples"] + if x["error"]["median_px"] is not None + ] + + cv_max_sample_median = ( + max(cv_sample_medians) + if cv_sample_medians + else None + ) + + # Inverse validation no conjunto final + try: + H_inv = np.linalg.inv(H) + + reverse_errors = reprojection_errors( + H_inv, + pairs["dst"], + pairs["src"], + ) + + reverse_fit = error_stats( + reverse_errors + ) + + except Exception: + H_inv = None + reverse_fit = error_stats([]) + + # QA + status = "good" + reasons = [] + + def check_low( + value, + warning, + bad, + name, + ): + nonlocal status + + if value < bad: + status = merge_status(status, "bad") + reasons.append( + f"{name}_bad:{value:.4f}" + ) + + elif value < warning: + status = merge_status(status, "warning") + reasons.append( + f"{name}_warning:{value:.4f}" + ) + + def check_high( + value, + warning, + bad, + name, + ): + nonlocal status + + if value > bad: + status = merge_status(status, "bad") + reasons.append( + f"{name}_bad:{value:.4f}" + ) + + elif value > warning: + status = merge_status(status, "warning") + reasons.append( + f"{name}_warning:{value:.4f}" + ) + + if len(retained) < qa["min_samples"]: + status = "bad" + reasons.append( + f"samples_bad:{len(retained)}" + ) + + if len(pairs["src"]) < qa["min_total_pairs"]: + status = "bad" + reasons.append( + f"pairs_bad:{len(pairs['src'])}" + ) + + if len(unique_ids) < qa["min_unique_charuco_ids"]: + status = "bad" + reasons.append( + f"unique_ids_bad:{len(unique_ids)}" + ) + + check_low( + inlier_ratio, + qa["inlier_ratio_warning"], + qa["inlier_ratio_bad"], + "inlier_ratio", + ) + + if fit["median_px"] is not None: + check_high( + fit["median_px"], + qa["fit_median_error_warning_px"], + qa["fit_median_error_bad_px"], + "fit_median_px", + ) + + if fit["p95_px"] is not None: + check_high( + fit["p95_px"], + qa["fit_p95_error_warning_px"], + qa["fit_p95_error_bad_px"], + "fit_p95_px", + ) + + if not cv_report["valid"]: + status = "bad" + reasons.append( + "cross_validation_invalid" + ) + + if cv_overall["median_px"] is not None: + check_high( + cv_overall["median_px"], + qa["cv_median_error_warning_px"], + qa["cv_median_error_bad_px"], + "cv_median_px", + ) + + if cv_overall["p95_px"] is not None: + check_high( + cv_overall["p95_px"], + qa["cv_p95_error_warning_px"], + qa["cv_p95_error_bad_px"], + "cv_p95_px", + ) + + if cv_max_sample_median is not None: + check_high( + cv_max_sample_median, + qa["cv_max_sample_median_warning_px"], + qa["cv_max_sample_median_bad_px"], + "cv_max_sample_median_px", + ) + + check_low( + coverage["span_x"], + qa["coverage_span_x_warning"], + qa["coverage_span_x_bad"], + "coverage_span_x", + ) + + check_low( + coverage["span_y"], + qa["coverage_span_y_warning"], + qa["coverage_span_y_bad"], + "coverage_span_y", + ) + + check_low( + coverage["hull_area_fraction"], + qa["coverage_hull_warning"], + qa["coverage_hull_bad"], + "coverage_hull", + ) + + check_low( + overlap, + qa["overlap_warning"], + qa["overlap_bad"], + "overlap", + ) + + check_high( + condition, + qa["condition_number_warning"], + qa["condition_number_bad"], + "condition_number", + ) + + return { + "status": status, + "reasons": reasons, + "role": role, + "homography": H.tolist(), + "homography_inverse": ( + H_inv.tolist() + if H_inv is not None + else None + ), + "source_size": [ + int(src_size[0]), + int(src_size[1]), + ], + "destination_size": [ + int(dst_size[0]), + int(dst_size[1]), + ], + "retained_sample_ids": retained, + "rejected_sample_ids": rejection[ + "rejected_sample_ids" + ], + "sample_rejection": rejection, + "points": int(len(pairs["src"])), + "unique_charuco_ids": len(unique_ids), + "unique_charuco_id_list": unique_ids, + "inliers": int(np.sum(inlier)), + "outliers": int( + len(inlier) - np.sum(inlier) + ), + "inlier_ratio": inlier_ratio, + "fit_error": fit, + "reverse_fit_error_source_px": reverse_fit, + "cross_validation": cv_report, + "cv_max_sample_median_px": cv_max_sample_median, + "coverage": coverage, + "overlap_fraction": overlap, + "condition_number": condition, + } + + +# ============================================================ +# Profile QA +# ============================================================ + +def evaluate_profile( + samples, + specs, + qa, +): + models = { + role: build_role_model( + samples, + role, + specs, + qa, + ) + for role in SPEC_ROLES + } + + status = "good" + reasons = [] + + for role in SPEC_ROLES: + model = models[role] + + status = merge_status( + status, + model["status"], + ) + + reasons.extend( + [ + f"{role}:{x}" + for x in model.get( + "reasons", + [], + ) + ] + ) + + retained_common = sorted( + set( + models["re"].get( + "retained_sample_ids", + [], + ) + ) + & set( + models["nir"].get( + "retained_sample_ids", + [], + ) + ) + ) + + return { + "status": status, + "reasons": reasons, + "models": models, + "common_retained_sample_ids": retained_common, + "common_frames_used": len( + retained_common + ), + } + + +# ============================================================ +# Detection / sample capture +# ============================================================ + +def detect_triplet( + triplet, + board, + dictionary, + detector_params, + qa, + allow_invert=True, +): + detections = {} + + for role in ROLES: + detections[role] = detect_charuco_best( + triplet["frames"][role], + board, + dictionary, + detector_params, + allow_invert=allow_invert, + ) + + reasons = [] + + for role in ROLES: + det = detections[role] + + if det["markers"] < qa["min_markers_each"]: + reasons.append( + f"{role}:markers={det['markers']}" + ) + + if det["corners"] < qa["min_charuco_corners_each"]: + reasons.append( + f"{role}:corners={det['corners']}" + ) + + pairs = {} + + rgb_points = detections["rgb"]["points"] + + for role in SPEC_ROLES: + ids, src, dst = common_pairs( + rgb_points, + detections[role]["points"], + ) + + pairs[role] = { + "charuco_ids": ids, + "src_points": [ + [float(x), float(y)] + for x, y in src + ], + "dst_points": [ + [float(x), float(y)] + for x, y in dst + ], + } + + if len(ids) < qa["min_common_corners_each_pair"]: + reasons.append( + f"{role}:common={len(ids)}" + ) + + sync_dt_ms = triplet["sync_dt_ms"] + + if ( + sync_dt_ms is not None + and sync_dt_ms > qa["sync_tolerance_ms"] + ): + reasons.append( + f"sync_dt_ms={sync_dt_ms:.2f}" + ) + + signature = pose_signature( + rgb_points, + triplet["frames"]["rgb"].shape[:2], + ) + + return { + "valid": len(reasons) == 0, + "reasons": reasons, + "detections": detections, + "pairs": pairs, + "pose_signature": asdict(signature), + "sync_dt_ms": sync_dt_ms, + } + + +# ============================================================ +# Preflight +# ============================================================ + +def sample_ae_controls( + runtime: AcquisitionRuntime, + args, +): + locked = {} + + for role in ROLES: + exp_override = getattr( + args, + f"{role}_exp_us", + ) + + iso_override = getattr( + args, + f"{role}_iso", + ) + + if ( + exp_override is not None + or iso_override is not None + ): + if ( + exp_override is None + or iso_override is None + ): + raise ValueError( + f"Override {role}: informe EXP e ISO." + ) + + locked[role] = LockedControl( + role=role, + exposure_time_us=int( + exp_override + ), + sensitivity_iso=int( + iso_override + ), + source="cli_manual", + ) + + continue + + exp_values = [] + iso_values = [] + + t0 = time.time() + + while time.time() - t0 < 0.8: + fresh = runtime.poll() + + if role not in fresh: + time.sleep(0.002) + continue + + ctrl = runtime.controls[role] + + exp = safe_int( + ctrl.get( + "exposure_time_us" + ), + None, + ) + + iso = safe_int( + ctrl.get( + "sensitivity_iso" + ), + None, + ) + + if exp is not None and exp > 0: + exp_values.append(exp) + + if iso is not None and iso > 0: + iso_values.append(iso) + + if not exp_values or not iso_values: + raise RuntimeError( + f"Não consegui ler EXP/ISO de {role.upper()}." + ) + + locked[role] = LockedControl( + role=role, + exposure_time_us=int( + round( + np.median(exp_values) + ) + ), + sensitivity_iso=int( + round( + np.median(iso_values) + ) + ), + source="ae_snapshot", + ) + + return locked + + +def preflight_detection( + runtime, + board, + dictionary, + detector_params, + qa, + allow_invert, +): + frames = runtime.frames + + result = { + "status": "good", + "roles": {}, + "reasons": [], + } + + for role in ROLES: + if frames[role] is None: + result["status"] = "bad" + result["roles"][role] = { + "status": "bad", + "markers": 0, + "corners": 0, + "reasons": ["sem_frame"], + } + continue + + det = detect_charuco_best( + frames[role], + board, + dictionary, + detector_params, + allow_invert=allow_invert, + ) + + role_status = "good" + reasons = [] + + if det["markers"] < qa["min_markers_each"]: + role_status = "bad" + reasons.append( + f"markers={det['markers']}" + ) + + if det["corners"] < qa["min_charuco_corners_each"]: + role_status = "bad" + reasons.append( + f"corners={det['corners']}" + ) + + result["roles"][role] = { + "status": role_status, + "markers": det["markers"], + "corners": det["corners"], + "equalize": det["equalize"], + "invert": det["invert"], + "reasons": reasons, + } + + result["status"] = merge_status( + result["status"], + role_status, + ) + + result["reasons"].extend( + [ + f"{role}:{x}" + for x in reasons + ] + ) + + return result + + +# ============================================================ +# Visualization +# ============================================================ + +def draw_detected_points( + panel, + point_by_id, + source_shape_hw, + color, + max_labels=60, +): + if not point_by_id: + return + + ph, pw = panel.shape[:2] + sh, sw = source_shape_hw + + for idx, (cid, pt) in enumerate( + sorted(point_by_id.items()) + ): + x = int( + pt[0] + * pw + / max(sw, 1) + ) + + y = int( + pt[1] + * ph + / max(sh, 1) + ) + + cv2.circle( + panel, + (x, y), + 3, + color, + -1, + ) + + if idx < max_labels: + cv2.putText( + panel, + str(cid), + (x + 4, y - 4), + cv2.FONT_HERSHEY_SIMPLEX, + 0.34, + color, + 1, + cv2.LINE_AA, + ) + + +def provisional_h( + samples, + role, + qa, +): + if not samples: + return None + + pairs = role_pairs_from_samples( + samples, + role, + ) + + if len(pairs["src"]) < 4: + return None + + H, _ = find_homography_ransac( + pairs["src"], + pairs["dst"], + qa, + ) + + return H + + +def build_overlay( + rgb, + spec, + role, + H, + alpha=0.45, +): + if rgb is None: + return None + + if rgb.ndim == 2: + base = cv2.cvtColor( + rgb, + cv2.COLOR_GRAY2BGR, + ) + else: + base = rgb.copy() + + if spec is None or H is None: + return base + + dw = base.shape[1] + dh = base.shape[0] + + if spec.ndim == 3: + spec_gray = cv2.cvtColor( + spec, + cv2.COLOR_BGR2GRAY, + ) + else: + spec_gray = spec + + warped = cv2.warpPerspective( + spec_gray, + np.asarray(H, dtype=np.float64), + (dw, dh), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0, + ) + + colored = colorize_spec( + warped, + role, + ) + + return cv2.addWeighted( + base, + 1.0 - alpha, + colored, + alpha, + 0.0, + ) + + +def save_sample_preview( + path, + frames, + detection, + panel_width=480, + panel_height=300, +): + panels = [] + + colors = { + "rgb": (0, 255, 0), + "re": (0, 255, 255), + "nir": (255, 255, 0), + } + + for role in ROLES: + img = frames[role] + + if img.ndim == 2: + view = cv2.cvtColor( + img, + cv2.COLOR_GRAY2BGR, + ) + else: + view = img.copy() + + panel = resize_panel( + view, + panel_width, + panel_height, + ) + + draw_detected_points( + panel, + detection["detections"][role]["points"], + img.shape[:2], + colors[role], + ) + + overlay_hud( + panel, + [ + role.upper(), + f"markers={detection['detections'][role]['markers']}", + f"corners={detection['detections'][role]['corners']}", + ], + x=8, + y=18, + font_scale=0.38, + line_step=16, + ) + + panels.append(panel) + + board = np.hstack(panels) + + cv2.imwrite( + str(path), + board, + ) + + +def build_ui_board( + runtime, + specs, + samples, + selected_role, + last_detection, + profile_eval, + args, + qa, + message="", +): + pw = int(args.panel_width) + ph = int(args.panel_height) + + H_preview = provisional_h( + samples, + selected_role, + qa, + ) + + overlay_native = build_overlay( + runtime.frames["rgb"], + runtime.frames[selected_role], + selected_role, + H_preview, + alpha=args.overlay_alpha, + ) + + panels = {} + + for role in ROLES: + img = runtime.frames[role] + + if img is None: + panel = np.zeros( + (ph, pw, 3), + dtype=np.uint8, + ) + + overlay_hud( + panel, + [ + role.upper(), + "sem frame", + ], + ) + + panels[role] = panel + continue + + if img.ndim == 2: + if role in SPEC_ROLES: + view = colorize_spec( + img, + role, + ) + else: + view = cv2.cvtColor( + img, + cv2.COLOR_GRAY2BGR, + ) + else: + view = img.copy() + + panel = resize_panel( + view, + pw, + ph, + ) + + if ( + last_detection is not None + and role + in last_detection["detections"] + ): + colors = { + "rgb": (0, 255, 0), + "re": (0, 255, 255), + "nir": (255, 255, 0), + } + + draw_detected_points( + panel, + last_detection[ + "detections" + ][role]["points"], + img.shape[:2], + colors[role], + ) + + overlay_hud( + panel, + [ + f"{role.upper()} | {specs[role].socket_name} | {specs[role].sensor_name}", + f"{img.shape[1]}x{img.shape[0]}", + f"EXP={runtime.controls[role].get('exposure_time_us')}us " + f"ISO={runtime.controls[role].get('sensitivity_iso')}", + f"FPS={runtime.fps[role]:.1f}", + ], + x=8, + y=18, + font_scale=0.38, + line_step=16, + ) + + panels[role] = panel + + if overlay_native is None: + overlay_panel = np.zeros( + (ph, pw, 3), + dtype=np.uint8, + ) + else: + overlay_panel = resize_panel( + overlay_native, + pw, + ph, + ) + + overlay_hud( + overlay_panel, + [ + f"OVERLAY RGB + {selected_role.upper()}", + ( + "H provisional" + if H_preview is not None + else "H ainda indisponivel" + ), + ], + x=8, + y=18, + font_scale=0.40, + line_step=18, + ) + + data = np.zeros( + (ph, pw, 3), + dtype=np.uint8, + ) + + signatures = [ + PoseSignature( + **sample["pose_signature"] + ) + for sample in samples + ] + + rgb_pts_all = [] + + for sample in samples: + for role in SPEC_ROLES: + rgb_pts_all.extend( + sample["pairs"][role][ + "dst_points" + ] + ) + + coverage = coverage_stats( + rgb_pts_all, + ( + specs["rgb"].width, + specs["rgb"].height, + ), + ) if rgb_pts_all else { + "span_x": 0.0, + "span_y": 0.0, + "hull_area_fraction": 0.0, + } + + lines = [ + "HOMOGRAPHY CALIBRATION - PRODUCTION", + f"profile={args.profile_name} | depth={args.depth_cm} cm", + f"selected overlay={selected_role.upper()}", + "", + f"samples={len(samples)} / min={qa['min_samples']} rec={qa['recommended_samples']}", + f"coverage X={coverage['span_x']*100:.1f}%", + f"coverage Y={coverage['span_y']*100:.1f}%", + f"coverage hull={coverage['hull_area_fraction']*100:.1f}%", + "", + "G captura sample | A avalia", + "2/3 overlay RE/NIR | TAB alterna", + "V limpa sessão | S snapshot", + "Q/ESC cancela", + ] + + if last_detection is not None: + lines.extend([ + "", + f"last sync={last_detection.get('sync_dt_ms')} ms", + f"last RGB corners={last_detection['detections']['rgb']['corners']}", + f"last RE common={len(last_detection['pairs']['re']['charuco_ids'])}", + f"last NIR common={len(last_detection['pairs']['nir']['charuco_ids'])}", + ]) + + if profile_eval is not None: + lines.extend([ + "", + f"LAST EVAL={profile_eval['status'].upper()}", + ]) + + for role in SPEC_ROLES: + model = profile_eval["models"][role] + + if "fit_error" in model: + lines.append( + f"{role.upper()}: fit med=" + f"{model['fit_error']['median_px']:.2f}px " + f"CV med=" + f"{model['cross_validation']['overall']['median_px']:.2f}px " + f"overlap={model['overlap_fraction']*100:.1f}%" + ) + + overlay_hud( + data, + lines, + x=12, + y=20, + font_scale=0.40, + line_step=17, + ) + + board = np.vstack([ + np.hstack([ + overlay_panel, + panels["rgb"], + ]), + np.hstack([ + panels["re"], + panels["nir"], + ]), + ]) + + # Data mini-panel sobre o overlay inferior direito por composição? + # Usamos uma janela maior com data à direita para não esconder imagens. + top = np.hstack([ + overlay_panel, + panels["rgb"], + data, + ]) + + bottom = np.hstack([ + panels["re"], + panels["nir"], + np.zeros_like(data), + ]) + + board = np.vstack([ + top, + bottom, + ]) + + if message: + cv2.putText( + board, + message, + ( + 16, + board.shape[0] - 14, + ), + cv2.FONT_HERSHEY_SIMPLEX, + 0.55, + (0, 255, 0), + 2, + cv2.LINE_AA, + ) + + return board + + +# ============================================================ +# Active artifact / module fragment +# ============================================================ + +def hardware_signature(specs): + return { + role: { + "socket": specs[role].socket_name, + "sensor": specs[role].sensor_name, + "size": [ + specs[role].width, + specs[role].height, + ], + } + for role in ROLES + } + + +def profile_to_module_fragment( + profile_name, + profile_payload, + baseline_mm, +): + return { + "fusion_config": { + "alignment_mode": "homography", + "baseline_mm": float( + baseline_mm + ), + "reference_camera": "rgb", + "homography_profile": profile_name, + "homography_profile_by_role": { + "re": profile_name, + "nir": profile_name, + }, + "homography_profiles": { + profile_name: { + "description": profile_payload[ + "description" + ], + "depth": profile_payload[ + "depth_cm" + ], + "depth_unit": "cm", + "homography_source": ( + "charuco_native_multi_sample_group_cv" + ), + "coordinate_space": COORDINATE_SPACE, + "reference_size": profile_payload[ + "coordinate_spaces" + ]["rgb"]["size"], + "source_size_by_role": { + "re": profile_payload[ + "coordinate_spaces" + ]["re"]["size"], + "nir": profile_payload[ + "coordinate_spaces" + ]["nir"]["size"], + }, + # Compatibilidade semântica com assembler antigo: + # agora significa TAMANHO DA REFERÊNCIA RGB. + "homography_calibration_size": profile_payload[ + "coordinate_spaces" + ]["rgb"]["size"], + "homography_stats": profile_payload[ + "homography_stats" + ], + "homographies": { + "re_to_rgb": profile_payload[ + "homographies" + ]["re_to_rgb"], + "nir_to_rgb": profile_payload[ + "homographies" + ]["nir_to_rgb"], + }, + } + }, + } + } + + +def merge_all_profiles_fragment( + active_data, +): + profiles = active_data.get( + "profiles", + {}, + ) + + recommended = active_data.get( + "recommended_profile" + ) + + fusion_profiles = {} + + for name, profile in profiles.items(): + one = profile_to_module_fragment( + name, + profile, + active_data["baseline_mm"], + ) + + fusion_profiles[name] = ( + one["fusion_config"][ + "homography_profiles" + ][name] + ) + + return { + "fusion_config": { + "alignment_mode": "homography", + "baseline_mm": float( + active_data["baseline_mm"] + ), + "reference_camera": "rgb", + "homography_profile": recommended, + "homography_profile_by_role": { + "re": recommended, + "nir": recommended, + }, + "homography_profiles": fusion_profiles, + } + } + + +def create_or_update_active( + active_path: Path, + candidate_report: dict, + profile_payload: dict, + args, + specs, +): + active = load_json( + active_path + ) + + signature = hardware_signature( + specs + ) + + if active: + if active.get("schema") != SCHEMA: + raise RuntimeError( + f"Arquivo ativo existente usa schema " + f"{active.get('schema')!r}, esperado {SCHEMA!r}. " + "Use outro --active-json ou arquive o antigo." + ) + + if ( + active.get( + "hardware_signature" + ) + != signature + ): + raise RuntimeError( + "Homography ativa pertence a outro conjunto de sensores/resoluções. " + "Não é seguro misturar perfis antigos com o hardware atual." + ) + + if ( + active.get("coordinate_space") + != COORDINATE_SPACE + ): + raise RuntimeError( + "Coordinate space do ativo é incompatível." + ) + + else: + active = { + "schema": SCHEMA, + "created_at": now_str(), + "hardware_signature": signature, + "coordinate_space": COORDINATE_SPACE, + "reference_camera": "rgb", + "baseline_mm": float( + args.baseline_mm + ), + "profiles": {}, + "recommended_profile": None, + "module_params_fragment": {}, + } + + profile_name = args.profile_name + + active["updated_at"] = now_str() + active["profiles"][profile_name] = ( + profile_payload + ) + + if ( + active["recommended_profile"] is None + or args.set_recommended + ): + active["recommended_profile"] = ( + profile_name + ) + + active["module_params_fragment"] = ( + merge_all_profiles_fragment( + active + ) + ) + + active["last_candidate_session"] = ( + candidate_report["session_id"] + ) + + ensure_dir( + active_path.parent + ) + + if active_path.exists(): + backup = active_path.with_name( + active_path.stem + + f".backup_{session_stamp()}" + + active_path.suffix + ) + + shutil.copy2( + active_path, + backup, + ) + + save_json_atomic( + active_path, + active, + ) + + return active + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser( + description=( + "Homography calibration de produção por ChArUco " + "para OAK-FFC-3P OV9782/AR0234 + 2x OV9282." + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # Perfil + parser.add_argument( + "--profile-name", + default="media", + ) + + parser.add_argument( + "--depth-cm", + type=float, + default=66.0, + ) + + parser.add_argument( + "--profile-description", + default="Plano de calibração ChArUco.", + ) + + parser.add_argument( + "--baseline-mm", + type=float, + default=75.0, + ) + + parser.add_argument( + "--set-recommended", + action="store_true", + help=( + "Define este perfil como recomendado no JSON ativo." + ), + ) + + # Hardware + parser.add_argument( + "--mx-id", + default=None, + ) + + parser.add_argument( + "--fps", + type=float, + default=20.0, + ) + + parser.add_argument( + "--isp-sharpness", + type=int, + default=0, + ) + + parser.add_argument( + "--isp-luma-denoise", + type=int, + default=0, + ) + + parser.add_argument( + "--isp-chroma-denoise", + type=int, + default=0, + ) + + # Controles manuais opcionais + for role in ROLES: + parser.add_argument( + f"--{role}-exp-us", + type=int, + default=None, + ) + + parser.add_argument( + f"--{role}-iso", + type=int, + default=None, + ) + + parser.add_argument( + "--lock-settle-frames", + type=int, + default=8, + ) + + # ChArUco + parser.add_argument( + "--charuco-dictionary", + default="DICT_4X4_50", + ) + + parser.add_argument( + "--charuco-squares-x", + type=int, + default=13, + ) + + parser.add_argument( + "--charuco-squares-y", + type=int, + default=7, + ) + + parser.add_argument( + "--charuco-square-length", + type=float, + default=0.031, + ) + + parser.add_argument( + "--charuco-marker-length", + type=float, + default=0.023, + ) + + parser.add_argument( + "--no-auto-invert", + action="store_true", + help=( + "Não tenta versões invertidas na detecção RE/NIR/RGB." + ), + ) + + # Política + parser.add_argument( + "--promote-warning", + action="store_true", + help=( + "Permite promover perfil WARNING. BAD nunca promove." + ), + ) + + # UI + parser.add_argument( + "--panel-width", + type=int, + default=560, + ) + + parser.add_argument( + "--panel-height", + type=int, + default=350, + ) + + parser.add_argument( + "--overlay-alpha", + type=float, + default=0.45, + ) + + # Output + parser.add_argument( + "--candidate-root", + default="calibration/homography_candidates", + ) + + parser.add_argument( + "--active-json", + default="calibration/homography_calibration_v4.json", + ) + + # Rastreabilidade + parser.add_argument( + "--module-id", + default="", + ) + + parser.add_argument( + "--operator", + default="", + ) + + parser.add_argument( + "--notes", + default="", + ) + + args = parser.parse_args() + + qa = dict(DEFAULT_QA) + + sid = session_stamp() + candidate_dir = ( + Path(args.candidate_root) + / sid + ) + + sample_dir = ( + candidate_dir + / "samples" + ) + + snapshot_dir = ( + candidate_dir + / "snapshots" + ) + + candidate_report_path = ( + candidate_dir + / "report.json" + ) + + ensure_dir(sample_dir) + ensure_dir(snapshot_dir) + + require_aruco() + + dictionary = get_aruco_dictionary( + args.charuco_dictionary + ) + + board = create_charuco_board( + args.charuco_squares_x, + args.charuco_squares_y, + args.charuco_square_length, + args.charuco_marker_length, + dictionary, + ) + + detector_params = create_detector_params() + + camera_rows = [] + specs = {} + report = None + samples = [] + last_detection = None + profile_eval = None + actual_mx = None + usb_speed = None + + window_name = ( + "Homography Calibration - Production" + ) + + cv2.namedWindow( + window_name, + cv2.WINDOW_NORMAL, + ) + + try: + # ---------------------------------------------------- + # Hardware discovery + # ---------------------------------------------------- + + ( + camera_rows, + actual_mx, + usb_speed, + ) = discover_cameras( + args.mx_id + ) + + topology = validate_product_topology( + camera_rows + ) + + specs = build_specs( + camera_rows + ) + + print("=" * 88) + print("HOMOGRAPHY CALIBRATION - PRODUCTION") + print(f"Profile : {args.profile_name}") + print(f"Depth : {args.depth_cm} cm") + print(f"MX ID : {actual_mx}") + print(f"USB : {usb_speed}") + print("-" * 88) + + for role in ROLES: + spec = specs[role] + + print( + f"{spec.socket_name} -> " + f"{role.upper():3s} | " + f"{spec.sensor_name:8s} | " + f"{spec.width}x{spec.height} | " + f"{spec.source}" + ) + + print("-" * 88) + print( + f"H_RE : 1280x800 -> " + f"{specs['rgb'].width}x{specs['rgb'].height}" + ) + print( + f"H_NIR: 1280x800 -> " + f"{specs['rgb'].width}x{specs['rgb'].height}" + ) + print("=" * 88) + + pipeline, isp_settings = ( + build_pipeline( + specs, + args, + ) + ) + + device_info = ( + dai.DeviceInfo(actual_mx) + if actual_mx + else None + ) + + device_ctx = ( + dai.Device( + pipeline, + device_info, + ) + if device_info is not None + else dai.Device(pipeline) + ) + + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + "status": "running", + "promoted": False, + "depthai_version": getattr( + dai, + "__version__", + "unknown", + ), + "opencv_version": cv2.__version__, + "device_mx_id": actual_mx, + "usb_speed": usb_speed, + "topology": topology, + "camera_inventory": camera_rows, + "hardware_signature": hardware_signature( + specs + ), + "coordinate_space": COORDINATE_SPACE, + "coordinate_spaces": { + role: { + "role": role, + "sensor": specs[role].sensor_name, + "size": [ + specs[role].width, + specs[role].height, + ], + "stream_source": specs[role].source, + "external_undistort_applied": False, + } + for role in ROLES + }, + "profile": { + "name": args.profile_name, + "depth_cm": float( + args.depth_cm + ), + "description": args.profile_description, + "same_physical_plane_required": True, + }, + "baseline_mm": float( + args.baseline_mm + ), + "charuco": { + "dictionary": args.charuco_dictionary, + "squares_x": int( + args.charuco_squares_x + ), + "squares_y": int( + args.charuco_squares_y + ), + "square_length": float( + args.charuco_square_length + ), + "marker_length": float( + args.charuco_marker_length + ), + "auto_invert": not args.no_auto_invert, + }, + "isp_measurement_settings": isp_settings, + "qa_thresholds": qa, + "traceability": { + "module_id": args.module_id or None, + "operator": args.operator or None, + }, + "notes": args.notes or "", + "locked_controls": None, + "preflight": None, + "samples": [], + "evaluation": None, + "module_params_fragment": None, + "candidate_dir": str( + candidate_dir + ), + "active_json": args.active_json, + } + + with device_ctx as device: + runtime = AcquisitionRuntime( + device, + specs, + args, + qa, + ) + + runtime.wait_all() + + # ------------------------------------------------ + # Preflight + # ------------------------------------------------ + + print("") + print("[PRE-FLIGHT]") + print( + "Mostre o ChArUco às três câmeras no plano que será calibrado." + ) + print( + "ENTER congela exposição quando as três detecções estiverem GOOD." + ) + + while True: + runtime.poll() + + preflight = preflight_detection( + runtime, + board, + dictionary, + detector_params, + qa, + allow_invert=( + not args.no_auto_invert + ), + ) + + panels = [] + + for role in ROLES: + img = runtime.frames[role] + + if img.ndim == 2: + view = cv2.cvtColor( + img, + cv2.COLOR_GRAY2BGR, + ) + else: + view = img.copy() + + panel = resize_panel( + view, + args.panel_width, + args.panel_height, + ) + + p = preflight["roles"][role] + + overlay_hud( + panel, + [ + f"{role.upper()} | {specs[role].sensor_name}", + f"PREFLIGHT={p['status'].upper()}", + f"markers={p['markers']} corners={p['corners']}", + f"EXP={runtime.controls[role].get('exposure_time_us')}us " + f"ISO={runtime.controls[role].get('sensitivity_iso')}", + ], + x=10, + y=22, + font_scale=0.42, + line_step=18, + ) + + panels.append(panel) + + data = np.zeros( + ( + args.panel_height, + args.panel_width, + 3, + ), + dtype=np.uint8, + ) + + overlay_hud( + data, + [ + "HOMOGRAPHY PRE-FLIGHT", + "", + f"GLOBAL={preflight['status'].upper()}", + "", + "Mesmo plano físico durante toda a sessão.", + "Mova lateralmente, não mude altura/inclinação.", + "", + "ENTER = lock EXP/ISO", + "Q/ESC = cancelar", + "", + *[ + f"! {x}" + for x in preflight[ + "reasons" + ][:8] + ], + ], + x=14, + y=28, + font_scale=0.44, + line_step=20, + ) + + pre_board = np.vstack([ + np.hstack([ + panels[0], + panels[1], + ]), + np.hstack([ + panels[2], + data, + ]), + ]) + + cv2.imshow( + window_name, + pre_board, + ) + + k = cv2.waitKey(1) & 0xFF + + if k in ( + ord("q"), + ord("Q"), + 27, + ): + raise KeyboardInterrupt( + "Cancelado no preflight." + ) + + if k in (13, 10): + if preflight["status"] != "good": + print( + "[BLOCK] Preflight ainda não está GOOD." + ) + continue + + locked = sample_ae_controls( + runtime, + args, + ) + + runtime.send_manual( + locked + ) + + runtime.verify_manual( + locked, + settle_frames=args.lock_settle_frames, + ) + + report["preflight"] = ( + preflight + ) + + report["locked_controls"] = { + role: asdict( + locked[role] + ) + for role in ROLES + } + + break + + time.sleep(0.002) + + # ------------------------------------------------ + # Capture loop + # ------------------------------------------------ + + selected_role = "re" + last_message = "" + last_message_t = 0.0 + + print("") + print("[CAPTURE]") + print( + "G = captura. Mova o chart pelo MESMO plano entre samples." + ) + print( + f"Meta: >= {qa['recommended_samples']} samples, boa cobertura do FOV." + ) + + while True: + runtime.poll() + + msg = ( + last_message + if ( + last_message + and time.time() + - last_message_t + < 4.0 + ) + else "" + ) + + board_ui = build_ui_board( + runtime, + specs, + samples, + selected_role, + last_detection, + profile_eval, + args, + qa, + message=msg, + ) + + cv2.imshow( + window_name, + board_ui, + ) + + k = cv2.waitKey(1) & 0xFF + + if k in ( + ord("q"), + ord("Q"), + 27, + ): + raise KeyboardInterrupt( + "Sessão cancelada." + ) + + elif k == ord("2"): + selected_role = "re" + last_message = ( + "Overlay: RGB + RE" + ) + last_message_t = time.time() + + elif k == ord("3"): + selected_role = "nir" + last_message = ( + "Overlay: RGB + NIR" + ) + last_message_t = time.time() + + elif k == 9: + selected_role = ( + "nir" + if selected_role == "re" + else "re" + ) + last_message = ( + f"Overlay: RGB + {selected_role.upper()}" + ) + last_message_t = time.time() + + elif k in ( + ord("v"), + ord("V"), + ): + samples.clear() + report["samples"] = [] + last_detection = None + profile_eval = None + + last_message = ( + "Todas as amostras foram limpas." + ) + last_message_t = time.time() + + save_json_atomic( + candidate_report_path, + report, + ) + + elif k in ( + ord("s"), + ord("S"), + ): + snap_path = ( + snapshot_dir + / ( + "board_" + + session_stamp() + + ".png" + ) + ) + + cv2.imwrite( + str(snap_path), + board_ui, + ) + + last_message = ( + f"Snapshot salvo: {snap_path.name}" + ) + last_message_t = time.time() + + elif k in ( + ord("g"), + ord("G"), + ): + try: + triplet = ( + runtime.fresh_triplet( + timeout=3.0 + ) + ) + + detection = ( + detect_triplet( + triplet, + board, + dictionary, + detector_params, + qa, + allow_invert=( + not args.no_auto_invert + ), + ) + ) + + last_detection = ( + detection + ) + + if not detection["valid"]: + last_message = ( + "REJEITADO: " + + " | ".join( + detection[ + "reasons" + ] + ) + ) + last_message_t = ( + time.time() + ) + continue + + signature = PoseSignature( + **detection[ + "pose_signature" + ] + ) + + prior = [ + PoseSignature( + **x[ + "pose_signature" + ] + ) + for x in samples + ] + + duplicate, duplicate_info = ( + is_duplicate_pose( + signature, + prior, + qa, + ) + ) + + if duplicate: + last_message = ( + "REJEITADO: pose muito parecida; " + "mova o ChArUco para outra região." + ) + last_message_t = ( + time.time() + ) + continue + + sample_id = ( + len(samples) + 1 + ) + + sample = { + "sample_id": sample_id, + "captured_at": now_str(), + "sync_dt_ms": detection[ + "sync_dt_ms" + ], + "pose_signature": detection[ + "pose_signature" + ], + "frame_controls": triplet[ + "controls" + ], + "detections": { + role: { + "markers": detection[ + "detections" + ][role][ + "markers" + ], + "corners": detection[ + "detections" + ][role][ + "corners" + ], + "equalize": detection[ + "detections" + ][role][ + "equalize" + ], + "invert": detection[ + "detections" + ][role][ + "invert" + ], + } + for role in ROLES + }, + "pairs": detection[ + "pairs" + ], + } + + samples.append( + sample + ) + + preview_path = ( + sample_dir + / f"sample_{sample_id:03d}.png" + ) + + save_sample_preview( + preview_path, + triplet["frames"], + detection, + ) + + sample[ + "preview_png" + ] = str(preview_path) + + report["samples"] = ( + samples + ) + + save_json_atomic( + candidate_report_path, + report, + ) + + last_message = ( + f"ACCEPTED sample #{sample_id:03d} | " + f"RE={len(sample['pairs']['re']['charuco_ids'])} " + f"NIR={len(sample['pairs']['nir']['charuco_ids'])} " + f"sync={sample['sync_dt_ms']}" + ) + last_message_t = ( + time.time() + ) + + print( + "[SAMPLE] " + + last_message + ) + + except Exception as exc: + last_message = ( + f"Captura rejeitada: {exc}" + ) + last_message_t = ( + time.time() + ) + + elif k in ( + ord("a"), + ord("A"), + ): + if ( + len(samples) + < qa["min_samples"] + ): + last_message = ( + f"Precisa >= {qa['min_samples']} samples. " + f"Atual={len(samples)}" + ) + last_message_t = ( + time.time() + ) + continue + + profile_eval = ( + evaluate_profile( + samples, + specs, + qa, + ) + ) + + report["evaluation"] = ( + profile_eval + ) + + save_json_atomic( + candidate_report_path, + report, + ) + + if ( + profile_eval["status"] + == "bad" + ): + last_message = ( + "PROFILE BAD: " + + " | ".join( + profile_eval[ + "reasons" + ][:4] + ) + ) + last_message_t = ( + time.time() + ) + + print( + "[EVAL BAD] " + + last_message + ) + + continue + + if ( + profile_eval["status"] + == "warning" + and not args.promote_warning + ): + last_message = ( + "PROFILE WARNING. " + "Não promove sem --promote-warning." + ) + last_message_t = ( + time.time() + ) + + print( + "[EVAL WARNING] " + + " | ".join( + profile_eval[ + "reasons" + ] + ) + ) + + continue + + # ---------------------------------------- + # PASS / profile payload + # ---------------------------------------- + + re_model = ( + profile_eval[ + "models" + ]["re"] + ) + + nir_model = ( + profile_eval[ + "models" + ]["nir"] + ) + + homography_stats = { + "re_total_points": re_model[ + "points" + ], + "re_inliers": re_model[ + "inliers" + ], + "re_inlier_pct": ( + re_model[ + "inlier_ratio" + ] + * 100.0 + ), + "nir_total_points": nir_model[ + "points" + ], + "nir_inliers": nir_model[ + "inliers" + ], + "nir_inlier_pct": ( + nir_model[ + "inlier_ratio" + ] + * 100.0 + ), + "re_frames_used": len( + re_model[ + "retained_sample_ids" + ] + ), + "nir_frames_used": len( + nir_model[ + "retained_sample_ids" + ] + ), + "common_frames_used": ( + profile_eval[ + "common_frames_used" + ] + ), + "re_fit_median_error_px": ( + re_model[ + "fit_error" + ]["median_px"] + ), + "nir_fit_median_error_px": ( + nir_model[ + "fit_error" + ]["median_px"] + ), + "re_cv_median_error_px": ( + re_model[ + "cross_validation" + ]["overall"][ + "median_px" + ] + ), + "nir_cv_median_error_px": ( + nir_model[ + "cross_validation" + ]["overall"][ + "median_px" + ] + ), + "re_overlap_pct": ( + re_model[ + "overlap_fraction" + ] + * 100.0 + ), + "nir_overlap_pct": ( + nir_model[ + "overlap_fraction" + ] + * 100.0 + ), + "overlap_common_pct": ( + min( + re_model[ + "overlap_fraction" + ], + nir_model[ + "overlap_fraction" + ], + ) + * 100.0 + ), + } + + profile_payload = { + "profile_name": args.profile_name, + "created_at": now_str(), + "description": args.profile_description, + "depth_cm": float( + args.depth_cm + ), + "same_physical_plane_required": True, + "coordinate_space": COORDINATE_SPACE, + "coordinate_spaces": { + role: { + "size": [ + specs[role].width, + specs[role].height, + ], + "sensor": specs[role].sensor_name, + "stream_source": specs[role].source, + "external_undistort_applied": False, + } + for role in ROLES + }, + "homography_source": ( + "charuco_native_multi_sample_group_cv" + ), + "homography_stats": homography_stats, + "homographies": { + "re_to_rgb": re_model[ + "homography" + ], + "nir_to_rgb": nir_model[ + "homography" + ], + }, + "quality": profile_eval, + "charuco": report[ + "charuco" + ], + "locked_controls": report[ + "locked_controls" + ], + "source_session_id": sid, + } + + report[ + "profile_payload" + ] = profile_payload + + report[ + "module_params_fragment" + ] = ( + profile_to_module_fragment( + args.profile_name, + profile_payload, + args.baseline_mm, + ) + ) + + report["status"] = ( + "pass" + if profile_eval[ + "status" + ] == "good" + else "warning_promoted" + ) + + report["finished_at"] = ( + now_str() + ) + + report["promoted"] = True + + save_json_atomic( + candidate_report_path, + report, + ) + + active = ( + create_or_update_active( + Path( + args.active_json + ), + report, + profile_payload, + args, + specs, + ) + ) + + report[ + "active_json_sha256" + ] = sha256_file( + args.active_json + ) + + save_json_atomic( + candidate_report_path, + report, + ) + + marker = ( + candidate_dir + / ( + "PASS.txt" + if profile_eval[ + "status" + ] == "good" + else "PASS_WARNING.txt" + ) + ) + + marker.write_text( + ( + f"status={report['status']}\n" + f"profile={args.profile_name}\n" + f"depth_cm={args.depth_cm}\n" + f"active_json={args.active_json}\n" + ), + encoding="utf-8", + ) + + print("") + print("=" * 88) + print( + "[PASS] HOMOGRAPHY PROFILE PROMOVIDO" + ) + print( + f"[PROFILE] {args.profile_name} @ {args.depth_cm} cm" + ) + print( + f"[ACTIVE] {args.active_json}" + ) + print( + f"[CANDIDATE] {candidate_dir}" + ) + print("=" * 88) + + final = np.zeros( + (720, 1280, 3), + dtype=np.uint8, + ) + + final_lines = [ + "HOMOGRAPHY CALIBRATION - PASS", + "", + f"profile={args.profile_name}", + f"depth={args.depth_cm} cm", + "", + f"RE fit med={re_model['fit_error']['median_px']:.2f}px", + f"RE CV med={re_model['cross_validation']['overall']['median_px']:.2f}px", + f"RE overlap={re_model['overlap_fraction']*100:.1f}%", + "", + f"NIR fit med={nir_model['fit_error']['median_px']:.2f}px", + f"NIR CV med={nir_model['cross_validation']['overall']['median_px']:.2f}px", + f"NIR overlap={nir_model['overlap_fraction']*100:.1f}%", + "", + f"Active: {args.active_json}", + "", + "Qualquer tecla fecha.", + ] + + overlay_hud( + final, + final_lines, + x=46, + y=62, + font_scale=0.68, + line_step=31, + ) + + cv2.imshow( + window_name, + final, + ) + + cv2.waitKey(0) + + break + + time.sleep(0.001) + + except KeyboardInterrupt as exc: + if report is None: + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + "coordinate_space": COORDINATE_SPACE, + } + + report["status"] = "cancelled" + report["finished_at"] = now_str() + report["promoted"] = False + report["error"] = str(exc) + report["samples"] = samples + + try: + save_json_atomic( + candidate_report_path, + report, + ) + + ( + candidate_dir + / "CANCELLED.txt" + ).write_text( + f"reason={exc}\n", + encoding="utf-8", + ) + + except Exception: + pass + + print(f"[CANCELADO] {exc}") + print( + "[SAFE] Homography ativa anterior preservada." + ) + + except Exception as exc: + if report is None: + report = { + "schema": SCHEMA, + "session_id": sid, + "created_at": now_str(), + "coordinate_space": COORDINATE_SPACE, + } + + report["status"] = "error" + report["finished_at"] = now_str() + report["promoted"] = False + report["error"] = ( + f"{type(exc).__name__}: {exc}" + ) + report["samples"] = samples + + try: + save_json_atomic( + candidate_report_path, + report, + ) + + ( + candidate_dir + / "FAIL.txt" + ).write_text( + ( + f"error={type(exc).__name__}: {exc}\n" + ), + encoding="utf-8", + ) + + except Exception: + pass + + print("") + print("=" * 88) + print( + "[ERRO] HOMOGRAPHY CALIBRATION ABORTADA" + ) + print( + f"{type(exc).__name__}: {exc}" + ) + print( + "[SAFE] Homography ativa anterior preservada." + ) + print("=" * 88) + + raise + + finally: + cv2.destroyAllWindows() + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/_8_build_module_params.py b/Python/OAK/datasets/oak-fcc-3/utils/_8_build_module_params.py new file mode 100644 index 000000000..036f7d53f --- /dev/null +++ b/Python/OAK/datasets/oak-fcc-3/utils/_8_build_module_params.py @@ -0,0 +1,2869 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +module_params_assembler_production.py +===================================== + +Assembler FINAL de produção do module_params do módulo multiespectral. + +Este script NÃO calibra nada. +Ele somente: + 1. carrega os artefatos HOMOLOGADOS das etapas anteriores; + 2. valida schema/status/promoção; + 3. cruza hardware/MX ID; + 4. cruza domínio geométrico Intrinsics <-> Homography; + 5. cruza Startup Profile <-> Radiometry; + 6. valida o NPZ do Flat-Field; + 7. preserva apenas políticas runtime explicitamente permitidas; + 8. monta o module_params final de forma fail-closed; + 9. faz validação estrutural final; + 10. salva atomicamente + backup + relatório de assembly. + +Linha oficial de entrada +------------------------ + Focus: + calibration/focus_qc_active.json + + Flat-Field: + calibration/flatfield_maps_v1.json + calibration/flatfield_maps_v1.npz + + Radiometry: + calibration/radiometry_calibration_v5.json + + Camera Startup: + calibration/camera_startup_profile_v3.json + + Intrinsics: + calibration/intrinsics_calibration_v1.json + + Homography: + calibration/homography_calibration_v4.json + + Runtime base opcional: + calibration/module_params.json + +O runtime base NÃO é fonte de calibração. +Ele só pode fornecer políticas não calibradas, por exemplo: + - onnx_model_path + - frame_type / capture mode / raw policy + - rgb_processing + - alguns knobs de fusion runtime: + use_remap_cache + use_remap_for_rgb + use_remap_for_spec + crop_valid_common + resize_after_crop + target_size + +Todo bloco calibrado é substituído pelos artefatos homologados. + +Política final +-------------- + radiometric_config.enabled = false + patch_normalization.enabled = false + rgb_calibration.enabled = false / unity + +Essas políticas foram deliberadamente separadas da calibração de fábrica. + +Compatibilidade +--------------- +O schema final continua: + multispec_module_params_v3 + +para evitar quebrar consumers existentes. + +Entretanto o arquivo passa a conter também: + sensor_size_by_role + camera_hardware + intrinsics_config + calibration_provenance + assembly_metadata + +Root sensor_width/sensor_height: + representam a câmera RGB de referência. + +Para AR0234: + sensor_width = 1920 + sensor_height = 1200 + +RE/NIR permanecem explicitamente registrados em: + sensor_size_by_role + +Bayer +----- +O bayer_pattern root é obtido do Flat-Field de produção, que já resolveu +o Bayer da câmera RGB durante a calibração. + +Fail-closed +----------- +O assembler ABORTA se: + - algum artefato obrigatório não existe; + - schema não é o esperado; + - artefato não foi promovido; + - Focus não é homologação normal; + - hardware signatures divergem; + - MX IDs divergem; + - Startup veio de outra Radiometry; + - reference_controls divergem; + - Homography e Intrinsics nasceram em espaços geométricos incompatíveis; + - runtime_undistort está ligado com Homography no espaço errado; + - Flat-Field NPZ não existe; + - Flat-Field NPZ não contém os gains obrigatórios; + - perfil de Homography selecionado não existe; + - matriz H ou K/D é estruturalmente inválida. + +Uso normal +---------- + python module_params_assembler_production.py + +Selecionando perfil de homografia: + python module_params_assembler_production.py ^ + --homography-profile media + +Somente auditoria: + python module_params_assembler_production.py --check-only + +Saídas +------ + calibration/module_params.json + calibration/module_params_assembly_report.json +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import zipfile +from copy import deepcopy +from datetime import datetime +from pathlib import Path +from typing import Any, Optional + + +# ============================================================ +# Contratos +# ============================================================ + +FINAL_SCHEMA = "multispec_module_params_v3" +ASSEMBLER_SCHEMA = "multispec_module_params_assembly_v1" + +EXPECTED_SCHEMAS = { + "focus": "multispec_focus_qc_v3", + "flatfield": "multispec_flatfield_production_v2", + "radiometry": "multispec_radiometric_calibration_v5", + "startup": "multispec_camera_startup_profile_v3", + "intrinsics": "multispec_intrinsics_calibration_v1", + "homography": "multispec_homography_calibration_v4", +} + +ROLES = ("rgb", "re", "nir") +CHANNELS = ("R", "G", "B", "RE", "NIR") + +EXPECTED_TOPOLOGY = { + "rgb": { + "socket": "CAM_A", + "allowed_sensors": ("OV9782", "AR0234"), + }, + "re": { + "socket": "CAM_B", + "allowed_sensors": ("OV9282",), + }, + "nir": { + "socket": "CAM_C", + "allowed_sensors": ("OV9282",), + }, +} + +EXPECTED_NATIVE_SIZES = { + "OV9782": [1280, 800], + "AR0234": [1920, 1200], + "OV9282": [1280, 800], +} + +CALIBRATION_SPACE_NATIVE = "native_stream_no_external_undistort" + + +# ============================================================ +# Helpers +# ============================================================ + +def now_str() -> str: + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def stamp() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f") + + +def ensure_dir(path: str | Path): + Path(path).mkdir(parents=True, exist_ok=True) + + +def load_json(path: str | Path, *, required: bool = True) -> dict: + p = Path(path) + + if not p.is_file(): + if required: + raise FileNotFoundError( + f"Arquivo obrigatório não encontrado: {p}" + ) + + return {} + + with p.open("r", encoding="utf-8") as f: + data = json.load(f) + + if not isinstance(data, dict): + raise RuntimeError( + f"JSON root deve ser object/dict: {p}" + ) + + return data + + +def save_json_atomic(path: str | Path, data: dict): + p = Path(path) + ensure_dir(p.parent) + + tmp = p.with_suffix( + p.suffix + ".tmp" + ) + + with tmp.open( + "w", + encoding="utf-8", + ) as f: + json.dump( + data, + f, + ensure_ascii=False, + indent=2, + ) + f.write("\n") + f.flush() + os.fsync(f.fileno()) + + os.replace( + tmp, + p, + ) + + +def sha256_file(path: str | Path) -> str: + h = hashlib.sha256() + + with open( + path, + "rb", + ) as f: + while True: + chunk = f.read( + 1024 * 1024 + ) + + if not chunk: + break + + h.update( + chunk + ) + + return h.hexdigest() + + +def norm_path(path: str | Path) -> str: + return str( + path + ).replace( + "\\", + "/", + ) + + +def deep_merge( + base: dict, + update: dict, +) -> dict: + """ + Merge simples para POLÍTICAS runtime. + Calibrações não devem depender disso. + """ + out = deepcopy( + base + if isinstance(base, dict) + else {} + ) + + if not isinstance( + update, + dict, + ): + return out + + for key, value in update.items(): + if ( + isinstance(value, dict) + and isinstance( + out.get(key), + dict, + ) + ): + out[key] = deep_merge( + out[key], + value, + ) + else: + out[key] = deepcopy( + value + ) + + return out + + +def require_schema( + name: str, + data: dict, +): + expected = EXPECTED_SCHEMAS[ + name + ] + + actual = data.get( + "schema" + ) + + if actual != expected: + raise RuntimeError( + f"{name}: schema={actual!r}, " + f"esperado={expected!r}" + ) + + +def require_promoted( + name: str, + data: dict, + *, + allowed_statuses: tuple[str, ...], +): + if not bool( + data.get( + "promoted", + False, + ) + ): + raise RuntimeError( + f"{name}: artefato não promovido." + ) + + status = str( + data.get( + "status", + "" + ) + ).lower() + + if ( + status + not in allowed_statuses + ): + raise RuntimeError( + f"{name}: status={status!r}, " + f"aceitos={allowed_statuses}" + ) + + +def is_matrix_3x3(value) -> bool: + if not ( + isinstance( + value, + list, + ) + and len(value) == 3 + ): + return False + + return all( + isinstance( + row, + list, + ) + and len(row) == 3 + for row in value + ) + + +def is_positive_int(v) -> bool: + try: + return int(v) > 0 + except Exception: + return False + + +def artifact_meta( + path: str | Path, + data: dict, +) -> dict: + p = Path(path) + + return { + "path": norm_path(p), + "sha256": sha256_file( + p + ), + "schema": data.get( + "schema" + ), + "status": data.get( + "status" + ), + "promoted": data.get( + "promoted" + ), + "session_id": data.get( + "session_id" + ), + "created_at": data.get( + "created_at" + ), + "finished_at": data.get( + "finished_at" + ), + } + + +# ============================================================ +# Hardware signature normalization +# ============================================================ + +def normalize_role_item( + item: dict, +) -> dict: + if not isinstance( + item, + dict, + ): + raise RuntimeError( + f"Hardware role inválido: {item!r}" + ) + + socket = ( + item.get("socket") + or item.get( + "socket_name" + ) + ) + + sensor = ( + item.get("sensor") + or item.get( + "sensor_name" + ) + ) + + size = item.get( + "size" + ) + + if size is None: + width = item.get( + "width" + ) + + height = item.get( + "height" + ) + + if ( + width is not None + and height is not None + ): + size = [ + int(width), + int(height), + ] + + if ( + socket is None + or sensor is None + or size is None + or len(size) != 2 + ): + raise RuntimeError( + f"Hardware role incompleto: {item}" + ) + + return { + "socket": str( + socket + ), + "sensor": str( + sensor + ).upper(), + "size": [ + int( + size[0] + ), + int( + size[1] + ), + ], + } + + +def normalize_signature( + signature: dict, +) -> dict: + if not isinstance( + signature, + dict, + ): + raise RuntimeError( + "hardware_signature inválida." + ) + + out = {} + + for role in ROLES: + item = signature.get( + role + ) + + if item is None: + raise RuntimeError( + f"hardware_signature sem role {role}" + ) + + out[ + role + ] = normalize_role_item( + item + ) + + return out + + +def signature_from_resolved_setup( + resolved_setup: dict, +) -> dict: + return normalize_signature( + resolved_setup + ) + + +def signature_from_focus( + data: dict, +) -> dict: + return signature_from_resolved_setup( + data.get( + "resolved_setup", + {}, + ) + ) + + +def signature_from_flatfield( + data: dict, +) -> dict: + return signature_from_resolved_setup( + data.get( + "resolved_setup", + {}, + ) + ) + + +def signature_from_direct( + data: dict, +) -> dict: + return normalize_signature( + data.get( + "hardware_signature", + {}, + ) + ) + + +def validate_product_signature( + signature: dict, +): + for role in ROLES: + item = signature[ + role + ] + + contract = EXPECTED_TOPOLOGY[ + role + ] + + if ( + item["socket"] + != contract["socket"] + ): + raise RuntimeError( + f"{role}: socket={item['socket']}, " + f"esperado={contract['socket']}" + ) + + if ( + item["sensor"] + not in contract[ + "allowed_sensors" + ] + ): + raise RuntimeError( + f"{role}: sensor={item['sensor']}, " + f"permitidos={contract['allowed_sensors']}" + ) + + expected_size = ( + EXPECTED_NATIVE_SIZES[ + item["sensor"] + ] + ) + + if ( + item["size"] + != expected_size + ): + raise RuntimeError( + f"{role}/{item['sensor']}: " + f"size={item['size']}, " + f"esperado={expected_size}" + ) + + +def compare_signatures( + named_signatures: dict[str, dict], +) -> dict: + names = list( + named_signatures.keys() + ) + + if not names: + raise RuntimeError( + "Nenhuma assinatura de hardware." + ) + + reference_name = names[ + 0 + ] + + reference = named_signatures[ + reference_name + ] + + mismatches = [] + + for name in names[ + 1: + ]: + if ( + named_signatures[ + name + ] + != reference + ): + mismatches.append({ + "artifact": name, + "signature": named_signatures[ + name + ], + }) + + if mismatches: + raise RuntimeError( + "Artefatos pertencem a hardwares diferentes.\n" + f"Referência {reference_name}: {reference}\n" + f"Divergências: {mismatches}" + ) + + validate_product_signature( + reference + ) + + return reference + + +# ============================================================ +# MX ID / module ID cross-check +# ============================================================ + +def extract_mx_ids( + artifacts: dict[str, dict], +) -> dict: + out = {} + + for name, data in artifacts.items(): + candidates = [ + data.get( + "device_mx_id" + ), + ( + data.get( + "device", + {} + ) + or {} + ).get( + "mx_id" + ), + ] + + value = next( + ( + str(x) + for x in candidates + if x + ), + None, + ) + + if value: + out[ + name + ] = value + + return out + + +def validate_single_value( + values: dict[str, str], + label: str, +) -> Optional[str]: + unique = sorted( + set( + values.values() + ) + ) + + if len( + unique + ) > 1: + raise RuntimeError( + f"{label} divergente entre artefatos: {values}" + ) + + return ( + unique[0] + if unique + else None + ) + + +def extract_module_ids( + artifacts: dict[str, dict], +) -> dict: + out = {} + + for name, data in artifacts.items(): + trace = ( + data.get( + "traceability", + {} + ) + or {} + ) + + value = trace.get( + "module_id" + ) + + if value: + out[ + name + ] = str( + value + ) + + return out + + +# ============================================================ +# Artefact validators +# ============================================================ + +def validate_focus( + data: dict, +): + require_schema( + "focus", + data, + ) + + require_promoted( + "focus", + data, + allowed_statuses=( + "pass", + ), + ) + + if not bool( + data.get( + "normal_production_homologation", + False, + ) + ): + raise RuntimeError( + "Focus não é homologação normal de produção." + ) + + if data.get( + "runtime_effect" + ) != "none": + raise RuntimeError( + "Focus inesperadamente declara efeito de runtime." + ) + + +def validate_flatfield( + data: dict, + flatfield_json_path: str | Path, + flatfield_npz_override: Optional[str], +) -> tuple[dict, Path]: + require_schema( + "flatfield", + data, + ) + + require_promoted( + "flatfield", + data, + allowed_statuses=( + "good", + "warning", + ), + ) + + patch = data.get( + "suggested_module_params_patch" + ) + + if not isinstance( + patch, + dict, + ): + raise RuntimeError( + "Flat-Field sem suggested_module_params_patch." + ) + + cfg = patch.get( + "flatfield_config" + ) + + if not isinstance( + cfg, + dict, + ): + raise RuntimeError( + "Flat-Field sem suggested_module_params_patch.flatfield_config." + ) + + if not bool( + cfg.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "Flat-Field homologado, mas patch está disabled." + ) + + outputs = ( + data.get( + "outputs", + {} + ) + or {} + ) + + npz_path = ( + flatfield_npz_override + or outputs.get( + "active_npz" + ) + or cfg.get( + "npz_file" + ) + ) + + if not npz_path: + raise RuntimeError( + "Flat-Field não informa active NPZ." + ) + + npz = Path( + npz_path + ) + + if not npz.is_file(): + # Se caminho foi gravado relativo ao projeto, tenta relativo ao JSON. + candidate = ( + Path( + flatfield_json_path + ) + .parent + / npz_path + ) + + if candidate.is_file(): + npz = candidate + else: + raise FileNotFoundError( + f"Flat-Field NPZ não encontrado: {npz_path}" + ) + + required_keys = { + f"gain_{ch}" + for ch in CHANNELS + } + + with zipfile.ZipFile( + npz, + "r", + ) as zf: + names = set( + zf.namelist() + ) + + # NPZ guarda .npy + expected_members = { + f"{key}.npy" + for key in required_keys + } + + missing = sorted( + expected_members - names + ) + + if missing: + raise RuntimeError( + f"Flat-Field NPZ sem gains obrigatórios: {missing}" + ) + + runtime_cfg = deepcopy( + cfg + ) + + # Canonicaliza channel_maps para o contrato real do RawProcessorCore. + # O calibrador Flat-Field pode emitir: + # "R": "gain_R" + # enquanto o runtime atual consome: + # "R": { + # "gain_key": "gain_R", + # "dark_median_key": "dark_median_R", + # } + raw_channel_maps = ( + runtime_cfg.get( + "channel_maps", + {}, + ) + or {} + ) + + normalized_channel_maps = {} + + for ch in CHANNELS: + item = raw_channel_maps.get( + ch + ) + + if isinstance( + item, + str, + ): + normalized_channel_maps[ + ch + ] = { + "gain_key": item, + "dark_median_key": ( + f"dark_median_{ch}" + ), + } + + elif isinstance( + item, + dict, + ): + entry = deepcopy( + item + ) + + entry.setdefault( + "gain_key", + f"gain_{ch}", + ) + + entry.setdefault( + "dark_median_key", + f"dark_median_{ch}", + ) + + normalized_channel_maps[ + ch + ] = entry + + else: + normalized_channel_maps[ + ch + ] = { + "gain_key": ( + f"gain_{ch}" + ), + "dark_median_key": ( + f"dark_median_{ch}" + ), + } + + runtime_cfg[ + "channel_maps" + ] = normalized_channel_maps + + runtime_cfg[ + "npz_file" + ] = norm_path( + npz + ) + + runtime_cfg[ + "json_file" + ] = norm_path( + flatfield_json_path + ) + + runtime_cfg[ + "schema" + ] = data.get( + "schema" + ) + + runtime_cfg[ + "created_at" + ] = data.get( + "created_at" + ) + + # Política final já decidida: não ativar dark model simples. + runtime_cfg[ + "subtract_dark" + ] = False + + return runtime_cfg, npz + + +def validate_radiometry( + data: dict, +) -> dict: + require_schema( + "radiometry", + data, + ) + + require_promoted( + "radiometry", + data, + allowed_statuses=( + "good", + "warning", + ), + ) + + frag = ( + data.get( + "module_params_fragment", + {} + ) + or {} + ) + + cfg = frag.get( + "radiometric_normalization" + ) + + if not isinstance( + cfg, + dict, + ): + raise RuntimeError( + "Radiometry sem module_params_fragment.radiometric_normalization." + ) + + if not bool( + cfg.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "Radiometric normalization homologada, mas disabled." + ) + + if ( + cfg.get( + "method" + ) + != "oak_ae_frame_controls_v1" + ): + raise RuntimeError( + f"Método radiométrico inesperado: {cfg.get('method')}" + ) + + if ( + cfg.get( + "factor_model" + ) + != "exposure_time_us_x_iso" + ): + raise RuntimeError( + f"factor_model inesperado: {cfg.get('factor_model')}" + ) + + refs = ( + cfg.get( + "reference_controls", + {} + ) + or {} + ) + + for role in ROLES: + ref = refs.get( + role + ) + + if not isinstance( + ref, + dict, + ): + raise RuntimeError( + f"Radiometry sem reference_controls.{role}" + ) + + if not is_positive_int( + ref.get( + "exposure_time_us" + ) + ): + raise RuntimeError( + f"Radiometry exposure inválida em {role}" + ) + + if not is_positive_int( + ref.get( + "sensitivity_iso" + ) + ): + raise RuntimeError( + f"Radiometry ISO inválido em {role}" + ) + + return deepcopy( + cfg + ) + + +def validate_startup( + data: dict, + radiometry: dict, +) -> tuple[dict, dict]: + require_schema( + "startup", + data, + ) + + require_promoted( + "startup", + data, + allowed_statuses=( + "good", + ), + ) + + frag = ( + data.get( + "module_params_fragment", + {} + ) + or {} + ) + + camera_settings = frag.get( + "camera_settings" + ) + + rgb_calibration = frag.get( + "rgb_calibration" + ) + + if not isinstance( + camera_settings, + dict, + ): + raise RuntimeError( + "Startup sem camera_settings." + ) + + if not isinstance( + rgb_calibration, + dict, + ): + raise RuntimeError( + "Startup sem rgb_calibration." + ) + + if bool( + rgb_calibration.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "Startup tentou habilitar rgb_calibration." + ) + + gains = ( + rgb_calibration.get( + "gains", + {} + ) + or {} + ) + + for ch in ( + "R", + "G", + "B", + ): + if float( + gains.get( + ch, + 0.0, + ) + ) != 1.0: + raise RuntimeError( + f"Startup rgb gain {ch} não é unity." + ) + + for role in ROLES: + cfg = camera_settings.get( + role + ) + + if not isinstance( + cfg, + dict, + ): + raise RuntimeError( + f"Startup sem camera_settings.{role}" + ) + + if not bool( + cfg.get( + "ae_enable", + False, + ) + ): + raise RuntimeError( + f"Startup produto exige AE=true em {role}." + ) + + source_rad = ( + data.get( + "source_radiometry", + {} + ) + or {} + ) + + rad_session = radiometry.get( + "session_id" + ) + + source_session = source_rad.get( + "session_id" + ) + + if ( + rad_session + and source_session + and rad_session + != source_session + ): + raise RuntimeError( + "Startup Profile foi gerado a partir de outra Radiometry: " + f"startup={source_session}, atual={rad_session}" + ) + + rad_refs = ( + ( + radiometry.get( + "module_params_fragment", + {} + ) + or {} + ) + .get( + "radiometric_normalization", + {} + ) + .get( + "reference_controls", + {} + ) + ) + + startup_refs = source_rad.get( + "reference_controls", + {} + ) + + if ( + startup_refs + and startup_refs != rad_refs + ): + raise RuntimeError( + "Startup reference_controls divergem da Radiometry ativa." + ) + + return ( + deepcopy( + camera_settings + ), + deepcopy( + rgb_calibration + ), + ) + + +def validate_intrinsics( + data: dict, +) -> dict: + require_schema( + "intrinsics", + data, + ) + + require_promoted( + "intrinsics", + data, + allowed_statuses=( + "pass", + "warning_promoted", + ), + ) + + frag = ( + data.get( + "module_params_fragment", + {} + ) + or {} + ) + + cfg = frag.get( + "intrinsics_config" + ) + + if not isinstance( + cfg, + dict, + ): + raise RuntimeError( + "Intrinsics sem module_params_fragment.intrinsics_config." + ) + + if not bool( + cfg.get( + "enabled", + False, + ) + ): + raise RuntimeError( + "Intrinsics homologado, mas intrinsics_config disabled." + ) + + cameras = ( + cfg.get( + "cameras", + {} + ) + or {} + ) + + for role in ROLES: + cam = cameras.get( + role + ) + + if not isinstance( + cam, + dict, + ): + raise RuntimeError( + f"Intrinsics sem câmera {role}." + ) + + if not is_matrix_3x3( + cam.get( + "camera_matrix" + ) + ): + raise RuntimeError( + f"Intrinsics K inválida em {role}." + ) + + D = cam.get( + "dist_coeffs" + ) + + if not ( + isinstance( + D, + list, + ) + and len(D) >= 4 + ): + raise RuntimeError( + f"Intrinsics dist_coeffs inválida em {role}." + ) + + size = cam.get( + "image_size" + ) + + if not ( + isinstance( + size, + list, + ) + and len(size) == 2 + and is_positive_int( + size[0] + ) + and is_positive_int( + size[1] + ) + ): + raise RuntimeError( + f"Intrinsics image_size inválida em {role}." + ) + + return deepcopy( + cfg + ) + + +def validate_homography( + data: dict, + profile_override: Optional[str], +) -> dict: + require_schema( + "homography", + data, + ) + + # O active v4 é um container de perfis e não carrega promoted=true. + profiles = ( + data.get( + "profiles", + {} + ) + or {} + ) + + if not profiles: + raise RuntimeError( + "Homography ativa não possui perfis." + ) + + profile = ( + profile_override + or data.get( + "recommended_profile" + ) + ) + + if not profile: + raise RuntimeError( + "Homography sem recommended_profile. " + "Use --homography-profile." + ) + + if profile not in profiles: + raise RuntimeError( + f"Perfil de homografia {profile!r} não existe. " + f"Disponíveis={sorted(profiles)}" + ) + + # Garante que todos os perfis guardados são produtos de um acceptance válido. + for name, payload in profiles.items(): + quality = ( + payload.get( + "quality", + {} + ) + or {} + ) + + status = str( + quality.get( + "status", + "" + ) + ).lower() + + if status not in ( + "good", + "warning", + ): + raise RuntimeError( + f"Homography profile {name}: quality.status={status!r}" + ) + + homographies = ( + payload.get( + "homographies", + {} + ) + or {} + ) + + for key in ( + "re_to_rgb", + "nir_to_rgb", + ): + if not is_matrix_3x3( + homographies.get( + key + ) + ): + raise RuntimeError( + f"Homography profile {name}: matriz {key} inválida." + ) + + fragment = ( + data.get( + "module_params_fragment", + {} + ) + or {} + ) + + cfg = fragment.get( + "fusion_config" + ) + + if not isinstance( + cfg, + dict, + ): + raise RuntimeError( + "Homography active sem module_params_fragment.fusion_config." + ) + + out = deepcopy( + cfg + ) + + out[ + "homography_profile" + ] = profile + + out[ + "homography_profile_by_role" + ] = { + "re": profile, + "nir": profile, + } + + selected = ( + out.get( + "homography_profiles", + {} + ) + or {} + ).get( + profile + ) + + if not isinstance( + selected, + dict, + ): + raise RuntimeError( + "Fragmento fusion_config não contém o perfil selecionado." + ) + + out[ + "homography_calibration_size" + ] = deepcopy( + selected.get( + "reference_size" + ) + or selected.get( + "homography_calibration_size" + ) + ) + + return out + + +# ============================================================ +# Cross-artifact geometry checks +# ============================================================ + +def validate_geometry_contract( + intrinsics_cfg: dict, + homography_data: dict, + fusion_cfg: dict, +): + intr_space = intrinsics_cfg.get( + "calibration_space" + ) + + hom_space = homography_data.get( + "coordinate_space" + ) + + runtime_und = ( + intrinsics_cfg.get( + "runtime_undistort", + {} + ) + or {} + ) + + und_enabled = bool( + runtime_und.get( + "enabled", + False, + ) + ) + + if not und_enabled: + if ( + intr_space != CALIBRATION_SPACE_NATIVE + or hom_space != CALIBRATION_SPACE_NATIVE + ): + raise RuntimeError( + "Domínio geométrico inconsistente: " + f"intrinsics={intr_space!r}, homography={hom_space!r}, " + "runtime_undistort=false." + ) + + else: + if ( + hom_space + == CALIBRATION_SPACE_NATIVE + ): + raise RuntimeError( + "runtime_undistort=true, mas Homography foi calibrada " + "no espaço nativo distorcido. Recalibre a Homography." + ) + + selected_profile = fusion_cfg[ + "homography_profile" + ] + + profile = ( + fusion_cfg[ + "homography_profiles" + ][ + selected_profile + ] + ) + + ref_size = ( + profile.get( + "reference_size" + ) + or profile.get( + "homography_calibration_size" + ) + ) + + src_sizes = ( + profile.get( + "source_size_by_role", + {} + ) + or {} + ) + + cams = intrinsics_cfg[ + "cameras" + ] + + if ( + ref_size + != cams[ + "rgb" + ][ + "image_size" + ] + ): + raise RuntimeError( + "Homography RGB reference_size diverge de Intrinsics RGB: " + f"{ref_size} != {cams['rgb']['image_size']}" + ) + + for role in ( + "re", + "nir", + ): + source_size = src_sizes.get( + role + ) + + if ( + source_size is not None + and source_size + != cams[ + role + ][ + "image_size" + ] + ): + raise RuntimeError( + f"Homography source_size {role} diverge de Intrinsics: " + f"{source_size} != {cams[role]['image_size']}" + ) + + +# ============================================================ +# Runtime base, whitelist only +# ============================================================ + +def runtime_defaults() -> dict: + return { + "onnx_model_path": None, + "frame_type": "RAW_BRUTO", + "capture_mode_requested": "AUTO", + "capture_mode_effective": "AUTO", + "raw_policy": "allow_single", + "rgb_processing": { + "mode": "bayer_planes", + }, + "fusion_runtime": { + "use_remap_cache": True, + "use_remap_for_rgb": False, + "use_remap_for_spec": True, + "crop_valid_common": True, + "resize_after_crop": True, + "target_size": None, + }, + } + + +def read_runtime_policy( + base: dict, +) -> dict: + """ + Extrai SOMENTE campos que não são calibração. + """ + defaults = runtime_defaults() + + if not isinstance( + base, + dict, + ): + return defaults + + out = deepcopy( + defaults + ) + + for key in ( + "onnx_model_path", + "frame_type", + "capture_mode_requested", + "capture_mode_effective", + "raw_policy", + ): + if key in base: + out[ + key + ] = deepcopy( + base[ + key + ] + ) + + if isinstance( + base.get( + "rgb_processing" + ), + dict, + ): + out[ + "rgb_processing" + ] = deepcopy( + base[ + "rgb_processing" + ] + ) + + old_fusion = ( + base.get( + "fusion_config", + {} + ) + or {} + ) + + for key in ( + "use_remap_cache", + "use_remap_for_rgb", + "use_remap_for_spec", + "crop_valid_common", + "resize_after_crop", + "target_size", + ): + if key in old_fusion: + out[ + "fusion_runtime" + ][ + key + ] = deepcopy( + old_fusion[ + key + ] + ) + + return out + + +# ============================================================ +# Bayer from Flat-Field +# ============================================================ + +def resolve_bayer_from_flatfield( + flatfield: dict, +) -> str: + setup = ( + flatfield.get( + "resolved_setup", + {} + ) + or {} + ) + + rgb = ( + setup.get( + "rgb", + {} + ) + or {} + ) + + bayer = rgb.get( + "bayer_pattern" + ) + + if not bayer: + raise RuntimeError( + "Flat-Field homologado não registra RGB bayer_pattern. " + "Rode novamente o Flat-Field de produção." + ) + + bayer = str( + bayer + ).upper() + + if bayer not in ( + "RGGB", + "BGGR", + "GRBG", + "GBRG", + ): + raise RuntimeError( + f"Bayer inválido no Flat-Field: {bayer!r}" + ) + + return bayer + + +# ============================================================ +# Final build +# ============================================================ + +def disabled_radiometric_controller() -> dict: + return { + "enabled": False, + "policy": "disabled_product_default", + "reason": ( + "Field radiometric controller is not part of factory calibration. " + "Per-frame exposure variation is handled by " + "radiometric_normalization." + ), + } + + +def disabled_patch_normalization() -> dict: + return { + "enabled": False, + "policy": "disabled_product_default", + "reason": ( + "Reference-patch post-fusion normalization is not active " + "in the homologated product pipeline." + ), + } + + +def build_final_module_params( + runtime_policy: dict, + hardware_signature: dict, + bayer_pattern: str, + camera_settings: dict, + rgb_calibration: dict, + radiometric_normalization: dict, + flatfield_config: dict, + intrinsics_config: dict, + fusion_config: dict, + provenance: dict, + assembly_report_path: str, +) -> dict: + rgb_hw = hardware_signature[ + "rgb" + ] + + fusion = deepcopy( + fusion_config + ) + + for key, value in runtime_policy[ + "fusion_runtime" + ].items(): + fusion[ + key + ] = deepcopy( + value + ) + + result = { + "schema": FINAL_SCHEMA, + "saved_at": now_str(), + + # Runtime/model policy + "onnx_model_path": runtime_policy.get( + "onnx_model_path" + ), + "frame_type": runtime_policy[ + "frame_type" + ], + "capture_mode_requested": runtime_policy[ + "capture_mode_requested" + ], + "capture_mode_effective": runtime_policy[ + "capture_mode_effective" + ], + "raw_policy": runtime_policy[ + "raw_policy" + ], + + # Compatibilidade root: RGB reference camera + "sensor_width": int( + rgb_hw[ + "size" + ][0] + ), + "sensor_height": int( + rgb_hw[ + "size" + ][1] + ), + "bayer_pattern": bayer_pattern, + + # Novo contrato explícito de geometria física + "sensor_size_by_role": { + role: deepcopy( + hardware_signature[ + role + ][ + "size" + ] + ) + for role in ROLES + }, + "camera_hardware": deepcopy( + hardware_signature + ), + + "rgb_processing": deepcopy( + runtime_policy[ + "rgb_processing" + ] + ), + + # Calibrações/políticas homologadas + "camera_settings": deepcopy( + camera_settings + ), + "fusion_config": fusion, + "intrinsics_config": deepcopy( + intrinsics_config + ), + "radiometric_config": ( + disabled_radiometric_controller() + ), + "radiometric_normalization": deepcopy( + radiometric_normalization + ), + "patch_normalization": ( + disabled_patch_normalization() + ), + "rgb_calibration": deepcopy( + rgb_calibration + ), + "flatfield_config": deepcopy( + flatfield_config + ), + + # Auditoria. Consumers antigos podem ignorar. + "calibration_provenance": deepcopy( + provenance + ), + "assembly_metadata": { + "schema": ASSEMBLER_SCHEMA, + "assembled_at": now_str(), + "assembly_report": norm_path( + assembly_report_path + ), + "calibration_line": [ + "focus", + "intrinsics", + "flatfield", + "radiometry", + "camera_startup", + "homography", + "module_params_assembler", + ], + "runtime_undistort_enabled": bool( + ( + intrinsics_config.get( + "runtime_undistort", + {} + ) + or {} + ).get( + "enabled", + False, + ) + ), + }, + } + + # Não escreve null desnecessário para ONNX se base não tinha. + if ( + result[ + "onnx_model_path" + ] + is None + ): + result.pop( + "onnx_model_path", + None, + ) + + return result + + +# ============================================================ +# Final structural validation +# ============================================================ + +def validate_final_module_params( + mp: dict, +): + required_root = ( + "schema", + "sensor_width", + "sensor_height", + "bayer_pattern", + "sensor_size_by_role", + "camera_hardware", + "rgb_processing", + "camera_settings", + "fusion_config", + "intrinsics_config", + "radiometric_config", + "radiometric_normalization", + "patch_normalization", + "rgb_calibration", + "flatfield_config", + ) + + missing = [ + key + for key in required_root + if key not in mp + ] + + if missing: + raise RuntimeError( + f"module_params final sem chaves: {missing}" + ) + + if ( + mp[ + "schema" + ] + != FINAL_SCHEMA + ): + raise RuntimeError( + "Schema final inválido." + ) + + if not is_positive_int( + mp[ + "sensor_width" + ] + ) or not is_positive_int( + mp[ + "sensor_height" + ] + ): + raise RuntimeError( + "sensor_width/height inválidos." + ) + + for role in ROLES: + if role not in mp[ + "camera_settings" + ]: + raise RuntimeError( + f"camera_settings sem {role}" + ) + + if role not in mp[ + "sensor_size_by_role" + ]: + raise RuntimeError( + f"sensor_size_by_role sem {role}" + ) + + fusion = mp[ + "fusion_config" + ] + + if ( + fusion.get( + "alignment_mode" + ) + != "homography" + ): + raise RuntimeError( + "fusion_config.alignment_mode deve ser homography." + ) + + selected = fusion.get( + "homography_profile" + ) + + profiles = ( + fusion.get( + "homography_profiles", + {} + ) + or {} + ) + + if ( + not selected + or selected not in profiles + ): + raise RuntimeError( + "Perfil de homografia final inválido." + ) + + if bool( + mp[ + "radiometric_config" + ].get( + "enabled", + True, + ) + ): + raise RuntimeError( + "radiometric_config deve permanecer disabled." + ) + + if bool( + mp[ + "patch_normalization" + ].get( + "enabled", + True, + ) + ): + raise RuntimeError( + "patch_normalization deve permanecer disabled." + ) + + if bool( + mp[ + "rgb_calibration" + ].get( + "enabled", + True, + ) + ): + raise RuntimeError( + "rgb_calibration deve permanecer disabled." + ) + + if not bool( + mp[ + "radiometric_normalization" + ].get( + "enabled", + False, + ) + ): + raise RuntimeError( + "radiometric_normalization deve estar enabled." + ) + + if not bool( + mp[ + "flatfield_config" + ].get( + "enabled", + False, + ) + ): + raise RuntimeError( + "flatfield_config deve estar enabled." + ) + + flat_maps = ( + mp[ + "flatfield_config" + ].get( + "channel_maps", + {}, + ) + or {} + ) + + for ch in CHANNELS: + ch_cfg = flat_maps.get( + ch + ) + + if not isinstance( + ch_cfg, + dict, + ): + raise RuntimeError( + f"flatfield_config.channel_maps.{ch} deve ser dict." + ) + + if not ch_cfg.get( + "gain_key" + ): + raise RuntimeError( + f"flatfield_config.channel_maps.{ch}.gain_key ausente." + ) + + if not bool( + mp[ + "intrinsics_config" + ].get( + "enabled", + False, + ) + ): + raise RuntimeError( + "intrinsics_config deve estar enabled." + ) + + # Root deve refletir RGB. + rgb_size = mp[ + "sensor_size_by_role" + ][ + "rgb" + ] + + if [ + mp[ + "sensor_width" + ], + mp[ + "sensor_height" + ], + ] != rgb_size: + raise RuntimeError( + "sensor_width/height root não correspondem ao RGB." + ) + + +# ============================================================ +# Main +# ============================================================ + +def main(): + parser = argparse.ArgumentParser( + description=( + "Assembler fail-closed do module_params final " + "para o módulo multiespectral." + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument( + "--focus-json", + default="calibration/focus_qc_active.json", + ) + + parser.add_argument( + "--flatfield-json", + default="calibration/flatfield_maps_v1.json", + ) + + parser.add_argument( + "--flatfield-npz", + default=None, + help=( + "Override opcional do NPZ. Normalmente é obtido " + "do próprio flatfield JSON." + ), + ) + + parser.add_argument( + "--radiometry-json", + default="calibration/radiometry_calibration_v5.json", + ) + + parser.add_argument( + "--startup-json", + default="calibration/camera_startup_profile_v3.json", + ) + + parser.add_argument( + "--intrinsics-json", + default="calibration/intrinsics_calibration_v1.json", + ) + + parser.add_argument( + "--homography-json", + default="calibration/homography_calibration_v4.json", + ) + + parser.add_argument( + "--homography-profile", + default=None, + help=( + "Override do perfil ativo. Vazio usa recommended_profile " + "da calibração de homografia." + ), + ) + + parser.add_argument( + "--runtime-base-json", + default="calibration/module_params.json", + help=( + "Fonte SOMENTE de políticas runtime não calibradas. " + "Se inexistente, usa defaults seguros." + ), + ) + + parser.add_argument( + "--out", + default="calibration/module_params.json", + ) + + parser.add_argument( + "--report", + default="calibration/module_params_assembly_report.json", + ) + + parser.add_argument( + "--check-only", + action="store_true", + help="Audita tudo sem escrever module_params.", + ) + + args = parser.parse_args() + + # -------------------------------------------------------- + # Load all official artifacts + # -------------------------------------------------------- + + focus = load_json( + args.focus_json + ) + + flatfield = load_json( + args.flatfield_json + ) + + radiometry = load_json( + args.radiometry_json + ) + + startup = load_json( + args.startup_json + ) + + intrinsics = load_json( + args.intrinsics_json + ) + + homography = load_json( + args.homography_json + ) + + artifacts = { + "focus": focus, + "flatfield": flatfield, + "radiometry": radiometry, + "startup": startup, + "intrinsics": intrinsics, + "homography": homography, + } + + # -------------------------------------------------------- + # Individual validation + # -------------------------------------------------------- + + validate_focus( + focus + ) + + flatfield_cfg, flatfield_npz = ( + validate_flatfield( + flatfield, + args.flatfield_json, + args.flatfield_npz, + ) + ) + + rad_norm = validate_radiometry( + radiometry + ) + + camera_settings, rgb_calibration = ( + validate_startup( + startup, + radiometry, + ) + ) + + intrinsics_cfg = ( + validate_intrinsics( + intrinsics + ) + ) + + fusion_cfg = ( + validate_homography( + homography, + args.homography_profile, + ) + ) + + # -------------------------------------------------------- + # Hardware consistency + # -------------------------------------------------------- + + signatures = { + "focus": signature_from_focus( + focus + ), + "flatfield": signature_from_flatfield( + flatfield + ), + "radiometry": signature_from_direct( + radiometry + ), + "startup": signature_from_direct( + startup + ), + "intrinsics": signature_from_direct( + intrinsics + ), + "homography": normalize_signature( + homography.get( + "hardware_signature", + {}, + ) + ), + } + + common_signature = ( + compare_signatures( + signatures + ) + ) + + mx_ids = extract_mx_ids( + artifacts + ) + + common_mx = validate_single_value( + mx_ids, + "MX ID", + ) + + module_ids = extract_module_ids( + artifacts + ) + + common_module_id = ( + validate_single_value( + module_ids, + "module_id", + ) + ) + + # -------------------------------------------------------- + # Cross-contract checks + # -------------------------------------------------------- + + validate_geometry_contract( + intrinsics_cfg, + homography, + fusion_cfg, + ) + + # Intrinsics dimensions must exactly match common hardware. + for role in ROLES: + intr_size = ( + intrinsics_cfg[ + "cameras" + ][role][ + "image_size" + ] + ) + + if ( + intr_size + != common_signature[ + role + ][ + "size" + ] + ): + raise RuntimeError( + f"Intrinsics {role} size={intr_size} " + f"!= hardware={common_signature[role]['size']}" + ) + + # Startup fallback must match calibrated reference controls. + refs = rad_norm[ + "reference_controls" + ] + + for role in ROLES: + startup_role = camera_settings[ + role + ] + + if int( + startup_role[ + "exposure_time_us" + ] + ) != int( + refs[ + role + ][ + "exposure_time_us" + ] + ): + raise RuntimeError( + f"Startup fallback EXP de {role} diverge da Radiometry." + ) + + # -------------------------------------------------------- + # Runtime policy whitelist + # -------------------------------------------------------- + + runtime_base = load_json( + args.runtime_base_json, + required=False, + ) + + runtime_policy = ( + read_runtime_policy( + runtime_base + ) + ) + + # Bayer comes from production Flat-Field hardware resolution. + bayer = resolve_bayer_from_flatfield( + flatfield + ) + + # -------------------------------------------------------- + # Provenance + # -------------------------------------------------------- + + provenance = { + "hardware_signature": deepcopy( + common_signature + ), + "device_mx_id": common_mx, + "module_id": common_module_id, + "artifacts": { + "focus": artifact_meta( + args.focus_json, + focus, + ), + "flatfield": { + **artifact_meta( + args.flatfield_json, + flatfield, + ), + "npz_path": norm_path( + flatfield_npz + ), + "npz_sha256": sha256_file( + flatfield_npz + ), + }, + "radiometry": artifact_meta( + args.radiometry_json, + radiometry, + ), + "startup": artifact_meta( + args.startup_json, + startup, + ), + "intrinsics": artifact_meta( + args.intrinsics_json, + intrinsics, + ), + "homography": { + **artifact_meta( + args.homography_json, + homography, + ), + "selected_profile": ( + fusion_cfg[ + "homography_profile" + ] + ), + }, + }, + } + + # -------------------------------------------------------- + # Assemble + # -------------------------------------------------------- + + module_params = build_final_module_params( + runtime_policy=runtime_policy, + hardware_signature=common_signature, + bayer_pattern=bayer, + camera_settings=camera_settings, + rgb_calibration=rgb_calibration, + radiometric_normalization=rad_norm, + flatfield_config=flatfield_cfg, + intrinsics_config=intrinsics_cfg, + fusion_config=fusion_cfg, + provenance=provenance, + assembly_report_path=args.report, + ) + + validate_final_module_params( + module_params + ) + + report = { + "schema": ASSEMBLER_SCHEMA, + "created_at": now_str(), + "status": "pass", + "check_only": bool( + args.check_only + ), + "output": norm_path( + args.out + ), + "runtime_base_json": norm_path( + args.runtime_base_json + ), + "runtime_base_found": bool( + runtime_base + ), + "hardware_signature": ( + common_signature + ), + "device_mx_id": ( + common_mx + ), + "module_id": ( + common_module_id + ), + "bayer_pattern": bayer, + "homography_profile": ( + fusion_cfg[ + "homography_profile" + ] + ), + "geometric_contract": { + "intrinsics_space": ( + intrinsics_cfg.get( + "calibration_space" + ) + ), + "homography_space": ( + homography.get( + "coordinate_space" + ) + ), + "runtime_undistort_enabled": bool( + ( + intrinsics_cfg.get( + "runtime_undistort", + {} + ) + or {} + ).get( + "enabled", + False, + ) + ), + }, + "runtime_policy": runtime_policy, + "calibration_provenance": provenance, + "final_checks": { + "focus_gate": "pass", + "flatfield": "pass", + "radiometry": "pass", + "startup": "pass", + "intrinsics": "pass", + "homography": "pass", + "hardware_consistency": "pass", + "geometry_consistency": "pass", + "startup_radiometry_consistency": "pass", + "flatfield_npz_integrity": "pass", + "module_params_structure": "pass", + }, + } + + # -------------------------------------------------------- + # Print audit before write + # -------------------------------------------------------- + + print("=" * 92) + print("MODULE PARAMS ASSEMBLER - PRODUCTION") + print(f"MX ID : {common_mx}") + print(f"Module ID : {common_module_id}") + print( + "Hardware : " + f"RGB={common_signature['rgb']['sensor']} " + f"{common_signature['rgb']['size'][0]}x{common_signature['rgb']['size'][1]} | " + f"RE={common_signature['re']['sensor']} " + f"{common_signature['re']['size'][0]}x{common_signature['re']['size'][1]} | " + f"NIR={common_signature['nir']['sensor']} " + f"{common_signature['nir']['size'][0]}x{common_signature['nir']['size'][1]}" + ) + print(f"Bayer : {bayer}") + print( + f"RGB process : " + f"{runtime_policy['rgb_processing'].get('mode')}" + ) + print( + f"Homography : " + f"{fusion_cfg['homography_profile']}" + ) + print( + f"Undistort : " + f"{intrinsics_cfg['runtime_undistort'].get('enabled', False)}" + ) + print( + "Policies : " + "radiometric_config=OFF | patch_normalization=OFF | rgb_calibration=OFF" + ) + print( + "Active : " + "radiometric_normalization=ON | flatfield=ON | intrinsics=ON | homography=ON" + ) + print("-" * 92) + + for name in ( + "focus", + "flatfield", + "radiometry", + "startup", + "intrinsics", + "homography", + ): + meta = provenance[ + "artifacts" + ][name] + + print( + f"[OK] {name:11s} " + f"schema={meta.get('schema')} " + f"sha256={meta.get('sha256', '')[:12]}..." + ) + + if args.check_only: + print("-" * 92) + print("[CHECK-ONLY] Tudo consistente. Nenhum arquivo foi alterado.") + print("=" * 92) + return + + # -------------------------------------------------------- + # Atomic write + backup + # -------------------------------------------------------- + + out = Path( + args.out + ) + + ensure_dir( + out.parent + ) + + if out.exists(): + backup = out.with_name( + out.stem + + ".backup_" + + stamp() + + out.suffix + ) + + shutil.copy2( + out, + backup, + ) + + report[ + "backup_created" + ] = norm_path( + backup + ) + + save_json_atomic( + out, + module_params, + ) + + report[ + "output_sha256" + ] = sha256_file( + out + ) + + save_json_atomic( + args.report, + report, + ) + + print("-" * 92) + print(f"[PASS] module_params : {out}") + print(f"[PASS] assembly report: {args.report}") + print(f"[SHA256] {report['output_sha256']}") + print("=" * 92) + + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/build_module_params.py b/Python/OAK/datasets/oak-fcc-3/utils/build_module_params.py deleted file mode 100644 index 798f1b23f..000000000 --- a/Python/OAK/datasets/oak-fcc-3/utils/build_module_params.py +++ /dev/null @@ -1,654 +0,0 @@ -import json -import argparse -import os -from copy import deepcopy -from datetime import datetime - - -# ============================================================ -# Helpers -# ============================================================ - - -def now_str(): - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - -def load_json(path, required=True): - if not path or not os.path.isfile(path): - if required: - raise FileNotFoundError(f"Arquivo não encontrado: {path}") - return {} - - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - - -def save_json(path, data): - out_dir = os.path.dirname(os.path.abspath(path)) - if out_dir: - os.makedirs(out_dir, exist_ok=True) - - with open(path, "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=2) - f.write("\n") - - -def rel_or_abs(path): - """ - Mantém o caminho como veio, mas normaliza separadores. - Isso evita quebrar projetos Windows/Linux e deixa o module_params legível. - """ - if path is None: - return None - return str(path).replace("\\", "/") - - -def deep_merge(base, update, *, skip_none=True): - """ - Merge recursivo seguro. - - - dict + dict: combina recursivamente. - - listas/escalares: valor novo substitui o antigo. - - None: por padrão NÃO apaga valor antigo, para evitar perder calibração quando - um arquivo fonte não conhece determinada chave. - """ - if not isinstance(base, dict): - base = {} - out = deepcopy(base) - - if not isinstance(update, dict): - return out - - for key, value in update.items(): - if value is None and skip_none: - continue - - if isinstance(value, dict) and isinstance(out.get(key), dict): - out[key] = deep_merge(out[key], value, skip_none=skip_none) - else: - out[key] = deepcopy(value) - - return out - - -def first_dict(*values): - for value in values: - if isinstance(value, dict): - return value - return None - - -# ============================================================ -# Defaults coerentes com RawProcessorCore + module_params atual -# ============================================================ - - -DEFAULT_RGB_PROCESSING = { - "mode": "bayer_planes", -} - - -DEFAULT_PATCH_NORMALIZATION = { - "enabled": True, - "apply_when_metering_mode": "reference_patches", - "apply_stage": "after_fusion", - "method": "gray_scale_with_white_guard", - "space": "multispec_tensor", - "targets_by_patch_channel": { - "black": { - "R": 0.06, - "G": 0.06, - "B": 0.06, - "RE": 0.06, - "NIR": 0.06, - }, - "gray": { - "R": 0.34, - "G": 0.34, - "B": 0.34, - "RE": 0.24, - "NIR": 0.30, - }, - "white": { - "R": 0.78, - "G": 0.78, - "B": 0.78, - "RE": 0.78, - "NIR": 0.78, - }, - }, - "white_guard_max": 0.92, - "white_guard_max_by_channel": { - "R": 0.92, - "G": 0.92, - "B": 0.92, - "RE": 0.88, - "NIR": 0.88, - }, - "scale_min": 0.35, - "scale_max": 2.5, - "clip_output": True, - "require_valid_gray": True, - "use_black_for_offset": False, - "save_patch_stats": True, - "rgb_saturation_guard_enabled": True, - "rgb_saturation_guard_mode": "fade_strength", - "rgb_saturation_soft_start": 0.88, - "rgb_saturation_hard": 0.97, - "rgb_saturation_threshold": 0.97, -} - - -DEFAULT_FLATFIELD_RUNTIME = { - "strength": 0.35, - "strength_by_channel": { - "R": 0.9, - "G": 0.9, - "B": 0.9, - "RE": 0.25, - "NIR": 0.25, - }, - "gain_min_runtime": 0.75, - "gain_max_runtime": 1.35, - "runtime_smooth_ksize": 81, - "saturation_guard_enabled": True, - "saturation_guard_mode": "fade_strength", - "saturation_guard_threshold": 0.97, - "saturation_guard_soft_start": 0.88, - "saturation_guard_hard": 0.97, -} - - -DEFAULT_RADIOMETRIC_NORMALIZATION = { - "enabled": False, - "method": "exposure_gain_reference", - "apply_stage": "after_dark_before_flat_gain", - "reference_controls": { - "rgb": {"exposure_time_us": 3000, "analogue_gain": 1.0}, - "re": {"exposure_time_us": 7000, "analogue_gain": 1.0}, - "nir": {"exposure_time_us": 7000, "analogue_gain": 1.0}, - }, - "clip_output": True, -} - - -DEFAULT_RADIOMETRIC_CONFIG = { - "enabled": True, - "interval_s": 0.25, - "verbose": True, - "metering_mode": "reference_patches", - "spectral_control_mode": "shared", - "control_metric": "p50", - "target_value": 0.5, - "deadband": 0.055, - "p95_limit": 0.975, - "saturation_limit_pct": 5.0, - "alpha": 0.18, - "exp_step_gain": 0.55, - "prefer_exposure": True, - "exp_min_us": 100, - "exp_max_us": 80000, - "gain_min": 1.0, - "gain_max": 4.0, - "exp_apply_threshold_us": 15, - "gain_apply_threshold": 0.05, - "apply_same_spectral_to_both": True, - "spectral_roles": ["re", "nir"], - "dark_limit_pct": 35.0, - "control_strategy": "ratio", - "ratio_alpha": 0.42, - "ratio_min": 0.72, - "ratio_max": 1.38, - "reduce_fast_factor": 0.8, - "factor_min": 0.62, - "factor_max": 1.42, - "gain_return_enabled": True, - "gain_reduce_on_saturation": True, - "gain_increase_required_cycles": 3, - "gain_decrease_required_cycles": 1, - "gain_step_up": 0.3, - "gain_step_down": 0.5, - "gain_hard_reset_on_saturation": False, - "exp_high_ratio_for_gain": 0.95, - "exp_low_ratio_for_gain_return": 0.75, - "role_limits": { - "rgb": {"exp_min_us": 100, "exp_max_us": 80000, "gain_min": 1.0, "gain_max": 2.0}, - "re": {"exp_min_us": 100, "exp_max_us": 3500, "gain_min": 1.0, "gain_max": 2.0}, - "nir": {"exp_min_us": 100, "exp_max_us": 3500, "gain_min": 1.0, "gain_max": 2.0}, - }, - "ready_required_cycles": 3, - "patch_control_mode": "gray_primary", - "patch_require_order": True, - "patch_min_separation": 0.08, - "patch_white_sat_limit_pct": 5.0, - "patch_white_p95_limit": 0.985, - "patch_black_dark_limit_pct": 80.0, - "patch_black_max_p50": 0.2, - "patch_gray_min_p50": 0.08, - "patch_gray_max_p50": 0.85, - "patch_roi_contract": "multi_roi_by_role_v1", - "patch_roi_reduce_method": "median_valid_rois", - "patch_roi_outlier_reject": True, - "patch_roi_max_p50_delta": 0.12, - "global_saturation_guard_enabled": True, - "global_guard_roi_pct": {"x0": 0.05, "y0": 0.05, "x1": 0.95, "y1": 0.76}, - "global_guard_sat_threshold": 0.985, - "global_guard_near_sat_threshold": 0.94, - "global_guard_sat_pct_soft": 0.50, - "global_guard_sat_pct_hard": 1.5, - "global_guard_sat_pct_extreme": 5.0, - "global_guard_blob_pct_soft": 0.20, - "global_guard_blob_pct_hard": 0.80, - "global_guard_blob_pct_extreme": 2.2, - "global_guard_min_blob_px": 48, - "global_guard_downsample_max_side": 320, - "global_guard_reduce_factor_soft": 0.96, - "global_guard_reduce_factor_hard": 0.82, - "global_guard_reduce_factor_extreme": 0.62, - "sun_guard_enabled": True, - "sun_guard_p99_threshold": 0.96, - "sun_guard_near_sat_pct_threshold": 2.0, - "sun_guard_freeze_increase_cycles": 1, - "sun_guard_allow_decrease": True, - "guard_force_apply_enabled": True, - "guard_force_apply_soft": False, - "guard_force_apply_hard": True, - "guard_force_apply_extreme": True, - "guard_force_apply_on_patch_saturation": True, - "guard_freeze_cycles_soft": 1, - "guard_freeze_cycles_hard": 2, - "guard_freeze_cycles_extreme": 3, - "guard_reapply_min_exp_on_emergency": True, - "guard_min_exp_margin_us": 80, - "patch_two_roi_soften_risk": True, - "patch_two_roi_white_risk_percentile": 75, - "patch_two_roi_other_risk_percentile": 50, - "patch_white_single_roi_saturation_reject": True, - "patch_white_roi_reject_sat_pct": 5.0, - "patch_white_roi_reject_p95": 0.995, -} - - -def default_module_template(): - return { - "schema": "multispec_module_params_v3", - "saved_at": now_str(), - "frame_type": "RAW_BRUTO", - "capture_mode_requested": "AUTO", - "capture_mode_effective": "AUTO", - "raw_policy": "allow_single", - "sensor_width": 1280, - "sensor_height": 800, - "bayer_pattern": "BGGR", - "rgb_processing": deepcopy(DEFAULT_RGB_PROCESSING), - "camera_settings": {}, - "fusion_config": { - "alignment_mode": "manual_affine", - "baseline_mm": 75.0, - "reference_camera": "rgb", - "manual_offsets": { - "re": {"dx": 0, "dy": 0, "theta_deg": 0.0}, - "nir": {"dx": 0, "dy": 0, "theta_deg": 0.0}, - }, - "homographies": { - "re_to_rgb": None, - "nir_to_rgb": None, - }, - "crop_valid_common": True, - "resize_after_crop": True, - "target_size": None, - }, - "radiometric_config": deepcopy(DEFAULT_RADIOMETRIC_CONFIG), - "radiometric_normalization": deepcopy(DEFAULT_RADIOMETRIC_NORMALIZATION), - "patch_normalization": deepcopy(DEFAULT_PATCH_NORMALIZATION), - "rgb_calibration": { - "enabled": False, - "gains": {"R": 1.0, "G": 1.0, "B": 1.0}, - }, - "flatfield_config": deep_merge( - { - "enabled": False, - "reason": "flatfield não informado ou arquivo inexistente", - "subtract_dark": True, - "apply_before_fusion": True, - "apply_after_decode": True, - "apply_space": "native_camera_space", - "map_type": "gain", - "channels": ["R", "G", "B", "RE", "NIR"], - "channel_maps": {}, - "clip_output": True, - }, - DEFAULT_FLATFIELD_RUNTIME, - ), - } - - -# ============================================================ -# Builders / normalizers -# ============================================================ - - -def build_flatfield_config(flatfield_json_path, flatfield_data, previous_flatfield_config=None): - """ - Espera o JSON gerado pelo flatfield_calibration_tool_v2.py. - - Importante: usa previous_flatfield_config como base para preservar knobs runtime - que não existem no arquivo de calibração do flat-field, como strength, - gain_min_runtime, runtime_smooth_ksize e saturation_guard_*. - """ - base = deep_merge( - deep_merge({}, previous_flatfield_config or {}), - DEFAULT_FLATFIELD_RUNTIME, - ) - - if not isinstance(flatfield_data, dict): - return deep_merge(base, { - "enabled": False, - "reason": "flatfield_json ausente ou inválido", - }) - - outputs = flatfield_data.get("outputs", {}) or {} - maps = flatfield_data.get("maps", {}) or {} - - npz_path = outputs.get("npz") - if not npz_path: - base_name, _ = os.path.splitext(flatfield_json_path) - npz_path = base_name + ".npz" - - channels = flatfield_data.get("channels") or base.get("channels") or ["R", "G", "B", "RE", "NIR"] - - channel_maps = {} - previous_channel_maps = base.get("channel_maps", {}) or {} - for ch in channels: - m = maps.get(ch, {}) or {} - prev = previous_channel_maps.get(ch, {}) or {} - channel_maps[ch] = deep_merge(prev, { - "gain_key": m.get("gain_key", f"gain_{ch}"), - "flat_norm_key": m.get("flat_norm_key", f"flat_norm_{ch}"), - "white_median_key": m.get("white_median_key", f"white_median_{ch}"), - "dark_median_key": m.get("dark_median_key", f"dark_median_{ch}"), - "shape": m.get("shape"), - "gain_min": m.get("gain_min"), - "gain_max": m.get("gain_max"), - "gain_mean": m.get("gain_mean"), - "gain_std": m.get("gain_std"), - }) - - generated = { - "enabled": True, - "subtract_dark": True, - "schema": flatfield_data.get("schema", "multispec_flatfield_v1"), - "created_at": flatfield_data.get("created_at"), - "json_file": rel_or_abs(flatfield_json_path), - "npz_file": rel_or_abs(npz_path), - "apply_before_fusion": True, - "apply_after_decode": True, - "apply_space": "native_camera_space", - "map_type": "gain", - "formula": "channel_corrected = max(channel_linear - dark, 0) * gain_map", - "channels": channels, - "channel_maps": channel_maps, - "exp_gain_correct_during_flat_capture": bool(flatfield_data.get("exp_gain_correct", False)), - "smooth_ksize": flatfield_data.get("smooth_ksize"), - "min_gain": flatfield_data.get("min_gain"), - "max_gain": flatfield_data.get("max_gain"), - "notes": flatfield_data.get("notes", ""), - } - - return deep_merge(base, generated) - - -def pick_radiometric_config(radiometric_data: dict, selected_profile: str | None = None): - if not isinstance(radiometric_data, dict): - return None - - root_cfg = radiometric_data.get("radiometric_config") - if isinstance(root_cfg, dict): - return root_cfg - - active_profile = radiometric_data.get("active_profile") - if active_profile in ("global_scene_mode", "three_reference_patches_mode"): - cfg = radiometric_data.get(active_profile, {}).get("radiometric_config") - if isinstance(cfg, dict): - return cfg - - if selected_profile: - cfg = radiometric_data.get(selected_profile, {}).get("radiometric_config") - if isinstance(cfg, dict): - return cfg - - return None - - -def normalize_patch_normalization_contract(base_patch_config, incoming_patch_config=None): - """ - Garante o contrato atual do RawProcessorCore. - - - Sempre tem targets_by_patch_channel. - - Preserva white_guard_max_by_channel. - - Preserva rgb_saturation_guard_*. - - Remove a chave legada targets, porque ela não é usada pelo core atual. - """ - cfg = deep_merge(DEFAULT_PATCH_NORMALIZATION, base_patch_config or {}) - cfg = deep_merge(cfg, incoming_patch_config or {}) - - legacy_targets = cfg.pop("targets", None) - if isinstance(legacy_targets, dict) and "targets_by_patch_channel" not in cfg: - # Fallback conservador. Na prática, com DEFAULT_PATCH_NORMALIZATION acima, - # normalmente não entra aqui. Mantido só para arquivos muito antigos. - t = deepcopy(DEFAULT_PATCH_NORMALIZATION["targets_by_patch_channel"]) - for patch_type in ("black", "gray", "white"): - if patch_type in legacy_targets: - scalar = legacy_targets.get(patch_type) - try: - scalar = float(scalar) - for ch in ("R", "G", "B", "RE", "NIR"): - t[patch_type][ch] = scalar - except Exception: - pass - cfg["targets_by_patch_channel"] = t - - cfg = deep_merge(DEFAULT_PATCH_NORMALIZATION, cfg) - return cfg - - -def normalize_radiometric_config(base_rad_config, incoming_rad_config=None): - cfg = deep_merge(DEFAULT_RADIOMETRIC_CONFIG, base_rad_config or {}) - cfg = deep_merge(cfg, incoming_rad_config or {}) - return cfg - - -def normalize_radiometric_normalization(base_config, incoming_config=None): - cfg = deep_merge(DEFAULT_RADIOMETRIC_NORMALIZATION, base_config or {}) - cfg = deep_merge(cfg, incoming_config or {}) - cfg["enabled"] = bool(cfg.get("enabled", False)) - return cfg - - -def build_fusion_config(fusion_data, previous_fusion_config=None): - base = previous_fusion_config or {} - generated = { - "alignment_mode": fusion_data.get("alignment_mode"), - "baseline_mm": fusion_data.get("baseline_mm"), - "reference_camera": fusion_data.get("reference_camera"), - "manual_offsets": fusion_data.get("manual_offsets"), - "homographies": fusion_data.get("homographies"), - "crop_valid_common": fusion_data.get("crop_valid_common"), - "resize_after_crop": fusion_data.get("resize_after_crop"), - "target_size": fusion_data.get("target_size"), - } - cfg = deep_merge(default_module_template()["fusion_config"], base) - cfg = deep_merge(cfg, generated) - return cfg - - -def load_base_module(args): - """ - Carrega defaults do module_params atual. - - Prioridade: - 1. --base_module_json, se informado. - 2. --out, se já existir. - 3. template interno coerente com o contrato atual. - """ - candidates = [] - if args.base_module_json: - candidates.append(args.base_module_json) - if args.out: - candidates.append(args.out) - - for path in candidates: - if path and os.path.isfile(path): - print(f"[INFO] Usando module_params base: {path}") - return deep_merge(default_module_template(), load_json(path, required=True)) - - print("[WARN] Nenhum module_params base encontrado. Usando defaults internos.") - return default_module_template() - - -# ============================================================ -# Main -# ============================================================ - - -def main(): - parser = argparse.ArgumentParser( - description="Monta o module_params.json preservando o contrato atual do RawProcessorCore.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument("--camera_json", default="calibration/sensor_calibration.json") - parser.add_argument("--fusion_json", default="calibration/manual_offsets.json") - parser.add_argument("--radiometric_json", default="calibration/radiometric_config.json") - parser.add_argument( - "--radiometric_profile", - default="global_scene_mode", - choices=["global_scene_mode", "three_reference_patches_mode"], - ) - parser.add_argument("--flatfield_json", default="calibration/flatfield_maps_v1.json") - parser.add_argument("--disable_flatfield", action="store_true") - parser.add_argument("--base_module_json", default=None, help="module_params atual usado como defaults antes de sobrescrever") - parser.add_argument("--out", default="calibration/module_params.json") - args = parser.parse_args() - - module_base = load_base_module(args) - - cam_data = load_json(args.camera_json, required=True) - fusion_data = load_json(args.fusion_json, required=True) - radiometric_data = load_json(args.radiometric_json, required=False) if args.radiometric_json else {} - - # ========================= - # MODULE PARAMS FINAL - # ========================= - module_params = deepcopy(module_base) - module_params["schema"] = "multispec_module_params_v3" - module_params["saved_at"] = now_str() - - # ========================= - # ROOT / CAMERA - # ========================= - root_updates = { - "frame_type": cam_data.get("frame_type", fusion_data.get("frame_type")), - "capture_mode_requested": cam_data.get("capture_mode_requested"), - "capture_mode_effective": cam_data.get("capture_mode_effective"), - "raw_policy": cam_data.get("raw_policy"), - "sensor_width": cam_data.get("sensor_width", fusion_data.get("sensor_width")), - "sensor_height": cam_data.get("sensor_height", fusion_data.get("sensor_height")), - "bayer_pattern": cam_data.get("bayer_pattern", fusion_data.get("bayer_pattern")), - } - module_params = deep_merge(module_params, root_updates) - - module_params["rgb_processing"] = deep_merge( - deep_merge(DEFAULT_RGB_PROCESSING, module_base.get("rgb_processing", {})), - cam_data.get("rgb_processing") if isinstance(cam_data.get("rgb_processing"), dict) else {}, - ) - - camera_settings = cam_data.get("camera_settings") - if not isinstance(camera_settings, dict): - camera_settings = module_base.get("camera_settings") - if not isinstance(camera_settings, dict): - raise RuntimeError("camera_json sem camera_settings válido e sem fallback no module_params base") - module_params["camera_settings"] = camera_settings - - module_params["rgb_calibration"] = deep_merge( - module_base.get("rgb_calibration", {}), - cam_data.get("rgb_calibration") if isinstance(cam_data.get("rgb_calibration"), dict) else {}, - ) - - # ========================= - # FUSION - # ========================= - module_params["fusion_config"] = build_fusion_config( - fusion_data, - previous_fusion_config=module_base.get("fusion_config", {}), - ) - - # ========================= - # RADIOMETRIC - # ========================= - incoming_rad = pick_radiometric_config(radiometric_data, selected_profile=args.radiometric_profile) - if not isinstance(incoming_rad, dict): - incoming_rad = cam_data.get("radiometric_config") if isinstance(cam_data.get("radiometric_config"), dict) else {} - - module_params["radiometric_config"] = normalize_radiometric_config( - module_base.get("radiometric_config", {}), - incoming_rad, - ) - - incoming_rad_norm = first_dict( - radiometric_data.get("radiometric_normalization") if isinstance(radiometric_data, dict) else None, - cam_data.get("radiometric_normalization") if isinstance(cam_data, dict) else None, - ) or {} - module_params["radiometric_normalization"] = normalize_radiometric_normalization( - module_base.get("radiometric_normalization", {}), - incoming_rad_norm, - ) - - incoming_patch_norm = first_dict( - radiometric_data.get("patch_normalization") if isinstance(radiometric_data, dict) else None, - cam_data.get("patch_normalization") if isinstance(cam_data, dict) else None, - ) or {} - module_params["patch_normalization"] = normalize_patch_normalization_contract( - module_base.get("patch_normalization", {}), - incoming_patch_norm, - ) - - # ========================= - # FLATFIELD - # ========================= - previous_flatfield = module_base.get("flatfield_config", {}) or {} - if args.disable_flatfield: - module_params["flatfield_config"] = deep_merge(previous_flatfield, { - "enabled": False, - "reason": "desabilitado via --disable_flatfield", - }) - elif args.flatfield_json and os.path.isfile(args.flatfield_json): - flatfield_data = load_json(args.flatfield_json, required=True) - module_params["flatfield_config"] = build_flatfield_config( - args.flatfield_json, - flatfield_data, - previous_flatfield_config=previous_flatfield, - ) - else: - # Não achou novo flatfield: preserva o anterior se já existia. - module_params["flatfield_config"] = deep_merge(previous_flatfield, DEFAULT_FLATFIELD_RUNTIME) - if not module_params["flatfield_config"].get("enabled", False): - module_params["flatfield_config"]["reason"] = "flatfield não informado ou arquivo inexistente" - - # ========================= - # Save - # ========================= - save_json(args.out, module_params) - - print(f"[OK] module_params gerado em: {args.out}") - print("[OK] contrato preservado: rgb_processing, patch_normalization, radiometric_config e knobs runtime do flatfield") - - flat_cfg = module_params.get("flatfield_config", {}) or {} - if flat_cfg.get("enabled"): - print(f"[OK] flatfield habilitado: {flat_cfg.get('npz_file')}") - else: - print(f"[WARN] flatfield desabilitado: {flat_cfg.get('reason')}") - - -if __name__ == "__main__": - main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/flatfield_calibration_tool_quality.py b/Python/OAK/datasets/oak-fcc-3/utils/flatfield_calibration_tool_quality.py deleted file mode 100644 index 0452a96c8..000000000 --- a/Python/OAK/datasets/oak-fcc-3/utils/flatfield_calibration_tool_quality.py +++ /dev/null @@ -1,1345 +0,0 @@ -import os -import json -import time -import argparse -from datetime import datetime -from pathlib import Path - -import cv2 -import numpy as np - -from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient - - -# ============================================================ -# Helpers gerais -# ============================================================ - -def now_str() -> str: - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - -def ensure_dir(path: str): - os.makedirs(path, exist_ok=True) - - -def safe_float(v, default=None): - try: - if v is None: - return default - return float(v) - except Exception: - return default - - -def safe_int(v, default=None): - try: - if v is None: - return default - return int(v) - except Exception: - return default - - -def overlay_hud( - img_bgr, - lines, - x=12, - y=24, - font_scale=0.58, - line_step=22, - color=(255, 255, 255), - shadow=(0, 0, 0), -): - yy = int(y) - for s in lines: - cv2.putText(img_bgr, str(s), (int(x), yy), cv2.FONT_HERSHEY_SIMPLEX, - font_scale, shadow, 3, cv2.LINE_AA) - cv2.putText(img_bgr, str(s), (int(x), yy), cv2.FONT_HERSHEY_SIMPLEX, - font_scale, color, 1, cv2.LINE_AA) - yy += int(line_step) - - -def to_bgr_u8_from_rgb01(rgb01: np.ndarray) -> np.ndarray: - rgb_u8 = np.clip(rgb01 * 255.0, 0, 255).astype(np.uint8) - return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR) - - -def gray_to_bgr_u8(gray01: np.ndarray) -> np.ndarray: - g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8) - return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR) - - -def resize_if_needed(img: np.ndarray, target_hw: tuple[int, int]) -> np.ndarray: - if img is None: - return None - th, tw = target_hw - if img.shape[:2] == (th, tw): - return img - return cv2.resize(img, (tw, th), interpolation=cv2.INTER_LINEAR) - - -def get_decoded_by_role(decoded: dict, role: str): - role = str(role).lower() - for cam_id, item in (decoded or {}).items(): - if str(item.get("role", "")).lower() == role: - return cam_id, item - return None, None - - -def get_image_by_role(decoded: dict, role: str): - cam_id, item = get_decoded_by_role(decoded, role) - if item is None: - return cam_id, None - return cam_id, item.get("image") - - -def validate_module_ready(status: dict, raw_policy: str): - if not status.get("ok", True): - raise RuntimeError(f"Status invalido retornado pelo modulo: {status}") - - active_roles = status.get("active_roles", {}) or {} - active_count = int(status.get("camera_count_active", 0)) - - if raw_policy == "require_triple": - missing = [role for role in ("rgb", "nir", "re") if role not in active_roles] - if missing: - raise RuntimeError( - "RAW_BRUTO com require_triple exige rgb/nir/re ativas. " - f"Faltando: {missing}. Ativas: {active_roles}" - ) - elif active_count < 1: - raise RuntimeError("RAW_BRUTO requer ao menos uma câmera ativa.") - - -def compute_image_stats(img01: np.ndarray) -> dict: - if img01 is None: - return { - "valid": False, - "mean": 0.0, - "std": 0.0, - "p01": 0.0, - "p05": 0.0, - "p50": 0.0, - "p95": 0.0, - "p99": 0.0, - "sat_pct": 0.0, - "dark_pct": 0.0, - } - - arr = img01.astype(np.float32).reshape(-1) - return { - "valid": True, - "mean": float(arr.mean()), - "std": float(arr.std()), - "p01": float(np.percentile(arr, 1)), - "p05": float(np.percentile(arr, 5)), - "p50": float(np.percentile(arr, 50)), - "p95": float(np.percentile(arr, 95)), - "p99": float(np.percentile(arr, 99)), - "sat_pct": float((arr >= 0.985).mean() * 100.0), - "dark_pct": float((arr <= 0.015).mean() * 100.0), - } - - -def robust_center(x: np.ndarray, low_pct=10.0, high_pct=90.0) -> float: - arr = x[np.isfinite(x)].astype(np.float32).reshape(-1) - if arr.size == 0: - return 1.0 - - lo = np.percentile(arr, low_pct) - hi = np.percentile(arr, high_pct) - core = arr[(arr >= lo) & (arr <= hi)] - if core.size == 0: - core = arr - - v = float(np.median(core)) - if not np.isfinite(v) or v <= 1e-8: - return 1.0 - return v - - -def normalize_for_display(img: np.ndarray) -> np.ndarray: - if img is None: - return None - - arr = img.astype(np.float32) - finite = arr[np.isfinite(arr)] - if finite.size == 0: - return np.zeros_like(arr, dtype=np.float32) - - lo = np.percentile(finite, 1) - hi = np.percentile(finite, 99) - if hi <= lo: - hi = lo + 1e-6 - - out = (arr - lo) / (hi - lo) - return np.clip(out, 0.0, 1.0) - - -def smooth_map_gain(gain: np.ndarray, ksize: int) -> np.ndarray: - if ksize is None or ksize <= 1: - return gain.astype(np.float32) - - if ksize % 2 == 0: - ksize += 1 - - return cv2.GaussianBlur( - gain.astype(np.float32), - (ksize, ksize), - sigmaX=0, - sigmaY=0, - borderType=cv2.BORDER_REFLECT, - ) - - -def clip_gain_map(gain: np.ndarray, min_gain: float, max_gain: float) -> np.ndarray: - return np.clip(gain.astype(np.float32), float(min_gain), float(max_gain)).astype(np.float32) - - -# ============================================================ -# Qualidade de calibração flat-field -# ============================================================ - -QUALITY_THRESHOLDS = { - # Frame WHITE antes/durante captura - "white_sat_warning_pct": 0.10, - "white_sat_bad_pct": 0.50, - "white_dark_warning_pct": 1.00, - "white_dark_bad_pct": 5.00, - "white_uniformity_warning": 0.20, - "white_uniformity_bad": 0.35, - "white_side_ratio_low_warning": 0.80, - "white_side_ratio_low_bad": 0.65, - "white_side_ratio_high_warning": 1.25, - "white_side_ratio_high_bad": 1.45, - - # Mapa final de ganho - "gain_p99_warning": 1.60, - "gain_p99_bad": 1.85, - "gain_max_warning": 1.80, - "gain_max_bad": 2.10, - "gain_side_ratio_warning": 1.30, - "gain_side_ratio_bad": 1.55, - "gain_center_low_warning": 0.80, - "gain_center_low_bad": 0.65, - - # Defeitos locais (sujeira/manchas) após remover tendência suave - "white_local_defect_dev_warning": 0.12, - "white_local_defect_dev_bad": 0.20, - "white_local_outlier_pct_warning": 1.00, - "white_local_outlier_pct_bad": 3.00, - "gain_local_defect_dev_warning": 0.10, - "gain_local_defect_dev_bad": 0.18, - "gain_local_outlier_pct_warning": 1.00, - "gain_local_outlier_pct_bad": 3.00, -} - - -def _status_rank(status: str) -> int: - return {"good": 0, "warning": 1, "bad": 2}.get(str(status).lower(), 0) - - -def _merge_status(a: str, b: str) -> str: - return a if _status_rank(a) >= _status_rank(b) else b - - -def _append_reason(reasons: list[str], condition: bool, status_ref: str, reason: str): - if condition: - reasons.append(reason) - return status_ref - return "good" - - -def _safe_ratio(num: float, den: float, default: float = 1.0) -> float: - try: - num = float(num) - den = float(den) - if not np.isfinite(num) or not np.isfinite(den) or abs(den) <= 1e-9: - return default - return num / den - except Exception: - return default - - -def _crop_margin(arr: np.ndarray, margin_frac: float = 0.08) -> np.ndarray: - h, w = arr.shape[:2] - mx = int(w * margin_frac) - my = int(h * margin_frac) - x0, x1 = mx, max(mx + 1, w - mx) - y0, y1 = my, max(my + 1, h - my) - return arr[y0:y1, x0:x1] - - -def region_profile_stats(img01: np.ndarray) -> dict: - """ - Mede homogeneidade espacial em 3x3 e também em faixas esquerda/centro/direita. - Usa p50 para ser robusto contra textura fina/ruído. - """ - if img01 is None: - return {"valid": False} - - arr = img01.astype(np.float32) - if arr.ndim == 3: - arr = (0.299 * arr[:, :, 0] + 0.587 * arr[:, :, 1] + 0.114 * arr[:, :, 2]).astype(np.float32) - - arr = _crop_margin(arr, margin_frac=0.04) - h, w = arr.shape[:2] - if h < 9 or w < 9: - return {"valid": False} - - cells = [] - grid = [] - for gy in range(3): - row = [] - y0 = int(round(gy * h / 3.0)) - y1 = int(round((gy + 1) * h / 3.0)) - for gx in range(3): - x0 = int(round(gx * w / 3.0)) - x1 = int(round((gx + 1) * w / 3.0)) - cell = arr[y0:y1, x0:x1].reshape(-1) - cell = cell[np.isfinite(cell)] - p50 = float(np.percentile(cell, 50)) if cell.size else 0.0 - row.append(p50) - cells.append(p50) - grid.append(row) - - cells_np = np.array(cells, dtype=np.float32) - center = float(grid[1][1]) - if center <= 1e-9 or not np.isfinite(center): - center = float(np.median(cells_np)) if cells_np.size else 1.0 - - left = float(np.median([grid[0][0], grid[1][0], grid[2][0]])) - mid = float(np.median([grid[0][1], grid[1][1], grid[2][1]])) - right = float(np.median([grid[0][2], grid[1][2], grid[2][2]])) - top = float(np.median(grid[0])) - bottom = float(np.median(grid[2])) - - return { - "valid": True, - "grid_p50": grid, - "center_p50": center, - "left_p50": left, - "mid_p50": mid, - "right_p50": right, - "top_p50": top, - "bottom_p50": bottom, - "min_region_p50": float(cells_np.min()), - "max_region_p50": float(cells_np.max()), - "median_region_p50": float(np.median(cells_np)), - "uniformity_delta": _safe_ratio(float(cells_np.max() - cells_np.min()), center, default=0.0), - "left_center_ratio": _safe_ratio(left, center), - "right_center_ratio": _safe_ratio(right, center), - "top_center_ratio": _safe_ratio(top, center), - "bottom_center_ratio": _safe_ratio(bottom, center), - } - - - - -def local_defect_stats(img01: np.ndarray, blur_frac: float = 0.18, margin_frac: float = 0.04, outlier_thr: float = 0.10) -> dict: - """ - Detecta defeitos locais após remover tendência suave (vinheta/gradiente global). - Útil para sujeira na lente, manchas, poeira, gotas e sombras localizadas. - """ - if img01 is None: - return {"valid": False} - - arr = img01.astype(np.float32) - if arr.ndim == 3: - arr = (0.299 * arr[:, :, 0] + 0.587 * arr[:, :, 1] + 0.114 * arr[:, :, 2]).astype(np.float32) - - arr = _crop_margin(arr, margin_frac=margin_frac) - h, w = arr.shape[:2] - if h < 32 or w < 32: - return {"valid": False} - - k = int(round(min(h, w) * blur_frac)) - k = max(31, k) - if k % 2 == 0: - k += 1 - - smooth = cv2.GaussianBlur(arr, (k, k), sigmaX=0, sigmaY=0, borderType=cv2.BORDER_REFLECT) - ratio = arr / np.maximum(smooth, 1e-6) - ratio = np.clip(ratio, 0.0, 4.0) - resid = ratio - 1.0 - abs_resid = np.abs(resid) - - finite = abs_resid[np.isfinite(abs_resid)] - if finite.size == 0: - return {"valid": False} - - p95 = float(np.percentile(finite, 95)) - p99 = float(np.percentile(finite, 99)) - max_dev = float(np.max(finite)) - outlier_pct = float((finite >= float(outlier_thr)).mean() * 100.0) - - # Percentis direcionais para diagnosticar manchas escuras/claras. - ratio_flat = ratio[np.isfinite(ratio)].reshape(-1) - r_p01 = float(np.percentile(ratio_flat, 1)) if ratio_flat.size else 1.0 - r_p99 = float(np.percentile(ratio_flat, 99)) if ratio_flat.size else 1.0 - - return { - "valid": True, - "blur_ksize": int(k), - "ratio_p01": r_p01, - "ratio_p99": r_p99, - "local_dev_p95": p95, - "local_dev_p99": p99, - "local_dev_max": max_dev, - "local_outlier_pct": outlier_pct, - "outlier_threshold": float(outlier_thr), - } - -def evaluate_white_frame_quality(channels: dict, thresholds: dict | None = None) -> dict: - """Avalia se a cena WHITE/flat está homogênea antes de capturar.""" - th = dict(QUALITY_THRESHOLDS) - if thresholds: - th.update(thresholds) - - result = { - "status": "good", - "reasons": [], - "channels": {}, - "thresholds": th, - } - - for ch, img in (channels or {}).items(): - st = compute_image_stats(img) - rp = region_profile_stats(img) - ld = local_defect_stats(img, outlier_thr=th.get("white_local_defect_dev_warning", 0.10)) - status = "good" - reasons = [] - - sat = float(st.get("sat_pct", 0.0)) - dark = float(st.get("dark_pct", 0.0)) - uniformity = float(rp.get("uniformity_delta", 0.0)) if rp.get("valid") else 0.0 - lcr = float(rp.get("left_center_ratio", 1.0)) if rp.get("valid") else 1.0 - rcr = float(rp.get("right_center_ratio", 1.0)) if rp.get("valid") else 1.0 - tcr = float(rp.get("top_center_ratio", 1.0)) if rp.get("valid") else 1.0 - bcr = float(rp.get("bottom_center_ratio", 1.0)) if rp.get("valid") else 1.0 - ld_dev = float(ld.get("local_dev_p99", 0.0)) if ld.get("valid") else 0.0 - ld_out = float(ld.get("local_outlier_pct", 0.0)) if ld.get("valid") else 0.0 - - if sat > th["white_sat_bad_pct"]: - status = _merge_status(status, "bad"); reasons.append(f"sat_pct_bad:{sat:.2f}%") - elif sat > th["white_sat_warning_pct"]: - status = _merge_status(status, "warning"); reasons.append(f"sat_pct_warning:{sat:.2f}%") - - if dark > th["white_dark_bad_pct"]: - status = _merge_status(status, "bad"); reasons.append(f"dark_pct_bad:{dark:.2f}%") - elif dark > th["white_dark_warning_pct"]: - status = _merge_status(status, "warning"); reasons.append(f"dark_pct_warning:{dark:.2f}%") - - if uniformity > th["white_uniformity_bad"]: - status = _merge_status(status, "bad"); reasons.append(f"uniformity_bad:{uniformity:.2f}") - elif uniformity > th["white_uniformity_warning"]: - status = _merge_status(status, "warning"); reasons.append(f"uniformity_warning:{uniformity:.2f}") - - for name, val in (("left", lcr), ("right", rcr), ("top", tcr), ("bottom", bcr)): - if val < th["white_side_ratio_low_bad"] or val > th["white_side_ratio_high_bad"]: - status = _merge_status(status, "bad"); reasons.append(f"{name}_center_ratio_bad:{val:.2f}") - elif val < th["white_side_ratio_low_warning"] or val > th["white_side_ratio_high_warning"]: - status = _merge_status(status, "warning"); reasons.append(f"{name}_center_ratio_warning:{val:.2f}") - - if ld.get("valid"): - if ld_dev > th["white_local_defect_dev_bad"]: - status = _merge_status(status, "bad"); reasons.append(f"local_defect_dev_bad:{ld_dev:.2f}") - elif ld_dev > th["white_local_defect_dev_warning"]: - status = _merge_status(status, "warning"); reasons.append(f"local_defect_dev_warning:{ld_dev:.2f}") - - if ld_out > th["white_local_outlier_pct_bad"]: - status = _merge_status(status, "bad"); reasons.append(f"local_outlier_pct_bad:{ld_out:.2f}%") - elif ld_out > th["white_local_outlier_pct_warning"]: - status = _merge_status(status, "warning"); reasons.append(f"local_outlier_pct_warning:{ld_out:.2f}%") - - result["channels"][ch] = { - "status": status, - "reasons": reasons, - "stats": st, - "region_profile": rp, - "local_defect": ld, - } - result["status"] = _merge_status(result["status"], status) - result["reasons"].extend([f"{ch}:{r}" for r in reasons[:3]]) - - result["summary"] = { - "channel_count": len(result["channels"]), - "bad_count": sum(1 for v in result["channels"].values() if v.get("status") == "bad"), - "warning_count": sum(1 for v in result["channels"].values() if v.get("status") == "warning"), - } - return result - - -def evaluate_gain_maps_quality(gain_maps: dict, corrected_white: dict | None = None, thresholds: dict | None = None) -> dict: - """Avalia se os mapas de ganho finais estão agressivos demais ou assimétricos.""" - th = dict(QUALITY_THRESHOLDS) - if thresholds: - th.update(thresholds) - - result = { - "status": "good", - "reasons": [], - "channels": {}, - "thresholds": th, - } - - for ch, gain in (gain_maps or {}).items(): - g = gain.astype(np.float32) - finite = g[np.isfinite(g)] - if finite.size == 0: - ch_status = "bad" - reasons = ["gain_map_invalid"] - stats = {} - rp = {"valid": False} - ld = {"valid": False} - else: - stats = { - "min": float(np.min(finite)), - "p01": float(np.percentile(finite, 1)), - "p05": float(np.percentile(finite, 5)), - "p50": float(np.percentile(finite, 50)), - "p95": float(np.percentile(finite, 95)), - "p99": float(np.percentile(finite, 99)), - "max": float(np.max(finite)), - "mean": float(np.mean(finite)), - "std": float(np.std(finite)), - } - rp = region_profile_stats(g) - ld = local_defect_stats(g, outlier_thr=th.get("gain_local_defect_dev_warning", 0.08)) - ch_status = "good" - reasons = [] - - if stats["p99"] > th["gain_p99_bad"]: - ch_status = _merge_status(ch_status, "bad"); reasons.append(f"gain_p99_bad:{stats['p99']:.2f}") - elif stats["p99"] > th["gain_p99_warning"]: - ch_status = _merge_status(ch_status, "warning"); reasons.append(f"gain_p99_warning:{stats['p99']:.2f}") - - if stats["max"] > th["gain_max_bad"]: - ch_status = _merge_status(ch_status, "bad"); reasons.append(f"gain_max_bad:{stats['max']:.2f}") - elif stats["max"] > th["gain_max_warning"]: - ch_status = _merge_status(ch_status, "warning"); reasons.append(f"gain_max_warning:{stats['max']:.2f}") - - if rp.get("valid"): - center = float(rp.get("center_p50", 1.0)) - rcr = float(rp.get("right_center_ratio", 1.0)) - lcr = float(rp.get("left_center_ratio", 1.0)) - center_gain = center - - for name, ratio in (("left", lcr), ("right", rcr)): - inv = max(ratio, 1.0 / max(ratio, 1e-6)) - if inv > th["gain_side_ratio_bad"]: - ch_status = _merge_status(ch_status, "bad"); reasons.append(f"gain_{name}_center_ratio_bad:{ratio:.2f}") - elif inv > th["gain_side_ratio_warning"]: - ch_status = _merge_status(ch_status, "warning"); reasons.append(f"gain_{name}_center_ratio_warning:{ratio:.2f}") - - if center_gain < th["gain_center_low_bad"]: - ch_status = _merge_status(ch_status, "bad"); reasons.append(f"gain_center_low_bad:{center_gain:.2f}") - elif center_gain < th["gain_center_low_warning"]: - ch_status = _merge_status(ch_status, "warning"); reasons.append(f"gain_center_low_warning:{center_gain:.2f}") - - if ld.get("valid"): - ld_dev = float(ld.get("local_dev_p99", 0.0)) - ld_out = float(ld.get("local_outlier_pct", 0.0)) - if ld_dev > th["gain_local_defect_dev_bad"]: - ch_status = _merge_status(ch_status, "bad"); reasons.append(f"gain_local_defect_dev_bad:{ld_dev:.2f}") - elif ld_dev > th["gain_local_defect_dev_warning"]: - ch_status = _merge_status(ch_status, "warning"); reasons.append(f"gain_local_defect_dev_warning:{ld_dev:.2f}") - - if ld_out > th["gain_local_outlier_pct_bad"]: - ch_status = _merge_status(ch_status, "bad"); reasons.append(f"gain_local_outlier_pct_bad:{ld_out:.2f}%") - elif ld_out > th["gain_local_outlier_pct_warning"]: - ch_status = _merge_status(ch_status, "warning"); reasons.append(f"gain_local_outlier_pct_warning:{ld_out:.2f}%") - - white_quality = None - if corrected_white is not None and ch in corrected_white: - white_quality = evaluate_white_frame_quality({ch: corrected_white[ch]}, thresholds=th).get("channels", {}).get(ch) - - result["channels"][ch] = { - "status": ch_status, - "reasons": reasons, - "gain_stats": stats, - "gain_region_profile": rp, - "local_defect": ld, - "white_signal_quality": white_quality, - } - result["status"] = _merge_status(result["status"], ch_status) - result["reasons"].extend([f"{ch}:{r}" for r in reasons[:4]]) - - result["summary"] = { - "channel_count": len(result["channels"]), - "bad_count": sum(1 for v in result["channels"].values() if v.get("status") == "bad"), - "warning_count": sum(1 for v in result["channels"].values() if v.get("status") == "warning"), - } - return result - - -def format_quality_lines(report: dict | None, title: str = "QUALITY", max_channels: int = 5, max_reasons: int = 5) -> list[str]: - if not report: - return [f"{title}: sem dados"] - - status = str(report.get("status", "unknown")).upper() - lines = [f"{title}: {status}"] - - channels = report.get("channels", {}) or {} - for ch in list(channels.keys())[:max_channels]: - item = channels.get(ch, {}) or {} - st = str(item.get("status", "unknown")).upper() - rp = item.get("region_profile") or item.get("gain_region_profile") or {} - stats = item.get("stats") or item.get("gain_stats") or {} - ld = item.get("local_defect") or {} - ld_txt = "" - if isinstance(ld, dict) and ld.get("valid"): - ld_txt = f" LD={ld.get('local_dev_p99', 0):.2f}/{ld.get('local_outlier_pct', 0):.1f}%" - if rp.get("valid"): - lines.append( - f"{ch}: {st} | p50={stats.get('p50', stats.get('p50', 0)):.3f} " - f"unif={rp.get('uniformity_delta', 0):.2f} " - f"L/C={rp.get('left_center_ratio', 1):.2f} " - f"R/C={rp.get('right_center_ratio', 1):.2f}{ld_txt}" - ) - else: - lines.append(f"{ch}: {st}{ld_txt}") - - reasons = report.get("reasons", []) or [] - for r in reasons[:max_reasons]: - lines.append(f"! {r}") - - if len(reasons) > max_reasons: - lines.append(f"! +{len(reasons) - max_reasons} avisos") - - return lines - - -# ============================================================ -# Controle de câmera e extração de canais -# ============================================================ - -def get_controls_for_role(cam: MultiSpectralClient, role: str) -> dict: - try: - ctrl = cam.svc.get_camera_controls(role=role) or {} - return { - "ok": True, - "role": role, - "ae_enable": bool(ctrl.get("ae_enable", False)), - "awb_enable": bool(ctrl.get("awb_enable", False)), - "exposure_time_us": safe_int(ctrl.get("exposure_time_us"), None), - "analogue_gain": safe_float(ctrl.get("analogue_gain"), None), - "colour_gains": ctrl.get("colour_gains", None), - "raw": ctrl, - } - except Exception as e: - return { - "ok": False, - "role": role, - "error": str(e), - "ae_enable": None, - "awb_enable": None, - "exposure_time_us": None, - "analogue_gain": None, - "colour_gains": None, - } - - -def get_all_controls(cam: MultiSpectralClient) -> dict: - return {role: get_controls_for_role(cam, role) for role in ("rgb", "re", "nir")} - - -def exposure_gain_factor(ctrl: dict, fallback_exp=1.0, fallback_gain=1.0) -> float: - exp = safe_float(ctrl.get("exposure_time_us"), fallback_exp) - gain = safe_float(ctrl.get("analogue_gain"), fallback_gain) - - if exp is None or exp <= 0: - exp = fallback_exp - if gain is None or gain <= 0: - gain = fallback_gain - - return float(exp * gain) - - -def extract_channels_from_decoded(decoded: dict) -> dict: - """ - Retorna canais em float32 0..1 no espaço nativo de cada câmera: - R/G/B vêm do debayer da role rgb. - RE vem da role re. - NIR vem da role nir. - """ - _, rgb01 = get_image_by_role(decoded, "rgb") - _, re01 = get_image_by_role(decoded, "re") - _, nir01 = get_image_by_role(decoded, "nir") - - def assert_not_raw10_packed_image(role, img, expected_w=1280): - if img is None: - return - - if img.ndim == 2 and img.shape[1] == int(expected_w * 10 / 8): - raise RuntimeError( - f"{role.upper()} parece RAW10_PACKED interpretado como imagem: " - f"shape={img.shape}. Esperado decodificado com largura {expected_w}." - ) - - assert_not_raw10_packed_image("re", re01, expected_w=1280) - assert_not_raw10_packed_image("nir", nir01, expected_w=1280) - - out = {} - - if rgb01 is not None: - rgb01 = rgb01.astype(np.float32) - if rgb01.ndim == 3 and rgb01.shape[2] >= 3: - out["R"] = rgb01[:, :, 0].copy() - out["G"] = rgb01[:, :, 1].copy() - out["B"] = rgb01[:, :, 2].copy() - - if re01 is not None: - out["RE"] = re01.astype(np.float32).copy() - - if nir01 is not None: - out["NIR"] = nir01.astype(np.float32).copy() - - return out - - -def channel_to_role(ch: str) -> str: - ch = ch.upper() - if ch in ("R", "G", "B"): - return "rgb" - if ch == "RE": - return "re" - if ch == "NIR": - return "nir" - raise ValueError(f"Canal desconhecido: {ch}") - - -def apply_exp_gain_correction(channels: dict, controls: dict, enabled: bool) -> dict: - if not enabled: - return {k: v.astype(np.float32).copy() for k, v in channels.items()} - - corrected = {} - for ch, img in channels.items(): - role = channel_to_role(ch) - ctrl = controls.get(role, {}) or {} - factor = exposure_gain_factor(ctrl, fallback_exp=1.0, fallback_gain=1.0) - corrected[ch] = (img.astype(np.float32) / max(factor, 1e-6)).astype(np.float32) - - return corrected - - -# ============================================================ -# UI -# ============================================================ - -def build_board(decoded, controls, state_lines, progress_lines, preview_scale=1.0, quality_lines=None): - rgb_id, rgb01 = get_image_by_role(decoded, "rgb") - re_id, re01 = get_image_by_role(decoded, "re") - nir_id, nir01 = get_image_by_role(decoded, "nir") - - if rgb01 is not None: - rgb_panel = to_bgr_u8_from_rgb01(rgb01) - base_h, base_w = rgb01.shape[:2] - else: - base_h, base_w = 800, 1280 - rgb_panel = np.zeros((base_h, base_w, 3), dtype=np.uint8) - overlay_hud(rgb_panel, ["RGB", "sem frame"]) - - re01 = resize_if_needed(re01, (base_h, base_w)) if re01 is not None else None - nir01 = resize_if_needed(nir01, (base_h, base_w)) if nir01 is not None else None - - re_panel = gray_to_bgr_u8(re01) if re01 is not None else np.zeros_like(rgb_panel) - nir_panel = gray_to_bgr_u8(nir01) if nir01 is not None else np.zeros_like(rgb_panel) - - rgb_stats = compute_image_stats(rgb01[:, :, 1] if rgb01 is not None and rgb01.ndim == 3 else None) - re_stats = compute_image_stats(re01) - nir_stats = compute_image_stats(nir01) - - overlay_hud(rgb_panel, [ - f"RGB ({rgb_id})", - f"p50={rgb_stats['p50']:.3f} p95={rgb_stats['p95']:.3f} sat={rgb_stats['sat_pct']:.2f}%", - f"EXP={controls.get('rgb', {}).get('exposure_time_us')} GAIN={controls.get('rgb', {}).get('analogue_gain')}", - ]) - - overlay_hud(re_panel, [ - f"RE ({re_id})", - f"p50={re_stats['p50']:.3f} p95={re_stats['p95']:.3f} sat={re_stats['sat_pct']:.2f}%", - f"EXP={controls.get('re', {}).get('exposure_time_us')} GAIN={controls.get('re', {}).get('analogue_gain')}", - ]) - - overlay_hud(nir_panel, [ - f"NIR ({nir_id})", - f"p50={nir_stats['p50']:.3f} p95={nir_stats['p95']:.3f} sat={nir_stats['sat_pct']:.2f}%", - f"EXP={controls.get('nir', {}).get('exposure_time_us')} GAIN={controls.get('nir', {}).get('analogue_gain')}", - ]) - - data_panel = np.zeros_like(rgb_panel) - - lines = [] - lines.extend(state_lines) - lines.append("") - lines.extend(progress_lines) - if quality_lines: - lines.append("") - lines.extend(quality_lines) - lines.append("") - lines.extend([ - "ENTER = iniciar etapa atual", - "S = pular etapa dark/preto", - "Q / ESC = sair sem salvar", - "", - "Dica: branco/preto devem preencher todo o campo de visao.", - "Para dark-frame perfeito, tampe as lentes em vez de usar fundo preto.", - ]) - - overlay_hud(data_panel, lines, x=18, y=34, font_scale=0.58, line_step=24) - - top = np.hstack([rgb_panel, re_panel]) - bottom = np.hstack([nir_panel, data_panel]) - board = np.vstack([top, bottom]) - - if preview_scale != 1.0: - board = cv2.resize( - board, - (int(board.shape[1] * preview_scale), int(board.shape[0] * preview_scale)), - interpolation=cv2.INTER_NEAREST, - ) - - return board - - -# ============================================================ -# Captura e processamento -# ============================================================ - -def capture_stage( - cam: MultiSpectralClient, - stage_name: str, - frames_count: int, - discard_frames: int, - exp_gain_correct: bool, - preview_scale: float, - window_name: str, -): - """ - Captura frames decodificados e retorna: - channel_stack: dict canal -> list[np.ndarray] - controls_log: lista dos controles reais lidos por frame - meta_log: lista de metadados básicos por frame - """ - channel_stack = {} - controls_log = [] - meta_log = [] - - total_target = int(frames_count) - captured = 0 - seen_frame_ids = set() - last_decoded = {} - last_controls = {} - - while captured < total_target: - frame, meta, decoded = cam.get_next_decoded(timeout=2.0) - if meta is None or frame is None: - continue - - frame_id = meta.get("frame_id") - if frame_id in seen_frame_ids: - continue - seen_frame_ids.add(frame_id) - - last_decoded = decoded - last_controls = get_all_controls(cam) - - if len(seen_frame_ids) <= discard_frames: - progress_lines = [ - f"Etapa: {stage_name}", - f"Descartando frames iniciais: {len(seen_frame_ids)}/{discard_frames}", - "Aguardando estabilizacao de exposicao/stream...", - ] - board = build_board( - decoded=last_decoded, - controls=last_controls, - state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"], - progress_lines=progress_lines, - preview_scale=preview_scale, - ) - cv2.imshow(window_name, board) - cv2.waitKey(1) - continue - - channels = extract_channels_from_decoded(decoded) - channels = apply_exp_gain_correction(channels, last_controls, enabled=exp_gain_correct) - - for ch, img in channels.items(): - channel_stack.setdefault(ch, []).append(img.astype(np.float32).copy()) - - meta_log.append({ - "frame_id": frame_id, - "sync_ok": meta.get("sync_ok"), - "sync_dt_ms": meta.get("sync_dt_ms"), - "timestamps": meta.get("timestamps"), - }) - controls_log.append(last_controls) - - captured += 1 - - progress_lines = [ - f"Etapa: {stage_name}", - f"Capturando: {captured}/{total_target}", - f"exp_gain_correction={'ON' if exp_gain_correct else 'OFF'}", - ] - - board = build_board( - decoded=last_decoded, - controls=last_controls, - state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"], - progress_lines=progress_lines, - preview_scale=preview_scale, - ) - - # Barra de progresso simples. - h, w = board.shape[:2] - pct = captured / max(total_target, 1) - cv2.rectangle(board, (30, h - 38), (w - 30, h - 18), (80, 80, 80), -1) - cv2.rectangle(board, (30, h - 38), (30 + int((w - 60) * pct), h - 18), (0, 220, 0), -1) - - cv2.imshow(window_name, board) - - k = cv2.waitKey(1) & 0xFF - if k in (ord("q"), ord("Q"), 27): - raise KeyboardInterrupt("Captura cancelada pelo usuario.") - - return channel_stack, controls_log, meta_log - - -def median_stack(channel_stack: dict) -> dict: - med = {} - for ch, frames in channel_stack.items(): - if not frames: - continue - arr = np.stack(frames, axis=0).astype(np.float32) - med[ch] = np.median(arr, axis=0).astype(np.float32) - return med - - -def build_gain_maps( - white_med: dict, - dark_med: dict | None, - epsilon: float, - smooth_ksize: int, - min_gain: float, - max_gain: float, -): - gain_maps = {} - flat_norm_maps = {} - corrected_white = {} - dark_used = {} - - for ch, white in white_med.items(): - white = white.astype(np.float32) - - if dark_med is not None and ch in dark_med: - dark = resize_if_needed(dark_med[ch].astype(np.float32), white.shape[:2]) - else: - dark = np.zeros_like(white, dtype=np.float32) - - signal = white - dark - signal = np.maximum(signal, float(epsilon)).astype(np.float32) - - center = robust_center(signal, low_pct=10.0, high_pct=90.0) - flat_norm = signal / max(center, epsilon) - - gain = center / np.maximum(signal, epsilon) - gain = smooth_map_gain(gain, smooth_ksize) - gain = clip_gain_map(gain, min_gain=min_gain, max_gain=max_gain) - - gain_maps[ch] = gain.astype(np.float32) - flat_norm_maps[ch] = flat_norm.astype(np.float32) - corrected_white[ch] = signal.astype(np.float32) - dark_used[ch] = dark.astype(np.float32) - - return gain_maps, flat_norm_maps, corrected_white, dark_used - - -def save_preview_maps(out_dir: str, gain_maps: dict, corrected_white: dict): - ensure_dir(out_dir) - - for ch, gain in gain_maps.items(): - gain_vis = normalize_for_display(gain) - cv2.imwrite(os.path.join(out_dir, f"gain_{ch}.png"), gray_to_bgr_u8(gain_vis)) - - for ch, white in corrected_white.items(): - white_vis = normalize_for_display(white) - cv2.imwrite(os.path.join(out_dir, f"white_signal_{ch}.png"), gray_to_bgr_u8(white_vis)) - - -def show_final_preview(window_name: str, gain_maps: dict, preview_scale: float, quality_report: dict | None = None): - order = ["R", "G", "B", "RE", "NIR"] - panels = [] - - # RGB e mono podem sair com tamanhos diferentes dependendo do decode/preview. - # Para o painel final, padronizamos tudo para o maior H/W encontrado. - shapes = [gain_maps[ch].shape[:2] for ch in order if ch in gain_maps] - if not shapes: - return - - target_h = max(s[0] for s in shapes) - target_w = max(s[1] for s in shapes) - - def fit_panel(img_bgr: np.ndarray) -> np.ndarray: - if img_bgr.shape[:2] == (target_h, target_w): - return img_bgr - return cv2.resize(img_bgr, (target_w, target_h), interpolation=cv2.INTER_NEAREST) - - for ch in order: - if ch not in gain_maps: - panel = np.zeros((target_h, target_w, 3), dtype=np.uint8) - overlay_hud(panel, [ch, "sem mapa"]) - else: - vis = normalize_for_display(gain_maps[ch]) - panel = gray_to_bgr_u8(vis) - panel = fit_panel(panel) - g = gain_maps[ch] - overlay_hud(panel, [ - f"GAIN MAP {ch}", - f"shape={list(g.shape)}", - f"min={float(np.min(g)):.3f} max={float(np.max(g)):.3f}", - f"mean={float(np.mean(g)):.3f} std={float(np.std(g)):.3f}", - ]) - panels.append(panel) - - blank = np.zeros((target_h, target_w, 3), dtype=np.uint8) - final_lines = [ - "Flat-field salvo com sucesso.", - "ENTER/qualquer tecla = fechar", - "", - ] - final_lines.extend(format_quality_lines(quality_report, title="FINAL QUALITY", max_channels=5, max_reasons=8)) - final_lines.extend([ - "", - "Use estes mapas antes da fusao geometrica.", - "", - "Obs: RGB e mono podem ter shapes diferentes;", - "isso e normal se o decode gerar resolucoes distintas.", - ]) - overlay_hud(blank, final_lines, x=18, y=36) - - top = np.hstack([panels[0], panels[1], panels[2]]) - bottom = np.hstack([panels[3], panels[4], blank]) - board = np.vstack([top, bottom]) - - if preview_scale != 1.0: - board = cv2.resize( - board, - (int(board.shape[1] * preview_scale), int(board.shape[0] * preview_scale)), - interpolation=cv2.INTER_NEAREST, - ) - - cv2.imshow(window_name, board) - cv2.waitKey(0) - - -def wait_for_enter_or_skip( - cam: MultiSpectralClient, - window_name: str, - title: str, - instruction_lines: list[str], - preview_scale: float, - allow_skip=False, - quality_check_white=False, - require_good_quality=False, -): - while True: - frame, meta, decoded = cam.get_next_decoded(timeout=2.0) - if meta is None or frame is None: - continue - - controls = get_all_controls(cam) - - progress_lines = list(instruction_lines) - quality_report = None - quality_lines = None - if quality_check_white: - try: - channels_now = extract_channels_from_decoded(decoded) - quality_report = evaluate_white_frame_quality(channels_now) - quality_lines = format_quality_lines(quality_report, title="WHITE PREFLIGHT") - if require_good_quality and quality_report.get("status") != "good": - progress_lines.append("") - progress_lines.append("ENTER bloqueado: ajuste fundo/luz ate WHITE PREFLIGHT=GOOD.") - else: - progress_lines.append("") - progress_lines.append("ENTER inicia. Use o status abaixo para decidir se esta seguro.") - except Exception as e: - quality_report = {"status": "bad", "reasons": [f"preflight_error:{e}"], "channels": {}} - quality_lines = format_quality_lines(quality_report, title="WHITE PREFLIGHT") - - print(f"QUALITY_LINES: {quality_lines}") - - if allow_skip: - progress_lines.append("") - progress_lines.append("S = pular esta etapa") - - board = build_board( - decoded=decoded, - controls=controls, - state_lines=[title], - progress_lines=progress_lines, - preview_scale=preview_scale, - quality_lines=quality_lines, - ) - cv2.imshow(window_name, board) - - k = cv2.waitKey(1) & 0xFF - if k in (13, 10): - if quality_check_white and require_good_quality and quality_report and quality_report.get("status") != "good": - continue - return "start" - if allow_skip and k in (ord("s"), ord("S")): - return "skip" - if k in (ord("q"), ord("Q"), 27): - raise KeyboardInterrupt("Cancelado pelo usuario.") - - -# ============================================================ -# Main -# ============================================================ - -def main(): - parser = argparse.ArgumentParser( - description="Calibrador automatico de flat-field/dark-frame para o módulo RGB/RE/NIR OAK-FCC-3.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument("--fps", type=int, default=20) - parser.add_argument("--width", type=int, default=1280) - parser.add_argument("--height", type=int, default=800) - parser.add_argument("--bayer", default="BGGR", choices=["GBRG", "GRBG", "RGGB", "BGGR"]) - parser.add_argument("--capture_mode", default="AUTO", choices=["AUTO", "SINGLE", "DOUBLE", "TRIPLE"]) - parser.add_argument("--raw_policy", default="require_triple", choices=["allow_single", "require_triple"]) - parser.add_argument("--module_calibration_json", default="calibration/module_params.json") - - parser.add_argument("--frames", type=int, default=60, help="Frames úteis capturados por etapa.") - parser.add_argument("--discard_frames", type=int, default=15, help="Frames descartados antes de cada etapa.") - parser.add_argument("--preview_scale", type=float, default=0.65) - - parser.add_argument("--out_npz", default="calibration/flatfield_maps_v1.npz") - parser.add_argument("--out_json", default="calibration/flatfield_maps_v1.json") - parser.add_argument("--preview_dir", default="calibration/flatfield_previews") - - parser.add_argument("--smooth_ksize", type=int, default=31, help="Kernel gaussiano para suavizar mapa de ganho. Use 1 para desligar.") - parser.add_argument("--min_gain", type=float, default=0.25) - parser.add_argument("--max_gain", type=float, default=4.0) - parser.add_argument("--epsilon", type=float, default=1e-8) - - parser.add_argument("--exp_gain_correct", action="store_true", - help="Divide frames por exposure_time_us*analogue_gain antes de calcular o flat.") - parser.add_argument("--skip_dark", action="store_true", - help="Pula etapa dark/preto. O mapa será calculado sem subtração dark.") - parser.add_argument("--notes", default="") - parser.add_argument("--require_good_preflight", action="store_true", - help="Bloqueia ENTER na etapa WHITE se o preflight estiver BAD/WARNING.") - - args = parser.parse_args() - - ensure_dir(os.path.dirname(args.out_npz) or ".") - ensure_dir(os.path.dirname(args.out_json) or ".") - ensure_dir(args.preview_dir) - - window_name = "Flat Field Calibration Tool" - cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) - - session_meta = { - "schema": "multispec_flatfield_v1", - "created_at": now_str(), - "sensor_width": args.width, - "sensor_height": args.height, - "bayer_pattern": args.bayer, - "fps": args.fps, - "capture_mode_requested": args.capture_mode, - "raw_policy": args.raw_policy, - "module_calibration_json": args.module_calibration_json, - "frames_per_stage": args.frames, - "discard_frames": args.discard_frames, - "exp_gain_correct": bool(args.exp_gain_correct), - "smooth_ksize": args.smooth_ksize, - "min_gain": args.min_gain, - "max_gain": args.max_gain, - "epsilon": args.epsilon, - "channels": ["R", "G", "B", "RE", "NIR"], - "notes": args.notes, - "require_good_preflight": bool(args.require_good_preflight), - "stages": {}, - "outputs": { - "npz": args.out_npz, - "json": args.out_json, - "preview_dir": args.preview_dir, - }, - } - - try: - with MultiSpectralClient( - width=args.width, - height=args.height, - bayer=args.bayer, - fps=args.fps, - frame_type="RAW_BRUTO", - output_dtype="uint8", - capture_mode=args.capture_mode, - raw_policy=args.raw_policy, - module_calibration_json=args.module_calibration_json or None - ) as cam: - - validate_module_ready(cam.get_status(), args.raw_policy) - - wait_for_enter_or_skip( - cam=cam, - window_name=window_name, - title="ETAPA 1/2 - WHITE / FLAT FIELD", - instruction_lines=[ - "Posicione o modulo na altura real de operacao.", - "Aponte para uma superficie branca/cinza fosca, uniforme e sem textura.", - "Evite reflexos, sombras laterais e saturacao.", - "A superficie deve preencher todo o campo de visao.", - "Pressione ENTER para comecar a captura WHITE.", - ], - preview_scale=args.preview_scale, - allow_skip=False, - quality_check_white=True, - require_good_quality=args.require_good_preflight, - ) - - white_stack, white_controls, white_meta = capture_stage( - cam=cam, - stage_name="white", - frames_count=args.frames, - discard_frames=args.discard_frames, - exp_gain_correct=args.exp_gain_correct, - preview_scale=args.preview_scale, - window_name=window_name, - ) - - session_meta["stages"]["white"] = { - "controls_log": white_controls, - "meta_log": white_meta, - "captured_channels": {ch: len(v) for ch, v in white_stack.items()}, - } - - dark_stack = None - dark_controls = [] - dark_meta = [] - - if not args.skip_dark: - action = wait_for_enter_or_skip( - cam=cam, - window_name=window_name, - title="ETAPA 2/2 - DARK / PRETO", - instruction_lines=[ - "Agora faca a captura dark/preto.", - "Melhor opcao: tampe as lentes completamente.", - "Alternativa: use fundo preto fosco preenchendo todo o frame.", - "Mantenha exposicao/ganho iguais aos da etapa anterior, se possivel.", - "Pressione ENTER para capturar DARK/PRETO.", - ], - preview_scale=args.preview_scale, - allow_skip=True, - ) - - if action == "start": - dark_stack, dark_controls, dark_meta = capture_stage( - cam=cam, - stage_name="dark", - frames_count=args.frames, - discard_frames=args.discard_frames, - exp_gain_correct=args.exp_gain_correct, - preview_scale=args.preview_scale, - window_name=window_name, - ) - - session_meta["stages"]["dark"] = { - "controls_log": dark_controls, - "meta_log": dark_meta, - "captured_channels": {ch: len(v) for ch, v in dark_stack.items()}, - } - else: - session_meta["stages"]["dark"] = {"skipped": True} - else: - session_meta["stages"]["dark"] = {"skipped": True} - - # Processamento robusto. - processing_panel = np.zeros((720, 1280, 3), dtype=np.uint8) - overlay_hud(processing_panel, [ - "Processando calibracao flat-field...", - "Calculando medianas robustas por canal.", - "Gerando mapas de ganho e previews.", - ], x=40, y=80, font_scale=0.8, line_step=34) - cv2.imshow(window_name, processing_panel) - cv2.waitKey(1) - - white_med = median_stack(white_stack) - dark_med = median_stack(dark_stack) if dark_stack is not None else None - - gain_maps, flat_norm_maps, corrected_white, dark_used = build_gain_maps( - white_med=white_med, - dark_med=dark_med, - epsilon=args.epsilon, - smooth_ksize=args.smooth_ksize, - min_gain=args.min_gain, - max_gain=args.max_gain, - ) - - white_median_quality = evaluate_white_frame_quality(white_med) - final_quality_report = evaluate_gain_maps_quality(gain_maps, corrected_white=corrected_white) - session_meta["white_median_quality"] = white_median_quality - session_meta["quality_report"] = final_quality_report - - # Salva .npz. - save_payload = {} - for ch, arr in gain_maps.items(): - save_payload[f"gain_{ch}"] = arr.astype(np.float32) - for ch, arr in flat_norm_maps.items(): - save_payload[f"flat_norm_{ch}"] = arr.astype(np.float32) - for ch, arr in white_med.items(): - save_payload[f"white_median_{ch}"] = arr.astype(np.float32) - if dark_med is not None: - for ch, arr in dark_med.items(): - save_payload[f"dark_median_{ch}"] = arr.astype(np.float32) - - np.savez_compressed(args.out_npz, **save_payload) - - # Estatísticas finais. - session_meta["maps"] = {} - for ch, gain in gain_maps.items(): - session_meta["maps"][ch] = { - "shape": list(gain.shape), - "gain_key": f"gain_{ch}", - "flat_norm_key": f"flat_norm_{ch}", - "white_median_key": f"white_median_{ch}", - "dark_median_key": f"dark_median_{ch}" if dark_med is not None and ch in dark_med else None, - "gain_min": float(np.min(gain)), - "gain_max": float(np.max(gain)), - "gain_mean": float(np.mean(gain)), - "gain_std": float(np.std(gain)), - "white_signal_stats": compute_image_stats(normalize_for_display(corrected_white[ch])), - "quality": (final_quality_report.get("channels", {}).get(ch, {}) if isinstance(final_quality_report, dict) else {}), - } - - save_preview_maps(args.preview_dir, gain_maps, corrected_white) - - with open(args.out_json, "w", encoding="utf-8") as f: - json.dump(session_meta, f, ensure_ascii=False, indent=2) - - show_final_preview(window_name, gain_maps, args.preview_scale, quality_report=final_quality_report) - - print("") - print("[OK] Flat-field gerado com sucesso.") - print(f"[OK] NPZ : {args.out_npz}") - print(f"[OK] JSON: {args.out_json}") - print(f"[OK] PNGs: {args.preview_dir}") - print(f"[QUALITY] status={final_quality_report.get('status')} reasons={final_quality_report.get('reasons', [])[:6]}") - - except KeyboardInterrupt as e: - print(f"[CANCELADO] {e}") - - finally: - cv2.destroyAllWindows() - - -if __name__ == "__main__": - main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/focus_calibration_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/focus_calibration_tool.py deleted file mode 100644 index 4a3a564a0..000000000 --- a/Python/OAK/datasets/oak-fcc-3/utils/focus_calibration_tool.py +++ /dev/null @@ -1,1673 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Focus Calibration Tool - OAK-FFC-3P - Sensor Aware -==================================================== - -Objetivo --------- -Ferramenta standalone para: - - detectar automaticamente os sensores conectados; - - suportar CAM_A com OV9782 (1280x800) OU AR0234 (1920x1200); - - visualizar RGB + RE + NIR simultaneamente; - - medir foco manual por Laplacian / Tenengrad / Brenner; - - permitir ROI por câmera; - - salvar melhores scores e snapshots em JSON/PNG. - -Importante ----------- -Este script NÃO usa o OakFcc3Client e NÃO depende do pipeline RAW_BRUTO. -Para foco óptico ele usa: - - saída ISP da câmera colorida (RGB); - - saída nativa das câmeras mono (RE/NIR). - -Assim, Bayer pattern e normalização do dataset não interferem no teste de foco. - -Configuração padrão esperada: - CAM_A = RGB -> OV9782 ou AR0234 - CAM_B = RE -> OV9282 - CAM_C = NIR -> OV9282 - -Exemplos --------- -# Detecta e abre tudo que estiver disponível: -python focus_calibration_sensor_aware.py - -# Primeiro teste somente da RGB nova: -python focus_calibration_sensor_aware.py --mode rgb - -# Exige as três câmeras: -python focus_calibration_sensor_aware.py --mode triple - -# Se RE e NIR estiverem fisicamente invertidas: -python focus_calibration_sensor_aware.py --re-socket CAM_C --nir-socket CAM_B - -Teclas ------- -1 = RGB -2 = RE -3 = NIR -M = troca métrica -D = informa sentido atual da lente -E = equalização ON/OFF -L = trava/destrava ROI -C = centraliza ROI -R = reseta score da câmera/métrica ativa -S = salva snapshot PNG + registro -SPACE = salva JSON completo -Q / ESC = sair -""" - -import os -import json -import time -import argparse -from dataclasses import dataclass, asdict -from datetime import datetime -from collections import deque -from typing import Dict, Optional, Tuple, List - -import cv2 -import numpy as np -import depthai as dai - - -# ============================================================ -# Helpers gerais -# ============================================================ - -METHODS = ("laplacian", "tenengrad", "brenner") -ROLES = ("rgb", "re", "nir") - - -def now_str() -> str: - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - -def now_file_str() -> str: - return datetime.now().strftime("%Y%m%d_%H%M%S") - - -def ensure_dir(path: str): - if path: - os.makedirs(path, exist_ok=True) - - -def overlay_hud( - img_bgr, - lines, - x=12, - y=24, - font_scale=0.58, - line_step=22, - color=(255, 255, 255), - shadow=True, -): - yy = y - h, _ = img_bgr.shape[:2] - - for s in lines: - if yy > h - 8: - break - - text = str(s) - - if shadow: - cv2.putText( - img_bgr, - text, - (x, yy), - cv2.FONT_HERSHEY_SIMPLEX, - font_scale, - (0, 0, 0), - 3, - cv2.LINE_AA, - ) - - cv2.putText( - img_bgr, - text, - (x, yy), - cv2.FONT_HERSHEY_SIMPLEX, - font_scale, - color, - 1, - cv2.LINE_AA, - ) - - yy += line_step - - -def build_empty_panel(shape_hw: Tuple[int, int], title: str, text="sem frame disponivel"): - h, w = shape_hw - img = np.zeros((h, w, 3), dtype=np.uint8) - overlay_hud( - img, - [title, text], - x=18, - y=44, - font_scale=0.75, - line_step=32, - ) - return img - - -def socket_name(socket) -> str: - name = getattr(socket, "name", None) - if name: - return str(name) - - s = str(socket) - for candidate in ("CAM_A", "CAM_B", "CAM_C", "CAM_D"): - if candidate in s: - return candidate - - return s - - -def get_socket(name: str): - name = str(name).upper().strip() - mapping = { - "CAM_A": dai.CameraBoardSocket.CAM_A, - "CAM_B": dai.CameraBoardSocket.CAM_B, - "CAM_C": dai.CameraBoardSocket.CAM_C, - } - - if hasattr(dai.CameraBoardSocket, "CAM_D"): - mapping["CAM_D"] = dai.CameraBoardSocket.CAM_D - - if name not in mapping: - raise ValueError(f"Socket inválido: {name}. Opções: {sorted(mapping)}") - - return mapping[name] - - -def supported_type_strings(feature) -> List[str]: - values = getattr(feature, "supportedTypes", []) or [] - return [str(v).upper() for v in values] - - -def feature_is_color(feature) -> bool: - types = supported_type_strings(feature) - sensor = str(getattr(feature, "sensorName", "") or "").upper() - - if any("COLOR" in t for t in types): - return True - - if any("MONO" in t for t in types): - return False - - # Fallback conhecido do nosso módulo. - return sensor in {"OV9782", "AR0234"} - - -def feature_is_mono(feature) -> bool: - types = supported_type_strings(feature) - - if any("MONO" in t for t in types): - return True - - if any("COLOR" in t for t in types): - return False - - return not feature_is_color(feature) - - -def bgr_to_gray01(img_bgr: np.ndarray) -> Optional[np.ndarray]: - if img_bgr is None: - return None - - if img_bgr.ndim == 2: - gray = img_bgr - elif img_bgr.ndim == 3 and img_bgr.shape[2] == 1: - gray = img_bgr[:, :, 0] - else: - gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) - - gray = gray.astype(np.float32) / 255.0 - return np.clip(gray, 0.0, 1.0) - - -def resize_panel(img: np.ndarray, target_hw: Tuple[int, int]) -> np.ndarray: - th, tw = target_hw - return cv2.resize(img, (tw, th), interpolation=cv2.INTER_AREA) - - -def colorize_mono_for_view(img_bgr: np.ndarray, role: str) -> np.ndarray: - if img_bgr is None: - return None - - if img_bgr.ndim == 3: - gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) - else: - gray = img_bgr - - z = np.zeros_like(gray) - - role = str(role).lower() - - # Apenas visual. As métricas são calculadas no frame original. - if role == "re": - # Vermelho no BGR. - return np.dstack([z, z, gray]) - - if role == "nir": - # Ciano no BGR. - return np.dstack([gray, gray, z]) - - return cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR) - - -def default_roi_for_shape(shape_hw, frac=0.42): - h, w = shape_hw - rw = max(8, int(w * frac)) - rh = max(8, int(h * frac)) - x0 = (w - rw) // 2 - y0 = (h - rh) // 2 - return (x0, y0, x0 + rw, y0 + rh) - - -def sanitize_roi(rect, shape_hw): - if rect is None: - return None - - h, w = shape_hw - x0, y0, x1, y1 = rect - - x0, x1 = sorted((int(x0), int(x1))) - y0, y1 = sorted((int(y0), int(y1))) - - x0 = max(0, min(w - 1, x0)) - x1 = max(1, min(w, x1)) - y0 = max(0, min(h - 1, y0)) - y1 = max(1, min(h, y1)) - - if x1 - x0 < 4 or y1 - y0 < 4: - return None - - return (x0, y0, x1, y1) - - -def crop_rect(img: np.ndarray, rect): - if img is None or rect is None: - return None - - rect = sanitize_roi(rect, img.shape[:2]) - if rect is None: - return None - - x0, y0, x1, y1 = rect - return img[y0:y1, x0:x1] - - -# ============================================================ -# Descoberta / resolução sensor-aware -# ============================================================ - -@dataclass -class SensorSpec: - role: str - socket_name: str - sensor_name: str - feature_width: int - feature_height: int - is_color: bool - configured_width: int - configured_height: int - resolution_name: str - stream_name: str - source: str - - -def discover_cameras(mx_id: Optional[str]): - device_info = dai.DeviceInfo(mx_id) if mx_id else None - - if device_info is not None: - ctx = dai.Device(device_info) - else: - ctx = dai.Device() - - with ctx as device: - features = list(device.getConnectedCameraFeatures()) - - actual_mx = None - for attr in ("getMxId", "getDeviceId"): - if hasattr(device, attr): - try: - actual_mx = str(getattr(device, attr)()) - if actual_mx: - break - except Exception: - pass - - usb_speed = None - try: - usb_speed = str(device.getUsbSpeed()) - except Exception: - pass - - rows = [] - for f in features: - rows.append({ - "socket_obj": f.socket, - "socket_name": socket_name(f.socket), - "sensor_name": str(getattr(f, "sensorName", "") or ""), - "width": int(getattr(f, "width", 0) or 0), - "height": int(getattr(f, "height", 0) or 0), - "supported_types": supported_type_strings(f), - "is_color": feature_is_color(f), - "is_mono": feature_is_mono(f), - "has_autofocus_ic": int(getattr(f, "hasAutofocusIC", 0) or 0), - }) - - return rows, actual_mx, usb_speed - - -def enum_if_exists(enum_cls, name: str): - return getattr(enum_cls, name, None) - - -def pick_color_resolution(sensor_name: str, width: int, height: int): - """ - Retorna: - (enum_resolution, resolution_name, configured_width, configured_height) - """ - sensor = str(sensor_name or "").upper() - enum_cls = dai.ColorCameraProperties.SensorResolution - - if "AR0234" in sensor: - enum_value = enum_if_exists(enum_cls, "THE_1200_P") - if enum_value is None: - raise RuntimeError( - "Seu depthai não possui ColorCameraProperties.SensorResolution.THE_1200_P. " - "Atualize a biblioteca DepthAI antes de testar a AR0234." - ) - return enum_value, "THE_1200_P", 1920, 1200 - - if "OV9782" in sensor: - enum_value = enum_if_exists(enum_cls, "THE_800_P") - if enum_value is None: - raise RuntimeError("DepthAI sem THE_800_P para ColorCamera.") - return enum_value, "THE_800_P", 1280, 800 - - # Fallback por resolução anunciada pelo próprio sensor. - candidates = [ - ((1920, 1200), "THE_1200_P"), - ((1280, 800), "THE_800_P"), - ((1920, 1080), "THE_1080_P"), - ((1280, 720), "THE_720_P"), - ((3840, 2160), "THE_4_K"), - ] - - for (w, h), enum_name in candidates: - if (width, height) == (w, h): - enum_value = enum_if_exists(enum_cls, enum_name) - if enum_value is not None: - return enum_value, enum_name, w, h - - raise RuntimeError( - f"Sensor colorido não mapeado: {sensor_name} ({width}x{height}). " - "Adicione o modo em pick_color_resolution()." - ) - - -def pick_mono_resolution(sensor_name: str, width: int, height: int): - sensor = str(sensor_name or "").upper() - enum_cls = dai.MonoCameraProperties.SensorResolution - - if "OV9282" in sensor or (width, height) == (1280, 800): - enum_value = enum_if_exists(enum_cls, "THE_800_P") - if enum_value is None: - raise RuntimeError("DepthAI sem THE_800_P para MonoCamera.") - return enum_value, "THE_800_P", 1280, 800 - - candidates = [ - ((1280, 800), "THE_800_P"), - ((1280, 720), "THE_720_P"), - ((640, 480), "THE_480_P"), - ((640, 400), "THE_400_P"), - ] - - for (w, h), enum_name in candidates: - if (width, height) == (w, h): - enum_value = enum_if_exists(enum_cls, enum_name) - if enum_value is not None: - return enum_value, enum_name, w, h - - raise RuntimeError( - f"Sensor mono não mapeado: {sensor_name} ({width}x{height}). " - "Adicione o modo em pick_mono_resolution()." - ) - - -def build_specs(camera_rows, args) -> Dict[str, SensorSpec]: - by_socket = {row["socket_name"]: row for row in camera_rows} - - role_socket = { - "rgb": args.rgb_socket.upper(), - "re": args.re_socket.upper(), - "nir": args.nir_socket.upper(), - } - - if args.mode == "rgb": - requested_roles = ["rgb"] - else: - requested_roles = ["rgb", "re", "nir"] - - specs = {} - - for role in requested_roles: - sock_name = role_socket[role] - row = by_socket.get(sock_name) - - if row is None: - if args.mode == "triple": - raise RuntimeError( - f"Modo triple exige {role.upper()} em {sock_name}, " - f"mas esse socket não foi detectado." - ) - continue - - if role == "rgb": - if not row["is_color"]: - raise RuntimeError( - f"{sock_name} foi escolhido como RGB, mas o sensor detectado " - f"({row['sensor_name']}) não foi anunciado como COLOR." - ) - - _, res_name, cw, ch = pick_color_resolution( - row["sensor_name"], row["width"], row["height"] - ) - - specs[role] = SensorSpec( - role=role, - socket_name=sock_name, - sensor_name=row["sensor_name"], - feature_width=row["width"], - feature_height=row["height"], - is_color=True, - configured_width=cw, - configured_height=ch, - resolution_name=res_name, - stream_name="focus_rgb", - source="ISP", - ) - - else: - if not row["is_mono"]: - raise RuntimeError( - f"{sock_name} foi escolhido como {role.upper()}, mas o sensor detectado " - f"({row['sensor_name']}) não foi anunciado como MONO." - ) - - _, res_name, cw, ch = pick_mono_resolution( - row["sensor_name"], row["width"], row["height"] - ) - - specs[role] = SensorSpec( - role=role, - socket_name=sock_name, - sensor_name=row["sensor_name"], - feature_width=row["width"], - feature_height=row["height"], - is_color=False, - configured_width=cw, - configured_height=ch, - resolution_name=res_name, - stream_name=f"focus_{role}", - source="MONO_OUT", - ) - - if "rgb" not in specs: - raise RuntimeError( - f"RGB não encontrada em {args.rgb_socket}. " - "Confira os flats e/ou use --rgb-socket." - ) - - if args.mode == "triple": - missing = [r for r in ROLES if r not in specs] - if missing: - raise RuntimeError(f"Modo triple: faltando roles {missing}") - - return specs - - -def build_pipeline(specs: Dict[str, SensorSpec], fps: float): - pipeline = dai.Pipeline() - - for role, spec in specs.items(): - socket = get_socket(spec.socket_name) - - if spec.is_color: - cam = pipeline.createColorCamera() - cam.setBoardSocket(socket) - - enum_value, _, _, _ = pick_color_resolution( - spec.sensor_name, - spec.feature_width, - spec.feature_height, - ) - - cam.setResolution(enum_value) - cam.setFps(float(fps)) - cam.setInterleaved(False) - - try: - cam.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR) - except Exception: - pass - - xout = pipeline.createXLinkOut() - xout.setStreamName(spec.stream_name) - - # ISP mantém a resolução útil do sensor e getCvFrame() entrega - # uma imagem pronta para inspeção, sem depender do nosso Bayer. - cam.isp.link(xout.input) - - else: - cam = pipeline.createMonoCamera() - cam.setBoardSocket(socket) - - enum_value, _, _, _ = pick_mono_resolution( - spec.sensor_name, - spec.feature_width, - spec.feature_height, - ) - - cam.setResolution(enum_value) - cam.setFps(float(fps)) - - xout = pipeline.createXLinkOut() - xout.setStreamName(spec.stream_name) - cam.out.link(xout.input) - - return pipeline - - -# ============================================================ -# Métricas de foco -# ============================================================ - -def preprocess_focus_gray(gray01: np.ndarray, equalize=False): - g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8) - - if equalize: - g = cv2.equalizeHist(g) - - return g - - -def focus_laplacian_var(gray_u8: np.ndarray) -> float: - lap = cv2.Laplacian(gray_u8, cv2.CV_64F, ksize=3) - return float(lap.var()) - - -def focus_tenengrad(gray_u8: np.ndarray) -> float: - sx = cv2.Sobel(gray_u8, cv2.CV_64F, 1, 0, ksize=3) - sy = cv2.Sobel(gray_u8, cv2.CV_64F, 0, 1, ksize=3) - return float(np.mean(sx * sx + sy * sy)) - - -def focus_brenner(gray_u8: np.ndarray) -> float: - arr = gray_u8.astype(np.float32) - - if arr.shape[1] < 3: - return 0.0 - - diff = arr[:, 2:] - arr[:, :-2] - return float(np.mean(diff * diff)) - - -def compute_focus_metrics(img_bgr: np.ndarray, roi_rect, equalize=False): - gray01 = bgr_to_gray01(img_bgr) - roi = crop_rect(gray01, roi_rect) - - if roi is None or roi.size < 64: - return { - "valid": False, - "laplacian": 0.0, - "tenengrad": 0.0, - "brenner": 0.0, - "mean": 0.0, - "std": 0.0, - "p95": 0.0, - "pct_saturated": 0.0, - "pct_dark": 0.0, - "pixels": 0, - } - - gray_u8 = preprocess_focus_gray(roi, equalize=equalize) - arr = roi.astype(np.float32).reshape(-1) - - return { - "valid": True, - "laplacian": focus_laplacian_var(gray_u8), - "tenengrad": focus_tenengrad(gray_u8), - "brenner": focus_brenner(gray_u8), - "mean": float(arr.mean()), - "std": float(arr.std()), - "p95": float(np.percentile(arr, 95)), - "pct_saturated": float((arr >= 0.98).mean() * 100.0), - "pct_dark": float((arr <= 0.02).mean() * 100.0), - "pixels": int(arr.size), - } - - -def metric_value(metrics: dict, method: str) -> float: - return float(metrics.get(method, 0.0) or 0.0) - - -def smooth_from_history(history, window: int) -> float: - if not history: - return 0.0 - - vals = [float(x["score"]) for x in list(history)[-max(1, window):]] - return float(np.mean(vals)) - - -def analyze_trend(history, best_score, direction_name: str, drop_warn_pct=3.0): - if len(history) < 6: - return { - "status": "coletando", - "instruction": "gire devagar e observe o grafico", - "delta": 0.0, - "pct_of_best": 0.0, - } - - recent = [float(x["smooth"]) for x in list(history)[-5:]] - - if len(history) >= 12: - old = [float(x["smooth"]) for x in list(history)[-12:-7]] - else: - old = [float(x["smooth"]) for x in list(history)[:5]] - - recent_mean = float(np.mean(recent)) - old_mean = float(np.mean(old)) - delta = recent_mean - old_mean - - pct_of_best = 0.0 if best_score <= 0 else (recent_mean / best_score) * 100.0 - drop_from_best = 100.0 - pct_of_best - - if best_score > 0 and drop_from_best >= drop_warn_pct: - return { - "status": "passou_do_pico", - "instruction": f"volte um pouco: contrario de {direction_name}", - "delta": delta, - "pct_of_best": pct_of_best, - } - - eps = max(best_score * 0.002, 1e-6) - - if delta > eps: - return { - "status": "melhorando", - "instruction": f"continue {direction_name}", - "delta": delta, - "pct_of_best": pct_of_best, - } - - if delta < -eps: - return { - "status": "piorando", - "instruction": f"inverta: contrario de {direction_name}", - "delta": delta, - "pct_of_best": pct_of_best, - } - - return { - "status": "estavel", - "instruction": "ajuste fino ou trave a lente", - "delta": delta, - "pct_of_best": pct_of_best, - } - - -# ============================================================ -# Desenho -# ============================================================ - -def draw_roi(panel, roi_src, src_shape_hw, active=False): - if roi_src is None: - return - - ph, pw = panel.shape[:2] - sh, sw = src_shape_hw - - x0, y0, x1, y1 = roi_src - - px0 = int(x0 * pw / max(1, sw)) - px1 = int(x1 * pw / max(1, sw)) - py0 = int(y0 * ph / max(1, sh)) - py1 = int(y1 * ph / max(1, sh)) - - color = (0, 255, 255) if active else (0, 180, 255) - - cv2.rectangle(panel, (px0, py0), (px1, py1), color, 2) - cv2.putText( - panel, - "FOCUS ROI", - (px0 + 6, max(20, py0 - 8)), - cv2.FONT_HERSHEY_SIMPLEX, - 0.55, - color, - 2, - cv2.LINE_AA, - ) - - -def draw_crosshair(panel): - h, w = panel.shape[:2] - - cv2.line( - panel, - (w // 2 - 18, h // 2), - (w // 2 + 18, h // 2), - (255, 255, 255), - 1, - cv2.LINE_AA, - ) - - cv2.line( - panel, - (w // 2, h // 2 - 18), - (w // 2, h // 2 + 18), - (255, 255, 255), - 1, - cv2.LINE_AA, - ) - - -def draw_panel_title(panel, title, selected=False): - color = (0, 255, 255) if selected else (255, 255, 255) - overlay_hud( - panel, - [title], - x=12, - y=24, - font_scale=0.60, - line_step=24, - color=color, - ) - - -def draw_score_bar(panel, pct, x, y, w, h, label): - pct = float(max(0.0, min(100.0, pct))) - - cv2.rectangle(panel, (x, y), (x + w, y + h), (80, 80, 80), 1) - - fill_w = int((pct / 100.0) * w) - cv2.rectangle(panel, (x, y), (x + fill_w, y + h), (230, 230, 230), -1) - - cv2.rectangle(panel, (x, y), (x + w, y + h), (180, 180, 180), 1) - - cv2.putText( - panel, - f"{label}: {pct:5.1f}%", - (x, y - 8), - cv2.FONT_HERSHEY_SIMPLEX, - 0.50, - (255, 255, 255), - 1, - cv2.LINE_AA, - ) - - -def draw_history_graph(panel, history, x, y, w, h, best_score): - cv2.rectangle(panel, (x, y), (x + w, y + h), (35, 35, 35), -1) - cv2.rectangle(panel, (x, y), (x + w, y + h), (120, 120, 120), 1) - - if len(history) < 2: - cv2.putText( - panel, - "grafico aguardando historico...", - (x + 10, y + h // 2), - cv2.FONT_HERSHEY_SIMPLEX, - 0.50, - (180, 180, 180), - 1, - cv2.LINE_AA, - ) - return - - vals = np.array([float(item["smooth"]) for item in history], dtype=np.float32) - vals = vals[-max(2, w):] - - max_val = max(float(np.max(vals)), float(best_score), 1e-6) - min_val = min(float(np.min(vals)), max_val * 0.90) - span = max(max_val - min_val, 1e-6) - - pts = [] - - for i, value in enumerate(vals): - px = x + int((i / max(1, len(vals) - 1)) * (w - 1)) - py = y + h - 1 - int(((float(value) - min_val) / span) * (h - 1)) - pts.append((px, py)) - - for p0, p1 in zip(pts[:-1], pts[1:]): - cv2.line(panel, p0, p1, (255, 255, 255), 2, cv2.LINE_AA) - - if best_score > 0: - by = y + h - 1 - int(((best_score - min_val) / span) * (h - 1)) - by = max(y, min(y + h - 1, by)) - - cv2.line(panel, (x, by), (x + w, by), (0, 255, 255), 1, cv2.LINE_AA) - - cv2.putText( - panel, - "best", - (x + 6, max(y + 16, by - 4)), - cv2.FONT_HERSHEY_SIMPLEX, - 0.43, - (0, 255, 255), - 1, - cv2.LINE_AA, - ) - - -def make_data_panel( - shape_hw, - spec: SensorSpec, - selected_role, - method, - metrics, - score, - smooth, - best, - trend, - fps_by_role, - fps_view, - direction_name, - history, - roi_locked, - equalize, -): - h, w = shape_hw - panel = np.zeros((h, w, 3), dtype=np.uint8) - - best_score = float(best.get("smooth", 0.0) or 0.0) - score_pct = 0.0 if best_score <= 0 else (smooth / best_score) * 100.0 - - lines = [ - "FOCUS CALIBRATION - SENSOR AWARE", - f"ativa={selected_role.upper()} | metodo={method}", - f"sensor={spec.sensor_name} | socket={spec.socket_name}", - f"modo={spec.resolution_name} | {spec.configured_width}x{spec.configured_height}", - f"fonte={spec.source}", - f"score={score:.1f} | smooth={smooth:.1f}", - f"best={best_score:.1f} | atual/best={score_pct:.1f}%", - f"status={trend.get('status', 'coletando')}", - f"acao={trend.get('instruction', 'gire devagar')}", - f"sentido={direction_name}", - f"fps RGB/RE/NIR={fps_by_role.get('rgb', 0):.1f}/{fps_by_role.get('re', 0):.1f}/{fps_by_role.get('nir', 0):.1f}", - f"fps_view={fps_view:.1f}", - f"ROI={'travada' if roi_locked else 'editavel'} | equalize={'ON' if equalize else 'OFF'}", - ] - - if metrics and metrics.get("valid"): - lines.extend([ - "-", - f"mean={metrics['mean']:.3f} std={metrics['std']:.3f} p95={metrics['p95']:.3f}", - f"sat={metrics['pct_saturated']:.2f}% dark={metrics['pct_dark']:.2f}%", - ]) - - overlay_hud( - panel, - lines, - x=14, - y=26, - font_scale=0.48, - line_step=20, - ) - - bar_y = min(h - 165, 315) - bar_y = max(255, bar_y) - - draw_score_bar( - panel, - min(100.0, score_pct), - 18, - bar_y, - max(80, w - 36), - 22, - "nitidez relativa", - ) - - graph_y = bar_y + 46 - graph_h = max(65, h - graph_y - 62) - - draw_history_graph( - panel, - history, - 18, - graph_y, - max(100, w - 36), - graph_h, - best_score, - ) - - help_lines = [ - "1 RGB | 2 RE | 3 NIR | M metrica | D sentido", - "mouse ROI | C centraliza | L trava | E equalize | R reset", - "S snapshot PNG | SPACE salva JSON | Q sai", - ] - - overlay_hud( - panel, - help_lines, - x=14, - y=h - 48, - font_scale=0.40, - line_step=16, - ) - - return panel - - -# ============================================================ -# Estado / persistência -# ============================================================ - -def empty_best(): - return { - "score": 0.0, - "smooth": 0.0, - "metrics": None, - "timestamp": None, - "roi": None, - } - - -def json_safe_best(best): - return { - "score": float(best.get("score", 0.0) or 0.0), - "smooth": float(best.get("smooth", 0.0) or 0.0), - "metrics": best.get("metrics"), - "timestamp": best.get("timestamp"), - "roi": best.get("roi"), - } - - -def build_result_payload( - args, - camera_rows, - specs, - best_by_role_method, - snapshots, - actual_mx, - usb_speed, -): - results = {} - - for role in ROLES: - results[role] = {} - for method in METHODS: - results[role][method] = json_safe_best(best_by_role_method[role][method]) - - return { - "schema": "oak_ffc_focus_calibration_sensor_aware_v2", - "saved_at": now_str(), - "depthai_version": getattr(dai, "__version__", "unknown"), - "device_mx_id": actual_mx, - "usb_speed": usb_speed, - "mode": args.mode, - "fps_requested": args.fps, - "role_socket_map": { - "rgb": args.rgb_socket, - "re": args.re_socket, - "nir": args.nir_socket, - }, - "camera_inventory": [ - {k: v for k, v in row.items() if k != "socket_obj"} - for row in camera_rows - ], - "active_specs": { - role: asdict(spec) - for role, spec in specs.items() - }, - "results_by_role_and_method": results, - "snapshots": snapshots, - "notes": args.notes or "", - } - - -def save_json(path, payload): - ensure_dir(os.path.dirname(path) or ".") - - with open(path, "w", encoding="utf-8") as f: - json.dump(payload, f, ensure_ascii=False, indent=2) - - -# ============================================================ -# Main -# ============================================================ - -def main(): - parser = argparse.ArgumentParser( - description=( - "Focus Calibration Tool standalone para OAK-FFC-3P, " - "sensor-aware para OV9782 e AR0234." - ), - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument( - "--mode", - default="auto", - choices=["auto", "rgb", "triple"], - help="auto abre o que existir; rgb abre só CAM_A/RGB; triple exige RGB+RE+NIR", - ) - - parser.add_argument("--fps", type=float, default=20.0) - parser.add_argument("--mx-id", default=None) - - parser.add_argument("--rgb-socket", default="CAM_A") - parser.add_argument("--re-socket", default="CAM_B") - parser.add_argument("--nir-socket", default="CAM_C") - - parser.add_argument( - "--panel-width", - type=int, - default=640, - help="Largura visual de cada quadrante. Métrica continua na resolução nativa.", - ) - parser.add_argument( - "--panel-height", - type=int, - default=400, - help="Altura visual de cada quadrante.", - ) - - parser.add_argument( - "--method", - default="laplacian", - choices=list(METHODS), - ) - parser.add_argument("--history", type=int, default=260) - parser.add_argument("--smooth-window", type=int, default=5) - parser.add_argument("--drop-warn-pct", type=float, default=3.0) - parser.add_argument("--equalize", action="store_true") - - parser.add_argument( - "--out-json", - default="calibration/focus_calibration_sensor_aware.json", - ) - parser.add_argument( - "--snapshot-dir", - default="calibration/focus_snapshots", - ) - parser.add_argument("--notes", default="") - - args = parser.parse_args() - - # -------------------------------------------------------- - # Descoberta - # -------------------------------------------------------- - - camera_rows, actual_mx, usb_speed = discover_cameras(args.mx_id) - - print("=" * 74) - print("FOCUS CALIBRATION TOOL - SENSOR AWARE") - print(f"DepthAI : {getattr(dai, '__version__', 'unknown')}") - print(f"MX ID : {actual_mx}") - print(f"USB : {usb_speed}") - print("-" * 74) - - for row in camera_rows: - print( - f"{row['socket_name']:5s} | " - f"{row['sensor_name']:12s} | " - f"{row['width']}x{row['height']} | " - f"types={row['supported_types']} | " - f"AF_IC={row['has_autofocus_ic']}" - ) - - specs = build_specs(camera_rows, args) - - print("-" * 74) - - for role, spec in specs.items(): - print( - f"[PIPE] {role.upper():3s} <- {spec.socket_name} " - f"{spec.sensor_name} | {spec.resolution_name} " - f"{spec.configured_width}x{spec.configured_height} | {spec.source}" - ) - - print("=" * 74) - - # -------------------------------------------------------- - # Pipeline - # -------------------------------------------------------- - - pipeline = build_pipeline(specs, args.fps) - - device_info = dai.DeviceInfo(actual_mx) if actual_mx else None - - if device_info is not None: - device_ctx = dai.Device(pipeline, device_info) - else: - device_ctx = dai.Device(pipeline) - - # -------------------------------------------------------- - # Estado da UI - # -------------------------------------------------------- - - selected_role = "rgb" - method = args.method - equalize = bool(args.equalize) - - direction_idx = 0 - direction_names = ["rosqueando", "desrosqueando"] - - frames: Dict[str, Optional[np.ndarray]] = {role: None for role in ROLES} - roi_rects = {role: None for role in ROLES} - - history_by_role_method = { - role: { - m: deque(maxlen=args.history) - for m in METHODS - } - for role in ROLES - } - - best_by_role_method = { - role: { - m: empty_best() - for m in METHODS - } - for role in ROLES - } - - snapshots = [] - - fps_by_role = {role: 0.0 for role in ROLES} - fps_count = {role: 0 for role in ROLES} - fps_t0 = {role: time.time() for role in ROLES} - - fps_view = 0.0 - view_count = 0 - view_t0 = time.time() - - roi_locked = False - dragging_roi = False - drag_start_src = None - - panel_rects = { - "rgb": None, - "re": None, - "nir": None, - "data": None, - } - - last_board = None - last_msg = "" - last_msg_t = 0.0 - - window_name = "Focus Calibration Tool - Sensor Aware" - cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) - - def set_msg(text): - nonlocal last_msg, last_msg_t - last_msg = str(text) - last_msg_t = time.time() - - def inside(rect, px, py): - if rect is None: - return False - - x0, y0, x1, y1 = rect - return x0 <= px < x1 and y0 <= py < y1 - - def display_to_source(role, px, py): - rect = panel_rects.get(role) - img = frames.get(role) - - if rect is None or img is None: - return None - - x0, y0, x1, y1 = rect - pw = max(1, x1 - x0) - ph = max(1, y1 - y0) - - sh, sw = img.shape[:2] - - lx = max(0, min(pw - 1, int(px - x0))) - ly = max(0, min(ph - 1, int(py - y0))) - - sx = int(lx * sw / pw) - sy = int(ly * sh / ph) - - sx = max(0, min(sw - 1, sx)) - sy = max(0, min(sh - 1, sy)) - - return sx, sy - - def on_mouse(event, x, y, flags, param): - nonlocal dragging_roi, drag_start_src - - if roi_locked: - return - - rect = panel_rects.get(selected_role) - - if rect is None or not inside(rect, x, y): - return - - src_pt = display_to_source(selected_role, x, y) - - if src_pt is None: - return - - if event == cv2.EVENT_LBUTTONDOWN: - dragging_roi = True - drag_start_src = src_pt - - sx, sy = src_pt - roi_rects[selected_role] = (sx, sy, sx + 1, sy + 1) - - elif event == cv2.EVENT_MOUSEMOVE and dragging_roi and drag_start_src: - x0, y0 = drag_start_src - x1, y1 = src_pt - roi_rects[selected_role] = (x0, y0, x1, y1) - - elif event == cv2.EVENT_LBUTTONUP and dragging_roi and drag_start_src: - x0, y0 = drag_start_src - x1, y1 = src_pt - - rect_src = sanitize_roi( - (x0, y0, x1, y1), - frames[selected_role].shape[:2], - ) - - if rect_src is not None: - roi_rects[selected_role] = rect_src - set_msg(f"ROI atualizada: {selected_role.upper()}") - - dragging_roi = False - drag_start_src = None - - cv2.setMouseCallback(window_name, on_mouse) - - ensure_dir(args.snapshot_dir) - - # -------------------------------------------------------- - # Execução - # -------------------------------------------------------- - - try: - with device_ctx as device: - queues = { - role: device.getOutputQueue( - name=spec.stream_name, - maxSize=2, - blocking=False, - ) - for role, spec in specs.items() - } - - print("[OK] Pipeline iniciado. Ajuste a lente devagar.") - print("[OK] Q/ESC sai | 1/2/3 troca câmera | S salva snapshot") - - while True: - # ------------------------------------------------ - # Coleta independente: foco não precisa sync rígido. - # ------------------------------------------------ - for role, queue in queues.items(): - packet = queue.tryGet() - - if packet is None: - continue - - try: - img = packet.getCvFrame() - except Exception as exc: - set_msg(f"Falha getCvFrame {role}: {exc}") - continue - - if img is None: - continue - - if img.ndim == 2: - img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) - - frames[role] = img - - if roi_rects[role] is None: - roi_rects[role] = default_roi_for_shape(img.shape[:2], frac=0.42) - - fps_count[role] += 1 - dt = time.time() - fps_t0[role] - - if dt >= 1.0: - fps_by_role[role] = fps_count[role] / dt - fps_count[role] = 0 - fps_t0[role] = time.time() - - # ------------------------------------------------ - # Se ativa não existe, cai para a primeira disponível. - # ------------------------------------------------ - if selected_role not in specs or frames.get(selected_role) is None: - available = [ - r for r in ROLES - if r in specs and frames.get(r) is not None - ] - - if available: - selected_role = available[0] - - active_img = frames.get(selected_role) - - # ------------------------------------------------ - # Métrica - # ------------------------------------------------ - metrics = None - score = 0.0 - smooth = 0.0 - trend = { - "status": "aguardando", - "instruction": "aguardando frame", - "pct_of_best": 0.0, - } - - history = history_by_role_method[selected_role][method] - best = best_by_role_method[selected_role][method] - - if active_img is not None: - if roi_rects[selected_role] is None: - roi_rects[selected_role] = default_roi_for_shape( - active_img.shape[:2], - frac=0.42, - ) - - metrics = compute_focus_metrics( - active_img, - roi_rects[selected_role], - equalize=equalize, - ) - - if metrics.get("valid"): - score = metric_value(metrics, method) - - history.append({ - "t": time.time(), - "score": score, - "smooth": score, - }) - - smooth = smooth_from_history( - history, - args.smooth_window, - ) - - history[-1]["smooth"] = smooth - - if smooth > float(best.get("smooth", 0.0) or 0.0): - best.update({ - "score": float(score), - "smooth": float(smooth), - "metrics": metrics, - "timestamp": now_str(), - "roi": list(map(int, roi_rects[selected_role])), - }) - - trend = analyze_trend( - history, - float(best.get("smooth", 0.0) or 0.0), - direction_names[direction_idx], - drop_warn_pct=args.drop_warn_pct, - ) - - # ------------------------------------------------ - # Painéis - # ------------------------------------------------ - ph = int(args.panel_height) - pw = int(args.panel_width) - target_hw = (ph, pw) - - panels = {} - - for role in ROLES: - img = frames.get(role) - spec = specs.get(role) - - if img is None: - if spec is None: - panels[role] = build_empty_panel( - target_hw, - role.upper(), - "camera nao ativa neste modo", - ) - else: - panels[role] = build_empty_panel( - target_hw, - role.upper(), - f"aguardando {spec.sensor_name}", - ) - continue - - view = img.copy() - - if role in ("re", "nir"): - view = colorize_mono_for_view(view, role) - - panel = resize_panel(view, target_hw) - - roi = roi_rects.get(role) - if roi is not None: - draw_roi( - panel, - roi, - img.shape[:2], - active=(role == selected_role), - ) - - draw_crosshair(panel) - - spec = specs[role] - title = ( - f"{role.upper()} | {spec.socket_name} | " - f"{spec.sensor_name} | {img.shape[1]}x{img.shape[0]}" - ) - - draw_panel_title( - panel, - title, - selected=(role == selected_role), - ) - - panels[role] = panel - - active_spec = specs.get(selected_role) - - if active_spec is None: - # Fallback apenas defensivo. - active_spec = next(iter(specs.values())) - - data_panel = make_data_panel( - target_hw, - active_spec, - selected_role, - method, - metrics or {}, - score, - smooth, - best, - trend, - fps_by_role, - fps_view, - direction_names[direction_idx], - history, - roi_locked, - equalize, - ) - - # Layout: - # RGB | RE - # NIR | DATA - panel_rects["rgb"] = (0, 0, pw, ph) - panel_rects["re"] = (pw, 0, pw * 2, ph) - panel_rects["nir"] = (0, ph, pw, ph * 2) - panel_rects["data"] = (pw, ph, pw * 2, ph * 2) - - top = np.hstack([panels["rgb"], panels["re"]]) - bottom = np.hstack([panels["nir"], data_panel]) - board = np.vstack([top, bottom]) - - if last_msg and (time.time() - last_msg_t) < 2.5: - cv2.putText( - board, - last_msg, - (16, board.shape[0] - 14), - cv2.FONT_HERSHEY_SIMPLEX, - 0.58, - (0, 255, 0), - 2, - cv2.LINE_AA, - ) - - last_board = board.copy() - - cv2.imshow(window_name, board) - - # FPS da UI - view_count += 1 - dt_view = time.time() - view_t0 - - if dt_view >= 1.0: - fps_view = view_count / dt_view - view_count = 0 - view_t0 = time.time() - - # ------------------------------------------------ - # Teclas - # ------------------------------------------------ - k = cv2.waitKey(1) & 0xFF - - if k in (ord("q"), ord("Q"), 27): - break - - elif k == ord("1"): - if "rgb" in specs: - selected_role = "rgb" - set_msg("Selecionada: RGB") - - elif k == ord("2"): - if "re" in specs: - selected_role = "re" - set_msg("Selecionada: RE") - else: - set_msg("RE nao ativa neste modo") - - elif k == ord("3"): - if "nir" in specs: - selected_role = "nir" - set_msg("Selecionada: NIR") - else: - set_msg("NIR nao ativa neste modo") - - elif k in (ord("m"), ord("M")): - method = METHODS[(METHODS.index(method) + 1) % len(METHODS)] - set_msg(f"Metrica -> {method}") - - elif k in (ord("d"), ord("D")): - direction_idx = 1 - direction_idx - set_msg(f"Sentido -> {direction_names[direction_idx]}") - - elif k in (ord("e"), ord("E")): - equalize = not equalize - set_msg(f"Equalize -> {'ON' if equalize else 'OFF'}") - - elif k in (ord("l"), ord("L")): - roi_locked = not roi_locked - set_msg(f"ROI -> {'travada' if roi_locked else 'editavel'}") - - elif k in (ord("c"), ord("C")): - img = frames.get(selected_role) - - if img is not None: - roi_rects[selected_role] = default_roi_for_shape( - img.shape[:2], - frac=0.42, - ) - set_msg(f"ROI centralizada: {selected_role.upper()}") - - elif k in (ord("r"), ord("R")): - history_by_role_method[selected_role][method].clear() - best_by_role_method[selected_role][method] = empty_best() - - set_msg( - f"Reset: {selected_role.upper()} / {method}" - ) - - elif k in (ord("s"), ord("S")): - stamp = now_file_str() - - png_path = os.path.join( - args.snapshot_dir, - f"focus_{selected_role}_{method}_{stamp}.png", - ) - - if last_board is not None: - cv2.imwrite(png_path, last_board) - - snap = { - "timestamp": now_str(), - "role": selected_role, - "method": method, - "sensor": asdict(specs[selected_role]), - "roi": ( - list(map(int, roi_rects[selected_role])) - if roi_rects[selected_role] is not None - else None - ), - "current_score": float(score), - "current_smooth": float(smooth), - "best": json_safe_best( - best_by_role_method[selected_role][method] - ), - "direction_name": direction_names[direction_idx], - "equalize": bool(equalize), - "png_path": png_path, - } - - snapshots.append(snap) - set_msg(f"Snapshot -> {png_path}") - - elif k == 32: - payload = build_result_payload( - args, - camera_rows, - specs, - best_by_role_method, - snapshots, - actual_mx, - usb_speed, - ) - - save_json(args.out_json, payload) - set_msg(f"JSON salvo -> {args.out_json}") - - finally: - cv2.destroyAllWindows() - - # Salva automaticamente no fechamento também. - try: - payload = build_result_payload( - args, - camera_rows, - specs, - best_by_role_method, - snapshots, - actual_mx, - usb_speed, - ) - save_json(args.out_json, payload) - print(f"[OK] Resultado final salvo em: {args.out_json}") - except Exception as exc: - print(f"[WARN] Não foi possível salvar resultado final: {exc}") - - print("Fim da calibração de foco.") - - -if __name__ == "__main__": - main() diff --git a/Python/OAK/datasets/oak-fcc-3/utils/radiometric_config_tool.py b/Python/OAK/datasets/oak-fcc-3/utils/radiometric_config_tool.py deleted file mode 100644 index 25e88dc64..000000000 --- a/Python/OAK/datasets/oak-fcc-3/utils/radiometric_config_tool.py +++ /dev/null @@ -1,1617 +0,0 @@ -import os -import json -import time -import argparse -from datetime import datetime - -import cv2 -import numpy as np - -from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient - - -# ============================================================ -# Helpers gerais -# ============================================================ - -def now_str() -> str: - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - -def ensure_dir(path: str): - if path: - os.makedirs(path, exist_ok=True) - - -def clamp(v, lo, hi): - return max(lo, min(hi, v)) - - -def overlay_hud( - img_bgr, - lines, - x=12, - y=24, - font_scale=0.58, - line_step=22, - color=(255, 255, 255), - shadow=(0, 0, 0), -): - yy = int(y) - for s in lines: - cv2.putText(img_bgr, str(s), (int(x), yy), cv2.FONT_HERSHEY_SIMPLEX, - font_scale, shadow, 3, cv2.LINE_AA) - cv2.putText(img_bgr, str(s), (int(x), yy), cv2.FONT_HERSHEY_SIMPLEX, - font_scale, color, 1, cv2.LINE_AA) - yy += int(line_step) - - -def to_bgr_u8_from_rgb01(rgb01: np.ndarray) -> np.ndarray: - rgb_u8 = np.clip(rgb01 * 255.0, 0, 255).astype(np.uint8) - return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR) - - -def gray_to_bgr_u8(gray01: np.ndarray) -> np.ndarray: - g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8) - return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR) - - -def resize_if_needed(img: np.ndarray, target_hw: tuple[int, int]) -> np.ndarray: - if img is None: - return None - th, tw = target_hw - if img.shape[:2] == (th, tw): - return img - return cv2.resize(img, (tw, th), interpolation=cv2.INTER_LINEAR) - - -def compute_stats(img01: np.ndarray, roi_px=None) -> dict: - if img01 is None: - return { - "valid": False, - "pixels": 0, - "mean": 0.0, - "std": 0.0, - "p05": 0.0, - "p50": 0.0, - "p95": 0.0, - "sat_pct": 0.0, - "dark_pct": 0.0, - } - - if img01.ndim == 3: - arr = ( - 0.299 * img01[:, :, 0] + - 0.587 * img01[:, :, 1] + - 0.114 * img01[:, :, 2] - ).astype(np.float32) - else: - arr = img01.astype(np.float32) - - if roi_px is not None: - x0, y0, x1, y1 = roi_px - x0, x1 = sorted((int(x0), int(x1))) - y0, y1 = sorted((int(y0), int(y1))) - x0 = clamp(x0, 0, arr.shape[1] - 1) - x1 = clamp(x1, x0 + 1, arr.shape[1]) - y0 = clamp(y0, 0, arr.shape[0] - 1) - y1 = clamp(y1, y0 + 1, arr.shape[0]) - arr = arr[y0:y1, x0:x1] - - flat = arr.reshape(-1).astype(np.float32) - if flat.size == 0: - return { - "valid": False, - "pixels": 0, - "mean": 0.0, - "std": 0.0, - "p05": 0.0, - "p50": 0.0, - "p95": 0.0, - "sat_pct": 0.0, - "dark_pct": 0.0, - } - - return { - "valid": True, - "pixels": int(flat.size), - "mean": float(flat.mean()), - "std": float(flat.std()), - "p05": float(np.percentile(flat, 5)), - "p50": float(np.percentile(flat, 50)), - "p95": float(np.percentile(flat, 95)), - "sat_pct": float((flat >= 0.98).mean() * 100.0), - "dark_pct": float((flat <= 0.02).mean() * 100.0), - } - - -def pct_to_px(roi_pct: dict, w: int, h: int): - x0 = int(float(roi_pct.get("x0", 0.0)) * w) - y0 = int(float(roi_pct.get("y0", 0.0)) * h) - x1 = int(float(roi_pct.get("x1", 1.0)) * w) - y1 = int(float(roi_pct.get("y1", 1.0)) * h) - - x0 = clamp(x0, 0, w - 1) - x1 = clamp(x1, x0 + 1, w) - y0 = clamp(y0, 0, h - 1) - y1 = clamp(y1, y0 + 1, h) - - return x0, y0, x1, y1 - - -def px_to_pct(rect, w: int, h: int): - x0, y0, x1, y1 = rect - x0, x1 = sorted((int(x0), int(x1))) - y0, y1 = sorted((int(y0), int(y1))) - - x0 = clamp(x0, 0, w - 1) - x1 = clamp(x1, x0 + 1, w) - y0 = clamp(y0, 0, h - 1) - y1 = clamp(y1, y0 + 1, h) - - return { - "x0": round(x0 / float(w), 6), - "y0": round(y0 / float(h), 6), - "x1": round(x1 / float(w), 6), - "y1": round(y1 / float(h), 6), - } - - -def get_decoded_by_role(decoded: dict, role: str): - role = str(role).lower() - for cam_id, item in (decoded or {}).items(): - if str(item.get("role", "")).lower() == role: - return cam_id, item - return None, None - - -def get_image_by_role(decoded: dict, role: str): - cam_id, item = get_decoded_by_role(decoded, role) - if item is None: - return cam_id, None - return cam_id, item.get("image") - - -def get_visual_preview_by_role(visual_previews: dict, meta: dict, role: str): - """ - Busca uma imagem visual BGR dentro do retorno de cam.build_visual_preview_from_raw(), - usando camera_info para descobrir o role rgb/re/nir. - - Retorna: cam_id, img_bgr - """ - if not visual_previews: - return None, None - - camera_info = (meta or {}).get("camera_info", {}) or {} - role = str(role).lower() - - for cam_id, img in visual_previews.items(): - cam_role = str(camera_info.get(cam_id, {}).get("role", "")).lower() - if cam_role == role: - return cam_id, img - - return None, None - - -def validate_module_ready(status: dict, raw_policy: str): - if not status.get("ok", True): - raise RuntimeError(f"Status inválido retornado pelo modulo: {status}") - - active_roles = status.get("active_roles", {}) or {} - active_count = int(status.get("camera_count_active", 0)) - - if raw_policy == "require_triple": - missing = [role for role in ("rgb", "nir", "re") if role not in active_roles] - if missing: - raise RuntimeError( - f"RAW_BRUTO com require_triple exige rgb/nir/re ativas. " - f"Faltando: {missing}. Ativas: {active_roles}" - ) - elif active_count < 1: - raise RuntimeError("RAW_BRUTO requer ao menos uma câmera ativa.") - - -# ============================================================ -# Config radiométrico -# ============================================================ - -# Defaults espelhados do module_params.json atual. -# Este bloco é a "semente boa" do AE Rad: patches + Global Saturation Guard + Sun Guard. -DEFAULT_RADIOMETRIC_CONFIG = {'enabled': True, - 'interval_s': 0.25, - 'verbose': True, - 'metering_mode': 'reference_patches', - 'spectral_control_mode': 'shared', - 'control_metric': 'p50', - 'target_value': 0.5, - 'deadband': 0.035, - 'p95_limit': 0.94, - 'saturation_limit_pct': 0.5, - 'alpha': 0.18, - 'exp_step_gain': 0.55, - 'prefer_exposure': True, - 'exp_min_us': 100, - 'exp_max_us': 80000, - 'gain_min': 1.0, - 'gain_max': 4.0, - 'reference_patches': [{'name': 'black_reference', - 'type': 'black', - 'roles': ['rgb', 're', 'nir'], - 'roi_pct': {'x0': 0.365625, 'y0': 0.8225, 'x1': 0.432812, 'y1': 0.995}, - 'target_value': 0.08, - 'weight': 0.7, - 'roi_pct_by_role': {'rgb': {'x0': 0.365625, 'y0': 0.8225, 'x1': 0.432812, 'y1': 0.995}, - 're': {'x0': 0.395313, 'y0': 0.745, 'x1': 0.4625, 'y1': 0.9225}, - 'nir': {'x0': 0.353125, 'y0': 0.78, 'x1': 0.420312, 'y1': 0.9525}}, - 'roi_list_by_role': {'rgb': [{'name': 'rgb_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.365625, - 'y0': 0.8225, - 'x1': 0.432812, - 'y1': 0.995}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:19:46'}], - 're': [{'name': 're_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.395313, - 'y0': 0.745, - 'x1': 0.4625, - 'y1': 0.9225}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:20:14'}], - 'nir': [{'name': 'nir_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.353125, - 'y0': 0.78, - 'x1': 0.420312, - 'y1': 0.9525}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:20:47'}]}}, - {'name': 'gray_reference', - 'type': 'gray', - 'roles': ['rgb', 're', 'nir'], - 'roi_pct': {'x0': 0.29375, 'y0': 0.825, 'x1': 0.3625, 'y1': 0.995}, - 'target_value': 0.35, - 'target_value_by_role': {'rgb': 0.34, 're': 0.24, 'nir': 0.3}, - 'weight': 1.0, - 'roi_pct_by_role': {'rgb': {'x0': 0.29375, 'y0': 0.825, 'x1': 0.3625, 'y1': 0.995}, - 're': {'x0': 0.325, 'y0': 0.75, 'x1': 0.389062, 'y1': 0.915}, - 'nir': {'x0': 0.284375, 'y0': 0.79, 'x1': 0.35, 'y1': 0.9525}}, - 'roi_list_by_role': {'rgb': [{'name': 'rgb_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.29375, - 'y0': 0.825, - 'x1': 0.3625, - 'y1': 0.995}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:19:29'}], - 're': [{'name': 're_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.325, 'y0': 0.75, 'x1': 0.389062, 'y1': 0.915}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:20:22'}], - 'nir': [{'name': 'nir_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.284375, 'y0': 0.79, 'x1': 0.35, 'y1': 0.9525}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:20:53'}]}}, - {'name': 'white_reference', - 'type': 'white', - 'roles': ['rgb', 're', 'nir'], - 'roi_pct': {'x0': 0.220312, 'y0': 0.8225, 'x1': 0.2875, 'y1': 0.995}, - 'target_value': 0.82, - 'weight': 0.8, - 'roi_pct_by_role': {'rgb': {'x0': 0.220312, 'y0': 0.8225, 'x1': 0.2875, 'y1': 0.995}, - 're': {'x0': 0.25, 'y0': 0.7525, 'x1': 0.315625, 'y1': 0.9225}, - 'nir': {'x0': 0.214062, 'y0': 0.785, 'x1': 0.282813, 'y1': 0.9525}}, - 'roi_list_by_role': {'rgb': [{'name': 'rgb_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.220312, - 'y0': 0.8225, - 'x1': 0.2875, - 'y1': 0.995}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:19:04'}], - 're': [{'name': 're_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.25, - 'y0': 0.7525, - 'x1': 0.315625, - 'y1': 0.9225}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:20:27'}], - 'nir': [{'name': 'nir_legacy_01', - 'enabled': True, - 'roi_pct': {'x0': 0.214062, - 'y0': 0.785, - 'x1': 0.282813, - 'y1': 0.9525}, - 'created_at': '2026-05-07 14:36:25', - 'updated_at': '2026-05-08 09:21:02'}]}}], - 'exp_apply_threshold_us': 80, - 'gain_apply_threshold': 0.05, - 'apply_same_spectral_to_both': True, - 'spectral_roles': ['re', 'nir'], - 'dark_limit_pct': 35.0, - 'control_strategy': 'ratio', - 'ratio_alpha': 0.35, - 'ratio_min': 0.65, - 'ratio_max': 1.35, - 'reduce_fast_factor': 0.8, - 'factor_min': 0.55, - 'factor_max': 1.28, - 'gain_return_enabled': True, - 'gain_reduce_on_saturation': True, - 'gain_increase_required_cycles': 5, - 'gain_decrease_required_cycles': 2, - 'gain_step_up': 0.2, - 'gain_step_down': 0.5, - 'gain_hard_reset_on_saturation': False, - 'exp_high_ratio_for_gain': 0.95, - 'exp_low_ratio_for_gain_return': 0.75, - 'role_limits': {'rgb': {'exp_min_us': 100, 'exp_max_us': 80000, 'gain_min': 1.0, 'gain_max': 2.0}, - 're': {'exp_min_us': 100, 'exp_max_us': 2500, 'gain_min': 1.0, 'gain_max': 2.0}, - 'nir': {'exp_min_us': 100, 'exp_max_us': 3000, 'gain_min': 1.0, 'gain_max': 2.0}}, - 'ready_required_cycles': 3, - 'patch_control_mode': 'gray_primary', - 'patch_require_order': True, - 'patch_min_separation': 0.08, - 'patch_white_sat_limit_pct': 0.5, - 'patch_white_p95_limit': 0.94, - 'patch_black_dark_limit_pct': 80.0, - 'patch_black_max_p50': 0.2, - 'patch_gray_min_p50': 0.08, - 'patch_gray_max_p50': 0.85, - 'patch_roi_contract': 'multi_roi_by_role_v1', - 'patch_roi_reduce_method': 'median_valid_rois', - 'patch_roi_outlier_reject': True, - 'patch_roi_max_p50_delta': 0.12, - 'global_saturation_guard_enabled': True, - 'global_guard_roi_pct': {'x0': 0.05, 'y0': 0.05, 'x1': 0.95, 'y1': 0.95}, - 'global_guard_sat_threshold': 0.985, - 'global_guard_near_sat_threshold': 0.94, - 'global_guard_sat_pct_soft': 0.05, - 'global_guard_sat_pct_hard': 0.2, - 'global_guard_sat_pct_extreme': 0.8, - 'global_guard_blob_pct_soft': 0.015, - 'global_guard_blob_pct_hard': 0.08, - 'global_guard_blob_pct_extreme': 0.25, - 'global_guard_min_blob_px': 48, - 'global_guard_downsample_max_side': 320, - 'global_guard_reduce_factor_soft': 0.82, - 'global_guard_reduce_factor_hard': 0.6, - 'global_guard_reduce_factor_extreme': 0.35, - 'sun_guard_enabled': True, - 'sun_guard_p99_threshold': 0.9, - 'sun_guard_near_sat_pct_threshold': 0.8, - 'sun_guard_freeze_increase_cycles': 2, - 'sun_guard_allow_decrease': True, - 'guard_force_apply_enabled': True, - 'guard_force_apply_soft': True, - 'guard_force_apply_hard': True, - 'guard_force_apply_extreme': True, - 'guard_force_apply_on_patch_saturation': True, - 'guard_freeze_cycles_soft': 3, - 'guard_freeze_cycles_hard': 5, - 'guard_freeze_cycles_extreme': 8, - 'guard_reapply_min_exp_on_emergency': True, - 'guard_min_exp_margin_us': 80} - -DEFAULT_PATCH_NORMALIZATION = {'enabled': True, - 'apply_when_metering_mode': 'reference_patches', - 'apply_stage': 'after_fusion', - 'method': 'gray_scale_with_white_guard', - 'space': 'multispec_tensor', - 'targets': {'black': 0.06, 'gray': 0.4, 'white': 0.78}, - 'white_guard_max': 0.92, - 'scale_min': 0.35, - 'scale_max': 2.5, - 'clip_output': True, - 'require_valid_gray': True, - 'use_black_for_offset': False, - 'save_patch_stats': True} - -PROFILE_SCHEMA = "multispec_radiometric_config_profiles_v4" -MODULE_PARAMS_SCHEMA = "multispec_module_params_v3" - - -def deep_clone(obj): - return json.loads(json.dumps(obj)) - - -def is_module_params_contract(data: dict) -> bool: - """Detecta o contrato completo do module_params.json, para não salvar wrappers do tool nele.""" - if not isinstance(data, dict): - return False - schema = str(data.get("schema", "")) - if schema == MODULE_PARAMS_SCHEMA: - return True - module_keys = ("camera_settings", "fusion_config", "rgb_calibration", "flatfield_config") - return "radiometric_config" in data and any(k in data for k in module_keys) - - -def sanitize_radiometric_config(cfg: dict) -> dict: - """Garante que o radiometric_config salvo siga o contrato runtime atual.""" - out = deep_clone(DEFAULT_RADIOMETRIC_CONFIG) - if isinstance(cfg, dict): - # Preserva valores/ROIs escolhidos no tool, mas injeta qualquer chave nova faltante. - for k, v in cfg.items(): - out[k] = v - - out.setdefault("reference_patches", deep_clone(DEFAULT_RADIOMETRIC_CONFIG.get("reference_patches", []))) - - # Garante contrato multi_roi_by_role_v1 em todos os patches. - out["patch_roi_contract"] = "multi_roi_by_role_v1" - for patch in out.get("reference_patches", []) or []: - if not isinstance(patch, dict): - continue - patch.setdefault("roles", ROLES[:] if "ROLES" in globals() else ["rgb", "re", "nir"]) - patch.setdefault("roi_pct", {}) - patch.setdefault("roi_pct_by_role", {"rgb": {}, "re": {}, "nir": {}}) - patch.setdefault("roi_list_by_role", {"rgb": [], "re": [], "nir": []}) - ensure_patch_roi_lists_by_role(patch) - - # Garante guardas parrudas mesmo em arquivos antigos. - for k, v in DEFAULT_RADIOMETRIC_CONFIG.items(): - if k.startswith("global_guard_") or k.startswith("sun_guard_") or k.startswith("guard_"): - out.setdefault(k, v) - - return out - - -def base_ae_contract(): - cfg = deep_clone(DEFAULT_RADIOMETRIC_CONFIG) - # Removemos somente campos específicos de patches quando usado como base global. - cfg.pop("reference_patches", None) - cfg.pop("patch_control_mode", None) - cfg.pop("patch_require_order", None) - cfg.pop("patch_min_separation", None) - cfg.pop("patch_white_sat_limit_pct", None) - cfg.pop("patch_white_p95_limit", None) - cfg.pop("patch_black_dark_limit_pct", None) - cfg.pop("patch_black_max_p50", None) - cfg.pop("patch_gray_min_p50", None) - cfg.pop("patch_gray_max_p50", None) - cfg.pop("patch_roi_contract", None) - cfg.pop("patch_roi_reduce_method", None) - cfg.pop("patch_roi_outlier_reject", None) - cfg.pop("patch_roi_max_p50_delta", None) - return cfg - - -def default_profile_global(): - cfg = base_ae_contract() - base = {"x0": 0.08, "y0": 0.08, "x1": 0.92, "y1": 0.92} - guard_base = deep_clone(DEFAULT_RADIOMETRIC_CONFIG.get("global_guard_roi_pct", {"x0": 0.05, "y0": 0.05, "x1": 0.95, "y1": 0.95})) - - cfg.update({ - "metering_mode": "global", - "spectral_control_mode": DEFAULT_RADIOMETRIC_CONFIG.get("spectral_control_mode", "shared"), - "global_roi_pct": base, - "global_roi_pct_by_role": {"rgb": dict(base), "re": dict(base), "nir": dict(base)}, - "global_guard_roi_pct": guard_base, - }) - return {"radiometric_config": cfg} - - -def make_default_patch(patch_type: str, target: float, weight: float): - # Preferimos copiar o patch correspondente do module_params.json atual. - for p in DEFAULT_RADIOMETRIC_CONFIG.get("reference_patches", []) or []: - if str(p.get("type", "")).lower() == str(patch_type).lower(): - patch = deep_clone(p) - patch.setdefault("target_value", target) - patch.setdefault("weight", weight) - patch.setdefault("roles", ROLES[:] if "ROLES" in globals() else ["rgb", "re", "nir"]) - patch.setdefault("roi_pct", {}) - patch.setdefault("roi_pct_by_role", {"rgb": {}, "re": {}, "nir": {}}) - patch.setdefault("roi_list_by_role", {"rgb": [], "re": [], "nir": []}) - return patch - - return { - "name": f"{patch_type}_reference", - "type": patch_type, - "roles": ROLES[:] if "ROLES" in globals() else ["rgb", "re", "nir"], - "target_value": target, - "weight": weight, - "roi_pct": {}, - "roi_pct_by_role": {"rgb": {}, "re": {}, "nir": {}}, - "roi_list_by_role": {"rgb": [], "re": [], "nir": []}, - } - - -def default_profile_patches(): - cfg = sanitize_radiometric_config(DEFAULT_RADIOMETRIC_CONFIG) - cfg["metering_mode"] = "reference_patches" - cfg["spectral_control_mode"] = DEFAULT_RADIOMETRIC_CONFIG.get("spectral_control_mode", "shared") - return {"radiometric_config": cfg} - -def get_active_profile_name(data: dict) -> str: - name = str(data.get("active_profile", "global_scene_mode")) - if name not in ("global_scene_mode", "three_reference_patches_mode"): - return "global_scene_mode" - return name - - -def set_active_profile_name(data: dict, profile_name: str): - if profile_name not in ("global_scene_mode", "three_reference_patches_mode"): - profile_name = "global_scene_mode" - data["active_profile"] = profile_name - - -def get_active_radiometric_config(data: dict) -> dict: - profile_name = get_active_profile_name(data) - profile = data.get(profile_name, {}) or {} - cfg = profile.get("radiometric_config", {}) or {} - return json.loads(json.dumps(cfg)) - - -def update_root_radiometric_config(data: dict): - data["radiometric_config"] = get_active_radiometric_config(data) - - -def load_or_default_config(path: str): - """ - Carrega tanto: - 1) calibration/module_params.json completo, contrato multispec_module_params_v3; - 2) arquivo isolado do tool com perfis. - - Em ambos os casos, o root radiometric_config é mantido no mesmo contrato do runtime. - """ - if path and os.path.isfile(path): - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - else: - data = {} - - module_contract = is_module_params_contract(data) - root_cfg = data.get("radiometric_config") if isinstance(data.get("radiometric_config"), dict) else None - - if module_contract: - # Não troca o schema do module_params. Apenas cria perfis internos para a UI. - data.setdefault("schema", MODULE_PARAMS_SCHEMA) - else: - data.setdefault("schema", PROFILE_SCHEMA) - - data.setdefault("saved_at", now_str()) - - # Se já existe um radiometric_config na raiz, ele é a fonte da verdade. - if root_cfg: - root_cfg = sanitize_radiometric_config(root_cfg) - active = "three_reference_patches_mode" if str(root_cfg.get("metering_mode", "")).lower() == "reference_patches" else "global_scene_mode" - data["active_profile"] = active - data.setdefault("global_scene_mode", default_profile_global()) - data.setdefault("three_reference_patches_mode", default_profile_patches()) - data[active]["radiometric_config"] = root_cfg - else: - data.setdefault("active_profile", "global_scene_mode") - data.setdefault("global_scene_mode", default_profile_global()) - data.setdefault("three_reference_patches_mode", default_profile_patches()) - - data.setdefault("patch_normalization", deep_clone(DEFAULT_PATCH_NORMALIZATION)) - - # Migração: injeta chaves novas nos dois perfis sem sobrescrever ROIs existentes. - for profile_name, default_fn in ( - ("global_scene_mode", default_profile_global), - ("three_reference_patches_mode", default_profile_patches), - ): - default_profile = default_fn() - data.setdefault(profile_name, default_profile) - data[profile_name].setdefault("radiometric_config", {}) - - default_cfg = default_profile["radiometric_config"] - cfg = data[profile_name]["radiometric_config"] - - for k, v in default_cfg.items(): - cfg.setdefault(k, deep_clone(v)) - - if profile_name == "three_reference_patches_mode": - data[profile_name]["radiometric_config"] = sanitize_radiometric_config(cfg) - - update_root_radiometric_config(data) - return data - -def save_config(path: str, data: dict): - ensure_dir(os.path.dirname(path) or ".") - module_contract = is_module_params_contract(data) - - # Atualiza radiometric_config root a partir do perfil ativo, usando o contrato runtime atual. - update_root_radiometric_config(data) - data["radiometric_config"] = sanitize_radiometric_config(data.get("radiometric_config", {})) - data["patch_normalization"] = data.get("patch_normalization") or deep_clone(DEFAULT_PATCH_NORMALIZATION) - data["saved_at"] = now_str() - - if module_contract: - # Salva limpo no contrato multispec_module_params_v3, sem wrappers internos da UI. - out = dict(data) - out["schema"] = MODULE_PARAMS_SCHEMA - out.pop("active_profile", None) - out.pop("global_scene_mode", None) - out.pop("three_reference_patches_mode", None) - else: - out = dict(data) - out["schema"] = PROFILE_SCHEMA - out["active_profile"] = get_active_profile_name(data) - update_root_radiometric_config(out) - out["radiometric_config"] = sanitize_radiometric_config(out.get("radiometric_config", {})) - - with open(path, "w", encoding="utf-8") as f: - json.dump(out, f, ensure_ascii=False, indent=2) - - - -ROLES = ["rgb", "re", "nir"] - - -def normalize_role(role: str) -> str: - role = str(role or "rgb").lower() - return role if role in ROLES else "rgb" - - -def default_roi(): - return {"x0": 0.08, "y0": 0.08, "x1": 0.92, "y1": 0.92} - - -def clone_roi(roi: dict) -> dict: - roi = roi or {} - return { - "x0": float(roi.get("x0", 0.08)), - "y0": float(roi.get("y0", 0.08)), - "x1": float(roi.get("x1", 0.92)), - "y1": float(roi.get("y1", 0.92)), - } - - -def make_roi_by_role(base_roi=None): - base = clone_roi(base_roi or default_roi()) - return {role: dict(base) for role in ROLES} - - -def ensure_global_roi_by_role(data: dict): - data.setdefault("global_scene_mode", default_profile_global()) - cfg = data["global_scene_mode"].setdefault( - "radiometric_config", - default_profile_global()["radiometric_config"], - ) - - legacy = cfg.get("global_roi_pct", default_roi()) - by_role = cfg.setdefault("global_roi_pct_by_role", make_roi_by_role(legacy)) - - for role in ROLES: - if role not in by_role or not by_role[role]: - by_role[role] = clone_roi(legacy) - - return by_role - - -def get_global_roi_for_role(data: dict, role: str): - role = normalize_role(role) - by_role = ensure_global_roi_by_role(data) - return by_role.get(role, clone_roi(default_roi())) - - -def set_global_roi_for_role(data: dict, role: str, roi_pct: dict): - role = normalize_role(role) - by_role = ensure_global_roi_by_role(data) - by_role[role] = roi_pct - - # Compatibilidade: mantém uma ROI antiga preenchida. - # Uso: média/legado/visual antigo. O controller novo deverá usar by_role. - data["global_scene_mode"]["radiometric_config"]["global_roi_pct"] = by_role.get("rgb", roi_pct) - - -def get_patches(data: dict): - return ( - data.get("three_reference_patches_mode", {}) - .get("radiometric_config", {}) - .get("reference_patches", []) - ) - - -def make_roi_entry(roi_pct: dict, name: str | None = None, enabled: bool = True) -> dict: - return { - "name": name or "roi_01", - "enabled": bool(enabled), - "roi_pct": clone_roi(roi_pct), - "created_at": now_str(), - "updated_at": now_str(), - } - - -def normalize_roi_entry(entry, idx: int) -> dict | None: - """Aceita formatos antigos e novos, devolvendo sempre um item padrão.""" - if not entry: - return None - - if isinstance(entry, dict) and "roi_pct" in entry: - roi = entry.get("roi_pct") or {} - if not roi: - return None - out = dict(entry) - out["name"] = str(out.get("name") or f"roi_{idx + 1:02d}") - out["enabled"] = bool(out.get("enabled", True)) - out["roi_pct"] = clone_roi(roi) - out.setdefault("created_at", now_str()) - out["updated_at"] = str(out.get("updated_at") or now_str()) - return out - - if isinstance(entry, dict) and all(k in entry for k in ("x0", "y0", "x1", "y1")): - return make_roi_entry(entry, name=f"roi_{idx + 1:02d}", enabled=True) - - return None - - -def sync_patch_legacy_roi_fields(patch: dict): - """Mantém roi_pct e roi_pct_by_role compatíveis com scripts antigos.""" - roi_lists = patch.setdefault("roi_list_by_role", {}) - by_role = patch.setdefault("roi_pct_by_role", {}) - - for role in ROLES: - entries = roi_lists.setdefault(role, []) - first_active = next((e.get("roi_pct") for e in entries if e.get("enabled", True) and e.get("roi_pct")), {}) - by_role[role] = clone_roi(first_active) if first_active else {} - - patch["roi_pct"] = by_role.get("rgb", {}) or {} - - -def ensure_patch_roi_lists_by_role(patch: dict): - """ - Migra o formato antigo: - roi_pct_by_role[role] = {x0,y0,x1,y1} - para o formato novo: - roi_list_by_role[role] = [{name, enabled, roi_pct, ...}, ...] - - Também aceita, por tolerância, caso alguém já tenha salvo uma lista dentro de roi_pct_by_role. - """ - legacy_global = patch.get("roi_pct", {}) or {} - legacy_by_role = patch.get("roi_pct_by_role", {}) or {} - roi_lists = patch.setdefault("roi_list_by_role", {}) - - for role in ROLES: - raw_list = roi_lists.get(role, []) - - # Caso raro: formato novo foi salvo diretamente em roi_pct_by_role. - if not raw_list and isinstance(legacy_by_role.get(role), list): - raw_list = legacy_by_role.get(role) or [] - - normalized = [] - if isinstance(raw_list, list): - for idx, item in enumerate(raw_list): - entry = normalize_roi_entry(item, idx) - if entry is not None: - normalized.append(entry) - elif isinstance(raw_list, dict) and raw_list: - entry = normalize_roi_entry(raw_list, 0) - if entry is not None: - normalized.append(entry) - - # Migração do formato antigo, se ainda não houver lista. - if not normalized: - old_roi = legacy_by_role.get(role, {}) if isinstance(legacy_by_role, dict) else {} - if not old_roi and legacy_global: - old_roi = legacy_global - if isinstance(old_roi, dict) and old_roi: - normalized.append(make_roi_entry(old_roi, name=f"{role}_legacy_01", enabled=True)) - - # Garante nomes estáveis e únicos. - seen = set() - for idx, entry in enumerate(normalized): - name = str(entry.get("name") or f"roi_{idx + 1:02d}") - if name in seen: - name = f"{name}_{idx + 1:02d}" - seen.add(name) - entry["name"] = name - - roi_lists[role] = normalized - - sync_patch_legacy_roi_fields(patch) - return roi_lists - - -# Alias antigo mantido para não quebrar chamadas existentes. -def ensure_patch_roi_by_role(patch: dict): - ensure_patch_roi_lists_by_role(patch) - return patch.setdefault("roi_pct_by_role", {}) - - -def get_patch_by_type(data: dict, patch_type: str): - patch_type = str(patch_type).lower() - for p in get_patches(data): - if str(p.get("type", "")).lower() == patch_type: - ensure_patch_roi_lists_by_role(p) - return p - return None - - -def ensure_patch_exists(data: dict, patch_type: str): - data.setdefault("three_reference_patches_mode", default_profile_patches()) - cfg = data["three_reference_patches_mode"].setdefault( - "radiometric_config", - default_profile_patches()["radiometric_config"], - ) - - patches = cfg.setdefault( - "reference_patches", - default_profile_patches()["radiometric_config"]["reference_patches"], - ) - - patch_type = str(patch_type).lower() - target = {"black": 0.06, "gray": 0.40, "white": 0.78}.get(patch_type, 0.40) - weight = {"black": 0.25, "gray": 1.0, "white": 0.7}.get(patch_type, 1.0) - - for p in patches: - if str(p.get("type", "")).lower() == patch_type: - ensure_patch_roi_lists_by_role(p) - return p - - patch = make_default_patch(patch_type, target, weight) - patches.append(patch) - ensure_patch_roi_lists_by_role(patch) - return patch - - -def get_patch_roi_entries_for_role(data: dict, patch_type: str, role: str, enabled_only: bool = False): - role = normalize_role(role) - patch = get_patch_by_type(data, patch_type) - if not patch: - return [] - roi_lists = ensure_patch_roi_lists_by_role(patch) - entries = list(roi_lists.get(role, []) or []) - if enabled_only: - entries = [e for e in entries if e.get("enabled", True) and e.get("roi_pct")] - return entries - - -def get_patch_roi_for_role(data: dict, patch_type: str, role: str): - """Compatibilidade: retorna a primeira ROI ativa da lista.""" - entries = get_patch_roi_entries_for_role(data, patch_type, role, enabled_only=True) - if entries: - return entries[0].get("roi_pct", {}) or {} - - patch = get_patch_by_type(data, patch_type) - if not patch: - return {} - return patch.get("roi_pct_by_role", {}).get(normalize_role(role), {}) or patch.get("roi_pct", {}) or {} - - -def get_patch_roi_entry(data: dict, patch_type: str, role: str, index: int): - entries = get_patch_roi_entries_for_role(data, patch_type, role, enabled_only=False) - if not entries: - return None, -1 - index = clamp(int(index), 0, len(entries) - 1) - return entries[index], index - - -def set_patch_roi_for_role(data: dict, patch_type: str, role: str, roi_pct: dict, index: int | None = None, append: bool = False): - patch = ensure_patch_exists(data, patch_type) - role = normalize_role(role) - roi_lists = ensure_patch_roi_lists_by_role(patch) - entries = roi_lists.setdefault(role, []) - - if append or index is None or index >= len(entries) or index < 0: - entry = make_roi_entry( - roi_pct, - name=f"{patch_type}_{role}_{len(entries) + 1:02d}", - enabled=True, - ) - entries.append(entry) - saved_index = len(entries) - 1 - else: - saved_index = int(index) - old = entries[saved_index] - old["roi_pct"] = clone_roi(roi_pct) - old["enabled"] = bool(old.get("enabled", True)) - old["updated_at"] = now_str() - - sync_patch_legacy_roi_fields(patch) - return saved_index - - -def delete_patch_roi_for_role(data: dict, patch_type: str, role: str, index: int): - patch = get_patch_by_type(data, patch_type) - if not patch: - return False, 0 - role = normalize_role(role) - roi_lists = ensure_patch_roi_lists_by_role(patch) - entries = roi_lists.setdefault(role, []) - if not entries: - return False, 0 - index = clamp(int(index), 0, len(entries) - 1) - entries.pop(index) - sync_patch_legacy_roi_fields(patch) - return True, len(entries) - - -def toggle_patch_roi_enabled_for_role(data: dict, patch_type: str, role: str, index: int): - entry, idx = get_patch_roi_entry(data, patch_type, role, index) - if entry is None: - return False, False - entry["enabled"] = not bool(entry.get("enabled", True)) - entry["updated_at"] = now_str() - patch = get_patch_by_type(data, patch_type) - if patch: - sync_patch_legacy_roi_fields(patch) - return True, bool(entry["enabled"]) - - -def add_empty_patch_roi_slot(data: dict, patch_type: str, role: str): - # Usa uma ROI pequena central como placeholder, para o usuário arrastar por cima depois. - return set_patch_roi_for_role( - data, - patch_type, - role, - {"x0": 0.45, "y0": 0.45, "x1": 0.55, "y1": 0.55}, - append=True, - ) - -def set_shared_mode(data: dict, shared: bool): - for profile in ("global_scene_mode", "three_reference_patches_mode"): - data.setdefault(profile, default_profile_global() if profile == "global_scene_mode" else default_profile_patches()) - cfg = data[profile].setdefault("radiometric_config", {}) - cfg["spectral_control_mode"] = "shared" if shared else "independent" - cfg["apply_same_spectral_to_both"] = bool(shared) - - update_root_radiometric_config(data) - - -# ============================================================ -# UI -# ============================================================ - -PATCH_COLORS = { - "global": (0, 255, 255), - "black": (80, 80, 80), - "gray": (180, 180, 180), - "white": (255, 255, 255), -} - -PATCH_ORDER = ["black", "gray", "white"] - - -def draw_roi_on_panel(panel, roi_pct, label, color, thickness=2): - if roi_pct is None: - return - h, w = panel.shape[:2] - x0, y0, x1, y1 = pct_to_px(roi_pct, w, h) - - cv2.rectangle(panel, (x0, y0), (x1, y1), color, thickness) - cv2.putText(panel, label, (x0 + 5, max(20, y0 - 6)), cv2.FONT_HERSHEY_SIMPLEX, - 0.55, (0, 0, 0), 3, cv2.LINE_AA) - cv2.putText(panel, label, (x0 + 5, max(20, y0 - 6)), cv2.FONT_HERSHEY_SIMPLEX, - 0.55, color, 1, cv2.LINE_AA) - - -def draw_all_rois(panel, data, selected_target, mode, panel_role, edit_role, selected_roi_index=0): - panel_role = normalize_role(panel_role) - edit_role = normalize_role(edit_role) - - is_edit_panel = panel_role == edit_role - - if mode == "global": - roi = get_global_roi_for_role(data, panel_role) - label = f"GLOBAL/{panel_role.upper()}" - thickness = 3 if is_edit_panel else 2 - draw_roi_on_panel(panel, roi, label, PATCH_COLORS["global"], thickness) - - else: - for p in get_patches(data): - typ = str(p.get("type", "")).lower() - color = PATCH_COLORS.get(typ, (0, 255, 255)) - entries = get_patch_roi_entries_for_role(data, typ, panel_role, enabled_only=False) - - for idx, entry in enumerate(entries): - roi = entry.get("roi_pct", {}) - if not roi: - continue - - enabled = bool(entry.get("enabled", True)) - selected = typ == selected_target and is_edit_panel and idx == selected_roi_index - thickness = 3 if selected else 1 if not enabled else 2 - - label = f"{typ.upper()}/{panel_role.upper()}#{idx + 1}" - if not enabled: - label += " OFF" - - draw_roi_on_panel(panel, roi, label, color, thickness) - - -def build_board( - decoded, - data, - mode, - selected_target, - edit_role, - selected_roi_index, - drag_rect_local, - drag_role, - panel_rects, - preview_scale=1.0, - visual_previews=None, - meta=None, - beauty_preview=True, -): - rgb_id, rgb01 = get_image_by_role(decoded, "rgb") - re_id, re01 = get_image_by_role(decoded, "re") - nir_id, nir01 = get_image_by_role(decoded, "nir") - - # ------------------------------------------------------------ - # Tamanho base SEMPRE vem do decoded, porque ROI/stats usam dado real. - # O preview visual é só para desenhar bonito. - # ------------------------------------------------------------ - if rgb01 is not None: - base_h, base_w = rgb01.shape[:2] - elif re01 is not None: - base_h, base_w = re01.shape[:2] - elif nir01 is not None: - base_h, base_w = nir01.shape[:2] - else: - base_h, base_w = 800, 1280 - - # ------------------------------------------------------------ - # Preview bonito, igual ao capture. - # ------------------------------------------------------------ - rgb_vis_id, rgb_vis = get_visual_preview_by_role(visual_previews, meta, "rgb") - re_vis_id, re_vis = get_visual_preview_by_role(visual_previews, meta, "re") - nir_vis_id, nir_vis = get_visual_preview_by_role(visual_previews, meta, "nir") - - if beauty_preview and rgb_vis is not None: - rgb_panel = rgb_vis.copy() - if rgb_panel.shape[:2] != (base_h, base_w): - rgb_panel = cv2.resize(rgb_panel, (base_w, base_h), interpolation=cv2.INTER_LINEAR) - rgb_id = rgb_vis_id - else: - if rgb01 is not None: - rgb_panel = to_bgr_u8_from_rgb01(rgb01) - else: - rgb_panel = np.zeros((base_h, base_w, 3), dtype=np.uint8) - overlay_hud(rgb_panel, ["RGB", "sem frame"]) - - if beauty_preview and re_vis is not None: - re_panel = re_vis.copy() - if re_panel.shape[:2] != (base_h, base_w): - re_panel = cv2.resize(re_panel, (base_w, base_h), interpolation=cv2.INTER_LINEAR) - re_id = re_vis_id - else: - re01_show = resize_if_needed(re01, (base_h, base_w)) if re01 is not None else None - re_panel = gray_to_bgr_u8(re01_show) if re01_show is not None else np.zeros_like(rgb_panel) - - if beauty_preview and nir_vis is not None: - nir_panel = nir_vis.copy() - if nir_panel.shape[:2] != (base_h, base_w): - nir_panel = cv2.resize(nir_panel, (base_w, base_h), interpolation=cv2.INTER_LINEAR) - nir_id = nir_vis_id - else: - nir01_show = resize_if_needed(nir01, (base_h, base_w)) if nir01 is not None else None - nir_panel = gray_to_bgr_u8(nir01_show) if nir01_show is not None else np.zeros_like(rgb_panel) - - draw_all_rois(rgb_panel, data, selected_target, mode, "rgb", edit_role, selected_roi_index) - draw_all_rois(re_panel, data, selected_target, mode, "re", edit_role, selected_roi_index) - draw_all_rois(nir_panel, data, selected_target, mode, "nir", edit_role, selected_roi_index) - - if drag_rect_local is not None: - x0, y0, x1, y1 = drag_rect_local - color = PATCH_COLORS["global"] if mode == "global" else PATCH_COLORS.get(selected_target, (0, 255, 255)) - - if drag_role == "rgb": - cv2.rectangle(rgb_panel, (x0, y0), (x1, y1), color, 1) - elif drag_role == "re": - cv2.rectangle(re_panel, (x0, y0), (x1, y1), color, 1) - elif drag_role == "nir": - cv2.rectangle(nir_panel, (x0, y0), (x1, y1), color, 1) - - overlay_hud(rgb_panel, [f"RGB ({rgb_id})"], y=24) - overlay_hud(re_panel, [f"RE ({re_id})"], y=24) - overlay_hud(nir_panel, [f"NIR ({nir_id})"], y=24) - - ph = max(rgb_panel.shape[0], re_panel.shape[0], nir_panel.shape[0]) - pw = max(rgb_panel.shape[1], re_panel.shape[1], nir_panel.shape[1]) - - def fit_panel(img): - if img.shape[:2] != (ph, pw): - return cv2.resize(img, (pw, ph), interpolation=cv2.INTER_NEAREST) - return img - - rgb_panel = fit_panel(rgb_panel) - re_panel = fit_panel(re_panel) - nir_panel = fit_panel(nir_panel) - - data_panel = np.zeros((ph, pw, 3), dtype=np.uint8) - - panel_rects["rgb"] = (0, 0, pw, ph) - panel_rects["re"] = (pw, 0, pw * 2, ph) - panel_rects["nir"] = (0, ph, pw, ph * 2) - panel_rects["data"] = (pw, ph, pw * 2, ph * 2) - - top = np.hstack([rgb_panel, re_panel]) - bottom = np.hstack([nir_panel, data_panel]) - board = np.vstack([top, bottom]) - - x0, y0, x1, y1 = panel_rects["data"] - lines = build_data_lines(decoded, data, mode, selected_target, edit_role, selected_roi_index, base_w, base_h) - overlay_hud(board, lines, x=x0 + 16, y=y0 + 28, font_scale=0.50, line_step=20) - - if preview_scale != 1.0: - board = cv2.resize( - board, - (int(board.shape[1] * preview_scale), int(board.shape[0] * preview_scale)), - interpolation=cv2.INTER_NEAREST, - ) - - return board - - -def build_data_lines(decoded, data, mode, selected_target, edit_role, selected_roi_index, base_w, base_h): - edit_role = normalize_role(edit_role) - active_profile = get_active_profile_name(data) - active_cfg = get_active_radiometric_config(data) - - lines = [ - "RADIOMETRIC CONFIG TOOL", - f"modo={mode.upper()} | camera={edit_role.upper()} | active={active_profile}", - f"spectral={active_cfg.get('spectral_control_mode')} | strategy={active_cfg.get('control_strategy')}", - f"roi_contract={active_cfg.get('patch_roi_contract', 'legacy_single_roi')}", - "", - "Arraste no painel da camera editada para definir/atualizar ROI.", - "PATCHES agora suportam N ROIs por cor e por camera.", - "", - ] - - if mode == "global": - lines.append("GLOBAL ROI por camera:") - for role in ROLES: - roi_pct = get_global_roi_for_role(data, role) - marker = "*" if role == edit_role else " " - lines.append(f"{marker} {role.upper()}: roi={roi_pct}") - - lines.append("") - lines.append("Stats GLOBAL:") - lines.extend(stats_lines_for_mode(data, decoded, mode="global", patch_type=None)) - - else: - entries_edit = get_patch_roi_entries_for_role(data, selected_target, edit_role, enabled_only=False) - n_edit = len(entries_edit) - selected_roi_index = clamp(selected_roi_index, 0, max(0, n_edit - 1)) if n_edit else 0 - - lines.append(f"PATCH selecionado: {selected_target.upper()}") - lines.append(f"ROI selecionada {edit_role.upper()}: #{selected_roi_index + 1 if n_edit else 0}/{n_edit}") - lines.append("Contagem de ROIs por camera:") - for role in ROLES: - entries = get_patch_roi_entries_for_role(data, selected_target, role, enabled_only=False) - enabled = sum(1 for e in entries if e.get("enabled", True)) - marker = "*" if role == edit_role else " " - lines.append(f"{marker} {role.upper()}: {enabled}/{len(entries)} ativas") - - if n_edit: - entry = entries_edit[selected_roi_index] - lines.append(f"ROI atual: {entry.get('name')} | enabled={entry.get('enabled', True)}") - lines.append(f"rect={entry.get('roi_pct')}") - else: - lines.append("ROI atual: nenhuma. Arraste para criar a primeira.") - - sel_patch = get_patch_by_type(data, selected_target) - if sel_patch: - lines.append( - f"target={float(sel_patch.get('target_value', 0.0)):.2f} " - f"weight={float(sel_patch.get('weight', 1.0)):.2f}" - ) - - lines.append("") - lines.append(f"Stats robustas {selected_target.upper()}:") - lines.extend(stats_lines_for_mode(data, decoded, mode="patches", patch_type=selected_target)) - - lines.extend([ - "", - "M = GLOBAL/PATCHES | C = camera | V = preview bonito/bruto", - "1/2/3 = BLACK/GRAY/WHITE | S = shared/independent", - "N = nova ROI | [ ] = troca ROI | D = apaga ROI | T = liga/desliga ROI", - "P ou SPACE = salva JSON | R = defaults | Q/Esc = sai", - ]) - - return lines - - -def aggregate_roi_stats(stats_list: list[dict]) -> dict: - valid = [s for s in stats_list if s.get("valid")] - if not valid: - return {"valid": False, "count": 0} - - p50 = np.array([s["p50"] for s in valid], dtype=np.float32) - p95 = np.array([s["p95"] for s in valid], dtype=np.float32) - sat = np.array([s["sat_pct"] for s in valid], dtype=np.float32) - dark = np.array([s["dark_pct"] for s in valid], dtype=np.float32) - std = np.array([s["std"] for s in valid], dtype=np.float32) - - return { - "valid": True, - "count": len(valid), - "p50": float(np.median(p50)), - "p95": float(np.median(p95)), - "sat_pct": float(np.median(sat)), - "dark_pct": float(np.median(dark)), - "std": float(np.median(std)), - "p50_spread": float(p50.max() - p50.min()) if len(p50) > 1 else 0.0, - "p50_min": float(p50.min()), - "p50_max": float(p50.max()), - } - - -def stats_lines_for_mode(data, decoded, mode: str, patch_type: str | None = None): - lines = [] - - for role in ROLES: - _, img = get_image_by_role(decoded, role) - if img is None: - lines.append(f"{role.upper()}: sem frame") - continue - - h, w = img.shape[:2] - - if mode == "global": - roi_pct = get_global_roi_for_role(data, role) - if not roi_pct: - lines.append(f"{role.upper()}: sem ROI") - continue - roi = pct_to_px(roi_pct, w, h) - st = compute_stats(img, roi) - lines.append( - f"{role.upper()}: p50={st['p50']:.3f} p95={st['p95']:.3f} " - f"sat={st['sat_pct']:.2f}% dark={st['dark_pct']:.1f}%" - ) - continue - - entries = get_patch_roi_entries_for_role(data, patch_type, role, enabled_only=True) - if not entries: - lines.append(f"{role.upper()}: sem ROI ativa") - continue - - stats = [] - p50_each = [] - for entry in entries: - roi_pct = entry.get("roi_pct", {}) - if not roi_pct: - continue - roi = pct_to_px(roi_pct, w, h) - st = compute_stats(img, roi) - stats.append(st) - if st.get("valid"): - p50_each.append(st["p50"]) - - ag = aggregate_roi_stats(stats) - if not ag.get("valid"): - lines.append(f"{role.upper()}: ROIs invalidas") - continue - - mini = ",".join(f"{v:.2f}" for v in p50_each[:4]) - if len(p50_each) > 4: - mini += ",..." - - lines.append( - f"{role.upper()}: n={ag['count']} p50_med={ag['p50']:.3f} " - f"spread={ag['p50_spread']:.3f} sat_med={ag['sat_pct']:.2f}%" - ) - lines.append(f" p50_each=[{mini}]") - - return lines - -def rect_inside(rect, x, y): - if rect is None: - return False - x0, y0, x1, y1 = rect - return x0 <= x < x1 and y0 <= y < y1 - - -def local_from_rect(rect, x, y): - x0, y0, _, _ = rect - return int(x - x0), int(y - y0) - - -# ============================================================ -# Main -# ============================================================ - -def main(): - parser = argparse.ArgumentParser( - description="Ferramenta visual para parametrizar o radiometric_config global ou por 3 patches.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument("--fps", type=int, default=20) - parser.add_argument("--width", type=int, default=1280) - parser.add_argument("--height", type=int, default=800) - parser.add_argument("--bayer", default="RGGB", choices=["GBRG", "GRBG", "RGGB", "BGGR"]) - parser.add_argument("--capture_mode", default="AUTO", choices=["AUTO", "SINGLE", "DOUBLE", "TRIPLE"]) - parser.add_argument("--raw_policy", default="allow_single", choices=["allow_single", "require_triple"]) - parser.add_argument("--module_calibration_json", default="calibration/module_params.json") - parser.add_argument("--out_json", default="calibration/radiometric_config.json") - parser.add_argument("--load_json", default="") - parser.add_argument("--preview_scale", type=float, default=0.75) - - args = parser.parse_args() - - config_path = args.load_json or args.out_json - data = load_or_default_config(config_path) - - mode = "global" - selected_target = "gray" - selected_roi_index = 0 - edit_role = "rgb" - drag_role = None - beauty_preview = True - visual_previews_last = {} - raw_meta_last = {} - - panel_rects = {"rgb": None, "re": None, "nir": None, "data": None} - dragging = False - drag_start = None - drag_rect_local = None - - last_msg = "" - last_msg_t = 0.0 - last_frame_id = -1 - decoded_last = {} - - window_name = "Radiometric Config Tool" - - def on_mouse(event, x, y, flags, param): - nonlocal dragging, drag_start, drag_rect_local, last_msg, last_msg_t, data, drag_role, selected_roi_index - - # Coordenadas vêm depois do preview_scale. Reescala para board real. - if args.preview_scale != 1.0: - x = int(x / args.preview_scale) - y = int(y / args.preview_scale) - - edit_rect = panel_rects.get(edit_role) - if not rect_inside(edit_rect, x, y): - return - - lx, ly = local_from_rect(edit_rect, x, y) - - if event == cv2.EVENT_LBUTTONDOWN: - dragging = True - drag_role = edit_role - drag_start = (lx, ly) - drag_rect_local = (lx, ly, lx + 1, ly + 1) - - elif event == cv2.EVENT_MOUSEMOVE and dragging: - sx, sy = drag_start - drag_rect_local = (sx, sy, lx, ly) - - elif event == cv2.EVENT_LBUTTONUP and dragging: - dragging = False - sx, sy = drag_start - rect = (sx, sy, lx, ly) - drag_rect_local = None - - # Descobre tamanho local do painel da camera editada. - edit_rect = panel_rects.get(drag_role or edit_role) - if edit_rect is None: - return - - _, _, x1, y1 = edit_rect - x0r, y0r, _, _ = edit_rect - w = x1 - x0r - h = y1 - y0r - - roi_pct = px_to_pct(rect, w, h) - - role_to_save = normalize_role(drag_role or edit_role) - - if mode == "global": - set_global_roi_for_role(data, role_to_save, roi_pct) - last_msg = f"GLOBAL ROI {role_to_save.upper()} atualizada: {roi_pct}" - else: - selected_roi_index = set_patch_roi_for_role( - data, selected_target, role_to_save, roi_pct, index=selected_roi_index, append=False - ) - last_msg = ( - f"{selected_target.upper()} ROI {role_to_save.upper()} " - f"#{selected_roi_index + 1} atualizada: {roi_pct}" - ) - - drag_role = None - last_msg_t = time.time() - - cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) - cv2.setMouseCallback(window_name, on_mouse) - - try: - with MultiSpectralClient( - width=args.width, - height=args.height, - bayer=args.bayer, - fps=args.fps, - frame_type="RAW_BRUTO", - output_dtype="uint8", - capture_mode=args.capture_mode, - raw_policy=args.raw_policy, - module_calibration_json=args.module_calibration_json - ) as cam: - - validate_module_ready(cam.get_status(), args.raw_policy) - - while True: - raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0) - visual_previews = {} - - try: - if isinstance(raw_frame, dict): - visual_previews = cam.build_visual_preview_from_raw(raw_frame, raw_meta) - except Exception as e: - visual_previews = {} - print(f"[WARN] Falha ao gerar beauty preview: {e}") - - if raw_meta is not None and raw_meta.get("frame_id") != last_frame_id: - last_frame_id = raw_meta.get("frame_id") - decoded_last = decoded - visual_previews_last = visual_previews - raw_meta_last = raw_meta - - if decoded_last: - board = build_board( - decoded=decoded_last, - data=data, - mode=mode, - selected_target=selected_target, - edit_role=edit_role, - selected_roi_index=selected_roi_index, - drag_rect_local=drag_rect_local, - drag_role=drag_role, - panel_rects=panel_rects, - preview_scale=args.preview_scale, - visual_previews=visual_previews_last, - meta=raw_meta_last, - beauty_preview=beauty_preview, - ) - - if last_msg and (time.time() - last_msg_t) < 2.5: - cv2.putText(board, last_msg, (18, board.shape[0] - 20), - cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 2, cv2.LINE_AA) - - cv2.imshow(window_name, board) - else: - blank = np.zeros((720, 1280, 3), dtype=np.uint8) - overlay_hud(blank, ["Aguardando frames..."], x=40, y=80, font_scale=1.0) - cv2.imshow(window_name, blank) - - k = cv2.waitKey(1) & 0xFF - - if k in (ord("q"), ord("Q"), 27): - break - - elif k in (ord("m"), ord("M")): - mode = "patches" if mode == "global" else "global" - - if mode == "global": - set_active_profile_name(data, "global_scene_mode") - else: - set_active_profile_name(data, "three_reference_patches_mode") - - update_root_radiometric_config(data) - - selected_roi_index = 0 - last_msg = f"Modo -> {mode} | active_profile={data['active_profile']}" - last_msg_t = time.time() - - elif k == ord("1"): - mode = "patches" - selected_target = "black" - selected_roi_index = 0 - set_active_profile_name(data, "three_reference_patches_mode") - update_root_radiometric_config(data) - last_msg = "Selecionado: BLACK" - last_msg_t = time.time() - - elif k == ord("2"): - mode = "patches" - selected_target = "gray" - selected_roi_index = 0 - set_active_profile_name(data, "three_reference_patches_mode") - update_root_radiometric_config(data) - last_msg = "Selecionado: GRAY" - last_msg_t = time.time() - - elif k == ord("3"): - mode = "patches" - selected_target = "white" - selected_roi_index = 0 - set_active_profile_name(data, "three_reference_patches_mode") - update_root_radiometric_config(data) - last_msg = "Selecionado: WHITE" - last_msg_t = time.time() - - elif k in (ord("s"), ord("S")): - cfg = data.get("global_scene_mode", {}).get("radiometric_config", {}) - curr = str(cfg.get("spectral_control_mode", "shared")).lower() - set_shared_mode(data, shared=(curr != "shared")) - new_mode = ( - data.get("global_scene_mode", {}) - .get("radiometric_config", {}) - .get("spectral_control_mode", "shared") - ) - last_msg = f"spectral_control_mode -> {new_mode}" - last_msg_t = time.time() - - elif k in (ord("r"), ord("R")): - # Restaura somente a parte radiométrica, preservando o restante do module_params quando existir. - module_contract = is_module_params_contract(data) - preserved = dict(data) if module_contract else {} - - if module_contract: - preserved["radiometric_config"] = sanitize_radiometric_config(DEFAULT_RADIOMETRIC_CONFIG) - preserved["patch_normalization"] = deep_clone(DEFAULT_PATCH_NORMALIZATION) - preserved["active_profile"] = "three_reference_patches_mode" - preserved["global_scene_mode"] = default_profile_global() - preserved["three_reference_patches_mode"] = default_profile_patches() - data = preserved - else: - data = { - "schema": PROFILE_SCHEMA, - "saved_at": now_str(), - "active_profile": "three_reference_patches_mode", - "global_scene_mode": default_profile_global(), - "three_reference_patches_mode": default_profile_patches(), - "patch_normalization": deep_clone(DEFAULT_PATCH_NORMALIZATION), - } - - update_root_radiometric_config(data) - last_msg = "Defaults restaurados" - last_msg_t = time.time() - - elif k in (ord("p"), ord("P"), 32): - save_config(args.out_json, data) - last_msg = f"Salvo em: {args.out_json}" - last_msg_t = time.time() - print(f"[OK] radiometric config salvo em: {args.out_json}") - - elif k in (ord("c"), ord("C")): - idx = ROLES.index(edit_role) if edit_role in ROLES else 0 - edit_role = ROLES[(idx + 1) % len(ROLES)] - selected_roi_index = 0 - last_msg = f"Camera editada -> {edit_role.upper()}" - last_msg_t = time.time() - - elif mode == "patches" and k in (ord("n"), ord("N")): - selected_roi_index = add_empty_patch_roi_slot(data, selected_target, edit_role) - last_msg = f"Nova ROI {selected_target.upper()}/{edit_role.upper()} #{selected_roi_index + 1}. Arraste para posicionar." - last_msg_t = time.time() - - elif mode == "patches" and k in (ord("["), ord(",")): - entries = get_patch_roi_entries_for_role(data, selected_target, edit_role, enabled_only=False) - if entries: - selected_roi_index = (selected_roi_index - 1) % len(entries) - last_msg = f"ROI selecionada -> #{selected_roi_index + 1}/{len(entries)}" - else: - last_msg = "Nenhuma ROI para selecionar" - last_msg_t = time.time() - - elif mode == "patches" and k in (ord("]"), ord(".")): - entries = get_patch_roi_entries_for_role(data, selected_target, edit_role, enabled_only=False) - if entries: - selected_roi_index = (selected_roi_index + 1) % len(entries) - last_msg = f"ROI selecionada -> #{selected_roi_index + 1}/{len(entries)}" - else: - last_msg = "Nenhuma ROI para selecionar" - last_msg_t = time.time() - - elif mode == "patches" and k in (ord("d"), ord("D")): - ok, n_left = delete_patch_roi_for_role(data, selected_target, edit_role, selected_roi_index) - selected_roi_index = clamp(selected_roi_index, 0, max(0, n_left - 1)) - last_msg = f"ROI apagada. Restam {n_left}." if ok else "Nenhuma ROI para apagar" - last_msg_t = time.time() - - elif mode == "patches" and k in (ord("t"), ord("T")): - ok, enabled = toggle_patch_roi_enabled_for_role(data, selected_target, edit_role, selected_roi_index) - last_msg = f"ROI #{selected_roi_index + 1} enabled={enabled}" if ok else "Nenhuma ROI para alternar" - last_msg_t = time.time() - - elif k in (ord("v"), ord("V")): - beauty_preview = not beauty_preview - last_msg = f"Beauty Preview -> {beauty_preview}" - last_msg_t = time.time() - - finally: - cv2.destroyAllWindows() - print("Fim da parametrizacao radiometrica.") - - -if __name__ == "__main__": - main()