Merge branch 'rwanda-feedback' into 'v3'

Hide background detect settings in an accordion

See merge request openflexure/openflexure-microscope-server!172
This commit is contained in:
Richard Bowman 2024-01-17 14:22:51 +00:00
commit f44fb02006
10 changed files with 165 additions and 57 deletions

View file

@ -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 = []

View file

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

View file

@ -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 "
@ -564,9 +573,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)
@ -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)

View file

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

View file

@ -2,11 +2,23 @@
<div class="host-input">
<div v-if="$store.state.available">
<div>
<div class="uk-margin-small-bottom">
<b>Microscope hostname:</b>
<br />
{{ $store.state.microscopeHostname }}
</div>
<div class="uk-margin-small-bottom">
<b>API origin:</b>
<br />
{{ $store.state.origin }}
</div>
<task-submitter
v-if="flashLedUri"
:submit-url="flashLedUri"
submit-label="Flash Illumination"
:can-terminate="false"
:submit-data="{'dt': 0.25}"
/>
</div>
<hr />
@ -71,13 +83,18 @@
<script>
import axios from "axios";
import taskSubmitter from '../../genericComponents/taskSubmitter.vue';
export default {
components: { taskSubmitter },
name: "StatusPane",
computed: {
things: function() {
return this.$store.getters["wot/thingDescriptions"];
},
flashLedUri() {
return this.thingActionUrl("stage", "flash_led");
}
},

View file

@ -4,6 +4,37 @@
The background detect Thing seems to be missing or incompatible.
</div>
<div v-show="backendOK">
<ul uk-accordion="multiple: true">
<li>
<a class="uk-accordion-title" href="#">Settings</a>
<div class="uk-accordion-content">
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="tolerance"
label="Tolerance"
/>
</div>
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="fraction"
label="Sample coverage required (%)"
/>
</div>
<div class="uk-margin">
<taskSubmitter
:submit-url="backgroundFractionUri"
submit-label="Check coverage"
:can-terminate="false"
:poll-interval="0.1"
@response="alertBackgroundFraction"
@error="backgroundDetectError"
/>
</div>
</div>
</li>
</ul>
<div class="uk-margin">
<taskSubmitter
:submit-url="setBackgroundUri"
@ -13,34 +44,10 @@
@response="alertBackgroundSet"
/>
</div>
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="tolerance"
label="Tolerance"
/>
</div>
<div class="uk-margin">
<propertyControl
thing-name="background_detect"
property-name="fraction"
label="Sample coverage required (%)"
/>
</div>
<div class="uk-margin">
<taskSubmitter
:submit-url="backgroundFractionUri"
submit-label="Check current image"
:can-terminate="false"
:poll-interval="0.1"
@response="alertBackgroundFraction"
@error="backgroundDetectError"
/>
</div>
<div class="uk-margin">
<taskSubmitter
:submit-url="labelImageUri"
submit-label="Label current image"
submit-label="Check current image"
:can-terminate="false"
:poll-interval="0.1"
@response="alertImageLabel"

View file

@ -42,16 +42,27 @@
<div class="uk-card">
<div class="uk-card-body">
<h3 class="uk-card-title">{{ item.name }}</h3>
<a
:href="`${scansUri}/${item.name}/images.zip`"
:download="`${item.name}_images.zip`"
class="uk-button uk-button-default"
>
Download images
</a>
<task-submitter
submit-label="Download ZIP"
:can-terminate="false"
:submit-data="{'scan_name': item.name}"
:button-primary="true"
:submit-url="createZipOfScanUri"
@response="downloadZipFile"
@error="modalError"
/>
<button class="uk-button" @click="deleteScan(item.name)">
Delete
</button>
<task-submitter
submit-label="Stitch Images"
:can-terminate="false"
:submit-data="{'scan_name': item.name}"
:button-primary="false"
:submit-url="stitchUri"
:modal-progress="true"
@error="modalError"
/>
<ul>
<li>{{ item.number_of_images }} images</li>
<li>created {{ formatDate(item.created) }}</li>
@ -73,9 +84,11 @@
<script>
import axios from "axios";
import taskSubmitter from '../genericComponents/taskSubmitter.vue';
// Export main app
export default {
components: { taskSubmitter },
name: "ScanListContent",
data: function() {
@ -92,6 +105,12 @@ export default {
"readproperty",
true
);
},
createZipOfScanUri() {
return this.thingActionUrl("smart_scan", "create_zip_of_scan");
},
stitchUri() {
return this.thingActionUrl("smart_scan", "stitch_scan");
}
},
@ -186,6 +205,17 @@ export default {
// if the confirmation was cancelled, it's rejected with null error
if (e) this.modalError(e);
}
},
async downloadZipFile(response) {
const scan_name = response.input.scan_name;
const filename = `${scan_name}_images.zip`
const url = response.output.href;
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
console.log(link);
document.body.appendChild(link);
link.click();
}
}
};

View file

@ -71,12 +71,12 @@ Vue.mixin({
}
await axios.put(url, value);
},
thingActionUrl(thing, action) {
thingActionUrl(thing, action, allow_missing=false) {
let url = this.$store.getters["wot/thingActionUrl"](
thing,
action,
"invokeaction",
false
allow_missing
);
return url;
},

View file

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

View file

@ -120,6 +120,7 @@ export const wotStoreModule = {
export function findFormHref(affordance, op) {
// Find the form in the affordance that matches the given operation type
if (affordance == undefined) return undefined;
let forms = affordance.forms;
let matchingForm = forms.find(f => f.op == op || f.op.includes(op));
if (matchingForm == undefined) return undefined;