summit/backend/venv/lib/python3.12/site-packages/rasterio/transform.py

577 lines
19 KiB
Python
Raw Normal View History

2025-12-08 16:31:30 +00:00
"""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')