Record fom as other sharpness metric

This commit is contained in:
jaknapper 2026-05-07 16:34:45 +01:00
parent eff66eb667
commit 881970ce34
2 changed files with 50 additions and 5 deletions

View file

@ -38,6 +38,7 @@ class SharpnessMethod(enum.Enum):
"""The possible SharpnessMethods for autofocus.""" """The possible SharpnessMethods for autofocus."""
JPEG = enum.auto() JPEG = enum.auto()
FOCUS_FOM = enum.auto()
class AutofocusParams(BaseModel): class AutofocusParams(BaseModel):
@ -235,6 +236,7 @@ class SharpnessDataArrays(BaseModel):
jpeg_times: NDArray jpeg_times: NDArray
jpeg_sizes: NDArray jpeg_sizes: NDArray
focus_foms: NDArray
stage_times: NDArray stage_times: NDArray
stage_positions: list[dict[str, int]] stage_positions: list[dict[str, int]]
@ -253,7 +255,12 @@ class JPEGSharpnessMonitor:
""" """
def __init__(self, stage: BaseStage, camera: BaseCamera) -> None: def __init__(
self,
stage: BaseStage,
camera: BaseCamera,
method: SharpnessMethod = SharpnessMethod.JPEG,
) -> None:
"""Initialise a new JPEGSharpnessMonitor. The args are injected automatically. """Initialise a new JPEGSharpnessMonitor. The args are injected automatically.
:param stage: A direct_thing_client dependency for the the microscope stage. :param stage: A direct_thing_client dependency for the the microscope stage.
@ -262,11 +269,13 @@ class JPEGSharpnessMonitor:
""" """
self.camera = camera self.camera = camera
self.stage = stage self.stage = stage
self.method = method
LOGGER.debug(f"Created sharpness monitor with {stage}, {camera}") LOGGER.debug(f"Created sharpness monitor with {stage}, {camera}")
self._stage_positions: list[Mapping[str, int]] = [] self._stage_positions: list[Mapping[str, int]] = []
self._stage_times: list[float] = [] self._stage_times: list[float] = []
self._jpeg_times: list[float] = [] self._jpeg_times: list[float] = []
self._jpeg_sizes: list[int] = [] self._jpeg_sizes: list[int] = []
self._focus_foms: list[float] = []
@property @property
def stage_positions(self) -> Sequence[Mapping[str, int]]: def stage_positions(self) -> Sequence[Mapping[str, int]]:
@ -288,14 +297,28 @@ class JPEGSharpnessMonitor:
"""The recorded JPEG frame sizes used as a sharpness metric.""" """The recorded JPEG frame sizes used as a sharpness metric."""
return self._jpeg_sizes return self._jpeg_sizes
@property
def focus_foms(self) -> Sequence[float]:
"""The recorded FocusFoM values."""
return self._focus_foms
running = False running = False
async def monitor_sharpness(self) -> None: async def monitor_sharpness(self) -> None:
"""Start monitoring the frame sizes.""" """Start monitoring sharpness metrics."""
self.running = True self.running = True
async for frame in self.camera.lores_mjpeg_stream.frame_async_generator(): async for frame in self.camera.lores_mjpeg_stream.frame_async_generator():
self._jpeg_times.append(time.time()) self._jpeg_times.append(time.time())
# JPEG sharpness metric
self._jpeg_sizes.append(len(frame)) self._jpeg_sizes.append(len(frame))
# FocusFoM metric
fom = getattr(self.camera, "_focus_fom", None)
if fom is not None:
self._focus_foms.append(float(fom))
else:
self._focus_foms.append(np.nan)
if not self.running: if not self.running:
break break
@ -352,7 +375,13 @@ class JPEGSharpnessMonitor:
if istop is None: if istop is None:
istop = istart + 2 istop = istart + 2
jpeg_times: np.ndarray = np.array(self.jpeg_times) jpeg_times: np.ndarray = np.array(self.jpeg_times)
jpeg_sizes: np.ndarray = np.array(self.jpeg_sizes) # Two sharpness metrics are measured - this chooses which to use to focus
if self.method == SharpnessMethod.JPEG:
sharpnesses = np.array(self.jpeg_sizes)
elif self.method == SharpnessMethod.FOCUS_FOM:
sharpnesses = np.array(self.focus_foms)
else:
raise ValueError(f"Unknown sharpness method: {self.method}")
stage_times: np.ndarray = np.array(self.stage_times)[istart:istop] stage_times: np.ndarray = np.array(self.stage_times)[istart:istop]
stage_heights: np.ndarray = np.array( stage_heights: np.ndarray = np.array(
[p["z"] for p in self.stage_positions[istart:istop]] [p["z"] for p in self.stage_positions[istart:istop]]
@ -373,7 +402,7 @@ class JPEGSharpnessMonitor:
LOGGER.debug("changing stop to %s", (stop)) LOGGER.debug("changing stop to %s", (stop))
jpeg_times = jpeg_times[start:stop] jpeg_times = jpeg_times[start:stop]
jpeg_heights: np.ndarray = np.interp(jpeg_times, stage_times, stage_heights) jpeg_heights: np.ndarray = np.interp(jpeg_times, stage_times, stage_heights)
return jpeg_times, jpeg_heights, jpeg_sizes[start:stop] return jpeg_times, jpeg_heights, sharpnesses[start:stop]
def sharpest_z_on_move(self, data_index: int) -> int: def sharpest_z_on_move(self, data_index: int) -> int:
"""Return the z position of the sharpest image on a given move.""" """Return the z position of the sharpest image on a given move."""
@ -388,7 +417,13 @@ class JPEGSharpnessMonitor:
def data_to_array(self) -> SharpnessDataArrays: def data_to_array(self) -> SharpnessDataArrays:
"""Return the gathered data as SharpnessDataArrays.""" """Return the gathered data as SharpnessDataArrays."""
data = {} data = {}
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]: for k in [
"jpeg_times",
"jpeg_sizes",
"stage_times",
"focus_foms",
"stage_positions",
]:
data[k] = getattr(self, k) data[k] = getattr(self, k)
return SharpnessDataArrays(**data) return SharpnessDataArrays(**data)

View file

@ -28,6 +28,7 @@ from types import TracebackType
from typing import Annotated, Any, Iterator, Literal, Mapping, Optional, Self from typing import Annotated, Any, Iterator, Literal, Mapping, Optional, Self
import numpy as np import numpy as np
from libcamera import Request
from picamera2 import Picamera2 from picamera2 import Picamera2
from picamera2.encoders import MJPEGEncoder from picamera2.encoders import MJPEGEncoder
from picamera2.outputs import Output from picamera2.outputs import Output
@ -366,6 +367,9 @@ class StreamingPiCamera2(BaseCamera):
if self._picamera is None: if self._picamera is None:
# Type narrow (error if failure) # Type narrow (error if failure)
raise RuntimeError("Failed to start Picamera") raise RuntimeError("Failed to start Picamera")
self._picamera.pre_callback = self._on_frame_complete
if check_sensor_model: if check_sensor_model:
hw_sensor_model = self._picamera.camera_properties["Model"] hw_sensor_model = self._picamera.camera_properties["Model"]
if hw_sensor_model != self._sensor_info.sensor_model: if hw_sensor_model != self._sensor_info.sensor_model:
@ -374,6 +378,12 @@ class StreamingPiCamera2(BaseCamera):
f"but found {hw_sensor_model}." f"but found {hw_sensor_model}."
) )
def _on_frame_complete(self, request: Request) -> None:
md = request.get_metadata()
fom = md.get("FocusFoM")
if fom is not None:
self._focus_fom = fom
def __enter__(self) -> Self: def __enter__(self) -> Self:
"""Start streaming when the Thing context manager is opened. """Start streaming when the Thing context manager is opened.