Merge branch 'live-scan-details' into 'v3'
Return more live scan details for the UI See merge request openflexure/openflexure-microscope-server!641
This commit is contained in:
commit
dcf5751310
5 changed files with 125 additions and 23 deletions
|
|
@ -17,6 +17,7 @@ from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Concatenate,
|
Concatenate,
|
||||||
|
Literal,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
ParamSpec,
|
ParamSpec,
|
||||||
|
|
@ -43,6 +44,7 @@ from .stage import BacklashCompensation, BaseStage
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
P = ParamSpec("P")
|
P = ParamSpec("P")
|
||||||
|
|
||||||
|
MIN_IMAGES_TO_STITCH = 2
|
||||||
|
|
||||||
# This allows ActiveScanData to hold arbitrary workflow settings models during a scan.
|
# This allows ActiveScanData to hold arbitrary workflow settings models during a scan.
|
||||||
AnyModel = Annotated[
|
AnyModel = Annotated[
|
||||||
|
|
@ -51,6 +53,15 @@ AnyModel = Annotated[
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LiveScanDetails(BaseModel):
|
||||||
|
"""Details of ongoing scan used for the UI."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
stitch_timestamp: Optional[float] = None
|
||||||
|
image_count: int = 0
|
||||||
|
scan_phase: Literal["Setup", "Scanning", "Stitching", "Complete"] = "Setup"
|
||||||
|
|
||||||
|
|
||||||
class ActiveScanData(scan_directories.BaseScanData):
|
class ActiveScanData(scan_directories.BaseScanData):
|
||||||
"""A model for the scan data during an ongoing scan.
|
"""A model for the scan data during an ongoing scan.
|
||||||
|
|
||||||
|
|
@ -276,12 +287,21 @@ class SmartScanThing(OFMThing):
|
||||||
|
|
||||||
_preview_stitcher: Optional[stitching.PreviewStitcher] = None
|
_preview_stitcher: Optional[stitching.PreviewStitcher] = None
|
||||||
|
|
||||||
_latest_scan_name: Optional[str] = None
|
_latest_scan_live_details: Optional[LiveScanDetails] = None
|
||||||
|
|
||||||
@lt.property
|
@lt.property
|
||||||
def latest_scan_name(self) -> Optional[str]:
|
def latest_scan_live_details(self) -> Optional[LiveScanDetails]:
|
||||||
"""The name of the last scan to be started."""
|
"""The details of any ongoing scan, or the previous scan if no scan ongoing."""
|
||||||
return self._latest_scan_name
|
if self._latest_scan_live_details is None:
|
||||||
|
return None
|
||||||
|
# Update image count and stitch timestamp if scan is ongoing.
|
||||||
|
if self._scan_data is not None:
|
||||||
|
self._latest_scan_live_details.image_count = self._scan_data.image_count
|
||||||
|
|
||||||
|
# Use walrus operator to not need to read filesystem timestamp twice
|
||||||
|
if (stitch_time := self.latest_preview_stitch_time) is not None:
|
||||||
|
self._latest_scan_live_details.stitch_timestamp = stitch_time
|
||||||
|
return self._latest_scan_live_details
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def sample_scan(self, scan_name: str = "") -> None:
|
def sample_scan(self, scan_name: str = "") -> None:
|
||||||
|
|
@ -305,7 +325,9 @@ class SmartScanThing(OFMThing):
|
||||||
|
|
||||||
workflow.check_before_start(scan_name)
|
workflow.check_before_start(scan_name)
|
||||||
self._ongoing_scan = self._scan_dir_manager.new_scan_dir(scan_name)
|
self._ongoing_scan = self._scan_dir_manager.new_scan_dir(scan_name)
|
||||||
self._latest_scan_name = self.ongoing_scan.name
|
self._latest_scan_live_details = LiveScanDetails(
|
||||||
|
name=self.ongoing_scan.name
|
||||||
|
)
|
||||||
self._run_scan(workflow)
|
self._run_scan(workflow)
|
||||||
# Save any final messages.
|
# Save any final messages.
|
||||||
self._ongoing_scan.save_scan_log(self)
|
self._ongoing_scan.save_scan_log(self)
|
||||||
|
|
@ -327,6 +349,18 @@ class SmartScanThing(OFMThing):
|
||||||
# Reraise so the action reports as cancelled if stitching is cancelled.
|
# Reraise so the action reports as cancelled if stitching is cancelled.
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
|
# Save some final details but not set to the base model until after
|
||||||
|
# resetting values and unlocking in case of Pydantic errors.
|
||||||
|
|
||||||
|
final_image_count = (
|
||||||
|
self._scan_data.image_count if self._scan_data is not None else None
|
||||||
|
)
|
||||||
|
last_timestamp = (
|
||||||
|
self.latest_preview_stitch_time
|
||||||
|
if self._preview_stitcher is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
# However the scan finishes, unset all variables and release lock
|
# However the scan finishes, unset all variables and release lock
|
||||||
self._ongoing_scan = None
|
self._ongoing_scan = None
|
||||||
self._scan_data = None
|
self._scan_data = None
|
||||||
|
|
@ -334,6 +368,14 @@ class SmartScanThing(OFMThing):
|
||||||
# Ensure any PreviewStitcher created cannot be reused.
|
# Ensure any PreviewStitcher created cannot be reused.
|
||||||
self._preview_stitcher = None
|
self._preview_stitcher = None
|
||||||
|
|
||||||
|
# Set the final details so they persist until the next scan
|
||||||
|
if self._latest_scan_live_details is not None:
|
||||||
|
if final_image_count is not None:
|
||||||
|
self._latest_scan_live_details.image_count = final_image_count
|
||||||
|
if last_timestamp is not None:
|
||||||
|
self._latest_scan_live_details.stitch_timestamp = last_timestamp
|
||||||
|
self._latest_scan_live_details.scan_phase = "Complete"
|
||||||
|
|
||||||
# Remove any scan folders containing zero images.
|
# Remove any scan folders containing zero images.
|
||||||
self.purge_empty_scans()
|
self.purge_empty_scans()
|
||||||
|
|
||||||
|
|
@ -409,7 +451,10 @@ class SmartScanThing(OFMThing):
|
||||||
# This scan can't stitch.
|
# This scan can't stitch.
|
||||||
return
|
return
|
||||||
# Begin stitching once two images are captured
|
# Begin stitching once two images are captured
|
||||||
if self.scan_data.image_count >= 2 and not self._preview_stitcher.running:
|
if (
|
||||||
|
self.scan_data.image_count >= MIN_IMAGES_TO_STITCH
|
||||||
|
and not self._preview_stitcher.running
|
||||||
|
):
|
||||||
self._preview_stitcher.start()
|
self._preview_stitcher.start()
|
||||||
|
|
||||||
@_scan_running
|
@_scan_running
|
||||||
|
|
@ -444,14 +489,20 @@ class SmartScanThing(OFMThing):
|
||||||
overlap=stitching_settings.overlap,
|
overlap=stitching_settings.overlap,
|
||||||
correlation_resize=stitching_settings.correlation_resize,
|
correlation_resize=stitching_settings.correlation_resize,
|
||||||
)
|
)
|
||||||
|
if self._latest_scan_live_details is not None:
|
||||||
|
self._latest_scan_live_details.scan_phase = "Scanning"
|
||||||
# This is the main loop of the scan!
|
# This is the main loop of the scan!
|
||||||
self._main_scan_loop(workflow)
|
self._main_scan_loop(workflow)
|
||||||
self._save_final_scan_data(scan_result="success")
|
self._save_final_scan_data(scan_result="success")
|
||||||
|
|
||||||
except lt.exceptions.InvocationCancelledError:
|
except lt.exceptions.InvocationCancelledError as e:
|
||||||
self.logger.info("Stopping scan because it was cancelled.")
|
self.logger.info("Stopping scan because it was cancelled.")
|
||||||
self._save_final_scan_data(scan_result="cancelled by user")
|
self._save_final_scan_data(scan_result="cancelled by user")
|
||||||
|
if self.scan_data.image_count < MIN_IMAGES_TO_STITCH:
|
||||||
|
# If too few images to stitch then, return to the start and report
|
||||||
|
# cancelled
|
||||||
|
self._return_to_starting_position()
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_name = type(e).__name__
|
err_name = type(e).__name__
|
||||||
if self._scan_data is not None:
|
if self._scan_data is not None:
|
||||||
|
|
@ -544,6 +595,8 @@ class SmartScanThing(OFMThing):
|
||||||
@_scan_running
|
@_scan_running
|
||||||
def _perform_final_stitch(self) -> None:
|
def _perform_final_stitch(self) -> None:
|
||||||
"""Update the scan zip and perform final stitch of the data."""
|
"""Update the scan zip and perform final stitch of the data."""
|
||||||
|
if self._latest_scan_live_details is not None:
|
||||||
|
self._latest_scan_live_details.scan_phase = "Stitching"
|
||||||
if self.scan_data.image_count <= 1:
|
if self.scan_data.image_count <= 1:
|
||||||
self.logger.info("Not performing a stitch as not enough images captured")
|
self.logger.info("Not performing a stitch as not enough images captured")
|
||||||
return
|
return
|
||||||
|
|
@ -667,14 +720,16 @@ class SmartScanThing(OFMThing):
|
||||||
@property
|
@property
|
||||||
def latest_preview_stitch_path(self) -> Optional[str]:
|
def latest_preview_stitch_path(self) -> Optional[str]:
|
||||||
"""The path of the latest preview stitched image, or None if not available."""
|
"""The path of the latest preview stitched image, or None if not available."""
|
||||||
if self.latest_scan_name is None:
|
if self._latest_scan_live_details is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._scan_dir_manager.get_file_path_from_img_dir(
|
return self._scan_dir_manager.get_file_path_from_img_dir(
|
||||||
scan_name=self.latest_scan_name, filename="preview.jpg", check_exists=True
|
scan_name=self._latest_scan_live_details.name,
|
||||||
|
filename="preview.jpg",
|
||||||
|
check_exists=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@lt.property
|
@property
|
||||||
def latest_preview_stitch_time(self) -> Optional[float]:
|
def latest_preview_stitch_time(self) -> Optional[float]:
|
||||||
"""The modification time of the latest preview image, to allow live updating.
|
"""The modification time of the latest preview image, to allow live updating.
|
||||||
|
|
||||||
|
|
@ -760,6 +815,6 @@ class SmartScanThing(OFMThing):
|
||||||
# Use the scan list (the data read by the gallery) to find any scans that
|
# Use the scan list (the data read by the gallery) to find any scans that
|
||||||
# need stitching.
|
# need stitching.
|
||||||
for scan in self.get_data_for_gallery():
|
for scan in self.get_data_for_gallery():
|
||||||
if scan.dzi is None and scan.number_of_images >= 2:
|
if scan.dzi is None and scan.number_of_images >= MIN_IMAGES_TO_STITCH:
|
||||||
self.logger.info(f"Stitching {scan.name}")
|
self.logger.info(f"Stitching {scan.name}")
|
||||||
self.stitch_scan(scan_name=scan.name)
|
self.stitch_scan(scan_name=scan.name)
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ def test_initial_properties(entered_smart_scan_thing):
|
||||||
"""
|
"""
|
||||||
smart_scan_thing = entered_smart_scan_thing
|
smart_scan_thing = entered_smart_scan_thing
|
||||||
assert smart_scan_thing._scan_dir_manager.base_dir == SCAN_DIR
|
assert smart_scan_thing._scan_dir_manager.base_dir == SCAN_DIR
|
||||||
assert smart_scan_thing.latest_scan_name is None
|
assert smart_scan_thing.latest_scan_live_details is None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -559,7 +559,7 @@ def test_run_scan_cancelled(scan_thing_mocked_for_run_scan, caplog, mocker):
|
||||||
scan_thing, "_main_scan_loop", side_effect=InvocationCancelledError()
|
scan_thing, "_main_scan_loop", side_effect=InvocationCancelledError()
|
||||||
)
|
)
|
||||||
|
|
||||||
result, logs, calls = check_run_scan(scan_thing, caplog)
|
result, logs, calls = check_run_scan(scan_thing, caplog, InvocationCancelledError)
|
||||||
|
|
||||||
assert result == "cancelled by user"
|
assert result == "cancelled by user"
|
||||||
# No logs at warning level.
|
# No logs at warning level.
|
||||||
|
|
@ -571,7 +571,7 @@ def test_run_scan_cancelled(scan_thing_mocked_for_run_scan, caplog, mocker):
|
||||||
"cam_change_streaming_mode_calls": 2,
|
"cam_change_streaming_mode_calls": 2,
|
||||||
"main_scan_loop_calls": 1,
|
"main_scan_loop_calls": 1,
|
||||||
"return_to_start_calls": 1,
|
"return_to_start_calls": 1,
|
||||||
"perform_final_stitch_calls": 1,
|
"perform_final_stitch_calls": 0,
|
||||||
"save_scan_data_calls": 2,
|
"save_scan_data_calls": 2,
|
||||||
}
|
}
|
||||||
assert calls == expected_calls_numbers
|
assert calls == expected_calls_numbers
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: "Submit",
|
default: "Submit",
|
||||||
},
|
},
|
||||||
|
cancelLabel: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "Cancel",
|
||||||
|
},
|
||||||
canTerminate: {
|
canTerminate: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -158,7 +163,9 @@ export default {
|
||||||
return this.isDisabled || this.isBroken || (this.taskRunning && !this.canTerminate);
|
return this.isDisabled || this.isBroken || (this.taskRunning && !this.canTerminate);
|
||||||
},
|
},
|
||||||
buttonLabel() {
|
buttonLabel() {
|
||||||
return this.taskRunning && this.canTerminate ? "Cancel" : this.submitLabel;
|
// If the action is running (and can terminate) then show the cancel label, otherwise
|
||||||
|
// show the submit label.
|
||||||
|
return this.taskRunning && this.canTerminate ? this.cancelLabel : this.submitLabel;
|
||||||
},
|
},
|
||||||
buttonClasses() {
|
buttonClasses() {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,10 @@ mode.
|
||||||
:force-id="taskId"
|
:force-id="taskId"
|
||||||
:force-url="taskUrl"
|
:force-url="taskUrl"
|
||||||
submit-label=""
|
submit-label=""
|
||||||
|
:cancel-label="cancelLabel"
|
||||||
:can-terminate="true"
|
:can-terminate="true"
|
||||||
@completed="$emit('completed')"
|
@completed="$emit('completed')"
|
||||||
|
@finished="$emit('finished')"
|
||||||
@update:task-status="taskStatus = $event"
|
@update:task-status="taskStatus = $event"
|
||||||
@update:progress="progress = $event"
|
@update:progress="progress = $event"
|
||||||
@update:log="log = $event"
|
@update:log="log = $event"
|
||||||
|
|
@ -131,9 +133,14 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
cancelLabel: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "Cancel",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ["closeTask", "completed", "actionStartedExternally"],
|
emits: ["closeTask", "completed", "finished", "actionStartedExternally"],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
:task-url="taskUrl"
|
:task-url="taskUrl"
|
||||||
:task-info-title="infoPaneTitle"
|
:task-info-title="infoPaneTitle"
|
||||||
:task-info-stream="displayImageOnRight"
|
:task-info-stream="displayImageOnRight"
|
||||||
@completed="onScanCompleted"
|
:cancel-label="cancelLabel"
|
||||||
|
@finished="onScanCompleted"
|
||||||
@close-task="closeTask"
|
@close-task="closeTask"
|
||||||
@action-started-externally="startScanning"
|
@action-started-externally="startScanning"
|
||||||
>
|
>
|
||||||
|
|
@ -39,7 +40,7 @@
|
||||||
<div class="uk-width-1-1 uk-flex uk-flex-center">
|
<div class="uk-width-1-1 uk-flex uk-flex-center">
|
||||||
<div class="uk-width-2-3">
|
<div class="uk-width-2-3">
|
||||||
<action-button
|
<action-button
|
||||||
v-if="scanComplete"
|
v-if="scanComplete && imageCount >= 1"
|
||||||
thing="smart_scan"
|
thing="smart_scan"
|
||||||
action="download_zip"
|
action="download_zip"
|
||||||
submit-label="Download ZIP"
|
submit-label="Download ZIP"
|
||||||
|
|
@ -51,7 +52,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 v-if="scanning" class="uk-margin-left">Scan ID: {{ lastScanName }}</h3>
|
<div class="uk-margin-left info-headings">
|
||||||
|
<h3 v-if="scanning" class="uk-margin-left">Scan ID: {{ lastScanName }}</h3>
|
||||||
|
<h3 v-if="scanning" class="uk-margin-left">Images captured: {{ imageCount }}</h3>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</actionTab>
|
</actionTab>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -79,6 +83,8 @@ export default {
|
||||||
taskId: null,
|
taskId: null,
|
||||||
taskUrl: null,
|
taskUrl: null,
|
||||||
lastStitchedImage: null,
|
lastStitchedImage: null,
|
||||||
|
imageCount: null,
|
||||||
|
scanPhase: null,
|
||||||
scanComplete: false,
|
scanComplete: false,
|
||||||
scan_name: "",
|
scan_name: "",
|
||||||
// This adds the parent name as value for prop streamId
|
// This adds the parent name as value for prop streamId
|
||||||
|
|
@ -98,6 +104,11 @@ export default {
|
||||||
if (!this.displayImageOnRight) return null;
|
if (!this.displayImageOnRight) return null;
|
||||||
return "Live stitching preview";
|
return "Live stitching preview";
|
||||||
},
|
},
|
||||||
|
cancelLabel() {
|
||||||
|
if (!this.scanPhase) return "Cancel";
|
||||||
|
if (this.scanPhase === "Complete") return "Cancel";
|
||||||
|
return `Cancel ${this.scanPhase}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -123,6 +134,8 @@ export default {
|
||||||
startScanning(id, url) {
|
startScanning(id, url) {
|
||||||
this.lastStitchedImage = null;
|
this.lastStitchedImage = null;
|
||||||
this.lastScanName = null;
|
this.lastScanName = null;
|
||||||
|
this.imageCount = 0;
|
||||||
|
this.scanPhase = null;
|
||||||
this.taskId = id;
|
this.taskId = id;
|
||||||
this.taskUrl = url;
|
this.taskUrl = url;
|
||||||
this.scanComplete = false;
|
this.scanComplete = false;
|
||||||
|
|
@ -135,17 +148,27 @@ export default {
|
||||||
this.taskId = null;
|
this.taskId = null;
|
||||||
this.taskUrl = null;
|
this.taskUrl = null;
|
||||||
this.lastStitchedImage = null;
|
this.lastStitchedImage = null;
|
||||||
|
this.imageCount = null;
|
||||||
|
this.scanPhase = null;
|
||||||
this.scanComplete = false;
|
this.scanComplete = false;
|
||||||
},
|
},
|
||||||
async pollScan() {
|
async pollScan() {
|
||||||
if (!this.scanComplete) {
|
if (!this.scanComplete) {
|
||||||
// while the scan is running
|
// while the scan is running
|
||||||
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time", true);
|
let scanDetails = await this.readThingProperty(
|
||||||
|
"smart_scan",
|
||||||
|
"latest_scan_live_details",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.lastScanName = scanDetails.name;
|
||||||
|
let mtime = scanDetails.stitch_timestamp;
|
||||||
|
this.imageCount = scanDetails.image_count;
|
||||||
|
this.scanPhase = scanDetails.scan_phase;
|
||||||
|
|
||||||
if (mtime !== null) {
|
if (mtime !== null) {
|
||||||
this.lastStitchedImage = `${this.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
|
this.lastStitchedImage = `${this.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name", true);
|
|
||||||
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
|
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -163,4 +186,14 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.info-headings h3 {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If one heading follows another don't have a large top margin. */
|
||||||
|
.info-headings h3 + h3 {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue