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

news2024/10/2 13:24:27

前言

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

1 多线程读取相机

1-1 代码实现
  • 结合上一节我们学习到的内容,我们使用面向对象的思路,简单写出以下的代码
#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对象超出这个作用域时,其析构函数会被调用,这将导致互斥锁被自动释放。
{
	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等)提取图片中的关键点,然后比较这些关键点的匹配程度。
  • 那我们选择哪一种呢,==答案是都不选!!!==上述图像处理操作都会经历一定程度上的耗时操作,即使按照最简单的将两张图像进行做差的结果来判断图像是否一致都需要进行一次做差运算,这是相对耗时间的。
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)类型。
  • 我们来看一个例子来获取毫秒时间戳
#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图像,那么问题来了,那我们要选取什么类型来存储时间戳呢.
#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 实现
  • 那我们实现一个结构体
struct FrameWithTimeStamp
{
	long long time_stamp;
	cv::Mat frame;
};
  • 完整代码如下,通过记录时间戳来进行对比
#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++并发第一步
  • 后续用空更新更多的并发教程~感谢大家的支持
  • 如有错误,欢迎指出!!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2184473.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Elasticsearch使用Easy-Es + RestHighLevelClient实现深度分页跳页

注意&#xff01;&#xff01;&#xff01;博主只在测试环境试了一下&#xff0c;没有发到生产环境跑。因为代码还没写完客户说不用弄了( •̩̩̩̩&#xff3f;•̩̩̩̩ ) 也好&#xff0c;少个功能少点BUG 使用from size的时候发现存在max_result_window10000的限制&…

认知杂谈67《耐心!征服世界的秘籍》

内容摘要&#xff1a; 人生需家人朋友支持&#xff0c;自信源于解决问题的实力。别怕挫折&#xff0c;努力向前&#xff0c;反思自我。人生如游戏&#xff0c;靠自己打拼。学习要提升沟通、逻辑思维和时间管理等技能&#xff0c;读经典书籍&#xff0c;在平台学编程等&#xff…

ThreadLocal内存泄漏分析

一、ThreadLocal内存泄漏分析 1.1 ThreadLocal实现原理 1.1.1、set(T value)方法 查看ThreadLocal源码的 set(T value)方法&#xff0c;可以发现数据是存在了ThreadLocalMap的静态内部类Entry里面 其中key为使用弱引用的ThreadLocal实例&#xff0c;value为set传入的值。核…

C for Graphic:DNF手游残影效果

dnf手游在作死的道路上越行越远&#xff0c;困难罗特斯完全打不动&#xff0c;提前在抖音上细看攻略&#xff0c;基本能躲过机制不死&#xff0c;但是伤害不够&#xff0c;全时打满也还剩3000管血&#xff0c;组团半天炸团半天完全浪费一天。 个人觉得策划完全没必要这么逼…

Vite:为什么选 Vite

一、现实问题 在浏览器支持 ES 模块之前&#xff0c;JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因&#xff1a;使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。 时过境迁&#xff0c;我…

开源模型应用落地-模型微调-语料采集-数据核验(三)

一、前言 在自然语言处理(NLP)的快速发展中,语料采集作为基础性的步骤显得尤为重要。它不仅为机器学习模型提供了所需的训练数据,还直接影响模型的性能和泛化能力。随着数据驱动技术的不断进步,如何有效并高效地收集、清洗和整理丰富多样的语料,已成为研究者和工程师们亟…

西门子智能从站

CPU1511作为CPU1513的智能IO设备_1511cpu-CSDN博客 掉站&#xff1a; 1511F作为智能从站其下挂的各子站设备掉站-通信与网络组件-找答案-西门子中国 同时做io控制器和智能从站&#xff1a; 1500PLC 同时做IO控制器和IO智能设备和DCS进行通讯-SIMATIC S7-1500系列-找答案-…

C++语言学习(3): type 的概念

type 的概念 C中的变量拥有类型&#xff0c; 这是显然的。 实际上&#xff0c;每个 object&#xff0c; 每个 reference&#xff0c; 每个 function&#xff0c; 每个 expression &#xff0c; 都有对应的 type &#xff08;类型&#xff09;&#xff1a; Each object, refer…

动手学LLM(ch2)——文本数据处理

前言 在这里&#xff0c;您将学习如何为训练大型语言模型&#xff08;LLMs&#xff09;准备输入文本。这包括将文本分割成单个词汇和子词汇token&#xff0c;然后将它们编码成向量表示&#xff0c;供大型语言模型&#xff08;LLM&#xff09;使用。您还将了解字节对编码等高级…

通信工程学习:什么是TFTP简单文件传输协议

TFTP&#xff1a;简单文件传输协议 TFTP&#xff08;Trivial File Transfer Protocol&#xff0c;简单文件传输协议&#xff09;是一种轻量级的文件传输协议&#xff0c;主要用于在计算机网络中传输小型文件。以下是对TFTP的详细解释&#xff1a; 一、TFTP简单文件传输协议的定…

无人机专业除理论外,飞手执照、组装、调试实操技术详解

无人机专业的学习除了丰富的理论知识外&#xff0c;飞手执照的获取、无人机的组装与调试等实操技术也是至关重要的。以下是对这些方面的详细解析&#xff1a; 一、无人机飞手执照 1. 必要性 法规要求&#xff1a;根据《民用无人驾驶航空器系统驾驶员管理暂行规定》等相关法规…

HTB:Oopsie[WriteUP]

目录 连接至HTB服务器并开启靶机 1.With what kind of tool can intercept web traffic? 2.What is the path to the directory on the webserver that returns a login page? 3.What can be modified in Firefox to get access to the upload page? 4.What is the acc…

关于TF-IDF的一个介绍

在这篇文章中我将介绍TF-IDF有关的一些知识&#xff0c;包括其概念、应用场景、局限性以及相应的代码。 一、概念 TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff09;是一种广泛用于信息检索和文本挖掘中的统计方法&#xff0c;用于评估一个词在一个文…

线路交换与分组交换的深度解析

1. 线路交换 原理 线路交换是一种在通信双方之间建立固定通信路径的方式。当用户发起通信时&#xff0c;网络为其分配一条专用的物理通道&#xff0c;这条通道在整个通话过程中保持不变。这意味着在通话期间&#xff0c;其他用户无法使用这条线路。 优点 稳定性&#xff1a…

在职场,没人告诉你的人情世故

职场中&#xff0c;想要过得游刃有余&#xff0c;就必须懂一些人情世故和处事原则。今天&#xff0c;给大家分享个人认为非常重要的5点人情世故&#xff0c;希望能帮你在职场里少吃点亏、多份从容。 01 不要空口道谢 在职场中&#xff0c;别人帮了你&#xff0c;口头道谢是基…

【GO语言】卡尔曼滤波例程

本文给出一个简单的卡尔曼滤波的 Go 语言实现示例&#xff0c;以及相应的讲解文档。 源代码 package mainimport ("fmt" )type KalmanFilter struct {x float64 // 状态估计P float64 // 估计误差协方差F float64 // 状态转移矩阵H float64 //…

在2核2G服务器安装部署MySQL数据库可以稳定运行吗?

阿里云2核2G服务器可以安装MySQL数据库吗&#xff1f;当然可以&#xff0c;并且可以稳定运行MySQL数据库&#xff0c;目前阿里云服务器网aliyunfuwuqi.com使用的就是阿里云2核2G服务器&#xff0c;在云服务器上安装MySQL数据库&#xff0c;可以稳定运行。 目前阿腾云用于运行M…

AWS IoT Core for Amazon Sidewalk

目录 1 前言2 AWS IoT2.1 准备条件2.2 创建Credentials2.2.1 创建user2.2.2 配置User 2.3 本地CLI配置Credentials 3 小结 1 前言 在测试Sidewalk时&#xff0c;device发送数据&#xff0c;网关接收到&#xff0c;网关通过网络发送给NS&#xff0c;而此处用到的NS是AWS IoT&am…

html中的文本标签(含标签的实现案例)

目录 1.标题标签 2.标题标签的align属性 3.段落标签 4.水平线标签hr 5.换行标签br 6.文本样式标签font ​编辑7.文本格式化标签 8.文本语义标签 1&#xff09;时间time标签 2&#xff09;文本高亮Mark标签 3&#xff09;cite标签 9.特殊字符标签 10.图像标签img 附录&#xff…

前端登录页面验证码

首先&#xff0c;在el-form-item里有两个div&#xff0c;各占一半&#xff0c;左边填验证码&#xff0c;右边生成验证码 <el-form-item prop"code"><div style"display: flex " prop"code"><el-input placeholder"请输入验证…