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();//获取port
QString ip_t = ui->ip->text();//获取ip
bool 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();
// delete
m_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);;//延时10ms
qint32 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。这也再次证明了前面设想的主次缓冲区的想法。至少存在这么一个机制。
以上是经验之谈,都是测试出来的,没有去查其具体的机制,如有什么错误,欢迎大佬指证,大家要是有什么其他问题,也欢迎评论区留言讨论。