Update tuning file utils for our own tuning files.

This commit is contained in:
Julian Stirling 2025-10-30 15:27:13 +00:00
parent f74a51866a
commit ec16cc17f8
3 changed files with 70 additions and 49 deletions

View file

@ -112,7 +112,7 @@ class SensorModeSelector(BaseModel):
These values are the output size and the bit depth.
This is a Pydantic model so that it can be saved to disk.
This is a Pydantic model so that it can sent by FastAPI
"""
output_size: tuple[int, int]
@ -126,12 +126,13 @@ class LensShading(BaseModel):
(12, 16) in size. The arrays are luminance, red-difference chroma (Cr), and
blue-difference chroma (Cb).
This is a Pydantic model so that it can be saved to the disk.
This is a Pydantic model so that it can sent by FastAPI
"""
luminance: list[list[float]]
Cr: list[list[float]]
Cb: list[list[float]]
colour_temp: int
class StreamingPiCamera2(BaseCamera):
@ -216,7 +217,8 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_property
def calibration_required(self) -> bool:
"""Whether the camera needs calibrating."""
return not self.lens_shading_is_static
# Check if the lens shading table is calibrated.
return not tf_utils.lst_calibrated(self.tuning)
## Persistent controls! These are settings
@ -730,7 +732,12 @@ class StreamingPiCamera2(BaseCamera):
# luminance (L), red-difference chroma (Cr), and blue-difference chroma
# (Cb).
L, Cr, Cb = recalibrate_utils.lst_from_camera(cam, self._sensor_info) # noqa: N806
self.tuning = tf_utils.set_static_lst(self.tuning, L, Cr, Cb)
self.tuning = tf_utils.set_lst(
self.tuning,
luminance=L,
cr=Cr,
cb=Cb,
)
# Re-initialise the picamera to reload the tuning file.
self._initialise_picamera()
@ -750,14 +757,14 @@ class StreamingPiCamera2(BaseCamera):
It is a 9 value tuple used to specify the 3x3 matrix that the GPU pipeline uses
to convert from the camera R,G,B vector to the standard R,G,B.
"""
return tuple(tf_utils.get_static_ccm(self.tuning)[0]["ccm"])
return tuple(tf_utils.get_ccm(self.tuning)[0]["ccm"])
@colour_correction_matrix.setter # type: ignore
def colour_correction_matrix(
self,
value: tuple[float, float, float, float, float, float, float, float, float],
) -> None:
self.tuning = tf_utils.set_static_ccm(self.tuning, value)
self.tuning = tf_utils.set_ccm(self.tuning, value)
if self._picamera is not None:
with self._streaming_picamera(pause_stream=True):
@ -833,8 +840,11 @@ class StreamingPiCamera2(BaseCamera):
with self._streaming_picamera(pause_stream=True):
# Generate and array of ones of the correct size for each channel
flat_array = np.ones((12, 16))
self.tuning = tf_utils.set_static_lst(
self.tuning, flat_array, flat_array, flat_array
self.tuning = tf_utils.set_lst(
self.tuning,
luminance=flat_array,
cr=flat_array,
cb=flat_array,
)
self._initialise_picamera()
@ -930,9 +940,6 @@ class StreamingPiCamera2(BaseCamera):
- adaptive control is enabled
- multiple LSTs in use (for different colour temperatures),
"""
if not self.lens_shading_is_static:
return None
# Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction"
alsc = self.get_tuning_algo("rpi.alsc")
@ -951,23 +958,25 @@ class StreamingPiCamera2(BaseCamera):
luminance=reshape_lst(alsc["luminance_lut"]),
Cr=reshape_lst(alsc["calibrations_Cr"][0]["table"]),
Cb=reshape_lst(alsc["calibrations_Cb"][0]["table"]),
colour_temp=alsc["calibrations_Cb"][0]["ct"],
)
@lens_shading_tables.setter
def lens_shading_tables(self, lst: LensShading) -> None:
"""Set the lens shading tables."""
with self._streaming_picamera(pause_stream=True):
self.tuning = tf_utils.set_static_lst(
self.tuning = tf_utils.set_lst(
self.tuning,
luminance=lst.luminance,
cr=lst.Cr,
cb=lst.Cb,
colour_temp=lst.colour_temp,
)
self._initialise_picamera()
@lt.thing_action
def flat_lens_shading_chrominance(self) -> None:
"""Disable flat-field correction.
"""Disable flat-field correction for colour only.
This method will set the chrominance of the lens shading table to be
flat, i.e. we'll correct vignetting of intensity, but not any change in
@ -977,7 +986,13 @@ class StreamingPiCamera2(BaseCamera):
alsc = self.get_tuning_algo("rpi.alsc")
luminance = alsc["luminance_lut"]
flat = np.ones((12, 16))
self.tuning = tf_utils.set_static_lst(self.tuning, luminance, flat, flat)
self.tuning = tf_utils.set_lst(
self.tuning,
luminance=luminance,
cr=flat,
cb=flat,
colour_temp=1234,
)
self._initialise_picamera()
@lt.thing_action
@ -995,17 +1010,6 @@ class StreamingPiCamera2(BaseCamera):
)
self._initialise_picamera()
@lt.thing_property
def lens_shading_is_static(self) -> bool:
"""Whether the lens shading is static.
This property is true if the lens shading correction has been set to use
a static table (i.e. the number of automatic correction iterations is zero).
The default LST is not static, but all the calibration controls will set it
to be static (except "reset")
"""
return tf_utils.lst_is_static(self.tuning)
@property
def thing_state(self) -> Mapping[str, Any]:
"""Update generic camera metadata with Picamera-specific data."""

View file

@ -1,7 +1,6 @@
"""Functions for loading, adjusting, or reading from the Picamera2 tuning file.
The functions that edit the tuning files edit them in place. This will change in
the future.
The functions that edit the tuning files return a new dictionary that is updated.
"""
from typing import Any
@ -60,44 +59,65 @@ def find_tuning_algo(tuning: dict[str, dict], name: str) -> dict[str, Any]:
return algo_dict[name]
def set_static_lst(
def set_lst(
tuning: dict,
*,
luminance: np.ndarray,
cr: np.ndarray,
cb: np.ndarray,
colour_temp: int = 5000,
) -> dict:
"""Update the ``rpi.alsc`` section of a camera tuning dict to use a static correction.
"""Update the ``rpi.alsc`` section of with new lens shading tables.
``tuning`` will be updated in-place to set its shading to static, and disable any
adaptive tweaking by the algorithm.
Only one set of tables is set so no adaptive lens shading will be used.
:param tuning: The current tuning file.
:param luminance: The table of luminance values, as (12, 16) numpy array
:param cr: The table of cr values, as (12, 16) numpy array
:param cb: The table of cb values, as (12, 16) numpy array
:param colour_temp: The colour temperature to set. By default this is 5000. Set a
different value for the PiCamera Thing to report that the lens shading is not
calibrated.
:return: an updated tuning dict with the new lens shading tables.
"""
output_tuning = deepcopy(tuning)
for table in luminance, cr, cb:
if np.array(table).shape != (12, 16):
raise ValueError("Lens shading tables must be 12x16!")
alsc = find_tuning_algo(output_tuning, "rpi.alsc")
alsc["n_iter"] = 0 # disable the adaptive part
alsc["n_iter"] = 0 # disable the adaptive part.
alsc["luminance_strength"] = 1.0
alsc["calibrations_Cr"] = [
{"ct": 4500, "table": _as_flat_rounded_list(cr, round_to=3)}
{"ct": colour_temp, "table": _as_flat_rounded_list(cr, round_to=3)}
]
alsc["calibrations_Cb"] = [
{"ct": 4500, "table": _as_flat_rounded_list(cb, round_to=3)}
{"ct": colour_temp, "table": _as_flat_rounded_list(cb, round_to=3)}
]
alsc["luminance_lut"] = _as_flat_rounded_list(luminance, round_to=3)
return output_tuning
def set_static_ccm(
def lst_calibrated(tuning: dict) -> bool:
"""Whether the lens shading table is calibrated.
This checks whether the lens shading table is has a colour temperature of 5000. As
this is what we set on calibration. Our tuning file sets a temperature of 1234.
"""
alsc = find_tuning_algo(tuning, "rpi.alsc")
return alsc["calibrations_Cr"][0]["ct"] == 5000
def set_ccm(
tuning: dict,
col_corr_matrix: tuple[
float, float, float, float, float, float, float, float, float
],
) -> dict:
"""Update the ``rpi.alsc`` section of a camera tuning dict to use a static correction.
"""Update the ``rpi.alsc`` section of a camera tuning dict set the colour correction matrix.
``tuning`` will be updated in-place to set its shading to static, and disable any
adaptive tweaking by the algorithm.
:param tuning: The current tuning dict
:param col_corr_matrix: The colour correction matrix to set
:return: an updated tuning dict with the new colour correction matrix.
"""
output_tuning = deepcopy(tuning)
ccm = find_tuning_algo(output_tuning, "rpi.ccm")
@ -105,32 +125,26 @@ def set_static_ccm(
return output_tuning
def get_static_ccm(tuning: dict) -> None:
def get_ccm(tuning: dict) -> None:
"""Get a copy of the the ``rpi.ccm`` section of a camera tuning dict."""
ccm = find_tuning_algo(tuning, "rpi.ccm")
return deepcopy(ccm["ccms"])
def lst_is_static(tuning: dict) -> bool:
"""Whether the lens shading table is set to static."""
alsc = find_tuning_algo(tuning, "rpi.alsc")
return alsc["n_iter"] == 0
def set_static_geq(
tuning: dict,
offset: int = 65535,
) -> dict:
"""Update the ``rpi.geq`` section of a camera tuning dict.
:param tuning: the raspberry pi tuning file. This will be updated in-place to
set the geq offset to the given value.
:param tuning: the raspberry pi tuning dictionary
:param offset: The desired green equalisation offset. Default 65535. The default is
the maximum allowed value. This means the brightness will always be below the
threshold where averaging is used. This is default as we always need the green
equalisation to averages the green pixels in the red and blue rows due to the
chief ray angle compensation issue when the the stock lens is replaced by an
objective.
:param return: An updated tuning dictionary
"""
output_tuning = deepcopy(tuning)
geq = find_tuning_algo(output_tuning, "rpi.geq")
@ -182,7 +196,7 @@ def copy_algo_from_other_tuning(
# Find the relevant sub-dict for each tuning file
from_i = _index_of_algorithm(copy_from["algorithms"], algo)
to_i = _index_of_algorithm(base_tuning_file["algorithms"], algo)
# Updating the dictionary in place.
# Updating the output_tuning copy.
output_tuning["algorithms"][to_i] = deepcopy(copy_from["algorithms"][from_i])
return output_tuning

View file

@ -123,7 +123,6 @@ def test_thing_description_equivalence(mock_picam_thing):
"colour_correction_matrix",
"lens_shading_tables",
"sensor_resolution",
"lens_shading_is_static",
"capture_metadata",
"camera_configuration",
"stream_resolution",
@ -131,5 +130,9 @@ def test_thing_description_equivalence(mock_picam_thing):
"sensor_modes",
"sensor_mode",
}
for action in picamera_extra_actions:
assert action in picamera_actions
for props in picamera_extra_props:
assert props in picamera_props
assert picamera_actions - base_actions == picamera_extra_actions
assert picamera_props - base_props == picamera_extra_props