From 7bcd259d9f9ebb58f651dd98d67ace73e29fa031 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Mon, 20 Apr 2026 15:53:28 -0300 Subject: [PATCH] ajustes hierarquia de arquivos para treinamento de IA --- Python/OAK/datasets/_0_capture_raw.py | 327 -------- Python/OAK/datasets/_2_create_full_mask.py | 2 +- .../OAK/datasets/_4_group_images_by_class.py | 2 +- Python/OAK/datasets/_5_augmentation.py | 2 +- Python/OAK/datasets/_6_normalize.py | 4 +- Python/OAK/datasets/_7_split.py | 2 +- Python/OAK/datasets/_8_train_segformer_b3.py | 2 +- .../datasets/_8_train_segformer_b3_dual.py | 2 +- Python/OAK/datasets/_9_test_segformer_b3.py | 2 +- .../OAK/datasets/_9_test_segformer_b3_dual.py | 2 +- Python/OAK/datasets/config_oak.json | 4 +- Python/OAK/datasets/gal5000/_0_capture.py | 422 ++++++++++ .../_1_weeds_pair_sorter.py} | 0 .../_2_create_full_mask.py} | 10 +- .../{ => gal5000}/_2_ingest_new_masks.py | 10 +- .../_3_copy_selected_images_to_mask.py | 6 +- .../_4_group_images_by_class.py} | 4 +- .../_5_augmentation.py} | 16 +- .../_6_normalize.py} | 11 +- .../{_7_split_raw.py => gal5000/_7_split.py} | 14 +- .../_8_train_segformer_b3.py} | 9 +- .../_9_test_segformer_b3.py} | 6 +- Python/OAK/datasets/{ => gal5000}/config.json | 6 +- .../datasets/gal5000/regenerate_previews.py | 390 +++++++++ .../multiespec_module/_1_weeds_pair_sorter.py | 669 +++++++++++++++ .../multiespec_module/_2_create_full_mask.py | 318 +++++++ .../multiespec_module/_2_ingest_new_masks.py | 205 +++++ .../_3_copy_selected_images_to_mask.py | 328 ++++++++ .../_4_group_images_by_class.py | 322 ++++++++ .../multiespec_module/_5_augmentation.py | 708 ++++++++++++++++ .../multiespec_module/_6_normalize.py | 325 ++++++++ .../datasets/multiespec_module/_7_split.py | 451 ++++++++++ .../_8_train_segformer_b3.py | 779 ++++++++++++++++++ .../multiespec_module/_9_test_segformer_b3.py | 535 ++++++++++++ .../datasets/multiespec_module/config.json | 19 + Python/raspi/multispectral_service.py | 146 ---- Python/raspi/stream_receiver.py | 139 ---- Python/raspi/test_capture_raw.py | 19 - Python/raspi/test_service.py | 29 - Python/raspi/test_stream_raw.py | 55 -- 40 files changed, 5528 insertions(+), 774 deletions(-) delete mode 100644 Python/OAK/datasets/_0_capture_raw.py create mode 100644 Python/OAK/datasets/gal5000/_0_capture.py rename Python/OAK/datasets/{_1_weeds_pair_sorter_raw.py => gal5000/_1_weeds_pair_sorter.py} (100%) rename Python/OAK/datasets/{_2_create_full_mask_raw.py => gal5000/_2_create_full_mask.py} (96%) rename Python/OAK/datasets/{ => gal5000}/_2_ingest_new_masks.py (95%) rename Python/OAK/datasets/{ => gal5000}/_3_copy_selected_images_to_mask.py (97%) rename Python/OAK/datasets/{_4_group_images_by_class_raw.py => gal5000/_4_group_images_by_class.py} (98%) rename Python/OAK/datasets/{_5_augmentation_raw.py => gal5000/_5_augmentation.py} (97%) rename Python/OAK/datasets/{_6_normalize_raw.py => gal5000/_6_normalize.py} (97%) rename Python/OAK/datasets/{_7_split_raw.py => gal5000/_7_split.py} (96%) rename Python/OAK/datasets/{_8_train_segformer_b3_raw.py => gal5000/_8_train_segformer_b3.py} (98%) rename Python/OAK/datasets/{_9_test_segformer_b3_raw.py => gal5000/_9_test_segformer_b3.py} (98%) rename Python/OAK/datasets/{ => gal5000}/config.json (75%) create mode 100644 Python/OAK/datasets/gal5000/regenerate_previews.py create mode 100644 Python/OAK/datasets/multiespec_module/_1_weeds_pair_sorter.py create mode 100644 Python/OAK/datasets/multiespec_module/_2_create_full_mask.py create mode 100644 Python/OAK/datasets/multiespec_module/_2_ingest_new_masks.py create mode 100644 Python/OAK/datasets/multiespec_module/_3_copy_selected_images_to_mask.py create mode 100644 Python/OAK/datasets/multiespec_module/_4_group_images_by_class.py create mode 100644 Python/OAK/datasets/multiespec_module/_5_augmentation.py create mode 100644 Python/OAK/datasets/multiespec_module/_6_normalize.py create mode 100644 Python/OAK/datasets/multiespec_module/_7_split.py create mode 100644 Python/OAK/datasets/multiespec_module/_8_train_segformer_b3.py create mode 100644 Python/OAK/datasets/multiespec_module/_9_test_segformer_b3.py create mode 100644 Python/OAK/datasets/multiespec_module/config.json delete mode 100644 Python/raspi/multispectral_service.py delete mode 100644 Python/raspi/stream_receiver.py delete mode 100644 Python/raspi/test_capture_raw.py delete mode 100644 Python/raspi/test_service.py delete mode 100644 Python/raspi/test_stream_raw.py diff --git a/Python/OAK/datasets/_0_capture_raw.py b/Python/OAK/datasets/_0_capture_raw.py deleted file mode 100644 index 26c4f02bf..000000000 --- a/Python/OAK/datasets/_0_capture_raw.py +++ /dev/null @@ -1,327 +0,0 @@ -import os -import time -import json -import argparse -from datetime import datetime - -import numpy as np -import cv2 - -from raw_segformer_service import make_bgr_preview_from_raw -from gal5000.gal_service import Gal5000Camera # ajuste o nome do módulo se estiver diferente - - -# ========================= -# Helpers gerais -# ========================= - -def clamp(v, lo, hi): - return lo if v < lo else hi if v > hi else v - - -def ts_name() -> str: - """Timestamp legível e único para nome de arquivo.""" - return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] - - -def norm8(x: np.ndarray, p_lo=2, p_hi=98) -> np.ndarray: - """ - Normaliza um canal (float32 0..1 ou uint8) em 0..255 com cortes por percentil. - Pensado pra deixar o preview bonitinho sem estourar tudo. - """ - x = np.asarray(x) - if x.dtype != np.float32 and x.dtype != np.float64: - x = x.astype(np.float32) - - # Se o canal já está em 0..1, escala pra 0..255 antes de cortar - if x.max() <= 1.5: - x = x * 255.0 - - lo = np.percentile(x, p_lo) - hi = np.percentile(x, p_hi) - - if hi <= lo + 1e-3: - y = x - else: - y = (x - lo) * (255.0 / (hi - lo)) - - return np.clip(y, 0, 255).astype(np.uint8) - - -def overlay_hud( - img_bgr: np.ndarray, - lines: list[str], - base_h: int = 720, - base_font_scale: float = 0.75, - base_line_step: int = 28, -): - """ - Escreve textos empilhados no canto superior esquerdo, - ajustando o tamanho do texto de acordo com a altura da imagem. - - base_h: altura de referência (ex: 720 ou a RAW_H original). - """ - h, w = img_bgr.shape[:2] - - # Fator de escala com base na altura atual - scale = h / float(base_h) - - # Evita ficar microscópico em resoluções muito baixas - scale = max(scale, 0.4) - - font_scale = base_font_scale * scale - line_step = int(base_line_step * scale) - - # Espessuras proporcionais - thick_outline = max(1, int(3 * scale)) - thick_text = max(1, int(2 * scale)) - - # Margem superior / esquerda também escaladas - y = int(24 * scale) - x = int(12 * scale) - - for s in lines: - # contorno preto - cv2.putText(img_bgr, s, (x, y), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 0, 0), thick_outline, cv2.LINE_AA) - # texto branco - cv2.putText(img_bgr, s, (x, y), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), thick_text, cv2.LINE_AA) - y += line_step - - -def save_sample_raw4( - base_dir: str, - raw4: np.ndarray, - preview_bgr: np.ndarray, - meta: dict, -): - """ - Salva: - - RAW4 como .raw float32 (4,H,W) - - preview RGB como .png - - metadados como .json - dentro de base_dir. - """ - os.makedirs(base_dir, exist_ok=True) - name = ts_name() - - raw_path = os.path.join(base_dir, f"{name}.raw") - png_path = os.path.join(base_dir, f"{name}.png") - json_path = os.path.join(base_dir, f"{name}.json") - - # RAW4 - #np.save(raw_path, raw4.astype(np.float32)) - raw4.astype(np.float32).tofile(raw_path) - - # Preview - cv2.imwrite(png_path, preview_bgr) - - # Metadados - with open(json_path, "w", encoding="utf-8") as f: - json.dump(meta, f, ensure_ascii=False, indent=2) - - return raw_path, png_path, json_path - - -# ========================= -# MAIN -# ========================= - -def main(): - parser = argparse.ArgumentParser( - description="Captura de dataset RAW4 (SegFormer B0) usando Gal5000 + AutoExposure.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument("--cana", required=True, choices=["baixa", "media", "alta"], help="Estado da cana no momento da coleta.") - parser.add_argument("--horario", required=True, choices=["cedo", "meio_dia", "entardecer", "nublado"], help="Janela de iluminação / horário da coleta.") - parser.add_argument("--out_root", default="dataset", help="Pasta raiz do dataset.") - parser.add_argument("--dll_dir", default=r"C:\ZendionInc\agrobot_base\Python\gal5000\dlls", help="Pasta onde está a VT_SDK64.dll (usada pelo Gal5000Camera).") - parser.add_argument("--dll_name", default="VT_SDK64.dll", help="Nome da DLL da câmera.") - parser.add_argument("--interval", type=float, default=1.0, help="Intervalo em segundos para auto-save quando ligado.") - parser.add_argument("--no_ae", action="store_true", help="Desliga o AutoExposure do service (por padrão ele vem ligado).") - parser.add_argument("--upscale", type=int, default=2, help="Fator de upscale visual do preview.") - - args = parser.parse_args() - - # ===== config ===== - with open("config.json", "r", encoding="utf-8") as f: - config = json.load(f) - MODELO = config["camera"] - RAW_W = config["raw_size"][0] - RAW_H = config["raw_size"][1] - - # Define diretório de sessão: - # dataset/cana_/// - session_dir = os.path.join(MODELO, args.out_root, "brutas", f"cana_{args.cana}", args.horario, datetime.now().strftime("%Y%m%d")) - os.makedirs(session_dir, exist_ok=True) - - print("============================================") - print("Coleta de dataset RAW4 - SegFormer B0") - print(f"Cana : {args.cana}") - print(f"Horário : {args.horario}") - print(f"Saída : {session_dir}") - print("============================================") - - window_name = "Dataset Capture - RAW4 (C/SPACE=save | A=auto-save | E=AE | Q=quit)" - cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) - - auto_save = False - last_auto_t = 0.0 - upscale = args.upscale - - # Estatísticas simples de FPS - t_fps = time.time() - frames = 0 - fps = 0.0 - - last_msg = "" - last_msg_t = 0.0 - - try: - cam = Gal5000Camera(dll_dir=args.dll_dir, dll_name=args.dll_name, raw_w=RAW_W, raw_h=RAW_H, use_auto_exposure=(not args.no_ae)) - with cam: - print("[CAM] Status inicial:", cam.get_status()) - - # opcional: você pode ligar streaming se quiser, mas grab_raw4 já usa single-frame - cam.configure_fps(20) - cam.start_streaming() - - while True: - t0 = time.time() - raw4_base, dbg = cam.grab_raw4(out_h=RAW_H, out_w=RAW_W, timeout_ms=2000, do_ae=True) - t1 = time.time() - - ae_dbg = dbg.get("ae", {}) or {} - exp_raw = dbg.get("exp_raw", None) - gain_a = dbg.get("gain_a", None) - gain_d = dbg.get("gain_d", None) - - apply_ir_comp = True - ir_k_r = 0.4 - ir_k_g = 0.1 - ir_k_b = 0.5 - bgr = make_bgr_preview_from_raw(raw4_base, rgirb=True, preview_fast=upscale > 0, preview_scale=upscale, apply_ir_comp=apply_ir_comp, ir_k_r=ir_k_r, ir_k_g=ir_k_g, ir_k_b=ir_k_b) - - # FPS - frames += 1 - dt_fps = time.time() - t_fps - if dt_fps >= 1.0: - fps = frames / dt_fps - frames = 0 - t_fps = time.time() - - # AE info - p95_disp = ae_dbg.get("p95_ema", ae_dbg.get("p95", 0.0)) - sat_disp = ae_dbg.get("sat", 0.0) - hold = ae_dbg.get("hold", False) - - ae_on = cam.is_auto_exposure_enabled() - - # HUD principal - lines = [ - f"CANA: {args.cana} | HORA: {args.horario} | Pasta: {os.path.basename(session_dir)}", - f"AE: {'ON' if ae_on else 'OFF'} | AutoSave: {'ON' if auto_save else 'OFF'} | Intervalo: {args.interval:.1f}s", - f"exp_raw={exp_raw} gain_a={gain_a} gain_d={gain_d} | FPS={fps:.1f}", - f"AEdbg: p95={p95_disp:.1f} sat={sat_disp:.3f} hold={hold}", - "Keys: C/SPACE=save | A=auto-save | E=AE toggle | M=preview scale | Q/Esc=quit", - ] - overlay_hud(bgr, lines, base_h=RAW_H) - - # Mensagem rápida (ex: arquivo salvo) - if last_msg and (time.time() - last_msg_t) < 2.0: - cv2.putText(bgr, last_msg, (12, bgr.shape[0] - 18), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, cv2.LINE_AA) - - cv2.imshow(window_name, bgr) - - # Auto-save - now = time.time() - if auto_save and (now - last_auto_t) >= args.interval: - meta = { - "ts": datetime.now().isoformat(timespec="milliseconds"), - "cana": args.cana, - "horario": args.horario, - "raw4_shape": list(raw4_base.shape), - "out_h": RAW_H, - "out_w": RAW_W, - "ae_enabled": bool(ae_on), - "exp_raw": int(exp_raw) if exp_raw is not None else None, - "gain_a": int(gain_a) if gain_a is not None else None, - "gain_d": int(gain_d) if gain_d is not None else None, - "apply_ir_comp": apply_ir_comp, - "ir_k_r": ir_k_r, - "ir_k_g": ir_k_g, - "ir_k_b": ir_k_b, - "ae_dbg": { - k: (float(v) if isinstance(v, (int, float, np.floating)) else v) - for k, v in ae_dbg.items() - }, - "note": "autosave", - } - rgb_clean_bgr_save = make_bgr_preview_from_raw(raw4_base, rgirb=True, preview_fast=False, apply_ir_comp=apply_ir_comp, ir_k_r=ir_k_r, ir_k_g=ir_k_g, ir_k_b=ir_k_b) - raw_path, _, _ = save_sample_raw4(session_dir, raw4_base, rgb_clean_bgr_save, meta) - last_msg = f"SALVO (auto): {os.path.basename(raw_path)}" - last_msg_t = now - last_auto_t = now - - # Teclado - k = cv2.waitKey(1) & 0xFF - if k in (ord("q"), ord("Q"), 27): # Q ou ESC - break - - elif k in (ord("a"), ord("A")): - auto_save = not auto_save - last_msg = f"AutoSave -> {'ON' if auto_save else 'OFF'}" - last_msg_t = time.time() - - elif k in (ord("e"), ord("E")): - cam.enable_auto_exposure(not ae_on) - last_msg = f"AE -> {'ON' if cam.is_auto_exposure_enabled() else 'OFF'}" - last_msg_t = time.time() - - elif k in (ord("m"), ord("M")): - upscale = 0 if upscale else args.upscale - last_msg = f"Preview UPSCALE -> {upscale}" - last_msg_t = time.time() - - elif k in (ord("c"), ord("C"), 32): # C ou SPACE - meta = { - "ts": datetime.now().isoformat(timespec="milliseconds"), - "cana": args.cana, - "horario": args.horario, - "raw4_shape": list(raw4_base.shape), - "out_h": RAW_H, - "out_w": RAW_W, - "ae_enabled": bool(ae_on), - "exp_raw": int(exp_raw) if exp_raw is not None else None, - "gain_a": int(gain_a) if gain_a is not None else None, - "gain_d": int(gain_d) if gain_d is not None else None, - "apply_ir_comp": apply_ir_comp, - "ir_k_r": ir_k_r, - "ir_k_g": ir_k_g, - "ir_k_b": ir_k_b, - "ae_dbg": { - k2: (float(v2) if isinstance(v2, (int, float, np.floating)) else v2) - for k2, v2 in ae_dbg.items() - }, - "note": "manual", - } - rgb_clean_bgr_save = make_bgr_preview_from_raw(raw4_base, rgirb=True, preview_fast=False) - raw_path, _, _ = save_sample_raw4(session_dir, raw4_base, rgb_clean_bgr_save, meta) - last_msg = f"SALVO (manual): {os.path.basename(raw_path)}" - last_msg_t = time.time() - - # você pode adicionar mais atalhos depois (ex: mudar intervalo, etc.) - - # Só pra não ficar rodando a 1000 FPS na UI - # mas sem travar muito a captura - dt_loop = time.time() - t0 - if dt_loop < 0.001: - time.sleep(0.001) - - finally: - cv2.destroyAllWindows() - print("Fim da captura.") - - -if __name__ == "__main__": - main() diff --git a/Python/OAK/datasets/_2_create_full_mask.py b/Python/OAK/datasets/_2_create_full_mask.py index e98693e02..43108692a 100644 --- a/Python/OAK/datasets/_2_create_full_mask.py +++ b/Python/OAK/datasets/_2_create_full_mask.py @@ -7,7 +7,7 @@ import argparse import numpy as np # ⚙️ Configurações -with open("config.json", "r") as f: +with open("config_oak.json", "r") as f: config = json.load(f) MODELO = config["camera"] diff --git a/Python/OAK/datasets/_4_group_images_by_class.py b/Python/OAK/datasets/_4_group_images_by_class.py index e1382d3a0..392a46626 100644 --- a/Python/OAK/datasets/_4_group_images_by_class.py +++ b/Python/OAK/datasets/_4_group_images_by_class.py @@ -30,7 +30,7 @@ from utils import carregar_labelmap_completo, converter_mask_rgb_para_ids # ====================== Configurações base ====================== def carregar_config_e_paths(): - with open("config.json", "r", encoding="utf-8") as f: + with open("config_oak.json", "r", encoding="utf-8") as f: config = json.load(f) MODELO = config.get("camera") USE_MASKS2 = config.get("dual_head", False) diff --git a/Python/OAK/datasets/_5_augmentation.py b/Python/OAK/datasets/_5_augmentation.py index 6cc449a7e..99613f26d 100644 --- a/Python/OAK/datasets/_5_augmentation.py +++ b/Python/OAK/datasets/_5_augmentation.py @@ -29,7 +29,7 @@ import albumentations as A import argparse # ⚙️ Configurações -with open("config.json", "r", encoding="utf-8") as f: +with open("config_oak.json", "r", encoding="utf-8") as f: config = json.load(f) MODELO = config.get("camera", ".") USE_MASKS2 = config.get("dual_head", False) diff --git a/Python/OAK/datasets/_6_normalize.py b/Python/OAK/datasets/_6_normalize.py index 2a2845045..0590b873e 100644 --- a/Python/OAK/datasets/_6_normalize.py +++ b/Python/OAK/datasets/_6_normalize.py @@ -27,7 +27,7 @@ import numpy as np from utils import carregar_labelmap_completo, converter_mask_rgb_para_ids # ⚙️ Configurações -with open("config.json", "r", encoding="utf-8") as f: +with open("config_oak.json", "r", encoding="utf-8") as f: config = json.load(f) MODELO = config["camera"] MODEL_NAME = config["model_name"] @@ -351,7 +351,7 @@ def main(args): # Se quiser, você pode nomear os canais explicitamente # dependendo da convenção do raw4: - channel_names = ["R", "G", "IR", "B"] + channel_names = ["R", "G", "B"] stats = { "channels": channel_names[:len(mean_list)], diff --git a/Python/OAK/datasets/_7_split.py b/Python/OAK/datasets/_7_split.py index 61937183d..b3d2df071 100644 --- a/Python/OAK/datasets/_7_split.py +++ b/Python/OAK/datasets/_7_split.py @@ -33,7 +33,7 @@ import random import argparse # ⚙️ Configurações -with open("config.json", "r", encoding="utf-8") as f: +with open("config_oak.json", "r", encoding="utf-8") as f: config = json.load(f) MODELO = config.get("camera") USE_MASKS2 = config.get("dual_head", False) diff --git a/Python/OAK/datasets/_8_train_segformer_b3.py b/Python/OAK/datasets/_8_train_segformer_b3.py index 533b47e66..020053d5f 100644 --- a/Python/OAK/datasets/_8_train_segformer_b3.py +++ b/Python/OAK/datasets/_8_train_segformer_b3.py @@ -258,7 +258,7 @@ def run_one_epoch(model: nn.Module, def main(): parser = argparse.ArgumentParser() - parser.add_argument("--config", default="config.json") + parser.add_argument("--config", default="config_oak.json") parser.add_argument("--epochs", type=int, default=120) parser.add_argument("--batch", type=int, default=1) # <<< default seguro pra 8GB parser.add_argument("--lr", type=float, default=6e-5) diff --git a/Python/OAK/datasets/_8_train_segformer_b3_dual.py b/Python/OAK/datasets/_8_train_segformer_b3_dual.py index 376a3b2d2..0a912fa01 100644 --- a/Python/OAK/datasets/_8_train_segformer_b3_dual.py +++ b/Python/OAK/datasets/_8_train_segformer_b3_dual.py @@ -595,7 +595,7 @@ def apply_warmup(optimizer, base_lr: float, epoch: int, warmup_epochs: int): def main(): ap = argparse.ArgumentParser() - ap.add_argument("--config", default="config.json") + ap.add_argument("--config", default="config_oak.json") ap.add_argument("--epochs", type=int, default=120) ap.add_argument("--batch", type=int, default=4) ap.add_argument("--num_workers", type=int, default=4) diff --git a/Python/OAK/datasets/_9_test_segformer_b3.py b/Python/OAK/datasets/_9_test_segformer_b3.py index 74addcc7b..5c9c459ef 100644 --- a/Python/OAK/datasets/_9_test_segformer_b3.py +++ b/Python/OAK/datasets/_9_test_segformer_b3.py @@ -215,7 +215,7 @@ def main(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Lê config do projeto (mesmo padrão do fastscnn) - with open("config.json", "r") as f: + with open("config_oak.json", "r") as f: config = json.load(f) MODELO = config["camera"] diff --git a/Python/OAK/datasets/_9_test_segformer_b3_dual.py b/Python/OAK/datasets/_9_test_segformer_b3_dual.py index 364486f70..924947ccb 100644 --- a/Python/OAK/datasets/_9_test_segformer_b3_dual.py +++ b/Python/OAK/datasets/_9_test_segformer_b3_dual.py @@ -457,7 +457,7 @@ def main(): device = find_device() print("Device:", device) - with open("config.json", "r") as f: + with open("config_oak.json", "r") as f: config = json.load(f) MODELO = config["camera"] MODEL_NAME = config["model_name"] # ex: "segformer_b3" diff --git a/Python/OAK/datasets/config_oak.json b/Python/OAK/datasets/config_oak.json index c7cb152bf..340983cbb 100644 --- a/Python/OAK/datasets/config_oak.json +++ b/Python/OAK/datasets/config_oak.json @@ -1,7 +1,7 @@ { "camera": "oak-d", "modelo": "segformer_b0", - "model_name": "nav_big", + "model_name": "nav_mit", "dual_head": false, "main_class_name": "navegavel", "es_classes": "", @@ -13,5 +13,5 @@ "shaves": 3, "channels": 3, "use_ndvi": false, - "backbone": "nvidia/segformer-b0-finetuned-ade-512-512" + "backbone": "nvidia/mit-b0" } \ No newline at end of file diff --git a/Python/OAK/datasets/gal5000/_0_capture.py b/Python/OAK/datasets/gal5000/_0_capture.py new file mode 100644 index 000000000..0867fb1b5 --- /dev/null +++ b/Python/OAK/datasets/gal5000/_0_capture.py @@ -0,0 +1,422 @@ +import os +import time +import json +import math +import ctypes as C +from ctypes import wintypes as W +from datetime import datetime + +import numpy as np +import cv2 + +# ========================= +# CONFIG +# ========================= +SDK_DIR = os.path.join(os.path.dirname(__file__), "dlls") +DLL_NAME = "VT_SDK64.dll" + +# Onde salvar o dataset +OUT_ROOT = os.path.join(os.path.dirname(__file__), "dataset") +SESSION_DIR = os.path.join(OUT_ROOT, datetime.now().strftime("%Y%m%d")) +os.makedirs(SESSION_DIR, exist_ok=True) + +# Camera scan/open +DEVICE_UDEF = 0 +DEVICE_INDEX = 0 +DATA_RAW = 0 + +# RAW geometry (se mudar no futuro, ajuste) +RAW_W = 2592 +RAW_H = 2056 + +TIMEOUT_MS = 2000 +WINDOW_NAME = "GAL5000 Dataset Capture (C/SPACE=save | A=auto-save | E=AE toggle | Q=quit)" + +# Preview +UPSCALE = 2 + +# Auto-save +CAPTURE_INTERVAL_S = 1.0 + +# Param IDs (VT_Param.h) +BUF_SIZE = 256 +PARAM_ID_SENSOR_EXPOSURETIMERAW = 0x00003010 +PARAM_ID_SENSOR_GAINANALOGRAW = 0x00003020 +PARAM_ID_SENSOR_GAINDIGITRAW = 0x0000302A + +# PARAM_VALUETYPE +VALUE_INT = 0 +VALUE_FLOAT = 1 +VALUE_STR = 2 + +# Exposure/Gain limits (ajuste depois conforme o sensor aceitar) +EXP_MIN = 1 +EXP_MAX = 20000 + +GAIN_A_MIN, GAIN_A_MAX = 0, 255 +GAIN_D_MIN, GAIN_D_MAX = 0, 255 + +# ========================= +# Helpers +# ========================= +def ck(ret: int, name: str): + if ret != 0: + raise RuntimeError(f"{name} falhou, ret={ret}") + +def ts_name() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] + +def clamp(v, lo, hi): + return lo if v < lo else hi if v > hi else v + +def norm8(x, p_lo=2, p_hi=98): + lo = np.percentile(x, p_lo) + hi = np.percentile(x, p_hi) + if hi <= lo + 1: + return x.astype(np.uint8) + y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + return np.clip(y, 0, 255).astype(np.uint8) + +def make_rgb_preview(raw: np.ndarray, upscale=2) -> np.ndarray: + # pattern: + # R G + # IR B + R = raw[0::2, 0::2] + G = raw[0::2, 1::2] + B = raw[1::2, 1::2] + + Rn, Gn, Bn = norm8(R), norm8(G), norm8(B) + bgr = np.dstack([Bn, Gn, Rn]) # OpenCV usa BGR + if upscale and upscale != 1: + bgr = cv2.resize(bgr, (bgr.shape[1]*upscale, bgr.shape[0]*upscale), interpolation=cv2.INTER_NEAREST) + return bgr + +def measure_raw_g_metrics(raw: np.ndarray): + """ + Mede brilho no canal G cru usando uma ROI na base (mais parecido com chão). + Retorna p90/p95 e fração saturada. + """ + G = raw[0::2, 1::2] # H/2 x W/2 + h2, w2 = G.shape + + # ROI: base da imagem, cortando laterais + y0, y1 = int(h2 * 0.55), int(h2 * 0.95) + x0, x1 = int(w2 * 0.15), int(w2 * 0.85) + roi = G[y0:y1, x0:x1] + + p90 = float(np.percentile(roi, 90)) + p95 = float(np.percentile(roi, 95)) + sat = float(np.mean(roi >= 250)) + return p90, p95, sat + +class RobustAE: + """ + Controle soft de exposure (sem depender do GET da camera): + - mede p95 do canal G cru em ROI + - usa EMA + deadband (pra não ficar "descendo até 16" como você viu) + - passo multiplicativo em log, com limite de passo + """ + def __init__(self, + exp_min=EXP_MIN, exp_max=EXP_MAX, + target_p95=140.0, + deadband=6.0, + k=0.12, + max_step=0.10, + ema_alpha=0.20, + sat_limit=0.01): + self.exp_min = exp_min + self.exp_max = exp_max + self.target = target_p95 + self.deadband = deadband + self.k = k + self.max_step = max_step + self.ema_alpha = ema_alpha + self.sat_limit = sat_limit + self.p95_ema = None + + def step(self, raw, exp_raw): + p90, p95, sat = measure_raw_g_metrics(raw) + + # EMA do p95 (estabiliza) + if self.p95_ema is None: + self.p95_ema = p95 + else: + self.p95_ema = (1 - self.ema_alpha) * self.p95_ema + self.ema_alpha * p95 + + e = self.target - self.p95_ema # erro em nível de pixel + + # deadband: segura a mão perto do alvo + if abs(e) <= self.deadband and sat <= self.sat_limit: + return exp_raw, {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "hold": True} + + # saturou: garante redução + if sat > self.sat_limit: + step = -min(self.max_step, 0.12) + else: + ratio = (self.target + 1e-6) / (self.p95_ema + 1e-6) + step = self.k * math.log(ratio) + step = max(-self.max_step, min(self.max_step, step)) + + new_exp = int(round(exp_raw * math.exp(step))) + new_exp = max(self.exp_min, min(self.exp_max, new_exp)) + + return new_exp, {"p90": p90, "p95": p95, "p95_ema": self.p95_ema, "sat": sat, "step": step, "hold": False} + +# ========================= +# STRUCTS + Param API +# ========================= +class VT_FRAMEINFO(C.Structure): + _fields_ = [ + ("lFrameID", W.DWORD), + ("lBufSize", W.DWORD), + ("lWidth", W.DWORD), + ("lHeight", W.DWORD), + ("lPixBits", C.c_ubyte), + ("_pad0", C.c_ubyte * 3), + ("pBufPtr", C.POINTER(C.c_ubyte)), + ("lFrameStatus", W.DWORD), + ("lPixType", W.DWORD), + ("lTimeStamp", W.DWORD), + ("_reserve", W.DWORD * 8), + ] + +class VT_DEVPARAM(C.Structure): + _fields_ = [ + ("bUseName", W.BOOL), + ("lParamByID", W.DWORD), + ("lParamByName", C.c_char * BUF_SIZE), + ] + +def devparam_by_id(pid: int) -> VT_DEVPARAM: + p = VT_DEVPARAM() + p.bUseName = False + p.lParamByID = pid + p.lParamByName = b"" + return p + +# ========================= +# DLL LOAD + prototypes +# ========================= +os.add_dll_directory(SDK_DIR) +dll = C.WinDLL(os.path.join(SDK_DIR, DLL_NAME)) +print("DLL carregada OK:", dll) + +dll.VT_DeviceScan.argtypes = [C.POINTER(C.c_ubyte), C.c_int] +dll.VT_DeviceScan.restype = C.c_int + +dll.VT_DeviceOpen.argtypes = [C.c_void_p, C.POINTER(W.HANDLE), C.c_int, C.c_int] +dll.VT_DeviceOpen.restype = C.c_int + +dll.VT_SingleFrameCapture.argtypes = [W.HANDLE, C.POINTER(VT_FRAMEINFO), C.c_int, C.c_int, W.BOOL] +dll.VT_SingleFrameCapture.restype = C.c_int + +dll.VT_DeviceClose.argtypes = [C.POINTER(W.HANDLE)] +dll.VT_DeviceClose.restype = C.c_int + +dll.VT_ParamGetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamGetValue.restype = C.c_int + +dll.VT_ParamSetValue.argtypes = [W.HANDLE, VT_DEVPARAM, C.c_void_p, C.c_int] +dll.VT_ParamSetValue.restype = C.c_int + +def param_set_int(h: W.HANDLE, pid: int, value: int): + p = devparam_by_id(pid) + v = C.c_int(int(value)) + ret = dll.VT_ParamSetValue(h, p, C.byref(v), VALUE_INT) + ck(ret, f"VT_ParamSetValue({hex(pid)})") + +def capture_raw8(h: W.HANDLE) -> np.ndarray: + fi = VT_FRAMEINFO() + ret = dll.VT_SingleFrameCapture(h, C.byref(fi), DATA_RAW, TIMEOUT_MS, True) + ck(ret, "VT_SingleFrameCapture") + + w, hh = int(fi.lWidth), int(fi.lHeight) + buf = C.string_at(fi.pBufPtr, fi.lBufSize) + arr = np.frombuffer(buf, dtype=np.uint8) + + needed = w * hh + if arr.size < needed: + arr = np.pad(arr, (0, needed - arr.size), mode="constant", constant_values=0) + arr = arr[:needed].reshape(hh, w) + return arr + +def overlay_hud(img_bgr, lines): + y = 28 + for s in lines: + cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0,0,0), 3, cv2.LINE_AA) + cv2.putText(img_bgr, s, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255,255,255), 2, cv2.LINE_AA) + y += 28 + +def save_sample(raw: np.ndarray, bgr_preview: np.ndarray, meta: dict): + name = ts_name() + raw_path = os.path.join(SESSION_DIR, f"{name}.raw") + png_path = os.path.join(SESSION_DIR, f"{name}.png") + json_path = os.path.join(SESSION_DIR, f"{name}.json") + + raw.tofile(raw_path) + cv2.imwrite(png_path, bgr_preview) + + with open(json_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + return raw_path, png_path, json_path + +def main(): + # scan + n = C.c_ubyte(0) + ck(dll.VT_DeviceScan(C.byref(n), DEVICE_UDEF), "VT_DeviceScan") + if n.value == 0: + raise RuntimeError("Nenhuma câmera encontrada.") + + # open + idx = C.c_ubyte(0) + h = W.HANDLE() + ck(dll.VT_DeviceOpen(C.byref(idx), C.byref(h), DEVICE_INDEX, DEVICE_UDEF), "VT_DeviceOpen") + print("DeviceOpen OK, handle=", h.value) + print("Saving to:", SESSION_DIR) + + cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) + + # Estado local (não dependemos de GET) + exp_raw = 1500 + gain_a = 0 + gain_d = 0 + + # Aplica estado inicial + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + param_set_int(h, PARAM_ID_SENSOR_GAINANALOGRAW, gain_a) + param_set_int(h, PARAM_ID_SENSOR_GAINDIGITRAW, gain_d) + except Exception as e: + print("[WARN] Falhou set inicial:", e) + + ae = RobustAE(target_p95=140.0, deadband=6.0, k=0.12, max_step=0.10, ema_alpha=0.20, sat_limit=0.01) + ae_on = True + auto_save = False + last_auto_t = 0.0 + + # FPS + t0 = time.time() + frames = 0 + fps = 0.0 + + last_msg = "" + last_msg_t = 0.0 + + try: + while True: + raw = capture_raw8(h) + + # soft AE + ae_dbg = {} + if ae_on: + new_exp, ae_dbg = ae.step(raw, exp_raw) + if new_exp != exp_raw: + exp_raw = new_exp + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + except Exception as e: + # se set falhar, desliga AE pra não ficar insistindo + print("[ERR] set exposure:", e) + ae_on = False + + # preview RGB bonitão + rgb_clean = make_rgb_preview(raw, upscale=UPSCALE) + bgr = rgb_clean.copy() + + # FPS + frames += 1 + dt = time.time() - t0 + if dt >= 1.0: + fps = frames / dt + frames = 0 + t0 = time.time() + + # HUD + lines = [ + f"AE: {'ON' if ae_on else 'OFF'} | AutoSave: {'ON' if auto_save else 'OFF'} | Interval: {CAPTURE_INTERVAL_S:.1f}s", + f"exp_raw={exp_raw} gain_a={gain_a} gain_d={gain_d} | FPS={fps:.1f}", + f"AEdbg: p95={ae_dbg.get('p95_ema', ae_dbg.get('p95', 0)):.1f} sat={ae_dbg.get('sat', 0):.3f} hold={ae_dbg.get('hold', False)}", + "Keys: C/SPACE=save | A=toggle autosave | E=toggle AE | +/- exp | Q/ESC quit", + ] + overlay_hud(bgr, lines) + + # msg pós-save + if last_msg and (time.time() - last_msg_t) < 2.0: + cv2.putText(bgr, last_msg, (12, bgr.shape[0] - 18), + cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,255,0), 2, cv2.LINE_AA) + + cv2.imshow(WINDOW_NAME, bgr) + + # autosave + now = time.time() + if auto_save and (now - last_auto_t) >= CAPTURE_INTERVAL_S: + meta = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "raw_w": RAW_W, "raw_h": RAW_H, + "exp_raw": int(exp_raw), + "gain_a": int(gain_a), + "gain_d": int(gain_d), + "ae_on": bool(ae_on), + "note": "autosave", + } + raw_path, png_path, json_path = save_sample(raw, rgb_clean, meta) + last_msg = f"SAVED: {os.path.basename(raw_path)}" + last_msg_t = now + last_auto_t = now + + k = cv2.waitKey(1) & 0xFF + if k in (ord('q'), ord('Q'), 27): + break + + elif k in (ord('a'), ord('A')): + auto_save = not auto_save + last_msg = f"AutoSave -> {'ON' if auto_save else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord('e'), ord('E')): + ae_on = not ae_on + last_msg = f"AE -> {'ON' if ae_on else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord('c'), ord('C'), 32): # C ou SPACE + meta = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "raw_w": RAW_W, "raw_h": RAW_H, + "exp_raw": int(exp_raw), + "gain_a": int(gain_a), + "gain_d": int(gain_d), + "ae_on": bool(ae_on), + "note": "manual", + } + raw_path, png_path, json_path = save_sample(raw, rgb_clean, meta) + last_msg = f"SAVED: {os.path.basename(raw_path)}" + last_msg_t = time.time() + + elif k in (ord('+'), ord('=')): + exp_raw = clamp(exp_raw + 200, EXP_MIN, EXP_MAX) + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + except Exception as e: + print("[ERR] manual exp +:", e) + + elif k in (ord('-'), ord('_')): + exp_raw = clamp(exp_raw - 200, EXP_MIN, EXP_MAX) + try: + param_set_int(h, PARAM_ID_SENSOR_EXPOSURETIMERAW, exp_raw) + except Exception as e: + print("[ERR] manual exp -:", e) + + finally: + try: + ret = dll.VT_DeviceClose(C.byref(h)) + if ret != 0: + print("VT_DeviceClose retornou:", ret) + except Exception as e: + print("Erro ao fechar:", e) + cv2.destroyAllWindows() + + print("Fim.") + +if __name__ == "__main__": + main() diff --git a/Python/OAK/datasets/_1_weeds_pair_sorter_raw.py b/Python/OAK/datasets/gal5000/_1_weeds_pair_sorter.py similarity index 100% rename from Python/OAK/datasets/_1_weeds_pair_sorter_raw.py rename to Python/OAK/datasets/gal5000/_1_weeds_pair_sorter.py diff --git a/Python/OAK/datasets/_2_create_full_mask_raw.py b/Python/OAK/datasets/gal5000/_2_create_full_mask.py similarity index 96% rename from Python/OAK/datasets/_2_create_full_mask_raw.py rename to Python/OAK/datasets/gal5000/_2_create_full_mask.py index 1149be10c..7dc475862 100644 --- a/Python/OAK/datasets/_2_create_full_mask_raw.py +++ b/Python/OAK/datasets/gal5000/_2_create_full_mask.py @@ -22,9 +22,9 @@ RAW_WIDTH = RESOLUCAO[0] # largura real do raw (ex: 1296) RAW_HEIGHT = RESOLUCAO[1] # altura real do raw (ex: 1028) RAW_EXTS = [".raw"] # se tiver outro, adiciona aqui -PASTA_FINAL_PREVIEWS = os.path.join(MODELO, "dataset", "original", "previews") -PASTA_FINAL_MASKS = os.path.join(MODELO, "dataset", "original", "masks") -PASTA_FINAL_RAWS = os.path.join(MODELO, "dataset", "original", "raws") +PASTA_FINAL_PREVIEWS = os.path.join("dataset", "original", "previews") +PASTA_FINAL_MASKS = os.path.join("dataset", "original", "masks") +PASTA_FINAL_RAWS = os.path.join("dataset", "original", "raws") # Cor da classe será obtida via labelmap # COR_CLASSE_RGB = (128, 0, 0) @@ -200,7 +200,7 @@ def localizar_raw_correspondente(pasta_new_raws, nome_img: str) -> str | None: return None def processar_novas_imagens(cana, horario, grupo, cor_classe_rgb, fazer_copia_final=True, manifesto_csv=None, orignais=False): - source = os.path.join(MODELO, "dataset", "brutas", f"cana_{cana}", horario) if not orignais else os.path.join(MODELO, "dataset", "original") + source = os.path.join("dataset", "brutas", f"cana_{cana}", horario) if not orignais else os.path.join("dataset", "original") pasta_new_previews = os.path.join(source, "group", grupo, "previews") pasta_new_masks = os.path.join(source, "group", grupo, "masks") pasta_new_raws = os.path.join(source, "group", grupo, "raws") @@ -296,7 +296,7 @@ if __name__ == "__main__": args = ap.parse_args() # Caminho padrão pro labelmap, caso não seja informado - labelmap_path = os.path.join(MODELO, "dataset", "labelmap.txt") + labelmap_path = os.path.join("dataset", "labelmap.txt") if not os.path.exists(labelmap_path): raise SystemExit(f"Labelmap não encontrado em: {labelmap_path}") diff --git a/Python/OAK/datasets/_2_ingest_new_masks.py b/Python/OAK/datasets/gal5000/_2_ingest_new_masks.py similarity index 95% rename from Python/OAK/datasets/_2_ingest_new_masks.py rename to Python/OAK/datasets/gal5000/_2_ingest_new_masks.py index a130fb559..80a71e525 100644 --- a/Python/OAK/datasets/_2_ingest_new_masks.py +++ b/Python/OAK/datasets/gal5000/_2_ingest_new_masks.py @@ -12,15 +12,15 @@ with open("config.json", "r") as f: MODELO = config["camera"] # Raiz das brutas agrupadas -PASTA_BRUTAS_GROUP_ROOT = os.path.join(MODELO, "dataset", "brutas") +PASTA_BRUTAS_GROUP_ROOT = os.path.join("dataset", "brutas") # Onde as máscaras novas (rotuladas externamente) são colocadas -PASTA_NEW_MASKS = os.path.join(MODELO, "dataset", "new_masks") +PASTA_NEW_MASKS = os.path.join("dataset", "new_masks") # Destino final do dataset consolidado -PASTA_FINAL_PREVIEWS = os.path.join(MODELO, "dataset", "original", "previews") -PASTA_FINAL_RAWS = os.path.join(MODELO, "dataset", "original", "raws") -PASTA_FINAL_MASKS = os.path.join(MODELO, "dataset", "original", "masks") +PASTA_FINAL_PREVIEWS = os.path.join("dataset", "original", "previews") +PASTA_FINAL_RAWS = os.path.join("dataset", "original", "raws") +PASTA_FINAL_MASKS = os.path.join("dataset", "original", "masks") EXT_PREVIEWS = (".png", ".jpg", ".jpeg") EXT_MASKS = (".png",) diff --git a/Python/OAK/datasets/_3_copy_selected_images_to_mask.py b/Python/OAK/datasets/gal5000/_3_copy_selected_images_to_mask.py similarity index 97% rename from Python/OAK/datasets/_3_copy_selected_images_to_mask.py rename to Python/OAK/datasets/gal5000/_3_copy_selected_images_to_mask.py index c1a05bf0f..fe8c527e7 100644 --- a/Python/OAK/datasets/_3_copy_selected_images_to_mask.py +++ b/Python/OAK/datasets/gal5000/_3_copy_selected_images_to_mask.py @@ -12,14 +12,14 @@ with open("config.json", "r") as f: MODELO = config["camera"] # Raiz das brutas (todas as canas/horários/grupos) -PASTA_BRUTAS_ROOT = os.path.join(MODELO, "dataset", "brutas") +PASTA_BRUTAS_ROOT = os.path.join("dataset", "brutas") # Onde você coloca os previews selecionados (tudo misturado) -PASTA_SELECTED_PREVIEWS = os.path.join(MODELO, "dataset", "selected_previews") +PASTA_SELECTED_PREVIEWS = os.path.join("dataset", "selected_previews") # Destino final, organizado por grupo: # dataset/original/group/{GRUPO}/{previews,raws,metas,masks} -PASTA_ORIGINAL_GROUP_ROOT = os.path.join(MODELO, "dataset", "original", "group") +PASTA_ORIGINAL_GROUP_ROOT = os.path.join("dataset", "original", "group") EXT_PREVIEWS = (".png", ".jpg", ".jpeg") EXT_RAWS = (".raw",) diff --git a/Python/OAK/datasets/_4_group_images_by_class_raw.py b/Python/OAK/datasets/gal5000/_4_group_images_by_class.py similarity index 98% rename from Python/OAK/datasets/_4_group_images_by_class_raw.py rename to Python/OAK/datasets/gal5000/_4_group_images_by_class.py index 03c15aa46..7e328fa8b 100644 --- a/Python/OAK/datasets/_4_group_images_by_class_raw.py +++ b/Python/OAK/datasets/gal5000/_4_group_images_by_class.py @@ -309,8 +309,8 @@ def build_cli(): if __name__ == "__main__": ap = build_cli() args = ap.parse_args() - originals_dir = os.path.join(MODELO, "dataset", "original") - labelmap_dir = os.path.join(MODELO, "dataset", "labelmap.txt") + originals_dir = os.path.join("dataset", "original") + labelmap_dir = os.path.join("dataset", "labelmap.txt") processar( originals_dir=originals_dir, labelmap_path=labelmap_dir, diff --git a/Python/OAK/datasets/_5_augmentation_raw.py b/Python/OAK/datasets/gal5000/_5_augmentation.py similarity index 97% rename from Python/OAK/datasets/_5_augmentation_raw.py rename to Python/OAK/datasets/gal5000/_5_augmentation.py index f38f305e0..87a96389c 100644 --- a/Python/OAK/datasets/_5_augmentation_raw.py +++ b/Python/OAK/datasets/gal5000/_5_augmentation.py @@ -6,15 +6,15 @@ Augmenta imagens/máscaras *por grupo* usando RAW4 (float32, 0..1, R G IR B). Novo fluxo (GAL5000): Entrada: - MODELO/dataset/original/group//raws (.raw, 4 canais float32) - MODELO/dataset/original/group//masks (.png/.jpg) - (opcional) MODELO/dataset/original/group//masks2 + dataset/original/group//raws (.raw, 4 canais float32) + dataset/original/group//masks (.png/.jpg) + (opcional) dataset/original/group//masks2 Saída: - MODELO/dataset/augmented/group//previews (RGB gerado do RAW blurado) - MODELO/dataset/augmented/group//raws (RAW4 augmentado: geom + blur) - MODELO/dataset/augmented/group//masks - MODELO/dataset/augmented/group//masks2 + dataset/augmented/group//previews (RGB gerado do RAW blurado) + dataset/augmented/group//raws (RAW4 augmentado: geom + blur) + dataset/augmented/group//masks + dataset/augmented/group//masks2 Regras: - GEOMETRIA (flip, shift, rotate) aplicada em RAW + masks (+ masks2). @@ -44,7 +44,7 @@ RESOLUCAO = config.get("raw_size") USE_MASKS2 = config.get("dual_head", False) # Pastas base -DATASET_BASE = os.path.join(MODELO, "dataset") +DATASET_BASE = os.path.join("dataset") ORIG_GROUP_ROOT = os.path.join(DATASET_BASE, "original", "group") AUG_GROUP_ROOT = os.path.join(DATASET_BASE, "augmented", "group") diff --git a/Python/OAK/datasets/_6_normalize_raw.py b/Python/OAK/datasets/gal5000/_6_normalize.py similarity index 97% rename from Python/OAK/datasets/_6_normalize_raw.py rename to Python/OAK/datasets/gal5000/_6_normalize.py index 05a374b60..e2d382ffa 100644 --- a/Python/OAK/datasets/_6_normalize_raw.py +++ b/Python/OAK/datasets/gal5000/_6_normalize.py @@ -4,11 +4,11 @@ Normaliza/redimensiona PREVIEW + RAW + MASK (+MASK2), mantendo ESTRUTURA POR GRUPO. Entradas: - MODELO/dataset/original/group//{previews,raws,masks,(masks2)} - MODELO/dataset/augmented/group//{previews,raws,masks,(masks2)} + dataset/original/group//{previews,raws,masks,(masks2)} + dataset/augmented/group//{previews,raws,masks,(masks2)} Saídas (por resolução): - MODELO/dataset//group//{previews,raws,masks,(masks2)} + dataset//group//{previews,raws,masks,(masks2)} Conversão de máscara: - Lê máscara RGB e converte para IDs via utils.converter_mask_rgb_para_ids @@ -37,9 +37,10 @@ USE_MASKS2 = config["dual_head"] RESOLUCAO = tuple(config["resolucao"]) # [W,H] MODEL_NAME = config["model_name"] CHANNELS = int(config.get("channels", 4)) -pasta_base = os.path.join(MODELO, "dataset") +pasta_base = os.path.join("dataset") labelmap_path = os.path.join(pasta_base, "labelmap.txt") -save_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME, f"raw{CHANNELS}") +stats_source_tag = config.get("stats_source_tag", "stacked_raw4") +save_path = os.path.join("backup", config["modelo"], MODEL_NAME, stats_source_tag) RESOLUCOES = {f"{RESOLUCAO[0]}x{RESOLUCAO[1]}": (RESOLUCAO[0], RESOLUCAO[1])} FONTES = ["original", "augmented"] diff --git a/Python/OAK/datasets/_7_split_raw.py b/Python/OAK/datasets/gal5000/_7_split.py similarity index 96% rename from Python/OAK/datasets/_7_split_raw.py rename to Python/OAK/datasets/gal5000/_7_split.py index 085d482d6..bbfe12433 100644 --- a/Python/OAK/datasets/_7_split_raw.py +++ b/Python/OAK/datasets/gal5000/_7_split.py @@ -5,10 +5,10 @@ Split estratificado por GRUPO com **val/test só do ORIGINAL** e garantia de NÃO VAZAMENTO entre splits, agora para estrutura RAW: Lê de: - MODELO/dataset//group//{previews,raws,masks,(masks2)} + dataset//group//{previews,raws,masks,(masks2)} Escreve em: - MODELO/dataset/split//group//{previews,raws,(masks),(masks2)} + dataset/split//group//{previews,raws,(masks),(masks2)} Definições: - "Família" = todas as variações da MESMA base original: @@ -33,8 +33,8 @@ USE_MASKS2 = config.get("dual_head", False) RESOLUCAO = tuple(config.get("resolucao")) # Pastas (ajustadas para PREVIEWS/RAWS) -pasta_origem = os.path.join(MODELO, "dataset", f"{RESOLUCAO[0]}x{RESOLUCAO[1]}", "group") -pasta_destino = os.path.join(MODELO, "dataset", "split") +pasta_origem = os.path.join("dataset", f"{RESOLUCAO[0]}x{RESOLUCAO[1]}", "group") +pasta_destino = os.path.join("dataset", "split") IMG_EXTS = (".jpg", ".jpeg", ".png") MSK_EXT = ".png" # máscaras normalizadas em PNG @@ -376,7 +376,6 @@ def main(): ap.add_argument("--min-val", type=int, default=1, help="Mínimo de FAMÍLIAS por grupo em val (default=1).") ap.add_argument("--min-test", type=int, default=0, help="Mínimo de FAMÍLIAS por grupo em test (default=0).") - ap.add_argument("--modelo", type=str, default=None, help="Sobrescreve MODELO do config.json.") ap.add_argument("--resolucao", type=str, default=None, help="Sobrescreve resolução no formato WxH (ex: 960x544).") ap.add_argument("--cap-train-families", type=str, default="", @@ -384,7 +383,6 @@ def main(): args = ap.parse_args() - modelo = args.modelo or MODELO if args.resolucao: try: w, h = args.resolucao.lower().split("x") @@ -406,8 +404,8 @@ def main(): caps_map = parse_cap_map(args.cap_train_families) global pasta_origem, pasta_destino - pasta_origem = os.path.join(modelo, "dataset", f"{resolucao[0]}x{resolucao[1]}", "group") - pasta_destino = os.path.join(modelo, "dataset", "split") + pasta_origem = os.path.join("dataset", f"{resolucao[0]}x{resolucao[1]}", "group") + pasta_destino = os.path.join("dataset", "split") soma = args.train + args.val + args.test if soma <= 0: diff --git a/Python/OAK/datasets/_8_train_segformer_b3_raw.py b/Python/OAK/datasets/gal5000/_8_train_segformer_b3.py similarity index 98% rename from Python/OAK/datasets/_8_train_segformer_b3_raw.py rename to Python/OAK/datasets/gal5000/_8_train_segformer_b3.py index a4f245ccd..7ed4be4b7 100644 --- a/Python/OAK/datasets/_8_train_segformer_b3_raw.py +++ b/Python/OAK/datasets/gal5000/_8_train_segformer_b3.py @@ -358,10 +358,10 @@ def main(): stats_source_tag = config.get("stats_source_tag", "stacked_raw4") experiment_tag = f"{FUSION_MODE}_raw{CHANNELS}" - save_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME, experiment_tag) + save_path = os.path.join("backup", config["modelo"], MODEL_NAME, experiment_tag) - #save_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME, f"raw{CHANNELS}") - dataset_path = os.path.join(MODELO, "dataset") + #save_path = os.path.join("backup", config["modelo"], MODEL_NAME, f"raw{CHANNELS}") + dataset_path = os.path.join("dataset") labelmap_path = os.path.join(dataset_path, "labelmap.txt") os.makedirs(save_path, exist_ok=True) @@ -378,7 +378,7 @@ def main(): # Caminho padrão: dentro do dataset, nome do arquivo de stats # (ajusta aqui pro nome que você realmente usou: norm_stats.json, por ex.) #norm_stats_path = os.path.join(save_path, "norm_stats.json") - norm_stats_path = os.path.join(MODELO, "backup", config["modelo"], MODEL_NAME, stats_source_tag, "norm_stats.json") + norm_stats_path = os.path.join("backup", config["modelo"], MODEL_NAME, stats_source_tag, "norm_stats.json") if args.norm_stats is not None: norm_stats_path = args.norm_stats @@ -547,7 +547,6 @@ def main(): strict=False, ) patch_segformer_input_channels(model, in_ch=CHANNELS) - elif FUSION_MODE == "dual_branch": model = build_dual_branch_segformer_model( num_classes=num_classes, diff --git a/Python/OAK/datasets/_9_test_segformer_b3_raw.py b/Python/OAK/datasets/gal5000/_9_test_segformer_b3.py similarity index 98% rename from Python/OAK/datasets/_9_test_segformer_b3_raw.py rename to Python/OAK/datasets/gal5000/_9_test_segformer_b3.py index d01803810..8deae3d69 100644 --- a/Python/OAK/datasets/_9_test_segformer_b3_raw.py +++ b/Python/OAK/datasets/gal5000/_9_test_segformer_b3.py @@ -243,7 +243,7 @@ def main(): print(f"[cfg] resolucao nominal RAW: {W}x{H}") print(f"[cfg] channels={CHANNELS} use_ndvi={USE_NDVI}") - dataset_path = os.path.join(MODELO, "dataset") + dataset_path = os.path.join("dataset") labelmap_path = os.path.join(dataset_path, "labelmap.txt") # ========================== @@ -254,7 +254,7 @@ def main(): experiment_tag = f"{FUSION_MODE}_raw{CHANNELS}" - norm_stats_path = os.path.join(MODELO, "backup", modelo_folder, MODEL_NAME, experiment_tag, "norm_stats.json") + norm_stats_path = os.path.join("backup", modelo_folder, MODEL_NAME, experiment_tag, "norm_stats.json") if args.norm_stats is not None: norm_stats_path = args.norm_stats if os.path.isfile(norm_stats_path): @@ -307,7 +307,7 @@ def main(): if args.ckpt is not None: ckpt_path = args.ckpt else: - save_path = os.path.join(MODELO, "backup", modelo_folder, MODEL_NAME, experiment_tag) + save_path = os.path.join("backup", modelo_folder, MODEL_NAME, experiment_tag) ckpt_path = os.path.join(save_path, "best_miou.pt") if not os.path.isfile(ckpt_path): diff --git a/Python/OAK/datasets/config.json b/Python/OAK/datasets/gal5000/config.json similarity index 75% rename from Python/OAK/datasets/config.json rename to Python/OAK/datasets/gal5000/config.json index 95135c152..08427fba4 100644 --- a/Python/OAK/datasets/config.json +++ b/Python/OAK/datasets/gal5000/config.json @@ -1,19 +1,19 @@ { "camera": "gal5000", "modelo": "segformer_b1", - "model_name": "pulv_new", + "model_name": "pulv_mit", "dual_head": false, "main_class_name": "cana", "es_classes": "", "model_to_use": "geral", "raw_size": [1296, 1028], - "resolucao": [1008, 800], + "resolucao": [1024, 800], "roi_inicio": 0.0, "roi_tamanho": 1.0, "shaves": 3, "channels": 4, "use_ndvi": false, - "backbone": "nvidia/segformer-b1-finetuned-ade-512-512", + "backbone": "nvidia/mit-b1", "fusion_mode": "stacked", "stats_source_tag": "stacked_raw4" } \ No newline at end of file diff --git a/Python/OAK/datasets/gal5000/regenerate_previews.py b/Python/OAK/datasets/gal5000/regenerate_previews.py new file mode 100644 index 000000000..b4d8c55db --- /dev/null +++ b/Python/OAK/datasets/gal5000/regenerate_previews.py @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- +""" +Regenera previews a partir dos RAWs antigos da GAL5000, +aplicando compensação de IR no preview. + +Estrutura esperada: +dataset/brutas/group/{GRUPO}/ + masks/ + previews/ + raws/ + metas/ + +Uso exemplo: +python regen_previews_from_raws.py ^ + --root dataset/brutas/group ^ + --raw-h 1028 ^ + --raw-w 1296 ^ + --layout rgirb ^ + --ir-k-r 0.8 ^ + --ir-k-g 0.4 ^ + --ir-k-b 0.9 + +Observações: +- Para .raw, informe --raw-h e --raw-w +- Para .npy/.npz, o shape é lido automaticamente +- O script recria os previews em cada pasta previews do grupo +- Por padrão sobrescreve os previews existentes +""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +from typing import Optional, Tuple + +import numpy as np + +try: + import cv2 +except ImportError: + cv2 = None + from PIL import Image + + +# --------------------------------------------------------- +# Preview +# --------------------------------------------------------- +def make_bgr_preview_from_raw( + raw_np: np.ndarray, + rgirb: bool, + preview_fast: bool, + preview_scale: int = 2, + apply_ir_comp: bool = True, + ir_k_r: float = 0.40, + ir_k_g: float = 0.10, + ir_k_b: float = 0.50, +) -> np.ndarray: + """ + Gera preview BGR (OpenCV) a partir do tensor raw_np (C,H,W) float32 em 0..1. + + Casos esperados: + - rgirb=False: + raw_np = [R, G, B] ou [R, G, B, NDVI] + - rgirb=True: + raw_np = [R, G, IR, B] ou [R, G, IR, B, NDVI] + """ + assert raw_np.ndim == 3, "raw_np deve ser (C,H,W)" + C = raw_np.shape[0] + if C not in (3, 4, 5): + raise RuntimeError(f"Esperado C=3, 4 ou 5, veio {C}") + + raw_np = raw_np.astype(np.float32, copy=False) + + r = raw_np[0] + g = raw_np[1] + b = raw_np[3] if rgirb else raw_np[2] + + if preview_fast: + if preview_scale > 1: + r = r[::preview_scale, ::preview_scale] + g = g[::preview_scale, ::preview_scale] + b = b[::preview_scale, ::preview_scale] + + if rgirb and apply_ir_comp and C >= 4: + ir = raw_np[2] + if preview_scale > 1: + ir = ir[::preview_scale, ::preview_scale] + + r = np.clip(r - ir_k_r * ir, 0.0, 1.0) + g = np.clip(g - ir_k_g * ir, 0.0, 1.0) + b = np.clip(b - ir_k_b * ir, 0.0, 1.0) + + bgr = np.stack([b, g, r], axis=0) + bgr = np.power(np.clip(bgr, 0.0, 1.0), 1 / 1.8) + bgr8 = (bgr * 255.0).clip(0, 255).astype(np.uint8) + return np.transpose(bgr8, (1, 2, 0)).copy() + + if rgirb and apply_ir_comp and C >= 4: + ir = raw_np[2] + r = np.clip(r - ir_k_r * ir, 0.0, 1.0) + g = np.clip(g - ir_k_g * ir, 0.0, 1.0) + b = np.clip(b - ir_k_b * ir, 0.0, 1.0) + + def stretch_channel(x: np.ndarray, p_low: float = 1.0, p_high: float = 99.0) -> np.ndarray: + lo = np.percentile(x, p_low) + hi = np.percentile(x, p_high) + + if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo: + return np.clip(x, 0.0, 1.0) + + x = (x - lo) / (hi - lo) + return np.clip(x, 0.0, 1.0) + + r = stretch_channel(r) + g = stretch_channel(g) + b = stretch_channel(b) + + bgr = np.stack([b, g, r], axis=0).astype(np.float32) + bgr = np.power(np.clip(bgr, 0.0, 1.0), 1 / 2.0) + + bgr8 = (bgr * 255.0).clip(0, 255).astype(np.uint8) + return np.transpose(bgr8, (1, 2, 0)).copy() + + +# --------------------------------------------------------- +# IO helpers +# --------------------------------------------------------- +def save_bgr_image(path: Path, bgr: np.ndarray) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + + if cv2 is not None: + ok = cv2.imwrite(str(path), bgr) + if not ok: + raise RuntimeError(f"Falha ao salvar imagem: {path}") + else: + rgb = bgr[..., ::-1] + Image.fromarray(rgb).save(path) + + +def to_chw_float01(arr: np.ndarray, layout: str) -> np.ndarray: + """ + Converte entrada para (C,H,W) float32 0..1. + + layout: + - rgirb + - rgbir + - rgb + """ + arr = np.asarray(arr) + + if arr.ndim != 3: + raise RuntimeError(f"Esperava array 3D, veio shape={arr.shape}") + + # HWC -> CHW + if arr.shape[-1] in (3, 4, 5) and arr.shape[0] not in (3, 4, 5): + arr = np.transpose(arr, (2, 0, 1)) + + if arr.shape[0] not in (3, 4, 5): + raise RuntimeError(f"Não consegui interpretar canais em shape={arr.shape}") + + arr = arr.astype(np.float32, copy=False) + + # Normalização para 0..1 + if arr.dtype == np.uint8: + arr = arr / 255.0 + elif arr.dtype == np.uint16: + arr = arr / 65535.0 + else: + # Se já vier float mas fora de 0..1, tenta ajustar + maxv = float(np.nanmax(arr)) if arr.size else 1.0 + if maxv > 1.0: + arr = arr / maxv + + arr = np.clip(arr, 0.0, 1.0) + + # Reorganiza para o contrato do preview + # Queremos: + # rgirb=True -> [R,G,IR,B] + # rgirb=False -> [R,G,B] + if layout == "rgirb": + # já assume [R,G,IR,B] ou [R,G,IR,B,NDVI] + return arr + elif layout == "rgbir": + # [R,G,B,IR] -> [R,G,IR,B] + if arr.shape[0] < 4: + raise RuntimeError("layout=rgbir exige pelo menos 4 canais") + if arr.shape[0] == 4: + arr = arr[[0, 1, 3, 2], :, :] + else: + # [R,G,B,IR,NDVI] -> [R,G,IR,B,NDVI] + arr = arr[[0, 1, 3, 2, 4], :, :] + return arr + elif layout == "rgb": + return arr[:3] + else: + raise ValueError(f"layout inválido: {layout}") + + +def load_raw_file(raw_path: Path, raw_hw: Optional[Tuple[int, int]], layout: str) -> np.ndarray: + """ + Retorna (C,H,W) float32 em 0..1. + + Suporta: + - .npy + - .npz + - .raw + + Para .raw: + - mosaico uint8 HxW em padrão 2x2 R,G / IR,B + - ou RAW4 float32 (4,H,W) salvo em [R,G,IR,B] + """ + ext = raw_path.suffix.lower() + + if ext == ".npy": + arr = np.load(raw_path) + return to_chw_float01(arr, layout) + + if ext == ".npz": + z = np.load(raw_path) + key = list(z.keys())[0] + arr = z[key] + return to_chw_float01(arr, layout) + + if ext == ".raw": + if raw_hw is None: + raise RuntimeError(f"{raw_path.name}: para .raw informe --raw-h e --raw-w") + + H, W = raw_hw + size_bytes = raw_path.stat().st_size + mosa_bytes = H * W + raw4_bytes = 4 * H * W * 4 # float32 + + if size_bytes == mosa_bytes: + # mosaico uint8 cru + arr = np.fromfile(raw_path, dtype=np.uint8).reshape(H, W) + + if (H % 2) != 0 or (W % 2) != 0: + raise RuntimeError(f"{raw_path.name}: H e W precisam ser pares para mosaico 2x2") + + r_sub = arr[0::2, 0::2] + g_sub = arr[0::2, 1::2] + ir_sub = arr[1::2, 0::2] + b_sub = arr[1::2, 1::2] + + if cv2 is not None: + r = cv2.resize(r_sub, (W, H), interpolation=cv2.INTER_LINEAR) + g = cv2.resize(g_sub, (W, H), interpolation=cv2.INTER_LINEAR) + ir = cv2.resize(ir_sub, (W, H), interpolation=cv2.INTER_LINEAR) + b = cv2.resize(b_sub, (W, H), interpolation=cv2.INTER_LINEAR) + else: + r = np.array(Image.fromarray(r_sub).resize((W, H), resample=Image.BILINEAR)) + g = np.array(Image.fromarray(g_sub).resize((W, H), resample=Image.BILINEAR)) + ir = np.array(Image.fromarray(ir_sub).resize((W, H), resample=Image.BILINEAR)) + b = np.array(Image.fromarray(b_sub).resize((W, H), resample=Image.BILINEAR)) + + chw = np.stack([r, g, ir, b], axis=0).astype(np.float32) / 255.0 + return chw + + if size_bytes == raw4_bytes: + # RAW4 float32 salvo como (4,H,W) em [R,G,IR,B] + arr = np.fromfile(raw_path, dtype=np.float32).reshape(4, H, W) + arr = np.clip(arr, 0.0, 1.0) + return to_chw_float01(arr, layout="rgirb") + + raise RuntimeError( + f"{raw_path.name}: tamanho inesperado {size_bytes} bytes " + f"(esperado mosaico={mosa_bytes} ou raw4 float32={raw4_bytes})" + ) + + raise RuntimeError(f"Extensão não suportada: {raw_path}") + + +# --------------------------------------------------------- +# Processamento +# --------------------------------------------------------- +def process_group( + group_dir: Path, + raw_hw: Optional[Tuple[int, int]], + layout: str, + preview_fast: bool, + preview_scale: int, + apply_ir_comp: bool, + ir_k_r: float, + ir_k_g: float, + ir_k_b: float, + overwrite: bool, +) -> tuple[int, int]: + raws_dir = group_dir / "raws" + previews_dir = group_dir / "previews" + + if not raws_dir.is_dir(): + return 0, 0 + + previews_dir.mkdir(parents=True, exist_ok=True) + + raw_files = [] + for ext in ("*.raw", "*.npy", "*.npz"): + raw_files.extend(sorted(raws_dir.glob(ext))) + + done = 0 + failed = 0 + + for raw_path in raw_files: + out_path = previews_dir / f"{raw_path.stem}.jpg" + + if out_path.exists() and not overwrite: + continue + + try: + raw_np = load_raw_file(raw_path, raw_hw=raw_hw, layout=layout) + + bgr = make_bgr_preview_from_raw( + raw_np=raw_np, + rgirb=(layout == "rgirb" or layout == "rgbir"), + preview_fast=preview_fast, + preview_scale=preview_scale, + apply_ir_comp=apply_ir_comp, + ir_k_r=ir_k_r, + ir_k_g=ir_k_g, + ir_k_b=ir_k_b, + ) + + save_bgr_image(out_path, bgr) + done += 1 + print(f"[OK] {group_dir.name}/{raw_path.name} -> {out_path.name}") + + except Exception as e: + failed += 1 + print(f"[ERRO] {group_dir.name}/{raw_path.name}: {e}") + + return done, failed + + +def main(): + parser = argparse.ArgumentParser(description="Regenera previews a partir dos RAWs antigos.") + parser.add_argument("--root", type=str, required=True, help="Pasta group, ex: dataset/brutas/group") + parser.add_argument("--raw-h", type=int, default=None, help="Altura do RAW para arquivos .raw") + parser.add_argument("--raw-w", type=int, default=None, help="Largura do RAW para arquivos .raw") + parser.add_argument("--layout", type=str, default="rgirb", choices=["rgirb", "rgbir", "rgb"], help="Layout dos canais dos RAWs") + parser.add_argument("--preview-fast", action="store_true", help="Usa modo rápido") + parser.add_argument("--preview-scale", type=int, default=2, help="Escala no preview_fast") + parser.add_argument("--no-ir-comp", action="store_true", help="Desliga compensação de IR") + parser.add_argument("--ir-k-r", type=float, default=0.8) + parser.add_argument("--ir-k-g", type=float, default=0.4) + parser.add_argument("--ir-k-b", type=float, default=0.9) + parser.add_argument("--no-overwrite", action="store_true", help="Não sobrescreve previews existentes") + + args = parser.parse_args() + + root = Path(args.root) + if not root.is_dir(): + raise RuntimeError(f"Pasta root não encontrada: {root}") + + raw_hw = None + if args.raw_h is not None and args.raw_w is not None: + raw_hw = (args.raw_h, args.raw_w) + + total_done = 0 + total_failed = 0 + + group_dirs = [p for p in sorted(root.iterdir()) if p.is_dir()] + if not group_dirs: + raise RuntimeError(f"Nenhum grupo encontrado em: {root}") + + for group_dir in group_dirs: + done, failed = process_group( + group_dir=group_dir, + raw_hw=raw_hw, + layout=args.layout, + preview_fast=args.preview_fast, + preview_scale=args.preview_scale, + apply_ir_comp=not args.no_ir_comp, + ir_k_r=args.ir_k_r, + ir_k_g=args.ir_k_g, + ir_k_b=args.ir_k_b, + overwrite=not args.no_overwrite, + ) + total_done += done + total_failed += failed + + print("\n============================================") + print("Regeneração concluída") + print(f"Previews gerados : {total_done}") + print(f"Falhas : {total_failed}") + print("============================================") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Python/OAK/datasets/multiespec_module/_1_weeds_pair_sorter.py b/Python/OAK/datasets/multiespec_module/_1_weeds_pair_sorter.py new file mode 100644 index 000000000..45ce73cad --- /dev/null +++ b/Python/OAK/datasets/multiespec_module/_1_weeds_pair_sorter.py @@ -0,0 +1,669 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +raw_triplet_sorter.py +------------------------------------------------- +Ferramenta para classificar manualmente amostras RAW do dataset +(arquivos .raw + .png + .json) em pastas de labels, +usando atalhos de teclado. + +Fluxo: +- Entrada: uma ou mais pastas de sessão (cada uma contendo N arquivos): + - NOME.raw + - NOME.png + - NOME.json +- O script mostra o preview (PNG) e você usa teclas 1..9/0 para enviar + o TRIPLO (raw+preview+json) para uma pasta de saída organizada por label. + +Estrutura de saída: + out_root/ +