目錄
- 背景
- 源代碼
- end
背景
用python寫一個windows環境運行的動作視頻切截圖研究器,用路徑瀏覽的方式指定待處理的視頻文件,然后點擊分析按鈕,再預覽區域顯示視頻預覽畫面,然后拖動時間軸,可以在預覽區域刷新顯示相應的畫面,時間軸要能夠顯示視頻總時長和當前按鈕所在位置的,輸入想要截取的視頻時間范圍和間隔的毫秒數,最后再視頻所在路徑保存所有截取的視頻畫面圖片。
遇到CV2中文路徑無法保存的問題
filename = os.path.join(output_dir, f"frame_{i:04d}.jpg")
cv2.imwrite(filename, frame)
filename = os.path.normpath(os.path.join(output_dir, self.video_name+f"_{i:04d}.jpg"))
源代碼
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import cv2
import os
from PIL import Image, ImageTk
import threading
import queueclass VideoSlicerApp:def __init__(self, root):self.root = rootself.root.title("動作視頻切片研究器")# 窗口設置screen_width = root.winfo_screenwidth()screen_height = root.winfo_screenheight()self.root.geometry(f"{int(screen_width * 0.7)}x{int(screen_height * 0.7)}")self.root.minsize(800, 600)# 初始化變量self.video_path = tk.StringVar()self.video_name = "video"self.cap = Noneself.total_frames = 0self.fps = 0self.current_frame = 0self.original_size = (0, 0)self.preview_size = (800, 450)self.progress_queue = queue.Queue()# 創建界面self.create_widgets()self.root.bind("<Configure>", self.on_window_resize)# 定期檢查進度self.root.after(100, self.check_progress)def create_widgets(self):main_frame = ttk.Frame(self.root)main_frame.pack(fill='both', expand=True, padx=10, pady=5)# 文件選擇file_frame = ttk.Frame(main_frame)file_frame.pack(fill='x', pady=5)ttk.Label(file_frame, text="視頻文件:").pack(side='left')ttk.Entry(file_frame, textvariable=self.video_path, width=40).pack(side='left', padx=5, fill='x', expand=True)ttk.Button(file_frame, text="瀏覽", command=self.browse_file).pack(side='left')# 分析按鈕self.analyze_button = ttk.Button(main_frame, text="分析視頻", command=self.analyze_video)self.analyze_button.pack(pady=5)# 預覽區域self.preview_container = ttk.Frame(main_frame)self.preview_container.pack(fill='both', expand=True)self.preview_label = ttk.Label(self.preview_container)self.preview_label.pack(fill='both', expand=True)# 時間軸control_frame = ttk.Frame(main_frame)control_frame.pack(fill='x', pady=5)self.time_label = ttk.Label(control_frame, text="00:00:00 / 00:00:00")self.time_label.pack(side='bottom', fill='x')self.time_scale = ttk.Scale(control_frame, from_=0, to=100, command=self.update_frame)self.time_scale.pack(fill='x', padx=5)# 參數輸入param_frame = ttk.Frame(main_frame)param_frame.pack(fill='x', pady=5)ttk.Label(param_frame, text="開始時間(ms):").grid(row=0, column=0, padx=5)self.start_time = ttk.Entry(param_frame, width=10)self.start_time.grid(row=0, column=1, padx=5)ttk.Label(param_frame, text="結束時間(ms):").grid(row=0, column=2, padx=5)self.end_time = ttk.Entry(param_frame, width=10)self.end_time.grid(row=0, column=3, padx=5)ttk.Label(param_frame, text="間隔(ms):").grid(row=0, column=4, padx=5)self.interval = ttk.Entry(param_frame, width=10)self.interval.grid(row=0, column=5, padx=5)# 進度條self.progress = ttk.Progressbar(main_frame, orient='horizontal', mode='determinate')self.progress.pack(fill='x', pady=5)# 保存按鈕self.save_button = ttk.Button(main_frame, text="保存切片", command=self.save_slices)self.save_button.pack(pady=5)def browse_file(self):file_path = filedialog.askopenfilename(filetypes=[("視頻文件", "*.mp4 *.avi *.mov")])if file_path:self.video_path.set(file_path)def analyze_video(self):if not self.video_path.get():messagebox.showerror("錯誤", "請先選擇視頻文件")returnself.video_name = os.path.splitext(os.path.basename(repr(self.video_path.get())))[0]print(self.video_name)self.cap = cv2.VideoCapture(self.video_path.get())if not self.cap.isOpened():messagebox.showerror("錯誤", "無法打開視頻文件")returnself.fps = self.cap.get(cv2.CAP_PROP_FPS)self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))self.time_scale.config(to=self.total_frames)self.original_size = (int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)),int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))self.update_preview_size()self.show_frame(0)def update_preview_size(self):container_width = self.preview_container.winfo_width()container_height = self.preview_container.winfo_height()max_width = max(container_width - 20, 100)max_height = max(container_height - 20, 100)ratio = min(max_width / self.original_size[0], max_height / self.original_size[1])self.preview_size = (int(self.original_size[0] * ratio),int(self.original_size[1] * ratio))def on_window_resize(self, event):if self.cap and self.cap.isOpened():self.update_preview_size()self.show_frame(self.current_frame)def show_frame(self, frame_num):self.current_frame = frame_numself.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num)ret, frame = self.cap.read()if ret:frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)img = Image.fromarray(frame)img = img.resize(self.preview_size, Image.Resampling.LANCZOS)img_tk = ImageTk.PhotoImage(img)self.preview_label.config(image=img_tk)self.preview_label.image = img_tkcurrent_time = frame_num / self.fpstotal_time = self.total_frames / self.fpsself.time_label.config(text=f"{self.format_time(current_time)} / {self.format_time(total_time)}")def format_time(self, seconds):ms = int((seconds - int(seconds)) * 1000)seconds = int(seconds)h = seconds // 3600m = (seconds % 3600) // 60s = seconds % 60return f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}"def update_frame(self, value):frame_num = int(float(value))self.show_frame(frame_num)def save_slices(self):if not self.cap:messagebox.showerror("錯誤", "請先分析視頻")returntry:start = int(self.start_time.get())end = int(self.end_time.get())interval = int(self.interval.get())if start >= end or interval <= 0:raise ValueErrorexcept ValueError:messagebox.showerror("輸入錯誤", "請輸入有效的時間范圍\n(開始<結束,間隔>0)")return# 禁用按鈕防止重復點擊self.save_button["state"] = "disabled"self.analyze_button["state"] = "disabled"# 計算幀數范圍start_frame = int(start * self.fps / 1000)end_frame = int(end * self.fps / 1000)interval_frames = int(interval * self.fps / 1000)total_saves = (end_frame - start_frame) // interval_framesself.progress['maximum'] = total_savesself.progress['value'] = 0save_thread = threading.Thread(target=self.process_slices,args=(start_frame, end_frame, interval_frames))save_thread.daemon = Truesave_thread.start()def process_slices(self, start_frame, end_frame, interval_frames):try:# 使用獨立視頻實例cap = cv2.VideoCapture(self.video_path.get())output_dir = os.path.join(os.path.dirname(self.video_path.get()),self.video_name+"_slices")if not os.path.exists(output_dir):os.makedirs(output_dir)frame_count = 0total = (end_frame - start_frame) // interval_framesfor i in range(total):frame_num = start_frame + i * interval_framescap.set(cv2.CAP_PROP_POS_FRAMES, frame_num)ret, frame = cap.read()if ret:filename = os.path.normpath(os.path.join(output_dir, self.video_name+f"_{i:04d}.jpg"))print(filename)# cv2.imwrite(filename, frame)img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)img_pil = Image.fromarray(img)img_pil.save(filename)self.progress_queue.put(("progress", i + 1))else:self.progress_queue.put(("error", f"無法讀取 {frame_num} 幀"))cap.release()self.progress_queue.put(("done", total))except Exception as e:self.progress_queue.put(("error", str(e)))def check_progress(self):try:while not self.progress_queue.empty():msg_type, msg_data = self.progress_queue.get_nowait()if msg_type == "error":messagebox.showerror("保存錯誤", msg_data)self.reset_controls()elif msg_type == "done":self.progress['value'] = self.progress['maximum']messagebox.showinfo("完成", f"成功保存{msg_data}張切片")self.reset_controls()elif msg_type == "progress":self.progress['value'] = msg_dataexcept queue.Empty:passself.root.after(100, self.check_progress)def reset_controls(self):self.save_button["state"] = "normal"self.analyze_button["state"] = "normal"if __name__ == "__main__":root = tk.Tk()app = VideoSlicerApp(root)root.mainloop()