agrobot_base/Python/raspi/cam_2/pi/frame_service.py

405 lines
15 KiB
Python

import time
import threading
import base64
import numpy as np
class FrameService:
def __init__(self, state, trigger_manager, camera_manager):
self.state = state
self.trigger = trigger_manager
self.camera = camera_manager
self._capture_lock = threading.RLock()
self.raw_processors = {}
def _get_camera_spec(self, cam_id):
cam = self.state.get_camera(cam_id)
if cam is None:
raise RuntimeError(f"Câmera '{cam_id}' não encontrada no state")
return cam
def _ensure_raw_processor_for_camera(self, cam_id):
cam = self._get_camera_spec(cam_id)
rp = self.raw_processors.get(cam_id)
if (
rp is None or
rp.sensor_width != cam.width or
rp.sensor_height != cam.height or
rp.bayer_pattern.upper() != cam.bayer_pattern.upper()
):
from raw_processor_core import RawProcessorCore
rp = RawProcessorCore(
sensor_width=cam.width,
sensor_height=cam.height,
bayer_pattern=cam.bayer_pattern,
)
self.raw_processors[cam_id] = rp
return rp
def _capture_with_retry(self, required_sources, max_attempts=10, retry_delay_s=0.02):
last_frames = None
for _ in range(max_attempts):
frames = self.camera.capture_raw_frames()
ok = True
for cam_id in required_sources:
info = frames.get(cam_id)
if info is None:
ok = False
break
frame, width, height, channels, frame_id, frame_ts = info
if frame is None or width <= 0 or height <= 0 or channels <= 0:
ok = False
break
if ok:
return frames
last_frames = frames
time.sleep(retry_delay_s)
raise RuntimeError(
f"Capture retornou frames insuficientes após {max_attempts} tentativas. "
f"required_sources={required_sources}, received_sources={list((last_frames or {}).keys())}"
)
def capture_frame_raw(self):
with self._capture_lock:
if not self.state.initialized:
raise RuntimeError("Módulo não inicializado")
t0_perf = time.perf_counter()
t0_unix = time.time()
dt_trigger = 0.0
dt_settle = 0.0
dt_capture = 0.0
dt_process = 0.0
required_sources = list(self.state.payload.sources)
try:
if self.state.trigger_enabled:
trig_start = time.perf_counter()
self.trigger.pulse()
trig_end = time.perf_counter()
dt_trigger = trig_end - trig_start
settle_ms = float(getattr(self.state, "trigger_settle_delay_ms", 0.0) or 0.0)
if settle_ms > 0:
settle_start = time.perf_counter()
time.sleep(settle_ms / 1000.0)
settle_end = time.perf_counter()
dt_settle = settle_end - settle_start
cap_start = time.perf_counter()
raw_frames = self._capture_with_retry(required_sources)
cap_end = time.perf_counter()
dt_capture = cap_end - cap_start
proc_start = time.perf_counter()
frame_out, meta_extra = self._build_output_frame(raw_frames)
proc_end = time.perf_counter()
dt_process = proc_end - proc_start
except Exception as e:
raise RuntimeError(f"Falha durante captura/processamento de frame: {e}") from e
if frame_out is None:
raise RuntimeError("Frame processado retornou nulo")
total_end = time.perf_counter()
self.state.stream_frame_id += 1
frame_id = self.state.stream_frame_id
camera_frames_meta = {}
for cam_id, info in raw_frames.items():
frame, width, height, channels, cam_frame_id, cam_frame_ts = info
cam = self._get_camera_spec(cam_id)
camera_frames_meta[cam_id] = {
"camera_frame_id": int(cam_frame_id),
"camera_frame_ts": cam_frame_ts,
"width": int(width),
"height": int(height),
"channels": int(channels),
"bayer_pattern": cam.bayer_pattern,
"bit_depth": int(cam.bit_depth),
}
meta = {
"frame_id": frame_id,
"ts_pi": t0_unix,
"ts_pi_monotonic": t0_perf,
"dt_trigger": dt_trigger,
"dt_settle": dt_settle,
"dt_capture": dt_capture,
"dt_process": dt_process,
"dt_total_pi": total_end - t0_perf,
"camera_frames": camera_frames_meta,
"capture_mode_resolved": self.state.resolve_capture_mode(),
"payload_format_version": self.state.payload_format_version,
}
meta.update(meta_extra)
return frame_out, meta
def capture_frame_base64(self):
frame, meta = self.capture_frame_raw()
if isinstance(frame, dict):
encoded = {}
total_size = 0
for cam_id, arr in frame.items():
frame_bytes = arr.tobytes()
total_size += len(frame_bytes)
encoded[cam_id] = {
"encoding": "base64",
"size": len(frame_bytes),
"data": base64.b64encode(frame_bytes).decode("ascii"),
}
meta["encoding"] = "base64-multi"
meta["size"] = total_size
meta["frames"] = encoded
return meta
frame_bytes = frame.tobytes()
meta["encoding"] = "base64"
meta["size"] = len(frame_bytes)
meta["data"] = base64.b64encode(frame_bytes).decode("ascii")
return meta
def _build_output_frame(self, raw_frames):
if self.state.frame_type == "RAW_BRUTO":
return self._process_raw_bruto(raw_frames)
if self.state.frame_type == "RGB":
return self._process_rgb(raw_frames)
if self.state.frame_type == "RGBNIR":
return self._process_rgbnir(raw_frames)
raise RuntimeError(f"frame_type inválido: {self.state.frame_type}")
def _convert_output_dtype(self, arr, bit_depth):
max_sensor_value = (1 << int(bit_depth)) - 1
if self.state.output_dtype == "uint8":
if arr.dtype == "uint8":
return arr
if arr.dtype == "float32":
return (arr * 255.0).clip(0, 255).astype("uint8")
if arr.dtype.kind in ("u", "i"):
max_val = arr.max() if arr.size > 0 else 0
if max_val <= 255:
return arr.astype("uint8")
return ((arr.astype("float32") / max_sensor_value) * 255.0).clip(0, 255).astype("uint8")
raise RuntimeError(f"dtype não suportado para uint8: {arr.dtype}")
if self.state.output_dtype == "uint16":
if arr.dtype == "uint16":
return arr
if arr.dtype == "uint8":
return (arr.astype("uint16") << 8)
if arr.dtype == "float32":
return (arr * 65535.0).clip(0, 65535).astype("uint16")
if arr.dtype.kind in ("u", "i"):
return arr.astype("uint16")
raise RuntimeError(f"dtype não suportado para uint16: {arr.dtype}")
if self.state.output_dtype == "float32":
if arr.dtype == "float32":
return arr
if arr.dtype == "uint8":
return arr.astype("float32") / 255.0
if arr.dtype.kind in ("u", "i"):
max_val = arr.max() if arr.size > 0 else 0
if max_val <= 255:
return arr.astype("float32") / 255.0
return arr.astype("float32") / max_sensor_value
raise RuntimeError(f"dtype não suportado para float32: {arr.dtype}")
raise RuntimeError(f"output_dtype inválido: {self.state.output_dtype}")
def _process_raw_bruto(self, raw_frames):
sources = list(self.state.payload.sources)
if len(sources) == 1:
cam_id = sources[0]
frame, width, height, channels, cam_frame_id, cam_frame_ts = raw_frames[cam_id]
cam = self._get_camera_spec(cam_id)
meta_extra = {
"frame_type": self.state.frame_type,
"output_dtype": str(frame.dtype),
"dtype": str(frame.dtype),
"output_layout": "HW",
"output_channels": 1,
"output_channel_names": ["RAW10_PACKED"],
"output_width": int(width),
"output_height": int(height),
"width": int(width),
"height": int(height),
"channels": 1,
"payload_sources": [cam_id],
"source_camera": {
"id": cam_id,
"bayer_pattern": cam.bayer_pattern,
"bit_depth": int(cam.bit_depth),
},
}
return frame, meta_extra
frames_out = {}
sources_meta = []
for cam_id in sources:
frame, width, height, channels, cam_frame_id, cam_frame_ts = raw_frames[cam_id]
cam = self._get_camera_spec(cam_id)
frames_out[cam_id] = frame
sources_meta.append({
"id": cam_id,
"width": int(width),
"height": int(height),
"bayer_pattern": cam.bayer_pattern,
"bit_depth": int(cam.bit_depth),
})
meta_extra = {
"frame_type": self.state.frame_type,
"output_dtype": "multi",
"dtype": "multi",
"output_layout": "MULTI_HW",
"output_channels": len(frames_out),
"output_channel_names": [f"RAW_{cam_id.upper()}" for cam_id in sources],
"output_width": None,
"output_height": None,
"width": None,
"height": None,
"channels": len(frames_out),
"payload_sources": sources,
"raw_sources": sources_meta,
}
return frames_out, meta_extra
def _process_rgb(self, raw_frames):
cam_id = self.state.payload.sources[0]
packed, packed_width, packed_height, _, _, _ = raw_frames[cam_id]
cam = self._get_camera_spec(cam_id)
rp = self._ensure_raw_processor_for_camera(cam_id)
if packed.ndim == 3 and packed.shape[2] == 1:
packed = packed[:, :, 0]
raw16 = rp.unpack_raw10_packed(packed)
rgb_chw = rp.build_training_rgb(raw16, bit_depth=cam.bit_depth)
rgb_chw = self._convert_output_dtype(rgb_chw, cam.bit_depth)
meta_extra = {
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"dtype": str(rgb_chw.dtype),
"output_layout": "CHW",
"output_channels": 3,
"output_channel_names": ["R", "G", "B"],
"output_width": int(rgb_chw.shape[2]),
"output_height": int(rgb_chw.shape[1]),
"width": int(rgb_chw.shape[2]),
"height": int(rgb_chw.shape[1]),
"channels": 3,
"payload_sources": [cam_id],
"packed_width": int(packed_width),
"packed_height": int(packed_height),
"source_camera": {
"id": cam_id,
"bayer_pattern": cam.bayer_pattern,
"bit_depth": int(cam.bit_depth),
"source_width": int(cam.width),
"source_height": int(cam.height),
},
}
return rgb_chw, meta_extra
def _process_rgbnir(self, raw_frames):
sources = list(self.state.payload.sources)
if len(sources) < 2:
raise RuntimeError("RGBNIR requer duas câmeras ativas")
cam_rgb_id = sources[0]
cam_nir_id = sources[1]
packed_rgb, _, _, _, _, _ = raw_frames[cam_rgb_id]
packed_nir, _, _, _, _, _ = raw_frames[cam_nir_id]
cam_rgb = self._get_camera_spec(cam_rgb_id)
cam_nir = self._get_camera_spec(cam_nir_id)
rp_rgb = self._ensure_raw_processor_for_camera(cam_rgb_id)
rp_nir = self._ensure_raw_processor_for_camera(cam_nir_id)
if packed_rgb.ndim == 3 and packed_rgb.shape[2] == 1:
packed_rgb = packed_rgb[:, :, 0]
if packed_nir.ndim == 3 and packed_nir.shape[2] == 1:
packed_nir = packed_nir[:, :, 0]
raw16_rgb = rp_rgb.unpack_raw10_packed(packed_rgb)
raw16_nir = rp_nir.unpack_raw10_packed(packed_nir)
rgb_chw = rp_rgb.build_training_rgb(raw16_rgb, bit_depth=cam_rgb.bit_depth).astype("float32")
cam2_rgb = rp_nir.build_training_rgb(raw16_nir, bit_depth=cam_nir.bit_depth).astype("float32")
min_h = min(rgb_chw.shape[1], cam2_rgb.shape[1])
min_w = min(rgb_chw.shape[2], cam2_rgb.shape[2])
rgb_chw = rgb_chw[:, :min_h, :min_w]
cam2_rgb = cam2_rgb[:, :min_h, :min_w]
# assumindo ordem [R, G, B]
re_single = cam2_rgb[0:1, :, :]
nir_single = cam2_rgb[2:3, :, :]
rgbnir = np.concatenate([rgb_chw, nir_single, re_single], axis=0)
rgbnir = self._convert_output_dtype(rgbnir, max(cam_rgb.bit_depth, cam_nir.bit_depth))
meta_extra = {
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"dtype": str(rgbnir.dtype),
"output_layout": "CHW",
"output_channels": 5,
"output_channel_names": ["R", "G", "B", "NIR", "RE"],
"output_width": int(rgbnir.shape[2]),
"output_height": int(rgbnir.shape[1]),
"width": int(rgbnir.shape[2]),
"height": int(rgbnir.shape[1]),
"channels": 5,
"payload_sources": [cam_rgb_id, cam_nir_id],
"source_cameras": [
{
"id": cam_rgb_id,
"role": cam_rgb.role,
"bayer_pattern": cam_rgb.bayer_pattern,
"bit_depth": int(cam_rgb.bit_depth),
},
{
"id": cam_nir_id,
"role": cam_nir.role,
"bayer_pattern": cam_nir.bayer_pattern,
"bit_depth": int(cam_nir.bit_depth),
},
],
"rgbnir_note": "RGB da cam0; NIR extraído do canal B da cam1; RE extraído do canal R da cam1",
}
return rgbnir, meta_extra