Docstrings, typehints, and clarifications in PiCamera and its utils
This commit is contained in:
parent
83e0e08ccc
commit
ee8c58c99f
2 changed files with 119 additions and 23 deletions
|
|
@ -1,3 +1,20 @@
|
||||||
|
"""
|
||||||
|
This SubModule interacts with a Raspberry Pi camera using the Picamera2 library.
|
||||||
|
|
||||||
|
The Picamera2 library uses LibCamera as the underlying camera stack. This gives us
|
||||||
|
some control of the GPU pipeline for the image.
|
||||||
|
|
||||||
|
The API documentation for PiCamera2 is unfortunatly not in a standard auto-generated
|
||||||
|
website. For documentation of the PiCamera2 API there is a PDF called
|
||||||
|
"The Picamera2 Library" available at:
|
||||||
|
https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf
|
||||||
|
|
||||||
|
For information on the algorithms used to tune/calibrate the Raspberry Pi Camera see
|
||||||
|
the guide called "Raspberry Pi Camera Algorithm and Tuning Guide"
|
||||||
|
Available at:
|
||||||
|
https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
|
@ -71,6 +88,11 @@ class PicameraStreamOutput(Output):
|
||||||
|
|
||||||
|
|
||||||
class SensorMode(BaseModel):
|
class SensorMode(BaseModel):
|
||||||
|
"""
|
||||||
|
A Pydantic model holding all the information about a specific
|
||||||
|
sensor mode as reported by the PiCamera.
|
||||||
|
"""
|
||||||
|
|
||||||
unpacked: str
|
unpacked: str
|
||||||
bit_depth: int
|
bit_depth: int
|
||||||
size: tuple[int, int]
|
size: tuple[int, int]
|
||||||
|
|
@ -81,11 +103,28 @@ class SensorMode(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class SensorModeSelector(BaseModel):
|
class SensorModeSelector(BaseModel):
|
||||||
|
"""
|
||||||
|
A Pydantic model holding the two values needed to select a PiCamera
|
||||||
|
Sensor mode. The output size and the bit depth.
|
||||||
|
|
||||||
|
This is a Pydantic modell so that it can be saved to the disk.
|
||||||
|
"""
|
||||||
|
|
||||||
output_size: tuple[int, int]
|
output_size: tuple[int, int]
|
||||||
bit_depth: int
|
bit_depth: int
|
||||||
|
|
||||||
|
|
||||||
class LensShading(BaseModel):
|
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 modell so that it can be saved to the disk.
|
||||||
|
"""
|
||||||
|
|
||||||
luminance: list[list[float]]
|
luminance: list[list[float]]
|
||||||
Cr: list[list[float]]
|
Cr: list[list[float]]
|
||||||
Cb: list[list[float]]
|
Cb: list[list[float]]
|
||||||
|
|
@ -203,6 +242,14 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
|
|
||||||
@exposure_time.setter
|
@exposure_time.setter
|
||||||
def exposure_time(self, value: int):
|
def exposure_time(self, value: int):
|
||||||
|
"""
|
||||||
|
Custom setter for the above exposure_time PicameraControl.
|
||||||
|
|
||||||
|
This is overriding the standard setter for a PicameraControl, as
|
||||||
|
the value needs to be adjusted before setting to behave as expected.
|
||||||
|
|
||||||
|
See comment within the function for more detail.
|
||||||
|
"""
|
||||||
with self.picamera() as cam:
|
with self.picamera() as cam:
|
||||||
# Note: This set a value 1 higher than requested as picamera2 always sets
|
# Note: This set a value 1 higher than requested as picamera2 always sets
|
||||||
# a lower value than requested, even if the requested is allowed
|
# a lower value than requested, even if the requested is allowed
|
||||||
|
|
@ -375,8 +422,18 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
main_resolution: the resolution for the main configuration. Defaults to
|
main_resolution: the resolution for the main configuration. Defaults to
|
||||||
(820, 616), 1/4 sensor size.
|
(820, 616), 1/4 sensor size.
|
||||||
buffer_count: the number of frames to hold in the buffer. Higher uses more memory,
|
buffer_count: the number of frames to hold in the buffer. Higher uses more memory,
|
||||||
lower may cause dropped frames. Defaults to 6.
|
lower may cause dropped frames. Value must be between 1 and 8, Defaults to 6.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Buffer count can't be negative, zero, or too high.
|
||||||
|
if buffer_count < 1 or buffer_count < 8:
|
||||||
|
# 8 is slightly arbitrary. 6 is the PiCamera default for video
|
||||||
|
# and the documentation only says that setting values higher gives
|
||||||
|
# diminishing returns, and that the true maximum is hardware dependent
|
||||||
|
raise ValueError(
|
||||||
|
f"Can't set a buffer count of {buffer_count}. "
|
||||||
|
"Buffer count must be an integer from 1-8"
|
||||||
|
)
|
||||||
with self.picamera() as picam:
|
with self.picamera() as picam:
|
||||||
try:
|
try:
|
||||||
if picam.started:
|
if picam.started:
|
||||||
|
|
@ -388,7 +445,6 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
sensor=self.thing_settings.get("sensor_mode", None),
|
sensor=self.thing_settings.get("sensor_mode", None),
|
||||||
controls=self.persistent_controls,
|
controls=self.persistent_controls,
|
||||||
)
|
)
|
||||||
# Set buffer count - can't be negative
|
|
||||||
stream_config["buffer_count"] = buffer_count
|
stream_config["buffer_count"] = buffer_count
|
||||||
picam.configure(stream_config)
|
picam.configure(stream_config)
|
||||||
logging.info("Starting picamera MJPEG stream...")
|
logging.info("Starting picamera MJPEG stream...")
|
||||||
|
|
@ -626,8 +682,10 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
the processed images. It should not affect raw images.
|
the processed images. It should not affect raw images.
|
||||||
"""
|
"""
|
||||||
with self.picamera(pause_stream=True) as cam:
|
with self.picamera(pause_stream=True) as cam:
|
||||||
# Suppress lint warning that L, Cr, and Cb are not lowercase, as this is the
|
# Suppress lint warning that L, Cr, and Cb are not lowercase, as these are
|
||||||
# Standard format for these mathematical vars.
|
# the standard mathematical terms for:
|
||||||
|
# luminance (L), red-difference chroma (Cr), and blue-difference chroma
|
||||||
|
# (Cb).
|
||||||
L, Cr, Cb = recalibrate_utils.lst_from_camera(cam) # noqa: N806
|
L, Cr, Cb = recalibrate_utils.lst_from_camera(cam) # noqa: N806
|
||||||
recalibrate_utils.set_static_lst(self.tuning, L, Cr, Cb)
|
recalibrate_utils.set_static_lst(self.tuning, L, Cr, Cb)
|
||||||
self.initialise_picamera()
|
self.initialise_picamera()
|
||||||
|
|
@ -649,8 +707,14 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def reset_ccm(self):
|
def reset_ccm(self):
|
||||||
"""Overwrite the colour correction matrix in camera tuning with default values from the documentation"""
|
"""
|
||||||
c = [
|
Overwrite the colour correction matrix in camera tuning with default values.
|
||||||
|
|
||||||
|
These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page
|
||||||
|
45.
|
||||||
|
"""
|
||||||
|
# This is flattened 3x3 matrix. See `calibrate_colour_correction`
|
||||||
|
col_corr_matrix = [
|
||||||
1.80439,
|
1.80439,
|
||||||
-0.73699,
|
-0.73699,
|
||||||
-0.06739,
|
-0.06739,
|
||||||
|
|
@ -661,13 +725,25 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
-0.56403,
|
-0.56403,
|
||||||
1.64781,
|
1.64781,
|
||||||
]
|
]
|
||||||
self.colour_correction_matrix = c
|
self.colour_correction_matrix = col_corr_matrix
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def calibrate_colour_correction(self, c: tuple) -> None:
|
def calibrate_colour_correction(
|
||||||
"""Overwrite the colour correction matrix in camera tuning"""
|
self,
|
||||||
|
col_corr_matrix: tuple[
|
||||||
|
float, float, float, float, float, float, float, float, float
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Overwrite the colour correction matrix in camera tuning
|
||||||
|
|
||||||
|
col_corr_matrix: This 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.
|
||||||
|
|
||||||
|
See page Raspberry Pi Camera Algorithm and Tuning Guide, page 45.
|
||||||
|
"""
|
||||||
with self.picamera(pause_stream=True):
|
with self.picamera(pause_stream=True):
|
||||||
recalibrate_utils.set_static_ccm(self.tuning, c)
|
recalibrate_utils.set_static_ccm(self.tuning, col_corr_matrix)
|
||||||
self.initialise_picamera()
|
self.initialise_picamera()
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
|
|
@ -710,29 +786,43 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
This method will set a completely flat lens shading table. It is not the
|
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
|
same as the default behaviour, which is to use an adaptive lens shading
|
||||||
table.
|
table.
|
||||||
|
|
||||||
|
This flat table is used to take an image wth no lens shading so that the
|
||||||
|
correct lens shading table can be calibrated.
|
||||||
"""
|
"""
|
||||||
with self.picamera(pause_stream=True):
|
with self.picamera(pause_stream=True):
|
||||||
f = np.ones((12, 16))
|
# Generate and array of ones of the correct size for each channel
|
||||||
recalibrate_utils.set_static_lst(self.tuning, f, f, f)
|
flat_array = np.ones((12, 16))
|
||||||
|
recalibrate_utils.set_static_lst(
|
||||||
|
self.tuning, flat_array, flat_array, flat_array
|
||||||
|
)
|
||||||
self.initialise_picamera()
|
self.initialise_picamera()
|
||||||
|
|
||||||
@thing_property
|
@thing_property
|
||||||
def lens_shading_tables(self) -> Optional[LensShading]:
|
def lens_shading_tables(self) -> Optional[LensShading]:
|
||||||
"""The current lens shading (i.e. flat-field correction)
|
"""The current lens shading (i.e. flat-field correction)
|
||||||
|
|
||||||
This returns the current lens shading correction, as three 2D lists
|
Return the current lens shading correction, as three 2D lists each with
|
||||||
each with dimensions 16x12. This assumes that we are using a static
|
dimensions 16x12, if a static lens shading table is in use.
|
||||||
lens shading table - if adaptive control is enabled, or if there
|
|
||||||
are multiple LSTs in use for different colour temperatures,
|
Return None if:
|
||||||
we return a null value to avoid confusion.
|
- adaptive control is enabled
|
||||||
|
- multiple LSTs in use (for different colour temperatures),
|
||||||
"""
|
"""
|
||||||
if not self.lens_shading_is_static:
|
if not self.lens_shading_is_static:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction"
|
||||||
alsc = Picamera2.find_tuning_algo(self.tuning, "rpi.alsc")
|
alsc = Picamera2.find_tuning_algo(self.tuning, "rpi.alsc")
|
||||||
if any(len(alsc[f"calibrations_C{c}"]) != 1 for c in ("r", "b")):
|
|
||||||
|
# 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
|
return None
|
||||||
|
|
||||||
def reshape_lst(lin: list[float]) -> list[list[float]]:
|
def reshape_lst(lin: list[float]) -> list[list[float]]:
|
||||||
|
"""Reshape the 192 element list into a 2D 16x12 list"""
|
||||||
w, h = 16, 12
|
w, h = 16, 12
|
||||||
return [lin[w * i : w * (i + 1)] for i in range(h)]
|
return [lin[w * i : w * (i + 1)] for i in range(h)]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -326,10 +326,11 @@ def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
|
|
||||||
def get_16x12_grid(chan: np.ndarray, dx: int, dy: int):
|
def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray:
|
||||||
"""Compresses channel down to a 16x12 grid - from libcamera
|
"""Compresses channel down to a 16x12 grid - from libcamera
|
||||||
|
|
||||||
This is taken from https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py
|
This is taken from
|
||||||
|
https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py
|
||||||
for consistency.
|
for consistency.
|
||||||
"""
|
"""
|
||||||
grid = []
|
grid = []
|
||||||
|
|
@ -347,7 +348,7 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int):
|
||||||
return np.reshape(np.array(grid), (12, 16))
|
return np.reshape(np.array(grid), (12, 16))
|
||||||
|
|
||||||
|
|
||||||
def upsample_channels(grids: np.ndarray, shape: tuple[int]):
|
def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray:
|
||||||
"""Zoom an image in the last two dimensions
|
"""Zoom an image in the last two dimensions
|
||||||
|
|
||||||
This is effectively the inverse operation of `get_16x12_grid`
|
This is effectively the inverse operation of `get_16x12_grid`
|
||||||
|
|
@ -452,14 +453,19 @@ def set_static_lst(
|
||||||
alsc["luminance_lut"] = as_flat_rounded_list(luminance, round_to=3)
|
alsc["luminance_lut"] = as_flat_rounded_list(luminance, round_to=3)
|
||||||
|
|
||||||
|
|
||||||
def set_static_ccm(tuning: dict, c: list) -> None:
|
def set_static_ccm(
|
||||||
|
tuning: dict,
|
||||||
|
col_corr_matrix: tuple[
|
||||||
|
float, float, float, float, float, float, float, float, float
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
"""Update the `rpi.alsc` section of a camera tuning dict to use a static correcton.
|
"""Update the `rpi.alsc` section of a camera tuning dict to use a static correcton.
|
||||||
|
|
||||||
`tuning` will be updated in-place to set its shading to static, and disable any
|
`tuning` will be updated in-place to set its shading to static, and disable any
|
||||||
adaptive tweaking by the algorithm.
|
adaptive tweaking by the algorithm.
|
||||||
"""
|
"""
|
||||||
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
|
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
|
||||||
ccm["ccms"] = [{"ct": 2860, "ccm": c}]
|
ccm["ccms"] = [{"ct": 2860, "ccm": col_corr_matrix}]
|
||||||
|
|
||||||
|
|
||||||
def get_static_ccm(tuning: dict) -> None:
|
def get_static_ccm(tuning: dict) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue