diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py index 5a0f56dc..1f8caf88 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py @@ -2,8 +2,6 @@ import logging from contextlib import contextmanager import labthings.fields as fields -import numpy as np -import picamerax from flask import abort from labthings import find_component from labthings.extensions import BaseExtension @@ -13,12 +11,10 @@ from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.microscope import Microscope from .recalibrate_utils import ( - auto_expose_and_freeze_settings, - flat_lens_shading_table, - get_channel_percentiles, - recalibrate_camera, adjust_shutter_and_gain_from_raw, adjust_white_balance_from_raw, + flat_lens_shading_table, + get_channel_percentiles, lst_from_camera, ) @@ -82,11 +78,9 @@ class LSTExtension(BaseExtension): "/auto_white_balance_from_raw", endpoint="auto_white_balance_from_raw", ) - - -def find_microscope(): +def find_microscope() -> Microscope: """Locate the microscope and raise a sensible error if it's missing.""" microscope = find_component("org.openflexure.microscope") @@ -96,9 +90,7 @@ def find_microscope(): ) if not hasattr(microscope.camera, "picamera"): - abort( - 503, "The PiCamera calibration plugin requires a Raspberry Pi camera." - ) + abort(503, "The PiCamera calibration plugin requires a Raspberry Pi camera.") return microscope @@ -106,10 +98,15 @@ def find_microscope(): class RecalibrateView(ActionView): args = { "skip_auto_exposure": fields.Bool( - missing=False, - description="Whether to " + missing=False, + description=( + "By default we perform an auto-exposure and AWB " + "step before updating the lens shading table. Set " + "this argument to True to disable those steps." + ), ) } + def post(self, args): """Reset the camera's settings. @@ -184,8 +181,7 @@ class AutoExposureFromRawView(ActionView): def post(self, args): camera = find_component("org.openflexure.microscope").camera.picamera adjust_shutter_and_gain_from_raw( - camera, - **args # I'm relying on the schema to fill in missing values + camera, **args # I'm relying on the schema to fill in missing values ) @@ -206,8 +202,7 @@ class AutoWhiteBalanceFromRawView(ActionView): def post(self, args): camera = find_component("org.openflexure.microscope").camera.picamera adjust_white_balance_from_raw( - camera, - **args # I'm relying on the schema to fill in missing values + camera, **args # I'm relying on the schema to fill in missing values ) diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py index b8b46044..788d856d 100644 --- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py @@ -7,6 +7,12 @@ import numpy as np from picamerax import PiCamera from picamerax.array import PiBayerArray, PiRGBArray +# I have used f strings throughout, for readability. +# The performance implication of using them in logging statements +# really doesn't matter here - also, loglevel is usually set to +# INFO to report progress via the LabThings action API +# pylint: disable=logging-fstring-interpolation + def rgb_image( camera: PiCamera, resize: Optional[Tuple[int, int]] = None, **kwargs @@ -66,6 +72,12 @@ def adjust_exposure_and_gain( iterations = 0 while True: iterations += 1 + if iterations > max_iterations: + logging.warning( + f"Auto exposure is stopping after {max_iterations} " + "but it has not converged." + ) + break with PiBayerArray(camera) as a: camera.capture(a, format="jpeg", bayer=True) @@ -86,7 +98,7 @@ def adjust_exposure_and_gain( f"{previous_shutter_speed} to {current_shutter_speed}" ) if current_shutter_speed == previous_shutter_speed: - logging.info(f"Shutter speed has saturated, stopping.") + logging.info("Shutter speed has saturated, stopping.") break if max_brightness > target_white_level: @@ -153,7 +165,6 @@ def adjust_shutter_and_gain_from_raw( # until either the brightness is high enough, or we can't increase the # shutter speed any more. iterations = 0 - shutter_speed_maximised = False shutter_speed_increments = [10, 2, None] shutter_speed_increment = shutter_speed_increments.pop(0) while iterations < max_iterations: @@ -202,14 +213,9 @@ def adjust_shutter_and_gain_from_raw( if current_shutter_speed == tested_shutter_speed: camera.analog_gain *= 2 if camera.analog_gain == tested_analog_gain: - logging.info(f"Shutter speed and gain are both maxed out. Giving up!") + logging.info("Shutter speed and gain are both maxed out. Giving up!") break - logging.info( - f"Shutter speed has maxed out at {current_shutter_speed}, doubled gain." - ) - - max_brightness = np.max(get_channel_percentiles(camera, percentile)) - logging.info(f"Brightness {max_brightness} after {iterations} tries.") + logging.info("Shutter speed has maxed out, doubled gain to compensate.") return max_brightness @@ -330,6 +336,7 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray: lens_shading_table: np.ndarray = gains.astype(np.uint8) return lens_shading_table[::-1, :, :].copy() + def lst_from_camera(camera: PiCamera) -> np.ndarray: """Acquire a raw image and use it to calculate a lens shading table.""" with PiBayerArray(camera) as a: @@ -343,6 +350,7 @@ def lst_from_camera(camera: PiCamera) -> np.ndarray: channels = channels_from_bayer_array(raw_image) return lst_from_channels(channels) + def recalibrate_camera(camera: PiCamera): """Reset the lens shading table and exposure settings.