├── LICENSE ├── README.md └── ReadOutTracker.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 jkulozik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UltimateTracker_python 2 | 3 | Directly read orientation and position data from the VIVE Ultimate Tracker using Python. Only available on Windows. 4 | 5 | ## Pre-requirements 6 | - Install Steam VR. 7 | - Enable the null driver for a virtual headset using this: [SteamVRNoHeadset](https://github.com/username223/SteamVRNoHeadset). 8 | - Install the VIVE Streaming Hub and activate the PC streaming beta: 9 | - Download from: [VIVEStreamingHub](https://www.vive.com/us/vive-hub/download/) 10 | - Activate teh PC Beta with code: "VIVEUTRCPreview" (valid as of 08/2024). 11 | - Follow instructions in the VIVE Streaming Hub. (update: 21.08.2024, Launch of SteamVR no longer necessary but installation and enabling null HMD is still needed) 12 | - Follow the instructions to create a map. Ignore the last step demanding a SteamVR headset connection. 13 | - When the trackers indicate ready, launch the code as explained below. 14 | 15 | ## VIVE Tracker DirectRead 16 | 17 | ### Overview 18 | **VIVE Tracker DirectRead** is a Python tool to read orientation and position data from the VIVE Ultimate Tracker without a VR headset. It logs data to a CSV file and provides real-time 2D and 3D plotting. 19 | 20 | ### Features 21 | - **Direct Tracker Data Access**: Retrieve orientation and position data of the Ultimate Tracker. 22 | - **CSV Logging**: Log data for analysis. 23 | - **Live Plotting**: Real-time 2D and 3D plotting. 24 | 25 | ### Requirements 26 | - Python 3.6+ 27 | - Libraries: `openvr`, `numpy`, `matplotlib`, `collections`, `mpl_toolkits.mplot3d`, `win_precise_time` 28 | 29 | ## Application 30 | - run the python code and modify in line 244 to 246 if you want your tracking data to be: 31 | - a. live plotted in a 3D plot (might impact system performance) 32 | - b. live plotted in a time/XYZ plot 33 | - c. saved in a .csv file (file name and path to be defined in line 251) 34 | 35 | ### Contact 36 | 37 | For any questions or issues, please contact [kulozik[at]isir.upmc.fr]. 38 | 39 | ### License 40 | 41 | This code is licensed under the MIT License. 42 | 43 | ### Attribution 44 | 45 | When using this code, please cite the following: 46 | 47 | [Julian Kulozik]. (2024). VIVE Tracker DirectRead. GitHub repository. URL: [https://github.com/jkulozik/UltimateTracker_python] 48 | 49 | Install the required Python libraries: 50 | 51 | ```sh 52 | pip install openvr numpy matplotlib collections mpl_toolkits.mplot3d win_precise_time 53 | -------------------------------------------------------------------------------- /ReadOutTracker.py: -------------------------------------------------------------------------------- 1 | import time 2 | import openvr 3 | import csv 4 | import math 5 | import numpy as np 6 | from win_precise_time import sleep 7 | import matplotlib.pyplot as plt 8 | from collections import deque 9 | from mpl_toolkits.mplot3d import Axes3D 10 | 11 | # Define the sampling rate (in Hz) 12 | SAMPLING_RATE = 120 13 | file_name = "ultimate_tracker_data.csv" 14 | def precise_wait(duration): 15 | """ 16 | Wait for a specified duration with high precision. 17 | Uses sleep for durations >= 1 ms, otherwise uses busy-wait. 18 | """ 19 | now = time.time() 20 | end = now + duration 21 | if duration >= 0.001: 22 | sleep(duration) 23 | while now < end: 24 | now = time.time() 25 | 26 | class VRSystemManager: 27 | def __init__(self): 28 | """ 29 | Initialize the VR system manager. 30 | """ 31 | self.vr_system = None 32 | 33 | def initialize_vr_system(self): 34 | """ 35 | Initialize the VR system. 36 | """ 37 | try: 38 | openvr.init(openvr.VRApplication_Other) 39 | self.vr_system = openvr.VRSystem() 40 | print(f"Starting Capture") 41 | except Exception as e: 42 | print(f"Failed to initialize VR system: {e}") 43 | return False 44 | return True 45 | 46 | def get_tracker_data(self): 47 | """ 48 | Retrieve tracker data from the VR system. 49 | """ 50 | poses = self.vr_system.getDeviceToAbsoluteTrackingPose( 51 | openvr.TrackingUniverseStanding, 0, openvr.k_unMaxTrackedDeviceCount) 52 | return poses 53 | 54 | def print_discovered_objects(self): 55 | """ 56 | Print information about discovered VR devices. 57 | """ 58 | for device_index in range(openvr.k_unMaxTrackedDeviceCount): 59 | device_class = self.vr_system.getTrackedDeviceClass(device_index) 60 | if device_class != openvr.TrackedDeviceClass_Invalid: 61 | serial_number = self.vr_system.getStringTrackedDeviceProperty( 62 | device_index, openvr.Prop_SerialNumber_String) 63 | model_number = self.vr_system.getStringTrackedDeviceProperty( 64 | device_index, openvr.Prop_ModelNumber_String) 65 | print(f"Device {device_index}: {serial_number} ({model_number})") 66 | 67 | def shutdown_vr_system(self): 68 | """ 69 | Shutdown the VR system. 70 | """ 71 | if self.vr_system: 72 | openvr.shutdown() 73 | 74 | class CSVLogger: 75 | def __init__(self): 76 | """ 77 | Initialize the CSV logger. 78 | """ 79 | self.file = None 80 | self.csv_writer = None 81 | 82 | def init_csv(self, filename): 83 | """ 84 | Initialize the CSV file for logging tracker data. 85 | """ 86 | try: 87 | self.file = open(filename, 'w', newline='') 88 | self.csv_writer = csv.writer(self.file) 89 | self.csv_writer.writerow(['TrackerIndex', 'Time', 'PositionX', 'PositionY', 'PositionZ', 'RotationW', 'RotationX', 'RotationY', 'RotationZ']) 90 | except Exception as e: 91 | print(f"Failed to initialize CSV file: {e}") 92 | return False 93 | return True 94 | 95 | def log_data_csv(self, index, current_time, position): 96 | """ 97 | Log tracker data to CSV file. 98 | """ 99 | try: 100 | self.csv_writer.writerow([index, current_time, *position]) 101 | except Exception as e: 102 | print(f"Failed to write data to CSV file: {e}") 103 | 104 | def close_csv(self): 105 | """ 106 | Close the CSV file if it's open. 107 | """ 108 | if self.file: 109 | self.file.close() 110 | 111 | class DataConverter: 112 | @staticmethod 113 | def convert_to_quaternion(pose_mat): 114 | """ 115 | Convert pose matrix to quaternion and position. 116 | """ 117 | r_w = math.sqrt(abs(1 + pose_mat[0][0] + pose_mat[1][1] + pose_mat[2][2])) / 2 118 | if r_w == 0: r_w = 0.0001 119 | r_x = (pose_mat[2][1] - pose_mat[1][2]) / (4 * r_w) 120 | r_y = (pose_mat[0][2] - pose_mat[2][0]) / (4 * r_w) 121 | r_z = (pose_mat[1][0] - pose_mat[0][1]) / (4 * r_w) 122 | 123 | x = pose_mat[0][3] 124 | y = pose_mat[1][3] 125 | z = pose_mat[2][3] 126 | 127 | return [x, y, z, r_w, r_x, r_y, r_z] 128 | 129 | class LivePlotter: 130 | def __init__(self): 131 | """ 132 | Initialize the live plotter. 133 | """ 134 | self.fig = None 135 | self.ax1 = None 136 | self.ax2 = None 137 | self.ax3 = None 138 | self.x_data = deque() 139 | self.y_data = deque() 140 | self.z_data = deque() 141 | self.time_data = deque() 142 | self.first = True 143 | self.firstx = 0 144 | self.firsty = 0 145 | self.firstz = 0 146 | self.start_time = time.time() 147 | self.vive_PosVIVE = np.zeros([3]) 148 | 149 | def init_live_plot(self): 150 | """ 151 | Initialize the live plot for VIVE tracker data. 152 | """ 153 | self.fig, (self.ax1, self.ax2, self.ax3) = plt.subplots(3, 1) 154 | self.ax1.set_title('X Position') 155 | self.ax2.set_title('Y Position') 156 | self.ax3.set_title('Z Position') 157 | self.x_line, = self.ax1.plot([], [], 'r-') 158 | self.y_line, = self.ax2.plot([], [], 'g-') 159 | self.z_line, = self.ax3.plot([], [], 'b-') 160 | plt.ion() 161 | plt.show() 162 | 163 | def update_live_plot(self, vive_PosVIVE): 164 | """ 165 | Update the live plot with new VIVE tracker data. 166 | """ 167 | current_time = time.time() 168 | self.x_data.append(vive_PosVIVE[0]) 169 | self.y_data.append(vive_PosVIVE[1]) 170 | self.z_data.append(vive_PosVIVE[2]) 171 | self.time_data.append(current_time - self.start_time) 172 | 173 | self.x_line.set_data(self.time_data, self.x_data) 174 | self.y_line.set_data(self.time_data, self.y_data) 175 | self.z_line.set_data(self.time_data, self.z_data) 176 | 177 | if self.first: 178 | self.firstx = self.x_data[0] 179 | self.firsty = self.y_data[0] 180 | self.firstz = self.z_data[0] 181 | self.first = False 182 | 183 | self.ax1.set_xlim(self.time_data[0], self.time_data[-1]) 184 | self.ax1.set_ylim([self.firstx - 1.5, self.firstx + 1.5]) 185 | 186 | self.ax2.set_xlim(self.time_data[0], self.time_data[-1]) 187 | self.ax2.set_ylim([self.firsty - 1.5, self.firsty + 1.5]) 188 | 189 | self.ax3.set_xlim(self.time_data[0], self.time_data[-1]) 190 | self.ax3.set_ylim([self.firstz - 1.5, self.firstz + 1.5]) 191 | 192 | self.fig.canvas.draw() 193 | self.fig.canvas.flush_events() 194 | 195 | def init_3d_plot(self): 196 | """ 197 | Initialize the 3D live plot for VIVE tracker data. 198 | """ 199 | self.fig_3d = plt.figure() 200 | self.ax_3d = self.fig_3d.add_subplot(111, projection='3d') 201 | self.ax_3d.view_init(elev=1, azim=180, roll=None, vertical_axis='y') 202 | 203 | self.maxlen_3d = 50 204 | self.x_data_3d = deque(maxlen=self.maxlen_3d) 205 | self.y_data_3d = deque(maxlen=self.maxlen_3d) 206 | self.z_data_3d = deque(maxlen=self.maxlen_3d) 207 | 208 | self.line_3d, = self.ax_3d.plot([], [], [], 'r-') 209 | 210 | self.ax_3d.set_xlabel('X') 211 | self.ax_3d.set_ylabel('Y') 212 | self.ax_3d.set_zlabel('Z') 213 | self.ax_3d.set_title('3D Tracker Position') 214 | 215 | plt.ion() 216 | plt.show() 217 | 218 | def update_3d_plot(self, vive_PosVIVE): 219 | """ 220 | Update the 3D live plot with new VIVE tracker data. 221 | """ 222 | x, y, z = vive_PosVIVE 223 | 224 | self.x_data_3d.append(x) 225 | self.y_data_3d.append(y) 226 | self.z_data_3d.append(z) 227 | 228 | self.line_3d.set_data(self.x_data_3d, self.y_data_3d) 229 | self.line_3d.set_3d_properties(self.z_data_3d) 230 | 231 | if len(self.x_data_3d) > 1: 232 | self.ax_3d.set_xlim(min(self.x_data_3d), max(self.x_data_3d)) 233 | self.ax_3d.set_ylim(min(self.y_data_3d), max(self.y_data_3d)) 234 | self.ax_3d.set_zlim(min(self.z_data_3d), max(self.z_data_3d)) 235 | 236 | self.fig_3d.canvas.draw() 237 | self.fig_3d.canvas.flush_events() 238 | 239 | def main(): 240 | vr_manager = VRSystemManager() 241 | csv_logger = CSVLogger() 242 | plotter = LivePlotter() 243 | 244 | # enable or disable plots (for maximum performance disable all plots) 245 | plot_3d = False # live 3D plot (might affect performance) 246 | plot_t_xyz = False # live plot of x, y, z positions 247 | log_data = True # log data to CSV file 248 | print_data = True # print data to console 249 | 250 | if not vr_manager.initialize_vr_system(): 251 | return 252 | 253 | if not csv_logger.init_csv(file_name): 254 | return 255 | 256 | if plot_t_xyz: plotter.init_live_plot() 257 | if plot_3d: plotter.init_3d_plot() 258 | 259 | try: 260 | while True: 261 | poses = vr_manager.get_tracker_data() 262 | for i in range(openvr.k_unMaxTrackedDeviceCount): 263 | if poses[i].bPoseIsValid: 264 | device_class = vr_manager.vr_system.getTrackedDeviceClass(i) 265 | if device_class == openvr.TrackedDeviceClass_GenericTracker: 266 | current_time = time.time() 267 | position = DataConverter.convert_to_quaternion(poses[i].mDeviceToAbsoluteTracking) 268 | if plot_t_xyz: plotter.update_live_plot(position[:3]) 269 | if plot_3d: plotter.update_3d_plot(position[:3]) 270 | if log_data: csv_logger.log_data_csv(i - 1, current_time, position) 271 | if print_data: print(f"Tracker {i - 1}: {position}") 272 | precise_wait(1 / SAMPLING_RATE) 273 | except KeyboardInterrupt: 274 | print("Stopping data collection...") 275 | finally: 276 | vr_manager.shutdown_vr_system() 277 | csv_logger.close_csv() 278 | 279 | 280 | if __name__ == "__main__": 281 | main() 282 | --------------------------------------------------------------------------------