Commit 34f7ab2744786ef0fc220f059231a18170fd6be7

Authored by Renan
Committed by Thiago Franco de Moraes
1 parent 8a5a3505
Exists in master

Set target (#125)

* working

* Highlight target and added target icon

* The markers angles are saved now. Target ID created.

-Load markers works for data with angles and/or without angles data
-Load markers recognizes target ID.

* bug fix

* Task panel and dialog to configure coil tracking
- ADD: task panel for coil registration
- ADD: dialog with vtk window and coil obj

* Fixed markers angles bug
-Sometimes the sendmessage "Navigation Status" was arriving before the while thread is over.

* testing...

* Improve coil tracking and tracker communication
- ADD: dialog for coil registration
- ADD: panel for coil registration
- ENH: coordinate recording with Polhemus

* Improve Polhemus communication and trackers management
- ENH: Polhemus wrapper communication, connect and disconnect
- ENH: Choice of trackers
- FIX: Polhemus Fastrak navigation

* show/hide coil tracker
-tracker angles is updating coil tracker

* Just one target can be set

* Uncheck camera volume box when "set target" is selected

* Create target and track distance and orientation

-coil turn to green when orientation is less than the threshold (accept)

* Camera position and focus is based on target

- set transparency on target
- reposition 'distance txt'

* Improve coordinates handling and coil tracking interface
- ENH: coordinates managed with one array, each row is one tracker sensor
- FIX: several minor bugs and wx import masking
- ENH: better dialog for coil tracking
- ADD: coil tracking test thread simulating navigation
- ENH: dynamic reference must be applied outside coordinates module

* Disabled the set target menu when navigation is off

- Avoided bug if user set ID as TARGET

* Temporary coil tracking pipeline
- ENH: improved dialog UI for coil registration
- ENH: multiple coregistration methods depending on coil registration
- ADD: coil tracking to volume viewer

* Minor adjustments in UI and coil tracking

* Zoom camera with target distance

-set arrow upper limit

* Fixed distance text

* Fixed camera position

* Add translate of coil object in vtk matrix

* Set colors

* Text improvements

* Changes in coil registration
- ENH: improve variables and general bugs
- FIX: math for coil registration almost working

* Created coil tracker constants

* Coil coord threshold constants

* Fixed object orientation in dynamic and static navigation
- FIX: coil registration in static mode (sensor 1,2 working)
- ENH: object update in volume viewer improved
- ENH: coregistration adpated to object tracking
- TODO: allow coil registration with sensor 3

* Fixed object tracking and registration bugs
- ENH: UI for object tracking
- ADD: load and save object registration files
- Obs: orientation tracking works for static coil mode (1 ref)
- Obs: orientation tracking works for static and dynamic navigation

* Wrappers are set as metric

* Bug fix
-Avoided bug when no data is selected and the user tries to remove marker

* Removed unused icon

* Code refactoring for object registration
- ADD: button for object registration save in navigation panel
- ADD: dynamic object registration (3 sensors)
- ENH: object registration information saved
- Code cleaned and removed axes for visualization

* Group coil tracking interface with navigation refactoring
- Merge of vhosouza and rmatsuda branchs to track coil
- Navigation still not fully working
- Coil registration not working properly

* Enhance dynamic reference transformation equation

* Created ctrls angles and dist threshold

* Created option to record coordinates and angles along time

* Improve object orientation tracking in dynamic reference

* improvements

* Trials to fix track object orientation

* Fixed coils position

* Created message box for objects registration

* Coregistration changed to matrix multiplication
- Using transformations module
- Only Dynamic Reference without object is working

* Attempts to coil tracking improved

* Changes in object tracking algorithm
- Still not working

* Coregistration improvements

* New changes to track object orientation

* Object tracking with commented code working, axis rotation alligned

* Dynamic object registration working for rotation
- ENH: object registration base creation
- ENH: object registration algorithm for rotation
- TODO: translation and offset not working properly

* Dynamic object registration trying to fix object offset

* Dynamic object registration working
- FIX: offset of sensor to object center

* Code refactoring for navigation with object registration
- FIX: Close project not restarting navigation panel
- ENH: Navigation coregistration algorithms
- ENH: Object data handling in viewer volume
- ENH: Panels and controls in navigation task

* More closing project fix for navigation mode

* Enable object new, load and save buttons
- FIX: minor fix in navigation interface

* Improvement in object tracking panel
- FIX: restart variables while closing project
- ENH: object show and track checkboxes

* Update to correct Neurosoft Fig 8 TMS coil model

* Coil tracking improvements

-TODO: remove dummy coil when set target mode is off
-TODO: Enable target mode button only when target and object are set

* Enable target mode button only when target and object are set

-TODO: remove dummy coil when set target mode is off

* Disable target mode when track obj is off

* Fixed arrow directions and created text for obj registration

* Crucial fix to the object registration algorithm
- object was registered to the head m_head, now registered to the object basis in image space
- minor changes in variables related to navigation

* Camera orientation and distance text update

-Dist text is showing with dummy coil actor
-Camera is always orienting parallel to dummy coil actor
-TODO: fix - when target mode is off the dist text and the dummy coil keeps green
-TODO: fix - if the obj are not loaded the load markers (target) doesnt work properly

* Fixed initial current_coord

* Fixed fiducials creation markers and ENH: just showing coil when nav is on

* Target is reset when target mode is off

* Remove messages when project is closed

* Disable ref mode for obj registration

* Improvements on camera position

* Set vol. camera default true

- TODO: when nav is off, vol is reset to front position

* Camera is not reset when nav is off, but when target_mode is off

* Nav off do not set target mode off anymore
icons/target.png 0 → 100644

564 Bytes

invesalius/constants.py
... ... @@ -56,6 +56,7 @@ STEREO_ANAGLYPH = _("Anaglyph")
56 56 TEXT_SIZE_SMALL = 11
57 57 TEXT_SIZE = 12
58 58 TEXT_SIZE_LARGE = 16
  59 +TEXT_SIZE_EXTRA_LARGE = 20
59 60 TEXT_COLOUR = (1,1,1)
60 61  
61 62 (X,Y) = (0.03, 0.97)
... ... @@ -678,6 +679,10 @@ DYNAMIC_REF = 1
678 679 DEFAULT_REF_MODE = DYNAMIC_REF
679 680 REF_MODE = [_("Static ref."), _("Dynamic ref.")]
680 681  
  682 +DEFAULT_COIL = SELECT
  683 +COIL = [_("Select coil:"), _("Neurosoft Figure-8"),
  684 + _("Magstim 70 mm"), _("Nexstim")]
  685 +
681 686 IR1 = wx.NewId()
682 687 IR2 = wx.NewId()
683 688 IR3 = wx.NewId()
... ... @@ -708,5 +713,34 @@ TIPS_TRK = [_("Select left ear with spatial tracker"),
708 713 _("Select nasion with spatial tracker"),
709 714 _("Show set coordinates in image")]
710 715  
  716 +OBJL = wx.NewId()
  717 +OBJR = wx.NewId()
  718 +OBJA = wx.NewId()
  719 +OBJC = wx.NewId()
  720 +OBJF = wx.NewId()
  721 +
  722 +BTNS_OBJ = {OBJL: {0: _('Left')},
  723 + OBJR: {1: _('Right')},
  724 + OBJA: {2: _('Anterior')},
  725 + OBJC: {3: _('Center')},
  726 + OBJF: {4: _('Fixed')}}
  727 +
  728 +TIPS_OBJ = [_("Select left object fiducial"),
  729 + _("Select right object fiducial"),
  730 + _("Select anterior object fiducial"),
  731 + _("Select object center"),
  732 + _("Attach sensor to object")]
  733 +
711 734 CAL_DIR = os.path.abspath(os.path.join(FILE_PATH, '..', 'navigation', 'mtc_files', 'CalibrationFiles'))
712 735 MAR_DIR = os.path.abspath(os.path.join(FILE_PATH, '..', 'navigation', 'mtc_files', 'Markers'))
  736 +
  737 +#OBJECT TRACKING
  738 +OBJ_DIR = os.path.abspath(os.path.join(FILE_PATH, '..', 'navigation', 'objects'))
  739 +ARROW_SCALE = 3
  740 +ARROW_UPPER_LIMIT = 30
  741 +#COIL_ANGLES_THRESHOLD = 3 * ARROW_SCALE
  742 +COIL_ANGLES_THRESHOLD = 3
  743 +COIL_COORD_THRESHOLD = 3
  744 +TIMESTAMP = 2.0
  745 +
  746 +CAM_MODE = True
... ...
invesalius/data/bases.py
1   -from math import sqrt
  1 +from math import sqrt, pi
2 2 import numpy as np
  3 +import invesalius.data.coordinates as dco
  4 +import invesalius.data.transformations as tr
3 5  
4 6  
5 7 def angle_calculation(ap_axis, coil_axis):
... ... @@ -19,7 +21,7 @@ def angle_calculation(ap_axis, coil_axis):
19 21 return float(angle)
20 22  
21 23  
22   -def base_creation(fiducials):
  24 +def base_creation_old(fiducials):
23 25 """
24 26 Calculate the origin and matrix for coordinate system
25 27 transformation.
... ... @@ -55,31 +57,70 @@ def base_creation(fiducials):
55 57 [g2[0], g2[1], g2[2]],
56 58 [g3[0], g3[1], g3[2]]])
57 59  
58   - q.shape = (3, 1)
59   - q = np.matrix(q.copy())
60 60 m_inv = m.I
61 61  
62   - # print"M: ", m
63   - # print"q: ", q
  62 + return m, q, m_inv
  63 +
  64 +
  65 +def base_creation(fiducials):
  66 + """
  67 + Calculate the origin and matrix for coordinate system
  68 + transformation.
  69 + q: origin of coordinate system
  70 + g1, g2, g3: orthogonal vectors of coordinate system
  71 +
  72 + :param fiducials: array of 3 rows (p1, p2, p3) and 3 columns (x, y, z) with fiducials coordinates
  73 + :return: matrix and origin for base transformation
  74 + """
  75 +
  76 + p1 = fiducials[0, :]
  77 + p2 = fiducials[1, :]
  78 + p3 = fiducials[2, :]
  79 +
  80 + sub1 = p2 - p1
  81 + sub2 = p3 - p1
  82 + lamb = (sub1[0]*sub2[0]+sub1[1]*sub2[1]+sub1[2]*sub2[2])/np.dot(sub1, sub1)
  83 +
  84 + q = p1 + lamb*sub1
  85 + g1 = p3 - q
  86 + g2 = p1 - q
  87 +
  88 + if not g1.any():
  89 + g1 = p2 - q
  90 +
  91 + g3 = np.cross(g1, g2)
  92 +
  93 + g1 = g1/sqrt(np.dot(g1, g1))
  94 + g2 = g2/sqrt(np.dot(g2, g2))
  95 + g3 = g3/sqrt(np.dot(g3, g3))
  96 +
  97 + m = np.matrix([[g1[0], g2[0], g3[0]],
  98 + [g1[1], g2[1], g3[1]],
  99 + [g1[2], g2[2], g3[2]]])
  100 +
  101 + m_inv = m.I
64 102  
65 103 return m, q, m_inv
66 104  
67 105  
68   -def calculate_fre(fiducials, minv, n, q1, q2):
  106 +def calculate_fre(fiducials, minv, n, q, o):
69 107 """
70 108 Calculate the Fiducial Registration Error for neuronavigation.
71 109  
72 110 :param fiducials: array of 6 rows (image and tracker fiducials) and 3 columns (x, y, z) with coordinates
73 111 :param minv: inverse matrix given by base creation
74 112 :param n: base change matrix given by base creation
75   - :param q1: origin of first base
76   - :param q2: origin of second base
  113 + :param q: origin of first base
  114 + :param o: origin of second base
77 115 :return: float number of fiducial registration error
78 116 """
79 117  
80 118 img = np.zeros([3, 3])
81 119 dist = np.zeros([3, 1])
82 120  
  121 + q1 = np.mat(q).reshape(3, 1)
  122 + q2 = np.mat(o).reshape(3, 1)
  123 +
83 124 p1 = np.mat(fiducials[3, :]).reshape(3, 1)
84 125 p2 = np.mat(fiducials[4, :]).reshape(3, 1)
85 126 p3 = np.mat(fiducials[5, :]).reshape(3, 1)
... ... @@ -133,3 +174,98 @@ def flip_x(point):
133 174 x, y, z = point_rot.tolist()[0][:3]
134 175  
135 176 return x, y, z
  177 +
  178 +
  179 +def flip_x_m(point):
  180 + """
  181 + Rotate coordinates of a vector by pi around X axis in static reference frame.
  182 +
  183 + InVesalius also require to multiply the z coordinate by (-1). Possibly
  184 + because the origin of coordinate system of imagedata is
  185 + located in superior left corner and the origin of VTK scene coordinate
  186 + system (polygonal surface) is in the interior left corner. Second
  187 + possibility is the order of slice stacking
  188 +
  189 + :param point: list of coordinates x, y and z
  190 + :return: rotated coordinates
  191 + """
  192 +
  193 + point_4 = np.hstack((point, 1.)).reshape([4, 1])
  194 + point_4[2, 0] = -point_4[2, 0]
  195 +
  196 + m_rot = np.asmatrix(tr.euler_matrix(pi, 0, 0))
  197 +
  198 + point_rot = m_rot*point_4
  199 +
  200 + return point_rot[0, 0], point_rot[1, 0], point_rot[2, 0]
  201 +
  202 +
  203 +def object_registration(fiducials, orients, coord_raw, m_change):
  204 + """
  205 +
  206 + :param fiducials: 3x3 array of fiducials translations
  207 + :param orients: 3x3 array of fiducials orientations in degrees
  208 + :param coord_raw: nx6 array of coordinates from tracking device where n = 1 is the reference attached to the head
  209 + :param m_change: 3x3 array representing change of basis from head in tracking system to vtk head system
  210 + :return:
  211 + """
  212 +
  213 + coords_aux = np.hstack((fiducials, orients))
  214 + mask = np.ones(len(coords_aux), dtype=bool)
  215 + mask[[3]] = False
  216 + coords = coords_aux[mask]
  217 +
  218 + fids_dyn = np.zeros([4, 6])
  219 + fids_img = np.zeros([4, 6])
  220 + fids_raw = np.zeros([3, 3])
  221 +
  222 + # compute fiducials of object with reference to the fixed probe in source frame
  223 + for ic in range(0, 3):
  224 + fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3]
  225 +
  226 + # compute initial alignment of probe fixed in the object in source frame
  227 + t_s0_raw = np.asmatrix(tr.translation_matrix(coords[3, :3]))
  228 + r_s0_raw = np.asmatrix(tr.euler_matrix(np.radians(coords[3, 3]), np.radians(coords[3, 4]),
  229 + np.radians(coords[3, 5]), 'rzyx'))
  230 + s0_raw = np.asmatrix(tr.concatenate_matrices(t_s0_raw, r_s0_raw))
  231 +
  232 + # compute change of basis for object fiducials in source frame
  233 + base_obj_raw, q_obj_raw, base_inv_obj_raw = base_creation(fids_raw[:3, :3])
  234 + r_obj_raw = np.asmatrix(np.identity(4))
  235 + r_obj_raw[:3, :3] = base_obj_raw[:3, :3]
  236 + t_obj_raw = np.asmatrix(tr.translation_matrix(q_obj_raw))
  237 + m_obj_raw = np.asmatrix(tr.concatenate_matrices(t_obj_raw, r_obj_raw))
  238 +
  239 + for ic in range(0, 4):
  240 + if coord_raw.any():
  241 + # compute object fiducials in reference frame
  242 + fids_dyn[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coord_raw[1, :])
  243 + fids_dyn[ic, 2] = -fids_dyn[ic, 2]
  244 + else:
  245 + # compute object fiducials in source frame
  246 + fids_dyn[ic, :] = coords[ic, :]
  247 +
  248 + # compute object fiducials in vtk head frame
  249 + a, b, g = np.radians(fids_dyn[ic, 3:])
  250 + T_p = tr.translation_matrix(fids_dyn[ic, :3])
  251 + R_p = tr.euler_matrix(a, b, g, 'rzyx')
  252 + M_p = np.asmatrix(tr.concatenate_matrices(T_p, R_p))
  253 + M_img = np.asmatrix(m_change) * M_p
  254 +
  255 + angles_img = np.degrees(np.asarray(tr.euler_from_matrix(M_img, 'rzyx')))
  256 + coord_img = np.asarray(flip_x_m(tr.translation_from_matrix(M_img)))
  257 +
  258 + fids_img[ic, :] = np.hstack((coord_img, angles_img))
  259 +
  260 + # compute object base change in vtk head frame
  261 + base_obj_img, q_obj_img, base_inv_obj_img = base_creation(fids_img[:3, :3])
  262 + r_obj_img = np.asmatrix(np.identity(4))
  263 + r_obj_img[:3, :3] = base_obj_img[:3, :3]
  264 +
  265 + # compute initial alignment of probe fixed in the object in reference (or static) frame
  266 + s0_trans_dyn = np.asmatrix(tr.translation_matrix(fids_dyn[3, :3]))
  267 + s0_rot_dyn = np.asmatrix(tr.euler_matrix(np.radians(fids_dyn[3, 3]), np.radians(fids_dyn[3, 4]),
  268 + np.radians(fids_dyn[3, 5]), 'rzyx'))
  269 + s0_dyn = np.asmatrix(tr.concatenate_matrices(s0_trans_dyn, s0_rot_dyn))
  270 +
  271 + return t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img
... ...
invesalius/data/coordinates.py
... ... @@ -20,10 +20,13 @@
20 20 from math import sin, cos
21 21 import numpy as np
22 22  
  23 +import invesalius.data.transformations as tr
  24 +
23 25 from time import sleep
24 26 from random import uniform
25 27 from wx.lib.pubsub import pub as Publisher
26 28  
  29 +
27 30 def GetCoordinates(trck_init, trck_id, ref_mode):
28 31  
29 32 """
... ... @@ -54,7 +57,7 @@ def ClaronCoord(trck_init, trck_id, ref_mode):
54 57 scale = np.array([1.0, 1.0, -1.0])
55 58 coord = None
56 59 k = 0
57   - # TODO: try to replace while and use some Claron internal computation
  60 + # TODO: try to replace 'while' and use some Claron internal computation
58 61  
59 62 if ref_mode:
60 63 while k < 20:
... ... @@ -77,6 +80,7 @@ def ClaronCoord(trck_init, trck_id, ref_mode):
77 80 trck.Run()
78 81 coord = np.array([trck.PositionTooltipX1 * scale[0], trck.PositionTooltipY1 * scale[1],
79 82 trck.PositionTooltipZ1 * scale[2], trck.AngleX1, trck.AngleY1, trck.AngleZ1])
  83 +
80 84 k = 30
81 85 except AttributeError:
82 86 k += 1
... ... @@ -104,27 +108,23 @@ def PolhemusCoord(trck, trck_id, ref_mode):
104 108  
105 109 def PolhemusWrapperCoord(trck, trck_id, ref_mode):
106 110  
107   - scale = 25.4 * np.array([1., 1.0, -1.0])
108   - coord = None
  111 + trck.Run()
  112 + scale = 10.0 * np.array([1., 1., 1.])
109 113  
110   - if ref_mode:
111   - trck.Run()
112   - probe = np.array([float(trck.PositionTooltipX1), float(trck.PositionTooltipY1),
113   - float(trck.PositionTooltipZ1), float(trck.AngleX1), float(trck.AngleY1),
114   - float(trck.AngleZ1)])
115   - reference = np.array([float(trck.PositionTooltipX2), float(trck.PositionTooltipY2),
116   - float(trck.PositionTooltipZ2), float(trck.AngleX2), float(trck.AngleY2),
117   - float(trck.AngleZ2)])
  114 + coord1 = np.array([float(trck.PositionTooltipX1)*scale[0], float(trck.PositionTooltipY1)*scale[1],
  115 + float(trck.PositionTooltipZ1)*scale[2],
  116 + float(trck.AngleX1), float(trck.AngleY1), float(trck.AngleZ1)])
118 117  
119   - if probe.all() and reference.all():
120   - coord = dynamic_reference(probe, reference)
121   - coord = (coord[0] * scale[0], coord[1] * scale[1], coord[2] * scale[2], coord[3], coord[4], coord[5])
  118 + coord2 = np.array([float(trck.PositionTooltipX2)*scale[0], float(trck.PositionTooltipY2)*scale[1],
  119 + float(trck.PositionTooltipZ2)*scale[2],
  120 + float(trck.AngleX2), float(trck.AngleY2), float(trck.AngleZ2)])
  121 + coord = np.vstack([coord1, coord2])
122 122  
123   - else:
124   - trck.Run()
125   - coord = np.array([float(trck.PositionTooltipX1) * scale[0], float(trck.PositionTooltipY1) * scale[1],
126   - float(trck.PositionTooltipZ1) * scale[2], float(trck.AngleX1), float(trck.AngleY1),
127   - float(trck.AngleZ1)])
  123 + if trck_id == 2:
  124 + coord3 = np.array([float(trck.PositionTooltipX3) * scale[0], float(trck.PositionTooltipY3) * scale[1],
  125 + float(trck.PositionTooltipZ3) * scale[2],
  126 + float(trck.AngleX3), float(trck.AngleY3), float(trck.AngleZ3)])
  127 + coord = np.vstack([coord, coord3])
128 128  
129 129 if trck.StylusButton:
130 130 Publisher.sendMessage('PLH Stylus Button On')
... ... @@ -213,22 +213,21 @@ def DebugCoord(trk_init, trck_id, ref_mode):
213 213 :param trck_id: id of tracking device
214 214 :return: six coordinates x, y, z, alfa, beta and gama
215 215 """
216   - sleep(0.2)
217   - if ref_mode:
218   - probe = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
219   - uniform(1, 200), uniform(1, 200), uniform(1, 200)])
220   - reference = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
221   - uniform(1, 200), uniform(1, 200), uniform(1, 200)])
222 216  
223   - coord = dynamic_reference(probe, reference)
  217 + sleep(0.05)
224 218  
225   - else:
226   - coord = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
227   - uniform(1, 200), uniform(1, 200), uniform(1, 200)])
  219 + coord1 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
  220 + uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
  221 +
  222 + coord2 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
  223 + uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
  224 +
  225 + coord3 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
  226 + uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
228 227  
229 228 Publisher.sendMessage('Sensors ID', [int(uniform(0, 5)), int(uniform(0, 5))])
230 229  
231   - return coord
  230 + return np.vstack([coord1, coord2, coord3])
232 231  
233 232  
234 233 def dynamic_reference(probe, reference):
... ... @@ -245,28 +244,143 @@ def dynamic_reference(probe, reference):
245 244 """
246 245 a, b, g = np.radians(reference[3:6])
247 246  
248   - vet = probe[0:3] - reference[0:3]
249   - vet = np.mat(vet.reshape(3, 1))
  247 + vet = np.asmatrix(probe[0:3] - reference[0:3])
  248 + # vet = np.mat(vet.reshape(3, 1))
  249 +
  250 + # Attitude matrix given by Patriot manual
  251 + # a: rotation of plane (X, Y) around Z axis (azimuth)
  252 + # b: rotation of plane (X', Z) around Y' axis (elevation)
  253 + # a: rotation of plane (Y', Z') around X'' axis (roll)
  254 + m_rot = np.mat([[cos(a) * cos(b), sin(b) * sin(g) * cos(a) - cos(g) * sin(a),
  255 + cos(a) * sin(b) * cos(g) + sin(a) * sin(g)],
  256 + [cos(b) * sin(a), sin(b) * sin(g) * sin(a) + cos(g) * cos(a),
  257 + cos(g) * sin(b) * sin(a) - sin(g) * cos(a)],
  258 + [-sin(b), sin(g) * cos(b), cos(b) * cos(g)]])
  259 +
  260 + # coord_rot = m_rot.T * vet
  261 + coord_rot = vet*m_rot
  262 + coord_rot = np.squeeze(np.asarray(coord_rot))
  263 +
  264 + return coord_rot[0], coord_rot[1], -coord_rot[2], probe[3], probe[4], probe[5]
250 265  
251   - # Attitude Matrix given by Patriot Manual
252   - Mrot = np.mat([[cos(a) * cos(b), sin(b) * sin(g) * cos(a) - cos(g) * sin(a),
253   - cos(a) * sin(b) * cos(g) + sin(a) * sin(g)],
254   - [cos(b) * sin(a), sin(b) * sin(g) * sin(a) + cos(g) * cos(a),
255   - cos(g) * sin(b) * sin(a) - sin(g) * cos(a)],
256   - [-sin(b), sin(g) * cos(b), cos(b) * cos(g)]])
257 266  
258   - coord_rot = Mrot.T * vet
  267 +def dynamic_reference_m(probe, reference):
  268 + """
  269 + Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama
  270 + rotation angles of reference to rotate the probe coordinate and returns the x, y, z
  271 + difference between probe and reference. Angles sequences and equation was extracted from
  272 + Polhemus manual and Attitude matrix in Wikipedia.
  273 + General equation is:
  274 + coord = Mrot * (probe - reference)
  275 + :param probe: sensor one defined as probe
  276 + :param reference: sensor two defined as reference
  277 + :return: rotated and translated coordinates
  278 + """
  279 +
  280 + a, b, g = np.radians(reference[3:6])
  281 +
  282 + T = tr.translation_matrix(reference[:3])
  283 + R = tr.euler_matrix(a, b, g, 'rzyx')
  284 + M = np.asmatrix(tr.concatenate_matrices(T, R))
  285 + # M = tr.compose_matrix(angles=np.radians(reference[3:6]), translate=reference[:3])
  286 + # print M
  287 + probe_4 = np.vstack((np.asmatrix(probe[:3]).reshape([3, 1]), 1.))
  288 + coord_rot = M.I * probe_4
259 289 coord_rot = np.squeeze(np.asarray(coord_rot))
260 290  
261   - return coord_rot[0], coord_rot[1], coord_rot[2], probe[3], probe[4], probe[5]
  291 + return coord_rot[0], coord_rot[1], -coord_rot[2], probe[3], probe[4], probe[5]
  292 +
  293 +def dynamic_reference_m2(probe, reference):
  294 + """
  295 + Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama
  296 + rotation angles of reference to rotate the probe coordinate and returns the x, y, z
  297 + difference between probe and reference. Angles sequences and equation was extracted from
  298 + Polhemus manual and Attitude matrix in Wikipedia.
  299 + General equation is:
  300 + coord = Mrot * (probe - reference)
  301 + :param probe: sensor one defined as probe
  302 + :param reference: sensor two defined as reference
  303 + :return: rotated and translated coordinates
  304 + """
  305 +
  306 + a, b, g = np.radians(reference[3:6])
  307 + a_p, b_p, g_p = np.radians(probe[3:6])
  308 +
  309 + T = tr.translation_matrix(reference[:3])
  310 + T_p = tr.translation_matrix(probe[:3])
  311 + R = tr.euler_matrix(a, b, g, 'rzyx')
  312 + R_p = tr.euler_matrix(a_p, b_p, g_p, 'rzyx')
  313 + M = np.asmatrix(tr.concatenate_matrices(T, R))
  314 + M_p = np.asmatrix(tr.concatenate_matrices(T_p, R_p))
  315 + # M = tr.compose_matrix(angles=np.radians(reference[3:6]), translate=reference[:3])
  316 + # print M
  317 +
  318 + M_dyn = M.I * M_p
  319 +
  320 + al, be, ga = tr.euler_from_matrix(M_dyn, 'rzyx')
  321 + coord_rot = tr.translation_from_matrix(M_dyn)
  322 +
  323 + coord_rot = np.squeeze(coord_rot)
  324 +
  325 + # probe_4 = np.vstack((np.asmatrix(probe[:3]).reshape([3, 1]), 1.))
  326 + # coord_rot_test = M.I * probe_4
  327 + # coord_rot_test = np.squeeze(np.asarray(coord_rot_test))
  328 + #
  329 + # print "coord_rot: ", coord_rot
  330 + # print "coord_rot_test: ", coord_rot_test
  331 + # print "test: ", np.allclose(coord_rot, coord_rot_test[:3])
  332 +
  333 + return coord_rot[0], coord_rot[1], coord_rot[2], np.degrees(al), np.degrees(be), np.degrees(ga)
  334 +
  335 +# def dynamic_reference_m3(probe, reference):
  336 +# """
  337 +# Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama
  338 +# rotation angles of reference to rotate the probe coordinate and returns the x, y, z
  339 +# difference between probe and reference. Angles sequences and equation was extracted from
  340 +# Polhemus manual and Attitude matrix in Wikipedia.
  341 +# General equation is:
  342 +# coord = Mrot * (probe - reference)
  343 +# :param probe: sensor one defined as probe
  344 +# :param reference: sensor two defined as reference
  345 +# :return: rotated and translated coordinates
  346 +# """
  347 +#
  348 +# a, b, g = np.radians(reference[3:6])
  349 +# a_p, b_p, g_p = np.radians(probe[3:6])
  350 +#
  351 +# T = tr.translation_matrix(reference[:3])
  352 +# T_p = tr.translation_matrix(probe[:3])
  353 +# R = tr.euler_matrix(a, b, g, 'rzyx')
  354 +# R_p = tr.euler_matrix(a_p, b_p, g_p, 'rzyx')
  355 +# M = np.asmatrix(tr.concatenate_matrices(T, R))
  356 +# M_p = np.asmatrix(tr.concatenate_matrices(T_p, R_p))
  357 +# # M = tr.compose_matrix(angles=np.radians(reference[3:6]), translate=reference[:3])
  358 +# # print M
  359 +#
  360 +# M_dyn = M.I * M_p
  361 +#
  362 +# # al, be, ga = tr.euler_from_matrix(M_dyn, 'rzyx')
  363 +# # coord_rot = tr.translation_from_matrix(M_dyn)
  364 +# #
  365 +# # coord_rot = np.squeeze(coord_rot)
  366 +#
  367 +# # probe_4 = np.vstack((np.asmatrix(probe[:3]).reshape([3, 1]), 1.))
  368 +# # coord_rot_test = M.I * probe_4
  369 +# # coord_rot_test = np.squeeze(np.asarray(coord_rot_test))
  370 +# #
  371 +# # print "coord_rot: ", coord_rot
  372 +# # print "coord_rot_test: ", coord_rot_test
  373 +# # print "test: ", np.allclose(coord_rot, coord_rot_test[:3])
  374 +#
  375 +# return M_dyn
262 376  
263 377  
264 378 def str2float(data):
265 379 """
266   - Converts string detected wth Polhemus device to float array of coordinates. THis method applies
  380 + Converts string detected wth Polhemus device to float array of coordinates. This method applies
267 381 a correction for the minus sign in string that raises error while splitting the string into coordinates.
268 382 :param data: string of coordinates read with Polhemus
269   - :return: six float coordinates x, y, z, alfa, beta and gama
  383 + :return: six float coordinates x, y, z, alpha, beta and gamma
270 384 """
271 385  
272 386 count = 0
... ...
invesalius/data/coregistration.py
... ... @@ -20,16 +20,123 @@
20 20 import threading
21 21 from time import sleep
22 22  
23   -from numpy import mat
  23 +from numpy import asmatrix, mat, degrees, radians, identity
24 24 import wx
25 25 from wx.lib.pubsub import pub as Publisher
26 26  
27 27 import invesalius.data.coordinates as dco
  28 +import invesalius.data.transformations as tr
28 29  
29 30 # TODO: Optimize navigation thread. Remove the infinite loop and optimize sleep.
30 31  
31 32  
32   -class Coregistration(threading.Thread):
  33 +class CoregistrationStatic(threading.Thread):
  34 + """
  35 + Thread to update the coordinates with the fiducial points
  36 + co-registration method while the Navigation Button is pressed.
  37 + Sleep function in run method is used to avoid blocking GUI and
  38 + for better real-time navigation
  39 + """
  40 +
  41 + def __init__(self, coreg_data, nav_id, trck_info):
  42 + threading.Thread.__init__(self)
  43 + self.coreg_data = coreg_data
  44 + self.nav_id = nav_id
  45 + self.trck_info = trck_info
  46 + self._pause_ = False
  47 + self.start()
  48 +
  49 + def stop(self):
  50 + self._pause_ = True
  51 +
  52 + def run(self):
  53 + # m_change = self.coreg_data[0]
  54 + # obj_ref_mode = self.coreg_data[2]
  55 + #
  56 + # trck_init = self.trck_info[0]
  57 + # trck_id = self.trck_info[1]
  58 + # trck_mode = self.trck_info[2]
  59 +
  60 + m_change, obj_ref_mode = self.coreg_data
  61 + trck_init, trck_id, trck_mode = self.trck_info
  62 +
  63 + while self.nav_id:
  64 + coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  65 +
  66 + psi, theta, phi = coord_raw[obj_ref_mode, 3:]
  67 + t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))
  68 +
  69 + t_probe_raw[2, -1] = -t_probe_raw[2, -1]
  70 +
  71 + m_img = m_change * t_probe_raw
  72 +
  73 + coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], psi, theta, phi
  74 +
  75 + wx.CallAfter(Publisher.sendMessage, 'Co-registered points', (m_img, coord))
  76 +
  77 + # TODO: Optimize the value of sleep for each tracking device.
  78 + sleep(0.175)
  79 +
  80 + if self._pause_:
  81 + return
  82 +
  83 +
  84 +class CoregistrationDynamic(threading.Thread):
  85 + """
  86 + Thread to update the coordinates with the fiducial points
  87 + co-registration method while the Navigation Button is pressed.
  88 + Sleep function in run method is used to avoid blocking GUI and
  89 + for better real-time navigation
  90 + """
  91 +
  92 + def __init__(self, coreg_data, nav_id, trck_info):
  93 + threading.Thread.__init__(self)
  94 + self.coreg_data = coreg_data
  95 + self.nav_id = nav_id
  96 + self.trck_info = trck_info
  97 + self._pause_ = False
  98 + self.start()
  99 +
  100 + def stop(self):
  101 + self._pause_ = True
  102 +
  103 + def run(self):
  104 + m_change, obj_ref_mode = self.coreg_data
  105 + trck_init, trck_id, trck_mode = self.trck_info
  106 +
  107 + while self.nav_id:
  108 + coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  109 +
  110 + psi, theta, phi = radians(coord_raw[obj_ref_mode, 3:])
  111 + r_probe = tr.euler_matrix(psi, theta, phi, 'rzyx')
  112 + t_probe = tr.translation_matrix(coord_raw[obj_ref_mode, :3])
  113 + m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))
  114 +
  115 + psi_ref, theta_ref, phi_ref = radians(coord_raw[1, 3:])
  116 + r_ref = tr.euler_matrix(psi_ref, theta_ref, phi_ref, 'rzyx')
  117 + t_ref = tr.translation_matrix(coord_raw[1, :3])
  118 + m_ref = asmatrix(tr.concatenate_matrices(t_ref, r_ref))
  119 +
  120 + m_dyn = m_ref.I * m_probe
  121 + m_dyn[2, -1] = -m_dyn[2, -1]
  122 +
  123 + m_img = m_change * m_dyn
  124 +
  125 + scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)
  126 +
  127 + coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
  128 + degrees(angles[0]), degrees(angles[1]), degrees(angles[2])
  129 +
  130 + wx.CallAfter(Publisher.sendMessage, 'Co-registered points', (m_img, coord))
  131 +
  132 + # TODO: Optimize the value of sleep for each tracking device.
  133 + sleep(0.175)
  134 +
  135 + if self._pause_:
  136 + return
  137 +
  138 +
  139 +class CoregistrationDynamic_old(threading.Thread):
33 140 """
34 141 Thread to update the coordinates with the fiducial points
35 142 co-registration method while the Navigation Button is pressed.
... ... @@ -44,10 +151,10 @@ class Coregistration(threading.Thread):
44 151 self.trck_info = trck_info
45 152 self._pause_ = False
46 153 self.start()
47   -
  154 +
48 155 def stop(self):
49 156 self._pause_ = True
50   -
  157 +
51 158 def run(self):
52 159 m_inv = self.bases[0]
53 160 n = self.bases[1]
... ... @@ -58,24 +165,199 @@ class Coregistration(threading.Thread):
58 165 trck_mode = self.trck_info[2]
59 166  
60 167 while self.nav_id:
61   - trck_coord = dco.GetCoordinates(trck_init, trck_id, trck_mode)
62   - trck_xyz = mat([[trck_coord[0]], [trck_coord[1]], [trck_coord[2]]])
  168 + # trck_coord, probe, reference = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  169 + coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
63 170  
64   - img = q1 + (m_inv*n)*(trck_xyz - q2)
  171 + trck_coord = dco.dynamic_reference(coord_raw[0, :], coord_raw[1, :])
  172 +
  173 + trck_xyz = mat([[trck_coord[0]], [trck_coord[1]], [trck_coord[2]]])
  174 + img = q1 + (m_inv * n) * (trck_xyz - q2)
65 175  
66 176 coord = (float(img[0]), float(img[1]), float(img[2]), trck_coord[3],
67 177 trck_coord[4], trck_coord[5])
  178 + angles = coord_raw[0, 3:6]
68 179  
69 180 # Tried several combinations and different locations to send the messages,
70 181 # however only this one does not block the GUI during navigation.
71   - wx.CallAfter(Publisher.sendMessage, 'Co-registered points', coord[0:3])
72   - wx.CallAfter(Publisher.sendMessage, 'Set camera in volume', coord[0:3])
  182 + wx.CallAfter(Publisher.sendMessage, 'Co-registered points', coord)
  183 + wx.CallAfter(Publisher.sendMessage, 'Set camera in volume', coord)
  184 + wx.CallAfter(Publisher.sendMessage, 'Update tracker angles', angles)
73 185  
74 186 # TODO: Optimize the value of sleep for each tracking device.
75 187 # Debug tracker is not working with 0.175 so changed to 0.2
76 188 # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.
77   - #sleep(.3)
  189 + # sleep(.3)
  190 + sleep(0.175)
  191 +
  192 + if self._pause_:
  193 + return
  194 +
  195 +
  196 +class CoregistrationObjectStatic(threading.Thread):
  197 + """
  198 + Thread to update the coordinates with the fiducial points
  199 + co-registration method while the Navigation Button is pressed.
  200 + Sleep function in run method is used to avoid blocking GUI and
  201 + for better real-time navigation
  202 + """
  203 +
  204 + def __init__(self, coreg_data, nav_id, trck_info):
  205 + threading.Thread.__init__(self)
  206 + self.coreg_data = coreg_data
  207 + self.nav_id = nav_id
  208 + self.trck_info = trck_info
  209 + self._pause_ = False
  210 + self.start()
  211 +
  212 + def stop(self):
  213 + self._pause_ = True
  214 +
  215 + def run(self):
  216 + # m_change = self.coreg_data[0]
  217 + # t_obj_raw = self.coreg_data[1]
  218 + # s0_raw = self.coreg_data[2]
  219 + # r_s0_raw = self.coreg_data[3]
  220 + # s0_dyn = self.coreg_data[4]
  221 + # m_obj_raw = self.coreg_data[5]
  222 + # r_obj_img = self.coreg_data[6]
  223 + # obj_ref_mode = self.coreg_data[7]
  224 + #
  225 + # trck_init = self.trck_info[0]
  226 + # trck_id = self.trck_info[1]
  227 + # trck_mode = self.trck_info[2]
  228 +
  229 + m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = self.coreg_data
  230 + trck_init, trck_id, trck_mode = self.trck_info
  231 +
  232 + while self.nav_id:
  233 + coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  234 +
  235 + as1, bs1, gs1 = radians(coord_raw[obj_ref_mode, 3:])
  236 + r_probe = asmatrix(tr.euler_matrix(as1, bs1, gs1, 'rzyx'))
  237 + t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))
  238 + t_offset_aux = r_s0_raw.I * r_probe * t_obj_raw
  239 + t_offset = asmatrix(identity(4))
  240 + t_offset[:, -1] = t_offset_aux[:, -1]
  241 + t_probe = s0_raw * t_offset * s0_raw.I * t_probe_raw
  242 + m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))
  243 +
  244 + m_probe[2, -1] = -m_probe[2, -1]
  245 +
  246 + m_img = m_change * m_probe
  247 + r_obj = r_obj_img * m_obj_raw.I * s0_dyn.I * m_probe * m_obj_raw
  248 +
  249 + m_img[:3, :3] = r_obj[:3, :3]
  250 +
  251 + scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)
  252 +
  253 + coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
  254 + degrees(angles[0]), degrees(angles[1]), degrees(angles[2])
  255 +
  256 + wx.CallAfter(Publisher.sendMessage, 'Co-registered points', (m_img, coord))
  257 + wx.CallAfter(Publisher.sendMessage, 'Update object matrix', (m_img, coord))
  258 +
  259 + # TODO: Optimize the value of sleep for each tracking device.
78 260 sleep(0.175)
79 261  
  262 + # Debug tracker is not working with 0.175 so changed to 0.2
  263 + # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.
  264 + # sleep(.3)
  265 +
  266 + # partially working for translate and offset,
  267 + # but offset is kept always in same axis, have to fix for rotation
  268 + # M_dyn = M_reference.I * T_stylus
  269 + # M_dyn[2, -1] = -M_dyn[2, -1]
  270 + # M_dyn_ch = M_change * M_dyn
  271 + # ddd = M_dyn_ch[0, -1], M_dyn_ch[1, -1], M_dyn_ch[2, -1]
  272 + # M_dyn_ch[:3, -1] = asmatrix(db.flip_x_m(ddd)).reshape([3, 1])
  273 + # M_final = S0 * M_obj_trans_0 * S0.I * M_dyn_ch
  274 +
  275 + # this works for static reference object rotation
  276 + # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_raw.I * R_stylus * M_obj_rot_raw
  277 + # this works for dynamic reference in rotation but not in translation
  278 + # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_dyn.I * R_reference.I * R_stylus * M_obj_rot_raw
  279 +
80 280 if self._pause_:
81 281 return
  282 +
  283 +
  284 +class CoregistrationObjectDynamic(threading.Thread):
  285 + """
  286 + Thread to update the coordinates with the fiducial points
  287 + co-registration method while the Navigation Button is pressed.
  288 + Sleep function in run method is used to avoid blocking GUI and
  289 + for better real-time navigation
  290 + """
  291 +
  292 + def __init__(self, coreg_data, nav_id, trck_info):
  293 + threading.Thread.__init__(self)
  294 + self.coreg_data = coreg_data
  295 + self.nav_id = nav_id
  296 + self.trck_info = trck_info
  297 + self._pause_ = False
  298 + self.start()
  299 +
  300 + def stop(self):
  301 + self._pause_ = True
  302 +
  303 + def run(self):
  304 +
  305 + m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = self.coreg_data
  306 + trck_init, trck_id, trck_mode = self.trck_info
  307 +
  308 + while self.nav_id:
  309 + coord_raw = dco.GetCoordinates(trck_init, trck_id, trck_mode)
  310 +
  311 + as1, bs1, gs1 = radians(coord_raw[obj_ref_mode, 3:])
  312 + r_probe = asmatrix(tr.euler_matrix(as1, bs1, gs1, 'rzyx'))
  313 + t_probe_raw = asmatrix(tr.translation_matrix(coord_raw[obj_ref_mode, :3]))
  314 + t_offset_aux = r_s0_raw.I * r_probe * t_obj_raw
  315 + t_offset = asmatrix(identity(4))
  316 + t_offset[:, -1] = t_offset_aux[:, -1]
  317 + t_probe = s0_raw * t_offset * s0_raw.I * t_probe_raw
  318 + m_probe = asmatrix(tr.concatenate_matrices(t_probe, r_probe))
  319 +
  320 + a, b, g = radians(coord_raw[1, 3:])
  321 + r_ref = tr.euler_matrix(a, b, g, 'rzyx')
  322 + t_ref = tr.translation_matrix(coord_raw[1, :3])
  323 + m_ref = asmatrix(tr.concatenate_matrices(t_ref, r_ref))
  324 +
  325 + m_dyn = m_ref.I * m_probe
  326 + m_dyn[2, -1] = -m_dyn[2, -1]
  327 +
  328 + m_img = m_change * m_dyn
  329 + r_obj = r_obj_img * m_obj_raw.I * s0_dyn.I * m_dyn * m_obj_raw
  330 +
  331 + m_img[:3, :3] = r_obj[:3, :3]
  332 +
  333 + scale, shear, angles, trans, persp = tr.decompose_matrix(m_img)
  334 +
  335 + coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\
  336 + degrees(angles[0]), degrees(angles[1]), degrees(angles[2])
  337 +
  338 + wx.CallAfter(Publisher.sendMessage, 'Co-registered points', (m_img, coord))
  339 + wx.CallAfter(Publisher.sendMessage, 'Update object matrix', (m_img, coord))
  340 +
  341 + # TODO: Optimize the value of sleep for each tracking device.
  342 + sleep(0.175)
  343 +
  344 + # Debug tracker is not working with 0.175 so changed to 0.2
  345 + # However, 0.2 is too low update frequency ~5 Hz. Need optimization URGENTLY.
  346 + # sleep(.3)
  347 +
  348 + # partially working for translate and offset,
  349 + # but offset is kept always in same axis, have to fix for rotation
  350 + # M_dyn = M_reference.I * T_stylus
  351 + # M_dyn[2, -1] = -M_dyn[2, -1]
  352 + # M_dyn_ch = M_change * M_dyn
  353 + # ddd = M_dyn_ch[0, -1], M_dyn_ch[1, -1], M_dyn_ch[2, -1]
  354 + # M_dyn_ch[:3, -1] = asmatrix(db.flip_x_m(ddd)).reshape([3, 1])
  355 + # M_final = S0 * M_obj_trans_0 * S0.I * M_dyn_ch
  356 +
  357 + # this works for static reference object rotation
  358 + # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_raw.I * R_stylus * M_obj_rot_raw
  359 + # this works for dynamic reference in rotation but not in translation
  360 + # R_dyn = M_vtk * M_obj_rot_raw.I * S0_rot_dyn.I * R_reference.I * R_stylus * M_obj_rot_raw
  361 +
  362 + if self._pause_:
  363 + return
82 364 \ No newline at end of file
... ...
invesalius/data/record_coords.py 0 → 100644
... ... @@ -0,0 +1,67 @@
  1 +#--------------------------------------------------------------------------
  2 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  3 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  4 +# Homepage: http://www.softwarepublico.gov.br
  5 +# Contact: invesalius@cti.gov.br
  6 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  7 +#--------------------------------------------------------------------------
  8 +# Este programa e software livre; voce pode redistribui-lo e/ou
  9 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  10 +# publicada pela Free Software Foundation; de acordo com a versao 2
  11 +# da Licenca.
  12 +#
  13 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  14 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  15 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  16 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  17 +# detalhes.
  18 +#--------------------------------------------------------------------------
  19 +
  20 +import threading
  21 +import time
  22 +
  23 +import wx
  24 +from numpy import array, savetxt, hstack,vstack, asarray
  25 +import invesalius.gui.dialogs as dlg
  26 +from wx.lib.pubsub import pub as Publisher
  27 +
  28 +
  29 +class Record(threading.Thread):
  30 + """
  31 + Thread created to save obj coords with software during neuronavigation
  32 + """
  33 +
  34 + def __init__(self, nav_id, timestamp):
  35 + threading.Thread.__init__(self)
  36 + self.nav_id = nav_id
  37 + self.coord = None
  38 + self.timestamp = timestamp
  39 + self.coord_list = array([])
  40 + self.__bind_events()
  41 + self._pause_ = False
  42 + self.start()
  43 +
  44 + def __bind_events(self):
  45 + Publisher.subscribe(self.UpdateCurrentCoords, 'Co-registered points')
  46 +
  47 + def UpdateCurrentCoords(self, pubsub_evt):
  48 + self.coord = asarray(pubsub_evt.data[1])
  49 +
  50 + def stop(self):
  51 + self._pause_ = True
  52 + #save coords dialog
  53 + filename = dlg.ShowSaveCoordsDialog("coords.csv")
  54 + if filename:
  55 + savetxt(filename, self.coord_list, delimiter=',', fmt='%.4f', header="time, x, y, z, a, b, g", comments="")
  56 +
  57 + def run(self):
  58 + initial_time = time.time()
  59 + while self.nav_id:
  60 + relative_time = asarray(time.time() - initial_time)
  61 + time.sleep(self.timestamp)
  62 + if self.coord_list.size == 0:
  63 + self.coord_list = hstack((relative_time, self.coord))
  64 + else:
  65 + self.coord_list = vstack((self.coord_list, hstack((relative_time, self.coord))))
  66 + if self._pause_:
  67 + return
0 68 \ No newline at end of file
... ...
invesalius/data/styles.py
... ... @@ -238,7 +238,7 @@ class CrossInteractorStyle(DefaultInteractorStyle):
238 238 Publisher.sendMessage('Update cross position', (wx, wy, wz))
239 239 self.ScrollSlice(coord)
240 240 Publisher.sendMessage('Set ball reference position', (wx, wy, wz))
241   - Publisher.sendMessage('Set camera in volume', (wx, wy, wz))
  241 + Publisher.sendMessage('Co-registered points', (None, (wx, wy, wz, 0., 0., 0.)))
242 242  
243 243 iren.Render()
244 244  
... ...
invesalius/data/trackers.py
... ... @@ -21,18 +21,19 @@
21 21 # TODO: Test if there are too many prints when connection fails
22 22  
23 23  
24   -def TrackerConnection(tracker_id, action):
  24 +def TrackerConnection(tracker_id, trck_init, action):
25 25 """
26   - Initialize spatial trackers for coordinate detection during navigation.
  26 + Initialize or disconnect spatial trackers for coordinate detection during navigation.
27 27  
28 28 :param tracker_id: ID of tracking device.
  29 + :param trck_init: tracker initialization instance.
29 30 :param action: string with to decide whether connect or disconnect the selected device.
30 31 :return spatial tracker initialization instance or None if could not open device.
31 32 """
32 33  
33 34 if action == 'connect':
34 35 trck_fcn = {1: ClaronTracker,
35   - 2: PolhemusTrackerFT, # FASTRAK
  36 + 2: PolhemusTracker, # FASTRAK
36 37 3: PolhemusTracker, # ISOTRAK
37 38 4: PolhemusTracker, # PATRIOT
38 39 5: DebugTracker}
... ... @@ -40,7 +41,7 @@ def TrackerConnection(tracker_id, action):
40 41 trck_init = trck_fcn[tracker_id](tracker_id)
41 42  
42 43 elif action == 'disconnect':
43   - trck_init = DisconnectTracker(tracker_id)
  44 + trck_init = DisconnectTracker(tracker_id, trck_init)
44 45  
45 46 return trck_init
46 47  
... ... @@ -87,29 +88,10 @@ def ClaronTracker(tracker_id):
87 88  
88 89 return trck_init, lib_mode
89 90  
90   -def PolhemusTrackerFT(tracker_id):
91   - trck_init = None
92   - lib_mode = 'wrapper'
93   - try:
94   - import polhemusFT
95   -
96   - trck_init = polhemusFT.polhemusFT()
97   - trck_check = trck_init.Initialize()
98   -
99   - if trck_check:
100   - # First run is necessary to discard the first coord collection
101   - trck_init.Run()
102   - else:
103   - trck_init = trck_check
104   - except:
105   - print 'Could not connect to Polhemus via wrapper.'
106   -
107   - return trck_init, lib_mode
108 91  
109 92 def PolhemusTracker(tracker_id):
110   - trck_init = None
111 93 try:
112   - trck_init = PlhWrapperConnection()
  94 + trck_init = PlhWrapperConnection(tracker_id)
113 95 lib_mode = 'wrapper'
114 96 if not trck_init:
115 97 print 'Could not connect with Polhemus wrapper, trying USB connection...'
... ... @@ -120,6 +102,7 @@ def PolhemusTracker(tracker_id):
120 102 trck_init = PlhSerialConnection(tracker_id)
121 103 lib_mode = 'serial'
122 104 except:
  105 + trck_init = None
123 106 lib_mode = 'error'
124 107 print 'Could not connect to Polhemus.'
125 108  
... ... @@ -132,21 +115,29 @@ def DebugTracker(tracker_id):
132 115 return trck_init, 'debug'
133 116  
134 117  
135   -def PlhWrapperConnection():
136   - trck_init = None
  118 +def PlhWrapperConnection(tracker_id):
137 119 try:
138   - import polhemus
  120 + from time import sleep
  121 + if tracker_id == 2:
  122 + import polhemusFT
  123 + trck_init = polhemusFT.polhemusFT()
  124 + else:
  125 + import polhemus
  126 + trck_init = polhemus.polhemus()
139 127  
140   - trck_init = polhemus.polhemus()
141 128 trck_check = trck_init.Initialize()
142 129  
143 130 if trck_check:
144   - # First run is necessary to discard the first coord collection
145   - trck_init.Run()
  131 + # Sequence of runs necessary to throw away unnecessary data
  132 + for n in range(0, 5):
  133 + trck_init.Run()
  134 + sleep(0.175)
146 135 else:
147   - trck_init = trck_check
  136 + trck_init = None
  137 + print 'Could not connect to Polhemus via wrapper without error.'
148 138 except:
149   - print 'Could not connect to Polhemus via wrapper.'
  139 + trck_init = None
  140 + print 'Could not connect to Polhemus via wrapper with error.'
150 141  
151 142 return trck_init
152 143  
... ... @@ -172,10 +163,11 @@ def PlhSerialConnection(tracker_id):
172 163  
173 164 if not data:
174 165 trck_init = None
  166 + print 'Could not connect to Polhemus serial without error.'
175 167  
176 168 except:
177 169 trck_init = None
178   - print 'Could not connect to Polhemus serial.'
  170 + print 'Could not connect to Polhemus serial with error.'
179 171  
180 172 return trck_init
181 173  
... ... @@ -184,7 +176,10 @@ def PlhUSBConnection(tracker_id):
184 176 trck_init = None
185 177 try:
186 178 import usb.core as uc
187   - trck_init = uc.find(idVendor=0x0F44, idProduct=0x0003)
  179 + # Check the idProduct using the usbdeview software, the idProduct is unique for each
  180 + # device and connection fails when is incorrect
  181 + # trck_init = uc.find(idVendor=0x0F44, idProduct=0x0003) [used in a different device]
  182 + trck_init = uc.find(idVendor=0x0F44, idProduct=0xEF12)
188 183 cfg = trck_init.get_active_configuration()
189 184 for i in cfg:
190 185 for x in i:
... ... @@ -203,58 +198,35 @@ def PlhUSBConnection(tracker_id):
203 198 endpoint.wMaxPacketSize)
204 199 if not data:
205 200 trck_init = None
  201 + print 'Could not connect to Polhemus USB without error.'
206 202  
207 203 except:
208   - print 'Could not connect to Polhemus USB.'
  204 + print 'Could not connect to Polhemus USB with error.'
209 205  
210 206 return trck_init
211 207  
212 208  
213   -def DisconnectTracker(tracker_id):
  209 +def DisconnectTracker(tracker_id, trck_init):
214 210 """
215 211 Disconnect current spatial tracker
216 212  
217 213 :param tracker_id: ID of tracking device.
  214 + :param trck_init: Initialization variable of tracking device.
218 215 """
219   - from wx.lib.pubsub import pub as Publisher
220   - Publisher.sendMessage('Update status text in GUI', _("Disconnecting tracker ..."))
221   - Publisher.sendMessage('Remove sensors ID')
222   - trck_init = None
223   - # TODO: create individual functions to disconnect each other device, e.g. Polhemus.
224   - if tracker_id == 1:
225   - try:
226   - import pyclaron
227   - pyclaron.pyclaron().Close()
228   - lib_mode = 'wrapper'
229   - print 'Claron tracker disconnected.'
230   - except ImportError:
231   - lib_mode = 'error'
232   - print 'The ClaronTracker library is not installed.'
233   -
234   - elif tracker_id == 2:
235   - try:
236   - import polhemusFT
237   - polhemusFT.polhemusFT().Close()
238   - lib_mode = 'wrapper'
239   - print 'Polhemus tracker disconnected.'
240   - except ImportError:
241   - lib_mode = 'error'
242   - print 'The polhemus library is not installed.'
243 216  
244   - elif tracker_id == 4:
  217 + if tracker_id == 5:
  218 + trck_init = False
  219 + lib_mode = 'debug'
  220 + print 'Debug tracker disconnected.'
  221 + else:
245 222 try:
246   - import polhemus
247   - polhemus.polhemus().Close()
  223 + trck_init.Close()
  224 + trck_init = False
248 225 lib_mode = 'wrapper'
249   - print 'Polhemus tracker disconnected.'
250   - except ImportError:
  226 + print 'Tracker disconnected.'
  227 + except:
  228 + trck_init = True
251 229 lib_mode = 'error'
252   - print 'The polhemus library is not installed.'
253   -
254   - elif tracker_id == 5:
255   - print 'Debug tracker disconnected.'
256   - lib_mode = 'debug'
257   -
258   - Publisher.sendMessage('Update status text in GUI', _("Ready"))
  230 + print 'The tracker could not be disconnected.'
259 231  
260 232 return trck_init, lib_mode
261 233 \ No newline at end of file
... ...
invesalius/data/viewer_slice.py
... ... @@ -934,13 +934,13 @@ class Viewer(wx.Panel):
934 934  
935 935 def UpdateSlicesNavigation(self, pubsub_evt):
936 936 # Get point from base change
937   - wx, wy, wz = pubsub_evt.data
938   - px, py = self.get_slice_pixel_coord_by_world_pos(wx, wy, wz)
  937 + ux, uy, uz = pubsub_evt.data[1][:3]
  938 + px, py = self.get_slice_pixel_coord_by_world_pos(ux, uy, uz)
939 939 coord = self.calcultate_scroll_position(px, py)
940 940  
941   - self.cross.SetFocalPoint((wx, wy, wz))
  941 + self.cross.SetFocalPoint((ux, uy, uz))
942 942 self.ScrollSlice(coord)
943   - Publisher.sendMessage('Set ball reference position', (wx, wy, wz))
  943 + Publisher.sendMessage('Set ball reference position', (ux, uy, uz))
944 944  
945 945 def ScrollSlice(self, coord):
946 946 if self.orientation == "AXIAL":
... ...
invesalius/data/viewer_volume.py
... ... @@ -20,6 +20,7 @@
20 20 # detalhes.
21 21 #--------------------------------------------------------------------------
22 22  
  23 +from math import cos, sin
23 24 import os
24 25 import sys
25 26  
... ... @@ -29,14 +30,16 @@ import wx
29 30 import vtk
30 31 from vtk.wx.wxVTKRenderWindowInteractor import wxVTKRenderWindowInteractor
31 32 from wx.lib.pubsub import pub as Publisher
  33 +import random
  34 +from scipy.spatial import distance
32 35  
33 36 import invesalius.constants as const
34 37 import invesalius.data.bases as bases
  38 +import invesalius.data.transformations as tr
35 39 import invesalius.data.vtk_utils as vtku
36 40 import invesalius.project as prj
37 41 import invesalius.style as st
38 42 import invesalius.utils as utils
39   -import invesalius.data.measures as measures
40 43  
41 44 if sys.platform == 'win32':
42 45 try:
... ... @@ -95,6 +98,14 @@ class Viewer(wx.Panel):
95 98 self.text.SetValue("")
96 99 self.ren.AddActor(self.text.actor)
97 100  
  101 + # axes = vtk.vtkAxesActor()
  102 + # axes.SetXAxisLabelText('x')
  103 + # axes.SetYAxisLabelText('y')
  104 + # axes.SetZAxisLabelText('z')
  105 + # axes.SetTotalLength(50, 50, 50)
  106 + #
  107 + # self.ren.AddActor(axes)
  108 +
98 109  
99 110 self.slice_plane = None
100 111  
... ... @@ -123,9 +134,20 @@ class Viewer(wx.Panel):
123 134 self.repositioned_coronal_plan = 0
124 135 self.added_actor = 0
125 136  
126   - self.camera_state = True
  137 + self.camera_state = const.CAM_MODE
  138 +
  139 + self.nav_status = False
127 140  
128 141 self.ball_actor = None
  142 + self.obj_actor = None
  143 + self.obj_axes = None
  144 + self.obj_name = False
  145 + self.obj_state = None
  146 + self.obj_actor_list = None
  147 + self.arrow_actor_list = None
  148 +
  149 + # self.obj_axes = None
  150 +
129 151 self._mode_cross = False
130 152 self._to_show_ball = 0
131 153 self._ball_ref_visibility = False
... ... @@ -136,6 +158,13 @@ class Viewer(wx.Panel):
136 158 self.timer = False
137 159 self.index = False
138 160  
  161 + self.target_coord = None
  162 + self.aim_actor = None
  163 + self.dummy_coil_actor = None
  164 + self.target_mode = False
  165 + self.anglethreshold = const.COIL_ANGLES_THRESHOLD
  166 + self.distthreshold = const.COIL_COORD_THRESHOLD
  167 +
139 168 def __bind_events(self):
140 169 Publisher.subscribe(self.LoadActor,
141 170 'Load surface actor into viewer')
... ... @@ -159,7 +188,7 @@ class Viewer(wx.Panel):
159 188 'Update raycasting preset')
160 189 ###
161 190 Publisher.subscribe(self.AppendActor,'AppendActor')
162   - Publisher.subscribe(self.SetWidgetInteractor,
  191 + Publisher.subscribe(self.SetWidgetInteractor,
163 192 'Set Widget Interactor')
164 193 Publisher.subscribe(self.OnSetViewAngle,
165 194 'Set volume view angle')
... ... @@ -173,7 +202,8 @@ class Viewer(wx.Panel):
173 202 Publisher.subscribe(self.LoadSlicePlane, 'Load slice plane')
174 203  
175 204 Publisher.subscribe(self.ResetCamClippingRange, 'Reset cam clipping range')
176   - Publisher.subscribe(self.SetVolumeCamera, 'Set camera in volume')
  205 + Publisher.subscribe(self.SetVolumeCamera, 'Co-registered points')
  206 + # Publisher.subscribe(self.SetVolumeCamera, 'Set camera in volume')
177 207 Publisher.subscribe(self.SetVolumeCameraState, 'Update volume camera state')
178 208  
179 209 Publisher.subscribe(self.OnEnableStyle, 'Enable style')
... ... @@ -190,16 +220,16 @@ class Viewer(wx.Panel):
190 220 Publisher.subscribe(self.OnCloseProject, 'Close project data')
191 221  
192 222 Publisher.subscribe(self.RemoveAllActor, 'Remove all volume actors')
193   -
  223 +
194 224 Publisher.subscribe(self.OnExportPicture,'Export picture to file')
195 225  
196 226 Publisher.subscribe(self.OnStartSeed,'Create surface by seeding - start')
197 227 Publisher.subscribe(self.OnEndSeed,'Create surface by seeding - end')
198 228  
199 229 Publisher.subscribe(self.SetStereoMode, 'Set stereo mode')
200   -
  230 +
201 231 Publisher.subscribe(self.Reposition3DPlane, 'Reposition 3D Plane')
202   -
  232 +
203 233 Publisher.subscribe(self.RemoveVolume, 'Remove Volume')
204 234  
205 235 Publisher.subscribe(self.SetBallReferencePosition,
... ... @@ -219,10 +249,25 @@ class Viewer(wx.Panel):
219 249 Publisher.subscribe(self.BlinkMarker, 'Blink Marker')
220 250 Publisher.subscribe(self.StopBlinkMarker, 'Stop Blink Marker')
221 251  
  252 + # Related to object tracking during neuronavigation
  253 + Publisher.subscribe(self.OnNavigationStatus, 'Navigation status')
  254 + Publisher.subscribe(self.UpdateObjectOrientation, 'Update object matrix')
  255 + Publisher.subscribe(self.UpdateTrackObjectState, 'Update track object state')
  256 + Publisher.subscribe(self.UpdateShowObjectState, 'Update show object state')
  257 +
  258 + Publisher.subscribe(self.ActivateTargetMode, 'Target navigation mode')
  259 + Publisher.subscribe(self.OnUpdateObjectTargetGuide, 'Update object matrix')
  260 + Publisher.subscribe(self.OnUpdateTargetCoordinates, 'Update target')
  261 + Publisher.subscribe(self.OnRemoveTarget, 'Disable or enable coil tracker')
  262 + # Publisher.subscribe(self.UpdateObjectTargetView, 'Co-registered points')
  263 + Publisher.subscribe(self.OnTargetMarkerTransparency, 'Set target transparency')
  264 + Publisher.subscribe(self.OnUpdateAngleThreshold, 'Update angle threshold')
  265 + Publisher.subscribe(self.OnUpdateDistThreshold, 'Update dist threshold')
  266 +
222 267 def SetStereoMode(self, pubsub_evt):
223 268 mode = pubsub_evt.data
224 269 ren_win = self.interactor.GetRenderWindow()
225   -
  270 +
226 271 if mode == const.STEREO_OFF:
227 272 ren_win.StereoRenderOff()
228 273 else:
... ... @@ -245,7 +290,7 @@ class Viewer(wx.Panel):
245 290 ren_win.SetStereoTypeToAnaglyph()
246 291  
247 292 ren_win.StereoRenderOn()
248   -
  293 +
249 294 self.interactor.Render()
250 295  
251 296 def _check_ball_reference(self, pubsub_evt):
... ... @@ -318,10 +363,10 @@ class Viewer(wx.Panel):
318 363 def OnStartSeed(self, pubsub_evt):
319 364 index = pubsub_evt.data
320 365 self.seed_points = []
321   -
  366 +
322 367 def OnEndSeed(self, pubsub_evt):
323 368 Publisher.sendMessage("Create surface from seeds",
324   - self.seed_points)
  369 + self.seed_points)
325 370  
326 371 def OnExportPicture(self, pubsub_evt):
327 372 id, filename, filetype = pubsub_evt.data
... ... @@ -458,8 +503,7 @@ class Viewer(wx.Panel):
458 503  
459 504 def AddMarker(self, pubsub_evt):
460 505 """
461   - Markers create by navigation tools and
462   - rendered in volume viewer.
  506 + Markers created by navigation tools and rendered in volume viewer.
463 507 """
464 508 self.ball_id = pubsub_evt.data[0]
465 509 ballsize = pubsub_evt.data[1]
... ... @@ -487,7 +531,7 @@ class Viewer(wx.Panel):
487 531 self.ball_id = self.ball_id + 1
488 532 #self.UpdateRender()
489 533 self.Refresh()
490   -
  534 +
491 535 def HideAllMarkers(self, pubsub_evt):
492 536 ballid = pubsub_evt.data
493 537 for i in range(0, ballid):
... ... @@ -521,11 +565,11 @@ class Viewer(wx.Panel):
521 565 self.staticballs[self.index].SetVisibility(1)
522 566 self.index = pubsub_evt.data
523 567 self.timer = wx.Timer(self)
524   - self.Bind(wx.EVT_TIMER, self.blink, self.timer)
  568 + self.Bind(wx.EVT_TIMER, self.OnBlinkMarker, self.timer)
525 569 self.timer.Start(500)
526 570 self.timer_count = 0
527 571  
528   - def blink(self, evt):
  572 + def OnBlinkMarker(self, evt):
529 573 self.staticballs[self.index].SetVisibility(int(self.timer_count % 2))
530 574 self.Refresh()
531 575 self.timer_count += 1
... ... @@ -533,11 +577,499 @@ class Viewer(wx.Panel):
533 577 def StopBlinkMarker(self, pubsub_evt):
534 578 if self.timer:
535 579 self.timer.Stop()
536   - if pubsub_evt.data == None:
  580 + if pubsub_evt.data is None:
537 581 self.staticballs[self.index].SetVisibility(1)
538 582 self.Refresh()
539 583 self.index = False
540 584  
  585 + def OnTargetMarkerTransparency(self, pubsub_evt):
  586 + status = pubsub_evt.data[0]
  587 + index = pubsub_evt.data[1]
  588 + if status:
  589 + self.staticballs[index].GetProperty().SetOpacity(1)
  590 + # self.staticballs[index].GetProperty().SetOpacity(0.4)
  591 + else:
  592 + self.staticballs[index].GetProperty().SetOpacity(1)
  593 +
  594 + def OnUpdateAngleThreshold(self, pubsub_evt):
  595 + self.anglethreshold = pubsub_evt.data
  596 +
  597 + def OnUpdateDistThreshold(self, pubsub_evt):
  598 + self.distthreshold = pubsub_evt.data
  599 +
  600 + def ActivateTargetMode(self, pubsub_evt):
  601 + self.target_mode = pubsub_evt.data
  602 + if self.target_coord and self.target_mode:
  603 + self.CreateTargetAim()
  604 +
  605 + # Create a line
  606 + self.ren.SetViewport(0, 0, 0.75, 1)
  607 + self.ren2 = vtk.vtkRenderer()
  608 +
  609 + self.interactor.GetRenderWindow().AddRenderer(self.ren2)
  610 + self.ren2.SetViewport(0.75, 0, 1, 1)
  611 + self.CreateTextDistance()
  612 +
  613 + obj_polydata = self.CreateObjectPolyData(self.obj_name)
  614 +
  615 + normals = vtk.vtkPolyDataNormals()
  616 + normals.SetInputData(obj_polydata)
  617 + normals.SetFeatureAngle(80)
  618 + normals.AutoOrientNormalsOn()
  619 + normals.Update()
  620 +
  621 + mapper = vtk.vtkPolyDataMapper()
  622 + mapper.SetInputData(normals.GetOutput())
  623 + mapper.ScalarVisibilityOff()
  624 + mapper.ImmediateModeRenderingOn() # improve performance
  625 +
  626 + obj_roll = vtk.vtkActor()
  627 + obj_roll.SetMapper(mapper)
  628 + obj_roll.SetPosition(0, 25, -30)
  629 + obj_roll.RotateX(-60)
  630 + obj_roll.RotateZ(180)
  631 +
  632 + obj_yaw = vtk.vtkActor()
  633 + obj_yaw.SetMapper(mapper)
  634 + obj_yaw.SetPosition(0, -115, 5)
  635 + obj_yaw.RotateZ(180)
  636 +
  637 + obj_pitch = vtk.vtkActor()
  638 + obj_pitch.SetMapper(mapper)
  639 + obj_pitch.SetPosition(5, -265, 5)
  640 + obj_pitch.RotateY(90)
  641 + obj_pitch.RotateZ(180)
  642 +
  643 + arrow_roll_z1 = self.CreateArrowActor([-50, -35, 12], [-50, -35, 50])
  644 + arrow_roll_z1.GetProperty().SetColor(1, 1, 0)
  645 + arrow_roll_z1.RotateX(-60)
  646 + arrow_roll_z1.RotateZ(180)
  647 + arrow_roll_z2 = self.CreateArrowActor([50, -35, 0], [50, -35, -50])
  648 + arrow_roll_z2.GetProperty().SetColor(1, 1, 0)
  649 + arrow_roll_z2.RotateX(-60)
  650 + arrow_roll_z2.RotateZ(180)
  651 +
  652 + arrow_yaw_y1 = self.CreateArrowActor([-50, -35, 0], [-50, 5, 0])
  653 + arrow_yaw_y1.GetProperty().SetColor(0, 1, 0)
  654 + arrow_yaw_y1.SetPosition(0, -150, 0)
  655 + arrow_yaw_y1.RotateZ(180)
  656 + arrow_yaw_y2 = self.CreateArrowActor([50, -35, 0], [50, -75, 0])
  657 + arrow_yaw_y2.GetProperty().SetColor(0, 1, 0)
  658 + arrow_yaw_y2.SetPosition(0, -150, 0)
  659 + arrow_yaw_y2.RotateZ(180)
  660 +
  661 + arrow_pitch_x1 = self.CreateArrowActor([0, 65, 38], [0, 65, 68])
  662 + arrow_pitch_x1.GetProperty().SetColor(1, 0, 0)
  663 + arrow_pitch_x1.SetPosition(0, -300, 0)
  664 + arrow_pitch_x1.RotateY(90)
  665 + arrow_pitch_x1.RotateZ(180)
  666 + arrow_pitch_x2 = self.CreateArrowActor([0, -55, 5], [0, -55, -30])
  667 + arrow_pitch_x2.GetProperty().SetColor(1, 0, 0)
  668 + arrow_pitch_x2.SetPosition(0, -300, 0)
  669 + arrow_pitch_x2.RotateY(90)
  670 + arrow_pitch_x2.RotateZ(180)
  671 +
  672 + self.obj_actor_list = obj_roll, obj_yaw, obj_pitch
  673 + self.arrow_actor_list = arrow_roll_z1, arrow_roll_z2, arrow_yaw_y1, arrow_yaw_y2,\
  674 + arrow_pitch_x1, arrow_pitch_x2
  675 +
  676 + for ind in self.obj_actor_list:
  677 + self.ren2.AddActor(ind)
  678 +
  679 + for ind in self.arrow_actor_list:
  680 + self.ren2.AddActor(ind)
  681 +
  682 + self.ren.ResetCamera()
  683 + self.SetCameraTarget()
  684 + #self.ren.GetActiveCamera().Zoom(4)
  685 +
  686 + self.ren2.ResetCamera()
  687 + self.ren2.GetActiveCamera().Zoom(2)
  688 + self.ren2.InteractiveOff()
  689 + self.interactor.Render()
  690 +
  691 + else:
  692 + self.DisableCoilTracker()
  693 +
  694 + def OnUpdateObjectTargetGuide(self, pubsub_evt):
  695 + coord = pubsub_evt.data[1]
  696 + if self.target_coord and self.target_mode:
  697 +
  698 + target_dist = distance.euclidean(coord[0:3],
  699 + (self.target_coord[0], -self.target_coord[1], self.target_coord[2]))
  700 +
  701 + # self.txt.SetCoilDistanceValue(target_dist)
  702 + self.textSource.SetText('Dist: ' + str("{:06.2f}".format(target_dist)) + ' mm')
  703 + self.ren.ResetCamera()
  704 + self.SetCameraTarget()
  705 + if target_dist > 100:
  706 + target_dist = 100
  707 + # ((-0.0404*dst) + 5.0404) is the linear equation to normalize the zoom between 1 and 5 times with
  708 + # the distance between 1 and 100 mm
  709 + self.ren.GetActiveCamera().Zoom((-0.0404 * target_dist) + 5.0404)
  710 +
  711 + if target_dist <= self.distthreshold:
  712 + thrdist = True
  713 + self.aim_actor.GetProperty().SetColor(0, 1, 0)
  714 + else:
  715 + thrdist = False
  716 + self.aim_actor.GetProperty().SetColor(1, 1, 1)
  717 +
  718 + coordx = self.target_coord[3] - coord[3]
  719 + if coordx > const.ARROW_UPPER_LIMIT:
  720 + coordx = const.ARROW_UPPER_LIMIT
  721 + elif coordx < -const.ARROW_UPPER_LIMIT:
  722 + coordx = -const.ARROW_UPPER_LIMIT
  723 + coordx = const.ARROW_SCALE * coordx
  724 +
  725 + coordy = self.target_coord[4] - coord[4]
  726 + if coordy > const.ARROW_UPPER_LIMIT:
  727 + coordy = const.ARROW_UPPER_LIMIT
  728 + elif coordy < -const.ARROW_UPPER_LIMIT:
  729 + coordy = -const.ARROW_UPPER_LIMIT
  730 + coordy = const.ARROW_SCALE * coordy
  731 +
  732 + coordz = self.target_coord[5] - coord[5]
  733 + if coordz > const.ARROW_UPPER_LIMIT:
  734 + coordz = const.ARROW_UPPER_LIMIT
  735 + elif coordz < -const.ARROW_UPPER_LIMIT:
  736 + coordz = -const.ARROW_UPPER_LIMIT
  737 + coordz = const.ARROW_SCALE * coordz
  738 +
  739 + for ind in self.arrow_actor_list:
  740 + self.ren2.RemoveActor(ind)
  741 +
  742 + if self.anglethreshold * const.ARROW_SCALE > coordx > -self.anglethreshold * const.ARROW_SCALE:
  743 + thrcoordx = True
  744 + self.obj_actor_list[0].GetProperty().SetColor(0, 1, 0)
  745 + else:
  746 + thrcoordx = False
  747 + self.obj_actor_list[0].GetProperty().SetColor(1, 1, 1)
  748 +
  749 + offset = 5
  750 +
  751 + arrow_roll_x1 = self.CreateArrowActor([-55, -35, offset], [-55, -35, offset - coordx])
  752 + arrow_roll_x1.RotateX(-60)
  753 + arrow_roll_x1.RotateZ(180)
  754 + arrow_roll_x1.GetProperty().SetColor(1, 1, 0)
  755 +
  756 + arrow_roll_x2 = self.CreateArrowActor([55, -35, offset], [55, -35, offset + coordx])
  757 + arrow_roll_x2.RotateX(-60)
  758 + arrow_roll_x2.RotateZ(180)
  759 + arrow_roll_x2.GetProperty().SetColor(1, 1, 0)
  760 +
  761 + if self.anglethreshold * const.ARROW_SCALE > coordz > -self.anglethreshold * const.ARROW_SCALE:
  762 + thrcoordz = True
  763 + self.obj_actor_list[1].GetProperty().SetColor(0, 1, 0)
  764 + else:
  765 + thrcoordz = False
  766 + self.obj_actor_list[1].GetProperty().SetColor(1, 1, 1)
  767 +
  768 + offset = -35
  769 +
  770 + arrow_yaw_z1 = self.CreateArrowActor([-55, offset, 0], [-55, offset - coordz, 0])
  771 + arrow_yaw_z1.SetPosition(0, -150, 0)
  772 + arrow_yaw_z1.RotateZ(180)
  773 + arrow_yaw_z1.GetProperty().SetColor(0, 1, 0)
  774 +
  775 + arrow_yaw_z2 = self.CreateArrowActor([55, offset, 0], [55, offset + coordz, 0])
  776 + arrow_yaw_z2.SetPosition(0, -150, 0)
  777 + arrow_yaw_z2.RotateZ(180)
  778 + arrow_yaw_z2.GetProperty().SetColor(0, 1, 0)
  779 +
  780 + if self.anglethreshold * const.ARROW_SCALE > coordy > -self.anglethreshold * const.ARROW_SCALE:
  781 + thrcoordy = True
  782 + self.obj_actor_list[2].GetProperty().SetColor(0, 1, 0)
  783 + else:
  784 + thrcoordy = False
  785 + self.obj_actor_list[2].GetProperty().SetColor(1, 1, 1)
  786 +
  787 + offset = 38
  788 + arrow_pitch_y1 = self.CreateArrowActor([0, 65, offset], [0, 65, offset + coordy])
  789 + arrow_pitch_y1.SetPosition(0, -300, 0)
  790 + arrow_pitch_y1.RotateY(90)
  791 + arrow_pitch_y1.RotateZ(180)
  792 + arrow_pitch_y1.GetProperty().SetColor(1, 0, 0)
  793 +
  794 + offset = 5
  795 + arrow_pitch_y2 = self.CreateArrowActor([0, -55, offset], [0, -55, offset - coordy])
  796 + arrow_pitch_y2.SetPosition(0, -300, 0)
  797 + arrow_pitch_y2.RotateY(90)
  798 + arrow_pitch_y2.RotateZ(180)
  799 + arrow_pitch_y2.GetProperty().SetColor(1, 0, 0)
  800 +
  801 + if thrdist and thrcoordx and thrcoordy and thrcoordz:
  802 + self.dummy_coil_actor.GetProperty().SetColor(0, 1, 0)
  803 + else:
  804 + self.dummy_coil_actor.GetProperty().SetColor(1, 1, 1)
  805 +
  806 + self.arrow_actor_list = arrow_roll_x1, arrow_roll_x2, arrow_yaw_z1, arrow_yaw_z2, \
  807 + arrow_pitch_y1, arrow_pitch_y2
  808 +
  809 + for ind in self.arrow_actor_list:
  810 + self.ren2.AddActor(ind)
  811 +
  812 + self.Refresh()
  813 +
  814 + def OnUpdateTargetCoordinates(self, pubsub_evt):
  815 + self.target_coord = pubsub_evt.data[0:6]
  816 + self.target_coord[1] = -self.target_coord[1]
  817 + self.CreateTargetAim()
  818 +
  819 + def OnRemoveTarget(self, pubsub_evt):
  820 + status = pubsub_evt.data
  821 + if not status:
  822 + self.target_mode = None
  823 + self.target_coord = None
  824 + self.RemoveTargetAim()
  825 + self.DisableCoilTracker()
  826 +
  827 + def CreateTargetAim(self):
  828 + if self.aim_actor:
  829 + self.RemoveTargetAim()
  830 + self.aim_actor = None
  831 +
  832 + self.textSource = vtk.vtkVectorText()
  833 + mapper = vtk.vtkPolyDataMapper()
  834 + mapper.SetInputConnection(self.textSource.GetOutputPort())
  835 + tactor = vtk.vtkFollower()
  836 + tactor.SetMapper(mapper)
  837 + tactor.GetProperty().SetColor(1.0, 0.25, 0.0)
  838 + tactor.SetScale(5)
  839 + tactor.SetPosition(self.target_coord[0]+10, self.target_coord[1]+30, self.target_coord[2]+20)
  840 + self.ren.AddActor(tactor)
  841 + self.tactor = tactor
  842 + tactor.SetCamera(self.ren.GetActiveCamera())
  843 +
  844 +
  845 + # v3, M_plane_inv = self.Plane(self.target_coord[0:3], self.pTarget)
  846 + # mat4x4 = vtk.vtkMatrix4x4()
  847 + # for i in range(4):
  848 + # mat4x4.SetElement(i, 0, M_plane_inv[i][0])
  849 + # mat4x4.SetElement(i, 1, M_plane_inv[i][1])
  850 + # mat4x4.SetElement(i, 2, M_plane_inv[i][2])
  851 + # mat4x4.SetElement(i, 3, M_plane_inv[i][3])
  852 +
  853 + a, b, g = np.radians(self.target_coord[3:])
  854 + r_ref = tr.euler_matrix(a, b, g, 'sxyz')
  855 + t_ref = tr.translation_matrix(self.target_coord[:3])
  856 + m_img = np.asmatrix(tr.concatenate_matrices(t_ref, r_ref))
  857 +
  858 + m_img_vtk = vtk.vtkMatrix4x4()
  859 +
  860 + for row in range(0, 4):
  861 + for col in range(0, 4):
  862 + m_img_vtk.SetElement(row, col, m_img[row, col])
  863 +
  864 + self.m_img_vtk = m_img_vtk
  865 +
  866 + filename = os.path.join(const.OBJ_DIR, "aim.stl")
  867 +
  868 + reader = vtk.vtkSTLReader()
  869 + reader.SetFileName(filename)
  870 + mapper = vtk.vtkPolyDataMapper()
  871 + mapper.SetInputConnection(reader.GetOutputPort())
  872 +
  873 + # Transform the polydata
  874 + transform = vtk.vtkTransform()
  875 + transform.SetMatrix(m_img_vtk)
  876 + #transform.SetMatrix(mat4x4)
  877 + transformPD = vtk.vtkTransformPolyDataFilter()
  878 + transformPD.SetTransform(transform)
  879 + transformPD.SetInputConnection(reader.GetOutputPort())
  880 + transformPD.Update()
  881 + # mapper transform
  882 + mapper.SetInputConnection(transformPD.GetOutputPort())
  883 +
  884 + aim_actor = vtk.vtkActor()
  885 + aim_actor.SetMapper(mapper)
  886 + aim_actor.GetProperty().SetColor(1, 1, 1)
  887 + aim_actor.GetProperty().SetOpacity(0.6)
  888 + self.aim_actor = aim_actor
  889 + self.ren.AddActor(aim_actor)
  890 +
  891 + obj_polydata = self.CreateObjectPolyData(os.path.join(const.OBJ_DIR, "magstim_fig8_coil_no_handle.stl"))
  892 +
  893 + transform = vtk.vtkTransform()
  894 + transform.RotateZ(90)
  895 +
  896 + transform_filt = vtk.vtkTransformPolyDataFilter()
  897 + transform_filt.SetTransform(transform)
  898 + transform_filt.SetInputData(obj_polydata)
  899 + transform_filt.Update()
  900 +
  901 + normals = vtk.vtkPolyDataNormals()
  902 + normals.SetInputData(transform_filt.GetOutput())
  903 + normals.SetFeatureAngle(80)
  904 + normals.AutoOrientNormalsOn()
  905 + normals.Update()
  906 +
  907 + obj_mapper = vtk.vtkPolyDataMapper()
  908 + obj_mapper.SetInputData(normals.GetOutput())
  909 + obj_mapper.ScalarVisibilityOff()
  910 + obj_mapper.ImmediateModeRenderingOn() # improve performance
  911 +
  912 + self.dummy_coil_actor = vtk.vtkActor()
  913 + self.dummy_coil_actor.SetMapper(obj_mapper)
  914 + self.dummy_coil_actor.GetProperty().SetOpacity(0.4)
  915 + self.dummy_coil_actor.SetVisibility(1)
  916 + self.dummy_coil_actor.SetUserMatrix(m_img_vtk)
  917 +
  918 + self.ren.AddActor(self.dummy_coil_actor)
  919 +
  920 + self.Refresh()
  921 +
  922 + def RemoveTargetAim(self):
  923 + self.ren.RemoveActor(self.aim_actor)
  924 + self.ren.RemoveActor(self.dummy_coil_actor)
  925 + self.ren.RemoveActor(self.tactor)
  926 + self.Refresh()
  927 +
  928 + def CreateTextDistance(self):
  929 + txt = vtku.Text()
  930 + txt.SetSize(const.TEXT_SIZE_EXTRA_LARGE)
  931 + txt.SetPosition((0.76, 0.05))
  932 + txt.ShadowOff()
  933 + txt.BoldOn()
  934 + self.txt = txt
  935 + self.ren2.AddActor(txt.actor)
  936 +
  937 + def DisableCoilTracker(self):
  938 + try:
  939 + self.ren.SetViewport(0, 0, 1, 1)
  940 + self.interactor.GetRenderWindow().RemoveRenderer(self.ren2)
  941 + self.SetViewAngle(const.VOL_FRONT)
  942 + self.ren.RemoveActor(self.txt.actor)
  943 + self.CreateTargetAim()
  944 + self.interactor.Render()
  945 + except:
  946 + None
  947 +
  948 + def CreateArrowActor(self, startPoint, endPoint):
  949 + # Compute a basis
  950 + normalizedX = [0 for i in range(3)]
  951 + normalizedY = [0 for i in range(3)]
  952 + normalizedZ = [0 for i in range(3)]
  953 +
  954 + # The X axis is a vector from start to end
  955 + math = vtk.vtkMath()
  956 + math.Subtract(endPoint, startPoint, normalizedX)
  957 + length = math.Norm(normalizedX)
  958 + math.Normalize(normalizedX)
  959 +
  960 + # The Z axis is an arbitrary vector cross X
  961 + arbitrary = [0 for i in range(3)]
  962 + arbitrary[0] = random.uniform(-10, 10)
  963 + arbitrary[1] = random.uniform(-10, 10)
  964 + arbitrary[2] = random.uniform(-10, 10)
  965 + math.Cross(normalizedX, arbitrary, normalizedZ)
  966 + math.Normalize(normalizedZ)
  967 +
  968 + # The Y axis is Z cross X
  969 + math.Cross(normalizedZ, normalizedX, normalizedY)
  970 + matrix = vtk.vtkMatrix4x4()
  971 +
  972 + # Create the direction cosine matrix
  973 + matrix.Identity()
  974 + for i in range(3):
  975 + matrix.SetElement(i, 0, normalizedX[i])
  976 + matrix.SetElement(i, 1, normalizedY[i])
  977 + matrix.SetElement(i, 2, normalizedZ[i])
  978 +
  979 + # Apply the transforms arrow 1
  980 + transform_1 = vtk.vtkTransform()
  981 + transform_1.Translate(startPoint)
  982 + transform_1.Concatenate(matrix)
  983 + transform_1.Scale(length, length, length)
  984 + # source
  985 + arrowSource1 = vtk.vtkArrowSource()
  986 + arrowSource1.SetTipResolution(50)
  987 + # Create a mapper and actor
  988 + mapper = vtk.vtkPolyDataMapper()
  989 + mapper.SetInputConnection(arrowSource1.GetOutputPort())
  990 + # Transform the polydata
  991 + transformPD = vtk.vtkTransformPolyDataFilter()
  992 + transformPD.SetTransform(transform_1)
  993 + transformPD.SetInputConnection(arrowSource1.GetOutputPort())
  994 + # mapper transform
  995 + mapper.SetInputConnection(transformPD.GetOutputPort())
  996 + # actor
  997 + actor_arrow = vtk.vtkActor()
  998 + actor_arrow.SetMapper(mapper)
  999 +
  1000 + return actor_arrow
  1001 +
  1002 + def CenterOfMass(self):
  1003 + proj = prj.Project()
  1004 + surface = proj.surface_dict[0].polydata
  1005 + barycenter = [0.0, 0.0, 0.0]
  1006 + n = surface.GetNumberOfPoints()
  1007 + for i in range(n):
  1008 + point = surface.GetPoint(i)
  1009 + barycenter[0] += point[0]
  1010 + barycenter[1] += point[1]
  1011 + barycenter[2] += point[2]
  1012 + barycenter[0] /= n
  1013 + barycenter[1] /= n
  1014 + barycenter[2] /= n
  1015 +
  1016 + return barycenter
  1017 +
  1018 + def Plane(self, x0, pTarget):
  1019 + v3 = np.array(pTarget) - x0 # normal to the plane
  1020 + v3 = v3 / np.linalg.norm(v3) # unit vector
  1021 +
  1022 + d = np.dot(v3, x0)
  1023 + # prevents division by zero.
  1024 + if v3[0] == 0.0:
  1025 + v3[0] = 1e-09
  1026 +
  1027 + x1 = np.array([(d - v3[1] - v3[2]) / v3[0], 1, 1])
  1028 + v2 = x1 - x0
  1029 + v2 = v2 / np.linalg.norm(v2) # unit vector
  1030 + v1 = np.cross(v3, v2)
  1031 + v1 = v1 / np.linalg.norm(v1) # unit vector
  1032 + x2 = x0 + v1
  1033 + # calculates the matrix for the change of coordinate systems (from canonical to the plane's).
  1034 + # remember that, in np.dot(M,p), even though p is a line vector (e.g.,np.array([1,2,3])), it is treated as a column for the dot multiplication.
  1035 + M_plane_inv = np.array([[v1[0], v2[0], v3[0], x0[0]],
  1036 + [v1[1], v2[1], v3[1], x0[1]],
  1037 + [v1[2], v2[2], v3[2], x0[2]],
  1038 + [0, 0, 0, 1]])
  1039 +
  1040 + return v3, M_plane_inv
  1041 +
  1042 + def SetCameraTarget(self):
  1043 + cam_focus = self.target_coord[0:3]
  1044 + cam = self.ren.GetActiveCamera()
  1045 +
  1046 + oldcamVTK = vtk.vtkMatrix4x4()
  1047 + oldcamVTK.DeepCopy(cam.GetViewTransformMatrix())
  1048 +
  1049 + newvtk = vtk.vtkMatrix4x4()
  1050 + newvtk.Multiply4x4(self.m_img_vtk, oldcamVTK, newvtk)
  1051 +
  1052 + transform = vtk.vtkTransform()
  1053 + transform.SetMatrix(newvtk)
  1054 + transform.Update()
  1055 + cam.ApplyTransform(transform)
  1056 +
  1057 + cam.Roll(90)
  1058 +
  1059 + cam_pos0 = np.array(cam.GetPosition())
  1060 + cam_focus0 = np.array(cam.GetFocalPoint())
  1061 + v0 = cam_pos0 - cam_focus0
  1062 + v0n = np.sqrt(inner1d(v0, v0))
  1063 +
  1064 + v1 = (cam_focus[0] - cam_focus0[0], cam_focus[1] - cam_focus0[1], cam_focus[2] - cam_focus0[2])
  1065 + v1n = np.sqrt(inner1d(v1, v1))
  1066 + if not v1n:
  1067 + v1n = 1.0
  1068 + cam_pos = (v1 / v1n) * v0n + cam_focus
  1069 + cam.SetFocalPoint(cam_focus)
  1070 + cam.SetPosition(cam_pos)
  1071 +
  1072 +
541 1073 def CreateBallReference(self):
542 1074 """
543 1075 Red sphere on volume visualization to reference center of
... ... @@ -545,7 +1077,7 @@ class Viewer(wx.Panel):
545 1077 The sphere's radius will be scale times bigger than the average of
546 1078 image spacing values.
547 1079 """
548   - scale = 3.0
  1080 + scale = 2.0
549 1081 proj = prj.Project()
550 1082 s = proj.spacing
551 1083 r = (s[0] + s[1] + s[2]) / 3.0 * scale
... ... @@ -588,6 +1120,129 @@ class Viewer(wx.Panel):
588 1120 else:
589 1121 self.RemoveBallReference()
590 1122  
  1123 + def CreateObjectPolyData(self, filename):
  1124 + """
  1125 + Coil for navigation rendered in volume viewer.
  1126 + """
  1127 +
  1128 + if filename:
  1129 + if filename.lower().endswith('.stl'):
  1130 + reader = vtk.vtkSTLReader()
  1131 + elif filename.lower().endswith('.ply'):
  1132 + reader = vtk.vtkPLYReader()
  1133 + elif filename.lower().endswith('.obj'):
  1134 + reader = vtk.vtkOBJReader()
  1135 + elif filename.lower().endswith('.vtp'):
  1136 + reader = vtk.vtkXMLPolyDataReader()
  1137 + else:
  1138 + wx.MessageBox(_("File format not reconized by InVesalius"), _("Import surface error"))
  1139 + return
  1140 + else:
  1141 + filename = os.path.join(const.OBJ_DIR, "magstim_fig8_coil.stl")
  1142 + reader = vtk.vtkSTLReader()
  1143 +
  1144 + if _has_win32api:
  1145 + obj_name = win32api.GetShortPathName(filename).encode(const.FS_ENCODE)
  1146 + else:
  1147 + obj_name = filename.encode(const.FS_ENCODE)
  1148 +
  1149 + reader.SetFileName(obj_name)
  1150 + reader.Update()
  1151 + obj_polydata = reader.GetOutput()
  1152 +
  1153 + if obj_polydata.GetNumberOfPoints() == 0:
  1154 + wx.MessageBox(_("InVesalius was not able to import this surface"), _("Import surface error"))
  1155 + obj_polydata = None
  1156 +
  1157 + return obj_polydata
  1158 +
  1159 + def AddObjectActor(self, obj_name):
  1160 + """
  1161 + Coil for navigation rendered in volume viewer.
  1162 + """
  1163 +
  1164 + obj_polydata = self.CreateObjectPolyData(obj_name)
  1165 +
  1166 + transform = vtk.vtkTransform()
  1167 + transform.RotateZ(90)
  1168 +
  1169 + transform_filt = vtk.vtkTransformPolyDataFilter()
  1170 + transform_filt.SetTransform(transform)
  1171 + transform_filt.SetInputData(obj_polydata)
  1172 + transform_filt.Update()
  1173 +
  1174 + normals = vtk.vtkPolyDataNormals()
  1175 + normals.SetInputData(transform_filt.GetOutput())
  1176 + normals.SetFeatureAngle(80)
  1177 + normals.AutoOrientNormalsOn()
  1178 + normals.Update()
  1179 +
  1180 + obj_mapper = vtk.vtkPolyDataMapper()
  1181 + obj_mapper.SetInputData(normals.GetOutput())
  1182 + obj_mapper.ScalarVisibilityOff()
  1183 + obj_mapper.ImmediateModeRenderingOn() # improve performance
  1184 +
  1185 + self.obj_actor = vtk.vtkActor()
  1186 + self.obj_actor.SetMapper(obj_mapper)
  1187 + self.obj_actor.GetProperty().SetOpacity(0.9)
  1188 + self.obj_actor.SetVisibility(0)
  1189 +
  1190 + self.ren.AddActor(self.obj_actor)
  1191 +
  1192 + # self.obj_axes = vtk.vtkAxesActor()
  1193 + # self.obj_axes.SetShaftTypeToCylinder()
  1194 + # self.obj_axes.SetXAxisLabelText("x")
  1195 + # self.obj_axes.SetYAxisLabelText("y")
  1196 + # self.obj_axes.SetZAxisLabelText("z")
  1197 + # self.obj_axes.SetTotalLength(50.0, 50.0, 50.0)
  1198 +
  1199 + # self.ren.AddActor(self.obj_axes)
  1200 +
  1201 + def OnNavigationStatus(self, pubsub_evt):
  1202 + self.nav_status = pubsub_evt.data
  1203 + self.pTarget = self.CenterOfMass()
  1204 + if self.obj_actor and self.nav_status:
  1205 + self.obj_actor.SetVisibility(self.obj_state)
  1206 + if not self.obj_state:
  1207 + self.Refresh()
  1208 +
  1209 + def UpdateObjectOrientation(self, pubsub_evt):
  1210 +
  1211 + m_img = pubsub_evt.data[0]
  1212 +
  1213 + m_img[:3, -1] = np.asmatrix(bases.flip_x_m((m_img[0, -1], m_img[1, -1], m_img[2, -1]))).reshape([3, 1])
  1214 +
  1215 + m_img_vtk = vtk.vtkMatrix4x4()
  1216 +
  1217 + for row in range(0, 4):
  1218 + for col in range(0, 4):
  1219 + m_img_vtk.SetElement(row, col, m_img[row, col])
  1220 +
  1221 + self.obj_actor.SetUserMatrix(m_img_vtk)
  1222 + # self.obj_axes.SetUserMatrix(m_rot_vtk)
  1223 +
  1224 + self.Refresh()
  1225 +
  1226 + def UpdateTrackObjectState(self, pubsub_evt):
  1227 + if pubsub_evt.data[0]:
  1228 + self.obj_name = pubsub_evt.data[1]
  1229 +
  1230 + if not self.obj_actor:
  1231 + self.AddObjectActor(self.obj_name)
  1232 +
  1233 + else:
  1234 + if self.obj_actor:
  1235 + self.ren.RemoveActor(self.obj_actor)
  1236 + self.obj_actor = None
  1237 +
  1238 + self.Refresh()
  1239 +
  1240 + def UpdateShowObjectState(self, pubsub_evt):
  1241 + self.obj_state = pubsub_evt.data
  1242 + if self.obj_actor and not self.obj_state:
  1243 + self.obj_actor.SetVisibility(self.obj_state)
  1244 + self.Refresh()
  1245 +
591 1246 def __bind_events_wx(self):
592 1247 #self.Bind(wx.EVT_SIZE, self.OnSize)
593 1248 pass
... ... @@ -613,7 +1268,7 @@ class Viewer(wx.Panel):
613 1268 "LeftButtonReleaseEvent": self.OnReleaseSpinClick,
614 1269 },
615 1270 const.STATE_WL:
616   - {
  1271 + {
617 1272 "MouseMoveEvent": self.OnWindowLevelMove,
618 1273 "LeftButtonPressEvent": self.OnWindowLevelClick,
619 1274 "LeftButtonReleaseEvent":self.OnWindowLevelRelease
... ... @@ -661,7 +1316,7 @@ class Viewer(wx.Panel):
661 1316 else:
662 1317 style = vtk.vtkInteractorStyleTrackballCamera()
663 1318 self.interactor.SetInteractorStyle(style)
664   - self.style = style
  1319 + self.style = style
665 1320  
666 1321 # Check each event available for each mode
667 1322 for event in action[state]:
... ... @@ -755,8 +1410,8 @@ class Viewer(wx.Panel):
755 1410  
756 1411 def SetVolumeCamera(self, pubsub_evt):
757 1412 if self.camera_state:
758   - #TODO: exclude dependency on initial focus
759   - cam_focus = np.array(bases.flip_x(pubsub_evt.data))
  1413 + # TODO: exclude dependency on initial focus
  1414 + cam_focus = np.array(bases.flip_x(pubsub_evt.data[1][:3]))
760 1415 cam = self.ren.GetActiveCamera()
761 1416  
762 1417 if self.initial_focus is None:
... ... @@ -764,11 +1419,14 @@ class Viewer(wx.Panel):
764 1419  
765 1420 cam_pos0 = np.array(cam.GetPosition())
766 1421 cam_focus0 = np.array(cam.GetFocalPoint())
767   -
768 1422 v0 = cam_pos0 - cam_focus0
769 1423 v0n = np.sqrt(inner1d(v0, v0))
770 1424  
771   - v1 = (cam_focus - self.initial_focus)
  1425 + if self.obj_state:
  1426 + v1 = (cam_focus[0] - self.pTarget[0], cam_focus[1] - self.pTarget[1], cam_focus[2] - self.pTarget[2])
  1427 + else:
  1428 + v1 = (cam_focus - self.initial_focus)
  1429 +
772 1430 v1n = np.sqrt(inner1d(v1, v1))
773 1431 if not v1n:
774 1432 v1n = 1.0
... ... @@ -960,7 +1618,7 @@ class Viewer(wx.Panel):
960 1618 cam.SetViewUp(xv,yv,zv)
961 1619 cam.SetPosition(xp,yp,zp)
962 1620  
963   - self.ren.ResetCameraClippingRange()
  1621 + self.ren.ResetCameraClippingRange()
964 1622 self.ren.ResetCamera()
965 1623 self.interactor.Render()
966 1624  
... ... @@ -1020,10 +1678,10 @@ class Viewer(wx.Panel):
1020 1678 x,y = self.interactor.GetEventPosition()
1021 1679 self.measure_picker.Pick(x, y, 0, self.ren)
1022 1680 x, y, z = self.measure_picker.GetPickPosition()
1023   -
  1681 +
1024 1682 proj = prj.Project()
1025 1683 radius = min(proj.spacing) * PROP_MEASURE
1026   - if self.measure_picker.GetActor():
  1684 + if self.measure_picker.GetActor():
1027 1685 # if not self.measures or self.measures[-1].IsComplete():
1028 1686 # m = measures.LinearMeasure(self.ren)
1029 1687 # m.AddPoint(x, y, z)
... ... @@ -1032,7 +1690,7 @@ class Viewer(wx.Panel):
1032 1690 # m = self.measures[-1]
1033 1691 # m.AddPoint(x, y, z)
1034 1692 # if m.IsComplete():
1035   - # Publisher.sendMessage("Add measure to list",
  1693 + # Publisher.sendMessage("Add measure to list",
1036 1694 # (u"3D", _(u"%.3f mm" % m.GetValue())))
1037 1695 Publisher.sendMessage("Add measurement point",
1038 1696 ((x, y,z), const.LINEAR, const.SURFACE, radius))
... ... @@ -1045,7 +1703,7 @@ class Viewer(wx.Panel):
1045 1703  
1046 1704 proj = prj.Project()
1047 1705 radius = min(proj.spacing) * PROP_MEASURE
1048   - if self.measure_picker.GetActor():
  1706 + if self.measure_picker.GetActor():
1049 1707 # if not self.measures or self.measures[-1].IsComplete():
1050 1708 # m = measures.AngularMeasure(self.ren)
1051 1709 # m.AddPoint(x, y, z)
... ...
invesalius/data/vtk_utils.py
... ... @@ -124,6 +124,9 @@ class Text(object):
124 124 def ShadowOff(self):
125 125 self.property.ShadowOff()
126 126  
  127 + def BoldOn(self):
  128 + self.property.BoldOn()
  129 +
127 130 def SetSize(self, size):
128 131 self.property.SetFontSize(size)
129 132  
... ... @@ -135,15 +138,32 @@ class Text(object):
135 138 # With some encoding in some dicom fields (like name) raises a
136 139 # UnicodeEncodeError because they have non-ascii characters. To avoid
137 140 # that we encode in utf-8.
138   -
  141 +
139 142 if sys.platform == 'win32':
140   - self.mapper.SetInput(value.encode("utf-8"))
  143 + self.mapper.SetInput(value.encode("utf-8"))
141 144 else:
142 145 try:
143 146 self.mapper.SetInput(value.encode("latin-1"))
144 147 except(UnicodeEncodeError):
145 148 self.mapper.SetInput(value.encode("utf-8"))
146 149  
  150 + def SetCoilDistanceValue(self, value):
  151 + if isinstance(value, int) or isinstance(value, float):
  152 + value = 'Dist: ' + str("{:06.2f}".format(value)) + ' mm'
  153 + if sys.platform == 'win32':
  154 + value += "" # Otherwise 0 is not shown under win32
  155 + # With some encoding in some dicom fields (like name) raises a
  156 + # UnicodeEncodeError because they have non-ascii characters. To avoid
  157 + # that we encode in utf-8.
  158 +
  159 + if sys.platform == 'win32':
  160 + self.mapper.SetInput(value.encode("utf-8"))
  161 + else:
  162 + try:
  163 + self.mapper.SetInput(value.encode("latin-1"))
  164 + except(UnicodeEncodeError):
  165 + self.mapper.SetInput(value.encode("utf-8"))
  166 +
147 167 def SetPosition(self, position):
148 168 self.actor.GetPositionCoordinate().SetValue(position[0],
149 169 position[1])
... ...
invesalius/gui/default_viewers.py
... ... @@ -306,7 +306,7 @@ import wx.lib.colourselect as csel
306 306  
307 307 import invesalius.constants as const
308 308  
309   -[BUTTON_RAYCASTING, BUTTON_VIEW, BUTTON_SLICE_PLANE, BUTTON_3D_STEREO] = [wx.NewId() for num in xrange(4)]
  309 +[BUTTON_RAYCASTING, BUTTON_VIEW, BUTTON_SLICE_PLANE, BUTTON_3D_STEREO, BUTTON_TARGET] = [wx.NewId() for num in xrange(5)]
310 310 RAYCASTING_TOOLS = wx.NewId()
311 311  
312 312 ID_TO_NAME = {}
... ... @@ -346,6 +346,9 @@ class VolumeToolPanel(wx.Panel):
346 346 BMP_3D_STEREO = wx.Bitmap(os.path.join(const.ICON_DIR, "3D_glasses.png"),
347 347 wx.BITMAP_TYPE_PNG)
348 348  
  349 + BMP_TARGET = wx.Bitmap(os.path.join(const.ICON_DIR, "target.png"),
  350 + wx.BITMAP_TYPE_PNG)
  351 +
349 352  
350 353 button_raycasting = pbtn.PlateButton(self, BUTTON_RAYCASTING,"",
351 354 BMP_RAYCASTING, style=pbtn.PB_STYLE_SQUARE,
... ... @@ -359,6 +362,11 @@ class VolumeToolPanel(wx.Panel):
359 362 BMP_SLICE_PLANE, style=pbtn.PB_STYLE_SQUARE,
360 363 size=(32,32))
361 364  
  365 + button_target = self.button_target = pbtn.PlateButton(self, BUTTON_TARGET,"",
  366 + BMP_TARGET, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE,
  367 + size=(32,32))
  368 + self.button_target.Enable(0)
  369 +
362 370 self.button_raycasting = button_raycasting
363 371 self.button_stereo = button_stereo
364 372  
... ... @@ -389,7 +397,11 @@ class VolumeToolPanel(wx.Panel):
389 397 sizer.Add(button_view, 0, wx.TOP|wx.BOTTOM, 1)
390 398 sizer.Add(button_slice_plane, 0, wx.TOP|wx.BOTTOM, 1)
391 399 sizer.Add(button_stereo, 0, wx.TOP|wx.BOTTOM, 1)
  400 + sizer.Add(button_target, 0, wx.TOP | wx.BOTTOM, 1)
392 401  
  402 + self.navigation_status = False
  403 + self.status_target_select = False
  404 + self.status_obj_tracker = False
393 405  
394 406 sizer.Fit(self)
395 407  
... ... @@ -408,6 +420,8 @@ class VolumeToolPanel(wx.Panel):
408 420 Publisher.subscribe(self.DisablePreset, 'Close project data')
409 421 Publisher.subscribe(self.Uncheck, 'Uncheck image plane menu')
410 422 Publisher.subscribe(self.DisableVolumeCutMenu, 'Disable volume cut menu')
  423 + Publisher.subscribe(self.StatusTargetSelect, 'Disable or enable coil tracker')
  424 + Publisher.subscribe(self.StatusObjTracker, 'Status target button')
411 425  
412 426 def DisablePreset(self, pubsub_evt):
413 427 self.off_item.Check(1)
... ... @@ -419,6 +433,7 @@ class VolumeToolPanel(wx.Panel):
419 433 self.button_view.Bind(wx.EVT_LEFT_DOWN, self.OnButtonView)
420 434 self.button_colour.Bind(csel.EVT_COLOURSELECT, self.OnSelectColour)
421 435 self.button_stereo.Bind(wx.EVT_LEFT_DOWN, self.OnButtonStereo)
  436 + self.button_target.Bind(wx.EVT_LEFT_DOWN, self.OnButtonTarget)
422 437  
423 438 def OnButtonRaycasting(self, evt):
424 439 # MENU RELATED TO RAYCASTING TYPES
... ... @@ -433,6 +448,29 @@ class VolumeToolPanel(wx.Panel):
433 448 def OnButtonSlicePlane(self, evt):
434 449 self.button_slice_plane.PopupMenu(self.slice_plane_menu)
435 450  
  451 + def StatusObjTracker(self, pubsub_evt):
  452 + self.status_obj_tracker = pubsub_evt.data
  453 + self.StatusNavigation()
  454 +
  455 + def StatusTargetSelect(self, pubsub_evt):
  456 + self.status_target_select = pubsub_evt.data
  457 + self.StatusNavigation()
  458 +
  459 + def StatusNavigation(self):
  460 + if self.status_target_select and self.status_obj_tracker:
  461 + self.button_target.Enable(1)
  462 + else:
  463 + self.OnButtonTarget(False)
  464 + self.button_target.Enable(0)
  465 +
  466 + def OnButtonTarget(self, evt):
  467 + if not self.button_target.IsPressed() and evt is not False:
  468 + self.button_target._pressed = True
  469 + Publisher.sendMessage('Target navigation mode', self.button_target._pressed)
  470 + elif self.button_target.IsPressed() or evt is False:
  471 + self.button_target._pressed = False
  472 + Publisher.sendMessage('Target navigation mode', self.button_target._pressed)
  473 +
436 474 def OnSavePreset(self, evt):
437 475 d = wx.TextEntryDialog(self, _("Preset name"))
438 476 if d.ShowModal() == wx.ID_OK:
... ...
invesalius/gui/dialogs.py
... ... @@ -18,10 +18,20 @@
18 18 # PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
19 19 # detalhes.
20 20 #--------------------------------------------------------------------------
  21 +
21 22 import os
22 23 import random
23 24 import sys
24 25  
  26 +if sys.platform == 'win32':
  27 + try:
  28 + import win32api
  29 + _has_win32api = True
  30 + except ImportError:
  31 + _has_win32api = False
  32 +else:
  33 + _has_win32api = False
  34 +
25 35 import vtk
26 36 import wx
27 37 import wx.combo
... ... @@ -33,6 +43,7 @@ from wx.lib.wordwrap import wordwrap
33 43 from wx.lib.pubsub import pub as Publisher
34 44  
35 45 import invesalius.constants as const
  46 +import invesalius.data.coordinates as dco
36 47 import invesalius.gui.widgets.gradient as grad
37 48 import invesalius.session as ses
38 49 import invesalius.utils as utils
... ... @@ -470,6 +481,36 @@ def ShowSaveMarkersDialog(default_filename=None):
470 481 os.chdir(current_dir)
471 482 return filename
472 483  
  484 +def ShowSaveCoordsDialog(default_filename=None):
  485 + current_dir = os.path.abspath(".")
  486 + dlg = wx.FileDialog(None,
  487 + _("Save coords as..."), # title
  488 + "", # last used directory
  489 + default_filename,
  490 + _("Coordinates files (*.csv)|*.csv"),
  491 + wx.SAVE | wx.OVERWRITE_PROMPT)
  492 + # dlg.SetFilterIndex(0) # default is VTI
  493 +
  494 + filename = None
  495 + try:
  496 + if dlg.ShowModal() == wx.ID_OK:
  497 + filename = dlg.GetPath()
  498 + ok = 1
  499 + else:
  500 + ok = 0
  501 + except(wx._core.PyAssertionError): # TODO: fix win64
  502 + filename = dlg.GetPath()
  503 + ok = 1
  504 +
  505 + if (ok):
  506 + extension = "csv"
  507 + if sys.platform != 'win32':
  508 + if filename.split(".")[-1] != extension:
  509 + filename = filename + "." + extension
  510 +
  511 + os.chdir(current_dir)
  512 + return filename
  513 +
473 514  
474 515 def ShowLoadMarkersDialog():
475 516 current_dir = os.path.abspath(".")
... ... @@ -500,6 +541,66 @@ def ShowLoadMarkersDialog():
500 541 return filepath
501 542  
502 543  
  544 +def ShowSaveRegistrationDialog(default_filename=None):
  545 + current_dir = os.path.abspath(".")
  546 + dlg = wx.FileDialog(None,
  547 + _("Save object registration as..."), # title
  548 + "", # last used directory
  549 + default_filename,
  550 + _("Registration files (*.obr)|*.obr"),
  551 + wx.SAVE | wx.OVERWRITE_PROMPT)
  552 + # dlg.SetFilterIndex(0) # default is VTI
  553 +
  554 + filename = None
  555 + try:
  556 + if dlg.ShowModal() == wx.ID_OK:
  557 + filename = dlg.GetPath()
  558 + ok = 1
  559 + else:
  560 + ok = 0
  561 + except(wx._core.PyAssertionError): # TODO: fix win64
  562 + filename = dlg.GetPath()
  563 + ok = 1
  564 +
  565 + if (ok):
  566 + extension = "obr"
  567 + if sys.platform != 'win32':
  568 + if filename.split(".")[-1] != extension:
  569 + filename = filename + "." + extension
  570 +
  571 + os.chdir(current_dir)
  572 + return filename
  573 +
  574 +
  575 +def ShowLoadRegistrationDialog():
  576 + current_dir = os.path.abspath(".")
  577 +
  578 + dlg = wx.FileDialog(None, message=_("Load object registration"),
  579 + defaultDir="",
  580 + defaultFile="",
  581 + wildcard=_("Registration files (*.obr)|*.obr"),
  582 + style=wx.OPEN|wx.CHANGE_DIR)
  583 +
  584 + # inv3 filter is default
  585 + dlg.SetFilterIndex(0)
  586 +
  587 + # Show the dialog and retrieve the user response. If it is the OK response,
  588 + # process the data.
  589 + filepath = None
  590 + try:
  591 + if dlg.ShowModal() == wx.ID_OK:
  592 + # This returns a Python list of files that were selected.
  593 + filepath = dlg.GetPath()
  594 + except(wx._core.PyAssertionError): # FIX: win64
  595 + filepath = dlg.GetPath()
  596 +
  597 + # Destroy the dialog. Don't do this until you are done with it!
  598 + # BAD things can happen otherwise!
  599 + dlg.Destroy()
  600 + os.chdir(current_dir)
  601 + return filepath
  602 +
  603 +
503 604 class MessageDialog(wx.Dialog):
504 605 def __init__(self, message):
505 606 pre = wx.PreDialog()
... ... @@ -729,7 +830,19 @@ def SurfaceSelectionRequiredForDuplication():
729 830  
730 831 # Dialogs for neuronavigation mode
731 832 def InvalidFiducials():
732   - msg = _("Fiducials are invalid. Select six coordinates.")
  833 + msg = _("Fiducials are invalid. Select all coordinates.")
  834 + if sys.platform == 'darwin':
  835 + dlg = wx.MessageDialog(None, "", msg,
  836 + wx.ICON_INFORMATION | wx.OK)
  837 + else:
  838 + dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",
  839 + wx.ICON_INFORMATION | wx.OK)
  840 + dlg.ShowModal()
  841 + dlg.Destroy()
  842 +
  843 +
  844 +def InvalidObjectRegistration():
  845 + msg = _("Perform coil registration before navigation.")
733 846 if sys.platform == 'darwin':
734 847 dlg = wx.MessageDialog(None, "", msg,
735 848 wx.ICON_INFORMATION | wx.OK)
... ... @@ -793,6 +906,7 @@ def NoMarkerSelected():
793 906 dlg.ShowModal()
794 907 dlg.Destroy()
795 908  
  909 +
796 910 def DeleteAllMarkers():
797 911 msg = _("Do you really want to delete all markers?")
798 912 if sys.platform == 'darwin':
... ... @@ -805,6 +919,38 @@ def DeleteAllMarkers():
805 919 dlg.Destroy()
806 920 return result
807 921  
  922 +def DeleteTarget():
  923 + msg = _("Target deleted")
  924 + if sys.platform == 'darwin':
  925 + dlg = wx.MessageDialog(None, "", msg,
  926 + wx.ICON_INFORMATION | wx.OK)
  927 + else:
  928 + dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",
  929 + wx.ICON_INFORMATION | wx.OK)
  930 + dlg.ShowModal()
  931 + dlg.Destroy()
  932 +
  933 +def NewTarget():
  934 + msg = _("New target selected")
  935 + if sys.platform == 'darwin':
  936 + dlg = wx.MessageDialog(None, "", msg,
  937 + wx.ICON_INFORMATION | wx.OK)
  938 + else:
  939 + dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",
  940 + wx.ICON_INFORMATION | wx.OK)
  941 + dlg.ShowModal()
  942 + dlg.Destroy()
  943 +
  944 +def InvalidTargetID():
  945 + msg = _("Sorry, you cannot use 'TARGET' ID")
  946 + if sys.platform == 'darwin':
  947 + dlg = wx.MessageDialog(None, "", msg,
  948 + wx.ICON_INFORMATION | wx.OK)
  949 + else:
  950 + dlg = wx.MessageDialog(None, msg, "InVesalius 3 - Neuronavigator",
  951 + wx.ICON_INFORMATION | wx.OK)
  952 + dlg.ShowModal()
  953 + dlg.Destroy()
808 954  
809 955 def EnterMarkerID(default):
810 956 msg = _("Edit marker ID")
... ... @@ -2967,3 +3113,246 @@ class FillHolesAutoDialog(wx.Dialog):
2967 3113 else:
2968 3114 self.panel3dcon.Enable(1)
2969 3115 self.panel2dcon.Enable(0)
  3116 +
  3117 +
  3118 +class ObjectCalibrationDialog(wx.Dialog):
  3119 +
  3120 + def __init__(self, nav_prop):
  3121 +
  3122 + self.tracker_id = nav_prop[0]
  3123 + self.trk_init = nav_prop[1]
  3124 + self.obj_ref_id = 0
  3125 + self.obj_name = None
  3126 +
  3127 + self.obj_fiducials = np.full([5, 3], np.nan)
  3128 + self.obj_orients = np.full([5, 3], np.nan)
  3129 +
  3130 + pre = wx.PreDialog()
  3131 + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Object calibration"), size=(450, 440),
  3132 + style=wx.DEFAULT_DIALOG_STYLE | wx.FRAME_FLOAT_ON_PARENT)
  3133 + self.PostCreate(pre)
  3134 +
  3135 + self._init_gui()
  3136 + self.LoadObject()
  3137 +
  3138 + def _init_gui(self):
  3139 + self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize())
  3140 + self.interactor.Enable(1)
  3141 + self.ren = vtk.vtkRenderer()
  3142 + self.interactor.GetRenderWindow().AddRenderer(self.ren)
  3143 +
  3144 + # Initialize list of buttons and txtctrls for wx objects
  3145 + self.btns_coord = [None] * 5
  3146 + self.text_actors = [None] * 5
  3147 + self.ball_actors = [None] * 5
  3148 + self.txt_coord = [list(), list(), list(), list(), list()]
  3149 +
  3150 + # ComboBox for tracker reference mode
  3151 + tooltip = wx.ToolTip(_(u"Choose the object reference mode"))
  3152 + choice_ref = wx.ComboBox(self, -1, "", size=wx.Size(90, 23),
  3153 + choices=const.REF_MODE, style=wx.CB_DROPDOWN | wx.CB_READONLY)
  3154 + choice_ref.SetSelection(self.obj_ref_id)
  3155 + choice_ref.SetToolTip(tooltip)
  3156 + choice_ref.Bind(wx.EVT_COMBOBOX, self.OnChoiceRefMode)
  3157 + choice_ref.Enable(0)
  3158 +
  3159 + # Buttons to finish or cancel object registration
  3160 + tooltip = wx.ToolTip(_(u"Registration done"))
  3161 + # btn_ok = wx.Button(self, -1, _(u"Done"), size=wx.Size(90, 30))
  3162 + btn_ok = wx.Button(self, wx.ID_OK, _(u"Done"), size=wx.Size(90, 30))
  3163 + btn_ok.SetToolTip(tooltip)
  3164 +
  3165 + extra_sizer = wx.FlexGridSizer(rows=2, cols=1, hgap=5, vgap=30)
  3166 + extra_sizer.AddMany([choice_ref,
  3167 + btn_ok])
  3168 +
  3169 + # Push buttons for object fiducials
  3170 + btns_obj = const.BTNS_OBJ
  3171 + tips_obj = const.TIPS_OBJ
  3172 +
  3173 + for k in btns_obj:
  3174 + n = btns_obj[k].keys()[0]
  3175 + lab = btns_obj[k].values()[0]
  3176 + self.btns_coord[n] = wx.Button(self, k, label=lab, size=wx.Size(60, 23))
  3177 + self.btns_coord[n].SetToolTip(wx.ToolTip(tips_obj[n]))
  3178 + self.btns_coord[n].Bind(wx.EVT_BUTTON, self.OnGetObjectFiducials)
  3179 +
  3180 + for m in range(0, 5):
  3181 + for n in range(0, 3):
  3182 + self.txt_coord[m].append(wx.StaticText(self, -1, label='-',
  3183 + style=wx.ALIGN_RIGHT, size=wx.Size(40, 23)))
  3184 +
  3185 + coord_sizer = wx.GridBagSizer(hgap=20, vgap=5)
  3186 +
  3187 + for m in range(0, 5):
  3188 + coord_sizer.Add(self.btns_coord[m], pos=wx.GBPosition(m, 0))
  3189 + for n in range(0, 3):
  3190 + coord_sizer.Add(self.txt_coord[m][n], pos=wx.GBPosition(m, n + 1), flag=wx.TOP, border=5)
  3191 +
  3192 + group_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=50, vgap=5)
  3193 + group_sizer.AddMany([(coord_sizer, 0, wx.LEFT, 20),
  3194 + (extra_sizer, 0, wx.LEFT, 10)])
  3195 +
  3196 + main_sizer = wx.BoxSizer(wx.VERTICAL)
  3197 + main_sizer.Add(self.interactor, 0, wx.EXPAND)
  3198 + main_sizer.Add(group_sizer, 0,
  3199 + wx.EXPAND|wx.GROW|wx.LEFT|wx.TOP|wx.RIGHT|wx.BOTTOM|wx.ALIGN_CENTER_HORIZONTAL, 10)
  3200 +
  3201 + self.SetSizer(main_sizer)
  3202 + main_sizer.Fit(self)
  3203 +
  3204 + def ObjectImportDialog(self):
  3205 + msg = _("Would like to use InVesalius default object?")
  3206 + if sys.platform == 'darwin':
  3207 + dlg = wx.MessageDialog(None, "", msg,
  3208 + wx.ICON_QUESTION | wx.YES_NO)
  3209 + else:
  3210 + dlg = wx.MessageDialog(None, msg,
  3211 + "InVesalius 3",
  3212 + wx.ICON_QUESTION | wx.YES_NO)
  3213 + answer = dlg.ShowModal()
  3214 + dlg.Destroy()
  3215 +
  3216 + if answer == wx.ID_YES:
  3217 + return 1
  3218 + else: # answer == wx.ID_NO:
  3219 + return 0
  3220 +
  3221 + def LoadObject(self):
  3222 + default = self.ObjectImportDialog()
  3223 + if not default:
  3224 + filename = ShowImportMeshFilesDialog()
  3225 +
  3226 + if filename:
  3227 + if filename.lower().endswith('.stl'):
  3228 + reader = vtk.vtkSTLReader()
  3229 + elif filename.lower().endswith('.ply'):
  3230 + reader = vtk.vtkPLYReader()
  3231 + elif filename.lower().endswith('.obj'):
  3232 + reader = vtk.vtkOBJReader()
  3233 + elif filename.lower().endswith('.vtp'):
  3234 + reader = vtk.vtkXMLPolyDataReader()
  3235 + else:
  3236 + wx.MessageBox(_("File format not reconized by InVesalius"), _("Import surface error"))
  3237 + return
  3238 + else:
  3239 + filename = os.path.join(const.OBJ_DIR, "magstim_fig8_coil.stl")
  3240 + reader = vtk.vtkSTLReader()
  3241 + else:
  3242 + filename = os.path.join(const.OBJ_DIR, "magstim_fig8_coil.stl")
  3243 + reader = vtk.vtkSTLReader()
  3244 +
  3245 + if _has_win32api:
  3246 + self.obj_name = win32api.GetShortPathName(filename).encode(const.FS_ENCODE)
  3247 + else:
  3248 + self.obj_name = filename.encode(const.FS_ENCODE)
  3249 +
  3250 + reader.SetFileName(self.obj_name)
  3251 + reader.Update()
  3252 + polydata = reader.GetOutput()
  3253 +
  3254 + if polydata.GetNumberOfPoints() == 0:
  3255 + wx.MessageBox(_("InVesalius was not able to import this surface"), _("Import surface error"))
  3256 +
  3257 + transform = vtk.vtkTransform()
  3258 + transform.RotateZ(90)
  3259 +
  3260 + transform_filt = vtk.vtkTransformPolyDataFilter()
  3261 + transform_filt.SetTransform(transform)
  3262 + transform_filt.SetInputData(polydata)
  3263 + transform_filt.Update()
  3264 +
  3265 + normals = vtk.vtkPolyDataNormals()
  3266 + normals.SetInputData(transform_filt.GetOutput())
  3267 + normals.SetFeatureAngle(80)
  3268 + normals.AutoOrientNormalsOn()
  3269 + normals.Update()
  3270 +
  3271 + mapper = vtk.vtkPolyDataMapper()
  3272 + mapper.SetInputData(normals.GetOutput())
  3273 + mapper.ScalarVisibilityOff()
  3274 + mapper.ImmediateModeRenderingOn()
  3275 +
  3276 + obj_actor = vtk.vtkActor()
  3277 + obj_actor.SetMapper(mapper)
  3278 +
  3279 + self.ball_actors[0], self.text_actors[0] = self.OnCreateObjectText('Left', (0,55,0))
  3280 + self.ball_actors[1], self.text_actors[1] = self.OnCreateObjectText('Right', (0,-55,0))
  3281 + self.ball_actors[2], self.text_actors[2] = self.OnCreateObjectText('Anterior', (23,0,0))
  3282 +
  3283 + self.ren.AddActor(obj_actor)
  3284 + self.ren.ResetCamera()
  3285 +
  3286 + self.interactor.Render()
  3287 +
  3288 + def OnCreateObjectText(self, name, coord):
  3289 + ball_source = vtk.vtkSphereSource()
  3290 + ball_source.SetRadius(3)
  3291 + mapper = vtk.vtkPolyDataMapper()
  3292 + mapper.SetInputConnection(ball_source.GetOutputPort())
  3293 + ball_actor = vtk.vtkActor()
  3294 + ball_actor.SetMapper(mapper)
  3295 + ball_actor.SetPosition(coord)
  3296 + ball_actor.GetProperty().SetColor(1, 0, 0)
  3297 +
  3298 + textSource = vtk.vtkVectorText()
  3299 + textSource.SetText(name)
  3300 +
  3301 + mapper = vtk.vtkPolyDataMapper()
  3302 + mapper.SetInputConnection(textSource.GetOutputPort())
  3303 + tactor = vtk.vtkFollower()
  3304 + tactor.SetMapper(mapper)
  3305 + tactor.GetProperty().SetColor(1.0, 0.0, 0.0)
  3306 + tactor.SetScale(5)
  3307 + ball_position = ball_actor.GetPosition()
  3308 + tactor.SetPosition(ball_position[0]+5, ball_position[1]+5, ball_position[2]+10)
  3309 + self.ren.AddActor(tactor)
  3310 + tactor.SetCamera(self.ren.GetActiveCamera())
  3311 + self.ren.AddActor(ball_actor)
  3312 + return ball_actor, tactor
  3313 +
  3314 + def OnGetObjectFiducials(self, evt):
  3315 + btn_id = const.BTNS_OBJ[evt.GetId()].keys()[0]
  3316 +
  3317 + if self.trk_init and self.tracker_id:
  3318 + coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, self.obj_ref_id)
  3319 + if self.obj_ref_id and btn_id == 4:
  3320 + coord = coord_raw[2, :]
  3321 + else:
  3322 + coord = coord_raw[0, :]
  3323 + else:
  3324 + NavigationTrackerWarning(0, 'choose')
  3325 +
  3326 + # Update text controls with tracker coordinates
  3327 + if coord is not None or np.sum(coord) != 0.0:
  3328 + self.obj_fiducials[btn_id, :] = coord[:3]
  3329 + self.obj_orients[btn_id, :] = coord[3:]
  3330 + for n in [0, 1, 2]:
  3331 + self.txt_coord[btn_id][n].SetLabel(str(round(coord[n], 1)))
  3332 + if self.text_actors[btn_id]:
  3333 + self.text_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0)
  3334 + self.ball_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0)
  3335 + self.Refresh()
  3336 + else:
  3337 + NavigationTrackerWarning(0, 'choose')
  3338 +
  3339 + def OnChoiceRefMode(self, evt):
  3340 + # When ref mode is changed the tracker coordinates are set to nan
  3341 + # This is for Polhemus FASTRAK wrapper, where the sensor attached to the object can be the stylus (Static
  3342 + # reference - Selection 0 - index 0 for coordinates) or can be a 3rd sensor (Dynamic reference - Selection 1 -
  3343 + # index 2 for coordinates)
  3344 + # I use the index 2 directly here to send to the coregistration module where it is possible to access without
  3345 + # any conditional statement the correct index of coordinates.
  3346 +
  3347 + if evt.GetSelection():
  3348 + self.obj_ref_id = 2
  3349 + else:
  3350 + self.obj_ref_id = 0
  3351 + for m in range(0, 5):
  3352 + self.obj_fiducials[m, :] = np.full([1, 3], np.nan)
  3353 + self.obj_orients[m, :] = np.full([1, 3], np.nan)
  3354 + for n in range(0, 3):
  3355 + self.txt_coord[m][n].SetLabel('-')
  3356 +
  3357 + def GetValue(self):
  3358 + return self.obj_fiducials, self.obj_orients, self.obj_ref_id, self.obj_name
... ...
invesalius/gui/frame.py
... ... @@ -707,7 +707,7 @@ class MenuBar(wx.MenuBar):
707 707 sub(self.OnEnableState, "Enable state project")
708 708 sub(self.OnEnableUndo, "Enable undo")
709 709 sub(self.OnEnableRedo, "Enable redo")
710   - sub(self.OnEnableNavigation, "Navigation Status")
  710 + sub(self.OnEnableNavigation, "Navigation status")
711 711  
712 712 sub(self.OnAddMask, "Add mask")
713 713 sub(self.OnRemoveMasks, "Remove masks")
... ...
invesalius/gui/task_navigator.py
... ... @@ -19,6 +19,7 @@
19 19  
20 20 from functools import partial
21 21 import sys
  22 +import os
22 23  
23 24 import numpy as np
24 25 import wx
... ... @@ -27,15 +28,25 @@ import wx.lib.masked.numctrl
27 28 import wx.lib.foldpanelbar as fpb
28 29 from wx.lib.pubsub import pub as Publisher
29 30 import wx.lib.colourselect as csel
  31 +import wx.lib.platebtn as pbtn
30 32  
  33 +from math import cos, sin, pi
  34 +from time import sleep
  35 +
  36 +import invesalius.data.transformations as tr
31 37 import invesalius.constants as const
32 38 import invesalius.data.bases as db
33 39 import invesalius.data.coordinates as dco
34 40 import invesalius.data.coregistration as dcr
35 41 import invesalius.data.trackers as dt
36 42 import invesalius.data.trigger as trig
  43 +import invesalius.data.record_coords as rec
37 44 import invesalius.gui.dialogs as dlg
38 45  
  46 +BTN_NEW = wx.NewId()
  47 +BTN_IMPORT_LOCAL = wx.NewId()
  48 +
  49 +
39 50 class TaskPanel(wx.Panel):
40 51 def __init__(self, parent):
41 52 wx.Panel.__init__(self, parent)
... ... @@ -71,7 +82,6 @@ class InnerTaskPanel(wx.Panel):
71 82 fold_panel = FoldPanel(self)
72 83 fold_panel.SetBackgroundColour(default_colour)
73 84  
74   -
75 85 # Add line sizer into main sizer
76 86 main_sizer = wx.BoxSizer(wx.VERTICAL)
77 87 main_sizer.Add(txt_sizer, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT, 5)
... ... @@ -120,7 +130,7 @@ class InnerFoldPanel(wx.Panel):
120 130 (10, 350), 0, fpb.FPB_SINGLE_FOLD)
121 131 else:
122 132 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
123   - (10, 293), 0, fpb.FPB_SINGLE_FOLD)
  133 + (10, 320), 0, fpb.FPB_SINGLE_FOLD)
124 134 # Fold panel style
125 135 style = fpb.CaptionBarStyle()
126 136 style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V)
... ... @@ -132,11 +142,19 @@ class InnerFoldPanel(wx.Panel):
132 142 ntw = NeuronavigationPanel(item)
133 143  
134 144 fold_panel.ApplyCaptionStyle(item, style)
135   - fold_panel.AddFoldPanelWindow(item, ntw, spacing= 0,
  145 + fold_panel.AddFoldPanelWindow(item, ntw, spacing=0,
136 146 leftSpacing=0, rightSpacing=0)
137 147 fold_panel.Expand(fold_panel.GetFoldPanel(0))
138 148  
139   - # Fold 2 - Markers panel
  149 + # Fold 2 - Object registration panel
  150 + item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True)
  151 + otw = ObjectRegistrationPanel(item)
  152 +
  153 + fold_panel.ApplyCaptionStyle(item, style)
  154 + fold_panel.AddFoldPanelWindow(item, otw, spacing=0,
  155 + leftSpacing=0, rightSpacing=0)
  156 +
  157 + # Fold 3 - Markers panel
140 158 item = fold_panel.AddFoldPanel(_("Extra tools"), collapsed=True)
141 159 mtw = MarkersPanel(item)
142 160  
... ... @@ -147,26 +165,38 @@ class InnerFoldPanel(wx.Panel):
147 165  
148 166 # Check box for camera update in volume rendering during navigation
149 167 tooltip = wx.ToolTip(_("Update camera in volume"))
150   - checkcamera = wx.CheckBox(self, -1, _('Volume camera'))
  168 + checkcamera = wx.CheckBox(self, -1, _('Vol. camera'))
151 169 checkcamera.SetToolTip(tooltip)
152   - checkcamera.SetValue(True)
153   - checkcamera.Bind(wx.EVT_CHECKBOX, partial(self.UpdateVolumeCamera, ctrl=checkcamera))
  170 + checkcamera.SetValue(const.CAM_MODE)
  171 + checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCamera)
  172 + self.checkcamera = checkcamera
154 173  
155   - # Check box for camera update in volume rendering during navigation
  174 + # Check box for trigger monitoring to create markers from serial port
156 175 tooltip = wx.ToolTip(_("Enable external trigger for creating markers"))
157   - checktrigger = wx.CheckBox(self, -1, _('External trigger'))
  176 + checktrigger = wx.CheckBox(self, -1, _('Ext. trigger'))
158 177 checktrigger.SetToolTip(tooltip)
159 178 checktrigger.SetValue(False)
160   - checktrigger.Bind(wx.EVT_CHECKBOX, partial(self.UpdateExternalTrigger, ctrl=checktrigger))
  179 + checktrigger.Bind(wx.EVT_CHECKBOX, partial(self.OnExternalTrigger, ctrl=checktrigger))
161 180 self.checktrigger = checktrigger
162 181  
  182 + # Check box for object position and orientation update in volume rendering during navigation
  183 + tooltip = wx.ToolTip(_("Show and track TMS coil"))
  184 + checkobj = wx.CheckBox(self, -1, _('Show coil'))
  185 + checkobj.SetToolTip(tooltip)
  186 + checkobj.SetValue(False)
  187 + checkobj.Disable()
  188 + checkobj.Bind(wx.EVT_CHECKBOX, self.OnShowObject)
  189 + self.checkobj = checkobj
  190 +
163 191 if sys.platform != 'win32':
164   - checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
  192 + self.checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
165 193 checktrigger.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
  194 + checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
166 195  
167 196 line_sizer = wx.BoxSizer(wx.HORIZONTAL)
168 197 line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5)
169   - line_sizer.Add(checktrigger, 1,wx.ALIGN_RIGHT | wx.RIGHT | wx.LEFT, 5)
  198 + line_sizer.Add(checktrigger, 0, wx.ALIGN_CENTER)
  199 + line_sizer.Add(checkobj, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.LEFT, 5)
170 200 line_sizer.Fit(self)
171 201  
172 202 # Panel sizer to expand fold panel
... ... @@ -175,27 +205,49 @@ class InnerFoldPanel(wx.Panel):
175 205 sizer.Add(line_sizer, 1, wx.GROW | wx.EXPAND)
176 206 sizer.Fit(self)
177 207  
  208 + self.track_obj = False
  209 +
178 210 self.SetSizer(sizer)
179 211 self.Update()
180 212 self.SetAutoLayout(1)
181 213  
182 214 def __bind_events(self):
183   - Publisher.subscribe(self.OnTrigger, 'Navigation Status')
  215 + Publisher.subscribe(self.OnCheckStatus, 'Navigation status')
  216 + Publisher.subscribe(self.OnShowObject, 'Update track object state')
  217 + Publisher.subscribe(self.OnVolumeCamera, 'Target navigation mode')
184 218  
185   - def OnTrigger(self, pubsub_evt):
  219 + def OnCheckStatus(self, pubsub_evt):
186 220 status = pubsub_evt.data
187 221 if status:
188 222 self.checktrigger.Enable(False)
  223 + self.checkobj.Enable(False)
189 224 else:
190 225 self.checktrigger.Enable(True)
  226 + if self.track_obj:
  227 + self.checkobj.Enable(True)
191 228  
192   - def UpdateExternalTrigger(self, evt, ctrl):
  229 + def OnExternalTrigger(self, evt, ctrl):
193 230 Publisher.sendMessage('Update trigger state', ctrl.GetValue())
194 231  
195   - def UpdateVolumeCamera(self, evt, ctrl):
196   - Publisher.sendMessage('Update volume camera state', ctrl.GetValue())
  232 + def OnShowObject(self, evt):
  233 + if hasattr(evt, 'data'):
  234 + if evt.data[0]:
  235 + self.checkobj.Enable(True)
  236 + self.track_obj = True
  237 + Publisher.sendMessage('Status target button', True)
  238 + else:
  239 + self.checkobj.Enable(False)
  240 + self.checkobj.SetValue(False)
  241 + self.track_obj = False
  242 + Publisher.sendMessage('Status target button', False)
197 243  
  244 + Publisher.sendMessage('Update show object state', self.checkobj.GetValue())
198 245  
  246 + def OnVolumeCamera(self, evt):
  247 + if hasattr(evt, 'data'):
  248 + if evt.data is True:
  249 + self.checkcamera.SetValue(0)
  250 + Publisher.sendMessage('Update volume camera state', self.checkcamera.GetValue())
199 251  
200 252  
201 253 class NeuronavigationPanel(wx.Panel):
... ... @@ -215,6 +267,9 @@ class NeuronavigationPanel(wx.Panel):
215 267 self.trk_init = None
216 268 self.trigger = None
217 269 self.trigger_state = False
  270 + self.obj_reg = None
  271 + self.obj_reg_status = False
  272 + self.track_obj = False
218 273  
219 274 self.tracker_id = const.DEFAULT_TRACKER
220 275 self.ref_mode_id = const.DEFAULT_REF_MODE
... ... @@ -230,6 +285,7 @@ class NeuronavigationPanel(wx.Panel):
230 285 choice_trck.SetToolTip(tooltip)
231 286 choice_trck.SetSelection(const.DEFAULT_TRACKER)
232 287 choice_trck.Bind(wx.EVT_COMBOBOX, partial(self.OnChoiceTracker, ctrl=choice_trck))
  288 + self.choice_trck = choice_trck
233 289  
234 290 # ComboBox for tracker reference mode
235 291 tooltip = wx.ToolTip(_("Choose the navigation reference mode"))
... ... @@ -259,7 +315,7 @@ class NeuronavigationPanel(wx.Panel):
259 315 lab = btns_trk[k].values()[0]
260 316 self.btns_coord[n] = wx.Button(self, k, label=lab, size=wx.Size(45, 23))
261 317 self.btns_coord[n].SetToolTip(wx.ToolTip(tips_trk[n-3]))
262   - # Excepetion for event of button that set image coordinates
  318 + # Exception for event of button that set image coordinates
263 319 if n == 6:
264 320 self.btns_coord[n].Bind(wx.EVT_BUTTON, self.OnSetImageCoordinates)
265 321 else:
... ... @@ -275,12 +331,13 @@ class NeuronavigationPanel(wx.Panel):
275 331 txtctrl_fre.SetBackgroundColour('WHITE')
276 332 txtctrl_fre.SetEditable(0)
277 333 txtctrl_fre.SetToolTip(tooltip)
  334 + self.txtctrl_fre = txtctrl_fre
278 335  
279 336 # Toggle button for neuronavigation
280 337 tooltip = wx.ToolTip(_("Start navigation"))
281 338 btn_nav = wx.ToggleButton(self, -1, _("Navigate"), size=wx.Size(80, -1))
282 339 btn_nav.SetToolTip(tooltip)
283   - btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn=(btn_nav, choice_trck, choice_ref, txtctrl_fre)))
  340 + btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn=(btn_nav, choice_trck, choice_ref)))
284 341  
285 342 # Image and tracker coordinates number controls
286 343 for m in range(0, 7):
... ... @@ -288,7 +345,7 @@ class NeuronavigationPanel(wx.Panel):
288 345 self.numctrls_coord[m].append(
289 346 wx.lib.masked.numctrl.NumCtrl(parent=self, integerWidth=4, fractionWidth=1))
290 347  
291   - # Sizers to group all GUI objects
  348 + # Sizer to group all GUI objects
292 349 choice_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5)
293 350 choice_sizer.AddMany([(choice_trck, wx.LEFT),
294 351 (choice_ref, wx.RIGHT)])
... ... @@ -326,8 +383,11 @@ class NeuronavigationPanel(wx.Panel):
326 383 def __bind_events(self):
327 384 Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials')
328 385 Publisher.subscribe(self.UpdateTriggerState, 'Update trigger state')
  386 + Publisher.subscribe(self.UpdateTrackObjectState, 'Update track object state')
329 387 Publisher.subscribe(self.UpdateImageCoordinates, 'Set ball reference position')
330 388 Publisher.subscribe(self.OnDisconnectTracker, 'Disconnect tracker')
  389 + Publisher.subscribe(self.UpdateObjectRegistration, 'Update object registration')
  390 + Publisher.subscribe(self.OnCloseProject, 'Close project data')
331 391  
332 392 def LoadImageFiducials(self, pubsub_evt):
333 393 marker_id = pubsub_evt.data[0]
... ... @@ -347,26 +407,40 @@ class NeuronavigationPanel(wx.Panel):
347 407 for m in [0, 1, 2, 6]:
348 408 if m == 6 and self.btns_coord[m].IsEnabled():
349 409 for n in [0, 1, 2]:
350   - self.numctrls_coord[m][n].SetValue(self.current_coord[n])
  410 + self.numctrls_coord[m][n].SetValue(float(self.current_coord[n]))
351 411 elif m != 6 and not self.btns_coord[m].GetValue():
352 412 # btn_state = self.btns_coord[m].GetValue()
353 413 # if not btn_state:
354 414 for n in [0, 1, 2]:
355   - self.numctrls_coord[m][n].SetValue(self.current_coord[n])
  415 + self.numctrls_coord[m][n].SetValue(float(self.current_coord[n]))
  416 +
  417 + def UpdateObjectRegistration(self, pubsub_evt):
  418 + if pubsub_evt.data:
  419 + self.obj_reg = pubsub_evt.data
  420 + self.obj_reg_status = True
  421 + else:
  422 + self.obj_reg = None
  423 + self.obj_reg_status = False
356 424  
357   - def UpdateTriggerState (self, pubsub_evt):
  425 + def UpdateTrackObjectState(self, pubsub_evt):
  426 + if pubsub_evt.data[0]:
  427 + self.track_obj = pubsub_evt.data[0]
  428 + else:
  429 + self.track_obj = False
  430 +
  431 + def UpdateTriggerState(self, pubsub_evt):
358 432 self.trigger_state = pubsub_evt.data
359 433  
360 434 def OnDisconnectTracker(self, pubsub_evt):
361 435 if self.tracker_id:
362   - dt.TrackerConnection(self.tracker_id, 'disconnect')
  436 + dt.TrackerConnection(self.tracker_id, self.trk_init[0], 'disconnect')
363 437  
364 438 def OnChoiceTracker(self, evt, ctrl):
365 439 Publisher.sendMessage('Update status text in GUI', _("Configuring tracker ..."))
366   - if evt:
  440 + if hasattr(evt, 'GetSelection'):
367 441 choice = evt.GetSelection()
368 442 else:
369   - choice = self.tracker_id
  443 + choice = 6
370 444  
371 445 if self.trk_init:
372 446 trck = self.trk_init[0]
... ... @@ -377,26 +451,35 @@ class NeuronavigationPanel(wx.Panel):
377 451 # has been initialized before
378 452 if trck and choice != 6:
379 453 self.ResetTrackerFiducials()
380   - self.trk_init = dt.TrackerConnection(self.tracker_id, 'disconnect')
  454 + Publisher.sendMessage('Update status text in GUI', _("Disconnecting tracker..."))
  455 + Publisher.sendMessage('Remove sensors ID')
  456 + self.trk_init = dt.TrackerConnection(self.tracker_id, trck, 'disconnect')
381 457 self.tracker_id = choice
382   - if not self.trk_init[0]:
383   - self.trk_init = dt.TrackerConnection(self.tracker_id, 'connect')
  458 + if not self.trk_init[0] and choice:
  459 + Publisher.sendMessage('Update status text in GUI', _("Tracker disconnected successfully"))
  460 + self.trk_init = dt.TrackerConnection(self.tracker_id, None, 'connect')
384 461 if not self.trk_init[0]:
385 462 dlg.NavigationTrackerWarning(self.tracker_id, self.trk_init[1])
386 463 ctrl.SetSelection(0)
387 464 print "Tracker not connected!"
388 465 else:
  466 + Publisher.sendMessage('Update status text in GUI', _("Ready"))
389 467 ctrl.SetSelection(self.tracker_id)
390 468 print "Tracker connected!"
391 469 elif choice == 6:
392 470 if trck:
393   - self.trk_init = dt.TrackerConnection(self.tracker_id, 'disconnect')
  471 + Publisher.sendMessage('Update status text in GUI', _("Disconnecting tracker ..."))
  472 + Publisher.sendMessage('Remove sensors ID')
  473 + self.trk_init = dt.TrackerConnection(self.tracker_id, trck, 'disconnect')
394 474 if not self.trk_init[0]:
395   - dlg.NavigationTrackerWarning(self.tracker_id, 'disconnect')
  475 + if evt is not False:
  476 + dlg.NavigationTrackerWarning(self.tracker_id, 'disconnect')
396 477 self.tracker_id = 0
397 478 ctrl.SetSelection(self.tracker_id)
  479 + Publisher.sendMessage('Update status text in GUI', _("Tracker disconnected"))
398 480 print "Tracker disconnected!"
399 481 else:
  482 + Publisher.sendMessage('Update status text in GUI', _("Tracker still connected"))
400 483 print "Tracker still connected!"
401 484 else:
402 485 ctrl.SetSelection(self.tracker_id)
... ... @@ -405,34 +488,38 @@ class NeuronavigationPanel(wx.Panel):
405 488 # If trk_init is None try to connect. If doesn't succeed show dialog.
406 489 if choice:
407 490 self.tracker_id = choice
408   - self.trk_init = dt.TrackerConnection(self.tracker_id, 'connect')
  491 + self.trk_init = dt.TrackerConnection(self.tracker_id, None, 'connect')
409 492 if not self.trk_init[0]:
410 493 dlg.NavigationTrackerWarning(self.tracker_id, self.trk_init[1])
411 494 self.tracker_id = 0
412 495 ctrl.SetSelection(self.tracker_id)
413   - Publisher.sendMessage('Update status text in GUI', _("Ready"))
  496 + else:
  497 + Publisher.sendMessage('Update status text in GUI', _("Ready"))
  498 +
  499 + Publisher.sendMessage('Update tracker initializer', (self.tracker_id, self.trk_init, self.ref_mode_id))
414 500  
415 501 def OnChoiceRefMode(self, evt, ctrl):
416   - # When ref mode is changed the tracker coords are set to zero
  502 + # When ref mode is changed the tracker coordinates are set to zero
417 503 self.ref_mode_id = evt.GetSelection()
418 504 self.ResetTrackerFiducials()
419 505 # Some trackers do not accept restarting within this time window
420 506 # TODO: Improve the restarting of trackers after changing reference mode
421 507 # self.OnChoiceTracker(None, ctrl)
  508 + Publisher.sendMessage('Update tracker initializer', (self.tracker_id, self.trk_init, self.ref_mode_id))
422 509 print "Reference mode changed!"
423 510  
424 511 def OnSetImageCoordinates(self, evt):
425 512 # FIXME: Cross does not update in last clicked slice, only on the other two
426 513 btn_id = const.BTNS_TRK[evt.GetId()].keys()[0]
427 514  
428   - wx, wy, wz = self.numctrls_coord[btn_id][0].GetValue(), \
429   - self.numctrls_coord[btn_id][1].GetValue(), \
  515 + ux, uy, uz = self.numctrls_coord[btn_id][0].GetValue(),\
  516 + self.numctrls_coord[btn_id][1].GetValue(),\
430 517 self.numctrls_coord[btn_id][2].GetValue()
431 518  
432   - Publisher.sendMessage('Set ball reference position', (wx, wy, wz))
433   - Publisher.sendMessage('Set camera in volume', (wx, wy, wz))
434   - Publisher.sendMessage('Co-registered points', (wx, wy, wz))
435   - Publisher.sendMessage('Update cross position', (wx, wy, wz))
  519 + Publisher.sendMessage('Set ball reference position', (ux, uy, uz))
  520 + # Publisher.sendMessage('Set camera in volume', (ux, uy, uz))
  521 + Publisher.sendMessage('Co-registered points', (ux, uy, uz, 0., 0., 0.))
  522 + Publisher.sendMessage('Update cross position', (ux, uy, uz))
436 523  
437 524 def OnImageFiducials(self, evt):
438 525 btn_id = const.BTNS_IMG_MKS[evt.GetId()].keys()[0]
... ... @@ -441,13 +528,13 @@ class NeuronavigationPanel(wx.Panel):
441 528 if self.btns_coord[btn_id].GetValue():
442 529 coord = self.numctrls_coord[btn_id][0].GetValue(),\
443 530 self.numctrls_coord[btn_id][1].GetValue(),\
444   - self.numctrls_coord[btn_id][2].GetValue()
  531 + self.numctrls_coord[btn_id][2].GetValue(), 0, 0, 0
445 532  
446 533 self.fiducials[btn_id, :] = coord[0:3]
447 534 Publisher.sendMessage('Create marker', (coord, marker_id))
448 535 else:
449 536 for n in [0, 1, 2]:
450   - self.numctrls_coord[btn_id][n].SetValue(self.current_coord[n])
  537 + self.numctrls_coord[btn_id][n].SetValue(float(self.current_coord[n]))
451 538  
452 539 self.fiducials[btn_id, :] = np.nan
453 540 Publisher.sendMessage('Delete fiducial marker', marker_id)
... ... @@ -457,7 +544,13 @@ class NeuronavigationPanel(wx.Panel):
457 544 coord = None
458 545  
459 546 if self.trk_init and self.tracker_id:
460   - coord = dco.GetCoordinates(self.trk_init, self.tracker_id, self.ref_mode_id)
  547 + coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, self.ref_mode_id)
  548 + if self.ref_mode_id:
  549 + coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :])
  550 + else:
  551 + coord = coord_raw[0, :]
  552 + coord[2] = -coord[2]
  553 +
461 554 else:
462 555 dlg.NavigationTrackerWarning(0, 'choose')
463 556  
... ... @@ -471,7 +564,6 @@ class NeuronavigationPanel(wx.Panel):
471 564 btn_nav = btn[0]
472 565 choice_trck = btn[1]
473 566 choice_ref = btn[2]
474   - txtctrl_fre = btn[3]
475 567  
476 568 nav_id = btn_nav.GetValue()
477 569 if nav_id:
... ... @@ -479,6 +571,9 @@ class NeuronavigationPanel(wx.Panel):
479 571 dlg.InvalidFiducials()
480 572 btn_nav.SetValue(False)
481 573  
  574 + elif not self.trk_init[0]:
  575 + dlg.NavigationTrackerWarning(0, 'choose')
  576 +
482 577 else:
483 578 tooltip = wx.ToolTip(_("Stop neuronavigation"))
484 579 btn_nav.SetToolTip(tooltip)
... ... @@ -489,28 +584,75 @@ class NeuronavigationPanel(wx.Panel):
489 584 for btn_c in self.btns_coord:
490 585 btn_c.Enable(False)
491 586  
492   - m, q1, minv = db.base_creation(self.fiducials[0:3, :])
493   - n, q2, ninv = db.base_creation(self.fiducials[3::, :])
  587 + # fids_head_img = np.zeros([3, 3])
  588 + # for ic in range(0, 3):
  589 + # fids_head_img[ic, :] = np.asarray(db.flip_x_m(self.fiducials[ic, :]))
  590 + #
  591 + # m_head_aux, q_head_aux, m_inv_head_aux = db.base_creation(fids_head_img)
  592 + # m_head = np.asmatrix(np.identity(4))
  593 + # m_head[:3, :3] = m_head_aux[:3, :3]
  594 +
  595 + m, q1, minv = db.base_creation_old(self.fiducials[:3, :])
  596 + n, q2, ninv = db.base_creation_old(self.fiducials[3:, :])
  597 +
  598 + m_change = tr.affine_matrix_from_points(self.fiducials[3:, :].T, self.fiducials[:3, :].T,
  599 + shear=False, scale=False)
  600 +
  601 + # coreg_data = [m_change, m_head]
494 602  
495 603 tracker_mode = self.trk_init, self.tracker_id, self.ref_mode_id
496 604 # FIXME: FRE is taking long to calculate so it updates on GUI delayed to navigation - I think its fixed
497 605 # TODO: Exhibit FRE in a warning dialog and only starts navigation after user clicks ok
498 606 fre = db.calculate_fre(self.fiducials, minv, n, q1, q2)
499 607  
500   - txtctrl_fre.SetValue(str(round(fre, 2)))
  608 + self.txtctrl_fre.SetValue(str(round(fre, 2)))
501 609 if fre <= 3:
502   - txtctrl_fre.SetBackgroundColour('GREEN')
  610 + self.txtctrl_fre.SetBackgroundColour('GREEN')
503 611 else:
504   - txtctrl_fre.SetBackgroundColour('RED')
  612 + self.txtctrl_fre.SetBackgroundColour('RED')
505 613  
506 614 if self.trigger_state:
507 615 self.trigger = trig.Trigger(nav_id)
508 616  
509   - Publisher.sendMessage("Navigation Status", True)
  617 + Publisher.sendMessage("Navigation status", True)
510 618 Publisher.sendMessage("Toggle Cross", const.SLICE_STATE_CROSS)
511 619 Publisher.sendMessage("Hide current mask")
512 620  
513   - self.correg = dcr.Coregistration((minv, n, q1, q2), nav_id, tracker_mode)
  621 + if self.track_obj:
  622 + if self.obj_reg_status:
  623 + # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix
  624 + obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg
  625 +
  626 + if self.trk_init and self.tracker_id:
  627 +
  628 + coreg_data = [m_change, obj_ref_mode]
  629 +
  630 + if self.ref_mode_id:
  631 + coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, self.ref_mode_id)
  632 + obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)
  633 + coreg_data.extend(obj_data)
  634 +
  635 + self.correg = dcr.CoregistrationObjectDynamic(coreg_data, nav_id, tracker_mode)
  636 + else:
  637 + coord_raw = np.array([None])
  638 + obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)
  639 + coreg_data.extend(obj_data)
  640 +
  641 + self.correg = dcr.CoregistrationObjectStatic(coreg_data, nav_id, tracker_mode)
  642 +
  643 + else:
  644 + dlg.NavigationTrackerWarning(0, 'choose')
  645 +
  646 + else:
  647 + dlg.InvalidObjectRegistration()
  648 +
  649 + else:
  650 + coreg_data = [m_change, 0]
  651 + if self.ref_mode_id:
  652 + # self.correg = dcr.CoregistrationDynamic_old(bases_coreg, nav_id, tracker_mode)
  653 + self.correg = dcr.CoregistrationDynamic(coreg_data, nav_id, tracker_mode)
  654 + else:
  655 + self.correg = dcr.CoregistrationStatic(coreg_data, nav_id, tracker_mode)
514 656  
515 657 else:
516 658 tooltip = wx.ToolTip(_("Start neuronavigation"))
... ... @@ -527,13 +669,272 @@ class NeuronavigationPanel(wx.Panel):
527 669  
528 670 self.correg.stop()
529 671  
530   - Publisher.sendMessage("Navigation Status", False)
  672 + Publisher.sendMessage("Navigation status", False)
  673 +
  674 + def ResetImageFiducials(self):
  675 + for m in range(0, 3):
  676 + self.btns_coord[m].SetValue(False)
  677 + self.fiducials[m, :] = [np.nan, np.nan, np.nan]
  678 + for n in range(0, 3):
  679 + self.numctrls_coord[m][n].SetValue(0.0)
  680 +
  681 + for n in range(0, 3):
  682 + self.numctrls_coord[6][n].SetValue(0.0)
531 683  
532 684 def ResetTrackerFiducials(self):
533 685 for m in range(3, 6):
  686 + self.fiducials[m, :] = [np.nan, np.nan, np.nan]
534 687 for n in range(0, 3):
535 688 self.numctrls_coord[m][n].SetValue(0.0)
536 689  
  690 + self.txtctrl_fre.SetValue('')
  691 + self.txtctrl_fre.SetBackgroundColour('WHITE')
  692 +
  693 + def OnCloseProject(self, pubsub_evt):
  694 + self.ResetTrackerFiducials()
  695 + self.ResetImageFiducials()
  696 + self.OnChoiceTracker(False, self.choice_trck)
  697 + Publisher.sendMessage('Update object registration', False)
  698 + Publisher.sendMessage('Update track object state', (False, False))
  699 + Publisher.sendMessage('Delete all markers', 'close')
  700 + # TODO: Reset camera initial focus
  701 + Publisher.sendMessage('Reset cam clipping range')
  702 +
  703 +
  704 +class ObjectRegistrationPanel(wx.Panel):
  705 + def __init__(self, parent):
  706 + wx.Panel.__init__(self, parent)
  707 + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
  708 + self.SetBackgroundColour(default_colour)
  709 +
  710 + self.coil_list = const.COIL
  711 +
  712 + self.nav_prop = None
  713 + self.obj_fiducials = None
  714 + self.obj_orients = None
  715 + self.obj_ref_mode = None
  716 + self.obj_name = None
  717 + self.timestamp = const.TIMESTAMP
  718 +
  719 + self.SetAutoLayout(1)
  720 + self.__bind_events()
  721 +
  722 + # Button for creating new coil
  723 + tooltip = wx.ToolTip(_("Create new coil"))
  724 + btn_new = wx.Button(self, -1, _("New"), size=wx.Size(65, 23))
  725 + btn_new.SetToolTip(tooltip)
  726 + btn_new.Enable(1)
  727 + btn_new.Bind(wx.EVT_BUTTON, self.OnLinkCreate)
  728 + self.btn_new = btn_new
  729 +
  730 + # Button for import config coil file
  731 + tooltip = wx.ToolTip(_("Load coil configuration file"))
  732 + btn_load = wx.Button(self, -1, _("Load"), size=wx.Size(65, 23))
  733 + btn_load.SetToolTip(tooltip)
  734 + btn_load.Enable(1)
  735 + btn_load.Bind(wx.EVT_BUTTON, self.OnLinkLoad)
  736 + self.btn_load = btn_load
  737 +
  738 + # Save button for object registration
  739 + tooltip = wx.ToolTip(_(u"Save object registration file"))
  740 + btn_save = wx.Button(self, -1, _(u"Save"), size=wx.Size(65, 23))
  741 + btn_save.SetToolTip(tooltip)
  742 + btn_save.Enable(1)
  743 + btn_save.Bind(wx.EVT_BUTTON, self.ShowSaveObjectDialog)
  744 + self.btn_save = btn_save
  745 +
  746 + # Create a horizontal sizer to represent button save
  747 + line_save = wx.BoxSizer(wx.HORIZONTAL)
  748 + line_save.Add(btn_new, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4)
  749 + line_save.Add(btn_load, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4)
  750 + line_save.Add(btn_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4)
  751 +
  752 + # Change angles threshold
  753 + text_angles = wx.StaticText(self, -1, _("Angle threshold [degrees]:"))
  754 + spin_size_angles = wx.SpinCtrl(self, -1, "", size=wx.Size(50, 23))
  755 + spin_size_angles.SetRange(1, 99)
  756 + spin_size_angles.SetValue(const.COIL_ANGLES_THRESHOLD)
  757 + spin_size_angles.Bind(wx.EVT_TEXT, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles))
  758 + spin_size_angles.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles))
  759 +
  760 + # Change dist threshold
  761 + text_dist = wx.StaticText(self, -1, _("Distance threshold [mm]:"))
  762 + spin_size_dist = wx.SpinCtrl(self, -1, "", size=wx.Size(50, 23))
  763 + spin_size_dist.SetRange(1, 99)
  764 + spin_size_dist.SetValue(const.COIL_ANGLES_THRESHOLD)
  765 + spin_size_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist))
  766 + spin_size_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist))
  767 +
  768 + # Change timestamp interval
  769 + text_timestamp = wx.StaticText(self, -1, _("Timestamp interval [s]:"))
  770 + spin_timestamp_dist = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc = 0.1)
  771 + spin_timestamp_dist.SetRange(0.5, 60.0)
  772 + spin_timestamp_dist.SetValue(self.timestamp)
  773 + spin_timestamp_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist))
  774 + spin_timestamp_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist))
  775 + self.spin_timestamp_dist = spin_timestamp_dist
  776 +
  777 + # Create a horizontal sizer to threshold configs
  778 + line_angle_threshold = wx.BoxSizer(wx.HORIZONTAL)
  779 + line_angle_threshold.AddMany([(text_angles, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5),
  780 + (spin_size_angles, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)])
  781 +
  782 + line_dist_threshold = wx.BoxSizer(wx.HORIZONTAL)
  783 + line_dist_threshold.AddMany([(text_dist, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5),
  784 + (spin_size_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)])
  785 +
  786 + line_timestamp = wx.BoxSizer(wx.HORIZONTAL)
  787 + line_timestamp.AddMany([(text_timestamp, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5),
  788 + (spin_timestamp_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)])
  789 +
  790 + # Check box for trigger monitoring to create markers from serial port
  791 + checkrecordcoords = wx.CheckBox(self, -1, _('Record coordinates'))
  792 + checkrecordcoords.SetValue(False)
  793 + checkrecordcoords.Enable(0)
  794 + checkrecordcoords.Bind(wx.EVT_CHECKBOX, partial(self.OnRecordCoords, ctrl=checkrecordcoords))
  795 + self.checkrecordcoords = checkrecordcoords
  796 +
  797 + # Check box to track object or simply the stylus
  798 + checktrack = wx.CheckBox(self, -1, _('Track object'))
  799 + checktrack.SetValue(False)
  800 + checktrack.Enable(0)
  801 + checktrack.Bind(wx.EVT_CHECKBOX, partial(self.OnTrackObject, ctrl=checktrack))
  802 + self.checktrack = checktrack
  803 +
  804 + line_checks = wx.BoxSizer(wx.HORIZONTAL)
  805 + line_checks.Add(checkrecordcoords, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5)
  806 + line_checks.Add(checktrack, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.LEFT, 5)
  807 +
  808 + # Add line sizers into main sizer
  809 + main_sizer = wx.BoxSizer(wx.VERTICAL)
  810 + main_sizer.Add(line_save, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.ALIGN_CENTER_HORIZONTAL, 5)
  811 + main_sizer.Add(line_angle_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
  812 + main_sizer.Add(line_dist_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
  813 + main_sizer.Add(line_timestamp, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
  814 + main_sizer.Add(line_checks, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, 10)
  815 + main_sizer.Fit(self)
  816 +
  817 + self.SetSizer(main_sizer)
  818 + self.Update()
  819 +
  820 + def __bind_events(self):
  821 + Publisher.subscribe(self.UpdateTrackerInit, 'Update tracker initializer')
  822 + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
  823 + Publisher.subscribe(self.OnCloseProject, 'Close project data')
  824 +
  825 + def UpdateTrackerInit(self, pubsub_evt):
  826 + self.nav_prop = pubsub_evt.data
  827 +
  828 + def UpdateNavigationStatus(self, pubsub_evt):
  829 + nav_status = pubsub_evt.data
  830 + if nav_status:
  831 + self.checkrecordcoords.Enable(1)
  832 + self.checktrack.Enable(0)
  833 + self.btn_save.Enable(0)
  834 + self.btn_new.Enable(0)
  835 + self.btn_load.Enable(0)
  836 + else:
  837 + self.OnRecordCoords(nav_status, self.checkrecordcoords)
  838 + self.checkrecordcoords.SetValue(False)
  839 + self.checkrecordcoords.Enable(0)
  840 + self.btn_save.Enable(1)
  841 + self.btn_new.Enable(1)
  842 + self.btn_load.Enable(1)
  843 + if self.obj_fiducials is not None:
  844 + self.checktrack.Enable(1)
  845 + Publisher.sendMessage('Enable target button', True)
  846 +
  847 + def OnSelectAngleThreshold(self, evt, ctrl):
  848 + Publisher.sendMessage('Update angle threshold', ctrl.GetValue())
  849 +
  850 + def OnSelectDistThreshold(self, evt, ctrl):
  851 + Publisher.sendMessage('Update dist threshold', ctrl.GetValue())
  852 +
  853 + def OnSelectTimestamp(self, evt, ctrl):
  854 + self.timestamp = ctrl.GetValue()
  855 +
  856 + def OnRecordCoords(self, evt, ctrl):
  857 + if ctrl.GetValue() and evt:
  858 + self.spin_timestamp_dist.Enable(0)
  859 + self.thr_record = rec.Record(ctrl.GetValue(), self.timestamp)
  860 + elif (not ctrl.GetValue() and evt) or (ctrl.GetValue() and not evt) :
  861 + self.spin_timestamp_dist.Enable(1)
  862 + self.thr_record.stop()
  863 + elif not ctrl.GetValue() and not evt:
  864 + None
  865 +
  866 + def OnTrackObject(self, evt, ctrl):
  867 + Publisher.sendMessage('Update track object state', (evt.GetSelection(), self.obj_name))
  868 +
  869 + def OnComboCoil(self, evt):
  870 + # coil_name = evt.GetString()
  871 + coil_index = evt.GetSelection()
  872 + Publisher.sendMessage('Change selected coil', self.coil_list[coil_index][1])
  873 +
  874 + def OnLinkCreate(self, event=None):
  875 +
  876 + if self.nav_prop:
  877 + dialog = dlg.ObjectCalibrationDialog(self.nav_prop)
  878 + try:
  879 + if dialog.ShowModal() == wx.ID_OK:
  880 + self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name = dialog.GetValue()
  881 + if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all():
  882 + self.checktrack.Enable(1)
  883 + Publisher.sendMessage('Update object registration',
  884 + (self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name))
  885 + Publisher.sendMessage('Update status text in GUI', _("Ready"))
  886 +
  887 + except wx._core.PyAssertionError: # TODO FIX: win64
  888 + pass
  889 +
  890 + else:
  891 + dlg.NavigationTrackerWarning(0, 'choose')
  892 +
  893 + def OnLinkLoad(self, event=None):
  894 + filename = dlg.ShowLoadRegistrationDialog()
  895 +
  896 + if filename:
  897 + data = np.loadtxt(filename, delimiter='\t')
  898 + self.obj_fiducials = data[:, :3]
  899 + self.obj_orients = data[:, 3:]
  900 +
  901 + text_file = open(filename, "r")
  902 + header = text_file.readline().split('\t')
  903 + text_file.close()
  904 +
  905 + self.obj_name = header[1]
  906 + self.obj_ref_mode = int(header[-1])
  907 +
  908 + self.checktrack.Enable(1)
  909 + Publisher.sendMessage('Update object registration',
  910 + (self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name))
  911 + Publisher.sendMessage('Update status text in GUI', _("Ready"))
  912 + wx.MessageBox(_("Object file successfully loaded"), _("Load"))
  913 +
  914 + def ShowSaveObjectDialog(self, evt):
  915 + if np.isnan(self.obj_fiducials).any() or np.isnan(self.obj_orients).any():
  916 + wx.MessageBox(_("Digitize all object fiducials before saving"), _("Save error"))
  917 + else:
  918 + filename = dlg.ShowSaveRegistrationDialog("object_registration.obr")
  919 + if filename:
  920 + hdr = 'Object' + "\t" + self.obj_name + "\t" + 'Reference' + "\t" + str('%d' % self.obj_ref_mode)
  921 + data = np.hstack([self.obj_fiducials, self.obj_orients])
  922 + np.savetxt(filename, data, fmt='%.4f', delimiter='\t', newline='\n', header=hdr)
  923 + wx.MessageBox(_("Object file successfully saved"), _("Save"))
  924 +
  925 + def OnCloseProject(self, pubsub_evt):
  926 + self.checkrecordcoords.SetValue(False)
  927 + self.checkrecordcoords.Enable(0)
  928 + self.checktrack.SetValue(False)
  929 + self.checktrack.Enable(0)
  930 +
  931 + self.nav_prop = None
  932 + self.obj_fiducials = None
  933 + self.obj_orients = None
  934 + self.obj_ref_mode = None
  935 + self.obj_name = None
  936 + self.timestamp = const.TIMESTAMP
  937 +
537 938  
538 939 class MarkersPanel(wx.Panel):
539 940 def __init__(self, parent):
... ... @@ -545,9 +946,12 @@ class MarkersPanel(wx.Panel):
545 946  
546 947 self.__bind_events()
547 948  
548   - self.current_coord = 0, 0, 0
  949 + self.current_coord = 0, 0, 0, 0, 0, 0
  950 + self.current_angle = 0, 0, 0
549 951 self.list_coord = []
550 952 self.marker_ind = 0
  953 + self.tgt_flag = self.tgt_index = None
  954 + self.nav_status = False
551 955  
552 956 self.marker_colour = (0.0, 0.0, 1.)
553 957 self.marker_size = 4
... ... @@ -608,8 +1012,8 @@ class MarkersPanel(wx.Panel):
608 1012 self.lc.SetColumnWidth(1, 50)
609 1013 self.lc.SetColumnWidth(2, 50)
610 1014 self.lc.SetColumnWidth(3, 50)
611   - self.lc.SetColumnWidth(4, 50)
612   - self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnListEditMarkerId)
  1015 + self.lc.SetColumnWidth(4, 60)
  1016 + self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown)
613 1017 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink)
614 1018 self.lc.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnStopItemBlink)
615 1019  
... ... @@ -625,17 +1029,34 @@ class MarkersPanel(wx.Panel):
625 1029 self.Update()
626 1030  
627 1031 def __bind_events(self):
628   - Publisher.subscribe(self.UpdateCurrentCoord, 'Set ball reference position')
  1032 + Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points')
629 1033 Publisher.subscribe(self.OnDeleteSingleMarker, 'Delete fiducial marker')
  1034 + Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers')
630 1035 Publisher.subscribe(self.OnCreateMarker, 'Create marker')
  1036 + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
631 1037  
632 1038 def UpdateCurrentCoord(self, pubsub_evt):
633   - self.current_coord = pubsub_evt.data
  1039 + self.current_coord = pubsub_evt.data[1][:]
  1040 + #self.current_angle = pubsub_evt.data[1][3:]
  1041 +
  1042 + def UpdateNavigationStatus(self, pubsub_evt):
  1043 + if pubsub_evt.data is False:
  1044 + sleep(0.5)
  1045 + #self.current_coord[3:] = 0, 0, 0
  1046 + self.nav_status = False
  1047 + else:
  1048 + self.nav_status = True
  1049 +
  1050 + def OnMouseRightDown(self, evt):
  1051 + self.OnListEditMarkerId(self.nav_status)
634 1052  
635   - def OnListEditMarkerId(self, evt):
  1053 + def OnListEditMarkerId(self, status):
636 1054 menu_id = wx.Menu()
637   - menu_id.Append(-1, _('Edit ID'))
638   - menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerId)
  1055 + edit_id = menu_id.Append(0, _('Edit ID'))
  1056 + menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerId, edit_id)
  1057 + target_menu = menu_id.Append(1, _('Set as target'))
  1058 + menu_id.Bind(wx.EVT_MENU, self.OnMenuSetTarget, target_menu)
  1059 + target_menu.Enable(status)
639 1060 self.PopupMenu(menu_id)
640 1061 menu_id.Destroy()
641 1062  
... ... @@ -646,20 +1067,64 @@ class MarkersPanel(wx.Panel):
646 1067 Publisher.sendMessage('Stop Blink Marker')
647 1068  
648 1069 def OnMenuEditMarkerId(self, evt):
649   - id_label = dlg.EnterMarkerID(self.lc.GetItemText(self.lc.GetFocusedItem(), 4))
650 1070 list_index = self.lc.GetFocusedItem()
  1071 + if evt == 'TARGET':
  1072 + id_label = evt
  1073 + else:
  1074 + id_label = dlg.EnterMarkerID(self.lc.GetItemText(list_index, 4))
  1075 + if id_label == 'TARGET':
  1076 + id_label = ''
  1077 + dlg.InvalidTargetID()
651 1078 self.lc.SetStringItem(list_index, 4, id_label)
652 1079 # Add the new ID to exported list
653   - self.list_coord[list_index][7] = str(id_label)
  1080 + if len(self.list_coord[list_index]) > 8:
  1081 + self.list_coord[list_index][10] = str(id_label)
  1082 + else:
  1083 + self.list_coord[list_index][7] = str(id_label)
  1084 +
  1085 + def OnMenuSetTarget(self, evt):
  1086 + if isinstance(evt, int):
  1087 + self.lc.Focus(evt)
  1088 +
  1089 + if self.tgt_flag:
  1090 + self.lc.SetItemBackgroundColour(self.tgt_index, 'white')
  1091 + Publisher.sendMessage('Set target transparency', [False, self.tgt_index])
  1092 + self.lc.SetStringItem(self.tgt_index, 4, '')
  1093 + # Add the new ID to exported list
  1094 + if len(self.list_coord[self.tgt_index]) > 8:
  1095 + self.list_coord[self.tgt_index][10] = str('')
  1096 + else:
  1097 + self.list_coord[self.tgt_index][7] = str('')
  1098 +
  1099 + self.tgt_index = self.lc.GetFocusedItem()
  1100 + self.lc.SetItemBackgroundColour(self.tgt_index, 'RED')
  1101 +
  1102 + Publisher.sendMessage('Update target', self.list_coord[self.tgt_index])
  1103 + Publisher.sendMessage('Set target transparency', [True, self.tgt_index])
  1104 + Publisher.sendMessage('Disable or enable coil tracker', True)
  1105 + self.OnMenuEditMarkerId('TARGET')
  1106 + self.tgt_flag = True
  1107 + dlg.NewTarget()
  1108 +
  1109 + def OnDeleteAllMarkers(self, evt):
  1110 + if self.list_coord:
  1111 + if hasattr(evt, 'data'):
  1112 + result = wx.ID_OK
  1113 + else:
  1114 + result = dlg.DeleteAllMarkers()
  1115 +
  1116 + if result == wx.ID_OK:
  1117 + self.list_coord = []
  1118 + self.marker_ind = 0
  1119 + Publisher.sendMessage('Remove all markers', self.lc.GetItemCount())
  1120 + self.lc.DeleteAllItems()
  1121 + Publisher.sendMessage('Stop Blink Marker', 'DeleteAll')
654 1122  
655   - def OnDeleteAllMarkers(self, pubsub_evt):
656   - result = dlg.DeleteAllMarkers()
657   - if result == wx.ID_OK:
658   - self.list_coord = []
659   - self.marker_ind = 0
660   - Publisher.sendMessage('Remove all markers', self.lc.GetItemCount())
661   - self.lc.DeleteAllItems()
662   - Publisher.sendMessage('Stop Blink Marker', 'DeleteAll')
  1123 + if self.tgt_flag:
  1124 + self.tgt_flag = self.tgt_index = None
  1125 + Publisher.sendMessage('Disable or enable coil tracker', False)
  1126 + if not hasattr(evt, 'data'):
  1127 + dlg.DeleteTarget()
663 1128  
664 1129 def OnDeleteSingleMarker(self, evt):
665 1130 # OnDeleteSingleMarker is used for both pubsub and button click events
... ... @@ -682,6 +1147,10 @@ class MarkersPanel(wx.Panel):
682 1147 index = None
683 1148  
684 1149 if index:
  1150 + if self.tgt_flag and self.tgt_index == index[0]:
  1151 + self.tgt_flag = self.tgt_index = None
  1152 + Publisher.sendMessage('Disable or enable coil tracker', False)
  1153 + dlg.DeleteTarget()
685 1154 self.DeleteMarker(index)
686 1155 else:
687 1156 dlg.NoMarkerSelected()
... ... @@ -711,20 +1180,42 @@ class MarkersPanel(wx.Panel):
711 1180  
712 1181 if filepath:
713 1182 try:
  1183 + count_line = self.lc.GetItemCount()
714 1184 content = [s.rstrip() for s in open(filepath)]
715 1185 for data in content:
  1186 + target = None
716 1187 line = [s for s in data.split()]
717   - coord = float(line[0]), float(line[1]), float(line[2])
718   - colour = float(line[3]), float(line[4]), float(line[5])
719   - size = float(line[6])
  1188 + if len(line) > 8:
  1189 + coord = float(line[0]), float(line[1]), float(line[2]), float(line[3]), float(line[4]), float(line[5])
  1190 + colour = float(line[6]), float(line[7]), float(line[8])
  1191 + size = float(line[9])
  1192 +
  1193 + if len(line) == 11:
  1194 + for i in const.BTNS_IMG_MKS:
  1195 + if line[10] in const.BTNS_IMG_MKS[i].values()[0]:
  1196 + Publisher.sendMessage('Load image fiducials', (line[10], coord))
  1197 + elif line[10] == 'TARGET':
  1198 + target = count_line
  1199 + else:
  1200 + line.append("")
  1201 +
  1202 + self.CreateMarker(coord, colour, size, line[10])
  1203 + if target is not None:
  1204 + self.OnMenuSetTarget(target)
720 1205  
721   - if len(line) == 8:
722   - for i in const.BTNS_IMG_MKS:
723   - if line[7] in const.BTNS_IMG_MKS[i].values()[0]:
724   - Publisher.sendMessage('Load image fiducials', (line[7], coord))
725 1206 else:
726   - line.append("")
727   - self.CreateMarker(coord, colour, size, line[7])
  1207 + coord = float(line[0]), float(line[1]), float(line[2]), 0, 0, 0
  1208 + colour = float(line[3]), float(line[4]), float(line[5])
  1209 + size = float(line[6])
  1210 +
  1211 + if len(line) == 8:
  1212 + for i in const.BTNS_IMG_MKS:
  1213 + if line[7] in const.BTNS_IMG_MKS[i].values()[0]:
  1214 + Publisher.sendMessage('Load image fiducials', (line[7], coord))
  1215 + else:
  1216 + line.append("")
  1217 + self.CreateMarker(coord, colour, size, line[7])
  1218 + count_line += 1
728 1219 except:
729 1220 dlg.InvalidMarkersFile()
730 1221  
... ... @@ -744,14 +1235,16 @@ class MarkersPanel(wx.Panel):
744 1235 text_file = open(filename, "w")
745 1236 list_slice1 = self.list_coord[0]
746 1237 coord = str('%.3f' %self.list_coord[0][0]) + "\t" + str('%.3f' %self.list_coord[0][1]) + "\t" + str('%.3f' %self.list_coord[0][2])
747   - properties = str('%.3f' %list_slice1[3]) + "\t" + str('%.3f' %list_slice1[4]) + "\t" + str('%.3f' %list_slice1[5]) + "\t" + str('%.1f' %list_slice1[6]) + "\t" + list_slice1[7]
748   - line = coord + "\t" + properties + "\n"
  1238 + angles = str('%.3f' %self.list_coord[0][3]) + "\t" + str('%.3f' %self.list_coord[0][4]) + "\t" + str('%.3f' %self.list_coord[0][5])
  1239 + properties = str('%.3f' %list_slice1[6]) + "\t" + str('%.3f' %list_slice1[7]) + "\t" + str('%.3f' %list_slice1[8]) + "\t" + str('%.1f' %list_slice1[9]) + "\t" + list_slice1[10]
  1240 + line = coord + "\t" + angles + "\t" + properties + "\n"
749 1241 list_slice = self.list_coord[1:]
750 1242  
751 1243 for value in list_slice:
752 1244 coord = str('%.3f' %value[0]) + "\t" + str('%.3f' %value[1]) + "\t" + str('%.3f' %value[2])
753   - properties = str('%.3f' %value[3]) + "\t" + str('%.3f' %value[4]) + "\t" + str('%.3f' %value[5]) + "\t" + str('%.1f' %value[6]) + "\t" + value[7]
754   - line = line + coord + "\t" + properties + "\n"
  1245 + angles = str('%.3f' % value[3]) + "\t" + str('%.3f' % value[4]) + "\t" + str('%.3f' % value[5])
  1246 + properties = str('%.3f' %value[6]) + "\t" + str('%.3f' %value[7]) + "\t" + str('%.3f' %value[8]) + "\t" + str('%.1f' %value[9]) + "\t" + value[10]
  1247 + line = line + coord + "\t" + angles + "\t" +properties + "\n"
755 1248  
756 1249 text_file.writelines(line)
757 1250 text_file.close()
... ... @@ -766,12 +1259,13 @@ class MarkersPanel(wx.Panel):
766 1259 # TODO: Use matrix coordinates and not world coordinates as current method.
767 1260 # This makes easier for inter-software comprehension.
768 1261  
769   - Publisher.sendMessage('Add marker', (self.marker_ind, size, colour, coord))
  1262 + Publisher.sendMessage('Add marker', (self.marker_ind, size, colour, coord[0:3]))
770 1263  
771 1264 self.marker_ind += 1
772 1265  
773 1266 # List of lists with coordinates and properties of a marker
774   - line = [coord[0], coord[1], coord[2], colour[0], colour[1], colour[2], self.marker_size, marker_id]
  1267 +
  1268 + line = [coord[0], coord[1], coord[2], coord[3], coord[4], coord[5], colour[0], colour[1], colour[2], size, marker_id]
775 1269  
776 1270 # Adding current line to a list of all markers already created
777 1271 if not self.list_coord:
... ...
navigation/objects/aim.stl 0 → 100644
No preview for this file type
navigation/objects/magstim_fig8_coil.stl 0 → 100644
No preview for this file type
navigation/objects/magstim_fig8_coil_no_handle.stl 0 → 100644
No preview for this file type