diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 9dffba23e1ee..0b18a06fb207 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -339,6 +339,12 @@ set(QGIS_ANALYSIS_SRCS processing/pdal/qgsalgorithmpdalthinbydecimate.cpp processing/pdal/qgsalgorithmpdalthinbyradius.cpp processing/pdal/qgsalgorithmpdaltile.cpp + processing/pdal/qgsalgorithmpdalheightabovegroundnearestneighbour.cpp + processing/pdal/qgsalgorithmpdalheightabovegroundtriangulation.cpp + processing/pdal/qgsalgorithmpdalfilternoisestatistical.cpp + processing/pdal/qgsalgorithmpdalfilternoiseradius.cpp + processing/pdal/qgsalgorithmpdalclassifyground.cpp + processing/pdal/qgsalgorithmpdaltransform.cpp raster/qgsalignraster.cpp raster/qgsninecellfilter.cpp diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalclassifyground.cpp b/src/analysis/processing/pdal/qgsalgorithmpdalclassifyground.cpp new file mode 100644 index 000000000000..0ffc9f5eeeb0 --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalclassifyground.cpp @@ -0,0 +1,120 @@ +/*************************************************************************** + qgsalgorithmpdalclassifyground.cpp + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmpdalclassifyground.h" + +#include "qgspointcloudlayer.h" +#include "qgsrunprocess.h" + +///@cond PRIVATE + +QString QgsPdalClassifyGroundAlgorithm::name() const +{ + return QStringLiteral( "classifyground" ); +} + +QString QgsPdalClassifyGroundAlgorithm::displayName() const +{ + return QObject::tr( "Classify ground points" ); +} + +QString QgsPdalClassifyGroundAlgorithm::group() const +{ + return QObject::tr( "Point cloud data management" ); +} + +QString QgsPdalClassifyGroundAlgorithm::groupId() const +{ + return QStringLiteral( "pointclouddatamanagement" ); +} + +QStringList QgsPdalClassifyGroundAlgorithm::tags() const +{ + return QObject::tr( "pdal,lidar,classify,ground,elevation" ).split( ',' ); +} + +QString QgsPdalClassifyGroundAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm classifies ground points using the Simple Morphological Filter (SMRF) algorithm." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "Cell Size is the cell size for processing grid in map units, where smaller values give finer detail but may increase noise. Scalar is the threshold for steeper slopes; a higher value is needed if the terrain is rough, otherwise real ground might be misclassified. Slope is the slope threshold measured as rise over run, indicating how much slope is tolerated as ground and should be higher for steep terrain. Threshold is the elevation threshold for separating ground from objects; higher values allow larger deviations from the ground. Window Size is the maximum filter window size, where higher values better identify large buildings or objects while smaller values protect smaller features." ); +} + +QString QgsPdalClassifyGroundAlgorithm::shortDescription() const +{ + return QObject::tr( "Classifies ground points using the Simple Morphological Filter (SMRF) algorithm." ); +} + +QgsPdalClassifyGroundAlgorithm *QgsPdalClassifyGroundAlgorithm::createInstance() const +{ + return new QgsPdalClassifyGroundAlgorithm(); +} + +void QgsPdalClassifyGroundAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterPointCloudLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + + QgsProcessingParameterNumber *cellSizeParam = new QgsProcessingParameterNumber( QStringLiteral( "CELL_SIZE" ), QObject::tr( "Grid Cell Size" ), Qgis::ProcessingNumberParameterType::Double, 1.0 ); + cellSizeParam->setHelp( QObject::tr( "Cell size for processing grid (in map units). Smaller values give finer detail but may increase noise." ) ); + addParameter( cellSizeParam ); + + QgsProcessingParameterNumber *scalarParam = new QgsProcessingParameterNumber( QStringLiteral( "SCALAR" ), QObject::tr( "Scalar" ), Qgis::ProcessingNumberParameterType::Double, 1.25 ); + scalarParam->setHelp( QObject::tr( "Threshold for steeper slopes. Higher value if the terrain is rough, otherwise real ground might be misclassified." ) ); + addParameter( scalarParam ); + + QgsProcessingParameterNumber *slopeParam = new QgsProcessingParameterNumber( QStringLiteral( "SLOPE" ), QObject::tr( "Slope" ), Qgis::ProcessingNumberParameterType::Double, 0.15 ); + slopeParam->setHelp( QObject::tr( "Slope threshold (rise over run). How much slope is tolerated as ground. Should be higher for steep terrain." ) ); + addParameter( slopeParam ); + + QgsProcessingParameterNumber *thresholdParam = new QgsProcessingParameterNumber( QStringLiteral( "THRESHOLD" ), QObject::tr( "Threshold" ), Qgis::ProcessingNumberParameterType::Double, 0.5 ); + thresholdParam->setHelp( QObject::tr( "Elevation threshold for separating ground from objects. Higher values allow larger deviations from the ground." ) ); + addParameter( thresholdParam ); + + QgsProcessingParameterNumber *windowSizeParam = new QgsProcessingParameterNumber( QStringLiteral( "WINDOW_SIZE" ), QObject::tr( "Window Size" ), Qgis::ProcessingNumberParameterType::Double, 18.0 ); + windowSizeParam->setHelp( QObject::tr( "Maximum filter window size. Higher values better identify large buildings or objects, smaller values protect smaller features." ) ); + addParameter( windowSizeParam ); + + addParameter( new QgsProcessingParameterPointCloudDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Classified Ground" ) ) ); +} + +QStringList QgsPdalClassifyGroundAlgorithm::createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( feedback ); + + QgsPointCloudLayer *layer = parameterAsPointCloudLayer( parameters, QStringLiteral( "INPUT" ), context, QgsProcessing::LayerOptionsFlag::SkipIndexGeneration ); + if ( !layer ) + throw QgsProcessingException( invalidPointCloudError( parameters, QStringLiteral( "INPUT" ) ) ); + + const QString outputName = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT" ), context ); + QString outputFile = fixOutputFileName( layer->source(), outputName, context ); + checkOutputFormat( layer->source(), outputFile ); + setOutputValue( QStringLiteral( "OUTPUT" ), outputFile ); + + const double cellSize = parameterAsDouble( parameters, QStringLiteral( "CELL_SIZE" ), context ); + const double scalar = parameterAsDouble( parameters, QStringLiteral( "SCALAR" ), context ); + const double slope = parameterAsDouble( parameters, QStringLiteral( "SLOPE" ), context ); + const double threshold = parameterAsDouble( parameters, QStringLiteral( "THRESHOLD" ), context ); + const double windowSize = parameterAsDouble( parameters, QStringLiteral( "WINDOW_SIZE" ), context ); + + QStringList args = { QStringLiteral( "classify_ground" ), QStringLiteral( "--input=%1" ).arg( layer->source() ), QStringLiteral( "--output=%1" ).arg( outputFile ), QStringLiteral( "--cell-size=%1" ).arg( cellSize ), QStringLiteral( "--scalar=%1" ).arg( scalar ), QStringLiteral( "--slope=%1" ).arg( slope ), QStringLiteral( "--threshold=%1" ).arg( threshold ), QStringLiteral( "--window-size=%1" ).arg( windowSize ) }; + + applyCommonParameters( args, layer->crs(), parameters, context ); + applyThreadsParameter( args, context ); + return args; +} + +///@endcond diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalclassifyground.h b/src/analysis/processing/pdal/qgsalgorithmpdalclassifyground.h new file mode 100644 index 000000000000..b3c9e8df2b58 --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalclassifyground.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsalgorithmpdalclassifyground.h + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMPDALCLASSIFYGROUND_H +#define QGSALGORITHMPDALCLASSIFYGROUND_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgspdalalgorithmbase.h" + +///@cond PRIVATE + +/** + * Native point cloud filter noise using radius algorithm. + */ +class QgsPdalClassifyGroundAlgorithm : public QgsPdalAlgorithmBase +{ + public: + QgsPdalClassifyGroundAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QString group() const override; + QString groupId() const override; + QStringList tags() const override; + QString shortHelpString() const override; + QString shortDescription() const override; + QgsPdalClassifyGroundAlgorithm *createInstance() const override SIP_FACTORY; + + QStringList createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + friend class TestQgsProcessingPdalAlgs; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMPDALCLASSIFYGROUND_H diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalfilternoiseradius.cpp b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoiseradius.cpp new file mode 100644 index 000000000000..d5841e5cd24f --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoiseradius.cpp @@ -0,0 +1,107 @@ +/*************************************************************************** + qgsalgorithmpdalfilternoiseradius.cpp + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmpdalfilternoiseradius.h" + +#include "qgspointcloudlayer.h" +#include "qgsrunprocess.h" + +///@cond PRIVATE + +QString QgsPdalFilterNoiseRadiusAlgorithm::name() const +{ + return QStringLiteral( "filternoiseradius" ); +} + +QString QgsPdalFilterNoiseRadiusAlgorithm::displayName() const +{ + return QObject::tr( "Filter noise (using radius)" ); +} + +QString QgsPdalFilterNoiseRadiusAlgorithm::group() const +{ + return QObject::tr( "Point cloud data management" ); +} + +QString QgsPdalFilterNoiseRadiusAlgorithm::groupId() const +{ + return QStringLiteral( "pointclouddatamanagement" ); +} + +QStringList QgsPdalFilterNoiseRadiusAlgorithm::tags() const +{ + return QObject::tr( "pdal,lidar,filter,noise,radius" ).split( ',' ); +} + +QString QgsPdalFilterNoiseRadiusAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm filters noise in a point cloud using radius algorithm." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "Points are marked as noise if they have fewer than the minimum number of neighbors within the specified radius." ); +} + +QString QgsPdalFilterNoiseRadiusAlgorithm::shortDescription() const +{ + return QObject::tr( "Filters noise in a point cloud using radius algorithm." ); +} + +QgsPdalFilterNoiseRadiusAlgorithm *QgsPdalFilterNoiseRadiusAlgorithm::createInstance() const +{ + return new QgsPdalFilterNoiseRadiusAlgorithm(); +} + +void QgsPdalFilterNoiseRadiusAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterPointCloudLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "REMOVE_NOISE_POINTS" ), QObject::tr( "Remove noise points" ), false ) ); + + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "MIN_K" ), QObject::tr( "Minimum number of neighbors in radius" ), Qgis::ProcessingNumberParameterType::Integer, 2 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "RADIUS" ), QObject::tr( "Radius" ), Qgis::ProcessingNumberParameterType::Double, 1.0 ) ); + + addParameter( new QgsProcessingParameterPointCloudDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Filtered (radius algorithm)" ) ) ); +} + +QStringList QgsPdalFilterNoiseRadiusAlgorithm::createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( feedback ); + + QgsPointCloudLayer *layer = parameterAsPointCloudLayer( parameters, QStringLiteral( "INPUT" ), context, QgsProcessing::LayerOptionsFlag::SkipIndexGeneration ); + if ( !layer ) + throw QgsProcessingException( invalidPointCloudError( parameters, QStringLiteral( "INPUT" ) ) ); + + const QString outputName = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT" ), context ); + QString outputFile = fixOutputFileName( layer->source(), outputName, context ); + checkOutputFormat( layer->source(), outputFile ); + setOutputValue( QStringLiteral( "OUTPUT" ), outputFile ); + + const double minK = parameterAsDouble( parameters, QStringLiteral( "MIN_K" ), context ); + const double radius = parameterAsDouble( parameters, QStringLiteral( "RADIUS" ), context ); + + QString removeNoisePoints = "false"; + if ( parameterAsBoolean( parameters, QStringLiteral( "REMOVE_NOISE_POINTS" ), context ) ) + { + removeNoisePoints = "true"; + } + + QStringList args = { QStringLiteral( "filter_noise" ), QStringLiteral( "--input=%1" ).arg( layer->source() ), QStringLiteral( "--output=%1" ).arg( outputFile ), QStringLiteral( "--algorithm=radius" ), QStringLiteral( "--remove-noise-points=%1" ).arg( removeNoisePoints ), QStringLiteral( "--radius-min-k=%1" ).arg( minK ), QStringLiteral( "--radius-radius=%1" ).arg( radius ) }; + + applyCommonParameters( args, layer->crs(), parameters, context ); + applyThreadsParameter( args, context ); + return args; +} + +///@endcond diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalfilternoiseradius.h b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoiseradius.h new file mode 100644 index 000000000000..0141b0d288e1 --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoiseradius.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsalgorithmpdalfilternoiseradius.h + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMPDALFILTERNOISERADIUS_H +#define QGSALGORITHMPDALFILTERNOISERADIUS_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgspdalalgorithmbase.h" + +///@cond PRIVATE + +/** + * Native point cloud filter noise using radius algorithm. + */ +class QgsPdalFilterNoiseRadiusAlgorithm : public QgsPdalAlgorithmBase +{ + public: + QgsPdalFilterNoiseRadiusAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QString group() const override; + QString groupId() const override; + QStringList tags() const override; + QString shortHelpString() const override; + QString shortDescription() const override; + QgsPdalFilterNoiseRadiusAlgorithm *createInstance() const override SIP_FACTORY; + + QStringList createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + friend class TestQgsProcessingPdalAlgs; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMPDALFILTERNOISERADIUS_H diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalfilternoisestatistical.cpp b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoisestatistical.cpp new file mode 100644 index 000000000000..eb8ac92717ce --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoisestatistical.cpp @@ -0,0 +1,107 @@ +/*************************************************************************** + qgsalgorithmpdalfilternoisestatistical.cpp + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmpdalfilternoisestatistical.h" + +#include "qgspointcloudlayer.h" +#include "qgsrunprocess.h" + +///@cond PRIVATE + +QString QgsPdalFilterNoiseStatisticalAlgorithm::name() const +{ + return QStringLiteral( "filternoisestatistical" ); +} + +QString QgsPdalFilterNoiseStatisticalAlgorithm::displayName() const +{ + return QObject::tr( "Filter noise" ); +} + +QString QgsPdalFilterNoiseStatisticalAlgorithm::group() const +{ + return QObject::tr( "Point cloud data management" ); +} + +QString QgsPdalFilterNoiseStatisticalAlgorithm::groupId() const +{ + return QStringLiteral( "pointclouddatamanagement" ); +} + +QStringList QgsPdalFilterNoiseStatisticalAlgorithm::tags() const +{ + return QObject::tr( "pdal,lidar,filter,noise,statistical" ).split( ',' ); +} + +QString QgsPdalFilterNoiseStatisticalAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm filters noise in a point cloud using a statistical outlier removal algorithm." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "For each point, the algorithm computes the mean distance to its K nearest neighbors. Points whose mean distance exceeds a threshold (mean distance + multiplier × standard deviation) are classified as noise." ); +} + +QString QgsPdalFilterNoiseStatisticalAlgorithm::shortDescription() const +{ + return QObject::tr( "Filters noise in a point cloud using a statistical outlier removal algorithm." ); +} + +QgsPdalFilterNoiseStatisticalAlgorithm *QgsPdalFilterNoiseStatisticalAlgorithm::createInstance() const +{ + return new QgsPdalFilterNoiseStatisticalAlgorithm(); +} + +void QgsPdalFilterNoiseStatisticalAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterPointCloudLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "REMOVE_NOISE_POINTS" ), QObject::tr( "Remove noise points" ), false ) ); + + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "MEAN_K" ), QObject::tr( "Mean number of neighbors" ), Qgis::ProcessingNumberParameterType::Integer, 8 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "MULTIPLIER" ), QObject::tr( "Standard deviation multiplier" ), Qgis::ProcessingNumberParameterType::Double, 2.0 ) ); + + addParameter( new QgsProcessingParameterPointCloudDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Filtered (statistical algorithm)" ) ) ); +} + +QStringList QgsPdalFilterNoiseStatisticalAlgorithm::createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( feedback ); + + QgsPointCloudLayer *layer = parameterAsPointCloudLayer( parameters, QStringLiteral( "INPUT" ), context, QgsProcessing::LayerOptionsFlag::SkipIndexGeneration ); + if ( !layer ) + throw QgsProcessingException( invalidPointCloudError( parameters, QStringLiteral( "INPUT" ) ) ); + + const QString outputName = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT" ), context ); + QString outputFile = fixOutputFileName( layer->source(), outputName, context ); + checkOutputFormat( layer->source(), outputFile ); + setOutputValue( QStringLiteral( "OUTPUT" ), outputFile ); + + const int meanK = parameterAsInt( parameters, QStringLiteral( "MEAN_K" ), context ); + const double multiplier = parameterAsDouble( parameters, QStringLiteral( "MULTIPLIER" ), context ); + + QString removeNoisePoints = "false"; + if ( parameterAsBoolean( parameters, QStringLiteral( "REMOVE_NOISE_POINTS" ), context ) ) + { + removeNoisePoints = "true"; + } + + QStringList args = { QStringLiteral( "filter_noise" ), QStringLiteral( "--input=%1" ).arg( layer->source() ), QStringLiteral( "--output=%1" ).arg( outputFile ), QStringLiteral( "--algorithm=statistical" ), QStringLiteral( "--remove-noise-points=%1" ).arg( removeNoisePoints ), QStringLiteral( "--statistical-mean-k=%1" ).arg( meanK ), QStringLiteral( "--statistical-multiplier=%1" ).arg( multiplier ) }; + + applyCommonParameters( args, layer->crs(), parameters, context ); + applyThreadsParameter( args, context ); + return args; +} + +///@endcond diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalfilternoisestatistical.h b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoisestatistical.h new file mode 100644 index 000000000000..5f0c8e3f0485 --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalfilternoisestatistical.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsalgorithmpdalfilternoisestatistical.h + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMPDALFILTERNOISESTATISTICAL_H +#define QGSALGORITHMPDALFILTERNOISESTATISTICAL_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgspdalalgorithmbase.h" + +///@cond PRIVATE + +/** + * Native point cloud filter noise using statistical algorithm. + */ +class QgsPdalFilterNoiseStatisticalAlgorithm : public QgsPdalAlgorithmBase +{ + public: + QgsPdalFilterNoiseStatisticalAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QString group() const override; + QString groupId() const override; + QStringList tags() const override; + QString shortHelpString() const override; + QString shortDescription() const override; + QgsPdalFilterNoiseStatisticalAlgorithm *createInstance() const override SIP_FACTORY; + + QStringList createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + friend class TestQgsProcessingPdalAlgs; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMPDALFILTERNOISESTATISTICAL_H diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundnearestneighbour.cpp b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundnearestneighbour.cpp new file mode 100644 index 000000000000..b1708a1d2d5c --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundnearestneighbour.cpp @@ -0,0 +1,110 @@ +/*************************************************************************** + qgsalgorithmpdalheightabovegroundnearestneighbour.cpp + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmpdalheightabovegroundnearestneighbour.h" + +#include "qgspointcloudlayer.h" +#include "qgsrunprocess.h" + +///@cond PRIVATE + +QString QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::name() const +{ + return QStringLiteral( "heightabovegroundbynearestneighbor" ); +} + +QString QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::displayName() const +{ + return QObject::tr( "Height above ground" ); +} + +QString QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::group() const +{ + return QObject::tr( "Point cloud data management" ); +} + +QString QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::groupId() const +{ + return QStringLiteral( "pointclouddatamanagement" ); +} + +QStringList QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::tags() const +{ + return QObject::tr( "pdal,lidar,height above ground,nearest neighbour,elevation" ).split( ',' ); +} + +QString QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm calculates the height of points above the ground surface in a point cloud using a nearest neighbor algorithm." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "For each point, the algorithm finds the specified number of nearest ground-classified points (classification value 2) and interpolates the ground elevation from them using inverse distance weighting." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "The output adds a HeightAboveGround dimension to the point cloud. If 'Replace Z values' is enabled, the Z coordinate will be replaced with the height above ground value." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "Maximum Distance parameter can limit the search radius (0 = no limit)." ); +} + +QString QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::shortDescription() const +{ + return QObject::tr( "Calculates the height of points above the ground surface in a point cloud using a nearest neighbor algorithm." ); +} + +QgsPdalHeightAboveGroundNearestNeighbourAlgorithm *QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::createInstance() const +{ + return new QgsPdalHeightAboveGroundNearestNeighbourAlgorithm(); +} + +void QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterPointCloudLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "REPLACE_Z" ), QObject::tr( "Replace Z values with height above ground" ), true ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "COUNT" ), QObject::tr( "Number of neighbors for terrain interpolation" ), Qgis::ProcessingNumberParameterType::Integer, 1 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "MAX_DISTANCE" ), QObject::tr( "Maximum search distance" ), Qgis::ProcessingNumberParameterType::Double, 0.0 ) ); + + addParameter( new QgsProcessingParameterPointCloudDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Height above ground (nearest neighbour)" ) ) ); +} + +QStringList QgsPdalHeightAboveGroundNearestNeighbourAlgorithm::createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( feedback ); + + QgsPointCloudLayer *layer = parameterAsPointCloudLayer( parameters, QStringLiteral( "INPUT" ), context, QgsProcessing::LayerOptionsFlag::SkipIndexGeneration ); + if ( !layer ) + throw QgsProcessingException( invalidPointCloudError( parameters, QStringLiteral( "INPUT" ) ) ); + + const QString outputName = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT" ), context ); + QString outputFile = fixOutputFileName( layer->source(), outputName, context ); + checkOutputFormat( layer->source(), outputFile ); + setOutputValue( QStringLiteral( "OUTPUT" ), outputFile ); + + const int count = parameterAsInt( parameters, QStringLiteral( "COUNT" ), context ); + const double maxDistance = parameterAsDouble( parameters, QStringLiteral( "MAX_DISTANCE" ), context ); + + QString replaceZ = "false"; + if ( parameterAsBoolean( parameters, QStringLiteral( "REPLACE_Z" ), context ) ) + { + replaceZ = "true"; + } + + QStringList args = { QStringLiteral( "height_above_ground" ), QStringLiteral( "--input=%1" ).arg( layer->source() ), QStringLiteral( "--output=%1" ).arg( outputFile ), QStringLiteral( "--algorithm=nn" ), QStringLiteral( "--replace-z=%1" ).arg( replaceZ ), QStringLiteral( "--nn-count=%1" ).arg( count ), QStringLiteral( "--nn-max-distance=%1" ).arg( maxDistance ) }; + + applyCommonParameters( args, layer->crs(), parameters, context ); + applyThreadsParameter( args, context ); + return args; +} + +///@endcond diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundnearestneighbour.h b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundnearestneighbour.h new file mode 100644 index 000000000000..d30554bc34af --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundnearestneighbour.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsalgorithmpdalheightabovegroundnearestneighbour.h + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMPDALHEIGHTABOVEGROUNDNEARESTNEIGHBOUR_H +#define QGSALGORITHMPDALHEIGHTABOVEGROUNDNEARESTNEIGHBOUR_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgspdalalgorithmbase.h" + +///@cond PRIVATE + +/** + * Native point cloud height above ground using nearest neighbour algorithm. + */ +class QgsPdalHeightAboveGroundNearestNeighbourAlgorithm : public QgsPdalAlgorithmBase +{ + public: + QgsPdalHeightAboveGroundNearestNeighbourAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QString group() const override; + QString groupId() const override; + QStringList tags() const override; + QString shortHelpString() const override; + QString shortDescription() const override; + QgsPdalHeightAboveGroundNearestNeighbourAlgorithm *createInstance() const override SIP_FACTORY; + + QStringList createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + friend class TestQgsProcessingPdalAlgs; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMPDALHEIGHTABOVEGROUNDNEARESTNEIGHBOUR_H diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundtriangulation.cpp b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundtriangulation.cpp new file mode 100644 index 000000000000..5eca51a92b8a --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundtriangulation.cpp @@ -0,0 +1,106 @@ +/*************************************************************************** + qgsalgorithmpdalheightabovegrounddelaunay.cpp + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmpdalheightabovegroundtriangulation.h" + +#include "qgspointcloudlayer.h" +#include "qgsrunprocess.h" + +///@cond PRIVATE + +QString QgsPdalHeightAboveGroundTriangulationAlgorithm::name() const +{ + return QStringLiteral( "heightabovegroundtriangulation" ); +} + +QString QgsPdalHeightAboveGroundTriangulationAlgorithm::displayName() const +{ + return QObject::tr( "Height above ground (using triangulation)" ); +} + +QString QgsPdalHeightAboveGroundTriangulationAlgorithm::group() const +{ + return QObject::tr( "Point cloud data management" ); +} + +QString QgsPdalHeightAboveGroundTriangulationAlgorithm::groupId() const +{ + return QStringLiteral( "pointclouddatamanagement" ); +} + +QStringList QgsPdalHeightAboveGroundTriangulationAlgorithm::tags() const +{ + return QObject::tr( "pdal,lidar,height above ground,delaunay,triangulation,elevation" ).split( ',' ); +} + +QString QgsPdalHeightAboveGroundTriangulationAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm calculates the height of points above the ground surface in a point cloud using a Delaunay triangulation algorithm." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "The algorithm uses ground-classified points (classification value 2) to create a triangulated irregular network (TIN) from specified number of neighbors, then computes the height above this surface for all points." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "The output adds a HeightAboveGround dimension to the point cloud. If 'Replace Z values' is enabled, the Z coordinate will be replaced with the height above ground value." ); +} + +QString QgsPdalHeightAboveGroundTriangulationAlgorithm::shortDescription() const +{ + return QObject::tr( "Calculates the height of points above the ground surface using a Delaunay algorithm." ); +} + +QgsPdalHeightAboveGroundTriangulationAlgorithm *QgsPdalHeightAboveGroundTriangulationAlgorithm::createInstance() const +{ + return new QgsPdalHeightAboveGroundTriangulationAlgorithm(); +} + +void QgsPdalHeightAboveGroundTriangulationAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterPointCloudLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "REPLACE_Z" ), QObject::tr( "Replace Z values with height above ground" ), true ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "COUNT" ), QObject::tr( "Number of ground neighbors for terrain construction" ), Qgis::ProcessingNumberParameterType::Integer, 10 ) ); + + addParameter( new QgsProcessingParameterPointCloudDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Height above ground (delaunay algorithm)" ) ) ); +} + +QStringList QgsPdalHeightAboveGroundTriangulationAlgorithm::createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( feedback ); + + QgsPointCloudLayer *layer = parameterAsPointCloudLayer( parameters, QStringLiteral( "INPUT" ), context, QgsProcessing::LayerOptionsFlag::SkipIndexGeneration ); + if ( !layer ) + throw QgsProcessingException( invalidPointCloudError( parameters, QStringLiteral( "INPUT" ) ) ); + + const QString outputName = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT" ), context ); + QString outputFile = fixOutputFileName( layer->source(), outputName, context ); + checkOutputFormat( layer->source(), outputFile ); + setOutputValue( QStringLiteral( "OUTPUT" ), outputFile ); + + const int count = parameterAsInt( parameters, QStringLiteral( "COUNT" ), context ); + + QString replaceZ = "false"; + if ( parameterAsBoolean( parameters, QStringLiteral( "REPLACE_Z" ), context ) ) + { + replaceZ = "true"; + } + + QStringList args = { QStringLiteral( "height_above_ground" ), QStringLiteral( "--input=%1" ).arg( layer->source() ), QStringLiteral( "--output=%1" ).arg( outputFile ), QStringLiteral( "--algorithm=delaunay" ), QStringLiteral( "--replace-z=%1" ).arg( replaceZ ), QStringLiteral( "--delaunay-count=%1" ).arg( count ) }; + + applyCommonParameters( args, layer->crs(), parameters, context ); + applyThreadsParameter( args, context ); + return args; +} + +///@endcond diff --git a/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundtriangulation.h b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundtriangulation.h new file mode 100644 index 000000000000..de260ace0512 --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdalheightabovegroundtriangulation.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsalgorithmpdalheightabovegrounddelaunay.h + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMPDALHEIGHTABOVEGROUNDTRIANGULATION_H +#define QGSALGORITHMPDALHEIGHTABOVEGROUNDTRIANGULATION_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgspdalalgorithmbase.h" + +///@cond PRIVATE + +/** + * Native point cloud height above ground using triangulation algorithm. + */ +class QgsPdalHeightAboveGroundTriangulationAlgorithm : public QgsPdalAlgorithmBase +{ + public: + QgsPdalHeightAboveGroundTriangulationAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QString group() const override; + QString groupId() const override; + QStringList tags() const override; + QString shortHelpString() const override; + QString shortDescription() const override; + QgsPdalHeightAboveGroundTriangulationAlgorithm *createInstance() const override SIP_FACTORY; + + QStringList createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + friend class TestQgsProcessingPdalAlgs; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMPDALHEIGHTABOVEGROUNDTRIANGULATION_H diff --git a/src/analysis/processing/pdal/qgsalgorithmpdaltransform.cpp b/src/analysis/processing/pdal/qgsalgorithmpdaltransform.cpp new file mode 100644 index 000000000000..23715b63ecf4 --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdaltransform.cpp @@ -0,0 +1,150 @@ +/*************************************************************************** + qgsalgorithmpdaltransform.cpp + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmpdaltransform.h" + +#include "qgsmatrix4x4.h" +#include "qgspointcloudlayer.h" +#include "qgsrunprocess.h" + +#include +#include + +///@cond PRIVATE + +QString QgsPdalTransformAlgorithm::name() const +{ + return QStringLiteral( "transformpointcloud" ); +} + +QString QgsPdalTransformAlgorithm::displayName() const +{ + return QObject::tr( "Transform point cloud" ); +} + +QString QgsPdalTransformAlgorithm::group() const +{ + return QObject::tr( "Point cloud data management" ); +} + +QString QgsPdalTransformAlgorithm::groupId() const +{ + return QStringLiteral( "pointclouddatamanagement" ); +} + +QStringList QgsPdalTransformAlgorithm::tags() const +{ + return QObject::tr( "pdal,lidar,assign,set,transform,coordinates" ).split( ',' ); +} + +QString QgsPdalTransformAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm transforms point cloud coordinates using translation, rotation, and scaling operations using a 4x4 transformation matrix." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "The algorithm applies transformations in the following order: scaling, rotation (using Euler angles), then translation." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "Rotation angles are specified in degrees around the X, Y, and Z axes." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "All parameters are combined into a 4×4 transformation matrix that is passed to PDAL Wrench." ); +} + +QString QgsPdalTransformAlgorithm::shortDescription() const +{ + return QObject::tr( "Transforms point cloud coordinates using translation, rotation, and scaling using 4x4 matrix." ); +} + +QgsPdalTransformAlgorithm *QgsPdalTransformAlgorithm::createInstance() const +{ + return new QgsPdalTransformAlgorithm(); +} + +void QgsPdalTransformAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterPointCloudLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TRANSLATE_X" ), QObject::tr( "X Translation" ), Qgis::ProcessingNumberParameterType::Double, 0.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TRANSLATE_Y" ), QObject::tr( "Y Translation" ), Qgis::ProcessingNumberParameterType::Double, 0.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TRANSLATE_Z" ), QObject::tr( "Z Translation" ), Qgis::ProcessingNumberParameterType::Double, 0.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "SCALE_X" ), QObject::tr( "X Scale" ), Qgis::ProcessingNumberParameterType::Double, 1.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "SCALE_Y" ), QObject::tr( "Y Scale" ), Qgis::ProcessingNumberParameterType::Double, 1.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "SCALE_Z" ), QObject::tr( "Z Scale" ), Qgis::ProcessingNumberParameterType::Double, 1.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "ROTATE_X" ), QObject::tr( "X Rotation" ), Qgis::ProcessingNumberParameterType::Double, 0.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "ROTATE_Y" ), QObject::tr( "Y Rotation" ), Qgis::ProcessingNumberParameterType::Double, 0.0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "ROTATE_Z" ), QObject::tr( "Z Rotation" ), Qgis::ProcessingNumberParameterType::Double, 0.0 ) ); + + addParameter( new QgsProcessingParameterPointCloudDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Transformed" ) ) ); +} + +QStringList QgsPdalTransformAlgorithm::createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( feedback ); + + QgsPointCloudLayer *layer = parameterAsPointCloudLayer( parameters, QStringLiteral( "INPUT" ), context, QgsProcessing::LayerOptionsFlag::SkipIndexGeneration ); + if ( !layer ) + throw QgsProcessingException( invalidPointCloudError( parameters, QStringLiteral( "INPUT" ) ) ); + + const QString outputName = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT" ), context ); + QString outputFile = fixOutputFileName( layer->source(), outputName, context ); + checkOutputFormat( layer->source(), outputFile ); + setOutputValue( QStringLiteral( "OUTPUT" ), outputFile ); + + const double translateX = parameterAsDouble( parameters, QStringLiteral( "TRANSLATE_X" ), context ); + const double translateY = parameterAsDouble( parameters, QStringLiteral( "TRANSLATE_Y" ), context ); + const double translateZ = parameterAsDouble( parameters, QStringLiteral( "TRANSLATE_Z" ), context ); + const double scaleX = parameterAsDouble( parameters, QStringLiteral( "SCALE_X" ), context ); + const double scaleY = parameterAsDouble( parameters, QStringLiteral( "SCALE_Y" ), context ); + const double scaleZ = parameterAsDouble( parameters, QStringLiteral( "SCALE_Z" ), context ); + const float rotateX = static_cast( parameterAsDouble( parameters, QStringLiteral( "ROTATE_X" ), context ) ); + const float rotateY = static_cast( parameterAsDouble( parameters, QStringLiteral( "ROTATE_Y" ), context ) ); + const float rotateZ = static_cast( parameterAsDouble( parameters, QStringLiteral( "ROTATE_Z" ), context ) ); + + const QMatrix3x3 rotation3x3 = QQuaternion::fromEulerAngles( rotateX, rotateY, rotateZ ).toRotationMatrix(); + + const QgsMatrix4x4 rotateMatrix = QgsMatrix4x4( rotation3x3( 0, 0 ), rotation3x3( 0, 1 ), rotation3x3( 0, 2 ), 0, rotation3x3( 1, 0 ), rotation3x3( 1, 1 ), rotation3x3( 1, 2 ), 0, rotation3x3( 2, 0 ), rotation3x3( 2, 1 ), rotation3x3( 2, 2 ), 0, 0, 0, 0, 1 ); + + const QgsMatrix4x4 translateMatrix = QgsMatrix4x4( 1, 0, 0, translateX, 0, 1, 0, translateY, 0, 0, 1, translateZ, 0, 0, 0, 1 ); + + const QgsMatrix4x4 scaleMatrix = QgsMatrix4x4( scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1 ); + + QgsMatrix4x4 transformMatrix = translateMatrix * rotateMatrix * scaleMatrix; + + const QString transformString = QStringLiteral( "%1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12 %13 %14 %15 %16" ) + .arg( transformMatrix.data()[0] ) + .arg( transformMatrix.data()[4] ) + .arg( transformMatrix.data()[8] ) + .arg( transformMatrix.data()[12] ) + .arg( transformMatrix.data()[1] ) + .arg( transformMatrix.data()[5] ) + .arg( transformMatrix.data()[9] ) + .arg( transformMatrix.data()[13] ) + .arg( transformMatrix.data()[2] ) + .arg( transformMatrix.data()[6] ) + .arg( transformMatrix.data()[10] ) + .arg( transformMatrix.data()[14] ) + .arg( transformMatrix.data()[3] ) + .arg( transformMatrix.data()[7] ) + .arg( transformMatrix.data()[11] ) + .arg( transformMatrix.data()[15] ); + + QStringList args = { QStringLiteral( "translate" ), QStringLiteral( "--input=%1" ).arg( layer->source() ), QStringLiteral( "--output=%1" ).arg( outputFile ), QStringLiteral( "--transform-matrix=%1" ).arg( transformString ) }; + + applyCommonParameters( args, layer->crs(), parameters, context ); + applyThreadsParameter( args, context ); + return args; +} + +///@endcond diff --git a/src/analysis/processing/pdal/qgsalgorithmpdaltransform.h b/src/analysis/processing/pdal/qgsalgorithmpdaltransform.h new file mode 100644 index 000000000000..37107606306e --- /dev/null +++ b/src/analysis/processing/pdal/qgsalgorithmpdaltransform.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsalgorithmpdaltransform.h + --------------------- + begin : December 2025 + copyright : (C) 2025 by Jan Caha + email : jan.caha at outlook dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMPDALTRANSFORM_H +#define QGSALGORITHMPDALTRANSFORM_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgspdalalgorithmbase.h" + +///@cond PRIVATE + +/** + * Native point cloud transform algorithm. + */ +class QgsPdalTransformAlgorithm : public QgsPdalAlgorithmBase +{ + public: + QgsPdalTransformAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QString group() const override; + QString groupId() const override; + QStringList tags() const override; + QString shortHelpString() const override; + QString shortDescription() const override; + QgsPdalTransformAlgorithm *createInstance() const override SIP_FACTORY; + + QStringList createArgumentLists( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + friend class TestQgsProcessingPdalAlgs; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMPDALTRANSFORM_H diff --git a/src/analysis/processing/pdal/qgspdalalgorithms.cpp b/src/analysis/processing/pdal/qgspdalalgorithms.cpp index 8bd1d05e7b47..ba7e53a49474 100644 --- a/src/analysis/processing/pdal/qgspdalalgorithms.cpp +++ b/src/analysis/processing/pdal/qgspdalalgorithms.cpp @@ -20,6 +20,7 @@ #include "qgsalgorithmpdalassignprojection.h" #include "qgsalgorithmpdalboundary.h" #include "qgsalgorithmpdalbuildvpc.h" +#include "qgsalgorithmpdalclassifyground.h" #include "qgsalgorithmpdalclip.h" #include "qgsalgorithmpdalconvertformat.h" #include "qgsalgorithmpdalcreatecopc.h" @@ -28,12 +29,17 @@ #include "qgsalgorithmpdalexportrastertin.h" #include "qgsalgorithmpdalexportvector.h" #include "qgsalgorithmpdalfilter.h" +#include "qgsalgorithmpdalfilternoiseradius.h" +#include "qgsalgorithmpdalfilternoisestatistical.h" +#include "qgsalgorithmpdalheightabovegroundnearestneighbour.h" +#include "qgsalgorithmpdalheightabovegroundtriangulation.h" #include "qgsalgorithmpdalinformation.h" #include "qgsalgorithmpdalmerge.h" #include "qgsalgorithmpdalreproject.h" #include "qgsalgorithmpdalthinbydecimate.h" #include "qgsalgorithmpdalthinbyradius.h" #include "qgsalgorithmpdaltile.h" +#include "qgsalgorithmpdaltransform.h" #include "qgsapplication.h" #include "qgsruntimeprofiler.h" @@ -111,6 +117,12 @@ void QgsPdalAlgorithms::loadAlgorithms() addAlgorithm( new QgsPdalThinByDecimateAlgorithm() ); addAlgorithm( new QgsPdalThinByRadiusAlgorithm() ); addAlgorithm( new QgsPdalTileAlgorithm() ); + addAlgorithm( new QgsPdalHeightAboveGroundNearestNeighbourAlgorithm() ); + addAlgorithm( new QgsPdalHeightAboveGroundTriangulationAlgorithm() ); + addAlgorithm( new QgsPdalFilterNoiseStatisticalAlgorithm() ); + addAlgorithm( new QgsPdalFilterNoiseRadiusAlgorithm() ); + addAlgorithm( new QgsPdalClassifyGroundAlgorithm() ); + addAlgorithm( new QgsPdalTransformAlgorithm() ); } ///@endcond diff --git a/tests/src/analysis/testqgsprocessingpdalalgs.cpp b/tests/src/analysis/testqgsprocessingpdalalgs.cpp index e86d3d4a89f5..b85b8c89646a 100644 --- a/tests/src/analysis/testqgsprocessingpdalalgs.cpp +++ b/tests/src/analysis/testqgsprocessingpdalalgs.cpp @@ -57,6 +57,12 @@ class TestQgsProcessingPdalAlgs : public QgsTest void thinByDecimate(); void thinByRadius(); void tile(); + void heightAboveGroundTriangulation(); + void heightAboveGroundNearestNeighbour(); + void classifyGround(); + void filterNoiseStatistical(); + void filterNoiseRadius(); + void transformCoordinates(); void useIndexCopcFile(); @@ -903,5 +909,182 @@ void TestQgsProcessingPdalAlgs::useIndexCopcFile() QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); } +void TestQgsProcessingPdalAlgs::heightAboveGroundTriangulation() +{ + QgsPdalAlgorithmBase *alg = const_cast( static_cast( QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "pdal:heightabovegroundtriangulation" ) ) ) ); + + auto context = std::make_unique(); + context->setProject( QgsProject::instance() ); + context->setMaximumThreads( 0 ); + + QgsProcessingFeedback feedback; + + const QString outputPointCloud = QDir::tempPath() + "/heightabovegroundtriangulation.laz"; + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), mPointCloudLayerPath ); + parameters.insert( QStringLiteral( "OUTPUT" ), outputPointCloud ); + + QStringList args = alg->createArgumentLists( parameters, *context, &feedback ); + QCOMPARE( args, QStringList() << QStringLiteral( "height_above_ground" ) << QStringLiteral( "--input=%1" ).arg( mPointCloudLayerPath ) << QStringLiteral( "--output=%1" ).arg( outputPointCloud ) << QStringLiteral( "--algorithm=delaunay" ) << QStringLiteral( "--replace-z=true" ) << QStringLiteral( "--delaunay-count=10" ) ); + QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); +} + +void TestQgsProcessingPdalAlgs::heightAboveGroundNearestNeighbour() +{ + QgsPdalAlgorithmBase *alg = const_cast( static_cast( QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "pdal:heightabovegroundbynearestneighbor" ) ) ) ); + + auto context = std::make_unique(); + context->setProject( QgsProject::instance() ); + context->setMaximumThreads( 0 ); + + QgsProcessingFeedback feedback; + + const QString outputPointCloud = QDir::tempPath() + "/heightabovegroundnn.laz"; + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), mPointCloudLayerPath ); + parameters.insert( QStringLiteral( "OUTPUT" ), outputPointCloud ); + parameters.insert( QStringLiteral( "REPLACE_Z" ), false ); + + QStringList args = alg->createArgumentLists( parameters, *context, &feedback ); + QCOMPARE( args, QStringList() << QStringLiteral( "height_above_ground" ) << QStringLiteral( "--input=%1" ).arg( mPointCloudLayerPath ) << QStringLiteral( "--output=%1" ).arg( outputPointCloud ) << QStringLiteral( "--algorithm=nn" ) << QStringLiteral( "--replace-z=false" ) << QStringLiteral( "--nn-count=1" ) << QStringLiteral( "--nn-max-distance=0" ) ); + QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); +} + +void TestQgsProcessingPdalAlgs::classifyGround() +{ + QgsPdalAlgorithmBase *alg = const_cast( static_cast( QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "pdal:classifyground" ) ) ) ); + + auto context = std::make_unique(); + context->setProject( QgsProject::instance() ); + context->setMaximumThreads( 0 ); + + QgsProcessingFeedback feedback; + + const QString outputPointCloud = QDir::tempPath() + "/classifyground.laz"; + + double cellSize = 1.5; + double scalar = 1.3; + double slope = 0.2; + double threshold = 0.55; + double windowSize = 20; + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), mPointCloudLayerPath ); + parameters.insert( QStringLiteral( "OUTPUT" ), outputPointCloud ); + parameters.insert( QStringLiteral( "CELL_SIZE" ), cellSize ); + parameters.insert( QStringLiteral( "SCALAR" ), scalar ); + parameters.insert( QStringLiteral( "SLOPE" ), slope ); + parameters.insert( QStringLiteral( "THRESHOLD" ), threshold ); + parameters.insert( QStringLiteral( "WINDOW_SIZE" ), windowSize ); + + QStringList args = alg->createArgumentLists( parameters, *context, &feedback ); + QCOMPARE( args, QStringList() << QStringLiteral( "classify_ground" ) << QStringLiteral( "--input=%1" ).arg( mPointCloudLayerPath ) << QStringLiteral( "--output=%1" ).arg( outputPointCloud ) << QStringLiteral( "--cell-size=%1" ).arg( cellSize ) << QStringLiteral( "--scalar=%1" ).arg( scalar ) << QStringLiteral( "--slope=%1" ).arg( slope ) << QStringLiteral( "--threshold=%1" ).arg( threshold ) << QStringLiteral( "--window-size=%1" ).arg( windowSize ) ); + QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); +} + +void TestQgsProcessingPdalAlgs::filterNoiseStatistical() +{ + QgsPdalAlgorithmBase *alg = const_cast( static_cast( QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "pdal:filternoisestatistical" ) ) ) ); + + auto context = std::make_unique(); + context->setProject( QgsProject::instance() ); + context->setMaximumThreads( 0 ); + + QgsProcessingFeedback feedback; + + const QString outputPointCloud = QDir::tempPath() + "/filternoisestatistical.laz"; + + double meanK = 10; + double multiplier = 3.0; + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), mPointCloudLayerPath ); + parameters.insert( QStringLiteral( "OUTPUT" ), outputPointCloud ); + parameters.insert( QStringLiteral( "REMOVE_NOISE_POINTS" ), true ); + parameters.insert( QStringLiteral( "MEAN_K" ), meanK ); + parameters.insert( QStringLiteral( "MULTIPLIER" ), multiplier ); + + + QStringList args = alg->createArgumentLists( parameters, *context, &feedback ); + QCOMPARE( args, QStringList() << QStringLiteral( "filter_noise" ) << QStringLiteral( "--input=%1" ).arg( mPointCloudLayerPath ) << QStringLiteral( "--output=%1" ).arg( outputPointCloud ) << QStringLiteral( "--algorithm=statistical" ) << QStringLiteral( "--remove-noise-points=true" ) << QStringLiteral( "--statistical-mean-k=%1" ).arg( meanK ) << QStringLiteral( "--statistical-multiplier=%1" ).arg( multiplier ) ); + QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); +} + +void TestQgsProcessingPdalAlgs::filterNoiseRadius() +{ + QgsPdalAlgorithmBase *alg = const_cast( static_cast( QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "pdal:filternoiseradius" ) ) ) ); + + auto context = std::make_unique(); + context->setProject( QgsProject::instance() ); + context->setMaximumThreads( 0 ); + + QgsProcessingFeedback feedback; + + const QString outputPointCloud = QDir::tempPath() + "/filternoiseradius.laz"; + + double minK = 2.5; + double radius = 1.5; + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), mPointCloudLayerPath ); + parameters.insert( QStringLiteral( "OUTPUT" ), outputPointCloud ); + parameters.insert( QStringLiteral( "REMOVE_NOISE_POINTS" ), false ); + parameters.insert( QStringLiteral( "MIN_K" ), minK ); + parameters.insert( QStringLiteral( "RADIUS" ), radius ); + + QStringList args = alg->createArgumentLists( parameters, *context, &feedback ); + QCOMPARE( args, QStringList() << QStringLiteral( "filter_noise" ) << QStringLiteral( "--input=%1" ).arg( mPointCloudLayerPath ) << QStringLiteral( "--output=%1" ).arg( outputPointCloud ) << QStringLiteral( "--algorithm=radius" ) << QStringLiteral( "--remove-noise-points=false" ) << QStringLiteral( "--radius-min-k=%1" ).arg( minK ) << QStringLiteral( "--radius-radius=%1" ).arg( radius ) ); + QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); +} + +void TestQgsProcessingPdalAlgs::transformCoordinates() +{ + QgsPdalAlgorithmBase *alg = const_cast( static_cast( QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "pdal:transformpointcloud" ) ) ) ); + + auto context = std::make_unique(); + context->setProject( QgsProject::instance() ); + context->setMaximumThreads( 0 ); + + QgsProcessingFeedback feedback; + + const QString outputPointCloud = QDir::tempPath() + "/transformcoordinates.laz"; + + double translateX = 10.0; + double translateY = 20.0; + double translateZ = 30.0; + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), mPointCloudLayerPath ); + parameters.insert( QStringLiteral( "OUTPUT" ), outputPointCloud ); + parameters.insert( QStringLiteral( "TRANSLATE_X" ), translateX ); + parameters.insert( QStringLiteral( "TRANSLATE_Y" ), translateY ); + parameters.insert( QStringLiteral( "TRANSLATE_Z" ), translateZ ); + + QString transformMatrix = QStringLiteral( "1 0 0 %1 0 1 0 %2 0 0 1 %3 0 0 0 1" ).arg( translateX ).arg( translateY ).arg( translateZ ); + + QStringList args = alg->createArgumentLists( parameters, *context, &feedback ); + QCOMPARE( args, QStringList() << QStringLiteral( "translate" ) << QStringLiteral( "--input=%1" ).arg( mPointCloudLayerPath ) << QStringLiteral( "--output=%1" ).arg( outputPointCloud ) << QStringLiteral( "--transform-matrix=%1" ).arg( transformMatrix ) ); + QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); + + double scaleX = 2.0; + double scaleY = 3.0; + double scaleZ = 4.0; + + parameters.clear(); + parameters.insert( QStringLiteral( "INPUT" ), mPointCloudLayerPath ); + parameters.insert( QStringLiteral( "OUTPUT" ), outputPointCloud ); + parameters.insert( QStringLiteral( "SCALE_X" ), scaleX ); + parameters.insert( QStringLiteral( "SCALE_Y" ), scaleY ); + parameters.insert( QStringLiteral( "SCALE_Z" ), scaleZ ); + + transformMatrix = QStringLiteral( "%1 0 0 0 0 %2 0 0 0 0 %3 0 0 0 0 1" ).arg( scaleX ).arg( scaleY ).arg( scaleZ ); + + args = alg->createArgumentLists( parameters, *context, &feedback ); + QCOMPARE( args, QStringList() << QStringLiteral( "translate" ) << QStringLiteral( "--input=%1" ).arg( mPointCloudLayerPath ) << QStringLiteral( "--output=%1" ).arg( outputPointCloud ) << QStringLiteral( "--transform-matrix=%1" ).arg( transformMatrix ) ); + QVERIFY( args.at( 1 ).endsWith( "copc.laz" ) ); +} + QGSTEST_MAIN( TestQgsProcessingPdalAlgs ) #include "testqgsprocessingpdalalgs.moc"