357 lines
11 KiB
Python
357 lines
11 KiB
Python
import time
|
|
import json
|
|
|
|
import numpy as np
|
|
|
|
from cam_3.multispectral_service import MultiSpectralService
|
|
from cam_3.stream_receiver import StreamReceiver
|
|
from cam_3.pi.raw_processor_core import RawProcessorCore
|
|
from cam_3.pi.raw_processor_preview import RawProcessorPreview
|
|
from cam_3.radiometric_controller import RadiometricController
|
|
|
|
|
|
class MultiSpectralClient:
|
|
def __init__(
|
|
self,
|
|
pi_host="192.168.105.6",
|
|
pc_host="192.168.105.5",
|
|
server_port=5000,
|
|
stream_port=6001,
|
|
timeout=10,
|
|
width=640,
|
|
height=480,
|
|
bayer="GBRG",
|
|
fps=15,
|
|
frame_type="RAW_BRUTO",
|
|
output_dtype="uint8",
|
|
capture_mode="AUTO",
|
|
raw_policy="allow_single",
|
|
module_calibration_json=None,
|
|
radiometric_enabled=False,
|
|
):
|
|
self.pi_host = pi_host
|
|
self.pc_host = pc_host
|
|
self.server_port = server_port
|
|
self.stream_port = stream_port
|
|
|
|
self.width = width
|
|
self.height = height
|
|
self.bayer = bayer
|
|
self.fps = fps
|
|
self.frame_type = frame_type
|
|
self.output_dtype = output_dtype
|
|
self.capture_mode = capture_mode
|
|
self.raw_policy = raw_policy
|
|
self.module_calibration_json = module_calibration_json
|
|
|
|
self.svc = MultiSpectralService(
|
|
host=pi_host,
|
|
port=server_port,
|
|
timeout=timeout,
|
|
)
|
|
|
|
self.receiver = StreamReceiver(
|
|
host="0.0.0.0",
|
|
port=stream_port,
|
|
)
|
|
|
|
self.core = RawProcessorCore(
|
|
sensor_width=width,
|
|
sensor_height=height,
|
|
bayer_pattern=bayer,
|
|
calibration_json_path=module_calibration_json,
|
|
)
|
|
|
|
self.preview = RawProcessorPreview(
|
|
sensor_width=width,
|
|
sensor_height=height,
|
|
bayer_pattern=bayer,
|
|
)
|
|
|
|
self.last_frame_id = None
|
|
self.status = None
|
|
self.begin_resp = None
|
|
self.applied_params = None
|
|
|
|
self.radiometric_enabled = bool(radiometric_enabled)
|
|
self.radiometric_controller = None
|
|
|
|
def __enter__(self):
|
|
self.start()
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
self.stop()
|
|
|
|
def start(self, print_debug=True):
|
|
if print_debug:
|
|
print(f"[INFO] Verificando módulo em {self.pi_host}:{self.server_port}...")
|
|
|
|
if not self.svc.check_connection(2):
|
|
raise RuntimeError("Módulo não encontrado ou não respondeu ao ping.")
|
|
|
|
self.receiver.start()
|
|
time.sleep(0.5)
|
|
|
|
self.svc.connect()
|
|
|
|
if print_debug:
|
|
print("[OK] Módulo conectado.")
|
|
|
|
self._configure_module(print_debug=print_debug)
|
|
self._begin_module(print_debug=print_debug)
|
|
self._apply_module_params(print_debug=print_debug)
|
|
self._start_stream(print_debug=print_debug)
|
|
|
|
if self.radiometric_enabled:
|
|
self.enable_radiometric_controller()
|
|
|
|
return self
|
|
|
|
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_params["camera_settings"])
|
|
|
|
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 _configure_module(self, print_debug=True):
|
|
r0 = self.svc.set_resolution(self.width, self.height)
|
|
r1 = self.svc.set_bayer(self.bayer)
|
|
r2 = self.svc.set_fps(self.fps)
|
|
r3 = self.svc.set_capture_mode(self.capture_mode)
|
|
r4 = self.svc.set_frame_type(self.frame_type)
|
|
r5 = self.svc.set_output_dtype(self.output_dtype)
|
|
if print_debug:
|
|
print("SET RES:", r0)
|
|
print("SET BAYER:", r1)
|
|
print("SET FPS:", r2)
|
|
print("SET CAPTURE MODE:", r3)
|
|
print("SET FRAME TYPE:", r4)
|
|
print("SET OUTPUT DTYPE:", r5)
|
|
|
|
def _begin_module(self, print_debug=True):
|
|
self.begin_resp = self.svc.begin(
|
|
frame_type=self.frame_type,
|
|
output_dtype=self.output_dtype,
|
|
capture_mode=self.capture_mode,
|
|
)
|
|
|
|
self.status = self.svc.get_status()
|
|
|
|
self.svc.validate_module_ready(
|
|
self.status,
|
|
self.frame_type,
|
|
self.raw_policy,
|
|
self.capture_mode,
|
|
)
|
|
|
|
if print_debug:
|
|
print("BEGIN:", self.begin_resp)
|
|
print("STATUS:", json.dumps({
|
|
"status": self.status.get("status"),
|
|
"detected_mode": self.status.get("detected_mode"),
|
|
"camera_count_active": self.status.get("camera_count_active"),
|
|
"active_camera_ids": self.status.get("active_camera_ids"),
|
|
}, ensure_ascii=False))
|
|
|
|
def _apply_module_params(self, print_debug=True):
|
|
if not self.module_calibration_json:
|
|
return None
|
|
|
|
self.applied_params = self.svc.apply_camera_params_json(
|
|
self.module_calibration_json
|
|
)
|
|
|
|
if print_debug:
|
|
print("[OK] Parâmetros do módulo aplicados:")
|
|
print(json.dumps(self.applied_params.get("applied"), ensure_ascii=False, indent=2))
|
|
|
|
return self.applied_params
|
|
|
|
def _start_stream(self, print_debug=True):
|
|
resp = self.svc.start_stream(
|
|
self.pc_host,
|
|
self.stream_port,
|
|
fps=self.fps,
|
|
)
|
|
|
|
if print_debug:
|
|
print("START STREAM:", resp)
|
|
|
|
return resp
|
|
|
|
def get_latest(self):
|
|
return self.receiver.last_frame, self.receiver.last_meta
|
|
|
|
def get_next_frame(self, timeout=2.0):
|
|
t0 = time.perf_counter()
|
|
|
|
while time.perf_counter() - t0 < timeout:
|
|
frame = self.receiver.last_frame
|
|
meta = self.receiver.last_meta
|
|
|
|
if frame is None or meta is None:
|
|
time.sleep(0.001)
|
|
continue
|
|
|
|
frame_id = meta.get("frame_id")
|
|
|
|
if frame_id != self.last_frame_id:
|
|
self.last_frame_id = frame_id
|
|
return frame, meta
|
|
|
|
time.sleep(0.001)
|
|
|
|
raise TimeoutError("Timeout aguardando novo frame do stream.")
|
|
|
|
def get_next_decoded(self, timeout=2.0, update_radiometry=True):
|
|
frame, meta = self.get_next_frame(timeout=timeout)
|
|
|
|
frame_type = meta.get("frame_type")
|
|
|
|
if frame_type == "RAW_BRUTO":
|
|
decoded = self.core.decode_stream_cameras(frame, meta)
|
|
|
|
elif frame_type in ("RGB", "MULTISPEC"):
|
|
decoded = {
|
|
"cam2": {
|
|
"name": "RGB",
|
|
"image": frame,
|
|
"meta": meta
|
|
}
|
|
}
|
|
|
|
else:
|
|
raise RuntimeError(f"frame_type não suportado: {frame_type}")
|
|
|
|
if update_radiometry:
|
|
self.update_radiometry(decoded, meta)
|
|
|
|
return frame, meta, decoded
|
|
|
|
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 build_preview_from_raw_payload(self, frame, meta: dict):
|
|
"""
|
|
Gera preview priorizando a câmera RGB (cam2).
|
|
Se cam2 não estiver presente, cai para fallback usando a primeira câmera mono disponível.
|
|
Retorna:
|
|
preview_bgr
|
|
payload_float_preview
|
|
preview_source_id
|
|
"""
|
|
payload_sources = meta.get("payload_sources", []) or []
|
|
|
|
# Caso multi-payload: tenta usar cam2 primeiro
|
|
if isinstance(frame, dict):
|
|
if "cam2" in frame:
|
|
rgb_frame = frame["cam2"]
|
|
|
|
if rgb_frame.ndim != 3 or rgb_frame.shape[2] != 3:
|
|
raise RuntimeError(f"cam2 recebida mas inválida para preview RGB: shape={rgb_frame.shape}")
|
|
|
|
preview_bgr = rgb_frame.copy()
|
|
payload_float = rgb_frame[:, :, ::-1].astype(np.float32) / 255.0
|
|
payload_float = np.transpose(payload_float, (2, 0, 1))
|
|
|
|
return preview_bgr, payload_float, "cam2"
|
|
|
|
# fallback: usa a primeira câmera mono disponível
|
|
fallback_id = None
|
|
for cid in ("cam0", "cam1"):
|
|
if cid in frame:
|
|
fallback_id = cid
|
|
break
|
|
|
|
if fallback_id is None:
|
|
raise RuntimeError("Nenhuma câmera disponível no payload para gerar preview")
|
|
|
|
packed = frame[fallback_id]
|
|
if packed.ndim == 3 and packed.shape[2] == 1:
|
|
packed = packed[:, :, 0]
|
|
|
|
cam_frames = meta.get("camera_frames", {}) or {}
|
|
cam_meta = cam_frames.get(fallback_id, {})
|
|
bit_depth = int(cam_meta.get("bit_depth", 10))
|
|
|
|
raw16 = self.core.unpack_raw10_packed(packed)
|
|
preview_bgr = self.preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth)
|
|
|
|
payload_float = self.core.build_training_rgb(
|
|
raw16,
|
|
output_dtype="float32",
|
|
bit_depth=bit_depth,
|
|
)
|
|
|
|
return preview_bgr, payload_float, fallback_id
|
|
|
|
# Caso single-payload
|
|
if isinstance(frame, np.ndarray):
|
|
# Se vier HWC/3ch, tratamos como RGB USB
|
|
if frame.ndim == 3 and frame.shape[2] == 3:
|
|
preview_bgr = frame.copy()
|
|
payload_float = frame[:, :, ::-1].astype(np.float32) / 255.0
|
|
payload_float = np.transpose(payload_float, (2, 0, 1))
|
|
return preview_bgr, payload_float, "cam2"
|
|
|
|
# Se vier mono packed, fallback antigo
|
|
packed = frame
|
|
if packed.ndim == 3 and packed.shape[2] == 1:
|
|
packed = packed[:, :, 0]
|
|
|
|
source_camera = meta.get("source_camera") or {}
|
|
bit_depth = int(source_camera.get("bit_depth", meta.get("source_bit_depth", 10)))
|
|
|
|
raw16 = self.core.unpack_raw10_packed(packed)
|
|
preview_bgr = self.preview.raw16_to_preview_bgr(raw16, bit_depth=bit_depth)
|
|
|
|
payload_float = self.core.build_training_rgb(
|
|
raw16,
|
|
output_dtype="float32",
|
|
bit_depth=bit_depth,
|
|
)
|
|
|
|
return preview_bgr, payload_float, source_camera.get("id", "unknown")
|
|
|
|
raise RuntimeError(f"Tipo de frame não suportado para preview: {type(frame)}")
|
|
|
|
def stop(self):
|
|
try:
|
|
self.svc.stop_stream()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.svc.stop()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.svc.disconnect()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.receiver.stop()
|
|
except Exception:
|
|
pass |