diff --git a/docs/api/pylabrobot.plate_reading.rst b/docs/api/pylabrobot.plate_reading.rst index 67b6fc34a18..bc2eb829f1f 100644 --- a/docs/api/pylabrobot.plate_reading.rst +++ b/docs/api/pylabrobot.plate_reading.rst @@ -10,7 +10,9 @@ This package contains APIs for working with plate readers. :nosignatures: :recursive: - plate_reader.PlateReader + plate_reader.PlateReader + imager.Imager + standard.ImagingResult Backends diff --git a/docs/user_guide/02_analytical/plate-reading/cytation5.ipynb b/docs/user_guide/02_analytical/plate-reading/cytation5.ipynb index d3cd3b3fb2d..6511debecc8 100644 --- a/docs/user_guide/02_analytical/plate-reading/cytation5.ipynb +++ b/docs/user_guide/02_analytical/plate-reading/cytation5.ipynb @@ -317,7 +317,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -342,7 +342,7 @@ } ], "source": [ - "ims = await pr.capture(\n", + "res = await pr.capture(\n", " well=(1, 2),\n", " mode=ImagingMode.BRIGHTFIELD,\n", " objective=Objective.O_4x_PL_FL_PHASE,\n", @@ -351,7 +351,7 @@ " gain=16,\n", " led_intensity=10,\n", ")\n", - "plt.imshow(ims[0], cmap=\"gray\", vmin=0, vmax=255)" + "plt.imshow(res.images[0], cmap=\"gray\", vmin=0, vmax=255)" ] }, { @@ -374,7 +374,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -399,7 +399,7 @@ } ], "source": [ - "ims = await pr.capture(\n", + "res = await pr.capture(\n", " well=(1, 2),\n", " mode=ImagingMode.BRIGHTFIELD,\n", " objective=Objective.O_4x_PL_FL_PHASE,\n", @@ -408,7 +408,7 @@ " gain=16,\n", " led_intensity=10\n", ")\n", - "plt.imshow(ims[0], cmap=\"gray\", vmin=0, vmax=255)" + "plt.imshow(res.images[0], cmap=\"gray\", vmin=0, vmax=255)" ] }, { @@ -440,7 +440,7 @@ "from pylabrobot.plate_reading.imager import Imager, max_pixel_at_fraction, fraction_overexposed\n", "from pylabrobot.plate_reading.standard import AutoExposure\n", "\n", - "ims = await pr.capture(\n", + "res = await pr.capture(\n", " exposure_time=AutoExposure(\n", " # evaluate_exposure=fraction_overexposed(fraction=0.005, margin=0.005/10),\n", " evaluate_exposure=max_pixel_at_fraction(fraction=0.90, margin=0.05),\n", @@ -467,14 +467,14 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from PIL import Image\n", "import numpy as np\n", "\n", - "array = np.array(ims[0], dtype=np.float32)\n", + "array = np.array(res.images[0], dtype=np.float32)\n", "array_uint16 = (array * (65535 / 255)).astype(np.uint16)\n", "Image.fromarray(array_uint16).save(\"test.tiff\")" ] @@ -492,7 +492,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -510,7 +510,7 @@ "num_rows = 4\n", "num_cols = 4\n", "\n", - "ims = await pr.capture(\n", + "res = await pr.capture(\n", " well=(1, 2),\n", " mode=ImagingMode.BRIGHTFIELD,\n", " objective=Objective.O_4x_PL_FL_PHASE,\n", @@ -520,12 +520,12 @@ " coverage=(num_rows, num_cols),\n", " center_position=(-6, 0),\n", ")\n", - "len(ims)" + "len(res.images)" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -544,7 +544,7 @@ "for row in range(num_rows):\n", " for col in range(num_cols):\n", " plt.subplot(num_rows, num_cols, row * num_cols + col + 1)\n", - " plt.imshow(ims[row * num_cols + col], cmap=\"gray\", vmin=0, vmax=255)\n", + " plt.imshow(res.images[row * num_cols + col], cmap=\"gray\", vmin=0, vmax=255)\n", " plt.axis(\"off\")" ] }, diff --git a/pylabrobot/plate_reading/__init__.py b/pylabrobot/plate_reading/__init__.py index 966430ea098..a80f03d029f 100644 --- a/pylabrobot/plate_reading/__init__.py +++ b/pylabrobot/plate_reading/__init__.py @@ -3,4 +3,11 @@ from .image_reader import ImageReader from .imager import Imager from .plate_reader import PlateReader -from .standard import Exposure, FocalPosition, Gain, ImagingMode, Objective +from .standard import ( + Exposure, + FocalPosition, + Gain, + ImagingMode, + ImagingResult, + Objective, +) diff --git a/pylabrobot/plate_reading/backend.py b/pylabrobot/plate_reading/backend.py index bc6bd8e1a85..5de74dfc464 100644 --- a/pylabrobot/plate_reading/backend.py +++ b/pylabrobot/plate_reading/backend.py @@ -8,8 +8,8 @@ Exposure, FocalPosition, Gain, - Image, ImagingMode, + ImagingResult, Objective, ) from pylabrobot.resources.plate import Plate @@ -69,7 +69,7 @@ async def capture( focal_height: FocalPosition, gain: Gain, plate: Plate, - ) -> List[Image]: + ) -> ImagingResult: """Capture an image of the plate in the specified mode.""" diff --git a/pylabrobot/plate_reading/biotek_backend.py b/pylabrobot/plate_reading/biotek_backend.py index a576a826a47..9abd3ae9a39 100644 --- a/pylabrobot/plate_reading/biotek_backend.py +++ b/pylabrobot/plate_reading/biotek_backend.py @@ -43,6 +43,7 @@ Gain, Image, ImagingMode, + ImagingResult, Objective, ) @@ -882,7 +883,7 @@ async def auto_focus(self, timeout: float = 30): # objective function: variance of laplacian async def evaluate_focus(focus_value): - images = await self.capture( # TODO: _acquire_image + result = await self.capture( # TODO: _acquire_image plate=plate, row=row, column=column, @@ -892,7 +893,7 @@ async def evaluate_focus(focus_value): exposure_time=exposure, gain=gain, ) - image = images[0] # self.capture returns List now + image = result.images[0] if not CV2_AVAILABLE: raise RuntimeError( @@ -1158,7 +1159,7 @@ async def capture( overlap: Optional[float] = None, color_processing_algorithm: int = SPINNAKER_COLOR_PROCESSING_ALGORITHM_HQ_LINEAR, pixel_format: int = PixelFormat_Mono8, - ) -> List[Image]: + ) -> ImagingResult: """Capture image using the microscope speed: 211 ms ± 331 μs per loop (mean ± std. dev. of 7 runs, 10 loops each) @@ -1234,4 +1235,8 @@ def image_size(magnification: float) -> Tuple[float, float]: ) ) - return images + exposure_ms = float(self.cam.ExposureTime.GetValue()) / 1000 + assert self._focal_height is not None, "Focal height not set. Run set_focus() first." + focal_height_val = float(self._focal_height) + + return ImagingResult(images=images, exposure_time=exposure_ms, focal_height=focal_height_val) diff --git a/pylabrobot/plate_reading/imager.py b/pylabrobot/plate_reading/imager.py index 8d4dfb2a959..9928fd0f12b 100644 --- a/pylabrobot/plate_reading/imager.py +++ b/pylabrobot/plate_reading/imager.py @@ -1,5 +1,5 @@ import math -from typing import Awaitable, Callable, List, Literal, Optional, Tuple, Union, cast +from typing import Awaitable, Callable, Literal, Optional, Tuple, Union, cast from pylabrobot.machines import Machine from pylabrobot.plate_reading.backend import ImagerBackend @@ -10,6 +10,7 @@ Gain, Image, ImagingMode, + ImagingResult, NoPlateError, Objective, ) @@ -68,7 +69,7 @@ async def _capture_auto_exposure( focal_height: float, gain: float, **backend_kwargs, - ) -> List[Image]: + ) -> ImagingResult: """ Capture an image with auto exposure. @@ -99,7 +100,7 @@ def _rms_split(low: float, high: float) -> float: rounds += 1 p = _rms_split(low, high) - ims = await self.capture( + res = await self.capture( well=well, mode=mode, objective=objective, @@ -108,18 +109,18 @@ def _rms_split(low: float, high: float) -> float: gain=gain, **backend_kwargs, ) - assert len(ims) == 1, "Expected exactly one image to be returned" - im = ims[0] - result = await auto_exposure.evaluate_exposure(im) + assert len(res.images) == 1, "Expected exactly one image to be returned" + im = res.images[0] + evaluation = await auto_exposure.evaluate_exposure(im) - if result == "good": - return ims - if result == "lower": + if evaluation == "good": + return res + if evaluation == "lower": high = p - elif result == "higher": + elif evaluation == "higher": low = p else: - raise ValueError(f"Unexpected evaluation result: {result}") + raise ValueError(f"Unexpected evaluation result: {evaluation}") raise RuntimeError("Failed to find a good exposure time.") @@ -132,7 +133,7 @@ async def capture( focal_height: FocalPosition = "machine-auto", gain: Gain = "machine-auto", **backend_kwargs, - ) -> List[Image]: + ) -> ImagingResult: if isinstance(well, tuple): row, column = well else: diff --git a/pylabrobot/plate_reading/standard.py b/pylabrobot/plate_reading/standard.py index 579ff158e1b..1129bca443b 100644 --- a/pylabrobot/plate_reading/standard.py +++ b/pylabrobot/plate_reading/standard.py @@ -101,3 +101,10 @@ class AutoExposure: Exposure = Union[float, Literal["machine-auto"]] FocalPosition = Union[float, Literal["machine-auto"]] Gain = Union[float, Literal["machine-auto"]] + + +@dataclass +class ImagingResult: + images: List[Image] + exposure_time: float + focal_height: float