Total Complexity | 112 |
Total Lines | 816 |
Duplicated Lines | 1.47 % |
Changes | 4 | ||
Bugs | 3 | Features | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like PoiManagerLogic often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | # -*- coding: utf-8 -*- |
||
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 |