OpenCV多线程编程:从单线程到多线程的视频处理

前言

多年前刚开始接触 OpenCV 那会儿,AI 还没火起来。第一次处理视频时,USB 摄像头直接显示还挺流畅,但一旦换成 RTSP 网络流,再叠加点图像处理,画面就卡得没法看。
网上搜了一圈,都说要用多线程。但找到的代码要么复杂臃肿,要么掺杂了大量业务逻辑,花了好大力气才理清楚核心思路。
最近同事也遇到同样的问题,我让他直接看代码,他说业务耦合太重,根本不好剥离。这让我想到:何不写一篇最纯粹的教程?只保留多线程处理视频流的本质——一个线程抓帧,一个线程处理,没有任何业务干扰。
就像后来我做的 Qt 快速开发系统一样,核心理念都是"开箱即用":你需要的是一顿饭,而不是先买锅碗瓢盆。
QT快速开发框架

一、最简单的摄像头显示程序

让我们从最基础的版本开始:一个单线程程序,直接从摄像头读取并显示画面。

基础版本代码

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;

int main() {
    // 打开摄像头(默认摄像头编号0)
    cv::VideoCapture cap(0);
    if (!cap.isOpened()) {
        cerr << "Error: Could not open camera!" << endl;
        return -1;
    }

    cv::Mat frame;
    while (true) {
        cap >> frame;  // 读取一帧
        if (frame.empty()) {
            cerr << "Error: Empty frame!" << endl;
            break;
        }
        
        cv::imshow("Camera", frame);  // 显示画面
        
        char key = cv::waitKey(1);
        if (key == 'q' || key == 'Q') {
            break;  // 按q键退出
        }
    }

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

基础版本的特点

  • 优点:简单直接,易于理解
  • 缺点:所有操作都在一个线程中执行,如果添加复杂的图像处理,会导致画面卡顿

二、尝试使用线程

初学者可能会尝试将摄像头读取放入单独的线程

#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>
using namespace std;

void captureThread() {
    cv::VideoCapture cap(0);
    if (!cap.isOpened()) {
        std::cerr << "Error: Could not open camera!" << std::endl;
        return;
    }

    cv::Mat frame;
    while (true) {
        cap >> frame;
        if (frame.empty()) break;
        
        cv::imshow("Camera", frame); 
        char key = cv::waitKey(1);
        if (key == 'q' || key == 'Q') break;
    }

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

int main() {
    thread captureVideo(captureThread);
    captureVideo.join();
    return 0; 
}

三、再进一步使用双线程实现

#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>
#include <mutex>
#include <atomic>
using namespace std;

// 共享数据
cv::Mat sharedFrame;
mutex mtx;
atomic<bool> running(true);

// 线程1:负责捕获视频帧
void captureThread() {
    cv::VideoCapture cap(0);
    if (!cap.isOpened()) {
        cerr << "Error: Could not open camera!" << endl;
        running = false;
        return;
    }

    cv::Mat frame;
    while (running) {
        cap >> frame;
        if (frame.empty()) {
            cerr << "Error: Empty frame!" << endl;
            break;
        }
        
        // 使用互斥锁保护共享数据
        lock_guard<mutex> lock(mtx);
        frame.copyTo(sharedFrame);
    }

    cap.release();
}

// 线程2:负责处理和显示
void displayAndProcessThread() {
    cv::namedWindow("Camera", cv::WINDOW_AUTOSIZE);
    
    while (running) {
        cv::Mat frame;
        
        // 获取最新的帧
        {
            lock_guard<mutex> lock(mtx);
            if (!sharedFrame.empty()) {
                sharedFrame.copyTo(frame);
            }
        }
        
        if (!frame.empty()) {
            // ===== 在这里添加你的图像处理代码 =====
            
            // 示例1:添加文字
            string text = "Hello OpenCV!";
            cv::putText(frame, text, cv::Point(50, 50), 
                       cv::FONT_HERSHEY_SIMPLEX, 1.0, 
                       cv::Scalar(0, 255, 0), 2);
            
            // 示例2:添加时间戳信息
            cv::putText(frame, "Press 'q' to quit", 
                       cv::Point(10, frame.rows - 10),
                       cv::FONT_HERSHEY_SIMPLEX, 0.5, 
                       cv::Scalar(0, 0, 255), 1);
            
            // 显示处理后的画面
            cv::imshow("Camera", frame);
        }
        
        // 检查退出条件
        char key = cv::waitKey(30);
        if (key == 'q' || key == 'Q') {
            running = false;
            break;
        }
    }
    
    cv::destroyAllWindows();
}

int main() {
    cout << "Program started. Press 'q' to quit." << endl;
    
    // 创建两个线程
    thread capture(captureThread);
    thread display(displayAndProcessThread);
    
    // 等待线程结束
    display.join();
    capture.join();
    
    cout << "Program terminated." << endl;
    return 0;
}

四、代码解析

1. 线程同步机制

mutex mtx;              // 互斥锁,防止数据竞争
atomic<bool> running;   // 原子变量,控制线程结束
  • 互斥锁:确保同一时刻只有一个线程访问共享数据
  • 原子变量:安全地在多线程间传递状态信息

2. 线程分工

线程 职责 说明
captureThread 捕获视频帧 持续从摄像头读取,存入共享变量
displayAndProcessThread 处理和显示 获取帧,添加特效,显示画面

3. 关键代码说明

// 保护共享数据的访问
{
    lock_guard<mutex> lock(mtx);  // 自动加锁解锁
    frame.copyTo(sharedFrame);    // 安全的拷贝
}

五、进阶:添加更多图像处理效果

你可以在显示线程中添加各种OpenCV特效:

// 在显示线程的处理部分添加

// 1. 转为灰度图
cv::Mat gray;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);

// 2. 边缘检测
cv::Mat edges;
cv::Canny(gray, edges, 50, 150);

// 3. 人脸检测(需要haar cascade文件)
// cv::CascadeClassifier faceCascade;
// faceCascade.load("haarcascade_frontalface_default.xml");
// vector<cv::Rect> faces;
// faceCascade.detectMultiScale(gray, faces);

// 4. 添加帧率显示
static int frameCount = 0;
static auto startTime = chrono::steady_clock::now();
frameCount++;
auto currentTime = chrono::steady_clock::now();
auto elapsed = chrono::duration_cast<chrono::seconds>(currentTime - startTime).count();
if (elapsed >= 1) {
    double fps = frameCount / elapsed;
    cout << "FPS: " << fps << endl;
    frameCount = 0;
    startTime = currentTime;
}

六、总结

单线程 vs 双线程对比

特性 单线程 双线程
实现复杂度 简单 中等
响应性 极好
处理复杂任务 会卡顿 流畅
CPU利用率 一般 更好
代码可维护性 简单 良好

多线程编程要点

  1. 正确使用互斥锁保护共享数据
  2. 避免死锁:注意加锁顺序
  3. 使用原子变量控制线程状态
  4. 确保主线程等待子线程结束
  5. OpenCV的显示操作必须在主线程

改进点

有朋友说我这个会导致使用的那个线程空转,然后上面那个如果挂了,底下一直阻塞。的确是有这个问题,我的出发点是最简单的实现,既然这么说了,那就优化一下

#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>
using namespace std;


cv::Mat shareFrame;
atomic<bool> running(true);
mutex mtx;
condition_variable condin_v;
bool frameReady = false;


void captureThread()
{
	cv::VideoCapture cap(0);
	if (!cap.isOpened())
	{
		std::cerr << "Error: Could not open camera!" << std::endl;
		running = false;
		condin_v.notify_all();
		return;
	}

	cv::Mat frame;

	cap.set(cv::CAP_PROP_FPS, 30);


	while (running) {

		cap >> frame;
		if (frame.empty())
		{
			std::cerr << "Error: Empty frame!" << std::endl;
			break;
		}

		{
			lock_guard<mutex> lock(mtx);
			frame.copyTo(shareFrame);
			frameReady = true;
		}
		condin_v.notify_one();
		this_thread::sleep_for(chrono::microseconds(33));

	}
	cap.release();
	condin_v.notify_all();

}

void displayAndProcessThread()
{
	cv::namedWindow("Camera", cv::WINDOW_AUTOSIZE);
	cv::Mat frame;

	while (running)
	{

		{
			unique_lock<mutex> lock(mtx);
			condin_v.wait(lock, [] {return frameReady || !running; });

			if (!running) break;


			if (!shareFrame.empty())
			{
				shareFrame.copyTo(frame);
				frameReady = false;
			}
		}

		if (!frame.empty())
		{
			string text = "Hello Opencv!";
			cv::putText(frame, text, cv::Point(50, 50), cv::FONT_HERSHEY_DUPLEX, 1.0, cv::Scalar(0, 255, 0), 2);
			cv::imshow("Camera", frame);
		}

		char key = cv::waitKey(30);
		if (key == 'Q' || key == 'q')
		{
			running = false;
			condin_v.notify_all();
			break;
		}
	}
	cv::destroyAllWindows();
}

int main() {

	thread captureVideo(captureThread);
	thread display(displayAndProcessThread);
	captureVideo.join();
	display.join();

	return 0;
}
时间 → 
生产者线程          |   消费者线程
--------------------|--------------------
获取锁              |   
生产帧              |   (可能正在等待)
frameReady = true   |   
释放锁              |   
notify_one()  ------→  被唤醒
                    |   尝试获取锁
                    |   获取锁成功
                    |   检查 frameReady = true
                    |   消费帧
                    |   frameReady = false
                    |   释放锁
                    |   处理并显示帧

假如你需要一份纯粹的代码(不包含任何业务)
QT快速开发框架

posted @ 2026-03-19 10:08  Tlink  阅读(248)  评论(0)    收藏  举报