当前位置: 首页 > article >正文

用OpenCV写个视频播放器可还行?(C++版)

引言

提到OpenCV,大家首先想到的可能是图像处理、目标检测,但你是否想过——用OpenCV实现一个带进度条、倍速播放、暂停功能的视频播放器?本文将通过一个实战项目,带你深入掌握OpenCV的视频处理能力,并解锁以下功能:

  • 基础播放/暂停
  • 动态倍速调节(0.5x~4x)
  • 交互式进度条
  • 实时时间戳显示

文末提供完整代码,可直接运行!

一、环境准备

安装OpenCV

请参考其他博客,C++版本的OpenCV安装,每个操作系统都有自己的玩法。比如我在Ubuntu中下载了OpenCV 4.11版本的源码自行编译。可以参考我的上一篇文章

准备测试视频

准备一个MP4或AVI格式的视频文件(示例代码路径为/home/user/video.mp4,读者自行替换)。

二、核心功能实现

1. 基础播放器

#include <opencv2/opencv.hpp>

int main() {
    // 1. 创建视频捕获对象(核心对象)
    cv::VideoCapture cap("video.mp4");
    
    // 2. 主循环(核心逻辑)
    while(true) {
        cv::Mat frame;
        
        // 3. 读取帧(核心方法)
        if (!cap.read(frame)) 
            break;
        
        // 4. 显示帧(核心输出)
        cv::imshow("Video", frame);
        
        // 5. 控制逻辑(核心交互)
        int key = cv::waitKey(25); // 控制播放速度
        if(key == 27) break;       // ESC退出
    }
    
    // 6. 资源释放(核心清理)
    cap.release();
    cv::destroyAllWindows();
    return 0;
}

代码解析

  • VideoCapture:支持文件、摄像头、网络流多种输入源。
  • waitKey(25):控制播放速度(25ms对应约40 FPS)。

三、功能扩展:让播放器更强大

1. 倍速播放

通过调整waitKey的延迟时间实现变速:

#include <opencv2/opencv.hpp>

int main() {
    cv::VideoCapture cap("video.mp4");
    double fps = cap.get(cv::CAP_PROP_FPS);
    int base_delay = 1000 / fps;  // 基准延迟
    
    double speed = 1.0;  // 初始速度
    
    while(true) {
        cv::Mat frame;
        if(!cap.read(frame)) break;
        
        cv::imshow("Video", frame);
        
        // 核心控制逻辑
        int delay = static_cast<int>(base_delay / speed);
        int key = cv::waitKey(std::max(1, delay)); 
        
        if(key == 27) break;        // ESC退出
        else if(key == '+') speed = std::min(4.0, speed + 0.5); // 加速
        else if(key == '-') speed = std::max(0.5, speed - 0.5); // 减速
    }
    
    cap.release();
    cv::destroyAllWindows();
    return 0;
}

按+加速,按-减速,速度范围限制在0.5x~4x。

2. 进度条与跳转

利用OpenCV的滑动条控件实现交互:

#include <opencv2/opencv.hpp>

using namespace cv;

// 全局变量
VideoCapture cap;
int currentFrame = 0;
int totalFrames = 0;

// 进度条回调函数
void onTrackbar(int pos, void* userdata) {
    cap.set(CAP_PROP_POS_FRAMES, pos);
    currentFrame = pos;
}

int main() {
    cap.open("video.mp4");
    totalFrames = cap.get(CAP_PROP_FRAME_COUNT);
    
    namedWindow("Video Player");
    createTrackbar("Progress", "Video Player", &currentFrame, totalFrames, onTrackbar);

    Mat frame;
    while(true) {
        cap.read(frame);
        if(frame.empty()) break;
        
        // 更新当前帧位置
        currentFrame = cap.get(CAP_PROP_POS_FRAMES);
        setTrackbarPos("Progress", "Video Player", currentFrame);
        
        imshow("Video Player", frame);
        
        if(waitKey(30) == 27) break; // ESC退出
    }
    
    cap.release();
    destroyAllWindows();
    return 0;
}

3. 实时信息叠加

在视频帧上绘制进度条和时间戳:

    void drawOverlay(Mat& frame) {
        int videoWidth = frame.cols;
        float progressRatio = static_cast<float>(currentFrame) / totalFrames;
        int progressWidth = static_cast<int>(videoWidth * progressRatio);

        rectangle(frame, Point(0, 10), Point(progressWidth, 30), Scalar(0, 255, 0), FILLED);

        double currentTime = currentFrame / fps;
        ostringstream timeText;
        timeText << "Time: " << fixed << setprecision(2) << currentTime << "s";
        putText(frame, timeText.str(), Point(10, 60), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);

        ostringstream speedText;
        speedText << "Speed: " << fixed << setprecision(1) << speed << "x";
        putText(frame, speedText.str(), Point(10, 100), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
    }

四、完整代码

注意,完整代码中头文件是分开引入的。只引入了opencv两个模块:highgui和imgproc。并优化了快进时CPU占用100%的问题,当然,也将程序引入了复杂的困境。

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <iomanip>
#include <chrono>
#include <string>
#include <sstream>

using namespace cv;
using namespace std;
using namespace chrono;

class VideoPlayer {
private:
    VideoCapture cap;
    int totalFrames;
    double fps;
    int baseDelay;
    bool pause;
    int currentFrame;
    double speed;
    string windowName;
    high_resolution_clock::time_point lastFrameTime;

    static void onTrackbar(int pos, void* userdata) {
        VideoPlayer* self = static_cast<VideoPlayer*>(userdata);
        if (self->currentFrame != pos) {
            self->cap.set(CAP_PROP_POS_FRAMES, pos);
            self->currentFrame = pos;
        }
    }

    void drawOverlay(Mat& frame) {
        int videoWidth = frame.cols;
        float progressRatio = static_cast<float>(currentFrame) / totalFrames;
        int progressWidth = static_cast<int>(videoWidth * progressRatio);

        rectangle(frame, Point(0, 10), Point(progressWidth, 30), Scalar(0, 255, 0), FILLED);

        double currentTime = currentFrame / fps;
        ostringstream timeText;
        timeText << "Time: " << fixed << setprecision(2) << currentTime << "s";
        putText(frame, timeText.str(), Point(10, 60), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);

        ostringstream speedText;
        speedText << "Speed: " << fixed << setprecision(1) << speed << "x";
        putText(frame, speedText.str(), Point(10, 100), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
    }

public:
    VideoPlayer(const string& videoPath) :
        windowName("OpenCV Video Player"),
        pause(false),
        currentFrame(0),
        speed(1.0) {

        cap.open(videoPath);
        if (!cap.isOpened()) {
            throw runtime_error("无法打开视频文件");
        }

        totalFrames = static_cast<int>(cap.get(CAP_PROP_FRAME_COUNT));
        if (totalFrames <= 0) {
            throw runtime_error("无效的视频文件");
        }

        fps = cap.get(CAP_PROP_FPS);
        baseDelay = static_cast<int>(1000.0 / fps);

        namedWindow(windowName, WINDOW_AUTOSIZE);
        createTrackbar("Progress", windowName, nullptr, totalFrames, onTrackbar, this);
        setTrackbarPos("Progress", windowName, 0);
        lastFrameTime = high_resolution_clock::now();
    }

    void run() {
        Mat frame;
        int64 frameCounter = 0;
        double accumulatedDelay = 0;

        while (true) {
            auto startTime = high_resolution_clock::now();

            if (!pause) {
                // 动态帧处理控制
                double speedFactor = max(0.5, min(4.0, speed));
                int framesToProcess = static_cast<int>(speedFactor);
                int actuallyProcessed = 0;

                for (int i = 0; i < framesToProcess; ++i) {
                    bool ret = cap.read(frame);
                    if (!ret) {
                        cap.set(CAP_PROP_POS_FRAMES, 0);
                        currentFrame = 0;
                        setTrackbarPos("Progress", windowName, currentFrame);
                        break;
                    }
                    currentFrame = static_cast<int>(cap.get(CAP_PROP_POS_FRAMES));
                    actuallyProcessed++;
                }

                if (actuallyProcessed > 0) {
                    // 更新进度条(每5帧更新一次以降低开销)
                    if (frameCounter++ % 5 == 0) {
                        setTrackbarPos("Progress", windowName, currentFrame);
                    }

                    drawOverlay(frame);
                    imshow(windowName, frame);
                }
            }

            // 智能延迟控制
            auto processTime = duration_cast<milliseconds>(high_resolution_clock::now() - startTime).count();
            int targetDelay = static_cast<int>((baseDelay / speed) - processTime);
            int actualDelay = max(1, targetDelay);

            int key = waitKeyEx(actualDelay);
            handleKeyInput(key);

            // 自动跳帧机制(当处理落后时)
            if (targetDelay < 0 && speed > 1.0) {
                int skipFrames = static_cast<int>(-targetDelay / baseDelay * speed);
                currentFrame = min(currentFrame + skipFrames, totalFrames - 1);
                cap.set(CAP_PROP_POS_FRAMES, currentFrame);
            }
        }
    }

private:
    void handleKeyInput(int key) {
        switch (key) {
        case 27: // ESC
            cap.release();
            destroyAllWindows();
            exit(0);
        case 32: // Space
            pause = !pause;
            break;
        case '+':
            speed = min(4.0, speed + 0.5);
            cout << "Speed: " << speed << "x" << endl;
            break;
        case '-':
            speed = max(0.5, speed - 0.5);
            cout << "Speed: " << speed << "x" << endl;
            break;
        }
    }
};

int main() {
    try {
        VideoPlayer player("C:\\Users\\lebro\\Videos\\video_test.mp4");
        player.run();
    }
    catch (const exception& e) {
        cerr << "Error: " << e.what() << endl;
        return -1;
    }
    return 0;
}

五、后记

请参考《学习OpenCV3》,第二章 OpenCV初探中的第二个程序:视频。


http://www.kler.cn/a/578703.html

相关文章:

  • 靶场(四)---小白心得全流程分析
  • AIP-162 资源修订
  • Docker(认识且会基础操作)
  • yolov5训练自己数据集的全流程+踩过的坑
  • Linux 入门:常用命令速查手册
  • 【 <一> 炼丹初探:JavaWeb 的起源与基础】之 JSP 标签库:自定义标签的开发与应用
  • 2025开源SCA工具推荐 | 组件依赖包安全风险检测利器
  • 探索在直播中的面部吸引力预测新的基准和多模态方法
  • 服务远程调用(RPC)架构及原理
  • 欢乐力扣:汇总区间
  • QwQ-32B 开源!本地部署+微调教程来了
  • 文心一言:中国大模型时代的破局者与探路者
  • STM32中输入/输出有无默认电平
  • Vue3中computed计算属性的高级玩法
  • Vue3基础之响应式原理
  • 【java】StringJoiner
  • MyBatis-Plus分页控件使用及使用过程发现的一个坑
  • 【形态学操作中的开运算和闭运算详细讲解】
  • 系统架构设计师—系统架构设计篇—特定领域软件体系结构
  • 为AI聊天工具添加一个知识系统 之141 设计重审 之6 文章学 之 引言 之0 总括生命的形式:意识形态 诗和逻辑