Fix issues found by linter in PiCamera Code

Issue #423 created based on lens shading TODOs
This commit is contained in:
Julian Stirling 2025-06-23 12:33:42 +01:00
parent 0e3f4305b5
commit 72faba0272
4 changed files with 26 additions and 29 deletions

View file

@ -303,7 +303,7 @@ class AutofocusThing(Thing):
jpeg_path = os.path.join(stack_dir, f"{capture_count}.jpeg") jpeg_path = os.path.join(stack_dir, f"{capture_count}.jpeg")
start_time = time.time() start_time = time.time()
cam.capture_to_memory(logger=logger, metadata_getter=logger) cam.capture_to_memory(logger=logger, metadata_getter=metadata_getter)
captured_time = time.time() captured_time = time.time()
if capture_count + 1 < images_to_capture: if capture_count + 1 < images_to_capture:

View file

@ -41,8 +41,7 @@ class PicameraControl(PropertyDescriptor):
def _getter(self, obj: StreamingPiCamera2): def _getter(self, obj: StreamingPiCamera2):
with obj.picamera() as cam: with obj.picamera() as cam:
ret = cam.capture_metadata()[self.control_name] return cam.capture_metadata()[self.control_name]
return ret
def _setter(self, obj: StreamingPiCamera2, value: Any): def _setter(self, obj: StreamingPiCamera2, value: Any):
with obj.picamera() as cam: with obj.picamera() as cam:
@ -171,6 +170,7 @@ class StreamingPiCamera2(BaseCamera):
initial_value=(820, 616), initial_value=(820, 616),
description="Resolution to use for the MJPEG stream", description="Resolution to use for the MJPEG stream",
) )
mjpeg_bitrate = PropertyDescriptor( mjpeg_bitrate = PropertyDescriptor(
Optional[int], Optional[int],
initial_value=100000000, initial_value=100000000,
@ -368,8 +368,6 @@ class StreamingPiCamera2(BaseCamera):
lower may cause dropped frames. Defaults to 6. lower may cause dropped frames. Defaults to 6.
""" """
with self.picamera() as picam: with self.picamera() as picam:
# TODO: Filip: can we use the lores output to keep preview stream going
# while recording? According to picamera2 docs 4.2.1.6 this should work
try: try:
if picam.started: if picam.started:
picam.stop() picam.stop()

View file

@ -80,16 +80,15 @@ def set_minimum_exposure(camera: Picamera2):
We set exposure mode to manual, analog and digital gain We set exposure mode to manual, analog and digital gain
to 1, and shutter speed to the minimum (8us for Pi Camera v2) to 1, and shutter speed to the minimum (8us for Pi Camera v2)
NB ISO is left at auto, because this is needed for the gains
Note ISO is left at auto, because this is needed for the gains
to be set correctly. to be set correctly.
""" """
camera.set_controls({"AeEnable": False, "AnalogueGain": 1, "ExposureTime": 1}) # Disable Automatic exposure and gain algoritm (AeEnable), and set analogue
# camera.iso = 0 # We must set ISO=0 (auto) or we can't set gain # gain and exposure time.
# camera.analog_gain = 1
# camera.digital_gain = 1 (not configurable)
# Setting the shutter speed to 1us will result in it being set # Setting the shutter speed to 1us will result in it being set
# to the minimum possible, which is probably 8us for PiCamera v2 # to the minimum possible, which is ~8us for PiCamera v2
# camera.shutter_speed = 1 camera.set_controls({"AeEnable": False, "AnalogueGain": 1, "ExposureTime": 1})
time.sleep(0.5) time.sleep(0.5)
@ -138,8 +137,7 @@ def test_exposure_settings(camera: Picamera2, percentile: float) -> ExposureTest
def check_convergence(test: ExposureTest, target: int, tolerance: float): def check_convergence(test: ExposureTest, target: int, tolerance: float):
"""Check whether the brightness is within the specified target range""" """Check whether the brightness is within the specified target range"""
converged = abs(test.level - target) < target * tolerance return abs(test.level - target) < target * tolerance
return converged
def adjust_shutter_and_gain_from_raw( def adjust_shutter_and_gain_from_raw(
@ -257,7 +255,6 @@ def adjust_white_balance_from_raw(
camera.configure(config) camera.configure(config)
camera.start() camera.start()
channels = channels_from_bayer_array(camera.capture_array("raw")) channels = channels_from_bayer_array(camera.capture_array("raw"))
# logging.info(f"White balance: channels were retrieved with shape {channels.shape}.")
if luminance is not None and Cr is not None and Cb is not None: 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 # Reconstruct a low-resolution image from the lens shading tables
# and use it to normalise the raw image, to compensate for # and use it to normalise the raw image, to compensate for
@ -328,10 +325,9 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int):
for consistency. for consistency.
""" """
grid = [] grid = []
"""
since left and bottom border will not necessarily have rectangles of # since left and bottom border will not necessarily have rectangles of
dimension dx x dy, the 32nd iteration has to be handled separately. # dimension dx x dy, the final iteration has to be handled separately.
"""
for i in range(11): for i in range(11):
for j in range(15): for j in range(15):
grid.append(np.mean(chan[dy * i : dy * (1 + i), dx * j : dx * (1 + j)])) grid.append(np.mean(chan[dy * i : dy * (1 + i), dx * j : dx * (1 + j)]))
@ -339,9 +335,7 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int):
for j in range(15): for j in range(15):
grid.append(np.mean(chan[11 * dy :, dx * j : dx * (1 + j)])) grid.append(np.mean(chan[11 * dy :, dx * j : dx * (1 + j)]))
grid.append(np.mean(chan[11 * dy :, 15 * dx :])) grid.append(np.mean(chan[11 * dy :, 15 * dx :]))
""" # return as np.array, ready for further manipulation
return as np.array, ready for further manipulation
"""
return np.reshape(np.array(grid), (12, 16)) return np.reshape(np.array(grid), (12, 16))
@ -387,24 +381,27 @@ def lst_from_channels(channels: np.ndarray) -> LensShadingTables:
def lst_from_grids(grids: np.ndarray) -> LensShadingTables: def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
"""Given 4 downsampled grids, generate the luminance and chrominance tables """Given 4 downsampled grids, generate the luminance and chrominance tables
The grids are the 4 BAYER channels RGGB
The LST format has changed with `picamera2` and now uses a fixed resolution, The LST format has changed with `picamera2` and now uses a fixed resolution,
and is in luminance, Cr, Cb format. This function returns three ndarrays of and is in luminance, Cr, Cb format. This function returns three ndarrays of
luminance, Cr, Cb, each with shape (12, 16). luminance, Cr, Cb, each with shape (12, 16).
# TODO: make consistent with
https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py
""" """
# Calculated red, green, and blue channels from Bayer data
r: np.ndarray = grids[3, ...] r: np.ndarray = grids[3, ...]
g: np.ndarray = np.mean(grids[1:3, ...], axis=0) g: np.ndarray = np.mean(grids[1:3, ...], axis=0)
b: np.ndarray = grids[0, ...] b: np.ndarray = grids[0, ...]
# What we actually want to calculate is the gains needed to compensate for the # What we actually want to calculate is the gains needed to compensate for the
# lens shading - that's 1/lens_shading_table_float as we currently have it. # lens shading - that's 1/lens_shading_table_float as we currently have it.
luminance_gains: np.ndarray = np.max(g) / g # Minimum luminance gain is 1
# Minimum luminance gain is 1
luminance_gains: np.ndarray = np.max(g) / g
cr_gains: np.ndarray = g / r cr_gains: np.ndarray = g / r
# cr_gains /= cr_gains[5, 7] # Normalise so the central colour doesn't change
cb_gains: np.ndarray = g / b cb_gains: np.ndarray = g / b
# cb_gains /= cb_gains[5, 7]
return luminance_gains, cr_gains, cb_gains return luminance_gains, cr_gains, cb_gains
@ -491,11 +488,12 @@ def _geq_is_static(tuning: dict) -> bool:
return geq["offset"] == 65535 return geq["offset"] == 65535
def index_of_algorithm(algorithms: list[dict], algorithm: str): def index_of_algorithm(algorithms: list[dict], algorithm: str) -> int:
"""Find the index of an algorithm's section in the tuning file""" """Find the index of an algorithm's section in the tuning file"""
for i, a in enumerate(algorithms): for i, a in enumerate(algorithms):
if algorithm in a: if algorithm in a:
return i return i
raise ValueError(f"Algorithm {algorithm} is not available.")
def copy_alsc_section(from_tuning: dict, to_tuning: dict): def copy_alsc_section(from_tuning: dict, to_tuning: dict):

View file

@ -31,6 +31,7 @@ from ..stage import StageProtocol as Stage
# higher related to a faster movement # higher related to a faster movement
RATIO = 0.2 RATIO = 0.2
class SimulatedCamera(BaseCamera): class SimulatedCamera(BaseCamera):
"""A Thing representing an OpenCV camera""" """A Thing representing an OpenCV camera"""