def write_video_as_chunked_tiffs(input_reader, tiffs_to_trace_directory, chunk_size=200, chunk_name_pattern='chunk%08d.tif', stop_after_frame=None, monitor_video=None, timestamps_filename=None, monitor_video_kwargs=None): """Write frames to disk as tiff stacks input_reader : object providing .iter_frames() method and perhaps also a .timestamps attribute. For instance, PFReader, or some FFmpegReader object. tiffs_to_trace_directory : where to store the chunked tiffs stop_after_frame : to stop early monitor_video : if not None, should be a filename to write a movie to timestamps_filename : if not None, should be the name to write timestamps monitor_video_kwargs : ffmpeg params Returns: ChunkedTiffWriter object """ # Tiff writer ctw = WhiskiWrap.ChunkedTiffWriter(tiffs_to_trace_directory, chunk_size=chunk_size, chunk_name_pattern=chunk_name_pattern) # FFmpeg writer is initalized after first frame ffw = None # Iterate over frames for nframe, frame in enumerate(input_reader.iter_frames()): # Stop early? if stop_after_frame is not None and nframe >= stop_after_frame: break # Write to chunked tiff ctw.write(frame) # Optionally write to monitor video if monitor_video is not None: # Initialize ffw after first frame so we know the size if ffw is None: ffw = WhiskiWrap.FFmpegWriter(monitor_video, frame_width=frame.shape[1], frame_height=frame.shape[0], **monitor_video_kwargs) ffw.write(frame) # Finalize writers ctw.close() if ffw is not None: ff_stdout, ff_stderr = ffw.close() # Also write timestamps as numpy file if hasattr(input_reader, 'timestamps') and timestamps_filename is not None: timestamps = np.concatenate(input_reader.timestamps) assert len(timestamps) >= ctw.frames_written np.save(timestamps_filename, timestamps[:ctw.frames_written]) return ctw
def write_video_with_overlays_from_data( output_filename, input_reader, input_width, input_height, verbose=True, frame_triggers=None, trigger_dstart=-250, trigger_dstop=50, plot_trial_numbers=True, d_temporal=5, d_spatial=1, dpi=50, output_fps=30, input_video_alpha=1, whiskers_table=None, whiskers_file_handle=None, side='left', edge_a=None, edge_alpha=1, typical_edges_hist2d=None, contacts_table=None, post_contact_linger=50, write_stderr_to_screen=True, input_frame_offset=0, get_extra_text=None, contact_colors=None, also_plot_traces=False, trace_data_x=None, trace_data_y=None, trace_data_kwargs=None, ffmpeg_writer_kwargs=None, f=None, ax=None, func_update_figure=None, whisker_lw=2, whisker_marker=None, ): """Creating a video overlaid with whiskers, contacts, etc. The overall dataflow is this: 1. Load chunks of frames from the input 2. One by one, plot the frame with matplotlib. Overlay whiskers, edges, contacts, whatever. 3. Dump the frame to an ffmpeg writer. # Input and output output_filename : file to create input_reader : PFReader or input video # Timing and spatial parameters frame_triggers : Only plot frames within (trigger_dstart, trigger_dstop) of a value in this array. trigger_dstart, trigger_dstop : number of frames d_temporal : Save time by plotting every Nth frame d_spatial : Save time by spatially undersampling the image The bottleneck is typically plotting the raw image in matplotlib # Video parameters dpi : The output video will always be pixel by pixel the same as the input (keeping d_spatial in mind). But this dpi value affects font and marker size. output_fps : set the frame rate of the output video (ffmpeg -r) input_video_alpha : alpha of image input_frame_offset : If you already seeked this many frames in the input_reader. Thus, now we know that the first frame to be read is actually frame `input_frame_offset` in the source (and thus, in the edge_a, contacts_table, etc.). This is the only parameter you need to adjust in this case, not frame_triggers or anything else. ffmpeg_writer_kwargs : other parameters for FFmpegWriter # Other sources of input edge_alpha : alpha of edge post_contact_linger : How long to leave the contact displayed This is the total duration, so 0 will display nothing, and 1 is minimal. # Misc get_extra_text : if not None, should be a function that accepts a frame number and returns some text to add to the display. This is a "real" frame number after accounting for any offset. contact_colors : list of color specs to use func_update_figure : optional, function that takes the frame number as input and updates the figure """ # We need FFmpegWriter # Probably that object should be moved to my.video # Or maybe a new repo ffmpeg_tricks import WhiskiWrap # Parse the arguments frame_triggers = np.asarray(frame_triggers).astype(np.int) announced_frame_trigger = 0 input_width = int(input_width) input_height = int(input_height) if contact_colors is None: contact_colors = my.plot.generate_colorbar(7) if ffmpeg_writer_kwargs is None: ffmpeg_writer_kwargs = {} ## Set up the graphical handles if verbose: print "setting up handles" if ax is None: # Create a figure with an image that fills it # We want the figsize to be in inches, so divide by dpi # And we want one invisible axis containing an image that fills the whole figure figsize = input_width / float(dpi), input_height / float(dpi) f = plt.figure(frameon=False, dpi=dpi / d_spatial, figsize=figsize) ax = f.add_axes([0, 0, 1, 1]) ax.axis('off') # This return results in pixels, so should be the same as input width # and height. If not, probably rounding error above canvas_width, canvas_height = f.canvas.get_width_height() if \ input_width / d_spatial != canvas_width or \ input_height / d_spatial != canvas_height: raise ValueError("canvas size is not the same as input size") else: assert f is not None # This is used later in creating the writer canvas_width, canvas_height = f.canvas.get_width_height() # Plot typical edge images as static alpha if typical_edges_hist2d is not None: im1 = my.plot.imshow(typical_edges_hist2d, ax=ax, axis_call='image', extent=(0, input_width, input_height, 0), cmap=plt.cm.gray) im1.set_alpha(edge_alpha) # Plot input video frames in_image = np.zeros((input_height, input_width)) im2 = my.plot.imshow(in_image[::d_spatial, ::d_spatial], ax=ax, axis_call='image', cmap=plt.cm.gray, extent=(0, input_width, input_height, 0)) im2.set_alpha(input_video_alpha) im2.set_clim((0, 255)) # Plot contact positions dynamically if contacts_table is not None: contact_positions_l = [] for color in contact_colors: contact_positions_l.append( ax.plot([np.nan], [np.nan], '.', ms=15, color=color)[0]) #~ contact_positions, = ax.plot([np.nan], [np.nan], 'r.', ms=15) else: contact_positions_l = None # Dynamic edge if edge_a is not None: edge_a_obj, = ax.plot([np.nan], [np.nan], '-', color='pink', lw=3) else: edge_a_obj = None # Text of trial if plot_trial_numbers: txt = ax.text(0, ax.get_ylim()[0], 'waiting', size=20, ha='left', va='bottom', color='w') trial_number = -1 # This will hold whisker objects whisker_handles = [] # Create the writer writer = WhiskiWrap.FFmpegWriter( output_filename=output_filename, frame_width=canvas_width, frame_height=canvas_height, output_fps=output_fps, input_pix_fmt='argb', write_stderr_to_screen=write_stderr_to_screen, **ffmpeg_writer_kwargs) ## Loop until input frames exhausted for nnframe, frame in enumerate(input_reader.iter_frames()): # Account for the fact that we skipped the first input_frame_offset frames nframe = nnframe + input_frame_offset # Break if we're past the last trigger if nframe > np.max(frame_triggers) + trigger_dstop: break # Skip if we're not on a dframe if np.mod(nframe, d_temporal) != 0: continue # Skip if we're not near a trial nearest_choice_idx = np.nanargmin(np.abs(frame_triggers - nframe)) nearest_choice = frame_triggers[nearest_choice_idx] if not (nframe > nearest_choice + trigger_dstart and nframe < nearest_choice + trigger_dstop): continue # Announce if ((announced_frame_trigger < len(frame_triggers)) and (nframe > frame_triggers[announced_frame_trigger] + trigger_dstart)): print "Reached trigger for frame", frame_triggers[ announced_frame_trigger] announced_frame_trigger += 1 # Update the trial text if plot_trial_numbers: # and (nearest_choice_idx > trial_number): if get_extra_text is not None: extra_text = get_extra_text(nframe) else: extra_text = '' txt.set_text('frame %d trial %d %s' % (nframe, nearest_choice_idx, extra_text)) trial_number = nearest_choice_idx # Update the frame whisker_handles = frame_update(ax, nframe, frame, whisker_handles, contacts_table, post_contact_linger, whiskers_table, whiskers_file_handle, edge_a, im2, edge_a_obj, contact_positions_l, d_spatial, d_temporal, contact_colors, whisker_lw=whisker_lw, whisker_marker=whisker_marker) if func_update_figure is not None: func_update_figure(nframe) # Write to pipe f.canvas.draw() string_bytes = f.canvas.tostring_argb() writer.write_bytes(string_bytes) ## Clean up if whiskers_file_handle is not None: whiskers_file_handle.close() if not input_reader.isclosed(): input_reader.close() writer.close() plt.close(f)
def interleaved_reading_and_tracing( input_reader, tiffs_to_trace_directory, sensitive=False, chunk_size=200, chunk_name_pattern='chunk%08d.tif', stop_after_frame=None, delete_tiffs=True, timestamps_filename=None, monitor_video=None, monitor_video_kwargs=None, write_monitor_ffmpeg_stderr_to_screen=False, h5_filename=None, frame_func=None, n_trace_processes=4, expectedrows=1000000, verbose=True): """Read, write, and trace each chunk, one at a time. This is an alternative to first calling: write_video_as_chunked_tiffs And then calling trace_chunked_tiffs input_reader : Typically a PFReader or FFmpegReader tiffs_to_trace_directory : Location to write the tiffs sensitive: if False, use default. If True, lower MIN_SIGNAL chunk_size : frames per chunk chunk_name_pattern : how to name them stop_after_frame : break early, for debugging delete_tiffs : whether to delete tiffs after done tracing timestamps_filename : Where to store the timestamps Only vallid for PFReader input_reader monitor_video : filename for a monitor video If None, no monitor video will be written monitor_video_kwargs : kwargs to pass to FFmpegWriter for monitor write_monitor_ffmpeg_stderr_to_screen : whether to display output from ffmpeg writing instance h5_filename : hdf5 file to stitch whiskers information into frame_func : function to apply to each frame If 'invert', will apply 255 - frame n_trace_processes : number of simultaneous trace processes expectedrows : how to set up hdf5 file verbose : verbose Returns: dict trace_pool_results : result of each call to trace monitor_ff_stderr, monitor_ff_stdout : results from monitor video ffmpeg instance """ ## Set up kwargs if monitor_video_kwargs is None: monitor_video_kwargs = {} if frame_func == 'invert': frame_func = lambda frame: 255 - frame # Check commands WhiskiWrap.utils.probe_needed_commands() ## Initialize readers and writers if verbose: print "initalizing readers and writers" # Tiff writer ctw = WhiskiWrap.ChunkedTiffWriter(tiffs_to_trace_directory, chunk_size=chunk_size, chunk_name_pattern=chunk_name_pattern) # FFmpeg writer is initalized after first frame ffw = None # Setup the result file setup_hdf5(h5_filename, expectedrows) # Copy the parameters files copy_parameters_files(tiffs_to_trace_directory, sensitive=sensitive) ## Set up the worker pool # Pool of trace workers trace_pool = multiprocessing.Pool(n_trace_processes) # Keep track of results trace_pool_results = [] deleted_tiffs = [] def log_result(result): trace_pool_results.append(result) ## Iterate over chunks out_of_frames = False nframe = 0 # Init the iterator outside of the loop so that it persists iter_obj = input_reader.iter_frames() while not out_of_frames: # Get a chunk of frames if verbose: print "loading chunk of frames starting with ", nframe chunk_of_frames = [] for frame in iter_obj: if frame_func is not None: frame = frame_func(frame) chunk_of_frames.append(frame) nframe = nframe + 1 if stop_after_frame is not None and nframe >= stop_after_frame: break if len(chunk_of_frames) == chunk_size: break # Check if we ran out if len(chunk_of_frames) != chunk_size: out_of_frames = True ## Write tiffs # We do this synchronously to ensure that it happens before # the trace starts for frame in chunk_of_frames: ctw.write(frame) # Make sure the chunk was written, in case this is the last one # and we didn't reach chunk_size yet if len(chunk_of_frames) != chunk_size: ctw._write_chunk() assert ctw.count_unwritten_frames() == 0 # Figure out which tiff file was just generated tif_filename = ctw.chunknames_written[-1] ## Start trace trace_pool.apply_async(trace_chunk, args=(tif_filename, delete_tiffs), callback=log_result) ## Determine whether we can delete any tiffs #~ if delete_tiffs: #~ tiffs_to_delete = [ #~ tpres['video_filename'] for tpres in trace_pool_results #~ if tpres['video_filename'] not in deleted_tiffs] #~ for filename in tiffs_to_delete: #~ if verbose: #~ print "deleting", filename #~ os.remove(filename) ## Start monitor encode # This is also synchronous, otherwise the input buffer might fill up if monitor_video is not None: if ffw is None: ffw = WhiskiWrap.FFmpegWriter( monitor_video, frame_width=frame.shape[1], frame_height=frame.shape[0], write_stderr_to_screen= write_monitor_ffmpeg_stderr_to_screen, **monitor_video_kwargs) for frame in chunk_of_frames: ffw.write(frame) ## Determine if we should pause while len(ctw.chunknames_written ) > len(trace_pool_results) + 2 * n_trace_processes: print "waiting for tracing to catch up" time.sleep(30) ## Wait for trace to complete if verbose: print "done with reading and writing, just waiting for tracing" # Tell it no more jobs, so close when done trace_pool.close() # Wait for everything to finish trace_pool.join() ## Error check the tifs that were processed # Get the tifs we wrote, and the tifs we trace written_chunks = sorted(ctw.chunknames_written) traced_filenames = sorted( [res['video_filename'] for res in trace_pool_results]) # Check that they are the same if not np.all(np.array(written_chunks) == np.array(traced_filenames)): raise ValueError("not all chunks were traced") ## Extract the chunk numbers from the filenames # The tiffs have been written, figure out which they are split_traced_filenames = [os.path.split(fn)[1] for fn in traced_filenames] tif_file_number_strings = my.misc.apply_and_filter_by_regex( '^chunk(\d+).tif$', split_traced_filenames, sort=False) tif_full_filenames = [ os.path.join(tiffs_to_trace_directory, 'chunk%s.tif' % fns) for fns in tif_file_number_strings ] tif_file_numbers = map(int, tif_file_number_strings) tif_ordering = np.argsort(tif_file_numbers) tif_sorted_filenames = np.array(tif_full_filenames)[tif_ordering] tif_sorted_file_numbers = np.array(tif_file_numbers)[tif_ordering] # stitch print "Stitching" for chunk_start, chunk_name in zip(tif_sorted_file_numbers, tif_sorted_filenames): # Append each chunk to the hdf5 file fn = WhiskiWrap.utils.FileNamer.from_tiff_stack(chunk_name) append_whiskers_to_hdf5(whisk_filename=fn.whiskers, h5_filename=h5_filename, chunk_start=chunk_start) # Finalize writers ctw.close() if ffw is not None: ff_stdout, ff_stderr = ffw.close() else: ff_stdout, ff_stderr = None, None # Also write timestamps as numpy file if hasattr(input_reader, 'timestamps') and timestamps_filename is not None: timestamps = np.concatenate(input_reader.timestamps) assert len(timestamps) >= ctw.frames_written np.save(timestamps_filename, timestamps[:ctw.frames_written]) return { 'trace_pool_results': trace_pool_results, 'monitor_ff_stdout': ff_stdout, 'monitor_ff_stderr': ff_stderr, }