Implantação de Redes PointNet para Inferência com ONNX Runtime

Exportação de Pesos Treinados para Formato ONNX

A integração com frameworks de inferência exige a conversão dos checkpoints nativos do PyTorch para o padrão aberto .onnx. Os scripts abaixo foram reestruturados para garantir compatibilidade com provedorse CUDA e CPU, mantendo a precisão numérica original.

1. Classificação de Nuvens de Pontos

import torch
import pointnet_cls

DIM_PONTOS = 1024
NUM_CLASSE = 10
USAR_NORMALIZACAO = False

rede = pointnet_cls.get_model(NUM_CLASSE, USAR_NORMALIZACAO)
rede.to('cuda')
rede.eval()

caminho_checkpoint = './checkpoint_cls.pth'
dados_ckpt = torch.load(caminho_checkpoint, map_location='cuda')
rede.load_state_dict(dados_ckpt['estado_modelo'])

entrada_dummy = torch.rand(1, 6 if USAR_NORMALIZACAO else 3, DIM_PONTOS).cuda()

torch.onnx.export(
    rede,
    entrada_dummy,
    'modelo_classificacao.onnx',
    opset_version=11,
    input_names=['nuvem_entrada'],
    output_names=['probabilidade_classe']
)

Inferência via Python

import numpy as np
import onnxruntime

def preprocessar_pontos(pontos_brutos):
    centro = np.mean(pontos_brutos, axis=0)
    centrado = pontos_brutos - centro
    raio_max = np.max(np.sqrt(np.sum(centrado**2, axis=1)))
    return (centrado / raio_max) if raio_max > 0 else centrado

def executar_inferencia_python(caminho_modelo, caminho_dados):
    dados_raw = np.loadtxt(caminho_dados, delimiter=',').astype(np.float32)
    coordenadas = dados_raw[:, :3][:DIM_PONTOS]
    
    pontos_formatados = preprocessar_pontos(coordenadas)
    tensor_entrada = pontos_formatados.T.reshape(1, 3, DIM_PONTOS)

    sessao = onnxruntime.InferenceSession(caminho_modelo, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
    
    nomes_entrada = [i.name for i in sessao.get_inputs()]
    nome_saida = sessao.get_outputs()[0].name
    
    previsao = sessao.run([nome_saida], dict(zip(nomes_entrada, [tensor_entrada])))[0]
    print(f"Classe predita: {np.argmax(previsao)}")

if __name__ == '__main__':
    executar_inferencia_python('modelo_classificacao.onnx', 'ponto_bed.txt')

Inferência via C++

#include <onnxruntime_cxx_api.h>
#include <fstream>
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>

void padronizar_pontos(std::vector<float>& pts) {
    float mean_x = 0.f, mean_y = 0.f, mean_z = 0.f;
    for (size_t i = 0; i < pts.size(); ++i) {
        mean_x += pts[i * 3]; mean_y += pts[i * 3 + 1]; mean_z += pts[i * 3 + 2];
    }
    size_t count = pts.size() / 3;
    mean_x /= count; mean_y /= count; mean_z /= count;

    for (size_t i = 0; i < count; ++i) {
        pts[i * 3] -= mean_x; pts[i * 3 + 1] -= mean_y; pts[i * 3 + 2] -= mean_z;
    }

    float max_dist = 0.f;
    for (size_t i = 0; i < count; ++i) {
        float dist = std::sqrt(pts[i*3]*pts[i*3] + pts[i*3+1]*pts[i*3+1] + pts[i*3+2]*pts[i*3+2]);
        if (dist > max_dist) max_dist = dist;
    }
    for (size_t i = 0; i < count; ++i) {
        pts[i * 3] /= max_dist; pts[i * 3 + 1] /= max_dist; pts[i * 3 + 2] /= max_dist;
    }
}

void rodar_classificador_cpp(const std::string& path_modelo) {
    Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "Classificador");
    Ort::SessionOptions opts;
    opts.SetIntraOpNumThreads(2);
    opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);

    OrtCUDAProviderOptions cuda_opts{};
    cuda_opts.device_id = 0;
    cuda_opts.arena_extend_strategy = 0;
    cuda_opts.cudnn_conv_algo_search = OrtCudnnConvAlgoSearchExhaustive;
    cuda_opts.gpu_mem_limit = SIZE_MAX;
    cuda_opts.do_copy_in_default_stream = 1;
    opts.AppendExecutionProvider_CUDA(cuda_opts);

    std::wstring w_model(path_modelo.begin(), path_modelo.end());
    Ort::Session session(env, w_model.c_str(), opts);
    Ort::AllocatorWithDefaultOptions alloc;

    auto get_names = [&](const auto& getter, size_t count) {
        std::vector<const char*> res;
        for (size_t i = 0; i < count; ++i) res.push_back(getter(i, alloc));
        return res;
    };

    auto input_names = get_names(session.GetInputName, session.GetInputCount());
    auto output_names = get_names(session.GetOutputName, session.GetOutputCount());

    // Leitura simplificada do arquivo .txt (apenas XYZ)
    std::vector<float> raw_pts;
    std::ifstream infile("ponto_bed.txt");
    float x, y, z;
    char sep;
    while(infile >> x >> sep >> y >> sep >> z) raw_pts.push_back(x), raw_pts.push_back(y), raw_pts.push_back(z);
    
    padronizar_pontos(raw_pts);
    
    size_t total_elems = 1 * 3 * DIM_PONTOS;
    std::vector<float> tensor_data(total_elems, 0.f);
    for(int c=0; c<3; ++c)
        for(int p=0; p<DIM_PONTOS; ++p)
            tensor_data[c * DIM_PONTOS + p] = raw_pts[p * 3 + c];

    auto mem_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
    std::vector<int64_t> dims = {1, 3, DIM_PONTOS};
    Ort::Value input_val = Ort::Value::CreateTensor<float>(mem_info, tensor_data.data(), total_elems, dims.data(), dims.size());
    
    std::vector<Ort::Value> inputs_vec;
    inputs_vec.push_back(std::move(input_val));

    auto outputs = session.Run(Ort::RunOptions{nullptr}, input_names.data(), inputs_vec.data(), input_names.size(), output_names.data(), output_names.size());
    const float* preds = outputs[0].GetTensorData<float>();
    size_t n_elem = outputs[0].GetTensorTypeAndShapeInfo().GetElementCount();
    
    int best_idx = 0;
    float max_val = preds[0];
    for(size_t i=1; i<n_elem; ++i) {
        if(preds[i] > max_val) { max_val = preds[i]; best_idx = i; }
    }
    std::cout << "Resultado C++: " << best_idx << std::endl;
}

2. Segmentação Particular (Part Segmentation)

Neste cenário, o modelo recebe além da nuvem de pontos um vetor de one-hot representando a classe global do objeto. A exportação deve lidar com múltiplos fluxos de entrada.

import torch
import pointnet_part_seg

NUM_PARTES = 50
NUM_CLASSES_GLOBAL = 16
QTD_PONTOS = 2048

def converter_para_onehot(rótulo, num_cls):
    hot = torch.zeros(1, num_cls)
    hot[0][rótulo.item()] = 1.0
    return hot.cuda()

rede_partes = pointnet_part_seg.get_model(NUM_PARTES, False)
rede_partes.to('cuda')
rede_partes.eval()

dados_ckpt = torch.load('./checkpoint_partes.pth', map_location='cuda')
rede_partes.load_state_dict(dados_ckpt['estado_modelo'])

fluxo_pts = torch.rand(1, 3, QTD_PONTOS).cuda()
fluxo_rotulo = converter_para_onehot(torch.tensor([0]), NUM_CLASSES_GLOBAL)

torch.onnx.export(
    rede_partes,
    (fluxo_pts, fluxo_rotulo),
    'segmentacao_partes.onnx',
    opset_version=11,
    input_names=['pontos', 'rotulo_global'],
    output_names=['distribuicao_partes']
)

Implementação Python

import numpy as np
import onnxruntime

def gerar_onehot(label, classes):
    vec = np.zeros((1, classes), dtype=np.float32)
    vec[0][label] = 1.0
    return vec

def rodar_segmentacao_partes():
    sess = onnxruntime.InferenceSession('segmentacao_partes.onnx', providers=['CUDAExecutionProvider'])
    nomes_inp = [i.name for i in sess.get_inputs()]
    nomes_out = [o.name for o in sess.get_outputs()]
    
    # Simulação de carregamento de dados
    dados = np.random.rand(3000, 3).astype(np.float32)
    amostra = dados[:QTD_PONTOS]
    amostra_norm = (amostra - np.mean(amostra, axis=0)) / np.max(np.linalg.norm(amostra, axis=1))
    tensor_pts = amostra_norm.T.reshape(1, 3, QTD_PONTOS)
    tensor_label = gerar_onehot(0, NUM_CLASSES_GLOBAL)
    
    saidas = sess.run(nomes_out, {nomes_inp[0]: tensor_pts, nomes_inp[1]: tensor_label})
    logits = saidas[0][0]
    rotulos_parte = np.argmax(logits, axis=1)
    
    print("Mapa de segmentação gerado com sucesso.")
    return rotulos_parte

Implementação C++

std::vector<int> inferir_partes_cpp() {
    Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "PartSeg");
    Ort::SessionOptions ops;
    ops.SetIntraOpNumThreads(1);
    ops.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);

    OrtCUDAProviderOptions gpu_cfg{};
    gpu_cfg.device_id = 0; gpu_cfg.gpu_mem_limit = SIZE_MAX;
    ops.AppendExecutionProvider_CUDA(gpu_cfg);

    std::wstring mp(L"segmentacao_partes.onnx");
    Ort::Session sess(env, mp.c_str(), ops);
    Ort::AllocatorWithDefaultOptions alloc;

    auto pegar_nomes = [](auto getter, size_t qtd, Ort::AllocatorWithDefaultOptions& al) {
        std::vector<const char*> v;
        for(size_t i=0; i<qtd; ++i) v.push_back(getter(i, al));
        return v;
    };
    
    auto nms_in = pegar_nomes(sess.GetInputName, sess.GetInputCount(), alloc);
    auto nms_out = pegar_nomes(sess.GetOutputName, sess.GetOutputCount(), alloc);

    // Construção do tensor de rótulo one-hot
    size_t tam_label = 1 * 1 * NUM_CLASSES_GLOBAL;
    std::vector<float> vals_label(tam_label, 0.f);
    vals_label[0] = 1.0f; // Classe 0

    auto mi = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
    std::vector<int64> d_dim = {1, 1, NUM_CLASSES_GLOBAL};
    Ort::Value val_label = Ort::Value::CreateTensor<float>(mi, vals_label.data(), tam_label, d_dim.data(), d_dim.size());

    // Construção do tensor de pontos (mimetizando estrutura similar à classificação)
    std::vector<float> pts_buffer(QTD_PONTOS * 3, 0.f); 
    // ... preenchimento de pts_buffer omitido para brevidade ...

    size_t tam_pts = 1 * 3 * QTD_PONTOS;
    std::vector<float> tens_pt(tam_pts);
    for(int c=0; c<3; ++c)
        for(int p=0; p<QTD_PONTOS; ++p)
            tens_pt[c * QTD_PONTOS + p] = pts_buffer[p * 3 + c];

    std::vector<int64> d_pts = {1, 3, QTD_PONTOS};
    Ort::Value val_pts = Ort::Value::CreateTensor<float>(mi, tens_pt.data(), tam_pts, d_pts.data(), d_pts.size());

    std::vector<Ort::Value> feeds = {std::move(val_pts), std::move(val_label)};
    auto ret = sess.Run(Ort::RunOptions{}, nms_in.data(), feeds.data(), nms_in.size(), nms_out.data(), nms_out.size());

    // Extração e busca do máximo por ponto
    const float* raw = ret[0].GetTensorData<float>();
    size_t elems = ret[0].GetTensorTypeAndShapeInfo().GetElementCount();
    std::vector<int> resultado(QTD_PONTOS);
    
    for(int i=0; i<QTD_PONTOS; ++i) {
        float mx = -FLT_MAX; int idx = 0;
        for(int j=0; j<NUM_PARTES; ++j) {
            if(raw[i * NUM_PARTES + j] > mx) { mx = raw[i * NUM_PARTES + j]; idx = j; }
        }
        resultado[i] = idx;
    }
    return resultado;
}

3. Segmentação Semântica (Sematnic Segmentation)

Modelos semânticos geralmente operam sobre áreas extensas que exigem processamento em janelas deslizantes (sliding windows). O resultado final agrega votações probabilísticas para cada vértice original.

import torch
import pointnet_sem_seg

PONTOS_AMOSTRA = 4096
CLASSES_SEMANTICAS = 13

rede_sem = pointnet_sem_seg.get_model(CLASSES_SEMANTICAS)
rede_sem.to('cuda')
rede_sem.eval()

ckpt = torch.load('./sem_seg_check.pth', map_location='cuda')
rede_sem.load_state_dict(ckpt['estado_modelo'])

x_teste = torch.rand(1, 9, PONTOS_AMOSTRA).cuda() # 3 coords + 3 rgb + 3 normais

torch.onnx.export(
    rede_sem,
    x_teste,
    'semantico.onnx',
    opset_version=11,
    input_names=['bloco_geometria'],
    output_names=['scores_semanticos']
)

Script Python para Processamento em Blocos

import numpy as np
import onnxruntime

STRIDE = 0.5
BLOCO_TAM = 1.0
BATCH_SIZE = 1

def inferir_semanitico_area(path_arquivo, path_modelo):
    # Carregamento e preparação básica
    arr = np.load(path_arquivo)
    pontos_orig = arr[:, :6]
    
    xmin, xmax = pontos_orig[:, 0].min(), pontos_orig[:, 0].max()
    ymin, ymax = pontos_orig[:, 1].min(), pontos_orig[:, 1].max()
    
    gx = int(np.ceil((xmax - xmin - BLOCO_TAM)/STRIDE)) + 1
    gy = int(np.ceil((ymax - ymin - BLOCO_TAM)/STRIDE)) + 1
    
    lote_acumulado = []
    indice_mapeamento = []
    
    for iy in range(gy):
        for ix in range(gx):
            sx = xmin + ix * STRIDE
            ex = min(sx + BLOCO_TAM, xmax)
            sy = ymin + iy * STRIDE
            ey = min(sy + BLOCO_TAM, ymax)
            
            mask = (pontos_orig[:, 0] >= ex - BLOCO_TAM) & \
                   (pontos_orig[:, 0] <= ex) & \
                   (pontos_orig[:, 1] >= ey - BLOCO_TAM) & \
                   (pontos_orig[:, 1] <= ey)
            
            indices_janela = np.where(mask)[0]
            if len(indices_janela) == 0: continue
            
            # Replicação controlada para atingir batch fixed
            num_batches = int(np.ceil(len(indices_janela) / PONTOS_AMOSTRA))
            tam_final = num_batches * PONTOS_AMOSTRA
            replace = (tam_final - len(indices_janela) <= len(indices_janela))
            repleca = np.random.choice(indices_janela, tam_final - len(indices_janela), replace=replace)
            indices_finais = np.concatenate([indices_janela, repleca])
            np.random.shuffle(indices_finais)
            
            janela_pts = pontos_orig[indices_finais]
            
            # Normalização espacial dentro do bloco
            cx, cy = (sx + BLOCO_TAM/2), (sy + BLOCO_TAM/2)
            janela_pts[:, 0] -= cx
            janela_pts[:, 1] -= cy
            janela_pts[:, 3:] /= 255.0
            
            # Concatenação das coordenadas normalizadas globais
            xyz_norm = np.column_stack([
                janela_pts[:, 0]/xmax, janela_pts[:, 1]/ymax, janela_pts[:, 2]/arr[:, 2].max()
            ])
            lote_completo = np.hstack([janela_pts, xyz_norm])
            
            lote_acumulado.append(lote_completo.reshape(-1, PONTOS_AMOSTRA, 9))
            indice_mapeamento.append(indices_finais.reshape(-1, PONTOS_AMOSTRA))

    sess = onnxruntime.InferenceSession(path_modelo, providers=['CUDAExecutionProvider'])
    inp_n = [i.name for i in sess.get_inputs()]
    out_n = [o.name for o in sess.get_outputs()]
    
    vote_pool = np.zeros((pontos_orig.shape[0], CLASSES_SEMANTICAS))
    
    for blk, idx_blk in zip(lote_acumulado, indice_mapeamento):
        batch_t = blk.swapaxes(1, 2).astype(np.float32)
        res = sess.run(out_n, {inp_n[0]: batch_t})[0]
        preditos = np.argmax(res, axis=2)
        
        for b in range(preditos.shape[0]):
            for p in range(preditos.shape[1]):
                vote_pool[int(idx_blk[b,p]), int(preditos[b,p])] += 1

    mapa_final = np.argmax(vote_pool, axis=1)
    np.savetxt('resultado_semantico.txt', np.column_stack([pontos_orig[:,:3], mapa_final]), fmt='%.6f %d')
    return mapa_final

Adaptação para C++

A lógica de janelamento requer manipulação eficiente de memória. Abaixo segue o núcleo de inferência já estruturado para C++ moderno:

void executar_semanitico_cpp() {
    Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "SemSegCPP");
    Ort::SessionOptions config;
    config.SetIntraOpNumThreads(2);
    config.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
    
    OrtCUDAProviderOptions gpu{};
    gpu.device_id = 0; gpu.gpu_mem_limit = SIZE_MAX;
    config.AppendExecutionProvider_CUDA(gpu);

    std::wstring mpath(L"semantico.onnx");
    Ort::Session sess(env, mpath.c_str(), config);
    Ort::AllocatorWithDefaultOptions al;

    auto nms_i = [&sess,&al]{ std::vector<const char*> v; for(size_t k=0;k<sess.GetInputCount();++k) v.push_back(sess.GetInputName(k,al)); return v; }();
    auto nms_o = [&sess,&al]{ std::vector<const char*> v; for(size_t k=0;k<sess.GetOutputCount();++k) v.push_back(sess.GetOutputName(k,al)); return v; }();

    // Estrutura simulada de votação
    std::vector<std::vector<int>> votos(NUM_PONTOS_TOTAL, std::vector<int>(CLASSES_SEMANTICAS, 0));
    
    // Laço iterativo sobre blocos (dados pré-processados em data_rooms)
    for(size_t blk=0; blk<num_blocos; ++blk) {
        std::vector<float> buffer(9 * PONTOS_AMOSTRA);
        // Preenchimento de buffer a partir de data_rooms[blk] omitido...

        std::vector<float> tens_in(9 * PONTOS_AMOSTRA);
        for(int c=0; c<9; ++c)
            for(int p=0; p<PONTOS_AMOSTRA; ++p)
                tens_in[c * PONTOS_AMOSTRA + p] = buffer[p * 9 + c];

        auto mi = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
        std::vector<int64> d = {1, 9, PONTOS_AMOSTRA};
        Ort::Value t_val = Ort::Value::CreateTensor<float>(mi, tens_in.data(), tens_in.size(), d.data(), d.size());
        
        std::vector<Ort::Value> feed = {std::move(t_val)};
        auto outs = sess.Run(Ort::RunOptions{}, nms_i.data(), feed.data(), nms_i.size(), nms_o.data(), nms_o.size());

        const float* score_ptr = outs[0].GetTensorData<float>();
        for(int p=0; p<PONTOS_AMOSTRA; ++p) {
            int cls_best = 0; float mx = score_ptr[p * CLASSES_SEMANTICAS];
            for(int c=1; c<CLASSES_SEMANTICAS; ++c) {
                if(score_ptr[p * CLASSES_SEMANTICAS + c] > mx) { mx = score_ptr[p * CLASSES_SEMANTICAS + c]; cls_best = c; }
            }
            votos[indice_original[blk][p]][cls_best]++;
        }
    }

    std::ofstream fout("saida_semantica.txt");
    for(size_t i=0; i<NUM_PONTOS_TOTAL; ++i) {
        auto& v = votos[i];
        int best = std::distance(v.begin(), std::max_element(v.begin(), v.end()));
        fout << pontox[i] << " " << pontoy[i] << " " << pontoz[i] << " " << best << "\n";
    }
}

Notas de Compatibilidade de Formato de Dados

O C++ padrão não possui suporte nativo leve para arquivos binários NumPy (.npy). Para manter a pipeline independente de dependências externas pesadas, recomenda-se converter a matriz bruta antes da compilação do sistema. Um utilitário direto para essa transição:

import numpy as np
matriz_bruta = np.load("area_conference.npy")
np.savetxt("area_conference_ready.txt", matriz_bruta, delimiter=",", fmt="%.6f")

Esta etapa garante que leitores C++ baseados em fluxo textual possam interpretar coordenadas e atributos RGB/Normais sem erros de alinhamento de bytes.

Tags: onnx-runtime pointnet point-cloud-inference c++-gpu-computing python-deployment

Publicado em 9-3 14:53