一个欲儿的博客

一个欲儿的博客

OpenCV基于YOLOv4实现的目标检测与视频录制
2025-03-30

项目已托管到github

github.com/Anyuersuper/CameraDetect


可以检测人脸,车辆,行人,讲道理来说,凡是yolov4可支持的检测你都可以检测出来,自己加一个标签就好了


项目结构:

CameraDetect/
├── main.py              # 主程序入口
├── VideoRecorder.py     # 视频录制模块
├── VideoPlayer.py       # 视频播放模块
├── VideoBrowser.py      # 视频浏览模块
├── Yolo4Detect.py       # YOLOv4 目标检测模块
├── videos/              # 录制的视频存储目录
└── yolo/                # YOLOv4 模型文件目录

主界面:

19f6aedfa3369d53a026bd73a3f2929f.png

目标检测效果:

e4a46f902df4e2f098a89e966ea1f048.png


main.py

import cv2
import time
import os
import subprocess  
import numpy as np

from Yolo4Detect import Yolo4Detect
from VideoBrowser import VideoBrowser
from VideoRecorder import VideoRecorder
from VideoPlayer import VideoPlayer

import tkinter as tk
from tkinter import messagebox, filedialog ,simpledialog
from threading import Thread


# 创建 Tkinter 窗口
class Application(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title("目标检测与视频录制")
        self.geometry("300x350")
        
        # 创建 VideoRecorder 实例
        self.recorder = VideoRecorder(self)

        # 存储路径变量
        self.storage_path = self.recorder.video_dir  # 直接从 recorder 获取 video_dir
        self.timepoint = self.recorder.timepoint
        
        # 开始录制按钮
        self.start_button = tk.Button(self, text="开始检测", command=self.start_recording)
        self.start_button.pack(pady=10)

        # 停止录制按钮
        self.stop_button = tk.Button(self, text="停止检测", command=self.stop_recording)
        self.stop_button.pack(pady=10)
        self.stop_button.config(state=tk.DISABLED)

        # 查看本地视频按钮
        self.view_button = tk.Button(self, text="查看本地视频", command=self.view_local_videos)
        self.view_button.pack(pady=10)
        
        # 绑定关闭窗口事件
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

        # 时间间隔设置
        self.timepointgrid = tk.Frame(self, width=300 ,height= 100)
        self.timepointgrid.pack(pady=10)  
        tk.Label(self.timepointgrid, text="存储间隔(s):").grid(row=0, column=0)
        self.showtimepoint = tk.Label(self.timepointgrid, width=2,text=self.timepoint, justify="right")  
        self.showtimepoint.grid(row=0, column=1)
        self.changetimepoint = tk.Button(self.timepointgrid, text="...", width=3, command = self.change_timepoint)
        self.changetimepoint.grid(row=0, column=2, padx=5)
        
        # 存储路径布局
        self.savapathgrid = tk.Frame(self, width=300)
        self.savapathgrid.pack(pady=10)
        tk.Label(self.savapathgrid, text="存储路径:").grid(row=0, column=0)
        self.path_label = tk.Label(self.savapathgrid, width=20,text=self.storage_path, justify="right")  # 直接显示路径
        self.path_label.grid(row=0, column=1)
        # 选择路径的按钮
        self.browse_button = tk.Button(self.savapathgrid, text="...", width=3, command=self.changesavepath)
        self.browse_button.grid(row=0, column=2, padx=5)

        # 分辨率布局
        self.fenbianlvgrid = tk.Frame(self, width=300, height=100)
        self.fenbianlvgrid.pack(pady=10) 
        tk.Label(self.fenbianlvgrid, text="分辨率:").grid(row=0, column=0)
        self.p480 = tk.Button(self.fenbianlvgrid, text="480p", width=5, command=self.p480)
        self.p480.grid(row=0, column=1)
        self.p720 = tk.Button(self.fenbianlvgrid, text="720p", width=5, command=self.p720)
        self.p720.grid(row=0, column=2)
        self.p1080 = tk.Button(self.fenbianlvgrid, text="1080p", width=5, command=self.p1080)
        self.p1080.grid(row=0, column=3)
        
        # 帧率控制布局
        self.zhenlvgrid = tk.Frame(self, width=300 ,height= 100)
        self.zhenlvgrid.pack(pady=10)  
        # 帧率切换
        tk.Label(self.zhenlvgrid, text="帧率:").grid(row=0, column=0)
        self.zhen1 = tk.Button(self.zhenlvgrid, text="1", width=5, command=self.fps1)
        self.zhen1.grid(row=0, column=1)
        self.zhen5 = tk.Button(self.zhenlvgrid, text="5", width=5, command=self.fps5)
        self.zhen5.grid(row=0, column=2)
        self.zhen15 = tk.Button(self.zhenlvgrid, text="15", width=5, command=self.fps15)
        self.zhen15.grid(row=0, column=3)
        self.zhen30 = tk.Button(self.zhenlvgrid, text="30", width=5, command=self.fps30)
        self.zhen30.grid(row=0, column=4)
        self.zhen60 = tk.Button(self.zhenlvgrid, text="60", width=5, command=self.fps60)
        self.zhen60.grid(row=0, column=5)


    # Application 类中的分辨率按钮回调函数
    def p480(self):
        self.recorder.setfenbianlv_async(640, 480)  # 异步切换分辨率
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)

    def p720(self):
        self.recorder.setfenbianlv_async(1280, 720)  # 异步切换分辨率
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)

    def p1080(self):
        self.recorder.setfenbianlv_async(1920, 1080)  # 异步切换分辨率
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)

    def fps1(self):
        self.recorder.setfps(1)
    def fps5(self):
        self.recorder.setfps(5)
    def fps15(self):
        self.recorder.setfps(15)
    def fps30(self):
        self.recorder.setfps(30)
    def fps60(self):
        self.recorder.setfps(60)
    
    def changesavepath(self):
        """更新存储路径"""
        # 打开文件对话框选择新路径
        new_path = filedialog.askdirectory(initialdir=self.storage_path, title="选择存储路径")

        if new_path:
            # 更新 VideoRecorder 的 video_dir 属性
            self.recorder.set_video_dir(new_path)

            # 更新界面显示的路径
            self.storage_path = new_path
            self.path_label.config(text=self.storage_path)

            # 读取并修改 config.info 文件中的路径
            with open("config.info", "r") as config_file:
                lines = config_file.readlines()

            # 找到并修改 path 行
            with open("config.info", "w") as config_file:
                for line in lines:
                    if line.startswith('path='):
                        config_file.write(f'path={new_path}\n')
                    else:
                        config_file.write(line)
                        
    def change_timepoint(self):
        # 弹出输入框让用户输入新的时间间隔
        new_timepoint = simpledialog.askinteger("输入新的时间间隔", "请输入新的时间间隔(秒):", parent=self)
        if new_timepoint:
            # 更新 VideoRecorder 的 video_dir 属性
            self.recorder.settimepoint(new_timepoint)

            # 更新界面显示的路径
            self.timepoint = new_timepoint
            self.showtimepoint.config(text=self.timepoint)

            # 读取并修改 config.info 文件中的路径
            with open("config.info", "r") as config_file:
                lines = config_file.readlines()

            # 找到并修改 path 行
            with open("config.info", "w") as config_file:
                for line in lines:
                    if line.startswith('timepoint='):
                        config_file.write(f'timepoint={new_timepoint}\n')
                    else:
                        config_file.write(line)
        

    
    def start_recording(self):
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)
        self.recorder.start_recording()

    def stop_recording(self):
        self.start_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        self.recorder.stop_recording()

    def view_local_videos(self):
        """打开本地视频浏览器"""
        VideoBrowser(self.recorder.video_dir)
        
    def on_closing(self):
        """当点击窗口右上角关闭按钮时释放摄像头资源"""
        self.recorder.cap.release()  # 释放摄像头
        self.destroy()  # 关闭窗口

# 启动 Tkinter 应用
if __name__ == "__main__":
    app = Application()
    app.mainloop()


Yolo4Detect.py

#Yolo4检测类
import cv2
import os
import numpy as np


class Yolo4Detect:
    def __init__(self):
        self.yolo_cfg = "yolo/yolov4.cfg"
        self.yolo_weights = "yolo/yolov4.weights"
        self.yolo_classes = "yolo/coco.names"
        
        # 读取类别名称
        with open(self.yolo_classes, "r") as f:
            self.class_names = f.read().strip().split("\n")
        
        # 只检测车辆相关类别(COCO 数据集 ID)
        self.vehicle_classes = ["car", "bus", "truck", "motorbike","person"]
        
        # 加载 YOLO
        self.net = cv2.dnn.readNet(self.yolo_weights, self.yolo_cfg)
        self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)  # 使用 OpenCV DNN 加速
        self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)  # 运行在 CPU(可修改为 DNN_TARGET_CUDA 以使用 GPU)
        
        # 获取 YOLO 的输出层
        layer_names = self.net.getLayerNames()
        self.output_layers = [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers()]
    
    def detectbyyolo4(self, image, conf_threshold=0.5, nms_threshold=0.4, show_result=False, save_result=False, save_path="detected_image.jpg"):
        """
        进行车辆检测
        :param image: 可以是图片路径(str) 或 OpenCV 读取的 numpy 数组
        """
        # 如果传入的是路径,则读取图片
        if isinstance(image, str):
            image = cv2.imread(image)
            if image is None:
                print(f"无法读取图片: {image_path}")
                return None

        height, width = image.shape[:2]

        # YOLO 需要 416x416 或 608x608 输入
        blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (416, 416), swapRB=True, crop=False)
        self.net.setInput(blob)

        # 前向传播
        outputs = self.net.forward(self.output_layers)

        # 解析检测结果
        boxes, confidences, class_ids = [], [], []

        for output in outputs:
            for detection in output:
                scores = detection[5:]
                class_id = np.argmax(scores)
                confidence = scores[class_id]

                # 只检测车辆类别,并设定置信度阈值
                if self.class_names[class_id] in self.vehicle_classes and confidence > conf_threshold:
                    center_x, center_y, w, h = (detection[:4] * [width, height, width, height]).astype("int")

                    # 计算矩形框的左上角坐标
                    x = int(center_x - w / 2)
                    y = int(center_y - h / 2)

                    boxes.append([x, y, w, h])
                    confidences.append(float(confidence))
                    class_ids.append(class_id)

        # NMS 去重
        indices = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)
        if len(indices) > 0:
            for i in indices.flatten():
                x, y, w, h = boxes[i]
                label = f"{self.class_names[class_ids[i]]}: {confidences[i]:.2f}"
                cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
                cv2.putText(image, label, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
        else:
            yuer = 1
            #print("没有检测到任何目标!")

        # 显示或保存检测结果
        if show_result:
            cv2.imshow("Vehicle Detection", image)
            cv2.waitKey(0)
            cv2.destroyAllWindows()

        if save_result:
            cv2.imwrite(save_path, image)
            print(f"检测结果已保存到 {save_path}")

        return image

    
if __name__ == "__main__": 
    yolodetector = Yolo4Detect()
    yolodetector.detectbyyolo4(r"test/CarandPerson.png",show_result=True)


VideoRecorder.py

#视频录制类
import os
import cv2
import time
import threading

from Yolo4Detect import Yolo4Detect

import tkinter as tk
from tkinter import messagebox, filedialog

from threading import Thread


class VideoRecorder(tk.Toplevel):
    def __init__(self, root):
        self.deviceid = 0
        self.root = root
        self.width = 640
        self.height = 480
        self.cap = cv2.VideoCapture(self.deviceid)
        
        # 从 config.info 文件中读取路径配置
        self.load_config()
        
        # 设置视频捕获的宽高
        self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
        self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
        
        self.fps = 30
        self.fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        self.out = None
        self.recording = False
        self.start_time = None

        # 如果没有视频存储目录,则创建
        if not os.path.exists(self.video_dir):
            os.makedirs(self.video_dir)

    def settimepoint(self,times):
        self.timepoint= times

    def load_config(self):
        """读取 config.info 文件并设置 video_dir 和 frontalfacepath"""
        try:
            with open('config.info', 'r') as file:
                for line in file:
                    # 查找配置项 path
                    if line.startswith("path="):
                        self.video_dir = line.strip().split('=')[1]
                    # 查找配置项 frontalfacepath
                    elif line.startswith("frontalface="):
                        self.frontalfacepath = line.strip().split('=')[1]
                    elif line.startswith("timepoint="):
                        self.timepoint = line.strip().split('=')[1]
        except Exception as e:
            print(f"读取 config.info 时发生错误: {e}")
            # 如果读取配置文件出错,使用默认目录和默认人脸分类器路径
            self.video_dir = 'videos'
            self.frontalface = "cvxml/haarcascade_frontalface_default.xml"
            self.timepoint = 60

        
    def set_video_dir(self,new_path):
        self.video_dir = new_path
    def setfps(self, fps):
        self.fps = fps

    def getmyinfo(self):
        print("video_dir:", self.video_dir)

    def cv2init(self):
        """重新初始化摄像头"""
        self.cap.release()
        self.cap = cv2.VideoCapture(self.deviceid)
        self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
        self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)

    def start_recording(self):
        """开始录制视频"""
        if not self.cap.isOpened():
            self.cv2init()
        self.thread = Thread(target=self.record_video)  # 启动录制线程
        self.thread.start()

    def stop_recording(self):
        """停止录制视频"""
        self.recording = False
        if self.out:
            self.out.release()
            self.thread.join()  # 等待线程关闭结束
        messagebox.showinfo("停止录制", "视频已保存。")

    def record_video(self):
        """视频录制线程函数"""
        self.recording = True
        self.start_time = time.time()
        video_path = os.path.join(self.video_dir, f"video_" + str(time.time()) + ".mp4")  # 生成视频文件名
        self.out = cv2.VideoWriter(video_path, self.fourcc, self.fps, (self.width, self.height))
        
        print(self.frontalfacepath)
        face_cascade = cv2.CascadeClassifier("cvxml/haarcascade_frontalface_default.xml")  # 加载 opencv-Haar 分类器
        yolodetector = Yolo4Detect()  # 初始化 yolov4 判断器
        
        while self.recording:
            ret, frame = self.cap.read()
            if not ret:
                print("无法接收视频帧,退出")
                break
            
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 转为灰度图像进行人脸检测
            faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
            
            for (x, y, w, h) in faces:
                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
            
            frame = yolodetector.detectbyyolo4(frame)  # yolo4再检测一轮
            
            # 每 X 秒切换一个新文件
            if time.time() - self.start_time >= int(self.timepoint):
                if self.out:
                    self.out.release()
                video_path = os.path.join(self.video_dir, f"video_" + str(time.time()) + ".mp4")
                self.out = cv2.VideoWriter(video_path, self.fourcc, self.fps, (self.width, self.height))
                self.start_time = time.time()
            
            self.out.write(frame)  # 记录视频
            cv2.imshow('Camera', frame)  # 显示视频帧
            cv2.waitKey(int(1000 / self.fps))
        
        cv2.destroyAllWindows()

    def setfenbianlv(self, width, height):
        """设置分辨率"""
        if self.recording:
            # 停止当前录制
            self.recording = False
            if self.out:
                self.out.release()
                #self.cap.release()
                self.thread.join()  # 等待线程关闭结束

            self.width = width
            self.height = height
            self.cv2init()
            # 检查是否支持该分辨率
            actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
            actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
            
            if actual_width == width and actual_height == height:
                # 使用 after() 将消息框更新操作放到主线程
                self.root.after(100, self.show_resolution_message, "分辨率兼容", "切换成功!")
                #self.cv2init()  # 重新初始化摄像头
                self.thread = Thread(target=self.record_video)
                self.thread.start()
            else:
                self.root.after(100, self.show_resolution_message, "分辨率不兼容", "已切换回默认分辨率")
                self.width = 640
                self.height = 480
                self.cv2init()  # 切换回默认分辨率
                self.thread = Thread(target=self.record_video)
                self.thread.start()
        else:
            self.recording = False
            if self.out:
                self.out.release()
                self.cap.release()
                self.thread.join()
            self.width = width
            self.height = height
            self.cv2init()  # 重新初始化摄像头
            actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
            actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
            # 检查摄像头是否支持该分辨率
            if actual_width == width and actual_height == height:
                self.root.after(100, self.show_resolution_message, "分辨率兼容", "切换成功!")
                self.thread = Thread(target=self.record_video)
                self.thread.start()
            else:
                self.root.after(100, self.show_resolution_message, "分辨率不兼容", "切换回默认分辨率。")
                self.width = 640
                self.height = 480
                self.cv2init()  # 切换回默认分辨率
            


    def setfenbianlv_async(self, width, height):
        """异步设置分辨率"""
        threading.Thread(target=self.setfenbianlv, args=(width, height)).start()

    def show_resolution_message(self, title, message):
        """显示分辨率切换的提示信息"""
        messagebox.showinfo(title, message)


if __name__ == "__main__":
    root = tk.Tk()
    recorder = VideoRecorder(root)  # 创建 VideoRecorder 实例
    recorder.getmyinfo()
    root.mainloop()


VideoBrowser.py

#浏览本地视频类
import os
import subprocess
import tkinter as tk
from tkinter import messagebox, filedialog
from VideoPlayer import VideoPlayer


class VideoBrowser(tk.Toplevel):
    def __init__(self, video_dir):
        super().__init__()

        self.title("本地视频浏览器")
        self.geometry("400x300")
        self.video_dir = video_dir

        # 列表框
        self.listbox = tk.Listbox(self, width=50, height=15)
        self.listbox.pack(pady=10)

        # 绑定双击事件
        self.listbox.bind("<Double-Button-1>", self.open_selected_file)

        # 加载视频列表
        self.load_videos()

    def load_videos(self):
        """加载 videos 目录下的所有 .mp4 文件"""
        if not os.path.isdir(self.video_dir):
            messagebox.showerror("错误", "视频目录不存在!")
            return
        
        mp4_files = [f for f in os.listdir(self.video_dir) if f.endswith('.mp4')]
        self.listbox.delete(0, tk.END)  # 清空列表
        for file in mp4_files:
            self.listbox.insert(tk.END, file)

    def open_selected_file(self, event):
        """双击打开选中的 MP4 文件"""
        selected_index = self.listbox.curselection()
        if selected_index:
            selected_file = self.listbox.get(selected_index)  # 获取选中的文件名
            file_path = os.path.join(self.video_dir, selected_file)  # 构造完整路径
            vp = VideoPlayer(file_path)
        else:
            messagebox.showerror("错误", "文件不存在!")

# 确保 main 函数启动
if __name__ == "__main__":
    # 指定视频目录路径
    video_dir = "videos"  # 修改为实际视频文件夹路径
    root = tk.Tk()  # 创建主窗口
    video_browser = VideoBrowser(video_dir)  # 创建 VideoBrowser 实例
    root.mainloop()  # 启动主事件循环


VideoPlayer.py

#视频播放类
import cv2
import tkinter as tk
from PIL import Image, ImageTk

class VideoPlayer(tk.Toplevel):
    def __init__(self, video_source):
        super().__init__()

        self.title("查看视频")
        self.state("zoomed")
        # 视频源
        self.video_source = video_source
        self.vid = cv2.VideoCapture(video_source)

        if not self.vid.isOpened():
            print("无法打开视频文件:", video_source)
            self.destroy()
            return

        # 获取视频的基本信息
        self.frame_count = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
        self.fps = int(self.vid.get(cv2.CAP_PROP_FPS))
        self.original_width = int(self.vid.get(cv2.CAP_PROP_FRAME_WIDTH))
        self.original_height = int(self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT))

        # 默认分辨率(初始化时设为原始分辨率)
        self.width = self.original_width
        self.height = self.original_height

        # 获取屏幕的最大尺寸
        self.max_width = self.winfo_screenwidth()
        self.max_height = self.winfo_screenheight() - 150

        # 限制视频画布的大小
        self.width = min(self.width, self.max_width)
        self.height = min(self.height, self.max_height)

        # 创建 Tkinter 画布
        self.canvas = tk.Canvas(self, width=self.width, height=self.height)
        self.canvas.pack()

        # 创建控制按钮
        self.create_controls()

        # 进度条
        self.progress = tk.Scale(self, from_=0, to=self.frame_count, orient=tk.HORIZONTAL, length=500, command=self.set_position)
        self.progress.pack(fill=tk.X)

        # 绑定窗口关闭事件
        self.protocol("WM_DELETE_WINDOW", self.on_close)

        # 开始播放
        self.update()

    def create_controls(self):
        """ 创建播放控制按钮 """
        control_frame = tk.Frame(self)
        control_frame.pack()

        # 帧率切换
        tk.Label(control_frame, text="选择帧率:").pack(side=tk.LEFT)
        for fps in [1,5, 10, 15, 30, 60]:
            tk.Button(control_frame, text=f"{fps}帧", width=5, command=lambda f=fps: self.set_fps(f)).pack(side=tk.LEFT)

        # 快进 & 回退
        tk.Button(control_frame, text="回退5秒", command=self.jump_backward).pack(side=tk.LEFT, padx=5)
        tk.Button(control_frame, text="快进5秒", command=self.jump_forward).pack(side=tk.LEFT, padx=5)

        # 播放/暂停
        self.pause_button = tk.Button(control_frame, text="⏸ 暂停", command=self.toggle_pause)
        self.pause_button.pack(side=tk.LEFT, padx=5)

        # 分辨率切换
        tk.Label(control_frame, text="分辨率:").pack(side=tk.LEFT)
        tk.Button(control_frame, text="480p", command=lambda: self.set_resolution(854, 480)).pack(side=tk.LEFT)
        tk.Button(control_frame, text="720p", command=lambda: self.set_resolution(1280, 720)).pack(side=tk.LEFT)
        tk.Button(control_frame, text="1080p", command=lambda: self.set_resolution(1920, 1080)).pack(side=tk.LEFT)
        tk.Button(control_frame, text="原始", command=lambda: self.set_resolution(self.original_width, self.original_height)).pack(side=tk.LEFT)

    def update(self):
        """ 更新视频帧 """
        if not hasattr(self, "is_paused") or not self.is_paused:
            ret, frame = self.vid.read()
            if ret:
                self.current_frame = int(self.vid.get(cv2.CAP_PROP_POS_FRAMES))
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

                # 根据当前分辨率调整缩放比例
                frame = cv2.resize(frame, (self.width, self.height))
                img = Image.fromarray(frame)
                imgtk = ImageTk.PhotoImage(image=img)
                self.canvas.create_image(0, 0, anchor=tk.NW, image=imgtk)
                self.canvas.imgtk = imgtk

                # 更新进度条
                self.progress.set(self.current_frame)

        self.after(int(1000 / self.fps), self.update)

    def set_fps(self, fps):
        """ 设置帧率 """
        self.fps = fps

    def set_position(self, val):
        """ 拖动进度条时调整视频位置 """
        frame_number = int(val)
        self.vid.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
        self.current_frame = frame_number

        # 让视频画面立即更新
        ret, frame = self.vid.read()
        if ret:
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frame = cv2.resize(frame, (self.width, self.height))
            img = Image.fromarray(frame)
            imgtk = ImageTk.PhotoImage(image=img)
            self.canvas.create_image(0, 0, anchor=tk.NW, image=imgtk)
            self.canvas.imgtk = imgtk

    def jump_forward(self):
        """ 快进 5 秒 """
        target_frame = min(self.current_frame + self.fps * 5, self.frame_count - 1)
        self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
        self.current_frame = target_frame

        # 同步进度条 & 立即更新画面
        self.progress.set(target_frame)
        self.update()

    def jump_backward(self):
        """ 回退 5 秒 """
        target_frame = max(self.current_frame - self.fps * 5, 0)
        self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
        self.current_frame = target_frame

        # 同步进度条 & 立即更新画面
        self.progress.set(target_frame)
        self.update()

    def toggle_pause(self):
        """ 播放/暂停切换 """
        self.is_paused = not getattr(self, "is_paused", False)
        if self.is_paused:
            self.pause_button.config(text="▶️ 播放")
        else:
            self.pause_button.config(text="⏸ 暂停")

    def set_resolution(self, width, height):
        """ 设置分辨率,确保不超过屏幕最大尺寸 """
        self.width, self.height = min(width, self.max_width), min(height, self.max_height)
        self.canvas.config(width=self.width, height=self.height)

    def on_close(self):
        """ 释放资源并关闭窗口 """
        if self.vid.isOpened():
            self.vid.release()
        self.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    root.geometry('200x100')
    video_path = 'test/videodemo.mp4'
    player = VideoPlayer(video_path)
    root.mainloop()


发表评论: