From 5c26d6637857a6530200e171f680b3345e38103d Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Wed, 22 Apr 2026 17:08:49 -0300 Subject: [PATCH] Limpeza e refinamento na firmware do modulo multiespectral --- .gitignore | 4 +- Python/OAK/datasets/_0_capture_raw.py | 328 ----- Python/OAK/datasets/gal5000/_0_capture.py | 598 ++++----- .../datasets/gal5000/_2_create_full_mask.py | 2 + .../gal5000/_4_group_images_by_class.py | 2 + Python/OAK/datasets/gal5000/_6_normalize.py | 3 + .../datasets/gal5000/_9_test_segformer_b3.py | 6 +- .../{ => gal5000}/raw_segformer_service.py | 3 + .../datasets/multiespec_module/_0_capture.py | 822 +++++++++++++ .../_12_check_percent_class.py | 226 ++++ .../multiespec_module/_1_weeds_pair_sorter.py | 499 ++++---- ....py => _2_copy_selected_images_to_mask.py} | 180 +-- .../multiespec_module/_2_create_full_mask.py | 378 ++++-- ...st_new_masks.py => _3_ingest_new_masks.py} | 197 +-- .../_4_group_images_by_class.py | 237 ++-- .../multiespec_module/_5_augmentation.py | 1065 ++++++++--------- .../multiespec_module/_6_normalize.py | 381 ++---- .../datasets/multiespec_module/_7_split.py | 281 ++--- ..._segformer_b3.py => _8_train_segformer.py} | 129 +- .../multiespec_module/_9_test_segformer.py | 413 +++++++ .../multiespec_module/_9_test_segformer_b3.py | 535 --------- .../datasets/multiespec_module/config.json | 6 +- .../multispec_segformer_service.py | 683 +++++++++++ .../multispectral_service.py | 405 +++++++ .../pi/raw_processor_core.py | 453 +++++++ .../pi/raw_processor_preview.py | 125 ++ .../multiespec_module/stream_receiver.py | 324 +++++ Python/OAK/datasets/test_fps.py | 2 +- Python/OAK/datasets/utils.py | 11 + Python/raspi/cam_3/pi/camera_manager.py | 111 +- Python/raspi/cam_3/pi/frame_service.py | 31 +- Python/raspi/cam_3/pi/server.py | 17 +- Python/raspi/cam_3/pi/state.py | 80 +- Python/raspi/cam_3/pi/stream_sender.py | 11 +- 34 files changed, 5506 insertions(+), 3042 deletions(-) delete mode 100644 Python/OAK/datasets/_0_capture_raw.py rename Python/OAK/datasets/{ => gal5000}/raw_segformer_service.py (99%) create mode 100644 Python/OAK/datasets/multiespec_module/_0_capture.py create mode 100644 Python/OAK/datasets/multiespec_module/_12_check_percent_class.py rename Python/OAK/datasets/multiespec_module/{_3_copy_selected_images_to_mask.py => _2_copy_selected_images_to_mask.py} (61%) rename Python/OAK/datasets/multiespec_module/{_2_ingest_new_masks.py => _3_ingest_new_masks.py} (55%) rename Python/OAK/datasets/multiespec_module/{_8_train_segformer_b3.py => _8_train_segformer.py} (82%) create mode 100644 Python/OAK/datasets/multiespec_module/_9_test_segformer.py delete mode 100644 Python/OAK/datasets/multiespec_module/_9_test_segformer_b3.py create mode 100644 Python/OAK/datasets/multiespec_module/multispec_segformer_service.py create mode 100644 Python/OAK/datasets/multiespec_module/multispectral_service.py create mode 100644 Python/OAK/datasets/multiespec_module/pi/raw_processor_core.py create mode 100644 Python/OAK/datasets/multiespec_module/pi/raw_processor_preview.py create mode 100644 Python/OAK/datasets/multiespec_module/stream_receiver.py diff --git a/.gitignore b/.gitignore index e5890c09b..84ee260e9 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ Python/OAK/datasets/oak-d/dataset/ Python/OAK/datasets/oak-d/backup/ Python/OAK/datasets/gal5000/dataset/ Python/OAK/datasets/gal5000/backup/ +Python/OAK/datasets/multiespec_module/dataset/ +Python/OAK/datasets/multiespec_module/backup/ Python/yolov8-seg/venv/ Python/yolov8-seg/images/ @@ -79,4 +81,4 @@ AgroBase/AgroBase/bin/x64/Debug/Python/venv/ /Exemplos /Python/raspi/cam_2/imx296_pi/ -/Python/raspi/cam_3/imx296_pi/ \ No newline at end of file +/Python/raspi/cam_3/imx296_pi/ diff --git a/Python/OAK/datasets/_0_capture_raw.py b/Python/OAK/datasets/_0_capture_raw.py deleted file mode 100644 index 48dd8f236..000000000 --- a/Python/OAK/datasets/_0_capture_raw.py +++ /dev/null @@ -1,328 +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() - - apply_ir_comp = True - ir_k_r = 0.8 - ir_k_g = 0.4 - ir_k_b = 0.9 - - 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) - - 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, 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 (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/gal5000/_0_capture.py b/Python/OAK/datasets/gal5000/_0_capture.py index 0867fb1b5..113867dd5 100644 --- a/Python/OAK/datasets/gal5000/_0_capture.py +++ b/Python/OAK/datasets/gal5000/_0_capture.py @@ -1,302 +1,176 @@ import os import time import json -import math -import ctypes as C -from ctypes import wintypes as W +import argparse 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" +from raw_segformer_service import make_bgr_preview_from_raw +from gal_service import Gal5000Camera # ajuste o nome do módulo se estiver diferente -# 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 +# Helpers gerais # ========================= -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): + +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 + 1: - return x.astype(np.uint8) - y = (x.astype(np.float32) - lo) * (255.0 / (hi - lo)) + + 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 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): +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, +): """ - Mede brilho no canal G cru usando uma ROI na base (mais parecido com chão). - Retorna p90/p95 e fração saturada. + 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). """ - G = raw[0::2, 1::2] # H/2 x W/2 - h2, w2 = G.shape + h, w = img_bgr.shape[:2] - # 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] + # Fator de escala com base na altura atual + scale = h / float(base_h) - p90 = float(np.percentile(roi, 90)) - p95 = float(np.percentile(roi, 95)) - sat = float(np.mean(roi >= 250)) - return p90, p95, sat + # Evita ficar microscópico em resoluções muito baixas + scale = max(scale, 0.4) -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 + font_scale = base_font_scale * scale + line_step = int(base_line_step * scale) - def step(self, raw, exp_raw): - p90, p95, sat = measure_raw_g_metrics(raw) + # Espessuras proporcionais + thick_outline = max(1, int(3 * scale)) + thick_text = max(1, int(2 * scale)) - # 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 + # Margem superior / esquerda também escaladas + y = int(24 * scale) + x = int(12 * scale) - 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 + # 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(raw: np.ndarray, bgr_preview: np.ndarray, meta: dict): + +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(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) + 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(): - # 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.") + parser = argparse.ArgumentParser( + description="Captura de dataset RAW4 (SegFormer B0) usando Gal5000 + AutoExposure.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) - # 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) + 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.") - cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) + args = parser.parse_args() - # Estado local (não dependemos de GET) - exp_raw = 1500 - gain_a = 0 - gain_d = 0 + # ===== 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(args.out_root, "brutas", f"cana_{args.cana}", args.horario, datetime.now().strftime("%Y%m%d")) + os.makedirs(session_dir, exist_ok=True) - # 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) + 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) - 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 + upscale = args.upscale - # FPS - t0 = time.time() + # Estatísticas simples de FPS + t_fps = time.time() frames = 0 fps = 0.0 @@ -304,119 +178,151 @@ def main(): last_msg_t = 0.0 try: - while True: - raw = capture_raw8(h) + 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()) - # 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 + # opcional: você pode ligar streaming se quiser, mas grab_raw4 já usa single-frame + cam.configure_fps(20) + cam.start_streaming() - # preview RGB bonitão - rgb_clean = make_rgb_preview(raw, upscale=UPSCALE) - bgr = rgb_clean.copy() + apply_ir_comp = True + ir_k_r = 0.8 + ir_k_g = 0.4 + ir_k_b = 0.9 - # FPS - frames += 1 - dt = time.time() - t0 - if dt >= 1.0: - fps = frames / dt - frames = 0 + 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() - # 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) + 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) - # 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) + 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) - cv2.imshow(WINDOW_NAME, bgr) + # FPS + frames += 1 + dt_fps = time.time() - t_fps + if dt_fps >= 1.0: + fps = frames / dt_fps + frames = 0 + t_fps = time.time() - # 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 + # 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) - k = cv2.waitKey(1) & 0xFF - if k in (ord('q'), ord('Q'), 27): - break + ae_on = cam.is_auto_exposure_enabled() - 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() + # 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) - 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() + # 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) - 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() + cv2.imshow(window_name, bgr) - 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) + # 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 - 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) + # 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, 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 (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: - 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 da captura.") - print("Fim.") if __name__ == "__main__": main() diff --git a/Python/OAK/datasets/gal5000/_2_create_full_mask.py b/Python/OAK/datasets/gal5000/_2_create_full_mask.py index 7dc475862..eb71758a4 100644 --- a/Python/OAK/datasets/gal5000/_2_create_full_mask.py +++ b/Python/OAK/datasets/gal5000/_2_create_full_mask.py @@ -1,11 +1,13 @@ import json import os +import sys import cv2 import csv import shutil import argparse import numpy as np +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from utils import carregar_labelmap_completo # importa da sua utils.py # =================================================== diff --git a/Python/OAK/datasets/gal5000/_4_group_images_by_class.py b/Python/OAK/datasets/gal5000/_4_group_images_by_class.py index 7e328fa8b..9becc2e86 100644 --- a/Python/OAK/datasets/gal5000/_4_group_images_by_class.py +++ b/Python/OAK/datasets/gal5000/_4_group_images_by_class.py @@ -21,6 +21,7 @@ Saída: """ import os +import sys import cv2 import csv import json @@ -28,6 +29,7 @@ import shutil import argparse import numpy as np +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from utils import carregar_labelmap_completo, converter_mask_rgb_para_ids with open("config.json", "r", encoding="utf-8") as f: diff --git a/Python/OAK/datasets/gal5000/_6_normalize.py b/Python/OAK/datasets/gal5000/_6_normalize.py index e2d382ffa..aa9f2606c 100644 --- a/Python/OAK/datasets/gal5000/_6_normalize.py +++ b/Python/OAK/datasets/gal5000/_6_normalize.py @@ -21,11 +21,14 @@ RAW: import argparse import os import json +import sys import cv2 import numpy as np from typing import Dict, List, Tuple from gal5000.gal_service import mosaic_to_raw4_resized_buf from raw_segformer_service import _infer_ignore_id + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from utils import carregar_labelmap_completo, converter_mask_rgb_para_ids # ===== config ===== diff --git a/Python/OAK/datasets/gal5000/_9_test_segformer_b3.py b/Python/OAK/datasets/gal5000/_9_test_segformer_b3.py index 8deae3d69..5ae0f2240 100644 --- a/Python/OAK/datasets/gal5000/_9_test_segformer_b3.py +++ b/Python/OAK/datasets/gal5000/_9_test_segformer_b3.py @@ -21,6 +21,7 @@ Modos: import os import json +import sys import time import argparse from collections import deque @@ -29,11 +30,12 @@ import cv2 import torch import numpy as np -from utils import converter_mask_ids_para_bgr, desenhar_legenda_horizontal - from raw_segformer_service import RawSegformerService, RawSegDataset # :contentReference[oaicite:0]{index=0} from gal5000.gal_service import Gal5000Camera +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from utils import converter_mask_ids_para_bgr, desenhar_legenda_horizontal + # ============================================================ # Helpers para modo "sem máscara" diff --git a/Python/OAK/datasets/raw_segformer_service.py b/Python/OAK/datasets/gal5000/raw_segformer_service.py similarity index 99% rename from Python/OAK/datasets/raw_segformer_service.py rename to Python/OAK/datasets/gal5000/raw_segformer_service.py index 6cbf8995d..55b3288f5 100644 --- a/Python/OAK/datasets/raw_segformer_service.py +++ b/Python/OAK/datasets/gal5000/raw_segformer_service.py @@ -11,6 +11,7 @@ import os import json from contextlib import nullcontext +import sys from typing import List, Optional, Tuple import threading import time @@ -23,6 +24,8 @@ from torch.utils.data import Dataset import torch.nn.functional as F from transformers import SegformerForSemanticSegmentation +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + from utils import (carregar_labelmap_completo, converter_mask_ids_para_bgr) def build_raw_segformer_model( diff --git a/Python/OAK/datasets/multiespec_module/_0_capture.py b/Python/OAK/datasets/multiespec_module/_0_capture.py new file mode 100644 index 000000000..425abe06b --- /dev/null +++ b/Python/OAK/datasets/multiespec_module/_0_capture.py @@ -0,0 +1,822 @@ +import os +import time +import json +import argparse +from datetime import datetime + +import cv2 +import numpy as np + +from multispectral_service import MultiSpectralService +from stream_receiver import StreamReceiver +from pi.raw_processor_core import RawProcessorCore +from pi.raw_processor_preview import RawProcessorPreview + + +STREAM_PORT = 6001 +PI_HOST = "192.168.105.6" +PC_HOST = "192.168.105.5" + + +# ========================= +# Helpers gerais +# ========================= + +def ts_name() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] + + +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, +): + h, w = img_bgr.shape[:2] + + scale = h / float(base_h) + scale = max(scale, 0.4) + + font_scale = base_font_scale * scale + line_step = int(base_line_step * scale) + + thick_outline = max(1, int(3 * scale)) + thick_text = max(1, int(2 * scale)) + + y = int(24 * scale) + x = int(12 * scale) + + for s in lines: + cv2.putText(img_bgr, s, (x, y), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 0, 0), thick_outline, cv2.LINE_AA) + 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( + base_dir: str, + frame_type: str, + preview_bgr: np.ndarray, + meta: dict, + raw_payload: np.ndarray | None = None, + packed_raw: np.ndarray | None = None, + packed_raw_by_camera: dict | None = None, +): + os.makedirs(base_dir, exist_ok=True) + name = ts_name() + + png_path = os.path.join(base_dir, f"{name}.png") + json_path = os.path.join(base_dir, f"{name}.json") + + if frame_type in ("RGB", "MULTISPEC"): + if raw_payload is None: + raise ValueError(f"raw_payload não pode ser None quando frame_type='{frame_type}'") + + payload_path = os.path.join(base_dir, f"{name}.raw") + raw_payload.astype(np.float32).tofile(payload_path) + + meta["saved_payload_type"] = frame_type.lower() + meta["saved_payload_path"] = os.path.basename(payload_path) + meta["saved_payload_dtype"] = "float32" + meta["saved_payload_shape"] = list(raw_payload.shape) + + elif frame_type == "RAW_BRUTO": + if packed_raw_by_camera is not None: + payload_files = {} + payload_shapes = {} + payload_dtypes = {} + + for cam_id, arr in packed_raw_by_camera.items(): + path = os.path.join(base_dir, f"{name}_{cam_id}.bin") + arr.tofile(path) + payload_files[cam_id] = os.path.basename(path) + payload_shapes[cam_id] = list(arr.shape) + payload_dtypes[cam_id] = str(arr.dtype) + + meta["saved_payload_type"] = "raw_native_multi" + meta["saved_payload_paths"] = payload_files + meta["saved_payload_shapes"] = payload_shapes + meta["saved_payload_dtypes"] = payload_dtypes + + else: + if packed_raw is None: + raise ValueError("packed_raw não pode ser None quando frame_type='RAW_BRUTO'") + + payload_path = os.path.join(base_dir, f"{name}.bin") + packed_raw.tofile(payload_path) + + meta["saved_payload_type"] = "raw_native_single" + meta["saved_payload_path"] = os.path.basename(payload_path) + meta["saved_payload_dtype"] = str(packed_raw.dtype) + meta["saved_payload_shape"] = list(packed_raw.shape) + + else: + raise ValueError(f"frame_type não suportado para save: {frame_type}") + + cv2.imwrite(png_path, preview_bgr) + + with open(json_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + return png_path, json_path + + +def get_camera_map_from_status(status: dict) -> dict: + result = {} + for cam in status.get("cameras", []): + result[cam.get("id")] = cam + return result + + +def validate_module_ready(status: dict, frame_type: str, raw_policy: str, capture_mode: str): + if not status.get("ok", True): + raise RuntimeError(f"Status inválido retornado pelo módulo: {status}") + + active_ids = list(status.get("active_camera_ids", [])) + active_count = int(status.get("camera_count_active", 0)) + + if frame_type == "RGB": + if "cam2" not in active_ids: + raise RuntimeError( + "Modo RGB requer cam2 ativa (USB RGB), mas o módulo não reportou cam2 como ativa." + ) + return + + if frame_type == "MULTISPEC": + if capture_mode == "TRIPLE": + missing = [cid for cid in ("cam0", "cam1", "cam2") if cid not in active_ids] + if missing: + raise RuntimeError( + f"Modo MULTISPEC/TRIPLE requer cam0, cam1 e cam2 ativas. " + f"Faltando: {missing}. Ativas atuais: {active_ids}" + ) + return + + if capture_mode == "DOUBLE": + has_rgb = "cam2" in active_ids + has_spec = ("cam0" in active_ids) or ("cam1" in active_ids) + + if not has_rgb or not has_spec: + raise RuntimeError( + f"Modo MULTISPEC/DOUBLE requer cam2 + (cam0 ou cam1). " + f"Ativas atuais: {active_ids}" + ) + return + + # AUTO ou outros casos + has_rgb = "cam2" in active_ids + has_spec = ("cam0" in active_ids) or ("cam1" in active_ids) + + if not (has_rgb and has_spec): + raise RuntimeError( + f"Modo MULTISPEC requer pelo menos RGB + 1 canal espectral. " + f"Ativas atuais: {active_ids}" + ) + return + + if frame_type == "RAW_BRUTO": + if raw_policy == "require_triple": + missing = [cid for cid in ("cam0", "cam1", "cam2") if cid not in active_ids] + if missing: + raise RuntimeError( + f"RAW_BRUTO com política require_triple exige três câmeras ativas. " + f"Faltando: {missing}. Ativas atuais: {active_ids}" + ) + else: + if active_count < 1: + raise RuntimeError("RAW_BRUTO requer ao menos uma câmera ativa, mas nenhuma foi detectada.") + return + + raise RuntimeError(f"frame_type desconhecido para validação: {frame_type}") + + +def build_preview_from_raw_payload( + frame, + meta: dict, + processor_core: RawProcessorCore, + processor_preview: RawProcessorPreview, +): + """ + Gera preview priorizando a câmera RGB (cam2). + Se cam2 não estiver presente, cai para fallback usando a primeira câmera mono disponível. + Retorna: + preview_bgr + payload_float_preview + preview_source_id + """ + payload_sources = meta.get("payload_sources", []) or [] + + # Caso multi-payload: tenta usar cam2 primeiro + if isinstance(frame, dict): + if "cam2" in frame: + rgb_frame = frame["cam2"] + + if rgb_frame.ndim != 3 or rgb_frame.shape[2] != 3: + raise RuntimeError(f"cam2 recebida mas inválida para preview RGB: shape={rgb_frame.shape}") + + preview_bgr = rgb_frame.copy() + payload_float = rgb_frame[:, :, ::-1].astype(np.float32) / 255.0 + payload_float = np.transpose(payload_float, (2, 0, 1)) + + return preview_bgr, payload_float, "cam2" + + # fallback: usa a primeira câmera mono disponível + fallback_id = None + for cid in ("cam0", "cam1"): + if cid in frame: + fallback_id = cid + break + + if fallback_id is None: + raise RuntimeError("Nenhuma câmera disponível no payload para gerar preview") + + packed = frame[fallback_id] + if packed.ndim == 3 and packed.shape[2] == 1: + packed = packed[:, :, 0] + + cam_frames = meta.get("camera_frames", {}) or {} + cam_meta = cam_frames.get(fallback_id, {}) + bit_depth = int(cam_meta.get("bit_depth", 10)) + + raw16 = processor_core.unpack_raw10_packed(packed) + preview_bgr = processor_preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth) + + payload_float = processor_core.build_training_rgb( + raw16, + output_dtype="float32", + bit_depth=bit_depth, + ) + + return preview_bgr, payload_float, fallback_id + + # Caso single-payload + if isinstance(frame, np.ndarray): + # Se vier HWC/3ch, tratamos como RGB USB + if frame.ndim == 3 and frame.shape[2] == 3: + preview_bgr = frame.copy() + payload_float = frame[:, :, ::-1].astype(np.float32) / 255.0 + payload_float = np.transpose(payload_float, (2, 0, 1)) + return preview_bgr, payload_float, "cam2" + + # Se vier mono packed, fallback antigo + packed = frame + if packed.ndim == 3 and packed.shape[2] == 1: + packed = packed[:, :, 0] + + source_camera = meta.get("source_camera") or {} + bit_depth = int(source_camera.get("bit_depth", meta.get("source_bit_depth", 10))) + + raw16 = processor_core.unpack_raw10_packed(packed) + preview_bgr = processor_preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth) + + payload_float = processor_core.build_training_rgb( + raw16, + output_dtype="float32", + bit_depth=bit_depth, + ) + + return preview_bgr, payload_float, source_camera.get("id", "unknown") + + raise RuntimeError(f"Tipo de frame não suportado para preview: {type(frame)}") + + +def resolve_effective_capture_mode(frame_type: str, raw_policy: str, requested_mode: str) -> str: + """ + Decide o modo real que será pedido ao módulo. + + Nova regra: + - Se o usuário pediu explicitamente SINGLE/DOUBLE/TRIPLE, respeitamos. + - Se pediu AUTO, deixamos AUTO ir para o módulo. + - A única exceção opcional é MULTISPEC + require_triple implícito, + mas mesmo assim podemos deixar o módulo resolver se preferirmos. + """ + if requested_mode in ("SINGLE", "DOUBLE", "TRIPLE"): + return requested_mode + + # requested_mode == AUTO + return "AUTO" + +# ========================= +# MAIN +# ========================= + +def main(): + parser = argparse.ArgumentParser( + description="Captura de dataset usando módulo multispectral Pi + StreamReceiver.", + 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("--pi_host", default=PI_HOST, help="IP do servidor no Raspberry Pi.") + parser.add_argument("--pc_host", default=PC_HOST, help="IP local do notebook/PC que receberá o stream.") + parser.add_argument("--stream_port", type=int, default=STREAM_PORT, help="Porta TCP do receiver de stream.") + parser.add_argument("--server_port", type=int, default=5000, help="Porta TCP do servidor de comandos no Pi.") + parser.add_argument("--fps", type=int, default=20, help="FPS desejado.") + parser.add_argument("--width", type=int, default=640, help="Largura óptica da câmera.") + parser.add_argument("--height", type=int, default=480, help="Altura óptica da câmera.") + parser.add_argument("--interval", type=float, default=1.0, help="Intervalo em segundos para auto-save quando ligado.") + parser.add_argument("--preview_upscale", type=int, default=2, help="Fator de upscale visual do preview.") + parser.add_argument("--bayer", default="GBRG", choices=["GBRG", "GRBG", "RGGB", "BGGR"], help="Padrão Bayer das câmeras.") + parser.add_argument("--output_dtype", default="float32", choices=["uint8", "uint16", "float32"], help="Dtype do payload processado no Pi.") + parser.add_argument("--frame_type", default="RAW_BRUTO", choices=["RAW_BRUTO", "RGB", "MULTISPEC"], help="Tipo de payload pedido ao Pi.") + parser.add_argument("--capture_mode", default="AUTO", choices=["AUTO", "SINGLE", "DOUBLE", "TRIPLE"], help="Modo de captura desejado no módulo.") + parser.add_argument("--raw_policy", default="allow_single", choices=["allow_single", "require_triple"], help="Quando frame_type=RAW_BRUTO, define se o script aceita 1 câmera ou exige 3.") + + args = parser.parse_args() + + effective_capture_mode = resolve_effective_capture_mode( + frame_type=args.frame_type, + raw_policy=args.raw_policy, + requested_mode=args.capture_mode, + ) + + raw_w = args.width + raw_h = args.height + + session_dir = os.path.join( + 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 - Módulo Multiespectral") + print(f"Cana : {args.cana}") + print(f"Horário : {args.horario}") + print(f"Saída : {session_dir}") + print(f"Sensor : {raw_w}x{raw_h} | Bayer={args.bayer}") + print(f"FrameType : {args.frame_type}") + print(f"CaptureMode : {args.capture_mode} -> efetivo={effective_capture_mode}") + print(f"RAW policy : {args.raw_policy}") + print("============================================") + + auto_save = False + last_auto_t = 0.0 + preview_upscale = args.preview_upscale + + t_view_fps = time.time() + view_frames = 0 + fps_view = 0.0 + + t_stream_fps = time.time() + last_stream_frame_id = None + stream_frames_accum = 0 + fps_stream = 0.0 + + last_msg = "" + last_msg_t = 0.0 + + receiver = StreamReceiver(host="0.0.0.0", port=args.stream_port) + svc = MultiSpectralService(host=args.pi_host, port=args.server_port, timeout=10) + + print(f"[INFO] Verificando conexão com o módulo em {args.pi_host}:{args.server_port}...") + + svc.ensure_alive() + + if not svc.is_alive(): + raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.") + + print("[OK] Módulo conectado e respondendo.") + + window_name = "Dataset Capture (C/SPACE=save | A=auto-save | M=preview scale | Q=quit)" + cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) + + processor_core_cam0 = RawProcessorCore( + sensor_width=raw_w, + sensor_height=raw_h, + bayer_pattern=args.bayer, + ) + processor_preview_cam0 = RawProcessorPreview( + sensor_width=raw_w, + sensor_height=raw_h, + bayer_pattern=args.bayer, + ) + + last_frame_id = -1 + last_payload_float = None + last_packed_raw = None + last_packed_raw_by_camera = None + last_preview_bgr = None + last_meta_stream = None + + try: + receiver.start() + time.sleep(0.5) + + svc.connect() + + if args.frame_type in ("RAW_BRUTO", "MULTISPEC"): + modes_resp = svc.get_sensor_modes() + if not modes_resp.get("ok"): + print(f"[WARN] Falha ao obter sensor_modes: {modes_resp}") + else: + for mode in modes_resp.get("sensor_modes", []): + print( + f"[cam={mode.get('camera_id')} mode={mode.get('mode_index')}] " + f"size={mode.get('size')} " + f"format={mode.get('format')} " + f"bit_depth={mode.get('bit_depth')} " + f"fps={mode.get('fps')}" + ) + else: + print("[INFO] get_sensor_modes pulado para frame_type=RGB") + + print("SET CAM0 RES:", svc.set_camera_resolution(0, raw_w, raw_h)) + print("SET CAM1 RES:", svc.set_camera_resolution(1, raw_w, raw_h)) + print("SET CAM2 RES:", svc.set_camera_resolution(2, raw_w, raw_h)) + print("SET CAM0 BAYER:", svc.set_camera_bayer(0, args.bayer)) + print("SET CAM1 BAYER:", svc.set_camera_bayer(1, args.bayer)) + print("SET FPS:", svc.set_fps(args.fps)) + print("SET CAPTURE MODE:", svc.set_capture_mode(effective_capture_mode)) + print("SET FRAME TYPE:", svc.set_frame_type(args.frame_type)) + print("SET OUTPUT DTYPE:", svc.set_output_dtype(args.output_dtype)) + + begin_resp = svc.begin( + frame_type=args.frame_type, + output_dtype=args.output_dtype, + capture_mode=effective_capture_mode, + ) + print("BEGIN:", begin_resp) + + status = svc.get_status() + print("STATUS:", json.dumps({ + "status": status.get("status"), + "detected_mode": status.get("detected_mode"), + "camera_count_active": status.get("camera_count_active"), + "active_camera_ids": status.get("active_camera_ids"), + }, ensure_ascii=False)) + + validate_module_ready(status, args.frame_type, args.raw_policy, effective_capture_mode) + + print("START STREAM:", svc.start_stream(args.pc_host, args.stream_port, fps=args.fps)) + + camera_ctrl = svc.get_camera_controls() + + ae_enabled = bool(camera_ctrl.get("ae_enable", True)) + awb_enabled = bool(camera_ctrl.get("awb_enable", True)) + manual_exposure_us = camera_ctrl.get("exposure_time_us", None) + manual_gain = camera_ctrl.get("analogue_gain", None) + manual_colour_gains = camera_ctrl.get("colour_gains", None) + + while True: + t0 = time.time() + + meta = receiver.last_meta + frame = receiver.last_frame + + if meta is not None and frame is not None and meta.get("frame_id") != last_frame_id: + last_frame_id = meta["frame_id"] + + try: + frame_type = meta.get("frame_type", "RAW_BRUTO") + dtype_str = meta.get("dtype") or meta.get("output_dtype", "uint8") + preview_source_id = "cam2" + + if frame_type == "RAW_BRUTO": + if isinstance(frame, dict): + packed_by_camera = frame + + preview_bgr, raw3_preview, preview_source_id = build_preview_from_raw_payload( + frame=frame, + meta=meta, + processor_core=processor_core_cam0, + processor_preview=processor_preview_cam0, + ) + + last_packed_raw = None + last_packed_raw_by_camera = {cam_id: arr.copy() for cam_id, arr in packed_by_camera.items()} + last_payload_float = raw3_preview.copy() + + else: + preview_bgr, raw3_preview, preview_source_id = build_preview_from_raw_payload( + frame=frame, + meta=meta, + processor_core=processor_core_cam0, + processor_preview=processor_preview_cam0, + ) + + last_packed_raw = frame.copy() + last_packed_raw_by_camera = None + last_payload_float = raw3_preview.copy() + + elif frame_type == "RGB": + rgb_chw = frame + if not isinstance(rgb_chw, np.ndarray) or rgb_chw.ndim != 3: + raise RuntimeError(f"Frame RGB inválido: type={type(rgb_chw)}") + + if dtype_str == "uint8": + payload_float = rgb_chw.astype(np.float32) / 255.0 + elif dtype_str == "float32": + payload_float = rgb_chw.astype(np.float32) + elif dtype_str == "uint16": + payload_float = rgb_chw.astype(np.float32) / 65535.0 + else: + raise RuntimeError(f"dtype RGB não suportado: {dtype_str}") + + preview_rgb = np.transpose(payload_float, (1, 2, 0)) + preview_bgr = cv2.cvtColor( + np.clip(preview_rgb * 255.0, 0, 255).astype(np.uint8), + cv2.COLOR_RGB2BGR + ) + + last_payload_float = payload_float.copy() + last_packed_raw = None + last_packed_raw_by_camera = None + + elif frame_type == "MULTISPEC": + multispec_chw = frame + if not isinstance(multispec_chw, np.ndarray) or multispec_chw.ndim != 3 or multispec_chw.shape[0] not in (4, 5): + raise RuntimeError(f"Frame MULTISPEC inválido: shape={getattr(multispec_chw, 'shape', None)}") + + if dtype_str == "uint8": + payload_float = multispec_chw.astype(np.float32) / 255.0 + elif dtype_str == "float32": + payload_float = multispec_chw.astype(np.float32) + elif dtype_str == "uint16": + payload_float = multispec_chw.astype(np.float32) / 65535.0 + else: + raise RuntimeError(f"dtype MULTISPEC não suportado: {dtype_str}") + + preview_rgb = np.transpose(payload_float[:3], (1, 2, 0)) + preview_bgr = cv2.cvtColor( + np.clip(preview_rgb * 255.0, 0, 255).astype(np.uint8), + cv2.COLOR_RGB2BGR + ) + + last_payload_float = payload_float.copy() + last_packed_raw = None + last_packed_raw_by_camera = None + + else: + raise RuntimeError(f"frame_type não suportado neste script: {frame_type}") + + if preview_upscale and preview_upscale > 1: + preview_show = cv2.resize( + preview_bgr, + (preview_bgr.shape[1] * preview_upscale, preview_bgr.shape[0] * preview_upscale), + interpolation=cv2.INTER_NEAREST, + ) + else: + preview_show = preview_bgr.copy() + + curr_frame_id = meta.get("frame_id") + + if curr_frame_id is not None: + if last_stream_frame_id != curr_frame_id: + stream_frames_accum += 1 + + last_stream_frame_id = curr_frame_id + + dt_stream = time.time() - t_stream_fps + if dt_stream >= 1.0: + fps_stream = stream_frames_accum / dt_stream + stream_frames_accum = 0 + t_stream_fps = time.time() + + view_frames += 1 + dt_view = time.time() - t_view_fps + if dt_view >= 1.0: + fps_view = view_frames / dt_view + view_frames = 0 + t_view_fps = time.time() + + active_sources = meta.get("payload_sources") + lines = [ + f"CANA: {args.cana} | HORA: {args.horario} | Pasta: {os.path.basename(session_dir)}", + f"Type={meta.get('frame_type')} | CaptureMode={effective_capture_mode} | RAW policy={args.raw_policy}", + f"Sources={active_sources} | FPS_STREAM={fps_stream:.1f} | FPS_VIEW={fps_view:.1f}", + f"frame_id={meta.get('frame_id')} | layout={meta.get('output_layout')} | dtype={meta.get('dtype') or meta.get('output_dtype')}", + f"codec={meta.get('codec_name', meta.get('codec_family', '-'))} | comp={meta.get('dt_comp', 0):.4f}s | send={meta.get('dt_send_payload_prev', 0):.4f}s", + f"AE={'ON' if ae_enabled else 'OFF'} | AWB={'ON' if awb_enabled else 'OFF'} | EXP={manual_exposure_us} | GAIN={manual_gain}", + "Keys: C/SPACE=save | A=auto-save | E=AE | W=AWB | I/K=exp | O/L=gain | R=reset | M=preview | Q/Esc=quit", + ] + overlay_hud(preview_show, lines, base_h=raw_h) + + if last_msg and (time.time() - last_msg_t) < 2.0: + cv2.putText(preview_show, last_msg, (12, preview_show.shape[0] - 18), + cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, cv2.LINE_AA) + + cv2.imshow(window_name, preview_show) + + last_preview_bgr = preview_bgr.copy() + last_meta_stream = dict(meta) + + except Exception as e: + err = np.zeros((500, 1200, 3), dtype=np.uint8) + cv2.putText(err, f"Erro ao processar frame: {e}", (20, 60), + cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2, cv2.LINE_AA) + cv2.imshow(window_name, err) + print(f"[ERRO FRAME] {e}") + + now = time.time() + can_save = ( + last_meta_stream is not None and + last_preview_bgr is not None and + ( + (last_meta_stream.get("frame_type") in ("RGB", "MULTISPEC") and last_payload_float is not None) or + (last_meta_stream.get("frame_type") == "RAW_BRUTO" and (last_packed_raw is not None or last_packed_raw_by_camera is not None)) + ) + ) + + if auto_save and can_save and (now - last_auto_t) >= args.interval: + frame_type_save = last_meta_stream.get("frame_type") + + meta_save = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "cana": args.cana, + "horario": args.horario, + "sensor_width": raw_w, + "sensor_height": raw_h, + "bayer_pattern": args.bayer, + "fps_target": args.fps, + "frame_type": frame_type_save, + "capture_mode_requested": args.capture_mode, + "capture_mode_effective": effective_capture_mode, + "raw_policy": args.raw_policy, + "stream_meta": last_meta_stream, + "note": "autosave", + "raw_preview_reference_camera": preview_source_id, + } + + save_sample( + session_dir, + frame_type=frame_type_save, + preview_bgr=last_preview_bgr, + meta=meta_save, + raw_payload=last_payload_float, + packed_raw=last_packed_raw, + packed_raw_by_camera=last_packed_raw_by_camera, + ) + + last_msg = "SALVO (auto)" + 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("m"), ord("M")): + preview_upscale = 0 if preview_upscale else args.preview_upscale + last_msg = f"Preview UPSCALE -> {preview_upscale}" + last_msg_t = time.time() + + elif k in (ord("e"), ord("E")): + ae_enabled = not ae_enabled + resp = svc.set_ae_enable(ae_enabled) + ae_enabled = bool(resp.get("ae_enable", ae_enabled)) + last_msg = f"AE -> {'ON' if ae_enabled else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord("w"), ord("W")): + awb_enabled = not awb_enabled + resp = svc.set_awb_enable(awb_enabled) + awb_enabled = bool(resp.get("awb_enable", awb_enabled)) + last_msg = f"AWB -> {'ON' if awb_enabled else 'OFF'}" + last_msg_t = time.time() + + elif k in (ord("i"), ord("I")): + if manual_exposure_us is None: + manual_exposure_us = 15000 + else: + manual_exposure_us = min(int(manual_exposure_us * 1.15), 200000) + + if ae_enabled: + ae_enabled = False + svc.set_ae_enable(False) + + resp = svc.set_exposure_time(int(manual_exposure_us)) + manual_exposure_us = resp.get("exposure_time_us", manual_exposure_us) + last_msg = f"ExposureTime -> {manual_exposure_us} us" + last_msg_t = time.time() + + elif k in (ord("k"), ord("K")): + if manual_exposure_us is None: + manual_exposure_us = 15000 + else: + manual_exposure_us = max(int(manual_exposure_us / 1.15), 100) + + if ae_enabled: + ae_enabled = False + svc.set_ae_enable(False) + + resp = svc.set_exposure_time(int(manual_exposure_us)) + manual_exposure_us = resp.get("exposure_time_us", manual_exposure_us) + last_msg = f"ExposureTime -> {manual_exposure_us} us" + last_msg_t = time.time() + + elif k in (ord("o"), ord("O")): + if manual_gain is None: + manual_gain = 1.0 + else: + manual_gain = min(float(manual_gain) * 1.10, 32.0) + + if ae_enabled: + ae_enabled = False + svc.set_ae_enable(False) + + resp = svc.set_analogue_gain(float(manual_gain)) + manual_gain = resp.get("analogue_gain", manual_gain) + last_msg = f"AnalogueGain -> {manual_gain:.2f}" + last_msg_t = time.time() + + elif k in (ord("l"), ord("L")): + if manual_gain is None: + manual_gain = 1.0 + else: + manual_gain = max(float(manual_gain) / 1.10, 1.0) + + if ae_enabled: + ae_enabled = False + svc.set_ae_enable(False) + + resp = svc.set_analogue_gain(float(manual_gain)) + manual_gain = resp.get("analogue_gain", manual_gain) + last_msg = f"AnalogueGain -> {manual_gain:.2f}" + last_msg_t = time.time() + + elif k in (ord("r"), ord("R")): + svc.clear_exposure_time() + svc.clear_analogue_gain() + svc.clear_colour_gains() + + manual_exposure_us = None + manual_gain = None + manual_colour_gains = None + + last_msg = "Manual controls resetados" + last_msg_t = time.time() + + elif k in (ord("c"), ord("C"), 32): + if can_save: + frame_type_save = last_meta_stream.get("frame_type") + meta_save = { + "ts": datetime.now().isoformat(timespec="milliseconds"), + "cana": args.cana, + "horario": args.horario, + "sensor_width": raw_w, + "sensor_height": raw_h, + "bayer_pattern": args.bayer, + "fps_target": args.fps, + "frame_type": frame_type_save, + "capture_mode_requested": args.capture_mode, + "capture_mode_effective": effective_capture_mode, + "raw_policy": args.raw_policy, + "stream_meta": last_meta_stream, + "camera_controls": { + "ae_enable": ae_enabled, + "awb_enable": awb_enabled, + "exposure_time_us": manual_exposure_us, + "analogue_gain": manual_gain, + "colour_gains": manual_colour_gains, + }, + "note": "manual", + "raw_preview_reference_camera": preview_source_id, + } + + save_sample( + session_dir, + frame_type=frame_type_save, + preview_bgr=last_preview_bgr, + meta=meta_save, + raw_payload=last_payload_float, + packed_raw=last_packed_raw, + packed_raw_by_camera=last_packed_raw_by_camera, + ) + + last_msg = "SALVO (manual)" + last_msg_t = time.time() + + dt_loop = time.time() - t0 + if dt_loop < 0.001: + time.sleep(0.001) + + finally: + try: + print("STOP STREAM:", svc.stop_stream()) + except Exception: + pass + + try: + print("STOP:", svc.stop()) + except Exception: + pass + + svc.disconnect() + receiver.stop() + cv2.destroyAllWindows() + print("Fim da captura.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Python/OAK/datasets/multiespec_module/_12_check_percent_class.py b/Python/OAK/datasets/multiespec_module/_12_check_percent_class.py new file mode 100644 index 000000000..33ae702b9 --- /dev/null +++ b/Python/OAK/datasets/multiespec_module/_12_check_percent_class.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Conta a porcentagem de pixels por classe dentro da ROI, por grupo e por split, +na estrutura nova baseada em tensors/masks .npy. + +Estrutura esperada: + dataset/split//group//masks/ + original_xxx.npy + original_xxx.png # opcional, debug + augmented_xxx.npy + augmented_xxx.png # opcional, debug + +Regras: +- Prioriza máscaras .npy +- Se não encontrar .npy, pode usar .png como fallback +- Ignora pixels com ignore_id +- Normaliza a porcentagem somente sobre classes válidas +""" + +import argparse +import os +import json +import sys +import numpy as np +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from utils import carregar_labelmap_completo, _infer_ignore_id + + +# ================= CONFIG ================= +with open("config.json", "r", encoding="utf-8") as f: + config = json.load(f) + +W, H = config["resolucao"][0], config["resolucao"][1] +ROI_INICIO = float(config["roi_inicio"]) +ROI_TAMANHO = float(config["roi_tamanho"]) + +pasta_base = "dataset" +labelmap_path = os.path.join(pasta_base, "labelmap.txt") + +MAX_SAMPLES_PER_GROUP = 1000 + + +# ================= HELPERS ================= +def roi_slice(h: int): + """ + Replica a lógica antiga de ROI vertical. + """ + y_fim = int((1.0 - ROI_TAMANHO) * h) + y_ini = int(ROI_INICIO * h) + + if y_ini <= y_fim: + y_fim = max(0, h - int(ROI_TAMANHO * h)) + y_ini = h + + return slice(y_fim, y_ini) + + +def load_mask(path: str) -> np.ndarray: + """ + Carrega máscara .npy ou .png. + A máscara oficial agora é .npy (IDs inteiros). + """ + ext = os.path.splitext(path)[1].lower() + + if ext == ".npy": + arr = np.load(path) + if arr.ndim != 2: + raise RuntimeError(f"Máscara .npy inválida (esperado 2D): {path} shape={arr.shape}") + return arr.astype(np.int32) + + if ext == ".png": + import cv2 + m = cv2.imread(path, cv2.IMREAD_UNCHANGED) + if m is None: + raise RuntimeError(f"Falha ao ler PNG: {path}") + if m.ndim == 3: + # PNG de debug pode ter 3 canais; nesse caso ele não serve como máscara de IDs + raise RuntimeError( + f"PNG colorido encontrado em {path}. " + f"Para estatística, use a máscara .npy correspondente." + ) + return m.astype(np.int32) + + raise RuntimeError(f"Extensão de máscara não suportada: {path}") + + +def list_mask_files(mask_dir: str, prefer_npy: bool = True): + """ + Lista máscaras evitando contar duas vezes a mesma amostra + quando existem .npy e .png com o mesmo base. + """ + if not os.path.isdir(mask_dir): + return [] + + files = os.listdir(mask_dir) + by_base = {} + + for fname in files: + low = fname.lower() + if not (low.endswith(".npy") or low.endswith(".png")): + continue + + base, ext = os.path.splitext(fname) + ext = ext.lower() + full = os.path.join(mask_dir, fname) + + if base not in by_base: + by_base[base] = full + else: + cur_ext = os.path.splitext(by_base[base])[1].lower() + + if prefer_npy: + if cur_ext != ".npy" and ext == ".npy": + by_base[base] = full + else: + if cur_ext != ".png" and ext == ".png": + by_base[base] = full + + return [by_base[k] for k in sorted(by_base.keys())] + + +def summarize_group(mask_dir, class_ids_sorted, classes, ignore_id, max_samples): + totals = {cid: 0 for cid in class_ids_sorted} + ignored_pixels = 0 + valid_pixels = 0 + n = 0 + + for path in list_mask_files(mask_dir, prefer_npy=True): + mask = load_mask(path) + + rs = roi_slice(mask.shape[0]) + roi = mask[rs, :] + + ignored_pixels += int((roi == ignore_id).sum()) + + for cid in class_ids_sorted: + c = int((roi == cid).sum()) + totals[cid] += c + valid_pixels += c + + n += 1 + if n >= max_samples: + break + + return totals, valid_pixels, ignored_pixels, n + + +# ================= MAIN ================= +def main(args): + _, _colormap_rgb, classes, ignore_rgb = carregar_labelmap_completo(labelmap_path) + ignore_id = _infer_ignore_id(ignore_rgb, default_id=255) + + class_ids_sorted = sorted(classes.keys()) + + root = os.path.join(pasta_base, "split", args.split, "group") + if not os.path.isdir(root): + raise SystemExit(f"Nenhum diretório encontrado em: {root}") + + grupos = [g for g in os.listdir(root) if os.path.isdir(os.path.join(root, g))] + + global_totals = {cid: 0 for cid in class_ids_sorted} + global_valid_pixels = 0 + global_ignored_pixels = 0 + global_samples = 0 + + print(f"Split: {args.split}") + print(f"ROI: inicio={ROI_INICIO:.3f} tamanho={ROI_TAMANHO:.3f}") + print(f"Ignore ID: {ignore_id}") + print() + + for g in sorted(grupos): + mdir = os.path.join(root, g, "masks") + if not os.path.isdir(mdir): + continue + + totals, valid_pixels, ignored_pixels, n = summarize_group( + mask_dir=mdir, + class_ids_sorted=class_ids_sorted, + classes=classes, + ignore_id=ignore_id, + max_samples=args.max_samples, + ) + + denom = valid_pixels if valid_pixels > 0 else 1 + + parts = [] + for cid in class_ids_sorted: + name = classes[cid] + perc = totals[cid] / denom + parts.append(f"{name}={perc:6.2%}") + + ignore_ratio = ignored_pixels / max(1, (valid_pixels + ignored_pixels)) + + print( + f"{g:18s} " + + " ".join(parts) + + f" | ignore={ignore_ratio:6.2%} | amostras={n}" + ) + + for cid in class_ids_sorted: + global_totals[cid] += totals[cid] + global_valid_pixels += valid_pixels + global_ignored_pixels += ignored_pixels + global_samples += n + + print("\nResumo global:") + denom = global_valid_pixels if global_valid_pixels > 0 else 1 + parts = [] + for cid in class_ids_sorted: + name = classes[cid] + perc = global_totals[cid] / denom + parts.append(f"{name}={perc:6.2%}") + + ignore_ratio = global_ignored_pixels / max(1, (global_valid_pixels + global_ignored_pixels)) + print(" " + " ".join(parts)) + print(f" ignore={ignore_ratio:6.2%} | amostras={global_samples}") + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Verifica densidade de classes no dataset splitado.") + ap.add_argument("--split", type=str, default="train", help="Split para verificar: train, val ou test.") + ap.add_argument("--max-samples", type=int, default=MAX_SAMPLES_PER_GROUP, help="Máximo de amostras por grupo.") + args = ap.parse_args() + main(args) \ 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 index 45ce73cad..c72a77f4c 100644 --- a/Python/OAK/datasets/multiespec_module/_1_weeds_pair_sorter.py +++ b/Python/OAK/datasets/multiespec_module/_1_weeds_pair_sorter.py @@ -1,142 +1,161 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -raw_triplet_sorter.py +sample_bundle_sorter.py ------------------------------------------------- -Ferramenta para classificar manualmente amostras RAW do dataset -(arquivos .raw + .png + .json) em pastas de labels, -usando atalhos de teclado. +Classificador manual para o formato de dataset do módulo multiespectral. + +Cada amostra é formada por: + .png + .json + _cam0.bin + _cam1.bin + _cam2.bin # opcional 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. +- Entrada: uma ou mais pastas-raiz contendo amostras em subpastas. +- O script mostra o preview (.png). +- Você usa teclas 1..9/0 para enviar o conjunto da amostra para uma label. Estrutura de saída: out_root/