From 5ac83f69839195bb5ef924bec78180c52647602e Mon Sep 17 00:00:00 2001 From: frarol96 Date: Mon, 9 Oct 2023 23:14:31 +0000 Subject: [PATCH] Update Video2Crops.py Implemented value checks, code optimization and preparing to migrate functions to modules --- Video2Crops.py | 292 ++++++++++--------------------------------------- 1 file changed, 58 insertions(+), 234 deletions(-) diff --git a/Video2Crops.py b/Video2Crops.py index 63c8208..7fca611 100644 --- a/Video2Crops.py +++ b/Video2Crops.py @@ -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,20 +233,24 @@ def process_video(video_filenames, main_window): return np.std(grayscale) def check_duplicate(ref_frame, frame): - frame_is_duplicate = True - # Calculate the Absolute Difference between the frames - frame_diff = cv2.absdiff(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) - # Calculate the Mean Squared Error (MSE) between the frames - mse = np.mean(frame_diff) + # Calculate the Mean Squared Error (MSE) between the frames + mse = np.mean(frame_diff) - if 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 + if 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 - return frame_is_duplicate, mse, ref_frame + 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,14 +526,20 @@ 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) - contrast_threshold_value = tk.Label(preferences_window, textvariable=contrast_threshold_var) + 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) 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) - duplicate_threshold_value = tk.Label(preferences_window, textvariable=duplicate_threshold_var) + 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) @@ -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)