|
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
|
|
|
|