mirror of https://github.com/django/django.git
Fixed #25734 -- Made GDALBand min and max properties use GDALComputeRasterStatistics.
Thanks Sergey Fedoseev and Tim Graham for the review.
This commit is contained in:
parent
2cb50f935a
commit
8f5904560a
|
@ -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_datatype = int_output(std_call('GDALGetRasterDataType'), [c_void_p])
|
||||||
get_band_nodata_value = double_output(std_call('GDALGetRasterNoDataValue'), [c_void_p, POINTER(c_int)])
|
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])
|
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_statistics = void_output(std_call('GDALGetRasterStatistics'),
|
||||||
get_band_maximum = double_output(std_call('GDALGetRasterMaximum'), [c_void_p, POINTER(c_int)])
|
[
|
||||||
|
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
|
# Reprojection routine
|
||||||
reproject_image = void_output(std_call('GDALReprojectImage'),
|
reproject_image = void_output(std_call('GDALReprojectImage'),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
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.base import GDALBase
|
||||||
from django.contrib.gis.gdal.error import GDALException
|
from django.contrib.gis.gdal.error import GDALException
|
||||||
|
@ -19,6 +20,14 @@ class GDALBand(GDALBase):
|
||||||
self.source = source
|
self.source = source
|
||||||
self._ptr = capi.get_ds_raster_band(source._ptr, index)
|
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
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
"""
|
"""
|
||||||
|
@ -47,19 +56,80 @@ class GDALBand(GDALBase):
|
||||||
"""
|
"""
|
||||||
return self.width * self.height
|
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
|
@property
|
||||||
def min(self):
|
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
|
@property
|
||||||
def max(self):
|
def max(self):
|
||||||
"""
|
"""
|
||||||
Returns the maximum pixel value for this band.
|
Return the maximum pixel value for this band.
|
||||||
"""
|
"""
|
||||||
return capi.get_band_maximum(self._ptr, byref(c_int()))
|
return self.statistics()[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mean(self):
|
||||||
|
"""
|
||||||
|
Return the mean of all pixel values of this band.
|
||||||
|
"""
|
||||||
|
return self.statistics()[2]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def std(self):
|
||||||
|
"""
|
||||||
|
Return the standard deviation of all pixel values of this band.
|
||||||
|
"""
|
||||||
|
return self.statistics()[3]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nodata_value(self):
|
def nodata_value(self):
|
||||||
|
@ -84,7 +154,7 @@ class GDALBand(GDALBase):
|
||||||
if not isinstance(value, (int, float)):
|
if not isinstance(value, (int, float)):
|
||||||
raise ValueError('Nodata value must be numeric.')
|
raise ValueError('Nodata value must be numeric.')
|
||||||
capi.set_band_nodata_value(self._ptr, value)
|
capi.set_band_nodata_value(self._ptr, value)
|
||||||
self.source._flush()
|
self._flush()
|
||||||
|
|
||||||
def datatype(self, as_string=False):
|
def datatype(self, as_string=False):
|
||||||
"""
|
"""
|
||||||
|
@ -149,7 +219,7 @@ class GDALBand(GDALBase):
|
||||||
else:
|
else:
|
||||||
return list(data_array)
|
return list(data_array)
|
||||||
else:
|
else:
|
||||||
self.source._flush()
|
self._flush()
|
||||||
|
|
||||||
|
|
||||||
class BandList(list):
|
class BandList(list):
|
||||||
|
|
|
@ -1413,6 +1413,35 @@ blue.
|
||||||
|
|
||||||
The total number of pixels in this band. Is equal to ``width * height``.
|
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
|
.. attribute:: min
|
||||||
|
|
||||||
The minimum pixel value of the band (excluding the "no data" value).
|
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).
|
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
|
.. attribute:: nodata_value
|
||||||
|
|
||||||
The "no data" value for a band is generally a special marker value used
|
The "no data" value for a band is generally a special marker value used
|
||||||
|
|
|
@ -74,6 +74,11 @@ Minor features
|
||||||
* Added the :meth:`GEOSGeometry.covers()
|
* Added the :meth:`GEOSGeometry.covers()
|
||||||
<django.contrib.gis.geos.GEOSGeometry.covers>` binary predicate.
|
<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`
|
:mod:`django.contrib.messages`
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -310,14 +310,34 @@ class GDALBandTests(unittest.TestCase):
|
||||||
self.band = rs.bands[0]
|
self.band = rs.bands[0]
|
||||||
|
|
||||||
def test_band_data(self):
|
def test_band_data(self):
|
||||||
|
pam_file = self.rs_path + '.aux.xml'
|
||||||
self.assertEqual(self.band.width, 163)
|
self.assertEqual(self.band.width, 163)
|
||||||
self.assertEqual(self.band.height, 174)
|
self.assertEqual(self.band.height, 174)
|
||||||
self.assertEqual(self.band.description, '')
|
self.assertEqual(self.band.description, '')
|
||||||
self.assertEqual(self.band.datatype(), 1)
|
self.assertEqual(self.band.datatype(), 1)
|
||||||
self.assertEqual(self.band.datatype(as_string=True), 'GDT_Byte')
|
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)
|
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):
|
def test_read_mode_error(self):
|
||||||
# Open raster in read mode
|
# Open raster in read mode
|
||||||
|
@ -413,3 +433,31 @@ class GDALBandTests(unittest.TestCase):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(bandmemjson.data(), list(range(25)))
|
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))
|
||||||
|
|
Loading…
Reference in New Issue