Set colour gains directly from Lens Shading Table

This commit is contained in:
jaknapper 2025-10-23 14:55:23 +01:00 committed by Julian Stirling
parent 21f0c81a88
commit 506693f653
2 changed files with 5 additions and 120 deletions

View file

@ -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",

View file

@ -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)