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:
commit
f44fb02006
10 changed files with 165 additions and 57 deletions
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue