Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Use point cloud metadata statistics to pick a much better default
2d renderer for point clouds
  • Loading branch information
nyalldawson committed Dec 2, 2020
1 parent bd28407 commit e70c698
Show file tree
Hide file tree
Showing 18 changed files with 1,050 additions and 9 deletions.
Expand Up @@ -153,9 +153,9 @@ renderer was not found in the registry.
Returns a list of available renderers.
%End

static QgsPointCloudRenderer *defaultRenderer( const QgsPointCloudAttributeCollection &attributes ) /Factory/;
static QgsPointCloudRenderer *defaultRenderer( const QgsPointCloudDataProvider *provider ) /Factory/;
%Docstring
Returns a new default point cloud renderer for a layer with the specified ``attributes``.
Returns a new default point cloud renderer for a layer with the specified ``provider``.

Caller takes ownership of the returned renderer.
%End
Expand Down
4 changes: 2 additions & 2 deletions src/core/pointcloud/qgspointcloudlayer.cpp
Expand Up @@ -165,7 +165,7 @@ bool QgsPointCloudLayer::readStyle( const QDomNode &node, QString &, QgsReadWrit
// make sure layer has a renderer - if none exists, fallback to a default renderer
if ( !mRenderer )
{
setRenderer( QgsApplication::pointCloudRendererRegistry()->defaultRenderer( attributes() ) );
setRenderer( QgsApplication::pointCloudRendererRegistry()->defaultRenderer( mDataProvider.get() ) );
}
}

Expand Down Expand Up @@ -334,7 +334,7 @@ void QgsPointCloudLayer::setDataSource( const QString &dataSource, const QString
if ( !defaultLoadedFlag )
{
// all else failed, create default renderer
setRenderer( QgsApplication::pointCloudRendererRegistry()->defaultRenderer( attributes() ) );
setRenderer( QgsApplication::pointCloudRendererRegistry()->defaultRenderer( mDataProvider.get() ) );
}
}

Expand Down
59 changes: 56 additions & 3 deletions src/core/pointcloud/qgspointcloudrendererregistry.cpp
Expand Up @@ -74,12 +74,65 @@ QStringList QgsPointCloudRendererRegistry::renderersList() const
return renderers;
}

QgsPointCloudRenderer *QgsPointCloudRendererRegistry::defaultRenderer( const QgsPointCloudAttributeCollection &attributes )
QgsPointCloudRenderer *QgsPointCloudRendererRegistry::defaultRenderer( const QgsPointCloudDataProvider *provider )
{
if ( !provider )
return new QgsPointCloudAttributeByRampRenderer();

const QgsPointCloudAttributeCollection attributes = provider->attributes();

//if red/green/blue attributes are present, then default to a RGB renderer
if ( attributes.indexOf( QStringLiteral( "Red" ) ) >= 0 && attributes.indexOf( QStringLiteral( "Green" ) ) >= 0 && attributes.indexOf( QStringLiteral( "Blue" ) ) >= 0 )
return new QgsPointCloudRgbRenderer();
{
std::unique_ptr< QgsPointCloudRgbRenderer > renderer = qgis::make_unique< QgsPointCloudRgbRenderer >();

// set initial guess for rgb ranges
const QVariant redMax = provider->metadataStatistic( QStringLiteral( "Red" ), QgsStatisticalSummary::Max );
const QVariant greenMax = provider->metadataStatistic( QStringLiteral( "Red" ), QgsStatisticalSummary::Max );
const QVariant blueMax = provider->metadataStatistic( QStringLiteral( "Red" ), QgsStatisticalSummary::Max );
if ( redMax.isValid() && greenMax.isValid() && blueMax.isValid() )
{
const int maxValue = std::max( blueMax.toInt(), std::max( redMax.toInt(), greenMax.toInt() ) );
// try and guess suitable range from input max values -- we don't just take the provider max value directly here, but rather see if it's
// likely to be 8 bit or 16 bit color values
const int rangeGuess = maxValue > 255 ? 65024 : 255;

if ( rangeGuess > 255 )
{
// looks like 16 bit colors, so default to a stretch contrast enhancement
QgsContrastEnhancement contrast( Qgis::UnknownDataType );
contrast.setMinimumValue( 0 );
contrast.setMaximumValue( rangeGuess );
contrast.setContrastEnhancementAlgorithm( QgsContrastEnhancement::StretchToMinimumMaximum );
renderer->setRedContrastEnhancement( new QgsContrastEnhancement( contrast ) );
renderer->setGreenContrastEnhancement( new QgsContrastEnhancement( contrast ) );
renderer->setBlueContrastEnhancement( new QgsContrastEnhancement( contrast ) );
}
}

return new QgsPointCloudAttributeByRampRenderer();
return renderer.release();
}
else
{
// default to shading by Z
std::unique_ptr< QgsPointCloudAttributeByRampRenderer > renderer = qgis::make_unique< QgsPointCloudAttributeByRampRenderer >();
renderer->setAttribute( QStringLiteral( "Z" ) );

// set initial range for z values if possible
const QVariant zMin = provider->metadataStatistic( QStringLiteral( "Z" ), QgsStatisticalSummary::Min );
const QVariant zMax = provider->metadataStatistic( QStringLiteral( "Z" ), QgsStatisticalSummary::Max );
if ( zMin.isValid() && zMax.isValid() )
{
renderer->setMinimum( zMin.toDouble() );
renderer->setMaximum( zMax.toDouble() );

QgsColorRampShader shader = renderer->colorRampShader();
shader.setMinimumValue( zMin.toDouble() );
shader.setMaximumValue( zMax.toDouble() );
shader.classifyColorRamp( 5, -1, QgsRectangle(), nullptr );
renderer->setColorRampShader( shader );
}
return renderer.release();
}
}

5 changes: 3 additions & 2 deletions src/core/pointcloud/qgspointcloudrendererregistry.h
Expand Up @@ -31,6 +31,7 @@ class QgsPointCloudRendererWidget SIP_EXTERNAL;
#endif

class QgsPointCloudAttributeCollection;
class QgsPointCloudDataProvider;

/**
* \ingroup core
Expand Down Expand Up @@ -219,11 +220,11 @@ class CORE_EXPORT QgsPointCloudRendererRegistry
QStringList renderersList() const;

/**
* Returns a new default point cloud renderer for a layer with the specified \a attributes.
* Returns a new default point cloud renderer for a layer with the specified \a provider.
*
* Caller takes ownership of the returned renderer.
*/
static QgsPointCloudRenderer *defaultRenderer( const QgsPointCloudAttributeCollection &attributes ) SIP_FACTORY;
static QgsPointCloudRenderer *defaultRenderer( const QgsPointCloudDataProvider *provider ) SIP_FACTORY;

private:
#ifdef SIP_RUN
Expand Down
12 changes: 12 additions & 0 deletions tests/src/python/test_qgspointcloudattributebyramprenderer.py
Expand Up @@ -53,6 +53,18 @@ def tearDownClass(cls):
with open(report_file_path, 'a') as report_file:
report_file.write(cls.report)

@unittest.skipIf('ept' not in QgsProviderRegistry.instance().providerList(), 'EPT provider not available')
def testSetLayer(self):
layer = QgsPointCloudLayer(unitTestDataPath() + '/point_clouds/ept/norgb/ept.json', 'test', 'ept')
self.assertTrue(layer.isValid())

# test that a point cloud with no RGB attributes is automatically assigned the ramp renderer
self.assertIsInstance(layer.renderer(), QgsPointCloudAttributeByRampRenderer)

# check default range
self.assertAlmostEqual(layer.renderer().minimum(), -1.98, 6)
self.assertAlmostEqual(layer.renderer().maximum(), -1.92, 6)

def testBasic(self):
renderer = QgsPointCloudAttributeByRampRenderer()
renderer.setAttribute('attr')
Expand Down
24 changes: 24 additions & 0 deletions tests/src/python/test_qgspointcloudrgbrenderer.py
Expand Up @@ -60,6 +60,30 @@ def testSetLayer(self):
# test that a point cloud with RGB attributes is automatically assigned the RGB renderer by default
self.assertIsInstance(layer.renderer(), QgsPointCloudRgbRenderer)

# for this point cloud, we should default to 0-255 ranges (ie. no contrast enhancement)
self.assertIsNone(layer.renderer().redContrastEnhancement())
self.assertIsNone(layer.renderer().greenContrastEnhancement())
self.assertIsNone(layer.renderer().blueContrastEnhancement())

@unittest.skipIf('ept' not in QgsProviderRegistry.instance().providerList(), 'EPT provider not available')
def testSetLayer16(self):
layer = QgsPointCloudLayer(unitTestDataPath() + '/point_clouds/ept/rgb16/ept.json', 'test', 'ept')
self.assertTrue(layer.isValid())

# test that a point cloud with RGB attributes is automatically assigned the RGB renderer by default
self.assertIsInstance(layer.renderer(), QgsPointCloudRgbRenderer)

# for this point cloud, we should default to 0-65024 ranges with contrast enhancement
self.assertEqual(layer.renderer().redContrastEnhancement().minimumValue(), 0)
self.assertEqual(layer.renderer().redContrastEnhancement().maximumValue(), 65024.0)
self.assertEqual(layer.renderer().redContrastEnhancement().contrastEnhancementAlgorithm(), QgsContrastEnhancement.StretchToMinimumMaximum)
self.assertEqual(layer.renderer().greenContrastEnhancement().minimumValue(), 0)
self.assertEqual(layer.renderer().greenContrastEnhancement().maximumValue(), 65024.0)
self.assertEqual(layer.renderer().greenContrastEnhancement().contrastEnhancementAlgorithm(), QgsContrastEnhancement.StretchToMinimumMaximum)
self.assertEqual(layer.renderer().blueContrastEnhancement().minimumValue(), 0)
self.assertEqual(layer.renderer().blueContrastEnhancement().maximumValue(), 65024.0)
self.assertEqual(layer.renderer().blueContrastEnhancement().contrastEnhancementAlgorithm(), QgsContrastEnhancement.StretchToMinimumMaximum)

def testBasic(self):
renderer = QgsPointCloudRgbRenderer()
renderer.setBlueAttribute('b')
Expand Down
6 changes: 6 additions & 0 deletions tests/testdata/point_clouds/ept/norgb/ept-build.json
@@ -0,0 +1,6 @@
{
"maxNodeSize": 65536,
"minNodeSize": 16384,
"software": "Entwine",
"version": "2.1.0"
}
Binary file not shown.
@@ -0,0 +1,3 @@
{
"0-0-0-0": 22
}

0 comments on commit e70c698

Please sign in to comment.