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.