OpenCV C/C++ 视频播放器 (支持调速和进度控制)

发布于:2025-06-05 ⋅ 阅读:(31) ⋅ 点赞:(0)

OpenCV C/C++ 视频播放器 (支持调速和进度控制)

本文将引导你使用 C++ 和 OpenCV 库创建一个功能稍复杂的视频播放器。该播放器不仅能播放视频,还允许用户通过滑动条来调整播放速度(加速/减速)以及控制视频的播放进度。
使用opencv打开不会压缩画质,你看起来的效果和其他播放器打开的不一样,会觉得很高清
请添加图片描述

目录

  1. 功能简介
  2. 先决条件
  3. 核心组件
  4. 代码实现
  5. 编译和运行
  6. 使用说明
  7. 代码解释
  8. 可能的改进
  9. 总结

功能简介

  • 播放本地视频文件。
  • 通过滑动条调整播放速度(例如,0.1x 到 4.0x)。
  • 通过滑动条跳转到视频的任意位置。
  • 显示当前播放进度。
  • 支持暂停和继续播放。

先决条件

  • C++ 编译器: 如 GCC (MinGW for Windows), Clang, 或 MSVC。
  • OpenCV 库: 版本 3.x 或 4.x 已安装并正确配置。
  • 基本的 C++ 知识: 函数、循环、变量、指针等。
  • 基本的 OpenCV 知识: cv::VideoCapture, cv::Mat, cv::imshow, cv::createTrackbar, cv::waitKey

核心组件

  1. cv::VideoCapture: 用于打开和读取视频帧。
  2. cv::imshow: 用于在窗口中显示视频帧。
  3. cv::createTrackbar: 用于创建速度控制和进度控制的滑动条。
  4. 回调函数: 用于响应滑动条数值的变化。
  5. 主循环: 控制视频的读取、显示、延迟和用户输入。

代码实现

我们将逐步构建代码。

全局变量与结构

为了在回调函数和主循环之间共享数据,我们会使用一些全局变量。对于更大型的应用,通常会封装在一个类中。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <algorithm> // For std::max and std::min

// --- 全局变量 ---
cv::VideoCapture cap;
std::string window_name = "OpenCV Video Player";
int g_slider_progress = 0;          // 进度条的当前位置
int g_total_frames = 0;             // 视频总帧数
bool g_user_is_dragging_progress_slider = false; // 标记用户是否正在拖动进度条

// 速度控制相关
int g_speed_trackbar_val = 100;     // 速度条的值 (例如100代表1.0x)
const int g_speed_trackbar_max = 400; // 速度条最大值 (例如400代表4.0x)
const int g_speed_trackbar_min = 10;  // 速度条最小值 (例如10代表0.1x)
double g_current_fps_multiplier = 1.0; // 当前播放速度倍率

bool g_paused = false;              // 暂停状态
cv::Mat g_current_frame_for_pause;  // 用于暂停时显示的当前帧

滑动条回调函数

我们需要两个回调函数:一个用于进度条,一个用于速度条。

// --- 进度条回调函数 ---
void on_trackbar_progress(int pos, void* userdata) {
    if (g_user_is_dragging_progress_slider && cap.isOpened()) {
        // 只有当用户实际改变滑块位置时才跳转
        // (避免程序自身更新滑块位置时触发不必要的跳转)
        if (std::abs(pos - (int)cap.get(cv::CAP_PROP_POS_FRAMES)) > 1) { // 容差,避免微小抖动
             cap.set(cv::CAP_PROP_POS_FRAMES, pos);
        }
        g_slider_progress = pos; // 确保全局变量也更新
    }
}

// --- 速度条回调函数 ---
void on_trackbar_speed(int pos, void* userdata) {
    if (pos < g_speed_trackbar_min) { // 防止速度过低或为0
        g_speed_trackbar_val = g_speed_trackbar_min;
        cv::setTrackbarPos("Speed x0.01", window_name, g_speed_trackbar_min);
    } else {
        g_speed_trackbar_val = pos;
    }
    g_current_fps_multiplier = static_cast<double>(g_speed_trackbar_val) / 100.0;
}

// OpenCV的createTrackbar在内部处理鼠标事件,
// 我们需要一个通用的鼠标回调来检测用户是否开始/停止拖动进度条
void on_mouse_event_progress(int event, int x, int y, int flags, void* userdata) {
    // 这个函数可以用来更精确地控制 g_user_is_dragging_progress_slider
    // 但OpenCV的Trackbar没有直接提供这种拖动状态,这里简化处理
    // 简单地假设,如果回调被调用且值改变,就是用户操作
    // 更稳健的方法需要自己绘制滑动条或使用更高级的UI库

    // 这里我们依赖于一个简化的逻辑:在设置进度条位置前,先设置 g_user_is_dragging_progress_slider
    // 并在 on_trackbar_progress 中检查。
    // 对于进度条,更常见的是,当用户按下鼠标左键在滑动条上时,我们设置一个标志,
    // 释放时清除标志。OpenCV 的 createTrackbar 没有直接暴露这些事件。
    // 替代方案:on_trackbar_progress 被调用时,我们认为可能是用户操作。
    // 主循环中程序更新进度条时,我们不希望 on_trackbar_progress 错误地认为用户在拖动。
    // 这就是为什么我们在主循环中更新进度条时要小心。

    // 为了简化,我们直接在 on_trackbar_progress 中处理,并接受程序更新也可能调用它。
    // 如果 pos 与当前 cap.get(cv::CAP_PROP_POS_FRAMES) 不同,则认为是用户或程序触发的有效seek。
    // 在本例中,我们将在主循环中设置一个标志,表明是程序在更新。
}

注意: 精确检测用户是否正在 拖动 OpenCV 原生滑动条是比较棘手的。createTrackbar 的回调是在值 改变后 触发的。更理想的方案是,程序更新滑动条时不应触发 cap.set

一个更可行的简化方案是在 on_trackbar_progress 中,仅当值与视频当前帧显著不同时才 cap.set,并在主循环中更新 g_slider_progresscv::setTrackbarPos

// 简化的进度条回调
void on_trackbar_progress_simplified(int pos, void* userdata) {
    if (!cap.isOpened()) return;
    // 只有当滑动条的位置与视频当前帧位置有显著差异时,才认为是用户拖动并执行跳转
    // 或者我们引入一个外部标志来区分用户拖动和程序更新
    // 这里假设回调是用户操作的结果
    cap.set(cv::CAP_PROP_POS_FRAMES, pos);
    g_slider_progress = pos; // 同步全局变量
}

主函数 main

这里是所有逻辑的汇集处。

int main(int argc, char** argv) {
    std::string video_path;
    if (argc > 1) {
        video_path = argv[1];
    } else {
        std::cout << "请输入视频文件路径: ";
        std::cin >> video_path;
    }

    if (!cap.open(video_path)) {
        std::cerr << "错误: 无法打开视频文件: " << video_path << std::endl;
        return -1;
    }

    g_total_frames = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_COUNT));
    double fps_original = cap.get(cv::CAP_PROP_FPS);

    if (fps_original <= 0) { // 防止除以0或负数
        std::cout << "警告: 无法获取视频的有效FPS,将使用默认值30 FPS。" << std::endl;
        fps_original = 30.0;
    }
    int base_delay_ms = static_cast<int>(1000.0 / fps_original);

    cv::namedWindow(window_name, cv::WINDOW_AUTOSIZE);

    // 创建进度条
    // 注意:传递 &g_slider_progress 使滑动条直接绑定到该变量
    // 当用户拖动时,g_slider_progress 会被OpenCV更新,然后回调被触发
    cv::createTrackbar("Progress", window_name, &g_slider_progress, g_total_frames > 0 ? g_total_frames - 1 : 0, on_trackbar_progress_simplified);

    // 创建速度条
    cv::createTrackbar("Speed x0.01", window_name, &g_speed_trackbar_val, g_speed_trackbar_max, on_trackbar_speed);
    cv::setTrackbarMin("Speed x0.01", window_name, g_speed_trackbar_min); // 设置最小值
    cv::setTrackbarPos("Speed x0.01", window_name, 100); // 初始速度1.0x (对应值100)
    on_trackbar_speed(100, 0); // 初始化速度倍率

    cv::Mat frame;
    int current_frame_number = 0;

    while (true) {
        if (!g_paused) {
            bool success = cap.read(frame);
            if (!success) { // 如果读取失败(视频结束或错误)
                std::cout << "视频结束或读取帧失败。" << std::endl;
                // 可以选择重置播放或退出
                cap.set(cv::CAP_PROP_POS_FRAMES, 0); // 重置到开头
                cv::setTrackbarPos("Progress", window_name, 0);
                g_slider_progress = 0;
                // continue; // 如果想循环播放
                g_paused = true; // 暂停在最后一帧或黑屏
                if(frame.empty() && !g_current_frame_for_pause.empty()){
                    frame = g_current_frame_for_pause.clone(); // 显示暂停前的最后一帧
                } else if (frame.empty()){
                     break; // 如果一开始就没帧,则退出
                }
            }
            if (!frame.empty()) {
                 g_current_frame_for_pause = frame.clone(); // 保存当前帧用于暂停
            }
        } else { // 如果是暂停状态
            if (g_current_frame_for_pause.empty() && cap.isOpened()) { 
                // 如果暂停时没有缓存帧,尝试读取一帧
                cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress); //确保位置正确
                cap.read(g_current_frame_for_pause);
            }
            // 如果仍然为空,可能视频有问题或已结束
            if(g_current_frame_for_pause.empty()){
                std::cout << "暂停时无有效帧可显示。" << std::endl;
                frame = cv::Mat::zeros(cv::Size(640,480), CV_8UC3); // 显示黑屏
            } else {
                 frame = g_current_frame_for_pause.clone(); // 使用暂停时缓存的帧
            }
        }
        
        if (frame.empty()){
            // 如果所有尝试后帧仍然为空,可能真的无法播放了
            std::cout << "错误:帧为空,无法继续播放。" << std::endl;
            break;
        }

        // 更新进度条的当前位置 (非用户拖动时)
        current_frame_number = static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES));
        if (current_frame_number != g_slider_progress) { // 避免不必要的setTrackbarPos调用
            g_slider_progress = current_frame_number;
            cv::setTrackbarPos("Progress", window_name, g_slider_progress);
        }
        
        cv::imshow(window_name, frame);

        int delay = static_cast<int>(static_cast<double>(base_delay_ms) / g_current_fps_multiplier);
        if (delay <= 0) delay = 1; // waitKey至少需要1ms

        char key = (char)cv::waitKey(delay);

        if (key == 27 || key == 'q' || key == 'Q') { // ESC 或 q/Q 退出
            break;
        } else if (key == ' ') { // 空格键 暂停/播放
            g_paused = !g_paused;
            if (!g_paused && !g_current_frame_for_pause.empty()) {
                // 从暂停状态恢复时,确保视频捕获对象的位置与滑块一致
                // 防止因暂停期间滑块被拖动而导致的播放位置不匹配
                if(std::abs(g_slider_progress - (int)cap.get(cv::CAP_PROP_POS_FRAMES)) > 1) {
                    cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress);
                }
            }
        }
    }

    cap.release();
    cv::destroyAllWindows();
    return 0;
}

完整代码

将上述所有部分(全局变量、回调函数、main 函数)合并到一个 .cpp 文件中。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <algorithm> // For std::max and std::min

// --- 全局变量 ---
cv::VideoCapture cap;
std::string window_name = "OpenCV Video Player";
int g_slider_progress = 0;          // 进度条的当前位置
int g_total_frames = 0;             // 视频总帧数

// 速度控制相关
int g_speed_trackbar_val = 100;     // 速度条的值 (例如100代表1.0x)
const int g_speed_trackbar_max = 400; // 速度条最大值 (例如400代表4.0x)
const int g_speed_trackbar_min = 10;  // 速度条最小值 (例如10代表0.1x)
double g_current_fps_multiplier = 1.0; // 当前播放速度倍率

bool g_paused = false;              // 暂停状态
cv::Mat g_current_frame_for_pause;  // 用于暂停时显示的当前帧

// --- 进度条回调函数 ---
// 当用户拖动进度条时,此函数被调用
void on_trackbar_progress(int pos, void* userdata) {
    if (!cap.isOpened()) return;
    // `pos` 是滑动条的新位置
    // 我们只在 `pos` 与视频捕获对象的内部帧计数器显著不同时才设置帧位置,
    // 以避免在程序更新滑动条时(非用户拖动)产生循环调用或抖动。
    // `userdata` 在这里没有使用。
    if (std::abs(pos - static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES))) > 1) {
         cap.set(cv::CAP_PROP_POS_FRAMES, pos);
    }
    g_slider_progress = pos; // 确保全局变量与滑动条同步
}

// --- 速度条回调函数 ---
// 当用户拖动速度条时,此函数被调用
void on_trackbar_speed(int pos, void* userdata) {
    if (pos < g_speed_trackbar_min) {
        g_speed_trackbar_val = g_speed_trackbar_min;
        // 如果用户尝试设置低于最小值,强制将滑动条也设回最小值
        cv::setTrackbarPos("Speed x0.01", window_name, g_speed_trackbar_min);
    } else {
        g_speed_trackbar_val = pos;
    }
    // 将滑动条的值 (例如 10 到 400) 转换为速度倍率 (例如 0.1x 到 4.0x)
    g_current_fps_multiplier = static_cast<double>(g_speed_trackbar_val) / 100.0;
}


int main(int argc, char** argv) {
    std::string video_path;
    if (argc > 1) {
        video_path = argv[1];
    } else {
        std::cout << "使用方法: " << argv[0] << " <视频文件路径>" << std::endl;
        std::cout << "请输入视频文件路径: ";
        std::getline(std::cin, video_path); // 使用getline读取可能带空格的路径
    }

    if (video_path.empty()){
        std::cerr << "错误: 未提供视频文件路径。" << std::endl;
        return -1;
    }
    
    // 移除路径两端可能存在的引号(常见于拖拽文件到命令行)
    if (video_path.front() == '"' && video_path.back() == '"') {
        video_path = video_path.substr(1, video_path.length() - 2);
    }


    if (!cap.open(video_path)) {
        std::cerr << "错误: 无法打开视频文件: \"" << video_path << "\"" << std::endl;
        return -1;
    }

    g_total_frames = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_COUNT));
    double fps_original = cap.get(cv::CAP_PROP_FPS);

    if (fps_original <= 0) {
        std::cout << "警告: 无法获取视频的有效FPS,将使用默认值 30 FPS。" << std::endl;
        fps_original = 30.0;
    }
    int base_delay_ms = static_cast<int>(1000.0 / fps_original);

    cv::namedWindow(window_name, cv::WINDOW_AUTOSIZE);

    // 创建进度条
    if (g_total_frames > 0) {
        cv::createTrackbar("Progress", window_name, &g_slider_progress, g_total_frames - 1, on_trackbar_progress);
    }

    // 创建速度条
    cv::createTrackbar("Speed x0.01", window_name, &g_speed_trackbar_val, g_speed_trackbar_max, on_trackbar_speed);
    cv::setTrackbarMin("Speed x0.01", window_name, g_speed_trackbar_min);
    cv::setTrackbarPos("Speed x0.01", window_name, 100); // 初始速度1.0x
    on_trackbar_speed(100, 0); // 手动调用一次以初始化g_current_fps_multiplier

    cv::Mat frame;
    int current_frame_display_number = 0; // 用于UI显示的帧号

    std::cout << "按 '空格键' 暂停/播放, 'ESC' 或 'q' 退出." << std::endl;

    while (true) {
        if (!g_paused) {
            bool success = cap.read(frame);
            if (!success) {
                std::cout << "视频结束或读取帧失败。" << std::endl;
                // 视频结束时可以选择暂停在最后一帧
                g_paused = true; 
                if (g_current_frame_for_pause.empty()) { // 如果从未成功读取过帧
                    std::cout << "没有帧可以显示,退出。" << std::endl;
                    break;
                }
                frame = g_current_frame_for_pause.clone(); // 显示最后一帧
            } else {
                 g_current_frame_for_pause = frame.clone(); // 保存当前有效帧
            }
        } else { // 暂停状态
            if (g_current_frame_for_pause.empty()) {
                // 尝试在当前进度条位置读取一帧作为暂停帧
                // 这通常在视频开始就暂停,或跳转后暂停时发生
                if (cap.isOpened() && g_total_frames > 0) {
                    cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress);
                    cap.read(g_current_frame_for_pause);
                }
            }
            // 如果仍然没有可显示的暂停帧,则显示黑屏或之前的帧
            if (!g_current_frame_for_pause.empty()) {
                frame = g_current_frame_for_pause.clone();
            } else {
                // 作为最后手段,显示一个黑屏
                cv::Size frame_size = cap.isOpened() ? 
                                      cv::Size(static_cast<int>(cap.get(cv::CAP_PROP_FRAME_WIDTH)), static_cast<int>(cap.get(cv::CAP_PROP_FRAME_HEIGHT))) :
                                      cv::Size(640, 480); // 默认大小
                if(frame_size.width <= 0 || frame_size.height <=0) frame_size = cv::Size(640,480);
                frame = cv::Mat::zeros(frame_size, CV_8UC3);
                 std::cout << "暂停时无有效帧可显示,显示黑屏。" << std::endl;
            }
        }
        
        if (frame.empty()){
            std::cout << "错误:帧为空,无法继续播放。" << std::endl;
            break;
        }

        // 更新进度条的当前位置 (仅当未被用户拖动时)
        // cv::VideoCapture::get(cv::CAP_PROP_POS_FRAMES) 获取的是下一帧的索引
        current_frame_display_number = static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES));
        if (current_frame_display_number > 0 && !g_paused) { // 通常 POS_FRAMES 指向下一帧
            current_frame_display_number--; // 显示的是当前帧的编号
        }
        if (g_paused) { // 暂停时,进度条应显示当前暂停帧的编号
            current_frame_display_number = g_slider_progress;
        }


        // 避免在回调函数中再次设置,导致可能的抖动
        // 我们只在程序逻辑前进时更新滑动条
        // 而用户拖动滑动条时,回调函数会更新 g_slider_progress 和视频位置
        if (g_total_frames > 0 && std::abs(g_slider_progress - current_frame_display_number) > 0 && !g_paused) {
            // 只有当程序播放导致帧号变化时,才更新滑动条绑定的g_slider_progress
            // 并且确保不是因为回调函数刚刚设置了cap.set而立即又被这里的cap.get更新回来
            // 这是一个简化模型,最理想的是区分用户拖动和程序更新
             g_slider_progress = current_frame_display_number;
             cv::setTrackbarPos("Progress", window_name, g_slider_progress);
        } else if (g_total_frames > 0 && g_paused) {
            // 暂停时,如果用户拖动了滑块,g_slider_progress会被回调更新
            // 我们也需要确保滑块视觉位置正确
            cv::setTrackbarPos("Progress", window_name, g_slider_progress);
        }
        
        cv::imshow(window_name, frame);

        int delay = static_cast<int>(static_cast<double>(base_delay_ms) / g_current_fps_multiplier);
        if (delay <= 0) delay = 1;

        char key = (char)cv::waitKey(delay);

        if (key == 27 || key == 'q' || key == 'Q') {
            break;
        } else if (key == ' ') {
            g_paused = !g_paused;
            if (!g_paused) { // 从暂停恢复播放
                // 确保视频从滑动条指示的位置开始播放
                 if (cap.isOpened() && std::abs(g_slider_progress - static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES))) > 1) {
                    cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress);
                 }
                 std::cout << "播放中..." << std::endl;
            } else { // 进入暂停
                std::cout << "已暂停." << std::endl;
                // g_current_frame_for_pause 应该已经被保存了最新的帧
            }
        }
    }

    cap.release();
    cv::destroyAllWindows();
    return 0;
}

编译和运行

使用 CMake (推荐):

  1. 将上述代码保存为 video_player.cpp
  2. 创建 CMakeLists.txt 文件:
    cmake_minimum_required(VERSION 3.10)
    project(VideoPlayer)
    
    set(CMAKE_CXX_STANDARD 14) # C++14 或更高
    set(CMAKE_CXX_STANDARD_REQUIRED True)
    
    find_package(OpenCV REQUIRED)
    
    include_directories(${OpenCV_INCLUDE_DIRS})
    add_executable(VideoPlayer video_player.cpp)
    target_link_libraries(VideoPlayer PRIVATE ${OpenCV_LIBS})
    
  3. 编译:
    mkdir build
    cd build
    cmake ..
    make      # 或者ninja, 或者在Visual Studio中构建项目
    
  4. 运行:
    ./VideoPlayer /path/to/your/video.mp4
    # 或者在Windows上 (如果构建在Debug目录):
    # .\Debug\VideoPlayer.exe C:\path\to\your\video.mp4
    
    如果没有在命令行提供路径,程序会提示你输入。

使用 g++ (Linux/macOS):

g++ video_player.cpp -o VideoPlayer $(pkg-config --cflags --libs opencv4) # 或 opencv
./VideoPlayer /path/to/your/video.mp4

使用说明

  • 启动程序时,如果未在命令行参数中指定视频路径,程序会提示你输入。
  • Progress 滑动条: 显示当前播放进度,拖动它可以跳转到视频的不同位置。
  • Speed x0.01 滑动条: 调整播放速度。值为 100 表示 1.0x (正常速度),50 表示 0.5x (半速),200 表示 2.0x (两倍速)。范围是 0.1x 到 4.0x。
  • 空格键: 暂停或继续播放。
  • ESC 或 q/Q键: 退出播放器。

代码解释

速度控制

  • 视频的原始 fps_original (每秒帧数) 决定了每帧的基础延迟 base_delay_ms = 1000 / fps_original
  • 速度滑动条的值 g_speed_trackbar_val (例如范围 10-400) 被转换为一个倍率 g_current_fps_multiplier (例如 0.1 - 4.0)。
  • 实际的帧间延迟 delay 通过 base_delay_ms / g_current_fps_multiplier 计算。
    • 倍率 > 1.0 (加速): delay 减小,播放加快。
    • 倍率 < 1.0 (减速): delay 增大,播放减慢。
  • cv::waitKey(delay) 不仅提供延迟,还处理 GUI 事件(如滑动条的拖动)。

进度控制

  • 进度条的最大值设置为视频的总帧数 g_total_frames - 1
  • 当用户拖动进度条时,on_trackbar_progress 回调函数被触发。
  • 在该回调中,cap.set(cv::CAP_PROP_POS_FRAMES, pos) 用于将视频的当前读取位置跳转到滑动条指定的新位置 pos
  • 在主循环中,程序会读取视频的当前帧号 cap.get(cv::CAP_PROP_POS_FRAMES),并用 cv::setTrackbarPos 更新滑动条的显示位置,以反映实际的播放进度。

暂停/播放

  • 一个布尔变量 g_paused 跟踪播放状态。
  • 按下空格键时,g_paused 的状态会切换。
  • 如果 g_pausedtrue
    • 主循环不读取新的视频帧 (cap.read() 不被调用)。
    • cv::imshow 持续显示暂停时捕获的最后一帧 g_current_frame_for_pause
  • 如果 g_pausedfalse
    • 正常读取和显示视频帧。

可能的改进

  • 更精确的进度条拖动检测: 当前实现中,程序更新进度条也可能触发回调。可以通过更复杂的事件处理或自定义UI控件来区分用户拖动和程序更新。
  • 显示时间码: 在窗口上显示 “当前时间 / 总时间” 而不是帧号。
  • 音量控制和音频播放: OpenCV 主要处理视频。播放音频需要额外的库,如 SDL、PortAudio,或者使用 FFmpeg 更底层的 API。
  • 逐帧步进: 添加按钮或快捷键实现向前/向后逐帧播放。
  • 错误处理: 更详细的错误报告和处理。
  • UI美化: 使用 Qt 或其他 GUI 框架替换 OpenCV HighGUI 可以创建更美观和功能丰富的界面。
  • 播放列表: 支持加载和管理多个视频文件。

总结

这个项目展示了如何使用 OpenCV 创建一个具有基本播放控制功能的视频播放器。通过滑动条,用户可以方便地控制播放速度和进度,并通过键盘快捷键进行暂停和退出。这是一个很好的练习,可以帮助你熟悉 OpenCV 的视频处理和简单的 GUI 交互。


网站公告

今日签到

点亮在社区的每一天
去签到