agrobot_base/Python/raspi/pi/frame_service.py

240 lines
8.9 KiB
Python

import time
import threading
import base64
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._ensure_raw_processor()
def _ensure_raw_processor(self):
if (
not hasattr(self, "raw_processor") or
self.raw_processor.sensor_width != self.state.width or
self.raw_processor.sensor_height != self.state.height or
self.raw_processor.bayer_pattern.upper() != self.state.source_bayer_pattern.upper()
):
from raw_processor_core import RawProcessorCore
self.raw_processor = RawProcessorCore(
sensor_width=self.state.width,
sensor_height=self.state.height,
bayer_pattern=self.state.source_bayer_pattern,
)
def _capture_with_retry(self, max_attempts=10, retry_delay_s=0.02):
last_info = None
for _ in range(max_attempts):
info = self.camera.capture_raw_frame()
if (
info is not None and
info.get("frame") is not None and
info.get("width", 0) > 0 and
info.get("height", 0) > 0 and
info.get("channels", 0) > 0
):
return info
last_info = info
time.sleep(retry_delay_s)
if last_info is None:
raise RuntimeError(f"Capture retornou frame vazio após {max_attempts} tentativas")
raise RuntimeError(
f"Capture retornou frame vazio após {max_attempts} tentativas: "
f"width={last_info.get('width')}, "
f"height={last_info.get('height')}, "
f"channels={last_info.get('channels')}, "
f"camera_frame_id={last_info.get('frame_id')}, "
f"camera_frame_ts={last_info.get('timestamp')}"
)
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
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_frame_info = self._capture_with_retry()
cap_end = time.perf_counter()
dt_capture = cap_end - cap_start
proc_start = time.perf_counter()
if self.state.frame_type != "RAW_BRUTO":
self._ensure_raw_processor()
frame_out, meta_extra = self._build_output_frame(raw_frame_info)
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
meta = {
"frame_id": frame_id,
"camera_frame_id": int(raw_frame_info["frame_id"]),
"camera_frame_ts": raw_frame_info["timestamp"],
"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,
"source_bayer_pattern": self.state.source_bayer_pattern,
"source_bit_depth": self.state.source_bit_depth,
}
meta.update(meta_extra)
return frame_out, meta
def capture_frame_base64(self):
frame, meta = self.capture_frame_raw()
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_frame_info):
if self.state.frame_type == "RAW_BRUTO":
return self._process_raw_bruto(raw_frame_info)
if self.state.frame_type == "RGB":
return self._process_rgb(raw_frame_info)
if self.state.frame_type == "RGBNIR":
return self._process_rgbnir(raw_frame_info)
raise RuntimeError(f"frame_type inválido: {self.state.frame_type}")
def _convert_output_dtype(self, arr):
max_sensor_value = (1 << int(self.state.source_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"):
# assume 10-bit/16-bit vindo do pipeline
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}")
elif 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_frame_info):
frame = raw_frame_info["frame"]
meta_extra = {
"frame_type": self.state.frame_type,
"payload_format_version": self.state.payload_format_version,
"output_dtype": str(frame.dtype),
"dtype": str(frame.dtype),
"output_layout": "HW",
"output_channels": 1,
"output_channel_names": ["RAW10_PACKED"],
"output_width": int(raw_frame_info["width"]),
"output_height": int(raw_frame_info["height"]),
"source_width": int(self.state.width),
"source_height": int(self.state.height),
"packed_width": int(raw_frame_info["width"]),
"packed_height": int(raw_frame_info["height"]),
"width": int(raw_frame_info["width"]),
"height": int(raw_frame_info["height"]),
"channels": 1,
}
return frame, meta_extra
def _process_rgb(self, raw_frame_info):
packed = raw_frame_info["frame"]
b_depth = self.state.source_bit_depth
if packed.ndim == 3 and packed.shape[2] == 1:
packed = packed[:, :, 0]
raw16 = self.raw_processor.unpack_raw10_packed(packed)
rgb_chw = self.raw_processor.build_training_rgb(raw16, bit_depth=b_depth)
rgb_chw = self._convert_output_dtype(rgb_chw)
meta_extra = {
"frame_type": self.state.frame_type,
"payload_format_version": self.state.payload_format_version,
"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]),
"source_width": int(self.state.width),
"source_height": int(self.state.height),
"width": int(rgb_chw.shape[2]),
"height": int(rgb_chw.shape[1]),
"channels": 3,
"packed_width": int(raw_frame_info["width"]),
"packed_height": int(raw_frame_info["height"]),
}
return rgb_chw, meta_extra
def _process_rgbnir(self, raw_frame_info):
raise NotImplementedError("RGBNIR ainda não implementado")