Merge branch 'refactor-scanning' into 'v3'
Refactor scanning See merge request openflexure/openflexure-microscope-server!240
This commit is contained in:
commit
5ab0a62964
4 changed files with 547 additions and 437 deletions
|
|
@ -76,7 +76,7 @@ class OFMHandler(logging.Handler):
|
|||
def append_record(self, record):
|
||||
"""
|
||||
Use the built in formatter to format the record, then save
|
||||
it to an array. Pop any in excess of the mamimum number of logs
|
||||
it to an array. Pop any in excess of the maximum number of logs
|
||||
"""
|
||||
self._log.append(self.format(record))
|
||||
while len(self._log) > self._max_logs:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,117 +0,0 @@
|
|||
from typing import Optional
|
||||
import os
|
||||
from subprocess import CompletedProcess, run, PIPE
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.dependencies.raw_thing import raw_thing_dependency
|
||||
from labthings_fastapi.dependencies.invocation import InvocationLogger
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
from labthings_fastapi.outputs.blob import blob_type
|
||||
|
||||
from .smart_scan import SmartScanThing
|
||||
|
||||
SmartScanDep = raw_thing_dependency(SmartScanThing)
|
||||
|
||||
|
||||
JPEGBlob = blob_type("image/jpeg")
|
||||
|
||||
|
||||
class Stitcher(Thing):
|
||||
def __init__(self, path_to_openflexure_stitch: str):
|
||||
self._script = path_to_openflexure_stitch
|
||||
|
||||
def run_subprocess(
|
||||
self,
|
||||
logger: InvocationLogger,
|
||||
cmd: list[str],
|
||||
) -> CompletedProcess:
|
||||
"""Run a subprocess and log any output"""
|
||||
logger.info(f"Running command in subprocess: `{' '.join(cmd)}")
|
||||
output = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
|
||||
for pipe in [output.stdout, output.stderr]:
|
||||
if pipe:
|
||||
logger.info(pipe)
|
||||
output.check_returncode()
|
||||
return output
|
||||
|
||||
def images_folder(
|
||||
self, smart_scan: SmartScanDep, scan_name: Optional[str] = None
|
||||
) -> str:
|
||||
scan_folder = smart_scan.scan_folder_path(scan_name=scan_name)
|
||||
return os.path.join(scan_folder, "images")
|
||||
|
||||
@staticmethod
|
||||
def output_up_to_date(
|
||||
folder: str,
|
||||
output_filename: str,
|
||||
image_prefix: str = "image",
|
||||
image_suffix: str = ".jpg",
|
||||
) -> bool:
|
||||
"""Check if any of the images in a folder are newer than a file
|
||||
|
||||
If there are no images (files with the prefix and suffix) newer than the
|
||||
`output_filename`, we return `True`, i.e. the output is up to date. If
|
||||
any image in the folder is newer, we return `False`.
|
||||
|
||||
This is not flawless logic - if an update process is slow, images might be
|
||||
saved between starting that process and saving the output. Consequently,
|
||||
a `True` from this function does not guarantee the output is up to date.
|
||||
|
||||
If the output file is missing, we also return `False`.
|
||||
"""
|
||||
output_path = os.path.join(folder, output_filename)
|
||||
if not os.path.exists(output_path):
|
||||
return False
|
||||
mtime = os.path.getmtime(output_path)
|
||||
for fname in os.listdir(folder):
|
||||
if fname.startswith(image_prefix) and fname.endswith(image_suffix):
|
||||
if os.path.getmtime(os.path.join(folder, fname)) > mtime:
|
||||
return False
|
||||
return True
|
||||
|
||||
@thing_action
|
||||
def stitch_scan_from_stage(
|
||||
self,
|
||||
logger: InvocationLogger,
|
||||
smart_scan: SmartScanDep,
|
||||
scan_name: Optional[str] = None,
|
||||
downsample: float = 1.0,
|
||||
) -> JPEGBlob:
|
||||
"""Generate a stitched image based on stage position metadata"""
|
||||
output_fname = "stitched_from_stage.jpg"
|
||||
images_folder = self.images_folder(smart_scan=smart_scan, scan_name=scan_name)
|
||||
if self.output_up_to_date(images_folder, output_fname):
|
||||
logger.info(f"No images are newer than {output_fname}, skipping.")
|
||||
else:
|
||||
self.run_subprocess(
|
||||
logger,
|
||||
[self._script, "--stitching_mode", "only_stage_stitch", images_folder],
|
||||
)
|
||||
return JPEGBlob.from_file(os.path.join(images_folder, output_fname))
|
||||
|
||||
@thing_action
|
||||
def update_scan_correlations(
|
||||
self,
|
||||
logger: InvocationLogger,
|
||||
smart_scan: SmartScanDep,
|
||||
scan_name: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Generate a stitched image based on stage position metadata"""
|
||||
images_folder = self.images_folder(smart_scan=smart_scan, scan_name=scan_name)
|
||||
self.run_subprocess(
|
||||
logger, [self._script, "--stitching_mode", "only_correlate", images_folder]
|
||||
)
|
||||
|
||||
@thing_action
|
||||
def stitch_scan(
|
||||
self,
|
||||
logger: InvocationLogger,
|
||||
smart_scan: SmartScanDep,
|
||||
scan_name: Optional[str] = None,
|
||||
downsample: float = 1.0,
|
||||
) -> None:
|
||||
"""Generate a stitched image based on stage position metadata"""
|
||||
images_folder = self.images_folder(smart_scan=smart_scan, scan_name=scan_name)
|
||||
self.run_subprocess(
|
||||
logger, [self._script, "--stitching_mode", "all", images_folder]
|
||||
)
|
||||
81
src/openflexure_microscope_server/utilities.py
Normal file
81
src/openflexure_microscope_server/utilities.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""
|
||||
This module contains some utility functions and classes
|
||||
"""
|
||||
|
||||
from threading import Thread
|
||||
|
||||
|
||||
class ErrorCapturingThread(Thread):
|
||||
"""
|
||||
This is a a subclass or Thread. It wraps the target function in a
|
||||
try-except block.
|
||||
|
||||
Execution will stop with an unhandled exception, but the exception will not be raised
|
||||
until the join method is called. When the join method is called, the exception is
|
||||
called in the calling thread.
|
||||
|
||||
This allows for error handling to be performed by the calling thread at the time that
|
||||
join is run.
|
||||
"""
|
||||
|
||||
def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None):
|
||||
"""
|
||||
Initialise with the same arguments as Thread
|
||||
"""
|
||||
# As all inputs are keywords we need to set the default values for args and kwargs:
|
||||
if args is None:
|
||||
args = ()
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
# Make an empty list for any error.
|
||||
# We are only ever going to have one or zero errors, this is an empty
|
||||
# list as we need to pass the reference not the value to collect the
|
||||
# error. If we could simply make a pointer we would have done that.
|
||||
self._error_buffer = []
|
||||
|
||||
# Add the target function end error buffer to the thread to the start of
|
||||
# the argument list so they are passed to _wrap_and_catch_errors()
|
||||
args = (target, self._error_buffer, *args)
|
||||
# Start the thread with _wrap_and_catch_errors() as the target
|
||||
super().__init__(
|
||||
group=group,
|
||||
target=_wrap_and_catch_errors,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
daemon=daemon,
|
||||
)
|
||||
|
||||
def join(self, timeout=None):
|
||||
"""
|
||||
Join when the thread is complete. If the thread ended due to an unhandled exception,
|
||||
the exception will be raised when this method is called.
|
||||
"""
|
||||
super().join(timeout)
|
||||
# If there is an error in the error buffer clear the buffer and raise it
|
||||
if self._error_buffer:
|
||||
err = self._error_buffer[0]
|
||||
self._error_buffer = []
|
||||
raise err
|
||||
|
||||
|
||||
def _wrap_and_catch_errors(target, error_buffer, *args, **kwargs):
|
||||
"""
|
||||
This function is designed only to be used by
|
||||
ErrorCapturingThread
|
||||
|
||||
It will run a target function in a try-except block
|
||||
any errors caught are added to an empty list that
|
||||
should be supplied as a keyword argument
|
||||
|
||||
Arguments:
|
||||
* target - The target function to call
|
||||
* error_buffer - An empty list that is used for returning
|
||||
the exception from the thread
|
||||
*args: The arguments for the target function
|
||||
**kwargs: The Keyword arguments for the target function
|
||||
"""
|
||||
try:
|
||||
target(*args, **kwargs)
|
||||
except BaseException as e:
|
||||
error_buffer.append(e)
|
||||
Loading…
Add table
Add a link
Reference in a new issue