PTset/video_frame_extractor_gui.py
2026-04-17 16:23:12 +08:00

240 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import math
import os
import tkinter as tk
from tkinter import filedialog, messagebox
import cv2
class VideoFrameExtractorApp:
def __init__(self, root):
self.root = root
self.root.title("视频拆分图片工具")
self.root.geometry("760x520")
self.video_path = ""
self.output_dir = ""
self.video_info = {}
self._build_ui()
def _build_ui(self):
main = tk.Frame(self.root, padx=14, pady=14)
main.pack(fill="both", expand=True)
source_frame = tk.LabelFrame(main, text="视频源", padx=10, pady=10)
source_frame.pack(fill="x", pady=(0, 10))
self.video_path_var = tk.StringVar()
tk.Entry(source_frame, textvariable=self.video_path_var).pack(side="left", fill="x", expand=True)
tk.Button(source_frame, text="添加视频", width=12, command=self.choose_video).pack(side="left", padx=(8, 0))
info_frame = tk.LabelFrame(main, text="视频信息", padx=10, pady=10)
info_frame.pack(fill="x", pady=(0, 10))
self.info_labels = {}
fields = [
("file_name", "文件名"),
("resolution", "分辨率"),
("fps", "视频帧率"),
("frame_count", "总帧数"),
("duration", "总时长"),
("codec", "编码格式"),
("size", "文件大小"),
]
for row, (key, title) in enumerate(fields):
tk.Label(info_frame, text=f"{title}", width=12, anchor="w").grid(row=row, column=0, sticky="w", pady=2)
label = tk.Label(info_frame, text="未加载", anchor="w")
label.grid(row=row, column=1, sticky="w", pady=2)
self.info_labels[key] = label
settings_frame = tk.LabelFrame(main, text="抽帧设置", padx=10, pady=10)
settings_frame.pack(fill="x", pady=(0, 10))
tk.Label(settings_frame, text="每秒抽取图片数:", width=14, anchor="w").grid(row=0, column=0, sticky="w")
self.target_fps_var = tk.StringVar(value="5")
tk.Entry(settings_frame, textvariable=self.target_fps_var, width=12).grid(row=0, column=1, sticky="w")
tk.Label(settings_frame, text="输出文件夹:", width=14, anchor="w").grid(row=1, column=0, sticky="w", pady=(10, 0))
self.output_dir_var = tk.StringVar()
tk.Entry(settings_frame, textvariable=self.output_dir_var).grid(row=1, column=1, sticky="ew", pady=(10, 0))
tk.Button(settings_frame, text="选择输出", width=12, command=self.choose_output_dir).grid(row=1, column=2, padx=(8, 0), pady=(10, 0))
settings_frame.grid_columnconfigure(1, weight=1)
action_frame = tk.Frame(main)
action_frame.pack(fill="x", pady=(0, 10))
tk.Button(action_frame, text="开始拆分", width=14, command=self.extract_frames).pack(side="left")
self.progress_var = tk.StringVar(value="等待开始")
tk.Label(main, textvariable=self.progress_var, anchor="w", justify="left", fg="darkgreen").pack(fill="x")
def choose_video(self):
video_path = filedialog.askopenfilename(
title="选择视频文件",
filetypes=[("Video files", "*.mp4 *.avi *.mov *.mkv *.flv *.wmv"), ("All files", "*.*")],
)
if not video_path:
return
self.video_path = video_path
self.video_path_var.set(video_path)
self.load_video_info(video_path)
if not self.output_dir_var.get().strip():
default_name = os.path.splitext(os.path.basename(video_path))[0] + "_frames"
self.output_dir = os.path.join(os.path.dirname(video_path), default_name)
self.output_dir_var.set(self.output_dir)
def choose_output_dir(self):
output_dir = filedialog.askdirectory(title="选择输出文件夹")
if not output_dir:
return
self.output_dir = output_dir
self.output_dir_var.set(output_dir)
def load_video_info(self, video_path):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
messagebox.showerror("错误", "无法打开该视频文件。")
return
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
duration_seconds = frame_count / fps if fps > 0 else 0
fourcc_value = int(cap.get(cv2.CAP_PROP_FOURCC) or 0)
cap.release()
self.video_info = {
"file_name": os.path.basename(video_path),
"resolution": f"{width} x {height}",
"fps": f"{fps:.3f}" if fps else "未知",
"frame_count": str(frame_count) if frame_count else "未知",
"duration": self.format_duration(duration_seconds),
"codec": self.decode_fourcc(fourcc_value),
"size": self.format_size(os.path.getsize(video_path)),
}
for key, value in self.video_info.items():
self.info_labels[key].config(text=value)
self.progress_var.set("视频信息已读取,可以设置抽帧参数。")
def decode_fourcc(self, value):
if value <= 0:
return "未知"
chars = [chr((value >> 8 * i) & 0xFF) for i in range(4)]
codec = "".join(chars).strip()
return codec if codec else "未知"
def format_duration(self, seconds):
seconds = max(0, int(round(seconds)))
hours = seconds // 3600
minutes = (seconds % 3600) // 60
secs = seconds % 60
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
def format_size(self, size_bytes):
units = ["B", "KB", "MB", "GB", "TB"]
size = float(size_bytes)
index = 0
while size >= 1024 and index < len(units) - 1:
size /= 1024
index += 1
return f"{size:.2f} {units[index]}"
def extract_frames(self):
video_path = self.video_path_var.get().strip()
output_dir = self.output_dir_var.get().strip()
target_fps_text = self.target_fps_var.get().strip()
if not video_path:
messagebox.showwarning("提示", "请先添加视频文件。")
return
if not os.path.exists(video_path):
messagebox.showwarning("提示", "视频文件不存在,请重新选择。")
return
if not output_dir:
messagebox.showwarning("提示", "请选择输出文件夹。")
return
try:
target_fps = float(target_fps_text)
except ValueError:
messagebox.showwarning("提示", "每秒抽取图片数必须是数字。")
return
if target_fps <= 0:
messagebox.showwarning("提示", "每秒抽取图片数必须大于 0。")
return
os.makedirs(output_dir, exist_ok=True)
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
messagebox.showerror("错误", "无法打开视频文件。")
return
original_fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
if original_fps <= 0:
cap.release()
messagebox.showerror("错误", "无法读取视频帧率。")
return
if target_fps >= original_fps:
interval = 1
actual_fps = original_fps
else:
interval = max(1, int(round(original_fps / target_fps)))
actual_fps = original_fps / interval
frame_index = 0
saved_count = 0
base_name = os.path.splitext(os.path.basename(video_path))[0]
self.progress_var.set(
f"开始拆分:原始 FPS={original_fps:.3f},目标={target_fps:.3f},实际约={actual_fps:.3f} 张/秒"
)
self.root.update_idletasks()
while True:
success, frame = cap.read()
if not success:
break
if frame_index % interval == 0:
timestamp = frame_index / original_fps
output_name = f"{base_name}_{saved_count:06d}_{timestamp:09.3f}s.jpg"
output_path = os.path.join(output_dir, output_name)
cv2.imwrite(output_path, frame)
saved_count += 1
frame_index += 1
if frame_count and frame_index % 100 == 0:
percent = frame_index / frame_count * 100
self.progress_var.set(f"处理中:{frame_index}/{frame_count} 帧,约 {percent:.1f}%")
self.root.update_idletasks()
cap.release()
self.progress_var.set(
f"拆分完成:共导出 {saved_count} 张图片,输出目录:{output_dir}"
)
messagebox.showinfo(
"完成",
f"视频拆分完成。\n\n导出图片:{saved_count}\n输出目录:{output_dir}\n实际抽取频率:约 {actual_fps:.3f} 张/秒",
)
def main():
root = tk.Tk()
VideoFrameExtractorApp(root)
root.mainloop()
if __name__ == "__main__":
main()