Fast AF use normal stream
This commit is contained in:
parent
0b40974138
commit
574f4a615d
4 changed files with 72 additions and 159 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from collections import namedtuple
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -14,40 +13,6 @@ from openflexure_microscope.utilities import set_properties
|
||||||
|
|
||||||
### Autofocus utilities
|
### 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:
|
class JPEGSharpnessMonitor:
|
||||||
def __init__(self, microscope):
|
def __init__(self, microscope):
|
||||||
|
|
@ -62,36 +27,17 @@ class JPEGSharpnessMonitor:
|
||||||
self.jpeg_times = []
|
self.jpeg_times = []
|
||||||
self.jpeg_sizes = []
|
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):
|
def start(self):
|
||||||
# Log the recording start time
|
# Log the recording start time
|
||||||
self.recording_start_time = time.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):
|
def stop(self):
|
||||||
self.stop_recording()
|
self.camera.stream.stop_tracking()
|
||||||
logging.info("Stopped recording for autofocus")
|
self.camera.stream.reset_tracking()
|
||||||
|
|
||||||
def focus_rel(self, dz, backlash=False, **kwargs):
|
def focus_rel(self, dz, backlash=False, **kwargs):
|
||||||
# Store the start time and position
|
# Store the start time and position
|
||||||
|
self.camera.stream.start_tracking()
|
||||||
self.stage_times.append(time.time())
|
self.stage_times.append(time.time())
|
||||||
self.stage_positions.append(self.stage.position)
|
self.stage_positions.append(self.stage.position)
|
||||||
|
|
||||||
|
|
@ -99,11 +45,12 @@ class JPEGSharpnessMonitor:
|
||||||
self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs)
|
self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs)
|
||||||
|
|
||||||
# Store the end time and position
|
# Store the end time and position
|
||||||
|
self.camera.stream.stop_tracking()
|
||||||
self.stage_times.append(time.time())
|
self.stage_times.append(time.time())
|
||||||
self.stage_positions.append(self.stage.position)
|
self.stage_positions.append(self.stage.position)
|
||||||
|
|
||||||
# Retrieve frame data
|
# Retrieve frame data
|
||||||
for frame in self.stream.frames:
|
for frame in self.camera.stream.frames:
|
||||||
# Make timestamp absolute Unix time
|
# Make timestamp absolute Unix time
|
||||||
self.jpeg_times.append(frame.time)
|
self.jpeg_times.append(frame.time)
|
||||||
self.jpeg_sizes.append(frame.size)
|
self.jpeg_sizes.append(frame.size)
|
||||||
|
|
@ -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
|
# We've deliberately undershot - figure out how much further we should move based on the curve
|
||||||
logging.debug("Calculate remining movement")
|
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
|
imax = np.argmax(js) # we want to crop out just the bit below the peak
|
||||||
js = js[imax:] # NB z is in DECREASING order
|
js = js[imax:] # NB z is in DECREASING order
|
||||||
jz = jz[imax:]
|
jz = jz[imax:]
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,56 @@
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import io
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
from labthings import ClientEvent, StrictLock
|
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):
|
class BaseCamera(metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
|
|
@ -18,6 +64,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
self.lock = StrictLock(name="Camera", timeout=None)
|
self.lock = StrictLock(name="Camera", timeout=None)
|
||||||
|
|
||||||
|
self.stream = FrameStream()
|
||||||
self.frames_iterator = None
|
self.frames_iterator = None
|
||||||
self.frame = None
|
self.frame = None
|
||||||
self.last_access = 0
|
self.last_access = 0
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -32,9 +31,6 @@ class MissingCamera(BaseCamera):
|
||||||
self.jpeg_quality = 75
|
self.jpeg_quality = 75
|
||||||
self.framerate = 10
|
self.framerate = 10
|
||||||
|
|
||||||
# Create an empty stream
|
|
||||||
self.stream = io.BytesIO()
|
|
||||||
|
|
||||||
# Generate an initial dummy image
|
# Generate an initial dummy image
|
||||||
self.generate_new_dummy_image()
|
self.generate_new_dummy_image()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ Still capture (if use_video_port == False) uses pause_stream
|
||||||
to temporarily increase the capture resolution.
|
to temporarily increase the capture resolution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import division
|
|
||||||
|
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -111,7 +108,6 @@ class PiCameraStreamer(BaseCamera):
|
||||||
) #: str: Path of .npy lens shading table file
|
) #: str: Path of .npy lens shading table file
|
||||||
|
|
||||||
# Start the stream worker on init
|
# Start the stream worker on init
|
||||||
self.stream = io.BytesIO() # Create a stream object
|
|
||||||
self.start_worker()
|
self.start_worker()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -403,21 +399,18 @@ class PiCameraStreamer(BaseCamera):
|
||||||
# Update state
|
# Update state
|
||||||
self.record_active = False
|
self.record_active = False
|
||||||
|
|
||||||
def stop_stream_recording(
|
def stop_stream_recording(self, splitter_port: int = 1, **kwargs) -> None:
|
||||||
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
|
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
splitter_port (int): Splitter port to stop recording on
|
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:
|
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
|
# Stop the camera video recording on port 1
|
||||||
try:
|
try:
|
||||||
self.camera.stop_recording(splitter_port=splitter_port)
|
self.camera.stop_recording(splitter_port=splitter_port)
|
||||||
|
|
@ -426,7 +419,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
else:
|
else:
|
||||||
logging.info(
|
logging.info(
|
||||||
"Stopped MJPEG stream on port {1}. Switching to {0}.".format(
|
"Stopped MJPEG stream on port {1}. Switching to {0}.".format(
|
||||||
resolution, splitter_port
|
self.image_resolution, splitter_port
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -434,30 +427,20 @@ class PiCameraStreamer(BaseCamera):
|
||||||
time.sleep(
|
time.sleep(
|
||||||
0.2
|
0.2
|
||||||
) # Sprinkled a sleep to prevent camera getting confused by rapid commands
|
) # 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(
|
def start_stream_recording(self, splitter_port: int = 1, **kwargs) -> None:
|
||||||
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
|
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
splitter_port (int): Splitter port to start recording on
|
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):
|
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
|
# Reduce the resolution for video streaming
|
||||||
try:
|
try:
|
||||||
self.camera._check_recording_stopped() # pylint: disable=W0212
|
self.camera._check_recording_stopped() # pylint: disable=W0212
|
||||||
|
|
@ -466,7 +449,9 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"Error while changing resolution: Recording already running."
|
"Error while changing resolution: Recording already running."
|
||||||
)
|
)
|
||||||
else:
|
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 the stream should be active
|
||||||
if self.stream_active:
|
if self.stream_active:
|
||||||
|
|
@ -488,7 +473,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
else:
|
else:
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"Started MJPEG stream at {} on port {}".format(
|
"Started MJPEG stream at {} on port {}".format(
|
||||||
resolution, splitter_port
|
self.stream_resolution, splitter_port
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -522,7 +507,6 @@ class PiCameraStreamer(BaseCamera):
|
||||||
# Set resolution and stop stream recording if necessary
|
# Set resolution and stop stream recording if necessary
|
||||||
if not use_video_port:
|
if not use_video_port:
|
||||||
self.stop_stream_recording()
|
self.stop_stream_recording()
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
self.camera.capture(
|
self.camera.capture(
|
||||||
output,
|
output,
|
||||||
|
|
@ -532,7 +516,6 @@ class PiCameraStreamer(BaseCamera):
|
||||||
bayer=(not use_video_port) and bayer,
|
bayer=(not use_video_port) and bayer,
|
||||||
use_video_port=use_video_port,
|
use_video_port=use_video_port,
|
||||||
)
|
)
|
||||||
# time.sleep(0.1)
|
|
||||||
|
|
||||||
# Set resolution and start stream recording if necessary
|
# Set resolution and start stream recording if necessary
|
||||||
if not use_video_port:
|
if not use_video_port:
|
||||||
|
|
@ -540,49 +523,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def yuv(
|
def array(self) -> np.ndarray:
|
||||||
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:
|
|
||||||
"""Capture an uncompressed still RGB image to a Numpy array.
|
"""Capture an uncompressed still RGB image to a Numpy array.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -593,30 +534,12 @@ class PiCameraStreamer(BaseCamera):
|
||||||
output_array (np.ndarray): Output array of capture
|
output_array (np.ndarray): Output array of capture
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
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")
|
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))
|
logging.info("Capturing to {}".format(output))
|
||||||
|
|
||||||
self.camera.capture(
|
self.camera.capture(output, format="rgb", use_video_port=True)
|
||||||
output, resize=size, format="rgb", use_video_port=use_video_port
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resume stream
|
|
||||||
self.start_stream_recording()
|
|
||||||
|
|
||||||
return output.array
|
return output.array
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue