diff --git a/pupil_src/shared_modules/file_methods.py b/pupil_src/shared_modules/file_methods.py index 3afcf892c8..e11a4bca45 100644 --- a/pupil_src/shared_modules/file_methods.py +++ b/pupil_src/shared_modules/file_methods.py @@ -42,7 +42,9 @@ def __init__(self, file_path, *args, **kwargs): super().__init__(*args, **kwargs) self.file_path = os.path.expanduser(file_path) try: - self.update(**load_object(self.file_path, allow_legacy=False)) + if os.path.getsize(file_path) > 0: + # Only try to load object if file is not empty + self.update(**load_object(self.file_path, allow_legacy=False)) except IOError: logger.debug( f"Session settings file '{self.file_path}' not found." diff --git a/pupil_src/shared_modules/surface_tracker/background_tasks.py b/pupil_src/shared_modules/surface_tracker/background_tasks.py index 6464a3ab32..e1bd860432 100644 --- a/pupil_src/shared_modules/surface_tracker/background_tasks.py +++ b/pupil_src/shared_modules/surface_tracker/background_tasks.py @@ -19,6 +19,10 @@ import background_helper import player_methods +import file_methods + +from .surface_marker import Surface_Marker + logger = logging.getLogger(__name__) @@ -221,6 +225,7 @@ def get_export_proxy( gaze_positions, fixations, camera_model, + marker_cache_path, mp_context, ): exporter = Exporter( @@ -231,6 +236,7 @@ def get_export_proxy( gaze_positions, fixations, camera_model, + marker_cache_path, ) proxy = background_helper.IPC_Logging_Task_Proxy( "Offline Surface Tracker Exporter", @@ -250,6 +256,7 @@ def __init__( gaze_positions, fixations, camera_model, + marker_cache_path, ): self.export_range = export_range self.metrics_dir = os.path.join(export_dir, "surfaces") @@ -260,6 +267,7 @@ def __init__( self.camera_model = camera_model self.gaze_on_surfaces = None self.fixations_on_surfaces = None + self.marker_cache_path = marker_cache_path def save_surface_statisics_to_file(self): logger.info("exporting metrics to {}".format(self.metrics_dir)) @@ -298,6 +306,17 @@ def save_surface_statisics_to_file(self): "Saved surface gaze and fixation data for '{}'".format(surface.name) ) + # Cleanup surface related data to release memory + self.surfaces = None + self.fixations = None + self.gaze_positions = None + self.gaze_on_surfaces = None + self.fixations_on_surfaces = None + + # Perform marker export *after* surface data is released + # to avoid holding everything in memory all at once. + self._export_marker_detections() + logger.info("Done exporting reference surface data.") return @@ -338,6 +357,46 @@ def _map_gaze_and_fixations(self): return gaze_on_surface, fixations_on_surface + def _export_marker_detections(self): + + # Load the temporary marker cache created by the offline surface tracker + marker_cache = file_methods.Persistent_Dict(self.marker_cache_path) + marker_cache = marker_cache["marker_cache"] + + try: + file_path = os.path.join(self.metrics_dir, "marker_detections.csv") + with open(file_path, "w", encoding="utf-8", newline="") as csv_file: + csv_writer = csv.writer(csv_file, delimiter=",") + csv_writer.writerow( + ( + "world_index", + "marker_uid", + "corner_0_x", + "corner_0_y", + "corner_1_x", + "corner_1_y", + "corner_2_x", + "corner_2_y", + "corner_3_x", + "corner_3_y", + ) + ) + for idx, serialized_markers in enumerate(marker_cache): + for m in map(Surface_Marker.deserialize, serialized_markers): + flat_corners = [x for c in m.verts_px for x in c[0]] + assert len(flat_corners) == 8 # sanity check + csv_writer.writerow( + ( + idx, + m.uid, + *flat_corners, + ) + ) + finally: + # Delete the temporary marker cache created by the offline surface tracker + os.remove(self.marker_cache_path) + self.marker_cache_path = None + def _export_surface_visibility(self): with open( os.path.join(self.metrics_dir, "surface_visibility.csv"), diff --git a/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py b/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py index 86cdbc4550..9094085155 100644 --- a/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py +++ b/pupil_src/shared_modules/surface_tracker/surface_tracker_offline.py @@ -14,6 +14,7 @@ import multiprocessing import os import platform +import tempfile import time import typing as T @@ -559,6 +560,16 @@ def on_notify(self, notification): break elif notification["subject"] == "should_export": + # Create new marker cache temporary file + # Backgroud exporter is responsible of removing the temporary file when finished + file_handle, marker_cache_path = tempfile.mkstemp() + os.close(file_handle) # https://bugs.python.org/issue42830 + + # Save marker cache into the new temporary file + temp_marker_cache = file_methods.Persistent_Dict(marker_cache_path) + temp_marker_cache["marker_cache"] = self.marker_cache + temp_marker_cache.save() + proxy = background_tasks.get_export_proxy( notification["export_dir"], notification["range"], @@ -567,6 +578,7 @@ def on_notify(self, notification): self.g_pool.gaze_positions, self.g_pool.fixations, self.camera_model, + marker_cache_path, mp_context, ) self.export_proxies.add(proxy)