You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

696 lines
22 KiB

/*
* PointCloud.cpp
*
* Copyright (c) 2014-2015 SEACAVE
*
* Author(s):
*
* cDc <cdc.seacave@gmail.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* Additional Terms:
*
* You are required to preserve legal notices and author attributions in
* that material or in the Appropriate Legal Notices displayed by works
* containing it.
*/
#include "Common.h"
#include "PointCloud.h"
#include "DepthMap.h"
using namespace MVS;
// D E F I N E S ///////////////////////////////////////////////////
// S T R U C T S ///////////////////////////////////////////////////
PointCloud& MVS::PointCloud::Swap(PointCloud& rhs)
{
points.Swap(rhs.points);
pointViews.Swap(rhs.pointViews);
pointWeights.Swap(rhs.pointWeights);
normals.Swap(rhs.normals);
colors.Swap(rhs.colors);
return *this;
}
/*----------------------------------------------------------------*/
void PointCloud::Release()
{
points.Release();
pointViews.Release();
pointWeights.Release();
normals.Release();
colors.Release();
}
/*----------------------------------------------------------------*/
void PointCloud::RemovePoint(IDX idx)
{
ASSERT(pointViews.IsEmpty() || pointViews.GetSize() == points.GetSize());
if (!pointViews.IsEmpty())
pointViews.RemoveAt(idx);
ASSERT(pointWeights.IsEmpty() || pointWeights.GetSize() == points.GetSize());
if (!pointWeights.IsEmpty())
pointWeights.RemoveAt(idx);
ASSERT(normals.IsEmpty() || normals.GetSize() == points.GetSize());
if (!normals.IsEmpty())
normals.RemoveAt(idx);
ASSERT(colors.IsEmpty() || colors.GetSize() == points.GetSize());
if (!colors.IsEmpty())
colors.RemoveAt(idx);
points.RemoveAt(idx);
}
void PointCloud::RemovePointsOutside(const OBB3f& obb) {
ASSERT(obb.IsValid());
RFOREACH(i, points)
if (!obb.Intersects(points[i]))
RemovePoint(i);
}
void PointCloud::RemoveMinViews(uint32_t thMinViews) {
ASSERT(!pointViews.empty());
RFOREACH(i, points)
if (pointViews[i].size() < thMinViews)
RemovePoint(i);
}
/*----------------------------------------------------------------*/
// compute the axis-aligned bounding-box of the point-cloud
PointCloud::Box PointCloud::GetAABB() const
{
Box box(true);
for (const Point& X: points)
box.InsertFull(X);
return box;
}
// same, but only for points inside the given AABB
PointCloud::Box PointCloud::GetAABB(const Box& bound) const
{
Box box(true);
for (const Point& X: points)
if (bound.Intersects(X))
box.InsertFull(X);
return box;
}
// compute the axis-aligned bounding-box of the point-cloud
// with more than the given number of views
PointCloud::Box PointCloud::GetAABB(unsigned minViews) const
{
if (pointViews.empty())
return GetAABB();
Box box(true);
FOREACH(idx, points)
if (pointViews[idx].size() >= minViews)
box.InsertFull(points[idx]);
return box;
}
// compute the center of the point-cloud as the median
PointCloud::Point PointCloud::GetCenter() const
{
const Index step(5);
const Index numPoints(points.size()/step);
if (numPoints == 0)
return Point::INF;
typedef CLISTDEF0IDX(Point::Type,Index) Scalars;
Scalars x(numPoints), y(numPoints), z(numPoints);
for (Index i=0; i<numPoints; ++i) {
const Point& X = points[i*step];
x[i] = X.x;
y[i] = X.y;
z[i] = X.z;
}
return Point(x.GetMedian(), y.GetMedian(), z.GetMedian());
}
/*----------------------------------------------------------------*/
// estimate the ground-plane as the plane agreeing with most points
// - planeThreshold: threshold used to estimate the ground plane (0 - auto)
Planef PointCloud::EstimateGroundPlane(const ImageArr& images, float planeThreshold, const String& fileExportPlane) const
{
ASSERT(!IsEmpty());
// remove some random points to speed up plane fitting
const unsigned randMinPoints(150000);
PointArr workPoints;
const PointArr* pPoints;
if (GetSize() > randMinPoints) {
#ifndef _RELEASE
SEACAVE::Random rnd(SEACAVE::Random::default_seed);
#else
SEACAVE::Random rnd;
#endif
const REAL randPointsRatio(MAXF(REAL(1e-4),(REAL)randMinPoints/GetSize()));
const SEACAVE::Random::result_type randPointsTh(CEIL2INT<SEACAVE::Random::result_type>(randPointsRatio*SEACAVE::Random::max()));
workPoints.reserve(CEIL2INT<PointArr::IDX>(randPointsRatio*GetSize()));
for (const Point& X: points)
if (rnd() <= randPointsTh)
workPoints.emplace_back(X);
pPoints = &workPoints;
} else {
pPoints = &points;
}
// fit plane to the point-cloud
Planef plane;
const float minInliersRatio(0.05f);
double threshold(planeThreshold>0 ? (double)planeThreshold : DBL_MAX);
const unsigned numInliers(planeThreshold > 0 ? EstimatePlaneTh(*pPoints, plane, threshold) : EstimatePlane(*pPoints, plane, threshold));
if (numInliers < MINF(ROUND2INT<unsigned>(minInliersRatio*pPoints->size()), 1000u)) {
plane.Invalidate();
return plane;
}
if (planeThreshold <= 0)
DEBUG("Ground plane estimated threshold: %g", threshold);
// refine plane to inliers
CLISTDEF0(Planef::POINT) inliers;
const float maxThreshold(static_cast<float>(threshold * 2));
for (const Point& X: *pPoints)
if (plane.DistanceAbs(X) < maxThreshold)
inliers.emplace_back(X);
const RobustNorm::GemanMcClure<double> robust(threshold);
plane.Optimize(inliers.data(), inliers.size(), robust);
// make sure the plane is well oriented, negate plane normal if it faces same direction as cameras on average
if (!images.empty()) {
FloatArr cosView(0, images.size());
for (const Image& imageData: images) {
if (!imageData.IsValid())
continue;
cosView.push_back(plane.m_vN.dot((const Point3f::EVecMap&)Cast<float>(imageData.camera.Direction())));
}
if (cosView.GetMedian() > 0)
plane.Negate();
}
// export points on the found plane if requested
if (!fileExportPlane.empty()) {
PointCloud pc;
const Point orig(Point(plane.m_vN)*-plane.m_fD);
pc.colors.emplace_back(Color::RED);
pc.points.emplace_back(orig);
for (const Point& X: *pPoints) {
const float dist(plane.DistanceAbs(X));
if (dist < threshold) {
pc.points.emplace_back(X);
const uint8_t color((uint8_t)(255.f*(1.f-dist/threshold)));
pc.colors.emplace_back(color, color, color);
}
}
pc.Save(fileExportPlane);
}
return plane;
}
/*----------------------------------------------------------------*/
// define a PLY file format composed only of vertices
namespace PointCloudInternal {
namespace BasicPLY {
// list of the kinds of elements in the PLY
static const char* elem_names[] = {
"vertex"
};
// list of property information for a vertex
struct Vertex {
PointCloud::Point p;
PointCloud::Color c;
PointCloud::Normal n;
struct Views {
uint8_t num;
uint32_t* pIndices;
float* pWeights;
} views;
float confidence;
float scale;
static void InitLoadProps(PLY& ply, int elem_count,
PointCloud::PointArr& points, PointCloud::ColorArr& colors, PointCloud::NormalArr& normals, PointCloud::PointViewArr& views, PointCloud::PointWeightArr& weights)
{
PLY::PlyElement* elm = ply.find_element(elem_names[0]);
const size_t nMaxProps(SizeOfArray(props));
for (size_t p=0; p<nMaxProps; ++p) {
if (ply.find_property(elm, props[p].name.c_str()) < 0)
continue;
ply.setup_property(props[p]);
switch (p) {
case 0: points.resize((IDX)elem_count); break;
case 3: case 13: colors.resize((IDX)elem_count); break;
case 6: normals.resize((IDX)elem_count); break;
case 9: views.resize((IDX)elem_count); break;
case 10: weights.resize((IDX)elem_count); break;
}
}
}
static void InitSaveProps(PLY& ply, int elem_count,
bool bColors, bool bNormals, bool bViews, bool bWeights, bool bConfidence=false, bool bScale=false)
{
ply.describe_property(elem_names[0], 3, props+0);
if (bColors)
ply.describe_property(elem_names[0], 3, props+3);
if (bNormals)
ply.describe_property(elem_names[0], 3, props+6);
if (bViews)
ply.describe_property(elem_names[0], props[9]);
if (bWeights)
ply.describe_property(elem_names[0], props[10]);
if (bConfidence)
ply.describe_property(elem_names[0], props[11]);
if (bScale)
ply.describe_property(elem_names[0], props[12]);
if (elem_count)
ply.element_count(elem_names[0], elem_count);
}
static const PLY::PlyProperty props[16];
};
const PLY::PlyProperty Vertex::props[16] = {
{"x", PLY::Float32, PLY::Float32, offsetof(Vertex,p.x), 0, 0, 0, 0},
{"y", PLY::Float32, PLY::Float32, offsetof(Vertex,p.y), 0, 0, 0, 0},
{"z", PLY::Float32, PLY::Float32, offsetof(Vertex,p.z), 0, 0, 0, 0},
{"red", PLY::Uint8, PLY::Uint8, offsetof(Vertex,c.r), 0, 0, 0, 0},
{"green", PLY::Uint8, PLY::Uint8, offsetof(Vertex,c.g), 0, 0, 0, 0},
{"blue", PLY::Uint8, PLY::Uint8, offsetof(Vertex,c.b), 0, 0, 0, 0},
{"nx", PLY::Float32, PLY::Float32, offsetof(Vertex,n.x), 0, 0, 0, 0},
{"ny", PLY::Float32, PLY::Float32, offsetof(Vertex,n.y), 0, 0, 0, 0},
{"nz", PLY::Float32, PLY::Float32, offsetof(Vertex,n.z), 0, 0, 0, 0},
{"view_indices", PLY::Uint32, PLY::Uint32, offsetof(Vertex,views.pIndices), 1, PLY::Uint8, PLY::Uint8, offsetof(Vertex,views.num)},
{"view_weights", PLY::Float32, PLY::Float32, offsetof(Vertex,views.pWeights), 1, PLY::Uint8, PLY::Uint8, offsetof(Vertex,views.num)},
{"confidence", PLY::Float32, PLY::Float32, offsetof(Vertex,confidence), 0, 0, 0, 0},
{"value", PLY::Float32, PLY::Float32, offsetof(Vertex,scale), 0, 0, 0, 0},
// duplicates
{"diffuse_red", PLY::Uint8, PLY::Uint8, offsetof(Vertex,c.r), 0, 0, 0, 0},
{"diffuse_green", PLY::Uint8, PLY::Uint8, offsetof(Vertex,c.g), 0, 0, 0, 0},
{"diffuse_blue", PLY::Uint8, PLY::Uint8, offsetof(Vertex,c.b), 0, 0, 0, 0}
};
} // namespace BasicPLY
} // namespace PointCloudInternal
// load the dense point cloud from a PLY file
bool PointCloud::Load(const String& fileName)
{
TD_TIMER_STARTD();
ASSERT(!fileName.empty());
Release();
// open PLY file and read header
using namespace PointCloudInternal;
PLY ply;
if (!ply.read(fileName)) {
DEBUG_EXTRA("error: invalid PLY file");
return false;
}
// read PLY body
for (int i = 0; i < ply.get_elements_count(); i++) {
int elem_count;
LPCSTR elem_name = ply.setup_element_read(i, &elem_count);
if (PLY::equal_strings(BasicPLY::elem_names[0], elem_name)) {
BasicPLY::Vertex::InitLoadProps(ply, elem_count, points, colors, normals, pointViews, pointWeights);
BasicPLY::Vertex vertex;
for (int v=0; v<elem_count; ++v) {
ply.get_element(&vertex);
points[v] = vertex.p;
if (!colors.empty())
colors[v] = vertex.c;
if (!normals.empty())
normals[v] = vertex.n;
if (!pointViews.empty()) {
ViewArr pv(vertex.views.num, vertex.views.pIndices);
pointViews[v].CopyOfRemove(pv);
}
if (!pointWeights.empty()){
WeightArr pw(vertex.views.num, vertex.views.pWeights);
pointWeights[v].CopyOfRemove(pw);
}
}
} else {
ply.get_other_element();
}
}
if (points.empty()) {
DEBUG_EXTRA("error: invalid point-cloud");
return false;
}
DEBUG_EXTRA("Point-cloud '%s' loaded: %u points (%s)", Util::getFileNameExt(fileName).c_str(), points.size(), TD_TIMER_GET_FMT().c_str());
return true;
} // Load
// save the dense point cloud as PLY file
bool PointCloud::Save(const String& fileName, bool bViews, bool bLegacyTypes, bool bBinary) const
{
if (points.empty())
return false;
TD_TIMER_STARTD();
// create PLY object
ASSERT(!fileName.empty());
Util::ensureFolder(fileName);
using namespace PointCloudInternal;
PLY ply;
if (bLegacyTypes)
ply.set_legacy_type_names();
if (!ply.write(fileName, 1, BasicPLY::elem_names, bBinary?PLY::BINARY_LE:PLY::ASCII))
return false;
// write the header
BasicPLY::Vertex::InitSaveProps(ply, (int)points.size(), !colors.empty(), !normals.empty(),
bViews && !pointViews.empty(), bViews && !pointWeights.empty());
if (!ply.header_complete())
return false;
// export the array of 3D points
BasicPLY::Vertex vertex;
FOREACH(i, points) {
// export the vertex position, color, normal and views
vertex.p = points[i];
if (!colors.empty())
vertex.c = colors[i];
if (!normals.empty())
vertex.n = normals[i];
if (!pointViews.empty()) {
vertex.views.num = pointViews[i].size();
vertex.views.pIndices = pointViews[i].data();
}
if (!pointWeights.empty()) {
ASSERT(vertex.views.num == pointWeights[i].size());
vertex.views.pWeights = pointWeights[i].data();
}
ply.put_element(&vertex);
}
ASSERT(ply.get_current_element_count() == (int)points.size());
DEBUG_EXTRA("Point-cloud '%s' saved: %u points (%s)", Util::getFileNameExt(fileName).c_str(), points.GetSize(), TD_TIMER_GET_FMT().c_str());
return true;
} // Save
// save the dense point cloud having >=N views as PLY file
bool PointCloud::SaveNViews(const String& fileName, uint32_t minViews, bool bLegacyTypes, bool bBinary) const
{
if (points.IsEmpty())
return false;
TD_TIMER_STARTD();
// create PLY object
ASSERT(!fileName.IsEmpty());
Util::ensureFolder(fileName);
using namespace PointCloudInternal;
PLY ply;
if (bLegacyTypes)
ply.set_legacy_type_names();
if (!ply.write(fileName, 1, BasicPLY::elem_names, bBinary?PLY::BINARY_LE:PLY::ASCII, 64*1024))
return false;
BasicPLY::Vertex vertex;
if (normals.IsEmpty()) {
// describe what properties go into the vertex elements
ply.describe_property(BasicPLY::elem_names[0], 6, BasicPLY::Vertex::props);
// export the array of 3D points
FOREACH(i, points) {
if (pointViews[i].size() < minViews)
continue;
// export the vertex position and color
vertex.p = points[i];
vertex.c = colors.empty() ? Pixel8U::WHITE : colors[i];
ply.put_element(&vertex);
}
} else {
// describe what properties go into the vertex elements
ply.describe_property(BasicPLY::elem_names[0], 9, BasicPLY::Vertex::props);
// export the array of 3D points
FOREACH(i, points) {
if (pointViews[i].size() < minViews)
continue;
// export the vertex position, normal and color
vertex.p = points[i];
vertex.n = normals[i];
vertex.c = colors.empty() ? Pixel8U::WHITE : colors[i];
ply.put_element(&vertex);
}
}
const int numPoints(ply.get_current_element_count());
// write the header
if (!ply.header_complete())
return false;
DEBUG_EXTRA("Point-cloud saved: %u points with at least %u views each (%s)", numPoints, minViews, TD_TIMER_GET_FMT().c_str());
return true;
} // SaveNViews
// save the dense point cloud + scale as PLY file
bool PointCloud::SaveWithScale(const String& fileName, const ImageArr& images, float scaleMult, bool bLegacyTypes, bool bBinary) const
{
if (points.empty())
return false;
TD_TIMER_STARTD();
// create PLY object
ASSERT(!fileName.empty());
Util::ensureFolder(fileName);
using namespace PointCloudInternal;
PLY ply;
if (bLegacyTypes)
ply.set_legacy_type_names();
if (!ply.write(fileName, 1, BasicPLY::elem_names, bBinary?PLY::BINARY_LE:PLY::ASCII))
return false;
// export the array of 3D points
BasicPLY::Vertex::InitSaveProps(ply, (int)points.size(), !colors.empty(), !normals.empty(), false, false, true, true);
if (!ply.header_complete())
return false;
BasicPLY::Vertex vertex;
FOREACH(i, points) {
// export the vertex position, normal and scale
vertex.p = points[i];
if (!colors.empty())
vertex.c = colors[i];
if (!normals.empty())
vertex.n = normals[i];
#if 0
// one sample per view
vertex.confidence = 1;
for (IIndex idxView: pointViews[i]) {
const float scale((float)images[idxView].camera.GetFootprintWorld(Cast<REAL>(vertex.p)));
ASSERT(scale > 0);
vertex.scale = scale*scaleMult;
ply.put_element(&vertex);
}
#else
// one sample per point
vertex.scale = FLT_MAX;
if (pointWeights.empty()) {
vertex.confidence = (float)pointViews[i].size();
for (IIndex idxView: pointViews[i]) {
const float scale((float)images[idxView].camera.GetFootprintWorld(Cast<REAL>(vertex.p)));
ASSERT(scale > 0);
if (vertex.scale > scale)
vertex.scale = scale;
}
} else {
vertex.confidence = 0;
float scaleWeightBest = FLT_MAX;
FOREACH(j, pointViews[i]) {
const IIndex idxView = pointViews[i][j];
const float scale((float)images[idxView].camera.GetFootprintWorld(Cast<REAL>(vertex.p)));
ASSERT(scale > 0);
const float conf(pointWeights[i][j]);
const float scaleWeight(scale/conf);
if (scaleWeightBest > scaleWeight) {
scaleWeightBest = scaleWeight;
vertex.scale = scale;
vertex.confidence = conf;
}
}
}
ASSERT(vertex.scale != FLT_MAX);
vertex.scale *= scaleMult;
ply.put_element(&vertex);
#endif
}
ASSERT(ply.get_current_element_count() == (int)points.size());
DEBUG_EXTRA("Point-cloud saved: %u points with scale (%s)", points.size(), TD_TIMER_GET_FMT().c_str());
return true;
} // SaveWithScale
/*----------------------------------------------------------------*/
// print various statistics about the point cloud
void PointCloud::PrintStatistics(const Image* pImages, const OBB3f* pObb) const
{
String strPoints;
if (pObb && pObb->IsValid()) {
// print points distribution
size_t nInsidePoints(0);
MeanStdMinMax<double> accInside, accOutside;
FOREACH(idx, points) {
const bool bInsideROI(pObb->Intersects(points[idx]));
if (bInsideROI)
++nInsidePoints;
if (!pointViews.empty()) {
if (bInsideROI)
accInside.Update(pointViews[idx].size());
else
accOutside.Update(pointViews[idx].size());
}
}
strPoints = String::FormatString(
"\n - points info:"
"\n\t%u points inside ROI (%.2f%%)",
nInsidePoints, 100.0*nInsidePoints/GetSize()
);
if (!pointViews.empty()) {
strPoints += String::FormatString(
"\n\t inside ROI track length: %g min / %g mean (%g std) / %g max"
"\n\toutside ROI track length: %g min / %g mean (%g std) / %g max",
accInside.minVal, accInside.GetMean(), accInside.GetStdDev(), accInside.maxVal,
accOutside.minVal, accOutside.GetMean(), accOutside.GetStdDev(), accOutside.maxVal
);
}
}
String strViews;
if (!pointViews.empty()) {
// print views distribution
size_t nViews(0);
size_t nPoints1m(0), nPoints2(0), nPoints3(0), nPoints4p(0);
size_t nPointsOpposedViews(0);
MeanStdMinMax<double> acc;
FOREACH(idx, points) {
const PointCloud::ViewArr& views = pointViews[idx];
nViews += views.size();
switch (views.size()) {
case 0:
case 1:
++nPoints1m;
break;
case 2:
++nPoints2;
break;
case 3:
++nPoints3;
break;
default:
++nPoints4p;
}
acc.Update(views.size());
}
strViews = String::FormatString(
"\n - visibility info (%u views - %.2f views/point)%s:"
"\n\t% 9u points with 1- views (%.2f%%)"
"\n\t% 9u points with 2 views (%.2f%%)"
"\n\t% 9u points with 3 views (%.2f%%)"
"\n\t% 9u points with 4+ views (%.2f%%)"
"\n\t%g min / %g mean (%g std) / %g max",
nViews, (REAL)nViews/GetSize(),
nPointsOpposedViews ? String::FormatString(" (%u (%.2f%%) points with opposed views)", nPointsOpposedViews, 100.f*nPointsOpposedViews/GetSize()).c_str() : "",
nPoints1m, 100.f*nPoints1m/GetSize(), nPoints2, 100.f*nPoints2/GetSize(), nPoints3, 100.f*nPoints3/GetSize(), nPoints4p, 100.f*nPoints4p/GetSize(),
acc.minVal, acc.GetMean(), acc.GetStdDev(), acc.maxVal
);
}
String strNormals;
if (!normals.empty()) {
if (!pointViews.empty() && pImages != NULL) {
// print normal/views angle distribution
size_t nViews(0);
size_t nPointsm(0), nPoints3(0), nPoints10(0), nPoints25(0), nPoints40(0), nPoints60(0), nPoints90p(0);
const REAL thCosAngle3(COS(D2R(3.f)));
const REAL thCosAngle10(COS(D2R(10.f)));
const REAL thCosAngle25(COS(D2R(25.f)));
const REAL thCosAngle40(COS(D2R(40.f)));
const REAL thCosAngle60(COS(D2R(60.f)));
const REAL thCosAngle90(COS(D2R(90.f)));
FOREACH(idx, points) {
const PointCloud::Point& X = points[idx];
const PointCloud::Normal& N = normals[idx];
const PointCloud::ViewArr& views = pointViews[idx];
nViews += views.size();
for (IIndex idxImage: views) {
const Point3f X2Cam(Cast<float>(pImages[idxImage].camera.C)-X);
const REAL cosAngle(ComputeAngle(X2Cam.ptr(), N.ptr()));
if (cosAngle <= thCosAngle90)
++nPoints90p;
else if (cosAngle <= thCosAngle60)
++nPoints60;
else if (cosAngle <= thCosAngle40)
++nPoints40;
else if (cosAngle <= thCosAngle25)
++nPoints25;
else if (cosAngle <= thCosAngle10)
++nPoints10;
else if (cosAngle <= thCosAngle3)
++nPoints3;
else
++nPointsm;
}
}
strNormals = String::FormatString(
"\n - normals visibility info:"
"\n\t% 9u points with 3- degrees (%.2f%%)"
"\n\t% 9u points with 10 degrees (%.2f%%)"
"\n\t% 9u points with 25 degrees (%.2f%%)"
"\n\t% 9u points with 40 degrees (%.2f%%)"
"\n\t% 9u points with 60 degrees (%.2f%%)"
"\n\t% 9u points with 90+ degrees (%.2f%%)",
nPointsm, 100.f*nPointsm/nViews, nPoints3, 100.f*nPoints3/nViews, nPoints10, 100.f*nPoints10/nViews,
nPoints40, 100.f*nPoints40/nViews, nPoints60, 100.f*nPoints60/nViews, nPoints90p, 100.f*nPoints90p/nViews
);
} else {
strNormals = "\n - normals info";
}
}
String strWeights;
if (!pointWeights.empty()) {
// print weights statistics
MeanStdMinMax<double> acc;
for (const PointCloud::WeightArr& weights: pointWeights) {
float avgWeight(0);
for (PointCloud::Weight w: weights)
avgWeight += w;
acc.Update(avgWeight/weights.size());
}
strWeights = String::FormatString(
"\n - weights info:"
"\n\t%g min / %g mean (%g std) / %g max",
acc.minVal, acc.GetMean(), acc.GetStdDev(), acc.maxVal
);
}
String strColors;
if (!colors.empty()) {
// print colors statistics
strColors = "\n - colors";
}
VERBOSE("Point-cloud composed of %u points with:%s%s%s%s",
GetSize(),
strPoints.c_str(),
strViews.c_str(),
strNormals.c_str(),
strWeights.c_str(),
strColors.c_str()
);
} // PrintStatistics
/*----------------------------------------------------------------*/