diff --git a/pupil_src/launchables/player.py b/pupil_src/launchables/player.py index 949bf3c6d4..97069b43ec 100644 --- a/pupil_src/launchables/player.py +++ b/pupil_src/launchables/player.py @@ -572,6 +572,18 @@ def set_window_size(): g_pool.gui.append(g_pool.quickbar) # we always load these plugins + _pupil_producer_plugins = [ + # In priority order (first is default) + ("Pupil_From_Recording", {}), + ("Offline_Pupil_Detection", {}), + ] + _pupil_producer_plugins = list(reversed(_pupil_producer_plugins)) + _gaze_producer_plugins = [ + # In priority order (first is default) + ("GazeFromRecording", {}), + ("GazeFromOfflineCalibration", {}), + ] + _gaze_producer_plugins = list(reversed(_gaze_producer_plugins)) default_plugins = [ ("Plugin_Manager", {}), ("Seek_Control", {}), @@ -582,14 +594,25 @@ def set_window_size(): ("System_Graphs", {}), ("System_Timelines", {}), ("World_Video_Exporter", {}), - ("Pupil_From_Recording", {}), - ("GazeFromRecording", {}), + *_pupil_producer_plugins, + *_gaze_producer_plugins, ("Audio_Playback", {}), ] - - g_pool.plugins = Plugin_List( - g_pool, session_settings.get("loaded_plugins", default_plugins) - ) + _plugins_to_load = session_settings.get("loaded_plugins", None) + if _plugins_to_load is None: + # If no plugins are available from a previous session, + # then use the default plugin list + _plugins_to_load = default_plugins + else: + # If there are plugins available from a previous session, + # then prepend plugins that are required, but might have not been available before + _plugins_to_load = [ + *_pupil_producer_plugins, + *_gaze_producer_plugins, + *_plugins_to_load, + ] + + g_pool.plugins = Plugin_List(g_pool, _plugins_to_load) # Manually add g_pool.capture to the plugin list g_pool.plugins._plugins.append(g_pool.capture) diff --git a/pupil_src/shared_modules/accuracy_visualizer.py b/pupil_src/shared_modules/accuracy_visualizer.py index fb4d6f0a17..4e5d7b1930 100644 --- a/pupil_src/shared_modules/accuracy_visualizer.py +++ b/pupil_src/shared_modules/accuracy_visualizer.py @@ -36,9 +36,49 @@ logger = logging.getLogger(__name__) -Calculation_Result = namedtuple( - "Calculation_Result", ["result", "num_used", "num_total"] -) + +class CalculationResult(T.NamedTuple): + result: float + num_used: int + num_total: int + + +class CorrelatedAndCoordinateTransformedResult(T.NamedTuple): + """Holds result from correlating reference and gaze data and their respective + transformations into norm, image, and camera coordinate systems. + """ + + norm_space: np.ndarray # shape: 2*n, 2 + image_space: np.ndarray # shape: 2*n, 2 + camera_space: np.ndarray # shape: 2*n, 3 + + @staticmethod + def empty() -> "CorrelatedAndCoordinateTransformedResult": + return CorrelatedAndCoordinateTransformedResult( + norm_space=np.ndarray([]), + image_space=np.ndarray([]), + camera_space=np.ndarray([]), + ) + + +class CorrelationError(ValueError): + pass + + +class AccuracyPrecisionResult(T.NamedTuple): + accuracy: CalculationResult + precision: CalculationResult + error_lines: np.ndarray + correlation: CorrelatedAndCoordinateTransformedResult + + @staticmethod + def failed() -> "AccuracyPrecisionResult": + return AccuracyPrecisionResult( + accuracy=CalculationResult(0.0, 0, 0), + precision=CalculationResult(0.0, 0, 0), + error_lines=np.array([]), + correlation=CorrelatedAndCoordinateTransformedResult.empty(), + ) class ValidationInput: @@ -105,10 +145,6 @@ def update( @staticmethod def __gazer_class_from_name(gazer_class_name: str) -> T.Optional[T.Any]: - if "HMD" in gazer_class_name: - logger.info("Accuracy visualization is disabled for HMD calibration") - return None - gazers_by_name = gazer_classes_by_class_name(registered_gazer_classes()) try: @@ -337,7 +373,7 @@ def recalculate(self): succession_threshold=self.succession_threshold, ) - accuracy = results[0].result + accuracy = results.accuracy.result if np.isnan(accuracy): self.accuracy = None logger.warning( @@ -349,7 +385,7 @@ def recalculate(self): "Angular accuracy: {}. Used {} of {} samples.".format(*results[0]) ) - precision = results[1].result + precision = results.precision.result if np.isnan(precision): self.precision = None logger.warning( @@ -361,9 +397,8 @@ def recalculate(self): "Angular precision: {}. Used {} of {} samples.".format(*results[1]) ) - self.error_lines = results[2] - - ref_locations = [loc["norm_pos"] for loc in self.recent_input.ref_list] + self.error_lines = results.error_lines + ref_locations = results.correlation.norm_space[1::2, :] if len(ref_locations) >= 3: hull = ConvexHull(ref_locations) # requires at least 3 points self.calibration_area = hull.points[hull.vertices, :] @@ -378,36 +413,25 @@ def calc_acc_prec_errlines( intrinsics, outlier_threshold, succession_threshold=np.cos(np.deg2rad(0.5)), - ): + ) -> AccuracyPrecisionResult: gazer = gazer_class(g_pool, params=gazer_params) gaze_pos = gazer.map_pupil_to_gaze(pupil_list) ref_pos = ref_list - width, height = intrinsics.resolution - - # reuse closest_matches_monocular to correlate one label to each prediction - # correlated['ref']: prediction, correlated['pupil']: label location - correlated = closest_matches_monocular(gaze_pos, ref_pos) - # [[pred.x, pred.y, label.x, label.y], ...], shape: n x 4 - locations = np.array( - [(*e["ref"]["norm_pos"], *e["pupil"]["norm_pos"]) for e in correlated] - ) - if locations.size == 0: - accuracy_result = Calculation_Result(0.0, 0, 0) - precision_result = Calculation_Result(0.0, 0, 0) - error_lines = np.array([]) - return accuracy_result, precision_result, error_lines - error_lines = locations.copy() # n x 4 - locations[:, ::2] *= width - locations[:, 1::2] = (1.0 - locations[:, 1::2]) * height - locations.shape = -1, 2 + try: + correlation_result = Accuracy_Visualizer.correlate_and_coordinate_transform( + gaze_pos, ref_pos, intrinsics + ) + error_lines = correlation_result.norm_space.reshape(-1, 4) + undistorted_3d = correlation_result.camera_space + except CorrelationError: + return AccuracyPrecisionResult.failed() # Accuracy is calculated as the average angular # offset (distance) (in degrees of visual angle) # between fixations locations and the corresponding # locations of the fixation targets. - undistorted_3d = intrinsics.unprojectPoints(locations, normalize=True) # Cosine distance of A and B: (A @ B) / (||A|| * ||B||) # No need to calculate norms, since A and B are normalized in our case. @@ -426,7 +450,7 @@ def calc_acc_prec_errlines( -1, 2 ) # shape: num_used x 2 accuracy = np.rad2deg(np.arccos(selected_samples.clip(-1.0, 1.0).mean())) - accuracy_result = Calculation_Result(accuracy, num_used, num_total) + accuracy_result = CalculationResult(accuracy, num_used, num_total) # lets calculate precision: (RMS of distance of succesive samples.) # This is a little rough as we do not compensate headmovements in this test. @@ -457,9 +481,89 @@ def calc_acc_prec_errlines( precision = np.sqrt( np.mean(np.rad2deg(np.arccos(succesive_distances.clip(-1.0, 1.0))) ** 2) ) - precision_result = Calculation_Result(precision, num_used, num_total) + precision_result = CalculationResult(precision, num_used, num_total) + + return AccuracyPrecisionResult( + accuracy_result, precision_result, error_lines, correlation_result + ) + + @staticmethod + def correlate_and_coordinate_transform( + gaze_pos, ref_pos, intrinsics + ) -> CorrelatedAndCoordinateTransformedResult: + # reuse closest_matches_monocular to correlate one label to each prediction + # correlated['ref']: prediction, correlated['pupil']: label location + # NOTE the switch of the ref and pupil keys! This effects mostly hmd data. + correlated = closest_matches_monocular(gaze_pos, ref_pos) + # [[pred.x, pred.y, label.x, label.y], ...], shape: n x 4 + if not correlated: + raise CorrelationError("No correlation possible") - return accuracy_result, precision_result, error_lines + try: + return Accuracy_Visualizer._coordinate_transform_ref_in_norm_space( + correlated, intrinsics + ) + except KeyError as err: + if "norm_pos" in err.args: + return Accuracy_Visualizer._coordinate_transform_ref_in_camera_space( + correlated, intrinsics + ) + else: + raise + + @staticmethod + def _coordinate_transform_ref_in_norm_space( + correlated, intrinsics + ) -> CorrelatedAndCoordinateTransformedResult: + width, height = intrinsics.resolution + locations_norm = np.array( + [(*e["ref"]["norm_pos"], *e["pupil"]["norm_pos"]) for e in correlated] + ) + locations_image = locations_norm.copy() # n x 4 + locations_image[:, ::2] *= width + locations_image[:, 1::2] = (1.0 - locations_image[:, 1::2]) * height + locations_image.shape = -1, 2 + locations_norm.shape = -1, 2 + locations_camera = intrinsics.unprojectPoints(locations_image, normalize=True) + return CorrelatedAndCoordinateTransformedResult( + locations_norm, locations_image, locations_camera + ) + + @staticmethod + def _coordinate_transform_ref_in_camera_space( + correlated, intrinsics + ) -> CorrelatedAndCoordinateTransformedResult: + width, height = intrinsics.resolution + locations_mixed = np.array( + # NOTE: This looks incorrect, but is actually correct. The switch comes from + # using closest_matches_monocular() above with switched arguments. + [(*e["ref"]["norm_pos"], *e["pupil"]["mm_pos"]) for e in correlated] + ) # n x 5 + pupil_norm = locations_mixed[:, 0:2] # n x 2 + pupil_image = pupil_norm.copy() + pupil_image[:, 0] *= width + pupil_image[:, 1] = (1.0 - pupil_image[:, 1]) * height + pupil_camera = intrinsics.unprojectPoints(pupil_image, normalize=True) # n x 3 + + ref_camera = locations_mixed[:, 2:5] # n x 3 + ref_camera /= np.linalg.norm(ref_camera, axis=1, keepdims=True) + ref_image = intrinsics.projectPoints(ref_camera) # n x 2 + ref_norm = ref_image.copy() + ref_norm[:, 0] /= width + ref_norm[:, 1] = 1.0 - (ref_norm[:, 1] / height) + + locations_norm = np.hstack([pupil_norm, ref_norm]) # n x 4 + locations_norm.shape = -1, 2 + + locations_image = np.hstack([pupil_image, ref_image]) # n x 4 + locations_image.shape = -1, 2 + + locations_camera = np.hstack([pupil_camera, ref_camera]) # n x 6 + locations_camera.shape = -1, 3 + + return CorrelatedAndCoordinateTransformedResult( + locations_norm, locations_image, locations_camera + ) def gl_display(self): if self.vis_mapping_error and self.error_lines is not None: diff --git a/pupil_src/shared_modules/audio_playback.py b/pupil_src/shared_modules/audio_playback.py index 22cd749b5f..6a394ade7f 100644 --- a/pupil_src/shared_modules/audio_playback.py +++ b/pupil_src/shared_modules/audio_playback.py @@ -123,17 +123,10 @@ def _setup_input_audio_part(self, part_idx): self.audio_paused = False self.audio.stream.seek(0) - first_frame = next(self.audio_frame_iterator) - self.audio_pts_rate = first_frame.samples - self.audio_start_pts = first_frame.pts - - logger.debug( - "audio_pts_rate = {} start_pts = {}".format( - self.audio_pts_rate, self.audio_start_pts - ) - ) - self.check_ts_consistency(reference_frame=first_frame) - self.seek_to_audio_frame(0) + if self.should_check_ts_consistency: + first_frame = next(self.audio_frame_iterator) + self.check_ts_consistency(reference_frame=first_frame) + self.seek_to_audio_frame(0) logger.debug( "Audio file format {} chans {} rate {} framesize {}".format( @@ -166,6 +159,12 @@ def _setup_output_audio(self): except ValueError: self.pa_stream = None + except OSError: + self.pa_stream = None + import traceback + + logger.warning("Audio found, but playback failed (#2103)") + logger.debug(traceback.format_exc()) def _setup_audio_vis(self): self.audio_timeline = None @@ -254,13 +253,11 @@ def get_audio_frame_iterator(self): yield frame def audio_idx_to_pts(self, idx): - return idx * self.audio_pts_rate + return self.audio.pts[idx] def seek_to_audio_frame(self, seek_pos): try: - self.audio.stream.seek( - self.audio_start_pts + self.audio_idx_to_pts(seek_pos) - ) + self.audio.stream.seek(self.audio_idx_to_pts(seek_pos)) except av.AVError: raise FileSeekError() else: @@ -321,7 +318,7 @@ def update_audio_viz(self): self.audio_viz_data, finished = self.audio_viz_trans.get_data( log_scale=self.log_scale ) - if not finished: + if not finished and self.audio_timeline: self.audio_timeline.refresh() def setup_pyaudio_output_if_necessary(self): diff --git a/pupil_src/shared_modules/audio_utils.py b/pupil_src/shared_modules/audio_utils.py index 86df37cb22..f5be657743 100644 --- a/pupil_src/shared_modules/audio_utils.py +++ b/pupil_src/shared_modules/audio_utils.py @@ -25,7 +25,7 @@ class NoAudioLoadedError(Exception): LoadedAudio = collections.namedtuple( - "LoadedAudio", ["container", "stream", "timestamps"] + "LoadedAudio", ["container", "stream", "timestamps", "pts"] ) @@ -71,23 +71,26 @@ def _load_audio_single(file_path, return_pts_based_timestamps=False): except IOError: return None + start = timestamps[0] + packet_pts = np.array( + [p.pts for p in container.demux(stream) if p is not None and p.pts is not None], + ) + if return_pts_based_timestamps: - start = timestamps[0] - timestamps = np.fromiter( - ( - float(start + p.pts * p.time_base) - for p in container.demux(audio=0) - if p is not None and p.pts is not None and p.time_base is not None - ), - dtype=float, - ) - try: - container.seek(0) - except av.AVError as err: - logger.debug(f"{err}") - return None + timestamps = start + packet_pts * stream.time_base + + # pts seeking requires primitive Python integers and does not accept numpy int types; + # `.tolist()` converts numpy integers to primitive Python integers; do conversion after + # `packet_pts * stream.time_base` to leverage numpy element-wise function application + packet_pts = packet_pts.tolist() + + try: + container.seek(0) + except av.AVError as err: + logger.debug(f"{err}") + return None - return LoadedAudio(container, stream, timestamps) + return LoadedAudio(container, stream, timestamps, packet_pts) class Audio_Viz_Transform: diff --git a/pupil_src/shared_modules/blink_detection.py b/pupil_src/shared_modules/blink_detection.py index 35826eac94..5abddeff4f 100644 --- a/pupil_src/shared_modules/blink_detection.py +++ b/pupil_src/shared_modules/blink_detection.py @@ -28,6 +28,7 @@ import player_methods as pm from observable import Observable from plugin import Plugin +from pupil_recording import PupilRecording, RecordingInfo logger = logging.getLogger(__name__) @@ -185,6 +186,19 @@ def get_init_dict(self): class Offline_Blink_Detection(Observable, Blink_Detection): + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + if g_pool.app == "player": + recording = PupilRecording(rec_dir=g_pool.rec_dir) + meta_info = recording.meta_info + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE + ): + # Disable blink detector in Player if Pupil Invisible recording + return False + return super().is_available_within_context(g_pool) + def __init__( self, g_pool, diff --git a/pupil_src/shared_modules/fixation_detector.py b/pupil_src/shared_modules/fixation_detector.py index 115c96b86c..4853f89915 100644 --- a/pupil_src/shared_modules/fixation_detector.py +++ b/pupil_src/shared_modules/fixation_detector.py @@ -50,6 +50,7 @@ import player_methods as pm from methods import denormalize from plugin import Plugin +from pupil_recording import PupilRecording, RecordingInfo logger = logging.getLogger(__name__) @@ -269,6 +270,19 @@ class Offline_Fixation_Detector(Observable, Fixation_Detector_Base): fixations will have their method field set to "gaze". """ + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + if g_pool.app == "player": + recording = PupilRecording(rec_dir=g_pool.rec_dir) + meta_info = recording.meta_info + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE + ): + # Disable fixation detector in Player if Pupil Invisible recording + return False + return super().is_available_within_context(g_pool) + def __init__( self, g_pool, diff --git a/pupil_src/shared_modules/gaze_producer/gaze_from_offline_calibration.py b/pupil_src/shared_modules/gaze_producer/gaze_from_offline_calibration.py index 7a128dd58e..22ec99aa06 100644 --- a/pupil_src/shared_modules/gaze_producer/gaze_from_offline_calibration.py +++ b/pupil_src/shared_modules/gaze_producer/gaze_from_offline_calibration.py @@ -13,7 +13,7 @@ from gaze_producer import ui as plugin_ui from gaze_producer.gaze_producer_base import GazeProducerBase from plugin_timeline import PluginTimeline -from pupil_recording import PupilRecording +from pupil_recording import PupilRecording, RecordingInfo from tasklib.manager import UniqueTaskManager @@ -23,6 +23,19 @@ class GazeFromOfflineCalibration(GazeProducerBase): icon_chr = chr(0xEC14) icon_font = "pupil_icons" + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + if g_pool.app == "player": + recording = PupilRecording(rec_dir=g_pool.rec_dir) + meta_info = recording.meta_info + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE + ): + # Disable post-hoc gaze calibration in Player if Pupil Invisible recording + return False + return super().is_available_within_context(g_pool) + @classmethod def plugin_menu_label(cls) -> str: return "Post-Hoc Gaze Calibration" diff --git a/pupil_src/shared_modules/gaze_producer/gaze_from_recording.py b/pupil_src/shared_modules/gaze_producer/gaze_from_recording.py index 01d9bd9e26..84fda54ad3 100644 --- a/pupil_src/shared_modules/gaze_producer/gaze_from_recording.py +++ b/pupil_src/shared_modules/gaze_producer/gaze_from_recording.py @@ -13,9 +13,23 @@ import file_methods as fm import player_methods as pm from gaze_producer.gaze_producer_base import GazeProducerBase +from pupil_recording import PupilRecording, RecordingInfo class GazeFromRecording(GazeProducerBase): + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + if g_pool.app == "player": + recording = PupilRecording(rec_dir=g_pool.rec_dir) + meta_info = recording.meta_info + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_MOBILE + ): + # Disable gaze from recording in Player if Pupil Mobile recording + return False + return super().is_available_within_context(g_pool) + @classmethod def plugin_menu_label(cls) -> str: return "Gaze Data From Recording" diff --git a/pupil_src/shared_modules/gaze_producer/gaze_producer_base.py b/pupil_src/shared_modules/gaze_producer/gaze_producer_base.py index bc0f41c8a1..fc0b342a1e 100644 --- a/pupil_src/shared_modules/gaze_producer/gaze_producer_base.py +++ b/pupil_src/shared_modules/gaze_producer/gaze_producer_base.py @@ -56,6 +56,12 @@ def _create_plugin_selector(self): for p in self.g_pool.plugin_by_name.values() if issubclass(p, GazeProducerBase) ] + # Skip gaze producers that are not available within g_pool context + gaze_producer_plugins = [ + p + for p in gaze_producer_plugins + if p.is_available_within_context(self.g_pool) + ] gaze_producer_plugins.sort(key=lambda p: p.gaze_data_source_selection_label()) gaze_producer_plugins.sort(key=lambda p: p.gaze_data_source_selection_order()) gaze_producer_labels = [ diff --git a/pupil_src/shared_modules/gaze_producer/worker/validate_gaze.py b/pupil_src/shared_modules/gaze_producer/worker/validate_gaze.py index 1bb693bafe..6115e12562 100644 --- a/pupil_src/shared_modules/gaze_producer/worker/validate_gaze.py +++ b/pupil_src/shared_modules/gaze_producer/worker/validate_gaze.py @@ -72,7 +72,7 @@ def validate( for ref in refs_in_validation_range ] - accuracy_result, precision_result, _ = Accuracy_Visualizer.calc_acc_prec_errlines( + result = Accuracy_Visualizer.calc_acc_prec_errlines( g_pool=g_pool, gazer_class=gazer_class, gazer_params=gazer_params, @@ -81,7 +81,7 @@ def validate( intrinsics=g_pool.capture.intrinsics, outlier_threshold=gaze_mapper.validation_outlier_threshold_deg, ) - return accuracy_result, precision_result + return result.accuracy, result.precision def _create_ref_dict(ref, frame_size): diff --git a/pupil_src/shared_modules/plugin.py b/pupil_src/shared_modules/plugin.py index 548e18db64..0c9dee1505 100644 --- a/pupil_src/shared_modules/plugin.py +++ b/pupil_src/shared_modules/plugin.py @@ -253,6 +253,13 @@ def pretty_class_name(self): def parse_pretty_class_name(cls) -> str: return cls.__name__.replace("_", " ") + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + """ + Returns `True` if the plugin class is available within the `g_pool` context. + """ + return True + def add_menu(self): """ This fn is called when the plugin ui is initialized. Do not change! @@ -364,6 +371,14 @@ def __init__(self, g_pool, plugin_initializers): expanded_initializers.sort(key=lambda data: data[0].order) + # skip plugins that are not available within g_pool context + # not removing them here will break the uniqueness logic bellow + expanded_initializers = [ + (plugin, name, args) + for (plugin, name, args) in expanded_initializers + if plugin.is_available_within_context(self.g_pool) + ] + # only add plugins that won't be replaced by newer plugins for i, (plugin, name, args) in enumerate(expanded_initializers): for new_plugin, new_name, _ in expanded_initializers[i + 1 :]: @@ -393,6 +408,14 @@ def add(self, new_plugin_cls, args={}): """ add a plugin instance to the list. """ + + # Check if the plugin class is supported within the current g_pool context + if not new_plugin_cls.is_available_within_context(self.g_pool): + logger.debug( + f"Plugin {new_plugin_cls.__name__} not available; skip adding it to plugin list." + ) + return + self._find_and_remove_duplicates(new_plugin_cls) plugin_instance = new_plugin_cls(self.g_pool, **args) diff --git a/pupil_src/shared_modules/plugin_manager.py b/pupil_src/shared_modules/plugin_manager.py index edfd07434e..b9626aff62 100644 --- a/pupil_src/shared_modules/plugin_manager.py +++ b/pupil_src/shared_modules/plugin_manager.py @@ -8,6 +8,7 @@ See COPYING and COPYING.LESSER for license details. ---------------------------------------------------------------------------~(*) """ +import logging from plugin import System_Plugin_Base from pyglui import ui @@ -16,6 +17,9 @@ from video_capture import Base_Manager, Base_Source +logger = logging.getLogger(__name__) + + class Plugin_Manager(System_Plugin_Base): icon_chr = chr(0xE8C0) icon_font = "pupil_icons" @@ -29,13 +33,23 @@ def __init__(self, g_pool): CalibrationChoreographyPlugin, GazerBase, ) - self.user_plugins = [ - p - for p in sorted( - g_pool.plugin_by_name.values(), key=lambda p: p.__name__.lower() - ) - if not issubclass(p, non_user_plugins) - ] + all_available_plugins = sorted( + g_pool.plugin_by_name.values(), key=lambda p: p.__name__.lower() + ) + + available_and_supported_user_plugins = [] + + for plugin in all_available_plugins: + if issubclass(plugin, non_user_plugins): + continue + if not plugin.is_available_within_context(g_pool): + logger.debug( + f"Plugin {plugin.__name__} not available; skip adding it to plugin list." + ) + continue + available_and_supported_user_plugins.append(plugin) + + self.user_plugins = available_and_supported_user_plugins def init_ui(self): self.add_menu() diff --git a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py index 3930f4df8b..34ad04a596 100644 --- a/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py +++ b/pupil_src/shared_modules/pupil_detector_plugins/pye3d_plugin.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) version_installed = getattr(pye3d, "__version__", "0.0.1") -version_supported = "0.0.5" +version_supported = "0.0.6" if version_installed != version_supported: logger.info( diff --git a/pupil_src/shared_modules/pupil_producers.py b/pupil_src/shared_modules/pupil_producers.py index 2e1557ed12..afb7b83f42 100644 --- a/pupil_src/shared_modules/pupil_producers.py +++ b/pupil_src/shared_modules/pupil_producers.py @@ -27,6 +27,7 @@ import zmq_tools from observable import Observable from plugin import System_Plugin_Base +from pupil_recording import PupilRecording, RecordingInfo from pyglui import ui from pyglui.pyfontstash import fontstash as fs from video_capture.utils import VideoSet @@ -80,6 +81,12 @@ def init_ui(self): for p in self.g_pool.plugin_by_name.values() if issubclass(p, Pupil_Producer_Base) ] + # Skip pupil producers that are not available within g_pool context + pupil_producer_plugins = [ + p + for p in pupil_producer_plugins + if p.is_available_within_context(self.g_pool) + ] pupil_producer_plugins.sort(key=lambda p: p.pupil_data_source_selection_label()) pupil_producer_plugins.sort(key=lambda p: p.pupil_data_source_selection_order()) pupil_producer_labels = [ @@ -285,6 +292,19 @@ def _legend_font(self, scale): class Pupil_From_Recording(Pupil_Producer_Base): + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + if g_pool.app == "player": + recording = PupilRecording(rec_dir=g_pool.rec_dir) + meta_info = recording.meta_info + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_MOBILE + ): + # Disable pupil from recording in Player if Pupil Mobile recording + return False + return super().is_available_within_context(g_pool) + @classmethod def plugin_menu_label(cls) -> str: return "Pupil Data From Recording" @@ -312,6 +332,19 @@ class Offline_Pupil_Detection(Pupil_Producer_Base): session_data_version = 4 session_data_name = "offline_pupil" + @classmethod + def is_available_within_context(cls, g_pool) -> bool: + if g_pool.app == "player": + recording = PupilRecording(rec_dir=g_pool.rec_dir) + meta_info = recording.meta_info + if ( + meta_info.recording_software_name + == RecordingInfo.RECORDING_SOFTWARE_NAME_PUPIL_INVISIBLE + ): + # Disable post-hoc pupil detector in Player if Pupil Invisible recording + return False + return super().is_available_within_context(g_pool) + @classmethod def plugin_menu_label(cls) -> str: return "Post-Hoc Pupil Detection" diff --git a/pupil_src/shared_modules/pupil_recording/__init__.py b/pupil_src/shared_modules/pupil_recording/__init__.py index 74e0b7af53..032d1aa0fd 100644 --- a/pupil_src/shared_modules/pupil_recording/__init__.py +++ b/pupil_src/shared_modules/pupil_recording/__init__.py @@ -8,6 +8,6 @@ See COPYING and COPYING.LESSER for license details. ---------------------------------------------------------------------------~(*) """ -from .info import RecordingInfoFile +from .info import RecordingInfoFile, RecordingInfo from .recording import PupilRecording from .recording_utils import InvalidRecordingException, assert_valid_recording_type diff --git a/pupil_src/shared_modules/pupil_recording/info/__init__.py b/pupil_src/shared_modules/pupil_recording/info/__init__.py index a53b7d8a49..3b74bf5637 100644 --- a/pupil_src/shared_modules/pupil_recording/info/__init__.py +++ b/pupil_src/shared_modules/pupil_recording/info/__init__.py @@ -8,7 +8,7 @@ See COPYING and COPYING.LESSER for license details. ---------------------------------------------------------------------------~(*) """ -from .recording_info import RecordingInfoFile +from .recording_info import RecordingInfoFile, RecordingInfo from .recording_info_2_0 import _RecordingInfoFile_2_0 from .recording_info_2_1 import _RecordingInfoFile_2_1 from .recording_info_2_2 import _RecordingInfoFile_2_2 diff --git a/requirements.txt b/requirements.txt index 60bcda1316..12ea29d8af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ opencv-python==3.* ; platform_system == "Windows" ### pupil-apriltags==1.0.4 pupil-detectors==2.0.* -pye3d==0.0.5 +pye3d==0.0.6 # pupil-labs/PyAV 0.4.6 av @ git+https://github.com/pupil-labs/PyAV@v0.4.6 ; platform_system != "Windows"