From 1f3211e3ee5c21a4861c872f173deafc5bf3f1d3 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 16 Apr 2025 09:40:14 +0100 Subject: [PATCH 01/12] Functional z stack Thing --- ofm_config_full.json | 3 +- .../things/smart_scan.py | 83 ++++++----- .../things/z_stack.py | 137 ++++++++++++++++++ 3 files changed, 182 insertions(+), 41 deletions(-) create mode 100644 src/openflexure_microscope_server/things/z_stack.py diff --git a/ofm_config_full.json b/ofm_config_full.json index a6076833..75ef4935 100644 --- a/ofm_config_full.json +++ b/ofm_config_full.json @@ -13,7 +13,8 @@ "path_to_openflexure_stitch": "application/openflexure-stitching/.venv/bin/openflexure-stitch" } }, - "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing" + "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", + "/z_stack/": "openflexure_microscope_server.things.smart_scan:ZStackThing" }, "settings_folder": "/var/openflexure/settings/" } diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 9126b31b..aae9ddef 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -34,6 +34,7 @@ from openflexure_microscope_server.utilities import ErrorCapturingThread from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.background_detect import BackgroundDetectThing +from openflexure_microscope_server.things.z_stack import ZStackThing from openflexure_microscope_server import scan_planners CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") @@ -41,7 +42,7 @@ AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/") BackgroundDep = direct_thing_client_dependency( BackgroundDetectThing, "/background_detect/" ) - +ZStackDep = direct_thing_client_dependency(ZStackThing, "/z_stack/") IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpeg$") @@ -159,6 +160,7 @@ class SmartScanThing(Thing): self._background_detect: Optional[BackgroundDep] = None self._ongoing_scan_name: Optional[str] = None self._starting_position: Optional[Mapping[str, int]] = None + self._z_stack: Optional[ZStackDep] = None self._capture_thread: Optional[ErrorCapturingThread] = None self._scan_images_taken: Optional[int] = None # TODO Scan data is a dict during refactoring, should become a dataclass @@ -175,6 +177,7 @@ class SmartScanThing(Thing): metadata_getter: GetThingStates, csm: CSMDep, background_detect: BackgroundDep, + z_stack: ZStackDep, scan_name: str = "", ): """Move the stage to cover an area, taking images that can be tiled together. @@ -198,6 +201,7 @@ class SmartScanThing(Thing): self._csm = csm self._background_detect = background_detect self._capture_thread = None + self._z_stack = z_stack self._scan_images_taken = 0 # Don't set self._scan_data dictionary. This is done at the start of _run_scan @@ -232,6 +236,7 @@ class SmartScanThing(Thing): self._ongoing_scan_name = None self._scan_images_taken = None self._scan_data = None + self._z_stack = None self._scan_lock.release() @_scan_running @@ -586,22 +591,24 @@ class SmartScanThing(Thing): ) # wait for the previous capture to be saved, i.e. don't leave more than one image saving in the background - if self._capture_thread: - self._wait_for_capture_thread() - # increment capure counter as thread has completed - self._scan_images_taken += 1 - # Add it to the incremental zip - self.create_zip_of_scan( - logger=self._scan_logger, - scan_name=self._ongoing_scan_name, - download_zip=False, - ) + # if self._capture_thread: + # self._wait_for_capture_thread() + # increment capure counter as thread has completed + self._scan_images_taken += 1 + # Add it to the incremental zip + self.create_zip_of_scan( + logger=self._scan_logger, + scan_name=self._ongoing_scan_name, + download_zip=False, + ) name = f"image_{new_pos_xyz[0]}_{new_pos_xyz[1]}.jpg" - jpeg_path = os.path.join(self._ongoing_scan_images_dir, name) - self._capture_thread, acquired = self._start_capture_thread(jpeg_path) + # jpeg_path = os.path.join(self._ongoing_scan_images_dir, name) + self._z_stack.smart_stack(images_dir = self._ongoing_scan_images_dir) + # self._capture_thread = self._start_capture_thread(jpeg_path) + # wait until the image is acquired - acquired.wait() + # acquired.wait() @_scan_running def _try_autofocus( @@ -659,31 +666,31 @@ class SmartScanThing(Thing): f"Waited {wait_time:.1f}s for the previous capture to finish saving." ) - @_scan_running - def _start_capture_thread( - self, jpeg_path: str - ) -> tuple[ErrorCapturingThread, Event]: - """ - Start the capture thread. + # @_scan_running + # def _start_capture_thread( + # self, jpeg_path: str + # ) -> tuple[ErrorCapturingThread, Event]: + # """ + # Start the capture thread. - Args: - jpeg_path, the path to save the image once aquired + # Args: + # jpeg_path, the path to save the image once aquired - Return the thread and an event that will be set when the image is aquired - """ - acquired = Event() - time.sleep(0.2) + # Return the thread and an event that will be set when the image is aquired + # """ + # time.sleep(0.2) - # Acquire the image in a thread, and continue once it's acquired - # (i.e. leave saving in the background) Use ErrorCapturingThread - # intead of Thread. This will raise errors in the calling thread - # only when join() is called, allowing us to handle this appropriately. - capture_thread = ErrorCapturingThread( - target=self._capture_and_save, - kwargs={"acquired": acquired, "jpeg_path": jpeg_path}, - ) - capture_thread.start() - return capture_thread, acquired + # # Acquire the image in a thread, and continue once it's acquired + # # (i.e. leave saving in the background) Use ErrorCapturingThread + # # intead of Thread. This will raise errors in the calling thread + # # only when join() is called, allowing us to handle this appropriately. + # capture_thread = ErrorCapturingThread( + # # target=self._capture_and_save, + # target=self._z_stack.smart_stack(), + # kwargs={"images_dir": self._ongoing_scan_images_dir}, + # ) + # capture_thread.start() + # return capture_thread @_scan_running def _return_to_starting_position(self): @@ -1241,7 +1248,3 @@ class SmartScanThing(Thing): """List the relative paths of all files and folders in the zip folder specified""" scan_zip = zipfile.ZipFile(zip_path) return [os.path.normpath(i) for i in scan_zip.namelist()] - - -class CaptureError(RuntimeError): - """An error trying to capture from Picamera""" diff --git a/src/openflexure_microscope_server/things/z_stack.py b/src/openflexure_microscope_server/things/z_stack.py new file mode 100644 index 00000000..97cd83a7 --- /dev/null +++ b/src/openflexure_microscope_server/things/z_stack.py @@ -0,0 +1,137 @@ +import os +from typing import Mapping, Optional +import cv2 +import numpy as np +from PIL import Image +from pydantic import BaseModel +from scipy.stats import norm +import time +import piexif +import json + +from labthings_fastapi.dependencies.metadata import GetThingStates +from labthings_fastapi.thing import Thing +from labthings_fastapi.decorators import thing_action, thing_property +from .camera import CameraDependency as CamDep +from .stage import StageDependency as StageDep +from labthings_fastapi.dependencies.invocation import ( + CancelHook, + InvocationLogger, + InvocationCancelledError, +) + + +class CaptureError(RuntimeError): + """An error trying to capture from Picamera""" + +class ZStackThing(Thing): + @thing_property + def images_to_capture(self) -> int: + """The number of images to capture and save in a stack + Defaults to 1 unless you need to see either side of focus""" + return self.thing_settings.get("stack_height", 9) + + @images_to_capture.setter + def images_to_capture(self, value: int) -> None: + self.thing_settings["images_to_capture"] = value + + @thing_property + def stack_dz(self) -> int: + """Space in steps between images in a z-stack + Suggested is 50 for 60-100x + 100 for 40x + 200 for 20x""" + return self.thing_settings.get("stack_dz", 50) + + @stack_dz.setter + def stack_dz(self, value: int) -> None: + self.thing_settings["stack_dz"] = value + + @thing_action + def smart_stack( + self, + cam: CamDep, + stage: StageDep, + images_dir: str, + logger: InvocationLogger, + metadata_getter: GetThingStates, + ) -> None: + stack_dz = self.stack_dz + images_to_capture = self.images_to_capture + + stack_z_range = stack_dz * (images_to_capture - 1) + stage.move_relative(z=-stack_z_range / 2) + + for capture_count in range(images_to_capture): + jpeg_path = os.path.join( + images_dir, + f"{capture_count}.jpeg", + ) + self._capture_and_save( + jpeg_path, cam, logger, metadata_getter, + ) + stage.move_relative(z=stack_dz) + + def _capture_and_save( + self, + jpeg_path: str, + cam: CamDep, + logger: InvocationLogger, + metadata_getter: GetThingStates, + ) -> None: + """Capture an image and save it to disk + + This will set the event `acquired` once the image has been acquired, so + that the stage may be moved while it's saved. + """ + capture_start = time.time() + image, metadata = self._capture_image(cam, metadata_getter,) + acquisition_time = time.time() + self._save_capture(jpeg_path, image, metadata) + save_time = time.time() + acquisition_duration = round(acquisition_time - capture_start, 1) + saving_duration = round(save_time - acquisition_time, 1) + logger.info( + f"Acquired {jpeg_path} in {acquisition_duration}s then {saving_duration}s saving to disk" + ) + + def _capture_image(self, cam, metadata_getter) -> tuple[np.ndarray, dict]: + """Capture an image in memory and return it with metadata + This will set the event `acquired` once the image has been acquired, so + that the stage may be moved while it's saved. + CaptureError raised if the capture fails for any reason + returns tuple with numpy array of image data, and dict of metadata + """ + try: + metadata = metadata_getter() + image = cam.capture_array()[..., :3] + except Exception as e: + raise CaptureError("An error occurred while capturing") from e + return image, metadata + + def _save_capture( + self, + jpeg_path: str, + image: np.ndarray, + metadata: dict, + ) -> None: + """Saving the captured image and metadata to disk + logger warning (via InvocationLogger) is raised if metadata is failed to be added + IOError is raised if the file cannot be saved + nothing is returned on success""" + try: + Image.fromarray(image.astype("uint8"), "RGB").save( + jpeg_path, quality=95, subsampling=0 + ) + try: + exif_dict = piexif.load(jpeg_path) + exif_dict["Exif"][piexif.ExifIFD.UserComment] = json.dumps( + metadata + ).encode("utf-8") + piexif.insert(piexif.dump(exif_dict), jpeg_path) + except: # noqa: E722 + # We need to capture any exception as there are many reasons metadata + # might not be added. We warn rather than log the error. + logger.warning(f"Failed to add metadata to {jpeg_path}") + except Exception as e: + raise IOError(f"An error occurred while saving {jpeg_path}") from e From 0b7a8176743b00c29ec40a818760c8d6afbea490 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 16 Apr 2025 11:17:55 +0100 Subject: [PATCH 02/12] Ruff formatting and removing all capturing from smart_scan --- .../things/smart_scan.py | 128 +----------------- .../things/z_stack.py | 9 +- 2 files changed, 4 insertions(+), 133 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index aae9ddef..d11d81c3 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -11,7 +11,6 @@ from PIL import Image from pydantic import BaseModel from datetime import datetime from subprocess import CompletedProcess, Popen, PIPE, SubprocessError, STDOUT -from threading import Event import glob import json import piexif @@ -590,9 +589,8 @@ class SmartScanThing(Thing): current_pos_xyz, imaged=True, focused=focused ) - # wait for the previous capture to be saved, i.e. don't leave more than one image saving in the background - # if self._capture_thread: - # self._wait_for_capture_thread() + self._z_stack.smart_stack(images_dir = self._ongoing_scan_images_dir) + # increment capure counter as thread has completed self._scan_images_taken += 1 # Add it to the incremental zip @@ -602,14 +600,6 @@ class SmartScanThing(Thing): download_zip=False, ) - name = f"image_{new_pos_xyz[0]}_{new_pos_xyz[1]}.jpg" - # jpeg_path = os.path.join(self._ongoing_scan_images_dir, name) - self._z_stack.smart_stack(images_dir = self._ongoing_scan_images_dir) - # self._capture_thread = self._start_capture_thread(jpeg_path) - - # wait until the image is acquired - # acquired.wait() - @_scan_running def _try_autofocus( self, @@ -646,52 +636,6 @@ class SmartScanThing(Thing): self._scan_logger.warning("Could not autofocus after 3 attempts.") return False, self._stage.position["z"] - @_scan_running - def _wait_for_capture_thread(self) -> None: - """ - Wait for the capture thread to be complete. - """ - wait_start = time.time() - thread_was_alive = self._capture_thread.is_alive() - - # If the capture thread has thrown an exception it will be raised - # when join is called, this will cause the scan to end. If we want - # to retry captures at a later date this is where we will need to - # catch the IOError or CaptureError from the thread. - self._capture_thread.join() - time.sleep(0.2) - if thread_was_alive: - wait_time = time.time() - wait_start - self._scan_logger.info( - f"Waited {wait_time:.1f}s for the previous capture to finish saving." - ) - - # @_scan_running - # def _start_capture_thread( - # self, jpeg_path: str - # ) -> tuple[ErrorCapturingThread, Event]: - # """ - # Start the capture thread. - - # Args: - # jpeg_path, the path to save the image once aquired - - # Return the thread and an event that will be set when the image is aquired - # """ - # time.sleep(0.2) - - # # Acquire the image in a thread, and continue once it's acquired - # # (i.e. leave saving in the background) Use ErrorCapturingThread - # # intead of Thread. This will raise errors in the calling thread - # # only when join() is called, allowing us to handle this appropriately. - # capture_thread = ErrorCapturingThread( - # # target=self._capture_and_save, - # target=self._z_stack.smart_stack(), - # kwargs={"images_dir": self._ongoing_scan_images_dir}, - # ) - # capture_thread.start() - # return capture_thread - @_scan_running def _return_to_starting_position(self): self._scan_logger.info("Returning to starting position.") @@ -750,74 +694,6 @@ class SmartScanThing(Thing): raise HTTPException(404, "File not found") return FileResponse(path) - @_scan_running - def _capture_and_save( - self, - acquired: Event, - jpeg_path: str, - ) -> None: - """Capture an image and save it to disk - - This will set the event `acquired` once the image has been acquired, so - that the stage may be moved while it's saved. - """ - try: - capture_start = time.time() - image, metadata = self._capture_image() - finally: - # Ensure aquired is set even if capture fails or program will hang forever. - acquired.set() - acquisition_time = time.time() - self._save_capture(jpeg_path, image, metadata) - save_time = time.time() - acquisition_duration = round(acquisition_time - capture_start, 1) - saving_duration = round(save_time - acquisition_time, 1) - self._scan_logger.debug( - f"Acquired {jpeg_path} in {acquisition_duration}s then {saving_duration}s saving to disk" - ) - - @_scan_running - def _capture_image(self) -> tuple[np.ndarray, dict]: - """Capture an image in memory and return it with metadata - This will set the event `acquired` once the image has been acquired, so - that the stage may be moved while it's saved. - CaptureError raised if the capture fails for any reason - returns tuple with numpy array of image data, and dict of metadata - """ - try: - metadata = self._metadata_getter() - image = self._cam.capture_array()[..., :3] - except Exception as e: - raise CaptureError("An error occurred while capturing") from e - return image, metadata - - @_scan_running - def _save_capture( - self, - jpeg_path: str, - image: np.ndarray, - metadata: dict, - ) -> None: - """Saving the captured image and metadata to disk - logger warning (via InvocationLogger) is raised if metadata is failed to be added - IOError is raised if the file cannot be saved - nothing is returned on success""" - try: - Image.fromarray(image.astype("uint8"), "RGB").save( - jpeg_path, quality=95, subsampling=0 - ) - try: - exif_dict = piexif.load(jpeg_path) - exif_dict["Exif"][piexif.ExifIFD.UserComment] = json.dumps( - metadata - ).encode("utf-8") - piexif.insert(piexif.dump(exif_dict), jpeg_path) - except: # noqa: E722 - # We need to capture any exception as there are many reasons metadata - # might not be added. We warn rather than log the error. - self._scan_logger.warning(f"Failed to add metadata to {jpeg_path}") - except Exception as e: - raise IOError(f"An error occurred while saving {jpeg_path}") from e @thing_property def max_range(self) -> int: diff --git a/src/openflexure_microscope_server/things/z_stack.py b/src/openflexure_microscope_server/things/z_stack.py index 97cd83a7..9b1edfe6 100644 --- a/src/openflexure_microscope_server/things/z_stack.py +++ b/src/openflexure_microscope_server/things/z_stack.py @@ -1,10 +1,6 @@ import os -from typing import Mapping, Optional -import cv2 import numpy as np from PIL import Image -from pydantic import BaseModel -from scipy.stats import norm import time import piexif import json @@ -15,9 +11,7 @@ from labthings_fastapi.decorators import thing_action, thing_property from .camera import CameraDependency as CamDep from .stage import StageDependency as StageDep from labthings_fastapi.dependencies.invocation import ( - CancelHook, InvocationLogger, - InvocationCancelledError, ) @@ -87,7 +81,7 @@ class ZStackThing(Thing): capture_start = time.time() image, metadata = self._capture_image(cam, metadata_getter,) acquisition_time = time.time() - self._save_capture(jpeg_path, image, metadata) + self._save_capture(jpeg_path, image, metadata, logger) save_time = time.time() acquisition_duration = round(acquisition_time - capture_start, 1) saving_duration = round(save_time - acquisition_time, 1) @@ -114,6 +108,7 @@ class ZStackThing(Thing): jpeg_path: str, image: np.ndarray, metadata: dict, + logger: InvocationLogger, ) -> None: """Saving the captured image and metadata to disk logger warning (via InvocationLogger) is raised if metadata is failed to be added From ade713b6d08065af121970ebff8e3f1a82695a59 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 16 Apr 2025 18:14:24 +0100 Subject: [PATCH 03/12] Create a stack folder and copy the middle image to image_dir --- .../things/smart_scan.py | 12 +++++- .../things/z_stack.py | 42 +++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index d11d81c3..bf33ad10 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -45,6 +45,7 @@ ZStackDep = direct_thing_client_dependency(ZStackThing, "/z_stack/") IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpeg$") + def unpack_autofocus(scan_data): """Extract z, sharpness data from a move_and_measure call @@ -589,7 +590,16 @@ class SmartScanThing(Thing): current_pos_xyz, imaged=True, focused=focused ) - self._z_stack.smart_stack(images_dir = self._ongoing_scan_images_dir) + site_folder = os.path.join( + self._ongoing_scan_images_dir, + "stacks", + f"{new_pos_xyz[0]}_{new_pos_xyz[1]}", + ) + os.makedirs(site_folder, exist_ok=True) + self._z_stack.smart_stack( + images_dir=self._ongoing_scan_images_dir, + stack_dir=site_folder, + ) # increment capure counter as thread has completed self._scan_images_taken += 1 diff --git a/src/openflexure_microscope_server/things/z_stack.py b/src/openflexure_microscope_server/things/z_stack.py index 9b1edfe6..1e6922e6 100644 --- a/src/openflexure_microscope_server/things/z_stack.py +++ b/src/openflexure_microscope_server/things/z_stack.py @@ -4,6 +4,8 @@ from PIL import Image import time import piexif import json +import shutil +import glob from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.thing import Thing @@ -18,12 +20,13 @@ from labthings_fastapi.dependencies.invocation import ( class CaptureError(RuntimeError): """An error trying to capture from Picamera""" + class ZStackThing(Thing): @thing_property def images_to_capture(self) -> int: """The number of images to capture and save in a stack Defaults to 1 unless you need to see either side of focus""" - return self.thing_settings.get("stack_height", 9) + return self.thing_settings.get("images_to_capture", 1) @images_to_capture.setter def images_to_capture(self, value: int) -> None: @@ -46,9 +49,10 @@ class ZStackThing(Thing): self, cam: CamDep, stage: StageDep, - images_dir: str, logger: InvocationLogger, metadata_getter: GetThingStates, + images_dir: str, + stack_dir: str, ) -> None: stack_dz = self.stack_dz images_to_capture = self.images_to_capture @@ -58,13 +62,19 @@ class ZStackThing(Thing): for capture_count in range(images_to_capture): jpeg_path = os.path.join( - images_dir, + stack_dir, f"{capture_count}.jpeg", ) self._capture_and_save( - jpeg_path, cam, logger, metadata_getter, + jpeg_path=jpeg_path, + cam=cam, + logger=logger, + metadata_getter=metadata_getter, ) stage.move_relative(z=stack_dz) + time.sleep(0.3) + + self.copy_central_image(images_dir, stack_dir, logger) def _capture_and_save( self, @@ -79,7 +89,10 @@ class ZStackThing(Thing): that the stage may be moved while it's saved. """ capture_start = time.time() - image, metadata = self._capture_image(cam, metadata_getter,) + image, metadata = self._capture_image( + cam, + metadata_getter, + ) acquisition_time = time.time() self._save_capture(jpeg_path, image, metadata, logger) save_time = time.time() @@ -91,8 +104,6 @@ class ZStackThing(Thing): def _capture_image(self, cam, metadata_getter) -> tuple[np.ndarray, dict]: """Capture an image in memory and return it with metadata - This will set the event `acquired` once the image has been acquired, so - that the stage may be moved while it's saved. CaptureError raised if the capture fails for any reason returns tuple with numpy array of image data, and dict of metadata """ @@ -130,3 +141,20 @@ class ZStackThing(Thing): logger.warning(f"Failed to add metadata to {jpeg_path}") except Exception as e: raise IOError(f"An error occurred while saving {jpeg_path}") from e + + def copy_central_image( + self, + images_dir: str, + stack_dir: str, + logger: InvocationLogger, + ): + """Gets a list of images in a folder (stack_dir), sorts them, and copies the central image + to images dir.""" + image_list = glob.glob(os.path.join(stack_dir, "*")) + image_list.sort() + central_index = (len(image_list) - 1) // 2 + central_image = image_list[central_index] + logger.warning(central_image) + xy_location = os.path.basename(stack_dir) + + shutil.copy(central_image, os.path.join(images_dir, f"{xy_location}.jpeg")) From 9b2ccea66065724ffd8735f9f877ac939bbdab27 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 16 Apr 2025 18:30:21 +0100 Subject: [PATCH 04/12] Don't move further after the final image of the stack --- src/openflexure_microscope_server/things/z_stack.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/z_stack.py b/src/openflexure_microscope_server/things/z_stack.py index 1e6922e6..434ccb3a 100644 --- a/src/openflexure_microscope_server/things/z_stack.py +++ b/src/openflexure_microscope_server/things/z_stack.py @@ -71,8 +71,11 @@ class ZStackThing(Thing): logger=logger, metadata_getter=metadata_getter, ) - stage.move_relative(z=stack_dz) - time.sleep(0.3) + + # If the stack isn't complete yet, move + if capture_count + 1 < images_to_capture: + stage.move_relative(z=stack_dz) + time.sleep(0.3) self.copy_central_image(images_dir, stack_dir, logger) From dee11deec1f6f05058739fd10fd2cc359b4fe2d7 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 17 Apr 2025 16:57:45 +0100 Subject: [PATCH 05/12] New capturing Thing --- ofm_config_full.json | 3 +- .../things/smart_scan.py | 2 +- .../things/z_stack.py | 124 +++++++++--------- 3 files changed, 68 insertions(+), 61 deletions(-) diff --git a/ofm_config_full.json b/ofm_config_full.json index 75ef4935..4cd0cb47 100644 --- a/ofm_config_full.json +++ b/ofm_config_full.json @@ -14,7 +14,8 @@ } }, "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", - "/z_stack/": "openflexure_microscope_server.things.smart_scan:ZStackThing" + "/z_stack/": "openflexure_microscope_server.things.z_stack:ZStackThing", + "/capturing/": "openflexure_microscope_server.things.z_stack:CapturingThing" }, "settings_folder": "/var/openflexure/settings/" } diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index bf33ad10..f3592591 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -596,7 +596,7 @@ class SmartScanThing(Thing): f"{new_pos_xyz[0]}_{new_pos_xyz[1]}", ) os.makedirs(site_folder, exist_ok=True) - self._z_stack.smart_stack( + self._z_stack.run_z_stack( images_dir=self._ongoing_scan_images_dir, stack_dir=site_folder, ) diff --git a/src/openflexure_microscope_server/things/z_stack.py b/src/openflexure_microscope_server/things/z_stack.py index 434ccb3a..e9eacf22 100644 --- a/src/openflexure_microscope_server/things/z_stack.py +++ b/src/openflexure_microscope_server/things/z_stack.py @@ -15,70 +15,15 @@ from .stage import StageDependency as StageDep from labthings_fastapi.dependencies.invocation import ( InvocationLogger, ) +from labthings_fastapi.dependencies.thing import direct_thing_client_dependency class CaptureError(RuntimeError): """An error trying to capture from Picamera""" -class ZStackThing(Thing): - @thing_property - def images_to_capture(self) -> int: - """The number of images to capture and save in a stack - Defaults to 1 unless you need to see either side of focus""" - return self.thing_settings.get("images_to_capture", 1) - - @images_to_capture.setter - def images_to_capture(self, value: int) -> None: - self.thing_settings["images_to_capture"] = value - - @thing_property - def stack_dz(self) -> int: - """Space in steps between images in a z-stack - Suggested is 50 for 60-100x - 100 for 40x - 200 for 20x""" - return self.thing_settings.get("stack_dz", 50) - - @stack_dz.setter - def stack_dz(self, value: int) -> None: - self.thing_settings["stack_dz"] = value - +class CapturingThing(Thing): @thing_action - def smart_stack( - self, - cam: CamDep, - stage: StageDep, - logger: InvocationLogger, - metadata_getter: GetThingStates, - images_dir: str, - stack_dir: str, - ) -> None: - stack_dz = self.stack_dz - images_to_capture = self.images_to_capture - - stack_z_range = stack_dz * (images_to_capture - 1) - stage.move_relative(z=-stack_z_range / 2) - - for capture_count in range(images_to_capture): - jpeg_path = os.path.join( - stack_dir, - f"{capture_count}.jpeg", - ) - self._capture_and_save( - jpeg_path=jpeg_path, - cam=cam, - logger=logger, - metadata_getter=metadata_getter, - ) - - # If the stack isn't complete yet, move - if capture_count + 1 < images_to_capture: - stage.move_relative(z=stack_dz) - time.sleep(0.3) - - self.copy_central_image(images_dir, stack_dir, logger) - def _capture_and_save( self, jpeg_path: str, @@ -145,11 +90,73 @@ class ZStackThing(Thing): except Exception as e: raise IOError(f"An error occurred while saving {jpeg_path}") from e + +CapturingDep = direct_thing_client_dependency(CapturingThing, "/capturing/") + + +class ZStackThing(Thing): + @thing_property + def images_to_capture(self) -> int: + """The number of images to capture and save in a stack + Defaults to 1 unless you need to see either side of focus""" + return self.thing_settings.get("images_to_capture", 1) + + @images_to_capture.setter + def images_to_capture(self, value: int) -> None: + self.thing_settings["images_to_capture"] = value + + @thing_property + def stack_dz(self) -> int: + """Space in steps between images in a z-stack + Suggested is 50 for 60-100x + 100 for 40x + 200 for 20x""" + return self.thing_settings.get("stack_dz", 50) + + @stack_dz.setter + def stack_dz(self, value: int) -> None: + self.thing_settings["stack_dz"] = value + + @thing_action + def run_z_stack( + self, + cam: CamDep, + stage: StageDep, + logger: InvocationLogger, + metadata_getter: GetThingStates, + capture: CapturingDep, + images_dir: str, + stack_dir: str, + ) -> None: + stack_dz = self.stack_dz + images_to_capture = self.images_to_capture + + stack_z_range = stack_dz * (images_to_capture - 1) + stage.move_relative(z=-stack_z_range / 2) + + for capture_count in range(images_to_capture): + jpeg_path = os.path.join( + stack_dir, + f"{capture_count}.jpeg", + ) + capture._capture_and_save( + jpeg_path=jpeg_path, + cam=cam, + logger=logger, + metadata_getter=metadata_getter, + ) + + # If the stack isn't complete yet, move + if capture_count + 1 < images_to_capture: + stage.move_relative(z=stack_dz) + time.sleep(0.3) + + self.copy_central_image(images_dir, stack_dir, logger) + def copy_central_image( self, images_dir: str, stack_dir: str, - logger: InvocationLogger, ): """Gets a list of images in a folder (stack_dir), sorts them, and copies the central image to images dir.""" @@ -157,7 +164,6 @@ class ZStackThing(Thing): image_list.sort() central_index = (len(image_list) - 1) // 2 central_image = image_list[central_index] - logger.warning(central_image) xy_location = os.path.basename(stack_dir) shutil.copy(central_image, os.path.join(images_dir, f"{xy_location}.jpeg")) From 9f39f61d0345e0bd62684047421625b4f740cdd6 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 23 Apr 2025 19:16:23 +0100 Subject: [PATCH 06/12] Stacking in autofocus with temp capture Thing --- ofm_config_full.json | 3 +- .../things/autofocus.py | 87 +++++++++++++++++- .../things/{z_stack.py => capture.py} | 90 ++----------------- .../things/smart_scan.py | 7 +- 4 files changed, 92 insertions(+), 95 deletions(-) rename src/openflexure_microscope_server/things/{z_stack.py => capture.py} (52%) diff --git a/ofm_config_full.json b/ofm_config_full.json index 4cd0cb47..7ae0469d 100644 --- a/ofm_config_full.json +++ b/ofm_config_full.json @@ -14,8 +14,7 @@ } }, "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", - "/z_stack/": "openflexure_microscope_server.things.z_stack:ZStackThing", - "/capturing/": "openflexure_microscope_server.things.z_stack:CapturingThing" + "/capture/": "openflexure_microscope_server.things.capture:CaptureThing" }, "settings_folder": "/var/openflexure/settings/" } diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 92eabd41..b85d9cb2 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -11,21 +11,29 @@ from contextlib import contextmanager import logging import time from typing import Annotated, Mapping, Optional, Sequence +import os +import shutil +import glob from fastapi import Depends from labthings_fastapi.thing import Thing from labthings_fastapi.dependencies.blocking_portal import BlockingPortal -from labthings_fastapi.decorators import thing_action +from labthings_fastapi.decorators import thing_action, thing_property +from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.types.numpy import NDArray +from labthings_fastapi.dependencies.thing import direct_thing_client_dependency +from labthings_fastapi.dependencies.invocation import InvocationLogger + from .camera import RawCameraDependency as Camera from .camera import CameraDependency as WrappedCamera from .stage import StageDependency as Stage +from .capture import CaptureThing import numpy as np from pydantic import BaseModel -### Autofocus utilities +CaptureDep = direct_thing_client_dependency(CaptureThing, "/capture/") class JPEGSharpnessMonitor: @@ -251,3 +259,78 @@ class AutofocusThing(Thing): cutoff = threshold * (peak - base) return current_sharpness >= base + cutoff + + @thing_property + def images_to_capture(self) -> int: + """The number of images to capture and save in a stack + Defaults to 1 unless you need to see either side of focus""" + return self.thing_settings.get("images_to_capture", 1) + + @images_to_capture.setter + def images_to_capture(self, value: int) -> None: + self.thing_settings["images_to_capture"] = value + + @thing_property + def stack_dz(self) -> int: + """Space in steps between images in a z-stack + Suggested is 50 for 60-100x + 100 for 40x + 200 for 20x""" + return self.thing_settings.get("stack_dz", 50) + + @stack_dz.setter + def stack_dz(self, value: int) -> None: + self.thing_settings["stack_dz"] = value + + @thing_action + def run_z_stack( + self, + cam: WrappedCamera, + stage: Stage, + logger: InvocationLogger, + metadata_getter: GetThingStates, + capture: CaptureDep, + images_dir: str, + stack_dir: str, + ) -> None: + """Run a z stack, saving all images to stack_dir and copying the + central image to stack_dir""" + stack_dz = self.stack_dz + images_to_capture = self.images_to_capture + + stack_z_range = stack_dz * (images_to_capture - 1) + stage.move_relative(z=-stack_z_range / 2) + + for capture_count in range(images_to_capture): + jpeg_path = os.path.join( + stack_dir, + f"{capture_count}.jpeg", + ) + capture._capture_and_save( + jpeg_path=jpeg_path, + cam=cam, + logger=logger, + metadata_getter=metadata_getter, + ) + + # If the stack isn't complete yet, move + if capture_count + 1 < images_to_capture: + stage.move_relative(z=stack_dz) + time.sleep(0.3) + + self.copy_central_image(images_dir, stack_dir) + + def copy_central_image( + self, + images_dir: str, + stack_dir: str, + ): + """Gets a list of images in a folder (stack_dir), sorts them, and copies the central image + to images dir.""" + image_list = glob.glob(os.path.join(stack_dir, "*")) + image_list.sort() + central_index = (len(image_list) - 1) // 2 + central_image = image_list[central_index] + xy_location = os.path.basename(stack_dir) + + shutil.copy(central_image, os.path.join(images_dir, f"{xy_location}.jpeg")) diff --git a/src/openflexure_microscope_server/things/z_stack.py b/src/openflexure_microscope_server/things/capture.py similarity index 52% rename from src/openflexure_microscope_server/things/z_stack.py rename to src/openflexure_microscope_server/things/capture.py index e9eacf22..214c05b9 100644 --- a/src/openflexure_microscope_server/things/z_stack.py +++ b/src/openflexure_microscope_server/things/capture.py @@ -1,28 +1,26 @@ -import os import numpy as np from PIL import Image import time import piexif import json -import shutil -import glob from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.thing import Thing -from labthings_fastapi.decorators import thing_action, thing_property +from labthings_fastapi.decorators import thing_action from .camera import CameraDependency as CamDep -from .stage import StageDependency as StageDep from labthings_fastapi.dependencies.invocation import ( InvocationLogger, ) -from labthings_fastapi.dependencies.thing import direct_thing_client_dependency class CaptureError(RuntimeError): """An error trying to capture from Picamera""" -class CapturingThing(Thing): +class CaptureThing(Thing): + """A temporary Thing to handle capturing to disk with associated metadata + Will be moved to the camera Thing or dependency in due course""" + @thing_action def _capture_and_save( self, @@ -89,81 +87,3 @@ class CapturingThing(Thing): logger.warning(f"Failed to add metadata to {jpeg_path}") except Exception as e: raise IOError(f"An error occurred while saving {jpeg_path}") from e - - -CapturingDep = direct_thing_client_dependency(CapturingThing, "/capturing/") - - -class ZStackThing(Thing): - @thing_property - def images_to_capture(self) -> int: - """The number of images to capture and save in a stack - Defaults to 1 unless you need to see either side of focus""" - return self.thing_settings.get("images_to_capture", 1) - - @images_to_capture.setter - def images_to_capture(self, value: int) -> None: - self.thing_settings["images_to_capture"] = value - - @thing_property - def stack_dz(self) -> int: - """Space in steps between images in a z-stack - Suggested is 50 for 60-100x - 100 for 40x - 200 for 20x""" - return self.thing_settings.get("stack_dz", 50) - - @stack_dz.setter - def stack_dz(self, value: int) -> None: - self.thing_settings["stack_dz"] = value - - @thing_action - def run_z_stack( - self, - cam: CamDep, - stage: StageDep, - logger: InvocationLogger, - metadata_getter: GetThingStates, - capture: CapturingDep, - images_dir: str, - stack_dir: str, - ) -> None: - stack_dz = self.stack_dz - images_to_capture = self.images_to_capture - - stack_z_range = stack_dz * (images_to_capture - 1) - stage.move_relative(z=-stack_z_range / 2) - - for capture_count in range(images_to_capture): - jpeg_path = os.path.join( - stack_dir, - f"{capture_count}.jpeg", - ) - capture._capture_and_save( - jpeg_path=jpeg_path, - cam=cam, - logger=logger, - metadata_getter=metadata_getter, - ) - - # If the stack isn't complete yet, move - if capture_count + 1 < images_to_capture: - stage.move_relative(z=stack_dz) - time.sleep(0.3) - - self.copy_central_image(images_dir, stack_dir, logger) - - def copy_central_image( - self, - images_dir: str, - stack_dir: str, - ): - """Gets a list of images in a folder (stack_dir), sorts them, and copies the central image - to images dir.""" - image_list = glob.glob(os.path.join(stack_dir, "*")) - image_list.sort() - central_index = (len(image_list) - 1) // 2 - central_image = image_list[central_index] - xy_location = os.path.basename(stack_dir) - - shutil.copy(central_image, os.path.join(images_dir, f"{xy_location}.jpeg")) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index f3592591..940c1d3a 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -33,7 +33,6 @@ from openflexure_microscope_server.utilities import ErrorCapturingThread from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.background_detect import BackgroundDetectThing -from openflexure_microscope_server.things.z_stack import ZStackThing from openflexure_microscope_server import scan_planners CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") @@ -160,7 +159,6 @@ class SmartScanThing(Thing): self._background_detect: Optional[BackgroundDep] = None self._ongoing_scan_name: Optional[str] = None self._starting_position: Optional[Mapping[str, int]] = None - self._z_stack: Optional[ZStackDep] = None self._capture_thread: Optional[ErrorCapturingThread] = None self._scan_images_taken: Optional[int] = None # TODO Scan data is a dict during refactoring, should become a dataclass @@ -177,7 +175,6 @@ class SmartScanThing(Thing): metadata_getter: GetThingStates, csm: CSMDep, background_detect: BackgroundDep, - z_stack: ZStackDep, scan_name: str = "", ): """Move the stage to cover an area, taking images that can be tiled together. @@ -201,7 +198,6 @@ class SmartScanThing(Thing): self._csm = csm self._background_detect = background_detect self._capture_thread = None - self._z_stack = z_stack self._scan_images_taken = 0 # Don't set self._scan_data dictionary. This is done at the start of _run_scan @@ -236,7 +232,6 @@ class SmartScanThing(Thing): self._ongoing_scan_name = None self._scan_images_taken = None self._scan_data = None - self._z_stack = None self._scan_lock.release() @_scan_running @@ -596,7 +591,7 @@ class SmartScanThing(Thing): f"{new_pos_xyz[0]}_{new_pos_xyz[1]}", ) os.makedirs(site_folder, exist_ok=True) - self._z_stack.run_z_stack( + self._autofocus.run_z_stack( images_dir=self._ongoing_scan_images_dir, stack_dir=site_folder, ) From bf6437b42cfb7f8afd593d1ec479fbb37928cce6 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 25 Apr 2025 12:09:08 +0100 Subject: [PATCH 07/12] Renaming z_stack features for clarity, docstring on AutofocusThing --- .../things/autofocus.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index b85d9cb2..f823b529 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -146,6 +146,11 @@ class SharpnessDataArrays(BaseModel): class AutofocusThing(Thing): + """The Thing concerned with combinations of z axis movements and the camera. + + Actions here involve moving a stage in z, and using the camera to either + capture images (generally, z-stacking) and measuring the sharpness of the + field of view to assess focus (autofocus and testing the success of a z-stack)""" @thing_action def fast_autofocus( self, @@ -177,7 +182,7 @@ class AutofocusThing(Thing): return m.data_dict() @thing_action - def move_and_measure( + def z_move_and_measure_sharpness( self, m: SharpnessMonitorDep, dz: Sequence[int], @@ -261,14 +266,14 @@ class AutofocusThing(Thing): return current_sharpness >= base + cutoff @thing_property - def images_to_capture(self) -> int: + def stack_images_to_capture(self) -> int: """The number of images to capture and save in a stack Defaults to 1 unless you need to see either side of focus""" - return self.thing_settings.get("images_to_capture", 1) + return self.thing_settings.get("stack_images_to_capture", 1) - @images_to_capture.setter - def images_to_capture(self, value: int) -> None: - self.thing_settings["images_to_capture"] = value + @stack_images_to_capture.setter + def stack_images_to_capture(self, value: int) -> None: + self.thing_settings["stack_images_to_capture"] = value @thing_property def stack_dz(self) -> int: @@ -318,9 +323,9 @@ class AutofocusThing(Thing): stage.move_relative(z=stack_dz) time.sleep(0.3) - self.copy_central_image(images_dir, stack_dir) + self.copy_central_image_from_stack(images_dir, stack_dir) - def copy_central_image( + def copy_central_image_from_stack( self, images_dir: str, stack_dir: str, From f50580cdd0da1b7b5f6357f1586d4e30a8789d2d Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 25 Apr 2025 12:29:11 +0100 Subject: [PATCH 08/12] Give sharpness monitor a descriptive name, not m --- .../things/autofocus.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index f823b529..1adb81bf 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -147,14 +147,14 @@ class SharpnessDataArrays(BaseModel): class AutofocusThing(Thing): """The Thing concerned with combinations of z axis movements and the camera. - + Actions here involve moving a stage in z, and using the camera to either capture images (generally, z-stacking) and measuring the sharpness of the field of view to assess focus (autofocus and testing the success of a z-stack)""" @thing_action def fast_autofocus( self, - m: SharpnessMonitorDep, + sharpness_monitor: SharpnessMonitorDep, dz: int = 2000, start: str = "centre", ) -> SharpnessDataArrays: @@ -164,27 +164,27 @@ class AutofocusThing(Thing): the position where the image was sharpest. We'll then move back down, and finally up to the sharpest point. """ - with m.run(): + with sharpness_monitor.run(): # Move to (-dz / 2) if start == "centre": - m.focus_rel(-dz / 2) + sharpness_monitor.focus_rel(-dz / 2) # Move to dz while monitoring sharpness # i: Sharpness monitor index for this move # z: Final z position after move - i, z = m.focus_rel(dz, block_cancellation=True) + i, z = sharpness_monitor.focus_rel(dz, block_cancellation=True) # Get the z position with highest sharpness from the previous move (index i) - fz: int = m.sharpest_z_on_move(i) + fz: int = sharpness_monitor.sharpest_z_on_move(i) # Move all the way to the start so it's consistent - i, z = m.focus_rel(-dz) + i, z = sharpness_monitor.focus_rel(-dz) # Move to the target position fz (relative move of (fz - z)) - m.focus_rel(fz - z) + sharpness_monitor.focus_rel(fz - z) # Return all focus data - return m.data_dict() + return sharpness_monitor.data_dict() @thing_action def z_move_and_measure_sharpness( self, - m: SharpnessMonitorDep, + sharpness_monitor: SharpnessMonitorDep, dz: Sequence[int], wait: float = 0, ) -> SharpnessDataArrays: @@ -200,16 +200,16 @@ class AutofocusThing(Thing): If `wait` is specified, we will wait for that many seconds between moves. """ - with m.run(): + with sharpness_monitor.run(): for i, current_dz in enumerate(dz): if i > 0 and wait > 0: time.sleep(wait) - m.focus_rel(current_dz) - return m.data_dict() + sharpness_monitor.focus_rel(current_dz) + return sharpness_monitor.data_dict() @thing_action def looping_autofocus( - self, stage: Stage, m: SharpnessMonitorDep, dz=2000, start="centre" + self, stage: Stage, sharpness_monitor: SharpnessMonitorDep, dz=2000, start="centre" ): """Repeatedly autofocus the stage until it looks focused. @@ -222,14 +222,14 @@ class AutofocusThing(Thing): attempts = 0 backlash = 200 - with m.run(): + with sharpness_monitor.run(): while repeat and attempts < 10: if start == "centre": stage.move_relative(x=0, y=0, z=-(backlash + dz / 2)) stage.move_relative(x=0, y=0, z=backlash) - i, z = m.focus_rel(dz, block_cancellation=True) - _, heights, sizes = m.move_data(i) + i, z = sharpness_monitor.focus_rel(dz, block_cancellation=True) + _, heights, sizes = sharpness_monitor.move_data(i) peak_height = heights[np.argmax(sizes)] height_min = np.min(heights) @@ -301,7 +301,7 @@ class AutofocusThing(Thing): """Run a z stack, saving all images to stack_dir and copying the central image to stack_dir""" stack_dz = self.stack_dz - images_to_capture = self.images_to_capture + images_to_capture = self.stack_images_to_capture stack_z_range = stack_dz * (images_to_capture - 1) stage.move_relative(z=-stack_z_range / 2) From 6cf99d50960b9d23240a40093ff505fc9f2dad1d Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 25 Apr 2025 14:08:07 +0100 Subject: [PATCH 09/12] Stack settings in GUI --- .../things/autofocus.py | 7 ++++++- .../tabContentComponents/slideScanContent.vue | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 1adb81bf..c6faf8e7 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -151,6 +151,7 @@ class AutofocusThing(Thing): Actions here involve moving a stage in z, and using the camera to either capture images (generally, z-stacking) and measuring the sharpness of the field of view to assess focus (autofocus and testing the success of a z-stack)""" + @thing_action def fast_autofocus( self, @@ -209,7 +210,11 @@ class AutofocusThing(Thing): @thing_action def looping_autofocus( - self, stage: Stage, sharpness_monitor: SharpnessMonitorDep, dz=2000, start="centre" + self, + stage: Stage, + sharpness_monitor: SharpnessMonitorDep, + dz=2000, + start="centre", ): """Repeatedly autofocus the stage until it looks focused. diff --git a/webapp/src/components/tabContentComponents/slideScanContent.vue b/webapp/src/components/tabContentComponents/slideScanContent.vue index 8a152fc8..4c4d4643 100644 --- a/webapp/src/components/tabContentComponents/slideScanContent.vue +++ b/webapp/src/components/tabContentComponents/slideScanContent.vue @@ -24,6 +24,20 @@ label="Autofocus range (steps)" /> +
+ +
+
+ +
Date: Mon, 28 Apr 2025 13:49:37 +0100 Subject: [PATCH 10/12] Update camera thing to match upstream --- src/openflexure_microscope_server/things/camera/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 48a9b21d..8d3fab23 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -44,7 +44,7 @@ class CameraProtocol(Protocol): def capture_array( self, - resolution: Literal["lores", "main", "full"] = "main", + stream_name: Literal["main", "lores", "raw"] = "main", ) -> NDArray: ... def capture_jpeg( @@ -166,7 +166,7 @@ class CameraStub(BaseCamera): @thing_action def capture_array( self, - resolution: Literal["lores", "main", "full"] = "main", + stream_name: Literal["main", "lores", "raw"] = "main", ) -> NDArray: raise NotImplementedError("Cameras must not inherit from CameraStub") From 673a135157e980f5e9d78c86b8342cf9595575d4 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 6 May 2025 10:48:01 +0100 Subject: [PATCH 11/12] Reorganise imports --- src/openflexure_microscope_server/things/autofocus.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index c6faf8e7..a85b0d77 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -16,6 +16,8 @@ import shutil import glob from fastapi import Depends +import numpy as np +from pydantic import BaseModel from labthings_fastapi.thing import Thing from labthings_fastapi.dependencies.blocking_portal import BlockingPortal @@ -29,9 +31,6 @@ from .camera import RawCameraDependency as Camera from .camera import CameraDependency as WrappedCamera from .stage import StageDependency as Stage from .capture import CaptureThing -import numpy as np -from pydantic import BaseModel - CaptureDep = direct_thing_client_dependency(CaptureThing, "/capture/") From 2205459aebfa8b023c04dcbeb9cea5ae972a1588 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 7 May 2025 19:58:24 +0100 Subject: [PATCH 12/12] Removed z_stack Thing from rebase --- src/openflexure_microscope_server/things/smart_scan.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 940c1d3a..21a541ac 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -13,7 +13,6 @@ from datetime import datetime from subprocess import CompletedProcess, Popen, PIPE, SubprocessError, STDOUT import glob import json -import piexif import re from labthings_fastapi.thing import Thing @@ -40,9 +39,8 @@ AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/") BackgroundDep = direct_thing_client_dependency( BackgroundDetectThing, "/background_detect/" ) -ZStackDep = direct_thing_client_dependency(ZStackThing, "/z_stack/") -IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpeg$") +IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpeg$") def unpack_autofocus(scan_data): @@ -699,7 +697,6 @@ class SmartScanThing(Thing): raise HTTPException(404, "File not found") return FileResponse(path) - @thing_property def max_range(self) -> int: """The maximum distance from the centre of the scan before we break"""