目錄
- 服務端
- 開啟攝像頭,捕獲存儲圖片
- 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。這也再次證明了前面設想的主次緩沖區的想法。至少存在這么一個機制。
以上是經驗之談,都是測試出來的,沒有去查其具體的機制,如有什么錯誤,歡迎大佬指證,大家要是有什么其他問題,也歡迎評論區留言討論。