Ensure raster elevation filtering works nicely with contour renderer

With the contour renderer we must treat out of range pixels as
no data values, so that the gdal contouring algorithm correctly
ignores them
This commit is contained in:
Nyall Dawson 2024-04-08 09:28:51 +10:00
parent b5a8722446
commit 365d26ece4
13 changed files with 101 additions and 16 deletions

View File

@ -1272,7 +1272,8 @@ Qgis.RasterResamplingStage.__doc__ = "Stage at which raster resampling occurs.\n
Qgis.RasterResamplingStage.baseClass = Qgis Qgis.RasterResamplingStage.baseClass = Qgis
# monkey patching scoped based enum # monkey patching scoped based enum
Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ = "The renderer internally handles the raster layer's opacity, so the default layer level opacity handling should not be applied." Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ = "The renderer internally handles the raster layer's opacity, so the default layer level opacity handling should not be applied."
Qgis.RasterRendererFlag.__doc__ = "Flags which control behavior of raster renderers.\n\n.. versionadded:: 3.28\n\n" + '* ``InternalLayerOpacityHandling``: ' + Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ Qgis.RasterRendererFlag.UseNoDataForOutOfRangePixels.__doc__ = "Out of range pixels (eg those values outside of the rendered map's z range filter) should be set using additional nodata values instead of additional transparency values (since QGIS 3.38)"
Qgis.RasterRendererFlag.__doc__ = "Flags which control behavior of raster renderers.\n\n.. versionadded:: 3.28\n\n" + '* ``InternalLayerOpacityHandling``: ' + Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ + '\n' + '* ``UseNoDataForOutOfRangePixels``: ' + Qgis.RasterRendererFlag.UseNoDataForOutOfRangePixels.__doc__
# -- # --
Qgis.RasterRendererFlags = lambda flags=0: Qgis.RasterRendererFlag(flags) Qgis.RasterRendererFlags = lambda flags=0: Qgis.RasterRendererFlag(flags)
Qgis.RasterRendererFlag.baseClass = Qgis Qgis.RasterRendererFlag.baseClass = Qgis

View File

@ -685,6 +685,7 @@ The development version
enum class RasterRendererFlag /BaseType=IntFlag/ enum class RasterRendererFlag /BaseType=IntFlag/
{ {
InternalLayerOpacityHandling, InternalLayerOpacityHandling,
UseNoDataForOutOfRangePixels,
}; };
typedef QFlags<Qgis::RasterRendererFlag> RasterRendererFlags; typedef QFlags<Qgis::RasterRendererFlag> RasterRendererFlags;

View File

@ -34,6 +34,8 @@ Creates a contour renderer
%Docstring %Docstring
QgsRasterContourRenderer cannot be copied. Use :py:func:`~QgsRasterContourRenderer.clone` instead. QgsRasterContourRenderer cannot be copied. Use :py:func:`~QgsRasterContourRenderer.clone` instead.
%End %End
virtual Qgis::RasterRendererFlags flags() const;
static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/; static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/;
%Docstring %Docstring

View File

@ -1247,7 +1247,8 @@ Qgis.RasterResamplingStage.__doc__ = "Stage at which raster resampling occurs.\n
Qgis.RasterResamplingStage.baseClass = Qgis Qgis.RasterResamplingStage.baseClass = Qgis
# monkey patching scoped based enum # monkey patching scoped based enum
Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ = "The renderer internally handles the raster layer's opacity, so the default layer level opacity handling should not be applied." Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ = "The renderer internally handles the raster layer's opacity, so the default layer level opacity handling should not be applied."
Qgis.RasterRendererFlag.__doc__ = "Flags which control behavior of raster renderers.\n\n.. versionadded:: 3.28\n\n" + '* ``InternalLayerOpacityHandling``: ' + Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ Qgis.RasterRendererFlag.UseNoDataForOutOfRangePixels.__doc__ = "Out of range pixels (eg those values outside of the rendered map's z range filter) should be set using additional nodata values instead of additional transparency values (since QGIS 3.38)"
Qgis.RasterRendererFlag.__doc__ = "Flags which control behavior of raster renderers.\n\n.. versionadded:: 3.28\n\n" + '* ``InternalLayerOpacityHandling``: ' + Qgis.RasterRendererFlag.InternalLayerOpacityHandling.__doc__ + '\n' + '* ``UseNoDataForOutOfRangePixels``: ' + Qgis.RasterRendererFlag.UseNoDataForOutOfRangePixels.__doc__
# -- # --
Qgis.RasterRendererFlag.baseClass = Qgis Qgis.RasterRendererFlag.baseClass = Qgis
Qgis.RasterRendererFlags.baseClass = Qgis Qgis.RasterRendererFlags.baseClass = Qgis

View File

@ -685,6 +685,7 @@ The development version
enum class RasterRendererFlag enum class RasterRendererFlag
{ {
InternalLayerOpacityHandling, InternalLayerOpacityHandling,
UseNoDataForOutOfRangePixels,
}; };
typedef QFlags<Qgis::RasterRendererFlag> RasterRendererFlags; typedef QFlags<Qgis::RasterRendererFlag> RasterRendererFlags;

View File

@ -34,6 +34,8 @@ Creates a contour renderer
%Docstring %Docstring
QgsRasterContourRenderer cannot be copied. Use :py:func:`~QgsRasterContourRenderer.clone` instead. QgsRasterContourRenderer cannot be copied. Use :py:func:`~QgsRasterContourRenderer.clone` instead.
%End %End
virtual Qgis::RasterRendererFlags flags() const;
static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/; static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/;
%Docstring %Docstring

View File

@ -1151,6 +1151,7 @@ class CORE_EXPORT Qgis
enum class RasterRendererFlag : int SIP_ENUM_BASETYPE( IntFlag ) enum class RasterRendererFlag : int SIP_ENUM_BASETYPE( IntFlag )
{ {
InternalLayerOpacityHandling = 1 << 0, //!< The renderer internally handles the raster layer's opacity, so the default layer level opacity handling should not be applied. InternalLayerOpacityHandling = 1 << 0, //!< The renderer internally handles the raster layer's opacity, so the default layer level opacity handling should not be applied.
UseNoDataForOutOfRangePixels = 1 << 1, //!< Out of range pixels (eg those values outside of the rendered map's z range filter) should be set using additional nodata values instead of additional transparency values (since QGIS 3.38)
}; };
/** /**

View File

@ -43,6 +43,11 @@ QgsRasterContourRenderer *QgsRasterContourRenderer::clone() const
return renderer; return renderer;
} }
Qgis::RasterRendererFlags QgsRasterContourRenderer::flags() const
{
return Qgis::RasterRendererFlag::UseNoDataForOutOfRangePixels;
}
QgsRasterRenderer *QgsRasterContourRenderer::create( const QDomElement &elem, QgsRasterInterface *input ) QgsRasterRenderer *QgsRasterContourRenderer::create( const QDomElement &elem, QgsRasterInterface *input )
{ {
if ( elem.isNull() ) if ( elem.isNull() )

View File

@ -40,6 +40,7 @@ class CORE_EXPORT QgsRasterContourRenderer : public QgsRasterRenderer
const QgsRasterContourRenderer &operator=( const QgsRasterContourRenderer & ) = delete; const QgsRasterContourRenderer &operator=( const QgsRasterContourRenderer & ) = delete;
QgsRasterContourRenderer *clone() const override SIP_FACTORY; QgsRasterContourRenderer *clone() const override SIP_FACTORY;
Qgis::RasterRendererFlags flags() const override;
//! Creates an instance of the renderer based on definition from XML (used by renderer registry) //! Creates an instance of the renderer based on definition from XML (used by renderer registry)
static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY; static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY;

View File

@ -37,6 +37,7 @@
#include "qgsrasterlayerutils.h" #include "qgsrasterlayerutils.h"
#include "qgsinterval.h" #include "qgsinterval.h"
#include "qgsunittypes.h" #include "qgsunittypes.h"
#include "qgsrasternuller.h"
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QPointer> #include <QPointer>
@ -354,6 +355,7 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender
if ( !rendererContext.zRange().isInfinite() ) if ( !rendererContext.zRange().isInfinite() )
{ {
// NOLINTBEGIN(bugprone-branch-clone)
switch ( elevationProperties->mode() ) switch ( elevationProperties->mode() )
{ {
case Qgis::RasterElevationMode::FixedElevationRange: case Qgis::RasterElevationMode::FixedElevationRange:
@ -371,28 +373,54 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender
if ( mPipe->renderer()->usesBands().contains( mElevationBand ) ) if ( mPipe->renderer()->usesBands().contains( mElevationBand ) )
{ {
// if layer has elevation settings and we are only rendering a slice of z values => we need to filter pixels by elevation // if layer has elevation settings and we are only rendering a slice of z values => we need to filter pixels by elevation
if ( mPipe->renderer()->flags() & Qgis::RasterRendererFlag::UseNoDataForOutOfRangePixels )
{
std::unique_ptr< QgsRasterNuller> nuller;
if ( const QgsRasterNuller *existingNuller = mPipe->nuller() )
nuller.reset( existingNuller->clone() );
else
nuller = std::make_unique< QgsRasterNuller >();
std::unique_ptr< QgsRasterTransparency > transparency; // account for z offset/zscale by reversing these calculations, so that we get the z range in
if ( const QgsRasterTransparency *rendererTransparency = mPipe->renderer()->rasterTransparency() ) // raw pixel values
transparency = std::make_unique< QgsRasterTransparency >( *rendererTransparency ); QgsRasterRangeList nullRanges;
const double adjustedLower = ( rendererContext.zRange().lower() - mElevationOffset ) / mElevationScale;
const double adjustedUpper = ( rendererContext.zRange().upper() - mElevationOffset ) / mElevationScale;
nullRanges.append( QgsRasterRange( std::numeric_limits<double>::lowest(), adjustedLower, rendererContext.zRange().includeLower() ? QgsRasterRange::BoundsType::IncludeMin : QgsRasterRange::BoundsType::IncludeMinAndMax ) );
nullRanges.append( QgsRasterRange( adjustedUpper, std::numeric_limits<double>::max(), rendererContext.zRange().includeUpper() ? QgsRasterRange::BoundsType::IncludeMax : QgsRasterRange::BoundsType::IncludeMinAndMax ) );
nuller->setOutputNoDataValue( mElevationBand, static_cast< int >( adjustedLower - 1 ) );
nuller->setNoData( mElevationBand, nullRanges );
if ( !mPipe->insert( 1, nuller.release() ) )
{
QgsDebugError( QStringLiteral( "Cannot set pipe nuller" ) );
}
}
else else
transparency = std::make_unique< QgsRasterTransparency >(); {
std::unique_ptr< QgsRasterTransparency > transparency;
if ( const QgsRasterTransparency *rendererTransparency = mPipe->renderer()->rasterTransparency() )
transparency = std::make_unique< QgsRasterTransparency >( *rendererTransparency );
else
transparency = std::make_unique< QgsRasterTransparency >();
QVector<QgsRasterTransparency::TransparentSingleValuePixel> transparentPixels = transparency->transparentSingleValuePixelList(); QVector<QgsRasterTransparency::TransparentSingleValuePixel> transparentPixels = transparency->transparentSingleValuePixelList();
// account for z offset/zscale by reversing these calculations, so that we get the z range in // account for z offset/zscale by reversing these calculations, so that we get the z range in
// raw pixel values // raw pixel values
const double adjustedLower = ( rendererContext.zRange().lower() - mElevationOffset ) / mElevationScale; const double adjustedLower = ( rendererContext.zRange().lower() - mElevationOffset ) / mElevationScale;
const double adjustedUpper = ( rendererContext.zRange().upper() - mElevationOffset ) / mElevationScale; const double adjustedUpper = ( rendererContext.zRange().upper() - mElevationOffset ) / mElevationScale;
transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( std::numeric_limits<double>::lowest(), adjustedLower, 0, true, !rendererContext.zRange().includeLower() ) ); transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( std::numeric_limits<double>::lowest(), adjustedLower, 0, true, !rendererContext.zRange().includeLower() ) );
transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( adjustedUpper, std::numeric_limits<double>::max(), 0, !rendererContext.zRange().includeUpper(), true ) ); transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( adjustedUpper, std::numeric_limits<double>::max(), 0, !rendererContext.zRange().includeUpper(), true ) );
transparency->setTransparentSingleValuePixelList( transparentPixels ); transparency->setTransparentSingleValuePixelList( transparentPixels );
mPipe->renderer()->setRasterTransparency( transparency.release() ); mPipe->renderer()->setRasterTransparency( transparency.release() );
}
} }
break; break;
} }
} }
// NOLINTEND(bugprone-branch-clone)
} }
} }

View File

@ -28,10 +28,12 @@ from qgis.core import (
QgsRectangle, QgsRectangle,
QgsDoubleRange, QgsDoubleRange,
QgsSingleBandGrayRenderer, QgsSingleBandGrayRenderer,
QgsRasterContourRenderer,
QgsContrastEnhancement, QgsContrastEnhancement,
QgsRasterLayerElevationProperties, QgsRasterLayerElevationProperties,
QgsProperty, QgsProperty,
QgsDateTimeRange QgsDateTimeRange,
QgsLineSymbol
) )
import unittest import unittest
from qgis.testing import start_app, QgisTestCase from qgis.testing import start_app, QgisTestCase
@ -127,6 +129,46 @@ class TestQgsRasterLayerRenderer(QgisTestCase):
map_settings) map_settings)
) )
def test_contour_render_dem_with_z_range_filter(self):
raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, '3d', 'dtm.tif'))
renderer = QgsRasterContourRenderer(raster_layer.dataProvider())
renderer.setContourInterval(10)
renderer.setContourSymbol(
QgsLineSymbol.createSimple({'width': 1})
)
renderer.setContourIndexInterval(0)
raster_layer.setRenderer(renderer)
self.assertTrue(raster_layer.isValid())
map_settings = QgsMapSettings()
map_settings.setOutputSize(QSize(400, 400))
map_settings.setOutputDpi(96)
map_settings.setDestinationCrs(raster_layer.crs())
map_settings.setExtent(raster_layer.extent())
map_settings.setLayers([raster_layer])
map_settings.setZRange(QgsDoubleRange(96, 132))
# set layer as elevation enabled
raster_layer.elevationProperties().setEnabled(True)
self.assertTrue(
self.render_map_settings_check(
'Z range filter on map settings, contour renderer',
'dem_filter_contour',
map_settings)
)
# with offset and scaling
raster_layer.elevationProperties().setZOffset(50)
raster_layer.elevationProperties().setZScale(0.75)
self.assertTrue(
self.render_map_settings_check(
'Z range filter on map settings, contour renderer, elevation enabled layer with offset and scale',
'dem_filter_contour_offset_and_scale',
map_settings)
)
def test_render_fixed_elevation_range_with_z_range_filter(self): def test_render_fixed_elevation_range_with_z_range_filter(self):
""" """
Test rendering a raster with a fixed elevation range when Test rendering a raster with a fixed elevation range when

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB