Skip to content

Commit 18cb3b0

Browse files
authored
Create VID2IMG.py
1 parent b609eb7 commit 18cb3b0

1 file changed

Lines changed: 197 additions & 0 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import tkinter as tk
2+
from tkinter import messagebox, filedialog
3+
import ttkbootstrap as tb
4+
from ttkbootstrap.widgets.scrolled import ScrolledText
5+
import threading
6+
import os
7+
import cv2 # OpenCV for video processing
8+
import time
9+
10+
# =================== Utility Functions ===================
11+
def resource_path(file_name):
12+
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
13+
return os.path.join(base_path, file_name)
14+
15+
# ================= HELPERS =================
16+
def show_error(title, msg):
17+
messagebox.showerror(title, msg)
18+
19+
def show_info(title, msg):
20+
messagebox.showinfo(title, msg)
21+
22+
# ================= APP =================
23+
app = tb.Window("VID2IMG - Video to Image Extractor", themename="superhero", size=(1000, 600))
24+
app.grid_columnconfigure(0, weight=1)
25+
app.grid_columnconfigure(1, weight=1)
26+
app.grid_rowconfigure(1, weight=1)
27+
28+
# ================= TOP LEFT: VIDEO INPUT =================
29+
video_card = tb.Labelframe(app, text="Video Input", padding=15)
30+
video_card.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
31+
32+
video_path_var = tk.StringVar()
33+
tb.Label(video_card, text="Select Video File").pack(anchor="w")
34+
video_entry = tb.Entry(video_card, textvariable=video_path_var, width=50)
35+
video_entry.pack(side="left", pady=5, padx=(0,5))
36+
tb.Button(video_card, text="Browse", bootstyle="info", command=lambda: select_video()).pack(side="left")
37+
38+
def select_video():
39+
path = filedialog.askopenfilename(filetypes=[("Video Files", "*.mp4 *.avi *.mov")])
40+
if path:
41+
video_path_var.set(path)
42+
43+
# ================= TOP RIGHT: OUTPUT SETTINGS =================
44+
output_card = tb.Labelframe(app, text="Output Settings", padding=15)
45+
output_card.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
46+
47+
tb.Label(output_card, text="Output Folder").grid(row=0, column=0, sticky="w")
48+
output_folder_var = tk.StringVar()
49+
output_entry = tb.Entry(output_card, textvariable=output_folder_var, width=40)
50+
output_entry.grid(row=0, column=1, sticky="w", padx=5)
51+
tb.Button(output_card, text="Browse", bootstyle="info", command=lambda: select_output()).grid(row=0, column=2, sticky="w")
52+
53+
def select_output():
54+
path = filedialog.askdirectory()
55+
if path:
56+
output_folder_var.set(path)
57+
58+
tb.Label(output_card, text="Image Format").grid(row=1, column=0, sticky="w", pady=5)
59+
image_format = tb.Combobox(output_card, values=["JPG", "PNG"], state="readonly", width=8)
60+
image_format.set("JPG")
61+
image_format.grid(row=1, column=1, sticky="w", padx=5)
62+
63+
tb.Label(output_card, text="Frame Interval").grid(row=2, column=0, sticky="w", pady=5)
64+
frame_interval_var = tk.IntVar(value=30)
65+
tb.Entry(output_card, textvariable=frame_interval_var, width=10).grid(row=2, column=1, sticky="w", padx=5)
66+
67+
tb.Label(output_card, text="Image Name Prefix").grid(row=3, column=0, sticky="w", pady=5)
68+
image_prefix_var = tk.StringVar(value="frame")
69+
tb.Entry(output_card, textvariable=image_prefix_var, width=20).grid(row=3, column=1, sticky="w", padx=5)
70+
71+
# ================= BODY: LOG / PROGRESS =================
72+
log_card = tb.Labelframe(app, text="Process Log", padding=15)
73+
log_card.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=10, pady=10)
74+
log_card.grid_rowconfigure(0, weight=1)
75+
log_card.grid_columnconfigure(0, weight=1)
76+
77+
log = ScrolledText(log_card, height=20)
78+
log.grid(row=0, column=0, sticky="nsew")
79+
log.text.config(state="disabled")
80+
81+
progress_var = tk.DoubleVar(value=0)
82+
progress = tb.Progressbar(log_card, variable=progress_var, maximum=100)
83+
progress.grid(row=1, column=0, sticky="ew", pady=5)
84+
85+
# ================= FOOTER: ACTION BUTTONS =================
86+
footer_frame = tb.Frame(app)
87+
footer_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=10, pady=5)
88+
89+
stop_event = threading.Event()
90+
91+
def log_line(text):
92+
log.text.config(state="normal")
93+
log.text.insert("end", text + "\n")
94+
log.text.see("end")
95+
log.text.config(state="disabled")
96+
97+
def extract_frames():
98+
video_path = video_path_var.get()
99+
output_folder = output_folder_var.get()
100+
fmt = image_format.get().lower()
101+
interval = frame_interval_var.get()
102+
prefix = image_prefix_var.get()
103+
104+
if not video_path or not os.path.exists(video_path):
105+
show_error("Input Error", "Please select a valid video file.")
106+
return
107+
if not output_folder:
108+
show_error("Output Error", "Please select an output folder.")
109+
return
110+
111+
stop_event.clear()
112+
log.text.config(state="normal")
113+
log.text.delete("1.0", "end")
114+
log.text.config(state="disabled")
115+
progress_var.set(0)
116+
117+
def worker():
118+
cap = cv2.VideoCapture(video_path)
119+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
120+
current_frame = 0
121+
saved_count = 0
122+
123+
while cap.isOpened():
124+
if stop_event.is_set():
125+
log_line("⛔ Extraction stopped by user.")
126+
break
127+
128+
ret, frame = cap.read()
129+
if not ret:
130+
break
131+
132+
if current_frame % interval == 0:
133+
filename = f"{prefix}_{current_frame}.{fmt}"
134+
filepath = os.path.join(output_folder, filename)
135+
cv2.imwrite(filepath, frame)
136+
saved_count += 1
137+
log_line(f"Saved {filename}")
138+
139+
current_frame += 1
140+
progress_var.set(current_frame / total_frames * 100)
141+
app.update_idletasks()
142+
time.sleep(0.01)
143+
144+
cap.release()
145+
if not stop_event.is_set():
146+
show_info("Done", f"Extraction completed!\nTotal images: {saved_count}")
147+
progress_var.set(100)
148+
149+
threading.Thread(target=worker, daemon=True).start()
150+
151+
def stop_extraction():
152+
stop_event.set()
153+
154+
tb.Button(footer_frame, text="Start Extraction", bootstyle="success", width=20, command=extract_frames).pack(side="left", padx=5)
155+
tb.Button(footer_frame, text="Stop", bootstyle="danger", width=15, command=stop_extraction).pack(side="left", padx=5)
156+
157+
# ================= HELP =================
158+
def show_help():
159+
guide_window = tb.Toplevel(app)
160+
guide_window.title("📘 VID2IMG - Guide")
161+
guide_window.geometry("600x450")
162+
guide_window.resizable(False, False)
163+
guide_window.grab_set()
164+
165+
frame = tb.Frame(guide_window, padding=10)
166+
frame.pack(fill="both", expand=True)
167+
168+
sections = {
169+
"About VID2IMG": (
170+
"VID2IMG is a simple software to extract images from video files.\n"
171+
"Supports editing, anonymization, and photogrammetry workflows."
172+
),
173+
"Key Features": (
174+
"- Extract frames from videos (MP4, AVI, MOV)\n"
175+
"- Set frame intervals and output format (JPG/PNG)\n"
176+
"- Choose output folder and filename prefix\n"
177+
"- Stop/resume extraction\n"
178+
"- Progress bar and live logging"
179+
),
180+
"Requirements": (
181+
"- Windows PC\n"
182+
"- Python 3.x\n"
183+
"- OpenCV installed (`pip install opencv-python`)\n"
184+
"- ttkbootstrap installed (`pip install ttkbootstrap`)"
185+
),
186+
}
187+
188+
for title, text in sections.items():
189+
tb.Label(frame, text=title, font=("Segoe UI", 12, "bold")).pack(anchor="w", pady=(10, 0))
190+
tb.Label(frame, text=text, font=("Segoe UI", 10), wraplength=580, justify="left").pack(anchor="w", pady=(2, 5))
191+
192+
tb.Button(frame, text="Close", bootstyle="danger-outline", width=15,
193+
command=guide_window.destroy).pack(pady=10)
194+
195+
tb.Button(footer_frame, text="Help / Guide", bootstyle="info", width=15, command=show_help).pack(side="right", padx=5)
196+
197+
app.mainloop()

0 commit comments

Comments
 (0)