Commit 8f590456 authored by Daniel Wiesmann's avatar Daniel Wiesmann Committed by Tim Graham
Browse files

Fixed #25734 -- Made GDALBand min and max properties use GDALComputeRasterStatistics.

Thanks Sergey Fedoseev and Tim Graham for the review.
parent 2cb50f93
Loading
Loading
Loading
Loading
+11 −2
Original line number Diff line number Diff line
@@ -62,8 +62,17 @@ get_band_ds = voidptr_output(std_call('GDALGetBandDataset'), [c_void_p])
get_band_datatype = int_output(std_call('GDALGetRasterDataType'), [c_void_p])
get_band_nodata_value = double_output(std_call('GDALGetRasterNoDataValue'), [c_void_p, POINTER(c_int)])
set_band_nodata_value = void_output(std_call('GDALSetRasterNoDataValue'), [c_void_p, c_double])
get_band_minimum = double_output(std_call('GDALGetRasterMinimum'), [c_void_p, POINTER(c_int)])
get_band_maximum = double_output(std_call('GDALGetRasterMaximum'), [c_void_p, POINTER(c_int)])
get_band_statistics = void_output(std_call('GDALGetRasterStatistics'),
    [
        c_void_p, c_int, c_int, POINTER(c_double), POINTER(c_double),
        POINTER(c_double), POINTER(c_double), c_void_p, c_void_p,
    ],
    errcheck=False
)
compute_band_statistics = void_output(std_call('GDALComputeRasterStatistics'),
    [c_void_p, c_int, POINTER(c_double), POINTER(c_double), POINTER(c_double), POINTER(c_double), c_void_p, c_void_p],
    errcheck=False
)

# Reprojection routine
reproject_image = void_output(std_call('GDALReprojectImage'),
+77 −7
Original line number Diff line number Diff line
from ctypes import byref, c_int
import math
from ctypes import byref, c_double, c_int, c_void_p

from django.contrib.gis.gdal.base import GDALBase
from django.contrib.gis.gdal.error import GDALException
@@ -19,6 +20,14 @@ class GDALBand(GDALBase):
        self.source = source
        self._ptr = capi.get_ds_raster_band(source._ptr, index)

    def _flush(self):
        """
        Call the flush method on the Band's parent raster and force a refresh
        of the statistics attribute when requested the next time.
        """
        self.source._flush()
        self._stats_refresh = True

    @property
    def description(self):
        """
@@ -47,19 +56,80 @@ class GDALBand(GDALBase):
        """
        return self.width * self.height

    _stats_refresh = False

    def statistics(self, refresh=False, approximate=False):
        """
        Compute statistics on the pixel values of this band.

        The return value is a tuple with the following structure:
        (minimum, maximum, mean, standard deviation).

        If approximate=True, the statistics may be computed based on overviews
        or a subset of image tiles.

        If refresh=True, the statistics will be computed from the data directly,
        and the cache will be updated where applicable.

        For empty bands (where all pixel values are nodata), all statistics
        values are returned as None.

        For raster formats using Persistent Auxiliary Metadata (PAM) services,
        the statistics might be cached in an auxiliary file.
        """
        # Prepare array with arguments for capi function
        smin, smax, smean, sstd = c_double(), c_double(), c_double(), c_double()
        stats_args = [
            self._ptr, c_int(approximate), byref(smin), byref(smax),
            byref(smean), byref(sstd), c_void_p(), c_void_p(),
        ]

        if refresh or self._stats_refresh:
            capi.compute_band_statistics(*stats_args)
        else:
            # Add additional argument to force computation if there is no
            # existing PAM file to take the values from.
            force = True
            stats_args.insert(2, c_int(force))
            capi.get_band_statistics(*stats_args)

        result = smin.value, smax.value, smean.value, sstd.value

        # Check if band is empty (in that case, set all statistics to None)
        if any((math.isnan(val) for val in result)):
            result = (None, None, None, None)

        self._stats_refresh = False

        return result

    @property
    def min(self):
        """
        Returns the minimum pixel value for this band.
        Return the minimum pixel value for this band.
        """
        return capi.get_band_minimum(self._ptr, byref(c_int()))
        return self.statistics()[0]

    @property
    def max(self):
        """
        Returns the maximum pixel value for this band.
        Return the maximum pixel value for this band.
        """
        return self.statistics()[1]

    @property
    def mean(self):
        """
        Return the mean of all pixel values of this band.
        """
        return capi.get_band_maximum(self._ptr, byref(c_int()))
        return self.statistics()[2]

    @property
    def std(self):
        """
        Return the standard deviation of all pixel values of this band.
        """
        return self.statistics()[3]

    @property
    def nodata_value(self):
@@ -84,7 +154,7 @@ class GDALBand(GDALBase):
        if not isinstance(value, (int, float)):
            raise ValueError('Nodata value must be numeric.')
        capi.set_band_nodata_value(self._ptr, value)
        self.source._flush()
        self._flush()

    def datatype(self, as_string=False):
        """
@@ -149,7 +219,7 @@ class GDALBand(GDALBase):
            else:
                return list(data_array)
        else:
            self.source._flush()
            self._flush()


class BandList(list):
+43 −0
Original line number Diff line number Diff line
@@ -1413,6 +1413,35 @@ blue.

        The total number of pixels in this band. Is equal to ``width * height``.

    .. method:: statistics(refresh=False, approximate=False)

        .. versionadded:: 1.10

        Compute statistics on the pixel values of this band. The return value
        is a tuple with the following structure:
        ``(minimum, maximum, mean, standard deviation)``.

        If the ``approximate`` argument is set to ``True``, the statistics may
        be computed based on overviews or a subset of image tiles.

        If the ``refresh`` argument is set to ``True``, the statistics will be
        computed from the data directly, and the cache will be updated with the
        result.

        If a persistent cache value is found, that value is returned. For
        raster formats using Persistent Auxiliary Metadata (PAM) services, the
        statistics might be cached in an auxiliary file. In some cases this
        metadata might be out of sync with the pixel values or cause values
        from a previous call to be returned which don't reflect the value of
        the ``approximate`` argument. In such cases, use the ``refresh``
        argument to get updated values and store them in the cache.

        For empty bands (where all pixel values are "no data"), all statistics
        are returned as ``None``.

        The statistics can also be retrieved directly by accessing the
        :attr:`min`, :attr:`max`, :attr:`mean`, and :attr:`std` properties.

    .. attribute:: min

        The minimum pixel value of the band (excluding the "no data" value).
@@ -1421,6 +1450,20 @@ blue.

        The maximum pixel value of the band (excluding the "no data" value).

    .. attribute:: mean

        .. versionadded:: 1.10

        The mean of all pixel values of the band (excluding the "no data"
        value).

    .. attribute:: std

        .. versionadded:: 1.10

        The standard deviation of all pixel values of the band (excluding the
        "no data" value).

    .. attribute:: nodata_value

        The "no data" value for a band is generally a special marker value used
+5 −0
Original line number Diff line number Diff line
@@ -74,6 +74,11 @@ Minor features
* Added the :meth:`GEOSGeometry.covers()
  <django.contrib.gis.geos.GEOSGeometry.covers>` binary predicate.

* Added the :meth:`GDALBand.statistics()
  <django.contrib.gis.gdal.GDALBand.statistics>` method and
  :attr:`~django.contrib.gis.gdal.GDALBand.mean`
  and :attr:`~django.contrib.gis.gdal.GDALBand.std` attributes.

:mod:`django.contrib.messages`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

+50 −2
Original line number Diff line number Diff line
@@ -310,14 +310,34 @@ class GDALBandTests(unittest.TestCase):
        self.band = rs.bands[0]

    def test_band_data(self):
        pam_file = self.rs_path + '.aux.xml'
        self.assertEqual(self.band.width, 163)
        self.assertEqual(self.band.height, 174)
        self.assertEqual(self.band.description, '')
        self.assertEqual(self.band.datatype(), 1)
        self.assertEqual(self.band.datatype(as_string=True), 'GDT_Byte')
        self.assertEqual(self.band.min, 0)
        self.assertEqual(self.band.max, 255)
        self.assertEqual(self.band.nodata_value, 15)
        try:
            self.assertEqual(
                self.band.statistics(approximate=True),
                (0.0, 9.0, 2.842331288343558, 2.3965567248965356)
            )
            self.assertEqual(
                self.band.statistics(approximate=False, refresh=True),
                (0.0, 9.0, 2.828326634228898, 2.4260526986669095)
            )
            self.assertEqual(self.band.min, 0)
            self.assertEqual(self.band.max, 9)
            self.assertEqual(self.band.mean, 2.8283266342289)
            self.assertEqual(self.band.std, 2.4260526986669)
            # Check that statistics are persisted into PAM file on band close
            self.band = None
            self.assertTrue(os.path.isfile(pam_file))
        finally:
            # Close band and remove file if created
            self.band = None
            if os.path.isfile(pam_file):
                os.remove(pam_file)

    def test_read_mode_error(self):
        # Open raster in read mode
@@ -413,3 +433,31 @@ class GDALBandTests(unittest.TestCase):
            )
        else:
            self.assertEqual(bandmemjson.data(), list(range(25)))

    def test_band_statistics_automatic_refresh(self):
        rsmem = GDALRaster({
            'srid': 4326,
            'width': 2,
            'height': 2,
            'bands': [{'data': [0] * 4, 'nodata_value': 99}],
        })
        band = rsmem.bands[0]
        # Populate statistics cache
        self.assertEqual(band.statistics(), (0, 0, 0, 0))
        # Change data
        band.data([1, 1, 0, 0])
        # Statistics are properly updated
        self.assertEqual(band.statistics(), (0.0, 1.0, 0.5, 0.5))
        # Change nodata_value
        band.nodata_value = 0
        # Statistics are properly updated
        self.assertEqual(band.statistics(), (1.0, 1.0, 1.0, 0.0))

    def test_band_statistics_empty_band(self):
        rsmem = GDALRaster({
            'srid': 4326,
            'width': 1,
            'height': 1,
            'bands': [{'data': [0], 'nodata_value': 0}],
        })
        self.assertEqual(rsmem.bands[0].statistics(), (None, None, None, None))