Compare commits

..

No commits in common. "main" and "v1.1.0" have entirely different histories.
main ... v1.1.0

6 changed files with 92 additions and 657 deletions

View File

@ -3,5 +3,4 @@
Video2Crops is a cross-platform Python-based program that accepts video files, and chops them into a series of still images defined by a custom ROI Video2Crops is a cross-platform Python-based program that accepts video files, and chops them into a series of still images defined by a custom ROI
See [Releases](https://git.rolfsvaag.no/frarol96/Video2Crops/releases) for executables. See [Releases](https://git.rolfsvaag.no/frarol96/Video2Crops/releases) for executables.
See [Wiki](https://git.rolfsvaag.no/frarol96/Video2Crops/wiki) for more information. See [Wiki](https://git.rolfsvaag.no/frarol96/Video2Crops/wiki) for more information.
See [FAQs](https://git.rolfsvaag.no/frarol96/Video2Crops/wiki/FAQs) for frequently asked questions.

View File

@ -1,180 +1,41 @@
import cv2, os, sys, re, threading, webbrowser, screeninfo import cv2
import os
import numpy as np import numpy as np
import tkinter as tk import tkinter as tk
import tkdnd
from tkinterdnd2 import DND_FILES, TkinterDnD from tkinterdnd2 import DND_FILES, TkinterDnD
from tkinter import filedialog, simpledialog, messagebox, scrolledtext from tkinter import filedialog, simpledialog, messagebox
from datetime import datetime from datetime import datetime
import feedparser import sys
import subprocess import webbrowser
import platform import screeninfo
import urllib.request
from v2cmodules.toolbox import extract_version, debug, checkinifile, write2ini, extract_changelog, toggle_save, toggle_show, create_output_directory, truncate_path
from v2cmodules.common import *
checkinifile() # Constants
v2cversion1 = 1
debug('INFO', "Video2Crops starting ...") v2cversion2 = 0
v2cversion3 = 0
v2cversion = f"{v2cversion1}.{v2cversion2}.{v2cversion3}"
frame_step = 100
value_threshold = 11.0
SAVE = True
SHOW = True
processing = False
# Create the root window # Create the root window
root = TkinterDnD.Tk() root = TkinterDnD.Tk()
root.title("Video2Crops") root.title("Video2Crops")
root.geometry(tksize)
def open_changelog_window(): # Function to create the output directory and return its path
debug('DEBUG', 'Loading changelog window') def create_output_directory(video_filename):
changelog_window = tk.Toplevel(root) base_directory = os.getcwd() # You can change this to specify a different base directory
changelog_window.title("Video2Crops Changelog") video_filename_base, _ = os.path.splitext(os.path.basename(video_filename))
output_folder_path = os.path.join(base_directory, video_filename_base)
try: # Create the output directory if it doesn't exist
debug('DEBUG', f"Reading the RSS feed from: {RSS_FEED_URL}") if not os.path.exists(output_folder_path):
# Parse the RSS feed os.makedirs(output_folder_path)
feed = feedparser.parse(RSS_FEED_URL)
if not feed.entries: return output_folder_path
scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD)
scrolled_text.pack(fill=tk.BOTH, expand=True)
scrolled_text.insert(tk.END, "No updates available.")
return
# Create a single scrolled text widget to display all changelog entries
scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD)
scrolled_text.pack(fill=tk.BOTH, expand=True)
debug('DEBUG', f"Done collecting RSS data")
entry_num = 0
for entry in feed.entries:
entry_num += 1
version_str = extract_version(entry.link)
changelog_md = extract_changelog(entry.content[0].value)
if version_str is not None and changelog_md is not None:
version_info = f"# v{version_str}"
print(version_str)
if str(version_str) == str(V2CVERSION) and entry_num == 1:
version_info += "(up to date)"
elif str(version_str) == str(V2CVERSION):
version_info += "(current)"
elif entry_num == 1:
version_info += "(latest)"
version_info += "\n"
date_pattern = r'(\w{3}, \d{2} \w{3} \d{4})'
pub_date = re.search(date_pattern, entry.published)
if pub_date:
rel_date = pub_date.group(1)
version_info += f"\n{rel_date}\n\n"
# Append the version information and changelog entry to the scrolled text widget
changelog_text = version_info + changelog_md + f"\n--------------------\n"
scrolled_text.insert(tk.END, changelog_text)
debug('DEBUG', f"Finished processing changelog")
except Exception as e:
scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD)
scrolled_text.pack(fill=tk.BOTH, expand=True)
scrolled_text.insert(tk.END, f"An error occurred: {str(e)}")
debug('ERROR', f"An error occured while processing changelog:\n{str(e)}\n")
# Function for updating Video2Crops
def check_for_updates():
debug('DEBUG', f"Checking for updates")
try:
# Parse the RSS feed
feed = feedparser.parse(RSS_FEED_URL)
if not feed.entries and not STARTING:
response = tk.messagebox.showinfo(
"No updates found",
f"Latest version: ({V2CVERSION})\n"
f"Current version: ({V2CVERSION})\n"
)
debug('DEBUG', f"No updates found")
return
# Get the latest release
latest_release = feed.entries[0]
latest_version_str = extract_version(latest_release.link)
if latest_version_str is None and not STARTING:
tk.messagebox.showinfo(
"No update Available",
f"Latest version: ({V2CVERSION})\n"
f"Current version: ({V2CVERSION})\n"
)
debug('DEBUG', f"No updates available")
return
# Extract version numbers from the latest release title
latest_version = tuple(map(int, latest_version_str.split('.')))
# Compare the versions
if latest_version > (V2CV[0], V2CV[1], V2CV[2]):
# Prompt the user for an update
response = tk.messagebox.askyesno(
"Update Available",
f"A newer version ({latest_version_str}) is available.\n"
f"Current version: ({V2CVERSION})\n"
"Do you want to update?"
)
debug('DEBUG', f"Found new update, prompting user")
if response:
debug('DEBUG', f"User chose to update ...")
# Determine the platform
current_platform = platform.system().lower()
if current_platform == "windows":
debug('DEBUG', f"Detected Windows system, grabbing installer")
# On Windows, download and run the installer
installer_url = "https://git.rolfsvaag.no/frarol96/Video2Crops/releases/download/v{}/Video2Crops-windows-installer.exe".format(latest_version_str)
try:
urllib.request.urlretrieve(installer_url, "Video2Crops-windows-installer.exe")
subprocess.Popen("Video2Crops-windows-installer.exe", shell=True)
debug('DEBUG', f"Downloaded new installer, running it ...")
root.quit()
except:
tk.messagebox.showinfo(
"Installer not found",
f"The installer file was not found.\nPlease check your antivirus and network connectivity"
)
debug('ERROR', f"Windows installer not found")
elif current_platform == "linux":
debug('DEBUG', f"Detected Linux system, grabbing executable")
# On Linux, download and run the executable
executable_url = "https://git.rolfsvaag.no/frarol96/Video2Crops/releases/download/v{}/Video2Crops-linux".format(latest_version_str)
try:
urllib.request.urlretrieve(executable_url, "Video2Crops-linux")
os.chmod("Video2Crops-linux", 0o755)
subprocess.Popen("./Video2Crops-linux", shell=True)
debug('DEBUG', f"Downloaded new executable, running it ...")
root.quit()
except:
tk.messagebox.showinfo(
"Executable not found",
f"The executable file was not found.\nPlease check your antivirus and network connectivity"
)
debug('ERROR', f"Linux executable not found")
else:
debug('DEBUG', f"Non-supported platform found: {current_platform}")
tk.messagebox.showinfo(
"Non-supported platform",
f"Video2Crops detected non-supported platform: {current_platform}.\nCurrently, only Windows and Linux are supported.\n\nYou can build from source if you need to run Video2Crops on other platforms."
)
else:
debug('INFO', 'User closed prompt window')
else:
if not STARTING:
tk.messagebox.showinfo(
"No update Available",
f"Latest version: ({V2CVERSION})\n"
f"Current version: ({V2CVERSION})\n"
)
debug('DEBUG', f"No update available")
except Exception as e:
tk.messagebox.showerror("Error", f"An error occurred: {str(e)}")
debug('ERROR', f"An error occured while checking for updates:\n{str(e)}\n")
# Function to calculate ROI position based on alignment and size settings # Function to calculate ROI position based on alignment and size settings
def calculate_roi(video_width, video_height, vertical_alignment, horizontal_alignment, roi_size): def calculate_roi(video_width, video_height, vertical_alignment, horizontal_alignment, roi_size):
@ -202,52 +63,19 @@ def calculate_roi(video_width, video_height, vertical_alignment, horizontal_alig
# Create a function to process video files # Create a function to process video files
def process_video(video_filenames, main_window): def process_video(video_filenames, main_window):
debug('DEBUG', f"Starting video processing ...")
global processing global processing
debug('DEBUG', f"Processing state: {processing}") processing = True
def write_log(message): def write_log(message):
logfile_path = os.path.join(output_folder, 'logfile.txt') logfile_path = os.path.join(output_folder, 'logfile.txt')
with open(logfile_path, 'a') as logfile: with open(logfile_path, 'a') as logfile:
log_message = f"{datetime.now().strftime('%m-%d_%H:%M:%S')} - {message}\n" log_message = f"{datetime.now().strftime('%m-%d_%H:%M:%S')} - {message}\n"
logfile.write(log_message) logfile.write(log_message)
debug('LOG EVENT', f"{log_message}") print(log_message)
def calc_value(image): def calc_value(image):
grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return np.std(grayscale) return np.std(grayscale)
def check_duplicate(ref_frame, frame):
global first_frame
min_possible_mse = 0
# Calculate the maximum possible MSE (when images are completely different)
max_possible_mse = 255**2 * roi_size * roi_size
if duplicate_threshold > 0 and not first_frame:
frame_is_duplicate = True
# Calculate the Absolute Difference between the frames
frame_diff = cv2.absdiff(ref_frame, frame)
# Calculate the Mean Squared Error (MSE) between the frames
mse = np.mean(frame_diff)
normalized_mse = 100 * (1 - (mse - min_possible_mse) / (max_possible_mse - min_possible_mse))
if normalized_mse > duplicate_threshold:
# The frame is significantly different, store it
ref_frame = frame.copy()
# Process the frame here or store it for later processing
frame_is_duplicate = False
debug('DEBUG', f"Frame is duplicate: {frame_is_duplicate}, MSE: {mse}, Normalized MSE: {normalized_mse}")
return frame_is_duplicate, mse, ref_frame
else:
if first_frame:
debug('DEBUG', f"Frame is the first frame, storing as initial reference")
first_frame = False
else:
debug('DEBUG', f"First Frame Duplicate check is disabled, ignoring!")
return False, 0, frame
corner1 = (-1, -1) corner1 = (-1, -1)
corner2 = (-1, -1) corner2 = (-1, -1)
@ -255,13 +83,6 @@ def process_video(video_filenames, main_window):
cropped_image = None cropped_image = None
for videofilename in video_filenames: for videofilename in video_filenames:
debug('DEBUG', f"Processing video file: {videofilename}")
if not processing:
debug('DEBUG', f"Breaking loop due to processing = {processing}")
break
global first_frame
first_frame = True
main_window.title(f"Video Processing App") main_window.title(f"Video Processing App")
@ -296,7 +117,6 @@ def process_video(video_filenames, main_window):
vertical_alignment = vertical_alignment_var.get() vertical_alignment = vertical_alignment_var.get()
horizontal_alignment = horizontal_alignment_var.get() horizontal_alignment = horizontal_alignment_var.get()
debug('DEBUG', f"Displaying video player window: {SHOW}")
if SHOW: if SHOW:
vp_title = "Video2Crops Video Player" vp_title = "Video2Crops Video Player"
# Get the screen resolution # Get the screen resolution
@ -333,16 +153,12 @@ def process_video(video_filenames, main_window):
test = cv2.rectangle(frame.copy(), (x1 - 1, y2 - 1), (x2 + 1, y1 + 1), (255, 0, 0), 4) test = cv2.rectangle(frame.copy(), (x1 - 1, y2 - 1), (x2 + 1, y1 + 1), (255, 0, 0), 4)
cropped_image = frame[y1:y2, x1:x2] cropped_image = frame[y1:y2, x1:x2]
if first_frame:
reference_frame = cropped_image
if SHOW: if SHOW:
cv2.imshow(vp_title, test) cv2.imshow(vp_title, test)
key = cv2.waitKey(10) key = cv2.waitKey(10)
if key == ord('q') or (not processing): if key == ord('q'):
debug('DEBUG', f"Breaking loop due to cancel event")
break break
if cropped_image is not None and frame_cnt % frame_step == 0: if cropped_image is not None and frame_cnt % frame_step == 0:
@ -351,10 +167,8 @@ def process_video(video_filenames, main_window):
cv2.namedWindow(ci_title, cv2.WINDOW_NORMAL) cv2.namedWindow(ci_title, cv2.WINDOW_NORMAL)
cv2.imshow(ci_title, cropped_image) cv2.imshow(ci_title, cropped_image)
frame_is_duplicate, frame_duplicate_mse, reference_frame = check_duplicate(reference_frame, cropped_image)
frame_value = calc_value(cropped_image) frame_value = calc_value(cropped_image)
if frame_value >= value_threshold:
if (frame_value >= contrast_threshold or contrast_threshold == 0) and not frame_is_duplicate:
cropped_image_status = "Accepted" cropped_image_status = "Accepted"
filename = videofilename_base + '_' + str(counter).zfill(4) + ".png" filename = videofilename_base + '_' + str(counter).zfill(4) + ".png"
@ -363,7 +177,7 @@ def process_video(video_filenames, main_window):
if SAVE: if SAVE:
cv2.imwrite(image_filename, cropped_image) cv2.imwrite(image_filename, cropped_image)
cropped_image_info = f"Image on frame {frame_cnt} ({cropped_image_status} | {frame_duplicate_mse}) - {frame_value} / {contrast_threshold} - image nr: {str(str(counter)).zfill(4)}" cropped_image_info = f"Image on frame {frame_cnt} ({cropped_image_status}) - {frame_value} / {value_threshold} - image nr: {str(str(counter)).zfill(4)}"
write_log(cropped_image_info) write_log(cropped_image_info)
counter += 1 counter += 1
else: else:
@ -372,255 +186,132 @@ def process_video(video_filenames, main_window):
if SAVE: if SAVE:
cv2.imwrite(image_filename, cropped_image) cv2.imwrite(image_filename, cropped_image)
cropped_image_info = f"Image on frame {frame_cnt} ({cropped_image_status} | {frame_duplicate_mse}) - {frame_value} / {contrast_threshold} - image nr: {str(str(counter)).zfill(4)}" cropped_image_info = f"Image on frame {frame_cnt} ({cropped_image_status}) - {frame_value} / {value_threshold} - image nr: {str(str(counter)).zfill(4)}"
write_log(cropped_image_info) write_log(cropped_image_info)
discarded_frames += 1 discarded_frames += 1
frame_cnt += 1 frame_cnt += 1
if frame_cnt == 1:
debug('DEBUG', f"Video size: {video_width}x{video_height}")
debug('DEBUG', f"ROI location: {vertical_alignment}-{horizontal_alignment}")
debug('DEBUG', f"ROI size: {roi_size}")
debug('DEBUG', f"ROI position (left, top, right, bottom): {x1}, {y1}, {x2}, {y2}")
cap.release() cap.release()
cv2.destroyAllWindows() cv2.destroyAllWindows()
debug('DEBUG', f"Destroyed all current CV2 windows")
write_log(f"Video2Crops run complete!\nSaved {counter} frames.\nDiscarded {discarded_frames} frames.") write_log(f"Video2Crops run complete!\nSaved {counter} frames.\nDiscarded {discarded_frames} frames.")
processing = False processing = False
# Create a function to add files to the selected files Listbox # Create a function to add files to the selected files Listbox
def add_files(): def add_files():
global processing_button file_paths = filedialog.askopenfilenames(filetypes=[("Video Files", "*.mp4 *.avi *.mov")])
selected_files = selected_files_listbox.get(0, tk.END)
file_paths = filedialog.askopenfilenames(title='Select Video File(s)', filetypes=[("Video Files", FILE_TYPES)])
if file_paths: if file_paths:
for file_path in file_paths: for file_path in file_paths:
if file_path not in selected_files: selected_files_listbox.insert(tk.END, file_path)
selected_files_listbox.insert(tk.END, file_path)
debug('DEBUG', f"Queued video file: {file_path}")
else:
debug('DEBUG', f"Attempted to queue already-queued file \'{file_path}\'")
selected_files = selected_files_listbox.get(0, tk.END)
if len(selected_files) > 0:
processing_button.config(state='normal')
# Create a button to remove the selected file from the Listbox # Create a button to remove the selected file from the Listbox
def remove_file(): def remove_file():
global processing_button
selected_index = selected_files_listbox.curselection() selected_index = selected_files_listbox.curselection()
if selected_index: if selected_index:
selected_files_listbox.delete(selected_index) selected_files_listbox.delete(selected_index)
debug('DEBUG', f"Removed file from queue: {selected_index}")
selected_files = selected_files_listbox.get(0, tk.END)
if len(selected_files) <= 0:
processing_button.config(state='disabled')
# Create a function to clear the queue # Create a function to clear the queue
def clear_queue(): def clear_queue():
global processing_button
selected_files_listbox.delete(0, tk.END) selected_files_listbox.delete(0, tk.END)
processing_button.config(state='disabled')
debug('DEBUG', f"Cleared the queue")
# Create a function to cancel processing # Create a function to cancel processing
def cancel_processing(): def cancel_processing():
global processing global processing
debug('DEBUG', f"Attempting cancelation ...")
if processing: if processing:
processing = False processing = False
if processing_thread.is_alive():
processing_thread.join() # Wait for the processing thread to finish cleanly
messagebox.showinfo("Processing Canceled", "Video processing has been canceled.") messagebox.showinfo("Processing Canceled", "Video processing has been canceled.")
elif not processing:
processing = False
messagebox.showinfo("Nothing to cancel", "Video2Crops isn't processing anything.")
debug('DEBUG', f"Cancel complete. Processing state: {processing}")
# Create a button to process the selected files # Create a button to process the selected files
def process_files(): def process_files():
debug('DEBUG', f"Starting file processing ...") global processing
global processing, processing_thread, selected_files, user_defined_output_dir
if not processing: if not processing:
processing = True
selected_files = selected_files_listbox.get(0, tk.END) selected_files = selected_files_listbox.get(0, tk.END)
processing_thread = threading.Thread(target=process_video, args=(selected_files, root)) for file_path in selected_files:
processing_thread.daemon = True # Set the thread as a daemon so it terminates when the main program exits process_video([file_path], root)
processing_thread.start()
else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
debug('DEBUG', f"File processing done")
# Define dynamic process/cancel button
def process_cancel_toggle():
debug('DEBUG', f"Toggling process/cancel button state")
global processing, processing_thread, selected_files, user_defined_output_dir
if os.path.exists(user_defined_output_dir):
if not processing:
toggle_process_button(False)
processing = True
selected_files = selected_files_listbox.get(0, tk.END)
processing_thread = threading.Thread(target=process_video, args=(selected_files, root))
processing_thread.daemon = True
processing_thread.start()
elif processing:
toggle_process_button(True)
processing = False
if processing_thread.is_alive():
processing_thread.join()
debug('DEBUG', f"Processing/Cancel button state switch finished")
elif not os.path.exists(user_defined_output_dir):
debug('DEBUG', f"The provided output directory \'{user_defined_output_dir}\' does not exist, prompting for new path")
messagebox.showinfo("Invalid output directory", f"The current output directory does not exist:\n{user_defined_output_dir}\nPlease select a valid directory in your Preferences!")
async def toggle_process_button(enable=bool):
"""True = Set state to 'Process Files' | False = Set state to 'Cancel'"""
global processing_button
if enable:
processing_button.config(text="Process Files")
else:
processing_button.config(text="Cancel")
# Create a function to open the preferences window # Create a function to open the preferences window
def open_preferences(): def open_preferences():
debug('DEBUG', f"Opening preferences window")
preferences_window = tk.Toplevel(root) preferences_window = tk.Toplevel(root)
preferences_window.title("Preferences") preferences_window.title("Preferences")
debug('DEBUG', f"Received truncated path: {truncated_udodir} ({type(truncated_udodir)})")
def change_frame_step(): def change_frame_step():
global frame_step
if not processing: if not processing:
new_frame_step = simpledialog.askinteger("Frame Step", "Enter the new frame step:", initialvalue=frame_step, minvalue=1) new_frame_step = simpledialog.askinteger("Frame Step", "Enter the new frame step:")
if new_frame_step is not None: if new_frame_step is not None:
frame_step_var.set(str(new_frame_step)) frame_step_var.set(str(new_frame_step))
global frame_step
frame_step = new_frame_step frame_step = new_frame_step
write2ini('settings', 'frame_step', new_frame_step)
debug('DEBUG', f"Set frame stepping to {frame_step}")
else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
def change_contrast_threshold(): def change_value_threshold():
global contrast_threshold
if not processing: if not processing:
new_contrast_threshold = simpledialog.askfloat("Contrast Threshold", "Enter the new value threshold:", initialvalue=contrast_threshold, minvalue=0) new_value_threshold = simpledialog.askfloat("Value Threshold", "Enter the new value threshold:")
if new_contrast_threshold is not None: if new_value_threshold is not None:
contrast_threshold_var.set(str(new_contrast_threshold)) value_threshold_var.set(str(new_value_threshold))
write2ini('settings', 'contrast_threshold', new_contrast_threshold)
debug('DEBUG', f"Set contrast discard threshold to {new_contrast_threshold}")
else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
def change_duplicate_threshold(): def toggle_save():
global duplicate_threshold
if not processing: if not processing:
new_duplicate_threshold = simpledialog.askfloat("Duplicate Threshold", "Enter the new value threshold:", initialvalue=duplicate_threshold, minvalue=0, maxvalue=100) global SAVE
if new_duplicate_threshold is not None: SAVE = not SAVE
duplicate_threshold_var.set(str(new_duplicate_threshold))
write2ini('settings', 'duplicate_threshold', new_duplicate_threshold) def toggle_show():
debug('DEBUG', f"Set duplicate discard threshold to {new_duplicate_threshold}") if not processing:
else: global SHOW
messagebox.showinfo("Info", "Video2Crops is currently processing files!") SHOW = not SHOW
def apply_roi_size(): def apply_roi_size():
global roi_size
if not processing: if not processing:
new_roi_size = simpledialog.askinteger("ROI Size", "Enter the new ROI size:", initialvalue=roi_size, minvalue=1) new_roi_size = simpledialog.askinteger("ROI Size", "Enter the new ROI size:")
if new_roi_size is not None: if new_roi_size is not None:
roi_size_var.set(str(new_roi_size)) roi_size_var.set(str(new_roi_size))
write2ini('settings', 'roi_size', new_roi_size)
debug('DEBUG', f"Set new ROI size to {new_roi_size}")
else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
def change_output_dir():
global user_defined_output_dir
if not processing:
new_user_defined_output_dir = filedialog.askdirectory(title='Select Output Directory', mustexist=True, initialdir=user_defined_output_dir)
if new_user_defined_output_dir:
user_defined_output_dir = new_user_defined_output_dir
write2ini('settings', 'user_defined_output_dir', new_user_defined_output_dir)
debug('DEBUG', f"Set output directory to {new_user_defined_output_dir}")
# Update the text of the Label widget and truncated path
truncated_udodir = truncate_path(new_user_defined_output_dir, pre_max=10, post_max=20)
else:
debug('DEBUG', f"User canceled selection of output directory, reverting to \'{user_defined_output_dir}\'")
truncated_udodir = truncate_path(user_defined_output_dir, pre_max=10, post_max=20)
udodir_var.set(truncated_udodir)
else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
debug('DEBUG', f"Creating preference fields ...")
# Create preference fields # Create preference fields
frame_step_label = tk.Label(preferences_window, text="Skip n frames:") frame_step_label = tk.Label(preferences_window, text="Skip n frames:")
frame_step_label.grid(row=0, column=0, padx=20, pady=5) frame_step_label.grid(row=0, column=0, padx=20, pady=10)
frame_step_value = tk.Label(preferences_window, textvariable=frame_step_var) frame_step_value = tk.Label(preferences_window, textvariable=frame_step_var)
frame_step_value.grid(row=0, column=1, padx=20, pady=5) frame_step_value.grid(row=0, column=1, padx=20, pady=10)
frame_step_button = tk.Button(preferences_window, text="Edit", command=change_frame_step) frame_step_button = tk.Button(preferences_window, text="Edit", command=change_frame_step)
frame_step_button.grid(row=0, column=2, padx=20, pady=5) frame_step_button.grid(row=0, column=2, padx=20, pady=10)
contrast_threshold_label = tk.Label(preferences_window, text="Contrast Threshold:") value_threshold_label = tk.Label(preferences_window, text="Discard Threshold:")
contrast_threshold_label.grid(row=1, column=0, padx=20, pady=5) value_threshold_label.grid(row=1, column=0, padx=20, pady=10)
if contrast_threshold == float(0): value_threshold_value = tk.Label(preferences_window, textvariable=value_threshold_var)
contrast_threshold_value = tk.Label(preferences_window, text="Disabled") value_threshold_value.grid(row=1, column=1, padx=20, pady=10)
else: value_threshold_button = tk.Button(preferences_window, text="Edit", command=change_value_threshold)
contrast_threshold_value = tk.Label(preferences_window, textvariable=contrast_threshold_var) value_threshold_button.grid(row=1, column=2, padx=20, pady=10)
contrast_threshold_value.grid(row=1, column=1, padx=20, pady=5)
contrast_threshold_button = tk.Button(preferences_window, text="Edit", command=change_contrast_threshold)
contrast_threshold_button.grid(row=1, column=2, padx=20, pady=5)
duplicate_threshold_label = tk.Label(preferences_window, text="Duplicate Threshold:")
duplicate_threshold_label.grid(row=2, column=0, padx=20, pady=5)
if duplicate_threshold == float(0):
duplicate_threshold_value = tk.Label(preferences_window, text="Disabled")
else:
duplicate_threshold_value = tk.Label(preferences_window, textvariable=duplicate_threshold_var)
duplicate_threshold_value.grid(row=2, column=1, padx=20, pady=5)
duplicate_threshold_button = tk.Button(preferences_window, text="Edit", command=change_duplicate_threshold)
duplicate_threshold_button.grid(row=2, column=2, padx=20, pady=5)
save_checkbox = tk.Checkbutton(preferences_window, text="Save Output Images", variable=save_var, command=toggle_save) save_checkbox = tk.Checkbutton(preferences_window, text="Save Output Images", variable=save_var, command=toggle_save)
save_checkbox.grid(row=3, column=0, columnspan=3, padx=20, pady=5) save_checkbox.grid(row=2, column=0, columnspan=3, padx=20, pady=10)
show_checkbox = tk.Checkbutton(preferences_window, text="Display Playback", variable=show_var, command=toggle_show) show_checkbox = tk.Checkbutton(preferences_window, text="Display Playback", variable=show_var, command=toggle_show)
show_checkbox.grid(row=4, column=0, columnspan=3, padx=20, pady=5) show_checkbox.grid(row=3, column=0, columnspan=3, padx=20, pady=10)
# Section divider # Section divider
divider_label = tk.Label(preferences_window, text="ROI Settings", font=("Helvetica", 12, "bold")) divider_label = tk.Label(preferences_window, text="ROI Settings", font=("Helvetica", 12, "bold"))
divider_label.grid(row=5, column=0, columnspan=3, padx=20, pady=(20, 10)) divider_label.grid(row=4, column=0, columnspan=3, padx=20, pady=(20, 10))
# Create ROI fields # Create ROI fields
vertical_alignment_label = tk.Label(preferences_window, text="Vertical Alignment:") vertical_alignment_label = tk.Label(preferences_window, text="Vertical Alignment:")
vertical_alignment_label.grid(row=6, column=0, padx=20, pady=5) vertical_alignment_label.grid(row=5, column=0, padx=20, pady=10)
vertical_alignment_var.set("Center") # Default alignment vertical_alignment_var.set("Center") # Default alignment
vertical_alignment_menu = tk.OptionMenu(preferences_window, vertical_alignment_var, "Top", "Center", "Bottom") vertical_alignment_menu = tk.OptionMenu(preferences_window, vertical_alignment_var, "Top", "Center", "Bottom")
vertical_alignment_menu.grid(row=6, column=1, padx=20, pady=5) vertical_alignment_menu.grid(row=5, column=1, padx=20, pady=10)
horizontal_alignment_label = tk.Label(preferences_window, text="Horizontal Alignment:") horizontal_alignment_label = tk.Label(preferences_window, text="Horizontal Alignment:")
horizontal_alignment_label.grid(row=7, column=0, padx=20, pady=5) horizontal_alignment_label.grid(row=6, column=0, padx=20, pady=10)
horizontal_alignment_var.set("Center") # Default alignment horizontal_alignment_var.set("Center") # Default alignment
horizontal_alignment_menu = tk.OptionMenu(preferences_window, horizontal_alignment_var, "Left", "Center", "Right") horizontal_alignment_menu = tk.OptionMenu(preferences_window, horizontal_alignment_var, "Left", "Center", "Right")
horizontal_alignment_menu.grid(row=7, column=1, padx=20, pady=5) horizontal_alignment_menu.grid(row=6, column=1, padx=20, pady=10)
roi_size_label = tk.Label(preferences_window, text="ROI Size:") roi_size_label = tk.Label(preferences_window, text="ROI Size:")
roi_size_label.grid(row=8, column=0, padx=20, pady=5) roi_size_label.grid(row=7, column=0, padx=20, pady=10)
roi_size_value = tk.Label(preferences_window, textvariable=roi_size_var) roi_size_value = tk.Label(preferences_window, textvariable=roi_size_var)
roi_size_value.grid(row=8, column=1, padx=20, pady=5) roi_size_value.grid(row=7, column=1, padx=20, pady=10)
roi_size_button = tk.Button(preferences_window, text="Edit", command=apply_roi_size) roi_size_button = tk.Button(preferences_window, text="Edit", command=apply_roi_size)
roi_size_button.grid(row=8, column=2, padx=20, pady=5) roi_size_button.grid(row=7, column=2, padx=20, pady=10)
udodir_label = tk.Label(preferences_window, text="Output Directory: ")
udodir_label.grid(row=9, column=0, columnspan=3, padx=20, pady=5)
udodir_value = tk.Label(preferences_window, textvariable=udodir_var)
udodir_value.grid(row=10, column=0, columnspan=2, padx=20, pady=5)
udodir_button = tk.Button(preferences_window, text="Edit", command=change_output_dir)
udodir_button.grid(row=10, column=2, padx=20, pady=5)
debug('DEBUG', f"Done creating preference fields")
debug('DEBUG', 'Functions defined')
# Create a menu # Create a menu
menu = tk.Menu(root) menu = tk.Menu(root)
@ -628,26 +319,21 @@ root.config(menu=menu)
# Variables for preferences # Variables for preferences
frame_step_var = tk.StringVar() frame_step_var = tk.StringVar()
contrast_threshold_var = tk.StringVar() value_threshold_var = tk.StringVar()
duplicate_threshold_var = tk.StringVar() save_var = tk.BooleanVar()
save_var = tk.BooleanVar() # Save Output show_var = tk.BooleanVar()
show_var = tk.BooleanVar() # Show Output
udodir_var = tk.StringVar() # User Defined Output DIRectory
# Variables for ROI # Variables for ROI
vertical_alignment_var = tk.StringVar() vertical_alignment_var = tk.StringVar()
horizontal_alignment_var = tk.StringVar() horizontal_alignment_var = tk.StringVar()
roi_size_var = tk.StringVar() roi_size_var = tk.StringVar()
roi_size_var.set("400")
# Set initial values for preference variables # Set initial values for preference variables
truncated_udodir = truncate_path(user_defined_output_dir, pre_max=10, post_max=20)
frame_step_var.set(str(frame_step)) frame_step_var.set(str(frame_step))
contrast_threshold_var.set(str(contrast_threshold)) value_threshold_var.set(str(value_threshold))
duplicate_threshold_var.set(str(duplicate_threshold))
save_var.set(SAVE) save_var.set(SAVE)
show_var.set(SHOW) show_var.set(SHOW)
roi_size_var.set(roi_size)
udodir_var.set(truncated_udodir)
# Create a "Queue" submenu # Create a "Queue" submenu
queue_menu = tk.Menu(menu, tearoff=0) queue_menu = tk.Menu(menu, tearoff=0)
@ -655,47 +341,38 @@ menu.add_cascade(label="Queue", menu=queue_menu)
queue_menu.add_command(label="Add file", command=add_files) queue_menu.add_command(label="Add file", command=add_files)
queue_menu.add_command(label="Clear queue", command=clear_queue) queue_menu.add_command(label="Clear queue", command=clear_queue)
queue_menu.add_separator() queue_menu.add_separator()
queue_menu.add_command(label="Cancel", state="disabled", command=cancel_processing) queue_menu.add_command(label="Cancel", command=cancel_processing)
queue_menu.add_command(label="Cancel & Clear", state="disabled", command=lambda: [cancel_processing(), clear_queue()]) queue_menu.add_command(label="Cancel & Clear", command=lambda: [cancel_processing(), clear_queue()])
# Create a "Preferences" submenu # Create a "Preferences" submenu
menu.add_command(label="Preferences", command=open_preferences) menu.add_command(label="Preferences", command=open_preferences)
# Create an "Info" submenu # Create an "Info" submenu
info_menu = tk.Menu(menu, tearoff=0, disabledforeground="#000") info_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Info", menu=info_menu) menu.add_cascade(label="Info", menu=info_menu)
info_menu.add_command(label="Project Folder", command=lambda: webbrowser.open(os.getcwd())) info_menu.add_command(label="Project Folder", command=lambda: webbrowser.open(os.getcwd()))
info_menu.add_separator() info_menu.add_separator()
info_menu.add_command(label="About", command=lambda: webbrowser.open(WIKI_URL)) info_menu.add_command(label="About", command=lambda: webbrowser.open("about.md"))
info_menu.add_command(label=f"Check for updates", command=check_for_updates) info_menu.add_command(label=f"Version: {str(v2cversion)}")
info_menu.add_command(label=f"View Changelog", command=open_changelog_window)
info_menu.add_separator()
info_menu.add_command(state="disabled", label=f"Version: {str(V2CVERSION)}")
# Create a label for selected files # Create a label for selected files
label = tk.Label(root, text="Selected Files:") label = tk.Label(root, text="Selected Files:")
label.pack(padx=20, pady=5) label.pack(padx=20, pady=10)
# Create a Listbox to display the selected files # Create a Listbox to display the selected files
selected_files_listbox = tk.Listbox(root, selectmode=tk.SINGLE, exportselection=0) selected_files_listbox = tk.Listbox(root, selectmode=tk.SINGLE, exportselection=0)
selected_files_listbox.pack(fill=tk.BOTH, expand=True, padx=20, pady=5) selected_files_listbox.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
selected_files = selected_files_listbox.get(0, tk.END)
# Create a button to select and add files to the Listbox # Create a button to select and add files to the Listbox
select_files_button = tk.Button(root, text="Select Files", command=add_files) select_files_button = tk.Button(root, text="Select Files", command=add_files)
select_files_button.pack(padx=20, pady=5) select_files_button.pack(padx=20, pady=10)
# Create a button to remove the selected file from the Listbox # Create a button to remove the selected file from the Listbox
remove_file_button = tk.Button(root, text="Remove File", command=remove_file) remove_file_button = tk.Button(root, text="Remove File", command=remove_file)
remove_file_button.pack(padx=20, pady=5) remove_file_button.pack(padx=20, pady=10)
# Create a button to process the selected files # Create a button to process the selected files
processing_button = tk.Button(root, text="Process Files", command=process_cancel_toggle, state='disabled') process_files_button = tk.Button(root, text="Process Files", command=process_files)
processing_button.pack(padx=20, pady=5) process_files_button.pack(padx=20, pady=10)
debug('DEBUG', 'UI generated, running main loop')
debug('INFO', '... Video2Crops initialized and ready!')
check_for_updates()
STARTING = False
root.mainloop() root.mainloop()

View File

@ -1,23 +0,0 @@
; Video2Crops Settings
[settings]
; (INT) Skip every n frame
frame_step = 100
; (FLOAT) Contrast/blur discard threshold
contrast_threshold = 11.0
; (INT) Image similarity discard threshold
duplicate_threshold = 1000
; (BOOL) Save processing output
save_output = True
; (BOOL) Display processing output and playback
show_playback = True
; (INT) Region of Interest area square size (pixels)
roi_size = 400
; (STR) Initial application window dimensions
window_size = 300x450
; (STR) User-defined output base directory
user_defined_output_dir =
[debug]
; (BOOL) Set to 'True' to output debug messages to console
debug_output = False

View File

View File

@ -1,33 +0,0 @@
import os, configparser, multiprocessing
CONFIG_FILE = "config.ini"
INVALID_INI = False
STARTING = True
V2CV = [1, 2, 0]
V2CVERSION = f"{V2CV[0]}.{V2CV[1]}.{V2CV[2]}"
RSS_FEED_URL = "https://git.rolfsvaag.no/frarol96/Video2Crops/releases.rss"
WIKI_URL = "https://git.rolfsvaag.no/frarol96/Video2Crops/wiki"
FILE_TYPES = '*.mp4 *.avi *.mov'
FILE_TYPES = f"{FILE_TYPES} {FILE_TYPES.upper()}"
processing = False
# Setup config file
config = configparser.ConfigParser()
config.read(CONFIG_FILE)
processing_button_state = "start"
# Load ini settings
frame_step = int(config.get('settings', 'frame_step'))
contrast_threshold = float(config.get('settings', 'contrast_threshold'))
duplicate_threshold = float(config.get('settings', 'duplicate_threshold'))
SAVE = config.get('settings', 'save_output')
SHOW = config.get('settings', 'show_playback')
roi_size = int(config.get('settings', 'roi_size'))
tksize = config.get('settings', 'window_size')
user_defined_output_dir = config.get('settings', 'user_defined_output_dir')
DEBUG = config.get('debug', 'debug_output')
# Assign conditional variables here
# If output directory is not assigned or does not exist, assign it as cwd
if not user_defined_output_dir or not os.path.exists(user_defined_output_dir): user_defined_output_dir = os.getcwd()

View File

@ -1,185 +0,0 @@
import re, os, urllib.request
import tkinter as tk
from datetime import datetime
from bs4 import BeautifulSoup
from tkinter import filedialog, simpledialog, messagebox, scrolledtext
from v2cmodules.common import *
def extract_version(title):
# Use regular expressions to extract the version number from the title
match = re.search(r'(\d+\.\d+\.\d+)', title)
if match:
return match.group(1)
return None
def fetch_ini():
global INVALID_INI
try:
with open(CONFIG_FILE, 'w') as file:
config_url = 'https://git.rolfsvaag.no/frarol96/Video2Crops/raw/branch/main/config.ini'
with urllib.request.urlopen(config_url) as config_template:
config_template = config_template.read().decode("utf-8")
file.write(config_template)
tk.messagebox.showinfo("Reset config file", f"Config file has been reset")
debug('DEBUG', f"Config file was reset to default")
INVALID_INI = False
except:
print(f"[ERROR] Unable to fetch the missing config.ini file!\nCheck directory permissions and network connectivity!")
exit
# Debug function
def debug(state, message):
if DEBUG:
allowed_states = ['LOG EVENT', 'DEBUG', 'ERROR', 'INFO']
if state in allowed_states:
debug_message = f"[{state}] {message}"
print(f"{debug_message}")
debugfile_path = os.path.join(os.getcwd(), 'debugfile.txt')
with open(debugfile_path, 'a') as debugfile:
log_debug_message = f"{datetime.now().strftime('%m-%d_%H:%M:%S')} | [{state}] {message}\n"
debugfile.write(log_debug_message)
else:
raise ValueError(f"Invalid state '{state}'. Please choose from {', '.join(allowed_states)}.")
def checkinifile():
global INVALID_INI
# Check for the presence of sections and keys
expected_sections = ['settings', 'debug']
expected_settings_keys = {
'frame_step': int,
'contrast_threshold': float,
'duplicate_threshold': int,
'save_output': bool,
'show_playback': bool,
'roi_size': int,
'window_size': str,
'user_defined_output_dir': str
}
expected_debug_keys = {
'debug_output': bool
}
CHECK_SECTIONS = True
CHECK_KEYS = True
CHECK_VALUES = False
# Check for the presence of sections
if CHECK_SECTIONS:
for section in expected_sections:
if section not in config.sections():
INVALID_INI = True
print(f"Missing section: [{section}]")
# Check for the presence of keys and their types in the 'settings' section
if 'settings' in config.sections():
for key, expected_type in expected_settings_keys.items():
if key not in config['settings']:
if CHECK_KEYS:
INVALID_INI = True
print(f"Missing key '{key}' in [settings] section")
else:
value = config.get('settings', key)
if not isinstance(value, expected_type):
if CHECK_VALUES:
INVALID_INI = True
print(f"Value of '{key}' in [settings] section is not of expected type {expected_type}")
# Check for the presence of keys and their types in the 'debug' section
if 'debug' in config.sections():
for key, expected_type in expected_debug_keys.items():
if key not in config['debug']:
if CHECK_KEYS:
INVALID_INI = True
print(f"Missing key '{key}' in [debug] section")
else:
value = config.get('debug', key)
if not isinstance(value, expected_type):
if CHECK_VALUES:
INVALID_INI = True
print(f"Value of '{key}' in [debug] section is not of expected type {expected_type}")
if INVALID_INI:
fetch_ini()
def write2ini(section, variable, value):
config.set(section, str(variable), str(value))
# Save the changes to the INI file
with open(CONFIG_FILE, 'w') as configfile:
config.write(configfile)
def extract_changelog(description):
# Parse the HTML content using BeautifulSoup
soup = BeautifulSoup(description, 'html.parser')
# Find all changelog sections
changelog_sections = soup.find_all('h3', id='user-content-changelog')
changelogs = []
for changelog_section in changelog_sections:
# Extract the changelog text as plain text
changelog_text = changelog_section.find_next('ul').get_text()
changelogs.append(changelog_text.strip())
# Join the changelogs into a single string
changelog_str = '\n'.join(changelogs)
return changelog_str if changelog_str else None
def toggle_save():
if not processing:
global SAVE
SAVE = not SAVE
write2ini('settings', 'save_output', SAVE)
debug('DEBUG', f"Saving output: {SAVE}")
else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
def toggle_show():
if not processing:
global SHOW
SHOW = not SHOW
write2ini('settings', 'show_playback', SHOW)
debug('DEBUG', f"Displaying output: {SHOW}")
else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
# Function to create the output directory and return its path
def create_output_directory(video_filename):
debug('DEBUG', f"Creating output directory")
base_directory = user_defined_output_dir
video_filename_base, _ = os.path.splitext(os.path.basename(video_filename))
output_folder_path = os.path.join(base_directory, video_filename_base)
# Create the output directory if it doesn't exist
if not os.path.exists(output_folder_path):
os.makedirs(output_folder_path)
debug('DEBUG', f"Processing file: {video_filename}")
debug('DEBUG', f"Created output directory at: {output_folder_path}")
return output_folder_path
def truncate_path(path, pre_max=None, post_max=None):
# Split the path into components
path_components = os.path.normpath(path).split(os.path.sep)
# Ensure the first two components are displayed in full
pre_part = os.path.sep.join(path_components[:2])
# Ensure the last two components are displayed in full
post_part = os.path.sep.join(path_components[-2:])
# Truncate the pre-portion if pre_max is specified
if pre_max is not None:
pre_part = pre_part[:pre_max]
# Truncate the post-portion if post_max is specified
if post_max is not None:
post_part = post_part[-post_max:]
# Combine the parts with ellipsis in between
truncated_path = f"{pre_part}...{post_part}"
debug('DEBUG', f"Truncated the path \'{path}\' to \'{truncated_path}\'")
return truncated_path