前言
因业务需求,需要用到grpc架构来传输图片,在网上找了很久也找不到很好的例子,一开始确实很没有头绪,别人的例子都是传输什么文件呀,数组之类的基本类型数据的东西,可是mat类图片,这可咋整,用我们广东来说:扑街咯。但是冷静一想,图像不就是一个二维数组表示的吗,它就是个矩阵。我只要把里面的数据读出来,放到数组里面再传输不就可以了吗?嘿嘿嘿。本例子的代码有足够详细的注释。一般的程序靓仔应该可以看得懂了。如果这篇文章对你有帮助,请加个关注评论一下哟,
文章末尾有第二种高效的方法链接
准备
本例子的程序代码在win10+vs2017 平台上实测通过,需要配置grpc编译环境,以及opencv。没有配置的靓仔请转移到我的另外两章博客
grpc配置:
https://blog.csdn.net/liyangbinbin/article/details/100134465
。
opencv 编译好的lib和dll:
https://blog.csdn.net/liyangbinbin/article/details/100038824
protobuf文件
syntax = "proto3";
package namespace_uploadpic;
service upload_pic_servicer {
rpc Upload(stream ChunkOneLine) returns (Reply) {}
}
message Chunk
{
int32 pic_data0 = 1;
int32 pic_data1 = 2;
int32 pic_data2 = 3;
int32 pic_data3 = 4;
}
message imgparm
{
int32 i_type = 1;
int32 i_rows = 2;
int32 i_cols = 3;
int32 i_channel = 4;
}
message ChunkOneLine
{
repeated Chunk oneLineData=1;
imgparm pic_parm_data=2;
}
message Reply {
int32 length = 1;
}
首先Chunk里面存放的是一个像素的每个通道的值。最多4通道。而imgparm表示的是图像的参数,行,列,通道,类型。然后组合到ChunkOneLine去。
生成C++文件:
protoc –grpc_out=. –plugin=protoc-gen-grpc=grpc_cpp_plugin.exe uppic.proto
protoc –cpp_out=. uppic.proto
需要把protoc.exe,grpc_cpp_plugin.exe和.proto文件放在同个目录。protoc.exe,grpc_cpp_plugin.exe是配置grpc生成的。没有请返回文章头部。
服务器代码:
#include<string>
#include <iostream>
#include <grpc/grpc.h>
#include <grpc++/server.h>
#include <grpc++/server_builder.h>
#include <grpc++/server_context.h>
#include <grpc++/security/server_credentials.h>
#include "uppic.grpc.pb.h"
#include <time.h>
#include <chrono>
#include "opencv.hpp"
//命名空间
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::ServerReader;
using grpc::Status;
using grpc::Channel;
using namespace namespace_uploadpic;
using grpc::ClientContext;
//define GRPC_DEFAULT_MAX_RECV_MESSAGE_LENGTH(INT_MAX)
class upPicserver final :public namespace_uploadpic::upload_pic_servicer::Service
{
public:
//这个Upload是重写了rpc里面的方法
Status Upload(ServerContext *context, ServerReader<ChunkOneLine> *reader, Reply *reply);
};
Status upPicserver::Upload(ServerContext *context, ServerReader<ChunkOneLine> *reader, Reply *reply)
{
//记录当前时间
std::chrono::system_clock::time_point start_time =std::chrono::system_clock::now();
//定义接收的对象
ChunkOneLine oneLie;
imgparm imgp;
//开始读首行消息
reader->Read(&oneLie);
//首行信息是图像的参数(行,列,类型,通道)
//赋值
imgp = oneLie.pic_parm_data();
//根据接收到的参数,初始化一个图像
cv::Mat mat(imgp.i_rows(), imgp.i_cols(), imgp.i_type());
//i相当于rows
int i = 0;
while (reader->Read(&oneLie))
{
//j相当于cols
int j = 0;
//定义一个Chunk类型的数组onechunk,相当于一行的数据
::google::protobuf::RepeatedPtrField<Chunk>onechunk = oneLie.onelinedata();
//读取并赋值给刚才初始化的图像
for (auto ones: onechunk)
{
//获取mat图像(i,j)这点的指针
uchar *pd = mat.ptr<uchar>(i,j);
//赋值
switch (imgp.i_channel())
{
//单通道
case 1:
*pd = ones.pic_data0();
break;
//三通道
case 3:
pd[0] = ones.pic_data0();
pd[1] = ones.pic_data1();
pd[2] = ones.pic_data2();
break;
//四通道
case 4:
pd[0] = ones.pic_data0();
pd[1] = ones.pic_data1();
pd[2] = ones.pic_data2();
pd[3] = ones.pic_data3();
break;
default:
std::cerr << "channels error!" << imgp.i_channel()<< std::endl;
break;
}
j++;//这个j++,呃虽然看起来多余的,但是可别手痒删除了
}
i++;//同上
}
//再次记录当前时间
std::chrono::system_clock::time_point end_time = std::chrono::system_clock::now();
//duration_cast是个模板类,可以自定义转换类型,milliseconds就是要转换的单位,
//(end_time - start_time)把它转换成milliseconds,也就是毫秒
auto sec = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
//把消耗的时间返回给客户端
reply->set_length(sec.count());
cv::imshow("bb", mat);
cv::waitKey(1);
return grpc::Status::OK;
}
int main()
{
//创建一个用于响应的类
upPicserver service;
//监听的端口,前面的IP地址,似乎只有0,0,0,0和127.0.0.1可用
//应该是代表本地的IP吧
std::string add_ip("0.0.0.0:50051");
//创建一个服务类
ServerBuilder builder;
//监听,后面那个参数代表不使用ssl加密
builder.AddListeningPort(add_ip, grpc::InsecureServerCredentials());
//把我们自己写的响应的类挂上去
builder.RegisterService(&service);
//开始
std::unique_ptr<Server>server(builder.BuildAndStart());
std::cout << "Server listening on " << add_ip << std::endl;
server->Wait();
return 0;
}
客户端代码:
#pragma comment(lib,"ws2_32.lib")
#include <iostream>
#include <fstream>
#include <string>
#include <grpc/grpc.h>
#include <grpc++/server.h>
#include <grpc++/server_builder.h>
#include <grpc++/server_context.h>
#include <grpc++/security/server_credentials.h>
#include "uppic.grpc.pb.h"
#include <grpc++/channel.h>
#include <grpc++/client_context.h>
#include <grpc++/create_channel.h>
#include "opencv.hpp"
#include <memory.h>
#include <conio.h>
using grpc::Status;
using grpc::Channel;
using grpc::ClientContext;
using grpc::ClientWriter;
using namespace namespace_uploadpic;
using grpc::ClientContext;
class uppicIml
{
public:
//构造函数,创建一个频道,用于指向服务器
uppicIml(std::shared_ptr<Channel>channl) :stu_(upload_pic_servicer::NewStub(channl)) {}
void uppp()
{
//创建一个ChunkOneLine*的vector,用于存储图像的数据
std::vector<ChunkOneLine*>chunkonelie;
//读入一个图片
cv::Mat img = cv::imread("C:/Users/Administrator/Desktop/10698.tiff");
//调整容器的大小
chunkonelie.reserve(img.rows);
Chunk *dd1 = NULL;
imgparm * imgp = NULL;
ChunkOneLine *onedata = NULL;
onedata = new ChunkOneLine();
imgp = new imgparm();
//先把图像的信息赋值给imgp,作为首行
imgp->set_i_channel(img.channels());
imgp->set_i_cols(img.cols);
imgp->set_i_rows(img.rows);
imgp->set_i_type(img.type());
//存到vector容器中
onedata->set_allocated_pic_parm_data(imgp);
chunkonelie.push_back(onedata);
//把图片的数据读出来
for (int i = 0; i < img.rows; i++)
{
//这个用来存储一行的像素点
onedata = new ChunkOneLine();
for (int j = 0; j < img.cols; j++)
{
//增加一个像素点
dd1 = onedata->add_onelinedata();
//获取这点的指针
uchar *pd = img.ptr<uchar>(i, j);
switch (img.channels())
{
//单通道
case 1:
dd1->set_pic_data0(*pd); break;
//3通道
case 3:
dd1->set_pic_data0(int(pd[0]));
dd1->set_pic_data1(int(pd[1]));
dd1->set_pic_data2(int(pd[2]));
break;
//4通道
case 4:
dd1->set_pic_data0(pd[0]);
dd1->set_pic_data1(pd[1]);
dd1->set_pic_data2(pd[2]);
dd1->set_pic_data2(pd[3]);
break;
default:
std::cerr << "channels error!"<< img.channels()<<std::endl;
break;
}
}
//把整行放到容器中
chunkonelie.push_back(onedata);
}
//客户端的上下文,这个有点难理解,算是流程化东西吧,照样就行
ClientContext context;
//定义一个用来存储返回信息的变量
Reply reply;
//获得远程API(俗称远程方法)的指针
std::unique_ptr<ClientWriter<::namespace_uploadpic::ChunkOneLine>> writer=stu_->Upload(&context, &reply);
//开始写(发送)
for (ChunkOneLine *n : chunkonelie)
{
//每次发送一行,第一行是图像的信息
if (!writer->Write(*n))
break;
}
//写完了
writer->WritesDone();
//读取状态
grpc::Status status = writer->Finish();
if (status.ok())
{
std::cout << "数据传输完成\n";
std::cout << "传输时间为:" << reply.length();
}
else
{
std::cout << "数据传输失败\n";
}
if (onedata)
{
delete onedata;
onedata = nullptr;
}
}
private:
//这个是远程方法(API)的一个指针
std::unique_ptr<upload_pic_servicer::Stub>stu_;
};
int main()
{
//定义一类并初始化
//CreateChannel是创建一个频道,里面包括远程主机的地址和商品,第二个表示不加密
uppicIml upppp(grpc::CreateChannel("127.0.0.1:50051", grpc::InsecureChannelCredentials()));
//我们的方法写
upppp.uppp();
system("pause");
return 0;
}
结果
本地实测,传输一个3072×3072,bmp三通道的图像,大概9.00 MB (9,438,262 字节)大小,需要1秒的时间,一张3072×3072,11.8 MB (12,457,430 字节)tiff大小的图像大概949毫秒的时间。差不多1秒时间。而传输一张3072×1728,15.1 MB (15,925,302 字节)大小的bmp图像只需要567毫秒。可想,最耗时的是每个像素的读取,而不是数据的大小。这对于速度有要求的项目来说,太慢了。
有没有更加高效的方法呢,当然有了,请移师到我另外一篇博客上
:
https://blog.csdn.net/liyangbinbin/article/details/100571906