本文将介绍如何使用gRPC框架来构建一个点云处理服务,该服务可以实现跨网络、夸语言的高效点云数据传输和处理。
准备 .proto
文件
在gRPC中,.proto
文件用于定义服务接口和消息格式。对于点云处理服务,我们需要定义一个服务接口,该接口包含处理点云数据的函数和结构。
syntax = "proto3";
package pointcloud;
// 简单的点云数据结构,实际应用中可能需要更复杂的结构
message Point {
float x = 1;
float y = 2;
float z = 3;
}
// 处理参数
message ProcessingParameters {
float parameter1 = 1;
}
// 状态响应结构
message StatusResponse {
bool success = 1;
string message = 2;
}
// 点云处理结构
message PointCloudChunk {
repeated Point points = 1;
// 状态响应字段
StatusResponse status = 2;
}
// 定义一个统一的消息类型来封装所有可能的消息
message StreamMessage {
// 使用 oneof 来定义多个字段,只有一个字段会被使用
oneof payload {
PointCloudChunk chunk = 1;
ProcessingParameters parameters = 2;
}
}
// 定义一个服务,用于处理点云数据流
service StreamService {
// 参数为 StreamMessage 类型的流,返回值也是 StreamMessage 类型的流
rpc SendPointCloud(stream StreamMessage) returns (stream StreamMessage) {}
}
生成C++ grpc接口文件:
其中./grpc_test替换为实际的项目路径,H:/software/grpc/bin/grpc_cpp_plugin.exe替换为实际的grpc安装目录中找到的grpc_cpp_plugin.exe
protoc.exe --grpc_out="./gprc_test" \
--cpp_out="./gprc_test" \
-I "./gprc_test/protos" \
--plugin=protoc-gen-grpc="H:/software/grpc/bin/grpc_cpp_plugin.exe" \
"./gprc_test/protos/hello_world.proto"
生成Python grpc接口文件:
其中./grpc_test替换为实际的项目路径
python -m grpc_tools.protoc -I ./grpc_test --python_out=./grpc_test --grpc_python_out=./grpc_test ./grpc_test/streaming.pr
oto
实现客户端
客户端采用C++编写,完整代码如下:
#include "streaming.grpc.pb.h"
#include <grpc++/grpc++.h>
#include <iostream>
#include <pcl/io/ply_io.h>
#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using pointcloud::Point;
using pointcloud::PointCloudChunk;
using pointcloud::ProcessingParameters;
using pointcloud::StreamMessage;
using pointcloud::StreamService;
class StreamClient
{
public:
StreamClient(std::shared_ptr<Channel> channel)
: stub_(StreamService::NewStub(channel))
{
}
// 调用服务器端接收点云的RPC方法
void SendPointCloud(const std::string& filename)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// 创建一个PLYReader对象
pcl::PLYReader reader;
// 使用reader从文件中读取点云数据到cloud
if (reader.read(filename, *cloud) == -1)
{
PCL_ERROR("Couldn't read file\n");
return;
}
ClientContext context;
auto stream = stub_->SendPointCloud(&context);
// 假设发送处理参数
StreamMessage parameter_msg;
parameter_msg.mutable_parameters()->set_parameter1(0.06); // 参数
stream->Write(parameter_msg);
// 发送点云数据块
const size_t chunkSize = 1000;
PointCloudChunk chunk;
// 创建发送点的消息
StreamMessage chunk_msg;
for (size_t i = 0; i < cloud->points.size(); i++)
{
// 添加点到chunk中,其中mutable_chunk()方法返回一个指向PointCloudChunk的指针
Point* p = chunk_msg.mutable_chunk()->add_points();
p->set_x(cloud->points[i].x);
p->set_y(cloud->points[i].y);
p->set_z(cloud->points[i].z);
// 如果chunk已满或者已经处理完所有点,就发送chunk
if ((i + 1) % chunkSize == 0 || i + 1 == cloud->points.size())
{
if (!stream->Write(chunk_msg))
{
break;
}
chunk_msg.mutable_chunk()->clear_points(); // 清除points以便下一个块
}
}
// 发送完所有点后,调用WritesDone()方法,告诉服务器端数据发送完毕
stream->WritesDone();
// 从服务器接收处理后的点云和状态信息
StreamMessage response_msg;
pcl::PointCloud<pcl::PointXYZ>::Ptr processedCloud(new pcl::PointCloud<pcl::PointXYZ>);
while (stream->Read(&response_msg))
{
// 处理服务器端返回的点云数据
if (response_msg.has_chunk())
{
std::cout << response_msg.chunk().points_size() << " points received" << std::endl;
for (const auto& point : response_msg.chunk().points())
{
pcl::PointXYZ pclPoint;
pclPoint.x = point.x();
pclPoint.y = point.y();
pclPoint.z = point.z();
processedCloud->push_back(pclPoint);
}
}
}
// 完成通信
Status status = stream->Finish();
if (!status.ok())
{
std::cerr << "PointCloud stream failed: " << status.error_message() << std::endl;
}
else
{
std::cout << "Processed points is: " << processedCloud->size() << std::endl;
pcl::io::savePCDFileASCII("processed.pcd", *processedCloud);
}
}
private:
std::unique_ptr<StreamService::Stub> stub_;
};
int main(int argc, char** argv)
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <pcd file>" << std::endl;
return -1;
}
grpc::ChannelArguments channelArgs;
// 设置发送消息的最大大小为10MB
channelArgs.SetMaxSendMessageSize(10 * 1024 * 1024);
// 创建一个Channel对象, 参数为:服务器地址和端口,凭证,和ChannelArguments对象
auto channel = grpc::CreateCustomChannel("192.168.30.232:50051", grpc::InsecureChannelCredentials(), channelArgs);
// 创建一个StreamClient对象
StreamClient client(channel);
// 调用客户端的SendPointCloud方法
client.SendPointCloud(argv[1]);
return 0;
}
C++ 客户端主要功能
1)读取点云数据
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// 创建一个PLYReader对象
pcl::PLYReader reader;
// 使用reader从文件中读取点云数据到cloud
if (reader.read(filename, *cloud) == -1)
{
PCL_ERROR("Couldn't read file\n");
return;
}
2)调用服务端接口,发送点云数据和参数。考虑到点云数据可能会比较大,把数据进行分块处理,使用grpc的双向流式模式进行数据交互。
ClientContext context;
auto stream = stub_->SendPointCloud(&context);
// 假设发送处理参数
StreamMessage parameter_msg;
parameter_msg.mutable_parameters()->set_parameter1(0.06); // 参数
stream->Write(parameter_msg);
// 发送点云数据块
const size_t chunkSize = 1000;
PointCloudChunk chunk;
// 创建发送点的消息
StreamMessage chunk_msg;
for (size_t i = 0; i < cloud->points.size(); i++)
{
// 添加点到chunk中,其中mutable_chunk()方法返回一个指向PointCloudChunk的指针
Point* p = chunk_msg.mutable_chunk()->add_points();
p->set_x(cloud->points[i].x);
p->set_y(cloud->points[i].y);
p->set_z(cloud->points[i].z);
// 如果chunk已满或者已经处理完所有点,就发送chunk
if ((i + 1) % chunkSize == 0 || i + 1 == cloud->points.size())
{
if (!stream->Write(chunk_msg))
{
break;
}
chunk_msg.mutable_chunk()->clear_points(); // 清除points以便下一个块
}
}
// 发送完所有点后,调用WritesDone()方法,告诉服务器端数据发送完毕
stream->WritesDone();
3)获取处理结果,并保存处理后的点云到文件
// 从服务器接收处理后的点云和状态信息
StreamMessage response_msg;
pcl::PointCloud<pcl::PointXYZ>::Ptr processedCloud(new pcl::PointCloud<pcl::PointXYZ>);
while (stream->Read(&response_msg))
{
// 处理服务器端返回的点云数据
if (response_msg.has_chunk())
{
std::cout << response_msg.chunk().points_size() << " points received" << std::endl;
for (const auto& point : response_msg.chunk().points())
{
pcl::PointXYZ pclPoint;
pclPoint.x = point.x();
pclPoint.y = point.y();
pclPoint.z = point.z();
processedCloud->push_back(pclPoint);
}
}
}
// 完成通信
Status status = stream->Finish();
if (!status.ok())
{
std::cerr << "PointCloud stream failed: " << status.error_message() << std::endl;
}
else
{
std::cout << "Processed points is: " << processedCloud->size() << std::endl;
pcl::io::savePCDFileASCII("processed.pcd", *processedCloud);
}
4) 连接服务端,其中192.168.30.232为服务端的ip地址
grpc::ChannelArguments channelArgs;
// 设置发送消息的最大大小为10MB
channelArgs.SetMaxSendMessageSize(10 * 1024 * 1024);
// 创建一个Channel对象, 参数为:服务器地址和端口,凭证,和ChannelArguments对象
auto channel = grpc::CreateCustomChannel("192.168.30.232:50051", grpc::InsecureChannelCredentials(), channelArgs);
// 创建一个StreamClient对象
StreamClient client(channel);
// 调用客户端的SendPointCloud方法
client.SendPointCloud(argv[1]);
实现服务端
服务端使用Python实现。完整代码如下:
from concurrent import futures
import grpc
import streaming_pb2
import streaming_pb2_grpc
import open3d as o3d
import numpy as np
# 定义服务端类,继承自streaming_pb2_grpc.StreamServiceServicer
class StreamServiceServicer(streaming_pb2_grpc.StreamServiceServicer):
def SendPointCloud(self, request_iterator, context):
voxel_size = 0.01 # 默认体素大小
all_points = [] # 存储所有点云数据
# 从客户端接收参数和点云数据
for message in request_iterator:
# 判断消息类型
if message.HasField("parameters"):
# 处理接收到的参数
voxel_size = message.parameters.parameter1
elif message.HasField("chunk"):
# 处理点云数据
points = message.chunk.points
# 将点云数据转换为列表
for point in points:
all_points.append([point.x, point.y, point.z])
# 使用Open3D创建点云
cloud = o3d.geometry.PointCloud()
# 将点云数据转换为Open3D点云格式
cloud.points = o3d.utility.Vector3dVector(np.array(all_points))
# 体素降采样
cloud_filtered = cloud.voxel_down_sample(voxel_size=voxel_size)
# 分块发送处理后的点云数据
chunk_size = 1000 # 定义每个块的大小
# 获取处理后的点云数据,并转换为numpy数组
points_filtered = np.asarray(cloud_filtered.points)
# 获取点云数据的数量
num_points = points_filtered.shape[0]
# 分块发送处理后的点云数据
for i in range(0, num_points, chunk_size):
chunk_points = points_filtered[i : i + chunk_size] # 获取当前块的点云数据
response_message = streaming_pb2.StreamMessage() # 创建响应消息
for pt in chunk_points:
# 将处理后的点云数据添加到响应消息中
p = response_message.chunk.points.add()
p.x, p.y, p.z = pt.tolist()
response_message.chunk.status.success = True # 设置处理状态为成功
# 设置响应消息的状态信息
response_message.chunk.status.message = (
"Processed point cloud data successfully."
)
# 发送响应消息
yield response_message
print("Processed point cloud data sent to client.")
# 创建gRPC服务器
def serve():
# 创建gRPC服务器, 并指定线程池
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 将服务端类添加到服务器中
streaming_pb2_grpc.add_StreamServiceServicer_to_server(
StreamServiceServicer(), server
)
# 监听本地的50051端口
server.add_insecure_port("0.0.0.0:50051")
# 启动服务器
server.start()
# 服务器一直运行
server.wait_for_termination()
if __name__ == "__main__":
serve()
Python服务端主要功能
1)从客户端接收参数和点云数据
for message in request_iterator:
# 判断消息类型
if message.HasField("parameters"):
# 处理接收到的参数
voxel_size = message.parameters.parameter1
elif message.HasField("chunk"):
# 处理点云数据
points = message.chunk.points
# 将点云数据转换为列表
for point in points:
all_points.append([point.x, point.y, point.z])
2)使用Open3D对点云进行滤波处理
cloud = o3d.geometry.PointCloud()
# 将点云数据转换为Open3D点云格式
cloud.points = o3d.utility.Vector3dVector(np.array(all_points))
# 体素降采样
cloud_filtered = cloud.voxel_down_sample(voxel_size=voxel_size)
3)发送处理后的点云数据。同样需要分块发送
for i in range(0, num_points, chunk_size):
chunk_points = points_filtered[i : i + chunk_size] # 获取当前块的点云数据
response_message = streaming_pb2.StreamMessage() # 创建响应消息
for pt in chunk_points:
# 将处理后的点云数据添加到响应消息中
p = response_message.chunk.points.add()
p.x, p.y, p.z = pt.tolist()
response_message.chunk.status.success = True # 设置处理状态为成功
# 设置响应消息的状态信息
response_message.chunk.status.message = (
"Processed point cloud data successfully."
)
# 发送响应消息
yield response_message
4)启动grpc服务
# 创建gRPC服务器, 并指定线程池
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 将服务端类添加到服务器中
streaming_pb2_grpc.add_StreamServiceServicer_to_server(
StreamServiceServicer(), server
)
# 监听本地的50051端口
server.add_insecure_port("0.0.0.0:50051")
# 启动服务器
server.start()
# 服务器一直运行
server.wait_for_termination()