【C++并发入门】opencv摄像头帧率计算和多线程相机读取(下):完整代码实现

前言

  • 高帧率摄像头往往应用在很多opencv项目中,今天就来通过简单计算摄像头帧率,抛出一个单线程读取摄像头会遇到的问题,同时提出一种解决方案,使用多线程对摄像头进行读取。
  • 上一期:【C++并发入门】摄像头帧率计算和多线程相机读取(上):并发基础概念和代码实现-CSDN博客
  • 本教程使用的环境:
    • opencv C++ 4.5
    • C++11
    • KS1A293黑白240fps摄像头
  • 上一期我们介绍了并发的基础入门知识,讲解了摄像头帧率计算,线程进程,并发和并行,std::thread,std::mutex,死锁,数据竞争问题,以及std::lock_guard。这一期我们来看看如何把并发运用到实际的多线程读取相机上。

1 多线程读取相机

1-1 代码实现
  • 结合上一节我们学习到的内容,我们使用面向对象的思路,简单写出以下的代码
cpp 复制代码
#include<iostream>
#include <opencv2/opencv.hpp>
#include <thread>
#include<chrono>
#define _CRT_SECURE_NO_WARNINGS 1

class ThreadCam
{
private:
	cv::Mat  frame;
	cv::VideoCapture cap;
	std::thread cameraCaptureThread;
	std::thread cameraProcessingThread;
	std::mutex mtx;
	std::chrono::time_point<std::chrono::steady_clock> startTime = std::chrono::steady_clock::now();
	std::chrono::time_point<std::chrono::steady_clock> endTime;

	void cameraCaptureThreadFunc() 
	{
		while (true) 
		{
			{
				std::lock_guard<std::mutex> guard(mtx);
				bool ret = cap.read(frame);
			}
		}
	}
	void cameraProcessingThreadFunc()
	{
		double frame_count = 0;
		double fps = 0;
		while (true)
		{
			if (frame.empty())
				continue;
			frame_count++;
			endTime = std::chrono::steady_clock::now();
	        double timeTaken = std::chrono::duration<double, std::milli>(endTime - startTime).count();
	        if (timeTaken >= 1000)
	        {
	            fps = frame_count;
	            startTime = std::chrono::steady_clock::now();
	            frame_count = 0;
	        }
			

			cv::putText(frame, std::to_string(int(fps)) + " FPS", cv::Point(frame.cols / 4, frame.rows / 3), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 0, 0), 2);
		
			
			cv::imshow("Frame", frame);
			
			if (cv::waitKey(1) == 'q')
			    break;
			
		}
	}
public:
	ThreadCam() :cap(0)
	{
		if (!cap.isOpened())
		{
			std::cerr << "open camera failed!" << std::endl;
			std::abort();
		}
		cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
		cap.set(cv::CAP_PROP_FRAME_HEIGHT, 400);
		cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));
		cameraCaptureThread = std::thread(&ThreadCam::cameraCaptureThreadFunc, this);
		cameraProcessingThread = std::thread(&ThreadCam::cameraProcessingThreadFunc, this);

		cameraCaptureThread.join();
		cameraProcessingThread.join();
	}
};
int main()
{
	try {
		ThreadCam thread_cam;
	
	}
	catch (const std::exception& e) {
		std::cerr << "Exception caught: " << e.what() << std::endl;
		return -1;
	}

	return 0;
}
  • cameraCaptureThreadFunc:这个线程不断地从摄像头捕获帧,并将捕获到的帧存储在frame变量中。它使用std::lock_guard来保证在读取和写入frame时不会发生竞争条件。
  • cameraProcessingThreadFunc:这个线程捕获摄像头画面计算并显示视频的帧率。它首先检查frame是否为空,然后计算自startTime以来的时间。如果时间超过1000毫秒,则计算帧率,重置startTime,并将帧计数器frame_count重置为0。然后,它在每一帧上显示当前的帧率,并在按下'q'键时退出循环。
  • 下面代码创建了一个std::lock_guard对象guard,它自动锁定互斥量mtxstd::lock_guard的作用域是紧随其声明之后的代码块,即大括号{}内的区域。当std::lock_guard对象超出这个作用域时,其析构函数会被调用,这将导致互斥锁被自动释放。
cpp 复制代码
{
	std::lock_guard<std::mutex> guard(mtx);
	bool ret = cap.read(frame);
}
1-2 效果展示
  • 运行效果如下,一看帧率???甚至超出了摄像头的最高帧率,这是怎么回事呢

  • 由于摄像头捕获的线程和摄像头处理(FPS计算的线程)是异步,那也就意味着摄像头处理的线程甚至可能快于摄像头捕获线程的运行速率,导致捕获到的frame会出现连续相同的画面,以导致画面帧数计算错误。那解决这个问题也很简单,如果我们需要计算真正的FPS,我们只需要剔除重复的画面即可。

  • 但是在这样多线程的读取捕获处理下,即使会出现相同的画面,也就不会出现像上一节那样由于耗时操作导致捕获到的画面不及时,一定程度上解决了这个问题。


2 判重优化

2-1 方法选择
  • 那为了正确计算图像的真实FPS,要做的就是进行判重剔除,那在opencv中如何做到判重呢
    1. 像素级比较:直接比较两张图片的每个像素值。如果所有像素都相同,则认为两张图片一致。
    2. 哈希方法:使用哈希函数(如MD5、SHA-1等)为图片生成一个唯一的指纹。如果两张图片的哈希值相同,则它们很可能是一致的。
    3. 特征匹配: 使用特征检测算法(如SIFT、SURF、ORB等)提取图片中的关键点,然后比较这些关键点的匹配程度。
  • 那我们选择哪一种呢,==答案是都不选!!!==上述图像处理操作都会经历一定程度上的耗时操作,即使按照最简单的将两张图像进行做差的结果来判断图像是否一致都需要进行一次做差运算,这是相对耗时间的。
cpp 复制代码
bool isFrameSame(const  cv::Mat& frame1, const cv::Mat& frame2, double threshold = 1e-5) {

    if (frame1.empty() || frame2.empty()) 
	    return false;
    if (frame1.size() != frame2.size())
	    return false;

    cv::Mat diff;
    cv::absdiff(img1, img2, diff);
    // 检查差值图像是否接近全黑(意味着两幅图像一致)
    double diffMean = cv::mean(diff)[0];
    return diffMean < threshold;
}
  • 这里我们提出一种计算图像帧是否相同的方法,其核心思路就是记录图像捕获时的时间戳,通过对比时间戳是否发生改变来判断图像是否更新。

2-2 时间戳
  • 时间戳(Timestamp)是一个能够表示特定时间点的数据,通常是一个计数器,用来记录自某个特定时间点(如1970年1月1日)以来的秒数或毫秒数。时间戳常用于记录事件的发生时间,或作为文件、数据记录的版本标识。
  • 在C++中,可以使用<chrono>库来获取时间戳。<chrono>库提供了多种时间点(time_point)和时间段(duration)的表示,以及相关的时钟(clock)类型。
  • 我们来看一个例子来获取毫秒时间戳
cpp 复制代码
#include <iostream>
#include <chrono>

int main() {
    // 获取当前系统时间的时间点
    auto now = std::chrono::system_clock::now();

    // 将时间点转换为自纪元(1970年1月1日)以来的毫秒数
    auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());

    // 输出时间戳
    std::cout << "当前时间戳(毫秒): " << now_ms.count() << std::endl;

    return 0;
}
  • 可以看到得到的是一串非常长的数字

  • 其中

    • now 的类型是 std::chrono::system_clock::time_point。这是一个表示特定时间点的类型,它是 std::chrono 库中的一个模板类型,专门用于表示系统时间。
    • now_ms 的类型是 std::chrono::milliseconds。这是一个表示时间间隔的类型,它是 std::chrono 库中的一个模板类型,专门用于表示毫秒级的时间间隔。
    • 对于 now_ms.count() 的类型,它是 long long 类型。这是因为 std::chrono::milliseconds::count() 方法返回的是一个表示毫秒数的时间长度,而 std::chrono::milliseconds 类型是一个模板类型,其默认的Rep(表示时间的类型)是 long long。因此,now_ms.count() 返回的纳秒数是一个 long long 类型的整数。

2-3 时间戳存储选择
  • 我们创建一个带时间戳的图像结构体,这个结构体需要存储时间戳和cv::Mat图像,那么问题来了,那我们要选取什么类型来存储时间戳呢.
cpp 复制代码
#include <iostream>
#include <string>

int main() {
    long long numLongLong = 1727760106400LL;

    std::string numString = "1727760106400";
    const char* numConstChar = "1727760106400";
    std::cout << "long long: " << sizeof(numLongLong) << " bytes" << numLongLong << std::endl;
    std::cout << "std::string: " << sizeof(numString) << " bytes" << numString << std::endl;


    std::cout << "const char* pointer: " << sizeof(numConstChar) << " bytes" << numConstChar << std::endl;
  
    return 0;
}
  • 结果如下

2-3 实现
  • 那我们实现一个结构体
cpp 复制代码
struct FrameWithTimeStamp
{
	long long time_stamp;
	cv::Mat frame;
};
  • 完整代码如下,通过记录时间戳来进行对比
cpp 复制代码
#include<iostream>
#include <opencv2/opencv.hpp>
#include <thread>
#include<chrono>
#define _CRT_SECURE_NO_WARNINGS 1
struct FrameWithTimeStamp
{
	long long time_stamp;
	cv::Mat frame;
};

class ThreadCam
{
private:
	FrameWithTimeStamp  frame_t;
	cv::VideoCapture cap;
	std::thread cameraCaptureThread;
	std::thread cameraProcessingThread;
	std::mutex mtx;
	std::chrono::time_point<std::chrono::steady_clock> startTime = std::chrono::steady_clock::now();
	std::chrono::time_point<std::chrono::steady_clock> endTime;
	void setCurrentTimeStamp(long long& time_stamp)
	{
		auto now = std::chrono::system_clock::now();

		// 将时间点转换为自纪元(1970年1月1日)以来的毫秒数
		auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
		time_stamp = now_ms.count();
	}
	void cameraCaptureThreadFunc() 
	{
		while (true) 
		{
			{
				std::lock_guard<std::mutex> guard(mtx);
				bool ret = cap.read(frame_t.frame);
				setCurrentTimeStamp(frame_t.time_stamp);
			}
		}
	}
	void cameraProcessingThreadFunc()
	{
		double frame_count = 0;
		double fps = 0;
		auto now = std::chrono::system_clock::now();
		long long last_timeStamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
		while (true)
		{
			if (frame_t.frame.empty())
				continue;

			if (last_timeStamp == frame_t.time_stamp)
				continue;
			frame_count++;
			endTime = std::chrono::steady_clock::now();
	        double timeTaken = std::chrono::duration<double, std::milli>(endTime - startTime).count();
	        if (timeTaken >= 1000)
	        {
	            fps = frame_count;
	            startTime = std::chrono::steady_clock::now();
	            frame_count = 0;
	        }
			last_timeStamp = frame_t.time_stamp;

			cv::putText(frame_t.frame, std::to_string(int(fps)) + " FPS", cv::Point(frame_t.frame.cols / 4, frame_t.frame.rows / 3), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 0, 0), 2);
			cv::putText(frame_t.frame, std::to_string(frame_t.time_stamp), cv::Point(frame_t.frame.cols /2, frame_t.frame.rows / 3), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 0, 0), 2);
		
			
			cv::imshow("Frame", frame_t.frame);
			
			if (cv::waitKey(1) == 'q')
			    break;
			
		}
	}
public:
	ThreadCam() :cap(0)
	{
		if (!cap.isOpened())
		{
			std::cerr << "open camera failed!" << std::endl;
			std::abort();
		}
		cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
		cap.set(cv::CAP_PROP_FRAME_HEIGHT, 400);
		cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));
		cameraCaptureThread = std::thread(&ThreadCam::cameraCaptureThreadFunc, this);
		cameraProcessingThread = std::thread(&ThreadCam::cameraProcessingThreadFunc, this);

		cameraCaptureThread.join();
		cameraProcessingThread.join();
	}
};
int main()
{
	try {
		ThreadCam thread_cam;
	
	}
	catch (const std::exception& e) {
		std::cerr << "Exception caught: " << e.what() << std::endl;
		return -1;
	}

	return 0;
}
  • 效果如下,稳定在相机的最高帧率180fps左右,右边是显示的时间戳

总结

  • 至此我们完成了多线程相机的全部读取,正式完成了C++并发第一步
  • 后续用空更新更多的并发教程~感谢大家的支持
  • 如有错误,欢迎指出!!!!!!
相关推荐
獨枭1 小时前
CMake 构建项目并整理头文件和库文件
c++·github·cmake
西猫雷婶2 小时前
python学opencv|读取图像(十九)使用cv2.rectangle()绘制矩形
开发语言·python·opencv
小王爱吃月亮糖3 小时前
C++的23种设计模式
开发语言·c++·qt·算法·设计模式·ecmascript
小哈龙4 小时前
c++ 类似与c# 线程 AutoResetEvent 和 ManualResetEvent的实现
c++·c#·多线程
yuanbenshidiaos5 小时前
C++--------------树
java·数据库·c++
海螺姑娘的小魏5 小时前
Effective C++ 条款 15:在资源管理类中提供对原始资源的访问
开发语言·c++
青青丘比特7 小时前
STL.string(下)
开发语言·c++
jjjxxxhhh1237 小时前
C++ 模板是为了解决啥问题
开发语言·c++·算法
c++初学者ABC7 小时前
GESP2级2403 小杨的日字矩阵
c++·算法
代码小将8 小时前
PTA数据结构编程题7-1最大子列和问题
数据结构·c++·笔记·学习·算法