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

前言

  • 高帧率摄像头往往应用在很多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.mfbz.cn/a/885938.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

1.5 测试用例

欢迎大家订阅【软件测试】 专栏&#xff0c;开启你的软件测试学习之旅&#xff01; 文章目录 前言1 测试用例介绍2 测试用例编写3 案例分析4 执行测试用例 前言 测试用例的设计和编制是软件活动中最重要的工作。本文详细讲解了测试用例的基本概念以及如何编写测试用例。 本篇文…

深入掌握 Protobuf 与 RPC 的高效结合:实现C++工程中的高效通信

目录 一、Protobuf与RPC框架的通信流程概述二、Protobuf与RPC在C中的实际应用2.1 定义 .proto 文件2.2 编译 .proto 文件生成C代码2.3 实现服务器端逻辑2.4 实现客户端逻辑2.5 使用CMake构建工程2.6 编译与运行2.7 关键组件解析2.8 序列化与反序列化的实现 三、关键实现与解析四…

Redis: Sentinel哨兵监控架构及环境搭建

概述 在主从模式下&#xff0c;我们通过从节点只读模式提高了系统的并发能力并发不断增加&#xff0c;只需要扩展从节点即可&#xff0c;只要主从服务器之间&#xff0c;网络连接正常主服务器就会将写入自己的数据同步更新给从服务器&#xff0c;从而保证主从服务器的数据相同…

网络安全 DVWA通关指南 DVWA Weak Session IDs(弱会话)

DVWA Weak Session IDs&#xff08;弱会话&#xff09; 文章目录 DVWA Weak Session IDs&#xff08;弱会话&#xff09;Low LevelMedium LevelHigh LevelImpossible Level 参考文献 WEB 安全靶场通关指南 相关阅读 Brute Force (爆破) Command Injection&#xff08;命令注入…

C# 无边框窗体,加阴影效果、多组件拖动、改变大小等功能完美实现优化版效果体验

一、预览效果 国庆节第一天,祝祖国繁荣昌盛! 1.1 效果图 (WinForm无边框窗体,F11可全屏) 拖动窗体时半透明效果(拖动时参考窗体后面释放位置) 说明:本功能的实现基于网友的原型完善而来,更多代码可以参考他的文章 h

Golang | Leetcode Golang题解之第449题序列化和反序列化二叉搜索树

题目&#xff1a; 题解&#xff1a; type Codec struct{}func Constructor() (_ Codec) { return }func (Codec) serialize(root *TreeNode) string {arr : []string{}var postOrder func(*TreeNode)postOrder func(node *TreeNode) {if node nil {return}postOrder(node.Le…

量产小妙招---向量间的Project和Product

1 背景 在日常工作中&#xff0c;不管是在感知或者规控&#xff0c;或者其它的模块中&#xff0c;经常需要处理两个向量之间的关系&#xff0c;这就引入了本篇博客和读者朋友们讨论的一个话题&#xff1a;Project和Product。 2 Project和Product 向量间的Project和Product在定义…

C++语言学习(2): name lookup 的概念

何谓 name lookup C 中很重要的一个概念&#xff1a;name lookup。 当编译器在遇到一个 name 的时候&#xff0c; 会做查找&#xff08;lookup&#xff09;&#xff0c;会把引入这个 name 的声明和它关联起来&#xff0c;具体来说&#xff0c;又包含两种类型的 lookup&#xf…

【学习笔记】手写 Tomcat 八

目录 一、NIO 1. 创建 Tomcat NIO 类 2. 启动 Tomcat 3. 测试 二、解析请求信息 三、响应数据 创建响应类 修改调用的响应类 四、完整代码 五、测试 六、总结 七、获取全部用户的功能 POJO 生成 POJO 1. 在 Dao 层定义接口 2. 获取用户数据 3. 在 Service 层定…

ArcGIS与ArcGIS Pro去除在线地图服务名单

我们之前给大家分享了很多在线地图集&#xff0c;有些地图集会带有制作者信息&#xff0c;在布局制图的时候会带上信息影响出图美观。 一套GIS图源集搞定&#xff01;清新规划底图、影像图、境界、海洋、地形阴影图、导航图 比如ArcGIS&#xff1a; 比如ArcGIS Pro&#xff1a…

.Net 基于IIS部署blazor webassembly或WebApi

1.安装IIS(若安装&#xff0c;请忽略) 选择:控制面板–>程序–>程序和功能 选择:启动或关闭Windows功能&#xff0c;勾选相关项&#xff0c;再点击确定即可。 2.安装Hosting Bundle 以.net6为例&#xff0c;点击连接https://dotnet.microsoft.com/en-us/download/dot…

zabbix7.0创建自定义模板的案例详解(以监控httpd服务为例)

前言 服务端配置 链接: rocky9.2部署zabbix服务端的详细过程 环境 主机ip应用zabbix-server192.168.10.11zabbix本体zabbix-client192.168.10.12zabbix-agent zabbix-server(服务端已配置) 创建模板 模板组直接写一个新的&#xff0c;不用选择 通过名称查找模板&#xf…

详解CSS中的伪元素

4.3 伪元素 可以把样式应用到文档树中根本不存在的元素上。 ::first-line 文本中的第一行 ::first-letter 文本中的第一个字母 ::after 元素之后添加 ::before 元素之前 代码&#xff1a; <!DOCTYPE html> <html> <head><meta charset"utf-8&q…

测试用例的举例

1. 基于测试公式设计测试用例 通过功能&#xff0c;性能&#xff0c;安全性&#xff0c;界面&#xff0c;安全性&#xff0c;易用&#xff0c;兼容对于一个水杯进行测试用例的设计&#xff1b; 对于一个软件的测试用例设计&#xff1a; 功能&#xff1a;软件本质上能够用来干什…

本科生已不够 AI公司雇佣各领域专家训练大模型

9月29日消息&#xff0c;人工智能模型的性能在很大程度上依赖于其训练数据的质量。传统方法通常是雇用大量低成本劳动力对图像、文本等数据进行标注&#xff0c;以满足模型训练的基本需求。然而&#xff0c;这种方式容易导致模型在理解和生成信息时出现“幻觉”现象&#xff0c…

【递归】11. leetcode 129 求根节点到叶节点数字之和

1 题目描述 题目链接&#xff1a; 求根节点到叶节点数字之和 2 解答思路 第一步&#xff1a;挖掘出相同的子问题 &#xff08;关系到具体函数头的设计&#xff09; 第二步&#xff1a;只关心具体子问题做了什么 &#xff08;关系到具体函数体怎么写&#xff0c;是一个宏观…

追随 HarmonyOS NEXT,Solon v3.0 将在10月8日发布

Solon &#xff08;开放原子开源基金会&#xff0c;孵化项目&#xff09;原计划10月1日发布 v3.0 正式版。看到 HarmonyOS NEXT 将在 10月8日启用公测&#xff0c;现改为10月8日发布以示庆贺。另外&#xff0c;Solon 将在2025年启动“仓颉”版开发&#xff08;届时&#xff0c;…

数据仓库的建设——从数据到知识的桥梁

数据仓库的建设——从数据到知识的桥梁 前言数据仓库的建设 前言 企业每天都在产生海量的数据&#xff0c;这些数据就像无数散落的珍珠&#xff0c;看似杂乱无章&#xff0c;但每一颗都蕴含着潜在的价值。而数据仓库&#xff0c;就是那根将珍珠串起来的线&#xff0c;它能够把…

WebSocket消息防丢ACK和心跳机制对信息安全性的作用及实现方法

WebSocket消息防丢ACK和心跳机制对信息安全性的作用及实现方法 在现代即时通讯&#xff08;IM&#xff09;系统和实时通信应用中&#xff0c;WebSocket作为一种高效的双向通信协议&#xff0c;得到了广泛应用。然而&#xff0c;在实际使用中&#xff0c;如何确保消息的可靠传输…

解决Windows远程桌面 “为安全考虑,已锁定该用户账户,原因是登录尝试或密码更改尝试过多,请稍后片刻再重试,或与系统管理员或技术支持联系“问题

根本原因就是当前主机被通过远程桌面输入了过多的错误密码&#xff0c;被系统锁定。这种情况多数是你的服务器远程桌面被人试图攻击了&#xff0c;不建议取消系统锁定策略。如果阿里云或者腾讯云主机&#xff0c;只需要在管理后台通过管理终端或者VNC登陆一次&#xff0c;锁定即…