Completed
Push — master ( b34ce1...c4d77b )
by Jan
02:38
created

PoiManagerLogic.on_activate()   A

Complexity

Conditions 1

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
dl 0
loc 16
rs 9.4285
c 2
b 1
f 0
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:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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