前言
前面的博客有寫過如果使用TCP搭建一個客戶端與服務器,連接並互發信息,這裏主是演示,如何把客戶端的圖像發往服務器,服務器得到圖像後,按指令做不同的處理,並返回給客戶端處理之後的結果,客戶端只負責打開和發送圖像,所有關於圖像的圖像的處理,比如灰度圖像,人臉檢測啊,都經過服務器處理之後返回給客戶端。而服務器用來處理圖像的是OpenCV庫,
代碼註釋比較多,具體流程可以看源碼,就應該瞭解。
代碼
1.客戶端
client.h
#ifndef CLIENT_H
#define CLIENT_H
#include <QMainWindow>
#include <QTcpSocket>
#include <QHostAddress>
#include <QMessageBox>
#include <QFile>
#include <QFileDialog>
#include <QCompleter>
#include <QBuffer>
#include <QDebug>
#include <QTextEdit>
#include <QImageReader>
#include <QTextCursor>
#include <QColor>
QT_BEGIN_NAMESPACE
namespace Ui { class Client; }
QT_END_NAMESPACE
class Client : public QMainWindow
{
Q_OBJECT
public:
Client(QWidget *parent = nullptr);
~Client();
void initUI();
void insertImage(QTextEdit *ui_text, QImage &image);
void transformImage(QImage &image,qint64 mask);
QByteArray getImageData(const QImage &image);
void transformImage(QImage &image, QByteArray &block,int mask);
QImage getImage(const QString &data);
QByteArray imageToBuffer(QImage &image);
private slots:
void on_buttonConnect_clicked();//連接服務器
void on_buttonDisconnect_clicked();//斷開連接
void on_buttonOpenImage_clicked();//打開圖像
void readServerMessage();//讀取消息
void on_buttonFaceDetection_clicked();//人臉檢測
void on_buttonGaryImage_clicked();//灰度圖像
void on_buttonCannyImage_clicked();//邊緣檢測
private:
QTcpSocket *tcpClient;
QImage image;
Ui::Client *ui;
QByteArray outBlock; //數據緩衝區,即存放每次要發送的數據塊
int totalBytes; // 發送數據的總大小
int mask;
QString fileName;
int imageSize;
int bytesReceived;
};
#endif // CLIENT_H
client.cpp
#include "client.h"
#include "ui_client.h"
Client::Client(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::Client)
{
ui->setupUi(this);
initUI();
tcpClient = new QTcpSocket(this);
//取消原有連接
tcpClient->abort();
bytesReceived = 0;
imageSize = 0;
connect(tcpClient, SIGNAL(readyRead()), this, SLOT(readServerMessage()));
}
//UI界面相關
void Client::initUI()
{
ui->buttonDisconnect->setEnabled(false);
}
Client::~Client()
{
delete ui;
}
//連接服務器
void Client::on_buttonConnect_clicked()
{
tcpClient->connectToHost(ui->lineEditServerIP->text(),ui->lineEditServerPorts->text().toInt());
if(tcpClient->waitForConnected(1000))
{
ui->buttonConnect->setEnabled(false);
ui->buttonDisconnect->setEnabled(true);
ui->textEditStatus->append("連接服務器成功!");
}
else
{
ui->textEditStatus->append("連接失敗,請檢查IP地址和端口!");
}
}
//斷開
void Client::on_buttonDisconnect_clicked()
{
tcpClient->disconnectFromHost();
//斷開成功
if (tcpClient->state() == QAbstractSocket::UnconnectedState || tcpClient->waitForDisconnected(1000))
{
ui->buttonConnect->setEnabled(true);
ui->textEditStatus->append("連接已斷開!");
ui->buttonDisconnect->setEnabled(false);
ui->buttonConnect->setEnabled(true);
}
else
{
ui->textEditStatus->append("無法斷開與服務器的連接!");
}
}
void Client::on_buttonFaceDetection_clicked()
{
}
void Client::on_buttonGaryImage_clicked()
{
transformImage(image,outBlock,10);
if(image.isNull())
{
ui->textEditStatus->append("當前圖像不能發送!");
return;
}
if(outBlock.size() != 0)
{
tcpClient->write(outBlock);
ui->textEditStatus->append("圖像發送成功!");
ui->textEditInput->clear();
}
}
void Client::on_buttonCannyImage_clicked()
{
transformImage(image,outBlock,9);
if(image.isNull())
{
ui->textEditStatus->append("當前圖像不能發送!");
return;
}
if(outBlock.size() != 0)
{
tcpClient->write(outBlock);
ui->textEditStatus->append("圖像發送成功!");
ui->textEditInput->clear();
}
}
//接收服務器端的信息並顯示
void Client::readServerMessage()
{
QDataStream in(tcpClient);
in.setVersion(QDataStream::Qt_5_7);
QImage imageData;
QString imageContent;
// 如果已接收到的數據小於16個字節,保存到文件頭結構
if (bytesReceived <= sizeof(int)*3)
{
if((tcpClient->bytesAvailable() >= sizeof(int)*3)&& (imageSize == 0))
{
// 接收數據總大小信息和文件名大小信息
in >>mask>> totalBytes >> imageSize;
bytesReceived += sizeof(int) * 3;
}
if((tcpClient->bytesAvailable() >= imageSize) && (imageSize != 0))
{
// 接收文件,並建立文件
in >> imageContent;
imageData = getImage(imageContent);
if(imageData.isNull())
{
ui->textEditStatus->append("沒有接收圖像數據!");
}
insertImage(ui->textEditAccept,imageData);
bytesReceived += imageSize;
if(bytesReceived == totalBytes)
{
ui->textEditStatus->append("接收文件成功");
totalBytes = 0;
bytesReceived = 0;
imageSize = 0;
}
}
}
}
//QTextEdit顯示圖像
void Client::insertImage(QTextEdit *ui_text_edit, QImage &image)
{
QUrl Uri;
QTextDocument * textDocument = ui_text_edit->document();
textDocument->addResource( QTextDocument::ImageResource, Uri, QVariant ( image ) );
QTextCursor cursor = ui_text_edit->textCursor();
QTextImageFormat imageFormat;
imageFormat.setWidth( image.width() );
imageFormat.setHeight( image.height() );
cursor.insertImage(imageFormat);
}
//打開圖像按鍵
void Client::on_buttonOpenImage_clicked()
{
fileName = QFileDialog::getOpenFileName(this);
if (!fileName.isEmpty())
{
image = QImage(fileName);
if(image.isNull())
{
ui->textEditStatus->append("打開圖像失敗!");
return;
}
else
{
QBuffer buffer(&outBlock);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "JPG");
insertImage(ui->textEditInput,image);
ui->textEditStatus->append("打開圖像成功!");
}
}
else
{
ui->textEditStatus->append("打開路徑失敗,請確定是否是圖像路徑!");
return;
}
}
//圖像轉換
/*QImage &image 輸入圖像
*QByteArray &block 輸出流
*int mask 指令
*/
void Client::transformImage(QImage &image, QByteArray &block,int mask)
{
int total_size = 0;
QDataStream sendOut(&block, QIODevice::WriteOnly);
sendOut.setVersion(QDataStream::Qt_5_7);
//獲得圖片數據
QString imageData = getImageData(image);
// 保留總大小信息空間、圖像大小信息空間,然後輸入圖像信息
sendOut << int(0) << int(0) << int(0) << imageData;
// 這裏的總大小是總大小信息、圖像大小信息和實際圖像信息的總和
total_size += block.size();
sendOut.device()->seek(0);
int image_size = int((block.size() - sizeof(int)*3));
// 返回outBolock的開始,用實際的大小信息代替兩個qint64(0)空間
sendOut << mask << total_size << image_size;
}
//圖像轉Base 64
QByteArray Client::getImageData(const QImage &image)
{
QByteArray imageData;
//開緩衝區
QBuffer buffer(&imageData);
//存入緩衝區
image.save(&buffer, "jpg");
//轉成Base64
imageData = imageData.toBase64();
return imageData;
}
//Base64 轉圖像
QImage Client::getImage(const QString &data)
{
QByteArray imageData = QByteArray::fromBase64(data.toLatin1());
QImage image;
image.loadFromData(imageData);
return image;
}
2.服務器端
server.h
#ifndef SERVER_H
#define SERVER_H
#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
#include <QNetworkInterface>
#include <QMessageBox>
#include <QFileDialog>
#include <iostream>
#include <QBuffer>
#include <opencv2/opencv.hpp>
#include <QTextEdit>
#include <QFile>
QT_BEGIN_NAMESPACE
namespace Ui { class Server; }
QT_END_NAMESPACE
class Server : public QMainWindow
{
Q_OBJECT
public:
Server(QWidget *parent = nullptr);
~Server();
void initUI();//界面初始化
QImage base64ToQImage(const QString &data);//base64轉QImage
void insertImage(QTextEdit *ui_text_edit, QImage &image);//QTextEdit顯示圖像
cv::Mat QImage2cvMat(QImage &image);//QImage轉Mat
QImage cvMat2QImage(const cv::Mat& mat);//Mat轉QImage
void transformImage(QImage &image, QByteArray &block,int mask);//封裝發送數據
QByteArray QImageToBase64(const QImage &image);//QImage轉Base64
void edgeDetection(cv::Mat &cv_src,cv::Mat &cv_dst);
private:
QTcpServer *tcpServer;//服務器類
QList<QTcpSocket*> tcpClient;
QTcpSocket *currentClient;
int mask,total_size;
int total_bytes;
int bytes_received;
int image_size;
QString image_content;
QByteArray out_block; //數據緩衝區,即存放每次要發送的數據塊
QImage image_data;
QString file_name;
QImage image;
QByteArray buffer_all;
private slots:
void on_buttonMonitor_clicked();//監聽事件
void on_buttonDisconnect_clicked();//斷開事件
void newConnectionSlot();//新客戶端連接事件
void readMessageData();//接收信息
void on_buttonOpenImage_clicked();//打開圖像
void on_buttonSendMessage_clicked();//發送圖像
private:
Ui::Server *ui;
};
#endif // SERVER_H
server.cpp
#include "server.h"
#include "ui_server.h"
Server::Server(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::Server)
{
ui->setupUi(this);
initUI();
tcpServer = new QTcpServer(this);
image_size = 0;
total_bytes = 0;
bytes_received = 0;
//有新的連接時的槽函數
connect(tcpServer,SIGNAL(newConnection()),this, SLOT(newConnectionSlot()));
}
void Server::initUI()
{
ui->buttonDisconnect->setEnabled(false);
}
Server::~Server()
{
delete ui;
}
//啓動服務器開始監聽
void Server::on_buttonMonitor_clicked()
{
//監聽所有IP地址
bool monitor = tcpServer->listen(QHostAddress::Any,ui->lineEditServerPorts->text().toInt());
//按鍵狀態
if(monitor)
{
ui->buttonMonitor->setEnabled(false);
ui->buttonDisconnect->setEnabled(true);
ui->textEditStatus->append("開始監端口......");
}
else
{
ui->textEditStatus->append("啓動服務器失敗!");
}
}
//有新客戶端連接時
void Server::newConnectionSlot()
{
//返回套接字指針
currentClient = tcpServer->nextPendingConnection();
tcpClient.append(currentClient);
ui->comboBoxIP->addItem(tr("%1:%2").arg(currentClient->peerAddress().toString().split("::ffff:")[1])\
.arg(currentClient->peerPort()));
//讀取消息處理
connect(currentClient, SIGNAL(readyRead()), this, SLOT(readMessageData()));
}
//斷開與所有客戶端的連接
void Server::on_buttonDisconnect_clicked()
{
//如果有客戶端連接
if(tcpClient.length() > 0)
{
for(int i = 0; i < tcpClient.length();i++)
{
//斷開
tcpClient.at(i)->disconnectFromHost();
//等待
bool dis = tcpClient.at(i)->waitForDisconnected(1000);
if(dis)
{
//刪除客戶端
tcpClient.removeAt(i);
tcpServer->close();
ui->buttonMonitor->setEnabled(true);
ui->buttonDisconnect->setEnabled(false);
ui->textEditStatus->append("斷開連接成功!");
ui->buttonMonitor->setEnabled(true);
}
else
{
ui->textEditStatus->append("斷開連接失敗!");
}
}
}
else
{
ui->textEditStatus->append("當前沒有連接的客戶端!");
ui->buttonMonitor->setEnabled(true);
}
}
//接收消息並顯示到界面
void Server::readMessageData()
{
//由於readyRead信號並未提供SocketDecriptor,所以需要遍歷所有客戶端
//遍歷所有連接的客戶端
for(int i=0; i<tcpClient.length(); i++)
{
QDataStream in(tcpClient.at(i));
// 如果已接收到的數據小於16個字節,保存到文件頭結構
if (bytes_received <= sizeof(int)*3)
{
if((currentClient->bytesAvailable() >= sizeof(int)*3)&& (image_size == 0))
{
// 接收數據總大小信息和文件名大小信息
in >>mask>>total_bytes >> image_size;
bytes_received += sizeof(int) * 3;
ui->textEditStatus->append("開始接收圖像......");
}
if((currentClient->bytesAvailable() >= image_size) && (image_size != 0))
{
// 接收文件,並建立文件
in >> image_content;
image_data = base64ToQImage(image_content);
insertImage(ui->textEditAccept,image_data);
bytes_received += image_size;
if(!image_data.isNull())
{
ui->textEditStatus->append("接收文件成功");
//判斷轉過來的指令,對圖像進行處理
switch (mask)
{
case 9:
{
QByteArray out;
//使用opencv處理圖像
cv::Mat cv_src = QImage2cvMat(image_data);
cv::Mat cv_dst;
edgeDetection(cv_src,cv_dst);
QImage qt_image = cvMat2QImage(cv_dst);
//處理完成之後返回給客戶端
transformImage(qt_image,out,9);
//返回給之前發送信息的客戶端
tcpClient.at(i)->write(out);
ui->textEditStatus->append("處理並返回圖像成功!");
mask = 0;
break;
}
case 10:
{
QByteArray out;
//使用opencv處理圖像
cv::Mat cv_src = QImage2cvMat(image_data);
cv::Mat cv_dst;
cv::cvtColor(cv_src,cv_dst,cv::COLOR_BGR2GRAY);
QImage qt_image = cvMat2QImage(cv_dst);
//處理完成之後返回給客戶端
transformImage(qt_image,out,10);
//返回給之前發送信息的客戶端
tcpClient.at(i)->write(out);
ui->textEditStatus->append("處理並返回圖像成功!");
mask = 0;
break;
}
}
}
if(bytes_received == total_bytes)
{
total_bytes = 0;
bytes_received = 0;
image_size = 0;
}
}
}
}
}
void Server::edgeDetection(cv::Mat &cv_src, cv::Mat &cv_dst)
{
cv::Mat cv_gray,cv_edge;
cv::cvtColor(cv_src,cv_gray,cv::COLOR_BGR2GRAY);
cv::blur(cv_gray, cv_gray, cv::Size(3, 3));
//調用Canny算子
cv::Canny(cv_edge, cv_dst, 10, 30, 3);
}
//Base64轉QImage
QImage Server::base64ToQImage(const QString &data)
{
QByteArray imageData = QByteArray::fromBase64(data.toLatin1());
QImage image;
image.loadFromData(imageData);
return image;
}
//QTextEdit顯示圖像
void Server::insertImage(QTextEdit *ui_text_edit, QImage &image)
{
QUrl Uri;
QTextDocument * textDocument = ui_text_edit->document();
textDocument->addResource( QTextDocument::ImageResource, Uri, QVariant ( image ) );
QTextCursor cursor = ui_text_edit->textCursor();
QTextImageFormat imageFormat;
imageFormat.setWidth( image.width() );
imageFormat.setHeight( image.height() );
cursor.insertImage(imageFormat);
}
// QImage轉換成cv::Mat
cv::Mat Server::QImage2cvMat(QImage &image)
{
cv::Mat mat;
switch (image.format())
{
case QImage::Format_ARGB32:
case QImage::Format_RGB32:
case QImage::Format_ARGB32_Premultiplied:
mat = cv::Mat(image.height(), image.width(), CV_8UC4, (void*)image.constBits(), image.bytesPerLine());
break;
case QImage::Format_RGB888:
mat = cv::Mat(image.height(), image.width(), CV_8UC3, (void*)image.constBits(), image.bytesPerLine());
cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB);
break;
case QImage::Format_Indexed8:
mat = cv::Mat(image.height(), image.width(), CV_8UC1, (void*)image.constBits(), image.bytesPerLine());
break;
}
return mat;
}
// cv::Mat轉換成QImage
QImage Server::cvMat2QImage(const cv::Mat& mat)
{
if (mat.type() == CV_8UC1) // 單通道
{
QImage image(mat.cols, mat.rows, QImage::Format_Indexed8);
image.setColorCount(256); // 灰度級數256
for (int i = 0; i < 256; i++)
{
image.setColor(i, qRgb(i, i, i));
}
uchar *pSrc = mat.data; // 複製mat數據
for (int row = 0; row < mat.rows; row++)
{
uchar *pDest = image.scanLine(row);
memcpy(pDest, pSrc, mat.cols);
pSrc += mat.step;
}
return image;
}
else if (mat.type() == CV_8UC3) // 3通道
{
const uchar *pSrc = (const uchar*)mat.data; // 複製像素
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); // R, G, B 對應 0,1,2
return image.rgbSwapped(); // rgbSwapped是爲了顯示效果色彩好一些。
}
else if (mat.type() == CV_8UC4) // 4通道
{
const uchar *pSrc = (const uchar*)mat.data; // 複製像素
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_ARGB32); // B,G,R,A 對應 0,1,2,3
return image.copy();
}
else
{
return QImage();
}
}
//轉換封裝圖像數據
void Server::transformImage(QImage &image, QByteArray &block,int mask)
{
int total = 0;
QDataStream sendOut(&block, QIODevice::WriteOnly);
sendOut.setVersion(QDataStream::Qt_5_7);
//獲得圖片數據
QString imageData = QImageToBase64(image);
// 保留總大小信息空間、圖像大小信息空間,然後輸入圖像信息
sendOut << int(0) << int(0) << int(0) << imageData;
// 這裏的總大小是總大小信息、圖像大小信息和實際圖像信息的總和
total += block.size();
sendOut.device()->seek(0);
// 返回outBolock的開始,用實際的大小信息代替兩個qint64(0)空間
sendOut << mask << total << int((block.size() - sizeof(int)*3));
}
QByteArray Server::QImageToBase64(const QImage &image)
{
QByteArray imageData;
//開緩衝區
QBuffer buffer(&imageData);
//存入緩衝區
image.save(&buffer, "jpg");
//轉成Base64
imageData = imageData.toBase64();
return imageData;
}
//打開圖像
void Server::on_buttonOpenImage_clicked()
{
file_name = QFileDialog::getOpenFileName(this);
if (!file_name.isEmpty())
{
image = QImage(file_name);
if(image.isNull())
{
ui->textEditStatus->append("打開圖像失敗!");
return;
}
else
{
insertImage(ui->textEditInput,image);
ui->textEditStatus->append("打開圖像成功!");
ui->buttonSendMessage->setEnabled(true);
}
}
else
{
ui->textEditStatus->append("打開路徑失敗,請確定是否是圖像路徑!");
return;
}
}
void Server::on_buttonSendMessage_clicked()
{
transformImage(image_data,out_block,10);
//如果選擇全部發送信息
if(ui->comboBoxIP->currentIndex() == 0)
{
for(int i = 0; i < tcpClient.length(); i++)
{
tcpClient.at(i)->write(out_block);
ui->textEditStatus->append("信息發送成功!");
ui->textEditInput->clear();
}
}
//指定接收的客戶端
/*else
{
//得到選擇的IP地址
QString client_IP = ui->comboBoxIP->currentText().split(":").at(0);
//得到端口
int client_port = ui->comboBoxIP->currentText().split(":").at(1).toInt();
//遍歷連接到的客戶端
for(int i = 0; i < tcpClient.length(); i++)
{
if(tcpClient[i]->peerAddress().toString().split("::ffff:")[1]==client_IP\
&& tcpClient[i]->peerPort()==client_port)
{
tcpClient.at(i)->write(input_data.toLatin1());
ui->textEditStatus->append("發送信息到:"+client_IP+"成功!");
//ui->textEditInput->clear();
input_data.clear();
return; //ip:port唯一,無需繼續檢索
}
}
}*/
}
3.運行效果