From db6af62f88a17119333d02983529b2b295dc48ac Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 20:03:32 +0200 Subject: [PATCH 01/20] Generic Video Overlay: Basic functionality --- pupil_src/launchables/player.py | 2 + .../plugins/world_video_exporter.py | 2 + .../shared_modules/video_overlay/__init__.py | 0 .../video_overlay/controllers/config.py | 44 +++++++++ .../video_overlay/controllers/overlay.py | 56 ++++++++++++ .../video_overlay/controllers/video.py | 44 +++++++++ .../video_overlay/plugins/__init__.py | 1 + .../video_overlay/plugins/generic_overlay.py | 31 +++++++ .../video_overlay/utils/constraints.py | 67 ++++++++++++++ .../video_overlay/utils/image_manipulation.py | 89 +++++++++++++++++++ 10 files changed, 336 insertions(+) create mode 100644 pupil_src/shared_modules/video_overlay/__init__.py create mode 100644 pupil_src/shared_modules/video_overlay/controllers/config.py create mode 100644 pupil_src/shared_modules/video_overlay/controllers/overlay.py create mode 100644 pupil_src/shared_modules/video_overlay/controllers/video.py create mode 100644 pupil_src/shared_modules/video_overlay/plugins/__init__.py create mode 100644 pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py create mode 100644 pupil_src/shared_modules/video_overlay/utils/constraints.py create mode 100644 pupil_src/shared_modules/video_overlay/utils/image_manipulation.py diff --git a/pupil_src/launchables/player.py b/pupil_src/launchables/player.py index 30ba5743ac..6de85a00f5 100644 --- a/pupil_src/launchables/player.py +++ b/pupil_src/launchables/player.py @@ -120,6 +120,7 @@ def player(rec_dir, ipc_pub_url, ipc_sub_url, ipc_push_url, user_dir, app_versio from video_export.plugins.eye_video_exporter import Eye_Video_Exporter from video_export.plugins.world_video_exporter import World_Video_Exporter from video_capture import File_Source + from video_overlay.plugins import Vis_Generic_Video_Overlay assert VersionFormat(pyglui_version) >= VersionFormat( "1.23" @@ -142,6 +143,7 @@ def player(rec_dir, ipc_pub_url, ipc_sub_url, ipc_push_url, user_dir, app_versio Vis_Cross, Vis_Watermark, Vis_Eye_Video_Overlay, + Vis_Generic_Video_Overlay, # Vis_Scan_Path, Offline_Fixation_Detector, Offline_Blink_Detection, diff --git a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py index 5968cb1fa8..535a1de2a8 100644 --- a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py +++ b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py @@ -116,6 +116,7 @@ def _export_world_video( # Plug-ins from plugin import Plugin_List, import_runtime_plugins from video_capture import EndofVideoError, File_Source + from video_overlay.plugins import Vis_Generic_Video_Overlay from vis_circle import Vis_Circle from vis_cross import Vis_Cross from vis_eye_video_overlay import Vis_Eye_Video_Overlay @@ -140,6 +141,7 @@ def _export_world_video( Vis_Watermark, Vis_Scan_Path, Vis_Eye_Video_Overlay, + Vis_Generic_Video_Overlay, ], key=lambda x: x.__name__, ) diff --git a/pupil_src/shared_modules/video_overlay/__init__.py b/pupil_src/shared_modules/video_overlay/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pupil_src/shared_modules/video_overlay/controllers/config.py b/pupil_src/shared_modules/video_overlay/controllers/config.py new file mode 100644 index 0000000000..9130805f82 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/controllers/config.py @@ -0,0 +1,44 @@ +from video_overlay.utils.constraints import ( + ConstraintedPosition, + ConstraintedValue, + BooleanConstraint, + InclusiveConstraint, +) + + +class Controller: + __slots__ = ("origin", "scale", "alpha", "hflip", "vflip") + + @classmethod + def from_updated_defaults(cls, config_subset): + defaults = cls.default_dict() + defaults.update(config_subset) + return cls(**defaults) + + @staticmethod + def default_dict(): + return { + "origin_x": 0, + "origin_y": 0, + "scale": 1.0, + "alpha": 1.0, + "hflip": False, + "vflip": False, + } + + def __init__(self, origin_x, origin_y, scale, alpha, hflip, vflip): + self.origin = ConstraintedPosition(origin_x, origin_y) + self.scale = ConstraintedValue(scale, InclusiveConstraint(low=0.2, high=1.0)) + self.alpha = ConstraintedValue(alpha, InclusiveConstraint(low=0.1, high=1.0)) + self.hflip = ConstraintedValue(hflip, BooleanConstraint()) + self.vflip = ConstraintedValue(vflip, BooleanConstraint()) + + def get_init_dict(self): + return { + "origin_x": self.origin.x.value, + "origin_y": self.origin.y.value, + "scale": self.scale.value, + "alpha": self.alpha.value, + "hflip": self.hflip.value, + "vflip": self.vflip.value, + } diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay.py b/pupil_src/shared_modules/video_overlay/controllers/overlay.py new file mode 100644 index 0000000000..0c1158b1ed --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay.py @@ -0,0 +1,56 @@ +import logging +from collections import OrderedDict + +import player_methods as pm + +import video_overlay.utils.image_manipulation as IM +from video_overlay.controllers.config import Controller as ConfigController +from video_overlay.controllers.video import Controller as VideoController + +logger = logging.getLogger(__name__) + + +class Controller: + __slots__ = ("valid_video_loaded", "video", "config", "pipeline") + + def __init__(self, video_path, config): + self.attempt_to_load_video(video_path) + self.config = ConfigController.from_updated_defaults(config) + self.pipeline = self.setup_pipeline() + + def attempt_to_load_video(self, video_path): + try: + self.video = VideoController(video_path) + self.valid_video_loaded = True + except FileNotFoundError: + logger.debug("Could not load overlay: {}".format(video_path)) + self.valid_video_loaded = False + return self.valid_video_loaded + + def setup_pipeline(self): + return OrderedDict( + ( + (self.config.scale, IM.ScaleTransform()), + (self.config.hflip, IM.HorizontalFlip()), + (self.config.vflip, IM.VerticalFlip()), + ) + ) + + def draw_on_frame(self, target_frame): + if not self.valid_video_loaded: + return + overlay_frame = self.video.closest_frame_to_ts(target_frame.timestamp) + overlay_image = overlay_frame.img + for param, manipulation in self.pipeline.items(): + overlay_image = manipulation.apply_to(overlay_image, param.value) + self._render_overlay(target_frame.img, overlay_image) + + def _render_overlay(self, target_image, overlay_image): + overlay_origin = (self.config.origin.y.value, self.config.origin.x.value) + pm.transparent_image_overlay( + overlay_origin, overlay_image, target_image, self.config.alpha.value + ) + + @property + def video_path(self): + return self.video.source.source_path if self.valid_video_loaded else None diff --git a/pupil_src/shared_modules/video_overlay/controllers/video.py b/pupil_src/shared_modules/video_overlay/controllers/video.py new file mode 100644 index 0000000000..b5013dd015 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/controllers/video.py @@ -0,0 +1,44 @@ +import logging + +import player_methods as pm + +from video_capture import File_Source, EndofVideoError + +logger = logging.getLogger(__name__) + + +class _Empty: + """Replacement for actual g_pool object""" + + +class Controller: + __slots__ = ("source", "current_frame") + + def __init__(self, video_path): + try: + self.source = File_Source(_Empty(), source_path=video_path, timing=None) + except AssertionError as err: + raise FileNotFoundError(video_path) from err + self.current_frame = self.source.get_frame() + + def initialised(self): + return self.source.initialised + + def closest_frame_to_ts(self, ts): + closest_idx = pm.find_closest(self.source.timestamps, ts) + return self.frame_for_idx(closest_idx) + + def frame_for_idx(self, requested_frame_idx): + if requested_frame_idx != self.current_frame.index: + if requested_frame_idx == self.source.get_frame_index() + 2: + # if we just need to seek by one frame, + # its faster to just read one and and throw it away. + self.source.get_frame() + if requested_frame_idx != self.source.get_frame_index() + 1: + self.source.seek_to_frame(int(requested_frame_idx)) + + try: + self.current_frame = self.source.get_frame() + except EndofVideoError: + logger.info("End of video {}.".format(self.source.source_path)) + return self.current_frame diff --git a/pupil_src/shared_modules/video_overlay/plugins/__init__.py b/pupil_src/shared_modules/video_overlay/plugins/__init__.py new file mode 100644 index 0000000000..5efdb7f7a8 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/plugins/__init__.py @@ -0,0 +1 @@ +from video_overlay.plugins.generic_overlay import Vis_Generic_Video_Overlay diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py new file mode 100644 index 0000000000..474c0fb678 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -0,0 +1,31 @@ +from plugin import Plugin + +from video_overlay.controllers.overlay import Controller as OverlayController + + +class Vis_Generic_Video_Overlay(Plugin): + uniqueness = "not_unique" + + def __init__(self, g_pool, video_path=None, config=None): + super().__init__(g_pool) + config = config or {"scale": 0.5, "alpha": 0.9, "hflip": False, "vflip": False} + self.controller = OverlayController(video_path, config) + + def get_init_dict(self): + return { + "video_path": self.controller.video_path, + "config": self.controller.config.get_init_dict(), + } + + def recent_events(self, events): + if "frame" in events: + frame = events["frame"] + self.controller.draw_on_frame(frame) + + def on_drop(self, paths): + remaining_paths = paths.copy() + while remaining_paths and not self.controller.valid_video_loaded: + video_path = remaining_paths.pop(0) + if self.controller.attempt_to_load_video(video_path): + return True # event consumed + return False # event not consumed diff --git a/pupil_src/shared_modules/video_overlay/utils/constraints.py b/pupil_src/shared_modules/video_overlay/utils/constraints.py new file mode 100644 index 0000000000..d0c21ab2a7 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/utils/constraints.py @@ -0,0 +1,67 @@ +import abc + +INF = float("inf") + + +class BaseConstraint(metaclass=abc.ABCMeta): + @abc.abstractmethod + def apply_to(self, value): + raise NotImplementedError + + +class NoConstraint(BaseConstraint): + def apply_to(self, value): + return value + + +class InclusiveConstraint(BaseConstraint): + __slots__ = ("low", "high") + + def __init__(self, *, low=-INF, high=INF): + self.low = low + self.high = high + + def apply_to(self, value): + return min(max(self.low, value), self.high) + + +class BooleanConstraint(BaseConstraint): + def apply_to(self, value): + return bool(value) + + +class ConstraintedValue: + __slots__ = ("_val", "_constraint") + + def __init__(self, value, constraint=NoConstraint()): + self._constraint = constraint + self._val = value + + @property + def value(self): + return self._val + + @value.setter + def value(self, new_val): + self._val = self.constraint.apply_to(new_val) + + @property + def constraint(self): + return self._constraint + + @constraint.setter + def constraint(self, new_constraint): + self._constraint = new_constraint + self.value = self.value # apply new constraint + + @constraint.deleter + def constraint(self): + self.constraint = NoConstraint() + + +class ConstraintedPosition: + __slots__ = ("x", "y") + + def __init__(self, x, y): + self.x = ConstraintedValue(x) + self.y = ConstraintedValue(y) diff --git a/pupil_src/shared_modules/video_overlay/utils/image_manipulation.py b/pupil_src/shared_modules/video_overlay/utils/image_manipulation.py new file mode 100644 index 0000000000..5dfe45214b --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/utils/image_manipulation.py @@ -0,0 +1,89 @@ +import abc + +import cv2 +import numpy as np + + +class ImageManipulator(metaclass=abc.ABCMeta): + @abc.abstractmethod + def apply_to(self, image, parameter): + raise NotImplementedError + + +class ScaleTransform(ImageManipulator): + def apply_to(self, image, parameter): + """parameter: scale factor as float""" + return cv2.resize(image, (0, 0), fx=parameter, fy=parameter) + + +class HorizontalFlip(ImageManipulator): + def apply_to(self, image, parameter): + """parameter: boolean indicating if image should be flipped""" + return np.fliplr(image) if parameter else image + + +class VerticalFlip(ImageManipulator): + def apply_to(self, image, parameter): + """parameter: boolean indicating if image should be flipped""" + return np.flipud(image) if parameter else image + + +class PupilRenderer(ImageManipulator): + __slots__ = "pupil_getter" + + def __init__(self, pupil_getter): + self.pupil_getter = pupil_getter + + def apply_to(self, image, parameter): + """parameter: boolean indicating if pupil should be rendered""" + if parameter: + pupil_position = self.pupil_getter() + self.render_pupil(image, pupil_position) + return image + + def render_pupil(self, image, pupil_position): + el = pupil_position["ellipse"] + conf = int( + pupil_position.get( + "model_confidence", pupil_position.get("confidence", 0.1) + ) + * 255 + ) + el_points = self.get_ellipse_points((el["center"], el["axes"], el["angle"])) + cv2.polylines( + image, + [np.asarray(el_points, dtype="i")], + True, + (0, 0, 255, conf), + thickness=1, + ) + cv2.circle( + image, + (int(el["center"][0]), int(el["center"][1])), + 5, + (0, 0, 255, conf), + thickness=-1, + ) + + @staticmethod + def get_ellipse_points(e, num_pts=10): + c1 = e[0][0] + c2 = e[0][1] + a = e[1][0] + b = e[1][1] + angle = e[2] + + steps = np.linspace(0, 2 * np.pi, num=num_pts, endpoint=False) + rot = cv2.getRotationMatrix2D((0, 0), -angle, 1) + + pts1 = a / 2.0 * np.cos(steps) + pts2 = b / 2.0 * np.sin(steps) + pts = np.column_stack((pts1, pts2, np.ones(pts1.shape[0]))) + + pts_rot = np.matmul(rot, pts.T) + pts_rot = pts_rot.T + + pts_rot[:, 0] += c1 + pts_rot[:, 1] += c2 + + return pts_rot From 64a0f490514b061cf438ae747fb674218620242c Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 20:04:49 +0200 Subject: [PATCH 02/20] player_methods: Catch cv2 errors explicitly in transparent_image_overlay --- pupil_src/shared_modules/player_methods.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/player_methods.py b/pupil_src/shared_modules/player_methods.py index 13c8267a28..24cea03e90 100644 --- a/pupil_src/shared_modules/player_methods.py +++ b/pupil_src/shared_modules/player_methods.py @@ -260,8 +260,7 @@ def transparent_image_overlay(pos, overlay_img, img, alpha): ) try: cv2.addWeighted(overlay_img, alpha, img[roi], 1.0 - alpha, 0, img[roi]) - except: + except (TypeError, cv2.error): logger.debug( "transparent_image_overlay was outside of the world image and was not drawn" ) - pass From 8f97e5786ea5eaa27ff8dc3652218e8675abf775 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 20:59:14 +0200 Subject: [PATCH 03/20] Generic Overlay Video: Add ui elements --- .../video_overlay/controllers/overlay.py | 5 +-- .../video_overlay/plugins/generic_overlay.py | 31 ++++++++++++++++ .../shared_modules/video_overlay/ui/menu.py | 37 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 pupil_src/shared_modules/video_overlay/ui/menu.py diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay.py b/pupil_src/shared_modules/video_overlay/controllers/overlay.py index 0c1158b1ed..b2219fd9f9 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/overlay.py +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay.py @@ -2,6 +2,7 @@ from collections import OrderedDict import player_methods as pm +from observable import Observable import video_overlay.utils.image_manipulation as IM from video_overlay.controllers.config import Controller as ConfigController @@ -10,9 +11,7 @@ logger = logging.getLogger(__name__) -class Controller: - __slots__ = ("valid_video_loaded", "video", "config", "pipeline") - +class Controller(Observable): def __init__(self, video_path, config): self.attempt_to_load_video(video_path) self.config = ConfigController.from_updated_defaults(config) diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py index 474c0fb678..667ede78f2 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -1,6 +1,9 @@ +import os + from plugin import Plugin from video_overlay.controllers.overlay import Controller as OverlayController +from video_overlay.ui.menu import generic_overlay_elements, no_valid_video_elements class Vis_Generic_Video_Overlay(Plugin): @@ -29,3 +32,31 @@ def on_drop(self, paths): if self.controller.attempt_to_load_video(video_path): return True # event consumed return False # event not consumed + + def init_ui(self): + self.add_menu() + self._refresh_menu() + self.controller.add_observer("attempt_to_load_video", self._refresh_menu) + + def deinit_ui(self): + self.controller.remove_observer("attempt_to_load_video", self._refresh_menu) + self.remove_menu() + + def _refresh_menu(self, *args, **kwargs): + + if self.controller.valid_video_loaded: + menu_elements = generic_overlay_elements( + self.controller.video_path, self.controller.config + ) + icon_chr = "O" + title = "Video Overlay: {}".format( + os.path.basename(self.controller.video_path) + ) + else: + menu_elements = no_valid_video_elements() + icon_chr = "!" + title = "Video Overlay: No valid video loaded" + # first element corresponds to `Close` button, added in add_menu() + self.menu[1:] = menu_elements + self.menu_icon.label = icon_chr + self.menu.label = title diff --git a/pupil_src/shared_modules/video_overlay/ui/menu.py b/pupil_src/shared_modules/video_overlay/ui/menu.py new file mode 100644 index 0000000000..5c088b4ebb --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/ui/menu.py @@ -0,0 +1,37 @@ +import abc +from pyglui import ui + + +def no_valid_video_elements(): + return ( + ui.Info_Text("No valid overlay video loaded yet."), + ui.Info_Text("To load a video, drag and drop it onto Player."), + ui.Info_Text( + "Valid overlay videos conform to the Pupil data format and " + "their timestamps are in sync with the opened recording." + ), + ) + + +def generic_overlay_elements(video_path, config): + return ( + ui.Info_Text("Loaded video: {}".format(video_path)), + ui.Slider( + "value", + config.scale, + label="Scale", + min=config.scale.constraint.low, + max=config.scale.constraint.high, + step=0.05, + ), + ui.Slider( + "value", + config.alpha, + label="Transparency", + min=config.alpha.constraint.low, + max=config.alpha.constraint.high, + step=0.05, + ), + ui.Switch("value", config.hflip, label="Flip horizontally"), + ui.Switch("value", config.vflip, label="Flip vertically"), + ) From d4d5ad2f1b50cdd6973ce3657b5c551888ced6d7 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 21:26:02 +0200 Subject: [PATCH 04/20] Generic Video Overlay: Extract ui setup functions --- .../video_overlay/plugins/generic_overlay.py | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py index 667ede78f2..f8a4526bcc 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -1,3 +1,4 @@ +import collections import os from plugin import Plugin @@ -35,28 +36,44 @@ def on_drop(self, paths): def init_ui(self): self.add_menu() - self._refresh_menu() - self.controller.add_observer("attempt_to_load_video", self._refresh_menu) + self.refresh_menu() + self.controller.add_observer("attempt_to_load_video", self.refresh_menu) def deinit_ui(self): - self.controller.remove_observer("attempt_to_load_video", self._refresh_menu) + self.controller.remove_observer("attempt_to_load_video", self.refresh_menu) self.remove_menu() - def _refresh_menu(self, *args, **kwargs): - - if self.controller.valid_video_loaded: - menu_elements = generic_overlay_elements( - self.controller.video_path, self.controller.config - ) - icon_chr = "O" - title = "Video Overlay: {}".format( - os.path.basename(self.controller.video_path) - ) - else: - menu_elements = no_valid_video_elements() - icon_chr = "!" - title = "Video Overlay: No valid video loaded" + def refresh_menu(self, *args, **kwargs): + ui_setup = self._ui_setup() # first element corresponds to `Close` button, added in add_menu() - self.menu[1:] = menu_elements - self.menu_icon.label = icon_chr - self.menu.label = title + self.menu_icon.label = ui_setup.icon + self.menu.label = ui_setup.title + self.menu[1:] = ui_setup.menu_elements + + def _ui_setup(self): + return ( + self._ui_setup_if_valid() + if self.controller.valid_video_loaded + else self._ui_setup_if_not_valid() + ) + + def _ui_setup_if_valid(self): + video_basename = os.path.basename(self.controller.video_path) + menu_elements = generic_overlay_elements( + self.controller.video_path, self.controller.config + ) + return UISetup( + icon="O", + title="Video Overlay: {}".format(video_basename), + menu_elements=menu_elements, + ) + + def _ui_setup_if_not_valid(self): + return UISetup( + icon="!", + title="Video Overlay: No valid video loaded", + menu_elements=no_valid_video_elements(), + ) + + +UISetup = collections.namedtuple("UISetup", ("icon", "title", "menu_elements")) From db44abfe3057b22c8326496723c122ef6bb6c2d2 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 22:25:22 +0200 Subject: [PATCH 05/20] ConstrainedPosition: Implement __str__ --- pupil_src/shared_modules/video_overlay/utils/constraints.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pupil_src/shared_modules/video_overlay/utils/constraints.py b/pupil_src/shared_modules/video_overlay/utils/constraints.py index d0c21ab2a7..d0a0b0f41c 100644 --- a/pupil_src/shared_modules/video_overlay/utils/constraints.py +++ b/pupil_src/shared_modules/video_overlay/utils/constraints.py @@ -65,3 +65,6 @@ class ConstraintedPosition: def __init__(self, x, y): self.x = ConstraintedValue(x) self.y = ConstraintedValue(y) + + def __str__(self): + return "(x={}, y={})".format(self.x.value, self.y.value) From 16b6431062af48968dbf249b982681c1beae7240 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 22:25:57 +0200 Subject: [PATCH 06/20] video_overlay: Fix rendering with flipped origin --- pupil_src/shared_modules/video_overlay/controllers/overlay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay.py b/pupil_src/shared_modules/video_overlay/controllers/overlay.py index b2219fd9f9..c007e0fb28 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/overlay.py +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay.py @@ -45,7 +45,7 @@ def draw_on_frame(self, target_frame): self._render_overlay(target_frame.img, overlay_image) def _render_overlay(self, target_image, overlay_image): - overlay_origin = (self.config.origin.y.value, self.config.origin.x.value) + overlay_origin = (self.config.origin.x.value, self.config.origin.y.value) pm.transparent_image_overlay( overlay_origin, overlay_image, target_image, self.config.alpha.value ) From 9420cb24f5f350c6a8b377e515588819935a5d78 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 22:26:25 +0200 Subject: [PATCH 07/20] Video Overlay Draggable --- .../video_overlay/ui/interactions.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pupil_src/shared_modules/video_overlay/ui/interactions.py diff --git a/pupil_src/shared_modules/video_overlay/ui/interactions.py b/pupil_src/shared_modules/video_overlay/ui/interactions.py new file mode 100644 index 0000000000..647ea43e39 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/ui/interactions.py @@ -0,0 +1,44 @@ +class Draggable: + __slots__ = ("overlay", "drag_offset") + + def __init__(self, overlay): + self.drag_offset = None + self.overlay = overlay + + def on_click(self, pos, button, action): + if not self.overlay.valid_video_loaded: + return False # click event has not been consumed + + click_engaged = action == 1 + if click_engaged and self._in_bounds(pos): + self.drag_offset = self._calculate_offset(pos) + return True + self.drag_offset = None + return False + + def on_pos(self, pos): + if self.overlay.valid_video_loaded and self.drag_offset: + self.overlay.config.origin.x.value = int(pos[0] + self.drag_offset[0]) + self.overlay.config.origin.y.value = int(pos[1] + self.drag_offset[1]) + + def _in_bounds(self, pos): + curr_x, curr_y = pos + origin = self.overlay.config.origin + width, height = self._effective_overlay_frame_size() + x_in_bounds = origin.x.value < curr_x < origin.x.value + width + y_in_bounds = origin.y.value < curr_y < origin.y.value + height + return x_in_bounds and y_in_bounds + + def _calculate_offset(self, pos): + curr_x, curr_y = pos + origin = self.overlay.config.origin + x_offset = origin.x.value - curr_x + y_offset = origin.y.value - curr_y + return (x_offset, y_offset) + + def _effective_overlay_frame_size(self): + overlay_scale = self.overlay.config.scale.value + overlay_width, overlay_height = self.overlay.video.source.frame_size + overlay_width = round(overlay_width * overlay_scale) + overlay_height = round(overlay_height * overlay_scale) + return overlay_width, overlay_height From 4351a454ba92f31cc96873d6f61d26e53d438845 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 22:26:45 +0200 Subject: [PATCH 08/20] Generic Video Overlay: Support Draggable --- .../video_overlay/plugins/generic_overlay.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py index f8a4526bcc..4b18c32d2c 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -5,6 +5,7 @@ from video_overlay.controllers.overlay import Controller as OverlayController from video_overlay.ui.menu import generic_overlay_elements, no_valid_video_elements +from video_overlay.ui.interactions import Draggable class Vis_Generic_Video_Overlay(Plugin): @@ -38,8 +39,10 @@ def init_ui(self): self.add_menu() self.refresh_menu() self.controller.add_observer("attempt_to_load_video", self.refresh_menu) + self._setup_draggable() def deinit_ui(self): + self._tear_down_draggable() self.controller.remove_observer("attempt_to_load_video", self.refresh_menu) self.remove_menu() @@ -75,5 +78,17 @@ def _ui_setup_if_not_valid(self): menu_elements=no_valid_video_elements(), ) + def _setup_draggable(self): + self.draggable = Draggable(self.controller) + + def _tear_down_draggable(self): + del self.draggable + + def on_click(self, *args, **kwargs): + return self.draggable.on_click(*args, **kwargs) + + def on_pos(self, *args, **kwargs): + return self.draggable.on_pos(*args, **kwargs) + UISetup = collections.namedtuple("UISetup", ("icon", "title", "menu_elements")) From 00ddd9b4e27004c6ad32e66881244a9f4b59d295 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Sun, 14 Apr 2019 23:06:27 +0200 Subject: [PATCH 09/20] video_overlay: Adjust origin constraint before rendering --- .../shared_modules/video_overlay/controllers/overlay.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay.py b/pupil_src/shared_modules/video_overlay/controllers/overlay.py index c007e0fb28..a13efed249 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/overlay.py +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay.py @@ -5,6 +5,7 @@ from observable import Observable import video_overlay.utils.image_manipulation as IM +from video_overlay.utils.constraints import InclusiveConstraint from video_overlay.controllers.config import Controller as ConfigController from video_overlay.controllers.video import Controller as VideoController @@ -42,8 +43,16 @@ def draw_on_frame(self, target_frame): overlay_image = overlay_frame.img for param, manipulation in self.pipeline.items(): overlay_image = manipulation.apply_to(overlay_image, param.value) + + self._adjust_origin_constraint(target_frame.img, overlay_image) self._render_overlay(target_frame.img, overlay_image) + def _adjust_origin_constraint(self, target_image, overlay_image): + max_x = target_image.shape[1] - overlay_image.shape[1] + max_y = target_image.shape[0] - overlay_image.shape[0] + self.config.origin.x.constraint = InclusiveConstraint(low=0, high=max_x) + self.config.origin.y.constraint = InclusiveConstraint(low=0, high=max_y) + def _render_overlay(self, target_image, overlay_image): overlay_origin = (self.config.origin.x.value, self.config.origin.y.value) pm.transparent_image_overlay( From 8f8a90dd8d92a4d4a0f0165161a2b25df6e93352 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Mon, 15 Apr 2019 17:14:20 +0200 Subject: [PATCH 10/20] Move storage code from gaze_producer into global scope --- .../gaze_producer/model/__init__.py | 3 -- .../gaze_producer/model/calibration.py | 4 +- .../model/calibration_storage.py | 4 +- .../gaze_producer/model/gaze_mapper.py | 4 +- .../model/gaze_mapper_storage.py | 4 +- .../gaze_producer/model/reference_location.py | 4 +- .../model/reference_location_storage.py | 3 +- .../model/single_file_storage.py | 50 ------------------- .../{gaze_producer/model => }/storage.py | 39 +++++++++++++-- 9 files changed, 50 insertions(+), 65 deletions(-) delete mode 100644 pupil_src/shared_modules/gaze_producer/model/single_file_storage.py rename pupil_src/shared_modules/{gaze_producer/model => }/storage.py (75%) diff --git a/pupil_src/shared_modules/gaze_producer/model/__init__.py b/pupil_src/shared_modules/gaze_producer/model/__init__.py index 3064649ec7..51ee5d42eb 100644 --- a/pupil_src/shared_modules/gaze_producer/model/__init__.py +++ b/pupil_src/shared_modules/gaze_producer/model/__init__.py @@ -9,9 +9,6 @@ ---------------------------------------------------------------------------~(*) """ -from gaze_producer.model import storage -from gaze_producer.model.single_file_storage import SingleFileStorage - from gaze_producer.model.calibration import Calibration, CalibrationResult from gaze_producer.model.calibration_storage import CalibrationStorage diff --git a/pupil_src/shared_modules/gaze_producer/model/calibration.py b/pupil_src/shared_modules/gaze_producer/model/calibration.py index 6f9b49f967..7e09751c24 100644 --- a/pupil_src/shared_modules/gaze_producer/model/calibration.py +++ b/pupil_src/shared_modules/gaze_producer/model/calibration.py @@ -10,7 +10,7 @@ """ from collections import namedtuple -from gaze_producer import model +from storage import StorageItem # this plugin does not care about the content of the result, it just receives it from # the calibration routine and handles it to the gaze mapper @@ -19,7 +19,7 @@ ) -class Calibration(model.storage.StorageItem): +class Calibration(StorageItem): version = 1 def __init__( diff --git a/pupil_src/shared_modules/gaze_producer/model/calibration_storage.py b/pupil_src/shared_modules/gaze_producer/model/calibration_storage.py index 4d7dc5be47..4dc1dd64af 100644 --- a/pupil_src/shared_modules/gaze_producer/model/calibration_storage.py +++ b/pupil_src/shared_modules/gaze_producer/model/calibration_storage.py @@ -14,13 +14,15 @@ import file_methods as fm import make_unique + +from storage import Storage from gaze_producer import model from observable import Observable logger = logging.getLogger(__name__) -class CalibrationStorage(model.storage.Storage, Observable): +class CalibrationStorage(Storage, Observable): _calibration_suffix = "plcal" def __init__(self, rec_dir, plugin, get_recording_index_range, recording_uuid): diff --git a/pupil_src/shared_modules/gaze_producer/model/gaze_mapper.py b/pupil_src/shared_modules/gaze_producer/model/gaze_mapper.py index 344fa73181..4b5cc41ada 100644 --- a/pupil_src/shared_modules/gaze_producer/model/gaze_mapper.py +++ b/pupil_src/shared_modules/gaze_producer/model/gaze_mapper.py @@ -9,10 +9,10 @@ ---------------------------------------------------------------------------~(*) """ -from gaze_producer import model +from storage import StorageItem -class GazeMapper(model.storage.StorageItem): +class GazeMapper(StorageItem): version = 1 def __init__( diff --git a/pupil_src/shared_modules/gaze_producer/model/gaze_mapper_storage.py b/pupil_src/shared_modules/gaze_producer/model/gaze_mapper_storage.py index b7a3dfbd9b..75ce63fb8b 100644 --- a/pupil_src/shared_modules/gaze_producer/model/gaze_mapper_storage.py +++ b/pupil_src/shared_modules/gaze_producer/model/gaze_mapper_storage.py @@ -14,13 +14,15 @@ import file_methods as fm import make_unique + +from storage import SingleFileStorage from gaze_producer import model from observable import Observable logger = logging.getLogger(__name__) -class GazeMapperStorage(model.SingleFileStorage, Observable): +class GazeMapperStorage(SingleFileStorage, Observable): def __init__(self, calibration_storage, rec_dir, plugin, get_recording_index_range): super().__init__(rec_dir, plugin) self._calibration_storage = calibration_storage diff --git a/pupil_src/shared_modules/gaze_producer/model/reference_location.py b/pupil_src/shared_modules/gaze_producer/model/reference_location.py index 19c5c088a8..fafdbd7518 100644 --- a/pupil_src/shared_modules/gaze_producer/model/reference_location.py +++ b/pupil_src/shared_modules/gaze_producer/model/reference_location.py @@ -8,10 +8,10 @@ See COPYING and COPYING.LESSER for license details. ---------------------------------------------------------------------------~(*) """ -from gaze_producer import model +from storage import StorageItem -class ReferenceLocation(model.storage.StorageItem): +class ReferenceLocation(StorageItem): version = 1 def __init__(self, screen_pos, frame_index, timestamp): diff --git a/pupil_src/shared_modules/gaze_producer/model/reference_location_storage.py b/pupil_src/shared_modules/gaze_producer/model/reference_location_storage.py index c921dd7f15..8ef0962451 100644 --- a/pupil_src/shared_modules/gaze_producer/model/reference_location_storage.py +++ b/pupil_src/shared_modules/gaze_producer/model/reference_location_storage.py @@ -11,13 +11,14 @@ import logging +from storage import SingleFileStorage from gaze_producer import model from observable import Observable logger = logging.getLogger(__name__) -class ReferenceLocationStorage(model.SingleFileStorage, Observable): +class ReferenceLocationStorage(SingleFileStorage, Observable): def __init__(self, rec_dir, plugin): super().__init__(rec_dir, plugin) self._reference_locations = {} diff --git a/pupil_src/shared_modules/gaze_producer/model/single_file_storage.py b/pupil_src/shared_modules/gaze_producer/model/single_file_storage.py deleted file mode 100644 index 013b1db998..0000000000 --- a/pupil_src/shared_modules/gaze_producer/model/single_file_storage.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -(*)~--------------------------------------------------------------------------- -Pupil - eye tracking platform -Copyright (C) 2012-2019 Pupil Labs - -Distributed under the terms of the GNU -Lesser General Public License (LGPL v3.0). -See COPYING and COPYING.LESSER for license details. ----------------------------------------------------------------------------~(*) -""" -import abc -import logging -import os - -from gaze_producer import model - -logger = logging.getLogger(__name__) - - -class SingleFileStorage(model.storage.Storage, abc.ABC): - """ - Storage that can save and load all items from / to a single file - """ - def __init__(self, rec_dir, plugin): - super().__init__(plugin) - self._rec_dir = rec_dir - - def save_to_disk(self): - item_tuple_list = [item.as_tuple for item in self.items] - self._save_data_to_file(self._storage_file_path, item_tuple_list) - - def _load_from_disk(self): - item_tuple_list = self._load_data_from_file(self._storage_file_path) - if item_tuple_list: - for item_tuple in item_tuple_list: - item = self._item_class.from_tuple(item_tuple) - self.add(item) - - @property - @abc.abstractmethod - def _storage_file_name(self): - pass - - @property - def _storage_file_path(self): - return os.path.join(self._storage_folder_path, self._storage_file_name) - - @property - def _storage_folder_path(self): - return os.path.join(self._rec_dir, "offline_data") diff --git a/pupil_src/shared_modules/gaze_producer/model/storage.py b/pupil_src/shared_modules/storage.py similarity index 75% rename from pupil_src/shared_modules/gaze_producer/model/storage.py rename to pupil_src/shared_modules/storage.py index 59f7f0446e..2c791fb4d9 100644 --- a/pupil_src/shared_modules/gaze_producer/model/storage.py +++ b/pupil_src/shared_modules/storage.py @@ -55,7 +55,6 @@ def create_unique_id_from_string(string): return unique_id - class Storage(abc.ABC): def __init__(self, plugin): plugin.add_observer("cleanup", self._on_cleanup) @@ -124,7 +123,41 @@ def get_valid_filename(file_name): Copied from Django: https://github.com/django/django/blob/master/django/utils/text.py#L219 """ - file_name = str(file_name).strip().replace(' ', '_') + file_name = str(file_name).strip().replace(" ", "_") # django uses \w instead of _a-zA-Z0-9 but this leaves characters like ä, Ü, é # in the filename, which might be problematic - return re.sub(r'(?u)[^-_a-zA-Z0-9.]', '', file_name) + return re.sub(r"(?u)[^-_a-zA-Z0-9.]", "", file_name) + + +class SingleFileStorage(Storage, abc.ABC): + """ + Storage that can save and load all items from / to a single file + """ + + def __init__(self, rec_dir, plugin): + super().__init__(plugin) + self._rec_dir = rec_dir + + def save_to_disk(self): + item_tuple_list = [item.as_tuple for item in self.items] + self._save_data_to_file(self._storage_file_path, item_tuple_list) + + def _load_from_disk(self): + item_tuple_list = self._load_data_from_file(self._storage_file_path) + if item_tuple_list: + for item_tuple in item_tuple_list: + item = self._item_class.from_tuple(item_tuple) + self.add(item) + + @property + @abc.abstractmethod + def _storage_file_name(self): + pass + + @property + def _storage_file_path(self): + return os.path.join(self._storage_folder_path, self._storage_file_name) + + @property + def _storage_folder_path(self): + return os.path.join(self._rec_dir, "offline_data") From 18f8354483073c426834c98df0712ed98e705c0e Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Mon, 15 Apr 2019 17:16:23 +0200 Subject: [PATCH 11/20] Generic Overlay: Let single Overlay plugin manage multiple overlays --- .../video_overlay/controllers/config.py | 44 ------- .../controllers/overlay_manager.py | 40 +++++++ .../video_overlay/models/config.py | 45 ++++++++ .../video_overlay/plugins/generic_overlay.py | 108 ++++++------------ .../video_overlay/ui/management.py | 59 ++++++++++ .../shared_modules/video_overlay/ui/menu.py | 28 ++++- .../video.py => workers/frame_fetcher.py} | 2 +- .../overlay_renderer.py} | 22 ++-- 8 files changed, 211 insertions(+), 137 deletions(-) delete mode 100644 pupil_src/shared_modules/video_overlay/controllers/config.py create mode 100644 pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py create mode 100644 pupil_src/shared_modules/video_overlay/models/config.py create mode 100644 pupil_src/shared_modules/video_overlay/ui/management.py rename pupil_src/shared_modules/video_overlay/{controllers/video.py => workers/frame_fetcher.py} (98%) rename pupil_src/shared_modules/video_overlay/{controllers/overlay.py => workers/overlay_renderer.py} (74%) diff --git a/pupil_src/shared_modules/video_overlay/controllers/config.py b/pupil_src/shared_modules/video_overlay/controllers/config.py deleted file mode 100644 index 9130805f82..0000000000 --- a/pupil_src/shared_modules/video_overlay/controllers/config.py +++ /dev/null @@ -1,44 +0,0 @@ -from video_overlay.utils.constraints import ( - ConstraintedPosition, - ConstraintedValue, - BooleanConstraint, - InclusiveConstraint, -) - - -class Controller: - __slots__ = ("origin", "scale", "alpha", "hflip", "vflip") - - @classmethod - def from_updated_defaults(cls, config_subset): - defaults = cls.default_dict() - defaults.update(config_subset) - return cls(**defaults) - - @staticmethod - def default_dict(): - return { - "origin_x": 0, - "origin_y": 0, - "scale": 1.0, - "alpha": 1.0, - "hflip": False, - "vflip": False, - } - - def __init__(self, origin_x, origin_y, scale, alpha, hflip, vflip): - self.origin = ConstraintedPosition(origin_x, origin_y) - self.scale = ConstraintedValue(scale, InclusiveConstraint(low=0.2, high=1.0)) - self.alpha = ConstraintedValue(alpha, InclusiveConstraint(low=0.1, high=1.0)) - self.hflip = ConstraintedValue(hflip, BooleanConstraint()) - self.vflip = ConstraintedValue(vflip, BooleanConstraint()) - - def get_init_dict(self): - return { - "origin_x": self.origin.x.value, - "origin_y": self.origin.y.value, - "scale": self.scale.value, - "alpha": self.alpha.value, - "hflip": self.hflip.value, - "vflip": self.vflip.value, - } diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py new file mode 100644 index 0000000000..1461752dbb --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py @@ -0,0 +1,40 @@ +import abc +from storage import SingleFileStorage +from video_overlay.models.config import Configuration +from video_overlay.workers.overlay_renderer import OverlayRenderer + + +class OverlayManager(SingleFileStorage): + def __init__(self, rec_dir, plugin): + super().__init__(rec_dir, plugin) + self._overlays = [] + self._load_from_disk() + + @property + def _storage_file_name(self): + return "video_overlays.msgpack" + + @property + def _item_class(self): + return Configuration + + def add(self, item): + overlay = OverlayRenderer(item) + self._overlays.append(overlay) + + def delete(self, item): + for overlay in self._overlays.copy(): + if overlay.config is item: + self._overlays.remove(overlay) + + @property + def items(self): + yield from (overlay.config for overlay in self._overlays) + + @property + def overlays(self): + return self._overlays + + @property + def most_recent(self): + return self._overlays[-1] diff --git a/pupil_src/shared_modules/video_overlay/models/config.py b/pupil_src/shared_modules/video_overlay/models/config.py new file mode 100644 index 0000000000..d61b06fab6 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/models/config.py @@ -0,0 +1,45 @@ +from storage import StorageItem + +from video_overlay.utils.constraints import ( + ConstraintedPosition, + ConstraintedValue, + BooleanConstraint, + InclusiveConstraint, +) + + +class Configuration(StorageItem): + version = 0 + + def __init__( + self, + video_path=None, + origin_x=0, + origin_y=0, + scale=1.0, + alpha=1.0, + hflip=False, + vflip=False, + ): + self.video_path = video_path + self.origin = ConstraintedPosition(origin_x, origin_y) + self.scale = ConstraintedValue(scale, InclusiveConstraint(low=0.2, high=1.0)) + self.alpha = ConstraintedValue(alpha, InclusiveConstraint(low=0.1, high=1.0)) + self.hflip = ConstraintedValue(hflip, BooleanConstraint()) + self.vflip = ConstraintedValue(vflip, BooleanConstraint()) + + @property + def as_tuple(self): + return ( + self.video_path, + self.origin.x.value, + self.origin.y.value, + self.scale.value, + self.alpha.value, + self.hflip.value, + self.vflip.value, + ) + + @staticmethod + def from_tuple(tuple_): + return Configuration(*tuple_) diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py index 4b18c32d2c..19185a7180 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -1,94 +1,56 @@ import collections import os +from observable import Observable from plugin import Plugin +from video_capture.utils import VIDEO_EXTS -from video_overlay.controllers.overlay import Controller as OverlayController -from video_overlay.ui.menu import generic_overlay_elements, no_valid_video_elements -from video_overlay.ui.interactions import Draggable +from video_overlay.models.config import Configuration +from video_overlay.controllers.overlay_manager import OverlayManager +from video_overlay.ui.management import UIManagement -class Vis_Generic_Video_Overlay(Plugin): - uniqueness = "not_unique" +class Vis_Generic_Video_Overlay(Observable, Plugin): + icon_chr = "O" - def __init__(self, g_pool, video_path=None, config=None): + def __init__(self, g_pool): super().__init__(g_pool) - config = config or {"scale": 0.5, "alpha": 0.9, "hflip": False, "vflip": False} - self.controller = OverlayController(video_path, config) - - def get_init_dict(self): - return { - "video_path": self.controller.video_path, - "config": self.controller.config.get_init_dict(), - } + self.manager = OverlayManager(g_pool.rec_dir, self) def recent_events(self, events): if "frame" in events: frame = events["frame"] - self.controller.draw_on_frame(frame) + for overlay in self.manager.overlays: + overlay.draw_on_frame(frame) def on_drop(self, paths): - remaining_paths = paths.copy() - while remaining_paths and not self.controller.valid_video_loaded: - video_path = remaining_paths.pop(0) - if self.controller.attempt_to_load_video(video_path): - return True # event consumed - return False # event not consumed + valid_paths = [p for p in paths if self.valid_path(p)] + for video_path in valid_paths: + self._add_overlay_to_storage(video_path) + # event only consumed if at least one valid file was present + return bool(valid_paths) + + @staticmethod + def valid_path(path): + # splitext()[1] always starts with `.` + ext = os.path.splitext(path)[1][1:] + return ext in VIDEO_EXTS + + def _add_overlay_to_storage(self, video_path): + config = Configuration(video_path) + self.manager.add(config) + self.manager.save_to_disk() + self._overlay_added_to_storage(self.manager.most_recent) + + def _overlay_added_to_storage(self, overlay): + print(type(overlay), overlay) + pass # observed to create menus and draggables def init_ui(self): self.add_menu() - self.refresh_menu() - self.controller.add_observer("attempt_to_load_video", self.refresh_menu) - self._setup_draggable() + self.menu.label = "Generic Video Overlays" + self.ui = UIManagement(self, self.menu, self.manager.overlays) def deinit_ui(self): - self._tear_down_draggable() - self.controller.remove_observer("attempt_to_load_video", self.refresh_menu) + self.ui.teardown() self.remove_menu() - - def refresh_menu(self, *args, **kwargs): - ui_setup = self._ui_setup() - # first element corresponds to `Close` button, added in add_menu() - self.menu_icon.label = ui_setup.icon - self.menu.label = ui_setup.title - self.menu[1:] = ui_setup.menu_elements - - def _ui_setup(self): - return ( - self._ui_setup_if_valid() - if self.controller.valid_video_loaded - else self._ui_setup_if_not_valid() - ) - - def _ui_setup_if_valid(self): - video_basename = os.path.basename(self.controller.video_path) - menu_elements = generic_overlay_elements( - self.controller.video_path, self.controller.config - ) - return UISetup( - icon="O", - title="Video Overlay: {}".format(video_basename), - menu_elements=menu_elements, - ) - - def _ui_setup_if_not_valid(self): - return UISetup( - icon="!", - title="Video Overlay: No valid video loaded", - menu_elements=no_valid_video_elements(), - ) - - def _setup_draggable(self): - self.draggable = Draggable(self.controller) - - def _tear_down_draggable(self): - del self.draggable - - def on_click(self, *args, **kwargs): - return self.draggable.on_click(*args, **kwargs) - - def on_pos(self, *args, **kwargs): - return self.draggable.on_pos(*args, **kwargs) - - -UISetup = collections.namedtuple("UISetup", ("icon", "title", "menu_elements")) diff --git a/pupil_src/shared_modules/video_overlay/ui/management.py b/pupil_src/shared_modules/video_overlay/ui/management.py new file mode 100644 index 0000000000..33d63c10eb --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/ui/management.py @@ -0,0 +1,59 @@ +from pyglui import ui + +from video_overlay.ui.interactions import Draggable +from video_overlay.ui.menu import GenericOverlayMenu + + +class UIManagement: + def __init__(self, plugin, parent_menu, existing_overlays): + self._parent_menu = parent_menu + self._draggables = [] + + self._add_menu_with_general_elements() + self._add_menu_for_existing_overlays(existing_overlays) + self._add_draggable_for_existing_overlays(existing_overlays) + + plugin.add_observer("on_click", self._on_click) + plugin.add_observer("on_pos", self._on_pos) + plugin.add_observer("_add_overlay_to_storage", self._add_overlay_menu) + plugin.add_observer("_add_overlay_to_storage", self._add_overlay_draggable) + + def _add_menu_with_general_elements(self): + self._parent_menu.append( + ui.Info_Text( + "This plugin is able to overlay videos with synchronized timestamps." + ) + ) + self._parent_menu.append( + ui.Info_Text( + "Drag and drop such videos onto the " + "main Player window in order to load them" + ) + ) + self._parent_menu.append(ui.Separator()) + + def _add_menu_for_existing_overlays(self, existing_overlays): + for overlay in existing_overlays: + self._add_overlay_menu(overlay) + + def _add_overlay_menu(self, overlay): + self._parent_menu.append(GenericOverlayMenu(overlay)) + + def _add_draggable_for_existing_overlays(self, existing_overlays): + for overlay in existing_overlays: + self._add_overlay_draggable(overlay) + + def _add_overlay_draggable(self, overlay): + draggable = Draggable(overlay) + self._draggables.append(draggable) + + def teardown(self): + del self._parent_menu[:] + del self._draggables[:] + del self._parent_menu + + def _on_click(self, *args, **kwargs): + pass + + def _on_pos(self, *args, **kwargs): + pass diff --git a/pupil_src/shared_modules/video_overlay/ui/menu.py b/pupil_src/shared_modules/video_overlay/ui/menu.py index 5c088b4ebb..2acd812002 100644 --- a/pupil_src/shared_modules/video_overlay/ui/menu.py +++ b/pupil_src/shared_modules/video_overlay/ui/menu.py @@ -1,11 +1,12 @@ -import abc +import os +import weakref + from pyglui import ui -def no_valid_video_elements(): +def not_valid_video_elements(video_path): return ( - ui.Info_Text("No valid overlay video loaded yet."), - ui.Info_Text("To load a video, drag and drop it onto Player."), + ui.Info_Text("No valid overlay video found at {}".format(video_path)), ui.Info_Text( "Valid overlay videos conform to the Pupil data format and " "their timestamps are in sync with the opened recording." @@ -13,9 +14,9 @@ def no_valid_video_elements(): ) -def generic_overlay_elements(video_path, config): +def generic_overlay_elements(config): return ( - ui.Info_Text("Loaded video: {}".format(video_path)), + ui.Info_Text("Loaded video: {}".format(config.video_path)), ui.Slider( "value", config.scale, @@ -35,3 +36,18 @@ def generic_overlay_elements(video_path, config): ui.Switch("value", config.hflip, label="Flip horizontally"), ui.Switch("value", config.vflip, label="Flip vertically"), ) + + +class GenericOverlayMenu: + def __init__(self, overlay): + self.overlay = weakref.ref(overlay) + video_basename = os.path.basename(self.overlay().config.video_path) + self.menu = ui.Growing_Menu(video_basename) + self.menu.collapsed = True + + @property + def update_menu(self): + if self.overlay().valid_video_loaded: + self.menu[:] = generic_overlay_elements(self.overlay().config) + else: + self.menu[:] = not_valid_video_elements(self.overlay().config.video_path) diff --git a/pupil_src/shared_modules/video_overlay/controllers/video.py b/pupil_src/shared_modules/video_overlay/workers/frame_fetcher.py similarity index 98% rename from pupil_src/shared_modules/video_overlay/controllers/video.py rename to pupil_src/shared_modules/video_overlay/workers/frame_fetcher.py index b5013dd015..bf2acdb0a9 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/video.py +++ b/pupil_src/shared_modules/video_overlay/workers/frame_fetcher.py @@ -11,7 +11,7 @@ class _Empty: """Replacement for actual g_pool object""" -class Controller: +class FrameFetcher: __slots__ = ("source", "current_frame") def __init__(self, video_path): diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay.py b/pupil_src/shared_modules/video_overlay/workers/overlay_renderer.py similarity index 74% rename from pupil_src/shared_modules/video_overlay/controllers/overlay.py rename to pupil_src/shared_modules/video_overlay/workers/overlay_renderer.py index a13efed249..7938cd3958 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/overlay.py +++ b/pupil_src/shared_modules/video_overlay/workers/overlay_renderer.py @@ -6,24 +6,24 @@ import video_overlay.utils.image_manipulation as IM from video_overlay.utils.constraints import InclusiveConstraint -from video_overlay.controllers.config import Controller as ConfigController -from video_overlay.controllers.video import Controller as VideoController +from video_overlay.models.config import Configuration +from video_overlay.workers.frame_fetcher import FrameFetcher logger = logging.getLogger(__name__) -class Controller(Observable): - def __init__(self, video_path, config): - self.attempt_to_load_video(video_path) - self.config = ConfigController.from_updated_defaults(config) +class OverlayRenderer: + def __init__(self, config): + self.config = config + self.attempt_to_load_video() self.pipeline = self.setup_pipeline() - def attempt_to_load_video(self, video_path): + def attempt_to_load_video(self): try: - self.video = VideoController(video_path) + self.video = FrameFetcher(self.config.video_path) self.valid_video_loaded = True except FileNotFoundError: - logger.debug("Could not load overlay: {}".format(video_path)) + logger.debug("Could not load overlay: {}".format(self.config.video_path)) self.valid_video_loaded = False return self.valid_video_loaded @@ -58,7 +58,3 @@ def _render_overlay(self, target_image, overlay_image): pm.transparent_image_overlay( overlay_origin, overlay_image, target_image, self.config.alpha.value ) - - @property - def video_path(self): - return self.video.source.source_path if self.valid_video_loaded else None From 5269b86f1f32a0a327bbf856bd60c387812b30cd Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Thu, 18 Apr 2019 11:40:36 +0200 Subject: [PATCH 12/20] Generic Video Overlay, ui interactions --- .../controllers/overlay_manager.py | 4 ++ .../video_overlay/models/config.py | 4 +- .../video_overlay/plugins/generic_overlay.py | 21 +++++++++-- .../video_overlay/ui/interactions.py | 14 +++++++ .../video_overlay/ui/management.py | 37 ++++++++++++++----- .../shared_modules/video_overlay/ui/menu.py | 16 +++++++- 6 files changed, 78 insertions(+), 18 deletions(-) diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py index 1461752dbb..e8c7396ac3 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py @@ -38,3 +38,7 @@ def overlays(self): @property def most_recent(self): return self._overlays[-1] + + def remove_overlay(self, overlay): + self._overlays.remove(overlay) + self.save_to_disk() diff --git a/pupil_src/shared_modules/video_overlay/models/config.py b/pupil_src/shared_modules/video_overlay/models/config.py index d61b06fab6..66d6ef6abd 100644 --- a/pupil_src/shared_modules/video_overlay/models/config.py +++ b/pupil_src/shared_modules/video_overlay/models/config.py @@ -16,8 +16,8 @@ def __init__( video_path=None, origin_x=0, origin_y=0, - scale=1.0, - alpha=1.0, + scale=0.6, + alpha=0.8, hflip=False, vflip=False, ): diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py index 19185a7180..aca812169d 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -8,6 +8,7 @@ from video_overlay.models.config import Configuration from video_overlay.controllers.overlay_manager import OverlayManager from video_overlay.ui.management import UIManagement +from video_overlay.ui.interactions import current_mouse_pos class Vis_Generic_Video_Overlay(Observable, Plugin): @@ -24,9 +25,19 @@ def recent_events(self, events): overlay.draw_on_frame(frame) def on_drop(self, paths): + multi_drop_offset = 10 + inital_drop = current_mouse_pos( + self.g_pool.main_window, + self.g_pool.camera_render_size, + self.g_pool.capture.frame_size, + ) valid_paths = [p for p in paths if self.valid_path(p)] for video_path in valid_paths: - self._add_overlay_to_storage(video_path) + self._add_overlay_to_storage(video_path, inital_drop) + inital_drop = ( + inital_drop[0] + multi_drop_offset, + inital_drop[1] + multi_drop_offset, + ) # event only consumed if at least one valid file was present return bool(valid_paths) @@ -36,20 +47,22 @@ def valid_path(path): ext = os.path.splitext(path)[1][1:] return ext in VIDEO_EXTS - def _add_overlay_to_storage(self, video_path): - config = Configuration(video_path) + def _add_overlay_to_storage(self, video_path, initial_pos=(0, 0)): + config = Configuration( + video_path, origin_x=initial_pos[0], origin_y=initial_pos[1] + ) self.manager.add(config) self.manager.save_to_disk() self._overlay_added_to_storage(self.manager.most_recent) def _overlay_added_to_storage(self, overlay): - print(type(overlay), overlay) pass # observed to create menus and draggables def init_ui(self): self.add_menu() self.menu.label = "Generic Video Overlays" self.ui = UIManagement(self, self.menu, self.manager.overlays) + self.ui.add_observer("remove_overlay", self.manager.remove_overlay) def deinit_ui(self): self.ui.teardown() diff --git a/pupil_src/shared_modules/video_overlay/ui/interactions.py b/pupil_src/shared_modules/video_overlay/ui/interactions.py index 647ea43e39..4f0536a095 100644 --- a/pupil_src/shared_modules/video_overlay/ui/interactions.py +++ b/pupil_src/shared_modules/video_overlay/ui/interactions.py @@ -1,3 +1,7 @@ +from glfw import getHDPIFactor, glfwGetCurrentContext, glfwGetCursorPos +from methods import normalize, denormalize + + class Draggable: __slots__ = ("overlay", "drag_offset") @@ -42,3 +46,13 @@ def _effective_overlay_frame_size(self): overlay_width = round(overlay_width * overlay_scale) overlay_height = round(overlay_height * overlay_scale) return overlay_width, overlay_height + + +def current_mouse_pos(window, camera_render_size, frame_size): + hdpi_fac = getHDPIFactor(window) + x, y = glfwGetCursorPos(glfwGetCurrentContext()) + pos = x * hdpi_fac, y * hdpi_fac + pos = normalize(pos, camera_render_size) + # Position in img pixels + pos = denormalize(pos, frame_size) + return (int(pos[0]), int(pos[1])) diff --git a/pupil_src/shared_modules/video_overlay/ui/management.py b/pupil_src/shared_modules/video_overlay/ui/management.py index 33d63c10eb..69ffe6b86e 100644 --- a/pupil_src/shared_modules/video_overlay/ui/management.py +++ b/pupil_src/shared_modules/video_overlay/ui/management.py @@ -1,13 +1,18 @@ +from collections import OrderedDict from pyglui import ui +from observable import Observable + from video_overlay.ui.interactions import Draggable -from video_overlay.ui.menu import GenericOverlayMenu +from video_overlay.ui.menu import GenericOverlayMenuRenderer -class UIManagement: +class UIManagement(Observable): def __init__(self, plugin, parent_menu, existing_overlays): self._parent_menu = parent_menu - self._draggables = [] + self._menu_renderers = {} + # Insrt order is important for correct dragging behavior + self._draggables = OrderedDict() self._add_menu_with_general_elements() self._add_menu_for_existing_overlays(existing_overlays) @@ -15,8 +20,8 @@ def __init__(self, plugin, parent_menu, existing_overlays): plugin.add_observer("on_click", self._on_click) plugin.add_observer("on_pos", self._on_pos) - plugin.add_observer("_add_overlay_to_storage", self._add_overlay_menu) - plugin.add_observer("_add_overlay_to_storage", self._add_overlay_draggable) + plugin.add_observer("_overlay_added_to_storage", self._add_overlay_menu) + plugin.add_observer("_overlay_added_to_storage", self._add_overlay_draggable) def _add_menu_with_general_elements(self): self._parent_menu.append( @@ -37,7 +42,10 @@ def _add_menu_for_existing_overlays(self, existing_overlays): self._add_overlay_menu(overlay) def _add_overlay_menu(self, overlay): - self._parent_menu.append(GenericOverlayMenu(overlay)) + renderer = GenericOverlayMenuRenderer(overlay) + renderer.add_observer("remove_button_clicked", self.remove_overlay) + self._menu_renderers[overlay] = renderer + self._parent_menu.append(renderer.menu) def _add_draggable_for_existing_overlays(self, existing_overlays): for overlay in existing_overlays: @@ -45,15 +53,24 @@ def _add_draggable_for_existing_overlays(self, existing_overlays): def _add_overlay_draggable(self, overlay): draggable = Draggable(overlay) - self._draggables.append(draggable) + self._draggables[overlay] = draggable + + def remove_overlay(self, overlay): + renderer = self._menu_renderers[overlay] + renderer.remove_all_observers("remove_button_clicked") + del self._menu_renderers[overlay] + del self._draggables[overlay] + self._parent_menu.remove(renderer.menu) def teardown(self): del self._parent_menu[:] - del self._draggables[:] del self._parent_menu + self._draggables.clear() + self._menu_renderers.clear() def _on_click(self, *args, **kwargs): - pass + # iterate over draggables in reverse order since last element is drawn on top + any(d.on_click(*args, **kwargs) for d in reversed(self._draggables.values())) def _on_pos(self, *args, **kwargs): - pass + any(d.on_pos(*args, **kwargs) for d in reversed(self._draggables.values())) diff --git a/pupil_src/shared_modules/video_overlay/ui/menu.py b/pupil_src/shared_modules/video_overlay/ui/menu.py index 2acd812002..2ef0722de1 100644 --- a/pupil_src/shared_modules/video_overlay/ui/menu.py +++ b/pupil_src/shared_modules/video_overlay/ui/menu.py @@ -3,6 +3,8 @@ from pyglui import ui +from observable import Observable + def not_valid_video_elements(video_path): return ( @@ -38,16 +40,26 @@ def generic_overlay_elements(config): ) -class GenericOverlayMenu: +class GenericOverlayMenuRenderer(Observable): def __init__(self, overlay): self.overlay = weakref.ref(overlay) video_basename = os.path.basename(self.overlay().config.video_path) self.menu = ui.Growing_Menu(video_basename) self.menu.collapsed = True + self.update_menu() - @property def update_menu(self): if self.overlay().valid_video_loaded: self.menu[:] = generic_overlay_elements(self.overlay().config) else: self.menu[:] = not_valid_video_elements(self.overlay().config.video_path) + self._append_remove_button() + + def _append_remove_button(self): + self.menu.append(ui.Button("Remove overlay", self._remove)) + + def _remove(self): + self.remove_button_clicked(self.overlay()) + + def remove_button_clicked(self, overlay): + pass # observable From de45b5643d86d6502fea22924c7354412ca90122 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Thu, 18 Apr 2019 14:08:31 +0200 Subject: [PATCH 13/20] Vie Eye Overlay, adapt t new code base, part 1 --- .../controllers/overlay_manager.py | 5 ++ .../video_overlay/models/config.py | 12 +++ .../video_overlay/plugins/eye_overlay.py | 73 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py index e8c7396ac3..ef6ad6ab67 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py @@ -10,6 +10,11 @@ def __init__(self, rec_dir, plugin): self._overlays = [] self._load_from_disk() + # Save current settings to disk on get_init_dict() instead of cleanup(). + # This ensures that the World Video Exporter loads the most recent settings. + plugin.remove_observer("cleanup", self._on_cleanup) + plugin.add_observer("get_init_dict", self._on_cleanup) + @property def _storage_file_name(self): return "video_overlays.msgpack" diff --git a/pupil_src/shared_modules/video_overlay/models/config.py b/pupil_src/shared_modules/video_overlay/models/config.py index 66d6ef6abd..325aa53945 100644 --- a/pupil_src/shared_modules/video_overlay/models/config.py +++ b/pupil_src/shared_modules/video_overlay/models/config.py @@ -43,3 +43,15 @@ def as_tuple(self): @staticmethod def from_tuple(tuple_): return Configuration(*tuple_) + + @property + def as_dict(self): + return { + "video_path": self.video_path, + "origin": self.origin.x.value, + "origin": self.origin.y.value, + "scale": self.scale.value, + "alpha": self.alpha.value, + "hflip": self.hflip.value, + "vflip": self.vflip.value, + } diff --git a/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py new file mode 100644 index 0000000000..4be22b8eb1 --- /dev/null +++ b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py @@ -0,0 +1,73 @@ +import os +import glob + +from plugin import Visualizer_Plugin_Base + +from video_overlay.workers.overlay_renderer import OverlayRenderer +from video_overlay.models.config import Configuration + + +class Vis_Eye_Video_Overlay(Visualizer_Plugin_Base): + icon_chr = chr(0xEC02) + icon_font = "pupil_icons" + + def __init__(self, g_pool, scale, alpha, eye0_config, eye1_config): + super().__init__(g_pool) + self._scale = scale + self._alpha = alpha + + self.eye0 = self._setup_eye(0, eye0_config) + self.eye1 = self._setup_eye(1, eye1_config) + + def recent_events(self, events): + if "frame" in events: + frame = events["frame"] + for overlay in (self.eye0, self.eye1): + overlay.draw_on_frame(frame) + + @property + def scale(self): + return self._scale + + @scale.setter + def scale(self, val): + self._scale = val + self.eye0.config.scale = val + self.eye1.config.scale = val + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, val): + self._alpha = val + self.eye0.config.alpha = val + self.eye1.config.alpha = val + + def _setup_eye(self, eye_id, prefilled_config): + video_path = self._video_path_for_eye(eye_id) + prefilled_config["video_path"] = video_path + prefilled_config["scale"] = self.scale + prefilled_config["alpha"] = self.alpha + config = Configuration(**prefilled_config) + overlay = OverlayRenderer(config) + return overlay + + def _video_path_for_eye(self, eye_id): + rec_dir = self.g_pool.rec_dir + video_file_pattern = "eye{}.*".format(eye_id) + video_path_pattern = os.path.join(rec_dir, video_file_pattern) + try: + video_path_candidates = glob.iglob(video_path_pattern) + return next(video_path_candidates) + except StopIteration: + return None + + def get_init_dict(self): + return { + "scale": self.scale, + "alpha": self.alpha, + "eye0_config": self.eye0.config.as_dict(), + "eye1_config": self.eye1.config.as_dict(), + } From 45210f77d576e74d2ad15c279f27bf1a45d8d116 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Tue, 23 Apr 2019 11:29:18 +0200 Subject: [PATCH 14/20] Vis Eye Overlay, adapt to new code base, part 2 --- pupil_src/launchables/player.py | 6 +- .../plugins/world_video_exporter.py | 3 +- .../controllers/overlay_manager.py | 15 ++- .../video_overlay/models/config.py | 5 +- .../video_overlay/plugins/__init__.py | 1 + .../video_overlay/plugins/eye_overlay.py | 38 ++++++-- .../video_overlay/plugins/generic_overlay.py | 4 +- .../video_overlay/ui/management.py | 97 ++++++++++++++----- .../shared_modules/video_overlay/ui/menu.py | 9 +- 9 files changed, 130 insertions(+), 48 deletions(-) diff --git a/pupil_src/launchables/player.py b/pupil_src/launchables/player.py index 6de85a00f5..c76a064373 100644 --- a/pupil_src/launchables/player.py +++ b/pupil_src/launchables/player.py @@ -97,7 +97,6 @@ def player(rec_dir, ipc_pub_url, ipc_sub_url, ipc_push_url, user_dir, app_versio from vis_fixation import Vis_Fixation # from vis_scan_path import Vis_Scan_Path - from vis_eye_video_overlay import Vis_Eye_Video_Overlay from seek_control import Seek_Control from offline_surface_tracker import Offline_Surface_Tracker @@ -120,7 +119,10 @@ def player(rec_dir, ipc_pub_url, ipc_sub_url, ipc_push_url, user_dir, app_versio from video_export.plugins.eye_video_exporter import Eye_Video_Exporter from video_export.plugins.world_video_exporter import World_Video_Exporter from video_capture import File_Source - from video_overlay.plugins import Vis_Generic_Video_Overlay + from video_overlay.plugins import ( + Vis_Generic_Video_Overlay, + Vis_Eye_Video_Overlay, + ) assert VersionFormat(pyglui_version) >= VersionFormat( "1.23" diff --git a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py index 535a1de2a8..5f5a356d59 100644 --- a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py +++ b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py @@ -116,10 +116,9 @@ def _export_world_video( # Plug-ins from plugin import Plugin_List, import_runtime_plugins from video_capture import EndofVideoError, File_Source - from video_overlay.plugins import Vis_Generic_Video_Overlay + from video_overlay.plugins import Vis_Generic_Video_Overlay, Vis_Eye_Video_Overlay from vis_circle import Vis_Circle from vis_cross import Vis_Cross - from vis_eye_video_overlay import Vis_Eye_Video_Overlay from vis_light_points import Vis_Light_Points from vis_polyline import Vis_Polyline from vis_scan_path import Vis_Scan_Path diff --git a/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py index ef6ad6ab67..fd99b8755a 100644 --- a/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py +++ b/pupil_src/shared_modules/video_overlay/controllers/overlay_manager.py @@ -9,11 +9,7 @@ def __init__(self, rec_dir, plugin): super().__init__(rec_dir, plugin) self._overlays = [] self._load_from_disk() - - # Save current settings to disk on get_init_dict() instead of cleanup(). - # This ensures that the World Video Exporter loads the most recent settings. - plugin.remove_observer("cleanup", self._on_cleanup) - plugin.add_observer("get_init_dict", self._on_cleanup) + self._patch_on_cleanup(plugin) @property def _storage_file_name(self): @@ -47,3 +43,12 @@ def most_recent(self): def remove_overlay(self, overlay): self._overlays.remove(overlay) self.save_to_disk() + + def _patch_on_cleanup(self, plugin): + """Patches cleanup observer to trigger on get_init_dict(). + + Save current settings to disk on get_init_dict() instead of cleanup(). + This ensures that the World Video Exporter loads the most recent settings. + """ + plugin.remove_observer("cleanup", self._on_cleanup) + plugin.add_observer("get_init_dict", self._on_cleanup) diff --git a/pupil_src/shared_modules/video_overlay/models/config.py b/pupil_src/shared_modules/video_overlay/models/config.py index 325aa53945..106b098c28 100644 --- a/pupil_src/shared_modules/video_overlay/models/config.py +++ b/pupil_src/shared_modules/video_overlay/models/config.py @@ -44,12 +44,11 @@ def as_tuple(self): def from_tuple(tuple_): return Configuration(*tuple_) - @property def as_dict(self): return { "video_path": self.video_path, - "origin": self.origin.x.value, - "origin": self.origin.y.value, + "origin_x": self.origin.x.value, + "origin_y": self.origin.y.value, "scale": self.scale.value, "alpha": self.alpha.value, "hflip": self.hflip.value, diff --git a/pupil_src/shared_modules/video_overlay/plugins/__init__.py b/pupil_src/shared_modules/video_overlay/plugins/__init__.py index 5efdb7f7a8..5c6e3b6644 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/__init__.py +++ b/pupil_src/shared_modules/video_overlay/plugins/__init__.py @@ -1 +1,2 @@ from video_overlay.plugins.generic_overlay import Vis_Generic_Video_Overlay +from video_overlay.plugins.eye_overlay import Vis_Eye_Video_Overlay diff --git a/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py index 4be22b8eb1..0ecfe09d2e 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py @@ -1,18 +1,32 @@ import os import glob -from plugin import Visualizer_Plugin_Base +from plugin import Plugin +from observable import Observable from video_overlay.workers.overlay_renderer import OverlayRenderer from video_overlay.models.config import Configuration +from video_overlay.ui.management import UIManagementEyes -class Vis_Eye_Video_Overlay(Visualizer_Plugin_Base): +class Vis_Eye_Video_Overlay(Observable, Plugin): icon_chr = chr(0xEC02) icon_font = "pupil_icons" - def __init__(self, g_pool, scale, alpha, eye0_config, eye1_config): + def __init__( + self, + g_pool, + scale=0.6, + alpha=0.8, + show_ellipses=True, + eye0_config=None, + eye1_config=None, + ): super().__init__(g_pool) + eye0_config = eye0_config or {"vflip": True, "origin_x": 210, "origin_y": 60} + eye1_config = eye1_config or {"hflip": True, "origin_x": 10, "origin_y": 60} + + self.show_ellipses = show_ellipses self._scale = scale self._alpha = alpha @@ -32,8 +46,8 @@ def scale(self): @scale.setter def scale(self, val): self._scale = val - self.eye0.config.scale = val - self.eye1.config.scale = val + self.eye0.config.scale.value = val + self.eye1.config.scale.value = val @property def alpha(self): @@ -42,8 +56,17 @@ def alpha(self): @alpha.setter def alpha(self, val): self._alpha = val - self.eye0.config.alpha = val - self.eye1.config.alpha = val + self.eye0.config.alpha.value = val + self.eye1.config.alpha.value = val + + def init_ui(self): + self.add_menu() + self.menu.label = "Eye Video Overlays" + self.ui = UIManagementEyes(self, self.menu, (self.eye0, self.eye1)) + + def deinit_ui(self): + self.ui.teardown() + self.remove_menu() def _setup_eye(self, eye_id, prefilled_config): video_path = self._video_path_for_eye(eye_id) @@ -68,6 +91,7 @@ def get_init_dict(self): return { "scale": self.scale, "alpha": self.alpha, + "show_ellipses": self.show_ellipses, "eye0_config": self.eye0.config.as_dict(), "eye1_config": self.eye1.config.as_dict(), } diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py index aca812169d..913cd3ec6a 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -7,7 +7,7 @@ from video_overlay.models.config import Configuration from video_overlay.controllers.overlay_manager import OverlayManager -from video_overlay.ui.management import UIManagement +from video_overlay.ui.management import UIManagementGeneric from video_overlay.ui.interactions import current_mouse_pos @@ -61,7 +61,7 @@ def _overlay_added_to_storage(self, overlay): def init_ui(self): self.add_menu() self.menu.label = "Generic Video Overlays" - self.ui = UIManagement(self, self.menu, self.manager.overlays) + self.ui = UIManagementGeneric(self, self.menu, self.manager.overlays) self.ui.add_observer("remove_overlay", self.manager.remove_overlay) def deinit_ui(self): diff --git a/pupil_src/shared_modules/video_overlay/ui/management.py b/pupil_src/shared_modules/video_overlay/ui/management.py index 69ffe6b86e..a3c959e9ee 100644 --- a/pupil_src/shared_modules/video_overlay/ui/management.py +++ b/pupil_src/shared_modules/video_overlay/ui/management.py @@ -1,17 +1,19 @@ +import abc +import weakref from collections import OrderedDict from pyglui import ui from observable import Observable from video_overlay.ui.interactions import Draggable -from video_overlay.ui.menu import GenericOverlayMenuRenderer +from video_overlay.ui.menu import GenericOverlayMenuRenderer, OverlayMenuRenderer -class UIManagement(Observable): +class UIManagement(Observable, abc.ABC): def __init__(self, plugin, parent_menu, existing_overlays): self._parent_menu = parent_menu self._menu_renderers = {} - # Insrt order is important for correct dragging behavior + # Insert order is important for correct dragging behavior self._draggables = OrderedDict() self._add_menu_with_general_elements() @@ -20,6 +22,44 @@ def __init__(self, plugin, parent_menu, existing_overlays): plugin.add_observer("on_click", self._on_click) plugin.add_observer("on_pos", self._on_pos) + + @abc.abstractmethod + def _add_menu_with_general_elements(self): + raise NotImplementedError + + @abc.abstractmethod + def _add_overlay_menu(self, overlay): + raise NotImplementedError + + def teardown(self): + del self._parent_menu[:] + del self._parent_menu + self._draggables.clear() + self._menu_renderers.clear() + + def _on_click(self, *args, **kwargs): + # iterate over draggables in reverse order since last element is drawn on top + any(d.on_click(*args, **kwargs) for d in reversed(self._draggables.values())) + + def _on_pos(self, *args, **kwargs): + any(d.on_pos(*args, **kwargs) for d in reversed(self._draggables.values())) + + def _add_menu_for_existing_overlays(self, existing_overlays): + for overlay in existing_overlays: + self._add_overlay_menu(overlay) + + def _add_draggable_for_existing_overlays(self, existing_overlays): + for overlay in existing_overlays: + self._add_overlay_draggable(overlay) + + def _add_overlay_draggable(self, overlay): + draggable = Draggable(overlay) + self._draggables[overlay] = draggable + + +class UIManagementGeneric(UIManagement): + def __init__(self, plugin, parent_menu, existing_overlays): + super().__init__(plugin, parent_menu, existing_overlays) plugin.add_observer("_overlay_added_to_storage", self._add_overlay_menu) plugin.add_observer("_overlay_added_to_storage", self._add_overlay_draggable) @@ -37,24 +77,12 @@ def _add_menu_with_general_elements(self): ) self._parent_menu.append(ui.Separator()) - def _add_menu_for_existing_overlays(self, existing_overlays): - for overlay in existing_overlays: - self._add_overlay_menu(overlay) - def _add_overlay_menu(self, overlay): renderer = GenericOverlayMenuRenderer(overlay) renderer.add_observer("remove_button_clicked", self.remove_overlay) self._menu_renderers[overlay] = renderer self._parent_menu.append(renderer.menu) - def _add_draggable_for_existing_overlays(self, existing_overlays): - for overlay in existing_overlays: - self._add_overlay_draggable(overlay) - - def _add_overlay_draggable(self, overlay): - draggable = Draggable(overlay) - self._draggables[overlay] = draggable - def remove_overlay(self, overlay): renderer = self._menu_renderers[overlay] renderer.remove_all_observers("remove_button_clicked") @@ -62,15 +90,34 @@ def remove_overlay(self, overlay): del self._draggables[overlay] self._parent_menu.remove(renderer.menu) - def teardown(self): - del self._parent_menu[:] - del self._parent_menu - self._draggables.clear() - self._menu_renderers.clear() - def _on_click(self, *args, **kwargs): - # iterate over draggables in reverse order since last element is drawn on top - any(d.on_click(*args, **kwargs) for d in reversed(self._draggables.values())) +class UIManagementEyes(UIManagement): + def __init__(self, plugin, parent_menu, existing_overlays): + self.plugin = weakref.ref(plugin) + super().__init__(plugin, parent_menu, existing_overlays) - def _on_pos(self, *args, **kwargs): - any(d.on_pos(*args, **kwargs) for d in reversed(self._draggables.values())) + def _add_menu_with_general_elements(self): + self._parent_menu.append( + ui.Info_Text( + "Show the eye video overlaid on top of the world video. " + "Eye 0 is usually the right eye." + ) + ) + self._parent_menu.append( + ui.Slider( + "alpha", self.plugin(), min=0.1, step=0.05, max=1.0, label="Opacity" + ) + ) + self._parent_menu.append( + ui.Slider( + "scale", self.plugin(), min=0.2, step=0.05, max=1.0, label="Video Scale" + ) + ) + self._parent_menu.append( + ui.Switch("show_ellipses", self.plugin(), label="Visualize Ellipses") + ) + + def _add_overlay_menu(self, overlay): + renderer = OverlayMenuRenderer(overlay) + self._menu_renderers[overlay] = renderer + self._parent_menu.append(renderer.menu) diff --git a/pupil_src/shared_modules/video_overlay/ui/menu.py b/pupil_src/shared_modules/video_overlay/ui/menu.py index 2ef0722de1..8052db7899 100644 --- a/pupil_src/shared_modules/video_overlay/ui/menu.py +++ b/pupil_src/shared_modules/video_overlay/ui/menu.py @@ -30,7 +30,7 @@ def generic_overlay_elements(config): ui.Slider( "value", config.alpha, - label="Transparency", + label="Opacity", min=config.alpha.constraint.low, max=config.alpha.constraint.high, step=0.05, @@ -40,7 +40,7 @@ def generic_overlay_elements(config): ) -class GenericOverlayMenuRenderer(Observable): +class OverlayMenuRenderer(Observable): def __init__(self, overlay): self.overlay = weakref.ref(overlay) video_basename = os.path.basename(self.overlay().config.video_path) @@ -55,6 +55,11 @@ def update_menu(self): self.menu[:] = not_valid_video_elements(self.overlay().config.video_path) self._append_remove_button() + def _append_remove_button(self): + pass # do not show remove button by default + + +class GenericOverlayMenuRenderer(OverlayMenuRenderer): def _append_remove_button(self): self.menu.append(ui.Button("Remove overlay", self._remove)) From ed2378c976769cbe783b2df91e3df8de4c2b3c29 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Thu, 25 Apr 2019 13:56:08 +0200 Subject: [PATCH 15/20] Vis Eye Overlay, adapt to new code base, part 3 --- .../video_overlay/plugins/eye_overlay.py | 30 +++++- .../video_overlay/ui/management.py | 4 +- .../shared_modules/video_overlay/ui/menu.py | 100 ++++++++++++------ .../video_overlay/utils/image_manipulation.py | 3 +- .../video_overlay/workers/overlay_renderer.py | 21 ++-- 5 files changed, 111 insertions(+), 47 deletions(-) diff --git a/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py index 0ecfe09d2e..63b7021f26 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py @@ -1,12 +1,14 @@ import os import glob +import player_methods as pm from plugin import Plugin from observable import Observable -from video_overlay.workers.overlay_renderer import OverlayRenderer +from video_overlay.workers.overlay_renderer import EyeOverlayRenderer from video_overlay.models.config import Configuration from video_overlay.ui.management import UIManagementEyes +from video_overlay.utils.constraints import ConstraintedValue, BooleanConstraint class Vis_Eye_Video_Overlay(Observable, Plugin): @@ -26,7 +28,8 @@ def __init__( eye0_config = eye0_config or {"vflip": True, "origin_x": 210, "origin_y": 60} eye1_config = eye1_config or {"hflip": True, "origin_x": 10, "origin_y": 60} - self.show_ellipses = show_ellipses + self.current_frame_ts = None + self.show_ellipses = ConstraintedValue(show_ellipses, BooleanConstraint()) self._scale = scale self._alpha = alpha @@ -36,6 +39,7 @@ def __init__( def recent_events(self, events): if "frame" in events: frame = events["frame"] + self.current_frame_ts = frame.timestamp for overlay in (self.eye0, self.eye1): overlay.draw_on_frame(frame) @@ -74,7 +78,9 @@ def _setup_eye(self, eye_id, prefilled_config): prefilled_config["scale"] = self.scale prefilled_config["alpha"] = self.alpha config = Configuration(**prefilled_config) - overlay = OverlayRenderer(config) + overlay = EyeOverlayRenderer( + config, self.show_ellipses, self.make_current_pupil_datum_getter(eye_id) + ) return overlay def _video_path_for_eye(self, eye_id): @@ -85,13 +91,27 @@ def _video_path_for_eye(self, eye_id): video_path_candidates = glob.iglob(video_path_pattern) return next(video_path_candidates) except StopIteration: - return None + return "/not/found/eye{}.mp4".format(eye_id) def get_init_dict(self): return { "scale": self.scale, "alpha": self.alpha, - "show_ellipses": self.show_ellipses, + "show_ellipses": self.show_ellipses.value, "eye0_config": self.eye0.config.as_dict(), "eye1_config": self.eye1.config.as_dict(), } + + def make_current_pupil_datum_getter(self, eye_id): + def _pupil_getter(): + try: + pupil_data = self.g_pool.pupil_positions_by_id[eye_id] + closest_pupil_idx = pm.find_closest( + pupil_data.data_ts, self.current_frame_ts + ) + current_datum = pupil_data.data[closest_pupil_idx] + return current_datum + except (IndexError, ValueError): + return None + + return _pupil_getter diff --git a/pupil_src/shared_modules/video_overlay/ui/management.py b/pupil_src/shared_modules/video_overlay/ui/management.py index a3c959e9ee..60e77de4c6 100644 --- a/pupil_src/shared_modules/video_overlay/ui/management.py +++ b/pupil_src/shared_modules/video_overlay/ui/management.py @@ -6,7 +6,7 @@ from observable import Observable from video_overlay.ui.interactions import Draggable -from video_overlay.ui.menu import GenericOverlayMenuRenderer, OverlayMenuRenderer +from video_overlay.ui.menu import GenericOverlayMenuRenderer, EyesOverlayMenuRenderer class UIManagement(Observable, abc.ABC): @@ -118,6 +118,6 @@ def _add_menu_with_general_elements(self): ) def _add_overlay_menu(self, overlay): - renderer = OverlayMenuRenderer(overlay) + renderer = EyesOverlayMenuRenderer(overlay) self._menu_renderers[overlay] = renderer self._parent_menu.append(renderer.menu) diff --git a/pupil_src/shared_modules/video_overlay/ui/menu.py b/pupil_src/shared_modules/video_overlay/ui/menu.py index 8052db7899..996f897cd6 100644 --- a/pupil_src/shared_modules/video_overlay/ui/menu.py +++ b/pupil_src/shared_modules/video_overlay/ui/menu.py @@ -1,3 +1,4 @@ +import abc import os import weakref @@ -6,41 +7,37 @@ from observable import Observable -def not_valid_video_elements(video_path): - return ( - ui.Info_Text("No valid overlay video found at {}".format(video_path)), - ui.Info_Text( - "Valid overlay videos conform to the Pupil data format and " - "their timestamps are in sync with the opened recording." - ), +def make_scale_slider(config): + return ui.Slider( + "value", + config.scale, + label="Scale", + min=config.scale.constraint.low, + max=config.scale.constraint.high, + step=0.05, ) -def generic_overlay_elements(config): - return ( - ui.Info_Text("Loaded video: {}".format(config.video_path)), - ui.Slider( - "value", - config.scale, - label="Scale", - min=config.scale.constraint.low, - max=config.scale.constraint.high, - step=0.05, - ), - ui.Slider( - "value", - config.alpha, - label="Opacity", - min=config.alpha.constraint.low, - max=config.alpha.constraint.high, - step=0.05, - ), - ui.Switch("value", config.hflip, label="Flip horizontally"), - ui.Switch("value", config.vflip, label="Flip vertically"), +def make_alpha_slider(config): + return ui.Slider( + "value", + config.alpha, + label="Opacity", + min=config.alpha.constraint.low, + max=config.alpha.constraint.high, + step=0.05, ) -class OverlayMenuRenderer(Observable): +def make_hflip_switch(config): + return ui.Switch("value", config.hflip, label="Flip horizontally") + + +def make_vflip_switch(config): + return ui.Switch("value", config.vflip, label="Flip vertically") + + +class OverlayMenuRenderer(Observable, abc.ABC): def __init__(self, overlay): self.overlay = weakref.ref(overlay) video_basename = os.path.basename(self.overlay().config.video_path) @@ -50,14 +47,22 @@ def __init__(self, overlay): def update_menu(self): if self.overlay().valid_video_loaded: - self.menu[:] = generic_overlay_elements(self.overlay().config) + self.menu[:] = self._generic_overlay_elements() else: - self.menu[:] = not_valid_video_elements(self.overlay().config.video_path) + self.menu[:] = self._not_valid_video_elements() self._append_remove_button() def _append_remove_button(self): pass # do not show remove button by default + @abc.abstractmethod + def _generic_overlay_elements(self): + raise NotImplementedError + + @abc.abstractmethod + def _not_valid_video_elements(self): + raise NotImplementedError + class GenericOverlayMenuRenderer(OverlayMenuRenderer): def _append_remove_button(self): @@ -68,3 +73,36 @@ def _remove(self): def remove_button_clicked(self, overlay): pass # observable + + def _generic_overlay_elements(self): + config = self.overlay().config + return ( + ui.Info_Text("Loaded video: {}".format(config.video_path)), + make_scale_slider(config), + make_alpha_slider(config), + make_hflip_switch(config), + make_vflip_switch(config), + ) + + def _not_valid_video_elements(self): + video_path = self.overlay().config.video_path + return ( + ui.Info_Text("No valid overlay video found at {}".format(video_path)), + ui.Info_Text( + "Valid overlay videos conform to the Pupil data format and " + "their timestamps are in sync with the opened recording." + ), + ) + + +class EyesOverlayMenuRenderer(OverlayMenuRenderer): + def _generic_overlay_elements(self): + config = self.overlay().config + return (make_hflip_switch(config), make_vflip_switch(config)) + + def _not_valid_video_elements(self): + video_path = self.overlay().config.video_path + video_name = os.path.basename(video_path) + return ( + ui.Info_Text("{} was not recorded or cannot be found.".format(video_name)), + ) diff --git a/pupil_src/shared_modules/video_overlay/utils/image_manipulation.py b/pupil_src/shared_modules/video_overlay/utils/image_manipulation.py index 5dfe45214b..e8a7d162cd 100644 --- a/pupil_src/shared_modules/video_overlay/utils/image_manipulation.py +++ b/pupil_src/shared_modules/video_overlay/utils/image_manipulation.py @@ -38,7 +38,8 @@ def apply_to(self, image, parameter): """parameter: boolean indicating if pupil should be rendered""" if parameter: pupil_position = self.pupil_getter() - self.render_pupil(image, pupil_position) + if pupil_position: + self.render_pupil(image, pupil_position) return image def render_pupil(self, image, pupil_position): diff --git a/pupil_src/shared_modules/video_overlay/workers/overlay_renderer.py b/pupil_src/shared_modules/video_overlay/workers/overlay_renderer.py index 7938cd3958..e52831cb14 100644 --- a/pupil_src/shared_modules/video_overlay/workers/overlay_renderer.py +++ b/pupil_src/shared_modules/video_overlay/workers/overlay_renderer.py @@ -28,20 +28,18 @@ def attempt_to_load_video(self): return self.valid_video_loaded def setup_pipeline(self): - return OrderedDict( - ( - (self.config.scale, IM.ScaleTransform()), - (self.config.hflip, IM.HorizontalFlip()), - (self.config.vflip, IM.VerticalFlip()), - ) - ) + return [ + (self.config.scale, IM.ScaleTransform()), + (self.config.hflip, IM.HorizontalFlip()), + (self.config.vflip, IM.VerticalFlip()), + ] def draw_on_frame(self, target_frame): if not self.valid_video_loaded: return overlay_frame = self.video.closest_frame_to_ts(target_frame.timestamp) overlay_image = overlay_frame.img - for param, manipulation in self.pipeline.items(): + for param, manipulation in self.pipeline: overlay_image = manipulation.apply_to(overlay_image, param.value) self._adjust_origin_constraint(target_frame.img, overlay_image) @@ -58,3 +56,10 @@ def _render_overlay(self, target_image, overlay_image): pm.transparent_image_overlay( overlay_origin, overlay_image, target_image, self.config.alpha.value ) + + +class EyeOverlayRenderer(OverlayRenderer): + def __init__(self, config, should_render_pupil_data, pupil_getter): + super().__init__(config) + pupil_renderer = (should_render_pupil_data, IM.PupilRenderer(pupil_getter)) + self.pipeline.insert(0, pupil_renderer) From c3c2892ab5f5c4235d573acae8e9fd1d0abe247a Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Thu, 25 Apr 2019 13:56:52 +0200 Subject: [PATCH 16/20] World Video Exporter: Create pupil_positions_by_id --- .../video_export/plugins/world_video_exporter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py index 5f5a356d59..601013ef3d 100644 --- a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py +++ b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py @@ -73,6 +73,8 @@ def _precomputed_eye_data_for_range(self, export_range): pre_computed = { "gaze": self.g_pool.gaze_positions, "pupil": self.g_pool.pupil_positions, + "pupil_by_id_0": self.g_pool.pupil_positions_by_id[0], + "pupil_by_id_1": self.g_pool.pupil_positions_by_id[1], "fixations": self.g_pool.fixations, } @@ -234,6 +236,10 @@ def _export_world_video( ] g_pool.pupil_positions = pm.Bisector(**pre_computed_eye_data["pupil"]) + g_pool.pupil_positions_by_id = ( + pm.Bisector(**pre_computed_eye_data["pupil_by_id_0"]), + pm.Bisector(**pre_computed_eye_data["pupil_by_id_1"]), + ) g_pool.gaze_positions = pm.Bisector(**pre_computed_eye_data["gaze"]) g_pool.fixations = pm.Affiliator(**pre_computed_eye_data["fixations"]) From 917932a24847780d65afe1d78861b92ea3a1adb9 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Thu, 25 Apr 2019 14:08:31 +0200 Subject: [PATCH 17/20] Overlay plugins: Rename and remove Vis_ prefix --- pupil_src/launchables/player.py | 9 +++------ .../video_export/plugins/world_video_exporter.py | 6 +++--- .../shared_modules/video_overlay/plugins/__init__.py | 4 ++-- .../shared_modules/video_overlay/plugins/eye_overlay.py | 2 +- .../video_overlay/plugins/generic_overlay.py | 2 +- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/pupil_src/launchables/player.py b/pupil_src/launchables/player.py index c76a064373..fe34e277be 100644 --- a/pupil_src/launchables/player.py +++ b/pupil_src/launchables/player.py @@ -119,10 +119,7 @@ def player(rec_dir, ipc_pub_url, ipc_sub_url, ipc_push_url, user_dir, app_versio from video_export.plugins.eye_video_exporter import Eye_Video_Exporter from video_export.plugins.world_video_exporter import World_Video_Exporter from video_capture import File_Source - from video_overlay.plugins import ( - Vis_Generic_Video_Overlay, - Vis_Eye_Video_Overlay, - ) + from video_overlay.plugins import Video_Overlay, Eye_Overlay assert VersionFormat(pyglui_version) >= VersionFormat( "1.23" @@ -144,8 +141,8 @@ def player(rec_dir, ipc_pub_url, ipc_sub_url, ipc_push_url, user_dir, app_versio Vis_Light_Points, Vis_Cross, Vis_Watermark, - Vis_Eye_Video_Overlay, - Vis_Generic_Video_Overlay, + Eye_Overlay, + Video_Overlay, # Vis_Scan_Path, Offline_Fixation_Detector, Offline_Blink_Detection, diff --git a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py index 601013ef3d..1689148586 100644 --- a/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py +++ b/pupil_src/shared_modules/video_export/plugins/world_video_exporter.py @@ -118,7 +118,7 @@ def _export_world_video( # Plug-ins from plugin import Plugin_List, import_runtime_plugins from video_capture import EndofVideoError, File_Source - from video_overlay.plugins import Vis_Generic_Video_Overlay, Vis_Eye_Video_Overlay + 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 @@ -141,8 +141,8 @@ def _export_world_video( Vis_Light_Points, Vis_Watermark, Vis_Scan_Path, - Vis_Eye_Video_Overlay, - Vis_Generic_Video_Overlay, + Eye_Overlay, + Video_Overlay, ], key=lambda x: x.__name__, ) diff --git a/pupil_src/shared_modules/video_overlay/plugins/__init__.py b/pupil_src/shared_modules/video_overlay/plugins/__init__.py index 5c6e3b6644..e34f43ab19 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/__init__.py +++ b/pupil_src/shared_modules/video_overlay/plugins/__init__.py @@ -1,2 +1,2 @@ -from video_overlay.plugins.generic_overlay import Vis_Generic_Video_Overlay -from video_overlay.plugins.eye_overlay import Vis_Eye_Video_Overlay +from video_overlay.plugins.generic_overlay import Video_Overlay +from video_overlay.plugins.eye_overlay import Eye_Overlay diff --git a/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py index 63b7021f26..c76dc7ccff 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/eye_overlay.py @@ -11,7 +11,7 @@ from video_overlay.utils.constraints import ConstraintedValue, BooleanConstraint -class Vis_Eye_Video_Overlay(Observable, Plugin): +class Eye_Overlay(Observable, Plugin): icon_chr = chr(0xEC02) icon_font = "pupil_icons" diff --git a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py index 913cd3ec6a..f361ec98dc 100644 --- a/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py +++ b/pupil_src/shared_modules/video_overlay/plugins/generic_overlay.py @@ -11,7 +11,7 @@ from video_overlay.ui.interactions import current_mouse_pos -class Vis_Generic_Video_Overlay(Observable, Plugin): +class Video_Overlay(Observable, Plugin): icon_chr = "O" def __init__(self, g_pool): From a1116932b97404ce26c96a00ebf2d7530d5e0df6 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Thu, 25 Apr 2019 14:59:14 +0200 Subject: [PATCH 18/20] Overlays: Use glfw constant instead of hardcoded number --- pupil_src/shared_modules/video_overlay/ui/interactions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pupil_src/shared_modules/video_overlay/ui/interactions.py b/pupil_src/shared_modules/video_overlay/ui/interactions.py index 4f0536a095..39550e1b29 100644 --- a/pupil_src/shared_modules/video_overlay/ui/interactions.py +++ b/pupil_src/shared_modules/video_overlay/ui/interactions.py @@ -1,4 +1,4 @@ -from glfw import getHDPIFactor, glfwGetCurrentContext, glfwGetCursorPos +from glfw import getHDPIFactor, glfwGetCurrentContext, glfwGetCursorPos, GLFW_PRESS from methods import normalize, denormalize @@ -13,7 +13,7 @@ def on_click(self, pos, button, action): if not self.overlay.valid_video_loaded: return False # click event has not been consumed - click_engaged = action == 1 + click_engaged = action == GLFW_PRESS if click_engaged and self._in_bounds(pos): self.drag_offset = self._calculate_offset(pos) return True From 53640535d5de0270239c69f5e616b1a4ab8bf384 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Thu, 25 Apr 2019 17:06:55 +0200 Subject: [PATCH 19/20] =?UTF-8?q?Eye=20Overlay:=20Fix=20=E2=80=9Cshow=20el?= =?UTF-8?q?lipses=E2=80=9D=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pupil_src/shared_modules/video_overlay/ui/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupil_src/shared_modules/video_overlay/ui/management.py b/pupil_src/shared_modules/video_overlay/ui/management.py index 60e77de4c6..5a9e6a55b2 100644 --- a/pupil_src/shared_modules/video_overlay/ui/management.py +++ b/pupil_src/shared_modules/video_overlay/ui/management.py @@ -114,7 +114,7 @@ def _add_menu_with_general_elements(self): ) ) self._parent_menu.append( - ui.Switch("show_ellipses", self.plugin(), label="Visualize Ellipses") + ui.Switch("value", self.plugin().show_ellipses, label="Visualize Ellipses") ) def _add_overlay_menu(self, overlay): From 28ace73b1f115c3f3454588f5ef552323a5d3de5 Mon Sep 17 00:00:00 2001 From: Roman Roibu Date: Mon, 29 Apr 2019 11:59:18 +0200 Subject: [PATCH 20/20] Apply suggestions from code review Co-Authored-By: papr --- pupil_src/shared_modules/video_overlay/utils/constraints.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pupil_src/shared_modules/video_overlay/utils/constraints.py b/pupil_src/shared_modules/video_overlay/utils/constraints.py index d0a0b0f41c..ecad3fce04 100644 --- a/pupil_src/shared_modules/video_overlay/utils/constraints.py +++ b/pupil_src/shared_modules/video_overlay/utils/constraints.py @@ -39,11 +39,11 @@ def __init__(self, value, constraint=NoConstraint()): @property def value(self): - return self._val + return self.constraint.apply_to(self._val) @value.setter def value(self, new_val): - self._val = self.constraint.apply_to(new_val) + self._val = new_val @property def constraint(self): @@ -52,7 +52,6 @@ def constraint(self): @constraint.setter def constraint(self, new_constraint): self._constraint = new_constraint - self.value = self.value # apply new constraint @constraint.deleter def constraint(self):