├── CMakeLists.txt ├── README.md ├── msg ├── GazeInfo.msg └── PupilInfo.msg ├── package.xml ├── setup.py └── src └── gazetracking ├── __init__.py ├── pupil_capture.py ├── pupil_capture.pyc └── pupil_listener.py /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(gazetracking) 3 | 4 | ## Find catkin macros and libraries 5 | ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) 6 | ## is used, also find other catkin packages 7 | find_package(catkin REQUIRED COMPONENTS 8 | message_generation 9 | rospy 10 | std_msgs 11 | ) 12 | 13 | ## System dependencies are found with CMake's conventions 14 | # find_package(Boost REQUIRED COMPONENTS system) 15 | 16 | 17 | ## Uncomment this if the package has a setup.py. This macro ensures 18 | ## modules and global scripts declared therein get installed 19 | ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html 20 | catkin_python_setup() 21 | 22 | ################################################ 23 | ## Declare ROS messages, services and actions ## 24 | ################################################ 25 | 26 | ## To declare and build messages, services or actions from within this 27 | ## package, follow these steps: 28 | ## * Let MSG_DEP_SET be the set of packages whose message types you use in 29 | ## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...). 30 | ## * In the file package.xml: 31 | ## * add a build_depend tag for "message_generation" 32 | ## * add a build_depend and a run_depend tag for each package in MSG_DEP_SET 33 | ## * If MSG_DEP_SET isn't empty the following dependency has been pulled in 34 | ## but can be declared for certainty nonetheless: 35 | ## * add a run_depend tag for "message_runtime" 36 | ## * In this file (CMakeLists.txt): 37 | ## * add "message_generation" and every package in MSG_DEP_SET to 38 | ## find_package(catkin REQUIRED COMPONENTS ...) 39 | ## * add "message_runtime" and every package in MSG_DEP_SET to 40 | ## catkin_package(CATKIN_DEPENDS ...) 41 | ## * uncomment the add_*_files sections below as needed 42 | ## and list every .msg/.srv/.action file to be processed 43 | ## * uncomment the generate_messages entry below 44 | ## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) 45 | 46 | ## Generate messages in the 'msg' folder 47 | add_message_files( 48 | FILES 49 | PupilInfo.msg 50 | GazeInfo.msg 51 | ) 52 | 53 | ## Generate services in the 'srv' folder 54 | # add_service_files( 55 | # FILES 56 | # Service1.srv 57 | # Service2.srv 58 | # ) 59 | 60 | ## Generate actions in the 'action' folder 61 | # add_action_files( 62 | # FILES 63 | # Action1.action 64 | # Action2.action 65 | # ) 66 | 67 | ## Generate added messages and services with any dependencies listed here 68 | generate_messages( 69 | DEPENDENCIES 70 | std_msgs # Or other packages containing msgs 71 | gazetracking 72 | ) 73 | 74 | ################################################ 75 | ## Declare ROS dynamic reconfigure parameters ## 76 | ################################################ 77 | 78 | ## To declare and build dynamic reconfigure parameters within this 79 | ## package, follow these steps: 80 | ## * In the file package.xml: 81 | ## * add a build_depend and a run_depend tag for "dynamic_reconfigure" 82 | ## * In this file (CMakeLists.txt): 83 | ## * add "dynamic_reconfigure" to 84 | ## find_package(catkin REQUIRED COMPONENTS ...) 85 | ## * uncomment the "generate_dynamic_reconfigure_options" section below 86 | ## and list every .cfg file to be processed 87 | 88 | ## Generate dynamic reconfigure parameters in the 'cfg' folder 89 | # generate_dynamic_reconfigure_options( 90 | # cfg/DynReconf1.cfg 91 | # cfg/DynReconf2.cfg 92 | # ) 93 | 94 | ################################### 95 | ## catkin specific configuration ## 96 | ################################### 97 | ## The catkin_package macro generates cmake config files for your package 98 | ## Declare things to be passed to dependent projects 99 | ## INCLUDE_DIRS: uncomment this if you package contains header files 100 | ## LIBRARIES: libraries you create in this project that dependent projects also need 101 | ## CATKIN_DEPENDS: catkin_packages dependent projects also need 102 | ## DEPENDS: system dependencies of this project that dependent projects also need 103 | catkin_package( 104 | # INCLUDE_DIRS include 105 | # LIBRARIES gazetracking 106 | CATKIN_DEPENDS rospy std_msgs message_runtime 107 | # DEPENDS 108 | ) 109 | 110 | ########### 111 | ## Build ## 112 | ########### 113 | 114 | ## Specify additional locations of header files 115 | ## Your package locations should be listed before other locations 116 | # include_directories(include) 117 | 118 | ## Declare a C++ library 119 | # add_library(gazetracking 120 | # src/${PROJECT_NAME}/gazetracking.cpp 121 | # ) 122 | 123 | ## Add cmake target dependencies of the library 124 | ## as an example, code may need to be generated before libraries 125 | ## either from message generation or dynamic reconfigure 126 | # add_dependencies(gazetracking ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) 127 | 128 | ## Declare a C++ executable 129 | # add_executable(gazetracking_node src/gazetracking_node.cpp) 130 | 131 | ## Add cmake target dependencies of the executable 132 | ## same as for the library above 133 | # add_dependencies(gazetracking_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) 134 | 135 | ## Specify libraries to link a library or executable target against 136 | # target_link_libraries(gazetracking_node 137 | # ${catkin_LIBRARIES} 138 | # ) 139 | 140 | ############# 141 | ## Install ## 142 | ############# 143 | 144 | # all install targets should use catkin DESTINATION variables 145 | # See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html 146 | 147 | ## Mark executable scripts (Python etc.) for installation 148 | ## in contrast to setup.py, you can choose the destination 149 | # install(PROGRAMS 150 | # scripts/my_python_script 151 | # DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 152 | # ) 153 | 154 | ## Mark executables and/or libraries for installation 155 | # install(TARGETS gazetracking gazetracking_node 156 | # ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} 157 | # LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} 158 | # RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 159 | # ) 160 | 161 | ## Mark cpp header files for installation 162 | # install(DIRECTORY include/${PROJECT_NAME}/ 163 | # DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} 164 | # FILES_MATCHING PATTERN "*.h" 165 | # PATTERN ".svn" EXCLUDE 166 | # ) 167 | 168 | ## Mark other files for installation (e.g. launch and bag files, etc.) 169 | # install(FILES 170 | # # myfile1 171 | # # myfile2 172 | # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 173 | # ) 174 | 175 | ############# 176 | ## Testing ## 177 | ############# 178 | 179 | ## Add gtest based cpp test target and link libraries 180 | # catkin_add_gtest(${PROJECT_NAME}-test test/test_gazetracking.cpp) 181 | # if(TARGET ${PROJECT_NAME}-test) 182 | # target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}) 183 | # endif() 184 | 185 | ## Add folders to be run by python nosetests 186 | # catkin_add_nosetests(test) 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gazetracking 2 | Eye gaze tracking using Pupil Labs head-mounted tracker. 3 | 4 | ## Publishing gaze positions with ROS 5 | Start Pupil Capture. Ensure that the Pupil Remote plugin is running (available in Pupil Capture under "General" -> "Open plugin"). 6 | 7 | Ensure that the Pupil Remote `IP` and `port` are set to match the values in `pupil_listener.py` (usually `tcp://127.0.0.1:50020`). 8 | 9 | Run the pupil listener: 10 | ```bash 11 | rosrun gazetracking pupil_listener.py 12 | ``` 13 | 14 | This should publish information on two ROS topics: `pupil_info` and `gaze_info`. The `gaze_info` topic contains a subset of the information from `pupil_info` that is most commonly used, specifically the position of the gaze (`norm_pos`) and the confidence value (`confidence`). 15 | 16 | For more information about the data contained in `pupil_info`, see the Pupil Capture website here: [https://github.com/pupil-labs/pupil/wiki/Data-Format](https://github.com/pupil-labs/pupil/wiki/Data-Format). 17 | 18 | 19 | -------------------------------------------------------------------------------- /msg/GazeInfo.msg: -------------------------------------------------------------------------------- 1 | float32 timestamp 2 | uint32 index 3 | float32 confidence 4 | float32[] norm_pos 5 | -------------------------------------------------------------------------------- /msg/PupilInfo.msg: -------------------------------------------------------------------------------- 1 | float32 timestamp 2 | uint32 index 3 | float32 confidence 4 | float32[] norm_pos 5 | float32 diameter 6 | string method 7 | float32[] ellipse_center 8 | float32[] ellipse_axis 9 | float32 ellipse_angle 10 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | gazetracking 4 | 0.0.0 5 | The gazetracking package 6 | 7 | 8 | 9 | 10 | hadmoni 11 | 12 | 13 | 14 | 15 | 16 | TODO 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | message_generation 36 | rospy 37 | std_msgs 38 | 39 | 40 | 41 | message_runtime 42 | rospy 43 | std_msgs 44 | 45 | 46 | catkin 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | from catkin_pkg.python_setup import generate_distutils_setup 5 | 6 | package_info = generate_distutils_setup() 7 | package_info['packages'] = ['gazetracking'] 8 | package_info['package_dir'] = {'':'src'} 9 | package_info['install_requires'] = [] 10 | 11 | setup(**package_info) 12 | -------------------------------------------------------------------------------- /src/gazetracking/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalrobotics/gazetracking/75cfefcb140f45045be3f1624767e25945632f26/src/gazetracking/__init__.py -------------------------------------------------------------------------------- /src/gazetracking/pupil_capture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import zmq 4 | import time 5 | 6 | 7 | class PupilCapture(): 8 | context = zmq.Context() 9 | socket = context.socket(zmq.REQ) 10 | logger = None 11 | 12 | def setup(self, log): 13 | 14 | # setting IP 15 | self.socket.connect('tcp://127.0.0.1:50020') 16 | self.logger = log 17 | 18 | # Not sure this is necessary 19 | #socket.send('T 0.0') #set timebase to 0.0 20 | 21 | # Test connection speed 22 | t = time.time() 23 | self.socket.send('t') # ask for timestamp; ignoring for now 24 | self.socket.recv() 25 | delay = time.time()-t 26 | 27 | self.logger.info("Pupil remote controller connected, command time delay: " + str(delay)) 28 | 29 | def start(self): 30 | self.socket.send('R') #start recording with specified name 31 | self.socket.recv() 32 | 33 | def stop(self): 34 | self.socket.send('r') # stop recording 35 | self.socket.recv() -------------------------------------------------------------------------------- /src/gazetracking/pupil_capture.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/personalrobotics/gazetracking/75cfefcb140f45045be3f1624767e25945632f26/src/gazetracking/pupil_capture.pyc -------------------------------------------------------------------------------- /src/gazetracking/pupil_listener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import zmq 3 | from msgpack import loads 4 | import json 5 | import rospy 6 | from gazetracking.msg import PupilInfo, GazeInfo 7 | 8 | context = zmq.Context() 9 | # open a req port to talk to pupil 10 | addr = '127.0.0.1' # remote ip or localhost 11 | req_port = "50020" # same as in the pupil remote gui 12 | req = context.socket(zmq.REQ) 13 | req.connect("tcp://{}:{}".format(addr, req_port)) 14 | # ask for the sub port 15 | req.send_string('SUB_PORT') 16 | sub_port = req.recv_string() 17 | 18 | # open a sub port to listen to pupil 19 | sub = context.socket(zmq.SUB) 20 | sub.connect("tcp://{}:{}".format(addr, sub_port)) 21 | sub.setsockopt_string(zmq.SUBSCRIBE, u'') 22 | 23 | # Create publishers 24 | pub_pupil = rospy.Publisher('/pupil_info', PupilInfo, queue_size=10) 25 | pub_gaze = rospy.Publisher('/gaze_info', GazeInfo, queue_size=10) 26 | 27 | def parse_pupil_pos(msg): 28 | ''' Parse pupil_position into a PupilInfo message. 29 | 30 | Return PupilInfo message, or -1 if msg argument was empty. 31 | ''' 32 | #msg may be a list of pupil positions 33 | if len(msg) < 1: return -1 34 | 35 | outmsg = PupilInfo() 36 | 37 | # Parse message data 38 | outmsg.timestamp = msg['timestamp'] 39 | #outmsg.index = m['index'] 40 | outmsg.confidence = msg['confidence'] 41 | outmsg.norm_pos = msg['norm_pos'] 42 | outmsg.diameter = msg['diameter'] 43 | outmsg.method = msg['method'] 44 | 45 | # Parse optional data 46 | if 'ellipse' in msg: 47 | outmsg.ellipse_center = msg['ellipse']['center'] 48 | outmsg.ellipse_axis = msg['ellipse']['axes'] 49 | outmsg.ellipse_angle = msg['ellipse']['angle'] 50 | 51 | # Warn that 3D data hasn't been parsed 52 | # TODO: parse 3D data 53 | if 'method' == '3d c++': 54 | rospy.logwarn("3D information parser not yet implemented,\ 55 | 3D data from JSON message not included in ROS message.") 56 | 57 | return outmsg 58 | 59 | def parse_gaze_pos(msg): 60 | ''' Parse gaze_positions into a GazeInfo message. 61 | 62 | Return GazeInfo message, or -1 if msg argument was empty. 63 | ''' 64 | #msg may be a list of gaze positions 65 | if len(msg) < 1: return -1 66 | 67 | outmsg = GazeInfo() 68 | 69 | # Parse message data 70 | outmsg.timestamp = msg['timestamp'] 71 | #outmsg.index = m['index'] 72 | outmsg.confidence = msg['confidence'] 73 | outmsg.norm_pos = msg['norm_pos'] 74 | 75 | return outmsg 76 | 77 | # Helper function that prints topic and message in a readable format 78 | def prettyprint(topic, msg): 79 | string = "\n\n" + str(topic) + ":\n" + str(msg) 80 | return string 81 | 82 | if __name__ == "__main__": 83 | 84 | rospy.loginfo("Starting pupil listener.") 85 | print "Starting pupil listener" 86 | 87 | rospy.init_node('pupillistener') 88 | 89 | print "listening for socket message...." 90 | while not rospy.is_shutdown(): 91 | # Receive message from socket, convert it to Python dict 92 | topic, msgstr = sub.recv_multipart() 93 | msg = loads(msgstr) 94 | # print "" + str(topic) + ": " + str(msg) 95 | 96 | # Convert message to ROS message 97 | if "pupil" in topic: 98 | rospy.logdebug("Reading pupil position: \n" + prettyprint(topic, msg)) 99 | 100 | # Parse and publish 101 | outmsg = parse_pupil_pos(msg) 102 | if not outmsg == -1: pub_pupil.publish(outmsg) 103 | 104 | elif topic == "gaze": 105 | rospy.logdebug("Reading pupil position: \n" + prettyprint(topic, msg)) 106 | 107 | # Parse and publish 108 | outmsg = parse_gaze_pos(msg) 109 | if not outmsg == -1: pub_gaze.publish(outmsg) 110 | 111 | elif topic == "dt": 112 | rospy.logdebug("Ignoring dt: " + str(msg)) 113 | 114 | else: 115 | rospy.logerr("Unrecognized topic from socket: " + topic) 116 | 117 | 118 | --------------------------------------------------------------------------------