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
# 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.__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.RasterRendererFlag.baseClass = Qgis

View File

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

View File

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

View File

@ -1247,7 +1247,8 @@ Qgis.RasterResamplingStage.__doc__ = "Stage at which raster resampling occurs.\n
Qgis.RasterResamplingStage.baseClass = Qgis
# 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.__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.RasterRendererFlags.baseClass = Qgis

View File

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

View File

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

View File

@ -1151,6 +1151,7 @@ class CORE_EXPORT Qgis
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.
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;
}
Qgis::RasterRendererFlags QgsRasterContourRenderer::flags() const
{
return Qgis::RasterRendererFlag::UseNoDataForOutOfRangePixels;
}
QgsRasterRenderer *QgsRasterContourRenderer::create( const QDomElement &elem, QgsRasterInterface *input )
{
if ( elem.isNull() )

View File

@ -40,6 +40,7 @@ class CORE_EXPORT QgsRasterContourRenderer : public QgsRasterRenderer
const QgsRasterContourRenderer &operator=( const QgsRasterContourRenderer & ) = delete;
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)
static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY;

View File

@ -37,6 +37,7 @@
#include "qgsrasterlayerutils.h"
#include "qgsinterval.h"
#include "qgsunittypes.h"
#include "qgsrasternuller.h"
#include <QElapsedTimer>
#include <QPointer>
@ -354,6 +355,7 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender
if ( !rendererContext.zRange().isInfinite() )
{
// NOLINTBEGIN(bugprone-branch-clone)
switch ( elevationProperties->mode() )
{
case Qgis::RasterElevationMode::FixedElevationRange:
@ -371,28 +373,54 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender
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 ( 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;
if ( const QgsRasterTransparency *rendererTransparency = mPipe->renderer()->rasterTransparency() )
transparency = std::make_unique< QgsRasterTransparency >( *rendererTransparency );
// account for z offset/zscale by reversing these calculations, so that we get the z range in
// raw pixel values
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
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
// raw pixel values
const double adjustedLower = ( rendererContext.zRange().lower() - 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( adjustedUpper, std::numeric_limits<double>::max(), 0, !rendererContext.zRange().includeUpper(), true ) );
// account for z offset/zscale by reversing these calculations, so that we get the z range in
// raw pixel values
const double adjustedLower = ( rendererContext.zRange().lower() - 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( adjustedUpper, std::numeric_limits<double>::max(), 0, !rendererContext.zRange().includeUpper(), true ) );
transparency->setTransparentSingleValuePixelList( transparentPixels );
mPipe->renderer()->setRasterTransparency( transparency.release() );
transparency->setTransparentSingleValuePixelList( transparentPixels );
mPipe->renderer()->setRasterTransparency( transparency.release() );
}
}
break;
}
}
// NOLINTEND(bugprone-branch-clone)
}
}

View File

@ -28,10 +28,12 @@ from qgis.core import (
QgsRectangle,
QgsDoubleRange,
QgsSingleBandGrayRenderer,
QgsRasterContourRenderer,
QgsContrastEnhancement,
QgsRasterLayerElevationProperties,
QgsProperty,
QgsDateTimeRange
QgsDateTimeRange,
QgsLineSymbol
)
import unittest
from qgis.testing import start_app, QgisTestCase
@ -127,6 +129,46 @@ class TestQgsRasterLayerRenderer(QgisTestCase):
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):
"""
Test rendering a raster with a fixed elevation range when

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB