Further tidying of scan directory variables

This commit is contained in:
Julian Stirling 2025-04-10 13:24:11 +01:00
parent 90cfce146d
commit a5d55c95b8
2 changed files with 65 additions and 53 deletions

View file

@ -148,6 +148,7 @@ DOWNLOADABLE_SCAN_FILES = ("images.zip",)
JPEGBlob = blob_type("image/jpeg") JPEGBlob = blob_type("image/jpeg")
ZipBlob = blob_type("application/zip") ZipBlob = blob_type("application/zip")
IMG_DIR_NAME = "images"
class SmartScanThing(Thing): class SmartScanThing(Thing):
@ -260,7 +261,7 @@ class SmartScanThing(Thing):
@property @property
def base_scan_dir(self) -> str: def base_scan_dir(self) -> str:
"""This folder will hold all the scans we do.""" """This directory will hold all the scans we do."""
# TODO: This should be determined using sensible configuration. # TODO: This should be determined using sensible configuration.
# If the working directory is `/var/openflexure` this will result # If the working directory is `/var/openflexure` this will result
# in scans being saved at `/var/openflexure/scans/` # in scans being saved at `/var/openflexure/scans/`
@ -271,19 +272,28 @@ class SmartScanThing(Thing):
"""The name of the last scan to be started.""" """The name of the last scan to be started."""
return self._latest_scan_name return self._latest_scan_name
def dir_for_scan_name(self, scan_name: Optional[str] = None): def dir_for_scan(self, scan_name: str):
"""The path to the scan folder with a given name""" """The path to the scan directory for scan with input name"""
if not scan_name:
if not self.latest_scan_name:
raise IOError("There is no latest scan to return")
scan_name = self.latest_scan_name
return os.path.join(self.base_scan_dir, scan_name) return os.path.join(self.base_scan_dir, scan_name)
def images_dir_for_scan(self, scan_name: str) -> str:
"""The path to theimages directory for scan with input name"""
scan_dir = self.dir_for_scan(scan_name=scan_name)
return os.path.join(scan_dir, IMG_DIR_NAME)
@property @property
def _ongoing_scan_folder(self): def _ongoing_scan_dir(self):
"""For the ongoing scan this returns the scan directory"""
if not self._ongoing_scan_name: if not self._ongoing_scan_name:
return None raise RuntimeError("Cannot get ongoing scan name while no scan is running")
return self.dir_for_scan_name(self._ongoing_scan_name) return self.dir_for_scan(self._ongoing_scan_name)
@property
def _ongoing_scan_images_dir(self):
"""For the ongoing scan this returns the scan directory"""
if not self._ongoing_scan_name:
raise RuntimeError("Cannot get ongoing scan name while no scan is running")
return self.images_dir_for_scan(self._ongoing_scan_name)
def _get_uniuqe_scan_name_and_dir(self, scan_name: str) -> str: def _get_uniuqe_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
@ -301,7 +311,7 @@ class SmartScanThing(Thing):
Returns the scan name. Returns the scan name.
The directory can be accessed by self.dir_for_scan_name(unique_scan_name) The directory can be accessed by self.dir_for_scan(unique_scan_name)
""" """
if not os.path.exists(self.base_scan_dir): if not os.path.exists(self.base_scan_dir):
@ -314,7 +324,7 @@ class SmartScanThing(Thing):
for j in range(9999): for j in range(9999):
trial_unique_scan_name = f"{scan_name}_{j:04}" trial_unique_scan_name = f"{scan_name}_{j:04}"
trial_dir = self.dir_for_scan_name(trial_unique_scan_name) trial_dir = self.dir_for_scan(trial_unique_scan_name)
if not os.path.exists(trial_dir): if not os.path.exists(trial_dir):
os.makedirs(trial_dir) os.makedirs(trial_dir)
# If we made the directory this is the scan name # If we made the directory this is the scan name
@ -357,11 +367,9 @@ class SmartScanThing(Thing):
capture_thread = None capture_thread = None
try: try:
images_folder = os.path.join(self._ongoing_scan_folder, "images") os.mkdir(self._ongoing_scan_images_dir)
os.mkdir(images_folder)
raw_images_folder = os.path.join(images_folder, "raw") self._logger.info(f"Saving images to {self._ongoing_scan_images_dir}")
os.mkdir(raw_images_folder)
self._logger.info(f"Saving images to {images_folder}")
max_dist = self.max_range max_dist = self.max_range
@ -423,14 +431,15 @@ class SmartScanThing(Thing):
"skipping background": self.skip_background, "skipping background": self.skip_background,
} }
with open( scan_inputs_fname = os.path.join(
os.path.join(images_folder, "scan_inputs.json"), "w", encoding="utf-8" self._ongoing_scan_images_dir, "scan_inputs.json"
) as f: )
with open(scan_inputs_fname, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4) json.dump(data, f, ensure_ascii=False, indent=4)
if self._scan_images_taken != 0: if self._scan_images_taken != 0:
raise RuntimeError( raise RuntimeError(
"_scan_images_taken fhould be zero before starting scanning" "_scan_images_taken should be zero before starting scanning"
) )
# At the start of the loop, we simultaneously capture an image and move to the next scan point. # At the start of the loop, we simultaneously capture an image and move to the next scan point.
# 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
@ -438,12 +447,12 @@ class SmartScanThing(Thing):
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(images_folder) 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(images_folder, overlap=overlap) self.correlate_start(overlap=overlap)
ensure_free_disk_space(self._ongoing_scan_folder) ensure_free_disk_space(self._ongoing_scan_dir)
# Check if the image is background # Check if the image is background
if self.skip_background: if self.skip_background:
@ -544,7 +553,7 @@ class SmartScanThing(Thing):
self._scan_images_taken += 1 self._scan_images_taken += 1
acquired = Event() acquired = Event()
name = f"image_{loc[0]}_{loc[1]}.jpg" name = f"image_{loc[0]}_{loc[1]}.jpg"
jpeg_path = os.path.join(images_folder, name) jpeg_path = os.path.join(self._ongoing_scan_images_dir, name)
time.sleep(0.2) time.sleep(0.2)
# Use ErrorCapturingThread intead of Thread. This will raise errors in the calling # Use ErrorCapturingThread intead of Thread. This will raise errors in the calling
@ -650,11 +659,9 @@ class SmartScanThing(Thing):
self.preview_stitch_wait() self.preview_stitch_wait()
self.correlate_wait() self.correlate_wait()
try: try:
if self._ongoing_scan_folder and self.stitch_automatically: if self.stitch_automatically:
self._logger.info("Stitching final image (may take some time)...") self._logger.info("Stitching final image (may take some time)...")
self.stitch_scan( self.stitch_scan(self._ongoing_scan_name, overlap=overlap)
os.path.basename(self._ongoing_scan_folder), overlap=overlap
)
except SubprocessError as e: except SubprocessError as e:
self._logger.error(f"Stitching failed: {e}", exc_info=e) self._logger.error(f"Stitching failed: {e}", exc_info=e)
@ -795,7 +802,7 @@ class SmartScanThing(Thing):
for f in os.listdir(self.base_scan_dir): for f in os.listdir(self.base_scan_dir):
path = os.path.join(self.base_scan_dir, f) path = os.path.join(self.base_scan_dir, f)
if os.path.isdir(path): if os.path.isdir(path):
images_folder = os.path.join(path, "images") images_folder = os.path.join(path, IMG_DIR_NAME)
if os.path.isdir(images_folder): if os.path.isdir(images_folder):
number_of_images = len(os.listdir(images_folder)) number_of_images = len(os.listdir(images_folder))
else: else:
@ -871,14 +878,22 @@ class SmartScanThing(Thing):
for scan in self.scans: for scan in self.scans:
self.delete_scan(scan.name) self.delete_scan(scan.name)
def images_folder(self, scan_name: Optional[str] = None) -> str:
scan_folder = self.dir_for_scan_name(scan_name=scan_name)
return os.path.join(scan_folder, "images")
@property @property
def latest_preview_stitch_path(self): def latest_preview_stitch_path(self):
"""The path of the latest preview stitched image""" """Return the path of the latest preview stitched image
return os.path.join(self.images_folder(), "stitched_from_stage.jpg")
If the image cannot be found (because there is no latest
scan, or because the latest scan has no preview stitch) then
raise FileNotFoundError
"""
if not self.latest_scan_name:
raise FileNotFoundError("No latest scan found")
images_dir = self.images_dir_for_scan(self.latest_scan_name)
stitch_path = os.path.join(images_dir, "stitched_from_stage.jpg")
if not os.path.isfile(stitch_path):
raise FileNotFoundError("Latest scan has no preview stitch")
return stitch_path
@thing_property @thing_property
def latest_preview_stitch_time(self) -> Optional[datetime]: def latest_preview_stitch_time(self) -> Optional[datetime]:
@ -887,12 +902,9 @@ class SmartScanThing(Thing):
This will return `null` if there is no preview image to return. This will return `null` if there is no preview image to return.
""" """
try: try:
fpath = self.latest_preview_stitch_path return os.path.getmtime(self.latest_preview_stitch_path)
if os.path.exists(fpath): except FileNotFoundError:
return os.path.getmtime(fpath)
except IOError:
return None return None
return None
@fastapi_endpoint( @fastapi_endpoint(
"get", "get",
@ -907,12 +919,12 @@ class SmartScanThing(Thing):
) )
def get_latest_preview(self) -> FileResponse: def get_latest_preview(self) -> FileResponse:
"""Retrieve the latest preview image.""" """Retrieve the latest preview image."""
path = self.latest_preview_stitch_path try:
if not os.path.isfile(path): return FileResponse(self.latest_preview_stitch_path)
except FileNotFoundError:
raise HTTPException(404, "File not found") raise HTTPException(404, "File not found")
return FileResponse(path)
def preview_stitch_start(self, images_folder: str) -> None: def preview_stitch_start(self) -> None:
"""Start stitching a preview of the scan in a backgroun subprocess """Start stitching a preview of the scan in a backgroun subprocess
This uses popen and returns immediatly This uses popen and returns immediatly
@ -929,7 +941,7 @@ class SmartScanThing(Thing):
self._stitching_script, self._stitching_script,
"--stitching_mode", "--stitching_mode",
"only_stage_stitch", "only_stage_stitch",
images_folder, self._ongoing_scan_images_dir,
] ]
) )
@ -947,7 +959,7 @@ class SmartScanThing(Thing):
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, images_folder: str, overlap: float = 0.1) -> None: 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")
@ -959,7 +971,7 @@ class SmartScanThing(Thing):
"only_correlate", "only_correlate",
"--minimum_overlap", "--minimum_overlap",
f"{round(overlap * 0.9, 2)}", f"{round(overlap * 0.9, 2)}",
images_folder, self._ongoing_scan_images_dir,
] ]
) )
@ -1011,11 +1023,11 @@ class SmartScanThing(Thing):
@thing_action @thing_action
def stitch_scan( def stitch_scan(
self, self,
scan_name: Optional[str] = None, 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"""
images_folder = self.images_folder(scan_name=scan_name) images_folder = self.images_dir_for_scan(scan_name=scan_name)
if self.stitch_tiff: if self.stitch_tiff:
tiff_arg = "--stitch_tiff" tiff_arg = "--stitch_tiff"
@ -1049,8 +1061,8 @@ class SmartScanThing(Thing):
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."""
images_folder = self.images_folder(scan_name=scan_name) images_folder = self.images_dir_for_scan(scan_name=scan_name)
scan_folder = self.dir_for_scan_name(scan_name=scan_name) scan_folder = self.dir_for_scan(scan_name=scan_name)
if not os.path.isdir(images_folder): if not os.path.isdir(images_folder):
raise FileNotFoundError( raise FileNotFoundError(

View file

@ -37,7 +37,7 @@ class Stitcher(Thing):
def images_folder( def images_folder(
self, smart_scan: SmartScanDep, scan_name: Optional[str] = None self, smart_scan: SmartScanDep, scan_name: Optional[str] = None
) -> str: ) -> str:
scan_folder = smart_scan.dir_for_scan_name(scan_name=scan_name) scan_folder = smart_scan.dir_for_scan(scan_name=scan_name)
return os.path.join(scan_folder, "images") return os.path.join(scan_folder, "images")
@staticmethod @staticmethod