Exemplo n.º 1
0
 def __init__(self, g_pool, show_segmentation=True):
     super().__init__(g_pool)
     self.storage = model.Classified_Segment_Storage(plugin=self,
                                                     rec_dir=g_pool.rec_dir)
     self.seek_controller = controller.Eye_Movement_Seek_Controller(
         plugin=self,
         storage=self.storage,
         seek_to_timestamp=self.seek_to_timestamp)
     self.offline_controller = controller.Eye_Movement_Offline_Controller(
         plugin=self,
         storage=self.storage,
         on_status=self.on_task_status,
         on_progress=self.on_task_progress,
         on_exception=self.on_task_exception,
         on_completed=self.on_task_completed,
     )
     self.menu_content = ui.Menu_Content(
         plugin=self,
         label_text=self.MENU_LABEL_TEXT,
         show_segmentation=show_segmentation,
     )
     self.prev_segment_button = ui.Prev_Segment_Button(
         on_click=self.seek_controller.jump_to_prev_segment)
     self.next_segment_button = ui.Next_Segment_Button(
         on_click=self.seek_controller.jump_to_next_segment)
     self._gaze_changed_listener = Listener(plugin=self,
                                            topic="gaze_positions",
                                            rec_dir=g_pool.rec_dir)
     self._gaze_changed_listener.add_observer(
         method_name="on_data_changed",
         observer=self.offline_controller.classify)
     self._eye_movement_changed_announcer = Announcer(
         plugin=self,
         topic=EYE_MOVEMENT_ANNOUNCER_TOPIC,
         rec_dir=g_pool.rec_dir)
Exemplo n.º 2
0
    def __init__(self,
                 g_pool,
                 polyline_style_init_dict={},
                 scan_path_init_dict={},
                 **kwargs):
        super().__init__(g_pool)

        self.polyline_style_controller = PolylineStyleController(
            **polyline_style_init_dict)

        self.scan_path_controller = ScanPathController(g_pool,
                                                       **scan_path_init_dict)
        self.scan_path_controller.add_observer("on_update_ui",
                                               self._update_scan_path_ui)

        self._gaze_changed_listener = Listener(plugin=self,
                                               topic="gaze_positions",
                                               rec_dir=g_pool.rec_dir)
        self._gaze_changed_listener.add_observer(
            method_name="on_data_changed",
            observer=self.scan_path_controller.on_gaze_data_changed,
        )
Exemplo n.º 3
0
class Vis_Polyline(Visualizer_Plugin_Base, Observable):
    order = 0.9
    uniqueness = "not_unique"
    icon_chr = chr(0xE922)
    icon_font = "pupil_icons"

    def __init__(self,
                 g_pool,
                 polyline_style_init_dict={},
                 scan_path_init_dict={},
                 **kwargs):
        super().__init__(g_pool)

        self.polyline_style_controller = PolylineStyleController(
            **polyline_style_init_dict)

        self.scan_path_controller = ScanPathController(g_pool,
                                                       **scan_path_init_dict)
        self.scan_path_controller.add_observer("on_update_ui",
                                               self._update_scan_path_ui)

        self._gaze_changed_listener = Listener(plugin=self,
                                               topic="gaze_positions",
                                               rec_dir=g_pool.rec_dir)
        self._gaze_changed_listener.add_observer(
            method_name="on_data_changed",
            observer=self.scan_path_controller.on_gaze_data_changed,
        )

    def get_init_dict(self):
        return {
            "polyline_style_init_dict":
            self.polyline_style_controller.get_init_dict(),
            "scan_path_init_dict":
            self.scan_path_controller.get_init_dict(),
        }

    def init_ui(self):

        polyline_style_thickness_slider = ui.Slider(
            "thickness",
            self.polyline_style_controller,
            min=self.polyline_style_controller.thickness_min,
            max=self.polyline_style_controller.thickness_max,
            step=self.polyline_style_controller.thickness_step,
            label="Line thickness",
        )

        polyline_style_color_info_text = ui.Info_Text(
            "Set RGB color component values.")

        polyline_style_color_r_slider = ui.Slider(
            "r",
            self.polyline_style_controller,
            min=self.polyline_style_controller.rgba_min,
            max=self.polyline_style_controller.rgba_max,
            step=self.polyline_style_controller.rgba_step,
            label="Red",
        )
        polyline_style_color_g_slider = ui.Slider(
            "g",
            self.polyline_style_controller,
            min=self.polyline_style_controller.rgba_min,
            max=self.polyline_style_controller.rgba_max,
            step=self.polyline_style_controller.rgba_step,
            label="Green",
        )
        polyline_style_color_b_slider = ui.Slider(
            "b",
            self.polyline_style_controller,
            min=self.polyline_style_controller.rgba_min,
            max=self.polyline_style_controller.rgba_max,
            step=self.polyline_style_controller.rgba_step,
            label="Blue",
        )

        scan_path_timeframe_range = ui.Slider(
            "timeframe",
            self.scan_path_controller,
            min=self.scan_path_controller.min_timeframe,
            max=self.scan_path_controller.max_timeframe,
            step=self.scan_path_controller.timeframe_step,
            label="Duration",
        )

        scan_path_doc = ui.Info_Text(
            "Duration of past gaze to include in polyline.")
        scan_path_status = ui.Info_Text("")

        polyline_style_color_menu = ui.Growing_Menu("Color")
        polyline_style_color_menu.collapsed = True
        polyline_style_color_menu.append(polyline_style_color_info_text)
        polyline_style_color_menu.append(polyline_style_color_r_slider)
        polyline_style_color_menu.append(polyline_style_color_g_slider)
        polyline_style_color_menu.append(polyline_style_color_b_slider)

        scan_path_menu = ui.Growing_Menu("Gaze History")
        scan_path_menu.collapsed = False
        scan_path_menu.append(scan_path_doc)
        scan_path_menu.append(scan_path_timeframe_range)
        scan_path_menu.append(scan_path_status)

        self.add_menu()
        self.menu.label = "Gaze Polyline"
        self.menu.append(polyline_style_thickness_slider)
        self.menu.append(polyline_style_color_menu)
        self.menu.append(scan_path_menu)

        self.scan_path_timeframe_range = scan_path_timeframe_range
        self.scan_path_status = scan_path_status

        self._update_scan_path_ui()

    def deinit_ui(self):
        self.remove_menu()
        self.scan_path_timeframe_range = None
        self.scan_path_status = None

    def recent_events(self, events):
        self.scan_path_controller.process()

        frame = events.get("frame")
        if not frame:
            return

        self._draw_polyline_path(frame, events)
        # self._draw_scan_path_debug(frame, events)

    def cleanup(self):
        self.scan_path_controller.cleanup()

    def _update_scan_path_ui(self):
        if self.menu_icon:
            self.menu_icon.indicator_stop = self.scan_path_controller.progress
        if self.scan_path_status:
            self.scan_path_status.text = self.scan_path_controller.status_string

    def _polyline_points(self, image_size, base_gaze_data,
                         scan_path_gaze_data):
        if scan_path_gaze_data is not None:
            points_fields = ["norm_x", "norm_y"]
            gaze_points = scan_path_gaze_data[points_fields]
            gaze_points = np.array(
                gaze_points.tolist(),
                dtype=gaze_points.dtype[0])  # FIXME: This is a workaround
            gaze_points = gaze_points.reshape((-1, len(points_fields)))
            gaze_points = np_denormalize(gaze_points, image_size, flip_y=True)
            return gaze_points.tolist()
        else:
            return [
                denormalize(datum["norm_pos"], image_size, flip_y=True)
                for datum in base_gaze_data
                if datum["confidence"] >= self.g_pool.min_data_confidence
            ]

    def _draw_polyline_path(self, frame, events):
        pts = self._polyline_points(
            image_size=frame.img.shape[:-1][::-1],
            base_gaze_data=events.get("gaze", []),
            scan_path_gaze_data=self.scan_path_controller.
            scan_path_gaze_for_frame(frame),
        )

        if not pts:
            return

        pts = np.array([pts], dtype=np.int32)
        cv2.polylines(
            frame.img,
            pts,
            isClosed=False,
            color=self.polyline_style_controller.cv2_bgra,
            thickness=self.polyline_style_controller.thickness,
            lineType=cv2.LINE_AA,
        )

    def _draw_scan_path_debug(self, frame, events):
        from methods import denormalize
        from player_methods import transparent_circle

        gaze_data = self.scan_path_controller.scan_path_gaze_for_frame(frame)

        if gaze_data is None:
            return

        points_to_draw_count = len(gaze_data)
        image_size = frame.img.shape[:-1][::-1]

        for idx, datum in enumerate(gaze_data):
            point = (datum["norm_x"], datum["norm_y"])
            point = denormalize(point, image_size, flip_y=True)

            gray = float(idx) / points_to_draw_count
            transparent_circle(frame.img,
                               point,
                               radius=20,
                               color=(gray, gray, gray, 0.9),
                               thickness=2)
Exemplo n.º 4
0
class Offline_Eye_Movement_Detector(Observable, Eye_Movement_Detector_Base):
    """
    Eye movement classification detector based on segmented linear regression.

    Event identification is based on segmentation that simultaneously denoises the signal and determines event
    boundaries. The full gaze position time-series is segmented into an approximately optimal piecewise linear
    function in O(n) time. Gaze feature parameters for classification into fixations, saccades, smooth pursuits and post-saccadic oscillations
    are derived from human labeling in a data-driven manner.

    More details about this approach can be found here:
    https://www.nature.com/articles/s41598-017-17983-x

    The open source implementation can be found here:
    https://gitlab.com/nslr/nslr-hmm
    """

    MENU_LABEL_TEXT = "Eye Movement Detector"

    def __init__(self, g_pool, show_segmentation=True):
        super().__init__(g_pool)
        self.storage = model.Classified_Segment_Storage(plugin=self,
                                                        rec_dir=g_pool.rec_dir)
        self.add_observer("on_task_completed", self.storage.save_to_disk)
        self.seek_controller = controller.Eye_Movement_Seek_Controller(
            plugin=self,
            storage=self.storage,
            seek_to_timestamp=self.seek_to_timestamp)
        self.offline_controller = controller.Eye_Movement_Offline_Controller(
            plugin=self,
            storage=self.storage,
            on_started=self.on_task_started,
            on_status=self.on_task_status,
            on_progress=self.on_task_progress,
            on_exception=self.on_task_exception,
            on_completed=self.on_task_completed,
        )
        self.menu_content = ui.Menu_Content(
            plugin=self,
            label_text=self.MENU_LABEL_TEXT,
            show_segmentation=show_segmentation,
        )
        self.prev_segment_button = ui.Prev_Segment_Button(
            on_click=self.seek_controller.jump_to_prev_segment)
        self.next_segment_button = ui.Next_Segment_Button(
            on_click=self.seek_controller.jump_to_next_segment)
        self._gaze_changed_listener = Listener(plugin=self,
                                               topic="gaze_positions",
                                               rec_dir=g_pool.rec_dir)
        self._gaze_changed_listener.add_observer(
            method_name="on_data_changed",
            observer=self.offline_controller.classify)
        self._eye_movement_changed_announcer = Announcer(
            plugin=self,
            topic=EYE_MOVEMENT_ANNOUNCER_TOPIC,
            rec_dir=g_pool.rec_dir)

    #

    def trigger_recalculate(self):
        self.notify_all({
            "subject": Notification_Subject.SHOULD_RECALCULATE,
            "delay": 0.5
        })

    def seek_to_timestamp(self, timestamp):
        self.notify_all({
            "subject": "seek_control.should_seek",
            "timestamp": timestamp
        })

    def on_task_started(self):
        self.menu_content.update_error_text("")

    def on_task_progress(self, progress: float):
        self.menu_content.update_progress(progress)

    def on_task_status(self, status: str):
        self.menu_content.update_status(status)

    def on_task_exception(self, exception: Exception):
        error_message = f"{exception}"
        logger.error(error_message)
        self.menu_content.update_error_text(error_message)

    def on_task_completed(self):
        self._eye_movement_changed_announcer.announce_new()

    #

    def init_ui(self):
        self.add_menu()
        self.menu_content.add_to_menu(self.menu)
        self.prev_segment_button.add_to_quickbar(self.g_pool.quickbar)
        self.next_segment_button.add_to_quickbar(self.g_pool.quickbar)

        if len(self.storage):
            status = "Loaded from cache"
            self.menu_content.update_status(status)
        else:
            self.trigger_recalculate()

    def deinit_ui(self):
        self.remove_menu()
        self.prev_segment_button.remove_from_quickbar(self.g_pool.quickbar)
        self.next_segment_button.remove_from_quickbar(self.g_pool.quickbar)

    def get_init_dict(self):
        return {"show_segmentation": self.menu_content.show_segmentation}

    def on_notify(self, notification):
        if notification["subject"] in (
                Notification_Subject.SHOULD_RECALCULATE,
                Notification_Subject.MIN_DATA_CONFIDENCE_CHANGED,
        ):
            self.offline_controller.classify()
        elif notification["subject"] == "should_export":
            self.export_eye_movement(notification["ts_window"],
                                     notification["export_dir"])

    def recent_events(self, events):

        frame = events.get("frame")
        if not frame:
            return

        visible_segments = self.storage.segments_in_frame(frame)
        self.seek_controller.update_visible_segments(visible_segments)

        self.menu_content.update_detail_text(
            current_index=self.seek_controller.current_segment_index,
            total_segment_count=self.seek_controller.total_segment_count,
            current_segment=self.seek_controller.current_segment,
            prev_segment=self.seek_controller.prev_segment,
            next_segment=self.seek_controller.next_segment,
        )

        if self.menu_content.show_segmentation:
            segment_renderer = ui.Segment_Overlay_Image_Renderer(
                canvas_size=(frame.width, frame.height), image=frame.img)
            for segment in visible_segments:
                segment_renderer.draw(segment)

        events[utils.EYE_MOVEMENT_EVENT_KEY] = visible_segments

    def export_eye_movement(self, export_window, export_dir):

        segments_in_section = self.storage.segments_in_timestamp_window(
            export_window)

        if segments_in_section:
            by_segment_csv_exporter = controller.Eye_Movement_By_Segment_CSV_Exporter(
            )
            by_segment_csv_exporter.csv_export(segments_in_section,
                                               export_dir=export_dir)

            export_window_start, export_window_stop = export_window
            ts_segment_class_pairs = ((gaze["timestamp"], seg.segment_class)
                                      for seg in segments_in_section
                                      for gaze in seg.segment_data
                                      if export_window_start <=
                                      gaze["timestamp"] <= export_window_stop)
            by_gaze_csv_exporter = controller.Eye_Movement_By_Gaze_CSV_Exporter(
            )
            by_gaze_csv_exporter.csv_export(ts_segment_class_pairs,
                                            export_dir=export_dir)
        else:
            logger.warning(
                "The selected export range does not include eye movement detections"
            )

    def cleanup(self):
        self.remove_observer("on_task_completed", self.storage.save_to_disk)