Merge branch 'master' into stage-calibration

This commit is contained in:
Richard Bowman 2020-04-10 15:16:25 +01:00
commit 70d4c7e950
23 changed files with 1285 additions and 138 deletions

View file

@ -1,10 +1,6 @@
#!/usr/bin/env python
from gevent import monkey
# Patch most system modules. Leave threads untouched so we can still use them normally if needed.
print("Monkey patching with Gevenet")
monkey.patch_all()
print("Monkey patching successful")
import time
import atexit

View file

@ -24,7 +24,3 @@ with handle_extension_error("camera stage mapping"):
from .camera_stage_mapping import csm_extension
with handle_extension_error("lens shading calibration"):
from .picamera_autocalibrate import lst_extension_v2
#FIXME: this oughtn't stay in the production version...
with handle_extension_error("custom element example"):
from ..example_extensions.custom_element import customelement_extension_v2

View file

@ -8,11 +8,13 @@ from openflexure_microscope.utilities import set_properties
import time
import numpy as np
import threading
import logging
from scipy import ndimage
from contextlib import contextmanager
from threading import Thread, Event
### Autofocus utilities
@ -25,7 +27,7 @@ class JPEGSharpnessMonitor:
self.jpeg_times = []
self.stage_positions = []
self.stage_times = []
self.stop_event = threading.Event()
self.stop_event = Event()
self.timeout = timeout
self.keep_alive()
self.background_thread = None
@ -48,26 +50,31 @@ class JPEGSharpnessMonitor:
def start(self):
"Start monitoring sharpness by looking at JPEG size"
self.background_thread = threading.Thread(target=self._measure_jpegs)
self.background_thread = Thread(target=self._measure_jpegs)
self.background_thread.start()
return self
def stop(self):
"Stop the background thread"
self.stop_event.set()
print("Joining JPEG thread")
self.background_thread.join()
def _measure_jpegs(self):
"Function that runs in a background thread to record sharpness"
logging.info("Starting sharpness measurement in background thread")
logging.debug("Starting sharpness measurement in background thread")
self.keep_alive()
logging.debug(f"_measure_jpegs stop_event: {self.stop_event.is_set()}")
while not self.stop_event.is_set() and not self.should_stop():
self.jpeg_sizes.append(self.jpeg_size())
self.jpeg_times.append(time.time())
size_now = self.jpeg_size()
time_now = time.time()
self.jpeg_sizes.append(size_now)
self.jpeg_times.append(time_now)
print("Exited JPEG measure loop")
if self.stop_event.is_set():
logging.info("Cleanly stopped sharpness measurement in background thread")
logging.debug("Cleanly stopped sharpness measurement in background thread")
if self.should_stop():
logging.info("Sharpness measurement timed out and has stopped")
logging.debug("Sharpness measurement timed out and has stopped")
def jpeg_size(self):
"""Return the size of a frame from the MJPEG stream"""
@ -195,7 +202,7 @@ def move_and_find_focus(microscope, dz):
def fast_autofocus(microscope, dz=2000, backlash=None):
"""Perform a down-up-down-up autofocus"""
with monitor_sharpness(microscope) as m, microscope.camera.lock:
with monitor_sharpness(microscope) as m, microscope.camera.lock, microscope.stage.lock:
i, z = m.focus_rel(-dz / 2)
i, z = m.focus_rel(dz)
fz = m.sharpest_z_on_move(i)
@ -245,17 +252,20 @@ def fast_up_down_up_autofocus(
might slightly hurt accuracy, but is unlikely to be a big issue. Too big
may cause you to overshoot, which is a problem.
"""
with monitor_sharpness(microscope) as m, microscope.camera.lock:
with monitor_sharpness(microscope) as m, microscope.camera.lock, microscope.stage.lock:
# Ensure the MJPEG stream has started
microscope.camera.start_stream_recording()
df = dz # TODO: refactor so I actually use dz in the code below!
logging.debug("Initial move")
if initial_move_up:
m.focus_rel(df / 2)
# move down
logging.debug("Move down")
i, z = m.focus_rel(-df)
# now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack
logging.debug("Get target_s")
jt, jz, js = m.move_data(i)
best_z = jz[np.argmax(js)]
target_s = np.interp(
@ -263,11 +273,13 @@ def fast_up_down_up_autofocus(
) # NB jz is decreasing
# now move to the start of the z stack
logging.debug("Move to the start of the z stack")
i, z = m.focus_rel(
best_z + target_z - z + mini_backlash
) # takes us to the start of the stack
# We've deliberately undershot - figure out how much further we should move based on the curve
logging.debug("Calculate remining movement")
current_js = m.jpeg_size()
imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order
@ -279,6 +291,7 @@ def fast_up_down_up_autofocus(
# So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow]
logging.debug("Correction move")
correction_move = best_z + target_z - jz[inow]
logging.debug(
"Fast autofocus scan: correcting backlash by moving {} steps".format(
@ -317,7 +330,7 @@ class AutofocusAPI(View):
dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
if microscope.has_real_stage():
logging.info("Running autofocus...")
logging.debug("Running autofocus...")
task = taskify(autofocus)(microscope, dz)
# return a handle on the autofocus task
@ -348,7 +361,7 @@ class FastAutofocusAPI(View):
backlash = 0
if microscope.has_real_stage():
logging.info("Running autofocus...")
logging.debug("Running autofocus...")
task = taskify(fast_up_down_up_autofocus)(microscope, dz=dz, mini_backlash=backlash)
# return a handle on the autofocus task

View file

@ -7,10 +7,35 @@ from labthings.core.tasks import taskify
from flask import abort
from contextlib import contextmanager
import logging
from .recalibrate_utils import recalibrate_camera, auto_expose_and_freeze_settings
# Type hinting
from typing import Tuple
from .recalibrate_utils import recalibrate_camera, auto_expose_and_freeze_settings, flat_lens_shading_table
@contextmanager
def pause_stream(scamera, resolution: Tuple[int, int] = None):
"""This context manager locks a streaming camera, and pauses the stream.
The stream is re-enabled, with the original resolution, once the with
block has finished.
"""
with scamera.lock:
assert not scamera.record_active, "We can't pause the camera's video stream while a recording is in progress."
streaming = scamera.stream_active
old_resolution = scamera.camera.resolution
if streaming:
logging.info("Stopping stream in pause_stream context manager")
scamera.stop_stream_recording(resolution=resolution)
try:
yield scamera
finally:
scamera.camera.resolution = old_resolution
if streaming:
logging.info("Restarting stream in pause_stream context manager")
scamera.start_stream_recording()
def recalibrate(microscope):
"""Reset the camera's settings.
@ -19,24 +44,10 @@ def recalibrate(microscope):
table such that the background is as uniform as possible
with a gray level of 230. It takes a little while to run.
"""
scamera = microscope.camera
with scamera.lock:
assert not scamera.record_active, "Can't recalibrate while recording!"
streaming = scamera.stream_active
if streaming:
logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480))
old_resolution = scamera.camera.resolution
try:
scamera.camera.resolution = (640, 480)
auto_expose_and_freeze_settings(scamera.camera)
recalibrate_camera(scamera.camera)
finally:
scamera.camera.resolution = old_resolution
microscope.save_settings()
if streaming:
logging.info("Restarting stream after recalibration")
scamera.start_stream_recording()
with pause_stream(microscope.camera) as scamera:
auto_expose_and_freeze_settings(scamera.camera) # scamera.camera is the PiCamera object
recalibrate_camera(scamera.camera)
microscope.save_settings()
@ThingAction
@ -52,9 +63,42 @@ class RecalibrateView(View):
return taskify(recalibrate)(microscope)
@ThingAction
class FlattenLSTView(View):
def post(self):
microscope = find_component("org.openflexure.microscope")
if not microscope:
abort(503, "No microscope connected. Unable to flatten the lens shading table.")
try:
with pause_stream(microscope.camera) as scamera:
flat_lst = flat_lens_shading_table(scamera.camera)
scamera.camera.lens_shading_table = flat_lst
microscope.save_settings()
except:
logging.exception("Error flattening the lens shading table.")
abort(503, "Couldn't flatten the lens shading table - do you have the forked PiCamera library installed?")
@ThingAction
class DeleteLSTView(View):
def post(self):
microscope = find_component("org.openflexure.microscope")
if not microscope:
abort(503, "No microscope connected. Unable to flatten the lens shading table.")
try:
with pause_stream(microscope.camera) as scamera:
scamera.camera.lens_shading_table = None
microscope.save_settings()
except:
logging.exception("Error deleting the lens shading table.")
abort(503, "Couldn't flatten the lens shading table - do you have the forked PiCamera library installed?")
lst_extension_v2 = BaseExtension(
"org.openflexure.calibration.picamera", version="2.0.0-beta.1"
"org.openflexure.calibration.picamera", version="2.0.0-beta.1", description="Routines to perform flat-field correction on the camera."
)
lst_extension_v2.add_method(
@ -62,3 +106,5 @@ lst_extension_v2.add_method(
)
lst_extension_v2.add_view(RecalibrateView, "/recalibrate")
lst_extension_v2.add_view(FlattenLSTView, "/flatten_lens_shading_table")
lst_extension_v2.add_view(DeleteLSTView, "/delete_lens_shading_table")

View file

@ -198,6 +198,7 @@ def tile(
else:
# Run slow autofocus. Client should provide dz ~ 50
autofocus_extension.autofocus(
microscope,
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
)
logging.debug("Finished autofocus")

View file

@ -42,13 +42,18 @@ customelement_extension_v2 = CustomElementExtension()
def wc_func():
return {
"href": customelement_extension_v2.static_file_url("my-custom-element.min.js"),
"name": "my-custom-element",
"href": customelement_extension_v2.static_file_url("vue-web-component.min.js"),
"name": "vue-web-component",
}
def gui_func():
return {"icon": "pets", "title": "Element", "viewPanel": "stream", "wc": wc_func()}
return {
"icon": "grid_on",
"title": "Med Scan",
"viewPanel": "stream",
"wc": wc_func(),
}
customelement_extension_v2.add_meta("wc", wc_func)

View file

@ -0,0 +1,22 @@
# Example Web Component using Vue framework
## Structure
### Web component
* `/src/components/VueWebComponent.vue`
### Debug app
* For debugging purposes, the component can be mounted into a demo app
* `/src/App.vue` and `/src/main.js` provide demo app functionality, and do not affect the final built web component
## Building
* Run `npm run build`
## Debugging
* Run `npm run serve` to serve the demo app. This can be used to debug the component. Note, the base URL will be set to it's default value of a localhost microscope server

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18020f0209272ab81d01b2d68da517fcff98dd07d2ad7fe738c66df75003f7cf
oid sha256:e07e1d811b5182b4e96ab2a29d82db90c8de77d340844cf0acd284388154a671
size 197

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94cc5b4e5b3ddb8c6b94ef9b81b49060c582935aedf6c7d1626f4166c1827442
size 288006

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba98b02d051bf6f7d2d4302154512a72ae6ddc1eff261e171fd92a067e1bdf84
size 92018

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f03a6823dea074cef32557902c64407cf67899aaa7f6c0d19ff7746bd149fbe0
size 288124

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:155a1dc5d6cbdc787012f83bf82c84b97944ab6b2141f9c566faa375571a3f4a
size 92117

View file

@ -1,9 +1,10 @@
{
"name": "vue-web-component-project",
"name": "vue-web-component",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "vue-cli-service build --target wc --inline-vue --name my-custom-element ./src/components/VueWebComponent.vue"
"serve": "vue-cli-service serve",
"build": "cross-var vue-cli-service build --target wc --inline-vue --name $npm_package_name ./src/components/VueWebComponent.vue"
},
"dependencies": {
"axios": "^0.19.2",
@ -13,6 +14,7 @@
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-service": "^4.1.0",
"cross-var": "^1.1.0",
"vue-template-compiler": "^2.6.10"
}
}

View file

@ -0,0 +1,18 @@
<template>
<div id="app">
<VueWebComponent/>
</div>
</template>
<script>
import VueWebComponent from './components/VueWebComponent.vue'
export default {
name: 'app',
components: {
VueWebComponent
}
}
</script>
<style>
</style>

View file

@ -20,7 +20,7 @@ export default {
props: {
'componentBaseURL': {
required: false,
default: null,
default: "http://localhost:5000/api/v2",
type: String
}
},
@ -33,7 +33,8 @@ export default {
};
},
mounted () {
mounted: function() {
console.log(this.componentBaseURL)
if (this.componentBaseURL){
axios
.get(`${this.componentBaseURL}`)

View file

@ -1,7 +1,6 @@
import Vue from 'vue';
import wrap from '@vue/web-component-wrapper';
import VueWebComponent from './components/VueWebComponent';
import App from './App.vue';
const CustomElement = wrap(Vue, VueWebComponent);
window.customElements.define('my-custom-element', CustomElement);
new Vue({
render: h => h(App)
}).$mount('#app')

View file

@ -1,3 +1,4 @@
process.env.VUE_APP_NAME = require('./package.json').name
module.exports = {
productionSourceMap: false
};