def _video_path_for_eye(self, eye_id: int) -> str: # Get all eye videos for eye_id recording = PupilRecording(self.g_pool.rec_dir) eye_videos = list(recording.files().videos().eye_id(eye_id)) if eye_videos: return str(eye_videos[0]) else: return "/not/found/eye{}.mp4".format(eye_id)
def _init_videoset(self): rec, set_name = self.get_rec_set_name(self.source_path) self.videoset = VideoSet(rec, set_name, self.fill_gaps) self.videoset.load_or_build_lookup() if self.videoset.is_empty() and self.fill_gaps: # create artificial lookup table here recording = PupilRecording(rec) start_time = recording.meta_info.start_time_synced_s if ( recording.meta_info.recording_software_name == recording.meta_info.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE ): # TODO: Currently PI timestamp data is shifted to start at 0 (by # subtracting start_time_synced) in order to deal with opengl rounding # issues for large numbers in float32 precision. This will change in the # future, which will require this to be updated again. start_time = 0 duration = recording.meta_info.duration_s # since the eye recordings might be slightly longer than the world recording # (due to notification delays) we want to make sure that we generate enough # fake world frames to display all eye data, so we make the world recording # artificially longer BACK_BUFFER_SECONDS = 3 end_time = start_time + duration + BACK_BUFFER_SECONDS fallback_framerate = 30 timestamps = np.arange(start_time, end_time, 1 / fallback_framerate) self.videoset.build_lookup(timestamps) assert not self.videoset.is_empty()
def _get_recording_start_date(source_folder): recording = PupilRecording(source_folder) meta = recording.meta_info # NOTE: This is potentially incorrect, since we don't know the timezone. But we are # keeping this format for backwards compatibility with the old-style info.csv. start_datetime = datetime.datetime.fromtimestamp(meta.start_time_system_s) return start_datetime.strftime("%d_%m_%Y_%H_%M_%S")
def _copy_info_csv(source_folder, destination_folder): # TODO: The iMotions export still relies on the old-style info.csv, so we have to # generate this here manually. We should clarify with iMotions whether we can update # this to our new recording format. recording = PupilRecording(source_folder) meta = recording.meta_info # NOTE: This is potentially incorrect, since we don't know the timezone. But we are # keeping this format for backwards compatibility with the old-style info.csv. start_datetime = datetime.datetime.fromtimestamp(meta.start_time_system_s) start_date = start_datetime.strftime("%d.%m.%Y") start_time = start_datetime.strftime("%H:%M:%S") duration_full_s = meta.duration_s duration_h = int(duration_full_s // 3600) duration_m = int((duration_full_s % 3600) // 60) duration_s = int(round(duration_full_s % 3600 % 60)) duration_time = f"{duration_h:02}:{duration_m:02}:{duration_s:02}" try: world_video = recording.files().core().world().videos()[0] except IndexError: logger.error( "Error while exporting iMotions data. World video not found!") return cap = File_Source(SimpleNamespace(), world_video) world_frames = cap.get_frame_count() world_resolution = f"{cap.frame_size[0]}x{cap.frame_size[1]}" data = {} data["Recording Name"] = meta.recording_name data["Start Date"] = start_date data["Start Time"] = start_time data["Start Time (System)"] = meta.start_time_system_s data["Start Time (Synced)"] = meta.start_time_synced_s data["Recording UUID"] = str(meta.recording_uuid) data["Duration Time"] = duration_time data["World Camera Frames"] = world_frames data["World Camera Resolution"] = world_resolution data["Capture Software Version"] = meta.recording_software_version data["Data Format Version"] = str(meta.min_player_version) data["System Info"] = meta.system_info info_dest = os.path.join(destination_folder, "iMotions_info.csv") with open(info_dest, "w", newline="", encoding="utf-8") as f: csv_utils.write_key_value_file(f, data)
def generate_frames(g_pool): recording = PupilRecording(g_pool.rec_dir) video_path = recording.files().world().videos()[0] fs = File_Source(g_pool, source_path=video_path, fill_gaps=True) total_frame_count = fs.get_frame_count() while True: try: current_frame = fs.get_frame() except EndofVideoError: break progress = current_frame.index / total_frame_count yield progress, current_frame
def is_available_within_context(cls, g_pool) -> bool: if g_pool.app == "player": recording = PupilRecording(rec_dir=g_pool.rec_dir) meta_info = recording.meta_info if (meta_info.recording_software_name == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE): # Disable blink detector in Player if Pupil Invisible recording return False return super().is_available_within_context(g_pool)
def is_available_within_context(cls, g_pool) -> bool: if g_pool.app == "player": recording = PupilRecording(rec_dir=g_pool.rec_dir) meta_info = recording.meta_info if (meta_info.recording_software_name == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_MOBILE): # Disable gaze from recording in Player if Pupil Mobile recording return False return super().is_available_within_context(g_pool)
def is_available_within_context(cls, g_pool) -> bool: if g_pool.app == "player": recording = PupilRecording(rec_dir=g_pool.rec_dir) meta_info = recording.meta_info if (meta_info.recording_software_name == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE): # Enable in Player only if Pupil Invisible recording return True return False
def generate_frame_indices_with_deserialized_gaze(g_pool): recording = PupilRecording(g_pool.rec_dir) video_name = recording.files().world().videos()[0].stem videoset = VideoSet(rec=g_pool.rec_dir, name=video_name, fill_gaps=True) videoset.load_or_build_lookup() frame_indices = np.flatnonzero(videoset.lookup.container_idx > -1) frame_count = len(frame_indices) for frame_index in frame_indices: progress = (frame_index + 1) / frame_count frame_ts_window = pm.enclosing_window(g_pool.timestamps, frame_index) gaze_data = g_pool.gaze_positions.by_ts_window(frame_ts_window) gaze_data = [(frame_index, g["timestamp"], g["norm_pos"][0], g["norm_pos"][1]) for g in gaze_data if g["confidence"] >= g_pool.min_data_confidence] gaze_data = scan_path_numpy_array_from(gaze_data) yield progress, gaze_data
def __init__(self, g_pool): super().__init__(g_pool) self._task_manager = PluginTaskManager(plugin=self) self._current_recording_uuid = str( PupilRecording(g_pool.rec_dir).meta_info.recording_uuid) self._setup_storages() self._setup_controllers() self._setup_renderers() self._setup_menus() self._setup_timelines()
def export_data(self, export_range, export_dir): pupil_recording = PupilRecording(rec_dir=self.g_pool.rec_dir) meta = pupil_recording.meta_info if meta.recording_software_name == meta.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE: logger.error( "The iMotions exporter does not yet support Pupil Invisible recordings!" ) return rec_start = _get_recording_start_date(self.g_pool.rec_dir) im_dir = os.path.join(export_dir, "iMotions_{}".format(rec_start)) try: csv_header, csv_rows = _csv_exported_gaze_data( gaze_positions=self.g_pool.gaze_positions, destination_folder=im_dir, export_range=export_range, timestamps=self.g_pool.timestamps, capture=self.g_pool.capture, ) except _iMotionsExporterNo3DGazeDataError: logger.error( "Currently, the iMotions export only supports 3d gaze data") return try: self.add_export_job( export_range, im_dir, input_name="world", output_name="scene", process_frame=_process_frame, timestamp_export_format=None, ) except FileNotFoundError: logger.info( "'world' video not found. Export continues with gaze data.") _copy_info_csv(self.g_pool.rec_dir, im_dir) gaze_file_path = os.path.join(im_dir, "gaze.tlv") with open(gaze_file_path, "w", encoding="utf-8", newline="") as csv_file: csv_writer = csv.writer(csv_file, delimiter="\t") csv_writer.writerow(csv_header) for csv_row in csv_rows: csv_writer.writerow(csv_row)
def __update_and_save_calibration_v1_as_latest_version(cls, rec_dir, data): legacy_calibration = CalibrationV1.from_tuple(data) recording_uuid = str(PupilRecording(rec_dir).meta_info.recording_uuid) is_imported = legacy_calibration.recording_uuid != recording_uuid if is_imported: raise ValueError( "Updating imported (read-only) calibrations is not supported. " f"{legacy_calibration.name}") updated_calibration = legacy_calibration.updated() cls._save_calibration_to_file(rec_dir, updated_calibration, overwrite_if_exists=False)
def __init__(self, g_pool): super().__init__(g_pool) self.inject_plugin_dependencies() self._task_manager = PluginTaskManager(plugin=self) self._recording_uuid = PupilRecording( g_pool.rec_dir).meta_info.recording_uuid self._setup_storages() self._setup_controllers() self._setup_ui() self._setup_timelines() self._pupil_changed_listener = data_changed.Listener("pupil_positions", g_pool.rec_dir, plugin=self) self._pupil_changed_listener.add_observer( "on_data_changed", self._calculate_all_controller.calculate_all)
def is_available_within_context(cls, g_pool) -> bool: if g_pool.app != "player": # Plugin not available if not running in Player return False recording = PupilRecording(rec_dir=g_pool.rec_dir) meta_info = recording.meta_info if (meta_info.recording_software_name != RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE): # Plugin not available if recording is not from Pupil Invisible return False imu_recs = cls._imu_recordings(g_pool) if not len(imu_recs): # Plugin not available if recording doesn't have IMU files (due to hardware failure, for example) logger.debug( f"{cls.__name__} unavailable because there are no IMU files") return False return True
def _export_world_video( rec_dir, user_dir, min_data_confidence, start_frame, end_frame, plugin_initializers, out_file_path, pre_computed_eye_data, ): """ Simulates the generation for the world video and saves a certain time range as a video. It simulates a whole g_pool such that all plugins run as normal. """ from glob import glob from time import time import file_methods as fm import player_methods as pm from av_writer import MPEG_Audio_Writer # We are not importing manual gaze correction. In Player corrections have already # been applied. from fixation_detector import Offline_Fixation_Detector # Plug-ins from plugin import Plugin_List, import_runtime_plugins from video_capture import EndofVideoError, File_Source from video_overlay.plugins import Video_Overlay, Eye_Overlay from vis_circle import Vis_Circle from vis_cross import Vis_Cross from vis_light_points import Vis_Light_Points from vis_polyline import Vis_Polyline from vis_watermark import Vis_Watermark PID = str(os.getpid()) logger = logging.getLogger(f"{__name__} with pid: {PID}") start_status = f"Starting video export with pid: {PID}" logger.info(start_status) yield start_status, 0 try: vis_plugins = sorted( [ Vis_Circle, Vis_Cross, Vis_Polyline, Vis_Light_Points, Vis_Watermark, Eye_Overlay, Video_Overlay, ], key=lambda x: x.__name__, ) analysis_plugins = [Offline_Fixation_Detector] user_plugins = sorted( import_runtime_plugins(os.path.join(user_dir, "plugins")), key=lambda x: x.__name__, ) available_plugins = vis_plugins + analysis_plugins + user_plugins name_by_index = [p.__name__ for p in available_plugins] plugin_by_name = dict(zip(name_by_index, available_plugins)) recording = PupilRecording(rec_dir) meta_info = recording.meta_info g_pool = GlobalContainer() g_pool.app = "exporter" g_pool.process = "exporter" g_pool.min_data_confidence = min_data_confidence videos = recording.files().core().world().videos() if not videos: raise FileNotFoundError("No world video found") source_path = videos[0].resolve() cap = File_Source(g_pool, source_path=source_path, fill_gaps=True, timing=None) if not cap.initialised: warn = "Trying to export zero-duration world video." logger.warning(warn) yield warn, 0.0 return timestamps = cap.timestamps file_name = os.path.basename(out_file_path) dir_name = os.path.dirname(out_file_path) out_file_path = os.path.expanduser(os.path.join(dir_name, file_name)) if os.path.isfile(out_file_path): logger.warning("Video out file already exsists. I will overwrite!") os.remove(out_file_path) logger.debug("Saving Video to {}".format(out_file_path)) # Trim mark verification # make sure the trim marks (start frame, end frame) make sense: # We define them like python list slices, thus we can test them like such. trimmed_timestamps = timestamps[start_frame:end_frame] if len(trimmed_timestamps) == 0: warn = "Start and end frames are set such that no video will be exported." logger.warning(warn) yield warn, 0.0 return if start_frame is None: start_frame = 0 # these two vars are shared with the launching process and # give a job length and progress report. frames_to_export = len(trimmed_timestamps) current_frame = 0 logger.debug( f"Will export from frame {start_frame} to frame " f"{start_frame + frames_to_export}. This means I will export " f"{frames_to_export} frames.") cap.seek_to_frame(start_frame) start_time = time() g_pool.plugin_by_name = plugin_by_name g_pool.capture = cap g_pool.rec_dir = rec_dir g_pool.user_dir = user_dir g_pool.meta_info = meta_info g_pool.timestamps = timestamps g_pool.delayed_notifications = {} g_pool.notifications = [] for initializers in pre_computed_eye_data.values(): initializers["data"] = [ fm.Serialized_Dict(msgpack_bytes=serialized) for serialized in initializers["data"] ] g_pool.pupil_positions = pm.PupilDataBisector.from_init_dict( pre_computed_eye_data["pupil"]) g_pool.gaze_positions = pm.Bisector(**pre_computed_eye_data["gaze"]) g_pool.fixations = pm.Affiliator(**pre_computed_eye_data["fixations"]) # add plugins g_pool.plugins = Plugin_List(g_pool, plugin_initializers) try: # setup of writer writer = MPEG_Audio_Writer( out_file_path, start_time_synced=trimmed_timestamps[0], audio_dir=rec_dir, ) while frames_to_export > current_frame: try: frame = cap.get_frame() except EndofVideoError: break events = {"frame": frame} # new positions and events frame_window = pm.enclosing_window(g_pool.timestamps, frame.index) events["gaze"] = g_pool.gaze_positions.by_ts_window( frame_window) events["pupil"] = g_pool.pupil_positions.by_ts_window( frame_window) # publish delayed notifications when their time has come. for n in list(g_pool.delayed_notifications.values()): if n["_notify_time_"] < time(): del n["_notify_time_"] del g_pool.delayed_notifications[n["subject"]] g_pool.notifications.append(n) # notify each plugin if there are new notifications: while g_pool.notifications: n = g_pool.notifications.pop(0) for p in g_pool.plugins: p.on_notify(n) # allow each Plugin to do its work. for p in g_pool.plugins: p.recent_events(events) writer.write_video_frame(frame) current_frame += 1 yield "Exporting with pid {}".format(PID), current_frame except GeneratorExit: logger.warning(f"Video export with pid {PID} was canceled.") writer.close(timestamp_export_format=None, closed_suffix=".canceled") return writer.close(timestamp_export_format="all") duration = time() - start_time effective_fps = float(current_frame) / duration logger.info( f"Export done: Exported {current_frame} frames to {out_file_path}. " f"This took {duration} seconds. " f"Exporter ran at {effective_fps} frames per second.") yield "Export done. This took {:.0f} seconds.".format( duration), current_frame except GeneratorExit: logger.warning(f"Video export with pid {PID} was canceled.")
def player(rec_dir, ipc_pub_url, ipc_sub_url, ipc_push_url, user_dir, app_version, debug): # general imports from time import sleep import logging from glob import glob from time import time, strftime, localtime # networking import zmq import zmq_tools import numpy as np # zmq ipc setup zmq_ctx = zmq.Context() ipc_pub = zmq_tools.Msg_Dispatcher(zmq_ctx, ipc_push_url) notify_sub = zmq_tools.Msg_Receiver(zmq_ctx, ipc_sub_url, topics=("notify", )) # log setup logging.getLogger("OpenGL").setLevel(logging.ERROR) logger = logging.getLogger() logger.handlers = [] logger.setLevel(logging.NOTSET) logger.addHandler(zmq_tools.ZMQ_handler(zmq_ctx, ipc_push_url)) # create logger for the context of this function logger = logging.getLogger(__name__) try: from background_helper import IPC_Logging_Task_Proxy IPC_Logging_Task_Proxy.push_url = ipc_push_url from tasklib.background.patches import IPCLoggingPatch IPCLoggingPatch.ipc_push_url = ipc_push_url # imports from file_methods import Persistent_Dict, next_export_sub_dir # display import glfw # check versions for our own depedencies as they are fast-changing from pyglui import __version__ as pyglui_version from pyglui import ui, cygl from pyglui.cygl.utils import Named_Texture, RGBA import gl_utils # capture from video_capture import File_Source # helpers/utils from version_utils import VersionFormat from methods import normalize, denormalize, delta_t, get_system_info import player_methods as pm from pupil_recording import PupilRecording from csv_utils import write_key_value_file # Plug-ins from plugin import Plugin, Plugin_List, import_runtime_plugins from plugin_manager import Plugin_Manager from vis_circle import Vis_Circle from vis_cross import Vis_Cross from vis_polyline import Vis_Polyline from vis_light_points import Vis_Light_Points from vis_watermark import Vis_Watermark from vis_fixation import Vis_Fixation from seek_control import Seek_Control from surface_tracker import Surface_Tracker_Offline # from marker_auto_trim_marks import Marker_Auto_Trim_Marks from fixation_detector import Offline_Fixation_Detector from log_display import Log_Display from annotations import Annotation_Player from raw_data_exporter import Raw_Data_Exporter from log_history import Log_History from pupil_producers import Pupil_From_Recording, Offline_Pupil_Detection from gaze_producer.gaze_from_recording import GazeFromRecording from gaze_producer.gaze_from_offline_calibration import ( GazeFromOfflineCalibration, ) from system_graphs import System_Graphs from system_timelines import System_Timelines from blink_detection import Offline_Blink_Detection from audio_playback import Audio_Playback from video_export.plugins.imotions_exporter import iMotions_Exporter from video_export.plugins.eye_video_exporter import Eye_Video_Exporter from video_export.plugins.world_video_exporter import World_Video_Exporter from head_pose_tracker.offline_head_pose_tracker import ( Offline_Head_Pose_Tracker, ) from video_capture import File_Source from video_overlay.plugins import Video_Overlay, Eye_Overlay from pupil_recording import ( assert_valid_recording_type, InvalidRecordingException, ) assert VersionFormat(pyglui_version) >= VersionFormat( "1.27"), "pyglui out of date, please upgrade to newest version" process_was_interrupted = False def interrupt_handler(sig, frame): import traceback trace = traceback.format_stack(f=frame) logger.debug(f"Caught signal {sig} in:\n" + "".join(trace)) nonlocal process_was_interrupted process_was_interrupted = True signal.signal(signal.SIGINT, interrupt_handler) runtime_plugins = import_runtime_plugins( os.path.join(user_dir, "plugins")) system_plugins = [ Log_Display, Seek_Control, Plugin_Manager, System_Graphs, System_Timelines, Audio_Playback, ] user_plugins = [ Vis_Circle, Vis_Fixation, Vis_Polyline, Vis_Light_Points, Vis_Cross, Vis_Watermark, Eye_Overlay, Video_Overlay, Offline_Fixation_Detector, Offline_Blink_Detection, Surface_Tracker_Offline, Raw_Data_Exporter, Annotation_Player, Log_History, Pupil_From_Recording, Offline_Pupil_Detection, GazeFromRecording, GazeFromOfflineCalibration, World_Video_Exporter, iMotions_Exporter, Eye_Video_Exporter, Offline_Head_Pose_Tracker, ] + runtime_plugins plugins = system_plugins + user_plugins # Callback functions def on_resize(window, w, h): nonlocal window_size nonlocal hdpi_factor if w == 0 or h == 0: return hdpi_factor = glfw.getHDPIFactor(window) g_pool.gui.scale = g_pool.gui_user_scale * hdpi_factor window_size = w, h g_pool.camera_render_size = w - int( icon_bar_width * g_pool.gui.scale), h g_pool.gui.update_window(*window_size) g_pool.gui.collect_menus() for p in g_pool.plugins: p.on_window_resize(window, *g_pool.camera_render_size) def on_window_key(window, key, scancode, action, mods): g_pool.gui.update_key(key, scancode, action, mods) def on_window_char(window, char): g_pool.gui.update_char(char) def on_window_mouse_button(window, button, action, mods): g_pool.gui.update_button(button, action, mods) def on_pos(window, x, y): x, y = x * hdpi_factor, y * hdpi_factor g_pool.gui.update_mouse(x, y) pos = x, y pos = normalize(pos, g_pool.camera_render_size) # Position in img pixels pos = denormalize(pos, g_pool.capture.frame_size) for p in g_pool.plugins: p.on_pos(pos) def on_scroll(window, x, y): g_pool.gui.update_scroll(x, y * scroll_factor) def on_drop(window, count, paths): paths = [paths[x].decode("utf-8") for x in range(count)] for path in paths: try: assert_valid_recording_type(path) _restart_with_recording(path) return except InvalidRecordingException as err: logger.debug(str(err)) for plugin in g_pool.plugins: if plugin.on_drop(paths): break def _restart_with_recording(rec_dir): logger.debug("Starting new session with '{}'".format(rec_dir)) ipc_pub.notify({ "subject": "player_drop_process.should_start", "rec_dir": rec_dir }) glfw.glfwSetWindowShouldClose(g_pool.main_window, True) tick = delta_t() def get_dt(): return next(tick) recording = PupilRecording(rec_dir) meta_info = recording.meta_info # log info about Pupil Platform and Platform in player.log logger.info("Application Version: {}".format(app_version)) logger.info("System Info: {}".format(get_system_info())) logger.debug(f"Debug flag: {debug}") icon_bar_width = 50 window_size = None hdpi_factor = 1.0 # create container for globally scoped vars g_pool = SimpleNamespace() g_pool.app = "player" g_pool.zmq_ctx = zmq_ctx g_pool.ipc_pub = ipc_pub g_pool.ipc_pub_url = ipc_pub_url g_pool.ipc_sub_url = ipc_sub_url g_pool.ipc_push_url = ipc_push_url g_pool.plugin_by_name = {p.__name__: p for p in plugins} g_pool.camera_render_size = None video_path = recording.files().core().world().videos()[0].resolve() File_Source( g_pool, timing="external", source_path=video_path, buffered_decoding=True, fill_gaps=True, ) # load session persistent settings session_settings = Persistent_Dict( os.path.join(user_dir, "user_settings_player")) if VersionFormat(session_settings.get("version", "0.0")) != app_version: logger.info( "Session setting are a different version of this app. I will not use those." ) session_settings.clear() width, height = g_pool.capture.frame_size width += icon_bar_width width, height = session_settings.get("window_size", (width, height)) window_pos = session_settings.get("window_position", window_position_default) window_name = f"Pupil Player: {meta_info.recording_name} - {rec_dir}" glfw.glfwInit() main_window = glfw.glfwCreateWindow(width, height, window_name, None, None) glfw.glfwSetWindowPos(main_window, window_pos[0], window_pos[1]) glfw.glfwMakeContextCurrent(main_window) cygl.utils.init() g_pool.main_window = main_window def set_scale(new_scale): g_pool.gui_user_scale = new_scale window_size = ( g_pool.camera_render_size[0] + int(icon_bar_width * g_pool.gui_user_scale * hdpi_factor), glfw.glfwGetFramebufferSize(main_window)[1], ) logger.warning(icon_bar_width * g_pool.gui_user_scale * hdpi_factor) glfw.glfwSetWindowSize(main_window, *window_size) g_pool.version = app_version g_pool.timestamps = g_pool.capture.timestamps g_pool.get_timestamp = lambda: 0.0 g_pool.user_dir = user_dir g_pool.rec_dir = rec_dir g_pool.meta_info = meta_info g_pool.min_data_confidence = session_settings.get( "min_data_confidence", MIN_DATA_CONFIDENCE_DEFAULT) g_pool.min_calibration_confidence = session_settings.get( "min_calibration_confidence", MIN_CALIBRATION_CONFIDENCE_DEFAULT) # populated by producers g_pool.pupil_positions = pm.PupilDataBisector() g_pool.gaze_positions = pm.Bisector() g_pool.fixations = pm.Affiliator() g_pool.eye_movements = pm.Affiliator() def set_data_confidence(new_confidence): g_pool.min_data_confidence = new_confidence notification = {"subject": "min_data_confidence_changed"} notification["_notify_time_"] = time() + 0.8 g_pool.ipc_pub.notify(notification) def do_export(_): left_idx = g_pool.seek_control.trim_left right_idx = g_pool.seek_control.trim_right export_range = left_idx, right_idx + 1 # exclusive range.stop export_ts_window = pm.exact_window(g_pool.timestamps, (left_idx, right_idx)) export_dir = os.path.join(g_pool.rec_dir, "exports") export_dir = next_export_sub_dir(export_dir) os.makedirs(export_dir) logger.info('Created export dir at "{}"'.format(export_dir)) export_info = { "Player Software Version": str(g_pool.version), "Data Format Version": meta_info.min_player_version, "Export Date": strftime("%d.%m.%Y", localtime()), "Export Time": strftime("%H:%M:%S", localtime()), "Frame Index Range:": g_pool.seek_control.get_frame_index_trim_range_string(), "Relative Time Range": g_pool.seek_control.get_rel_time_trim_range_string(), "Absolute Time Range": g_pool.seek_control.get_abs_time_trim_range_string(), } with open(os.path.join(export_dir, "export_info.csv"), "w") as csv: write_key_value_file(csv, export_info) notification = { "subject": "should_export", "range": export_range, "ts_window": export_ts_window, "export_dir": export_dir, } g_pool.ipc_pub.notify(notification) def reset_restart(): logger.warning("Resetting all settings and restarting Player.") glfw.glfwSetWindowShouldClose(main_window, True) ipc_pub.notify({"subject": "clear_settings_process.should_start"}) ipc_pub.notify({ "subject": "player_process.should_start", "rec_dir": rec_dir, "delay": 2.0, }) def toggle_general_settings(collapsed): # this is the menu toggle logic. # Only one menu can be open. # If no menu is open the menubar should collapse. g_pool.menubar.collapsed = collapsed for m in g_pool.menubar.elements: m.collapsed = True general_settings.collapsed = collapsed g_pool.gui = ui.UI() g_pool.gui_user_scale = session_settings.get("gui_scale", 1.0) g_pool.menubar = ui.Scrolling_Menu("Settings", pos=(-500, 0), size=(-icon_bar_width, 0), header_pos="left") g_pool.iconbar = ui.Scrolling_Menu("Icons", pos=(-icon_bar_width, 0), size=(0, 0), header_pos="hidden") g_pool.timelines = ui.Container((0, 0), (0, 0), (0, 0)) g_pool.timelines.horizontal_constraint = g_pool.menubar g_pool.user_timelines = ui.Timeline_Menu("User Timelines", pos=(0.0, -150.0), size=(0.0, 0.0), header_pos="headline") g_pool.user_timelines.color = RGBA(a=0.0) g_pool.user_timelines.collapsed = True # add container that constaints itself to the seekbar height vert_constr = ui.Container((0, 0), (0, -50.0), (0, 0)) vert_constr.append(g_pool.user_timelines) g_pool.timelines.append(vert_constr) def set_window_size(): f_width, f_height = g_pool.capture.frame_size f_width += int(icon_bar_width * g_pool.gui.scale) glfw.glfwSetWindowSize(main_window, f_width, f_height) general_settings = ui.Growing_Menu("General", header_pos="headline") general_settings.append(ui.Button("Reset window size", set_window_size)) general_settings.append( ui.Selector( "gui_user_scale", g_pool, setter=set_scale, selection=[0.8, 0.9, 1.0, 1.1, 1.2] + list(np.arange(1.5, 5.1, 0.5)), label="Interface Size", )) general_settings.append( ui.Info_Text( f"Minimum Player Version: {meta_info.min_player_version}")) general_settings.append( ui.Info_Text(f"Player Version: {g_pool.version}")) general_settings.append( ui.Info_Text( f"Recording Software: {meta_info.recording_software_name}")) general_settings.append( ui.Info_Text( f"Recording Software Version: {meta_info.recording_software_version}" )) general_settings.append( ui.Info_Text( "High level data, e.g. fixations, or visualizations only consider gaze data that has an equal or higher confidence than the minimum data confidence." )) general_settings.append( ui.Slider( "min_data_confidence", g_pool, setter=set_data_confidence, step=0.05, min=0.0, max=1.0, label="Minimum data confidence", )) general_settings.append( ui.Button("Restart with default settings", reset_restart)) g_pool.menubar.append(general_settings) icon = ui.Icon( "collapsed", general_settings, label=chr(0xE8B8), on_val=False, off_val=True, setter=toggle_general_settings, label_font="pupil_icons", ) icon.tooltip = "General Settings" g_pool.iconbar.append(icon) user_plugin_separator = ui.Separator() user_plugin_separator.order = 0.35 g_pool.iconbar.append(user_plugin_separator) g_pool.quickbar = ui.Stretching_Menu("Quick Bar", (0, 100), (100, -100)) g_pool.export_button = ui.Thumb( "export", label=chr(0xE2C5), getter=lambda: False, setter=do_export, hotkey="e", label_font="pupil_icons", ) g_pool.quickbar.extend([g_pool.export_button]) g_pool.gui.append(g_pool.menubar) g_pool.gui.append(g_pool.timelines) g_pool.gui.append(g_pool.iconbar) g_pool.gui.append(g_pool.quickbar) # we always load these plugins default_plugins = [ ("Plugin_Manager", {}), ("Seek_Control", {}), ("Log_Display", {}), ("Raw_Data_Exporter", {}), ("Vis_Polyline", {}), ("Vis_Circle", {}), ("System_Graphs", {}), ("System_Timelines", {}), ("World_Video_Exporter", {}), ("Pupil_From_Recording", {}), ("GazeFromRecording", {}), ("Audio_Playback", {}), ] g_pool.plugins = Plugin_List( g_pool, session_settings.get("loaded_plugins", default_plugins)) # Manually add g_pool.capture to the plugin list g_pool.plugins._plugins.append(g_pool.capture) g_pool.plugins._plugins.sort(key=lambda p: p.order) g_pool.capture.init_ui() general_settings.insert( -1, ui.Text_Input( "rel_time_trim_section", getter=g_pool.seek_control.get_rel_time_trim_range_string, setter=g_pool.seek_control.set_rel_time_trim_range_string, label="Relative time range to export", ), ) general_settings.insert( -1, ui.Text_Input( "frame_idx_trim_section", getter=g_pool.seek_control.get_frame_index_trim_range_string, setter=g_pool.seek_control.set_frame_index_trim_range_string, label="Frame index range to export", ), ) # Register callbacks main_window glfw.glfwSetFramebufferSizeCallback(main_window, on_resize) glfw.glfwSetKeyCallback(main_window, on_window_key) glfw.glfwSetCharCallback(main_window, on_window_char) glfw.glfwSetMouseButtonCallback(main_window, on_window_mouse_button) glfw.glfwSetCursorPosCallback(main_window, on_pos) glfw.glfwSetScrollCallback(main_window, on_scroll) glfw.glfwSetDropCallback(main_window, on_drop) toggle_general_settings(True) g_pool.gui.configuration = session_settings.get("ui_config", {}) # gl_state settings gl_utils.basic_gl_setup() g_pool.image_tex = Named_Texture() # trigger on_resize on_resize(main_window, *glfw.glfwGetFramebufferSize(main_window)) def handle_notifications(n): subject = n["subject"] if subject == "start_plugin": g_pool.plugins.add(g_pool.plugin_by_name[n["name"]], args=n.get("args", {})) elif subject.startswith("meta.should_doc"): ipc_pub.notify({ "subject": "meta.doc", "actor": g_pool.app, "doc": player.__doc__ }) for p in g_pool.plugins: if (p.on_notify.__doc__ and p.__class__.on_notify != Plugin.on_notify): ipc_pub.notify({ "subject": "meta.doc", "actor": p.class_name, "doc": p.on_notify.__doc__, }) while (not glfw.glfwWindowShouldClose(main_window) and not process_was_interrupted): # fetch newest notifications new_notifications = [] while notify_sub.new_data: t, n = notify_sub.recv() new_notifications.append(n) # notify each plugin if there are new notifications: for n in new_notifications: handle_notifications(n) for p in g_pool.plugins: p.on_notify(n) events = {} # report time between now and the last loop interation events["dt"] = get_dt() # pupil and gaze positions are added by their respective producer plugins events["pupil"] = [] events["gaze"] = [] # allow each Plugin to do its work. for p in g_pool.plugins: p.recent_events(events) # check if a plugin need to be destroyed g_pool.plugins.clean() glfw.glfwMakeContextCurrent(main_window) glfw.glfwPollEvents() # render visual feedback from loaded plugins if gl_utils.is_window_visible(main_window): gl_utils.glViewport(0, 0, *g_pool.camera_render_size) g_pool.capture.gl_display() for p in g_pool.plugins: p.gl_display() gl_utils.glViewport(0, 0, *window_size) try: clipboard = glfw.glfwGetClipboardString( main_window).decode() except AttributeError: # clipbaord is None, might happen on startup clipboard = "" g_pool.gui.update_clipboard(clipboard) user_input = g_pool.gui.update() if user_input.clipboard and user_input.clipboard != clipboard: # only write to clipboard if content changed glfw.glfwSetClipboardString(main_window, user_input.clipboard.encode()) for b in user_input.buttons: button, action, mods = b x, y = glfw.glfwGetCursorPos(main_window) pos = x * hdpi_factor, y * hdpi_factor pos = normalize(pos, g_pool.camera_render_size) pos = denormalize(pos, g_pool.capture.frame_size) for plugin in g_pool.plugins: if plugin.on_click(pos, button, action): break for key, scancode, action, mods in user_input.keys: for plugin in g_pool.plugins: if plugin.on_key(key, scancode, action, mods): break for char_ in user_input.chars: for plugin in g_pool.plugins: if plugin.on_char(char_): break # present frames at appropriate speed g_pool.seek_control.wait(events["frame"].timestamp) glfw.glfwSwapBuffers(main_window) session_settings["loaded_plugins"] = g_pool.plugins.get_initializers() session_settings["min_data_confidence"] = g_pool.min_data_confidence session_settings[ "min_calibration_confidence"] = g_pool.min_calibration_confidence session_settings["gui_scale"] = g_pool.gui_user_scale session_settings["ui_config"] = g_pool.gui.configuration session_settings["window_position"] = glfw.glfwGetWindowPos( main_window) session_settings["version"] = str(g_pool.version) session_window_size = glfw.glfwGetWindowSize(main_window) if 0 not in session_window_size: session_settings["window_size"] = session_window_size session_settings.close() # de-init all running plugins for p in g_pool.plugins: p.alive = False g_pool.plugins.clean() g_pool.gui.terminate() glfw.glfwDestroyWindow(main_window) except Exception: import traceback trace = traceback.format_exc() logger.error("Process Player crashed with trace:\n{}".format(trace)) finally: logger.info("Process shutting down.") ipc_pub.notify({"subject": "player_process.stopped"}) sleep(1.0)
def _imu_recordings(cls, g_pool) -> T.List[IMURecording]: rec = PupilRecording(g_pool.rec_dir) imu_files: T.List[pathlib.Path] = sorted( rec.files().filter_patterns(cls.IMU_PATTERN_RAW) ) return [IMURecording(imu_file) for imu_file in imu_files]