Merge branch 'gui-dzi-viewer' into 'v3'

Gui dzi viewer

See merge request openflexure/openflexure-microscope-server!266
This commit is contained in:
Julian Stirling 2025-05-23 15:45:30 +00:00
commit 58a4dc521d
6 changed files with 218 additions and 12 deletions

View file

@ -44,3 +44,10 @@ def add_static_files(app: FastAPI) -> None:
StaticFiles(directory=fpath),
name=f"static_{fname}",
)
# Mount the scan directory to .../scans/, to allow dzi viewing
app.mount(
"/scans/",
StaticFiles(directory="/var/openflexure/scans/"),
name="scans",
)

View file

@ -77,6 +77,7 @@ class ScanInfo(BaseModel):
modified: datetime
number_of_images: int
stitch_available: bool
dzi: Optional[str]
DOWNLOADABLE_SCAN_FILES = (
@ -819,6 +820,11 @@ class SmartScanThing(Thing):
]
number_of_images = len(scan_images)
stitch_available = len(stitches) > 0
dzi = [i for i in folder_contents if i.endswith("dzi")]
if len(dzi) > 0:
dzi = str(dzi[0])
else:
dzi = None
else:
number_of_images = 0
stitch_available = False
@ -831,6 +837,7 @@ class SmartScanThing(Thing):
modified=modified,
number_of_images=number_of_images,
stitch_available=stitch_available,
dzi=dzi,
)
)
return scans
@ -925,7 +932,7 @@ class SmartScanThing(Thing):
raise FileNotFoundError("No latest scan found")
images_dir = self.images_dir_for_scan(self.latest_scan_name)
stitch_path = os.path.join(images_dir, "stitched_from_stage.jpg")
stitch_path = os.path.join(images_dir, "preview.jpg")
if not os.path.isfile(stitch_path):
raise FileNotFoundError("Latest scan has no preview stitch")
return stitch_path
@ -980,7 +987,7 @@ class SmartScanThing(Thing):
[
STITCHING_CMD,
"--stitching_mode",
"only_stage_stitch",
"preview_stitch",
"--minimum_overlap",
f"{min_overlap}",
self._ongoing_scan_images_dir,
@ -1096,6 +1103,7 @@ class SmartScanThing(Thing):
"--stitching_mode",
"all",
f"{tiff_arg}",
"--stitch_dzi",
"--minimum_overlap",
f"{round(overlap * 0.9, 2)}",
images_folder,
@ -1158,6 +1166,7 @@ class SmartScanThing(Thing):
"stitched_from",
"stitched.om",
"stitching_correlations",
"preview.jp",
]
with zipfile.ZipFile(zip_fname, mode="a") as scan_zip:

View file

@ -13,6 +13,7 @@
"material-design-icons": "^3.0",
"material-symbols": "^0.14.3",
"mousetrap": "^1.6.5",
"openseadragon": "^5.0.0",
"vue-observe-visibility": "^0.4"
},
"devDependencies": {
@ -10539,6 +10540,14 @@
"opener": "bin/opener-bin.js"
}
},
"node_modules/openseadragon": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/openseadragon/-/openseadragon-5.0.1.tgz",
"integrity": "sha512-a/hjouW9i3UfWxRADVYN2MyRhXMGnE7x9VVL7/4jXCcDLFyO4UM5o4RStYtqa5BfaHw/wMNAaD2WbxQF8f1pJg==",
"funding": {
"url": "https://opencollective.com/openseadragon"
}
},
"node_modules/opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",

View file

@ -17,6 +17,7 @@
"material-design-icons": "^3.0",
"material-symbols": "^0.14.3",
"mousetrap": "^1.6.5",
"openseadragon": "^5.0.0",
"vue-observe-visibility": "^0.4"
},
"devDependencies": {

View file

@ -0,0 +1,96 @@
<template>
<div v-observe-visibility="visibilityChanged">
<div id="openseadragon"></div>
</div>
</template>
<script>
import OpenSeaDragon from "openseadragon";
// Export main app
export default {
name: "OpenSeadragonViewer",
components: { },
props: {
src: {
type: String,
required: true
}
},
data: function() {
return {
osdViewer: null
};
},
watch: {
src: {
immediate: true,
handler(newVal) {
this.loadOpenSeaDragon(newVal);
}
}
},
async mounted() {
if (this.src) {
this.loadOpenSeaDragon(this.src);
}
},
beforeDestroy() {
// Remove global signal listener to perform a gallery refresh
this.osdViewer.destroy();
},
methods: {
visibilityChanged(isVisible) {
if (isVisible) {
this.loadOpenSeaDragon();
}else{
this.osdViewer.destroy();
}
},
async loadOpenSeaDragon() {
if (this.osdViewer) {
this.osdViewer.destroy();
}
this.osdViewer = OpenSeaDragon({
id: "openseadragon",
crossOriginPolicy: 'Anonymous',
tileSources: this.src,
showNavigationControl: false,
maxZoomPixelRatio: 2,
gestureSettingsMouse: {
clickToZoom: false
}
});
},
openFullscreen() {
if (this.osdViewer) {
this.osdViewer.setFullScreen(true);
}
},
}
};
</script>
<style lang="less" scoped>
#openseadragon {
width: 100%;
height: 100%;
background-color: black;
z-index:1;
}
#info-panel {
position: relative;
top: 10px;
left: 10px;
background-color: rgba(255, 255, 255, 0.7);
padding: 5px;
border-radius: 1px;
z-index: 1000;
}
</style>

View file

@ -1,8 +1,7 @@
<template>
<div
v-observe-visibility="visibilityChanged"
class="galleryDisplay uk-padding uk-padding-remove-top"
>
class="galleryDisplay uk-padding uk-padding-remove-top">
<!-- Gallery nav bar -->
<nav
class="gallery-navbar uk-navbar-container uk-navbar-transparent"
@ -35,6 +34,26 @@
</div>
</nav>
<!-- Modal for scan display -->
<div id="scan-modal" ref="scanModal" style="padding: 10px;" uk-modal>
<div class="uk-modal-dialog uk-modal-body " v-if="selectedScan" style="padding: 10px; width: 95%; height: 95%;">
<h2 class="uk-modal-title">
{{ selectedScan.name }}
<button class="uk-modal-close uk-float-right" type="button"><span class="material-symbols-outlined">close</span></button>
<button class="uk-float-right" type="button" @click="goFullscreen">
<span class="material-symbols-outlined">fullscreen</span>
</button>
</h2>
<div v-if="selectedScanDZIAvailable" id="viewer_container" style="height: 80%;">
<OpenSeadragonViewer
id="openseadragon"
ref="openseadragon"
:src="selectedScanDZI"
/>
</div>
</div>
</div>
<!-- Gallery -->
<div
v-if="$store.getters.ready"
@ -61,11 +80,13 @@
class="thumbnail-fit"
:src="thumbnailPath(item.name)"
onerror="this.src='/titleiconpink.svg';"
@click="showScan(item)"
/>
</div>
</div>
<h3 class="uk-card-title" style="text-align: center;">{{ item.name }}</h3>
<action-button
<div class="button-container">
<action-button
thing="smart_scan"
action="download_zip"
submit-label="Download ZIP"
@ -85,40 +106,54 @@
submit-label="Stitch Images"
thing="smart_scan"
action="stitch_scan"
v-if="item.can_stitch"
v-if="item.can_stitch | !item.dzi"
:can-terminate="false"
:submit-data="{ scan_name: item.name }"
:button-primary="false"
:modal-progress="true"
@error="modalError"
/>
<button
v-if="item.dzi" class="uk-button uk-button-default uk-width-1-1"
@click="showScan(item)"
>
Show Stitched Scan
</button>
</div>
<div>
<ul>
<li>{{ item.number_of_images }} images</li>
<li>created: {{ formatDate(item.created) }}</li>
<li>modified: {{ formatDate(item.modified) }}</li>
<li v-if="item.number_of_images<3" style="color:red; font-weight: bold;">Not enough images to stitch</li>
<li v-else-if=!item.stitch_available style="color:red; font-weight: bold;">Scan not stitched</li>
</ul>
</div>
<li v-if="item.number_of_images<3" class="warning-msg">Not enough images to stitch</li>
<li v-else-if="!item.dzi & item.stitch_available" class="alert-msg">Interactive preview not available</li>
<li v-else-if=!item.stitch_available class="alert-msg">High quality stitch not available</li>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
import UIkit from "uikit";
import actionButton from "../labThingsComponents/actionButton.vue";
import OpenSeadragonViewer from "./scanListComponents/openSeadragonViewer.vue";
// Export main app
export default {
name: "ScanListContent",
components: { actionButton },
components: { actionButton, OpenSeadragonViewer },
data: function() {
return {
scans: []
scans: [],
selectedScan: null,
osdViewer: null
};
},
@ -133,6 +168,16 @@ export default {
},
scansEmpty() {
return this.scans.length == 0;
},
selectedScanDZI() {
if (this.selectedScan && this.dzi!="") {
return `${this.$store.getters.baseUri}/scans/${this.selectedScan.name}/images/${this.selectedScan.dzi}`;
} else {
return null;
}
},
selectedScanDZIAvailable() {
return this.selectedScan && this.selectedScan.dzi;
}
},
@ -185,6 +230,9 @@ export default {
this.updateScans();
}
},
goFullscreen() {
this.$refs.openseadragon.openFullscreen();
},
async updateScans() {
try {
let scans = await this.readThingProperty("smart_scan", "scans");
@ -256,7 +304,16 @@ export default {
console.log(link);
document.body.appendChild(link);
link.click();
}
},
showScan(scan) {
if (scan.dzi){
this.selectedScan = scan;
UIkit.modal(this.$refs.scanModal).show();
}
else {
this.modalError(`Scan not stitched for viewing in webapp, please download or stitch`)
}
},
}
};
</script>
@ -285,6 +342,20 @@ export default {
margin-bottom: 20px;
}
#openseadragon {
width: 100%;
height: 100%;
}
#info-panel {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(255, 255, 255, 0.7);
padding: 5px;
border-radius: 1px;
z-index: 1000;
}
/deep/ .capture-card {
width: 300px;
height: 100%; // Used to have all cards in a row match their heights
@ -296,5 +367,18 @@ ul {
display: inline-block;
text-align: center;
list-style-type:none;
margin: 5px 0px 10px 0px;
}
.warning-msg {
color: red;
text-align: center;
font-weight: bold;
}
.alert-msg{
color: orange;
text-align: center;
font-weight: bold;
}
</style>