From 41dfb3c06e759ea9edc4c236999df644bb0d9f70 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Fri, 24 Apr 2026 15:03:44 -0300 Subject: [PATCH] Ajustes nas ferramentas do modulo multiespectral --- .../datasets/multiespec_module/_0_capture.py | 635 +- .../multiespec_module/_6_normalize.py | 14 +- .../multiespec_module/_8_train_segformer.py | 7 +- .../multiespec_module/_9_test_segformer.py | 212 +- .../datasets/multiespec_module/config.json | 2 +- .../multiespec_module/module_params.json | 77 + .../multispectral_service.py | 111 +- .../pi/multispectral_client.py | 304 + .../pi/raw_processor_core.py | 82 +- Python/raspi/build_module_params.py | 64 + Python/raspi/calibration/manual_offsets.json | 50 + Python/raspi/calibration/module_params.json | 77 + .../raspi/calibration/sensor_calibration.json | 8123 +---------------- Python/raspi/cam_3/capture_dataset.py | 824 -- Python/raspi/cam_3/multispectral_client.py | 218 + Python/raspi/cam_3/multispectral_service.py | 110 +- Python/raspi/cam_3/pi/camera_manager.py | 116 +- Python/raspi/cam_3/pi/raw_processor_core.py | 630 +- Python/raspi/cam_3/pi/server.py | 2 - Python/raspi/cam_3/pi/state.py | 1 + Python/raspi/check_saved_files.py | 4 +- Python/raspi/manual_fusion_calibrator.py | 703 +- Python/raspi/sensor_calibration_tool.py | 392 +- Python/raspi/test_service.py | 92 +- 24 files changed, 2776 insertions(+), 10074 deletions(-) create mode 100644 Python/OAK/datasets/multiespec_module/module_params.json create mode 100644 Python/OAK/datasets/multiespec_module/pi/multispectral_client.py create mode 100644 Python/raspi/build_module_params.py create mode 100644 Python/raspi/calibration/manual_offsets.json create mode 100644 Python/raspi/calibration/module_params.json delete mode 100644 Python/raspi/cam_3/capture_dataset.py create mode 100644 Python/raspi/cam_3/multispectral_client.py diff --git a/Python/OAK/datasets/multiespec_module/_0_capture.py b/Python/OAK/datasets/multiespec_module/_0_capture.py index 88de24f73..fff6d61d1 100644 --- a/Python/OAK/datasets/multiespec_module/_0_capture.py +++ b/Python/OAK/datasets/multiespec_module/_0_capture.py @@ -7,22 +7,13 @@ 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" +from pi.multispectral_client import MultiSpectralClient with open("config.json", "r", encoding="utf-8") as f: config = json.load(f) -MODELO = config.get("camera", ".") -RAW_SIZE = config.get("raw_size", [1296, 1028]) # [W, H] -CAMERA_PARAMS = config.get("camera_params_json") +RAW_SIZE = config.get("raw_size") # [W, H] +MODULE_PARAMS = config.get("module_params_json") # ========================= @@ -135,158 +126,6 @@ def get_camera_map_from_status(status: dict) -> dict: 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)}") - - # ========================= # MAIN # ========================= @@ -300,9 +139,9 @@ def main(): 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("--pi_host", default="192.168.105.6", help="IP do servidor no Raspberry Pi.") + parser.add_argument("--pc_host", default="192.168.105.5", help="IP local do notebook/PC que receberá o stream.") + parser.add_argument("--stream_port", type=int, default=6001, 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=RAW_SIZE[0], help="Largura óptica da câmera.") @@ -314,7 +153,7 @@ def main(): 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.") - parser.add_argument("--camera_params_json", default=CAMERA_PARAMS, help="JSON salvo pelo calibrador de sensores com parâmetros fixos por câmera.") + parser.add_argument("--module_calibration_json", default=MODULE_PARAMS, help="JSON salvo pelo calibrador de sensores com parâmetros fixos por câmera.") args = parser.parse_args() @@ -359,30 +198,9 @@ def main(): 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}...") - - if not svc.check_connection(2): - 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 @@ -391,278 +209,173 @@ def main(): 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( + with MultiSpectralClient( + pi_host=args.pi_host, + pc_host=args.pc_host, + server_port=args.server_port, + stream_port=args.stream_port, + width=raw_w, + height=raw_h, + bayer=args.bayer, + fps=args.fps, frame_type=args.frame_type, output_dtype=args.output_dtype, capture_mode=effective_capture_mode, - ) - print("BEGIN:", begin_resp) + raw_policy=args.raw_policy, + module_calibration_json=args.module_calibration_json, + ) as cam: + while True: + t0 = time.time() - 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)) + frame, meta = cam.get_next_frame(timeout=1.0) - validate_module_ready(status, args.frame_type, args.raw_policy, effective_capture_mode) + if meta is not None and frame is not None and meta.get("frame_id") != last_frame_id: + last_frame_id = meta["frame_id"] - params_resp = svc.apply_camera_params_json(args.camera_params_json) - if params_resp is not None: - camera_settings = params_resp["camera_settings"] - applied_camera_controls = params_resp["applied"] - print("[OK] Parâmetros fixos das câmeras aplicados:") - print(json.dumps(applied_camera_controls, ensure_ascii=False, indent=2)) - else: - print("[OK] Parâmetros fixos das câmeras não aplicados") + try: + frame_type = meta.get("frame_type", "RAW_BRUTO") + dtype_str = meta.get("dtype") or meta.get("output_dtype", "uint8") + preview_source_id = "cam2" - print("START STREAM:", svc.start_stream(args.pc_host, args.stream_port, fps=args.fps)) + if frame_type == "RAW_BRUTO": + if isinstance(frame, dict): + packed_by_camera = frame - while True: - t0 = time.time() + preview_bgr, raw3_preview, preview_source_id = cam.build_preview_from_raw_payload(frame=frame, meta=meta) - meta = receiver.last_meta - frame = receiver.last_frame + 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() - if meta is not None and frame is not None and meta.get("frame_id") != last_frame_id: - last_frame_id = meta["frame_id"] + else: + preview_bgr, raw3_preview, preview_source_id = cam.build_preview_from_raw_payload(frame=frame, meta=meta) - try: - frame_type = meta.get("frame_type", "RAW_BRUTO") - dtype_str = meta.get("dtype") or meta.get("output_dtype", "uint8") - preview_source_id = "cam2" + 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 frame_type == "RAW_BRUTO": - if isinstance(frame, dict): - packed_by_camera = frame + 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_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, + 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 = {cam_id: arr.copy() for cam_id, arr in packed_by_camera.items()} - last_payload_float = raw3_preview.copy() + last_packed_raw_by_camera = None - 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, + 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_packed_raw = frame.copy() + last_payload_float = payload_float.copy() + last_packed_raw = None 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}") + raise RuntimeError(f"frame_type não suportado neste script: {frame_type}") - 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 + 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: - raise RuntimeError(f"dtype MULTISPEC não suportado: {dtype_str}") + preview_show = preview_bgr.copy() - 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 - ) + curr_frame_id = meta.get("frame_id") - last_payload_float = payload_float.copy() - last_packed_raw = None - last_packed_raw_by_camera = None + if curr_frame_id is not None: + if last_stream_frame_id != curr_frame_id: + stream_frames_accum += 1 - else: - raise RuntimeError(f"frame_type não suportado neste script: {frame_type}") + last_stream_frame_id = curr_frame_id - 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() + 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() - curr_frame_id = meta.get("frame_id") + 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() - if curr_frame_id is not None: - if last_stream_frame_id != curr_frame_id: - stream_frames_accum += 1 + 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"CAM_PARAMS={os.path.basename(args.module_calibration_json)} | controles fixos aplicados", + "Keys: C/SPACE=save | A=auto-save | M=preview | Q/Esc=quit" + ] + overlay_hud(preview_show, lines, base_h=raw_h) - last_stream_frame_id = curr_frame_id + 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) - 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() + cv2.imshow(window_name, preview_show) - 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() + last_preview_bgr = preview_bgr.copy() + last_meta_stream = dict(meta) - 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"CAM_PARAMS={os.path.basename(args.camera_params_json)} | controles fixos aplicados", - "Keys: C/SPACE=save | A=auto-save | M=preview | Q/Esc=quit" - ] - overlay_hud(preview_show, lines, base_h=raw_h) + 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}") - 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, - "applied_camera_controls": applied_camera_controls, - "camera_params_json": args.camera_params_json, - "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, + 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)) + ) ) - 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("c"), ord("C"), 32): - if can_save: + 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, @@ -676,9 +389,9 @@ def main(): "capture_mode_effective": effective_capture_mode, "raw_policy": args.raw_policy, "stream_meta": last_meta_stream, - "applied_camera_controls": applied_camera_controls, - "camera_params_json": args.camera_params_json, - "note": "manual", + "applied_camera_controls": cam.applied_camera_controls, + "camera_params_json": args.module_calibration_json, + "note": "autosave", "raw_preview_reference_camera": preview_source_id, } @@ -692,26 +405,64 @@ def main(): packed_raw_by_camera=last_packed_raw_by_camera, ) - last_msg = "SALVO (manual)" + 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() - dt_loop = time.time() - t0 - if dt_loop < 0.001: - time.sleep(0.001) + 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("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, + "applied_camera_controls": cam.applied_camera_controls, + "camera_params_json": args.module_calibration_json, + "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.") diff --git a/Python/OAK/datasets/multiespec_module/_6_normalize.py b/Python/OAK/datasets/multiespec_module/_6_normalize.py index a29d0ff09..d7557743b 100644 --- a/Python/OAK/datasets/multiespec_module/_6_normalize.py +++ b/Python/OAK/datasets/multiespec_module/_6_normalize.py @@ -32,6 +32,8 @@ with open("config.json", "r", encoding="utf-8") as f: config = json.load(f) RES = tuple(config["resolucao"]) # (W,H) +raw_w, raw_h = tuple(config["raw_size"]) # (W,H) +MODULE_PARAMS = config.get("module_params_json") pasta_base = "dataset" INPUTS = [ @@ -71,7 +73,7 @@ def process(): cor_para_id, _, _, ignore_rgb = carregar_labelmap_completo(labelmap_path) ignore_id = _infer_ignore_id(ignore_rgb, 255) - core = RawProcessorCore(640, 480) + core = RawProcessorCore(raw_w, raw_h, calibration_json_path=MODULE_PARAMS) total = 0 @@ -127,15 +129,7 @@ def process(): bins_meta.append(cam_meta) # ===== BUILD TENSOR ===== - tensor, channel_names = core.build_multispectral_tensor(bins_data, bins_meta) - - # ===== RESIZE ===== - chans = [] - for ch in tensor: - ch_res = cv2.resize(ch, RES, interpolation=cv2.INTER_AREA) - chans.append(ch_res.astype(np.float32)) - - tensor = np.stack(chans, axis=0) + tensor, channel_names = core.build_multispectral_tensor(bins_data, bins_meta, target_size=RES) # ===== STATS ===== c, h, w = tensor.shape diff --git a/Python/OAK/datasets/multiespec_module/_8_train_segformer.py b/Python/OAK/datasets/multiespec_module/_8_train_segformer.py index 5a6bbd186..c68a8a43c 100644 --- a/Python/OAK/datasets/multiespec_module/_8_train_segformer.py +++ b/Python/OAK/datasets/multiespec_module/_8_train_segformer.py @@ -342,10 +342,8 @@ def main(): with open(args.config, "r") as f: config = json.load(f) - MODELO = config["camera"] MODEL_NAME = config["model_name"] - RESOLUCAO = config["resolucao"] - W, H = RESOLUCAO[0], RESOLUCAO[1] + W, H = config["resolucao"] MAIN_CLASS_NAME = str(config.get("main_class_name", "erva")).lower() # novos parâmetros do config @@ -415,8 +413,7 @@ def main(): resize_hw = (args.resize_h, args.resize_w) else: # Se quiser, pode forçar pra RESOLUCAO do config (H,W) - # resize_hw = (RESOLUCAO[1], RESOLUCAO[0]) - pass + resize_hw = (H, W) # Datasets (RAW com 4 ou 5 canais) ds_train = MultispecSegDataset( diff --git a/Python/OAK/datasets/multiespec_module/_9_test_segformer.py b/Python/OAK/datasets/multiespec_module/_9_test_segformer.py index e8cbfaf56..b0e010bb9 100644 --- a/Python/OAK/datasets/multiespec_module/_9_test_segformer.py +++ b/Python/OAK/datasets/multiespec_module/_9_test_segformer.py @@ -28,10 +28,7 @@ import numpy as np sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from utils import converter_mask_ids_para_bgr, desenhar_legenda_horizontal -from multispec_segformer_service import ( - MultispecSegformerService, - MultispecSegDataset, -) +from multispec_segformer_service import (MultispecSegformerService, MultispecSegDataset) # ============================================================ @@ -97,9 +94,9 @@ def main(): parser.add_argument("--resize_h", type=int, default=None, help="Altura para inferência (override)") parser.add_argument("--resize_w", type=int, default=None, help="Largura para inferência (override)") parser.add_argument("--alpha", type=float, default=0.45, help="Alpha do overlay da máscara") - parser.add_argument("--camera_frame_type", type=str, default="MULTISPEC", choices=["RAW_BRUTO", "RGB", "MULTISPEC"]) + parser.add_argument("--camera_frame_type", type=str, default="RAW_BRUTO", choices=["RAW_BRUTO", "RGB", "MULTISPEC"]) parser.add_argument("--camera_capture_mode", type=str, default="AUTO", choices=["AUTO", "SINGLE", "DOUBLE", "TRIPLE"]) - parser.add_argument("--camera_params_json", default=None, help="JSON salvo pelo calibrador de sensores com parâmetros fixos por câmera.") + parser.add_argument("--module_calibration_json", default=None, help="JSON salvo pelo calibrador de sensores com parâmetros fixos por câmera.") args = parser.parse_args() @@ -110,17 +107,16 @@ def main(): with open(args.config, "r", encoding="utf-8") as f: config = json.load(f) - MODELO = config["camera"] MODEL_NAME = config["model_name"] modelo_folder = config["modelo"] - CAMERA_PARAMS = config.get("camera_params_json") - if args.camera_params_json is not None: - CAMERA_PARAMS = args.camera_params_json + MODULE_PARAMS = config.get("module_params_json") + if args.module_calibration_json is not None: + MODULE_PARAMS = args.module_calibration_json CHANNELS = int(config.get("channels", 5)) FUSION_MODE = config.get("fusion_mode", "stacked") - RESOLUCAO = config["resolucao"] - W, H = RESOLUCAO[0], RESOLUCAO[1] + W, H = config["resolucao"] + faw_w, raw_h = config["raw_size"] if args.resize_h is not None: H = args.resize_h if args.resize_w is not None: @@ -205,50 +201,7 @@ def main(): if args.camera: print("[mode] Câmera MULTISPEC + SegFormer") - from multispectral_service import MultiSpectralService - from stream_receiver import StreamReceiver - from pi.raw_processor_core import RawProcessorCore - - STREAM_PORT = 6001 - PI_HOST = "192.168.105.6" - PC_HOST = "192.168.105.5" - - receiver = StreamReceiver(host="0.0.0.0", port=STREAM_PORT) - svc = MultiSpectralService(host=PI_HOST, port=5000, timeout=10) - - if not svc.check_connection(2): - raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.") - - receiver.start() - time.sleep(0.5) - - svc.connect() - - core = RawProcessorCore(sensor_width=W, sensor_height=H) - camera_frame_type = args.camera_frame_type - camera_output_dtype = "uint8" - camera_capture_mode = args.camera_capture_mode - - print("SET FRAME TYPE:", svc.set_frame_type(camera_frame_type)) - print("SET OUTPUT DTYPE:", svc.set_output_dtype(camera_output_dtype)) - print("SET FPS:", svc.set_fps(15)) - - print("BEGIN:", svc.begin( - frame_type=camera_frame_type, - output_dtype=camera_output_dtype, - capture_mode=camera_capture_mode, - )) - - params_resp = svc.apply_camera_params_json(CAMERA_PARAMS) - if params_resp is not None: - camera_settings = params_resp["camera_settings"] - applied_camera_controls = params_resp["applied"] - print("[OK] Parâmetros fixos das câmeras aplicados:") - print(json.dumps(applied_camera_controls, ensure_ascii=False, indent=2)) - else: - print("[OK] Parâmetros fixos das câmeras não aplicados") - - print("START STREAM:", svc.start_stream(PC_HOST, STREAM_PORT, fps=15)) + from pi.multispectral_client import MultiSpectralClient win = "CAMERA MULTISPEC + SEGFORMER (Q=quit)" cv2.namedWindow(win, cv2.WINDOW_NORMAL) @@ -261,94 +214,95 @@ def main(): fps_smooth = 0.15 try: - while True: - meta = receiver.last_meta - frame = receiver.last_frame + with MultiSpectralClient( + pi_host="192.168.105.6", + pc_host="192.168.105.5", + server_port=5000, + stream_port=6001, + width=raw_h, + height=raw_h, + fps=15, + frame_type=args.camera_frame_type, + output_dtype="uint8", + capture_mode=args.camera_capture_mode, + raw_policy="allow_single", + module_calibration_json=MODULE_PARAMS, + ) as cam: + while True: + frame, meta = cam.get_next_frame(timeout=0.5) - if meta is None or frame is None: - continue + if meta is None or frame is None: + continue - frame_id = meta.get("frame_id") - if frame_id == last_frame_id: - continue + frame_id = meta.get("frame_id") + if frame_id == last_frame_id: + continue - last_frame_id = frame_id - now_pc = time.perf_counter() + last_frame_id = frame_id + now_pc = time.perf_counter() - # FPS real percebido no PC - if last_pc_frame_ts is not None: - dt_pc = now_pc - last_pc_frame_ts - if dt_pc > 0: - inst_fps_pc = 1.0 / dt_pc - if fps_pc <= 0: - fps_pc = inst_fps_pc + # FPS real percebido no PC + if last_pc_frame_ts is not None: + dt_pc = now_pc - last_pc_frame_ts + if dt_pc > 0: + inst_fps_pc = 1.0 / dt_pc + if fps_pc <= 0: + fps_pc = inst_fps_pc + else: + fps_pc = (1.0 - fps_smooth) * fps_pc + fps_smooth * inst_fps_pc + last_pc_frame_ts = now_pc + + # FPS reportado pelo módulo/Pi + dt_frame_period = meta.get("dt_frame_period") + if dt_frame_period is not None and dt_frame_period > 0: + inst_fps_pi = 1.0 / float(dt_frame_period) + if fps_pi <= 0: + fps_pi = inst_fps_pi else: - fps_pc = (1.0 - fps_smooth) * fps_pc + fps_smooth * inst_fps_pc - last_pc_frame_ts = now_pc + fps_pi = (1.0 - fps_smooth) * fps_pi + fps_smooth * inst_fps_pi - # FPS reportado pelo módulo/Pi - dt_frame_period = meta.get("dt_frame_period") - if dt_frame_period is not None and dt_frame_period > 0: - inst_fps_pi = 1.0 / float(dt_frame_period) - if fps_pi <= 0: - fps_pi = inst_fps_pi - else: - fps_pi = (1.0 - fps_smooth) * fps_pi + fps_smooth * inst_fps_pi + try: + raw_np = cam.build_infer_tensor(frame, meta, channels_expected=CHANNELS, target_size=(W, H)) - try: - raw_np = core.build_infer_tensor_from_stream(frame, meta, channels_expected=CHANNELS) + # ========================================================= + # INFERÊNCIA + # ========================================================= + pred_ids, preview_bgr, pred_rgb, overlay, t_inf, t_pvw = model_svc.infer_and_preview(raw_np, alpha=args.alpha) - # ========================================================= - # INFERÊNCIA - # ========================================================= - pred_ids, preview_bgr, pred_rgb, overlay, t_inf, t_pvw = model_svc.infer_and_preview(raw_np, alpha=args.alpha) + # ========================================================= + # HUD + # ========================================================= + lines = [ + f"C={CHANNELS}", + f"fps_pc={fps_pc:.1f} | fps_pi={fps_pi:.1f}", + f"inf={t_inf:.1f}ms | pvw={t_pvw:.1f}ms" + ] - # ========================================================= - # HUD - # ========================================================= - lines = [ - f"C={CHANNELS}", - f"fps_pc={fps_pc:.1f} | fps_pi={fps_pi:.1f}", - f"inf={t_inf:.1f}ms | pvw={t_pvw:.1f}ms" - ] + y0 = 24 + dy = 28 # espaçamento entre linhas - y0 = 24 - dy = 28 # espaçamento entre linhas + for i, line in enumerate(lines): + y = y0 + i * dy + cv2.putText( + overlay, + line, + (10, y), + cv2.FONT_HERSHEY_SIMPLEX, + 0.7, + (0, 255, 0), + 2, + ) - for i, line in enumerate(lines): - y = y0 + i * dy - cv2.putText( - overlay, - line, - (10, y), - cv2.FONT_HERSHEY_SIMPLEX, - 0.7, - (0, 255, 0), - 2, - ) + cv2.imshow(win, overlay) - cv2.imshow(win, overlay) + except Exception as e: + print("[ERRO FRAME]", e) - except Exception as e: - print("[ERRO FRAME]", e) - - key = cv2.waitKey(1) & 0xFF - if key in (ord("q"), ord("Q"), 27): - break + key = cv2.waitKey(1) & 0xFF + if key in (ord("q"), ord("Q"), 27): + break finally: - try: - svc.stop_stream() - except: - pass - - try: - svc.stop() - except: - pass - - svc.disconnect() - receiver.stop() cv2.destroyAllWindows() return diff --git a/Python/OAK/datasets/multiespec_module/config.json b/Python/OAK/datasets/multiespec_module/config.json index 68f433ee9..c664fa8f7 100644 --- a/Python/OAK/datasets/multiespec_module/config.json +++ b/Python/OAK/datasets/multiespec_module/config.json @@ -16,5 +16,5 @@ "backbone": "nvidia/mit-b1", "fusion_mode": "stacked", "stats_source_tag": "stacked_raw4", - "camera_params_json": "camera_params.json" + "module_params_json": "module_params.json" } \ No newline at end of file diff --git a/Python/OAK/datasets/multiespec_module/module_params.json b/Python/OAK/datasets/multiespec_module/module_params.json new file mode 100644 index 000000000..6196e60fe --- /dev/null +++ b/Python/OAK/datasets/multiespec_module/module_params.json @@ -0,0 +1,77 @@ +{ + "schema": "multispec_module_params_v1", + "saved_at": "2026-04-24 10:46:59", + "frame_type": "RAW_BRUTO", + "capture_mode_requested": "AUTO", + "capture_mode_effective": "AUTO", + "raw_policy": "allow_single", + "sensor_width": 640, + "sensor_height": 480, + "bayer_pattern": "GBRG", + "camera_settings": { + "cam0": { + "ae_enable": false, + "awb_enable": false, + "exposure_time_us": 15000, + "analogue_gain": 1.0, + "colour_gains": null + }, + "cam1": { + "ae_enable": false, + "awb_enable": false, + "exposure_time_us": 15000, + "analogue_gain": 1.0, + "colour_gains": null + }, + "cam2": { + "ae_enable": true, + "awb_enable": true, + "exposure_time_us": 15000, + "analogue_gain": 1.0, + "colour_gains": [ + 1.0, + 1.0 + ] + } + }, + "fusion_config": { + "alignment_mode": "homography", + "baseline_mm": 75.0, + "reference_camera": "cam2", + "manual_offsets": { + "cam0": { + "dx": 0, + "dy": 0, + "theta_deg": 0.0 + }, + "cam1": { + "dx": 0, + "dy": 0, + "theta_deg": 0.0 + } + }, + "homographies": { + "cam0_to_cam2": [ + [ + 0.6438707381367006, + -0.44541142339026096, + 171.26012619956475 + ], + [ + -0.04083362402731768, + 0.6855417219007448, + 17.531834653386724 + ], + [ + -0.00015145530359180688, + -0.0006687764363592821, + 1.0 + ] + ], + "cam1_to_cam2": null + }, + "crop_valid_common": true, + "resize_after_crop": true, + "target_size": null + } +} \ No newline at end of file diff --git a/Python/OAK/datasets/multiespec_module/multispectral_service.py b/Python/OAK/datasets/multiespec_module/multispectral_service.py index 211917ef5..5a55717ce 100644 --- a/Python/OAK/datasets/multiespec_module/multispectral_service.py +++ b/Python/OAK/datasets/multiespec_module/multispectral_service.py @@ -95,6 +95,68 @@ class MultiSpectralService: return json.loads(line.strip()) + def validate_module_ready(self, 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}") + + # ========================================================= # Helpers numpy # ========================================================= @@ -259,13 +321,20 @@ class MultiSpectralService: def get_config(self): return self._send_command({"cmd": "get_config"}) - def begin(self, frame_type: str = "RAW_BRUTO", output_dtype: str = "uint8", capture_mode: str = "AUTO"): - return self._send_command({ - "cmd": "begin", - "frame_type": frame_type, - "output_dtype": output_dtype, - "capture_mode": capture_mode, - }) + def begin(self, frame_type="RAW_BRUTO", output_dtype="uint8", capture_mode="AUTO", timeout=15): + old_timeout = self.sock.gettimeout() if self.sock else None + if self.sock and timeout is not None: + self.sock.settimeout(timeout) + try: + return self._send_command({ + "cmd": "begin", + "frame_type": frame_type, + "output_dtype": output_dtype, + "capture_mode": capture_mode, + }) + finally: + if self.sock and old_timeout is not None: + self.sock.settimeout(old_timeout) def stop(self): return self._send_command({"cmd": "stop"}) @@ -306,6 +375,31 @@ class MultiSpectralService: "width": width, "height": height }) + + def set_resolution(self, width: int, height: int): + res = [] + for index in range(0, 3): + res.append( + self._send_command({ + "cmd": "set_camera_resolution", + "index": index, + "width": width, + "height": height + }) + ) + return res + + def set_bayer(self, bayer_pattern: str): + res = [] + for index in range(0, 2): + res.append( + self._send_command({ + "cmd": "set_camera_bayer", + "index": index, + "pattern": bayer_pattern + }) + ) + return res # ========================================================= # Captura @@ -430,7 +524,8 @@ class MultiSpectralService: def get_sensor_modes(self): return self._send_command({"cmd": "get_sensor_modes"}) - + + def load_camera_params_json(self, path: str) -> dict: if not path or not os.path.isfile(path): raise FileNotFoundError(f"Arquivo de parâmetros das câmeras não encontrado: {path}") diff --git a/Python/OAK/datasets/multiespec_module/pi/multispectral_client.py b/Python/OAK/datasets/multiespec_module/pi/multispectral_client.py new file mode 100644 index 000000000..a67ffe5cb --- /dev/null +++ b/Python/OAK/datasets/multiespec_module/pi/multispectral_client.py @@ -0,0 +1,304 @@ +import time +import json + +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 + + +class MultiSpectralClient: + def __init__( + self, + pi_host="192.168.105.6", + pc_host="192.168.105.5", + server_port=5000, + stream_port=6001, + timeout=10, + width=640, + height=480, + bayer="GBRG", + fps=15, + frame_type="RAW_BRUTO", + output_dtype="uint8", + capture_mode="AUTO", + raw_policy="allow_single", + module_calibration_json=None, + ): + self.pi_host = pi_host + self.pc_host = pc_host + self.server_port = server_port + self.stream_port = stream_port + + self.width = width + self.height = height + self.bayer = bayer + self.fps = fps + self.frame_type = frame_type + self.output_dtype = output_dtype + self.capture_mode = capture_mode + self.raw_policy = raw_policy + self.module_calibration_json = module_calibration_json + + self.svc = MultiSpectralService( + host=pi_host, + port=server_port, + timeout=timeout, + ) + + self.receiver = StreamReceiver( + host="0.0.0.0", + port=stream_port, + ) + + self.core = RawProcessorCore( + sensor_width=width, + sensor_height=height, + bayer_pattern=bayer, + calibration_json_path=module_calibration_json, + ) + + self.preview = RawProcessorPreview( + sensor_width=width, + sensor_height=height, + bayer_pattern=bayer, + ) + + self.last_frame_id = None + self.status = None + self.begin_resp = None + self.applied_params = None + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc, tb): + self.stop() + + def start(self, print_debug=True): + if print_debug: + print(f"[INFO] Verificando módulo em {self.pi_host}:{self.server_port}...") + + if not self.svc.check_connection(2): + raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.") + + self.receiver.start() + time.sleep(0.5) + + self.svc.connect() + + if print_debug: + print("[OK] Módulo conectado.") + + self._configure_module(print_debug=print_debug) + self._begin_module(print_debug=print_debug) + self._apply_module_params(print_debug=print_debug) + self._start_stream(print_debug=print_debug) + + return self + + def _configure_module(self, print_debug=True): + r0 = self.svc.set_resolution(self.width, self.height) + r1 = self.svc.set_bayer(self.bayer) + r2 = self.svc.set_fps(self.fps) + r3 = self.svc.set_capture_mode(self.capture_mode) + r4 = self.svc.set_frame_type(self.frame_type) + r5 = self.svc.set_output_dtype(self.output_dtype) + if print_debug: + print("SET RES:", r0) + print("SET BAYER:", r1) + print("SET FPS:", r2) + print("SET CAPTURE MODE:", r3) + print("SET FRAME TYPE:", r4) + print("SET OUTPUT DTYPE:", r5) + + def _begin_module(self, print_debug=True): + self.begin_resp = self.svc.begin( + frame_type=self.frame_type, + output_dtype=self.output_dtype, + capture_mode=self.capture_mode, + ) + + self.status = self.svc.get_status() + + self.svc.validate_module_ready( + self.status, + self.frame_type, + self.raw_policy, + self.capture_mode, + ) + + if print_debug: + print("BEGIN:", self.begin_resp) + print("STATUS:", json.dumps({ + "status": self.status.get("status"), + "detected_mode": self.status.get("detected_mode"), + "camera_count_active": self.status.get("camera_count_active"), + "active_camera_ids": self.status.get("active_camera_ids"), + }, ensure_ascii=False)) + + def _apply_module_params(self, print_debug=True): + if not self.module_calibration_json: + return None + + self.applied_params = self.svc.apply_camera_params_json( + self.module_calibration_json + ) + + if print_debug: + print("[OK] Parâmetros do módulo aplicados:") + print(json.dumps(self.applied_params.get("applied"), ensure_ascii=False, indent=2)) + + return self.applied_params + + def _start_stream(self, print_debug=True): + resp = self.svc.start_stream( + self.pc_host, + self.stream_port, + fps=self.fps, + ) + + if print_debug: + print("START STREAM:", resp) + + return resp + + def get_latest(self): + return self.receiver.last_frame, self.receiver.last_meta + + def get_next_frame(self, timeout=2.0): + t0 = time.perf_counter() + + while time.perf_counter() - t0 < timeout: + frame = self.receiver.last_frame + meta = self.receiver.last_meta + + if frame is None or meta is None: + time.sleep(0.001) + continue + + frame_id = meta.get("frame_id") + + if frame_id != self.last_frame_id: + self.last_frame_id = frame_id + return frame, meta + + time.sleep(0.001) + + raise TimeoutError("Timeout aguardando novo frame do stream.") + + def build_infer_tensor(self, frame, meta, channels_expected, target_size=None): + return self.core.build_infer_tensor_from_stream( + frame, + meta, + channels_expected=channels_expected, + target_size=target_size, + ) + + def build_preview_from_raw_payload(self, frame, meta: dict): + """ + 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 = self.core.unpack_raw10_packed(packed) + preview_bgr = self.preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth) + + payload_float = self.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 = self.core.unpack_raw10_packed(packed) + preview_bgr = self.preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth) + + payload_float = self.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 stop(self): + try: + self.svc.stop_stream() + except Exception: + pass + + try: + self.svc.stop() + except Exception: + pass + + try: + self.svc.disconnect() + except Exception: + pass + + try: + self.receiver.stop() + except Exception: + pass \ No newline at end of file diff --git a/Python/OAK/datasets/multiespec_module/pi/raw_processor_core.py b/Python/OAK/datasets/multiespec_module/pi/raw_processor_core.py index a44027aff..c8e3b56ef 100644 --- a/Python/OAK/datasets/multiespec_module/pi/raw_processor_core.py +++ b/Python/OAK/datasets/multiespec_module/pi/raw_processor_core.py @@ -1,3 +1,4 @@ +import json import os import cv2 import numpy as np @@ -6,7 +7,7 @@ from typing import Optional class RawProcessorCore: - def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"): + def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG", calibration_json_path=None): self.sensor_width = sensor_width self.sensor_height = sensor_height self.bayer_pattern = bayer_pattern.upper() @@ -25,6 +26,8 @@ class RawProcessorCore: "resize_after_crop": True, "target_size": None, # (w, h) ou None para manter o shape da RGB } + if calibration_json_path: + self.load_fusion_config_json(calibration_json_path) def unpack_raw10_packed( self, @@ -181,14 +184,22 @@ class RawProcessorCore: return decoded - def build_multispectral_tensor(self, bins_data, bins_meta): + def build_multispectral_tensor(self, bins_data, bins_meta, target_size=None): decoded = self.decode_bins_cameras(bins_data, bins_meta) if "cam2" not in decoded: raise RuntimeError("RGB obrigatório") channel_names = self._channel_names_from_decoded(decoded) - tensor = self.fuse_multispec_cameras(decoded, meta=None, channels_expected=len(channel_names)) + + tensor = self.fuse_multispec_cameras( + decoded, + meta=None, + channels_expected=len(channel_names) + ) + + tensor = self.resize_tensor_chw(tensor, target_size=target_size) + return tensor, channel_names def build_infer_tensor_from_stream_old(self, frame, meta, channels_expected): @@ -306,15 +317,17 @@ class RawProcessorCore: raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}") - def build_infer_tensor_from_stream(self, frame, meta, channels_expected): + def build_infer_tensor_from_stream(self, frame, meta, channels_expected, target_size=None): frame_type = meta.get("frame_type") if frame_type == "RAW_BRUTO": decoded = self.decode_stream_cameras(frame, meta) - return self.fuse_multispec_cameras(decoded, meta, channels_expected) + tensor = self.fuse_multispec_cameras(decoded, meta, channels_expected) + return self.resize_tensor_chw(tensor, target_size=target_size) if frame_type in ("RGB", "MULTISPEC"): - return self.build_infer_tensor_from_stream_old(frame, meta, channels_expected) + tensor = self.build_infer_tensor_from_stream_old(frame, meta, channels_expected) + return self.resize_tensor_chw(tensor, target_size=target_size) raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}") @@ -472,16 +485,23 @@ class RawProcessorCore: elif mode == "homography": H = cfg.get("homographies", {}).get(f"{cam_id}_to_cam2") + if H is None: warped = img warped_mask = mask else: + H = np.asarray(H, dtype=np.float32) + + if H.shape != (3, 3): + raise RuntimeError(f"Homografia inválida para {cam_id}: shape={H.shape}") + warped = cv2.warpPerspective( img, H, (ref_w, ref_h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0 ) + warped_mask = cv2.warpPerspective( mask, H, (ref_w, ref_h), flags=cv2.INTER_NEAREST, @@ -544,6 +564,29 @@ class RawProcessorCore: return resized + def resize_tensor_chw(self, tensor, target_size=None): + if target_size is None: + return tensor + + target_w, target_h = target_size + + if tensor.ndim != 3: + raise RuntimeError(f"Tensor esperado em CHW. Veio shape={tensor.shape}") + + _, h, w = tensor.shape + + if (w, h) == (target_w, target_h): + return tensor.astype(np.float32, copy=False) + + interp = cv2.INTER_AREA if target_w < w or target_h < h else cv2.INTER_LINEAR + + chans = [] + for ch in tensor: + ch_res = cv2.resize(ch, (target_w, target_h), interpolation=interp) + chans.append(ch_res.astype(np.float32)) + + return np.stack(chans, axis=0) + def extract_camera_meta(self, meta_json: dict, cam_id: str) -> dict: cam_frames = meta_json.get("camera_frames", {}) or meta_json.get("stream_meta", {}).get("camera_frames", {}) @@ -711,4 +754,29 @@ class RawProcessorCore: raise ValueError( f"Formato não suportado para salvar: channels={channels}, bit_depth={bit_depth}" ) - \ No newline at end of file + + + def load_fusion_config_json(self, path: str): + if not path or not os.path.isfile(path): + raise FileNotFoundError(f"Arquivo de calibração não encontrado: {path}") + + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + + fusion = data.get("fusion_config") + if not isinstance(fusion, dict): + print("[WARN] JSON sem fusion_config. Mantendo config padrão.") + return + + self.fusion_config = self._merge_fusion_config(self.fusion_config, fusion) + + def _merge_fusion_config(self, default_cfg: dict, loaded_cfg: dict) -> dict: + cfg = json.loads(json.dumps(default_cfg)) + + for key, value in loaded_cfg.items(): + if isinstance(value, dict) and isinstance(cfg.get(key), dict): + cfg[key].update(value) + else: + cfg[key] = value + + return cfg \ No newline at end of file diff --git a/Python/raspi/build_module_params.py b/Python/raspi/build_module_params.py new file mode 100644 index 000000000..88c5f1d55 --- /dev/null +++ b/Python/raspi/build_module_params.py @@ -0,0 +1,64 @@ +import json +import argparse +from datetime import datetime + + +def now_str(): + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def load_json(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--camera_json", default="calibration/sensor_calibration.json") + parser.add_argument("--fusion_json", default="calibration/manual_offsets.json") + parser.add_argument("--out", default="calibration/module_params.json") + args = parser.parse_args() + + cam_data = load_json(args.camera_json) + fusion_data = load_json(args.fusion_json) + + camera_settings = cam_data.get("camera_settings") + if not isinstance(camera_settings, dict): + raise RuntimeError("camera_json sem camera_settings válido") + + fusion_config = { + "alignment_mode": fusion_data.get("alignment_mode", "manual_affine"), + "baseline_mm": fusion_data.get("baseline_mm", 75.0), + "reference_camera": fusion_data.get("reference_camera", "cam2"), + "manual_offsets": fusion_data.get("manual_offsets", {}), + "homographies": fusion_data.get("homographies", {}), + "crop_valid_common": fusion_data.get("crop_valid_common", True), + "resize_after_crop": fusion_data.get("resize_after_crop", True), + "target_size": fusion_data.get("target_size", None), + } + + module_params = { + "schema": "multispec_module_params_v1", + "saved_at": now_str(), + + "frame_type": cam_data.get("frame_type", fusion_data.get("frame_type", "RAW_BRUTO")), + "capture_mode_requested": cam_data.get("capture_mode_requested", "AUTO"), + "capture_mode_effective": cam_data.get("capture_mode_effective", "AUTO"), + "raw_policy": cam_data.get("raw_policy", "allow_single"), + + "sensor_width": cam_data.get("sensor_width", fusion_data.get("sensor_width")), + "sensor_height": cam_data.get("sensor_height", fusion_data.get("sensor_height")), + "bayer_pattern": cam_data.get("bayer_pattern", fusion_data.get("bayer_pattern", "GBRG")), + + "camera_settings": camera_settings, + "fusion_config": fusion_config, + } + + with open(args.out, "w", encoding="utf-8") as f: + json.dump(module_params, f, ensure_ascii=False, indent=2) + + print(f"[OK] module_params gerado em: {args.out}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Python/raspi/calibration/manual_offsets.json b/Python/raspi/calibration/manual_offsets.json new file mode 100644 index 000000000..7fe996a1b --- /dev/null +++ b/Python/raspi/calibration/manual_offsets.json @@ -0,0 +1,50 @@ +{ + "schema": "manual_multispec_offsets_v1", + "saved_at": "2026-04-24 09:28:21", + "pi_host": "192.168.105.6", + "pc_host": "192.168.105.5", + "stream_port": 6001, + "frame_type": "RAW_BRUTO", + "capture_mode_requested": "AUTO", + "capture_mode_effective": "AUTO", + "raw_policy": "allow_single", + "sensor_width": 640, + "sensor_height": 480, + "bayer_pattern": "GBRG", + "reference_camera": "cam2", + "baseline_mm": 75.0, + "alignment_mode": "homography", + "manual_offsets": { + "cam0": { + "dx": 0, + "dy": 0, + "theta_deg": 0.0 + }, + "cam1": { + "dx": 0, + "dy": 0, + "theta_deg": 0.0 + } + }, + "homographies": { + "cam0_to_cam2": [ + [ + 0.6438707381367006, + -0.44541142339026096, + 171.26012619956475 + ], + [ + -0.04083362402731768, + 0.6855417219007448, + 17.531834653386724 + ], + [ + -0.00015145530359180688, + -0.0006687764363592821, + 1.0 + ] + ], + "cam1_to_cam2": null + }, + "notes": "" +} \ No newline at end of file diff --git a/Python/raspi/calibration/module_params.json b/Python/raspi/calibration/module_params.json new file mode 100644 index 000000000..6196e60fe --- /dev/null +++ b/Python/raspi/calibration/module_params.json @@ -0,0 +1,77 @@ +{ + "schema": "multispec_module_params_v1", + "saved_at": "2026-04-24 10:46:59", + "frame_type": "RAW_BRUTO", + "capture_mode_requested": "AUTO", + "capture_mode_effective": "AUTO", + "raw_policy": "allow_single", + "sensor_width": 640, + "sensor_height": 480, + "bayer_pattern": "GBRG", + "camera_settings": { + "cam0": { + "ae_enable": false, + "awb_enable": false, + "exposure_time_us": 15000, + "analogue_gain": 1.0, + "colour_gains": null + }, + "cam1": { + "ae_enable": false, + "awb_enable": false, + "exposure_time_us": 15000, + "analogue_gain": 1.0, + "colour_gains": null + }, + "cam2": { + "ae_enable": true, + "awb_enable": true, + "exposure_time_us": 15000, + "analogue_gain": 1.0, + "colour_gains": [ + 1.0, + 1.0 + ] + } + }, + "fusion_config": { + "alignment_mode": "homography", + "baseline_mm": 75.0, + "reference_camera": "cam2", + "manual_offsets": { + "cam0": { + "dx": 0, + "dy": 0, + "theta_deg": 0.0 + }, + "cam1": { + "dx": 0, + "dy": 0, + "theta_deg": 0.0 + } + }, + "homographies": { + "cam0_to_cam2": [ + [ + 0.6438707381367006, + -0.44541142339026096, + 171.26012619956475 + ], + [ + -0.04083362402731768, + 0.6855417219007448, + 17.531834653386724 + ], + [ + -0.00015145530359180688, + -0.0006687764363592821, + 1.0 + ] + ], + "cam1_to_cam2": null + }, + "crop_valid_common": true, + "resize_after_crop": true, + "target_size": null + } +} \ No newline at end of file diff --git a/Python/raspi/calibration/sensor_calibration.json b/Python/raspi/calibration/sensor_calibration.json index 6d0e76c99..68e2fbfba 100644 --- a/Python/raspi/calibration/sensor_calibration.json +++ b/Python/raspi/calibration/sensor_calibration.json @@ -1,6 +1,6 @@ { "schema": "multispec_camera_params_v1", - "saved_at": "2026-04-23 22:26:09", + "saved_at": "2026-04-24 13:44:56", "pi_host": "192.168.105.6", "pc_host": "192.168.105.5", "stream_port": 6001, @@ -15,20 +15,20 @@ "cam0": { "ae_enable": false, "awb_enable": false, - "exposure_time_us": 100, + "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": null }, "cam1": { "ae_enable": false, "awb_enable": false, - "exposure_time_us": 100, + "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": null }, "cam2": { - "ae_enable": false, - "awb_enable": false, + "ae_enable": true, + "awb_enable": true, "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": [ @@ -40,160 +40,52 @@ "rois": { "cam2": [ { - "name": "erva", + "name": "pessoa", "type": "polygon", "points": [ [ - 314, - 1028 + 331, + 3 ], [ - 322, - 952 + 351, + 78 ], [ - 262, - 963 + 353, + 122 ], [ - 262, - 885 + 390, + 156 ], [ - 343, - 833 + 419, + 177 ], [ - 314, - 814 + 495, + 178 ], [ - 367, - 752 + 567, + 148 ], [ - 384, - 714 + 584, + 105 ], [ - 412, - 714 + 588, + 70 ], [ - 427, - 695 + 600, + 52 ], [ - 465, - 695 - ], - [ - 512, - 695 - ], - [ - 482, - 663 - ], - [ - 459, - 620 - ], - [ - 533, - 657 - ], - [ - 557, - 628 - ], - [ - 565, - 668 - ], - [ - 619, - 644 - ], - [ - 655, - 652 - ], - [ - 629, - 714 - ], - [ - 629, - 736 - ], - [ - 663, - 766 - ], - [ - 706, - 825 - ], - [ - 700, - 841 - ], - [ - 614, - 836 - ], - [ - 657, - 879 - ], - [ - 666, - 915 - ], - [ - 672, - 979 - ], - [ - 659, - 982 - ], - [ - 663, - 1039 - ], - [ - 591, - 988 - ], - [ - 591, - 1025 - ], - [ - 557, - 1007 - ], - [ - 538, - 1023 - ], - [ - 491, - 1066 - ], - [ - 454, - 1036 - ], - [ - 427, - 982 - ], - [ - 369, - 1023 + 592, + 4 ] ], "color": [ @@ -203,92 +95,24 @@ ] }, { - "name": "cana", + "name": "parede", "type": "polygon", "points": [ [ - 843, - 950 + 35, + 187 ], [ - 685, - 473 + 48, + 79 ], [ - 787, - 647 + 133, + 98 ], [ - 747, - 519 - ], - [ - 785, - 549 - ], - [ - 815, - 584 - ], - [ - 787, - 387 - ], - [ - 836, - 503 - ], - [ - 847, - 460 - ], - [ - 809, - 363 - ], - [ - 892, - 465 - ], - [ - 900, - 327 - ], - [ - 930, - 465 - ], - [ - 994, - 357 - ], - [ - 956, - 490 - ], - [ - 1007, - 419 - ], - [ - 981, - 552 - ], - [ - 1056, - 455 - ], - [ - 1005, - 633 - ], - [ - 981, - 909 - ], - [ - 943, - 944 + 131, + 205 ] ], "color": [ @@ -296,147 +120,44 @@ 255, 0 ] - }, - { - "name": "solo", - "type": "polygon", - "points": [ - [ - 49, - 1307 - ], - [ - 55, - 1123 - ], - [ - 356, - 1112 - ], - [ - 358, - 1318 - ] - ], - "color": [ - 255, - 255, - 0 - ] } ], "cam0": [ { - "name": "erva", + "name": "pessoa", "type": "polygon", "points": [ [ - 309, - 1042 + 3, + 309 ], [ - 258, - 920 + 11, + 10 ], [ - 320, - 777 + 116, + 63 ], [ - 401, - 685 + 193, + 161 ], [ - 497, - 685 + 206, + 235 ], [ - 448, - 625 + 186, + 293 ], [ - 484, - 620 + 126, + 338 ], [ - 531, - 636 - ], - [ - 567, - 617 - ], - [ - 572, - 660 - ], - [ - 614, - 636 - ], - [ - 653, - 644 - ], - [ - 634, - 687 - ], - [ - 661, - 728 - ], - [ - 717, - 823 - ], - [ - 678, - 831 - ], - [ - 627, - 828 - ], - [ - 683, - 885 - ], - [ - 683, - 936 - ], - [ - 661, - 979 - ], - [ - 661, - 1028 - ], - [ - 604, - 1036 - ], - [ - 548, - 1012 - ], - [ - 499, - 1066 - ], - [ - 448, - 1047 - ], - [ - 429, - 1023 - ], - [ - 369, - 1023 + 54, + 336 ] ], "color": [ @@ -446,124 +167,24 @@ ] }, { - "name": "cana", + "name": "teto", "type": "polygon", "points": [ [ - 868, - 952 + 262, + 124 ], [ - 802, - 760 + 270, + 268 ], [ - 727, - 598 + 371, + 269 ], [ - 668, - 511 - ], - [ - 747, - 590 - ], - [ - 678, - 430 - ], - [ - 762, - 576 - ], - [ - 779, - 547 - ], - [ - 753, - 441 - ], - [ - 813, - 582 - ], - [ - 789, - 422 - ], - [ - 774, - 352 - ], - [ - 841, - 519 - ], - [ - 841, - 406 - ], - [ - 841, - 365 - ], - [ - 883, - 498 - ], - [ - 896, - 298 - ], - [ - 924, - 411 - ], - [ - 939, - 457 - ], - [ - 981, - 373 - ], - [ - 962, - 514 - ], - [ - 981, - 460 - ], - [ - 1011, - 409 - ], - [ - 979, - 538 - ], - [ - 1041, - 465 - ], - [ - 1026, - 563 - ], - [ - 992, - 766 - ], - [ - 994, - 928 - ], - [ - 941, - 952 + 371, + 125 ] ], "color": [ @@ -571,690 +192,13 @@ 255, 0 ] - }, - { - "name": "solo", - "type": "polygon", - "points": [ - [ - 45, - 1328 - ], - [ - 45, - 1115 - ], - [ - 365, - 1123 - ], - [ - 378, - 1326 - ] - ], - "color": [ - 255, - 255, - 0 - ] } ], - "cam1": [ - { - "name": "erva", - "type": "polygon", - "points": [ - [ - 314, - 1105 - ], - [ - 245, - 965 - ], - [ - 284, - 851 - ], - [ - 386, - 743 - ], - [ - 427, - 694 - ], - [ - 510, - 716 - ], - [ - 459, - 648 - ], - [ - 459, - 621 - ], - [ - 518, - 626 - ], - [ - 533, - 648 - ], - [ - 563, - 621 - ], - [ - 565, - 662 - ], - [ - 627, - 640 - ], - [ - 668, - 653 - ], - [ - 636, - 699 - ], - [ - 661, - 716 - ], - [ - 715, - 762 - ], - [ - 721, - 797 - ], - [ - 678, - 800 - ], - [ - 687, - 851 - ], - [ - 646, - 856 - ], - [ - 685, - 883 - ], - [ - 723, - 902 - ], - [ - 736, - 962 - ], - [ - 713, - 967 - ], - [ - 672, - 967 - ], - [ - 687, - 1059 - ], - [ - 634, - 1067 - ], - [ - 580, - 1086 - ], - [ - 535, - 1078 - ], - [ - 499, - 1119 - ], - [ - 439, - 1046 - ], - [ - 382, - 1067 - ] - ], - "color": [ - 0, - 255, - 255 - ] - }, - { - "name": "cana", - "type": "polygon", - "points": [ - [ - 911, - 948 - ], - [ - 855, - 913 - ], - [ - 809, - 783 - ], - [ - 783, - 697 - ], - [ - 738, - 597 - ], - [ - 689, - 499 - ], - [ - 768, - 610 - ], - [ - 781, - 605 - ], - [ - 766, - 521 - ], - [ - 749, - 450 - ], - [ - 742, - 426 - ], - [ - 813, - 551 - ], - [ - 809, - 461 - ], - [ - 759, - 367 - ], - [ - 832, - 464 - ], - [ - 826, - 415 - ], - [ - 864, - 459 - ], - [ - 843, - 375 - ], - [ - 815, - 326 - ], - [ - 887, - 434 - ], - [ - 900, - 350 - ], - [ - 913, - 448 - ], - [ - 945, - 386 - ], - [ - 937, - 467 - ], - [ - 986, - 391 - ], - [ - 964, - 502 - ], - [ - 996, - 432 - ], - [ - 975, - 515 - ], - [ - 1020, - 442 - ], - [ - 996, - 534 - ], - [ - 1052, - 478 - ], - [ - 1013, - 580 - ], - [ - 1075, - 505 - ], - [ - 1020, - 618 - ], - [ - 998, - 729 - ], - [ - 979, - 867 - ], - [ - 930, - 921 - ] - ], - "color": [ - 0, - 255, - 0 - ] - }, - { - "name": "solo", - "type": "polygon", - "points": [ - [ - 30, - 1308 - ], - [ - 34, - 1162 - ], - [ - 384, - 1159 - ], - [ - 365, - 1343 - ] - ], - "color": [ - 255, - 255, - 0 - ] - } - ] + "cam1": [] }, "snapshots": [ { - "timestamp": "2026-04-23 22:04:21", - "camera": "cam0", - "camera_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "scene_health": { - "mean": 0.46242380142211914, - "std": 0.2910088896751404, - "p05": 0.0332355834543705, - "p95": 0.9384164214134216, - "pct_saturated": 1.3829627403846154, - "pct_dark": 3.406164148351648, - "comment": "ok" - }, - "rois": [ - { - "name": "erva", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "points": [ - [ - 309, - 1042 - ], - [ - 258, - 920 - ], - [ - 320, - 777 - ], - [ - 401, - 685 - ], - [ - 497, - 685 - ], - [ - 448, - 625 - ], - [ - 484, - 620 - ], - [ - 531, - 636 - ], - [ - 567, - 617 - ], - [ - 572, - 660 - ], - [ - 614, - 636 - ], - [ - 653, - 644 - ], - [ - 634, - 687 - ], - [ - 661, - 728 - ], - [ - 717, - 823 - ], - [ - 678, - 831 - ], - [ - 627, - 828 - ], - [ - 683, - 885 - ], - [ - 683, - 936 - ], - [ - 661, - 979 - ], - [ - 661, - 1028 - ], - [ - 604, - 1036 - ], - [ - 548, - 1012 - ], - [ - 499, - 1066 - ], - [ - 448, - 1047 - ], - [ - 429, - 1023 - ], - [ - 369, - 1023 - ] - ] - } - ] - }, - { - "timestamp": "2026-04-23 22:04:23", - "camera": "cam1", - "camera_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "scene_health": { - "mean": 0.5154572129249573, - "std": 0.30768442153930664, - "p05": 0.0332355834543705, - "p95": 0.9540566802024841, - "pct_saturated": 1.8862322573260073, - "pct_dark": 3.57103508470696, - "comment": "ok" - }, - "rois": [ - { - "name": "erva", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "points": [ - [ - 314, - 1105 - ], - [ - 245, - 965 - ], - [ - 284, - 851 - ], - [ - 386, - 743 - ], - [ - 427, - 694 - ], - [ - 510, - 716 - ], - [ - 459, - 648 - ], - [ - 459, - 621 - ], - [ - 518, - 626 - ], - [ - 533, - 648 - ], - [ - 563, - 621 - ], - [ - 565, - 662 - ], - [ - 627, - 640 - ], - [ - 668, - 653 - ], - [ - 636, - 699 - ], - [ - 661, - 716 - ], - [ - 715, - 762 - ], - [ - 721, - 797 - ], - [ - 678, - 800 - ], - [ - 687, - 851 - ], - [ - 646, - 856 - ], - [ - 685, - 883 - ], - [ - 723, - 902 - ], - [ - 736, - 962 - ], - [ - 713, - 967 - ], - [ - 672, - 967 - ], - [ - 687, - 1059 - ], - [ - 634, - 1067 - ], - [ - 580, - 1086 - ], - [ - 535, - 1078 - ], - [ - 499, - 1119 - ], - [ - 439, - 1046 - ], - [ - 382, - 1067 - ] - ] - } - ] - }, - { - "timestamp": "2026-04-23 22:05:13", + "timestamp": "2026-04-24 13:44:41", "camera": "cam2", "camera_settings": { "ae_enable": true, @@ -1267,486 +211,115 @@ ] }, "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, + "mean": 0.4009036719799042, + "std": 0.3026207685470581, + "p05": 0.003921568859368563, + "p95": 0.8666666746139526, + "pct_saturated": 1.8621961805555556, + "pct_dark": 7.487955729166666, "comment": "ok" }, "rois": [ { - "name": "erva", + "name": "pessoa", "type": "polygon", "metrics": { "valid": true, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, + "mean": 0.2285451740026474, + "std": 0.1853915899991989, "min": 0.0, - "max": 0.9607843160629272, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, + "max": 0.9215686321258545, + "p05": 0.0313725508749485, + "p95": 0.7568627595901489, "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 + "pct_dark": 2.340194455937112, + "pixels": 116016 }, "points": [ [ - 314, - 1028 + 331, + 3 ], [ - 322, - 952 + 351, + 78 ], [ - 262, - 963 + 353, + 122 ], [ - 262, - 885 + 390, + 156 ], [ - 343, - 833 + 419, + 177 ], [ - 314, - 814 + 495, + 178 ], [ - 367, - 752 + 567, + 148 ], [ - 384, - 714 + 584, + 105 ], [ - 412, - 714 + 588, + 70 ], [ - 427, - 695 + 600, + 52 ], [ - 465, - 695 - ], - [ - 512, - 695 - ], - [ - 482, - 663 - ], - [ - 459, - 620 - ], - [ - 533, - 657 - ], - [ - 557, - 628 - ], - [ - 565, - 668 - ], - [ - 619, - 644 - ], - [ - 655, - 652 - ], - [ - 629, - 714 - ], - [ - 629, - 736 - ], - [ - 663, - 766 - ], - [ - 706, - 825 - ], - [ - 700, - 841 - ], - [ - 614, - 836 - ], - [ - 657, - 879 - ], - [ - 666, - 915 - ], - [ - 672, - 979 - ], - [ - 659, - 982 - ], - [ - 663, - 1039 - ], - [ - 591, - 988 - ], - [ - 591, - 1025 - ], - [ - 557, - 1007 - ], - [ - 538, - 1023 - ], - [ - 491, - 1066 - ], - [ - 454, - 1036 - ], - [ - 427, - 982 - ], - [ - 369, - 1023 - ] - ] - } - ] - }, - { - "timestamp": "2026-04-23 22:05:59", - "camera": "cam2", - "camera_settings": { - "ae_enable": true, - "awb_enable": true, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "rois": [ - { - "name": "erva", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "min": 0.0, - "max": 0.9607843160629272, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "points": [ - [ - 314, - 1028 - ], - [ - 322, - 952 - ], - [ - 262, - 963 - ], - [ - 262, - 885 - ], - [ - 343, - 833 - ], - [ - 314, - 814 - ], - [ - 367, - 752 - ], - [ - 384, - 714 - ], - [ - 412, - 714 - ], - [ - 427, - 695 - ], - [ - 465, - 695 - ], - [ - 512, - 695 - ], - [ - 482, - 663 - ], - [ - 459, - 620 - ], - [ - 533, - 657 - ], - [ - 557, - 628 - ], - [ - 565, - 668 - ], - [ - 619, - 644 - ], - [ - 655, - 652 - ], - [ - 629, - 714 - ], - [ - 629, - 736 - ], - [ - 663, - 766 - ], - [ - 706, - 825 - ], - [ - 700, - 841 - ], - [ - 614, - 836 - ], - [ - 657, - 879 - ], - [ - 666, - 915 - ], - [ - 672, - 979 - ], - [ - 659, - 982 - ], - [ - 663, - 1039 - ], - [ - 591, - 988 - ], - [ - 591, - 1025 - ], - [ - 557, - 1007 - ], - [ - 538, - 1023 - ], - [ - 491, - 1066 - ], - [ - 454, - 1036 - ], - [ - 427, - 982 - ], - [ - 369, - 1023 + 592, + 4 ] ] }, { - "name": "cana", + "name": "parede", "type": "polygon", "metrics": { "valid": true, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "min": 0.0, - "max": 0.9803921580314636, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 + "mean": 0.7700283527374268, + "std": 0.04279294237494469, + "min": 0.6470588445663452, + "max": 0.8784313797950745, + "p05": 0.6980392336845398, + "p95": 0.8470588326454163, + "pct_saturated": 0.0, + "pct_dark": 0.0, + "pixels": 30192 }, "points": [ [ - 843, - 950 + 35, + 187 ], [ - 685, - 473 + 48, + 79 ], [ - 787, - 647 + 133, + 98 ], [ - 747, - 519 - ], - [ - 785, - 549 - ], - [ - 815, - 584 - ], - [ - 787, - 387 - ], - [ - 836, - 503 - ], - [ - 847, - 460 - ], - [ - 809, - 363 - ], - [ - 892, - 465 - ], - [ - 900, - 327 - ], - [ - 930, - 465 - ], - [ - 994, - 357 - ], - [ - 956, - 490 - ], - [ - 1007, - 419 - ], - [ - 981, - 552 - ], - [ - 1056, - 455 - ], - [ - 1005, - 633 - ], - [ - 981, - 909 - ], - [ - 943, - 944 + 131, + 205 ] ] } ] }, { - "timestamp": "2026-04-23 22:06:49", + "timestamp": "2026-04-24 13:44:45", "camera": "cam0", "camera_settings": { "ae_enable": false, @@ -1756,1670 +329,96 @@ "colour_gains": null }, "scene_health": { - "mean": 0.46242380142211914, - "std": 0.2910088896751404, - "p05": 0.0332355834543705, - "p95": 0.9384164214134216, - "pct_saturated": 1.3829627403846154, - "pct_dark": 3.406164148351648, - "comment": "ok" + "mean": 0.030308637768030167, + "std": 0.009111250750720501, + "p05": 0.015640273690223694, + "p95": 0.045943304896354675, + "pct_saturated": 0.0, + "pct_dark": 17.569986979166664, + "comment": "baixo contraste" }, "rois": [ { - "name": "erva", + "name": "pessoa", "type": "polygon", "metrics": { "valid": true, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 + "mean": 0.017233407124876976, + "std": 0.0033262341748923063, + "min": 0.01173020526766777, + "max": 0.04496578872203827, + "p05": 0.014662756584584713, + "p95": 0.022482894361019135, + "pct_saturated": 0.0, + "pct_dark": 89.6567197666592, + "pixels": 49027 }, "points": [ [ - 309, - 1042 + 3, + 309 ], [ - 258, - 920 + 11, + 10 ], [ - 320, - 777 + 116, + 63 ], [ - 401, - 685 + 193, + 161 ], [ - 497, - 685 + 206, + 235 ], [ - 448, - 625 + 186, + 293 ], [ - 484, - 620 + 126, + 338 ], [ - 531, - 636 - ], - [ - 567, - 617 - ], - [ - 572, - 660 - ], - [ - 614, - 636 - ], - [ - 653, - 644 - ], - [ - 634, - 687 - ], - [ - 661, - 728 - ], - [ - 717, - 823 - ], - [ - 678, - 831 - ], - [ - 627, - 828 - ], - [ - 683, - 885 - ], - [ - 683, - 936 - ], - [ - 661, - 979 - ], - [ - 661, - 1028 - ], - [ - 604, - 1036 - ], - [ - 548, - 1012 - ], - [ - 499, - 1066 - ], - [ - 448, - 1047 - ], - [ - 429, - 1023 - ], - [ - 369, - 1023 + 54, + 336 ] ] }, { - "name": "cana", + "name": "teto", "type": "polygon", "metrics": { "valid": true, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 + "mean": 0.03869995102286339, + "std": 0.005530648864805698, + "min": 0.026392962783575058, + "max": 0.050830889493227005, + "p05": 0.03128054738044739, + "p95": 0.04692082107067108, + "pct_saturated": 0.0, + "pct_dark": 0.0, + "pixels": 15370 }, "points": [ [ - 868, - 952 + 262, + 124 ], [ - 802, - 760 + 270, + 268 ], [ - 727, - 598 + 371, + 269 ], [ - 668, - 511 - ], - [ - 747, - 590 - ], - [ - 678, - 430 - ], - [ - 762, - 576 - ], - [ - 779, - 547 - ], - [ - 753, - 441 - ], - [ - 813, - 582 - ], - [ - 789, - 422 - ], - [ - 774, - 352 - ], - [ - 841, - 519 - ], - [ - 841, - 406 - ], - [ - 841, - 365 - ], - [ - 883, - 498 - ], - [ - 896, - 298 - ], - [ - 924, - 411 - ], - [ - 939, - 457 - ], - [ - 981, - 373 - ], - [ - 962, - 514 - ], - [ - 981, - 460 - ], - [ - 1011, - 409 - ], - [ - 979, - 538 - ], - [ - 1041, - 465 - ], - [ - 1026, - 563 - ], - [ - 992, - 766 - ], - [ - 994, - 928 - ], - [ - 941, - 952 - ] - ] - } - ] - }, - { - "timestamp": "2026-04-23 22:07:36", - "camera": "cam1", - "camera_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "scene_health": { - "mean": 0.5154572129249573, - "std": 0.30768442153930664, - "p05": 0.0332355834543705, - "p95": 0.9540566802024841, - "pct_saturated": 1.8862322573260073, - "pct_dark": 3.57103508470696, - "comment": "ok" - }, - "rois": [ - { - "name": "erva", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "points": [ - [ - 314, - 1105 - ], - [ - 245, - 965 - ], - [ - 284, - 851 - ], - [ - 386, - 743 - ], - [ - 427, - 694 - ], - [ - 510, - 716 - ], - [ - 459, - 648 - ], - [ - 459, - 621 - ], - [ - 518, - 626 - ], - [ - 533, - 648 - ], - [ - 563, - 621 - ], - [ - 565, - 662 - ], - [ - 627, - 640 - ], - [ - 668, - 653 - ], - [ - 636, - 699 - ], - [ - 661, - 716 - ], - [ - 715, - 762 - ], - [ - 721, - 797 - ], - [ - 678, - 800 - ], - [ - 687, - 851 - ], - [ - 646, - 856 - ], - [ - 685, - 883 - ], - [ - 723, - 902 - ], - [ - 736, - 962 - ], - [ - 713, - 967 - ], - [ - 672, - 967 - ], - [ - 687, - 1059 - ], - [ - 634, - 1067 - ], - [ - 580, - 1086 - ], - [ - 535, - 1078 - ], - [ - 499, - 1119 - ], - [ - 439, - 1046 - ], - [ - 382, - 1067 - ] - ] - }, - { - "name": "cana", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "min": 0.0009775171056389809, - "max": 0.9872922897338867, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "points": [ - [ - 911, - 948 - ], - [ - 855, - 913 - ], - [ - 809, - 783 - ], - [ - 783, - 697 - ], - [ - 738, - 597 - ], - [ - 689, - 499 - ], - [ - 768, - 610 - ], - [ - 781, - 605 - ], - [ - 766, - 521 - ], - [ - 749, - 450 - ], - [ - 742, - 426 - ], - [ - 813, - 551 - ], - [ - 809, - 461 - ], - [ - 759, - 367 - ], - [ - 832, - 464 - ], - [ - 826, - 415 - ], - [ - 864, - 459 - ], - [ - 843, - 375 - ], - [ - 815, - 326 - ], - [ - 887, - 434 - ], - [ - 900, - 350 - ], - [ - 913, - 448 - ], - [ - 945, - 386 - ], - [ - 937, - 467 - ], - [ - 986, - 391 - ], - [ - 964, - 502 - ], - [ - 996, - 432 - ], - [ - 975, - 515 - ], - [ - 1020, - 442 - ], - [ - 996, - 534 - ], - [ - 1052, - 478 - ], - [ - 1013, - 580 - ], - [ - 1075, - 505 - ], - [ - 1020, - 618 - ], - [ - 998, - 729 - ], - [ - 979, - 867 - ], - [ - 930, - 921 - ] - ] - } - ] - }, - { - "timestamp": "2026-04-23 22:08:13", - "camera": "cam0", - "camera_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "scene_health": { - "mean": 0.46242380142211914, - "std": 0.2910088896751404, - "p05": 0.0332355834543705, - "p95": 0.9384164214134216, - "pct_saturated": 1.3829627403846154, - "pct_dark": 3.406164148351648, - "comment": "ok" - }, - "rois": [ - { - "name": "erva", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "points": [ - [ - 309, - 1042 - ], - [ - 258, - 920 - ], - [ - 320, - 777 - ], - [ - 401, - 685 - ], - [ - 497, - 685 - ], - [ - 448, - 625 - ], - [ - 484, - 620 - ], - [ - 531, - 636 - ], - [ - 567, - 617 - ], - [ - 572, - 660 - ], - [ - 614, - 636 - ], - [ - 653, - 644 - ], - [ - 634, - 687 - ], - [ - 661, - 728 - ], - [ - 717, - 823 - ], - [ - 678, - 831 - ], - [ - 627, - 828 - ], - [ - 683, - 885 - ], - [ - 683, - 936 - ], - [ - 661, - 979 - ], - [ - 661, - 1028 - ], - [ - 604, - 1036 - ], - [ - 548, - 1012 - ], - [ - 499, - 1066 - ], - [ - 448, - 1047 - ], - [ - 429, - 1023 - ], - [ - 369, - 1023 - ] - ] - }, - { - "name": "cana", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "points": [ - [ - 868, - 952 - ], - [ - 802, - 760 - ], - [ - 727, - 598 - ], - [ - 668, - 511 - ], - [ - 747, - 590 - ], - [ - 678, - 430 - ], - [ - 762, - 576 - ], - [ - 779, - 547 - ], - [ - 753, - 441 - ], - [ - 813, - 582 - ], - [ - 789, - 422 - ], - [ - 774, - 352 - ], - [ - 841, - 519 - ], - [ - 841, - 406 - ], - [ - 841, - 365 - ], - [ - 883, - 498 - ], - [ - 896, - 298 - ], - [ - 924, - 411 - ], - [ - 939, - 457 - ], - [ - 981, - 373 - ], - [ - 962, - 514 - ], - [ - 981, - 460 - ], - [ - 1011, - 409 - ], - [ - 979, - 538 - ], - [ - 1041, - 465 - ], - [ - 1026, - 563 - ], - [ - 992, - 766 - ], - [ - 994, - 928 - ], - [ - 941, - 952 - ] - ] - }, - { - "name": "solo", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - }, - "points": [ - [ - 45, - 1328 - ], - [ - 45, - 1115 - ], - [ - 365, - 1123 - ], - [ - 378, - 1326 - ] - ] - } - ] - }, - { - "timestamp": "2026-04-23 22:08:31", - "camera": "cam1", - "camera_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "scene_health": { - "mean": 0.5154572129249573, - "std": 0.30768442153930664, - "p05": 0.0332355834543705, - "p95": 0.9540566802024841, - "pct_saturated": 1.8862322573260073, - "pct_dark": 3.57103508470696, - "comment": "ok" - }, - "rois": [ - { - "name": "erva", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "points": [ - [ - 314, - 1105 - ], - [ - 245, - 965 - ], - [ - 284, - 851 - ], - [ - 386, - 743 - ], - [ - 427, - 694 - ], - [ - 510, - 716 - ], - [ - 459, - 648 - ], - [ - 459, - 621 - ], - [ - 518, - 626 - ], - [ - 533, - 648 - ], - [ - 563, - 621 - ], - [ - 565, - 662 - ], - [ - 627, - 640 - ], - [ - 668, - 653 - ], - [ - 636, - 699 - ], - [ - 661, - 716 - ], - [ - 715, - 762 - ], - [ - 721, - 797 - ], - [ - 678, - 800 - ], - [ - 687, - 851 - ], - [ - 646, - 856 - ], - [ - 685, - 883 - ], - [ - 723, - 902 - ], - [ - 736, - 962 - ], - [ - 713, - 967 - ], - [ - 672, - 967 - ], - [ - 687, - 1059 - ], - [ - 634, - 1067 - ], - [ - 580, - 1086 - ], - [ - 535, - 1078 - ], - [ - 499, - 1119 - ], - [ - 439, - 1046 - ], - [ - 382, - 1067 - ] - ] - }, - { - "name": "cana", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "min": 0.0009775171056389809, - "max": 0.9872922897338867, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "points": [ - [ - 911, - 948 - ], - [ - 855, - 913 - ], - [ - 809, - 783 - ], - [ - 783, - 697 - ], - [ - 738, - 597 - ], - [ - 689, - 499 - ], - [ - 768, - 610 - ], - [ - 781, - 605 - ], - [ - 766, - 521 - ], - [ - 749, - 450 - ], - [ - 742, - 426 - ], - [ - 813, - 551 - ], - [ - 809, - 461 - ], - [ - 759, - 367 - ], - [ - 832, - 464 - ], - [ - 826, - 415 - ], - [ - 864, - 459 - ], - [ - 843, - 375 - ], - [ - 815, - 326 - ], - [ - 887, - 434 - ], - [ - 900, - 350 - ], - [ - 913, - 448 - ], - [ - 945, - 386 - ], - [ - 937, - 467 - ], - [ - 986, - 391 - ], - [ - 964, - 502 - ], - [ - 996, - 432 - ], - [ - 975, - 515 - ], - [ - 1020, - 442 - ], - [ - 996, - 534 - ], - [ - 1052, - 478 - ], - [ - 1013, - 580 - ], - [ - 1075, - 505 - ], - [ - 1020, - 618 - ], - [ - 998, - 729 - ], - [ - 979, - 867 - ], - [ - 930, - 921 - ] - ] - }, - { - "name": "solo", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "min": 0.0009775171056389809, - "max": 0.9872922897338867, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - }, - "points": [ - [ - 30, - 1308 - ], - [ - 34, - 1162 - ], - [ - 384, - 1159 - ], - [ - 365, - 1343 - ] - ] - } - ] - }, - { - "timestamp": "2026-04-23 22:09:12", - "camera": "cam1", - "camera_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "scene_health": { - "mean": 0.5154572129249573, - "std": 0.30768442153930664, - "p05": 0.0332355834543705, - "p95": 0.9540566802024841, - "pct_saturated": 1.8862322573260073, - "pct_dark": 3.57103508470696, - "comment": "ok" - }, - "rois": [ - { - "name": "erva", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "min": 0.0, - "max": 0.9872922897338867, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "points": [ - [ - 314, - 1105 - ], - [ - 245, - 965 - ], - [ - 284, - 851 - ], - [ - 386, - 743 - ], - [ - 427, - 694 - ], - [ - 510, - 716 - ], - [ - 459, - 648 - ], - [ - 459, - 621 - ], - [ - 518, - 626 - ], - [ - 533, - 648 - ], - [ - 563, - 621 - ], - [ - 565, - 662 - ], - [ - 627, - 640 - ], - [ - 668, - 653 - ], - [ - 636, - 699 - ], - [ - 661, - 716 - ], - [ - 715, - 762 - ], - [ - 721, - 797 - ], - [ - 678, - 800 - ], - [ - 687, - 851 - ], - [ - 646, - 856 - ], - [ - 685, - 883 - ], - [ - 723, - 902 - ], - [ - 736, - 962 - ], - [ - 713, - 967 - ], - [ - 672, - 967 - ], - [ - 687, - 1059 - ], - [ - 634, - 1067 - ], - [ - 580, - 1086 - ], - [ - 535, - 1078 - ], - [ - 499, - 1119 - ], - [ - 439, - 1046 - ], - [ - 382, - 1067 - ] - ] - }, - { - "name": "cana", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "min": 0.0009775171056389809, - "max": 0.9872922897338867, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "points": [ - [ - 911, - 948 - ], - [ - 855, - 913 - ], - [ - 809, - 783 - ], - [ - 783, - 697 - ], - [ - 738, - 597 - ], - [ - 689, - 499 - ], - [ - 768, - 610 - ], - [ - 781, - 605 - ], - [ - 766, - 521 - ], - [ - 749, - 450 - ], - [ - 742, - 426 - ], - [ - 813, - 551 - ], - [ - 809, - 461 - ], - [ - 759, - 367 - ], - [ - 832, - 464 - ], - [ - 826, - 415 - ], - [ - 864, - 459 - ], - [ - 843, - 375 - ], - [ - 815, - 326 - ], - [ - 887, - 434 - ], - [ - 900, - 350 - ], - [ - 913, - 448 - ], - [ - 945, - 386 - ], - [ - 937, - 467 - ], - [ - 986, - 391 - ], - [ - 964, - 502 - ], - [ - 996, - 432 - ], - [ - 975, - 515 - ], - [ - 1020, - 442 - ], - [ - 996, - 534 - ], - [ - 1052, - 478 - ], - [ - 1013, - 580 - ], - [ - 1075, - 505 - ], - [ - 1020, - 618 - ], - [ - 998, - 729 - ], - [ - 979, - 867 - ], - [ - 930, - 921 - ] - ] - }, - { - "name": "solo", - "type": "polygon", - "metrics": { - "valid": true, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "min": 0.0009775171056389809, - "max": 0.9872922897338867, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - }, - "points": [ - [ - 30, - 1308 - ], - [ - 34, - 1162 - ], - [ - 384, - 1159 - ], - [ - 365, - 1343 + 371, + 125 ] ] } @@ -3427,4783 +426,5 @@ } ], "notes": "", - "calibration_guidance_log": [ - { - "timestamp": "2026-04-23 22:08:42", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:14:17", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:14:17", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:49", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:49", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:52", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:52", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:53", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:53", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:54", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:54", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:55", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:55", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:56", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:56", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:56", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:56", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:57", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:57", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:58", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:58", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:58", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:58", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:59", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:15:59", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:00", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:00", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:01", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:01", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:02", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:02", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:03", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:03", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:05", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:16:05", - "camera": "cam1", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.21%). Reduzir exposição.", - "channel": "cam1", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4611518681049347, - "std": 0.25701242685317993, - "p05": 0.06451612710952759, - "p95": 0.9227761626243591, - "pct_saturated": 1.0718586659773144, - "pct_dark": 2.2389236016973197, - "pixels": 158603 - }, - "cana": { - "count": 1, - "mean": 0.4849100708961487, - "std": 0.2785700559616089, - "p05": 0.0498533733189106, - "p95": 0.939393937587738, - "pct_saturated": 1.341215417356349, - "pct_dark": 2.7486403405060296, - "pixels": 105725 - }, - "solo": { - "count": 1, - "mean": 0.5673046112060547, - "std": 0.251604288816452, - "p05": 0.09384164214134216, - "p95": 0.9227761626243591, - "pct_saturated": 0.8950423047339346, - "pct_dark": 1.6135235298230892, - "pixels": 57204 - } - }, - "veg_mean": 0.4730309695005417, - "solo_mean": 0.5673046112060547, - "separation": -0.094273641705513, - "veg_p95": 0.9310850501060486, - "veg_sat": 1.2065370416668317, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:46", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:46", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:47", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:47", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 14000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:48", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:48", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 13000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:49", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:49", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 12000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:49", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:49", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 11000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:50", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:50", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 10000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:50", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:50", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 9000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:51", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:51", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 8000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:51", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:51", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 7000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:52", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:52", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 6000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:52", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:52", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 5000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:53", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:53", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 4000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:53", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:53", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 3000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:54", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:54", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 2000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:54", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:54", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 1000, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:55", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:55", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:57", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:25:57", - "camera": "cam0", - "result": { - "status": "adjust", - "action": "decrease_exposure", - "reason": "Vegetação saturando (1.45%). Reduzir exposição.", - "channel": "cam0", - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.4457639753818512, - "std": 0.27479854226112366, - "p05": 0.04887585714459419, - "p95": 0.9247311949729919, - "pct_saturated": 1.2417743381211603, - "pct_dark": 2.9754487221529917, - "pixels": 137223 - }, - "cana": { - "count": 1, - "mean": 0.4643779695034027, - "std": 0.29758161306381226, - "p05": 0.034213099628686905, - "p95": 0.9403714537620544, - "pct_saturated": 1.6609685605526576, - "pct_dark": 3.2972649090652757, - "pixels": 113488 - }, - "solo": { - "count": 1, - "mean": 0.4692467749118805, - "std": 0.2457830160856247, - "p05": 0.04887585714459419, - "p95": 0.8602150678634644, - "pct_saturated": 0.45446574701893855, - "pct_dark": 2.324935702595277, - "pixels": 68432 - } - }, - "veg_mean": 0.45507097244262695, - "solo_mean": 0.4692467749118805, - "separation": -0.01417580246925354, - "veg_p95": 0.9325513243675232, - "veg_sat": 1.451371449336909, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 100, - "analogue_gain": 1.0, - "colour_gains": null - } - } - }, - { - "timestamp": "2026-04-23 22:26:04", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": true, - "awb_enable": true, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - }, - { - "timestamp": "2026-04-23 22:26:04", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": true, - "awb_enable": true, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - }, - { - "timestamp": "2026-04-23 22:26:05", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - }, - { - "timestamp": "2026-04-23 22:26:05", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - }, - { - "timestamp": "2026-04-23 22:26:06", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - }, - { - "timestamp": "2026-04-23 22:26:06", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - }, - { - "timestamp": "2026-04-23 22:26:08", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - }, - { - "timestamp": "2026-04-23 22:26:08", - "camera": "cam2", - "result": { - "status": "ok", - "action": "keep", - "reason": "RGB parece aceitável.", - "channel": "cam2", - "scene_health": { - "mean": 0.40037205815315247, - "std": 0.2858090400695801, - "p05": 0.0117647061124444, - "p95": 0.9058823585510254, - "pct_saturated": 0.009240976037851038, - "pct_dark": 6.297564197954824, - "comment": "ok" - }, - "class_metrics": { - "erva": { - "count": 1, - "mean": 0.2773081362247467, - "std": 0.19852407276630402, - "p05": 0.003921568859368563, - "p95": 0.6039215922355652, - "pct_saturated": 0.0, - "pct_dark": 9.312112611594372, - "pixels": 358329 - }, - "cana": { - "count": 1, - "mean": 0.3092897832393646, - "std": 0.25642046332359314, - "p05": 0.003921568859368563, - "p95": 0.8196078538894653, - "pct_saturated": 0.0011790541038451902, - "pct_dark": 13.049770821358564, - "pixels": 339255 - }, - "solo": { - "count": 1, - "mean": 0.2286829948425293, - "std": 0.1858908087015152, - "p05": 0.019607843831181526, - "p95": 0.5843137502670288, - "pct_saturated": 0.008336529002784402, - "pct_dark": 5.343715090784801, - "pixels": 179931 - } - }, - "before_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - }, - "after_settings": { - "ae_enable": false, - "awb_enable": false, - "exposure_time_us": 15000, - "analogue_gain": 1.0, - "colour_gains": [ - 1.0, - 1.0 - ] - } - } - } - ] + "calibration_guidance_log": [] } \ No newline at end of file diff --git a/Python/raspi/cam_3/capture_dataset.py b/Python/raspi/cam_3/capture_dataset.py deleted file mode 100644 index e092bdd50..000000000 --- a/Python/raspi/cam_3/capture_dataset.py +++ /dev/null @@ -1,824 +0,0 @@ -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("--modelo", default="imx296_pi", help="Nome do módulo/câmera para montar a pasta.") - 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.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 - 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/raspi/cam_3/multispectral_client.py b/Python/raspi/cam_3/multispectral_client.py new file mode 100644 index 000000000..e7cd1c806 --- /dev/null +++ b/Python/raspi/cam_3/multispectral_client.py @@ -0,0 +1,218 @@ +import time +import json + +from cam_3.multispectral_service import MultiSpectralService +from cam_3.stream_receiver import StreamReceiver +from cam_3.pi.raw_processor_core import RawProcessorCore +from cam_3.pi.raw_processor_preview import RawProcessorPreview + + +class MultiSpectralClient: + def __init__( + self, + pi_host="192.168.105.6", + pc_host="192.168.105.5", + server_port=5000, + stream_port=6001, + timeout=10, + width=640, + height=480, + bayer="GBRG", + fps=15, + frame_type="RAW_BRUTO", + output_dtype="uint8", + capture_mode="AUTO", + raw_policy="allow_single", + module_calibration_json=None, + ): + self.pi_host = pi_host + self.pc_host = pc_host + self.server_port = server_port + self.stream_port = stream_port + + self.width = width + self.height = height + self.bayer = bayer + self.fps = fps + self.frame_type = frame_type + self.output_dtype = output_dtype + self.capture_mode = capture_mode + self.raw_policy = raw_policy + self.module_calibration_json = module_calibration_json + + self.svc = MultiSpectralService( + host=pi_host, + port=server_port, + timeout=timeout, + ) + + self.receiver = StreamReceiver( + host="0.0.0.0", + port=stream_port, + ) + + self.core = RawProcessorCore( + sensor_width=width, + sensor_height=height, + bayer_pattern=bayer, + calibration_json_path=module_calibration_json, + ) + + self.preview = RawProcessorPreview( + sensor_width=width, + sensor_height=height, + bayer_pattern=bayer, + ) + + self.last_frame_id = None + self.status = None + self.begin_resp = None + self.applied_params = None + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc, tb): + self.stop() + + def start(self, print_debug=True): + if print_debug: + print(f"[INFO] Verificando módulo em {self.pi_host}:{self.server_port}...") + + if not self.svc.check_connection(2): + raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.") + + self.receiver.start() + time.sleep(0.5) + + self.svc.connect() + + if print_debug: + print("[OK] Módulo conectado.") + + self._configure_module(print_debug=print_debug) + self._begin_module(print_debug=print_debug) + self._apply_module_params(print_debug=print_debug) + self._start_stream(print_debug=print_debug) + + return self + + def _configure_module(self, print_debug=True): + r0 = self.svc.set_resolution(self.width, self.height) + r1 = self.svc.set_bayer(self.bayer) + r2 = self.svc.set_fps(self.fps) + r3 = self.svc.set_capture_mode(self.capture_mode) + r4 = self.svc.set_frame_type(self.frame_type) + r5 = self.svc.set_output_dtype(self.output_dtype) + if print_debug: + print("SET RES:", r0) + print("SET BAYER:", r1) + print("SET FPS:", r2) + print("SET CAPTURE MODE:", r3) + print("SET FRAME TYPE:", r4) + print("SET OUTPUT DTYPE:", r5) + + def _begin_module(self, print_debug=True): + self.begin_resp = self.svc.begin( + frame_type=self.frame_type, + output_dtype=self.output_dtype, + capture_mode=self.capture_mode, + ) + + self.status = self.svc.get_status() + + self.svc.validate_module_ready( + self.status, + self.frame_type, + self.raw_policy, + self.capture_mode, + ) + + if print_debug: + print("BEGIN:", self.begin_resp) + print("STATUS:", json.dumps({ + "status": self.status.get("status"), + "detected_mode": self.status.get("detected_mode"), + "camera_count_active": self.status.get("camera_count_active"), + "active_camera_ids": self.status.get("active_camera_ids"), + }, ensure_ascii=False)) + + def _apply_module_params(self, print_debug=True): + if not self.module_calibration_json: + return None + + self.applied_params = self.svc.apply_camera_params_json( + self.module_calibration_json + ) + + if print_debug: + print("[OK] Parâmetros do módulo aplicados:") + print(json.dumps(self.applied_params.get("applied"), ensure_ascii=False, indent=2)) + + return self.applied_params + + def _start_stream(self, print_debug=True): + resp = self.svc.start_stream( + self.pc_host, + self.stream_port, + fps=self.fps, + ) + + if print_debug: + print("START STREAM:", resp) + + return resp + + def get_latest(self): + return self.receiver.last_frame, self.receiver.last_meta + + def get_next_frame(self, timeout=2.0): + t0 = time.perf_counter() + + while time.perf_counter() - t0 < timeout: + frame = self.receiver.last_frame + meta = self.receiver.last_meta + + if frame is None or meta is None: + time.sleep(0.001) + continue + + frame_id = meta.get("frame_id") + + if frame_id != self.last_frame_id: + self.last_frame_id = frame_id + return frame, meta + + time.sleep(0.001) + + raise TimeoutError("Timeout aguardando novo frame do stream.") + + def build_infer_tensor(self, frame, meta, channels_expected, target_size=None): + return self.core.build_infer_tensor_from_stream( + frame, + meta, + channels_expected=channels_expected, + target_size=target_size, + ) + + def stop(self): + try: + self.svc.stop_stream() + except Exception: + pass + + try: + self.svc.stop() + except Exception: + pass + + try: + self.svc.disconnect() + except Exception: + pass + + try: + self.receiver.stop() + except Exception: + pass \ No newline at end of file diff --git a/Python/raspi/cam_3/multispectral_service.py b/Python/raspi/cam_3/multispectral_service.py index 7ff661a12..e015c1a52 100644 --- a/Python/raspi/cam_3/multispectral_service.py +++ b/Python/raspi/cam_3/multispectral_service.py @@ -95,6 +95,68 @@ class MultiSpectralService: return json.loads(line.strip()) + def validate_module_ready(self, 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}") + + # ========================================================= # Helpers numpy # ========================================================= @@ -259,13 +321,20 @@ class MultiSpectralService: def get_config(self): return self._send_command({"cmd": "get_config"}) - def begin(self, frame_type: str = "RAW_BRUTO", output_dtype: str = "uint8", capture_mode: str = "AUTO"): - return self._send_command({ - "cmd": "begin", - "frame_type": frame_type, - "output_dtype": output_dtype, - "capture_mode": capture_mode, - }) + def begin(self, frame_type="RAW_BRUTO", output_dtype="uint8", capture_mode="AUTO", timeout=15): + old_timeout = self.sock.gettimeout() if self.sock else None + if self.sock and timeout is not None: + self.sock.settimeout(timeout) + try: + return self._send_command({ + "cmd": "begin", + "frame_type": frame_type, + "output_dtype": output_dtype, + "capture_mode": capture_mode, + }) + finally: + if self.sock and old_timeout is not None: + self.sock.settimeout(old_timeout) def stop(self): return self._send_command({"cmd": "stop"}) @@ -306,11 +375,36 @@ class MultiSpectralService: "width": width, "height": height }) + + def set_resolution(self, width: int, height: int): + res = [] + for index in range(0, 3): + res.append( + self._send_command({ + "cmd": "set_camera_resolution", + "index": index, + "width": width, + "height": height + }) + ) + return res + + def set_bayer(self, bayer_pattern: str): + res = [] + for index in range(0, 2): + res.append( + self._send_command({ + "cmd": "set_camera_bayer", + "index": index, + "pattern": bayer_pattern + }) + ) + return res # ========================================================= # Captura # ========================================================= - + def capture_frame(self): t0 = time.perf_counter() diff --git a/Python/raspi/cam_3/pi/camera_manager.py b/Python/raspi/cam_3/pi/camera_manager.py index ed0ee8893..bf6e8feba 100644 --- a/Python/raspi/cam_3/pi/camera_manager.py +++ b/Python/raspi/cam_3/pi/camera_manager.py @@ -6,6 +6,7 @@ import numpy as np import cv2 from pathlib import Path from typing import Optional +import subprocess class CameraManager: @@ -87,6 +88,9 @@ class CameraManager: if not required_ids: print("[WARN] Nenhuma câmera requerida para o frame_type/capture_mode atual") + for cam in self.state.cameras: + cam.attempted = False + cams_to_open = [cam for cam in self.state.cameras if cam.id in required_ids] cams_to_open.sort( @@ -94,6 +98,8 @@ class CameraManager: ) for cam in cams_to_open: + cam.attempted = True + if cam.id not in required_ids: self.state.set_camera_connected(cam.index, False) continue @@ -128,7 +134,7 @@ class CameraManager: deadline = time.perf_counter() + 1.0 while time.perf_counter() < deadline: - if self.state.camera_count_active > 0: + if self.state.camera_count_active > 2: break time.sleep(0.02) @@ -173,6 +179,9 @@ class CameraManager: api_preference = cv2.CAP_V4L2 if backend_name == "V4L2" else cv2.CAP_ANY source = self._resolve_usb_video_path(cam) + device = source or f"/dev/video{cam.index}" + runtime["device"] = device + if source: cap = cv2.VideoCapture(source, api_preference) else: @@ -189,24 +198,20 @@ class CameraManager: f"Falha ao abrir câmera USB. device_path={getattr(cam, 'device_path', None)} index={cam.index}" ) - # Configuração desejada + device = source or f"/dev/video{cam.index}" + self._v4l2_set_ctrls(device, { + "exposure_dynamic_framerate": 0, + "auto_exposure": 3, + "white_balance_automatic": 1, + "gain": 0, + }) + + # Configura formato primeiro + cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG")) cap.set(cv2.CAP_PROP_FRAME_WIDTH, cam.width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, cam.height) cap.set(cv2.CAP_PROP_FPS, float(self.state.fps)) - # Tenta reduzir buffer - try: - cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) - except Exception: - pass - - # Tenta MJPG, se a câmera suportar - try: - fourcc = cv2.VideoWriter_fourcc(*"MJPG") - cap.set(cv2.CAP_PROP_FOURCC, fourcc) - except Exception: - pass - # Pequeno warmup time.sleep(0.25) @@ -512,41 +517,66 @@ class CameraManager: cam = self.state.get_camera(camera_id) ctrl = self.state.get_camera_controls(camera_id) - # FPS - try: - cap.set(cv2.CAP_PROP_FPS, float(self.state.fps)) - except Exception: - pass + device = runtime.get("device") + if not device: + device = getattr(cam, "device_path", None) if cam is not None else None + if not device: + device = f"/dev/video{runtime['camera_index']}" - # Exposição - try: - if ctrl.ae_enable: - # automático em muitos backends V4L2 - cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.75) - else: - cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) - except Exception: - pass + # Sempre protege FPS + self._v4l2_set_ctrls(device, { + "exposure_dynamic_framerate": 0, + }) - if not ctrl.ae_enable and ctrl.exposure_time_us is not None: - try: - cap.set(cv2.CAP_PROP_EXPOSURE, float(ctrl.exposure_time_us)) - except Exception: - pass + # AE + if ctrl.ae_enable: + self._v4l2_set_ctrls(device, { + "auto_exposure": 3, + }) + else: + self._v4l2_set_ctrls(device, { + "auto_exposure": 1, + }) + + if ctrl.exposure_time_us is not None: + # V4L2 exposure_time_absolute é em unidades de 100 us + exp_abs = int(ctrl.exposure_time_us / 100) + exp_abs = max(1, min(5000, exp_abs)) + + self._v4l2_set_ctrls(device, { + "exposure_time_absolute": exp_abs, + }) # Ganho if ctrl.analogue_gain is not None: - try: - cap.set(cv2.CAP_PROP_GAIN, float(ctrl.analogue_gain)) - except Exception: + gain = int(max(0, min(100, ctrl.analogue_gain))) + self._v4l2_set_ctrls(device, { + "gain": gain, + }) + + # AWB + if cam is not None and cam.role == "rgb": + self._v4l2_set_ctrls(device, { + "white_balance_automatic": 1 if ctrl.awb_enable else 0, + }) + + if not ctrl.awb_enable and ctrl.colour_gains is not None: + # Aqui não dá para aplicar r_gain/b_gain diretamente nessa webcam. + # Ela só tem white_balance_temperature. pass - # AWB / WB - só RGB - if cam is not None and cam.role == "rgb": - try: - cap.set(cv2.CAP_PROP_AUTO_WB, 1 if ctrl.awb_enable else 0) - except Exception: - pass + cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG")) + cap.set(cv2.CAP_PROP_FPS, float(self.state.fps)) + + def _v4l2_set_ctrls(self, device, controls: dict): + args = ["v4l2-ctl", "-d", device] + for k, v in controls.items(): + args += ["-c", f"{k}={v}"] + + try: + subprocess.run(args, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception: + pass # ========================================================= # Sensor modes diff --git a/Python/raspi/cam_3/pi/raw_processor_core.py b/Python/raspi/cam_3/pi/raw_processor_core.py index 37206b999..2c6707f9f 100644 --- a/Python/raspi/cam_3/pi/raw_processor_core.py +++ b/Python/raspi/cam_3/pi/raw_processor_core.py @@ -1,13 +1,33 @@ +import json +import os +import cv2 import numpy as np import math from typing import Optional class RawProcessorCore: - def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"): + def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG", calibration_json_path=None): self.sensor_width = sensor_width self.sensor_height = sensor_height self.bayer_pattern = bayer_pattern.upper() + self.fusion_config = { + "alignment_mode": "manual_affine", # identity | manual_offset | manual_affine | homography + "baseline_mm": 75.0, + "manual_offsets": { + "cam0": {"dx": 0, "dy": 0, "theta_deg": 0.0}, + "cam1": {"dx": 0, "dy": 0, "theta_deg": 0.0}, + }, + "homographies": { + "cam0_to_cam2": None, + "cam1_to_cam2": None, + }, + "crop_valid_common": True, + "resize_after_crop": True, + "target_size": None, # (w, h) ou None para manter o shape da RGB + } + if calibration_json_path: + self.load_fusion_config_json(calibration_json_path) def unpack_raw10_packed( self, @@ -21,6 +41,9 @@ class RawProcessorCore: width = sensor_width if sensor_width is not None else self.sensor_width height = sensor_height if sensor_height is not None else self.sensor_height + if width % 4 != 0: + raise ValueError(f"Largura {width} não é múltipla de 4 para RAW10 packed") + expected_packed_width = math.ceil(width * 10 / 8) actual_h, actual_w = packed_frame.shape[:2] @@ -120,4 +143,607 @@ class RawProcessorCore: if output_dtype == "uint16": return (chw * 65535.0).clip(0, 65535).astype(np.uint16) - raise ValueError(f"output_dtype não suportado: {output_dtype}") \ No newline at end of file + raise ValueError(f"output_dtype não suportado: {output_dtype}") + + def _channel_names_from_decoded(self, decoded): + names = ["R", "G", "B"] + if "cam0" in decoded: + names.append("RE") + if "cam1" in decoded: + names.append("NIR") + return names + + def decode_bins_cameras(self, bins_data, bins_meta): + decoded = {} + + for data, meta in zip(bins_data, bins_meta): + role = (meta.get("role") or "").strip().lower() + bit_depth = int(meta.get("bit_depth", 10)) + max_val = float((1 << bit_depth) - 1) + + if role == "rgb": + decoded["cam2"] = { + "name": "RGB", + "image": data.astype(np.float32) / max_val, + "meta": meta, + } + + elif role == "re": + decoded["cam0"] = { + "name": "RE", + "image": data.astype(np.float32) / max_val, + "meta": meta, + } + + elif role == "nir": + decoded["cam1"] = { + "name": "NIR", + "image": data.astype(np.float32) / max_val, + "meta": meta, + } + + return decoded + + def build_multispectral_tensor(self, bins_data, bins_meta): + decoded = self.decode_bins_cameras(bins_data, bins_meta) + + if "cam2" not in decoded: + raise RuntimeError("RGB obrigatório") + + channel_names = self._channel_names_from_decoded(decoded) + tensor = self.fuse_multispec_cameras(decoded, meta=None, channels_expected=len(channel_names)) + return tensor, channel_names + + def build_infer_tensor_from_stream_old(self, frame, meta, channels_expected): + """ + Converte o frame vindo do stream do Pi em tensor (C,H,W) float32 0..1 + compatível com o modelo. + Suporta: + - RGB uint8/float32 já pronto + - MULTISPEC uint8/float32 já pronto + - RAW_BRUTO multi_payload (cam2 RGB + cam0/cam1 packed) + """ + frame_type = meta.get("frame_type") + dtype_str = meta.get("dtype") or meta.get("output_dtype", "uint8") + camera_frames = meta.get("camera_frames", {}) or {} + + # ------------------------------------------------- + # RAW_BRUTO multi_payload + # ------------------------------------------------- + if frame_type == "RAW_BRUTO": + if not isinstance(frame, dict): + raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi") + + arrays = [] + channel_names = [] + + # RGB USB + if "cam2" in frame: + rgb_bgr = frame["cam2"] + if rgb_bgr.ndim != 3 or rgb_bgr.shape[2] != 3: + raise RuntimeError(f"cam2 RGB inválida: shape={rgb_bgr.shape}") + + rgb = rgb_bgr[:, :, ::-1].astype(np.float32) / 255.0 + rgb_chw = np.transpose(rgb, (2, 0, 1)) + arrays.append(rgb_chw) + channel_names.extend(["R", "G", "B"]) + else: + raise RuntimeError("RAW_BRUTO para inferência precisa incluir cam2 (RGB)") + + # RE / NIR + for cam_id, spec_name in (("cam0", "RE"), ("cam1", "NIR")): + if cam_id not in frame: + continue + + packed = frame[cam_id] + if packed.ndim == 3 and packed.shape[2] == 1: + packed = packed[:, :, 0] + + cam_meta = camera_frames.get(cam_id, {}) + packed_width = int(cam_meta.get("width", packed.shape[1])) + height = int(cam_meta.get("height", packed.shape[0])) + bayer = cam_meta.get("bayer_pattern", self.bayer_pattern) + bit_depth = int(cam_meta.get("bit_depth", 10)) + + if bit_depth == 10: + real_width = int((packed_width * 8) / 10) + else: + real_width = packed_width + + rp = RawProcessorCore( + sensor_width=real_width, + sensor_height=height, + bayer_pattern=bayer, + ) + + raw16 = rp.unpack_raw10_packed(packed) + + max_val = float((1 << bit_depth) - 1) + single = np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)[None, :, :] + + arrays.append(single) + channel_names.append(spec_name) + + if len(arrays) < 2: + raise RuntimeError("RAW_BRUTO requer RGB + pelo menos um canal espectral para inferência") + + min_h = min(a.shape[1] for a in arrays) + min_w = min(a.shape[2] for a in arrays) + arrays = [a[:, :min_h, :min_w] for a in arrays] + + raw_np = np.concatenate(arrays, axis=0) + + if raw_np.shape[0] != channels_expected: + raise RuntimeError( + f"Tensor RAW_BRUTO montado com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected} | got={channel_names}" + ) + + return raw_np + + # ------------------------------------------------- + # RGB ou MULTISPEC já pronto + # ------------------------------------------------- + if frame_type == "RGB" or frame_type == "MULTISPEC": + if not isinstance(frame, np.ndarray): + raise RuntimeError(f"Frame {frame_type} esperado como ndarray") + + if frame.ndim != 3: + raise RuntimeError(f"Frame {frame_type} inválido: shape={frame.shape}") + + if dtype_str == "uint8": + raw_np = frame.astype(np.float32) / 255.0 + elif dtype_str == "float32": + raw_np = frame.astype(np.float32) + elif dtype_str == "uint16": + raw_np = frame.astype(np.float32) / 65535.0 + else: + raise RuntimeError(f"dtype {frame_type} não suportado: {dtype_str}") + + if raw_np.shape[0] != channels_expected: + raise RuntimeError( + f"Frame {frame_type} com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected}" + ) + + return raw_np + + + raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}") + + def build_infer_tensor_from_stream(self, frame, meta, channels_expected): + frame_type = meta.get("frame_type") + + if frame_type == "RAW_BRUTO": + decoded = self.decode_stream_cameras(frame, meta) + return self.fuse_multispec_cameras(decoded, meta, channels_expected) + + if frame_type in ("RGB", "MULTISPEC"): + return self.build_infer_tensor_from_stream_old(frame, meta, channels_expected) + + raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}") + + def decode_stream_cameras(self, frame, meta): + if not isinstance(frame, dict): + raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi") + + camera_frames = meta.get("camera_frames", {}) or {} + decoded = {} + + if "cam2" in frame: + rgb_bgr = frame["cam2"] + if rgb_bgr.ndim != 3 or rgb_bgr.shape[2] != 3: + raise RuntimeError(f"cam2 RGB inválida: shape={rgb_bgr.shape}") + + rgb = rgb_bgr[:, :, ::-1].astype(np.float32) / 255.0 + decoded["cam2"] = { + "name": "RGB", + "image": rgb, + "meta": camera_frames.get("cam2", {}) + } + + for cam_id, spec_name in (("cam0", "RE"), ("cam1", "NIR")): + if cam_id not in frame: + continue + + packed = frame[cam_id] + if packed.ndim == 3 and packed.shape[2] == 1: + packed = packed[:, :, 0] + + cam_meta = camera_frames.get(cam_id, {}) + packed_width = int(cam_meta.get("width", packed.shape[1])) + height = int(cam_meta.get("height", packed.shape[0])) + bayer = cam_meta.get("bayer_pattern", self.bayer_pattern) + bit_depth = int(cam_meta.get("bit_depth", 10)) + + real_width = int((packed_width * 8) / 10) if bit_depth == 10 else packed_width + + rp = RawProcessorCore( + sensor_width=real_width, + sensor_height=height, + bayer_pattern=bayer, + ) + + raw16 = rp.unpack_raw10_packed(packed) + max_val = float((1 << bit_depth) - 1) + single = np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0) + + decoded[cam_id] = { + "name": spec_name, + "image": single, + "meta": cam_meta + } + + return decoded + + def fuse_multispec_cameras(self, decoded, meta, channels_expected): + if "cam2" not in decoded: + raise RuntimeError("Fusão requer cam2 (RGB) como referência") + + rgb = decoded["cam2"]["image"] + h, w = rgb.shape[:2] + + rgb_chw = np.transpose(rgb, (2, 0, 1)) + channels = [rgb_chw] + names = ["R", "G", "B"] + + valid_masks = [np.ones((h, w), dtype=np.uint8)] + + for cam_id, ch_name in (("cam0", "RE"), ("cam1", "NIR")): + if cam_id not in decoded: + continue + + img = decoded[cam_id]["image"] + aligned, valid_mask = self._warp_with_valid_mask(img, cam_id, (h, w), meta) + + channels.append(aligned[None, :, :]) + names.append(ch_name) + valid_masks.append(valid_mask) + + cfg = self.fusion_config + if cfg.get("crop_valid_common", False): + crop_box = self._compute_common_crop_box(valid_masks) + if crop_box is not None: + channels = self._crop_and_resize_channels(channels, crop_box, (h, w)) + + tensor = np.concatenate(channels, axis=0) + + if tensor.shape[0] != channels_expected: + raise RuntimeError( + f"Tensor fundido com canais inesperados: {tensor.shape[0]} | " + f"esperado={channels_expected} | got={names}" + ) + + return tensor.astype(np.float32, copy=False) + + def _shift_image(self, img, dx, dy): + h, w = img.shape[:2] + M = np.float32([[1, 0, dx], [0, 1, dy]]) + return cv2.warpAffine( + img, M, (w, h), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0 + ) + + def _affine_image(self, img, dx, dy, theta_deg): + h, w = img.shape[:2] + center = (w * 0.5, h * 0.5) + + M = cv2.getRotationMatrix2D(center, theta_deg, 1.0) + M[0, 2] += dx + M[1, 2] += dy + + return cv2.warpAffine( + img, + M, + (w, h), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0 + ) + + def _warp_with_valid_mask(self, img, cam_id, ref_shape, meta): + ref_h, ref_w = ref_shape + + if img.shape[:2] != (ref_h, ref_w): + img = cv2.resize(img, (ref_w, ref_h), interpolation=cv2.INTER_LINEAR) + + cfg = self.fusion_config + mode = cfg.get("alignment_mode", "identity") + + mask = np.ones((ref_h, ref_w), dtype=np.uint8) * 255 + + if mode == "identity": + warped = img + warped_mask = mask + + elif mode == "manual_offset": + offs = cfg.get("manual_offsets", {}).get(cam_id, {}) + dx = int(offs.get("dx", 0)) + dy = int(offs.get("dy", 0)) + + warped = self._shift_image(img, dx, dy) + warped_mask = self._shift_image(mask, dx, dy) + + elif mode == "manual_affine": + offs = cfg.get("manual_offsets", {}).get(cam_id, {}) + dx = int(offs.get("dx", 0)) + dy = int(offs.get("dy", 0)) + theta_deg = float(offs.get("theta_deg", 0.0)) + + warped = self._affine_image(img, dx, dy, theta_deg) + warped_mask = self._affine_image(mask, dx, dy, theta_deg) + + elif mode == "homography": + H = cfg.get("homographies", {}).get(f"{cam_id}_to_cam2") + + if H is None: + warped = img + warped_mask = mask + else: + H = np.asarray(H, dtype=np.float32) + + if H.shape != (3, 3): + raise RuntimeError(f"Homografia inválida para {cam_id}: shape={H.shape}") + + warped = cv2.warpPerspective( + img, H, (ref_w, ref_h), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0 + ) + + warped_mask = cv2.warpPerspective( + mask, H, (ref_w, ref_h), + flags=cv2.INTER_NEAREST, + borderMode=cv2.BORDER_CONSTANT, + borderValue=0 + ) + + else: + raise RuntimeError(f"alignment_mode inválido: {mode}") + + warped_mask = (warped_mask > 0).astype(np.uint8) + return warped, warped_mask + + def _compute_common_crop_box(self, masks): + if not masks: + return None + + common = masks[0].copy() + for m in masks[1:]: + common = np.logical_and(common > 0, m > 0) + + ys, xs = np.where(common) + if len(xs) == 0 or len(ys) == 0: + return None + + x0 = int(xs.min()) + x1 = int(xs.max()) + 1 + y0 = int(ys.min()) + y1 = int(ys.max()) + 1 + + return x0, y0, x1, y1 + + def _crop_and_resize_channels(self, channels, crop_box, ref_shape): + x0, y0, x1, y1 = crop_box + ref_h, ref_w = ref_shape + + cropped = [ch[:, y0:y1, x0:x1] for ch in channels] + + cfg = self.fusion_config + if not cfg.get("resize_after_crop", False): + return cropped + + target_size = cfg.get("target_size", None) + if target_size is None: + target_w, target_h = ref_w, ref_h + else: + target_w, target_h = target_size + + resized = [] + for ch in cropped: + ch_resized = np.stack([ + cv2.resize( + ch_i, + (target_w, target_h), + interpolation=cv2.INTER_LINEAR + ) + for ch_i in ch + ], axis=0) + resized.append(ch_resized) + + return resized + + + def extract_camera_meta(self, meta_json: dict, cam_id: str) -> dict: + cam_frames = meta_json.get("camera_frames", {}) or meta_json.get("stream_meta", {}).get("camera_frames", {}) + + cam = cam_frames.get(cam_id) + if not cam: + raise ValueError(f"Camera {cam_id} não encontrada no meta") + + # Detecta RAW10 packed mono + if int(cam.get("channels", 1)) == 1 and int(cam.get("bit_depth", 10)) == 10: + packed_width = int(cam.get("width")) + height = int(cam.get("height")) + + # 🔥 converte packed → real + real_width = int((packed_width * 8) / 10) + + return { + "camera_id": cam_id, + "width": real_width, + "height": height, + "channels": 1, + "bit_depth": 10, + "shape": [height, packed_width], # packed shape + "role": cam.get("role") + } + + # RGB + else: + width = int(cam.get("width")) + height = int(cam.get("height")) + channels = int(cam.get("channels", 3)) + + return { + "camera_id": cam_id, + "width": width, + "height": height, + "channels": channels, + "bit_depth": int(cam.get("bit_depth", 8)), + "shape": [height, width, channels], # 🔥 AQUI está a correção + "role": cam.get("role") + } + + # ============================================================ + # RAW10 PACKED + # ============================================================ + + def packed_width_for_raw10(self, sensor_width: int = None) -> int: + width = sensor_width if sensor_width is not None else self.sensor_width + return math.ceil(width * 10 / 8) + + def pack_raw10_packed(self, raw16: np.ndarray) -> np.ndarray: + h, w = raw16.shape + + if w % 4 != 0: + raise ValueError(f"Width precisa ser múltiplo de 4 para pack otimizado. Veio {w}") + + raw16 = np.clip(raw16, 0, 1023).astype(np.uint16) + + p0 = raw16[:, 0::4] + p1 = raw16[:, 1::4] + p2 = raw16[:, 2::4] + p3 = raw16[:, 3::4] + + b0 = (p0 >> 2).astype(np.uint8) + b1 = (p1 >> 2).astype(np.uint8) + b2 = (p2 >> 2).astype(np.uint8) + b3 = (p3 >> 2).astype(np.uint8) + + b4 = ( + ((p0 & 0x03) << 0) | + ((p1 & 0x03) << 2) | + ((p2 & 0x03) << 4) | + ((p3 & 0x03) << 6) + ).astype(np.uint8) + + packed = np.empty((h, w // 4, 5), dtype=np.uint8) + packed[:, :, 0] = b0 + packed[:, :, 1] = b1 + packed[:, :, 2] = b2 + packed[:, :, 3] = b3 + packed[:, :, 4] = b4 + + return packed.reshape(h, w // 4 * 5) + + def load_raw10_packed_file(self, path: str, width: int, height: int) -> np.ndarray: + packed_width = self.packed_width_for_raw10(width) + + expected_size = height * packed_width + actual_size = os.path.getsize(path) + + if actual_size != expected_size: + raise ValueError( + f"Tamanho inválido RAW10: {actual_size}, esperado {expected_size} em {path}" + ) + + packed = np.fromfile(path, dtype=np.uint8).reshape(height, packed_width) + return self.unpack_raw10_packed(packed, sensor_width=width, sensor_height=height) + + def save_raw10_packed_file(self, path: str, raw16: np.ndarray): + packed = self.pack_raw10_packed(raw16) + packed.tofile(path) + + # ============================================================ + # RGB UINT8 + # ============================================================ + + def load_rgb_u8_file(self, path: str, shape) -> np.ndarray: + arr = np.fromfile(path, dtype=np.uint8) + + expected = np.prod(shape) + if arr.size != expected: + raise ValueError( + f"Tamanho inválido RGB: {arr.size}, esperado {expected} em {path}" + ) + + return arr.reshape(shape) + + + def save_rgb_u8_file(self, path: str, arr: np.ndarray): + arr.astype(np.uint8).tofile(path) + + + # ============================================================ + # DISPATCHER (O MAIS IMPORTANTE) + # ============================================================ + + def load_native_bin(self, path: str, cam_meta: dict) -> np.ndarray: + """ + Decide automaticamente como carregar o .bin baseado no meta. + """ + + channels = int(cam_meta.get("channels", 1)) + bit_depth = int(cam_meta.get("bit_depth", 10)) + shape = cam_meta.get("shape") + + if channels == 1 and bit_depth == 10: + width = int(cam_meta["width"]) + height = int(cam_meta["height"]) + return self.load_raw10_packed_file(path, width, height) + + elif channels == 3 and bit_depth == 8: + return self.load_rgb_u8_file(path, shape) + + else: + raise ValueError( + f"Formato não suportado: channels={channels}, bit_depth={bit_depth}" + ) + + + def save_native_bin(self, path: str, arr: np.ndarray, cam_meta: dict): + """ + Salva no formato correto baseado no meta. + """ + + channels = int(cam_meta.get("channels", 1)) + bit_depth = int(cam_meta.get("bit_depth", 10)) + + if channels == 1 and bit_depth == 10: + self.save_raw10_packed_file(path, arr) + + elif channels == 3 and bit_depth == 8: + self.save_rgb_u8_file(path, arr) + + else: + raise ValueError( + f"Formato não suportado para salvar: channels={channels}, bit_depth={bit_depth}" + ) + + + def load_fusion_config_json(self, path: str): + if not path or not os.path.isfile(path): + raise FileNotFoundError(f"Arquivo de calibração não encontrado: {path}") + + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + + fusion = data.get("fusion_config") + if not isinstance(fusion, dict): + print("[WARN] JSON sem fusion_config. Mantendo config padrão.") + return + + self.fusion_config = self._merge_fusion_config(self.fusion_config, fusion) + + def _merge_fusion_config(self, default_cfg: dict, loaded_cfg: dict) -> dict: + cfg = json.loads(json.dumps(default_cfg)) + + for key, value in loaded_cfg.items(): + if isinstance(value, dict) and isinstance(cfg.get(key), dict): + cfg[key].update(value) + else: + cfg[key] = value + + return cfg \ No newline at end of file diff --git a/Python/raspi/cam_3/pi/server.py b/Python/raspi/cam_3/pi/server.py index 33e641c49..1b9635917 100644 --- a/Python/raspi/cam_3/pi/server.py +++ b/Python/raspi/cam_3/pi/server.py @@ -469,7 +469,6 @@ class ModuleServer: return {"ok": False, "error": "Módulo não inicializado"} self._restart_module_if_needed() - if getattr(self.stream_sender, "is_running", False): return { "ok": True, @@ -487,7 +486,6 @@ class ModuleServer: self.state.stream_port = port self.state.stream_fps = fps self.state.status = "streaming" - return { "ok": True, "streaming": True, diff --git a/Python/raspi/cam_3/pi/state.py b/Python/raspi/cam_3/pi/state.py index 04961f89e..8c13063ea 100644 --- a/Python/raspi/cam_3/pi/state.py +++ b/Python/raspi/cam_3/pi/state.py @@ -450,6 +450,7 @@ class ModuleState: else: self.detected_mode = "NONE" + def resolve_capture_mode(self) -> str: connected_re = self.get_active_camera_by_role("re") is not None connected_nir = self.get_active_camera_by_role("nir") is not None diff --git a/Python/raspi/check_saved_files.py b/Python/raspi/check_saved_files.py index 3adfacfdc..92237d376 100644 --- a/Python/raspi/check_saved_files.py +++ b/Python/raspi/check_saved_files.py @@ -6,8 +6,8 @@ from pathlib import Path import cv2 import numpy as np -from cam_2.pi.raw_processor_core import RawProcessorCore -from cam_2.pi.raw_processor_preview import RawProcessorPreview +from cam_3.pi.raw_processor_core import RawProcessorCore +from cam_3.pi.raw_processor_preview import RawProcessorPreview def load_json(path: Path) -> dict: diff --git a/Python/raspi/manual_fusion_calibrator.py b/Python/raspi/manual_fusion_calibrator.py index fe291c7e9..3911a2bfc 100644 --- a/Python/raspi/manual_fusion_calibrator.py +++ b/Python/raspi/manual_fusion_calibrator.py @@ -7,15 +7,7 @@ from datetime import datetime import cv2 import numpy as np -from cam_3.multispectral_service import MultiSpectralService -from cam_3.stream_receiver import StreamReceiver -from cam_3.pi.raw_processor_core import RawProcessorCore -from cam_3.pi.raw_processor_preview import RawProcessorPreview - - -STREAM_PORT = 6001 -PI_HOST = "192.168.105.6" -PC_HOST = "192.168.105.5" +from cam_3.multispectral_client import MultiSpectralClient # ============================================================ @@ -172,7 +164,7 @@ def stack_2x2(a: np.ndarray, b: np.ndarray, c: np.ndarray, d: np.ndarray) -> np. def build_empty_panel_like(ref_bgr: np.ndarray, title: str) -> np.ndarray: img = np.zeros_like(ref_bgr) - overlay_hud(img, [title, "sem frame disponível"], x=18, y=40, font_scale=0.8, line_step=34) + overlay_hud(img, [title, "sem frame disponivel"], x=18, y=40, font_scale=0.8, line_step=34) return img @@ -199,76 +191,6 @@ def validate_module_ready(status: dict, frame_type: str, raw_policy: str, captur raise RuntimeError(f"frame_type desconhecido para validação: {frame_type}") -def resolve_effective_capture_mode(requested_mode: str) -> str: - if requested_mode in ("SINGLE", "DOUBLE", "TRIPLE"): - return requested_mode - return "AUTO" - - -# ============================================================ -# Decodificação do stream RAW_BRUTO -# ============================================================ - -class StreamDecoder: - def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"): - self.sensor_width = sensor_width - self.sensor_height = sensor_height - self.bayer_pattern = bayer_pattern - - def decode_stream_cameras(self, frame, meta): - if not isinstance(frame, dict): - raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi") - - camera_frames = meta.get("camera_frames", {}) or {} - decoded = {} - - if "cam2" in frame: - rgb_bgr = frame["cam2"] - if rgb_bgr.ndim != 3 or rgb_bgr.shape[2] != 3: - raise RuntimeError(f"cam2 RGB inválida: shape={rgb_bgr.shape}") - - rgb = rgb_bgr[:, :, ::-1].astype(np.float32) / 255.0 - decoded["cam2"] = { - "name": "RGB", - "image": rgb, - "meta": camera_frames.get("cam2", {}), - } - - for cam_id, spec_name in (("cam0", "RE"), ("cam1", "NIR")): - if cam_id not in frame: - continue - - packed = frame[cam_id] - if packed.ndim == 3 and packed.shape[2] == 1: - packed = packed[:, :, 0] - - cam_meta = camera_frames.get(cam_id, {}) - packed_width = int(cam_meta.get("width", packed.shape[1])) - height = int(cam_meta.get("height", packed.shape[0])) - bayer = cam_meta.get("bayer_pattern", self.bayer_pattern) - bit_depth = int(cam_meta.get("bit_depth", 10)) - - real_width = int((packed_width * 8) / 10) if bit_depth == 10 else packed_width - - rp = RawProcessorCore( - sensor_width=real_width, - sensor_height=height, - bayer_pattern=bayer, - ) - - raw16 = rp.unpack_raw10_packed(packed) - max_val = float((1 << bit_depth) - 1) - single = np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0) - - decoded[cam_id] = { - "name": spec_name, - "image": single, - "meta": cam_meta, - } - - return decoded - - # ============================================================ # Persistência dos offsets # ============================================================ @@ -339,9 +261,9 @@ def main(): description="Calibrador manual de offsets para fusão RGB/RE/NIR a partir do stream RAW_BRUTO.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - parser.add_argument("--pi_host", default=PI_HOST) - parser.add_argument("--pc_host", default=PC_HOST) - parser.add_argument("--stream_port", type=int, default=STREAM_PORT) + parser.add_argument("--pi_host", default="192.168.105.6") + parser.add_argument("--pc_host", default="192.168.105.5") + parser.add_argument("--stream_port", type=int, default=6001) parser.add_argument("--server_port", type=int, default=5000) parser.add_argument("--fps", type=int, default=20) parser.add_argument("--width", type=int, default=640) @@ -400,11 +322,7 @@ def main(): last_msg_t = time.time() return - effective_capture_mode = resolve_effective_capture_mode(args.capture_mode) - - receiver = StreamReceiver(host="0.0.0.0", port=args.stream_port) - svc = MultiSpectralService(host=args.pi_host, port=args.server_port, timeout=10) - decoder = StreamDecoder(sensor_width=args.width, sensor_height=args.height, bayer_pattern=args.bayer) + effective_capture_mode = args.capture_mode offsets_data = load_offsets_json(args.load_json, args, effective_capture_mode) offsets = offsets_data["manual_offsets"] @@ -444,361 +362,326 @@ def main(): cv2.setMouseCallback(window_name, on_mouse) try: - receiver.start() - time.sleep(0.5) + with MultiSpectralClient( + pi_host=args.pi_host, + pc_host=args.pc_host, + server_port=args.server_port, + stream_port=args.stream_port, + width=args.width, + height=args.height, + bayer=args.bayer, + fps=args.fps, + frame_type="RAW_BRUTO", + output_dtype="uint8", + capture_mode=effective_capture_mode, + raw_policy=args.raw_policy, + module_calibration_json=None, + ) as cam: + while True: + t0 = time.time() - print(f"[INFO] Verificando conexão com o módulo em {args.pi_host}:{args.server_port}...") - if not svc.check_connection(2): - raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.") - print("[OK] Módulo conectado e respondendo.") + frame, meta = cam.get_next_frame(timeout=2.0) - svc.connect() + if meta is not None and frame is not None and meta.get("frame_id") != last_frame_id: + last_frame_id = meta["frame_id"] - print("SET CAM0 RES:", svc.set_camera_resolution(0, args.width, args.height)) - print("SET CAM1 RES:", svc.set_camera_resolution(1, args.width, args.height)) - print("SET CAM2 RES:", svc.set_camera_resolution(2, args.width, args.height)) - 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("RAW_BRUTO")) - print("SET OUTPUT DTYPE:", svc.set_output_dtype("float32")) + if not isinstance(frame, dict): + raise RuntimeError("Este calibrador espera RAW_BRUTO multi-payload como dict de câmeras.") - begin_resp = svc.begin(frame_type="RAW_BRUTO", output_dtype="float32", capture_mode=effective_capture_mode) - print("BEGIN:", begin_resp) + decoded = cam.core.decode_stream_cameras(frame, meta) + decoded_last = decoded - 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)) + curr_frame_id = meta.get("frame_id") + if curr_frame_id is not None and last_stream_frame_id != curr_frame_id: + stream_frames_accum += 1 + last_stream_frame_id = curr_frame_id - validate_module_ready(status, "RAW_BRUTO", args.raw_policy, effective_capture_mode) - print("START STREAM:", svc.start_stream(args.pc_host, args.stream_port, fps=args.fps)) + 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() - while True: - t0 = 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() - meta = receiver.last_meta - frame = receiver.last_frame + if decoded_last: + rgb01 = decoded_last.get("cam2", {}).get("image") + re01 = decoded_last.get("cam0", {}).get("image") + nir01 = decoded_last.get("cam1", {}).get("image") - if meta is not None and frame is not None and meta.get("frame_id") != last_frame_id: - last_frame_id = meta["frame_id"] + if rgb01 is None: + # fallback para exibição quando não houver RGB + if re01 is not None: + rgb01 = np.stack([re01, re01, re01], axis=2) + elif nir01 is not None: + rgb01 = np.stack([nir01, nir01, nir01], axis=2) + else: + rgb01 = np.zeros((args.height, args.width, 3), dtype=np.float32) - if not isinstance(frame, dict): - raise RuntimeError("Este calibrador espera RAW_BRUTO multi-payload como dict de câmeras.") - - decoded = decoder.decode_stream_cameras(frame, meta) - decoded_last = decoded - - curr_frame_id = meta.get("frame_id") - if curr_frame_id is not None and 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() - - if decoded_last: - rgb01 = decoded_last.get("cam2", {}).get("image") - re01 = decoded_last.get("cam0", {}).get("image") - nir01 = decoded_last.get("cam1", {}).get("image") - - if rgb01 is None: - # fallback para exibição quando não houver RGB + base_h, base_w = rgb01.shape[:2] if re01 is not None: - rgb01 = np.stack([re01, re01, re01], axis=2) - elif nir01 is not None: - rgb01 = np.stack([nir01, nir01, nir01], axis=2) - else: - rgb01 = np.zeros((args.height, args.width, 3), dtype=np.float32) + re01 = resize_if_needed(re01, (base_h, base_w)) + if nir01 is not None: + nir01 = resize_if_needed(nir01, (base_h, base_w)) - base_h, base_w = rgb01.shape[:2] - if re01 is not None: - re01 = resize_if_needed(re01, (base_h, base_w)) - if nir01 is not None: - nir01 = resize_if_needed(nir01, (base_h, base_w)) + rgb_panel = to_bgr_u8_from_rgb01(rgb01) + re_panel = gray_to_color_bgr(re01, "RE") if re01 is not None else build_empty_panel_like(rgb_panel, "RE") + nir_panel = gray_to_color_bgr(nir01, "NIR") if nir01 is not None else build_empty_panel_like(rgb_panel, "NIR") - rgb_panel = to_bgr_u8_from_rgb01(rgb01) - re_panel = gray_to_color_bgr(re01, "RE") if re01 is not None else build_empty_panel_like(rgb_panel, "RE") - nir_panel = gray_to_color_bgr(nir01, "NIR") if nir01 is not None else build_empty_panel_like(rgb_panel, "NIR") + active_spec_name = "RE" if selected_cam == "cam0" else "NIR" + active_spec = re01 if selected_cam == "cam0" else nir01 + dx = int(offsets.get(selected_cam, {}).get("dx", 0)) + dy = int(offsets.get(selected_cam, {}).get("dy", 0)) + theta_deg = float(offsets.get(selected_cam, {}).get("theta_deg", 0.0)) - active_spec_name = "RE" if selected_cam == "cam0" else "NIR" - active_spec = re01 if selected_cam == "cam0" else nir01 - dx = int(offsets.get(selected_cam, {}).get("dx", 0)) - dy = int(offsets.get(selected_cam, {}).get("dy", 0)) - theta_deg = float(offsets.get(selected_cam, {}).get("theta_deg", 0.0)) + H_key = f"{selected_cam}_to_cam2" + H = offsets_data.get("homographies", {}).get(H_key) - H_key = f"{selected_cam}_to_cam2" - H = offsets_data.get("homographies", {}).get(H_key) - - fuse_panel = build_overlay_fuse( - rgb01, - active_spec, - active_spec_name, - dx, - dy, - theta_deg=theta_deg, - alpha=args.alpha, - calibration_mode=calibration_mode, - H=H, - ) - - spec_pts = len(selected_points_spec[selected_cam]) - rgb_pts = len(selected_points_rgb[selected_cam]) - - lines_fuse = [ - f"FUSE: RGB + {active_spec_name}", - f"mode={calibration_mode} | selecionada={selected_cam}", - f"dx={dx} | dy={dy} | theta={theta_deg:.2f}g | step={args.step} | ang_step={args.angle_step:.2f}g", - f"pts_spec={spec_pts} | pts_rgb={rgb_pts} | min=4 | fps_stream={fps_stream:.1f} | fps_view={fps_view:.1f}" - ] - overlay_hud(fuse_panel, lines_fuse) - - lines_rgb = ["RGB (cam2)"] - overlay_hud(rgb_panel, lines_rgb) - - re_dx = int(offsets.get("cam0", {}).get("dx", 0)) - re_dy = int(offsets.get("cam0", {}).get("dy", 0)) - re_theta = float(offsets.get("cam0", {}).get("theta_deg", 0.0)) - nir_dx = int(offsets.get("cam1", {}).get("dx", 0)) - nir_dy = int(offsets.get("cam1", {}).get("dy", 0)) - nir_theta = float(offsets.get("cam1", {}).get("theta_deg", 0.0)) - overlay_hud(re_panel, [f"RE (cam0) | dx={re_dx} dy={re_dy} th={re_theta:.2f}g", "R seleciona RE"], y=24) - overlay_hud(nir_panel, [f"NIR (cam1) | dx={nir_dx} dy={nir_dy} th={nir_theta:.2f}g", "N seleciona NIR"], y=24) - - ph = max(fuse_panel.shape[0], rgb_panel.shape[0], re_panel.shape[0], nir_panel.shape[0]) - pw = max(fuse_panel.shape[1], rgb_panel.shape[1], re_panel.shape[1], nir_panel.shape[1]) - - def fit_panel(img): - if img.shape[:2] != (ph, pw): - return cv2.resize(img, (pw, ph), interpolation=cv2.INTER_NEAREST) - return img - - fuse_panel = fit_panel(fuse_panel) - rgb_panel = fit_panel(rgb_panel) - re_panel = fit_panel(re_panel) - nir_panel = fit_panel(nir_panel) - - panel_rects["fuse"] = (0, 0, pw, ph) - panel_rects["rgb"] = (pw, 0, pw * 2, ph) - panel_rects["cam0"] = (0, ph, pw, ph * 2) - panel_rects["cam1"] = (pw, ph, pw * 2, ph * 2) - - top = np.hstack([fuse_panel, rgb_panel]) - bottom = np.hstack([re_panel, nir_panel]) - board = np.vstack([top, bottom]) - - help_lines = [ - "M=manual_affine | H=homography | clique pares correspondentes | >=4 pares | SPACE=salva | C=limpa pts | Z=zera sel | X=zera tudo", - "A/W/S/D movem | J/L rotacionam | O/P muda passo angular | I/U remove ultimo ponto | ENTER calcula H | TAB alterna camera | Q/Esc sai", - ] - overlay_hud(board, help_lines, x=16, y=board.shape[0] - 44, font_scale=0.55, line_step=20) - - if last_msg and (time.time() - last_msg_t) < 2.5: - cv2.putText(board, last_msg, (16, board.shape[0] - 72), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA) - - if args.preview_scale != 1.0: - board = cv2.resize( - board, - (int(board.shape[1] * args.preview_scale), int(board.shape[0] * args.preview_scale)), - interpolation=cv2.INTER_NEAREST, + fuse_panel = build_overlay_fuse( + rgb01, + active_spec, + active_spec_name, + dx, + dy, + theta_deg=theta_deg, + alpha=args.alpha, + calibration_mode=calibration_mode, + H=H, ) - if calibration_mode == "homography": - color_spec = (0, 255, 255) - color_rgb = (0, 255, 0) + spec_pts = len(selected_points_spec[selected_cam]) + rgb_pts = len(selected_points_rgb[selected_cam]) - for idx, pt in enumerate(selected_points_spec[selected_cam]): - rect = panel_rects[selected_cam] - if rect is not None: - x0, y0, _, _ = rect - px = int(x0 + pt[0]) - py = int(y0 + pt[1]) - cv2.circle(board, (px, py), 5, color_spec, -1) - cv2.putText(board, str(idx + 1), (px + 6, py - 6), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, color_spec, 1, cv2.LINE_AA) + lines_fuse = [ + f"FUSE: RGB + {active_spec_name}", + f"mode={calibration_mode} | selecionada={selected_cam}", + f"dx={dx} | dy={dy} | theta={theta_deg:.2f}g | step={args.step} | ang_step={args.angle_step:.2f}g", + f"pts_spec={spec_pts} | pts_rgb={rgb_pts} | min=4 | fps_stream={fps_stream:.1f} | fps_view={fps_view:.1f}" + ] + overlay_hud(fuse_panel, lines_fuse) - for idx, pt in enumerate(selected_points_rgb[selected_cam]): - rect = panel_rects["rgb"] - if rect is not None: - x0, y0, _, _ = rect - px = int(x0 + pt[0]) - py = int(y0 + pt[1]) - cv2.circle(board, (px, py), 5, color_rgb, -1) - cv2.putText(board, str(idx + 1), (px + 6, py - 6), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, color_rgb, 1, cv2.LINE_AA) + lines_rgb = ["RGB (cam2)"] + overlay_hud(rgb_panel, lines_rgb) - cv2.imshow(window_name, board) - else: - blank = np.zeros((720, 1280, 3), dtype=np.uint8) - overlay_hud(blank, ["Aguardando frames do módulo..."], x=40, y=80, font_scale=1.0, line_step=34) - cv2.imshow(window_name, blank) + re_dx = int(offsets.get("cam0", {}).get("dx", 0)) + re_dy = int(offsets.get("cam0", {}).get("dy", 0)) + re_theta = float(offsets.get("cam0", {}).get("theta_deg", 0.0)) + nir_dx = int(offsets.get("cam1", {}).get("dx", 0)) + nir_dy = int(offsets.get("cam1", {}).get("dy", 0)) + nir_theta = float(offsets.get("cam1", {}).get("theta_deg", 0.0)) + overlay_hud(re_panel, [f"RE (cam0) | dx={re_dx} dy={re_dy} th={re_theta:.2f}g", "2 seleciona RE"], y=24) + overlay_hud(nir_panel, [f"NIR (cam1) | dx={nir_dx} dy={nir_dy} th={nir_theta:.2f}g", "3 seleciona NIR"], y=24) - k = cv2.waitKey(1) & 0xFF - if k in (ord("q"), ord("Q"), 27): - break - elif k in (ord("m"), ord("M")): - calibration_mode = "manual_affine" - offsets_data["alignment_mode"] = calibration_mode - last_msg = "Modo: manual_affine" - last_msg_t = time.time() - elif k in (ord("h"), ord("H")): - calibration_mode = "homography" - offsets_data["alignment_mode"] = calibration_mode - last_msg = "Modo: homography" - last_msg_t = time.time() - elif k in (ord("c"), ord("C")): - selected_points_spec[selected_cam] = [] - selected_points_rgb[selected_cam] = [] - last_msg = f"Pontos limpos: {selected_cam}" - last_msg_t = time.time() - elif k == 13: # ENTER - spec_pts = selected_points_spec[selected_cam] - rgb_pts = selected_points_rgb[selected_cam] + ph = max(fuse_panel.shape[0], rgb_panel.shape[0], re_panel.shape[0], nir_panel.shape[0]) + pw = max(fuse_panel.shape[1], rgb_panel.shape[1], re_panel.shape[1], nir_panel.shape[1]) - if len(spec_pts) >= 4 and len(rgb_pts) >= 4 and len(spec_pts) == len(rgb_pts): - src = np.array(spec_pts, dtype=np.float32) - dst = np.array(rgb_pts, dtype=np.float32) + def fit_panel(img): + if img.shape[:2] != (ph, pw): + return cv2.resize(img, (pw, ph), interpolation=cv2.INTER_NEAREST) + return img - H, status = cv2.findHomography(src, dst, method=cv2.RANSAC) - if H is not None: - offsets_data.setdefault("homographies", {}) - offsets_data["homographies"][f"{selected_cam}_to_cam2"] = H.tolist() - inliers = int(status.sum()) if status is not None else len(spec_pts) - last_msg = f"H calculada para {selected_cam} | pts={len(spec_pts)} | inliers={inliers}" + fuse_panel = fit_panel(fuse_panel) + rgb_panel = fit_panel(rgb_panel) + re_panel = fit_panel(re_panel) + nir_panel = fit_panel(nir_panel) + + panel_rects["fuse"] = (0, 0, pw, ph) + panel_rects["rgb"] = (pw, 0, pw * 2, ph) + panel_rects["cam0"] = (0, ph, pw, ph * 2) + panel_rects["cam1"] = (pw, ph, pw * 2, ph * 2) + + top = np.hstack([fuse_panel, rgb_panel]) + bottom = np.hstack([re_panel, nir_panel]) + board = np.vstack([top, bottom]) + + help_lines = [ + "M=manual_affine | H=homography | clique pares correspondentes | >=4 pares | SPACE=salva | C=limpa pts | Z=zera sel | X=zera tudo", + "A/W/S/D movem | J/L rotacionam | O/P muda passo angular | I/U remove ultimo ponto | ENTER calcula H | TAB alterna camera | Q/Esc sai", + ] + overlay_hud(board, help_lines, x=16, y=board.shape[0] - 44, font_scale=0.55, line_step=20) + + if last_msg and (time.time() - last_msg_t) < 2.5: + cv2.putText(board, last_msg, (16, board.shape[0] - 72), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA) + + if args.preview_scale != 1.0: + board = cv2.resize( + board, + (int(board.shape[1] * args.preview_scale), int(board.shape[0] * args.preview_scale)), + interpolation=cv2.INTER_NEAREST, + ) + + if calibration_mode == "homography": + color_spec = (0, 255, 255) + color_rgb = (0, 255, 0) + + for idx, pt in enumerate(selected_points_spec[selected_cam]): + rect = panel_rects[selected_cam] + if rect is not None: + x0, y0, _, _ = rect + px = int(x0 + pt[0]) + py = int(y0 + pt[1]) + cv2.circle(board, (px, py), 5, color_spec, -1) + cv2.putText(board, str(idx + 1), (px + 6, py - 6), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, color_spec, 1, cv2.LINE_AA) + + for idx, pt in enumerate(selected_points_rgb[selected_cam]): + rect = panel_rects["rgb"] + if rect is not None: + x0, y0, _, _ = rect + px = int(x0 + pt[0]) + py = int(y0 + pt[1]) + cv2.circle(board, (px, py), 5, color_rgb, -1) + cv2.putText(board, str(idx + 1), (px + 6, py - 6), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, color_rgb, 1, cv2.LINE_AA) + + cv2.imshow(window_name, board) + else: + blank = np.zeros((720, 1280, 3), dtype=np.uint8) + overlay_hud(blank, ["Aguardando frames do módulo..."], x=40, y=80, font_scale=1.0, line_step=34) + cv2.imshow(window_name, blank) + + k = cv2.waitKey(1) & 0xFF + if k in (ord("q"), ord("Q"), 27): + break + elif k in (ord("m"), ord("M")): + calibration_mode = "manual_affine" + offsets_data["alignment_mode"] = calibration_mode + last_msg = "Modo: manual_affine" + last_msg_t = time.time() + elif k in (ord("h"), ord("H")): + calibration_mode = "homography" + offsets_data["alignment_mode"] = calibration_mode + last_msg = "Modo: homography" + last_msg_t = time.time() + elif k in (ord("c"), ord("C")): + selected_points_spec[selected_cam] = [] + selected_points_rgb[selected_cam] = [] + last_msg = f"Pontos limpos: {selected_cam}" + last_msg_t = time.time() + elif k == 13: # ENTER + spec_pts = selected_points_spec[selected_cam] + rgb_pts = selected_points_rgb[selected_cam] + + if len(spec_pts) >= 4 and len(rgb_pts) >= 4 and len(spec_pts) == len(rgb_pts): + src = np.array(spec_pts, dtype=np.float32) + dst = np.array(rgb_pts, dtype=np.float32) + + H, status = cv2.findHomography(src, dst, method=cv2.RANSAC) + if H is not None: + offsets_data.setdefault("homographies", {}) + offsets_data["homographies"][f"{selected_cam}_to_cam2"] = H.tolist() + inliers = int(status.sum()) if status is not None else len(spec_pts) + last_msg = f"H calculada para {selected_cam} | pts={len(spec_pts)} | inliers={inliers}" + else: + last_msg = f"Falha ao calcular H para {selected_cam}" else: - last_msg = f"Falha ao calcular H para {selected_cam}" - else: - last_msg = f"{selected_cam}: precisa de >=4 pares e mesmo numero de pontos" + last_msg = f"{selected_cam}: precisa de >=4 pares e mesmo numero de pontos" - last_msg_t = time.time() - elif k in (ord("r"), ord("R")): - if "cam0" in decoded_last: - selected_cam = "cam0" - last_msg = "Selecionada: cam0 / RE" - else: - last_msg = "cam0 / RE não disponível neste frame" - last_msg_t = time.time() - elif k in (ord("n"), ord("N")): - if "cam1" in decoded_last: - selected_cam = "cam1" - last_msg = "Selecionada: cam1 / NIR" - else: - last_msg = "cam1 / NIR não disponível neste frame" - last_msg_t = time.time() - elif k == 9: # TAB - choices = [cid for cid in ("cam0", "cam1") if cid in decoded_last] - if len(choices) >= 2: - selected_cam = choices[1] if selected_cam == choices[0] else choices[0] - last_msg = f"Selecionada: {selected_cam}" last_msg_t = time.time() - elif k in (ord("z"), ord("Z")): - offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) - offsets[selected_cam]["dx"] = 0 - offsets[selected_cam]["dy"] = 0 - offsets[selected_cam]["theta_deg"] = 0.0 - last_msg = f"Offset zerado: {selected_cam}" - last_msg_t = time.time() - elif k in (ord("x"), ord("X")): - offsets["cam0"] = {"dx": 0, "dy": 0, "theta_deg": 0.0} - offsets["cam1"] = {"dx": 0, "dy": 0, "theta_deg": 0.0} - last_msg = "Todos offsets zerados" - last_msg_t = time.time() - elif k == 32: - # Se uma câmera não apareceu, salva zerada como pedido - if "cam0" not in decoded_last: + elif k == ord("2"): + if "cam0" in decoded_last: + selected_cam = "cam0" + last_msg = "Selecionada: cam0 / RE" + else: + last_msg = "cam0 / RE nao disponivel neste frame" + last_msg_t = time.time() + elif k == ord("3"): + if "cam1" in decoded_last: + selected_cam = "cam1" + last_msg = "Selecionada: cam1 / NIR" + else: + last_msg = "cam1 / NIR nao disponivel neste frame" + last_msg_t = time.time() + elif k == 9: # TAB + choices = [cid for cid in ("cam0", "cam1") if cid in decoded_last] + if len(choices) >= 2: + selected_cam = choices[1] if selected_cam == choices[0] else choices[0] + last_msg = f"Selecionada: {selected_cam}" + last_msg_t = time.time() + elif k in (ord("z"), ord("Z")): + offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) + offsets[selected_cam]["dx"] = 0 + offsets[selected_cam]["dy"] = 0 + offsets[selected_cam]["theta_deg"] = 0.0 + last_msg = f"Offset zerado: {selected_cam}" + last_msg_t = time.time() + elif k in (ord("x"), ord("X")): offsets["cam0"] = {"dx": 0, "dy": 0, "theta_deg": 0.0} - if "cam1" not in decoded_last: offsets["cam1"] = {"dx": 0, "dy": 0, "theta_deg": 0.0} + last_msg = "Todos offsets zerados" + last_msg_t = time.time() + elif k == 32: + # Se uma câmera não apareceu, salva zerada como pedido + if "cam0" not in decoded_last: + offsets["cam0"] = {"dx": 0, "dy": 0, "theta_deg": 0.0} + if "cam1" not in decoded_last: + offsets["cam1"] = {"dx": 0, "dy": 0, "theta_deg": 0.0} - offsets_data["manual_offsets"] = offsets - save_offsets_json(args.out_json, offsets_data) - last_msg = f"Offsets salvos em: {args.out_json}" - last_msg_t = time.time() - elif k in (ord("+"), ord("=")): - args.step = min(args.step + 1, 50) - last_msg = f"Step -> {args.step}px" - last_msg_t = time.time() - elif k in (ord("-"), ord("_")): - args.step = max(args.step - 1, 1) - last_msg = f"Step -> {args.step}px" - last_msg_t = time.time() - elif k in (ord("a"), ord("A")): - if selected_cam in decoded_last: - offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) - offsets[selected_cam]["dx"] -= args.step - elif k in (ord("d"), ord("D")): - if selected_cam in decoded_last: - offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) - offsets[selected_cam]["dx"] += args.step - elif k in (ord("w"), ord("W")): - if selected_cam in decoded_last: - offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) - offsets[selected_cam]["dy"] -= args.step - elif k in (ord("s"), ord("S")): - if selected_cam in decoded_last: - offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) - offsets[selected_cam]["dy"] += args.step - elif k in (ord("j"), ord("J")): - if selected_cam in decoded_last: - offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) - offsets[selected_cam]["theta_deg"] -= args.angle_step - elif k in (ord("l"), ord("L")): - if selected_cam in decoded_last: - offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) - offsets[selected_cam]["theta_deg"] += args.angle_step - elif k in (ord("o"), ord("O")): - args.angle_step = max(args.angle_step - 0.05, 0.01) - last_msg = f"Angle step -> {args.angle_step:.2f}°" - last_msg_t = time.time() - elif k in (ord("p"), ord("P")): - args.angle_step = min(args.angle_step + 0.05, 5.0) - last_msg = f"Angle step -> {args.angle_step:.2f}°" - last_msg_t = time.time() - elif k in (ord("u"), ord("U")): - if selected_points_spec[selected_cam]: - selected_points_spec[selected_cam].pop() - last_msg = f"Removido ultimo ponto SPEC de {selected_cam}" + offsets_data["manual_offsets"] = offsets + save_offsets_json(args.out_json, offsets_data) + last_msg = f"Offsets salvos em: {args.out_json}" last_msg_t = time.time() - elif k in (ord("i"), ord("I")): - if selected_points_rgb[selected_cam]: - selected_points_rgb[selected_cam].pop() - last_msg = f"Removido ultimo ponto RGB de {selected_cam}" + elif k in (ord("+"), ord("=")): + args.step = min(args.step + 1, 50) + last_msg = f"Step -> {args.step}px" last_msg_t = time.time() - dt_loop = time.time() - t0 - if dt_loop < 0.001: - time.sleep(0.001) + elif k in (ord("-"), ord("_")): + args.step = max(args.step - 1, 1) + last_msg = f"Step -> {args.step}px" + last_msg_t = time.time() + elif k in (ord("a"), ord("A")): + if selected_cam in decoded_last: + offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) + offsets[selected_cam]["dx"] -= args.step + elif k in (ord("d"), ord("D")): + if selected_cam in decoded_last: + offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) + offsets[selected_cam]["dx"] += args.step + elif k in (ord("w"), ord("W")): + if selected_cam in decoded_last: + offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) + offsets[selected_cam]["dy"] -= args.step + elif k in (ord("s"), ord("S")): + if selected_cam in decoded_last: + offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) + offsets[selected_cam]["dy"] += args.step + elif k in (ord("j"), ord("J")): + if selected_cam in decoded_last: + offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) + offsets[selected_cam]["theta_deg"] -= args.angle_step + elif k in (ord("l"), ord("L")): + if selected_cam in decoded_last: + offsets.setdefault(selected_cam, {"dx": 0, "dy": 0, "theta_deg": 0.0}) + offsets[selected_cam]["theta_deg"] += args.angle_step + elif k in (ord("o"), ord("O")): + args.angle_step = max(args.angle_step - 0.05, 0.01) + last_msg = f"Angle step -> {args.angle_step:.2f}°" + last_msg_t = time.time() + elif k in (ord("p"), ord("P")): + args.angle_step = min(args.angle_step + 0.05, 5.0) + last_msg = f"Angle step -> {args.angle_step:.2f}°" + last_msg_t = time.time() + elif k in (ord("u"), ord("U")): + if selected_points_spec[selected_cam]: + selected_points_spec[selected_cam].pop() + last_msg = f"Removido ultimo ponto SPEC de {selected_cam}" + last_msg_t = time.time() + elif k in (ord("i"), ord("I")): + if selected_points_rgb[selected_cam]: + selected_points_rgb[selected_cam].pop() + last_msg = f"Removido ultimo ponto RGB de {selected_cam}" + 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 - try: - svc.disconnect() - except Exception: - pass - try: - receiver.stop() - except Exception: - pass cv2.destroyAllWindows() print("Fim da calibração manual.") diff --git a/Python/raspi/sensor_calibration_tool.py b/Python/raspi/sensor_calibration_tool.py index 018356f04..1939c817e 100644 --- a/Python/raspi/sensor_calibration_tool.py +++ b/Python/raspi/sensor_calibration_tool.py @@ -7,15 +7,7 @@ from datetime import datetime import cv2 import numpy as np -from cam_3.multispectral_service import MultiSpectralService -from cam_3.stream_receiver import StreamReceiver -from cam_3.pi.raw_processor_core import RawProcessorCore - - -STREAM_PORT = 6001 -PI_HOST = "192.168.105.6" -PC_HOST = "192.168.105.5" - +from cam_3.multispectral_client import MultiSpectralClient # ============================================================ # Helpers @@ -30,17 +22,54 @@ def ensure_dir(path: str): def overlay_hud( - img_bgr: np.ndarray, - lines: list[str], - x: int = 12, - y: int = 22, - font_scale: float = 0.55, - line_step: int = 22, + img_bgr, + lines, + x=12, + y=22, + area_h=None, + max_font_scale=None, + min_font_scale=None, + max_line_step=None, + min_line_step=None, + bottom_margin=12, ): + h, w = img_bgr.shape[:2] + + if area_h is None: + area_h = h - y - bottom_margin + + scale = max(1.0, min(1.45, area_h / 480.0)) + + if max_font_scale is None: + max_font_scale = 0.62 * scale + if min_font_scale is None: + min_font_scale = 0.34 * scale + if max_line_step is None: + max_line_step = int(22 * scale) + if min_line_step is None: + min_line_step = int(13 * scale) + + available_h = max(1, area_h - bottom_margin) + n = max(1, len(lines)) + + font_scale = max_font_scale + line_step = max_line_step + + needed_h = n * line_step + if needed_h > available_h: + shrink = available_h / float(needed_h) + font_scale = max(min_font_scale, max_font_scale * shrink) + line_step = max(min_line_step, int(max_line_step * shrink)) + yy = y for s in lines: - cv2.putText(img_bgr, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 0, 0), 3, cv2.LINE_AA) - cv2.putText(img_bgr, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), 1, cv2.LINE_AA) + if yy > y + area_h - bottom_margin: + break + + cv2.putText(img_bgr, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, + font_scale, (0, 0, 0), 3, cv2.LINE_AA) + cv2.putText(img_bgr, s, (x, yy), cv2.FONT_HERSHEY_SIMPLEX, + font_scale, (255, 255, 255), 1, cv2.LINE_AA) yy += line_step @@ -87,7 +116,7 @@ def validate_module_ready(status: dict, frame_type: str, raw_policy: str, captur def build_empty_panel(shape_hw: tuple[int, int], title: str) -> np.ndarray: h, w = shape_hw img = np.zeros((h, w, 3), dtype=np.uint8) - overlay_hud(img, [title, "sem frame disponivel"], x=18, y=40, font_scale=0.8, line_step=34) + overlay_hud(img, [title, "sem frame disponivel"]) return img @@ -267,70 +296,6 @@ def draw_current_polygon(panel_bgr: np.ndarray, points: list): cv2.polylines(panel_bgr, [pts], isClosed=False, color=(0, 255, 255), thickness=1) -# ============================================================ -# Decodificação do stream RAW_BRUTO -# ============================================================ - -class StreamDecoder: - def __init__(self, sensor_width: int, sensor_height: int, bayer_pattern: str = "GBRG"): - self.sensor_width = sensor_width - self.sensor_height = sensor_height - self.bayer_pattern = bayer_pattern - - def decode_stream_cameras(self, frame, meta): - if not isinstance(frame, dict): - raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi") - - camera_frames = meta.get("camera_frames", {}) or {} - decoded = {} - - if "cam2" in frame: - rgb_bgr = frame["cam2"] - if rgb_bgr.ndim != 3 or rgb_bgr.shape[2] != 3: - raise RuntimeError(f"cam2 RGB inválida: shape={rgb_bgr.shape}") - - rgb = rgb_bgr[:, :, ::-1].astype(np.float32) / 255.0 - decoded["cam2"] = { - "name": "RGB", - "image": rgb, - "meta": camera_frames.get("cam2", {}), - } - - for cam_id, spec_name in (("cam0", "RE"), ("cam1", "NIR")): - if cam_id not in frame: - continue - - packed = frame[cam_id] - if packed.ndim == 3 and packed.shape[2] == 1: - packed = packed[:, :, 0] - - cam_meta = camera_frames.get(cam_id, {}) - packed_width = int(cam_meta.get("width", packed.shape[1])) - height = int(cam_meta.get("height", packed.shape[0])) - bayer = cam_meta.get("bayer_pattern", self.bayer_pattern) - bit_depth = int(cam_meta.get("bit_depth", 10)) - - real_width = int((packed_width * 8) / 10) if bit_depth == 10 else packed_width - - rp = RawProcessorCore( - sensor_width=real_width, - sensor_height=height, - bayer_pattern=bayer, - ) - - raw16 = rp.unpack_raw10_packed(packed) - max_val = float((1 << bit_depth) - 1) - single = np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0) - - decoded[cam_id] = { - "name": spec_name, - "image": single, - "meta": cam_meta, - } - - return decoded - - # ============================================================ # MOCK # ============================================================ @@ -422,7 +387,7 @@ def save_offline_sample( return png_path, json_path -def load_offline_sample_decoded(json_path: str, decoder: StreamDecoder): +def load_offline_sample_decoded(json_path: str, cam: MultiSpectralClient): if not os.path.isfile(json_path): raise FileNotFoundError(f"Sample offline não encontrado: {json_path}") @@ -459,7 +424,7 @@ def load_offline_sample_decoded(json_path: str, decoder: StreamDecoder): stream_meta.setdefault("camera_frames", meta.get("camera_frames", {})) stream_meta.setdefault("frame_type", "RAW_BRUTO") - decoded = decoder.decode_stream_cameras(frame, stream_meta) + decoded = cam.core.decode_stream_cameras(frame, stream_meta) preview_path = meta.get("saved_preview_path") preview_bgr = None @@ -610,7 +575,7 @@ def analyze_spectral_guidance(selected_cam: str, img01: np.ndarray, rois_for_cam new_ctrl = json.loads(json.dumps(ctrl)) action = "keep" status = "ok" - reason = "Parâmetros parecem aceitáveis." + reason = "Parametros parecem aceitaveis." if veg_mean is None: return { @@ -646,14 +611,14 @@ def analyze_spectral_guidance(selected_cam: str, img01: np.ndarray, rois_for_cam new_ctrl["exposure_time_us"] = int(max(exp - exp_step, MIN_EXP_US)) action = "decrease_exposure" status = "adjust" - reason = f"Vegetação saturando ({veg_sat:.2f}%). Reduzir exposição." + reason = f"Vegetacao saturando ({veg_sat:.2f}%). Reduzir exposicao." elif gain > MIN_GAIN: new_ctrl["analogue_gain"] = float(max(gain / (1.0 + gain_step), MIN_GAIN)) action = "decrease_gain" status = "adjust" reason = ( - f"Vegetação saturando ({veg_sat:.2f}%), mas exposição já está no mínimo. " + f"Vegetacao saturando ({veg_sat:.2f}%), mas exposicao ja esta no minimo. " "Reduzir ganho." ) @@ -661,8 +626,8 @@ def analyze_spectral_guidance(selected_cam: str, img01: np.ndarray, rois_for_cam action = "keep" status = "limit" reason = ( - f"Vegetação saturando ({veg_sat:.2f}%), mas exposição e ganho já estão no mínimo. " - "Não há ajuste possível por software." + f"Vegetacao saturando ({veg_sat:.2f}%), mas exposicao e ganho ja estao no minimo. " + "Nao ha ajuste possivel por software." ) # 2) Vegetação pouco iluminada @@ -670,14 +635,14 @@ def analyze_spectral_guidance(selected_cam: str, img01: np.ndarray, rois_for_cam new_ctrl["exposure_time_us"] = int(min(exp + exp_step, 200000)) action = "increase_exposure" status = "adjust" - reason = f"p95 da vegetação baixo ({veg_p95:.3f}). Aumentar exposição." + reason = f"p95 da vegetacao baixo ({veg_p95:.3f}). Aumentar exposicao." # 3) Vegetação muito perto do teto elif veg_p95 is not None and veg_p95 > 0.96: new_ctrl["exposure_time_us"] = int(max(exp - exp_step, 100)) action = "decrease_exposure" status = "adjust" - reason = f"p95 da vegetação alto ({veg_p95:.3f}). Reduzir exposição." + reason = f"p95 da vegetacao alto ({veg_p95:.3f}). Reduzir exposicao." # 4) Separação ruim elif separation is not None and separation < 0.25: @@ -685,12 +650,12 @@ def analyze_spectral_guidance(selected_cam: str, img01: np.ndarray, rois_for_cam new_ctrl["exposure_time_us"] = int(min(exp + exp_step, 200000)) action = "increase_exposure" status = "adjust" - reason = f"Separação baixa ({separation:.3f}) e há margem no p95. Aumentar exposição." + reason = f"Separacao baixa ({separation:.3f}) e ha margem no p95. Aumentar exposicao." else: new_ctrl["analogue_gain"] = float(min(gain * (1.0 + gain_step), 32.0)) action = "increase_gain" status = "adjust" - reason = f"Separação baixa ({separation:.3f}) sem muita margem de exposição. Aumentar ganho levemente." + reason = f"Separacao baixa ({separation:.3f}) sem muita margem de exposicao. Aumentar ganho levemente." return { "status": status, @@ -785,9 +750,9 @@ def main(): description="Ferramenta de calibração dos sensores RGB/RE/NIR com controle manual e ROIs em tempo real.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - parser.add_argument("--pi_host", default=PI_HOST) - parser.add_argument("--pc_host", default=PC_HOST) - parser.add_argument("--stream_port", type=int, default=STREAM_PORT) + parser.add_argument("--pi_host", default="192.168.105.6") + parser.add_argument("--pc_host", default="192.168.105.5") + parser.add_argument("--stream_port", type=int, default=6001) parser.add_argument("--server_port", type=int, default=5000) parser.add_argument("--fps", type=int, default=20) parser.add_argument("--width", type=int, default=640) @@ -809,15 +774,27 @@ def main(): parser.add_argument("--offline_save_dir", default="calibration/offline_samples", help="Pasta para salvar frames brutos offline") args = parser.parse_args() + cam = MultiSpectralClient( + pi_host=args.pi_host, + pc_host=args.pc_host, + server_port=args.server_port, + stream_port=args.stream_port, + width=args.width, + height=args.height, + bayer=args.bayer, + fps=args.fps, + frame_type="RAW_BRUTO", + output_dtype="uint8", + capture_mode=args.capture_mode, + raw_policy=args.raw_policy, + module_calibration_json=None, + ) + offline_mode = bool(args.offline_sample_json) live_mode = not args.mock and not offline_mode effective_capture_mode = args.capture_mode - receiver = StreamReceiver(host="0.0.0.0", port=args.stream_port) - svc = MultiSpectralService(host=args.pi_host, port=args.server_port, timeout=10) - decoder = StreamDecoder(sensor_width=args.width, sensor_height=args.height, bayer_pattern=args.bayer) - data_payload = load_payload(args.load_json, args, effective_capture_mode) selected_cam = "cam2" @@ -837,6 +814,10 @@ def main(): last_raw_frame = None last_preview_bgr = None + roi_name_input_active = False + roi_name_buffer = "" + roi_name_points_pending = [] + guidance_log = data_payload.get("calibration_guidance_log", []) last_guidance = guidance_log[-1]["result"] if guidance_log else None @@ -912,31 +893,28 @@ def main(): decoded_last = build_mock_decoded(args) if offline_mode: - decoded_last, last_meta_stream, last_raw_frame, last_preview_bgr = load_offline_sample_decoded( - args.offline_sample_json, - decoder, - ) + decoded_last, last_meta_stream, last_raw_frame, last_preview_bgr = load_offline_sample_decoded(args.offline_sample_json, cam) def apply_controls_to_selected_cam(): - nonlocal last_msg, last_msg_t + nonlocal cam, last_msg, last_msg_t ctrl = camera_controls[selected_cam] try: - resp = svc.set_ae_enable(selected_cam, bool(ctrl["ae_enable"])) + resp = cam.svc.set_ae_enable(selected_cam, bool(ctrl["ae_enable"])) ctrl["ae_enable"] = bool(resp.get("ae_enable", ctrl["ae_enable"])) if selected_cam == "cam2": - resp = svc.set_awb_enable(selected_cam, bool(ctrl["awb_enable"])) + resp = cam.svc.set_awb_enable(selected_cam, bool(ctrl["awb_enable"])) ctrl["awb_enable"] = bool(resp.get("awb_enable", ctrl["awb_enable"])) if not ctrl["ae_enable"]: if ctrl["exposure_time_us"] is not None: - resp = svc.set_exposure_time(selected_cam, int(ctrl["exposure_time_us"])) + resp = cam.svc.set_exposure_time(selected_cam, int(ctrl["exposure_time_us"])) exp_val = resp.get("exposure_time_us", ctrl["exposure_time_us"]) ctrl["exposure_time_us"] = int(exp_val) if exp_val is not None else None if ctrl["analogue_gain"] is not None: - resp = svc.set_analogue_gain(selected_cam, float(ctrl["analogue_gain"])) + resp = cam.svc.set_analogue_gain(selected_cam, float(ctrl["analogue_gain"])) gain_val = resp.get("analogue_gain", ctrl["analogue_gain"]) ctrl["analogue_gain"] = float(gain_val) if gain_val is not None else None @@ -981,80 +959,51 @@ def main(): } return snap + def sync_camera_controls_from_pi(): + nonlocal cam, camera_controls + + if not live_mode: + return + + for cam_id in camera_controls.keys(): + try: + initial_ctrl = cam.svc.get_camera_controls(cam_id) + + camera_controls[cam_id]["ae_enable"] = bool( + initial_ctrl.get("ae_enable", camera_controls[cam_id]["ae_enable"]) + ) + + camera_controls[cam_id]["awb_enable"] = bool( + initial_ctrl.get("awb_enable", camera_controls[cam_id]["awb_enable"]) + ) + + exp_val = initial_ctrl.get("exposure_time_us", camera_controls[cam_id]["exposure_time_us"]) + camera_controls[cam_id]["exposure_time_us"] = int(exp_val) if exp_val is not None else None + + gain_val = initial_ctrl.get("analogue_gain", camera_controls[cam_id]["analogue_gain"]) + camera_controls[cam_id]["analogue_gain"] = float(gain_val) if gain_val is not None else None + + camera_controls[cam_id]["colour_gains"] = initial_ctrl.get( + "colour_gains", + camera_controls[cam_id]["colour_gains"] + ) + + except Exception as e: + print(f"[WARN] Falha ao ler controles iniciais de {cam_id}: {e}") + try: if live_mode: - receiver.start() - time.sleep(0.5) - - print(f"[INFO] Verificando conexão com o módulo em {args.pi_host}:{args.server_port}...") - if not svc.check_connection(2): - raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.") - print("[OK] Módulo conectado e respondendo.") - - svc.connect() - - print("SET CAM0 RES:", svc.set_camera_resolution(0, args.width, args.height)) - print("SET CAM1 RES:", svc.set_camera_resolution(1, args.width, args.height)) - print("SET CAM2 RES:", svc.set_camera_resolution(2, args.width, args.height)) - 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("RAW_BRUTO")) - print("SET OUTPUT DTYPE:", svc.set_output_dtype("float32")) - - begin_resp = svc.begin(frame_type="RAW_BRUTO", output_dtype="float32", 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, "RAW_BRUTO", args.raw_policy, effective_capture_mode) - print("START STREAM:", svc.start_stream(args.pc_host, args.stream_port, fps=args.fps)) + cam.start(print_debug=True) + sync_camera_controls_from_pi() else: last_msg = "MODO OFFLINE ativo" if offline_mode else "MODO MOCK ativo" last_msg_t = time.time() - if live_mode: - try: - for cam_id, _ in camera_controls.items(): - initial_ctrl = svc.get_camera_controls(cam_id) - - camera_controls[cam_id]["ae_enable"] = bool( - initial_ctrl.get("ae_enable", camera_controls[cam_id]["ae_enable"]) - ) - camera_controls[cam_id]["awb_enable"] = bool( - initial_ctrl.get("awb_enable", camera_controls[cam_id]["awb_enable"]) - ) - - exp_val = initial_ctrl.get("exposure_time_us", camera_controls[cam_id]["exposure_time_us"]) - if exp_val is not None: - exp_val = int(exp_val) - camera_controls[cam_id]["exposure_time_us"] = exp_val - - gain_val = initial_ctrl.get("analogue_gain", camera_controls[cam_id]["analogue_gain"]) - if gain_val is not None: - gain_val = float(gain_val) - camera_controls[cam_id]["analogue_gain"] = gain_val - - camera_controls[cam_id]["colour_gains"] = initial_ctrl.get( - "colour_gains", - camera_controls[cam_id]["colour_gains"] - ) - except Exception as e: - print(f"[WARN] Falha ao ler controles iniciais: {e}") - while True: t0 = time.time() if live_mode: - meta = receiver.last_meta - frame = receiver.last_frame + frame, meta = cam.get_next_frame(timeout=2.0) if meta is not None and frame is not None and meta.get("frame_id") != last_frame_id: last_frame_id = meta["frame_id"] @@ -1062,7 +1011,7 @@ def main(): if not isinstance(frame, dict): raise RuntimeError("Este calibrador espera RAW_BRUTO multi-payload como dict de câmeras.") - decoded_last = decoder.decode_stream_cameras(frame, meta) + decoded_last = cam.core.decode_stream_cameras(frame, meta) last_meta_stream = dict(meta) last_raw_frame = {cam_id: arr.copy() for cam_id, arr in frame.items()} @@ -1172,6 +1121,14 @@ def main(): if sep is not None: lines.append(f"sep_veg_solo={sep:.3f}") + if roi_name_input_active: + lines.extend([ + "-", + "NOME DA ROI:", + f"> {roi_name_buffer}_", + "ENTER confirma | ESC cancela | BACKSPACE apaga", + ]) + lines.append("-") lines.append(f"ROIs: {len(rois[selected_cam])}") for idx, roi in enumerate(rois[selected_cam][:6]): @@ -1189,8 +1146,14 @@ def main(): "F salva frame bruto | SPACE salva PARAMS | S snapshot | Q sai", ]) - x0, y0, _, _ = panel_rects["data"] - overlay_hud(board, lines, x=x0 + 12, y=y0 + 22, font_scale=0.52, line_step=20) + x0, y0, x1, y1 = panel_rects["data"] + overlay_hud( + board, + lines, + x=x0 + 12, + y=y0 + 22, + area_h=(y1 - y0) - 22, + ) if last_msg and (time.time() - last_msg_t) < 2.5: cv2.putText(board, last_msg, (12, board.shape[0] - 16), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 2, cv2.LINE_AA) @@ -1205,10 +1168,49 @@ def main(): cv2.imshow(window_name, board) else: blank = np.zeros((720, 1280, 3), dtype=np.uint8) - overlay_hud(blank, ["Aguardando frames do módulo..."], x=40, y=80, font_scale=1.0, line_step=34) + overlay_hud(blank, ["Aguardando frames do módulo..."], x=40, y=80) cv2.imshow(window_name, blank) k = cv2.waitKey(1) & 0xFF + + if roi_name_input_active: + if k in (13, 10): # ENTER + name = roi_name_buffer.strip() + if not name: + name = f"roi_{len(rois[selected_cam]) + 1}" + + roi = { + "name": name, + "type": "polygon", + "points": list(roi_name_points_pending), + "color": color_for_index(len(rois[selected_cam])), + } + + rois[selected_cam].append(roi) + + current_polygon_points = [] + roi_name_points_pending = [] + roi_name_buffer = "" + roi_name_input_active = False + + last_msg = f"ROI criada em {selected_cam}: {name}" + last_msg_t = time.time() + + elif k in (27,): # ESC + roi_name_input_active = False + roi_name_buffer = "" + roi_name_points_pending = [] + last_msg = "Criacao de ROI cancelada" + last_msg_t = time.time() + + elif k in (8, 127): # BACKSPACE + roi_name_buffer = roi_name_buffer[:-1] + + elif 32 <= k <= 126: + roi_name_buffer += chr(k) + + continue + if k in (ord("q"), ord("Q"), 27): break elif k == ord("1"): @@ -1386,21 +1388,10 @@ def main(): last_msg = "ROI poligonal precisa de pelo menos 3 pontos" last_msg_t = time.time() else: - name = input(f"Nome da ROI para {selected_cam}: ").strip() - if not name: - name = f"roi_{len(rois[selected_cam]) + 1}" - - roi = { - "name": name, - "type": "polygon", - "points": list(current_polygon_points), - "color": color_for_index(len(rois[selected_cam])), - } - - rois[selected_cam].append(roi) - current_polygon_points = [] - - last_msg = f"ROI criada em {selected_cam}: {name}" + roi_name_input_active = True + roi_name_buffer = "" + roi_name_points_pending = list(current_polygon_points) + last_msg = "Digite o nome da ROI na tela" last_msg_t = time.time() dt_loop = time.time() - t0 @@ -1409,22 +1400,7 @@ def main(): finally: if live_mode: - try: - print("STOP STREAM:", svc.stop_stream()) - except Exception: - pass - try: - print("STOP:", svc.stop()) - except Exception: - pass - try: - svc.disconnect() - except Exception: - pass - try: - receiver.stop() - except Exception: - pass + cam.stop() cv2.destroyAllWindows() print("Fim da calibração dos sensores.") diff --git a/Python/raspi/test_service.py b/Python/raspi/test_service.py index a324aebfa..3f1a2173d 100644 --- a/Python/raspi/test_service.py +++ b/Python/raspi/test_service.py @@ -1,29 +1,77 @@ import time +import cv2 +import numpy as np from cam_3.multispectral_service import MultiSpectralService -svc = MultiSpectralService(host="192.168.105.6", port=5000) -svc.connect() +svc = MultiSpectralService(host="192.168.105.6", port=5000, timeout=10) -print("PING:", svc.ping()) -print("STATUS:", svc.get_status()) -print("SET FPS:", svc.set_fps(15)) -print("SET JPG:", svc.set_jpeg_quality(85)) -print("SET RES:", svc.set_resolution(1280, 720)) -print("BEGIN:", svc.begin()) -jpg = None -for i in range(1, 6): # Começa em 1 e vai até 5 - t0 = time.time() - jpg = svc.capture_jpg_base64() - status = "OK" if jpg is not None else "Falha" - tempo = time.time() - t0 - print(f"CAPTURE {i} JPG: {status}, Tempo: {tempo:.4f}") -print("STOP:", svc.stop()) -print("CONFIG:", svc.get_config()) -print("STATUS FINAL:", svc.get_status()) +try: + svc.connect() -svc.disconnect() + print("PING:", svc.ping()) + print("STATUS:", svc.get_status()) -if jpg is not None: - with open("capture.jpg", "wb") as f: - f.write(jpg) + print("SET FPS:", svc.set_fps(15)) + print("SET RES:", svc.set_resolution(640, 480)) + print("SET CAPTURE MODE:", svc.set_capture_mode("AUTO")) + print("SET FRAME TYPE:", svc.set_frame_type("RAW_BRUTO")) + print("SET OUTPUT DTYPE:", svc.set_output_dtype("uint8")) + + print("BEGIN:", svc.begin( + frame_type="RAW_BRUTO", + output_dtype="uint8", + capture_mode="AUTO", + )) + + last_frame = None + last_meta = None + + for i in range(1, 6): + t0 = time.time() + frame, meta = svc.capture_frame() + tempo = time.time() - t0 + + last_frame = frame + last_meta = meta + + print( + f"CAPTURE {i}: OK, Tempo={tempo:.4f}s, " + f"type={type(frame)}, frame_type={meta.get('frame_type')}, " + f"sources={meta.get('payload_sources')}" + ) + + print("CONFIG:", svc.get_config()) + print("STATUS FINAL:", svc.get_status()) + +finally: + try: + print("STOP:", svc.stop()) + except Exception: + pass + + svc.disconnect() + + +# Salva uma imagem simples de preview +if last_frame is not None: + if isinstance(last_frame, dict) and "cam2" in last_frame: + # cam2 vem BGR do OpenCV + cv2.imwrite("capture_cam2.jpg", last_frame["cam2"]) + print("[OK] Salvo: capture_cam2.jpg") + + elif isinstance(last_frame, np.ndarray): + if last_frame.ndim == 3: + # Pode ser CHW ou HWC + img = last_frame + if img.shape[0] in (3, 4, 5): + img = np.transpose(img[:3], (1, 2, 0)) + + if img.dtype != np.uint8: + img = np.clip(img * 255.0, 0, 255).astype(np.uint8) + + cv2.imwrite("calibration/capture.jpg", img) + print("[OK] Salvo: calibration/capture.jpg") + else: + cv2.imwrite("calibration/capture_gray.jpg", last_frame) + print("[OK] Salvo: calibration/capture_gray.jpg") \ No newline at end of file