Video2Crops/Video2Crops.py

547 lines
21 KiB
Python

import cv2
import os
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
print(f"New start\n")
# Constants
v2cversion1 = 1
v2cversion2 = 1
v2cversion3 = 0
v2cversion = f"{v2cversion1}.{v2cversion2}.{v2cversion3}"
frame_step = 100
value_threshold = 11.0
SAVE = True
SHOW = True
processing = False
# 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("300x450")
def open_changelog_window():
changelog_window = tk.Toplevel(root)
changelog_window.title("Video2Crops Changelog")
try:
# Parse the RSS feed
feed = feedparser.parse(rss_feed_url)
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)
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)
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)}")
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():
try:
# Parse the RSS feed
feed = feedparser.parse(rss_feed_url)
if not feed.entries:
response = tk.messagebox.showinfo(
"No updates found",
f"Latest version: ({v2cversion})\n"
f"Current version: ({v2cversion})\n"
)
return
# Get the latest release
latest_release = feed.entries[0]
latest_version_str = extract_version(latest_release.link)
if latest_version_str is None:
tk.messagebox.showinfo(
"No update Available",
f"Latest version: ({v2cversion})\n"
f"Current version: ({v2cversion})\n"
)
return
print(f"RSS Version: {latest_version_str}")
# Extract version numbers from the latest release title
latest_version = tuple(map(int, latest_version_str.split('.')))
# Compare the versions
if latest_version > (v2cversion1, v2cversion2, v2cversion3):
# 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?"
)
if response:
# Determine the platform
current_platform = platform.system().lower()
if current_platform == "windows":
# 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)
root.quit()
except:
tk.messagebox.showinfo(
"Installer not found",
f"The installer file was not found.\nPlease check your antivirus and network connectivity"
)
elif current_platform == "linux":
# 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)
root.quit()
except:
tk.messagebox.showinfo(
"Executable not found",
f"The executable file was not found.\nPlease check your antivirus and network connectivity"
)
else:
tk.messagebox.showinfo(
"No update Available",
f"Latest version: ({v2cversion})\n"
f"Current version: ({v2cversion})\n"
)
except Exception as e:
tk.messagebox.showerror("Error", f"An error occurred: {str(e)}")
# Function to create the output directory and return its path
def create_output_directory(video_filename):
base_directory = os.getcwd() # You can change this to specify a different base directory
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)
return output_folder_path
# Function to calculate ROI position based on alignment and size settings
def calculate_roi(video_width, video_height, vertical_alignment, horizontal_alignment, roi_size):
y1 = (video_height - roi_size) // 2
x1 = (video_width - roi_size) // 2
if vertical_alignment == "Top":
y1 = 0
elif vertical_alignment == "Center":
y1 = (video_height - roi_size) // 2
elif vertical_alignment == "Bottom":
y1 = video_height - roi_size
if horizontal_alignment == "Left":
x1 = 0
elif horizontal_alignment == "Center":
x1 = (video_width - roi_size) // 2
elif horizontal_alignment == "Right":
x1 = video_width - roi_size
x2 = x1 + roi_size
y2 = y1 + roi_size
return x1, y1, x2, y2
# Create a function to process video files
def process_video(video_filenames, main_window):
global processing
processing = True
def write_log(message):
logfile_path = os.path.join(output_folder, 'logfile.txt')
with open(logfile_path, 'a') as logfile:
log_message = f"{datetime.now().strftime('%m-%d_%H:%M:%S')} - {message}\n"
logfile.write(log_message)
print(log_message)
def calc_value(image):
grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return np.std(grayscale)
corner1 = (-1, -1)
corner2 = (-1, -1)
drawing = False
cropped_image = None
for videofilename in video_filenames:
main_window.title(f"Video Processing App")
video_file_src = videofilename
# Define videofilename_base based on the video file name
videofilename_base, _ = os.path.splitext(os.path.basename(videofilename))
cap = cv2.VideoCapture(video_file_src, 0)
if not cap.isOpened():
write_log(f"Error: Could not open video file: {video_file_src}")
sys.exit()
counter = 0
frame_cnt = 0
discarded_frames = 0
# Create the output folder early on
output_folder = create_output_directory(videofilename)
# Create the "Discarded Frames" subfolder within the output folder
discarded_folder = os.path.join(output_folder, 'Discarded Frames')
if not os.path.exists(discarded_folder) and SAVE:
os.makedirs(discarded_folder)
write_log(f"[DEBUG] Output Folder: {output_folder}\n[DEBUG] Discarded Folder: {discarded_folder}")
# Initialize ROI parameters from preferences
roi_size = int(roi_size_var.get())
vertical_alignment = vertical_alignment_var.get()
horizontal_alignment = horizontal_alignment_var.get()
if SHOW:
vp_title = "Video2Crops Video Player"
# Get the screen resolution
screen = screeninfo.get_monitors()[0]
screen_width, screen_height = screen.width, screen.height
# Calculate the initial window size as a quarter of the screen resolution
initial_width = screen_width // 2
initial_height = screen_height // 2
# Create the video player window
cv2.namedWindow(vp_title, cv2.WINDOW_NORMAL)
cv2.resizeWindow(vp_title, initial_width, initial_height)
while True:
ret, frame = cap.read()
if not ret:
break
# Get the video width and height from the frame
video_width = frame.shape[1]
video_height = frame.shape[0]
x1, y1, x2, y2 = calculate_roi(video_width, video_height, vertical_alignment, horizontal_alignment, roi_size)
cropped_image_status = "Rejected"
frame_copy = frame.copy()
if drawing:
cv2.rectangle(frame_copy, corner1, corner2, (0, 255, 0), 3)
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 SHOW:
cv2.imshow(vp_title, test)
key = cv2.waitKey(10)
if key == ord('q'):
break
if cropped_image is not None and frame_cnt % frame_step == 0:
if SHOW:
ci_title = "Video2Crops Cropped Image"
cv2.namedWindow(ci_title, cv2.WINDOW_NORMAL)
cv2.imshow(ci_title, cropped_image)
frame_value = calc_value(cropped_image)
if frame_value >= value_threshold:
cropped_image_status = "Accepted"
filename = videofilename_base + '_' + str(counter).zfill(4) + ".png"
image_filename = os.path.join(output_folder, filename)
if SAVE:
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)}"
write_log(cropped_image_info)
counter += 1
else:
filename = videofilename_base + '_' + str(discarded_frames).zfill(4) + ".png"
image_filename = os.path.join(discarded_folder, filename)
if SAVE:
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)}"
write_log(cropped_image_info)
discarded_frames += 1
frame_cnt += 1
cap.release()
cv2.destroyAllWindows()
write_log(f"Video2Crops run complete!\nSaved {counter} frames.\nDiscarded {discarded_frames} frames.")
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")])
if file_paths:
for file_path in file_paths:
selected_files_listbox.insert(tk.END, file_path)
# Create a button to remove the selected file from the Listbox
def remove_file():
selected_index = selected_files_listbox.curselection()
if selected_index:
selected_files_listbox.delete(selected_index)
# Create a function to clear the queue
def clear_queue():
selected_files_listbox.delete(0, tk.END)
# Create a function to cancel processing
def cancel_processing():
global processing
if processing:
processing = False
messagebox.showinfo("Processing Canceled", "Video processing has been canceled.")
# Create a button to process the selected files
def process_files():
global processing
if not processing:
selected_files = selected_files_listbox.get(0, tk.END)
for file_path in selected_files:
process_video([file_path], root)
# Create a function to open the preferences window
def open_preferences():
preferences_window = tk.Toplevel(root)
preferences_window.title("Preferences")
def change_frame_step():
if not processing:
new_frame_step = simpledialog.askinteger("Frame Step", "Enter the new frame step:")
if new_frame_step is not None:
frame_step_var.set(str(new_frame_step))
global frame_step
frame_step = new_frame_step
def change_value_threshold():
if not processing:
new_value_threshold = simpledialog.askfloat("Value Threshold", "Enter the new value threshold:")
if new_value_threshold is not None:
value_threshold_var.set(str(new_value_threshold))
def toggle_save():
if not processing:
global SAVE
SAVE = not SAVE
def toggle_show():
if not processing:
global SHOW
SHOW = not SHOW
def apply_roi_size():
if not processing:
new_roi_size = simpledialog.askinteger("ROI Size", "Enter the new ROI size:")
if new_roi_size is not None:
roi_size_var.set(str(new_roi_size))
# Create preference fields
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_value = tk.Label(preferences_window, textvariable=frame_step_var)
frame_step_value.grid(row=0, column=1, padx=20, pady=10)
frame_step_button = tk.Button(preferences_window, text="Edit", command=change_frame_step)
frame_step_button.grid(row=0, column=2, padx=20, pady=10)
value_threshold_label = tk.Label(preferences_window, text="Discard Threshold:")
value_threshold_label.grid(row=1, column=0, padx=20, pady=10)
value_threshold_value = tk.Label(preferences_window, textvariable=value_threshold_var)
value_threshold_value.grid(row=1, column=1, padx=20, pady=10)
value_threshold_button = tk.Button(preferences_window, text="Edit", command=change_value_threshold)
value_threshold_button.grid(row=1, column=2, padx=20, pady=10)
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)
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)
# Section divider
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))
# Create ROI fields
vertical_alignment_label = tk.Label(preferences_window, text="Vertical Alignment:")
vertical_alignment_label.grid(row=5, column=0, padx=20, pady=10)
vertical_alignment_var.set("Center") # Default alignment
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)
horizontal_alignment_label = tk.Label(preferences_window, text="Horizontal Alignment:")
horizontal_alignment_label.grid(row=6, column=0, padx=20, pady=10)
horizontal_alignment_var.set("Center") # Default alignment
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)
roi_size_label = tk.Label(preferences_window, text="ROI Size:")
roi_size_label.grid(row=7, column=0, padx=20, pady=10)
roi_size_value = tk.Label(preferences_window, textvariable=roi_size_var)
roi_size_value.grid(row=7, column=1, padx=20, pady=10)
roi_size_button = tk.Button(preferences_window, text="Edit", command=apply_roi_size)
roi_size_button.grid(row=7, column=2, padx=20, pady=10)
# Create a menu
menu = tk.Menu(root)
root.config(menu=menu)
# Variables for preferences
frame_step_var = tk.StringVar()
value_threshold_var = tk.StringVar()
save_var = tk.BooleanVar()
show_var = tk.BooleanVar()
# Variables for ROI
vertical_alignment_var = tk.StringVar()
horizontal_alignment_var = tk.StringVar()
roi_size_var = tk.StringVar()
roi_size_var.set("400")
# Set initial values for preference variables
frame_step_var.set(str(frame_step))
value_threshold_var.set(str(value_threshold))
save_var.set(SAVE)
show_var.set(SHOW)
# Create a "Queue" submenu
queue_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Queue", menu=queue_menu)
queue_menu.add_command(label="Add file", command=add_files)
queue_menu.add_command(label="Clear queue", command=clear_queue)
queue_menu.add_separator()
queue_menu.add_command(label="Cancel", command=cancel_processing)
queue_menu.add_command(label="Cancel & Clear", command=lambda: [cancel_processing(), clear_queue()])
# Create a "Preferences" submenu
menu.add_command(label="Preferences", command=open_preferences)
# Create an "Info" submenu
info_menu = tk.Menu(menu, tearoff=0)
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=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)
# Create a label for selected files
label = tk.Label(root, text="Selected Files:")
label.pack(padx=20, pady=10)
# 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=10)
# 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=10)
# 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.pack(padx=20, pady=10)
# Create a button to process the selected files
process_files_button = tk.Button(root, text="Process Files", command=process_files)
process_files_button.pack(padx=20, pady=10)
root.mainloop()