以前一直使用opencv来打开摄像头和获取图像数据,最近搜了一下,原来摄像头模块qt自己也封装了一个,就想着试一下看能不能用tcp实时传输摄像头数据
效果图:
分为服务端server和客户端Client两个程序,两者分别的功能如下:
Server:
1,获取摄像头列表,打开指定摄像头
2,建立tcp服务侦听,等待服务器的链接
3,设置定时器,每隔200毫秒获取一个摄像头的一个图像截图
4,将获取的图像截图图像,变成字节流后,压缩,分块发送给服务器
Client:
1,链接服务端
2,对发来的数据进行检验,一次没有发完则等待发完
3,对一次发来的所有数据进行解压缩还原,显示到界面上
主要用到的一些对象和库的使用方法
qt摄像头库
QCamera #摄像头类
QCameraViewfinder #摄像头图像存放,使用它来承载摄像头的资源
QCameraImageCapture #摄像头截图,使用它来获取摄像头图像的一帧
QCameraInfo #摄像头列表,使用他来存放本机的摄像头资源
qt Tcp库
QTcpServer #Qt的tcp服务端
QTcpSocket #Qt的tcp套接字
总体流程:
客户端(获取摄像头的图像数据)——进行压缩——发送给服务端——服务端解压缩——服务端接收显示
qt自己的摄像头模块总感觉比opencv打开的更清晰,而且更流畅,以后会尝试用qt自己封装的一些库做一些更有趣的实验。
上代码
服务端 mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include<QCamera>
#include<QCameraViewfinder>
#include<QCameraImageCapture>
#include<QCameraInfo>
#include<QTimer>
#include<QTcpServer>
#include<QTcpSocket>
#include<QBuffer>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
//将Qimage通过Tcp传递到子端
void To_client(int index,QImage ima);
private slots:
void on_pushButton_open_clicked();
void on_pushButton_close_clicked();
void newConnect();
void readClientRequest();
private:
Ui::MainWindow *ui;
QCamera *ca;
QCameraImageCapture *cap;
QList<QCameraInfo> cameras;
QTimer *timer;
QTcpServer *server;
QTcpSocket *socket;
quint16 clientRequestSize;
};
#endif // MAINWINDOW_H
服务端 mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ca = new QCamera();
cameras = QCameraInfo::availableCameras();
// foreach (const QCameraInfo &cameraInfo, cameras) {
// if (cameraInfo.deviceName() == "mycamera")
// ca = new QCamera(cameraInfo);
// }
foreach(const QCameraInfo &info,cameras)
{
qDebug()<<info.deviceName();
}
timer = new QTimer();
connect(timer,&QTimer::timeout,[&](){
cap->capture();
});
server = new QTcpServer(this);
server->listen(QHostAddress::Any,8888);
connect(server,&QTcpServer::newConnection,this,&MainWindow::newConnect);
//ServerListenSocket = new QTcpServer(this); //服务器端监听套接字 ServerListenSocket
clientRequestSize = 0;
socket = nullptr;
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_open_clicked()
{
//构造一下摄像头对象
ca = new QCamera(cameras[0],this);
//创建截图对象的内存 截图软件要和摄像头对象相互关联
cap = new QCameraImageCapture(ca);
//将截图信号与显示截图的槽函数关联一下
//连接 截图信号和显示截图的槽函数 一旦发出imageCaptured截图信号,就触发截图的槽函数
connect(cap,&QCameraImageCapture::imageCaptured,this,&MainWindow::To_client);
//将QCameraViewfinder绑定到一个控件上
QCameraViewfinder *v = new QCameraViewfinder(ui->label);
v->resize(ui->label->size());
ca->setViewfinder(v);
v->show();
//启动摄像头
ca->start();
}
void MainWindow::on_pushButton_close_clicked()
{
//启动时间循环
timer->start(20);
}
void MainWindow::To_client(int index,QImage ima)
{
//如果没有链接的情况,就直接退出,不至于闪退
if(socket == nullptr)
{
return;
}
QByteArray byte; //The QByteArray class provides an array of bytes.
QBuffer buf(&byte); //缓存区域
//QString imageSize = "image size is:" + QString::number(frame.cols*frame.rows * 3) + " Bytes";
//ui.info->addItem(imageSize);//图像的大小(字节数)
ima.save(&buf, "JPEG"); //将图像以jpeg的压缩方式压缩了以后保存在 buf当中
//QString jpegImageSize = "jpeg image size is " + QString::number(buf.size()) + " Bytes";
//ui.info->addItem(jpegImageSize); //压缩后的jpg图像的大小(字节数)
QByteArray ss = qCompress(byte, 1);//将压缩后的jpg图像 再用qCompress 压缩 ,第二个参数1-9,9是最大压缩率
//QString ssSize="ss's size is "+ QString::number(ss.size()) + " Bytes";
//ui.info->addItem(ssSize);//用qCompress 压缩后的数据大小(字节数)
//将压缩后的字节串数据编码成Base64方式,字节数会比压缩前稍微变多一些
QByteArray vv = ss.toBase64(); // QByteArray QByteArray::toBase64() const : Returns a copy of the byte array, encoded as Base64.
//QString vvSize = "vv's size is " + QString::number(vv.size()) + " Bytes";
//ui.info->addItem(vvSize); //编码后的数据的大小
QByteArray ba;
QDataStream out(&ba, QIODevice::WriteOnly); //二进制只写输出流
out.setVersion(QDataStream::Qt_5_10); //输出流的版本
/*当操作复杂数据类型时,我们就要确保读取和写入时的QDataStream版本是一样的,简单类型,比如char,short,int,char* 等不需要指定版本也行*/
/*上面这些编解码的过程肯定是会影响 时效性的,可以考虑只使用jpeg 压缩后就进行发送 。*/
out << (quint64)0; //写入套接字的经压缩-编码后的图像数据的大小
out << vv; //写入套接字的经压缩-编码后的图像数据
out.device()->seek(0);
out << (quint64)(ba.size() - sizeof(quint64));//写入套接字的经压缩-编码后的图像数据的大小
socket->write(ba); //将整块数据写入套接字
//update(); //更新界面
}
void MainWindow::newConnect()
{
//ui->textEdit->append("An new client is connected!");
socket = server->nextPendingConnection(); //返回已连接套接字对象
connect(socket, SIGNAL(readyRead()), this, SLOT(readClientRequest())); //将已连接套接字对象的准备好可读信号readyRead与 readClientRequest()槽函数连接
//connect(socket, SIGNAL(disconnected()), socket,SLOT(deleterLater())); //已连接套接字的断开信号与自身的稍后删除信号相连接
}
void MainWindow::readClientRequest()
{
QDataStream in(socket); //绑定套接字
in.setVersion(QDataStream::Qt_5_10); //指定版本
//如果客户端发送过来的第一段数据块的大小为0,说明确实是第一次交互
if (clientRequestSize == 0)
{
//客户端发送过来的第一段数据块的大小如果小于 64bit ,则说明:还未收到客户端发送过来的前64bit的数据,这64bit的数据存储了客户端第一次请求包的大小(字节数)
if (socket->bytesAvailable() < sizeof(quint16))
{
return; //返回,继续等待 接收数据,数据还在套接字缓存当中
}
else//如果 客户端发送过来的第一段数据块的大小>= 64bit 了
{
in >> clientRequestSize;//将数据的前64bit提取出来,存储到quint64 类型的clientRequestSize
}
}
if (socket->bytesAvailable() < clientRequestSize)//当前套接字缓冲区中存储的数据如果小于clientRequestSize个字节
{
return;//返回,继续等待 接收数据,数据还在套接字缓存当中
}
quint8 requestType;
in >> requestType;//从套接字缓冲区中读取 8bit的数据解释为quint8类型,存储到requestType中
if (requestType == 'R') //如果requestType是 'R' 字符R的ASCII值
{
connect(timer,&QTimer::timeout,[&](){
cap->capture();
});//将30ms时间到与发送数据的 SendData() 连接
}
}
客户端mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include<QTcpSocket>
#include<QString>
#include<QtNetwork>
#include<QMessageBox>
#include<QImage>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QTcpSocket tcpSocket; //连接套接字
QImage *img;
qint64 imageBlockSize;
protected:
void ShowImage(QByteArray ba);
private slots:
void connectToServer();
void sendRequest();
void ReceiveData();
void connectionCloseByServer();
void error();
void tcpConnected();
void on_pushButton_clicked(); //连接
void on_pushButton_2_clicked(); //请求
};
#endif // MAINWINDOW_H
客户端mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
imageBlockSize = 0; //一次接收的图像数据的大小(字节数)
connect(&tcpSocket, SIGNAL(disconnect()), this, SLOT(connectionCloseByServer()));//套接字的断开信号
connect(&tcpSocket, SIGNAL(readyRead()), this, SLOT(ReceiveData()));//套接字一次可读的触发信号
connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error()));//套接字的错误消息信号
}
MainWindow::~MainWindow()
{
delete ui;
}
//连接
void MainWindow::on_pushButton_clicked()
{
connectToServer();
bool k=connect(&tcpSocket, SIGNAL(connected()), this, SLOT(tcpConnected()));
}
//请求
void MainWindow::on_pushButton_2_clicked()
{
sendRequest();
}
void MainWindow::connectToServer()
{
//连接到本机的8888端口
tcpSocket.connectToHost(QHostAddress::LocalHost, 8888);//connectToHost是异步连接函数(不阻塞),一经调用结束立刻返回
ui->textEdit->append("connecting to LocalHost port 8888...");
//ui.connectToServer->setEnabled(false);
}
void MainWindow::sendRequest()//发送请求,请求视频图像序列
{
QByteArray requestMessage; //请求消息(字节数组)
QDataStream out(&requestMessage, QIODevice::WriteOnly);//只读输出流
out.setVersion(QDataStream::Qt_5_10);
out << quint16(0) << quint8('R');//将请求消息的大小、长度(字节数)与请求消息 'R'写入 输出数据流out
out.device()->seek(0);
out << quint16(requestMessage.size() - sizeof(quint16));
tcpSocket.write(requestMessage);//将输出数据流中的数据写入套接字
ui->textEdit->append("Sending request...");
//ui.requestVideo->setEnabled(false);
}
static int receiveCount = 0;//接收 readyRead()信号的触发计数
static int imageCount = 0; //接收到的图像数据的计数
void MainWindow::ReceiveData()
{
receiveCount++;
QString rCount = QString::number(receiveCount);
//ui.receiveCount->setText(rCount);
QByteArray message;//存放从服务器接收到的字节流数据
QDataStream in(&tcpSocket); //将客户端套接字与输入数据流对象in绑定
in.setVersion(QDataStream::Qt_5_10);//设置数据流的版本
/*接收端的 这部分控制逻辑很重要*/
if (imageBlockSize == 0)
{
//如果imageBlockSize == 0 则说明,一幅图像的大小信息还未传输过来
//uint64是8字节的8 Bytes 64bit
//判断接收的数据是否有8字节(文件大小信息)
//如果有则保存到basize变量中,没有则返回,继续接收数据
if (tcpSocket.bytesAvailable() < (int)sizeof(quint64))
{//一幅图像的大小信息还未传输过来
return;
}
in >> imageBlockSize;//一幅图像的大小信息 //先接受图片的大小
if (imageBlockSize == (quint64)0xFFFFFFFFFFFFFFFF)//视频结束的标注符
{
tcpSocket.close();
QMessageBox::information(this, tr("warning"), tr("the video is end!"));
return;
}
qDebug() << "imageBlockSize is " << imageBlockSize;
QString imageBlockS = "imageBlockSize is " + QString::number(imageBlockSize) + "Bytes!";
ui->textEdit->append(imageBlockS);
message.resize(imageBlockSize);
}
//如果没有得到一幅图像的全部数据,则返回继续接收数据
if (tcpSocket.bytesAvailable() < imageBlockSize)
{
return;
}
in >> message;//一幅图像所有像素的完整字节流
imageBlockSize = 0;//已经收到一幅完整的图像,将imageBlockSize置0,等待接收下一幅图像
imageCount++; //已接收的图像计数
QString iCount = QString::number(imageCount);
//ui.imageCount->setText(iCount);
ShowImage(message); //显示当前接收到的这一幅图像
}
void MainWindow::connectionCloseByServer()//服务端主动断开了已连接套接字
{
ui->textEdit->append("Error:Connection closed by server!");
tcpSocket.close();//关闭客户端套接字
//ui.connectToServer->setEnabled(true);
}
void MainWindow::error()
{
ui->textEdit->append(tcpSocket.errorString());
tcpSocket.close();
//ui.connectToServer->setEnabled(true);
}
void MainWindow::tcpConnected()//套接字已经建立连接信号的处理槽函数
{
//ui.requestVideo->setEnabled(true);
}
void MainWindow::ShowImage(QByteArray ba) //从接收到了字节流中,执行与服务器断相反的操作:解压缩、解释为图像数据
{
QString ss = QString::fromLatin1(ba.data(), ba.size());
QByteArray rc;
rc = QByteArray::fromBase64(ss.toLatin1());
QByteArray rdc = qUncompress(rc);
QImage img;
//img.loadFromData(rdc,"JPEG");//解释为jpg格式的图像
img.loadFromData(rdc);//解释为jpg格式的图像
ui->label->setPixmap(QPixmap::fromImage(img));
ui->label->resize(img.size());
update();
}
参考文档: