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 os
import cv2, os, sys, re, threading, webbrowser, screeninfo
import numpy as np
import tkinter as tk
from tkinterdnd2 import DND_FILES, TkinterDnD
from tkinter import filedialog, simpledialog, messagebox, scrolledtext
from datetime import datetime
import sys
import webbrowser
import screeninfo
import feedparser
import subprocess
import platform
import urllib.request
import re
from bs4 import BeautifulSoup
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()
from v2cmodules.toolbox import extract_version, debug, checkinifile, write2ini, extract_changelog, toggle_save, toggle_show
from v2cmodules.common import *
checkinifile()
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
root = TkinterDnD.Tk()
root.title("Video2Crops")
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():
debug('DEBUG', 'Loading changelog window')
changelog_window = tk.Toplevel(root)
changelog_window.title("Video2Crops Changelog")
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
feed = feedparser.parse(rss_feed_url)
feed = feedparser.parse(RSS_FEED_URL)
if not feed.entries:
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:
version_info = f"# v{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)"
elif str(version_str) == str(v2cversion):
elif str(version_str) == str(V2CVERSION):
version_info += "(current)"
elif entry_num == 1:
version_info += "(latest)"
@ -222,44 +78,18 @@ def open_changelog_window():
scrolled_text.insert(tk.END, f"An error occurred: {str(e)}")
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
def check_for_updates():
debug('DEBUG', f"Checking for updates")
try:
# Parse the RSS feed
feed = feedparser.parse(rss_feed_url)
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"
f"Latest version: ({V2CVERSION})\n"
f"Current version: ({V2CVERSION})\n"
)
debug('DEBUG', f"No updates found")
return
@ -271,8 +101,8 @@ def check_for_updates():
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"
f"Latest version: ({V2CVERSION})\n"
f"Current version: ({V2CVERSION})\n"
)
debug('DEBUG', f"No updates available")
return
@ -281,12 +111,12 @@ def check_for_updates():
latest_version = tuple(map(int, latest_version_str.split('.')))
# Compare the versions
if latest_version > (v2cversion1, v2cversion2, v2cversion3):
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"
f"Current version: ({V2CVERSION})\n"
"Do you want to update?"
)
debug('DEBUG', f"Found new update, prompting user")
@ -338,8 +168,8 @@ def check_for_updates():
if not STARTING:
tk.messagebox.showinfo(
"No update Available",
f"Latest version: ({v2cversion})\n"
f"Current version: ({v2cversion})\n"
f"Latest version: ({V2CVERSION})\n"
f"Current version: ({V2CVERSION})\n"
)
debug('DEBUG', f"No update available")
except Exception as e:
@ -403,6 +233,8 @@ def process_video(video_filenames, main_window):
return np.std(grayscale)
def check_duplicate(ref_frame, frame):
global first_frame
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)
@ -417,6 +249,8 @@ def process_video(video_filenames, main_window):
frame_is_duplicate = False
return frame_is_duplicate, mse, ref_frame
else:
return False, 0000, frame
corner1 = (-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}")
break
global first_frame
first_frame = True
main_window.title(f"Video Processing App")
video_file_src = videofilename
@ -477,9 +314,6 @@ def process_video(video_filenames, main_window):
cv2.namedWindow(vp_title, cv2.WINDOW_NORMAL)
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:
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)
cropped_image = frame[y1:y2, x1:x2]
if first_frame:
reference_frame = cropped_image
if SHOW:
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_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"
filename = videofilename_base + '_' + str(counter).zfill(4) + ".png"
@ -552,12 +389,10 @@ def process_video(video_filenames, main_window):
cap.release()
cv2.destroyAllWindows()
debug('DEBUG', f"Destroyed all current CV2 windows")
write_log(f"Video2Crops run complete!\nSaved {counter} frames.\nDiscarded {discarded_frames} frames.")
first_frame = False
processing = False
# Create a function to add files to the selected files Listbox
def add_files():
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!")
debug('DEBUG', f"File processing done")
# Define dynamic process/cancel button
def process_cancel_toggle():
debug('DEBUG', f"Toggling process/cancel button state")
@ -629,7 +463,6 @@ def process_cancel_toggle():
processing_button_state = "start"
debug('DEBUG', f"Processing/Cancel button state switch finished")
# Create a function to open the preferences window
def open_preferences():
debug('DEBUG', f"Opening preferences window")
@ -637,11 +470,11 @@ def open_preferences():
preferences_window.title("Preferences")
def change_frame_step():
global frame_step
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:
frame_step_var.set(str(new_frame_step))
global frame_step
frame_step = new_frame_step
write2ini('settings', 'frame_step', new_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!")
def change_contrast_threshold():
global contrast_threshold
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:
contrast_threshold_var.set(str(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!")
def change_duplicate_threshold():
global duplicate_threshold
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:
duplicate_threshold_var.set(str(new_duplicate_threshold))
write2ini('settings', 'duplicate_threshold', new_duplicate_threshold)
@ -668,27 +503,10 @@ def open_preferences():
else:
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():
global roi_size
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:
roi_size_var.set(str(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.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.grid(row=1, column=1, padx=20, pady=5)
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.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)
@ -795,11 +619,11 @@ info_menu = tk.Menu(menu, tearoff=0, disabledforeground="#000")
menu.add_cascade(label="Info", menu=info_menu)
info_menu.add_command(label="Project Folder", command=lambda: webbrowser.open(os.getcwd()))
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"View Changelog", command=open_changelog_window)
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
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
selected_files_listbox = tk.Listbox(root, selectmode=tk.SINGLE, exportselection=0)
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
select_files_button = tk.Button(root, text="Select Files", command=add_files)
select_files_button.pack(padx=20, pady=5)