def setUp(self): ldfh = LegacyDataFileSystemHelper(head_path=self.__class__.HEAD_PATH, ) self.latitude_series = ldfh.get_field_from_npz_file( session_dir_name=self.__class__.SESSION_DIR, filename=LegacyDataFileSystemHelper.TAG_NPZ_FILE, fieldname=LegacyDataFileSystemHelper.TAG_LATITUDE_FIELD, ) self.longitude_series = ldfh.get_field_from_npz_file( session_dir_name=self.__class__.SESSION_DIR, filename=LegacyDataFileSystemHelper.TAG_NPZ_FILE, fieldname=LegacyDataFileSystemHelper.TAG_LONGITUDE_FIELD, ) self.time_series = ldfh.get_field_from_npz_file( session_dir_name=self.__class__.SESSION_DIR, filename=LegacyDataFileSystemHelper.TAG_NPZ_FILE, fieldname=LegacyDataFileSystemHelper.TAG_TIME_FIELD, ) self.lat_long = self.latitude_series[0], self.longitude_series[0]
def setUp(self): ldfh = LegacyDataFileSystemHelper(self.__class__.HEAD_PATH) self.latitude_series = ldfh.get_field_from_npz_file( self.__class__.SESSION_DIR_NAME, LegacyDataFileSystemHelper.TAG_NPZ_FILE, LegacyDataFileSystemHelper.TAG_LATITUDE_FIELD, ) self.longitude_series = ldfh.get_field_from_npz_file( self.__class__.SESSION_DIR_NAME, LegacyDataFileSystemHelper.TAG_NPZ_FILE, LegacyDataFileSystemHelper.TAG_LONGITUDE_FIELD, ) self.time_series = ldfh.get_field_from_npz_file( self.__class__.SESSION_DIR_NAME, LegacyDataFileSystemHelper.TAG_NPZ_FILE, LegacyDataFileSystemHelper.TAG_TIME_FIELD, ) return self
def __init__( self, session_dir: str, input_data_head_path=Path('/Volumes/WD'), results_head_path=Path('.') / 'data/calibration_data', cache_dir_path=Path('/Volumes/WD/image_cache'), num_points=1, ): self._session_dir = session_dir self._input_data_head_path = input_data_head_path self._results_head_path = results_head_path self._cache_dir_path = cache_dir_path self._num_points = num_points # Helpers self._ldfh = LDFH(self._input_data_head_path) self._calibration_data_filer = CalibrationDataFiler( self._results_head_path) self.maxima_text_color = 'red' self.minima_text_color = 'green' self.fontsize = 8 # Lazy initializers self._pan_motor_read_data_series = None self._base_time_series = None self._motor_maxima = None self._motor_minima = None self._motor_maxima_from_series = None self._motor_minima_from_series = None self._motor_maxima_from_archive = None self._motor_minima_from_archive = None self._archived_results = None self._selected_frames_from_archive = None # State self._base_time = None self._motor_idx = None self._min_or_max = None self._alignment_results = []
def __init__(self, session_dir: str, input_data_head_path=Path('/Volumes/WD'), results_head_path=Path('.') / 'data/calibration_data', cache_dir_path=Path('/Volumes/WD/image_cache'), ): self._session_dir = session_dir self._input_data_head_path = input_data_head_path self._results_head_path = results_head_path self._cache_dir_path = cache_dir_path # helpers self._ldfh = LDFH(self._input_data_head_path) self._calibration_data_filer = CalibrationDataFiler(self._results_head_path) # state self._alignment_data = [] # lazy inits self._latitude_series = None self._longitude_series = None self._time_series = None
def __init__( self, session_dir: str, input_data_head_path=Path('/Volumes/WD'), results_head_path=Path('.') / 'data/calibration_data', cache_dir_path=Path('/Volumes/WD/image_cache'), ): self._session_dir = session_dir self._input_data_head_path = input_data_head_path self._results_head_path = results_head_path self._cache_dir_path = cache_dir_path # settings self._anotation_color = 'green' self._anotation_fontsize = '8' self._ldfh = LDFH(self._input_data_head_path) self._calibration_data_filer = CalibrationDataFiler( self._results_head_path) # lazy inits self._fov_time_series = None self._fov_data_series = None self._unique_fovs = None self._transition_indexes = None self._transition_levels = None self._unique_transitions = None # state self._alignment_results = {} self._current_transition = None self._alignment_stats_delays = [] self._alignment_stats_durations = [] self._alignment_stats_zoom_out_delays = [] self._alignment_stats_zoom_out_durations = [] self._alignment_stats_zoom_in_delays = [] self._alignment_stats_zoom_in_durations = []
def setUp(self): ldfh = LegacyDataFileSystemHelper(self.__class__.INPUT_DATA_HEAD_PATH) image_from_video_grabber = ImageFromVideoGrabber( ldfh.get_video_path(self.__class__.SESSION_DIR), cache_dir_path=self.__class__.IMAGE_CACHE_DIR_PATH, ) middle_frame = int(image_from_video_grabber.get_frame_count() / 2) images_from_video = image_from_video_grabber.get_images_around_frame_number( middle_frame, 100, 100) video_scrub_picker = ScrubPicker(images_from_video=images_from_video, ) latitude_series = ldfh.get_field_from_npz_file( session_dir_name=self.__class__.SESSION_DIR, filename=LegacyDataFileSystemHelper.TAG_NPZ_FILE, fieldname=LegacyDataFileSystemHelper.TAG_LATITUDE_FIELD, ) longitude_series = ldfh.get_field_from_npz_file( session_dir_name=self.__class__.SESSION_DIR, filename=LegacyDataFileSystemHelper.TAG_NPZ_FILE, fieldname=LegacyDataFileSystemHelper.TAG_LONGITUDE_FIELD, ) time_series = ldfh.get_field_from_npz_file( session_dir_name=self.__class__.SESSION_DIR, filename=LegacyDataFileSystemHelper.TAG_NPZ_FILE, fieldname=LegacyDataFileSystemHelper.TAG_TIME_FIELD, ) geo_map_scrubber = GeoMapScrubber( latitude_series=latitude_series, longitude_series=longitude_series, time_series=time_series, width=700, ) self.video_plus_map_scrub_picker = VideoPlusMapScrubPicker( video_scrub_picker=video_scrub_picker, geo_map_scrubber=geo_map_scrubber, ) return self
def setUp(self): self.ldfh = LDFH(self.__class__.HEAD_PATH)
class TestInputDataStructure(unittest.TestCase): '''Verifies that the legacy data files are in the expected directory structure and contain the expected data. Currently the head of the file heirarchy is the SESSION_DB folder''' # Change this to the path containing the SESSION_DB folder HEAD_PATH = Path('/Volumes/WD') def setUp(self): self.ldfh = LDFH(self.__class__.HEAD_PATH) def test_top_level_dir_exists(self): self.assertTrue(self.ldfh.get_top_level_dir_path().exists()) def test_at_least_one_session_dir_exists(self): try: num_session_dirs = sum(1 for _ in self.ldfh.get_session_dirs()) self.assertGreater(num_session_dirs, 0) except FileNotFoundError: self.fail('Top level dir not found') def test_all_children_of_top_level_dir_are_dirs(self): try: for session_dir in self.ldfh.get_session_dirs(): self.assertTrue(session_dir.is_dir()) except FileNotFoundError: self.fail('Top level dir not found') def test_directory_structure_correct(self): for session_dir_path in self.ldfh.get_session_dirs(): self.verify_directory_and_sub_directories( LDFH.DIRECTORY_STRUCTURE, session_dir_path, ) def test_no_extra_files_in_npz_dirs(self): for session_dir_path in self.ldfh.get_session_dirs(): npz_dir = session_dir_path / LDFH.NPZ_DIR for p_path in npz_dir.iterdir(): if not p_path.name in LDFH.DIRECTORY_STRUCTURE[LDFH.NPZ_DIR]: if '.DS_Store' not in p_path.name: self.fail(f'Extra NPZ file: {p_path}') def test_npz_fields_exist_with_no_extras(self): for session_dir_path in self.ldfh.get_session_dirs(): for npz_file, expected_npz_fields in LDFH.NPZ_FIELDS.items(): npz_file_path = session_dir_path / LDFH.NPZ_DIR / npz_file actual_npz_fields = np.load(npz_file_path).files for expected_field in expected_npz_fields: if expected_field not in actual_npz_fields: self.fail(f'No {expected_field} in {npz_file_path}') for actual_field in actual_npz_fields: if actual_field not in expected_npz_fields: self.fail( f'Extra field: {actual_field} in {npz_file_path}') def test_npz_fields_have_the_same_length(self): for npz_file_path in self.ldfh.get_all_npz_file_paths(): np_data = np.load(npz_file_path) actual_npz_fields = np_data.files name_of_first_field = actual_npz_fields[0] length_of_first_field = np_data[name_of_first_field].size self.assertGreater( length_of_first_field, 0, msg=(f'{name_of_first_field} in {npz_file_path} ' f'is too short ({length_of_first_field}).')) for actual_field in actual_npz_fields: field_len = np_data[actual_field].size self.assertEqual( field_len, length_of_first_field, msg= (f'{actual_field} ({field_len}) ' f'not same length as {name_of_first_field} ({length_of_first_field}) ' f'in {npz_file_path}')) def test_non_implemented_fields_are_full_of_nones(self): for npz_file_path in self.ldfh.get_all_npz_file_paths(): np_data = np.load(npz_file_path) for field in np_data.files: if field in LDFH.UNIMPLEMENTED_FIELDS: field_data = np_data[field] if field_data.size == 0: self.fail( f'Apparent unimplemented field: {field} in {npz_file_path} is empty' ) if (field_data != None).all(): # pylint: disable=C0121 self.fail( f'Apparent unimplemented field: {field} in {npz_file_path} not full of Nones' ) def test_implemented_fields_have_no_nones(self): for npz_file_path in self.ldfh.get_all_npz_file_paths(): np_data = np.load(npz_file_path) for field in np_data.files: if field not in LDFH.UNIMPLEMENTED_FIELDS: field_data = np_data[field] if field_data.size == 0: self.fail( f'Apparent implemented field: {field} in {npz_file_path} is empty' ) if (field_data == None).any(): # pylint: disable=C0121 self.fail( f'Apparent implemented field: {field} in {npz_file_path} has a None' ) def test_video_file_can_be_opened_with_cv2(self): for video_path in self.ldfh.get_video_paths(): if not cv2.VideoCapture(str(video_path.resolve())).isOpened(): self.fail(f'Could not open {video_path} with cv2.VideoCapture') def test_time_fields_duration_same_as_video_duration(self): for session_dir in self.ldfh.get_session_dirs(): video_path = self.ldfh.get_video_path(session_dir) assert video_path is not None,\ (f'No video found in {session_dir.name} ' f'cannot run {self.test_time_fields_duration_same_as_video_duration.__name__}') video_duration = self.get_duration_ms_of_video_at_path(video_path) for npz_path in self.ldfh.get_npz_file_paths(session_dir): npz_duration = self.get_duration_in_ms_for_npz_with_a_time_field( npz_path) if npz_duration is None: continue delta = 200 self.assertAlmostEqual( video_duration, npz_duration, delta=delta, msg= (f'{session_dir.name}: video duration: ({video_duration}) ' f'not within {delta} ms of ' f'{npz_path.name} timefield duration ({npz_duration})')) def test_all_time_fields_monotonically_asscend(self): for session_dir in self.ldfh.get_session_dirs(): for field, data in self.ldfh.get_npz_time_fields( session_dir).items(): self.assertTrue( self.field_is_monotonically_increasing(data), msg=(f'{session_dir.name}: ' f'{field} is not monotonically increasing.')) def test_maximum_time_tick_size(self): for session_dir in self.ldfh.get_session_dirs(): for field_name, data in self.ldfh.get_npz_time_fields( session_dir).items(): max_tick_size = np.max(np.diff(data)) # min_tick_size = np.min(np.diff(data)) self.assertLess( max_tick_size, 340, msg=(f'{session_dir.name}: {field_name}: ' f'max tick size to large ({max_tick_size})')) def verify_directory_and_sub_directories(self, dir_structure, path: Path): if isinstance(dir_structure, dict): for child in dir_structure.keys(): self.verify_wild_path_exists(path, child) for wild in path.glob(child): self.verify_directory_and_sub_directories( dir_structure[child], path / wild) elif isinstance(dir_structure, list): for filename in dir_structure: self.verify_wild_path_exists(path, filename) def verify_wild_path_exists(self, path, dir_or_file_with_wild_chars): wilds = path.glob(dir_or_file_with_wild_chars) num_wilds = sum(1 for _ in wilds) if num_wilds < 1: self.fail(f'{path / dir_or_file_with_wild_chars} not found.') if num_wilds > 1 and dir_or_file_with_wild_chars not in LDFH.MULTIPLE_MATCHING_FILES_ALLOWED: self.fail(f'Too many {path / dir_or_file_with_wild_chars} found.') for wild in wilds: self.assertTrue((path / wild).exists()) def get_duration_ms_of_video_at_path(self, video_path): cap = cv2.VideoCapture(str(video_path.resolve())) if not cap.isOpened(): return 0 cap.set(cv2.CAP_PROP_POS_AVI_RATIO, 1) return cap.get(cv2.CAP_PROP_POS_MSEC) def get_duration_in_ms_for_npz_with_a_time_field(self, npz_path): npz_data = np.load(npz_path) time_fields = [ field for field in npz_data.files if field in LDFH.TIME_FIELDS ] if not time_fields: return None time_data = npz_data[time_fields[0]] return time_data[-1] - time_data[0] def field_is_monotonically_increasing(self, data): return np.all(np.diff(data) >= 0)
def setUp(self): head_path = Path('/Volumes/WD') session_dir = 'Aug_17_Palo_Alto_High_2nd_time_B80_ottofillmore' ldfh = LDFH(head_path) tag_latitude_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.TAG_LATITUDE_FIELD, ) tag_longitude_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.TAG_LONGITUDE_FIELD, ) self.tag_time_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.TAG_TIME_FIELD, ) self.geo_map_scrubber = GeoMapScrubber( latitude_series=tag_latitude_series, longitude_series=tag_longitude_series, time_series=self.tag_time_series, ) tag = Tag( latitude_series=tag_latitude_series, longitude_series=tag_longitude_series, time_series=self.tag_time_series, map_coordinate_transformer=self.geo_map_scrubber. _get_map_coordinate_transformer(), ) self.base_latitude_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.BASE_LATITUDE_FIELD, ) self.base_longitude_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.BASE_LONGITUDE_FIELD, ) reported_base = Base( gps_latitude_series=self.base_latitude_series, gps_longitude_series=self.base_longitude_series, map_coordinate_transformer=self.geo_map_scrubber. _get_map_coordinate_transformer(), ) # Base position we believe to be correct based on where we believe we set up # the base on the field. Point picked on map using GeoMapScrubber # x: 132 y: 612 latitude: 37.38639419602273 longitude: -122.11008779357967 actual_base_latitude_series = np.full(self.base_latitude_series.size, 37.38639419602273) actual_base_longitude_series = np.full(self.base_longitude_series.size, -122.11008779357967) actual_base = Base( gps_latitude_series=actual_base_latitude_series, gps_longitude_series=actual_base_longitude_series, map_coordinate_transformer=self.geo_map_scrubber. _get_map_coordinate_transformer(), ) fov_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.LENS_NPZ_FILE, fieldname=LDFH.LENS_FOV_FIELD, ) fov_time_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.LENS_NPZ_FILE, fieldname=LDFH.LENS_TIME_FIELD, ) self.tag_position_analyzer_with_actual_base = TagPositionInStableFovSegmentsAnalyzer( fov_series=fov_series, fov_time_series=fov_time_series, tag=tag, base=actual_base, ) self.tag_position_analyzer_with_reported_base = TagPositionInStableFovSegmentsAnalyzer( fov_series=fov_series, fov_time_series=fov_time_series, tag=tag, base=reported_base, ) return self
class FovTimebaseAligner: '''Determines with user assistance via scrubber picker statistics for offset in time between fov timebase and video time''' AGGREGATE_RESULTS = 'aggregate_results' UNIQUE_FOVS = 'unique_fovs' UNIQUE_TRANSITIONS = 'unique_transitions' INDIVIDUAL_RESULTS = 'individual_results' FOV_DATA = 'fov_data' TRANSITION_LEVELS = 'transition_levels' TRANSITION_INDEX = 'transition_index' TRANSITION_TIME = 'transition_time' BEGIN = 'begin' END = 'end' ALL = 'all' ZOOM_IN = 'zoom_in' ZOOM_OUT = 'zoom_out' VIDEO_DATA = 'video_data' FRAME_NUM = 'frame_num' TIME_MS = 'time_ms' DURATION = 'duration' FRAMES = 'frames' ALIGNMENT = 'alignment' DELAY_FOV_TO_VIDEO = 'delay_fov_to_video' ALIGNMENT_STATS = 'alignment_stats' DURATION = 'duration' DELAY = 'delay' MIN = 'min' MAX = 'max' MEAN = 'mean' def __init__( self, session_dir: str, input_data_head_path=Path('/Volumes/WD'), results_head_path=Path('.') / 'data/calibration_data', cache_dir_path=Path('/Volumes/WD/image_cache'), ): self._session_dir = session_dir self._input_data_head_path = input_data_head_path self._results_head_path = results_head_path self._cache_dir_path = cache_dir_path # settings self._anotation_color = 'green' self._anotation_fontsize = '8' self._ldfh = LDFH(self._input_data_head_path) self._calibration_data_filer = CalibrationDataFiler( self._results_head_path) # lazy inits self._fov_time_series = None self._fov_data_series = None self._unique_fovs = None self._transition_indexes = None self._transition_levels = None self._unique_transitions = None # state self._alignment_results = {} self._current_transition = None self._alignment_stats_delays = [] self._alignment_stats_durations = [] self._alignment_stats_zoom_out_delays = [] self._alignment_stats_zoom_out_durations = [] self._alignment_stats_zoom_in_delays = [] self._alignment_stats_zoom_in_durations = [] def visualize(self): fig, axis = plt.subplots() # pylint: disable=W0612 axis.plot(self.get_normalized_fov_time(), self.get_fov_data_series()) for key, hsh in self.get_unique_transitions().items(): axis.text( self.get_normalized_fov_time()[hsh[ self.__class__.TRANSITION_INDEX]], hsh[self.__class__.TRANSITION_LEVELS][0], key, color=self._anotation_color, fontsize=self._anotation_fontsize, ) plt.show() def get_fov_time_series(self): if self._fov_time_series is None: self._fov_time_series =\ self._ldfh.get_field_from_npz_file(self._session_dir, LDFH.LENS_NPZ_FILE, LDFH.LENS_TIME_FIELD, ) return self._fov_time_series def get_fov_data_series(self): if self._fov_data_series is None: self._fov_data_series =\ self._ldfh.get_field_from_npz_file(self._session_dir, LDFH.LENS_NPZ_FILE, LDFH.LENS_FOV_FIELD, ) return self._fov_data_series def get_normalized_fov_time(self): return self.get_fov_time_series() - self.get_fov_data_series()[0] def get_unique_fovs(self): if self._unique_fovs is None: self._unique_fovs = np.unique(self.get_fov_data_series()) return self._unique_fovs def get_fov_transition_indexes(self): if self._transition_indexes is None: self._transition_indexes = self.get_fov_data_series().transitions() return self._transition_indexes def get_fov_transition_levels(self): if self._transition_levels is None: self._transition_levels =\ self.get_fov_data_series().levels_around_transitions(self.get_fov_transition_indexes()) return self._transition_levels def get_unique_transitions(self): if self._unique_transitions is None: r = {} for idx, transition_index in enumerate( self.get_fov_transition_indexes()): transition_levels = self.get_fov_transition_levels()[idx] if str(transition_levels) in r: continue r[str(transition_levels)] = { self.__class__.TRANSITION_LEVELS: transition_levels, self.__class__.TRANSITION_INDEX: transition_index, self.__class__.TRANSITION_TIME: self.get_fov_time_series()[transition_index], } self._unique_transitions = r return self._unique_transitions def show_scrub_picker_for_transitions(self): for _, transition in self.get_unique_transitions().items(): self._current_transition = transition self.show_scrub_picker_for_time( transition[self.__class__.TRANSITION_INDEX], transition[self.__class__.TRANSITION_TIME]) def show_scrub_picker_for_time(self, idx, time): ScrubPicker( images_from_video=self.get_images_around_time(time), selector_type=ScrubPicker.SELECT_RANGE, callback=self._scrubber_callback, selected_start_frame_num=None, ).run() def get_images_around_time(self, time): return self.get_ifv_grabber().get_images_around_time_ms(time, before=50, after=80) def get_ifv_grabber(self) -> ImageFromVideoGrabber: return ImageFromVideoGrabber(self.get_video_path(), cache_dir_path=self._cache_dir_path) def get_video_path(self): return self._ldfh.get_video_path(self._session_dir) def _get_aggregate_data(self): r = { self.__class__.UNIQUE_FOVS: self.get_unique_fovs().tolist(), self.__class__.UNIQUE_TRANSITIONS: [ self._get_name_for_transition(transition) for _, transition in self.get_unique_transitions().items() ], self.__class__.ALIGNMENT_STATS: self._get_alignment_stats(), } return r def _get_alignment_stats(self): return { self.__class__.ALL: { self.__class__.DELAY: { self.__class__.MIN: int(np.min(np.array(self._alignment_stats_delays))), self.__class__.MAX: int(np.max(np.array(self._alignment_stats_delays))), self.__class__.MEAN: int(np.mean(np.array(self._alignment_stats_delays))), }, self.__class__.DURATION: { self.__class__.MIN: int(np.min(np.array(self._alignment_stats_durations))), self.__class__.MAX: int(np.max(np.array(self._alignment_stats_durations))), self.__class__.MEAN: int(np.mean(np.array(self._alignment_stats_durations))), }, }, self.__class__.ZOOM_IN: { self.__class__.DELAY: { self.__class__.MIN: int(np.min(np.array( self._alignment_stats_zoom_in_delays))), self.__class__.MAX: int(np.max(np.array( self._alignment_stats_zoom_in_delays))), self.__class__.MEAN: int(np.mean(np.array( self._alignment_stats_zoom_in_delays))), }, self.__class__.DURATION: { self.__class__.MIN: int( np.min( np.array( self._alignment_stats_zoom_in_durations))), self.__class__.MAX: int( np.max( np.array( self._alignment_stats_zoom_in_durations))), self.__class__.MEAN: int( np.mean( np.array( self._alignment_stats_zoom_in_durations))), }, }, self.__class__.ZOOM_OUT: { self.__class__.DELAY: { self.__class__.MIN: int(np.min(np.array( self._alignment_stats_zoom_out_delays))), self.__class__.MAX: int(np.max(np.array( self._alignment_stats_zoom_out_delays))), self.__class__.MEAN: int( np.mean(np.array( self._alignment_stats_zoom_out_delays))), }, self.__class__.DURATION: { self.__class__.MIN: int( np.min( np.array( self._alignment_stats_zoom_out_durations))), self.__class__.MAX: int( np.max( np.array( self._alignment_stats_zoom_out_durations))), self.__class__.MEAN: int( np.mean( np.array( self._alignment_stats_zoom_out_durations))), }, }, } def _get_results_object(self): r = {} r[self.__class__.AGGREGATE_RESULTS] = self._get_aggregate_data() r[self.__class__.INDIVIDUAL_RESULTS] = self._alignment_results return r def save_results(self): self._calibration_data_filer.save_as_yml( self._get_results_object(), self._session_dir, CalibrationDataFiler.FOV_TIMEBASE_ALIGNMENT, ) def _scrubber_callback(self, selection: list): delay_fov_to_video = \ int(selection[0].get_time_ms() - self._current_transition[self.__class__.TRANSITION_TIME]) duration = self._get_duration_ms(selection[0], selection[1]) r = { self.__class__.VIDEO_DATA: { self.__class__.BEGIN: self._get_ifv_info(selection[0]), self.__class__.END: self._get_ifv_info(selection[1]), self.__class__.DURATION: { self.__class__.TIME_MS: duration, self.__class__.FRAMES: self._get_duration_frames(selection[0], selection[1]), }, }, self.__class__.FOV_DATA: { self.__class__.TRANSITION_INDEX: int(self._current_transition[self.__class__.TRANSITION_INDEX]), self.__class__.TRANSITION_TIME: int(self._current_transition[self.__class__.TRANSITION_TIME]), }, self.__class__.ALIGNMENT: { self.__class__.DELAY_FOV_TO_VIDEO: delay_fov_to_video, }, } self._alignment_results[self._get_name_for_transition( self._current_transition)] = r self._alignment_stats_delays.append(delay_fov_to_video) self._alignment_stats_durations.append(duration) if self._get_zoom_type_for_transition( self._current_transition) == self.__class__.ZOOM_IN: self._alignment_stats_zoom_in_delays.append(delay_fov_to_video) self._alignment_stats_zoom_in_durations.append(duration) else: self._alignment_stats_zoom_out_delays.append(delay_fov_to_video) self._alignment_stats_zoom_out_durations.append(duration) def _get_ifv_info(self, ivf): return { self.__class__.FRAME_NUM: ivf.get_frame_num(), self.__class__.TIME_MS: ivf.get_time_ms(), } def _get_duration_ms(self, begin_ifv: ImageFromVideo, end_ifv: ImageFromVideo): return end_ifv.get_time_ms() - begin_ifv.get_time_ms() def _get_duration_frames(self, begin_ifv: ImageFromVideo, end_ifv: ImageFromVideo): return end_ifv.get_frame_num() - begin_ifv.get_frame_num() def _get_name_for_transition(self, transition): return (f'{self._get_zoom_type_for_transition(transition)}: ' f'{str(transition[self.__class__.TRANSITION_LEVELS])}') def _get_zoom_type_for_transition(self, transition: dict): if transition[self.__class__.TRANSITION_LEVELS][0] < transition[ self.__class__.TRANSITION_LEVELS][1]: return self.__class__.ZOOM_OUT else: return self.__class__.ZOOM_IN def run(self): self.show_scrub_picker_for_transitions() self.save_results()
def setUp(self): head_path = Path('/Volumes/WD') session_dir = 'Aug_17_Palo_Alto_High_2nd_time_B80_ottofillmore' results_head_path = Path('.') / 'data/calibration_data', ldfh = LDFH( head_path=head_path, ) cdf = CDF( top_level_dir=results_head_path ) fov_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.LENS_NPZ_FILE, fieldname=LDFH.LENS_FOV_FIELD, ) fov_time_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.LENS_NPZ_FILE, fieldname=LDFH.LENS_TIME_FIELD, ) tag_latitude_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.TAG_LATITUDE_FIELD, ) tag_longitude_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.TAG_LONGITUDE_FIELD, ) tag_time_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.TAG_TIME_FIELD, ) self.geo_map_scrubber = GeoMapScrubber( latitude_series=tag_latitude_series, longitude_series=tag_longitude_series, time_series=tag_time_series, ) self.video_path = ldfh.get_video_path(session_dir) map_fitter = MapFitter( latitude_series=tag_latitude_series, longitude_series=tag_longitude_series, ) map_coordinate_transformer = MapCoordinateTransformer.init_with_map_fitter( map_fitter=map_fitter, ) tag = Tag( latitude_series=tag_latitude_series, longitude_series=tag_longitude_series, time_series=tag_time_series, alignment_offset_video_to_tag_ms=-340, # From calibration data map_coordinate_transformer=map_coordinate_transformer, ) base_gps_latitude = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.BASE_LATITUDE_FIELD, ) base_gps_longitude = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.TAG_NPZ_FILE, fieldname=LDFH.BASE_LONGITUDE_FIELD, ) base_motor_time_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.BASE_NPZ_FILE, fieldname=LDFH.BASE_TIME_FIELD, ) pan_motor_angle_series = ldfh.get_field_from_npz_file( session_dir_name=session_dir, filename=LDFH.BASE_NPZ_FILE, fieldname=LDFH.PAN_MOTOR_READ_FIELD, ) self.base = Base( gps_latitude_series=base_gps_latitude, gps_longitude_series=base_gps_longitude, base_motor_time_series=base_motor_time_series, map_coordinate_transformer=map_coordinate_transformer, pan_motor_angle_series=pan_motor_angle_series, actual_latitude=37.386206696022725, actual_longitude=-122.11009181597608, # actual_latitude=37.386203, # actual_longitude=-122.110091, ) self.tag_position_analyzer = TagPositionInStableFovSegmentsAnalyzer( fov_series=fov_series, fov_time_series=fov_time_series, tag=tag, base=self.base, ) self.frames_limit = 1 self.threshold_deg = 20 self.min_distance_to_camera = 100 # self.frames_limit = None # self.threshold_deg = 20 # self.min_distance_to_camera = 100 self.legacy_data_base_position_calibrator = LegacyDataBasePositionCalibrator( session_dir=session_dir, tag_position_in_stable_fov_segments_analyzer=self.tag_position_analyzer, legacy_data_file_system_helper=ldfh, calibration_data_filer=cdf, frames_limit=self.frames_limit, angle_threshold_rad=np.radians(self.threshold_deg), min_distance_to_camera=self.min_distance_to_camera, ) return self
class PanMotorTimeBaseAligner: '''Determines with user assistance via scrubber picker statistics for offset in time between pan_motor_read timebase and video time''' RAW_RESULTS = 'raw_results' AGGREGATE_RESULTS = 'aggregate_results' MIN_OR_MAX = 'min_or_max' MIN = 'min' MAX = 'max' MEAN = 'mean' N = 'n' DATA = 'data' DELAY_MOTOR_TO_VIDEO = 'delay_motor_to_video' BASE_TIME = 'base_time' MOTOR_IDX = 'motor_idx' PAN_LOCAL_MINIMA = 'pan_local_minima' PAN_LOCAL_MAXIMA = 'pan_local_maxima' PAN_COMBINED = 'pan_combined' def __init__( self, session_dir: str, input_data_head_path=Path('/Volumes/WD'), results_head_path=Path('.') / 'data/calibration_data', cache_dir_path=Path('/Volumes/WD/image_cache'), num_points=1, ): self._session_dir = session_dir self._input_data_head_path = input_data_head_path self._results_head_path = results_head_path self._cache_dir_path = cache_dir_path self._num_points = num_points # Helpers self._ldfh = LDFH(self._input_data_head_path) self._calibration_data_filer = CalibrationDataFiler( self._results_head_path) self.maxima_text_color = 'red' self.minima_text_color = 'green' self.fontsize = 8 # Lazy initializers self._pan_motor_read_data_series = None self._base_time_series = None self._motor_maxima = None self._motor_minima = None self._motor_maxima_from_series = None self._motor_minima_from_series = None self._motor_maxima_from_archive = None self._motor_minima_from_archive = None self._archived_results = None self._selected_frames_from_archive = None # State self._base_time = None self._motor_idx = None self._min_or_max = None self._alignment_results = [] def visualize_motor_local_maxima_and_minima(self): fig, axis = plt.subplots() # pylint: disable=W0612 axis.plot(self.get_normalized_base_time(), self.get_motor_data()) # maxima_shoulder_steepness = [self.get_motor_data().get_shoulder_steepness_around_idx(maximum, 4) # for maximum in self.get_motor_maxima()] for idx, val in enumerate(self._get_motor_values_at_maxima()): axis.text( self.get_normalized_base_time()[self.get_motor_maxima()[idx]], val, str(idx), color=self.maxima_text_color, fontsize=self.fontsize, ) for idx, val in enumerate(self._get_motor_values_at_minima()): axis.text( self.get_normalized_base_time()[self.get_motor_minima()[idx]], val, str(idx), color=self.minima_text_color, fontsize=self.fontsize, ) plt.show() def get_motor_maxima(self): if self._motor_maxima is None: if self._get_motor_maxima_from_archived_results() is not None \ and self._get_motor_maxima_from_archived_results().size == self._num_points: self._motor_maxima = self._get_motor_maxima_from_archived_results( ) else: self._motor_maxima = self._get_motor_maxima_from_series() return self._motor_maxima def _get_motor_maxima_from_series(self): if self._motor_maxima_from_series is None: self._motor_maxima_from_series = self.get_motor_data().\ local_maxima_sorted_by_peakyness_and_monotonic_with_steep_shoulders(8, 4, 2) # pylint: disable=C0301 return self._motor_maxima_from_series[:self._num_points] def _get_motor_values_at_maxima(self): return np.array( [self.get_motor_data()[idx] for idx in self.get_motor_maxima()]) def _get_time_at_maxima(self): return [(maximum, self.get_normalized_base_time()[maximum]) for maximum in self.get_motor_maxima()] def get_motor_minima(self): if self._motor_minima is None: if self._get_motor_minima_from_archived_results() is not None and \ self._get_motor_minima_from_archived_results().size == self._num_points: self._motor_minima = self._get_motor_minima_from_archived_results( ) else: self._motor_minima = self._get_motor_minima_from_series() return self._motor_minima def _get_motor_minima_from_series(self): if self._motor_minima_from_series is None: self._motor_minima_from_series = self.get_motor_data().\ local_minima_sorted_by_peakyness_and_monotonic_with_steep_shoulders(8, 4, 2) # pylint: disable=C0301 return self._motor_minima_from_series[:self._num_points] def _get_motor_values_at_minima(self): return np.array( [self.get_motor_data()[idx] for idx in self.get_motor_minima()]) def _get_time_at_minima(self): return [(minimum, self.get_normalized_base_time()[minimum]) for minimum in self.get_motor_minima()] def show_scrub_picker_for_maxima(self): self._min_or_max = self.__class__.MAX for idx, time in self._get_time_at_maxima(): self._motor_idx = idx self._base_time = time self.show_scrub_picker_for_time(idx, time) def show_scrub_picker_for_minima(self): self._min_or_max = self.__class__.MIN for idx, time in self._get_time_at_minima(): self._motor_idx = idx self._base_time = time self.show_scrub_picker_for_time(idx, time) def show_scrub_picker_for_min_and_max(self): self.show_scrub_picker_for_minima() self.show_scrub_picker_for_maxima() def show_scrub_picker_for_time(self, motor_idx, time): ScrubPicker(images_from_video=self.get_images_around_time(time), selector_type=ScrubPicker.SELECT_SINGLE_IMAGE, callback=self._scrubber_callback, selected_start_frame_num=self. _get_selected_frame_num_for_motor_idx(motor_idx)).run() def get_images_around_time(self, time): return self.get_ifv_grabber().get_images_around_time_ms(time, before=40, after=40) def get_motor_data(self) -> np.ndarray: if self._pan_motor_read_data_series is None: self._pan_motor_read_data_series = \ self._ldfh.get_field_from_npz_file(self._session_dir, LDFH.BASE_NPZ_FILE, LDFH.PAN_MOTOR_READ_FIELD, ) return self._pan_motor_read_data_series def get_base_time(self): if self._base_time_series is None: self._base_time_series = self._ldfh.get_field_from_npz_file( self._session_dir, LDFH.BASE_NPZ_FILE, LDFH.BASE_TIME_FIELD, ) return self._base_time_series def get_normalized_base_time(self): return self.get_base_time() - self.get_base_time()[0] def get_motor_data_time_tuples(self): return np.dstack( (self.get_normalized_base_time(), self.get_motor_data()))[0] def get_video_path(self): return self._ldfh.get_video_path(self._session_dir) def get_ifv_grabber(self) -> ImageFromVideoGrabber: return ImageFromVideoGrabber(self.get_video_path(), cache_dir_path=self._cache_dir_path) def _scrubber_callback(self, images_from_video: List[ImageFromVideo]): ifv = images_from_video[0] r = ifv.get_as_dict() r[self.__class__.MIN_OR_MAX] = self._min_or_max r[self.__class__.MOTOR_IDX] = int(self._motor_idx) r[self.__class__.BASE_TIME] = int(self._base_time) r[self.__class__.DELAY_MOTOR_TO_VIDEO] = ifv.get_time_ms() - int( self._base_time) self._alignment_results.append(r) def _get_aggregate_data(self): r = {} local_min_data = np.array([]) local_max_data = np.array([]) for result in self._alignment_results: if result[self.__class__.MIN_OR_MAX] == self.__class__.MIN: local_min_data = np.append( local_min_data, result[self.__class__.DELAY_MOTOR_TO_VIDEO]) else: local_max_data = np.append( local_max_data, result[self.__class__.DELAY_MOTOR_TO_VIDEO]) combined_data = np.concatenate((local_min_data, local_max_data)) r[self.__class__.PAN_LOCAL_MINIMA] = self._get_stats_from_data( local_min_data) r[self.__class__.PAN_LOCAL_MAXIMA] = self._get_stats_from_data( local_max_data) r[self.__class__.PAN_COMBINED] = self._get_stats_from_data( combined_data) return r def _get_stats_from_data(self, data: np.ndarray): r = {} r[self.__class__.N] = int(data.size) r[self.__class__.DATA] = data.tolist() r[self.__class__.MIN] = int(np.min(data)) r[self.__class__.MAX] = int(np.max(data)) r[self.__class__.MEAN] = int(np.mean(data)) return r def _get_results_object(self): r = {} r[self.__class__.RAW_RESULTS] = self._alignment_results r[self.__class__.AGGREGATE_RESULTS] = self._get_aggregate_data() return r def save_results(self): self._calibration_data_filer.save_as_yml( self._get_results_object(), self._session_dir, CalibrationDataFiler.PAN_MOTOR_TIMEBASE_ALIGNMENT, ) def _get_archived_results(self): if self._archived_results is None: self._archived_results = self._calibration_data_filer.load( self._session_dir, CalibrationDataFiler.PAN_MOTOR_TIMEBASE_ALIGNMENT, # pylint: disable=C0301 ) return self._archived_results def _get_motor_idxs_from_archived_results(self, min_or_max: str): if self._get_archived_results() is None: return None results = self._get_archived_results()[self.__class__.RAW_RESULTS] return [ result[self.__class__.MOTOR_IDX] for result in results if result[self.__class__.MIN_OR_MAX] == min_or_max ] def _get_motor_maxima_from_archived_results(self): if self._motor_maxima_from_archive is None: self._motor_maxima_from_archive = self._get_motor_idxs_from_archived_results( self.__class__.MAX) if self._motor_maxima_from_archive is not None: self._motor_maxima_from_archive = np.array( self._motor_maxima_from_archive)[:self._num_points] return self._motor_maxima_from_archive def _get_motor_minima_from_archived_results(self): if self._motor_minima_from_archive is None: self._motor_minima_from_archive = self._get_motor_idxs_from_archived_results( self.__class__.MIN) if self._motor_minima_from_archive is not None: self._motor_minima_from_archive = np.array( self._motor_minima_from_archive)[:self._num_points] return self._motor_minima_from_archive def _get_selected_frames_from_archived_results(self): if self._selected_frames_from_archive is None: self._selected_frames_from_archive = {} if self._get_archived_results() is not None: results = self._get_archived_results()[ self.__class__.RAW_RESULTS] for result in results: self._selected_frames_from_archive[result[self.__class__.MOTOR_IDX]] = \ result[ImageFromVideo.FRAME_NUM] return self._selected_frames_from_archive def _get_selected_frame_num_for_motor_idx(self, motor_idx): if motor_idx not in self._get_selected_frames_from_archived_results(): return None else: return self._get_selected_frames_from_archived_results()[motor_idx] def run(self): self.show_scrub_picker_for_min_and_max() self.save_results()
class TagGpsTimebaseAligner: '''Determines with user assistance statistics for offset in time between tag_gps timebase and video time''' IFV = 'ifv' TAG_IDX = 'tag_idx' TAG_TIME = 'tag_time' AGGREGATE_RESULTS = 'aggregate_results' ALIGNMENT_STATS = 'alignment_stats' DELAY_VIDEO_TO_TAG_GPS = 'delay_video_to_tag_gps' MAX = 'max' MEAN = 'mean' MIN = 'min' NUM = 'num' INDIVIDUAL_RESULTS = 'individual_results' TAG_DATA = 'tag_data' TAG_IDX = 'tag_idx' TAG_TIME = 'tag_time' VIDEO_DATA = 'video_data' FRAME_NUM = 'frame_num' TIME_MS = 'time_ms' VIDEO_ID = 'video_id' VIDEO_URL = 'video_url' def __init__(self, session_dir: str, input_data_head_path=Path('/Volumes/WD'), results_head_path=Path('.') / 'data/calibration_data', cache_dir_path=Path('/Volumes/WD/image_cache'), ): self._session_dir = session_dir self._input_data_head_path = input_data_head_path self._results_head_path = results_head_path self._cache_dir_path = cache_dir_path # helpers self._ldfh = LDFH(self._input_data_head_path) self._calibration_data_filer = CalibrationDataFiler(self._results_head_path) # state self._alignment_data = [] # lazy inits self._latitude_series = None self._longitude_series = None self._time_series = None def _display_ui(self): geo_map_scrubber = GeoMapScrubber( latitude_series=self._get_latitude_series(), longitude_series=self._get_longitude_series(), time_series=self._get_time_series(), ) TagGpsTimebaseAlignerUi( geo_map_scrubber=geo_map_scrubber, video_path=self._get_video_path(), save_alignment_callback=self._save_alignment_callback, done_callback=self._done_callback, ).run() def _save_alignment_callback(self, image_from_video, tag_idx, tag_time): self._alignment_data.append( { self.__class__.IFV: image_from_video, self.__class__.TAG_IDX: tag_idx, self.__class__.TAG_TIME: tag_time, } ) print('Saved alignment delay: ', self._get_delay_video_to_tag(self._alignment_data[-1])) def _done_callback(self): self._calibration_data_filer.save_as_yml( obj=self._get_stats(), data_dir_name=self._session_dir, calibration_file=CalibrationDataFiler.TAG_GPS_TIMEBASE_ALIGNMENT, ) def _get_stats(self): return { self.__class__.AGGREGATE_RESULTS: self._get_aggregate_stats(), self.__class__.INDIVIDUAL_RESULTS: self._get_individual_stats_list(), } def _get_aggregate_stats(self): delays = [stat[self.__class__.DELAY_VIDEO_TO_TAG_GPS] for stat in self._get_individual_stats_list()] delays = np.array(delays) return { self.__class__.DELAY_VIDEO_TO_TAG_GPS: { self.__class__.MEAN: int(round(np.mean(delays))), self.__class__.MAX: int(round(np.max(delays))), self.__class__.MIN: int(round(np.min(delays))), } } def _get_individual_stats_list(self): r = [] for datum in self._alignment_data: r.append( { self.__class__.TAG_DATA: { self.__class__.TAG_IDX: int(datum[self.__class__.TAG_IDX]), self.__class__.TAG_TIME: int(datum[self.__class__.TAG_TIME]), }, self.__class__.VIDEO_DATA: datum[self.__class__.IFV].get_as_dict(), self.__class__.DELAY_VIDEO_TO_TAG_GPS: self._get_delay_video_to_tag(datum), } ) return r def _get_delay_video_to_tag(self, datum): video_time = datum[self.__class__.IFV].get_time_ms() return int(datum[self.__class__.TAG_TIME] - video_time) def _get_latitude_series(self): if self._latitude_series is None: self._latitude_series =\ self._ldfh.get_field_from_npz_file(self._session_dir, LDFH.TAG_NPZ_FILE, LDFH.TAG_LATITUDE_FIELD, ) return self._latitude_series def _get_longitude_series(self): if self._longitude_series is None: self._longitude_series =\ self._ldfh.get_field_from_npz_file(self._session_dir, LDFH.TAG_NPZ_FILE, LDFH.TAG_LONGITUDE_FIELD, ) return self._longitude_series def _get_time_series(self): if self._time_series is None: self._time_series =\ self._ldfh.get_field_from_npz_file(self._session_dir, LDFH.TAG_NPZ_FILE, LDFH.TAG_TIME_FIELD, ) return self._time_series def _get_video_path(self): return self._ldfh.get_video_path(self._session_dir)