Loading openflexure_microscope/api/default_extensions/autofocus.py +6 −59 Original line number Diff line number Diff line import logging import time from collections import namedtuple from contextlib import contextmanager import numpy as np Loading @@ -14,40 +13,6 @@ from openflexure_microscope.utilities import set_properties ### Autofocus utilities # Class to store a frames metadata TrackerFrame = namedtuple("TrackerFrame", ["size", "time"]) class FrameTracker(object): """ A file-like object used to analyse MJPEG frames. Instead of analysing a load of real MJPEG frames after they've been stored in a BytesIO stream, we tell the camera to write frames to this class instead. We then do analysis as the frames are written, and discard the heavier image data. """ def __init__(self): # Array of TrackerFrame objects self.frames = [] self.last = None def write(self, s): # Store the incoming frame metadata # Discards the actual frame image data # NOTE: This could be used for smarter processing # E.g. different sharpness metrics frame = TrackerFrame(size=len(s), time=time.time()) self.frames.append(frame) self.last = frame def flush(self): # Called at end of recording pass class JPEGSharpnessMonitor: def __init__(self, microscope): Loading @@ -62,36 +27,17 @@ class JPEGSharpnessMonitor: self.jpeg_times = [] self.jpeg_sizes = [] # Splitter port 3 is reserved for autofocus self.splitter_port = 3 self.stream = FrameTracker() def stop_recording(self): logging.info("Stopping recording for autofocus...") self.camera.camera.stop_recording(splitter_port=self.splitter_port) logging.info("Stopped recording for autofocus!") def start(self): # Log the recording start time self.recording_start_time = time.time() # Start the stream recording self.camera.camera.start_recording( self.stream, format="mjpeg", quality=100, # Use best quality (we don't store the frames anyway!) bitrate=-1, # RWB: disable bitrate control # (bitrate control makes JPEG size less good as a focus # metric) splitter_port=self.splitter_port, ) logging.info("Started recording for autofocus") def stop(self): self.stop_recording() logging.info("Stopped recording for autofocus") self.camera.stream.stop_tracking() self.camera.stream.reset_tracking() def focus_rel(self, dz, backlash=False, **kwargs): # Store the start time and position self.camera.stream.start_tracking() self.stage_times.append(time.time()) self.stage_positions.append(self.stage.position) Loading @@ -99,11 +45,12 @@ class JPEGSharpnessMonitor: self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs) # Store the end time and position self.camera.stream.stop_tracking() self.stage_times.append(time.time()) self.stage_positions.append(self.stage.position) # Retrieve frame data for frame in self.stream.frames: for frame in self.camera.stream.frames: # Make timestamp absolute Unix time self.jpeg_times.append(frame.time) self.jpeg_sizes.append(frame.size) Loading Loading @@ -329,7 +276,7 @@ def fast_up_down_up_autofocus( # We've deliberately undershot - figure out how much further we should move based on the curve logging.debug("Calculate remining movement") current_js = m.stream.last.size current_js = m.camera.stream.last.size imax = np.argmax(js) # we want to crop out just the bit below the peak js = js[imax:] # NB z is in DECREASING order jz = jz[imax:] Loading openflexure_microscope/camera/base.py +47 −0 Original line number Diff line number Diff line Loading @@ -2,10 +2,56 @@ import logging import threading import time import io from abc import ABCMeta, abstractmethod from collections import namedtuple from labthings import ClientEvent, StrictLock # Class to store a frames metadata TrackerFrame = namedtuple("TrackerFrame", ["size", "time"]) class FrameStream(io.BytesIO): """ A file-like object used to analyse MJPEG frames. Instead of analysing a load of real MJPEG frames after they've been stored in a BytesIO stream, we tell the camera to write frames to this class instead. We then do analysis as the frames are written, and discard the heavier image data. """ def __init__(self, *args, **kwargs): # Array of TrackerFrame objects io.BytesIO.__init__(self, *args, **kwargs) self.frames = [] self.last = None self.tracking = False def start_tracking(self): if not self.tracking: logging.info("Started tracking frame data") self.tracking = True def stop_tracking(self): if self.tracking: logging.info("Stopped tracking frame data") self.tracking = False def reset_tracking(self): self.frames = [] def write(self, s): if self.tracking: frame = TrackerFrame(size=len(s), time=time.time()) self.frames.append(frame) self.last = frame io.BytesIO.write(self, s) class BaseCamera(metaclass=ABCMeta): """ Loading @@ -18,6 +64,7 @@ class BaseCamera(metaclass=ABCMeta): self.lock = StrictLock(name="Camera", timeout=None) self.stream = FrameStream() self.frames_iterator = None self.frame = None self.last_access = 0 Loading openflexure_microscope/camera/mock.py +0 −4 Original line number Diff line number Diff line Loading @@ -2,7 +2,6 @@ from __future__ import division import io import logging import time from datetime import datetime Loading Loading @@ -32,9 +31,6 @@ class MissingCamera(BaseCamera): self.jpeg_quality = 75 self.framerate = 10 # Create an empty stream self.stream = io.BytesIO() # Generate an initial dummy image self.generate_new_dummy_image() Loading openflexure_microscope/camera/pi.py +19 −96 Original line number Diff line number Diff line Loading @@ -26,9 +26,6 @@ Still capture (if use_video_port == False) uses pause_stream to temporarily increase the capture resolution. """ from __future__ import division import io import logging import time Loading Loading @@ -111,7 +108,6 @@ class PiCameraStreamer(BaseCamera): ) #: str: Path of .npy lens shading table file # Start the stream worker on init self.stream = io.BytesIO() # Create a stream object self.start_worker() @property Loading Loading @@ -403,21 +399,18 @@ class PiCameraStreamer(BaseCamera): # Update state self.record_active = False def stop_stream_recording( self, splitter_port: int = 1, resolution: Tuple[int, int] = None ) -> None: def stop_stream_recording(self, splitter_port: int = 1, **kwargs) -> 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 resolution ((int, int)): Resolution to set the camera to, after stopping recording. """ for k in kwargs.keys(): logging.warning( "Warning, kwarg %s is invalid for stop_stream_recording.", k ) with self.lock: # If no resolution is specified, default to image_resolution if not resolution: resolution = self.image_resolution # Stop the camera video recording on port 1 try: self.camera.stop_recording(splitter_port=splitter_port) Loading @@ -426,7 +419,7 @@ class PiCameraStreamer(BaseCamera): else: logging.info( "Stopped MJPEG stream on port {1}. Switching to {0}.".format( resolution, splitter_port self.image_resolution, splitter_port ) ) Loading @@ -434,30 +427,20 @@ class PiCameraStreamer(BaseCamera): time.sleep( 0.2 ) # Sprinkled a sleep to prevent camera getting confused by rapid commands self.camera.resolution = resolution self.camera.resolution = self.image_resolution def start_stream_recording( self, splitter_port: int = 1, resolution: Tuple[int, int] = None ) -> None: def start_stream_recording(self, splitter_port: int = 1, **kwargs) -> 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 resolution ((int, int)): Resolution to set the camera to, before starting recording. Defaults to `self.stream_resolution`. """ for k in kwargs.keys(): logging.warning( "Warning, kwarg %s is invalid for stop_stream_recording.", k ) with self.lock(timeout=None): # If stream object was destroyed if not hasattr(self, "stream"): self.stream = io.BytesIO() # Create a stream object # If no explicit resolution is passed if not resolution: resolution = ( self.stream_resolution ) # Default to video recording resolution # Reduce the resolution for video streaming try: self.camera._check_recording_stopped() # pylint: disable=W0212 Loading @@ -466,7 +449,9 @@ class PiCameraStreamer(BaseCamera): "Error while changing resolution: Recording already running." ) else: self.camera.resolution = resolution self.camera.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 if self.stream_active: Loading @@ -488,7 +473,7 @@ class PiCameraStreamer(BaseCamera): else: logging.debug( "Started MJPEG stream at {} on port {}".format( resolution, splitter_port self.stream_resolution, splitter_port ) ) Loading Loading @@ -522,7 +507,6 @@ class PiCameraStreamer(BaseCamera): # Set resolution and stop stream recording if necessary if not use_video_port: self.stop_stream_recording() time.sleep(0.1) self.camera.capture( output, Loading @@ -532,7 +516,6 @@ class PiCameraStreamer(BaseCamera): bayer=(not use_video_port) and bayer, use_video_port=use_video_port, ) # time.sleep(0.1) # Set resolution and start stream recording if necessary if not use_video_port: Loading @@ -540,49 +523,7 @@ class PiCameraStreamer(BaseCamera): return output def yuv( self, use_video_port: bool = True, resize: Tuple[int, int] = None ) -> np.ndarray: """Capture an uncompressed still YUV image to a Numpy array. Args: use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. resize ((int, int)): Resize the captured image. Returns: output_array (np.ndarray): Output array of capture """ with self.lock: if use_video_port: resolution = self.stream_resolution else: resolution = self.numpy_resolution if resize: size = resize else: size = resolution if not use_video_port: self.stop_stream_recording(resolution=resolution) logging.debug("Creating PiYUVArray") with picamerax.array.PiYUVArray(self.camera, size=size) as output: logging.info("Capturing to {}".format(output)) self.camera.capture( output, resize=size, format="yuv", use_video_port=use_video_port ) if not use_video_port: self.start_stream_recording() return output.array def array( self, use_video_port: bool = True, resize: Tuple[int, int] = None ) -> np.ndarray: def array(self) -> np.ndarray: """Capture an uncompressed still RGB image to a Numpy array. Args: Loading @@ -593,30 +534,12 @@ class PiCameraStreamer(BaseCamera): output_array (np.ndarray): Output array of capture """ with self.lock: if use_video_port: resolution = self.stream_resolution else: resolution = self.numpy_resolution if resize: size = resize else: size = resolution # Always pause stream, to prevent resizer memory issues self.stop_stream_recording(resolution=resolution) logging.debug("Creating PiRGBArray") with picamerax.array.PiRGBArray(self.camera, size=size) as output: with picamerax.array.PiRGBArray(self.camera) as output: logging.info("Capturing to {}".format(output)) self.camera.capture( output, resize=size, format="rgb", use_video_port=use_video_port ) # Resume stream self.start_stream_recording() self.camera.capture(output, format="rgb", use_video_port=True) return output.array Loading Loading
openflexure_microscope/api/default_extensions/autofocus.py +6 −59 Original line number Diff line number Diff line import logging import time from collections import namedtuple from contextlib import contextmanager import numpy as np Loading @@ -14,40 +13,6 @@ from openflexure_microscope.utilities import set_properties ### Autofocus utilities # Class to store a frames metadata TrackerFrame = namedtuple("TrackerFrame", ["size", "time"]) class FrameTracker(object): """ A file-like object used to analyse MJPEG frames. Instead of analysing a load of real MJPEG frames after they've been stored in a BytesIO stream, we tell the camera to write frames to this class instead. We then do analysis as the frames are written, and discard the heavier image data. """ def __init__(self): # Array of TrackerFrame objects self.frames = [] self.last = None def write(self, s): # Store the incoming frame metadata # Discards the actual frame image data # NOTE: This could be used for smarter processing # E.g. different sharpness metrics frame = TrackerFrame(size=len(s), time=time.time()) self.frames.append(frame) self.last = frame def flush(self): # Called at end of recording pass class JPEGSharpnessMonitor: def __init__(self, microscope): Loading @@ -62,36 +27,17 @@ class JPEGSharpnessMonitor: self.jpeg_times = [] self.jpeg_sizes = [] # Splitter port 3 is reserved for autofocus self.splitter_port = 3 self.stream = FrameTracker() def stop_recording(self): logging.info("Stopping recording for autofocus...") self.camera.camera.stop_recording(splitter_port=self.splitter_port) logging.info("Stopped recording for autofocus!") def start(self): # Log the recording start time self.recording_start_time = time.time() # Start the stream recording self.camera.camera.start_recording( self.stream, format="mjpeg", quality=100, # Use best quality (we don't store the frames anyway!) bitrate=-1, # RWB: disable bitrate control # (bitrate control makes JPEG size less good as a focus # metric) splitter_port=self.splitter_port, ) logging.info("Started recording for autofocus") def stop(self): self.stop_recording() logging.info("Stopped recording for autofocus") self.camera.stream.stop_tracking() self.camera.stream.reset_tracking() def focus_rel(self, dz, backlash=False, **kwargs): # Store the start time and position self.camera.stream.start_tracking() self.stage_times.append(time.time()) self.stage_positions.append(self.stage.position) Loading @@ -99,11 +45,12 @@ class JPEGSharpnessMonitor: self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs) # Store the end time and position self.camera.stream.stop_tracking() self.stage_times.append(time.time()) self.stage_positions.append(self.stage.position) # Retrieve frame data for frame in self.stream.frames: for frame in self.camera.stream.frames: # Make timestamp absolute Unix time self.jpeg_times.append(frame.time) self.jpeg_sizes.append(frame.size) Loading Loading @@ -329,7 +276,7 @@ def fast_up_down_up_autofocus( # We've deliberately undershot - figure out how much further we should move based on the curve logging.debug("Calculate remining movement") current_js = m.stream.last.size current_js = m.camera.stream.last.size imax = np.argmax(js) # we want to crop out just the bit below the peak js = js[imax:] # NB z is in DECREASING order jz = jz[imax:] Loading
openflexure_microscope/camera/base.py +47 −0 Original line number Diff line number Diff line Loading @@ -2,10 +2,56 @@ import logging import threading import time import io from abc import ABCMeta, abstractmethod from collections import namedtuple from labthings import ClientEvent, StrictLock # Class to store a frames metadata TrackerFrame = namedtuple("TrackerFrame", ["size", "time"]) class FrameStream(io.BytesIO): """ A file-like object used to analyse MJPEG frames. Instead of analysing a load of real MJPEG frames after they've been stored in a BytesIO stream, we tell the camera to write frames to this class instead. We then do analysis as the frames are written, and discard the heavier image data. """ def __init__(self, *args, **kwargs): # Array of TrackerFrame objects io.BytesIO.__init__(self, *args, **kwargs) self.frames = [] self.last = None self.tracking = False def start_tracking(self): if not self.tracking: logging.info("Started tracking frame data") self.tracking = True def stop_tracking(self): if self.tracking: logging.info("Stopped tracking frame data") self.tracking = False def reset_tracking(self): self.frames = [] def write(self, s): if self.tracking: frame = TrackerFrame(size=len(s), time=time.time()) self.frames.append(frame) self.last = frame io.BytesIO.write(self, s) class BaseCamera(metaclass=ABCMeta): """ Loading @@ -18,6 +64,7 @@ class BaseCamera(metaclass=ABCMeta): self.lock = StrictLock(name="Camera", timeout=None) self.stream = FrameStream() self.frames_iterator = None self.frame = None self.last_access = 0 Loading
openflexure_microscope/camera/mock.py +0 −4 Original line number Diff line number Diff line Loading @@ -2,7 +2,6 @@ from __future__ import division import io import logging import time from datetime import datetime Loading Loading @@ -32,9 +31,6 @@ class MissingCamera(BaseCamera): self.jpeg_quality = 75 self.framerate = 10 # Create an empty stream self.stream = io.BytesIO() # Generate an initial dummy image self.generate_new_dummy_image() Loading
openflexure_microscope/camera/pi.py +19 −96 Original line number Diff line number Diff line Loading @@ -26,9 +26,6 @@ Still capture (if use_video_port == False) uses pause_stream to temporarily increase the capture resolution. """ from __future__ import division import io import logging import time Loading Loading @@ -111,7 +108,6 @@ class PiCameraStreamer(BaseCamera): ) #: str: Path of .npy lens shading table file # Start the stream worker on init self.stream = io.BytesIO() # Create a stream object self.start_worker() @property Loading Loading @@ -403,21 +399,18 @@ class PiCameraStreamer(BaseCamera): # Update state self.record_active = False def stop_stream_recording( self, splitter_port: int = 1, resolution: Tuple[int, int] = None ) -> None: def stop_stream_recording(self, splitter_port: int = 1, **kwargs) -> 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 resolution ((int, int)): Resolution to set the camera to, after stopping recording. """ for k in kwargs.keys(): logging.warning( "Warning, kwarg %s is invalid for stop_stream_recording.", k ) with self.lock: # If no resolution is specified, default to image_resolution if not resolution: resolution = self.image_resolution # Stop the camera video recording on port 1 try: self.camera.stop_recording(splitter_port=splitter_port) Loading @@ -426,7 +419,7 @@ class PiCameraStreamer(BaseCamera): else: logging.info( "Stopped MJPEG stream on port {1}. Switching to {0}.".format( resolution, splitter_port self.image_resolution, splitter_port ) ) Loading @@ -434,30 +427,20 @@ class PiCameraStreamer(BaseCamera): time.sleep( 0.2 ) # Sprinkled a sleep to prevent camera getting confused by rapid commands self.camera.resolution = resolution self.camera.resolution = self.image_resolution def start_stream_recording( self, splitter_port: int = 1, resolution: Tuple[int, int] = None ) -> None: def start_stream_recording(self, splitter_port: int = 1, **kwargs) -> 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 resolution ((int, int)): Resolution to set the camera to, before starting recording. Defaults to `self.stream_resolution`. """ for k in kwargs.keys(): logging.warning( "Warning, kwarg %s is invalid for stop_stream_recording.", k ) with self.lock(timeout=None): # If stream object was destroyed if not hasattr(self, "stream"): self.stream = io.BytesIO() # Create a stream object # If no explicit resolution is passed if not resolution: resolution = ( self.stream_resolution ) # Default to video recording resolution # Reduce the resolution for video streaming try: self.camera._check_recording_stopped() # pylint: disable=W0212 Loading @@ -466,7 +449,9 @@ class PiCameraStreamer(BaseCamera): "Error while changing resolution: Recording already running." ) else: self.camera.resolution = resolution self.camera.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 if self.stream_active: Loading @@ -488,7 +473,7 @@ class PiCameraStreamer(BaseCamera): else: logging.debug( "Started MJPEG stream at {} on port {}".format( resolution, splitter_port self.stream_resolution, splitter_port ) ) Loading Loading @@ -522,7 +507,6 @@ class PiCameraStreamer(BaseCamera): # Set resolution and stop stream recording if necessary if not use_video_port: self.stop_stream_recording() time.sleep(0.1) self.camera.capture( output, Loading @@ -532,7 +516,6 @@ class PiCameraStreamer(BaseCamera): bayer=(not use_video_port) and bayer, use_video_port=use_video_port, ) # time.sleep(0.1) # Set resolution and start stream recording if necessary if not use_video_port: Loading @@ -540,49 +523,7 @@ class PiCameraStreamer(BaseCamera): return output def yuv( self, use_video_port: bool = True, resize: Tuple[int, int] = None ) -> np.ndarray: """Capture an uncompressed still YUV image to a Numpy array. Args: use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. resize ((int, int)): Resize the captured image. Returns: output_array (np.ndarray): Output array of capture """ with self.lock: if use_video_port: resolution = self.stream_resolution else: resolution = self.numpy_resolution if resize: size = resize else: size = resolution if not use_video_port: self.stop_stream_recording(resolution=resolution) logging.debug("Creating PiYUVArray") with picamerax.array.PiYUVArray(self.camera, size=size) as output: logging.info("Capturing to {}".format(output)) self.camera.capture( output, resize=size, format="yuv", use_video_port=use_video_port ) if not use_video_port: self.start_stream_recording() return output.array def array( self, use_video_port: bool = True, resize: Tuple[int, int] = None ) -> np.ndarray: def array(self) -> np.ndarray: """Capture an uncompressed still RGB image to a Numpy array. Args: Loading @@ -593,30 +534,12 @@ class PiCameraStreamer(BaseCamera): output_array (np.ndarray): Output array of capture """ with self.lock: if use_video_port: resolution = self.stream_resolution else: resolution = self.numpy_resolution if resize: size = resize else: size = resolution # Always pause stream, to prevent resizer memory issues self.stop_stream_recording(resolution=resolution) logging.debug("Creating PiRGBArray") with picamerax.array.PiRGBArray(self.camera, size=size) as output: with picamerax.array.PiRGBArray(self.camera) as output: logging.info("Capturing to {}".format(output)) self.camera.capture( output, resize=size, format="rgb", use_video_port=use_video_port ) # Resume stream self.start_stream_recording() self.camera.capture(output, format="rgb", use_video_port=True) return output.array Loading