Update Video2Crops.py

Implemented value checks, code optimization and preparing to migrate functions to modules
This commit is contained in:
frarol96 2023-10-09 23:14:31 +00:00
parent 16b60efe0b
commit 5ac83f6983

View File

@ -1,178 +1,34 @@
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
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, scrolledtext
from datetime import datetime from datetime import datetime
import sys
import webbrowser
import screeninfo
import feedparser import feedparser
import subprocess import subprocess
import platform import platform
import urllib.request import urllib.request
import re from v2cmodules.toolbox import extract_version, debug, checkinifile, write2ini, extract_changelog, toggle_save, toggle_show
from bs4 import BeautifulSoup from v2cmodules.common import *
import threading
import configparser
DEBUG = True
STARTING = True
INVALID_INI = False
# 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 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
# Setup config file
config_file = 'config.ini'
config = configparser.ConfigParser()
if not os.path.exists(config_file):
fetch_ini()
config.read(config_file)
def checkinifile():
# 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
}
expected_debug_keys = {
'debug_output': bool
}
CHECK_SECTIONS = True
CHECK_KEYS = True
CHECK_VALUES = False
global INVALID_INI
# 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()
checkinifile() checkinifile()
debug('INFO', "Video2Crops starting ...") debug('INFO', "Video2Crops starting ...")
# START INITIATING PROGRAM VARIABLES FROM HERE
# Constants
v2cv = [1, 2, 0]
v2cversion1 = v2cv[0]
v2cversion2 = v2cv[1]
v2cversion3 = v2cv[2]
v2cversion = f"{v2cversion1}.{v2cversion2}.{v2cversion3}"
processing = False
processing_button_state = "start"
# Load ini settings
frame_step = config.get('settings', 'frame_step')
contrast_threshold = config.get('settings', 'contrast_threshold')
duplicate_threshold = config.get('settings', 'duplicate_threshold')
SAVE = config.get('settings', 'save_output')
SHOW = config.get('settings', 'show_playback')
roi_size = config.get('settings', 'roi_size')
tksize = config.get('settings', 'window_size')
DEBUG = config.get('debug', 'debug_output')
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)
# Define the RSS feed URL
rss_feed_url = "https://git.rolfsvaag.no/frarol96/Video2Crops/releases.rss"
# Create the root window # Create the root window
root = TkinterDnD.Tk() root = TkinterDnD.Tk()
root.title("Video2Crops") root.title("Video2Crops")
root.geometry(tksize) root.geometry(tksize)
# Function to clear the queue
def clear_queue():
selected_files_listbox.delete(0, tk.END)
debug('DEBUG', 'Cleared work queue')
def open_changelog_window(): def open_changelog_window():
debug('DEBUG', 'Loading changelog window') debug('DEBUG', 'Loading changelog window')
changelog_window = tk.Toplevel(root) changelog_window = tk.Toplevel(root)
changelog_window.title("Video2Crops Changelog") changelog_window.title("Video2Crops Changelog")
try: try:
debug('DEBUG', f"Reading the RSS feed from: {rss_feed_url}") debug('DEBUG', f"Reading the RSS feed from: {RSS_FEED_URL}")
# Parse the RSS feed # Parse the RSS feed
feed = feedparser.parse(rss_feed_url) feed = feedparser.parse(RSS_FEED_URL)
if not feed.entries: if not feed.entries:
scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD) scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD)
@ -195,9 +51,9 @@ def open_changelog_window():
if version_str is not None and changelog_md is not None: if version_str is not None and changelog_md is not None:
version_info = f"# v{version_str}" version_info = f"# v{version_str}"
print(version_str) print(version_str)
if str(version_str) == str(v2cversion) and entry_num == 1: if str(version_str) == str(V2CVERSION) and entry_num == 1:
version_info += "(up to date)" version_info += "(up to date)"
elif str(version_str) == str(v2cversion): elif str(version_str) == str(V2CVERSION):
version_info += "(current)" version_info += "(current)"
elif entry_num == 1: elif entry_num == 1:
version_info += "(latest)" version_info += "(latest)"
@ -222,44 +78,18 @@ def open_changelog_window():
scrolled_text.insert(tk.END, f"An error occurred: {str(e)}") scrolled_text.insert(tk.END, f"An error occurred: {str(e)}")
debug('ERROR', f"An error occured while processing changelog:\n{str(e)}\n") debug('ERROR', f"An error occured while processing changelog:\n{str(e)}\n")
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 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
# Function for updating Video2Crops # Function for updating Video2Crops
def check_for_updates(): def check_for_updates():
debug('DEBUG', f"Checking for updates") debug('DEBUG', f"Checking for updates")
try: try:
# Parse the RSS feed # Parse the RSS feed
feed = feedparser.parse(rss_feed_url) feed = feedparser.parse(RSS_FEED_URL)
if not feed.entries and not STARTING: if not feed.entries and not STARTING:
response = tk.messagebox.showinfo( response = tk.messagebox.showinfo(
"No updates found", "No updates found",
f"Latest version: ({v2cversion})\n" f"Latest version: ({V2CVERSION})\n"
f"Current version: ({v2cversion})\n" f"Current version: ({V2CVERSION})\n"
) )
debug('DEBUG', f"No updates found") debug('DEBUG', f"No updates found")
return return
@ -271,8 +101,8 @@ def check_for_updates():
if latest_version_str is None and not STARTING: if latest_version_str is None and not STARTING:
tk.messagebox.showinfo( tk.messagebox.showinfo(
"No update Available", "No update Available",
f"Latest version: ({v2cversion})\n" f"Latest version: ({V2CVERSION})\n"
f"Current version: ({v2cversion})\n" f"Current version: ({V2CVERSION})\n"
) )
debug('DEBUG', f"No updates available") debug('DEBUG', f"No updates available")
return return
@ -281,12 +111,12 @@ def check_for_updates():
latest_version = tuple(map(int, latest_version_str.split('.'))) latest_version = tuple(map(int, latest_version_str.split('.')))
# Compare the versions # Compare the versions
if latest_version > (v2cversion1, v2cversion2, v2cversion3): if latest_version > (V2CV[0], V2CV[1], V2CV[2]):
# Prompt the user for an update # Prompt the user for an update
response = tk.messagebox.askyesno( response = tk.messagebox.askyesno(
"Update Available", "Update Available",
f"A newer version ({latest_version_str}) is available.\n" f"A newer version ({latest_version_str}) is available.\n"
f"Current version: ({v2cversion})\n" f"Current version: ({V2CVERSION})\n"
"Do you want to update?" "Do you want to update?"
) )
debug('DEBUG', f"Found new update, prompting user") debug('DEBUG', f"Found new update, prompting user")
@ -338,8 +168,8 @@ def check_for_updates():
if not STARTING: if not STARTING:
tk.messagebox.showinfo( tk.messagebox.showinfo(
"No update Available", "No update Available",
f"Latest version: ({v2cversion})\n" f"Latest version: ({V2CVERSION})\n"
f"Current version: ({v2cversion})\n" f"Current version: ({V2CVERSION})\n"
) )
debug('DEBUG', f"No update available") debug('DEBUG', f"No update available")
except Exception as e: except Exception as e:
@ -403,6 +233,8 @@ def process_video(video_filenames, main_window):
return np.std(grayscale) return np.std(grayscale)
def check_duplicate(ref_frame, frame): def check_duplicate(ref_frame, frame):
global first_frame
if duplicate_threshold > 0 and not first_frame:
frame_is_duplicate = True frame_is_duplicate = True
# Calculate the Absolute Difference between the frames # Calculate the Absolute Difference between the frames
frame_diff = cv2.absdiff(ref_frame, frame) frame_diff = cv2.absdiff(ref_frame, frame)
@ -417,6 +249,8 @@ def process_video(video_filenames, main_window):
frame_is_duplicate = False frame_is_duplicate = False
return frame_is_duplicate, mse, ref_frame return frame_is_duplicate, mse, ref_frame
else:
return False, 0000, frame
corner1 = (-1, -1) corner1 = (-1, -1)
corner2 = (-1, -1) corner2 = (-1, -1)
@ -429,6 +263,9 @@ def process_video(video_filenames, main_window):
debug('DEBUG', f"Breaking loop due to processing = {processing}") debug('DEBUG', f"Breaking loop due to processing = {processing}")
break break
global first_frame
first_frame = True
main_window.title(f"Video Processing App") main_window.title(f"Video Processing App")
video_file_src = videofilename video_file_src = videofilename
@ -477,9 +314,6 @@ def process_video(video_filenames, main_window):
cv2.namedWindow(vp_title, cv2.WINDOW_NORMAL) cv2.namedWindow(vp_title, cv2.WINDOW_NORMAL)
cv2.resizeWindow(vp_title, initial_width, initial_height) cv2.resizeWindow(vp_title, initial_width, initial_height)
# Store the first frame as reference for the duplicate check
ret, reference_frame = cap.read()
while True: while True:
ret, frame = cap.read() ret, frame = cap.read()
@ -502,6 +336,9 @@ 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)
@ -520,7 +357,7 @@ def process_video(video_filenames, main_window):
frame_is_duplicate, frame_duplicate_mse, reference_frame = check_duplicate(reference_frame, 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 >= contrast_threshold and not frame_is_duplicate: 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"
@ -552,12 +389,10 @@ def process_video(video_filenames, main_window):
cap.release() cap.release()
cv2.destroyAllWindows() cv2.destroyAllWindows()
debug('DEBUG', f"Destroyed all current CV2 windows") 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.")
first_frame = False
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")]) file_paths = filedialog.askopenfilenames(filetypes=[("Video Files", "*.mp4 *.avi *.mov")])
@ -606,7 +441,6 @@ def process_files():
messagebox.showinfo("Info", "Video2Crops is currently processing files!") messagebox.showinfo("Info", "Video2Crops is currently processing files!")
debug('DEBUG', f"File processing done") debug('DEBUG', f"File processing done")
# Define dynamic process/cancel button # Define dynamic process/cancel button
def process_cancel_toggle(): def process_cancel_toggle():
debug('DEBUG', f"Toggling process/cancel button state") debug('DEBUG', f"Toggling process/cancel button state")
@ -629,7 +463,6 @@ def process_cancel_toggle():
processing_button_state = "start" processing_button_state = "start"
debug('DEBUG', f"Processing/Cancel button state switch finished") debug('DEBUG', f"Processing/Cancel button state switch finished")
# 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") debug('DEBUG', f"Opening preferences window")
@ -637,11 +470,11 @@ def open_preferences():
preferences_window.title("Preferences") preferences_window.title("Preferences")
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) write2ini('settings', 'frame_step', new_frame_step)
debug('DEBUG', f"Set frame stepping to {frame_step}") debug('DEBUG', f"Set frame stepping to {frame_step}")
@ -649,8 +482,9 @@ def open_preferences():
messagebox.showinfo("Info", "Video2Crops is currently processing files!") messagebox.showinfo("Info", "Video2Crops is currently processing files!")
def change_contrast_threshold(): def change_contrast_threshold():
global contrast_threshold
if not processing: if not processing:
new_contrast_threshold = simpledialog.askfloat("Contrast 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_contrast_threshold is not None: if new_contrast_threshold is not None:
contrast_threshold_var.set(str(new_contrast_threshold)) contrast_threshold_var.set(str(new_contrast_threshold))
write2ini('settings', 'contrast_threshold', new_contrast_threshold) write2ini('settings', 'contrast_threshold', new_contrast_threshold)
@ -659,8 +493,9 @@ def open_preferences():
messagebox.showinfo("Info", "Video2Crops is currently processing files!") messagebox.showinfo("Info", "Video2Crops is currently processing files!")
def change_duplicate_threshold(): def change_duplicate_threshold():
global duplicate_threshold
if not processing: if not processing:
new_duplicate_threshold = simpledialog.askfloat("Duplicate Threshold", "Enter the new value threshold:") new_duplicate_threshold = simpledialog.askfloat("Duplicate Threshold", "Enter the new value threshold:", initialvalue=duplicate_threshold, minvalue=0, maxvalue=100)
if new_duplicate_threshold is not None: if new_duplicate_threshold is not None:
duplicate_threshold_var.set(str(new_duplicate_threshold)) duplicate_threshold_var.set(str(new_duplicate_threshold))
write2ini('settings', 'duplicate_threshold', new_duplicate_threshold) write2ini('settings', 'duplicate_threshold', new_duplicate_threshold)
@ -668,27 +503,10 @@ def open_preferences():
else: else:
messagebox.showinfo("Info", "Video2Crops is currently processing files!") messagebox.showinfo("Info", "Video2Crops is currently processing files!")
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!")
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) write2ini('settings', 'roi_size', new_roi_size)
@ -708,6 +526,9 @@ def open_preferences():
contrast_threshold_label = tk.Label(preferences_window, text="Contrast Threshold:") contrast_threshold_label = tk.Label(preferences_window, text="Contrast Threshold:")
contrast_threshold_label.grid(row=1, column=0, padx=20, pady=5) contrast_threshold_label.grid(row=1, column=0, padx=20, pady=5)
if contrast_threshold == float(0):
contrast_threshold_value = tk.Label(preferences_window, text="Disabled")
else:
contrast_threshold_value = tk.Label(preferences_window, textvariable=contrast_threshold_var) 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_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 = tk.Button(preferences_window, text="Edit", command=change_contrast_threshold)
@ -715,6 +536,9 @@ def open_preferences():
duplicate_threshold_label = tk.Label(preferences_window, text="Duplicate Threshold:") duplicate_threshold_label = tk.Label(preferences_window, text="Duplicate Threshold:")
duplicate_threshold_label.grid(row=2, column=0, padx=20, pady=5) 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 = tk.Label(preferences_window, textvariable=duplicate_threshold_var)
duplicate_threshold_value.grid(row=2, column=1, padx=20, pady=5) 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 = tk.Button(preferences_window, text="Edit", command=change_duplicate_threshold)
@ -795,11 +619,11 @@ 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("https://git.rolfsvaag.no/frarol96/Video2Crops/wiki")) info_menu.add_command(label="About", command=lambda: webbrowser.open(WIKI_URL))
info_menu.add_command(label=f"Check for updates", command=check_for_updates) 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_command(label=f"View Changelog", command=open_changelog_window)
info_menu.add_separator() info_menu.add_separator()
info_menu.add_command(state="disabled", label=f"Version: {str(v2cversion)}") 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:")
@ -808,7 +632,7 @@ 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=5) selected_files_listbox.pack(fill=tk.BOTH, expand=True, padx=20, pady=5)
DEBUG
# 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=5)