Commit 0673940c840118edf2b74d57dd93d5dd648a61a0

Authored by Victor Hugo Souza
Committed by GitHub
1 parent b7470270
Exists in master

Refactor of thread management in neuronavigation (#242)

* ADD: Initial support for real time tracts visualization during navigation

* ENH: Improved affine transformations for importing external polydata in world coordinates

* ENH: Tracts visualization with cross move started

* Create Trekker thread

* Tracts style working and save and load project with affine

* Tract computing incorporated to navigation pipeline

* Add offset to coil normal to get coordinate inside brain

* ADD: Pipeline for navigation with tract visualiztion and better debug tracker

* ENH: Refactor the cross update feature

* FIX: Fixed update cross during navigation and improved tract visualization

* FIX: Seed flip when applying offset and added list to offset user control

* ADD: Brain surface texture mapping with peeling depth

* Improved sleep for navigation with tractography

* ENH: Tract computation speed doubled and tracts colors fixed

* ENH: Print removed in tract coloring

* ENH: Improved visualization of tracts

* ENH: Divide tract computation in chuncks

* ADD: Threads using Queue for navigation

* ADD: Thread computing with class and pipeline

* ENH: Navigation working with separate threads and lock

* ENH: Navigation with tracts running in producer-consumer method with locks

* ENH: Refactoring tracts threads

* Update tractography data exchange

* ENH: Improved Trekker GUI and tractography navigation working

* Update optimal Trekker parameters

* ADD: Dialog to adjust Trekker parameters

* ENH: Dialog to load Trekker JSON configuration file

* ENH: Replaced Trekker default list by dictionary with values[C

* ENH: Remove tract code comments and refactor navigation pipeline

* ENH: Replaced dialog warnings for message box in navigation tab

* ENH: Split the removal and update of tracts

* ENH: Major changes in navigation array management and tracts cleaning
- Replaced all matrix to array, now using python matrix operator
- Refactored flip_x function
- Improved cleaning of tracts

* ENH: Tract visualization enabled in cross function and matrix operations replaced by arrays
- Additional code cleaning and commenting
- Refactoring of tip offset snippet

* ADD: Control of brain mask opacity

* FIX: Remove brain surface when closing project

* ENH: Update trekker parameters after loading files and major refactor in flip_x
- Additional refactor of FRE computation and use of base_creation

* ENH: Improved object load dialog, OnNavigate pipeline and better tracts cleaning

* FIX: Navigation with tractography working
- Improved offset computation for seed
- Improved flip_x. No need to use a function, only multiply Y coordinate by -1

* Refactored compute_seed, affine_to_vtk, and set_message for tracts computation
- Cleaned unecessary comments
- Providing m_img already flip_x while set_message

* FIX: Tract removal with update cross position was disabled

* ENH: Threading of navigation replaced to  LifoQueue, working with dev_tracker but not tested in real tracker

* ENH: Tractography running with new limited Queue and nowait
- Tracts actors are computed all at once, no optimization for computing blocks

* FIX: Update pubsub in tracker
- Navgation with tractography running

* ENH: Multiblock tract computation and fixed tract cleaning
- Tractography navigation working at 0.1-s sleep

* ENH: Documented tractography computation and cleaned code

* FIX: Navigation was not running if tracts were not found
- created thread in new format for trigger
- removed comments of unused pipelines and threading
- removed commented old FRE base creation

* FIX: Generalize new navigation to objects even without tracts
- Navigation with object working seamless with and without tracts
- Navigation in other modes (single ref, no object) not working
- Added one extra queue to generalize the pipeline in navigation

* FIX: Static object navigation working and refactor navigation pipeline
- Object corregistration split in functions
- Cleaned comments on reference computation

* FIX: Tractography navigation working with and without coil
- Applied the new threading to the non-object navigations
- Improved visualization of the offset marker for tractography
- Cleaned commented code

* ENH: Clean solved TODOs and commented unused navigation code

* FIX: Fixed open and save dialogs for navigation purposes

* FIX: Reading affine from old projects and replaced affine function form transformation

* UPD: Removed surface back culling prints

* DEL: Hided tractography panel and removed brain mesh mask
invesalius/constants.py
@@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
19 19
20 import os.path 20 import os.path
21 import platform 21 import platform
  22 +import psutil
22 import sys 23 import sys
23 import wx 24 import wx
24 import itertools 25 import itertools
@@ -521,6 +522,11 @@ ID_GOTO_COORD = wx.NewId() @@ -521,6 +522,11 @@ ID_GOTO_COORD = wx.NewId()
521 522
522 ID_MANUAL_WWWL = wx.NewId() 523 ID_MANUAL_WWWL = wx.NewId()
523 524
  525 +# Tractography with Trekker
  526 +ID_TREKKER_MASK = wx.NewId()
  527 +ID_TREKKER_IMG = wx.NewId()
  528 +ID_TREKKER_FOD = wx.NewId()
  529 +
524 #--------------------------------------------------------- 530 #---------------------------------------------------------
525 STATE_DEFAULT = 1000 531 STATE_DEFAULT = 1000
526 STATE_WL = 1001 532 STATE_WL = 1001
@@ -545,6 +551,7 @@ SLICE_STATE_REMOVE_MASK_PARTS = 3012 @@ -545,6 +551,7 @@ SLICE_STATE_REMOVE_MASK_PARTS = 3012
545 SLICE_STATE_SELECT_MASK_PARTS = 3013 551 SLICE_STATE_SELECT_MASK_PARTS = 3013
546 SLICE_STATE_FFILL_SEGMENTATION = 3014 552 SLICE_STATE_FFILL_SEGMENTATION = 3014
547 SLICE_STATE_CROP_MASK = 3015 553 SLICE_STATE_CROP_MASK = 3015
  554 +SLICE_STATE_TRACTS = 3016
548 555
549 VOLUME_STATE_SEED = 2001 556 VOLUME_STATE_SEED = 2001
550 # STATE_LINEAR_MEASURE = 3001 557 # STATE_LINEAR_MEASURE = 3001
@@ -559,7 +566,7 @@ TOOL_STATES = [STATE_WL, STATE_SPIN, STATE_ZOOM, @@ -559,7 +566,7 @@ TOOL_STATES = [STATE_WL, STATE_SPIN, STATE_ZOOM,
559 566
560 567
561 TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL, 568 TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL,
562 - SLICE_STATE_REORIENT] 569 + SLICE_STATE_REORIENT, SLICE_STATE_TRACTS]
563 570
564 571
565 SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES 572 SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES
@@ -651,6 +658,8 @@ BOOLEAN_XOR = 4 @@ -651,6 +658,8 @@ BOOLEAN_XOR = 4
651 658
652 #------------ Navigation defaults ------------------- 659 #------------ Navigation defaults -------------------
653 660
  661 +MARKER_COLOUR = (1.0, 1.0, 0.)
  662 +MARKER_SIZE = 2
654 SELECT = 0 663 SELECT = 0
655 MTC = 1 664 MTC = 1
656 FASTRAK = 2 665 FASTRAK = 2
@@ -738,3 +747,17 @@ COIL_COORD_THRESHOLD = 3 @@ -738,3 +747,17 @@ COIL_COORD_THRESHOLD = 3
738 TIMESTAMP = 2.0 747 TIMESTAMP = 2.0
739 748
740 CAM_MODE = True 749 CAM_MODE = True
  750 +
  751 +# Tractography visualization
  752 +N_TRACTS = 100
  753 +PEEL_DEPTH = 5
  754 +MAX_PEEL_DEPTH = 30
  755 +SEED_OFFSET = 15
  756 +SEED_RADIUS = 1.5
  757 +SLEEP_NAVIGATION = 0.1
  758 +BRAIN_OPACITY = 0.5
  759 +N_CPU = psutil.cpu_count()
  760 +TREKKER_CONFIG = {'seed_max': 1, 'step_size': 0.1, 'min_fod': 0.1, 'probe_quality': 3,
  761 + 'max_interval': 1, 'min_radius_curv': 0.8, 'probe_length': 0.4,
  762 + 'write_interval': 50, 'numb_threads': '', 'max_lenth': 200,
  763 + 'min_lenth': 20, 'max_sampling_step': 100}
invesalius/control.py
@@ -32,6 +32,7 @@ import invesalius.data.mask as msk @@ -32,6 +32,7 @@ import invesalius.data.mask as msk
32 import invesalius.data.measures as measures 32 import invesalius.data.measures as measures
33 import invesalius.data.slice_ as sl 33 import invesalius.data.slice_ as sl
34 import invesalius.data.surface as srf 34 import invesalius.data.surface as srf
  35 +import invesalius.data.transformations as tr
35 import invesalius.data.volume as volume 36 import invesalius.data.volume as volume
36 import invesalius.gui.dialogs as dialog 37 import invesalius.gui.dialogs as dialog
37 import invesalius.project as prj 38 import invesalius.project as prj
@@ -57,7 +58,6 @@ class Controller(): @@ -57,7 +58,6 @@ class Controller():
57 def __init__(self, frame): 58 def __init__(self, frame):
58 self.surface_manager = srf.SurfaceManager() 59 self.surface_manager = srf.SurfaceManager()
59 self.volume = volume.Volume() 60 self.volume = volume.Volume()
60 - self.plugin_manager = plugins.PluginManager()  
61 self.__bind_events() 61 self.__bind_events()
62 self.frame = frame 62 self.frame = frame
63 self.progress_dialog = None 63 self.progress_dialog = None
@@ -76,12 +76,9 @@ class Controller(): @@ -76,12 +76,9 @@ class Controller():
76 76
77 Publisher.sendMessage('Load Preferences') 77 Publisher.sendMessage('Load Preferences')
78 78
79 - self.plugin_manager.find_plugins()  
80 -  
81 def __bind_events(self): 79 def __bind_events(self):
82 Publisher.subscribe(self.OnImportMedicalImages, 'Import directory') 80 Publisher.subscribe(self.OnImportMedicalImages, 'Import directory')
83 Publisher.subscribe(self.OnImportGroup, 'Import group') 81 Publisher.subscribe(self.OnImportGroup, 'Import group')
84 - Publisher.subscribe(self.OnImportFolder, 'Import folder')  
85 Publisher.subscribe(self.OnShowDialogImportDirectory, 82 Publisher.subscribe(self.OnShowDialogImportDirectory,
86 'Show import directory dialog') 83 'Show import directory dialog')
87 Publisher.subscribe(self.OnShowDialogImportOtherFiles, 84 Publisher.subscribe(self.OnShowDialogImportOtherFiles,
@@ -123,8 +120,6 @@ class Controller(): @@ -123,8 +120,6 @@ class Controller():
123 120
124 Publisher.subscribe(self.Send_affine, 'Get affine matrix') 121 Publisher.subscribe(self.Send_affine, 'Get affine matrix')
125 122
126 - Publisher.subscribe(self.create_project_from_matrix, 'Create project from matrix')  
127 -  
128 def SetBitmapSpacing(self, spacing): 123 def SetBitmapSpacing(self, spacing):
129 proj = prj.Project() 124 proj = prj.Project()
130 proj.spacing = spacing 125 proj.spacing = spacing
@@ -336,6 +331,10 @@ class Controller(): @@ -336,6 +331,10 @@ class Controller():
336 331
337 self.Slice.window_level = proj.level 332 self.Slice.window_level = proj.level
338 self.Slice.window_width = proj.window 333 self.Slice.window_width = proj.window
  334 + if proj.affine:
  335 + self.Slice.affine = np.asarray(proj.affine).reshape(4, 4)
  336 + else:
  337 + self.Slice.affine = None
339 338
340 Publisher.sendMessage('Update threshold limits list', 339 Publisher.sendMessage('Update threshold limits list',
341 threshold_range=proj.threshold_range) 340 threshold_range=proj.threshold_range)
@@ -504,32 +503,7 @@ class Controller(): @@ -504,32 +503,7 @@ class Controller():
504 self.LoadProject() 503 self.LoadProject()
505 Publisher.sendMessage("Enable state project", state=True) 504 Publisher.sendMessage("Enable state project", state=True)
506 505
507 - def OnImportFolder(self, folder):  
508 - Publisher.sendMessage('Begin busy cursor')  
509 - folder = os.path.abspath(folder)  
510 -  
511 - proj = prj.Project()  
512 - proj.load_from_folder(folder)  
513 -  
514 - self.Slice = sl.Slice()  
515 - self.Slice._open_image_matrix(proj.matrix_filename,  
516 - tuple(proj.matrix_shape),  
517 - proj.matrix_dtype)  
518 -  
519 - self.Slice.window_level = proj.level  
520 - self.Slice.window_width = proj.window  
521 -  
522 - Publisher.sendMessage('Update threshold limits list',  
523 - threshold_range=proj.threshold_range)  
524 -  
525 - session = ses.Session()  
526 - filename = proj.name+".inv3"  
527 - filename = filename.replace("/", "") #Fix problem case other/Skull_DICOM  
528 - dirpath = session.CreateProject(filename)  
529 - self.LoadProject()  
530 - Publisher.sendMessage("Enable state project", state=True)  
531 -  
532 - Publisher.sendMessage('End busy cursor') 506 + #-------------------------------------------------------------------------------------
533 507
534 def LoadProject(self): 508 def LoadProject(self):
535 proj = prj.Project() 509 proj = prj.Project()
@@ -688,6 +662,9 @@ class Controller(): @@ -688,6 +662,9 @@ class Controller():
688 proj.matrix_dtype = matrix.dtype.name 662 proj.matrix_dtype = matrix.dtype.name
689 proj.matrix_filename = matrix_filename 663 proj.matrix_filename = matrix_filename
690 664
  665 + if self.affine is not None:
  666 + proj.affine = self.affine.tolist()
  667 +
691 # Orientation must be CORONAL in order to as_closes_canonical and 668 # Orientation must be CORONAL in order to as_closes_canonical and
692 # swap axis in img2memmap to work in a standardized way. 669 # swap axis in img2memmap to work in a standardized way.
693 # TODO: Create standard import image for all acquisition orientations 670 # TODO: Create standard import image for all acquisition orientations
@@ -700,7 +677,6 @@ class Controller(): @@ -700,7 +677,6 @@ class Controller():
700 proj.level = self.Slice.window_level 677 proj.level = self.Slice.window_level
701 proj.threshold_range = int(matrix.min()), int(matrix.max()) 678 proj.threshold_range = int(matrix.min()), int(matrix.max())
702 proj.spacing = self.Slice.spacing 679 proj.spacing = self.Slice.spacing
703 - proj.affine = self.affine.tolist()  
704 680
705 ###### 681 ######
706 session = ses.Session() 682 session = ses.Session()
@@ -872,11 +848,13 @@ class Controller(): @@ -872,11 +848,13 @@ class Controller():
872 def OnOpenOtherFiles(self, filepath): 848 def OnOpenOtherFiles(self, filepath):
873 filepath = utils.decode(filepath, const.FS_ENCODE) 849 filepath = utils.decode(filepath, const.FS_ENCODE)
874 if not(filepath) == None: 850 if not(filepath) == None:
875 - name = os.path.basename(filepath).split(".")[0] 851 + name = filepath.rpartition('\\')[-1].split('.')
  852 +
876 group = oth.ReadOthers(filepath) 853 group = oth.ReadOthers(filepath)
  854 +
877 if group: 855 if group:
878 matrix, matrix_filename = self.OpenOtherFiles(group) 856 matrix, matrix_filename = self.OpenOtherFiles(group)
879 - self.CreateOtherProject(name, matrix, matrix_filename) 857 + self.CreateOtherProject(str(name[0]), matrix, matrix_filename)
880 self.LoadProject() 858 self.LoadProject()
881 Publisher.sendMessage("Enable state project", state=True) 859 Publisher.sendMessage("Enable state project", state=True)
882 else: 860 else:
@@ -981,10 +959,10 @@ class Controller(): @@ -981,10 +959,10 @@ class Controller():
981 self.matrix, scalar_range, self.filename = image_utils.img2memmap(group) 959 self.matrix, scalar_range, self.filename = image_utils.img2memmap(group)
982 960
983 hdr = group.header 961 hdr = group.header
984 - if group.affine.any():  
985 - self.affine = group.affine  
986 - Publisher.sendMessage('Update affine matrix',  
987 - affine=self.affine, status=True) 962 + # if group.affine.any():
  963 + # self.affine = group.affine
  964 + # Publisher.sendMessage('Update affine matrix',
  965 + # affine=self.affine, status=True)
988 hdr.set_data_dtype('int16') 966 hdr.set_data_dtype('int16')
989 dims = hdr.get_zooms() 967 dims = hdr.get_zooms()
990 dimsf = tuple([float(s) for s in dims]) 968 dimsf = tuple([float(s) for s in dims])
@@ -1000,6 +978,18 @@ class Controller(): @@ -1000,6 +978,18 @@ class Controller():
1000 self.Slice.window_level = wl 978 self.Slice.window_level = wl
1001 self.Slice.window_width = ww 979 self.Slice.window_width = ww
1002 980
  981 + if group.affine.any():
  982 + # TODO: replace the inverse of the affine by the actual affine in the whole code
  983 + # remove scaling factor for non-unitary voxel dimensions
  984 + # self.affine = image_utils.world2invspace(affine=group.affine)
  985 + scale, shear, angs, trans, persp = tr.decompose_matrix(group.affine)
  986 + self.affine = np.linalg.inv(tr.compose_matrix(scale=None, shear=shear,
  987 + angles=angs, translate=trans, perspective=persp))
  988 + # print("repos_img: {}".format(repos_img))
  989 + self.Slice.affine = self.affine
  990 + Publisher.sendMessage('Update affine matrix',
  991 + affine=self.affine, status=True)
  992 +
1003 scalar_range = int(scalar_range[0]), int(scalar_range[1]) 993 scalar_range = int(scalar_range[0]), int(scalar_range[1])
1004 Publisher.sendMessage('Update threshold limits list', 994 Publisher.sendMessage('Update threshold limits list',
1005 threshold_range=scalar_range) 995 threshold_range=scalar_range)
@@ -1066,24 +1056,3 @@ class Controller(): @@ -1066,24 +1056,3 @@ class Controller():
1066 1056
1067 def ApplyReorientation(self): 1057 def ApplyReorientation(self):
1068 self.Slice.apply_reorientation() 1058 self.Slice.apply_reorientation()
1069 -  
1070 - def start_new_inv_instance(self, image, name, spacing, modality, orientation, window_width, window_level):  
1071 - p = prj.Project()  
1072 - project_folder = tempfile.mkdtemp()  
1073 - p.create_project_file(name, spacing, modality, orientation, window_width, window_level, image, folder=project_folder)  
1074 - err_msg = ''  
1075 - try:  
1076 - sp = subprocess.Popen([sys.executable, sys.argv[0], '--import-folder', project_folder],  
1077 - stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=os.getcwd())  
1078 - except Exception as err:  
1079 - err_msg = str(err)  
1080 - else:  
1081 - try:  
1082 - if sp.wait(2):  
1083 - err_msg = sp.stderr.read().decode('utf8')  
1084 - sp.terminate()  
1085 - except subprocess.TimeoutExpired:  
1086 - pass  
1087 -  
1088 - if err_msg:  
1089 - dialog.MessageBox(None, "It was not possible to launch new instance of InVesalius3 dsfa dfdsfa sdfas fdsaf asdfasf dsaa", err_msg)  
invesalius/data/bases.py
1 -from math import sqrt, pi  
2 import numpy as np 1 import numpy as np
3 import invesalius.data.coordinates as dco 2 import invesalius.data.coordinates as dco
4 import invesalius.data.transformations as tr 3 import invesalius.data.transformations as tr
@@ -23,8 +22,7 @@ def angle_calculation(ap_axis, coil_axis): @@ -23,8 +22,7 @@ def angle_calculation(ap_axis, coil_axis):
23 22
24 def base_creation_old(fiducials): 23 def base_creation_old(fiducials):
25 """ 24 """
26 - Calculate the origin and matrix for coordinate system  
27 - transformation. 25 + Calculate the origin and matrix for coordinate system transformation.
28 q: origin of coordinate system 26 q: origin of coordinate system
29 g1, g2, g3: orthogonal vectors of coordinate system 27 g1, g2, g3: orthogonal vectors of coordinate system
30 28
@@ -49,9 +47,9 @@ def base_creation_old(fiducials): @@ -49,9 +47,9 @@ def base_creation_old(fiducials):
49 47
50 g3 = np.cross(g2, g1) 48 g3 = np.cross(g2, g1)
51 49
52 - g1 = g1/sqrt(np.dot(g1, g1))  
53 - g2 = g2/sqrt(np.dot(g2, g2))  
54 - g3 = g3/sqrt(np.dot(g3, g3)) 50 + g1 = g1/np.sqrt(np.dot(g1, g1))
  51 + g2 = g2/np.sqrt(np.dot(g2, g2))
  52 + g3 = g3/np.sqrt(np.dot(g3, g3))
55 53
56 m = np.matrix([[g1[0], g1[1], g1[2]], 54 m = np.matrix([[g1[0], g1[1], g1[2]],
57 [g2[0], g2[1], g2[2]], 55 [g2[0], g2[1], g2[2]],
@@ -79,7 +77,7 @@ def base_creation(fiducials): @@ -79,7 +77,7 @@ def base_creation(fiducials):
79 77
80 sub1 = p2 - p1 78 sub1 = p2 - p1
81 sub2 = p3 - p1 79 sub2 = p3 - p1
82 - lamb = (sub1[0]*sub2[0]+sub1[1]*sub2[1]+sub1[2]*sub2[2])/np.dot(sub1, sub1) 80 + lamb = np.dot(sub1, sub2)/np.dot(sub1, sub1)
83 81
84 q = p1 + lamb*sub1 82 q = p1 + lamb*sub1
85 g1 = p3 - q 83 g1 = p3 - q
@@ -90,20 +88,52 @@ def base_creation(fiducials): @@ -90,20 +88,52 @@ def base_creation(fiducials):
90 88
91 g3 = np.cross(g1, g2) 89 g3 = np.cross(g1, g2)
92 90
93 - g1 = g1/sqrt(np.dot(g1, g1))  
94 - g2 = g2/sqrt(np.dot(g2, g2))  
95 - g3 = g3/sqrt(np.dot(g3, g3))  
96 -  
97 - m = np.matrix([[g1[0], g2[0], g3[0]],  
98 - [g1[1], g2[1], g3[1]],  
99 - [g1[2], g2[2], g3[2]]])  
100 -  
101 - m_inv = m.I  
102 -  
103 - return m, q, m_inv  
104 -  
105 -  
106 -def calculate_fre(fiducials, minv, n, q, o): 91 + g1 = g1/np.sqrt(np.dot(g1, g1))
  92 + g2 = g2/np.sqrt(np.dot(g2, g2))
  93 + g3 = g3/np.sqrt(np.dot(g3, g3))
  94 +
  95 + m = np.zeros([3, 3])
  96 + m[:, 0] = g1/np.sqrt(np.dot(g1, g1))
  97 + m[:, 1] = g2/np.sqrt(np.dot(g2, g2))
  98 + m[:, 2] = g3/np.sqrt(np.dot(g3, g3))
  99 +
  100 + return m, q
  101 +
  102 +
  103 +# def calculate_fre(fiducials, minv, n, q, o):
  104 +# """
  105 +# Calculate the Fiducial Registration Error for neuronavigation.
  106 +#
  107 +# :param fiducials: array of 6 rows (image and tracker fiducials) and 3 columns (x, y, z) with coordinates
  108 +# :param minv: inverse matrix given by base creation
  109 +# :param n: base change matrix given by base creation
  110 +# :param q: origin of first base
  111 +# :param o: origin of second base
  112 +# :return: float number of fiducial registration error
  113 +# """
  114 +#
  115 +# img = np.zeros([3, 3])
  116 +# dist = np.zeros([3, 1])
  117 +#
  118 +# q1 = np.mat(q).reshape(3, 1)
  119 +# q2 = np.mat(o).reshape(3, 1)
  120 +#
  121 +# p1 = np.mat(fiducials[3, :]).reshape(3, 1)
  122 +# p2 = np.mat(fiducials[4, :]).reshape(3, 1)
  123 +# p3 = np.mat(fiducials[5, :]).reshape(3, 1)
  124 +#
  125 +# img[0, :] = np.asarray((q1 + (minv * n) * (p1 - q2)).reshape(1, 3))
  126 +# img[1, :] = np.asarray((q1 + (minv * n) * (p2 - q2)).reshape(1, 3))
  127 +# img[2, :] = np.asarray((q1 + (minv * n) * (p3 - q2)).reshape(1, 3))
  128 +#
  129 +# dist[0] = np.sqrt(np.sum(np.power((img[0, :] - fiducials[0, :]), 2)))
  130 +# dist[1] = np.sqrt(np.sum(np.power((img[1, :] - fiducials[1, :]), 2)))
  131 +# dist[2] = np.sqrt(np.sum(np.power((img[2, :] - fiducials[2, :]), 2)))
  132 +#
  133 +# return float(np.sqrt(np.sum(dist ** 2) / 3))
  134 +
  135 +
  136 +def calculate_fre_m(fiducials):
107 """ 137 """
108 Calculate the Fiducial Registration Error for neuronavigation. 138 Calculate the Fiducial Registration Error for neuronavigation.
109 139
@@ -115,19 +145,29 @@ def calculate_fre(fiducials, minv, n, q, o): @@ -115,19 +145,29 @@ def calculate_fre(fiducials, minv, n, q, o):
115 :return: float number of fiducial registration error 145 :return: float number of fiducial registration error
116 """ 146 """
117 147
  148 + m, q1, minv = base_creation_old(fiducials[:3, :])
  149 + n, q2, ninv = base_creation_old(fiducials[3:, :])
  150 +
  151 + # TODO: replace the old by the new base creation
  152 + # the values differ greatly if FRE is computed using the old or new base_creation
  153 + # check the reason for the difference, because they should be the same
  154 + # m, q1 = base_creation(fiducials[:3, :])
  155 + # n, q2 = base_creation(fiducials[3:, :])
  156 + # minv = np.linalg.inv(m)
  157 +
118 img = np.zeros([3, 3]) 158 img = np.zeros([3, 3])
119 dist = np.zeros([3, 1]) 159 dist = np.zeros([3, 1])
120 160
121 - q1 = np.mat(q).reshape(3, 1)  
122 - q2 = np.mat(o).reshape(3, 1) 161 + q1 = q1.reshape(3, 1)
  162 + q2 = q2.reshape(3, 1)
123 163
124 - p1 = np.mat(fiducials[3, :]).reshape(3, 1)  
125 - p2 = np.mat(fiducials[4, :]).reshape(3, 1)  
126 - p3 = np.mat(fiducials[5, :]).reshape(3, 1) 164 + p1 = fiducials[3, :].reshape(3, 1)
  165 + p2 = fiducials[4, :].reshape(3, 1)
  166 + p3 = fiducials[5, :].reshape(3, 1)
127 167
128 - img[0, :] = np.asarray((q1 + (minv * n) * (p1 - q2)).reshape(1, 3))  
129 - img[1, :] = np.asarray((q1 + (minv * n) * (p2 - q2)).reshape(1, 3))  
130 - img[2, :] = np.asarray((q1 + (minv * n) * (p3 - q2)).reshape(1, 3)) 168 + img[0, :] = (q1 + (minv @ n) * (p1 - q2)).reshape(1, 3)
  169 + img[1, :] = (q1 + (minv @ n) * (p2 - q2)).reshape(1, 3)
  170 + img[2, :] = (q1 + (minv @ n) * (p3 - q2)).reshape(1, 3)
131 171
132 dist[0] = np.sqrt(np.sum(np.power((img[0, :] - fiducials[0, :]), 2))) 172 dist[0] = np.sqrt(np.sum(np.power((img[0, :] - fiducials[0, :]), 2)))
133 dist[1] = np.sqrt(np.sum(np.power((img[1, :] - fiducials[1, :]), 2))) 173 dist[1] = np.sqrt(np.sum(np.power((img[1, :] - fiducials[1, :]), 2)))
@@ -136,44 +176,68 @@ def calculate_fre(fiducials, minv, n, q, o): @@ -136,44 +176,68 @@ def calculate_fre(fiducials, minv, n, q, o):
136 return float(np.sqrt(np.sum(dist ** 2) / 3)) 176 return float(np.sqrt(np.sum(dist ** 2) / 3))
137 177
138 178
139 -def flip_x(point):  
140 - """  
141 - Flip coordinates of a vector according to X axis  
142 - Coronal Images do not require this transformation - 1 tested  
143 - and for this case, at navigation, the z axis is inverted  
144 -  
145 - It's necessary to multiply the z coordinate by (-1). Possibly  
146 - because the origin of coordinate system of imagedata is  
147 - located in superior left corner and the origin of VTK scene coordinate  
148 - system (polygonal surface) is in the interior left corner. Second  
149 - possibility is the order of slice stacking  
150 -  
151 - :param point: list of coordinates x, y and z  
152 - :return: flipped coordinates  
153 - """  
154 -  
155 - # TODO: check if the Flip function is related to the X or Y axis  
156 -  
157 - point = np.matrix(point + (0,))  
158 - point[0, 2] = -point[0, 2]  
159 -  
160 - m_rot = np.matrix([[1.0, 0.0, 0.0, 0.0],  
161 - [0.0, -1.0, 0.0, 0.0],  
162 - [0.0, 0.0, -1.0, 0.0],  
163 - [0.0, 0.0, 0.0, 1.0]])  
164 - m_trans = np.matrix([[1.0, 0, 0, -point[0, 0]],  
165 - [0.0, 1.0, 0, -point[0, 1]],  
166 - [0.0, 0.0, 1.0, -point[0, 2]],  
167 - [0.0, 0.0, 0.0, 1.0]])  
168 - m_trans_return = np.matrix([[1.0, 0, 0, point[0, 0]],  
169 - [0.0, 1.0, 0, point[0, 1]],  
170 - [0.0, 0.0, 1.0, point[0, 2]],  
171 - [0.0, 0.0, 0.0, 1.0]])  
172 -  
173 - point_rot = point*m_trans*m_rot*m_trans_return  
174 - x, y, z = point_rot.tolist()[0][:3]  
175 -  
176 - return x, y, z 179 +# def flip_x(point):
  180 +# """
  181 +# Flip coordinates of a vector according to X axis
  182 +# Coronal Images do not require this transformation - 1 tested
  183 +# and for this case, at navigation, the z axis is inverted
  184 +#
  185 +# It's necessary to multiply the z coordinate by (-1). Possibly
  186 +# because the origin of coordinate system of imagedata is
  187 +# located in superior left corner and the origin of VTK scene coordinate
  188 +# system (polygonal surface) is in the interior left corner. Second
  189 +# possibility is the order of slice stacking
  190 +#
  191 +# :param point: list of coordinates x, y and z
  192 +# :return: flipped coordinates
  193 +# """
  194 +#
  195 +# # TODO: check if the Flip function is related to the X or Y axis
  196 +#
  197 +# point = np.matrix(point + (0,))
  198 +# point[0, 2] = -point[0, 2]
  199 +#
  200 +# m_rot = np.matrix([[1.0, 0.0, 0.0, 0.0],
  201 +# [0.0, -1.0, 0.0, 0.0],
  202 +# [0.0, 0.0, -1.0, 0.0],
  203 +# [0.0, 0.0, 0.0, 1.0]])
  204 +# m_trans = np.matrix([[1.0, 0, 0, -point[0, 0]],
  205 +# [0.0, 1.0, 0, -point[0, 1]],
  206 +# [0.0, 0.0, 1.0, -point[0, 2]],
  207 +# [0.0, 0.0, 0.0, 1.0]])
  208 +# m_trans_return = np.matrix([[1.0, 0, 0, point[0, 0]],
  209 +# [0.0, 1.0, 0, point[0, 1]],
  210 +# [0.0, 0.0, 1.0, point[0, 2]],
  211 +# [0.0, 0.0, 0.0, 1.0]])
  212 +#
  213 +# point_rot = point*m_trans*m_rot*m_trans_return
  214 +# x, y, z = point_rot.tolist()[0][:3]
  215 +#
  216 +# return x, y, z
  217 +
  218 +
  219 +# def flip_x_m(point):
  220 +# """
  221 +# Rotate coordinates of a vector by pi around X axis in static reference frame.
  222 +#
  223 +# InVesalius also require to multiply the z coordinate by (-1). Possibly
  224 +# because the origin of coordinate system of imagedata is
  225 +# located in superior left corner and the origin of VTK scene coordinate
  226 +# system (polygonal surface) is in the interior left corner. Second
  227 +# possibility is the order of slice stacking
  228 +#
  229 +# :param point: list of coordinates x, y and z
  230 +# :return: rotated coordinates
  231 +# """
  232 +#
  233 +# point_4 = np.hstack((point, 1.)).reshape([4, 1])
  234 +# point_4[2, 0] = -point_4[2, 0]
  235 +#
  236 +# m_rot = tr.euler_matrix(pi, 0, 0)
  237 +#
  238 +# point_rot = m_rot @ point_4
  239 +#
  240 +# return point_rot[0, 0], point_rot[1, 0], point_rot[2, 0]
177 241
178 242
179 def flip_x_m(point): 243 def flip_x_m(point):
@@ -190,14 +254,14 @@ def flip_x_m(point): @@ -190,14 +254,14 @@ def flip_x_m(point):
190 :return: rotated coordinates 254 :return: rotated coordinates
191 """ 255 """
192 256
193 - point_4 = np.hstack((point, 1.)).reshape([4, 1]) 257 + point_4 = np.hstack((point, 1.)).reshape(4, 1)
194 point_4[2, 0] = -point_4[2, 0] 258 point_4[2, 0] = -point_4[2, 0]
195 259
196 - m_rot = np.asmatrix(tr.euler_matrix(pi, 0, 0)) 260 + m_rot = tr.euler_matrix(np.pi, 0, 0)
197 261
198 - point_rot = m_rot*point_4 262 + point_rot = m_rot @ point_4
199 263
200 - return point_rot[0, 0], point_rot[1, 0], point_rot[2, 0] 264 + return point_rot
201 265
202 266
203 def object_registration(fiducials, orients, coord_raw, m_change): 267 def object_registration(fiducials, orients, coord_raw, m_change):
@@ -224,48 +288,48 @@ def object_registration(fiducials, orients, coord_raw, m_change): @@ -224,48 +288,48 @@ def object_registration(fiducials, orients, coord_raw, m_change):
224 fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3] 288 fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3]
225 289
226 # compute initial alignment of probe fixed in the object in source frame 290 # compute initial alignment of probe fixed in the object in source frame
227 - t_s0_raw = np.asmatrix(tr.translation_matrix(coords[3, :3]))  
228 - r_s0_raw = np.asmatrix(tr.euler_matrix(np.radians(coords[3, 3]), np.radians(coords[3, 4]),  
229 - np.radians(coords[3, 5]), 'rzyx'))  
230 - s0_raw = np.asmatrix(tr.concatenate_matrices(t_s0_raw, r_s0_raw)) 291 + t_s0_raw = tr.translation_matrix(coords[3, :3])
  292 + r_s0_raw = tr.euler_matrix(np.radians(coords[3, 3]), np.radians(coords[3, 4]),
  293 + np.radians(coords[3, 5]), 'rzyx')
  294 + s0_raw = tr.concatenate_matrices(t_s0_raw, r_s0_raw)
231 295
232 # compute change of basis for object fiducials in source frame 296 # compute change of basis for object fiducials in source frame
233 - base_obj_raw, q_obj_raw, base_inv_obj_raw = base_creation(fids_raw[:3, :3])  
234 - r_obj_raw = np.asmatrix(np.identity(4)) 297 + base_obj_raw, q_obj_raw = base_creation(fids_raw[:3, :3])
  298 + r_obj_raw = np.identity(4)
235 r_obj_raw[:3, :3] = base_obj_raw[:3, :3] 299 r_obj_raw[:3, :3] = base_obj_raw[:3, :3]
236 - t_obj_raw = np.asmatrix(tr.translation_matrix(q_obj_raw))  
237 - m_obj_raw = np.asmatrix(tr.concatenate_matrices(t_obj_raw, r_obj_raw)) 300 + t_obj_raw = tr.translation_matrix(q_obj_raw)
  301 + m_obj_raw = tr.concatenate_matrices(t_obj_raw, r_obj_raw)
238 302
239 for ic in range(0, 4): 303 for ic in range(0, 4):
240 if coord_raw.any(): 304 if coord_raw.any():
241 # compute object fiducials in reference frame 305 # compute object fiducials in reference frame
242 fids_dyn[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coord_raw[1, :]) 306 fids_dyn[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coord_raw[1, :])
243 - fids_dyn[ic, 2] = -fids_dyn[ic, 2]  
244 else: 307 else:
245 # compute object fiducials in source frame 308 # compute object fiducials in source frame
246 fids_dyn[ic, :] = coords[ic, :] 309 fids_dyn[ic, :] = coords[ic, :]
  310 + fids_dyn[ic, 2] = -fids_dyn[ic, 2]
247 311
248 # compute object fiducials in vtk head frame 312 # compute object fiducials in vtk head frame
249 a, b, g = np.radians(fids_dyn[ic, 3:]) 313 a, b, g = np.radians(fids_dyn[ic, 3:])
250 T_p = tr.translation_matrix(fids_dyn[ic, :3]) 314 T_p = tr.translation_matrix(fids_dyn[ic, :3])
251 R_p = tr.euler_matrix(a, b, g, 'rzyx') 315 R_p = tr.euler_matrix(a, b, g, 'rzyx')
252 - M_p = np.asmatrix(tr.concatenate_matrices(T_p, R_p))  
253 - M_img = np.asmatrix(m_change) * M_p 316 + M_p = tr.concatenate_matrices(T_p, R_p)
  317 + M_img = m_change @ M_p
254 318
255 angles_img = np.degrees(np.asarray(tr.euler_from_matrix(M_img, 'rzyx'))) 319 angles_img = np.degrees(np.asarray(tr.euler_from_matrix(M_img, 'rzyx')))
256 - coord_img = np.asarray(flip_x_m(tr.translation_from_matrix(M_img))) 320 + coord_img = flip_x_m(tr.translation_from_matrix(M_img))
257 321
258 - fids_img[ic, :] = np.hstack((coord_img, angles_img)) 322 + fids_img[ic, :] = np.hstack((coord_img[:3, 0], angles_img))
259 323
260 # compute object base change in vtk head frame 324 # compute object base change in vtk head frame
261 - base_obj_img, q_obj_img, base_inv_obj_img = base_creation(fids_img[:3, :3])  
262 - r_obj_img = np.asmatrix(np.identity(4)) 325 + base_obj_img, _ = base_creation(fids_img[:3, :3])
  326 + r_obj_img = np.identity(4)
263 r_obj_img[:3, :3] = base_obj_img[:3, :3] 327 r_obj_img[:3, :3] = base_obj_img[:3, :3]
264 328
265 # compute initial alignment of probe fixed in the object in reference (or static) frame 329 # compute initial alignment of probe fixed in the object in reference (or static) frame
266 - s0_trans_dyn = np.asmatrix(tr.translation_matrix(fids_dyn[3, :3]))  
267 - s0_rot_dyn = np.asmatrix(tr.euler_matrix(np.radians(fids_dyn[3, 3]), np.radians(fids_dyn[3, 4]),  
268 - np.radians(fids_dyn[3, 5]), 'rzyx'))  
269 - s0_dyn = np.asmatrix(tr.concatenate_matrices(s0_trans_dyn, s0_rot_dyn)) 330 + s0_trans_dyn = tr.translation_matrix(fids_dyn[3, :3])
  331 + s0_rot_dyn = tr.euler_matrix(np.radians(fids_dyn[3, 3]), np.radians(fids_dyn[3, 4]),
  332 + np.radians(fids_dyn[3, 5]), 'rzyx')
  333 + s0_dyn = tr.concatenate_matrices(s0_trans_dyn, s0_rot_dyn)
270 334
271 return t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img 335 return t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img
invesalius/data/coordinates.py
@@ -20,6 +20,7 @@ @@ -20,6 +20,7 @@
20 from math import sin, cos 20 from math import sin, cos
21 import numpy as np 21 import numpy as np
22 22
  23 +import invesalius.data.bases as db
23 import invesalius.data.transformations as tr 24 import invesalius.data.transformations as tr
24 import invesalius.constants as const 25 import invesalius.constants as const
25 26
@@ -74,7 +75,7 @@ def PolarisCoord(trck_init, trck_id, ref_mode): @@ -74,7 +75,7 @@ def PolarisCoord(trck_init, trck_id, ref_mode):
74 coord3 = np.hstack((trans_obj, angles_obj)) 75 coord3 = np.hstack((trans_obj, angles_obj))
75 76
76 coord = np.vstack([coord1, coord2, coord3]) 77 coord = np.vstack([coord1, coord2, coord3])
77 - Publisher.sendMessage('Sensors ID', probe_id=trck.probeID, ref_id=trck.refID, obj_id=trck.objID) 78 + # Publisher.sendMessage('Sensors ID', probe_id=trck.probeID, ref_id=trck.refID, obj_id=trck.objID)
78 79
79 return coord 80 return coord
80 81
@@ -232,19 +233,45 @@ def DebugCoord(trk_init, trck_id, ref_mode): @@ -232,19 +233,45 @@ def DebugCoord(trk_init, trck_id, ref_mode):
232 :return: six coordinates x, y, z, alfa, beta and gama 233 :return: six coordinates x, y, z, alfa, beta and gama
233 """ 234 """
234 235
235 - sleep(0.05) 236 + # Started to take a more reasonable, limited random coordinate generator based on
  237 + # the collected fiducials, but it is more complicated than this. It should account for the
  238 + # dynamic reference computation
  239 + # if trk_init:
  240 + # fiducials = trk_init[3:, :]
  241 + # fids_max = fiducials.max(axis=0)
  242 + # fids_min = fiducials.min(axis=0)
  243 + # fids_lim = np.hstack((fids_min[np.newaxis, :].T, fids_max[np.newaxis, :].T))
  244 + #
  245 + # dx = fids_max[]
  246 + # dt = [-180, 180]
  247 + #
  248 + # else:
236 249
237 - coord1 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),  
238 - uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)]) 250 + dx = [-70, 70]
  251 + dt = [-180, 180]
239 252
240 - coord2 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),  
241 - uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)]) 253 + coord1 = np.array([uniform(*dx), uniform(*dx), uniform(*dx),
  254 + uniform(*dt), uniform(*dt), uniform(*dt)])
  255 + coord2 = np.array([uniform(*dx), uniform(*dx), uniform(*dx),
  256 + uniform(*dt), uniform(*dt), uniform(*dt)])
  257 + coord3 = np.array([uniform(*dx), uniform(*dx), uniform(*dx),
  258 + uniform(*dt), uniform(*dt), uniform(*dt)])
  259 + coord4 = np.array([uniform(*dx), uniform(*dx), uniform(*dx),
  260 + uniform(*dt), uniform(*dt), uniform(*dt)])
242 261
243 - coord3 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),  
244 - uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)]) 262 + sleep(0.05)
245 263
246 - coord4 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),  
247 - uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)]) 264 + # coord1 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
  265 + # uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
  266 + #
  267 + # coord2 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
  268 + # uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
  269 + #
  270 + # coord3 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
  271 + # uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
  272 + #
  273 + # coord4 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
  274 + # uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
248 275
249 Publisher.sendMessage('Sensors ID', probe_id=int(uniform(0, 5)), ref_id=int(uniform(0, 5)), obj_id=int(uniform(0, 5))) 276 Publisher.sendMessage('Sensors ID', probe_id=int(uniform(0, 5)), ref_id=int(uniform(0, 5)), obj_id=int(uniform(0, 5)))
250 277
@@ -297,19 +324,20 @@ def dynamic_reference_m(probe, reference): @@ -297,19 +324,20 @@ def dynamic_reference_m(probe, reference):
297 :param reference: sensor two defined as reference 324 :param reference: sensor two defined as reference
298 :return: rotated and translated coordinates 325 :return: rotated and translated coordinates
299 """ 326 """
300 -  
301 a, b, g = np.radians(reference[3:6]) 327 a, b, g = np.radians(reference[3:6])
302 328
303 - T = tr.translation_matrix(reference[:3])  
304 - R = tr.euler_matrix(a, b, g, 'rzyx')  
305 - M = np.asmatrix(tr.concatenate_matrices(T, R))  
306 - # M = tr.compose_matrix(angles=np.radians(reference[3:6]), translate=reference[:3])  
307 - # print M  
308 - probe_4 = np.vstack((np.asmatrix(probe[:3]).reshape([3, 1]), 1.))  
309 - coord_rot = M.I * probe_4  
310 - coord_rot = np.squeeze(np.asarray(coord_rot)) 329 + trans = tr.translation_matrix(reference[:3])
  330 + rot = tr.euler_matrix(a, b, g, 'rzyx')
  331 + affine = tr.concatenate_matrices(trans, rot)
  332 + probe_4 = np.vstack((probe[:3].reshape([3, 1]), 1.))
  333 + coord_rot = np.linalg.inv(affine) @ probe_4
  334 + # minus sign to the z coordinate
  335 + coord_rot[2, 0] = -coord_rot[2, 0]
  336 + coord_rot = coord_rot[:3, 0].tolist()
  337 + coord_rot.extend(probe[3:])
  338 +
  339 + return coord_rot
311 340
312 - return coord_rot[0], coord_rot[1], -coord_rot[2], probe[3], probe[4], probe[5]  
313 341
314 def dynamic_reference_m2(probe, reference): 342 def dynamic_reference_m2(probe, reference):
315 """ 343 """
@@ -331,70 +359,18 @@ def dynamic_reference_m2(probe, reference): @@ -331,70 +359,18 @@ def dynamic_reference_m2(probe, reference):
331 T_p = tr.translation_matrix(probe[:3]) 359 T_p = tr.translation_matrix(probe[:3])
332 R = tr.euler_matrix(a, b, g, 'rzyx') 360 R = tr.euler_matrix(a, b, g, 'rzyx')
333 R_p = tr.euler_matrix(a_p, b_p, g_p, 'rzyx') 361 R_p = tr.euler_matrix(a_p, b_p, g_p, 'rzyx')
334 - M = np.asmatrix(tr.concatenate_matrices(T, R))  
335 - M_p = np.asmatrix(tr.concatenate_matrices(T_p, R_p))  
336 - # M = tr.compose_matrix(angles=np.radians(reference[3:6]), translate=reference[:3])  
337 - # print M 362 + M = tr.concatenate_matrices(T, R)
  363 + M_p = tr.concatenate_matrices(T_p, R_p)
338 364
339 - M_dyn = M.I * M_p 365 + M_dyn = np.linalg.inv(M) @ M_p
340 366
341 al, be, ga = tr.euler_from_matrix(M_dyn, 'rzyx') 367 al, be, ga = tr.euler_from_matrix(M_dyn, 'rzyx')
342 coord_rot = tr.translation_from_matrix(M_dyn) 368 coord_rot = tr.translation_from_matrix(M_dyn)
343 369
344 coord_rot = np.squeeze(coord_rot) 370 coord_rot = np.squeeze(coord_rot)
345 371
346 - # probe_4 = np.vstack((np.asmatrix(probe[:3]).reshape([3, 1]), 1.))  
347 - # coord_rot_test = M.I * probe_4  
348 - # coord_rot_test = np.squeeze(np.asarray(coord_rot_test))  
349 - #  
350 - # print "coord_rot: ", coord_rot  
351 - # print "coord_rot_test: ", coord_rot_test  
352 - # print "test: ", np.allclose(coord_rot, coord_rot_test[:3])  
353 -  
354 return coord_rot[0], coord_rot[1], coord_rot[2], np.degrees(al), np.degrees(be), np.degrees(ga) 372 return coord_rot[0], coord_rot[1], coord_rot[2], np.degrees(al), np.degrees(be), np.degrees(ga)
355 373
356 -# def dynamic_reference_m3(probe, reference):  
357 -# """  
358 -# Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama  
359 -# rotation angles of reference to rotate the probe coordinate and returns the x, y, z  
360 -# difference between probe and reference. Angles sequences and equation was extracted from  
361 -# Polhemus manual and Attitude matrix in Wikipedia.  
362 -# General equation is:  
363 -# coord = Mrot * (probe - reference)  
364 -# :param probe: sensor one defined as probe  
365 -# :param reference: sensor two defined as reference  
366 -# :return: rotated and translated coordinates  
367 -# """  
368 -#  
369 -# a, b, g = np.radians(reference[3:6])  
370 -# a_p, b_p, g_p = np.radians(probe[3:6])  
371 -#  
372 -# T = tr.translation_matrix(reference[:3])  
373 -# T_p = tr.translation_matrix(probe[:3])  
374 -# R = tr.euler_matrix(a, b, g, 'rzyx')  
375 -# R_p = tr.euler_matrix(a_p, b_p, g_p, 'rzyx')  
376 -# M = np.asmatrix(tr.concatenate_matrices(T, R))  
377 -# M_p = np.asmatrix(tr.concatenate_matrices(T_p, R_p))  
378 -# # M = tr.compose_matrix(angles=np.radians(reference[3:6]), translate=reference[:3])  
379 -# # print M  
380 -#  
381 -# M_dyn = M.I * M_p  
382 -#  
383 -# # al, be, ga = tr.euler_from_matrix(M_dyn, 'rzyx')  
384 -# # coord_rot = tr.translation_from_matrix(M_dyn)  
385 -# #  
386 -# # coord_rot = np.squeeze(coord_rot)  
387 -#  
388 -# # probe_4 = np.vstack((np.asmatrix(probe[:3]).reshape([3, 1]), 1.))  
389 -# # coord_rot_test = M.I * probe_4  
390 -# # coord_rot_test = np.squeeze(np.asarray(coord_rot_test))  
391 -# #  
392 -# # print "coord_rot: ", coord_rot  
393 -# # print "coord_rot_test: ", coord_rot_test  
394 -# # print "test: ", np.allclose(coord_rot, coord_rot_test[:3])  
395 -#  
396 -# return M_dyn  
397 -  
398 374
399 def str2float(data): 375 def str2float(data):
400 """ 376 """
@@ -414,3 +390,15 @@ def str2float(data): @@ -414,3 +390,15 @@ def str2float(data):
414 data = [float(s) for s in data[1:len(data)]] 390 data = [float(s) for s in data[1:len(data)]]
415 391
416 return data 392 return data
  393 +
  394 +
  395 +def offset_coordinate(p_old, norm_vec, offset):
  396 + """
  397 + Translate the coordinates of a point along a vector
  398 + :param p_old: (x, y, z) array with current point coordinates
  399 + :param norm_vec: (vx, vy, vz) array with normal vector coordinates
  400 + :param offset: double representing the magnitude of offset
  401 + :return: (x_new, y_new, z_new) array of offset coordinates
  402 + """
  403 + p_offset = p_old - offset * norm_vec
  404 + return p_offset
invesalius/data/coregistration.py
@@ -17,347 +17,620 @@ @@ -17,347 +17,620 @@
17 # detalhes. 17 # detalhes.
18 #-------------------------------------------------------------------------- 18 #--------------------------------------------------------------------------
19 19
  20 +import numpy as np
  21 +import queue
20 import threading 22 import threading
21 from time import sleep 23 from time import sleep
22 24
23 -from numpy import asmatrix, mat, degrees, radians, identity  
24 -import wx  
25 -from pubsub import pub as Publisher  
26 -  
27 import invesalius.data.coordinates as dco 25 import invesalius.data.coordinates as dco
28 import invesalius.data.transformations as tr 26 import invesalius.data.transformations as tr
29 27
30 -# TODO: Optimize navigation thread. Remove the infinite loop and optimize sleep.  
31 -  
32 28
33 -class CoregistrationStatic(threading.Thread):  
34 - """  
35 - Thread to update the coordinates with the fiducial points  
36 - co-registration method while the Navigation Button is pressed.  
37 - Sleep function in run method is used to avoid blocking GUI and  
38 - for better real-time navigation 29 +# TODO: Replace the use of degrees by radians in every part of the navigation pipeline
  30 +
  31 +def object_marker_to_center(coord_raw, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw):
  32 + """Translate and rotate the raw coordinate given by the tracking device to the reference system created during
  33 + the object registration.
  34 +
  35 + :param coord_raw: Coordinates returned by the tracking device
  36 + :type coord_raw: numpy.ndarray
  37 + :param obj_ref_mode:
  38 + :type obj_ref_mode: int
  39 + :param t_obj_raw:
  40 + :type t_obj_raw: numpy.ndarray
  41 + :param s0_raw:
  42 + :type s0_raw: numpy.ndarray
  43 + :param r_s0_raw: rotation transformation from marker to object basis
  44 + :type r_s0_raw: numpy.ndarray
  45 + :return: 4 x 4 numpy double array
  46 + :rtype: numpy.ndarray
39 """ 47 """
40 48
41 - def __init__(self, coreg_data, nav_id, trck_info):  
42 - threading.Thread.__init__(self)  
43 - self.coreg_data = coreg_data  
44 - self.nav_id = nav_id  
45 - self.trck_info = trck_info  
46 - self._pause_ = False  
47 - self.start()  
48 -  
49 - def stop(self):  
50 - self._pause_ = True  
51 -  
52 - def run(self):  
53 - # m_change = self.coreg_data[0]  
54 - # obj_ref_mode = self.coreg_data[2]  
55 - #  
56 - # trck_init = self.trck_info[0]  
57 - # trck_id = self.trck_info[1]  
58 - # trck_mode = self.trck_info[2]  
59 -  
60 - m_change, obj_ref_mode = self.coreg_data  
61 - trck_init, trck_id, trck_mode = self.trck_info  
62 -  
63 - while self.nav_id:  
64 - coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)  
65 -  
66 - psi, theta, phi = coord_raw[obj_ref_mode, 3:]  
67 - t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3])) 49 + as1, bs1, gs1 = np.radians(coord_raw[obj_ref_mode, 3:])
  50 + r_probe = tr.euler_matrix(as1, bs1, gs1, 'rzyx')
  51 + t_probe_raw = tr.translation_matrix(coord_raw[obj_ref_mode, :3])
  52 + t_offset_aux = np.linalg.inv(r_s0_raw) @ r_probe @ t_obj_raw
  53 + t_offset = np.identity(4)
  54 + t_offset[:, -1] = t_offset_aux[:, -1]
  55 + t_probe = s0_raw @ t_offset @ np.linalg.inv(s0_raw) @ t_probe_raw
  56 + m_probe = tr.concatenate_matrices(t_probe, r_probe)
68 57
69 - t_probe_raw[2, -1] = -t_probe_raw[2, -1] 58 + return m_probe
70 59
71 - m_img = m_change * t_probe_raw  
72 60
73 - coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], psi, theta, phi 61 +def object_to_reference(coord_raw, m_probe):
  62 + """Compute affine transformation matrix to the reference basis
74 63
75 - wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)  
76 -  
77 - # TODO: Optimize the value of sleep for each tracking device.  
78 - sleep(0.175)  
79 -  
80 - if self._pause_:  
81 - return  
82 -  
83 -  
84 -class CoregistrationDynamic(threading.Thread):  
85 - """  
86 - Thread to update the coordinates with the fiducial points  
87 - co-registration method while the Navigation Button is pressed.  
88 - Sleep function in run method is used to avoid blocking GUI and  
89 - for better real-time navigation 64 + :param coord_raw: Coordinates returned by the tracking device
  65 + :type coord_raw: numpy.ndarray
  66 + :param m_probe: Probe coordinates
  67 + :type m_probe: numpy.ndarray
  68 + :return: 4 x 4 numpy double array
  69 + :rtype: numpy.ndarray
90 """ 70 """
91 71
92 - def __init__(self, coreg_data, nav_id, trck_info):  
93 - threading.Thread.__init__(self)  
94 - self.coreg_data = coreg_data  
95 - self.nav_id = nav_id  
96 - self.trck_info = trck_info  
97 - self._pause_ = False  
98 - self.start()  
99 -  
100 - def stop(self):  
101 - self._pause_ = True  
102 -  
103 - def run(self):  
104 - m_change, obj_ref_mode = self.coreg_data  
105 - trck_init, trck_id, trck_mode = self.trck_info  
106 -  
107 - while self.nav_id:  
108 - coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)  
109 -  
110 - psi, theta, phi = radians(coord_raw[obj_ref_mode, 3:])  
111 - r_probe = tr.euler_matrix(psi, theta, phi, 'rzyx')  
112 - t_probe = tr.translation_matrix(coord_raw[obj_ref_mode, :3])  
113 - m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))  
114 -  
115 - psi_ref, theta_ref, phi_ref = radians(coord_raw[1, 3:])  
116 - r_ref = tr.euler_matrix(psi_ref, theta_ref, phi_ref, 'rzyx')  
117 - t_ref = tr.translation_matrix(coord_raw[1, :3])  
118 - m_ref = asmatrix(tr.concatenate_matrices(t_ref, r_ref))  
119 -  
120 - m_dyn = m_ref.I * m_probe  
121 - m_dyn[2, -1] = -m_dyn[2, -1]  
122 -  
123 - m_img = m_change * m_dyn  
124 -  
125 - scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)  
126 -  
127 - coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \  
128 - degrees(angles[0]), degrees(angles[1]), degrees(angles[2])  
129 -  
130 - wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)  
131 -  
132 - # TODO: Optimize the value of sleep for each tracking device.  
133 - sleep(0.175)  
134 -  
135 - if self._pause_:  
136 - return  
137 -  
138 -  
139 -class CoregistrationDynamic_old(threading.Thread):  
140 - """  
141 - Thread to update the coordinates with the fiducial points  
142 - co-registration method while the Navigation Button is pressed.  
143 - Sleep function in run method is used to avoid blocking GUI and  
144 - for better real-time navigation 72 + a, b, g = np.radians(coord_raw[1, 3:])
  73 + r_ref = tr.euler_matrix(a, b, g, 'rzyx')
  74 + t_ref = tr.translation_matrix(coord_raw[1, :3])
  75 + m_ref = tr.concatenate_matrices(t_ref, r_ref)
  76 +
  77 + m_dyn = np.linalg.inv(m_ref) @ m_probe
  78 + return m_dyn
  79 +
  80 +
  81 +def tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn):
  82 + """Compute affine transformation matrix to the reference basis
  83 +
  84 + :param m_change: Corregistration transformation obtained from fiducials
  85 + :type m_change: numpy.ndarray
  86 + :param m_probe_ref: Object or probe in reference coordinate system
  87 + :type m_probe_ref: numpy.ndarray
  88 + :param r_obj_img: Object coordinate system in image space (3d model)
  89 + :type r_obj_img: numpy.ndarray
  90 + :param m_obj_raw: Object basis in raw coordinates from tacker
  91 + :type m_obj_raw: numpy.ndarray
  92 + :param s0_dyn:
  93 + :type s0_dyn: numpy.ndarray
  94 + :return: 4 x 4 numpy double array
  95 + :rtype: numpy.ndarray
145 """ 96 """
146 97
147 - def __init__(self, bases, nav_id, trck_info):  
148 - threading.Thread.__init__(self)  
149 - self.bases = bases  
150 - self.nav_id = nav_id 98 + m_img = m_change @ m_probe_ref
  99 + r_obj = r_obj_img @ np.linalg.inv(m_obj_raw) @ np.linalg.inv(s0_dyn) @ m_probe_ref @ m_obj_raw
  100 + m_img[:3, :3] = r_obj[:3, :3]
  101 + return m_img
  102 +
  103 +
  104 +def corregistrate_object_dynamic(inp, coord_raw, ref_mode_id):
  105 +
  106 + m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = inp
  107 +
  108 + # transform raw marker coordinate to object center
  109 + m_probe = object_marker_to_center(coord_raw, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw)
  110 + # transform object center to reference marker if specified as dynamic reference
  111 + if ref_mode_id:
  112 + m_probe_ref = object_to_reference(coord_raw, m_probe)
  113 + else:
  114 + m_probe_ref = m_probe
  115 + # invert y coordinate
  116 + m_probe_ref[2, -1] = -m_probe_ref[2, -1]
  117 + # corregistrate from tracker to image space
  118 + m_img = tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn)
  119 + # compute rotation angles
  120 + _, _, angles, _, _ = tr.decompose_matrix(m_img)
  121 + # create output coordiante list
  122 + coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
  123 + np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2])
  124 +
  125 + return coord, m_img
  126 +
  127 +
  128 +def compute_marker_transformation(coord_raw, obj_ref_mode):
  129 + psi, theta, phi = np.radians(coord_raw[obj_ref_mode, 3:])
  130 + r_probe = tr.euler_matrix(psi, theta, phi, 'rzyx')
  131 + t_probe = tr.translation_matrix(coord_raw[obj_ref_mode, :3])
  132 + m_probe = tr.concatenate_matrices(t_probe, r_probe)
  133 + return m_probe
  134 +
  135 +
  136 +def corregistrate_dynamic(inp, coord_raw, ref_mode_id):
  137 +
  138 + m_change, obj_ref_mode = inp
  139 +
  140 + # transform raw marker coordinate to object center
  141 + m_probe = compute_marker_transformation(coord_raw, obj_ref_mode)
  142 + # transform object center to reference marker if specified as dynamic reference
  143 + if ref_mode_id:
  144 + m_ref = compute_marker_transformation(coord_raw, 1)
  145 + m_probe_ref = np.linalg.inv(m_ref) @ m_probe
  146 + else:
  147 + m_probe_ref = m_probe
  148 +
  149 + # invert y coordinate
  150 + m_probe_ref[2, -1] = -m_probe_ref[2, -1]
  151 + # corregistrate from tracker to image space
  152 + m_img = m_change @ m_probe_ref
  153 + # compute rotation angles
  154 + _, _, angles, _, _ = tr.decompose_matrix(m_img)
  155 + # create output coordiante list
  156 + coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\
  157 + np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2])
  158 +
  159 + return coord, m_img
  160 +
  161 +
  162 +class CoordinateCorregistrate(threading.Thread):
  163 + def __init__(self, ref_mode_id, trck_info, coreg_data, coord_queue, view_tracts, coord_tracts_queue, event, sle):
  164 + threading.Thread.__init__(self, name='CoordCoregObject')
  165 + self.ref_mode_id = ref_mode_id
151 self.trck_info = trck_info 166 self.trck_info = trck_info
152 - self._pause_ = False  
153 - self.start()  
154 -  
155 - def stop(self):  
156 - self._pause_ = True  
157 -  
158 - def run(self):  
159 - m_inv = self.bases[0]  
160 - n = self.bases[1]  
161 - q1 = self.bases[2]  
162 - q2 = self.bases[3]  
163 - trck_init = self.trck_info[0]  
164 - trck_id = self.trck_info[1]  
165 - trck_mode = self.trck_info[2]  
166 -  
167 - while self.nav_id:  
168 - # trck_coord, probe, reference = dco.GetCoordinates(trck_init, trck_id, trck_mode)  
169 - coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)  
170 -  
171 - trck_coord = dco.dynamic_reference(coord_raw[0, :], coord_raw[1, :])  
172 -  
173 - trck_xyz = mat([[trck_coord[0]], [trck_coord[1]], [trck_coord[2]]])  
174 - img = q1 + (m_inv * n) * (trck_xyz - q2)  
175 -  
176 - coord = (float(img[0]), float(img[1]), float(img[2]), trck_coord[3],  
177 - trck_coord[4], trck_coord[5])  
178 - angles = coord_raw[0, 3:6]  
179 -  
180 - # Tried several combinations and different locations to send the messages,  
181 - # however only this one does not block the GUI during navigation.  
182 - wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=None, position=coord)  
183 - wx.CallAfter(Publisher.sendMessage, 'Set camera in volume', coord)  
184 - wx.CallAfter(Publisher.sendMessage, 'Update tracker angles', angles)  
185 -  
186 - # TODO: Optimize the value of sleep for each tracking device.  
187 - # Debug tracker is not working with 0.175 so changed to 0.2  
188 - # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.  
189 - # sleep(.3)  
190 - sleep(0.175)  
191 -  
192 - if self._pause_:  
193 - return  
194 -  
195 -  
196 -class CoregistrationObjectStatic(threading.Thread):  
197 - """  
198 - Thread to update the coordinates with the fiducial points  
199 - co-registration method while the Navigation Button is pressed.  
200 - Sleep function in run method is used to avoid blocking GUI and  
201 - for better real-time navigation  
202 - """  
203 -  
204 - def __init__(self, coreg_data, nav_id, trck_info):  
205 - threading.Thread.__init__(self)  
206 self.coreg_data = coreg_data 167 self.coreg_data = coreg_data
207 - self.nav_id = nav_id  
208 - self.trck_info = trck_info  
209 - self._pause_ = False  
210 - self.start()  
211 -  
212 - def stop(self):  
213 - self._pause_ = True 168 + self.coord_queue = coord_queue
  169 + self.view_tracts = view_tracts
  170 + self.coord_tracts_queue = coord_tracts_queue
  171 + self.event = event
  172 + self.sle = sle
214 173
215 def run(self): 174 def run(self):
216 - # m_change = self.coreg_data[0]  
217 - # t_obj_raw = self.coreg_data[1]  
218 - # s0_raw = self.coreg_data[2]  
219 - # r_s0_raw = self.coreg_data[3]  
220 - # s0_dyn = self.coreg_data[4]  
221 - # m_obj_raw = self.coreg_data[5]  
222 - # r_obj_img = self.coreg_data[6]  
223 - # obj_ref_mode = self.coreg_data[7]  
224 - #  
225 - # trck_init = self.trck_info[0]  
226 - # trck_id = self.trck_info[1]  
227 - # trck_mode = self.trck_info[2]  
228 -  
229 - m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = self.coreg_data  
230 - trck_init, trck_id, trck_mode = self.trck_info  
231 -  
232 - while self.nav_id:  
233 - coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)  
234 -  
235 - as1, bs1, gs1 = radians(coord_raw[obj_ref_mode, 3:])  
236 - r_probe = asmatrix(tr.euler_matrix(as1, bs1, gs1, 'rzyx'))  
237 - t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))  
238 - t_offset_aux = r_s0_raw.I * r_probe * t_obj_raw  
239 - t_offset = asmatrix(identity(4))  
240 - t_offset[:, -1] = t_offset_aux[:, -1]  
241 - t_probe = s0_raw * t_offset * s0_raw.I * t_probe_raw  
242 - m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))  
243 -  
244 - m_probe[2, -1] = -m_probe[2, -1]  
245 -  
246 - m_img = m_change * m_probe  
247 - r_obj = r_obj_img * m_obj_raw.I * s0_dyn.I * m_probe * m_obj_raw  
248 -  
249 - m_img[:3, :3] = r_obj[:3, :3]  
250 -  
251 - scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)  
252 -  
253 - coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \  
254 - degrees(angles[0]), degrees(angles[1]), degrees(angles[2])  
255 -  
256 - wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)  
257 - wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)  
258 -  
259 - # TODO: Optimize the value of sleep for each tracking device.  
260 - sleep(0.175)  
261 -  
262 - # Debug tracker is not working with 0.175 so changed to 0.2  
263 - # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.  
264 - # sleep(.3)  
265 -  
266 - # partially working for translate and offset,  
267 - # but offset is kept always in same axis, have to fix for rotation  
268 - # M_dyn = M_reference.I * T_stylus  
269 - # M_dyn[2, -1] = -M_dyn[2, -1]  
270 - # M_dyn_ch = M_change * M_dyn  
271 - # ddd = M_dyn_ch[0, -1], M_dyn_ch[1, -1], M_dyn_ch[2, -1]  
272 - # M_dyn_ch[:3, -1] = asmatrix(db.flip_x_m(ddd)).reshape([3, 1])  
273 - # M_final = S0 * M_obj_trans_0 * S0.I * M_dyn_ch  
274 -  
275 - # this works for static reference object rotation  
276 - # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_raw.I * R_stylus * M_obj_rot_raw  
277 - # this works for dynamic reference in rotation but not in translation  
278 - # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_dyn.I * R_reference.I * R_stylus * M_obj_rot_raw  
279 -  
280 - if self._pause_:  
281 - return  
282 -  
283 -  
284 -class CoregistrationObjectDynamic(threading.Thread):  
285 - """  
286 - Thread to update the coordinates with the fiducial points  
287 - co-registration method while the Navigation Button is pressed.  
288 - Sleep function in run method is used to avoid blocking GUI and  
289 - for better real-time navigation  
290 - """  
291 -  
292 - def __init__(self, coreg_data, nav_id, trck_info):  
293 - threading.Thread.__init__(self)  
294 - self.coreg_data = coreg_data  
295 - self.nav_id = nav_id 175 + trck_info = self.trck_info
  176 + coreg_data = self.coreg_data
  177 + view_obj = 1
  178 +
  179 + trck_init, trck_id, trck_mode = trck_info
  180 + # print('CoordCoreg: event {}'.format(self.event.is_set()))
  181 + while not self.event.is_set():
  182 + try:
  183 + # print(f"Set the coordinate")
  184 + coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  185 + coord, m_img = corregistrate_object_dynamic(coreg_data, coord_raw, self.ref_mode_id)
  186 + m_img_flip = m_img.copy()
  187 + m_img_flip[1, -1] = -m_img_flip[1, -1]
  188 + # self.pipeline.set_message(m_img_flip)
  189 + self.coord_queue.put_nowait([coord, m_img, view_obj])
  190 + # print('CoordCoreg: put {}'.format(count))
  191 + # count += 1
  192 +
  193 + if self.view_tracts:
  194 + self.coord_tracts_queue.put_nowait(m_img_flip)
  195 +
  196 + # The sleep has to be in both threads
  197 + sleep(self.sle)
  198 + except queue.Full:
  199 + pass
  200 +
  201 +
  202 +class CoordinateCorregistrateNoObject(threading.Thread):
  203 + def __init__(self, ref_mode_id, trck_info, coreg_data, coord_queue, view_tracts, coord_tracts_queue, event, sle):
  204 + threading.Thread.__init__(self, name='CoordCoregNoObject')
  205 + self.ref_mode_id = ref_mode_id
296 self.trck_info = trck_info 206 self.trck_info = trck_info
297 - self._pause_ = False  
298 - self.start()  
299 -  
300 - def stop(self):  
301 - self._pause_ = True 207 + self.coreg_data = coreg_data
  208 + self.coord_queue = coord_queue
  209 + self.view_tracts = view_tracts
  210 + self.coord_tracts_queue = coord_tracts_queue
  211 + self.event = event
  212 + self.sle = sle
302 213
303 def run(self): 214 def run(self):
  215 + trck_info = self.trck_info
  216 + coreg_data = self.coreg_data
  217 + view_obj = 0
  218 +
  219 + trck_init, trck_id, trck_mode = trck_info
  220 + # print('CoordCoreg: event {}'.format(self.event.is_set()))
  221 + while not self.event.is_set():
  222 + try:
  223 + # print(f"Set the coordinate")
  224 + coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  225 + coord, m_img = corregistrate_dynamic(coreg_data, coord_raw, self.ref_mode_id)
  226 + # print("Coord: ", coord)
  227 + m_img_flip = m_img.copy()
  228 + m_img_flip[1, -1] = -m_img_flip[1, -1]
  229 + self.coord_queue.put_nowait([coord, m_img, view_obj])
  230 +
  231 + if self.view_tracts:
  232 + self.coord_tracts_queue.put_nowait(m_img_flip)
  233 +
  234 + # The sleep has to be in both threads
  235 + sleep(self.sle)
  236 + except queue.Full:
  237 + pass
  238 +
  239 +
  240 +# class CoregistrationStatic(threading.Thread):
  241 +# """
  242 +# Thread to update the coordinates with the fiducial points
  243 +# co-registration method while the Navigation Button is pressed.
  244 +# Sleep function in run method is used to avoid blocking GUI and
  245 +# for better real-time navigation
  246 +# """
  247 +#
  248 +# def __init__(self, coreg_data, nav_id, trck_info):
  249 +# threading.Thread.__init__(self)
  250 +# self.coreg_data = coreg_data
  251 +# self.nav_id = nav_id
  252 +# self.trck_info = trck_info
  253 +# self._pause_ = False
  254 +# self.start()
  255 +#
  256 +# def stop(self):
  257 +# self._pause_ = True
  258 +#
  259 +# def run(self):
  260 +# # m_change = self.coreg_data[0]
  261 +# # obj_ref_mode = self.coreg_data[2]
  262 +# #
  263 +# # trck_init = self.trck_info[0]
  264 +# # trck_id = self.trck_info[1]
  265 +# # trck_mode = self.trck_info[2]
  266 +#
  267 +# m_change, obj_ref_mode = self.coreg_data
  268 +# trck_init, trck_id, trck_mode = self.trck_info
  269 +#
  270 +# while self.nav_id:
  271 +# coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  272 +#
  273 +# psi, theta, phi = coord_raw[obj_ref_mode, 3:]
  274 +# t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))
  275 +#
  276 +# t_probe_raw[2, -1] = -t_probe_raw[2, -1]
  277 +#
  278 +# m_img = m_change * t_probe_raw
  279 +#
  280 +# coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], psi, theta, phi
  281 +#
  282 +# wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)
  283 +#
  284 +# # TODO: Optimize the value of sleep for each tracking device.
  285 +# sleep(0.175)
  286 +#
  287 +# if self._pause_:
  288 +# return
  289 +#
  290 +#
  291 +# class CoregistrationDynamic(threading.Thread):
  292 +# """
  293 +# Thread to update the coordinates with the fiducial points
  294 +# co-registration method while the Navigation Button is pressed.
  295 +# Sleep function in run method is used to avoid blocking GUI and
  296 +# for better real-time navigation
  297 +# """
  298 +#
  299 +# def __init__(self, coreg_data, nav_id, trck_info):
  300 +# threading.Thread.__init__(self)
  301 +# self.coreg_data = coreg_data
  302 +# self.nav_id = nav_id
  303 +# self.trck_info = trck_info
  304 +# # self.tracts_info = tracts_info
  305 +# # self.tracts = None
  306 +# self._pause_ = False
  307 +# # self.start()
  308 +#
  309 +# def stop(self):
  310 +# # self.tracts.stop()
  311 +# self._pause_ = True
  312 +#
  313 +# def run(self):
  314 +# m_change, obj_ref_mode = self.coreg_data
  315 +# trck_init, trck_id, trck_mode = self.trck_info
  316 +# # seed, tracker, affine, affine_vtk = self.tracts_info
  317 +#
  318 +# while self.nav_id:
  319 +# coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  320 +#
  321 +# psi, theta, phi = radians(coord_raw[obj_ref_mode, 3:])
  322 +# r_probe = tr.euler_matrix(psi, theta, phi, 'rzyx')
  323 +# t_probe = tr.translation_matrix(coord_raw[obj_ref_mode, :3])
  324 +# m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))
  325 +#
  326 +# psi_ref, theta_ref, phi_ref = radians(coord_raw[1, 3:])
  327 +# r_ref = tr.euler_matrix(psi_ref, theta_ref, phi_ref, 'rzyx')
  328 +# t_ref = tr.translation_matrix(coord_raw[1, :3])
  329 +# m_ref = asmatrix(tr.concatenate_matrices(t_ref, r_ref))
  330 +#
  331 +# m_dyn = m_ref.I * m_probe
  332 +# m_dyn[2, -1] = -m_dyn[2, -1]
  333 +#
  334 +# m_img = m_change * m_dyn
  335 +#
  336 +# scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)
  337 +#
  338 +# coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
  339 +# degrees(angles[0]), degrees(angles[1]), degrees(angles[2])
  340 +#
  341 +# # pos_world_aux = np.ones([4, 1])
  342 +# # pos_world_aux[:3, -1] = db.flip_x((m_img[0, -1], m_img[1, -1], m_img[2, -1]))[:3]
  343 +# # pos_world = np.linalg.inv(affine) @ pos_world_aux
  344 +# # seed_aux = pos_world.reshape([1, 4])[0, :3]
  345 +# # seed = seed_aux[np.newaxis, :]
  346 +# #
  347 +# # self.tracts = dtr.compute_tracts(tracker, seed, affine_vtk, True)
  348 +#
  349 +# # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)
  350 +# wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord)
  351 +#
  352 +# # TODO: Optimize the value of sleep for each tracking device.
  353 +# sleep(3.175)
  354 +#
  355 +# if self._pause_:
  356 +# return
  357 +#
  358 +#
  359 +# class CoregistrationDynamic_old(threading.Thread):
  360 +# """
  361 +# Thread to update the coordinates with the fiducial points
  362 +# co-registration method while the Navigation Button is pressed.
  363 +# Sleep function in run method is used to avoid blocking GUI and
  364 +# for better real-time navigation
  365 +# """
  366 +#
  367 +# def __init__(self, bases, nav_id, trck_info):
  368 +# threading.Thread.__init__(self)
  369 +# self.bases = bases
  370 +# self.nav_id = nav_id
  371 +# self.trck_info = trck_info
  372 +# self._pause_ = False
  373 +# self.start()
  374 +#
  375 +# def stop(self):
  376 +# self._pause_ = True
  377 +#
  378 +# def run(self):
  379 +# m_inv = self.bases[0]
  380 +# n = self.bases[1]
  381 +# q1 = self.bases[2]
  382 +# q2 = self.bases[3]
  383 +# trck_init = self.trck_info[0]
  384 +# trck_id = self.trck_info[1]
  385 +# trck_mode = self.trck_info[2]
  386 +#
  387 +# while self.nav_id:
  388 +# # trck_coord, probe, reference = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  389 +# coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  390 +#
  391 +# trck_coord = dco.dynamic_reference(coord_raw[0, :], coord_raw[1, :])
  392 +#
  393 +# trck_xyz = mat([[trck_coord[0]], [trck_coord[1]], [trck_coord[2]]])
  394 +# img = q1 + (m_inv * n) * (trck_xyz - q2)
  395 +#
  396 +# coord = (float(img[0]), float(img[1]), float(img[2]), trck_coord[3],
  397 +# trck_coord[4], trck_coord[5])
  398 +# angles = coord_raw[0, 3:6]
  399 +#
  400 +# # Tried several combinations and different locations to send the messages,
  401 +# # however only this one does not block the GUI during navigation.
  402 +# wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=None, position=coord)
  403 +# wx.CallAfter(Publisher.sendMessage, 'Set camera in volume', coord)
  404 +# wx.CallAfter(Publisher.sendMessage, 'Update tracker angles', angles)
  405 +#
  406 +# # TODO: Optimize the value of sleep for each tracking device.
  407 +# # Debug tracker is not working with 0.175 so changed to 0.2
  408 +# # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.
  409 +# # sleep(.3)
  410 +# sleep(0.175)
  411 +#
  412 +# if self._pause_:
  413 +# return
  414 +#
  415 +#
  416 +# class CoregistrationObjectStatic(threading.Thread):
  417 +# """
  418 +# Thread to update the coordinates with the fiducial points
  419 +# co-registration method while the Navigation Button is pressed.
  420 +# Sleep function in run method is used to avoid blocking GUI and
  421 +# for better real-time navigation
  422 +# """
  423 +#
  424 +# def __init__(self, coreg_data, nav_id, trck_info):
  425 +# threading.Thread.__init__(self)
  426 +# self.coreg_data = coreg_data
  427 +# self.nav_id = nav_id
  428 +# self.trck_info = trck_info
  429 +# self._pause_ = False
  430 +# self.start()
  431 +#
  432 +# def stop(self):
  433 +# self._pause_ = True
  434 +#
  435 +# def run(self):
  436 +# # m_change = self.coreg_data[0]
  437 +# # t_obj_raw = self.coreg_data[1]
  438 +# # s0_raw = self.coreg_data[2]
  439 +# # r_s0_raw = self.coreg_data[3]
  440 +# # s0_dyn = self.coreg_data[4]
  441 +# # m_obj_raw = self.coreg_data[5]
  442 +# # r_obj_img = self.coreg_data[6]
  443 +# # obj_ref_mode = self.coreg_data[7]
  444 +# #
  445 +# # trck_init = self.trck_info[0]
  446 +# # trck_id = self.trck_info[1]
  447 +# # trck_mode = self.trck_info[2]
  448 +#
  449 +# m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = self.coreg_data
  450 +# trck_init, trck_id, trck_mode = self.trck_info
  451 +#
  452 +# while self.nav_id:
  453 +# coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  454 +#
  455 +# as1, bs1, gs1 = radians(coord_raw[obj_ref_mode, 3:])
  456 +# r_probe = asmatrix(tr.euler_matrix(as1, bs1, gs1, 'rzyx'))
  457 +# t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))
  458 +# t_offset_aux = r_s0_raw.I * r_probe * t_obj_raw
  459 +# t_offset = asmatrix(identity(4))
  460 +# t_offset[:, -1] = t_offset_aux[:, -1]
  461 +# t_probe = s0_raw * t_offset * s0_raw.I * t_probe_raw
  462 +# m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))
  463 +#
  464 +# m_probe[2, -1] = -m_probe[2, -1]
  465 +#
  466 +# m_img = m_change * m_probe
  467 +# r_obj = r_obj_img * m_obj_raw.I * s0_dyn.I * m_probe * m_obj_raw
  468 +#
  469 +# m_img[:3, :3] = r_obj[:3, :3]
  470 +#
  471 +# scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)
  472 +#
  473 +# coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
  474 +# degrees(angles[0]), degrees(angles[1]), degrees(angles[2])
  475 +#
  476 +# wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)
  477 +# wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)
  478 +#
  479 +# # TODO: Optimize the value of sleep for each tracking device.
  480 +# sleep(0.175)
  481 +#
  482 +# # Debug tracker is not working with 0.175 so changed to 0.2
  483 +# # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.
  484 +# # sleep(.3)
  485 +#
  486 +# # partially working for translate and offset,
  487 +# # but offset is kept always in same axis, have to fix for rotation
  488 +# # M_dyn = M_reference.I * T_stylus
  489 +# # M_dyn[2, -1] = -M_dyn[2, -1]
  490 +# # M_dyn_ch = M_change * M_dyn
  491 +# # ddd = M_dyn_ch[0, -1], M_dyn_ch[1, -1], M_dyn_ch[2, -1]
  492 +# # M_dyn_ch[:3, -1] = asmatrix(db.flip_x_m(ddd)).reshape([3, 1])
  493 +# # M_final = S0 * M_obj_trans_0 * S0.I * M_dyn_ch
  494 +#
  495 +# # this works for static reference object rotation
  496 +# # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_raw.I * R_stylus * M_obj_rot_raw
  497 +# # this works for dynamic reference in rotation but not in translation
  498 +# # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_dyn.I * R_reference.I * R_stylus * M_obj_rot_raw
  499 +#
  500 +# if self._pause_:
  501 +# return
  502 +#
  503 +#
  504 +# class CoregistrationObjectDynamic(threading.Thread):
  505 +# """
  506 +# Thread to update the coordinates with the fiducial points
  507 +# co-registration method while the Navigation Button is pressed.
  508 +# Sleep function in run method is used to avoid blocking GUI and
  509 +# for better real-time navigation
  510 +# """
  511 +#
  512 +# def __init__(self, coreg_data, nav_id, trck_info, tracts_info):
  513 +# threading.Thread.__init__(self)
  514 +# self.coreg_data = coreg_data
  515 +# self.nav_id = nav_id
  516 +# self.trck_info = trck_info
  517 +# # self.tracts_info = tracts_info
  518 +# # self.tracts = None
  519 +# self._pause_ = False
  520 +# self.start()
  521 +#
  522 +# def stop(self):
  523 +# # self.tracts.stop()
  524 +# self._pause_ = True
  525 +#
  526 +# def run(self):
  527 +#
  528 +# m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = self.coreg_data
  529 +# trck_init, trck_id, trck_mode = self.trck_info
  530 +# # seed, tracker, affine, affine_vtk = self.tracts_info
  531 +#
  532 +# while self.nav_id:
  533 +# coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  534 +#
  535 +# as1, bs1, gs1 = radians(coord_raw[obj_ref_mode, 3:])
  536 +# r_probe = asmatrix(tr.euler_matrix(as1, bs1, gs1, 'rzyx'))
  537 +# t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))
  538 +# t_offset_aux = r_s0_raw.I * r_probe * t_obj_raw
  539 +# t_offset = asmatrix(identity(4))
  540 +# t_offset[:, -1] = t_offset_aux[:, -1]
  541 +# t_probe = s0_raw * t_offset * s0_raw.I * t_probe_raw
  542 +# m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))
  543 +#
  544 +# a, b, g = radians(coord_raw[1, 3:])
  545 +# r_ref = tr.euler_matrix(a, b, g, 'rzyx')
  546 +# t_ref = tr.translation_matrix(coord_raw[1, :3])
  547 +# m_ref = asmatrix(tr.concatenate_matrices(t_ref, r_ref))
  548 +#
  549 +# m_dyn = m_ref.I * m_probe
  550 +# m_dyn[2, -1] = -m_dyn[2, -1]
  551 +#
  552 +# m_img = m_change * m_dyn
  553 +# r_obj = r_obj_img * m_obj_raw.I * s0_dyn.I * m_dyn * m_obj_raw
  554 +#
  555 +# m_img[:3, :3] = r_obj[:3, :3]
  556 +#
  557 +# scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)
  558 +#
  559 +# coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\
  560 +# degrees(angles[0]), degrees(angles[1]), degrees(angles[2])
  561 +#
  562 +# # norm_vec = m_img[:3, 2].reshape([1, 3]).tolist()
  563 +# # p0 = m_img[:3, -1].reshape([1, 3]).tolist()
  564 +# # p2 = [x - 30 * y for x, y in zip(p0[0], norm_vec[0])]
  565 +# # m_tract = m_img.copy()
  566 +# # m_tract[:3, -1] = np.reshape(np.asarray(p2)[np.newaxis, :], [3, 1])
  567 +#
  568 +# # pos_world_aux = np.ones([4, 1])
  569 +# # pos_world_aux[:3, -1] = db.flip_x((p2[0], p2[1], p2[2]))[:3]
  570 +# # pos_world = np.linalg.inv(affine) @ pos_world_aux
  571 +# # seed_aux = pos_world.reshape([1, 4])[0, :3]
  572 +# # seed = seed_aux[np.newaxis, :]
  573 +#
  574 +# # self.tracts = dtr.compute_tracts(tracker, seed, affine_vtk, True)
  575 +#
  576 +# # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)
  577 +# wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord)
  578 +# wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)
  579 +#
  580 +# # TODO: Optimize the value of sleep for each tracking device.
  581 +# #sleep(2.175)
  582 +# sleep(0.175)
  583 +#
  584 +# # Debug tracker is not working with 0.175 so changed to 0.2
  585 +# # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.
  586 +# # sleep(.3)
  587 +#
  588 +# # partially working for translate and offset,
  589 +# # but offset is kept always in same axis, have to fix for rotation
  590 +# # M_dyn = M_reference.I * T_stylus
  591 +# # M_dyn[2, -1] = -M_dyn[2, -1]
  592 +# # M_dyn_ch = M_change * M_dyn
  593 +# # ddd = M_dyn_ch[0, -1], M_dyn_ch[1, -1], M_dyn_ch[2, -1]
  594 +# # M_dyn_ch[:3, -1] = asmatrix(db.flip_x_m(ddd)).reshape([3, 1])
  595 +# # M_final = S0 * M_obj_trans_0 * S0.I * M_dyn_ch
  596 +#
  597 +# # this works for static reference object rotation
  598 +# # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_raw.I * R_stylus * M_obj_rot_raw
  599 +# # this works for dynamic reference in rotation but not in translation
  600 +# # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_dyn.I * R_reference.I * R_stylus * M_obj_rot_raw
  601 +#
  602 +# if self._pause_:
  603 +# return
  604 +#
  605 +#
  606 +# def corregistrate_object(inp, coord_raw):
  607 +# m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = inp
  608 +# as1, bs1, gs1 = radians(coord_raw[obj_ref_mode, 3:])
  609 +# r_probe = tr.euler_matrix(as1, bs1, gs1, 'rzyx')
  610 +# t_probe_raw = tr.translation_matrix(coord_raw[obj_ref_mode, :3])
  611 +# t_offset_aux = np.linalg.inv(r_s0_raw) @ r_probe @ t_obj_raw
  612 +# t_offset = identity(4)
  613 +# t_offset[:, -1] = t_offset_aux[:, -1]
  614 +# t_probe = s0_raw @ t_offset @ np.linalg.inv(s0_raw) @ t_probe_raw
  615 +# m_probe = tr.concatenate_matrices(t_probe, r_probe)
  616 +#
  617 +# a, b, g = radians(coord_raw[1, 3:])
  618 +# r_ref = tr.euler_matrix(a, b, g, 'rzyx')
  619 +# t_ref = tr.translation_matrix(coord_raw[1, :3])
  620 +# m_ref = tr.concatenate_matrices(t_ref, r_ref)
  621 +#
  622 +# m_dyn = np.linalg.inv(m_ref) @ m_probe
  623 +# m_dyn[2, -1] = -m_dyn[2, -1]
  624 +#
  625 +# m_img = m_change @ m_dyn
  626 +# r_obj = r_obj_img @ np.linalg.inv(m_obj_raw) @ np.linalg.inv(s0_dyn) @ m_dyn @ m_obj_raw
  627 +#
  628 +# m_img[:3, :3] = r_obj[:3, :3]
  629 +#
  630 +# scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)
  631 +#
  632 +# coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
  633 +# degrees(angles[0]), degrees(angles[1]), degrees(angles[2])
  634 +#
  635 +# return coord, m_img
304 636
305 - m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = self.coreg_data  
306 - trck_init, trck_id, trck_mode = self.trck_info  
307 -  
308 - while self.nav_id:  
309 - coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)  
310 -  
311 - as1, bs1, gs1 = radians(coord_raw[obj_ref_mode, 3:])  
312 - r_probe = asmatrix(tr.euler_matrix(as1, bs1, gs1, 'rzyx'))  
313 - t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))  
314 - t_offset_aux = r_s0_raw.I * r_probe * t_obj_raw  
315 - t_offset = asmatrix(identity(4))  
316 - t_offset[:, -1] = t_offset_aux[:, -1]  
317 - t_probe = s0_raw * t_offset * s0_raw.I * t_probe_raw  
318 - m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))  
319 -  
320 - a, b, g = radians(coord_raw[1, 3:])  
321 - r_ref = tr.euler_matrix(a, b, g, 'rzyx')  
322 - t_ref = tr.translation_matrix(coord_raw[1, :3])  
323 - m_ref = asmatrix(tr.concatenate_matrices(t_ref, r_ref))  
324 -  
325 - m_dyn = m_ref.I * m_probe  
326 - m_dyn[2, -1] = -m_dyn[2, -1]  
327 -  
328 - m_img = m_change * m_dyn  
329 - r_obj = r_obj_img * m_obj_raw.I * s0_dyn.I * m_dyn * m_obj_raw  
330 -  
331 - m_img[:3, :3] = r_obj[:3, :3]  
332 -  
333 - scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)  
334 -  
335 - coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\  
336 - degrees(angles[0]), degrees(angles[1]), degrees(angles[2])  
337 -  
338 - wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)  
339 - wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)  
340 -  
341 - # TODO: Optimize the value of sleep for each tracking device.  
342 - sleep(0.175)  
343 -  
344 - # Debug tracker is not working with 0.175 so changed to 0.2  
345 - # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.  
346 - # sleep(.3)  
347 -  
348 - # partially working for translate and offset,  
349 - # but offset is kept always in same axis, have to fix for rotation  
350 - # M_dyn = M_reference.I * T_stylus  
351 - # M_dyn[2, -1] = -M_dyn[2, -1]  
352 - # M_dyn_ch = M_change * M_dyn  
353 - # ddd = M_dyn_ch[0, -1], M_dyn_ch[1, -1], M_dyn_ch[2, -1]  
354 - # M_dyn_ch[:3, -1] = asmatrix(db.flip_x_m(ddd)).reshape([3, 1])  
355 - # M_final = S0 * M_obj_trans_0 * S0.I * M_dyn_ch  
356 -  
357 - # this works for static reference object rotation  
358 - # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_raw.I * R_stylus * M_obj_rot_raw  
359 - # this works for dynamic reference in rotation but not in translation  
360 - # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_dyn.I * R_reference.I * R_stylus * M_obj_rot_raw  
361 -  
362 - if self._pause_:  
363 - return  
invesalius/data/imagedata_utils.py
@@ -37,6 +37,8 @@ from invesalius.data import vtk_utils as vtk_utils @@ -37,6 +37,8 @@ from invesalius.data import vtk_utils as vtk_utils
37 import invesalius.reader.bitmap_reader as bitmap_reader 37 import invesalius.reader.bitmap_reader as bitmap_reader
38 import invesalius.utils as utils 38 import invesalius.utils as utils
39 import invesalius.data.converters as converters 39 import invesalius.data.converters as converters
  40 +import invesalius.data.slice_ as sl
  41 +import invesalius.data.transformations as tr
40 42
41 from invesalius import inv_paths 43 from invesalius import inv_paths
42 44
@@ -516,3 +518,67 @@ def image_normalize(image, min_=0.0, max_=1.0, output_dtype=np.int16): @@ -516,3 +518,67 @@ def image_normalize(image, min_=0.0, max_=1.0, output_dtype=np.int16):
516 imin, imax = image.min(), image.max() 518 imin, imax = image.min(), image.max()
517 output[:] = (image - imin) * ((max_ - min_) / (imax - imin)) + min_ 519 output[:] = (image - imin) * ((max_ - min_) / (imax - imin)) + min_
518 return output 520 return output
  521 +
  522 +
  523 +def world2invspace(affine=None):
  524 + """
  525 + Normalize image pixel intensity for int16 gray scale values.
  526 +
  527 + :param repos: list of translation and rotation [trans_x, trans_y, trans_z, rot_x, rot_y, rot_z] to reposition the
  528 + vtk object prior to applying the affine matrix transformation. Note: rotation given in degrees
  529 + :param user_matrix: affine matrix from image header, prefered QForm matrix
  530 + :return: vtk transform filter for repositioning the polydata and affine matrix to be used as SetUserMatrix in actor
  531 + """
  532 +
  533 + # remove scaling factor for non-unitary voxel dimensions
  534 + scale, shear, angs, trans, persp = tr.decompose_matrix(affine)
  535 + affine_noscale = tr.compose_matrix(scale=None, shear=shear, angles=angs, translate=trans, perspective=persp)
  536 + # repos_img = [0.] * 6
  537 + # repos_img[1] = -float(shape[1])
  538 + #
  539 + # repos_mat = np.identity(4)
  540 + # # translation
  541 + # repos_mat[:3, -1] = repos_img[:3]
  542 + # # rotation (in principle for invesalius space no rotation is needed)
  543 + # repos_mat[:3, :3] = tr.euler_matrix(*np.deg2rad(repos_img[3:]), axes='sxyz')[:3, :3]
  544 +
  545 + # if repos:
  546 + # transx, transy, transz, rotx, roty, rotz = repos
  547 + # # create a transform that rotates the stl source
  548 + # transform = vtk.vtkTransform()
  549 + # transform.PostMultiply()
  550 + # transform.RotateX(rotx)
  551 + # transform.RotateY(roty)
  552 + # transform.RotateZ(rotz)
  553 + # transform.Translate(transx, transy, transz)
  554 + #
  555 + # transform_filt = vtk.vtkTransformPolyDataFilter()
  556 + # transform_filt.SetTransform(transform)
  557 + # transform_filt.Update()
  558 +
  559 + # assuming vtk default transformation order is PreMultiply, the user matrix is set so:
  560 + # 1. Replace the object -> 2. Transform the object to desired position/orientation
  561 + # PreMultiplty: M = M*A where M is current transformation and A is applied transformation
  562 + # user_matrix = np.linalg.inv(user_matrix) @ repos_mat
  563 +
  564 + return np.linalg.inv(affine_noscale)
  565 +
  566 +
  567 +def convert_world_to_voxel(xyz, affine):
  568 + """
  569 + Convert a coordinate from the world space ((x, y, z); scanner space; millimeters) to the
  570 + voxel space ((i, j, k)). This is achieved by multiplying a coordinate by the inverse
  571 + of the affine transformation.
  572 + More information: https://nipy.org/nibabel/coordinate_systems.html
  573 + :param xyz: a list or array of 3 coordinates (x, y, z) in the world coordinates
  574 + :param affine: a 4x4 array containing the image affine transformation in homogeneous coordinates
  575 + :return: a 1x3 array with the point coordinates in image space (i, j, k)
  576 + """
  577 +
  578 + # print("xyz: ", xyz, "\naffine", affine)
  579 + # convert xyz coordinate to 1x4 homogeneous coordinates array
  580 + xyz_homo = np.hstack((xyz, 1.)).reshape([4, 1])
  581 + ijk_homo = np.linalg.inv(affine) @ xyz_homo
  582 + ijk = ijk_homo.T[np.newaxis, 0, :3]
  583 +
  584 + return ijk
invesalius/data/record_coords.py
@@ -42,7 +42,8 @@ class Record(threading.Thread): @@ -42,7 +42,8 @@ class Record(threading.Thread):
42 self.start() 42 self.start()
43 43
44 def __bind_events(self): 44 def __bind_events(self):
45 - Publisher.subscribe(self.UpdateCurrentCoords, 'Co-registered points') 45 + # Publisher.subscribe(self.UpdateCurrentCoords, 'Co-registered points')
  46 + Publisher.subscribe(self.UpdateCurrentCoords, 'Update cross position')
46 47
47 def UpdateCurrentCoords(self, arg, position): 48 def UpdateCurrentCoords(self, arg, position):
48 self.coord = asarray(position) 49 self.coord = asarray(position)
@@ -50,7 +51,10 @@ class Record(threading.Thread): @@ -50,7 +51,10 @@ class Record(threading.Thread):
50 def stop(self): 51 def stop(self):
51 self._pause_ = True 52 self._pause_ = True
52 #save coords dialog 53 #save coords dialog
53 - filename = dlg.ShowSaveCoordsDialog("coords.csv") 54 + filename = dlg.ShowLoadSaveDialog(message=_(u"Save coords as..."),
  55 + wildcard=_("Coordinates files (*.csv)|*.csv"),
  56 + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
  57 + default_filename="coords.csv", save_ext="csv")
54 if filename: 58 if filename:
55 savetxt(filename, self.coord_list, delimiter=',', fmt='%.4f', header="time, x, y, z, a, b, g", comments="") 59 savetxt(filename, self.coord_list, delimiter=',', fmt='%.4f', header="time, x, y, z, a, b, g", comments="")
56 60
invesalius/data/slice_.py
@@ -82,6 +82,9 @@ class Slice(metaclass=utils.Singleton): @@ -82,6 +82,9 @@ class Slice(metaclass=utils.Singleton):
82 self.blend_filter = None 82 self.blend_filter = None
83 self.histogram = None 83 self.histogram = None
84 self._matrix = None 84 self._matrix = None
  85 + self._affine = np.identity(4)
  86 + self._n_tracts = 0
  87 + self._tracker = None
85 self.aux_matrices = {} 88 self.aux_matrices = {}
86 self.aux_matrices_colours = {} 89 self.aux_matrices_colours = {}
87 self.state = const.STATE_DEFAULT 90 self.state = const.STATE_DEFAULT
@@ -144,6 +147,30 @@ class Slice(metaclass=utils.Singleton): @@ -144,6 +147,30 @@ class Slice(metaclass=utils.Singleton):
144 (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing) 147 (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing)
145 ] 148 ]
146 149
  150 + @property
  151 + def affine(self):
  152 + return self._affine
  153 +
  154 + @affine.setter
  155 + def affine(self, value):
  156 + self._affine = value
  157 +
  158 + @property
  159 + def n_tracts(self):
  160 + return self._n_tracts
  161 +
  162 + @n_tracts.setter
  163 + def n_tracts(self, value):
  164 + self._n_tracts = value
  165 +
  166 + @property
  167 + def tracker(self):
  168 + return self._tracker
  169 +
  170 + @tracker.setter
  171 + def tracker(self, value):
  172 + self._tracker = value
  173 +
147 def __bind_events(self): 174 def __bind_events(self):
148 # General slice control 175 # General slice control
149 Publisher.subscribe(self.CreateSurfaceFromIndex, "Create surface from index") 176 Publisher.subscribe(self.CreateSurfaceFromIndex, "Create surface from index")
invesalius/data/styles.py
@@ -47,6 +47,14 @@ from invesalius.data.measures import (CircleDensityMeasure, MeasureData, @@ -47,6 +47,14 @@ from invesalius.data.measures import (CircleDensityMeasure, MeasureData,
47 47
48 from invesalius_cy import floodfill 48 from invesalius_cy import floodfill
49 49
  50 +# For tracts
  51 +import invesalius.data.tractography as dtr
  52 +# import invesalius.project as prj
  53 +import invesalius.data.slice_ as sl
  54 +import invesalius.data.bases as bases
  55 +# from time import sleep
  56 +# ---
  57 +
50 ORIENTATIONS = { 58 ORIENTATIONS = {
51 "AXIAL": const.AXIAL, 59 "AXIAL": const.AXIAL,
52 "CORONAL": const.CORONAL, 60 "CORONAL": const.CORONAL,
@@ -465,6 +473,18 @@ class CrossInteractorStyle(DefaultInteractorStyle): @@ -465,6 +473,18 @@ class CrossInteractorStyle(DefaultInteractorStyle):
465 self.slice_actor = viewer.slice_data.actor 473 self.slice_actor = viewer.slice_data.actor
466 self.slice_data = viewer.slice_data 474 self.slice_data = viewer.slice_data
467 475
  476 + # tracts
  477 + # self.seed = [0., 0., 0.]
  478 + # slic = sl.Slice()
  479 + # self.affine = slic.affine
  480 + # self.tracker = slic.tracker
  481 + #
  482 + # self.affine_vtk = vtk.vtkMatrix4x4()
  483 + # for row in range(0, 4):
  484 + # for col in range(0, 4):
  485 + # self.affine_vtk.SetElement(row, col, self.affine[row, col])
  486 + # ---
  487 +
468 self.picker = vtk.vtkWorldPointPicker() 488 self.picker = vtk.vtkWorldPointPicker()
469 489
470 self.AddObserver("MouseMoveEvent", self.OnCrossMove) 490 self.AddObserver("MouseMoveEvent", self.OnCrossMove)
@@ -478,6 +498,7 @@ class CrossInteractorStyle(DefaultInteractorStyle): @@ -478,6 +498,7 @@ class CrossInteractorStyle(DefaultInteractorStyle):
478 498
479 def CleanUp(self): 499 def CleanUp(self):
480 self.viewer._set_cross_visibility(0) 500 self.viewer._set_cross_visibility(0)
  501 + Publisher.sendMessage('Remove tracts')
481 Publisher.sendMessage('Toggle toolbar item', 502 Publisher.sendMessage('Toggle toolbar item',
482 _id=self.state_code, value=False) 503 _id=self.state_code, value=False)
483 504
@@ -497,12 +518,20 @@ class CrossInteractorStyle(DefaultInteractorStyle): @@ -497,12 +518,20 @@ class CrossInteractorStyle(DefaultInteractorStyle):
497 wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) 518 wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker)
498 px, py = self.viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) 519 px, py = self.viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz)
499 coord = self.viewer.calcultate_scroll_position(px, py) 520 coord = self.viewer.calcultate_scroll_position(px, py)
500 - Publisher.sendMessage('Update cross position', position=(wx, wy, wz)) 521 +
  522 + # Tracts
  523 + # pos_world_aux = np.ones([4, 1])
  524 + # pos_world_aux[:3, -1] = bases.flip_x((wx, wy, wz))[:3]
  525 + # pos_world = np.linalg.inv(self.affine) @ pos_world_aux
  526 + # seed_aux = pos_world.reshape([1, 4])[0, :3]
  527 + # self.seed = seed_aux[np.newaxis, :]
  528 + # print("Check the seed: ", self.seed)
  529 + #
  530 +
  531 + Publisher.sendMessage('Update cross position', arg=None, position=(wx, wy, wz, 0., 0., 0.))
501 self.ScrollSlice(coord) 532 self.ScrollSlice(coord)
502 - Publisher.sendMessage('Set ball reference position', position=(wx, wy, wz))  
503 - Publisher.sendMessage('Co-registered points', arg=None, position=(wx, wy, wz, 0., 0., 0.))  
504 533
505 - iren.Render() 534 + # iren.Render()
506 535
507 def ScrollSlice(self, coord): 536 def ScrollSlice(self, coord):
508 if self.orientation == "AXIAL": 537 if self.orientation == "AXIAL":
@@ -522,6 +551,92 @@ class CrossInteractorStyle(DefaultInteractorStyle): @@ -522,6 +551,92 @@ class CrossInteractorStyle(DefaultInteractorStyle):
522 index=coord[0]) 551 index=coord[0])
523 552
524 553
  554 +class TractsInteractorStyle(CrossInteractorStyle):
  555 + """
  556 + Interactor style responsible for tracts visualization.
  557 + """
  558 + def __init__(self, viewer):
  559 + CrossInteractorStyle.__init__(self, viewer)
  560 +
  561 + # self.state_code = const.SLICE_STATE_TRACTS
  562 +
  563 + self.viewer = viewer
  564 + # print("Im fucking brilliant!")
  565 + self.tracts = None
  566 +
  567 + # data_dir = b'C:\Users\deoliv1\OneDrive\data\dti'
  568 + # FOD_path = b"sub-P0_dwi_FOD.nii"
  569 + # full_path = os.path.join(data_dir, FOD_path)
  570 + # self.tracker = Trekker.tracker(full_path)
  571 + # self.orientation = viewer.orientation
  572 + # self.slice_actor = viewer.slice_data.actor
  573 + # self.slice_data = viewer.slice_data
  574 +
  575 + # self.picker = vtk.vtkWorldPointPicker()
  576 +
  577 + self.AddObserver("MouseMoveEvent", self.OnTractsMove)
  578 + self.AddObserver("LeftButtonPressEvent", self.OnTractsMouseClick)
  579 + self.AddObserver("LeftButtonReleaseEvent", self.OnTractsReleaseLeftButton)
  580 +
  581 + # def SetUp(self):
  582 + # self.viewer._set_cross_visibility(1)
  583 + # Publisher.sendMessage('Toggle toolbar item',
  584 + # _id=self.state_code, value=True)
  585 +
  586 + # def CleanUp(self):
  587 + # self.viewer._set_cross_visibility(0)
  588 + # Publisher.sendMessage('Toggle toolbar item',
  589 + # _id=self.state_code, value=False)
  590 +
  591 + def OnTractsMove(self, obj, evt):
  592 + # The user moved the mouse with left button pressed
  593 + if self.left_pressed:
  594 + # print("OnTractsMove interactor style")
  595 + # iren = obj.GetInteractor()
  596 + self.ChangeTracts(True)
  597 +
  598 + def OnTractsMouseClick(self, obj, evt):
  599 + # print("Single mouse click")
  600 + # self.tracts = dtr.compute_tracts(self.tracker, self.seed, self.left_pressed)
  601 + self.ChangeTracts(True)
  602 +
  603 + def OnTractsReleaseLeftButton(self, obj, evt):
  604 + # time.sleep(3.)
  605 + self.tracts.stop()
  606 + # self.ChangeCrossPosition(iren)
  607 +
  608 + def ChangeTracts(self, pressed):
  609 + # print("Trying to compute tracts")
  610 + self.tracts = dtr.compute_tracts(self.tracker, self.seed, self.affine_vtk, pressed)
  611 + # mouse_x, mouse_y = iren.GetEventPosition()
  612 + # wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker)
  613 + # px, py = self.viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz)
  614 + # coord = self.viewer.calcultate_scroll_position(px, py)
  615 + # Publisher.sendMessage('Update cross position', position=(wx, wy, wz))
  616 + # # self.ScrollSlice(coord)
  617 + # Publisher.sendMessage('Set ball reference position', position=(wx, wy, wz))
  618 + # Publisher.sendMessage('Co-registered points', arg=None, position=(wx, wy, wz, 0., 0., 0.))
  619 +
  620 + # iren.Render()
  621 +
  622 + # def ScrollSlice(self, coord):
  623 + # if self.orientation == "AXIAL":
  624 + # Publisher.sendMessage(('Set scroll position', 'SAGITAL'),
  625 + # index=coord[0])
  626 + # Publisher.sendMessage(('Set scroll position', 'CORONAL'),
  627 + # index=coord[1])
  628 + # elif self.orientation == "SAGITAL":
  629 + # Publisher.sendMessage(('Set scroll position', 'AXIAL'),
  630 + # index=coord[2])
  631 + # Publisher.sendMessage(('Set scroll position', 'CORONAL'),
  632 + # index=coord[1])
  633 + # elif self.orientation == "CORONAL":
  634 + # Publisher.sendMessage(('Set scroll position', 'AXIAL'),
  635 + # index=coord[2])
  636 + # Publisher.sendMessage(('Set scroll position', 'SAGITAL'),
  637 + # index=coord[0])
  638 +
  639 +
525 class WWWLInteractorStyle(DefaultInteractorStyle): 640 class WWWLInteractorStyle(DefaultInteractorStyle):
526 """ 641 """
527 Interactor style responsible for Window Level & Width functionality. 642 Interactor style responsible for Window Level & Width functionality.
@@ -2784,6 +2899,7 @@ class Styles: @@ -2784,6 +2899,7 @@ class Styles:
2784 const.SLICE_STATE_SELECT_MASK_PARTS: SelectMaskPartsInteractorStyle, 2899 const.SLICE_STATE_SELECT_MASK_PARTS: SelectMaskPartsInteractorStyle,
2785 const.SLICE_STATE_FFILL_SEGMENTATION: FloodFillSegmentInteractorStyle, 2900 const.SLICE_STATE_FFILL_SEGMENTATION: FloodFillSegmentInteractorStyle,
2786 const.SLICE_STATE_CROP_MASK: CropMaskInteractorStyle, 2901 const.SLICE_STATE_CROP_MASK: CropMaskInteractorStyle,
  2902 + const.SLICE_STATE_TRACTS: TractsInteractorStyle,
2787 } 2903 }
2788 2904
2789 @classmethod 2905 @classmethod
invesalius/data/surface.py
@@ -383,6 +383,7 @@ class SurfaceManager(): @@ -383,6 +383,7 @@ class SurfaceManager():
383 383
384 actor = vtk.vtkActor() 384 actor = vtk.vtkActor()
385 actor.SetMapper(mapper) 385 actor.SetMapper(mapper)
  386 + actor.GetProperty().SetBackfaceCulling(1)
386 387
387 print("BOunds", actor.GetBounds()) 388 print("BOunds", actor.GetBounds())
388 389
@@ -494,6 +495,7 @@ class SurfaceManager(): @@ -494,6 +495,7 @@ class SurfaceManager():
494 495
495 # Represent an object (geometry & properties) in the rendered scene 496 # Represent an object (geometry & properties) in the rendered scene
496 actor = vtk.vtkActor() 497 actor = vtk.vtkActor()
  498 + actor.GetProperty().SetBackfaceCulling(1)
497 actor.SetMapper(mapper) 499 actor.SetMapper(mapper)
498 500
499 # Set actor colour and transparency 501 # Set actor colour and transparency
@@ -536,6 +538,7 @@ class SurfaceManager(): @@ -536,6 +538,7 @@ class SurfaceManager():
536 538
537 # Represent an object (geometry & properties) in the rendered scene 539 # Represent an object (geometry & properties) in the rendered scene
538 actor = vtk.vtkActor() 540 actor = vtk.vtkActor()
  541 + actor.GetProperty().SetBackfaceCulling(1)
539 actor.SetMapper(mapper) 542 actor.SetMapper(mapper)
540 del mapper 543 del mapper
541 #Create Surface instance 544 #Create Surface instance
invesalius/data/trackers.py
@@ -62,6 +62,7 @@ def DefaultTracker(tracker_id): @@ -62,6 +62,7 @@ def DefaultTracker(tracker_id):
62 # return tracker initialization variable and type of connection 62 # return tracker initialization variable and type of connection
63 return trck_init, 'wrapper' 63 return trck_init, 'wrapper'
64 64
  65 +
65 def PolarisTracker(tracker_id): 66 def PolarisTracker(tracker_id):
66 from wx import ID_OK 67 from wx import ID_OK
67 trck_init = None 68 trck_init = None
@@ -91,6 +92,7 @@ def PolarisTracker(tracker_id): @@ -91,6 +92,7 @@ def PolarisTracker(tracker_id):
91 # return tracker initialization variable and type of connection 92 # return tracker initialization variable and type of connection
92 return trck_init, lib_mode 93 return trck_init, lib_mode
93 94
  95 +
94 def CameraTracker(tracker_id): 96 def CameraTracker(tracker_id):
95 trck_init = None 97 trck_init = None
96 try: 98 try:
@@ -105,6 +107,7 @@ def CameraTracker(tracker_id): @@ -105,6 +107,7 @@ def CameraTracker(tracker_id):
105 # return tracker initialization variable and type of connection 107 # return tracker initialization variable and type of connection
106 return trck_init, 'wrapper' 108 return trck_init, 'wrapper'
107 109
  110 +
108 def ClaronTracker(tracker_id): 111 def ClaronTracker(tracker_id):
109 import invesalius.constants as const 112 import invesalius.constants as const
110 from invesalius import inv_paths 113 from invesalius import inv_paths
invesalius/data/tractography.py 0 → 100644
@@ -0,0 +1,471 @@ @@ -0,0 +1,471 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +#--------------------------------------------------------------------------
  4 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  5 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  6 +# Homepage: http://www.softwarepublico.gov.br
  7 +# Contact: invesalius@cti.gov.br
  8 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  9 +#--------------------------------------------------------------------------
  10 +# Este programa e software livre; voce pode redistribui-lo e/ou
  11 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  12 +# publicada pela Free Software Foundation; de acordo com a versao 2
  13 +# da Licenca.
  14 +#
  15 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  16 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  17 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  18 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  19 +# detalhes.
  20 +#--------------------------------------------------------------------------
  21 +
  22 +# Author: Victor Hugo Souza (victorhos-at-hotmail.com)
  23 +# Contributions: Dogu Baran Aydogan
  24 +# Initial date: 8 May 2020
  25 +
  26 +import threading
  27 +import time
  28 +
  29 +import numpy as np
  30 +import queue
  31 +from pubsub import pub as Publisher
  32 +import vtk
  33 +
  34 +import invesalius.constants as const
  35 +import invesalius.data.imagedata_utils as img_utils
  36 +
  37 +# Nice print for arrays
  38 +# np.set_printoptions(precision=2)
  39 +# np.set_printoptions(suppress=True)
  40 +
  41 +
  42 +def compute_directions(trk_n):
  43 + """Compute direction of a single tract in each point and return as an RGB color
  44 +
  45 + :param trk_n: nx3 array of doubles (x, y, z) point coordinates composing the tract
  46 + :type trk_n: numpy.ndarray
  47 + :return: nx3 array of int (x, y, z) RGB colors in the range 0 - 255
  48 + :rtype: numpy.ndarray
  49 + """
  50 +
  51 + # trk_d = np.diff(trk_n, axis=0, append=2*trk_n[np.newaxis, -1, :])
  52 + trk_d = np.diff(trk_n, axis=0, append=trk_n[np.newaxis, -2, :])
  53 + trk_d[-1, :] *= -1
  54 + # check that linalg norm makes second norm
  55 + # https://stackoverflow.com/questions/21030391/how-to-normalize-an-array-in-numpy
  56 + direction = 255 * np.absolute((trk_d / np.linalg.norm(trk_d, axis=1)[:, None]))
  57 + return direction.astype(int)
  58 +
  59 +
  60 +def compute_tubes(trk, direction):
  61 + """Compute and assign colors to a vtkTube for visualization of a single tract
  62 +
  63 + :param trk: nx3 array of doubles (x, y, z) point coordinates composing the tract
  64 + :type trk: numpy.ndarray
  65 + :param direction: nx3 array of int (x, y, z) RGB colors in the range 0 - 255
  66 + :type direction: numpy.ndarray
  67 + :return: a vtkTubeFilter instance
  68 + :rtype: vtkTubeFilter
  69 + """
  70 +
  71 + numb_points = trk.shape[0]
  72 + points = vtk.vtkPoints()
  73 + lines = vtk.vtkCellArray()
  74 +
  75 + colors = vtk.vtkUnsignedCharArray()
  76 + colors.SetNumberOfComponents(3)
  77 +
  78 + k = 0
  79 + lines.InsertNextCell(numb_points)
  80 + for j in range(numb_points):
  81 + points.InsertNextPoint(trk[j, :])
  82 + colors.InsertNextTuple(direction[j, :])
  83 + lines.InsertCellPoint(k)
  84 + k += 1
  85 +
  86 + trk_data = vtk.vtkPolyData()
  87 + trk_data.SetPoints(points)
  88 + trk_data.SetLines(lines)
  89 + trk_data.GetPointData().SetScalars(colors)
  90 +
  91 + # make it a tube
  92 + trk_tube = vtk.vtkTubeFilter()
  93 + trk_tube.SetRadius(0.5)
  94 + trk_tube.SetNumberOfSides(4)
  95 + trk_tube.SetInputData(trk_data)
  96 + trk_tube.Update()
  97 +
  98 + return trk_tube
  99 +
  100 +
  101 +def combine_tracts_root(out_list, root, n_block):
  102 + """Adds a set of tracts to given position in a given vtkMultiBlockDataSet
  103 +
  104 + :param out_list: List of vtkTubeFilters representing the tracts
  105 + :type out_list: list
  106 + :param root: A collection of tracts as a vtkMultiBlockDataSet
  107 + :type root: vtkMultiBlockDataSet
  108 + :param n_block: The location in the given vtkMultiBlockDataSet to insert the new tracts
  109 + :type n_block: int
  110 + :return: The updated collection of tracts as a vtkMultiBlockDataSet
  111 + :rtype: vtkMultiBlockDataSet
  112 + """
  113 +
  114 + # create tracts only when at least one was computed
  115 + # print("Len outlist in root: ", len(out_list))
  116 + if not out_list.count(None) == len(out_list):
  117 + for n, tube in enumerate(out_list):
  118 + root.SetBlock(n_block + n, tube.GetOutput())
  119 +
  120 + return root
  121 +
  122 +
  123 +def combine_tracts_branch(out_list):
  124 + """Combines a set of tracts in vtkMultiBlockDataSet
  125 +
  126 + :param out_list: List of vtkTubeFilters representing the tracts
  127 + :type out_list: list
  128 + :return: A collection of tracts as a vtkMultiBlockDataSet
  129 + :rtype: vtkMultiBlockDataSet
  130 + """
  131 +
  132 + branch = vtk.vtkMultiBlockDataSet()
  133 + # create tracts only when at least one was computed
  134 + # print("Len outlist in root: ", len(out_list))
  135 + if not out_list.count(None) == len(out_list):
  136 + for n, tube in enumerate(out_list):
  137 + branch.SetBlock(n, tube.GetOutput())
  138 +
  139 + return branch
  140 +
  141 +
  142 +def tracts_computation(trk_list, root, n_tracts):
  143 + """Convert the list of all computed tracts given by Trekker run and returns a vtkMultiBlockDataSet
  144 +
  145 + :param trk_list: List of lists containing the computed tracts and corresponding coordinates
  146 + :type trk_list: list
  147 + :param root: A collection of tracts as a vtkMultiBlockDataSet
  148 + :type root: vtkMultiBlockDataSet
  149 + :param n_tracts:
  150 + :type n_tracts: int
  151 + :return: The updated collection of tracts as a vtkMultiBlockDataSet
  152 + :rtype: vtkMultiBlockDataSet
  153 + """
  154 +
  155 + # Transform tracts to array
  156 + trk_arr = [np.asarray(trk_n).T if trk_n else None for trk_n in trk_list]
  157 +
  158 + # Compute the directions
  159 + trk_dir = [compute_directions(trk_n) for trk_n in trk_arr]
  160 +
  161 + # Compute the vtk tubes
  162 + out_list = [compute_tubes(trk_arr_n, trk_dir_n) for trk_arr_n, trk_dir_n in zip(trk_arr, trk_dir)]
  163 +
  164 + root = combine_tracts_root(out_list, root, n_tracts)
  165 +
  166 + return root
  167 +
  168 +
  169 +def compute_tracts(trekker, position, affine, affine_vtk, n_tracts):
  170 + """ Compute tractograms using the Trekker library.
  171 +
  172 + :param trekker: Trekker library instance
  173 + :type trekker: Trekker.T
  174 + :param position: 3 double coordinates (x, y, z) in list or array
  175 + :type position: list
  176 + :param affine: 4 x 4 numpy double array
  177 + :type affine: numpy.ndarray
  178 + :param affine_vtk: vtkMatrix4x4 isntance with affine transformation matrix
  179 + :type affine_vtk: vtkMatrix4x4
  180 + :param n_tracts: number of tracts to compute
  181 + :type n_tracts: int
  182 + """
  183 +
  184 + # during neuronavigation, root needs to be initialized outside the while loop so the new tracts
  185 + # can be appended to the root block set
  186 + root = vtk.vtkMultiBlockDataSet()
  187 + # Juuso's
  188 + # seed = np.array([[-8.49, -8.39, 2.5]])
  189 + # Baran M1
  190 + # seed = np.array([[27.53, -77.37, 46.42]])
  191 + seed_trk = img_utils.convert_world_to_voxel(position, affine)
  192 + # print("seed example: {}".format(seed_trk))
  193 + trekker.seed_coordinates(np.repeat(seed_trk, n_tracts, axis=0))
  194 + # print("trk list len: ", len(trekker.run()))
  195 + trk_list = trekker.run()
  196 + if trk_list:
  197 + root = tracts_computation(trk_list, root, 0)
  198 + Publisher.sendMessage('Remove tracts')
  199 + Publisher.sendMessage('Update tracts', flag=True, root=root, affine_vtk=affine_vtk)
  200 + else:
  201 + Publisher.sendMessage('Remove tracts')
  202 +
  203 +
  204 +def tracts_computation_branch(trk_list):
  205 + """Convert the list of all computed tracts given by Trekker run and returns a vtkMultiBlockDataSet
  206 +
  207 + :param trk_list: List of lists containing the computed tracts and corresponding coordinates
  208 + :type trk_list: list
  209 + :return: The collection of tracts as a vtkMultiBlockDataSet
  210 + :rtype: vtkMultiBlockDataSet
  211 + """
  212 + # Transform tracts to array
  213 + trk_arr = [np.asarray(trk_n).T if trk_n else None for trk_n in trk_list]
  214 + # Compute the directions
  215 + trk_dir = [compute_directions(trk_n) for trk_n in trk_arr]
  216 + # Compute the vtk tubes
  217 + tube_list = [compute_tubes(trk_arr_n, trk_dir_n) for trk_arr_n, trk_dir_n in zip(trk_arr, trk_dir)]
  218 + branch = combine_tracts_branch(tube_list)
  219 +
  220 + return branch
  221 +
  222 +
  223 +class ComputeTractsThread(threading.Thread):
  224 +
  225 + def __init__(self, inp, affine_vtk, coord_tracts_queue, tracts_queue, event, sle):
  226 + """Class (threading) to compute real time tractography data for visualization.
  227 +
  228 + Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/)
  229 + For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one
  230 + vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as
  231 + bundle, to obtain fast computation and visualization. The bundle dataset is mapped to a single vtkActor.
  232 + Mapper and Actor are computer in the data/viewer_volume.py module for easier handling in the invesalius 3D scene.
  233 +
  234 + Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation
  235 +
  236 + :param inp: List of inputs: trekker instance, affine numpy array, seed_offset, seed_radius, n_threads
  237 + :type inp: list
  238 + :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene
  239 + :type affine_vtk: vtkMatrix4x4
  240 + :param coord_queue: Queue instance that manage coordinates read from tracking device and coregistered
  241 + :type coord_queue: queue.Queue
  242 + :param visualization_queue: Queue instance that manage coordinates to be visualized
  243 + :type visualization_queue: queue.Queue
  244 + :param event: Threading event to coordinate when tasks as done and allow UI release
  245 + :type event: threading.Event
  246 + :param sle: Sleep pause in seconds
  247 + :type sle: float
  248 + """
  249 +
  250 + threading.Thread.__init__(self, name='ComputeTractsThread')
  251 + self.inp = inp
  252 + self.affine_vtk = affine_vtk
  253 + # self.coord_queue = coord_queue
  254 + self.coord_tracts_queue = coord_tracts_queue
  255 + self.tracts_queue = tracts_queue
  256 + # self.visualization_queue = visualization_queue
  257 + self.event = event
  258 + self.sle = sle
  259 +
  260 + def run(self):
  261 +
  262 + trekker, affine, offset, n_tracts_total, seed_radius, n_threads = self.inp
  263 + # n_threads = n_tracts_total
  264 + p_old = np.array([[0., 0., 0.]])
  265 + n_tracts = 0
  266 +
  267 + # Compute the tracts
  268 + # print('ComputeTractsThread: event {}'.format(self.event.is_set()))
  269 + while not self.event.is_set():
  270 + try:
  271 + # print("Computing tracts")
  272 + # get from the queue the coordinates, coregistration transformation matrix, and flipped matrix
  273 + m_img_flip = self.coord_tracts_queue.get_nowait()
  274 + # coord, m_img, m_img_flip = self.coord_queue.get_nowait()
  275 + # print('ComputeTractsThread: get {}'.format(count))
  276 +
  277 + # 20200402: in this new refactored version the m_img comes different than the position
  278 + # the new version m_img is already flixped in y, which means that Y is negative
  279 + # if only the Y is negative maybe no need for the flip_x funtcion at all in the navigation
  280 + # but check all coord_queue before why now the m_img comes different than position
  281 + # 20200403: indeed flip_x is just a -1 multiplication to the Y coordinate, remove function flip_x
  282 + # m_img_flip = m_img.copy()
  283 + # m_img_flip[1, -1] = -m_img_flip[1, -1]
  284 +
  285 + # translate the coordinate along the normal vector of the object/coil
  286 + coord_offset = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2]
  287 + # coord_offset = np.array([[27.53, -77.37, 46.42]])
  288 + dist = abs(np.linalg.norm(p_old - np.asarray(coord_offset)))
  289 + p_old = coord_offset.copy()
  290 +
  291 + # print("p_new_shape", coord_offset.shape)
  292 + # print("m_img_flip_shape", m_img_flip.shape)
  293 + seed_trk = img_utils.convert_world_to_voxel(coord_offset, affine)
  294 + # Juuso's
  295 + # seed_trk = np.array([[-8.49, -8.39, 2.5]])
  296 + # Baran M1
  297 + # seed_trk = np.array([[27.53, -77.37, 46.42]])
  298 + # print("Seed: {}".format(seed))
  299 +
  300 + # set the seeds for trekker, one seed is repeated n_threads times
  301 + # trekker has internal multiprocessing approach done in C. Here the number of available threads is give,
  302 + # but in case a large number of tracts is requested, it will compute all in parallel automatically
  303 + # for a more fluent navigation, better to compute the maximum number the computer handles
  304 + trekker.seed_coordinates(np.repeat(seed_trk, n_threads, axis=0))
  305 +
  306 + # run the trekker, this is the slowest line of code, be careful to just use once!
  307 + trk_list = trekker.run()
  308 +
  309 + if trk_list:
  310 + # print("dist: {}".format(dist))
  311 + if dist >= seed_radius:
  312 + # when moving the coil further than the seed_radius restart the bundle computation
  313 + bundle = vtk.vtkMultiBlockDataSet()
  314 + n_branches = 0
  315 + branch = tracts_computation_branch(trk_list)
  316 + bundle.SetBlock(n_branches, branch)
  317 + n_branches += 1
  318 + n_tracts = branch.GetNumberOfBlocks()
  319 +
  320 + # TODO: maybe keep computing even if reaches the maximum
  321 + elif dist < seed_radius and n_tracts < n_tracts_total:
  322 + # compute tracts blocks and add to bungle until reaches the maximum number of tracts
  323 + branch = tracts_computation_branch(trk_list)
  324 + if bundle:
  325 + bundle.SetBlock(n_branches, branch)
  326 + n_tracts += branch.GetNumberOfBlocks()
  327 + n_branches += 1
  328 +
  329 + else:
  330 + bundle = None
  331 +
  332 + # rethink if this should be inside the if condition, it may lock the thread if no tracts are found
  333 + # use no wait to ensure maximum speed and avoid visualizing old tracts in the queue, this might
  334 + # be more evident in slow computer or for heavier tract computations, it is better slow update
  335 + # than visualizing old data
  336 + # self.visualization_queue.put_nowait([coord, m_img, bundle])
  337 + self.tracts_queue.put_nowait((bundle, self.affine_vtk, coord_offset))
  338 + # print('ComputeTractsThread: put {}'.format(count))
  339 +
  340 + self.coord_tracts_queue.task_done()
  341 + # self.coord_queue.task_done()
  342 + # print('ComputeTractsThread: done {}'.format(count))
  343 +
  344 + # sleep required to prevent user interface from being unresponsive
  345 + time.sleep(self.sle)
  346 + # if no coordinates pass
  347 + except queue.Empty:
  348 + # print("Empty queue in tractography")
  349 + pass
  350 + # if queue is full mark as done (may not be needed in this new "nowait" method)
  351 + except queue.Full:
  352 + # self.coord_queue.task_done()
  353 + self.coord_tracts_queue.task_done()
  354 +
  355 +
  356 +class ComputeTractsThreadSingleBlock(threading.Thread):
  357 +
  358 + def __init__(self, inp, affine_vtk, coord_queue, visualization_queue, event, sle):
  359 + """Class (threading) to compute real time tractography data for visualization in a single loop.
  360 +
  361 + Different than ComputeTractsThread because it does not keep adding tracts to the bundle until maximum,
  362 + is reached. It actually compute all requested tracts at once. (Might be deleted in the future)!
  363 + Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/)
  364 + For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one
  365 + vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as
  366 + bundle, to obtain fast computation and visualization. The bundle dataset is mapped to a single vtkActor.
  367 + Mapper and Actor are computer in the data/viewer_volume.py module for easier handling in the invesalius 3D scene.
  368 +
  369 + Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation
  370 +
  371 + :param inp: List of inputs: trekker instance, affine numpy array, seed_offset, seed_radius, n_threads
  372 + :type inp: list
  373 + :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene
  374 + :type affine_vtk: vtkMatrix4x4
  375 + :param coord_queue: Queue instance that manage coordinates read from tracking device and coregistered
  376 + :type coord_queue: queue.Queue
  377 + :param visualization_queue: Queue instance that manage coordinates to be visualized
  378 + :type visualization_queue: queue.Queue
  379 + :param event: Threading event to coordinate when tasks as done and allow UI release
  380 + :type event: threading.Event
  381 + :param sle: Sleep pause in seconds
  382 + :type sle: float
  383 + """
  384 +
  385 + threading.Thread.__init__(self, name='ComputeTractsThread')
  386 + self.inp = inp
  387 + self.affine_vtk = affine_vtk
  388 + self.coord_queue = coord_queue
  389 + self.visualization_queue = visualization_queue
  390 + self.event = event
  391 + self.sle = sle
  392 +
  393 + def run(self):
  394 +
  395 + trekker, affine, offset, n_tracts_total, seed_radius, n_threads = self.inp
  396 + # as a single block, computes all the maximum number of tracts at once, not optimal for navigation
  397 + n_threads = n_tracts_total
  398 + p_old = np.array([[0., 0., 0.]])
  399 + root = vtk.vtkMultiBlockDataSet()
  400 +
  401 + # Compute the tracts
  402 + # print('ComputeTractsThread: event {}'.format(self.event.is_set()))
  403 + while not self.event.is_set():
  404 + try:
  405 + coord, m_img, m_img_flip = self.coord_queue.get_nowait()
  406 +
  407 + # translate the coordinate along the normal vector of the object/coil
  408 + coord_offset = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2]
  409 + # coord_offset = np.array([[27.53, -77.37, 46.42]])
  410 + dist = abs(np.linalg.norm(p_old - np.asarray(coord_offset)))
  411 + p_old = coord_offset.copy()
  412 + seed_trk = img_utils.convert_world_to_voxel(coord_offset, affine)
  413 + # Juuso's
  414 + # seed_trk = np.array([[-8.49, -8.39, 2.5]])
  415 + # Baran M1
  416 + # seed_trk = np.array([[27.53, -77.37, 46.42]])
  417 + # print("Seed: {}".format(seed))
  418 +
  419 + # set the seeds for trekker, one seed is repeated n_threads times
  420 + # trekker has internal multiprocessing approach done in C. Here the number of available threads is give,
  421 + # but in case a large number of tracts is requested, it will compute all in parallel automatically
  422 + # for a more fluent navigation, better to compute the maximum number the computer handles
  423 + trekker.seed_coordinates(np.repeat(seed_trk, n_threads, axis=0))
  424 + # run the trekker, this is the slowest line of code, be careful to just use once!
  425 + trk_list = trekker.run()
  426 +
  427 + if trk_list:
  428 + # if the seed is outside the defined radius, restart the bundle computation
  429 + if dist >= seed_radius:
  430 + root = tracts_computation(trk_list, root, 0)
  431 + self.visualization_queue.put_nowait((coord, m_img, root))
  432 +
  433 + self.coord_queue.task_done()
  434 + time.sleep(self.sle)
  435 + except queue.Empty:
  436 + pass
  437 + except queue.Full:
  438 + self.coord_queue.task_done()
  439 +
  440 +
  441 +def set_trekker_parameters(trekker, params):
  442 + """Set all user-defined parameters for tractography computation using the Trekker library
  443 +
  444 + :param trekker: Trekker instance
  445 + :type trekker: Trekker.T
  446 + :param params: Dictionary containing the parameters values to set in Trekker. Initial values are in constants.py
  447 + :type params: dict
  448 + :return: List containing the Trekker instance and number of threads for parallel processing in the computer
  449 + :rtype: list
  450 + """
  451 + trekker.seed_maxTrials(params['seed_max'])
  452 + # trekker.stepSize(params['step_size'])
  453 + trekker.minFODamp(params['min_fod'])
  454 + # trekker.probeQuality(params['probe_quality'])
  455 + # trekker.maxEstInterval(params['max_interval'])
  456 + # trekker.minRadiusOfCurvature(params['min_radius_curv'])
  457 + # trekker.probeLength(params['probe_length'])
  458 + trekker.writeInterval(params['write_interval'])
  459 + trekker.maxLength(params['max_lenth'])
  460 + trekker.minLength(params['min_lenth'])
  461 + trekker.maxSamplingPerStep(params['max_sampling_step'])
  462 +
  463 + # check number if number of cores is valid in configuration file,
  464 + # otherwise use the maximum number of threads which is usually 2*N_CPUS
  465 + n_threads = 2 * const.N_CPU
  466 + if isinstance((params['numb_threads']), int) and params['numb_threads'] <= 2*const.N_CPU:
  467 + n_threads = params['numb_threads']
  468 +
  469 + trekker.numberOfThreads(n_threads)
  470 + # print("Trekker config updated: n_threads, {}; seed_max, {}".format(n_threads, params['seed_max']))
  471 + return trekker, n_threads
invesalius/data/trigger.py
@@ -39,7 +39,7 @@ class Trigger(threading.Thread): @@ -39,7 +39,7 @@ class Trigger(threading.Thread):
39 try: 39 try:
40 import serial 40 import serial
41 41
42 - self.trigger_init = serial.Serial('COM1', baudrate=9600, timeout=0) 42 + self.trigger_init = serial.Serial('COM5', baudrate=115200, timeout=0)
43 self.COM = True 43 self.COM = True
44 44
45 except: 45 except:
@@ -82,3 +82,91 @@ class Trigger(threading.Thread): @@ -82,3 +82,91 @@ class Trigger(threading.Thread):
82 if self.trigger_init: 82 if self.trigger_init:
83 self.trigger_init.close() 83 self.trigger_init.close()
84 return 84 return
  85 +
  86 +
  87 +class TriggerNew(threading.Thread):
  88 +
  89 + def __init__(self, trigger_queue, event, sle):
  90 + """Class (threading) to compute real time tractography data for visualization in a single loop.
  91 +
  92 + Different than ComputeTractsThread because it does not keep adding tracts to the bundle until maximum,
  93 + is reached. It actually compute all requested tracts at once. (Might be deleted in the future)!
  94 + Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/)
  95 + For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one
  96 + vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as
  97 + bundle, to obtain fast computation and visualization. The bundle dataset is mapped to a single vtkActor.
  98 + Mapper and Actor are computer in the data/viewer_volume.py module for easier handling in the invesalius 3D scene.
  99 +
  100 + Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation
  101 +
  102 + :param inp: List of inputs: trekker instance, affine numpy array, seed_offset, seed_radius, n_threads
  103 + :type inp: list
  104 + :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene
  105 + :type affine_vtk: vtkMatrix4x4
  106 + :param coord_queue: Queue instance that manage coordinates read from tracking device and coregistered
  107 + :type coord_queue: queue.Queue
  108 + :param visualization_queue: Queue instance that manage coordinates to be visualized
  109 + :type visualization_queue: queue.Queue
  110 + :param event: Threading event to coordinate when tasks as done and allow UI release
  111 + :type event: threading.Event
  112 + :param sle: Sleep pause in seconds
  113 + :type sle: float
  114 + """
  115 +
  116 + threading.Thread.__init__(self, name='Trigger')
  117 +
  118 + self.trigger_init = None
  119 + self.stylusplh = False
  120 + # self.COM = False
  121 + self.__bind_events()
  122 + try:
  123 + import serial
  124 +
  125 + self.trigger_init = serial.Serial('COM5', baudrate=115200, timeout=0)
  126 + # self.COM = True
  127 +
  128 + except:
  129 + #wx.MessageBox(_('Connection with port COM1 failed'), _('Communication error'), wx.OK | wx.ICON_ERROR)
  130 + print("Trigger init error: Connection with port COM failed")
  131 + # self.COM = False
  132 + pass
  133 +
  134 + # self.coord_queue = coord_queue
  135 + self.trigger_queue = trigger_queue
  136 + self.event = event
  137 + self.sle = sle
  138 +
  139 + def run(self):
  140 +
  141 + while not self.event.is_set():
  142 + trigger_on = False
  143 + try:
  144 + self.trigger_init.write(b'0')
  145 + sleep(0.3)
  146 + lines = self.trigger_init.readlines()
  147 + # Following lines can simulate a trigger in 3 sec repetitions
  148 + # sleep(3)
  149 + # lines = True
  150 + if lines:
  151 + trigger_on = True
  152 + # wx.CallAfter(Publisher.sendMessage, 'Create marker')
  153 +
  154 + if self.stylusplh:
  155 + trigger_on = True
  156 + # wx.CallAfter(Publisher.sendMessage, 'Create marker')
  157 + self.stylusplh = False
  158 +
  159 + self.trigger_queue.put_nowait(trigger_on)
  160 + sleep(self.sle)
  161 +
  162 + except:
  163 + print("Trigger not read, error")
  164 + pass
  165 +
  166 + # except queue.Empty:
  167 + # pass
  168 + # except queue.Full:
  169 + # self.coord_queue.task_done()
  170 + else:
  171 + if self.trigger_init:
  172 + self.trigger_init.close()
invesalius/data/viewer_slice.py
@@ -573,7 +573,7 @@ class Viewer(wx.Panel): @@ -573,7 +573,7 @@ class Viewer(wx.Panel):
573 573
574 self.cross.SetFocalPoint((ux, uy, uz)) 574 self.cross.SetFocalPoint((ux, uy, uz))
575 self.ScrollSlice(coord) 575 self.ScrollSlice(coord)
576 - Publisher.sendMessage('Set ball reference position', position=(ux, uy, uz)) 576 + self.interactor.Render()
577 577
578 def ScrollSlice(self, coord): 578 def ScrollSlice(self, coord):
579 if self.orientation == "AXIAL": 579 if self.orientation == "AXIAL":
@@ -817,9 +817,9 @@ class Viewer(wx.Panel): @@ -817,9 +817,9 @@ class Viewer(wx.Panel):
817 ('Set scroll position', 817 ('Set scroll position',
818 self.orientation)) 818 self.orientation))
819 Publisher.subscribe(self.__update_cross_position, 819 Publisher.subscribe(self.__update_cross_position,
820 - 'Update cross position')  
821 - Publisher.subscribe(self.UpdateSlicesNavigation,  
822 - 'Co-registered points') 820 + 'Update cross position')
  821 + # Publisher.subscribe(self.UpdateSlicesNavigation,
  822 + # 'Co-registered points')
823 ### 823 ###
824 # Publisher.subscribe(self.ChangeBrushColour, 824 # Publisher.subscribe(self.ChangeBrushColour,
825 # 'Add mask') 825 # 'Add mask')
@@ -1136,8 +1136,9 @@ class Viewer(wx.Panel): @@ -1136,8 +1136,9 @@ class Viewer(wx.Panel):
1136 1136
1137 renderer.AddActor(cross_actor) 1137 renderer.AddActor(cross_actor)
1138 1138
1139 - def __update_cross_position(self, position):  
1140 - self.cross.SetFocalPoint(position) 1139 + def __update_cross_position(self, arg, position):
  1140 + # self.cross.SetFocalPoint(position[:3])
  1141 + self.UpdateSlicesNavigation(None, position)
1141 1142
1142 def _set_cross_visibility(self, visibility): 1143 def _set_cross_visibility(self, visibility):
1143 self.cross_actor.SetVisibility(visibility) 1144 self.cross_actor.SetVisibility(visibility)
@@ -1296,8 +1297,8 @@ class Viewer(wx.Panel): @@ -1296,8 +1297,8 @@ class Viewer(wx.Panel):
1296 if self.state == const.SLICE_STATE_CROSS: 1297 if self.state == const.SLICE_STATE_CROSS:
1297 # Update other slice's cross according to the new focal point from 1298 # Update other slice's cross according to the new focal point from
1298 # the actual orientation. 1299 # the actual orientation.
1299 - focal_point = self.cross.GetFocalPoint()  
1300 - Publisher.sendMessage('Update cross position', position=focal_point) 1300 + x, y, z = self.cross.GetFocalPoint()
  1301 + Publisher.sendMessage('Update cross position', arg=None, position=(x, y, z, 0., 0., 0.))
1301 Publisher.sendMessage('Update slice viewer') 1302 Publisher.sendMessage('Update slice viewer')
1302 else: 1303 else:
1303 self.interactor.Render() 1304 self.interactor.Render()
@@ -1509,7 +1510,6 @@ class Viewer(wx.Panel): @@ -1509,7 +1510,6 @@ class Viewer(wx.Panel):
1509 1510
1510 def UpdateCross(self, coord): 1511 def UpdateCross(self, coord):
1511 self.cross.SetFocalPoint(coord) 1512 self.cross.SetFocalPoint(coord)
1512 - Publisher.sendMessage('Set ball reference position', position=self.cross.GetFocalPoint())  
1513 Publisher.sendMessage('Co-registered points', arg=None, position=(coord[0], coord[1], coord[2], 0., 0., 0.)) 1513 Publisher.sendMessage('Co-registered points', arg=None, position=(coord[0], coord[1], coord[2], 0., 0., 0.))
1514 self.OnScrollBar() 1514 self.OnScrollBar()
1515 self.interactor.Render() 1515 self.interactor.Render()
invesalius/data/viewer_volume.py
@@ -19,9 +19,10 @@ @@ -19,9 +19,10 @@
19 # detalhes. 19 # detalhes.
20 #-------------------------------------------------------------------------- 20 #--------------------------------------------------------------------------
21 21
22 -from math import cos, sin 22 +# from math import cos, sin
23 import os 23 import os
24 import sys 24 import sys
  25 +import time
25 26
26 import numpy as np 27 import numpy as np
27 from numpy.core.umath_tests import inner1d 28 from numpy.core.umath_tests import inner1d
@@ -36,6 +37,7 @@ from imageio import imsave @@ -36,6 +37,7 @@ from imageio import imsave
36 37
37 import invesalius.constants as const 38 import invesalius.constants as const
38 import invesalius.data.bases as bases 39 import invesalius.data.bases as bases
  40 +import invesalius.data.slice_ as sl
39 import invesalius.data.transformations as tr 41 import invesalius.data.transformations as tr
40 import invesalius.data.vtk_utils as vtku 42 import invesalius.data.vtk_utils as vtku
41 import invesalius.project as prj 43 import invesalius.project as prj
@@ -168,6 +170,10 @@ class Viewer(wx.Panel): @@ -168,6 +170,10 @@ class Viewer(wx.Panel):
168 #self.pTarget = [0., 0., 0.] 170 #self.pTarget = [0., 0., 0.]
169 171
170 # self.obj_axes = None 172 # self.obj_axes = None
  173 + self.x_actor = None
  174 + self.y_actor = None
  175 + self.z_actor = None
  176 + self.mark_actor = None
171 177
172 self._mode_cross = False 178 self._mode_cross = False
173 self._to_show_ball = 0 179 self._to_show_ball = 0
@@ -188,12 +194,31 @@ class Viewer(wx.Panel): @@ -188,12 +194,31 @@ class Viewer(wx.Panel):
188 self.anglethreshold = const.COIL_ANGLES_THRESHOLD 194 self.anglethreshold = const.COIL_ANGLES_THRESHOLD
189 self.distthreshold = const.COIL_COORD_THRESHOLD 195 self.distthreshold = const.COIL_COORD_THRESHOLD
190 196
  197 + # for DTI support tests
  198 + # self.ntimes = False
  199 + # self._to_show_stream = True
  200 + # data_dir = b'C:\Users\deoliv1\OneDrive\data\dti'
  201 + # nii_path = b'sub-P0_dwi_FOD.nii'
  202 + # trk_path = os.path.join(data_dir, nii_path)
  203 + # self.tracker_FOD = Trekker.tracker(trk_path)
  204 + # proj = prj.Project()
  205 + # self.affine = np.identity(4)
  206 + self.actor_tracts = None
  207 + self.actor_peel = None
  208 + self.seed_offset = const.SEED_OFFSET
  209 + # Publisher.sendMessage('Get affine matrix')
  210 +
  211 + # initialize Trekker parameters
  212 + slic = sl.Slice()
  213 + affine = slic.affine
  214 + self.affine_vtk = vtku.numpy_to_vtkMatrix4x4(affine)
  215 +
191 def __bind_events(self): 216 def __bind_events(self):
192 Publisher.subscribe(self.LoadActor, 217 Publisher.subscribe(self.LoadActor,
193 'Load surface actor into viewer') 218 'Load surface actor into viewer')
194 Publisher.subscribe(self.RemoveActor, 219 Publisher.subscribe(self.RemoveActor,
195 'Remove surface actor from viewer') 220 'Remove surface actor from viewer')
196 - Publisher.subscribe(self.OnShowSurface, 'Show surface') 221 + # Publisher.subscribe(self.OnShowSurface, 'Show surface')
197 Publisher.subscribe(self.UpdateRender, 222 Publisher.subscribe(self.UpdateRender,
198 'Render volume viewer') 223 'Render volume viewer')
199 Publisher.subscribe(self.ChangeBackgroundColour, 224 Publisher.subscribe(self.ChangeBackgroundColour,
@@ -225,7 +250,8 @@ class Viewer(wx.Panel): @@ -225,7 +250,8 @@ class Viewer(wx.Panel):
225 Publisher.subscribe(self.LoadSlicePlane, 'Load slice plane') 250 Publisher.subscribe(self.LoadSlicePlane, 'Load slice plane')
226 251
227 Publisher.subscribe(self.ResetCamClippingRange, 'Reset cam clipping range') 252 Publisher.subscribe(self.ResetCamClippingRange, 'Reset cam clipping range')
228 - Publisher.subscribe(self.SetVolumeCamera, 'Co-registered points') 253 + # Publisher.subscribe(self.SetVolumeCamera, 'Update cross position')
  254 + # Publisher.subscribe(self.SetVolumeCamera, 'Co-registered points')
229 # Publisher.subscribe(self.SetVolumeCamera, 'Set camera in volume') 255 # Publisher.subscribe(self.SetVolumeCamera, 'Set camera in volume')
230 Publisher.subscribe(self.SetVolumeCameraState, 'Update volume camera state') 256 Publisher.subscribe(self.SetVolumeCameraState, 'Update volume camera state')
231 257
@@ -255,8 +281,8 @@ class Viewer(wx.Panel): @@ -255,8 +281,8 @@ class Viewer(wx.Panel):
255 281
256 Publisher.subscribe(self.RemoveVolume, 'Remove Volume') 282 Publisher.subscribe(self.RemoveVolume, 'Remove Volume')
257 283
258 - Publisher.subscribe(self.SetBallReferencePosition,  
259 - 'Set ball reference position') 284 + Publisher.subscribe(self.UpdateCameraBallPosition,
  285 + 'Update cross position')
260 Publisher.subscribe(self._check_ball_reference, 'Enable style') 286 Publisher.subscribe(self._check_ball_reference, 'Enable style')
261 Publisher.subscribe(self._uncheck_ball_reference, 'Disable style') 287 Publisher.subscribe(self._uncheck_ball_reference, 'Disable style')
262 288
@@ -283,11 +309,17 @@ class Viewer(wx.Panel): @@ -283,11 +309,17 @@ class Viewer(wx.Panel):
283 Publisher.subscribe(self.OnUpdateObjectTargetGuide, 'Update object matrix') 309 Publisher.subscribe(self.OnUpdateObjectTargetGuide, 'Update object matrix')
284 Publisher.subscribe(self.OnUpdateTargetCoordinates, 'Update target') 310 Publisher.subscribe(self.OnUpdateTargetCoordinates, 'Update target')
285 Publisher.subscribe(self.OnRemoveTarget, 'Disable or enable coil tracker') 311 Publisher.subscribe(self.OnRemoveTarget, 'Disable or enable coil tracker')
286 - # Publisher.subscribe(self.UpdateObjectTargetView, 'Co-registered points')  
287 Publisher.subscribe(self.OnTargetMarkerTransparency, 'Set target transparency') 312 Publisher.subscribe(self.OnTargetMarkerTransparency, 'Set target transparency')
288 Publisher.subscribe(self.OnUpdateAngleThreshold, 'Update angle threshold') 313 Publisher.subscribe(self.OnUpdateAngleThreshold, 'Update angle threshold')
289 Publisher.subscribe(self.OnUpdateDistThreshold, 'Update dist threshold') 314 Publisher.subscribe(self.OnUpdateDistThreshold, 'Update dist threshold')
290 315
  316 + Publisher.subscribe(self.OnUpdateTracts, 'Update tracts')
  317 + Publisher.subscribe(self.OnRemoveTracts, 'Remove tracts')
  318 + Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset')
  319 + Publisher.subscribe(self.UpdateMarkerOffsetState, 'Update marker offset state')
  320 + Publisher.subscribe(self.UpdateMarkerOffsetPosition, 'Update marker offset')
  321 + Publisher.subscribe(self.AddPeeledSurface, 'Update peel')
  322 +
291 def SetStereoMode(self, mode): 323 def SetStereoMode(self, mode):
292 ren_win = self.interactor.GetRenderWindow() 324 ren_win = self.interactor.GetRenderWindow()
293 325
@@ -319,13 +351,23 @@ class Viewer(wx.Panel): @@ -319,13 +351,23 @@ class Viewer(wx.Panel):
319 def _check_ball_reference(self, style): 351 def _check_ball_reference(self, style):
320 if style == const.SLICE_STATE_CROSS: 352 if style == const.SLICE_STATE_CROSS:
321 self._mode_cross = True 353 self._mode_cross = True
322 - self._check_and_set_ball_visibility() 354 + # self._check_and_set_ball_visibility()
  355 + self._ball_ref_visibility = True
  356 + # if self._to_show_ball:
  357 + if not self.ball_actor:
  358 + self.CreateBallReference()
  359 +
323 self.interactor.Render() 360 self.interactor.Render()
324 361
325 def _uncheck_ball_reference(self, style): 362 def _uncheck_ball_reference(self, style):
326 if style == const.SLICE_STATE_CROSS: 363 if style == const.SLICE_STATE_CROSS:
327 self._mode_cross = False 364 self._mode_cross = False
328 - self.RemoveBallReference() 365 + # self.RemoveBallReference()
  366 + self._ball_ref_visibility = False
  367 + if self.ball_actor:
  368 + self.ren.RemoveActor(self.ball_actor)
  369 + self.ball_actor = None
  370 +
329 self.interactor.Render() 371 self.interactor.Render()
330 372
331 def OnSensors(self, probe_id, ref_id, obj_id=0): 373 def OnSensors(self, probe_id, ref_id, obj_id=0):
@@ -385,12 +427,12 @@ class Viewer(wx.Panel): @@ -385,12 +427,12 @@ class Viewer(wx.Panel):
385 self.probe = self.ref = self.obj = False 427 self.probe = self.ref = self.obj = False
386 self.interactor.Render() 428 self.interactor.Render()
387 429
388 - def OnShowSurface(self, index, visibility):  
389 - if visibility:  
390 - self._to_show_ball += 1  
391 - else:  
392 - self._to_show_ball -= 1  
393 - self._check_and_set_ball_visibility() 430 + # def OnShowSurface(self, index, visibility):
  431 + # if visibility:
  432 + # self._to_show_ball += 1
  433 + # else:
  434 + # self._to_show_ball -= 1
  435 + # self._check_and_set_ball_visibility()
394 436
395 def OnStartSeed(self): 437 def OnStartSeed(self):
396 self.seed_points = [] 438 self.seed_points = []
@@ -485,8 +527,8 @@ class Viewer(wx.Panel): @@ -485,8 +527,8 @@ class Viewer(wx.Panel):
485 if (volumes.GetNumberOfItems()): 527 if (volumes.GetNumberOfItems()):
486 self.ren.RemoveVolume(volumes.GetLastProp()) 528 self.ren.RemoveVolume(volumes.GetLastProp())
487 self.interactor.Render() 529 self.interactor.Render()
488 - self._to_show_ball -= 1  
489 - self._check_and_set_ball_visibility() 530 + # self._to_show_ball -= 1
  531 + # self._check_and_set_ball_visibility()
490 532
491 def RemoveActors(self, actors): 533 def RemoveActors(self, actors):
492 "Remove a list of actors" 534 "Remove a list of actors"
@@ -534,11 +576,11 @@ class Viewer(wx.Panel): @@ -534,11 +576,11 @@ class Viewer(wx.Panel):
534 Markers created by navigation tools and rendered in volume viewer. 576 Markers created by navigation tools and rendered in volume viewer.
535 """ 577 """
536 self.ball_id = ball_id 578 self.ball_id = ball_id
537 - x, y, z = bases.flip_x(coord) 579 + coord_flip = bases.flip_x_m(coord)[:3, 0]
538 580
539 ball_ref = vtk.vtkSphereSource() 581 ball_ref = vtk.vtkSphereSource()
540 ball_ref.SetRadius(size) 582 ball_ref.SetRadius(size)
541 - ball_ref.SetCenter(x, y, z) 583 + ball_ref.SetCenter(coord_flip)
542 584
543 mapper = vtk.vtkPolyDataMapper() 585 mapper = vtk.vtkPolyDataMapper()
544 mapper.SetInputConnection(ball_ref.GetOutputPort()) 586 mapper.SetInputConnection(ball_ref.GetOutputPort())
@@ -557,6 +599,35 @@ class Viewer(wx.Panel): @@ -557,6 +599,35 @@ class Viewer(wx.Panel):
557 #self.UpdateRender() 599 #self.UpdateRender()
558 self.Refresh() 600 self.Refresh()
559 601
  602 + def add_marker(self, coord, color):
  603 + """Simplified version for creating a spherical marker in the 3D scene
  604 +
  605 + :param coord:
  606 + :param color:
  607 + :return: vtkActor
  608 + """
  609 +
  610 + # x, y, z = coord
  611 +
  612 + ball_ref = vtk.vtkSphereSource()
  613 + ball_ref.SetRadius(2)
  614 + ball_ref.SetCenter(coord)
  615 +
  616 + mapper = vtk.vtkPolyDataMapper()
  617 + mapper.SetInputConnection(ball_ref.GetOutputPort())
  618 +
  619 + prop = vtk.vtkProperty()
  620 + prop.SetColor(color)
  621 +
  622 + actor = vtk.vtkActor()
  623 + actor.SetMapper(mapper)
  624 + actor.SetProperty(prop)
  625 + actor.GetProperty().SetOpacity(.5)
  626 +
  627 + # ren.AddActor(actor)
  628 +
  629 + return actor
  630 +
560 def HideAllMarkers(self, indexes): 631 def HideAllMarkers(self, indexes):
561 ballid = indexes 632 ballid = indexes
562 for i in range(0, ballid): 633 for i in range(0, ballid):
@@ -610,7 +681,6 @@ class Viewer(wx.Panel): @@ -610,7 +681,6 @@ class Viewer(wx.Panel):
610 self.staticballs[index].GetProperty().SetColor(color) 681 self.staticballs[index].GetProperty().SetColor(color)
611 self.Refresh() 682 self.Refresh()
612 683
613 -  
614 def OnTargetMarkerTransparency(self, status, index): 684 def OnTargetMarkerTransparency(self, status, index):
615 if status: 685 if status:
616 self.staticballs[index].GetProperty().SetOpacity(1) 686 self.staticballs[index].GetProperty().SetOpacity(1)
@@ -1104,7 +1174,6 @@ class Viewer(wx.Panel): @@ -1104,7 +1174,6 @@ class Viewer(wx.Panel):
1104 cam.SetFocalPoint(cam_focus) 1174 cam.SetFocalPoint(cam_focus)
1105 cam.SetPosition(cam_pos) 1175 cam.SetPosition(cam_pos)
1106 1176
1107 -  
1108 def CreateBallReference(self): 1177 def CreateBallReference(self):
1109 """ 1178 """
1110 Red sphere on volume visualization to reference center of 1179 Red sphere on volume visualization to reference center of
@@ -1129,31 +1198,14 @@ class Viewer(wx.Panel): @@ -1129,31 +1198,14 @@ class Viewer(wx.Panel):
1129 1198
1130 self.ren.AddActor(self.ball_actor) 1199 self.ren.AddActor(self.ball_actor)
1131 1200
1132 - def ActivateBallReference(self):  
1133 - self._mode_cross = True  
1134 - self._ball_ref_visibility = True  
1135 - if self._to_show_ball:  
1136 - if not self.ball_actor:  
1137 - self.CreateBallReference()  
1138 -  
1139 - def RemoveBallReference(self):  
1140 - self._mode_cross = False  
1141 - self._ball_ref_visibility = False  
1142 - if self.ball_actor:  
1143 - self.ren.RemoveActor(self.ball_actor)  
1144 - self.ball_actor = None 1201 + def UpdateCameraBallPosition(self, arg, position):
  1202 + coord_flip = bases.flip_x_m(position[:3])[:3, 0]
  1203 + self.ball_actor.SetPosition(coord_flip)
  1204 + self.SetVolumeCamera(coord_flip)
1145 1205
1146 def SetBallReferencePosition(self, position): 1206 def SetBallReferencePosition(self, position):
1147 - if self._to_show_ball:  
1148 - if not self.ball_actor:  
1149 - self.ActivateBallReference()  
1150 -  
1151 - coord = position  
1152 - x, y, z = bases.flip_x(coord)  
1153 - self.ball_actor.SetPosition(x, y, z)  
1154 -  
1155 - else:  
1156 - self.RemoveBallReference() 1207 + coord_flip = bases.flip_x_m(position[:3])[:3, 0]
  1208 + self.ball_actor.SetPosition(coord_flip)
1157 1209
1158 def CreateObjectPolyData(self, filename): 1210 def CreateObjectPolyData(self, filename):
1159 """ 1211 """
@@ -1225,7 +1277,14 @@ class Viewer(wx.Panel): @@ -1225,7 +1277,14 @@ class Viewer(wx.Panel):
1225 self.obj_actor.GetProperty().SetOpacity(.4) 1277 self.obj_actor.GetProperty().SetOpacity(.4)
1226 self.obj_actor.SetVisibility(0) 1278 self.obj_actor.SetVisibility(0)
1227 1279
  1280 + self.x_actor = self.add_line([0., 0., 0.], [1., 0., 0.], color=[.0, .0, 1.0])
  1281 + self.y_actor = self.add_line([0., 0., 0.], [0., 1., 0.], color=[.0, 1.0, .0])
  1282 + self.z_actor = self.add_line([0., 0., 0.], [0., 0., 1.], color=[1.0, .0, .0])
  1283 +
1228 self.ren.AddActor(self.obj_actor) 1284 self.ren.AddActor(self.obj_actor)
  1285 + self.ren.AddActor(self.x_actor)
  1286 + self.ren.AddActor(self.y_actor)
  1287 + self.ren.AddActor(self.z_actor)
1229 1288
1230 # self.obj_axes = vtk.vtkAxesActor() 1289 # self.obj_axes = vtk.vtkAxesActor()
1231 # self.obj_axes.SetShaftTypeToCylinder() 1290 # self.obj_axes.SetShaftTypeToCylinder()
@@ -1236,25 +1295,88 @@ class Viewer(wx.Panel): @@ -1236,25 +1295,88 @@ class Viewer(wx.Panel):
1236 1295
1237 # self.ren.AddActor(self.obj_axes) 1296 # self.ren.AddActor(self.obj_axes)
1238 1297
1239 - def OnNavigationStatus(self, status):  
1240 - self.nav_status = status 1298 + def add_line(self, p1, p2, color=[0.0, 0.0, 1.0]):
  1299 + line = vtk.vtkLineSource()
  1300 + line.SetPoint1(p1)
  1301 + line.SetPoint2(p2)
  1302 +
  1303 + mapper = vtk.vtkPolyDataMapper()
  1304 + mapper.SetInputConnection(line.GetOutputPort())
  1305 +
  1306 + actor = vtk.vtkActor()
  1307 + actor.SetMapper(mapper)
  1308 + actor.GetProperty().SetColor(color)
  1309 +
  1310 + return actor
  1311 +
  1312 + def AddPeeledSurface(self, flag, actor):
  1313 + if self.actor_peel:
  1314 + self.ren.RemoveActor(self.actor_peel)
  1315 + self.actor_peel = None
  1316 + if flag and actor:
  1317 + self.ren.AddActor(actor)
  1318 + self.actor_peel = actor
  1319 + self.Refresh()
  1320 +
  1321 + def OnNavigationStatus(self, nav_status, vis_status):
  1322 + self.nav_status = nav_status
  1323 + self.tracts_status = vis_status[1]
1241 self.pTarget = self.CenterOfMass() 1324 self.pTarget = self.CenterOfMass()
1242 - if self.obj_actor and self.nav_status:  
1243 - self.obj_actor.SetVisibility(self.obj_state)  
1244 - if not self.obj_state:  
1245 - self.Refresh() 1325 +
  1326 + if self.nav_status:
  1327 + if self.obj_actor:
  1328 + self.obj_actor.SetVisibility(self.obj_state)
  1329 + self.x_actor.SetVisibility(self.obj_state)
  1330 + self.y_actor.SetVisibility(self.obj_state)
  1331 + self.z_actor.SetVisibility(self.obj_state)
  1332 +
  1333 + self.Refresh()
  1334 +
  1335 + def UpdateSeedOffset(self, data):
  1336 + self.seed_offset = data
  1337 +
  1338 + def UpdateMarkerOffsetState(self, create=False):
  1339 + if create:
  1340 + if not self.mark_actor:
  1341 + self.mark_actor = self.add_marker([0., 0., 0.], color=[0., 1., 1.])
  1342 + self.ren.AddActor(self.mark_actor)
  1343 + else:
  1344 + if self.mark_actor:
  1345 + self.ren.RemoveActor(self.mark_actor)
  1346 + self.mark_actor = None
  1347 + self.Refresh()
  1348 +
  1349 + def CreateMarkerOffset(self):
  1350 + self.mark_actor = self.add_marker([0., 0., 0.], color=[0., 1., 1.])
  1351 + self.ren.AddActor(self.mark_actor)
  1352 + self.Refresh()
  1353 +
  1354 + def UpdateMarkerOffsetPosition(self, coord_offset):
  1355 + self.mark_actor.SetPosition(coord_offset)
  1356 + self.Refresh()
1246 1357
1247 def UpdateObjectOrientation(self, m_img, coord): 1358 def UpdateObjectOrientation(self, m_img, coord):
1248 - m_img[:3, -1] = np.asmatrix(bases.flip_x_m((m_img[0, -1], m_img[1, -1], m_img[2, -1]))).reshape([3, 1]) 1359 + # print("Update object orientation")
1249 1360
1250 - m_img_vtk = vtk.vtkMatrix4x4() 1361 + m_img_flip = m_img.copy()
  1362 + m_img_flip[1, -1] = -m_img_flip[1, -1]
1251 1363
1252 - for row in range(0, 4):  
1253 - for col in range(0, 4):  
1254 - m_img_vtk.SetElement(row, col, m_img[row, col]) 1364 + # translate coregistered coordinate to display a marker where Trekker seed is computed
  1365 + # coord_offset = m_img_flip[:3, -1] - self.seed_offset * m_img_flip[:3, 2]
  1366 +
  1367 + # print("m_img copy in viewer_vol: {}".format(m_img_copy))
  1368 +
  1369 + # m_img[:3, 0] is from posterior to anterior direction of the coil
  1370 + # m_img[:3, 1] is from left to right direction of the coil
  1371 + # m_img[:3, 2] is from bottom to up direction of the coil
  1372 +
  1373 + m_img_vtk = vtku.numpy_to_vtkMatrix4x4(m_img_flip)
1255 1374
1256 self.obj_actor.SetUserMatrix(m_img_vtk) 1375 self.obj_actor.SetUserMatrix(m_img_vtk)
1257 # self.obj_axes.SetUserMatrix(m_rot_vtk) 1376 # self.obj_axes.SetUserMatrix(m_rot_vtk)
  1377 + self.x_actor.SetUserMatrix(m_img_vtk)
  1378 + self.y_actor.SetUserMatrix(m_img_vtk)
  1379 + self.z_actor.SetUserMatrix(m_img_vtk)
1258 1380
1259 self.Refresh() 1381 self.Refresh()
1260 1382
@@ -1267,13 +1389,42 @@ class Viewer(wx.Panel): @@ -1267,13 +1389,42 @@ class Viewer(wx.Panel):
1267 else: 1389 else:
1268 if self.obj_actor: 1390 if self.obj_actor:
1269 self.ren.RemoveActor(self.obj_actor) 1391 self.ren.RemoveActor(self.obj_actor)
  1392 + self.ren.RemoveActor(self.x_actor)
  1393 + self.ren.RemoveActor(self.y_actor)
  1394 + self.ren.RemoveActor(self.z_actor)
  1395 + self.ren.RemoveActor(self.mark_actor)
1270 self.obj_actor = None 1396 self.obj_actor = None
  1397 + self.x_actor = None
  1398 + self.y_actor = None
  1399 + self.z_actor = None
  1400 + self.mark_actor = None
1271 self.Refresh() 1401 self.Refresh()
1272 1402
1273 def UpdateShowObjectState(self, state): 1403 def UpdateShowObjectState(self, state):
1274 self.obj_state = state 1404 self.obj_state = state
1275 if self.obj_actor and not self.obj_state: 1405 if self.obj_actor and not self.obj_state:
1276 self.obj_actor.SetVisibility(self.obj_state) 1406 self.obj_actor.SetVisibility(self.obj_state)
  1407 + self.x_actor.SetVisibility(self.obj_state)
  1408 + self.y_actor.SetVisibility(self.obj_state)
  1409 + self.z_actor.SetVisibility(self.obj_state)
  1410 +
  1411 + self.Refresh()
  1412 +
  1413 + def OnUpdateTracts(self, evt=None, flag=None, actor=None, root=None, affine_vtk=None, count=0):
  1414 + mapper = vtk.vtkCompositePolyDataMapper2()
  1415 + mapper.SetInputDataObject(root)
  1416 +
  1417 + self.actor_tracts = vtk.vtkActor()
  1418 + self.actor_tracts.SetMapper(mapper)
  1419 + self.actor_tracts.SetUserMatrix(affine_vtk)
  1420 +
  1421 + self.ren.AddActor(self.actor_tracts)
  1422 + self.Refresh()
  1423 +
  1424 + def OnRemoveTracts(self):
  1425 + if self.actor_tracts:
  1426 + self.ren.RemoveActor(self.actor_tracts)
  1427 + self.actor_tracts = None
1277 self.Refresh() 1428 self.Refresh()
1278 1429
1279 def __bind_events_wx(self): 1430 def __bind_events_wx(self):
@@ -1448,10 +1599,12 @@ class Viewer(wx.Panel): @@ -1448,10 +1599,12 @@ class Viewer(wx.Panel):
1448 def SetVolumeCameraState(self, camera_state): 1599 def SetVolumeCameraState(self, camera_state):
1449 self.camera_state = camera_state 1600 self.camera_state = camera_state
1450 1601
1451 - def SetVolumeCamera(self, arg, position): 1602 + # def SetVolumeCamera(self, arg, position):
  1603 + def SetVolumeCamera(self, cam_focus):
1452 if self.camera_state: 1604 if self.camera_state:
1453 # TODO: exclude dependency on initial focus 1605 # TODO: exclude dependency on initial focus
1454 - cam_focus = np.array(bases.flip_x(position[:3])) 1606 + # cam_focus = np.array(bases.flip_x(position[:3]))
  1607 + # cam_focus = np.array(bases.flip_x(position))
1455 cam = self.ren.GetActiveCamera() 1608 cam = self.ren.GetActiveCamera()
1456 1609
1457 if self.initial_focus is None: 1610 if self.initial_focus is None:
@@ -1545,16 +1698,16 @@ class Viewer(wx.Panel): @@ -1545,16 +1698,16 @@ class Viewer(wx.Panel):
1545 def OnShowRaycasting(self): 1698 def OnShowRaycasting(self):
1546 if not self.raycasting_volume: 1699 if not self.raycasting_volume:
1547 self.raycasting_volume = True 1700 self.raycasting_volume = True
1548 - self._to_show_ball += 1  
1549 - self._check_and_set_ball_visibility() 1701 + # self._to_show_ball += 1
  1702 + # self._check_and_set_ball_visibility()
1550 if self.on_wl: 1703 if self.on_wl:
1551 self.text.Show() 1704 self.text.Show()
1552 1705
1553 def OnHideRaycasting(self): 1706 def OnHideRaycasting(self):
1554 self.raycasting_volume = False 1707 self.raycasting_volume = False
1555 self.text.Hide() 1708 self.text.Hide()
1556 - self._to_show_ball -= 1  
1557 - self._check_and_set_ball_visibility() 1709 + # self._to_show_ball -= 1
  1710 + # self._check_and_set_ball_visibility()
1558 1711
1559 def OnSize(self, evt): 1712 def OnSize(self, evt):
1560 self.UpdateRender() 1713 self.UpdateRender()
@@ -1581,16 +1734,16 @@ class Viewer(wx.Panel): @@ -1581,16 +1734,16 @@ class Viewer(wx.Panel):
1581 1734
1582 #self.ShowOrientationCube() 1735 #self.ShowOrientationCube()
1583 self.interactor.Render() 1736 self.interactor.Render()
1584 - self._to_show_ball += 1  
1585 - self._check_and_set_ball_visibility() 1737 + # self._to_show_ball += 1
  1738 + # self._check_and_set_ball_visibility()
1586 1739
1587 def RemoveActor(self, actor): 1740 def RemoveActor(self, actor):
1588 utils.debug("RemoveActor") 1741 utils.debug("RemoveActor")
1589 ren = self.ren 1742 ren = self.ren
1590 ren.RemoveActor(actor) 1743 ren.RemoveActor(actor)
1591 self.interactor.Render() 1744 self.interactor.Render()
1592 - self._to_show_ball -= 1  
1593 - self._check_and_set_ball_visibility() 1745 + # self._to_show_ball -= 1
  1746 + # self._check_and_set_ball_visibility()
1594 1747
1595 def RemoveAllActor(self): 1748 def RemoveAllActor(self):
1596 utils.debug("RemoveAllActor") 1749 utils.debug("RemoveAllActor")
@@ -1602,7 +1755,8 @@ class Viewer(wx.Panel): @@ -1602,7 +1755,8 @@ class Viewer(wx.Panel):
1602 1755
1603 def LoadVolume(self, volume, colour, ww, wl): 1756 def LoadVolume(self, volume, colour, ww, wl):
1604 self.raycasting_volume = True 1757 self.raycasting_volume = True
1605 - self._to_show_ball += 1 1758 + # self._to_show_ball += 1
  1759 + # self._check_and_set_ball_visibility()
1606 1760
1607 self.light = self.ren.GetLights().GetNextItem() 1761 self.light = self.ren.GetLights().GetNextItem()
1608 1762
@@ -1622,15 +1776,14 @@ class Viewer(wx.Panel): @@ -1622,15 +1776,14 @@ class Viewer(wx.Panel):
1622 self.ren.ResetCamera() 1776 self.ren.ResetCamera()
1623 self.ren.ResetCameraClippingRange() 1777 self.ren.ResetCameraClippingRange()
1624 1778
1625 - self._check_and_set_ball_visibility()  
1626 self.UpdateRender() 1779 self.UpdateRender()
1627 1780
1628 def UnloadVolume(self, volume): 1781 def UnloadVolume(self, volume):
1629 self.ren.RemoveVolume(volume) 1782 self.ren.RemoveVolume(volume)
1630 del volume 1783 del volume
1631 self.raycasting_volume = False 1784 self.raycasting_volume = False
1632 - self._to_show_ball -= 1  
1633 - self._check_and_set_ball_visibility() 1785 + # self._to_show_ball -= 1
  1786 + # self._check_and_set_ball_visibility()
1634 1787
1635 def OnSetViewAngle(self, view): 1788 def OnSetViewAngle(self, view):
1636 self.SetViewAngle(view) 1789 self.SetViewAngle(view)
@@ -1777,16 +1930,17 @@ class Viewer(wx.Panel): @@ -1777,16 +1930,17 @@ class Viewer(wx.Panel):
1777 self.SetViewAngle(const.VOL_ISO) 1930 self.SetViewAngle(const.VOL_ISO)
1778 self.repositioned_coronal_plan = 1 1931 self.repositioned_coronal_plan = 1
1779 1932
1780 - def _check_and_set_ball_visibility(self):  
1781 - #TODO: When creating Raycasting volume and cross is pressed, it is not  
1782 - # automatically creating the ball reference.  
1783 - if self._mode_cross:  
1784 - if self._to_show_ball > 0 and not self._ball_ref_visibility:  
1785 - self.ActivateBallReference()  
1786 - self.interactor.Render()  
1787 - elif not self._to_show_ball and self._ball_ref_visibility:  
1788 - self.RemoveBallReference()  
1789 - self.interactor.Render() 1933 + # def _check_and_set_ball_visibility(self):
  1934 + # #TODO: When creating Raycasting volume and cross is pressed, it is not
  1935 + # # automatically creating the ball reference.
  1936 + # # print("mode_cross, show_ball, ball_vis ", self._mode_cross, self._to_show_ball, self._ball_ref_visibility)
  1937 + # if self._mode_cross:
  1938 + # if self._to_show_ball > 0 and not self._ball_ref_visibility:
  1939 + # self.ActivateBallReference()
  1940 + # self.interactor.Render()
  1941 + # elif not self._to_show_ball and self._ball_ref_visibility:
  1942 + # self.RemoveBallReference()
  1943 + # self.interactor.Render()
1790 1944
1791 class SlicePlane: 1945 class SlicePlane:
1792 def __init__(self): 1946 def __init__(self):
invesalius/data/vtk_utils.py
@@ -287,3 +287,21 @@ class TextZero(object): @@ -287,3 +287,21 @@ class TextZero(object):
287 # font.SetWeight(wx.FONTWEIGHT_BOLD) 287 # font.SetWeight(wx.FONTWEIGHT_BOLD)
288 font.SetSymbolicSize(self.symbolic_syze) 288 font.SetSymbolicSize(self.symbolic_syze)
289 canvas.draw_text(self.text, (x, y), font=font) 289 canvas.draw_text(self.text, (x, y), font=font)
  290 +
  291 +
  292 +def numpy_to_vtkMatrix4x4(affine):
  293 + """
  294 + Convert a numpy 4x4 array to a vtk 4x4 matrix
  295 + :param affine: 4x4 array
  296 + :return: vtkMatrix4x4 object representing the affine
  297 + """
  298 + # test for type and shape of affine matrix
  299 + # assert isinstance(affine, np.ndarray)
  300 + assert affine.shape == (4, 4)
  301 +
  302 + affine_vtk = vtk.vtkMatrix4x4()
  303 + for row in range(0, 4):
  304 + for col in range(0, 4):
  305 + affine_vtk.SetElement(row, col, affine[row, col])
  306 +
  307 + return affine_vtk
invesalius/gui/dialogs.py
@@ -387,6 +387,15 @@ def ShowImportOtherFilesDialog(id_type): @@ -387,6 +387,15 @@ def ShowImportOtherFilesDialog(id_type):
387 if id_type == const.ID_NIFTI_IMPORT: 387 if id_type == const.ID_NIFTI_IMPORT:
388 dlg.SetMessage(_("Import NIFTi 1 file")) 388 dlg.SetMessage(_("Import NIFTi 1 file"))
389 dlg.SetWildcard(WILDCARD_NIFTI) 389 dlg.SetWildcard(WILDCARD_NIFTI)
  390 + elif id_type == const.ID_TREKKER_MASK:
  391 + dlg.SetMessage(_("Import Trekker mask"))
  392 + dlg.SetWildcard(WILDCARD_NIFTI)
  393 + elif id_type == const.ID_TREKKER_IMG:
  394 + dlg.SetMessage(_("Import Trekker anatomical image"))
  395 + dlg.SetWildcard(WILDCARD_NIFTI)
  396 + elif id_type == const.ID_TREKKER_FOD:
  397 + dlg.SetMessage(_("Import Trekker FOD"))
  398 + dlg.SetWildcard(WILDCARD_NIFTI)
390 elif id_type == const.ID_PARREC_IMPORT: 399 elif id_type == const.ID_PARREC_IMPORT:
391 dlg.SetMessage(_("Import PAR/REC file")) 400 dlg.SetMessage(_("Import PAR/REC file"))
392 dlg.SetWildcard(WILDCARD_PARREC) 401 dlg.SetWildcard(WILDCARD_PARREC)
@@ -508,79 +517,11 @@ def ShowSaveAsProjectDialog(default_filename=None): @@ -508,79 +517,11 @@ def ShowSaveAsProjectDialog(default_filename=None):
508 return filename, wildcard == INV_COMPRESSED 517 return filename, wildcard == INV_COMPRESSED
509 518
510 519
511 -# Dialog for neuronavigation markers  
512 -def ShowSaveMarkersDialog(default_filename=None):  
513 - current_dir = os.path.abspath(".")  
514 - dlg = wx.FileDialog(None,  
515 - _("Save markers as..."), # title  
516 - "", # last used directory  
517 - default_filename,  
518 - _("Markers files (*.mks)|*.mks"),  
519 - wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)  
520 - # dlg.SetFilterIndex(0) # default is VTI  
521 -  
522 - filename = None  
523 - try:  
524 - if dlg.ShowModal() == wx.ID_OK:  
525 - filename = dlg.GetPath()  
526 - ok = 1  
527 - else:  
528 - ok = 0  
529 - except(wx._core.PyAssertionError): # TODO: fix win64  
530 - filename = dlg.GetPath()  
531 - ok = 1  
532 -  
533 - if (ok):  
534 - extension = "mks"  
535 - if sys.platform != 'win32':  
536 - if filename.split(".")[-1] != extension:  
537 - filename = filename + "." + extension  
538 -  
539 - os.chdir(current_dir)  
540 - return filename  
541 -  
542 -def ShowSaveCoordsDialog(default_filename=None):  
543 - current_dir = os.path.abspath(".")  
544 - dlg = wx.FileDialog(None,  
545 - _("Save coords as..."), # title  
546 - "", # last used directory  
547 - default_filename,  
548 - _("Coordinates files (*.csv)|*.csv"),  
549 - wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)  
550 - # dlg.SetFilterIndex(0) # default is VTI  
551 -  
552 - filename = None  
553 - try:  
554 - if dlg.ShowModal() == wx.ID_OK:  
555 - filename = dlg.GetPath()  
556 - ok = 1  
557 - else:  
558 - ok = 0  
559 - except(wx._core.PyAssertionError): # TODO: fix win64  
560 - filename = dlg.GetPath()  
561 - ok = 1 520 +def ShowLoadSaveDialog(message=_(u"Load File"), current_dir=os.path.abspath("."), style=wx.FD_OPEN | wx.FD_CHANGE_DIR,
  521 + wildcard=_("Registration files (*.obr)|*.obr"), default_filename="", save_ext=None):
562 522
563 - if (ok):  
564 - extension = "csv"  
565 - if sys.platform != 'win32':  
566 - if filename.split(".")[-1] != extension:  
567 - filename = filename + "." + extension  
568 -  
569 - os.chdir(current_dir)  
570 - return filename  
571 -  
572 -  
573 -def ShowLoadMarkersDialog():  
574 - current_dir = os.path.abspath(".")  
575 -  
576 - dlg = wx.FileDialog(None, message=_("Load markers"),  
577 - defaultDir="",  
578 - defaultFile="",  
579 - wildcard=_("Markers files (*.mks)|*.mks"),  
580 - style=wx.FD_OPEN|wx.FD_CHANGE_DIR)  
581 -  
582 - # inv3 filter is default  
583 - dlg.SetFilterIndex(0) 523 + dlg = wx.FileDialog(None, message=message, defaultDir="", defaultFile=default_filename,
  524 + wildcard=wildcard, style=style)
584 525
585 # Show the dialog and retrieve the user response. If it is the OK response, 526 # Show the dialog and retrieve the user response. If it is the OK response,
586 # process the data. 527 # process the data.
@@ -589,73 +530,25 @@ def ShowLoadMarkersDialog(): @@ -589,73 +530,25 @@ def ShowLoadMarkersDialog():
589 if dlg.ShowModal() == wx.ID_OK: 530 if dlg.ShowModal() == wx.ID_OK:
590 # This returns a Python list of files that were selected. 531 # This returns a Python list of files that were selected.
591 filepath = dlg.GetPath() 532 filepath = dlg.GetPath()
  533 + ok_press = 1
  534 + else:
  535 + ok_press = 0
592 except(wx._core.PyAssertionError): # FIX: win64 536 except(wx._core.PyAssertionError): # FIX: win64
593 filepath = dlg.GetPath() 537 filepath = dlg.GetPath()
  538 + ok_press = 1
594 539
595 - # Destroy the dialog. Don't do this until you are done with it!  
596 - # BAD things can happen otherwise!  
597 - dlg.Destroy()  
598 - os.chdir(current_dir)  
599 - return filepath  
600 -  
601 -  
602 -def ShowSaveRegistrationDialog(default_filename=None):  
603 - current_dir = os.path.abspath(".")  
604 - dlg = wx.FileDialog(None,  
605 - _("Save object registration as..."), # title  
606 - "", # last used directory  
607 - default_filename,  
608 - _("Registration files (*.obr)|*.obr"),  
609 - wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)  
610 - # dlg.SetFilterIndex(0) # default is VTI  
611 -  
612 - filename = None  
613 - try:  
614 - if dlg.ShowModal() == wx.ID_OK:  
615 - filename = dlg.GetPath()  
616 - ok = 1  
617 - else:  
618 - ok = 0  
619 - except(wx._core.PyAssertionError): # TODO: fix win64  
620 - filename = dlg.GetPath()  
621 - ok = 1  
622 -  
623 - if (ok):  
624 - extension = "obr" 540 + # fix the extension if set different than expected
  541 + if save_ext and ok_press:
  542 + extension = save_ext
625 if sys.platform != 'win32': 543 if sys.platform != 'win32':
626 - if filename.split(".")[-1] != extension:  
627 - filename = filename + "." + extension  
628 -  
629 - os.chdir(current_dir)  
630 - return filename  
631 -  
632 -  
633 -def ShowLoadRegistrationDialog():  
634 - current_dir = os.path.abspath(".")  
635 -  
636 - dlg = wx.FileDialog(None, message=_("Load object registration"),  
637 - defaultDir="",  
638 - defaultFile="",  
639 - wildcard=_("Registration files (*.obr)|*.obr"),  
640 - style=wx.FD_OPEN|wx.FD_CHANGE_DIR)  
641 -  
642 - # inv3 filter is default  
643 - dlg.SetFilterIndex(0)  
644 -  
645 - # Show the dialog and retrieve the user response. If it is the OK response,  
646 - # process the data.  
647 - filepath = None  
648 - try:  
649 - if dlg.ShowModal() == wx.ID_OK:  
650 - # This returns a Python list of files that were selected.  
651 - filepath = dlg.GetPath()  
652 - except(wx._core.PyAssertionError): # FIX: win64  
653 - filepath = dlg.GetPath() 544 + if filepath.split(".")[-1] != extension:
  545 + filepath = filepath + "." + extension
654 546
655 # Destroy the dialog. Don't do this until you are done with it! 547 # Destroy the dialog. Don't do this until you are done with it!
656 # BAD things can happen otherwise! 548 # BAD things can happen otherwise!
657 dlg.Destroy() 549 dlg.Destroy()
658 os.chdir(current_dir) 550 os.chdir(current_dir)
  551 +
659 return filepath 552 return filepath
660 553
661 554
@@ -920,31 +813,9 @@ def SurfaceSelectionRequiredForDuplication(): @@ -920,31 +813,9 @@ def SurfaceSelectionRequiredForDuplication():
920 813
921 814
922 # Dialogs for neuronavigation mode 815 # Dialogs for neuronavigation mode
923 -def InvalidFiducials():  
924 - msg = _("Fiducials are invalid. Select all coordinates.")  
925 - if sys.platform == 'darwin':  
926 - dlg = wx.MessageDialog(None, "", msg,  
927 - wx.ICON_INFORMATION | wx.OK)  
928 - else:  
929 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",  
930 - wx.ICON_INFORMATION | wx.OK)  
931 - dlg.ShowModal()  
932 - dlg.Destroy()  
933 - 816 +# ----------------------------------
934 817
935 -def InvalidObjectRegistration():  
936 - msg = _("Perform coil registration before navigation.")  
937 - if sys.platform == 'darwin':  
938 - dlg = wx.MessageDialog(None, "", msg,  
939 - wx.ICON_INFORMATION | wx.OK)  
940 - else:  
941 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",  
942 - wx.ICON_INFORMATION | wx.OK)  
943 - dlg.ShowModal()  
944 - dlg.Destroy()  
945 -  
946 -  
947 -def NavigationTrackerWarning(trck_id, lib_mode): 818 +def ShowNavigationTrackerWarning(trck_id, lib_mode):
948 """ 819 """
949 Spatial Tracker connection error 820 Spatial Tracker connection error
950 """ 821 """
@@ -976,85 +847,46 @@ def NavigationTrackerWarning(trck_id, lib_mode): @@ -976,85 +847,46 @@ def NavigationTrackerWarning(trck_id, lib_mode):
976 dlg.Destroy() 847 dlg.Destroy()
977 848
978 849
979 -def InvalidMarkersFile():  
980 - msg = _("The TXT file is invalid.")  
981 - if sys.platform == 'darwin':  
982 - dlg = wx.MessageDialog(None, "", msg,  
983 - wx.ICON_INFORMATION | wx.OK)  
984 - else:  
985 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",  
986 - wx.ICON_INFORMATION | wx.OK)  
987 - dlg.ShowModal()  
988 - dlg.Destroy()  
989 -  
990 -  
991 -def NoMarkerSelected():  
992 - msg = _("No data selected") 850 +def ShowEnterMarkerID(default):
  851 + msg = _("Edit marker ID")
993 if sys.platform == 'darwin': 852 if sys.platform == 'darwin':
994 - dlg = wx.MessageDialog(None, "", msg,  
995 - wx.ICON_INFORMATION | wx.OK) 853 + dlg = wx.TextEntryDialog(None, "", msg, defaultValue=default)
996 else: 854 else:
997 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",  
998 - wx.ICON_INFORMATION | wx.OK) 855 + dlg = wx.TextEntryDialog(None, msg, "InVesalius 3", value=default)
999 dlg.ShowModal() 856 dlg.ShowModal()
  857 + result = dlg.GetValue()
1000 dlg.Destroy() 858 dlg.Destroy()
  859 + return result
1001 860
1002 861
1003 -def DeleteAllMarkers():  
1004 - msg = _("Do you really want to delete all markers?") 862 +def ShowConfirmationDialog(msg=_('Proceed?')):
  863 + # msg = _("Do you want to delete all markers?")
1005 if sys.platform == 'darwin': 864 if sys.platform == 'darwin':
1006 dlg = wx.MessageDialog(None, "", msg, 865 dlg = wx.MessageDialog(None, "", msg,
1007 wx.OK | wx.CANCEL | wx.ICON_QUESTION) 866 wx.OK | wx.CANCEL | wx.ICON_QUESTION)
1008 else: 867 else:
1009 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator", 868 + dlg = wx.MessageDialog(None, msg, "InVesalius 3",
1010 wx.OK | wx.CANCEL | wx.ICON_QUESTION) 869 wx.OK | wx.CANCEL | wx.ICON_QUESTION)
1011 result = dlg.ShowModal() 870 result = dlg.ShowModal()
1012 dlg.Destroy() 871 dlg.Destroy()
1013 return result 872 return result
1014 873
1015 -def DeleteTarget():  
1016 - msg = _("Target deleted")  
1017 - if sys.platform == 'darwin':  
1018 - dlg = wx.MessageDialog(None, "", msg,  
1019 - wx.ICON_INFORMATION | wx.OK)  
1020 - else:  
1021 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",  
1022 - wx.ICON_INFORMATION | wx.OK)  
1023 - dlg.ShowModal()  
1024 - dlg.Destroy()  
1025 874
1026 -def NewTarget():  
1027 - msg = _("New target selected")  
1028 - if sys.platform == 'darwin':  
1029 - dlg = wx.MessageDialog(None, "", msg,  
1030 - wx.ICON_INFORMATION | wx.OK)  
1031 - else:  
1032 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",  
1033 - wx.ICON_INFORMATION | wx.OK)  
1034 - dlg.ShowModal()  
1035 - dlg.Destroy() 875 +def ShowColorDialog(color_current):
  876 + cdata = wx.ColourData()
  877 + cdata.SetColour(wx.Colour(color_current))
  878 + dlg = wx.ColourDialog(None, data=cdata)
  879 + dlg.GetColourData().SetChooseFull(True)
1036 880
1037 -def InvalidTargetID():  
1038 - msg = _("Sorry, you cannot use 'TARGET' ID")  
1039 - if sys.platform == 'darwin':  
1040 - dlg = wx.MessageDialog(None, "", msg,  
1041 - wx.ICON_INFORMATION | wx.OK) 881 + if dlg.ShowModal() == wx.ID_OK:
  882 + color_new = dlg.GetColourData().GetColour().Get(includeAlpha=False)
1042 else: 883 else:
1043 - dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",  
1044 - wx.ICON_INFORMATION | wx.OK)  
1045 - dlg.ShowModal()  
1046 - dlg.Destroy() 884 + color_new = None
1047 885
1048 -def EnterMarkerID(default):  
1049 - msg = _("Edit marker ID")  
1050 - if sys.platform == 'darwin':  
1051 - dlg = wx.TextEntryDialog(None, "", msg, defaultValue=default)  
1052 - else:  
1053 - dlg = wx.TextEntryDialog(None, msg, "InVesalius 3", value=default)  
1054 - dlg.ShowModal()  
1055 - result = dlg.GetValue()  
1056 dlg.Destroy() 886 dlg.Destroy()
1057 - return result 887 + return color_new
  888 +
  889 +# ----------------------------------
1058 890
1059 891
1060 class NewMask(wx.Dialog): 892 class NewMask(wx.Dialog):
@@ -3569,7 +3401,10 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3569,7 +3401,10 @@ class ObjectCalibrationDialog(wx.Dialog):
3569 else: 3401 else:
3570 coord = coord_raw[0, :] 3402 coord = coord_raw[0, :]
3571 else: 3403 else:
3572 - NavigationTrackerWarning(0, 'choose') 3404 + ShowNavigationTrackerWarning(0, 'choose')
  3405 +
  3406 + if btn_id == 3:
  3407 + coord = np.zeros([6,])
3573 3408
3574 # Update text controls with tracker coordinates 3409 # Update text controls with tracker coordinates
3575 if coord is not None or np.sum(coord) != 0.0: 3410 if coord is not None or np.sum(coord) != 0.0:
@@ -3582,7 +3417,7 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3582,7 +3417,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3582 self.ball_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0) 3417 self.ball_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0)
3583 self.Refresh() 3418 self.Refresh()
3584 else: 3419 else:
3585 - NavigationTrackerWarning(0, 'choose') 3420 + ShowNavigationTrackerWarning(0, 'choose')
3586 3421
3587 def OnChoiceRefMode(self, evt): 3422 def OnChoiceRefMode(self, evt):
3588 # When ref mode is changed the tracker coordinates are set to nan 3423 # When ref mode is changed the tracker coordinates are set to nan
invesalius/gui/frame.py
@@ -1185,12 +1185,13 @@ class MenuBar(wx.MenuBar): @@ -1185,12 +1185,13 @@ class MenuBar(wx.MenuBar):
1185 else: 1185 else:
1186 self.FindItemById(const.ID_GOTO_COORD).Enable(False) 1186 self.FindItemById(const.ID_GOTO_COORD).Enable(False)
1187 1187
1188 - def OnEnableNavigation(self, status): 1188 + def OnEnableNavigation(self, nav_status, vis_status):
1189 """ 1189 """
1190 Disable mode menu when navigation is on. 1190 Disable mode menu when navigation is on.
1191 - :param status: Navigation status 1191 + :param nav_status: Navigation status
  1192 + :param vis_status: Status of items visualization during navigation
1192 """ 1193 """
1193 - value = status 1194 + value = nav_status
1194 if value: 1195 if value:
1195 self.FindItemById(const.ID_MODE_NAVIGATION).Enable(False) 1196 self.FindItemById(const.ID_MODE_NAVIGATION).Enable(False)
1196 else: 1197 else:
invesalius/gui/task_navigator.py
@@ -18,10 +18,13 @@ @@ -18,10 +18,13 @@
18 #-------------------------------------------------------------------------- 18 #--------------------------------------------------------------------------
19 19
20 from functools import partial 20 from functools import partial
  21 +# import os
  22 +import queue
21 import sys 23 import sys
22 -import os 24 +import threading
23 25
24 import numpy as np 26 import numpy as np
  27 +# import Trekker
25 import wx 28 import wx
26 29
27 try: 30 try:
@@ -31,22 +34,23 @@ except ImportError: @@ -31,22 +34,23 @@ except ImportError:
31 import wx.lib.hyperlink as hl 34 import wx.lib.hyperlink as hl
32 import wx.lib.foldpanelbar as fpb 35 import wx.lib.foldpanelbar as fpb
33 36
  37 +import wx.lib.colourselect as csel
34 import wx.lib.masked.numctrl 38 import wx.lib.masked.numctrl
35 from pubsub import pub as Publisher 39 from pubsub import pub as Publisher
36 -import wx.lib.colourselect as csel  
37 -import wx.lib.platebtn as pbtn  
38 -  
39 -from math import cos, sin, pi  
40 from time import sleep 40 from time import sleep
41 41
42 -import invesalius.data.transformations as tr  
43 import invesalius.constants as const 42 import invesalius.constants as const
44 import invesalius.data.bases as db 43 import invesalius.data.bases as db
  44 +# import invesalius.data.brainmesh_handler as brain
45 import invesalius.data.coordinates as dco 45 import invesalius.data.coordinates as dco
46 import invesalius.data.coregistration as dcr 46 import invesalius.data.coregistration as dcr
  47 +import invesalius.data.slice_ as sl
47 import invesalius.data.trackers as dt 48 import invesalius.data.trackers as dt
  49 +import invesalius.data.tractography as dti
  50 +import invesalius.data.transformations as tr
48 import invesalius.data.trigger as trig 51 import invesalius.data.trigger as trig
49 import invesalius.data.record_coords as rec 52 import invesalius.data.record_coords as rec
  53 +import invesalius.data.vtk_utils as vtk_utils
50 import invesalius.gui.dialogs as dlg 54 import invesalius.gui.dialogs as dlg
51 from invesalius import utils 55 from invesalius import utils
52 56
@@ -136,7 +140,7 @@ class InnerFoldPanel(wx.Panel): @@ -136,7 +140,7 @@ class InnerFoldPanel(wx.Panel):
136 # Study this. 140 # Study this.
137 141
138 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, 142 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
139 - (10, 290), 0, fpb.FPB_SINGLE_FOLD) 143 + (10, 310), 0, fpb.FPB_SINGLE_FOLD)
140 # Fold panel style 144 # Fold panel style
141 style = fpb.CaptionBarStyle() 145 style = fpb.CaptionBarStyle()
142 style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) 146 style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V)
@@ -161,15 +165,23 @@ class InnerFoldPanel(wx.Panel): @@ -161,15 +165,23 @@ class InnerFoldPanel(wx.Panel):
161 leftSpacing=0, rightSpacing=0) 165 leftSpacing=0, rightSpacing=0)
162 166
163 # Fold 3 - Markers panel 167 # Fold 3 - Markers panel
164 - item = fold_panel.AddFoldPanel(_("Extra tools"), collapsed=True) 168 + item = fold_panel.AddFoldPanel(_("Markers"), collapsed=True)
165 mtw = MarkersPanel(item) 169 mtw = MarkersPanel(item)
166 170
167 fold_panel.ApplyCaptionStyle(item, style) 171 fold_panel.ApplyCaptionStyle(item, style)
168 fold_panel.AddFoldPanelWindow(item, mtw, spacing= 0, 172 fold_panel.AddFoldPanelWindow(item, mtw, spacing= 0,
169 leftSpacing=0, rightSpacing=0) 173 leftSpacing=0, rightSpacing=0)
170 174
171 - # Fold 4 - DBS 175 + # Fold 4 - Tractography panel
  176 + item = fold_panel.AddFoldPanel(_("Tractography"), collapsed=True)
  177 + otw = TractographyPanel(item)
172 178
  179 + fold_panel.ApplyCaptionStyle(item, style)
  180 + fold_panel.AddFoldPanelWindow(item, otw, spacing=0,
  181 + leftSpacing=0, rightSpacing=0)
  182 + item.Hide()
  183 +
  184 + # Fold 5 - DBS
173 self.dbs_item = fold_panel.AddFoldPanel(_("Deep Brain Stimulation"), collapsed=True) 185 self.dbs_item = fold_panel.AddFoldPanel(_("Deep Brain Stimulation"), collapsed=True)
174 dtw = DbsPanel(self.dbs_item) #Atribuir nova var, criar panel 186 dtw = DbsPanel(self.dbs_item) #Atribuir nova var, criar panel
175 187
@@ -239,8 +251,8 @@ class InnerFoldPanel(wx.Panel): @@ -239,8 +251,8 @@ class InnerFoldPanel(wx.Panel):
239 def OnHideDbs(self): 251 def OnHideDbs(self):
240 self.dbs_item.Hide() 252 self.dbs_item.Hide()
241 253
242 - def OnCheckStatus(self, status):  
243 - if status: 254 + def OnCheckStatus(self, nav_status, vis_status):
  255 + if nav_status:
244 self.checktrigger.Enable(False) 256 self.checktrigger.Enable(False)
245 self.checkobj.Enable(False) 257 self.checkobj.Enable(False)
246 else: 258 else:
@@ -295,6 +307,23 @@ class NeuronavigationPanel(wx.Panel): @@ -295,6 +307,23 @@ class NeuronavigationPanel(wx.Panel):
295 self.obj_reg = None 307 self.obj_reg = None
296 self.obj_reg_status = False 308 self.obj_reg_status = False
297 self.track_obj = False 309 self.track_obj = False
  310 + self.event = threading.Event()
  311 +
  312 + self.coord_queue = QueueCustom(maxsize=1)
  313 + # self.visualization_queue = QueueCustom(maxsize=1)
  314 + self.trigger_queue = QueueCustom(maxsize=1)
  315 + self.coord_tracts_queue = QueueCustom(maxsize=1)
  316 + self.tracts_queue = QueueCustom(maxsize=1)
  317 +
  318 + # Tractography parameters
  319 + self.trk_inp = None
  320 + self.trekker = None
  321 + self.n_threads = None
  322 + self.view_tracts = False
  323 + self.n_tracts = const.N_TRACTS
  324 + self.seed_offset = const.SEED_OFFSET
  325 + self.seed_radius = const.SEED_RADIUS
  326 + self.sleep_nav = const.SLEEP_NAVIGATION
298 327
299 self.tracker_id = const.DEFAULT_TRACKER 328 self.tracker_id = const.DEFAULT_TRACKER
300 self.ref_mode_id = const.DEFAULT_REF_MODE 329 self.ref_mode_id = const.DEFAULT_REF_MODE
@@ -405,10 +434,17 @@ class NeuronavigationPanel(wx.Panel): @@ -405,10 +434,17 @@ class NeuronavigationPanel(wx.Panel):
405 Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials') 434 Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials')
406 Publisher.subscribe(self.UpdateTriggerState, 'Update trigger state') 435 Publisher.subscribe(self.UpdateTriggerState, 'Update trigger state')
407 Publisher.subscribe(self.UpdateTrackObjectState, 'Update track object state') 436 Publisher.subscribe(self.UpdateTrackObjectState, 'Update track object state')
408 - Publisher.subscribe(self.UpdateImageCoordinates, 'Set ball reference position') 437 + Publisher.subscribe(self.UpdateImageCoordinates, 'Update cross position')
409 Publisher.subscribe(self.OnDisconnectTracker, 'Disconnect tracker') 438 Publisher.subscribe(self.OnDisconnectTracker, 'Disconnect tracker')
410 Publisher.subscribe(self.UpdateObjectRegistration, 'Update object registration') 439 Publisher.subscribe(self.UpdateObjectRegistration, 'Update object registration')
411 Publisher.subscribe(self.OnCloseProject, 'Close project data') 440 Publisher.subscribe(self.OnCloseProject, 'Close project data')
  441 + Publisher.subscribe(self.UpdateTrekkerObject, 'Update Trekker object')
  442 + Publisher.subscribe(self.UpdateNumTracts, 'Update number of tracts')
  443 + Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset')
  444 + Publisher.subscribe(self.UpdateSeedRadius, 'Update seed radius')
  445 + Publisher.subscribe(self.UpdateSleep, 'Update sleep')
  446 + Publisher.subscribe(self.UpdateNumberThreads, 'Update number of threads')
  447 + Publisher.subscribe(self.UpdateTractsVisualization, 'Update tracts visualization')
412 448
413 def LoadImageFiducials(self, marker_id, coord): 449 def LoadImageFiducials(self, marker_id, coord):
414 for n in const.BTNS_IMG_MKS: 450 for n in const.BTNS_IMG_MKS:
@@ -420,7 +456,37 @@ class NeuronavigationPanel(wx.Panel): @@ -420,7 +456,37 @@ class NeuronavigationPanel(wx.Panel):
420 for m in [0, 1, 2]: 456 for m in [0, 1, 2]:
421 self.numctrls_coord[btn_id][m].SetValue(coord[m]) 457 self.numctrls_coord[btn_id][m].SetValue(coord[m])
422 458
423 - def UpdateImageCoordinates(self, position): 459 + def UpdateFRE(self, fre):
  460 + # TODO: Exhibit FRE in a warning dialog and only starts navigation after user clicks ok
  461 + self.txtctrl_fre.SetValue(str(round(fre, 2)))
  462 + if fre <= 3:
  463 + self.txtctrl_fre.SetBackgroundColour('GREEN')
  464 + else:
  465 + self.txtctrl_fre.SetBackgroundColour('RED')
  466 +
  467 + def UpdateTrekkerObject(self, data):
  468 + # self.trk_inp = data
  469 + self.trekker = data
  470 +
  471 + def UpdateNumTracts(self, data):
  472 + self.n_tracts = data
  473 +
  474 + def UpdateSeedOffset(self, data):
  475 + self.seed_offset = data
  476 +
  477 + def UpdateSeedRadius(self, data):
  478 + self.seed_radius = data
  479 +
  480 + def UpdateSleep(self, data):
  481 + self.sleep_nav = data
  482 +
  483 + def UpdateNumberThreads(self, data):
  484 + self.n_threads = data
  485 +
  486 + def UpdateTractsVisualization(self, data):
  487 + self.view_tracts = data
  488 +
  489 + def UpdateImageCoordinates(self, arg, position):
424 # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication. 490 # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication.
425 self.current_coord = position 491 self.current_coord = position
426 for m in [0, 1, 2]: 492 for m in [0, 1, 2]:
@@ -473,7 +539,7 @@ class NeuronavigationPanel(wx.Panel): @@ -473,7 +539,7 @@ class NeuronavigationPanel(wx.Panel):
473 label=_("Tracker disconnected successfully")) 539 label=_("Tracker disconnected successfully"))
474 self.trk_init = dt.TrackerConnection(self.tracker_id, None, 'connect') 540 self.trk_init = dt.TrackerConnection(self.tracker_id, None, 'connect')
475 if not self.trk_init[0]: 541 if not self.trk_init[0]:
476 - dlg.NavigationTrackerWarning(self.tracker_id, self.trk_init[1]) 542 + dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1])
477 ctrl.SetSelection(0) 543 ctrl.SetSelection(0)
478 print("Tracker not connected!") 544 print("Tracker not connected!")
479 else: 545 else:
@@ -488,7 +554,7 @@ class NeuronavigationPanel(wx.Panel): @@ -488,7 +554,7 @@ class NeuronavigationPanel(wx.Panel):
488 self.trk_init = dt.TrackerConnection(self.tracker_id, trck, 'disconnect') 554 self.trk_init = dt.TrackerConnection(self.tracker_id, trck, 'disconnect')
489 if not self.trk_init[0]: 555 if not self.trk_init[0]:
490 if evt is not False: 556 if evt is not False:
491 - dlg.NavigationTrackerWarning(self.tracker_id, 'disconnect') 557 + dlg.ShowNavigationTrackerWarning(self.tracker_id, 'disconnect')
492 self.tracker_id = 0 558 self.tracker_id = 0
493 ctrl.SetSelection(self.tracker_id) 559 ctrl.SetSelection(self.tracker_id)
494 Publisher.sendMessage('Update status text in GUI', 560 Publisher.sendMessage('Update status text in GUI',
@@ -507,7 +573,7 @@ class NeuronavigationPanel(wx.Panel): @@ -507,7 +573,7 @@ class NeuronavigationPanel(wx.Panel):
507 self.tracker_id = choice 573 self.tracker_id = choice
508 self.trk_init = dt.TrackerConnection(self.tracker_id, None, 'connect') 574 self.trk_init = dt.TrackerConnection(self.tracker_id, None, 'connect')
509 if not self.trk_init[0]: 575 if not self.trk_init[0]:
510 - dlg.NavigationTrackerWarning(self.tracker_id, self.trk_init[1]) 576 + dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1])
511 self.tracker_id = 0 577 self.tracker_id = 0
512 ctrl.SetSelection(self.tracker_id) 578 ctrl.SetSelection(self.tracker_id)
513 579
@@ -549,7 +615,18 @@ class NeuronavigationPanel(wx.Panel): @@ -549,7 +615,18 @@ class NeuronavigationPanel(wx.Panel):
549 coord = None 615 coord = None
550 616
551 if self.trk_init and self.tracker_id: 617 if self.trk_init and self.tracker_id:
  618 + # if self.tracker_id == const.DEBUGTRACK:
  619 + # if btn_id == 3:
  620 + # coord1 = np.array([-120., 0., 0., 0., 0., 0.])
  621 + # elif btn_id == 4:
  622 + # coord1 = np.array([120., 0., 0., 0., 0., 0.])
  623 + # elif btn_id == 5:
  624 + # coord1 = np.array([0., 120., 0., 0., 0., 0.])
  625 + # coord2 = np.zeros([3, 6])
  626 + # coord_raw = np.vstack([coord1, coord2])
  627 + # else:
552 coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, self.ref_mode_id) 628 coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, self.ref_mode_id)
  629 +
553 if self.ref_mode_id: 630 if self.ref_mode_id:
554 coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :]) 631 coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :])
555 else: 632 else:
@@ -557,7 +634,7 @@ class NeuronavigationPanel(wx.Panel): @@ -557,7 +634,7 @@ class NeuronavigationPanel(wx.Panel):
557 coord[2] = -coord[2] 634 coord[2] = -coord[2]
558 635
559 else: 636 else:
560 - dlg.NavigationTrackerWarning(0, 'choose') 637 + dlg.ShowNavigationTrackerWarning(0, 'choose')
561 638
562 # Update number controls with tracker coordinates 639 # Update number controls with tracker coordinates
563 if coord is not None: 640 if coord is not None:
@@ -569,112 +646,152 @@ class NeuronavigationPanel(wx.Panel): @@ -569,112 +646,152 @@ class NeuronavigationPanel(wx.Panel):
569 btn_nav = btn[0] 646 btn_nav = btn[0]
570 choice_trck = btn[1] 647 choice_trck = btn[1]
571 choice_ref = btn[2] 648 choice_ref = btn[2]
  649 + errors = False
  650 +
  651 + # initialize jobs list
  652 + jobs_list = []
  653 + vis_components = [self.trigger_state, self.view_tracts]
  654 + vis_queues = [self.coord_queue, self.trigger_queue, self.tracts_queue]
572 655
573 nav_id = btn_nav.GetValue() 656 nav_id = btn_nav.GetValue()
574 - if nav_id: 657 + if not nav_id:
  658 + self.event.set()
  659 +
  660 + # print("coord unfinished: {}, queue {}", self.coord_queue.unfinished_tasks, self.coord_queue.qsize())
  661 + # print("coord_tracts unfinished: {}, queue {}", self.coord_tracts_queue.unfinished_tasks, self.coord_tracts_queue.qsize())
  662 + # print("tracts unfinished: {}, queue {}", self.tracts_queue.unfinished_tasks, self.tracts_queue.qsize())
  663 + self.coord_queue.clear()
  664 + # self.visualization_queue.clear()
  665 + if self.trigger_state:
  666 + self.trigger_queue.clear()
  667 + if self.view_tracts:
  668 + self.coord_tracts_queue.clear()
  669 + self.tracts_queue.clear()
  670 +
  671 + # print("coord after unfinished: {}, queue {}", self.coord_queue.unfinished_tasks, self.coord_queue.qsize())
  672 + # print("coord_tracts after unfinished: {}, queue {}", self.coord_tracts_queue.unfinished_tasks, self.coord_tracts_queue.qsize())
  673 + # print("tracts after unfinished: {}, queue {}", self.tracts_queue.unfinished_tasks, self.tracts_queue.qsize())
  674 + self.coord_queue.join()
  675 + # self.visualization_queue.join()
  676 + if self.trigger_state:
  677 + self.trigger_queue.join()
  678 + if self.view_tracts:
  679 + self.coord_tracts_queue.join()
  680 + self.tracts_queue.join()
  681 +
  682 + # print("coord join unfinished: {}, queue {}", self.coord_queue.unfinished_tasks, self.coord_queue.qsize())
  683 + # print("vis join unfinished: {}, queue {}", self.visualization_queue.unfinished_tasks, self.visualization_queue.qsize())
  684 +
  685 + tooltip = wx.ToolTip(_("Start neuronavigation"))
  686 + btn_nav.SetToolTip(tooltip)
  687 +
  688 + # Enable all navigation buttons
  689 + choice_ref.Enable(True)
  690 + choice_trck.Enable(True)
  691 + for btn_c in self.btns_coord:
  692 + btn_c.Enable(True)
  693 +
  694 + # if self.trigger_state:
  695 + # self.trigger.stop()
  696 +
  697 + Publisher.sendMessage("Navigation status", nav_status=False, vis_status=vis_components)
  698 +
  699 + else:
  700 +
575 if np.isnan(self.fiducials).any(): 701 if np.isnan(self.fiducials).any():
576 - dlg.InvalidFiducials() 702 + wx.MessageBox(_("Invalid fiducials, select all coordinates."), _("InVesalius 3"))
577 btn_nav.SetValue(False) 703 btn_nav.SetValue(False)
578 704
579 - elif not self.trk_init[0]:  
580 - dlg.NavigationTrackerWarning(0, 'choose') 705 + elif not self.trk_init[0] or not self.tracker_id:
  706 + dlg.ShowNavigationTrackerWarning(0, 'choose')
  707 + errors = True
581 708
582 else: 709 else:
  710 + if self.event.is_set():
  711 + self.event.clear()
  712 +
  713 + # prepare GUI for navigation
  714 + Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components)
  715 + Publisher.sendMessage("Toggle Cross", id=const.SLICE_STATE_CROSS)
  716 + Publisher.sendMessage("Hide current mask")
583 tooltip = wx.ToolTip(_("Stop neuronavigation")) 717 tooltip = wx.ToolTip(_("Stop neuronavigation"))
584 btn_nav.SetToolTip(tooltip) 718 btn_nav.SetToolTip(tooltip)
585 719
586 - # Disable all navigation buttons 720 + # disable all navigation buttons
587 choice_ref.Enable(False) 721 choice_ref.Enable(False)
588 choice_trck.Enable(False) 722 choice_trck.Enable(False)
589 for btn_c in self.btns_coord: 723 for btn_c in self.btns_coord:
590 btn_c.Enable(False) 724 btn_c.Enable(False)
591 725
592 - # fids_head_img = np.zeros([3, 3])  
593 - # for ic in range(0, 3):  
594 - # fids_head_img[ic, :] = np.asarray(db.flip_x_m(self.fiducials[ic, :]))  
595 - #  
596 - # m_head_aux, q_head_aux, m_inv_head_aux = db.base_creation(fids_head_img)  
597 - # m_head = np.asmatrix(np.identity(4))  
598 - # m_head[:3, :3] = m_head_aux[:3, :3]  
599 -  
600 - m, q1, minv = db.base_creation_old(self.fiducials[:3, :])  
601 - n, q2, ninv = db.base_creation_old(self.fiducials[3:, :])  
602 - 726 + # fiducials matrix
603 m_change = tr.affine_matrix_from_points(self.fiducials[3:, :].T, self.fiducials[:3, :].T, 727 m_change = tr.affine_matrix_from_points(self.fiducials[3:, :].T, self.fiducials[:3, :].T,
604 shear=False, scale=False) 728 shear=False, scale=False)
605 -  
606 - # coreg_data = [m_change, m_head]  
607 - 729 + # initialize spatial tracker parameters
608 tracker_mode = self.trk_init, self.tracker_id, self.ref_mode_id 730 tracker_mode = self.trk_init, self.tracker_id, self.ref_mode_id
609 - # FIXME: FRE is taking long to calculate so it updates on GUI delayed to navigation - I think its fixed  
610 - # TODO: Exhibit FRE in a warning dialog and only starts navigation after user clicks ok  
611 - fre = db.calculate_fre(self.fiducials, minv, n, q1, q2)  
612 -  
613 - self.txtctrl_fre.SetValue(str(round(fre, 2)))  
614 - if fre <= 3:  
615 - self.txtctrl_fre.SetBackgroundColour('GREEN')  
616 - else:  
617 - self.txtctrl_fre.SetBackgroundColour('RED')  
618 -  
619 - if self.trigger_state:  
620 - self.trigger = trig.Trigger(nav_id)  
621 731
622 - Publisher.sendMessage("Navigation status", status=True)  
623 - Publisher.sendMessage("Toggle Cross", id=const.SLICE_STATE_CROSS)  
624 - Publisher.sendMessage("Hide current mask") 732 + # compute fiducial registration error (FRE)
  733 + # this is the old way to compute the fre, left here to recheck if new works fine.
  734 + # fre = db.calculate_fre(self.fiducials, minv, n, q1, q2)
  735 + fre = db.calculate_fre_m(self.fiducials)
  736 + self.UpdateFRE(fre)
625 737
626 if self.track_obj: 738 if self.track_obj:
627 - if self.obj_reg_status: 739 + # if object tracking is selected
  740 + if not self.obj_reg_status:
  741 + # check if object registration was performed
  742 + wx.MessageBox(_("Perform coil registration before navigation."), _("InVesalius 3"))
  743 + errors = True
  744 + else:
  745 + # if object registration was correctly performed continue with navigation
628 # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix 746 # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix
629 obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg 747 obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg
630 748
631 - if self.trk_init and self.tracker_id:  
632 -  
633 - coreg_data = [m_change, obj_ref_mode]  
634 -  
635 - if self.ref_mode_id:  
636 - coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, self.ref_mode_id)  
637 - obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)  
638 - coreg_data.extend(obj_data)  
639 -  
640 - self.correg = dcr.CoregistrationObjectDynamic(coreg_data, nav_id, tracker_mode)  
641 - else:  
642 - coord_raw = np.array([None])  
643 - obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)  
644 - coreg_data.extend(obj_data)  
645 -  
646 - self.correg = dcr.CoregistrationObjectStatic(coreg_data, nav_id, tracker_mode) 749 + coreg_data = [m_change, obj_ref_mode]
647 750
  751 + if self.ref_mode_id:
  752 + coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, self.ref_mode_id)
648 else: 753 else:
649 - dlg.NavigationTrackerWarning(0, 'choose')  
650 -  
651 - else:  
652 - dlg.InvalidObjectRegistration()  
653 -  
654 - else:  
655 - coreg_data = [m_change, 0]  
656 - if self.ref_mode_id:  
657 - # self.correg = dcr.CoregistrationDynamic_old(bases_coreg, nav_id, tracker_mode)  
658 - self.correg = dcr.CoregistrationDynamic(coreg_data, nav_id, tracker_mode)  
659 - else:  
660 - self.correg = dcr.CoregistrationStatic(coreg_data, nav_id, tracker_mode) 754 + coord_raw = np.array([None])
661 755
662 - else:  
663 - tooltip = wx.ToolTip(_("Start neuronavigation"))  
664 - btn_nav.SetToolTip(tooltip)  
665 -  
666 - # Enable all navigation buttons  
667 - choice_ref.Enable(True)  
668 - choice_trck.Enable(True)  
669 - for btn_c in self.btns_coord:  
670 - btn_c.Enable(True)  
671 -  
672 - if self.trigger_state:  
673 - self.trigger.stop() 756 + obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)
  757 + coreg_data.extend(obj_data)
674 758
675 - self.correg.stop() 759 + jobs_list.append(dcr.CoordinateCorregistrate(self.ref_mode_id, tracker_mode, coreg_data, self.coord_queue,
  760 + self.view_tracts, self.coord_tracts_queue,
  761 + self.event, self.sleep_nav))
676 762
677 - Publisher.sendMessage("Navigation status", status=False) 763 + else:
  764 + coreg_data = (m_change, 0)
  765 + jobs_list.append(dcr.CoordinateCorregistrateNoObject(self.ref_mode_id, tracker_mode, coreg_data,
  766 + self.coord_queue,
  767 + self.view_tracts, self.coord_tracts_queue,
  768 + self.event, self.sleep_nav))
  769 +
  770 + if not errors:
  771 + #TODO: Test the trigger thread
  772 + if self.trigger_state:
  773 + # self.trigger = trig.Trigger(nav_id)
  774 + jobs_list.append(trig.TriggerNew(self.trigger_queue, self.event, self.sleep_nav))
  775 +
  776 + if self.view_tracts:
  777 + # initialize Trekker parameters
  778 + slic = sl.Slice()
  779 + affine = slic.affine
  780 + affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)
  781 + Publisher.sendMessage("Update marker offset state", create=True)
  782 + self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius, self.n_threads
  783 + # print("Appending the tract computation thread!")
  784 + jobs_list.append(dti.ComputeTractsThread(self.trk_inp, affine_vtk,
  785 + self.coord_tracts_queue, self.tracts_queue,
  786 + self.event, self.sleep_nav))
  787 +
  788 + jobs_list.append(UpdateNavigationScene(vis_queues, vis_components,
  789 + self.event, self.sleep_nav))
  790 +
  791 + for jobs in jobs_list:
  792 + # jobs.daemon = True
  793 + jobs.start()
  794 + # del jobs
678 795
679 def ResetImageFiducials(self): 796 def ResetImageFiducials(self):
680 for m in range(0, 3): 797 for m in range(0, 3):
@@ -699,6 +816,8 @@ class NeuronavigationPanel(wx.Panel): @@ -699,6 +816,8 @@ class NeuronavigationPanel(wx.Panel):
699 Publisher.sendMessage('Update object registration') 816 Publisher.sendMessage('Update object registration')
700 Publisher.sendMessage('Update track object state', flag=False, obj_name=False) 817 Publisher.sendMessage('Update track object state', flag=False, obj_name=False)
701 Publisher.sendMessage('Delete all markers') 818 Publisher.sendMessage('Delete all markers')
  819 + Publisher.sendMessage("Update marker offset state", create=False)
  820 + Publisher.sendMessage("Remove tracts")
702 # TODO: Reset camera initial focus 821 # TODO: Reset camera initial focus
703 Publisher.sendMessage('Reset cam clipping range') 822 Publisher.sendMessage('Reset cam clipping range')
704 823
@@ -830,8 +949,7 @@ class ObjectRegistrationPanel(wx.Panel): @@ -830,8 +949,7 @@ class ObjectRegistrationPanel(wx.Panel):
830 def UpdateTrackerInit(self, nav_prop): 949 def UpdateTrackerInit(self, nav_prop):
831 self.nav_prop = nav_prop 950 self.nav_prop = nav_prop
832 951
833 - def UpdateNavigationStatus(self, status):  
834 - nav_status = status 952 + def UpdateNavigationStatus(self, nav_status, vis_status):
835 if nav_status: 953 if nav_status:
836 self.checkrecordcoords.Enable(1) 954 self.checkrecordcoords.Enable(1)
837 self.checktrack.Enable(0) 955 self.checktrack.Enable(0)
@@ -898,37 +1016,50 @@ class ObjectRegistrationPanel(wx.Panel): @@ -898,37 +1016,50 @@ class ObjectRegistrationPanel(wx.Panel):
898 pass 1016 pass
899 1017
900 else: 1018 else:
901 - dlg.NavigationTrackerWarning(0, 'choose') 1019 + dlg.ShowNavigationTrackerWarning(0, 'choose')
902 1020
903 def OnLinkLoad(self, event=None): 1021 def OnLinkLoad(self, event=None):
904 - filename = dlg.ShowLoadRegistrationDialog() 1022 + filename = dlg.ShowLoadSaveDialog(message=_(u"Load object registration"),
  1023 + wildcard=_("Registration files (*.obr)|*.obr"))
  1024 + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\pilot_20200131'
  1025 + # coil_path = 'magstim_coil_dell_laptop.obr'
  1026 + # filename = os.path.join(data_dir, coil_path)
905 1027
906 - if filename:  
907 - data = np.loadtxt(filename, delimiter='\t')  
908 - self.obj_fiducials = data[:, :3]  
909 - self.obj_orients = data[:, 3:]  
910 -  
911 - text_file = open(filename, "r")  
912 - header = text_file.readline().split('\t')  
913 - text_file.close()  
914 -  
915 - self.obj_name = header[1]  
916 - self.obj_ref_mode = int(header[-1])  
917 -  
918 - self.checktrack.Enable(1)  
919 - Publisher.sendMessage('Update object registration',  
920 - data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name))  
921 - Publisher.sendMessage('Update status text in GUI', label=_("Ready"))  
922 - self.checktrack.SetValue(True)  
923 - Publisher.sendMessage('Update track object state', flag=True, obj_name=self.obj_name)  
924 - Publisher.sendMessage('Change camera checkbox', status=False)  
925 - wx.MessageBox(_("Object file successfully loaded"), _("Load")) 1028 + try:
  1029 + if filename:
  1030 + #TODO: Improve method to read the file, using "with" similar to OnLoadParameters
  1031 + data = np.loadtxt(filename, delimiter='\t')
  1032 + self.obj_fiducials = data[:, :3]
  1033 + self.obj_orients = data[:, 3:]
  1034 +
  1035 + text_file = open(filename, "r")
  1036 + header = text_file.readline().split('\t')
  1037 + text_file.close()
  1038 +
  1039 + self.obj_name = header[1]
  1040 + self.obj_ref_mode = int(header[-1])
  1041 +
  1042 + self.checktrack.Enable(1)
  1043 + self.checktrack.SetValue(True)
  1044 + Publisher.sendMessage('Update object registration',
  1045 + data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name))
  1046 + Publisher.sendMessage('Update status text in GUI',
  1047 + label=_("Object file successfully loaded"))
  1048 + Publisher.sendMessage('Update track object state', flag=True, obj_name=self.obj_name)
  1049 + Publisher.sendMessage('Change camera checkbox', status=False)
  1050 + # wx.MessageBox(_("Object file successfully loaded"), _("Load"))
  1051 + except:
  1052 + wx.MessageBox(_("Object registration file incompatible."), _("InVesalius 3"))
  1053 + Publisher.sendMessage('Update status text in GUI', label="")
926 1054
927 def ShowSaveObjectDialog(self, evt): 1055 def ShowSaveObjectDialog(self, evt):
928 if np.isnan(self.obj_fiducials).any() or np.isnan(self.obj_orients).any(): 1056 if np.isnan(self.obj_fiducials).any() or np.isnan(self.obj_orients).any():
929 wx.MessageBox(_("Digitize all object fiducials before saving"), _("Save error")) 1057 wx.MessageBox(_("Digitize all object fiducials before saving"), _("Save error"))
930 else: 1058 else:
931 - filename = dlg.ShowSaveRegistrationDialog("object_registration.obr") 1059 + filename = dlg.ShowLoadSaveDialog(message=_(u"Save object registration as..."),
  1060 + wildcard=_("Registration files (*.obr)|*.obr"),
  1061 + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
  1062 + default_filename="object_registration.obr", save_ext="obr")
932 if filename: 1063 if filename:
933 hdr = 'Object' + "\t" + utils.decode(self.obj_name, const.FS_ENCODE) + "\t" + 'Reference' + "\t" + str('%d' % self.obj_ref_mode) 1064 hdr = 'Object' + "\t" + utils.decode(self.obj_name, const.FS_ENCODE) + "\t" + 'Reference' + "\t" + str('%d' % self.obj_ref_mode)
934 data = np.hstack([self.obj_fiducials, self.obj_orients]) 1065 data = np.hstack([self.obj_fiducials, self.obj_orients])
@@ -969,8 +1100,8 @@ class MarkersPanel(wx.Panel): @@ -969,8 +1100,8 @@ class MarkersPanel(wx.Panel):
969 self.tgt_flag = self.tgt_index = None 1100 self.tgt_flag = self.tgt_index = None
970 self.nav_status = False 1101 self.nav_status = False
971 1102
972 - self.marker_colour = (1.0, 1.0, 0.)  
973 - self.marker_size = 3 1103 + self.marker_colour = const.MARKER_COLOUR
  1104 + self.marker_size = const.MARKER_SIZE
974 1105
975 # Change marker size 1106 # Change marker size
976 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23)) 1107 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
@@ -1045,7 +1176,8 @@ class MarkersPanel(wx.Panel): @@ -1045,7 +1176,8 @@ class MarkersPanel(wx.Panel):
1045 self.Update() 1176 self.Update()
1046 1177
1047 def __bind_events(self): 1178 def __bind_events(self):
1048 - Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points') 1179 + # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points')
  1180 + Publisher.subscribe(self.UpdateCurrentCoord, 'Update cross position')
1049 Publisher.subscribe(self.OnDeleteSingleMarker, 'Delete fiducial marker') 1181 Publisher.subscribe(self.OnDeleteSingleMarker, 'Delete fiducial marker')
1050 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') 1182 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers')
1051 Publisher.subscribe(self.OnCreateMarker, 'Create marker') 1183 Publisher.subscribe(self.OnCreateMarker, 'Create marker')
@@ -1055,8 +1187,8 @@ class MarkersPanel(wx.Panel): @@ -1055,8 +1187,8 @@ class MarkersPanel(wx.Panel):
1055 self.current_coord = position[:] 1187 self.current_coord = position[:]
1056 #self.current_angle = pubsub_evt.data[1][3:] 1188 #self.current_angle = pubsub_evt.data[1][3:]
1057 1189
1058 - def UpdateNavigationStatus(self, status):  
1059 - if not status: 1190 + def UpdateNavigationStatus(self, nav_status, vis_status):
  1191 + if not nav_status:
1060 sleep(0.5) 1192 sleep(0.5)
1061 #self.current_coord[3:] = 0, 0, 0 1193 #self.current_coord[3:] = 0, 0, 0
1062 self.nav_status = False 1194 self.nav_status = False
@@ -1073,6 +1205,9 @@ class MarkersPanel(wx.Panel): @@ -1073,6 +1205,9 @@ class MarkersPanel(wx.Panel):
1073 menu_id.AppendSeparator() 1205 menu_id.AppendSeparator()
1074 target_menu = menu_id.Append(1, _('Set as target')) 1206 target_menu = menu_id.Append(1, _('Set as target'))
1075 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetTarget, target_menu) 1207 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetTarget, target_menu)
  1208 + # TODO: Create the remove target option so the user can disable the target without removing the marker
  1209 + # target_menu_rem = menu_id.Append(3, _('Remove target'))
  1210 + # menu_id.Bind(wx.EVT_MENU, self.OnMenuRemoveTarget, target_menu_rem)
1076 1211
1077 target_menu.Enable(True) 1212 target_menu.Enable(True)
1078 self.PopupMenu(menu_id) 1213 self.PopupMenu(menu_id)
@@ -1089,10 +1224,10 @@ class MarkersPanel(wx.Panel): @@ -1089,10 +1224,10 @@ class MarkersPanel(wx.Panel):
1089 if evt == 'TARGET': 1224 if evt == 'TARGET':
1090 id_label = evt 1225 id_label = evt
1091 else: 1226 else:
1092 - id_label = dlg.EnterMarkerID(self.lc.GetItemText(list_index, 4)) 1227 + id_label = dlg.ShowEnterMarkerID(self.lc.GetItemText(list_index, 4))
1093 if id_label == 'TARGET': 1228 if id_label == 'TARGET':
1094 id_label = '' 1229 id_label = ''
1095 - dlg.InvalidTargetID() 1230 + wx.MessageBox(_("Invalid TARGET ID."), _("InVesalius 3"))
1096 self.lc.SetItem(list_index, 4, id_label) 1231 self.lc.SetItem(list_index, 4, id_label)
1097 # Add the new ID to exported list 1232 # Add the new ID to exported list
1098 if len(self.list_coord[list_index]) > 8: 1233 if len(self.list_coord[list_index]) > 8:
@@ -1122,34 +1257,29 @@ class MarkersPanel(wx.Panel): @@ -1122,34 +1257,29 @@ class MarkersPanel(wx.Panel):
1122 Publisher.sendMessage('Disable or enable coil tracker', status=True) 1257 Publisher.sendMessage('Disable or enable coil tracker', status=True)
1123 self.OnMenuEditMarkerId('TARGET') 1258 self.OnMenuEditMarkerId('TARGET')
1124 self.tgt_flag = True 1259 self.tgt_flag = True
1125 - dlg.NewTarget() 1260 + wx.MessageBox(_("New target selected."), _("InVesalius 3"))
1126 1261
1127 def OnMenuSetColor(self, evt): 1262 def OnMenuSetColor(self, evt):
1128 index = self.lc.GetFocusedItem() 1263 index = self.lc.GetFocusedItem()
1129 - cdata = wx.ColourData()  
1130 - cdata.SetColour(wx.Colour(self.list_coord[index][6]*255,self.list_coord[index][7]*255,self.list_coord[index][8]*255))  
1131 - dlg = wx.ColourDialog(self, data=cdata)  
1132 - dlg.GetColourData().SetChooseFull(True)  
1133 - if dlg.ShowModal() == wx.ID_OK:  
1134 - self.r, self.g, self.b = dlg.GetColourData().GetColour().Get(includeAlpha=False)  
1135 - r = float(self.r) / 255.0  
1136 - g = float(self.g) / 255.0  
1137 - b = float(self.b) / 255.0  
1138 - dlg.Destroy()  
1139 - color = [r,g,b]  
1140 -  
1141 - Publisher.sendMessage('Set new color', index=index, color=color)  
1142 -  
1143 - self.list_coord[index][6] = r  
1144 - self.list_coord[index][7] = g  
1145 - self.list_coord[index][8] = b 1264 +
  1265 + color_current = [self.list_coord[index][n] * 255 for n in range(6, 9)]
  1266 +
  1267 + color_new = dlg.ShowColorDialog(color_current=color_current)
  1268 +
  1269 + if color_new:
  1270 + assert len(color_new) == 3
  1271 + for n, col in enumerate(color_new):
  1272 + self.list_coord[index][n+6] = col/255.0
  1273 +
  1274 + Publisher.sendMessage('Set new color', index=index, color=color_new)
1146 1275
1147 def OnDeleteAllMarkers(self, evt=None): 1276 def OnDeleteAllMarkers(self, evt=None):
1148 if self.list_coord: 1277 if self.list_coord:
1149 if evt is None: 1278 if evt is None:
1150 result = wx.ID_OK 1279 result = wx.ID_OK
1151 else: 1280 else:
1152 - result = dlg.DeleteAllMarkers() 1281 + # result = dlg.DeleteAllMarkers()
  1282 + result = dlg.ShowConfirmationDialog(msg=_("Remove all markers? Cannot be undone."))
1153 1283
1154 if result == wx.ID_OK: 1284 if result == wx.ID_OK:
1155 self.list_coord = [] 1285 self.list_coord = []
@@ -1162,7 +1292,7 @@ class MarkersPanel(wx.Panel): @@ -1162,7 +1292,7 @@ class MarkersPanel(wx.Panel):
1162 self.tgt_flag = self.tgt_index = None 1292 self.tgt_flag = self.tgt_index = None
1163 Publisher.sendMessage('Disable or enable coil tracker', status=False) 1293 Publisher.sendMessage('Disable or enable coil tracker', status=False)
1164 if not hasattr(evt, 'data'): 1294 if not hasattr(evt, 'data'):
1165 - dlg.DeleteTarget() 1295 + wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
1166 1296
1167 def OnDeleteSingleMarker(self, evt=None, marker_id=None): 1297 def OnDeleteSingleMarker(self, evt=None, marker_id=None):
1168 # OnDeleteSingleMarker is used for both pubsub and button click events 1298 # OnDeleteSingleMarker is used for both pubsub and button click events
@@ -1183,14 +1313,16 @@ class MarkersPanel(wx.Panel): @@ -1183,14 +1313,16 @@ class MarkersPanel(wx.Panel):
1183 else: 1313 else:
1184 index = None 1314 index = None
1185 1315
  1316 + #TODO: There are bugs when no marker is selected, test and improve
1186 if index: 1317 if index:
1187 if self.tgt_flag and self.tgt_index == index[0]: 1318 if self.tgt_flag and self.tgt_index == index[0]:
1188 self.tgt_flag = self.tgt_index = None 1319 self.tgt_flag = self.tgt_index = None
1189 Publisher.sendMessage('Disable or enable coil tracker', status=False) 1320 Publisher.sendMessage('Disable or enable coil tracker', status=False)
1190 - dlg.DeleteTarget() 1321 + wx.MessageBox(_("No data selected."), _("InVesalius 3"))
  1322 +
1191 self.DeleteMarker(index) 1323 self.DeleteMarker(index)
1192 else: 1324 else:
1193 - dlg.NoMarkerSelected() 1325 + wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
1194 1326
1195 def DeleteMarker(self, index): 1327 def DeleteMarker(self, index):
1196 for i in reversed(index): 1328 for i in reversed(index):
@@ -1213,12 +1345,16 @@ class MarkersPanel(wx.Panel): @@ -1213,12 +1345,16 @@ class MarkersPanel(wx.Panel):
1213 self.CreateMarker(self.current_coord, self.marker_colour, self.marker_size) 1345 self.CreateMarker(self.current_coord, self.marker_colour, self.marker_size)
1214 1346
1215 def OnLoadMarkers(self, evt): 1347 def OnLoadMarkers(self, evt):
1216 - filepath = dlg.ShowLoadMarkersDialog() 1348 + filename = dlg.ShowLoadSaveDialog(message=_(u"Load markers"),
  1349 + wildcard=_("Markers files (*.mks)|*.mks"))
  1350 + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\pilot_20200131'
  1351 + # marker_path = 'markers.mks'
  1352 + # filename = os.path.join(data_dir, marker_path)
1217 1353
1218 - if filepath: 1354 + if filename:
1219 try: 1355 try:
1220 count_line = self.lc.GetItemCount() 1356 count_line = self.lc.GetItemCount()
1221 - content = [s.rstrip() for s in open(filepath)] 1357 + content = [s.rstrip() for s in open(filename)]
1222 for data in content: 1358 for data in content:
1223 target = None 1359 target = None
1224 line = [s for s in data.split()] 1360 line = [s for s in data.split()]
@@ -1254,7 +1390,7 @@ class MarkersPanel(wx.Panel): @@ -1254,7 +1390,7 @@ class MarkersPanel(wx.Panel):
1254 self.CreateMarker(coord, colour, size, line[7]) 1390 self.CreateMarker(coord, colour, size, line[7])
1255 count_line += 1 1391 count_line += 1
1256 except: 1392 except:
1257 - dlg.InvalidMarkersFile() 1393 + wx.MessageBox(_("Invalid markers file."), _("InVesalius 3"))
1258 1394
1259 def OnMarkersVisibility(self, evt, ctrl): 1395 def OnMarkersVisibility(self, evt, ctrl):
1260 1396
@@ -1266,7 +1402,11 @@ class MarkersPanel(wx.Panel): @@ -1266,7 +1402,11 @@ class MarkersPanel(wx.Panel):
1266 ctrl.SetLabel('Hide') 1402 ctrl.SetLabel('Hide')
1267 1403
1268 def OnSaveMarkers(self, evt): 1404 def OnSaveMarkers(self, evt):
1269 - filename = dlg.ShowSaveMarkersDialog("markers.mks") 1405 + filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."),
  1406 + wildcard=_("Marker files (*.mks)|*.mks"),
  1407 + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
  1408 + default_filename="markers.mks", save_ext="mks")
  1409 +
1270 if filename: 1410 if filename:
1271 if self.list_coord: 1411 if self.list_coord:
1272 text_file = open(filename, "w") 1412 text_file = open(filename, "w")
@@ -1339,3 +1479,459 @@ class DbsPanel(wx.Panel): @@ -1339,3 +1479,459 @@ class DbsPanel(wx.Panel):
1339 except AttributeError: 1479 except AttributeError:
1340 default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) 1480 default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
1341 1481
  1482 +
  1483 +class TractographyPanel(wx.Panel):
  1484 +
  1485 + def __init__(self, parent):
  1486 + wx.Panel.__init__(self, parent)
  1487 + try:
  1488 + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
  1489 + except AttributeError:
  1490 + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
  1491 + self.SetBackgroundColour(default_colour)
  1492 +
  1493 + self.affine = None
  1494 + self.affine_vtk = None
  1495 + self.trekker = None
  1496 + self.n_tracts = const.N_TRACTS
  1497 + self.peel_depth = const.PEEL_DEPTH
  1498 + self.view_tracts = False
  1499 + self.seed_offset = const.SEED_OFFSET
  1500 + self.seed_radius = const.SEED_RADIUS
  1501 + self.sleep_nav = const.SLEEP_NAVIGATION
  1502 + self.brain_opacity = const.BRAIN_OPACITY
  1503 + self.brain_peel = None
  1504 + self.brain_actor = None
  1505 + self.n_peels = const.MAX_PEEL_DEPTH
  1506 + self.p_old = np.array([[0., 0., 0.]])
  1507 + self.tracts_run = None
  1508 + self.trekker_cfg = const.TREKKER_CONFIG
  1509 + self.nav_status = False
  1510 +
  1511 + self.SetAutoLayout(1)
  1512 + self.__bind_events()
  1513 +
  1514 + # Button for creating new coil
  1515 + tooltip = wx.ToolTip(_("Load brain visualization"))
  1516 + btn_mask = wx.Button(self, -1, _("Load brain"), size=wx.Size(65, 23))
  1517 + btn_mask.SetToolTip(tooltip)
  1518 + btn_mask.Enable(1)
  1519 + btn_mask.Bind(wx.EVT_BUTTON, self.OnLinkBrain)
  1520 + # self.btn_new = btn_new
  1521 +
  1522 + # Button for import config coil file
  1523 + tooltip = wx.ToolTip(_("Load FOD"))
  1524 + btn_load = wx.Button(self, -1, _("Load FOD"), size=wx.Size(65, 23))
  1525 + btn_load.SetToolTip(tooltip)
  1526 + btn_load.Enable(1)
  1527 + btn_load.Bind(wx.EVT_BUTTON, self.OnLinkFOD)
  1528 + # self.btn_load = btn_load
  1529 +
  1530 + # Save button for object registration
  1531 + tooltip = wx.ToolTip(_(u"Load Trekker configuration parameters"))
  1532 + btn_load_cfg = wx.Button(self, -1, _(u"Configure"), size=wx.Size(65, 23))
  1533 + btn_load_cfg.SetToolTip(tooltip)
  1534 + btn_load_cfg.Enable(1)
  1535 + btn_load_cfg.Bind(wx.EVT_BUTTON, self.OnLoadParameters)
  1536 + # self.btn_load_cfg = btn_load_cfg
  1537 +
  1538 + # Create a horizontal sizer to represent button save
  1539 + line_btns = wx.BoxSizer(wx.HORIZONTAL)
  1540 + line_btns.Add(btn_load, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4)
  1541 + line_btns.Add(btn_load_cfg, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4)
  1542 + line_btns.Add(btn_mask, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4)
  1543 +
  1544 + # Change peeling depth
  1545 + text_peel_depth = wx.StaticText(self, -1, _("Peeling depth (mm):"))
  1546 + spin_peel_depth = wx.SpinCtrl(self, -1, "", size=wx.Size(50, 23))
  1547 + spin_peel_depth.Enable(1)
  1548 + spin_peel_depth.SetRange(0, const.MAX_PEEL_DEPTH)
  1549 + spin_peel_depth.SetValue(const.PEEL_DEPTH)
  1550 + spin_peel_depth.Bind(wx.EVT_TEXT, partial(self.OnSelectPeelingDepth, ctrl=spin_peel_depth))
  1551 + spin_peel_depth.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectPeelingDepth, ctrl=spin_peel_depth))
  1552 +
  1553 + # Change number of tracts
  1554 + text_ntracts = wx.StaticText(self, -1, _("Number tracts:"))
  1555 + spin_ntracts = wx.SpinCtrl(self, -1, "", size=wx.Size(50, 23))
  1556 + spin_ntracts.Enable(1)
  1557 + spin_ntracts.SetRange(1, 2000)
  1558 + spin_ntracts.SetValue(const.N_TRACTS)
  1559 + spin_ntracts.Bind(wx.EVT_TEXT, partial(self.OnSelectNumTracts, ctrl=spin_ntracts))
  1560 + spin_ntracts.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectNumTracts, ctrl=spin_ntracts))
  1561 +
  1562 + # Change seed offset for computing tracts
  1563 + text_offset = wx.StaticText(self, -1, _("Seed offset (mm):"))
  1564 + spin_offset = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc = 0.1)
  1565 + spin_offset.Enable(1)
  1566 + spin_offset.SetRange(0, 100.0)
  1567 + spin_offset.SetValue(self.seed_offset)
  1568 + spin_offset.Bind(wx.EVT_TEXT, partial(self.OnSelectOffset, ctrl=spin_offset))
  1569 + spin_offset.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectOffset, ctrl=spin_offset))
  1570 + # self.spin_offset = spin_offset
  1571 +
  1572 + # Change seed radius for computing tracts
  1573 + text_radius = wx.StaticText(self, -1, _("Seed radius (mm):"))
  1574 + spin_radius = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.1)
  1575 + spin_radius.Enable(1)
  1576 + spin_radius.SetRange(0, 100.0)
  1577 + spin_radius.SetValue(self.seed_radius)
  1578 + spin_radius.Bind(wx.EVT_TEXT, partial(self.OnSelectRadius, ctrl=spin_radius))
  1579 + spin_radius.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectRadius, ctrl=spin_radius))
  1580 + # self.spin_radius = spin_radius
  1581 +
  1582 + # Change sleep pause between navigation loops
  1583 + text_sleep = wx.StaticText(self, -1, _("Sleep (s):"))
  1584 + spin_sleep = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.01)
  1585 + spin_sleep.Enable(1)
  1586 + spin_sleep.SetRange(0.01, 10.0)
  1587 + spin_sleep.SetValue(self.sleep_nav)
  1588 + spin_sleep.Bind(wx.EVT_TEXT, partial(self.OnSelectSleep, ctrl=spin_sleep))
  1589 + spin_sleep.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectSleep, ctrl=spin_sleep))
  1590 +
  1591 + # Change opacity of brain mask visualization
  1592 + text_opacity = wx.StaticText(self, -1, _("Brain opacity:"))
  1593 + spin_opacity = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.1)
  1594 + spin_opacity.Enable(0)
  1595 + spin_opacity.SetRange(0, 1.0)
  1596 + spin_opacity.SetValue(self.brain_opacity)
  1597 + spin_opacity.Bind(wx.EVT_TEXT, partial(self.OnSelectOpacity, ctrl=spin_opacity))
  1598 + spin_opacity.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectOpacity, ctrl=spin_opacity))
  1599 + self.spin_opacity = spin_opacity
  1600 +
  1601 + # Create a horizontal sizer to threshold configs
  1602 + border = 1
  1603 + line_peel_depth = wx.BoxSizer(wx.HORIZONTAL)
  1604 + line_peel_depth.AddMany([(text_peel_depth, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border),
  1605 + (spin_peel_depth, 0, wx.ALL | wx.EXPAND | wx.GROW, border)])
  1606 +
  1607 + line_ntracts = wx.BoxSizer(wx.HORIZONTAL)
  1608 + line_ntracts.AddMany([(text_ntracts, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border),
  1609 + (spin_ntracts, 0, wx.ALL | wx.EXPAND | wx.GROW, border)])
  1610 +
  1611 + line_offset = wx.BoxSizer(wx.HORIZONTAL)
  1612 + line_offset.AddMany([(text_offset, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border),
  1613 + (spin_offset, 0, wx.ALL | wx.EXPAND | wx.GROW, border)])
  1614 +
  1615 + line_radius = wx.BoxSizer(wx.HORIZONTAL)
  1616 + line_radius.AddMany([(text_radius, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border),
  1617 + (spin_radius, 0, wx.ALL | wx.EXPAND | wx.GROW, border)])
  1618 +
  1619 + line_sleep = wx.BoxSizer(wx.HORIZONTAL)
  1620 + line_sleep.AddMany([(text_sleep, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border),
  1621 + (spin_sleep, 0, wx.ALL | wx.EXPAND | wx.GROW, border)])
  1622 +
  1623 + line_opacity = wx.BoxSizer(wx.HORIZONTAL)
  1624 + line_opacity.AddMany([(text_opacity, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border),
  1625 + (spin_opacity, 0, wx.ALL | wx.EXPAND | wx.GROW, border)])
  1626 +
  1627 + # Check box to enable tract visualization
  1628 + checktracts = wx.CheckBox(self, -1, _('Enable tracts'))
  1629 + checktracts.SetValue(False)
  1630 + checktracts.Enable(0)
  1631 + checktracts.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableTracts, ctrl=checktracts))
  1632 + self.checktracts = checktracts
  1633 +
  1634 + # Check box to enable surface peeling
  1635 + checkpeeling = wx.CheckBox(self, -1, _('Peel surface'))
  1636 + checkpeeling.SetValue(False)
  1637 + checkpeeling.Enable(0)
  1638 + checkpeeling.Bind(wx.EVT_CHECKBOX, partial(self.OnShowPeeling, ctrl=checkpeeling))
  1639 + self.checkpeeling = checkpeeling
  1640 +
  1641 + border_last = 1
  1642 + line_checks = wx.BoxSizer(wx.HORIZONTAL)
  1643 + line_checks.Add(checktracts, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, border_last)
  1644 + line_checks.Add(checkpeeling, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.LEFT, border_last)
  1645 +
  1646 + # Add line sizers into main sizer
  1647 + border = 1
  1648 + border_last = 10
  1649 + main_sizer = wx.BoxSizer(wx.VERTICAL)
  1650 + main_sizer.Add(line_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, border_last)
  1651 + main_sizer.Add(line_peel_depth, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border)
  1652 + main_sizer.Add(line_ntracts, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border)
  1653 + main_sizer.Add(line_offset, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border)
  1654 + main_sizer.Add(line_radius, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border)
  1655 + main_sizer.Add(line_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border)
  1656 + main_sizer.Add(line_opacity, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border)
  1657 + main_sizer.Add(line_checks, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, border_last)
  1658 + main_sizer.Fit(self)
  1659 +
  1660 + self.SetSizer(main_sizer)
  1661 + self.Update()
  1662 +
  1663 + def __bind_events(self):
  1664 + Publisher.subscribe(self.OnCloseProject, 'Close project data')
  1665 + Publisher.subscribe(self.OnUpdateTracts, 'Update cross position')
  1666 + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
  1667 +
  1668 + def OnSelectPeelingDepth(self, evt, ctrl):
  1669 + self.peel_depth = ctrl.GetValue()
  1670 + if self.checkpeeling.GetValue():
  1671 + actor = self.brain_peel.get_actor(self.peel_depth)
  1672 + Publisher.sendMessage('Update peel', flag=True, actor=actor)
  1673 +
  1674 + def OnSelectNumTracts(self, evt, ctrl):
  1675 + self.n_tracts = ctrl.GetValue()
  1676 + # self.tract.n_tracts = ctrl.GetValue()
  1677 + Publisher.sendMessage('Update number of tracts', data=self.n_tracts)
  1678 +
  1679 + def OnSelectOffset(self, evt, ctrl):
  1680 + self.seed_offset = ctrl.GetValue()
  1681 + # self.tract.seed_offset = ctrl.GetValue()
  1682 + Publisher.sendMessage('Update seed offset', data=self.seed_offset)
  1683 +
  1684 + def OnSelectRadius(self, evt, ctrl):
  1685 + self.seed_radius = ctrl.GetValue()
  1686 + # self.tract.seed_offset = ctrl.GetValue()
  1687 + Publisher.sendMessage('Update seed radius', data=self.seed_radius)
  1688 +
  1689 + def OnSelectSleep(self, evt, ctrl):
  1690 + self.sleep_nav = ctrl.GetValue()
  1691 + # self.tract.seed_offset = ctrl.GetValue()
  1692 + Publisher.sendMessage('Update sleep', data=self.sleep_nav)
  1693 +
  1694 + def OnSelectOpacity(self, evt, ctrl):
  1695 + self.brain_actor.GetProperty().SetOpacity(ctrl.GetValue())
  1696 + Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor)
  1697 +
  1698 + def OnShowPeeling(self, evt, ctrl):
  1699 + # self.view_peeling = ctrl.GetValue()
  1700 + if ctrl.GetValue():
  1701 + actor = self.brain_peel.get_actor(self.peel_depth)
  1702 + else:
  1703 + actor = None
  1704 + Publisher.sendMessage('Update peel', flag=ctrl.GetValue(), actor=actor)
  1705 +
  1706 + def OnEnableTracts(self, evt, ctrl):
  1707 + self.view_tracts = ctrl.GetValue()
  1708 + Publisher.sendMessage('Update tracts visualization', data=self.view_tracts)
  1709 + if not self.view_tracts:
  1710 + Publisher.sendMessage('Remove tracts')
  1711 + Publisher.sendMessage("Update marker offset state", create=False)
  1712 +
  1713 + def UpdateNavigationStatus(self, nav_status, vis_status):
  1714 + self.nav_status = nav_status
  1715 +
  1716 + def OnLinkBrain(self, event=None):
  1717 + Publisher.sendMessage('Update status text in GUI', label=_("Busy"))
  1718 + Publisher.sendMessage('Begin busy cursor')
  1719 + mask_path = dlg.ShowImportOtherFilesDialog(const.ID_TREKKER_MASK)
  1720 + img_path = dlg.ShowImportOtherFilesDialog(const.ID_TREKKER_IMG)
  1721 + # data_dir = os.environ.get('OneDriveConsumer') + '\\data\\dti'
  1722 + # mask_file = 'sub-P0_dwi_mask.nii'
  1723 + # mask_path = os.path.join(data_dir, mask_file)
  1724 + # img_file = 'sub-P0_T1w_biascorrected.nii'
  1725 + # img_path = os.path.join(data_dir, img_file)
  1726 +
  1727 + if not self.affine_vtk:
  1728 + slic = sl.Slice()
  1729 + self.affine = slic.affine
  1730 + self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine)
  1731 +
  1732 + try:
  1733 + self.brain_peel = brain.Brain(img_path, mask_path, self.n_peels, self.affine_vtk)
  1734 + self.brain_actor = self.brain_peel.get_actor(self.peel_depth)
  1735 + self.brain_actor.GetProperty().SetOpacity(self.brain_opacity)
  1736 + Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor)
  1737 + self.checkpeeling.Enable(1)
  1738 + self.checkpeeling.SetValue(True)
  1739 + self.spin_opacity.Enable(1)
  1740 + Publisher.sendMessage('Update status text in GUI', label=_("Brain model loaded"))
  1741 + except:
  1742 + wx.MessageBox(_("Unable to load brain mask."), _("InVesalius 3"))
  1743 +
  1744 + Publisher.sendMessage('End busy cursor')
  1745 +
  1746 + def OnLinkFOD(self, event=None):
  1747 + Publisher.sendMessage('Update status text in GUI', label=_("Busy"))
  1748 + Publisher.sendMessage('Begin busy cursor')
  1749 + filename = dlg.ShowImportOtherFilesDialog(const.ID_TREKKER_FOD)
  1750 + # Juuso
  1751 + # data_dir = os.environ.get('OneDriveConsumer') + '\\data\\dti'
  1752 + # FOD_path = 'sub-P0_dwi_FOD.nii'
  1753 + # Baran
  1754 + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\pilot_20200131'
  1755 + # FOD_path = 'Baran_FOD.nii'
  1756 + # filename = os.path.join(data_dir, FOD_path)
  1757 +
  1758 + if not self.affine_vtk:
  1759 + slic = sl.Slice()
  1760 + self.affine = slic.affine
  1761 + self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine)
  1762 +
  1763 + # try:
  1764 +
  1765 + self.trekker = Trekker.initialize(filename.encode('utf-8'))
  1766 + self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg)
  1767 +
  1768 + self.checktracts.Enable(1)
  1769 + self.checktracts.SetValue(True)
  1770 + self.view_tracts = True
  1771 + Publisher.sendMessage('Update Trekker object', data=self.trekker)
  1772 + Publisher.sendMessage('Update number of threads', data=n_threads)
  1773 + Publisher.sendMessage('Update tracts visualization', data=1)
  1774 + Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialized"))
  1775 + # except:
  1776 + # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3"))
  1777 +
  1778 + Publisher.sendMessage('End busy cursor')
  1779 +
  1780 + def OnLoadParameters(self, event=None):
  1781 + import json
  1782 + filename = dlg.ShowLoadSaveDialog(message=_(u"Load Trekker configuration"),
  1783 + wildcard=_("JSON file (*.json)|*.json"))
  1784 + try:
  1785 + # Check if filename exists, read the JSON file and check if all parameters match
  1786 + # with the required list defined in the constants module
  1787 + # if a parameter is missing, raise an error
  1788 + if filename:
  1789 + with open(filename) as json_file:
  1790 + self.trekker_cfg = json.load(json_file)
  1791 + assert all(name in self.trekker_cfg for name in const.TREKKER_CONFIG)
  1792 + if self.trekker:
  1793 + self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg)
  1794 + Publisher.sendMessage('Update Trekker object', data=self.trekker)
  1795 + Publisher.sendMessage('Update number of threads', data=n_threads)
  1796 +
  1797 + Publisher.sendMessage('Update status text in GUI', label=_("Trekker config loaded"))
  1798 +
  1799 + except (AssertionError, json.decoder.JSONDecodeError):
  1800 + # Inform user that file is not compatible
  1801 + self.trekker_cfg = const.TREKKER_CONFIG
  1802 + wx.MessageBox(_("File incompatible, using default configuration."), _("InVesalius 3"))
  1803 + Publisher.sendMessage('Update status text in GUI', label="")
  1804 +
  1805 + def OnUpdateTracts(self, arg, position):
  1806 + """
  1807 + Minimal working version of tract computation. Updates when cross sends Pubsub message to update.
  1808 + Position refers to the coordinates in InVesalius 2D space. To represent the same coordinates in the 3D space,
  1809 + flip_x the coordinates and multiply the z coordinate by -1. This is all done in the flix_x function.
  1810 +
  1811 + :param arg: event for pubsub
  1812 + :param position: list or array with the x, y, and z coordinates in InVesalius space
  1813 + """
  1814 + # Minimal working version of tract computation
  1815 + # It updates when cross updates
  1816 + # pass
  1817 + if self.view_tracts and not self.nav_status:
  1818 + # print("Running during navigation")
  1819 + coord_flip = db.flip_x_m(position[:3])[:3, 0]
  1820 + dti.compute_tracts(self.trekker, coord_flip, self.affine, self.affine_vtk,
  1821 + self.n_tracts)
  1822 +
  1823 + def OnCloseProject(self):
  1824 + self.checktracts.SetValue(False)
  1825 + self.checktracts.Enable(0)
  1826 + self.checkpeeling.SetValue(False)
  1827 + self.checkpeeling.Enable(0)
  1828 +
  1829 + self.spin_opacity.SetValue(const.BRAIN_OPACITY)
  1830 + self.spin_opacity.Enable(0)
  1831 + Publisher.sendMessage('Update peel', flag=False, actor=self.brain_actor)
  1832 +
  1833 + self.peel_depth = const.PEEL_DEPTH
  1834 + self.n_tracts = const.N_TRACTS
  1835 +
  1836 + Publisher.sendMessage('Remove tracts')
  1837 +
  1838 +
  1839 +class QueueCustom(queue.Queue):
  1840 + """
  1841 + A custom queue subclass that provides a :meth:`clear` method.
  1842 + https://stackoverflow.com/questions/6517953/clear-all-items-from-the-queue
  1843 + Modified to a LIFO Queue type (Last-in-first-out). Seems to make sense for the navigation
  1844 + threads, as the last added coordinate should be the first to be processed.
  1845 + In the first tests in a short run, seems to increase the coord queue size considerably,
  1846 + possibly limiting the queue size is good.
  1847 + """
  1848 +
  1849 + def clear(self):
  1850 + """
  1851 + Clears all items from the queue.
  1852 + """
  1853 +
  1854 + with self.mutex:
  1855 + unfinished = self.unfinished_tasks - len(self.queue)
  1856 + if unfinished <= 0:
  1857 + if unfinished < 0:
  1858 + raise ValueError('task_done() called too many times')
  1859 + self.all_tasks_done.notify_all()
  1860 + self.unfinished_tasks = unfinished
  1861 + self.queue.clear()
  1862 + self.not_full.notify_all()
  1863 +
  1864 +
  1865 +class UpdateNavigationScene(threading.Thread):
  1866 +
  1867 + def __init__(self, vis_queues, vis_components, event, sle):
  1868 + """Class (threading) to update the navigation scene with all graphical elements.
  1869 +
  1870 + Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation
  1871 +
  1872 + :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene
  1873 + :type affine_vtk: vtkMatrix4x4
  1874 + :param visualization_queue: Queue instance that manage coordinates to be visualized
  1875 + :type visualization_queue: queue.Queue
  1876 + :param event: Threading event to coordinate when tasks as done and allow UI release
  1877 + :type event: threading.Event
  1878 + :param sle: Sleep pause in seconds
  1879 + :type sle: float
  1880 + """
  1881 +
  1882 + threading.Thread.__init__(self, name='UpdateScene')
  1883 + self.trigger_state, self.view_tracts = vis_components
  1884 + self.coord_queue, self.trigger_queue, self.tracts_queue = vis_queues
  1885 + self.sle = sle
  1886 + self.event = event
  1887 +
  1888 + def run(self):
  1889 + # count = 0
  1890 + while not self.event.is_set():
  1891 + got_coords = False
  1892 + try:
  1893 + coord, m_img, view_obj = self.coord_queue.get_nowait()
  1894 + got_coords = True
  1895 +
  1896 + # print('UpdateScene: get {}'.format(count))
  1897 +
  1898 + # use of CallAfter is mandatory otherwise crashes the wx interface
  1899 + if self.view_tracts:
  1900 + bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait()
  1901 + wx.CallAfter(Publisher.sendMessage, 'Remove tracts')
  1902 + wx.CallAfter(Publisher.sendMessage, 'Update tracts', flag=True, root=bundle,
  1903 + affine_vtk=affine_vtk)
  1904 + wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset)
  1905 + self.tracts_queue.task_done()
  1906 +
  1907 + if self.trigger_state:
  1908 + trigger_on = self.trigger_queue.get_nowait()
  1909 + if trigger_on:
  1910 + wx.CallAfter(Publisher.sendMessage, 'Create marker')
  1911 + self.trigger_queue.task_done()
  1912 +
  1913 + # TODO: If using the view_tracts substitute the raw coord from the offset coordinate, so the user
  1914 + # see the red cross in the position of the offset marker
  1915 + wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord)
  1916 +
  1917 + if view_obj:
  1918 + wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)
  1919 +
  1920 + self.coord_queue.task_done()
  1921 + # print('UpdateScene: done {}'.format(count))
  1922 + # count += 1
  1923 +
  1924 + sleep(self.sle)
  1925 + except queue.Empty:
  1926 + if got_coords:
  1927 + self.coord_queue.task_done()
  1928 +
  1929 +
  1930 +class InputAttributes(object):
  1931 + # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
  1932 + def __init__(self, *initial_data, **kwargs):
  1933 + for dictionary in initial_data:
  1934 + for key in dictionary:
  1935 + setattr(self, key, dictionary[key])
  1936 + for key in kwargs:
  1937 + setattr(self, key, kwargs[key])
invesalius/project.py
@@ -313,8 +313,9 @@ class Project(metaclass=Singleton): @@ -313,8 +313,9 @@ class Project(metaclass=Singleton):
313 self.spacing = project["spacing"] 313 self.spacing = project["spacing"]
314 if project.get("affine", ""): 314 if project.get("affine", ""):
315 self.affine = project["affine"] 315 self.affine = project["affine"]
  316 + # self.affine = project.get("affine")
316 Publisher.sendMessage('Update affine matrix', 317 Publisher.sendMessage('Update affine matrix',
317 - affine=self.affine, status=True) 318 + affine=np.asarray(self.affine).reshape(4, 4), status=True)
318 319
319 self.compress = project.get("compress", True) 320 self.compress = project.get("compress", True)
320 321