Merge branch 'remove-whitebalance' into 'v3'
Set Colour Gains from lens shading tables Closes #451 See merge request openflexure/openflexure-microscope-server!417
This commit is contained in:
commit
5cd8a0a38d
3 changed files with 7 additions and 153 deletions
Binary file not shown.
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue