diff --git a/openflexure_microscope/plugins/__init__.py b/openflexure_microscope/plugins/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/openflexure_microscope/plugins/default/__init__.py b/openflexure_microscope/plugins/default/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/openflexure_microscope/plugins/default/autofocus/__init__.py b/openflexure_microscope/plugins/default/autofocus/__init__.py deleted file mode 100644 index c13cd9f7..00000000 --- a/openflexure_microscope/plugins/default/autofocus/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__all__ = ["AutofocusPlugin"] -from .plugin import AutofocusPlugin diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py deleted file mode 100644 index e04bd529..00000000 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ /dev/null @@ -1,64 +0,0 @@ -import numpy as np -import logging - -from openflexure_microscope.devel import ( - MicroscopeViewPlugin, - JsonResponse, - request, - jsonify, - taskify, - abort, -) - - -class MeasureSharpnessAPI(MicroscopeViewPlugin): - def post(self): - payload = JsonResponse(request) - return jsonify({"sharpness": self.plugin.measure_sharpness()}) - - -class AutofocusAPI(MicroscopeViewPlugin): - """ - Run a standard autofocus - """ - - def post(self): - payload = JsonResponse(request) - - # Figure out the range of z values to use - dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array) - - if self.microscope.has_real_stage(): - logging.info("Running autofocus...") - task = taskify(self.plugin.autofocus)(dz) - - # return a handle on the autofocus task - return jsonify(task.state), 201 - - else: - abort(503, "No stage connected. Unable to autofocus.") - - -class FastAutofocusAPI(MicroscopeViewPlugin): - """ - Run a fast autofocus - """ - - def post(self): - payload = JsonResponse(request) - - # Figure out the parameters to use - dz = payload.param("dz", default=2000, convert=int) - backlash = payload.param("backlash", default=0, convert=int) - if backlash < 0: - backlash = 0 - - if self.microscope.has_real_stage(): - logging.info("Running autofocus...") - task = taskify(self.plugin.fast_autofocus)(dz, backlash=backlash) - - # return a handle on the autofocus task - return jsonify(task.state), 201 - - else: - abort(503, "No stage connected. Unable to autofocus.") diff --git a/openflexure_microscope/plugins/default/autofocus/focus_utils.py b/openflexure_microscope/plugins/default/autofocus/focus_utils.py deleted file mode 100644 index 232a2b85..00000000 --- a/openflexure_microscope/plugins/default/autofocus/focus_utils.py +++ /dev/null @@ -1,129 +0,0 @@ -import time -import numpy as np -import threading -import logging -from scipy import ndimage - - -class JPEGSharpnessMonitor: - def __init__(self, microscope, timeout=60): - self.microscope = microscope - self.camera = microscope.camera - self.stage = microscope.stage - self.jpeg_sizes = [] - self.jpeg_times = [] - self.stage_positions = [] - self.stage_times = [] - self.stop_event = threading.Event() - self.timeout = timeout - self.keep_alive() - self.background_thread = None - - def is_alive(self): - if self.background_thread is None: - return False - else: - return self.background_thread.is_alive() - - def should_stop(self): - import time - - return time.time() - self.kept_alive > self.timeout - - def keep_alive(self): - import time - - self.kept_alive = time.time() - - def start(self): - "Start monitoring sharpness by looking at JPEG size" - self.background_thread = threading.Thread(target=self._measure_jpegs) - self.background_thread.start() - return self - - def stop(self): - "Stop the background thread" - self.stop_event.set() - 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") - self.keep_alive() - 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()) - if self.stop_event.is_set(): - logging.info("Cleanly stopped sharpness measurement in background thread") - if self.should_stop(): - logging.info("Sharpness measurement timed out and has stopped") - - def jpeg_size(self): - """Return the size of a frame from the MJPEG stream""" - return len(self.camera.get_frame()) - - def focus_rel(self, dz, backlash=False, **kwargs): - self.keep_alive() - self.stage_times.append(time.time()) - self.stage_positions.append(self.stage.position) - self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs) - self.stage_times.append(time.time()) - self.stage_positions.append(self.stage.position) - i = len(self.stage_positions) - 2 - return i, self.stage_positions[-1][2] - - def move_data(self, istart, istop=None): - "Extract sharpness as a function of (interpolated) z" - global np, logging - if istop is None: - istop = istart + 2 - jpeg_times = np.array(self.jpeg_times) - jpeg_sizes = np.array(self.jpeg_sizes) - stage_times = np.array(self.stage_times)[istart:istop] - stage_zs = np.array(self.stage_positions)[istart:istop, 2] - start = np.argmax(jpeg_times > stage_times[0]) - stop = np.argmax(jpeg_times > stage_times[1]) - if stop < 1: - stop = len(jpeg_times) - logging.debug("changing stop to {}".format(stop)) - jpeg_times = jpeg_times[start:stop] - jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs) - return jpeg_times, jpeg_zs, jpeg_sizes[start:stop] - - def sharpest_z_on_move(self, index): - """Return the z position of the sharpest image on a given move""" - jt, jz, js = self.move_data(index) - return jz[np.argmax(js)] - - def data_dict(self): - """Return the gathered data as a single convenient dictionary""" - data = {} - for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]: - data[k] = getattr(self, k) - return data - - -def decimate_to(shape, image): - """Decimate an image to reduce its size if it's too big.""" - decimation = np.max( - np.ceil(np.array(image.shape, dtype=np.float)[: len(shape)] / np.array(shape)) - ) - return image[:: int(decimation), :: int(decimation), ...] - - -def sharpness_sum_lap2(rgb_image): - """Return an image sharpness metric: sum(laplacian(image)**")""" - # image_bw=np.mean(decimate_to((1000,1000), rgb_image),2) - image_bw = np.mean(rgb_image, 2) - image_lap = ndimage.filters.laplace(image_bw) - return np.mean(image_lap.astype(np.float) ** 4) - - -def sharpness_edge(image): - """Return a sharpness metric optimised for vertical lines""" - gray = np.mean(image.astype(float), 2) - n = 20 - edge = np.array([[-1] * n + [1] * n]) - return np.sum( - [np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]] - ) diff --git a/openflexure_microscope/plugins/default/autofocus/plugin.py b/openflexure_microscope/plugins/default/autofocus/plugin.py deleted file mode 100644 index 2cba84bb..00000000 --- a/openflexure_microscope/plugins/default/autofocus/plugin.py +++ /dev/null @@ -1,168 +0,0 @@ -import time -import logging -import numpy as np -from contextlib import contextmanager - -from openflexure_microscope.utilities import set_properties - -from .focus_utils import sharpness_sum_lap2, JPEGSharpnessMonitor -from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI - -from openflexure_microscope.devel import MicroscopePlugin - - -class AutofocusPlugin(MicroscopePlugin): - """ - Basic autofocus plugin - """ - - api_views = { - "/measure_sharpness": MeasureSharpnessAPI, - "/autofocus": AutofocusAPI, - "/fast_autofocus": FastAutofocusAPI, - } - - ### SLOW AUTOFOCUS - - def autofocus(self, dz, settle=0.5, metric_fn=sharpness_sum_lap2): - """Perform a simple autofocus routine. - The stage is moved to z positions (relative to current position) in dz, - and at each position an image is captured and the sharpness function - evaulated. We then move back to the position where the sharpness was - highest. No interpolation is performed. - dz is assumed to be in ascending order (starting at -ve values) - """ - camera = self.microscope.camera - stage = self.microscope.stage - - with set_properties(stage, backlash=256), stage.lock, camera.lock: - sharpnesses = [] - positions = [] - camera.annotate_text = "" - - for _ in stage.scan_z(dz, return_to_start=False): - positions.append(stage.position[2]) - time.sleep(settle) - sharpnesses.append(self.measure_sharpness(metric_fn)) - - newposition = positions[np.argmax(sharpnesses)] - stage.move_rel([0, 0, newposition - stage.position[2]]) - - return positions, sharpnesses - - def measure_sharpness(self, metric_fn=sharpness_sum_lap2): - """Measure the sharpness of the camera's current view.""" - return metric_fn(self.microscope.camera.array(use_video_port=True)) - - ### FAST AUTOFOCUS - - @contextmanager - def monitor_sharpness(self): - m = JPEGSharpnessMonitor(self.microscope) - m.start() - try: - yield m - finally: - m.stop() - - def move_and_find_focus(self, dz): - """Make a relative Z move and return the peak sharpness position""" - with self.monitor_sharpness() as m: - m.focus_rel(dz) - return m.sharpest_z_on_move(0) - - def fast_autofocus(self, dz=2000, backlash=None): - """Perform a down-up-down-up autofocus""" - with self.monitor_sharpness() as m: - i, z = m.focus_rel(-dz / 2) - i, z = m.focus_rel(dz) - fz = m.sharpest_z_on_move(i) - if backlash is None: - i, z = m.focus_rel( - -dz - ) # move all the way to the start so it's consistent - else: - i, z = m.focus_rel(fz - z - backlash) - m.focus_rel(fz - z) - return m.data_dict() - - def fast_up_down_up_autofocus( - self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150 - ): - """Autofocus by measuring on the way down, and moving back up with feedback. - - This autofocus method is very efficient, as it only passes the peak once. - The sequence of moves it performs is: - - 1. Move to the top of the range `dz/2` (can be disabled) - - 2. Move down by `dz` while monitoring JPEG size to find the focus. - - 3. Move back up to the `target_z` position, relative to the sharpest image. - - 4. Measure the sharpness, and compare against the curve recorded in (2) to \\ - estimate how much further we need to go. Make this move, to reach our \\ - target position. - - Moving back to the target position in two steps allows us to correct for - backlash, by using the sharpness-vs-z curve as a rough encoder for Z. - - Parameters: - dz: number of steps over which to scan (optional, default 2000) - - target_z: we aim to finish at this position, relative to focus. This may - be useful if, for example, you want to acquire a stack of images in Z. - It is optional, and the default value of 0 will finish at the focus. - - initial_move_up: (optional, default True) set this to `False` to move down - from the starting position. Mostly useful if you're able to combine - the initial move with something else, e.g. moving to the next scan point. - - mini_backlash: (optional, default 50) is a small extra move made in step - 3 to help counteract backlash. It should be small enough that you - would always expect there to be greater backlash than this. Too small - might slightly hurt accuracy, but is unlikely to be a big issue. Too big - may cause you to overshoot, which is a problem. - """ - with self.monitor_sharpness() as m, self.microscope.camera.lock: - # Ensure the MJPEG stream has started - self.microscope.camera.start_stream_recording() - - df = dz # TODO: refactor so I actually use dz in the code below! - if initial_move_up: - m.focus_rel(df / 2) - # 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 - jt, jz, js = m.move_data(i) - best_z = jz[np.argmax(js)] - target_s = np.interp( - [best_z + target_z], jz[::-1], js[::-1] - ) # NB jz is decreasing - - # now 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 - 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 - jz = jz[imax:] - inow = np.argmax( - js < current_js - ) # use the curve we recorded to estimate our position - # TODO: fancy interpolation stuff - - # So, the Z position corresponding to our current sharpness value is zs[inow] - # That means we should move forwards, by best_z - zs[inow] - correction_move = best_z + target_z - jz[inow] - logging.debug( - "Fast autofocus scan: correcting backlash by moving {} steps".format( - correction_move - ) - ) - m.focus_rel(correction_move) - return m.data_dict() diff --git a/openflexure_microscope/plugins/default/camera_calibration/__init__.py b/openflexure_microscope/plugins/default/camera_calibration/__init__.py deleted file mode 100644 index 46032671..00000000 --- a/openflexure_microscope/plugins/default/camera_calibration/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__all__ = ["Plugin"] -from .plugin import AutocalibrationPlugin diff --git a/openflexure_microscope/plugins/default/camera_calibration/plugin.py b/openflexure_microscope/plugins/default/camera_calibration/plugin.py deleted file mode 100644 index b682be02..00000000 --- a/openflexure_microscope/plugins/default/camera_calibration/plugin.py +++ /dev/null @@ -1,57 +0,0 @@ -from openflexure_microscope.devel import ( - MicroscopePlugin, - MicroscopeViewPlugin, - JsonResponse, - request, - jsonify, - taskify, -) - -import logging - -from .recalibrate_utils import recalibrate_camera, auto_expose_and_freeze_settings - - -class RecalibrateAPIView(MicroscopeViewPlugin): - def post(self): - logging.info("Starting microscope recalibration...") - task = taskify(self.plugin.recalibrate)() - - # Return a handle on the autofocus task - return jsonify(task.state), 201 - - -class AutocalibrationPlugin(MicroscopePlugin): - """ - Auto-calibration plugin - """ - - api_views = {"/recalibrate": RecalibrateAPIView} - - def recalibrate(self): - """Reset the camera's settings. - - This generates new gains, exposure time, and lens shading - table such that the background is as uniform as possible - with a gray level of 230. It takes a little while to run. - """ - scamera = self.microscope.camera - with scamera.lock: - assert not scamera.status[ - "record_active" - ], "Can't recalibrate while recording!" - streaming = scamera.status["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 - self.microscope.save_settings() - if streaming: - logging.info("Restarting stream after recalibration") - scamera.start_stream_recording() diff --git a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py b/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py deleted file mode 100644 index 5dae166d..00000000 --- a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py +++ /dev/null @@ -1,191 +0,0 @@ -import numpy as np -import time - -from picamera import PiCamera -from picamera.array import PiRGBArray, PiBayerArray - - -def rgb_image(camera, resize=None, **kwargs): - """Capture an image and return an RGB numpy array""" - with PiRGBArray(camera, size=resize) as output: - camera.capture(output, format="rgb", resize=resize, **kwargs) - return output.array - - -def flat_lens_shading_table(camera): - """Return a flat (i.e. unity gain) lens shading table. - - This is mostly useful because it makes it easy to get the size - of the array correct. NB if you are not using the forked picamera - library (with lens shading table support) it will raise an error. - """ - if not hasattr(PiCamera, "lens_shading_table"): - raise ImportError( - "This program requires the forked picamera library with lens shading support" - ) - return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32 - - -def adjust_exposure_to_setpoint(camera, setpoint): - """Adjust the camera's exposure time until the maximum pixel value is .""" - print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="") - for i in range(3): - print(".", end="") - camera.shutter_speed = int( - camera.shutter_speed * setpoint / np.max(rgb_image(camera)) - ) - time.sleep(1) - print("done") - - -def auto_expose_and_freeze_settings(camera): - """Freeze the settings after auto-exposing to white illumination""" - print("Allowing the camera to auto-expose") - camera.awb_mode = "auto" - camera.exposure_mode = "auto" - camera.iso = ( - 0 - ) # This is important, if it's on a fixed ISO, gain might not set properly. - for i in range(6): - print(".", end="") - time.sleep(0.5) - print("done") - - print("Freezing the camera settings...") - camera.shutter_speed = camera.exposure_speed - print("Shutter speed = {}".format(camera.shutter_speed)) - camera.exposure_mode = "off" - print("Auto exposure disabled") - g = camera.awb_gains - camera.awb_mode = "off" - camera.awb_gains = g - print("Auto white balance disabled, gains are {}".format(g)) - print( - "Analogue gain: {}, Digital gain: {}".format( - camera.analog_gain, camera.digital_gain - ) - ) - adjust_exposure_to_setpoint(camera, 215) - - -def channels_from_bayer_array(bayer_array): - """Given the 'array' from a PiBayerArray, return the 4 channels.""" - bayer_pattern = [(i // 2, i % 2) for i in range(4)] - channels = np.zeros( - (4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2), - dtype=bayer_array.dtype, - ) - for i, offset in enumerate(bayer_pattern): - # We simplify life by dealing with only one channel at a time. - channels[i, :, :] = np.sum( - bayer_array[offset[0] :: 2, offset[1] :: 2, :], axis=2 - ) - - return channels - - -def lst_from_channels(channels): - """Given the 4 Bayer colour channels from a white image, generate a LST.""" - full_resolution = np.array(channels.shape[1:]) * 2 # channels have been binned - # lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int)) - lst_resolution = [(r // 64) + 1 for r in full_resolution] - # NB the size of the LST is 1/64th of the image, but rounded UP. - print("Generating a lens shading table at {}x{}".format(*lst_resolution)) - lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float) - for i in range(lens_shading.shape[0]): - image_channel = channels[i, :, :] - iw, ih = image_channel.shape - ls_channel = lens_shading[i, :, :] - lw, lh = ls_channel.shape - # The lens shading table is rounded **up** in size to 1/64th of the size of - # the image. Rather than handle edge images separately, I'm just going to - # pad the image by copying edge pixels, so that it is exactly 32 times the - # size of the lens shading table (NB 32 not 64 because each channel is only - # half the size of the full image - remember the Bayer pattern... This - # should give results very close to 6by9's solution, albeit considerably - # less computationally efficient! - padded_image_channel = np.pad( - image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge" - ) # Pad image to the right and bottom - print( - "Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format( - iw, ih, lw * 32, lh * 32, padded_image_channel.shape - ) - ) - # Next, fill the shading table (except edge pixels). Please excuse the - # for loop - I know it's not fast but this code needn't be! - box = 3 # We average together a square of this side length for each pixel. - # NB this isn't quite what 6by9's program does - it averages 3 pixels - # horizontally, but not vertically. - for dx in np.arange(box) - box // 2: - for dy in np.arange(box) - box // 2: - ls_channel[:, :] += ( - padded_image_channel[16 + dx :: 32, 16 + dy :: 32] - 64 - ) - ls_channel /= box ** 2 - # The original C code written by 6by9 normalises to the central 64 pixels in each channel. - # ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4]) - # I have had better results just normalising to the maximum: - ls_channel /= np.max(ls_channel) - # NB the central pixel should now be *approximately* 1.0 (may not be exactly - # due to different averaging widths between the normalisation & shading table) - # For most sensible lenses I'd expect that 1.0 is the maximum value. - # NB ls_channel should be a "view" of the whole lens shading array, so we don't - # need to update the big array here. - - # What we actually want to calculate is the gains needed to compensate for the - # lens shading - that's 1/lens_shading_table_float as we currently have it. - gains = 32.0 / lens_shading # 32 is unity gain - gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32 - gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) - lens_shading_table = gains.astype(np.uint8) - return lens_shading_table[::-1, :, :].copy() - - -def recalibrate_camera(camera): - """Reset the lens shading table and exposure settings. - - This method first resets to a flat lens shading table, then auto-exposes, - then generates a new lens shading table to make the current view uniform. - It should be run when the camera is looking at a uniform white scene. - - NB the only parameter ``camera`` is a ``PiCamera`` instance and **not** a - ``StreamingCamera``. - """ - camera.lens_shading_table = flat_lens_shading_table(camera) - _ = rgb_image(camera) # for some reason the camera won't work unless I do this! - - with PiBayerArray(camera) as a: - camera.capture(a, format="jpeg", bayer=True) - raw_image = a.array.copy() - - # Now we need to calculate a lens shading table that would make this flat. - # raw_image is a 3D array, with full resolution and 3 colour channels. No - # de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B - # channels, 1/2 for green because there's twice as many green pixels). - channels = channels_from_bayer_array(raw_image) - lens_shading_table = lst_from_channels(channels) - - camera.lens_shading_table = lens_shading_table - _ = rgb_image(camera) - - # Fix the AWB gains so the image is neutral - channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0) - old_gains = camera.awb_gains - camera.awb_gains = ( - channel_means[1] / channel_means[0] * old_gains[0], - channel_means[1] / channel_means[2] * old_gains[1], - ) - time.sleep(1) - # Ensure the background is bright but not saturated - adjust_exposure_to_setpoint(camera, 230) - - -if __name__ == "__main__": - with PiCamera() as camera: - camera.start_preview() - time.sleep(3) - print("Recalibrating...") - recalibrate_camera(camera) - print("Done.") - time.sleep(2) diff --git a/openflexure_microscope/plugins/default/scan/__init__.py b/openflexure_microscope/plugins/default/scan/__init__.py deleted file mode 100644 index ef2437f5..00000000 --- a/openflexure_microscope/plugins/default/scan/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__all__ = ["ScanPlugin"] -from .plugin import ScanPlugin diff --git a/openflexure_microscope/plugins/default/scan/api.py b/openflexure_microscope/plugins/default/scan/api.py deleted file mode 100644 index 13a9a8f2..00000000 --- a/openflexure_microscope/plugins/default/scan/api.py +++ /dev/null @@ -1,63 +0,0 @@ -from openflexure_microscope.devel import ( - MicroscopeViewPlugin, - JsonResponse, - request, - jsonify, - abort, - taskify, -) - -import logging - - -class TileScanAPI(MicroscopeViewPlugin): - def post(self): - payload = JsonResponse(request) - - # Get params - filename = payload.param("filename") - temporary = payload.param("temporary", default=False, convert=bool) - - step_size = payload.param("step_size", default=[2000, 1500, 100], convert=list) - step_size = [int(i) for i in step_size] - - grid = payload.param("grid", default=[3, 3, 5], convert=list) - grid = [int(i) for i in grid] - - style = payload.param("style", default="raster", convert=str) - autofocus_dz = payload.param("autofocus_dz", default=50, convert=int) - fast_autofocus = payload.param("fast_autofocus", default=False, convert=bool) - - use_video_port = payload.param("use_video_port", default=True, convert=bool) - resize = payload.param("size", default=None) - if resize: - if ("width" in resize) and ("height" in resize): - resize = ( - int(resize["width"]), - int(resize["height"]), - ) # Convert dict to tuple - else: - abort(404) - - bayer = payload.param("bayer", default=False, convert=bool) - metadata = payload.param("metadata", default={}, convert=dict) - tags = payload.param("tags", default=[], convert=list) - - logging.info("Running tile scan...") - task = taskify(self.plugin.tile)( - basename=filename, - temporary=temporary, - step_size=step_size, - grid=grid, - style=style, - autofocus_dz=autofocus_dz, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - fast_autofocus=fast_autofocus, - metadata=metadata, - tags=tags, - ) - - # return a handle on the scan task - return jsonify(task.state), 201 diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py deleted file mode 100644 index 0863ff51..00000000 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ /dev/null @@ -1,328 +0,0 @@ -import time -from typing import Tuple -from functools import reduce -import uuid -import itertools -import logging - -from openflexure_microscope.camera.base import generate_basename - -from openflexure_microscope.devel import ( - MicroscopePlugin, - update_task_progress, - update_task_data, -) - -from .api import TileScanAPI - - -def construct_grid(initial, step_sizes, n_steps, style="raster"): - """ - Given an initial position, step sizes, and number of steps, - construct a 2-dimensional list of scan x-y positions. - """ - arr = [] - - for i in range(n_steps[0]): # x axis - arr.append([]) - for j in range(n_steps[1]): # y axis - # Create a coordinate array - coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)] - # Append coordinate array to position grid - arr[i].append(tuple(coord)) - - # Style modifiers - if style == "snake": - for i, line in enumerate(arr): - if i % 2 != 0: - line.reverse() - - return arr - - -def flatten_grid(grid): - """ - Convert a 3D list of scan positions into a flat list - of sequential positions. - """ - - grid = list(itertools.chain(*grid)) - return grid - - -class ScanPlugin(MicroscopePlugin): - """ - Stack and tile plugin - """ - - api_views = {"/tile": TileScanAPI} - - def __init__(self): - MicroscopePlugin.__init__(self) - - self.images_to_be_captured: int = 1 - update_task_data({"images_to_be_captured": self.images_to_be_captured}) - - @property - def progress(self): - progress = (self.images_captured_so_far / self.images_to_be_captured) * 100 - logging.info(progress) - return progress - - def capture( - self, - basename, - scan_id, - temporary: bool = False, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - metadata: dict = {}, - tags: list = [], - ): - - # Construct a tile filename - filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position) - folder = "SCAN_{}".format(basename) - - # Create output object - output = self.microscope.camera.new_image( - temporary=temporary, filename=filename, folder=folder - ) - - # Capture - self.microscope.camera.capture( - output.file, use_video_port=use_video_port, resize=resize, bayer=bayer - ) - - # Affix metadata - if "scan" not in tags: - tags.append("scan") - - # Inject system metadata - output.put_metadata(self.microscope.metadata, system=True) - - # Insert custom metadata - output.put_metadata(metadata) - - # Insert custom tags - output.put_tags(tags) - - def tile( - self, - basename: str = None, - temporary: bool = False, - step_size: int = [2000, 1500, 100], - grid: list = [3, 3, 5], - style="raster", - autofocus_dz: int = 50, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - fast_autofocus=False, - metadata: dict = {}, - tags: list = [], - ): - - # Keep task progress - # TODO: Make this line not nasty - self.images_to_be_captured = reduce((lambda x, y: x * y), grid) - self.images_captured_so_far = 0 - - # Generate a basename if none given - if not basename: - basename = generate_basename() - - # Generate a stack ID - scan_id = uuid.uuid4() - - # Store initial position - initial_position = self.microscope.stage.position - - # Add scan metadata - if "time" not in metadata: - metadata["time"] = generate_basename() - - metadata.update( - { - "scan_id": scan_id, - "basename": basename, - "scan_parameters": { - "step_size": step_size, - "grid": grid, - "style": style, - "autofocus_dz": autofocus_dz, - }, - } - ) - - # Check if autofocus is enabled - if ( - autofocus_dz - and hasattr(self.microscope.plugins, "default_autofocus") - and self.microscope.has_real_stage() - and self.microscope.has_real_camera() - ): - autofocus_enabled = True - else: - autofocus_enabled = False - - if fast_autofocus and not hasattr( - self.microscope.plugins.default_autofocus, "monitor_sharpness" - ): - logging.warning( - "Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?" - ) - fast_autofocus = False - z_stack_dz = ( - grid[2] * step_size[2] if grid[2] > 1 else 0 - ) # shorthand for Z stack range - - # Construct an x-y grid (worry about z later) - x_y_grid = construct_grid( - initial_position, step_size[:2], grid[:2], style=style - ) - - # Keep the initial Z position the same as our current position - next_z = initial_position[2] - if fast_autofocus: # If fast autofocus is enabled, make - next_z += autofocus_dz / 2 # sure we start from the top of the range - initial_z = next_z # Save this value for use in raster scans - - # Now step through each point in the x-y coordinate array - for line in x_y_grid: - # If rastering, rather than snake (or eventually spiral) - # Return focus to initial position - if style == "raster": - next_z = initial_z - logging.debug("Returning to initial z position") - self.microscope.stage.move_abs( - [line[0][0], line[0][1], next_z] - ) # RWB: I think this line is redundant - - for x_y in line: - # Move to new grid position without changing z - logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z])) - self.microscope.stage.move_abs([x_y[0], x_y[1], next_z]) - # Refocus - if autofocus_enabled: - if fast_autofocus: - self.microscope.plugins.default_autofocus.fast_up_down_up_autofocus( - dz=autofocus_dz, - target_z=-z_stack_dz / 2.0, # Finish below the focus - initial_move_up=False, # We're already at the top of the scan - ) - # TODO: save the focus data for future reference? Use it for diagnostics? - else: - logging.debug("Running autofocus") - self.microscope.plugins.default_autofocus.autofocus( - range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz) - ) - logging.debug("Finished autofocus") - time.sleep(1) # TODO: Remove - - # If we're not doing a z-stack, just capture - if grid[2] <= 1: - self.capture( - basename, - scan_id, - temporary=temporary, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - metadata=metadata, - tags=tags, - ) - # Update task progress - self.images_captured_so_far += 1 - update_task_progress(self.progress) - else: - logging.debug("Entering z-stack") - self.stack( - basename=basename, - temporary=temporary, - scan_id=scan_id, - step_size=step_size[2], - steps=grid[2], - center=not fast_autofocus, # fast_autofocus does this for us! - return_to_start=not fast_autofocus, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - metadata=metadata, - tags=tags, - ) - # Make sure we use our current best estimate of focus (i.e. the current position) next point - next_z = self.microscope.stage.position[2] - if fast_autofocus: - next_z += ( - autofocus_dz / 2 - ) # Fast autofocus requires us to start at the top of the range - if grid[2] > 1: - next_z -= int( - grid[2] / 2.0 * step_size[2] - ) # Z stacking means we're higher up to start with - - logging.debug("Returning to {}".format(initial_position)) - self.microscope.stage.move_abs(initial_position) - - def stack( - self, - basename: str = None, - temporary: bool = False, - scan_id: str = None, - step_size: int = 100, - steps: int = 5, - center: bool = True, - return_to_start: bool = True, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - metadata: dict = {}, - tags: list = [], - ): - - # Generate a basename if none given - if not basename: - basename = generate_basename() - - # Generate a stack ID - if not scan_id: - scan_id = uuid.uuid4() - - # Add scan metadata - if not "time" in metadata: - metadata["time"] = generate_basename() - - # Store initial position - initial_position = self.microscope.stage.position - - with self.microscope.lock: - # Move to center scan - if center: - logging.debug("Moving to starting position") - self.microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)]) - - for i in range(steps): - time.sleep(0.1) - logging.debug("Capturing...") - self.capture( - basename, - scan_id, - temporary=temporary, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - metadata=metadata, - tags=tags, - ) - # Update task progress - self.images_captured_so_far += 1 - update_task_progress(self.progress) - - if i != steps - 1: - logging.debug("Moving z by {}".format(step_size)) - self.microscope.stage.move_rel([0, 0, step_size]) - if return_to_start: - logging.debug("Returning to {}".format(initial_position)) - self.microscope.stage.move_abs(initial_position) diff --git a/openflexure_microscope/plugins/default/zip_builder/__init__.py b/openflexure_microscope/plugins/default/zip_builder/__init__.py deleted file mode 100644 index 56ec492f..00000000 --- a/openflexure_microscope/plugins/default/zip_builder/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .plugin import ZipBuilderPlugin diff --git a/openflexure_microscope/plugins/default/zip_builder/plugin.py b/openflexure_microscope/plugins/default/zip_builder/plugin.py deleted file mode 100644 index 545f6a1f..00000000 --- a/openflexure_microscope/plugins/default/zip_builder/plugin.py +++ /dev/null @@ -1,138 +0,0 @@ -from openflexure_microscope.devel import ( - MicroscopePlugin, - MicroscopeViewPlugin, - JsonResponse, - request, - jsonify, - taskify, - update_task_progress, -) - -from flask import send_file, abort - -import uuid -import os -import zipfile -import tempfile -import logging - - -class ZipBuilderAPIView(MicroscopeViewPlugin): - def post(self): - - ids = list(JsonResponse(request).json) - - task = taskify(self.plugin.build_zip_from_capture_ids)(ids) - - # Return a handle on the autofocus task - return jsonify(task.state), 201 - - -class ZipListAPIView(MicroscopeViewPlugin): - def get(self): - return jsonify(self.plugin.session_zips) - - -class ZipGetterAPIView(MicroscopeViewPlugin): - def get(self, session_id): - if not session_id in self.plugin.session_zips: - return abort(404) # 404 Not Found - - logging.info(f"Session ID: {session_id}") - - return send_file( - self.plugin.zip_from_id(session_id).name, - mimetype="application/zip", - as_attachment=True, - attachment_filename=f"{session_id}.zip", - ) - - def delete(self, session_id): - if not session_id in self.plugin.session_zips: - return abort(404) # 404 Not Found - - logging.info(f"Session ID: {session_id}") - - fp = self.plugin.zip_from_id(session_id) - logging.debug(fp.name) - fp.close() - os.unlink(fp.name) - - assert not os.path.exists(fp.name) - - del self.plugin.session_zips[session_id] - - return jsonify({"return": session_id}) - - -class ZipBuilderPlugin(MicroscopePlugin): - """ - ZIP-builder plugin - """ - - def __init__(self): - super().__init__() - - self.session_zips = {} - - self.add_view("/get/", ZipGetterAPIView) - self.add_view("/get", ZipListAPIView) - - self.add_view("/build", ZipBuilderAPIView) - - def build_zip_from_capture_ids(self, capture_id_list): - logging.debug(capture_id_list) - - # Get array of captures from IDs - capture_list = [ - self.microscope.camera.image_from_id(capture_id) - for capture_id in capture_id_list - ] - # Remove Nones from list (missing/invalid captures) - capture_list = [capture for capture in capture_list if capture] - - # Get size (in bytes) of each capture - capture_sizes = [ - os.path.getsize(capture_obj.file) for capture_obj in capture_list - ] - # Calculate size of input data in megabytes - data_size_megabytes = sum(capture_sizes) * 1e-6 - - # If more than 1GB - if data_size_megabytes > 1000: - # Throw exception - raise Exception( - "Zip data cannot exceed 1GB. Please transfer data manually." - ) - - # Number of files to add (used for task progress) - n_files = len(capture_id_list) - - # Create temporary file - fp = tempfile.NamedTemporaryFile(delete=False) - - # Open temp file as a ZIP file - with zipfile.ZipFile(fp, "w") as zipObj: - for index, capture_obj in enumerate(capture_list): - # Add to ZIP file if it exists - file_path = capture_obj.file - rel_path = os.path.relpath( - file_path, self.microscope.camera.paths["default"] - ) - zipObj.write(file_path, arcname=rel_path) - # Update task progress - update_task_progress(int((index / n_files) * 100)) - - session_id = uuid.uuid4() - # self.session_zips[session_id] = fp - self.session_zips[session_id] = { - "id": session_id, - "fp": fp, - "data_size": data_size_megabytes, - "zip_size": os.path.getsize(fp.name) * 1e-6, - } - - return self.session_zips[session_id] - - def zip_from_id(self, session_id): - return self.session_zips[session_id]["fp"] diff --git a/openflexure_microscope/plugins/testing/__init__.py b/openflexure_microscope/plugins/testing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/openflexure_microscope/plugins/testing/dynamic_form_example/__init__.py b/openflexure_microscope/plugins/testing/dynamic_form_example/__init__.py deleted file mode 100644 index 571c9f38..00000000 --- a/openflexure_microscope/plugins/testing/dynamic_form_example/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .plugin import DynamicExamplePlugin -from . import api diff --git a/openflexure_microscope/plugins/testing/dynamic_form_example/api.py b/openflexure_microscope/plugins/testing/dynamic_form_example/api.py deleted file mode 100644 index 7a46ca94..00000000 --- a/openflexure_microscope/plugins/testing/dynamic_form_example/api.py +++ /dev/null @@ -1,24 +0,0 @@ -from openflexure_microscope.devel import ( - MicroscopeViewPlugin, - JsonResponse, - request, - jsonify, - taskify, -) - -import logging - - -class DoAPI(MicroscopeViewPlugin): - """ - A dynamic example API plugin - """ - - def get(self): - values = {"val_int": self.plugin.val_int} - return jsonify(values) - - def post(self): - self.plugin.val_int += 1 - - return jsonify({"response": "completed"}) diff --git a/openflexure_microscope/plugins/testing/dynamic_form_example/plugin.py b/openflexure_microscope/plugins/testing/dynamic_form_example/plugin.py deleted file mode 100644 index b25afb84..00000000 --- a/openflexure_microscope/plugins/testing/dynamic_form_example/plugin.py +++ /dev/null @@ -1,52 +0,0 @@ -import random -import time -import os -import json -from openflexure_microscope.devel import MicroscopePlugin, update_task_progress - -from .api import DoAPI - - -class DynamicExamplePlugin(MicroscopePlugin): - """ - An example plugin using a comprehensive form - """ - - api_views = {"/do": DoAPI} - - def __init__(self): - MicroscopePlugin.__init__(self) - - self.val_int = 0 - self.val_str = "Hello" - - self.set_gui(self.dynamic_form) - - def dynamic_form(self): - return { - "id": "test-plugin", - "icon": "pets", - "forms": [ - { - "name": "Simple request", - "isCollapsible": False, - "isTask": False, - "selfUpdate": True, - "route": "/do", - "submitLabel": "Do things", - "schema": [ - { - "fieldType": "numberInput", - "name": "val_int", - "label": "Number value", - "minValue": 0, - }, - { - "fieldType": "htmlBlock", - "name": "html_block", - "content": f"Value is:
{self.val_int}.", - }, - ], - } - ], - } diff --git a/openflexure_microscope/plugins/testing/form_example/__init__.py b/openflexure_microscope/plugins/testing/form_example/__init__.py deleted file mode 100644 index 97549f5d..00000000 --- a/openflexure_microscope/plugins/testing/form_example/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .plugin import ExamplePlugin -from . import api diff --git a/openflexure_microscope/plugins/testing/form_example/api.py b/openflexure_microscope/plugins/testing/form_example/api.py deleted file mode 100644 index d4fc40f6..00000000 --- a/openflexure_microscope/plugins/testing/form_example/api.py +++ /dev/null @@ -1,60 +0,0 @@ -from openflexure_microscope.devel import ( - MicroscopeViewPlugin, - JsonResponse, - request, - jsonify, - taskify, -) - -import logging - - -class DoAPI(MicroscopeViewPlugin): - """ - A simple example API plugin - """ - - def get(self): - return jsonify(self.plugin.get_values_dict()) - - def post(self): - # Get payload JSON - payload = JsonResponse(request) - - # Extract a values from the JSON payload. - val_int = payload.param("val_int", default=None, convert=int) - val_str = payload.param("val_str", default=None, convert=str) - val_radio = payload.param("val_radio", default=None) - val_check = payload.param("val_check", default=None) - val_select = payload.param("val_select", default=None) - val_disposable = payload.param("val_disposable", default=None) - - self.plugin.set_values( - val_int, val_str, val_radio, val_check, val_select, val_disposable - ) - - print(self.plugin.get_values_dict()) - - return jsonify({"response": "completed"}) - - -class TaskAPI(MicroscopeViewPlugin): - """ - A task example API plugin - """ - - def get(self): - return jsonify({"run_time": self.plugin.run_time}) - - def post(self): - # Get payload JSON - payload = JsonResponse(request) - - # Extract a values from the JSON payload. - val_int = payload.param("run_time", default=5, convert=int) - - logging.info("Running task...") - task = taskify(self.plugin.generate_random_numbers_for_a_while)(val_int) - - # return a handle on the autofocus task - return jsonify(task.state), 201 diff --git a/openflexure_microscope/plugins/testing/form_example/forms.json b/openflexure_microscope/plugins/testing/form_example/forms.json deleted file mode 100644 index 6bc16dc0..00000000 --- a/openflexure_microscope/plugins/testing/form_example/forms.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "id": "test-plugin", - "icon": "pets", - "forms": [{ - "name": "Simple request", - "isCollapsible": false, - "isTask": false, - "selfUpdate": true, - "route": "/do", - "submitLabel": "Do things", - "schema": [ - [{ - "fieldType": "numberInput", - "name": "val_int", - "label": "Number value", - "minValue": 0 - }, - { - "fieldType": "textInput", - "placeholder": "Some string", - "label": "String value", - "name": "val_str" - } - ], - [{ - "fieldType": "radioList", - "name": "val_radio", - "label": "Radio value", - "options": ["First", "Second", "Third"] - }, - { - "fieldType": "checkList", - "name": "val_check", - "label": "Checklist values", - "options": ["Foo", "Bar", "Baz"] - } - ], - { - "fieldType": "htmlBlock", - "name": "html_block", - "content": "This is a block of HTML in a plugin!
I can do paragraph breaks and stuff." - }, - { - "fieldType": "selectList", - "name": "val_select", - "multi": false, - "label": "Some selection", - "options": ["Most", "Average", "Least"] - }, - - { - "fieldType": "textInput", - "label": "Non-persistent string", - "default": "A default value", - "name": "val_disposable" - } - ] - }, - { - "name": "Task form", - "isTask": true, - "selfUpdate": true, - "route": "/task", - "submitLabel": "Start task", - "schema": [{ - "fieldType": "numberInput", - "name": "run_time", - "label": "Run time (seconds)", - "minValue": 1 - }] - } - ] -} \ No newline at end of file diff --git a/openflexure_microscope/plugins/testing/form_example/plugin.py b/openflexure_microscope/plugins/testing/form_example/plugin.py deleted file mode 100644 index aabf6880..00000000 --- a/openflexure_microscope/plugins/testing/form_example/plugin.py +++ /dev/null @@ -1,80 +0,0 @@ -import random -import time -import os -import json -from openflexure_microscope.devel import MicroscopePlugin, update_task_progress - -from .api import DoAPI, TaskAPI - -HERE = os.path.dirname(os.path.realpath(__file__)) -FORM_PATH = os.path.join(HERE, "forms.json") - - -class ExamplePlugin(MicroscopePlugin): - """ - An example plugin using a comprehensive form - """ - - global FORM_PATH - - with open(FORM_PATH, "r") as sc: - api_form = json.load(sc) - - api_views = {"/do": DoAPI, "/task": TaskAPI} - - def __init__(self): - MicroscopePlugin.__init__(self) - - self.val_int = 10 - self.val_str = "Hello" - self.val_radio = "First" - self.val_check = ["Foo", "Bar"] - self.val_select = "Most" - self.val_unused = "I'm an unused string, here to confuse the form parsing" - - self.run_time = 5 - - def set_values( - self, val_int, val_str, val_radio, val_check, val_select, val_disposable - ): - """ - Demonstrate a plugin with form - """ - if val_int: - self.val_int = int(val_int) - if val_str: - self.val_str = str(val_str) - if val_radio: - self.val_radio = val_radio - if val_check is not None: - print(val_check) - self.val_check = val_check if (type(val_check) is list) else [val_check] - if val_select: - self.val_select = val_select - - if val_disposable: - print("DISPOSABLE VALUE: {}".format(val_disposable)) - - def get_values_dict(self): - return { - "val_int": self.val_int, - "val_str": self.val_str, - "val_radio": self.val_radio, - "val_check": self.val_check, - "val_select": self.val_select, - "val_unused": self.val_unused, - } - - def generate_random_numbers_for_a_while(self, run_time: int): - self.run_time = run_time - vals = [] - - for t in range(run_time): - vals.append(random.random()) - time.sleep(1) - - # Update task progress (if running as a task) - percent_complete = int(((t + 1) / run_time) * 100) - update_task_progress(percent_complete) - - return vals