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