Restructured text fixes so that pydoctor would return without an error

This commit is contained in:
Julian Stirling 2025-07-09 23:55:46 +01:00
parent 58b056988a
commit a84a916719
31 changed files with 269 additions and 256 deletions

View file

@ -70,12 +70,12 @@ class CameraMemoryBuffer:
every time an image is added.
:param image: The image to add. A PIL image is recommended, but cameras
can choose to use other formats
can choose to use other formats
:param metadata: Optional, a dictionary of the image metadata.
:param buffer_max: The maximum number of images that should be in the buffer
once this images is added. Default is 1.
once this images is added. Default is 1.
:return buffer_id: The id in the buffer for this image
:returns: The id in the buffer for this image
"""
self._latest_id += 1
self._create_space(buffer_max)
@ -94,9 +94,8 @@ class CameraMemoryBuffer:
:param buffer_id: The buffer id of the image to retrieve
:param remove: True (default) to remove this image from the buffer, False
to leave the image in the buffer.
to leave the image in the buffer.
"""
# No id given
if buffer_id is None:
# Get the latest image and metadata tuple from storage
@ -128,7 +127,7 @@ class CameraMemoryBuffer:
Create space to add an image.
:param buffer_max: The maximum number of images that should be in the buffer
once another images is added.
once another images is added.
"""
# If only one image to be stored just clear the storage and return
if buffer_max <= 1:
@ -164,7 +163,8 @@ class BaseCamera(lt.Thing):
self, main_resolution: tuple[int, int], buffer_count: int
) -> None:
"""Start (or stop and restart) the camera with the given resolution
for the main stream, and buffer_count number of images in the buffer"""
for the main stream, and buffer_count number of images in the buffer
"""
raise NotImplementedError(
"CameraThings must define their own start_streaming method"
)
@ -175,9 +175,9 @@ class BaseCamera(lt.Thing):
This is called when uvicorn gets the a shutdown signal. As this is called from
the event loop it cannot interact with the our ThingProperties or run
`self.mjpeg_stream.stop()` as the portal cannot be called from this loop.
``self.mjpeg_stream.stop()`` as the portal cannot be called from this loop.
Instead we just set the `_streaming` value to False. This stops the async frame
Instead we just set the ``_streaming`` value to False. This stops the async frame
generator when the next frame notifies.
"""
if self.stream_active:
@ -186,7 +186,7 @@ class BaseCamera(lt.Thing):
@lt.thing_property
def stream_active(self) -> bool:
"Whether the MJPEG stream is active"
"""Whether the MJPEG stream is active"""
raise NotImplementedError(
"CameraThings must define their own stream_active method"
)
@ -221,7 +221,7 @@ class BaseCamera(lt.Thing):
) -> JPEGBlob:
"""Acquire one image from the preview stream and return as an array
This differs from `capture_jpeg` in that it does not pause the MJPEG
This differs from ``capture_jpeg`` in that it does not pause the MJPEG
preview stream. Instead, we simply return the next frame from that
stream (either "main" for the preview stream, or "lores" for the low
resolution preview). No metadata is returned.
@ -267,11 +267,11 @@ class BaseCamera(lt.Thing):
:param jpeg_path: The path to save the file to
:param logger: This should be injected automatically by Labthings FastAPI
when calling the action
when calling the action
:param metadata_getter: This should be injected automatically by Labthings
FastAPI when calling the action
FastAPI when calling the action
:param save_resolution: can be set to resize the image before saving. By
default this is None meaning that the image is saved at original resolution.
default this is None meaning that the image is saved at original resolution.
"""
image, metadata = self._robust_image_capture(
metadata_getter,
@ -294,19 +294,19 @@ class BaseCamera(lt.Thing):
buffer_max: int = 1,
) -> None:
"""
Capture an image to memory. This can be saved later with `save_from_memory`
Capture an image to memory. This can be saved later with ``save_from_memory``
Note that only one image is held in memory so this will overwrite any image
in memory.
:param logger: This should be injected automatically by Labthings FastAPI
when calling the action
when calling the action
:param metadata_getter: This should be injected automatically by Labthings
FastAPI when calling the action
FastAPI when calling the action
:param buffer_max: The maximum number of images that should be in the buffer
once this images is added. Default is 1.
once this images is added. Default is 1.
:return: the buffer id of the image captured
:returns: the buffer id of the image captured
"""
image, metadata = self._robust_image_capture(metadata_getter, logger)
return self._memory_buffer.add_image(image, metadata, buffer_max=buffer_max)
@ -324,11 +324,12 @@ class BaseCamera(lt.Thing):
:param jpeg_path: The path to save the file to
:param logger: This should be injected automatically by Labthings FastAPI
when calling the action
when calling the action
:param save_resolution: can be set to resize the image before saving. By
default this is None meaning that the image is saved at original resolution.
default this is None meaning that the image is saved at original
resolution.
:param buffer_id: The buffer id of the image to save, this was returned by
`capture_to_memory`
``capture_to_memory``
"""
image, metadata = self._memory_buffer.get_image(buffer_id)
@ -379,7 +380,8 @@ class BaseCamera(lt.Thing):
"""Saving the captured image and metadata to disk
logger warning (via InvocationLogger) is raised if metadata is failed to be added
IOError is raised if the file cannot be saved
nothing is returned on success"""
nothing is returned on success
"""
if save_resolution is not None and image.size != save_resolution:
image = image.resize(save_resolution, Image.BOX)
try:

View file

@ -1,7 +1,7 @@
"""OpenFlexure Microscope OpenCV Camera
This module defines a camera Thing that uses OpenCV's
`VideoCapture`.
``VideoCapture``.
See repository root for licensing information.
"""
@ -45,7 +45,7 @@ class OpenCVCamera(BaseCamera):
@lt.thing_property
def stream_active(self) -> bool:
"Whether the MJPEG stream is active"
"""Whether the MJPEG stream is active"""
if self._capture_enabled and self._capture_thread:
return self._capture_thread.is_alive()
return False

View file

@ -264,7 +264,6 @@ class StreamingPiCamera2(BaseCamera):
@sensor_mode.setter
def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]):
"""Change the sensor mode used"""
if new_mode is None:
self._sensor_mode = None
elif isinstance(new_mode, SensorModeSelector):
@ -287,9 +286,9 @@ class StreamingPiCamera2(BaseCamera):
tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
def _initialise_picamera(self):
"""Acquire the picamera device and store it as `self._picamera`.
"""Acquire the picamera device and store it as ``self._picamera``.
This duplicates logic in `Picamera2.__init__` to provide a tuning file that
This duplicates logic in ``Picamera2.__init__`` to provide a tuning file that
will be read when the camera system initialises.
"""
if self._picamera_lock is not None:
@ -332,16 +331,17 @@ class StreamingPiCamera2(BaseCamera):
@contextmanager
def _streaming_picamera(self, pause_stream=False) -> Iterator[Picamera2]:
"""Lock access to picamera and return the underlying `Picamera2` instance.
"""Lock access to picamera and return the underlying ``Picamera2`` instance.
Optionally the stream can be paused to allow updating the camera settings.
:param pause_stream: If False the `Picamera2` instance is simply yielded.
If True:
* Stop the MJPEG Stream
* Yield the `Picamera2` instance for function calling the context manager to
make changes.
* On closing of the context manager the stream will restart.
:param pause_stream: If False the ``Picamera2`` instance is simply yielded.
If True:
* Stop the MJPEG Stream
* Yield the ``Picamera2`` instance for function calling the context manager to
make changes.
* On closing of the context manager the stream will restart.
"""
already_streaming = self.stream_active
with self._picamera_lock:
@ -372,8 +372,9 @@ class StreamingPiCamera2(BaseCamera):
manual.
Create two streams:
- `lores_mjpeg_stream` for autofocus at low-res resolution
- `mjpeg_stream` for preview. This is the `main_resolution` if this is less
* ``lores_mjpeg_stream`` for autofocus at low-res resolution
* ``mjpeg_stream`` for preview. This is the ``main_resolution`` if this is less
than (1280, 960), or the low-res resolution if above. This allows for
high resolution capture without streaming high resolution video.
@ -489,7 +490,6 @@ class StreamingPiCamera2(BaseCamera):
A TimeoutError is raised if this time is exceeded during capture.
Default = 0.9s, lower than the 1s timeout default in picamera yaml settings
"""
# This was slower than capture_image for our use case, but directly returning
# an image as an array is still a useful feature
if stream_name == "full":
@ -523,13 +523,13 @@ class StreamingPiCamera2(BaseCamera):
) -> JPEGBlob:
"""Acquire one image from the camera as a JPEG
The JPEG will be acquired using `Picamera2.capture_file`. If the
`resolution` parameter is `main` or `lores`, it will be captured
The JPEG will be acquired using ``Picamera2.capture_file``. If the
``resolution`` parameter is ``main`` or ``lores``, it will be captured
from the main preview stream, or the low-res preview stream,
respectively. This means the camera won't be reconfigured, and
the stream will not pause (though it may miss one frame).
If `full` resolution is requested, we will briefly pause the
If ``full`` resolution is requested, we will briefly pause the
MJPEG stream and reconfigure the camera to capture a full
resolution image.
@ -588,11 +588,12 @@ class StreamingPiCamera2(BaseCamera):
the image reaches the specified white level.
:param target_white_level: The target 10bit white level. 10-bit data has a
theoretical maximum of 1023, but with black level correction the true maxiumum
is about 950. Default is 700 as this is approximately 70% saturated.
theoretical maximum of 1023, but with black level correction the true
maximum is about 950. Default is 700 as this is approximately 70%
saturated.
:param percentile: The percentile to use instead of maximum. Default 99.9. When
calculating the brightest pixel, a percentile is used rather than the
maximum in order to be robust to a small number of noisy/bright pixels.
calculating the brightest pixel, a percentile is used rather than the
maximum in order to be robust to a small number of noisy/bright pixels.
"""
with self._streaming_picamera(pause_stream=True) as cam:
recalibrate_utils.adjust_shutter_and_gain_from_raw(
@ -615,7 +616,7 @@ class StreamingPiCamera2(BaseCamera):
image with the lens shading correction applied, which should mean
that the image is uniform, rather than weighted towards the centre.
If `method` is `"centre"`, we will correct the mean of the central 10%
If ``method`` is ``"centre"``, we will correct the mean of the central 10%
of the image.
"""
with self._streaming_picamera(pause_stream=True) as cam:
@ -657,7 +658,7 @@ class StreamingPiCamera2(BaseCamera):
def colour_correction_matrix(
self,
) -> tuple[float, float, float, float, float, float, float, float, float]:
"""The `colour_correction_matrix` from the tuning file.
"""The ``colour_correction_matrix`` from the tuning file.
This is broken out into its own property for convenience and compatibility with
the micromanager API
@ -720,11 +721,11 @@ class StreamingPiCamera2(BaseCamera):
This function will call the other calibration actions in sequence:
* `flat_lens_shading` to disable flat-field
* `auto_expose_from_minimum`
* `set_static_green_equalisation` to set geq offset to max
* `calibrate_lens_shading`
* `calibrate_white_balance`
* ``flat_lens_shading`` to disable flat-field
* ``auto_expose_from_minimum``
* ``set_static_green_equalisation`` to set geq offset to max
* ``calibrate_lens_shading``
* ``calibrate_white_balance``
"""
self.flat_lens_shading()
self.auto_expose_from_minimum()
@ -804,15 +805,15 @@ class StreamingPiCamera2(BaseCamera):
The white balance algorithm we use assumes the brightest pixels
should be white, and that the only thing affecting the colour of
said pixels is the `colour_gains`.
said pixels is the ``colour_gains``.
The lens shading correction is normalised such that the *minimum*
gain in the `Cr` and `Cb` channels is 1. The white balance
gain in the ``Cr`` and ``Cb`` channels is 1. The white balance
assumption above requires that the gain for the brightest pixels
is 1. The solution might be that, when calibrating, we note which
pixels are brightest (usually the centre) and explicitly use
the LST values for there. However, for now I will assume that we
need to normalise by the **maximum** of the `Cr` and `Cb`
need to normalise by the **maximum** of the ``Cr`` and ``Cb``
channels, which is correct the majority of the time.
"""
if not self.lens_shading_is_static:

View file

@ -3,7 +3,7 @@ Functions to set up a Raspberry Pi Camera v2 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
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.
@ -20,14 +20,15 @@ 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()
.. code-block:: python
picamera = picamera2.Picamera2()
adjust_shutter_and_gain_from_raw(picamera)
adjust_white_balance_from_raw(picamera)
lst = lst_from_camera(picamera)
picamera.lens_shading_table = lst
adjust_shutter_and_gain_from_raw(picamera)
adjust_white_balance_from_raw(picamera)
lst = lst_from_camera(picamera)
picamera.lens_shading_table = lst
```
"""
# Disable N806 & 803, which checks that all variables and args are lowercase.
@ -55,7 +56,7 @@ def load_default_tuning(cam: Picamera2) -> dict:
"""Load the default tuning file for the camera
This will open and close the camera to determine its model. If you are
using a model that's supported by `picamera2` it should have a tuning
using a model that's supported by ``picamera2`` it should have a tuning
file built in. If not, this will probably crash with an error.
Error handling for unsupported cameras is not something we are likely
@ -152,25 +153,19 @@ def adjust_shutter_and_gain_from_raw(
This routine is slow but effective. It uses raw images, so we
are not affected by white balance or digital gain.
:param camera: A Picamera2 object.
:param target_white_level: The raw, 10-bit value we aim for. The brightest pixels
should be approximately this bright. Maximum possible is about 900, 700 is
reasonable.
:param max_iterations: We will terminate once we perform this many iterations,
whether or not we converge. More than 10 shouldn't happen.
:param tolerance: How close to the target value we consider "done". Expressed as a
fraction of the ``target_white_level`` so 0.05 means +/- 5%
:param percentile: Rather 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()``.
Arguments:
target_white_level:
The raw, 10-bit value we aim for. The brightest pixels
should be approximately this bright. Maximum possible
is about 900, 700 is reasonable.
max_iterations:
We will terminate once we perform this many iterations,
whether or not we converge. More than 10 shouldn't happen.
tolerance:
How close to the target value we consider "done". Expressed
as a fraction of the ``target_white_level`` so 0.05 means
+/- 5%
percentile:
Rather 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()``.
"""
# TODO: read black level and bit depth from camera?
if target_white_level * (tolerance + 1) >= 959:
@ -351,7 +346,7 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray:
def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray:
"""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``
"""
zoom_factors = [
1,
@ -381,7 +376,7 @@ def downsampled_channels(channels: np.ndarray, blacklevel=64) -> list[np.ndarray
def lst_from_channels(channels: np.ndarray) -> LensShadingTables:
"""Given the 4 Bayer colour channels from a white image, generate a LST.
Internally, is just calls `downsampled_channels` and `lst_from_grids`.
Internally, is just calls ``downsampled_channels`` and ``lst_from_grids``.
"""
grids = downsampled_channels(channels)
return lst_from_grids(grids)
@ -392,11 +387,10 @@ def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
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
luminance, Cr, Cb, each with shape (12, 16).
"""
# Calculated red, green, and blue channels from Bayer data
r: np.ndarray = grids[3, ...]
g: np.ndarray = np.mean(grids[1:3, ...], axis=0)
@ -419,7 +413,7 @@ def grids_from_lst(lum: np.ndarray, Cr: np.ndarray, Cb: np.ndarray) -> np.ndarra
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
``channels_from_raw_image`` function. This should probably change in the
future.
"""
G = 1 / np.array(lum)
@ -434,9 +428,9 @@ def set_static_lst(
cr: np.ndarray,
cb: np.ndarray,
) -> 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.
"""
for table in luminance, cr, cb:
@ -459,9 +453,9 @@ def set_static_ccm(
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.
"""
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
@ -469,7 +463,7 @@ def set_static_ccm(
def get_static_ccm(tuning: dict) -> None:
"""Get the `rpi.ccm` section of a camera tuning dict"""
"""Get the ``rpi.ccm`` section of a camera tuning dict"""
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
return ccm["ccms"]
@ -484,14 +478,13 @@ def set_static_geq(
tuning: dict,
offset: int = 65535,
) -> None:
"""Update the `rpi.geq` section of a camera tuning dict to always use green
"""Update the ``rpi.geq`` section of a camera tuning dict to always use green
equalisation that averages the green pixels in the red and blue rows.
`tuning` will be updated in-place to set the geq offest to the given value.
``tuning`` will be updated in-place to set the geq offest to the given value.
The default 65535 is the maximum allowed value. This means
the brightness will always be below the threshold where averaging is used.
"""
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
geq["offset"] = offset # max out offset to disable the adaptive green equalisation
@ -511,7 +504,7 @@ def index_of_algorithm(algorithms: list[dict], algorithm: str) -> int:
def copy_alsc_section(from_tuning: dict, to_tuning: dict) -> None:
"""Copy the `rpi.alsc` algorithm from one tuning to another.
"""Copy the ``rpi.alsc`` algorithm from one tuning to another.
This is done in-place, i.e. modifying to_tuning.
"""

View file

@ -145,7 +145,7 @@ class SimulatedCamera(BaseCamera):
@lt.thing_property
def stream_active(self) -> bool:
"Whether the MJPEG stream is active"
"""Whether the MJPEG stream is active"""
if self._capture_enabled and self._capture_thread:
return self._capture_thread.is_alive()
return False