From 506693f6532c10a0db9681ddfeb8997363358c85 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 23 Oct 2025 14:55:23 +0100 Subject: [PATCH] Set colour gains directly from Lens Shading Table --- .../things/camera/picamera.py | 46 +---------- .../camera/picamera_recalibrate_utils.py | 79 +------------------ 2 files changed, 5 insertions(+), 120 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 68f5c281..7e93ba9a 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,12 @@ 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) + self._initialise_picamera() + with self._streaming_picamera(pause_stream=True) as cam: + self.colour_gains = (float(np.min(Cr)), float(np.min(Cb))) + @lt.thing_property def colour_correction_matrix( self, @@ -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", 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)