Combine xlarge width with updated heading

This commit is contained in:
Joe Knapper 2025-05-21 15:05:50 +00:00
commit 931554226e
22 changed files with 150 additions and 217 deletions

View file

@ -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"
},

View file

@ -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"
},

View file

@ -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"
},

View file

@ -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",

View file

@ -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):

View file

@ -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)

View file

@ -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 {

View file

@ -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
}

View file

@ -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)

View file

@ -25,12 +25,6 @@
Apply
</button>
</form>
<form class="uk-form-stacked" @submit.prevent="resetTour">
<label class="uk-form-label">Re-run tour on next load</label>
<button class="uk-button uk-button-default uk-margin-small">
Reset
</button>
</form>
</div>
</template>

View file

@ -3,12 +3,12 @@
<div v-if="$store.state.available">
<div>
<div class="uk-margin-small-bottom">
<b>Microscope hostname:</b>
<b>Microscope Hostname:</b>
<br />
{{ $store.state.microscopeHostname }}
</div>
<div class="uk-margin-small-bottom">
<b>API origin:</b>
<b>API Origin:</b>
<br />
{{ $store.state.origin }}
</div>
@ -24,7 +24,7 @@
<hr />
<div>
<b>Server version:</b> <br />
<b>Server Version:</b> <br />
TODO
</div>

View file

@ -19,14 +19,14 @@
<propertyControl
thing-name="background_detect"
property-name="fraction"
label="Sample coverage required (%)"
label="Sample Coverage Required (%)"
/>
</div>
<div class="uk-margin">
<action-button
thing="background_detect"
action="background_fraction"
submit-label="Check coverage"
submit-label="Check Coverage"
:can-terminate="false"
:poll-interval="0.1"
@response="alertBackgroundFraction"
@ -40,7 +40,7 @@
<action-button
thing="background_detect"
action="set_background"
submit-label="Set background"
submit-label="Set Background"
:can-terminate="false"
:poll-interval="0.1"
@response="alertBackgroundSet"
@ -50,7 +50,7 @@
<action-button
thing="background_detect"
action="image_is_sample"
submit-label="Check current image"
submit-label="Check Current Image"
:can-terminate="false"
:poll-interval="0.1"
@response="alertImageLabel"

View file

@ -5,7 +5,7 @@
<li>
<a class="uk-accordion-title" href="#">Configure</a>
<div class="uk-accordion-content">
<b>STEP SIZE</b>
<b>Step Size</b>
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
<div>
<label class="uk-form-label" for="form-stacked-text">x</label>

View file

@ -5,9 +5,9 @@
uk-grid
>
<div class="uk-width-xlarge">
<h3>Camera/stage mapping</h3>
<h3>Camera to Stage Mapping</h3>
<p>
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.
</p>

View file

@ -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
</button>
<div v-if="this.csmMatrix!='undefined'" style="margin:10px;">
<details><summary>Calibration Details</summary>

View file

@ -13,19 +13,19 @@
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
<div class="uk-accordion-content">
<PropertyControl
label="Exposure time (0-33251)"
label="Exposure Time (0-33251)"
property-name="exposure_time"
thing-name="camera"
:read-back-delay="1000"
/>
<PropertyControl
label="Analogue gain"
label="Analogue Gain"
property-name="analogue_gain"
thing-name="camera"
:read-back-delay="1000"
/>
<PropertyControl
label="Colour gains"
label="Colour Gains"
property-name="colour_gains"
thing-name="camera"
:read-back-delay="1000"
@ -36,13 +36,13 @@
<a class="uk-accordion-title" href="#">Image Quality</a>
<div class="uk-accordion-content">
<PropertyControl
label="MJPEG stream bit rate"
label="MJPEG Stream Bit Rate"
property-name="mjpeg_bitrate"
thing-name="camera"
:read-back-delay="100"
/>
<PropertyControl
label="MJPEG stream resolution"
label="MJPEG Stream Resolution"
property-name="stream_resolution"
thing-name="camera"
:read-back-delay="100"

View file

@ -21,7 +21,7 @@
:requires-confirmation="false"
thing="camera"
action="auto_expose_from_minimum"
:submit-label="'Auto gain &amp; shutter speed'"
:submit-label="'Auto Gain &amp; Shutter Speed'"
@response="onRecalibrateResponse"
@error="modalError"
/>
@ -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"
/>

View file

@ -1,90 +0,0 @@
<template>
<div id="appSettings" class="uk-width-large">
<h3>Additional features</h3>
<p class="uk-margin-small" :class="{ 'uk-text-muted': !imjoyPermitted }">
<label :disabled="!imjoyPermitted">
<input
v-model="imjoyEnabled"
class="uk-checkbox"
type="checkbox"
:disabled="!imjoyPermitted"
/>
Enable ImJoy plugin engine
</label>
</p>
<p v-if="imjoyPermitted" class="uk-margin-small">
<a href="https://imjoy.io/">ImJoy</a> enables integration with a wide
variety of microscopy and image analysis applications, including ImageJ.JS
and Kaibu. Support for ImJoy within the OFM software is currently
experimental, and it may slow down loading of the application.
</p>
<p v-if="!imjoyPermitted" class="uk-margin-small uk-text-muted">
ImJoy plugins are disabled in this build of the OpenFlexure software.
</p>
<p class="uk-margin-small">
<label
><input v-model="galleryEnabled" class="uk-checkbox" type="checkbox" />
Enable Gallery</label
>
</p>
<p class="uk-margin-small">
The "gallery" tab can cause performance issues; un-tick the box above to
disable it.
</p>
</div>
</template>
<script>
// Export main app
export default {
name: "FeaturesSettings",
data: function() {
return {};
},
computed: {
imjoyEnabled: {
get() {
return this.$store.state.imjoyEnabled;
},
set(value) {
this.$store.commit("changeImjoyEnabled", value);
}
},
galleryEnabled: {
get() {
return this.$store.state.galleryEnabled;
},
set(value) {
this.$store.commit("changeGalleryEnabled", value);
}
},
imjoyPermitted: function() {
// ImJoy may be disabled using an environment variable.
// If this function returns false, it is never possible to
// use ImJoy, so we should gray out the option.
return process.env.VUE_APP_ENABLE_IMJOY === "true";
}
},
watch: {
imjoyEnabled: function() {
this.setLocalStorageObj("imjoyEnabled", this.imjoyEnabled);
},
galleryEnabled: function() {
this.setLocalStorageObj("galleryEnabled", this.galleryEnabled);
}
},
mounted() {
// Try loading settings from localStorage. If null, don't change.
this.imjoyEnabled =
this.getLocalStorageObj("imjoyEnabled") || this.imjoyEnabled;
this.galleryEnabled =
this.getLocalStorageObj("galleryEnabled") || this.galleryEnabled;
}
};
</script>
<style lang="less"></style>

View file

@ -1,10 +1,10 @@
<template>
<div id="streamSettings" class="uk-width-large">
<div>
<h3>Stream settings</h3>
<h3>Stream Settings</h3>
<label
><input v-model="disableStream" class="uk-checkbox" type="checkbox" />
Disable web stream</label
Disable Web Stream</label
>
<p class="uk-margin-small">
This will disable the embedded web stream of the camera.

View file

@ -3,7 +3,7 @@
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
<div class="settings-nav">
<ul class="uk-nav uk-nav-default">
<li class="uk-nav-header">Application settings</li>
<li class="uk-nav-header">Application Settings</li>
<li>
<tabIcon
id="settings-display-icon"
@ -17,20 +17,7 @@
Display
</tabIcon>
</li>
<li>
<tabIcon
id="settings-features-icon"
tab-i-d="features"
:show-title="false"
:show-tooltip="false"
:require-connection="false"
:current-tab="currentTab"
@set-tab="setTab"
>
Features
</tabIcon>
</li>
<li class="uk-nav-header">Microscope settings</li>
<li class="uk-nav-header">Microscope Settings</li>
<li>
<tabIcon
id="settings-camera-icon"
@ -67,15 +54,14 @@
:current-tab="currentTab"
@set-tab="setTab"
>
Camera/stage mapping
Camera to Stage Mapping
</tabIcon>
</li>
</ul>
<action-button
thing="settings"
action="save_all_thing_settings"
submit-label="Save all settings"
class="uk-margin"
submit-label="Save All Settings"
/>
</div>
<div class="view-component uk-width-expand uk-padding-small">
@ -90,16 +76,6 @@
</div>
</tabContent>
<tabContent
tab-i-d="features"
:require-connection="false"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<featuresSettings />
</div>
</tabContent>
<tabContent
tab-i-d="camera"
:require-connection="true"
@ -137,7 +113,6 @@
import streamSettings from "./settingsComponents/streamSettings.vue";
import cameraSettings from "./settingsComponents/cameraSettings.vue";
import appSettings from "./settingsComponents/appSettings.vue";
import featuresSettings from "./settingsComponents/featuresSettings.vue";
import CSMSettings from "./settingsComponents/CSMSettings.vue";
import stageSettings from "./settingsComponents/stageSettings.vue";
// Import generic components
@ -155,7 +130,6 @@ export default {
stageSettings,
CSMSettings,
appSettings,
featuresSettings,
tabIcon,
tabContent,
ActionButton

View file

@ -21,7 +21,7 @@
<propertyControl
thing-name="smart_scan"
property-name="autofocus_dz"
label="Autofocus range (steps)"
label="Autofocus Range (steps)"
/>
</div>
<div class="uk-margin">
@ -35,21 +35,21 @@
<propertyControl
thing-name="autofocus"
property-name="stack_images_to_capture"
label="Images in stack to capture"
label="Images in Stack to Capture"
/>
</div>
<div class="uk-margin">
<propertyControl
thing-name="smart_scan"
property-name="overlap"
label="Image overlap (0-1)"
label="Image Overlap (0-1)"
/>
</div>
<div class="uk-margin">
<propertyControl
thing-name="smart_scan"
property-name="stitch_tiff"
label="When stitching, produce a pyramidal tiff"
label="When Stitching, Produce a Pyramidal TIFF"
/>
</div>
</div>
@ -61,14 +61,14 @@
<propertyControl
thing-name="smart_scan"
property-name="skip_background"
label="Detect and skip empty fields"
label="Detect and Skip Empty Fields"
/>
</div>
<div class="uk-margin">
<propertyControl
thing-name="smart_scan"
property-name="stitch_automatically"
label="Automatically stitch images together"
label="Automatically Stitch Images Together"
/>
</div>
</div>
@ -89,7 +89,7 @@
thing="smart_scan"
action="sample_scan"
:submit-data="{ scan_name: scan_name }"
submit-label="Start smart scan"
submit-label="Start Smart Scan"
:can-terminate="true"
@taskStarted="startScanning"
@update:taskStatus="taskStatus = $event"

View file

@ -144,10 +144,16 @@ Vue.mixin({
if (error.response) {
// If the response is a nicely formatted JSON response from the server
if (error.response.data.message) {
return `${error.response.status}: ${error.response.data.message}`;
return `${error.response.data.message}`;
}
if (error.response.data.detail) {
return `${error.response.data.detail}`;
}
// If the response is just some generic error response
return `${error.response.status}: ${error.response.data}`;
if (error.response.data){
return `${error.response.data}`;
}
return `${error.response}`;
}
// If we have an error object with a message, use that
if (error.message) return `${error.message}`;