From 6e18a91d1fc05e936e9c917e4958583f2c22dce3 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 17 Jan 2024 11:28:36 +0000 Subject: [PATCH 01/11] Hide background detect settings in an accordion Feedback from clinicians is that this tab was too busy. This commit hides all but the two essential buttons. --- .../paneBackgroundDetect.vue | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue b/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue index 1c9ba84d..b6af91d9 100644 --- a/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue +++ b/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue @@ -4,6 +4,37 @@ The background detect Thing seems to be missing or incompatible.
+
-
- -
-
- -
-
- -
Date: Wed, 17 Jan 2024 11:53:08 +0000 Subject: [PATCH 02/11] Expose hostname in settings manager I've added a `hostname` property to the settings manager. --- .../things/settings_manager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/things/settings_manager.py b/src/openflexure_microscope_server/things/settings_manager.py index 24ed8052..7514d7f1 100644 --- a/src/openflexure_microscope_server/things/settings_manager.py +++ b/src/openflexure_microscope_server/things/settings_manager.py @@ -102,6 +102,11 @@ class SettingsManager(Thing): self.thing_settings["microscope_id"] = str(uuid4()) return UUID(self.thing_settings["microscope_id"]) + @thing_property + def hostname(self) -> str: + """The hostname of the microscope, as reported by its operating system.""" + return gethostname() + @thing_action def get_things_state(self, metadata_getter: GetThingStates) -> Mapping: """Metadata summarising the current state of all Things in the server""" @@ -119,7 +124,7 @@ class SettingsManager(Thing): @property def thing_state(self) -> Mapping: state = { - "hostname": gethostname(), + "hostname": self.hostname, "microscope-uuid": str(self.microscope_id), } metadata = self.external_metadata From 674704b0d6cfee1dbf6ee3b0d959603292c5290b Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 17 Jan 2024 11:55:03 +0000 Subject: [PATCH 03/11] Display the microscope's hostname in the interface --- webapp/src/App.vue | 7 +++++++ .../tabContentComponents/aboutComponents/statusPane.vue | 5 +++++ webapp/src/store.js | 6 +++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/webapp/src/App.vue b/webapp/src/App.vue index e1234a92..2dfbe3d0 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -321,6 +321,13 @@ export default { ); } } + try { + let hostname = await this.readThingProperty("settings", "hostname"); + this.$store.commit("changeMicroscopeHostname", hostname); + document.title = `OpenFlexure Microscope: ${hostname}`; + } catch { + this.$store.commit("changeMicroscopeHostname", null); + } this.$store.commit("setConnected"); this.$store.commit("setErrorMessage", null); } catch (error) { diff --git a/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue b/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue index 63d12f02..88c06096 100644 --- a/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue +++ b/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue @@ -2,6 +2,11 @@
+
+ Microscope hostname: +
+ {{ $store.state.microscopeHostname }} +
API origin:
diff --git a/webapp/src/store.js b/webapp/src/store.js index f2b2bf68..fa7bcd70 100644 --- a/webapp/src/store.js +++ b/webapp/src/store.js @@ -91,7 +91,8 @@ export default new Vuex.Store({ imjoyEnabled: false, galleryEnabled: true, appTheme: "system", - activeStreams: {} + activeStreams: {}, + microscopeHostname: "" }, mutations: { @@ -144,6 +145,9 @@ export default new Vuex.Store({ }, removeStream(state, id) { state.activeStreams[id] = false; + }, + changeMicroscopeHostname(state, value) { + state.microscopeHostname = value; } }, From f627d6e2f293ba925e4b0b80d2a91153253db119 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 17 Jan 2024 11:59:30 +0000 Subject: [PATCH 04/11] Always generate the preview stitch The preview stitch is useful even if stitch-as-you-go is not desired. I've enabled it all the time, not just when the box is ticked. --- src/openflexure_microscope_server/things/smart_scan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index d7f11ab4..7bf05a5e 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -564,9 +564,9 @@ class SmartScanThing(Thing): positions.append(loc[:2]) names.append(name) + if not self.preview_stitch_running(): + self.preview_stitch_start(images_folder) if self.stitch_automatically: - if not self.preview_stitch_running(): - self.preview_stitch_start(images_folder) if not self.correlate_running(): self.correlate_start(images_folder, overlap=overlap) From 65ae6c1f76bba09fcc21ec0f5c7fa542b43a36d3 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 17 Jan 2024 12:25:17 +0000 Subject: [PATCH 05/11] Removed a couple of print statements --- .../things/auto_recentre_stage.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/openflexure_microscope_server/things/auto_recentre_stage.py b/src/openflexure_microscope_server/things/auto_recentre_stage.py index 45a8cae5..0e46e97b 100644 --- a/src/openflexure_microscope_server/things/auto_recentre_stage.py +++ b/src/openflexure_microscope_server/things/auto_recentre_stage.py @@ -61,7 +61,6 @@ class RecentringThing(Thing): stage.position["z"] - height_min < dz / 5 or height_max - stage.position["z"] < dz / 5 ): - print(heights) attempts += 1 else: repeat = False @@ -111,8 +110,6 @@ class RecentringThing(Thing): focused_pos[direction] = [list(stage.position.values())] moves = +1 - print(centre) - stage.move_absolute(x=centre[0], y=centre[1], z=centre[2]) steps = 0 all_heights = [] From b35db02f343975eb7efff98df4397e547cf97d8f Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 17 Jan 2024 13:14:43 +0000 Subject: [PATCH 06/11] 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. --- .../things/smart_scan.py | 72 ++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 7bf05a5e..51a44b44 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -1,4 +1,5 @@ import shutil +import zipfile import threading from typing import Mapping, Optional import cv2 @@ -312,6 +313,9 @@ DOWNLOADABLE_SCAN_FILES = ( class JPEGBlob(BlobOutput): media_type = "image/jpeg" +class ZipBlob(BlobOutput): + media_type = "application/zip" + class SmartScanThing(Thing): def __init__(self, path_to_openflexure_stitch: str): self._script = path_to_openflexure_stitch @@ -425,6 +429,11 @@ class SmartScanThing(Thing): r = cam.grab_jpeg() 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]: logger.error( f"Images are, by default, {arr.shape[:2]}, but the CSM was " @@ -593,15 +602,17 @@ class SmartScanThing(Thing): except InvocationCancelledError: logger.error("Stopping scan because it was cancelled.") except NotEnoughFreeSpaceError as e: - logger.exception( + logger.error( f"Stopping scan to avoid filling up the disk: {e}", + exc_info=e, ) raise e except Exception as e: - logger.exception( + logger.error( f"The scan stopped because of an error: {e}", "We will attempt to stitch and archive the images acquired " - "so far." + "so far.", + exc_info=e, ) raise e finally: @@ -614,18 +625,13 @@ class SmartScanThing(Thing): logger.info("Waiting for background processes to finish...") self.preview_stitch_wait() self.correlate_wait() - logger.info("Stitching final image (may take some time)...") try: 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) except SubprocessError as e: - logger.exception(f"Stitching failed: {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 - ) - + logger.error(f"Stitching failed: {e}", exc_info=e) + @thing_property def max_range(self) -> int: """The maximum distance from the centre of the scan before we break""" @@ -777,12 +783,17 @@ class SmartScanThing(Thing): @thing_property def latest_preview_stitch_time(self) -> Optional[datetime]: - """The modification time of the latest preview image""" - fpath = self.latest_preview_stitch_path - if not os.path.exists(fpath): + """The modification time of the latest preview image + + 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 - else: - return os.path.getmtime(fpath) + return None @fastapi_endpoint( "get", @@ -868,4 +879,33 @@ class SmartScanThing(Thing): """Generate a stitched image based on stage position metadata""" 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]) + + @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) From 50cf8e6f7842a5639c2ae5c7da29ac72c1f471d3 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 17 Jan 2024 13:16:19 +0000 Subject: [PATCH 07/11] Generate zip files before download Previously, zip files were only downloaded if they already existed. Now, they are created with an action, then downloaded from the result. Chrome now insists on downloading the files as `output.zip`, I can't see why this has changed, but perhaps it's related to the security warning. This seems to be specific to zip files. --- .../tabContentComponents/scanListContent.vue | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/webapp/src/components/tabContentComponents/scanListContent.vue b/webapp/src/components/tabContentComponents/scanListContent.vue index 2f22165a..0864405b 100644 --- a/webapp/src/components/tabContentComponents/scanListContent.vue +++ b/webapp/src/components/tabContentComponents/scanListContent.vue @@ -42,13 +42,15 @@

{{ item.name }}

- - Download images - + @@ -73,9 +75,11 @@