Visualizzazione post con etichetta Cloudcompare. Mostra tutti i post
Visualizzazione post con etichetta Cloudcompare. Mostra tutti i post

lunedì 15 settembre 2025

Compilare plugin personalizzati in CloudCompare in Debian Trixie

 

Per compilare un plugin per Cloudcompare si deve partire dai sorgenti 

git clone --recursive https://github.com/CloudCompare/CloudCompare

Cloudcompare si puo' compilare solo su Qt5 mentre nelle versioni recenti di Debian il default e' Qt6  (dove non specificato le librerie si possono scaricare dal apt come per esempio geotiff senza dover ricompilare da sorgenti)

sudo apt update
sudo apt install -y \
    git cmake build-essential \
    qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools \
    qttools5-dev qttools5-dev-tools qtscript5-dev \
    libqt5svg5-dev libqt5opengl5-dev \
    libeigen3-dev libboost-all-dev \
    libfftw3-dev libtbb-dev \
    libgdal-dev libproj-dev \
    libpcl-dev \
    libxi-dev libxmu-dev libglu1-mesa-dev freeglut3-dev mesa-common-dev \
    libsqlite3-dev

Uno dei problemi in compilazione e' che Liblas e pdal non sono scaricabili da apt e si devono compilare da sorgenti. LibLas e Pdal dipendono da Boost e sono compatibili con la versione 1.88 pero' Pdal dipende a sua volta da Gdal che su apt e' compilata su Boost 1.83 e cio' ovviamente rompe il sistema

Si deve procedere quindi a compilare Gdal da sorgenti usando Boost 1.88

git clone https://github.com/OSGeo/gdal.git
cd gdal
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=ON \
    -DCMAKE_INSTALL_PREFIX=/usr/local
make -j$(nproc)
sudo make install
sudo ldconfig

 

Compilazione di PDAL

git clone  https://github.com/PDAL/PDAL

cd ~/PDAL
mkdir -p build && cd build
rm -rf *

cmake .. \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_INSTALL_PREFIX=/usr/local \
    -DWITH_GDAL=ON \
    -DWITH_LASZIP=ON \
    -DWITH_LAZPERF=ON \
    -DBUILD_SHARED_LIBS=ON \
    -DINSTALL_CMAKE_CONFIG=ON

make -j$(nproc)
sudo make install
sudo ldconfig

 

Compilazione di LibLas (non piu' necessaria nelle nuove versioni)


git clone https://github.com/libLAS/libLAS.git
cd libLAS
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DWITH_GDAL=ON -DWITH_GEOTIFF=ON
make -j$(nproc)
sudo make install
sudo ldconfig

Infine per compilare CloudCompare vero e proprio (comprensivo di plugin)

 cmake .. -DCMAKE_BUILD_TYPE=Release -DOPTION_USE_QT5=ON -DPLUGIN_GL_QEDL=ON  -DPLUGIN_GL_QSSAO=ON  -DPLUGIN_IO_QADDITIONAL=ON  -DPLUGIN_IO_QCORE=ON  -DPLUGIN_IO_QPHOTOSCAN=ON  -DPLUGIN_STANDARD_QANIMATION=ON  -DPLUGIN_STANDARD_QBROOM=ON  -DPLUGIN_STANDARD_QCANUPO=ON  -DPLUGIN_STANDARD_QCOMPASS=ON  -DPLUGIN_STANDARD_QFACETS=ON  -DPLUGIN_STANDARD_QHOUGH_NORMALS=ON  -DPLUGIN_STANDARD_QHPR=ON  -DPLUGIN_STANDARD_QM3C2=ON  -DPLUGIN_STANDARD_QPCV=ON  -DPLUGIN_STANDARD_QPOISSON_RECON=ON  -DPLUGIN_STANDARD_QRANSAC_SD=OFF  -DPLUGIN_STANDARD_QSRA=ON  -DEIGEN_ROOT_DIR=/usr/include/eigen


per vedere i plugin attivi si deve fare make install (non usare il binario in build/qCC)
i plugin sono dei file .so che si andranno ad installare in /usr/local/lib/cloudcompare/plugins
 
a questo punto si deve passare a compilare il plugin personalizzato. La via che ho trovato piu' comoda e'copiare il progetto di esempio in CloudCompare/plugins/example/ExamplePlugin in un plugins/core/Standard cambiando il nome del folder

per aggiungere il plugin si deve modificare anche CMakeLists.txt in plugins/core/Standard
Diciamo che il plugin custom si chiamera' qQueryMesh andremo a modificare i vari files del progetto
 
info.json
{
"type" : "Standard",
"name" : "qQueryMesh",
"icon" : ":/CC/plugin/qQueryMesh/images/icon.png",
"description": "Click and get info about the mesh",
"authors" : [
{
"name" : "Luca Innocenti",
"email" : "lucainnoc@gmail.com"
}
],
"maintainers" : [
{
"name" : "Luca Innocenti",
"email" : "lucainnoc@gmail.com"
}
],
"references" : [
{
"text" : "The unsuccessful self-treatment of a case of “writer's block”",
"url" : "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1311997/"
}
]
}


 
 
CMakeLists.txt
# Add an option to CMake to control whether we build this plugin or not
option( PLUGIN_EXAMPLE_STANDARD "Install example plugin" ON )

if ( PLUGIN_EXAMPLE_STANDARD )
project( qQueryMesh )
AddPlugin( NAME ${PROJECT_NAME} )
add_subdirectory( include )
add_subdirectory( src )
# set dependencies to necessary libraries
# target_link_libraries( ${PROJECT_NAME} LIB1 )
endif()
 
 
qQueryMesh.qrc
 
<RCC>
  <qresource prefix="/CC/plugin/qQueryMesh" >
    <file>images/icon.png</file>
    <file>info.json</file>
  </qresource>
</RCC>
 
 
 qQueryMesh.h(deve essere modificato il file CMakeLists.txt in include)
//##########################################################################
//# #
//# CLOUDCOMPARE PLUGIN: ExamplePlugin #
//# #
//# 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

#include "ccStdPluginInterface.h"
#include <QObject>
#include <QAction>

class qQueryMesh : public QObject, public ccStdPluginInterface
{
Q_OBJECT
Q_INTERFACES( ccPluginInterface ccStdPluginInterface )
Q_PLUGIN_METADATA( IID "cccorp.cloudcompare.plugin.qQueryMesh" FILE "../info.json" )

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

//explicit qQueryPolygon(QObject* parent = nullptr);
QList<QAction*> getActions() override;
void onNewSelection(const ccHObject::Container& selectedEntities) override;


// Inherited from ccStdPluginInterface
//void onNewSelection( const ccHObject::Container &selectedEntities ) override;
//QList<QAction *> getActions() override;

private:
QAction* m_action;
};


 
 
 qQueryMesh.cpp (deve essere modificato il file CMakeLists.txt in src)
#include <QtGui>

#include "qQueryMesh.h"

#include <ccMesh.h>
#include <ccLog.h>
#include <ccPolyline.h>
#include <ccPointCloud.h>
#include <ccGLWindowInterface.h>

#include <fstream>
#include <cmath>

qQueryMesh::qQueryMesh(QObject *parent)
: QObject(parent)
, ccStdPluginInterface(":/CC/plugin/qQueryMesh/info.json")
, m_action(nullptr)
{
}

void qQueryMesh::onNewSelection(const ccHObject::Container &selectedEntities)
{
if (m_action == nullptr)
{
return;
}

for (ccHObject *entity : selectedEntities)
{
if (!entity)
continue;

// Stampiamo il nome dell'entità
ccLog::Print(QString("Oggetto selezionato: %1").arg(entity->getName()));

// Determiniamo il tipo dell'entità
if (entity->isKindOf(CC_TYPES::MESH))
{
ccLog::Print("Tipo: Mesh");
ccMesh *mesh = static_cast<ccMesh *>(entity);

ccPointCloud *vertices = dynamic_cast<ccPointCloud *>(mesh->getAssociatedCloud());
if (!vertices)
{
ccLog::Print("Errore: impossibile ottenere il PointCloud associato alla mesh");
continue;
}

// Colora tutti i vertici di grigio
ccColor::Rgb color(128, 128, 128);
for (unsigned i = 0; i < vertices->size(); ++i)
vertices->setPointColor(i, color);

// Mostra i colori sulla mesh
mesh->showColors(true);
mesh->prepareDisplayForRefresh();

if (mesh->hasNormals())
{
if (vertices->size() < 3)
continue;

CCVector3 avgNormal(0, 0, 0);
CCVector3 centroid(0, 0, 0);

double A = 0.0;
double B = 0.0;
double C = 0.0;
double D = 0.0;

unsigned triCount = mesh->size();
for (unsigned i = 0; i < triCount; ++i)
{
CCCoreLib::VerticesIndexes *tri = mesh->getTriangleVertIndexes(i);
if (!tri)
continue;

const CCVector3 *pA = vertices->getPoint(tri->i1);
const CCVector3 *pB = vertices->getPoint(tri->i2);
const CCVector3 *pC = vertices->getPoint(tri->i3);

// centroide per il calcolo di D
centroid += (*pA + *pB + *pC) / 3.0f;

// normale triangolo
CCVector3 AB = *pB - *pA;
CCVector3 AC = *pC - *pA;
CCVector3 N = AB.cross(AC);
N.normalize();

avgNormal += N;
}

if (triCount > 0)
{
avgNormal.normalize();
centroid /= static_cast<PointCoordinateType>(triCount);

// piano: Ax + By + Cz + D = 0
A = avgNormal.x;
B = avgNormal.y;
C = avgNormal.z;
D = -(A * centroid.x + B * centroid.y + C * centroid.z);

ccLog::Print(QString("Piano mesh: normale=(%1,%2,%3), D=%4")
.arg(A)
.arg(B)
.arg(C)
.arg(D));
}

// Calcolo della normale media
CCVector3 normalSum(0, 0, 0);
unsigned count = 0;

for (unsigned i = 0; i < mesh->size(); ++i) // numero di triangoli
{
const CCCoreLib::VerticesIndexes *tri = mesh->getTriangleVertIndexes(i);
if (!tri)
continue;

const CCVector3 *p0 = vertices->getPoint(tri->i1);
const CCVector3 *p1 = vertices->getPoint(tri->i2);
const CCVector3 *p2 = vertices->getPoint(tri->i3);

CCVector3 v1 = *p1 - *p0;
CCVector3 v2 = *p2 - *p0;
CCVector3 N = v1.cross(v2);

if (N.norm2() > 0)
{
N.normalize();
normalSum += N;
++count;
}
}

if (count > 0)
{
normalSum /= static_cast<PointCoordinateType>(count);
normalSum.normalize();
ccLog::Print(QString("Normale media: (%1, %2, %3)")
.arg(normalSum.x)
.arg(normalSum.y)
.arg(normalSum.z));
}

// Scrittura in CSV in append
// coseni direttori
double norm = sqrt(A * A + B * B + C * C);
double l = A / norm;
double m = B / norm;
double n = C / norm;

// angoli in radianti → gradi
double alpha = acos(l) * 180.0 / M_PI;
double beta = acos(m) * 180.0 / M_PI;
double gamma = acos(n) * 180.0 / M_PI;

const char *filename = "/home/luca/dati.csv";
bool writeHeader = false;

{
std::ifstream checkFile(filename);
if (!checkFile.good() || checkFile.peek() == std::ifstream::traits_type::eof())
writeHeader = true;
}

std::ofstream file(filename, std::ios::app);
if (file.is_open())
{
if (writeHeader)
{
file << "A,B,C,D,alpha,beta,gamma\n";
}
file << A << "," << B << "," << C << "," << D << ","
<< alpha << "," << beta << "," << gamma << "\n";
file.close();
}
}
else
{
ccLog::Print("Mesh senza normali");
}
}
else if (entity->isKindOf(CC_TYPES::POLY_LINE))
{
ccLog::Print("Tipo: Polilinea");
}
else if (entity->isKindOf(CC_TYPES::POINT_CLOUD))
{
ccLog::Print("Tipo: Point Cloud");
}
else
{
ccLog::Print("Tipo: Altro");
}
}

// abilita se è selezionato un tema poligonale
bool hasPoly = false;
for (ccHObject *obj : selectedEntities)
{
if (obj && obj->isKindOf(CC_TYPES::POLY_LINE))
{
hasPoly = true;
break;
}
}

m_action->setEnabled(hasPoly);
}

QList<QAction *> qQueryMesh::getActions()
{
if (!m_action)
{
m_action = new QAction(getName(), this);
m_action->setToolTip(getDescription());
m_action->setIcon(getIcon());

connect(m_action, &QAction::triggered, this, [this]()
{
if (!m_app)
return;

const ccHObject::Container& selected = m_app->getSelectedEntities();
if (selected.empty())
{
ccLog::Warning("[qQueryPolygon] No polygon selected!");
return;
}

for (ccHObject* obj : selected)
{
if (!obj || !obj->isKindOf(CC_TYPES::POLY_LINE))
continue;

ccPolyline* poly = static_cast<ccPolyline*>(obj);
if (!poly)
continue;

ccPointCloud* vertices = static_cast<ccPointCloud*>(poly->getAssociatedCloud());
if (!vertices)
continue;

// ---- INFO ----
QString info;
info += QString("Polygon: %1\n").arg(poly->getName());
info += QString("Vertices: %1\n").arg(vertices->size());
info += QString("Closed: %1\n").arg(poly->isClosed() ? "Yes" : "No");

PointCoordinateType length = poly->computeLength();
info += QString("\nPerimeter length: %1").arg(length);

ccLog::Print(info);

// ---- HIGHLIGHT ----
poly->setColor(ccColor::red); // change line color
poly->setWidth(4); // make it thicker
poly->showColors(true);

if (m_app->getActiveGLWindow())
m_app->getActiveGLWindow()->redraw();
} });
}

return {m_action};
}


 
 Si controlla che il plugin e' attivo in Help/About Plugins. Poi si aggiunge il seguente parametro a cmake ..
-DPLUGIN_STANDARD_QQUERYMESH=ON
 
infine make e make install. Se tutto e' corretto make, sudo make install avremo il file  libqQueryMesh.so


 

mercoledì 1 marzo 2023

Change detection con Open3D su nuvole di punti

Anche la libreria Open3D ha una funzione che permette di calcolare la distanza tra due nuvole di punti anche se non e' esplicitato l'algoritmo (inoltre per i punti in cui non c'e' sovrapposizione immediata il codice del precedente post riporta NaN mentre qui un valore di distanza viene sempre proposto. Inoltre la matrice delle distanze contiene solo valori positivi



I risultati mi sembrano migliori quelli dell'algoritmo del precedente post

import numpy as np
import laspy as lp
import open3d as o3d

p2019= lp.read("2019.las")

points_2019 = np.vstack((p2019.x, p2019.y, p2019.z)).transpose()
#colors_2019 = np.vstack((p2019.red, p2019.green,p2019.blue)).transpose()

pcd2019 = o3d.geometry.PointCloud()
pcd2019.points = o3d.utility.Vector3dVector(points_2019)
#pcd2019.colors = o3d.utility.Vector3dVector(colors_2019 / 65535)


p2022 = lp.read("2022.las")

points_2022 = np.vstack((p2022.x, p2022.y, p2022.z)).transpose()
#colors_2022 = np.vstack((p2022.red, p2022.green,p2022.blue)).transpose()

pcd2022 = o3d.geometry.PointCloud()
pcd2022.points = o3d.utility.Vector3dVector(points_2022)
distance=pcd2019.compute_point_cloud_distance(pcd2022)

rows, columns = points_2019.shape

reds = np.empty(rows)
blues = np.empty(rows)
greens = np.empty(rows)


for i in range(0,rows):
#print(t)
if (distance[i]< -1.5):
Red = 255
Green = 0
Blue = 0

if ((distance[i]>=-1.5) and (distance[i]<=-0.75)):
Red = 255
Green = 255
Blue = 0

if ((distance[i]>=-0.75) and (distance[i]<=0.75)):
Red = 0
Green = 255
Blue = 0
if ((distance[i]>=0.75) and (distance[i]<=1.5)):
Red = 0
Green = 255
Blue = 255

if (distance[i]> 1.5):
Red = 0
Green = 0
Blue = 255
if (distance[i]> 10):
Red = 0
Green = 0
Blue = 0

if (distance[i]< 10):
Red = 0
Green = 0
Blue = 0

reds[i]= Red
greens[i]= Green
blues[i]=Blue

header = lp.LasHeader(point_format=3, version="1.2")
las = lp.LasData(header)

las.x = points_2019[:,0]
las.y = points_2019[:,1]
las.z = points_2019[:,2]
las.red = reds[:]
las.blue = blues[:]
las.green = greens[:]
las.write("open3d_result.las")






lunedì 27 febbraio 2023

Change detection con M3C2 su nuvole di punti

L'algoritmo M3C2 viene utlizzato da Cloudcompare per effettuare il change detection ...volevo trovare una soluzione per rendere automatico il processing e la pubblicazione

Si parte da due LAS e tramite la libreria py4dgeo si ottiene un file las con i punti colorati secondo una scala colore basata sulla distanza tra i due LAS si origine



import numpy as np
import py4dgeo
import laspy
import sys
import math

py4dgeo.set_num_threads(4) # numero di threads attivi
epoch1, epoch2 = py4dgeo.read_from_las("2019.las", "2022.las")
corepoints = epoch1.cloud[::30]

m3c2 = py4dgeo.M3C2(epochs=(epoch1, epoch2),corepoints=corepoints,cyl_radii=(2.0,),normal_radii=(0.5, 1.0, 2.0),)
distances, uncertainties = m3c2.run()

dist_max = np.nanmax(distances)
dist_min = np.nanmin(distances)

reds = np.empty(corepoints.shape)
blues = np.empty(corepoints.shape)
greens = np.empty(corepoints.shape)

fattore =(dist_max-dist_min)/(dist_max+dist_min)

Red =0
Blue = 0
Green = 0
for i in range (distances.shape[0]):
if math.isnan(distances[i]):
t = 0
else:
#print(distances[i])
t = int(((distances[i]-dist_min)/(dist_max-dist_min)) * 16581375)
#print(t)

if math.isnan(t):
Blue = 0
Green = 0
Red = 0
else:
if (distances[i]< -1.5):
Red = 255
Green = 0
Blue = 0

if ((distances[i]>=-1.5) and (distances[i]<=-0.75)):
Red = 255
Green = 255
Blue = 0

if ((distances[i]>=-0.75) and (distances[i]<=0.75)):
Red = 0
Green = 255
Blue = 0
if ((distances[i]>=0.75) and (distances[i]<=1.5)):
Red = 0
Green = 255
Blue = 255

if (distances[i]> 1.5):
Red = 0
Green = 0
Blue = 255
#RGBint = t
#Red = RGBint & 255
#Blue = (RGBint >> 8) & 255
#Green = (RGBint >> 16) & 255'''



reds[i]= Red
greens[i]= Green
blues[i]=Blue

header = laspy.LasHeader(point_format=3, version="1.2")
header.add_extra_dim(laspy.ExtraBytesParams(name="distance", type=np.float32))

xmin = np.floor(np.min(corepoints[:,0]))
ymin = np.floor(np.min(corepoints[:,1]))
zmin = np.floor(np.min(corepoints[:,2]))

header.offsets = [xmin,ymin,zmin]
#header.scales = np.array([0.1, 0.1, 0.1])
las = laspy.LasData(header)

las.x = corepoints[:,0]
las.y = corepoints[:,1]
las.z = corepoints[:,2]
las.red = reds[:,0]
las.blue = blues[:,0]
las.green = greens[:,0]
las.distance = np.copy(distances)
las.write("m3c2_result.las")


#./PotreeConverter m3c2_result.las -o /var/www/html/m3c2/ --generate-page index


Per la pubblicazione su web il file viene convertito e vestito tramite PoTree 

 
  • Anders, K., Winiwarter, L., Lindenbergh, R., Williams, J. G., Vos, S. E., & Höfle, B. (2020). 4D objects-by-change: Spatiotemporal segmentation of geomorphic surface change from LiDAR time series. ISPRS Journal of Photogrammetry and Remote Sensing, 159, pp. 352-363. doi: 10.1016/j.isprsjprs.2019.11.025.

  • Anders, K., Winiwarter, L., Mara, H., Lindenbergh, R., Vos, S. E., & Höfle, B. (2021). Fully automatic spatiotemporal segmentation of 3D LiDAR time series for the extraction of natural surface changes. ISPRS Journal of Photogrammetry and Remote Sensing, 173, pp. 297-308. doi: 10.1016/j.isprsjprs.2021.01.015.

  • Truong, C., Oudre, L., Vayatis, N. (2018): ruptures: Change point detection in python. arXiv preprint: abs/1801.00826.

Analisi MNF su spettri di riflettanza di plastica

Devo cerca di lavorare su spettri di riflettanza di plastica e la prima domanda e': quale sono le bande significative? Sono partito dal ...