diff --git a/ofm_config_full.json b/ofm_config_full.json index 7ae0469d..f182a774 100644 --- a/ofm_config_full.json +++ b/ofm_config_full.json @@ -7,12 +7,7 @@ "/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing", "/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager", - "/smart_scan/": { - "class": "openflexure_microscope_server.things.smart_scan:SmartScanThing", - "kwargs": { - "path_to_openflexure_stitch": "application/openflexure-stitching/.venv/bin/openflexure-stitch" - } - }, + "/smart_scan/": "openflexure_microscope_server.things.smart_scan:SmartScanThing", "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", "/capture/": "openflexure_microscope_server.things.capture:CaptureThing" }, diff --git a/ofm_config_simulation.json b/ofm_config_simulation.json index 62f427fb..0d06ad87 100644 --- a/ofm_config_simulation.json +++ b/ofm_config_simulation.json @@ -7,12 +7,7 @@ "/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing", "/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager", - "/smart_scan/": { - "class": "openflexure_microscope_server.things.smart_scan:SmartScanThing", - "kwargs": { - "path_to_openflexure_stitch": "application/openflexure-stitching/.venv/bin/openflexure-stitch" - } - }, + "/smart_scan/": "openflexure_microscope_server.things.smart_scan:SmartScanThing", "/background_detect/": "openflexure_microscope_server.things.smart_scan:BackgroundDetectThing", "/api_test/": "openflexure_microscope_server.things.test:APITestThing" }, diff --git a/ofm_config_stub.json b/ofm_config_stub.json index 14aa1d3f..d312795e 100644 --- a/ofm_config_stub.json +++ b/ofm_config_stub.json @@ -7,12 +7,7 @@ "/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing", "/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager", - "/smart_scan/": { - "class": "openflexure_microscope_server.things.smart_scan:SmartScanThing", - "kwargs": { - "path_to_openflexure_stitch": "application/openflexure-stitching/.venv/bin/openflexure-stitch" - } - }, + "/smart_scan/": "openflexure_microscope_server.things.smart_scan:SmartScanThing", "/background_detect/": "openflexure_microscope_server.things.smart_scan:BackgroundDetectThing", "/api_test/": "openflexure_microscope_server.things.test:APITestThing" }, diff --git a/pyproject.toml b/pyproject.toml index 1e4e4c77..fef6c92f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "labthings-sangaboard", "camera-stage-mapping ~= 0.1.10", "opencv-python ~= 4.11.0", + "openflexure-stitching[libvips]==0.1.0", "pillow ~= 10.4", "anyio ~= 4.0", "numpy ~= 2.2", diff --git a/src/openflexure_microscope_server/server/serve_static_files.py b/src/openflexure_microscope_server/server/serve_static_files.py index 5855d295..d87e5dec 100644 --- a/src/openflexure_microscope_server/server/serve_static_files.py +++ b/src/openflexure_microscope_server/server/serve_static_files.py @@ -1,41 +1,39 @@ +import os + from fastapi.responses import FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi import FastAPI -import os -import pathlib + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -def add_static_file(app: FastAPI, fname: str, folder: str): - print(f"Adding route for /{fname}") +def add_static_file(app: FastAPI, fname: str, folder: str) -> None: + """Add a single file to the root of the FastAPI app + The file with name `fname` will be mounted at `/fname` - the + `folder` does not affect where it is mounted in the app. + + app: The FastAPI app to add to, in this case the OpenFlexure server + fname: the name of the file to add + folder: the containing folder of the file + """ p = os.path.join(folder, fname) app.get(f"/{fname}", response_class=FileResponse, include_in_schema=False)( lambda: FileResponse(p) ) -def add_static_files(app: FastAPI): - # with importlib.resources.as_file(openflexure_microscope_server) as p: - # static_path = p.join("/static/") - # TODO: don't hard code this! - search_paths = [ - "/var/openflexure/application/openflexure-microscope-server/src/openflexure_microscope_server/static", - pathlib.Path().absolute() - / "application/openflexure-microscope-server/src/openflexure_microscope_server/static", - ] - if __file__: - search_paths.append(pathlib.Path(__file__).parent.parent / "static") +def add_static_files(app: FastAPI) -> None: + """Add the static files responsible for the webapp app to the FastAPI app - for static_path in search_paths: - if os.path.isdir(static_path): - break # stop once one of the paths exists - else: - # If we get to the else: block, no pat was found. - raise RuntimeError("Can't find static files :(") + app: The FastAPI app to add to, in this case the OpenFlexure server + """ + static_path = os.path.normpath(os.path.join(THIS_DIR, "..", "static")) @app.get("/", response_class=RedirectResponse) async def redirect_fastapi(): return "/index.html" + # Mounting the webapp at / file by file to allow other endpoints to be created for fname in os.listdir(static_path): fpath = os.path.join(static_path, fname) if os.path.isfile(fpath): diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 962310fb..3f11a7e7 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -88,6 +88,7 @@ JPEGBlob = blob_type("image/jpeg") ZipBlob = blob_type("application/zip") IMG_DIR_NAME = "images" SCAN_DATA_FILENAME = "scan_data.json" +STITCHING_CMD = "openflexure-stitch" SCAN_ZERO_PAD_DIGITS = 4 @@ -112,8 +113,7 @@ def _scan_running(method): class SmartScanThing(Thing): - def __init__(self, path_to_openflexure_stitch: str): - self._stitching_script = path_to_openflexure_stitch + def __init__(self): self._preview_stitch_popen = None self._preview_stitch_popen_lock = threading.Lock() self._scan_lock = threading.Lock() @@ -588,6 +588,9 @@ class SmartScanThing(Thing): self._return_to_starting_position() self._perform_final_stitch() + # Remove any scan folders containing zero images + self.purge_empty_scans(logger=self._scan_logger) + @_scan_running def _main_scan_loop(self): """ @@ -868,22 +871,26 @@ class SmartScanThing(Thing): 404: {"description": "Scan not found"}, }, ) - def delete_scan(self, scan_name: str) -> None: - """Delete all files from a scan. + def delete_scan(self, scan_name: str, logger: InvocationLogger) -> None: + """Delete the folder for the specified scan. This endpoint allows scans to be deleted from disk. + + Takes the scan name to delete, and the Invocation Logger """ path = os.path.join(self.base_scan_dir, scan_name) if not os.path.isdir(path): - print(f"can't find {path}") - raise HTTPException(404, "Scan not found") - shutil.rmtree(path) + logger.info(f"can't find {path}") + raise HTTPException(400, "Scan not found") + deleted_scan_success = self._delete_scan(path, logger) + if not deleted_scan_success: + raise HTTPException(400, "Couldn't delete scan, check log for details") @fastapi_endpoint( "delete", "scans", ) - def delete_all_scans(self) -> None: + def delete_all_scans(self, logger: InvocationLogger) -> None: """Delete all the scans on the microscope **This will irreversibly remove all scanned data from the @@ -891,7 +898,20 @@ class SmartScanThing(Thing): Use with extreme caution. """ for scan in self.scans: - self.delete_scan(scan.name) + path = os.path.join(self.base_scan_dir, scan.name) + self._delete_scan(path, logger) + + @thing_action + def _delete_scan(self, scan_path, logger: InvocationLogger) -> bool: + try: + shutil.rmtree(scan_path) + return True + except Exception as e: + logger.warning( + "Attempted to delete scan " + scan_path + ", which failed." + " Server sent response" + str(e) + ) + return False @property def latest_preview_stitch_path(self): @@ -958,7 +978,7 @@ class SmartScanThing(Thing): with self._preview_stitch_popen_lock: self._preview_stitch_popen = Popen( [ - self._stitching_script, + STITCHING_CMD, "--stitching_mode", "only_stage_stitch", "--minimum_overlap", @@ -1072,7 +1092,7 @@ class SmartScanThing(Thing): self.run_subprocess( logger, [ - self._stitching_script, + STITCHING_CMD, "--stitching_mode", "all", f"{tiff_arg}", @@ -1179,3 +1199,26 @@ class SmartScanThing(Thing): """List the relative paths of all files and folders in the zip folder specified""" scan_zip = zipfile.ZipFile(zip_path) return [os.path.normpath(i) for i in scan_zip.namelist()] + + @thing_action + def purge_empty_scans(self, logger: InvocationLogger) -> None: + """ + Delete all scan folders containing no images at the top level + """ + scan_list = self.scans + + # Filter out scans with no top level files, ignoring JSON files + # JSON is ignored as it's created before any images are captured + for scan in scan_list: + scan_folder = os.path.join(self.base_scan_dir, scan.name, IMG_DIR_NAME) + images_found = False + for fname in os.listdir(scan_folder): + fpath = os.path.join(scan_folder, fname) + if os.path.isfile(fpath) and IMAGE_REGEX.search(fname): + images_found = True + # break as soon as an image is found. + break + + if not images_found: + path = os.path.join(self.base_scan_dir, scan.name) + self._delete_scan(path, logger) diff --git a/webapp/src/assets/less/theme.less b/webapp/src/assets/less/theme.less index df88729b..a8c6e80f 100644 --- a/webapp/src/assets/less/theme.less +++ b/webapp/src/assets/less/theme.less @@ -80,7 +80,11 @@ // Nav // -@nav-header-font-size: 12px; +@nav-header-font-size: 16px; +.uk-nav-header{ + text-transform:none; +} + // // Subnav @@ -343,6 +347,11 @@ a:hover { a:hover { color: @inverse-primary-muted-color; } + + .uk-alert-success{ + color: rgba(28, 131, 45); + background-color: rgba(130, 221, 145, 0.671); + } } /* @@ -353,6 +362,7 @@ a:hover { padding: 0 8px; border-color: @global-border; margin-bottom: 2px; + text-transform:none !important; } .uk-button-default { diff --git a/webapp/src/components/appContent.vue b/webapp/src/components/appContent.vue index 2662eebc..fcbf7296 100644 --- a/webapp/src/components/appContent.vue +++ b/webapp/src/components/appContent.vue @@ -205,27 +205,27 @@ export default { topTabs: function() { let tabs = [ { - id: "view", + id: "View", icon: "visibility", component: viewContent }, { - id: "navigate", + id: "Navigate", icon: "gamepad", component: navigateContent }, { - id: "background detect", + id: "Background Detect", icon: "background_replace", component: backgroundDetectContent }, { - id: "slide scan", + id: "Slide Scan", icon: "settings_overscan", component: slideScanContent }, { - id: "scan list", + id: "Scan List", icon: "photo_library", component: ScanListContent } diff --git a/webapp/src/components/labThingsComponents/actionButton.vue b/webapp/src/components/labThingsComponents/actionButton.vue index 00346a75..7efbbbc9 100644 --- a/webapp/src/components/labThingsComponents/actionButton.vue +++ b/webapp/src/components/labThingsComponents/actionButton.vue @@ -251,7 +251,7 @@ export default { this.modalNotify(`The action '${this.submitLabel}' was cancelled.`); } } catch (error) { - this.$emit("error", error | Error("Unknown error")); + this.$emit("error", error); } finally { // Reset taskRunning and taskId this.taskRunning = false; @@ -297,7 +297,19 @@ export default { else if (result == "error") { // Pass the error string back with reject if (!this.progress) this.progress = 1; - reject(new Error(response.data.output)); + // Test whether the log is empty or the most recent message is not from an error + // If so, return a default message + if (response.data.log.length == 0 | response.data.log.at(-1).levelname != "ERROR") { + var message = "Unexpected error, please check the logs"; + } + // As LabThings Actions add the message from any raised exception to the log, the + // last message in the log is the message from the Exception. + //If the Exception was raised with no message, use a default. + else { + message = response.data.log.at(-1).message||"Unexpected error, please check the logs"; + } + // Raise an Error with the chosen message + reject(new Error(message)); } // If task ends without reporting an error // (NB this includes cancellation) diff --git a/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue b/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue index 0bcc9cb5..0c17a105 100644 --- a/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue +++ b/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue @@ -25,12 +25,6 @@ Apply -
- - -
diff --git a/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue b/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue index dc34291f..e3ca4f35 100644 --- a/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue +++ b/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue @@ -3,12 +3,12 @@
- Microscope hostname: + Microscope Hostname:
{{ $store.state.microscopeHostname }}
- API origin: + API Origin:
{{ $store.state.origin }}
@@ -24,7 +24,7 @@
- Server version:
+ Server Version:
TODO
diff --git a/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue b/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue index 11c4cc64..8d6c8bac 100644 --- a/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue +++ b/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue @@ -19,14 +19,14 @@
Configure
- STEP SIZE + Step Size
diff --git a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue index 266fe485..c27a8e55 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue @@ -5,9 +5,9 @@ uk-grid >
-

Camera/stage mapping

+

Camera to Stage Mapping

- Camera/stage mapping allows the stage to move relative to the camera + Camera-stage mapping allows the stage to move relative to the camera view. This enables functions like click-to-move, and more precise tile scans.

diff --git a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue index b9386616..05b551c1 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue @@ -11,7 +11,7 @@ " thing="camera_stage_mapping" action="calibrate_xy" - :submit-label="'Auto-Calibrate using camera'" + :submit-label="'Auto-Calibrate Using Camera'" :modal-progress="true" @response="onRecalibrateResponse" @error="modalError" @@ -24,7 +24,7 @@ class="uk-button uk-button-default uk-width-1-1" @click="getCalibrationData()" > - Download calibration data + Download Calibration Data
Calibration Details diff --git a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue index 2ad51115..69f3f837 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue @@ -13,19 +13,19 @@ Pi Camera Settings
Image Quality
@@ -32,7 +32,7 @@ :requires-confirmation="false" thing="camera" action="calibrate_white_balance" - :submit-label="'Auto white balance'" + :submit-label="'Auto White Balance'" @response="onRecalibrateResponse" @error="modalError" /> @@ -47,7 +47,7 @@ " thing="camera" action="calibrate_lens_shading" - :submit-label="'Auto flat field correction'" + :submit-label="'Auto Flat Field Correction'" @response="onRecalibrateResponse" @error="modalError" /> @@ -63,7 +63,7 @@ :requires-confirmation="false" thing="camera" action="flat_lens_shading" - :submit-label="'Disable flat field correction'" + :submit-label="'Disable Flat Field Correction'" @response="onRecalibrateResponse" @error="modalError" /> @@ -78,7 +78,7 @@ :requires-confirmation="false" thing="camera" action="reset_lens_shading" - :submit-label="'Reset flat field correction'" + :submit-label="'Reset Flat Field Correction'" @response="onRecalibrateResponse" @error="modalError" /> diff --git a/webapp/src/components/tabContentComponents/settingsComponents/featuresSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/featuresSettings.vue deleted file mode 100644 index a413d401..00000000 --- a/webapp/src/components/tabContentComponents/settingsComponents/featuresSettings.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue index c85ff7d9..b6013c11 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue @@ -1,10 +1,10 @@