Turn scan_data into a BaseModel with validation for custom formatting
This commit is contained in:
parent
d80d08dd83
commit
f449bcdd78
2 changed files with 181 additions and 96 deletions
|
|
@ -6,8 +6,9 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
import threading
|
import threading
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, field_validator, field_serializer
|
||||||
|
|
||||||
from openflexure_microscope_server.utilities import requires_lock
|
from openflexure_microscope_server.utilities import requires_lock
|
||||||
|
|
||||||
|
|
@ -17,6 +18,8 @@ SCAN_ZERO_PAD_DIGITS = 4
|
||||||
STITCH_REGEX = re.compile(r"stitched\.jpe?g$")
|
STITCH_REGEX = re.compile(r"stitched\.jpe?g$")
|
||||||
IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpe?g$")
|
IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpe?g$")
|
||||||
|
|
||||||
|
SCAN_DATA_FILENAME = "scan_data.json"
|
||||||
|
|
||||||
|
|
||||||
class NotEnoughFreeSpaceError(IOError):
|
class NotEnoughFreeSpaceError(IOError):
|
||||||
"""An exception raised if there is not enough free space on disk to scan."""
|
"""An exception raised if there is not enough free space on disk to scan."""
|
||||||
|
|
@ -33,6 +36,96 @@ class ScanInfo(BaseModel):
|
||||||
dzi: Optional[str]
|
dzi: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class ScanData(BaseModel):
|
||||||
|
"""Data about a scan to be saved to a JSON file in the directory.
|
||||||
|
|
||||||
|
This serialises into a human readable format where possible with
|
||||||
|
|
||||||
|
timestamps in %H_%M_%S-%d_%m_%Y format
|
||||||
|
timedeltas in %H:%M:%S format
|
||||||
|
|
||||||
|
Properties that are not known until the end have ``None`` serialised as "Unknown"
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Docs for each variable
|
||||||
|
# TODO: Make note about timestamp format. Is it too ambiguous?
|
||||||
|
scan_name: str
|
||||||
|
overlap: float
|
||||||
|
max_dist: int
|
||||||
|
dx: int
|
||||||
|
dy: int
|
||||||
|
autofocus_range: int
|
||||||
|
autofocus_on: bool
|
||||||
|
start_time: datetime
|
||||||
|
skip_background: str
|
||||||
|
stitch_automatically: str
|
||||||
|
stitch_resize: int
|
||||||
|
save_resolution: tuple[int, int]
|
||||||
|
final_image_count: Optional[int] = None
|
||||||
|
duration: Optional[timedelta] = None
|
||||||
|
scan_result: Optional[str] = None
|
||||||
|
|
||||||
|
def set_final_data(self, result: str, final_image_count: int):
|
||||||
|
"""Set the final data for the scan, scan duration is automatically calculated.
|
||||||
|
|
||||||
|
:param result: A string describing the result.
|
||||||
|
:param final_image_count: The total number of images captures.
|
||||||
|
"""
|
||||||
|
self.duration = datetime.now() - self.start_time
|
||||||
|
self.final_image_count = final_image_count
|
||||||
|
self.scan_result = result
|
||||||
|
|
||||||
|
@field_validator("start_time", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def parse_timestamp(cls, value: str | datetime) -> datetime:
|
||||||
|
"""Validate a timestamp that may be a string in Hrs_Min_Sec-Day_Month_Year format."""
|
||||||
|
if isinstance(value, str):
|
||||||
|
return datetime.strptime(value, "%H_%M_%S-%d_%m_%Y")
|
||||||
|
return value
|
||||||
|
|
||||||
|
@field_serializer("start_time")
|
||||||
|
def serialize_timestamp(self, value: datetime) -> str:
|
||||||
|
"""Serialise timestamp to Hrs_Min_Sec-Day_Month_Year format."""
|
||||||
|
return value.strftime("%H_%M_%S-%d_%m_%Y")
|
||||||
|
|
||||||
|
@field_validator("duration", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def parse_timedelta(cls, value: Optional[str | timedelta]) -> Optional[timedelta]:
|
||||||
|
"""Validate a timedelta that may be a string in Hrs:Min:Sec format or "Unknown"."""
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value == "Unknown":
|
||||||
|
return None
|
||||||
|
hrs, mins, secs = map(int, value.split(":"))
|
||||||
|
return timedelta(hours=hrs, minutes=mins, seconds=secs)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@field_serializer("duration")
|
||||||
|
def serialize_timedelta(self, value: Optional[timedelta]) -> str:
|
||||||
|
"""Serialise timedelta to Hrs:Min:Sec (or "Unknown" if None)."""
|
||||||
|
if value is None:
|
||||||
|
return "Unknown"
|
||||||
|
# Calculate the string manually rather than use str(), as this may convert to
|
||||||
|
# days for a very very long scan, and then it is much harder to parse.
|
||||||
|
total_secs = value.total_seconds()
|
||||||
|
hrs = int(total_secs // 3600)
|
||||||
|
mins = int((total_secs % 3600) // 60)
|
||||||
|
secs = int((total_secs % 60))
|
||||||
|
return f"{hrs}:{mins}:{secs}"
|
||||||
|
|
||||||
|
@field_validator("final_image_count", "scan_result", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def parse_unknown_as_none(cls, value: Optional[str | int]) -> Optional[str | int]:
|
||||||
|
"""Validate the string "Unknown" as None."""
|
||||||
|
if value == "Unknown":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
@field_serializer("final_image_count", "scan_result")
|
||||||
|
def serialize_none_as_unknown(self, value: Optional[str | int]) -> str:
|
||||||
|
"""Serialise None as "Unknown" for a more human readable result."""
|
||||||
|
return "Unknown" if value is None else str(value)
|
||||||
|
|
||||||
|
|
||||||
class ScanDirectoryManager:
|
class ScanDirectoryManager:
|
||||||
"""A class for managing interactions with scan directories."""
|
"""A class for managing interactions with scan directories."""
|
||||||
|
|
||||||
|
|
@ -111,6 +204,17 @@ class ScanDirectoryManager:
|
||||||
return None
|
return None
|
||||||
return self.get_file_path_from_img_dir(scan_name, stitch_fname)
|
return self.get_file_path_from_img_dir(scan_name, stitch_fname)
|
||||||
|
|
||||||
|
@requires_lock
|
||||||
|
def get_scan_data_path(self, scan_name: str) -> Optional[str]:
|
||||||
|
"""Return the file full path scan data JSON file.
|
||||||
|
|
||||||
|
If no scan data JSON file is found, return None
|
||||||
|
"""
|
||||||
|
scan_data_path = ScanDirectory(scan_name, self.base_dir).scan_data_path
|
||||||
|
if not os.path.isfile(scan_data_path):
|
||||||
|
return None
|
||||||
|
return scan_data_path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requires_lock
|
@requires_lock
|
||||||
def all_scans(self) -> list[str]:
|
def all_scans(self) -> list[str]:
|
||||||
|
|
@ -250,6 +354,16 @@ class ScanDirectory:
|
||||||
return im_path
|
return im_path
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scan_data_path(self) -> Optional[str]:
|
||||||
|
"""The path to the scan data json file for this directory.
|
||||||
|
|
||||||
|
Returns None if there is no images dir to write to.
|
||||||
|
"""
|
||||||
|
if self.images_dir is not None:
|
||||||
|
return os.path.join(self.images_dir, SCAN_DATA_FILENAME)
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_time(self) -> float:
|
def created_time(self) -> float:
|
||||||
"""The time the directory was created on disk."""
|
"""The time the directory was created on disk."""
|
||||||
|
|
@ -341,6 +455,16 @@ class ScanDirectory:
|
||||||
files.append(os.path.relpath(full_path, self.dir_path))
|
files.append(os.path.relpath(full_path, self.dir_path))
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
def save_scan_data(self, scan_data: ScanData):
|
||||||
|
"""Save the scan data for this scan to disk."""
|
||||||
|
if self.scan_data_path is None:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"There is no images directory to save scan data into."
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(self.scan_data_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(scan_data.model_dump_json(indent=4))
|
||||||
|
|
||||||
def zip_files(self, final_version: bool = False) -> str:
|
def zip_files(self, final_version: bool = False) -> str:
|
||||||
"""Zips any images from the scan not yet zipped, return full path to zip.
|
"""Zips any images from the scan not yet zipped, return full path to zip.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import threading
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
|
||||||
from subprocess import CompletedProcess, Popen, PIPE, SubprocessError, STDOUT
|
from subprocess import CompletedProcess, Popen, PIPE, SubprocessError, STDOUT
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
@ -40,7 +39,7 @@ AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocu
|
||||||
JPEGBlob = lt.blob.blob_type("image/jpeg")
|
JPEGBlob = lt.blob.blob_type("image/jpeg")
|
||||||
ZipBlob = lt.blob.blob_type("application/zip")
|
ZipBlob = lt.blob.blob_type("application/zip")
|
||||||
|
|
||||||
SCAN_DATA_FILENAME = "scan_data.json"
|
|
||||||
STITCHING_CMD = "openflexure-stitch"
|
STITCHING_CMD = "openflexure-stitch"
|
||||||
|
|
||||||
STITCHING_RESOLUTION = (820, 616)
|
STITCHING_RESOLUTION = (820, 616)
|
||||||
|
|
@ -111,7 +110,7 @@ class SmartScanThing(lt.Thing):
|
||||||
self._capture_thread: Optional[ErrorCapturingThread] = None
|
self._capture_thread: Optional[ErrorCapturingThread] = None
|
||||||
self._scan_images_taken: Optional[int] = None
|
self._scan_images_taken: Optional[int] = None
|
||||||
# TODO Scan data is a dict during refactoring, should become a dataclass
|
# TODO Scan data is a dict during refactoring, should become a dataclass
|
||||||
self._scan_data: Optional[dict] = None
|
self._scan_data: Optional[scan_directories.ScanData] = None
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def sample_scan(
|
def sample_scan(
|
||||||
|
|
@ -141,6 +140,7 @@ class SmartScanThing(lt.Thing):
|
||||||
self._autofocus = autofocus
|
self._autofocus = autofocus
|
||||||
self._stage = stage
|
self._stage = stage
|
||||||
self._cam = cam
|
self._cam = cam
|
||||||
|
# TODO check if metadata_getter this can be removed without error?
|
||||||
self._metadata_getter = metadata_getter
|
self._metadata_getter = metadata_getter
|
||||||
self._csm = csm
|
self._csm = csm
|
||||||
self._capture_thread = None
|
self._capture_thread = None
|
||||||
|
|
@ -280,14 +280,11 @@ class SmartScanThing(lt.Thing):
|
||||||
return dx, dy
|
return dx, dy
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _set_scan_data(self):
|
def _collect_scan_data(self) -> scan_directories.ScanData:
|
||||||
"""Set date for this scan to the ``self._scan_data`` variable.
|
"""Collect and return the data for this scan so it cannot be changed mid-scan."""
|
||||||
|
|
||||||
This needs to become a dataclass at some point.
|
|
||||||
"""
|
|
||||||
overlap = self.overlap
|
overlap = self.overlap
|
||||||
dx, dy = self._calc_displacement_from_test_image(overlap)
|
dx, dy = self._calc_displacement_from_test_image(overlap)
|
||||||
stitch_resize = STITCHING_RESOLUTION[0] / self.save_resolution[0]
|
stitch_resize = STITCHING_RESOLUTION[0] // self.save_resolution[0]
|
||||||
|
|
||||||
self._scan_logger.debug(
|
self._scan_logger.debug(
|
||||||
f"Resizing images when stitching by a factor of {stitch_resize}"
|
f"Resizing images when stitching by a factor of {stitch_resize}"
|
||||||
|
|
@ -308,78 +305,33 @@ class SmartScanThing(lt.Thing):
|
||||||
autofocus_dz = 0
|
autofocus_dz = 0
|
||||||
|
|
||||||
# Fix scan parameters in case UI is updated during scan.
|
# Fix scan parameters in case UI is updated during scan.
|
||||||
self._scan_data = {
|
return scan_directories.ScanData(
|
||||||
"scan_name": self._ongoing_scan.name,
|
scan_name=self._ongoing_scan.name,
|
||||||
"overlap": overlap,
|
overlap=overlap,
|
||||||
"max_dist": self.max_range,
|
max_dist=self.max_range,
|
||||||
"dx": dx,
|
dx=dx,
|
||||||
"dy": dy,
|
dy=dy,
|
||||||
"autofocus_dz": autofocus_dz,
|
autofocus_dz=autofocus_dz,
|
||||||
"autofocus_on": bool(autofocus_dz),
|
autofocus_on=bool(autofocus_dz),
|
||||||
"start_time": time.strftime("%H_%M_%S-%d_%m_%Y"),
|
start_time=time.strftime("%H_%M_%S-%d_%m_%Y"),
|
||||||
"skip_background": self.skip_background,
|
skip_background=self.skip_background,
|
||||||
"stitch_automatically": self.stitch_automatically,
|
stitch_automatically=self.stitch_automatically,
|
||||||
"stitch_resize": stitch_resize,
|
stitch_resize=stitch_resize,
|
||||||
"save_resolution": self.save_resolution,
|
save_resolution=self.save_resolution,
|
||||||
}
|
|
||||||
|
|
||||||
@_scan_running
|
|
||||||
def _save_scan_inputs_json(self):
|
|
||||||
"""Save scan inputs as a JSON file in the scan folder.
|
|
||||||
|
|
||||||
This file allows the user to review the settings used in the scan.
|
|
||||||
"""
|
|
||||||
# Should this be a method of the scan_data dataclass?
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"scan_name": self._ongoing_scan.name,
|
|
||||||
"overlap": self._scan_data["overlap"],
|
|
||||||
"autofocus range": self._scan_data["autofocus_dz"],
|
|
||||||
"dx": self._scan_data["dx"],
|
|
||||||
"dy": self._scan_data["dy"],
|
|
||||||
"start time": self._scan_data["start_time"],
|
|
||||||
"skipping background": self._scan_data["skip_background"],
|
|
||||||
"capture resolution": self._scan_data["save_resolution"],
|
|
||||||
}
|
|
||||||
|
|
||||||
scan_inputs_fname = os.path.join(
|
|
||||||
self._ongoing_scan.images_dir, SCAN_DATA_FILENAME
|
|
||||||
)
|
)
|
||||||
with open(scan_inputs_fname, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _update_scan_data_json(self, scan_result: str):
|
def _save_final_scan_data(self, scan_result: str):
|
||||||
"""Update scan data JSON file with data only known at the end of the scan.
|
"""Update scan data JSON file with data only known at the end of the scan.
|
||||||
|
|
||||||
Takes scan_result, a string that is either "success", "cancelled by user",
|
Takes scan_result, a string that is either "success", "cancelled by user",
|
||||||
or the error that ended the scan.
|
or the error that ended the scan.
|
||||||
"""
|
"""
|
||||||
# Should this be a method of the scan_data dataclass?
|
self.scan_data.set_final_data(
|
||||||
current_time = datetime.now().replace(microsecond=0)
|
result=scan_result,
|
||||||
start_time = datetime.strptime(
|
final_image_count=self._scan_images_taken,
|
||||||
self._scan_data["start_time"], "%H_%M_%S-%d_%m_%Y"
|
|
||||||
).replace(microsecond=0)
|
|
||||||
|
|
||||||
duration = current_time - start_time
|
|
||||||
|
|
||||||
outputs = {
|
|
||||||
"image_count": self._scan_images_taken,
|
|
||||||
"duration": str(duration),
|
|
||||||
"scan_result": scan_result,
|
|
||||||
}
|
|
||||||
|
|
||||||
scan_data_fname = os.path.join(
|
|
||||||
self._ongoing_scan.images_dir, SCAN_DATA_FILENAME
|
|
||||||
)
|
)
|
||||||
|
self._ongoing_scan.save_scan_data(self._scan_data)
|
||||||
with open(scan_data_fname, encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
data.update(outputs)
|
|
||||||
|
|
||||||
with open(scan_data_fname, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _manage_stitching_threads(self):
|
def _manage_stitching_threads(self):
|
||||||
|
|
@ -388,7 +340,7 @@ class SmartScanThing(lt.Thing):
|
||||||
# well constrained.
|
# well constrained.
|
||||||
if self._scan_images_taken > 3:
|
if self._scan_images_taken > 3:
|
||||||
if not self._preview_stitch_running():
|
if not self._preview_stitch_running():
|
||||||
self._preview_stitch_start(overlap=self._scan_data["overlap"])
|
self._preview_stitch_start(overlap=self._scan_data.overlap)
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _run_scan(self):
|
def _run_scan(self):
|
||||||
|
|
@ -404,25 +356,25 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._cam.start_streaming(main_resolution=(3280, 2464))
|
self._cam.start_streaming(main_resolution=(3280, 2464))
|
||||||
self._set_scan_data()
|
self._scan_data = self._collect_scan_data()
|
||||||
self._save_scan_inputs_json()
|
self._ongoing_scan.save_scan_data(self._scan_data)
|
||||||
if self._scan_images_taken != 0:
|
if self._scan_images_taken != 0:
|
||||||
msg = "_scan_images_taken should be zero before starting scanning"
|
msg = "_scan_images_taken should be zero before starting scanning"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
# This is the main loop of the scan!
|
# This is the main loop of the scan!
|
||||||
self._main_scan_loop()
|
self._main_scan_loop()
|
||||||
self._update_scan_data_json(scan_result="success")
|
self._save_final_scan_data(scan_result="success")
|
||||||
|
|
||||||
except lt.exceptions.InvocationCancelledError:
|
except lt.exceptions.InvocationCancelledError:
|
||||||
scan_successful = False
|
scan_successful = False
|
||||||
# Reset the cancel event so it can be thrown again
|
# Reset the cancel event so it can be thrown again
|
||||||
self._cancel.clear()
|
self._cancel.clear()
|
||||||
self._scan_logger.info("Stopping scan because it was cancelled.")
|
self._scan_logger.info("Stopping scan because it was cancelled.")
|
||||||
self._update_scan_data_json(scan_result="cancelled by user")
|
self._save_final_scan_data(scan_result="cancelled by user")
|
||||||
except scan_directories.NotEnoughFreeSpaceError as e:
|
except scan_directories.NotEnoughFreeSpaceError as e:
|
||||||
scan_successful = False
|
scan_successful = False
|
||||||
self._update_scan_data_json(scan_result=str(e))
|
self._save_final_scan_data(scan_result=str(e))
|
||||||
self._scan_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,
|
||||||
|
|
@ -430,6 +382,7 @@ class SmartScanThing(lt.Thing):
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
scan_successful = False
|
scan_successful = False
|
||||||
|
self._save_final_scan_data(scan_result=str(e))
|
||||||
self._scan_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.",
|
||||||
|
|
@ -475,9 +428,9 @@ class SmartScanThing(lt.Thing):
|
||||||
# have multiple starting positions, each of which will be visited before the
|
# have multiple starting positions, each of which will be visited before the
|
||||||
# scan can end.
|
# scan can end.
|
||||||
planner_settings = {
|
planner_settings = {
|
||||||
"dx": self._scan_data["dx"],
|
"dx": self._scan_data.dx,
|
||||||
"dy": self._scan_data["dy"],
|
"dy": self._scan_data.dy,
|
||||||
"max_dist": self._scan_data["max_dist"],
|
"max_dist": self._scan_data.max_dist,
|
||||||
}
|
}
|
||||||
route_planner = scan_planners.SmartSpiral(
|
route_planner = scan_planners.SmartSpiral(
|
||||||
initial_position=(self._stage.position["x"], self._stage.position["y"]),
|
initial_position=(self._stage.position["x"], self._stage.position["y"]),
|
||||||
|
|
@ -502,7 +455,7 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
capture_image = True
|
capture_image = True
|
||||||
# If skipping background, take an image to check if current field of view is background
|
# If skipping background, take an image to check if current field of view is background
|
||||||
if self._scan_data["skip_background"]:
|
if self._scan_data.skip_background:
|
||||||
capture_image, bg_message = self._cam.image_is_sample()
|
capture_image, bg_message = self._cam.image_is_sample()
|
||||||
|
|
||||||
if not capture_image:
|
if not capture_image:
|
||||||
|
|
@ -515,8 +468,8 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
focused, focused_height = self._autofocus.run_smart_stack(
|
focused, focused_height = self._autofocus.run_smart_stack(
|
||||||
images_dir=self._ongoing_scan.images_dir,
|
images_dir=self._ongoing_scan.images_dir,
|
||||||
autofocus_dz=self._scan_data["autofocus_dz"],
|
autofocus_dz=self._scan_data.autofocus_dz,
|
||||||
save_resolution=self._scan_data["save_resolution"],
|
save_resolution=self._scan_data.save_resolution,
|
||||||
)
|
)
|
||||||
|
|
||||||
current_pos_xyz = (new_pos_xyz[0], new_pos_xyz[1], focused_height)
|
current_pos_xyz = (new_pos_xyz[0], new_pos_xyz[1], focused_height)
|
||||||
|
|
@ -552,14 +505,14 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
self._preview_stitch_wait()
|
self._preview_stitch_wait()
|
||||||
try:
|
try:
|
||||||
if self._scan_data["stitch_automatically"]:
|
if self._scan_data.stitch_automatically:
|
||||||
self._scan_logger.info("Stitching final image (may take some time)...")
|
self._scan_logger.info("Stitching final image (may take some time)...")
|
||||||
self.stitch_scan(
|
self.stitch_scan(
|
||||||
logger=self._scan_logger,
|
logger=self._scan_logger,
|
||||||
cancel=self._cancel,
|
cancel=self._cancel,
|
||||||
scan_name=self._ongoing_scan.name,
|
scan_name=self._ongoing_scan.name,
|
||||||
stitch_resize=self._scan_data["stitch_resize"],
|
stitch_resize=self._scan_data.stitch_resize,
|
||||||
overlap=self._scan_data["overlap"],
|
overlap=self._scan_data.overlap,
|
||||||
)
|
)
|
||||||
except SubprocessError as e:
|
except SubprocessError as e:
|
||||||
self._scan_logger.error(f"Stitching failed: {e}", exc_info=e)
|
self._scan_logger.error(f"Stitching failed: {e}", exc_info=e)
|
||||||
|
|
@ -798,7 +751,7 @@ class SmartScanThing(lt.Thing):
|
||||||
"--minimum_overlap",
|
"--minimum_overlap",
|
||||||
f"{min_overlap}",
|
f"{min_overlap}",
|
||||||
"--resize",
|
"--resize",
|
||||||
f"{self._scan_data['stitch_resize']}",
|
f"{self._scan_data.stitch_resize}",
|
||||||
self._ongoing_scan.images_dir,
|
self._ongoing_scan.images_dir,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
@ -885,9 +838,9 @@ class SmartScanThing(lt.Thing):
|
||||||
Note that as this is a lt.thing_action it needs the logger passed as
|
Note that as this is a lt.thing_action it needs the logger passed as
|
||||||
a variable if called from another thing action
|
a variable if called from another thing action
|
||||||
"""
|
"""
|
||||||
json_fpath = self._scan_dir_manager.get_file_path_from_img_dir(
|
# TODO tidy this function
|
||||||
scan_name=scan_name, filename=SCAN_DATA_FILENAME
|
# TODO handle if json_path is None (easier once tidied)
|
||||||
)
|
json_fpath = self._scan_dir_manager.get_scan_data_path()
|
||||||
|
|
||||||
if self.stitch_tiff:
|
if self.stitch_tiff:
|
||||||
tiff_arg = "--stitch_tiff"
|
tiff_arg = "--stitch_tiff"
|
||||||
|
|
@ -904,7 +857,7 @@ class SmartScanThing(lt.Thing):
|
||||||
# the file not being there, it not being json in the file,
|
# the file not being there, it not being json in the file,
|
||||||
# or the imported data not being indexable
|
# or the imported data not being indexable
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Couldn't read scan data, is {SCAN_DATA_FILENAME} missing or corrupt? "
|
f"Couldn't read scan data, is {json_fpath} missing or corrupt? "
|
||||||
"Attempting stitch with overlap value of 0.1"
|
"Attempting stitch with overlap value of 0.1"
|
||||||
)
|
)
|
||||||
overlap = 0.1
|
overlap = 0.1
|
||||||
|
|
@ -919,20 +872,28 @@ class SmartScanThing(lt.Thing):
|
||||||
try:
|
try:
|
||||||
with open(json_fpath, "r", encoding="utf-8") as data_file:
|
with open(json_fpath, "r", encoding="utf-8") as data_file:
|
||||||
data_loaded = json.load(data_file)
|
data_loaded = json.load(data_file)
|
||||||
save_resolution = data_loaded["capture resolution"]
|
|
||||||
|
# Handle "capture resolution" being used to store the save resolution
|
||||||
|
# in old scans.
|
||||||
|
key = (
|
||||||
|
"capture resolution"
|
||||||
|
if "capture resolution" in data_loaded
|
||||||
|
else "save_resolution"
|
||||||
|
)
|
||||||
|
save_resolution = data_loaded[key]
|
||||||
stitch_resize = STITCHING_RESOLUTION[0] / save_resolution[0]
|
stitch_resize = STITCHING_RESOLUTION[0] / save_resolution[0]
|
||||||
except (json.decoder.JSONDecodeError, FileNotFoundError, TypeError):
|
except (json.decoder.JSONDecodeError, FileNotFoundError, TypeError):
|
||||||
# As there is no schema or pydantic model this should handle
|
# As there is no schema or pydantic model this should handle
|
||||||
# the file not being there, it not being json in the file,
|
# the file not being there, it not being json in the file,
|
||||||
# or the imported data not being indexable
|
# or the imported data not being indexable
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Couldn't read scan data, is {SCAN_DATA_FILENAME} missing or corrupt? "
|
f"Couldn't read scan data, is {json_fpath} missing or corrupt? "
|
||||||
"Attempting stitch with resize value of 0.5"
|
"Attempting stitch with resize value of 0.5"
|
||||||
)
|
)
|
||||||
stitch_resize = 0.5
|
stitch_resize = 0.5
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Value for capture resolution not found in scan data. "
|
"Value for save resolution not found in scan data. "
|
||||||
"Attempting stitch with resize value of 0.5"
|
"Attempting stitch with resize value of 0.5"
|
||||||
)
|
)
|
||||||
stitch_resize = 0.5
|
stitch_resize = 0.5
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue