Skip to content

Commit

Permalink
Raster paletted/unique float support
Browse files Browse the repository at this point in the history
Fixes #39058 an adds some small improvements in the
progress feedback.
  • Loading branch information
elpaso authored and nyalldawson committed Oct 5, 2020
1 parent 46ce308 commit 09fe306
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 45 deletions.
Expand Up @@ -24,12 +24,12 @@ Renderer for paletted raster images.

struct Class
{
Class( int value, const QColor &color = QColor(), const QString &label = QString() );
Class( double value, const QColor &color = QColor(), const QString &label = QString() );
%Docstring
Constructor for Class
%End

int value;
double value;

QColor color;
QString label;
Expand Down Expand Up @@ -63,14 +63,14 @@ Returns number of colors
Returns a map of value to classes (colors) used by the renderer.
%End

QString label( int idx ) const;
QString label( double idx ) const;
%Docstring
Returns optional category label

.. versionadded:: 2.1
%End

void setLabel( int idx, const QString &label );
void setLabel( double idx, const QString &label );
%Docstring
Set category label

Expand Down
130 changes: 96 additions & 34 deletions src/core/raster/qgspalettedrasterrenderer.cpp
Expand Up @@ -28,6 +28,9 @@
#include <QVector>
#include <memory>


const int QgsPalettedRasterRenderer::MAX_FLOAT_CLASSES = 65536;

QgsPalettedRasterRenderer::QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber, const ClassData &classes )
: QgsRasterRenderer( input, QStringLiteral( "paletted" ) )
, mBand( bandNumber )
Expand Down Expand Up @@ -69,7 +72,7 @@ QgsRasterRenderer *QgsPalettedRasterRenderer::create( const QDomElement &elem, Q
QColor color;
QString label;
entryElem = paletteEntries.at( i ).toElement();
value = static_cast<int>( entryElem.attribute( QStringLiteral( "value" ), QStringLiteral( "0" ) ).toDouble() );
value = entryElem.attribute( QStringLiteral( "value" ), QStringLiteral( "0" ) ).toDouble();
color = QColor( entryElem.attribute( QStringLiteral( "color" ), QStringLiteral( "#000000" ) ) );
color.setAlpha( entryElem.attribute( QStringLiteral( "alpha" ), QStringLiteral( "255" ) ).toInt() );
label = entryElem.attribute( QStringLiteral( "label" ) );
Expand All @@ -96,7 +99,7 @@ QgsPalettedRasterRenderer::ClassData QgsPalettedRasterRenderer::classes() const
return mClassData;
}

QString QgsPalettedRasterRenderer::label( int idx ) const
QString QgsPalettedRasterRenderer::label( double idx ) const
{
const auto constMClassData = mClassData;
for ( const Class &c : constMClassData )
Expand All @@ -108,7 +111,7 @@ QString QgsPalettedRasterRenderer::label( int idx ) const
return QString();
}

void QgsPalettedRasterRenderer::setLabel( int idx, const QString &label )
void QgsPalettedRasterRenderer::setLabel( double idx, const QString &label )
{
ClassData::iterator cIt = mClassData.begin();
for ( ; cIt != mClassData.end(); ++cIt )
Expand Down Expand Up @@ -179,30 +182,29 @@ QgsRasterBlock *QgsPalettedRasterRenderer::block( int, QgsRectangle const &exte
outputData[i] = myDefaultColor;
continue;
}
int val = static_cast< int >( value );
if ( !mColors.contains( val ) )
if ( !mColors.contains( value ) )
{
outputData[i] = myDefaultColor;
continue;
}

if ( !hasTransparency )
{
outputData[i] = mColors.value( val );
outputData[i] = mColors.value( value );
}
else
{
currentOpacity = mOpacity;
if ( mRasterTransparency )
{
currentOpacity = mRasterTransparency->alphaValue( val, mOpacity * 255 ) / 255.0;
currentOpacity = mRasterTransparency->alphaValue( value, mOpacity * 255 ) / 255.0;
}
if ( mAlphaBand > 0 )
{
currentOpacity *= alphaBlock->value( i ) / 255.0;
}

QRgb c = mColors.value( val );
QRgb c = mColors.value( value );
outputData[i] = qRgba( currentOpacity * qRed( c ), currentOpacity * qGreen( c ), currentOpacity * qBlue( c ), currentOpacity * qAlpha( c ) );
}
}
Expand Down Expand Up @@ -472,37 +474,92 @@ QgsPalettedRasterRenderer::ClassData QgsPalettedRasterRenderer::classDataFromRas
if ( !raster )
return ClassData();

// get min and max value from raster
QgsRasterBandStats stats = raster->bandStatistics( bandNumber, QgsRasterBandStats::Min | QgsRasterBandStats::Max, QgsRectangle(), 0, feedback );
if ( feedback && feedback->isCanceled() )
return ClassData();

double min = stats.minimumValue;
double max = stats.maximumValue;
// need count of every individual value
int bins = std::ceil( max - min ) + 1;
if ( bins <= 0 )
return ClassData();

QgsRasterHistogram histogram = raster->histogram( bandNumber, bins, min, max, QgsRectangle(), 0, false, feedback );
if ( feedback && feedback->isCanceled() )
return ClassData();

double interval = ( histogram.maximum - histogram.minimum + 1 ) / histogram.binCount;

ClassData data;

double currentValue = histogram.minimum;
double presentValues = 0;
for ( int idx = 0; idx < histogram.binCount; ++idx )

// Collect unique values for float rasters
if ( raster->dataType( bandNumber ) == Qgis::DataType::Float32 || raster->dataType( bandNumber ) == Qgis::DataType::Float64 )
{
QList<double> values;
if ( feedback )
{
// Start showing some progress
feedback->setProgress( 1 );
}
std::unique_ptr<QgsRasterBlock> block { raster->block( bandNumber, raster->extent(), raster->xSize(), raster->ySize(), feedback ) };
if ( feedback && feedback->isCanceled() )
{
return data;
}
// Max MAX_FLOAT_CLASSES classes!
qgssize col = 0;
qgssize row = 0;
for ( ; row < static_cast<qgssize>( raster->ySize() ); ++row )
{
if ( presentValues >= MAX_FLOAT_CLASSES )
{
break;
}
for ( col = 0; col < static_cast<qgssize>( raster->xSize() ); ++col )
{
if ( presentValues >= MAX_FLOAT_CLASSES )
{
break;
}
if ( feedback && feedback->isCanceled() )
{
return data;
}
bool isNoData;
const double currentValue { block->valueAndNoData( row, col, isNoData ) };
if ( ! isNoData && !values.contains( currentValue ) )
{
values.push_back( currentValue );
data.push_back( Class( currentValue, QColor(), QLocale().toString( currentValue ) ) );
presentValues++;
}
}
if ( feedback )
{
// Show no less than 2%, then the max between class fill and real progress
feedback->setProgress( std::max<int>( 2, 100 * std::max<double>( ( row + 1 ) / raster->ySize(), presentValues / MAX_FLOAT_CLASSES ) ) );
}
}
if ( presentValues == MAX_FLOAT_CLASSES && ( col < static_cast<qgssize>( raster->xSize() ) || row < static_cast<qgssize>( raster->ySize() ) ) )
{
QgsDebugMsg( QStringLiteral( "Numer of classes exceeded maximum (%1)." ).arg( MAX_FLOAT_CLASSES ) );
}
}
else
{
int count = histogram.histogramVector.at( idx );
if ( count > 0 )
// get min and max value from raster
QgsRasterBandStats stats = raster->bandStatistics( bandNumber, QgsRasterBandStats::Min | QgsRasterBandStats::Max, QgsRectangle(), 0, feedback );
if ( feedback && feedback->isCanceled() )
return ClassData();

double min = stats.minimumValue;
double max = stats.maximumValue;
// need count of every individual value
int bins = std::ceil( max - min ) + 1;
if ( bins <= 0 )
return ClassData();

QgsRasterHistogram histogram = raster->histogram( bandNumber, bins, min, max, QgsRectangle(), 0, false, feedback );
if ( feedback && feedback->isCanceled() )
return ClassData();

double interval = ( histogram.maximum - histogram.minimum + 1 ) / histogram.binCount;
double currentValue = histogram.minimum;
for ( int idx = 0; idx < histogram.binCount; ++idx )
{
data << Class( currentValue, QColor(), QLocale().toString( currentValue ) );
presentValues++;
int count = histogram.histogramVector.at( idx );
if ( count > 0 )
{
data << Class( currentValue, QColor(), QLocale().toString( currentValue ) );
presentValues++;
}
currentValue += interval;
}
currentValue += interval;
}

// assign colors from ramp
Expand All @@ -523,6 +580,11 @@ QgsPalettedRasterRenderer::ClassData QgsPalettedRasterRenderer::classDataFromRas
QgsPalettedRasterRenderer::ClassData::iterator cIt = data.begin();
for ( ; cIt != data.end(); ++cIt )
{
if ( feedback )
{
// Show no less than 1%, then the max between class fill and real progress
feedback->setProgress( std::max<int>( 1, 100 * ( i + 1 ) / presentValues ) );
}
cIt->color = ramp->color( i / presentValues );
i++;
}
Expand Down
13 changes: 8 additions & 5 deletions src/core/raster/qgspalettedrasterrenderer.h
Expand Up @@ -40,14 +40,14 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer
struct Class
{
//! Constructor for Class
Class( int value, const QColor &color = QColor(), const QString &label = QString() )
Class( double value, const QColor &color = QColor(), const QString &label = QString() )
: value( value )
, color( color )
, label( label )
{}

//! Value
int value;
double value;

//! Color to render value
QColor color;
Expand Down Expand Up @@ -85,13 +85,13 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer
* Returns optional category label
* \since QGIS 2.1
*/
QString label( int idx ) const;
QString label( double idx ) const;

/**
* Set category label
* \since QGIS 2.1
*/
void setLabel( int idx, const QString &label );
void setLabel( double idx, const QString &label );

/**
* Returns the raster band used for rendering the raster.
Expand Down Expand Up @@ -169,8 +169,11 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer
std::unique_ptr<QgsColorRamp> mSourceColorRamp;

//! Premultiplied color map
QMap< int, QRgb > mColors;
QMap< double, QRgb > mColors;
void updateArrays();

// Maximum number of allowed classes for float rasters
static const int MAX_FLOAT_CLASSES;
};

#endif // QGSPALETTEDRASTERRENDERER_H
10 changes: 9 additions & 1 deletion src/gui/raster/qgspalettedrendererwidget.cpp
Expand Up @@ -428,7 +428,11 @@ void QgsPalettedRendererWidget::classify()

mGatherer = new QgsPalettedRendererClassGatherer( mRasterLayer, mBandComboBox->currentBand(), mModel->classData(), btnColorRamp->colorRamp() );

connect( mGatherer, &QgsPalettedRendererClassGatherer::progressChanged, mCalculatingProgressBar, &QProgressBar::setValue );
connect( mGatherer, &QgsPalettedRendererClassGatherer::progressChanged, mCalculatingProgressBar, [ = ]( int progress )
{
mCalculatingProgressBar->setValue( progress );
} );

mCalculatingProgressBar->show();
mCancelButton->show();
connect( mCancelButton, &QPushButton::clicked, mGatherer, &QgsPalettedRendererClassGatherer::stop );
Expand Down Expand Up @@ -828,6 +832,8 @@ void QgsPalettedRendererClassGatherer::run()

// combine existing classes with new classes
QgsPalettedRasterRenderer::ClassData::iterator classIt = newClasses.begin();
emit progressChanged( 0 );
qlonglong i = 0;
for ( ; classIt != newClasses.end(); ++classIt )
{
// check if existing classes contains this same class
Expand All @@ -840,6 +846,8 @@ void QgsPalettedRendererClassGatherer::run()
break;
}
}
i ++;
emit progressChanged( 100 * ( i / static_cast<float>( newClasses.count() ) ) );
}
mClasses = newClasses;

Expand Down
35 changes: 34 additions & 1 deletion tests/src/python/test_qgsrasterlayer.py
Expand Up @@ -14,9 +14,11 @@

import qgis # NOQA

from osgeo import gdal
import os
import filecmp
from shutil import copyfile
import numpy as np

from qgis.PyQt.QtCore import QSize, QFileInfo, Qt, QTemporaryDir

Expand All @@ -28,7 +30,8 @@
)
from qgis.PyQt.QtXml import QDomDocument

from qgis.core import (QgsRaster,
from qgis.core import (Qgis,
QgsRaster,
QgsRasterLayer,
QgsReadWriteContext,
QgsColorRampShader,
Expand Down Expand Up @@ -813,6 +816,36 @@ def testPalettedRendererWithNegativeColorValue(self):
self.assertEqual(renderer.nColors(), 2)
self.assertEqual(renderer.usesBands(), [1])

def testPalettedRendererWithFloats(self):
"""Tests for https://github.com/qgis/QGIS/issues/39058"""

tempdir = QTemporaryDir()
temppath = os.path.join(tempdir.path(), 'paletted.tif')

# Create a float raster with unique values up to 65536 + one extra row
driver = gdal.GetDriverByName('GTiff')
outRaster = driver.Create(temppath, 256, 256 + 1, 1, gdal.GDT_Float32)
outband = outRaster.GetRasterBand(1)
data = []
for r in range(256 + 1):
data.append(list(range(r * 256, (r + 1) * 256)))
npdata = np.array(data, np.float32)
outband.WriteArray(npdata)
outband.FlushCache()
outRaster.FlushCache()
del outRaster

layer = QgsRasterLayer(temppath, 'paletted')
self.assertTrue(layer.isValid())
self.assertEqual(layer.dataProvider().dataType(1), Qgis.Float32)
classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1)
# Check max classes count, hardcoded in QGIS renderer
self.assertEqual(len(classes), 65536)
class_values = []
for c in classes:
class_values.append(c.value)
self.assertEqual(sorted(class_values), list(range(65536)))

def testClone(self):
myPath = os.path.join(unitTestDataPath('raster'),
'band1_float32_noct_epsg4326.tif')
Expand Down

0 comments on commit 09fe306

Please sign in to comment.