QT6.0以上版本实现实时图像传输

2024-06-04 03:20

本文主要是介绍QT6.0以上版本实现实时图像传输,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

    • 服务端
      • 开启摄像头,捕获存储图片
      • TCP图像传输
      • 延时函数
    • 客户端
      • 建立连接
      • 接收数据和处理
      • 缓冲区接收的一些想法

QT借助tcp实现图像传输,达到类似实时监控的目的。
QT到6.0以上后貌似原来的5.0的一些图像的捕获的函数都无法使用了,网上好像也没有人给出相关实现代码,查看qt文档找到的方法。
分为两个部分,一个为客户端,负责数据的接收和展示,服务端负责数据的发送。

服务端

1、开启摄像头,捕获图片到本地
2、开启服务,传输图片

开启摄像头,捕获存储图片

//头文件定义的类私有变量     
QList<QCameraDevice> cameras;
QCamera* camera;//摄像头设备
QImageCapture* imageCapture;
//头文件定义的类私有变量 
void MainWindow::camera_getimg()
{cameras = QMediaDevices::videoInputs();//获取可用摄像头设备列表for (const QCameraDevice &cameraDevice : cameras){qDebug() << cameraDevice.description();//摄像头的设备信息ui->history->append(cameraDevice.description()+" init success.");//          ui->Camerlist->addItem(cameraDevice.description());}QMediaCaptureSession *captureSession = new QMediaCaptureSession;camera = new QCamera(cameras.at(0));//cameras.at(0)是默认摄像头,也可以通过其他方式选择摄像头qDebug() << camera->cameraDevice().description();//摄像头的设备信息,名字//ui->history->append(camera->cameraDevice().description()+" using...");captureSession->setCamera(camera);//use the first one//    captureSession->setVideoOutput(show);//show ui 有专门用来设置显示ui的imageCapture = new QImageCapture(this);imageCapture->setFileFormat(QImageCapture::JPEG);captureSession->setImageCapture(imageCapture);imageCapture->setQuality(QImageCapture::NormalQuality);//质量选择imageCapture->setResolution(240,180);//设置图像尺寸imageCapture->captureToFile("D:/qt_rec.jpg");//捕获一次图像并存储的路径   camera->start();//启动摄像头
}

由于是实时获取图像并传输到客户端,客户端接收然后展示,需要配置定时器,过一段时间就捕获一次图片并发送,相关代码在tcp中实现。

TCP图像传输

//开启服务unsigned short port  = ui->port->text().toUShort();//获取portQString  ip_t = ui->ip->text();//获取ipbool sta = my_s->listen(QHostAddress(ip_t),port);//创建服务qDebug()<<my_s->errorString();if(sta){ser_sta = true;ui->history->append("server open success.");ui->start_bt->setText("关闭服务");// disable the button of start}else{ui->history->append(my_s->errorString());}

发送处理

connect(my_s,&QTcpServer::newConnection,this,[=](){// 自定义匿名的槽函数,用于获取连接的套接字对象m_tcp = my_s->nextPendingConnection();//m_status->setPixmap(QPixmap(":/img/status_1.png").scaled(20,20));//scaled 一个缩放函数,等比例//检测是否可以接收数据,也是信号量ui->history->append("a new client connected");connect(m_tcp,&QTcpSocket::readyRead,this,[=](){qDebug() << "cnt_sta: "<<cnt_sta;//read and show the tcp client's data.QByteArray data = m_tcp->readAll();//   m_tcp->write(data);ui->history->append("client: " + data);});connect(m_tcp,&QTcpSocket::disconnected,this,[=](){//disconnected ,set icon.m_status->setPixmap(QPixmap(":/img/status_0.png").scaled(20,20));//scaled 一个缩放函数,等比例m_tcp->close();
//            deletem_tcp->deleteLater();//释放对象,其实最后m_s释放的时候,他也会释放,这里手动释放,也可以使用delete。});//img//设置100ms 的定时器触发信号
//        uchar cout = 0;QTimer *timer = new QTimer(this);connect(timer, &QTimer::timeout, this, [=](){imageCapture->captureToFile("D:/qt_rec.jpg");//捕获并存储一帧图像cout++;QFile file("D:/qt_rec.jpg");QByteArray data;bool a= file.open(QIODevice::ReadOnly);if(a){data=file.readAll();file.close();}qint64 ssize= data.size();qDebug() <<"jpg size :"<<ssize;QString str_len = "img"+QString::number(ssize)+'\n';m_tcp->write(str_len.toUtf8());//发送图像大小信息//延时,防止过快的发送信号到达客户端,客户端一次性读取了长度信息和图像数据Delay_MSec(10);;//延时10msqint32  len=0;if(m_tcp->isValid())//发送图像数据  下面循环可以直接用len = m_tcp->write(data);替代while(ssize){len = m_tcp->write(data);qDebug() << len;ssize -= len;if(ssize<=0) break;}if(cout==3){cout = 0;//做另外的事情}});timer->start(100);//100ms一次});

延时函数

不阻塞延时

void MainWindow::Delay_MSec(unsigned int msec)
{QEventLoop loop;//定义一个新的事件循环QTimer::singleShot(msec, &loop, SLOT(quit()));//创建一个单次定时器,msec毫秒后执行槽函数,槽函数为循环的退出函数loop.exec();//事件循环开始执行,程序会卡在这里,直到定时时间到,循环退出
}

客户端

接收图片,存储,或者直接展示。
先存储后展示。

建立连接

    unsigned short port  = ui->port->text().toUShort();QString  ip_t = ui->ip->text();m_tcp->connectToHost(QHostAddress(ip_t),port);qDebug()<<"启动连接";qDebug()<<m_tcp->errorString();

接收数据和处理

bool tcp_sig =false;//0表示第一次读,1表示为接下来接收图片connect(m_tcp,&QTcpSocket::connected,this,[=](){qDebug()<<"连接成功";// 自定义匿名的槽函数,用于获取连接的套接字对象//检测是否可以接收数据,也是信号量});connect(m_tcp,&QTcpSocket::errorOccurred,this,[=](){qDebug()<<"连接错误";qDebug()<<m_tcp->error();//输出的错误信息更完整});connect(m_tcp,&QTcpSocket::readyRead,this,[=](){//read and show the tcp client's data.//qint32 len,allsize=0,nowsize = 0;if(tcp_sig==0){QByteArray rdata = m_tcp->readLine(1024);
//            qDebug()<<"recive some data len of"<< rdata.size();
//            qDebug()<< rdata;
//第一次读取区分数据类型,是图片还是其他数据if(rdata.startsWith("img")){tcp_sig = 1;//是图片,修改标志rdata.erase(rdata.cbegin(),rdata.cbegin()+3);//去除前向标志rdata.removeLast();//去除回车allsize = rdata.toUInt();qDebug()<< "rec imgsize = "<<allsize;}else if(rdata.startsWith("data")){//QString 其他数据处理rdata.erase(rdata.cbegin(),rdata.cbegin()+4);qDebug()<<" png err";}}else if(tcp_sig == 1){//读取图片Delay_MSec(10);//延时一定需要,根据具体情况进行设定大小,这个延时的目的是等待缓冲区接收完发送的数据while(1){QByteArray img_data = m_tcp->readAll();len  = img_data.size();qDebug()<<"img len size" << img_data.size() ;nowsize +=  len;qDebug()<<nowsize;
//                imgarr.append(img_data);if(nowsize>=allsize){QFile file("G:/qt_img/22.jpg");file.open(QIODevice::WriteOnly| QIODevice::Truncate);//QIODevice::Truncate这个必须要len  = file.write(img_data);
//                    img_data.clear();nowsize = 0;qDebug()<< "img loacal size = " <<len ;file.close();QImage image("G:/qt_img/22.jpg");if(image.isNull()){qDebug()<<"test and png err";}else{QPixmap pic=QPixmap::fromImage(image);ui->img_view->setPixmap(pic.scaled(ui->img_view->size(),Qt::KeepAspectRatio,Qt::SmoothTransformation));//有更高效的方法,此外还可以不需要先存储,直接将接收的数据转为图片进行显示}}if(len==0) break;}//QByteArray da = m_tcp->readAll();//本来是想出现错误,读取掉错乱数据,经过推测和测试没有作用tcp_sig = 0;nowsize = 0;}else{QByteArray da = m_tcp->readAll();//读取掉错乱数据}});

图片的接收和展示还可以进行简化,这是一个实现方式。整体上比较简单吧

其实传输过程中的主要问题就是图片接收的时候,有时候会出现失帧,和读取不全,图片只展示一半的问题,显然是数据没有读取完整,或者发送不完整。想通过图片大小的方式来确保读取完整的图片。
通过调试显示,发送是可以直接完全一次性发送的
接收有时候会出问题,一下多一下少。
而且通过循环的方式来不断读取缓冲区直到那么大小的数据的想法是错误的。这应该和QTTCP缓冲区接收的实现方式有问题,涉及到信号量readyRead,目前来看这个信号量是在服务端发送出数据,客户端缓冲区会接收数据,然后就会发送出这个信号量,但是他是接收到数据就发送了,也就是说不保证缓冲区已经完全接收完数据了。你此时通过信号量触发进行读取缓冲区接收的数据可能是不完整的,如果图片过大,而且存在网络延迟的情况下。这也是在接收的函数中要进行延时的原因,这个才算是解决问题的关键,非常重要

缓冲区接收的一些想法

此外,关于缓冲区接收,他似乎分为多个通道,看到文档中有指定通道进行读取数据, 但是我没有深入研究,不知道是怎么使用和实现的。
目前测出的情况是,当接收到readyRead,并使用read之类的读取函数时,在这个读取的槽函数中,你所读取的缓冲区似乎就是固定的了,相当于是某个时刻固定的。比如传来50k的数据,主缓冲区接收到了20k,此时发送出了readyRead信号,并触发了槽函数,这个过程假设又接收了10k数据(并行),但是还有20k数据没有接收(也有可能这个缓冲区是动态变化的,一开始20k,发现不够用,要动态扩展所以耗时更多),此时调用read之类的函数,相当于给你一个临时变量固定的一个带有30k数据的次缓冲区让你读取,当你将这个缓冲区读取完的时候,你就读不到数据了,应为次缓冲区数据已经被读取完了,如果此时你设立一个循环,如果读取的数据没有达到50k,就继续读,但是所读取的都是次缓冲区,这个是已经空了的,那么你就会进入死循环,一直无法退出。本人有幸进入过,很是掉头发。
主缓冲区和次缓冲区的存在首先是我自己猜想的,另外帮助理解是主缓冲区相当于定义的全局变量a,而次缓冲区可以理解为某个函数定义的和全局变量名字相同的局部变量a,而在这个函数中,访问变量a时是访问局部变量a,而不是全局变量a。主缓冲区就是全局变量a,次缓冲区是局部变量a。
所以说数据过大,或者延时高的情况,readyRead触发的读取槽函数时,需要先进行延时的处理,因为你一定调用了read函数,不管是read()、readAll()等,只要调用了read之类的函数,就会固定化次缓冲区的内容。为了确保接收完全,需要进行延迟。当然一次readyRead的触发,也意味着一次数据的传输到来。
观察过测试输出结果,如果你一开始读取30k,并调用了read函数,也就是主缓冲区还剩下20k数据,此时,又发送来10k数据,触发readyRead,在槽函数中调用readAll,这样可以读取到30k的数据,原来的20k加上新传输的10k。这也再次证明了前面设想的主次缓冲区的想法。至少存在这么一个机制。

以上是经验之谈,都是测试出来的,没有去查其具体的机制,如有什么错误,欢迎大佬指证,大家要是有什么其他问题,也欢迎评论区留言讨论。

这篇关于QT6.0以上版本实现实时图像传输的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1028916

相关文章

java实现docker镜像上传到harbor仓库的方式

《java实现docker镜像上传到harbor仓库的方式》:本文主要介绍java实现docker镜像上传到harbor仓库的方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 前 言2. 编写工具类2.1 引入依赖包2.2 使用当前服务器的docker环境推送镜像2.2

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Java easyExcel实现导入多sheet的Excel

《JavaeasyExcel实现导入多sheet的Excel》这篇文章主要为大家详细介绍了如何使用JavaeasyExcel实现导入多sheet的Excel,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录1.官网2.Excel样式3.代码1.官网easyExcel官网2.Excel样式3.代码

python实现对数据公钥加密与私钥解密

《python实现对数据公钥加密与私钥解密》这篇文章主要为大家详细介绍了如何使用python实现对数据公钥加密与私钥解密,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录公钥私钥的生成使用公钥加密使用私钥解密公钥私钥的生成这一部分,使用python生成公钥与私钥,然后保存在两个文

浏览器插件cursor实现自动注册、续杯的详细过程

《浏览器插件cursor实现自动注册、续杯的详细过程》Cursor简易注册助手脚本通过自动化邮箱填写和验证码获取流程,大大简化了Cursor的注册过程,它不仅提高了注册效率,还通过友好的用户界面和详细... 目录前言功能概述使用方法安装脚本使用流程邮箱输入页面验证码页面实战演示技术实现核心功能实现1. 随机

Golang如何对cron进行二次封装实现指定时间执行定时任务

《Golang如何对cron进行二次封装实现指定时间执行定时任务》:本文主要介绍Golang如何对cron进行二次封装实现指定时间执行定时任务问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录背景cron库下载代码示例【1】结构体定义【2】定时任务开启【3】使用示例【4】控制台输出总结背景

Golang如何用gorm实现分页的功能

《Golang如何用gorm实现分页的功能》:本文主要介绍Golang如何用gorm实现分页的功能方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录背景go库下载初始化数据【1】建表【2】插入数据【3】查看数据4、代码示例【1】gorm结构体定义【2】分页结构体

在Golang中实现定时任务的几种高效方法

《在Golang中实现定时任务的几种高效方法》本文将详细介绍在Golang中实现定时任务的几种高效方法,包括time包中的Ticker和Timer、第三方库cron的使用,以及基于channel和go... 目录背景介绍目的和范围预期读者文档结构概述术语表核心概念与联系故事引入核心概念解释核心概念之间的关系

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函

C++11作用域枚举(Scoped Enums)的实现示例

《C++11作用域枚举(ScopedEnums)的实现示例》枚举类型是一种非常实用的工具,C++11标准引入了作用域枚举,也称为强类型枚举,本文主要介绍了C++11作用域枚举(ScopedEnums... 目录一、引言二、传统枚举类型的局限性2.1 命名空间污染2.2 整型提升问题2.3 类型转换问题三、C