reestruturado oak-ffc-3
|
|
@ -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()
|
||||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1000 KiB |
|
After Width: | Height: | Size: 1001 KiB |
|
|
@ -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": ""
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -31,8 +31,8 @@ class OakFcc3Manager:
|
||||||
|
|
||||||
self.roles = roles or {
|
self.roles = roles or {
|
||||||
"CAM_A": "rgb",
|
"CAM_A": "rgb",
|
||||||
"CAM_B": "re",
|
"CAM_B": "nir",
|
||||||
"CAM_C": "nir",
|
"CAM_C": "re",
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sync_mode = sync_mode
|
self.sync_mode = sync_mode
|
||||||
|
|
@ -47,7 +47,13 @@ class OakFcc3Manager:
|
||||||
|
|
||||||
self.running = False
|
self.running = False
|
||||||
self.frame_id = 0
|
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):
|
def __enter__(self):
|
||||||
self.start()
|
self.start()
|
||||||
|
|
@ -56,6 +62,9 @@ class OakFcc3Manager:
|
||||||
def __exit__(self, exc_type, exc, tb):
|
def __exit__(self, exc_type, exc, tb):
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
def _is_preview_mode(self):
|
||||||
|
return str(self.frame_type).upper() == "PREVIEW"
|
||||||
|
|
||||||
def list_cameras(self):
|
def list_cameras(self):
|
||||||
with dai.Device() as dev:
|
with dai.Device() as dev:
|
||||||
result = []
|
result = []
|
||||||
|
|
@ -67,6 +76,10 @@ class OakFcc3Manager:
|
||||||
})
|
})
|
||||||
return result
|
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):
|
def start(self):
|
||||||
if self.running:
|
if self.running:
|
||||||
return
|
return
|
||||||
|
|
@ -79,6 +92,7 @@ class OakFcc3Manager:
|
||||||
self.queues.clear()
|
self.queues.clear()
|
||||||
self.buffers.clear()
|
self.buffers.clear()
|
||||||
self.camera_info.clear()
|
self.camera_info.clear()
|
||||||
|
self._last_raw_dims.clear()
|
||||||
|
|
||||||
for f in features:
|
for f in features:
|
||||||
socket = f.socket
|
socket = f.socket
|
||||||
|
|
@ -88,17 +102,22 @@ class OakFcc3Manager:
|
||||||
print(f"[OAK] Criando câmera {socket_name} sensor={f.sensorName} role={role}")
|
print(f"[OAK] Criando câmera {socket_name} sensor={f.sensorName} role={role}")
|
||||||
|
|
||||||
cam = self.pipeline.create(dai.node.Camera).build(socket)
|
cam = self.pipeline.create(dai.node.Camera).build(socket)
|
||||||
|
ctrl_q = cam.inputControl.createInputQueue()
|
||||||
|
|
||||||
out = cam.requestOutput(
|
if self._is_preview_mode():
|
||||||
self.size,
|
out = cam.requestOutput(
|
||||||
fps=self.fps
|
self.size,
|
||||||
)
|
fps=self.fps
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
out = cam.raw
|
||||||
|
|
||||||
q = out.createOutputQueue()
|
q = out.createOutputQueue()
|
||||||
cam_id = self._socket_to_cam_id(socket_name)
|
cam_id = socket_name
|
||||||
|
|
||||||
self.queues[cam_id] = q
|
self.queues[cam_id] = q
|
||||||
self.buffers[cam_id] = deque(maxlen=self.buffer_size)
|
self.buffers[cam_id] = deque(maxlen=self.buffer_size)
|
||||||
|
self.control_queues[cam_id] = ctrl_q
|
||||||
|
|
||||||
self.camera_info[cam_id] = {
|
self.camera_info[cam_id] = {
|
||||||
"id": cam_id,
|
"id": cam_id,
|
||||||
|
|
@ -133,6 +152,8 @@ class OakFcc3Manager:
|
||||||
self.queues.clear()
|
self.queues.clear()
|
||||||
self.buffers.clear()
|
self.buffers.clear()
|
||||||
self.camera_info.clear()
|
self.camera_info.clear()
|
||||||
|
self.control_queues.clear()
|
||||||
|
self._last_raw_dims.clear()
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
|
|
@ -178,40 +199,6 @@ class OakFcc3Manager:
|
||||||
f"Tente aumentar para 25 ou 35 ms para diagnóstico."
|
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):
|
def _drain_queues_to_buffers(self):
|
||||||
for cam_id, q in self.queues.items():
|
for cam_id, q in self.queues.items():
|
||||||
while q.has():
|
while q.has():
|
||||||
|
|
@ -222,7 +209,32 @@ class OakFcc3Manager:
|
||||||
except Exception:
|
except Exception:
|
||||||
ts = time.time()
|
ts = time.time()
|
||||||
|
|
||||||
frame = msg.getCvFrame()
|
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({
|
self.buffers[cam_id].append({
|
||||||
"frame": frame,
|
"frame": frame,
|
||||||
|
|
@ -288,7 +300,7 @@ class OakFcc3Manager:
|
||||||
return frames, timestamps, sync_dt_ms, sync_ok
|
return frames, timestamps, sync_dt_ms, sync_ok
|
||||||
|
|
||||||
def _get_required_cam_ids(self):
|
def _get_required_cam_ids(self):
|
||||||
available = list(self.queues.keys())
|
available = self._get_available_cam_ids_ordered()
|
||||||
|
|
||||||
if self.capture_mode == "SINGLE":
|
if self.capture_mode == "SINGLE":
|
||||||
return available[:1]
|
return available[:1]
|
||||||
|
|
@ -319,6 +331,37 @@ class OakFcc3Manager:
|
||||||
for cam_id, arr in frames.items()
|
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 {
|
return {
|
||||||
"frame_id": self.frame_id,
|
"frame_id": self.frame_id,
|
||||||
"backend": "oak_fcc3",
|
"backend": "oak_fcc3",
|
||||||
|
|
@ -328,7 +371,7 @@ class OakFcc3Manager:
|
||||||
"dtype": self.output_dtype,
|
"dtype": self.output_dtype,
|
||||||
"output_layout": "dict_by_camera",
|
"output_layout": "dict_by_camera",
|
||||||
"payload_sources": payload_sources,
|
"payload_sources": payload_sources,
|
||||||
"camera_info": self.camera_info,
|
"camera_info": camera_info,
|
||||||
"timestamps": timestamps,
|
"timestamps": timestamps,
|
||||||
"sync_dt_ms": sync_dt_ms,
|
"sync_dt_ms": sync_dt_ms,
|
||||||
"sync_ok": sync_ok,
|
"sync_ok": sync_ok,
|
||||||
|
|
@ -353,11 +396,152 @@ class OakFcc3Manager:
|
||||||
if self.raw_policy == "require_triple" and n < 3:
|
if self.raw_policy == "require_triple" and n < 3:
|
||||||
raise RuntimeError(f"raw_policy=require_triple exige 3 câmeras, mas detectou {n}.")
|
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
|
@staticmethod
|
||||||
def _socket_to_cam_id(socket_name):
|
def _gain_to_iso(gain: float) -> int:
|
||||||
mapping = {
|
# DepthAI usa ISO no setManualExposure(exposure_us, sensitivity_iso).
|
||||||
"CAM_A": "cam2", # RGB
|
# Mantemos analogue_gain estilo Pi e convertemos para ISO aproximado.
|
||||||
"CAM_B": "cam0", # RE
|
gain = max(1.0, float(gain))
|
||||||
"CAM_C": "cam1", # NIR
|
iso = int(round(gain * 100))
|
||||||
}
|
return max(100, min(1600, iso))
|
||||||
return mapping.get(socket_name, socket_name.lower())
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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))
|
||||||
|
|
@ -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 já pronto
|
||||||
|
- MULTISPEC uint8/float32 já pronto
|
||||||
|
- RAW_BRUTO multi_payload (cam2 RGB + cam0/cam1 packed)
|
||||||
|
"""
|
||||||
|
frame_type = meta.get("frame_type")
|
||||||
|
dtype_str = meta.get("dtype") or meta.get("output_dtype", "uint8")
|
||||||
|
camera_frames = meta.get("camera_frames", {}) or {}
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# RAW_BRUTO multi_payload
|
||||||
|
# -------------------------------------------------
|
||||||
|
if frame_type == "RAW_BRUTO":
|
||||||
|
if not isinstance(frame, dict):
|
||||||
|
raise RuntimeError("RAW_BRUTO esperado como dict de câmeras no modo multi")
|
||||||
|
|
||||||
|
arrays = []
|
||||||
|
channel_names = []
|
||||||
|
|
||||||
|
# RGB USB
|
||||||
|
if "cam2" in frame:
|
||||||
|
rgb_bgr = frame["cam2"]
|
||||||
|
if rgb_bgr.ndim != 3 or rgb_bgr.shape[2] != 3:
|
||||||
|
raise RuntimeError(f"cam2 RGB inválida: shape={rgb_bgr.shape}")
|
||||||
|
|
||||||
|
rgb = rgb_bgr[:, :, ::-1].astype(np.float32) / 255.0
|
||||||
|
rgb_chw = np.transpose(rgb, (2, 0, 1))
|
||||||
|
arrays.append(rgb_chw)
|
||||||
|
channel_names.extend(["R", "G", "B"])
|
||||||
|
else:
|
||||||
|
raise RuntimeError("RAW_BRUTO para inferência precisa incluir cam2 (RGB)")
|
||||||
|
|
||||||
|
# RE / NIR
|
||||||
|
for cam_id, spec_name in (("cam0", "RE"), ("cam1", "NIR")):
|
||||||
|
if cam_id not in frame:
|
||||||
|
continue
|
||||||
|
|
||||||
|
packed = frame[cam_id]
|
||||||
|
if packed.ndim == 3 and packed.shape[2] == 1:
|
||||||
|
packed = packed[:, :, 0]
|
||||||
|
|
||||||
|
cam_meta = camera_frames.get(cam_id, {})
|
||||||
|
packed_width = int(cam_meta.get("width", packed.shape[1]))
|
||||||
|
height = int(cam_meta.get("height", packed.shape[0]))
|
||||||
|
bayer = cam_meta.get("bayer_pattern", self.bayer_pattern)
|
||||||
|
bit_depth = int(cam_meta.get("bit_depth", 10))
|
||||||
|
|
||||||
|
if bit_depth == 10:
|
||||||
|
real_width = int((packed_width * 8) / 10)
|
||||||
|
else:
|
||||||
|
real_width = packed_width
|
||||||
|
|
||||||
|
rp = RawProcessorCore(
|
||||||
|
sensor_width=real_width,
|
||||||
|
sensor_height=height,
|
||||||
|
bayer_pattern=bayer,
|
||||||
|
)
|
||||||
|
|
||||||
|
raw16 = rp.unpack_raw10_packed(packed)
|
||||||
|
|
||||||
|
max_val = float((1 << bit_depth) - 1)
|
||||||
|
single = np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)[None, :, :]
|
||||||
|
|
||||||
|
arrays.append(single)
|
||||||
|
channel_names.append(spec_name)
|
||||||
|
|
||||||
|
if len(arrays) < 2:
|
||||||
|
raise RuntimeError("RAW_BRUTO requer RGB + pelo menos um canal espectral para inferência")
|
||||||
|
|
||||||
|
min_h = min(a.shape[1] for a in arrays)
|
||||||
|
min_w = min(a.shape[2] for a in arrays)
|
||||||
|
arrays = [a[:, :min_h, :min_w] for a in arrays]
|
||||||
|
|
||||||
|
raw_np = np.concatenate(arrays, axis=0)
|
||||||
|
|
||||||
|
if raw_np.shape[0] != channels_expected:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Tensor RAW_BRUTO montado com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected} | got={channel_names}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return raw_np
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# RGB ou MULTISPEC já pronto
|
||||||
|
# -------------------------------------------------
|
||||||
|
if frame_type == "RGB" or frame_type == "MULTISPEC":
|
||||||
|
if not isinstance(frame, np.ndarray):
|
||||||
|
raise RuntimeError(f"Frame {frame_type} esperado como ndarray")
|
||||||
|
|
||||||
|
if frame.ndim != 3:
|
||||||
|
raise RuntimeError(f"Frame {frame_type} inválido: shape={frame.shape}")
|
||||||
|
|
||||||
|
if dtype_str == "uint8":
|
||||||
|
raw_np = frame.astype(np.float32) / 255.0
|
||||||
|
elif dtype_str == "float32":
|
||||||
|
raw_np = frame.astype(np.float32)
|
||||||
|
elif dtype_str == "uint16":
|
||||||
|
raw_np = frame.astype(np.float32) / 65535.0
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"dtype {frame_type} não suportado: {dtype_str}")
|
||||||
|
|
||||||
|
if raw_np.shape[0] != channels_expected:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Frame {frame_type} com canais inesperados: {raw_np.shape[0]} | esperado={channels_expected}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return raw_np
|
||||||
|
|
||||||
|
|
||||||
|
raise RuntimeError(f"frame_type não suportado para inferência: {frame_type}")
|
||||||
|
|
||||||
|
def build_infer_tensor_from_stream(self, frame, meta, channels_expected, 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)
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 210 KiB |
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 257 KiB |
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 162 KiB |
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 196 KiB |
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 209 KiB |
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 178 KiB |
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 144 KiB |
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 132 KiB |
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 170 KiB |
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 283 KiB |
|
|
@ -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)
|
|
||||||
|
|
@ -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."
|
|
||||||
)
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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"]:
|
for frame_type in ["RAW_BRUTO", "RGB", "MULTISPEC"]:
|
||||||
print("\nTESTANDO:", frame_type)
|
print("\nTESTANDO:", frame_type)
|
||||||
|
|
@ -21,3 +21,9 @@ for frame_type in ["RAW_BRUTO", "RGB", "MULTISPEC"]:
|
||||||
print("channels:", meta.get("channels"))
|
print("channels:", meta.get("channels"))
|
||||||
print("shape:", getattr(frame, "shape", None))
|
print("shape:", getattr(frame, "shape", None))
|
||||||
print("dtype:", getattr(frame, "dtype", 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()
|
||||||
|
})
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -2,7 +2,7 @@ import time
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from oak_fcc3_service import OakFcc3Service
|
from core.oak_fcc3_service import OakFcc3Service
|
||||||
|
|
||||||
|
|
||||||
svc = OakFcc3Service(timeout=10)
|
svc = OakFcc3Service(timeout=10)
|
||||||
|
|
@ -60,9 +60,15 @@ finally:
|
||||||
|
|
||||||
|
|
||||||
if last_frame is not None:
|
if last_frame is not None:
|
||||||
if isinstance(last_frame, dict) and "cam2" in last_frame:
|
if isinstance(last_frame, dict):
|
||||||
cv2.imwrite("calibration/capture_cam2.jpg", last_frame["cam2"])
|
camera_info = (last_meta or {}).get("camera_info", {}) or {}
|
||||||
print("[OK] Salvo: calibration/capture_cam2.jpg")
|
|
||||||
|
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):
|
elif isinstance(last_frame, dict):
|
||||||
first_id = list(last_frame.keys())[0]
|
first_id = list(last_frame.keys())[0]
|
||||||
|
|
@ -6,8 +6,8 @@ from pathlib import Path
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from cam_3.pi.raw_processor_core import RawProcessorCore
|
from core.raw_processor_core import RawProcessorCore
|
||||||
from cam_3.pi.raw_processor_preview import RawProcessorPreview
|
from core.raw_processor_preview import RawProcessorPreview
|
||||||
|
|
||||||
|
|
||||||
def load_json(path: Path) -> dict:
|
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
|
return preview_bgr, desc
|
||||||
|
|
||||||
if saved_type == "multispec":
|
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}")
|
raise RuntimeError(f"Payload MULTISPEC inválido, shape={arr.shape}")
|
||||||
|
|
||||||
rgb_hwc = chw_to_hwc(arr[:3].astype(np.float32))
|
rgb_hwc = chw_to_hwc(arr[:3].astype(np.float32))
|
||||||
preview_bgr = normalize_float01_to_bgr(rgb_hwc)
|
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 saved_type == "raw_native_single":
|
||||||
if arr.ndim == 3 and arr.shape[2] == 3 and arr.dtype == np.uint8:
|
if arr.ndim == 3 and arr.shape[2] == 3 and arr.dtype == np.uint8:
|
||||||
|
|
@ -7,7 +7,7 @@ from datetime import datetime
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
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}")
|
raise RuntimeError(f"Status inválido retornado pelo módulo: {status}")
|
||||||
|
|
||||||
active_ids = list(status.get("active_camera_ids", []))
|
active_ids = list(status.get("active_camera_ids", []))
|
||||||
|
active_roles = status.get("active_roles", {}) or {}
|
||||||
active_count = int(status.get("camera_count_active", 0))
|
active_count = int(status.get("camera_count_active", 0))
|
||||||
|
|
||||||
if frame_type == "RAW_BRUTO":
|
if frame_type == "RAW_BRUTO":
|
||||||
if raw_policy == "require_triple":
|
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:
|
if missing:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"RAW_BRUTO com política require_triple exige três câmeras ativas. "
|
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 {
|
return {
|
||||||
"schema": "manual_multispec_offsets_v1",
|
"schema": "manual_multispec_offsets_v1",
|
||||||
"saved_at": now_str(),
|
"saved_at": now_str(),
|
||||||
"pi_host": args.pi_host,
|
|
||||||
"pc_host": args.pc_host,
|
|
||||||
"stream_port": args.stream_port,
|
|
||||||
"frame_type": "RAW_BRUTO",
|
"frame_type": "RAW_BRUTO",
|
||||||
"capture_mode_requested": args.capture_mode,
|
"capture_mode_requested": args.capture_mode,
|
||||||
"capture_mode_effective": effective_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_width": args.width,
|
||||||
"sensor_height": args.height,
|
"sensor_height": args.height,
|
||||||
"bayer_pattern": args.bayer,
|
"bayer_pattern": args.bayer,
|
||||||
"reference_camera": "cam2",
|
|
||||||
"baseline_mm": args.baseline_mm,
|
"baseline_mm": args.baseline_mm,
|
||||||
"alignment_mode": "manual_affine",
|
"alignment_mode": "manual_affine",
|
||||||
|
"reference_camera": "rgb",
|
||||||
"manual_offsets": {
|
"manual_offsets": {
|
||||||
"cam0": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
"re": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
||||||
"cam1": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
"nir": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
||||||
},
|
},
|
||||||
"homographies": {
|
"homographies": {
|
||||||
"cam0_to_cam2": None,
|
"re_to_rgb": None,
|
||||||
"cam1_to_cam2": None,
|
"nir_to_rgb": None,
|
||||||
},
|
},
|
||||||
"notes": args.notes or "",
|
"notes": args.notes or "",
|
||||||
}
|
}
|
||||||
|
|
@ -232,15 +230,13 @@ def load_offsets_json(path: str, args, effective_capture_mode: str):
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
data.setdefault("schema", "manual_multispec_offsets_v1")
|
data.setdefault("schema", "manual_multispec_offsets_v1")
|
||||||
data.setdefault("reference_camera", "cam2")
|
|
||||||
data.setdefault("baseline_mm", args.baseline_mm)
|
data.setdefault("baseline_mm", args.baseline_mm)
|
||||||
data.setdefault("alignment_mode", "manual_affine")
|
data.setdefault("alignment_mode", "manual_affine")
|
||||||
data.setdefault("manual_offsets", {})
|
data.setdefault("reference_camera", "rgb")
|
||||||
data["manual_offsets"].setdefault("cam0", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
data["manual_offsets"].setdefault("re", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
||||||
data["manual_offsets"].setdefault("cam1", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
data["manual_offsets"].setdefault("nir", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
||||||
data.setdefault("homographies", {})
|
data["homographies"].setdefault("re_to_rgb", None)
|
||||||
data["homographies"].setdefault("cam0_to_cam2", None)
|
data["homographies"].setdefault("nir_to_rgb", None)
|
||||||
data["homographies"].setdefault("cam1_to_cam2", None)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -252,6 +248,14 @@ def save_offsets_json(path: str, data: dict):
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
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
|
# 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.",
|
description="Calibrador manual de offsets para fusão RGB/RE/NIR a partir do stream RAW_BRUTO.",
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
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("--fps", type=int, default=20)
|
||||||
parser.add_argument("--width", type=int, default=640)
|
parser.add_argument("--width", type=int, default=640)
|
||||||
parser.add_argument("--height", type=int, default=480)
|
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_data = load_offsets_json(args.load_json, args, effective_capture_mode)
|
||||||
offsets = offsets_data["manual_offsets"]
|
offsets = offsets_data["manual_offsets"]
|
||||||
|
|
||||||
selected_cam = "cam0"
|
selected_role = "re"
|
||||||
calibration_mode = offsets_data.get("alignment_mode", "manual_affine")
|
calibration_mode = offsets_data.get("alignment_mode", "manual_affine")
|
||||||
|
|
||||||
selected_points_spec = {
|
selected_points_spec = {
|
||||||
"cam0": [],
|
"re": [],
|
||||||
"cam1": [],
|
"nir": [],
|
||||||
}
|
}
|
||||||
selected_points_rgb = {
|
selected_points_rgb = {
|
||||||
"cam0": [],
|
"re": [],
|
||||||
"cam1": [],
|
"nir": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
panel_rects = {
|
panel_rects = {
|
||||||
"fuse": None,
|
"fuse": None,
|
||||||
"rgb": None,
|
"rgb": None,
|
||||||
"cam0": None,
|
"re": None,
|
||||||
"cam1": None,
|
"nir": None,
|
||||||
}
|
}
|
||||||
last_msg = ""
|
last_msg = ""
|
||||||
last_msg_t = 0.0
|
last_msg_t = 0.0
|
||||||
|
|
@ -363,10 +362,6 @@ def main():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with MultiSpectralClient(
|
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,
|
width=args.width,
|
||||||
height=args.height,
|
height=args.height,
|
||||||
bayer=args.bayer,
|
bayer=args.bayer,
|
||||||
|
|
@ -376,6 +371,7 @@ def main():
|
||||||
capture_mode=effective_capture_mode,
|
capture_mode=effective_capture_mode,
|
||||||
raw_policy=args.raw_policy,
|
raw_policy=args.raw_policy,
|
||||||
module_calibration_json=None,
|
module_calibration_json=None,
|
||||||
|
radiometric_enabled=True
|
||||||
) as cam:
|
) as cam:
|
||||||
while True:
|
while True:
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
|
|
@ -409,9 +405,13 @@ def main():
|
||||||
t_view_fps = time.time()
|
t_view_fps = time.time()
|
||||||
|
|
||||||
if decoded_last:
|
if decoded_last:
|
||||||
rgb01 = decoded_last.get("cam2", {}).get("image")
|
rgb_id, rgb_item = get_decoded_by_role(decoded_last, "rgb")
|
||||||
re01 = decoded_last.get("cam0", {}).get("image")
|
re_id, re_item = get_decoded_by_role(decoded_last, "re")
|
||||||
nir01 = decoded_last.get("cam1", {}).get("image")
|
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:
|
if rgb01 is None:
|
||||||
# fallback para exibição quando não houver RGB
|
# 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")
|
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")
|
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_name = "RE" if selected_role == "cam0" else "NIR"
|
||||||
active_spec = re01 if selected_cam == "cam0" else nir01
|
active_spec = re01 if selected_role == "cam0" else nir01
|
||||||
dx = int(offsets.get(selected_cam, {}).get("dx", 0))
|
dx = int(offsets.get(selected_role, {}).get("dx", 0))
|
||||||
dy = int(offsets.get(selected_cam, {}).get("dy", 0))
|
dy = int(offsets.get(selected_role, {}).get("dy", 0))
|
||||||
theta_deg = float(offsets.get(selected_cam, {}).get("theta_deg", 0.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)
|
H = offsets_data.get("homographies", {}).get(H_key)
|
||||||
|
|
||||||
fuse_panel = build_overlay_fuse(
|
fuse_panel = build_overlay_fuse(
|
||||||
|
|
@ -7,7 +7,7 @@ from datetime import datetime
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from cam_3.multispectral_client import MultiSpectralClient
|
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
@ -444,9 +444,6 @@ def default_payload(args, effective_capture_mode: str):
|
||||||
return {
|
return {
|
||||||
"schema": "manual_sensor_calibration_v1",
|
"schema": "manual_sensor_calibration_v1",
|
||||||
"saved_at": now_str(),
|
"saved_at": now_str(),
|
||||||
"pi_host": args.pi_host,
|
|
||||||
"pc_host": args.pc_host,
|
|
||||||
"stream_port": args.stream_port,
|
|
||||||
"frame_type": "RAW_BRUTO",
|
"frame_type": "RAW_BRUTO",
|
||||||
"capture_mode_requested": args.capture_mode,
|
"capture_mode_requested": args.capture_mode,
|
||||||
"capture_mode_effective": effective_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.",
|
description="Ferramenta de calibração dos sensores RGB/RE/NIR com controle manual e ROIs em tempo real.",
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
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("--fps", type=int, default=20)
|
||||||
parser.add_argument("--width", type=int, default=640)
|
parser.add_argument("--width", type=int, default=640)
|
||||||
parser.add_argument("--height", type=int, default=480)
|
parser.add_argument("--height", type=int, default=480)
|
||||||
|
|
@ -775,10 +768,6 @@ def main():
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
cam = MultiSpectralClient(
|
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,
|
width=args.width,
|
||||||
height=args.height,
|
height=args.height,
|
||||||
bayer=args.bayer,
|
bayer=args.bayer,
|
||||||
|
|
@ -788,6 +777,7 @@ def main():
|
||||||
capture_mode=args.capture_mode,
|
capture_mode=args.capture_mode,
|
||||||
raw_policy=args.raw_policy,
|
raw_policy=args.raw_policy,
|
||||||
module_calibration_json=None,
|
module_calibration_json=None,
|
||||||
|
radiometric_enabled=True
|
||||||
)
|
)
|
||||||
|
|
||||||
offline_mode = bool(args.offline_sample_json)
|
offline_mode = bool(args.offline_sample_json)
|
||||||