module documentation

Functions to set up a Raspberry Pi Camera (v2 and HQ) for scientific use.

This module provides slower, simpler functions to set the gain, exposure, and white balance of a Raspberry Pi camera, using the picamera2 Python library. It's mostly used by the OpenFlexure Microscope, though it deliberately has no hard dependencies on said software, so that it's useful on its own.

There are three main calibration steps:

  • Setting exposure time and gain to get a reasonably bright image.
  • Fixing the white balance to get a neutral image
  • Taking a uniform white image and using it to calibrate the Lens Shading Table

The most reliable way to do this, avoiding any issues relating to "memory" or nonlinearities in the camera's image processing pipeline, is to use raw images. This is quite slow, but very reliable. The three steps above can be accomplished by:

picamera = picamera2.Picamera2()
sensor_info = IMX219_SENSOR_INFO

adjust_shutter_and_gain_from_raw(
    picamera,
    sensor_info,
    target_white_level=sensor_info.default_target_white_level,
)
adjust_white_balance_from_raw(picamera, sensor_info)
lst = lst_from_camera(picamera, sensor_info)
picamera.lens_shading_table = lst
Class SensorInfo Information about the sensor used for calibration and property setting.
Function adjust_shutter_and_gain_from_raw Adjust exposure and analog gain based on raw images.
Function lst_from_camera Acquire a raw image and use it to calculate a lens shading table.
Function recreate_camera_manager Delete and recreate the camera manager.
Constant IMX219_SENSOR_INFO Undocumented
Constant IMX477_SENSOR_INFO Undocumented
Constant LOGGER Undocumented
Type Alias LensShadingTables Undocumented
Class _ExposureTest Record the results of testing the camera's current exposure settings.
Function _channels_from_bayer_array Given the 'array' from a PiBayerArray, return the 4 channels.
Function _check_convergence Check whether the brightness is within the specified target range.
Function _downsampled_channels Generate a downsampled, un-normalised image from which to calculate the LST.
Function _get_16x12_grid Compresses channel down to a 16x12 grid - from libcamera.
Function _grids_from_lst Convert form luminance/chrominance dict to four RGGB channels.
Function _lst_from_channels Given the 4 Bayer colour channels from a white image, generate a LST.
Function _lst_from_grids Given 4 downsampled grids, generate the luminance and chrominance tables.
Function _raw_channels_from_camera Acquire a raw image and return a 4xNxM array of the colour channels.
Function _set_minimum_exposure Enable manual exposure, with low gain and shutter speed.
Function _test_exposure_settings Evaluate current exposure settings using a raw image.
Function _upsample_channels Zoom an image in the last two dimensions.
def adjust_shutter_and_gain_from_raw(camera: Picamera2, sensor_info: SensorInfo, target_white_level: int, max_iterations: int = 20, tolerance: float = 0.05, percentile: float = 99.9) -> float: (source)

Adjust exposure and analog gain based on raw images.

This routine is slow but effective. It uses raw images, so we are not affected by white balance or digital gain.

Parameters
camera:Picamera2A Picamera2 object.
sensor_info:SensorInfoUndocumented
target_white_level:intThe raw value we aim for, the raw value of the brightest pixels should be approximately this bright. The value to set depends on the sensor bit depth. We recommend values of 700 for 10-bit sensors and 2800 for 12-bit sensors. This is about 70% of saturated once the blacklevel is subtracted. The maximum possible value depends on the sensor bit depth, the sensor blacklevel and the tolerance argument.
max_iterations:intWe will terminate once we perform this many iterations, whether or not we converge. More than 10 shouldn't happen.
tolerance:floatHow close to the target value we consider "done". Expressed as a fraction of the target_white_level so 0.05 means +/- 5%
percentile:floatRather then use the maximum value for each channel, we calculate a percentile. This makes us robust to single pixels that are bright/noisy. 99.9% still picks the top of the brightness range, but seems much more reliable than just np.max().
Returns
floatUndocumented
def lst_from_camera(camera: Picamera2, sensor_info: SensorInfo) -> LensShadingTables: (source)

Acquire a raw image and use it to calculate a lens shading table.

def recreate_camera_manager(): (source)

Delete and recreate the camera manager.

This is necessary to ensure the tuning file is re-read.

IMX219_SENSOR_INFO = (source)

Undocumented

Value
SensorInfo(sensor_model='imx219',
           unpacked_pixel_format='SBGGR10',
           bit_depth=10,
           blacklevel=64,
           default_target_white_level=700,
           short_pause=0.2,
           long_pause=0.5)
IMX477_SENSOR_INFO = (source)

Undocumented

Value
SensorInfo(sensor_model='imx477',
           unpacked_pixel_format='SBGGR12',
           bit_depth=12,
           blacklevel=256,
           default_target_white_level=2800,
           short_pause=0.2,
           long_pause=1.0)

Undocumented

Value
logging.getLogger(__name__)
LensShadingTables = (source)

Undocumented

Value
tuple[np.ndarray, np.ndarray, np.ndarray]
def _channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray: (source)

Given the 'array' from a PiBayerArray, return the 4 channels.

def _check_convergence(test: _ExposureTest, target: int, tolerance: float) -> bool: (source)

Check whether the brightness is within the specified target range.

def _downsampled_channels(channels: np.ndarray, blacklevel: int) -> list[np.ndarray]: (source)

Generate a downsampled, un-normalised image from which to calculate the LST.

def _get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray: (source)

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 for consistency.

def _grids_from_lst(lum: np.ndarray, Cr: np.ndarray, Cb: np.ndarray) -> np.ndarray: (source)

Convert form luminance/chrominance dict to four RGGB channels.

Note that these will be normalised - the maximum green value is always 1. Also, note that the channels are BGGR, to be consistent with the channels_from_raw_image function. This should probably change in the future.

def _lst_from_channels(channels: np.ndarray, blacklevel: int) -> LensShadingTables: (source)

Given the 4 Bayer colour channels from a white image, generate a LST.

Internally, is just calls _downsampled_channels and _lst_from_grids.

def _lst_from_grids(grids: np.ndarray) -> LensShadingTables: (source)

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, and is in luminance, Cr, Cb format. This function returns three ndarrays of luminance, Cr, Cb, each with shape (12, 16).

def _raw_channels_from_camera(camera: Picamera2, sensor_info: SensorInfo) -> LensShadingTables: (source)

Acquire a raw image and return a 4xNxM array of the colour channels.

def _set_minimum_exposure(camera: Picamera2, sensor_info: SensorInfo): (source)

Enable manual exposure, with low gain and shutter speed.

Set exposure mode to manual, analog and digital gain to 1, and shutter speed to the minimum (8us for Pi Camera v2)

Note ISO is left at auto, because this is needed for the gains to be set correctly.

def _test_exposure_settings(camera: Picamera2, percentile: float) -> _ExposureTest: (source)

Evaluate current exposure settings using a raw image.

CAMERA SHOULD BE STARTED!

We will acquire a raw image and calculate the given percentile of the pixel values. We return a dictionary containing the percentile (which will be compared to the target), as well as the camera's shutter and gain values.

def _upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray: (source)

Zoom an image in the last two dimensions.

This is effectively the inverse operation of _get_16x12_grid