Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 73 additions & 41 deletions PYME/Acquire/Hardware/driftTracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,65 @@ def correlateAndCompareFrames(A, B):

return (As -B).mean(), dx, dy

from PYME.contrib import dispatch
class StandardFrameSource(object):
'''This is a simple source which emits frames once per polling interval of the frameWrangler
(i.e. corresponding to the onFrameGroup signal of the frameWrangler).

The intention is to reproduce the historical behaviour of the drift tracking code, whilst
abstracting some of the detailed knowledge of frame handling out of the actual tracking code.

'''
def __init__(self, frameWrangler):
self._fw = frameWrangler
self._on_frame = dispatch.Signal(['frameData'])
self._fw.onFrameGroup.connect(self.tick)


def tick(self, *args, **kwargs):
self._on_frame.send(sender=self, frameData=self._fw.currentFrame)

@property
def shape(self):
return self._fw.currentFrame.shape

def connect(self, callback):
self._on_frame.connect(callback)

def disconnect(self, callback):
self._on_frame.disconnect(callback)

class OIDICFrameSource(StandardFrameSource):
""" Emit frames from the camera to the tracking code only for a single OIDIC orientation.

Currently a straw man / skeleton pending details of OIDIC code.

TODO - should this reside here, or with the other OIDIC code (which I believe to be in a separate repo)?

"""

def __init__(self, frameWrangler, oidic_controller, oidic_orientation=0):
super().__init__(frameWrangler)

self._oidic = oidic_controller
self._target_orientation = oidic_orientation

def tick(self, *args, **kwargs):
# FIXME - change to match actual naming etc ... in OIDIC code.
# FIXME - check when onFrameGroup is emitted relative to when the OIDIC orientation is set.
# Is this predictable, or does it depend on the order in which OIDIC and drift tracking are
# registered with the frameWrangler?
if self._oidic.orientation == self._target_orientation:
super().tick(*args, **kwargs)
else:
# clobber all frames coming from camera when not in the correct DIC orientation
pass
class Correlator(object):
def __init__(self, scope, piezo=None):
self.scope = scope
def __init__(self, scope, piezo=None, frame_source=None):
self.piezo = piezo

if frame_source is None:
self.frame_source = StandardFrameSource(scope.frameWrangler)

self.focusTolerance = .05 #how far focus can drift before we correct
self.deltaZ = 0.2 #z increment used for calibration
Expand All @@ -91,8 +145,8 @@ def __init__(self, scope, piezo=None):
self.maxfac = 1.5e3
self.Zfactor = 1.0

def initialise(self):
d = 1.0*self.scope.frameWrangler.currentFrame.squeeze()
def _initialise(self, frame_data):
d = 1.0*frame_data.squeeze()

self.X, self.Y = np.mgrid[0.0:d.shape[0], 0.0:d.shape[1]]
# self.X -= d.shape[0]/2
Expand Down Expand Up @@ -124,37 +178,14 @@ def initialise(self):
self.historyCorrections = []


# def setRefA(self):
# d = 1.0*self.scope.frameWrangler.currentFrame.squeeze()
# self.refA = d/d.mean() - 1
# self.FA = ifftn(self.refA)
# self.refA *= self.mask

# def setRefB(self):
# d = 1.0*self.scope.frameWrangler.currentFrame.squeeze()
# self.refB = d/d.mean() - 1
# self.refB *= self.mask

# def setRefC(self):
# d = 1.0*self.scope.frameWrangler.currentFrame.squeeze()
# self.refC = d/d.mean() - 1
# self.refC *= self.mask

# self.dz = (self.refC - self.refB).ravel()
# self.dzn = 2./np.dot(self.dz, self.dz)

def setRefN(self, N):
d = 1.0*self.scope.frameWrangler.currentFrame.squeeze()
def _setRefN(self, frame_data, N):
d = 1.0*frame_data.squeeze()
ref = d/d.mean() - 1
self.refImages[:,:,N] = ref
self.calFTs[:,:,N] = ifftn(ref)
self.calImages[:,:,N] = ref*self.mask

#def setRefD(self):
# self.refD = (1.0*self.d).squeeze()/self.d.mean() - 1
# self.refD *= self.mask

#self.dz = (self.refC - self.refA).ravel()

def set_focus_tolerance(self, tolerance):
""" Set the tolerance for locking position
Expand Down Expand Up @@ -211,8 +242,8 @@ def get_offset(self):
def set_offset(self, offset):
self.piezo.SetOffset(offset)

def compare(self):
d = 1.0*self.scope.frameWrangler.currentFrame.squeeze()
def compare(self, frame_data):
d = 1.0*frame_data.squeeze()
dm = d/d.mean() - 1

#where is the piezo suppposed to be
Expand Down Expand Up @@ -280,11 +311,14 @@ def compare(self):
return dx, dy, dz, Cm, dz, nomPos, posInd, calPos, posDelta


def tick(self, **kwargs):
def tick(self, frameData = None, **kwargs):
if frameData is None:
raise ValueError('frameData must be specified')

targetZ = self.piezo.GetTargetPos(0)

if not 'mask' in dir(self) or not self.scope.frameWrangler.currentFrame.shape[:2] == self.mask.shape[:2]:
self.initialise()
if not 'mask' in dir(self) or not self.frame_source.shape[:2] == self.mask.shape[:2]:
self._initialise(frameData)

#called on a new frame becoming available
if self.calibState == 0:
Expand All @@ -306,7 +340,7 @@ def tick(self, **kwargs):
# print "cal proceed"
if (self.calibState % 1) == 0:
#full step - record current image and move on to next position
self.setRefN(int(self.calibState - 1))
self._setRefN(frameData, int(self.calibState - 1))
self.piezo.MoveTo(0, self.calPositions[int(self.calibState)])


Expand All @@ -315,7 +349,7 @@ def tick(self, **kwargs):

elif (self.calibState == self.NCalibStates):
# print "cal finishing"
self.setRefN(int(self.calibState - 1))
self.setRefN(frameData, int(self.calibState - 1))

#perform final bit of calibration - calcuate gradient between steps
#self.dz = (self.refC - self.refB).ravel()
Expand All @@ -333,7 +367,7 @@ def tick(self, **kwargs):

elif (self.calibState > self.NCalibStates) and np.allclose(self._last_target_z, targetZ):
# print "fully calibrated"
dx, dy, dz, cCoeff, dzcorr, nomPos, posInd, calPos, posDelta = self.compare()
dx, dy, dz, cCoeff, dzcorr, nomPos, posInd, calPos, posDelta = self.compare(frameData)

self.corrRef = max(self.corrRef, cCoeff)

Expand Down Expand Up @@ -376,13 +410,11 @@ def reCalibrate(self):
self.lockActive = False

def register(self):
#self.scope.frameWrangler.WantFrameGroupNotification.append(self.tick)
self.scope.frameWrangler.onFrameGroup.connect(self.tick)
self.frame_source.connect(self.tick)
self.tracking = True

def deregister(self):
#self.scope.frameWrangler.WantFrameGroupNotification.remove(self.tick)
self.scope.frameWrangler.onFrameGroup.disconnect(self.tick)
self.frame_source.disconnect(self.tick)
self.tracking = False

# def setRefs(self, piezo):
Expand Down
16 changes: 13 additions & 3 deletions PYME/Acquire/PYMEAcquire.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,17 @@ def __init__(self, options, *args):


def OnInit(self):
from PYME.Acquire import acquiremainframe
#wx.InitAllImageHandlers()
if self.options.server:
from PYME.Acquire import acquirewx as acquiremainframe
else:
from PYME.Acquire import acquiremainframe

self.main = acquiremainframe.create(None, self.options)
#self.main.Show()
self.SetTopWindow(self.main)

if self.options.browser:
import webbrowser
webbrowser.open('http://localhost:8999') #FIXME - delay this until server is up
return True


Expand All @@ -104,6 +110,10 @@ def main():

parser.add_option("-t", "--title", dest="window_title", default='PYME Acquire',
help="Set the PYMEAcquire display name (useful when running multiple copies - e.g. for drift tracking)")

parser.add_option('-p', '--port', dest='port', default=8999, help='port to use for server functions')
parser.add_option('-s', '--server', dest='server', default=False, action='store_true', help='run in server mode')
parser.add_option('-b', '--browser', dest='browser', default=False, action='store_true', help='launch web browser based ui')


(options, args) = parser.parse_args()
Expand Down
3 changes: 2 additions & 1 deletion PYME/Acquire/Scripts/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ def samp_db(MainFrame, scope):

@init_gui('Fake DMD')
def fake_dmd(MainFrame, scope):
from PYMEnf.Hardware import FakeDMD, DMDGui
from PYMEnf.Hardware import FakeDMD
from PYME.Acquire.Hardware import DMDGui
scope.LC = FakeDMD.FakeDMD(scope)

LCGui = DMDGui.DMDPanel(MainFrame,scope.LC, scope)
Expand Down
3 changes: 3 additions & 0 deletions PYME/Acquire/SpoolController.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,9 @@ def start_spooling(self, body, filename=None, preflight_mode='abort'):

"""
import json
# FIXME - do some sanity checks on filename (this can't be as simple as urlescaping, as we need to support
# URIs as well as filenames). In practice this is best dealt with by enforcing authentication and only using on a
# trusted network.
if len(body) > 0:
# have settings in message body
self.spool_controller.start_spooling(filename, settings=json.loads(body), preflight_mode=preflight_mode)
Expand Down
64 changes: 64 additions & 0 deletions PYME/Acquire/acquire_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import requests


class AcquireClient(object):
"""
Client for the Acquire server. Offers a pythonic interface to the PYMEAcquire REST API.
"""

def __init__(self, url='127.0.0.1', port=8999):
self.url = url
self.port = port
self.base_url = 'http://{}:{}'.format(self.url, self.port)
self._state = None


def _poll_state(self):
"""
Polls the state of the server. Uses the long-polling endpoint.
"""
while True:
self._state = requests.get(self.base_url + '/scope_state_longpoll').json()

def _start_polling(self):
"""
Starts polling the server state.
"""
import threading
t = threading.Thread(target=self._poll_state)
t.daemon = True
t.start()

@property
def state(self):
"""
Returns the current state of the server (as given by long-polling). Starts polling
the first time it is called.
"""
if self._state is None:
self._state = self._get_scope_state() # get initiial state
self._start_polling()

return self._state


def _get_scope_state(self):
"""
Returns the current state of the scope as a dictionary.
"""
return requests.get(self.base_url + '/get_scope_state').json()

def update_scope_state(self, state:dict):
"""
Updates the scope state with the provided dictionary.
"""
requests.post(self.base_url + '/update_scope_state', json=state)

def start_spooling(self, filename='', preflight_mode='abort', settings={}):
"""
Starts spooling images to disk. If filename is not provided, the default filename will be used.
"""

requests.post(self.base_url + f'/spool_controller/start_spooling?filename={filename}&preflight_mode={preflight_mode}', json=settings)


23 changes: 15 additions & 8 deletions PYME/Acquire/acquire_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@
from PYME.Acquire import event_loop
#from PYME.Acquire import webui

class PYMEAcquireServer(event_loop.EventLoop):
def __init__(self, options = None):
event_loop.EventLoop.__init__(self)
class PYMEAcquireServer(object):
def __init__(self, options = None, evt_loop = None):
if evt_loop is None:
self.evt_loop = event_loop.EventLoop()
else:
self.evt_loop = evt_loop

self.options = options

self.snapNum = 0
Expand Down Expand Up @@ -108,7 +112,7 @@ def main_loop(self):

try:
logger.debug('Starting event loop')
self.loop_forever()
self.evt_loop.loop_forever()
except:
logger.exception('Exception in event loop')
finally:
Expand Down Expand Up @@ -143,7 +147,7 @@ def _on_scope_state_change(self, *args, **kwargs):
self._state_updated_condition.notify()

def _start_polling_camera(self):
self.scope.startFrameWrangler(event_loop=self)
self.scope.startFrameWrangler(event_loop=self.evt_loop)
self.scope.frameWrangler.onFrameGroup.connect(self._on_frame_group)

@webframework.register_endpoint('/get_frame_pzf', mimetype='image/pzf')
Expand Down Expand Up @@ -324,8 +328,11 @@ def _shutdown(self):
from PYME.Acquire import webui
from PYME.Acquire import SpoolController
class AcquireHTTPServer(webframework.APIHTTPServer, PYMEAcquireServer):
def __init__(self, options, port, bind_addr=''):
PYMEAcquireServer.__init__(self, options)
def __init__(self, options, port, bind_addr=None, evt_loop=None):
PYMEAcquireServer.__init__(self, options, evt_loop=evt_loop)

if bind_addr is None:
bind_addr = 'localhost' # bind to localhost by default in an attempt to make this safer

server_address = (bind_addr, port)
webframework.APIHTTPServer.__init__(self, server_address)
Expand Down Expand Up @@ -380,7 +387,7 @@ def run(self):
try:
self.serve_forever()
finally:
self.stop()
self.evt_loop.stop()
#logger.info('Shutting down ...')
#self.distributor.shutdown()
logger.info('Closing server ...')
Expand Down
Loading