Commit 46e9180122ff30cd6e3d4c95c4a597e2fdd1ffab

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

ENH: Update tractography parameters and fix config file load (#400)

* ENH: Update tractography parameters and fix config file load

* FIX: Problem when loading the object registration after loading FOD

* FIX: Fix torus bug when creating surface and update tractography parameters

* FIX: Include spacing in the offset for tract computation

* ADD: Update tractography parameters and image shift with spacing

* ENH: Improve grid offset for tractography

* WIP: Improved tractography computation and seed sampling

* WIP: Refactored and cleaned ACT tractography needs testing in navigation

* FIX: Bump conda environment packages to python 3.8 and tract parameters

* FIX: Save the seed marker in world coordinate space

* ENH: Affine is identity by default and not None

Co-authored-by: Renan <renan_hiroshi@hotmail.com>
environment.yml
@@ -2,7 +2,7 @@ channels: @@ -2,7 +2,7 @@ channels:
2 - conda-forge 2 - conda-forge
3 - bioconda 3 - bioconda
4 dependencies: 4 dependencies:
5 - - python=3.7 5 + - python=3.8
6 - cython==0.29.24 6 - cython==0.29.24
7 - pillow==8.3.2 7 - pillow==8.3.2
8 - pypubsub==4.0.3 8 - pypubsub==4.0.3
@@ -17,13 +17,21 @@ dependencies: @@ -17,13 +17,21 @@ dependencies:
17 - scipy==1.7.1 17 - scipy==1.7.1
18 - vtk==9.0.3 18 - vtk==9.0.3
19 - wxpython==4.1.1 19 - wxpython==4.1.1
  20 + - mido==1.2.10
  21 + - nest-asyncio==1.5.1
20 - pip 22 - pip
21 - pip: 23 - pip:
22 - - python-gdcm==3.0.9.1 24 + - aioconsole==0.3.2
  25 + - opencv-python==4.5.3.56
23 - plaidml-keras==0.7.0 26 - plaidml-keras==0.7.0
24 - - theano==1.0.5  
25 - - pyacvd==0.2.3 27 + - python-gdcm==3.0.9.1
  28 + - python-rtmidi==1.4.9
  29 + - python-socketio[client]==5.3.0
  30 + - pyacvd==0.2.7
26 - pyclaron 31 - pyclaron
27 - polhemusFT 32 - polhemusFT
28 - polhemus 33 - polhemus
29 - pypolaris 34 - pypolaris
  35 + - theano==1.0.5
  36 + - uvicorn[standard]==0.15.0
  37 + - https://github.com/baranaydogan/trekker/raw/master/binaries/Trekker-0.9-cp38-cp38-win_amd64.whl
invesalius/constants.py
@@ -823,23 +823,35 @@ COIL_ANGLE_ARROW_PROJECTION_THRESHOLD = 5 @@ -823,23 +823,35 @@ COIL_ANGLE_ARROW_PROJECTION_THRESHOLD = 5
823 CAM_MODE = True 823 CAM_MODE = True
824 824
825 # Tractography visualization 825 # Tractography visualization
826 -N_TRACTS = 100  
827 -PEEL_DEPTH = 5  
828 -MAX_PEEL_DEPTH = 30  
829 -SEED_OFFSET = 15 826 +N_TRACTS = 200
  827 +PEEL_DEPTH = 10
  828 +MAX_PEEL_DEPTH = 40
  829 +SEED_OFFSET = 30
830 SEED_RADIUS = 1.5 830 SEED_RADIUS = 1.5
831 831
832 # Increased the default sleep parameter from 0.1 to 0.15 to decrease CPU load during navigation. 832 # Increased the default sleep parameter from 0.1 to 0.15 to decrease CPU load during navigation.
833 -SLEEP_NAVIGATION = 0.15 833 +SLEEP_NAVIGATION = 0.2
834 SLEEP_COORDINATES = 0.05 834 SLEEP_COORDINATES = 0.05
835 SLEEP_ROBOT = 0.01 835 SLEEP_ROBOT = 0.01
836 836
837 -BRAIN_OPACITY = 0.5 837 +BRAIN_OPACITY = 0.6
838 N_CPU = psutil.cpu_count() 838 N_CPU = psutil.cpu_count()
839 -TREKKER_CONFIG = {'seed_max': 1, 'step_size': 0.1, 'min_fod': 0.1, 'probe_quality': 3,  
840 - 'max_interval': 1, 'min_radius_curv': 0.8, 'probe_length': 0.4,  
841 - 'write_interval': 50, 'numb_threads': '', 'max_lenth': 200,  
842 - 'min_lenth': 20, 'max_sampling_step': 100} 839 +# the max_sampling_step can be set to something different as well. Above 100 is probably not necessary
  840 +TREKKER_CONFIG = {'seed_max': 1,
  841 + 'step_size': 0.03125,
  842 + 'min_fod': 0.05,
  843 + 'probe_quality': 3,
  844 + 'max_interval': 1,
  845 + 'min_radius_curvature': 0.625,
  846 + 'probe_length': 0.15625,
  847 + 'write_interval': 50,
  848 + 'numb_threads': '',
  849 + 'max_length': 250,
  850 + 'min_length': 10,
  851 + 'max_sampling_step': 100,
  852 + 'data_support_exponent': 0.5,
  853 + 'use_best_init': True,
  854 + 'init_max_est_trials': 100}
843 855
844 MARKER_FILE_MAGICK_STRING = "##INVESALIUS3_MARKER_FILE_" 856 MARKER_FILE_MAGICK_STRING = "##INVESALIUS3_MARKER_FILE_"
845 CURRENT_MARKER_FILE_VERSION = 0 857 CURRENT_MARKER_FILE_VERSION = 0
invesalius/control.py
@@ -69,7 +69,7 @@ class Controller(): @@ -69,7 +69,7 @@ class Controller():
69 #DICOM = 1 69 #DICOM = 1
70 #TIFF uCT = 2 70 #TIFF uCT = 2
71 self.img_type = 0 71 self.img_type = 0
72 - self.affine = None 72 + self.affine = np.identity(4)
73 73
74 #Init session 74 #Init session
75 session = ses.Session() 75 session = ses.Session()
@@ -340,7 +340,7 @@ class Controller(): @@ -340,7 +340,7 @@ class Controller():
340 if proj.affine: 340 if proj.affine:
341 self.Slice.affine = np.asarray(proj.affine).reshape(4, 4) 341 self.Slice.affine = np.asarray(proj.affine).reshape(4, 4)
342 else: 342 else:
343 - self.Slice.affine = None 343 + self.Slice.affine = np.identity(4)
344 344
345 Publisher.sendMessage('Update threshold limits list', 345 Publisher.sendMessage('Update threshold limits list',
346 threshold_range=proj.threshold_range) 346 threshold_range=proj.threshold_range)
@@ -623,6 +623,8 @@ class Controller(): @@ -623,6 +623,8 @@ class Controller():
623 Publisher.sendMessage(('Set scroll position', 'SAGITAL'),index=proj.matrix_shape[1]/2) 623 Publisher.sendMessage(('Set scroll position', 'SAGITAL'),index=proj.matrix_shape[1]/2)
624 Publisher.sendMessage(('Set scroll position', 'CORONAL'),index=proj.matrix_shape[2]/2) 624 Publisher.sendMessage(('Set scroll position', 'CORONAL'),index=proj.matrix_shape[2]/2)
625 625
  626 + # TODO: Check that this is needed with the new way of using affine
  627 + # now the affine should be at least the identity(4) and never None
626 if self.Slice.affine is not None: 628 if self.Slice.affine is not None:
627 Publisher.sendMessage('Enable Go-to-Coord', status=True) 629 Publisher.sendMessage('Enable Go-to-Coord', status=True)
628 else: 630 else:
@@ -731,6 +733,8 @@ class Controller(): @@ -731,6 +733,8 @@ class Controller():
731 proj.level = self.Slice.window_level 733 proj.level = self.Slice.window_level
732 proj.threshold_range = int(matrix.min()), int(matrix.max()) 734 proj.threshold_range = int(matrix.min()), int(matrix.max())
733 proj.spacing = self.Slice.spacing 735 proj.spacing = self.Slice.spacing
  736 + # TODO: Check that this is needed with the new way of using affine
  737 + # now the affine should be at least the identity(4) and never None
734 if self.Slice.affine is not None: 738 if self.Slice.affine is not None:
735 proj.affine = self.Slice.affine.tolist() 739 proj.affine = self.Slice.affine.tolist()
736 740
invesalius/data/coordinates.py
@@ -371,7 +371,7 @@ def DebugCoordRandom(trk_init, trck_id, ref_mode): @@ -371,7 +371,7 @@ def DebugCoordRandom(trk_init, trck_id, ref_mode):
371 # 371 #
372 # else: 372 # else:
373 373
374 - dx = [-70, 70] 374 + dx = [-30, 30]
375 dt = [-180, 180] 375 dt = [-180, 180]
376 376
377 coord1 = np.array([uniform(*dx), uniform(*dx), uniform(*dx), 377 coord1 = np.array([uniform(*dx), uniform(*dx), uniform(*dx),
invesalius/data/coregistration.py
@@ -427,7 +427,7 @@ class CoordinateCorregistrateNoObject(threading.Thread): @@ -427,7 +427,7 @@ class CoordinateCorregistrateNoObject(threading.Thread):
427 # # seed_aux = pos_world.reshape([1, 4])[0, :3] 427 # # seed_aux = pos_world.reshape([1, 4])[0, :3]
428 # # seed = seed_aux[np.newaxis, :] 428 # # seed = seed_aux[np.newaxis, :]
429 # # 429 # #
430 -# # self.tracts = dtr.compute_tracts(tracker, seed, affine_vtk, True) 430 +# # self.tracts = dtr.compute_and_visualize_tracts(tracker, seed, affine_vtk, True)
431 # 431 #
432 # # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord) 432 # # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)
433 # wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord) 433 # wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord)
@@ -654,7 +654,7 @@ class CoordinateCorregistrateNoObject(threading.Thread): @@ -654,7 +654,7 @@ class CoordinateCorregistrateNoObject(threading.Thread):
654 # # seed_aux = pos_world.reshape([1, 4])[0, :3] 654 # # seed_aux = pos_world.reshape([1, 4])[0, :3]
655 # # seed = seed_aux[np.newaxis, :] 655 # # seed = seed_aux[np.newaxis, :]
656 # 656 #
657 -# # self.tracts = dtr.compute_tracts(tracker, seed, affine_vtk, True) 657 +# # self.tracts = dtr.compute_and_visualize_tracts(tracker, seed, affine_vtk, True)
658 # 658 #
659 # # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord) 659 # # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord)
660 # wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord) 660 # wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord)
invesalius/data/styles.py
@@ -544,7 +544,7 @@ class TractsInteractorStyle(CrossInteractorStyle): @@ -544,7 +544,7 @@ class TractsInteractorStyle(CrossInteractorStyle):
544 544
545 def OnTractsMouseClick(self, obj, evt): 545 def OnTractsMouseClick(self, obj, evt):
546 # print("Single mouse click") 546 # print("Single mouse click")
547 - # self.tracts = dtr.compute_tracts(self.tracker, self.seed, self.left_pressed) 547 + # self.tracts = dtr.compute_and_visualize_tracts(self.tracker, self.seed, self.left_pressed)
548 self.ChangeTracts(True) 548 self.ChangeTracts(True)
549 549
550 def OnTractsReleaseLeftButton(self, obj, evt): 550 def OnTractsReleaseLeftButton(self, obj, evt):
@@ -554,7 +554,7 @@ class TractsInteractorStyle(CrossInteractorStyle): @@ -554,7 +554,7 @@ class TractsInteractorStyle(CrossInteractorStyle):
554 554
555 def ChangeTracts(self, pressed): 555 def ChangeTracts(self, pressed):
556 # print("Trying to compute tracts") 556 # print("Trying to compute tracts")
557 - self.tracts = dtr.compute_tracts(self.tracker, self.seed, self.affine_vtk, pressed) 557 + self.tracts = dtr.compute_and_visualize_tracts(self.tracker, self.seed, self.affine_vtk, pressed)
558 # mouse_x, mouse_y = iren.GetEventPosition() 558 # mouse_x, mouse_y = iren.GetEventPosition()
559 # wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) 559 # wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker)
560 # px, py = self.viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) 560 # px, py = self.viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz)
invesalius/data/surface.py
@@ -365,9 +365,16 @@ class SurfaceManager(): @@ -365,9 +365,16 @@ class SurfaceManager():
365 if self.convert2inv: 365 if self.convert2inv:
366 # convert between invesalius and world space with shift in the Y coordinate 366 # convert between invesalius and world space with shift in the Y coordinate
367 affine = sl.Slice().affine 367 affine = sl.Slice().affine
  368 + # TODO: Check that this is needed with the new way of using affine
  369 + # now the affine should be at least the identity(4) and never None
368 if affine is not None: 370 if affine is not None:
369 - affine[1, -1] -= sl.Slice().spacing[1] * (sl.Slice().matrix.shape[1] - 1) 371 + matrix_shape = sl.Slice().matrix.shape
  372 + spacing = sl.Slice().spacing
  373 + img_shift = spacing[1] * (matrix_shape[1] - 1)
  374 + affine = sl.Slice().affine.copy()
  375 + affine[1, -1] -= img_shift
370 affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) 376 affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)
  377 +
371 actor.SetUserMatrix(affine_vtk) 378 actor.SetUserMatrix(affine_vtk)
372 379
373 if overwrite: 380 if overwrite:
invesalius/data/tractography.py
@@ -29,14 +29,11 @@ import time @@ -29,14 +29,11 @@ import time
29 import numpy as np 29 import numpy as np
30 import queue 30 import queue
31 from invesalius.pubsub import pub as Publisher 31 from invesalius.pubsub import pub as Publisher
32 -from scipy.stats import norm  
33 import vtk 32 import vtk
34 33
35 import invesalius.constants as const 34 import invesalius.constants as const
36 import invesalius.data.imagedata_utils as img_utils 35 import invesalius.data.imagedata_utils as img_utils
37 36
38 -import invesalius.project as prj  
39 -  
40 # Nice print for arrays 37 # Nice print for arrays
41 # np.set_printoptions(precision=2) 38 # np.set_printoptions(precision=2)
42 # np.set_printoptions(suppress=True) 39 # np.set_printoptions(suppress=True)
@@ -104,75 +101,57 @@ def compute_tubes(trk, direction): @@ -104,75 +101,57 @@ def compute_tubes(trk, direction):
104 return trk_tube 101 return trk_tube
105 102
106 103
107 -def combine_tracts_root(out_list, root, n_block): 104 +def create_branch(out_list, n_block):
108 """Adds a set of tracts to given position in a given vtkMultiBlockDataSet 105 """Adds a set of tracts to given position in a given vtkMultiBlockDataSet
109 106
110 :param out_list: List of vtkTubeFilters representing the tracts 107 :param out_list: List of vtkTubeFilters representing the tracts
111 :type out_list: list 108 :type out_list: list
112 - :param root: A collection of tracts as a vtkMultiBlockDataSet  
113 - :type root: vtkMultiBlockDataSet  
114 :param n_block: The location in the given vtkMultiBlockDataSet to insert the new tracts 109 :param n_block: The location in the given vtkMultiBlockDataSet to insert the new tracts
115 :type n_block: int 110 :type n_block: int
116 - :return: The updated collection of tracts as a vtkMultiBlockDataSet  
117 - :rtype: vtkMultiBlockDataSet  
118 - """  
119 -  
120 - # create tracts only when at least one was computed  
121 - # print("Len outlist in root: ", len(out_list))  
122 - if not out_list.count(None) == len(out_list):  
123 - for n, tube in enumerate(out_list):  
124 - root.SetBlock(n_block + n, tube.GetOutput())  
125 -  
126 - return root  
127 -  
128 -  
129 -def combine_tracts_branch(out_list):  
130 - """Combines a set of tracts in vtkMultiBlockDataSet  
131 -  
132 - :param out_list: List of vtkTubeFilters representing the tracts  
133 - :type out_list: list  
134 - :return: A collection of tracts as a vtkMultiBlockDataSet 111 + :return: The collection of tracts (streamlines) as a vtkMultiBlockDataSet
135 :rtype: vtkMultiBlockDataSet 112 :rtype: vtkMultiBlockDataSet
136 """ 113 """
137 114
  115 + # create a branch and add the streamlines
138 branch = vtk.vtkMultiBlockDataSet() 116 branch = vtk.vtkMultiBlockDataSet()
  117 +
139 # create tracts only when at least one was computed 118 # create tracts only when at least one was computed
140 # print("Len outlist in root: ", len(out_list)) 119 # print("Len outlist in root: ", len(out_list))
  120 + # TODO: check if this if statement is required, because we should
  121 + # call this function only when tracts exist
141 if not out_list.count(None) == len(out_list): 122 if not out_list.count(None) == len(out_list):
142 for n, tube in enumerate(out_list): 123 for n, tube in enumerate(out_list):
143 - branch.SetBlock(n, tube.GetOutput()) 124 + branch.SetBlock(n_block + n, tube.GetOutput())
144 125
145 return branch 126 return branch
146 127
147 128
148 -def tracts_computation(trk_list, root, n_tracts): 129 +def compute_tracts(trk_list, n_tract=0, alpha=255):
149 """Convert the list of all computed tracts given by Trekker run and returns a vtkMultiBlockDataSet 130 """Convert the list of all computed tracts given by Trekker run and returns a vtkMultiBlockDataSet
150 131
151 :param trk_list: List of lists containing the computed tracts and corresponding coordinates 132 :param trk_list: List of lists containing the computed tracts and corresponding coordinates
152 :type trk_list: list 133 :type trk_list: list
153 - :param root: A collection of tracts as a vtkMultiBlockDataSet  
154 - :type root: vtkMultiBlockDataSet  
155 - :param n_tracts:  
156 - :type n_tracts: int 134 + :param n_tract: The integer ID of the block in the vtkMultiBlockDataSet
  135 + :type n_tract: int
  136 + :param alpha: The transparency of the streamlines from 0 to 255 (transparent to opaque)
  137 + :type alpha: int
157 :return: The updated collection of tracts as a vtkMultiBlockDataSet 138 :return: The updated collection of tracts as a vtkMultiBlockDataSet
158 :rtype: vtkMultiBlockDataSet 139 :rtype: vtkMultiBlockDataSet
159 """ 140 """
160 141
161 # Transform tracts to array 142 # Transform tracts to array
162 trk_arr = [np.asarray(trk_n).T if trk_n else None for trk_n in trk_list] 143 trk_arr = [np.asarray(trk_n).T if trk_n else None for trk_n in trk_list]
163 -  
164 # Compute the directions 144 # Compute the directions
165 - trk_dir = [compute_directions(trk_n) for trk_n in trk_arr]  
166 - 145 + trk_dir = [compute_directions(trk_n, alpha) for trk_n in trk_arr]
167 # Compute the vtk tubes 146 # Compute the vtk tubes
168 out_list = [compute_tubes(trk_arr_n, trk_dir_n) for trk_arr_n, trk_dir_n in zip(trk_arr, trk_dir)] 147 out_list = [compute_tubes(trk_arr_n, trk_dir_n) for trk_arr_n, trk_dir_n in zip(trk_arr, trk_dir)]
  148 + # create a branch and add the tracts
  149 + branch = create_branch(out_list, n_tract)
169 150
170 - root = combine_tracts_root(out_list, root, n_tracts)  
171 -  
172 - return root 151 + return branch
173 152
174 153
175 -def compute_tracts(trekker, position, affine, affine_vtk, n_tracts): 154 +def compute_and_visualize_tracts(trekker, position, affine, affine_vtk, n_tracts_max):
176 """ Compute tractograms using the Trekker library. 155 """ Compute tractograms using the Trekker library.
177 156
178 :param trekker: Trekker library instance 157 :param trekker: Trekker library instance
@@ -183,52 +162,51 @@ def compute_tracts(trekker, position, affine, affine_vtk, n_tracts): @@ -183,52 +162,51 @@ def compute_tracts(trekker, position, affine, affine_vtk, n_tracts):
183 :type affine: numpy.ndarray 162 :type affine: numpy.ndarray
184 :param affine_vtk: vtkMatrix4x4 isntance with affine transformation matrix 163 :param affine_vtk: vtkMatrix4x4 isntance with affine transformation matrix
185 :type affine_vtk: vtkMatrix4x4 164 :type affine_vtk: vtkMatrix4x4
186 - :param n_tracts: number of tracts to compute  
187 - :type n_tracts: int 165 + :param n_tracts_max: maximum number of tracts to compute
  166 + :type n_tracts_max: int
188 """ 167 """
189 168
190 - # during neuronavigation, root needs to be initialized outside the while loop so the new tracts  
191 - # can be appended to the root block set  
192 - root = vtk.vtkMultiBlockDataSet() 169 + # root = vtk.vtkMultiBlockDataSet()
193 # Juuso's 170 # Juuso's
194 # seed = np.array([[-8.49, -8.39, 2.5]]) 171 # seed = np.array([[-8.49, -8.39, 2.5]])
195 # Baran M1 172 # Baran M1
196 # seed = np.array([[27.53, -77.37, 46.42]]) 173 # seed = np.array([[27.53, -77.37, 46.42]])
197 seed_trk = img_utils.convert_world_to_voxel(position, affine) 174 seed_trk = img_utils.convert_world_to_voxel(position, affine)
198 - # print("seed example: {}".format(seed_trk))  
199 - trekker.seed_coordinates(np.repeat(seed_trk, n_tracts, axis=0))  
200 - # print("trk list len: ", len(trekker.run()))  
201 - trk_list = trekker.run()  
202 - if trk_list:  
203 - root = tracts_computation(trk_list, root, 0)  
204 - Publisher.sendMessage('Remove tracts')  
205 - Publisher.sendMessage('Update tracts', root=root, affine_vtk=affine_vtk, coord_offset=position)  
206 - else:  
207 - Publisher.sendMessage('Remove tracts')  
208 -  
209 -  
210 -def tracts_computation_branch(trk_list, alpha=255):  
211 - """Convert the list of all computed tracts given by Trekker run and returns a vtkMultiBlockDataSet  
212 -  
213 - :param trk_list: List of lists containing the computed tracts and corresponding coordinates  
214 - :type trk_list: list  
215 - :param alpha: opacity value in the interval [0, 255]. The 0 is no opacity (total transparency).  
216 - :type alpha: int  
217 - :return: The collection of tracts as a vtkMultiBlockDataSet  
218 - :rtype: vtkMultiBlockDataSet  
219 - """  
220 - # Transform tracts to array  
221 - trk_arr = [np.asarray(trk_n).T if trk_n else None for trk_n in trk_list]  
222 - # Compute the directions  
223 - trk_dir = [compute_directions(trk_n, alpha) for trk_n in trk_arr]  
224 - # Compute the vtk tubes  
225 - tube_list = [compute_tubes(trk_arr_n, trk_dir_n) for trk_arr_n, trk_dir_n in zip(trk_arr, trk_dir)]  
226 - branch = combine_tracts_branch(tube_list)  
227 -  
228 - return branch 175 + bundle = vtk.vtkMultiBlockDataSet()
  176 + n_branches, n_tracts, count_loop = 0, 0, 0
  177 + n_threads = 2 * const.N_CPU - 1
  178 +
  179 + while n_tracts < n_tracts_max:
  180 + n_param = 1 + (count_loop % 10)
  181 + # rescale the alpha value that defines the opacity of the branch
  182 + # the n interval is [1, 10] and the new interval is [51, 255]
  183 + # the new interval is defined to have no 0 opacity (minimum is 51, i.e., 20%)
  184 + alpha = (n_param - 1) * (255 - 51) / (10 - 1) + 51
  185 + trekker.minFODamp(n_param * 0.01)
  186 +
  187 + # print("seed example: {}".format(seed_trk))
  188 + trekker.seed_coordinates(np.repeat(seed_trk, n_threads, axis=0))
  189 + # print("trk list len: ", len(trekker.run()))
  190 + trk_list = trekker.run()
  191 + n_tracts += len(trk_list)
  192 + if len(trk_list):
  193 + branch = compute_tracts(trk_list, n_tract=0, alpha=alpha)
  194 + bundle.SetBlock(n_branches, branch)
  195 + n_branches += 1
  196 +
  197 + count_loop += 1
  198 +
  199 + if (count_loop == 20) and (n_tracts == 0):
  200 + break
  201 +
  202 + Publisher.sendMessage('Remove tracts')
  203 + if n_tracts:
  204 + Publisher.sendMessage('Update tracts', root=bundle, affine_vtk=affine_vtk,
  205 + coord_offset=position, coord_offset_w=seed_trk[0].tolist())
229 206
230 207
231 class ComputeTractsThread(threading.Thread): 208 class ComputeTractsThread(threading.Thread):
  209 + # TODO: Remove this class and create a case where no ACT is provided in the class ComputeTractsACTThread
232 210
233 def __init__(self, inp, queues, event, sle): 211 def __init__(self, inp, queues, event, sle):
234 """Class (threading) to compute real time tractography data for visualization. 212 """Class (threading) to compute real time tractography data for visualization.
@@ -265,6 +243,7 @@ class ComputeTractsThread(threading.Thread): @@ -265,6 +243,7 @@ class ComputeTractsThread(threading.Thread):
265 243
266 trekker, affine, offset, n_tracts_total, seed_radius, n_threads, act_data, affine_vtk, img_shift = self.inp 244 trekker, affine, offset, n_tracts_total, seed_radius, n_threads, act_data, affine_vtk, img_shift = self.inp
267 # n_threads = n_tracts_total 245 # n_threads = n_tracts_total
  246 + n_threads = int(n_threads/4)
268 p_old = np.array([[0., 0., 0.]]) 247 p_old = np.array([[0., 0., 0.]])
269 n_tracts = 0 248 n_tracts = 0
270 249
@@ -312,13 +291,13 @@ class ComputeTractsThread(threading.Thread): @@ -312,13 +291,13 @@ class ComputeTractsThread(threading.Thread):
312 # run the trekker, this is the slowest line of code, be careful to just use once! 291 # run the trekker, this is the slowest line of code, be careful to just use once!
313 trk_list = trekker.run() 292 trk_list = trekker.run()
314 293
315 - if trk_list: 294 + if len(trk_list) > 2:
316 # print("dist: {}".format(dist)) 295 # print("dist: {}".format(dist))
317 if dist >= seed_radius: 296 if dist >= seed_radius:
318 # when moving the coil further than the seed_radius restart the bundle computation 297 # when moving the coil further than the seed_radius restart the bundle computation
319 bundle = vtk.vtkMultiBlockDataSet() 298 bundle = vtk.vtkMultiBlockDataSet()
320 n_branches = 0 299 n_branches = 0
321 - branch = tracts_computation_branch(trk_list) 300 + branch = compute_tracts(trk_list, n_tract=0, alpha=255)
322 bundle.SetBlock(n_branches, branch) 301 bundle.SetBlock(n_branches, branch)
323 n_branches += 1 302 n_branches += 1
324 n_tracts = branch.GetNumberOfBlocks() 303 n_tracts = branch.GetNumberOfBlocks()
@@ -326,7 +305,7 @@ class ComputeTractsThread(threading.Thread): @@ -326,7 +305,7 @@ class ComputeTractsThread(threading.Thread):
326 # TODO: maybe keep computing even if reaches the maximum 305 # TODO: maybe keep computing even if reaches the maximum
327 elif dist < seed_radius and n_tracts < n_tracts_total: 306 elif dist < seed_radius and n_tracts < n_tracts_total:
328 # compute tracts blocks and add to bungle until reaches the maximum number of tracts 307 # compute tracts blocks and add to bungle until reaches the maximum number of tracts
329 - branch = tracts_computation_branch(trk_list) 308 + branch = compute_tracts(trk_list, n_tract=0, alpha=255)
330 if bundle: 309 if bundle:
331 bundle.SetBlock(n_branches, branch) 310 bundle.SetBlock(n_branches, branch)
332 n_tracts += branch.GetNumberOfBlocks() 311 n_tracts += branch.GetNumberOfBlocks()
@@ -361,314 +340,218 @@ class ComputeTractsThread(threading.Thread): @@ -361,314 +340,218 @@ class ComputeTractsThread(threading.Thread):
361 340
362 class ComputeTractsACTThread(threading.Thread): 341 class ComputeTractsACTThread(threading.Thread):
363 342
364 - def __init__(self, inp, queues, event, sle): 343 + def __init__(self, input_list, queues, event, sleep_thread):
365 """Class (threading) to compute real time tractography data for visualization. 344 """Class (threading) to compute real time tractography data for visualization.
366 345
367 Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/) 346 Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/)
368 For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one 347 For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one
369 vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as 348 vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as
370 bundle, to obtain fast computation and visualization. The bundle dataset is mapped to a single vtkActor. 349 bundle, to obtain fast computation and visualization. The bundle dataset is mapped to a single vtkActor.
371 - Mapper and Actor are computer in the data/viewer_volume.py module for easier handling in the invesalius 3D scene. 350 + Mapper and Actor are computer in the data/viewer_volume.py module for easier handling in the
  351 + invesalius 3D scene.
372 352
373 Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation 353 Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation
374 354
375 - :param inp: List of inputs: trekker instance, affine numpy array, seed_offset, seed_radius, n_threads  
376 - :type inp: list 355 + :param input_list: List of inputs: trekker instance, affine numpy array, seed offset, total number of tracts,
  356 + seed radius, number of threads in computer, ACT data array, affine vtk matrix,
  357 + image shift for vtk to mri transformation
  358 + :type input_list: list
377 :param queues: Queue list with coord_tracts_queue (Queue instance that manage co-registered coordinates) and 359 :param queues: Queue list with coord_tracts_queue (Queue instance that manage co-registered coordinates) and
378 tracts_queue (Queue instance that manage the tracts to be visualized) 360 tracts_queue (Queue instance that manage the tracts to be visualized)
379 :type queues: list[queue.Queue, queue.Queue] 361 :type queues: list[queue.Queue, queue.Queue]
380 :param event: Threading event to coordinate when tasks as done and allow UI release 362 :param event: Threading event to coordinate when tasks as done and allow UI release
381 :type event: threading.Event 363 :type event: threading.Event
382 - :param sle: Sleep pause in seconds  
383 - :type sle: float 364 + :param sleep_thread: Sleep pause in seconds
  365 + :type sleep_thread: float
384 """ 366 """
385 367
386 threading.Thread.__init__(self, name='ComputeTractsThreadACT') 368 threading.Thread.__init__(self, name='ComputeTractsThreadACT')
387 - self.inp = inp  
388 - # self.coord_queue = coord_queue 369 + self.input_list = input_list
389 self.coord_tracts_queue = queues[0] 370 self.coord_tracts_queue = queues[0]
390 self.tracts_queue = queues[1] 371 self.tracts_queue = queues[1]
391 - # on first pilots (january 12, 2021) used (-4, 4)  
392 - self.coord_list_w = img_utils.create_grid((-2, 2), (0, 20), inp[2]-5, 1)  
393 - # self.coord_list_sph = img_utils.create_spherical_grid(10, 1)  
394 - # self.coord_list_sph = img_utils.create_spherical_grid(10, 1)  
395 - # x_norm = np.linspace(norm.ppf(0.01), norm.ppf(0.99), 2*self.coord_list_sph.shape[0])  
396 - # self.pdf = np.flipud(norm.pdf(x_norm[:self.coord_list_sph.shape[0]], loc=0, scale=2.))  
397 - # self.sph_idx = np.linspace(0, self.coord_list_sph.shape[0], num=self.coord_list_sph.shape[0], dtype=int)  
398 - # self.visualization_queue = visualization_queue  
399 self.event = event 372 self.event = event
400 - self.sle = sle  
401 -  
402 - # prj_data = prj.Project()  
403 - # matrix_shape = tuple(prj_data.matrix_shape)  
404 - # self.img_shift = matrix_shape[1] 373 + self.sleep_thread = sleep_thread
405 374
406 def run(self): 375 def run(self):
407 376
408 - # trekker, affine, offset, n_tracts_total, seed_radius, n_threads = self.inp  
409 - trekker, affine, offset, n_tracts_total, seed_radius, n_threads, act_data, affine_vtk, img_shift = self.inp 377 + trekker, affine, offset, n_tracts_total, seed_radius, n_threads, act_data, affine_vtk, img_shift = self.input_list
410 378
411 - # n_threads = n_tracts_total  
412 p_old = np.array([[0., 0., 0.]]) 379 p_old = np.array([[0., 0., 0.]])
413 - p_old_pre = np.array([[0., 0., 0.]])  
414 - coord_offset = None  
415 - n_tracts = 0  
416 - n_branches = 0 380 + n_branches, n_tracts, count_loop = 0, 0, 0
417 bundle = None 381 bundle = None
418 - sph_sampling = True  
419 dist_radius = 1.5 382 dist_radius = 1.5
420 383
421 - xyz = img_utils.random_sample_sphere(radius=seed_radius, size=100)  
422 - coord_list_sph = np.hstack([xyz, np.ones([xyz.shape[0], 1])]).T 384 + # TODO: Try a denser and bigger grid, because it's just a matrix multiplication
  385 + # maybe 15 mm below the coil offset by default and 5 cm deep
  386 + # create the rectangular grid to find the gray-white matter boundary
  387 + coord_list_w = img_utils.create_grid((-2, 2), (0, 20), offset - 5, 1)
  388 +
  389 + # create the spherical grid to sample around the seed location
  390 + samples_in_sphere = img_utils.random_sample_sphere(radius=seed_radius, size=100)
  391 + coord_list_sphere = np.hstack([samples_in_sphere, np.ones([samples_in_sphere.shape[0], 1])]).T
423 m_seed = np.identity(4) 392 m_seed = np.identity(4)
  393 +
424 # Compute the tracts 394 # Compute the tracts
425 - # print('ComputeTractsThread: event {}'.format(self.event.is_set()))  
426 while not self.event.is_set(): 395 while not self.event.is_set():
427 try: 396 try:
428 - # print("Computing tracts")  
429 # get from the queue the coordinates, coregistration transformation matrix, and flipped matrix 397 # get from the queue the coordinates, coregistration transformation matrix, and flipped matrix
430 m_img_flip = self.coord_tracts_queue.get_nowait() 398 m_img_flip = self.coord_tracts_queue.get_nowait()
431 - # coord, m_img, m_img_flip = self.coord_queue.get_nowait()  
432 - # print('ComputeTractsThread: get {}'.format(count))  
433 -  
434 - # TODO: Remove this is not needed  
435 - # 20200402: in this new refactored version the m_img comes different than the position  
436 - # the new version m_img is already flixped in y, which means that Y is negative  
437 - # if only the Y is negative maybe no need for the flip_x funtcion at all in the navigation  
438 - # but check all coord_queue before why now the m_img comes different than position  
439 - # 20200403: indeed flip_x is just a -1 multiplication to the Y coordinate, remove function flip_x  
440 - # m_img_flip = m_img.copy()  
441 - # m_img_flip[1, -1] = -m_img_flip[1, -1]  
442 399
443 # DEBUG: Uncomment the m_img_flip below so that distance is fixed and tracts keep computing 400 # DEBUG: Uncomment the m_img_flip below so that distance is fixed and tracts keep computing
444 # m_img_flip[:3, -1] = (5., 10., 12.) 401 # m_img_flip[:3, -1] = (5., 10., 12.)
445 - dist = abs(np.linalg.norm(p_old_pre - np.asarray(m_img_flip[:3, -1])))  
446 - p_old_pre = m_img_flip[:3, -1].copy()  
447 -  
448 - # Uncertanity visualization --  
449 - # each tract branch is computed with one set of parameters ajusted from 1 to 10  
450 - n_param = 1 + (n_branches % 10) 402 + dist = abs(np.linalg.norm(p_old - np.asarray(m_img_flip[:3, -1])))
  403 + p_old = m_img_flip[:3, -1].copy()
  404 +
  405 + # Uncertainty visualization --
  406 + # each tract branch is computed with one minFODamp adjusted from 0.01 to 0.1
  407 + # the higher the minFODamp the more the streamlines are faithful to the data, so they become more strict
  408 + # but also may loose some connections.
  409 + # the lower the more relaxed streamline also increases the chance of false positives
  410 + n_param = 1 + (count_loop % 10)
451 # rescale the alpha value that defines the opacity of the branch 411 # rescale the alpha value that defines the opacity of the branch
452 # the n interval is [1, 10] and the new interval is [51, 255] 412 # the n interval is [1, 10] and the new interval is [51, 255]
453 # the new interval is defined to have no 0 opacity (minimum is 51, i.e., 20%) 413 # the new interval is defined to have no 0 opacity (minimum is 51, i.e., 20%)
454 alpha = (n_param - 1) * (255 - 51) / (10 - 1) + 51 414 alpha = (n_param - 1) * (255 - 51) / (10 - 1) + 51
455 trekker.minFODamp(n_param * 0.01) 415 trekker.minFODamp(n_param * 0.01)
456 - trekker.dataSupportExponent(n_param * 0.1)  
457 # --- 416 # ---
458 417
  418 + try:
  419 + # The original seed location is replaced by the gray-white matter interface that is closest to
  420 + # the coil center
  421 + coord_list_w_tr = m_img_flip @ coord_list_w
  422 + coord_offset = grid_offset(act_data, coord_list_w_tr, img_shift)
  423 + except IndexError:
  424 + # This error might be caused by the coordinate exceeding the image array dimensions.
  425 + # This happens during navigation because the coil location can have coordinates outside the image
  426 + # boundaries
  427 + # Translate the coordinate along the normal vector of the object/coil
  428 + coord_offset = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2]
  429 + # ---
  430 +
  431 + # Spherical sampling of seed coordinates ---
  432 + # compute the samples of a sphere centered on seed coordinate offset by the grid
  433 + # given in the invesalius-vtk space
  434 + samples = np.random.choice(coord_list_sphere.shape[1], size=100)
  435 + m_seed[:-1, -1] = coord_offset.copy()
  436 + # translate the spherical grid samples to the coil location in invesalius-vtk space
  437 + seed_trk_r_inv = m_seed @ coord_list_sphere[:, samples]
  438 +
  439 + coord_offset_w = np.hstack((coord_offset, 1.0)).reshape([4, 1])
  440 +
  441 + try:
  442 + # Anatomic constrained seed computation ---
  443 + # find only the samples inside the white matter as with the ACT enabled in the Trekker,
  444 + # it does not compute any tracts outside the white matter
  445 + # convert from inveslaius-vtk to mri space
  446 + seed_trk_r_mri = seed_trk_r_inv[:3, :].T.astype(int) + np.array([[0, img_shift, 0]], dtype=np.int32)
  447 + labs = act_data[seed_trk_r_mri[..., 0], seed_trk_r_mri[..., 1], seed_trk_r_mri[..., 2]]
  448 + # find all samples in the white matter
  449 + labs_id = np.where(labs == 1)
  450 + # Trekker has internal multiprocessing approach done in C. Here the number of available threads - 1
  451 + # is given, but in case a large number of tracts is requested, it will compute all in parallel
  452 + # automatically for a more fluent navigation, better to compute the maximum number the computer can
  453 + # handle otherwise gets too slow for the multithreading in Python
  454 + seed_trk_r_inv_sampled = seed_trk_r_inv[:, labs_id[0][:n_threads]]
  455 +
  456 + except IndexError:
  457 + # same as on the grid offset above, if the coil is too far from the mri volume the array indices
  458 + # are beyond the mri boundaries
  459 + # in this case use the grid center instead of the spherical samples
  460 + seed_trk_r_inv_sampled = coord_offset_w.copy()
  461 +
  462 + # convert to the world coordinate system for trekker
  463 + seed_trk_r_world_sampled = np.linalg.inv(affine) @ seed_trk_r_inv_sampled
  464 + seed_trk_r_world_sampled = seed_trk_r_world_sampled.T[:, :3]
  465 +
  466 + # convert to the world coordinate system for saving in the marker list
  467 + coord_offset_w = np.linalg.inv(affine) @ coord_offset_w
  468 + coord_offset_w = np.squeeze(coord_offset_w.T[:, :3])
  469 +
  470 + # DEBUG: uncomment the seed_trk below
  471 + # seed_trk.shape == [1, 3]
  472 + # Juuso's
  473 + # seed_trk = np.array([[-8.49, -8.39, 2.5]])
  474 + # Baran M1
  475 + # seed_trk = np.array([[27.53, -77.37, 46.42]])
  476 + # print("Given: {}".format(seed_trk.shape))
  477 + # print("Seed: {}".format(seed))
  478 + # joonas seed that has good tracts
  479 + # seed_trk = np.array([[29.12, -13.33, 31.65]])
  480 + # seed_trk_img = np.array([[117, 127, 161]])
  481 +
459 # When moving the coil further than the seed_radius restart the bundle computation 482 # When moving the coil further than the seed_radius restart the bundle computation
460 # Currently, it stops to compute tracts when the maximum number of tracts is reached maybe keep 483 # Currently, it stops to compute tracts when the maximum number of tracts is reached maybe keep
461 # computing even if reaches the maximum 484 # computing even if reaches the maximum
462 if dist >= dist_radius: 485 if dist >= dist_radius:
463 - # Anatomic constrained seed computation ---  
464 - # The original seed location is replaced by the gray-white matter interface that is closest to  
465 - # the coil center  
466 - try:  
467 - #TODO: Create a dialog error to say when the ACT data is not loaded and prevent  
468 - # the interface from freezing. Give the user a chance to load it (maybe in task_navigator)  
469 - coord_list_w_tr = m_img_flip @ self.coord_list_w  
470 - coord_offset = grid_offset(act_data, coord_list_w_tr, img_shift)  
471 - except IndexError:  
472 - # This error might be caused by the coordinate exceeding the image array dimensions.  
473 - # Needs further verification.  
474 - coord_offset = None  
475 - # ---  
476 -  
477 - # Translate the coordinate along the normal vector of the object/coil ---  
478 - if coord_offset is None:  
479 - # apply the coil transformation matrix  
480 - coord_offset = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2]  
481 - # ---  
482 -  
483 - # convert the world coordinates to the voxel space for using as a seed in Trekker  
484 - # seed_trk.shape == [1, 3]  
485 - seed_trk = img_utils.convert_world_to_voxel(coord_offset, affine)  
486 - # print("Desired: {}".format(seed_trk.shape))  
487 -  
488 - # DEBUG: uncomment the seed_trk below  
489 - # Juuso's  
490 - # seed_trk = np.array([[-8.49, -8.39, 2.5]])  
491 - # Baran M1  
492 - # seed_trk = np.array([[27.53, -77.37, 46.42]])  
493 - # print("Given: {}".format(seed_trk.shape))  
494 - # print("Seed: {}".format(seed))  
495 -  
496 - # Spherical sampling of seed coordinates ---  
497 - if sph_sampling:  
498 - # CHECK: We use ACT only for the origin seed, but not for all the other coordinates.  
499 - # Check how this can be solved. Applying ACT to all coordinates is maybe too much.  
500 - # Maybe it doesn't matter because the ACT is just to help finding the closest location to  
501 - # the TMS coil center. Also, note that the spherical sampling is applied only when the coil  
502 - # location changes, all further iterations used the fixed n_threads samples to compute the  
503 - # remaining tracts.  
504 -  
505 - # samples = np.random.choice(self.sph_idx, size=n_threads, p=self.pdf)  
506 - # m_seed[:-1, -1] = seed_trk  
507 - # sph_seed = m_seed @ self.coord_list_sph  
508 - # seed_trk_r = sph_seed[samples, :]  
509 - samples = np.random.choice(coord_list_sph.shape[1], size=n_threads)  
510 - m_seed[:-1, -1] = seed_trk  
511 - seed_trk_r = m_seed @ coord_list_sph[:, samples]  
512 - seed_trk_r = seed_trk_r[:-1, :].T  
513 - else:  
514 - # set the seeds for trekker, one seed is repeated n_threads times  
515 - # shape (24, 3)  
516 - seed_trk_r = np.repeat(seed_trk, n_threads, axis=0)  
517 -  
518 - # ---  
519 -  
520 - # Trekker has internal multiprocessing approach done in C. Here the number of available threads is  
521 - # given, but in case a large number of tracts is requested, it will compute all in parallel  
522 - # automatically for a more fluent navigation, better to compute the maximum number the computer  
523 - # handles  
524 - trekker.seed_coordinates(seed_trk_r)  
525 - # trekker.seed_coordinates(np.repeat(seed_trk, n_threads, axis=0)) 486 + bundle = None
  487 + n_tracts, n_branches = 0, 0
526 488
  489 + # we noticed that usually the navigation lags or crashes when moving the coil location
  490 + # to reduce the overhead for when the coil is moving, we compute only half the number of tracts
  491 + # that should be computed when the coil is fixed in the same location
  492 + # required input is Nx3 array
  493 + trekker.seed_coordinates(seed_trk_r_world_sampled[::2, :])
527 # run the trekker, this is the slowest line of code, be careful to just use once! 494 # run the trekker, this is the slowest line of code, be careful to just use once!
528 trk_list = trekker.run() 495 trk_list = trekker.run()
529 496
530 # check if any tract was found, otherwise doesn't count 497 # check if any tract was found, otherwise doesn't count
531 - if trk_list: 498 + if len(trk_list):
  499 + # a bundle consists for multiple branches and each branch consists of multiple streamlines
  500 + # every iteration in the main loop adds a branch to the bundle
  501 + branch = compute_tracts(trk_list, n_tract=0, alpha=alpha)
  502 + n_tracts = branch.GetNumberOfBlocks()
  503 +
  504 + # create and add branch to the bundle
532 bundle = vtk.vtkMultiBlockDataSet() 505 bundle = vtk.vtkMultiBlockDataSet()
533 - branch = tracts_computation_branch(trk_list, alpha)  
534 bundle.SetBlock(n_branches, branch) 506 bundle.SetBlock(n_branches, branch)
535 n_branches = 1 507 n_branches = 1
536 - n_tracts = branch.GetNumberOfBlocks()  
537 - else:  
538 - bundle = None  
539 - n_branches = 0  
540 - n_tracts = 0  
541 508
542 elif dist < dist_radius and n_tracts < n_tracts_total: 509 elif dist < dist_radius and n_tracts < n_tracts_total:
  510 + # when the coil is fixed in place and the number of tracts is smaller than the total
  511 + if not bundle:
  512 + # same as above, when creating the bundle (vtkMultiBlockDataSet) we only compute half the number
  513 + # of tracts to reduce the overhead
  514 + bundle = vtk.vtkMultiBlockDataSet()
  515 + # required input is Nx3 array
  516 + trekker.seed_coordinates(seed_trk_r_world_sampled[::2, :])
  517 + n_tracts, n_branches = 0, 0
  518 + else:
  519 + # if the bundle exists compute all tracts requested
  520 + # required input is Nx3 array
  521 + trekker.seed_coordinates(seed_trk_r_world_sampled)
  522 +
543 trk_list = trekker.run() 523 trk_list = trekker.run()
544 - if trk_list: 524 +
  525 + if len(trk_list):
545 # compute tract blocks and add to bundle until reaches the maximum number of tracts 526 # compute tract blocks and add to bundle until reaches the maximum number of tracts
546 # the alpha changes depending on the parameter set 527 # the alpha changes depending on the parameter set
547 - branch = tracts_computation_branch(trk_list, alpha)  
548 - if bundle:  
549 - bundle.SetBlock(n_branches, branch)  
550 - n_tracts += branch.GetNumberOfBlocks()  
551 - n_branches += 1  
552 - else:  
553 - bundle = vtk.vtkMultiBlockDataSet()  
554 - bundle.SetBlock(n_branches, branch)  
555 - n_branches = 1  
556 - n_tracts = branch.GetNumberOfBlocks()  
557 - # else:  
558 - # bundle = None 528 + branch = compute_tracts(trk_list, n_tract=0, alpha=alpha)
  529 + n_tracts += branch.GetNumberOfBlocks()
  530 + # add branch to the bundle
  531 + bundle.SetBlock(n_branches, branch)
  532 + n_branches += 1
559 533
560 - # else:  
561 - # bundle = None 534 + # keep adding to the number of loops even if the tracts were not find
  535 + # this will keep the minFODamp changing and new seed coordinates being tried which would allow
  536 + # higher probability of finding tracts
  537 + count_loop += 1
562 538
563 - # rethink if this should be inside the if condition, it may lock the thread if no tracts are found  
564 - # use no wait to ensure maximum speed and avoid visualizing old tracts in the queue, this might 539 + # use "nowait" to ensure maximum speed and avoid visualizing old tracts in the queue, this might
565 # be more evident in slow computer or for heavier tract computations, it is better slow update 540 # be more evident in slow computer or for heavier tract computations, it is better slow update
566 # than visualizing old data 541 # than visualizing old data
567 - # self.visualization_queue.put_nowait([coord, m_img, bundle])  
568 - self.tracts_queue.put_nowait((bundle, affine_vtk, coord_offset))  
569 - # print('ComputeTractsThread: put {}'.format(count))  
570 - 542 + self.tracts_queue.put_nowait((bundle, affine_vtk, coord_offset, coord_offset_w))
571 self.coord_tracts_queue.task_done() 543 self.coord_tracts_queue.task_done()
572 - # self.coord_queue.task_done()  
573 - # print('ComputeTractsThread: done {}'.format(count))  
574 544
575 # sleep required to prevent user interface from being unresponsive 545 # sleep required to prevent user interface from being unresponsive
576 - time.sleep(self.sle) 546 + time.sleep(self.sleep_thread)
577 # if no coordinates pass 547 # if no coordinates pass
578 except queue.Empty: 548 except queue.Empty:
579 - # print("Empty queue in tractography")  
580 pass 549 pass
581 # if queue is full mark as done (may not be needed in this new "nowait" method) 550 # if queue is full mark as done (may not be needed in this new "nowait" method)
582 except queue.Full: 551 except queue.Full:
583 - # self.coord_queue.task_done()  
584 self.coord_tracts_queue.task_done() 552 self.coord_tracts_queue.task_done()
585 553
586 554
587 -class ComputeTractsThreadSingleBlock(threading.Thread):  
588 -  
589 - def __init__(self, inp, affine_vtk, coord_queue, visualization_queue, event, sle):  
590 - """Class (threading) to compute real time tractography data for visualization in a single loop.  
591 -  
592 - Different than ComputeTractsThread because it does not keep adding tracts to the bundle until maximum,  
593 - is reached. It actually compute all requested tracts at once. (Might be deleted in the future)!  
594 - Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/)  
595 - For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one  
596 - vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as  
597 - bundle, to obtain fast computation and visualization. The bundle dataset is mapped to a single vtkActor.  
598 - Mapper and Actor are computer in the data/viewer_volume.py module for easier handling in the invesalius 3D scene.  
599 -  
600 - Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation  
601 -  
602 - :param inp: List of inputs: trekker instance, affine numpy array, seed_offset, seed_radius, n_threads  
603 - :type inp: list  
604 - :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene  
605 - :type affine_vtk: vtkMatrix4x4  
606 - :param coord_queue: Queue instance that manage coordinates read from tracking device and coregistered  
607 - :type coord_queue: queue.Queue  
608 - :param visualization_queue: Queue instance that manage coordinates to be visualized  
609 - :type visualization_queue: queue.Queue  
610 - :param event: Threading event to coordinate when tasks as done and allow UI release  
611 - :type event: threading.Event  
612 - :param sle: Sleep pause in seconds  
613 - :type sle: float  
614 - """  
615 -  
616 - threading.Thread.__init__(self, name='ComputeTractsThread')  
617 - self.inp = inp  
618 - self.affine_vtk = affine_vtk  
619 - self.coord_queue = coord_queue  
620 - self.visualization_queue = visualization_queue  
621 - self.event = event  
622 - self.sle = sle  
623 -  
624 - def run(self):  
625 -  
626 - trekker, affine, offset, n_tracts_total, seed_radius, n_threads = self.inp  
627 - # as a single block, computes all the maximum number of tracts at once, not optimal for navigation  
628 - n_threads = n_tracts_total  
629 - p_old = np.array([[0., 0., 0.]])  
630 - root = vtk.vtkMultiBlockDataSet()  
631 -  
632 - # Compute the tracts  
633 - # print('ComputeTractsThread: event {}'.format(self.event.is_set()))  
634 - while not self.event.is_set():  
635 - try:  
636 - coord, [coord_raw, markers_flag], m_img, m_img_flip = self.coord_queue.get_nowait()  
637 -  
638 - # translate the coordinate along the normal vector of the object/coil  
639 - coord_offset = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2]  
640 - # coord_offset = np.array([[27.53, -77.37, 46.42]])  
641 - dist = abs(np.linalg.norm(p_old - np.asarray(coord_offset)))  
642 - p_old = coord_offset.copy()  
643 - seed_trk = img_utils.convert_world_to_voxel(coord_offset, affine)  
644 - # Juuso's  
645 - # seed_trk = np.array([[-8.49, -8.39, 2.5]])  
646 - # Baran M1  
647 - # seed_trk = np.array([[27.53, -77.37, 46.42]])  
648 - # print("Seed: {}".format(seed))  
649 -  
650 - # set the seeds for trekker, one seed is repeated n_threads times  
651 - # trekker has internal multiprocessing approach done in C. Here the number of available threads is give,  
652 - # but in case a large number of tracts is requested, it will compute all in parallel automatically  
653 - # for a more fluent navigation, better to compute the maximum number the computer handles  
654 - trekker.seed_coordinates(np.repeat(seed_trk, n_threads, axis=0))  
655 - # run the trekker, this is the slowest line of code, be careful to just use once!  
656 - trk_list = trekker.run()  
657 -  
658 - if trk_list:  
659 - # if the seed is outside the defined radius, restart the bundle computation  
660 - if dist >= seed_radius:  
661 - root = tracts_computation(trk_list, root, 0)  
662 - self.visualization_queue.put_nowait((coord, m_img, root))  
663 -  
664 - self.coord_queue.task_done()  
665 - time.sleep(self.sle)  
666 - except queue.Empty:  
667 - pass  
668 - except queue.Full:  
669 - self.coord_queue.task_done()  
670 -  
671 -  
672 def set_trekker_parameters(trekker, params): 555 def set_trekker_parameters(trekker, params):
673 """Set all user-defined parameters for tractography computation using the Trekker library 556 """Set all user-defined parameters for tractography computation using the Trekker library
674 557
@@ -680,21 +563,27 @@ def set_trekker_parameters(trekker, params): @@ -680,21 +563,27 @@ def set_trekker_parameters(trekker, params):
680 :rtype: list 563 :rtype: list
681 """ 564 """
682 trekker.seed_maxTrials(params['seed_max']) 565 trekker.seed_maxTrials(params['seed_max'])
683 - # trekker.stepSize(params['step_size'])  
684 - trekker.minFODamp(params['min_fod'])  
685 - # trekker.probeQuality(params['probe_quality'])  
686 - # trekker.maxEstInterval(params['max_interval'])  
687 - # trekker.minRadiusOfCurvature(params['min_radius_curv'])  
688 - # trekker.probeLength(params['probe_length']) 566 + trekker.stepSize(params['step_size'])
  567 + # minFODamp is not set because it should vary in the loop to create the
  568 + # different transparency tracts
  569 + # trekker.minFODamp(params['min_fod'])
  570 + trekker.probeQuality(params['probe_quality'])
  571 + trekker.maxEstInterval(params['max_interval'])
  572 + trekker.minRadiusOfCurvature(params['min_radius_curvature'])
  573 + trekker.probeLength(params['probe_length'])
689 trekker.writeInterval(params['write_interval']) 574 trekker.writeInterval(params['write_interval'])
690 - trekker.maxLength(params['max_lenth'])  
691 - trekker.minLength(params['min_lenth']) 575 + # these two does not need to be set in the new package
  576 + # trekker.maxLength(params['max_length'])
  577 + trekker.minLength(params['min_length'])
692 trekker.maxSamplingPerStep(params['max_sampling_step']) 578 trekker.maxSamplingPerStep(params['max_sampling_step'])
  579 + trekker.dataSupportExponent(params['data_support_exponent'])
  580 + #trekker.useBestAtInit(params['use_best_init'])
  581 + #trekker.initMaxEstTrials(params['init_max_est_trials'])
693 582
694 # check number if number of cores is valid in configuration file, 583 # check number if number of cores is valid in configuration file,
695 # otherwise use the maximum number of threads which is usually 2*N_CPUS 584 # otherwise use the maximum number of threads which is usually 2*N_CPUS
696 - n_threads = 2 * const.N_CPU  
697 - if isinstance((params['numb_threads']), int) and params['numb_threads'] <= 2*const.N_CPU: 585 + n_threads = 2 * const.N_CPU - 1
  586 + if isinstance((params['numb_threads']), int) and params['numb_threads'] <= (2*const.N_CPU-1):
698 n_threads = params['numb_threads'] 587 n_threads = params['numb_threads']
699 588
700 trekker.numberOfThreads(n_threads) 589 trekker.numberOfThreads(n_threads)
@@ -712,13 +601,21 @@ def grid_offset(data, coord_list_w_tr, img_shift): @@ -712,13 +601,21 @@ def grid_offset(data, coord_list_w_tr, img_shift):
712 601
713 # extract the first occurrence of a specific label from the MRI image 602 # extract the first occurrence of a specific label from the MRI image
714 labs = data[coord_list_w_tr_mri[..., 0], coord_list_w_tr_mri[..., 1], coord_list_w_tr_mri[..., 2]] 603 labs = data[coord_list_w_tr_mri[..., 0], coord_list_w_tr_mri[..., 1], coord_list_w_tr_mri[..., 2]]
715 - lab_first = np.argmax(labs == 1)  
716 - if labs[lab_first] == 1:  
717 - pt_found = coord_list_w_tr_mri[lab_first, :] 604 + lab_first = np.where(labs == 1)
  605 + if not lab_first:
  606 + pt_found_inv = None
  607 + else:
  608 + pt_found = coord_list_w_tr[:, lab_first[0][0]][:3]
718 # convert coordinate back to invesalius 3D space 609 # convert coordinate back to invesalius 3D space
719 pt_found_inv = pt_found - np.array([0., img_shift, 0.]) 610 pt_found_inv = pt_found - np.array([0., img_shift, 0.])
720 - else:  
721 - pt_found_inv = None 611 +
  612 + # lab_first = np.argmax(labs == 1)
  613 + # if labs[lab_first] == 1:
  614 + # pt_found = coord_list_w_tr_mri[lab_first, :]
  615 + # # convert coordinate back to invesalius 3D space
  616 + # pt_found_inv = pt_found - np.array([0., img_shift, 0.])
  617 + # else:
  618 + # pt_found_inv = None
722 619
723 # # convert to world coordinate space to use as seed for fiber tracking 620 # # convert to world coordinate space to use as seed for fiber tracking
724 # pt_found_tr = np.append(pt_found, 1)[np.newaxis, :].T 621 # pt_found_tr = np.append(pt_found, 1)[np.newaxis, :].T
invesalius/data/viewer_volume.py
@@ -307,7 +307,6 @@ class Viewer(wx.Panel): @@ -307,7 +307,6 @@ class Viewer(wx.Panel):
307 Publisher.subscribe(self.OnRemoveTracts, 'Remove tracts') 307 Publisher.subscribe(self.OnRemoveTracts, 'Remove tracts')
308 Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset') 308 Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset')
309 Publisher.subscribe(self.UpdateMarkerOffsetState, 'Update marker offset state') 309 Publisher.subscribe(self.UpdateMarkerOffsetState, 'Update marker offset state')
310 - Publisher.subscribe(self.UpdateMarkerOffsetPosition, 'Update marker offset')  
311 Publisher.subscribe(self.AddPeeledSurface, 'Update peel') 310 Publisher.subscribe(self.AddPeeledSurface, 'Update peel')
312 Publisher.subscribe(self.GetPeelCenters, 'Get peel centers and normals') 311 Publisher.subscribe(self.GetPeelCenters, 'Get peel centers and normals')
313 Publisher.subscribe(self.Initlocator_viewer, 'Get init locator') 312 Publisher.subscribe(self.Initlocator_viewer, 'Get init locator')
@@ -857,8 +856,10 @@ class Viewer(wx.Panel): @@ -857,8 +856,10 @@ class Viewer(wx.Panel):
857 else: 856 else:
858 self.DisableCoilTracker() 857 self.DisableCoilTracker()
859 if self.actor_peel: 858 if self.actor_peel:
860 - self.object_orientation_torus_actor.SetVisibility(1)  
861 - self.obj_projection_arrow_actor.SetVisibility(1) 859 + if self.object_orientation_torus_actor:
  860 + self.object_orientation_torus_actor.SetVisibility(1)
  861 + if self.obj_projection_arrow_actor:
  862 + self.obj_projection_arrow_actor.SetVisibility(1)
862 863
863 def OnUpdateObjectTargetGuide(self, m_img, coord): 864 def OnUpdateObjectTargetGuide(self, m_img, coord):
864 865
@@ -1607,10 +1608,6 @@ class Viewer(wx.Panel): @@ -1607,10 +1608,6 @@ class Viewer(wx.Panel):
1607 self.ren.AddActor(self.mark_actor) 1608 self.ren.AddActor(self.mark_actor)
1608 self.Refresh() 1609 self.Refresh()
1609 1610
1610 - def UpdateMarkerOffsetPosition(self, coord_offset):  
1611 - self.mark_actor.SetPosition(coord_offset)  
1612 - self.Refresh()  
1613 -  
1614 def UpdateObjectOrientation(self, m_img, coord): 1611 def UpdateObjectOrientation(self, m_img, coord):
1615 # print("Update object orientation") 1612 # print("Update object orientation")
1616 1613
@@ -1687,7 +1684,7 @@ class Viewer(wx.Panel): @@ -1687,7 +1684,7 @@ class Viewer(wx.Panel):
1687 # self.ball_actor.SetVisibility(1) 1684 # self.ball_actor.SetVisibility(1)
1688 self.Refresh() 1685 self.Refresh()
1689 1686
1690 - def OnUpdateTracts(self, root=None, affine_vtk=None, coord_offset=None): 1687 + def OnUpdateTracts(self, root=None, affine_vtk=None, coord_offset=None, coord_offset_w=None):
1691 mapper = vtk.vtkCompositePolyDataMapper2() 1688 mapper = vtk.vtkCompositePolyDataMapper2()
1692 mapper.SetInputDataObject(root) 1689 mapper.SetInputDataObject(root)
1693 1690
invesalius/gui/dialogs.py
@@ -4203,7 +4203,7 @@ class GoToDialogScannerCoord(wx.Dialog): @@ -4203,7 +4203,7 @@ class GoToDialogScannerCoord(wx.Dialog):
4203 main_sizer.Fit(self) 4203 main_sizer.Fit(self)
4204 4204
4205 self.orientation = None 4205 self.orientation = None
4206 - self.affine = None 4206 + self.affine = np.identity(4)
4207 4207
4208 self.__bind_events() 4208 self.__bind_events()
4209 4209
invesalius/gui/task_navigator.py
@@ -165,7 +165,7 @@ class InnerFoldPanel(wx.Panel): @@ -165,7 +165,7 @@ class InnerFoldPanel(wx.Panel):
165 # Study this. 165 # Study this.
166 166
167 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, 167 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
168 - (10, 310), 0, fpb.FPB_SINGLE_FOLD) 168 + (10, 330), 0, fpb.FPB_SINGLE_FOLD)
169 169
170 # Initialize Tracker and PedalConnection objects here so that they are available to several panels. 170 # Initialize Tracker and PedalConnection objects here so that they are available to several panels.
171 # 171 #
@@ -1078,17 +1078,15 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1078,17 +1078,15 @@ class ObjectRegistrationPanel(wx.Panel):
1078 1078
1079 try: 1079 try:
1080 if filename: 1080 if filename:
1081 - #TODO: Improve method to read the file, using "with" similar to OnLoadParameters  
1082 - data = np.loadtxt(filename, delimiter='\t')  
1083 - self.obj_fiducials = data[:, :3]  
1084 - self.obj_orients = data[:, 3:] 1081 + with open(filename, 'r') as text_file:
  1082 + data = [s.split('\t') for s in text_file.readlines()]
1085 1083
1086 - text_file = open(filename, "r")  
1087 - header = text_file.readline().split('\t')  
1088 - text_file.close() 1084 + registration_coordinates = np.array(data[1:]).astype(np.float32)
  1085 + self.obj_fiducials = registration_coordinates[:, :3]
  1086 + self.obj_orients = registration_coordinates[:, 3:]
1089 1087
1090 - self.obj_name = header[1]  
1091 - self.obj_ref_mode = int(header[-1]) 1088 + self.obj_name = data[0][1]
  1089 + self.obj_ref_mode = int(data[0][-1])
1092 1090
1093 self.checktrack.Enable(1) 1091 self.checktrack.Enable(1)
1094 self.checktrack.SetValue(True) 1092 self.checktrack.SetValue(True)
@@ -1098,7 +1096,7 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1098,7 +1096,7 @@ class ObjectRegistrationPanel(wx.Panel):
1098 label=_("Object file successfully loaded")) 1096 label=_("Object file successfully loaded"))
1099 Publisher.sendMessage('Update track object state', flag=True, obj_name=self.obj_name) 1097 Publisher.sendMessage('Update track object state', flag=True, obj_name=self.obj_name)
1100 Publisher.sendMessage('Change camera checkbox', status=False) 1098 Publisher.sendMessage('Change camera checkbox', status=False)
1101 - # wx.MessageBox(_("Object file successfully loaded"), _("Load")) 1099 + wx.MessageBox(_("Object file successfully loaded"), _("InVesalius 3"))
1102 except: 1100 except:
1103 wx.MessageBox(_("Object registration file incompatible."), _("InVesalius 3")) 1101 wx.MessageBox(_("Object registration file incompatible."), _("InVesalius 3"))
1104 Publisher.sendMessage('Update status text in GUI', label="") 1102 Publisher.sendMessage('Update status text in GUI', label="")
@@ -1449,8 +1447,8 @@ class MarkersPanel(wx.Panel): @@ -1449,8 +1447,8 @@ class MarkersPanel(wx.Panel):
1449 else: 1447 else:
1450 self.nav_status = True 1448 self.nav_status = True
1451 1449
1452 - def UpdateSeedCoordinates(self, root=None, affine_vtk=None, coord_offset=(0, 0, 0)):  
1453 - self.current_seed = coord_offset 1450 + def UpdateSeedCoordinates(self, root=None, affine_vtk=None, coord_offset=(0, 0, 0), coord_offset_w=(0, 0, 0)):
  1451 + self.current_seed = coord_offset_w
1454 1452
1455 def UpdateRobotCoordinates(self, coordinates_raw, markers_flag): 1453 def UpdateRobotCoordinates(self, coordinates_raw, markers_flag):
1456 self.raw_target_robot = coordinates_raw[1], coordinates_raw[2] 1454 self.raw_target_robot = coordinates_raw[1], coordinates_raw[2]
@@ -1735,7 +1733,7 @@ class TractographyPanel(wx.Panel): @@ -1735,7 +1733,7 @@ class TractographyPanel(wx.Panel):
1735 default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) 1733 default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
1736 self.SetBackgroundColour(default_colour) 1734 self.SetBackgroundColour(default_colour)
1737 1735
1738 - self.affine = None 1736 + self.affine = np.identity(4)
1739 self.affine_vtk = None 1737 self.affine_vtk = None
1740 self.trekker = None 1738 self.trekker = None
1741 self.n_tracts = const.N_TRACTS 1739 self.n_tracts = const.N_TRACTS
@@ -1992,7 +1990,6 @@ class TractographyPanel(wx.Panel): @@ -1992,7 +1990,6 @@ class TractographyPanel(wx.Panel):
1992 self.nav_status = nav_status 1990 self.nav_status = nav_status
1993 1991
1994 def OnLinkBrain(self, event=None): 1992 def OnLinkBrain(self, event=None):
1995 - Publisher.sendMessage('Update status text in GUI', label=_("Busy"))  
1996 Publisher.sendMessage('Begin busy cursor') 1993 Publisher.sendMessage('Begin busy cursor')
1997 mask_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import brain mask")) 1994 mask_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import brain mask"))
1998 img_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import T1 anatomical image")) 1995 img_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import T1 anatomical image"))
@@ -2006,31 +2003,37 @@ class TractographyPanel(wx.Panel): @@ -2006,31 +2003,37 @@ class TractographyPanel(wx.Panel):
2006 slic = sl.Slice() 2003 slic = sl.Slice()
2007 prj_data = prj.Project() 2004 prj_data = prj.Project()
2008 matrix_shape = tuple(prj_data.matrix_shape) 2005 matrix_shape = tuple(prj_data.matrix_shape)
  2006 + spacing = tuple(prj_data.spacing)
  2007 + img_shift = spacing[1] * (matrix_shape[1] - 1)
2009 self.affine = slic.affine.copy() 2008 self.affine = slic.affine.copy()
2010 - self.affine[1, -1] -= matrix_shape[1] 2009 + self.affine[1, -1] -= img_shift
2011 self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine) 2010 self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine)
2012 2011
2013 - try:  
2014 - self.brain_peel = brain.Brain(img_path, mask_path, self.n_peels, self.affine_vtk)  
2015 - self.brain_actor = self.brain_peel.get_actor(self.peel_depth, self.affine_vtk)  
2016 - self.brain_actor.GetProperty().SetOpacity(self.brain_opacity)  
2017 - Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor)  
2018 - Publisher.sendMessage('Get peel centers and normals', centers=self.brain_peel.peel_centers,  
2019 - normals=self.brain_peel.peel_normals)  
2020 - Publisher.sendMessage('Get init locator', locator=self.brain_peel.locator)  
2021 - self.checkpeeling.Enable(1)  
2022 - self.checkpeeling.SetValue(True)  
2023 - self.spin_opacity.Enable(1)  
2024 - Publisher.sendMessage('Update status text in GUI', label=_("Brain model loaded"))  
2025 - self.peel_loaded = True  
2026 - Publisher.sendMessage('Update peel visualization', data= self.peel_loaded)  
2027 - except:  
2028 - wx.MessageBox(_("Unable to load brain mask."), _("InVesalius 3")) 2012 + if mask_path and img_path:
  2013 + Publisher.sendMessage('Update status text in GUI', label=_("Busy"))
  2014 + try:
  2015 + self.brain_peel = brain.Brain(img_path, mask_path, self.n_peels, self.affine_vtk)
  2016 + self.brain_actor = self.brain_peel.get_actor(self.peel_depth, self.affine_vtk)
  2017 + self.brain_actor.GetProperty().SetOpacity(self.brain_opacity)
  2018 +
  2019 + self.checkpeeling.Enable(1)
  2020 + self.checkpeeling.SetValue(True)
  2021 + self.spin_opacity.Enable(1)
  2022 + self.peel_loaded = True
  2023 +
  2024 + Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor)
  2025 + Publisher.sendMessage('Get peel centers and normals', centers=self.brain_peel.peel_centers,
  2026 + normals=self.brain_peel.peel_normals)
  2027 + Publisher.sendMessage('Get init locator', locator=self.brain_peel.locator)
  2028 + Publisher.sendMessage('Update status text in GUI', label=_("Brain model loaded"))
  2029 + Publisher.sendMessage('Update peel visualization', data= self.peel_loaded)
  2030 + except:
  2031 + Publisher.sendMessage('Update status text in GUI', label=_("Brain mask initialization failed."))
  2032 + wx.MessageBox(_("Unable to load brain mask."), _("InVesalius 3"))
2029 2033
2030 Publisher.sendMessage('End busy cursor') 2034 Publisher.sendMessage('End busy cursor')
2031 2035
2032 def OnLinkFOD(self, event=None): 2036 def OnLinkFOD(self, event=None):
2033 - Publisher.sendMessage('Update status text in GUI', label=_("Busy"))  
2034 Publisher.sendMessage('Begin busy cursor') 2037 Publisher.sendMessage('Begin busy cursor')
2035 filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import Trekker FOD")) 2038 filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import Trekker FOD"))
2036 # Juuso 2039 # Juuso
@@ -2041,69 +2044,83 @@ class TractographyPanel(wx.Panel): @@ -2041,69 +2044,83 @@ class TractographyPanel(wx.Panel):
2041 # FOD_path = 'Baran_FOD.nii' 2044 # FOD_path = 'Baran_FOD.nii'
2042 # filename = os.path.join(data_dir, FOD_path) 2045 # filename = os.path.join(data_dir, FOD_path)
2043 2046
2044 - # if not self.affine_vtk:  
2045 - # slic = sl.Slice()  
2046 - # self.affine = slic.affine  
2047 - # self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine)  
2048 -  
2049 if not self.affine_vtk: 2047 if not self.affine_vtk:
2050 slic = sl.Slice() 2048 slic = sl.Slice()
2051 prj_data = prj.Project() 2049 prj_data = prj.Project()
2052 matrix_shape = tuple(prj_data.matrix_shape) 2050 matrix_shape = tuple(prj_data.matrix_shape)
  2051 + spacing = tuple(prj_data.spacing)
  2052 + img_shift = spacing[1] * (matrix_shape[1] - 1)
2053 self.affine = slic.affine.copy() 2053 self.affine = slic.affine.copy()
2054 - self.affine[1, -1] -= matrix_shape[1] 2054 + self.affine[1, -1] -= img_shift
2055 self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine) 2055 self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine)
2056 2056
2057 - # try:  
2058 -  
2059 - self.trekker = Trekker.initialize(filename.encode('utf-8'))  
2060 - self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg)  
2061 -  
2062 - self.checktracts.Enable(1)  
2063 - self.checktracts.SetValue(True)  
2064 - self.view_tracts = True  
2065 - Publisher.sendMessage('Update Trekker object', data=self.trekker)  
2066 - Publisher.sendMessage('Update number of threads', data=n_threads)  
2067 - Publisher.sendMessage('Update tracts visualization', data=1)  
2068 - Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialized"))  
2069 - # except:  
2070 - # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3")) 2057 + if filename:
  2058 + Publisher.sendMessage('Update status text in GUI', label=_("Busy"))
  2059 + try:
  2060 + self.trekker = Trekker.initialize(filename.encode('utf-8'))
  2061 + self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg)
  2062 +
  2063 + self.checktracts.Enable(1)
  2064 + self.checktracts.SetValue(True)
  2065 + self.view_tracts = True
  2066 +
  2067 + Publisher.sendMessage('Update Trekker object', data=self.trekker)
  2068 + Publisher.sendMessage('Update number of threads', data=n_threads)
  2069 + Publisher.sendMessage('Update tracts visualization', data=1)
  2070 + Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialized"))
  2071 + # except:
  2072 + # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3"))
  2073 + except:
  2074 + Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialization failed."))
  2075 + wx.MessageBox(_("Unable to load FOD."), _("InVesalius 3"))
2071 2076
2072 Publisher.sendMessage('End busy cursor') 2077 Publisher.sendMessage('End busy cursor')
2073 2078
2074 def OnLoadACT(self, event=None): 2079 def OnLoadACT(self, event=None):
2075 - Publisher.sendMessage('Update status text in GUI', label=_("Busy"))  
2076 - Publisher.sendMessage('Begin busy cursor')  
2077 - filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import anatomical labels"))  
2078 - # Baran  
2079 - # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609'  
2080 - # act_path = 'Baran_trekkerACTlabels_inFODspace.nii'  
2081 - # filename = os.path.join(data_dir, act_path)  
2082 -  
2083 - act_data = nb.squeeze_image(nb.load(filename))  
2084 - act_data = nb.as_closest_canonical(act_data)  
2085 - act_data.update_header()  
2086 - act_data_arr = act_data.get_fdata() 2080 + if self.trekker:
  2081 + Publisher.sendMessage('Begin busy cursor')
  2082 + filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import anatomical labels"))
  2083 + # Baran
  2084 + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609'
  2085 + # act_path = 'Baran_trekkerACTlabels_inFODspace.nii'
  2086 + # filename = os.path.join(data_dir, act_path)
  2087 +
  2088 + if not self.affine_vtk:
  2089 + slic = sl.Slice()
  2090 + prj_data = prj.Project()
  2091 + matrix_shape = tuple(prj_data.matrix_shape)
  2092 + spacing = tuple(prj_data.spacing)
  2093 + img_shift = spacing[1] * (matrix_shape[1] - 1)
  2094 + self.affine = slic.affine.copy()
  2095 + self.affine[1, -1] -= img_shift
  2096 + self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine)
2087 2097
2088 - if not self.affine_vtk:  
2089 - slic = sl.Slice()  
2090 - prj_data = prj.Project()  
2091 - matrix_shape = tuple(prj_data.matrix_shape)  
2092 - self.affine = slic.affine.copy()  
2093 - self.affine[1, -1] -= matrix_shape[1]  
2094 - self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine)  
2095 -  
2096 - self.checkACT.Enable(1)  
2097 - self.checkACT.SetValue(True)  
2098 -  
2099 - Publisher.sendMessage('Update ACT data', data=act_data_arr)  
2100 - Publisher.sendMessage('Enable ACT', data=True)  
2101 - # Publisher.sendMessage('Create grid', data=act_data_arr, affine=self.affine)  
2102 - # Publisher.sendMessage('Update number of threads', data=n_threads)  
2103 - # Publisher.sendMessage('Update tracts visualization', data=1)  
2104 - Publisher.sendMessage('Update status text in GUI', label=_("Trekker ACT loaded"))  
2105 -  
2106 - Publisher.sendMessage('End busy cursor') 2098 + try:
  2099 + Publisher.sendMessage('Update status text in GUI', label=_("Busy"))
  2100 + if filename:
  2101 + act_data = nb.squeeze_image(nb.load(filename))
  2102 + act_data = nb.as_closest_canonical(act_data)
  2103 + act_data.update_header()
  2104 + act_data_arr = act_data.get_fdata()
  2105 +
  2106 + self.checkACT.Enable(1)
  2107 + self.checkACT.SetValue(True)
  2108 +
  2109 + # ACT rules should be as follows:
  2110 + self.trekker.pathway_stop_at_entry(filename.encode('utf-8'), -1) # outside
  2111 + self.trekker.pathway_discard_if_ends_inside(filename.encode('utf-8'), 1) # wm
  2112 + self.trekker.pathway_discard_if_enters(filename.encode('utf-8'), 0) # csf
  2113 +
  2114 + Publisher.sendMessage('Update ACT data', data=act_data_arr)
  2115 + Publisher.sendMessage('Enable ACT', data=True)
  2116 + Publisher.sendMessage('Update status text in GUI', label=_("Trekker ACT loaded"))
  2117 + except:
  2118 + Publisher.sendMessage('Update status text in GUI', label=_("ACT initialization failed."))
  2119 + wx.MessageBox(_("Unable to load ACT."), _("InVesalius 3"))
  2120 +
  2121 + Publisher.sendMessage('End busy cursor')
  2122 + else:
  2123 + wx.MessageBox(_("Load FOD image before the ACT."), _("InVesalius 3"))
2107 2124
2108 def OnLoadParameters(self, event=None): 2125 def OnLoadParameters(self, event=None):
2109 import json 2126 import json
@@ -2146,10 +2163,13 @@ class TractographyPanel(wx.Panel): @@ -2146,10 +2163,13 @@ class TractographyPanel(wx.Panel):
2146 # print("Running during navigation") 2163 # print("Running during navigation")
2147 coord_flip = list(position[:3]) 2164 coord_flip = list(position[:3])
2148 coord_flip[1] = -coord_flip[1] 2165 coord_flip[1] = -coord_flip[1]
2149 - dti.compute_tracts(self.trekker, coord_flip, self.affine, self.affine_vtk,  
2150 - self.n_tracts) 2166 + dti.compute_and_visualize_tracts(self.trekker, coord_flip, self.affine, self.affine_vtk,
  2167 + self.n_tracts)
2151 2168
2152 def OnCloseProject(self): 2169 def OnCloseProject(self):
  2170 + self.trekker = None
  2171 + self.trekker_cfg = const.TREKKER_CONFIG
  2172 +
2153 self.checktracts.SetValue(False) 2173 self.checktracts.SetValue(False)
2154 self.checktracts.Enable(0) 2174 self.checktracts.Enable(0)
2155 self.checkpeeling.SetValue(False) 2175 self.checkpeeling.SetValue(False)
invesalius/navigation/navigation.py
@@ -27,7 +27,6 @@ import numpy as np @@ -27,7 +27,6 @@ import numpy as np
27 import invesalius.constants as const 27 import invesalius.constants as const
28 import invesalius.project as prj 28 import invesalius.project as prj
29 import invesalius.data.bases as db 29 import invesalius.data.bases as db
30 -import invesalius.data.coordinates as dco  
31 import invesalius.data.coregistration as dcr 30 import invesalius.data.coregistration as dcr
32 import invesalius.data.serial_port_connection as spc 31 import invesalius.data.serial_port_connection as spc
33 import invesalius.data.slice_ as sl 32 import invesalius.data.slice_ as sl
@@ -98,12 +97,11 @@ class UpdateNavigationScene(threading.Thread): @@ -98,12 +97,11 @@ class UpdateNavigationScene(threading.Thread):
98 97
99 # use of CallAfter is mandatory otherwise crashes the wx interface 98 # use of CallAfter is mandatory otherwise crashes the wx interface
100 if self.view_tracts: 99 if self.view_tracts:
101 - bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait() 100 + bundle, affine_vtk, coord_offset, coord_offset_w = self.tracts_queue.get_nowait()
102 #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command 101 #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command
103 wx.CallAfter(Publisher.sendMessage, 'Remove tracts') 102 wx.CallAfter(Publisher.sendMessage, 'Remove tracts')
104 - wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle,  
105 - affine_vtk=affine_vtk, coord_offset=coord_offset)  
106 - # wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset) 103 + wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle, affine_vtk=affine_vtk,
  104 + coord_offset=coord_offset, coord_offset_w=coord_offset_w)
107 self.tracts_queue.task_done() 105 self.tracts_queue.task_done()
108 106
109 if self.serial_port_enabled: 107 if self.serial_port_enabled:
@@ -118,10 +116,11 @@ class UpdateNavigationScene(threading.Thread): @@ -118,10 +116,11 @@ class UpdateNavigationScene(threading.Thread):
118 wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord) 116 wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord)
119 wx.CallAfter(Publisher.sendMessage, 'Update raw coordinates', coordinates_raw=coordinates_raw, markers_flag=markers_flag) 117 wx.CallAfter(Publisher.sendMessage, 'Update raw coordinates', coordinates_raw=coordinates_raw, markers_flag=markers_flag)
120 wx.CallAfter(Publisher.sendMessage, 'Update slice viewer') 118 wx.CallAfter(Publisher.sendMessage, 'Update slice viewer')
  119 + wx.CallAfter(Publisher.sendMessage, 'Sensor ID', markers_flag=markers_flag)
121 120
122 if view_obj: 121 if view_obj:
123 wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord) 122 wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)
124 - wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix',m_img=m_img, coord=coord, flag= self.peel_loaded) 123 + wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix', m_img=m_img, coord=coord, flag= self.peel_loaded)
125 self.coord_queue.task_done() 124 self.coord_queue.task_done()
126 # print('UpdateScene: done {}'.format(count)) 125 # print('UpdateScene: done {}'.format(count))
127 # count += 1 126 # count += 1
@@ -191,7 +190,7 @@ class Navigation(): @@ -191,7 +190,7 @@ class Navigation():
191 190
192 def UpdateSleep(self, sleep): 191 def UpdateSleep(self, sleep):
193 self.sleep_nav = sleep 192 self.sleep_nav = sleep
194 - self.serial_port_connection.sleep_nav = sleep 193 + # self.serial_port_connection.sleep_nav = sleep
195 194
196 def UpdateSerialPort(self, serial_port_in_use, com_port=None, baud_rate=None): 195 def UpdateSerialPort(self, serial_port_in_use, com_port=None, baud_rate=None):
197 self.serial_port_in_use = serial_port_in_use 196 self.serial_port_in_use = serial_port_in_use
@@ -307,15 +306,22 @@ class Navigation(): @@ -307,15 +306,22 @@ class Navigation():
307 306
308 if self.view_tracts: 307 if self.view_tracts:
309 # initialize Trekker parameters 308 # initialize Trekker parameters
  309 + # TODO: This affine and affine_vtk are created 4 times. To improve, create a new affine object inside
  310 + # Slice() that contains the transformation with the img_shift. Rename it to avoid confusion to the
  311 + # affine, for instance it can be: affine_world_to_invesalius_vtk
310 slic = sl.Slice() 312 slic = sl.Slice()
311 prj_data = prj.Project() 313 prj_data = prj.Project()
312 matrix_shape = tuple(prj_data.matrix_shape) 314 matrix_shape = tuple(prj_data.matrix_shape)
  315 + spacing = tuple(prj_data.spacing)
  316 + img_shift = spacing[1] * (matrix_shape[1] - 1)
313 affine = slic.affine.copy() 317 affine = slic.affine.copy()
314 - affine[1, -1] -= matrix_shape[1] 318 + affine[1, -1] -= img_shift
315 affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) 319 affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)
  320 +
316 Publisher.sendMessage("Update marker offset state", create=True) 321 Publisher.sendMessage("Update marker offset state", create=True)
  322 +
317 self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius,\ 323 self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius,\
318 - self.n_threads, self.act_data, affine_vtk, matrix_shape[1] 324 + self.n_threads, self.act_data, affine_vtk, img_shift
319 # print("Appending the tract computation thread!") 325 # print("Appending the tract computation thread!")
320 queues = [self.coord_tracts_queue, self.tracts_queue] 326 queues = [self.coord_tracts_queue, self.tracts_queue]
321 if self.enable_act: 327 if self.enable_act: