def track_tails(self, make_video=False): """ processes the video and locates the tails """ # initialize the video self.load_video(make_video) # process first _frame to find objects tails = self.process_first_frame() # initialize the result structure tail_trajectories = collections.defaultdict(dict) # iterate through all frames iterator = display_progress(self.video) for self.frame_id, self._frame in enumerate(iterator, self.frame_start): self._frame_cache = {} #< delete cache per _frame if make_video: self.set_video_background(tails) # adapt the object outlines tails = self.adapt_tail_contours(tails) # store tail data in the result for tail_id, tail in enumerate(tails): tail_trajectories[tail_id][self.frame_id] = tail # update the debug output if make_video: self.update_video_output(tails, show_measurement_line=False) # save the data and close the videos self.result['tail_trajectories'] = tail_trajectories self.save_result() self.close()
def reduce_video(video, function, initial_value=None): """ applies function to consecutive frames """ result = initial_value for frame in display_progress(video): if result is None: result = frame else: result = function(frame, result) return result
def measure_mean(video): """ measures the mean of each movie pixel over time """ mean = np.zeros(video.shape[1:]) for n, frame in enumerate(display_progress(video)): mean = mean*n/(n + 1) + frame/(n + 1) return mean
def _iterate_over_video(self, video): """ internal function doing the heavy lifting by iterating over the video """ # load data from previous passes ground_profile = self.data['pass2/ground_profile'] background_frame_offset = self.data['pass1/video/background_frame_offset'] # create the video iterator with preprocessing that will be done in a # separate thread to speed up computations video_iter = VideoPreprocessor( video, functions={'gradient_strength': self.get_gradient_strenght}, use_threads=self.params['use_threads'] ) video_iter = display_progress(video_iter) # iterate over the video and analyze it for background_id, data in enumerate(video_iter): # extract the images from the preprocessed data frame = data['raw'] gradient_strength = data['gradient_strength'] # calculate _frame id in the original video self.frame_id = (background_frame_offset + background_id * self.params['output/video/period']) # copy _frame to debug video if 'video' in self.debug: self.debug['video'].set_frame(frame, copy=False) # retrieve data for current _frame self.ground = ground_profile.get_ground_profile(self.frame_id) # write the mouse trail to the mouse mask self.update_mouse_mask() # find the changes in the background if self.params['burrows/enabled_pass4']: self.find_burrows_using_active_contour(frame, gradient_strength) # store some debug information self.debug_process_frame(frame) if background_id % 1000 == 0: self.logger.debug('Analyzed _frame %d', self.frame_id)
def measure_mean_std(video): """ measures the mean and the standard deviation of each movie pixel over time Uses https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Incremental_algorithm """ mean = np.zeros(video.shape[1:]) M2 = np.zeros(video.shape[1:]) for n, frame in enumerate(display_progress(video)): delta = frame - mean mean = mean + delta/(n + 1) M2 = M2 + delta*(frame - mean) if (n < 2): return frame, 0 return mean, np.sqrt(M2/n)
def determine_average_frame_brightness(video_file_or_pattern, output_hdf5_file=None): """ iterates a video and determines its intensity, which will be stored in a hdf5 file, if the respective file name is given""" # read video data with closing(load_any_video(video_file_or_pattern)) as video: brightness = np.empty(video.frame_count, np.double) for k, frame in enumerate(display_progress(video)): brightness[k] = frame.mean() # restrict the result to the number of actual frames read if k < video.frame_count: brightness = brightness[:k + 1] # write brightness data if output_hdf5_file: with h5py.File(output_hdf5_file, "w") as fd: fd.create_dataset("brightness", data=brightness) return brightness
def _iterate_over_video(self, video): """ internal function doing the heavy lifting by iterating over the video """ # load data from previous passes mouse_track = self.data['pass2/mouse_trajectory'] ground_profile = self.data['pass2/ground_profile'] frame_offset = self.result['video/frames'][0] if frame_offset is None: frame_offset = 0 # iterate over the video and analyze it for self.frame_id, frame in enumerate(display_progress(video), frame_offset): # adapt the background to current _frame adaptation_rate = self.params['background/adaptation_rate'] self.background += adaptation_rate * (frame - self.background) # copy _frame to debug video if 'video' in self.debug: self.debug['video'].set_frame(frame, copy=False) # retrieve data for current _frame try: self.mouse_pos = mouse_track.pos[self.frame_id, :] except IndexError: # Sometimes the mouse trail has not been calculated till the end self.mouse_pos = (np.nan, np.nan) self.ground = ground_profile.get_ground_profile(self.frame_id) if self.params['burrows/enabled_pass3']: # find the burrow from the mouse trail self.find_burrows() # find out where the mouse currently is self.classify_mouse_state(mouse_track) # store some information in the debug dictionary self.debug_process_frame(frame, mouse_track) if self.frame_id % 1000 == 0: self.logger.debug('Analyzed _frame %d', self.frame_id)
def _iterate_over_video(self, video): """ internal function doing the heavy lifting by iterating over the video """ # load data from previous passes mouse_track = self.data['pass2/mouse_trajectory'] ground_profile = self.data['pass2/ground_profile'] frame_offset = self.result['video/frames'][0] if frame_offset is None: frame_offset = 0 # iterate over the video and analyze it for self.frame_id, frame in enumerate(display_progress(video), frame_offset): # adapt the background to current _frame adaptation_rate = self.params['background/adaptation_rate'] self.background += adaptation_rate*(frame - self.background) # copy _frame to debug video if 'video' in self.debug: self.debug['video'].set_frame(frame, copy=False) # retrieve data for current _frame try: self.mouse_pos = mouse_track.pos[self.frame_id, :] except IndexError: # Sometimes the mouse trail has not been calculated till the end self.mouse_pos = (np.nan, np.nan) self.ground = ground_profile.get_ground_profile(self.frame_id) if self.params['burrows/enabled_pass3']: # find the burrow from the mouse trail self.find_burrows() # find out where the mouse currently is self.classify_mouse_state(mouse_track) # store some information in the debug dictionary self.debug_process_frame(frame, mouse_track) if self.frame_id % 1000 == 0: self.logger.debug('Analyzed _frame %d', self.frame_id)
def copy(self, dtype=np.uint8, disp=True): """ Creates a copy of the current video and returns a VideoMemory instance. """ # prevent circular import by lazy importing from .memory import VideoMemory logger.debug('Copy a video stream and store it in memory') # copy the data into a numpy array data = np.empty(self.shape, dtype) if disp: iterator = display_progress(self) else: iterator = self for k, val in enumerate(iterator): data[k, ...] = val # construct the memory object without copying the data return VideoMemory(data, fps=self.fps, copy_data=False)
def do_linescans(self, make_video=True): """ do the line scans for all current_tails """ # initialize the video self.load_video(make_video) self.load_result() # load the previously track tail data try: tail_trajectories = self.result['tail_trajectories'] except KeyError: raise ValueError('Tails have to be tracked before line scans can ' 'be done.') current_tails = None # collect all line scans in a structure linescans = collections.defaultdict(list) # iterate through all frames and collect the line scans iterator = display_progress(self.video) for self.frame_id, self._frame in enumerate(iterator, self.frame_start): self._frame_cache = {} #< delete cache per _frame if make_video: self.set_video_background(current_tails) # do the line scans in each object current_tails = [] for tail_id, tail_trajectory in tail_trajectories.iteritems(): try: tail = tail_trajectory[self.frame_id] except KeyError: logging.debug('Could not find any information on tails ' 'for _frame %d', self.frame_id) break # get the line scan left and right of the centerline linescan_data= self.measure_single_tail(self._frame, tail) # append it to the temporary structure linescans[tail_id].append(linescan_data) current_tails.append(tail) #< safe for video output # update the debug output if make_video: self.update_video_output(current_tails, show_measurement_line=True) # create the kymographs from the line scans kymographs = collections.defaultdict(dict) # iterate over all tails found in the movie for tail_id, linescan_trajectory in linescans.iteritems(): # transpose data to have data for each side of the center line line_scans = zip(*linescan_trajectory) # iterate over all line scans (ventral and dorsal) for side_id, data in enumerate(line_scans): kymograph = Kymograph(data) side = tail.line_names[side_id] kymographs[tail_id][side] = kymograph # save the data and close the videos self.result['kymographs'] = kymographs self.save_result() self.close()
def produce_video(self): """ prepares everything for the debug output """ # load parameters for video output video_extension = self.params['output/video/extension'] video_codec = self.params['output/video/codec'] video_bitrate = self.params['output/video/bitrate'] if self.video is None: self.load_video() filename = self.get_filename('video' + video_extension, 'video') video = VideoComposer(filename, size=self.video.size, fps=self.video.fps, is_color=True, output_period=self.params['output/output_period'], codec=video_codec, bitrate=video_bitrate) mouse_track = self.data['pass2/mouse_trajectory'] ground_profile = self.data['pass2/ground_profile'] self.logger.info('Pass 2 - Start producing final video with %d frames', len(self.video)) # we used the first frame to determine the cage dimensions in the first pass source_video = self.video[1:] track_first = 0 tracks = self.data['pass1/objects/tracks'] tracks.sort(key=lambda track: track.start) burrow_tracks = self.data['pass1/burrows/tracks'] for frame_id, frame in enumerate(display_progress(source_video)): # set real video as background video.set_frame(frame) # plot the ground profile ground_line = ground_profile.get_groundline(frame_id) video.add_line(ground_line, is_closed=False, mark_points=False, color='y') # indicate burrow centerline for burrow in burrow_tracks.find_burrows(frame_id): ground = GroundProfile(ground_line) video.add_line(burrow.get_centerline(ground), 'k', is_closed=False, width=2) # indicate all moving objects # find the first track which is still active while tracks[track_first].end < frame_id: track_first += 1 # draw all tracks that are still active for track in tracks[track_first:]: if track.start > frame_id: # this is the last track that can possibly be active break elif track.start <= frame_id <= track.end: video.add_circle(track.get_pos(frame_id), self.params['mouse/model_radius'], '0.5', thickness=1) # indicate the mouse position if np.all(np.isfinite(mouse_track.pos[frame_id])): video.add_circle(mouse_track.pos[frame_id], self.params['mouse/model_radius'], 'w', thickness=2) # # add additional debug information video.add_text(str(frame_id), (20, 20), anchor='top') # video.add_text(str(self.frame_id/self.fps), (20, 20), anchor='top') video.close()
def make_cropped_video(result_file, output_video=None, display='{time} [{frame}]', scale_bar=True, border_buffer_cm=0, frame_compression=1, time_duration=None, progress=True): """ function that crops a video to an antfarm. `result_file` is the file where the results from the video analysis are stored. This is usually a *.yaml file `output_video` denotes the filename where the result video should be written to. `display` determines what information is displayed. There are several variables that would be replaced by data: {time} the current time stamp {frame} the current frame number `scale_bar` determines whether a scale bar is shown `border_buffer_cm` sets the extra space (in units of cm) around the cropping rectangle that is included in the analysis `frame_compression` sets the compression factor that determines how many frames are dropped compared to the original video `time_duration` sets the maximal number of seconds the video is supposed to last. Additional frames will not be written. `progress` flag that determines whether the progress is displayed """ logging.info('Analyze video `%s`', result_file) # load the respective result file analyzer = load_result_file(result_file) # load the full original video video_info = analyzer.load_video(frames=(0, None)) # crop the video to the cage video_input = analyzer.video cropping_cage = analyzer.data['pass1/video/cropping_cage'] border_buffer_px = int(border_buffer_cm / analyzer.length_scale_mag) # change rectangle size if necessary if border_buffer_px != 0: cropping_rect = Rectangle.from_list(cropping_cage) video_rect = Rectangle(0, 0, video_input.width - 1, video_input.height - 1) cropping_rect.buffer(border_buffer_px) cropping_rect.intersect(video_rect) cropping_cage = cropping_rect.to_list() if cropping_cage: # size_alignment=2 makes sure that the width and height are even numbers video_input = FilterCrop(video_input, rect=cropping_cage, size_alignment=2) if frame_compression is not None and frame_compression != 1: video_input = FilterDropFrames(video_input, compression=frame_compression) if time_duration is not None: index_max = int(time_duration * video_input.fps) video_input = video_input[:index_max] # determine the filename of the output video if output_video is None: # determine the complete filename automatically movie_ext = analyzer.params['output/video/extension'] filename = 'cropped' output_video = analyzer.get_filename(filename + movie_ext, 'video') elif '.' not in output_video: # determine the extension automatically movie_ext = analyzer.params['output/video/extension'] output_video = output_video + movie_ext logging.info('Write output to `%s`', output_video) # create the video writer video_codec = analyzer.params['output/video/codec'] video_bitrate = analyzer.params['output/video/crop_bitrate'] if video_bitrate is None: video_bitrate = analyzer.params['output/video/bitrate'] fps = video_input.fps video_output = VideoComposer( output_video, size=video_input.size, fps=fps, is_color=video_input.is_color, codec=video_codec, bitrate=video_bitrate, ) # time label position label_pos = video_input.width // 2, 30 # calculate size of scale bar and the position of its label pixel_size_cm = analyzer.data['pass2/pixel_size_cm'] scale_bar_size_cm = 10 scale_bar_size_px = np.round(scale_bar_size_cm / pixel_size_cm) scale_bar_rect = Rectangle(30, 50, scale_bar_size_px, 5) scale_bar_pos = (30 + scale_bar_size_px//2, 30) if progress: video_input = display_progress(video_input) for frame_id, frame in enumerate(video_input): video_output.set_frame(frame, copy=True) if scale_bar: # show a scale bar video_output.add_rectangle(scale_bar_rect, width=-1) video_output.add_text(str('%g cm' % scale_bar_size_cm), scale_bar_pos, color='w', anchor='upper center') # gather data about this frame frame_data = {'frame': frame_id} # calculate time stamp time_secs, time_frac = divmod(frame_id, fps) time_msecs = int(1000 * time_frac / fps) dt = datetime.timedelta(seconds=time_secs, milliseconds=time_msecs) frame_data['time'] = str(dt) # output the display data if display: display_text = display.format(**frame_data) video_output.add_text(display_text, label_pos, color='w', anchor='upper center') # show summary frames_total = video_info['frames'][1] - video_info['frames'][0] frames_written = video_output.frames_written logging.info('%d (%d%%) of %d frames written', frames_written, 100 * frames_written // frames_total, frames_total) # close and finalize video try: video_output.close() except IOError: logging.exception('Error while writing out the debug video `%s`', video_output)
def make_underground_video(result_file, output_video=None, display='{time} [{frame}]', scale_bar=True, min_duration=60, blank_duration=5, bouts_slice=slice(None, None), video_part=None): """ main routine of the program `result_file` is the file where the results from the video analysis are stored. This is usually a *.yaml file `output_video` denotes the filename where the result video should be written to. `display` determines what information is displayed. There are several variables that would be replaced by data: {time} the current time stamp {frame} the current frame number `scale_bar` determines whether a scale bar is shown `min_duration` determines how many frames the mouse has to be below ground for the bout to be included in the video `blank_duration` determines who many white frames are displayed between time intervals where the mouse is underground `bouts_slice` is a slice object that determines which bouts are included in the video. `video_part` determines which part of a longer video will be produced """ logging.info('Analyze video `%s`', result_file) # load the respective result file analyzer = load_result_file(result_file) # determine the bouts of this video bouts = get_underground_bouts(analyzer, bouts_slice, video_part) if len(bouts) == 0: raise RuntimeError('There are no bouts that could be turned into a ' 'video. This could be a problem with finding the ' 'mouse trajectory in the analysis or it could ' 'indicate that the requested bouts_slice or ' 'video_part resulted in empty data.') # load the original video video_info = analyzer.load_video() frame_offset = video_info['frames'][0] # crop the video to the cage video_input = analyzer.video cropping_cage = analyzer.data['pass1/video/cropping_cage'] if cropping_cage: video_input = FilterCrop(video_input, rect=cropping_cage) # determine the filename of the output video if output_video is None: # determine the complete filename automatically movie_ext = analyzer.params['output/video/extension'] if video_part is None: filename = 'underground' else: filename = 'underground_%d' % video_part output_video = analyzer.get_filename(filename + movie_ext, 'video_underground') elif '.' not in output_video: # determine the extension automatically movie_ext = analyzer.params['output/video/extension'] output_video = output_video + movie_ext logging.info('Write output to `%s`', output_video) # create the video writer video_codec = analyzer.params['output/video/codec'] video_bitrate = analyzer.params['output/video/bitrate'] fps = video_input.fps video_output = VideoComposer( output_video, size=video_input.size, fps=fps, is_color=False, codec=video_codec, bitrate=video_bitrate, ) # create blank frame with mean color of video blank_frame = np.full(video_input.shape[1:], video_input[0].mean(), dtype=np.uint8) # time label position label_pos = video_input.width // 2, 30 # calculate size of scale bar and the position of its label pixel_size_cm = analyzer.data['pass2/pixel_size_cm'] scale_bar_size_cm = 10 scale_bar_size_px = np.round(scale_bar_size_cm / pixel_size_cm) scale_bar_rect = Rectangle(30, 50, scale_bar_size_px, 5) scale_bar_pos = (30 + scale_bar_size_px//2, 30) # iterate over all bouts for start, finish in display_progress(bouts): duration = finish - start + 1 # check whether bout is long enough if duration < min_duration: continue if video_output.frames_written > 1: # write blank frame for _ in xrange(blank_duration): video_output.set_frame(blank_frame) video_bout = video_input[start - frame_offset : finish - frame_offset + 1] for frame_id, frame in enumerate(video_bout, start): video_output.set_frame(frame, copy=True) if scale_bar: # show a scale bar video_output.add_rectangle(scale_bar_rect, width=-1) video_output.add_text(str('%g cm' % scale_bar_size_cm), scale_bar_pos, color='w', anchor='upper center') # gather data about this frame frame_data = {'frame': frame_id} # calculate time stamp time_secs, time_frac = divmod(frame_id, fps) time_msecs = int(1000 * time_frac / fps) dt = datetime.timedelta(seconds=time_secs, milliseconds=time_msecs) frame_data['time'] = str(dt) # output the display data if display: display_text = display.format(**frame_data) video_output.add_text(display_text, label_pos, color='w', anchor='upper center') # show summary frames_total = video_info['frames'][1] - video_info['frames'][0] frames_written = video_output.frames_written logging.info('%d (%d%%) of %d frames written', frames_written, 100 * frames_written // frames_total, frames_total) # close and finalize video try: video_output.close() except IOError: logging.exception('Error while writing out the debug video `%s`', video_output)
def main(): """ main routine of the program """ # parse the command line arguments parser = argparse.ArgumentParser(description='Analyze antfarm polygons') parser.add_argument('-c', '--result_csv', dest='result_csv', type=str, metavar='FILE.csv', help='csv file to which statistics about the burrows ' 'are written') parser.add_argument('-p', '--result_pkl', dest='result_pkl', type=str, metavar='FILE.pkl', help='python pickle file to which all results from the ' 'algorithm are written') parser.add_argument('-l', '--load_pkl', dest='load_pkl', type=str, metavar='FILE.pkl', help='python pickle file from which data is loaded') parser.add_argument('-f', '--folder', dest='folder', type=str, help='folder where output images will be written to') parser.add_argument('--scale', dest='scale', type=float, default=default_parameters['scale_bar/length_cm'], help='length of the scale bar in cm') flags = parser.add_mutually_exclusive_group(required=False) flags.add_argument('-m', '--multi-processing', dest='multiprocessing', action='store_true', help='turns on multiprocessing') flags.add_argument('-d', '--debug', dest='debug', action='store_true', help='does debug output') parser.add_argument('files', metavar='FILE', type=str, nargs='*', help='files to analyze') # parse the command line arguments args = parser.parse_args() if args.debug: logging.getLogger().setLevel(logging.DEBUG) if args.folder: ensure_directory_exists(args.folder) if args.load_pkl: # load file from pickled data logging.info('Loading data from file `%s`.' % args.load_pkl) with open(args.load_pkl, "rb") as fp: results = pickle.load(fp) else: # get files to analyze files = args.files logging.info('Analyzing %d files.' % len(files)) # collect burrows from all files if args.multiprocessing: # use multiple processes to analyze data job_func = functools.partial(process_polygon_file, output_folder=args.folder, suppress_exceptions=True, scale=args.scale) pool = mp.Pool() results = pool.map(job_func, files) else: # analyze data in the current process job_func = functools.partial(process_polygon_file, output_folder=args.folder, suppress_exceptions=False, debug=args.debug, scale=args.scale) # iterate over all files results = [] for path in misc.display_progress(files): results.append(job_func(path)) # filter results results = [res for res in results if res is not None] # write complete results as pickle file if requested if args.result_pkl: with open(args.result_pkl, "wb") as fp: pickle.dump(results, fp) # write burrow results as csv file if requested if args.result_csv: # create a dictionary of lists table = collections.defaultdict(list) # iterate through all experiments and save information about the burrows for data in results: if data: # sort the burrows from left to right burrows = sorted(data['burrows'], key=operator.itemgetter('pos_x')) # create a single row per burrow for burrow_id, properties in enumerate(burrows, 1): properties['burrow_id'] = burrow_id properties['experiment'] = data['name'] # iterate over all burrow properties for k, v in properties.iteritems(): table[k].append(v) # write the data to a csv file first_columns = ['experiment', 'burrow_id'] data_structures.misc.save_dict_to_csv(table, args.result_csv, first_columns=first_columns)