Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
fd392088aa | |||
4311036977 | |||
c5faaf242b | |||
9b58cacaf3 | |||
e6aff29a81 | |||
a7a80677f6 | |||
863299fe6f | |||
91c4938968 | |||
4411f2684b | |||
5ac83f6983 | |||
16b60efe0b | |||
ac4894f318 | |||
d6e67151cd | |||
2ae89d1fa8 | |||
c1e34e1973 | |||
d9ad30e3e6 | |||
ca6234badd | |||
5adaa8d4e1 | |||
172d3afd8d |
@ -4,3 +4,4 @@ Video2Crops is a cross-platform Python-based program that accepts video files, a
|
|||||||
|
|
||||||
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.
|
505
Video2Crops.py
505
Video2Crops.py
@ -1,41 +1,180 @@
|
|||||||
import cv2
|
import cv2, os, sys, re, threading, webbrowser, screeninfo
|
||||||
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
|
from tkinter import filedialog, simpledialog, messagebox, scrolledtext
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import sys
|
import feedparser
|
||||||
import webbrowser
|
import subprocess
|
||||||
import screeninfo
|
import platform
|
||||||
|
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 *
|
||||||
|
|
||||||
# Constants
|
checkinifile()
|
||||||
v2cversion1 = 1
|
|
||||||
v2cversion2 = 0
|
debug('INFO', "Video2Crops starting ...")
|
||||||
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)
|
||||||
|
|
||||||
# Function to create the output directory and return its path
|
def open_changelog_window():
|
||||||
def create_output_directory(video_filename):
|
debug('DEBUG', 'Loading changelog window')
|
||||||
base_directory = os.getcwd() # You can change this to specify a different base directory
|
changelog_window = tk.Toplevel(root)
|
||||||
video_filename_base, _ = os.path.splitext(os.path.basename(video_filename))
|
changelog_window.title("Video2Crops Changelog")
|
||||||
output_folder_path = os.path.join(base_directory, video_filename_base)
|
|
||||||
|
|
||||||
# Create the output directory if it doesn't exist
|
try:
|
||||||
if not os.path.exists(output_folder_path):
|
debug('DEBUG', f"Reading the RSS feed from: {RSS_FEED_URL}")
|
||||||
os.makedirs(output_folder_path)
|
# Parse the RSS feed
|
||||||
|
feed = feedparser.parse(RSS_FEED_URL)
|
||||||
|
|
||||||
return output_folder_path
|
if not feed.entries:
|
||||||
|
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):
|
||||||
@ -63,26 +202,66 @@ 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
|
||||||
processing = True
|
debug('DEBUG', f"Processing state: {processing}")
|
||||||
|
|
||||||
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)
|
||||||
print(log_message)
|
debug('LOG EVENT', f"{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)
|
||||||
drawing = False
|
drawing = False
|
||||||
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")
|
||||||
|
|
||||||
@ -117,6 +296,7 @@ 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
|
||||||
@ -153,12 +333,16 @@ 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'):
|
if key == ord('q') or (not processing):
|
||||||
|
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:
|
||||||
@ -167,8 +351,10 @@ 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"
|
||||||
@ -177,7 +363,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_value} / {value_threshold} - image nr: {str(str(counter)).zfill(4)}"
|
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)}"
|
||||||
write_log(cropped_image_info)
|
write_log(cropped_image_info)
|
||||||
counter += 1
|
counter += 1
|
||||||
else:
|
else:
|
||||||
@ -186,132 +372,255 @@ 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_value} / {value_threshold} - image nr: {str(str(counter)).zfill(4)}"
|
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)}"
|
||||||
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():
|
||||||
file_paths = filedialog.askopenfilenames(filetypes=[("Video Files", "*.mp4 *.avi *.mov")])
|
global processing_button
|
||||||
|
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:
|
||||||
selected_files_listbox.insert(tk.END, file_path)
|
if file_path not in selected_files:
|
||||||
|
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():
|
||||||
global processing
|
debug('DEBUG', f"Starting file 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)
|
||||||
for file_path in selected_files:
|
processing_thread = threading.Thread(target=process_video, args=(selected_files, root))
|
||||||
process_video([file_path], root)
|
processing_thread.daemon = True # Set the thread as a daemon so it terminates when the main program exits
|
||||||
|
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:")
|
new_frame_step = simpledialog.askinteger("Frame Step", "Enter the new frame step:", initialvalue=frame_step, minvalue=1)
|
||||||
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_value_threshold():
|
def change_contrast_threshold():
|
||||||
|
global contrast_threshold
|
||||||
if not processing:
|
if not processing:
|
||||||
new_value_threshold = simpledialog.askfloat("Value Threshold", "Enter the new value threshold:")
|
new_contrast_threshold = simpledialog.askfloat("Contrast Threshold", "Enter the new value threshold:", initialvalue=contrast_threshold, minvalue=0)
|
||||||
if new_value_threshold is not None:
|
if new_contrast_threshold is not None:
|
||||||
value_threshold_var.set(str(new_value_threshold))
|
contrast_threshold_var.set(str(new_contrast_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 toggle_save():
|
def change_duplicate_threshold():
|
||||||
|
global duplicate_threshold
|
||||||
if not processing:
|
if not processing:
|
||||||
global SAVE
|
new_duplicate_threshold = simpledialog.askfloat("Duplicate Threshold", "Enter the new value threshold:", initialvalue=duplicate_threshold, minvalue=0, maxvalue=100)
|
||||||
SAVE = not SAVE
|
if new_duplicate_threshold is not None:
|
||||||
|
duplicate_threshold_var.set(str(new_duplicate_threshold))
|
||||||
def toggle_show():
|
write2ini('settings', 'duplicate_threshold', new_duplicate_threshold)
|
||||||
if not processing:
|
debug('DEBUG', f"Set duplicate discard threshold to {new_duplicate_threshold}")
|
||||||
global SHOW
|
else:
|
||||||
SHOW = not SHOW
|
messagebox.showinfo("Info", "Video2Crops is currently processing files!")
|
||||||
|
|
||||||
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:")
|
new_roi_size = simpledialog.askinteger("ROI Size", "Enter the new ROI size:", initialvalue=roi_size, minvalue=1)
|
||||||
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=10)
|
frame_step_label.grid(row=0, column=0, padx=20, pady=5)
|
||||||
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=10)
|
frame_step_value.grid(row=0, column=1, padx=20, pady=5)
|
||||||
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=10)
|
frame_step_button.grid(row=0, column=2, padx=20, pady=5)
|
||||||
|
|
||||||
value_threshold_label = tk.Label(preferences_window, text="Discard Threshold:")
|
contrast_threshold_label = tk.Label(preferences_window, text="Contrast Threshold:")
|
||||||
value_threshold_label.grid(row=1, column=0, padx=20, pady=10)
|
contrast_threshold_label.grid(row=1, column=0, padx=20, pady=5)
|
||||||
value_threshold_value = tk.Label(preferences_window, textvariable=value_threshold_var)
|
if contrast_threshold == float(0):
|
||||||
value_threshold_value.grid(row=1, column=1, padx=20, pady=10)
|
contrast_threshold_value = tk.Label(preferences_window, text="Disabled")
|
||||||
value_threshold_button = tk.Button(preferences_window, text="Edit", command=change_value_threshold)
|
else:
|
||||||
value_threshold_button.grid(row=1, column=2, padx=20, pady=10)
|
contrast_threshold_value = tk.Label(preferences_window, textvariable=contrast_threshold_var)
|
||||||
|
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=2, column=0, columnspan=3, padx=20, pady=10)
|
save_checkbox.grid(row=3, column=0, columnspan=3, padx=20, pady=5)
|
||||||
|
|
||||||
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=3, column=0, columnspan=3, padx=20, pady=10)
|
show_checkbox.grid(row=4, column=0, columnspan=3, padx=20, pady=5)
|
||||||
|
|
||||||
# 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=4, column=0, columnspan=3, padx=20, pady=(20, 10))
|
divider_label.grid(row=5, 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=5, column=0, padx=20, pady=10)
|
vertical_alignment_label.grid(row=6, column=0, padx=20, pady=5)
|
||||||
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=5, column=1, padx=20, pady=10)
|
vertical_alignment_menu.grid(row=6, column=1, padx=20, pady=5)
|
||||||
|
|
||||||
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=6, column=0, padx=20, pady=10)
|
horizontal_alignment_label.grid(row=7, column=0, padx=20, pady=5)
|
||||||
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=6, column=1, padx=20, pady=10)
|
horizontal_alignment_menu.grid(row=7, column=1, padx=20, pady=5)
|
||||||
|
|
||||||
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=7, column=0, padx=20, pady=10)
|
roi_size_label.grid(row=8, column=0, padx=20, pady=5)
|
||||||
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=7, column=1, padx=20, pady=10)
|
roi_size_value.grid(row=8, column=1, padx=20, pady=5)
|
||||||
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=7, column=2, padx=20, pady=10)
|
roi_size_button.grid(row=8, column=2, padx=20, pady=5)
|
||||||
|
|
||||||
|
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)
|
||||||
@ -319,21 +628,26 @@ root.config(menu=menu)
|
|||||||
|
|
||||||
# Variables for preferences
|
# Variables for preferences
|
||||||
frame_step_var = tk.StringVar()
|
frame_step_var = tk.StringVar()
|
||||||
value_threshold_var = tk.StringVar()
|
contrast_threshold_var = tk.StringVar()
|
||||||
save_var = tk.BooleanVar()
|
duplicate_threshold_var = tk.StringVar()
|
||||||
show_var = tk.BooleanVar()
|
save_var = tk.BooleanVar() # Save Output
|
||||||
|
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))
|
||||||
value_threshold_var.set(str(value_threshold))
|
contrast_threshold_var.set(str(contrast_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)
|
||||||
@ -341,38 +655,47 @@ 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", command=cancel_processing)
|
queue_menu.add_command(label="Cancel", state="disabled", command=cancel_processing)
|
||||||
queue_menu.add_command(label="Cancel & Clear", command=lambda: [cancel_processing(), clear_queue()])
|
queue_menu.add_command(label="Cancel & Clear", state="disabled", 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)
|
info_menu = tk.Menu(menu, tearoff=0, disabledforeground="#000")
|
||||||
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("about.md"))
|
info_menu.add_command(label="About", command=lambda: webbrowser.open(WIKI_URL))
|
||||||
info_menu.add_command(label=f"Version: {str(v2cversion)}")
|
info_menu.add_command(label=f"Check for updates", command=check_for_updates)
|
||||||
|
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=10)
|
label.pack(padx=20, pady=5)
|
||||||
|
|
||||||
# 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=10)
|
selected_files_listbox.pack(fill=tk.BOTH, expand=True, padx=20, pady=5)
|
||||||
|
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=10)
|
select_files_button.pack(padx=20, pady=5)
|
||||||
|
|
||||||
# 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=10)
|
remove_file_button.pack(padx=20, pady=5)
|
||||||
|
|
||||||
# Create a button to process the selected files
|
# Create a button to process the selected files
|
||||||
process_files_button = tk.Button(root, text="Process Files", command=process_files)
|
processing_button = tk.Button(root, text="Process Files", command=process_cancel_toggle, state='disabled')
|
||||||
process_files_button.pack(padx=20, pady=10)
|
processing_button.pack(padx=20, pady=5)
|
||||||
|
|
||||||
|
debug('DEBUG', 'UI generated, running main loop')
|
||||||
|
debug('INFO', '... Video2Crops initialized and ready!')
|
||||||
|
|
||||||
|
check_for_updates()
|
||||||
|
STARTING = False
|
||||||
root.mainloop()
|
root.mainloop()
|
23
config.ini
Normal file
23
config.ini
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
; 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
|
0
v2cmodules/__init__.py
Normal file
0
v2cmodules/__init__.py
Normal file
33
v2cmodules/common.py
Normal file
33
v2cmodules/common.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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()
|
185
v2cmodules/toolbox.py
Normal file
185
v2cmodules/toolbox.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
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
|
Loading…
Reference in New Issue
Block a user