mirror of
https://github.com/dgirardeau/q3DMASC.git
synced 2026-08-30 00:50:49 +08:00
932 lines
24 KiB
C++
932 lines
24 KiB
C++
//##########################################################################
|
|
//# #
|
|
//# CLOUDCOMPARE PLUGIN: q3DMASC #
|
|
//# #
|
|
//# This program is free software; you can redistribute it and/or modify #
|
|
//# it under the terms of the GNU General Public License as published by #
|
|
//# the Free Software Foundation; version 2 or later of the License. #
|
|
//# #
|
|
//# This program is distributed in the hope that it will be useful, #
|
|
//# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
//# GNU General Public License for more details. #
|
|
//# #
|
|
//# COPYRIGHT: Dimitri Lague / CNRS / UEB #
|
|
//# #
|
|
//##########################################################################
|
|
|
|
#include "PointFeature.h"
|
|
|
|
//Local
|
|
#include "q3DMASCTools.h"
|
|
|
|
#if defined(_OPENMP)
|
|
#include <omp.h>
|
|
#endif
|
|
|
|
//qPDALIO
|
|
#ifdef PLUGIN_IO_QPDAL
|
|
#include "../../../core/IO/qPDALIO/include/LASFields.h"
|
|
#else
|
|
#include "../../../core/IO/qLASIO/include/LasDetails.h"
|
|
enum LAS_FIELDS {
|
|
LAS_X = 0,
|
|
LAS_Y = 1,
|
|
LAS_Z = 2,
|
|
LAS_INTENSITY = 3,
|
|
LAS_RETURN_NUMBER = 4,
|
|
LAS_NUMBER_OF_RETURNS = 5,
|
|
LAS_SCAN_DIRECTION = 6,
|
|
LAS_FLIGHT_LINE_EDGE = 7,
|
|
LAS_CLASSIFICATION = 8,
|
|
LAS_SCAN_ANGLE_RANK = 9,
|
|
LAS_USER_DATA = 10,
|
|
LAS_POINT_SOURCE_ID = 11,
|
|
LAS_RED = 12,
|
|
LAS_GREEN = 13,
|
|
LAS_BLUE = 14,
|
|
LAS_TIME = 15,
|
|
LAS_EXTRA = 16,
|
|
//Sub fields
|
|
LAS_CLASSIF_VALUE = 17,
|
|
LAS_CLASSIF_SYNTHETIC = 18,
|
|
LAS_CLASSIF_KEYPOINT = 19,
|
|
LAS_CLASSIF_WITHHELD = 20,
|
|
LAS_CLASSIF_OVERLAP = 21,
|
|
//Invald flag
|
|
LAS_INVALID = 255
|
|
};
|
|
|
|
constexpr const char* LAS_FIELD_NAMES[22] = {"X",
|
|
"Y",
|
|
"Z",
|
|
LasNames::Intensity,
|
|
LasNames::ReturnNumber,
|
|
LasNames::NumberOfReturns,
|
|
LasNames::ScanDirectionFlag,
|
|
LasNames::EdgeOfFlightLine,
|
|
LasNames::Classification,
|
|
LasNames::ScanAngleRank,
|
|
LasNames::UserData,
|
|
LasNames::PointSourceId,
|
|
"Red",
|
|
"Green",
|
|
"Blue",
|
|
LasNames::GpsTime,
|
|
"extra",
|
|
"[Classif] Value",
|
|
"[Classif] Synthetic flag",
|
|
"[Classif] Key-point flag",
|
|
"[Classif] Withheld flag",
|
|
"[Classif] Overlap flag",
|
|
};
|
|
#endif
|
|
|
|
//qCC_db
|
|
#include <ccPointCloud.h>
|
|
#include <ccScalarField.h>
|
|
|
|
//CCLib
|
|
#include <WeibullDistribution.h>
|
|
|
|
//system
|
|
#include <assert.h>
|
|
|
|
//Qt
|
|
#include <QCoreApplication>
|
|
#include <QMutex>
|
|
|
|
static const char* s_echoRatioSFName = "EchoRat";
|
|
static const char* s_NIRSFName = "NIR";
|
|
static const char* s_M3C2SFName = "M3C2 distance";
|
|
static const char* s_PCVSFName = "Illuminance (PCV)";
|
|
static const char* s_normDipSFName = "Norm dip";
|
|
static const char* s_normDipDirSFName = "Norm dip dir.";
|
|
|
|
using namespace masc;
|
|
|
|
bool PointFeature::checkValidity(QString corePointRole, QString &error) const
|
|
{
|
|
if (!Feature::checkValidity(corePointRole, error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (type == Invalid)
|
|
{
|
|
assert(false);
|
|
error = "invalid feature type";
|
|
return false;
|
|
}
|
|
|
|
assert(cloud1);
|
|
|
|
if (!corePointRole.isEmpty() && !scaled() && cloud1Label != corePointRole) //in some cases, we don't know the role of the core points yet!
|
|
{
|
|
error = "Scale-less features can only be computed on the core points / classified cloud";
|
|
return false;
|
|
}
|
|
|
|
if (scaled() && stat == NO_STAT)
|
|
{
|
|
error = "scaled point features need a STAT measure to be defined";
|
|
return false;
|
|
}
|
|
|
|
if (op != NO_OPERATION && !cloud2)
|
|
{
|
|
error = "math operations require two clouds";
|
|
return false;
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case PointFeature::Intensity:
|
|
{
|
|
if (cloud1->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_INTENSITY]) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(LAS_FIELD_NAMES[LAS_INTENSITY]);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::X:
|
|
case PointFeature::Y:
|
|
case PointFeature::Z:
|
|
return true;
|
|
case PointFeature::NbRet:
|
|
{
|
|
if (cloud1->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_NUMBER_OF_RETURNS]) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(LAS_FIELD_NAMES[LAS_NUMBER_OF_RETURNS]);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::RetNb:
|
|
{
|
|
if (cloud1->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_RETURN_NUMBER]) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(LAS_FIELD_NAMES[LAS_RETURN_NUMBER]);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::EchoRat:
|
|
{
|
|
if (cloud1->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_NUMBER_OF_RETURNS]) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(LAS_FIELD_NAMES[LAS_NUMBER_OF_RETURNS]);
|
|
return false;
|
|
}
|
|
if (cloud1->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_RETURN_NUMBER]) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(LAS_FIELD_NAMES[LAS_RETURN_NUMBER]);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::R:
|
|
case PointFeature::G:
|
|
case PointFeature::B:
|
|
if (!cloud1->hasColors())
|
|
{
|
|
error = QString("Cloud %0 has no RGB color").arg(cloud1->getName());
|
|
return false;
|
|
}
|
|
return true;
|
|
case PointFeature::NIR:
|
|
{
|
|
if (cloud1->getScalarFieldIndexByName(s_NIRSFName) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(s_NIRSFName);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::Dip:
|
|
case PointFeature::DipDir:
|
|
{
|
|
if (!cloud1->hasNormals())
|
|
{
|
|
error = QString("Cloud %0 has no normals").arg(cloud1->getName());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::M3C2:
|
|
{
|
|
if (cloud1->getScalarFieldIndexByName(s_M3C2SFName) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(s_M3C2SFName);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::PCV:
|
|
{
|
|
if (cloud1->getScalarFieldIndexByName(s_PCVSFName) < 0)
|
|
{
|
|
error = QString("Cloud %0 has no '%1' scalar field").arg(cloud1->getName()).arg(s_PCVSFName);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case PointFeature::SF:
|
|
if (sourceSFIndex >= static_cast<int>(cloud1->getNumberOfScalarFields()))
|
|
{
|
|
error = QString("Cloud %0 has no scalar field #%1").arg(cloud1->getName()).arg(sourceSFIndex);
|
|
return false;
|
|
}
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
IScalarFieldWrapper::Shared PointFeature::retrieveField(ccPointCloud* cloud, QString& error)
|
|
{
|
|
if (!cloud)
|
|
{
|
|
assert(false);
|
|
return IScalarFieldWrapper::Shared(nullptr);
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case PointFeature::Intensity:
|
|
{
|
|
CCCoreLib::ScalarField* sf = Tools::RetrieveSF(cloud, LAS_FIELD_NAMES[LAS_INTENSITY], false);
|
|
if (!sf)
|
|
{
|
|
error = "Cloud has no 'intensity' scalar field";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldWrapper(sf));
|
|
}
|
|
case PointFeature::X:
|
|
return IScalarFieldWrapper::Shared(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimX));
|
|
case PointFeature::Y:
|
|
return IScalarFieldWrapper::Shared(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimY));
|
|
case PointFeature::Z:
|
|
return IScalarFieldWrapper::Shared(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimZ));
|
|
case PointFeature::NbRet:
|
|
{
|
|
CCCoreLib::ScalarField* sf = Tools::RetrieveSF(cloud, LAS_FIELD_NAMES[LAS_NUMBER_OF_RETURNS], false);
|
|
if (!sf)
|
|
{
|
|
error = "Cloud has no 'number of returns' scalar field";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldWrapper(sf));
|
|
}
|
|
case PointFeature::RetNb:
|
|
{
|
|
CCCoreLib::ScalarField* sf = Tools::RetrieveSF(cloud, LAS_FIELD_NAMES[LAS_RETURN_NUMBER], false);
|
|
if (!sf)
|
|
{
|
|
error = "Cloud has no 'return number' scalar field";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldWrapper(sf));
|
|
}
|
|
case PointFeature::EchoRat:
|
|
{
|
|
//retrieve the two scalar fields 'p/q'
|
|
CCCoreLib::ScalarField* numberOfRetSF = Tools::RetrieveSF(cloud, LAS_FIELD_NAMES[LAS_NUMBER_OF_RETURNS], false);
|
|
if (!numberOfRetSF)
|
|
{
|
|
error = "Can't compute the 'echo ratio' field: no 'Number of Return' SF available";
|
|
return nullptr;
|
|
}
|
|
CCCoreLib::ScalarField* retNumberSF = Tools::RetrieveSF(cloud, LAS_FIELD_NAMES[LAS_RETURN_NUMBER], false);
|
|
if (!retNumberSF)
|
|
{
|
|
error = "Can't compute the 'echo ratio' field: no 'Return number' SF available";
|
|
return nullptr;
|
|
}
|
|
if (retNumberSF->size() != numberOfRetSF->size() || retNumberSF->size() != cloud->size())
|
|
{
|
|
error = "Internal error (inconsistent scalar fields)";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldRatioWrapper(retNumberSF, numberOfRetSF, "EchoRat"));
|
|
}
|
|
case PointFeature::R:
|
|
return IScalarFieldWrapper::Shared(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Red));
|
|
case PointFeature::G:
|
|
return IScalarFieldWrapper::Shared(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Green));
|
|
case PointFeature::B:
|
|
return IScalarFieldWrapper::Shared(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Blue));
|
|
case PointFeature::NIR:
|
|
{
|
|
CCCoreLib::ScalarField* sf = Tools::RetrieveSF(cloud, s_NIRSFName, false);
|
|
if (!sf)
|
|
{
|
|
error = "Cloud has no 'NIR' scalar field";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldWrapper(sf));
|
|
}
|
|
case PointFeature::Dip:
|
|
case PointFeature::DipDir:
|
|
{
|
|
//we need normals to compute the dip and dip direction!
|
|
if (!cloud->hasNormals())
|
|
{
|
|
error = "Cloud has no normals: can't compute dip or dip dir. angles";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new NormDipAndDipDirFieldWrapper(cloud, type == PointFeature::Dip ? NormDipAndDipDirFieldWrapper::Dip : NormDipAndDipDirFieldWrapper::DipDir));
|
|
}
|
|
case PointFeature::M3C2:
|
|
{
|
|
CCCoreLib::ScalarField* sf = Tools::RetrieveSF(cloud, s_M3C2SFName, true);
|
|
if (!sf)
|
|
{
|
|
error = "Cloud has no 'm3c2 distance' scalar field";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldWrapper(sf));
|
|
}
|
|
case PointFeature::PCV:
|
|
{
|
|
CCCoreLib::ScalarField* sf = Tools::RetrieveSF(cloud, s_PCVSFName, true);
|
|
if (!sf)
|
|
{
|
|
error = "Cloud has no 'PCV/Illuminance' scalar field";
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldWrapper(sf));
|
|
}
|
|
case PointFeature::SF:
|
|
if (sourceSFIndex < 0 || sourceSFIndex >= static_cast<int>(cloud->getNumberOfScalarFields()))
|
|
{
|
|
error = QString("Can't retrieve the specified SF: invalid index (%1)").arg(sourceSFIndex);
|
|
return nullptr;
|
|
}
|
|
return IScalarFieldWrapper::Shared(new ScalarFieldWrapper(cloud->getScalarField(sourceSFIndex)));
|
|
default:
|
|
break;
|
|
}
|
|
|
|
error = "Unhandled feature type";
|
|
return nullptr;
|
|
}
|
|
|
|
static bool ComputeMathOpWithNearestNeighbor( const CorePoints& corePoints,
|
|
const IScalarFieldWrapper& field1,
|
|
CCCoreLib::ScalarField* outSF,
|
|
ccPointCloud& cloud2,
|
|
const IScalarFieldWrapper& field2,
|
|
masc::Feature::Operation op,
|
|
QString& error,
|
|
CCCoreLib::GenericProgressCallback* progressCb = nullptr)
|
|
{
|
|
if (op == masc::Feature::NO_OPERATION || !outSF || outSF->size() != corePoints.size())
|
|
{
|
|
//invalid input parameters
|
|
assert(false);
|
|
error = "invalid input parameters";
|
|
return false;
|
|
}
|
|
|
|
ccOctree::Shared octree = cloud2.getOctree();
|
|
if (!octree)
|
|
{
|
|
octree = cloud2.computeOctree(progressCb);
|
|
if (!octree)
|
|
{
|
|
error = "failed to compute octree on cloud " + cloud2.getName();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//now extract the neighborhoods
|
|
unsigned char octreeLevel = octree->findBestLevelForAGivenPopulationPerCell(3);
|
|
ccLog::Print(QString("[Initial octree level] level = %1").arg(octreeLevel));
|
|
|
|
unsigned pointCount = corePoints.size();
|
|
QString logMessage = QString("Extracting %1 core points nearest neighbors in cloud %2").arg(pointCount).arg(cloud2.getName());
|
|
if (progressCb)
|
|
{
|
|
progressCb->setMethodTitle("Compute math operation");
|
|
progressCb->setInfo(qPrintable(logMessage));
|
|
}
|
|
ccLog::Print(logMessage);
|
|
CCCoreLib::NormalizedProgress nProgress(progressCb, pointCount);
|
|
|
|
double meanNeighborhoodSize = 0;
|
|
int tenth = pointCount / 10;
|
|
error.clear();
|
|
bool cancelled = false;
|
|
#ifndef _DEBUG
|
|
#if defined(_OPENMP)
|
|
#pragma omp parallel for num_threads(std::max(1, omp_get_max_threads() - 2))
|
|
#endif
|
|
#endif
|
|
for (int i = 0; i < static_cast<int>(pointCount); ++i)
|
|
{
|
|
if (!cancelled)
|
|
{
|
|
const CCVector3* P = corePoints.cloud->getPoint(i);
|
|
CCCoreLib::ReferenceCloud Yk(&cloud2);
|
|
double maxSquareDist = 0;
|
|
|
|
ScalarType s = CCCoreLib::NAN_VALUE;
|
|
|
|
int neighborhoodSize = 0;
|
|
if (octree->findPointNeighbourhood(P, &Yk, 1, octreeLevel, maxSquareDist, 0.0, &neighborhoodSize) >= 1)
|
|
{
|
|
double s1 = field1.pointValue(corePoints.originIndex(i));
|
|
double s2 = field2.pointValue(Yk.getPointGlobalIndex(0));
|
|
s = masc::Feature::PerformMathOp(s1, s2, op);
|
|
}
|
|
|
|
outSF->setValue(i, s);
|
|
|
|
if (i && (i % tenth) == 0)
|
|
{
|
|
double density = meanNeighborhoodSize / tenth;
|
|
if (density < 1.1)
|
|
{
|
|
if (octreeLevel + 1 < CCCoreLib::DgmOctree::MAX_OCTREE_LEVEL)
|
|
++octreeLevel;
|
|
}
|
|
else while (density > 2.9)
|
|
{
|
|
if (octreeLevel <= 5)
|
|
break;
|
|
--octreeLevel;
|
|
density /= 2.0;
|
|
}
|
|
ccLog::Print(QString("[Adaptative octree level] Mean neighborhood size: %1 --> new level = %2").arg(meanNeighborhoodSize / tenth).arg(octreeLevel));
|
|
meanNeighborhoodSize = 0;
|
|
}
|
|
else
|
|
{
|
|
meanNeighborhoodSize += neighborhoodSize;
|
|
}
|
|
|
|
if (progressCb)
|
|
{
|
|
cancelled = !nProgress.oneStep();
|
|
if (cancelled)
|
|
{
|
|
//process cancelled by the user
|
|
error = "[Point feature] Process cancelled";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
outSF->computeMinAndMax();
|
|
|
|
if (progressCb)
|
|
{
|
|
progressCb->stop();
|
|
}
|
|
|
|
return error.isEmpty();
|
|
}
|
|
|
|
bool PointFeature::prepare( const CorePoints& corePoints,
|
|
QString& error,
|
|
CCCoreLib::GenericProgressCallback* progressCb/*=nullptr*/,
|
|
SFCollector* generatedScalarFields/*=nullptr*/)
|
|
{
|
|
if (!cloud1 || !corePoints.cloud)
|
|
{
|
|
//invalid input
|
|
assert(false);
|
|
error = "internal error (no input core points)";
|
|
return false;
|
|
}
|
|
|
|
//look for the source field
|
|
assert(!field1);
|
|
field1 = retrieveField(cloud1, error);
|
|
if (!field1)
|
|
{
|
|
//error should be up to date
|
|
return false;
|
|
}
|
|
|
|
assert(!field2);
|
|
if (cloud2)
|
|
{
|
|
//no need to compute the second scalar field if no MATH operation has to be performed?!
|
|
if (op != Feature::NO_OPERATION)
|
|
{
|
|
field2 = retrieveField(cloud2, error);
|
|
if (!field2)
|
|
{
|
|
//error should be up to date
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
assert(false);
|
|
error = "Feature has a second cloud associated but no MATH operation is defined";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool isScaled = scaled();
|
|
|
|
//build the final SF name
|
|
QString resultSF1Name = field1->getName();
|
|
if (cloud2 || corePoints.role != cloud1Label)
|
|
{
|
|
resultSF1Name += "_" + cloud1Label;
|
|
}
|
|
|
|
if (isScaled)
|
|
{
|
|
//shall we extract a statistical measure? (mandatory for scaled feature)
|
|
if (stat == Feature::NO_STAT)
|
|
{
|
|
assert(false);
|
|
error = "Scaled features (SCx) must have an associated STAT measure";
|
|
return false;
|
|
}
|
|
resultSF1Name += QString("_") + Feature::StatToString(stat);
|
|
}
|
|
else //not scaled
|
|
{
|
|
if (cloud1 != corePoints.cloud && cloud1 != corePoints.origin)
|
|
{
|
|
assert(false);
|
|
error = "Scale-less features (SC0) can only be defined on the core points (origin) cloud";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (field2 && op != Feature::NO_OPERATION)
|
|
{
|
|
//include the math operation as well if necessary!
|
|
resultSF1Name += "_" + Feature::OpToString(op) + "_" + field2->getName() + "_" + cloud2Label;
|
|
if (isScaled)
|
|
{
|
|
assert(stat != Feature::NO_STAT);
|
|
resultSF1Name += QString("_") + Feature::StatToString(stat);
|
|
}
|
|
}
|
|
|
|
if (isScaled)
|
|
{
|
|
resultSF1Name += "@" + QString::number(scale);
|
|
|
|
//prepare the corresponding scalar field
|
|
sf1WasAlreadyExisting = CheckSFExistence(corePoints.cloud, qPrintable(resultSF1Name));
|
|
if (sf1WasAlreadyExisting)
|
|
{
|
|
// if the SF exists, it is not added to generatedScalarFields
|
|
statSF1 = PrepareSF(corePoints.cloud, qPrintable(resultSF1Name), generatedScalarFields, SFCollector::ALWAYS_KEEP);
|
|
if (generatedScalarFields->scalarFields.contains(statSF1)) // i.e. the SF is existing but was not present at the startup of the plugin
|
|
generatedScalarFields->setBehavior(statSF1, SFCollector::CAN_REMOVE);
|
|
}
|
|
else
|
|
statSF1 = PrepareSF(corePoints.cloud, qPrintable(resultSF1Name), generatedScalarFields, SFCollector::CAN_REMOVE);
|
|
if (!statSF1)
|
|
{
|
|
error = QString("Failed to prepare scalar field for field '%1' @ scale %2").arg(field1->getName()).arg(scale);
|
|
return false;
|
|
}
|
|
source.name = statSF1->getName();
|
|
|
|
if (field2 && op != Feature::NO_OPERATION && !sf1WasAlreadyExisting) // nothing to do if statSF1 was already there
|
|
{
|
|
QString resultSF2Name = field2->getName() + QString("_") + cloud2Label + "_" + Feature::StatToString(stat) + "@" + QString::number(scale);
|
|
//keepStatSF2 = (corePoints.cloud->getScalarFieldIndexByName(qPrintable(resultSFName2)) >= 0); //we remember that the scalar field was already existing!
|
|
|
|
assert(!statSF2);
|
|
sf2WasAlreadyExisting = CheckSFExistence(corePoints.cloud, qPrintable(resultSF2Name));
|
|
if (sf2WasAlreadyExisting)
|
|
statSF2 = PrepareSF(corePoints.cloud, qPrintable(resultSF2Name), generatedScalarFields, SFCollector::ALWAYS_KEEP);
|
|
else
|
|
statSF2 = PrepareSF(corePoints.cloud, qPrintable(resultSF2Name), generatedScalarFields, SFCollector::ALWAYS_REMOVE);
|
|
if (!statSF2)
|
|
{
|
|
error = QString("Failed to prepare scalar field for field '%1' @ scale %2").arg(field2->getName()).arg(scale);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else //non scaled feature
|
|
{
|
|
assert(cloud1 == corePoints.cloud || cloud1 == corePoints.origin);
|
|
|
|
//retrieve/create a SF to host the result
|
|
int sfIdx = corePoints.cloud->getScalarFieldIndexByName(qPrintable(resultSF1Name));
|
|
|
|
CCCoreLib::ScalarField* resultSF = nullptr;
|
|
if (sfIdx >= 0)
|
|
{
|
|
//reuse the existing field
|
|
resultSF = corePoints.cloud->getScalarField(sfIdx);
|
|
}
|
|
else
|
|
{
|
|
//copy the SF1 field
|
|
resultSF = new ccScalarField(qPrintable(resultSF1Name));
|
|
if (!resultSF->resizeSafe(corePoints.cloud->size()))
|
|
{
|
|
error = "Not enough memory";
|
|
resultSF->release();
|
|
return false;
|
|
}
|
|
|
|
if (op == NO_OPERATION)
|
|
{
|
|
//simply copy the values
|
|
for (unsigned i = 0; i < corePoints.size(); ++i)
|
|
{
|
|
resultSF->setValue(i, field1->pointValue(corePoints.originIndex(i)));
|
|
}
|
|
resultSF->computeMinAndMax();
|
|
}
|
|
else if (field2)
|
|
{
|
|
if (!ComputeMathOpWithNearestNeighbor( corePoints,
|
|
*field1,
|
|
resultSF,
|
|
*cloud2,
|
|
*field2,
|
|
op,
|
|
error,
|
|
progressCb)
|
|
)
|
|
{
|
|
error = "Failed to perform the MATH operation (" + error + ")";
|
|
resultSF->release();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int newSFIdx = corePoints.cloud->addScalarField(static_cast<ccScalarField*>(resultSF));
|
|
if (generatedScalarFields)
|
|
{
|
|
//track the generated scalar-field
|
|
generatedScalarFields->push(corePoints.cloud, resultSF, SFCollector::CAN_REMOVE);
|
|
}
|
|
|
|
corePoints.cloud->setCurrentDisplayedScalarField(newSFIdx);
|
|
}
|
|
|
|
source.name = resultSF->getName();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool PointFeature::computeStat(const CCCoreLib::DgmOctree::NeighboursSet& pointsInNeighbourhood, const IScalarFieldWrapper::Shared& sourceField, double& outputValue) const
|
|
{
|
|
outputValue = std::numeric_limits<double>::quiet_NaN();
|
|
|
|
if (!sourceField || stat == Feature::NO_STAT)
|
|
{
|
|
//invalid input parameters
|
|
assert(false);
|
|
return false;
|
|
}
|
|
|
|
size_t kNN = pointsInNeighbourhood.size();
|
|
if (kNN == 0)
|
|
{
|
|
assert(false);
|
|
return false;
|
|
}
|
|
|
|
//specific case
|
|
if (stat == Feature::RANGE)
|
|
{
|
|
double minValue = 0;
|
|
double maxValue = 0;
|
|
|
|
for (size_t k = 0; k < kNN; ++k)
|
|
{
|
|
unsigned index = pointsInNeighbourhood[k].pointIndex;
|
|
double v = sourceField->pointValue(index);
|
|
|
|
//track min and max values
|
|
if (k != 0)
|
|
{
|
|
if (v < minValue)
|
|
minValue = v;
|
|
else if (v > maxValue)
|
|
maxValue = v;
|
|
}
|
|
else
|
|
{
|
|
minValue = maxValue = v;
|
|
}
|
|
}
|
|
|
|
outputValue = maxValue - minValue;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
bool withSums = (stat == Feature::MEAN || stat == Feature::STD);
|
|
bool storeValues = (stat == Feature::MEDIAN || stat == Feature::MODE || stat == Feature::SKEW);
|
|
double sum = 0.0;
|
|
double sum2 = 0.0;
|
|
|
|
CCCoreLib::WeibullDistribution::ScalarContainer values;
|
|
if (storeValues)
|
|
{
|
|
try
|
|
{
|
|
values.resize(kNN);
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
ccLog::Warning("Not enough memory");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (unsigned k = 0; k < kNN; ++k)
|
|
{
|
|
unsigned index = pointsInNeighbourhood[k].pointIndex;
|
|
double v = sourceField->pointValue(index);
|
|
|
|
if (withSums)
|
|
{
|
|
//compute average and std. dev.
|
|
sum += v;
|
|
sum2 += v * v;
|
|
}
|
|
|
|
if (storeValues)
|
|
{
|
|
values[k] = static_cast<ScalarType>(v);
|
|
}
|
|
}
|
|
|
|
switch (stat)
|
|
{
|
|
case Feature::MEAN:
|
|
{
|
|
outputValue = sum / kNN;
|
|
}
|
|
break;
|
|
|
|
case Feature::MODE:
|
|
{
|
|
CCCoreLib::WeibullDistribution w;
|
|
if (w.computeParameters(values))
|
|
outputValue = w.computeMode();
|
|
}
|
|
break;
|
|
|
|
case Feature::MEDIAN:
|
|
{
|
|
size_t medianIndex = values.size() / 2;
|
|
std::nth_element(values.begin(), values.begin() + medianIndex, values.end());
|
|
outputValue = values[medianIndex];
|
|
}
|
|
break;
|
|
|
|
case Feature::STD:
|
|
{
|
|
outputValue = sqrt(std::abs(sum2 * kNN - sum * sum)) / kNN;
|
|
}
|
|
break;
|
|
|
|
case Feature::RANGE:
|
|
{
|
|
//we can't be here
|
|
assert(false);
|
|
}
|
|
return false;
|
|
|
|
case Feature::SKEW:
|
|
{
|
|
CCCoreLib::WeibullDistribution w;
|
|
if (w.computeParameters(values))
|
|
outputValue = w.computeSkewness();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
ccLog::Warning("Unhandled STAT measure");
|
|
assert(false);
|
|
}
|
|
return false;
|
|
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PointFeature::finish(const CorePoints& corePoints, QString& error)
|
|
{
|
|
if (!scaled())
|
|
{
|
|
//nothing to do
|
|
return true;
|
|
}
|
|
|
|
if (!corePoints.cloud)
|
|
{
|
|
//invalid input
|
|
assert(false);
|
|
error = "internal error (no input core points)";
|
|
return false;
|
|
}
|
|
|
|
bool success = true;
|
|
|
|
if (statSF1)
|
|
{
|
|
statSF1->computeMinAndMax();
|
|
|
|
//update display
|
|
//if (corePoints.cloud->getDisplay())
|
|
{
|
|
int sfIndex1 = corePoints.cloud->getScalarFieldIndexByName(statSF1->getName());
|
|
corePoints.cloud->setCurrentDisplayedScalarField(sfIndex1);
|
|
//corePoints.cloud->getDisplay()->redraw();
|
|
//QCoreApplication::processEvents();
|
|
}
|
|
}
|
|
|
|
if (statSF2 && !sf1WasAlreadyExisting)
|
|
{
|
|
//now perform the math operation
|
|
if (op != Feature::NO_OPERATION)
|
|
{
|
|
if (!PerformMathOp(statSF1, statSF2, op))
|
|
{
|
|
error = "Failed to perform the MATH operation";
|
|
success = false;
|
|
}
|
|
}
|
|
// statSF2->computeMinAndMax();
|
|
|
|
//DGM: we don't delete it now! As it could be used by other features!
|
|
//if (!keepStatSF2)
|
|
//{
|
|
// int sfIndex2 = corePoints.cloud->getScalarFieldIndexByName(statSF2->getName());
|
|
// if (sfIndex2 >= 0)
|
|
// {
|
|
// corePoints.cloud->deleteScalarField(sfIndex2);
|
|
// }
|
|
// else
|
|
// {
|
|
// assert(false);
|
|
// statSF2->release();
|
|
// }
|
|
// statSF2 = nullptr;
|
|
//}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
QString PointFeature::toString() const
|
|
{
|
|
//default keyword otherwise
|
|
QString description = ToString(type);
|
|
|
|
//special case for the 'SF' type
|
|
if (type == SF)
|
|
{
|
|
//'SF' + sf index
|
|
description += QString::number(sourceSFIndex);
|
|
}
|
|
|
|
if (scaled())
|
|
{
|
|
description += QString("_SC%1_%2").arg(scale).arg(StatToString(stat));
|
|
}
|
|
else
|
|
{
|
|
description += "_SC0";
|
|
}
|
|
|
|
description += "_" + cloud1Label;
|
|
|
|
if (cloud2 && !cloud2Label.isEmpty())
|
|
{
|
|
description += "_" + cloud2Label;
|
|
|
|
if (op != NO_OPERATION)
|
|
{
|
|
description += "_" + OpToString(op);
|
|
}
|
|
}
|
|
|
|
//Point features always have a scale equal to 0 by definition
|
|
return description;
|
|
}
|