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

前言

多年前刚刚接触Opencv那会还没有AI,第一次处理视频的时候,仅仅通过usb摄像头显示都还可以,但是通过rtsp等网络方式的方法接入,在显示图像的过程再处理点什么,那简直是卡,通过网上搜索,建议使用多线程处理,然后一堆代码,终于从里面理清了,最近也有同事遇到同样的问题,我说让看代码,他说里面掺杂了太多业务,不太好看明白,所以才有了这篇文章:从最简单的方法实现多线程处理视频流,不参与任何业务。一个负责获取视频帧,一个负责处理,使用最简单的实践达到目的。同理的思想用于在一个系统之上,我开发了一个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. 线程分工

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 双线程对比

多线程编程要点

  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
                    |   释放锁
                    |   处理并显示帧

文章转载自: Tlink

原文链接: https://www.cnblogs.com/tlink/p/19737429

体验地址: http://www.jnpfsoft.com/?from=001YH

相关推荐
ZPC82102 小时前
OLOv11 + 深度相机的方案实现高精度3D定位
人工智能·数码相机·算法·机器人
星辰yzy2 小时前
个人用户怎么选AI套餐更划算
人工智能
weixin_457760002 小时前
基于pytorch实现LPR模型车牌识别
人工智能·pytorch·python·深度学习·lpr
市象2 小时前
AWE观察:一面“魔镜”照亮全屋智能,AI卫浴迎来新场景
人工智能·健康医疗·制造
Dfreedom.2 小时前
机器学习经典算法全景解析与演进脉络(监督学习篇)
人工智能·学习·算法·机器学习·监督学习
华农DrLai2 小时前
什么是Prompt注入攻击?为什么恶意输入能操控AI行为?
人工智能·深度学习·大模型·nlp·prompt
吃杠碰小鸡2 小时前
Python+Ai学习流程
人工智能·python·学习
奔袭的算法工程师2 小时前
用AI写天线阵列排布算法
人工智能·算法·信号处理
飞Link2 小时前
具身智能音频处理核心框架 PyAudio 深度拆解与实战
开发语言·python·音视频