240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
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()
|