Bootstrap

使用gRPC实现跨网络、跨语言点云处理服务

本文将介绍如何使用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()

 

 

;