2929picamera.lens_shading_table = lst
3030```
3131"""
32+
3233from __future__ import annotations
34+ import gc
3335import logging
3436import time
3537from typing import List , Literal , Optional , Tuple
3840from scipy .ndimage import zoom
3941
4042from picamera2 import Picamera2
43+ import picamera2
4144
4245
4346def load_default_tuning (cam : Picamera2 ) -> dict :
@@ -245,31 +248,34 @@ def adjust_white_balance_from_raw(
245248 camera .configure (config )
246249 camera .start ()
247250 channels = channels_from_bayer_array (camera .capture_array ("raw" ))
248- #logging.info(f"White balance: channels were retrieved with shape {channels.shape}.")
251+ # logging.info(f"White balance: channels were retrieved with shape {channels.shape}.")
249252 if luminance is not None and Cr is not None and Cb is not None :
250253 # Reconstruct a low-resolution image from the lens shading tables
251254 # and use it to normalise the raw image, to compensate for
252255 # the brightest pixels in each channel not coinciding.
253- grids = grids_from_lst (np .array (luminance )** luminance_power , Cr , Cb )
254- channel_gains = 1 / grids
256+ grids = grids_from_lst (np .array (luminance ) ** luminance_power , Cr , Cb )
257+ channel_gains = 1 / grids
255258 if channel_gains .shape [1 :] != channels .shape [1 :]:
256259 channel_gains = upsample_channels (channel_gains , channels .shape [1 :])
257260 logging .info (f"Before gains, channel maxima are { np .max (channels , axis = (1 ,2 ))} " )
258261 channels = channels * channel_gains
259262 logging .info (f"After gains, channel maxima are { np .max (channels , axis = (1 ,2 ))} " )
260263 if method == "centre" :
261264 _ , h , w = channels .shape
262- blue , g1 , g2 , red = np .mean (
263- channels [:, 9 * h // 20 :11 * h // 20 , 9 * w // 20 :11 * w // 20 ],
264- axis = (1 ,2 ),
265- ) - 64
265+ blue , g1 , g2 , red = (
266+ np .mean (
267+ channels [:, 9 * h // 20 : 11 * h // 20 , 9 * w // 20 : 11 * w // 20 ],
268+ axis = (1 , 2 ),
269+ )
270+ - 64
271+ )
266272 else :
267273 # TODO: read black level from camera rather than hard-coding 64
268274 blue , g1 , g2 , red = np .percentile (channels , percentile , axis = (1 , 2 )) - 64
269275 green = (g1 + g2 ) / 2.0
270276 new_awb_gains = (green / red , green / blue )
271277 if Cr is not None and Cb is not None :
272- # The LST algorithm normalises Cr and Cb by their minimum.
278+ # The LST algorithm normalises Cr and Cb by their minimum.
273279 # The lens shading correction only ever boosts the red and blue values.
274280 # Here, we decrease the gains by the minimum value of Cr and Cb.
275281 new_awb_gains = (green / red * np .min (Cr ), green / blue * np .min (Cb ))
@@ -320,40 +326,47 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int):
320326 """
321327 for i in range (11 ):
322328 for j in range (15 ):
323- grid .append (np .mean (chan [dy * i : dy * ( 1 + i ), dx * j : dx * ( 1 + j )]))
324- grid .append (np .mean (chan [dy * i : dy * ( 1 + i ), 15 * dx :]))
329+ grid .append (np .mean (chan [dy * i : dy * ( 1 + i ), dx * j : dx * ( 1 + j )]))
330+ grid .append (np .mean (chan [dy * i : dy * ( 1 + i ), 15 * dx :]))
325331 for j in range (15 ):
326- grid .append (np .mean (chan [11 * dy :, dx * j : dx * ( 1 + j )]))
327- grid .append (np .mean (chan [11 * dy :, 15 * dx :]))
332+ grid .append (np .mean (chan [11 * dy :, dx * j : dx * ( 1 + j )]))
333+ grid .append (np .mean (chan [11 * dy :, 15 * dx :]))
328334 """
329335 return as np.array, ready for further manipulation
330336 """
331337 return np .reshape (np .array (grid ), (12 , 16 ))
332338
339+
333340def upsample_channels (grids : np .ndarray , shape : tuple [int ]):
334341 """Zoom an image in the last two dimensions
335342
336343 This is effectively the inverse operation of `get_16x12_grid`
337344 """
338- zoom_factors = [1 ,] + list (np .ceil (np .array (shape )/ np .array (grids .shape [1 :])))
339- return zoom (grids , zoom_factors , order = 1 )[:, :shape [0 ], :shape [1 ]]
345+ zoom_factors = [
346+ 1 ,
347+ ] + list (np .ceil (np .array (shape ) / np .array (grids .shape [1 :])))
348+ return zoom (grids , zoom_factors , order = 1 )[:, : shape [0 ], : shape [1 ]]
349+
340350
341351def downsampled_channels (channels : np .ndarray , blacklevel = 64 ) -> list [np .ndarray ]:
342352 """Generate a downsampled, un-normalised image from which to calculate the LST
343353
344354 TODO: blacklevel probably ought to be determined from the camera...
345355 """
346356 channel_shape = np .array (channels .shape [1 :])
347- lst_shape = np .array ([12 ,16 ])
348- step = np .ceil (channel_shape / lst_shape ).astype (int )
357+ lst_shape = np .array ([12 , 16 ])
358+ step = np .ceil (channel_shape / lst_shape ).astype (int )
349359 return np .stack (
350360 [
351- get_16x12_grid (channels [i , ...].astype (float ) - blacklevel , step [1 ], step [0 ])
361+ get_16x12_grid (
362+ channels [i , ...].astype (float ) - blacklevel , step [1 ], step [0 ]
363+ )
352364 for i in range (channels .shape [0 ])
353365 ],
354366 axis = 0 ,
355367 )
356368
369+
357370def lst_from_channels (channels : np .ndarray ) -> LensShadingTables :
358371 """Given the 4 Bayer colour channels from a white image, generate a LST.
359372
@@ -373,32 +386,34 @@ def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
373386 # TODO: make consistent with
374387 https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py
375388 """
376- r : np .ndarray = grids [3 , ...]
389+ r : np .ndarray = grids [3 , ...]
377390 g : np .ndarray = np .mean (grids [1 :3 , ...], axis = 0 )
378391 b : np .ndarray = grids [0 , ...]
379392
380393 # What we actually want to calculate is the gains needed to compensate for the
381394 # lens shading - that's 1/lens_shading_table_float as we currently have it.
382395 luminance_gains : np .ndarray = np .max (g ) / g # Minimum luminance gain is 1
383396 cr_gains : np .ndarray = g / r
384- #cr_gains /= cr_gains[5, 7] # Normalise so the central colour doesn't change
397+ # cr_gains /= cr_gains[5, 7] # Normalise so the central colour doesn't change
385398 cb_gains : np .ndarray = g / b
386- #cb_gains /= cb_gains[5, 7]
399+ # cb_gains /= cb_gains[5, 7]
387400 return luminance_gains , cr_gains , cb_gains
388401
402+
389403def grids_from_lst (lum : np .ndarray , Cr : np .ndarray , Cb : np .ndarray ) -> np .ndarray :
390404 """Convert form luminance/chrominance dict to four RGGB channels
391-
405+
392406 Note that these will be normalised - the maximum green value is always 1.
393407 Also, note that the channels are BGGR, to be consistent with the
394408 `channels_from_raw_image` function. This should probably change in the
395409 future.
396410 """
397- G = 1 / np .array (lum )
398- R = G / np .array (Cr )
399- B = G / np .array (Cb )
411+ G = 1 / np .array (lum )
412+ R = G / np .array (Cr )
413+ B = G / np .array (Cb )
400414 return np .stack ([B , G , G , R ], axis = 0 )
401415
416+
402417def set_static_lst (
403418 tuning : dict ,
404419 luminance : np .ndarray ,
@@ -423,30 +438,23 @@ def set_static_lst(
423438 ]
424439 alsc ["luminance_lut" ] = np .reshape (luminance , (- 1 )).round (3 ).tolist ()
425440
426- def set_static_ccm (
427- tuning : dict ,
428- c : list
429- ) -> None :
441+
442+ def set_static_ccm (tuning : dict , c : list ) -> None :
430443 """Update the `rpi.alsc` section of a camera tuning dict to use a static correcton.
431444
432445 `tuning` will be updated in-place to set its shading to static, and disable any
433446 adaptive tweaking by the algorithm.
434447 """
435448 ccm = Picamera2 .find_tuning_algo (tuning , "rpi.ccm" )
436- ccm ["ccms" ] = [{
437- "ct" : 2860 ,
438- "ccm" : c
439- }
440- ]
449+ ccm ["ccms" ] = [{"ct" : 2860 , "ccm" : c }]
441450
442- def get_static_ccm (
443- tuning : dict
444- ) -> None :
445- """Get the `rpi.ccm` section of a camera tuning dict
446- """
451+
452+ def get_static_ccm (tuning : dict ) -> None :
453+ """Get the `rpi.ccm` section of a camera tuning dict"""
447454 ccm = Picamera2 .find_tuning_algo (tuning , "rpi.ccm" )
448455 return ccm ["ccms" ]
449456
457+
450458def lst_is_static (tuning : dict ) -> bool :
451459 """Whether the lens shading table is set to static"""
452460 alsc = Picamera2 .find_tuning_algo (tuning , "rpi.alsc" )
@@ -472,7 +480,7 @@ def set_static_geq(
472480def _geq_is_static (tuning : dict ) -> bool :
473481 """Whether the green equalisation is set to static"""
474482 geq = Picamera2 .find_tuning_algo (tuning , "rpi.geq" )
475- return alsc ["offset" ] == 65535
483+ return geq ["offset" ] == 65535
476484
477485
478486def index_of_algorithm (algorithms : list [dict ], algorithm : str ):
@@ -499,6 +507,7 @@ def lst_from_camera(camera: Picamera2) -> LensShadingTables:
499507 channels = raw_channels_from_camera (camera )
500508 return lst_from_channels (channels )
501509
510+
502511def raw_channels_from_camera (camera : Picamera2 ) -> LensShadingTables :
503512 """Acquire a raw image and return a 4xNxM array of the colour channels."""
504513 if camera .started :
@@ -521,6 +530,16 @@ def raw_channels_from_camera(camera: Picamera2) -> LensShadingTables:
521530 return channels_from_bayer_array (raw_image )
522531
523532
533+ def recreate_camera_manager ():
534+ """Delete and recreate the camera manager.
535+
536+ This is necessary to ensure the tuning file is re-read.
537+ """
538+ del Picamera2 ._cm
539+ gc .collect ()
540+ Picamera2 ._cm = picamera2 .picamera2 .CameraManager ()
541+
542+
524543if __name__ == "__main__" :
525544 """This block is untested but has been updated."""
526545 with Picamera2 () as cam :
0 commit comments