Exemple #1
0
    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)
Exemple #2
0
    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()
Exemple #3
0
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")
Exemple #4
0
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)
Exemple #5
0
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
Exemple #6
0
 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)
Exemple #8
0
 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
Exemple #9
0
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
Exemple #10
0
    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()
Exemple #11
0
    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)
Exemple #12
0
    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)
Exemple #13
0
    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)
Exemple #14
0
    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
Exemple #15
0
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.")
Exemple #16
0
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)
Exemple #17
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]