1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
""" |
3
|
|
|
This module contains a POI Manager core class which gives capability to mark |
4
|
|
|
points of interest, re-optimise their position, and keep track of sample drift |
5
|
|
|
over time. |
6
|
|
|
|
7
|
|
|
Qudi is free software: you can redistribute it and/or modify |
8
|
|
|
it under the terms of the GNU General Public License as published by |
9
|
|
|
the Free Software Foundation, either version 3 of the License, or |
10
|
|
|
(at your option) any later version. |
11
|
|
|
|
12
|
|
|
Qudi is distributed in the hope that it will be useful, |
13
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
14
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15
|
|
|
GNU General Public License for more details. |
16
|
|
|
|
17
|
|
|
You should have received a copy of the GNU General Public License |
18
|
|
|
along with Qudi. If not, see <http://www.gnu.org/licenses/>. |
19
|
|
|
|
20
|
|
|
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the |
21
|
|
|
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/> |
22
|
|
|
""" |
23
|
|
|
|
24
|
|
|
import logging |
25
|
|
|
import math |
26
|
|
|
import numpy as np |
27
|
|
|
import re |
28
|
|
|
import scipy.ndimage as ndimage |
29
|
|
|
import scipy.ndimage.filters as filters |
30
|
|
|
import time |
31
|
|
|
|
32
|
|
|
from collections import OrderedDict |
33
|
|
|
from core.module import Connector, StatusVar |
34
|
|
|
from core.util.mutex import Mutex |
35
|
|
|
from datetime import datetime |
36
|
|
|
from logic.generic_logic import GenericLogic |
37
|
|
|
from qtpy import QtCore |
38
|
|
|
|
39
|
|
|
|
40
|
|
|
class PoI: |
41
|
|
|
|
42
|
|
|
""" |
43
|
|
|
The actual individual poi is saved in this generic object. |
44
|
|
|
|
45
|
|
|
""" |
46
|
|
|
|
47
|
|
|
def __init__(self, pos=None, name=None, key=None): |
48
|
|
|
# Logging |
49
|
|
|
self.log = logging.getLogger(__name__) |
50
|
|
|
|
51
|
|
|
# The POI has fixed coordinates relative to the sample, enabling a map to be saved. |
52
|
|
|
self._coords_in_sample = [0, 0, 0] |
53
|
|
|
|
54
|
|
|
# The POI is at a scanner position, which may vary with time (drift). This time |
55
|
|
|
# trace records every time+position when the POI position was explicitly known. |
56
|
|
|
self._position_time_trace = [] |
57
|
|
|
|
58
|
|
|
# To avoid duplication while algorithmically setting POIs, we need the key string to |
59
|
|
|
# go to sub-second. This requires the datetime module. |
60
|
|
|
self._creation_time = datetime.now() |
61
|
|
|
|
62
|
|
|
if key is None: |
63
|
|
|
self._key = self._creation_time.strftime('poi_%Y%m%d_%H%M_%S_%f') |
64
|
|
|
else: |
65
|
|
|
self._key = key |
66
|
|
|
|
67
|
|
|
if pos is not None: |
68
|
|
|
if len(pos) != 3: |
69
|
|
|
self.log.error('Given position does not contain 3 dimensions.') |
70
|
|
|
pos = [0, 0, 0] |
71
|
|
|
|
72
|
|
|
# Store the time in the history log as seconds since 1970, |
73
|
|
|
# rather than as a datetime object. |
74
|
|
|
creation_time_sec = (self._creation_time - datetime.utcfromtimestamp(0)).total_seconds() |
75
|
|
|
self._position_time_trace.append(np.array([creation_time_sec, pos[0], pos[1], pos[2]])) |
76
|
|
|
self._coords_in_sample = pos |
77
|
|
|
|
78
|
|
|
if name is None: |
79
|
|
|
self._name = self._creation_time.strftime('poi_%H%M%S') |
80
|
|
|
else: |
81
|
|
|
self._name = name |
82
|
|
|
|
83
|
|
|
def to_dict(self): |
84
|
|
|
return { |
85
|
|
|
'name': self._name, |
86
|
|
|
'key': self._key, |
87
|
|
|
'time': self._creation_time, |
88
|
|
|
'pos': self._coords_in_sample, |
89
|
|
|
'history': self._position_time_trace |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
def set_coords_in_sample(self, coords=None): |
93
|
|
|
""" Defines the position of the poi relative to the sample, |
94
|
|
|
allowing a sample map to be constructed. Once set, these |
95
|
|
|
"coordinates in sample" will not be altered unless the user wants to |
96
|
|
|
manually redefine this POI (for example, they put the POI in |
97
|
|
|
the wrong place). |
98
|
|
|
|
99
|
|
|
@param float[3] coords: relative position of poi in sample |
100
|
|
|
""" |
101
|
|
|
|
102
|
|
|
if coords is not None: # FIXME: Futurewarning fired here. |
103
|
|
|
if len(coords) != 3: |
104
|
|
|
self.log.error('Given position does not contain 3 ' |
105
|
|
|
'dimensions.' |
106
|
|
|
) |
107
|
|
|
self._coords_in_sample = [coords[0], coords[1], coords[2]] |
108
|
|
|
|
109
|
|
|
def add_position_to_history(self, position=None): |
110
|
|
|
""" Adds an explicitly known position+time to the history of the POI. |
111
|
|
|
|
112
|
|
|
@param float[3] position: position coordinates of the poi |
113
|
|
|
|
114
|
|
|
@return int: error code (0:OK, -1:error) |
115
|
|
|
""" |
116
|
|
|
if position is None: |
117
|
|
|
position = [] |
118
|
|
|
if isinstance(position, (np.ndarray,)) and not position.size == 3: |
119
|
|
|
return -1 |
120
|
|
|
elif isinstance(position, (list, tuple)) and not len(position) == 3: |
121
|
|
|
return -1 |
122
|
|
|
else: |
123
|
|
|
self._position_time_trace.append( |
124
|
|
|
np.array([time.time(), position[0], position[1], position[2]])) |
125
|
|
|
|
126
|
|
|
def get_coords_in_sample(self): |
127
|
|
|
""" Returns the coordinates of the POI relative to the sample. |
128
|
|
|
|
129
|
|
|
@return float[3]: the POI coordinates. |
130
|
|
|
""" |
131
|
|
|
|
132
|
|
|
return self._coords_in_sample |
133
|
|
|
|
134
|
|
|
def set_name(self, name=None): |
135
|
|
|
""" Sets the name of the poi. |
136
|
|
|
|
137
|
|
|
@param string name: name to be set. |
138
|
|
|
|
139
|
|
|
@return int: error code (0:OK, -1:error) |
140
|
|
|
""" |
141
|
|
|
if self._name is 'crosshair' or self._name is 'sample': |
142
|
|
|
# self.log.error('You can not change the name of the crosshair.') |
143
|
|
|
return -1 |
144
|
|
|
if name is not None: |
145
|
|
|
self._name = name |
146
|
|
|
return 0 |
147
|
|
|
if len(self._position_time_trace) > 0: |
148
|
|
|
self._name = time.strftime('Point_%Y%m%d_%M%S%', self._creation_time) |
149
|
|
|
return -1 |
150
|
|
|
else: |
151
|
|
|
self._name = time.strftime('Point_%Y%m%d_%M%S%') |
152
|
|
|
return -1 |
153
|
|
|
|
154
|
|
|
def get_name(self): |
155
|
|
|
""" Returns the name of the poi. |
156
|
|
|
|
157
|
|
|
@return string: name |
158
|
|
|
""" |
159
|
|
|
return self._name |
160
|
|
|
|
161
|
|
|
def get_key(self): |
162
|
|
|
""" Returns the dictionary key of the poi. |
163
|
|
|
|
164
|
|
|
@return string: key |
165
|
|
|
""" |
166
|
|
|
return self._key |
167
|
|
|
|
168
|
|
|
def get_position_history(self): # TODO: instead of "trace": drift_log, history, |
169
|
|
|
""" Returns the whole position history as array. |
170
|
|
|
|
171
|
|
|
@return float[][4]: the whole position history |
172
|
|
|
""" |
173
|
|
|
|
174
|
|
|
return np.array(self._position_time_trace) |
175
|
|
|
|
176
|
|
|
def delete_last_position(self, empty_array_completely=False): |
177
|
|
|
""" Delete the last position in the history. |
178
|
|
|
@param bool empty_array_completely: If _position_time_trace can be deleted completely |
179
|
|
|
this variable is set to True if not the last value |
180
|
|
|
will not be deleted |
181
|
|
|
|
182
|
|
|
@return float[4]: the position just deleted. |
183
|
|
|
""" |
184
|
|
|
# do not delete initial position |
185
|
|
|
if len(self._position_time_trace) > 1: |
186
|
|
|
return self._position_time_trace.pop() |
187
|
|
|
elif empty_array_completely: |
188
|
|
|
return self._position_time_trace.pop() |
189
|
|
|
else: |
190
|
|
|
self.log.error('Position was not deleted, initial point of history reached.') |
191
|
|
|
return [-1., -1., -1., -1.] |
192
|
|
|
|
193
|
|
|
|
194
|
|
|
class PoiManagerLogic(GenericLogic): |
195
|
|
|
|
196
|
|
|
""" |
197
|
|
|
This is the Logic class for mapping and tracking bright features in the confocal scan. |
198
|
|
|
""" |
199
|
|
|
_modclass = 'poimanagerlogic' |
200
|
|
|
_modtype = 'logic' |
201
|
|
|
|
202
|
|
|
# declare connectors |
203
|
|
|
optimizer1 = Connector(interface='OptimizerLogic') |
204
|
|
|
scannerlogic = Connector(interface='ConfocalLogic') |
205
|
|
|
savelogic = Connector(interface='SaveLogic') |
206
|
|
|
|
207
|
|
|
# status vars |
208
|
|
|
poi_list = StatusVar(default=OrderedDict()) |
209
|
|
|
roi_name = StatusVar(default='') |
210
|
|
|
active_poi = StatusVar(default=None) |
211
|
|
|
|
212
|
|
|
signal_timer_updated = QtCore.Signal() |
213
|
|
|
signal_poi_updated = QtCore.Signal() |
214
|
|
|
signal_poi_deleted = QtCore.Signal(str) |
215
|
|
|
signal_confocal_image_updated = QtCore.Signal() |
216
|
|
|
signal_periodic_opt_started = QtCore.Signal() |
217
|
|
|
signal_periodic_opt_duration_changed = QtCore.Signal() |
218
|
|
|
signal_periodic_opt_stopped = QtCore.Signal() |
219
|
|
|
|
220
|
|
|
def __init__(self, config, **kwargs): |
221
|
|
|
super().__init__(config=config, **kwargs) |
222
|
|
|
|
223
|
|
|
self._current_poi_key = None |
224
|
|
|
self.go_to_crosshair_after_refocus = False # default value |
225
|
|
|
|
226
|
|
|
# timer and its handling for the periodic refocus |
227
|
|
|
self.timer = None |
228
|
|
|
self.time_left = 0 |
229
|
|
|
self.timer_step = 0 |
230
|
|
|
self.timer_duration = 300 |
231
|
|
|
|
232
|
|
|
# locking for thread safety |
233
|
|
|
self.threadlock = Mutex() |
234
|
|
|
|
235
|
|
|
def on_activate(self): |
236
|
|
|
""" Initialisation performed during activation of the module. |
237
|
|
|
""" |
238
|
|
|
|
239
|
|
|
self._optimizer_logic = self.optimizer1() |
240
|
|
|
self._confocal_logic = self.scannerlogic() |
241
|
|
|
self._save_logic = self.savelogic() |
242
|
|
|
|
243
|
|
|
# listen for the refocus to finish |
244
|
|
|
self._optimizer_logic.sigRefocusFinished.connect(self._refocus_done) |
245
|
|
|
|
246
|
|
|
# listen for the deactivation of a POI caused by moving to a different position |
247
|
|
|
self._confocal_logic.signal_change_position.connect(self.user_move_deactivates_poi) |
248
|
|
|
|
249
|
|
|
# Initialise the roi_map_data (xy confocal image) |
250
|
|
|
self.roi_map_data = self._confocal_logic.xy_image |
251
|
|
|
|
252
|
|
|
def on_deactivate(self): |
253
|
|
|
return |
254
|
|
|
|
255
|
|
|
def user_move_deactivates_poi(self, tag): |
256
|
|
|
""" Deactivate the active POI if the confocal microscope scanner position is |
257
|
|
|
moved by anything other than the optimizer |
258
|
|
|
""" |
259
|
|
|
pass |
260
|
|
|
|
261
|
|
|
def add_poi(self, position=None, key=None, emit_change=True): |
262
|
|
|
""" Creates a new poi and adds it to the list. |
263
|
|
|
|
264
|
|
|
@return int: key of this new poi |
265
|
|
|
|
266
|
|
|
A position can be provided (such as during re-loading a saved ROI). |
267
|
|
|
If no position is provided, then the current crosshair position is used. |
268
|
|
|
""" |
269
|
|
|
# If there are only 2 POIs (sample and crosshair) then the newly added POI needs to start the sample drift logging. |
270
|
|
|
if len(self.poi_list) == 2: |
271
|
|
|
self.poi_list['sample']._creation_time = time.time() |
272
|
|
|
# When the poimanager is activated the 'sample' poi is created because it is needed |
273
|
|
|
# from the beginning for various functionalities. If the tracking of the sample is started it has |
274
|
|
|
# to be reset such that this first point is deleted here |
275
|
|
|
# Probably this can be solved a lot nicer. |
276
|
|
|
self.poi_list['sample'].delete_last_position(empty_array_completely=True) |
277
|
|
|
self.poi_list['sample'].add_position_to_history(position=[0, 0, 0]) |
278
|
|
|
self.poi_list['sample'].set_coords_in_sample(coords=[0, 0, 0]) |
279
|
|
|
|
280
|
|
|
if position is None: |
281
|
|
|
position = self._confocal_logic.get_position()[:3] |
282
|
|
|
if len(position) != 3: |
283
|
|
|
self.log.error('Given position is not 3-dimensional.' |
284
|
|
|
'Please pass POIManager a 3-dimensional position to set a POI.') |
285
|
|
|
return |
286
|
|
|
|
287
|
|
|
new_poi = PoI(pos=position, key=key) |
288
|
|
|
self.poi_list[new_poi.get_key()] = new_poi |
289
|
|
|
|
290
|
|
|
# The POI coordinates are set relative to the last known sample position |
291
|
|
|
most_recent_sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] |
292
|
|
|
this_poi_coords = position - most_recent_sample_pos |
293
|
|
|
new_poi.set_coords_in_sample(coords=this_poi_coords) |
294
|
|
|
|
295
|
|
|
# Since POI was created at current scanner position, it automatically |
296
|
|
|
# becomes the active POI. |
297
|
|
|
self.set_active_poi(poikey=new_poi.get_key()) |
298
|
|
|
|
299
|
|
|
if emit_change: |
300
|
|
|
self.signal_poi_updated.emit() |
301
|
|
|
|
302
|
|
|
return new_poi.get_key() |
303
|
|
|
|
304
|
|
|
def get_confocal_image_data(self): |
305
|
|
|
""" Get the current confocal xy scan data to hold as image of ROI""" |
306
|
|
|
|
307
|
|
|
# get the roi_map_data (xy confocal image) |
308
|
|
|
self.roi_map_data = self._confocal_logic.xy_image |
309
|
|
|
|
310
|
|
|
self.signal_confocal_image_updated.emit() |
311
|
|
|
|
312
|
|
|
def get_all_pois(self, abc_sort=False): |
313
|
|
|
""" Returns a list of the names of all existing POIs. |
314
|
|
|
|
315
|
|
|
@return string[]: List of names of the POIs |
316
|
|
|
|
317
|
|
|
Also crosshair and sample are included. |
318
|
|
|
""" |
319
|
|
|
if abc_sort is False: |
320
|
|
|
return sorted(self.poi_list.keys()) |
321
|
|
|
|
322
|
|
|
elif abc_sort is True: |
323
|
|
|
# First create a dictionary with poikeys indexed against names |
324
|
|
|
poinames = [''] * len(self.poi_list.keys()) |
325
|
|
|
for i, poikey in enumerate(self.poi_list.keys()): |
326
|
|
|
poiname = self.poi_list[poikey].get_name() |
327
|
|
|
poinames[i] = [poiname, poikey] |
328
|
|
|
|
329
|
|
|
# Sort names in the way that humans expect (site1, site2, site11, etc) |
330
|
|
|
|
331
|
|
|
# Regular expressions to make sorting key |
332
|
|
|
convert = lambda text: int(text) if text.isdigit() else text |
333
|
|
|
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key[0])] |
334
|
|
|
# Now we can sort poinames by name and return keys in that order |
335
|
|
|
return [key for [name, key] in sorted(poinames, key=alphanum_key)] |
336
|
|
|
|
337
|
|
|
else: |
338
|
|
|
# TODO: produce sensible error about unknown value of abc_sort. |
339
|
|
|
self.log.debug('fix TODO!') |
340
|
|
|
|
341
|
|
|
# TODO: Find a way to return a list of POI keys sorted in order of the POI names. |
342
|
|
|
|
343
|
|
|
def delete_last_position(self, poikey=None): |
344
|
|
|
""" Delete the last position in the history. |
345
|
|
|
|
346
|
|
|
@param string poikey: the key of the poi |
347
|
|
|
|
348
|
|
|
@return int: error code (0:OK, -1:error) |
349
|
|
|
""" |
350
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
351
|
|
|
self.poi_list[poikey].delete_last_position() |
352
|
|
|
self.poi_list['sample'].delete_last_position() |
353
|
|
|
self.signal_poi_updated.emit() |
354
|
|
|
return 0 |
355
|
|
|
else: |
356
|
|
|
self.log.error('The last position of given POI ({0}) could not be deleted.'.format( |
357
|
|
|
poikey)) |
358
|
|
|
return -1 |
359
|
|
|
|
360
|
|
|
def delete_poi(self, poikey=None): |
361
|
|
|
""" Completely deletes the whole given poi. |
362
|
|
|
|
363
|
|
|
@param string poikey: the key of the poi |
364
|
|
|
|
365
|
|
|
@return int: error code (0:OK, -1:error) |
366
|
|
|
|
367
|
|
|
Does not delete the crosshair and sample. |
368
|
|
|
""" |
369
|
|
|
|
370
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
371
|
|
|
if poikey is 'crosshair' or poikey is 'sample': |
372
|
|
|
self.log.warning('You cannot delete the crosshair or sample.') |
373
|
|
|
return -1 |
374
|
|
|
del self.poi_list[poikey] |
375
|
|
|
|
376
|
|
|
# If the active poi was deleted, there is no way to automatically choose |
377
|
|
|
# another active POI, so we deactivate POI |
378
|
|
|
if self.active_poi is not None and poikey == self.active_poi.get_key(): |
379
|
|
|
self._deactivate_poi() |
380
|
|
|
|
381
|
|
|
self.signal_poi_updated.emit() |
382
|
|
|
self.signal_poi_deleted.emit(poikey) |
383
|
|
|
return 0 |
384
|
|
|
elif poikey is None: |
385
|
|
|
self.log.warning('No POI for deletion specified.') |
386
|
|
|
else: |
387
|
|
|
self.log.error('X. The given POI ({0}) does not exist.'.format( |
388
|
|
|
poikey)) |
389
|
|
|
return -1 |
390
|
|
|
|
391
|
|
|
def optimise_poi(self, poikey=None): |
392
|
|
|
""" Starts the optimisation procedure for the given poi. |
393
|
|
|
|
394
|
|
|
@param string poikey: the key of the poi |
395
|
|
|
|
396
|
|
|
@return int: error code (0:OK, -1:error) |
397
|
|
|
|
398
|
|
|
This is threaded, so it returns directly. |
399
|
|
|
The function _refocus_done handles the data when the optimisation returns. |
400
|
|
|
""" |
401
|
|
|
|
402
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
403
|
|
|
self.poi_list['crosshair'].add_position_to_history(position=self._confocal_logic.get_position()[:3]) |
404
|
|
|
self._current_poi_key = poikey |
405
|
|
|
self._optimizer_logic.start_refocus( |
406
|
|
|
initial_pos=self.get_poi_position(poikey=poikey), |
407
|
|
|
caller_tag='poimanager') |
408
|
|
|
return 0 |
409
|
|
|
else: |
410
|
|
|
self.log.error( |
411
|
|
|
'Z. The given POI ({0}) does not exist.'.format(poikey)) |
412
|
|
|
return -1 |
413
|
|
|
|
414
|
|
|
def go_to_poi(self, poikey=None): |
415
|
|
|
""" Goes to the given poi and saves it as the current one. |
416
|
|
|
|
417
|
|
|
@param string poikey: the key of the poi |
418
|
|
|
|
419
|
|
|
@return int: error code (0:OK, -1:error) |
420
|
|
|
""" |
421
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
422
|
|
|
self._current_poi_key = poikey |
423
|
|
|
x, y, z = self.get_poi_position(poikey=poikey) |
424
|
|
|
self._confocal_logic.set_position('poimanager', x=x, y=y, z=z) |
425
|
|
|
else: |
426
|
|
|
self.log.error('The given POI ({0}) does not exist.'.format( |
427
|
|
|
poikey)) |
428
|
|
|
return -1 |
429
|
|
|
# This is now the active POI to send to save logic for naming in any saved filenames. |
430
|
|
|
self.set_active_poi(poikey) |
431
|
|
|
|
432
|
|
|
#Fixme: After pressing the Go to Poi button the active poi is empty and the following lines do fix this |
433
|
|
|
# The time.sleep is somehow needed if not active_poi can not be set |
434
|
|
|
time.sleep(0.001) |
435
|
|
|
self.active_poi = self.poi_list[poikey] |
436
|
|
|
self.signal_poi_updated.emit() |
437
|
|
|
|
438
|
|
|
def get_poi_position(self, poikey=None): |
439
|
|
|
""" Returns the current position of the given poi, calculated from the |
440
|
|
|
POI coords in sample and the current sample position. |
441
|
|
|
|
442
|
|
|
@param string poikey: the key of the poi |
443
|
|
|
|
444
|
|
|
@return |
445
|
|
|
""" |
446
|
|
|
|
447
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
448
|
|
|
|
449
|
|
|
poi_coords = self.poi_list[poikey].get_coords_in_sample() |
450
|
|
|
sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] |
451
|
|
|
return sample_pos + poi_coords |
452
|
|
|
|
453
|
|
|
else: |
454
|
|
|
self.log.error('G. The given POI ({0}) does not exist.'.format( |
455
|
|
|
poikey)) |
456
|
|
|
return [-1., -1., -1.] |
457
|
|
|
|
458
|
|
|
def set_new_position(self, poikey=None, newpos=None): |
459
|
|
|
""" |
460
|
|
|
Moves the given POI to a new position, and uses this information to update |
461
|
|
|
the sample position. |
462
|
|
|
|
463
|
|
|
@param string poikey: the key of the poi |
464
|
|
|
@param float[3] newpos: coordinates of the new position |
465
|
|
|
|
466
|
|
|
@return int: error code (0:OK, -1:error) |
467
|
|
|
""" |
468
|
|
|
|
469
|
|
|
# If no new position is given, take the current confocal crosshair position |
470
|
|
|
if newpos is None: |
471
|
|
|
newpos = self._confocal_logic.get_position()[:3] |
472
|
|
|
|
473
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
474
|
|
|
if len(newpos) != 3: |
475
|
|
|
self.log.error('Length of set poi is not 3.') |
476
|
|
|
return -1 |
477
|
|
|
# Add new position to trace of POI |
478
|
|
|
self.poi_list[poikey].add_position_to_history(position=newpos) |
479
|
|
|
|
480
|
|
|
# Calculate sample shift and add it to the trace of 'sample' POI |
481
|
|
|
sample_shift = newpos - self.get_poi_position(poikey=poikey) |
482
|
|
|
sample_shift += self.poi_list['sample'].get_position_history()[-1, :][1:4] |
483
|
|
|
self.poi_list['sample'].add_position_to_history(position=sample_shift) |
484
|
|
|
|
485
|
|
|
# signal POI has been updated (this will cause GUI to redraw) |
486
|
|
|
if (poikey is not 'crosshair') and (poikey is not 'sample'): |
487
|
|
|
self.signal_poi_updated.emit() |
488
|
|
|
|
489
|
|
|
return 0 |
490
|
|
|
|
491
|
|
|
self.log.error('J. The given POI ({0}) does not exist.'.format(poikey)) |
492
|
|
|
return -1 |
493
|
|
|
|
494
|
|
|
def move_coords(self, poikey=None, newpos=None): |
495
|
|
|
"""Updates the coords of a given POI, and adds a position to the POI history, |
496
|
|
|
but DOES NOT update the sample position. |
497
|
|
|
""" |
498
|
|
|
if newpos is None: |
499
|
|
|
newpos = self._confocal_logic.get_position()[:3] |
500
|
|
|
|
501
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
502
|
|
|
if len(newpos) != 3: |
503
|
|
|
self.log.error('Length of set poi is not 3.') |
504
|
|
|
return -1 |
505
|
|
|
this_poi = self.poi_list[poikey] |
506
|
|
|
return_val = this_poi.add_position_to_history(position=newpos) |
507
|
|
|
|
508
|
|
|
sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] |
509
|
|
|
|
510
|
|
|
new_coords = newpos - sample_pos |
511
|
|
|
|
512
|
|
|
this_poi.set_coords_in_sample(new_coords) |
513
|
|
|
|
514
|
|
|
self.signal_poi_updated.emit() |
515
|
|
|
|
516
|
|
|
return return_val |
517
|
|
|
|
518
|
|
|
self.log.error('JJ. The given POI ({0}) does not exist.'.format(poikey)) |
519
|
|
|
return -1 |
520
|
|
|
|
521
|
|
|
def rename_poi(self, poikey=None, name=None, emit_change=True): |
522
|
|
|
""" Sets the name of the given poi. |
523
|
|
|
|
524
|
|
|
@param string poikey: the key of the poi |
525
|
|
|
@param string name: name of the poi to be set |
526
|
|
|
|
527
|
|
|
@return int: error code (0:OK, -1:error) |
528
|
|
|
""" |
529
|
|
|
|
530
|
|
|
if poikey is not None and name is not None and poikey in self.poi_list.keys(): |
531
|
|
|
|
532
|
|
|
success = self.poi_list[poikey].set_name(name=name) |
533
|
|
|
|
534
|
|
|
# if this is the active POI then we need to update poi tag in savelogic |
535
|
|
|
if self.poi_list[poikey] == self.active_poi: |
536
|
|
|
self.update_poi_tag_in_savelogic() |
537
|
|
|
|
538
|
|
|
if emit_change: |
539
|
|
|
self.signal_poi_updated.emit() |
540
|
|
|
|
541
|
|
|
return success |
542
|
|
|
|
543
|
|
|
else: |
544
|
|
|
self.log.error('AAAThe given POI ({0}) does not exist.'.format( |
545
|
|
|
poikey)) |
546
|
|
|
return -1 |
547
|
|
|
|
548
|
|
|
def start_periodic_refocus(self, poikey=None): |
549
|
|
|
""" Starts the perodic refocussing of the poi. |
550
|
|
|
|
551
|
|
|
@param float duration: (optional) the time between periodic optimization |
552
|
|
|
@param string poikey: (optional) the key of the poi to be set and refocussed on. |
553
|
|
|
|
554
|
|
|
@return int: error code (0:OK, -1:error) |
555
|
|
|
""" |
556
|
|
|
|
557
|
|
|
if poikey is not None and poikey in self.poi_list.keys(): |
558
|
|
|
self._current_poi_key = poikey |
559
|
|
|
else: |
560
|
|
|
# Todo: warning message that active POI used by default |
561
|
|
|
self._current_poi_key = self.active_poi.get_key() |
562
|
|
|
|
563
|
|
|
self.log.info('Periodic refocus on {0}.'.format(self._current_poi_key)) |
564
|
|
|
|
565
|
|
|
self.timer_step = 0 |
566
|
|
|
self.timer = QtCore.QTimer() |
567
|
|
|
self.timer.setSingleShot(False) |
568
|
|
|
self.timer.timeout.connect(self._periodic_refocus_loop) |
569
|
|
|
self.timer.start(300) |
570
|
|
|
|
571
|
|
|
self.signal_periodic_opt_started.emit() |
572
|
|
|
return 0 |
573
|
|
|
|
574
|
|
|
def set_periodic_optimize_duration(self, duration=None): |
575
|
|
|
""" Change the duration of the periodic optimize timer during active |
576
|
|
|
periodic refocussing. |
577
|
|
|
|
578
|
|
|
@param float duration: (optional) the time between periodic optimization. |
579
|
|
|
""" |
580
|
|
|
if duration is not None: |
581
|
|
|
self.timer_duration = duration |
582
|
|
|
else: |
583
|
|
|
self.log.warning('No timer duration given, using {0} s.'.format( |
584
|
|
|
self.timer_duration)) |
585
|
|
|
|
586
|
|
|
self.signal_periodic_opt_duration_changed.emit() |
587
|
|
|
|
588
|
|
|
def _periodic_refocus_loop(self): |
589
|
|
|
""" This is the looped function that does the actual periodic refocus. |
590
|
|
|
|
591
|
|
|
If the time has run out, it refocussed the current poi. |
592
|
|
|
Otherwise it just updates the time that is left. |
593
|
|
|
""" |
594
|
|
|
self.time_left = self.timer_step - time.time() + self.timer_duration |
595
|
|
|
self.signal_timer_updated.emit() |
596
|
|
|
if self.time_left <= 0: |
597
|
|
|
self.timer_step = time.time() |
598
|
|
|
self.optimise_poi(poikey=self._current_poi_key) |
599
|
|
|
|
600
|
|
|
def stop_periodic_refocus(self): |
601
|
|
|
""" Stops the perodic refocussing of the poi. |
602
|
|
|
|
603
|
|
|
@return int: error code (0:OK, -1:error) |
604
|
|
|
""" |
605
|
|
|
if self.timer is None: |
606
|
|
|
self.log.warning('No timer to stop.') |
607
|
|
|
return -1 |
608
|
|
|
self.timer.stop() |
609
|
|
|
self.timer = None |
610
|
|
|
|
611
|
|
|
self.signal_periodic_opt_stopped.emit() |
612
|
|
|
return 0 |
613
|
|
|
|
614
|
|
|
def _refocus_done(self, caller_tag, optimal_pos): |
615
|
|
|
""" Gets called automatically after the refocus is done and saves the new position |
616
|
|
|
to the poi history. |
617
|
|
|
|
618
|
|
|
Also it tracks the sample and may go back to the crosshair. |
619
|
|
|
|
620
|
|
|
@return int: error code (0:OK, -1:error) |
621
|
|
|
""" |
622
|
|
|
# We only need x, y, z |
623
|
|
|
optimized_position = optimal_pos[0:3] |
624
|
|
|
|
625
|
|
|
# If the refocus was on the crosshair, then only update crosshair POI and don't |
626
|
|
|
# do anything with sample position. |
627
|
|
|
caller_tags = ['confocalgui', 'magnet_logic', 'singleshot_logic'] |
628
|
|
|
if caller_tag in caller_tags: |
629
|
|
|
self.poi_list['crosshair'].add_position_to_history(position=optimized_position) |
630
|
|
|
|
631
|
|
|
# If the refocus was initiated here by poimanager, then update POI and sample |
632
|
|
|
elif caller_tag == 'poimanager': |
633
|
|
|
|
634
|
|
|
if self._current_poi_key is not None and self._current_poi_key in self.poi_list.keys(): |
635
|
|
|
|
636
|
|
|
self.set_new_position(poikey=self._current_poi_key, newpos=optimized_position) |
637
|
|
|
|
638
|
|
|
if self.go_to_crosshair_after_refocus: |
639
|
|
|
temp_key = self._current_poi_key |
640
|
|
|
self.go_to_poi(poikey='crosshair') |
641
|
|
|
self._current_poi_key = temp_key |
642
|
|
|
else: |
643
|
|
|
self.go_to_poi(poikey=self._current_poi_key) |
644
|
|
|
return 0 |
645
|
|
|
else: |
646
|
|
|
self.log.error('The given POI ({0}) does not exist.'.format( |
647
|
|
|
self._current_poi_key)) |
648
|
|
|
return -1 |
649
|
|
|
|
650
|
|
|
else: |
651
|
|
|
self.log.warning("Unknown caller_tag for the optimizer. POI " |
652
|
|
|
"Manager does not know what to do with optimized " |
653
|
|
|
"position, and has done nothing.") |
654
|
|
|
|
655
|
|
|
def reset_roi(self): |
656
|
|
|
|
657
|
|
|
del self.poi_list |
658
|
|
|
self.poi_list = dict() |
659
|
|
|
|
660
|
|
|
self.active_poi = None |
661
|
|
|
|
662
|
|
|
self.roi_name = '' |
663
|
|
|
|
664
|
|
|
# initally add crosshair to the pois |
665
|
|
|
crosshair = PoI(pos=[0, 0, 0], name='crosshair') |
666
|
|
|
crosshair._key = 'crosshair' |
667
|
|
|
self.poi_list[crosshair._key] = crosshair |
668
|
|
|
|
669
|
|
|
# Re-initialise sample in the poi list |
670
|
|
|
sample = PoI(pos=[0, 0, 0], name='sample') |
671
|
|
|
sample._key = 'sample' |
672
|
|
|
self.poi_list[sample._key] = sample |
673
|
|
|
|
674
|
|
|
self.signal_poi_updated.emit() |
675
|
|
|
|
676
|
|
|
def set_active_poi(self, poikey=None): |
677
|
|
|
""" |
678
|
|
|
Set the active POI object. |
679
|
|
|
""" |
680
|
|
|
|
681
|
|
|
if poikey is None: |
682
|
|
|
# If poikey is none and no active poi is set, then do nothing |
683
|
|
|
if self.active_poi is None: |
684
|
|
|
return |
685
|
|
|
else: |
686
|
|
|
self.active_poi = None |
687
|
|
|
|
688
|
|
|
elif poikey in self.get_all_pois(): |
689
|
|
|
# If poikey is the current active POI then do nothing |
690
|
|
|
if self.poi_list[poikey] == self.active_poi: |
691
|
|
|
return |
692
|
|
|
|
693
|
|
|
else: |
694
|
|
|
self.active_poi = self.poi_list[poikey] |
695
|
|
|
|
696
|
|
|
else: |
697
|
|
|
# todo: error poikey unknown |
698
|
|
|
return -1 |
699
|
|
|
|
700
|
|
|
self.update_poi_tag_in_savelogic() |
701
|
|
|
self.signal_poi_updated.emit() # todo: this breaks the emit_change = false case |
702
|
|
|
|
703
|
|
|
def _deactivate_poi(self): |
704
|
|
|
self.set_active_poi(poikey=None) |
705
|
|
|
|
706
|
|
|
def update_poi_tag_in_savelogic(self): |
707
|
|
|
|
708
|
|
|
if self.active_poi is not None: |
709
|
|
|
self._save_logic.active_poi_name = self.active_poi.get_name() |
710
|
|
|
else: |
711
|
|
|
self._save_logic.active_poi_name = '' |
712
|
|
|
|
713
|
|
|
def save_poi_map_as_roi(self): |
714
|
|
|
""" Save a list of POIs with their coordinates to a file. |
715
|
|
|
""" |
716
|
|
|
# File path and name |
717
|
|
|
filepath = self._save_logic.get_path_for_module(module_name='ROIs') |
718
|
|
|
|
719
|
|
|
# We will fill the data OderedDict to send to savelogic |
720
|
|
|
data = OrderedDict() |
721
|
|
|
|
722
|
|
|
# Lists for each column of the output file |
723
|
|
|
poinames = [] |
724
|
|
|
poikeys = [] |
725
|
|
|
x_coords = [] |
726
|
|
|
y_coords = [] |
727
|
|
|
z_coords = [] |
728
|
|
|
|
729
|
|
|
for poikey in self.get_all_pois(abc_sort=True): |
730
|
|
|
if poikey is not 'sample' and poikey is not 'crosshair': |
731
|
|
|
thispoi = self.poi_list[poikey] |
732
|
|
|
|
733
|
|
|
poinames.append(thispoi.get_name()) |
734
|
|
|
poikeys.append(poikey) |
735
|
|
|
x_coords.append(thispoi.get_coords_in_sample()[0]) |
736
|
|
|
y_coords.append(thispoi.get_coords_in_sample()[1]) |
737
|
|
|
z_coords.append(thispoi.get_coords_in_sample()[2]) |
738
|
|
|
|
739
|
|
|
data['POI Name'] = np.array(poinames) |
740
|
|
|
data['POI Key'] = np.array(poikeys) |
741
|
|
|
data['X'] = np.array(x_coords) |
742
|
|
|
data['Y'] = np.array(y_coords) |
743
|
|
|
data['Z'] = np.array(z_coords) |
744
|
|
|
|
745
|
|
|
self._save_logic.save_data( |
746
|
|
|
data, |
747
|
|
|
filepath=filepath, |
748
|
|
|
filelabel=self.roi_name, |
749
|
|
|
fmt=['%s', '%s', '%.6e', '%.6e', '%.6e'] |
750
|
|
|
) |
751
|
|
|
|
752
|
|
|
self.log.debug('ROI saved to:\n{0}'.format(filepath)) |
753
|
|
|
return 0 |
754
|
|
|
|
755
|
|
|
def load_roi_from_file(self, filename=None): |
756
|
|
|
|
757
|
|
|
if filename is None: |
758
|
|
|
return -1 |
759
|
|
|
|
760
|
|
|
with open(filename, 'r') as roifile: |
761
|
|
|
for line in roifile: |
762
|
|
|
if line[0] != '#' and line.split()[0] != 'NaN': |
763
|
|
|
saved_poi_name = line.split()[0] |
764
|
|
|
saved_poi_key = line.split()[1] |
765
|
|
|
saved_poi_coords = [ |
766
|
|
|
float(line.split()[2]), float(line.split()[3]), float(line.split()[4])] |
767
|
|
|
|
768
|
|
|
this_poi_key = self.add_poi( |
769
|
|
|
position=saved_poi_coords, |
770
|
|
|
key=saved_poi_key, |
771
|
|
|
emit_change=False) |
772
|
|
|
self.rename_poi(poikey=this_poi_key, name=saved_poi_name, emit_change=False) |
773
|
|
|
|
774
|
|
|
# Now that all the POIs are created, emit the signal for other things (ie gui) to update |
775
|
|
|
self.signal_poi_updated.emit() |
776
|
|
|
return 0 |
777
|
|
|
|
778
|
|
|
@poi_list.constructor |
779
|
|
|
def dict_to_poi_list(self, val): |
780
|
|
|
pdict = {} |
781
|
|
|
# initially add crosshair to the pois |
782
|
|
|
crosshair = PoI(pos=[0, 0, 0], name='crosshair') |
783
|
|
|
crosshair._key = 'crosshair' |
784
|
|
|
pdict[crosshair._key] = crosshair |
785
|
|
|
|
786
|
|
|
# initally add sample to the pois |
787
|
|
|
sample = PoI(pos=[0, 0, 0], name='sample') |
788
|
|
|
sample._key = 'sample' |
789
|
|
|
pdict[sample._key] = sample |
790
|
|
|
|
791
|
|
|
if isinstance(val, dict): |
792
|
|
|
for key, poidict in val.items(): |
793
|
|
|
try: |
794
|
|
View Code Duplication |
if len(poidict['pos']) >= 3: |
|
|
|
|
795
|
|
|
newpoi = PoI(name=poidict['name'], key=poidict['key']) |
796
|
|
|
newpoi.set_coords_in_sample(poidict['pos']) |
797
|
|
|
newpoi._creation_time = poidict['time'] |
798
|
|
|
newpoi._position_time_trace = poidict['history'] |
799
|
|
|
pdict[key] = newpoi |
800
|
|
|
except Exception as e: |
801
|
|
|
self.log.exception('Could not load PoI {0}: {1}'.format(key, poidict)) |
802
|
|
|
return pdict |
803
|
|
|
|
804
|
|
|
@poi_list.representer |
805
|
|
|
def poi_list_to_dict(self, val): |
806
|
|
|
pdict = { |
807
|
|
|
key: poi.to_dict() for key, poi in val.items() |
808
|
|
|
} |
809
|
|
|
return pdict |
810
|
|
|
|
811
|
|
|
@active_poi.representer |
812
|
|
|
def active_poi_to_dict(self, val): |
813
|
|
|
if isinstance(val, PoI): |
814
|
|
|
return val.to_dict() |
815
|
|
|
return None |
816
|
|
|
|
817
|
|
|
@active_poi.constructor |
818
|
|
|
def dict_to_active_poi(self, val): |
819
|
|
|
try: |
820
|
|
View Code Duplication |
if isinstance(val, dict): |
|
|
|
|
821
|
|
|
if len(val['pos']) >= 3: |
822
|
|
|
newpoi = PoI(pos=val['pos'], name=val['name'], key=val['key']) |
823
|
|
|
newpoi._creation_time = val['time'] |
824
|
|
|
newpoi._position_time_trace = val['history'] |
825
|
|
|
return newpoi |
826
|
|
|
except Exception as e: |
827
|
|
|
self.log.exception('Could not load active poi {0}'.format(val)) |
828
|
|
|
return None |
829
|
|
|
|
830
|
|
|
def triangulate(self, r, a1, b1, c1, a2, b2, c2): |
831
|
|
|
""" Reorients a coordinate r that is known relative to reference points a1, b1, c1 to |
832
|
|
|
produce a new vector rnew that has exactly the same relation to rotated/shifted/tilted |
833
|
|
|
reference positions a2, b2, c2. |
834
|
|
|
|
835
|
|
|
@param np.array r: position to be remapped. |
836
|
|
|
|
837
|
|
|
@param np.array a1: initial location of ref1. |
838
|
|
|
|
839
|
|
|
@param np.array a2: final location of ref1. |
840
|
|
|
|
841
|
|
|
@param np.array b1, b2, c1, c2: similar for ref2 and ref3 |
842
|
|
|
""" |
843
|
|
|
|
844
|
|
|
ab_old = b1 - a1 |
845
|
|
|
ac_old = c1 - a1 |
846
|
|
|
|
847
|
|
|
ab_new = b2 - a2 |
848
|
|
|
ac_new = c2 - a2 |
849
|
|
|
|
850
|
|
|
# Firstly, find the angle to rotate ab_old onto ab_new. This rotation must be done in |
851
|
|
|
# the plane that contains these two vectors, which means rotating about an axis |
852
|
|
|
# perpendicular to both of them (the cross product). |
853
|
|
|
|
854
|
|
|
axis1 = np.cross(ab_old, ab_new) # Only works if ab_old and ab_new are not parallel |
855
|
|
|
axis1length = np.sqrt((axis1 * axis1).sum()) |
856
|
|
|
|
857
|
|
|
if axis1length == 0: |
858
|
|
|
ab_olddif = ab_old + np.array([100, 0, 0]) |
859
|
|
|
axis1 = np.cross(ab_old, ab_olddif) |
860
|
|
|
|
861
|
|
|
# normalising the axis1 vector |
862
|
|
|
axis1 = axis1 / np.sqrt((axis1 * axis1).sum()) |
863
|
|
|
|
864
|
|
|
# The dot product gives the angle between ab_old and ab_new |
865
|
|
|
dot = np.dot(ab_old, ab_new) |
866
|
|
|
x_modulus = np.sqrt((ab_old * ab_old).sum()) |
867
|
|
|
y_modulus = np.sqrt((ab_new * ab_new).sum()) |
868
|
|
|
|
869
|
|
|
# float errors can cause the division to be slightly above 1 for 90 degree rotations, which |
870
|
|
|
# will confuse arccos. |
871
|
|
|
cos_angle = min(dot / x_modulus / y_modulus, 1) |
872
|
|
|
|
873
|
|
|
angle1 = np.arccos(cos_angle) # angle in radians |
874
|
|
|
|
875
|
|
|
# Construct a rotational matrix for axis1 |
876
|
|
|
n1 = axis1[0] |
877
|
|
|
n2 = axis1[1] |
878
|
|
|
n3 = axis1[2] |
879
|
|
|
|
880
|
|
|
m1 = np.matrix(((((n1 * n1) * (1 - np.cos(angle1)) + np.cos(angle1)), |
881
|
|
|
((n1 * n2) * (1 - np.cos(angle1)) - n3 * np.sin(angle1)), |
882
|
|
|
((n1 * n3) * (1 - np.cos(angle1)) + n2 * np.sin(angle1)) |
883
|
|
|
), |
884
|
|
|
(((n2 * n1) * (1 - np.cos(angle1)) + n3 * np.sin(angle1)), |
885
|
|
|
((n2 * n2) * (1 - np.cos(angle1)) + np.cos(angle1)), |
886
|
|
|
((n2 * n3) * (1 - np.cos(angle1)) - n1 * np.sin(angle1)) |
887
|
|
|
), |
888
|
|
|
(((n3 * n1) * (1 - np.cos(angle1)) - n2 * np.sin(angle1)), |
889
|
|
|
((n3 * n2) * (1 - np.cos(angle1)) + n1 * np.sin(angle1)), |
890
|
|
|
((n3 * n3) * (1 - np.cos(angle1)) + np.cos(angle1)) |
891
|
|
|
) |
892
|
|
|
) |
893
|
|
|
) |
894
|
|
|
|
895
|
|
|
# Now that ab_old can be rotated to overlap with ab_new, we need to rotate in another |
896
|
|
|
# axis to fix "tilt". By choosing ab_new as the rotation axis we ensure that the |
897
|
|
|
# ab vectors stay where they need to be. |
898
|
|
|
|
899
|
|
|
# ac_old_rot is the rotated ac_old (around axis1). We need to find the angle to rotate |
900
|
|
|
# ac_old_rot around ab_new to get ac_new. |
901
|
|
|
ac_old_rot = np.array(np.dot(m1, ac_old))[0] |
902
|
|
|
|
903
|
|
|
axis2 = -ab_new # TODO: check maths to find why this negative sign is necessary. Empirically it is now working. |
904
|
|
|
axis2 = axis2 / np.sqrt((axis2 * axis2).sum()) |
905
|
|
|
|
906
|
|
|
# To get the angle of rotation it is most convenient to work in the plane for which axis2 is the normal. |
907
|
|
|
# We must project vectors ac_old_rot and ac_new into this plane. |
908
|
|
|
a = ac_old_rot - np.dot(ac_old_rot, axis2) * axis2 # projection of ac_old_rot in the plane of rotation about axis2 |
909
|
|
|
b = ac_new - np.dot(ac_new, axis2) * axis2 # projection of ac_new in the plane of rotation about axis2 |
910
|
|
|
|
911
|
|
|
# The dot product gives the angle of rotation around axis2 |
912
|
|
|
dot = np.dot(a, b) |
913
|
|
|
|
914
|
|
|
x_modulus = np.sqrt((a * a).sum()) |
915
|
|
|
y_modulus = np.sqrt((b * b).sum()) |
916
|
|
|
cos_angle = min(dot / x_modulus / y_modulus, 1) # float errors can cause the division to be slightly above 1 for 90 degree rotations, which will confuse arccos. |
917
|
|
|
angle2 = np.arccos(cos_angle) # angle in radians |
918
|
|
|
|
919
|
|
|
# Construct a rotation matrix around axis2 |
920
|
|
|
n1 = axis2[0] |
921
|
|
|
n2 = axis2[1] |
922
|
|
|
n3 = axis2[2] |
923
|
|
|
|
924
|
|
|
m2 = np.matrix(((((n1 * n1) * (1 - np.cos(angle2)) + np.cos(angle2)), |
925
|
|
|
((n1 * n2) * (1 - np.cos(angle2)) - n3 * np.sin(angle2)), |
926
|
|
|
((n1 * n3) * (1 - np.cos(angle2)) + n2 * np.sin(angle2)) |
927
|
|
|
), |
928
|
|
|
(((n2 * n1) * (1 - np.cos(angle2)) + n3 * np.sin(angle2)), |
929
|
|
|
((n2 * n2) * (1 - np.cos(angle2)) + np.cos(angle2)), |
930
|
|
|
((n2 * n3) * (1 - np.cos(angle2)) - n1 * np.sin(angle2)) |
931
|
|
|
), |
932
|
|
|
(((n3 * n1) * (1 - np.cos(angle2)) - n2 * np.sin(angle2)), |
933
|
|
|
((n3 * n2) * (1 - np.cos(angle2)) + n1 * np.sin(angle2)), |
934
|
|
|
((n3 * n3) * (1 - np.cos(angle2)) + np.cos(angle2)) |
935
|
|
|
) |
936
|
|
|
) |
937
|
|
|
) |
938
|
|
|
|
939
|
|
|
# To find the new position of r, displace by (a2 - a1) and do the rotations |
940
|
|
|
a1r = r - a1 |
941
|
|
|
|
942
|
|
|
rnew = a2 + np.array(np.dot(m2, np.array(np.dot(m1, a1r))[0]))[0] |
943
|
|
|
|
944
|
|
|
return rnew |
945
|
|
|
|
946
|
|
|
def reorient_roi(self, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos): |
947
|
|
|
""" Move and rotate the ROI to a new position specified by the newpos of 3 reference POIs from the saved ROI. |
948
|
|
|
|
949
|
|
|
@param ref1_coords: coordinates (from ROI save file) of reference 1. |
950
|
|
|
|
951
|
|
|
@param ref2_coords: similar, ref2. |
952
|
|
|
|
953
|
|
|
@param ref3_coords: similar, ref3. |
954
|
|
|
|
955
|
|
|
@param ref1_newpos: the new (current) position of POI reference 1. |
956
|
|
|
|
957
|
|
|
@param ref2_newpos: similar, ref2. |
958
|
|
|
|
959
|
|
|
@param ref3_newpos: similar, ref3. |
960
|
|
|
""" |
961
|
|
|
|
962
|
|
|
for poikey in self.get_all_pois(abc_sort=True): |
963
|
|
|
if poikey is not 'sample' and poikey is not 'crosshair': |
964
|
|
|
thispoi = self.poi_list[poikey] |
965
|
|
|
|
966
|
|
|
old_coords = thispoi.get_coords_in_sample() |
967
|
|
|
|
968
|
|
|
new_coords = self.triangulate(old_coords, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos) |
969
|
|
|
|
970
|
|
|
self.move_coords(poikey=poikey, newpos=new_coords) |
971
|
|
|
|
972
|
|
|
def autofind_pois(self, neighborhood_size=1, min_threshold=10000, max_threshold=1e6): |
973
|
|
|
"""Automatically search the xy scan image for POIs. |
974
|
|
|
|
975
|
|
|
@param neighborhood_size: size in microns. Only the brightest POI per neighborhood will be found. |
976
|
|
|
|
977
|
|
|
@param min_threshold: POIs must have c/s above this threshold. |
978
|
|
|
|
979
|
|
|
@param max_threshold: POIs must have c/s below this threshold. |
980
|
|
|
""" |
981
|
|
|
|
982
|
|
|
# Calculate the neighborhood size in pixels from the image range and resolution |
983
|
|
|
x_range_microns = np.max(self.roi_map_data[:, :, 0]) - np.min(self.roi_map_data[:, :, 0]) |
984
|
|
|
y_range_microns = np.max(self.roi_map_data[:, :, 1]) - np.min(self.roi_map_data[:, :, 1]) |
985
|
|
|
y_pixels = len(self.roi_map_data) |
986
|
|
|
x_pixels = len(self.roi_map_data[1, :]) |
987
|
|
|
|
988
|
|
|
pixels_per_micron = np.max([x_pixels, y_pixels]) / np.max([x_range_microns, y_range_microns]) |
989
|
|
|
# The neighborhood in pixels is nbhd_size * pixels_per_um, but it must be 1 or greater |
990
|
|
|
neighborhood_pix = int(np.max([math.ceil(pixels_per_micron * neighborhood_size), 1])) |
991
|
|
|
|
992
|
|
|
data = self.roi_map_data[:, :, 3] |
993
|
|
|
|
994
|
|
|
data_max = filters.maximum_filter(data, neighborhood_pix) |
995
|
|
|
maxima = (data == data_max) |
996
|
|
|
data_min = filters.minimum_filter(data, 3 * neighborhood_pix) |
997
|
|
|
diff = ((data_max - data_min) > min_threshold) |
998
|
|
|
maxima[diff is False] = 0 |
999
|
|
|
|
1000
|
|
|
labeled, num_objects = ndimage.label(maxima) |
1001
|
|
|
xy = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects + 1))) |
1002
|
|
|
|
1003
|
|
|
for count, pix_pos in enumerate(xy): |
1004
|
|
|
poi_pos = self.roi_map_data[pix_pos[0], pix_pos[1], :][0:3] |
1005
|
|
|
this_poi_key = self.add_poi(position=poi_pos, emit_change=False) |
1006
|
|
|
self.rename_poi(poikey=this_poi_key, name='spot' + str(count), emit_change=False) |
1007
|
|
|
|
1008
|
|
|
# Now that all the POIs are created, emit the signal for other things (ie gui) to update |
1009
|
|
|
self.signal_poi_updated.emit() |
1010
|
|
|
|