本编利用Qt实现一个网络摄像头功能,包含一个服务端和一个客户端,服务端用于将USB摄像头转换为一个IP摄像头,当有客户端连接时,将其捕获到的图像通过TCP发送出去;客户端运行在Linux板子上,用于查看摄像头的实时画面。
1 必备基础知识
本篇需要编写一个服务器和客户端,关于TCP服务器/客户端的基础知识,可参考这篇:Linux进程间通信详解(七) socket套接字基础
注意,Qt中对Socket的操作进行了进一步的封装,其基本思想还是一样的。
下面就来看一下Qt中如何实现TCP Socket通信。
1.1 QTcpSocket与QTcpServer
这里整理Qt中TCP Socket的使用方法,配合Qt的信号与槽机制,即可实现服务端/客户端数据的收发处理。
1.2 QCamera相关
QCamer,获取当前系统可用的摄像头 类似获取串口
QCamerInfo,获取当前系统可用的摄像头 类似获取串口
QCameraViewfinder,取景框类,摄像头的实时画面显示到这个里面
QCameraImageCapture,图像录制类,与QCamer 配合使用可进行拍照
2 Win平台上测试
首先在Windows平台上用Qt Creator编写服务端和客户端程序,并运行测试。
2.1 服务器端
先来看下服务器端的最终效果:
- 左侧是摄像头的显示界面
- 可以切换不同的摄像头作为视频源(笔记本自带的摄像头与USB外接的摄像头)
- 可以切换摄像头的显示分辨率
- 可以选择开启或关闭摄像头的IP服务
2.1.1 摄像头画面显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this);
QComboBox *pCamType = new QComboBox(); m_pComboBox = ui->cbBox_resolution; pCamType = ui->cbBox_cameras; pCamType->clear();
cameraList = QCameraInfo::availableCameras(); foreach(const QCameraInfo &cameraInfo, cameraList) { qDebug() << "CameraInfo:" << cameraInfo; pCamType->addItem(cameraInfo.description()); }
m_pCamViewFind = new QCameraViewfinder(this); m_pCamViewFind->setGeometry(10, 10, W, H); m_pCamViewFind->show();
m_pCam = new QCamera(this); m_pCam->setViewfinder(m_pCamViewFind); m_pCam->start(); }
|
2.1.2 创建Socket服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| void Widget::on_btn_IPServer_toggled(bool checked) { if (checked) { m_pServer = new QTcpServer(this); if (!m_pServer->listen(QHostAddress::Any, 12345)) { QMessageBox::critical(this, "error", "listen port failed"); exit(0); } qDebug() << "start IP server";
m_pTimer = new QTimer(this); connect(m_pServer, SIGNAL(newConnection()), this, SLOT(new_client())); connect(m_pTimer, SIGNAL(timeout()), this, SLOT(timer_slot())); m_pTimer->start(100);
ui->btn_IPServer->setText("关闭IP服务"); } else { qDebug() << "stop IP server"; m_pServer->close(); delete m_pServer;
ui->btn_IPServer->setText("开启IP服务"); } }
|
2.1.3 读取图像并发送给客户端
先定义一下图像传送结构体和传送状态:
1 2 3 4 5 6 7 8 9 10 11 12 13
| enum TransStatus{ TS_IDLE, TS_RUNNING, TS_FIRST_DATA, };
class ImgData { public: char data[LEN] = {0}; int totalLen = 0; int hasSentLen = 0; TransStatus stats = TS_IDLE; };
|
具体的实现过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| void Widget::read_data() { QString str = m_pClient->readAll(); ImgData *pData = (ImgData*)m_pClient->userData(0); QString s("newImage:%1");
if (str == "new_request") { qDebug() << "read_data, new_request, d->len:" << pData->totalLen << "d->stats:" << pData->stats; if ((pData->totalLen > 0) && (pData->stats==TS_IDLE)) { pData->stats = TS_RUNNING; m_pClient->write(s.arg(pData->totalLen).toUtf8()); pData->hasSentLen = 0; } else { pData->stats = TS_FIRST_DATA; } } else if (str == "ack") { int len_send = P_LEN;
if (pData->hasSentLen >= pData->totalLen) { qDebug() << "read_data, send done! lenSent:" << pData->hasSentLen << "len" << pData->totalLen; return; }
if ((pData->hasSentLen + P_LEN) > pData->totalLen) { len_send = pData->totalLen - pData->hasSentLen; }
qDebug() << "read_data, ack, write len:" << len_send;
pData->hasSentLen += m_pClient->write(pData->data + pData->hasSentLen, len_send); if (pData->hasSentLen >= pData->totalLen) { pData->stats = TS_IDLE; pData->totalLen = 0; } } }
|
需要注意的是,图像是需要分包传送的,最后一包一般都不是设定的最大长度,需要计算一下最后一包的数据长度。
2.2 客户端
先来看下客户端的最终效果:
- 右侧是摄像头画面的显示框
- 可以修改要连接的服务端的IP地址
- 可以选择开启或关闭网络摄像头
2.2.1 创建Socket连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void Widget::on_pushButton_toggled(bool checked) { if (checked) { QString ip = ui->lineEdit->text(); m_pSocket->connectToHost(ip, 12345); if (!m_pSocket->waitForConnected(1000)) { QMessageBox::critical(this, "error", "server connection failed"); return; }
ui->pushButton->setText("关闭"); m_iRecvLen = 0; m_pSocket->write("new_request"); qDebug("on_bnt_connect_clicked, new_request"); } else { m_pSocket->close(); ui->pushButton->setText("打开"); } }
|
2.3.2 接收服务端的图像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void Widget::read_data() { int ret; QTime qTime; static int i = 0;
ret = m_pSocket->read(m_pData + m_iRecvLen, P_LEN); if (0 == strncmp("newImage", m_pData + m_iRecvLen, 8)) { m_iImgLen = atoi(m_pData + m_iRecvLen + 9); i++; } else { m_iRecvLen += ret; if (m_iRecvLen >= m_iImgLen) { QString timestamp = QString::number(QDateTime::currentMSecsSinceEpoch()); update();
return; } }
m_pSocket->write("ack"); }
|
2.3.3 将图像显示出来
1 2 3 4 5 6 7 8 9 10 11 12 13
| void Widget::paintEvent(QPaintEvent *event) { QPixmap map;
if ((m_iRecvLen >= m_iImgLen) && (m_iImgLen > 0)) { map.loadFromData((uchar *)m_pData, m_iImgLen); QPainter p(this); p.drawPixmap(140, 0, 640, 480, map); m_pSocket->write("new_request"); m_iRecvLen = 0; } }
|
3 嵌入式Linux平台上测试
3.1 交叉编译
将客户端程序的源代码拷贝到Ubunu中进行交叉编译,具体编译过程可参考之前的文章:
嵌入式Qt-动手编写并运行自己的第1个ARM-Qt程序
本篇的实验环境,继续使用的是烧录了野火i.MX6ULL自带的系统固件Linux板子,需要通过SSH的方式将编译的程序再发送到板子中,SSH传输文件的操作可参考上篇文章:
嵌入式Qt-控制硬件:滑动条控制RGB灯
3.2 实验演示
https://www.bilibili.com/video/BV12G4y1a7za
4 总结
本篇介绍了如何用Qt实现一个网络摄像头功能,通过服务端将USB摄像头转换为一个IP摄像头,Linux板子中的客户端来连接服务器,将摄像头的实时画面显示出来。