You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Python手动图片查看器添加时间轴以分析VPN调试帧

帧查看工具优化实现方案

背景

某VPN应用无本地日志,仅提供诊断窗口且保存功能无效,需通过录屏转帧分析。已使用以下PowerShell+ffmpeg命令将25fps、1920x1080的WMV视频转为无损BMP帧,文件名格式为\d{5}.bmp:

gci C:\Users\Xeni\Documents -filter *.wmv | %{
 $folder = 'C:\Users\Xeni\Desktop\' + $($_.name -replace '.wmv')
 md $folder;
 D:\ffmpeg\ffmpeg.exe -hwaccel cuda -i $_.fullname -vf fps=25 "$folder\%05d.bmp"
}

现有实现

基于OpenCV编写了基础的帧切换工具,支持左右箭头切换、ESC退出,但缺少全屏显示与时间轴定位功能,代码如下:

import cv2
import re
import sys
from pathlib import Path

def slideshow(folder):
    images = [str(file) for file in Path(folder).iterdir() if re.match('\d{5}.bmp', file.name)]
    total = len(images)
    index = 0
    key = -1
    while key != 27:
        img = cv2.imread(images[index])
        cv2.imshow('', img)
        key = cv2.waitKeyEx(0)
        if key == 2555904:
            index += 1
        elif key == 2424832:
            index -= 1
        index %= total
        del img
    cv2.destroyAllWindows()

if __name__ == '__main__':
    slideshow(sys.argv[1])

需求

  • 添加时间轴,对应帧的时间为文件名数值/25秒
  • 按需加载图片(内存仅16GiB,无法预加载数十GB帧),仅加载当前显示帧,切换后及时释放内存
  • 支持通过时间轴手动定位帧
  • 支持全屏显示

解决方案

结合tkinter实现带时间轴的GUI界面,同时满足所有需求:

import cv2
import re
import sys
import tkinter as tk
from pathlib import Path
from PIL import Image, ImageTk

class FrameViewer:
    def __init__(self, folder):
        self.folder = Path(folder)
        # 按帧编号排序,确保播放顺序正确
        self.frame_files = sorted(
            [f for f in self.folder.iterdir() if re.match(r'\d{5}\.bmp', f.name)],
            key=lambda x: int(x.stem)
        )
        self.total_frames = len(self.frame_files)
        if self.total_frames == 0:
            raise ValueError("文件夹中无符合格式的BMP帧文件")
        
        self.current_index = 0
        self.is_fullscreen = False
        
        # 初始化tkinter窗口
        self.root = tk.Tk()
        self.root.title("帧查看器")
        self.root.bind("<Escape>", self.exit_app)
        self.root.bind("<Left>", lambda e: self.change_frame(-1))
        self.root.bind("<Right>", lambda e: self.change_frame(1))
        self.root.bind("<F11>", self.toggle_fullscreen)
        
        # 图像显示画布
        self.canvas = tk.Canvas(self.root)
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        # 时间轴滑块
        self.timeline = tk.Scale(
            self.root,
            from_=0,
            to=self.total_frames - 1,
            orient=tk.HORIZONTAL,
            length=800,
            command=self.jump_to_frame
        )
        self.timeline.pack(fill=tk.X, padx=10, pady=5)
        
        # 时间信息标签
        self.time_label = tk.Label(self.root, text="")
        self.time_label.pack(pady=2)
        
        # 加载初始帧
        self.load_and_display_frame()

    def load_and_display_frame(self):
        # 主动释放前一帧资源
        if hasattr(self, 'tk_img'):
            self.tk_img = None
        
        # 加载当前帧
        frame_path = self.frame_files[self.current_index]
        img = cv2.imread(str(frame_path))
        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        pil_img = Image.fromarray(img_rgb)
        
        # 适配显示尺寸
        if self.is_fullscreen:
            screen_w, screen_h = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
            pil_img = pil_img.resize((screen_w, screen_h), Image.Resampling.LANCZOS)
        else:
            self.root.geometry(f"{pil_img.width}x{pil_img.height + 80}")
        
        # 转tkinter可用格式并显示
        self.tk_img = ImageTk.PhotoImage(pil_img)
        self.canvas.delete("all")
        self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_img)
        
        # 更新时间轴与时间标签
        self.timeline.set(self.current_index)
        frame_num = int(frame_path.stem)
        total_sec = frame_num / 25
        mins = int(total_sec // 60)
        secs = total_sec % 60
        self.time_label.config(text=f"帧编号: {frame_num:05d} | 时间: {mins:02d}:{secs:06.3f}")
        
        # 释放临时图像资源
        del img, img_rgb, pil_img

    def change_frame(self, delta):
        self.current_index += delta
        self.current_index = max(0, min(self.current_index, self.total_frames - 1))
        self.load_and_display_frame()

    def jump_to_frame(self, value):
        self.current_index = int(value)
        self.load_and_display_frame()

    def toggle_fullscreen(self, event):
        self.is_fullscreen = not self.is_fullscreen
        self.root.attributes("-fullscreen", self.is_fullscreen)
        self.load_and_display_frame()

    def exit_app(self, event):
        self.root.destroy()

    def run(self):
        self.root.mainloop()

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("用法: python frame_viewer.py <帧文件夹路径>")
        sys.exit(1)
    try:
        viewer = FrameViewer(sys.argv[1])
        viewer.run()
    except Exception as e:
        print(f"错误: {e}")
        sys.exit(1)

功能说明

  • 按需加载:每次仅加载当前显示的帧,切换后主动释放临时资源,避免内存占用过高
  • 时间轴定位:拖动底部滑块可直接跳转到对应帧,滑块值与帧索引一一对应
  • 时间显示:实时展示当前帧的编号和对应时间(按帧编号/25秒计算,格式为MM:SS.sss)
  • 全屏切换:按F11切换全屏模式,自动适配屏幕分辨率
  • 快捷键支持:
    • 左/右箭头:切换上一帧/下一帧
    • ESC:退出程序
    • F11:切换全屏

依赖安装

需要安装额外依赖包:

pip install opencv-python pillow

内容的提问来源于stack exchange,提问作者Ξένη Γήινος

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 13:52:12