在之前有写过一篇文章Python环境下OpenCV视频流的多线程处理方式,上面简单记录了如何使用Python实现对OpenCV视频流的多线程处理。简单来说,在目标检测等任务中,如果视频流的捕获、解码以及检测都在同一个线程中,那么很可能出现目标检测器实时性不高导致的检测时延问题。使用多线程处理,将视频帧的捕获和解码放在一个线程,推理放在一个线程,可以有效缓解时延的问题,使得目标检测的实时性看似有所提升。
C++的处理方式与Python大致相同,但却可能遇到一些问题,如使用OpneCV多线程时X11库报错、OpenCV显示卡死等问题,这些问题可能的解决方法会在后面简单提一下。在本文中,使用的多线程是c++11中引入的thread标准库,实现方式则包括函数封装和类封装两种。
函数封装的实现方式相比类封装要更为简洁,当然可复用性也会降低。简单的示例代码如下:
// video_test.cpp
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
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
头文件,并在main函数开头添加XInitThreads()
函数调用,在编译时还需要添加-lX11
链接库。我在Jetson Nano上运行时还遇到显示窗口卡死的情况,既imshow
函数出现问题,点击关闭窗户后又会重新打开新窗口正常显示。遇到这样的情况,可在main函数开头添加一行代码cv::setNumThreads(1)
,设置OpenCV在单线程的模式下运行可缓解窗口卡死的情况。