为什么需要多线程处理视频流

在之前有写过一篇文章Python环境下OpenCV视频流的多线程处理方式,上面简单记录了如何使用Python实现对OpenCV视频流的多线程处理。简单来说,在目标检测等任务中,如果视频流的捕获、解码以及检测都在同一个线程中,那么很可能出现目标检测器实时性不高导致的检测时延问题。使用多线程处理,将视频帧的捕获和解码放在一个线程,推理放在一个线程,可以有效缓解时延的问题,使得目标检测的实时性看似有所提升。

C++的多线程处理方式

C++的处理方式与Python大致相同,但却可能遇到一些问题,如使用OpneCV多线程时X11库报错、OpenCV显示卡死等问题,这些问题可能的解决方法会在后面简单提一下。在本文中,使用的多线程是c++11中引入的thread标准库,实现方式则包括函数封装和类封装两种。

函数封装的实现方式

函数封装的实现方式相比类封装要更为简洁,当然可复用性也会降低。简单的示例代码如下:

// video_test.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
static std::mutex mutex;
static std::atomic_bool isOpen;
static void cameraThreadFunc(int camId, int height, int width, cv::Mat* pFrame)
{
    cv::VideoCapture capture(camId);
    capture.set(cv::CAP_PROP_FOURCC, CV_FOURCC('M', 'J', 'P', 'G'));
    capture.set(cv::CAP_PROP_FRAME_WIDTH, width);
    capture.set(cv::CAP_PROP_FRAME_HEIGHT, height);
    capture.set(cv::CAP_PROP_FPS, 30);
    if (!capture.isOpened()) {
        isOpen = false;
        std::cout << "Failed to open camera with index " << camId << std::endl;
    }
    cv::Mat frame;
    while (isOpen) {
        capture >> frame;
        if (mutex.try_lock()) {
            frame.copyTo(*pFrame);
            mutex.unlock();
        }
        cv::waitKey(5);
    }
    capture.release();
}
int main(int argc, char* argv[])
{
    isOpen = true;
    cv::Mat frame(480, 640, CV_8UC3), gray;
    std::thread thread(cameraThreadFunc, 0, 480, 640, &frame);
    while (isOpen) {
        mutex.lock();
        frame.copyTo(gray);
        mutex.unlock();
        if (gray.empty()) {
            break;
        }
        cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
        cv::blur(gray, gray, cv::Size(3, 3));
        cv::Canny(gray, gray, 5 , 38 , 3);
        cv::waitKey(100);
        cv::imshow("video", gray);
        if (cv::waitKey(1) == 'q') {
            break;
        }
    }
    isOpen = false;
    thread.join();
    return 0;
}

在上面的代码中,摄像头的打开、帧捕获及解码都在cameraThreadFunc线程函数中进行。在c++11中,有关pthread的线程操作都封装在thread标准库中,线程的开启方式也由执行pthread_create()函数变为对thread类的操作。使用thread类时,第一个参数为线程函数的指针,后续的参数为传入线程函数的参数。需要注意的是:如果要传入参数引用,则需要使用std::ref()对参数进行包装;如果传入类成员函数时,则thread类构造函数的第二个参数必须为this

使用多线程时还需要考虑线程之间的同步问题,在上面的程序中,两个线程会同时访问pFrame指向的缓存空间,使用mutex可确保同一时刻下仅有一个线程能访问到缓存空间。另外,使用atomic_bool在多线程中进行状态切换也是必要的,原子操作使得对布尔变量的赋值在临界区中进行,可消除线程之间竞争访问或访问结果不一致的情况。

在上面的程序中,由于主线程会先访问pFrame变量,因此需要预先为pFrame申请空间,不然程序开始执行时出现pFrame为空的情况。在Ubuntu中使用g++编译的方法如下:

g++ video_test.cpp -std=c++11 -I/usr/local/include/ -lpthread -L/usr/local/lib -lopencv_highgui -lopencv_core -lopencv_imgproc -lopencv_videoio -o video_test

根据OpenCV版本和安装位置的不同,需要相应修改头文件和库文件的位置,例如对于OpenCV4,头文件目录应修改为/usr/local/include/opencv4。在Jetson平台上,头文件的位置在/usr/include/opencv4,库文件则在/usr/lib/aarch64-linux-gnu。如果有配置pkg-config,那么还可以使用如下方式进行编译:

g++ video_test.cpp -std=c++11 `pkg-config --cflags opencv` -pthread `pkg-config --libs opencv` -o video_test

类封装的实现方式

同函数封装的方式相似,类封装的方式仅是将线程函数和线程同步变量变为类成员,从而提升程序的可复用性。简单的示例代码如下:

// video_test.cpp
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <atomic>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class VideoCaptureMT {
public:
	VideoCaptureMT(int index, int height=480, int width=640);
	VideoCaptureMT(std::string filePath, int height=480, int width=640);
	~VideoCaptureMT();
	bool isOpened() {
		return m_IsOpen;
	}
	void release() {
		m_IsOpen = false;
	}
	bool read(cv::Mat& frame);
private:
	void captureInit(int index, std::string filePath, int height, int width);
	void captureFrame();
	cv::VideoCapture* m_pCapture;
	cv::Mat* m_pFrame;
	std::mutex* m_pMutex;
	std::thread* m_pThread;
	std::atomic_bool m_IsOpen;
};
VideoCaptureMT::VideoCaptureMT(int index, int height, int width)
{
	captureInit(index, std::string(), height, width);
}
VideoCaptureMT::VideoCaptureMT(std::string filePath, int height, int width)
{
	captureInit(0, filePath, height, width);
}
VideoCaptureMT::~VideoCaptureMT()
{
	m_IsOpen = false;
	m_pThread->join();
	if (m_pCapture->isOpened()) {
		m_pCapture->release();
	}
	delete m_pThread;
	delete m_pMutex;
	delete m_pCapture;
	delete m_pFrame;
}
void VideoCaptureMT::captureInit(int index, std::string filePath, int height, int width)
{
	if (!filePath.empty()) {
		m_pCapture = new cv::VideoCapture(filePath);
	}
	else {
		m_pCapture = new cv::VideoCapture(index);
	}
	m_pCapture->set(cv::CAP_PROP_FRAME_WIDTH, width);
	m_pCapture->set(cv::CAP_PROP_FRAME_HEIGHT, height);
	m_pCapture->set(cv::CAP_PROP_FPS, 30);
	m_IsOpen = true;
	m_pFrame = new cv::Mat(height, width, CV_8UC3);
	m_pMutex = new std::mutex();
	m_pThread = new std::thread(&VideoCaptureMT::captureFrame, this);
}
void VideoCaptureMT::captureFrame()
{
	cv::Mat frameBuff;
	while (m_IsOpen) {
		(*m_pCapture) >> frameBuff;
		if (m_pMutex->try_lock()) {
			frameBuff.copyTo(*m_pFrame);
			m_pMutex->unlock();
		}
		cv::waitKey(5);
	}
}
bool VideoCaptureMT::read(cv::Mat& frame)
{
	if (m_pFrame->empty()) {
		m_IsOpen = false;
	}
	else {
		m_pMutex->lock();
		m_pFrame->copyTo(frame);
		m_pMutex->unlock();
	}
	return m_IsOpen;
}
int main(int argc, char* argv[])
{
	VideoCaptureMT capture(0);
	cv::Mat frame, gray;
	while (capture.isOpened()) {
		if (!capture.read(frame)) {
			break;
		}
		cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
		cv::blur(gray, gray, cv::Size(3, 3));
		cv::Canny(gray, gray, 5 , 38 , 3);
		cv::waitKey(100);
		cv::imshow("image", gray);
        if (cv::waitKey(5) == 'q') {
			break;
		}
	}
	capture.release();
	return 0;
}

在上面的代码中,线程函数和线程间同步变量都是类成员,不同的地方在于:摄像头是在主线程中打开,在子线程中捕获和解码帧,但实际效果和函数封装的方式没有区别。

可能遇到的问题

使用C++编写OpenCV的多线程程序时可能会遇到一些问题,例如我在Jetson AGX上运行时会报错,提示需要进行XInitThreads的初始化。出现这样的情况时,需要在cpp文件中添加#include <X11/Xlib.h>头文件,并在main函数开头添加XInitThreads()函数调用,在编译时还需要添加-lX11链接库。我在Jetson Nano上运行时还遇到显示窗口卡死的情况,既imshow函数出现问题,点击关闭窗户后又会重新打开新窗口正常显示。遇到这样的情况,可在main函数开头添加一行代码cv::setNumThreads(1),设置OpenCV在单线程的模式下运行可缓解窗口卡死的情况。

原文地址:https://blog.csdn.net/hlld__/article/details/112600447

相关文章: