def export_tracks(self, database: TrackDatabase): """ Writes tracks to a track database. :param database: database to write track to. """ clip_id = os.path.basename(self.source_file) # overwrite any old clips. # Note: we do this even if there are no tracks so there there will be a blank clip entry as a record # that we have processed it. database.create_clip(clip_id, self) if len(self.tracks) == 0: return if not self.frame_buffer.has_flow: self.frame_buffer.generate_flow(self.opt_flow, self.flow_threshold) # get track data for track_number, track in enumerate(self.tracks): track_data = [] for i in range(len(track)): channels = self.get_track_channels(track, i) # zero out the filtered channel if not self.INCLUDE_FILTERED_CHANNEL: channels[TrackChannels.filtered] = 0 track_data.append(channels) track_id = track_number+1 database.add_track(clip_id, track_id, track_data, track, opts=blosc_zstd if self.ENABLE_COMPRESSION else None)
def export_tracks(self, full_path, tracker: TrackExtractor, database: TrackDatabase): """ Writes tracks to a track database. :param database: database to write track to. """ clip_id = os.path.basename(full_path) # overwrite any old clips. # Note: we do this even if there are no tracks so there there will be a blank clip entry as a record # that we have processed it. database.create_clip(clip_id, tracker) if len(tracker.tracks) == 0: return tracker.generate_optical_flow() # get track data for track_number, track in enumerate(tracker.tracks): track_data = [] for i in range(len(track)): channels = tracker.get_track_channels(track, i) # zero out the filtered channel if not self.config.extract.include_filtered_channel: channels[TrackChannels.filtered] = 0 track_data.append(channels) track_id = track_number+1 start_time, end_time = tracker.start_and_end_time_absolute(track) database.add_track(clip_id, track_id, track_data, track, opts=self.compression, start_time=start_time, end_time=end_time)
class ClipLoader: def __init__(self, config): self.config = config os.makedirs(self.config.tracks_folder, mode=0o775, exist_ok=True) self.database = TrackDatabase( os.path.join(self.config.tracks_folder, "dataset.hdf5")) self.compression = (tools.gzip_compression if self.config.load.enable_compression else None) self.track_config = config.tracking # number of threads to use when processing jobs. self.workers_threads = config.worker_threads self.previewer = Previewer.create_if_required(config, config.load.preview) self.track_extractor = ClipTrackExtractor( self.config.tracking, self.config.use_opt_flow or config.load.preview == Previewer.PREVIEW_TRACKING, self.config.load.cache_to_disk, ) def process_all(self, root=None): if root is None: root = self.config.source_folder jobs = [] for folder_path, _, files in os.walk(root): for name in files: if os.path.splitext(name)[1] == ".cptv": full_path = os.path.join(folder_path, name) jobs.append((self, full_path)) self._process_jobs(jobs) def _process_jobs(self, jobs): if self.workers_threads == 0: for job in jobs: process_job(job) else: pool = multiprocessing.Pool(self.workers_threads) try: pool.map(process_job, jobs, chunksize=1) pool.close() pool.join() except KeyboardInterrupt: logging.info("KeyboardInterrupt, terminating.") pool.terminate() exit() except Exception: logging.exception("Error processing files") else: pool.close() def _get_dest_folder(self, filename): return os.path.join(self.config.tracks_folder, get_distributed_folder(filename)) def _export_tracks(self, full_path, clip): """ Writes tracks to a track database. :param database: database to write track to. """ # overwrite any old clips. # Note: we do this even if there are no tracks so there there will be a blank clip entry as a record # that we have processed it. self.database.create_clip(clip) for track in clip.tracks: start_time, end_time = clip.start_and_end_time_absolute( track.start_s, track.end_s) track_data = [] for region in track.bounds_history: frame = clip.frame_buffer.get_frame(region.frame_number) frame = track.crop_by_region(frame, region) # zero out the filtered channel if not self.config.load.include_filtered_channel: frame[TrackChannels.filtered] = 0 track_data.append(frame) self.database.add_track( clip.get_id(), track, track_data, opts=self.compression, start_time=start_time, end_time=end_time, ) def _filter_clip_tracks(self, clip_metadata): """ Removes track metadata for tracks which are invalid. Tracks are invalid if they aren't confident or they are in the excluded_tags list. Returns valid tracks """ tracks_meta = clip_metadata.get("Tracks", []) valid_tracks = [ track for track in tracks_meta if self._track_meta_is_valid(track) ] clip_metadata["Tracks"] = valid_tracks return valid_tracks def _track_meta_is_valid(self, track_meta): """ Tracks are valid if their confidence meets the threshold and they are not in the excluded_tags list, defined in the config. """ min_confidence = self.track_config.min_tag_confidence excluded_tags = self.config.excluded_tags track_data = track_meta.get("data") if not track_data: return False track_tag = Track.get_best_human_tag(track_meta, self.config.load.tag_precedence, min_confidence) if track_tag is None: return False tag = track_tag.get("what") confidence = track_tag.get("confidence", 0) return tag and tag not in excluded_tags and confidence >= min_confidence def process_file(self, filename): start = time.time() base_filename = os.path.splitext(os.path.basename(filename))[0] logging.info(f"processing %s", filename) destination_folder = self._get_dest_folder(base_filename) os.makedirs(destination_folder, mode=0o775, exist_ok=True) # delete any previous files tools.purge(destination_folder, base_filename + "*.mp4") # read metadata metadata_filename = os.path.join(os.path.dirname(filename), base_filename + ".txt") if not os.path.isfile(metadata_filename): logging.error("No meta data found for %s", metadata_filename) return metadata = tools.load_clip_metadata(metadata_filename) valid_tracks = self._filter_clip_tracks(metadata) if not valid_tracks: logging.error("No valid track data found for %s", filename) return clip = Clip(self.track_config, filename) clip.load_metadata( metadata, self.config.load.include_filtered_channel, self.config.load.tag_precedence, ) self.track_extractor.parse_clip(clip) # , self.config.load.cache_to_disk, self.config.use_opt_flow if self.track_config.enable_track_output: self._export_tracks(filename, clip) # write a preview if self.previewer: preview_filename = base_filename + "-preview" + ".mp4" preview_filename = os.path.join(destination_folder, preview_filename) self.previewer.create_individual_track_previews( preview_filename, clip) self.previewer.export_clip_preview(preview_filename, clip) if self.track_config.verbose: num_frames = len(clip.frame_buffer.frames) ms_per_frame = (time.time() - start) * 1000 / max(1, num_frames) self._log_message( "Tracks {}. Frames: {}, Took {:.1f}ms per frame".format( len(clip.tracks), num_frames, ms_per_frame)) def _log_message(self, message): """ Record message in stdout. Will be printed if verbose is enabled. """ # note, python has really good logging... I should probably make use of this. if self.track_config.verbose: logging.info(message)
class ClipLoader: def __init__(self, config, reprocess=False): self.config = config os.makedirs(self.config.tracks_folder, mode=0o775, exist_ok=True) self.database = TrackDatabase( os.path.join(self.config.tracks_folder, "dataset.hdf5") ) self.reprocess = reprocess self.compression = ( tools.gzip_compression if self.config.load.enable_compression else None ) self.track_config = config.tracking # number of threads to use when processing jobs. self.workers_threads = config.worker_threads self.previewer = Previewer.create_if_required(config, config.load.preview) self.track_extractor = ClipTrackExtractor( self.config.tracking, self.config.use_opt_flow or config.load.preview == Previewer.PREVIEW_TRACKING, self.config.load.cache_to_disk, high_quality_optical_flow=self.config.load.high_quality_optical_flow, verbose=config.verbose, ) def process_all(self, root): job_queue = Queue() processes = [] for i in range(max(1, self.workers_threads)): p = Process( target=process_job, args=(self, job_queue), ) processes.append(p) p.start() if root is None: root = self.config.source_folder file_paths = [] for folder_path, _, files in os.walk(root): for name in files: if os.path.splitext(name)[1] == ".cptv": full_path = os.path.join(folder_path, name) file_paths.append(full_path) # allows us know the order of processing file_paths.sort() for file_path in file_paths: job_queue.put(file_path) logging.info("Processing %d", job_queue.qsize()) for i in range(len(processes)): job_queue.put("DONE") for process in processes: try: process.join() except KeyboardInterrupt: logging.info("KeyboardInterrupt, terminating.") for process in processes: process.terminate() exit() def _get_dest_folder(self, filename): return os.path.join(self.config.tracks_folder, get_distributed_folder(filename)) def _export_tracks(self, full_path, clip): """ Writes tracks to a track database. :param database: database to write track to. """ # overwrite any old clips. # Note: we do this even if there are no tracks so there there will be a blank clip entry as a record # that we have processed it. self.database.create_clip(clip) for track in clip.tracks: start_time, end_time = clip.start_and_end_time_absolute( track.start_s, track.end_s ) original_thermal = [] cropped_data = [] for region in track.bounds_history: frame = clip.frame_buffer.get_frame(region.frame_number) original_thermal.append(frame.thermal) cropped = frame.crop_by_region(region) # zero out the filtered channel if not self.config.load.include_filtered_channel: cropped.filtered = np.zeros(cropped.thermal.shape) cropped_data.append(cropped) sample_frames = get_sample_frames( clip.ffc_frames, [bounds.mass for bounds in track.bounds_history], self.config.build.segment_min_avg_mass, cropped_data, ) self.database.add_track( clip.get_id(), track, cropped_data, sample_frames=sample_frames, opts=self.compression, original_thermal=original_thermal, start_time=start_time, end_time=end_time, ) self.database.finished_processing(clip.get_id()) def _filter_clip_tracks(self, clip_metadata): """ Removes track metadata for tracks which are invalid. Tracks are invalid if they aren't confident or they are in the excluded_tags list. Returns valid tracks """ tracks_meta = clip_metadata.get("Tracks", []) valid_tracks = [ track for track in tracks_meta if self._track_meta_is_valid(track) ] clip_metadata["Tracks"] = valid_tracks return valid_tracks def _track_meta_is_valid(self, track_meta): """ Tracks are valid if their confidence meets the threshold and they are not in the excluded_tags list, defined in the config. """ min_confidence = self.track_config.min_tag_confidence track_data = track_meta.get("data") if not track_data: return False track_tags = [] if "TrackTags" in track_meta: track_tags = track_meta["TrackTags"] excluded_tags = [ tag for tag in track_tags if not tag.get("automatic", False) and tag in self.config.load.excluded_tags ] if len(excluded_tags) > 0: return False track_tag = Track.get_best_human_tag( track_tags, self.config.load.tag_precedence, min_confidence ) if track_tag is None: return False tag = track_tag.get("what") confidence = track_tag.get("confidence", 0) return tag and tag not in excluded_tags and confidence >= min_confidence def process_file(self, filename, classifier=None): start = time.time() base_filename = os.path.splitext(os.path.basename(filename))[0] logging.info(f"processing %s", filename) destination_folder = self._get_dest_folder(base_filename) # delete any previous files tools.purge(destination_folder, base_filename + "*.mp4") # read metadata metadata_filename = os.path.join( os.path.dirname(filename), base_filename + ".txt" ) if not os.path.isfile(metadata_filename): logging.error("No meta data found for %s", metadata_filename) return metadata = tools.load_clip_metadata(metadata_filename) if not self.reprocess and self.database.has_clip(str(metadata["id"])): logging.warning("Already loaded %s", filename) return valid_tracks = self._filter_clip_tracks(metadata) if not valid_tracks or len(valid_tracks) == 0: logging.error("No valid track data found for %s", filename) return clip = Clip(self.track_config, filename) clip.load_metadata( metadata, self.config.load.tag_precedence, ) tracker_versions = set( [ t.get("data", {}).get("tracker_version", 0) for t in metadata.get("Tracks", []) ] ) if len(tracker_versions) > 1: logginer.error( "Tracks with different tracking versions cannot process %s versions %s", filename, tracker_versions, ) return tracker_version = tracker_versions.pop() process_background = tracker_version < 10 logging.debug( "Processing background? %s tracker_versions %s", process_background, tracker_version, ) if not self.track_extractor.parse_clip( clip, process_background=process_background ): logging.error("No valid clip found for %s", filename) return # , self.config.load.cache_to_disk, self.config.use_opt_flow if self.track_config.enable_track_output: self._export_tracks(filename, clip) # write a preview if self.previewer: os.makedirs(destination_folder, mode=0o775, exist_ok=True) preview_filename = base_filename + "-preview" + ".mp4" preview_filename = os.path.join(destination_folder, preview_filename) self.previewer.create_individual_track_previews(preview_filename, clip) self.previewer.export_clip_preview(preview_filename, clip) if self.config.verbose: num_frames = len(clip.frame_buffer.frames) ms_per_frame = (time.time() - start) * 1000 / max(1, num_frames) self._log_message( "Tracks {}. Frames: {}, Took {:.1f}ms per frame".format( len(clip.tracks), num_frames, ms_per_frame ) ) def _log_message(self, message): """Record message in stdout. Will be printed if verbose is enabled.""" # note, python has really good logging... I should probably make use of this. if self.track_config.verbose: logging.info(message)