Skip to content

Commit

Permalink
resolved #611
Browse files Browse the repository at this point in the history
  • Loading branch information
janzandr committed Sep 11, 2023
1 parent c4ff924 commit 5fe2d45
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 3 deletions.
3 changes: 3 additions & 0 deletions enmapboxprocessing/algorithm/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
ClassFractionFromCategorizedVectorAlgorithm
from enmapboxprocessing.algorithm.classificationfromclassprobabilityalgorithm import \
ClassificationFromClassProbabilityAlgorithm
from enmapboxprocessing.algorithm.classificationfromrenderedimagealgorithm import \
ClassificationFromRenderedImageAlgorithm
from enmapboxprocessing.algorithm.classificationperformancesimplealgorithm import \
ClassificationPerformanceSimpleAlgorithm
from enmapboxprocessing.algorithm.classificationperformancestratifiedalgorithm import \
Expand Down Expand Up @@ -238,6 +240,7 @@ def algorithms():
ClassFractionFromCategorizedVectorAlgorithm(),
CreateSpectralIndicesAlgorithm(),
ClassificationFromClassProbabilityAlgorithm(),
ClassificationFromRenderedImageAlgorithm(),
ClassificationPerformanceSimpleAlgorithm(),
ClassificationPerformanceStratifiedAlgorithm(),
ClassificationWorkflowAlgorithm(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from typing import Dict, Any, List, Tuple

import numpy as np

from enmapbox.typeguard import typechecked
from enmapboxprocessing.driver import Driver
from enmapboxprocessing.enmapalgorithm import EnMAPProcessingAlgorithm, Group
from enmapboxprocessing.typing import Category
from enmapboxprocessing.utils import Utils
from qgis.PyQt.QtGui import QColor
from qgis.core import QgsProcessingContext, QgsProcessingFeedback, QgsRasterLayer, QgsMapLayer


@typechecked
class ClassificationFromRenderedImageAlgorithm(EnMAPProcessingAlgorithm):
P_RASTER, _RASTER = 'raster', 'Raster layer'
P_OUTPUT_CLASSIFICATION, _OUTPUT_CLASSIFICATION = 'outputClassification', 'Output classification layer'

@classmethod
def displayName(cls) -> str:
return 'Classification layer from rendered image'

def shortDescription(self) -> str:
return 'Creates classification layer from a rendered image. ' \
'Classes are derived from the unique renderer RGBA values.'

def helpParameters(self) -> List[Tuple[str, str]]:
return [
(self._RASTER, 'The raster layer to be classified.'),
(self._OUTPUT_CLASSIFICATION, self.RasterFileDestination)
]

def group(self):
return Group.Classification.value

def initAlgorithm(self, configuration: Dict[str, Any] = None):
self.addParameterRasterLayer(self.P_RASTER, self._RASTER)
self.addParameterRasterDestination(self.P_OUTPUT_CLASSIFICATION, self._OUTPUT_CLASSIFICATION)

def processAlgorithm(
self, parameters: Dict[str, Any], context: QgsProcessingContext, feedback: QgsProcessingFeedback
) -> Dict[str, Any]:
raster = self.parameterAsRasterLayer(parameters, self.P_RASTER, context)
filename = self.parameterAsFileOutput(parameters, self.P_OUTPUT_CLASSIFICATION, context)

with open(filename + '.log', 'w') as logfile:
feedback, feedback2 = self.createLoggingFeedback(feedback, logfile)
self.tic(feedback, parameters, context)
feedback.pushInfo(f'Raster renderer: {raster.renderer()}')
block = raster.renderer().block(1, raster.extent(), raster.width(), raster.height())
array = Utils().qgsRasterBlockToNumpyArray(block)
values = np.unique(array)
feedback.pushInfo(f'Number of unique classes: {len(values)}')
aValues = np.bitwise_and(values >> 24, 255)
rValues = np.bitwise_and(values >> 16, 255)
gValues = np.bitwise_and(values >> 8, 255)
bValues = np.bitwise_and(values, 255)
categories = list()
for a, r, g, b, v in zip(aValues, rValues, gValues, bValues, values):
if a == 0:
array[array == v] = 0
continue
name = color = QColor(r, g, b).name()
categories.append(Category(v, name, color))

writer = Driver(filename).createFromArray([array], raster.extent(), raster.crs())
writer.setNoDataValue(0)
writer.setBandName('classification from rgb image', 1)
del writer

layer = QgsRasterLayer(filename)
renderer = Utils().palettedRasterRendererFromCategories(layer.dataProvider(), 1, categories)
layer.setRenderer(renderer)
layer.saveDefaultStyle(QgsMapLayer.StyleCategory.AllStyleCategories)

result = {self.P_OUTPUT_CLASSIFICATION: filename}
self.toc(feedback, result)

return result
10 changes: 7 additions & 3 deletions enmapboxprocessing/enmapalgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,16 @@ def parameterAsRasterLayer(
self, parameters: Dict[str, Any], name: str, context: QgsProcessingContext
) -> Optional[QgsRasterLayer]:
layer = super().parameterAsRasterLayer(parameters, name, context)

if layer is None:
return None

# if layer is given by URI string or renderer is undefined, we need to manually load the default style
if isinstance(parameters.get(name), str) or layer.renderer() is None:
layer.loadDefaultStyle()
# if layer is given by string (but not by layer ID), ...
if isinstance(parameters.get(name), str) and parameters.get(name) not in QgsProject.instance().mapLayers():
layer.loadDefaultStyle() # ... we need to manually load the default style

if layer.renderer() is None: # if we still have no valid renderer...
layer.loadDefaultStyle() # ... we also load the dafault style

return layer

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from enmapboxprocessing.algorithm.classificationfromrenderedimagealgorithm import \
ClassificationFromRenderedImageAlgorithm
from enmapboxprocessing.algorithm.testcase import TestCase
from enmapboxprocessing.utils import Utils
from enmapboxtestdata import landcover_map_l3, enmap_berlin
from qgis.core import QgsMapLayer, QgsSingleBandPseudoColorRenderer, QgsMultiBandColorRenderer, \
QgsPalettedRasterRenderer, QgsRasterLayer


class TestClassificationFromRenderedImageAlgorithm(TestCase):

def test_paletted(self):
layer = QgsRasterLayer(landcover_map_l3)
self.assertIsInstance(layer.renderer(), QgsPalettedRasterRenderer)
alg = ClassificationFromRenderedImageAlgorithm()
alg.initAlgorithm()
parameters = {
alg.P_RASTER: landcover_map_l3,
alg.P_OUTPUT_CLASSIFICATION: self.filename('classification.tif'),
}
self.runalg(alg, parameters)
layer = QgsRasterLayer(parameters[alg.P_OUTPUT_CLASSIFICATION])
categories = Utils().categoriesFromRenderer(layer.renderer())
self.assertListEqual(
['#0064ff', '#267300', '#98e600', '#9c9c9c', '#a87000', '#e60000'],
[c.name for c in categories]
)
self.assertListEqual(
['#0064ff', '#267300', '#98e600', '#9c9c9c', '#a87000', '#e60000'],
[c.color for c in categories]
)

def test_pseudocolor(self):
layer = QgsRasterLayer(enmap_berlin)
qml = enmap_berlin.replace('enmap_berlin.bsq', 'enmap_berlin_pseudocolor.qml')
layer.loadNamedStyle(qml, QgsMapLayer.StyleCategory.AllStyleCategories)
self.assertIsInstance(layer.renderer(), QgsSingleBandPseudoColorRenderer)
alg = ClassificationFromRenderedImageAlgorithm()
alg.initAlgorithm()
parameters = {
alg.P_RASTER: layer,
alg.P_OUTPUT_CLASSIFICATION: self.filename('classification.tif'),
}
self.runalg(alg, parameters)
layer = QgsRasterLayer(parameters[alg.P_OUTPUT_CLASSIFICATION])
categories = Utils().categoriesFromRenderer(layer.renderer())
self.assertListEqual(
['#1a9641', '#a6d96a', '#d7191c', '#fdae61', '#ffffc0'],
[c.name for c in categories]
)
self.assertListEqual(
['#1a9641', '#a6d96a', '#d7191c', '#fdae61', '#ffffc0'],
[c.color for c in categories]
)

def test_multiband(self):
layer = QgsRasterLayer(enmap_berlin)
self.assertIsInstance(layer.renderer(), QgsMultiBandColorRenderer)
alg = ClassificationFromRenderedImageAlgorithm()
alg.initAlgorithm()
parameters = {
alg.P_RASTER: layer,
alg.P_OUTPUT_CLASSIFICATION: self.filename('classification.tif'),
}
self.runalg(alg, parameters)
layer = QgsRasterLayer(parameters[alg.P_OUTPUT_CLASSIFICATION])
categories = Utils().categoriesFromRenderer(layer.renderer())
self.assertEqual(56498, len(categories))
1 change: 1 addition & 0 deletions tests/src/enmapboxtestdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# Potsdam example data
# ...current example dataset is placed under enmapbox.exampledata
enmap_potsdam = enmapbox.exampledata.enmap
hires_potsdom = enmapbox.exampledata.hires
landcover_potsdam_polygon = enmapbox.exampledata.landcover_polygon
landcover_potsdam_point = enmapbox.exampledata.landcover_point

Expand Down
175 changes: 175 additions & 0 deletions tests/testdata/exampledata/berlin/enmap_berlin_pseudocolor.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis maxScale="0" version="3.28.8-Firenze" styleCategories="AllStyleCategories" hasScaleBasedVisibilityFlag="0" minScale="1e+08">
<flags>
<Identifiable>1</Identifiable>
<Removable>1</Removable>
<Searchable>1</Searchable>
<Private>0</Private>
</flags>
<temporal fetchMode="0" enabled="0" mode="0">
<fixedRange>
<start></start>
<end></end>
</fixedRange>
</temporal>
<elevation enabled="0" zoffset="0" band="1" symbology="Line" zscale="1">
<data-defined-properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data-defined-properties>
<profileLineSymbol>
<symbol alpha="1" clip_to_extent="1" is_animated="0" frame_rate="10" name="" force_rhr="0" type="line">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer enabled="1" class="SimpleLine" locked="0" pass="0">
<Option type="Map">
<Option name="align_dash_pattern" type="QString" value="0"/>
<Option name="capstyle" type="QString" value="square"/>
<Option name="customdash" type="QString" value="5;2"/>
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="customdash_unit" type="QString" value="MM"/>
<Option name="dash_pattern_offset" type="QString" value="0"/>
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
<Option name="draw_inside_polygon" type="QString" value="0"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="line_color" type="QString" value="190,178,151,255"/>
<Option name="line_style" type="QString" value="solid"/>
<Option name="line_width" type="QString" value="0.6"/>
<Option name="line_width_unit" type="QString" value="MM"/>
<Option name="offset" type="QString" value="0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="ring_filter" type="QString" value="0"/>
<Option name="trim_distance_end" type="QString" value="0"/>
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
<Option name="trim_distance_start" type="QString" value="0"/>
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
<Option name="use_custom_dash" type="QString" value="0"/>
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</profileLineSymbol>
<profileFillSymbol>
<symbol alpha="1" clip_to_extent="1" is_animated="0" frame_rate="10" name="" force_rhr="0" type="fill">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer enabled="1" class="SimpleFill" locked="0" pass="0">
<Option type="Map">
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="color" type="QString" value="190,178,151,255"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255"/>
<Option name="outline_style" type="QString" value="no"/>
<Option name="outline_width" type="QString" value="0.26"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</profileFillSymbol>
</elevation>
<customproperties>
<Option type="Map">
<Option name="WMSBackgroundLayer" type="bool" value="false"/>
<Option name="WMSPublishDataSourceUrl" type="bool" value="false"/>
<Option name="embeddedWidgets/count" type="int" value="0"/>
<Option name="identify/format" type="QString" value="Value"/>
</Option>
</customproperties>
<pipe-data-defined-properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</pipe-data-defined-properties>
<pipe>
<provider>
<resampling zoomedInResamplingMethod="nearestNeighbour" enabled="false" zoomedOutResamplingMethod="nearestNeighbour" maxOversampling="2"/>
</provider>
<rasterrenderer classificationMax="3759" opacity="1" nodataColor="" band="1" alphaBand="-1" classificationMin="110" type="singlebandpseudocolor">
<rasterTransparency/>
<minMaxOrigin>
<limits>MinMax</limits>
<extent>WholeRaster</extent>
<statAccuracy>Estimated</statAccuracy>
<cumulativeCutLower>0.02</cumulativeCutLower>
<cumulativeCutUpper>0.98</cumulativeCutUpper>
<stdDevFactor>2</stdDevFactor>
</minMaxOrigin>
<rastershader>
<colorrampshader minimumValue="110" maximumValue="3759" labelPrecision="0" colorRampType="DISCRETE" clip="0" classificationMode="3">
<colorramp name="[source]" type="gradient">
<Option type="Map">
<Option name="color1" type="QString" value="215,25,28,255"/>
<Option name="color2" type="QString" value="26,150,65,255"/>
<Option name="direction" type="QString" value="ccw"/>
<Option name="discrete" type="QString" value="0"/>
<Option name="rampType" type="QString" value="gradient"/>
<Option name="spec" type="QString" value="rgb"/>
<Option name="stops" type="QString" value="0.25;253,174,97,255;rgb;ccw:0.5;255,255,192,255;rgb;ccw:0.75;166,217,106,255;rgb;ccw"/>
</Option>
</colorramp>
<item alpha="255" label="&lt;= 284" value="284" color="#d7191c"/>
<item alpha="255" label="284 - 359" value="359" color="#fdae61"/>
<item alpha="255" label="359 - 436" value="436" color="#ffffc0"/>
<item alpha="255" label="436 - 550" value="550" color="#a6d96a"/>
<item alpha="255" label="> 550" value="inf" color="#1a9641"/>
<rampLegendSettings useContinuousLegend="1" maximumLabel="" orientation="2" prefix="" direction="0" suffix="" minimumLabel="">
<numericFormat id="basic">
<Option type="Map">
<Option name="decimal_separator" type="invalid"/>
<Option name="decimals" type="int" value="6"/>
<Option name="rounding_type" type="int" value="0"/>
<Option name="show_plus" type="bool" value="false"/>
<Option name="show_thousand_separator" type="bool" value="true"/>
<Option name="show_trailing_zeros" type="bool" value="false"/>
<Option name="thousand_separator" type="invalid"/>
</Option>
</numericFormat>
</rampLegendSettings>
</colorrampshader>
</rastershader>
</rasterrenderer>
<brightnesscontrast brightness="0" gamma="1" contrast="0"/>
<huesaturation saturation="0" colorizeBlue="128" colorizeStrength="100" colorizeOn="0" grayscaleMode="0" invertColors="0" colorizeRed="255" colorizeGreen="128"/>
<rasterresampler maxOversampling="2"/>
<resamplingStage>resamplingFilter</resamplingStage>
</pipe>
<blendMode>0</blendMode>
</qgis>

0 comments on commit 5fe2d45

Please sign in to comment.