Improved error handling and zip download

I've swapped out various `logging.exception` calls for
`logging.error` with `exc_info` set to the exception.
My reading of the docs is that the two shoulld be equivalent,
but the former fails with an error related to
message formatting, as if there was an extra (or missing) '%'.

I've changed the format of the downloaded zip file, so that it
now puts the stitched images at the top level (if they exist).

I've also modified the time of the last stitched image so it
returns None if it doesn't exist - this avoids HTTP errors in
the client.
This commit is contained in:
Richard Bowman 2024-01-17 13:14:43 +00:00
parent 65ae6c1f76
commit b35db02f34

View file

@ -1,4 +1,5 @@
import shutil import shutil
import zipfile
import threading import threading
from typing import Mapping, Optional from typing import Mapping, Optional
import cv2 import cv2
@ -312,6 +313,9 @@ DOWNLOADABLE_SCAN_FILES = (
class JPEGBlob(BlobOutput): class JPEGBlob(BlobOutput):
media_type = "image/jpeg" media_type = "image/jpeg"
class ZipBlob(BlobOutput):
media_type = "application/zip"
class SmartScanThing(Thing): class SmartScanThing(Thing):
def __init__(self, path_to_openflexure_stitch: str): def __init__(self, path_to_openflexure_stitch: str):
self._script = path_to_openflexure_stitch self._script = path_to_openflexure_stitch
@ -425,6 +429,11 @@ class SmartScanThing(Thing):
r = cam.grab_jpeg() r = cam.grab_jpeg()
arr = np.array(Image.open(r.open())) arr = np.array(Image.open(r.open()))
if csm.image_resolution is None:
raise RuntimeError(
"Camera-stage mapping is not calibrated. This is required before "
"scans can be carried out."
)
if list(arr.shape[:2]) != [int(i) for i in csm.image_resolution]: if list(arr.shape[:2]) != [int(i) for i in csm.image_resolution]:
logger.error( 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 "
@ -593,15 +602,17 @@ class SmartScanThing(Thing):
except InvocationCancelledError: except InvocationCancelledError:
logger.error("Stopping scan because it was cancelled.") logger.error("Stopping scan because it was cancelled.")
except NotEnoughFreeSpaceError as e: except NotEnoughFreeSpaceError as e:
logger.exception( 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,
) )
raise e raise e
except Exception as e: except Exception as e:
logger.exception( logger.error(
f"The scan stopped because of an error: {e}", f"The scan stopped because of an error: {e}",
"We will attempt to stitch and archive the images acquired " "We will attempt to stitch and archive the images acquired "
"so far." "so far.",
exc_info=e,
) )
raise e raise e
finally: finally:
@ -614,17 +625,12 @@ class SmartScanThing(Thing):
logger.info("Waiting for background processes to finish...") logger.info("Waiting for background processes to finish...")
self.preview_stitch_wait() self.preview_stitch_wait()
self.correlate_wait() self.correlate_wait()
logger.info("Stitching final image (may take some time)...")
try: try:
if scan_folder and self.stitch_automatically: if scan_folder and self.stitch_automatically:
logger.info("Stitching final image (may take some time)...")
self.stitch_scan(logger, os.path.basename(scan_folder), overlap=overlap) self.stitch_scan(logger, os.path.basename(scan_folder), overlap=overlap)
except SubprocessError as e: except SubprocessError as e:
logger.exception(f"Stitching failed: {e}") logger.error(f"Stitching failed: {e}", exc_info=e)
logger.info("Creating zip archive of images (may take some time)...")
if images_folder and os.path.isdir(images_folder):
shutil.make_archive(
os.path.join(scan_folder, "images"), "zip", images_folder
)
@thing_property @thing_property
def max_range(self) -> int: def max_range(self) -> int:
@ -777,12 +783,17 @@ class SmartScanThing(Thing):
@thing_property @thing_property
def latest_preview_stitch_time(self) -> Optional[datetime]: def latest_preview_stitch_time(self) -> Optional[datetime]:
"""The modification time of the latest preview image""" """The modification time of the latest preview image
fpath = self.latest_preview_stitch_path
if not os.path.exists(fpath): This will return `null` if there is no preview image to return.
"""
try:
fpath = self.latest_preview_stitch_path
if os.path.exists(fpath):
return os.path.getmtime(fpath)
except IOError:
return None return None
else: return None
return os.path.getmtime(fpath)
@fastapi_endpoint( @fastapi_endpoint(
"get", "get",
@ -869,3 +880,32 @@ class SmartScanThing(Thing):
images_folder = self.images_folder(scan_name=scan_name) images_folder = self.images_folder(scan_name=scan_name)
self.run_subprocess(logger, [self._script, "--stitching_mode", "all", "--minimum_overlap", f"{overlap*0.9}", images_folder]) self.run_subprocess(logger, [self._script, "--stitching_mode", "all", "--minimum_overlap", f"{overlap*0.9}", images_folder])
@thing_action
def create_zip_of_scan(self, logger: InvocationLogger, scan_name: Optional[str]=None) -> ZipBlob:
"""Generate a zip file that can be downloaded, with all the scan files in it."""
images_folder = self.images_folder(scan_name=scan_name)
scan_folder = self.scan_folder_path(scan_name=scan_name)
if scan_folder != os.path.dirname(images_folder) or os.path.basename(images_folder) != "images":
logger.error(
"There is a problem with filenames, the archive may be incorrect."
f"scan_folder: {scan_folder}, images_folder: {images_folder}."
)
if not os.path.isdir(images_folder):
raise FileNotFoundError(f"Tried to make a zip archive of {images_folder} but it does not exist.")
logger.info("Creating zip archive of images (may take some time)...")
shutil.make_archive(
os.path.join(scan_folder, "images"),
"zip",
scan_folder,
"images/",
logger=logger
)
zip_fname = os.path.join(scan_folder, "images.zip")
# Promote key files to the top level of the zip
with zipfile.ZipFile(zip_fname, mode="a") as zip:
for fname in ["stitched_from_stage.jpg", "stitched.jpg"]:
fpath = os.path.join(images_folder, fname)
if os.path.exists(fpath):
zip.write(fpath, arcname=fname)
return ZipBlob.from_file(zip_fname)