├── icon ├── icon.icns ├── icon.ico └── icon.png ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── .gitignore ├── README.md └── vidstabgui.py /icon/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorand/vidstabgui/HEAD/icon/icon.icns -------------------------------------------------------------------------------- /icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorand/vidstabgui/HEAD/icon/icon.ico -------------------------------------------------------------------------------- /icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorand/vidstabgui/HEAD/icon/icon.png -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorand/vidstabgui/HEAD/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorand/vidstabgui/HEAD/screenshot2.png -------------------------------------------------------------------------------- /screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorand/vidstabgui/HEAD/screenshot3.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.zip 3 | *.trf 4 | *.app 5 | ffmpeg 6 | dist/ 7 | 8 | _releases/ 9 | _issues.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VidStab GUI 2 | 3 | A simple graphical user interface for the [VidStab](https://github.com/georgmartius/vid.stab) video stabilization library. 4 | 5 | You can download the application to Windows or OSX at the [releases page](https://github.com/hlorand/vidstabgui/releases) 6 | 7 | *Sometimes Windows Defender marks vidstabgui.exe as a virus, which is a false positive alarm (because the exe files made with Pyinstaller rarely recognized by Windows as a virus). You can safely add it as an exception to your antivirus software and run it. If you are skeptical download the source code from GitHub and run `vidstabgui.py` directly in Python.* 8 | 9 | | | 10 | |--| 11 | | ![](screenshot1.png) | 12 | | ![](screenshot2.png) | 13 | 14 | ## Build 15 | 16 | **Windows (64 bit)** 17 | 18 | - Requirements: [py2exe](https://pypi.org/project/py2exe/) 19 | - Open an administrative Power Shell 20 | - Navigate to `build-scripts\win\` 21 | - Run `.\build-py2exe.ps1` 22 | - The configuration file for the script is: `setup.py` 23 | 24 | For 32 bit Windows you can use this build script, the only difference is that after the script finished, you have to manually download a [32 bit ffmpeg build](https://github.com/advancedfx/ffmpeg.zeranoe.com-builds-mirror/releases/tag/20200915) and replace `dist\ffmpeg.exe` with it (which is a 64 bit build). 25 | 26 | *If you receive an error message that says "VCRUNTIME140.dll not found", please install [Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685)* 27 | 28 | **OSX** 29 | 30 | - Requirements: [py2app](https://pypi.org/project/py2app/) 31 | - Open a Terminal 32 | - Navigate to `build-scripts/osx/` 33 | - `chmod +x ./build-py2app.sh` 34 | - Run `./build-py2app.sh` 35 | -------------------------------------------------------------------------------- /vidstabgui.py: -------------------------------------------------------------------------------- 1 | from tkinter import * 2 | from tkinter import filedialog as Filedialog 3 | from tkinter import messagebox as Messagebox 4 | from tkinter import ttk 5 | import subprocess 6 | import time 7 | import os 8 | import re 9 | 10 | tk = Tk() 11 | tk.title("Video Stabilization GUI") 12 | 13 | """ 14 | Base class for every GUI element. 15 | 16 | Stores the name and description. First it creates a frame and a label for the element, then 17 | places the element into the grid. 18 | 19 | Every GUI element has a numeric value that is used when generating the ffmpeg command. This 20 | value is stored in a variable called "valueHolder". You can get this value as is with getValue() 21 | or you can get the command line attribute in "valuename=value" format with getAttribute(). 22 | """ 23 | class GuiThing: 24 | valueHolder = None 25 | 26 | def __init__(self, name, description, tab): 27 | self.name = name 28 | 29 | self.frame = LabelFrame(tab, text=name.capitalize(), padx=10, pady=5) 30 | self.frame.pack(anchor=NW, fill='x') 31 | 32 | self.label = Label(self.frame, text=description, wraplength=500) 33 | self.label.pack(anchor=NW) 34 | 35 | def getValue(self): 36 | return self.valueHolder.get() 37 | 38 | def getArgument(self): 39 | return self.name + "=" + str(self.valueHolder.get()) 40 | 41 | 42 | """ 43 | GUI element responsible for opening files 44 | 45 | A single button that calls the browse() method, with which you can browse for video files. 46 | """ 47 | class GuiFiles(GuiThing): 48 | def browse(self): 49 | types = [ ('MP4 files', '*.mp4'), 50 | ('MOV files', '*.mov'), 51 | ('AVI files', '*.avi'), 52 | ('MPG files', '*.mpg'), 53 | ('MPEG files','*.mpeg'), 54 | ('M4V files', '*.m4v') ] 55 | self.files = list( Filedialog.askopenfilename(multiple=True, filetypes=types ) ) 56 | 57 | # Add filename to Listbox 58 | filelist.delete(0,self.list.size()) 59 | self.list.delete(0,self.list.size()) 60 | for i in range(len(self.files)): 61 | filename = self.files[i].split("/").pop() 62 | self.list.insert(i, filename) 63 | filelist.insert(i, filename) 64 | 65 | def __init__(self, name, description, tab): 66 | super().__init__(name, description, tab) 67 | 68 | self.files = [] 69 | 70 | self.button = Button(self.frame,text = "Browse video files...", padx="10", command=self.browse) 71 | self.button.pack(anchor=NW) 72 | 73 | self.list = Listbox(self.frame, width=80, height=25) 74 | self.list.pack(anchor=NW, expand = 1, fill ="both") 75 | 76 | 77 | """ 78 | Slider GUI element 79 | 80 | A simple slider. You can set the slider start and end value and step size. 81 | """ 82 | class GuiSlider(GuiThing): 83 | def __init__(self, name, description, tab, start, end, default, stepsize=1): 84 | super().__init__(name, description, tab) 85 | 86 | self.valueHolder = Scale(self.frame, from_=start, to=end, length=550, resolution=stepsize, orient=HORIZONTAL) 87 | self.valueHolder.pack(anchor=NW) 88 | self.valueHolder.set(default) 89 | 90 | 91 | """ 92 | Radio button group GUI element which 93 | 94 | You can set the options (value-description pairs) in the options list 95 | """ 96 | class GuiRadio(GuiThing): 97 | def __init__(self, name, description, tab, options): 98 | super().__init__(name, description, tab) 99 | 100 | self.options = options 101 | 102 | # Add every option as a radio button 103 | self.value = IntVar() 104 | for i in range(len(options)): 105 | text = f"{options[i][0]}" 106 | if options[i][1]: 107 | text += f" ({options[i][1]})" 108 | radio = Radiobutton(self.frame, text=text, variable=self.value, value=i) 109 | radio.pack(anchor=NW) 110 | 111 | self.valueHolder = self 112 | 113 | # Define a get() attribute to get the selected radio button value from the options list (and not as a single int) 114 | def get(self): 115 | return self.options[ self.value.get() ][0] 116 | 117 | 118 | """ 119 | A function that calls ffmpeg with the selected settings 120 | """ 121 | def stabilize(): 122 | # Check if files selected 123 | if len(files.files) == 0: 124 | Messagebox.showerror(title="Error", message="No files selected") 125 | return 126 | 127 | # Change directory to script directory - remove the "if" condition if building with pyinstaller on Windows 128 | if os.name == "posix": 129 | abspath = os.path.abspath(__file__) 130 | dirname = os.path.dirname(abspath) 131 | os.chdir(dirname) 132 | print("Working directory:", os.getcwd()) 133 | 134 | # Check if ffmpeg.exe is present 135 | if os.path.isfile("./ffmpeg.exe"): 136 | ffmpeg = "ffmpeg.exe" 137 | elif os.path.isfile("./ffmpeg"): 138 | ffmpeg = "./ffmpeg" 139 | else: 140 | Messagebox.showerror(title="Error", message="Can't find ffmpeg. Please download the build from https://ffmpeg.org/download.html and place it next to the vidstabgui") 141 | return 142 | 143 | for file in files.files: 144 | # Generate output filename. ( inputfilename.stabilized.time.mp4 ) 145 | output = ".".join( file.split(".")[0:-1] + ["stabilized",str(int(time.time())), "mp4"] ) 146 | 147 | # Show notification that the analysis has started 148 | index = files.files.index(file) 149 | filelist.delete(index) 150 | filelist.insert(index, "(Analysing) " + file.split("/").pop() ) 151 | filelist.selection_set(index) 152 | tk.update() 153 | 154 | # Analyze motion - only if transform file not generated yet 155 | slug = re.sub(r'[\W_]+', '-', file) 156 | transformfile = slug + str(shakiness.getValue()) + ".trf" 157 | 158 | if not os.path.isfile( transformfile ): 159 | command = f"{ffmpeg} -i \"{file}\"" 160 | command += f" -vf vidstabdetect={shakiness.getArgument()}:result={transformfile}" 161 | command += f" -f null -" 162 | print(command) 163 | subprocess.call(command, shell=bool(showconsole.get()) ) 164 | else: 165 | print(f"{transformfile} exists, skipping analyzation.") 166 | 167 | # Show notification that the stabilization process has started 168 | filelist.delete(index) 169 | filelist.insert(index, "(Stabilizing) " + file.split("/").pop() ) 170 | filelist.selection_set(index) 171 | tk.update() 172 | 173 | # Stabilize video 174 | command = f"{ffmpeg} -i \"{file}\"" 175 | command += f" -crf {crf.getValue()}" 176 | command += f" -preset {preset.getValue()}" 177 | command += f" -threads {limitcpu.getValue()}" 178 | if speedup.getValue() > 1: 179 | fps = min(30*speedup.getValue(), 120) # clamp fps 0-120 180 | command += f" -r {fps}" 181 | command += f" -af atempo={speedup.getValue()}" 182 | command += f" -vf setpts={1/speedup.getValue()}*PTS," 183 | else: 184 | command += f" -vf " 185 | command += f"unsharp=5:5:{sharpening.getValue()}:3:3:{sharpening.getValue()/2}," 186 | command += f"vidstabtransform=" 187 | command += f"{smoothing.getArgument()}" 188 | command += f":{crop.getArgument()}" 189 | command += f":{optalgo.getArgument()}" 190 | command += f":{optzoom.getArgument()}" 191 | command += f":{zoom.getArgument()}" 192 | command += f":{zoomspeed.getArgument()}" 193 | command += f":{interpol.getArgument()}" 194 | command += f":{maxshift.getArgument()}" 195 | command += f":maxangle={-1 if maxangle.getValue() < 0 else maxangle.getValue()*3.1415/180}" 196 | command += f":input='{transformfile}'" 197 | command += f" \"{output}\" -y" 198 | print(command) 199 | subprocess.call(command, shell=bool(showconsole.get()) ) 200 | 201 | # Show notification that the file stabilized 202 | filelist.delete(index) 203 | filelist.insert(index, "(OK) " + file.split("/").pop() ) 204 | filelist.selection_set(index) 205 | tk.update() 206 | 207 | # Open output folder in the file manager 208 | outputfolder = "/".join( file.split("/")[0:-1] ) 209 | print("Output folder:", outputfolder) 210 | subprocess.call(f"start \"\" \"{outputfolder}\" ", shell=True) # Windows 211 | subprocess.call(f"open \"{outputfolder}\" ", shell=True) # OSX 212 | 213 | 214 | ######################################################################### 215 | # Build GUI 216 | 217 | tabs = ttk.Notebook(tk) 218 | 219 | inputfiles = ttk.Frame(tabs) 220 | stabsettings = ttk.Frame(tabs) 221 | zoomsettings = ttk.Frame(tabs) 222 | mp4settings = ttk.Frame(tabs) 223 | output = ttk.Frame(tabs) 224 | 225 | tabs.add(inputfiles, text ='1) Input files') 226 | tabs.add(stabsettings, text ='2) Stabilization') 227 | tabs.add(zoomsettings, text ='3) Zoom') 228 | tabs.add(mp4settings, text ='4) MP4 settings') 229 | tabs.add(output, text ='5) Start') 230 | tabs.pack(expand = 1, fill ="both") 231 | 232 | files = GuiFiles("input files", "Open mp4, mov, avi... videos here", inputfiles) 233 | 234 | shakiness = GuiSlider("shakiness", "On a scale of 1 to 10 how quick is the camera shake in your opinion?", stabsettings, 1, 10, 6) 235 | smoothing = GuiSlider("smoothing", "Number of frames used in stabilization process. Bigger frame count = smoother motion. (A number of 10 means that 21 frames are used: 10 in the past and 10 in the future.) ", stabsettings, 1, 1000, 60,5) 236 | optalgo = GuiRadio("optalgo", "Set the camera path optimization algorithm.", stabsettings, [ ["gauss","Gaussian kernel low-pass filter on camera motion"],["avg"," Averaging on transformations"] ]) 237 | interpol = GuiRadio("interpol", "Specify type of interpolation", stabsettings, [ ["bilinear","Linear in both directions"],["bicubic","Cubic in both directions - slow"],["linear","Linear only horizontal"],["no","No interpolation"] ]) 238 | maxshift = GuiSlider("maxshift", "Set maximal number of pixels to translate frames. (-1 = no limit)", stabsettings, -1, 640, -1) 239 | maxangle = GuiSlider("maxangle", "Set maximal angle in degrees to rotate frames. (-1 = no limit)", stabsettings, -1, 360, -1) 240 | 241 | optzoom = GuiRadio("optzoom", "Set optimal zooming to avoid blank-borders. ", zoomsettings, [ ["0","Disabled"],["1","Optimal static zoom value is determined"], ["2", "Optimal adaptive zoom value is determined"] ]) 242 | zoom = GuiSlider("zoom", "Zoom in or out (percentage).", zoomsettings, -100, 100, 0) 243 | zoomspeed = GuiSlider("zoomspeed", "Set percent to zoom maximally each frame (enabled when optzoom is set to 2).", zoomsettings, 0, 5, 0.25, 0.05) 244 | crop = GuiRadio("crop", "How to deal with empty frame borders", zoomsettings, [ ["black","Fill the border-areas black."],["keep","Keep image information from previous frame"] ]) 245 | 246 | preset = GuiRadio("preset", "Select encoding speed. More speed = bigger file size.", mp4settings, [ ["medium",None],["faster",None], ["slower", None], ["ultrafast", None] ]) 247 | limitcpu = GuiSlider("limit cpu", "Limit CPU cores when encoding. (0 = no limit)", mp4settings, 0, 16, 0, 1) 248 | sharpening = GuiSlider("sharpening", "A little bit of sharpening is recommended after stabilization.", mp4settings, 0, 1.5, 0.8, 0.05) 249 | crf = GuiSlider("crf", "Output video compression rate factor. Smaller crf: Better quality, greater file size. Bigger crf: Better compression, smaller file size.", mp4settings, 0, 51, 21) 250 | speedup = GuiSlider("speed up", "Speed up video for hyperlapse effect. Use more smoothing for better stabilization.", mp4settings, 1, 20, 1, 1) 251 | 252 | # A frame that holds the Stabilize button 253 | stabilizeFrame = LabelFrame(output, text="Stabilize", padx=0, pady=0) 254 | stabilizeFrame.pack(anchor=NW, fill='x') 255 | 256 | filelist = Listbox(stabilizeFrame, width=80, height=25) 257 | filelist.pack(anchor=NW, expand = 1, fill ="both") 258 | 259 | stabilizeButton = Button(stabilizeFrame, text="Stabilize", padx=20, pady=10, bg="white", command=stabilize ) 260 | stabilizeButton.pack(pady=10) 261 | 262 | showconsole = IntVar() 263 | showconsole.set(1) 264 | if os.name != "posix": 265 | showconsolecheck = Checkbutton(stabilizeFrame, text='Show console during stabilization',variable=showconsole, onvalue=0, offvalue=1) 266 | showconsolecheck.pack(anchor=NW) 267 | 268 | tk.mainloop() 269 | --------------------------------------------------------------------------------