OpenCV视频流C++多线程处理方法详细分析
作者:hlld26
为什么需要多线程处理视频流
在之前有写过一篇文章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在单线程的模式下运行可缓解窗口卡死的情况。
到此这篇关于OpenCV视频流C++多线程处理方法详细分析的文章就介绍到这了,更多相关OpenCV视频流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!