Commit 3a9ab9cd0df30d332aba58994620b8b4b83e280a

Authored by Renan
Committed by GitHub
1 parent f9bfc475
Exists in master

Decouple robot control (#404)

* INIT testing..

* REM: Decouple robot

* FIX: trck_init gets the first value

* INIT testing..

* REM: Decouple robot

* FIX: trck_init gets the first value

* FIX: append tracker init robot

* FIX: transform robot coordinates to tracker coordinates system

* ENH: robot matrix estimation outside invesalius

* ENH: remove robot coordinates and target matrix from invesalius

* FIX: disconnected

* FIX: tracker connection

* ENH: Cleaning

* CLN: remove unused imports

* ENH: checks with target angle is zero/none to enable send target to robot

* more cleaning

* FIX: destroys robot dialog if not possible to connect to the robot

* ENH: deal with robot resetting

* ADD: function to estimate transformation matrix

-navigation starts after ICP box

* FIX: deals with tracker devices not connected

* FIX: transformation matrix to list

* ADD: send target to robot from image

-need tests

* FIX: image_to_tracker transformation

* ENH: apply icp matrix to target

* ENH: Show robot menu item only with tracker is robot

* FIX: typo and comments

* FIX: exception for BadNamespaceError

* ADD: set metaclass to navigation and icp

-allows access for plugins

* CLEAN: remove unused imports
invesalius/constants.py
... ... @@ -832,7 +832,6 @@ SEED_RADIUS = 1.5
832 832 # Increased the default sleep parameter from 0.1 to 0.15 to decrease CPU load during navigation.
833 833 SLEEP_NAVIGATION = 0.2
834 834 SLEEP_COORDINATES = 0.05
835   -SLEEP_ROBOT = 0.01
836 835  
837 836 BRAIN_OPACITY = 0.6
838 837 N_CPU = psutil.cpu_count()
... ... @@ -864,14 +863,3 @@ PULSE_DURATION_IN_MILLISECONDS = 0.2
864 863  
865 864 #Robot
866 865 ROBOT_ElFIN_IP = ['143.107.220.251', '169.254.153.251', '127.0.0.1']
867   -ROBOT_ElFIN_PORT = 10003
868   -ROBOT_MOTIONS = {"normal": 0, "linear out": 1, "arc": 2}
869   -ROBOT_HEAD_VELOCITY_THRESHOLD = 10 #mm/s
870   -ROBOT_ARC_THRESHOLD_DISTANCE = 100 #mm
871   -ROBOT_VERSOR_SCALE_FACTOR = 70
872   -#Robot Working Space is defined as 800mm in Elfin manual. For safety, the value is reduced by 5%.
873   -ROBOT_WORKING_SPACE = 760 #mm
874   -ROBOT_MOVE_STATE = {"free to move": 0,
875   - "in motion": 1009,
876   - "waiting for execution": 1013,
877   - "error": 1025}
... ...
invesalius/data/bases.py
... ... @@ -163,6 +163,12 @@ def transform_icp(m_img, m_icp):
163 163  
164 164 return m_img
165 165  
  166 +def inverse_transform_icp(m_img, m_icp):
  167 + coord_img = [m_img[0, -1], -m_img[1, -1], m_img[2, -1], 1]
  168 + m_img[0, -1], m_img[1, -1], m_img[2, -1], _ = np.linalg.inv(m_icp) @ coord_img
  169 + m_img[0, -1], m_img[1, -1], m_img[2, -1] = m_img[0, -1], -m_img[1, -1], m_img[2, -1]
  170 +
  171 + return m_img
166 172  
167 173 def object_registration(fiducials, orients, coord_raw, m_change):
168 174 """
... ... @@ -244,47 +250,3 @@ def object_registration(fiducials, orients, coord_raw, m_change):
244 250 )
245 251  
246 252 return t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img
247   -
248   -def compute_robot_to_head_matrix(raw_target_robot):
249   - """
250   - :param head: nx6 array of head coordinates from tracking device in robot space
251   - :param robot: nx6 array of robot coordinates
252   -
253   - :return: target_robot_matrix: 3x3 array representing change of basis from robot to head in robot system
254   - """
255   - head, robot = raw_target_robot
256   - # compute head target matrix
257   - m_head_target = dco.coordinates_to_transformation_matrix(
258   - position=head[:3],
259   - orientation=head[3:],
260   - axes='rzyx',
261   - )
262   -
263   - # compute robot target matrix
264   - m_robot_target = dco.coordinates_to_transformation_matrix(
265   - position=robot[:3],
266   - orientation=robot[3:],
267   - axes='rzyx',
268   - )
269   - robot_to_head_matrix = np.linalg.inv(m_head_target) @ m_robot_target
270   -
271   - return robot_to_head_matrix
272   -
273   -
274   -class transform_tracker_to_robot(object):
275   - M_tracker_to_robot = np.array([])
276   - def transformation_tracker_to_robot(self, tracker_coord):
277   - if not transform_tracker_to_robot.M_tracker_to_robot.any():
278   - return None
279   -
280   - M_tracker = dco.coordinates_to_transformation_matrix(
281   - position=tracker_coord[:3],
282   - orientation=tracker_coord[3:6],
283   - axes='rzyx',
284   - )
285   - M_tracker_in_robot = transform_tracker_to_robot.M_tracker_to_robot @ M_tracker
286   -
287   - translation, angles_as_deg = dco.transformation_matrix_to_coordinates(M_tracker_in_robot, axes='rzyx')
288   - tracker_in_robot = list(translation) + list(angles_as_deg)
289   -
290   - return tracker_in_robot
... ...
invesalius/data/coordinates.py
... ... @@ -22,7 +22,6 @@ import numpy as np
22 22 import threading
23 23 import wx
24 24  
25   -import invesalius.data.bases as db
26 25 import invesalius.data.transformations as tr
27 26 import invesalius.constants as const
28 27  
... ... @@ -47,14 +46,20 @@ class TrackerCoordinates():
47 46 def SetCoordinates(self, coord, markers_flag):
48 47 self.coord = coord
49 48 self.markers_flag = markers_flag
50   - if self.previous_markers_flag != self.markers_flag and not self.nav_status:
51   - wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)
52   - self.previous_markers_flag = self.markers_flag
  49 + if not self.nav_status:
  50 + wx.CallAfter(Publisher.sendMessage, 'Update tracker coordinates',
  51 + coord=self.coord.tolist(), markers_flag=self.markers_flag)
  52 + if self.previous_markers_flag != self.markers_flag:
  53 + wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)
  54 + self.previous_markers_flag = self.markers_flag
53 55  
54 56 def GetCoordinates(self):
55   - if self.previous_markers_flag != self.markers_flag and self.nav_status:
56   - wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)
57   - self.previous_markers_flag = self.markers_flag
  57 + if self.nav_status:
  58 + wx.CallAfter(Publisher.sendMessage, 'Update tracker coordinates',
  59 + coord=self.coord.tolist(), markers_flag=self.markers_flag)
  60 + if self.previous_markers_flag != self.markers_flag:
  61 + wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)
  62 + self.previous_markers_flag = self.markers_flag
58 63  
59 64 return self.coord, self.markers_flag
60 65  
... ... @@ -195,16 +200,6 @@ def PolarisCoord(trck_init, trck_id, ref_mode):
195 200  
196 201 return coord, [trck.probeID, trck.refID, trck.objID]
197 202  
198   -def ElfinCoord(trck_init):
199   - if len(trck_init) > 2:
200   - robotcoordinates = trck_init[-1]
201   - coord = robotcoordinates.GetRobotCoordinates()
202   - if coord is None:
203   - coord = np.array([0, 0, 0, 0, 0, 0])
204   - else:
205   - coord = np.array([0, 0, 0, 0, 0, 0])
206   -
207   - return coord
208 203  
209 204 def CameraCoord(trck_init, trck_id, ref_mode):
210 205 trck = trck_init[0]
... ... @@ -346,6 +341,13 @@ def PolhemusSerialCoord(trck_init, trck_id, ref_mode):
346 341  
347 342 return coord
348 343  
  344 +def RobotCoord(trk_init, trck_id, ref_mode):
  345 + #trck_id is the tracker related to the robot ID. To get the tracker ID, combined with the robot,
  346 + # it is required to get trk_init[1]
  347 + tracker_id = trk_init[1]
  348 + coord_tracker, markers_flag = GetCoordinatesForThread(trk_init[0], tracker_id, ref_mode)
  349 +
  350 + return np.vstack([coord_tracker[0], coord_tracker[1], coord_tracker[2]]), markers_flag
349 351  
350 352 def DebugCoordRandom(trk_init, trck_id, ref_mode):
351 353 """
... ... @@ -497,22 +499,6 @@ def dynamic_reference_m(probe, reference):
497 499 return coord_rot
498 500  
499 501  
500   -def RobotCoord(trk_init, trck_id, ref_mode):
501   - coord_tracker, markers_flag = GetCoordinatesForThread(trk_init[0][0], trk_init[1], ref_mode)
502   - coord_robot = ElfinCoord([trk_init[0][1]]+trk_init[1:])
503   -
504   - probe_tracker_in_robot = db.transform_tracker_to_robot().transformation_tracker_to_robot(coord_tracker[0])
505   - ref_tracker_in_robot = db.transform_tracker_to_robot().transformation_tracker_to_robot(coord_tracker[1])
506   - obj_tracker_in_robot = db.transform_tracker_to_robot().transformation_tracker_to_robot(coord_tracker[2])
507   -
508   - if probe_tracker_in_robot is None:
509   - probe_tracker_in_robot = coord_tracker[0]
510   - ref_tracker_in_robot = coord_tracker[1]
511   - obj_tracker_in_robot = coord_tracker[2]
512   -
513   - return np.vstack([probe_tracker_in_robot, ref_tracker_in_robot, coord_robot, obj_tracker_in_robot]), markers_flag
514   -
515   -
516 502 def dynamic_reference_m2(probe, reference):
517 503 """
518 504 Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama
... ... @@ -590,4 +576,4 @@ class ReceiveCoordinates(threading.Thread):
590 576 while not self.event.is_set():
591 577 coord_raw, markers_flag = GetCoordinatesForThread(self.trck_init, self.trck_id, const.DEFAULT_REF_MODE)
592 578 self.TrackerCoordinates.SetCoordinates(coord_raw, markers_flag)
593   - sleep(const.SLEEP_COORDINATES)
594 579 \ No newline at end of file
  580 + sleep(const.SLEEP_COORDINATES)
... ...
invesalius/data/coregistration.py
... ... @@ -101,6 +101,32 @@ def tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn):
101 101 m_img[:3, :3] = r_obj[:3, :3]
102 102 return m_img
103 103  
  104 +def image_to_tracker(m_change, target, icp):
  105 + """Compute the transformation matrix to the tracker coordinate system
  106 +
  107 + :param m_change: Corregistration transformation obtained from fiducials
  108 + :type m_change: numpy.ndarray
  109 + :param target: Target in invesalius coordinate system
  110 + :type target: numpy.ndarray
  111 + :param icp: ICP transformation matrix
  112 + :type icp: numpy.ndarray
  113 +
  114 + :return: 4 x 4 numpy double array
  115 + :rtype: numpy.ndarray
  116 + """
  117 + m_target_in_image = dco.coordinates_to_transformation_matrix(
  118 + position=target[:3],
  119 + orientation=[0, 0, 0],
  120 + axes='sxyz',
  121 + )
  122 + if icp.use_icp:
  123 + m_target_in_image = bases.inverse_transform_icp(m_target_in_image, icp.m_icp)
  124 + m_target_in_tracker = np.linalg.inv(m_change) @ m_target_in_image
  125 +
  126 + # invert y coordinate
  127 + m_target_in_tracker[2, -1] = -m_target_in_tracker[2, -1]
  128 +
  129 + return m_target_in_tracker
104 130  
105 131 def corregistrate_object_dynamic(inp, coord_raw, ref_mode_id, icp):
106 132  
... ... @@ -193,7 +219,6 @@ class CoordinateCorregistrate(threading.Thread):
193 219 self.object_at_target_queue = queues[3]
194 220 self.icp = None
195 221 self.m_icp = None
196   - self.robot_tracker_flag = None
197 222 self.last_coord = None
198 223 self.tracker_id = tracker_id
199 224 self.target = target
... ... @@ -254,7 +279,7 @@ class CoordinateCorregistrate(threading.Thread):
254 279 if self.icp:
255 280 m_img = bases.transform_icp(m_img, self.m_icp)
256 281  
257   - self.coord_queue.put_nowait([coord, [coord_raw, markers_flag], m_img, view_obj])
  282 + self.coord_queue.put_nowait([coord, markers_flag, m_img, view_obj])
258 283 # print('CoordCoreg: put {}'.format(count))
259 284 # count += 1
260 285  
... ... @@ -307,7 +332,7 @@ class CoordinateCorregistrateNoObject(threading.Thread):
307 332 if self.icp:
308 333 m_img = bases.transform_icp(m_img, self.m_icp)
309 334  
310   - self.coord_queue.put_nowait([coord, [coord_raw, markers_flag], m_img, view_obj])
  335 + self.coord_queue.put_nowait([coord, markers_flag, m_img, view_obj])
311 336  
312 337 if self.view_tracts:
313 338 self.coord_tracts_queue.put_nowait(m_img_flip)
... ...
invesalius/data/elfin.py
... ... @@ -1,252 +0,0 @@
1   -#!/usr/bin/env python3
2   -
3   -import sys
4   -from time import sleep
5   -from socket import socket, AF_INET, SOCK_STREAM
6   -import invesalius.constants as const
7   -
8   -class Elfin_Server():
9   - """
10   - This class is similar to tracker devices wrappers.
11   - It follows the same functions as the others (Initialize, Run and Close)
12   - """
13   - def __init__(self, server_ip, port_number):
14   - self.server_ip = server_ip
15   - self.port_number = port_number
16   -
17   - def Initialize(self):
18   - message_size = 1024
19   - robot_id = 0
20   - self.cobot = Elfin()
21   - self.cobot.connect(self.server_ip, self.port_number, message_size, robot_id)
22   - print("connected!")
23   -
24   - def Run(self):
25   - return self.cobot.ReadPcsActualPos()
26   -
27   - def SendCoordinates(self, target, motion_type=const.ROBOT_MOTIONS["normal"]):
28   - """
29   - It's not possible to send a move command to elfin if the robot is during a move.
30   - Status 1009 means robot in motion.
31   - """
32   - status = self.cobot.ReadMoveState()
33   - if status == const.ROBOT_MOVE_STATE["free to move"]:
34   - if motion_type == const.ROBOT_MOTIONS["normal"] or motion_type == const.ROBOT_MOTIONS["linear out"]:
35   - self.cobot.MoveL(target)
36   - elif motion_type == const.ROBOT_MOTIONS["arc"]:
37   - self.cobot.MoveC(target)
38   - elif status == const.ROBOT_MOVE_STATE["error"]:
39   - self.StopRobot()
40   -
41   - def StopRobot(self):
42   - # Takes some microseconds to the robot actual stops after the command.
43   - # The sleep time is required to guarantee the stop
44   - self.cobot.GrpStop()
45   - sleep(0.1)
46   -
47   - def Close(self):
48   - self.StopRobot()
49   - #TODO: robot function to close? self.cobot.close()
50   -
51   -class Elfin:
52   - def __init__(self):
53   - """
54   - Class to communicate with elfin robot. This class follows "HansRobot Communication Protocol Interface".
55   - """
56   - self.end_msg = ",;"
57   -
58   - def connect(self, server_ip, port_number, message_size, robot_id):
59   - mySocket = socket(AF_INET, SOCK_STREAM)
60   - mySocket.connect((server_ip, port_number))
61   -
62   - self.message_size = message_size
63   - self.robot_id = str(robot_id)
64   - self.mySocket = mySocket
65   -
66   - def send(self, message):
67   - self.mySocket.sendall(message.encode('utf-8'))
68   - data = self.mySocket.recv(self.message_size).decode('utf-8').split(',')
69   - status = self.check_status(data)
70   - if status and type(data) != bool:
71   - if len(data) > 3:
72   - return data[2:-1]
73   - return status
74   -
75   - def check_status(self, recv_message):
76   - status = recv_message[1]
77   - if status == 'OK':
78   - return True
79   -
80   - elif status == 'Fail':
81   - print("Error code: ", recv_message[2])
82   - return False
83   -
84   - def Electrify(self):
85   - """
86   - Function: Power the robot
87   - Notes: successful completion of power up before returning, power up time is
88   - about 44s.
89   - :return:
90   - if Error Return False
91   - if not Error Return True
92   - """
93   - message = "Electrify" + self.end_msg
94   - status = self.send(message)
95   - return status
96   -
97   - def BlackOut(self):
98   - """
99   - Function: Robot blackout
100   - Notes: successful power outage will only return, power failure time is 3s.
101   - :return:
102   - if Error Return False
103   - if not Error Return True
104   - """
105   - message = "BlackOut" + self.end_msg
106   - status = self.send(message)
107   - return status
108   -
109   - def StartMaster(self):
110   - """
111   - Function: Start master station
112   - Notes: the master station will not be returned until successfully started, startup
113   - master time is about 4s.
114   - :return:
115   - if Error Return False
116   - if not Error Return True
117   - """
118   - message = "StartMaster" + self.end_msg
119   - status = self.send(message)
120   - return status
121   -
122   - def CloseMaster(self):
123   - """
124   - Function: Close master station
125   - Notes: the master station will not be returned until successfully closed, shut
126   - down the master station time is about 2s.
127   - :return:
128   - if Error Return False
129   - if not Error Return True
130   - """
131   - message = "CloseMaster" + self.end_msg
132   - status = self.send(message)
133   - return status
134   -
135   - def GrpPowerOn(self):
136   - """
137   - Function: Robot servo on
138   - :return:
139   - if Error Return False
140   - if not Error Return True
141   - """
142   - message = "GrpPowerOn," + self.robot_id + self.end_msg
143   - status = self.send(message)
144   - return status
145   -
146   - def GrpPowerOff(self):
147   - """
148   - Function: Robot servo off
149   - :return:
150   - if Error Return False
151   - if not Error Return True
152   - """
153   - message = "GrpPowerOff," + self.robot_id + self.end_msg
154   - status = self.send(message)
155   - return status
156   -
157   - def GrpStop(self):
158   - """
159   - Function: Stop robot
160   - :return:
161   - if Error Return False
162   - if not Error Return True
163   - """
164   - message = "GrpStop," + self.robot_id + self.end_msg
165   - status = self.send(message)
166   - return status
167   -
168   - def SetOverride(self, override):
169   - """
170   - function: Set speed ratio
171   - :param override:
172   - double: set speed ratio, range of 0.01~1
173   - :return:
174   - if Error Return False
175   - if not Error Return True
176   - """
177   -
178   - message = "SetOverride," + self.robot_id + ',' + str(override) + self.end_msg
179   - status = self.send(message)
180   - return status
181   -
182   - def ReadPcsActualPos(self):
183   - """Function: Get the actual position of the space coordinate
184   - :return:
185   - if True Return x,y,z,a,b,c
186   - if Error Return False
187   - """
188   - message = "ReadPcsActualPos," + self.robot_id + self.end_msg
189   - coord = self.send(message)
190   - if coord:
191   - return [float(s) for s in coord]
192   -
193   - return coord
194   -
195   - def MoveL(self, target):
196   - """
197   - function: Robot moves straight to the specified space coordinates
198   - :param: target:[X,Y,Z,RX,RY,RZ]
199   - :return:
200   - """
201   - target = [str(s) for s in target]
202   - target = (",".join(target))
203   - message = "MoveL," + self.robot_id + ',' + target + self.end_msg
204   - return self.send(message)
205   -
206   - def SetToolCoordinateMotion(self, status):
207   - """
208   - function: Function: Set tool coordinate motion
209   - :param: int Switch 0=close 1=open
210   - :return:
211   - if Error Return False
212   - if not Error Return True
213   - """
214   - message = "SetToolCoordinateMotion," + self.robot_id + ',' + str(status) + self.end_msg
215   - status = self.send(message)
216   - return status
217   -
218   - def ReadMoveState(self):
219   - """
220   - Function: Get the motion state of the robot
221   - :return:
222   - Current state of motion of robot:
223   - 0=motion completion;
224   - 1009=in motion;
225   - 1013=waiting for execution;
226   - 1025 =Error reporting
227   - """
228   - message = "ReadMoveState," + self.robot_id + self.end_msg
229   - status = int(self.send(message)[0])
230   - return status
231   -
232   - def MoveHoming(self):
233   - """
234   - Function: Robot returns to the origin
235   - :return:
236   - if Error Return False
237   - if not Error Return True
238   - """
239   - message = "MoveHoming," + self.robot_id + self.end_msg
240   - status = self.send(message)
241   - return status
242   -
243   - def MoveC(self, target):
244   - """
245   - function: Arc motion
246   - :param: Through position[X,Y,Z],GoalCoord[X,Y,Z,RX,RY,RZ],Type[0 or 1],;
247   - :return:
248   - """
249   - target = [str(s) for s in target]
250   - target = (",".join(target))
251   - message = "MoveC," + self.robot_id + ',' + target + self.end_msg
252   - return self.send(message)
invesalius/data/elfin_processing.py
... ... @@ -1,212 +0,0 @@
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   -import numpy as np
20   -import cv2
21   -from time import time
22   -
23   -import invesalius.data.coregistration as dcr
24   -import invesalius.data.coordinates as dco
25   -import invesalius.constants as const
26   -
27   -
28   -class KalmanTracker:
29   - """
30   - Kalman filter to avoid sudden fluctuation from tracker device.
31   - The filter strength can be set by the cov_process, and cov_measure parameter
32   - It is required to create one instance for each variable (x, y, z, a, b, g)
33   - """
34   - def __init__(self,
35   - state_num=2,
36   - covariance_process=0.001,
37   - covariance_measure=0.1):
38   -
39   - self.state_num = state_num
40   - measure_num = 1
41   -
42   - # The filter itself.
43   - self.filter = cv2.KalmanFilter(state_num, measure_num, 0)
44   -
45   - self.state = np.zeros((state_num, 1), dtype=np.float32)
46   - self.measurement = np.array((measure_num, 1), np.float32)
47   - self.prediction = np.zeros((state_num, 1), np.float32)
48   -
49   -
50   - self.filter.transitionMatrix = np.array([[1, 1],
51   - [0, 1]], np.float32)
52   - self.filter.measurementMatrix = np.array([[1, 1]], np.float32)
53   - self.filter.processNoiseCov = np.array([[1, 0],
54   - [0, 1]], np.float32) * covariance_process
55   - self.filter.measurementNoiseCov = np.array([[1]], np.float32) * covariance_measure
56   -
57   - def update_kalman(self, measurement):
58   - self.prediction = self.filter.predict()
59   - self.measurement = np.array([[np.float32(measurement[0])]])
60   -
61   - self.filter.correct(self.measurement)
62   - self.state = self.filter.statePost
63   -
64   -
65   -class TrackerProcessing:
66   - def __init__(self):
67   - self.coord_vel = []
68   - self.timestamp = []
69   - self.velocity_vector = []
70   - self.kalman_coord_vector = []
71   - self.velocity_std = 0
72   -
73   - self.tracker_stabilizers = [KalmanTracker(
74   - state_num=2,
75   - covariance_process=0.001,
76   - covariance_measure=0.1) for _ in range(6)]
77   -
78   - def kalman_filter(self, coord_tracker):
79   - kalman_array = []
80   - pose_np = np.array((coord_tracker[:3], coord_tracker[3:])).flatten()
81   - for value, ps_stb in zip(pose_np, self.tracker_stabilizers):
82   - ps_stb.update_kalman([value])
83   - kalman_array.append(ps_stb.state[0])
84   - coord_kalman = np.hstack(kalman_array)
85   -
86   - self.kalman_coord_vector.append(coord_kalman[:3])
87   - if len(self.kalman_coord_vector) < 20: #avoid initial fluctuations
88   - coord_kalman = coord_tracker
89   - print('initializing filter')
90   - else:
91   - del self.kalman_coord_vector[0]
92   -
93   - return coord_kalman
94   -
95   - def estimate_head_velocity(self, coord_vel, timestamp):
96   - coord_vel = np.vstack(np.array(coord_vel))
97   - coord_init = coord_vel[:int(len(coord_vel) / 2)].mean(axis=0)
98   - coord_final = coord_vel[int(len(coord_vel) / 2):].mean(axis=0)
99   - velocity = (coord_final - coord_init)/(timestamp[-1] - timestamp[0])
100   - distance = (coord_final - coord_init)
101   -
102   - return velocity, distance
103   -
104   - def compute_versors(self, init_point, final_point):
105   - init_point = np.array(init_point)
106   - final_point = np.array(final_point)
107   - norm = (sum((final_point - init_point) ** 2)) ** 0.5
108   - versor_factor = (((final_point-init_point) / norm) * const.ROBOT_VERSOR_SCALE_FACTOR).tolist()
109   -
110   - return versor_factor
111   -
112   -
113   - def compute_arc_motion(self, current_robot_coordinates, head_center_coordinates, new_robot_coordinates):
114   - head_center = head_center_coordinates[0], head_center_coordinates[1], head_center_coordinates[2], \
115   - new_robot_coordinates[3], new_robot_coordinates[4], new_robot_coordinates[5]
116   -
117   - versor_factor_move_out = self.compute_versors(head_center, current_robot_coordinates)
118   - init_move_out_point = current_robot_coordinates[0] + versor_factor_move_out[0], \
119   - current_robot_coordinates[1] + versor_factor_move_out[1], \
120   - current_robot_coordinates[2] + versor_factor_move_out[2], \
121   - current_robot_coordinates[3], current_robot_coordinates[4], current_robot_coordinates[5]
122   -
123   - middle_point = ((new_robot_coordinates[0] + current_robot_coordinates[0]) / 2,
124   - (new_robot_coordinates[1] + current_robot_coordinates[1]) / 2,
125   - (new_robot_coordinates[2] + current_robot_coordinates[2]) / 2,
126   - 0, 0, 0)
127   - versor_factor_middle_arc = (np.array(self.compute_versors(head_center, middle_point))) * 2
128   - middle_arc_point = middle_point[0] + versor_factor_middle_arc[0], \
129   - middle_point[1] + versor_factor_middle_arc[1], \
130   - middle_point[2] + versor_factor_middle_arc[2]
131   -
132   - versor_factor_arc = self.compute_versors(head_center, new_robot_coordinates)
133   - final_ext_arc_point = new_robot_coordinates[0] + versor_factor_arc[0], \
134   - new_robot_coordinates[1] + versor_factor_arc[1], \
135   - new_robot_coordinates[2] + versor_factor_arc[2], \
136   - new_robot_coordinates[3], new_robot_coordinates[4], new_robot_coordinates[5], 0
137   -
138   - target_arc = middle_arc_point + final_ext_arc_point
139   -
140   - return init_move_out_point, target_arc
141   -
142   - def compute_head_move_threshold(self, current_ref):
143   - """
144   - Checks if the head velocity is bellow the threshold
145   - """
146   - self.coord_vel.append(current_ref)
147   - self.timestamp.append(time())
148   - if len(self.coord_vel) >= 10:
149   - head_velocity, head_distance = self.estimate_head_velocity(self.coord_vel, self.timestamp)
150   - self.velocity_vector.append(head_velocity)
151   -
152   - del self.coord_vel[0]
153   - del self.timestamp[0]
154   -
155   - if len(self.velocity_vector) >= 30:
156   - self.velocity_std = np.std(self.velocity_vector)
157   - del self.velocity_vector[0]
158   -
159   - if self.velocity_std > const.ROBOT_HEAD_VELOCITY_THRESHOLD:
160   - print('Velocity threshold activated')
161   - return False
162   - else:
163   - return True
164   -
165   - return False
166   -
167   - def compute_head_move_compensation(self, current_head, m_change_robot_to_head):
168   - """
169   - Estimates the new robot position to reach the target
170   - """
171   - M_current_head = dco.coordinates_to_transformation_matrix(
172   - position=current_head[:3],
173   - orientation=current_head[3:6],
174   - axes='rzyx',
175   - )
176   - m_robot_new = M_current_head @ m_change_robot_to_head
177   -
178   - translation, angles_as_deg = dco.transformation_matrix_to_coordinates(m_robot_new, axes='sxyz')
179   - new_robot_position = list(translation) + list(angles_as_deg)
180   -
181   - return new_robot_position
182   -
183   - def estimate_head_center(self, tracker, current_head):
184   - """
185   - Estimates the actual head center position using fiducials
186   - """
187   - m_probe_head_left, m_probe_head_right, m_probe_head_nasion = tracker.GetMatrixTrackerFiducials()
188   - m_current_head = dcr.compute_marker_transformation(np.array([current_head]), 0)
189   -
190   - m_ear_left_new = m_current_head @ m_probe_head_left
191   - m_ear_right_new = m_current_head @ m_probe_head_right
192   -
193   - return (m_ear_left_new[:3, -1] + m_ear_right_new[:3, -1])/2
194   -
195   - def correction_distance_calculation_target(self, coord_inv, actual_point):
196   - """
197   - Estimates the Euclidean distance between the actual position and the target
198   - """
199   - correction_distance_compensation = np.sqrt((coord_inv[0]-actual_point[0]) ** 2 +
200   - (coord_inv[1]-actual_point[1]) ** 2 +
201   - (coord_inv[2]-actual_point[2]) ** 2)
202   -
203   - return correction_distance_compensation
204   -
205   - def estimate_robot_target_length(self, robot_target):
206   - """
207   - Estimates the length of the 3D vector of the robot target
208   - """
209   - robot_target_length = np.sqrt(robot_target[0] ** 2 + robot_target[1] ** 2 + robot_target[2] ** 2)
210   -
211   - return robot_target_length
212   -
invesalius/data/trackers.py
... ... @@ -18,6 +18,7 @@
18 18 #--------------------------------------------------------------------------
19 19 import invesalius.constants as const
20 20 import invesalius.gui.dialogs as dlg
  21 +from invesalius.pubsub import pub as Publisher
21 22 # TODO: Disconnect tracker when a new one is connected
22 23 # TODO: Test if there are too many prints when connection fails
23 24 # TODO: Redesign error messages. No point in having "Could not connect to default tracker" in all trackers
... ... @@ -223,42 +224,23 @@ def OptitrackTracker(tracker_id):
223 224 print('#####')
224 225 return trck_init, lib_mode
225 226  
226   -def ElfinRobot(robot_IP):
227   - trck_init = None
228   - try:
229   - import invesalius.data.elfin as elfin
230   - print("Trying to connect Robot via: ", robot_IP)
231   - trck_init = elfin.Elfin_Server(robot_IP, const.ROBOT_ElFIN_PORT)
232   - trck_init.Initialize()
233   - lib_mode = 'wrapper'
234   - print('Connect to elfin robot tracking device.')
235   -
236   - except:
237   - lib_mode = 'error'
238   - trck_init = None
239   - print('Could not connect to elfin robot tracker.')
240   -
241   - # return tracker initialization variable and type of connection
242   - return trck_init, lib_mode
243   -
244 227 def RobotTracker(tracker_id):
245 228 from wx import ID_OK
246   - trck_init = None
247   - trck_init_robot = None
248   - tracker_id = None
  229 +
249 230 dlg_device = dlg.SetTrackerDeviceToRobot()
250 231 if dlg_device.ShowModal() == ID_OK:
251 232 tracker_id = dlg_device.GetValue()
252 233 if tracker_id:
253   - trck_connection = TrackerConnection(tracker_id, None, 'connect')
254   - if trck_connection[0]:
  234 + trck_init = TrackerConnection(tracker_id, None, 'connect')
  235 + if trck_init[0]:
255 236 dlg_ip = dlg.SetRobotIP()
256 237 if dlg_ip.ShowModal() == ID_OK:
257 238 robot_IP = dlg_ip.GetValue()
258   - trck_init = trck_connection
259   - trck_init_robot = ElfinRobot(robot_IP)
  239 + Publisher.sendMessage('Connect to robot', robot_IP=robot_IP)
  240 +
  241 + return trck_init, tracker_id
260 242  
261   - return [(trck_init, trck_init_robot), tracker_id]
  243 + return None, None
262 244  
263 245 def DebugTrackerRandom(tracker_id):
264 246 trck_init = True
... ... @@ -392,8 +374,9 @@ def DisconnectTracker(tracker_id, trck_init):
392 374 lib_mode = 'serial'
393 375 print('Tracker disconnected.')
394 376 elif tracker_id == const.ROBOT:
395   - trck_init[0][0].Close()
396   - trck_init[1][0].Close()
  377 + Publisher.sendMessage('Reset robot', data=None)
  378 + if trck_init[1] == const.DEBUGTRACKRANDOM or trck_init[1] == const.DEBUGTRACKAPPROACH:
  379 + trck_init[0].Close()
397 380 trck_init = False
398 381 lib_mode = 'wrapper'
399 382 print('Tracker disconnected.')
... ...
invesalius/gui/dialogs.py
... ... @@ -57,8 +57,6 @@ except ImportError:
57 57 from wx import AboutDialogInfo, AboutBox
58 58  
59 59 import invesalius.constants as const
60   -import invesalius.data.coordinates as dco
61   -import invesalius.data.transformations as tr
62 60 import invesalius.gui.widgets.gradient as grad
63 61 import invesalius.session as ses
64 62 import invesalius.utils as utils
... ... @@ -3597,11 +3595,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3597 3595 # and not change the function to the point of potentially breaking it.)
3598 3596 #
3599 3597 if self.obj_ref_id and fiducial_index == 4:
3600   - if self.tracker_id == const.ROBOT:
3601   - trck_init_robot = self.trk_init[1][0]
3602   - coord = trck_init_robot.Run()
3603   - else:
3604   - coord = coord_raw[self.obj_ref_id, :]
  3598 + coord = coord_raw[self.obj_ref_id, :]
3605 3599 else:
3606 3600 coord = coord_raw[0, :]
3607 3601  
... ... @@ -4323,7 +4317,7 @@ class SetTrackerDeviceToRobot(wx.Dialog):
4323 4317 def _init_gui(self):
4324 4318 # ComboBox for spatial tracker device selection
4325 4319 tooltip = wx.ToolTip(_("Choose the tracking device"))
4326   - trackers = const.TRACKERS
  4320 + trackers = const.TRACKERS.copy()
4327 4321 if not ses.Session().debug:
4328 4322 del trackers[-3:]
4329 4323 tracker_options = [_("Select tracker:")] + trackers
... ... @@ -4425,10 +4419,8 @@ class CreateTransformationMatrixRobot(wx.Dialog):
4425 4419 M_robot_2_tracker is created by an affine transformation. Robot TCP should be calibrated to the center of the tracker marker
4426 4420 '''
4427 4421 #TODO: make aboutbox
4428   - self.tracker_coord = []
4429   - self.tracker_angles = []
4430   - self.robot_coord = []
4431   - self.robot_angles = []
  4422 + self.matrix_tracker_to_robot = []
  4423 + self.robot_status = False
4432 4424  
4433 4425 self.tracker = tracker
4434 4426  
... ... @@ -4470,6 +4462,8 @@ class CreateTransformationMatrixRobot(wx.Dialog):
4470 4462  
4471 4463 btn_load = wx.Button(self, -1, label=_('Load'), size=wx.Size(65, 23))
4472 4464 btn_load.Bind(wx.EVT_BUTTON, self.OnLoadReg)
  4465 + btn_load.Enable(False)
  4466 + self.btn_load = btn_load
4473 4467  
4474 4468 # Create a horizontal sizers
4475 4469 border = 1
... ... @@ -4524,16 +4518,17 @@ class CreateTransformationMatrixRobot(wx.Dialog):
4524 4518 main_sizer.Fit(self)
4525 4519  
4526 4520 self.CenterOnParent()
  4521 + self.__bind_events()
4527 4522  
4528   - def affine_correg(self, tracker, robot):
4529   - m_change = tr.affine_matrix_from_points(robot[:].T, tracker[:].T,
4530   - shear=False, scale=False, usesvd=False)
4531   - return m_change
  4523 + def __bind_events(self):
  4524 + Publisher.subscribe(self.OnUpdateTransformationMatrix, 'Update robot transformation matrix')
  4525 + Publisher.subscribe(self.OnCoordinatesAdquired, 'Coordinates for the robot transformation matrix collected')
  4526 + Publisher.subscribe(self.OnRobotConnectionStatus, 'Robot connection status')
4532 4527  
4533 4528 def OnContinuousAcquisition(self, evt=None, btn=None):
4534 4529 value = btn.GetValue()
4535 4530 if value:
4536   - self.timer.Start(500)
  4531 + self.timer.Start(100)
4537 4532 else:
4538 4533 self.timer.Stop()
4539 4534  
... ... @@ -4541,56 +4536,50 @@ class CreateTransformationMatrixRobot(wx.Dialog):
4541 4536 self.OnCreatePoint(evt=None)
4542 4537  
4543 4538 def OnCreatePoint(self, evt):
4544   - coord_raw, markers_flag = self.tracker.TrackerCoordinates.GetCoordinates()
4545   - #robot thread is not initialized yet
4546   - coord_raw_robot = self.tracker.trk_init[0][1][0].Run()
4547   - coord_raw_tracker_obj = coord_raw[3]
4548   -
4549   - if markers_flag[2]:
4550   - self.tracker_coord.append(coord_raw_tracker_obj[:3])
4551   - self.tracker_angles.append(coord_raw_tracker_obj[3:])
4552   - self.robot_coord.append(coord_raw_robot[:3])
4553   - self.robot_angles.append(coord_raw_robot[3:])
4554   - self.txt_number.SetLabel(str(int(self.txt_number.GetLabel())+1))
4555   - else:
4556   - print('Cannot detect the coil markers, pls try again')
  4539 + Publisher.sendMessage('Collect coordinates for the robot transformation matrix', data=None)
  4540 +
  4541 + def OnCoordinatesAdquired(self):
  4542 + self.txt_number.SetLabel(str(int(self.txt_number.GetLabel())+1))
4557 4543  
4558   - if len(self.tracker_coord) >= 3:
  4544 + if self.robot_status and int(self.txt_number.GetLabel()) >= 3:
4559 4545 self.btn_apply_reg.Enable(True)
4560 4546  
  4547 + def OnRobotConnectionStatus(self, data):
  4548 + self.robot_status = data
  4549 + if self.robot_status:
  4550 + self.btn_load.Enable(True)
  4551 + if int(self.txt_number.GetLabel()) >= 3:
  4552 + self.btn_apply_reg.Enable(True)
  4553 +
4561 4554 def OnReset(self, evt):
  4555 + Publisher.sendMessage('Reset coordinates collection for the robot transformation matrix', data=None)
4562 4556 if self.btn_cont_point:
4563 4557 self.btn_cont_point.SetValue(False)
4564 4558 self.OnContinuousAcquisition(evt=None, btn=self.btn_cont_point)
4565 4559  
4566   - self.tracker_coord = []
4567   - self.tracker_angles = []
4568   - self.robot_coord = []
4569   - self.robot_angles = []
4570   - self.M_tracker_2_robot = []
4571 4560 self.txt_number.SetLabel('0')
4572 4561  
4573 4562 self.btn_apply_reg.Enable(False)
4574 4563 self.btn_save.Enable(False)
4575 4564 self.btn_ok.Enable(False)
4576 4565  
  4566 + self.matrix_tracker_to_robot = []
  4567 +
4577 4568 def OnApply(self, evt):
4578 4569 if self.btn_cont_point:
4579 4570 self.btn_cont_point.SetValue(False)
4580 4571 self.OnContinuousAcquisition(evt=None, btn=self.btn_cont_point)
4581 4572  
4582   - tracker_coord = np.array(self.tracker_coord)
4583   - robot_coord = np.array(self.robot_coord)
4584   -
4585   - M_robot_2_tracker = self.affine_correg(tracker_coord, robot_coord)
4586   - M_tracker_2_robot = tr.inverse_matrix(M_robot_2_tracker)
4587   - self.M_tracker_2_robot = M_tracker_2_robot
  4573 + Publisher.sendMessage('Robot transformation matrix estimation', data=None)
4588 4574  
4589 4575 self.btn_save.Enable(True)
4590 4576 self.btn_ok.Enable(True)
4591 4577  
4592 4578 #TODO: make a colored circle to sinalize that the transformation was made (green) (red if not)
4593 4579  
  4580 + def OnUpdateTransformationMatrix(self, data):
  4581 + self.matrix_tracker_to_robot = np.array(data)
  4582 +
4594 4583 def OnSaveReg(self, evt):
4595 4584 filename = ShowLoadSaveDialog(message=_(u"Save robot transformation file as..."),
4596 4585 wildcard=_("Robot transformation files (*.rbtf)|*.rbtf"),
... ... @@ -4598,10 +4587,10 @@ class CreateTransformationMatrixRobot(wx.Dialog):
4598 4587 default_filename="robottransform.rbtf", save_ext="rbtf")
4599 4588  
4600 4589 if filename:
4601   - if self.M_tracker_2_robot is not None:
  4590 + if self.matrix_tracker_to_robot is not None:
4602 4591 with open(filename, 'w', newline='') as file:
4603 4592 writer = csv.writer(file, delimiter='\t')
4604   - writer.writerows(self.M_tracker_2_robot)
  4593 + writer.writerows(np.vstack(self.matrix_tracker_to_robot).tolist())
4605 4594  
4606 4595 def OnLoadReg(self, evt):
4607 4596 filename = ShowLoadSaveDialog(message=_(u"Load robot transformation"),
... ... @@ -4611,12 +4600,12 @@ class CreateTransformationMatrixRobot(wx.Dialog):
4611 4600 reader = csv.reader(file, delimiter='\t')
4612 4601 content = [row for row in reader]
4613 4602  
4614   - self.M_tracker_2_robot = np.vstack(list(np.float_(content)))
4615   - print("Matrix tracker to robot:", self.M_tracker_2_robot)
4616   - self.btn_ok.Enable(True)
  4603 + self.matrix_tracker_to_robot = np.vstack(list(np.float_(content)))
  4604 + print("Matrix tracker to robot:", self.matrix_tracker_to_robot)
  4605 + Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist())
  4606 + if self.robot_status:
  4607 + self.btn_ok.Enable(True)
4617 4608  
4618   - def GetValue(self):
4619   - return self.M_tracker_2_robot
4620 4609  
4621 4610 class SetNDIconfigs(wx.Dialog):
4622 4611 def __init__(self, title=_("Setting NDI polaris configs:")):
... ...
invesalius/gui/task_navigator.py
... ... @@ -20,10 +20,7 @@
20 20 import dataclasses
21 21 from functools import partial
22 22 import itertools
23   -import csv
24   -import queue
25 23 import time
26   -import threading
27 24  
28 25 import nibabel as nb
29 26 import numpy as np
... ... @@ -41,7 +38,6 @@ except ImportError:
41 38 has_robot = False
42 39  
43 40 import wx
44   -import vtk
45 41  
46 42 try:
47 43 import wx.lib.agw.foldpanelbar as fpb
... ... @@ -51,7 +47,6 @@ except ImportError:
51 47 import wx.lib.colourselect as csel
52 48 import wx.lib.masked.numctrl
53 49 from invesalius.pubsub import pub as Publisher
54   -from time import sleep
55 50  
56 51 import invesalius.constants as const
57 52  
... ... @@ -64,6 +59,7 @@ import invesalius.data.tractography as dti
64 59 import invesalius.data.record_coords as rec
65 60 import invesalius.data.vtk_utils as vtk_utils
66 61 import invesalius.data.bases as db
  62 +import invesalius.data.coregistration as dcr
67 63 import invesalius.gui.dialogs as dlg
68 64 import invesalius.project as prj
69 65 import invesalius.session as ses
... ... @@ -73,7 +69,6 @@ from invesalius.gui import utils as gui_utils
73 69 from invesalius.navigation.icp import ICP
74 70 from invesalius.navigation.navigation import Navigation
75 71 from invesalius.navigation.tracker import Tracker
76   -from invesalius.navigation.robot import Robot
77 72 from invesalius.data.converters import to_vtk
78 73  
79 74 from invesalius.net.neuronavigation_api import NeuronavigationApi
... ... @@ -176,7 +171,7 @@ class InnerFoldPanel(wx.Panel):
176 171 #
177 172 tracker = Tracker()
178 173 pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None
179   -
  174 + icp = ICP()
180 175 neuronavigation_api = NeuronavigationApi()
181 176 navigation = Navigation(
182 177 pedal_connection=pedal_connection,
... ... @@ -194,6 +189,7 @@ class InnerFoldPanel(wx.Panel):
194 189 parent=item,
195 190 navigation=navigation,
196 191 tracker=tracker,
  192 + icp=icp,
197 193 pedal_connection=pedal_connection,
198 194 neuronavigation_api=neuronavigation_api,
199 195 )
... ... @@ -213,7 +209,7 @@ class InnerFoldPanel(wx.Panel):
213 209  
214 210 # Fold 3 - Markers panel
215 211 item = fold_panel.AddFoldPanel(_("Markers"), collapsed=True)
216   - mtw = MarkersPanel(item, navigation, tracker)
  212 + mtw = MarkersPanel(item, navigation, tracker, icp)
217 213  
218 214 fold_panel.ApplyCaptionStyle(item, style)
219 215 fold_panel.AddFoldPanelWindow(item, mtw, spacing= 0,
... ... @@ -352,7 +348,7 @@ class InnerFoldPanel(wx.Panel):
352 348  
353 349  
354 350 class NeuronavigationPanel(wx.Panel):
355   - def __init__(self, parent, navigation, tracker, pedal_connection, neuronavigation_api):
  351 + def __init__(self, parent, navigation, tracker, icp, pedal_connection, neuronavigation_api):
356 352 wx.Panel.__init__(self, parent)
357 353 try:
358 354 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
... ... @@ -369,9 +365,8 @@ class NeuronavigationPanel(wx.Panel):
369 365 self.neuronavigation_api = neuronavigation_api
370 366  
371 367 self.navigation = navigation
372   - self.icp = ICP()
  368 + self.icp = icp
373 369 self.tracker = tracker
374   - self.robot = Robot(tracker)
375 370  
376 371 self.nav_status = False
377 372 self.tracker_fiducial_being_set = None
... ... @@ -558,6 +553,7 @@ class NeuronavigationPanel(wx.Panel):
558 553 Publisher.subscribe(self.UpdateTarget, 'Update target')
559 554 Publisher.subscribe(self.OnStartNavigation, 'Start navigation')
560 555 Publisher.subscribe(self.OnStopNavigation, 'Stop navigation')
  556 + Publisher.subscribe(self.OnDialogRobotDestroy, 'Dialog robot destroy')
561 557  
562 558 def LoadImageFiducials(self, label, coord):
563 559 fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'label', label)
... ... @@ -671,8 +667,6 @@ class NeuronavigationPanel(wx.Panel):
671 667 self.checkbox_icp.SetValue(False)
672 668  
673 669 def OnDisconnectTracker(self):
674   - if self.tracker.tracker_id == const.ROBOT:
675   - self.robot.StopRobotThreadNavigation()
676 670 self.tracker.DisconnectTracker()
677 671 self.ResetICP()
678 672 self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre)
... ... @@ -691,7 +685,12 @@ class NeuronavigationPanel(wx.Panel):
691 685  
692 686 self.tracker.SetTracker(choice)
693 687 if self.tracker.tracker_id == const.ROBOT:
694   - self.tracker.ConnectToRobot(self.navigation, self.tracker, self.robot)
  688 + self.dlg_correg_robot = dlg.CreateTransformationMatrixRobot(self.tracker)
  689 + if self.dlg_correg_robot.ShowModal() == wx.ID_OK:
  690 + Publisher.sendMessage('Robot navigation mode', robot_mode=True)
  691 + else:
  692 + Publisher.sendMessage('Disconnect tracker')
  693 + wx.MessageBox(_("Not possible to connect to the robot."), _("InVesalius 3"))
695 694  
696 695 self.ResetICP()
697 696 self.tracker.UpdateUI(ctrl, self.numctrls_fiducial[3:6], self.txtctrl_fre)
... ... @@ -776,8 +775,8 @@ class NeuronavigationPanel(wx.Panel):
776 775  
777 776 self.navigation.StopNavigation()
778 777 if self.tracker.tracker_id == const.ROBOT:
779   - Publisher.sendMessage('Robot target matrix', robot_tracker_flag=False,
780   - m_change_robot_to_head=None)
  778 + Publisher.sendMessage('Update robot target', robot_tracker_flag=False,
  779 + target_index=None, target=None)
781 780  
782 781 # Enable all navigation buttons
783 782 choice_ref.Enable(True)
... ... @@ -786,6 +785,10 @@ class NeuronavigationPanel(wx.Panel):
786 785 for btn_c in self.btns_set_fiducial:
787 786 btn_c.Enable(True)
788 787  
  788 + def OnDialogRobotDestroy(self):
  789 + if self.dlg_correg_robot:
  790 + self.dlg_correg_robot.Destroy()
  791 +
789 792 def CheckFiducialRegistrationError(self):
790 793 self.navigation.UpdateFiducialRegistrationError(self.tracker)
791 794 fre, fre_ok = self.navigation.GetFiducialRegistrationError(self.icp)
... ... @@ -820,7 +823,7 @@ class NeuronavigationPanel(wx.Panel):
820 823 for btn_c in self.btns_set_fiducial:
821 824 btn_c.Enable(False)
822 825  
823   - self.navigation.StartNavigation(self.tracker)
  826 + self.navigation.EstimateTrackerToInVTransformationMatrix(self.tracker)
824 827  
825 828 if not self.CheckFiducialRegistrationError():
826 829 # TODO: Exhibit FRE in a warning dialog and only starts navigation after user clicks ok
... ... @@ -830,9 +833,11 @@ class NeuronavigationPanel(wx.Panel):
830 833 if self.icp.use_icp:
831 834 self.checkbox_icp.Enable(True)
832 835 self.checkbox_icp.SetValue(True)
833   - # Update FRE once more after starting the navigation, due to the optional use of ICP,
834   - # which improves FRE.
835   - self.CheckFiducialRegistrationError()
  836 + # Update FRE once more after starting the navigation, due to the optional use of ICP,
  837 + # which improves FRE.
  838 + self.CheckFiducialRegistrationError()
  839 +
  840 + self.navigation.StartNavigation(self.tracker)
836 841  
837 842 def OnNavigate(self, evt, btn_nav):
838 843 select_tracker_elem = self.select_tracker_elem
... ... @@ -881,7 +886,6 @@ class NeuronavigationPanel(wx.Panel):
881 886 )
882 887 self.tracker.__init__()
883 888 self.icp.__init__()
884   - self.robot.__init__(self.tracker)
885 889  
886 890  
887 891 class ObjectRegistrationPanel(wx.Panel):
... ... @@ -1248,20 +1252,7 @@ class MarkersPanel(wx.Panel):
1248 1252 if field.type is bool:
1249 1253 setattr(self, field.name, str_val=='True')
1250 1254  
1251   - @dataclasses.dataclass
1252   - class Robot_Marker:
1253   - """Class for storing robot target."""
1254   - m_robot_target : list = None
1255   -
1256   - @property
1257   - def robot_target_matrix(self):
1258   - return self.m_robot_target
1259   -
1260   - @robot_target_matrix.setter
1261   - def robot_target_matrix(self, new_m_robot_target):
1262   - self.m_robot_target = new_m_robot_target
1263   -
1264   - def __init__(self, parent, navigation, tracker):
  1255 + def __init__(self, parent, navigation, tracker, icp):
1265 1256 wx.Panel.__init__(self, parent)
1266 1257 try:
1267 1258 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
... ... @@ -1273,6 +1264,7 @@ class MarkersPanel(wx.Panel):
1273 1264  
1274 1265 self.navigation = navigation
1275 1266 self.tracker = tracker
  1267 + self.icp = icp
1276 1268  
1277 1269 self.__bind_events()
1278 1270  
... ... @@ -1280,11 +1272,9 @@ class MarkersPanel(wx.Panel):
1280 1272  
1281 1273 self.current_coord = [0, 0, 0, None, None, None]
1282 1274 self.current_seed = 0, 0, 0
1283   - self.current_robot_target_matrix = [None] * 9
  1275 +
1284 1276 self.markers = []
1285   - self.robot_markers = []
1286 1277 self.nav_status = False
1287   - self.raw_target_robot = None, None
1288 1278  
1289 1279 self.marker_colour = const.MARKER_COLOUR
1290 1280 self.marker_size = const.MARKER_SIZE
... ... @@ -1383,7 +1373,6 @@ class MarkersPanel(wx.Panel):
1383 1373 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
1384 1374 Publisher.subscribe(self.UpdateSeedCoordinates, 'Update tracts')
1385 1375 Publisher.subscribe(self.OnChangeCurrentSession, 'Current session changed')
1386   - Publisher.subscribe(self.UpdateRobotCoordinates, 'Update raw coordinates')
1387 1376  
1388 1377 def __find_target_marker(self):
1389 1378 """
... ... @@ -1417,7 +1406,6 @@ class MarkersPanel(wx.Panel):
1417 1406 """
1418 1407 for i in reversed(index):
1419 1408 del self.markers[i]
1420   - del self.robot_markers[i]
1421 1409 self.lc.DeleteItem(i)
1422 1410 for n in range(0, self.lc.GetItemCount()):
1423 1411 self.lc.SetItem(n, 0, str(n + 1))
... ... @@ -1470,34 +1458,37 @@ class MarkersPanel(wx.Panel):
1470 1458 def UpdateSeedCoordinates(self, root=None, affine_vtk=None, coord_offset=(0, 0, 0), coord_offset_w=(0, 0, 0)):
1471 1459 self.current_seed = coord_offset_w
1472 1460  
1473   - def UpdateRobotCoordinates(self, coordinates_raw, markers_flag):
1474   - self.raw_target_robot = coordinates_raw[1], coordinates_raw[2]
1475   -
1476 1461 def OnMouseRightDown(self, evt):
1477 1462 # TODO: Enable the "Set as target" only when target is created with registered object
1478 1463 menu_id = wx.Menu()
1479 1464 edit_id = menu_id.Append(0, _('Edit label'))
1480 1465 menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id)
1481   - color_id = menu_id.Append(2, _('Edit color'))
  1466 + color_id = menu_id.Append(1, _('Edit color'))
1482 1467 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id)
1483 1468 menu_id.AppendSeparator()
1484   - target_menu = menu_id.Append(1, _('Set as target'))
  1469 + target_menu = menu_id.Append(2, _('Set as target'))
1485 1470 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetTarget, target_menu)
1486 1471 menu_id.AppendSeparator()
1487   - send_target_to_robot = menu_id.Append(3, _('Send target to robot'))
1488   - menu_id.Bind(wx.EVT_MENU, self.OnMenuSendTargetToRobot, send_target_to_robot)
1489 1472  
1490   - if all([elem is not None for elem in self.markers[self.lc.GetFocusedItem()].coord[3:]]):
  1473 + check_target_angles = all([elem is not None for elem in self.markers[self.lc.GetFocusedItem()].coord[3:]])
  1474 + # Enable "Send target to robot" button only if tracker is robot, if navigation is on and if target is not none
  1475 + if self.tracker.tracker_id == const.ROBOT:
  1476 + send_target_to_robot_compensation = menu_id.Append(3, _('Sets target to robot head move compensation'))
  1477 + menu_id.Bind(wx.EVT_MENU, self.OnMenuSetRobotCompensation, send_target_to_robot_compensation)
  1478 + send_target_to_robot = menu_id.Append(4, _('Send target from InVesalius to robot'))
  1479 + menu_id.Bind(wx.EVT_MENU, self.OnMenuSendTargetToRobot, send_target_to_robot)
  1480 + if self.nav_status and check_target_angles:
  1481 + send_target_to_robot_compensation.Enable(True)
  1482 + send_target_to_robot.Enable(True)
  1483 + else:
  1484 + send_target_to_robot_compensation.Enable(False)
  1485 + send_target_to_robot.Enable(False)
  1486 +
  1487 + if check_target_angles:
1491 1488 target_menu.Enable(True)
1492 1489 else:
1493 1490 target_menu.Enable(False)
1494 1491  
1495   - # Enable "Send target to robot" button only if tracker is robot, if navigation is on and if target is not none
1496   - m_target_robot = np.array([self.robot_markers[self.lc.GetFocusedItem()].robot_target_matrix])
1497   - if self.tracker.tracker_id == const.ROBOT and self.nav_status and m_target_robot.any():
1498   - send_target_to_robot.Enable(True)
1499   - else:
1500   - send_target_to_robot.Enable(False)
1501 1492 # TODO: Create the remove target option so the user can disable the target without removing the marker
1502 1493 # target_menu_rem = menu_id.Append(3, _('Remove target'))
1503 1494 # menu_id.Bind(wx.EVT_MENU, self.OnMenuRemoveTarget, target_menu_rem)
... ... @@ -1547,14 +1538,33 @@ class MarkersPanel(wx.Panel):
1547 1538  
1548 1539 Publisher.sendMessage('Set new color', index=index, color=color_new)
1549 1540  
  1541 + def OnMenuSetRobotCompensation(self, evt):
  1542 + if isinstance(evt, int):
  1543 + self.lc.Focus(evt)
  1544 +
  1545 + Publisher.sendMessage('Reset robot process', data=None)
  1546 + matrix_tracker_fiducials = self.tracker.GetMatrixTrackerFiducials()
  1547 + Publisher.sendMessage('Update tracker fiducials matrix',
  1548 + matrix_tracker_fiducials=matrix_tracker_fiducials)
  1549 + Publisher.sendMessage('Update robot target', robot_tracker_flag=True, target_index=self.lc.GetFocusedItem(), target=None)
  1550 +
1550 1551 def OnMenuSendTargetToRobot(self, evt):
1551 1552 if isinstance(evt, int):
1552 1553 self.lc.Focus(evt)
  1554 + index = self.lc.GetFocusedItem()
  1555 + if index == -1:
  1556 + wx.MessageBox(_("No data selected."), _("InVesalius 3"))
  1557 + return
  1558 +
  1559 + Publisher.sendMessage('Reset robot process', data=None)
  1560 + matrix_tracker_fiducials = self.tracker.GetMatrixTrackerFiducials()
  1561 + Publisher.sendMessage('Update tracker fiducials matrix',
  1562 + matrix_tracker_fiducials=matrix_tracker_fiducials)
1553 1563  
1554   - m_target_robot = self.robot_markers[self.lc.GetFocusedItem()].robot_target_matrix
  1564 + target_coord = self.markers[index].coord[:3]
  1565 + target = dcr.image_to_tracker(self.navigation.m_change, target_coord, self.icp)
1555 1566  
1556   - Publisher.sendMessage('Reset robot process')
1557   - Publisher.sendMessage('Robot target matrix', robot_tracker_flag=True, m_change_robot_to_head=m_target_robot)
  1567 + Publisher.sendMessage('Update robot target', robot_tracker_flag=True, target_index=self.lc.GetFocusedItem(), target=target.tolist())
1558 1568  
1559 1569 def OnDeleteAllMarkers(self, evt=None):
1560 1570 if evt is not None:
... ... @@ -1566,9 +1576,11 @@ class MarkersPanel(wx.Panel):
1566 1576 Publisher.sendMessage('Disable or enable coil tracker', status=False)
1567 1577 if evt is not None:
1568 1578 wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
  1579 + if self.tracker.tracker_id == const.ROBOT:
  1580 + Publisher.sendMessage('Update robot target', robot_tracker_flag=False,
  1581 + target_index=None, target=None)
1569 1582  
1570 1583 self.markers = []
1571   - self.robot_markers = []
1572 1584 Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount())
1573 1585 self.lc.DeleteAllItems()
1574 1586 Publisher.sendMessage('Stop Blink Marker', index='DeleteAll')
... ... @@ -1596,8 +1608,8 @@ class MarkersPanel(wx.Panel):
1596 1608 if self.__find_target_marker() in index:
1597 1609 Publisher.sendMessage('Disable or enable coil tracker', status=False)
1598 1610 if self.tracker.tracker_id == const.ROBOT:
1599   - Publisher.sendMessage('Robot target matrix', robot_tracker_flag=False,
1600   - m_change_robot_to_head=[])
  1611 + Publisher.sendMessage('Update robot target', robot_tracker_flag=False,
  1612 + target_index=None, target=None)
1601 1613 wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
1602 1614  
1603 1615 self.__delete_multiple_markers(index)
... ... @@ -1702,12 +1714,11 @@ class MarkersPanel(wx.Panel):
1702 1714 new_marker.session_id = session_id or self.current_session
1703 1715  
1704 1716 if self.tracker.tracker_id == const.ROBOT and self.nav_status:
1705   - self.current_robot_target_matrix = db.compute_robot_to_head_matrix(self.raw_target_robot)
  1717 + current_head_robot_target_status = True
1706 1718 else:
1707   - self.current_robot_target_matrix = [None] * 9
  1719 + current_head_robot_target_status = False
1708 1720  
1709   - new_robot_marker = self.Robot_Marker()
1710   - new_robot_marker.robot_target_matrix = self.current_robot_target_matrix
  1721 + Publisher.sendMessage('Add marker to robot control', data=current_head_robot_target_status)
1711 1722  
1712 1723 # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added
1713 1724 if all([elem is not None for elem in new_marker.coord[3:]]):
... ... @@ -1723,7 +1734,6 @@ class MarkersPanel(wx.Panel):
1723 1734  
1724 1735  
1725 1736 self.markers.append(new_marker)
1726   - self.robot_markers.append(new_robot_marker)
1727 1737  
1728 1738 # Add item to list control in panel
1729 1739 num_items = self.lc.GetItemCount()
... ...
invesalius/navigation/icp.py
... ... @@ -21,10 +21,10 @@ import wx
21 21  
22 22 import invesalius.data.bases as db
23 23 import invesalius.gui.dialogs as dlg
24   -from invesalius.pubsub import pub as Publisher
25 24  
  25 +from invesalius.utils import Singleton
26 26  
27   -class ICP():
  27 +class ICP(metaclass=Singleton):
28 28 def __init__(self):
29 29 self.use_icp = False
30 30 self.m_icp = None
... ... @@ -35,7 +35,6 @@ class ICP():
35 35  
36 36 if not self.use_icp:
37 37 if dlg.ICPcorregistration(navigation.fre):
38   - Publisher.sendMessage('Stop navigation')
39 38 use_icp, self.m_icp = self.OnICP(navigation, tracker, navigation.m_change)
40 39 if use_icp:
41 40 self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials,
... ... @@ -43,7 +42,6 @@ class ICP():
43 42 self.SetICP(navigation, use_icp)
44 43 else:
45 44 print("ICP canceled")
46   - Publisher.sendMessage('Start navigation')
47 45  
48 46 def OnICP(self, navigation, tracker, m_change):
49 47 ref_mode_id = navigation.GetReferenceMode()
... ...
invesalius/navigation/navigation.py
... ... @@ -34,6 +34,7 @@ import invesalius.data.tractography as dti
34 34 import invesalius.data.transformations as tr
35 35 import invesalius.data.vtk_utils as vtk_utils
36 36 from invesalius.pubsub import pub as Publisher
  37 +from invesalius.utils import Singleton
37 38  
38 39  
39 40 class QueueCustom(queue.Queue):
... ... @@ -83,7 +84,7 @@ class UpdateNavigationScene(threading.Thread):
83 84  
84 85 threading.Thread.__init__(self, name='UpdateScene')
85 86 self.serial_port_enabled, self.view_tracts, self.peel_loaded = vis_components
86   - self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue, self.robot_target_queue = vis_queues
  87 + self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue = vis_queues
87 88 self.sle = sle
88 89 self.event = event
89 90 self.neuronavigation_api = neuronavigation_api
... ... @@ -93,7 +94,7 @@ class UpdateNavigationScene(threading.Thread):
93 94 while not self.event.is_set():
94 95 got_coords = False
95 96 try:
96   - coord, [coordinates_raw, markers_flag], m_img, view_obj = self.coord_queue.get_nowait()
  97 + coord, markers_flag, m_img, view_obj = self.coord_queue.get_nowait()
97 98 got_coords = True
98 99  
99 100 # print('UpdateScene: get {}'.format(count))
... ... @@ -117,7 +118,6 @@ class UpdateNavigationScene(threading.Thread):
117 118 # see the red cross in the position of the offset marker
118 119 wx.CallAfter(Publisher.sendMessage, 'Update slices position', position=coord[:3])
119 120 wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord)
120   - wx.CallAfter(Publisher.sendMessage, 'Update raw coordinates', coordinates_raw=coordinates_raw, markers_flag=markers_flag)
121 121 wx.CallAfter(Publisher.sendMessage, 'Update slice viewer')
122 122 wx.CallAfter(Publisher.sendMessage, 'Sensor ID', markers_flag=markers_flag)
123 123  
... ... @@ -140,7 +140,7 @@ class UpdateNavigationScene(threading.Thread):
140 140 self.coord_queue.task_done()
141 141  
142 142  
143   -class Navigation():
  143 +class Navigation(metaclass=Singleton):
144 144 def __init__(self, pedal_connection, neuronavigation_api):
145 145 self.pedal_connection = pedal_connection
146 146 self.neuronavigation_api = neuronavigation_api
... ... @@ -157,7 +157,6 @@ class Navigation():
157 157 self.coord_queue = QueueCustom(maxsize=1)
158 158 self.icp_queue = QueueCustom(maxsize=1)
159 159 self.object_at_target_queue = QueueCustom(maxsize=1)
160   - self.robot_target_queue = QueueCustom(maxsize=1)
161 160 # self.visualization_queue = QueueCustom(maxsize=1)
162 161 self.serial_port_queue = QueueCustom(maxsize=1)
163 162 self.coord_tracts_queue = QueueCustom(maxsize=1)
... ... @@ -245,9 +244,14 @@ class Navigation():
245 244 if state and permission_to_stimulate:
246 245 self.serial_port_connection.SendPulse()
247 246  
248   - def StartNavigation(self, tracker):
  247 + def EstimateTrackerToInVTransformationMatrix(self, tracker):
249 248 tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()
  249 + self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])
250 250  
  251 + self.m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T,
  252 + shear=False, scale=False)
  253 +
  254 + def StartNavigation(self, tracker):
251 255 # initialize jobs list
252 256 jobs_list = []
253 257  
... ... @@ -255,17 +259,9 @@ class Navigation():
255 259 self.event.clear()
256 260  
257 261 vis_components = [self.serial_port_in_use, self.view_tracts, self.peel_loaded]
258   - vis_queues = [self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue, self.robot_target_queue]
  262 + vis_queues = [self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue]
259 263  
260 264 Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components)
261   -
262   - self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])
263   -
264   - # fiducials matrix
265   - m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T,
266   - shear=False, scale=False)
267   - self.m_change = m_change
268   -
269 265 errors = False
270 266  
271 267 if self.track_obj:
... ... @@ -279,14 +275,14 @@ class Navigation():
279 275 # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix
280 276 obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg
281 277  
282   - coreg_data = [m_change, obj_ref_mode]
  278 + coreg_data = [self.m_change, obj_ref_mode]
283 279  
284 280 if self.ref_mode_id:
285 281 coord_raw, markers_flag = tracker.TrackerCoordinates.GetCoordinates()
286 282 else:
287 283 coord_raw = np.array([None])
288 284  
289   - obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)
  285 + obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, self.m_change)
290 286 coreg_data.extend(obj_data)
291 287  
292 288 queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue, self.object_at_target_queue]
... ... @@ -295,7 +291,7 @@ class Navigation():
295 291 self.event, self.sleep_nav, tracker.tracker_id,
296 292 self.target))
297 293 else:
298   - coreg_data = (m_change, 0)
  294 + coreg_data = (self.m_change, 0)
299 295 queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue]
300 296 jobs_list.append(dcr.CoordinateCorregistrateNoObject(self.ref_mode_id, tracker, coreg_data,
301 297 self.view_tracts, queues,
... ...
invesalius/navigation/robot.py
... ... @@ -1,308 +0,0 @@
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   -import numpy as np
20   -import wx
21   -import queue
22   -import threading
23   -from time import sleep
24   -
25   -import invesalius.data.bases as db
26   -import invesalius.gui.dialogs as dlg
27   -import invesalius.constants as const
28   -from invesalius.pubsub import pub as Publisher
29   -
30   -try:
31   - import invesalius.data.elfin as elfin
32   - import invesalius.data.elfin_processing as elfin_process
33   - has_robot = True
34   -except ImportError:
35   - has_robot = False
36   -
37   -class Robot():
38   - def __init__(self, tracker):
39   - """
40   - Class to establish the connection between the robot and InVesalius
41   - :param tracker: tracker.py instance
42   - """
43   - self.tracker = tracker
44   - self.trk_init = None
45   - self.robot_target_queue = None
46   - self.object_at_target_queue = None
47   - self.process_tracker = None
48   -
49   - self.thread_robot = None
50   -
51   - self.robotcoordinates = RobotCoordinates()
52   -
53   - self.__bind_events()
54   -
55   - def __bind_events(self):
56   - Publisher.subscribe(self.OnSendCoordinates, 'Send coord to robot')
57   - Publisher.subscribe(self.OnUpdateRobotTargetMatrix, 'Robot target matrix')
58   - Publisher.subscribe(self.OnObjectTarget, 'Coil at target')
59   - Publisher.subscribe(self.OnResetProcessTracker, 'Reset robot process')
60   -
61   - def OnRobotConnection(self):
62   - if not self.tracker.trk_init[0][0][0] or not self.tracker.trk_init[0][1][0]:
63   - dlg.ShowNavigationTrackerWarning(self.tracker.tracker_id, self.tracker.trk_init[1])
64   - self.tracker.tracker_id = 0
65   - self.tracker.tracker_connected = False
66   - else:
67   - self.tracker.trk_init.append(self.robotcoordinates)
68   - self.process_tracker = elfin_process.TrackerProcessing()
69   - dlg_correg_robot = dlg.CreateTransformationMatrixRobot(self.tracker)
70   - if dlg_correg_robot.ShowModal() == wx.ID_OK:
71   - M_tracker_to_robot = dlg_correg_robot.GetValue()
72   - db.transform_tracker_to_robot.M_tracker_to_robot = M_tracker_to_robot
73   - self.robot_server = self.tracker.trk_init[0][1][0]
74   - self.trk_init = self.tracker.trk_init
75   - else:
76   - dlg.ShowNavigationTrackerWarning(self.tracker.tracker_id, 'disconnect')
77   - self.tracker.trk_init = None
78   - self.tracker.tracker_id = 0
79   - self.tracker.tracker_connected = False
80   -
81   - def StartRobotThreadNavigation(self, tracker, coord_queue):
82   - if tracker.event_robot.is_set():
83   - tracker.event_robot.clear()
84   - self.thread_robot = ControlRobot(self.trk_init, tracker, self.robotcoordinates,
85   - [coord_queue, self.robot_target_queue, self.object_at_target_queue],
86   - self.process_tracker, tracker.event_robot)
87   - self.thread_robot.start()
88   -
89   - def StopRobotThreadNavigation(self):
90   - self.thread_robot.join()
91   - self.OnResetProcessTracker()
92   -
93   - def OnResetProcessTracker(self):
94   - self.process_tracker.__init__()
95   -
96   - def OnSendCoordinates(self, coord):
97   - self.robot_server.SendCoordinates(coord)
98   -
99   - def OnUpdateRobotTargetMatrix(self, robot_tracker_flag, m_change_robot_to_head):
100   - try:
101   - self.robot_target_queue.put_nowait([robot_tracker_flag, m_change_robot_to_head])
102   - except queue.Full:
103   - pass
104   -
105   - def OnObjectTarget(self, state):
106   - try:
107   - if self.object_at_target_queue:
108   - self.object_at_target_queue.put_nowait(state)
109   - except queue.Full:
110   - pass
111   -
112   - def SetRobotQueues(self, queues):
113   - self.robot_target_queue, self.object_at_target_queue = queues
114   -
115   -
116   -class RobotCoordinates():
117   - """
118   - Class to set/get robot coordinates. Robot coordinates are acquired in ControlRobot thread.
119   - The class is required to avoid acquisition conflict with different threads (coordinates and navigation)
120   - """
121   - def __init__(self):
122   - self.coord = None
123   -
124   - def SetRobotCoordinates(self, coord):
125   - self.coord = coord
126   -
127   - def GetRobotCoordinates(self):
128   - return self.coord
129   -
130   -
131   -class ControlRobot(threading.Thread):
132   - def __init__(self, trck_init, tracker, robotcoordinates, queues, process_tracker, event_robot):
133   - """Class (threading) to perform the robot control.
134   - A distinguish thread is required to increase perform and separate robot control from navigation thread
135   - (safetly layer for robot positining).
136   -
137   - There is no GUI involved, them no Sleep is required
138   -
139   - :param trck_init: Initialization variable of tracking device and connection type. See tracker.py.
140   - :param tracker: tracker.py instance
141   - :param robotcoordinates: RobotCoordinates() instance
142   - :param queues: Queue instance that manage coordinates and robot target
143   - :param process_tracker: TrackerProcessing() instance from elfin_process
144   - :param event_robot: Threading event to ControlRobot when tasks is done
145   - """
146   - threading.Thread.__init__(self, name='ControlRobot')
147   -
148   - self.trck_init_robot = trck_init[0][1][0]
149   - self.trck_init_tracker = trck_init[0][0][0]
150   - self.trk_id = trck_init[1]
151   - self.tracker = tracker
152   - self.robotcoordinates = robotcoordinates
153   - self.robot_tracker_flag = False
154   - self.objattarget_flag = False
155   - self.target_flag = False
156   - self.m_change_robot_to_head = None
157   - self.coord_inv_old = None
158   - self.coord_queue = queues[0]
159   - self.robot_target_queue = queues[1]
160   - self.object_at_target_queue = queues[2]
161   - self.process_tracker = process_tracker
162   - self.event_robot = event_robot
163   - self.arc_motion_flag = False
164   - self.arc_motion_step_flag = None
165   - self.target_linear_out = None
166   - self.target_linear_in = None
167   - self.target_arc = None
168   - self.previous_robot_status = False
169   -
170   - def get_coordinates_from_tracker_devices(self):
171   - coord_robot_raw = self.trck_init_robot.Run()
172   - coord_robot = np.array(coord_robot_raw)
173   - coord_robot[3], coord_robot[5] = coord_robot[5], coord_robot[3]
174   - self.robotcoordinates.SetRobotCoordinates(coord_robot)
175   -
176   - coord_raw, markers_flag = self.tracker.TrackerCoordinates.GetCoordinates()
177   -
178   - return coord_raw, coord_robot_raw, markers_flag
179   -
180   - def robot_motion_reset(self):
181   - self.trck_init_robot.StopRobot()
182   - self.arc_motion_flag = False
183   - self.arc_motion_step_flag = const.ROBOT_MOTIONS["normal"]
184   -
185   - def robot_move_decision(self, distance_target, new_robot_coordinates, current_robot_coordinates, current_head_filtered):
186   - """
187   - There are two types of robot movements.
188   - We can imagine in two concentric spheres of different sizes. The inside sphere is to compensate for small head movements.
189   - It was named "normal" moves.
190   - The outside sphere is for the arc motion. The arc motion is a safety feature for long robot movements.
191   - Even for a new target or a sudden huge head movement.
192   - 1) normal:
193   - A linear move from the actual position until the target position.
194   - This movement just happens when move distance is below a threshold (const.ROBOT_ARC_THRESHOLD_DISTANCE)
195   - 2) arc motion:
196   - It can be divided into three parts.
197   - The first one represents the movement from the inner sphere to the outer sphere.
198   - The robot moves back using a radial move (it use the center of the head as a reference).
199   - The second step is the actual arc motion (along the outer sphere).
200   - A middle point, between the actual position and the target, is required.
201   - The last step is to make a linear move until the target (goes to the inner sphere)
202   -
203   - """
204   - #Check if the target is inside the working space
205   - if self.process_tracker.estimate_robot_target_length(new_robot_coordinates) < const.ROBOT_WORKING_SPACE:
206   - #Check the target distance to define the motion mode
207   - if distance_target < const.ROBOT_ARC_THRESHOLD_DISTANCE and not self.arc_motion_flag:
208   - self.trck_init_robot.SendCoordinates(new_robot_coordinates, const.ROBOT_MOTIONS["normal"])
209   -
210   - elif distance_target >= const.ROBOT_ARC_THRESHOLD_DISTANCE or self.arc_motion_flag:
211   - actual_point = current_robot_coordinates
212   - if not self.arc_motion_flag:
213   - head_center_coordinates = self.process_tracker.estimate_head_center(self.tracker,
214   - current_head_filtered).tolist()
215   -
216   - self.target_linear_out, self.target_arc = self.process_tracker.compute_arc_motion(current_robot_coordinates, head_center_coordinates,
217   - new_robot_coordinates)
218   - self.arc_motion_flag = True
219   - self.arc_motion_step_flag = const.ROBOT_MOTIONS["linear out"]
220   -
221   - if self.arc_motion_flag and self.arc_motion_step_flag == const.ROBOT_MOTIONS["linear out"]:
222   - coord = self.target_linear_out
223   - if np.allclose(np.array(actual_point), np.array(self.target_linear_out), 0, 1):
224   - self.arc_motion_step_flag = const.ROBOT_MOTIONS["arc"]
225   - coord = self.target_arc
226   -
227   - elif self.arc_motion_flag and self.arc_motion_step_flag == const.ROBOT_MOTIONS["arc"]:
228   - head_center_coordinates = self.process_tracker.estimate_head_center(self.tracker,
229   - current_head_filtered).tolist()
230   -
231   - _, new_target_arc = self.process_tracker.compute_arc_motion(current_robot_coordinates, head_center_coordinates,
232   - new_robot_coordinates)
233   - if np.allclose(np.array(new_target_arc[3:-1]), np.array(self.target_arc[3:-1]), 0, 1):
234   - None
235   - else:
236   - if self.process_tracker.correction_distance_calculation_target(new_robot_coordinates, current_robot_coordinates) >= \
237   - const.ROBOT_ARC_THRESHOLD_DISTANCE*0.8:
238   - self.target_arc = new_target_arc
239   -
240   - coord = self.target_arc
241   -
242   - if np.allclose(np.array(actual_point), np.array(self.target_arc[3:-1]), 0, 10):
243   - self.arc_motion_flag = False
244   - self.arc_motion_step_flag = const.ROBOT_MOTIONS["normal"]
245   - coord = new_robot_coordinates
246   -
247   - self.trck_init_robot.SendCoordinates(coord, self.arc_motion_step_flag)
248   - robot_status = True
249   - else:
250   - print("Head is too far from the robot basis")
251   - robot_status = False
252   -
253   - return robot_status
254   -
255   - def robot_control(self, current_tracker_coordinates_in_robot, current_robot_coordinates, markers_flag):
256   - coord_head_tracker_in_robot = current_tracker_coordinates_in_robot[1]
257   - marker_head_flag = markers_flag[1]
258   - coord_obj_tracker_in_robot = current_tracker_coordinates_in_robot[2]
259   - marker_obj_flag = markers_flag[2]
260   - robot_status = False
261   -
262   - if self.robot_tracker_flag:
263   - current_head = coord_head_tracker_in_robot
264   - if current_head is not None and marker_head_flag:
265   - current_head_filtered = self.process_tracker.kalman_filter(current_head)
266   - if self.process_tracker.compute_head_move_threshold(current_head_filtered):
267   - new_robot_coordinates = self.process_tracker.compute_head_move_compensation(current_head_filtered,
268   - self.m_change_robot_to_head)
269   - robot_status = True
270   - if self.coord_inv_old is None:
271   - self.coord_inv_old = new_robot_coordinates
272   -
273   - if np.allclose(np.array(new_robot_coordinates), np.array(current_robot_coordinates), 0, 0.01):
274   - #avoid small movements (0.01 mm)
275   - pass
276   - elif not np.allclose(np.array(new_robot_coordinates), np.array(self.coord_inv_old), 0, 5):
277   - #if the head moves (>5mm) before the robot reach the target
278   - self.trck_init_robot.StopRobot()
279   - self.coord_inv_old = new_robot_coordinates
280   - else:
281   - distance_target = self.process_tracker.correction_distance_calculation_target(new_robot_coordinates, current_robot_coordinates)
282   - robot_status = self.robot_move_decision(distance_target, new_robot_coordinates, current_robot_coordinates, current_head_filtered)
283   - self.coord_inv_old = new_robot_coordinates
284   - else:
285   - self.trck_init_robot.StopRobot()
286   -
287   - return robot_status
288   -
289   - def run(self):
290   - while not self.event_robot.is_set():
291   - current_tracker_coordinates_in_robot, current_robot_coordinates, markers_flag = self.get_coordinates_from_tracker_devices()
292   -
293   - if not self.robot_target_queue.empty():
294   - self.robot_tracker_flag, self.m_change_robot_to_head = self.robot_target_queue.get_nowait()
295   - self.robot_motion_reset()
296   - self.robot_target_queue.task_done()
297   -
298   - if not self.object_at_target_queue.empty():
299   - self.target_flag = self.object_at_target_queue.get_nowait()
300   - self.object_at_target_queue.task_done()
301   -
302   - robot_status = self.robot_control(current_tracker_coordinates_in_robot, current_robot_coordinates, markers_flag)
303   -
304   - if self.previous_robot_status != robot_status:
305   - wx.CallAfter(Publisher.sendMessage, 'Update robot status', robot_status=robot_status)
306   - self.previous_robot_status = robot_status
307   -
308   - sleep(const.SLEEP_ROBOT)
invesalius/navigation/tracker.py
... ... @@ -42,7 +42,6 @@ class Tracker():
42 42 self.thread_coord = None
43 43  
44 44 self.event_coord = threading.Event()
45   - self.event_robot = threading.Event()
46 45  
47 46 self.TrackerCoordinates = dco.TrackerCoordinates()
48 47  
... ... @@ -78,11 +77,8 @@ class Tracker():
78 77  
79 78 if self.thread_coord:
80 79 self.event_coord.set()
81   - self.event_robot.set()
82 80 self.thread_coord.join()
83 81 self.event_coord.clear()
84   - self.event_robot.clear()
85   -
86 82  
87 83 Publisher.sendMessage('Update status text in GUI',
88 84 label=_("Tracker disconnected"))
... ... @@ -92,13 +88,6 @@ class Tracker():
92 88 label=_("Tracker still connected"))
93 89 print("Tracker still connected!")
94 90  
95   - def ConnectToRobot(self, navigation, tracker, robot):
96   - robot.SetRobotQueues([navigation.robot_target_queue, navigation.object_at_target_queue])
97   - robot.OnRobotConnection()
98   - trk_init_robot = self.trk_init[0][1][0]
99   - if trk_init_robot:
100   - robot.StartRobotThreadNavigation(tracker, navigation.coord_queue)
101   - Publisher.sendMessage('Robot navigation mode', robot_mode=True)
102 91  
103 92 def IsTrackerInitialized(self):
104 93 return self.trk_init and self.tracker_id and self.tracker_connected
... ... @@ -158,7 +147,7 @@ class Tracker():
158 147 m_probe_ref_right = np.linalg.inv(self.m_tracker_fiducials_raw[3]) @ self.m_tracker_fiducials_raw[2]
159 148 m_probe_ref_nasion = np.linalg.inv(self.m_tracker_fiducials_raw[5]) @ self.m_tracker_fiducials_raw[4]
160 149  
161   - return [m_probe_ref_left, m_probe_ref_right, m_probe_ref_nasion]
  150 + return [m_probe_ref_left.tolist(), m_probe_ref_right.tolist(), m_probe_ref_nasion.tolist()]
162 151  
163 152 def GetTrackerInfo(self):
164 153 return self.trk_init, self.tracker_id
... ...
invesalius/net/remote_control.py
... ... @@ -79,5 +79,7 @@ class RemoteControl:
79 79 })
80 80 except TypeError:
81 81 pass
  82 + except socketio.exceptions.BadNamespaceError:
  83 + pass
82 84  
83 85 Publisher.add_sendMessage_hook(_emit)
... ...