class Gaze_Recorder(Plugin):
    """Capture Recorder"""
    def __init__(self,
                 g_pool,
                 session_name=get_auto_name(),
                 rec_dir=None,
                 user_info={
                     'name': '',
                     'additional_field': 'change_me'
                 },
                 info_menu_conf={},
                 show_info_menu=False,
                 record_eye=False,
                 raw_jpeg=True):
        super().__init__(g_pool)
        # update name if it was autogenerated.
        if session_name.startswith('20') and len(session_name) == 10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep, 1)[0]
        default_rec_dir = os.path.join(base_dir, 'recordings')

        if rec_dir and rec_dir != default_rec_dir and self.verify_path(
                rec_dir):
            self.rec_dir = rec_dir
        else:
            try:
                os.makedirs(default_rec_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info(
                    'Created standard Rec dir at "{}"'.format(default_rec_dir))
            self.rec_dir = default_rec_dir

        self.raw_jpeg = raw_jpeg
        self.order = .9
        self.record_eye = record_eye
        self.session_name = session_name
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf

        self.socket = None
        self.udp_socket = None

        thread = threading.Thread(target=self.run_ws, args=())
        thread.daemon = True  # Daemonize thread
        thread.start()

        #run udp-client
        self.udp_socket = UDP_Socket(host="127.0.0.1", port=2010)

        self.rec_path = None

        self.running = False
        self.menu_toggle = None
        self.quickbar_toggle = None

        self.smooth_x = 0.5
        self.smooth_y = 0.5

    def run_ws(self):
        try:
            socket = WSClient('ws://localhost:3434/',
                              self)  # url, onOpen,onClose, onReceive)
            socket.connect()
            socket.run_forever()
        except:
            print("Exception Occured")

    def on_open(self, ws):
        print("On open Called")
        ws.send(
            json.dumps({
                "event": "registerForUpdates",
                "value": "ON_REC_PATH_CREATED"
            }))
        ws.send(
            json.dumps({
                "event": "registerForUpdates",
                "value": "ON_EYETRACKING_ENDED"
            }))

    def on_message_received(self, ws, message):
        logger.info("Yo-yo, message is received {}".format(message))
        json_message = json.loads(str(message))

        event_name = json_message['event']
        event_value = json_message['value']

        if event_name == "ON_REC_PATH_CREATED":
            self.rec_path = event_value
            self.notify_all({
                'subject': 'recording.must_start',
                'session_name': self.session_name
            })
            self.notify_all({
                'subject': 'recording.must_start',
                'session_name': self.session_name,
                'remote_notify': 'all'
            })
            #For OpenDS
            ws.send(
                json.dumps({
                    "event": "ON_EYETRACKING_STARTED",
                    "value": "STARTED"
                }))

        if event_name == "ON_EYETRACKING_ENDED":
            self.notify_all({'subject': 'recording.must_stop'})
            self.notify_all({
                'subject': 'recording.must_stop',
                'remote_notify': 'all'
            })
            ws.send(
                json.dumps({
                    "event": "ON_EYETRACKING_ENDED_AND_SAVED",
                    "value": "ENDED_SAVED"
                }))

    def on_close(self, ws, code, reason=None):
        logger.info(
            "He-he, the websocket is closed, code: {}, reason: {}".format(
                code, reason))

    def send_udp_data(self, x, y, ts):
        self.udp_socket.send_message(str((x, y, ts)))

    def get_init_dict(self):
        d = {}
        d['record_eye'] = self.record_eye
        d['session_name'] = self.session_name
        d['user_info'] = self.user_info
        d['info_menu_conf'] = self.info_menu_conf
        d['show_info_menu'] = self.show_info_menu
        d['rec_dir'] = self.rec_dir
        d['raw_jpeg'] = self.raw_jpeg
        return d

    def init_gui(self):
        self.menu = ui.Growing_Menu('Gaze Recorder')
        self.menu.collapsed = True

        # Check udp-connection
        self.text_udp_status = None
        if self.udp_socket is None or self.udp_socket.is_connected() == False:
            self.text_udp_status = ui.Info_Text('UDP Status: Not connected')
        else:
            self.text_udp_status = ui.Info_Text('UDP Status: Connected')
        self.menu.append(self.text_udp_status)

        #self.button = ui.Thumb('running', self, setter=self.toggle, label='REC', hotkey='q')
        self.button = ui.Thumb('running',
                               self,
                               setter=self.toggle,
                               label=chr(0xf03d),
                               label_font='fontawesome',
                               label_offset_size=-30,
                               hotkey='e')
        self.button.on_color[:] = (1, .0, .0, .8)
        #self.menu.append(self.button)
        self.g_pool.quickbar.insert(1, self.button)

        # self.button = ui.Thumb('running', self, setter=self.toggle, label='RC', hotkey='r')
        # self.button.on_color[:] = (1, .0, .0, .8)
        # self.g_pool.quickbar.insert(1, self.button)

        recording_menu = ui.Growing_Menu('Recording Settings')
        recording_menu.collapsed = True
        recording_menu.append(
            ui.Text_Input('rec_dir',
                          self,
                          setter=self.set_rec_dir,
                          label='Path to recordings'))
        recording_menu.append(
            ui.Text_Input('session_name',
                          self,
                          setter=self.set_session_name,
                          label='Recording session name'))

        self.menu.append(recording_menu)
        self.g_pool.sidebar.insert(3, self.menu)

    def deinit_gui(self):
        if self.menu:
            self.g_pool.sidebar.remove(self.menu)
            self.menu = None
        if self.button:
            self.g_pool.quickbar.remove(self.button)
            self.button = None

    def toggle(self, _=None):
        if self.running:
            self.notify_all({'subject': 'recording.must_stop'})
            self.notify_all({
                'subject': 'recording.must_stop',
                'remote_notify': 'all'
            })
        else:
            self.notify_all({
                'subject': 'recording.must_start',
                'session_name': self.session_name
            })
            self.notify_all({
                'subject': 'recording.must_start',
                'session_name': self.session_name,
                'remote_notify': 'all'
            })

    def on_notify(self, notification):
        """Handles recorder notifications
        Reacts to notifications:
            ``recording.should_start``: Starts a new recording session.
                fields: 'session_name' change session name
                    use `/` to att dirs.
                    start with `/` to ingore the rec base dir and start from root instead.
            ``recording.should_stop``: Stops current recording session
        Emits notifications:
            ``recording.started``: New recording session started
            ``recording.stopped``: Current recording session stopped
        Args:
            notification (dictionary): Notification dictionary
        """
        # notification wants to be recorded
        if notification.get('record', False) and self.running:
            if 'timestamp' not in notification:
                logger.error(
                    "Notification without timestamp will not be saved.")
            else:
                self.data['notifications'].append(notification)
        elif notification['subject'] == 'recording.must_start':
            if self.running:
                logger.info('Recording already running!')
            elif not self.g_pool.capture.online:
                logger.error(
                    "Current world capture is offline. Please reconnect or switch to fake capture"
                )
            else:
                if notification.get("session_name", ""):
                    self.set_session_name(notification["session_name"])
                self.start()

        elif notification['subject'] == 'recording.must_stop':
            if self.running:
                self.stop()
            else:
                logger.info('Recording already stopped!')

    def get_rec_time_str(self):
        rec_time = gmtime(time() - self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self):
        self.data = {
            'pupil_positions': [],
            'gaze_positions': [],
            'notifications': []
        }
        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()

        session = os.path.join(self.rec_dir, self.session_name)
        try:
            os.makedirs(session)
            logger.debug(
                "Created new recordings session dir {}".format(session))

        except:
            logger.debug(
                "Recordings session dir {} already exists, using it.".format(
                    session))

        if self.rec_path is None:
            # set up self incrementing folder within session folder
            counter = 0
            while True:
                self.rec_path = os.path.join(session,
                                             "{:03d}/".format(counter))
                try:
                    os.mkdir(self.rec_path)
                    logger.debug("Created new recording dir {}".format(
                        self.rec_path))
                    break
                except:
                    logger.debug(
                        "We dont want to overwrite data, incrementing counter & trying to make new data folder"
                    )
                    counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, 'w', newline='') as csvfile:
            csv_utils.write_key_value_file(
                csvfile, {
                    'Recording Name': self.session_name,
                    'Start Date': strftime("%d.%m.%Y",
                                           localtime(self.start_time)),
                    'Start Time': strftime("%H:%M:%S",
                                           localtime(self.start_time))
                })

        self.video_path = os.path.join(self.rec_path, "world.mp4")
        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.writer = JPEG_Writer(self.video_path,
                                      self.g_pool.capture.frame_rate)
        elif hasattr(self.g_pool.capture._recent_frame, 'h264_buffer'):
            self.writer = H264Writer(self.video_path,
                                     self.g_pool.capture.frame_size[0],
                                     self.g_pool.capture.frame_size[1],
                                     int(self.g_pool.capture.frame_rate))
        else:
            self.writer = AV_Writer(self.video_path,
                                    fps=self.g_pool.capture.frame_rate)

        try:
            cal_pt_path = os.path.join(self.g_pool.user_dir,
                                       "user_calibration_data")
            cal_data = load_object(cal_pt_path)
            notification = {
                'subject': 'calibration.calibration_data',
                'record': True
            }
            notification.update(cal_data)
            self.data['notifications'].append(notification)
        except:
            pass

        if self.show_info_menu:
            self.open_info_menu()
        logger.info("Started Recording.")
        self.notify_all({
            'subject': 'recording.started',
            'rec_path': self.rec_path,
            'session_name': self.session_name,
            'record_eye': self.record_eye,
            'compression': self.raw_jpeg
        })

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu('additional Recording Info',
                                         size=(300, 300),
                                         pos=(300, 300))
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.keys():
                self.info_menu.insert(0, ui.Text_Input(name, self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(
            ui.Info_Text(
                'Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'
            ))
        self.info_menu.append(
            ui.Text_Input('user_info',
                          self,
                          setter=set_user_info,
                          label="User info"))
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def extract_gazes(self, gaze_on_screen):
        raw_x, raw_y = self.smoother.normalize_gaze_by_length(gaze_on_screen)

        self.smooth_x += 0.35 * (raw_x - self.smooth_x)
        self.smooth_y += 0.35 * (raw_y - self.smooth_y)

        x = self.smooth_x
        y = self.smooth_y

        y = 1 - y  # inverting y so it shows up correctly on screen

        x = round(min(1, max(0, x)), 2)
        y = round(min(1, max(0, y)), 2)

        return x, y

    def smooth_gaze(self, raw_x, raw_y):
        self.smooth_x += 0.35 * (raw_x - self.smooth_x)
        self.smooth_y += 0.35 * (raw_y - self.smooth_y)

        x = self.smooth_x
        y = self.smooth_y

        y = 1 - y  # inverting y so it shows up correctly on screen

        x = round(min(1, max(0, x)), 2)
        y = round(min(1, max(0, y)), 2)

        return x, y

    def srf_tracked(self, events):
        srf_gazes = events.get('surfaces', [])
        if not srf_gazes:
            return None
        return srf_gazes[0]['name']

        #     if srf['name'] == "screen":
        #         fr_gazes = self.gaze_extractor.process_gaze_from_screen(srf['gaze_on_srf'])
        #         for pnt in srf['gaze_on_srf']:
        #             logger.warning(pnt['base_data']['norm_pos'])
        #         break
        #
        # return srf_gazes

    def smooth_data(self, gazes_on_screen, gazeLen):

        raw_x = raw_y = 0

        for gaze in gazes_on_screen:
            raw_x += gaze['norm_pos'][0]
            raw_y += gaze['norm_pos'][1]

        raw_x /= gazeLen
        raw_y /= gazeLen

        self.smooth_x += 0.35 * (raw_x - self.smooth_x)
        self.smooth_y += 0.35 * (raw_y - self.smooth_y)

        x = self.smooth_x
        y = self.smooth_y

        y = 1 - y

        # inverting y so it shows up correctly on screen
        x = round(min(1, max(0, x)), 2)
        y = round(min(1, max(0, y)), 2)

        # x *= int(self.x_dim)
        # y *= int(self.y_dim)
        return str((x, y, 1.0))

    def recent_events(self, events):
        # string = self.socket.recv()
        # topic, messagedata = string.split()
        # if topic == 10001:
        #     logger.info("{} {}".format(topic, messagedata))

        if self.running:
            srf_gazes = events.get('surfaces', [])

            for srf in srf_gazes:
                if srf['name'] == "screen" or srf['name'] == "wheel":
                    gazes_on_screen = srf['gaze_on_srf']
                    gazeLen = len(gazes_on_screen)

                    if gazeLen > 0:
                        #recent_gaze = gaze_on_screen[0]
                        raw_x = raw_y = 0
                        timestamps = []

                        # for gaze in gazes_on_screen:
                        #     raw_x += gaze['norm_pos'][0]
                        #     raw_y += gaze['norm_pos'][1]
                        #     #logger.info(gaze)
                        #     timestamp = gaze['base_data']['timestamp']
                        #     timestamps.append(timestamp)
                        #
                        #
                        # raw_x /= gazeLen
                        # raw_y /= gazeLen

                        raw_x = gazes_on_screen[0]['norm_pos'][0]
                        raw_y = gazes_on_screen[0]['norm_pos'][1]

                        self.smooth_x += 0.35 * (raw_x - self.smooth_x)
                        self.smooth_y += 0.35 * (raw_y - self.smooth_y)

                        x = self.smooth_x
                        y = 1 - self.smooth_y

                        gaze_positions = []
                        x = round(min(1, max(0, x)), 2) * 1.0
                        y = round(min(1, max(0, y)), 2) * 1.0
                        gaze_positions.append(x)
                        gaze_positions.append(y)

                        jsonObj = json.dumps({
                            "srf": srf['name'],
                            "gaze_positions": gaze_positions,
                            "timestamps": timestamps
                        })
                        self.udp_socket.send_message(jsonObj)

                        #print(jsonObj)

            for key, data in events.items():

                if key not in ('dt', 'frame', 'audio_packets'):
                    try:
                        self.data[key] += data
                    except KeyError:
                        self.data[key] = []
                        self.data[key] += data

            if 'frame' in events:
                frame = events['frame']
                self.writer.write_video_frame(frame)
                self.frame_count += 1

            # # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))

            self.button.status_text = self.get_rec_time_str()

    def stop(self):
        # explicit release of VideoWriter
        self.writer.release()
        self.writer = None

        save_object(self.data, os.path.join(self.rec_path, "pupil_data"))

        try:
            copy2(os.path.join(self.g_pool.user_dir, "surface_definitions"),
                  os.path.join(self.rec_path, "surface_definitions"))
        except:
            logger.info(
                "No surface_definitions data found. You may want this if you do marker tracking."
            )

        camera_calibration = load_camera_calibration(self.g_pool)
        if camera_calibration is not None:
            save_object(camera_calibration,
                        os.path.join(self.rec_path, "camera_calibration"))
        else:
            logger.info("No camera calibration found.")

        try:
            with open(self.meta_info_path, 'a', newline='') as csvfile:
                csv_utils.write_key_value_file(csvfile, {
                    'Duration Time':
                    self.get_rec_time_str(),
                    'World Camera Frames':
                    self.frame_count,
                    'World Camera Resolution':
                    str(self.g_pool.capture.frame_size[0]) + "x" +
                    str(self.g_pool.capture.frame_size[1]),
                    'Capture Software Version':
                    self.g_pool.version,
                    'Data Format Version':
                    self.g_pool.version,
                    'System Info':
                    get_system_info()
                },
                                               append=True)
        except Exception:
            logger.exception(
                "Could not save metadata. Please report this bug!")

        try:
            with open(os.path.join(self.rec_path, "user_info.csv"),
                      'w',
                      newline='') as csvfile:
                csv_utils.write_key_value_file(csvfile, self.user_info)
        except Exception:
            logger.exception(
                "Could not save userdata. Please report this bug!")

        self.close_info_menu()

        self.running = False
        self.menu.read_only = False
        self.button.status_text = ''

        self.data = {'pupil_positions': [], 'gaze_positions': []}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        logger.info("Saved Recording.")
        self.notify_all({
            'subject': 'recording.stopped',
            'rec_path': self.rec_path
        })

    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()
        self.deinit_gui()

    def verify_path(self, val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '{}'.".format(n_path))
            return False
        else:
            return n_path

    def set_rec_dir(self, val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if os.path.sep in val:
                logger.warning(
                    'You session name will create one or more subdirectories')
            self.session_name = val

    def old_method(self):
        pass
Exemple #2
0
class Recorder(Plugin):
    """Capture Recorder"""
    def __init__(self,
                 g_pool,
                 session_name=get_auto_name(),
                 rec_dir=None,
                 user_info={
                     'name': '',
                     'additional_field': 'change_me'
                 },
                 info_menu_conf={},
                 show_info_menu=False,
                 record_eye=False,
                 audio_src='No Audio',
                 raw_jpeg=True):
        super(Recorder, self).__init__(g_pool)
        #update name if it was autogenerated.
        if session_name.startswith('20') and len(session_name) == 10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep, 1)[0]
        default_rec_dir = os.path.join(base_dir, 'recordings')

        if rec_dir and rec_dir != default_rec_dir and self.verify_path(
                rec_dir):
            self.rec_dir = rec_dir
        else:
            try:
                os.makedirs(default_rec_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info('Created standard Rec dir at "%s"' %
                            default_rec_dir)
            self.rec_dir = default_rec_dir

        self.raw_jpeg = raw_jpeg
        self.order = .9
        self.record_eye = record_eye
        self.session_name = session_name
        self.audio_devices_dict = Audio_Input_Dict()
        if audio_src in self.audio_devices_dict.keys():
            self.audio_src = audio_src
        else:
            self.audio_src = 'No Audio'
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf

    def get_init_dict(self):
        d = {}
        d['record_eye'] = self.record_eye
        d['audio_src'] = self.audio_src
        d['session_name'] = self.session_name
        d['user_info'] = self.user_info
        d['info_menu_conf'] = self.info_menu_conf
        d['show_info_menu'] = self.show_info_menu
        d['rec_dir'] = self.rec_dir
        d['raw_jpeg'] = self.raw_jpeg
        return d

    def init_gui(self):
        self.menu = ui.Growing_Menu('Recorder')
        self.g_pool.sidebar.insert(3, self.menu)
        self.menu.append(
            ui.Info_Text(
                'Pupil recordings are saved like this: "path_to_recordings/recording_session_name/nnn" where "nnn" is an increasing number to avoid overwrites. You can use "/" in your session name to create subdirectories.'
            ))
        self.menu.append(
            ui.Info_Text(
                'Recordings are saved to "~/pupil_recordings". You can change the path here but note that invalid input will be ignored.'
            ))
        self.menu.append(
            ui.Text_Input('rec_dir',
                          self,
                          setter=self.set_rec_dir,
                          label='Path to recordings'))
        self.menu.append(
            ui.Text_Input('session_name',
                          self,
                          setter=self.set_session_name,
                          label='Recording session name'))
        self.menu.append(
            ui.Switch('show_info_menu',
                      self,
                      on_val=True,
                      off_val=False,
                      label='Request additional user info'))
        self.menu.append(
            ui.Selector(
                'raw_jpeg',
                self,
                selection=[True, False],
                labels=["bigger file, less CPU", "smaller file, more CPU"],
                label='compression'))
        self.menu.append(
            ui.Info_Text(
                'Recording the raw eye video is optional. We use it for debugging.'
            ))
        self.menu.append(
            ui.Switch('record_eye',
                      self,
                      on_val=True,
                      off_val=False,
                      label='Record eye'))
        self.menu.append(
            ui.Selector('audio_src',
                        self,
                        selection=self.audio_devices_dict.keys()))

        self.button = ui.Thumb('running',
                               self,
                               setter=self.toggle,
                               label='Record',
                               hotkey='r')
        self.button.on_color[:] = (1, .0, .0, .8)
        self.g_pool.quickbar.insert(1, self.button)

    def deinit_gui(self):
        if self.menu:
            self.g_pool.sidebar.remove(self.menu)
            self.menu = None
        if self.button:
            self.g_pool.quickbar.remove(self.button)
            self.button = None

    def toggle(self, _=None):
        if self.running:
            self.stop()
        else:
            self.start()

    def on_notify(self, notification):
        if notification['name'] == 'rec_should_start':
            if self.running:
                logger.warning('Recording is already running!')
            else:
                self.set_session_name(notification["session_name"])
                self.start(network_propagate=notification.get(
                    'network_propagate', True))
        elif notification['name'] == 'rec_should_stop':
            if self.running:
                self.stop(network_propagate=notification.get(
                    'network_propagate', True))
            else:
                logger.warning('Recording is already stopped!')

    def get_rec_time_str(self):
        rec_time = gmtime(time() - self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self, network_propagate=True):
        self.timestamps = []
        self.data = {'pupil_positions': [], 'gaze_positions': []}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()

        session = os.path.join(self.rec_dir, self.session_name)
        try:
            os.makedirs(session)
            logger.debug("Created new recordings session dir %s" % session)

        except:
            logger.debug(
                "Recordings session dir %s already exists, using it." %
                session)

        # set up self incrementing folder within session folder
        counter = 0
        while True:
            self.rec_path = os.path.join(session, "%03d/" % counter)
            try:
                os.mkdir(self.rec_path)
                logger.debug("Created new recording dir %s" % self.rec_path)
                break
            except:
                logger.debug(
                    "We dont want to overwrite data, incrementing counter & trying to make new data folder"
                )
                counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, 'w') as f:
            f.write("Recording Name\t" + self.session_name + "\n")
            f.write("Start Date\t" +
                    strftime("%d.%m.%Y", localtime(self.start_time)) + "\n")
            f.write("Start Time\t" +
                    strftime("%H:%M:%S", localtime(self.start_time)) + "\n")

        if self.audio_src != 'No Audio':
            audio_path = os.path.join(self.rec_path, "world.wav")
            self.audio_writer = Audio_Capture(
                audio_path, self.audio_devices_dict[self.audio_src])
        else:
            self.audio_writer = None

        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = JPEG_Writer(self.video_path,
                                      int(self.g_pool.capture.frame_rate))
        else:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = AV_Writer(self.video_path)
        # positions path to eye process
        if self.record_eye:
            for tx in self.g_pool.eye_tx:
                tx.send((self.rec_path, self.raw_jpeg))

        if self.show_info_menu:
            self.open_info_menu()

        self.notify_all({
            'name': 'rec_started',
            'rec_path': self.rec_path,
            'session_name': self.session_name,
            'network_propagate': network_propagate
        })

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu('additional Recording Info',
                                         size=(300, 300),
                                         pos=(300, 300))
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.iterkeys():
                self.info_menu.insert(0, ui.Text_Input(name, self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(
            ui.Info_Text(
                'Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'
            ))
        self.info_menu.append(
            ui.Text_Input('user_info',
                          self,
                          setter=set_user_info,
                          label="User info"))
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def update(self, frame, events):
        if self.running:
            self.data['pupil_positions'] += events['pupil_positions']
            self.data['gaze_positions'] += events['gaze_positions']
            self.timestamps.append(frame.timestamp)
            if self.g_pool.capture.jpeg_support:
                self.writer.write_video_frame_compressed(frame)
            else:
                self.writer.write_video_frame(frame)
            self.frame_count += 1

            # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))
            for p in events['pupil_positions']:
                pupil_pos = p['timestamp'], p['confidence'], p['id'], p[
                    'norm_pos'][0], p['norm_pos'][1], p['diameter']
                self.pupil_pos_list.append(pupil_pos)

            for g in events.get('gaze_positions', []):
                gaze_pos = g['timestamp'], g['confidence'], g['norm_pos'][
                    0], g['norm_pos'][1]
                self.gaze_pos_list.append(gaze_pos)

            self.button.status_text = self.get_rec_time_str()

    def stop(self, network_propagate=True):
        #explicit release of VideoWriter
        self.writer.release()
        self.writer = None

        if self.record_eye:
            for tx in self.g_pool.eye_tx:
                try:
                    tx.send((None, None))
                except:
                    logger.warning(
                        "Could not stop eye-recording. Please report this bug!"
                    )

        save_object(self.data, os.path.join(self.rec_path, "pupil_data"))

        gaze_list_path = os.path.join(self.rec_path, "gaze_positions.npy")
        np.save(gaze_list_path, np.asarray(self.gaze_pos_list))

        pupil_list_path = os.path.join(self.rec_path, "pupil_positions.npy")
        np.save(pupil_list_path, np.asarray(self.pupil_pos_list))

        timestamps_path = os.path.join(self.rec_path, "world_timestamps.npy")
        # ts = sanitize_timestamps(np.array(self.timestamps))
        ts = np.array(self.timestamps)
        np.save(timestamps_path, ts)

        try:
            copy2(os.path.join(self.g_pool.user_dir, "surface_definitions"),
                  os.path.join(self.rec_path, "surface_definitions"))
        except:
            logger.info(
                "No surface_definitions data found. You may want this if you do marker tracking."
            )

        try:
            copy2(os.path.join(self.g_pool.user_dir, "cal_pt_cloud.npy"),
                  os.path.join(self.rec_path, "cal_pt_cloud.npy"))
        except:
            logger.warning(
                "No calibration data found. Please calibrate first.")

        try:
            copy2(os.path.join(self.g_pool.user_dir, "camera_calibration"),
                  os.path.join(self.rec_path, "camera_calibration"))
        except:
            logger.info("No camera calibration found.")

        try:
            with open(self.meta_info_path, 'a') as f:
                f.write("Duration Time\t" + self.get_rec_time_str() + "\n")
                if self.g_pool.binocular:
                    f.write("Eye Mode\tbinocular\n")
                else:
                    f.write("Eye Mode\tmonocular\n")
                f.write("Duration Time\t" + self.get_rec_time_str() + "\n")
                f.write("World Camera Frames\t" + str(self.frame_count) + "\n")
                f.write("World Camera Resolution\t" +
                        str(self.g_pool.capture.frame_size[0]) + "x" +
                        str(self.g_pool.capture.frame_size[1]) + "\n")
                f.write("Capture Software Version\t%s\n" % self.g_pool.version)
                if platform.system() == "Windows":
                    username = os.environ["USERNAME"]
                    sysname, nodename, release, version, machine, _ = platform.uname(
                    )
                else:
                    username = getpass.getuser()
                    try:
                        sysname, nodename, release, version, machine = os.uname(
                        )
                    except:
                        sysname, nodename, release, version, machine = sys.platform, None, None, None, None
                f.write("User\t" + username + "\n")
                f.write("Platform\t" + sysname + "\n")
                f.write("Machine\t" + nodename + "\n")
                f.write("Release\t" + release + "\n")
                f.write("Version\t" + version + "\n")
        except Exception:
            logger.exception(
                "Could not save metadata. Please report this bug!")

        try:
            with open(os.path.join(self.rec_path, "user_info.csv"), 'w') as f:
                for name, val in self.user_info.iteritems():
                    f.write("%s\t%s\n" % (name, val))
        except Exception:
            logger.exception(
                "Could not save userdata. Please report this bug!")

        self.close_info_menu()

        if self.audio_writer:
            self.audio_writer = None

        self.running = False
        self.menu.read_only = False
        self.button.status_text = ''

        self.timestamps = []
        self.data = {'pupil_positions': [], 'gaze_positions': []}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        self.notify_all({
            'name': 'rec_stopped',
            'rec_path': self.rec_path,
            'network_propagate': network_propagate
        })

    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()
        self.deinit_gui()

    def verify_path(self, val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '%s'." % n_path)
            return False
        else:
            return n_path

    def set_rec_dir(self, val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if '/' in val:
                logger.warning(
                    'You session name will create one or more subdirectories')
            self.session_name = val
Exemple #3
0
class Recorder(Plugin):
    """Capture Recorder"""
    def __init__(self,g_pool,session_name = get_auto_name(),rec_dir=None, user_info={'name':'','additional_field':'change_me'},info_menu_conf={},show_info_menu=False, record_eye = False, audio_src = 'No Audio',raw_jpeg=True):
        super(Recorder, self).__init__(g_pool)
        #update name if it was autogenerated.
        if session_name.startswith('20') and len(session_name)==10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep,1)[0]
        default_rec_dir = os.path.join(base_dir,'recordings')

        if rec_dir and rec_dir != default_rec_dir and self.verify_path(rec_dir):
            self.rec_dir = rec_dir
        else:
            try:
                os.makedirs(default_rec_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info('Created standard Rec dir at "%s"'%default_rec_dir)
            self.rec_dir = default_rec_dir

        self.raw_jpeg = raw_jpeg
        self.order = .9
        self.record_eye = record_eye
        self.session_name = session_name
        self.audio_devices_dict = Audio_Input_Dict()
        if audio_src in self.audio_devices_dict.keys():
            self.audio_src = audio_src
        else:
            self.audio_src = 'No Audio'
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf


    def get_init_dict(self):
        d = {}
        d['record_eye'] = self.record_eye
        d['audio_src'] = self.audio_src
        d['session_name'] = self.session_name
        d['user_info'] = self.user_info
        d['info_menu_conf'] = self.info_menu_conf
        d['show_info_menu'] = self.show_info_menu
        d['rec_dir'] = self.rec_dir
        d['raw_jpeg'] = self.raw_jpeg
        return d


    def init_gui(self):
        self.menu = ui.Growing_Menu('Recorder')
        self.g_pool.sidebar.insert(3,self.menu)
        self.menu.append(ui.Info_Text('Pupil recordings are saved like this: "path_to_recordings/recording_session_name/nnn" where "nnn" is an increasing number to avoid overwrites. You can use "/" in your session name to create subdirectories.'))
        self.menu.append(ui.Info_Text('Recordings are saved to "~/pupil_recordings". You can change the path here but note that invalid input will be ignored.'))
        self.menu.append(ui.Text_Input('rec_dir',self,setter=self.set_rec_dir,label='Path to recordings'))
        self.menu.append(ui.Text_Input('session_name',self,setter=self.set_session_name,label='Recording session name'))
        self.menu.append(ui.Switch('show_info_menu',self,on_val=True,off_val=False,label='Request additional user info'))
        self.menu.append(ui.Selector('raw_jpeg',self,selection = [True,False], labels=["bigger file, less CPU", "smaller file, more CPU"],label='Compression'))
        self.menu.append(ui.Info_Text('Recording the raw eye video is optional. We use it for debugging.'))
        self.menu.append(ui.Switch('record_eye',self,on_val=True,off_val=False,label='Record eye'))
        self.menu.append(ui.Selector('audio_src',self, selection=self.audio_devices_dict.keys(),label='Audio Source'))

        self.button = ui.Thumb('running',self,setter=self.toggle,label='Record',hotkey='r')
        self.button.on_color[:] = (1,.0,.0,.8)
        self.g_pool.quickbar.insert(1,self.button)


    def deinit_gui(self):
        if self.menu:
            self.g_pool.sidebar.remove(self.menu)
            self.menu = None
        if self.button:
            self.g_pool.quickbar.remove(self.button)
            self.button = None



    def toggle(self, _=None):
        if self.running:
            self.notify_all( {'subject':'should_stop_recording','network_propagate':True} )
        else:
            self.notify_all( {'subject':'should_start_recording','session_name':self.session_name,'network_propagate':True} )


    def on_notify(self,notification):

        # notification wants to be recorded
        if notification.get('record',False) and self.running:
            self.data['notifications'].append(notification)


        # Notificatio to start recording
        elif notification['subject'] == 'should_start_recording':
            if self.running:
                logger.info('Recording already running!')
            else:
                if notification.get("session_name",""):
                    self.set_session_name(notification["session_name"])
                self.start()
        # Remote has stopped recording, we should stop as well.
        elif notification['subject'] == 'should_stop_recording':
            if self.running:
                self.stop()
            else:
                logger.info('Recording already stopped!')


    def get_rec_time_str(self):
        rec_time = gmtime(time()-self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self):
        self.timestamps = []
        self.data = {'pupil_positions':[],'gaze_positions':[],'notifications':[]}
        self.pupil_diameter = []  #addBy Changsoon
        self.gaze_point_z = []    #addBy Changsoon
        self.gaze_point_x = []    #addBy Changsoon
        self.gaze_point_y = []    #addBy Changsoon
        self.cnum = 0
        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()

        session = os.path.join(self.rec_dir, self.session_name)
        try:
            os.makedirs(session)
            logger.debug("Created new recordings session dir %s"%session)

        except:
            logger.debug("Recordings session dir %s already exists, using it." %session)

        # set up self incrementing folder within session folder
        counter = 0
        while True:
            self.rec_path = os.path.join(session, "%03d/" % counter)
            try:
                os.mkdir(self.rec_path)
                logger.debug("Created new recording dir %s"%self.rec_path)
                break
            except:
                logger.debug("We dont want to overwrite data, incrementing counter & trying to make new data folder")
                counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, 'w') as f:
            f.write("Recording Name\t"+self.session_name+ "\n")
            f.write("Start Date\t"+ strftime("%d.%m.%Y", localtime(self.start_time))+ "\n")
            f.write("Start Time\t"+ strftime("%H:%M:%S", localtime(self.start_time))+ "\n")


        if self.audio_src != 'No Audio':
            audio_path = os.path.join(self.rec_path, "world.wav")
            self.audio_writer = Audio_Capture(audio_path,self.audio_devices_dict[self.audio_src])
        else:
            self.audio_writer = None

        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = JPEG_Writer(self.video_path,self.g_pool.capture.frame_rate)
        else:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = AV_Writer(self.video_path,fps=self.g_pool.capture.frame_rate)

        # positions path to eye process
        if self.record_eye:
            for alive, pipe in zip(self.g_pool.eyes_are_alive,self.g_pool.eye_pipes):
                if alive.value:
                    pipe.send( ('Rec_Start',(self.rec_path,self.raw_jpeg) ) )

        if self.show_info_menu:
            self.open_info_menu()
        logger.info("Started Recording.")
        self.notify_all( {'subject':'rec_started','rec_path':self.rec_path,'session_name':self.session_name,'network_propagate':True} )

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu('additional Recording Info',size=(300,300),pos=(300,300))
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.iterkeys():
                self.info_menu.insert(0,ui.Text_Input(name,self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(ui.Info_Text('Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'))
        self.info_menu.append(ui.Text_Input('user_info',self,setter=set_user_info,label="User info"))
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def update(self,frame,events):
        if self.running:
            for key,data in events.iteritems():
                if key not in ('dt'):
                    try:
                        self.data[key] += data
                        if (key == 'pupil_positions'):
                            try:
                                pupilDict = data[0]
                                self.pupil_diameter.append(pupilDict.get('diameter'))
                            except:
                                print('error');
                        elif (key == 'gaze_positions'):
                            try:
                                gazeDict = data[0]
                                self.gaze_point_x.append(gazeDict.get('norm_pos')[0])
                                self.gaze_point_y.append(gazeDict.get('norm_pos')[1])
                                self.gaze_point_z.append(gazeDict.get('gaze_point_3d')[2])
                            except:
                                print('error')
                        #image process (OCR) find the condion that show use OCR
                    except KeyError:
                        self.data[key] = []
                        self.data[key] += data

            print str(self.cnum) 
            self.cnum = self.cnum + 1  
            if len(self.gaze_point_x) > 0 and self.cnum == 30:
                print('Enter TO the block')
	        height = (1 - self.gaze_point_y[-1]) * 720
	        height = int(height)
	        image = frame.img
	        if (height < 100):
	            image = frame.img[0:height+200, 300:1000]
	        elif (height > 520):
	            image = frame.img[height-100:720, 300:1000]
	        else:
	            image = frame.img[height-100:height+200, 300:1000]
	        cv2.imwrite('/home/eyetracking/pupil/'+"frame%d.jpg" %self.cnum,image)
	        image=Image.open('/home/eyetracking/pupil/'+"frame%d.jpg" %self.cnum)
	        enhancer = ImageEnhance.Contrast(image)
	        image = enhancer.enhance(4)
	        totemp = sys.stdout
	        sys.stdout = codecs.open('/home/eyetracking/pupil/ocr.txt', encoding='utf-8', mode='w+')   #save the OCR
	        print image_to_string(image)
	        sys.stdout.close()
	        sys.stdout = totemp
                try:
                    i=300
                    for line in open('/home/eyetracking/pupil/ocr.txt'):
                        i=i+20
                        cv2.putText(dst, line,(0,i), cv2.FONT_HERSHEY_COMPLEX,0.6,(0,0,255))
                    cv2.imshow("window",dst)
                    cv2.waitKey(33)
                except Exception as error:
                    print(error)
            self.timestamps.append(frame.timestamp)
            self.writer.write_video_frame(frame)
            self.frame_count += 1

            # # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))

            self.button.status_text = self.get_rec_time_str()

    def stop(self):
        #explicit release of VideoWriter
        self.writer.release()
        self.writer = None

        if self.record_eye:
            for alive, pipe in zip(self.g_pool.eyes_are_alive,self.g_pool.eye_pipes):
                if alive.value:
                    pipe.send(('Rec_Stop',None))

        save_object(self.data,os.path.join(self.rec_path, "pupil_data"))

        timestamps_path = os.path.join(self.rec_path, "world_timestamps.npy")
        # ts = sanitize_timestamps(np.array(self.timestamps))
        ts = np.array(self.timestamps)
        np.save(timestamps_path,ts)

        try:
            copy2(os.path.join(self.g_pool.user_dir,"surface_definitions"),os.path.join(self.rec_path,"surface_definitions"))
        except:
            logger.info("No surface_definitions data found. You may want this if you do marker tracking.")
        try:
            copy2(os.path.join(self.g_pool.user_dir,"user_calibration_data"),os.path.join(self.rec_path,"user_calibration_data"))
        except:
            logger.warning("No user calibration data found. Please calibrate first.")

        camera_calibration = load_camera_calibration(self.g_pool)
        if camera_calibration is not None:
            save_object(camera_calibration,os.path.join(self.rec_path, "camera_calibration"))
        else:
            logger.info("No camera calibration found.")

        try:
            with open(self.meta_info_path, 'a') as f:
                f.write("Duration Time\t"+ self.get_rec_time_str()+ "\n")
                f.write("World Camera Frames\t"+ str(self.frame_count)+ "\n")
                f.write("World Camera Resolution\t"+ str(self.g_pool.capture.frame_size[0])+"x"+str(self.g_pool.capture.frame_size[1])+"\n")
                f.write("Capture Software Version\t%s\n"%self.g_pool.version)
                f.write("System Info\t%s"%get_system_info())
        except Exception:
            logger.exception("Could not save metadata. Please report this bug!")

        try:
            with open(os.path.join(self.rec_path, "user_info.csv"), 'w') as f:
                for name,val in self.user_info.iteritems():
                    f.write("%s\t%s\n"%(name,val))
        except Exception:
            logger.exception("Could not save userdata. Please report this bug!")


        self.close_info_menu()

        if self.audio_writer:
            self.audio_writer = None

        self.running = False
        self.menu.read_only = False
        self.button.status_text = ''

        self.timestamps = []
        self.data = {'pupil_positions':[],'gaze_positions':[]}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        logger.info("Saved Recording.")
        self.notify_all( {'subject':'rec_stopped','rec_path':self.rec_path,'network_propagate':True} )

    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()
        self.deinit_gui()

    def verify_path(self,val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '%s'."%n_path)
            return False
        else:
            return n_path

    def set_rec_dir(self,val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if '/' in val:
                logger.warning('You session name will create one or more subdirectories')
            self.session_name = val
Exemple #4
0
class Recorder(Plugin):
    """Capture Recorder"""
    def __init__(self,g_pool,session_name = get_auto_name(),rec_dir=None, user_info={'name':'','additional_field':'change_me'},info_menu_conf={},show_info_menu=False, record_eye = False, audio_src = 'No Audio',raw_jpeg=False):
        super(Recorder, self).__init__(g_pool)
        #update name if it was autogenerated.
        if session_name.startswith('20') and len(session_name)==10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep,1)[0]
        default_rec_dir = os.path.join(base_dir,'recordings')

        if rec_dir and rec_dir != default_rec_dir and self.verify_path(rec_dir):
            self.rec_dir = rec_dir
        else:
            try:
                os.makedirs(default_rec_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info('Created standard Rec dir at "%s"'%default_rec_dir)
            self.rec_dir = default_rec_dir

        self.raw_jpeg = raw_jpeg
        self.order = .9
        self.record_eye = record_eye
        self.session_name = session_name
        self.audio_devices_dict = Audio_Input_Dict()
        if audio_src in self.audio_devices_dict.keys():
            self.audio_src = audio_src
        else:
            self.audio_src = 'No Audio'
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf


    def get_init_dict(self):
        d = {}
        d['record_eye'] = self.record_eye
        d['audio_src'] = self.audio_src
        d['session_name'] = self.session_name
        d['user_info'] = self.user_info
        d['info_menu_conf'] = self.info_menu_conf
        d['show_info_menu'] = self.show_info_menu
        d['rec_dir'] = self.rec_dir
        d['raw_jpeg'] = self.raw_jpeg
        return d


    def init_gui(self):
        self.menu = ui.Growing_Menu('Recorder')
        self.g_pool.sidebar.insert(3,self.menu)
        self.menu.append(ui.Info_Text('Pupil recordings are saved like this: "path_to_recordings/recording_session_name/nnn" where "nnn" is an increasing number to avoid overwrites. You can use "/" in your session name to create subdirectories.'))
        self.menu.append(ui.Info_Text('Recordings are saved to "~/pupil_recordings". You can change the path here but note that invalid input will be ignored.'))
        self.menu.append(ui.Text_Input('rec_dir',self,setter=self.set_rec_dir,label='Path to recordings'))
        self.menu.append(ui.Text_Input('session_name',self,setter=self.set_session_name,label='Recording session name'))
        self.menu.append(ui.Switch('show_info_menu',self,on_val=True,off_val=False,label='Request additional user info'))
        self.menu.append(ui.Selector('raw_jpeg',self,selection = [True,False], labels=["bigger file, less CPU", "smaller file, more CPU"],label='compression'))
        self.menu.append(ui.Info_Text('Recording the raw eye video is optional. We use it for debugging.'))
        self.menu.append(ui.Switch('record_eye',self,on_val=True,off_val=False,label='Record eye'))
        self.menu.append(ui.Selector('audio_src',self, selection=self.audio_devices_dict.keys()))

        self.button = ui.Thumb('running',self,setter=self.toggle,label='Record',hotkey='r')
        self.button.on_color[:] = (1,.0,.0,.8)
        self.g_pool.quickbar.insert(1,self.button)


    def deinit_gui(self):
        if self.menu:
            self.g_pool.sidebar.remove(self.menu)
            self.menu = None
        if self.button:
            self.g_pool.quickbar.remove(self.button)
            self.button = None



    def toggle(self, _=None):
        if self.running:
            self.stop()
        else:
            self.start()

    def on_notify(self,notification):
        if notification['subject'] == 'rec_should_start':
            if self.running:
                logger.warning('Recording is already running!')
            else:
                self.set_session_name(notification["session_name"])
                self.start(network_propagate=notification.get('network_propagate',True))
        elif notification['subject'] == 'rec_should_stop':
            if self.running:
                self.stop(network_propagate=notification.get('network_propagate',True))
            else:
                logger.warning('Recording is already stopped!')


    def get_rec_time_str(self):
        rec_time = gmtime(time()-self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self,network_propagate=True):
        self.timestamps = []
        self.data = {'pupil_positions':[],'gaze_positions':[]}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()

        session = os.path.join(self.rec_dir, self.session_name)
        try:
            os.makedirs(session)
            logger.debug("Created new recordings session dir %s"%session)

        except:
            logger.debug("Recordings session dir %s already exists, using it." %session)

        # set up self incrementing folder within session folder
        counter = 0
        while True:
            self.rec_path = os.path.join(session, "%03d/" % counter)
            try:
                os.mkdir(self.rec_path)
                logger.debug("Created new recording dir %s"%self.rec_path)
                break
            except:
                logger.debug("We dont want to overwrite data, incrementing counter & trying to make new data folder")
                counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, 'w') as f:
            f.write("Recording Name\t"+self.session_name+ "\n")
            f.write("Start Date\t"+ strftime("%d.%m.%Y", localtime(self.start_time))+ "\n")
            f.write("Start Time\t"+ strftime("%H:%M:%S", localtime(self.start_time))+ "\n")


        if self.audio_src != 'No Audio':
            audio_path = os.path.join(self.rec_path, "world.wav")
            self.audio_writer = Audio_Capture(audio_path,self.audio_devices_dict[self.audio_src])
        else:
            self.audio_writer = None

        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = JPEG_Writer(self.video_path,self.g_pool.capture.frame_rate)
        else:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = AV_Writer(self.video_path,fps=self.g_pool.capture.frame_rate)
        # positions path to eye process
        if self.record_eye:
            for tx in self.g_pool.eye_tx:
                tx.send((self.rec_path,self.raw_jpeg))

        if self.show_info_menu:
            self.open_info_menu()

        self.notify_all( {'subject':'rec_started','rec_path':self.rec_path,'session_name':self.session_name,'network_propagate':network_propagate} )

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu('additional Recording Info',size=(300,300),pos=(300,300))
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.iterkeys():
                self.info_menu.insert(0,ui.Text_Input(name,self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(ui.Info_Text('Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'))
        self.info_menu.append(ui.Text_Input('user_info',self,setter=set_user_info,label="User info"))
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def update(self,frame,events):
        if self.running:
            self.data['pupil_positions'] += events['pupil_positions']
            self.data['gaze_positions'] += events['gaze_positions']
            self.timestamps.append(frame.timestamp)
            self.writer.write_video_frame(frame)
            self.frame_count += 1

            # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))
            for p in events['pupil_positions']:
                pupil_pos = p['timestamp'],p['confidence'],p['id'],p['norm_pos'][0],p['norm_pos'][1],p['diameter']
                self.pupil_pos_list.append(pupil_pos)

            for g in events.get('gaze_positions',[]):
                gaze_pos = g['timestamp'],g['confidence'],g['norm_pos'][0],g['norm_pos'][1]
                self.gaze_pos_list.append(gaze_pos)

            self.button.status_text = self.get_rec_time_str()

    def stop(self,network_propagate=True):
        #explicit release of VideoWriter
        self.writer.release()
        self.writer = None

        if self.record_eye:
            for tx in self.g_pool.eye_tx:
                try:
                    tx.send((None,None))
                except:
                    logger.warning("Could not stop eye-recording. Please report this bug!")

        save_object(self.data,os.path.join(self.rec_path, "pupil_data"))

        gaze_list_path = os.path.join(self.rec_path, "gaze_positions.npy")
        np.save(gaze_list_path,np.asarray(self.gaze_pos_list))

        pupil_list_path = os.path.join(self.rec_path, "pupil_positions.npy")
        np.save(pupil_list_path,np.asarray(self.pupil_pos_list))

        timestamps_path = os.path.join(self.rec_path, "world_timestamps.npy")
        # ts = sanitize_timestamps(np.array(self.timestamps))
        ts = np.array(self.timestamps)
        np.save(timestamps_path,ts)

        try:
            copy2(os.path.join(self.g_pool.user_dir,"surface_definitions"),os.path.join(self.rec_path,"surface_definitions"))
        except:
            logger.info("No surface_definitions data found. You may want this if you do marker tracking.")

        try:
            copy2(os.path.join(self.g_pool.user_dir,"cal_pt_cloud.npy"),os.path.join(self.rec_path,"cal_pt_cloud.npy"))
        except:
            logger.warning("No calibration data found. Please calibrate first.")

        try:
            copy2(os.path.join(self.g_pool.user_dir,"camera_calibration"),os.path.join(self.rec_path,"camera_calibration"))
        except:
            logger.info("No camera calibration found.")

        try:
            with open(self.meta_info_path, 'a') as f:
                f.write("Duration Time\t"+ self.get_rec_time_str()+ "\n")
                if self.g_pool.binocular:
                    f.write("Eye Mode\tbinocular\n")
                else:
                    f.write("Eye Mode\tmonocular\n")
                f.write("Duration Time\t"+ self.get_rec_time_str()+ "\n")
                f.write("World Camera Frames\t"+ str(self.frame_count)+ "\n")
                f.write("World Camera Resolution\t"+ str(self.g_pool.capture.frame_size[0])+"x"+str(self.g_pool.capture.frame_size[1])+"\n")
                f.write("Capture Software Version\t%s\n"%self.g_pool.version)
                if platform.system() == "Windows":
                    username = os.environ["USERNAME"]
                    sysname, nodename, release, version, machine, _ = platform.uname()
                else:
                    username = getpass.getuser()
                    try:
                        sysname, nodename, release, version, machine = os.uname()
                    except:
                        sysname, nodename, release, version, machine = sys.platform,None,None,None,None
                f.write("User\t"+username+"\n")
                f.write("Platform\t"+sysname+"\n")
                f.write("Machine\t"+nodename+"\n")
                f.write("Release\t"+release+"\n")
                f.write("Version\t"+version+"\n")
        except Exception:
            logger.exception("Could not save metadata. Please report this bug!")

        try:
            with open(os.path.join(self.rec_path, "user_info.csv"), 'w') as f:
                for name,val in self.user_info.iteritems():
                    f.write("%s\t%s\n"%(name,val))
        except Exception:
            logger.exception("Could not save userdata. Please report this bug!")


        self.close_info_menu()

        if self.audio_writer:
            self.audio_writer = None

        self.running = False
        self.menu.read_only = False
        self.button.status_text = ''

        self.timestamps = []
        self.data = {'pupil_positions':[],'gaze_positions':[]}
        self.pupil_pos_list = []
        self.gaze_pos_list = []


        self.notify_all( {'subject':'rec_stopped','rec_path':self.rec_path,'network_propagate':network_propagate} )



    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()
        self.deinit_gui()

    def verify_path(self,val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '%s'."%n_path)
            return False
        else:
            return n_path

    def set_rec_dir(self,val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if '/' in val:
                logger.warning('You session name will create one or more subdirectories')
            self.session_name = val
Exemple #5
0
class Recorder(System_Plugin_Base):
    """Capture Recorder"""

    icon_chr = chr(0xE04B)
    icon_font = "pupil_icons"
    warning_low_disk_space_th = 5.0  # threshold in GB
    stop_rec_low_disk_space_th = 1.0  # threshold in GB

    def __init__(
        self,
        g_pool,
        session_name=get_auto_name(),
        rec_root_dir=None,
        user_info={"name": "", "additional_field": "change_me"},
        info_menu_conf={},
        show_info_menu=False,
        record_eye=True,
        raw_jpeg=True,
    ):
        super().__init__(g_pool)
        # update name if it was autogenerated.
        if session_name.startswith("20") and len(session_name) == 10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep, 1)[0]
        default_rec_root_dir = os.path.join(base_dir, "recordings")

        if (
            rec_root_dir
            and rec_root_dir != default_rec_root_dir
            and self.verify_path(rec_root_dir)
        ):
            self.rec_root_dir = rec_root_dir
        else:
            try:
                os.makedirs(default_rec_root_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info(
                    'Created standard Rec dir at "{}"'.format(default_rec_root_dir)
                )
            self.rec_root_dir = default_rec_root_dir

        self.raw_jpeg = raw_jpeg
        self.order = 0.9
        self.record_eye = record_eye
        self.session_name = session_name
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf

        self.low_disk_space_thumb = None
        check_timer = timer(1.0)
        self.check_space = lambda: next(check_timer)

    def get_init_dict(self):
        d = {}
        d["record_eye"] = self.record_eye
        d["session_name"] = self.session_name
        d["user_info"] = self.user_info
        d["info_menu_conf"] = self.info_menu_conf
        d["show_info_menu"] = self.show_info_menu
        d["rec_root_dir"] = self.rec_root_dir
        d["raw_jpeg"] = self.raw_jpeg
        return d

    def init_ui(self):
        self.add_menu()
        self.menu.label = "Recorder"
        self.menu_icon.order = 0.29

        self.menu.append(
            ui.Info_Text(
                'Pupil recordings are saved like this: "path_to_recordings/recording_session_name/nnn" where "nnn" is an increasing number to avoid overwrites. You can use "/" in your session name to create subdirectories.'
            )
        )
        self.menu.append(
            ui.Info_Text(
                'Recordings are saved to "~/pupil_recordings". You can change the path here but note that invalid input will be ignored.'
            )
        )
        self.menu.append(
            ui.Text_Input(
                "rec_root_dir",
                self,
                setter=self.set_rec_root_dir,
                label="Path to recordings",
            )
        )
        self.menu.append(
            ui.Text_Input(
                "session_name",
                self,
                setter=self.set_session_name,
                label="Recording session name",
            )
        )
        self.menu.append(
            ui.Switch(
                "show_info_menu",
                self,
                on_val=True,
                off_val=False,
                label="Request additional user info",
            )
        )
        self.menu.append(
            ui.Selector(
                "raw_jpeg",
                self,
                selection=[True, False],
                labels=["bigger file, less CPU", "smaller file, more CPU"],
                label="Compression",
            )
        )
        self.menu.append(
            ui.Info_Text(
                "Recording the raw eye video is optional. We use it for debugging."
            )
        )
        self.menu.append(
            ui.Switch(
                "record_eye", self, on_val=True, off_val=False, label="Record eye"
            )
        )
        self.button = ui.Thumb(
            "running", self, setter=self.toggle, label="R", hotkey="r"
        )
        self.button.on_color[:] = (1, 0.0, 0.0, 0.8)
        self.g_pool.quickbar.insert(2, self.button)

        self.low_disk_space_thumb = ui.Thumb(
            "low_disk_warn", label="!", getter=lambda: True, setter=lambda x: None
        )
        self.low_disk_space_thumb.on_color[:] = (1, 0.0, 0.0, 0.8)
        self.low_disk_space_thumb.status_text = "Low disk space"

    def deinit_ui(self):
        if self.low_disk_space_thumb in self.g_pool.quickbar:
            self.g_pool.quickbar.remove(self.low_disk_space_thumb)
        self.g_pool.quickbar.remove(self.button)
        self.button = None
        self.remove_menu()

    def toggle(self, _=None):
        if self.running:
            self.notify_all({"subject": "recording.should_stop"})
            self.notify_all(
                {"subject": "recording.should_stop", "remote_notify": "all"}
            )
        else:
            self.notify_all(
                {"subject": "recording.should_start", "session_name": self.session_name}
            )
            self.notify_all(
                {
                    "subject": "recording.should_start",
                    "session_name": self.session_name,
                    "remote_notify": "all",
                }
            )

    def on_notify(self, notification):
        """Handles recorder notifications

        Reacts to notifications:
            ``recording.should_start``: Starts a new recording session.
                fields:
                - 'session_name' change session name
                    start with `/` to ingore the rec base dir and start from root instead.
                - `record_eye` boolean that indicates recording of the eyes, defaults to current setting
            ``recording.should_stop``: Stops current recording session

        Emits notifications:
            ``recording.started``: New recording session started
            ``recording.stopped``: Current recording session stopped

        Args:
            notification (dictionary): Notification dictionary
        """
        # notification wants to be recorded
        if notification.get("record", False) and self.running:
            if "timestamp" not in notification:
                logger.error("Notification without timestamp will not be saved.")
            else:
                notification["topic"] = "notify." + notification["subject"]
                try:
                    writer = self.pldata_writers["notify"]
                except KeyError:
                    writer = PLData_Writer(self.rec_path, "notify")
                    self.pldata_writers["notify"] = writer
                writer.append(notification)

        elif notification["subject"] == "recording.should_start":
            if self.running:
                logger.info("Recording already running!")
            else:
                self.record_eye = notification.get("record_eye", self.record_eye)
                if notification.get("session_name", ""):
                    self.set_session_name(notification["session_name"])
                self.start()

        elif notification["subject"] == "recording.should_stop":
            if self.running:
                self.stop()
            else:
                logger.info("Recording already stopped!")

    def get_rec_time_str(self):
        rec_time = gmtime(time() - self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self):
        session = os.path.join(self.rec_root_dir, self.session_name)
        try:
            os.makedirs(session, exist_ok=True)
            logger.debug("Created new recordings session dir {}".format(session))
        except OSError:
            logger.error(
                "Could not start recording. Session dir {} not writable.".format(
                    session
                )
            )
            return

        self.pldata_writers = {}
        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()
        start_time_synced = self.g_pool.get_timestamp()
        recording_uuid = uuid.uuid4()

        # set up self incrementing folder within session folder
        counter = 0
        while True:
            self.rec_path = os.path.join(session, "{:03d}/".format(counter))
            try:
                os.mkdir(self.rec_path)
                logger.debug("Created new recording dir {}".format(self.rec_path))
                break
            except:
                logger.debug(
                    "We dont want to overwrite data, incrementing counter & trying to make new data folder"
                )
                counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, "w", newline="", encoding="utf-8") as csvfile:
            csv_utils.write_key_value_file(
                csvfile,
                {
                    "Recording Name": self.session_name,
                    "Start Date": strftime("%d.%m.%Y", localtime(self.start_time)),
                    "Start Time": strftime("%H:%M:%S", localtime(self.start_time)),
                    "Start Time (System)": self.start_time,
                    "Start Time (Synced)": start_time_synced,
                    "Recording UUID": recording_uuid,
                },
            )

        self.video_path = os.path.join(self.rec_path, "world.mp4")
        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.writer = JPEG_Writer(self.video_path, self.g_pool.capture.frame_rate)
        elif hasattr(self.g_pool.capture._recent_frame, "h264_buffer"):
            self.writer = H264Writer(
                self.video_path,
                self.g_pool.capture.frame_size[0],
                self.g_pool.capture.frame_size[1],
                int(self.g_pool.capture.frame_rate),
            )
        else:
            self.writer = AV_Writer(self.video_path, fps=self.g_pool.capture.frame_rate)

        try:
            cal_pt_path = os.path.join(self.g_pool.user_dir, "user_calibration_data")
            cal_data = load_object(cal_pt_path)
            notification = {"subject": "calibration.calibration_data", "record": True}
            notification.update(cal_data)
            notification["topic"] = "notify." + notification["subject"]

            writer = PLData_Writer(self.rec_path, "notify")
            writer.append(notification)
            self.pldata_writers["notify"] = writer
        except FileNotFoundError:
            pass

        if self.show_info_menu:
            self.open_info_menu()
        logger.info("Started Recording.")
        self.notify_all(
            {
                "subject": "recording.started",
                "rec_path": self.rec_path,
                "session_name": self.session_name,
                "record_eye": self.record_eye,
                "compression": self.raw_jpeg,
            }
        )

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu(
            "additional Recording Info", size=(300, 300), pos=(300, 300)
        )
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.keys():
                self.info_menu.insert(0, ui.Text_Input(name, self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(
            ui.Info_Text(
                'Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'
            )
        )
        self.info_menu.append(
            ui.Text_Input("user_info", self, setter=set_user_info, label="User info")
        )
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def recent_events(self, events):

        if self.check_space():
            disk_space = available_gb(self.rec_root_dir)
            if (
                disk_space < self.warning_low_disk_space_th
                and self.low_disk_space_thumb not in self.g_pool.quickbar
            ):
                self.g_pool.quickbar.append(self.low_disk_space_thumb)
            elif (
                disk_space >= self.warning_low_disk_space_th
                and self.low_disk_space_thumb in self.g_pool.quickbar
            ):
                self.g_pool.quickbar.remove(self.low_disk_space_thumb)

            if self.running and disk_space <= self.stop_rec_low_disk_space_th:
                self.stop()
                logger.error("Recording was stopped due to low disk space!")

        if self.running:
            for key, data in events.items():
                if key not in ("dt", "depth_frame") and not key.startswith("frame"):
                    try:
                        writer = self.pldata_writers[key]
                    except KeyError:
                        writer = PLData_Writer(self.rec_path, key)
                        self.pldata_writers[key] = writer
                    writer.extend(data)
            if "frame" in events:
                frame = events["frame"]
                self.writer.write_video_frame(frame)
                self.frame_count += 1

            # # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))

            self.button.status_text = self.get_rec_time_str()

    def stop(self):
        # explicit release of VideoWriter
        try:
            self.writer.release()
        except RuntimeError:
            logger.error("No world video recorded")
        else:
            logger.debug("Closed media container")
            self.g_pool.capture.intrinsics.save(self.rec_path, custom_name="world")
        finally:
            self.writer = None

        # save_object(self.data, os.path.join(self.rec_path, "pupil_data"))
        for writer in self.pldata_writers.values():
            writer.close()

        del self.pldata_writers

        try:
            copy2(
                os.path.join(self.g_pool.user_dir, "surface_definitions"),
                os.path.join(self.rec_path, "surface_definitions"),
            )
        except:
            logger.info(
                "No surface_definitions data found. You may want this if you do marker tracking."
            )

        try:
            with open(self.meta_info_path, "a", newline="") as csvfile:
                csv_utils.write_key_value_file(
                    csvfile,
                    {
                        "Duration Time": self.get_rec_time_str(),
                        "World Camera Frames": self.frame_count,
                        "World Camera Resolution": str(
                            self.g_pool.capture.frame_size[0]
                        )
                        + "x"
                        + str(self.g_pool.capture.frame_size[1]),
                        "Capture Software Version": self.g_pool.version,
                        "Data Format Version": self.g_pool.version,
                        "System Info": get_system_info(),
                    },
                    append=True,
                )
        except Exception:
            logger.exception("Could not save metadata. Please report this bug!")

        try:
            with open(
                os.path.join(self.rec_path, "user_info.csv"), "w", newline=""
            ) as csvfile:
                csv_utils.write_key_value_file(csvfile, self.user_info)
        except Exception:
            logger.exception("Could not save userdata. Please report this bug!")

        self.close_info_menu()

        self.running = False
        if self.menu:
            self.menu.read_only = False
            self.button.status_text = ""

        logger.info("Saved Recording.")
        self.notify_all({"subject": "recording.stopped", "rec_path": self.rec_path})

    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()

    def verify_path(self, val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '{}'.".format(n_path))
            return False
        else:
            return n_path

    def set_rec_root_dir(self, val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_root_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if os.path.sep in val:
                logger.warning(
                    "You session name will create one or more subdirectories"
                )
            self.session_name = val
Exemple #6
0
class Recorder(Plugin):
    """Capture Recorder"""
    def __init__(self,
                 g_pool,
                 session_name=get_auto_name(),
                 rec_dir=None,
                 user_info={
                     'name': '',
                     'additional_field': 'change_me'
                 },
                 info_menu_conf={},
                 show_info_menu=False,
                 record_eye=False,
                 audio_src='No Audio',
                 raw_jpeg=True):
        super().__init__(g_pool)
        # update name if it was autogenerated.
        if session_name.startswith('20') and len(session_name) == 10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep, 1)[0]
        default_rec_dir = os.path.join(base_dir, 'recordings')

        if rec_dir and rec_dir != default_rec_dir and self.verify_path(
                rec_dir):
            self.rec_dir = rec_dir
        else:
            try:
                os.makedirs(default_rec_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info(
                    'Created standard Rec dir at "{}"'.format(default_rec_dir))
            self.rec_dir = default_rec_dir

        self.raw_jpeg = raw_jpeg
        self.order = .9
        self.record_eye = record_eye
        self.session_name = session_name
        self.audio_devices_dict = Audio_Input_Dict()
        if audio_src in list(self.audio_devices_dict.keys()):
            self.audio_src = audio_src
        else:
            self.audio_src = 'No Audio'
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf

    def get_init_dict(self):
        d = {}
        d['record_eye'] = self.record_eye
        d['audio_src'] = self.audio_src
        d['session_name'] = self.session_name
        d['user_info'] = self.user_info
        d['info_menu_conf'] = self.info_menu_conf
        d['show_info_menu'] = self.show_info_menu
        d['rec_dir'] = self.rec_dir
        d['raw_jpeg'] = self.raw_jpeg
        return d

    def init_gui(self):
        self.menu = ui.Growing_Menu('Recorder')
        self.menu.collapsed = True
        self.g_pool.sidebar.insert(3, self.menu)
        self.menu.append(
            ui.Info_Text(
                'Pupil recordings are saved like this: "path_to_recordings/recording_session_name/nnn" where "nnn" is an increasing number to avoid overwrites. You can use "/" in your session name to create subdirectories.'
            ))
        self.menu.append(
            ui.Info_Text(
                'Recordings are saved to "~/pupil_recordings". You can change the path here but note that invalid input will be ignored.'
            ))
        self.menu.append(
            ui.Text_Input('rec_dir',
                          self,
                          setter=self.set_rec_dir,
                          label='Path to recordings'))
        self.menu.append(
            ui.Text_Input('session_name',
                          self,
                          setter=self.set_session_name,
                          label='Recording session name'))
        self.menu.append(
            ui.Switch('show_info_menu',
                      self,
                      on_val=True,
                      off_val=False,
                      label='Request additional user info'))
        self.menu.append(
            ui.Selector(
                'raw_jpeg',
                self,
                selection=[True, False],
                labels=["bigger file, less CPU", "smaller file, more CPU"],
                label='Compression'))
        self.menu.append(
            ui.Info_Text(
                'Recording the raw eye video is optional. We use it for debugging.'
            ))
        self.menu.append(
            ui.Switch('record_eye',
                      self,
                      on_val=True,
                      off_val=False,
                      label='Record eye'))

        def audio_dev_getter():
            # fetch list of currently available
            self.audio_devices_dict = Audio_Input_Dict()
            devices = list(self.audio_devices_dict.keys())
            return devices, devices

        self.menu.append(
            ui.Selector('audio_src',
                        self,
                        selection_getter=audio_dev_getter,
                        label='Audio Source'))

        self.button = ui.Thumb('running',
                               self,
                               setter=self.toggle,
                               label='R',
                               hotkey='r')
        self.button.on_color[:] = (1, .0, .0, .8)
        self.g_pool.quickbar.insert(1, self.button)

    def deinit_gui(self):
        if self.menu:
            self.g_pool.sidebar.remove(self.menu)
            self.menu = None
        if self.button:
            self.g_pool.quickbar.remove(self.button)
            self.button = None

    def toggle(self, _=None):
        if self.running:
            self.notify_all({'subject': 'recording.should_stop'})
            self.notify_all({
                'subject': 'recording.should_stop',
                'remote_notify': 'all'
            })
        else:
            self.notify_all({
                'subject': 'recording.should_start',
                'session_name': self.session_name
            })
            self.notify_all({
                'subject': 'recording.should_start',
                'session_name': self.session_name,
                'remote_notify': 'all'
            })

    def on_notify(self, notification):
        """Handles recorder notifications

        Reacts to notifications:
            ``recording.should_start``: Starts a new recording session
            ``recording.should_stop``: Stops current recording session

        Emits notifications:
            ``recording.started``: New recording session started
            ``recording.stopped``: Current recording session stopped

        Args:
            notification (dictionary): Notification dictionary
        """
        # notification wants to be recorded
        if notification.get('record', False) and self.running:
            if 'timestamp' not in notification:
                logger.error(
                    "Notification without timestamp will not be saved.")
            else:
                self.data['notifications'].append(notification)
        elif notification['subject'] == 'recording.should_start':
            if self.running:
                logger.info('Recording already running!')
            elif not self.g_pool.capture.online:
                logger.error(
                    "Current world capture is offline. Please reconnect or switch to fake capture"
                )
            else:
                if notification.get("session_name", ""):
                    self.set_session_name(notification["session_name"])
                self.start()

        elif notification['subject'] == 'recording.should_stop':
            if self.running:
                self.stop()
            else:
                logger.info('Recording already stopped!')

    def get_rec_time_str(self):
        rec_time = gmtime(time() - self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self):
        self.timestamps = []
        self.data = {
            'pupil_positions': [],
            'gaze_positions': [],
            'notifications': []
        }
        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()

        session = os.path.join(self.rec_dir, self.session_name)
        try:
            os.makedirs(session)
            logger.debug(
                "Created new recordings session dir {}".format(session))

        except:
            logger.debug(
                "Recordings session dir {} already exists, using it.".format(
                    session))

        # set up self incrementing folder within session folder
        counter = 0
        while True:
            self.rec_path = os.path.join(session, "{:03d}/".format(counter))
            try:
                os.mkdir(self.rec_path)
                logger.debug("Created new recording dir {}".format(
                    self.rec_path))
                break
            except:
                logger.debug(
                    "We dont want to overwrite data, incrementing counter & trying to make new data folder"
                )
                counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, 'w', newline='') as csvfile:
            csv_utils.write_key_value_file(
                csvfile, {
                    'Recording Name': self.session_name,
                    'Start Date': strftime("%d.%m.%Y",
                                           localtime(self.start_time)),
                    'Start Time': strftime("%H:%M:%S",
                                           localtime(self.start_time))
                })

        if self.audio_src != 'No Audio':
            audio_path = os.path.join(self.rec_path, "world.wav")
            self.audio_writer = Audio_Capture(
                audio_path, self.audio_devices_dict[self.audio_src])
        else:
            self.audio_writer = None

        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = JPEG_Writer(self.video_path,
                                      self.g_pool.capture.frame_rate)
        else:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = AV_Writer(self.video_path,
                                    fps=self.g_pool.capture.frame_rate)

        try:
            cal_pt_path = os.path.join(self.g_pool.user_dir,
                                       "user_calibration_data")
            cal_data = load_object(cal_pt_path)
            notification = {
                'subject': 'calibration.calibration_data',
                'record': True
            }
            notification.update(cal_data)
            self.data['notifications'].append(notification)
        except:
            pass

        if self.show_info_menu:
            self.open_info_menu()
        logger.info("Started Recording.")
        self.notify_all({
            'subject': 'recording.started',
            'rec_path': self.rec_path,
            'session_name': self.session_name,
            'record_eye': self.record_eye,
            'compression': self.raw_jpeg
        })

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu('additional Recording Info',
                                         size=(300, 300),
                                         pos=(300, 300))
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.keys():
                self.info_menu.insert(0, ui.Text_Input(name, self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(
            ui.Info_Text(
                'Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'
            ))
        self.info_menu.append(
            ui.Text_Input('user_info',
                          self,
                          setter=set_user_info,
                          label="User info"))
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def recent_events(self, events):
        if self.running:
            for key, data in events.items():
                if key not in ('dt', 'frame'):
                    try:
                        self.data[key] += data
                    except KeyError:
                        self.data[key] = []
                        self.data[key] += data

            if 'frame' in events:
                frame = events['frame']
                self.timestamps.append(frame.timestamp)
                self.writer.write_video_frame(frame)
                self.frame_count += 1

            # # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))

            self.button.status_text = self.get_rec_time_str()

    def stop(self):
        # explicit release of VideoWriter
        self.writer.release()
        self.writer = None

        save_object(self.data, os.path.join(self.rec_path, "pupil_data"))

        timestamps_path = os.path.join(self.rec_path, "world_timestamps.npy")
        # ts = sanitize_timestamps(np.array(self.timestamps))
        ts = np.array(self.timestamps)
        np.save(timestamps_path, ts)

        try:
            copy2(os.path.join(self.g_pool.user_dir, "surface_definitions"),
                  os.path.join(self.rec_path, "surface_definitions"))
        except:
            logger.info(
                "No surface_definitions data found. You may want this if you do marker tracking."
            )

        camera_calibration = load_camera_calibration(self.g_pool)
        if camera_calibration is not None:
            save_object(camera_calibration,
                        os.path.join(self.rec_path, "camera_calibration"))
        else:
            logger.info("No camera calibration found.")

        try:
            with open(self.meta_info_path, 'a', newline='') as csvfile:
                csv_utils.write_key_value_file(csvfile, {
                    'Duration Time':
                    self.get_rec_time_str(),
                    'World Camera Frames':
                    self.frame_count,
                    'World Camera Resolution':
                    str(self.g_pool.capture.frame_size[0]) + "x" +
                    str(self.g_pool.capture.frame_size[1]),
                    'Capture Software Version':
                    self.g_pool.version,
                    'Data Format Version':
                    self.g_pool.version,
                    'System Info':
                    get_system_info()
                },
                                               append=True)
        except Exception:
            logger.exception(
                "Could not save metadata. Please report this bug!")

        try:
            with open(os.path.join(self.rec_path, "user_info.csv"),
                      'w',
                      newline='') as csvfile:
                csv_utils.write_key_value_file(csvfile, self.user_info)
        except Exception:
            logger.exception(
                "Could not save userdata. Please report this bug!")

        self.close_info_menu()

        if self.audio_writer:
            self.audio_writer = None

        self.running = False
        self.menu.read_only = False
        self.button.status_text = ''

        self.timestamps = []
        self.data = {'pupil_positions': [], 'gaze_positions': []}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        logger.info("Saved Recording.")
        self.notify_all({
            'subject': 'recording.stopped',
            'rec_path': self.rec_path
        })

    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()
        self.deinit_gui()

    def verify_path(self, val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '{}'.".format(n_path))
            return False
        else:
            return n_path

    def set_rec_dir(self, val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if os.path.sep in val:
                logger.warning(
                    'You session name will create one or more subdirectories')
            self.session_name = val
Exemple #7
0
class Recorder(Plugin):
    """Capture Recorder"""
    def __init__(self,g_pool,session_name = get_auto_name(),rec_dir=None, user_info={},info_menu_conf={},show_info_menu=False, record_eye = True, audio_src = 'No Audio',raw_jpeg=True):
        super(Recorder, self).__init__(g_pool)
        #update name if it was autogenerated.
        if session_name.startswith('20') and len(session_name)==10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep,1)[0]
        default_rec_dir = os.path.join(base_dir,'recordings')

        if rec_dir and rec_dir != default_rec_dir and self.verify_path(rec_dir):
            self.rec_dir = rec_dir
        else:
            try:
                os.makedirs(default_rec_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info('Created standard Rec dir at "%s"'%default_rec_dir)
            self.rec_dir = default_rec_dir

        self.raw_jpeg = raw_jpeg
        self.order = .9
        self.record_eye = record_eye
        self.session_name = session_name
        self.audio_devices_dict = Audio_Input_Dict()
        if audio_src in self.audio_devices_dict.keys():
            self.audio_src = audio_src
        else:
            self.audio_src = 'No Audio'
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf

        self.calibration_start_at = None
        self.calibration_end_at = None

    def get_init_dict(self):
        d = {}
        d['record_eye'] = self.record_eye
        d['audio_src'] = self.audio_src
        d['session_name'] = self.session_name
        d['user_info'] = self.user_info
        d['info_menu_conf'] = self.info_menu_conf
        d['show_info_menu'] = self.show_info_menu
        d['rec_dir'] = self.rec_dir
        d['raw_jpeg'] = self.raw_jpeg
        return d


    def init_gui(self):
        self.menu = ui.Growing_Menu('Recorder')
        self.g_pool.sidebar.insert(3,self.menu)
        self.menu.append(ui.Info_Text('Pupil recordings are saved like this: "path_to_recordings/recording_session_name/nnn" where "nnn" is an increasing number to avoid overwrites. You can use "/" in your session name to create subdirectories.'))
        self.menu.append(ui.Info_Text('Recordings are saved to "~/pupil_recordings". You can change the path here but note that invalid input will be ignored.'))
        self.menu.append(ui.Text_Input('rec_dir',self,setter=self.set_rec_dir,label='Path to recordings'))
        self.menu.append(ui.Text_Input('session_name',self,setter=self.set_session_name,label='Recording session name'))
        self.menu.append(ui.Switch('show_info_menu',self,on_val=True,off_val=False,label='Request additional user info'))
        self.menu.append(ui.Selector('raw_jpeg',self,selection = [True,False], labels=["bigger file, less CPU", "smaller file, more CPU"],label='Compression'))
        self.menu.append(ui.Info_Text('Recording the raw eye video is optional. We use it for debugging.'))
        self.menu.append(ui.Switch('record_eye',self,on_val=True,off_val=False,label='Record eye'))
        self.menu.append(ui.Selector('audio_src',self, selection=self.audio_devices_dict.keys(),label='Audio Source'))

        # record on enter click from the remote shutter
        self.button = ui.Thumb('running',self,setter=self.toggle,label='Record',hotkey=GLFW_KEY_ENTER)

        self.button.on_color[:] = (1,.0,.0,.8)
        self.g_pool.quickbar.insert(1,self.button)


    def deinit_gui(self):
        if self.menu:
            self.g_pool.sidebar.remove(self.menu)
            self.menu = None
        if self.button:
            self.g_pool.quickbar.remove(self.button)
            self.button = None



    def toggle(self, _=None):
        if self.running:
            if self.calibration_start_at and not self.calibration_end_at: # Mark the end of claibration
                self.calibration_end_at = self.timestamps[-1]
            else:
                self.notify_all( {'subject':'should_stop_recording','network_propagate':True} )
        else:
            self.notify_all( {'subject':'should_start_recording','session_name':self.session_name,'network_propagate':True} )


    def on_notify(self,notification):

        # notification wants to be recorded
        if notification.get('record',False) and self.running:
            self.data['notifications'].append(notification)


        # Notificatio to start recording
        elif notification['subject'] == 'should_start_recording':
            if self.running:
                logger.info('Recording already running!')
            else:
                if notification.get("session_name",""):
                    self.set_session_name(notification["session_name"])
                self.start()
        # Remote has stopped recording, we should stop as well.
        elif notification['subject'] == 'should_stop_recording':
            if self.running:
                self.stop(shutdown_os=True)
            else:
                logger.info('Recording already stopped!')


    def get_rec_time_str(self):
        rec_time = gmtime(time()-self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self):
        self.timestamps = []
        self.data = {'pupil_positions':[],'gaze_positions':[],'notifications':[]}
        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()

        session = os.path.join(self.rec_dir, self.session_name)
        try:
            os.makedirs(session)
            logger.debug("Created new recordings session dir %s"%session)

        except:
            logger.debug("Recordings session dir %s already exists, using it." %session)

        # set up self incrementing folder within session folder
        counter = 0
        while True:
            self.rec_path = os.path.join(session, "%03d/" % counter)
            try:
                os.mkdir(self.rec_path)
                logger.debug("Created new recording dir %s"%self.rec_path)
                break
            except:
                logger.debug("We dont want to overwrite data, incrementing counter & trying to make new data folder")
                counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, 'w') as f:
            f.write("Recording Name\t"+self.session_name+ "\n")
            f.write("Start Date\t"+ strftime("%d.%m.%Y", localtime(self.start_time))+ "\n")
            f.write("Start Time\t"+ strftime("%H:%M:%S", localtime(self.start_time))+ "\n")


        if self.audio_src != 'No Audio':
            audio_path = os.path.join(self.rec_path, "world.wav")
            self.audio_writer = Audio_Capture(audio_path,self.audio_devices_dict[self.audio_src])
        else:
            self.audio_writer = None

        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = JPEG_Writer(self.video_path,self.g_pool.capture.frame_rate)
        else:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = AV_Writer(self.video_path,fps=self.g_pool.capture.frame_rate)

        # positions path to eye process
        if self.record_eye:
            for alive, pipe in zip(self.g_pool.eyes_are_alive,self.g_pool.eye_pipes):
                if alive.value:
                    pipe.send( ('Rec_Start',(self.rec_path,self.raw_jpeg) ) )

        if self.show_info_menu:
            self.open_info_menu()
        logger.info("Started Recording.")
        self.notify_all( {'subject':'rec_started','rec_path':self.rec_path,'session_name':self.session_name,'network_propagate':True} )

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu('additional Recording Info',size=(300,300),pos=(300,300))
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.iterkeys():
                self.info_menu.insert(0,ui.Text_Input(name,self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(ui.Info_Text('Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'))
        self.info_menu.append(ui.Text_Input('user_info',self,setter=set_user_info,label="User info"))
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def update(self,frame,events):
        ''' listen to commands from eye window '''
        if self.g_pool.eye_pipes[0].poll():
            cmd = self.g_pool.eye_pipes[0].recv()
            # toggle recording if enter key clicked
            if cmd == GLFW_KEY_ENTER:
                self.toggle()
        if self.running:
            # for key,data in events.iteritems():
            #     if key not in ('dt'):
            #         try:
            #             self.data[key] += data
            #         except KeyError:
            #             self.data[key] = []
            #             self.data[key] += data

            ''' Record when it first started as this will be the start of calibration. When user click Enter again this
             should end calibration. Click once more to end recording'''
            if not self.calibration_start_at:
                self.calibration_start_at = frame.timestamp

            self.timestamps.append(frame.timestamp)
            self.writer.write_video_frame(frame)
            self.frame_count += 1

            # # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))

            self.button.status_text = self.get_rec_time_str()

    def stop(self, shutdown_os=False):
        #explicit release of VideoWriter
        self.writer.release()
        self.writer = None

        if self.record_eye:
            for alive, pipe in zip(self.g_pool.eyes_are_alive,self.g_pool.eye_pipes):
                if alive.value:
                    pipe.send(('Rec_Stop',None))

        # Mohammad: This will now save an empty dicts list
        save_object(self.data,os.path.join(self.rec_path, "pupil_data"))

        timestamps_path = os.path.join(self.rec_path, "world_timestamps.npy")
        # ts = sanitize_timestamps(np.array(self.timestamps))
        ts = np.array(self.timestamps)
        np.save(timestamps_path,ts)

        try:
            copy2(os.path.join(self.g_pool.user_dir,"surface_definitions"),os.path.join(self.rec_path,"surface_definitions"))
        except:
            logger.info("No surface_definitions data found. You may want this if you do marker tracking.")
        try:
            copy2(os.path.join(self.g_pool.user_dir,"user_calibration_data"),os.path.join(self.rec_path,"user_calibration_data"))
        except:
            logger.warning("No user calibration data found. Please calibrate first.")

        # camera_calibration = load_camera_calibration(self.g_pool)
        # if camera_calibration is not None:
        #     save_object(camera_calibration,os.path.join(self.rec_path, "camera_calibration"))
        # else:
        #     logger.info("No camera calibration found.")

        try:
            with open(self.meta_info_path, 'a') as f:
                f.write("Duration Time\t"+ self.get_rec_time_str()+ "\n")
                f.write("World Camera Frames\t"+ str(self.frame_count)+ "\n")
                f.write("World Camera Resolution\t"+ str(self.g_pool.capture.frame_size[0])+"x"+str(self.g_pool.capture.frame_size[1])+"\n")
                f.write("Capture Software Version\t%s\n"%self.g_pool.version)
                f.write("System Info\t%s"%get_system_info())
        except Exception:
            logger.exception("Could not save metadata. Please report this bug!")

        try:
            with open(os.path.join(self.rec_path, "user_info.csv"), 'w') as f:
                self.user_info.update({'cal_start':self.calibration_start_at, 'cal_end':self.calibration_end_at})
                for name,val in self.user_info.iteritems():
                    f.write("%s\t%s\n"%(name,val))
        except Exception:
            logger.exception("Could not save userdata. Please report this bug!")

        self.calibration_start_at = None
        self.calibration_end_at = None


        self.close_info_menu()

        if self.audio_writer:
            self.audio_writer = None

        self.running = False
        self.menu.read_only = False
        self.button.status_text = ''

        self.timestamps = []
        self.data = {'pupil_positions':[],'gaze_positions':[]}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        logger.info("Saved Recording.")
        self.notify_all( {'subject':'rec_stopped','rec_path':self.rec_path,'network_propagate':True} )
        #     Mohammad - shutdown OS when stop recording
        if shutdown_os:
            os.system('shutdown now')

    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()
        self.deinit_gui()

    def verify_path(self,val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '%s'."%n_path)
            return False
        else:
            return n_path

    def set_rec_dir(self,val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if '/' in val:
                logger.warning('You session name will create one or more subdirectories')
            self.session_name = val
Exemple #8
0
class Recorder(Plugin):
    """Capture Recorder"""
    def __init__(self,g_pool,session_name = get_auto_name(),rec_dir=None, user_info={'name':'','additional_field':'change_me'},info_menu_conf={},show_info_menu=False, record_eye = True, audio_src = 'No Audio',raw_jpeg=True):
        super(Recorder, self).__init__(g_pool)
        #update name if it was autogenerated.
        if session_name.startswith('20') and len(session_name)==10:
            session_name = get_auto_name()

        base_dir = self.g_pool.user_dir.rsplit(os.path.sep,1)[0]
        default_rec_dir = os.path.join(base_dir,'recordings')

        if rec_dir and rec_dir != default_rec_dir and self.verify_path(rec_dir):
            self.rec_dir = rec_dir
        else:
            try:
                os.makedirs(default_rec_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    logger.error("Could not create Rec dir")
                    raise e
            else:
                logger.info('Created standard Rec dir at "%s"'%default_rec_dir)
            self.rec_dir = default_rec_dir

        self.raw_jpeg = raw_jpeg
        self.order = .9
        self.record_eye = record_eye
        self.session_name = session_name
        self.audio_devices_dict = Audio_Input_Dict()
        if audio_src in self.audio_devices_dict.keys():
            self.audio_src = audio_src
        else:
            self.audio_src = 'No Audio'
        self.running = False
        self.menu = None
        self.button = None

        self.user_info = user_info
        self.show_info_menu = show_info_menu
        self.info_menu = None
        self.info_menu_conf = info_menu_conf


    def get_init_dict(self):
        d = {}
        d['record_eye'] = self.record_eye
        d['audio_src'] = self.audio_src
        d['session_name'] = self.session_name
        d['user_info'] = self.user_info
        d['info_menu_conf'] = self.info_menu_conf
        d['show_info_menu'] = self.show_info_menu
        d['rec_dir'] = self.rec_dir
        d['raw_jpeg'] = self.raw_jpeg
        return d


    def init_gui(self):
        self.menu = ui.Growing_Menu('Recorder')
        self.g_pool.sidebar.insert(3,self.menu)
        self.menu.append(ui.Info_Text('Pupil recordings are saved like this: "path_to_recordings/recording_session_name/nnn" where "nnn" is an increasing number to avoid overwrites. You can use "/" in your session name to create subdirectories.'))
        self.menu.append(ui.Info_Text('Recordings are saved to "~/pupil_recordings". You can change the path here but note that invalid input will be ignored.'))
        self.menu.append(ui.Text_Input('rec_dir',self,setter=self.set_rec_dir,label='Path to recordings'))
        self.menu.append(ui.Text_Input('session_name',self,setter=self.set_session_name,label='Recording session name'))
        self.menu.append(ui.Switch('show_info_menu',self,on_val=True,off_val=False,label='Request additional user info'))
        self.menu.append(ui.Selector('raw_jpeg',self,selection = [True,False], labels=["bigger file, less CPU", "smaller file, more CPU"],label='Compression'))
        self.menu.append(ui.Info_Text('Recording the raw eye video is optional. We use it for debugging.'))
        self.menu.append(ui.Switch('record_eye',self,on_val=True,off_val=False,label='Record eye'))
        self.menu.append(ui.Selector('audio_src',self, selection=self.audio_devices_dict.keys(),label='Audio Source'))

        self.button = ui.Thumb('running',self,setter=self.toggle,label='Record',hotkey='r')
        self.button.on_color[:] = (1,.0,.0,.8)
        self.g_pool.quickbar.insert(1,self.button)


    def deinit_gui(self):
        if self.menu:
            self.g_pool.sidebar.remove(self.menu)
            self.menu = None
        if self.button:
            self.g_pool.quickbar.remove(self.button)
            self.button = None



    def toggle(self, _=None):
        if self.running:
            self.notify_all( {'subject':'recording.should_stop'} )
            self.notify_all( {'subject':'recording.should_stop', 'remote_notify':'all'} )
        else:
            self.notify_all( {'subject':'recording.should_start','session_name':self.session_name} )
            self.notify_all( {'subject':'recording.should_start','session_name':self.session_name,'remote_notify':'all'} )


    def on_notify(self,notification):
        """Handles recorder notifications

        Reacts to notifications:
            ``recording.should_start``: Starts a new recording session
            ``recording.should_stop``: Stops current recording session

        Emits notifications:
            ``recording.started``: New recording session started
            ``recording.stopped``: Current recording session stopped

        Args:
            notification (dictionary): Notification dictionary
        """
        # notification wants to be recorded
        if notification.get('record',False) and self.running:
            self.data['notifications'].append(notification)


        elif notification['subject'] == 'recording.should_start':
            if self.running:
                logger.info('Recording already running!')
            else:
                if notification.get("session_name",""):
                    self.set_session_name(notification["session_name"])
                self.start()

        elif notification['subject'] == 'recording.should_stop':
            if self.running:
                self.stop()
            else:
                logger.info('Recording already stopped!')


    def get_rec_time_str(self):
        rec_time = gmtime(time()-self.start_time)
        return strftime("%H:%M:%S", rec_time)

    def start(self):
        self.timestamps = []
        self.timestampsUnix = []
        self.glint_pos_list = []
        self.pupil_pos_list = []
        self.gaze_pos_list = []
        self.data = {'pupil_positions':[],'gaze_positions':[],'notifications':[]}
        self.frame_count = 0
        self.running = True
        self.menu.read_only = True
        self.start_time = time()

        session = os.path.join(self.rec_dir, self.session_name)
        try:
            os.makedirs(session)
            logger.debug("Created new recordings session dir %s"%session)

        except:
            logger.debug("Recordings session dir %s already exists, using it." %session)

        # set up self incrementing folder within session folder
        counter = 0
        while True:
            self.rec_path = os.path.join(session, "%03d/" % counter)
            try:
                os.mkdir(self.rec_path)
                logger.debug("Created new recording dir %s"%self.rec_path)
                break
            except:
                logger.debug("We dont want to overwrite data, incrementing counter & trying to make new data folder")
                counter += 1

        self.meta_info_path = os.path.join(self.rec_path, "info.csv")

        with open(self.meta_info_path, 'w') as csvfile:
            csv_utils.write_key_value_file(csvfile,{
                'Recording Name': self.session_name,
                'Start Date': strftime("%d.%m.%Y", localtime(self.start_time)),
                'Start Time': strftime("%H:%M:%S", localtime(self.start_time)),
                'Start Time (seconds since epoch)':  str(self.start_time)
            })

        if self.audio_src != 'No Audio':
            audio_path = os.path.join(self.rec_path, "world.wav")
            self.audio_writer = Audio_Capture(audio_path,self.audio_devices_dict[self.audio_src])
        else:
            self.audio_writer = None

        if self.raw_jpeg and self.g_pool.capture.jpeg_support:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = JPEG_Writer(self.video_path,self.g_pool.capture.frame_rate)
        else:
            self.video_path = os.path.join(self.rec_path, "world.mp4")
            self.writer = AV_Writer(self.video_path,fps=self.g_pool.capture.frame_rate)

        if self.show_info_menu:
            self.open_info_menu()
        logger.info("Started Recording.")
        self.notify_all( {'subject':'recording.started','rec_path':self.rec_path,'session_name':self.session_name,'record_eye':self.record_eye,'compression':self.raw_jpeg} )

    def open_info_menu(self):
        self.info_menu = ui.Growing_Menu('additional Recording Info',size=(300,300),pos=(300,300))
        self.info_menu.configuration = self.info_menu_conf

        def populate_info_menu():
            self.info_menu.elements[:-2] = []
            for name in self.user_info.iterkeys():
                self.info_menu.insert(0,ui.Text_Input(name,self.user_info))

        def set_user_info(new_string):
            self.user_info = new_string
            populate_info_menu()

        populate_info_menu()
        self.info_menu.append(ui.Info_Text('Use the *user info* field to add/remove additional fields and their values. The format must be a valid Python dictionary. For example -- {"key":"value"}. You can add as many fields as you require. Your custom fields will be saved for your next session.'))
        self.info_menu.append(ui.Text_Input('user_info',self,setter=set_user_info,label="User info"))
        self.g_pool.gui.append(self.info_menu)

    def close_info_menu(self):
        if self.info_menu:
            self.info_menu_conf = self.info_menu.configuration
            self.g_pool.gui.remove(self.info_menu)
            self.info_menu = None

    def update(self,frame,events):
        if self.running:
            for key,data in events.iteritems():
                if key not in ('dt') and key != 'timestamp_unix':
                    try:
                        self.data[key] += data
                    except KeyError:
                        self.data[key] = []
                        self.data[key] += data

            self.timestamps.append(frame.timestamp)
            self.writer.write_video_frame(frame)
            self.frame_count += 1

            for glint in events['glint_positions']:
                self.glint_pos_list += glint
            self.timestampsUnix.append(events['timestamp_unix'])
            # # cv2.putText(frame.img, "Frame %s"%self.frame_count,(200,200), cv2.FONT_HERSHEY_SIMPLEX,1,(255,100,100))

            self.button.status_text = self.get_rec_time_str()

    def stop(self):
        #explicit release of VideoWriter
        self.writer.release()
        self.writer = None

        save_object(self.data,os.path.join(self.rec_path, "pupil_data"))



        self.glint_pos_list = np.array(self.glint_pos_list)
        glint_list_path = os.path.join(self.rec_path, "glint_positions.npy")
        np.save(glint_list_path,self.glint_pos_list)

        timestamps_path = os.path.join(self.rec_path, "world_timestamps.npy")
        # ts = sanitize_timestamps(np.array(self.timestamps))
        ts = np.array(self.timestamps)
        np.save(timestamps_path,ts)

        timestampsUnix_path = os.path.join(self.rec_path, "world_timestamps_unix.npy")
        tsUnix = np.array(self.timestampsUnix)
        np.save(timestampsUnix_path,tsUnix)


        try:
            copy2(os.path.join(self.g_pool.user_dir,"surface_definitions"),os.path.join(self.rec_path,"surface_definitions"))
        except:
            logger.info("No surface_definitions data found. You may want this if you do marker tracking.")
        try:
            copy2(os.path.join(self.g_pool.user_dir,"user_calibration_data"),os.path.join(self.rec_path,"user_calibration_data"))
        except:
            logger.warning("No user calibration data found. Please calibrate first.")

        try:
            copy2(os.path.join(self.g_pool.user_dir,"cal_pt_cloud_glint.npy"),os.path.join(self.rec_path,"cal_pt_cloud_glint.npy"))
        except:
            logger.warning("No pupil-glint-vector calibration data found. Please calibrate first.")

        try:
            copy2(os.path.join(self.g_pool.user_dir,"cal_ref_list.npy"),os.path.join(self.rec_path,"cal_ref_list.npy"))
        except:
            logger.warning("No calibration reference list found.")

        try:
            copy2(os.path.join(self.g_pool.user_dir,"accuracy_test_pt_cloud.npy"),os.path.join(self.rec_path,"accuracy_test_pt_cloud.npy"))
            copy2(os.path.join(self.g_pool.user_dir,"accuracy_test_ref_list.npy"),os.path.join(self.rec_path,"accuracy_test_ref_list.npy"))
        except:
            logger.warning("No accuracy test found.")

        try:
            copy2(os.path.join(self.g_pool.user_dir,"accuracy_test_pt_cloud_previous.npy"),os.path.join(self.rec_path,"accuracy_test_pt_cloud_previous.npy"))
            copy2(os.path.join(self.g_pool.user_dir,"accuracy_test_ref_list_previous.npy"),os.path.join(self.rec_path,"accuracy_test_ref_list_previous.npy"))
        except:
            logger.warning("No previous accuracy test results.")


        camera_calibration = load_camera_calibration(self.g_pool)
        if camera_calibration is not None:
            save_object(camera_calibration,os.path.join(self.rec_path, "camera_calibration"))
        else:
            logger.info("No camera calibration found.")

        try:
            with open(self.meta_info_path, 'a') as csvfile:
                csv_utils.write_key_value_file(csvfile, {
                    'Duration Time': self.get_rec_time_str(),
                    'World Camera Frames': self.frame_count,
                    'World Camera Resolution': str(self.g_pool.capture.frame_size[0])+"x"+str(self.g_pool.capture.frame_size[1]),
                    'Capture Software Version': self.g_pool.version,
                    'System Info': get_system_info()
                }, append=True)
        except Exception:
            logger.exception("Could not save metadata. Please report this bug!")

        try:
            with open(os.path.join(self.rec_path, "user_info.csv"), 'w') as csvfile:
                csv_utils.write_key_value_file(csvfile, self.user_info)
        except Exception:
            logger.exception("Could not save userdata. Please report this bug!")


        self.close_info_menu()

        if self.audio_writer:
            self.audio_writer = None

        self.running = False
        self.menu.read_only = False
        self.button.status_text = ''

        self.timestamps = []
        self.data = {'pupil_positions':[],'gaze_positions':[]}
        self.pupil_pos_list = []
        self.gaze_pos_list = []

        logger.info("Saved Recording.")
        self.notify_all( {'subject':'rec_stopped','rec_path':self.rec_path,'network_propagate':True} )
        self.notify_all( {'subject':'recording.stopped','rec_path':self.rec_path} )

        copyfile(os.path.join(self.g_pool.user_dir,'capture.log'), os.path.join(self.rec_path,"capture.log"))

    def cleanup(self):
        """gets called when the plugin get terminated.
           either volunatily or forced.
        """
        if self.running:
            self.stop()
        self.deinit_gui()

    def verify_path(self,val):
        try:
            n_path = os.path.expanduser(val)
            logger.debug("Expanded user path.")
        except:
            n_path = val
        if not n_path:
            logger.warning("Please specify a path.")
            return False
        elif not os.path.isdir(n_path):
            logger.warning("This is not a valid path.")
            return False
        # elif not os.access(n_path, os.W_OK):
        elif not writable_dir(n_path):
            logger.warning("Do not have write access to '%s'."%n_path)
            return False
        else:
            return n_path

    def set_rec_dir(self,val):
        n_path = self.verify_path(val)
        if n_path:
            self.rec_dir = n_path

    def set_session_name(self, val):
        if not val:
            self.session_name = get_auto_name()
        else:
            if '/' in val:
                logger.warning('You session name will create one or more subdirectories')
            self.session_name = val