1165 lines
40 KiB
C++
1165 lines
40 KiB
C++
|
|
#include <pcl/visualization/pcl_visualizer.h>
|
|||
|
|
#include <pcl/point_cloud.h>
|
|||
|
|
#include <pcl/point_types.h>
|
|||
|
|
|
|||
|
|
#include <thread>
|
|||
|
|
#include <chrono>
|
|||
|
|
#include <mutex>
|
|||
|
|
#include <atomic>
|
|||
|
|
#include <iostream>
|
|||
|
|
#include <iomanip> // <- pra formatar tabela bonitinha
|
|||
|
|
#include <filesystem>
|
|||
|
|
#include <cstring>
|
|||
|
|
|
|||
|
|
#include <deque>
|
|||
|
|
#include <chrono>
|
|||
|
|
#include <cmath> // sqrt etc.
|
|||
|
|
|
|||
|
|
#include <Eigen/Dense>
|
|||
|
|
#include <pcl/registration/icp.h>
|
|||
|
|
#include <pcl/filters/voxel_grid.h> // downsample pra ajudar ICP
|
|||
|
|
|
|||
|
|
#include <pcl/filters/extract_indices.h>
|
|||
|
|
#include <pcl/segmentation/sac_segmentation.h>
|
|||
|
|
#include <pcl/segmentation/extract_clusters.h>
|
|||
|
|
|
|||
|
|
#include "livox_lidar_api.h"
|
|||
|
|
#include "livox_lidar_def.h"
|
|||
|
|
|
|||
|
|
namespace fs = std::filesystem;
|
|||
|
|
|
|||
|
|
// =====================================================
|
|||
|
|
// ESTRUTURAS / GLOBAIS
|
|||
|
|
// =====================================================
|
|||
|
|
|
|||
|
|
struct TimedPoint {
|
|||
|
|
pcl::PointXYZRGB p;
|
|||
|
|
uint64_t t_ms;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
static std::deque<TimedPoint> g_buffer_temporal;
|
|||
|
|
static const uint64_t WINDOW_MS = 100; // janela vis<69>vel: <20>ltimos 300 ms
|
|||
|
|
|
|||
|
|
// nuvem atual pra render
|
|||
|
|
static std::mutex g_cloud_mutex;
|
|||
|
|
static pcl::PointCloud<pcl::PointXYZRGB>::Ptr g_cloud(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
static std::atomic<bool> g_running(true);
|
|||
|
|
|
|||
|
|
// escolher modo de cor (true = dist<73>ncia radial, false = altura Z)
|
|||
|
|
static bool ver_distancia = true;
|
|||
|
|
static bool modo_map = false;
|
|||
|
|
static bool usar_slam = false;
|
|||
|
|
static bool desenhar_caixas = true;
|
|||
|
|
static bool sensor_invertido = true; // true = montado de cabe<62>a pra baixo
|
|||
|
|
|
|||
|
|
// nuvem global acumulada (map<61>o)
|
|||
|
|
static pcl::PointCloud<pcl::PointXYZRGB>::Ptr g_global_map(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
|
|||
|
|
// ---- IMU globals ----
|
|||
|
|
// vamos guardar o <20>ltimo pacote de IMU recebido
|
|||
|
|
static std::mutex g_imu_mutex;
|
|||
|
|
|
|||
|
|
// vamos manter valores "mais recentes"
|
|||
|
|
static float g_imu_acc_x = 0.0f;
|
|||
|
|
static float g_imu_acc_y = 0.0f;
|
|||
|
|
static float g_imu_acc_z = 0.0f;
|
|||
|
|
static float g_imu_gyro_x = 0.0f;
|
|||
|
|
static float g_imu_gyro_y = 0.0f;
|
|||
|
|
static float g_imu_gyro_z = 0.0f;
|
|||
|
|
static uint64_t g_imu_last_ms = 0; // timestamp local que recebemos
|
|||
|
|
|
|||
|
|
// Pose estimada do LiDAR no mundo (por enquanto s<> rota<74><61>o).
|
|||
|
|
static std::mutex g_pose_mutex;
|
|||
|
|
static float g_roll = 0.0f; // rota<74><61>o em torno de X
|
|||
|
|
static float g_pitch = 0.0f; // rota<74><61>o em torno de Y
|
|||
|
|
static float g_yaw = 0.0f; // rota<74><61>o em torno de Z
|
|||
|
|
static uint64_t g_last_imu_ms = 0;
|
|||
|
|
|
|||
|
|
// posi<73><69>o global estimada (em metros)
|
|||
|
|
static float g_pos_x = 0.0f;
|
|||
|
|
static float g_pos_y = 0.0f;
|
|||
|
|
static float g_pos_z = 0.0f;
|
|||
|
|
|
|||
|
|
// velocidade global estimada (m/s)
|
|||
|
|
static float g_vel_x = 0.0f;
|
|||
|
|
static float g_vel_y = 0.0f;
|
|||
|
|
static float g_vel_z = 0.0f;
|
|||
|
|
|
|||
|
|
// Pose global estimada via ICP+IMU
|
|||
|
|
// OBS: agora quem manda em posi<73><69>o <20> ICP, e n<>o mais a integra<72><61>o da acelera<72><61>o.
|
|||
|
|
// A gente N<>O vai mais usar g_pos_x/g_pos_y/g_pos_z que vinha da IMU que driftava infinito.
|
|||
|
|
// Em vez disso, vamos manter uma matriz 4x4 acumulada que representa a pose do LiDAR no "mapa global".
|
|||
|
|
|
|||
|
|
static Eigen::Matrix4f g_pose_Twm = Eigen::Matrix4f::Identity();
|
|||
|
|
// Twm = Transform from sensor(local frame "m") to world "w".
|
|||
|
|
// (world <20> o mapa global onde estamos acumulando pontos)
|
|||
|
|
|
|||
|
|
// nuvem 'keyframe' anterior para ICP
|
|||
|
|
static pcl::PointCloud<pcl::PointXYZRGB>::Ptr g_prev_cloud_raw(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
|
|||
|
|
// flag pra saber se j<> temos prev_cloud inicializada
|
|||
|
|
static bool g_has_prev_frame = false;
|
|||
|
|
|
|||
|
|
// acumula pontos estabilizados (rotacionados pela IMU) at<61> formar um "keyframe"
|
|||
|
|
static pcl::PointCloud<pcl::PointXYZRGB>::Ptr g_keyframe_accum(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
|
|||
|
|
// timestamp de quando come<6D>amos a acumular esse keyframe
|
|||
|
|
static uint64_t g_keyframe_start_ms = 0;
|
|||
|
|
|
|||
|
|
static const uint64_t KEYFRAME_WINDOW_MS = 150; // ~150ms de varredura por keyframe
|
|||
|
|
static const size_t KEYFRAME_MIN_POINTS = 2000; // se ainda n<>o bateu tempo mas j<> tem bastante ponto
|
|||
|
|
static const size_t KEYFRAME_MAX_POINTS = 20000; // se passou disso tamb<6D>m fecha logo
|
|||
|
|
|
|||
|
|
static uint64_t g_last_detection_ms = 0;
|
|||
|
|
static const uint64_t DETECTION_PERIOD_MS = 100; // roda cluster a cada ~300ms
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// value em [0,1] -> cor estilo "jet"
|
|||
|
|
static inline void ColorMapJet(float value, uint8_t& r, uint8_t& g, uint8_t& b) {
|
|||
|
|
if (value < 0.0f) value = 0.0f;
|
|||
|
|
if (value > 1.0f) value = 1.0f;
|
|||
|
|
|
|||
|
|
float fourValue = 4.0f * value;
|
|||
|
|
|
|||
|
|
float r_f = std::min(std::max(std::min(fourValue - 1.5f, -fourValue + 4.5f), 0.0f), 1.0f);
|
|||
|
|
float g_f = std::min(std::max(std::min(fourValue - 0.5f, -fourValue + 3.5f), 0.0f), 1.0f);
|
|||
|
|
float b_f = std::min(std::max(std::min(fourValue + 0.5f, -fourValue + 2.5f), 0.0f), 1.0f);
|
|||
|
|
|
|||
|
|
r = static_cast<uint8_t>(r_f * 255.0f);
|
|||
|
|
g = static_cast<uint8_t>(g_f * 255.0f);
|
|||
|
|
b = static_cast<uint8_t>(b_f * 255.0f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static Eigen::Matrix3f R_from_rpy(float roll, float pitch, float yaw) {
|
|||
|
|
float cr = std::cos(roll);
|
|||
|
|
float sr = std::sin(roll);
|
|||
|
|
float cp = std::cos(pitch);
|
|||
|
|
float sp = std::sin(pitch);
|
|||
|
|
float cy = std::cos(yaw);
|
|||
|
|
float sy = std::sin(yaw);
|
|||
|
|
|
|||
|
|
Eigen::Matrix3f R;
|
|||
|
|
R << cy * cp, cy* sp* sr - sy * cr, cy* sp* cr + sy * sr,
|
|||
|
|
sy* cp, sy* sp* sr + cy * cr, sy* sp* cr - cy * sr,
|
|||
|
|
-sp, cp* sr, cp* cr;
|
|||
|
|
return R;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct VoxelKey {
|
|||
|
|
int ix, iy, iz;
|
|||
|
|
bool operator==(const VoxelKey& o) const {
|
|||
|
|
return ix == o.ix && iy == o.iy && iz == o.iz;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// hash pra usar VoxelKey como chave em unordered_map
|
|||
|
|
struct VoxelKeyHash {
|
|||
|
|
std::size_t operator()(const VoxelKey& k) const noexcept {
|
|||
|
|
// hash combinando 3 ints
|
|||
|
|
// isso n<>o precisa ser perfeito, s<> razo<7A>vel
|
|||
|
|
std::size_t h1 = std::hash<int>()(k.ix);
|
|||
|
|
std::size_t h2 = std::hash<int>()(k.iy);
|
|||
|
|
std::size_t h3 = std::hash<int>()(k.iz);
|
|||
|
|
return h1 ^ (h2 << 1) ^ (h3 << 2);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr DownsampleVoxelLike(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr& in, float leaf)
|
|||
|
|
{
|
|||
|
|
auto out = pcl::PointCloud<pcl::PointXYZRGB>::Ptr(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
out->points.reserve(in->points.size());
|
|||
|
|
|
|||
|
|
std::unordered_map<VoxelKey, pcl::PointXYZRGB, VoxelKeyHash> voxels;
|
|||
|
|
voxels.reserve(in->points.size());
|
|||
|
|
|
|||
|
|
const float inv_leaf = 1.0f / leaf;
|
|||
|
|
|
|||
|
|
for (const auto& p : in->points) {
|
|||
|
|
if (!std::isfinite(p.x) || !std::isfinite(p.y) || !std::isfinite(p.z))
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
VoxelKey key{
|
|||
|
|
(int)std::floor(p.x * inv_leaf),
|
|||
|
|
(int)std::floor(p.y * inv_leaf),
|
|||
|
|
(int)std::floor(p.z * inv_leaf)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// mant<6E>m o primeiro ponto daquele voxel
|
|||
|
|
if (voxels.find(key) == voxels.end()) {
|
|||
|
|
voxels[key] = p;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
out->points.reserve(voxels.size());
|
|||
|
|
for (const auto& kv : voxels) {
|
|||
|
|
out->points.push_back(kv.second);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
out->width = static_cast<uint32_t>(out->points.size());
|
|||
|
|
out->height = 1;
|
|||
|
|
out->is_dense = false;
|
|||
|
|
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void DesenharBussolaDistancias(pcl::visualization::PCLVisualizer::Ptr& viewer, const pcl::PointCloud<pcl::PointXYZRGB>::Ptr& cloud)
|
|||
|
|
{
|
|||
|
|
// --- limpa linhas antigas (mant<6E>m s<> as novas por frame)
|
|||
|
|
const std::vector<std::string> nomes = {
|
|||
|
|
"linha_frente", "linha_tras", "linha_esquerda", "linha_direita",
|
|||
|
|
"linha_cima", "linha_baixo",
|
|||
|
|
"texto_frente", "texto_tras", "texto_esquerda", "texto_direita",
|
|||
|
|
"texto_cima", "texto_baixo"
|
|||
|
|
};
|
|||
|
|
for (auto& n : nomes) viewer->removeShape(n);
|
|||
|
|
|
|||
|
|
auto distancia_direcao = [&](auto seletor) {
|
|||
|
|
std::vector<float> dists;
|
|||
|
|
dists.reserve(cloud->points.size());
|
|||
|
|
for (const auto& p : cloud->points) {
|
|||
|
|
if (!std::isfinite(p.x) || !std::isfinite(p.y) || !std::isfinite(p.z)) continue;
|
|||
|
|
if (seletor(p)) {
|
|||
|
|
float dist = std::sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
|
|||
|
|
dists.push_back(dist);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (dists.size() < 5) return 9999.f; // muito poucos pontos = inv<6E>lido
|
|||
|
|
|
|||
|
|
std::sort(dists.begin(), dists.end());
|
|||
|
|
// m<>dia dos 10 menores (ou menos se tiver poucos)
|
|||
|
|
int n = std::min<int>(10, dists.size());
|
|||
|
|
float soma = 0.f;
|
|||
|
|
for (int i = 0; i < n; ++i) soma += dists[i];
|
|||
|
|
return soma / n;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
float frente = distancia_direcao([](const auto& p) { return p.y > 0 && std::fabs(p.x) < 0.5; });
|
|||
|
|
float tras = distancia_direcao([](const auto& p) { return p.y < 0 && std::fabs(p.x) < 0.5; });
|
|||
|
|
float direita = distancia_direcao([](const auto& p) { return p.x > 0 && std::fabs(p.y) < 0.5; });
|
|||
|
|
float esquerda = distancia_direcao([](const auto& p) { return p.x < 0 && std::fabs(p.y) < 0.5; });
|
|||
|
|
float cima = distancia_direcao([](const auto& p) { return p.z > 0; });
|
|||
|
|
float baixo = distancia_direcao([](const auto& p) { return p.z < 0; });
|
|||
|
|
|
|||
|
|
// --- fun<75><6E>o utilit<69>ria
|
|||
|
|
auto desenhar = [&](const std::string& nome, const Eigen::Vector3f& dir, float dist, const Eigen::Vector3f& cor, const std::string& label) {
|
|||
|
|
if (dist >= 9990.f) dist = 0.0f; // sem obst<73>culo
|
|||
|
|
Eigen::Vector3f origem(0, 0, 0);
|
|||
|
|
Eigen::Vector3f destino = dir.normalized() * dist;
|
|||
|
|
viewer->addLine(
|
|||
|
|
pcl::PointXYZ(origem.x(), origem.y(), origem.z()),
|
|||
|
|
pcl::PointXYZ(destino.x(), destino.y(), destino.z()),
|
|||
|
|
cor.x(), cor.y(), cor.z(),
|
|||
|
|
nome);
|
|||
|
|
|
|||
|
|
viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_LINE_WIDTH, 3, nome);
|
|||
|
|
|
|||
|
|
// adiciona o texto no final da linha
|
|||
|
|
char txt[64];
|
|||
|
|
if (dist > 0.001f)
|
|||
|
|
std::snprintf(txt, sizeof(txt), "%s: %.0f cm", label.c_str(), dist * 100.f);
|
|||
|
|
else
|
|||
|
|
std::snprintf(txt, sizeof(txt), "%s: --", label.c_str());
|
|||
|
|
|
|||
|
|
pcl::PointXYZ pos(destino.x(), destino.y(), destino.z());
|
|||
|
|
viewer->addText3D(txt, pos, 0.05, cor.x(), cor.y(), cor.z(), "texto_" + label);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// --- desenha as seis dire<72><65>es
|
|||
|
|
desenhar("linha_frente", { 1,0,0 }, frente, { 1,0,0 }, "frente");
|
|||
|
|
desenhar("linha_tras", { -1,0,0 }, tras, { 0.8,0,0 }, "tras");
|
|||
|
|
desenhar("linha_direita", { 0,1,0 }, direita, { 0,1,0 }, "direita");
|
|||
|
|
desenhar("linha_esquerda", { 0,-1,0 }, esquerda, { 0,0.8,0 }, "esquerda");
|
|||
|
|
desenhar("linha_cima", { 0,0,1 }, cima, { 0,0,1 }, "cima");
|
|||
|
|
desenhar("linha_baixo", { 0,0,-1 }, baixo, { 0,0,0.8 }, "baixo");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
void DetectarClustersEAdicionarCaixas(pcl::visualization::PCLVisualizer::Ptr& viewer, pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr cloud_in)
|
|||
|
|
{
|
|||
|
|
if (!desenhar_caixas) {
|
|||
|
|
// Se n<>o vamos desenhar caixas, limpa overlay e sai
|
|||
|
|
for (int i = 0; i < 100; ++i) {
|
|||
|
|
std::string box_id = "bbox_" + std::to_string(i);
|
|||
|
|
viewer->removeShape(box_id);
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Limpa overlay anterior (caixas antigas)
|
|||
|
|
for (int i = 0; i < 100; ++i) {
|
|||
|
|
std::string box_id = "bbox_" + std::to_string(i);
|
|||
|
|
viewer->removeShape(box_id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!cloud_in || cloud_in->empty()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 1. ROI (Regi<67>o de Interesse)
|
|||
|
|
// - S<> objetos at<61> ~5m de raio
|
|||
|
|
// - Ignorar muito baixo (abaixo do ch<63>o) / muito alto
|
|||
|
|
// IMPORTANTE: ajustar limites de z depois que voc<6F> montar o sensor.
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr roi(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
roi->reserve(cloud_in->points.size());
|
|||
|
|
|
|||
|
|
for (const auto& p : cloud_in->points) {
|
|||
|
|
float dist2 = p.x * p.x + p.y * p.y + p.z * p.z;
|
|||
|
|
// at<61> 5m -> 5 * 5 = 25
|
|||
|
|
if (dist2 > 9.0f) continue;
|
|||
|
|
|
|||
|
|
// corta lixo MUITO baixo ou MUITO alto
|
|||
|
|
// ajuste isso conforme a montagem do LiDAR:
|
|||
|
|
// Exemplo:
|
|||
|
|
// se o LiDAR est<73> a ~0.5m do ch<63>o e Z cresce pra cima,
|
|||
|
|
// voc<6F> pode querer ignorar z < -0.2 e z > 2m
|
|||
|
|
if (p.z < -0.2f) continue;
|
|||
|
|
if (p.z > 2.0f) continue;
|
|||
|
|
|
|||
|
|
roi->points.push_back(p);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (roi->points.size() < 20) {
|
|||
|
|
// quase nada perto -> nada pra detectar
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
roi->width = static_cast<uint32_t>(roi->points.size());
|
|||
|
|
roi->height = 1;
|
|||
|
|
roi->is_dense = true;
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 2. Downsample (voxel grid)
|
|||
|
|
// - A gente n<>o quer cada pontinho, quer blocos de ~7cm
|
|||
|
|
// - Isso mata ru<72>do fino tipo perna de trip<69> fina / cabo fino
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
//pcl::VoxelGrid<pcl::PointXYZRGB> vg;
|
|||
|
|
//vg.setInputCloud(roi);
|
|||
|
|
//vg.setLeafSize(0.05f, 0.05f, 0.05f); // ~7cm
|
|||
|
|
//pcl::PointCloud<pcl::PointXYZRGB>::Ptr coarse(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
//vg.filter(*coarse);
|
|||
|
|
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr coarse = DownsampleVoxelLike(roi, 0.05f);
|
|||
|
|
|
|||
|
|
if (coarse->points.size() < 20) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
DesenharBussolaDistancias(viewer, coarse);
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 3. Remover "ch<63>o" r<>pido via limiar de altura
|
|||
|
|
//
|
|||
|
|
// Aqui N<>O estamos fazendo RANSAC (caro).
|
|||
|
|
// Supondo que o sensor fica a uma altura razoavelmente fixa e o eixo Z
|
|||
|
|
// est<73> alinhado com "vertical".
|
|||
|
|
//
|
|||
|
|
// Ideia: tudo muito baixo (tipo at<61> 10cm do ch<63>o local) <20> ch<63>o.
|
|||
|
|
// E o que est<73> acima disso (0.10m, 0.15m...) <20> obst<73>culo.
|
|||
|
|
//
|
|||
|
|
// IMPORTANTE: voc<6F> precisa ajustar esse limite quando souber
|
|||
|
|
// a altura do sensor no rob<6F>. Aqui vou deixar 0.10 m.
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
const float ALTURA_SENSOR = 0.70f; // metros
|
|||
|
|
const float TOLERANCIA = 0.00f; // metros
|
|||
|
|
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr filtrada(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
filtrada->reserve(coarse->points.size());
|
|||
|
|
|
|||
|
|
for (const auto& p : coarse->points) {
|
|||
|
|
if (!sensor_invertido) {
|
|||
|
|
// sensor normal: remove ch<63>o
|
|||
|
|
float limite_chao = -ALTURA_SENSOR + TOLERANCIA;
|
|||
|
|
if (p.z < limite_chao)
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
// sensor invertido: remove teto
|
|||
|
|
float limite_teto = ALTURA_SENSOR - TOLERANCIA;
|
|||
|
|
if (p.z > limite_teto)
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
filtrada->points.push_back(p);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
filtrada->width = static_cast<uint32_t>(filtrada->points.size());
|
|||
|
|
filtrada->height = 1;
|
|||
|
|
filtrada->is_dense = false;
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 4. Clusteriza<7A><61>o Euclidiana
|
|||
|
|
// - Agora rodamos cluster s<> em "acima do ch<63>o"
|
|||
|
|
// - Com muito menos pontos, bem mais leve
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
pcl::search::KdTree<pcl::PointXYZRGB>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZRGB>);
|
|||
|
|
tree->setInputCloud(filtrada);
|
|||
|
|
|
|||
|
|
std::vector<pcl::PointIndices> cluster_indices;
|
|||
|
|
pcl::EuclideanClusterExtraction<pcl::PointXYZRGB> ec;
|
|||
|
|
ec.setClusterTolerance(0.20f); // 20cm de vizinhan<61>a pra juntar pontos
|
|||
|
|
ec.setMinClusterSize(50); // ignora micro-coisas
|
|||
|
|
ec.setMaxClusterSize(100000); // bem grande
|
|||
|
|
ec.setSearchMethod(tree);
|
|||
|
|
ec.setInputCloud(filtrada);
|
|||
|
|
ec.extract(cluster_indices);
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// 5. Criar bounding boxes por cluster
|
|||
|
|
// - S<> desenha clusters grandes o suficiente
|
|||
|
|
// - Voc<6F> falou: queremos ignorar coisa < ~30cm.
|
|||
|
|
// Vamos aplicar esse filtro aqui.
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
const float MIN_SIZE_FOR_BOX = 0.20f; // 30cm m<>nimo num dos eixos
|
|||
|
|
|
|||
|
|
int cluster_id = 0;
|
|||
|
|
|
|||
|
|
for (const auto& indices : cluster_indices) {
|
|||
|
|
if (indices.indices.size() < 30)
|
|||
|
|
continue; // redund<6E>ncia com MinClusterSize
|
|||
|
|
|
|||
|
|
float min_x = std::numeric_limits<float>::max();
|
|||
|
|
float max_x = -std::numeric_limits<float>::max();
|
|||
|
|
float min_y = std::numeric_limits<float>::max();
|
|||
|
|
float max_y = -std::numeric_limits<float>::max();
|
|||
|
|
float min_z = std::numeric_limits<float>::max();
|
|||
|
|
float max_z = -std::numeric_limits<float>::max();
|
|||
|
|
|
|||
|
|
for (int idx : indices.indices) {
|
|||
|
|
const auto& p = (*filtrada)[idx];
|
|||
|
|
|
|||
|
|
if (p.x < min_x) min_x = p.x;
|
|||
|
|
if (p.x > max_x) max_x = p.x;
|
|||
|
|
if (p.y < min_y) min_y = p.y;
|
|||
|
|
if (p.y > max_y) max_y = p.y;
|
|||
|
|
if (p.z < min_z) min_z = p.z;
|
|||
|
|
if (p.z > max_z) max_z = p.z;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
float size_x = max_x - min_x;
|
|||
|
|
float size_y = max_y - min_y;
|
|||
|
|
float size_z = max_z - min_z;
|
|||
|
|
|
|||
|
|
// filtra clusters muito pequenos
|
|||
|
|
if (size_x < MIN_SIZE_FOR_BOX &&
|
|||
|
|
size_y < MIN_SIZE_FOR_BOX &&
|
|||
|
|
size_z < MIN_SIZE_FOR_BOX) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
float cx = (min_x + max_x) * 0.5f;
|
|||
|
|
float cy = (min_y + max_y) * 0.5f;
|
|||
|
|
float cz = (min_z + max_z) * 0.5f;
|
|||
|
|
|
|||
|
|
std::string box_id = "bbox_" + std::to_string(cluster_id);
|
|||
|
|
|
|||
|
|
viewer->addCube(
|
|||
|
|
cx - size_x * 0.5f, cx + size_x * 0.5f,
|
|||
|
|
cy - size_y * 0.5f, cy + size_y * 0.5f,
|
|||
|
|
cz - size_z * 0.5f, cz + size_z * 0.5f,
|
|||
|
|
1.0, 0.0, 0.0, // vermelho (voc<6F> pode mudar depois)
|
|||
|
|
box_id
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
viewer->setShapeRenderingProperties(
|
|||
|
|
pcl::visualization::PCL_VISUALIZER_REPRESENTATION,
|
|||
|
|
pcl::visualization::PCL_VISUALIZER_REPRESENTATION_WIREFRAME,
|
|||
|
|
box_id);
|
|||
|
|
|
|||
|
|
viewer->setShapeRenderingProperties(
|
|||
|
|
pcl::visualization::PCL_VISUALIZER_LINE_WIDTH,
|
|||
|
|
2.0,
|
|||
|
|
box_id);
|
|||
|
|
|
|||
|
|
cluster_id++;
|
|||
|
|
if (cluster_id >= 100) break; // seguran<61>a pra n<>o floodar
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// =====================================================
|
|||
|
|
// CALLBACK DE NUVEM DE PONTOS
|
|||
|
|
// =====================================================
|
|||
|
|
void LidarPointCloudCallback(const uint32_t handle, const uint8_t dev_type, LivoxLidarEthernetPacket* packet, void* /*client_data*/) {
|
|||
|
|
|
|||
|
|
using namespace std::chrono;
|
|||
|
|
uint64_t now_ms = duration_cast<milliseconds>(
|
|||
|
|
steady_clock::now().time_since_epoch()
|
|||
|
|
).count();
|
|||
|
|
|
|||
|
|
std::lock_guard<std::mutex> lock(g_cloud_mutex);
|
|||
|
|
|
|||
|
|
// par<61>metros do nosso color map
|
|||
|
|
const float z_min = -0.2f;
|
|||
|
|
const float z_max = 1.5f;
|
|||
|
|
const float z_range = (z_max - z_min);
|
|||
|
|
|
|||
|
|
// vamos guardar os pontos "novos" desse pacote numa lista tempor<6F>ria,
|
|||
|
|
// pra poder reutilizar tanto no modo Live quanto no modo Map
|
|||
|
|
std::vector<TimedPoint> new_points;
|
|||
|
|
new_points.reserve(packet->dot_num);
|
|||
|
|
|
|||
|
|
auto make_point = [&](float x, float y, float z) {
|
|||
|
|
TimedPoint tp;
|
|||
|
|
tp.p.x = x;
|
|||
|
|
tp.p.y = y;
|
|||
|
|
tp.p.z = z;
|
|||
|
|
|
|||
|
|
uint8_t rr, gg, bb;
|
|||
|
|
if (ver_distancia) {
|
|||
|
|
float dist = std::sqrt(x * x + y * y + z * z); // metros
|
|||
|
|
float norm = dist / 5.0f; // normaliza em ~5m
|
|||
|
|
ColorMapJet(norm, rr, gg, bb);
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
float norm = (z - z_min) / (z_range > 0.0001f ? z_range : 1.0f);
|
|||
|
|
ColorMapJet(norm, rr, gg, bb);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tp.p.r = rr;
|
|||
|
|
tp.p.g = gg;
|
|||
|
|
tp.p.b = bb;
|
|||
|
|
|
|||
|
|
tp.t_ms = now_ms;
|
|||
|
|
new_points.push_back(tp);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 1. pegar pontos do pacote e montar new_points
|
|||
|
|
if (packet->data_type == kLivoxLidarCartesianCoordinateHighData) {
|
|||
|
|
auto* pts = reinterpret_cast<LivoxLidarCartesianHighRawPoint*>(packet->data);
|
|||
|
|
|
|||
|
|
for (uint16_t i = 0; i < packet->dot_num; ++i) {
|
|||
|
|
float x = pts[i].x / 1000.0f;
|
|||
|
|
float y = pts[i].y / 1000.0f;
|
|||
|
|
float z = pts[i].z / 1000.0f;
|
|||
|
|
make_point(x, y, z);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else if (packet->data_type == kLivoxLidarCartesianCoordinateLowData) {
|
|||
|
|
auto* pts = reinterpret_cast<LivoxLidarCartesianLowRawPoint*>(packet->data);
|
|||
|
|
|
|||
|
|
for (uint16_t i = 0; i < packet->dot_num; ++i) {
|
|||
|
|
float x = pts[i].x / 100.0f;
|
|||
|
|
float y = pts[i].y / 100.0f;
|
|||
|
|
float z = pts[i].z / 100.0f;
|
|||
|
|
make_point(x, y, z);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
// outros formatos ainda n<>o tratados
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. dependendo do modo, tratamos diferente
|
|||
|
|
if (!modo_map) {
|
|||
|
|
// ------ MODO LIVE ------
|
|||
|
|
// empurrar os pontos novos pro buffer temporal
|
|||
|
|
for (const auto& tp : new_points) {
|
|||
|
|
g_buffer_temporal.push_back(tp);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// remover pontos velhos > WINDOW_MS
|
|||
|
|
const uint64_t cutoff = now_ms - WINDOW_MS;
|
|||
|
|
while (!g_buffer_temporal.empty() &&
|
|||
|
|
g_buffer_temporal.front().t_ms < cutoff) {
|
|||
|
|
g_buffer_temporal.pop_front();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// reconstruir g_cloud a partir da janela temporal
|
|||
|
|
g_cloud->points.clear();
|
|||
|
|
g_cloud->points.reserve(g_buffer_temporal.size());
|
|||
|
|
for (const auto& tp : g_buffer_temporal) {
|
|||
|
|
g_cloud->points.push_back(tp.p);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
g_cloud->width = static_cast<uint32_t>(g_cloud->points.size());
|
|||
|
|
g_cloud->height = 1;
|
|||
|
|
g_cloud->is_dense = true;
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
// ------ MODO MAP COM ICP ROBUSTO ------
|
|||
|
|
|
|||
|
|
using namespace std::chrono;
|
|||
|
|
|
|||
|
|
// se o SLAM estiver desativado, apenas acumula os pontos diretamente
|
|||
|
|
if (!usar_slam) {
|
|||
|
|
Eigen::Matrix3f R_imu = R_from_rpy(g_roll, g_pitch, g_yaw);
|
|||
|
|
|
|||
|
|
for (const auto& tp : new_points) {
|
|||
|
|
Eigen::Vector3f v_local(tp.p.x, tp.p.y, tp.p.z);
|
|||
|
|
Eigen::Vector3f v_rot = R_imu * v_local;
|
|||
|
|
|
|||
|
|
pcl::PointXYZRGB p_out;
|
|||
|
|
p_out.x = v_rot.x();
|
|||
|
|
p_out.y = v_rot.y();
|
|||
|
|
p_out.z = v_rot.z();
|
|||
|
|
p_out.r = tp.p.r;
|
|||
|
|
p_out.g = tp.p.g;
|
|||
|
|
p_out.b = tp.p.b;
|
|||
|
|
|
|||
|
|
g_global_map->points.push_back(p_out);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
g_global_map->width = (uint32_t)g_global_map->points.size();
|
|||
|
|
g_global_map->height = 1;
|
|||
|
|
g_global_map->is_dense = true;
|
|||
|
|
|
|||
|
|
*g_cloud = *g_global_map;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. pega atitude atual
|
|||
|
|
float roll, pitch, yaw;
|
|||
|
|
{
|
|||
|
|
std::lock_guard<std::mutex> lock_pose(g_pose_mutex);
|
|||
|
|
roll = g_roll;
|
|||
|
|
pitch = g_pitch;
|
|||
|
|
yaw = g_yaw;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Eigen::Matrix3f R_imu = R_from_rpy(roll, pitch, yaw);
|
|||
|
|
|
|||
|
|
// 2. converte os pontos desse pacote para o frame estabilizado (IMU)
|
|||
|
|
// e acumula em g_keyframe_accum
|
|||
|
|
uint64_t now_ms = duration_cast<milliseconds>(
|
|||
|
|
steady_clock::now().time_since_epoch()
|
|||
|
|
).count();
|
|||
|
|
|
|||
|
|
if (g_keyframe_start_ms == 0) {
|
|||
|
|
g_keyframe_start_ms = now_ms;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const auto& tp : new_points) {
|
|||
|
|
Eigen::Vector3f v_local(tp.p.x, tp.p.y, tp.p.z);
|
|||
|
|
Eigen::Vector3f v_rot = R_imu * v_local;
|
|||
|
|
|
|||
|
|
pcl::PointXYZRGB p_out;
|
|||
|
|
p_out.x = v_rot.x();
|
|||
|
|
p_out.y = v_rot.y();
|
|||
|
|
p_out.z = v_rot.z();
|
|||
|
|
p_out.r = tp.p.r;
|
|||
|
|
p_out.g = tp.p.g;
|
|||
|
|
p_out.b = tp.p.b;
|
|||
|
|
|
|||
|
|
g_keyframe_accum->points.push_back(p_out);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. decidiu fechar keyframe?
|
|||
|
|
bool close_keyframe = false;
|
|||
|
|
|
|||
|
|
uint64_t elapsed = now_ms - g_keyframe_start_ms;
|
|||
|
|
size_t npts = g_keyframe_accum->points.size();
|
|||
|
|
|
|||
|
|
if (elapsed >= KEYFRAME_WINDOW_MS && npts >= KEYFRAME_MIN_POINTS) {
|
|||
|
|
close_keyframe = true;
|
|||
|
|
}
|
|||
|
|
if (npts >= KEYFRAME_MAX_POINTS) {
|
|||
|
|
close_keyframe = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!close_keyframe) {
|
|||
|
|
// ainda n<>o formou um keyframe completo
|
|||
|
|
// atualiza visualiza<7A><61>o s<> pra voc<6F> continuar vendo algo (opcional)
|
|||
|
|
// aqui a gente s<> mostra o mapa atual sem adicionar nada novo
|
|||
|
|
*g_cloud = *g_global_map;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------ FECHANDO KEYFRAME ------
|
|||
|
|
|
|||
|
|
// monta um ptr pra esse keyframe atual completo
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr curr_keyframe(
|
|||
|
|
new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
*curr_keyframe = *g_keyframe_accum;
|
|||
|
|
curr_keyframe->width = (uint32_t)curr_keyframe->points.size();
|
|||
|
|
curr_keyframe->height = 1;
|
|||
|
|
curr_keyframe->is_dense = true;
|
|||
|
|
|
|||
|
|
// limpa o acumulador pro pr<70>ximo ciclo
|
|||
|
|
g_keyframe_accum->points.clear();
|
|||
|
|
g_keyframe_accum->width = 0;
|
|||
|
|
g_keyframe_accum->height = 1;
|
|||
|
|
g_keyframe_accum->is_dense = true;
|
|||
|
|
g_keyframe_start_ms = now_ms;
|
|||
|
|
|
|||
|
|
// se esse <20> o primeiro keyframe, s<> inicializa estado
|
|||
|
|
if (!g_has_prev_frame) {
|
|||
|
|
*g_prev_cloud_raw = *curr_keyframe;
|
|||
|
|
g_has_prev_frame = true;
|
|||
|
|
|
|||
|
|
// injeta esse primeiro keyframe no mapa global com a pose inicial
|
|||
|
|
for (const auto& p_in : curr_keyframe->points) {
|
|||
|
|
Eigen::Vector4f v_local(p_in.x, p_in.y, p_in.z, 1.0f);
|
|||
|
|
Eigen::Vector4f v_world = g_pose_Twm * v_local;
|
|||
|
|
|
|||
|
|
pcl::PointXYZRGB p_out;
|
|||
|
|
p_out.x = v_world.x();
|
|||
|
|
p_out.y = v_world.y();
|
|||
|
|
p_out.z = v_world.z();
|
|||
|
|
p_out.r = p_in.r;
|
|||
|
|
p_out.g = p_in.g;
|
|||
|
|
p_out.b = p_in.b;
|
|||
|
|
|
|||
|
|
g_global_map->points.push_back(p_out);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
g_global_map->width = (uint32_t)g_global_map->points.size();
|
|||
|
|
g_global_map->height = 1;
|
|||
|
|
g_global_map->is_dense = true;
|
|||
|
|
|
|||
|
|
*g_cloud = *g_global_map;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------ j<> temos prev frame: ICP entre keyframes ------
|
|||
|
|
|
|||
|
|
// downsample prev e curr pra robustez
|
|||
|
|
pcl::VoxelGrid<pcl::PointXYZRGB> voxel;
|
|||
|
|
voxel.setLeafSize(0.03f, 0.03f, 0.03f);
|
|||
|
|
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr curr_ds(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr prev_ds(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
|
|||
|
|
voxel.setInputCloud(curr_keyframe);
|
|||
|
|
voxel.filter(*curr_ds);
|
|||
|
|
|
|||
|
|
voxel.setInputCloud(g_prev_cloud_raw);
|
|||
|
|
voxel.filter(*prev_ds);
|
|||
|
|
|
|||
|
|
// roda ICP
|
|||
|
|
pcl::IterativeClosestPoint<pcl::PointXYZRGB, pcl::PointXYZRGB> icp;
|
|||
|
|
icp.setInputSource(curr_ds); // atual
|
|||
|
|
icp.setInputTarget(prev_ds); // anterior
|
|||
|
|
icp.setMaximumIterations(40);
|
|||
|
|
icp.setTransformationEpsilon(1e-6);
|
|||
|
|
icp.setEuclideanFitnessEpsilon(1e-6);
|
|||
|
|
icp.setMaxCorrespondenceDistance(0.30f); // ~30 cm, porque agora <20> keyframe vs keyframe
|
|||
|
|
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB> icp_aligned;
|
|||
|
|
icp.align(icp_aligned);
|
|||
|
|
|
|||
|
|
bool accept_icp = false;
|
|||
|
|
Eigen::Matrix4f T_prev_to_curr = Eigen::Matrix4f::Identity();
|
|||
|
|
|
|||
|
|
if (icp.hasConverged()) {
|
|||
|
|
double score = icp.getFitnessScore();
|
|||
|
|
// std::cout << "[ICP KF] fitness=" << score << " points=" << curr_ds->points.size() << "\n";
|
|||
|
|
|
|||
|
|
if (score < 0.15) {
|
|||
|
|
// final transformation: curr -> prev
|
|||
|
|
Eigen::Matrix4f T_curr_to_prev = icp.getFinalTransformation();
|
|||
|
|
T_prev_to_curr = T_curr_to_prev.inverse();
|
|||
|
|
accept_icp = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (accept_icp) {
|
|||
|
|
g_pose_Twm = g_pose_Twm * T_prev_to_curr;
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
// se n<>o aceitamos o ICP, n<>o mexe a pose (evita teleporte)
|
|||
|
|
// isso tamb<6D>m evita mapear coisa aleat<61>ria
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// agora insere o keyframe atual no mapa global usando a pose global atual
|
|||
|
|
for (const auto& p_in : curr_keyframe->points) {
|
|||
|
|
Eigen::Vector4f v_local(p_in.x, p_in.y, p_in.z, 1.0f);
|
|||
|
|
Eigen::Vector4f v_world = g_pose_Twm * v_local;
|
|||
|
|
|
|||
|
|
pcl::PointXYZRGB p_out;
|
|||
|
|
p_out.x = v_world.x();
|
|||
|
|
p_out.y = v_world.y();
|
|||
|
|
p_out.z = v_world.z();
|
|||
|
|
p_out.r = p_in.r;
|
|||
|
|
p_out.g = p_in.g;
|
|||
|
|
p_out.b = p_in.b;
|
|||
|
|
|
|||
|
|
g_global_map->points.push_back(p_out);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
g_global_map->width = (uint32_t)g_global_map->points.size();
|
|||
|
|
g_global_map->height = 1;
|
|||
|
|
g_global_map->is_dense = true;
|
|||
|
|
|
|||
|
|
// prepara pro pr<70>ximo ICP
|
|||
|
|
*g_prev_cloud_raw = *curr_keyframe;
|
|||
|
|
|
|||
|
|
// atualiza o viewer
|
|||
|
|
*g_cloud = *g_global_map;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =====================================================
|
|||
|
|
// CALLBACK DE IMU
|
|||
|
|
// =====================================================
|
|||
|
|
void LidarImuDataCallback(const uint32_t handle, const uint8_t dev_type, LivoxLidarEthernetPacket* packet, void* /*client_data*/) {
|
|||
|
|
|
|||
|
|
using namespace std::chrono;
|
|||
|
|
uint64_t now_ms = duration_cast<milliseconds>(
|
|||
|
|
steady_clock::now().time_since_epoch()
|
|||
|
|
).count();
|
|||
|
|
|
|||
|
|
if (!packet || packet->dot_num == 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// pega a <20>ltima amostra do pacote de IMU
|
|||
|
|
auto* imu_points = reinterpret_cast<LivoxLidarImuRawPoint*>(packet->data);
|
|||
|
|
uint16_t n = packet->dot_num;
|
|||
|
|
LivoxLidarImuRawPoint imu = imu_points[n - 1];
|
|||
|
|
|
|||
|
|
// -------------------------
|
|||
|
|
// 1. guardar IMU pro HUD
|
|||
|
|
// -------------------------
|
|||
|
|
{
|
|||
|
|
std::lock_guard<std::mutex> lock(g_imu_mutex);
|
|||
|
|
|
|||
|
|
g_imu_gyro_x = imu.gyro_x;
|
|||
|
|
g_imu_gyro_y = imu.gyro_y;
|
|||
|
|
g_imu_gyro_z = imu.gyro_z;
|
|||
|
|
|
|||
|
|
g_imu_acc_x = imu.acc_x;
|
|||
|
|
g_imu_acc_y = imu.acc_y;
|
|||
|
|
g_imu_acc_z = imu.acc_z;
|
|||
|
|
|
|||
|
|
g_imu_last_ms = now_ms;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -------------------------------------------------
|
|||
|
|
// 2. integrar atitude (roll/pitch/yaw)
|
|||
|
|
// + filtro complementar com gravidade
|
|||
|
|
// -------------------------------------------------
|
|||
|
|
// assumindo gyro_* em rad/s
|
|||
|
|
float gx = imu.gyro_x;
|
|||
|
|
float gy = imu.gyro_y;
|
|||
|
|
float gz = imu.gyro_z;
|
|||
|
|
|
|||
|
|
// assumindo acc_* em "g" (1.0 ~= 9.81 m/s^2),
|
|||
|
|
// vamos converter pra m/s^2
|
|||
|
|
// se j<> vier em m/s^2 no seu SDK, coloque scale_acc = 1.0f
|
|||
|
|
const float g_m_s2 = 9.81f;
|
|||
|
|
float ax_raw = imu.acc_x * g_m_s2;
|
|||
|
|
float ay_raw = imu.acc_y * g_m_s2;
|
|||
|
|
float az_raw = imu.acc_z * g_m_s2;
|
|||
|
|
|
|||
|
|
std::lock_guard<std::mutex> pose_lock(g_pose_mutex);
|
|||
|
|
|
|||
|
|
if (g_last_imu_ms == 0) {
|
|||
|
|
// primeira leitura: s<> inicializa timestamp
|
|||
|
|
g_last_imu_ms = now_ms;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
float dt = (now_ms - g_last_imu_ms) / 1000.0f; // segundos
|
|||
|
|
g_last_imu_ms = now_ms;
|
|||
|
|
|
|||
|
|
// integra<72><61>o dos gyros (atitude "livre")
|
|||
|
|
float roll_gyro = g_roll + gx * dt;
|
|||
|
|
float pitch_gyro = g_pitch + gy * dt;
|
|||
|
|
float yaw_gyro = g_yaw + gz * dt;
|
|||
|
|
|
|||
|
|
// estima roll/pitch absoluto a partir da gravidade
|
|||
|
|
// assumindo eixos IMU:
|
|||
|
|
// X: frente
|
|||
|
|
// Y: esquerda
|
|||
|
|
// Z: cima
|
|||
|
|
// pitch = inclina<6E><61>o pra frente/tr<74>s
|
|||
|
|
// roll = inclina<6E><61>o pros lados
|
|||
|
|
float denom = std::sqrt(ay_raw * ay_raw + az_raw * az_raw);
|
|||
|
|
if (denom < 1e-6f) denom = 1e-6f;
|
|||
|
|
|
|||
|
|
float roll_acc = std::atan2(ay_raw, az_raw);
|
|||
|
|
float pitch_acc = std::atan2(-ax_raw, denom);
|
|||
|
|
|
|||
|
|
// filtro complementar
|
|||
|
|
const float alpha = 0.98f;
|
|||
|
|
float fused_roll = alpha * roll_gyro + (1.0f - alpha) * roll_acc;
|
|||
|
|
float fused_pitch = alpha * pitch_gyro + (1.0f - alpha) * pitch_acc;
|
|||
|
|
float fused_yaw = yaw_gyro; // yaw ainda s<> do gyro
|
|||
|
|
|
|||
|
|
// atualiza atitude global
|
|||
|
|
g_roll = fused_roll;
|
|||
|
|
g_pitch = fused_pitch;
|
|||
|
|
g_yaw = fused_yaw;
|
|||
|
|
|
|||
|
|
// -------------------------------------------------
|
|||
|
|
// 3. transformar acelera<72><61>o pro frame global
|
|||
|
|
// -------------------------------------------------
|
|||
|
|
// A IMU mede acelera<72><61>o no frame do sensor, incluindo gravidade.
|
|||
|
|
// A gente quer acelera<72><61>o linear do sensor no frame global.
|
|||
|
|
//
|
|||
|
|
// Passo 1: construir rota<74><61>o corpo->mundo (mesma R que usamos no mapper)
|
|||
|
|
// R = Rz(yaw)*Ry(pitch)*Rx(roll)
|
|||
|
|
|
|||
|
|
/*float cr = std::cos(g_roll);
|
|||
|
|
float sr = std::sin(g_roll);
|
|||
|
|
float cp = std::cos(g_pitch);
|
|||
|
|
float sp = std::sin(g_pitch);
|
|||
|
|
float cy = std::cos(g_yaw);
|
|||
|
|
float sy = std::sin(g_yaw);
|
|||
|
|
|
|||
|
|
float R00 = cy * cp;
|
|||
|
|
float R01 = cy * sp * sr - sy * cr;
|
|||
|
|
float R02 = cy * sp * cr + sy * sr;
|
|||
|
|
float R10 = sy * cp;
|
|||
|
|
float R11 = sy * sp * sr + cy * cr;
|
|||
|
|
float R12 = sy * sp * cr - cy * sr;
|
|||
|
|
float R20 = -sp;
|
|||
|
|
float R21 = cp * sr;
|
|||
|
|
float R22 = cp * cr;
|
|||
|
|
|
|||
|
|
// acelera<72><61>o medida no frame do sensor (m/s^2)
|
|||
|
|
// (ax_raw, ay_raw, az_raw)
|
|||
|
|
// projetar para mundo:
|
|||
|
|
float acc_world_x = R00 * ax_raw + R01 * ay_raw + R02 * az_raw;
|
|||
|
|
float acc_world_y = R10 * ax_raw + R11 * ay_raw + R12 * az_raw;
|
|||
|
|
float acc_world_z = R20 * ax_raw + R21 * ay_raw + R22 * az_raw;
|
|||
|
|
|
|||
|
|
// Passo 2: remover gravidade.
|
|||
|
|
// No mundo, gravidade <20> aproximadamente (0,0,-9.81)
|
|||
|
|
acc_world_z -= (-g_m_s2 * -1.0f);
|
|||
|
|
// cuidado: vamos deixar isso claro:
|
|||
|
|
// Se o eixo Z_global aponta "pra cima", gravidade global <20> (0,0,-9.81).
|
|||
|
|
// Ent<6E>o basta:
|
|||
|
|
acc_world_z -= (-g_m_s2); // ou seja: acc_world_z = acc_world_z + 9.81
|
|||
|
|
// vamos reescrever limpo pra n<>o confundir:
|
|||
|
|
|
|||
|
|
acc_world_z = acc_world_z + g_m_s2; // substitui a linha anterior inteira
|
|||
|
|
|
|||
|
|
// (Depois do ajuste acima, acc_world_* tenta ser acelera<72><61>o linear sem gravidade)
|
|||
|
|
|
|||
|
|
// -------------------------------------------------
|
|||
|
|
// 4. integrar acelera<72><61>o -> velocidade -> posi<73><69>o
|
|||
|
|
// -------------------------------------------------
|
|||
|
|
g_vel_x += acc_world_x * dt;
|
|||
|
|
g_vel_y += acc_world_y * dt;
|
|||
|
|
g_vel_z += acc_world_z * dt;
|
|||
|
|
|
|||
|
|
g_pos_x += g_vel_x * dt;
|
|||
|
|
g_pos_y += g_vel_y * dt;
|
|||
|
|
g_pos_z += g_vel_z * dt;*/
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =====================================================
|
|||
|
|
// CALLBACK DE INFO / ESTADO DO LIDAR
|
|||
|
|
// =====================================================
|
|||
|
|
void LidarInfoChangeCallback(const uint32_t handle, const LivoxLidarInfo* info, void* /*client_data*/) {
|
|||
|
|
|
|||
|
|
std::cout << "[INFO] LidarInfoChangeCb CHAMADO!\n";
|
|||
|
|
|
|||
|
|
if (!info) {
|
|||
|
|
std::cout << "[INFO] info == NULL\n";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::cout << "[INFO] handle=" << handle
|
|||
|
|
<< " dev_type=" << (int)info->dev_type
|
|||
|
|
<< " sn=" << info->sn
|
|||
|
|
<< " ip=" << info->lidar_ip
|
|||
|
|
<< std::endl;
|
|||
|
|
|
|||
|
|
// coloca em modo normal
|
|||
|
|
livox_status wm = SetLivoxLidarWorkMode(
|
|||
|
|
handle,
|
|||
|
|
kLivoxLidarNormal,
|
|||
|
|
nullptr,
|
|||
|
|
nullptr);
|
|||
|
|
std::cout << "[INFO] SetLivoxLidarWorkMode ret=" << wm << std::endl;
|
|||
|
|
|
|||
|
|
// habilita envio de pontos
|
|||
|
|
livox_status en = EnableLivoxLidarPointSend(
|
|||
|
|
handle,
|
|||
|
|
nullptr,
|
|||
|
|
nullptr);
|
|||
|
|
std::cout << "[INFO] EnableLivoxLidarPointSend ret=" << en << std::endl;
|
|||
|
|
|
|||
|
|
// habilita envio de imu (dependendo do SDK isso pode ser autom<6F>tico,
|
|||
|
|
// mas alguns modelos precisam de algo tipo EnableLivoxLidarImuDataSend())
|
|||
|
|
// Se existir essa fun<75><6E>o no seu SDK, chama aqui parecido:
|
|||
|
|
// livox_status en_imu = EnableLivoxLidarImuData(handle, nullptr, nullptr);
|
|||
|
|
// std::cout << "[INFO] EnableLivoxLidarImuData ret=" << en_imu << std::endl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =====================================================
|
|||
|
|
// CALLBACK DE COMANDOS DO TECLADO
|
|||
|
|
// =====================================================
|
|||
|
|
void KeyboardCallback(const pcl::visualization::KeyboardEvent& event, void*) {
|
|||
|
|
if (event.keyDown()) {
|
|||
|
|
if (event.getKeySym() == "m" || event.getKeySym() == "M") {
|
|||
|
|
modo_map = !modo_map;
|
|||
|
|
std::cout << "\n[KEY] Modo alterado para: "
|
|||
|
|
<< (modo_map ? "MAP" : "LIVE") << std::endl;
|
|||
|
|
}
|
|||
|
|
if (event.getKeySym() == "s" || event.getKeySym() == "S") {
|
|||
|
|
usar_slam = !usar_slam;
|
|||
|
|
std::cout << "\n[KEY] SLAM: "
|
|||
|
|
<< (usar_slam ? "ATIVADO (ICP ON)" : "DESATIVADO (bruto)") << std::endl;
|
|||
|
|
}
|
|||
|
|
if (event.getKeySym() == "d" || event.getKeySym() == "D") {
|
|||
|
|
ver_distancia = !ver_distancia;
|
|||
|
|
std::cout << "\n[KEY] Modo alterado para: "
|
|||
|
|
<< (ver_distancia ? "DISTANCIA" : "ALTURA") << std::endl;
|
|||
|
|
}
|
|||
|
|
if (event.getKeySym() == "b" || event.getKeySym() == "B") {
|
|||
|
|
desenhar_caixas = !desenhar_caixas;
|
|||
|
|
std::cout << "\n[KEY] Caixas de obstaculo: "
|
|||
|
|
<< (desenhar_caixas ? "ON" : "OFF") << std::endl;
|
|||
|
|
}
|
|||
|
|
if (event.getKeySym() == "i" || event.getKeySym() == "I") {
|
|||
|
|
sensor_invertido = !sensor_invertido;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =====================================================
|
|||
|
|
// MAIN
|
|||
|
|
// =====================================================
|
|||
|
|
int main(int argc, char** argv) {
|
|||
|
|
// monta caminho do config.json na mesma pasta do exe
|
|||
|
|
fs::path exe_dir = fs::current_path();
|
|||
|
|
fs::path cfg = exe_dir / "config.json";
|
|||
|
|
|
|||
|
|
std::cout << "[MAIN] usando config: " << cfg.string() << std::endl;
|
|||
|
|
|
|||
|
|
// init SDK
|
|||
|
|
if (!LivoxLidarSdkInit(cfg.string().c_str())) {
|
|||
|
|
std::cout << "[ERR] LivoxLidarSdkInit falhou\n";
|
|||
|
|
LivoxLidarSdkUninit();
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
std::cout << "[MAIN] LivoxLidarSdkInit OK\n";
|
|||
|
|
|
|||
|
|
// registra callbacks principais
|
|||
|
|
SetLivoxLidarPointCloudCallBack(LidarPointCloudCallback, nullptr);
|
|||
|
|
std::cout << "[MAIN] SetLivoxLidarPointCloudCallBack OK\n";
|
|||
|
|
|
|||
|
|
SetLivoxLidarInfoChangeCallback(LidarInfoChangeCallback, nullptr);
|
|||
|
|
std::cout << "[MAIN] SetLivoxLidarInfoChangeCallback OK\n";
|
|||
|
|
|
|||
|
|
// >>> novo: callback da IMU <<<
|
|||
|
|
// Essa fun<75><6E>o pode ter nome ligeiramente diferente dependendo da vers<72>o do SDK:
|
|||
|
|
// Ex.: SetLivoxLidarImuDataCallback, SetLivoxLidarImuCallBack, etc.
|
|||
|
|
// Use o nome que existir no seu livox_lidar_api.h.
|
|||
|
|
SetLivoxLidarImuDataCallback(LidarImuDataCallback, nullptr);
|
|||
|
|
std::cout << "[MAIN] SetLivoxLidarImuDataCallback OK\n";
|
|||
|
|
|
|||
|
|
// inicia sdk
|
|||
|
|
if (!LivoxLidarSdkStart()) {
|
|||
|
|
std::cout << "[ERR] LivoxLidarSdkStart falhou\n";
|
|||
|
|
LivoxLidarSdkUninit();
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
std::cout << "[MAIN] LivoxLidarSdkStart OK\n";
|
|||
|
|
|
|||
|
|
// cria viewer PCL
|
|||
|
|
pcl::visualization::PCLVisualizer::Ptr viewer(
|
|||
|
|
new pcl::visualization::PCLVisualizer("Mid-360 Live"));
|
|||
|
|
|
|||
|
|
viewer->setBackgroundColor(0, 0, 0);
|
|||
|
|
viewer->addCoordinateSystem(1.0);
|
|||
|
|
viewer->initCameraParameters();
|
|||
|
|
viewer->registerKeyboardCallback(KeyboardCallback);
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
std::lock_guard<std::mutex> lock(g_cloud_mutex);
|
|||
|
|
g_cloud->clear();
|
|||
|
|
g_cloud->width = 1;
|
|||
|
|
g_cloud->height = 1;
|
|||
|
|
g_cloud->is_dense = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(g_cloud);
|
|||
|
|
viewer->addPointCloud<pcl::PointXYZRGB>(g_cloud, rgb, "livox_cloud");
|
|||
|
|
viewer->setPointCloudRenderingProperties(
|
|||
|
|
pcl::visualization::PCL_VISUALIZER_POINT_SIZE,
|
|||
|
|
2,
|
|||
|
|
"livox_cloud");
|
|||
|
|
|
|||
|
|
std::cout << "[MAIN] Viewer + SDK rodando. Fecha a janela pra sair.\n";
|
|||
|
|
|
|||
|
|
// loop principal
|
|||
|
|
while (!viewer->wasStopped() && g_running.load()) {
|
|||
|
|
|
|||
|
|
// snapshot nuvem atual que vamos desenhar/usar pra percep<65><70>o
|
|||
|
|
pcl::PointCloud<pcl::PointXYZRGB>::Ptr local_copy(new pcl::PointCloud<pcl::PointXYZRGB>);
|
|||
|
|
{
|
|||
|
|
std::lock_guard<std::mutex> lock(g_cloud_mutex);
|
|||
|
|
*local_copy = *g_cloud;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// atualiza visualiza<7A><61>o da nuvem
|
|||
|
|
{
|
|||
|
|
pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb2(local_copy);
|
|||
|
|
viewer->updatePointCloud<pcl::PointXYZRGB>(local_copy, rgb2, "livox_cloud");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HUD texto
|
|||
|
|
viewer->removeShape("hud_text");
|
|||
|
|
{
|
|||
|
|
std::stringstream ss;
|
|||
|
|
ss << (modo_map ? "MODE: MAP " : "MODE: LIVE ")
|
|||
|
|
<< (ver_distancia ? "DISTANCIA " : "ALTURA ")
|
|||
|
|
<< "| SLAM: " << (usar_slam ? "ON" : "OFF")
|
|||
|
|
<< " | BOX: " << (desenhar_caixas ? "ON" : "OFF");
|
|||
|
|
viewer->addText(ss.str(), 10, 10, 14, 1.0, 1.0, 1.0, "hud_text");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// detec<65><63>o de obst<73>culos a cada DETECTION_PERIOD_MS
|
|||
|
|
{
|
|||
|
|
using namespace std::chrono;
|
|||
|
|
uint64_t now_ms = duration_cast<milliseconds>(
|
|||
|
|
steady_clock::now().time_since_epoch()
|
|||
|
|
).count();
|
|||
|
|
|
|||
|
|
if (now_ms - g_last_detection_ms >= DETECTION_PERIOD_MS) {
|
|||
|
|
g_last_detection_ms = now_ms;
|
|||
|
|
DetectarClustersEAdicionarCaixas(viewer, local_copy);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
viewer->spinOnce(15);
|
|||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
|||
|
|
|
|||
|
|
// 7. Imprime IMU no console, etc, como voc<6F> j<> fazia...
|
|||
|
|
/*{
|
|||
|
|
std::lock_guard<std::mutex> lock(g_imu_mutex);
|
|||
|
|
float acc_norm = std::sqrt(
|
|||
|
|
g_imu_acc_x * g_imu_acc_x +
|
|||
|
|
g_imu_acc_y * g_imu_acc_y +
|
|||
|
|
g_imu_acc_z * g_imu_acc_z
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
std::cout << "\r"
|
|||
|
|
<< std::fixed << std::setprecision(3)
|
|||
|
|
<< "[IMU] gyro: "
|
|||
|
|
<< " gx=" << std::setw(7) << g_imu_gyro_x
|
|||
|
|
<< " gy=" << std::setw(7) << g_imu_gyro_y
|
|||
|
|
<< " gz=" << std::setw(7) << g_imu_gyro_z
|
|||
|
|
<< " | acc(g?): "
|
|||
|
|
<< " ax=" << std::setw(7) << g_imu_acc_x
|
|||
|
|
<< " ay=" << std::setw(7) << g_imu_acc_y
|
|||
|
|
<< " az=" << std::setw(7) << g_imu_acc_z
|
|||
|
|
<< " | |acc|=" << std::setw(6) << acc_norm
|
|||
|
|
<< " | t_ms=" << g_imu_last_ms
|
|||
|
|
<< " "
|
|||
|
|
<< std::flush;
|
|||
|
|
}*/
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
g_running.store(false);
|
|||
|
|
LivoxLidarSdkUninit();
|
|||
|
|
std::cout << "\n[MAIN] Encerrado.\n";
|
|||
|
|
return 0;
|
|||
|
|
}
|