Merge branch 'gain-exposure-settings' into 'master'
Gain & exposure settings See merge request openflexure/openflexure-microscope-server!8
This commit is contained in:
commit
7088d78244
5 changed files with 20 additions and 174 deletions
|
|
@ -2,6 +2,7 @@ from openflexure_microscope.api.utilities import gen, JsonPayload
|
||||||
from openflexure_microscope.api.v1.views import MicroscopeView
|
from openflexure_microscope.api.v1.views import MicroscopeView
|
||||||
|
|
||||||
from flask import Response, Blueprint, jsonify, request
|
from flask import Response, Blueprint, jsonify, request
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class StreamAPI(MicroscopeView):
|
class StreamAPI(MicroscopeView):
|
||||||
|
|
@ -99,8 +100,6 @@ class ConfigAPI(MicroscopeView):
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"analog_gain": 1.0,
|
|
||||||
"digital_gain": 1.0,
|
|
||||||
"image_resolution": [
|
"image_resolution": [
|
||||||
2592,
|
2592,
|
||||||
1944
|
1944
|
||||||
|
|
@ -111,7 +110,9 @@ class ConfigAPI(MicroscopeView):
|
||||||
1312,
|
1312,
|
||||||
976
|
976
|
||||||
],
|
],
|
||||||
"picamera_params": {
|
"picamera_settings": {
|
||||||
|
"analog_gain": 1.0,
|
||||||
|
"digital_gain": 1.0,
|
||||||
"awb_gains": [
|
"awb_gains": [
|
||||||
0.92578125,
|
0.92578125,
|
||||||
2.94921875
|
2.94921875
|
||||||
|
|
@ -125,7 +126,6 @@ class ConfigAPI(MicroscopeView):
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"openflexure_microscope.plugins.default:Plugin"
|
"openflexure_microscope.plugins.default:Plugin"
|
||||||
],
|
],
|
||||||
"shading_table_path": "/home/pi/.openflexure/microscopelst.npy",
|
|
||||||
"video_resolution": [
|
"video_resolution": [
|
||||||
832,
|
832,
|
||||||
624
|
624
|
||||||
|
|
@ -170,7 +170,7 @@ class ConfigAPI(MicroscopeView):
|
||||||
"""
|
"""
|
||||||
payload = JsonPayload(request)
|
payload = JsonPayload(request)
|
||||||
|
|
||||||
print(payload.json)
|
logging.debug("Updating settings from POST request.")
|
||||||
|
|
||||||
self.microscope.write_config(payload.json)
|
self.microscope.write_config(payload.json)
|
||||||
self.microscope.save_config()
|
self.microscope.save_config()
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,14 @@ class BaseCamera(object):
|
||||||
self.images = self.load_capture_db(self.images_db)
|
self.images = self.load_capture_db(self.images_db)
|
||||||
self.videos = self.load_capture_db(self.videos_db)
|
self.videos = self.load_capture_db(self.videos_db)
|
||||||
|
|
||||||
|
def apply_config(self, config):
|
||||||
|
"""Update settings from a config dictionary"""
|
||||||
|
self.config.update(config)
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
"""Return the current settings as a dictionary"""
|
||||||
|
return self.config
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Create camera on context enter."""
|
"""Create camera on context enter."""
|
||||||
return self
|
return self
|
||||||
|
|
|
||||||
|
|
@ -184,12 +184,18 @@ class StreamingCamera(BaseCamera):
|
||||||
set_digital_gain(self.camera, value)
|
set_digital_gain(self.camera, value)
|
||||||
elif key == 'analog_gain':
|
elif key == 'analog_gain':
|
||||||
set_analog_gain(self.camera, value)
|
set_analog_gain(self.camera, value)
|
||||||
|
elif key == "shutter_speed":
|
||||||
|
# TODO: use types from CONFIG_KEYS? @jtc42
|
||||||
|
self.camera.shutter_speed = int(value)
|
||||||
else:
|
else:
|
||||||
setattr(self.camera, key, value) # Write setting to camera
|
setattr(self.camera, key, value) # Write setting to camera
|
||||||
|
|
||||||
# StreamingCamera parameters (applied via StreamingCamera config)
|
# StreamingCamera parameters (applied via StreamingCamera config)
|
||||||
for key, value in config.items(): # For each provided setting
|
for key, value in config.items(): # For each provided setting
|
||||||
if key != 'picamera_settings': # We already handled this
|
if key != 'picamera_settings': # We already handled this
|
||||||
|
if key not in CONFIG_KEYS.keys():
|
||||||
|
logging.warn("{} is not in the streaming camera settings dictionary - adding it.")
|
||||||
|
#continue #TODO: filter settings somehow?
|
||||||
logging.debug("Setting parameter {}: {}".format(key, value))
|
logging.debug("Setting parameter {}: {}".format(key, value))
|
||||||
self.config[key] = value
|
self.config[key] = value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ class Microscope(object):
|
||||||
# If attached to a camera
|
# If attached to a camera
|
||||||
if self.camera:
|
if self.camera:
|
||||||
# Update camera config
|
# Update camera config
|
||||||
self.camera.config.update(config)
|
self.camera.apply_config(config)
|
||||||
|
|
||||||
# If attached to a stage
|
# If attached to a stage
|
||||||
# TODO: Convert stage settings into a config expansion
|
# TODO: Convert stage settings into a config expansion
|
||||||
|
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
import time
|
|
||||||
import numpy as np
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from openflexure_microscope.plugins import MicroscopePlugin
|
|
||||||
from openflexure_microscope.utilities import set_properties
|
|
||||||
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
|
|
||||||
from openflexure_microscope.api.utilities import JsonPayload
|
|
||||||
from flask import request, jsonify, abort, current_app
|
|
||||||
import logging
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
class JPEGSharpnessMonitor():
|
|
||||||
def __init__(self, microscope, timeout=60):
|
|
||||||
self.microscope = microscope
|
|
||||||
self.camera = microscope.camera
|
|
||||||
self.stage = microscope.stage
|
|
||||||
self.jpeg_sizes = []
|
|
||||||
self.jpeg_times = []
|
|
||||||
self.stage_positions = []
|
|
||||||
self.stage_times = []
|
|
||||||
self.stop_event = threading.Event()
|
|
||||||
self.timeout = timeout
|
|
||||||
self.keep_alive()
|
|
||||||
self.background_thread = None
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
if self.background_thread is None:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return self.background_thread.is_alive()
|
|
||||||
|
|
||||||
def should_stop(self):
|
|
||||||
import time
|
|
||||||
return time.time() - self.kept_alive > self.timeout
|
|
||||||
|
|
||||||
def keep_alive(self):
|
|
||||||
import time
|
|
||||||
self.kept_alive = time.time()
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"Start monitoring sharpness by looking at JPEG size"
|
|
||||||
self.background_thread = threading.Thread(target=self._measure_jpegs)
|
|
||||||
self.background_thread.start()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"Stop the background thread"
|
|
||||||
self.stop_event.set()
|
|
||||||
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")
|
|
||||||
self.keep_alive()
|
|
||||||
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())
|
|
||||||
if self.stop_event.is_set():
|
|
||||||
logging.info("Cleanly stopped sharpness measurement in background thread")
|
|
||||||
if self.should_stop():
|
|
||||||
logging.info("Sharpness measurement timed out and has stopped")
|
|
||||||
|
|
||||||
def jpeg_size(self):
|
|
||||||
"""Return the size of a frame from the MJPEG stream"""
|
|
||||||
return len(self.camera.get_frame())
|
|
||||||
|
|
||||||
|
|
||||||
def focus_rel(self, dz, backlash=False, **kwargs):
|
|
||||||
self.keep_alive()
|
|
||||||
self.stage_times.append(time.time())
|
|
||||||
self.stage_positions.append(self.stage.position)
|
|
||||||
self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs)
|
|
||||||
self.stage_times.append(time.time())
|
|
||||||
self.stage_positions.append(self.stage.position)
|
|
||||||
i = len(self.stage_positions) - 2
|
|
||||||
return i, self.stage_positions[-1][2]
|
|
||||||
|
|
||||||
def move_data(self, istart, istop=None):
|
|
||||||
"Extract sharpness as a function of (interpolated) z"
|
|
||||||
global np, logging
|
|
||||||
if istop is None:
|
|
||||||
istop = istart + 2
|
|
||||||
jpeg_times = np.array(self.jpeg_times)
|
|
||||||
jpeg_sizes = np.array(self.jpeg_sizes)
|
|
||||||
stage_times = np.array(self.stage_times)[istart:istop]
|
|
||||||
stage_zs = np.array(self.stage_positions)[istart:istop, 2]
|
|
||||||
start = np.argmax(jpeg_times > stage_times[0])
|
|
||||||
stop = np.argmax(jpeg_times > stage_times[1])
|
|
||||||
if stop < 1:
|
|
||||||
stop = len(jpeg_times)
|
|
||||||
logging.debug("changing stop to {}".format(stop))
|
|
||||||
jpeg_times = jpeg_times[start:stop]
|
|
||||||
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
|
|
||||||
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
|
|
||||||
|
|
||||||
def sharpest_z_on_move(self, index):
|
|
||||||
"""Return the z position of the sharpest image on a given move"""
|
|
||||||
jt, jz, js = self.move_data(index)
|
|
||||||
return jz[np.argmax(js)]
|
|
||||||
|
|
||||||
def data_dict(self):
|
|
||||||
"""Return the gathered data as a single convenient dictionary"""
|
|
||||||
data = {}
|
|
||||||
for k in ['jpeg_times', 'jpeg_sizes', 'stage_times', 'stage_positions']:
|
|
||||||
data[k] = getattr(self, k)
|
|
||||||
return data
|
|
||||||
|
|
||||||
class FastAutofocusAPI(MicroscopeViewPlugin):
|
|
||||||
def post(self):
|
|
||||||
payload = JsonPayload(request)
|
|
||||||
|
|
||||||
# Figure out the parameters to use
|
|
||||||
dz = payload.param("dz", default=2000, convert=int)
|
|
||||||
backlash = payload.param("backlash", default=None, convert=int)
|
|
||||||
if backlash < 0:
|
|
||||||
backlash = None
|
|
||||||
|
|
||||||
print("Running autofocus...")
|
|
||||||
task = self.microscope.task.start(self.plugin.fast_autofocus, dz, backlash=backlash)
|
|
||||||
|
|
||||||
# return a handle on the autofocus task
|
|
||||||
return jsonify(task.state), 202
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(MicroscopePlugin):
|
|
||||||
"""
|
|
||||||
Plugin to provide fast autofocus
|
|
||||||
"""
|
|
||||||
JPEGSharpnessMonitor = JPEGSharpnessMonitor # make the class available
|
|
||||||
def sharpness_monitor(self):
|
|
||||||
return JPEGSharpnessMonitor(self.microscope)
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def monitor_sharpness(self):
|
|
||||||
m = self.sharpness_monitor()
|
|
||||||
m.start()
|
|
||||||
try:
|
|
||||||
yield m
|
|
||||||
finally:
|
|
||||||
m.stop()
|
|
||||||
|
|
||||||
def move_and_find_focus(self, dz):
|
|
||||||
"""Make a relative Z move and return the peak sharpness position"""
|
|
||||||
with self.monitor_sharpness() as m:
|
|
||||||
m.focus_rel(dz)
|
|
||||||
return m.sharpest_z_on_move(0)
|
|
||||||
|
|
||||||
|
|
||||||
def fast_autofocus(self, dz=2000, backlash=None):
|
|
||||||
"""Perform a down-up-down-up autofocus"""
|
|
||||||
with self.monitor_sharpness() as m:
|
|
||||||
i, z = m.focus_rel(-dz/2)
|
|
||||||
i, z = m.focus_rel(dz)
|
|
||||||
fz = m.sharpest_z_on_move(i)
|
|
||||||
if backlash is None:
|
|
||||||
i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent
|
|
||||||
else:
|
|
||||||
i, z = m.focus_rel(fz - z - backlash)
|
|
||||||
m.focus_rel(fz - z)
|
|
||||||
return m.data_dict()
|
|
||||||
|
|
||||||
|
|
||||||
api_views = {
|
|
||||||
'/fast_autofocus': FastAutofocusAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue