Static type analysis

This commit is contained in:
Joel Collins 2020-11-30 13:36:45 +00:00
parent 3aebb8bead
commit 7866ec0f47
63 changed files with 1825 additions and 2722 deletions

View file

@ -4,6 +4,8 @@ import logging
import time
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from types import TracebackType
from typing import BinaryIO, List, Optional, Tuple, Type, Union
from labthings import ClientEvent, StrictLock
@ -27,16 +29,29 @@ class FrameStream(io.BytesIO):
# Array of TrackerFrame objects
io.BytesIO.__init__(self, *args, **kwargs)
# Array of TrackerFramer objects
self.frames = []
self.frames: List[TrackerFrame] = []
# Last acquired TrackerFramer object
self.last = None
self.last: Optional[TrackerFrame] = None
# Are we currently tracking frame sizes?
self.tracking = False
self.tracking: bool = False
# Event to track if a new frame is available since the last getvalue() call
# We use a ClientEvent so that each thread can call getvalue() independantly
self.new_frame = ClientEvent()
self.new_frame: ClientEvent = ClientEvent()
def __enter__(self):
self.start_tracking()
return super().__enter__()
def __exit__(
self,
t: Optional[Type[BaseException]],
value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
self.stop_tracking()
return super().__exit__(t, value, traceback)
def start_tracking(self):
"""Start tracking frame sizes"""
@ -92,13 +107,16 @@ class BaseCamera(metaclass=ABCMeta):
def __init__(self):
#: :py:class:`labthings.StrictLock`: Access lock for the camera
self.lock = StrictLock(name="Camera", timeout=None)
self.lock: StrictLock = StrictLock(name="Camera", timeout=None)
#: :py:class:`FrameStream`: Streaming and analysis frame buffer
self.stream = FrameStream()
self.stream: FrameStream = FrameStream()
self.stream_active = False
self.record_active = False
self.preview_active = False
self.stream_active: bool = False
self.record_active: bool = False
self.preview_active: bool = False
self.image_resolution: Tuple[int, int] = (1312, 976)
self.stream_resolution: Tuple[int, int] = (640, 480)
@property
@abstractmethod
@ -114,6 +132,14 @@ class BaseCamera(metaclass=ABCMeta):
def settings(self):
return self.read_settings()
@abstractmethod
def start_stream(self):
"""Ensure the frame stream is actively running"""
@abstractmethod
def stop_stream(self):
"""Stop the active stream, if possible"""
@abstractmethod
def update_settings(self, config: dict):
"""Update settings from a config dictionary"""
@ -122,6 +148,28 @@ class BaseCamera(metaclass=ABCMeta):
def read_settings(self) -> dict:
"""Return the current settings as a dictionary"""
@abstractmethod
def capture(
self,
output: Union[str, BinaryIO],
fmt: str = "jpeg",
use_video_port: bool = False,
resize: Optional[Tuple[int, int]] = None,
bayer: bool = True,
thumbnail: Optional[Tuple[int, int, int]] = None,
):
"""
Perform a basic capture to output
Args:
output: String or file-like object to write capture data to
fmt: Format of the capture.
use_video_port: Capture from the video port used for streaming. Lower resolution, faster.
resize: Resize the captured image.
bayer: Store raw bayer data in capture
thumbnail: Dimensions and quality (x, y, quality) of a thumbnail to generate, if supported
"""
def __enter__(self):
"""Create camera on context enter."""
return self

View file

@ -8,6 +8,8 @@ import time
from datetime import datetime
# Type hinting
from typing import BinaryIO, Optional, Tuple, Union
from PIL import Image, ImageDraw
from openflexure_microscope.camera.base import BaseCamera
@ -26,17 +28,17 @@ class MissingCamera(BaseCamera):
BaseCamera.__init__(self)
# Update config properties
self.image_resolution = (1312, 976)
self.stream_resolution = (640, 480)
self.numpy_resolution = (1312, 976)
self.jpeg_quality = 75
self.framerate = 10
self.image_resolution: Tuple[int, int] = (1312, 976)
self.stream_resolution: Tuple[int, int] = (640, 480)
self.numpy_resolution: Tuple[int, int] = (1312, 976)
self.jpeg_quality: int = 75
self.framerate: int = 10
# Generate an initial dummy image
self.generate_new_dummy_image()
# Start streaming
self.stop = False # Used to indicate that the stream loop should break
self.stop: bool = False # Used to indicate that the stream loop should break
self.start_worker()
# Wait until frames are available
logging.info("Waiting for frames")
@ -178,7 +180,11 @@ class MissingCamera(BaseCamera):
"""
logging.warning("Zoom not implemented in mock camera")
# LAUNCH ACTIONS
def start_stream(self):
pass
def stop_stream(self):
pass
def start_preview(self, *_, **__):
"""Start the on board GPU camera preview."""
@ -211,7 +217,15 @@ class MissingCamera(BaseCamera):
with self.lock:
logging.warning("Recording not implemented in mock camera")
def capture(self, output, *_, **__):
def capture(
self,
output: Union[str, BinaryIO],
fmt: str = "jpeg",
use_video_port: bool = False,
resize: Optional[Tuple[int, int]] = None,
bayer: bool = True,
thumbnail: Optional[Tuple[int, int, int]] = None,
):
"""
Capture a still image to a StreamObject.

View file

@ -30,7 +30,7 @@ import logging
import time
# Type hinting
from typing import Tuple
from typing import BinaryIO, Tuple, Union
import numpy as np
@ -77,57 +77,64 @@ class PiCameraStreamer(BaseCamera):
BaseCamera.__init__(self)
#: :py:class:`picamerax.PiCamera`: Attached Picamera object
self.camera = picamerax.PiCamera()
self.picamera: picamerax.PiCamera = picamerax.PiCamera()
# Store state of PiCameraStreamer
self.preview_active = False
self.preview_active: bool = False
# Reset variable states
self.set_zoom(1.0)
#: tuple: Resolution for image captures
self.image_resolution = tuple(self.camera.MAX_RESOLUTION)
self.image_resolution: Tuple[int, int] = tuple(self.picamera.MAX_RESOLUTION)
#: tuple: Resolution for stream and video captures
self.stream_resolution = (832, 624)
self.stream_resolution: Tuple[int, int] = (832, 624)
#: tuple: Resolution for numpy array captures
self.numpy_resolution = (1312, 976)
self.numpy_resolution: Tuple[int, int] = (1312, 976)
self.jpeg_quality = 100 #: int: JPEG quality
self.mjpeg_quality = 75 #: int: MJPEG quality
self.jpeg_quality: int = 100 #: int: JPEG quality
self.mjpeg_quality: int = 75 #: int: MJPEG quality
# Start stream recording (and set resolution)
self.start_stream_recording()
self.start_stream()
# Wait until frames are available
logging.debug("Waiting for frames...")
self.stream.new_frame.wait()
logging.debug("Camera initialised")
@property
def configuration(self):
"""The current camera configuration."""
return {"board": self.camera.revision}
def camera(self):
logging.warning(
"PiCameraStreamer.camera is deprecated. Replace with PiCameraStreamer.picamera"
)
return self.picamera
@property
def state(self):
def configuration(self) -> dict:
"""The current camera configuration."""
return {"board": self.picamera.revision}
@property
def state(self) -> dict:
"""The current read-only camera state."""
return {}
def close(self):
"""Close the Raspberry Pi PiCameraStreamer."""
# Stop stream recording
self.stop_stream_recording()
self.stop_stream()
# Run BaseCamera close method
super().close()
# Detach Pi camera
if self.camera:
self.camera.close()
if self.picamera:
self.picamera.close()
# HANDLE SETTINGS
def read_settings(self) -> dict:
"""
Return config dictionary of the PiCameraStreamer.
"""
conf_dict = {
conf_dict: dict = {
"stream_resolution": self.stream_resolution,
"image_resolution": self.image_resolution,
"numpy_resolution": self.numpy_resolution,
@ -139,7 +146,7 @@ class PiCameraStreamer(BaseCamera):
# Include a subset of picamera properties. Excludes lens shading table
for key in PiCameraStreamer.picamera_settings_keys:
try:
value = getattr(self.camera, key)
value = getattr(self.picamera, key)
logging.debug("Reading PiCamera().%s: %s", key, value)
conf_dict["picamera"][key] = value
except AttributeError:
@ -147,11 +154,11 @@ class PiCameraStreamer(BaseCamera):
# Include a serialised lens shading table
if (
hasattr(self.camera, "lens_shading_table")
and getattr(self.camera, "lens_shading_table") is not None
hasattr(self.picamera, "lens_shading_table")
and getattr(self.picamera, "lens_shading_table") is not None
):
conf_dict["picamera"]["lens_shading_table"] = ndarray_to_json(
getattr(self.camera, "lens_shading_table")
getattr(self.picamera, "lens_shading_table")
)
return conf_dict
@ -179,7 +186,7 @@ class PiCameraStreamer(BaseCamera):
# Pause stream while changing settings
if self.stream_active: # If stream is active
logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream
self.stop_stream() # Pause stream
paused_stream = True # Remember to unpause stream when done
# PiCamera parameters
@ -190,11 +197,11 @@ class PiCameraStreamer(BaseCamera):
# Handle lens shading if camera supports it
if (
hasattr(self.camera, "lens_shading_table")
hasattr(self.picamera, "lens_shading_table")
and "lens_shading_table" in config["picamera"]
):
try:
self.camera.lens_shading_table = json_to_ndarray(
self.picamera.lens_shading_table = json_to_ndarray(
config["picamera"].get("lens_shading_table")
)
except KeyError as e:
@ -208,7 +215,7 @@ class PiCameraStreamer(BaseCamera):
# If stream was paused to update config, unpause
if paused_stream:
logging.info("Resuming stream.")
self.start_stream_recording()
self.start_stream()
else:
raise Exception(
@ -229,47 +236,47 @@ class PiCameraStreamer(BaseCamera):
logging.debug(
"Applying exposure_mode: %s", (settings_dict["exposure_mode"])
)
self.camera.exposure_mode = settings_dict["exposure_mode"]
self.picamera.exposure_mode = settings_dict["exposure_mode"]
# Apply gains and let them settle
if "analog_gain" in settings_dict:
logging.debug("Applying analog_gain: %s", (settings_dict["analog_gain"]))
set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
set_analog_gain(self.picamera, float(settings_dict["analog_gain"]))
if "digital_gain" in settings_dict:
logging.debug("Applying digital_gain: %s", (settings_dict["digital_gain"]))
set_digital_gain(self.camera, float(settings_dict["digital_gain"]))
set_digital_gain(self.picamera, float(settings_dict["digital_gain"]))
# Apply shutter speed
if "shutter_speed" in settings_dict:
logging.debug(
"Applying shutter_speed: %s", (settings_dict["shutter_speed"])
)
self.camera.shutter_speed = int(settings_dict["shutter_speed"])
self.picamera.shutter_speed = int(settings_dict["shutter_speed"])
time.sleep(0.2) # Let gains settle
# Handle AWB in a half-smart way
if "awb_gains" in settings_dict:
logging.debug("Applying awb_mode: off")
self.camera.awb_mode = "off"
self.picamera.awb_mode = "off"
logging.debug("Applying awb_gains: %s", (settings_dict["awb_gains"]))
self.camera.awb_gains = settings_dict["awb_gains"]
self.picamera.awb_gains = settings_dict["awb_gains"]
elif "awb_mode" in settings_dict:
logging.debug("Applying awb_mode: %s", (settings_dict["awb_mode"]))
self.camera.awb_mode = settings_dict["awb_mode"]
self.picamera.awb_mode = settings_dict["awb_mode"]
# Handle some properties that can be quickly applied
batched_keys = ["framerate", "saturation"]
for key in batched_keys:
if (key in settings_dict) and hasattr(self.camera, key):
if (key in settings_dict) and hasattr(self.picamera, key):
logging.debug("Applying %s: %s", key, settings_dict[key])
setattr(self.camera, key, settings_dict[key])
setattr(self.picamera, key, settings_dict[key])
# Final optional pause to settle
if pause_for_effect:
time.sleep(0.2)
def set_zoom(self, zoom_value: float = 1.0) -> None:
def set_zoom(self, zoom_value: Union[float, int] = 1.0) -> None:
"""
Change the camera zoom, handling re-centering and scaling.
"""
@ -278,7 +285,7 @@ class PiCameraStreamer(BaseCamera):
if self.zoom_value < 1:
self.zoom_value = 1
# Richard's code for zooming !
fov = self.camera.zoom
fov = self.picamera.zoom
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
size = 1.0 / self.zoom_value
# If the new zoom value would be invalid, move the centre to
@ -289,23 +296,23 @@ class PiCameraStreamer(BaseCamera):
centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5)
logging.info("setting zoom, centre %s, size %s", centre, size)
new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size)
self.camera.zoom = new_fov
self.picamera.zoom = new_fov
# LAUNCH ACTIONS
def start_preview(self, fullscreen=True, window=None):
def start_preview(
self, fullscreen: bool = True, window: Tuple[int, int, int, int] = None
):
"""Start the on board GPU camera preview."""
with self.lock(timeout=1):
try:
if not self.camera.preview:
if not self.picamera.preview:
logging.debug("Starting preview")
self.camera.start_preview(fullscreen=fullscreen, window=window)
self.picamera.start_preview(fullscreen=fullscreen, window=window)
else:
logging.debug("Resizing preview")
if window:
self.camera.preview.window = window
self.picamera.preview.window = window
if fullscreen:
self.camera.preview.fullscreen = fullscreen
self.picamera.preview.fullscreen = fullscreen
self.preview_active = True
except picamerax.exc.PiCameraMMALError as e:
logging.error(
@ -320,11 +327,13 @@ class PiCameraStreamer(BaseCamera):
def stop_preview(self):
"""Stop the on board GPU camera preview."""
with self.lock(timeout=1):
if self.camera.preview:
self.camera.stop_preview()
if self.picamera.preview:
self.picamera.stop_preview()
self.preview_active = False
def start_recording(self, output, fmt: str = "h264", quality: int = 15):
def start_recording(
self, output: Union[str, BinaryIO], fmt: str = "h264", quality: int = 15
):
"""Start recording.
Start a new video recording, writing to a output object.
@ -345,7 +354,7 @@ class PiCameraStreamer(BaseCamera):
# Start the camera video recording on port 2
logging.info("Recording to %s", (output))
self.camera.start_recording(
self.picamera.start_recording(
output,
format=fmt,
splitter_port=2,
@ -370,80 +379,67 @@ class PiCameraStreamer(BaseCamera):
with self.lock(timeout=5):
# Stop the camera video recording on port 2
logging.info("Stopping recording")
self.camera.stop_recording(splitter_port=2)
self.picamera.stop_recording(splitter_port=2)
logging.info("Recording stopped")
# Update state
self.record_active = False
def start_stream_recording(self, splitter_port: int = 1, **kwargs) -> None:
def start_stream(self) -> None:
"""
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
Args:
splitter_port (int): Splitter port to start recording on
"""
for k in kwargs.keys():
logging.warning(
"Warning, kwarg %s is invalid for stop_stream_recording.", k
)
with self.lock(timeout=None):
# Reduce the resolution for video streaming
try:
self.camera._check_recording_stopped() # pylint: disable=W0212
self.picamera._check_recording_stopped() # pylint: disable=W0212
except picamerax.exc.PiCameraRuntimeError:
logging.info(
"Error while changing resolution: Recording already running."
)
else:
self.camera.resolution = self.stream_resolution
self.picamera.resolution = self.stream_resolution
# Sprinkled a sleep to prevent camera getting confused by rapid commands
time.sleep(0.2)
# If the stream should be active
try:
# Start recording on stream port
self.camera.start_recording(
self.picamera.start_recording(
self.stream,
format="mjpeg",
quality=self.mjpeg_quality,
bitrate=-1, # RWB: disable bitrate control
# (bitrate control makes JPEG size less good as a focus
# metric)
splitter_port=splitter_port,
splitter_port=1,
)
except picamerax.exc.PiCameraAlreadyRecording:
logging.info("Error while starting preview: Recording already running.")
else:
self.stream_active = True
logging.debug(
"Started MJPEG stream at %s on port %s",
self.stream_resolution,
splitter_port,
"Started MJPEG stream at %s on port %s", self.stream_resolution, 1
)
def stop_stream_recording(self, splitter_port: int = 1, **kwargs) -> None:
def stop_stream(self) -> None:
"""
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
Args:
splitter_port (int): Splitter port to stop recording on
"""
for k in kwargs.keys():
logging.warning(
"Warning, kwarg %s is invalid for stop_stream_recording.", k
)
with self.lock:
# Stop the camera video recording on port 1
try:
self.camera.stop_recording(splitter_port=splitter_port)
self.picamera.stop_recording(splitter_port=1)
except picamerax.exc.PiCameraNotRecording:
logging.info("Not recording on splitter_port %s", (splitter_port))
logging.info("Not recording on splitter_port %s", (1))
else:
self.stream_active = False
logging.info(
"Stopped MJPEG stream on port %s. Switching to %s.",
splitter_port,
1,
self.image_resolution,
)
@ -451,16 +447,16 @@ class PiCameraStreamer(BaseCamera):
time.sleep(
0.2
) # Sprinkled a sleep to prevent camera getting confused by rapid commands
self.camera.resolution = self.image_resolution
self.picamera.resolution = self.image_resolution
def capture(
self,
output,
output: Union[str, BinaryIO],
fmt: str = "jpeg",
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = True,
thumbnail: tuple = None,
thumbnail: Tuple[int, int, int] = None,
):
"""
Capture a still image to a StreamObject.
@ -470,10 +466,11 @@ class PiCameraStreamer(BaseCamera):
Args:
output: String or file-like object to write capture data to
fmt (str): Format of the capture.
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
resize ((int, int)): Resize the captured image.
bayer (bool): Store raw bayer data in capture
fmt: Format of the capture.
use_video_port: Capture from the video port used for streaming. Lower resolution, faster.
resize: Resize the captured image.
bayer: Store raw bayer data in capture
thumbnail: Dimensions and quality (x, y, quality) of a thumbnail to generate, if supported
Returns:
output_object (str/BytesIO): Target object.
@ -483,9 +480,9 @@ class PiCameraStreamer(BaseCamera):
# Set resolution and stop stream recording if necessary
if not use_video_port:
self.stop_stream_recording()
self.stop_stream()
self.camera.capture(
self.picamera.capture(
output,
format=fmt,
quality=self.jpeg_quality,
@ -497,11 +494,11 @@ class PiCameraStreamer(BaseCamera):
# Set resolution and start stream recording if necessary
if not use_video_port:
self.start_stream_recording()
self.start_stream()
return output
def array(self, use_video_port=True) -> np.ndarray:
def array(self, use_video_port: bool = True) -> np.ndarray:
"""Capture an uncompressed still RGB image to a Numpy array.
Args:
@ -513,7 +510,9 @@ class PiCameraStreamer(BaseCamera):
"""
with self.lock:
logging.debug("Creating PiRGBArray")
with picamerax.array.PiRGBArray(self.camera) as output:
with picamerax.array.PiRGBArray(self.picamera) as output:
logging.info("Capturing to %s", (output))
self.camera.capture(output, format="rgb", use_video_port=use_video_port)
self.picamera.capture(
output, format="rgb", use_video_port=use_video_port
)
return output.array

View file

@ -2,16 +2,17 @@ from __future__ import print_function
import logging
import time
from typing import Union
import picamerax
from picamerax import exc, mmal
from picamerax.mmalobj import to_rational
MMAL_PARAMETER_ANALOG_GAIN = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x59
MMAL_PARAMETER_DIGITAL_GAIN = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x5A
MMAL_PARAMETER_ANALOG_GAIN: int = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x59
MMAL_PARAMETER_DIGITAL_GAIN: int = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x5A
def set_gain(camera, gain, value):
def set_gain(camera: picamerax.PiCamera, gain: int, value: Union[int, float]):
"""Set the analog gain of a PiCamera.
camera: the picamerax.PiCamera() instance you are configuring
@ -32,12 +33,12 @@ def set_gain(camera, gain, value):
raise exc.PiCameraMMALError(ret)
def set_analog_gain(camera, value):
def set_analog_gain(camera: picamerax.PiCamera, value: Union[int, float]):
"""Set the gain of a PiCamera object to a given value."""
set_gain(camera, MMAL_PARAMETER_ANALOG_GAIN, value)
def set_digital_gain(camera, value):
def set_digital_gain(camera: picamerax.PiCamera, value: Union[int, float]):
"""Set the digital gain of a PiCamera object to a given value."""
set_gain(camera, MMAL_PARAMETER_DIGITAL_GAIN, value)