Location>code7788 >text

cloud compare PCA plugin development detailed steps (two) with code

Popularity:873 ℃/2024-08-17 18:17:53

in the previous section/csy1021/article/details/141200135

We have completed the preparatory work for the specific development, including the setup of each level, the modification of the relevant content, and have successfully compiled the program.

image-20240814203105623

If you need the entire plug-in project, compiled dll, or other help, welcome to leave a message, private message or add group [group number: 392784757].

This section introduces advanced content for our qPCA plugin, including the introduction of third-party libraries, setting up the user-input interface, and writing specific logic.

The final overall qPCA plugin directory structure is as follows

image-20240817160238473

Third-party library introduction

Since cc deals mainly with point cloud and mesh data, it is less involved in solving large matrices, while PCA analysis needs to compute matrices, here we have to introduce a third-party library, Eigen, to help accomplish this.

Eigen is a lightweight C++ math library, mainly for matrix and linear algebra operations. One of its advantages is that it is a header library and does not need to be compiled into a library file, which makes it a little less difficult to introduce

Here we are using Eigen version 3.4 official website/?title=Main_Page

Download and unzip it in the extern directory

Specific path Format extern\eigen3\Eigen\**

Then modify the current qPCA plug-in's highest

Add Content

include_directories("${CMAKE_CURRENT_LIST_DIR}/extern/eigen3") # set the include directories

I've set it up here, and I've found that it doesn't work.

Also in the include folder, add the

include_directories("${CMAKE_CURRENT_LIST_DIR}/../extern/eigen3")

[The maximum of the final version of the qPCA plug-in is as follows].

# CloudCompare example for standard plugins

option( PLUGIN_qPCA "Install PCA plugin" OFF )

if ( PLUGIN_qPCA )
	project( QPCA_PLUGIN )

	AddPlugin( NAME ${PROJECT_NAME} )

	add_subdirectory( include )
	add_subdirectory( src )
	add_subdirectory( ui )
	include_directories("${CMAKE_CURRENT_LIST_DIR}/extern/eigen3")
	# set dependencies to necessary libraries
	# target_link_libraries( ${PROJECT_NAME} LIB1 )
endif()

[The final version of the include folder will look like this.

include_directories("${CMAKE_CURRENT_LIST_DIR}/../extern/eigen3")

target_sources( ${PROJECT_NAME}
	PRIVATE
		#${CMAKE_CURRENT_LIST_DIR}/
		${CMAKE_CURRENT_LIST_DIR}/
		${CMAKE_CURRENT_LIST_DIR}/
)

target_include_directories( ${PROJECT_NAME}
	PRIVATE
		${CMAKE_CURRENT_SOURCE_DIR}
)

The plugin is based on the ExamplePlugin, where the , and has not been removed. The content is commented out and will not be introduced into our plugin.

Then cmake and rebuild the project to test if the introduction was successful.

Add

#include <Eigen/Core>

Add the relevant header file to see if you can find it in the

#include <Eigen/Core>
#include <Eigen/Eigenvalues>
#include <Eigen/Dense>

In vs ctrl then click on the introduced header file, the introduction is successful you can see the specific header file, otherwise it is not introduced successfully

ui User Parameter Setting Receive Box

For the qPCA plugin I set the user parameter of which axis the main direction is aligned to, and the interface is shown as follows

image-20240817162314040

Can be aligned to different axes, in the beginning of the development, you can not introduce the user parameters to receive the box, the program is basically complete, and then introduce the ui, to improve the user experience

According to the real development, this subsection should be placed at the end, but it is fine to place it in the middle of the line

ui design

[Omitted]

Set up different layouts and corresponding input components according to your own plug-ins, which can be modified based on what CC already has.

The end should be followed by a ui file, which in this article is ui/.

Under the ui folder, the final version looks like this

target_sources( ${PROJECT_NAME}
	PRIVATE
		${CMAKE_CURRENT_LIST_DIR}/
)

In fact, you can also write the interface by code, do not need .ui, that is, .ui compiled ui_xxx.h header file, here is the knowledge of qt!

Introduced at the ui code level

New include/, src/

modifications

The top of the qPCA plugin has already been laid out above.

In the include file, as shown above.

src folder, the final version looks like this

target_sources( ${PROJECT_NAME}
	PRIVATE
		# ${CMAKE_CURRENT_LIST_DIR}/
		${CMAKE_CURRENT_LIST_DIR}/
		${CMAKE_CURRENT_LIST_DIR}/
)

#ifndef CC_PCA_DLG_HEADER
#define CC_PCA_DLG_HEADER

#include "ui_pcaDlg.h"

class ccPCADlg : public QDialog, public Ui::PCADialog
{
	Q_OBJECT

public:

	//! Default constructor
	explicit ccPCADlg(QWidget* parent = nullptr);


protected:

	//! Saves (temporarily) the dialog parameters on acceptation
	void saveSettings();

};

#endif

Here ccPCADlg will inherit from Ui::PCADialog in ui_pcaDlg.h after the ui file has been compiled, and the declared ccPCADlg can get all the components in the interface, and thus can get the values set by the user.

#include ""
#include <QButtonGroup>


static bool axis_x_checeked = true;
static bool axis_y_checeked = false;
static bool axis_z_checeked = false;

ccPCADlg::ccPCADlg(QWidget* parent)
	: QDialog(parent)
	, Ui::PCADialog()
{
	setupUi(this);

	connect(buttonBox, &QDialogButtonBox::accepted, this, &ccPCADlg::saveSettings);
	// Create a QButtonGroup Logically guarantees that only one is selected
	QButtonGroup* buttonGroup = new QButtonGroup(this);
	buttonGroup->addButton(radioButton);
	buttonGroup->addButton(radioButton_2);
	buttonGroup->addButton(radioButton_3);
	radioButton->setChecked(true); // default x
}


void ccPCADlg::saveSettings()
{
	axis_x_checeked = radioButton->isChecked();
	axis_y_checeked = radioButton_2->isChecked();
	axis_z_checeked = radioButton_3->isChecked();
}

Main program logic

#pragma once

#include ""
#include <Eigen/Core>

class qPCA : public QObject, public ccStdPluginInterface
{
	Q_OBJECT
	Q_INTERFACES(ccPluginInterface ccStdPluginInterface)
	Q_PLUGIN_METADATA(IID "" FILE "../")

public:
	explicit qPCA(QObject *parent = nullptr);
	~qPCA() override = default;

	// Inherited from ccStdPluginInterface
	void onNewSelection(const ccHObject::Container &selectedEntities) override;
	QList<QAction *> getActions() override;
	ccHObject* executePCA(ccPointCloud* ccPC,
		Eigen::Vector3f& eigenValuesPCA,
		Eigen::Matrix3f& eigenVectorsPCA,
		Eigen::Vector3f& pcaCentroid,
		bool silent);
protected:
	void doAction();
private:
	QAction *m_action;
};

included among these

ccHObject* executePCA(ccPointCloud* ccPC,
		Eigen::Vector3f& eigenValuesPCA,
		Eigen::Matrix3f& eigenVectorsPCA,
		Eigen::Vector3f& pcaCentroid,
		bool silent);

is our core function

Realize the corresponding functions one by one

Declare the cc-supplied program interface for the plug-in, exposing it for use by later functions.

static ccMainAppInterface* s_app = nullptr;

constructor

qPCA::qPCA( QObject *parent )
: QObject( parent )
, ccStdPluginInterface( ":/CC/plugin/qPCA/" )
, m_action( nullptr )
{
s_app = m_app; // m_app inherits from ccStdPluginInterface interface also ccMainAppInterface*
}

onNewSelection()

void qPCA::onNewSelection( const ccHObject::Container &selectedEntities )
{
	if (m_action)
		m_action->setEnabled(() == 1 && selectedEntities[0]->isA(CC_TYPES::POINT_CLOUD));

}

Ensure that an entity is selected and that it is a point cloud

getActions()

QList<QAction *> qPCA::getActions()
{
	// default action (if it has not been already created, this is the moment to do it)
	if ( !m_action )
	{
		// Here we use the default plugin name, description, and icon,
		// but each action should have its own.
		m_action = new QAction( getName(), this );
		m_action->setToolTip( getDescription() );
		m_action->setIcon( getIcon() );

		// Connect appropriate signal
		connect( m_action, &QAction::triggered, this, &qPCA::doAction);
	}

	return { m_action };
}

We only have one action, and it stays the same;

If you want to add more sub-functions, such as actions, you need to link the corresponding signal and slot functions here.

doAction()

Checks and preparations before executing the core logic, including entity checks and judgments, getting user-set values, entity type conversion to point cloud, preparation of core functions

static bool axis_x_checked = true;
static bool axis_y_checked = false;
static bool axis_z_checked = false;
void qPCA::doAction()
{
	assert(m_app);
	if (!m_app)
		return;
	m_app->dispToConsole("[qPCA] welcome use PCA plugin by xxx!", ccMainAppInterface::STD_CONSOLE_MESSAGE);
	QMessageBox::information(nullptr, "info", "welcome use PCA plugin");

	const ccHObject::Container& selectedEntities = m_app->getSelectedEntities();
	size_t selNum = ();
	if (selNum != 1)
	{
		ccLog::Error("[qPCA] Select only one cloud!");
		return;
	}

	ccHObject* ent = selectedEntities[0];
	assert(ent);
	if (!ent || !ent->isA(CC_TYPES::POINT_CLOUD))
	{
		ccLog::Error("[qPCA] Select a real point cloud!");
		return;
	}

	ccPointCloud* pc = static_cast<ccPointCloud*>(ent);

	// input cloud
	CCVector3 bbMin, bbMax;
	pc->getBoundingBox(bbMin, bbMax);
	/*CCVector3 diff = bbMax - bbMin;
	float scale = std::max(std::max(diff[0], diff[1]), diff[2]);*/

	ccPCADlg pcaDlg(m_app->getMainWindow());
	if (!())
	{
		return;
	}
	axis_x_checked = ->isChecked();
	axis_y_checked = pcaDlg.radioButton_2->isChecked();
	axis_z_checked = pcaDlg.radioButton_3->isChecked();


	Eigen::Vector3f eigenValuesPCA;
	Eigen::Matrix3f eigenVectorsPCA;
	Eigen::Vector3f pcaCentroid;
	ccHObject* group = executePCA(pc,eigenValuesPCA,eigenVectorsPCA, pcaCentroid,false);

	if (group)
	{
		m_app->addToDB(group);
		m_app->refreshAll();
	}
}

executePCA()

The main logic is

Calculate point cloud center ---> Calculate point cloud covariance matrix ---> Eigen solve singular values and singular vectors ---> Construct rotation matrix ---> Rotate the transform to a standard coordinate system, with the principal axes aligned to the axes chosen by the user, and the default x-axis.

ccHObject* qPCA::executePCA(ccPointCloud* ccPC,
	Eigen::Vector3f& eigenValuesPCA,
	Eigen::Matrix3f& eigenVectorsPCA,
	Eigen::Vector3f& pcaCentroid,
	bool silent)
{
	ccHObject* group = nullptr;
	const CCVector3d& globalShift = ccPC->getGlobalShift();
	double globalScale = ccPC->getGlobalScale();

	auto toEigen = [](const CCVector3* vec) {
		return Eigen::Vector3f(vec->x, vec->y, vec->z);
	};
	();
	for (unsigned i = 0; i < ccPC->size(); ++i)
	{
		const CCVector3* point = ccPC->getPoint(i);
		Eigen::Vector3f eigenPoint(point->x, point->y, point->z);
		pcaCentroid += eigenPoint;
	}
	pcaCentroid /= static_cast<float>(ccPC->size());

	Eigen::Matrix3f covarianceMatrix = Eigen::Matrix3f::Zero();
	for (unsigned i = 0; i < ccPC->size(); ++i)
	{
		Eigen::Vector3f diff = (toEigen(ccPC->getPoint(i))) - pcaCentroid;
		covarianceMatrix += diff * ();
	}
	covarianceMatrix /= static_cast<float>(ccPC->size());

	// carry out PCA:Solving for eigenvalues and eigenvectors
	Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> solver(covarianceMatrix);
	eigenValuesPCA = ();   // Returns the eigenvalue
	eigenVectorsPCA = (); // Returns the feature vector

	// log
	Eigen::IOFormat CleanFmt(4, 0, ", ", "\n", "[", "]");
	std::stringstream vectorStream, matrixStream;
	vectorStream << (CleanFmt);
	m_app->dispToConsole("[qPCA] pca center", ccMainAppInterface::STD_CONSOLE_MESSAGE);
	m_app->dispToConsole(QString::fromStdString(()), ccMainAppInterface::STD_CONSOLE_MESSAGE);

	("");
	m_app->dispToConsole("[qPCA] eigen values", ccMainAppInterface::STD_CONSOLE_MESSAGE);
	vectorStream << (CleanFmt);
	matrixStream << (CleanFmt);

	m_app->dispToConsole(QString::fromStdString(()), ccMainAppInterface::STD_CONSOLE_MESSAGE);
	m_app->dispToConsole("[qPCA] eigen vectors sorted by eigen value in descending order", ccMainAppInterface::STD_CONSOLE_MESSAGE);
	m_app->dispToConsole(QString::fromStdString(()), ccMainAppInterface::STD_CONSOLE_MESSAGE);
	//m_app->forceConsoleDisplay();

	// Converts the main direction of the point cloud to x y z axis
	char axis = axis_y_checked ? 'y' : (axis_z_checked ? 'z' : 'x');
	m_app->dispToConsole(QString::fromStdString("[qPCA] frist component 2 axis "+std::tolower(axis)), ccMainAppInterface::STD_CONSOLE_MESSAGE);
	//char axis = 'x'; //Getting through the dialog box default (setting)
	Eigen::Matrix4f rotationMatrix = Eigen::Matrix4f::Identity();
	Eigen::Matrix3f tmp;
	switch (axis)
	{
	case 'x':
		<3, 3>(0, 0) = (); // x y z
		break;
	case 'y':
		tmp = eigenVectorsPCA;
		(0).swap((1));
		<3, 3>(0, 0) = (); // y x z
		break;
	case 'z':
		tmp = eigenVectorsPCA;
		(0).swap((2));
		<3, 3>(0, 0) = (); // z x y
		break;
	default:
		break;
	}
	("");
	matrixStream << (CleanFmt);
	m_app->dispToConsole(QString::fromStdString(()), ccMainAppInterface::STD_CONSOLE_MESSAGE);

	<3, 1>(0, 3) = -1.0f * ((axis_x_checked ? () : ()) * pcaCentroid);

	("");
	matrixStream << (CleanFmt);
	m_app->dispToConsole(QString::fromStdString(()), ccMainAppInterface::STD_CONSOLE_MESSAGE);

	ccPointCloud* firstComponent = new ccPointCloud(
		QString("first  component - projecting to (%1) plane ").arg((axis_y_checked ? "xz" : (axis_z_checked ? "xy" : "yz")))
	);
	ccPointCloud* secondComponent = new ccPointCloud(
		QString("second component - projecting to (%1) plane ").arg((axis_y_checked ? "yz" : (axis_z_checked ? "zy" : "xz")))
	);
	ccPointCloud* thirdComponent = new ccPointCloud(
		QString("third  component - projecting to (%1) plane ").arg((axis_y_checked ? "yx" : (axis_z_checked ? "zx" : "xy")))
	); // principal component

	ccPointCloud* stdAxisCloud = new ccPointCloud("2stdAxisCloud");

	if (!firstComponent->reserve(static_cast<unsigned>(ccPC->size())))
	{
		ccLog::Error("[qPCA] Not enough memory!");
		delete firstComponent;
		return nullptr;
	}
	if (!secondComponent->reserve(static_cast<unsigned>(ccPC->size())))
	{
		ccLog::Error("[qPCA] Not enough memory!");
		delete secondComponent;
		return nullptr;
	}
	if (!thirdComponent->reserve(static_cast<unsigned>(ccPC->size())))
	{
		ccLog::Error("[qPCA] Not enough memory!");
		delete thirdComponent;
		return nullptr;
	}
	if (!stdAxisCloud->reserve(static_cast<unsigned>(ccPC->size())))
	{
		ccLog::Error("[qPCA] Not enough memory!");
		delete stdAxisCloud;
		return nullptr;
	}

	// Iterate over each point and apply the rotation matrix
	std::stringstream pointStream;
	for (unsigned i = 0; i < ccPC->size(); ++i)
	{
		("");
		CCVector3* point = const_cast<CCVector3*>(ccPC->getPoint(i));

		// commander-in-chief (military) CCVector3 convert to Eigen::Vector3f
		Eigen::Vector4f eigenPoint(point->x, point->y, point->z, 1.0f);

		// rotation point
		Eigen::Vector4f rotatedPoint = rotationMatrix * eigenPoint;

		// commander-in-chief (military)结果写回 CCVector3,// I'd rather not write back.
		/*point->x = ();
		point->y = ();
		point->z = ();*/
		//pointStream << point->x << "," << point->y << "," << point->z;
		//m_app->dispToConsole(QString::fromStdString(()), ccMainAppInterface::STD_CONSOLE_MESSAGE);

		stdAxisCloud->addPoint({ rotatedPoint[0],rotatedPoint[1],rotatedPoint[2] });

		if (axis_y_checked) // align to y // y x z
		{
			firstComponent->addPoint({ rotatedPoint[0],0.0f,rotatedPoint[2] });
			secondComponent->addPoint({ 0.0f,rotatedPoint[1],rotatedPoint[2] });
			thirdComponent->addPoint({ rotatedPoint[0],rotatedPoint[1],0.0f });
		}
		else if (axis_x_checked) // align to x // x y z
		{
			firstComponent->addPoint({ 0.0f,rotatedPoint[1],rotatedPoint[2] });
			secondComponent->addPoint({ rotatedPoint[0],0.0f,rotatedPoint[2] });
			thirdComponent->addPoint({ rotatedPoint[0],rotatedPoint[1],0.0f });
		}
		else if(axis_z_checked) // align to  z // z x y
		{
			firstComponent->addPoint({ rotatedPoint[0],rotatedPoint[1],0.0f });
			secondComponent->addPoint({ 0.0f,rotatedPoint[1],rotatedPoint[2] });
			thirdComponent->addPoint({ rotatedPoint[0],0.0f,rotatedPoint[2] });
		}
		else
		{
			ccLog::Error("[qPCA] axis error");
			return nullptr;
		}


	}
	// Update Point Cloud
	//ccPC->invalidateBoundingBox();
	//ccPC->setVisible(false);

	// set up principal component color visualization
	for (auto pcShape : { stdAxisCloud ,firstComponent,secondComponent,thirdComponent })
	{
		ccColor::Rgb col = ccColor::Generator::Random();
		pcShape->setColor(col);
		pcShape->showSF(false);
		pcShape->showColors(true);
		pcShape->showNormals(true);
		pcShape->setVisible(true);
	}


	// computational projection all directions principal component has been transformed to the standard coordinate system, direct coordinate assignment0
	//ccPointCloud firstComponent, secondComponent, thirdComponent; // Merge into the above loop to complete
	if (!group)
	{
		group = new ccHObject(QString("PCA processed - align to %1 axis (%2)").arg((axis_y_checked?"y":(axis_z_checked?"z":"x")), ccPC->getName()));
	}
	if (group)
	{
		group->addChild(stdAxisCloud);
		group->addChild(firstComponent);
		group->addChild(secondComponent);
		group->addChild(thirdComponent);
	}


	return group;
}

That's it. Everything's done.

cmake rebuild, then compile

Effective demonstration

(express) thanksFree GIF Compression - Specializes in free "GIF Compression" online tool (). Free gif compression service

cc-PCA

In the next article, let's learn the code of cc together.

If you need the entire plug-in project, compiled dll, or other help, welcome to leave a message, private message or add group [group number: 392784757].