Skip to content

Instantly share code, notes, and snippets.

@royshil
Last active June 5, 2023 02:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save royshil/236ee8d51961768511c1fe56461c3817 to your computer and use it in GitHub Desktop.
Save royshil/236ee8d51961768511c1fe56461c3817 to your computer and use it in GitHub Desktop.
Quicrop! A minimal tool for cropping video with Tkinter and ffmpeg-python
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