Add @scan_running decorator for scans to try to access to the scan logger for methods not called by the scan
This commit is contained in:
parent
26e265f026
commit
e030eb576b
1 changed files with 125 additions and 66 deletions
|
|
@ -162,8 +162,15 @@ class SmartScanThing(Thing):
|
||||||
|
|
||||||
# Variables set by the scan
|
# Variables set by the scan
|
||||||
self._latest_scan_name = None
|
self._latest_scan_name = None
|
||||||
|
|
||||||
|
# Scan logger is the invocation logger labthings-fastapi creates
|
||||||
|
# when the `sample_scan` thing_action is called. It is saved as
|
||||||
|
# private class variable along with many others here.
|
||||||
|
# Access to these variables requires a scan to be running,
|
||||||
|
# any method that calls these should be decorrected with
|
||||||
|
# @_scan_running
|
||||||
|
self._scan_logger = None
|
||||||
self._cancel = None
|
self._cancel = None
|
||||||
self._logger = None
|
|
||||||
self._autofocus = None
|
self._autofocus = None
|
||||||
self._stage = None
|
self._stage = None
|
||||||
self._cam = None
|
self._cam = None
|
||||||
|
|
@ -174,6 +181,24 @@ class SmartScanThing(Thing):
|
||||||
self._starting_position = None
|
self._starting_position = None
|
||||||
self._scan_images_taken = None
|
self._scan_images_taken = None
|
||||||
|
|
||||||
|
def _scan_running(method):
|
||||||
|
"""
|
||||||
|
This decorator is used by all methods that are using the variables
|
||||||
|
set for the scan. It will throw a runtime error if self._scan_logger
|
||||||
|
is not set, as all scan variables are set at the same time and released
|
||||||
|
with the lock
|
||||||
|
"""
|
||||||
|
|
||||||
|
def scan_running_wrapper(self, *args, **kwargs):
|
||||||
|
# Only start the method is the scan logger is set
|
||||||
|
if self._scan_logger is not None:
|
||||||
|
return method(self, *args, **kwargs)
|
||||||
|
raise RuntimeError(
|
||||||
|
"Calling a @scan_running method can only be done while a scan is running!"
|
||||||
|
)
|
||||||
|
|
||||||
|
return scan_running_wrapper
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def sample_scan(
|
def sample_scan(
|
||||||
self,
|
self,
|
||||||
|
|
@ -201,7 +226,7 @@ class SmartScanThing(Thing):
|
||||||
|
|
||||||
# Set private variables for this scan
|
# Set private variables for this scan
|
||||||
self._cancel = cancel
|
self._cancel = cancel
|
||||||
self._logger = logger
|
self._scan_logger = logger
|
||||||
self._autofocus = autofocus
|
self._autofocus = autofocus
|
||||||
self._stage = stage
|
self._stage = stage
|
||||||
self._cam = cam
|
self._cam = cam
|
||||||
|
|
@ -227,7 +252,7 @@ class SmartScanThing(Thing):
|
||||||
finally:
|
finally:
|
||||||
# However the scan finishes unset all variables and release lock
|
# However the scan finishes unset all variables and release lock
|
||||||
self._cancel = None
|
self._cancel = None
|
||||||
self._logger = None
|
self._scan_logger = None
|
||||||
self._autofocus = None
|
self._autofocus = None
|
||||||
self._stage = None
|
self._stage = None
|
||||||
self._cam = None
|
self._cam = None
|
||||||
|
|
@ -238,6 +263,7 @@ class SmartScanThing(Thing):
|
||||||
self._scan_images_taken = None
|
self._scan_images_taken = None
|
||||||
self._scan_lock.release()
|
self._scan_lock.release()
|
||||||
|
|
||||||
|
@_scan_running
|
||||||
def _check_background_is_set(self):
|
def _check_background_is_set(self):
|
||||||
"""Before starting a scan check that we've got a background set
|
"""Before starting a scan check that we've got a background set
|
||||||
|
|
||||||
|
|
@ -251,7 +277,7 @@ class SmartScanThing(Thing):
|
||||||
"Background is not set: you need to calibrate background detection."
|
"Background is not set: you need to calibrate background detection."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._logger.warning(
|
self._scan_logger.warning(
|
||||||
"This scan will run in a spiral from the starting point "
|
"This scan will run in a spiral from the starting point "
|
||||||
f"until you cancel it, or until it has moved by {self.max_range} steps "
|
f"until you cancel it, or until it has moved by {self.max_range} steps "
|
||||||
"in every direction. Make sure you watch it run to stop it leaving "
|
"in every direction. Make sure you watch it run to stop it leaving "
|
||||||
|
|
@ -282,6 +308,7 @@ class SmartScanThing(Thing):
|
||||||
return os.path.join(scan_dir, IMG_DIR_NAME)
|
return os.path.join(scan_dir, IMG_DIR_NAME)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@_scan_running
|
||||||
def _ongoing_scan_dir(self):
|
def _ongoing_scan_dir(self):
|
||||||
"""For the ongoing scan this returns the scan directory"""
|
"""For the ongoing scan this returns the scan directory"""
|
||||||
if not self._ongoing_scan_name:
|
if not self._ongoing_scan_name:
|
||||||
|
|
@ -289,12 +316,14 @@ class SmartScanThing(Thing):
|
||||||
return self.dir_for_scan(self._ongoing_scan_name)
|
return self.dir_for_scan(self._ongoing_scan_name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@_scan_running
|
||||||
def _ongoing_scan_images_dir(self):
|
def _ongoing_scan_images_dir(self):
|
||||||
"""For the ongoing scan this returns the image directory"""
|
"""For the ongoing scan this returns the image directory"""
|
||||||
if not self._ongoing_scan_name:
|
if not self._ongoing_scan_name:
|
||||||
raise RuntimeError("Cannot get ongoing scan name while no scan is running")
|
raise RuntimeError("Cannot get ongoing scan name while no scan is running")
|
||||||
return self.images_dir_for_scan(self._ongoing_scan_name)
|
return self.images_dir_for_scan(self._ongoing_scan_name)
|
||||||
|
|
||||||
|
@_scan_running
|
||||||
def _get_unique_scan_name_and_dir(self, scan_name: str) -> str:
|
def _get_unique_scan_name_and_dir(self, scan_name: str) -> str:
|
||||||
"""Get a unique name for this scan and create a directory for it
|
"""Get a unique name for this scan and create a directory for it
|
||||||
|
|
||||||
|
|
@ -335,7 +364,8 @@ class SmartScanThing(Thing):
|
||||||
return trial_unique_scan_name
|
return trial_unique_scan_name
|
||||||
raise FileExistsError("Could not create a new scan folder: all names in use!")
|
raise FileExistsError("Could not create a new scan folder: all names in use!")
|
||||||
|
|
||||||
def move_to_next_point(
|
@_scan_running
|
||||||
|
def _move_to_next_point(
|
||||||
self,
|
self,
|
||||||
path: list[list[int]],
|
path: list[list[int]],
|
||||||
focused_path: list[list[int]],
|
focused_path: list[list[int]],
|
||||||
|
|
@ -354,12 +384,13 @@ class SmartScanThing(Thing):
|
||||||
z = int(focused_path[z_index][2])
|
z = int(focused_path[z_index][2])
|
||||||
else:
|
else:
|
||||||
z = self._stage.position["z"]
|
z = self._stage.position["z"]
|
||||||
self._logger.info(f"Moving to {loc}")
|
self._scan_logger.info(f"Moving to {loc}")
|
||||||
self._stage.move_absolute(
|
self._stage.move_absolute(
|
||||||
x=int(loc[0]), y=int(loc[1]), z=z - self.autofocus_dz / 2
|
x=int(loc[0]), y=int(loc[1]), z=z - self.autofocus_dz / 2
|
||||||
)
|
)
|
||||||
return loc + [z]
|
return loc + [z]
|
||||||
|
|
||||||
|
@_scan_running
|
||||||
def _run_scan(self, scan_name, overlap):
|
def _run_scan(self, scan_name, overlap):
|
||||||
# Define these variables so we can use them in the finally: block
|
# Define these variables so we can use them in the finally: block
|
||||||
# (after testing they are not None)
|
# (after testing they are not None)
|
||||||
|
|
@ -369,14 +400,14 @@ class SmartScanThing(Thing):
|
||||||
try:
|
try:
|
||||||
os.mkdir(self._ongoing_scan_images_dir)
|
os.mkdir(self._ongoing_scan_images_dir)
|
||||||
|
|
||||||
self._logger.info(f"Saving images to {self._ongoing_scan_images_dir}")
|
self._scan_logger.info(f"Saving images to {self._ongoing_scan_images_dir}")
|
||||||
|
|
||||||
max_dist = self.max_range
|
max_dist = self.max_range
|
||||||
|
|
||||||
if self.autofocus_dz == 0:
|
if self.autofocus_dz == 0:
|
||||||
self._logger.info("Running scan without autofocus")
|
self._scan_logger.info("Running scan without autofocus")
|
||||||
elif self.autofocus_dz <= 200:
|
elif self.autofocus_dz <= 200:
|
||||||
self._logger.warning(
|
self._scan_logger.warning(
|
||||||
f"Your dz range is {self.autofocus_dz} steps, which is too short to attempt to focus. Running without autofocus"
|
f"Your dz range is {self.autofocus_dz} steps, which is too short to attempt to focus. Running without autofocus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -391,7 +422,7 @@ class SmartScanThing(Thing):
|
||||||
"scans can be carried out."
|
"scans can be carried out."
|
||||||
)
|
)
|
||||||
if list(arr.shape[:2]) != [int(i) for i in self._csm.image_resolution]:
|
if list(arr.shape[:2]) != [int(i) for i in self._csm.image_resolution]:
|
||||||
self._logger.error(
|
self._scan_logger.error(
|
||||||
f"Images are, by default, {arr.shape[:2]}, but the CSM was "
|
f"Images are, by default, {arr.shape[:2]}, but the CSM was "
|
||||||
f"calibrated at {self._csm.image_resolution}."
|
f"calibrated at {self._csm.image_resolution}."
|
||||||
)
|
)
|
||||||
|
|
@ -409,7 +440,7 @@ class SmartScanThing(Thing):
|
||||||
np.abs(np.dot(np.array([arr.shape[0] * (1 - overlap), 0]), CSM)[1])
|
np.abs(np.dot(np.array([arr.shape[0] * (1 - overlap), 0]), CSM)[1])
|
||||||
)
|
)
|
||||||
|
|
||||||
self._logger.info(
|
self._scan_logger.info(
|
||||||
f"Based on an overlap of {overlap}, we will make steps of {dx}, {dy}"
|
f"Based on an overlap of {overlap}, we will make steps of {dx}, {dy}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -445,12 +476,12 @@ class SmartScanThing(Thing):
|
||||||
# We skip capturing on the first run, because we've not focused yet - and also we skip capturing if
|
# We skip capturing on the first run, because we've not focused yet - and also we skip capturing if
|
||||||
# it looks like background.
|
# it looks like background.
|
||||||
while len(path) > 0:
|
while len(path) > 0:
|
||||||
loc = self.move_to_next_point(path=path, focused_path=focused_path)
|
loc = self._move_to_next_point(path=path, focused_path=focused_path)
|
||||||
if not self.preview_stitch_running():
|
if not self._preview_stitch_running():
|
||||||
self.preview_stitch_start()
|
self._preview_stitch_start()
|
||||||
if self.stitch_automatically:
|
if self.stitch_automatically:
|
||||||
if not self.correlate_running():
|
if not self._correlate_running():
|
||||||
self.correlate_start(overlap=overlap)
|
self._correlate_start(overlap=overlap)
|
||||||
|
|
||||||
ensure_free_disk_space(self._ongoing_scan_dir)
|
ensure_free_disk_space(self._ongoing_scan_dir)
|
||||||
|
|
||||||
|
|
@ -462,7 +493,7 @@ class SmartScanThing(Thing):
|
||||||
|
|
||||||
# if more than 92% of the image is background, treat it as background and continue
|
# if more than 92% of the image is background, treat it as background and continue
|
||||||
if not image_is_sample:
|
if not image_is_sample:
|
||||||
self._logger.info(
|
self._scan_logger.info(
|
||||||
f"Skipping {self._stage.position} as it is {round(self._background_detect.background_fraction(), 0)}% background."
|
f"Skipping {self._stage.position} as it is {round(self._background_detect.background_fraction(), 0)}% background."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
@ -491,7 +522,7 @@ class SmartScanThing(Thing):
|
||||||
autofocus_success = self._autofocus.verify_focus_sharpness(
|
autofocus_success = self._autofocus.verify_focus_sharpness(
|
||||||
sweep_sizes=jpeg_sizes, camera=CamDep, threshold=0.92
|
sweep_sizes=jpeg_sizes, camera=CamDep, threshold=0.92
|
||||||
)
|
)
|
||||||
self._logger.info(
|
self._scan_logger.info(
|
||||||
f"We just tested the focus! Result was {autofocus_success}"
|
f"We just tested the focus! Result was {autofocus_success}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -522,12 +553,12 @@ class SmartScanThing(Thing):
|
||||||
focused_path.append(loc)
|
focused_path.append(loc)
|
||||||
break
|
break
|
||||||
if attempts >= 3:
|
if attempts >= 3:
|
||||||
self._logger.warning(
|
self._scan_logger.warning(
|
||||||
"Could not autofocus after 3 attempts."
|
"Could not autofocus after 3 attempts."
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
# if the autofocus was rejected, we return to the height of the closest successful autofocus. not perfect, but better than wandering out of focus
|
# if the autofocus was rejected, we return to the height of the closest successful autofocus. not perfect, but better than wandering out of focus
|
||||||
self._logger.info(
|
self._scan_logger.info(
|
||||||
"The focus has shifted further than we expect: retrying."
|
"The focus has shifted further than we expect: retrying."
|
||||||
)
|
)
|
||||||
self._stage.move_absolute(z=int(loc[2]))
|
self._stage.move_absolute(z=int(loc[2]))
|
||||||
|
|
@ -545,7 +576,7 @@ class SmartScanThing(Thing):
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
if thread_was_alive:
|
if thread_was_alive:
|
||||||
wait_time = time.time() - wait_start
|
wait_time = time.time() - wait_start
|
||||||
self._logger.info(
|
self._scan_logger.info(
|
||||||
f"Waited {wait_time:.1f}s for the previous capture to finish saving."
|
f"Waited {wait_time:.1f}s for the previous capture to finish saving."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -559,7 +590,7 @@ class SmartScanThing(Thing):
|
||||||
# Use ErrorCapturingThread intead of Thread. This will raise errors in the calling
|
# Use ErrorCapturingThread intead of Thread. This will raise errors in the calling
|
||||||
# thread only when join() is called, allowing us to handle this appropriately.
|
# thread only when join() is called, allowing us to handle this appropriately.
|
||||||
capture_thread = ErrorCapturingThread(
|
capture_thread = ErrorCapturingThread(
|
||||||
target=self.capture_and_save,
|
target=self._capture_and_save,
|
||||||
kwargs={
|
kwargs={
|
||||||
"acquired": acquired,
|
"acquired": acquired,
|
||||||
"jpeg_path": jpeg_path,
|
"jpeg_path": jpeg_path,
|
||||||
|
|
@ -580,7 +611,7 @@ class SmartScanThing(Thing):
|
||||||
if distance_to_site(i, true_path[0][:2]) < max_dist:
|
if distance_to_site(i, true_path[0][:2]) < max_dist:
|
||||||
temp_path.append(i)
|
temp_path.append(i)
|
||||||
else:
|
else:
|
||||||
self._logger.info(
|
self._scan_logger.info(
|
||||||
f"Rejected moving to {i} as it is out of range"
|
f"Rejected moving to {i} as it is out of range"
|
||||||
)
|
)
|
||||||
path = temp_path.copy()
|
path = temp_path.copy()
|
||||||
|
|
@ -592,23 +623,24 @@ class SmartScanThing(Thing):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.create_zip_of_scan(
|
self.create_zip_of_scan(
|
||||||
|
logger=self._scan_logger,
|
||||||
scan_name=self._ongoing_scan_name,
|
scan_name=self._ongoing_scan_name,
|
||||||
download_zip=False,
|
download_zip=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
except InvocationCancelledError:
|
except InvocationCancelledError:
|
||||||
scan_successful = False
|
scan_successful = False
|
||||||
self._logger.info("Stopping scan because it was cancelled.")
|
self._scan_logger.info("Stopping scan because it was cancelled.")
|
||||||
except NotEnoughFreeSpaceError as e:
|
except NotEnoughFreeSpaceError as e:
|
||||||
scan_successful = False
|
scan_successful = False
|
||||||
self._logger.error(
|
self._scan_logger.error(
|
||||||
f"Stopping scan to avoid filling up the disk: {e}",
|
f"Stopping scan to avoid filling up the disk: {e}",
|
||||||
exc_info=e,
|
exc_info=e,
|
||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
scan_successful = False
|
scan_successful = False
|
||||||
self._logger.error(
|
self._scan_logger.error(
|
||||||
f"The scan stopped because of an error: {e} "
|
f"The scan stopped because of an error: {e} "
|
||||||
"Attempting to stitch and archive the images acquired so far.",
|
"Attempting to stitch and archive the images acquired so far.",
|
||||||
exc_info=e,
|
exc_info=e,
|
||||||
|
|
@ -624,7 +656,7 @@ class SmartScanThing(Thing):
|
||||||
# ignore any exceptions, but if it appeared to be successful
|
# ignore any exceptions, but if it appeared to be successful
|
||||||
# we will log an error.
|
# we will log an error.
|
||||||
if scan_successful:
|
if scan_successful:
|
||||||
self._logger.error(
|
self._scan_logger.error(
|
||||||
"The scan appears to have started successfully, however the final capture raised"
|
"The scan appears to have started successfully, however the final capture raised"
|
||||||
f"the following error: {e}."
|
f"the following error: {e}."
|
||||||
"Attempting to stitch and archive images.",
|
"Attempting to stitch and archive images.",
|
||||||
|
|
@ -636,36 +668,44 @@ class SmartScanThing(Thing):
|
||||||
self._return_to_starting_position()
|
self._return_to_starting_position()
|
||||||
self._perform_final_stitch(overlap)
|
self._perform_final_stitch(overlap)
|
||||||
|
|
||||||
|
@_scan_running
|
||||||
def _return_to_starting_position(self):
|
def _return_to_starting_position(self):
|
||||||
self._logger.info("Returning to starting position.")
|
self._scan_logger.info("Returning to starting position.")
|
||||||
if self._starting_position is not None:
|
if self._starting_position is not None:
|
||||||
self._stage.move_absolute(
|
self._stage.move_absolute(
|
||||||
**self._starting_position, block_cancellation=True
|
**self._starting_position, block_cancellation=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_scan_running
|
||||||
def _perform_final_stitch(self, overlap):
|
def _perform_final_stitch(self, overlap):
|
||||||
"""Perform final stitch of the data"""
|
"""Perform final stitch of the data"""
|
||||||
|
|
||||||
if self._scan_images_taken <= 3:
|
if self._scan_images_taken <= 3:
|
||||||
self._logger.info("Not performing a stitch as 3 or fewer images taken")
|
self._scan_logger.info("Not performing a stitch as 3 or fewer images taken")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.create_zip_of_scan(
|
self.create_zip_of_scan(
|
||||||
|
logger=self._scan_logger,
|
||||||
scan_name=self._ongoing_scan_name,
|
scan_name=self._ongoing_scan_name,
|
||||||
download_zip=False,
|
download_zip=False,
|
||||||
)
|
)
|
||||||
self._logger.info("Waiting for background processes to finish...")
|
self._scan_logger.info("Waiting for background processes to finish...")
|
||||||
|
|
||||||
self.preview_stitch_wait()
|
self._preview_stitch_wait()
|
||||||
self.correlate_wait()
|
self._correlate_wait()
|
||||||
try:
|
try:
|
||||||
if self.stitch_automatically:
|
if self.stitch_automatically:
|
||||||
self._logger.info("Stitching final image (may take some time)...")
|
self._scan_logger.info("Stitching final image (may take some time)...")
|
||||||
self.stitch_scan(self._ongoing_scan_name, overlap=overlap)
|
self.stitch_scan(
|
||||||
|
logger=self._scan_logger,
|
||||||
|
scan_name=self._ongoing_scan_name,
|
||||||
|
overlap=overlap,
|
||||||
|
)
|
||||||
except SubprocessError as e:
|
except SubprocessError as e:
|
||||||
self._logger.error(f"Stitching failed: {e}", exc_info=e)
|
self._scan_logger.error(f"Stitching failed: {e}", exc_info=e)
|
||||||
|
|
||||||
def capture_and_save(
|
@_scan_running
|
||||||
|
def _capture_and_save(
|
||||||
self,
|
self,
|
||||||
acquired: Event,
|
acquired: Event,
|
||||||
jpeg_path: str,
|
jpeg_path: str,
|
||||||
|
|
@ -677,20 +717,21 @@ class SmartScanThing(Thing):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
capture_start = time.time()
|
capture_start = time.time()
|
||||||
image, metadata = self.capture_image()
|
image, metadata = self._capture_image()
|
||||||
finally:
|
finally:
|
||||||
# Ensure aquired is set even if capture fails or program will hang forever.
|
# Ensure aquired is set even if capture fails or program will hang forever.
|
||||||
acquired.set()
|
acquired.set()
|
||||||
acquisition_time = time.time()
|
acquisition_time = time.time()
|
||||||
self.save_capture(jpeg_path, image, metadata)
|
self._save_capture(jpeg_path, image, metadata)
|
||||||
save_time = time.time()
|
save_time = time.time()
|
||||||
acquisition_duration = round(acquisition_time - capture_start, 1)
|
acquisition_duration = round(acquisition_time - capture_start, 1)
|
||||||
saving_duration = round(save_time - acquisition_time, 1)
|
saving_duration = round(save_time - acquisition_time, 1)
|
||||||
self._logger.debug(
|
self._scan_logger.debug(
|
||||||
f"Acquired {jpeg_path} in {acquisition_duration}s then {saving_duration}s saving to disk"
|
f"Acquired {jpeg_path} in {acquisition_duration}s then {saving_duration}s saving to disk"
|
||||||
)
|
)
|
||||||
|
|
||||||
def capture_image(self) -> tuple[np.ndarray, dict]:
|
@_scan_running
|
||||||
|
def _capture_image(self) -> tuple[np.ndarray, dict]:
|
||||||
"""Capture an image in memory and return it with metadata
|
"""Capture an image in memory and return it with metadata
|
||||||
This will set the event `acquired` once the image has been acquired, so
|
This will set the event `acquired` once the image has been acquired, so
|
||||||
that the stage may be moved while it's saved.
|
that the stage may be moved while it's saved.
|
||||||
|
|
@ -704,7 +745,8 @@ class SmartScanThing(Thing):
|
||||||
raise CaptureError("An error occurred while capturing") from e
|
raise CaptureError("An error occurred while capturing") from e
|
||||||
return image, metadata
|
return image, metadata
|
||||||
|
|
||||||
def save_capture(
|
@_scan_running
|
||||||
|
def _save_capture(
|
||||||
self,
|
self,
|
||||||
jpeg_path: str,
|
jpeg_path: str,
|
||||||
image: np.ndarray,
|
image: np.ndarray,
|
||||||
|
|
@ -725,7 +767,7 @@ class SmartScanThing(Thing):
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
piexif.insert(piexif.dump(exif_dict), jpeg_path)
|
piexif.insert(piexif.dump(exif_dict), jpeg_path)
|
||||||
except:
|
except:
|
||||||
self._logger.warning(f"Failed to add metadata to {jpeg_path}")
|
self._scan_logger.warning(f"Failed to add metadata to {jpeg_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IOError(f"An error occurred while saving {jpeg_path}") from e
|
raise IOError(f"An error occurred while saving {jpeg_path}") from e
|
||||||
|
|
||||||
|
|
@ -924,16 +966,18 @@ class SmartScanThing(Thing):
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise HTTPException(404, "File not found")
|
raise HTTPException(404, "File not found")
|
||||||
|
|
||||||
def preview_stitch_start(self) -> None:
|
@_scan_running
|
||||||
|
def _preview_stitch_start(self) -> None:
|
||||||
"""Start stitching a preview of the scan in a background subprocess
|
"""Start stitching a preview of the scan in a background subprocess
|
||||||
|
|
||||||
|
|
||||||
This uses popen and returns immediately
|
This uses popen and returns immediately
|
||||||
|
|
||||||
- self._preview_stitch_popen holds the popen for polling
|
- self._preview_stitch_popen holds the popen for polling
|
||||||
- self._preview_stitch_popen_lock is a lock aquired while interacting
|
- self._preview_stitch_popen_lock is a lock aquired while interacting
|
||||||
with Popen
|
with Popen
|
||||||
"""
|
"""
|
||||||
if self.preview_stitch_running():
|
if self._preview_stitch_running():
|
||||||
raise RuntimeError("Only one subprocess is allowed at a time")
|
raise RuntimeError("Only one subprocess is allowed at a time")
|
||||||
with self._preview_stitch_popen_lock:
|
with self._preview_stitch_popen_lock:
|
||||||
self._preview_stitch_popen = Popen(
|
self._preview_stitch_popen = Popen(
|
||||||
|
|
@ -945,7 +989,8 @@ class SmartScanThing(Thing):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def preview_stitch_running(self) -> bool:
|
@_scan_running
|
||||||
|
def _preview_stitch_running(self) -> bool:
|
||||||
"""Whether there is a preview stitch running in a subprocess"""
|
"""Whether there is a preview stitch running in a subprocess"""
|
||||||
with self._preview_stitch_popen_lock:
|
with self._preview_stitch_popen_lock:
|
||||||
if self._preview_stitch_popen is None:
|
if self._preview_stitch_popen is None:
|
||||||
|
|
@ -954,14 +999,16 @@ class SmartScanThing(Thing):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def preview_stitch_wait(self):
|
@_scan_running
|
||||||
if self.preview_stitch_running():
|
def _preview_stitch_wait(self):
|
||||||
|
if self._preview_stitch_running():
|
||||||
with self._preview_stitch_popen_lock:
|
with self._preview_stitch_popen_lock:
|
||||||
self._preview_stitch_popen.wait()
|
self._preview_stitch_popen.wait()
|
||||||
|
|
||||||
def correlate_start(self, overlap: float = 0.1) -> None:
|
@_scan_running
|
||||||
|
def _correlate_start(self, overlap: float = 0.1) -> None:
|
||||||
"""Start stitching a preview of the scan in a subprocess"""
|
"""Start stitching a preview of the scan in a subprocess"""
|
||||||
if self.correlate_running():
|
if self._correlate_running():
|
||||||
raise RuntimeError("Only one subprocess is allowed at a time")
|
raise RuntimeError("Only one subprocess is allowed at a time")
|
||||||
with self._correlate_popen_lock:
|
with self._correlate_popen_lock:
|
||||||
self._correlate_popen = Popen(
|
self._correlate_popen = Popen(
|
||||||
|
|
@ -975,7 +1022,8 @@ class SmartScanThing(Thing):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def correlate_running(self) -> bool:
|
@_scan_running
|
||||||
|
def _correlate_running(self) -> bool:
|
||||||
"""Whether there is a preview stitch running in a subprocess"""
|
"""Whether there is a preview stitch running in a subprocess"""
|
||||||
with self._correlate_popen_lock:
|
with self._correlate_popen_lock:
|
||||||
if self._correlate_popen is None:
|
if self._correlate_popen is None:
|
||||||
|
|
@ -984,28 +1032,28 @@ class SmartScanThing(Thing):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def correlate_wait(self):
|
@_scan_running
|
||||||
if self.correlate_running():
|
def _correlate_wait(self):
|
||||||
|
if self._correlate_running():
|
||||||
with self._correlate_popen_lock:
|
with self._correlate_popen_lock:
|
||||||
self._correlate_popen.wait()
|
self._correlate_popen.wait()
|
||||||
|
|
||||||
def run_subprocess(
|
def run_subprocess(
|
||||||
self,
|
self,
|
||||||
|
logger: InvocationLogger,
|
||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
) -> CompletedProcess:
|
) -> CompletedProcess:
|
||||||
"""Run a subprocess and log any output"""
|
"""Run a subprocess and log any output"""
|
||||||
self._logger.info(f"Running command in subprocess: `{' '.join(cmd)}`")
|
logger.info(f"Running command in subprocess: `{' '.join(cmd)}`")
|
||||||
|
|
||||||
p = Popen(cmd, stdout=PIPE, stderr=STDOUT, bufsize=1, universal_newlines=True)
|
p = Popen(cmd, stdout=PIPE, stderr=STDOUT, bufsize=1, universal_newlines=True)
|
||||||
os.set_blocking(p.stdout.fileno(), False)
|
os.set_blocking(p.stdout.fileno(), False)
|
||||||
self._logger.info(
|
logger.info(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))
|
||||||
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
|
|
||||||
)
|
|
||||||
while p.poll() is None:
|
while p.poll() is None:
|
||||||
try:
|
try:
|
||||||
output = p.stdout.readline()
|
output = p.stdout.readline()
|
||||||
if output != "" and output is not None:
|
if output != "" and output is not None:
|
||||||
self._logger.info(output)
|
logger.info(output)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -1013,20 +1061,25 @@ class SmartScanThing(Thing):
|
||||||
try:
|
try:
|
||||||
output = p.stdout.readline()
|
output = p.stdout.readline()
|
||||||
if output != "" and output is not None:
|
if output != "" and output is not None:
|
||||||
self._logger.info(output)
|
logger.info(output)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self._logger.info("Stitching complete")
|
logger.info("Stitching complete")
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def stitch_scan(
|
def stitch_scan(
|
||||||
self,
|
self,
|
||||||
|
logger: InvocationLogger,
|
||||||
scan_name: str,
|
scan_name: str,
|
||||||
overlap: float = 0.0,
|
overlap: float = 0.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate a stitched image based on stage position metadata"""
|
"""Generate a stitched image based on stage position metadata
|
||||||
|
|
||||||
|
Note that as this is a thing_action it needs the logger passed as
|
||||||
|
a variable if called from another thing action
|
||||||
|
"""
|
||||||
images_folder = self.images_dir_for_scan(scan_name=scan_name)
|
images_folder = self.images_dir_for_scan(scan_name=scan_name)
|
||||||
|
|
||||||
if self.stitch_tiff:
|
if self.stitch_tiff:
|
||||||
|
|
@ -1038,11 +1091,12 @@ class SmartScanThing(Thing):
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(images_folder, "scan_inputs.json")) as data_file:
|
with open(os.path.join(images_folder, "scan_inputs.json")) as data_file:
|
||||||
data_loaded = json.load(data_file)
|
data_loaded = json.load(data_file)
|
||||||
self._logger.info(data_loaded)
|
logger.info(data_loaded)
|
||||||
overlap = data_loaded["overlap"]
|
overlap = data_loaded["overlap"]
|
||||||
except:
|
except:
|
||||||
overlap = 0.1
|
overlap = 0.1
|
||||||
self.run_subprocess(
|
self.run_subprocess(
|
||||||
|
logger,
|
||||||
[
|
[
|
||||||
self._stitching_script,
|
self._stitching_script,
|
||||||
"--stitching_mode",
|
"--stitching_mode",
|
||||||
|
|
@ -1057,10 +1111,15 @@ class SmartScanThing(Thing):
|
||||||
@thing_action
|
@thing_action
|
||||||
def create_zip_of_scan(
|
def create_zip_of_scan(
|
||||||
self,
|
self,
|
||||||
|
logger: InvocationLogger,
|
||||||
scan_name: str,
|
scan_name: str,
|
||||||
download_zip=True,
|
download_zip=True,
|
||||||
) -> ZipBlob:
|
) -> ZipBlob:
|
||||||
"""Generate a zip file that can be downloaded, with all the scan files in it."""
|
"""Generate a zip file that can be downloaded, with all the scan files in it.
|
||||||
|
|
||||||
|
Note that as this is a thing_action it needs the logger passed as
|
||||||
|
a variable if called from another thing action
|
||||||
|
"""
|
||||||
images_folder = self.images_dir_for_scan(scan_name=scan_name)
|
images_folder = self.images_dir_for_scan(scan_name=scan_name)
|
||||||
scan_folder = self.dir_for_scan(scan_name=scan_name)
|
scan_folder = self.dir_for_scan(scan_name=scan_name)
|
||||||
|
|
||||||
|
|
@ -1111,7 +1170,7 @@ class SmartScanThing(Thing):
|
||||||
elif ".zip" in file or "raw" in file:
|
elif ".zip" in file or "raw" in file:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self._logger.info(f"appending {file} to zip")
|
logger.info(f"appending {file} to zip")
|
||||||
zip.write(os.path.join(scan_folder, file), arcname=file)
|
zip.write(os.path.join(scan_folder, file), arcname=file)
|
||||||
|
|
||||||
# Promote key files to the top level of the zip only at the end of the scan (when downloading)
|
# Promote key files to the top level of the zip only at the end of the scan (when downloading)
|
||||||
|
|
@ -1122,13 +1181,13 @@ class SmartScanThing(Thing):
|
||||||
for fname in ["stitched_from_stage.jpg", stitch_name, tiff_name]:
|
for fname in ["stitched_from_stage.jpg", stitch_name, tiff_name]:
|
||||||
fpath = os.path.join(images_folder, fname)
|
fpath = os.path.join(images_folder, fname)
|
||||||
if os.path.exists(fpath):
|
if os.path.exists(fpath):
|
||||||
self._logger.info(f"copying {fpath} to upper level")
|
logger.info(f"copying {fpath} to upper level")
|
||||||
zip.write(fpath, arcname=fname)
|
zip.write(fpath, arcname=fname)
|
||||||
for file in files:
|
for file in files:
|
||||||
if any(banned_name in file for banned_name in files_to_delay):
|
if any(banned_name in file for banned_name in files_to_delay):
|
||||||
self._logger.info(f"we are finally adding {file} into zip")
|
logger.info(f"we are finally adding {file} into zip")
|
||||||
zip.write(os.path.join(scan_folder, file), arcname=file)
|
zip.write(os.path.join(scan_folder, file), arcname=file)
|
||||||
self._logger.info("about to download zip")
|
logger.info("about to download zip")
|
||||||
return ZipBlob.from_file(zip_fname)
|
return ZipBlob.from_file(zip_fname)
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue