577 lines
19 KiB
Python
577 lines
19 KiB
Python
"""Geospatial transforms"""
|
|
|
|
from contextlib import ExitStack
|
|
from functools import partial
|
|
import numpy as np
|
|
import warnings
|
|
from numbers import Number
|
|
|
|
from affine import Affine
|
|
|
|
from rasterio.env import env_ctx_if_needed
|
|
from rasterio._transform import (
|
|
_transform_from_gcps,
|
|
RPCTransformerBase,
|
|
GCPTransformerBase,
|
|
)
|
|
from rasterio.enums import TransformDirection, TransformMethod
|
|
from rasterio.control import GroundControlPoint
|
|
from rasterio.rpc import RPC
|
|
from rasterio.errors import TransformError, RasterioDeprecationWarning
|
|
|
|
IDENTITY = Affine.identity()
|
|
GDAL_IDENTITY = IDENTITY.to_gdal()
|
|
|
|
|
|
class TransformMethodsMixin:
|
|
"""Mixin providing methods for calculations related
|
|
to transforming between rows and columns of the raster
|
|
array and the coordinates.
|
|
|
|
These methods are wrappers for the functionality in
|
|
`rasterio.transform` module.
|
|
|
|
A subclass with this mixin MUST provide a `transform`
|
|
property.
|
|
|
|
"""
|
|
|
|
def xy(
|
|
self,
|
|
row,
|
|
col,
|
|
z=None,
|
|
offset="center",
|
|
transform_method=TransformMethod.affine,
|
|
**rpc_options
|
|
):
|
|
"""Get the coordinates x, y of a pixel at row, col.
|
|
|
|
The pixel's center is returned by default, but a corner can be returned
|
|
by setting `offset` to one of `ul, ur, ll, lr`.
|
|
|
|
Parameters
|
|
----------
|
|
row : int
|
|
Pixel row.
|
|
col : int
|
|
Pixel column.
|
|
z : float, optional
|
|
Height associated with coordinates. Primarily used for RPC based
|
|
coordinate transformations. Ignored for affine based
|
|
transformations. Default: 0.
|
|
offset : str, optional
|
|
Determines if the returned coordinates are for the center of the
|
|
pixel or for a corner.
|
|
transform_method: TransformMethod, optional
|
|
The coordinate transformation method. Default: `TransformMethod.affine`.
|
|
rpc_options: dict, optional
|
|
Additional arguments passed to GDALCreateRPCTransformer
|
|
|
|
Returns
|
|
-------
|
|
tuple
|
|
x, y
|
|
|
|
"""
|
|
transform = getattr(self, transform_method.value)
|
|
if transform_method is TransformMethod.gcps:
|
|
transform = transform[0]
|
|
if not transform:
|
|
raise AttributeError(f"Dataset has no {transform_method}")
|
|
return xy(transform, row, col, zs=z, offset=offset, **rpc_options)
|
|
|
|
def index(
|
|
self,
|
|
x,
|
|
y,
|
|
z=None,
|
|
op=None,
|
|
precision=None,
|
|
transform_method=TransformMethod.affine,
|
|
**rpc_options
|
|
):
|
|
"""Get the (row, col) index of the pixel containing (x, y).
|
|
|
|
Parameters
|
|
----------
|
|
x : float
|
|
x value in coordinate reference system
|
|
y : float
|
|
y value in coordinate reference system
|
|
z : float, optional
|
|
Height associated with coordinates. Primarily used for RPC
|
|
based coordinate transformations. Ignored for affine based
|
|
transformations. Default: 0.
|
|
op : function, optional (default: numpy.floor)
|
|
Function to convert fractional pixels to whole numbers
|
|
(floor, ceiling, round)
|
|
transform_method: TransformMethod, optional
|
|
The coordinate transformation method. Default:
|
|
`TransformMethod.affine`.
|
|
rpc_options: dict, optional
|
|
Additional arguments passed to GDALCreateRPCTransformer
|
|
precision : int, optional
|
|
This parameter is unused, deprecated in rasterio 1.3.0, and
|
|
will be removed in version 2.0.0.
|
|
|
|
Returns
|
|
-------
|
|
tuple: int, int
|
|
(row index, col index)
|
|
|
|
"""
|
|
if precision is not None:
|
|
warnings.warn(
|
|
"The precision parameter is unused, deprecated, and will be removed in 2.0.0.",
|
|
RasterioDeprecationWarning,
|
|
)
|
|
|
|
transform = getattr(self, transform_method.value)
|
|
if transform_method is TransformMethod.gcps:
|
|
transform = transform[0]
|
|
if not transform:
|
|
raise AttributeError(f"Dataset has no {transform_method}")
|
|
return tuple(int(val) for val in rowcol(transform, x, y, zs=z, op=op, **rpc_options))
|
|
|
|
|
|
def get_transformer(transform, **rpc_options):
|
|
"""Return the appropriate transformer class"""
|
|
if transform is None:
|
|
raise ValueError("Invalid transform")
|
|
if isinstance(transform, Affine):
|
|
transformer_cls = partial(AffineTransformer, transform)
|
|
elif isinstance(transform, RPC):
|
|
transformer_cls = partial(RPCTransformer, transform, **rpc_options)
|
|
else:
|
|
transformer_cls = partial(GCPTransformer, transform)
|
|
return transformer_cls
|
|
|
|
|
|
def tastes_like_gdal(seq):
|
|
"""Return True if `seq` matches the GDAL geotransform pattern."""
|
|
return tuple(seq) == GDAL_IDENTITY or (
|
|
seq[2] == seq[4] == 0.0 and seq[1] > 0 and seq[5] < 0)
|
|
|
|
|
|
def guard_transform(transform):
|
|
"""Return an Affine transformation instance."""
|
|
if not isinstance(transform, Affine):
|
|
if tastes_like_gdal(transform):
|
|
raise TypeError(
|
|
"GDAL-style transforms have been deprecated. This "
|
|
"exception will be raised for a period of time to highlight "
|
|
"potentially confusing errors, but will eventually be removed.")
|
|
else:
|
|
transform = Affine(*transform)
|
|
return transform
|
|
|
|
|
|
def from_origin(west, north, xsize, ysize):
|
|
"""Return an Affine transformation given upper left and pixel sizes.
|
|
|
|
Return an Affine transformation for a georeferenced raster given
|
|
the coordinates of its upper left corner `west`, `north` and pixel
|
|
sizes `xsize`, `ysize`.
|
|
|
|
"""
|
|
return Affine.translation(west, north) * Affine.scale(xsize, -ysize)
|
|
|
|
|
|
def from_bounds(west, south, east, north, width, height):
|
|
"""Return an Affine transformation given bounds, width and height.
|
|
|
|
Return an Affine transformation for a georeferenced raster given
|
|
its bounds `west`, `south`, `east`, `north` and its `width` and
|
|
`height` in number of pixels.
|
|
|
|
"""
|
|
return Affine.translation(west, north) * Affine.scale(
|
|
(east - west) / width, (south - north) / height)
|
|
|
|
|
|
def array_bounds(height, width, transform):
|
|
"""Return the bounds of an array given height, width, and a transform.
|
|
|
|
Return the `west, south, east, north` bounds of an array given
|
|
its height, width, and an affine transform.
|
|
|
|
"""
|
|
a, b, c, d, e, f, _, _, _ = transform
|
|
if b == d == 0:
|
|
west, south, east, north = c, f + e * height, c + a * width, f
|
|
else:
|
|
c0x, c0y = c, f
|
|
c1x, c1y = transform * (0, height)
|
|
c2x, c2y = transform * (width, height)
|
|
c3x, c3y = transform * (width, 0)
|
|
xs = (c0x, c1x, c2x, c3x)
|
|
ys = (c0y, c1y, c2y, c3y)
|
|
west, south, east, north = min(xs), min(ys), max(xs), max(ys)
|
|
|
|
return west, south, east, north
|
|
|
|
|
|
def xy(transform, rows, cols, zs=None, offset='center', **rpc_options):
|
|
"""Get the x and y coordinates of pixels at `rows` and `cols`.
|
|
|
|
The pixel's center is returned by default, but a corner can be returned
|
|
by setting `offset` to one of `ul, ur, ll, lr`.
|
|
|
|
Supports affine, Ground Control Point (GCP), or Rational Polynomial
|
|
Coefficients (RPC) based coordinate transformations.
|
|
|
|
Parameters
|
|
----------
|
|
transform : Affine or sequence of GroundControlPoint or RPC
|
|
Transform suitable for input to AffineTransformer, GCPTransformer, or RPCTransformer.
|
|
rows : list or int
|
|
Pixel rows.
|
|
cols : int or sequence of ints
|
|
Pixel columns.
|
|
zs : list or float, optional
|
|
Height associated with coordinates. Primarily used for RPC based
|
|
coordinate transformations. Ignored for affine based
|
|
transformations. Default: 0.
|
|
offset : str, optional
|
|
Determines if the returned coordinates are for the center of the
|
|
pixel or for a corner.
|
|
rpc_options : dict, optional
|
|
Additional arguments passed to GDALCreateRPCTransformer.
|
|
|
|
Returns
|
|
-------
|
|
xs : float or list of floats
|
|
x coordinates in coordinate reference system
|
|
ys : float or list of floats
|
|
y coordinates in coordinate reference system
|
|
|
|
"""
|
|
transformer_cls = get_transformer(transform, **rpc_options)
|
|
with transformer_cls() as transformer:
|
|
return transformer.xy(rows, cols, zs=zs, offset=offset)
|
|
|
|
|
|
def rowcol(
|
|
transform,
|
|
xs,
|
|
ys,
|
|
zs=None,
|
|
op=None,
|
|
precision=None,
|
|
**rpc_options,
|
|
):
|
|
"""Get rows and cols of the pixels containing (x, y).
|
|
|
|
Parameters
|
|
----------
|
|
transform : Affine or sequence of GroundControlPoint or RPC
|
|
Transform suitable for input to AffineTransformer,
|
|
GCPTransformer, or RPCTransformer.
|
|
xs : list or float
|
|
x values in coordinate reference system.
|
|
ys : list or float
|
|
y values in coordinate reference system.
|
|
zs : list or float, optional
|
|
Height associated with coordinates. Primarily used for RPC based
|
|
coordinate transformations. Ignored for affine based
|
|
transformations. Default: 0.
|
|
op : function, optional (default: numpy.floor)
|
|
Function to convert fractional pixels to whole numbers (floor,
|
|
ceiling, round)
|
|
precision : int or float, optional
|
|
This parameter is unused, deprecated in rasterio 1.3.0, and
|
|
will be removed in version 2.0.0.
|
|
rpc_options : dict, optional
|
|
Additional arguments passed to GDALCreateRPCTransformer.
|
|
|
|
Returns
|
|
-------
|
|
rows : array of ints or floats
|
|
cols : array of ints or floats
|
|
Integers are the default. The numerical type is determined by
|
|
the type returned by op().
|
|
|
|
"""
|
|
if precision is not None:
|
|
warnings.warn(
|
|
"The precision parameter is unused, deprecated, and will be removed in 2.0.0.",
|
|
RasterioDeprecationWarning,
|
|
)
|
|
|
|
transformer_cls = get_transformer(transform, **rpc_options)
|
|
with transformer_cls() as transformer:
|
|
return transformer.rowcol(xs, ys, zs=zs, op=op)
|
|
|
|
|
|
def from_gcps(gcps):
|
|
"""Make an Affine transform from ground control points.
|
|
|
|
Parameters
|
|
----------
|
|
gcps : sequence of GroundControlPoint
|
|
Such as the first item of a dataset's `gcps` property.
|
|
|
|
Returns
|
|
-------
|
|
Affine
|
|
|
|
"""
|
|
return Affine.from_gdal(*_transform_from_gcps(gcps))
|
|
|
|
|
|
class TransformerBase:
|
|
"""Generic GDAL transformer base class
|
|
|
|
Notes
|
|
-----
|
|
Subclasses must have a _transformer attribute and implement a `_transform` method.
|
|
|
|
"""
|
|
def __init__(self):
|
|
self._transformer = None
|
|
|
|
@staticmethod
|
|
def _ensure_arr_input(xs, ys, zs=None):
|
|
"""Ensure all input coordinates are mapped to array-like objects
|
|
|
|
Raises
|
|
------
|
|
TransformError
|
|
If input coordinates are not all of the same length
|
|
"""
|
|
xs = np.atleast_1d(xs)
|
|
ys = np.atleast_1d(ys)
|
|
if zs is not None:
|
|
zs = np.atleast_1d(zs)
|
|
else:
|
|
zs = np.zeros(1)
|
|
|
|
try:
|
|
broadcasted = np.broadcast(xs, ys, zs)
|
|
except ValueError as error:
|
|
raise TransformError() from error
|
|
|
|
return xs, ys, zs
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
pass
|
|
|
|
def rowcol(self, xs, ys, zs=None, op=None, precision=None):
|
|
"""Get rows and cols coordinates given geographic coordinates.
|
|
|
|
Parameters
|
|
----------
|
|
xs, ys : float or list of float
|
|
Geographic coordinates
|
|
zs : float or list of float, optional
|
|
Height associated with coordinates. Primarily used for RPC
|
|
based coordinate transformations. Ignored for affine based
|
|
transformations. Default: 0.
|
|
op : function, optional (default: numpy.floor)
|
|
Function to convert fractional pixels to whole numbers
|
|
(floor, ceiling, round)
|
|
precision : int, optional (default: None)
|
|
This parameter is unused, deprecated in rasterio 1.3.0, and
|
|
will be removed in version 2.0.0.
|
|
|
|
Raises
|
|
------
|
|
TypeError
|
|
If coordinate transformation fails.
|
|
ValueError
|
|
If input coordinates are not all equal length.
|
|
|
|
Returns
|
|
-------
|
|
tuple of numbers or array of numbers.
|
|
Integers are the default. The numerical type is determined
|
|
by the type returned by op().
|
|
|
|
"""
|
|
if precision is not None:
|
|
warnings.warn(
|
|
"The precision parameter is unused, deprecated, and will be removed in 2.0.0.",
|
|
RasterioDeprecationWarning,
|
|
)
|
|
|
|
IS_SCALAR = isinstance(xs, Number) and isinstance(ys, Number)
|
|
xs, ys, zs = self._ensure_arr_input(xs, ys, zs=zs)
|
|
|
|
try:
|
|
new_cols, new_rows = self._transform(
|
|
xs, ys, zs, transform_direction=TransformDirection.reverse
|
|
)
|
|
|
|
if op is None:
|
|
new_rows = np.floor(new_rows).astype(dtype="int32")
|
|
new_cols = np.floor(new_cols).astype(dtype="int32")
|
|
elif isinstance(op, np.ufunc):
|
|
op(new_rows, out=new_rows)
|
|
op(new_cols, out=new_cols)
|
|
else:
|
|
new_rows = np.array(list(map(op, new_rows)))
|
|
new_cols = np.array(list(map(op, new_cols)))
|
|
|
|
if IS_SCALAR:
|
|
return new_rows[0], new_cols[0]
|
|
else:
|
|
return new_rows, new_cols
|
|
|
|
except TypeError:
|
|
raise TransformError("Invalid inputs")
|
|
|
|
def xy(self, rows, cols, zs=None, offset='center'):
|
|
"""
|
|
Returns geographic coordinates given dataset rows and cols coordinates
|
|
|
|
Parameters
|
|
----------
|
|
rows, cols : int or list of int
|
|
Image pixel coordinates
|
|
zs : float or list of float, optional
|
|
Height associated with coordinates. Primarily used for RPC based
|
|
coordinate transformations. Ignored for affine based
|
|
transformations. Default: 0.
|
|
offset : str, optional
|
|
Determines if the returned coordinates are for the center of the
|
|
pixel or for a corner. Available options include center, ul, ur, ll,
|
|
lr.
|
|
Raises
|
|
------
|
|
ValueError
|
|
If input coordinates are not all equal length
|
|
|
|
Returns
|
|
-------
|
|
tuple of float or list of float
|
|
|
|
"""
|
|
IS_SCALAR = isinstance(rows, Number) and isinstance(cols, Number)
|
|
rows, cols, zs = self._ensure_arr_input(rows, cols, zs=zs)
|
|
|
|
if offset == 'center':
|
|
coff, roff = (0.5, 0.5)
|
|
elif offset == 'ul':
|
|
coff, roff = (0, 0)
|
|
elif offset == 'ur':
|
|
coff, roff = (1, 0)
|
|
elif offset == 'll':
|
|
coff, roff = (0, 1)
|
|
elif offset == 'lr':
|
|
coff, roff = (1, 1)
|
|
else:
|
|
raise TransformError("Invalid offset")
|
|
|
|
try:
|
|
# shift input coordinates according to offset
|
|
T = IDENTITY.translation(coff, roff)
|
|
identity_transformer = AffineTransformer(T)
|
|
offset_cols, offset_rows = identity_transformer._transform(
|
|
cols, rows, zs, transform_direction=TransformDirection.forward
|
|
)
|
|
new_xs, new_ys = self._transform(
|
|
offset_cols, offset_rows, zs, transform_direction=TransformDirection.forward
|
|
)
|
|
|
|
if IS_SCALAR:
|
|
return new_xs[0], new_ys[0]
|
|
else:
|
|
return new_xs, new_ys
|
|
except TypeError:
|
|
raise TransformError("Invalid inputs")
|
|
|
|
def _transform(self, xs, ys, zs, transform_direction):
|
|
raise NotImplementedError
|
|
|
|
|
|
class GDALTransformerBase(TransformerBase):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._env = ExitStack()
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def __enter__(self):
|
|
self._env.enter_context(env_ctx_if_needed())
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
self.close()
|
|
self._env.close()
|
|
|
|
|
|
class AffineTransformer(TransformerBase):
|
|
"""A pure Python class related to affine based coordinate transformations."""
|
|
def __init__(self, affine_transform):
|
|
super().__init__()
|
|
if not isinstance(affine_transform, Affine):
|
|
raise ValueError("Not an affine transform")
|
|
self._transformer = affine_transform
|
|
self._transform_arr = np.empty((3, 3), dtype='float64')
|
|
|
|
def _transform(self, xs, ys, zs, transform_direction):
|
|
transform = self._transform_arr
|
|
if transform_direction is TransformDirection.forward:
|
|
transform.flat[:] = self._transformer
|
|
elif transform_direction is TransformDirection.reverse:
|
|
transform.flat[:] = ~self._transformer
|
|
|
|
bi = np.broadcast(xs, ys)
|
|
input_matrix = np.empty((3, bi.size))
|
|
input_matrix[0] = bi.iters[0]
|
|
input_matrix[1] = bi.iters[1]
|
|
input_matrix[2] = 1
|
|
transform.dot(input_matrix, out=input_matrix)
|
|
return input_matrix[0], input_matrix[1]
|
|
|
|
|
|
def __repr__(self):
|
|
return "<AffineTransformer>"
|
|
|
|
|
|
class RPCTransformer(RPCTransformerBase, GDALTransformerBase):
|
|
"""
|
|
Class related to Rational Polynomial Coeffecients (RPCs) based
|
|
coordinate transformations.
|
|
|
|
Uses GDALCreateRPCTransformer and GDALRPCTransform for computations. Options
|
|
for GDALCreateRPCTransformer may be passed using `rpc_options`.
|
|
Ensure that GDAL transformer objects are destroyed by calling `close()`
|
|
method or using context manager interface.
|
|
|
|
"""
|
|
def __init__(self, rpcs, **rpc_options):
|
|
if not isinstance(rpcs, (RPC, dict)):
|
|
raise ValueError("RPCTransformer requires RPC")
|
|
super().__init__(rpcs, **rpc_options)
|
|
|
|
def __repr__(self):
|
|
return "<{} RPCTransformer>".format(
|
|
self.closed and 'closed' or 'open')
|
|
|
|
|
|
class GCPTransformer(GCPTransformerBase, GDALTransformerBase):
|
|
"""
|
|
Class related to Ground Control Point (GCPs) based
|
|
coordinate transformations.
|
|
|
|
Uses GDALCreateGCPTransformer and GDALGCPTransform for computations.
|
|
Ensure that GDAL transformer objects are destroyed by calling `close()`
|
|
method or using context manager interface. If `tps` is set to True,
|
|
uses GDALCreateTPSTransformer and GDALTPSTransform instead.
|
|
|
|
"""
|
|
def __init__(self, gcps, tps=False):
|
|
if len(gcps) and not isinstance(gcps[0], GroundControlPoint):
|
|
raise ValueError("GCPTransformer requires sequence of GroundControlPoint")
|
|
super().__init__(gcps, tps)
|
|
|
|
def __repr__(self):
|
|
return "<{} GCPTransformer>".format(
|
|
self.closed and 'closed' or 'open')
|