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 {
|
||||
"CAM_A": "rgb",
|
||||
"CAM_B": "re",
|
||||
"CAM_C": "nir",
|
||||
"CAM_B": "nir",
|
||||
"CAM_C": "re",
|
||||
}
|
||||
|
||||
self.sync_mode = sync_mode
|
||||
|
|
@ -47,7 +47,13 @@ class OakFcc3Manager:
|
|||
|
||||
self.running = False
|
||||
self.frame_id = 0
|
||||
self.applied_camera_controls = {}
|
||||
self.control_queues = {}
|
||||
self.camera_controls = {
|
||||
"CAM_A": {"ae_enable": True, "awb_enable": True, "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": [1.0, 1.0]},
|
||||
"CAM_B": {"ae_enable": False, "awb_enable": False, "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": None},
|
||||
"CAM_C": {"ae_enable": False, "awb_enable": False, "exposure_time_us": 15000, "analogue_gain": 1.0, "colour_gains": None},
|
||||
}
|
||||
self._last_raw_dims = {}
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
|
|
@ -56,6 +62,9 @@ class OakFcc3Manager:
|
|||
def __exit__(self, exc_type, exc, tb):
|
||||
self.stop()
|
||||
|
||||
def _is_preview_mode(self):
|
||||
return str(self.frame_type).upper() == "PREVIEW"
|
||||
|
||||
def list_cameras(self):
|
||||
with dai.Device() as dev:
|
||||
result = []
|
||||
|
|
@ -67,6 +76,10 @@ class OakFcc3Manager:
|
|||
})
|
||||
return result
|
||||
|
||||
def _get_available_cam_ids_ordered(self):
|
||||
preferred_order = ["CAM_A", "CAM_B", "CAM_C"] # RGB, NIR, RE
|
||||
return [cam_id for cam_id in preferred_order if cam_id in self.queues]
|
||||
|
||||
def start(self):
|
||||
if self.running:
|
||||
return
|
||||
|
|
@ -79,6 +92,7 @@ class OakFcc3Manager:
|
|||
self.queues.clear()
|
||||
self.buffers.clear()
|
||||
self.camera_info.clear()
|
||||
self._last_raw_dims.clear()
|
||||
|
||||
for f in features:
|
||||
socket = f.socket
|
||||
|
|
@ -88,17 +102,22 @@ class OakFcc3Manager:
|
|||
print(f"[OAK] Criando câmera {socket_name} sensor={f.sensorName} role={role}")
|
||||
|
||||
cam = self.pipeline.create(dai.node.Camera).build(socket)
|
||||
ctrl_q = cam.inputControl.createInputQueue()
|
||||
|
||||
if self._is_preview_mode():
|
||||
out = cam.requestOutput(
|
||||
self.size,
|
||||
fps=self.fps
|
||||
)
|
||||
else:
|
||||
out = cam.raw
|
||||
|
||||
q = out.createOutputQueue()
|
||||
cam_id = self._socket_to_cam_id(socket_name)
|
||||
cam_id = socket_name
|
||||
|
||||
self.queues[cam_id] = q
|
||||
self.buffers[cam_id] = deque(maxlen=self.buffer_size)
|
||||
self.control_queues[cam_id] = ctrl_q
|
||||
|
||||
self.camera_info[cam_id] = {
|
||||
"id": cam_id,
|
||||
|
|
@ -133,6 +152,8 @@ class OakFcc3Manager:
|
|||
self.queues.clear()
|
||||
self.buffers.clear()
|
||||
self.camera_info.clear()
|
||||
self.control_queues.clear()
|
||||
self._last_raw_dims.clear()
|
||||
self.running = False
|
||||
|
||||
def get_status(self):
|
||||
|
|
@ -178,40 +199,6 @@ class OakFcc3Manager:
|
|||
f"Tente aumentar para 25 ou 35 ms para diagnóstico."
|
||||
)
|
||||
|
||||
def get_next_decoded(self, timeout=1.0, update_radiometry=True):
|
||||
frame, meta = self.get_next_frame(timeout=timeout)
|
||||
|
||||
decoded = {
|
||||
"frames": frame,
|
||||
"meta": meta,
|
||||
}
|
||||
|
||||
return frame, meta, decoded
|
||||
|
||||
def build_preview_from_raw_payload(self, frame, meta):
|
||||
if not isinstance(frame, dict) or not frame:
|
||||
raise RuntimeError("Payload inválido para preview.")
|
||||
|
||||
rgb_cam_id = None
|
||||
|
||||
for cam_id, info in self.camera_info.items():
|
||||
if info.get("role") == "rgb" and cam_id in frame:
|
||||
rgb_cam_id = cam_id
|
||||
break
|
||||
|
||||
preview_source_id = rgb_cam_id or list(frame.keys())[0]
|
||||
preview = frame[preview_source_id]
|
||||
|
||||
if preview.ndim == 2:
|
||||
import cv2
|
||||
preview_bgr = cv2.cvtColor(preview, cv2.COLOR_GRAY2BGR)
|
||||
else:
|
||||
preview_bgr = preview.copy()
|
||||
|
||||
preview_float = preview_bgr.astype(np.float32) / 255.0
|
||||
|
||||
return preview_bgr, preview_float, preview_source_id
|
||||
|
||||
def _drain_queues_to_buffers(self):
|
||||
for cam_id, q in self.queues.items():
|
||||
while q.has():
|
||||
|
|
@ -222,7 +209,32 @@ class OakFcc3Manager:
|
|||
except Exception:
|
||||
ts = time.time()
|
||||
|
||||
if self._is_preview_mode():
|
||||
frame = msg.getCvFrame()
|
||||
else:
|
||||
data = msg.getData()
|
||||
raw = np.frombuffer(data, dtype=np.uint8).copy()
|
||||
|
||||
h = int(msg.getHeight())
|
||||
w = int(msg.getWidth())
|
||||
stride = int(msg.getStride())
|
||||
|
||||
expected = h * stride
|
||||
|
||||
if raw.size < expected:
|
||||
raise RuntimeError(
|
||||
f"RAW menor que esperado: raw.size={raw.size}, esperado={expected}, "
|
||||
f"w={w}, h={h}, stride={stride}"
|
||||
)
|
||||
|
||||
frame = raw[:expected].reshape((h, stride))
|
||||
|
||||
self._last_raw_dims[cam_id] = {
|
||||
"sensor_width": w,
|
||||
"sensor_height": h,
|
||||
"stride": stride,
|
||||
"packed_width": stride,
|
||||
}
|
||||
|
||||
self.buffers[cam_id].append({
|
||||
"frame": frame,
|
||||
|
|
@ -288,7 +300,7 @@ class OakFcc3Manager:
|
|||
return frames, timestamps, sync_dt_ms, sync_ok
|
||||
|
||||
def _get_required_cam_ids(self):
|
||||
available = list(self.queues.keys())
|
||||
available = self._get_available_cam_ids_ordered()
|
||||
|
||||
if self.capture_mode == "SINGLE":
|
||||
return available[:1]
|
||||
|
|
@ -319,6 +331,37 @@ class OakFcc3Manager:
|
|||
for cam_id, arr in frames.items()
|
||||
}
|
||||
|
||||
camera_info = {}
|
||||
for cam_id, info in self.camera_info.items():
|
||||
item = dict(info)
|
||||
arr = frames.get(cam_id)
|
||||
|
||||
if arr is not None:
|
||||
item["shape"] = list(arr.shape)
|
||||
item["dtype"] = str(arr.dtype)
|
||||
|
||||
if self._is_preview_mode():
|
||||
item["interface"] = "OAK"
|
||||
item["channels"] = 3 if arr.ndim == 3 else 1
|
||||
item["bit_depth"] = 8 if arr.dtype == np.uint8 else 16
|
||||
item["height"] = int(arr.shape[0])
|
||||
item["width"] = int(arr.shape[1])
|
||||
else:
|
||||
raw_dims = self._last_raw_dims.get(cam_id, {})
|
||||
item["interface"] = "OAK_RAW"
|
||||
item["raw_format"] = "RAW10_PACKED"
|
||||
item["channels"] = 1
|
||||
item["bit_depth"] = 10
|
||||
item["height"] = int(raw_dims.get("sensor_height", arr.shape[0]))
|
||||
item["width"] = int(raw_dims.get("sensor_width", arr.shape[1]))
|
||||
item["stride"] = int(raw_dims.get("stride", arr.shape[1]))
|
||||
item["packed_width"] = int(raw_dims.get("packed_width", arr.shape[1]))
|
||||
item["shape"] = list(arr.shape)
|
||||
item["dtype"] = str(arr.dtype)
|
||||
item["packed"] = True
|
||||
|
||||
camera_info[cam_id] = item
|
||||
|
||||
return {
|
||||
"frame_id": self.frame_id,
|
||||
"backend": "oak_fcc3",
|
||||
|
|
@ -328,7 +371,7 @@ class OakFcc3Manager:
|
|||
"dtype": self.output_dtype,
|
||||
"output_layout": "dict_by_camera",
|
||||
"payload_sources": payload_sources,
|
||||
"camera_info": self.camera_info,
|
||||
"camera_info": camera_info,
|
||||
"timestamps": timestamps,
|
||||
"sync_dt_ms": sync_dt_ms,
|
||||
"sync_ok": sync_ok,
|
||||
|
|
@ -353,11 +396,152 @@ class OakFcc3Manager:
|
|||
if self.raw_policy == "require_triple" and n < 3:
|
||||
raise RuntimeError(f"raw_policy=require_triple exige 3 câmeras, mas detectou {n}.")
|
||||
|
||||
def get_camera_controls(self, cam_id):
|
||||
self._validate_cam_id_known(cam_id)
|
||||
return dict(self.camera_controls.get(cam_id, {}))
|
||||
|
||||
def set_ae_enable(self, cam_id, enable: bool):
|
||||
self._validate_cam_id_running(cam_id)
|
||||
|
||||
enable = bool(enable)
|
||||
ctrl_state = self.camera_controls[cam_id]
|
||||
ctrl_state["ae_enable"] = enable
|
||||
|
||||
ctrl = dai.CameraControl()
|
||||
|
||||
if enable:
|
||||
# Em alguns builds v3 esse método existe.
|
||||
if hasattr(ctrl, "setAutoExposureEnable"):
|
||||
ctrl.setAutoExposureEnable()
|
||||
else:
|
||||
# fallback: deixa AE assumir por região/algoritmo interno quando possível
|
||||
pass
|
||||
else:
|
||||
exp_us = int(ctrl_state.get("exposure_time_us") or 15000)
|
||||
gain = float(ctrl_state.get("analogue_gain") or 1.0)
|
||||
ctrl.setManualExposure(exp_us, self._gain_to_iso(gain))
|
||||
|
||||
self._send_control(cam_id, ctrl)
|
||||
|
||||
return dict(ctrl_state)
|
||||
|
||||
def set_awb_enable(self, cam_id, enable: bool):
|
||||
self._validate_cam_id_running(cam_id)
|
||||
|
||||
enable = bool(enable)
|
||||
ctrl_state = self.camera_controls[cam_id]
|
||||
ctrl_state["awb_enable"] = enable
|
||||
|
||||
ctrl = dai.CameraControl()
|
||||
|
||||
if hasattr(dai.CameraControl, "AutoWhiteBalanceMode"):
|
||||
if enable:
|
||||
ctrl.setAutoWhiteBalanceMode(dai.CameraControl.AutoWhiteBalanceMode.AUTO)
|
||||
else:
|
||||
ctrl.setAutoWhiteBalanceMode(dai.CameraControl.AutoWhiteBalanceMode.OFF)
|
||||
|
||||
self._send_control(cam_id, ctrl)
|
||||
|
||||
return dict(ctrl_state)
|
||||
|
||||
def set_exposure_time(self, cam_id, exposure_time_us: int):
|
||||
self._validate_cam_id_running(cam_id)
|
||||
|
||||
ctrl_state = self.camera_controls[cam_id]
|
||||
exposure_time_us = int(exposure_time_us)
|
||||
exposure_time_us = max(1, exposure_time_us)
|
||||
|
||||
ctrl_state["exposure_time_us"] = exposure_time_us
|
||||
ctrl_state["ae_enable"] = False
|
||||
|
||||
gain = float(ctrl_state.get("analogue_gain") or 1.0)
|
||||
|
||||
ctrl = dai.CameraControl()
|
||||
ctrl.setManualExposure(exposure_time_us, self._gain_to_iso(gain))
|
||||
|
||||
self._send_control(cam_id, ctrl)
|
||||
|
||||
return dict(ctrl_state)
|
||||
|
||||
def set_analogue_gain(self, cam_id, analogue_gain: float):
|
||||
self._validate_cam_id_running(cam_id)
|
||||
|
||||
ctrl_state = self.camera_controls[cam_id]
|
||||
analogue_gain = float(analogue_gain)
|
||||
analogue_gain = max(1.0, analogue_gain)
|
||||
|
||||
ctrl_state["analogue_gain"] = analogue_gain
|
||||
ctrl_state["ae_enable"] = False
|
||||
|
||||
exposure_time_us = int(ctrl_state.get("exposure_time_us") or 15000)
|
||||
|
||||
ctrl = dai.CameraControl()
|
||||
ctrl.setManualExposure(exposure_time_us, self._gain_to_iso(analogue_gain))
|
||||
|
||||
self._send_control(cam_id, ctrl)
|
||||
|
||||
return dict(ctrl_state)
|
||||
|
||||
def set_colour_gains(self, cam_id, red_gain: float, blue_gain: float):
|
||||
self._validate_cam_id_running(cam_id)
|
||||
|
||||
ctrl_state = self.camera_controls[cam_id]
|
||||
ctrl_state["colour_gains"] = [float(red_gain), float(blue_gain)]
|
||||
ctrl_state["awb_enable"] = False
|
||||
|
||||
ctrl = dai.CameraControl()
|
||||
|
||||
if hasattr(ctrl, "setManualWhiteBalance"):
|
||||
# Nem sempre esse método usa red/blue diretamente. Fica como placeholder seguro.
|
||||
pass
|
||||
|
||||
self._send_control(cam_id, ctrl)
|
||||
|
||||
return dict(ctrl_state)
|
||||
|
||||
def apply_camera_controls(self, cam_id, controls: dict):
|
||||
self._validate_cam_id_running(cam_id)
|
||||
|
||||
result = dict(self.camera_controls.get(cam_id, {}))
|
||||
|
||||
if "ae_enable" in controls:
|
||||
result = self.set_ae_enable(cam_id, bool(controls["ae_enable"]))
|
||||
|
||||
if "awb_enable" in controls:
|
||||
result = self.set_awb_enable(cam_id, bool(controls["awb_enable"]))
|
||||
|
||||
if "exposure_time_us" in controls and controls["exposure_time_us"] is not None:
|
||||
result = self.set_exposure_time(cam_id, int(controls["exposure_time_us"]))
|
||||
|
||||
if "analogue_gain" in controls and controls["analogue_gain"] is not None:
|
||||
result = self.set_analogue_gain(cam_id, float(controls["analogue_gain"]))
|
||||
|
||||
return result
|
||||
|
||||
def _send_control(self, cam_id, ctrl):
|
||||
if cam_id not in self.control_queues:
|
||||
raise RuntimeError(f"Fila de controle não existe para {cam_id}")
|
||||
|
||||
self.control_queues[cam_id].send(ctrl)
|
||||
|
||||
def _validate_cam_id_known(self, cam_id):
|
||||
if cam_id not in self.camera_controls:
|
||||
raise ValueError(f"cam_id inválido: {cam_id}")
|
||||
|
||||
def _validate_cam_id_running(self, cam_id):
|
||||
self._validate_cam_id_known(cam_id)
|
||||
|
||||
if not self.running:
|
||||
raise RuntimeError("Manager não está rodando.")
|
||||
|
||||
if cam_id not in self.control_queues:
|
||||
raise RuntimeError(f"Câmera {cam_id} não está ativa no pipeline.")
|
||||
|
||||
@staticmethod
|
||||
def _socket_to_cam_id(socket_name):
|
||||
mapping = {
|
||||
"CAM_A": "cam2", # RGB
|
||||
"CAM_B": "cam0", # RE
|
||||
"CAM_C": "cam1", # NIR
|
||||
}
|
||||
return mapping.get(socket_name, socket_name.lower())
|
||||
def _gain_to_iso(gain: float) -> int:
|
||||
# DepthAI usa ISO no setManualExposure(exposure_us, sensitivity_iso).
|
||||
# Mantemos analogue_gain estilo Pi e convertemos para ISO aproximado.
|
||||
gain = max(1.0, float(gain))
|
||||
iso = int(round(gain * 100))
|
||||
return max(100, min(1600, iso))
|
||||
|
||||
|
|
@ -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"]:
|
||||
print("\nTESTANDO:", frame_type)
|
||||
|
|
@ -21,3 +21,9 @@ for frame_type in ["RAW_BRUTO", "RGB", "MULTISPEC"]:
|
|||
print("channels:", meta.get("channels"))
|
||||
print("shape:", getattr(frame, "shape", None))
|
||||
print("dtype:", getattr(frame, "dtype", None))
|
||||
print("payload_sources:", meta.get("payload_sources"))
|
||||
print("active_roles:", cam.get_status().get("active_roles"))
|
||||
print("decoded_roles:", {
|
||||
cam_id: item.get("role")
|
||||
for cam_id, item in decoded.items()
|
||||
})
|
||||
|
|
@ -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 numpy as np
|
||||
|
||||
from oak_fcc3_service import OakFcc3Service
|
||||
from core.oak_fcc3_service import OakFcc3Service
|
||||
|
||||
|
||||
svc = OakFcc3Service(timeout=10)
|
||||
|
|
@ -60,9 +60,15 @@ finally:
|
|||
|
||||
|
||||
if last_frame is not None:
|
||||
if isinstance(last_frame, dict) and "cam2" in last_frame:
|
||||
cv2.imwrite("calibration/capture_cam2.jpg", last_frame["cam2"])
|
||||
print("[OK] Salvo: calibration/capture_cam2.jpg")
|
||||
if isinstance(last_frame, dict):
|
||||
camera_info = (last_meta or {}).get("camera_info", {}) or {}
|
||||
|
||||
for cam_id, arr in last_frame.items():
|
||||
role = camera_info.get(cam_id, {}).get("role", "unknown")
|
||||
path = f"calibration/capture_{cam_id}_{role}.bin"
|
||||
|
||||
arr.tofile(path)
|
||||
print(f"[OK] Salvo: {path}")
|
||||
|
||||
elif isinstance(last_frame, dict):
|
||||
first_id = list(last_frame.keys())[0]
|
||||
|
|
@ -6,8 +6,8 @@ from pathlib import Path
|
|||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from cam_3.pi.raw_processor_core import RawProcessorCore
|
||||
from cam_3.pi.raw_processor_preview import RawProcessorPreview
|
||||
from core.raw_processor_core import RawProcessorCore
|
||||
from core.raw_processor_preview import RawProcessorPreview
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
|
|
@ -129,13 +129,32 @@ def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str
|
|||
return preview_bgr, desc
|
||||
|
||||
if saved_type == "multispec":
|
||||
if arr.ndim != 3 or arr.shape[0] < 3:
|
||||
if arr.ndim != 3 or arr.shape[0] < 5:
|
||||
raise RuntimeError(f"Payload MULTISPEC inválido, shape={arr.shape}")
|
||||
|
||||
rgb_hwc = chw_to_hwc(arr[:3].astype(np.float32))
|
||||
preview_bgr = normalize_float01_to_bgr(rgb_hwc)
|
||||
desc = f"Reconstruido de MULTISPEC salvo | dtype={arr.dtype} | shape={arr.shape}"
|
||||
return preview_bgr, desc
|
||||
|
||||
re01 = arr[3].astype(np.float32)
|
||||
nir01 = arr[4].astype(np.float32)
|
||||
|
||||
re_bgr = cv2.cvtColor(
|
||||
np.clip(re01 * 255.0, 0, 255).astype(np.uint8),
|
||||
cv2.COLOR_GRAY2BGR
|
||||
)
|
||||
|
||||
nir_bgr = cv2.cvtColor(
|
||||
np.clip(nir01 * 255.0, 0, 255).astype(np.uint8),
|
||||
cv2.COLOR_GRAY2BGR
|
||||
)
|
||||
|
||||
combined = np.vstack([
|
||||
np.hstack([preview_bgr, re_bgr]),
|
||||
np.hstack([nir_bgr, np.zeros_like(preview_bgr)])
|
||||
])
|
||||
|
||||
desc = f"Reconstruido de MULTISPEC | dtype={arr.dtype} | shape={arr.shape} | canais=[R,G,B,RE,NIR]"
|
||||
return combined, desc
|
||||
|
||||
if saved_type == "raw_native_single":
|
||||
if arr.ndim == 3 and arr.shape[2] == 3 and arr.dtype == np.uint8:
|
||||
|
|
@ -7,7 +7,7 @@ from datetime import datetime
|
|||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from oak_fcc3_client import OakFcc3Client as MultiSpectralClient
|
||||
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -173,11 +173,12 @@ def validate_module_ready(status: dict, frame_type: str, raw_policy: str, captur
|
|||
raise RuntimeError(f"Status inválido retornado pelo módulo: {status}")
|
||||
|
||||
active_ids = list(status.get("active_camera_ids", []))
|
||||
active_roles = status.get("active_roles", {}) or {}
|
||||
active_count = int(status.get("camera_count_active", 0))
|
||||
|
||||
if frame_type == "RAW_BRUTO":
|
||||
if raw_policy == "require_triple":
|
||||
missing = [cid for cid in ("cam0", "cam1", "cam2") if cid not in active_ids]
|
||||
missing = [role for role in ("rgb", "nir", "re") if role not in active_roles]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"RAW_BRUTO com política require_triple exige três câmeras ativas. "
|
||||
|
|
@ -199,9 +200,6 @@ def default_offsets_payload(args, effective_capture_mode: str):
|
|||
return {
|
||||
"schema": "manual_multispec_offsets_v1",
|
||||
"saved_at": now_str(),
|
||||
"pi_host": args.pi_host,
|
||||
"pc_host": args.pc_host,
|
||||
"stream_port": args.stream_port,
|
||||
"frame_type": "RAW_BRUTO",
|
||||
"capture_mode_requested": args.capture_mode,
|
||||
"capture_mode_effective": effective_capture_mode,
|
||||
|
|
@ -209,16 +207,16 @@ def default_offsets_payload(args, effective_capture_mode: str):
|
|||
"sensor_width": args.width,
|
||||
"sensor_height": args.height,
|
||||
"bayer_pattern": args.bayer,
|
||||
"reference_camera": "cam2",
|
||||
"baseline_mm": args.baseline_mm,
|
||||
"alignment_mode": "manual_affine",
|
||||
"reference_camera": "rgb",
|
||||
"manual_offsets": {
|
||||
"cam0": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
||||
"cam1": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
||||
"re": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
||||
"nir": {"dx": 0, "dy": 0, "theta_deg": 0.0},
|
||||
},
|
||||
"homographies": {
|
||||
"cam0_to_cam2": None,
|
||||
"cam1_to_cam2": None,
|
||||
"re_to_rgb": None,
|
||||
"nir_to_rgb": None,
|
||||
},
|
||||
"notes": args.notes or "",
|
||||
}
|
||||
|
|
@ -232,15 +230,13 @@ def load_offsets_json(path: str, args, effective_capture_mode: str):
|
|||
data = json.load(f)
|
||||
|
||||
data.setdefault("schema", "manual_multispec_offsets_v1")
|
||||
data.setdefault("reference_camera", "cam2")
|
||||
data.setdefault("baseline_mm", args.baseline_mm)
|
||||
data.setdefault("alignment_mode", "manual_affine")
|
||||
data.setdefault("manual_offsets", {})
|
||||
data["manual_offsets"].setdefault("cam0", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
||||
data["manual_offsets"].setdefault("cam1", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
||||
data.setdefault("homographies", {})
|
||||
data["homographies"].setdefault("cam0_to_cam2", None)
|
||||
data["homographies"].setdefault("cam1_to_cam2", None)
|
||||
data.setdefault("reference_camera", "rgb")
|
||||
data["manual_offsets"].setdefault("re", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
||||
data["manual_offsets"].setdefault("nir", {"dx": 0, "dy": 0, "theta_deg": 0.0})
|
||||
data["homographies"].setdefault("re_to_rgb", None)
|
||||
data["homographies"].setdefault("nir_to_rgb", None)
|
||||
return data
|
||||
|
||||
|
||||
|
|
@ -252,6 +248,14 @@ def save_offsets_json(path: str, data: dict):
|
|||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def get_decoded_by_role(decoded, role):
|
||||
role = str(role).lower()
|
||||
for cam_id, item in decoded.items():
|
||||
if str(item.get("role", "")).lower() == role:
|
||||
return cam_id, item
|
||||
return None, None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main UI
|
||||
# ============================================================
|
||||
|
|
@ -261,10 +265,6 @@ def main():
|
|||
description="Calibrador manual de offsets para fusão RGB/RE/NIR a partir do stream RAW_BRUTO.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--pi_host", default="192.168.105.6")
|
||||
parser.add_argument("--pc_host", default="192.168.105.5")
|
||||
parser.add_argument("--stream_port", type=int, default=6001)
|
||||
parser.add_argument("--server_port", type=int, default=5000)
|
||||
parser.add_argument("--fps", type=int, default=20)
|
||||
parser.add_argument("--width", type=int, default=640)
|
||||
parser.add_argument("--height", type=int, default=480)
|
||||
|
|
@ -327,23 +327,22 @@ def main():
|
|||
offsets_data = load_offsets_json(args.load_json, args, effective_capture_mode)
|
||||
offsets = offsets_data["manual_offsets"]
|
||||
|
||||
selected_cam = "cam0"
|
||||
selected_role = "re"
|
||||
calibration_mode = offsets_data.get("alignment_mode", "manual_affine")
|
||||
|
||||
selected_points_spec = {
|
||||
"cam0": [],
|
||||
"cam1": [],
|
||||
"re": [],
|
||||
"nir": [],
|
||||
}
|
||||
selected_points_rgb = {
|
||||
"cam0": [],
|
||||
"cam1": [],
|
||||
"re": [],
|
||||
"nir": [],
|
||||
}
|
||||
|
||||
panel_rects = {
|
||||
"fuse": None,
|
||||
"rgb": None,
|
||||
"cam0": None,
|
||||
"cam1": None,
|
||||
"re": None,
|
||||
"nir": None,
|
||||
}
|
||||
last_msg = ""
|
||||
last_msg_t = 0.0
|
||||
|
|
@ -363,10 +362,6 @@ def main():
|
|||
|
||||
try:
|
||||
with MultiSpectralClient(
|
||||
pi_host=args.pi_host,
|
||||
pc_host=args.pc_host,
|
||||
server_port=args.server_port,
|
||||
stream_port=args.stream_port,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
bayer=args.bayer,
|
||||
|
|
@ -376,6 +371,7 @@ def main():
|
|||
capture_mode=effective_capture_mode,
|
||||
raw_policy=args.raw_policy,
|
||||
module_calibration_json=None,
|
||||
radiometric_enabled=True
|
||||
) as cam:
|
||||
while True:
|
||||
t0 = time.time()
|
||||
|
|
@ -409,9 +405,13 @@ def main():
|
|||
t_view_fps = time.time()
|
||||
|
||||
if decoded_last:
|
||||
rgb01 = decoded_last.get("cam2", {}).get("image")
|
||||
re01 = decoded_last.get("cam0", {}).get("image")
|
||||
nir01 = decoded_last.get("cam1", {}).get("image")
|
||||
rgb_id, rgb_item = get_decoded_by_role(decoded_last, "rgb")
|
||||
re_id, re_item = get_decoded_by_role(decoded_last, "re")
|
||||
nir_id, nir_item = get_decoded_by_role(decoded_last, "nir")
|
||||
|
||||
rgb01 = rgb_item.get("image") if rgb_item else None
|
||||
re01 = re_item.get("image") if re_item else None
|
||||
nir01 = nir_item.get("image") if nir_item else None
|
||||
|
||||
if rgb01 is None:
|
||||
# fallback para exibição quando não houver RGB
|
||||
|
|
@ -432,13 +432,13 @@ def main():
|
|||
re_panel = gray_to_color_bgr(re01, "RE") if re01 is not None else build_empty_panel_like(rgb_panel, "RE")
|
||||
nir_panel = gray_to_color_bgr(nir01, "NIR") if nir01 is not None else build_empty_panel_like(rgb_panel, "NIR")
|
||||
|
||||
active_spec_name = "RE" if selected_cam == "cam0" else "NIR"
|
||||
active_spec = re01 if selected_cam == "cam0" else nir01
|
||||
dx = int(offsets.get(selected_cam, {}).get("dx", 0))
|
||||
dy = int(offsets.get(selected_cam, {}).get("dy", 0))
|
||||
theta_deg = float(offsets.get(selected_cam, {}).get("theta_deg", 0.0))
|
||||
active_spec_name = "RE" if selected_role == "cam0" else "NIR"
|
||||
active_spec = re01 if selected_role == "cam0" else nir01
|
||||
dx = int(offsets.get(selected_role, {}).get("dx", 0))
|
||||
dy = int(offsets.get(selected_role, {}).get("dy", 0))
|
||||
theta_deg = float(offsets.get(selected_role, {}).get("theta_deg", 0.0))
|
||||
|
||||
H_key = f"{selected_cam}_to_cam2"
|
||||
H_key = f"{selected_role}_to_cam2"
|
||||
H = offsets_data.get("homographies", {}).get(H_key)
|
||||
|
||||
fuse_panel = build_overlay_fuse(
|
||||
|
|
@ -7,7 +7,7 @@ from datetime import datetime
|
|||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from cam_3.multispectral_client import MultiSpectralClient
|
||||
from core.oak_fcc3_client import OakFcc3Client as MultiSpectralClient
|
||||
|
||||
# ============================================================
|
||||
# Helpers
|
||||
|
|
@ -444,9 +444,6 @@ def default_payload(args, effective_capture_mode: str):
|
|||
return {
|
||||
"schema": "manual_sensor_calibration_v1",
|
||||
"saved_at": now_str(),
|
||||
"pi_host": args.pi_host,
|
||||
"pc_host": args.pc_host,
|
||||
"stream_port": args.stream_port,
|
||||
"frame_type": "RAW_BRUTO",
|
||||
"capture_mode_requested": args.capture_mode,
|
||||
"capture_mode_effective": effective_capture_mode,
|
||||
|
|
@ -750,10 +747,6 @@ def main():
|
|||
description="Ferramenta de calibração dos sensores RGB/RE/NIR com controle manual e ROIs em tempo real.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--pi_host", default="192.168.105.6")
|
||||
parser.add_argument("--pc_host", default="192.168.105.5")
|
||||
parser.add_argument("--stream_port", type=int, default=6001)
|
||||
parser.add_argument("--server_port", type=int, default=5000)
|
||||
parser.add_argument("--fps", type=int, default=20)
|
||||
parser.add_argument("--width", type=int, default=640)
|
||||
parser.add_argument("--height", type=int, default=480)
|
||||
|
|
@ -775,10 +768,6 @@ def main():
|
|||
args = parser.parse_args()
|
||||
|
||||
cam = MultiSpectralClient(
|
||||
pi_host=args.pi_host,
|
||||
pc_host=args.pc_host,
|
||||
server_port=args.server_port,
|
||||
stream_port=args.stream_port,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
bayer=args.bayer,
|
||||
|
|
@ -788,6 +777,7 @@ def main():
|
|||
capture_mode=args.capture_mode,
|
||||
raw_policy=args.raw_policy,
|
||||
module_calibration_json=None,
|
||||
radiometric_enabled=True
|
||||
)
|
||||
|
||||
offline_mode = bool(args.offline_sample_json)
|
||||