【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 1class 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.rhkb.cn/news/437188.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RDI ADCP命令与ASCII输出结构

RDI ADCP命令与ASCII输出结构 一、RDI垂直式ADCP:1.1固定命令&#xff1a;1.2 向导命令 二、RDI水平式ADCP三、ADCP 公共目录四、常用BBTalk命令五、ADCP的ASCII输出数据文件、流量与数据结构5.1 ASCII类输出&#xff1a;5.2 ASCII 输出数据文件头5.3 ASCII 输出数据集5.4 导航…

Llama 3.2来了,多模态且开源!AR眼镜黄仁勋首批体验,Quest 3S头显价格低到离谱

如果说 OpenAI 的 ChatGPT 拉开了「百模大战」的序幕&#xff0c;那 Meta 的 Ray-Ban Meta 智能眼镜无疑是触发「百镜大战」的导火索。自去年 9 月在 Meta Connect 2023 开发者大会上首次亮相&#xff0c;短短数月&#xff0c;Ray-Ban Meta 就突破百万销量&#xff0c;不仅让马…

位运算(6)_只出现一次的数字 II

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 位运算(6)_只出现一次的数字 II 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 …

psutil库的使用说明

前言 psutil是一个跨平台的库&#xff0c;用于获取系统的进程和系统利用率&#xff08;包括 CPU、内存、磁盘、网络等&#xff09;信息。 目录 安装 应用场景 常用方法 一、系统信息相关函数 二、进程信息相关函数 三、网络信息相关函数 四、其他实用函数 使用样例 监控应…

Could not find com.mapbox.mapboxsdk:mapbox-android-accounts:0.7.0.解决

AndroidStudio编译APK出现如下错误&#xff1a; Could not find com.mapbox.mapboxsdk:mapbox-android-accounts:0.7.0. 出现上面错误原因是因为没有打开对应的仓库导致的&#xff0c; 手动添加如下创建地址可解决&#xff1a; maven { url https://maven.aliyun.com/repos…

Windows远程Kylin系统-xrdp

Windows远程Kylin系统-xrdp 一. 查看开放端口 查看是否有3389端口二. 安装xrdp Kylin对应的是centos8 下载链接&#xff1a;https://rhel.pkgs.org/8/epel-x86_64/xrdp-0.10.1-1.el8.x86_64.rpm.html rpm -Uvh 包名 systemctl start xrdp 启动服务 systemctl enable xrdp …

【HTML5】html5开篇基础(4)

1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; Hello, Hello~ 亲爱的朋友们&#x1f44b;&#x1f44b;&#xff0c;这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章&#xff0c;请别吝啬你的点赞❤️❤️和收藏&#x1f4d6;&#x1f4d6;。如果你对我的…

解决问题AttributeError: “safe_load“ has been removed, use

解决问题AttributeError: "safe_load" has been removed, use~ 1. 问题描述2. 解决方法 1. 问题描述 在复现cdvae代码时&#xff0c;运行 python scripts/compute_metrics.py --root_path MODEL_PATH --tasks recon gen opt评估模型时&#xff0c;出现以下问题。 …

Python批量下载PPT模块并实现自动解压

日常工作中&#xff0c;我们总是找不到合适的PPT模板而烦恼。即使有免费的网站可以下载&#xff0c;但是一个一个地去下载&#xff0c;然后再批量解压进行查看也非常的麻烦&#xff0c;有没有更好方法呢&#xff1f; 今天&#xff0c;我们利用Python来爬取一个网站上的PPT&…

【ios】---swift开发从入门到放弃

swift开发从入门到放弃 环境swift入门变量与常量类型安全和类型推断print函数字符串整数双精度布尔运算符数组集合set字典区间元祖可选类型循环语句条件语句switch语句函数枚举类型闭包数组方法结构体 环境 1.在App Store下载Xcode 2.新建项目&#xff08;可以先使用这个&…

Hadoop HDFS命令操作实例

一.创建与查看HDFS目录 每次重启后&#xff0c;Jps和java -version执行出来的结果不符合就使用 source ~/.bash_profile 是在 Unix/Linux 系统上用来重新加载用户的 Bash 配置文件 ~/.bash_profile 的命令。这条命令的作用是使得当前的 Bash 环境重新读取并应用 ~/.bash_pro…

PHP安装后Apache无法运行的问题

问题 按照网上教程php安装点击跳转教程&#xff0c;然后修改Apache的httpd.conf文件&#xff0c;本来可以运行的Apache&#xff0c;无法运行了 然后在"C:\httpd-2.4.62-240904-win64-VS17\Apache24\logs\error.log"&#xff08;就是我下载Apache的目录下的logs中&am…

当AI成为作家,人工智能在写作领域的崛起

AI写作技术的应用正在多个领域展现出其强大的潜力和价值&#xff0c;它不仅极大地提升了内容创作的效率&#xff0c;还为创作者提供了一个全新的创作伙伴。 随着技术的进步&#xff0c;AI写作工具越来越能够理解复杂的语境和用户需求&#xff0c;帮助创作者生成高质量的内容。…

排水系统C++

题目&#xff1a; 样例解释&#xff1a; 1 号结点是接收口&#xff0c;4,5 号结点没有排出管道&#xff0c;因此是最终排水口。 1 吨污水流入 1 号结点后&#xff0c;均等地流向 2,3,5 号结点&#xff0c;三个结点各流入 1/3 吨污水。 2 号结点流入的 1/3​ 吨污水将均等地流向…

深度学习与数学归纳法

最近发现&#xff0c;深度学习可以分为两个主要的阶段&#xff0c;分别是前向推理以及反向传播&#xff0c;分别对应着网络的推理和参数训练两个步骤。其中推理有时候也称为归纳推理。 在做参数训练的时候&#xff0c;本质上是在利用历史数据求网络参数的先验分布&#xff1b; …

Java 基础语法 Day10

一、异常 1.1异常的基本处理 1.抛出异常&#xff1a;throw 2.捕获异常&#xff1a;try-catch 1.2异常的作用 1.定位程序bug的关键信息 2.可以作为方法内部的一种特殊返回值&#xff0c;通知给上层调用&#xff0c;方便处理 //需求&#xff1a;将两个数的除返回 public cla…

音视频入门基础:FLV专题(9)——Script Tag简介

一、SCRIPTDATA 根据《video_file_format_spec_v10_1.pdf》第75页到76页&#xff0c;如果某个Tag的Tag header中的TagType值为18&#xff0c;表示该Tag为Script Tag&#xff08;脚本Tag&#xff0c;又称Data Tag、SCRIPTDATA tag&#xff09;。这时如果Filter的值不为1表示未加…

UG NX二次开发(C++)-建模-采用NXOpen获取拉伸特征的信息

文章目录 1、前言2、创建一个特征3 采用NXOpen来实现拉伸特征信息的获取1、前言 UG NX二次开发过程中,大部分初学者喜欢用UFun函数来实现UG NX二次开发的功能,因为相较于NXOpen,UFun函数简单易懂;但是有时UFun函数如果初始值设置不好,出现的错误也比较难排查。比如对于拉…

Spark SQL分析层优化

导读&#xff1a;本期是《深入浅出Apache Spark》系列分享的第四期分享&#xff0c;第一期分享了Spark core的概念、原理和架构&#xff0c;第二期分享了Spark SQL的概念和原理&#xff0c;第三期则为Spark SQL解析层的原理和优化案例。本次分享内容主要是Spark SQL分析层的原理…

Redis篇(Redis原理 - 数据结构)(持续更新迭代)

目录 一、动态字符串 二、intset 三、Dict 1. 简介 2. Dict的扩容 3. Dict的rehash 4. 知识小结 四、ZipList 1. 简介 2. ZipListEntry 3. Encoding编码 五、ZipList的连锁更新问题 六、QuickList 七、SkipList 八、RedisObject 1. 什么是 redisObject 2. Redi…