From 8c9abe2de74752e28873827562f7a1a91d6a88fa Mon Sep 17 00:00:00 2001 From: Duong Pham Date: Thu, 4 Feb 2016 04:51:27 -0700 Subject: [PATCH] version 0.2.1.dev0 --- .gitattributes | 2 - .gitignore | 9 +- LICENSE | 45 +- MANIFEST.in | 6 +- README.md | 35 +- butterflow/__main__.py | 4 +- butterflow/avinfo.c | 43 +- butterflow/cli.py | 638 +++++++---------- butterflow/draw.py | 151 ++-- butterflow/interpolate.py | 19 +- butterflow/motion.cpp | 23 +- butterflow/mux.py | 148 ++-- butterflow/ocl.c | 15 +- butterflow/render.py | 676 ++++++------------ butterflow/sequence.py | 157 ++-- butterflow/settings.py | 60 +- butterflow/source.py | 70 +- docs/Example-Usage.md | 15 +- docs/Install-From-Source-Guide.md | 18 +- docs/README.md | 4 - docs/Setting-Up-OpenCL.md | 47 +- setup.py | 58 +- share/butterflow.ico | Bin 2238 -> 0 bytes tests/__init__.py | 1 + tests/test_avinfo.py | 68 +- tests/test_cli.py | 293 -------- tests/test_motion.py | 30 +- tests/test_mux.py | 40 -- tests/test_sequence.py | 274 +++---- tests/test_source.py | 123 ++-- vendor/.gitignore | 2 +- .../{LICENSE.txt => LICENSE} | 0 .../opencv-ndarray-conversion/conversion.h | 1 - .../src/conversion.cpp | 4 +- 34 files changed, 1186 insertions(+), 1893 deletions(-) delete mode 100644 .gitattributes delete mode 100644 share/butterflow.ico delete mode 100644 tests/test_cli.py delete mode 100644 tests/test_mux.py rename vendor/opencv-ndarray-conversion/{LICENSE.txt => LICENSE} (100%) diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b7e0d56..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# auto detect text files and perform LF normalization (default on nix and osx) -* text=auto diff --git a/.gitignore b/.gitignore index b178823..4c2cfa9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ # misc *~ + +# de *.DS_Store *.directory @@ -15,8 +17,6 @@ bin lib include -Scripts -env # pip pip-selfcheck.json @@ -35,10 +35,11 @@ __pycache__ .vs *.pyproj* *.sln +Scripts +env # development tags *.pth dev_settings.py -output.mp4 -clear.sh +out.mp4 diff --git a/LICENSE b/LICENSE index d418c25..bba9720 100644 --- a/LICENSE +++ b/LICENSE @@ -1,32 +1,21 @@ -Copyright (c) 2015 by Duong Pham +The MIT License (MIT) -All rights reserved. +Copyright (c) 2016 Duong Pham -Redistribution and use in source and binary forms of the software as well -as documentation, with or without modification, are permitted provided -that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND -CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT -NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER -OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index b4fb31d..422a43e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,8 @@ -recursive-include tests * +include README.md LICENSE +recursive-include tests *.py recursive-exclude tests *.pyc recursive-exclude vendor * recursive-include vendor/opencv-ndarray-conversion * include butterflow/avinfo.c include butterflow/ocl.c include butterflow/motion.cpp -exclude butterflow/dev_settings.py -include LICENSE -include README.md diff --git a/README.md b/README.md index eb0a20b..a540ba1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,23 @@ # Butterflow -*Butterflow* is an easy to use command-line tool that lets you create fluid slow -motion and motion interpolated videos. +*Butterflow* is a command-line tool that lets you make fluid slow motion and +motion interpolated videos. ## How does it work? -It works by rendering intermediate frames between existing frames. For example, -given two existing frames, `A` and `B`, this program can generate frames `C.1`, -`C.2`...`C.n` that are positioned between the two. This process, called -[motion interpolation](http://en.wikipedia.org/wiki/Motion_interpolation), -increases frame rates and can give the perception of smoother motion and more -fluid animation, an effect most people know as the "soap opera effect". -Butterflow takes advantage of newly-available frames to make high speed, slow -motion videos with minimal judder. +It works by rendering intermediate frames between existing frames using a +process called [motion interpolation](http://en.wikipedia.org/wiki/Motion_interpolation). +For example, given two existing frames, `A` and `B`, this program can generate +frames `C.1`, `C.2`...`C.n` that are positioned between the two. In contrast +to other tools that can only blend or dupe frames, this program warps pixels +based on motion to generate new ones. + +Butterflow uses these interpolated frames to increase a video's frame rate, +which can give the perception of smoother motion and more fluid animation, an +effect that most people know as the "soap opera effect". + +## Demonstration + +This is a demonstration of Butterflow leveraging motion interpolation to make +slow motion videos with minimal judder. ![](http://srv.dthpham.me/static/ink.gif) @@ -44,10 +51,10 @@ Refer to the for instructions. ## Setup -Butterflow requires no additional setup to use, but it's too slow out of the box -to do any serious work, so you need to set up a functional OpenCL environment on -your machine to take advantage of hardware accelerated methods that will make -rendering significantly faster. +Butterflow requires no additional setup to use, however it's too slow out of +the box to do any serious work. It's recommended that you set up a functional +OpenCL environment on your machine to take advantage of hardware accelerated +methods that will make rendering significantly faster. See [Setting up OpenCL](https://github.com/dthpham/butterflow/blob/master/docs/Setting-Up-OpenCL.md) for details on how to do this. diff --git a/butterflow/__main__.py b/butterflow/__main__.py index 1f6b294..4cce3d6 100644 --- a/butterflow/__main__.py +++ b/butterflow/__main__.py @@ -1,8 +1,6 @@ #!/usr/bin/env python2 -# console scripts entry point into butterflow - -import sys if __name__ == '__main__': + import sys from butterflow.cli import main sys.exit(main()) diff --git a/butterflow/avinfo.c b/butterflow/avinfo.c index 6ed6a9a..d04bcdf 100644 --- a/butterflow/avinfo.c +++ b/butterflow/avinfo.c @@ -1,8 +1,9 @@ -// retrieves media file information +// get av file info #include #include #include +#include #include #include #include @@ -14,12 +15,17 @@ PyErr_SetString(PyExc_RuntimeError, "set dict failed"); \ return (PyObject*)NULL; } #define MS_PER_SEC 1000 +#define EPSILON 1.0e-8 int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } +int almost_equal(double a, double b) { + return fabs(a - b) <= EPSILON; +} + static PyObject* get_av_info(PyObject *self, PyObject *arg) { char *path = PyString_AsString(arg); @@ -29,20 +35,19 @@ get_av_info(PyObject *self, PyObject *arg) { AVFormatContext *format_ctx = avformat_alloc_context(); - /* make quiet. should reset when finished */ int initial_level = av_log_get_level(); - av_log_set_level(AV_LOG_ERROR); + av_log_set_level(AV_LOG_ERROR); /* make quiet */ int rc = avformat_open_input(&format_ctx, path, NULL, NULL); if (rc != 0) { - PyErr_SetString(PyExc_RuntimeError, "could not open file"); + PyErr_SetString(PyExc_RuntimeError, "open input failed"); return (PyObject*)NULL; } rc = avformat_find_stream_info(format_ctx, NULL); if (rc < 0) { avformat_close_input(&format_ctx); - PyErr_SetString(PyExc_RuntimeError, "could not find stream info"); + PyErr_SetString(PyExc_RuntimeError, "no stream info found"); return (PyObject*)NULL; } @@ -77,7 +82,8 @@ get_av_info(PyObject *self, PyObject *arg) { unsigned long frames = 0; AVRational sar = {0, 0}; /* sample aspect ratio */ AVRational dar = {0, 0}; /* display aspect ratio */ - AVRational rate = {0, 0}; /* average fps */ + float rate = 0.0; /* average fps */ + AVRational rational_rate = {0, 0}; if (v_stream_exists) { AVStream *v_stream = format_ctx->streams[v_stream_idx]; @@ -90,9 +96,9 @@ get_av_info(PyObject *self, PyObject *arg) { int64_t c_duration = av_rescale_q(format_ctx->duration, av_tb, ms_tb); duration = v_duration; - if (v_duration == 0) { - /* fallback to the container duration if the video stream doesnt - * report anything */ + if (duration < 0 || almost_equal(v_duration, 0)) { + /* fallback to the container duration if the video stream + * doesnt report anything */ duration = c_duration; } @@ -101,10 +107,11 @@ get_av_info(PyObject *self, PyObject *arg) { w = v_codec_ctx->width; h = v_codec_ctx->height; - rate = format_ctx->streams[v_stream_idx]->avg_frame_rate; + rational_rate = format_ctx->streams[v_stream_idx]->avg_frame_rate; + rate = rational_rate.num * 1.0 / rational_rate.den; /* calculate num of frames ourselves */ - frames = (rate.num * 1.0 / rate.den) * (duration / 1000.0); + frames = rate * duration / 1000.0; /* if sample aspect ratio is unknown assume it is 1:1 */ sar = format_ctx->streams[v_stream_idx]->sample_aspect_ratio; @@ -145,8 +152,9 @@ get_av_info(PyObject *self, PyObject *arg) { py_safe_set(py_info, "dar_n", PyInt_FromLong(dar.num)); py_safe_set(py_info, "dar_d", PyInt_FromLong(dar.den)); py_safe_set(py_info, "duration", PyFloat_FromDouble(duration)); - py_safe_set(py_info, "rate_n", PyInt_FromLong(rate.num)); - py_safe_set(py_info, "rate_d", PyInt_FromLong(rate.den)); + py_safe_set(py_info, "rate_n", PyInt_FromLong(rational_rate.num)); + py_safe_set(py_info, "rate_d", PyInt_FromLong(rational_rate.den)); + py_safe_set(py_info, "rate", PyFloat_FromDouble(rate)); py_safe_set(py_info, "frames", PyLong_FromUnsignedLong(frames)); return py_info; @@ -156,7 +164,7 @@ static PyObject* print_av_info(PyObject *self, PyObject *arg) { PyObject *py_info = get_av_info(self, arg); - if (py_info == NULL) { /* something bad happened */ + if (py_info == NULL) { return (PyObject*)NULL; } @@ -194,11 +202,6 @@ print_av_info(PyObject *self, PyObject *arg) { x /= 60; hrs = x % 24; - /* calculate rate */ - int rate_n = PyInt_AsLong(PyDict_GetItemString(py_info, "rate_n")); - int rate_d = PyInt_AsLong(PyDict_GetItemString(py_info, "rate_d")); - float rate = rate_n * 1.0 / rate_d; - printf("Video information:"); printf("\n Streams available \t: %s" "\n Resolution \t: %dx%d, SAR %d:%d DAR %d:%d" @@ -212,7 +215,7 @@ print_av_info(PyObject *self, PyObject *arg) { (int)PyInt_AsLong(PyDict_GetItemString(py_info, "sar_d")), (int)PyInt_AsLong(PyDict_GetItemString(py_info, "dar_n")), (int)PyInt_AsLong(PyDict_GetItemString(py_info, "dar_d")), - rate, + (float)PyFloat_AsDouble(PyDict_GetItemString(py_info, "rate")), hrs, mins, secs, duration / 1000.0, PyInt_AsUnsignedLongMask(PyDict_GetItemString(py_info, "frames"))); diff --git a/butterflow/cli.py b/butterflow/cli.py index 066992d..03cc4ef 100644 --- a/butterflow/cli.py +++ b/butterflow/cli.py @@ -1,16 +1,15 @@ -# command line interface to butterflow +# cli to butterflow import os import sys import argparse -import math -import string -from butterflow import avinfo, motion, ocl, settings +import logging +import datetime +import cv2 +from butterflow.settings import default as settings +from butterflow import ocl, avinfo, motion from butterflow.render import Renderer -from butterflow.sequence import VideoSequence, RenderSubregion - -NO_VIDEO_SPECIFIED = 'Error: no input file specified' - +from butterflow.sequence import VideoSequence, Subregion def main(): par = argparse.ArgumentParser(usage='butterflow [options] [video]', @@ -45,18 +44,17 @@ def main(): 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') + 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'], + 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.default['out_path'], + 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 ' @@ -65,20 +63,20 @@ def main(): '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, + 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 region ' + '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-regions', action='store_true', + 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.default['video_scale']), + 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 ' @@ -97,62 +95,71 @@ def main(): 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'], + default=settings['pyr_scale'], help='Specify pyramid scale factor, ' '(default: %(default)s)') fgr.add_argument('--levels', type=int, - default=settings.default['levels'], + default=settings['levels'], help='Specify number of pyramid layers, ' '(default: %(default)s)') fgr.add_argument('--winsize', type=int, - default=settings.default['winsize'], + default=settings['winsize'], help='Specify averaging window size, ' '(default: %(default)s)') fgr.add_argument('--iters', type=int, - default=settings.default['iters'], + default=settings['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'], + 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.default['poly_s'], + 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.default['flow_filter'], + default=settings['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 + if arg[0] == '-' and arg[1].isdigit(): # accept args w/ - for -vs + sys.argv[i] = ' '+arg args = par.parse_args() - import logging - logging.basicConfig(level=settings.default['loglevel_a'], - format='%(levelname)-7s: %(message)s') - + logging.basicConfig(level=settings['loglevel_a'], + format='[butterflow.%(levelname)s]: %(message)s') log = logging.getLogger('butterflow') if args.verbose: - log.setLevel(settings.default['loglevel_b']) + log.setLevel(settings['loglevel_b']) if args.version: from butterflow.__init__ import __version__ - print('butterflow version %s' % __version__) + print('butterflow version {}'.format(__version__)) return 0 + cachedir = settings['tempdir'] if args.cache: - print_cache_info() + 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: - rm_cache() + if os.path.exists(cachedir): + import shutil + shutil.rmtree(cachedir) print('cache deleted, done.') return 0 @@ -160,391 +167,244 @@ def main(): ocl.print_ocl_devices() return 0 - if args.video is None: - print(NO_VIDEO_SPECIFIED) + if not args.video: + print('no file specified') return 1 - - if not os.path.exists(args.video): - print('Error: file does not exist at path') + elif not os.path.exists(args.video): + print('file does not exist') 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) + avinfo.print_av_info(args.video) 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 + av_info = avinfo.get_av_info(args.video) - 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 + args.flow_filter = cv2.OPTFLOW_FARNEBACK_GAUSSIAN + else: + args.flow_filter = 0 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) + 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 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']) + 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: - # 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)) + total_time = timeit.timeit(rnd.render_video, + setup='import gc;gc.enable()', + number=1) 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: + 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 human_sz(nbytes): - suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] - if nbytes == 0: - return '0 B' - i = 0 - while nbytes >= 1024 and i < len(suffixes) - 1: - nbytes /= 1024. - i += 1 - f = ('{:+.2f}'.format(nbytes)).rstrip('0').rstrip('.') - return '%s %s' % (f, suffixes[i]) - - -def print_cache_info(): - # clb_dir exists inside the tmp_dir - cache_dir = settings.default['tmp_dir'] - num_files = 0 - sz = 0 - for dirpath, dirnames, filenames in os.walk(cache_dir): - # ignore the clb_dir - if dirpath == settings.default['clb_dir']: - continue - for f in filenames: - num_files += 1 - fp = os.path.join(dirpath, f) - sz += os.path.getsize(fp) - sz = human_sz(float(sz)) - print('{} files, {}'.format(num_files, sz)) - print('cache @ %s' % cache_dir) - - -def rm_cache(): - # delete contents of the cache, including the clb_dir - cache_dir = settings.default['tmp_dir'] - if os.path.exists(cache_dir): - import shutil - shutil.rmtree(cache_dir) - - -def validate_chars_in_set(ch_set): - # decorator, ensures all chars in string args are in a char set - def wrapper(f): - def wrapped_f(*args, **kwargs): - strs = [] - for a in args: - if isinstance(a, str): - strs.append(a) - for k, v in kwargs: - if isinstance(v, str): - strs.append(v) - for s in strs: - for i, ch in enumerate(s): - if ch not in ch_set: - msg = 'unknown char `{}` at idx={}'.format(ch, i) - raise ValueError(msg) - return f(*args, **kwargs) - return wrapped_f - return wrapper - - -@validate_chars_in_set(string.digits + ':-.') -def w_h_from_str(string, source_width, source_height): - if ':' in string: # used `w:h` syntax - w, h = string.split(':') - w = int(w) - h = int(h) - if w < -1 or h < -1: - raise ValueError('unknown negative component') - # keep aspect ratio if either component is -1 - if w == -1 and h == -1: # ffmpeg allows this so we should too - return source_width, source_height - else: - if w == -1: - w = int(h * source_width / source_height) & ~1 # round to even - elif h == -1: - h = int(w * source_height / source_width) & ~1 - elif float(string) != 1.0: # using a singlular int or float - # round to nearest even number - even_pixel = lambda x: \ - int(math.floor(x * float(string) / 2) * 2) - w = even_pixel(source_width) - h = even_pixel(source_height) - else: # use source w,h by default - w = source_width - h = source_height - # w and h must be divisible by 2 for yuv420p outputs - # don't auto round when using the `w:h` syntax (with no -1 components) - # because the user may not expect the changes - if math.fmod(w, 2) != 0 or math.fmod(h, 2) != 0: - raise ValueError('components not divisible by two') - return w, h - - -@validate_chars_in_set(string.digits + '/x.') -def rate_from_str(string, source_rate): - if string is None: - return source_rate - if '/' in string: # got a fraction - # can't create Fraction object then cast to a float because it - # doesn't support non-rational numbers - n, d = string.split('/') - rate = float(n) / float(d) - elif 'x' in string: # used the "multiple of" syntax (e.g. `2x`) - rate = float(string.replace('x', '')) - rate = rate * source_rate - else: # got a singular integer or float - rate = float(string) - if rate <= 0: - raise ValueError('invalid frame rate value') - return rate - - -@validate_chars_in_set(string.digits + ':.') -def time_str_to_ms(time): +import re + +flt_pattern = r"(?P\d*\.\d+|\d+)" +wh_pattern = re.compile(r""" +(?=(?P.+:.+)|.+) +(?(semicolon) + (?P-1|\d+):(?P-1|\d+)| + {} +) +(?.+/.+)|.+)" +nd_pattern = r"(?P\d*\.\d+|\d+)/(?P\d*\.\d+|\d+)" +pr_pattern = re.compile(r""" +{} +(?(slash) + {}| + (?P\d*\.\d+x?|\d+x?) +) +""".format(sl_pattern, nd_pattern), re.X) +tm_pattern = r"""^ +(?: + (?:([01]?\d|2[0-3]):)? + ([0-5]?\d): +)? +(\.\d{1,3}|[0-5]?\d(?:\.\d{1,3})?)$ +""" +sr_tm_pattern = tm_pattern[1:-2] # remove ^$ +sr_end_pattern = r"end" +sr_ful_pattern = r"full" +sr_pattern = re.compile(r"""^ +a=(?P{tm}), +b=(?P{tm}), +(?Pfps|dur|spd)= +{} +(?P + (?(slash) + {}| + {} + ) +)$ +""".format(sl_pattern, nd_pattern, flt_pattern, tm=sr_tm_pattern), re.X) + +def time_str_to_milliseconds(s): # converts a time str to milliseconds # time str syntax: # [hrs:mins:secs.xxx], [mins:secs.xxx], [secs.xxx] hr = 0 minute = 0 sec = 0 - time = time.strip() - if time == '': - raise ValueError('no time specified') - if time.count(':') > 2: - raise ValueError('invalid time format') - val = time.split(':') - n = len(val) + tm_split = s.strip().split(':') + n = len(tm_split) # going backwards in the list # get secs.xxx portion if n >= 1: - if val[-1] != '': - sec = float(val[-1]) - # get mins portion + if tm_split[-1] != '': + sec = float(tm_split[-1]) + # get mins if n >= 2: - if val[-2] != '': - minute = float(val[-2]) - # get hrs portion + if tm_split[-2] != '': + minute = float(tm_split[-2]) + # get hrs if n == 3: - if val[-3] != '': - hr = float(val[-3]) - return (hr * 3600 + minute * 60 + sec) * 1000.0 - - -@validate_chars_in_set(string.digits + '=./' + 'spdurf') -def parse_tval_str(string): - # extract values from TARGET=VALUE string - # target can be fps, dur, spd - tgt = string.split('=')[0] # the `TARGET` portion - val = string.split('=')[1] # the `VALUE` portion - if tgt == 'fps': - val = rate_from_str(val, -1) - elif tgt == 'dur': - val = float(val) * 1000 # duration in ms - elif tgt == 'spd': - val = float(val) - else: - raise ValueError('invalid target') - return tgt, val - - -@validate_chars_in_set(string.digits + 'ab,=./:' + 'spdurf') -def sub_from_str(string): - # returns a subregion from string - # input syntax: - # a=