Merge branch 'lst-image' into 'master'

Added interface to get LST as a PNG image

See merge request openflexure/openflexure-microscope-server!87
This commit is contained in:
Joel Collins 2020-11-18 15:37:26 +00:00
commit bdd6d27abf
13 changed files with 198 additions and 100 deletions

View file

@ -29,10 +29,10 @@ from labthings import create_app
from labthings.extensions import find_extensions from labthings.extensions import find_extensions
from labthings.views import View from labthings.views import View
from openflexure_microscope.microscope import Microscope
from openflexure_microscope.api.utilities import init_default_extensions, list_routes from openflexure_microscope.api.utilities import init_default_extensions, list_routes
from openflexure_microscope.api.v2 import views from openflexure_microscope.api.v2 import views
from openflexure_microscope.json import JSONEncoder from openflexure_microscope.json import JSONEncoder
from openflexure_microscope.microscope import Microscope
from openflexure_microscope.paths import ( from openflexure_microscope.paths import (
OPENFLEXURE_EXTENSIONS_PATH, OPENFLEXURE_EXTENSIONS_PATH,
OPENFLEXURE_VAR_PATH, OPENFLEXURE_VAR_PATH,
@ -135,6 +135,9 @@ labthing.add_root_link(views.ConfigurationProperty, "instrumentConfiguration")
# Attach stage resources # Attach stage resources
labthing.add_view(views.StageTypeProperty, "/instrument/stage/type") labthing.add_view(views.StageTypeProperty, "/instrument/stage/type")
# Attach camera resources
labthing.add_view(views.LSTImageProperty, "/instrument/camera/lst")
# Attach streams resources # Attach streams resources
labthing.add_view(views.MjpegStream, "/streams/mjpeg") labthing.add_view(views.MjpegStream, "/streams/mjpeg")
labthing.add_view(views.SnapshotStream, "/streams/snapshot") labthing.add_view(views.SnapshotStream, "/streams/snapshot")

View file

@ -1,6 +1,18 @@
<template> <template>
<div id="CSMSettings"> <div
<CSMCalibrationSettings /> id="CSMSettings"
class="uk-grid uk-grid-divider uk-child-width-expand"
uk-grid
>
<div class="uk-width-large">
<h3>Camera/stage mapping</h3>
<p>
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>
<CSMCalibrationSettings />
</div>
<div id="mini-stream"> <div id="mini-stream">
<miniStreamDisplay /> <miniStreamDisplay />
</div> </div>
@ -147,7 +159,8 @@ export default {
margin-right: auto; margin-right: auto;
} }
#mini-stream { #mini-stream {
width: 500px; min-width: 300px;
max-width: 600px;
text-align: center; text-align: center;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;

View file

@ -167,11 +167,4 @@ export default {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
#mini-stream {
width: 500px;
text-align: center;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
}
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div id="appSettings"> <div id="appSettings" class="uk-width-large">
<h3>Appearance</h3> <h3>Appearance</h3>
<p> <p>
<label> <label>

View file

@ -1,67 +1,68 @@
<template> <template>
<div v-if="settings" id="cameraSettings"> <div v-if="settings" id="cameraSettings">
<div> <div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
<h3>Manual camera settings</h3> <div class="uk-width-large">
<form @submit.prevent="applyConfigRequest"> <h3>Manual camera settings</h3>
<div v-if="settings.picamera"> <form @submit.prevent="applyConfigRequest">
<!--PiCamera settings block--> <div v-if="settings.picamera">
<div v-if="settings.picamera.shutter_speed !== undefined"> <!--PiCamera settings block-->
<label class="uk-form-label" for="form-stacked-text" <div v-if="settings.picamera.shutter_speed !== undefined">
>Exposure time</label <label class="uk-form-label" for="form-stacked-text"
> >Exposure time</label
<div class="uk-form-controls"> >
<input <div class="uk-form-controls">
v-model="settings.picamera.shutter_speed" <input
class="uk-input uk-form-small" v-model="settings.picamera.shutter_speed"
type="number" class="uk-input uk-form-small"
/> type="number"
/>
</div>
</div>
<div v-if="settings.picamera.analog_gain !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Analogue gain</label
>
<div class="uk-form-controls">
<input
v-model="settings.picamera.analog_gain"
class="uk-input uk-form-small"
type="number"
step="0.000001"
/>
</div>
</div>
<div v-if="settings.picamera.digital_gain !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Digital gain</label
>
<div class="uk-form-controls">
<input
v-model="settings.picamera.digital_gain"
class="uk-input uk-form-small"
type="number"
step="0.000001"
/>
</div>
</div> </div>
</div> </div>
<div v-if="settings.picamera.analog_gain !== undefined"> <button
<label class="uk-form-label" for="form-stacked-text" type="submit"
>Analogue gain</label class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
> >
<div class="uk-form-controls"> Apply Settings
<input </button>
v-model="settings.picamera.analog_gain" </form>
class="uk-input uk-form-small"
type="number"
step="0.000001"
/>
</div>
</div>
<div v-if="settings.picamera.digital_gain !== undefined"> <h3>Automatic calibration</h3>
<label class="uk-form-label" for="form-stacked-text" <cameraCalibrationSettings></cameraCalibrationSettings>
>Digital gain</label </div>
>
<div class="uk-form-controls">
<input
v-model="settings.picamera.digital_gain"
class="uk-input uk-form-small"
type="number"
step="0.000001"
/>
</div>
</div>
</div>
<button <div id="mini-stream">
type="submit" <miniStreamDisplay />
class="uk-button uk-button-primary uk-margin-small uk-width-1-1" </div>
>
Apply Settings
</button>
</form>
</div>
<!--Show auto calibrate if default plugin is enabled-->
<h3>Automatic calibration</h3>
<cameraCalibrationSettings></cameraCalibrationSettings>
<div id="mini-stream">
<miniStreamDisplay />
</div> </div>
</div> </div>
</template> </template>
@ -141,7 +142,8 @@ export default {
<style lang="less"> <style lang="less">
#mini-stream { #mini-stream {
width: 500px; min-width: 300px;
max-width: 600px;
text-align: center; text-align: center;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;

View file

@ -16,24 +16,31 @@
</taskSubmitter> </taskSubmitter>
</div> </div>
<div v-show="showExtraSettings" class="uk-child-width-expand" uk-grid> <div v-show="showExtraSettings" class="uk-child-width-expand">
<div v-if="'flatten_lens_shading_table' in recalibrationLinks"> <button
<button v-if="'flatten_lens_shading_table' in recalibrationLinks"
class="uk-button uk-button-danger uk-width-1-1" class="uk-button uk-button-danger uk-width-1-1"
@click="flattenLensShadingTableRequest" @click="flattenLensShadingTableRequest"
> >
Disable flat-field correction Disable flat-field correction
</button> </button>
</div>
<div v-if="'delete_lens_shading_table' in recalibrationLinks"> <button
<button v-if="'delete_lens_shading_table' in recalibrationLinks"
class="uk-button uk-button-danger uk-width-1-1" class="uk-button uk-button-danger uk-margin-small-top uk-width-1-1"
@click="deleteLensShadingTableRequest" @click="deleteLensShadingTableRequest"
> >
Adaptive flat-field correction Adaptive flat-field correction
</button> </button>
</div> </div>
<div v-if="LstDownloadEnabled">
<a
class="uk-button uk-button-default uk-width-large uk-margin-small-top uk-align-center"
:href="LstDownloadUri"
download
>Download Lens-Shading Table</a
>
</div> </div>
</div> </div>
</template> </template>
@ -61,18 +68,23 @@ export default {
data: function() { data: function() {
return { return {
recalibrationLinks: {}, recalibrationLinks: {},
isCalibrating: false isCalibrating: false,
LstDownloadEnabled: false
}; };
}, },
computed: { computed: {
pluginsUri: function() { pluginsUri: function() {
return `${this.$store.getters.baseUri}/api/v2/extensions`; return `${this.$store.getters.baseUri}/api/v2/extensions`;
},
LstDownloadUri: function() {
return `${this.$store.getters.baseUri}/api/v2/instrument/camera/lst`;
} }
}, },
mounted() { mounted() {
this.updateRecalibrationLinks(); this.updateRecalibrationLinks();
this.checkLstDownload();
}, },
methods: { methods: {
@ -97,6 +109,21 @@ export default {
}); });
}, },
checkLstDownload: function() {
axios
.get(this.LstDownloadUri) // Get a list of plugins
.then(response => {
if (response.status === 200) {
this.LstDownloadEnabled = true;
} else {
this.LstDownloadEnabled = false;
}
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
onRecalibrateResponse: function() { onRecalibrateResponse: function() {
this.modalNotify("Finished recalibration."); this.modalNotify("Finished recalibration.");
}, },

View file

@ -1,5 +1,5 @@
<template> <template>
<div id="appSettings"> <div id="appSettings" class="uk-width-large">
<h3>Additional features</h3> <h3>Additional features</h3>
<p class="uk-margin-small"> <p class="uk-margin-small">
<label <label
@ -7,7 +7,7 @@
Enable IHI Slide Scan</label Enable IHI Slide Scan</label
> >
</p> </p>
<p class="uk-margin-small uk-width-xlarge"> <p class="uk-margin-small">
Enabling IHI Slide Scan will add a new tab designed to simplify acquiring Enabling IHI Slide Scan will add a new tab designed to simplify acquiring
blood smear tile scans. <br /> blood smear tile scans. <br />
While this may be useful for other applications, it is primarily designed While this may be useful for other applications, it is primarily designed

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-if="settings" id="microscopeSettings"> <div v-if="settings" id="microscopeSettings" class="uk-width-large">
<form @submit.prevent="applyConfigRequest"> <form @submit.prevent="applyConfigRequest">
<label class="uk-form-label" for="form-stacked-text" <label class="uk-form-label" for="form-stacked-text"
>Backlash compensation</label >Backlash compensation</label

View file

@ -1,5 +1,5 @@
<template> <template>
<div id="stageSettings"> <div id="stageSettings" class="uk-width-large">
<div> <div>
<h3>Stage settings</h3> <h3>Stage settings</h3>
<p> <p>

View file

@ -1,11 +1,14 @@
<template> <template>
<div id="streamSettings"> <div id="streamSettings" class="uk-width-large">
<div> <div>
<h3>Stream settings</h3> <h3>Stream settings</h3>
<label <label
><input v-model="disableStream" class="uk-checkbox" type="checkbox" /> ><input v-model="disableStream" class="uk-checkbox" type="checkbox" />
Disable live stream</label Disable web stream</label
> >
<p class="uk-margin-small">
This will disable the embedded web stream of the camera.
</p>
</div> </div>
<br /> <br />
@ -30,6 +33,18 @@
> >
</div> </div>
</div> </div>
<p>
Enable GPU preview turns on a low-latency camera preview drawn directly
to the Raspberry Pi display output.
</p>
<p class="uk-margin-small">
<b
>Content, such as your mouse, won't be visible behind this preview.</b
>
Track Window will attempt to resize the GPU preview based on the current
window size, however this is often imperfect and cannot track window
movement.
</p>
</div> </div>
</div> </div>
</template> </template>

View file

@ -133,12 +133,6 @@
:current-tab="currentTab" :current-tab="currentTab"
> >
<div class="settings-pane uk-padding-small"> <div class="settings-pane uk-padding-small">
<h3>Camera/stage mapping</h3>
<p>
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>
<CSMSettings /> <CSMSettings />
</div> </div>
</tabContent> </tabContent>

View file

@ -1,4 +1,5 @@
from .actions import enabled_root_actions from .actions import enabled_root_actions
from .camera import *
from .captures import * from .captures import *
from .instrument import * from .instrument import *
from .stage import * from .stage import *

View file

@ -0,0 +1,50 @@
import logging
from io import BytesIO
import datetime
import numpy as np
from flask import send_file
from labthings import find_component
from labthings.views import PropertyView
from PIL import Image
class LSTImageProperty(PropertyView):
"""Gets the lens-shading table as an image"""
responses = {
200: {
"content_type": "image/png",
"description": "Lens-shading table in RGB format",
}
}
def get(self):
"""
Get the stage geometry.
"""
microscope = find_component("org.openflexure.microscope")
# Return intentionally empty response if no LST is available
if microscope.get_configuration()["camera"]["type"] != "PiCameraStreamer":
logging.warning("Requested an LST from a non-PiCameraStreamer camera")
return ("", 204)
# Return intentionally empty response if we have a valid PiCamera but no LST
elif getattr(microscope.camera.camera, "lens_shading_table") is None:
logging.warning("PiCamera.lens_shading_table returned as None")
return ("", 204)
else:
lst = np.asarray(microscope.camera.camera.lens_shading_table)
# R next to G1
top = np.concatenate(lst[:2], axis=1)
# G2 next to B
bottom = np.concatenate(lst[2:], axis=1)
# Combined into a 2x2 grid of greyscale images
all_channels = np.concatenate((top, bottom), axis=0)
# Create a PNG
img_bytes = BytesIO()
Image.fromarray(np.uint8(all_channels), "L").save(img_bytes, "PNG")
img_bytes.seek(0)
# Return the image
#return Response(img_bytes.getvalue(), mimetype="image/png")
fname = f"lst-{datetime.date.today().isoformat()}.png"
return send_file(img_bytes, as_attachment=True, attachment_filename=fname)