reestruturado oak-ffc-3

This commit is contained in:
Diego Freitas 2026-05-05 10:15:09 -03:00
parent 2557bdba9f
commit 39be40aec2
73 changed files with 7281 additions and 724 deletions

View File

@ -0,0 +1,477 @@
import os
import time
import json
import argparse
from datetime import datetime
import cv2
import numpy as np
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
RAW_SIZE = config.get("raw_size") # [W, H]
MODULE_PARAMS = config.get("module_params_json")
# =========================
# 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
# =========================
# 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("--fps", type=int, default=20, help="FPS desejado.")
parser.add_argument("--width", type=int, default=RAW_SIZE[0], help="Largura óptica da câmera.")
parser.add_argument("--height", type=int, default=RAW_SIZE[1], 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.")
parser.add_argument("--module_calibration_json", default=MODULE_PARAMS, help="JSON salvo pelo calibrador de sensores com parâmetros fixos por câmera.")
parser.add_argument("--radiometric_ae", action="store_true", help="Liga controle automatico de exposicao radiometrico")
args = parser.parse_args()
effective_capture_mode = args.capture_mode
raw_w = args.width
raw_h = args.height
session_dir = os.path.join(
args.out_root,
"brutas",
f"cana_{args.cana}",
args.horario,
datetime.now().strftime("%Y%m%d"),
)
os.makedirs(session_dir, exist_ok=True)
print("============================================")
print("Coleta de dataset - Módulo Multiespectral")
print(f"Cana : {args.cana}")
print(f"Horário : {args.horario}")
print(f"Saída : {session_dir}")
print(f"Sensor : {raw_w}x{raw_h} | Bayer={args.bayer}")
print(f"FrameType : {args.frame_type}")
print(f"CaptureMode : {args.capture_mode} -> efetivo={effective_capture_mode}")
print(f"RAW policy : {args.raw_policy}")
print("============================================")
auto_save = False
last_auto_t = 0.0
preview_upscale = args.preview_upscale
t_view_fps = time.time()
view_frames = 0
fps_view = 0.0
t_stream_fps = time.time()
last_stream_frame_id = None
stream_frames_accum = 0
fps_stream = 0.0
last_msg = ""
last_msg_t = 0.0
window_name = "Dataset Capture (C/SPACE=save | A=auto-save | M=preview scale | Q=quit)"
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
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:
with MultiSpectralClient(
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,
raw_policy=args.raw_policy,
#module_calibration_json=args.module_calibration_json,
radiometric_enabled=args.radiometric_ae,
) as cam:
while True:
t0 = time.time()
frame, meta, decoded = cam.get_next_decoded(timeout=1.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"]
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 = cam.build_preview_from_raw_payload(frame=frame, meta=meta)
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 = cam.build_preview_from_raw_payload(frame=frame, meta=meta)
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")
rad = getattr(cam, "radiometric_controller", None)
if rad and rad.enabled:
st = rad.state
line_rad = (
f"RAD | "
f"RGB(exp={st['cam2']['exp']}, g={st['cam2']['gain']:.2f}) | "
f"RE(exp={st['cam0']['exp']}, g={st['cam0']['gain']:.2f}) | "
f"NIR(exp={st['cam1']['exp']}, g={st['cam1']['gain']:.2f})"
)
else:
line_rad = "RAD | OFF"
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",
line_rad,
"Keys: C/SPACE=save | A=auto-save | 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,
"applied_camera_controls": cam.applied_camera_controls,
"camera_params_json": args.module_calibration_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,
)
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:
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:
cv2.destroyAllWindows()
print("Fim da captura.")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 KiB

View File

@ -0,0 +1,34 @@
{
"schema": "manual_multispec_offsets_v1",
"saved_at": "2026-05-04 20:50:02",
"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": "manual_affine",
"manual_offsets": {
"cam0": {
"dx": -28,
"dy": 9,
"theta_deg": 0.0
},
"cam1": {
"dx": -4,
"dy": 31,
"theta_deg": 0.0
}
},
"homographies": {
"cam0_to_cam2": null,
"cam1_to_cam2": null
},
"notes": ""
}

View File

@ -0,0 +1,78 @@
{
"schema": "multispec_module_params_v1",
"saved_at": "2026-05-05 08:03:18",
"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": "manual_affine",
"baseline_mm": 75.0,
"reference_camera": "cam2",
"manual_offsets": {
"cam0": {
"dx": -28,
"dy": 9,
"theta_deg": 0.0
},
"cam1": {
"dx": -4,
"dy": 31,
"theta_deg": 0.0
}
},
"homographies": {
"cam0_to_cam2": null,
"cam1_to_cam2": null
},
"crop_valid_common": true,
"resize_after_crop": true,
"target_size": null
},
"radiometric_config": {
"interval_s": 0.5,
"strip_y0_pct": 0.95,
"strip_y1_pct": 1.0,
"patch_x0_pct": 0.35,
"patch_x1_pct": 0.75,
"target_mean": 0.7,
"deadband": 0.03,
"alpha": 0.18,
"exp_min_us": 100,
"exp_max_us": 80000,
"gain_min": 1.0,
"gain_max": 8.0,
"verbose": true,
"exp_apply_threshold_us": 50,
"gain_apply_threshold": 0.02
}
}

View File

@ -0,0 +1,213 @@
{
"schema": "multispec_camera_params_v1",
"saved_at": "2026-05-04 19:29:24",
"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",
"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
]
}
},
"rois": {
"cam2": [
{
"name": "mesa",
"type": "polygon",
"points": [
[
429,
305
],
[
446,
195
],
[
512,
199
],
[
512,
309
]
],
"color": [
0,
255,
255
]
},
{
"name": "teto",
"type": "polygon",
"points": [
[
148,
345
],
[
153,
269
],
[
216,
265
],
[
221,
345
]
],
"color": [
0,
255,
0
]
}
],
"cam0": [
{
"name": "mesa",
"type": "polygon",
"points": [
[
417,
235
],
[
433,
158
],
[
485,
162
],
[
479,
237
]
],
"color": [
0,
255,
255
]
},
{
"name": "tet",
"type": "polygon",
"points": [
[
218,
306
],
[
219,
230
],
[
274,
225
],
[
276,
305
]
],
"color": [
0,
255,
0
]
}
],
"cam1": [
{
"name": "mesa",
"type": "polygon",
"points": [
[
468,
227
],
[
480,
123
],
[
556,
137
],
[
549,
233
]
],
"color": [
0,
255,
255
]
},
{
"name": "teto",
"type": "polygon",
"points": [
[
139,
333
],
[
157,
238
],
[
237,
258
],
[
222,
351
]
],
"color": [
0,
255,
0
]
}
]
},
"snapshots": [],
"notes": "",
"calibration_guidance_log": []
}

View File

@ -0,0 +1,20 @@
{
"camera": "oak-fcc-3",
"modelo": "segformer_b1",
"model_name": "pulv_mit",
"dual_head": false,
"main_class_name": "cana",
"es_classes": "",
"model_to_use": "geral",
"raw_size": [640, 480],
"resolucao": [1024, 800],
"roi_inicio": 0.0,
"roi_tamanho": 1.0,
"shaves": 3,
"channels": 4,
"use_ndvi": false,
"backbone": "nvidia/mit-b1",
"fusion_mode": "stacked",
"stats_source_tag": "stacked_raw4",
"module_params_json": "calibration/module_params.json"
}

View File

@ -0,0 +1,407 @@
import numpy as np
import cv2
import json
import os
from .oak_fcc3_service import OakFcc3Service
from .raw_processor_core import RawProcessorCore
from .raw_processor_preview import RawProcessorPreview
from .radiometric_controller import RadiometricController
class OakFcc3Client:
def __init__(
self,
width=640,
height=400,
bayer="GBRG",
fps=30,
frame_type="RAW_BRUTO",
output_dtype="uint8",
capture_mode="AUTO",
raw_policy="allow_single",
module_calibration_json=None,
radiometric_enabled=False,
sync_mode="best",
sync_tolerance_ms=25.0,
**kwargs,
):
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.module_params = self._load_module_params(module_calibration_json)
self.fusion_config = self.module_params.get("fusion_config", {}) or {}
self.radiometric_enabled = radiometric_enabled
self.svc = OakFcc3Service(
timeout=10,
fps=fps,
width=width,
height=height,
frame_type=frame_type,
output_dtype=output_dtype,
capture_mode=capture_mode,
raw_policy=raw_policy,
sync_mode=sync_mode,
sync_tolerance_ms=sync_tolerance_ms,
**kwargs,
)
self.applied_camera_controls = {}
self.radiometric_controller = None
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,
)
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc, tb):
self.stop()
def _load_module_params(self, path):
if not path or not os.path.isfile(path):
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def apply_module_camera_settings(self):
camera_settings = self.module_params.get("camera_settings", {}) or {}
applied = {}
for cam_id, settings in camera_settings.items():
if not isinstance(settings, dict):
continue
try:
resp = self.svc.apply_camera_controls(cam_id, settings)
applied[cam_id] = resp
except Exception as e:
applied[cam_id] = {
"ok": False,
"error": str(e),
"requested": settings,
}
self.applied_camera_controls = applied
return applied
def enable_radiometric_controller(self):
self.radiometric_controller = RadiometricController(
client=self,
enabled=True,
config_json_path=self.module_calibration_json,
)
self.radiometric_controller.sync_from_camera_controls(self.applied_camera_controls)
return self.radiometric_controller
def update_radiometry(self, decoded, meta=None):
if self.radiometric_controller is None:
return None
return self.radiometric_controller.update(decoded, meta)
def start(self, print_debug=False):
self.svc.connect()
resp = self.svc.begin(
frame_type=self.frame_type,
output_dtype=self.output_dtype,
capture_mode=self.capture_mode,
)
applied = self.apply_module_camera_settings()
if print_debug:
print("[OAK CLIENT] START:", resp)
print("[OAK CLIENT] APPLIED CAMERA SETTINGS:", applied)
if self.radiometric_enabled:
self.enable_radiometric_controller()
return resp
def stop(self):
try:
self.svc.stop()
finally:
self.svc.disconnect()
def get_status(self):
return self.svc.get_status()
def get_next_raw_frame(self, timeout=1.0):
return self.svc.capture_frame(timeout=timeout)
def get_next_frame(self, timeout=2.0):
frame, meta, _ = self.get_next_decoded(
timeout=timeout,
update_radiometry=False,
)
return frame, meta
def get_next_decoded(self, timeout=2.0, update_radiometry=True):
raw_frame, raw_meta = self.get_next_raw_frame(timeout=timeout)
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
if update_radiometry:
self.update_radiometry(decoded, raw_meta)
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()
meta = dict(raw_meta)
if frame_type == "RAW_BRUTO":
frame = raw_frame
elif frame_type == "RGB":
frame = self.build_rgb_tensor(decoded)
meta["output_layout"] = "CHW"
meta["channels"] = ["R", "G", "B"]
meta["shape"] = list(frame.shape)
meta["dtype"] = str(frame.dtype)
elif frame_type == "MULTISPEC":
frame = self.build_multispec_tensor(decoded, meta=raw_meta)
meta["output_layout"] = "CHW"
meta["channels"] = ["R", "G", "B", "RE", "NIR"]
meta["shape"] = list(frame.shape)
meta["dtype"] = str(frame.dtype)
elif frame_type == "PREVIEW":
frame = raw_frame
else:
raise RuntimeError(f"frame_type não suportado: {frame_type}")
return frame, meta, decoded
def get_next_preview(self, timeout=2.0):
raw_frame, raw_meta = self.get_next_raw_frame(timeout=timeout)
meta = dict(raw_meta)
previews = self.build_visual_preview_from_raw(raw_frame, meta)
return previews, meta
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_infer_tensor_from_decoded(self, decoded, meta, channels_expected, target_size=None):
tensor = self.core.fuse_multispec_cameras(decoded, meta, channels_expected)
return self.core.resize_tensor_chw(tensor, target_size=target_size)
def decode_stream_cameras(self, frame, meta):
if str(meta.get("frame_type", self.frame_type)).upper() == "PREVIEW":
decoded = {}
camera_info = meta.get("camera_info", {}) or {}
for cam_id, img in frame.items():
info = camera_info.get(cam_id, {}) or {}
role = info.get("role", cam_id)
img01 = self._frame_to_float01(cam_id, img, role)
decoded[cam_id] = {
"name": role.upper(),
"role": role,
"image": img01,
"meta": {
"cam_id": cam_id,
"role": role,
"socket": info.get("socket"),
"sensor": info.get("sensor"),
"timestamp": (meta.get("timestamps") or {}).get(cam_id),
"shape": list(img.shape),
"dtype": str(img.dtype),
},
}
return decoded
return self.core.decode_stream_cameras(frame, meta)
def build_rgb_tensor(self, decoded):
cam_id, item = self._find_decoded_by_role(decoded, "rgb")
rgb01 = item["image"]
if rgb01.ndim != 3 or rgb01.shape[2] != 3:
raise RuntimeError(f"{cam_id} RGB inválida: shape={rgb01.shape}")
tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
def build_multispec_tensor(self, decoded, meta=None):
tensor = self.core.fuse_multispec_cameras(
decoded=decoded,
meta=meta,
channels_expected=5,
)
return np.ascontiguousarray(tensor.astype(np.float32, copy=False))
def build_preview_from_raw_payload(self, frame, meta):
"""
Gera preview priorizando a câmera com role='rgb'.
Retorna:
preview_bgr: imagem BGR uint8 para OpenCV
payload_float_preview: tensor CHW float32 [0..1]
preview_source_id: cam_id usado
"""
decoded = self.decode_stream_cameras(frame, meta)
if not decoded:
raise RuntimeError("Nenhum frame decodificado disponível para preview.")
try:
cam_id, item = self._find_decoded_by_role(decoded, "rgb")
rgb01 = item["image"]
if rgb01.ndim != 3 or rgb01.shape[2] != 3:
raise RuntimeError(f"{cam_id} decodificada inválida para preview RGB: shape={rgb01.shape}")
preview_bgr = self._rgb01_to_bgr(rgb01)
payload_float = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
return preview_bgr, np.ascontiguousarray(payload_float), cam_id
except RuntimeError:
pass
first_id = list(decoded.keys())[0]
img01 = decoded[first_id]["image"]
if img01.ndim == 2:
preview_bgr = self._gray01_to_bgr(img01)
payload_float = np.stack([img01, img01, img01], axis=0).astype(np.float32)
elif img01.ndim == 3 and img01.shape[2] == 3:
preview_bgr = self._rgb01_to_bgr(img01)
payload_float = np.transpose(img01.astype(np.float32), (2, 0, 1))
else:
raise RuntimeError(f"Frame decodificado inválido para preview: cam={first_id}, shape={img01.shape}")
return preview_bgr, np.ascontiguousarray(payload_float), first_id
def build_visual_preview_from_raw(self, frame, meta):
camera_info = meta.get("camera_info", {}) or {}
previews = {}
for cam_id, arr in frame.items():
info = camera_info.get(cam_id, {}) or {}
role = info.get("role", cam_id)
bit_depth = int(info.get("bit_depth", 10))
if arr.ndim == 3 and arr.shape[2] == 1:
arr = arr[:, :, 0]
if bit_depth == 10 and arr.ndim == 2:
raw16 = self.core.unpack_raw10_packed(
arr,
sensor_width=int(info.get("width", self.width)),
sensor_height=int(info.get("height", self.height)),
)
if role == "rgb":
previews[cam_id] = self.preview.raw16_to_preview_bgr(
raw16,
bit_depth=bit_depth,
apply_wb=True,
apply_contrast=True,
)
else:
vis8 = self.preview.raw16_to_vis8(
raw16,
bit_depth=bit_depth,
gamma=2.2,
)
previews[cam_id] = cv2.cvtColor(vis8, cv2.COLOR_GRAY2BGR)
else:
decoded = self.decode_stream_cameras({cam_id: arr}, {"camera_info": {cam_id: info}})
img01 = decoded[cam_id]["image"]
if img01.ndim == 2:
previews[cam_id] = self._gray01_to_bgr(img01)
else:
previews[cam_id] = self._rgb01_to_bgr(img01)
return previews
def _find_decoded_by_role(self, decoded, role):
role = str(role).lower()
for cam_id, item in decoded.items():
if str(item.get("role", "")).lower() == role:
return cam_id, item
raise RuntimeError(f"Nenhuma câmera com role={role} encontrada.")
def _frame_to_float01(self, cam_id, img, role):
if img is None:
return None
arr = img
if arr.dtype == np.uint8:
arr01 = arr.astype(np.float32) / 255.0
elif arr.dtype == np.uint16:
arr01 = arr.astype(np.float32) / 65535.0
else:
arr01 = arr.astype(np.float32)
if arr01.max() > 1.5:
arr01 = arr01 / 255.0
arr01 = np.clip(arr01, 0.0, 1.0)
if role == "rgb":
# DepthAI/OpenCV entrega BGR HWC. Calibradores esperam RGB HWC.
if arr01.ndim == 3 and arr01.shape[2] == 3:
arr01 = cv2.cvtColor((arr01 * 255).astype(np.uint8), cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
elif arr01.ndim == 2:
arr01 = np.stack([arr01, arr01, arr01], axis=2)
return arr01
# Espectrais devem virar mono HW.
if arr01.ndim == 3:
arr01 = cv2.cvtColor((arr01 * 255).astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
return arr01
@staticmethod
def _rgb01_to_bgr(rgb01):
rgb_u8 = np.clip(rgb01 * 255.0, 0, 255).astype(np.uint8)
return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
@staticmethod
def _gray01_to_bgr(gray01):
g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8)
return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR)

View File

@ -31,8 +31,8 @@ class OakFcc3Manager:
self.roles = roles or {
"CAM_A": "rgb",
"CAM_B": "re",
"CAM_C": "nir",
"CAM_B": "nir",
"CAM_C": "re",
}
self.sync_mode = sync_mode
@ -47,7 +47,13 @@ class OakFcc3Manager:
self.running = False
self.frame_id = 0
self.applied_camera_controls = {}
self.control_queues = {}
self.camera_controls = {
"CAM_A": {"ae_enable": True, "awb_enable": True, "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": [1.0, 1.0]},
"CAM_B": {"ae_enable": False, "awb_enable": False, "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": None},
"CAM_C": {"ae_enable": False, "awb_enable": False, "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": None},
}
self._last_raw_dims = {}
def __enter__(self):
self.start()
@ -56,6 +62,9 @@ class OakFcc3Manager:
def __exit__(self, exc_type, exc, tb):
self.stop()
def _is_preview_mode(self):
return str(self.frame_type).upper() == "PREVIEW"
def list_cameras(self):
with dai.Device() as dev:
result = []
@ -67,6 +76,10 @@ class OakFcc3Manager:
})
return result
def _get_available_cam_ids_ordered(self):
preferred_order = ["CAM_A", "CAM_B", "CAM_C"] # RGB, NIR, RE
return [cam_id for cam_id in preferred_order if cam_id in self.queues]
def start(self):
if self.running:
return
@ -79,6 +92,7 @@ class OakFcc3Manager:
self.queues.clear()
self.buffers.clear()
self.camera_info.clear()
self._last_raw_dims.clear()
for f in features:
socket = f.socket
@ -88,17 +102,22 @@ class OakFcc3Manager:
print(f"[OAK] Criando câmera {socket_name} sensor={f.sensorName} role={role}")
cam = self.pipeline.create(dai.node.Camera).build(socket)
ctrl_q = cam.inputControl.createInputQueue()
if self._is_preview_mode():
out = cam.requestOutput(
self.size,
fps=self.fps
)
else:
out = cam.raw
q = out.createOutputQueue()
cam_id = self._socket_to_cam_id(socket_name)
cam_id = socket_name
self.queues[cam_id] = q
self.buffers[cam_id] = deque(maxlen=self.buffer_size)
self.control_queues[cam_id] = ctrl_q
self.camera_info[cam_id] = {
"id": cam_id,
@ -133,6 +152,8 @@ class OakFcc3Manager:
self.queues.clear()
self.buffers.clear()
self.camera_info.clear()
self.control_queues.clear()
self._last_raw_dims.clear()
self.running = False
def get_status(self):
@ -178,40 +199,6 @@ class OakFcc3Manager:
f"Tente aumentar para 25 ou 35 ms para diagnóstico."
)
def get_next_decoded(self, timeout=1.0, update_radiometry=True):
frame, meta = self.get_next_frame(timeout=timeout)
decoded = {
"frames": frame,
"meta": meta,
}
return frame, meta, decoded
def build_preview_from_raw_payload(self, frame, meta):
if not isinstance(frame, dict) or not frame:
raise RuntimeError("Payload inválido para preview.")
rgb_cam_id = None
for cam_id, info in self.camera_info.items():
if info.get("role") == "rgb" and cam_id in frame:
rgb_cam_id = cam_id
break
preview_source_id = rgb_cam_id or list(frame.keys())[0]
preview = frame[preview_source_id]
if preview.ndim == 2:
import cv2
preview_bgr = cv2.cvtColor(preview, cv2.COLOR_GRAY2BGR)
else:
preview_bgr = preview.copy()
preview_float = preview_bgr.astype(np.float32) / 255.0
return preview_bgr, preview_float, preview_source_id
def _drain_queues_to_buffers(self):
for cam_id, q in self.queues.items():
while q.has():
@ -222,7 +209,32 @@ class OakFcc3Manager:
except Exception:
ts = time.time()
if self._is_preview_mode():
frame = msg.getCvFrame()
else:
data = msg.getData()
raw = np.frombuffer(data, dtype=np.uint8).copy()
h = int(msg.getHeight())
w = int(msg.getWidth())
stride = int(msg.getStride())
expected = h * stride
if raw.size < expected:
raise RuntimeError(
f"RAW menor que esperado: raw.size={raw.size}, esperado={expected}, "
f"w={w}, h={h}, stride={stride}"
)
frame = raw[:expected].reshape((h, stride))
self._last_raw_dims[cam_id] = {
"sensor_width": w,
"sensor_height": h,
"stride": stride,
"packed_width": stride,
}
self.buffers[cam_id].append({
"frame": frame,
@ -288,7 +300,7 @@ class OakFcc3Manager:
return frames, timestamps, sync_dt_ms, sync_ok
def _get_required_cam_ids(self):
available = list(self.queues.keys())
available = self._get_available_cam_ids_ordered()
if self.capture_mode == "SINGLE":
return available[:1]
@ -319,6 +331,37 @@ class OakFcc3Manager:
for cam_id, arr in frames.items()
}
camera_info = {}
for cam_id, info in self.camera_info.items():
item = dict(info)
arr = frames.get(cam_id)
if arr is not None:
item["shape"] = list(arr.shape)
item["dtype"] = str(arr.dtype)
if self._is_preview_mode():
item["interface"] = "OAK"
item["channels"] = 3 if arr.ndim == 3 else 1
item["bit_depth"] = 8 if arr.dtype == np.uint8 else 16
item["height"] = int(arr.shape[0])
item["width"] = int(arr.shape[1])
else:
raw_dims = self._last_raw_dims.get(cam_id, {})
item["interface"] = "OAK_RAW"
item["raw_format"] = "RAW10_PACKED"
item["channels"] = 1
item["bit_depth"] = 10
item["height"] = int(raw_dims.get("sensor_height", arr.shape[0]))
item["width"] = int(raw_dims.get("sensor_width", arr.shape[1]))
item["stride"] = int(raw_dims.get("stride", arr.shape[1]))
item["packed_width"] = int(raw_dims.get("packed_width", arr.shape[1]))
item["shape"] = list(arr.shape)
item["dtype"] = str(arr.dtype)
item["packed"] = True
camera_info[cam_id] = item
return {
"frame_id": self.frame_id,
"backend": "oak_fcc3",
@ -328,7 +371,7 @@ class OakFcc3Manager:
"dtype": self.output_dtype,
"output_layout": "dict_by_camera",
"payload_sources": payload_sources,
"camera_info": self.camera_info,
"camera_info": camera_info,
"timestamps": timestamps,
"sync_dt_ms": sync_dt_ms,
"sync_ok": sync_ok,
@ -353,11 +396,152 @@ class OakFcc3Manager:
if self.raw_policy == "require_triple" and n < 3:
raise RuntimeError(f"raw_policy=require_triple exige 3 câmeras, mas detectou {n}.")
def get_camera_controls(self, cam_id):
self._validate_cam_id_known(cam_id)
return dict(self.camera_controls.get(cam_id, {}))
def set_ae_enable(self, cam_id, enable: bool):
self._validate_cam_id_running(cam_id)
enable = bool(enable)
ctrl_state = self.camera_controls[cam_id]
ctrl_state["ae_enable"] = enable
ctrl = dai.CameraControl()
if enable:
# Em alguns builds v3 esse método existe.
if hasattr(ctrl, "setAutoExposureEnable"):
ctrl.setAutoExposureEnable()
else:
# fallback: deixa AE assumir por região/algoritmo interno quando possível
pass
else:
exp_us = int(ctrl_state.get("exposure_time_us") or 15000)
gain = float(ctrl_state.get("analogue_gain") or 1.0)
ctrl.setManualExposure(exp_us, self._gain_to_iso(gain))
self._send_control(cam_id, ctrl)
return dict(ctrl_state)
def set_awb_enable(self, cam_id, enable: bool):
self._validate_cam_id_running(cam_id)
enable = bool(enable)
ctrl_state = self.camera_controls[cam_id]
ctrl_state["awb_enable"] = enable
ctrl = dai.CameraControl()
if hasattr(dai.CameraControl, "AutoWhiteBalanceMode"):
if enable:
ctrl.setAutoWhiteBalanceMode(dai.CameraControl.AutoWhiteBalanceMode.AUTO)
else:
ctrl.setAutoWhiteBalanceMode(dai.CameraControl.AutoWhiteBalanceMode.OFF)
self._send_control(cam_id, ctrl)
return dict(ctrl_state)
def set_exposure_time(self, cam_id, exposure_time_us: int):
self._validate_cam_id_running(cam_id)
ctrl_state = self.camera_controls[cam_id]
exposure_time_us = int(exposure_time_us)
exposure_time_us = max(1, exposure_time_us)
ctrl_state["exposure_time_us"] = exposure_time_us
ctrl_state["ae_enable"] = False
gain = float(ctrl_state.get("analogue_gain") or 1.0)
ctrl = dai.CameraControl()
ctrl.setManualExposure(exposure_time_us, self._gain_to_iso(gain))
self._send_control(cam_id, ctrl)
return dict(ctrl_state)
def set_analogue_gain(self, cam_id, analogue_gain: float):
self._validate_cam_id_running(cam_id)
ctrl_state = self.camera_controls[cam_id]
analogue_gain = float(analogue_gain)
analogue_gain = max(1.0, analogue_gain)
ctrl_state["analogue_gain"] = analogue_gain
ctrl_state["ae_enable"] = False
exposure_time_us = int(ctrl_state.get("exposure_time_us") or 15000)
ctrl = dai.CameraControl()
ctrl.setManualExposure(exposure_time_us, self._gain_to_iso(analogue_gain))
self._send_control(cam_id, ctrl)
return dict(ctrl_state)
def set_colour_gains(self, cam_id, red_gain: float, blue_gain: float):
self._validate_cam_id_running(cam_id)
ctrl_state = self.camera_controls[cam_id]
ctrl_state["colour_gains"] = [float(red_gain), float(blue_gain)]
ctrl_state["awb_enable"] = False
ctrl = dai.CameraControl()
if hasattr(ctrl, "setManualWhiteBalance"):
# Nem sempre esse método usa red/blue diretamente. Fica como placeholder seguro.
pass
self._send_control(cam_id, ctrl)
return dict(ctrl_state)
def apply_camera_controls(self, cam_id, controls: dict):
self._validate_cam_id_running(cam_id)
result = dict(self.camera_controls.get(cam_id, {}))
if "ae_enable" in controls:
result = self.set_ae_enable(cam_id, bool(controls["ae_enable"]))
if "awb_enable" in controls:
result = self.set_awb_enable(cam_id, bool(controls["awb_enable"]))
if "exposure_time_us" in controls and controls["exposure_time_us"] is not None:
result = self.set_exposure_time(cam_id, int(controls["exposure_time_us"]))
if "analogue_gain" in controls and controls["analogue_gain"] is not None:
result = self.set_analogue_gain(cam_id, float(controls["analogue_gain"]))
return result
def _send_control(self, cam_id, ctrl):
if cam_id not in self.control_queues:
raise RuntimeError(f"Fila de controle não existe para {cam_id}")
self.control_queues[cam_id].send(ctrl)
def _validate_cam_id_known(self, cam_id):
if cam_id not in self.camera_controls:
raise ValueError(f"cam_id inválido: {cam_id}")
def _validate_cam_id_running(self, cam_id):
self._validate_cam_id_known(cam_id)
if not self.running:
raise RuntimeError("Manager não está rodando.")
if cam_id not in self.control_queues:
raise RuntimeError(f"Câmera {cam_id} não está ativa no pipeline.")
@staticmethod
def _socket_to_cam_id(socket_name):
mapping = {
"CAM_A": "cam2", # RGB
"CAM_B": "cam0", # RE
"CAM_C": "cam1", # NIR
}
return mapping.get(socket_name, socket_name.lower())
def _gain_to_iso(gain: float) -> int:
# DepthAI usa ISO no setManualExposure(exposure_us, sensitivity_iso).
# Mantemos analogue_gain estilo Pi e convertemos para ISO aproximado.
gain = max(1.0, float(gain))
iso = int(round(gain * 100))
return max(100, min(1600, iso))

View File

@ -0,0 +1,229 @@
import time
from .oak_fcc3_manager import OakFcc3Manager
class OakFcc3Service:
def __init__(self, timeout=10, **kwargs):
self.timeout = timeout
self.manager = OakFcc3Manager(**kwargs)
self.connected = False
def connect(self):
self.connected = True
return {"ok": True, "backend": "oak_fcc3", "connected": True}
def disconnect(self):
self.stop()
self.connected = False
return {"ok": True, "connected": False}
def ping(self):
return {
"ok": True,
"backend": "oak_fcc3",
"msg": "pong",
"ts": time.time(),
}
def get_status(self):
status = self.manager.get_status()
active_ids = [c["id"] for c in status.get("cameras", [])]
active_roles = {
c.get("role"): c.get("id")
for c in status.get("cameras", [])
}
status.update({
"ok": True,
"connected": self.connected,
"active_camera_ids": active_ids,
"active_roles": active_roles,
"camera_count_active": len(active_ids),
})
return status
def get_config(self):
return {
"ok": True,
"fps": self.manager.fps,
"width": self.manager.width,
"height": self.manager.height,
"frame_type": self.manager.frame_type,
"output_dtype": self.manager.output_dtype,
"capture_mode": self.manager.capture_mode,
"raw_policy": self.manager.raw_policy,
"sync_mode": getattr(self.manager, "sync_mode", "best"),
"sync_tolerance_ms": self.manager.sync_tolerance_ms,
}
def set_fps(self, fps):
self._ensure_stopped_for_config()
self.manager.fps = int(fps)
return {"ok": True, "fps": self.manager.fps}
def set_resolution(self, width, height):
self._ensure_stopped_for_config()
self.manager.width = int(width)
self.manager.height = int(height)
self.manager.size = (self.manager.width, self.manager.height)
return {
"ok": True,
"width": self.manager.width,
"height": self.manager.height,
}
def set_capture_mode(self, mode):
self._ensure_stopped_for_config()
mode = str(mode).upper()
self.manager.capture_mode = self._validate_capture_mode(mode)
return {"ok": True, "capture_mode": self.manager.capture_mode}
def set_frame_type(self, frame_type):
self._ensure_stopped_for_config()
frame_type = str(frame_type).upper()
self.manager.frame_type = self._validate_frame_type(frame_type)
return {"ok": True, "frame_type": self.manager.frame_type}
def set_output_dtype(self, dtype):
self._ensure_stopped_for_config()
dtype = str(dtype).lower()
self.manager.output_dtype = self._validate_output_dtype(dtype)
return {"ok": True, "output_dtype": self.manager.output_dtype}
def begin(self, frame_type=None, output_dtype=None, capture_mode=None):
if not self.connected:
self.connect()
if self.manager.running:
return {
"ok": True,
"started": True,
"already_running": True,
"status": self.get_status(),
}
if frame_type is not None:
self.manager.frame_type = self._validate_frame_type(frame_type)
if output_dtype is not None:
self.manager.output_dtype = self._validate_output_dtype(output_dtype)
if capture_mode is not None:
self.manager.capture_mode = self._validate_capture_mode(capture_mode)
self.manager.start()
return {
"ok": True,
"started": True,
"already_running": False,
"status": self.get_status(),
}
def capture_frame(self, timeout=None):
if timeout is None:
timeout = self.timeout
frame, meta = self.manager.get_next_frame(timeout=timeout)
return frame, meta
def stop(self):
self.manager.stop()
return {"ok": True, "stopped": True}
def _ensure_stopped_for_config(self):
if self.manager.running:
raise RuntimeError(
"Configuração estrutural só pode ser alterada com o manager parado. "
"Chame stop() antes."
)
def resolve_camera_id(self, cam_id=None, role=None):
if role is not None:
role = str(role).lower()
status = self.manager.get_status()
for cam in status.get("cameras", []):
if str(cam.get("role", "")).lower() == role:
return cam["id"]
raise ValueError(f"Nenhuma câmera ativa encontrada para role={role}")
if cam_id is None:
raise ValueError("Informe cam_id ou role.")
return str(cam_id)
def get_camera_controls(self, cam_id=None, role=None):
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
ctrl = self.manager.get_camera_controls(cam_id)
ctrl["ok"] = True
ctrl["camera_id"] = cam_id
ctrl["role"] = role
return ctrl
def set_ae_enable(self, cam_id=None, role=None, enable=False):
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
ctrl = self.manager.set_ae_enable(cam_id, bool(enable))
ctrl["ok"] = True
ctrl["camera_id"] = cam_id
ctrl["role"] = role
return ctrl
def set_awb_enable(self, cam_id=None, role=None, enable=False):
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
ctrl = self.manager.set_awb_enable(cam_id, bool(enable))
ctrl["ok"] = True
ctrl["camera_id"] = cam_id
ctrl["role"] = role
return ctrl
def set_exposure_time(self, cam_id=None, role=None, exposure_time_us=None):
if exposure_time_us is None:
raise ValueError("exposure_time_us é obrigatório.")
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
ctrl = self.manager.set_exposure_time(cam_id, int(exposure_time_us))
ctrl["ok"] = True
ctrl["camera_id"] = cam_id
ctrl["role"] = role
return ctrl
def set_analogue_gain(self, cam_id=None, role=None, analogue_gain=None):
if analogue_gain is None:
raise ValueError("analogue_gain é obrigatório.")
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
ctrl = self.manager.set_analogue_gain(cam_id, float(analogue_gain))
ctrl["ok"] = True
ctrl["camera_id"] = cam_id
ctrl["role"] = role
return ctrl
def apply_camera_controls(self, cam_id=None, role=None, controls=None):
cam_id = self.resolve_camera_id(cam_id=cam_id, role=role)
ctrl = self.manager.apply_camera_controls(cam_id, controls or {})
ctrl["ok"] = True
ctrl["camera_id"] = cam_id
ctrl["role"] = role
return ctrl
def _validate_frame_type(self, frame_type):
frame_type = str(frame_type).upper()
if frame_type not in ("RAW_BRUTO", "RGB", "MULTISPEC", "PREVIEW"):
raise ValueError(f"frame_type inválido: {frame_type}")
return frame_type
def _validate_output_dtype(self, dtype):
dtype = str(dtype).lower()
if dtype not in ("uint8", "uint16", "float32"):
raise ValueError(f"output_dtype inválido: {dtype}")
return dtype
def _validate_capture_mode(self, mode):
mode = str(mode).upper()
if mode not in ("AUTO", "SINGLE", "DOUBLE", "TRIPLE"):
raise ValueError(f"capture_mode inválido: {mode}")
return mode

View File

@ -0,0 +1,314 @@
import time
import json
import numpy as np
class RadiometricController:
def __init__(
self,
client,
enabled=True,
config_json_path=None,
interval_s=0.5,
strip_y0_pct=0.95,
strip_y1_pct=1.0,
patch_x0_pct=0.35,
patch_x1_pct=0.75,
target_mean=0.70,
deadband=0.03,
alpha=0.20,
exp_min_us=100,
exp_max_us=80000,
gain_min=1.0,
gain_max=8.0,
exp_step_gain=0.65,
prefer_exposure=True,
verbose=False,
):
self.client = client
cfg = self._load_config_json(config_json_path)
interval_s = cfg.get("interval_s", interval_s)
strip_y0_pct = cfg.get("strip_y0_pct", strip_y0_pct)
strip_y1_pct = cfg.get("strip_y1_pct", strip_y1_pct)
patch_x0_pct = cfg.get("patch_x0_pct", patch_x0_pct)
patch_x1_pct = cfg.get("patch_x1_pct", patch_x1_pct)
target_mean = cfg.get("target_mean", target_mean)
deadband = cfg.get("deadband", deadband)
alpha = cfg.get("alpha", alpha)
exp_min_us = cfg.get("exp_min_us", exp_min_us)
exp_max_us = cfg.get("exp_max_us", exp_max_us)
gain_min = cfg.get("gain_min", gain_min)
gain_max = cfg.get("gain_max", gain_max)
exp_step_gain = cfg.get("exp_step_gain", exp_step_gain)
prefer_exposure = cfg.get("prefer_exposure", prefer_exposure)
verbose = cfg.get("verbose", verbose)
exp_apply_threshold_us = cfg.get("exp_apply_threshold_us", 50)
gain_apply_threshold = cfg.get("gain_apply_threshold", 0.02)
self.enabled = bool(enabled)
self.interval_s = float(interval_s)
self.strip_y0_pct = float(strip_y0_pct)
self.strip_y1_pct = float(strip_y1_pct)
self.patch_x0_pct = float(patch_x0_pct)
self.patch_x1_pct = float(patch_x1_pct)
self.target_mean = float(target_mean)
self.deadband = float(deadband)
self.alpha = float(alpha)
self.exp_min_us = int(exp_min_us)
self.exp_max_us = int(exp_max_us)
self.gain_min = float(gain_min)
self.gain_max = float(gain_max)
self.exp_step_gain = float(exp_step_gain)
self.prefer_exposure = bool(prefer_exposure)
self.verbose = bool(verbose)
self.exp_apply_threshold_us = int(exp_apply_threshold_us)
self.gain_apply_threshold = float(gain_apply_threshold)
self.last_update_ts = 0.0
self.last_result = {}
self.state = {
"rgb": {"exp": 15000, "gain": 1.0},
"nir": {"exp": 15000, "gain": 1.0},
"re": {"exp": 15000, "gain": 1.0},
}
self._ae_disabled = set()
self._last_applied = {
"rgb": {"exp": None, "gain": None},
"nir": {"exp": None, "gain": None},
"re": {"exp": None, "gain": None},
}
def _load_config_json(self, path):
if not path:
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
return {}
cfg = data.get("radiometric_config", {})
return cfg if isinstance(cfg, dict) else {}
def sync_from_camera_controls(self, camera_controls: dict | None):
if not isinstance(camera_controls, dict):
return
for role, ctrl in camera_controls.items():
if role not in self.state:
continue
exp = ctrl.get("exposure_time_us")
gain = ctrl.get("analogue_gain")
if exp is not None:
self.state[role]["exp"] = int(exp)
if gain is not None:
self.state[role]["gain"] = float(gain)
def update(self, decoded: dict, meta: dict | None = None):
if not self.enabled:
return None
now = time.perf_counter()
if now - self.last_update_ts < self.interval_s:
return None
self.last_update_ts = now
results = {}
for role in ("rgb", "nir", "re"):
cam_id = self._resolve_cam_id(decoded, role)
if cam_id is None:
continue
img = decoded[cam_id].get("image")
if img is None:
continue
metrics = self.measure_reference_patch(img)
decision = self.compute_control(role, metrics)
apply_resp = self.apply_control(role, decision)
results[role] = {
"cam_id": cam_id,
"metrics": metrics,
"decision": decision,
"apply": apply_resp,
}
self.last_result = results
return results
def _resolve_cam_id(self, decoded, role):
role = str(role).lower()
for cam_id, data in decoded.items():
if str(data.get("role", "")).lower() == role:
return cam_id
return None
def measure_reference_patch(self, img01: np.ndarray) -> dict:
if img01.ndim == 3:
# RGB: usa luminância simples
img_gray = (
0.299 * img01[:, :, 0] +
0.587 * img01[:, :, 1] +
0.114 * img01[:, :, 2]
).astype(np.float32)
else:
img_gray = img01.astype(np.float32)
h, w = img_gray.shape[:2]
y0 = int(h * self.strip_y0_pct)
y1 = int(h * self.strip_y1_pct)
x0 = int(w * self.patch_x0_pct)
x1 = int(w * self.patch_x1_pct)
y0 = max(0, min(h - 1, y0))
y1 = max(y0 + 1, min(h, y1))
x0 = max(0, min(w - 1, x0))
x1 = max(x0 + 1, min(w, x1))
patch = img_gray[y0:y1, x0:x1]
arr = patch.reshape(-1)
return {
"valid": arr.size > 0,
"mean": float(arr.mean()) if arr.size else 0.0,
"p05": float(np.percentile(arr, 5)) if arr.size else 0.0,
"p95": float(np.percentile(arr, 95)) if arr.size else 0.0,
"sat_pct": float((arr >= 0.98).mean() * 100.0) if arr.size else 0.0,
"dark_pct": float((arr <= 0.02).mean() * 100.0) if arr.size else 0.0,
"roi": [x0, y0, x1, y1],
}
def compute_control(self, role: str, metrics: dict) -> dict:
st = self.state.setdefault(role, {"exp": 15000, "gain": 1.0})
old_exp = int(st["exp"])
old_gain = float(st["gain"])
if not metrics.get("valid"):
return {
"action": "hold",
"reason": "patch inválido",
"old_exp": old_exp,
"new_exp": old_exp,
"old_gain": old_gain,
"new_gain": old_gain,
}
mean = float(metrics["mean"])
p95 = float(metrics["p95"])
sat_pct = float(metrics["sat_pct"])
error = self.target_mean - mean
new_exp = old_exp
new_gain = old_gain
action = "hold"
reason = "dentro da faixa morta"
# Proteção contra saturação
if sat_pct > 1.0 or p95 > 0.96:
desired_exp = max(self.exp_min_us, int(old_exp * 0.85))
new_exp = self._smooth_int(old_exp, desired_exp)
action = "decrease_exposure"
reason = f"saturação detectada: sat={sat_pct:.2f}% p95={p95:.3f}"
elif abs(error) > self.deadband:
factor = 1.0 + self.exp_step_gain * error
factor = max(0.70, min(1.35, factor))
if self.prefer_exposure:
desired_exp = int(old_exp * factor)
desired_exp = self._clamp(desired_exp, self.exp_min_us, self.exp_max_us)
new_exp = self._smooth_int(old_exp, desired_exp)
# Se exposição bateu limite e ainda precisa clarear/escurecer, mexe no ganho
if desired_exp in (self.exp_min_us, self.exp_max_us):
desired_gain = old_gain * factor
desired_gain = self._clamp(desired_gain, self.gain_min, self.gain_max)
new_gain = self._smooth_float(old_gain, desired_gain)
action = "increase_exposure" if error > 0 else "decrease_exposure"
reason = f"corrigindo erro radiométrico: error={error:.3f}"
else:
desired_gain = old_gain * factor
desired_gain = self._clamp(desired_gain, self.gain_min, self.gain_max)
new_gain = self._smooth_float(old_gain, desired_gain)
action = "increase_gain" if error > 0 else "decrease_gain"
reason = f"corrigindo ganho: error={error:.3f}"
new_exp = int(self._clamp(new_exp, self.exp_min_us, self.exp_max_us))
new_gain = float(self._clamp(new_gain, self.gain_min, self.gain_max))
return {
"action": action,
"reason": reason,
"mean": mean,
"target_mean": self.target_mean,
"error": error,
"old_exp": old_exp,
"new_exp": new_exp,
"old_gain": old_gain,
"new_gain": new_gain,
}
def apply_control(self, role: str, decision: dict):
new_exp = int(decision["new_exp"])
new_gain = float(decision["new_gain"])
self.state[role]["exp"] = new_exp
self.state[role]["gain"] = new_gain
responses = {}
last = self._last_applied.setdefault(role, {"exp": None, "gain": None})
try:
if role not in self._ae_disabled:
responses["ae"] = self.client.svc.set_ae_enable(role=role, enable=False)
if role == "rgb":
responses["awb"] = self.client.svc.set_awb_enable(role=role, enable=False)
self._ae_disabled.add(role)
if last["exp"] is None or abs(new_exp - last["exp"]) >= self.exp_apply_threshold_us:
responses["exposure"] = self.client.svc.set_exposure_time(role=role, exposure_time_us=new_exp)
last["exp"] = new_exp
if last["gain"] is None or abs(new_gain - last["gain"]) >= self.gain_apply_threshold:
responses["gain"] = self.client.svc.set_analogue_gain(role=role, analogue_gain=new_gain)
last["gain"] = new_gain
except Exception as e:
responses["error"] = str(e)
if self.verbose:
print(f"[RAD] {role}: {json.dumps(decision, ensure_ascii=False)} | apply={responses}")
return responses
def _smooth_int(self, old, desired):
return int(round((1.0 - self.alpha) * old + self.alpha * desired))
def _smooth_float(self, old, desired):
return float((1.0 - self.alpha) * old + self.alpha * desired)
@staticmethod
def _clamp(v, lo, hi):
return max(lo, min(hi, v))

View File

@ -0,0 +1,877 @@
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", 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",
"baseline_mm": 75.0,
"manual_offsets": {
"nir": {"dx": 0, "dy": 0, "theta_deg": 0.0},
"re": {"dx": 0, "dy": 0, "theta_deg": 0.0},
},
"homographies": {
"nir_to_rgb": None,
"re_to_rgb": None,
},
"crop_valid_common": True,
"resize_after_crop": True,
"target_size": None,
}
if calibration_json_path:
self.load_fusion_config_json(calibration_json_path)
def unpack_raw10_packed(
self,
packed_frame: np.ndarray,
sensor_width: Optional[int] = None,
sensor_height: Optional[int] = None
):
if packed_frame.ndim == 3 and packed_frame.shape[2] == 1:
packed_frame = packed_frame[:, :, 0]
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]
padding = actual_w - expected_packed_width
if actual_h != height:
raise ValueError(
f"[ERRO FRAME] Altura packed inesperada: {packed_frame.shape}, "
f"esperado altura={height}"
)
if actual_w < expected_packed_width:
raise ValueError(
f"[ERRO FRAME] Largura packed menor que a útil esperada: {packed_frame.shape}, "
f"esperado pelo menos ({height}, {expected_packed_width})"
)
if padding > 64:
raise ValueError(
f"[ERRO FRAME] Padding excessivo no packed: {packed_frame.shape}, "
f"esperado útil ({height}, {expected_packed_width}), padding={padding}"
)
packed_frame = packed_frame[:, :expected_packed_width]
groups = packed_frame.reshape(height, width // 4, 5).astype(np.uint16)
b0 = groups[:, :, 0]
b1 = groups[:, :, 1]
b2 = groups[:, :, 2]
b3 = groups[:, :, 3]
b4 = groups[:, :, 4]
p0 = (b0 << 2) | ((b4 >> 0) & 0x03)
p1 = (b1 << 2) | ((b4 >> 2) & 0x03)
p2 = (b2 << 2) | ((b4 >> 4) & 0x03)
p3 = (b3 << 2) | ((b4 >> 6) & 0x03)
raw16 = np.empty((height, width), dtype=np.uint16)
raw16[:, 0::4] = p0
raw16[:, 1::4] = p1
raw16[:, 2::4] = p2
raw16[:, 3::4] = p3
return raw16
def extract_bayer_channels(self, raw16: np.ndarray) -> dict:
p = self.bayer_pattern
if p == "GBRG":
g1 = raw16[0::2, 0::2]
b = raw16[0::2, 1::2]
r = raw16[1::2, 0::2]
g2 = raw16[1::2, 1::2]
elif p == "GRBG":
g1 = raw16[0::2, 0::2]
r = raw16[0::2, 1::2]
b = raw16[1::2, 0::2]
g2 = raw16[1::2, 1::2]
elif p == "RGGB":
r = raw16[0::2, 0::2]
g1 = raw16[0::2, 1::2]
g2 = raw16[1::2, 0::2]
b = raw16[1::2, 1::2]
elif p == "BGGR":
b = raw16[0::2, 0::2]
g1 = raw16[0::2, 1::2]
g2 = raw16[1::2, 0::2]
r = raw16[1::2, 1::2]
else:
raise ValueError(f"Padrão Bayer não suportado: {p}")
return {"R": r, "G1": g1, "G2": g2, "B": b}
def build_training_rgb(
self,
raw16: np.ndarray,
output_dtype: str = "float32",
bit_depth: int = 10,
) -> np.ndarray:
ch = self.extract_bayer_channels(raw16)
max_val = float((1 << bit_depth) - 1)
r = ch["R"].astype(np.float32) / max_val
g = ((ch["G1"].astype(np.float32) + ch["G2"].astype(np.float32)) * 0.5) / max_val
b = ch["B"].astype(np.float32) / max_val
chw = np.stack([r, g, b], axis=0).astype(np.float32)
chw = np.clip(chw, 0.0, 1.0)
if output_dtype == "float32":
return chw
if output_dtype == "uint8":
return (chw * 255.0).clip(0, 255).astype(np.uint8)
if output_dtype == "uint16":
return (chw * 65535.0).clip(0, 65535).astype(np.uint16)
raise ValueError(f"output_dtype não suportado: {output_dtype}")
def _channel_names_from_decoded(self, decoded):
names = ["R", "G", "B"]
roles = {
data.get("role") or data.get("meta", {}).get("role"): cam_id
for cam_id, data in decoded.items()
}
if "re" in roles:
names.append("RE")
if "nir" in roles:
names.append("NIR")
return names
def _find_cam_by_role(self, decoded, role):
role = str(role).lower()
for cam_id, data in decoded.items():
data_role = (
data.get("role") or
data.get("meta", {}).get("role") or
""
)
if str(data_role).lower() == role:
return cam_id
return None
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)
cam_id = meta.get("cam_id") or meta.get("camera_id") or meta.get("id") or role
if role == "rgb":
decoded[cam_id] = {
"name": "RGB",
"image": data.astype(np.float32) / max_val,
"meta": meta,
}
elif role == "re":
decoded[cam_id] = {
"name": "RE",
"image": data.astype(np.float32) / max_val,
"meta": meta,
}
elif role == "nir":
decoded[cam_id] = {
"name": "NIR",
"image": data.astype(np.float32) / max_val,
"meta": meta,
}
decoded[cam_id]["role"] = role
return decoded
def build_multispectral_tensor(self, bins_data, bins_meta, target_size=None):
decoded = self.decode_bins_cameras(bins_data, bins_meta)
rgb_cam_id = self._find_cam_by_role(decoded, "rgb")
if rgb_cam_id is None:
raise RuntimeError("RGB obrigatório")
channel_names = self._channel_names_from_decoded(decoded)
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):
"""
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 pronto
- MULTISPEC uint8/float32 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, target_size=None):
frame_type = meta.get("frame_type")
if frame_type == "RAW_BRUTO":
decoded = self.decode_stream_cameras(frame, meta)
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"):
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}")
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 {}
camera_info = meta.get("camera_info", {}) or {}
decoded = {}
for cam_id, data in frame.items():
cam_meta = camera_frames.get(cam_id) or camera_info.get(cam_id) or {}
role = str(cam_meta.get("role", "")).lower()
if not role:
raise RuntimeError(f"Meta da câmera {cam_id} sem role. Esperado role='rgb', 'nir' ou 're'.")
bit_depth = int(cam_meta.get("bit_depth", 10))
raw_format = str(cam_meta.get("raw_format", "")).upper()
is_raw10 = raw_format == "RAW10_PACKED" or bit_depth == 10
if role == "rgb":
# Caso OAK RAW real: câmera RGB também vem RAW10 packed
if is_raw10 and data.ndim == 2:
sensor_w = int(cam_meta.get("width", self.sensor_width))
sensor_h = int(cam_meta.get("height", self.sensor_height))
raw16 = self.unpack_raw10_packed(
data,
sensor_width=sensor_w,
sensor_height=sensor_h,
)
rgb_chw = self.build_training_rgb(
raw16,
output_dtype="float32",
bit_depth=bit_depth,
)
rgb_hwc = np.transpose(rgb_chw, (1, 2, 0))
decoded[cam_id] = {
"name": "RGB",
"role": "rgb",
"image": rgb_hwc,
"meta": cam_meta,
}
else:
# Caso preview/processado antigo: BGR HWC uint8
if data.ndim != 3 or data.shape[2] != 3:
raise RuntimeError(f"{cam_id} RGB inválida: shape={data.shape}")
rgb = data[:, :, ::-1].astype(np.float32) / 255.0
decoded[cam_id] = {
"name": "RGB",
"role": "rgb",
"image": np.clip(rgb, 0.0, 1.0),
"meta": cam_meta,
}
elif role == "re":
decoded[cam_id] = {
"name": "RE",
"role": "re",
"image": self._decode_spectral_frame_to_float01(data, cam_meta),
"meta": cam_meta,
}
elif role == "nir":
decoded[cam_id] = {
"name": "NIR",
"role": "nir",
"image": self._decode_spectral_frame_to_float01(data, cam_meta),
"meta": cam_meta,
}
return decoded
def fuse_multispec_cameras(self, decoded, meta, channels_expected):
rgb_cam_id = self._find_cam_by_role(decoded, "rgb")
if rgb_cam_id is None:
raise RuntimeError("Fusão requer câmera com role='rgb' como referência")
rgb = decoded[rgb_cam_id]["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)]
role_to_cam = {
item.get("role", data.get("meta", {}).get("role")): cam_id
for cam_id, data in decoded.items()
for item in [data]
}
for role, ch_name in (("re", "RE"), ("nir", "NIR")):
cam_id = role_to_cam.get(role)
if cam_id is None:
continue
img = decoded[cam_id]["image"]
aligned, valid_mask = self._warp_with_valid_mask(img, role, (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, role, 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(role, {})
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(role, {})
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"{role}_to_rgb")
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 {role}: shape={H.shape}")
warped = cv2.warpPerspective(
img, H, (ref_w, ref_h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0
)
warped_mask = cv2.warpPerspective(
mask, H, (ref_w, ref_h),
flags=cv2.INTER_NEAREST,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0
)
else:
raise RuntimeError(f"alignment_mode inválido: {mode}")
warped_mask = (warped_mask > 0).astype(np.uint8)
return warped, warped_mask
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 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", {})
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
def _decode_spectral_frame_to_float01(self, data, cam_meta):
arr = data
if arr.ndim == 3 and arr.shape[2] == 1:
arr = arr[:, :, 0]
bit_depth = int(cam_meta.get("bit_depth", 8))
channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1
# Caso preview/processado: mono já vem uint8/uint16 normal.
if arr.ndim == 2 and arr.dtype == np.uint8:
return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0)
if arr.ndim == 2 and arr.dtype == np.uint16 and bit_depth != 10:
return np.clip(arr.astype(np.float32) / 65535.0, 0.0, 1.0)
if bit_depth == 10:
sensor_width = int(cam_meta.get("width", self.sensor_width))
sensor_height = int(cam_meta.get("height", arr.shape[0]))
raw16 = self.unpack_raw10_packed(
arr,
sensor_width=sensor_width,
sensor_height=sensor_height,
)
max_val = float((1 << bit_depth) - 1)
return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
# fallback
arr01 = arr.astype(np.float32)
if arr01.max() > 1.5:
arr01 /= 255.0
return np.clip(arr01, 0.0, 1.0)

View File

@ -0,0 +1,125 @@
import cv2
import numpy as np
from typing import Optional
class RawProcessorPreview:
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.upper()
def raw16_to_vis8(
self, raw16: np.ndarray,
black_level: Optional[int] = None,
white_level: Optional[int] = None,
gamma: float = 2.2,
bit_depth: int = 10
) -> np.ndarray:
"""
Conversão para visualização:
- auto-level
- gamma
"""
max_val = float((1 << bit_depth) - 1)
raw = raw16.astype(np.float32)
if black_level is None:
black_level = float(raw.min())
if white_level is None:
white_level = float(raw.max())
if white_level <= black_level:
norm = raw / max_val
else:
norm = (raw - black_level) / (white_level - black_level)
norm = np.clip(norm, 0.0, 1.0)
if gamma is not None and gamma > 0:
norm = np.power(norm, 1.0 / gamma)
return (norm * 255.0).clip(0, 255).astype(np.uint8)
def _debayer_code(self):
mapping = {
# Mapeamento ajustado para OpenCV gerar BGR correto a partir do padrão Bayer informado.
"GBRG": cv2.COLOR_BayerGR2BGR,
"GRBG": cv2.COLOR_BayerGB2BGR,
"RGGB": cv2.COLOR_BayerBG2BGR,
"BGGR": cv2.COLOR_BayerRG2BGR,
}
if self.bayer_pattern not in mapping:
raise ValueError(f"Padrão Bayer não suportado: {self.bayer_pattern}")
return mapping[self.bayer_pattern]
def apply_preview_white_balance(self, bgr: np.ndarray, strength: float = 1.0) -> np.ndarray:
"""
Gray-world simples para deixar o preview mais agradável.
Não usar no raw de treino.
"""
img = bgr.astype(np.float32)
mean_b = float(img[:, :, 0].mean())
mean_g = float(img[:, :, 1].mean())
mean_r = float(img[:, :, 2].mean())
mean_gray = (mean_b + mean_g + mean_r) / 3.0
eps = 1e-6
gain_b = mean_gray / max(mean_b, eps)
gain_g = mean_gray / max(mean_g, eps)
gain_r = mean_gray / max(mean_r, eps)
# strength=1 aplica total, strength=0 não aplica
gain_b = 1.0 + (gain_b - 1.0) * strength
gain_g = 1.0 + (gain_g - 1.0) * strength
gain_r = 1.0 + (gain_r - 1.0) * strength
img[:, :, 0] *= gain_b
img[:, :, 1] *= gain_g
img[:, :, 2] *= gain_r
return np.clip(img, 0, 255).astype(np.uint8)
def apply_preview_contrast(self, bgr: np.ndarray, alpha: float = 1.08, beta: float = 0.0) -> np.ndarray:
"""
Ajuste leve de contraste/brilho para preview.
"""
out = cv2.convertScaleAbs(bgr, alpha=alpha, beta=beta)
return out
def raw16_to_preview_bgr(
self,
raw16: np.ndarray,
gamma: float = 2.2,
wb_strength: float = 0.8,
apply_wb: bool = True,
apply_contrast: bool = True,
bit_depth: int = 10,
) -> np.ndarray:
"""
Pipeline de preview bonito:
1. auto-level + gamma no mosaico
2. demosaic
3. white balance simples
4. leve contraste final
"""
vis8 = self.raw16_to_vis8(raw16, gamma=gamma, bit_depth=bit_depth)
bgr = cv2.cvtColor(vis8, self._debayer_code())
if apply_wb:
bgr = self.apply_preview_white_balance(bgr, strength=wb_strength)
if apply_contrast:
bgr = self.apply_preview_contrast(bgr, alpha=1.08, beta=0.0)
return bgr
def raw16_to_preview_jpg_bytes(self, raw16: np.ndarray, jpeg_quality: int = 95) -> bytes:
bgr = self.raw16_to_preview_bgr(raw16)
ok, enc = cv2.imencode(".jpg", bgr, [int(cv2.IMWRITE_JPEG_QUALITY), int(jpeg_quality)])
if not ok:
raise RuntimeError("Falha ao codificar preview JPG")
return enc.tobytes()

View File

@ -0,0 +1,134 @@
{
"ts": "2026-05-04T19:53:58.192",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "MULTISPEC",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 128,
"backend": "oak_fcc3",
"frame_type": "MULTISPEC",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "CHW",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb"
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re"
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir"
}
},
"timestamps": {
"cam2": 200401.897136,
"cam0": 200401.905167,
"cam1": 200401.905153
},
"sync_dt_ms": 8.030999975744635,
"sync_ok": true,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
480,
640,
3
],
"cam0": [
480,
640
],
"cam1": [
480,
640
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0,
"channels": [
"R",
"G",
"B",
"RE",
"NIR"
],
"fusion_applied": true,
"fusion_alignment_mode": "manual_affine",
"module_calibration_json": ".\\calibration\\module_params.json",
"tensor_shape": [
5,
480,
640
]
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "multispec",
"saved_payload_path": "20260504_195358_192.raw",
"saved_payload_dtype": "float32",
"saved_payload_shape": [
5,
480,
640
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@ -0,0 +1,137 @@
{
"ts": "2026-05-04T19:54:53.002",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 47,
"backend": "oak_fcc3",
"frame_type": "RAW_BRUTO",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "dict_by_camera",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb"
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re"
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir"
}
},
"timestamps": {
"cam2": 200456.850225,
"cam0": 200456.857952,
"cam1": 200456.85794
},
"sync_dt_ms": 7.726999989245087,
"sync_ok": true,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
480,
640,
3
],
"cam0": [
480,
640
],
"cam1": [
480,
640
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "raw_native_multi",
"saved_payload_paths": {
"cam2": "20260504_195453_002_cam2.bin",
"cam0": "20260504_195453_002_cam0.bin",
"cam1": "20260504_195453_002_cam1.bin"
},
"saved_payload_shapes": {
"cam2": [
480,
640,
3
],
"cam0": [
480,
640
],
"cam1": [
480,
640
]
},
"saved_payload_dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,177 @@
{
"ts": "2026-05-04T20:41:33.526",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 45,
"backend": "oak_fcc3",
"frame_type": "RAW_BRUTO",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "dict_by_camera",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
}
},
"timestamps": {
"cam2": 203257.450285,
"cam0": 203257.439577,
"cam1": 203257.439565
},
"sync_dt_ms": 10.719999991124496,
"sync_ok": true,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "raw_native_multi",
"saved_payload_paths": {
"cam2": "20260504_204133_526_cam2.bin",
"cam0": "20260504_204133_526_cam0.bin",
"cam1": "20260504_204133_526_cam1.bin"
},
"saved_payload_shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"saved_payload_dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -0,0 +1,165 @@
{
"ts": "2026-05-04T20:42:00.031",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "MULTISPEC",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 115,
"backend": "oak_fcc3",
"frame_type": "MULTISPEC",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "CHW",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
480,
640,
3
],
"dtype": "uint8",
"interface": "OAK",
"channels": 3,
"bit_depth": 8,
"height": 480,
"width": 640
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
480,
640
],
"dtype": "uint8",
"interface": "OAK",
"channels": 1,
"bit_depth": 8,
"height": 480,
"width": 640
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
480,
640
],
"dtype": "uint8",
"interface": "OAK",
"channels": 1,
"bit_depth": 8,
"height": 480,
"width": 640
}
},
"timestamps": {
"cam2": 203284.122167,
"cam0": 203284.130001,
"cam1": 203284.129988
},
"sync_dt_ms": 7.834000018192455,
"sync_ok": true,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
480,
640,
3
],
"cam0": [
480,
640
],
"cam1": [
480,
640
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0,
"channels": [
"R",
"G",
"B",
"RE",
"NIR"
],
"fusion_applied": true,
"fusion_alignment_mode": "manual_affine",
"module_calibration_json": ".\\calibration\\module_params.json",
"tensor_shape": [
5,
480,
640
]
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "multispec",
"saved_payload_path": "20260504_204200_031.raw",
"saved_payload_dtype": "float32",
"saved_payload_shape": [
5,
480,
640
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

@ -0,0 +1,165 @@
{
"ts": "2026-05-04T20:50:33.329",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "MULTISPEC",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 71,
"backend": "oak_fcc3",
"frame_type": "MULTISPEC",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "CHW",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
480,
640,
3
],
"dtype": "uint8",
"interface": "OAK",
"channels": 3,
"bit_depth": 8,
"height": 480,
"width": 640
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
480,
640
],
"dtype": "uint8",
"interface": "OAK",
"channels": 1,
"bit_depth": 8,
"height": 480,
"width": 640
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
480,
640
],
"dtype": "uint8",
"interface": "OAK",
"channels": 1,
"bit_depth": 8,
"height": 480,
"width": 640
}
},
"timestamps": {
"cam2": 203797.43597,
"cam0": 203797.44401,
"cam1": 203797.443995
},
"sync_dt_ms": 8.040000015171245,
"sync_ok": true,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
480,
640,
3
],
"cam0": [
480,
640
],
"cam1": [
480,
640
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0,
"channels": [
"R",
"G",
"B",
"RE",
"NIR"
],
"fusion_applied": true,
"fusion_alignment_mode": "manual_affine",
"module_calibration_json": ".\\calibration\\module_params.json",
"tensor_shape": [
5,
480,
640
]
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "multispec",
"saved_payload_path": "20260504_205033_329.raw",
"saved_payload_dtype": "float32",
"saved_payload_shape": [
5,
480,
640
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View File

@ -0,0 +1,177 @@
{
"ts": "2026-05-04T21:17:32.153",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 76,
"backend": "oak_fcc3",
"frame_type": "RAW_BRUTO",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "dict_by_camera",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
}
},
"timestamps": {
"cam2": 205415.956354,
"cam0": 205416.11234,
"cam1": 205416.045648
},
"sync_dt_ms": 155.98599999793805,
"sync_ok": false,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "raw_native_multi",
"saved_payload_paths": {
"cam2": "20260504_211732_153_cam2.bin",
"cam0": "20260504_211732_153_cam0.bin",
"cam1": "20260504_211732_153_cam1.bin"
},
"saved_payload_shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"saved_payload_dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@ -0,0 +1,175 @@
{
"ts": "2026-05-04T21:17:46.563",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "MULTISPEC",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 16,
"backend": "oak_fcc3",
"frame_type": "MULTISPEC",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "CHW",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
}
},
"timestamps": {
"cam2": 205430.483298,
"cam0": 205430.406228,
"cam1": 205430.439549
},
"sync_dt_ms": 77.0699999993667,
"sync_ok": false,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0,
"channels": [
"R",
"G",
"B",
"RE",
"NIR"
],
"fusion_applied": true,
"fusion_alignment_mode": "manual_affine",
"module_calibration_json": ".\\calibration\\module_params.json",
"tensor_shape": [
5,
400,
640
]
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "multispec",
"saved_payload_path": "20260504_211746_571.raw",
"saved_payload_dtype": "float32",
"saved_payload_shape": [
5,
400,
640
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,172 @@
{
"ts": "2026-05-04T22:37:32.539",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "MULTISPEC",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 72,
"backend": "oak_fcc3",
"frame_type": "MULTISPEC",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "CHW",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
}
},
"timestamps": {
"cam2": 210216.453791,
"cam0": 210216.442723,
"cam1": 210216.376034
},
"sync_dt_ms": 77.757000020938,
"sync_ok": false,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0,
"channels": [
"R",
"G",
"B",
"RE",
"NIR"
],
"shape": [
5,
400,
640
]
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "multispec",
"saved_payload_path": "20260504_223732_542.raw",
"saved_payload_dtype": "float32",
"saved_payload_shape": [
5,
400,
640
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@ -0,0 +1,177 @@
{
"ts": "2026-05-04T22:37:47.700",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": "TRIPLE",
"capture_mode_effective": "TRIPLE",
"raw_policy": "require_triple",
"stream_meta": {
"frame_id": 11,
"backend": "oak_fcc3",
"frame_type": "RAW_BRUTO",
"capture_mode": "TRIPLE",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "dict_by_camera",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
}
},
"timestamps": {
"cam2": 210231.566515,
"cam0": 210231.655628,
"cam1": 210231.555617
},
"sync_dt_ms": 100.0110000022687,
"sync_ok": false,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0
},
"applied_camera_controls": {
"cam0": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam1": {
"ae_enable": false,
"awb_enable": false,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": null,
"ok": true
},
"cam2": {
"ae_enable": false,
"awb_enable": true,
"exposure_time_us": 15000,
"analogue_gain": 1.0,
"colour_gains": [
1.0,
1.0
],
"ok": true
}
},
"camera_params_json": ".\\calibration\\module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "raw_native_multi",
"saved_payload_paths": {
"cam2": "20260504_223747_700_cam2.bin",
"cam0": "20260504_223747_700_cam0.bin",
"cam1": "20260504_223747_700_cam1.bin"
},
"saved_payload_shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"saved_payload_dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -0,0 +1,149 @@
{
"ts": "2026-05-05T08:05:51.885",
"cana": "baixa",
"horario": "cedo",
"sensor_width": 640,
"sensor_height": 480,
"bayer_pattern": "GBRG",
"fps_target": 20,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": "AUTO",
"capture_mode_effective": "AUTO",
"raw_policy": "allow_single",
"stream_meta": {
"frame_id": 124,
"backend": "oak_fcc3",
"frame_type": "RAW_BRUTO",
"capture_mode": "AUTO",
"output_dtype": "float32",
"dtype": "float32",
"output_layout": "dict_by_camera",
"payload_sources": [
"cam2",
"cam0",
"cam1"
],
"camera_info": {
"cam2": {
"id": "cam2",
"socket": "CAM_A",
"sensor": "OV9782",
"role": "rgb",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam0": {
"id": "cam0",
"socket": "CAM_B",
"sensor": "OV9282",
"role": "re",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
},
"cam1": {
"id": "cam1",
"socket": "CAM_C",
"sensor": "OV9282",
"role": "nir",
"shape": [
800,
1600
],
"dtype": "uint8",
"interface": "OAK_RAW",
"raw_format": "RAW10_PACKED",
"channels": 1,
"bit_depth": 10,
"height": 800,
"width": 1280,
"stride": 1600,
"packed_width": 1600,
"packed": true
}
},
"timestamps": {
"cam2": 244316.809677,
"cam0": 244316.798187,
"cam1": 244316.798162
},
"sync_dt_ms": 11.51500002015382,
"sync_ok": true,
"sync_tolerance_ms": 25.0,
"shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
},
"codec_name": "none",
"codec_family": "none",
"dt_comp": 0.0,
"dt_send_payload_prev": 0.0
},
"applied_camera_controls": {},
"camera_params_json": "calibration/module_params.json",
"note": "manual",
"raw_preview_reference_camera": "cam2",
"saved_payload_type": "raw_native_multi",
"saved_payload_paths": {
"cam2": "20260505_080551_885_cam2.bin",
"cam0": "20260505_080551_885_cam0.bin",
"cam1": "20260505_080551_885_cam1.bin"
},
"saved_payload_shapes": {
"cam2": [
800,
1600
],
"cam0": [
800,
1600
],
"cam1": [
800,
1600
]
},
"saved_payload_dtypes": {
"cam2": "uint8",
"cam0": "uint8",
"cam1": "uint8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -1,391 +0,0 @@
import numpy as np
import cv2
import json
import os
from oak_fcc3_service import OakFcc3Service
class OakFcc3Client:
def __init__(
self,
pi_host=None,
pc_host=None,
server_port=None,
stream_port=None,
width=640,
height=400,
bayer="GBRG",
fps=30,
frame_type="RAW_BRUTO",
output_dtype="uint8",
capture_mode="AUTO",
raw_policy="allow_single",
module_calibration_json=None,
radiometric_enabled=False,
sync_mode="best",
sync_tolerance_ms=25.0,
**kwargs,
):
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.module_params = self._load_module_params(module_calibration_json)
self.fusion_config = self.module_params.get("fusion_config", {}) or {}
self.radiometric_enabled = radiometric_enabled
self.svc = OakFcc3Service(
timeout=10,
fps=fps,
width=width,
height=height,
frame_type=frame_type,
output_dtype=output_dtype,
capture_mode=capture_mode,
raw_policy=raw_policy,
sync_mode=sync_mode,
sync_tolerance_ms=sync_tolerance_ms,
**kwargs,
)
self.applied_camera_controls = {}
self.radiometric_controller = None
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc, tb):
self.stop()
def _load_module_params(self, path):
if not path or not os.path.isfile(path):
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def start(self, print_debug=False):
self.svc.connect()
resp = self.svc.begin(
frame_type=self.frame_type,
output_dtype=self.output_dtype,
capture_mode=self.capture_mode,
)
if print_debug:
print("[OAK CLIENT] START:", resp)
return resp
def stop(self):
try:
self.svc.stop()
finally:
self.svc.disconnect()
def get_status(self):
return self.svc.get_status()
def get_next_frame(self, timeout=1.0):
return self.svc.capture_frame(timeout=timeout)
def get_next_decoded(self, timeout=1.0, update_radiometry=True):
raw_frame, meta = self.get_next_frame(timeout=timeout)
decoded = self.decode_stream_cameras(raw_frame, meta)
frame_type = str(self.frame_type).upper()
if frame_type == "RAW_BRUTO":
meta["output_layout"] = "dict_by_camera"
return raw_frame, meta, decoded
if frame_type == "RGB":
tensor = self.build_rgb_tensor(decoded)
meta["frame_type"] = "RGB"
meta["output_layout"] = "CHW"
meta["channels"] = ["R", "G", "B"]
meta["dtype"] = "float32"
meta["output_dtype"] = "float32"
meta["tensor_shape"] = list(tensor.shape)
return tensor, meta, decoded
if frame_type == "MULTISPEC":
tensor = self.build_multispec_tensor(decoded)
meta["frame_type"] = "MULTISPEC"
meta["output_layout"] = "CHW"
meta["channels"] = ["R", "G", "B", "RE", "NIR"]
meta["fusion_applied"] = True
meta["fusion_alignment_mode"] = self.fusion_config.get("alignment_mode", "identity")
meta["module_calibration_json"] = self.module_calibration_json
meta["dtype"] = "float32"
meta["output_dtype"] = "float32"
meta["tensor_shape"] = list(tensor.shape)
return tensor, meta, decoded
raise RuntimeError(f"frame_type não suportado: {self.frame_type}")
def build_rgb_tensor(self, decoded):
if "cam2" not in decoded:
raise RuntimeError("RGB exige cam2 disponível.")
rgb01 = decoded["cam2"]["image"]
if rgb01.ndim != 3 or rgb01.shape[2] != 3:
raise RuntimeError(f"cam2 RGB inválida: shape={rgb01.shape}")
tensor = np.transpose(rgb01.astype(np.float32), (2, 0, 1))
return np.ascontiguousarray(tensor)
def build_multispec_tensor(self, decoded):
if "cam2" not in decoded:
raise RuntimeError("MULTISPEC exige cam2/RGB disponível.")
if "cam0" not in decoded:
raise RuntimeError("MULTISPEC exige cam0/RE disponível.")
if "cam1" not in decoded:
raise RuntimeError("MULTISPEC exige cam1/NIR disponível.")
rgb01 = decoded["cam2"]["image"]
re01 = decoded["cam0"]["image"]
nir01 = decoded["cam1"]["image"]
if rgb01.ndim != 3 or rgb01.shape[2] != 3:
raise RuntimeError(f"cam2 RGB inválida: shape={rgb01.shape}")
h, w = rgb01.shape[:2]
re01 = self._align_spectral_to_rgb(re01, "cam0", h, w)
nir01 = self._align_spectral_to_rgb(nir01, "cam1", h, w)
r = rgb01[:, :, 0]
g = rgb01[:, :, 1]
b = rgb01[:, :, 2]
tensor = np.stack(
[
r,
g,
b,
re01,
nir01,
],
axis=0,
).astype(np.float32)
return np.ascontiguousarray(tensor)
@staticmethod
def _resize_gray_to(img01, h, w):
if img01 is None:
raise RuntimeError("Canal espectral ausente.")
if img01.ndim == 3:
img01 = img01[:, :, 0]
if img01.shape[:2] != (h, w):
img01 = cv2.resize(img01, (w, h), interpolation=cv2.INTER_LINEAR)
return np.clip(img01.astype(np.float32), 0.0, 1.0)
def _align_spectral_to_rgb(self, img01, cam_id, h, w):
if img01 is None:
raise RuntimeError(f"Canal ausente: {cam_id}")
if img01.ndim == 3:
img01 = img01[:, :, 0]
if img01.shape[:2] != (h, w):
img01 = cv2.resize(img01, (w, h), interpolation=cv2.INTER_LINEAR)
cfg = self.fusion_config or {}
mode = cfg.get("alignment_mode", "identity")
if mode == "identity":
return np.clip(img01.astype(np.float32), 0.0, 1.0)
if mode == "manual_offset":
offs = cfg.get("manual_offsets", {}).get(cam_id, {})
dx = int(offs.get("dx", 0))
dy = int(offs.get("dy", 0))
M = np.float32([[1, 0, dx], [0, 1, dy]])
out = cv2.warpAffine(
img01,
M,
(w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
return np.clip(out.astype(np.float32), 0.0, 1.0)
if 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))
center = (w * 0.5, h * 0.5)
M = cv2.getRotationMatrix2D(center, theta_deg, 1.0)
M[0, 2] += dx
M[1, 2] += dy
out = cv2.warpAffine(
img01,
M,
(w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
return np.clip(out.astype(np.float32), 0.0, 1.0)
if mode == "homography":
H = cfg.get("homographies", {}).get(f"{cam_id}_to_cam2")
if H is None:
return np.clip(img01.astype(np.float32), 0.0, 1.0)
H = np.asarray(H, dtype=np.float32)
if H.shape != (3, 3):
raise RuntimeError(f"Homografia inválida para {cam_id}: shape={H.shape}")
out = cv2.warpPerspective(
img01,
H,
(w, h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
return np.clip(out.astype(np.float32), 0.0, 1.0)
raise RuntimeError(f"alignment_mode inválido: {mode}")
def decode_stream_cameras(self, frame, meta):
"""
Compatível com os calibradores antigos.
Retorna:
decoded["cam2"]["image"] = RGB float32 HWC [0..1]
decoded["cam0"]["image"] = RE float32 HW [0..1]
decoded["cam1"]["image"] = NIR float32 HW [0..1]
"""
decoded = {}
if not isinstance(frame, dict):
raise RuntimeError("OakFcc3Client espera frame como dict por câmera.")
camera_info = meta.get("camera_info", {}) or {}
for cam_id, img in frame.items():
info = camera_info.get(cam_id, {}) or {}
role = info.get("role", cam_id)
img01 = self._frame_to_float01(cam_id, img, role)
if role == "rgb":
name = "RGB"
elif role == "re":
name = "RE"
elif role == "nir":
name = "NIR"
else:
name = role.upper()
decoded[cam_id] = {
"name": name,
"role": role,
"image": img01,
"meta": {
"cam_id": cam_id,
"role": role,
"socket": info.get("socket"),
"sensor": info.get("sensor"),
"timestamp": (meta.get("timestamps") or {}).get(cam_id),
"shape": list(img.shape),
"dtype": str(img.dtype),
},
}
return decoded
def build_preview_from_raw_payload(self, frame, meta):
"""
Usado pelo capture antigo.
Retorna:
preview_bgr
payload_float_preview
preview_source_id
"""
decoded = self.decode_stream_cameras(frame, meta)
if "cam2" in decoded:
rgb01 = decoded["cam2"]["image"]
preview_bgr = self._rgb01_to_bgr(rgb01)
return preview_bgr, rgb01.copy(), "cam2"
first_id = list(decoded.keys())[0]
img01 = decoded[first_id]["image"]
if img01.ndim == 2:
preview_bgr = self._gray01_to_bgr(img01)
else:
preview_bgr = self._rgb01_to_bgr(img01)
return preview_bgr, img01.copy(), first_id
def _frame_to_float01(self, cam_id, img, role):
if img is None:
return None
arr = img
if arr.dtype == np.uint8:
arr01 = arr.astype(np.float32) / 255.0
elif arr.dtype == np.uint16:
arr01 = arr.astype(np.float32) / 65535.0
else:
arr01 = arr.astype(np.float32)
if arr01.max() > 1.5:
arr01 = arr01 / 255.0
arr01 = np.clip(arr01, 0.0, 1.0)
if role == "rgb":
# DepthAI/OpenCV entrega BGR HWC. Calibradores esperam RGB HWC.
if arr01.ndim == 3 and arr01.shape[2] == 3:
arr01 = cv2.cvtColor((arr01 * 255).astype(np.uint8), cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
elif arr01.ndim == 2:
arr01 = np.stack([arr01, arr01, arr01], axis=2)
return arr01
# Espectrais devem virar mono HW.
if arr01.ndim == 3:
arr01 = cv2.cvtColor((arr01 * 255).astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
return arr01
@staticmethod
def _rgb01_to_bgr(rgb01):
rgb_u8 = np.clip(rgb01 * 255.0, 0, 255).astype(np.uint8)
return cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2BGR)
@staticmethod
def _gray01_to_bgr(gray01):
g = np.clip(gray01 * 255.0, 0, 255).astype(np.uint8)
return cv2.cvtColor(g, cv2.COLOR_GRAY2BGR)

View File

@ -1,133 +0,0 @@
import time
from oak_fcc3_manager import OakFcc3Manager
class OakFcc3Service:
def __init__(self, timeout=10, **kwargs):
self.timeout = timeout
self.manager = OakFcc3Manager(**kwargs)
self.connected = False
def connect(self):
self.connected = True
return {"ok": True, "backend": "oak_fcc3", "connected": True}
def disconnect(self):
self.stop()
self.connected = False
return {"ok": True, "connected": False}
def ping(self):
return {
"ok": True,
"backend": "oak_fcc3",
"msg": "pong",
"ts": time.time(),
}
def get_status(self):
status = self.manager.get_status()
active_ids = [c["id"] for c in status.get("cameras", [])]
status.update({
"ok": True,
"connected": self.connected,
"active_camera_ids": active_ids,
"camera_count_active": len(active_ids),
})
return status
def get_config(self):
return {
"ok": True,
"fps": self.manager.fps,
"width": self.manager.width,
"height": self.manager.height,
"frame_type": self.manager.frame_type,
"output_dtype": self.manager.output_dtype,
"capture_mode": self.manager.capture_mode,
"raw_policy": self.manager.raw_policy,
"sync_mode": getattr(self.manager, "sync_mode", "best"),
"sync_tolerance_ms": self.manager.sync_tolerance_ms,
}
def set_fps(self, fps):
self._ensure_stopped_for_config()
self.manager.fps = int(fps)
return {"ok": True, "fps": self.manager.fps}
def set_resolution(self, width, height):
self._ensure_stopped_for_config()
self.manager.width = int(width)
self.manager.height = int(height)
self.manager.size = (self.manager.width, self.manager.height)
return {
"ok": True,
"width": self.manager.width,
"height": self.manager.height,
}
def set_capture_mode(self, mode):
self._ensure_stopped_for_config()
mode = str(mode).upper()
if mode not in ("AUTO", "SINGLE", "DOUBLE", "TRIPLE"):
raise ValueError(f"capture_mode inválido: {mode}")
self.manager.capture_mode = mode
return {"ok": True, "capture_mode": self.manager.capture_mode}
def set_frame_type(self, frame_type):
self._ensure_stopped_for_config()
frame_type = str(frame_type).upper()
if frame_type not in ("RAW_BRUTO", "RGB", "MULTISPEC"):
raise ValueError(f"frame_type inválido: {frame_type}")
self.manager.frame_type = frame_type
return {"ok": True, "frame_type": self.manager.frame_type}
def set_output_dtype(self, dtype):
self._ensure_stopped_for_config()
dtype = str(dtype).lower()
if dtype not in ("uint8", "uint16", "float32"):
raise ValueError(f"output_dtype inválido: {dtype}")
self.manager.output_dtype = dtype
return {"ok": True, "output_dtype": self.manager.output_dtype}
def begin(self, frame_type=None, output_dtype=None, capture_mode=None):
if frame_type is not None:
self.set_frame_type(frame_type)
if output_dtype is not None:
self.set_output_dtype(output_dtype)
if capture_mode is not None:
self.set_capture_mode(capture_mode)
self.manager.start()
return {
"ok": True,
"started": True,
"status": self.get_status(),
}
def capture_frame(self, timeout=None):
if timeout is None:
timeout = self.timeout
frame, meta = self.manager.get_next_frame(timeout=timeout)
return frame, meta
def stop(self):
self.manager.stop()
return {"ok": True, "stopped": True}
def _ensure_stopped_for_config(self):
if self.manager.running:
raise RuntimeError(
"Configuração estrutural só pode ser alterada com o manager parado. "
"Chame stop() antes."
)

View File

@ -1,35 +0,0 @@
import cv2
from oak_fcc3_manager import OakFcc3Manager
with OakFcc3Manager(
fps=30,
width=640,
height=400,
capture_mode="AUTO",
raw_policy="allow_single",
sync_mode="best",
sync_tolerance_ms=10.0,
buffer_size=12,
) as cam:
print(cam.get_status())
while True:
frame, meta, decoded = cam.get_next_decoded(timeout=1.0)
print(
"frame_id:", meta["frame_id"],
"sources:", meta["payload_sources"],
"sync_ms:", f"{meta['sync_dt_ms']:.2f}",
"sync_ok:", meta["sync_ok"]
)
for cam_id, img in frame.items():
info = meta["camera_info"].get(cam_id, {})
title = f"{cam_id} | {info.get('socket')} | {info.get('role')}"
cv2.imshow(title, img)
if cv2.waitKey(1) in (27, ord("q")):
break
cv2.destroyAllWindows()

View File

@ -1,48 +0,0 @@
import cv2
from oak_fcc3_client import OakFcc3Client
with OakFcc3Client(
fps=15,
width=640,
height=400,
frame_type="RAW_BRUTO",
output_dtype="uint8",
capture_mode="AUTO",
raw_policy="allow_single",
sync_mode="best",
sync_tolerance_ms=25.0,
module_calibration_json="calibration/module_params.json",
) as cam:
print("STATUS:", cam.get_status())
while True:
frame, meta, decoded = cam.get_next_decoded(timeout=2.0)
print(
"frame_id:", meta["frame_id"],
"sources:", meta["payload_sources"],
"decoded:", list(decoded.keys()),
"sync_ms:", f"{meta.get('sync_dt_ms', 0):.2f}",
"sync_ok:", meta.get("sync_ok"),
)
if "cam2" in decoded:
rgb01 = decoded["cam2"]["image"]
rgb_bgr = cv2.cvtColor((rgb01 * 255).astype("uint8"), cv2.COLOR_RGB2BGR)
cv2.imshow("cam2 RGB decoded", rgb_bgr)
if "cam0" in decoded:
re01 = decoded["cam0"]["image"]
cv2.imshow("cam0 RE decoded", (re01 * 255).astype("uint8"))
if "cam1" in decoded:
nir01 = decoded["cam1"]["image"]
cv2.imshow("cam1 NIR decoded", (nir01 * 255).astype("uint8"))
if cv2.waitKey(1) in (27, ord("q")):
break
cv2.destroyAllWindows()

View File

@ -0,0 +1,37 @@
from core.oak_fcc3_service import OakFcc3Service
svc = OakFcc3Service(
fps=15,
width=640,
height=400,
frame_type="MULTISPEC",
capture_mode="TRIPLE",
raw_policy="require_triple",
sync_mode="best",
sync_tolerance_ms=25.0,
)
svc.connect()
svc.begin(frame_type="RAW_BRUTO", output_dtype="uint8", capture_mode="TRIPLE")
frame, meta = svc.capture_frame(timeout=2.0)
print(meta["camera_info"])
for cam_id, arr in frame.items():
print(cam_id, arr.shape, arr.dtype, arr.size)
from core.raw_processor_core import RawProcessorCore
from core.raw_processor_preview import RawProcessorPreview
import cv2
core = RawProcessorCore(sensor_width=1280, sensor_height=800, bayer_pattern="GBRG")
preview = RawProcessorPreview(sensor_width=1280, sensor_height=800, bayer_pattern="GBRG")
raw16 = core.unpack_raw10_packed(frame["cam0"], sensor_width=1280, sensor_height=800)
img = preview.raw16_to_preview_bgr(raw16, bit_depth=10)
cv2.imwrite("calibration/cam0_raw_preview.png", img)
print(raw16.shape, raw16.dtype, raw16.min(), raw16.max())
svc.stop()
svc.disconnect()

View File

@ -1,4 +1,4 @@
from oak_fcc3_client import OakFcc3Client
from core.oak_fcc3_client import OakFcc3Client
for frame_type in ["RAW_BRUTO", "RGB", "MULTISPEC"]:
print("\nTESTANDO:", frame_type)
@ -21,3 +21,9 @@ for frame_type in ["RAW_BRUTO", "RGB", "MULTISPEC"]:
print("channels:", meta.get("channels"))
print("shape:", getattr(frame, "shape", None))
print("dtype:", getattr(frame, "dtype", None))
print("payload_sources:", meta.get("payload_sources"))
print("active_roles:", cam.get_status().get("active_roles"))
print("decoded_roles:", {
cam_id: item.get("role")
for cam_id, item in decoded.items()
})

View File

@ -0,0 +1,44 @@
import cv2
from core.oak_fcc3_client import OakFcc3Client
with OakFcc3Client(
fps=15,
width=640,
height=400,
frame_type="RAW_BRUTO",
output_dtype="uint8",
capture_mode="AUTO",
raw_policy="require_triple",
sync_mode="best",
sync_tolerance_ms=25.0,
#module_calibration_json="calibration/module_params.json",
#radiometric_enabled=False
) as cam:
print("STATUS:", cam.get_status())
while True:
previews, meta = cam.get_next_preview(timeout=2.0)
print(
"frame_id:", meta["frame_id"],
"sources:", meta["payload_sources"],
"sync_ms:", f"{meta.get('sync_dt_ms', 0):.2f}",
"sync_ok:", meta.get("sync_ok"),
)
camera_info = meta.get("camera_info", {}) or {}
for cam_id, preview in previews.items():
info = camera_info.get(cam_id, {}) or {}
role = info.get("role", "unknown")
sensor = info.get("sensor", "")
title = f"{cam_id} | {role.upper()} | {sensor}"
cv2.imshow(title, preview)
if cv2.waitKey(1) in (27, ord("q")):
break
cv2.destroyAllWindows()

View File

@ -0,0 +1,50 @@
import time
import cv2
import numpy as np
from core.oak_fcc3_client import OakFcc3Client
with OakFcc3Client(
fps=15,
width=640,
height=400,
frame_type="RAW_BRUTO",
output_dtype="uint8",
capture_mode="AUTO",
raw_policy="allow_single",
sync_mode="best",
sync_tolerance_ms=25.0,
) as cam:
print("STATUS:", cam.get_status())
print("CTRL rgb:", cam.svc.get_camera_controls(role="rgb"))
print("CTRL nir:", cam.svc.get_camera_controls(role="nir"))
print("CTRL re:", cam.svc.get_camera_controls(role="re"))
print("SET rgb AE OFF:", cam.svc.set_ae_enable(role="rgb", enable=False))
print("SET rgb EXP:", cam.svc.set_exposure_time(role="rgb", exposure_time_us=8000))
print("SET rgb GAIN:", cam.svc.set_analogue_gain(role="rgb", analogue_gain=1.5))
print("SET nir EXP:", cam.svc.set_exposure_time(role="nir", exposure_time_us=12000))
print("SET re EXP:", cam.svc.set_exposure_time(role="re", exposure_time_us=12000))
while True:
frame, meta, decoded = cam.get_next_decoded(timeout=2.0)
for cam_id, item in decoded.items():
img = item["image"]
if img.ndim == 3:
show = cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)
else:
show = (img * 255).astype(np.uint8)
role = item.get("role", cam_id)
cv2.imshow(f"{cam_id} | {role.upper()}", show)
if cv2.waitKey(1) in (27, ord("q")):
break
cv2.destroyAllWindows()

View File

@ -2,7 +2,7 @@ import time
import cv2
import numpy as np
from oak_fcc3_service import OakFcc3Service
from core.oak_fcc3_service import OakFcc3Service
svc = OakFcc3Service(timeout=10)
@ -60,9 +60,15 @@ finally:
if last_frame is not None:
if isinstance(last_frame, dict) and "cam2" in last_frame:
cv2.imwrite("calibration/capture_cam2.jpg", last_frame["cam2"])
print("[OK] Salvo: calibration/capture_cam2.jpg")
if isinstance(last_frame, dict):
camera_info = (last_meta or {}).get("camera_info", {}) or {}
for cam_id, arr in last_frame.items():
role = camera_info.get(cam_id, {}).get("role", "unknown")
path = f"calibration/capture_{cam_id}_{role}.bin"
arr.tofile(path)
print(f"[OK] Salvo: {path}")
elif isinstance(last_frame, dict):
first_id = list(last_frame.keys())[0]

View File

@ -6,8 +6,8 @@ from pathlib import Path
import cv2
import numpy as np
from cam_3.pi.raw_processor_core import RawProcessorCore
from cam_3.pi.raw_processor_preview import RawProcessorPreview
from core.raw_processor_core import RawProcessorCore
from core.raw_processor_preview import RawProcessorPreview
def load_json(path: Path) -> dict:
@ -129,13 +129,32 @@ def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str
return preview_bgr, desc
if saved_type == "multispec":
if arr.ndim != 3 or arr.shape[0] < 3:
if arr.ndim != 3 or arr.shape[0] < 5:
raise RuntimeError(f"Payload MULTISPEC inválido, shape={arr.shape}")
rgb_hwc = chw_to_hwc(arr[:3].astype(np.float32))
preview_bgr = normalize_float01_to_bgr(rgb_hwc)
desc = f"Reconstruido de MULTISPEC salvo | dtype={arr.dtype} | shape={arr.shape}"
return preview_bgr, desc
re01 = arr[3].astype(np.float32)
nir01 = arr[4].astype(np.float32)
re_bgr = cv2.cvtColor(
np.clip(re01 * 255.0, 0, 255).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
nir_bgr = cv2.cvtColor(
np.clip(nir01 * 255.0, 0, 255).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
combined = np.vstack([
np.hstack([preview_bgr, re_bgr]),
np.hstack([nir_bgr, np.zeros_like(preview_bgr)])
])
desc = f"Reconstruido de MULTISPEC | dtype={arr.dtype} | shape={arr.shape} | canais=[R,G,B,RE,NIR]"
return combined, desc
if saved_type == "raw_native_single":
if arr.ndim == 3 and arr.shape[2] == 3 and arr.dtype == np.uint8:

View File

@ -7,7 +7,7 @@ from datetime import datetime
import cv2
import numpy as np
from oak_fcc3_client import OakFcc3Client as MultiSpectralClient
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
# ============================================================
@ -173,11 +173,12 @@ def validate_module_ready(status: dict, frame_type: str, raw_policy: str, captur
raise RuntimeError(f"Status inválido retornado pelo módulo: {status}")
active_ids = list(status.get("active_camera_ids", []))
active_roles = status.get("active_roles", {}) or {}
active_count = int(status.get("camera_count_active", 0))
if frame_type == "RAW_BRUTO":
if raw_policy == "require_triple":
missing = [cid for cid in ("cam0", "cam1", "cam2") if cid not in active_ids]
missing = [role for role in ("rgb", "nir", "re") if role not in active_roles]
if missing:
raise RuntimeError(
f"RAW_BRUTO com política require_triple exige três câmeras ativas. "
@ -199,9 +200,6 @@ def default_offsets_payload(args, effective_capture_mode: str):
return {
"schema": "manual_multispec_offsets_v1",
"saved_at": now_str(),
"pi_host": args.pi_host,
"pc_host": args.pc_host,
"stream_port": args.stream_port,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": args.capture_mode,
"capture_mode_effective": effective_capture_mode,
@ -209,16 +207,16 @@ def default_offsets_payload(args, effective_capture_mode: str):
"sensor_width": args.width,
"sensor_height": args.height,
"bayer_pattern": args.bayer,
"reference_camera": "cam2",
"baseline_mm": args.baseline_mm,
"alignment_mode": "manual_affine",
"reference_camera": "rgb",
"manual_offsets": {
"cam0": {"dx": 0, "dy": 0, "theta_deg": 0.0},
"cam1": {"dx": 0, "dy": 0, "theta_deg": 0.0},
"re": {"dx": 0, "dy": 0, "theta_deg": 0.0},
"nir": {"dx": 0, "dy": 0, "theta_deg": 0.0},
},
"homographies": {
"cam0_to_cam2": None,
"cam1_to_cam2": None,
"re_to_rgb": None,
"nir_to_rgb": None,
},
"notes": args.notes or "",
}
@ -232,15 +230,13 @@ def load_offsets_json(path: str, args, effective_capture_mode: str):
data = json.load(f)
data.setdefault("schema", "manual_multispec_offsets_v1")
data.setdefault("reference_camera", "cam2")
data.setdefault("baseline_mm", args.baseline_mm)
data.setdefault("alignment_mode", "manual_affine")
data.setdefault("manual_offsets", {})
data["manual_offsets"].setdefault("cam0", {"dx": 0, "dy": 0, "theta_deg": 0.0})
data["manual_offsets"].setdefault("cam1", {"dx": 0, "dy": 0, "theta_deg": 0.0})
data.setdefault("homographies", {})
data["homographies"].setdefault("cam0_to_cam2", None)
data["homographies"].setdefault("cam1_to_cam2", None)
data.setdefault("reference_camera", "rgb")
data["manual_offsets"].setdefault("re", {"dx": 0, "dy": 0, "theta_deg": 0.0})
data["manual_offsets"].setdefault("nir", {"dx": 0, "dy": 0, "theta_deg": 0.0})
data["homographies"].setdefault("re_to_rgb", None)
data["homographies"].setdefault("nir_to_rgb", None)
return data
@ -252,6 +248,14 @@ def save_offsets_json(path: str, data: dict):
json.dump(data, f, ensure_ascii=False, indent=2)
def get_decoded_by_role(decoded, role):
role = str(role).lower()
for cam_id, item in decoded.items():
if str(item.get("role", "")).lower() == role:
return cam_id, item
return None, None
# ============================================================
# Main UI
# ============================================================
@ -261,10 +265,6 @@ 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="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)
parser.add_argument("--height", type=int, default=480)
@ -327,23 +327,22 @@ def main():
offsets_data = load_offsets_json(args.load_json, args, effective_capture_mode)
offsets = offsets_data["manual_offsets"]
selected_cam = "cam0"
selected_role = "re"
calibration_mode = offsets_data.get("alignment_mode", "manual_affine")
selected_points_spec = {
"cam0": [],
"cam1": [],
"re": [],
"nir": [],
}
selected_points_rgb = {
"cam0": [],
"cam1": [],
"re": [],
"nir": [],
}
panel_rects = {
"fuse": None,
"rgb": None,
"cam0": None,
"cam1": None,
"re": None,
"nir": None,
}
last_msg = ""
last_msg_t = 0.0
@ -363,10 +362,6 @@ def main():
try:
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,
@ -376,6 +371,7 @@ def main():
capture_mode=effective_capture_mode,
raw_policy=args.raw_policy,
module_calibration_json=None,
radiometric_enabled=True
) as cam:
while True:
t0 = time.time()
@ -409,9 +405,13 @@ def main():
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")
rgb_id, rgb_item = get_decoded_by_role(decoded_last, "rgb")
re_id, re_item = get_decoded_by_role(decoded_last, "re")
nir_id, nir_item = get_decoded_by_role(decoded_last, "nir")
rgb01 = rgb_item.get("image") if rgb_item else None
re01 = re_item.get("image") if re_item else None
nir01 = nir_item.get("image") if nir_item else None
if rgb01 is None:
# fallback para exibição quando não houver RGB
@ -432,13 +432,13 @@ def main():
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_role == "cam0" else "NIR"
active_spec = re01 if selected_role == "cam0" else nir01
dx = int(offsets.get(selected_role, {}).get("dx", 0))
dy = int(offsets.get(selected_role, {}).get("dy", 0))
theta_deg = float(offsets.get(selected_role, {}).get("theta_deg", 0.0))
H_key = f"{selected_cam}_to_cam2"
H_key = f"{selected_role}_to_cam2"
H = offsets_data.get("homographies", {}).get(H_key)
fuse_panel = build_overlay_fuse(

View File

@ -7,7 +7,7 @@ from datetime import datetime
import cv2
import numpy as np
from cam_3.multispectral_client import MultiSpectralClient
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
# ============================================================
# Helpers
@ -444,9 +444,6 @@ def default_payload(args, effective_capture_mode: str):
return {
"schema": "manual_sensor_calibration_v1",
"saved_at": now_str(),
"pi_host": args.pi_host,
"pc_host": args.pc_host,
"stream_port": args.stream_port,
"frame_type": "RAW_BRUTO",
"capture_mode_requested": args.capture_mode,
"capture_mode_effective": effective_capture_mode,
@ -750,10 +747,6 @@ 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="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)
parser.add_argument("--height", type=int, default=480)
@ -775,10 +768,6 @@ def main():
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,
@ -788,6 +777,7 @@ def main():
capture_mode=args.capture_mode,
raw_policy=args.raw_policy,
module_calibration_json=None,
radiometric_enabled=True
)
offline_mode = bool(args.offline_sample_json)