From 766b9de62871c785b6922c73a702b7b7fdf10cdd Mon Sep 17 00:00:00 2001 From: Paul Leroy Date: Wed, 22 Oct 2025 12:08:15 +0200 Subject: [PATCH] Compute normals using flann and CloudCompare --- CMakeLists.txt | 59 +++++++---- include/G3PointAction.h | 52 ++++++++- src/G3Point.cpp | 16 --- src/G3PointAction.cpp | 226 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 311 insertions(+), 42 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 77f51cc..e52bbad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,10 +11,10 @@ if ( PLUGIN_G3POINT ) AddPlugin( NAME ${PROJECT_NAME} ) -target_sources(G3PointPlugin - PRIVATE - ui/WolmanCustomPlot.ui -) + target_sources(G3PointPlugin + PRIVATE + ui/WolmanCustomPlot.ui + ) set(QG3POINT_PLUGIN_VERSION "0.6") @@ -23,14 +23,6 @@ target_sources(G3PointPlugin add_subdirectory( ui ) # target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) # for mlpack - - target_include_directories( ${PROJECT_NAME} PRIVATE - C:/opt/eigen-3.4.0 - C:/Users/PaulLeroy/miniconda3/envs/env_4_CloudCompare/include - C:/opt/open3d-devel-windows-amd64-0.19.0/include - C:/opt/GeometricTools/GTE - C:/opt/boost_1_77_0 - ) # may be needed for debug set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") @@ -42,19 +34,44 @@ target_sources(G3PointPlugin Qt5::PrintSupport ) - # Find installed Open3D, which exports Open3D::Open3D - find_package( Open3D REQUIRED ) - target_link_libraries( ${PROJECT_NAME} Open3D::Open3D ) + # EIGEN + set( EIGEN_ROOT_DIR "" CACHE PATH "Eigen root (contains the Eigen directory)" ) + if ( NOT EIGEN_ROOT_DIR ) + message( SEND_ERROR "No Eigen root directory specified (EIGEN_ROOT_DIR)" ) + else() + message( STATUS "EIGEN_ROOT_DIR " ${EIGEN_ROOT_DIR} ) + endif() + + # OPEN3D + option(USE_OPEN3D_WITH_G3POINT "Enable the use of Open3D with G3Point" OFF) + if (USE_OPEN3D_WITH_G3POINT) + find_package( Open3D REQUIRED ) # Find installed Open3D, which exports Open3D::Open3D + target_link_libraries( ${PROJECT_NAME} Open3D::Open3D ) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set( OPENCV_DEP_DLL_FILES + ${Open3D_DIR}/../bin/Open3D.dll + ${Open3D_DIR}/../bin/tbb12_debug.dll) + elseif(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + set( OPENCV_DEP_DLL_FILES + ${Open3D_DIR}/../bin/Open3D.dll + ${Open3D_DIR}/../bin/tbb12.dll) + endif() + message( "Open3D_DIR ${Open3D_DIR}" ) + copy_files( "${OPENCV_DEP_DLL_FILES}" "${CLOUDCOMPARE_DEST_FOLDER}" 1) #mind the quotes! + endif() - message( "Open3D_DIR ${Open3D_DIR}" ) - set( OPENCV_DEP_DLL_FILES - ${Open3D_DIR}/../bin/Open3D.dll - ${Open3D_DIR}/../bin/tbb12.dll - ) - copy_files( "${OPENCV_DEP_DLL_FILES}" "${CLOUDCOMPARE_DEST_FOLDER}" 1) #mind the quotes! copy_files( "${CMAKE_CURRENT_SOURCE_DIR}/shaders/DrawGrains.vs" "${CLOUDCOMPARE_DEST_FOLDER}/shaders/G3Point" 1) #mind the quotes! copy_files( "${CMAKE_CURRENT_SOURCE_DIR}/shaders/DrawGrains.fs" "${CLOUDCOMPARE_DEST_FOLDER}/shaders/G3Point" 1) #mind the quotes! + target_include_directories( ${PROJECT_NAME} PRIVATE + C:/Users/PaulLeroy/miniconda3/envs/env_4_CloudCompare/include + C:/opt/open3d-devel-windows-amd64-0.19.0/include + C:/opt/GeometricTools/GTE + C:/opt/boost_1_77_0 + ../../../../libs/qCC_db/extern/CCCoreLib/extern/nanoflann/include + ${EIGEN_ROOT_DIR} + ) + #================ # git commit hash # Get the current working branch diff --git a/include/G3PointAction.h b/include/G3PointAction.h index 78b04bc..87a1bb8 100644 --- a/include/G3PointAction.h +++ b/include/G3PointAction.h @@ -1,24 +1,27 @@ #include "Eigen/Dense" +#include +// qCC_db #include #include +#include + +// CCCoreLib +#include +#include #include #include #include - #include - #include - #include #pragma once class ccMainAppInterface; -class ccPointCloud; namespace G3Point { @@ -53,6 +56,38 @@ public: template static bool EigenArrayToFile(QString name, T array); + + // A small adaptor to let nanoflann access ccPointCloud data + struct CloudAdaptor + { + const ccPointCloud* cloud; + + CloudAdaptor(const ccPointCloud* c) : cloud(c) {} + + // Must return the number of data points + inline size_t kdtree_get_point_count() const { return cloud->size(); } + + // Returns the dim'th component of the idx'th point + inline float kdtree_get_pt(const size_t idx, int dim) const + { + if (dim == 0) + return cloud->getPoint(static_cast(idx))->x; + else if (dim == 1) + return cloud->getPoint(static_cast(idx))->y; + else + return cloud->getPoint(static_cast(idx))->z; + } + + // Optional bounding-box computation: return false to default to a standard bbox computation loop. + template + bool kdtree_get_bbox(BBOX&) const { return false; } + }; + + // Typedef for a 3D KD-tree index + using KDTree = nanoflann::KDTreeSingleIndexAdaptor, + CloudAdaptor, + 3 /* dim */>; + private: bool sfConvertToRandomRGB(const ccHObject::Container &selectedEntities, QWidget* parent); void addToStack(int index, const Eigen::ArrayXi& n_donors, const Eigen::ArrayXXi& donors, std::vector& stack); @@ -70,6 +105,13 @@ private: bool computeNormalsAndOrientThemWithCloudCompare(); void orientNormals(const Eigen::Vector3d &sensorCenter); bool computeNormalsWithOpen3D(); + static bool FindNearestNeighborsNanoFlann(ccPointCloud* cloud, unsigned globalIndex, int k, + CCCoreLib::ReferenceCloud *points, KDTree *kdTree); + static bool ComputeNormsAtLevel(const CCCoreLib::DgmOctree::octreeCell& cell, + void** additionalParameters, + CCCoreLib::NormalizedProgress* nProgress=nullptr); + bool computeNormalsWithCloudCompare(); + bool computeNormals(); bool queryNeighbors(ccPointCloud* cloud, ccMainAppInterface* appInterface, bool useParallelStrategy=true); void init(); void showDlg(); @@ -118,5 +160,7 @@ private: GrainsAsEllipsoids* m_grainsAsEllipsoids; int m_currentNumberOfSteps; + + QSharedPointer m_kdTree; }; } diff --git a/src/G3Point.cpp b/src/G3Point.cpp index 426f266..1b34e1f 100644 --- a/src/G3Point.cpp +++ b/src/G3Point.cpp @@ -15,22 +15,6 @@ //# # //########################################################################## -// First: -// Replace all occurrences of 'G3PointPlugin' by your own plugin class name in this file. -// This includes the resource path to info.json in the constructor. - -// Second: -// Open G3PointPlugin.qrc, change the "prefix" and the icon filename for your plugin. -// Change the name of the file to .qrc - -// Third: -// Open the info.json file and fill in the information about the plugin. -// "type" should be one of: "Standard", "GL", or "I/O" (required) -// "name" is the name of the plugin (required) -// "icon" is the Qt resource path to the plugin's icon (from the .qrc file) -// "description" is used as a tootip if the plugin has actions and is displayed in the plugin dialog -// "authors", "maintainers", and "references" show up in the plugin dialog as well - #include #include "G3Point.h" diff --git a/src/G3PointAction.cpp b/src/G3PointAction.cpp index 4598413..7e77adb 100644 --- a/src/G3PointAction.cpp +++ b/src/G3PointAction.cpp @@ -1,4 +1,6 @@ #include "G3PointAction.h" +#include "DgmOctreeReferenceCloud.h" +#include "Neighbourhood.h" // CCPluginAPI #include @@ -33,7 +35,9 @@ #include // Open3D +#ifdef USE_OPEN3D_WITH_G3POINT #include +#endif // Eigen #include @@ -1755,6 +1759,7 @@ void G3PointAction::orientNormals(const Eigen::Vector3d& sensorCenter) bool G3PointAction::computeNormalsWithOpen3D() { +#ifdef USE_OPEN3D_WITH_G3POINT // create an open3D point cloud from the original point cloud std::vector points(m_cloud->size()); for (int index =0; index < points.size(); index++) // copy all points @@ -1807,6 +1812,225 @@ bool G3PointAction::computeNormalsWithOpen3D() } return true; +#else + return false; +#endif +} + +bool G3PointAction::FindNearestNeighborsNanoFlann(ccPointCloud* cloud, + unsigned globalIndex, + int kNN, + CCCoreLib::ReferenceCloud* points, + KDTree* kdTree) +{ + // Prepare query + const CCVector3* Q = cloud->getPoint(globalIndex); + float query[3] = { Q->x, Q->y, Q->z }; + + std::vector retIndexes(kNN); + std::vector outDistsSqr(kNN); + + // Perform search + nanoflann::KNNResultSet resultSet(kNN); + resultSet.init(&retIndexes[0], &outDistsSqr[0]); + if(kdTree->findNeighbors(resultSet, &query[0])) + { + points->resize(kNN); + for (int i = 0; i < kNN; ++i) + { + points->setPointIndex(i, retIndexes[i]); + } + return true; + } + else + { + return false; + } +} + +bool G3PointAction::ComputeNormsAtLevel(const CCCoreLib::DgmOctree::octreeCell& cell, + void** additionalParameters, + CCCoreLib::NormalizedProgress* nProgress /*=nullptr*/) +{ + // additional parameters + NormsTableType* theNorms = static_cast(additionalParameters[0]); + int* kNN = static_cast(additionalParameters[1]); + KDTree* kdTree = static_cast(additionalParameters[2]); + ccPointCloud* cloud = static_cast(additionalParameters[3]); + + CCCoreLib::DgmOctree::NearestNeighboursSearchStruct nNSS; + nNSS.level = cell.level; + nNSS.minNumberOfNeighbors = *kNN; + cell.parentOctree->getCellPos(cell.truncatedCode, cell.level, nNSS.cellPos, true); + cell.parentOctree->computeCellCenter(nNSS.cellPos, cell.level, nNSS.cellCenter); + + // we already know which points are lying in the current cell + unsigned pointCount = cell.points->size(); + nNSS.pointsInNeighbourhood.resize(pointCount); + CCCoreLib::DgmOctree::NeighboursSet::iterator it = nNSS.pointsInNeighbourhood.begin(); + { + for (unsigned j = 0; j < pointCount; ++j, ++it) + { + it->point = cell.points->getPointPersistentPtr(j); + it->pointIndex = cell.points->getPointGlobalIndex(j); + } + } + nNSS.alreadyVisitedNeighbourhoodSize = 1; + + for (unsigned i = 0; i < pointCount; ++i) + { + cell.points->getPoint(i, nNSS.queryPoint); + unsigned int globalIndex = cell.points->getPointGlobalIndex(i); + + CCVector3 N; + + QScopedPointer points(new CCCoreLib::ReferenceCloud(cloud)); + if(FindNearestNeighborsNanoFlann(cloud, globalIndex, *kNN, points.data(), kdTree)) + { + CCCoreLib::Neighbourhood neighbourhood(points.data()); + N = *neighbourhood.getLSPlaneNormal(); + } + else + { + return false; + } + + theNorms->setValue(globalIndex, N); + + if (nProgress && !nProgress->oneStep()) + return false; + } + + return true; +} + +bool G3PointAction::computeNormalsWithCloudCompare() +{ + unsigned pointCount = m_cloud->size(); + + if (!m_cloud || m_cloud->size() == 0) + { + std::cerr << "Invalid cloud.\n"; + return false; + } + + CloudAdaptor adaptor(m_cloud); + + // Build KD-tree (parameter: number of leaf nodes to inspect per query) + + size_t leaf_max_size = 10; + nanoflann::KDTreeSingleIndexAdaptorFlags flags = nanoflann::KDTreeSingleIndexAdaptorFlags::None; + unsigned int n_thread_build = 0; // 0 => nanoflann automatically determines the number of threads to use + + nanoflann::KDTreeSingleIndexAdaptorParams params(leaf_max_size, flags, n_thread_build); + m_kdTree.reset(new KDTree(3, adaptor, params)); + m_kdTree->buildIndex(); + + ccOctree::Shared octree = m_cloud->getOctree(); + if (octree.isNull()) + { + octree.reset(new ccOctree(m_cloud)); + if (octree->build() <= 0) + { + octree.clear(); + return false; + } + } + unsigned char level = octree->findBestLevelForAGivenPopulationPerCell(m_kNN); + + // we instantiate 3D normal vectors + QScopedPointer theNorms(new NormsTableType); + QScopedPointer normsIndexes(new NormsIndexesTableType); + static const CCVector3 blankN(0, 0, 0); + if (!theNorms->resizeSafe(pointCount, true, &blankN)) + { + normsIndexes->resize(0); + if (nullptr == octree) + { + octree.clear(); + } + return false; + } + // theNorms->fill(0); + + void* additionalParameters[4] = {reinterpret_cast(theNorms.data()), + reinterpret_cast(&m_kNN), + reinterpret_cast(m_kdTree.data()), + reinterpret_cast(m_cloud)}; + + unsigned processedCells = 0; + QScopedPointer progressCb; + processedCells = octree->executeFunctionForAllCellsStartingAtLevel(level, + &(ComputeNormsAtLevel), + additionalParameters, + m_kNN / 2, + m_kNN * 3, + true, + progressCb.data(), + "Normals Computation [G3Point]"); + + // error or canceled by user? + if (processedCells == 0 || (progressCb && progressCb->isCancelRequested())) + { + normsIndexes->resize(0); + return false; + } + + if (!m_cloud->hasNormals()) + { + if (!m_cloud->resizeTheNormsTable()) + { + ccLog::Error(QString("Not enough memory to compute normals on cloud '%1'").arg(m_cloud->getName())); + return false; + } + } + + // we hide normals during process + m_cloud->showNormals(false); + + // compress the normals + for (unsigned i = 0; i < theNorms->currentSize(); i++) + { + const CCVector3& N = theNorms->at(i); + const CompressedNormType nCode = ccNormalVectors::GetNormIndex(N); + m_cloud->setPointNormalIndex(i, nCode); + } + + // preferred orientation + ccNormalVectors::UpdateNormalOrientations(m_cloud, *m_cloud->normals(), ccNormalVectors::PLUS_Z); + + return true; +} + +bool G3PointAction::computeNormals() +{ + // if there are normals, already, propose to keep them + if (m_cloud->hasNormals()) + { + QMessageBox msgBox; + msgBox.setInformativeText("Recompute normals?"); + msgBox.setText("There are existing normals, keep them or recompute."); + QPushButton *keepButton = msgBox.addButton(tr("Keep"), QMessageBox::ActionRole); + QPushButton *recomputeButton = msgBox.addButton(tr("Recompute"), QMessageBox::AcceptRole); + QPushButton *cancelButton = msgBox.addButton(tr("Cancel"), QMessageBox::AcceptRole); + + msgBox.exec(); + + if (msgBox.clickedButton() == keepButton) + { + return true; + } + else if (msgBox.clickedButton() == cancelButton) + { + return false; + } + } + +#ifdef USE_OPEN3D_WITH_G3POINT + return computeNormalsWithOpen3D(); +#else + return computeNormalsWithCloudCompare(); +#endif } bool G3PointAction::queryNeighbors(ccPointCloud* cloud, ccMainAppInterface* appInterface, bool useParallelStrategy) @@ -1879,7 +2103,7 @@ void G3PointAction::segment() computeNodeSurfaces(); - computeNormalsWithOpen3D(); + computeNormals(); // compute the centroid unsigned pointCount = m_cloud->size();