Commit 46e9180122ff30cd6e3d4c95c4a597e2fdd1ffab
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>
Showing
12 changed files
with
401 additions
and
450 deletions
Show diff stats
environment.yml
| ... | ... | @@ -2,7 +2,7 @@ channels: |
| 2 | 2 | - conda-forge |
| 3 | 3 | - bioconda |
| 4 | 4 | dependencies: |
| 5 | - - python=3.7 | |
| 5 | + - python=3.8 | |
| 6 | 6 | - cython==0.29.24 |
| 7 | 7 | - pillow==8.3.2 |
| 8 | 8 | - pypubsub==4.0.3 |
| ... | ... | @@ -17,13 +17,21 @@ dependencies: |
| 17 | 17 | - scipy==1.7.1 |
| 18 | 18 | - vtk==9.0.3 |
| 19 | 19 | - wxpython==4.1.1 |
| 20 | + - mido==1.2.10 | |
| 21 | + - nest-asyncio==1.5.1 | |
| 20 | 22 | - pip |
| 21 | 23 | - pip: |
| 22 | - - python-gdcm==3.0.9.1 | |
| 24 | + - aioconsole==0.3.2 | |
| 25 | + - opencv-python==4.5.3.56 | |
| 23 | 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 | 31 | - pyclaron |
| 27 | 32 | - polhemusFT |
| 28 | 33 | - polhemus |
| 29 | 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 | 823 | CAM_MODE = True |
| 824 | 824 | |
| 825 | 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 | 830 | SEED_RADIUS = 1.5 |
| 831 | 831 | |
| 832 | 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 | 834 | SLEEP_COORDINATES = 0.05 |
| 835 | 835 | SLEEP_ROBOT = 0.01 |
| 836 | 836 | |
| 837 | -BRAIN_OPACITY = 0.5 | |
| 837 | +BRAIN_OPACITY = 0.6 | |
| 838 | 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 | 856 | MARKER_FILE_MAGICK_STRING = "##INVESALIUS3_MARKER_FILE_" |
| 845 | 857 | CURRENT_MARKER_FILE_VERSION = 0 | ... | ... |
invesalius/control.py
| ... | ... | @@ -69,7 +69,7 @@ class Controller(): |
| 69 | 69 | #DICOM = 1 |
| 70 | 70 | #TIFF uCT = 2 |
| 71 | 71 | self.img_type = 0 |
| 72 | - self.affine = None | |
| 72 | + self.affine = np.identity(4) | |
| 73 | 73 | |
| 74 | 74 | #Init session |
| 75 | 75 | session = ses.Session() |
| ... | ... | @@ -340,7 +340,7 @@ class Controller(): |
| 340 | 340 | if proj.affine: |
| 341 | 341 | self.Slice.affine = np.asarray(proj.affine).reshape(4, 4) |
| 342 | 342 | else: |
| 343 | - self.Slice.affine = None | |
| 343 | + self.Slice.affine = np.identity(4) | |
| 344 | 344 | |
| 345 | 345 | Publisher.sendMessage('Update threshold limits list', |
| 346 | 346 | threshold_range=proj.threshold_range) |
| ... | ... | @@ -623,6 +623,8 @@ class Controller(): |
| 623 | 623 | Publisher.sendMessage(('Set scroll position', 'SAGITAL'),index=proj.matrix_shape[1]/2) |
| 624 | 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 | 628 | if self.Slice.affine is not None: |
| 627 | 629 | Publisher.sendMessage('Enable Go-to-Coord', status=True) |
| 628 | 630 | else: |
| ... | ... | @@ -731,6 +733,8 @@ class Controller(): |
| 731 | 733 | proj.level = self.Slice.window_level |
| 732 | 734 | proj.threshold_range = int(matrix.min()), int(matrix.max()) |
| 733 | 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 | 738 | if self.Slice.affine is not None: |
| 735 | 739 | proj.affine = self.Slice.affine.tolist() |
| 736 | 740 | ... | ... |
invesalius/data/coordinates.py
invesalius/data/coregistration.py
| ... | ... | @@ -427,7 +427,7 @@ class CoordinateCorregistrateNoObject(threading.Thread): |
| 427 | 427 | # # seed_aux = pos_world.reshape([1, 4])[0, :3] |
| 428 | 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 | 432 | # # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord) |
| 433 | 433 | # wx.CallAfter(Publisher.sendMessage, 'Update cross position', arg=m_img, position=coord) |
| ... | ... | @@ -654,7 +654,7 @@ class CoordinateCorregistrateNoObject(threading.Thread): |
| 654 | 654 | # # seed_aux = pos_world.reshape([1, 4])[0, :3] |
| 655 | 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 | 659 | # # wx.CallAfter(Publisher.sendMessage, 'Co-registered points', arg=m_img, position=coord) |
| 660 | 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 | 544 | |
| 545 | 545 | def OnTractsMouseClick(self, obj, evt): |
| 546 | 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 | 548 | self.ChangeTracts(True) |
| 549 | 549 | |
| 550 | 550 | def OnTractsReleaseLeftButton(self, obj, evt): |
| ... | ... | @@ -554,7 +554,7 @@ class TractsInteractorStyle(CrossInteractorStyle): |
| 554 | 554 | |
| 555 | 555 | def ChangeTracts(self, pressed): |
| 556 | 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 | 558 | # mouse_x, mouse_y = iren.GetEventPosition() |
| 559 | 559 | # wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) |
| 560 | 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 | 365 | if self.convert2inv: |
| 366 | 366 | # convert between invesalius and world space with shift in the Y coordinate |
| 367 | 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 | 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 | 376 | affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) |
| 377 | + | |
| 371 | 378 | actor.SetUserMatrix(affine_vtk) |
| 372 | 379 | |
| 373 | 380 | if overwrite: | ... | ... |
invesalius/data/tractography.py
| ... | ... | @@ -29,14 +29,11 @@ import time |
| 29 | 29 | import numpy as np |
| 30 | 30 | import queue |
| 31 | 31 | from invesalius.pubsub import pub as Publisher |
| 32 | -from scipy.stats import norm | |
| 33 | 32 | import vtk |
| 34 | 33 | |
| 35 | 34 | import invesalius.constants as const |
| 36 | 35 | import invesalius.data.imagedata_utils as img_utils |
| 37 | 36 | |
| 38 | -import invesalius.project as prj | |
| 39 | - | |
| 40 | 37 | # Nice print for arrays |
| 41 | 38 | # np.set_printoptions(precision=2) |
| 42 | 39 | # np.set_printoptions(suppress=True) |
| ... | ... | @@ -104,75 +101,57 @@ def compute_tubes(trk, direction): |
| 104 | 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 | 105 | """Adds a set of tracts to given position in a given vtkMultiBlockDataSet |
| 109 | 106 | |
| 110 | 107 | :param out_list: List of vtkTubeFilters representing the tracts |
| 111 | 108 | :type out_list: list |
| 112 | - :param root: A collection of tracts as a vtkMultiBlockDataSet | |
| 113 | - :type root: vtkMultiBlockDataSet | |
| 114 | 109 | :param n_block: The location in the given vtkMultiBlockDataSet to insert the new tracts |
| 115 | 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 | 112 | :rtype: vtkMultiBlockDataSet |
| 136 | 113 | """ |
| 137 | 114 | |
| 115 | + # create a branch and add the streamlines | |
| 138 | 116 | branch = vtk.vtkMultiBlockDataSet() |
| 117 | + | |
| 139 | 118 | # create tracts only when at least one was computed |
| 140 | 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 | 122 | if not out_list.count(None) == len(out_list): |
| 142 | 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 | 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 | 130 | """Convert the list of all computed tracts given by Trekker run and returns a vtkMultiBlockDataSet |
| 150 | 131 | |
| 151 | 132 | :param trk_list: List of lists containing the computed tracts and corresponding coordinates |
| 152 | 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 | 138 | :return: The updated collection of tracts as a vtkMultiBlockDataSet |
| 158 | 139 | :rtype: vtkMultiBlockDataSet |
| 159 | 140 | """ |
| 160 | 141 | |
| 161 | 142 | # Transform tracts to array |
| 162 | 143 | trk_arr = [np.asarray(trk_n).T if trk_n else None for trk_n in trk_list] |
| 163 | - | |
| 164 | 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 | 146 | # Compute the vtk tubes |
| 168 | 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 | 155 | """ Compute tractograms using the Trekker library. |
| 177 | 156 | |
| 178 | 157 | :param trekker: Trekker library instance |
| ... | ... | @@ -183,52 +162,51 @@ def compute_tracts(trekker, position, affine, affine_vtk, n_tracts): |
| 183 | 162 | :type affine: numpy.ndarray |
| 184 | 163 | :param affine_vtk: vtkMatrix4x4 isntance with affine transformation matrix |
| 185 | 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 | 170 | # Juuso's |
| 194 | 171 | # seed = np.array([[-8.49, -8.39, 2.5]]) |
| 195 | 172 | # Baran M1 |
| 196 | 173 | # seed = np.array([[27.53, -77.37, 46.42]]) |
| 197 | 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 | 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 | 211 | def __init__(self, inp, queues, event, sle): |
| 234 | 212 | """Class (threading) to compute real time tractography data for visualization. |
| ... | ... | @@ -265,6 +243,7 @@ class ComputeTractsThread(threading.Thread): |
| 265 | 243 | |
| 266 | 244 | trekker, affine, offset, n_tracts_total, seed_radius, n_threads, act_data, affine_vtk, img_shift = self.inp |
| 267 | 245 | # n_threads = n_tracts_total |
| 246 | + n_threads = int(n_threads/4) | |
| 268 | 247 | p_old = np.array([[0., 0., 0.]]) |
| 269 | 248 | n_tracts = 0 |
| 270 | 249 | |
| ... | ... | @@ -312,13 +291,13 @@ class ComputeTractsThread(threading.Thread): |
| 312 | 291 | # run the trekker, this is the slowest line of code, be careful to just use once! |
| 313 | 292 | trk_list = trekker.run() |
| 314 | 293 | |
| 315 | - if trk_list: | |
| 294 | + if len(trk_list) > 2: | |
| 316 | 295 | # print("dist: {}".format(dist)) |
| 317 | 296 | if dist >= seed_radius: |
| 318 | 297 | # when moving the coil further than the seed_radius restart the bundle computation |
| 319 | 298 | bundle = vtk.vtkMultiBlockDataSet() |
| 320 | 299 | n_branches = 0 |
| 321 | - branch = tracts_computation_branch(trk_list) | |
| 300 | + branch = compute_tracts(trk_list, n_tract=0, alpha=255) | |
| 322 | 301 | bundle.SetBlock(n_branches, branch) |
| 323 | 302 | n_branches += 1 |
| 324 | 303 | n_tracts = branch.GetNumberOfBlocks() |
| ... | ... | @@ -326,7 +305,7 @@ class ComputeTractsThread(threading.Thread): |
| 326 | 305 | # TODO: maybe keep computing even if reaches the maximum |
| 327 | 306 | elif dist < seed_radius and n_tracts < n_tracts_total: |
| 328 | 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 | 309 | if bundle: |
| 331 | 310 | bundle.SetBlock(n_branches, branch) |
| 332 | 311 | n_tracts += branch.GetNumberOfBlocks() |
| ... | ... | @@ -361,314 +340,218 @@ class ComputeTractsThread(threading.Thread): |
| 361 | 340 | |
| 362 | 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 | 344 | """Class (threading) to compute real time tractography data for visualization. |
| 366 | 345 | |
| 367 | 346 | Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/) |
| 368 | 347 | For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one |
| 369 | 348 | vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as |
| 370 | 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 | 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 | 359 | :param queues: Queue list with coord_tracts_queue (Queue instance that manage co-registered coordinates) and |
| 378 | 360 | tracts_queue (Queue instance that manage the tracts to be visualized) |
| 379 | 361 | :type queues: list[queue.Queue, queue.Queue] |
| 380 | 362 | :param event: Threading event to coordinate when tasks as done and allow UI release |
| 381 | 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 | 368 | threading.Thread.__init__(self, name='ComputeTractsThreadACT') |
| 387 | - self.inp = inp | |
| 388 | - # self.coord_queue = coord_queue | |
| 369 | + self.input_list = input_list | |
| 389 | 370 | self.coord_tracts_queue = queues[0] |
| 390 | 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 | 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 | 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 | 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 | 381 | bundle = None |
| 418 | - sph_sampling = True | |
| 419 | 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 | 392 | m_seed = np.identity(4) |
| 393 | + | |
| 424 | 394 | # Compute the tracts |
| 425 | - # print('ComputeTractsThread: event {}'.format(self.event.is_set())) | |
| 426 | 395 | while not self.event.is_set(): |
| 427 | 396 | try: |
| 428 | - # print("Computing tracts") | |
| 429 | 397 | # get from the queue the coordinates, coregistration transformation matrix, and flipped matrix |
| 430 | 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 | 400 | # DEBUG: Uncomment the m_img_flip below so that distance is fixed and tracts keep computing |
| 444 | 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 | 411 | # rescale the alpha value that defines the opacity of the branch |
| 452 | 412 | # the n interval is [1, 10] and the new interval is [51, 255] |
| 453 | 413 | # the new interval is defined to have no 0 opacity (minimum is 51, i.e., 20%) |
| 454 | 414 | alpha = (n_param - 1) * (255 - 51) / (10 - 1) + 51 |
| 455 | 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 | 482 | # When moving the coil further than the seed_radius restart the bundle computation |
| 460 | 483 | # Currently, it stops to compute tracts when the maximum number of tracts is reached maybe keep |
| 461 | 484 | # computing even if reaches the maximum |
| 462 | 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 | 494 | # run the trekker, this is the slowest line of code, be careful to just use once! |
| 528 | 495 | trk_list = trekker.run() |
| 529 | 496 | |
| 530 | 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 | 505 | bundle = vtk.vtkMultiBlockDataSet() |
| 533 | - branch = tracts_computation_branch(trk_list, alpha) | |
| 534 | 506 | bundle.SetBlock(n_branches, branch) |
| 535 | 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 | 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 | 523 | trk_list = trekker.run() |
| 544 | - if trk_list: | |
| 524 | + | |
| 525 | + if len(trk_list): | |
| 545 | 526 | # compute tract blocks and add to bundle until reaches the maximum number of tracts |
| 546 | 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 | 540 | # be more evident in slow computer or for heavier tract computations, it is better slow update |
| 566 | 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 | 543 | self.coord_tracts_queue.task_done() |
| 572 | - # self.coord_queue.task_done() | |
| 573 | - # print('ComputeTractsThread: done {}'.format(count)) | |
| 574 | 544 | |
| 575 | 545 | # sleep required to prevent user interface from being unresponsive |
| 576 | - time.sleep(self.sle) | |
| 546 | + time.sleep(self.sleep_thread) | |
| 577 | 547 | # if no coordinates pass |
| 578 | 548 | except queue.Empty: |
| 579 | - # print("Empty queue in tractography") | |
| 580 | 549 | pass |
| 581 | 550 | # if queue is full mark as done (may not be needed in this new "nowait" method) |
| 582 | 551 | except queue.Full: |
| 583 | - # self.coord_queue.task_done() | |
| 584 | 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 | 555 | def set_trekker_parameters(trekker, params): |
| 673 | 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 | 563 | :rtype: list |
| 681 | 564 | """ |
| 682 | 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 | 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 | 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 | 583 | # check number if number of cores is valid in configuration file, |
| 695 | 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 | 587 | n_threads = params['numb_threads'] |
| 699 | 588 | |
| 700 | 589 | trekker.numberOfThreads(n_threads) |
| ... | ... | @@ -712,13 +601,21 @@ def grid_offset(data, coord_list_w_tr, img_shift): |
| 712 | 601 | |
| 713 | 602 | # extract the first occurrence of a specific label from the MRI image |
| 714 | 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 | 609 | # convert coordinate back to invesalius 3D space |
| 719 | 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 | 620 | # # convert to world coordinate space to use as seed for fiber tracking |
| 724 | 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 | 307 | Publisher.subscribe(self.OnRemoveTracts, 'Remove tracts') |
| 308 | 308 | Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset') |
| 309 | 309 | Publisher.subscribe(self.UpdateMarkerOffsetState, 'Update marker offset state') |
| 310 | - Publisher.subscribe(self.UpdateMarkerOffsetPosition, 'Update marker offset') | |
| 311 | 310 | Publisher.subscribe(self.AddPeeledSurface, 'Update peel') |
| 312 | 311 | Publisher.subscribe(self.GetPeelCenters, 'Get peel centers and normals') |
| 313 | 312 | Publisher.subscribe(self.Initlocator_viewer, 'Get init locator') |
| ... | ... | @@ -857,8 +856,10 @@ class Viewer(wx.Panel): |
| 857 | 856 | else: |
| 858 | 857 | self.DisableCoilTracker() |
| 859 | 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 | 864 | def OnUpdateObjectTargetGuide(self, m_img, coord): |
| 864 | 865 | |
| ... | ... | @@ -1607,10 +1608,6 @@ class Viewer(wx.Panel): |
| 1607 | 1608 | self.ren.AddActor(self.mark_actor) |
| 1608 | 1609 | self.Refresh() |
| 1609 | 1610 | |
| 1610 | - def UpdateMarkerOffsetPosition(self, coord_offset): | |
| 1611 | - self.mark_actor.SetPosition(coord_offset) | |
| 1612 | - self.Refresh() | |
| 1613 | - | |
| 1614 | 1611 | def UpdateObjectOrientation(self, m_img, coord): |
| 1615 | 1612 | # print("Update object orientation") |
| 1616 | 1613 | |
| ... | ... | @@ -1687,7 +1684,7 @@ class Viewer(wx.Panel): |
| 1687 | 1684 | # self.ball_actor.SetVisibility(1) |
| 1688 | 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 | 1688 | mapper = vtk.vtkCompositePolyDataMapper2() |
| 1692 | 1689 | mapper.SetInputDataObject(root) |
| 1693 | 1690 | ... | ... |
invesalius/gui/dialogs.py
invesalius/gui/task_navigator.py
| ... | ... | @@ -165,7 +165,7 @@ class InnerFoldPanel(wx.Panel): |
| 165 | 165 | # Study this. |
| 166 | 166 | |
| 167 | 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 | 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 | 1078 | |
| 1079 | 1079 | try: |
| 1080 | 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 | 1091 | self.checktrack.Enable(1) |
| 1094 | 1092 | self.checktrack.SetValue(True) |
| ... | ... | @@ -1098,7 +1096,7 @@ class ObjectRegistrationPanel(wx.Panel): |
| 1098 | 1096 | label=_("Object file successfully loaded")) |
| 1099 | 1097 | Publisher.sendMessage('Update track object state', flag=True, obj_name=self.obj_name) |
| 1100 | 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 | 1100 | except: |
| 1103 | 1101 | wx.MessageBox(_("Object registration file incompatible."), _("InVesalius 3")) |
| 1104 | 1102 | Publisher.sendMessage('Update status text in GUI', label="") |
| ... | ... | @@ -1449,8 +1447,8 @@ class MarkersPanel(wx.Panel): |
| 1449 | 1447 | else: |
| 1450 | 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 | 1453 | def UpdateRobotCoordinates(self, coordinates_raw, markers_flag): |
| 1456 | 1454 | self.raw_target_robot = coordinates_raw[1], coordinates_raw[2] |
| ... | ... | @@ -1735,7 +1733,7 @@ class TractographyPanel(wx.Panel): |
| 1735 | 1733 | default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) |
| 1736 | 1734 | self.SetBackgroundColour(default_colour) |
| 1737 | 1735 | |
| 1738 | - self.affine = None | |
| 1736 | + self.affine = np.identity(4) | |
| 1739 | 1737 | self.affine_vtk = None |
| 1740 | 1738 | self.trekker = None |
| 1741 | 1739 | self.n_tracts = const.N_TRACTS |
| ... | ... | @@ -1992,7 +1990,6 @@ class TractographyPanel(wx.Panel): |
| 1992 | 1990 | self.nav_status = nav_status |
| 1993 | 1991 | |
| 1994 | 1992 | def OnLinkBrain(self, event=None): |
| 1995 | - Publisher.sendMessage('Update status text in GUI', label=_("Busy")) | |
| 1996 | 1993 | Publisher.sendMessage('Begin busy cursor') |
| 1997 | 1994 | mask_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import brain mask")) |
| 1998 | 1995 | img_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import T1 anatomical image")) |
| ... | ... | @@ -2006,31 +2003,37 @@ class TractographyPanel(wx.Panel): |
| 2006 | 2003 | slic = sl.Slice() |
| 2007 | 2004 | prj_data = prj.Project() |
| 2008 | 2005 | matrix_shape = tuple(prj_data.matrix_shape) |
| 2006 | + spacing = tuple(prj_data.spacing) | |
| 2007 | + img_shift = spacing[1] * (matrix_shape[1] - 1) | |
| 2009 | 2008 | self.affine = slic.affine.copy() |
| 2010 | - self.affine[1, -1] -= matrix_shape[1] | |
| 2009 | + self.affine[1, -1] -= img_shift | |
| 2011 | 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 | 2034 | Publisher.sendMessage('End busy cursor') |
| 2031 | 2035 | |
| 2032 | 2036 | def OnLinkFOD(self, event=None): |
| 2033 | - Publisher.sendMessage('Update status text in GUI', label=_("Busy")) | |
| 2034 | 2037 | Publisher.sendMessage('Begin busy cursor') |
| 2035 | 2038 | filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import Trekker FOD")) |
| 2036 | 2039 | # Juuso |
| ... | ... | @@ -2041,69 +2044,83 @@ class TractographyPanel(wx.Panel): |
| 2041 | 2044 | # FOD_path = 'Baran_FOD.nii' |
| 2042 | 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 | 2047 | if not self.affine_vtk: |
| 2050 | 2048 | slic = sl.Slice() |
| 2051 | 2049 | prj_data = prj.Project() |
| 2052 | 2050 | matrix_shape = tuple(prj_data.matrix_shape) |
| 2051 | + spacing = tuple(prj_data.spacing) | |
| 2052 | + img_shift = spacing[1] * (matrix_shape[1] - 1) | |
| 2053 | 2053 | self.affine = slic.affine.copy() |
| 2054 | - self.affine[1, -1] -= matrix_shape[1] | |
| 2054 | + self.affine[1, -1] -= img_shift | |
| 2055 | 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 | 2077 | Publisher.sendMessage('End busy cursor') |
| 2073 | 2078 | |
| 2074 | 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 | 2125 | def OnLoadParameters(self, event=None): |
| 2109 | 2126 | import json |
| ... | ... | @@ -2146,10 +2163,13 @@ class TractographyPanel(wx.Panel): |
| 2146 | 2163 | # print("Running during navigation") |
| 2147 | 2164 | coord_flip = list(position[:3]) |
| 2148 | 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 | 2169 | def OnCloseProject(self): |
| 2170 | + self.trekker = None | |
| 2171 | + self.trekker_cfg = const.TREKKER_CONFIG | |
| 2172 | + | |
| 2153 | 2173 | self.checktracts.SetValue(False) |
| 2154 | 2174 | self.checktracts.Enable(0) |
| 2155 | 2175 | self.checkpeeling.SetValue(False) | ... | ... |
invesalius/navigation/navigation.py
| ... | ... | @@ -27,7 +27,6 @@ import numpy as np |
| 27 | 27 | import invesalius.constants as const |
| 28 | 28 | import invesalius.project as prj |
| 29 | 29 | import invesalius.data.bases as db |
| 30 | -import invesalius.data.coordinates as dco | |
| 31 | 30 | import invesalius.data.coregistration as dcr |
| 32 | 31 | import invesalius.data.serial_port_connection as spc |
| 33 | 32 | import invesalius.data.slice_ as sl |
| ... | ... | @@ -98,12 +97,11 @@ class UpdateNavigationScene(threading.Thread): |
| 98 | 97 | |
| 99 | 98 | # use of CallAfter is mandatory otherwise crashes the wx interface |
| 100 | 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 | 101 | #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command |
| 103 | 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 | 105 | self.tracts_queue.task_done() |
| 108 | 106 | |
| 109 | 107 | if self.serial_port_enabled: |
| ... | ... | @@ -118,10 +116,11 @@ class UpdateNavigationScene(threading.Thread): |
| 118 | 116 | wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord) |
| 119 | 117 | wx.CallAfter(Publisher.sendMessage, 'Update raw coordinates', coordinates_raw=coordinates_raw, markers_flag=markers_flag) |
| 120 | 118 | wx.CallAfter(Publisher.sendMessage, 'Update slice viewer') |
| 119 | + wx.CallAfter(Publisher.sendMessage, 'Sensor ID', markers_flag=markers_flag) | |
| 121 | 120 | |
| 122 | 121 | if view_obj: |
| 123 | 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 | 124 | self.coord_queue.task_done() |
| 126 | 125 | # print('UpdateScene: done {}'.format(count)) |
| 127 | 126 | # count += 1 |
| ... | ... | @@ -191,7 +190,7 @@ class Navigation(): |
| 191 | 190 | |
| 192 | 191 | def UpdateSleep(self, sleep): |
| 193 | 192 | self.sleep_nav = sleep |
| 194 | - self.serial_port_connection.sleep_nav = sleep | |
| 193 | + # self.serial_port_connection.sleep_nav = sleep | |
| 195 | 194 | |
| 196 | 195 | def UpdateSerialPort(self, serial_port_in_use, com_port=None, baud_rate=None): |
| 197 | 196 | self.serial_port_in_use = serial_port_in_use |
| ... | ... | @@ -307,15 +306,22 @@ class Navigation(): |
| 307 | 306 | |
| 308 | 307 | if self.view_tracts: |
| 309 | 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 | 312 | slic = sl.Slice() |
| 311 | 313 | prj_data = prj.Project() |
| 312 | 314 | matrix_shape = tuple(prj_data.matrix_shape) |
| 315 | + spacing = tuple(prj_data.spacing) | |
| 316 | + img_shift = spacing[1] * (matrix_shape[1] - 1) | |
| 313 | 317 | affine = slic.affine.copy() |
| 314 | - affine[1, -1] -= matrix_shape[1] | |
| 318 | + affine[1, -1] -= img_shift | |
| 315 | 319 | affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) |
| 320 | + | |
| 316 | 321 | Publisher.sendMessage("Update marker offset state", create=True) |
| 322 | + | |
| 317 | 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 | 325 | # print("Appending the tract computation thread!") |
| 320 | 326 | queues = [self.coord_tracts_queue, self.tracts_queue] |
| 321 | 327 | if self.enable_act: | ... | ... |