mercoledì 8 ottobre 2025

Versione definitiva misura distanze con Aruco tag

Versione definitiva del programma per Aruco Tag

Misura le distanze relative tra i vari tag (il tag di riferimento e' definito dalla variabile ref_id) e la rotazione usando i quaternioni

Le immagini hanno nome del tipo 20250801.jpg e sono contenute nel folder denominato giorno 

 


 

import cv2
import numpy as np
from scipy.spatial.transform import Rotation as R
import os
import csv
import sys

output_rows = []

# --- Load image ---
folder = "giorno"
arrays = []
files = [f for f in os.listdir(folder) if f.lower().endswith(".jpg")]
files.sort()
for file in files:
img_path = os.path.join(folder, file)
img = cv2.imread(img_path)
print(file[0:8])
aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250)
parameters = cv2.aruco.DetectorParameters()
parameters.cornerRefinementMethod = 0

detector = cv2.aruco.ArucoDetector(aruco_dict, parameters)

# --- Detect markers ---
corners, ids, rejected = detector.detectMarkers(img)

if ids is None or len(ids) == 0:
print("No markers detected.")
else:
# --- Load camera calibration ---
camera_matrix = np.load("./camera_matrix.npy")
dist_coeffs = np.load("./dist_coeffs.npy")

# --- Marker size in meters ---
marker_length = 0.25

rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
corners, marker_length, camera_matrix, dist_coeffs
)

def rtvec_to_matrix(rvec, tvec):
R_mat, _ = cv2.Rodrigues(rvec)
T = np.eye(4)
T[:3, :3] = R_mat
T[:3, 3] = tvec.flatten()
return T

poses = {}
for i, marker_id in enumerate(ids.flatten()):
poses[marker_id] = rtvec_to_matrix(rvecs[i], tvecs[i])

# --- Compute relative poses w.r.t marker 11 ---
ref_id = 15
if ref_id in poses:
T_ref = poses[ref_id]
T_ref_inv = np.linalg.inv(T_ref)

for marker_id, T in poses.items():
if marker_id == ref_id:
continue # skip the reference itself

# Relative transform
T_rel = T_ref_inv @ T
R_rel = T_rel[:3, :3]
t_rel = T_rel[:3, 3]

distance = np.linalg.norm(t_rel)

# Convert rotation matrix to quaternion
quat = R.from_matrix(R_rel).as_quat() # [x, y, z, w]

#print(f"Marker {marker_id} relative to Marker {ref_id}:")
#print(" Translation (m):", t_rel)
#print(" Distance (m):", distance)
#print(" Quaternion [x, y, z, w]:", quat, "\n")
#print(file[0:8],marker_id,distance,quat[3])
output_rows.append([file[0:8],marker_id, distance, quat[3]])


with open("marker.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Date","MarkerID", "Distance_m", "Quat_w"])
writer.writerows(output_rows)

 

mercoledì 1 ottobre 2025

Cristallo di quarzo in frattura di marmo a Carrara

 


Da Cordova a CapacitorJS

 Recentemente ho preso in mano un vecchio progetto Android per il quale avevo usato Cordova perche' alcuni utenti si lamentavano di non poter scaricare la app dal Play Store


 

Da una breve analisi ho visto che anche la versione piu' recente di Cordova e' compatibile con API 35 mentre attualmente siamo alla v36 ed anche Android Studio 3 non permette di utilizzare piu' la v35

Ho fatto un pianto ed un lamento ed ho cercato una soluzione alternativa trovandola in CapacitorJS

 in estrema sintesi ci si posiziona nella root del progetto Cordova e poi si lancia

npm install @capacitor/core @capacitor/cli --save-dev

npx cap init

npm install @capacitor/android

cordova plugin remove cordova-plugin-whitelist

npm uninstall cordova-plugin-whitelist

npx cap copy

npx cap sync

a questo punto si puo' proseguire dentro Android Studio che risolve da solo le dipendenze mancanti 

 

 

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


 

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 ...