Commit 34f7ab2744786ef0fc220f059231a18170fd6be7
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
Showing
18 changed files
with
2461 additions
and
257 deletions
Show diff stats
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 | ... | ... |
... | ... | @@ -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: | ... | ... |
No preview for this file type
No preview for this file type
No preview for this file type