Last active
June 5, 2023 02:18
-
-
Save royshil/236ee8d51961768511c1fe56461c3817 to your computer and use it in GitHub Desktop.
Quicrop! A minimal tool for cropping video with Tkinter and ffmpeg-python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tkinter as tk | |
from tkVideoPlayer import TkinterVideo | |
from ffmpeg import FFmpeg, Progress | |
import os | |
import sys | |
import argparse | |
parser = argparse.ArgumentParser(description="Crop a video") | |
parser.add_argument("video", metavar="video", type=str, help="video file to crop") | |
args = parser.parse_args() | |
videofile = args.video | |
if not os.path.exists(videofile): | |
print(f"video file {videofile} doesnt exist") | |
sys.exit(1) | |
# Create the main window | |
window = tk.Tk() | |
window.minsize(640, 480) | |
window.title("Quicrop!") | |
window.update() | |
my_label = tk.Label(window) | |
my_label.pack(expand=True, fill="both") | |
videoplayer = TkinterVideo(master=my_label, scaled=True, keep_aspect=False) | |
videoplayer.load(videofile) | |
videoplayer.pack(expand=True, fill="both") | |
progress_value = tk.IntVar(my_label) | |
def seek(value): | |
"""used to seek a specific timeframe""" | |
videoplayer.seek(int(value)) | |
def update_scale(event): | |
"""updates the scale value""" | |
progress_value.set(videoplayer.current_duration()) | |
progress_slider = tk.Scale( | |
my_label, variable=progress_value, from_=0, to=0, orient="horizontal", command=seek | |
) | |
# progress_slider.bind("<ButtonRelease-1>", seek) | |
progress_slider.pack(side="left", fill="x", expand=True) | |
def update_duration(event): | |
"""updates the duration after finding the duration""" | |
duration = videoplayer.video_info()["duration"] | |
progress_slider["to"] = duration | |
canvas = tk.Canvas(videoplayer, width=0, height=0, bd=0, highlightthickness=0) | |
canvas.place(x=0, y=0) | |
fields = ["Width:", "Height:", "X-coordinate:", "Y-coordinate:"] | |
entries = [] | |
inputsroot = tk.Label(window) | |
for i, f in enumerate(fields): | |
label = tk.Label(inputsroot, text=f) | |
label.grid(row=i + 1, column=0) | |
entry_var = tk.StringVar() | |
entry = tk.Entry(inputsroot, textvariable=entry_var) | |
entry.grid(row=i + 1, column=1) | |
entries += [entry_var] | |
# Create a submit button that prints the numbers when clicked | |
def on_submit(): | |
print(f"Width: {entries[0].get()}") | |
print(f"Height: {entries[1].get()}") | |
print(f"X-coordinate: {entries[2].get()}") | |
print(f"Y-coordinate: {entries[3].get()}") | |
ffmpeg = ( | |
FFmpeg("/usr/local/bin/ffmpeg") | |
.option("y") | |
.input(videofile) | |
.output( | |
os.path.splitext(videofile)[0] + "_crop.mp4", | |
vf=f"crop={entries[0].get()}:{entries[1].get()}:{entries[2].get()}:{entries[3].get()}", | |
) | |
) | |
@ffmpeg.on("start") | |
def on_start(arguments: list[str]): | |
print("arguments:", arguments) | |
@ffmpeg.on("stderr") | |
def on_stderr(line): | |
print("stderr:", line) | |
@ffmpeg.on("progress") | |
def on_progress(progress: Progress): | |
print(progress) | |
@ffmpeg.on("completed") | |
def on_completed(): | |
print("completed") | |
@ffmpeg.on("terminated") | |
def on_terminated(): | |
print("terminated") | |
ffmpeg.execute() | |
window.destroy() | |
submit_button = tk.Button(inputsroot, text="Crop", command=on_submit) | |
submit_button.grid(row=5, column=1) | |
clickx = 0 | |
origclickx = 0 | |
clicky = 0 | |
origclicky = 0 | |
def vidframecoord(x, y): | |
fx, fy = videoplayer.video_info()["framesize"] | |
return ( | |
int(x / float(videoplayer.winfo_width()) * fx), | |
int(y / float(videoplayer.winfo_height()) * fy), | |
) | |
def getorigin(e): | |
global clickx, clicky, origclickx, origclicky | |
clickx, clicky = vidframecoord(e.x, e.y) | |
origclickx = e.x | |
origclicky = e.y | |
canvas.configure(width=0, height=0) | |
def dragging(e): | |
dragx, dragy = vidframecoord(e.x, e.y) | |
realx = min(clickx, dragx) | |
realy = min(clicky, dragy) | |
width = abs(clickx - dragx) | |
height = abs(clicky - dragy) | |
# make sure width and height are within image bounds | |
if (realx + width) > videoplayer.video_info()["framesize"][0]: | |
width = videoplayer.video_info()["framesize"][0] - realx | |
if (realy + height) > videoplayer.video_info()["framesize"][1]: | |
height = videoplayer.video_info()["framesize"][1] - realy | |
entries[0].set(width) | |
entries[1].set(height) | |
entries[2].set(realx) | |
entries[3].set(realy) | |
canvas.delete("all") | |
# hack the canvas to show up in the right place over the image | |
canvasx = min(origclickx, e.x) | |
canvasy = min(origclicky, e.y) | |
rw = abs(origclickx - e.x) | |
rh = abs(origclicky - e.y) | |
canvas.place(x=canvasx, y=canvasy) | |
canvas.configure(width=rw + 1, height=rh + 1) | |
canvas.create_rectangle(0, 0, rw, rh, fill="#ff0000") | |
canvas.create_line(0, 0, rw, rh, fill="#ff5555") | |
canvas.create_line(0, rh, rw, 0, fill="#ff5555") | |
canvas.create_text(rw / 2, rh / 2, text="CROP", fill="white") | |
inputsroot.pack() | |
def loopvideo(e): | |
videoplayer.seek(0) | |
videoplayer.play() | |
videoplayer.bind("<<SecondChanged>>", update_scale) | |
videoplayer.bind("<<Duration>>", update_duration) | |
videoplayer.bind("<<Ended>>", loopvideo) | |
videoplayer.play() # play the video | |
window.bind("<Escape>", lambda e: window.destroy()) | |
videoplayer.bind("<Button 1>", getorigin) | |
videoplayer.bind("<B1-Motion>", dragging) | |
# Start the GUI event loop | |
window.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment