commit b51c884df751c28ead445cfa6b1b397da8fea242 Author: Ioannis (Yiannis) Farmakis Date: Sat Sep 21 15:00:32 2024 +0300 Add files via upload diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7bd3754 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.18) + +option( PLUGIN_STANDARD_QVOXFALL "Install qVoxFall plugin" ON ) + +if ( PLUGIN_STANDARD_QVOXFALL ) + project( QVOXFALL_PLUGIN) + + AddPlugin( NAME ${PROJECT_NAME} ) + + add_subdirectory( include ) + add_subdirectory( src ) + add_subdirectory( ui ) + +endif() diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000..ad5ddf2 Binary files /dev/null and b/images/icon.png differ diff --git a/images/iconSwap.png b/images/iconSwap.png new file mode 100644 index 0000000..e815af0 Binary files /dev/null and b/images/iconSwap.png differ diff --git a/images/university_of_newcastle_icon.png b/images/university_of_newcastle_icon.png new file mode 100644 index 0000000..a2a9abc Binary files /dev/null and b/images/university_of_newcastle_icon.png differ diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt new file mode 100644 index 0000000..7c28107 --- /dev/null +++ b/include/CMakeLists.txt @@ -0,0 +1,14 @@ + +target_sources( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/qVoxFall.h + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallDialog.h + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallDisclaimerDialog.h + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallProcess.h + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallTools.h +) + +target_include_directories( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/include/qVoxFall.h b/include/qVoxFall.h new file mode 100644 index 0000000..c07ed86 --- /dev/null +++ b/include/qVoxFall.h @@ -0,0 +1,55 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + +#pragma once + +#ifndef Q_VOXFALL_PLUGIN_HEADER +#define Q_VOXFALL_PLUGIN_HEADER + +#include "ccStdPluginInterface.h" + + +//qCC_db +#include + + +class qVoxFall : public QObject, public ccStdPluginInterface +{ + Q_OBJECT + Q_INTERFACES( ccPluginInterface ccStdPluginInterface ) + Q_PLUGIN_METADATA( IID "cccorp.cloudcompare.plugin.qVoxFall" FILE "../info.json" ) + +public: + explicit qVoxFall( QObject *parent = nullptr ); + ~qVoxFall() override = default; + + // Inherited from ccStdPluginInterface + void onNewSelection( const ccHObject::Container &selectedEntities ) override; + QList getActions() override; + +private: + + void doAction(); + + //! Default action + QAction* m_action; + + //! Currently selected entities + ccHObject::Container m_selectedEntities; +}; + +#endif //Q_VOXFALL_PLUGIN_HEADER diff --git a/include/qVoxFallDialog.h b/include/qVoxFallDialog.h new file mode 100644 index 0000000..e9b545c --- /dev/null +++ b/include/qVoxFallDialog.h @@ -0,0 +1,82 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: XXX # +//# # +//########################################################################## + +#pragma once + +#ifndef Q_VOXFALL_DIALOG_HEADER +#define Q_VOXFALL_DIALOG_HEADER + +#include + +//Qt +#include + +class ccMainAppInterface; +class ccMesh; + +//! VOXFALL plugin's main dialog +class qVoxFallDialog : public QDialog, public Ui::VoxFallDialog +{ + Q_OBJECT + +public: + + //! Default constructor + qVoxFallDialog(ccMesh* mesh1, ccMesh* mesh2, ccMainAppInterface* app); + + //! Returns mesh #1 + ccMesh* getMesh1() const { return m_mesh1; } + //! Returns mesh #2 + ccMesh* getMesh2() const { return m_mesh2; } + + //! Returns voxel size + double getVoxelSize() const; + //! Returns slope azimuth + double getAzimuth() const; + //! Returns whether the blocks will be exported as meshes + bool getExportMeshesActivation() const; + //! Labels the blocks as loss or gain clusters + bool getLossGainActivation() const; + + //! Returns the max number of threads to use + int getMaxThreadCount() const; + + void qVoxFallDialog::loadParamsFromPersistentSettings(); + void qVoxFallDialog::loadParamsFrom(const QSettings& settings); + void qVoxFallDialog::saveParamsToPersistentSettings(); + void qVoxFallDialog::saveParamsTo(QSettings& settings); + +protected: + + void swapMeshes(); + void setMesh1Visibility(bool); + void setMesh2Visibility(bool); + +protected: //methods + + //! Sets meshes + void setMeshes(ccMesh* mesh1, ccMesh* mesh2); + +protected: //members + + ccMainAppInterface* m_app; + + ccMesh* m_mesh1; + ccMesh* m_mesh2; +}; + +#endif //Q_VOXFALL_DIALOG_HEADER diff --git a/include/qVoxFallDisclaimerDialog.h b/include/qVoxFallDisclaimerDialog.h new file mode 100644 index 0000000..787d5d2 --- /dev/null +++ b/include/qVoxFallDisclaimerDialog.h @@ -0,0 +1,47 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 or later of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + +#pragma once + +#ifndef VOXFALL_DISCLAIMER_DIALOG_HEADER +#define VOXFALL_DISCLAIMER_DIALOG_HEADER + +#include + +class ccMainAppInterface; + +namespace Ui { + class DisclaimerDialog; +} + +//! Dialog for displaying the VoxFall disclaimer +class DisclaimerDialog : public QDialog +{ +public: + DisclaimerDialog(QWidget* parent = nullptr); + ~DisclaimerDialog(); + + static bool show(ccMainAppInterface* app); + +private: + //whether disclaimer has already been displayed (and accepted) or not + static bool s_disclaimerAccepted; + + Ui::DisclaimerDialog* m_ui; +}; + +#endif //VOXFALL_DISCLAIMER_DIALOG_HEADER diff --git a/include/qVoxFallProcess.h b/include/qVoxFallProcess.h new file mode 100644 index 0000000..1995f1f --- /dev/null +++ b/include/qVoxFallProcess.h @@ -0,0 +1,52 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + +#pragma once + +#ifndef Q_VOXFALL_PROCESS_HEADER +#define Q_VOXFALL_PROCESS_HEADER + +//Local +#include "qVoxFallDialog.h" + +//qCC +#include "ccPointCloud.h" + + +class ccMainAppInterface; + +//! VoxFall process +/** See "VoxFall: Non-Parametric Volumetric Change Detection for Rockfalls", + Farmakis, I., Guccione, D.E., Thoeni, K. and Giacomini, A., 2024, + Computers and Geosciences +**/ +class qVoxFallProcess +{ +public: + + static bool Compute(const qVoxFallDialog& dlg, + QString& errorMessage, + bool allowDialogs, + QWidget* parentWidget = nullptr, + ccMainAppInterface* app = nullptr); + +}; + +#endif //Q_VOXFALL_PROCESS_HEADER + + + diff --git a/include/qVoxFallTools.h b/include/qVoxFallTools.h new file mode 100644 index 0000000..023b4c6 --- /dev/null +++ b/include/qVoxFallTools.h @@ -0,0 +1,85 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + +#pragma once + +#ifndef Q_VOXFALL_TOOLS_HEADER +#define Q_VOXFALL_TOOLS_HEADER + +//CCCoreLib +#include + +//local +#include "qVoxFallDialog.h" + +//qCC_db +#include +#include +#include +#include + +#include + + +class qVoxFallTransform +{ + double az; + float zRot; + +public: + ccGLMatrix* matrix; + ccGLMatrix* inverse; + qVoxFallTransform(double azimuth) + { + az = azimuth; + zRot = GetRotationAngle(azimuth); + + const Vector3Tpl X(std::cos(zRot), std::sin(zRot), 0); + const Vector3Tpl Y(-std::sin(zRot), std::cos(zRot), 0); + const Vector3Tpl Z(0, 0, 1); + const Vector3Tpl Tr(0, 0, 0); + matrix = &ccGLMatrix::ccGLMatrix(X, Y, Z, Tr); + + const Vector3Tpl rX(std::cos(-zRot), std::sin(-zRot), 0); + const Vector3Tpl rY(-std::sin(-zRot), std::cos(-zRot), 0); + const Vector3Tpl rZ(0, 0, 1); + const Vector3Tpl rTr(0, 0, 0); + inverse = &ccGLMatrix::ccGLMatrix(rX, rY, rZ, rTr); + } + + static ccBox* CreateVoxelMesh(CCVector3 V, float voxelSize, int voxelIdx); + +private: + static float GetRotationAngle(double azimuth); + +}; + + +class qVoxFallTools +{ +public: + + static std::vector FindAdjacents(Tuple3i V, CCVector3 steps, bool facetsOnly); + + static int Grid2Index(Tuple3i n, CCVector3 steps); + + static Tuple3i Index2Grid(unsigned index, CCVector3 steps); + +}; + + +#endif //Q_VOXFALL_PROCESS_HEADER diff --git a/info.json b/info.json new file mode 100644 index 0000000..2d4abfc --- /dev/null +++ b/info.json @@ -0,0 +1,28 @@ +{ + "type" : "Standard", + "core" : true, + "name" : "VOXFALL", + "icon" : ":/CC/plugin/qVoxFall/images/icon.png", + "description": "Voxelized rockFall (VoxFall).", + "authors" : [ + { + "name" : "Ioannis Farmakis", + "email" : "i.farmakis@icloud.com" + } + ], + "maintainers" : [ + { + "name" : "Ioannis Farmakis", + "email" : "i.farmakis@icloud.com" + } + ], + "references" : [ + { + "text" : "VoxFall: Non-parametric volumetric change detection for rockfalls", + "url" : "to be published by Engineering Geology" + }, + { + "text" : "The idea behind the development of this plug in was generated during my post-doc spell at the University of Newcastle, Australia." + } + ] +} diff --git a/qVoxFall.qrc b/qVoxFall.qrc new file mode 100644 index 0000000..3bc69f0 --- /dev/null +++ b/qVoxFall.qrc @@ -0,0 +1,8 @@ + + + images/university_of_newcastle_icon.png + images/icon.png + images/iconSwap.png + info.json + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..a13a38f --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,9 @@ + +target_sources( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/qVoxFall.cpp + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallDialog.cpp + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallDisclaimerDialog.cpp + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallProcess.cpp + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallTools.cpp +) diff --git a/src/qVoxFall.cpp b/src/qVoxFall.cpp new file mode 100644 index 0000000..2e528f7 --- /dev/null +++ b/src/qVoxFall.cpp @@ -0,0 +1,139 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + + +#include "qVoxFall.h" + +//Qt +#include + +//local +#include "qVoxFallDialog.h" +#include "qVoxFallDisclaimerDialog.h" +#include "qVoxFallProcess.h" + +//qCC_db +#include + + + +qVoxFall::qVoxFall( QObject *parent ) + : QObject( parent ) + , ccStdPluginInterface( ":/CC/plugin/qVoxFall/info.json" ) + , m_action( nullptr ) +{ +} + +void qVoxFall::onNewSelection( const ccHObject::Container &selectedEntities ) +{ + if ( m_action ) + { + m_action->setEnabled( selectedEntities.size() == 2 + && selectedEntities[0]->isA(CC_TYPES::MESH) + && selectedEntities[1]->isA(CC_TYPES::MESH) ); + } + + m_selectedEntities = selectedEntities; +} + +QList qVoxFall::getActions() +{ + if ( !m_action ) + { + m_action = new QAction( getName(), this ); + m_action->setToolTip( getDescription() ); + m_action->setIcon( getIcon() ); + + // Connect signal + connect( m_action, &QAction::triggered, this, &qVoxFall::doAction ); + } + + return { m_action }; +} + + +void qVoxFall::doAction() +{ + //disclaimer accepted? + if (!DisclaimerDialog::show(m_app)) + return; + + //m_app should have already been initialized by CC when plugin is loaded! + assert(m_app); + if (!m_app) + return; + + if (m_selectedEntities.size() != 2 + || !m_selectedEntities[0]->isA(CC_TYPES::MESH) + || !m_selectedEntities[1]->isA(CC_TYPES::MESH)) + { + m_app->dispToConsole("Select two meshes !", ccMainAppInterface::ERR_CONSOLE_MESSAGE); + return; + } + + ccMesh* mesh1 = ccHObjectCaster::ToMesh(m_selectedEntities[0]); + ccMesh* mesh2 = ccHObjectCaster::ToMesh(m_selectedEntities[1]); + + + m_app->dispToConsole("[VoxFall] Meshes loaded successfully", ccMainAppInterface::STD_CONSOLE_MESSAGE); + + //display dialog + qVoxFallDialog dlg(mesh1, mesh2, m_app); + if (!dlg.exec()) + { + //process cancelled by the user + return; + } + + ////display the voxel size in console + m_app->dispToConsole(QString("[VoxFall] Voxel size: %1 m").arg(dlg.getVoxelSize()), ccMainAppInterface::STD_CONSOLE_MESSAGE); + + QString errorMessage; + if (!qVoxFallProcess::Compute(dlg, errorMessage, true, m_app->getMainWindow(), m_app)) + { + if (!errorMessage.isEmpty()) + { + m_app->dispToConsole(errorMessage, ccMainAppInterface::WRN_CONSOLE_MESSAGE); + } + else + { + mesh1->setEnabled(false); + m_app->dispToConsole("[VoxFall] Completed!", ccMainAppInterface::STD_CONSOLE_MESSAGE); + } + } + + //'Compute' may change some parameters of the dialog + dlg.saveParamsToPersistentSettings(); +} + + + + + + + + + + + + + + + + + + diff --git a/src/qVoxFallDialog.cpp b/src/qVoxFallDialog.cpp new file mode 100644 index 0000000..092a66d --- /dev/null +++ b/src/qVoxFallDialog.cpp @@ -0,0 +1,223 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + +#include "qVoxFallDialog.h" + +//CCPluginAPI +#include +#include + +//qCC_db +#include + +//Qt +#include +#include + + +/*** HELPERS ***/ +static QString GetEntityName(ccHObject* obj) +{ + if (!obj) + { + assert(false); + return QString(); + } + + QString name = obj->getName(); + if (name.isEmpty()) + name = "unnamed"; + name += QString(" [ID %1]").arg(obj->getUniqueID()); + + return name; +} + +static ccMesh* GetMeshFromCombo(QComboBox* comboBox, ccHObject* dbRoot) +{ + assert(comboBox && dbRoot); + if (!comboBox || !dbRoot) + { + assert(false); + return nullptr; + } + + //return the mesh currently selected in the combox box + int index = comboBox->currentIndex(); + if (index < 0) + { + assert(false); + return nullptr; + } + assert(comboBox->itemData(index).isValid()); + unsigned uniqueID = comboBox->itemData(index).toUInt(); + ccHObject* item = dbRoot->find(uniqueID); + if (!item || !item->isA(CC_TYPES::MESH)) + { + assert(false); + return nullptr; + } + return static_cast(item); +} + +/*** HELPERS (END) ***/ + +qVoxFallDialog::qVoxFallDialog(ccMesh* mesh1, ccMesh* mesh2, ccMainAppInterface* app) + : QDialog(app ? app->getMainWindow() : nullptr) + , Ui::VoxFallDialog() + , m_app(app) + , m_mesh1(nullptr) + , m_mesh2(nullptr) +{ + setupUi(this); + + connect(showMesh1CheckBox, &QAbstractButton::toggled, this, &qVoxFallDialog::setMesh1Visibility); + connect(showMesh2CheckBox, &QAbstractButton::toggled, this, &qVoxFallDialog::setMesh2Visibility); + + connect(swapMeshesToolButton, &QAbstractButton::clicked, this, &qVoxFallDialog::swapMeshes); + + setMeshes(mesh1, mesh2); + + loadParamsFromPersistentSettings(); +} + +void qVoxFallDialog::swapMeshes() +{ + setMeshes(m_mesh2, m_mesh1); +} + +void qVoxFallDialog::setMeshes(ccMesh* mesh1, ccMesh* mesh2) +{ + if (!mesh1 || !mesh2) + { + assert(false); + return; + } + + m_mesh1 = mesh1; + m_mesh2 = mesh2; + + //mesh #1 + mesh1LineEdit->setText(GetEntityName(mesh1)); + showMesh1CheckBox->blockSignals(true); + showMesh1CheckBox->setChecked(mesh1->isVisible()); + showMesh1CheckBox->blockSignals(false); + + //mesh #2 + mesh2LineEdit->setText(GetEntityName(mesh2)); + showMesh2CheckBox->blockSignals(true); + showMesh2CheckBox->setChecked(mesh2->isVisible()); + showMesh2CheckBox->blockSignals(false); +} + +void qVoxFallDialog::setMesh1Visibility(bool state) +{ + if (m_mesh1) + { + m_mesh1->setVisible(state); + m_mesh1->prepareDisplayForRefresh(); + } + if (m_app) + { + m_app->refreshAll(); + m_app->updateUI(); + } +} + +void qVoxFallDialog::setMesh2Visibility(bool state) +{ + if (m_mesh2) + { + m_mesh2->setVisible(state); + m_mesh2->prepareDisplayForRefresh(); + } + if (m_app) + { + m_app->refreshAll(); + m_app->updateUI(); + } +} + +double qVoxFallDialog::getVoxelSize() const +{ + double voxelSize = voxelSizeDoubleSpinBox->value(); + return voxelSize; +} + +double qVoxFallDialog::getAzimuth() const +{ + double azimuth = azDoubleSpinBox->value(); + return azimuth; +} + +bool qVoxFallDialog::getExportMeshesActivation() const +{ + return exportCheckBox->isChecked(); +} + +bool qVoxFallDialog::getLossGainActivation() const +{ + return lossCheckBox->isChecked(); +} + +int qVoxFallDialog::getMaxThreadCount() const +{ + return QThread::idealThreadCount(); +} + +void qVoxFallDialog::loadParamsFromPersistentSettings() +{ + QSettings settings("qVoxFall"); + loadParamsFrom(settings); +} + +void qVoxFallDialog::loadParamsFrom(const QSettings& settings) +{ + //read parameters + double voxelSize = settings.value("VoxelSize", voxelSizeDoubleSpinBox->value()).toDouble(); + double azimuth = settings.value("Azimuth", azDoubleSpinBox->value()).toDouble(); + bool exportMeshesEnabled = settings.value("ExportMeshesEnabled", exportCheckBox->isChecked()).toBool(); + bool lossGainEnabled = settings.value("LossGainEnabled", lossCheckBox->isChecked()).toBool(); + + //apply parameters + voxelSizeDoubleSpinBox->setValue(voxelSize); + azDoubleSpinBox->setValue(azimuth); + exportCheckBox->setChecked(exportMeshesEnabled); + lossCheckBox->setChecked(lossGainEnabled); +} + +void qVoxFallDialog::saveParamsToPersistentSettings() +{ + QSettings settings("qVoxFall"); + saveParamsTo(settings); +} + +void qVoxFallDialog::saveParamsTo(QSettings& settings) +{ + //save parameters + settings.setValue("VoxelSize", voxelSizeDoubleSpinBox->value()); + settings.setValue("Azimuth", azDoubleSpinBox->value()); + settings.setValue("ExportMeshesEnabled", exportCheckBox->isChecked()); + settings.setValue("LossGainEnabled", lossCheckBox->isChecked()); +} + + + + + + + + diff --git a/src/qVoxFallDisclaimerDialog.cpp b/src/qVoxFallDisclaimerDialog.cpp new file mode 100644 index 0000000..dfa36ac --- /dev/null +++ b/src/qVoxFallDisclaimerDialog.cpp @@ -0,0 +1,51 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + +#include "qVoxFallDisclaimerDialog.h" +#include "ui_disclaimerDlg.h" + +//qCC_plugins +#include + +//Qt +#include + +bool DisclaimerDialog::s_disclaimerAccepted = false; + + +DisclaimerDialog::DisclaimerDialog(QWidget *parent) + : QDialog(parent) + , m_ui( new Ui::DisclaimerDialog ) +{ + m_ui->setupUi( this ); +} + +DisclaimerDialog::~DisclaimerDialog() +{ + delete m_ui; +} + +bool DisclaimerDialog::show(ccMainAppInterface *app) +{ + if ( !s_disclaimerAccepted ) + { + //if the user "cancels" it, then he refuses the disclaimer + s_disclaimerAccepted = DisclaimerDialog(app ? app->getMainWindow() : 0).exec(); + } + + return s_disclaimerAccepted; +} diff --git a/src/qVoxFallProcess.cpp b/src/qVoxFallProcess.cpp new file mode 100644 index 0000000..e7afc9a --- /dev/null +++ b/src/qVoxFallProcess.cpp @@ -0,0 +1,747 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + +#include "qVoxFallProcess.h" + +//system +#include + +//local +#include "qVoxFallDialog.h" +#include "qVoxFallTools.h" + +//CCCoreLib +#include +#include "Grid3D.h" + +//qCC_plugins +#include +#include + +//qCC_db +#include +#include +#include +#include +#include +#include +#include +#include + +//Qt +#include +#include +#include +#include +#include +#include + +#if defined(_OPENMP) +//OpenMP +#include +#endif +using namespace CCCoreLib; + + +//! Default name for VoxFall scalar fields +static const char OCCUPANCY_SF_NAME[] = "Occupancy"; +static const char CLUSTER_SF_NAME[] = "Cluster ID"; +static const char CHANGE_TYPE_SF_NAME[] = "Loss/gain"; +static const char VOLUME_SF_NAME[] = "Volume (m3)"; +static const char UNCERTAINTY_SF_NAME[] = "Uncertainty (%)"; + + +// Structure for parallel call +struct VoxFallParams +{ + //main options + float voxelSize = 0; + int clusterLabel = 0; + int currentLabel; + int changeType; + bool exportBlocksAsMeshes = false; + bool exportLossGain = false; + CCVector3 minBound, maxBound, extent, steps; + + //helpers + std::vector> nbs; + std::vector isEmpty; + std::vector isEmptyBefore; + std::vector nonEmptyVoxelsVisited; + std::vector clusters; + int emptyVoxelCount = 0; + CCVector3 centroid; + CCVector3 bbDims; + std::vector volumes; + std::vector clusterIndices; + int clusterOutterVoxelCount; + + //export + ccPointCloud* voxfall = nullptr; + + //scalar fields + ccScalarField* clusterSF = nullptr; //cluster ID + ccScalarField* changeTypeSF = nullptr; //loss or gain + ccScalarField* volumeSF = nullptr; //block volume + ccScalarField* uncertaintySF = nullptr; //volume uncertainty + + //progress notification + CCCoreLib::NormalizedProgress* nProgress = nullptr; + bool processCanceled = false; + bool processFailed = false; +}; +static VoxFallParams s_VoxFallParams; + + +bool InitializeOutputCloud(int voxelCount, GenericProgressCallback* progressCb = nullptr) +{ + //progress notification + NormalizedProgress nProgress(progressCb, voxelCount); + if (progressCb) + { + if (progressCb->textCanBeEdited()) + { + progressCb->setInfo("Initialization"); + progressCb->setMethodTitle("VoxFall Detection"); + } + progressCb->update(0); + progressCb->start(); + } + + float voxelSize = s_VoxFallParams.voxelSize; + CCVector3 minBound = s_VoxFallParams.minBound; + + for (int index = 0; index < voxelCount; ++index) + { + Tuple3i V = qVoxFallTools::Index2Grid(index, s_VoxFallParams.steps); + CCVector3 P(static_cast(V.x * voxelSize + minBound.x), + static_cast(V.y * voxelSize + minBound.y), + static_cast(V.z * voxelSize + minBound.z)); + s_VoxFallParams.voxfall->addPoint(P); + + //progress bar + if (progressCb && !nProgress.oneStep()) + { + return false; + } + } + + return true; +} + + +void GetVoxelOccupancy(const Tuple3i& cellPos, unsigned n) +{ + int index = qVoxFallTools::Grid2Index(cellPos, s_VoxFallParams.steps); + s_VoxFallParams.isEmpty[index] = false; +} + + +void GetVoxelOccupancyBefore(const Tuple3i& cellPos, unsigned n) +{ + int index = qVoxFallTools::Grid2Index(cellPos, s_VoxFallParams.steps); + s_VoxFallParams.isEmptyBefore[index] = false; +} + + +bool ClusterEmptySpace(int maxThreads, int voxelCount, GenericProgressCallback* progressCb = nullptr) +{ + //progress notification + NormalizedProgress nProgress(progressCb, voxelCount); + if (progressCb) + { + if (progressCb->textCanBeEdited()) + { + char buffer[64]; + snprintf(buffer, 64, "Clustering empty space \n Voxels: %u", voxelCount); + progressCb->setInfo(buffer); + progressCb->setMethodTitle("VoxFall Detection"); + } + progressCb->update(0); + progressCb->start(); + } + + auto steps = s_VoxFallParams.steps; + s_VoxFallParams.nbs.resize(voxelCount); +#if defined(_OPENMP) +#pragma omp parallel for schedule(static) \ + num_threads(maxThreads) +#endif + for (int index = 0; index < voxelCount; ++index) { + auto V = qVoxFallTools::Index2Grid(index, steps); + auto NN = qVoxFallTools::FindAdjacents(V, steps, false); + for (auto const& n : NN) + { + int nIdx = qVoxFallTools::Grid2Index(n, steps); + s_VoxFallParams.nbs[index].push_back(nIdx); + } +#if defined(_OPENMP) +#pragma omp critical(ClusterEmptySpace) + { nProgress.oneStep(); } +#endif + } + for (int index = 0; index < voxelCount; ++index) + { + // Check if voxel is empty. + if (!s_VoxFallParams.isEmpty[index]) + continue; + + // Label is not undefined. + if (s_VoxFallParams.clusterSF->getValue(index) != -1) + { + continue; + } + + // Check density. + int nCount = 0; + for (auto const& n : s_VoxFallParams.nbs[index]) + { + if (s_VoxFallParams.isEmpty[n]) + { + nCount++; + } + } + + std::unordered_set nbs_next(s_VoxFallParams.nbs[index].begin(), s_VoxFallParams.nbs[index].end()); + std::unordered_set visited; + visited.insert(index); + + s_VoxFallParams.clusterSF->setValue(index, static_cast(s_VoxFallParams.clusterLabel)); + if (s_VoxFallParams.clusterLabel > 0) // keep track of the total voxels included in volumes + { + s_VoxFallParams.emptyVoxelCount++; + } + if (progressCb && !nProgress.oneStep()) //progress bar + { + return false; + } + while (!nbs_next.empty()) + { + unsigned nb = *nbs_next.begin(); + nbs_next.erase(nbs_next.begin()); + // Check empty neighbor. + if (!s_VoxFallParams.isEmpty[nb]) + { + continue; + } + visited.insert(nb); + + // Not undefined label. + if (s_VoxFallParams.clusterSF->getValue(nb) != -1) + { + continue; + } + s_VoxFallParams.clusterSF->setValue(nb, static_cast(s_VoxFallParams.clusterLabel)); + if (s_VoxFallParams.clusterLabel > 0) // keep track of the total voxels included in volumes + { + s_VoxFallParams.emptyVoxelCount++; + } + if (progressCb && !nProgress.oneStep()) //progress bar + { + return false; + } + + // Get neighbor's density. + int nCount = 0; + for (auto const& n : s_VoxFallParams.nbs[nb]) + { + if (s_VoxFallParams.isEmpty[n]) + { + nCount++; + } + } + if (nCount >= 1) + { + for (int qnb : s_VoxFallParams.nbs[nb]) + { + if (s_VoxFallParams.isEmpty[qnb]) + { + if (visited.count(qnb) == 0) + { + nbs_next.insert(qnb); + } + } + } + } + } + s_VoxFallParams.clusterLabel++; + } + return true; +} + + +bool ComputeClusterVolume(int maxThreads, int clusterCount, ccHObject* clusterGroup = nullptr) +{ + + bool error = false; + CCVector3 minBound = s_VoxFallParams.maxBound; + CCVector3 maxBound = s_VoxFallParams.minBound; + int count = 0; + + if (s_VoxFallParams.processCanceled) + return error; + +#if defined(_OPENMP) +#pragma omp parallel for schedule(static) \ + num_threads(maxThreads) +#endif + for (int i = 0; i < clusterCount; i++) + { + int index = s_VoxFallParams.clusterIndices[i]; + + std::unordered_set nbs_next(s_VoxFallParams.nbs[index].begin(), s_VoxFallParams.nbs[index].end()); + while (!nbs_next.empty()) + { + unsigned nb = *nbs_next.begin(); + nbs_next.erase(nbs_next.begin()); + + // Check non empty neighbor. + if (s_VoxFallParams.isEmpty[nb]) + { + continue; + } + if (s_VoxFallParams.nonEmptyVoxelsVisited[nb] == false) + { + s_VoxFallParams.clusterOutterVoxelCount++; + s_VoxFallParams.nonEmptyVoxelsVisited[nb] = true; + + if (s_VoxFallParams.exportLossGain) + { + Tuple3i V = qVoxFallTools::Index2Grid(nb, s_VoxFallParams.steps); + CCVector3 voxel(static_cast(V.x * s_VoxFallParams.voxelSize + s_VoxFallParams.minBound.x), + static_cast(V.y * s_VoxFallParams.voxelSize + s_VoxFallParams.minBound.y), + static_cast(V.z * s_VoxFallParams.voxelSize + s_VoxFallParams.minBound.z)); + + if (voxel.x > maxBound.x) maxBound.x = static_cast(voxel.x); + if (voxel.y > maxBound.y) maxBound.y = static_cast(voxel.y); + if (voxel.z > maxBound.z) maxBound.z = static_cast(voxel.z); + + if (voxel.x < minBound.x) minBound.x = static_cast(voxel.x); + if (voxel.y < minBound.y) minBound.y = static_cast(voxel.y); + if (voxel.z < minBound.z) minBound.z = static_cast(voxel.z); + } + + } + if (s_VoxFallParams.exportBlocksAsMeshes) + { + s_VoxFallParams.clusters[nb] = s_VoxFallParams.currentLabel; + } + } + + //progress bar + if (!s_VoxFallParams.nProgress->oneStep()) + { + error = true; + break; + } + if (error) break; + } + + if (s_VoxFallParams.exportLossGain) + { + float ymin = minBound.y; + float ymax = maxBound.y; + CCVector3 extent = maxBound - minBound; + CCVector3 center = minBound + extent / 2; + minBound += extent / 2 * 0.9; + maxBound -= extent / 2 * 0.9; + maxBound.y = ymax + (ymax - ymin) / 2.0; + + s_VoxFallParams.centroid = minBound + (maxBound - minBound) / 1.5; + s_VoxFallParams.bbDims = (maxBound - minBound) / 2; + } + + if (error) return !error; + return !error; +} + + +bool qVoxFallProcess::Compute(const qVoxFallDialog& dlg, QString& errorMessage, bool allowDialogs, QWidget* parentWidget/*=nullptr*/, ccMainAppInterface* app/*=nullptr*/) +{ + errorMessage.clear(); + + //get the input meshes in the right order + ccMesh* mesh1 = dlg.getMesh1(); + ccMesh* mesh2 = dlg.getMesh2(); + + if (!mesh1 || !mesh2) + { + assert(false); + return false; + } + + //get parameters from dialog + double azimuth = dlg.getAzimuth(); + + //max thread count + int maxThreadCount = dlg.getMaxThreadCount(); + + if (app) + app->dispToConsole( QString("[VoxFall] Will use %1 threads").arg(maxThreadCount == 0 ? "the max number of" : QString::number(maxThreadCount)), + ccMainAppInterface::STD_CONSOLE_MESSAGE ); + + //progress dialog + ccProgressDialog pDlg(parentWidget); + + //Duration: initialization + QElapsedTimer initTimer; + initTimer.start(); + + auto mesh = mesh1->cloneMesh(); + mesh->merge(mesh2, false); + + auto transform = qVoxFallTransform(azimuth); + mesh->applyGLTransformation_recursive(transform.matrix); + mesh1->applyGLTransformation_recursive(transform.matrix); + + mesh1->setEnabled(false); + + //parameters are stored in 's_VoxFallParams' for parallel call + s_VoxFallParams = VoxFallParams(); + s_VoxFallParams.voxelSize = dlg.getVoxelSize(); + s_VoxFallParams.minBound = mesh->getOwnBB().minCorner(); + s_VoxFallParams.maxBound = mesh->getOwnBB().maxCorner(); + s_VoxFallParams.extent = s_VoxFallParams.maxBound - s_VoxFallParams.minBound; + s_VoxFallParams.steps = (s_VoxFallParams.extent / s_VoxFallParams.voxelSize) + Vector3Tpl(1, 1, 1); + s_VoxFallParams.exportBlocksAsMeshes = dlg.getExportMeshesActivation(); + s_VoxFallParams.exportLossGain = dlg.getLossGainActivation(); + s_VoxFallParams.voxfall = new ccPointCloud(mesh1->getName() + "_to_" + mesh2->getName() + QString(" [VoxFall grid] (voxel %1 m)").arg(s_VoxFallParams.voxelSize)); + + //Initialize voxel grid + auto voxelGrid = CCCoreLib::Grid3D(); + if (!voxelGrid.init( int(s_VoxFallParams.steps.x), + int(s_VoxFallParams.steps.y), + int(s_VoxFallParams.steps.z), + 0 )) //margin + { + errorMessage = "Failed to initialize voxel grid!"; + return false; + } + + // Initialize heplpers + s_VoxFallParams.voxfall->reserve(voxelGrid.innerCellCount()); + s_VoxFallParams.nbs.resize(voxelGrid.innerCellCount()); + s_VoxFallParams.isEmpty.resize(voxelGrid.innerCellCount(), true); + s_VoxFallParams.isEmptyBefore.resize(voxelGrid.innerCellCount(), true); + if (s_VoxFallParams.exportBlocksAsMeshes) + { + s_VoxFallParams.clusters.resize(voxelGrid.innerCellCount(), NULL); + } + + //allocate cluster ID SF + s_VoxFallParams.clusterSF = new ccScalarField(CLUSTER_SF_NAME); + s_VoxFallParams.clusterSF->link(); + if (!s_VoxFallParams.clusterSF->resizeSafe(voxelGrid.innerCellCount(), true, static_cast(-1.0))) + { + errorMessage = "Failed to allocate memory for cluster ID values!"; + return false; + } + + if (s_VoxFallParams.exportLossGain) + { + //allocate change type SF + s_VoxFallParams.changeTypeSF = new ccScalarField(CHANGE_TYPE_SF_NAME); + s_VoxFallParams.changeTypeSF->link(); + if (!s_VoxFallParams.changeTypeSF->resizeSafe(voxelGrid.innerCellCount(), true, CCCoreLib::NAN_VALUE)) + { + errorMessage = "Failed to allocate memory for change type values!"; + return false; + } + } + //allocate volume SF + s_VoxFallParams.volumeSF = new ccScalarField(VOLUME_SF_NAME); + s_VoxFallParams.volumeSF->link(); + if (!s_VoxFallParams.volumeSF->resizeSafe(voxelGrid.innerCellCount(), true, CCCoreLib::NAN_VALUE)) + { + errorMessage = "Failed to allocate memory for volume values!"; + return false; + } + //allocate volume uncertainty SF + s_VoxFallParams.uncertaintySF = new ccScalarField(UNCERTAINTY_SF_NAME); + s_VoxFallParams.uncertaintySF->link(); + if (!s_VoxFallParams.uncertaintySF->resizeSafe(voxelGrid.innerCellCount(), true, CCCoreLib::NAN_VALUE)) + { + errorMessage = "Failed to allocate memory for volume uncertainty values!"; + return false; + } + + // Initialize output cloud + if (!InitializeOutputCloud(voxelGrid.innerCellCount(), &pDlg)) + { + errorMessage = "Failed to initialize output data!"; + return false; + } + + qint64 initTime_ms = initTimer.elapsed(); + //we display init. timing only if no error occurred! + if (app) + app->dispToConsole( QString("[VoxFall] Initialization: %1 s").arg(initTime_ms / 1000.0, 0, 'f', 3), + ccMainAppInterface::STD_CONSOLE_MESSAGE ); + + +// BLOCK DETECTION +//======================================================================================================================= + + //Duration: Detection + QElapsedTimer detectTimer; + detectTimer.start(); + + if (!voxelGrid.intersectWith( mesh, + s_VoxFallParams.voxelSize, + s_VoxFallParams.minBound, + GetVoxelOccupancy, + &pDlg )) + { + errorMessage = "Failed to compute grid occupancy!"; + return false; + } + + + if (s_VoxFallParams.exportLossGain) + { + if (!voxelGrid.intersectWith(mesh1, + s_VoxFallParams.voxelSize, + s_VoxFallParams.minBound, + GetVoxelOccupancyBefore, + &pDlg)) + { + errorMessage = "Failed to compute grid occupancy!"; + return false; + } + } + + //cluster DBSCAN + if (!ClusterEmptySpace( maxThreadCount, + voxelGrid.innerCellCount(), + &pDlg )) + { + errorMessage = "Failed to compute grid occupancy!"; + return false; + } + + qint64 detectTime_ms = detectTimer.elapsed(); + //we display block extraction timing only if no error occurred! + if (app) + app->dispToConsole(QString("[VoxFall] Block detection: %1 s").arg(detectTime_ms / 1000.0, 0, 'f', 3), + ccMainAppInterface::STD_CONSOLE_MESSAGE); + app->dispToConsole( QString("[VoxFall] Blocks found: %1").arg(s_VoxFallParams.clusterLabel - 1), + ccMainAppInterface::STD_CONSOLE_MESSAGE ); + +// COMPUTE VOLUMES +//======================================================================================================================= + + //Duration: volume computation + QElapsedTimer volumeTimer; + volumeTimer.start(); + + //progress notification + pDlg.reset(); + NormalizedProgress nProgress(&pDlg, s_VoxFallParams.emptyVoxelCount); + char buffer[64]; + snprintf(buffer, 64, "VoxFall clusters: %u \n Empty voxels: %u", s_VoxFallParams.clusterLabel - 1, s_VoxFallParams.emptyVoxelCount); + pDlg.setInfo(buffer); + pDlg.setMethodTitle(QObject::tr("Compute Volumes")); + pDlg.update(0); + pDlg.start(); + s_VoxFallParams.nProgress = &nProgress; + + + s_VoxFallParams.volumes.reserve(s_VoxFallParams.clusterLabel); + s_VoxFallParams.nonEmptyVoxelsVisited.resize(voxelGrid.innerCellCount(), false); + for (unsigned label = 1; label < s_VoxFallParams.clusterLabel; ++label) + { + auto it = std::find(s_VoxFallParams.clusterSF->begin(), s_VoxFallParams.clusterSF->end(), label); + while (it != s_VoxFallParams.clusterSF->end()) + { + s_VoxFallParams.clusterIndices.push_back(it - s_VoxFallParams.clusterSF->begin()); + it = std::find(it + 1, s_VoxFallParams.clusterSF->end(), label); + } + + s_VoxFallParams.currentLabel = label; + s_VoxFallParams.clusterOutterVoxelCount = 0; + + + if (!ComputeClusterVolume( maxThreadCount, s_VoxFallParams.clusterIndices.size() )) + { + errorMessage = "Failed to compute cluster volume!"; + return false; + } + + if (s_VoxFallParams.exportLossGain) + { + int count = 0; + mesh1->placeIteratorAtBeginning(); + for (unsigned n = 0; n < mesh1->size(); n++) + { + //get the positions (in the grid) of each vertex + const GenericTriangle* T = mesh1->_getNextTriangle(); + + //current triangle vertices + const CCVector3* triPoints[3]{ T->_getA(), T->_getB(), T->_getC() }; + + if (CCMiscTools::TriBoxOverlap(s_VoxFallParams.centroid, s_VoxFallParams.bbDims, triPoints)) + { + count++; + } + } + if (count > 0) + { + s_VoxFallParams.changeType = -1; + } + else + { + s_VoxFallParams.changeType = 1; + } + } + ScalarType changeType = static_cast(s_VoxFallParams.changeType); + ScalarType uncertainty = static_cast(pow(s_VoxFallParams.voxelSize, 3) * s_VoxFallParams.clusterOutterVoxelCount / 2); + ScalarType volume = static_cast(pow(s_VoxFallParams.voxelSize, 3) * s_VoxFallParams.clusterIndices.size() + uncertainty); + s_VoxFallParams.volumes[label - 1] = static_cast(volume); + + for (unsigned i = 0; i < s_VoxFallParams.clusterIndices.size(); i++) + { + if (s_VoxFallParams.exportLossGain) + { + s_VoxFallParams.changeTypeSF->setValue(s_VoxFallParams.clusterIndices[i], changeType); + } + s_VoxFallParams.volumeSF->setValue(s_VoxFallParams.clusterIndices[i], volume); + s_VoxFallParams.uncertaintySF->setValue(s_VoxFallParams.clusterIndices[i], volume/uncertainty/100); + } + s_VoxFallParams.clusterIndices.clear(); + } + + qint64 volumeTime_ms = volumeTimer.elapsed(); + //we display block volume computation timing only if no error occurred! + if (app) + app->dispToConsole(QString("[VoxFall] Volume computation: %1 s").arg(volumeTime_ms / 1000.0, 0, 'f', 3), + ccMainAppInterface::STD_CONSOLE_MESSAGE); + + +// EXPORT BLOCKS AS VOXEL MESH MODELS (IF SELECTED) +//======================================================================================================================= + + if (s_VoxFallParams.exportBlocksAsMeshes) + { + //Duration: block meshing + QElapsedTimer meshTimer; + meshTimer.start(); + + //progress notification + pDlg.reset(); + NormalizedProgress nProgress(&pDlg, s_VoxFallParams.emptyVoxelCount); + char buffer[64]; + snprintf(buffer, 64, "Blocks: %u", s_VoxFallParams.clusterLabel - 1); + pDlg.setInfo(buffer); + pDlg.setMethodTitle(QObject::tr("Exporting blocks as meshes")); + pDlg.update(0); + pDlg.start(); + + //we create a new group to store all output meshes as 'VoxFall clusters' + ccHObject* ccGroup = new ccHObject(mesh1->getName() + "_to_" + mesh2->getName() + QString(" [VoxFall clusters] (voxel %1 m)").arg(s_VoxFallParams.voxelSize)); + + for (unsigned label = 1; label < s_VoxFallParams.clusterLabel; ++label) + { + std::vector indices; + auto it = std::find(s_VoxFallParams.clusters.begin(), s_VoxFallParams.clusters.end(), label); + while (it != s_VoxFallParams.clusters.end()) + { + indices.push_back(it - s_VoxFallParams.clusters.begin()); + it = std::find(it + 1, s_VoxFallParams.clusters.end(), label); + } + + ccHObject* clusterGroup = new ccHObject(QString("Cluster#%1 - (v: %2 m3)").arg(label).arg(s_VoxFallParams.volumes[label - 1])); + clusterGroup->setVisible(true); + ccGroup->addChild(clusterGroup); + for (int i = 0; i < indices.size(); i++) + { + CCVector3 V; + s_VoxFallParams.voxfall->getPoint(indices[i], V); + auto voxel = qVoxFallTransform::CreateVoxelMesh(V, s_VoxFallParams.voxelSize, indices[i]); + clusterGroup->addChild(voxel); + + //progress bar + if (!nProgress.oneStep()) + { + return false; + } + } + indices.clear(); + } + ccGroup->applyGLTransformation_recursive(transform.inverse); + ccGroup->setVisible(true); + app->addToDB(ccGroup); + + qint64 meshTime_ms = meshTimer.elapsed(); + //we display block as mesh export timing only if no error occurred! + if (app) + app->dispToConsole(QString("[VoxFall] Block as mesh export: %1 s").arg(meshTime_ms / 1000.0, 0, 'f', 3), + ccMainAppInterface::STD_CONSOLE_MESSAGE); + } + + +// OUTPUT FORMATION +//======================================================================================================================= + + //associate cluster ID scalar fields to the voxel grid + int sfIdx = -1; + if (s_VoxFallParams.clusterSF) + { + //add cluster ID SF to voxel grid + s_VoxFallParams.clusterSF->computeMinAndMax(); + sfIdx = s_VoxFallParams.voxfall->addScalarField(s_VoxFallParams.clusterSF); + } + if (s_VoxFallParams.exportLossGain) + { + //associate change type scalar fields to the voxel grid + if (s_VoxFallParams.changeTypeSF) + { + //add cluster ID SF to voxel grid + s_VoxFallParams.changeTypeSF->computeMinAndMax(); + sfIdx = s_VoxFallParams.voxfall->addScalarField(s_VoxFallParams.changeTypeSF); + } + } + //associate volume scalar field to the voxel grid + if (s_VoxFallParams.volumeSF) + { + //add volume SF to voxel grid + s_VoxFallParams.volumeSF->computeMinAndMax(); + sfIdx = s_VoxFallParams.voxfall->addScalarField(s_VoxFallParams.volumeSF); + } + //associate volume uncertainty scalar field to the voxel grid + if (s_VoxFallParams.uncertaintySF) + { + //add volume uncertainty SF to voxel grid + s_VoxFallParams.uncertaintySF->computeMinAndMax(); + sfIdx = s_VoxFallParams.voxfall->addScalarField(s_VoxFallParams.uncertaintySF); + } + + //prepare export cloud + mesh1->applyGLTransformation_recursive(transform.inverse); + s_VoxFallParams.voxfall->applyGLTransformation_recursive(transform.inverse); + sfIdx = s_VoxFallParams.voxfall->getScalarFieldIndexByName(CLUSTER_SF_NAME); + s_VoxFallParams.voxfall->setCurrentDisplayedScalarField(sfIdx);; + s_VoxFallParams.voxfall->showSF(true); + if (s_VoxFallParams.exportBlocksAsMeshes) + { + s_VoxFallParams.voxfall->setEnabled(false); + } + app->addToDB(s_VoxFallParams.voxfall); + + if (app) + app->refreshAll(); + +} diff --git a/src/qVoxFallTools.cpp b/src/qVoxFallTools.cpp new file mode 100644 index 0000000..49c3497 --- /dev/null +++ b/src/qVoxFallTools.cpp @@ -0,0 +1,150 @@ +//########################################################################## +//# # +//# CLOUDCOMPARE PLUGIN: qVoxFall # +//# # +//# This program is free software; you can redistribute it and/or modify # +//# it under the terms of the GNU General Public License as published by # +//# the Free Software Foundation; version 2 of the License. # +//# # +//# This program is distributed in the hope that it will be useful, # +//# but WITHOUT ANY WARRANTY; without even the implied warranty of # +//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +//# GNU General Public License for more details. # +//# # +//# COPYRIGHT: THE UNIVERSITY OF NEWCASTLE # +//# # +//########################################################################## + + +#include "qVoxFallTools.h" + +//qCC_db +#include + +//qCC +#include +#include + +//local +#include "qVoxFallDialog.h" + +//Qt +#include +#include +#include +#include +#include + + + +float qVoxFallTransform::GetRotationAngle(double azimuth) +{ + double azimuthRadians = azimuth * 3.14159 / 180; + + std::vector direction = { sin(azimuthRadians), cos(azimuthRadians) }; + std::vector xyView = { 0, 1 }; + + // compute dot product of unit vectors + double dotProduct = 0; + for (int i = 0; i < direction.size(); i++) + { + direction[i] = direction[i] / sqrt(direction[0] * direction[0] + direction[1] * direction[1]); + xyView[i] = xyView[i] / sqrt(xyView[0] * xyView[0] + xyView[1] * xyView[1]); + dotProduct += direction[i] * xyView[i]; + } + + float zRot = acos(std::max(-1.0, std::min(dotProduct, 1.0))); + + return zRot; +} + + +ccBox* qVoxFallTransform::CreateVoxelMesh(CCVector3 V, float voxelSize, int voxelIdx) +{ + CCVector3 dims = { voxelSize, voxelSize, voxelSize }; + QString name = QString("voxel#%1").arg(voxelIdx); + + const Vector3Tpl X(1, 0, 0); + const Vector3Tpl Y(0, 1, 0); + const Vector3Tpl Z(0, 0, 1); + ccGLMatrix* matrix = &ccGLMatrix::ccGLMatrix(X, Y, Z, V); + + ccBox* voxel = new ccBox(dims, matrix, name); + voxel->computePerTriangleNormals(); + return voxel; +} + + + +std::vector qVoxFallTools::FindAdjacents(Tuple3i V, CCVector3 steps, bool facetsOnly=false) +{ + std::vector set; + std::vector> adjacencyMatrix; + + if (!facetsOnly) + { + adjacencyMatrix = { + {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, + {0, -1, 0}, {0, 0, 1}, {0, 0, -1}, + {1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, + {-1, -1, 0}, {0, 1, 1}, {0, 1, -1}, + {0, -1, 1}, {0, -1, -1}, {1, 0, 1}, + {1, 0, -1}, {-1, 0, 1}, {-1, 0, -1}, + {1, 1, 1}, {-1, -1, -1}, {1, 1, -1}, + {1, -1, 1}, {-1, 1, 1}, {1, -1, -1}, + {-1, -1, 1}, {-1, 1, -1} + }; + } + else + { + adjacencyMatrix = { + {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, + {0, -1, 0}, {0, 0, 1}, {0, 0, -1}, + }; + } + + for (unsigned n = 0; n < adjacencyMatrix.size(); n++) + { + + int x = int(V.x) + adjacencyMatrix[n][0]; + int y = int(V.y) + adjacencyMatrix[n][1]; + int z = int(V.z) + adjacencyMatrix[n][2]; + + if (x < 0 || y < 0 || z < 0 || x >= int(steps.x) || y >= int(steps.y) || z >= int(steps.z)) + { + continue; + } + + set.push_back({ x, y, z }); + } + + return set; +} + + +int qVoxFallTools::Grid2Index(Tuple3i n, CCVector3 steps) +{ + int i = n.x; + int j = n.y; + int k = n.z; + + int index = (i)+(j * int(steps.x)) + (k * int(steps.x) * int(steps.y)); + return index; +} + + +Tuple3i qVoxFallTools::Index2Grid(unsigned index, CCVector3 steps) +{ + int k = std::floor(index / (int(steps.y) * int(steps.x))); + int remain = index - (int(steps.y) * int(steps.x) * k); + int j = std::floor(remain / int(steps.x)); + int i = remain - (int(steps.x) * j); + + Tuple3i V( static_cast(i), + static_cast(j), + static_cast(k) ); + return V; +} + + + diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt new file mode 100644 index 0000000..68cc0b3 --- /dev/null +++ b/ui/CMakeLists.txt @@ -0,0 +1,6 @@ + +target_sources( ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/disclaimerDlg.ui + ${CMAKE_CURRENT_LIST_DIR}/qVoxFallDialog.ui +) diff --git a/ui/disclaimerDlg.ui b/ui/disclaimerDlg.ui new file mode 100644 index 0000000..3ec5daf --- /dev/null +++ b/ui/disclaimerDlg.ui @@ -0,0 +1,112 @@ + + + DisclaimerDialog + + + + 0 + 0 + 540 + 250 + + + + qVoxFall (disclaimer) + + + + :/CC/plugin/qVoxFall/images/icon.png:/CC/plugin/qVoxFall/images/icon.png + + + + + + + + + 150 + 174 + + + + + + + :/CC/plugin/qVoxFall/images/university_of_newcastle_icon.png + + + true + + + + + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; background-color:#ffffff;"><span style=" font-family:'Calibri,sans-serif'; font-size:10pt; font-weight:600; color:#1f497d;">Voxelized rockfall (VoxFall) detection</span></p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; background-color:#ffffff;"><span style=" font-family:'Calibri,sans-serif'; font-size:10pt; font-style:italic; color:#1f497d;">Farmakis et al., VoxFall: Non-parametric volumetric change detection for rockfalls, Engineering Geology, 2024</span></p> +<p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Calibri,sans-serif'; font-size:10pt; font-style:italic; color:#1f497d; background-color:#ffffff;"><br /></p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; background-color:#ffffff;"><span style=" font-family:'MS Shell Dlg 2,serif'; font-size:8pt; color:#aa007f; background-color:#ffffff;">© The University of Newcastle 2024. This program is free software and can be redistributed and/or modified under the terms of version 3 of the GNU General Public License (GPLv3). This program is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License v3 for more details.</span></p> +<p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'arial,sans-serif'; font-size:10pt; font-style:italic; color:#222222; background-color:#ffffff;"><br /></p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; background-color:#ffffff;"><span style=" font-family:'Calibri,sans-serif'; font-size:10pt; color:#1f497d; background-color:#ffffff;">Research funded by the Australian Coal Association Research Program (ACARP) and the Australian Research Council (ARC).</span></p></body></html> + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + DisclaimerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DisclaimerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ui/qVoxFallDialog.ui b/ui/qVoxFallDialog.ui new file mode 100644 index 0000000..a60434d --- /dev/null +++ b/ui/qVoxFallDialog.ui @@ -0,0 +1,254 @@ + + + VoxFallDialog + + + + 0 + 0 + 500 + 220 + + + + VoxFall Detection + + + + :/CC/plugin/qVoxFall/images/icon.png:/CC/plugin/qVoxFall/images/icon.png + + + + + + + + + + Mesh #1 + + + true + + + + + + + + + + Mesh #2 + + + true + + + + + + + + + + + + + :/CC/plugin/qVoxFall/images/iconSwap.png:/CC/plugin/qVoxFall/images/iconSwap.png + + + + + + + + + + + Model properties + + + + + + + 0 + 0 + + + + Voxel size + + + + + + + 4 + + + 0.000100000000000 + + + 1000000000.000000000000000 + + + 0.10 + + + + + + + + 0 + 0 + + + + (Limit of Detection 95%) + + + + + + + + 0 + 0 + + + + Azimuth (deg) + + + + + + + 0 + + + 0.0 + + + 360.0 + + + 0.0 + + + + + + + + 0 + 0 + + + + (Slope's dip direction) + + + + + + + + + + Output options + + + + + + Export meshes + + + true + + + + + + + Loss/gain + + + false + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + showMesh1CheckBox + mesh1LineEdit + showMesh2CheckBox + mesh2LineEdit + swapMeshesToolButton + voxelSizeDoubleSpinBox + azDoubleSpinBox + exportCheckBox + lossCheckBox + buttonBox + + + + + + + buttonBox + accepted() + VoxFallDialog + accept() + + + 316 + 358 + + + 157 + 274 + + + + + buttonBox + rejected() + VoxFallDialog + reject() + + + 384 + 358 + + + 286 + 274 + + + + +