0
0
mirror of https://github.com/django/django.git synced 2024-12-01 15:42:04 +01:00

Added write support for GDALRaster

- Instantiation of GDALRaster instances from dict or json data.
- Retrieve and write pixel values in GDALBand objects.
- Support for the GDALFlushCache in gdal C prototypes
- Added private flush method to GDALRaster to make sure all
  data is written to files when file-based rasters are changed.
- Replaced ``ptr`` with ``_ptr`` for internal ptr variable

Refs #23804. Thanks Claude Paroz and Tim Graham for the reviews.
This commit is contained in:
Daniel Wiesmann 2015-03-13 18:49:02 +00:00 committed by Claude Paroz
parent 8758a63ddb
commit f269c1d6f6
11 changed files with 600 additions and 57 deletions

View File

@ -31,6 +31,7 @@ get_driver_description = const_string_output(lgdal.GDALGetDescription, [c_void_p
create_ds = voidptr_output(lgdal.GDALCreate, [c_void_p, c_char_p, c_int, c_int, c_int, c_int])
open_ds = voidptr_output(lgdal.GDALOpen, [c_char_p, c_int])
close_ds = void_output(lgdal.GDALClose, [c_void_p])
flush_ds = int_output(lgdal.GDALFlushCache, [c_void_p])
copy_ds = voidptr_output(lgdal.GDALCreateCopy, [c_void_p, c_char_p, c_void_p, c_int,
POINTER(c_char_p), c_void_p, c_void_p])
add_band_ds = void_output(lgdal.GDALAddBand, [c_void_p, c_int])

View File

@ -2,9 +2,11 @@ from ctypes import byref, c_int
from django.contrib.gis.gdal.base import GDALBase
from django.contrib.gis.gdal.prototypes import raster as capi
from django.contrib.gis.shortcuts import numpy
from django.utils import six
from django.utils.encoding import force_text
from .const import GDAL_PIXEL_TYPES
from .const import GDAL_PIXEL_TYPES, GDAL_TO_CTYPES
class GDALBand(GDALBase):
@ -13,51 +15,49 @@ class GDALBand(GDALBase):
"""
def __init__(self, source, index):
self.source = source
self.ptr = capi.get_ds_raster_band(source.ptr, index)
self._ptr = capi.get_ds_raster_band(source._ptr, index)
@property
def description(self):
"""
Returns the description string of the band.
"""
return force_text(capi.get_band_description(self.ptr))
return force_text(capi.get_band_description(self._ptr))
@property
def width(self):
"""
Width (X axis) in pixels of the band.
"""
return capi.get_band_xsize(self.ptr)
return capi.get_band_xsize(self._ptr)
@property
def height(self):
"""
Height (Y axis) in pixels of the band.
"""
return capi.get_band_ysize(self.ptr)
return capi.get_band_ysize(self._ptr)
def datatype(self, as_string=False):
@property
def pixel_count(self):
"""
Returns the GDAL Pixel Datatype for this band.
Returns the total number of pixels in this band.
"""
dtype = capi.get_band_datatype(self.ptr)
if as_string:
dtype = GDAL_PIXEL_TYPES[dtype]
return dtype
return self.width * self.height
@property
def min(self):
"""
Returns the minimum pixel value for this band.
"""
return capi.get_band_minimum(self.ptr, byref(c_int()))
return capi.get_band_minimum(self._ptr, byref(c_int()))
@property
def max(self):
"""
Returns the maximum pixel value for this band.
"""
return capi.get_band_maximum(self.ptr, byref(c_int()))
return capi.get_band_maximum(self._ptr, byref(c_int()))
@property
def nodata_value(self):
@ -65,5 +65,80 @@ class GDALBand(GDALBase):
Returns the nodata value for this band, or None if it isn't set.
"""
nodata_exists = c_int()
value = capi.get_band_nodata_value(self.ptr, nodata_exists)
value = capi.get_band_nodata_value(self._ptr, nodata_exists)
return value if nodata_exists else None
@nodata_value.setter
def nodata_value(self, value):
"""
Sets the nodata value for this band.
"""
if not isinstance(value, (int, float)):
raise ValueError('Nodata value must be numeric.')
capi.set_band_nodata_value(self._ptr, value)
self.source._flush()
def datatype(self, as_string=False):
"""
Returns the GDAL Pixel Datatype for this band.
"""
dtype = capi.get_band_datatype(self._ptr)
if as_string:
dtype = GDAL_PIXEL_TYPES[dtype]
return dtype
def data(self, data=None, offset=None, size=None, as_memoryview=False):
"""
Reads or writes pixel values for this band. Blocks of data can
be accessed by specifying the width, height and offset of the
desired block. The same specification can be used to update
parts of a raster by providing an array of values.
Allowed input data types are bytes, memoryview, list, tuple, and array.
"""
if not offset:
offset = (0, 0)
if not size:
size = (self.width - offset[0], self.height - offset[1])
if any(x <= 0 for x in size):
raise ValueError('Offset too big for this raster.')
if size[0] > self.width or size[1] > self.height:
raise ValueError('Size is larger than raster.')
# Create ctypes type array generator
ctypes_array = GDAL_TO_CTYPES[self.datatype()] * (size[0] * size[1])
if data is None:
# Set read mode
access_flag = 0
# Prepare empty ctypes array
data_array = ctypes_array()
else:
# Set write mode
access_flag = 1
# Instantiate ctypes array holding the input data
if isinstance(data, (bytes, six.memoryview, numpy.ndarray)):
data_array = ctypes_array.from_buffer_copy(data)
else:
data_array = ctypes_array(*data)
# Access band
capi.band_io(self._ptr, access_flag, offset[0], offset[1],
size[0], size[1], byref(data_array), size[0],
size[1], self.datatype(), 0, 0)
# Return data as numpy array if possible, otherwise as list
if data is None:
if as_memoryview:
return memoryview(data_array)
elif numpy:
return numpy.frombuffer(
data_array, dtype=numpy.dtype(data_array)).reshape(size)
else:
return list(data_array)
else:
self.source._flush()

View File

@ -1,6 +1,9 @@
"""
GDAL - Constant definitions
"""
from ctypes import (
c_byte, c_double, c_float, c_int16, c_int32, c_uint16, c_uint32,
)
# See http://www.gdal.org/gdal_8h.html#a22e22ce0a55036a96f652765793fb7a4
GDAL_PIXEL_TYPES = {
@ -17,3 +20,12 @@ GDAL_PIXEL_TYPES = {
10: 'GDT_CFloat32', # Complex Float32
11: 'GDT_CFloat64', # Complex Float64
}
# Lookup values to convert GDAL pixel type indices into ctypes objects.
# The GDAL band-io works with ctypes arrays to hold data to be written
# or to hold the space for data to be read into. The lookup below helps
# selecting the right ctypes object for a given gdal pixel type.
GDAL_TO_CTYPES = [
None, c_byte, c_uint16, c_int16, c_uint32, c_int32,
c_float, c_double, None, None, None, None
]

View File

@ -1,3 +1,4 @@
import json
import os
from ctypes import addressof, byref, c_double
@ -7,6 +8,7 @@ from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.gdal.prototypes import raster as capi
from django.contrib.gis.gdal.raster.band import GDALBand
from django.contrib.gis.gdal.srs import SpatialReference, SRSException
from django.contrib.gis.geometry.regex import json_regex
from django.utils import six
from django.utils.encoding import (
force_bytes, force_text, python_2_unicode_compatible,
@ -33,10 +35,22 @@ class TransformPoint(list):
def x(self):
return self[0]
@x.setter
def x(self, value):
gtf = self._raster.geotransform
gtf[self.indices[self._prop][0]] = value
self._raster.geotransform = gtf
@property
def y(self):
return self[1]
@y.setter
def y(self, value):
gtf = self._raster.geotransform
gtf[self.indices[self._prop][1]] = value
self._raster.geotransform = gtf
@python_2_unicode_compatible
class GDALRaster(GDALBase):
@ -47,17 +61,64 @@ class GDALRaster(GDALBase):
self._write = 1 if write else 0
Driver.ensure_registered()
# Preprocess json inputs. This converts json strings to dictionaries,
# which are parsed below the same way as direct dictionary inputs.
if isinstance(ds_input, six.string_types) and json_regex.match(ds_input):
ds_input = json.loads(ds_input)
# If input is a valid file path, try setting file as source.
if isinstance(ds_input, six.string_types):
if os.path.exists(ds_input):
try:
# GDALOpen will auto-detect the data source type.
self.ptr = capi.open_ds(force_bytes(ds_input), self._write)
except GDALException as err:
raise GDALException('Could not open the datasource at "{}" ({}).'.format(
ds_input, err))
else:
if not os.path.exists(ds_input):
raise GDALException('Unable to read raster source input "{}"'.format(ds_input))
try:
# GDALOpen will auto-detect the data source type.
self._ptr = capi.open_ds(force_bytes(ds_input), self._write)
except GDALException as err:
raise GDALException('Could not open the datasource at "{}" ({}).'.format(ds_input, err))
elif isinstance(ds_input, dict):
# A new raster needs to be created in write mode
self._write = 1
# Create driver (in memory by default)
driver = Driver(ds_input.get('driver', 'MEM'))
# For out of memory drivers, check filename argument
if driver.name != 'MEM' and 'name' not in ds_input:
raise GDALException('Specify name for creation of raster with driver "{}".'.format(driver.name))
# Check if width and height where specified
if 'width' not in ds_input or 'height' not in ds_input:
raise GDALException('Specify width and height attributes for JSON or dict input.')
# Create GDAL Raster
self._ptr = capi.create_ds(
driver._ptr,
force_bytes(ds_input.get('name', '')),
ds_input['width'],
ds_input['height'],
ds_input.get('nr_of_bands', len(ds_input.get('bands', []))),
ds_input.get('datatype', 6),
None
)
# Set band data if provided
for i, band_input in enumerate(ds_input.get('bands', [])):
self.bands[i].data(band_input['data'])
if 'nodata_value' in band_input:
self.bands[i].nodata_value = band_input['nodata_value']
# Set SRID, default to 0 (this assures SRS is always instanciated)
self.srs = ds_input.get('srid', 0)
# Set additional properties if provided
if 'origin' in ds_input:
self.origin.x, self.origin.y = ds_input['origin']
if 'scale' in ds_input:
self.scale.x, self.scale.y = ds_input['scale']
if 'skew' in ds_input:
self.skew.x, self.skew.y = ds_input['skew']
else:
raise GDALException('Invalid data source input type: "{}".'.format(type(ds_input)))
@ -72,15 +133,34 @@ class GDALRaster(GDALBase):
"""
Short-hand representation because WKB may be very large.
"""
return '<Raster object at %s>' % hex(addressof(self.ptr))
return '<Raster object at %s>' % hex(addressof(self._ptr))
def _flush(self):
"""
Flush all data from memory into the source file if it exists.
The data that needs flushing are geotransforms, coordinate systems,
nodata_values and pixel values. This function will be called
automatically wherever it is needed.
"""
# Raise an Exception if the value is being changed in read mode.
if not self._write:
raise GDALException('Raster needs to be opened in write mode to change values.')
capi.flush_ds(self._ptr)
@property
def name(self):
return force_text(capi.get_ds_description(self.ptr))
"""
Returns the name of this raster. Corresponds to filename
for file-based rasters.
"""
return force_text(capi.get_ds_description(self._ptr))
@cached_property
def driver(self):
ds_driver = capi.get_ds_driver(self.ptr)
"""
Returns the GDAL Driver used for this raster.
"""
ds_driver = capi.get_ds_driver(self._ptr)
return Driver(ds_driver)
@property
@ -88,48 +168,85 @@ class GDALRaster(GDALBase):
"""
Width (X axis) in pixels.
"""
return capi.get_ds_xsize(self.ptr)
return capi.get_ds_xsize(self._ptr)
@property
def height(self):
"""
Height (Y axis) in pixels.
"""
return capi.get_ds_ysize(self.ptr)
return capi.get_ds_ysize(self._ptr)
@property
def srs(self):
"""
Returns the Spatial Reference used in this GDALRaster.
Returns the SpatialReference used in this GDALRaster.
"""
try:
wkt = capi.get_ds_projection_ref(self.ptr)
wkt = capi.get_ds_projection_ref(self._ptr)
if not wkt:
return None
return SpatialReference(wkt, srs_type='wkt')
except SRSException:
return None
@cached_property
@srs.setter
def srs(self, value):
"""
Sets the spatial reference used in this GDALRaster. The input can be
a SpatialReference or any parameter accepted by the SpatialReference
constructor.
"""
if isinstance(value, SpatialReference):
srs = value
elif isinstance(value, six.integer_types + six.string_types):
srs = SpatialReference(value)
else:
raise ValueError('Could not create a SpatialReference from input.')
capi.set_ds_projection_ref(self._ptr, srs.wkt.encode())
self._flush()
@property
def geotransform(self):
"""
Returns the geotransform of the data source.
Returns the default geotransform if it does not exist or has not been
set previously. The default is (0.0, 1.0, 0.0, 0.0, 0.0, -1.0).
set previously. The default is [0.0, 1.0, 0.0, 0.0, 0.0, -1.0].
"""
# Create empty ctypes double array for data
gtf = (c_double * 6)()
capi.get_ds_geotransform(self.ptr, byref(gtf))
return tuple(gtf)
capi.get_ds_geotransform(self._ptr, byref(gtf))
return list(gtf)
@geotransform.setter
def geotransform(self, values):
"Sets the geotransform for the data source."
if sum([isinstance(x, (int, float)) for x in values]) != 6:
raise ValueError('Geotransform must consist of 6 numeric values.')
# Create ctypes double array with input and write data
values = (c_double * 6)(*values)
capi.set_ds_geotransform(self._ptr, byref(values))
self._flush()
@property
def origin(self):
"""
Coordinates of the raster origin.
"""
return TransformPoint(self, 'origin')
@property
def scale(self):
"""
Pixel scale in units of the raster projection.
"""
return TransformPoint(self, 'scale')
@property
def skew(self):
"""
Skew of pixels (rotation parameters).
"""
return TransformPoint(self, 'skew')
@property
@ -150,7 +267,10 @@ class GDALRaster(GDALBase):
@cached_property
def bands(self):
"""
Returns the bands of this raster as a list of GDALBand instances.
"""
bands = []
for idx in range(1, capi.get_ds_raster_count(self.ptr) + 1):
for idx in range(1, capi.get_ds_raster_count(self._ptr) + 1):
bands.append(GDALBand(self, idx))
return bands

View File

@ -13,13 +13,13 @@ formats.
GeoDjango provides a high-level Python interface for some of the
capabilities of OGR, including the reading and coordinate transformation
of vector spatial data.
of vector spatial data and minimal support for GDAL's features with respect
to raster (image) data.
.. note::
Although the module is named ``gdal``, GeoDjango only supports
some of the capabilities of OGR. Thus, GDAL's features with respect to
raster (image) data are minimally supported (read-only) at this time.
some of the capabilities of OGR and GDAL's raster features at this time.
__ http://www.gdal.org/
__ http://www.gdal.org/ogr/
@ -27,6 +27,8 @@ __ http://www.gdal.org/ogr/
Overview
========
.. _gdal_sample_data:
Sample Data
-----------
@ -37,6 +39,7 @@ have any data of your own to use, GeoDjango tests contain a number of
simple data sets that you can use for testing. You can download them here::
$ wget https://raw.githubusercontent.com/django/django/master/tests/gis_tests/data/cities/cities.{shp,prj,shx,dbf}
$ wget https://raw.githubusercontent.com/django/django/master/tests/gis_tests/data/rasters/raster.tif
Vector Data Source Objects
==========================
@ -1101,35 +1104,106 @@ one or more layers of data named bands. Each band, represented by a
image is represented as three bands: one for red, one for green, and one for
blue.
.. class:: GDALRaster(ds_input)
.. note::
The constructor for ``GDALRaster`` accepts a single parameter: the path of
the file you want to read.
For raster data there is no difference between a raster instance and its
data source. Unlike for the Geometry objects, :class:`GDALRaster` objects are
always a data source. Temporary rasters can be instantiated in memory
using the corresponding driver, but they will be of the same class as file-based
raster sources.
.. class:: GDALRaster(ds_input, write=False)
The constructor for ``GDALRaster`` accepts two parameters. The first parameter
defines the raster source, it is either a path to a file or spatial data with
values defining the properties of a new raster (such as size and name). If the
input is a file path, the second parameter specifies if the raster should
be opened with write access. The following example shows how rasters can be
created from different input sources (using the sample data from the GeoDjango
tests, see the :ref:`gdal_sample_data` section)::
>>> from django.contrib.gis.gdal.raster.source import GDALRaster
>>> rst = GDALRaster('/path/to/your/raster.tif', write=False)
>>> rst.name
'/path/to/your/raster.tif'
>>> rst.width, rst.height # This file has 163 x 174 pixels
(163, 174)
>>> rst = GDALRaster({'srid': 4326, 'width': 1, 'height': 2, 'datatype': 1
... 'bands': [{'data': [0, 1]}]}) # Creates in-memory raster
>>> rst.srs.srid
4326
>>> rst.width, rst.height
(1, 2)
>>> rst.bands[0].data()
array([[0, 1]], dtype=int8)
.. versionchanged:: 1.9
``GDALRaster`` objects can now be instantiated directly from raw data.
Setters have been added for the following properties: ``srs``,
``geotransform``, ``origin``, ``scale``, and ``skew``.
.. attribute:: name
The name of the source which is equivalent to the input file path.
The name of the source which is equivalent to the input file path or the name
provided upon instantiation.
>>> GDALRaster({'width': 10, 'height': 10, 'name': 'myraster'}).name
'myraster'
.. attribute:: driver
The name of the GDAL driver used to handle the input file. For example,
``GTiff`` for a ``GeoTiff`` file. See also the `GDAL Raster Formats`__
list.
The name of the GDAL driver used to handle the input file. For ``GDALRaster``\s created
from a file, the driver type is detected automatically. The creation of rasters from
scratch is a in-memory raster by default (``'MEM'``), but can be altered as
needed. For instance, use ``GTiff`` for a ``GeoTiff`` file. For a list of file types,
see also the `GDAL Raster Formats`__ list.
__ http://www.gdal.org/formats_list.html
An in-memory raster is created through the following example:
>>> GDALRaster({'width': 10, 'height': 10}).driver.name
'MEM'
A file based GeoTiff raster is created through the following example:
>>> import tempfile
>>> rstfile = tempfile.NamedTemporaryFile(suffix='.tif')
>>> rst = GDALRaster({'driver': 'GTiff', 'name': rstfile.name,
... 'width': 255, 'height': 255, 'nr_of_bands': 1})
>>> rst.name
'/tmp/tmp7x9H4J.tif' # The exact filename will be different on your computer
>>> rst.driver.name
'GTiff'
.. attribute:: width
The width of the source in pixels (X-axis).
The width of the source in pixels (X-axis).
>>> GDALRaster({'width': 10, 'height': 20}).width
10
.. attribute:: height
The height of the source in pixels (Y-axis).
>>> GDALRaster({'width': 10, 'height': 20}).height
20
.. attribute:: srs
The spatial reference system of the source, as a
:class:`SpatialReference` instance.
The spatial reference system of the raster, as a
:class:`SpatialReference` instance. The SRS can be changed by
setting it to an other :class:`SpatialReference` or providing any input
that is accepted by the :class:`SpatialReference` constructor.
>>> rst = GDALRaster({'width': 10, 'height': 20})
>>> rst.srs
None
>>> rst.srs = 4326
>>> rst.srs.srid
4326
.. attribute:: geotransform
@ -1144,34 +1218,75 @@ blue.
(indices 0 and 3), :attr:`scale` (indices 1 and 5) and :attr:`skew`
(indices 2 and 4) properties.
The default is ``[0.0, 1.0, 0.0, 0.0, 0.0, -1.0]``.
>>> rst = GDALRaster({'width': 10, 'height': 20})
>>> rst.geotransform
[0.0, 1.0, 0.0, 0.0, 0.0, -1.0]
.. attribute:: origin
Coordinates of the top left origin of the raster in the spatial
reference system of the source, as a point object with ``x`` and ``y``
members.
>>> rst = GDALRaster({'width': 10, 'height': 20})
>>> rst.origin
[0.0, 0.0]
>>> rst.origin.x = 1
>>> rst.origin
[1.0, 0.0]
.. attribute:: scale
Pixel width and height used for georeferencing the raster, as a as a
point object with ``x`` and ``y`` members. See :attr:`geotransform`
for more information.
>>> rst = GDALRaster({'width': 10, 'height': 20})
>>> rst.scale
[1.0, -1.0]
>>> rst.scale.x = 2
>>> rst.scale
[2.0, -1.0]
.. attribute:: skew
Skew coefficients used to georeference the raster, as a point object
with ``x`` and ``y`` members. In case of north up images, these
coefficients are both ``0``.
>>> rst = GDALRaster({'width': 10, 'height': 20})
>>> rst.skew
[0.0, 0.0]
>>> rst.skew.x = 3
>>> rst.skew
[3.0, 0.0]
.. attribute:: extent
Extent (boundary values) of the raster source, as a 4-tuple
``(xmin, ymin, xmax, ymax)`` in the spatial reference system of the
source.
>>> rst = GDALRaster({'width': 10, 'height': 20})
>>> rst.extent
(0.0, -20.0, 10.0, 0.0)
>>> rst.origin.x = 100
>>> rst.extent
(100.0, -20.0, 110.0, 0.0)
.. attribute:: bands
List of all bands of the source, as :class:`GDALBand` instances.
>>> rst = GDALRaster({"width": 1, "height": 2, "bands": [{"data": [0, 1]},
... {"data": [2, 3]}]})
>>> len(rst.bands)
2
>>> rst.bands[1].data()
array([[ 2., 3.]], dtype=float32)
``GDALBand``
------------
@ -1179,7 +1294,7 @@ blue.
``GDALBand`` instances are not created explicitly, but rather obtained
from a :class:`GDALRaster` object, through its :attr:`~GDALRaster.bands`
attribute.
attribute. The GDALBands contain the actual pixel values of the raster.
.. attribute:: description
@ -1193,6 +1308,12 @@ blue.
The height of the band in pixels (Y-axis).
.. attribute:: pixel_count
.. versionadded:: 1.9
The total number of pixels in this band. Is equal to ``width * height``.
.. attribute:: min
The minimum pixel value of the band (excluding the "no data" value).
@ -1207,6 +1328,10 @@ blue.
to mark pixels that are not valid data. Such pixels should generally not
be displayed, nor contribute to analysis operations.
.. versionchanged:: 1.9
This property can now be set as well.
.. method:: datatype([as_string=False])
The data type contained in the band, as an integer constant between 0
@ -1216,6 +1341,50 @@ blue.
``GDT_UInt32``, ``GDT_Int32``, ``GDT_Float32``, ``GDT_Float64``,
``GDT_CInt16``, ``GDT_CInt32``, ``GDT_CFloat32``, and ``GDT_CFloat64``.
.. method:: data(data=None, offset=None, size=None)
.. versionadded:: 1.9
The accessor to the pixel values of the ``GDALBand``. Returns the complete
data array if no parameters are provided. A subset of the pixel array can
be requested by specifying an offset and block size as tuples.
If NumPy is available, the data is returned as NumPy array. For performance
reasons, it is highly recommended to use NumPy.
Data is written to the ``GDALBand`` if the ``data`` parameter is provided.
The input can be of one of the following types - packed string, buffer, list,
array, and NumPy array. The number of items in the input must correspond to the
total number of pixels in the band, or to the number of pixels for a specific
block of pixel values if the ``offset`` and ``size`` parameters are provided.
For example:
>>> rst = GDALRaster({'width': 4, 'height': 4, 'datatype': 1, 'nr_of_bands': 1})
>>> bnd = rst.bands[0]
>>> bnd.data(range(16))
>>> bnd.data()
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]], dtype=int8)
>>> bnd.data(offset=(1,1), size=(2,2))
array([[ 5, 6],
[ 9, 10]], dtype=int8)
>>> bnd.data(data=[-1, -2, -3, -4], offset=(1,1), size=(2,2))
>>> bnd.data()
array([[ 0, 1, 2, 3],
[ 4, -1, -2, 7],
[ 8, -3, -4, 11],
[12, 13, 14, 15]], dtype=int8)
>>> bnd.data(data='\x9d\xa8\xb3\xbe', offset=(1,1), size=(2,2))
>>> bnd.data()
array([[ 0, 1, 2, 3],
[ 4, -99, -88, 7],
[ 8, -77, -66, 11],
[ 12, 13, 14, 15]], dtype=int8)
Settings
========

View File

@ -54,7 +54,10 @@ Minor features
:mod:`django.contrib.gis`
^^^^^^^^^^^^^^^^^^^^^^^^^^
* ...
* The GDAL interface now supports instantiating file-based and in-memory
:ref:`GDALRaster objects <raster-data-source-objects>` from raw data.
Setters for raster properties such as projection or pixel values have
been added.
:mod:`django.contrib.messages`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

View File

View File

@ -0,0 +1,12 @@
JSON_RASTER = """{
"srid": 4326,
"origin": [0, 0],
"scale": [1, 1],
"skew": [0, 0],
"width": 5,
"height": 5,
"nr_of_bands": 1,
"bands": [{"data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24]}]
}
"""

View File

@ -1,8 +1,8 @@
"""
gdalinfo django/contrib/gis/gdal/tests/data/raster.tif:
gdalinfo tests/gis_tests/data/rasters/raster.tif:
Driver: GTiff/GeoTIFF
Files: django/contrib/gis/gdal/tests/data/raster.tif
Files: tests/gis_tests/data/rasters/raster.tif
Size is 163, 174
Coordinate System is:
PROJCS["NAD83 / Florida GDL Albers",
@ -41,12 +41,18 @@ Band 1 Block=163x50 Type=Byte, ColorInterp=Gray
NoData Value=15
"""
import os
import struct
import tempfile
import unittest
from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.shortcuts import numpy
from django.utils import six
from django.utils._os import upath
from ..data.rasters.textrasters import JSON_RASTER
if HAS_GDAL:
from django.contrib.gis.gdal import GDALRaster
from django.contrib.gis.gdal.raster.band import GDALBand
@ -59,7 +65,7 @@ class GDALRasterTests(unittest.TestCase):
"""
def setUp(self):
self.rs_path = os.path.join(os.path.dirname(upath(__file__)),
'data/raster.tif')
'../data/rasters/raster.tif')
self.rs = GDALRaster(self.rs_path)
def test_rs_name_repr(self):
@ -78,8 +84,9 @@ class GDALRasterTests(unittest.TestCase):
self.assertEqual(self.rs.srs.units, (1.0, 'metre'))
def test_geotransform_and_friends(self):
# Assert correct values for file based raster
self.assertEqual(self.rs.geotransform,
(511700.4680706557, 100.0, 0.0, 435103.3771231986, 0.0, -100.0))
[511700.4680706557, 100.0, 0.0, 435103.3771231986, 0.0, -100.0])
self.assertEqual(self.rs.origin, [511700.4680706557, 435103.3771231986])
self.assertEqual(self.rs.origin.x, 511700.4680706557)
self.assertEqual(self.rs.origin.y, 435103.3771231986)
@ -89,22 +96,72 @@ class GDALRasterTests(unittest.TestCase):
self.assertEqual(self.rs.skew, [0, 0])
self.assertEqual(self.rs.skew.x, 0)
self.assertEqual(self.rs.skew.y, 0)
# Create in-memory rasters and change gtvalues
rsmem = GDALRaster(JSON_RASTER)
rsmem.geotransform = range(6)
self.assertEqual(rsmem.geotransform, [float(x) for x in range(6)])
self.assertEqual(rsmem.origin, [0, 3])
self.assertEqual(rsmem.origin.x, 0)
self.assertEqual(rsmem.origin.y, 3)
self.assertEqual(rsmem.scale, [1, 5])
self.assertEqual(rsmem.scale.x, 1)
self.assertEqual(rsmem.scale.y, 5)
self.assertEqual(rsmem.skew, [2, 4])
self.assertEqual(rsmem.skew.x, 2)
self.assertEqual(rsmem.skew.y, 4)
self.assertEqual(rsmem.width, 5)
self.assertEqual(rsmem.height, 5)
def test_rs_extent(self):
self.assertEqual(self.rs.extent,
(511700.4680706557, 417703.3771231986, 528000.4680706557, 435103.3771231986))
(511700.4680706557, 417703.3771231986,
528000.4680706557, 435103.3771231986))
def test_rs_bands(self):
self.assertEqual(len(self.rs.bands), 1)
self.assertIsInstance(self.rs.bands[0], GDALBand)
def test_file_based_raster_creation(self):
# Prepare tempfile
rstfile = tempfile.NamedTemporaryFile(suffix='.tif')
# Create file-based raster from scratch
GDALRaster({
'datatype': self.rs.bands[0].datatype(),
'driver': 'tif',
'name': rstfile.name,
'width': 163,
'height': 174,
'nr_of_bands': 1,
'srid': self.rs.srs.wkt,
'origin': (self.rs.origin.x, self.rs.origin.y),
'scale': (self.rs.scale.x, self.rs.scale.y),
'skew': (self.rs.skew.x, self.rs.skew.y),
'bands': [{
'data': self.rs.bands[0].data(),
'nodata_value': self.rs.bands[0].nodata_value
}]
})
# Reload newly created raster from file
restored_raster = GDALRaster(rstfile.name)
self.assertEqual(restored_raster.srs.wkt, self.rs.srs.wkt)
self.assertEqual(restored_raster.geotransform, self.rs.geotransform)
if numpy:
numpy.testing.assert_equal(
restored_raster.bands[0].data(),
self.rs.bands[0].data()
)
else:
self.assertEqual(restored_raster.bands[0].data(), self.rs.bands[0].data())
@unittest.skipUnless(HAS_GDAL, "GDAL is required")
class GDALBandTests(unittest.TestCase):
def setUp(self):
rs_path = os.path.join(os.path.dirname(upath(__file__)),
'data/raster.tif')
rs = GDALRaster(rs_path)
self.rs_path = os.path.join(os.path.dirname(upath(__file__)),
'../data/rasters/raster.tif')
rs = GDALRaster(self.rs_path)
self.band = rs.bands[0]
def test_band_data(self):
@ -116,3 +173,97 @@ class GDALBandTests(unittest.TestCase):
self.assertEqual(self.band.min, 0)
self.assertEqual(self.band.max, 255)
self.assertEqual(self.band.nodata_value, 15)
def test_read_mode_error(self):
# Open raster in read mode
rs = GDALRaster(self.rs_path, write=False)
band = rs.bands[0]
# Setting attributes in write mode raises exception in the _flush method
self.assertRaises(GDALException, setattr, band, 'nodata_value', 10)
def test_band_data_setters(self):
# Create in-memory raster and get band
rsmem = GDALRaster({
'datatype': 1,
'driver': 'MEM',
'name': 'mem_rst',
'width': 10,
'height': 10,
'nr_of_bands': 1
})
bandmem = rsmem.bands[0]
# Set nodata value
bandmem.nodata_value = 99
self.assertEqual(bandmem.nodata_value, 99)
# Set data for entire dataset
bandmem.data(range(100))
if numpy:
numpy.testing.assert_equal(bandmem.data(), numpy.arange(100).reshape(10, 10))
else:
self.assertEqual(bandmem.data(), range(100))
# Prepare data for setting values in subsequent tests
block = range(100, 104)
packed_block = struct.pack('<' + 'B B B B', *block)
# Set data from list
bandmem.data(block, (1, 1), (2, 2))
result = bandmem.data(offset=(1, 1), size=(2, 2))
if numpy:
numpy.testing.assert_equal(result, numpy.array(block).reshape(2, 2))
else:
self.assertEqual(result, block)
# Set data from packed block
bandmem.data(packed_block, (1, 1), (2, 2))
result = bandmem.data(offset=(1, 1), size=(2, 2))
if numpy:
numpy.testing.assert_equal(result, numpy.array(block).reshape(2, 2))
else:
self.assertEqual(result, block)
# Set data from bytes
bandmem.data(bytes(packed_block), (1, 1), (2, 2))
result = bandmem.data(offset=(1, 1), size=(2, 2))
if numpy:
numpy.testing.assert_equal(result, numpy.array(block).reshape(2, 2))
else:
self.assertEqual(result, block)
# Set data from bytearray
bandmem.data(bytearray(packed_block), (1, 1), (2, 2))
result = bandmem.data(offset=(1, 1), size=(2, 2))
if numpy:
numpy.testing.assert_equal(result, numpy.array(block).reshape(2, 2))
else:
self.assertEqual(result, block)
# Set data from memoryview
bandmem.data(six.memoryview(packed_block), (1, 1), (2, 2))
result = bandmem.data(offset=(1, 1), size=(2, 2))
if numpy:
numpy.testing.assert_equal(result, numpy.array(block).reshape(2, 2))
else:
self.assertEqual(result, block)
# Set data from numpy array
if numpy:
bandmem.data(numpy.array(block, dtype='int8').reshape(2, 2), (1, 1), (2, 2))
numpy.testing.assert_equal(
bandmem.data(offset=(1, 1), size=(2, 2)),
numpy.array(block).reshape(2, 2)
)
# Test json input data
rsmemjson = GDALRaster(JSON_RASTER)
bandmemjson = rsmemjson.bands[0]
if numpy:
numpy.testing.assert_equal(
bandmemjson.data(),
numpy.array(range(25)).reshape(5, 5)
)
else:
self.assertEqual(bandmemjson.data(), range(25))