From d4556f8ed16793043fec841386f4acbcbd87fd86 Mon Sep 17 00:00:00 2001 From: Couleur <82747632+couleurm@users.noreply.github.com> Date: Mon, 16 May 2022 12:36:08 +0200 Subject: [PATCH] rewrite Smoothie into multiple files, add flowblur --- settings/recipe.ini | 8 +- smoothie.py | 206 ------------------------------------- src/exec.py | 185 +++++++++++++++++++++++++++++++++ src/helpers.py | 25 +++++ src/main.py | 19 ++++ blender.vpy => vitamix.vpy | 103 ++++++++++++------- 6 files changed, 301 insertions(+), 245 deletions(-) delete mode 100644 smoothie.py create mode 100644 src/exec.py create mode 100644 src/helpers.py create mode 100644 src/main.py rename blender.vpy => vitamix.vpy (56%) diff --git a/settings/recipe.ini b/settings/recipe.ini index 2baca62..fe3ac79 100644 --- a/settings/recipe.ini +++ b/settings/recipe.ini @@ -12,18 +12,24 @@ gpu=true [frame blending] enabled=yes fps=60 -intensity=1.27 +intensity=1.2 weighting=equal +[flowblur] +amount=125 +mask= + [encoding] process=ffmpeg args=-c:v libx264 -preset slow -crf 15 [misc] +dingafter=5 folder= deduplication=y container=.MP4 flavors=fruits +dedupthreshold=0.01 [timescale] in=1.0 diff --git a/smoothie.py b/smoothie.py deleted file mode 100644 index 86f60dc..0000000 --- a/smoothie.py +++ /dev/null @@ -1,206 +0,0 @@ -from argparse import ArgumentParser -from sys import argv, exit -from os import path, system#, getcwd -from configparser import ConfigParser -import subprocess -from subprocess import run, PIPE, Popen -from random import choice # Randomize smoothie's flavor -import platform # Get OS (detect win/linux) - -if platform.architecture()[0] != '64bit': - print('This script is only compatible with 64bit systems.') - exit(1) - -isLinux=platform.system() == 'Linux' -isWin=platform.system() == 'Windows' -if not isWin and not isLinux: - print(f'Unsupported OS "{platform.system()}"') - exit(1) - -def pause(text): - none = input(text) - -# Bool aliases -yes = ['True','true','yes','y','1'] -no = ['False','false','no','n','0','null','',None] - -parser = ArgumentParser() -parser.add_argument("-peek", "-p", help="render a specific frame (outputs an image)", action="store", nargs=1, metavar='752', type=int) -parser.add_argument("-trim", "-t", help="Trim out the frames you don't want to use", action="store", nargs=1, metavar='0:23,1:34', type=str) -parser.add_argument("-dir", help="opens the directory where Smoothie resides", action="store_true" ) -parser.add_argument("-recipe", "-rc", help="opens default recipe.ini", action="store_true" ) -parser.add_argument("--config", "-c", help="specify override config file", action="store", nargs=1, metavar='PATH', type=str) -parser.add_argument("--encoding","-enc", help="specify override ffmpeg encoding arguments", action="store", type=str) -parser.add_argument("-verbose", "-v", help="increase output verbosity", action="store_true" ) -parser.add_argument("-curdir", "-cd", help="save all output to current directory", action="store_true", ) -parser.add_argument("-input", "-i", help="specify input video path(s)", action="store", nargs="+", metavar='PATH', type=str) -parser.add_argument("-output", "-o", help="specify output video path(s)", action="store", nargs="+", metavar='PATH', type=str) -parser.add_argument("-vpy", help="specify a VapourSynth script", action="store", nargs=1, metavar='PATH', type=str) -args = parser.parse_args() - -if args.dir: - scriptDir = path.dirname(__file__) - if isWin: - run(f'explorer {scriptDir}') - exit(0) - elif isLinux: - print(scriptDir) - exit(0) - -if args.recipe: - - recipe = path.abspath(path.join(path.dirname(__file__), "settings/recipe.ini")) - - if path.exists(recipe) == False: - print("config path does not exist (are you messing with files?), exitting..") - pause() - exit(1) - - if isWin: - run(path.abspath(recipe), shell=True) - exit(0) - elif isLinux: - print('What code editor would you like to open your recipe with? (e.g nano, vim, code)') - print(f'This file is located at {recipe}') - editor = input('Editor:') - run(f'{path.abspath(editor)} {path.abspath(recipe)}', shell=True) - - -conf = ConfigParser() - -if args.config: - config_filepath = path.abspath(args.config[0]) - conf.read(config_filepath) -else: - config_filepath = path.abspath(path.join(path.dirname(__file__), "settings/recipe.ini")) - conf.read(config_filepath) - -if path.exists(config_filepath) in [False,None]: - print("config path does not exist, exitting") - run('powershell -NoLogo') -elif args.verbose: - print(f"VERBOSE: using config file: {config_filepath}") - -if args.input in [no, None]: - parser.parse_args('-h'.split()) # If the user does not pass any args, just redirect to -h (Help) - -round = 0 # Reset the round counter - -for video in args.input: # Loops through every single video - - if not args.verbose: - if isWin: - clear = 'cls' - elif isLinux: - clear = 'clear' - run(clear, shell = True) - - round += 1 - - title = "Smoothie - " + path.basename(video) - - if len(args.input) > 1: - title = f'[{round}/{len(args.input)}] ' + title - - if isWin: - system(f"title {title}") - - # Suffix - - if str(conf['misc']['flavors']) in [yes,'fruits']: - flavors = [ - 'Berry','Cherry','Cranberry','Coconut','Kiwi','Avocado','Durian','Lemon','Lime','Fig','Mirabelle', - 'Peach','Apricot','Grape','Melon','Papaya','Banana','Apple','Pear','Orange','Mango','Plum','Pitaya' - ] - else: - flavors = ['Smoothie'] - - # Extension - - if args.peek: - ext = '.png' - elif conf['misc']['container'] in no: - ext = path.splitext(video)[1] - else: - ext = conf['misc']['container'] - - filename = path.basename(path.splitext(video)[0]) - - # Directory - - if args.curdir: - outdir = path.abspath(path.curdir) - elif conf['misc']['folder'] in no: - outdir = path.dirname(video) - else: - outdir = conf['misc']['folder'] - - out = path.join(outdir, filename + f' - {choice(flavors)}{ext}') - - count=2 - while path.exists(out): - out = path.join(outdir, f'{filename} - {choice(flavors)} ({count}){ext}') - count+=1 - - if args.output: - if ((type(args.input) is list) and (len(args.input) == 1)): - out = args.output[0] - - # VapourSynth - if isWin: - vspipe = path.join(path.dirname((path.dirname(__file__))),'VapourSynth','VSPipe.exe') - - elif isLinux: - vspipe = 'vspipe' - - if args.vpy: - - if path.dirname(args.vpy[0]) in no: - - vpy = path.join( path.dirname(__file__), (args.vpy[0]) ) - else: - vpy = path.abspath(args.vpy[0]) - else: - vpy = path.abspath(path.join(path.dirname(__file__),'blender.vpy')) - - command = [ # This is the master command, it gets appended some extra output args later down - f'{vspipe} "{vpy}" --arg input_video="{path.abspath(video)}" --arg config_filepath="{config_filepath}" -c y4m - ', - f'{conf["encoding"]["process"]} -hide_banner -loglevel error -stats -i - ', - ] - - if isWin: - map = '-map 0:v -map 1:a?' - elif isLinux: - map = '-map 0:v -map 1:a\?' - - if args.peek: - frame = int(args.peek[0]) # Extracting the frame passed from the singular array - command[0] += f'--start {frame} --end {frame}' - command[1] += f' "{out}"' # No need to specify audio map, simple image output - elif args.trim: - command[0] += f'--arg trim="{args.trim}"' - command[1] += f'{conf["encoding"]["args"]} "{out}"' - else: - # Adds as input the video to get it's audio tracks and gets encoding arguments from the config file - command[1] += f'-i "{path.abspath(video)}" {map} {conf["encoding"]["args"]} "{out}"' - - if args.verbose: - command[0] += ' --arg verbose=True' - for cmd in command: print(f"{cmd}\n") - print(f"Queuing video: {video}") - - #if run(' '.join(command),shell=True).returncode != 0: - # print(f"Something went wrong with {video}, press any key to un-pause") - # system('pause>nul') - - #run(command[1], stdin = Popen(command[0], stdout = PIPE).stdout) - #ps = subprocess.Popen((command[0]), stdout=subprocess.PIPE) - #output = subprocess.check_output((command[1]), stdin=ps.stdout) - #ps.wait() - exitcode = run((command[0] + '|' + command[1]), shell=True).returncode - if exitcode != 0: - print(f"Something went wrong with {video}, press any key to un-pause") - if isWin: system('pause>nul') - exit(1) - - system(f"title [{round}/{len(args.input)}] Smoothie - Finished! (EOF)") \ No newline at end of file diff --git a/src/exec.py b/src/exec.py new file mode 100644 index 0000000..ccbde20 --- /dev/null +++ b/src/exec.py @@ -0,0 +1,185 @@ +from helpers import * +from os import path, system, listdir +from glob import glob as resolve +from random import choice # Randomize smoothie's flavor +from configparser import ConfigParser +from subprocess import run +from sys import argv, exit + +def voidargs(args): + if args.dir: + scriptDir = path.dirname(argv[0]) + if isWin: + run(f'explorer {scriptDir}') + exit(0) + elif isLinux: + print(scriptDir) + exit(0) + + if args.recipe: + recipe = path.abspath(path.join(path.dirname(argv[0]), "settings/recipe.ini")) + if path.exists(recipe) == False: + print("recipe (config) path does not exist (are you messing with files?), exitting..") + pause() + exit(1) + if isWin: + run(path.abspath(recipe), shell=True) + exit(0) + elif isLinux: + print('What code editor would you like to open your recipe with? (e.g nano, vim, code)') + print(f'This file is located at {recipe}') + editor = input('Editor:') + run(f'{path.abspath(editor)} {path.abspath(recipe)}', shell=True) + +def runvpy(parser): + args = parser.parse_args() + + if args.input in [no, None]: + parser.print_help() # If the user does not pass any args, just redirect to -h (Help) + exit(0) + + if isWin and not args.verbose: + import win32gui + import win32con + hwnd = win32gui.GetForegroundWindow() + win32gui.SetWindowPos(hwnd,win32con.HWND_TOPMOST,0,0,1000,200,0) + + # This is done in order to be "directory agnostic" + # Just read it as, if I'm in a folder called src, do cd .. + parent, dir = path.split( path.split(path.abspath(argv[0]))[0] ) + if dir == 'src': + step = path.join(parent, path.basename(argv[0])) + if args.verbose: print(f'Stepping down from {argv[0]} to {step}') + argv[0] = step + + voidargs(args) + + conf = ConfigParser() + + if args.config: + config_filepath = path.abspath(args.config) + conf.read(config_filepath) + else: + config_filepath = path.abspath(path.join(path.dirname(argv[0]), "settings/recipe.ini")) + conf.read(config_filepath) + + mask_directory = path.abspath(path.join(path.dirname(argv[0]), "masks")) + if not path.exists(mask_directory): + print(f"mask folder does not exist, exitting (looked for {mask_directory})") + pause(); exit() + + + if not path.exists(config_filepath): + print(f"config path does not exist, exitting (looked for {config_filepath})") + pause(); exit() + if args.verbose: + print(f"VERBOSE: using config file: {config_filepath}") + + round = 0 + for video in args.input: + + if not args.verbose: + system('cls' if isWin else 'clear') # cls on windows, clear on anything else + + if '*' in video: # If filepath contains wildcard, resolve them + for file in resolve(video): + if path.isfile(file): + args.input.append(file) + continue + elif not path.exists(video): + print(f"Filepath {video} does not exist, skipping..") + continue + + if path.isdir(video): + for file in listdir(video): + file = path.join(video, file) + if path.isfile(file): + args.input.append(file) + continue + + video = path.expandvars( + path.abspath(video) + ) + + if isWin: + round += 1 + title = "Smoothie - " + path.basename(video) + if len(args.input) > 1: + title = f'[{round}/{len(args.input)}] ' + title + system(f"title {title}") + + + flavors = [ + 'Berry','Cherry','Cranberry','Coconut','Kiwi','Avocado','Durian','Lemon','Lime','Fig','Mirabelle', + 'Peach','Apricot','Grape','Melon','Papaya','Banana','Apple','Pear','Orange','Mango','Plum','Pitaya' + ] if str(conf['misc']['flavors']) in [yes,'fruits'] else ['Smoothie'] + + if args.curdir: + outdir = path.abspath(path.curdir) + elif conf['misc']['folder'] in no: + outdir = path.abspath(path.dirname(video)) + else: + outdir = conf['misc']['folder'] + + + if args.peek: + ext = '.png' + elif conf['misc']['container'] in no: + ext = path.splitext(video)[1] + else: + ext = conf['misc']['container'] + + filename = path.splitext(path.basename(video))[0] + + out = path.join(outdir, f'{filename} - {choice(flavors)}{ext}') + + count=2 + while path.exists(out): + out = path.join(outdir, f'{filename} - {choice(flavors)} ({count}){ext}') + count+=1 + + if args.output: + if ((type(args.input) is list) and (len(args.input) == 1)): + out = args.output[0] + + if isWin: + vspipe = path.join(path.dirname(path.dirname(argv[0])),'VapourSynth','VSPipe.exe') + elif isLinux: + vspipe = 'vspipe' + + if args.vpy: + if path.dirname(args.vpy[0]) in no: + vpy = path.join( path.dirname(argv[0]), (args.vpy[0]) ) + else: + vpy = path.abspath(args.vpy[0]) + else: vpy = path.abspath(path.join(path.dirname(argv[0]),'vitamix.vpy')) + + command = [ # This is the master command, it gets appended some extra output args later down + f'{vspipe} "{vpy}" --arg input_video="{path.abspath(video)}" --arg mask_directory="{mask_directory}" --arg config_filepath="{config_filepath}" -y - ', + f'{conf["encoding"]["process"]} -hide_banner -loglevel error -stats -i - ', + ] + + map = '-map 0:v -map 1:a?' if isWin else '-map 0:v -map 1:a\?' # Mapping the audio's file, Linux needs an escape character + + if args.peek: + frame = int(args.peek[0]) # Extracting the frame passed from the singular array + command[0] += f'--start {frame} --end {frame}' + command[1] += f' "{out}"' # No need to specify audio map, simple image output + elif args.trim: + command[0] += f'--arg trim="{args.trim}"' + command[1] += f'{conf["encoding"]["args"]} "{out}"' + else: + # Adds as input the video to get it's audio tracks and gets encoding arguments from the config file + command[1] += f'-i "{path.abspath(video)}" {map} {conf["encoding"]["args"]} "{out}"' + + if args.verbose: + command[0] += ' --arg verbose=True' + for cmd in command: print(f"{cmd}\n") + print(f"Queuing video: {video}, vspipe is {vspipe}") + + exitcode = run((command[0] + '|' + command[1]), shell=True).returncode + if exitcode != 0 and not args.verbose: + print(f"Something went wrong with {video}") + pause() ; exit(1) + + system(f"title [{round}/{len(args.input)}] Smoothie - Finished! (EOF)") \ No newline at end of file diff --git a/src/helpers.py b/src/helpers.py new file mode 100644 index 0000000..6c7cb9c --- /dev/null +++ b/src/helpers.py @@ -0,0 +1,25 @@ + +from platform import architecture, system +from getpass import getpass +from sys import exit + +def checkOS (): + from platform import architecture, system + if architecture()[0] != '64bit': + print('This script is only compatible with 64bit systems.') + exit(1) + + if system() not in ['Linux', 'Windows']: + # If hasn't returned yet then throw + print(f'Unsupported OS "{system()}"') + exit(1) + +isLinux = system() == 'Linux' +isWin = system() == 'Windows' + +def pause(): + getpass('Press enter to continue..') + +# Bool aliases +yes = ['True','true','yes','y','1'] +no = ['False','false','no','n','0','null','',None] diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..233641f --- /dev/null +++ b/src/main.py @@ -0,0 +1,19 @@ +import exec +import helpers +helpers.checkOS() + +from argparse import ArgumentParser +parser = ArgumentParser() +parser.add_argument("-peek", "-p", help="render a specific frame (outputs an image)", action="store", nargs=1, metavar='752', type=int) +parser.add_argument("-trim", "-t", help="Trim out the frames you don't want to use", action="store", nargs=1, metavar='0:23,1:34', type=str) +parser.add_argument("-dir", help="opens the directory where Smoothie resides", action="store_true" ) +parser.add_argument("-recipe", "-rc", help="opens default recipe.ini", action="store_true" ) +parser.add_argument("--config", "-c", help="specify override config file", action="store", nargs=1, metavar='PATH', type=str) +parser.add_argument("--encoding", "-enc", help="specify override ffmpeg encoding arguments", action="store", type=str) +parser.add_argument("-verbose", "-v", help="increase output verbosity", action="store_true" ) +parser.add_argument("-curdir", "-cd", help="save all output to current directory", action="store_true", ) +parser.add_argument("-input", "-i", help="specify input video path(s)", action="store", nargs="+", metavar='PATH', type=str) +parser.add_argument("-output", "-o", help="specify output video path(s)", action="store", nargs="+", metavar='PATH', type=str) +parser.add_argument("-vpy", help="specify a VapourSynth script", action="store", nargs=1, metavar='PATH', type=str) + +exec.runvpy(parser) \ No newline at end of file diff --git a/blender.vpy b/vitamix.vpy similarity index 56% rename from blender.vpy rename to vitamix.vpy index 4d1b37f..2d4d2a6 100644 --- a/blender.vpy +++ b/vitamix.vpy @@ -1,14 +1,16 @@ -from vapoursynth import core +from os import path +import sys +sys.path.append(path.curdir) import vapoursynth as vs -from os import path # To split file extension +from vapoursynth import core from configparser import ConfigParser -import havsfunc # aka Interframe2 +import havsfunc -conf = ConfigParser() -conf.read(config_filepath) -import logging -logging.basicConfig(level=logging.DEBUG) +# Bool aliases +yes = ['True','true','yes','y','1'] +no = ['False','false','no','n','0','null','',None] + def defined(var): # variable to test must be provided as a string @@ -19,24 +21,31 @@ def defined(var): return False else: return True - + def verb(msg): + import logging + logging.basicConfig(level=logging.DEBUG) if defined('verbose') == True: print(logging.debug(f' {msg}')) + +conf = ConfigParser() +conf.read(config_filepath) + +core.num_threads=16 #8 #16 +core.add_cache=True +core.max_cache_size=4000 -# Bool aliases -yes = ['True','true','yes','y','1'] -no = ['False','false','no','n','0','null','',None] +verb('Starting indexing..') if path.splitext(input_video)[1] == '.avi': video = core.avisource.AVISource(input_video) - video = core.fmtc.matrix(clip=video, mat="709", col_fam=vs.YUV, bits=16) # If you're gonna bother with .AVI (let me know of one valid case to use this container) you're probably using 709 anyways + video = core.fmtc.matrix(clip=video, mat="709", col_fam=vs.YUV, bits=16) + # If you're gonna bother working with .AVI you're probably using 709 anyways + # (let me know of one valid case to use this container as input) video = core.fmtc.resampling(clip=video, css="420") video = core.fmtc.bitdepth(clip=video, bits=8) else: - verb('Starting indexing..') video = core.ffms2.Source(source=input_video, cache=False) - verb(video) if defined('trim'): @@ -57,31 +66,14 @@ if defined('trim'): ToSeconds(start)*fps, ToSeconds(end)*fps ) -verb('calculated trim') + verb(f'Calculated trim from {ToSeconds(start)} to {ToSeconds(end)}') + if float(conf['timescale']['in']) != 1: # Input timescale, done before interpolation video = core.std.AssumeFPS(video, fpsnum=(video.fps * (1 / float(conf['timescale']['in'])))) -# Commented this out as I don't see RIFE being this much better than SVP, and it's not worth the time / long & confusing installation - -# if str(conf['interpolation']['pre interpolation']['enabled']).lower() in yes: # Pre-interpolating with RIFE -# -# video = core.resize.Bicubic(video, format=vs.RGBS) -# if str(conf['interpolation']['pre interpolation']['rife type']).lower() == 'cuda': -# from vsrife import RIFE -# while video.fps > conf['interpolation']['pre interpolation']['minimum fps']: -# video = RIFE(video) -# elif str(conf['interpolation']['pre interpolation']['rife type']).lower() == 'ncnn': -# video = core.resize.Bicubic(video, format=vs.RGBS) -# while video.fps < conf['interpolation']['pre interpolation']['minimum fps']: -# video = core.rife.RIFE(video) -# video = core.resize.Bicubic(video, format=vs.YUV420P8, matrix_s="709") - if str(conf['interpolation']['enabled']).lower() in yes: # Interpolation using Interframe2 (uses SVP-Flow, which is also what blur uses) - if (conf['interpolation']['gpu']) in yes: - useGPU = True - elif (conf['interpolation']['gpu']) in no: - useGPU = False + useGPU = (conf['interpolation']['gpu']) in yes if str(conf['interpolation']['fps']).endswith('x'): # if multiplier support interp_fps = int(video.fps * int((conf['interpolation']['fps']).replace('x',''))) @@ -96,13 +88,16 @@ if str(conf['interpolation']['enabled']).lower() in yes: # Interpolation using I Tuning=str(conf['interpolation']['tuning']), OverrideAlgo=int(conf['interpolation']['algorithm']) ) - + if float(conf['timescale']['out']) != 1: # Output timescale, done after interpolation video = core.std.AssumeFPS(video, fpsnum=(video.fps * float(conf['timescale']['out']))) -if str(conf['misc']['deduplication']).lower() in yes: +if conf['misc']['dedupthreshold'] not in no: import filldrops - video = filldrops.FillDrops(video, thresh=0.001) + video = filldrops.FillDrops( + video, + thresh = float((conf['misc']['dedupthreshold'])) + ) if str(conf['frame blending']['enabled']).lower() in yes: @@ -120,7 +115,39 @@ if str(conf['frame blending']['enabled']).lower() in yes: elif type(repartition) is str: weights = eval(f'weighting.{repartition}({blended_frames})') verb(f"Weights: {weights}") - video = core.frameblender.FrameBlend(video, weights, True) + video = core.frameblender.FrameBlend(video, weights) video = havsfunc.ChangeFPS(video, int(conf['frame blending']['fps'])) + +if conf['flowblur']['amount'] not in no: + original = video # Makes an un-smeared copy to use for the mask later + + s = core.mv.Super(video, 16, 16, rfilter=3) + bv = core.mv.Analyse(s, isb=True, blksize=16, plevel=2, dct=5) + fv = core.mv.Analyse(s, blksize=16, plevel=2, dct=5) + video = core.mv.FlowBlur(video, s, bv, fv, blur=(conf['flowblur']['amount'])) + + if conf['flowblur']['mask'] not in no: + + mask = conf['flowblur']['mask'] + verb(f'Mixing in {mask_directory} and {mask}..') + if not path.exists(mask): # Then user specified a relative path, and needs to be verified + if '.' in mask: # Then the user specified a file extension + mask = path.join(mask_directory, f'{mask}') + else: # Then the user did not specify any image extension and it needs to loop through common exts + for extension in ['png','jpg','jpeg']: + if not path.exists(mask): + mask = path.join(mask_directory, f'{mask}.{extension}') + else: + continue # Loops until it ends if it found valid mask path + + if not path.exists(mask): # Then even if we did some checks to convert to absolute path it still does not exists + raise vs.Error(f"The Mask filepath you provided does not exist: {mask}") + + verb(f'Using mask {mask}') + filtered = video.std.Expr(expr=['x 0 -','','']) + GW = core.ffms2.Source(mask, cache=False) + BW = GW.resize.Bicubic(video.width,video.height, matrix_s='709',format=vs.GRAY8) + BW = BW.std.Levels( min_in=0, max_in=235, gamma =0.05, min_out=0, max_out=255) + video = havsfunc.Overlay(original, filtered, mask=BW) video.set_output()