Let the tuning file utils handle lens shading table manipulation.
This commit is contained in:
parent
ec16cc17f8
commit
6763845e38
2 changed files with 129 additions and 134 deletions
|
|
@ -15,7 +15,7 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Annotated, Iterator, Literal, Mapping, Optional, overload, Any
|
from typing import Annotated, Iterator, Literal, Mapping, Optional, Any
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -119,22 +119,6 @@ class SensorModeSelector(BaseModel):
|
||||||
bit_depth: int
|
bit_depth: int
|
||||||
|
|
||||||
|
|
||||||
class LensShading(BaseModel):
|
|
||||||
"""A Pydantic model holding the lens shading tables.
|
|
||||||
|
|
||||||
PiCamera needs three numpy arrays for lens shading correction. Each array is
|
|
||||||
(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 sent by FastAPI
|
|
||||||
"""
|
|
||||||
|
|
||||||
luminance: list[list[float]]
|
|
||||||
Cr: list[list[float]]
|
|
||||||
Cb: list[list[float]]
|
|
||||||
colour_temp: int
|
|
||||||
|
|
||||||
|
|
||||||
class StreamingPiCamera2(BaseCamera):
|
class StreamingPiCamera2(BaseCamera):
|
||||||
"""A Thing that provides and interface to the Raspberry Pi Camera.
|
"""A Thing that provides and interface to the Raspberry Pi Camera.
|
||||||
|
|
||||||
|
|
@ -348,42 +332,6 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
|
tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
|
||||||
"""The Raspberry PiCamera Tuning File JSON."""
|
"""The Raspberry PiCamera Tuning File JSON."""
|
||||||
|
|
||||||
# Use overload to clarify that only a dictionary is returned if `raise_if_missing`
|
|
||||||
# is True
|
|
||||||
@overload
|
|
||||||
def get_tuning_algo(
|
|
||||||
self, algorithm_name: str, raise_if_missing: Literal[True]
|
|
||||||
) -> dict: ...
|
|
||||||
# Otherwise may also be None
|
|
||||||
@overload
|
|
||||||
def get_tuning_algo(
|
|
||||||
self, algorithm_name: str, raise_if_missing: bool
|
|
||||||
) -> Optional[dict]: ...
|
|
||||||
|
|
||||||
def get_tuning_algo(
|
|
||||||
self, algorithm_name: str, raise_if_missing: bool = True
|
|
||||||
) -> Optional[dict]:
|
|
||||||
"""Return the active tuning algorithm settings for the given algorithm.
|
|
||||||
|
|
||||||
:returns: The algorithm dictionary if found, returns None if no tuning data
|
|
||||||
is loaded or if the tuning algorithm is not found.
|
|
||||||
|
|
||||||
:raises MissingCalibrationError: If raise_if_missing is true and there is no
|
|
||||||
tuning file is available, or the requested algorithm is not present.
|
|
||||||
"""
|
|
||||||
if self.tuning is None:
|
|
||||||
if raise_if_missing:
|
|
||||||
raise MissingCalibrationError("No tuning data is set.")
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return tf_utils.find_tuning_algo(self.tuning, algorithm_name)
|
|
||||||
except StopIteration as e:
|
|
||||||
if raise_if_missing:
|
|
||||||
raise MissingCalibrationError(
|
|
||||||
f"No tuning algorithm with name {algorithm_name}."
|
|
||||||
) from e
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _initialise_picamera(self, check_sensor_model: bool = False) -> None:
|
def _initialise_picamera(self, check_sensor_model: bool = False) -> None:
|
||||||
"""Acquire the picamera device and store it as ``self._picamera``.
|
"""Acquire the picamera device and store it as ``self._picamera``.
|
||||||
|
|
||||||
|
|
@ -737,6 +685,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
luminance=L,
|
luminance=L,
|
||||||
cr=Cr,
|
cr=Cr,
|
||||||
cb=Cb,
|
cb=Cb,
|
||||||
|
colour_temp=tf_utils.CALIBRATED_COLOUR_TEMP,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Re-initialise the picamera to reload the tuning file.
|
# Re-initialise the picamera to reload the tuning file.
|
||||||
|
|
@ -826,28 +775,6 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
self.set_background(portal)
|
self.set_background(portal)
|
||||||
|
|
||||||
@lt.thing_action
|
|
||||||
def flat_lens_shading(self) -> None:
|
|
||||||
"""Disable flat-field correction.
|
|
||||||
|
|
||||||
This method will set a completely flat lens shading table. It is not the
|
|
||||||
same as the default behaviour, which is to use an adaptive lens shading
|
|
||||||
table.
|
|
||||||
|
|
||||||
This flat table is used to take an image with no lens shading so that the
|
|
||||||
correct lens shading table can be calibrated.
|
|
||||||
"""
|
|
||||||
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_lst(
|
|
||||||
self.tuning,
|
|
||||||
luminance=flat_array,
|
|
||||||
cr=flat_array,
|
|
||||||
cb=flat_array,
|
|
||||||
)
|
|
||||||
self._initialise_picamera()
|
|
||||||
|
|
||||||
@lt.thing_property
|
@lt.thing_property
|
||||||
def primary_calibration_actions(self) -> list[ActionButton]:
|
def primary_calibration_actions(self) -> list[ActionButton]:
|
||||||
"""The calibration actions for both calibration wizard and settings panel."""
|
"""The calibration actions for both calibration wizard and settings panel."""
|
||||||
|
|
@ -894,6 +821,12 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
can_terminate=False,
|
can_terminate=False,
|
||||||
button_primary=False,
|
button_primary=False,
|
||||||
),
|
),
|
||||||
|
action_button_for(
|
||||||
|
self.flat_lens_shading_chrominance,
|
||||||
|
submit_label="Disable Flat Field Chrominance",
|
||||||
|
can_terminate=False,
|
||||||
|
button_primary=False,
|
||||||
|
),
|
||||||
action_button_for(
|
action_button_for(
|
||||||
self.reset_lens_shading,
|
self.reset_lens_shading,
|
||||||
submit_label="Reset Flat Field Correction",
|
submit_label="Reset Flat Field Correction",
|
||||||
|
|
@ -930,39 +863,21 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
]
|
]
|
||||||
|
|
||||||
@lt.thing_property
|
@lt.thing_property
|
||||||
def lens_shading_tables(self) -> Optional[LensShading]:
|
def lens_shading_tables(self) -> Optional[tf_utils.LensShading]:
|
||||||
"""The current lens shading (i.e. flat-field correction).
|
"""The current lens shading (i.e. flat-field correction).
|
||||||
|
|
||||||
Return the current lens shading correction, as three 2D lists each with
|
Return the current lens shading correction, as three 2D lists each with
|
||||||
dimensions 16x12, if a static lens shading table is in use.
|
dimensions 16x12.
|
||||||
|
|
||||||
Return None if:
|
The colour temperature is returned. If the colour temperature us 5000 then this
|
||||||
- adaptive control is enabled
|
means the lens shading tables have been calibrated (with our illumination which
|
||||||
- multiple LSTs in use (for different colour temperatures),
|
has a 5000k colour temperature). Other numbers are set when flatening or
|
||||||
|
resetting the table.
|
||||||
"""
|
"""
|
||||||
# Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction"
|
return tf_utils.get_lst(self.tuning)
|
||||||
alsc = self.get_tuning_algo("rpi.alsc")
|
|
||||||
|
|
||||||
# Check there is exactly 1 correction table for red-difference chroma (Cr)
|
|
||||||
# and blue-difference chroma (Cb)
|
|
||||||
if len(alsc["calibrations_Cr"]) != 1 or len(alsc["calibrations_Cb"]) != 1:
|
|
||||||
# If there is not exactly one table, then lens shading isn't static.
|
|
||||||
return None
|
|
||||||
|
|
||||||
def reshape_lst(lin: list[float]) -> list[list[float]]:
|
|
||||||
"""Reshape the 192 element list into a 2D 16x12 list."""
|
|
||||||
w, h = 16, 12
|
|
||||||
return [lin[w * i : w * (i + 1)] for i in range(h)]
|
|
||||||
|
|
||||||
return LensShading(
|
|
||||||
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
|
@lens_shading_tables.setter
|
||||||
def lens_shading_tables(self, lst: LensShading) -> None:
|
def lens_shading_tables(self, lst: tf_utils.LensShading) -> None:
|
||||||
"""Set the lens shading tables."""
|
"""Set the lens shading tables."""
|
||||||
with self._streaming_picamera(pause_stream=True):
|
with self._streaming_picamera(pause_stream=True):
|
||||||
self.tuning = tf_utils.set_lst(
|
self.tuning = tf_utils.set_lst(
|
||||||
|
|
@ -974,6 +889,21 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
)
|
)
|
||||||
self._initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
|
@lt.thing_action
|
||||||
|
def flat_lens_shading(self) -> None:
|
||||||
|
"""Disable flat-field correction.
|
||||||
|
|
||||||
|
This method will set a completely flat lens shading table. It is not the
|
||||||
|
same as the default behaviour, which is to use an adaptive lens shading
|
||||||
|
table.
|
||||||
|
|
||||||
|
This flat table is used to take an image with no lens shading so that the
|
||||||
|
correct lens shading table can be calibrated.
|
||||||
|
"""
|
||||||
|
with self._streaming_picamera(pause_stream=True):
|
||||||
|
self.tuning = tf_utils.flatten_lst(self.tuning)
|
||||||
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def flat_lens_shading_chrominance(self) -> None:
|
def flat_lens_shading_chrominance(self) -> None:
|
||||||
"""Disable flat-field correction for colour only.
|
"""Disable flat-field correction for colour only.
|
||||||
|
|
@ -983,16 +913,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
colour across the image.
|
colour across the image.
|
||||||
"""
|
"""
|
||||||
with self._streaming_picamera(pause_stream=True):
|
with self._streaming_picamera(pause_stream=True):
|
||||||
alsc = self.get_tuning_algo("rpi.alsc")
|
self.tuning = tf_utils.flatten_lst(self.tuning, keep_luminance=True)
|
||||||
luminance = alsc["luminance_lut"]
|
|
||||||
flat = np.ones((12, 16))
|
|
||||||
self.tuning = tf_utils.set_lst(
|
|
||||||
self.tuning,
|
|
||||||
luminance=luminance,
|
|
||||||
cr=flat,
|
|
||||||
cb=flat,
|
|
||||||
colour_temp=1234,
|
|
||||||
)
|
|
||||||
self._initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,39 @@
|
||||||
The functions that edit the tuning files return a new dictionary that is updated.
|
The functions that edit the tuning files return a new dictionary that is updated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# The colour temperature to use when setting a value
|
||||||
|
CALIBRATED_COLOUR_TEMP = 5000
|
||||||
|
|
||||||
|
# The colour temperature to use for default uncalibrated values
|
||||||
|
DEFAULT_COLOUR_TEMP = 1234
|
||||||
|
|
||||||
|
|
||||||
|
class LensShading(BaseModel):
|
||||||
|
"""A Pydantic model holding the lens shading tables.
|
||||||
|
|
||||||
|
PiCamera needs three numpy arrays for lens shading correction. Each array is
|
||||||
|
(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 sent by FastAPI
|
||||||
|
"""
|
||||||
|
|
||||||
|
luminance: list[list[float]]
|
||||||
|
Cr: list[list[float]]
|
||||||
|
Cb: list[list[float]]
|
||||||
|
colour_temp: int
|
||||||
|
|
||||||
|
|
||||||
class TuningFileError(RuntimeError):
|
class TuningFileError(RuntimeError):
|
||||||
"""Raised if the tuning file cannot be loaded for any reason."""
|
"""Raised if the tuning file cannot be loaded for any reason."""
|
||||||
|
|
@ -62,41 +85,92 @@ def find_tuning_algo(tuning: dict[str, dict], name: str) -> dict[str, Any]:
|
||||||
def set_lst(
|
def set_lst(
|
||||||
tuning: dict,
|
tuning: dict,
|
||||||
*,
|
*,
|
||||||
luminance: np.ndarray,
|
luminance: Optional[np.ndarray],
|
||||||
cr: np.ndarray,
|
cr: Optional[np.ndarray],
|
||||||
cb: np.ndarray,
|
cb: Optional[np.ndarray],
|
||||||
colour_temp: int = 5000,
|
colour_temp: int,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Update the ``rpi.alsc`` section of with new lens shading tables.
|
"""Update the ``rpi.alsc`` section of with new lens shading tables.
|
||||||
|
|
||||||
Only one set of tables is set so no adaptive lens shading will be used.
|
Only one set of tables is set so no adaptive lens shading will be used.
|
||||||
|
|
||||||
:param tuning: The current tuning file.
|
:param tuning: The current tuning file.
|
||||||
:param luminance: The table of luminance values, as (12, 16) numpy array
|
:param luminance: The table of luminance values, as (12, 16) numpy array. Or None
|
||||||
:param cr: The table of cr values, as (12, 16) numpy array
|
to leave unchanged.
|
||||||
:param cb: The table of cb values, as (12, 16) numpy array
|
:param cr: The table of cr values, as (12, 16) numpy array. Or None to leave
|
||||||
:param colour_temp: The colour temperature to set. By default this is 5000. Set a
|
unchanged.
|
||||||
different value for the PiCamera Thing to report that the lens shading is not
|
:param cb: The table of cb values, as (12, 16) numpy array. Or None to leave
|
||||||
calibrated.
|
unchanged.
|
||||||
|
:param colour_temp: The colour temperature to set. On calibration this should be
|
||||||
|
set to 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.
|
:return: an updated tuning dict with the new lens shading tables.
|
||||||
"""
|
"""
|
||||||
output_tuning = deepcopy(tuning)
|
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 = 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["luminance_strength"] = 1.0
|
||||||
alsc["calibrations_Cr"] = [
|
|
||||||
{"ct": colour_temp, "table": _as_flat_rounded_list(cr, round_to=3)}
|
def check_shape(table: np.ndarray) -> None:
|
||||||
]
|
"""Throw error if the lens shading table is the wrong shape."""
|
||||||
alsc["calibrations_Cb"] = [
|
if np.array(table).shape != (12, 16):
|
||||||
{"ct": colour_temp, "table": _as_flat_rounded_list(cb, round_to=3)}
|
raise ValueError("Lens shading tables must be 12x16!")
|
||||||
]
|
|
||||||
alsc["luminance_lut"] = _as_flat_rounded_list(luminance, round_to=3)
|
if cr is not None:
|
||||||
|
check_shape(cr)
|
||||||
|
alsc["calibrations_Cr"] = [
|
||||||
|
{"ct": colour_temp, "table": _as_flat_rounded_list(cr, round_to=3)}
|
||||||
|
]
|
||||||
|
|
||||||
|
if cr is not None:
|
||||||
|
check_shape(cb)
|
||||||
|
alsc["calibrations_Cb"] = [
|
||||||
|
{"ct": colour_temp, "table": _as_flat_rounded_list(cb, round_to=3)}
|
||||||
|
]
|
||||||
|
|
||||||
|
if luminance is not None:
|
||||||
|
check_shape(luminance)
|
||||||
|
alsc["luminance_lut"] = _as_flat_rounded_list(luminance, round_to=3)
|
||||||
|
|
||||||
return output_tuning
|
return output_tuning
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_lst(tuning: dict, keep_luminance: bool = False) -> dict:
|
||||||
|
"""Flaten the len shading table ro an array of ones.
|
||||||
|
|
||||||
|
:param tuning: The current tuning dictionary.
|
||||||
|
:param keep_luminance: Set to True to only flatten the cr and cb tables.
|
||||||
|
:return: An updated tuning dict.
|
||||||
|
"""
|
||||||
|
flat = np.ones((12, 16))
|
||||||
|
return set_lst(
|
||||||
|
tuning,
|
||||||
|
luminance=None if keep_luminance else flat,
|
||||||
|
cr=flat,
|
||||||
|
cb=flat,
|
||||||
|
colour_temp=DEFAULT_COLOUR_TEMP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lst(tuning: dict) -> LensShading:
|
||||||
|
"""Return the lens shading as a LenSading Base Model."""
|
||||||
|
# Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction"
|
||||||
|
alsc = find_tuning_algo(tuning, "rpi.alsc")
|
||||||
|
|
||||||
|
def reshape_lst(lin: list[float]) -> list[list[float]]:
|
||||||
|
"""Reshape the 192 element list into a 2D 16x12 list."""
|
||||||
|
w, h = 16, 12
|
||||||
|
return [lin[w * i : w * (i + 1)] for i in range(h)]
|
||||||
|
|
||||||
|
return LensShading(
|
||||||
|
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"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def lst_calibrated(tuning: dict) -> bool:
|
def lst_calibrated(tuning: dict) -> bool:
|
||||||
"""Whether the lens shading table is calibrated.
|
"""Whether the lens shading table is calibrated.
|
||||||
|
|
||||||
|
|
@ -104,7 +178,7 @@ def lst_calibrated(tuning: dict) -> bool:
|
||||||
this is what we set on calibration. Our tuning file sets a temperature of 1234.
|
this is what we set on calibration. Our tuning file sets a temperature of 1234.
|
||||||
"""
|
"""
|
||||||
alsc = find_tuning_algo(tuning, "rpi.alsc")
|
alsc = find_tuning_algo(tuning, "rpi.alsc")
|
||||||
return alsc["calibrations_Cr"][0]["ct"] == 5000
|
return alsc["calibrations_Cr"][0]["ct"] == CALIBRATED_COLOUR_TEMP
|
||||||
|
|
||||||
|
|
||||||
def set_ccm(
|
def set_ccm(
|
||||||
|
|
@ -121,7 +195,7 @@ def set_ccm(
|
||||||
"""
|
"""
|
||||||
output_tuning = deepcopy(tuning)
|
output_tuning = deepcopy(tuning)
|
||||||
ccm = find_tuning_algo(output_tuning, "rpi.ccm")
|
ccm = find_tuning_algo(output_tuning, "rpi.ccm")
|
||||||
ccm["ccms"] = [{"ct": 5000, "ccm": col_corr_matrix}]
|
ccm["ccms"] = [{"ct": CALIBRATED_COLOUR_TEMP, "ccm": col_corr_matrix}]
|
||||||
return output_tuning
|
return output_tuning
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue