def test_get_av_info_non_multimedia_file_fails(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_non_multimedia_file_fails') with open(testfile, 'w'): pass with self.assertRaises(RuntimeError): avinfo.get_av_info(testfile)
def __init__(self, src, dest, sequence, rate, optflow_fn, interpolate_fn, w, h, scaling_method, lossless, keep_subregions, show_preview, add_info, text_type, mark_frames, mux): self.src = src self.dest = dest self.sequence = sequence self.rate = rate self.optflow_fn = optflow_fn self.interpolate_fn = interpolate_fn self.w = w self.h = h self.scaling_method = scaling_method self.lossless = lossless self.keep_subregions = keep_subregions self.show_preview = show_preview self.add_info = add_info self.text_type = text_type self.mark_frames = mark_frames self.mux = mux self.pipe = None self.fr_source = None self.av_info = avinfo.get_av_info(src) self.source_frs = 0 self.frs_interpolated = 0 self.frs_duped = 0 self.frs_dropped = 0 self.frs_written = 0 self.subs_to_render = 0 self.frs_to_render = 0 self.curr_sub_idx = 0 self.window_title = os.path.basename(self.src) + ' - Butterflow' self.progress = 0
def test_get_av_info_video_duration_rate_frames_fractional_rate(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_video_fractional_rate.mp4') mk_sample_video(testfile, 3, 640, 360, fractions.Fraction(30000, 1001)) av = avinfo.get_av_info(testfile) self.assertEqual(av['duration'], 3.003*1000) self.assertEqual(av['rate_n'], 30*1000.0) self.assertEqual(av['rate_d'], 1001) self.assertEqual(av['frames'], 30*1000.0/1001 * 3.003)
def test_get_av_info_video_duration_rate_frames_rational_rate(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_video_rational_rate.mp4') mk_sample_video(testfile, 2, 640, 360, fractions.Fraction(24, 1)) av = avinfo.get_av_info(testfile) self.assertEqual(av['duration'], 2 * 1000) self.assertEqual(av['rate_n'], 24) self.assertEqual(av['rate_d'], 1) self.assertEqual(av['frames'], 24 * 2)
def test_get_av_info_video_duration_rate_frames_fractional_rate(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_video_fractional_rate.mp4') mk_sample_video(testfile, 3, 640, 360, fractions.Fraction(30000, 1001)) av = avinfo.get_av_info(testfile) self.assertEqual(av['duration'], 3.003 * 1000) self.assertEqual(av['rate_n'], 30 * 1000.0) self.assertEqual(av['rate_d'], 1001) self.assertEqual(av['frames'], 30 * 1000.0 / 1001 * 3.003)
def test_get_av_info_video_duration_rate_frames_rational_rate(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_video_rational_rate.mp4') mk_sample_video(testfile, 2, 640, 360, fractions.Fraction(24, 1)) av = avinfo.get_av_info(testfile) self.assertEqual(av['duration'], 2*1000) self.assertEqual(av['rate_n'], 24) self.assertEqual(av['rate_d'], 1) self.assertEqual(av['frames'], 24*2)
def test_get_av_info_w_h_sar_unknown(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_w_h_sar_unknown.mp4') mk_sample_video(testfile, 1, 640, 360, fractions.Fraction(1, 1)) av = avinfo.get_av_info(testfile) self.assertEqual(av['w'], 640) self.assertEqual(av['h'], 360) self.assertEqual(av['sar_n'], 1) # assume sar is 1:1 self.assertEqual(av['sar_d'], 1) w_h = fractions.Fraction(640, 360) # dar is just the aspect ratio of # the pixel aspect ratio if sar is 1:1 self.assertEqual(av['dar_n'], w_h.numerator) self.assertEqual(av['dar_d'], w_h.denominator)
def test_get_av_info_w_h_sar_known_dar_known(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_w_h_sar_known_dar_known.mp4') sar = fractions.Fraction(4, 3) dar = fractions.Fraction(16, 9) mk_sample_video(testfile, 1, 320, 240, fractions.Fraction(1, 1), sar=sar, dar=dar) av = avinfo.get_av_info(testfile) self.assertEqual(av['w'], 320) self.assertEqual(av['h'], 240) self.assertEqual(av['sar_n'], sar.numerator) self.assertEqual(av['sar_d'], sar.denominator) self.assertEqual(av['dar_n'], dar.numerator) self.assertEqual(av['dar_d'], dar.denominator)
def test_get_av_info_w_h_sar_known_dar_known(self): testfile = os.path.join(settings['tempdir'], 'test_get_av_info_w_h_sar_known_dar_known.mp4') sar = fractions.Fraction(4, 3) dar = fractions.Fraction(16, 9) mk_sample_video(testfile, 1, 320, 240, fractions.Fraction(1, 1), sar=sar, dar=dar) av = avinfo.get_av_info(testfile) self.assertEqual(av['w'], 320) self.assertEqual(av['h'], 240) self.assertEqual(av['sar_n'], sar.numerator) self.assertEqual(av['sar_d'], sar.denominator) self.assertEqual(av['dar_n'], dar.numerator) self.assertEqual(av['dar_d'], dar.denominator)
def test_get_av_info_w_h_sar_known_dar_unknown(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_w_h_sar_known_dar_unknown.mp4') sar = fractions.Fraction(32, 27) mk_sample_video(testfile, 1, 714, 458, fractions.Fraction(1, 1), sar=sar) av = avinfo.get_av_info(testfile) self.assertEqual(av['w'], 714) self.assertEqual(av['h'], 458) self.assertEqual(av['sar_n'], sar.numerator) self.assertEqual(av['sar_d'], sar.denominator) dar = fractions.Fraction(714*32, 458*27) # use fractions.Fraction to # reduce the fraction, if needed self.assertEqual(av['dar_n'], dar.numerator) self.assertEqual(av['dar_d'], dar.denominator)
def test_get_av_info_stream_exists(self): videofile = os.path.join(settings['tmp_dir'], '~test_get_av_info_stream_exists.mp4') mk_sample_video(videofile, 1, 320, 240, fractions.Fraction(1, 1)) av = avinfo.get_av_info(videofile) self.assertTrue(av['v_stream_exists']) self.assertFalse(av['a_stream_exists']) self.assertFalse(av['s_stream_exists']) wavfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_stream_exists.wav') mk_sample_wav_file(wavfile, 1) av = avinfo.get_av_info(wavfile) self.assertTrue(av['a_stream_exists']) self.assertFalse(av['v_stream_exists']) self.assertFalse(av['s_stream_exists']) muxedfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_stream_exists.muxed.mp4') mux_video_with_files(muxedfile, videofile, wavfile) av = avinfo.get_av_info(muxedfile) self.assertTrue(av['v_stream_exists']) self.assertTrue(av['a_stream_exists']) self.assertFalse(av['s_stream_exists'])
def test_get_av_info_stream_exists(self): videofile = os.path.join(settings['tmp_dir'], '~test_get_av_info_stream_exists.mp4') mk_sample_video(videofile, 1, 320, 240, fractions.Fraction(1, 1)) av = avinfo.get_av_info(videofile) self.assertTrue( av['v_stream_exists']) self.assertFalse(av['a_stream_exists']) self.assertFalse(av['s_stream_exists']) wavfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_stream_exists.wav') mk_sample_wav_file(wavfile, 1) av = avinfo.get_av_info(wavfile) self.assertTrue(av['a_stream_exists']) self.assertFalse(av['v_stream_exists']) self.assertFalse(av['s_stream_exists']) muxedfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_stream_exists.muxed.mp4') mux_video_with_files(muxedfile, videofile, wavfile) av = avinfo.get_av_info(muxedfile) self.assertTrue( av['v_stream_exists']) self.assertTrue( av['a_stream_exists']) self.assertFalse(av['s_stream_exists'])
def test_get_av_info_w_h_sar_known_dar_unknown(self): testfile = os.path.join(settings['tempdir'], 'test_get_av_info_w_h_sar_known_dar_unknown.mp4') sar = fractions.Fraction(32, 27) mk_sample_video(testfile, 1, 714, 458, fractions.Fraction(1, 1), sar=sar) av = avinfo.get_av_info(testfile) self.assertEqual(av['w'], 714) self.assertEqual(av['h'], 458) self.assertEqual(av['sar_n'], sar.numerator) self.assertEqual(av['sar_d'], sar.denominator) dar = fractions.Fraction(714*32, 458*27) # use fractions.Fraction to # simplify the fraction, if needed self.assertEqual(av['dar_n'], dar.numerator) self.assertEqual(av['dar_d'], dar.denominator)
def test_get_av_info_multimedia_file_no_video_stream(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_multimedia_file_no_video_stream.wav') mk_sample_wav_file(testfile, 1) av = avinfo.get_av_info(testfile) self.assertEqual(av['path'], testfile) self.assertFalse(av['v_stream_exists']) self.assertEqual(av['w'], 0) self.assertEqual(av['h'], 0) self.assertEqual(av['sar_n'], 0) self.assertEqual(av['sar_d'], 0) self.assertEqual(av['dar_n'], 0) self.assertEqual(av['dar_d'], 0) self.assertEqual(av['duration'], 0) self.assertEqual(av['rate_n'], 0) self.assertEqual(av['rate_d'], 0) self.assertEqual(av['frames'], 0)
def test_get_av_info_multimedia_file_no_video_stream(self): testfile = os.path.join(settings['tempdir'], 'test_get_av_info_multimedia_file_no_video_stream.wav') mk_sample_wav_file(testfile, 1) av = avinfo.get_av_info(testfile) self.assertEqual(av['path'], testfile) self.assertFalse(av['v_stream_exists']) self.assertEqual(av['w'], 0) self.assertEqual(av['h'], 0) self.assertEqual(av['sar_n'], 0) self.assertEqual(av['sar_d'], 0) self.assertEqual(av['dar_n'], 0) self.assertEqual(av['dar_d'], 0) self.assertEqual(av['duration'], 0) self.assertEqual(av['rate_n'], 0) self.assertEqual(av['rate_d'], 0) self.assertEqual(av['frames'], 0)
def __init__(self, src, dst, sequence, rate, flow_fn, inter_fn, w, h, lossless, trim_regions, show_preview, add_info, text_type, mark_frames, mux): self.src = src self.dst = dst self.sequence = sequence self.rate = rate self.flow_fn = flow_fn self.inter_fn = inter_fn self.w = w self.h = h self.lossless = lossless self.trim_regions = trim_regions self.show_preview = show_preview self.add_info = add_info self.text_type = text_type self.mark_frames = mark_frames self.mux = mux self.render_pipe = None self.fr_source = None self.av_info = avinfo.get_av_info(src) self.scaling_method = None new_res = w * h src_res = self.av_info['w'] * self.av_info['h'] if new_res < src_res: self.scaling_method = settings['scaler_dn'] elif new_res > src_res: self.scaling_method = settings['scaler_up'] self.preview_win_title = os.path.basename(self.src) + ' - Butterflow' self.tot_src_frs = 0 self.tot_frs_int = 0 self.tot_frs_dup = 0 self.tot_frs_drp = 0 self.tot_frs_wrt = 0 self.tot_tgt_frs = 0 self.subs_to_render = 0 self.curr_sub_idx = 0
def main(): par = argparse.ArgumentParser(usage='butterflow [options] [video]', add_help=False) req = par.add_argument_group('Required arguments') gen = par.add_argument_group('General options') dsp = par.add_argument_group('Display options') vid = par.add_argument_group('Video options') mux = par.add_argument_group('Muxing options') fgr = par.add_argument_group('Advanced options') req.add_argument('video', type=str, nargs='?', default=None, help='Specify the input video') gen.add_argument('-h', '--help', action='help', help='Show this help message and exit') gen.add_argument('--version', action='store_true', help='Show program\'s version number and exit') gen.add_argument('-d', '--devices', action='store_true', help='Show detected OpenCL devices and exit') gen.add_argument('-sw', action='store_true', help='Set to force software rendering') gen.add_argument('-c', '--cache', action='store_true', help='Show cache information and exit') gen.add_argument('--rm-cache', action='store_true', help='Set to clear the cache and exit') gen.add_argument('-prb', '--probe', action='store_true', help='Show media file information and exit') gen.add_argument('-v', '--verbose', action='store_true', help='Set to increase output verbosity') dsp.add_argument('-np', '--no-preview', action='store_false', help='Set to disable video preview') dsp.add_argument('-a', '--add-info', action='store_true', help='Set to embed debugging info into the output ' 'video') dsp.add_argument('-tt', '--text-type', choices=['light', 'dark', 'stroke'], default=settings.default['text_type'], help='Specify text type for debugging info, ' '(default: %(default)s)') dsp.add_argument('-mrk', '--mark-frames', action='store_true', help='Set to mark interpolated frames') vid.add_argument('-o', '--output-path', type=str, default=settings.default['out_path'], help='Specify path to the output video') vid.add_argument('-r', '--playback-rate', type=str, help='Specify the playback rate as an integer or a ' 'float. Fractional forms are acceptable. To use a ' 'multiple of the source video\'s rate, follow a number ' 'with `x`, e.g., "2x" will double the frame rate. The ' 'original rate will be used by default if nothing is ' 'specified.') vid.add_argument('-s', '--sub-regions', type=str, help='Specify rendering subregions in the form: ' '"a=TIME,b=TIME,TARGET=VALUE" where TARGET is either ' '`fps`, `dur`, `spd`. Valid TIME syntaxes are [hr:m:s], ' '[m:s], [s], [s.xxx], or `end`, which signifies to the ' 'end the video. You can specify multiple subregions by ' 'separating them with a colon `:`. A special region ' 'format that conveniently describes the entire clip is ' 'available in the form: "full,TARGET=VALUE".') vid.add_argument('-t', '--trim-regions', action='store_true', help='Set to trim subregions that are not explicitly ' 'specified') vid.add_argument('-vs', '--video-scale', type=str, default=str(settings.default['video_scale']), help='Specify output video size in the form: ' '"WIDTH:HEIGHT" or by using a factor. To keep the ' 'aspect ratio only specify one component, either width ' 'or height, and set the other component to -1, ' '(default: %(default)s)') vid.add_argument('-l', '--lossless', action='store_true', help='Set to use lossless encoding settings') vid.add_argument('-sm', '--smooth-motion', action='store_true', help='Set to tune for smooth motion. This mode favors ' 'accurate and artifact-less frames above all and will ' 'emphasize blending frames over warping pixels.') mux.add_argument('-mux', action='store_true', help='Set to mux the source audio with the output video') fgr.add_argument('--fast-pyr', action='store_true', help='Set to use fast pyramids') fgr.add_argument('--pyr-scale', type=float, default=settings.default['pyr_scale'], help='Specify pyramid scale factor, ' '(default: %(default)s)') fgr.add_argument('--levels', type=int, default=settings.default['levels'], help='Specify number of pyramid layers, ' '(default: %(default)s)') fgr.add_argument('--winsize', type=int, default=settings.default['winsize'], help='Specify averaging window size, ' '(default: %(default)s)') fgr.add_argument('--iters', type=int, default=settings.default['iters'], help='Specify number of iterations at each pyramid ' 'level, (default: %(default)s)') fgr.add_argument('--poly-n', type=int, choices=settings.default['poly_n_choices'], default=settings.default['poly_n'], help='Specify size of pixel neighborhood, ' '(default: %(default)s)') fgr.add_argument('--poly-s', type=float, default=settings.default['poly_s'], help='Specify standard deviation to smooth derivatives, ' '(default: %(default)s)') fgr.add_argument('-ff', '--flow-filter', choices=['box', 'gaussian'], default=settings.default['flow_filter'], help='Specify which filter to use for optical flow ' 'estimation, (default: %(default)s)') # add a space to args that start with a `-` char to avoid an unexpected # argument error. needed for the `--video-scale` option for i, arg in enumerate(sys.argv): if (arg[0] == '-') and arg[1].isdigit(): sys.argv[i] = ' ' + arg args = par.parse_args() import logging logging.basicConfig(level=settings.default['loglevel_a'], format='%(levelname)-7s: %(message)s') log = logging.getLogger('butterflow') if args.verbose: log.setLevel(settings.default['loglevel_b']) if args.version: from butterflow.__init__ import __version__ print('butterflow version %s' % __version__) return 0 if args.cache: print_cache_info() return 0 if args.rm_cache: rm_cache() print('cache deleted, done.') return 0 if args.devices: ocl.print_ocl_devices() return 0 if args.video is None: print(NO_VIDEO_SPECIFIED) return 1 if not os.path.exists(args.video): print('Error: file does not exist at path') return 1 if args.probe: if args.video: try: avinfo.print_av_info(args.video) except Exception as e: print('Error: %s' % e) else: print(NO_VIDEO_SPECIFIED) return 0 try: vid_info = avinfo.get_av_info(args.video) except Exception as e: print('Error: %s' % e) return 1 if not vid_info['v_stream_exists']: print('Error: no video stream detected') return 1 try: sequence = sequence_from_str(vid_info['duration'], vid_info['frames'], args.sub_regions) except Exception as e: print('Bad subregion string: %s' % e) return 1 src_rate = (vid_info['rate_n'] * 1.0 / vid_info['rate_d']) try: rate = rate_from_str(args.playback_rate, src_rate) except Exception as e: print('Bad playback rate: %s' % e) return 1 if rate < src_rate: log.warning('rate=%s < src_rate=%s', rate, src_rate) try: w, h = w_h_from_str(args.video_scale, vid_info['w'], vid_info['h']) except Exception as e: print('Bad video scale: %s' % e) return 1 have_ocl = ocl.compat_ocl_device_available() use_sw = args.sw or not have_ocl if use_sw: log.warning('not using opencl accelerated methods') # make functions that will generate flows and interpolate frames from cv2 import calcOpticalFlowFarneback as sw_optical_flow farneback_method = sw_optical_flow if use_sw else \ motion.ocl_farneback_optical_flow flags = 0 if args.flow_filter == 'gaussian': import cv2 flags = cv2.OPTFLOW_FARNEBACK_GAUSSIAN if args.smooth_motion: args.poly_s = 0.01 # don't make the function with `lambda` because `draw_debug_text` will need # to retrieve kwargs with the `inspect` module def flow_function(x, y, pyr=args.pyr_scale, l=args.levels, w=args.winsize, i=args.iters, polyn=args.poly_n, polys=args.poly_s, fast=args.fast_pyr, filt=flags): if farneback_method == motion.ocl_farneback_optical_flow: return farneback_method(x, y, pyr, l, w, i, polyn, polys, fast, filt) else: return farneback_method(x, y, pyr, l, w, i, polyn, polys, filt) from butterflow.interpolate import sw_interpolate_flow interpolate_function = sw_interpolate_flow if use_sw else \ motion.ocl_interpolate_flow renderer = Renderer( args.output_path, vid_info, sequence, rate, flow_function, interpolate_function, w, h, args.lossless, args.trim_regions, args.no_preview, args.add_info, args.text_type, args.mark_frames, args.mux) motion.set_num_threads(settings.default['ocv_threads']) try: # time how long it takes to render. timeit will turn off gc, must turn # it back on to maximize performance re-nable it in the setup function import timeit tot_time = timeit.timeit(renderer.render, setup='import gc;gc.enable()', number=1) # only run once tot_time /= 60 # time in minutes new_sz = human_sz(float(os.path.getsize(args.output_path))) print('{} real, {} interpolated, {} duped, {} dropped'.format( renderer.tot_src_frs, renderer.tot_frs_int, renderer.tot_frs_dup, renderer.tot_frs_drp)) print('write ratio: {}/{}, ({:.2f}%) {}'.format( renderer.tot_frs_wrt, renderer.tot_tgt_frs, renderer.tot_frs_wrt * 100.0 / renderer.tot_tgt_frs, new_sz)) print('butterflow took {:.3g} minutes, done.'.format(tot_time)) except (KeyboardInterrupt, SystemExit): log.warning('stopped early, files were left in the cache') log.warning('recoverable @ {}'.format(settings.default['tmp_dir'])) return 1 except Exception: return 1
def test_n_frames_same_as_avinfo(self): av = avinfo.get_av_info(self.src_3.path) self.assertEqual(self.src_3.frames, av['frames'])
def main(): par = argparse.ArgumentParser(usage='butterflow [options] [video]', add_help=False) req = par.add_argument_group('Required arguments') gen = par.add_argument_group('General options') dsp = par.add_argument_group('Display options') vid = par.add_argument_group('Video options') mux = par.add_argument_group('Muxing options') fgr = par.add_argument_group('Advanced options') req.add_argument('video', type=str, nargs='?', default=None, help='Specify the input video') gen.add_argument('-h', '--help', action='help', help='Show this help message and exit') gen.add_argument('--version', action='store_true', help='Show program\'s version number and exit') gen.add_argument('-d', '--devices', action='store_true', help='Show detected OpenCL devices and exit') gen.add_argument('-sw', action='store_true', help='Set to force software rendering') gen.add_argument('-c', '--cache', action='store_true', help='Show cache information and exit') gen.add_argument('--rm-cache', action='store_true', help='Set to clear the cache and exit') gen.add_argument('-prb', '--probe', action='store_true', help='Show media file information and exit') gen.add_argument('-v', '--verbose', action='store_true', help='Set to increase output verbosity') dsp.add_argument('-np', '--no-preview', action='store_false', help='Set to disable video preview') dsp.add_argument('-a', '--add-info', action='store_true', help='Set to embed debugging info into the output video') dsp.add_argument('-tt', '--text-type', choices=['light', 'dark', 'stroke'], default=settings['text_type'], help='Specify text type for debugging info, ' '(default: %(default)s)') dsp.add_argument('-mrk', '--mark-frames', action='store_true', help='Set to mark interpolated frames') vid.add_argument('-o', '--output-path', type=str, default=settings['out_path'], help='Specify path to the output video') vid.add_argument('-r', '--playback-rate', type=str, help='Specify the playback rate as an integer or a ' 'float. Fractional forms are acceptable. To use a ' 'multiple of the source video\'s rate, follow a number ' 'with `x`, e.g., "2x" will double the frame rate. The ' 'original rate will be used by default if nothing is ' 'specified.') vid.add_argument('-s', '--subregions', type=str, help='Specify rendering subregions in the form: ' '"a=TIME,b=TIME,TARGET=VALUE" where TARGET is either ' '`fps`, `dur`, `spd`. Valid TIME syntaxes are [hr:m:s], ' '[m:s], [s], [s.xxx], or `end`, which signifies to the ' 'end the video. You can specify multiple subregions by ' 'separating them with a colon `:`. A special subregion ' 'format that conveniently describes the entire clip is ' 'available in the form: "full,TARGET=VALUE".') vid.add_argument('-t', '--trim-subregions', action='store_true', help='Set to trim subregions that are not explicitly ' 'specified') vid.add_argument('-vs', '--video-scale', type=str, default=str(settings['video_scale']), help='Specify output video size in the form: ' '"WIDTH:HEIGHT" or by using a factor. To keep the ' 'aspect ratio only specify one component, either width ' 'or height, and set the other component to -1, ' '(default: %(default)s)') vid.add_argument('-l', '--lossless', action='store_true', help='Set to use lossless encoding settings') vid.add_argument('-sm', '--smooth-motion', action='store_true', help='Set to tune for smooth motion. This mode favors ' 'accurate and artifact-less frames above all and will ' 'emphasize blending frames over warping pixels.') mux.add_argument('-mux', action='store_true', help='Set to mux the source audio with the output video') fgr.add_argument('--fast-pyr', action='store_true', help='Set to use fast pyramids') fgr.add_argument('--pyr-scale', type=float, default=settings['pyr_scale'], help='Specify pyramid scale factor, ' '(default: %(default)s)') fgr.add_argument('--levels', type=int, default=settings['levels'], help='Specify number of pyramid layers, ' '(default: %(default)s)') fgr.add_argument('--winsize', type=int, default=settings['winsize'], help='Specify averaging window size, ' '(default: %(default)s)') fgr.add_argument('--iters', type=int, default=settings['iters'], help='Specify number of iterations at each pyramid ' 'level, (default: %(default)s)') fgr.add_argument('--poly-n', type=int, choices=settings['poly_n_choices'], default=settings['poly_n'], help='Specify size of pixel neighborhood, ' '(default: %(default)s)') fgr.add_argument('--poly-s', type=float, default=settings['poly_s'], help='Specify standard deviation to smooth derivatives, ' '(default: %(default)s)') fgr.add_argument('-ff', '--flow-filter', choices=['box', 'gaussian'], default=settings['flow_filter'], help='Specify which filter to use for optical flow ' 'estimation, (default: %(default)s)') for i, arg in enumerate(sys.argv): if arg[0] == '-' and arg[1].isdigit(): # accept args w/ - for -vs sys.argv[i] = ' ' + arg args = par.parse_args() logging.basicConfig(level=settings['loglevel_a'], format='[butterflow.%(levelname)s]: %(message)s') log = logging.getLogger('butterflow') if args.verbose: log.setLevel(settings['loglevel_b']) if args.version: from butterflow.__init__ import __version__ print('butterflow version {}'.format(__version__)) return 0 cachedir = settings['tempdir'] if args.cache: nfiles = 0 sz = 0 for dirpath, dirnames, fnames in os.walk(cachedir): if dirpath == settings['clbdir']: continue for fname in fnames: nfiles += 1 fp = os.path.join(dirpath, fname) sz += os.path.getsize(fp) sz = sz / 1024.0**2 print('{} files, {:.2f} MB'.format(nfiles, sz)) print('cache @ ' + cachedir) return 0 if args.rm_cache: if os.path.exists(cachedir): import shutil shutil.rmtree(cachedir) print('cache deleted, done.') return 0 if args.devices: ocl.print_ocl_devices() return 0 if not args.video: print('no file specified') return 1 elif not os.path.exists(args.video): print('file does not exist') return 1 if args.probe: avinfo.print_av_info(args.video) return 0 av_info = avinfo.get_av_info(args.video) if args.flow_filter == 'gaussian': args.flow_filter = cv2.OPTFLOW_FARNEBACK_GAUSSIAN else: args.flow_filter = 0 if args.smooth_motion: args.polys = 0.01 use_sw_inter = args.sw or not ocl.compat_ocl_device_available() if use_sw_inter: log.warn('not using opencl, ctrl+c to quit') def flow_fn(x, y, pyr=args.pyr_scale, levels=args.levels, winsize=args.winsize, iters=args.iters, polyn=args.poly_n, polys=args.poly_s, fast=args.fast_pyr, filt=args.flow_filter): if use_sw_inter: return cv2.calcOpticalFlowFarneback(x, y, pyr, levels, winsize, iters, polyn, polys, filt) else: return motion.ocl_farneback_optical_flow(x, y, pyr, levels, winsize, iters, polyn, polys, fast, filt) inter_fn = None if use_sw_inter: from butterflow.interpolate import sw_interpolate_flow inter_fn = sw_interpolate_flow else: inter_fn = motion.ocl_interpolate_flow w, h = w_h_from_input_str(args.video_scale, av_info['w'], av_info['h']) def mk_even(x): return x & ~1 w = mk_even(w) h = mk_even(h) rnd = Renderer( args.video, args.output_path, sequence_from_input_str(args.subregions, av_info['duration'], av_info['frames']), rate_from_input_str(args.playback_rate, av_info['rate']), flow_fn, inter_fn, w, h, args.lossless, args.trim_subregions, args.no_preview, args.add_info, args.text_type, args.mark_frames, args.mux) motion.set_num_threads(settings['ocv_threads']) log.info('Will render:\n' + str(rnd.sequence)) success = True total_time = 0 try: import timeit total_time = timeit.timeit(rnd.render_video, setup='import gc;gc.enable()', number=1) except (KeyboardInterrupt, SystemExit): success = False if success: log.debug('Made: ' + args.output_path) out_sz = os.path.getsize(args.output_path) / 1024.0**2 log.debug('Write ratio: {}/{}, ({:.2f}%) {:.2f} MB'.format( rnd.tot_frs_wrt, rnd.tot_tgt_frs, rnd.tot_frs_wrt * 100.0 / rnd.tot_tgt_frs, out_sz)) print('Frames: {} real, {} interpolated, {} duped, {} dropped'.format( rnd.tot_src_frs, rnd.tot_frs_int, rnd.tot_frs_dup, rnd.tot_frs_drp)) print('butterflow took {:.3g} mins, done.'.format(total_time / 60)) return 0 else: log.warn('files left in cache @ ' + settings['tempdir']) return 1
def main(): par = argparse.ArgumentParser(usage='butterflow [options] [video]', add_help=False) req = par.add_argument_group('Required arguments') gen = par.add_argument_group('General options') dsp = par.add_argument_group('Display options') vid = par.add_argument_group('Video options') mux = par.add_argument_group('Muxing options') fgr = par.add_argument_group('Advanced options') req.add_argument('video', type=str, nargs='?', default=None, help='Specify the input video') gen.add_argument('-h', '--help', action='help', help='Show this help message and exit') gen.add_argument('--version', action='store_true', help='Show program\'s version number and exit') gen.add_argument('-d', '--devices', action='store_true', help='Show detected OpenCL devices and exit') gen.add_argument('-sw', action='store_true', help='Set to force software rendering') gen.add_argument('-c', '--cache', action='store_true', help='Show cache information and exit') gen.add_argument('--rm-cache', action='store_true', help='Set to clear the cache and exit') gen.add_argument('-prb', '--probe', action='store_true', help='Show media file information and exit') gen.add_argument('-v', '--verbose', action='store_true', help='Set to increase output verbosity') dsp.add_argument('-np', '--no-preview', action='store_false', help='Set to disable video preview') dsp.add_argument('-a', '--add-info', action='store_true', help='Set to embed debugging info into the output ' 'video') dsp.add_argument('-tt', '--text-type', choices=['light', 'dark', 'stroke'], default=settings.default['text_type'], help='Specify text type for debugging info, ' '(default: %(default)s)') dsp.add_argument('-mrk', '--mark-frames', action='store_true', help='Set to mark interpolated frames') vid.add_argument('-o', '--output-path', type=str, default=settings.default['out_path'], help='Specify path to the output video') vid.add_argument('-r', '--playback-rate', type=str, help='Specify the playback rate as an integer or a ' 'float. Fractional forms are acceptable. To use a ' 'multiple of the source video\'s rate, follow a number ' 'with `x`, e.g., "2x" will double the frame rate. The ' 'original rate will be used by default if nothing is ' 'specified.') vid.add_argument('-s', '--sub-regions', type=str, help='Specify rendering subregions in the form: ' '"a=TIME,b=TIME,TARGET=VALUE" where TARGET is either ' '`fps`, `dur`, `spd`. Valid TIME syntaxes are [hr:m:s], ' '[m:s], [s], [s.xxx], or `end`, which signifies to the ' 'end the video. You can specify multiple subregions by ' 'separating them with a colon `:`. A special region ' 'format that conveniently describes the entire clip is ' 'available in the form: "full,TARGET=VALUE".') vid.add_argument('-t', '--trim-regions', action='store_true', help='Set to trim subregions that are not explicitly ' 'specified') vid.add_argument('-vs', '--video-scale', type=str, default=str(settings.default['video_scale']), help='Specify output video size in the form: ' '"WIDTH:HEIGHT" or by using a factor. To keep the ' 'aspect ratio only specify one component, either width ' 'or height, and set the other component to -1, ' '(default: %(default)s)') vid.add_argument('-l', '--lossless', action='store_true', help='Set to use lossless encoding settings') vid.add_argument('-sm', '--smooth-motion', action='store_true', help='Set to tune for smooth motion. This mode favors ' 'accurate and artifact-less frames above all and will ' 'emphasize blending frames over warping pixels.') mux.add_argument('-mux', action='store_true', help='Set to mux the source audio with the output video') fgr.add_argument('--fast-pyr', action='store_true', help='Set to use fast pyramids') fgr.add_argument('--pyr-scale', type=float, default=settings.default['pyr_scale'], help='Specify pyramid scale factor, ' '(default: %(default)s)') fgr.add_argument('--levels', type=int, default=settings.default['levels'], help='Specify number of pyramid layers, ' '(default: %(default)s)') fgr.add_argument('--winsize', type=int, default=settings.default['winsize'], help='Specify averaging window size, ' '(default: %(default)s)') fgr.add_argument('--iters', type=int, default=settings.default['iters'], help='Specify number of iterations at each pyramid ' 'level, (default: %(default)s)') fgr.add_argument('--poly-n', type=int, choices=settings.default['poly_n_choices'], default=settings.default['poly_n'], help='Specify size of pixel neighborhood, ' '(default: %(default)s)') fgr.add_argument('--poly-s', type=float, default=settings.default['poly_s'], help='Specify standard deviation to smooth derivatives, ' '(default: %(default)s)') fgr.add_argument('-ff', '--flow-filter', choices=['box', 'gaussian'], default=settings.default['flow_filter'], help='Specify which filter to use for optical flow ' 'estimation, (default: %(default)s)') # add a space to args that start with a `-` char to avoid an unexpected # argument error. needed for the `--video-scale` option for i, arg in enumerate(sys.argv): if (arg[0] == '-') and arg[1].isdigit(): sys.argv[i] = ' ' + arg args = par.parse_args() import logging logging.basicConfig(level=settings.default['loglevel_a'], format='%(levelname)-7s: %(message)s') log = logging.getLogger('butterflow') if args.verbose: log.setLevel(settings.default['loglevel_b']) if args.version: from butterflow.__init__ import __version__ print('butterflow version %s' % __version__) return 0 if args.cache: print_cache_info() return 0 if args.rm_cache: rm_cache() print('cache deleted, done.') return 0 if args.devices: ocl.print_ocl_devices() return 0 if args.video is None: print(NO_VIDEO_SPECIFIED) return 1 if not os.path.exists(args.video): print('Error: file does not exist at path') return 1 if args.probe: if args.video: try: avinfo.print_av_info(args.video) except Exception as e: print('Error: %s' % e) else: print(NO_VIDEO_SPECIFIED) return 0 try: vid_info = avinfo.get_av_info(args.video) except Exception as e: print('Error: %s' % e) return 1 if not vid_info['v_stream_exists']: print('Error: no video stream detected') return 1 try: sequence = sequence_from_str(vid_info['duration'], vid_info['frames'], args.sub_regions) except Exception as e: print('Bad subregion string: %s' % e) return 1 src_rate = (vid_info['rate_n'] * 1.0 / vid_info['rate_d']) try: rate = rate_from_str(args.playback_rate, src_rate) except Exception as e: print('Bad playback rate: %s' % e) return 1 if rate < src_rate: log.warning('rate=%s < src_rate=%s', rate, src_rate) try: w, h = w_h_from_str(args.video_scale, vid_info['w'], vid_info['h']) except Exception as e: print('Bad video scale: %s' % e) return 1 have_ocl = ocl.compat_ocl_device_available() use_sw = args.sw or not have_ocl if use_sw: log.warning('not using opencl accelerated methods') # make functions that will generate flows and interpolate frames from cv2 import calcOpticalFlowFarneback as sw_optical_flow farneback_method = sw_optical_flow if use_sw else \ motion.ocl_farneback_optical_flow flags = 0 if args.flow_filter == 'gaussian': import cv2 flags = cv2.OPTFLOW_FARNEBACK_GAUSSIAN if args.smooth_motion: args.poly_s = 0.01 # don't make the function with `lambda` because `draw_debug_text` will need # to retrieve kwargs with the `inspect` module def flow_function(x, y, pyr=args.pyr_scale, l=args.levels, w=args.winsize, i=args.iters, polyn=args.poly_n, polys=args.poly_s, fast=args.fast_pyr, filt=flags): if farneback_method == motion.ocl_farneback_optical_flow: return farneback_method(x, y, pyr, l, w, i, polyn, polys, fast, filt) else: return farneback_method(x, y, pyr, l, w, i, polyn, polys, filt) from butterflow.interpolate import sw_interpolate_flow interpolate_function = sw_interpolate_flow if use_sw else \ motion.ocl_interpolate_flow renderer = Renderer(args.output_path, vid_info, sequence, rate, flow_function, interpolate_function, w, h, args.lossless, args.trim_regions, args.no_preview, args.add_info, args.text_type, args.mark_frames, args.mux) motion.set_num_threads(settings.default['ocv_threads']) try: # time how long it takes to render. timeit will turn off gc, must turn # it back on to maximize performance re-nable it in the setup function import timeit tot_time = timeit.timeit(renderer.render, setup='import gc;gc.enable()', number=1) # only run once tot_time /= 60 # time in minutes new_sz = human_sz(float(os.path.getsize(args.output_path))) print('{} real, {} interpolated, {} duped, {} dropped'.format( renderer.tot_src_frs, renderer.tot_frs_int, renderer.tot_frs_dup, renderer.tot_frs_drp)) print('write ratio: {}/{}, ({:.2f}%) {}'.format( renderer.tot_frs_wrt, renderer.tot_tgt_frs, renderer.tot_frs_wrt * 100.0 / renderer.tot_tgt_frs, new_sz)) print('butterflow took {:.3g} minutes, done.'.format(tot_time)) except (KeyboardInterrupt, SystemExit): log.warning('stopped early, files were left in the cache') log.warning('recoverable @ {}'.format(settings.default['tmp_dir'])) return 1 except Exception: return 1
def test_get_av_info_path(self): testfile = os.path.join(settings['tmp_dir'], '~test_get_av_info_path.mp4') mk_sample_video(testfile, 1, 320, 240, fractions.Fraction(1, 1)) av = avinfo.get_av_info(testfile) self.assertEqual(av['path'], testfile)
def test_no_file_at_path_fails(self): with self.assertRaises(RuntimeError): avinfo.get_av_info('does_not_exist.mp4')
def extract_audio(video, destination, start, end, spd=1.0): av_info = avinfo.get_av_info(video) if not av_info['a_stream_exists']: raise RuntimeError('no audio stream found') filename = os.path.splitext(os.path.basename(destination))[0] tempfile1 = '~{filename}.{ext}'.format( filename=filename, ext=settings['v_container']).lower() tempfile1 = os.path.join(settings['tmp_dir'], tempfile1) call = [ settings['avutil'], '-loglevel', settings['av_loglevel'], '-y', '-i', video, '-ss', str(start / 1000.0), '-to', str(end / 1000.0), '-map_metadata', '-1', '-map_chapters', '-1', '-vn', '-sn', ] # `aac` is considered an experimental encoder and so `-strict experimental` # or `-strict 2` is required if settings['ca'] == 'aac': call.extend(['-strict', '-2']) call.extend([ '-c:a', settings['ca'], '-b:a', settings['ba'], tempfile1 ]) proc = subprocess.call(call) if proc == 1: raise RuntimeError('extraction failed') # change speed of file using the `atempo` filter tempfile2 = '~{filename}.{spd}x.{ext}'.format( filename=filename, spd=spd, ext=settings['a_container'] ) tempfile2 = os.path.join(settings['tmp_dir'], tempfile2) # the `atempo` filter is limited to using values between `ATEMPO_MIN=0.5` # and `ATEMPO_MAX=2.0` work around this limitation by stringing multiple # `atempo` filters together atempo_chain = [] for f in atempo_factors_for_spd(spd): atempo_chain.append('atempo={}'.format(f)) call = [ settings['avutil'], '-loglevel', settings['av_loglevel'], '-y', '-i', tempfile1, '-filter:a', ','.join(atempo_chain), ] if settings['ca'] == 'aac': call.extend(['-strict', '-2']) call.extend([ '-c:a', settings['ca'], '-b:a', settings['ba'], tempfile2, ]) proc = subprocess.call(call) if proc == 1: raise RuntimeError('change tempo failed') os.remove(tempfile1) shutil.move(tempfile2, destination)
def extract_audio(video, destination, start, end, spd=1.0): av_info = avinfo.get_av_info(video) if not av_info['a_stream_exists']: raise RuntimeError('no audio stream found') filename = os.path.splitext(os.path.basename(destination))[0] tempfile1 = '~{filename}.{ext}'.format( filename=filename, ext=settings['v_container']).lower() tempfile1 = os.path.join(settings['tmp_dir'], tempfile1) call = [ settings['avutil'], '-loglevel', settings['av_loglevel'], '-y', '-i', video, '-ss', str(start / 1000.0), '-to', str(end / 1000.0), '-map_metadata', '-1', '-map_chapters', '-1', '-vn', '-sn', ] # `aac` is considered an experimental encoder and so `-strict experimental` # or `-strict 2` is required if settings['ca'] == 'aac': call.extend(['-strict', '-2']) call.extend(['-c:a', settings['ca'], '-b:a', settings['ba'], tempfile1]) proc = subprocess.call(call) if proc == 1: raise RuntimeError('extraction failed') # change speed of file using the `atempo` filter tempfile2 = '~{filename}.{spd}x.{ext}'.format(filename=filename, spd=spd, ext=settings['a_container']) tempfile2 = os.path.join(settings['tmp_dir'], tempfile2) # the `atempo` filter is limited to using values between `ATEMPO_MIN=0.5` # and `ATEMPO_MAX=2.0` work around this limitation by stringing multiple # `atempo` filters together atempo_chain = [] for f in atempo_factors_for_spd(spd): atempo_chain.append('atempo={}'.format(f)) call = [ settings['avutil'], '-loglevel', settings['av_loglevel'], '-y', '-i', tempfile1, '-filter:a', ','.join(atempo_chain), ] if settings['ca'] == 'aac': call.extend(['-strict', '-2']) call.extend([ '-c:a', settings['ca'], '-b:a', settings['ba'], tempfile2, ]) proc = subprocess.call(call) if proc == 1: raise RuntimeError('change tempo failed') os.remove(tempfile1) shutil.move(tempfile2, destination)
def test_nfrs_equal_to_avinfo_nfrs(self): av = avinfo.get_av_info(self.src_3.src) self.assertEqual(self.src_3.nfrs, av['frames'])
def main(): par = argparse.ArgumentParser(usage='butterflow [options] [video]', add_help=False) req = par.add_argument_group('Required arguments') gen = par.add_argument_group('General options') dev = par.add_argument_group('Device options') dsp = par.add_argument_group('Display options') vid = par.add_argument_group('Video options') mux = par.add_argument_group('Muxing options') fgr = par.add_argument_group('Advanced options') req.add_argument('video', type=str, nargs='?', default=None, help='Specify the input video') gen.add_argument('-h', '--help', action='help', help='Show this help message and exit') gen.add_argument('--version', action='store_true', help='Show program\'s version number and exit') gen.add_argument('--cache-dir', type=str, help='Specify path to the cache directory') gen.add_argument('-c', '--cache', action='store_true', help='Show cache information and exit') gen.add_argument('--rm-cache', action='store_true', help='Set to clear the cache and exit') gen.add_argument('-prb', '--probe', action='store_true', help='Show media file information and exit') gen.add_argument('-v', '--verbosity', action='count', help='Set to increase output verbosity') gen.add_argument('-q', '--quiet', action='store_true', help='Set to suppress console output') dev.add_argument('-d', '--show-devices', action='store_true', help='Show detected OpenCL devices and exit') dev.add_argument('-device', type=int, default=-1, help='Specify the preferred OpenCL device to use as an ' 'integer. Device numbers can be listed with the `-d` ' 'option. The device will be chosen automatically if ' 'nothing is specified.') dev.add_argument('-sw', action='store_true', help='Set to force software rendering') dsp.add_argument('-p', '--show-preview', action='store_true', help='Set to show video preview') dsp.add_argument('-e', '--embed-info', action='store_true', help='Set to embed debugging info into the output video') dsp.add_argument('-tt', '--text-type', choices=['light', 'dark', 'stroke'], default=settings['text_type'], help='Specify text type for embedded debugging info, ' '(default: %(default)s)') dsp.add_argument('-m', '--mark-frames', action='store_true', help='Set to mark interpolated frames') vid.add_argument('-o', '--output-path', type=str, default=settings['out_path'], help='Specify path to the output video') vid.add_argument('-r', '--playback-rate', type=str, help='Specify the playback rate as an integer or a float.' ' Fractional forms are acceptable, e.g., 24/1.001 is the ' 'same as 23.976. To use a multiple of the source ' 'video\'s rate, follow a number with `x`, e.g., "2x" ' 'will double the frame rate. The original rate will be ' 'used by default if nothing is specified.') vid.add_argument('-s', '--subregions', type=str, help='Specify rendering subregions in the form: ' '"a=TIME,b=TIME,TARGET=VALUE" where TARGET is either ' '`spd`, `dur`, `fps`. Valid TIME syntaxes are [hr:m:s], ' '[m:s], [s], [s.xxx], or `end`, which signifies to the ' 'end the video. You can specify multiple subregions by ' 'separating them with a colon `:`. A special subregion ' 'format that conveniently describes the entire clip is ' 'available in the form: "full,TARGET=VALUE".') vid.add_argument('-k', '--keep-subregions', action='store_true', help='Set to render subregions that are not explicitly ' 'specified') vid.add_argument('-vs', '--video-scale', type=str, default=str(settings['video_scale']), help='Specify output video size in the form: ' '"WIDTH:HEIGHT" or by using a factor. To keep the ' 'aspect ratio only specify one component, either width ' 'or height, and set the other component to -1, ' '(default: %(default)s)') vid.add_argument('-l', '--lossless', action='store_true', help='Set to use lossless encoding settings') vid.add_argument('-sm', '--smooth-motion', action='store_true', help='Set to tune for smooth motion. This mode yields ' 'artifact-less frames by emphasizing blended frames over ' 'warping pixels.') mux.add_argument('-audio', action='store_true', help='Set to add the source audio to the output video') fgr.add_argument('--fast-pyr', action='store_true', help='Set to use fast pyramids') fgr.add_argument('--pyr-scale', type=float, default=settings['pyr_scale'], help='Specify pyramid scale factor, ' '(default: %(default)s)') fgr.add_argument('--levels', type=int, default=settings['levels'], help='Specify number of pyramid layers, ' '(default: %(default)s)') fgr.add_argument('--winsize', type=int, default=settings['winsize'], help='Specify averaging window size, ' '(default: %(default)s)') fgr.add_argument('--iters', type=int, default=settings['iters'], help='Specify number of iterations at each pyramid ' 'level, (default: %(default)s)') fgr.add_argument('--poly-n', type=int, choices=settings['poly_n_choices'], default=settings['poly_n'], help='Specify size of pixel neighborhood, ' '(default: %(default)s)') fgr.add_argument('--poly-s', type=float, default=settings['poly_s'], help='Specify standard deviation to smooth derivatives, ' '(default: %(default)s)') fgr.add_argument('-ff', '--flow-filter', choices=['box', 'gaussian'], default=settings['flow_filter'], help='Specify which filter to use for optical flow ' 'estimation, (default: %(default)s)') for i, arg in enumerate(sys.argv): if arg[0] == '-' and arg[1].isdigit(): sys.argv[i] = ' '+arg args = par.parse_args() format = '[butterflow:%(levelname)s]: %(message)s' logging.basicConfig(level=settings['loglevel_0'], format=format) log = logging.getLogger('butterflow') if args.verbosity == 1: log.setLevel(settings['loglevel_1']) if args.verbosity >= 2: log.setLevel(settings['loglevel_2']) if args.quiet: log.setLevel(settings['loglevel_quiet']) settings['quiet'] = True if args.version: print(__version__) return 0 if args.cache_dir is not None: cachedir = os.path.normpath(args.cache_dir) if os.path.exists(cachedir): if not os.path.isdir(cachedir): print('Cache path is not a directory') return 1 else: os.makedirs(cachedir) settings['tempdir'] = cachedir settings['clbdir'] = os.path.join(cachedir, 'clb') if not os.path.exists(settings['clbdir']): os.makedirs(settings['clbdir']) ocl.set_cache_path(settings['clbdir'] + os.sep) cachedir = settings['tempdir'] cachedirs = [] tempfolder = os.path.dirname(cachedir) for dirpath, dirnames, filenames in os.walk(tempfolder): for d in dirnames: if 'butterflow' in d: if 'butterflow-'+__version__ not in d: cachedirs.append(os.path.join(dirpath, d)) break if args.cache: nfiles = 0 sz = 0 for dirpath, dirnames, filenames in os.walk(cachedir): if dirpath == settings['clbdir']: continue for filename in filenames: nfiles += 1 fp = os.path.join(dirpath, filename) sz += os.path.getsize(fp) sz = sz / 1024.0**2 print('{} files, {:.2f} MB'.format(nfiles, sz)) print('Cache: '+cachedir) return 0 if args.rm_cache: cachedirs.append(cachedir) for i, x in enumerate(cachedirs): print('[{}] {}'.format(i, x)) choice = raw_input('Remove these directories? [y/N] ') if choice != 'y': print('Leaving the cache alone, done.') return 0 for x in cachedirs: if os.path.exists(x): import shutil shutil.rmtree(x) print('Cache deleted, done.') return 0 if args.show_devices: ocl.print_ocl_devices() return 0 if not args.video: print('No file specified') return 1 elif not os.path.exists(args.video): print('File doesn\'t exist') return 1 if args.probe: avinfo.print_av_info(args.video) return 0 av_info = avinfo.get_av_info(args.video) if av_info['frames'] == 0: print('Bad file with 0 frames') return 1 extension = os.path.splitext(os.path.basename(args.output_path))[1].lower() if extension[1:] != settings['v_container']: print('Bad output file extension. Must be {}.'.format( settings['v_container'].upper())) return 0 if not args.sw and not ocl.compat_ocl_device_available(): print('No compatible OpenCL devices were detected. Must force software ' 'rendering with the `-sw` flag to continue.') return 1 log.info('Version '+__version__) log.info('Cache directory:\t%s' % cachedir) for x in cachedirs: log.warn('Stale cache directory (delete with `--rm-cache`): %s' % x) if not args.sw and ocl.compat_ocl_device_available(): log.info('At least one compatible OpenCL device was detected') else: log.warning('No compatible OpenCL devices were detected.') if args.device != -1: try: ocl.select_ocl_device(args.device) except IndexError as error: print('Error: '+str(error)) return 1 except ValueError: if not args.sw: print('An incompatible device was selected.\n' 'Must force software rendering with the `-sw` flag to continue.') return 1 s = "Using device: %s" if args.device == -1: s += " (autoselected)" log.info(s % ocl.get_current_ocl_device_name()) use_sw_interpolate = args.sw if args.flow_filter == 'gaussian': args.flow_filter = cv2.OPTFLOW_FARNEBACK_GAUSSIAN else: args.flow_filter = 0 if args.smooth_motion: args.poly_s = 0.01 def optflow_fn(x, y, pyr=args.pyr_scale, levels=args.levels, winsize=args.winsize, iters=args.iters, polyn=args.poly_n, polys=args.poly_s, fast=args.fast_pyr, filt=args.flow_filter): if use_sw_interpolate: return cv2.calcOpticalFlowFarneback( x, y, pyr, levels, winsize, iters, polyn, polys, filt) else: return motion.ocl_farneback_optical_flow( x, y, pyr, levels, winsize, iters, polyn, polys, fast, filt) interpolate_fn = None if use_sw_interpolate: from butterflow.interpolate import sw_interpolate_flow interpolate_fn = sw_interpolate_flow log.warn("Hardware acceleration is disabled. Rendering will be slow. " "Do Ctrl+c to quit or suspend the process with Ctrl+z and " "then stop it with `kill %1`, etc. You can list suspended " "processes with `jobs`.)") else: interpolate_fn = motion.ocl_interpolate_flow log.info("Hardware acceleration is enabled") try: w, h = w_h_from_input_str(args.video_scale, av_info['w'], av_info['h']) sequence = sequence_from_input_str(args.subregions, av_info['duration'], av_info['frames']) rate = rate_from_input_str(args.playback_rate, av_info['rate']) except (ValueError, AttributeError) as error: print('Error: '+str(error)) return 1 def nearest_even_int(x, tag=""): new_x = x & ~1 if x != new_x: log.warn("%s: %d is not divisible by 2, setting to %d", tag, x, new_x) return new_x w1, h1 = av_info['w'], av_info['h'] w2 = nearest_even_int(w, "W") if w2 > 256: if w2 % 4 > 0: old_w2 = w2 w2 -= 2 w2 = max(w2, 0) log.warn('W: %d > 256 but is not divisible by 4, setting to %d', old_w2, w2) h2 = nearest_even_int(h, "H") if w1*h1 > w2*h2: scaling_method = settings['scaler_dn'] elif w1*h1 < w2*h2: scaling_method = settings['scaler_up'] else: scaling_method = None rnd = Renderer(args.video, args.output_path, sequence, rate, optflow_fn, interpolate_fn, w2, h2, scaling_method, args.lossless, args.keep_subregions, args.show_preview, args.embed_info, args.text_type, args.mark_frames, args.audio) ocl.set_num_threads(settings['ocv_threads']) log.info('Rendering:') added_rate = False for x in str(rnd.sequence).split('\n'): x = x.strip() if not added_rate: x += ', Rate={}'.format(av_info['rate']) log.info(x) added_rate = True continue if not args.keep_subregions and 'autogenerated' in x: log.info(x[:-1]+ ', will skip when rendering)') continue log.info(x) temp_subs = rnd.sequence.subregions for x in rnd.sequence.subregions: overlaps = False for y in temp_subs: if x is y: continue elif x.intersects(y): overlaps = True break if overlaps: log.warn('At least 1 subregion overlaps with another') break success = True total_time = 0 try: import timeit total_time = timeit.timeit(rnd.render, setup='import gc;gc.enable()', number=1) except (KeyboardInterrupt, SystemExit): success = False if success: log_function = log.info if rnd.frs_written > rnd.frs_to_render: log_function = log.warn log.warn('Unexpected write ratio') log_function('Write ratio: {}/{}, ({:.2f}%)'.format( rnd.frs_written, rnd.frs_to_render, rnd.frs_written*100.0/rnd.frs_to_render)) txt = 'Final output frames: {} source, +{} interpolated, +{} duped, -{} dropped' if not settings['quiet']: log.info(txt.format(rnd.source_frs, rnd.frs_interpolated, rnd.frs_duped, rnd.frs_dropped)) old_sz = os.path.getsize(args.video) / 1024.0 new_sz = os.path.getsize(args.output_path) / 1024.0 log.info('Output file size:\t{:.2f} kB ({:.2f} kB)'.format(new_sz, new_sz - old_sz)) log.info('Rendering took {:.3g} mins, done.'.format(total_time / 60)) return 0 else: log.warn('Quit unexpectedly') log.warn('Files were left in the cache @ '+settings['tempdir']+'.') return 1
def main(): par = argparse.ArgumentParser(usage='butterflow [options] [video]', add_help=False) req = par.add_argument_group('Required arguments') gen = par.add_argument_group('General options') dsp = par.add_argument_group('Display options') vid = par.add_argument_group('Video options') mux = par.add_argument_group('Muxing options') fgr = par.add_argument_group('Advanced options') req.add_argument('video', type=str, nargs='?', default=None, help='Specify the input video') gen.add_argument('-h', '--help', action='help', help='Show this help message and exit') gen.add_argument('--version', action='store_true', help='Show program\'s version number and exit') gen.add_argument('-d', '--devices', action='store_true', help='Show detected OpenCL devices and exit') gen.add_argument('-sw', action='store_true', help='Set to force software rendering') gen.add_argument('-c', '--cache', action='store_true', help='Show cache information and exit') gen.add_argument('--rm-cache', action='store_true', help='Set to clear the cache and exit') gen.add_argument('-prb', '--probe', action='store_true', help='Show media file information and exit') gen.add_argument('-v', '--verbosity', action='count', help='Set to increase output verbosity') gen.add_argument('-q', '--quiet', action='store_true', help='Set to suppress console output') dsp.add_argument('-p', '--show-preview', action='store_true', help='Set to show video preview') dsp.add_argument('-a', '--add-info', action='store_true', help='Set to embed debugging info into the output video') dsp.add_argument('-tt', '--text-type', choices=['light', 'dark', 'stroke'], default=settings['text_type'], help='Specify text type for debugging info, ' '(default: %(default)s)') dsp.add_argument('-mrk', '--mark-frames', action='store_true', help='Set to mark interpolated frames') vid.add_argument('-o', '--output-path', type=str, default=settings['out_path'], help='Specify path to the output video') vid.add_argument('-r', '--playback-rate', type=str, help='Specify the playback rate as an integer or a float ' 'Fractional forms are acceptable, e.g., 24/1.001 is the ' 'same as 23.976. To use a multiple of the source ' 'video\'s rate, follow a number with `x`, e.g., "2x" ' 'will double the frame rate. The original rate will be ' 'used by default if nothing is specified.') vid.add_argument('-s', '--subregions', type=str, help='Specify rendering subregions in the form: ' '"a=TIME,b=TIME,TARGET=VALUE" where TARGET is either ' '`fps`, `dur`, `spd`. Valid TIME syntaxes are [hr:m:s], ' '[m:s], [s], [s.xxx], or `end`, which signifies to the ' 'end the video. You can specify multiple subregions by ' 'separating them with a colon `:`. A special subregion ' 'format that conveniently describes the entire clip is ' 'available in the form: "full,TARGET=VALUE".') vid.add_argument('-k', '--keep-subregions', action='store_true', help='Set to render subregions that are not explicitly ' 'specified') vid.add_argument('-vs', '--video-scale', type=str, default=str(settings['video_scale']), help='Specify output video size in the form: ' '"WIDTH:HEIGHT" or by using a factor. To keep the ' 'aspect ratio only specify one component, either width ' 'or height, and set the other component to -1, ' '(default: %(default)s)') vid.add_argument('-l', '--lossless', action='store_true', help='Set to use lossless encoding settings') vid.add_argument('-sm', '--smooth-motion', action='store_true', help='Set to tune for smooth motion. This mode yields ' 'artifact-less frames by emphasizing blended frames over ' 'warping pixels.') mux.add_argument('-mux', action='store_true', help='Set to mux the source audio with the output video') fgr.add_argument('--fast-pyr', action='store_true', help='Set to use fast pyramids') fgr.add_argument('--pyr-scale', type=float, default=settings['pyr_scale'], help='Specify pyramid scale factor, ' '(default: %(default)s)') fgr.add_argument('--levels', type=int, default=settings['levels'], help='Specify number of pyramid layers, ' '(default: %(default)s)') fgr.add_argument('--winsize', type=int, default=settings['winsize'], help='Specify averaging window size, ' '(default: %(default)s)') fgr.add_argument('--iters', type=int, default=settings['iters'], help='Specify number of iterations at each pyramid ' 'level, (default: %(default)s)') fgr.add_argument('--poly-n', type=int, choices=settings['poly_n_choices'], default=settings['poly_n'], help='Specify size of pixel neighborhood, ' '(default: %(default)s)') fgr.add_argument('--poly-s', type=float, default=settings['poly_s'], help='Specify standard deviation to smooth derivatives, ' '(default: %(default)s)') fgr.add_argument('-ff', '--flow-filter', choices=['box', 'gaussian'], default=settings['flow_filter'], help='Specify which filter to use for optical flow ' 'estimation, (default: %(default)s)') for i, arg in enumerate(sys.argv): if arg[0] == '-' and arg[1].isdigit(): sys.argv[i] = ' '+arg args = par.parse_args() fmt = '[butterflow:%(filename)s:%(funcName)s.%(levelname)s]: %(message)s' logging.basicConfig(level=settings['loglevel_0'], format=fmt) log = logging.getLogger('butterflow') if args.verbosity == 1: log.setLevel(settings['loglevel_1']) if args.verbosity >= 2: log.setLevel(settings['loglevel_2']) if args.quiet: log.setLevel(settings['loglevel_quiet']) settings['quiet'] = True if args.version: print(__version__) return 0 cachedir = settings['tempdir'] if args.cache: nfiles = 0 sz = 0 for dirpath, dirnames, filenames in os.walk(cachedir): if dirpath == settings['clbdir']: continue for filename in filenames: nfiles += 1 fp = os.path.join(dirpath, filename) sz += os.path.getsize(fp) sz = sz / 1024.0**2 print('{} files, {:.2f} MB'.format(nfiles, sz)) print('cache @ '+cachedir) return 0 if args.rm_cache: if os.path.exists(cachedir): import shutil shutil.rmtree(cachedir) print('cache deleted, done.') return 0 if args.devices: ocl.print_ocl_devices() return 0 if not args.video: print('no file specified, use: -h for help') return 1 elif not os.path.exists(args.video): print('file does not exist') return 1 if args.probe: avinfo.print_av_info(args.video) return 0 extension = os.path.splitext(os.path.basename(args.output_path))[1].lower() if extension[1:] != 'mp4': print('bad out file extension') return 0 av_info = avinfo.get_av_info(args.video) use_sw_interpolate = args.sw or not ocl.compat_ocl_device_available() if use_sw_interpolate: log.warn('not using opencl, ctrl+c to quit') if args.flow_filter == 'gaussian': args.flow_filter = cv2.OPTFLOW_FARNEBACK_GAUSSIAN else: args.flow_filter = 0 if args.smooth_motion: args.polys = 0.01 def optflow_fn(x, y, pyr=args.pyr_scale, levels=args.levels, winsize=args.winsize, iters=args.iters, polyn=args.poly_n, polys=args.poly_s, fast=args.fast_pyr, filt=args.flow_filter): if use_sw_interpolate: return cv2.calcOpticalFlowFarneback( x, y, pyr, levels, winsize, iters, polyn, polys, filt) else: return motion.ocl_farneback_optical_flow( x, y, pyr, levels, winsize, iters, polyn, polys, fast, filt) interpolate_fn = None if use_sw_interpolate: from butterflow.interpolate import sw_interpolate_flow interpolate_fn = sw_interpolate_flow else: interpolate_fn = motion.ocl_interpolate_flow try: w, h = w_h_from_input_str(args.video_scale, av_info['w'], av_info['h']) sequence = sequence_from_input_str(args.subregions, av_info['duration'], av_info['frames']) rate = rate_from_input_str(args.playback_rate, av_info['rate']) except (ValueError, AttributeError) as error: print('error: '+str(error)) return 1 def nearest_even_int(x): return x & ~1 w1, h1 = av_info['w'], av_info['h'] w2, h2 = nearest_even_int(w), nearest_even_int(h) if w1*h1 > w2*h2: scaling_method = settings['scaler_dn'] elif w1*h1 < w2*h2: scaling_method = settings['scaler_up'] else: scaling_method = None rnd = Renderer(args.video, args.output_path, sequence, rate, optflow_fn, interpolate_fn, w2, h2, scaling_method, args.lossless, args.keep_subregions, args.show_preview, args.add_info, args.text_type, args.mark_frames, args.mux) motion.set_num_threads(settings['ocv_threads']) log.info('will render:\n' + str(rnd.sequence)) success = True total_time = 0 try: import timeit total_time = timeit.timeit(rnd.render, setup='import gc;gc.enable()', number=1) except (KeyboardInterrupt, SystemExit): success = False if success: log.info('made: '+args.output_path) out_sz = os.path.getsize(args.output_path) / 1024.0**2 log.info('write ratio: {}/{}, ({:.2f}%) {:.2f} MB'.format( rnd.frs_written, rnd.frs_to_render, rnd.frs_written*100.0/rnd.frs_to_render, out_sz)) txt = 'frames: {} real, +{} interpolated, +{} dupe, -{} drop' if not settings['quiet']: print(txt.format(rnd.source_frs, rnd.frs_interpolated, rnd.frs_duped, rnd.frs_dropped)) log.info('butterflow took {:.3g} mins, done.'.format(total_time / 60)) return 0 else: log.warn('quit unexpectedly') log.warn('files left in cache @ '+settings['tempdir']) return 1