diff --git a/picamera_coverage.zip b/picamera_coverage.zip index 9592a404..2e0aa752 100644 Binary files a/picamera_coverage.zip and b/picamera_coverage.zip differ diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 68f5c281..430e7a40 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -715,41 +715,6 @@ class StreamingPiCamera2(BaseCamera): percentile=percentile, ) - @lt.thing_action - def calibrate_white_balance( - self, - method: Literal["percentile", "centre"] = "centre", - luminance_power: float = 1.0, - ) -> None: - """Correct the white balance of the image. - - This calibration requires a neutral image, such that the 99th centile - of each colour channel should correspond to white. We calculate the - centiles and use this to set the colour gains. This is done on the raw - image with the lens shading correction applied, which should mean - that the image is uniform, rather than weighted towards the centre. - - If ``method`` is ``"centre"``, we will correct the mean of the central 10% - of the image. - """ - with self._streaming_picamera(pause_stream=True) as cam: - if self.lens_shading_is_static: - lst: LensShading = self.lens_shading_tables - recalibrate_utils.adjust_white_balance_from_raw( - cam, - self._sensor_info, - percentile=99, - luminance=lst.luminance, - Cr=lst.Cr, - Cb=lst.Cb, - luminance_power=luminance_power, - method=method, - ) - else: - recalibrate_utils.adjust_white_balance_from_raw( - cam, self._sensor_info, percentile=99, method=method - ) - @lt.thing_action def calibrate_lens_shading(self) -> None: """Take an image and use it for flat-field correction. @@ -766,8 +731,13 @@ class StreamingPiCamera2(BaseCamera): # (Cb). L, Cr, Cb = recalibrate_utils.lst_from_camera(cam, self._sensor_info) # noqa: N806 tf_utils.set_static_lst(self.tuning, L, Cr, Cb) + + # Re-initialise the picamera to reload the tuning file. self._initialise_picamera() + # Set colour gains based on the LST results + self.colour_gains = (float(np.min(Cr)), float(np.min(Cb))) + @lt.thing_property def colour_correction_matrix( self, @@ -866,9 +836,8 @@ class StreamingPiCamera2(BaseCamera): * ``flat_lens_shading`` to disable flat-field * ``auto_expose_from_minimum`` * ``set_static_green_equalisation`` to set geq offset to max - * ``calibrate_lens_shading`` + * ``calibrate_lens_shading`` (also sets colour gains for white balance) * ``reset_ccm`` - * ``calibrate_white_balance`` * ``set_background`` """ self.flat_lens_shading() @@ -877,7 +846,6 @@ class StreamingPiCamera2(BaseCamera): self.set_ce_enable_to_off() self.calibrate_lens_shading() self.reset_ccm() - self.calibrate_white_balance() time.sleep(0.5) self.set_background(portal) @@ -926,12 +894,6 @@ class StreamingPiCamera2(BaseCamera): can_terminate=False, button_primary=False, ), - action_button_for( - self.calibrate_white_balance, - submit_label="Auto White Balance", - can_terminate=False, - button_primary=False, - ), action_button_for( self.calibrate_lens_shading, submit_label="Auto Flat Field Correction", @@ -1031,37 +993,6 @@ class StreamingPiCamera2(BaseCamera): ) self._initialise_picamera() - def correct_colour_gains_for_lens_shading( - self, colour_gains: tuple[float, float] - ) -> tuple[float, float]: - """Correct white balance gains for the effect of lens shading. - - The white balance algorithm we use assumes the brightest pixels - should be white, and that the only thing affecting the colour of - said pixels is the ``colour_gains``. - - The lens shading correction is normalised such that the *minimum* - gain in the ``Cr`` and ``Cb`` channels is 1. The white balance - assumption above requires that the gain for the brightest pixels - is 1. The solution might be that, when calibrating, we note which - pixels are brightest (usually the centre) and explicitly use - the LST values for there. However, for now I will assume that we - need to normalise by the **maximum** of the ``Cr`` and ``Cb`` - channels, which is correct the majority of the time. - """ - if not self.lens_shading_is_static: - return colour_gains - lst = self.lens_shading_tables - # The Cr and Cb corrections are normalised to have a minimum of 1, - # but the white balance algorithm normalises the brightest pixels - # to be white, assuming the brightest pixels have equal gain from - # the LST. - gain_r, gain_b = colour_gains - return ( - float(gain_r / np.max(lst.Cr)), - float(gain_b / np.max(lst.Cb)), - ) - @lt.thing_action def flat_lens_shading_chrominance(self) -> None: """Disable flat-field correction. diff --git a/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py b/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py index ff15e02d..fdd0fab6 100644 --- a/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py +++ b/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py @@ -44,7 +44,7 @@ from __future__ import annotations import gc import logging import time -from typing import List, Literal, Optional, Tuple +from typing import List, Tuple from pydantic import BaseModel import numpy as np from scipy.ndimage import zoom @@ -201,83 +201,6 @@ def adjust_shutter_and_gain_from_raw( return test.level -# Explicitly allow this to have 8 arguments as the later arguments are keyword only -# We should be able to enforce this without noqa once PyLint moves PLR0917 out of -# preview -def adjust_white_balance_from_raw( # noqa: PLR0913 - camera: Picamera2, - sensor_info: SensorInfo, - *, - percentile: float = 99, - luminance: Optional[np.ndarray] = None, - Cr: Optional[np.ndarray] = None, - Cb: Optional[np.ndarray] = None, - luminance_power: float = 1.0, - method: Literal["percentile", "centre"] = "centre", -) -> Tuple[float, float]: - """Adjust the white balance in a single shot, based on the raw image. - - NB if ``channels_from_raw_image`` is broken, this will go haywire. - We should probably have better logic to verify the channels really - are BGGR... - """ - config = camera.create_still_configuration( - raw={"format": sensor_info.unpacked_pixel_format} - ) - camera.configure(config) - camera.start() - channels = _channels_from_bayer_array(camera.capture_array("raw")) - - if luminance is not None and Cr is not None and Cb is not None: - # Reconstruct a low-resolution image from the lens shading tables - # and use it to normalise the raw image, to compensate for - # the brightest pixels in each channel not coinciding. - grids = _grids_from_lst(np.array(luminance) ** luminance_power, Cr, Cb) - channel_gains = 1 / grids - if channel_gains.shape[1:] != channels.shape[1:]: - channel_gains = _upsample_channels(channel_gains, channels.shape[1:]) - LOGGER.info(f"Before gains, channel maxima are {np.max(channels, axis=(1, 2))}") - channels = channels * channel_gains - LOGGER.info(f"After gains, channel maxima are {np.max(channels, axis=(1, 2))}") - if method == "centre": - _, height, width = channels.shape - # Cut out the central 10% from 9/20 to 11/20... - low_y_range = 9 * height // 20 - hi_y_range = 11 * height // 20 - low_x_range = 9 * width // 20 - hi_x_range = 11 * width // 20 - # ... and then take the mean of each bayer channel. - centre_means = np.mean( - channels[:, low_y_range:hi_y_range, low_x_range:hi_x_range], - axis=(1, 2), - ) - # Subtract blacklevel before splitting into channels - blue, g1, g2, red = centre_means - sensor_info.blacklevel - else: - blue, g1, g2, red = ( - np.percentile(channels, percentile, axis=(1, 2)) - sensor_info.blacklevel - ) - green = (g1 + g2) / 2.0 - new_awb_gains = (green / red, green / blue) - if Cr is not None and Cb is not None: - # The LST algorithm normalises Cr and Cb by their minimum. - # The lens shading correction only ever boosts the red and blue values. - # Here, we decrease the gains by the minimum value of Cr and Cb. - new_awb_gains = (green / red * np.min(Cr), green / blue * np.min(Cb)) - - LOGGER.info( - f"Raw white point is R: {red} G: {green} B: {blue}, " - f"setting AWB gains to ({new_awb_gains[0]:.2f}, " - f"{new_awb_gains[1]:.2f})." - ) - camera.controls.AwbEnable = False - camera.controls.ColourGains = new_awb_gains - time.sleep(sensor_info.long_pause) - m = camera.capture_metadata() - LOGGER.debug(f"Camera confirms gains are now {m['ColourGains']}") - return new_awb_gains - - def lst_from_camera(camera: Picamera2, sensor_info: SensorInfo) -> LensShadingTables: """Acquire a raw image and use it to calculate a lens shading table.""" channels = _raw_channels_from_camera(camera, sensor_info)