Merge branch 'master' into 'hotfix-2.0.x'
# Conflicts: # openflexure_microscope/api/app.py # poetry.lock # pyproject.toml
This commit is contained in:
commit
6f4791ec57
25 changed files with 11162 additions and 151 deletions
|
|
@ -13,3 +13,5 @@ except Exception as e:
|
|||
logging.error(
|
||||
f"Exception loading builtin extension picamera_autocalibrate: \n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
from ..example_extensions.custom_element import customelement_extension_v2
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ from openflexure_microscope.utilities import set_properties
|
|||
|
||||
import time
|
||||
import numpy as np
|
||||
import threading
|
||||
|
||||
import logging
|
||||
from scipy import ndimage
|
||||
from contextlib import contextmanager
|
||||
|
||||
from threading import Thread, Event
|
||||
|
||||
### Autofocus utilities
|
||||
|
||||
|
||||
|
|
@ -25,7 +27,7 @@ class JPEGSharpnessMonitor:
|
|||
self.jpeg_times = []
|
||||
self.stage_positions = []
|
||||
self.stage_times = []
|
||||
self.stop_event = threading.Event()
|
||||
self.stop_event = Event()
|
||||
self.timeout = timeout
|
||||
self.keep_alive()
|
||||
self.background_thread = None
|
||||
|
|
@ -48,26 +50,31 @@ class JPEGSharpnessMonitor:
|
|||
|
||||
def start(self):
|
||||
"Start monitoring sharpness by looking at JPEG size"
|
||||
self.background_thread = threading.Thread(target=self._measure_jpegs)
|
||||
self.background_thread = Thread(target=self._measure_jpegs)
|
||||
self.background_thread.start()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
"Stop the background thread"
|
||||
self.stop_event.set()
|
||||
print("Joining JPEG thread")
|
||||
self.background_thread.join()
|
||||
|
||||
def _measure_jpegs(self):
|
||||
"Function that runs in a background thread to record sharpness"
|
||||
logging.info("Starting sharpness measurement in background thread")
|
||||
logging.debug("Starting sharpness measurement in background thread")
|
||||
self.keep_alive()
|
||||
logging.debug(f"_measure_jpegs stop_event: {self.stop_event.is_set()}")
|
||||
while not self.stop_event.is_set() and not self.should_stop():
|
||||
self.jpeg_sizes.append(self.jpeg_size())
|
||||
self.jpeg_times.append(time.time())
|
||||
size_now = self.jpeg_size()
|
||||
time_now = time.time()
|
||||
self.jpeg_sizes.append(size_now)
|
||||
self.jpeg_times.append(time_now)
|
||||
print("Exited JPEG measure loop")
|
||||
if self.stop_event.is_set():
|
||||
logging.info("Cleanly stopped sharpness measurement in background thread")
|
||||
logging.debug("Cleanly stopped sharpness measurement in background thread")
|
||||
if self.should_stop():
|
||||
logging.info("Sharpness measurement timed out and has stopped")
|
||||
logging.debug("Sharpness measurement timed out and has stopped")
|
||||
|
||||
def jpeg_size(self):
|
||||
"""Return the size of a frame from the MJPEG stream"""
|
||||
|
|
@ -195,7 +202,7 @@ def move_and_find_focus(microscope, dz):
|
|||
|
||||
def fast_autofocus(microscope, dz=2000, backlash=None):
|
||||
"""Perform a down-up-down-up autofocus"""
|
||||
with monitor_sharpness(microscope) as m, microscope.camera.lock:
|
||||
with monitor_sharpness(microscope) as m, microscope.camera.lock, microscope.stage.lock:
|
||||
i, z = m.focus_rel(-dz / 2)
|
||||
i, z = m.focus_rel(dz)
|
||||
fz = m.sharpest_z_on_move(i)
|
||||
|
|
@ -245,17 +252,20 @@ def fast_up_down_up_autofocus(
|
|||
might slightly hurt accuracy, but is unlikely to be a big issue. Too big
|
||||
may cause you to overshoot, which is a problem.
|
||||
"""
|
||||
with monitor_sharpness(microscope) as m, microscope.camera.lock:
|
||||
with monitor_sharpness(microscope) as m, microscope.camera.lock, microscope.stage.lock:
|
||||
# Ensure the MJPEG stream has started
|
||||
microscope.camera.start_stream_recording()
|
||||
|
||||
df = dz # TODO: refactor so I actually use dz in the code below!
|
||||
logging.debug("Initial move")
|
||||
if initial_move_up:
|
||||
m.focus_rel(df / 2)
|
||||
# move down
|
||||
logging.debug("Move down")
|
||||
i, z = m.focus_rel(-df)
|
||||
# now inspect where the sharpest point is, and estimate the sharpness
|
||||
# (JPEG size) that we should find at the start of the Z stack
|
||||
logging.debug("Get target_s")
|
||||
jt, jz, js = m.move_data(i)
|
||||
best_z = jz[np.argmax(js)]
|
||||
target_s = np.interp(
|
||||
|
|
@ -263,11 +273,13 @@ def fast_up_down_up_autofocus(
|
|||
) # NB jz is decreasing
|
||||
|
||||
# now move to the start of the z stack
|
||||
logging.debug("Move to the start of the z stack")
|
||||
i, z = m.focus_rel(
|
||||
best_z + target_z - z + mini_backlash
|
||||
) # takes us to the start of the stack
|
||||
|
||||
# We've deliberately undershot - figure out how much further we should move based on the curve
|
||||
logging.debug("Calculate remining movement")
|
||||
current_js = m.jpeg_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
|
||||
|
|
@ -279,6 +291,7 @@ def fast_up_down_up_autofocus(
|
|||
|
||||
# So, the Z position corresponding to our current sharpness value is zs[inow]
|
||||
# That means we should move forwards, by best_z - zs[inow]
|
||||
logging.debug("Correction move")
|
||||
correction_move = best_z + target_z - jz[inow]
|
||||
logging.debug(
|
||||
"Fast autofocus scan: correcting backlash by moving {} steps".format(
|
||||
|
|
@ -317,7 +330,7 @@ class AutofocusAPI(View):
|
|||
dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
|
||||
|
||||
if microscope.has_real_stage():
|
||||
logging.info("Running autofocus...")
|
||||
logging.debug("Running autofocus...")
|
||||
task = taskify(autofocus)(microscope, dz)
|
||||
|
||||
# return a handle on the autofocus task
|
||||
|
|
@ -348,7 +361,7 @@ class FastAutofocusAPI(View):
|
|||
backlash = 0
|
||||
|
||||
if microscope.has_real_stage():
|
||||
logging.info("Running autofocus...")
|
||||
logging.debug("Running autofocus...")
|
||||
task = taskify(fast_up_down_up_autofocus)(microscope, dz=dz, mini_backlash=backlash)
|
||||
|
||||
# return a handle on the autofocus task
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ def dynamic_form():
|
|||
return {
|
||||
"icon": "sd_storage",
|
||||
"title": "Storage",
|
||||
"viewPanel": "gallery",
|
||||
"forms": [
|
||||
{
|
||||
"name": "Autostorage",
|
||||
|
|
|
|||
|
|
@ -7,10 +7,35 @@ from labthings.core.tasks import taskify
|
|||
|
||||
from flask import abort
|
||||
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
|
||||
from .recalibrate_utils import recalibrate_camera, auto_expose_and_freeze_settings
|
||||
# Type hinting
|
||||
from typing import Tuple
|
||||
|
||||
from .recalibrate_utils import recalibrate_camera, auto_expose_and_freeze_settings, flat_lens_shading_table
|
||||
|
||||
@contextmanager
|
||||
def pause_stream(scamera, resolution: Tuple[int, int] = None):
|
||||
"""This context manager locks a streaming camera, and pauses the stream.
|
||||
|
||||
The stream is re-enabled, with the original resolution, once the with
|
||||
block has finished.
|
||||
"""
|
||||
with scamera.lock:
|
||||
assert not scamera.record_active, "We can't pause the camera's video stream while a recording is in progress."
|
||||
streaming = scamera.stream_active
|
||||
old_resolution = scamera.camera.resolution
|
||||
if streaming:
|
||||
logging.info("Stopping stream in pause_stream context manager")
|
||||
scamera.stop_stream_recording(resolution=resolution)
|
||||
try:
|
||||
yield scamera
|
||||
finally:
|
||||
scamera.camera.resolution = old_resolution
|
||||
if streaming:
|
||||
logging.info("Restarting stream in pause_stream context manager")
|
||||
scamera.start_stream_recording()
|
||||
|
||||
def recalibrate(microscope):
|
||||
"""Reset the camera's settings.
|
||||
|
|
@ -19,24 +44,10 @@ def recalibrate(microscope):
|
|||
table such that the background is as uniform as possible
|
||||
with a gray level of 230. It takes a little while to run.
|
||||
"""
|
||||
scamera = microscope.camera
|
||||
with scamera.lock:
|
||||
assert not scamera.record_active, "Can't recalibrate while recording!"
|
||||
streaming = scamera.stream_active
|
||||
if streaming:
|
||||
logging.info("Stopping stream before recalibration")
|
||||
scamera.stop_stream_recording(resolution=(640, 480))
|
||||
old_resolution = scamera.camera.resolution
|
||||
try:
|
||||
scamera.camera.resolution = (640, 480)
|
||||
auto_expose_and_freeze_settings(scamera.camera)
|
||||
recalibrate_camera(scamera.camera)
|
||||
finally:
|
||||
scamera.camera.resolution = old_resolution
|
||||
microscope.save_settings()
|
||||
if streaming:
|
||||
logging.info("Restarting stream after recalibration")
|
||||
scamera.start_stream_recording()
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
auto_expose_and_freeze_settings(scamera.camera) # scamera.camera is the PiCamera object
|
||||
recalibrate_camera(scamera.camera)
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
@ThingAction
|
||||
|
|
@ -52,9 +63,42 @@ class RecalibrateView(View):
|
|||
|
||||
return taskify(recalibrate)(microscope)
|
||||
|
||||
@ThingAction
|
||||
class FlattenLSTView(View):
|
||||
def post(self):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to flatten the lens shading table.")
|
||||
|
||||
try:
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
flat_lst = flat_lens_shading_table(scamera.camera)
|
||||
scamera.camera.lens_shading_table = flat_lst
|
||||
microscope.save_settings()
|
||||
except:
|
||||
logging.exception("Error flattening the lens shading table.")
|
||||
abort(503, "Couldn't flatten the lens shading table - do you have the forked PiCamera library installed?")
|
||||
|
||||
@ThingAction
|
||||
class DeleteLSTView(View):
|
||||
def post(self):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to flatten the lens shading table.")
|
||||
|
||||
try:
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
scamera.camera.lens_shading_table = None
|
||||
microscope.save_settings()
|
||||
except:
|
||||
logging.exception("Error deleting the lens shading table.")
|
||||
abort(503, "Couldn't flatten the lens shading table - do you have the forked PiCamera library installed?")
|
||||
|
||||
|
||||
lst_extension_v2 = BaseExtension(
|
||||
"org.openflexure.calibration.picamera", version="2.0.0-beta.1"
|
||||
"org.openflexure.calibration.picamera", version="2.0.0-beta.1", description="Routines to perform flat-field correction on the camera."
|
||||
)
|
||||
|
||||
lst_extension_v2.add_method(
|
||||
|
|
@ -62,3 +106,5 @@ lst_extension_v2.add_method(
|
|||
)
|
||||
|
||||
lst_extension_v2.add_view(RecalibrateView, "/recalibrate")
|
||||
lst_extension_v2.add_view(FlattenLSTView, "/flatten_lens_shading_table")
|
||||
lst_extension_v2.add_view(DeleteLSTView, "/delete_lens_shading_table")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue