Skip to content

meta

RasterMeta

Bases: BaseModel

Raster metadata.

Attributes:

Name Type Description
crs InstanceOf[CRS]

Coordinate reference system.

transform InstanceOf[Affine]

The affine transformation associated with the raster. This is based on the CRS, the cell size, as well as the offset/origin.

Source code in src/rastr/meta.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
class RasterMeta(BaseModel, extra="forbid"):
    """Raster metadata.

    Attributes:
        crs: Coordinate reference system.
        transform: The affine transformation associated with the raster. This is based
                   on the CRS, the cell size, as well as the offset/origin.
    """

    crs: InstanceOf[CRS]
    transform: InstanceOf[Affine]

    @field_validator("transform")
    @classmethod
    def check_non_rotated_non_skewed(cls, v: Affine) -> Affine:
        """Validator to ensure the transform is non-rotated and non-skewed."""
        if v.b != 0 or v.d != 0:
            msg = (
                "Only non-rotated and non-skewed transforms are currently supported"
                " (i.e. affine coefficients `b` and `d` must be 0)."
            )
            raise NotImplementedError(msg)
        return v

    @property
    def cell_size(self) -> tuple[float, float]:
        """Cell size as (width, height) in CRS units, derived from the transform."""
        return abs(self.transform.a), abs(self.transform.e)

    @property
    def cell_height(self) -> float:
        """Cell height derived from the transform's y-pixel height."""
        return abs(self.transform.e)

    @property
    def cell_width(self) -> float:
        """Cell width derived from the transform's x-pixel width."""
        return abs(self.transform.a)

    @property
    def has_square_cells(self) -> bool:
        """Whether the cells are square (i.e. cell width == cell height)."""
        return bool(np.isclose(self.cell_width, self.cell_height))

    @property
    def square_cell_size(self) -> float:
        """Cell size if the cells are square, otherwise raises an error."""
        if not self.has_square_cells:
            msg = "Cells are not square, so square_cell_size is undefined."
            raise NonSquareCellsError(msg)

        return self.cell_width

    @classmethod
    def example(cls) -> Self:
        """Create an example RasterMeta object."""
        return cls(
            crs=CRS.from_epsg(2193),
            transform=Affine.scale(2.0, 2.0),
        )

    def get_cell_centre_coords(self, shape: tuple[int, int]) -> NDArray:
        """Return an array of (x, y) coordinates for the center of each cell.

        The coordinates will be in the coordinate system defined by the
        raster's transform.

        Args:
            shape: (rows, cols) of the raster array.

        Returns:
            (x, y) coordinates for each cell center, with shape (rows, cols, 2)
        """
        x_coords = self.get_cell_x_coords(shape[1])  # cols for x-coordinates
        y_coords = self.get_cell_y_coords(shape[0])  # rows for y-coordinates
        coords = np.stack(np.meshgrid(x_coords, y_coords), axis=-1)
        return coords

    def get_cell_x_coords(self, n_columns: int) -> NDArray:
        """Return an array of x coordinates for the center of each cell.

        The coordinates will be in the coordinate system defined by the
        raster's transform.

        Args:
            n_columns: Number of columns in the raster array.

        Returns:
            x_coordinates at cell centers, with shape (n_columns,)
        """
        x_idx = np.arange(n_columns) + 0.5
        y_idx = np.zeros_like(x_idx)  # Use y=0 for a single row
        x_coords, _ = self.transform * (x_idx, y_idx)  # type: ignore[reportAssignmentType] overloaded tuple size in affine
        return x_coords

    def get_cell_y_coords(self, n_rows: int) -> NDArray:
        """Return an array of y coordinates for the center of each cell.

        The coordinates will be in the coordinate system defined by the
        raster's transform.

        Args:
            n_rows: Number of rows in the raster array.

        Returns:
            y_coordinates at cell centers, with shape (n_rows,)
        """
        x_idx = np.zeros(n_rows)  # Use x=0 for a single column
        y_idx = np.arange(n_rows) + 0.5
        _, y_coords = self.transform * (x_idx, y_idx)  # type: ignore[reportAssignmentType] overloaded tuple size in affine
        return y_coords

    @classmethod
    def infer(
        cls,
        x: np.ndarray,
        y: np.ndarray,
        *,
        cell_size: tuple[float, float] | float | None = None,
        crs: CRS,
    ) -> tuple[Self, tuple[int, int]]:
        """Automatically get recommended raster metadata (and shape) using data points.

        The cell size can be provided, or a heuristic will be used based on the spacing
        of the (x, y) points. Square cells are assumed unless a `(cell_width,
        cell_height)` pair is explicitly provided.
        """
        # Heuristic for cell size if not provided
        if cell_size is None:
            cell_size = infer_cell_size(x, y)

        cell_size = _ensure_pair(cell_size)

        shape = infer_shape(x, y, cell_size=cell_size)
        transform = infer_transform(x, y, cell_size=cell_size, crs=crs)

        raster_meta = cls(
            crs=crs,
            transform=transform,
        )
        return raster_meta, shape

cell_height property

Cell height derived from the transform's y-pixel height.

cell_size property

Cell size as (width, height) in CRS units, derived from the transform.

cell_width property

Cell width derived from the transform's x-pixel width.

has_square_cells property

Whether the cells are square (i.e. cell width == cell height).

square_cell_size property

Cell size if the cells are square, otherwise raises an error.

check_non_rotated_non_skewed(v) classmethod

Validator to ensure the transform is non-rotated and non-skewed.

Source code in src/rastr/meta.py
31
32
33
34
35
36
37
38
39
40
41
@field_validator("transform")
@classmethod
def check_non_rotated_non_skewed(cls, v: Affine) -> Affine:
    """Validator to ensure the transform is non-rotated and non-skewed."""
    if v.b != 0 or v.d != 0:
        msg = (
            "Only non-rotated and non-skewed transforms are currently supported"
            " (i.e. affine coefficients `b` and `d` must be 0)."
        )
        raise NotImplementedError(msg)
    return v

example() classmethod

Create an example RasterMeta object.

Source code in src/rastr/meta.py
72
73
74
75
76
77
78
@classmethod
def example(cls) -> Self:
    """Create an example RasterMeta object."""
    return cls(
        crs=CRS.from_epsg(2193),
        transform=Affine.scale(2.0, 2.0),
    )

get_cell_centre_coords(shape)

Return an array of (x, y) coordinates for the center of each cell.

The coordinates will be in the coordinate system defined by the raster's transform.

Parameters:

Name Type Description Default
shape tuple[int, int]

(rows, cols) of the raster array.

required

Returns:

Type Description
NDArray

(x, y) coordinates for each cell center, with shape (rows, cols, 2)

Source code in src/rastr/meta.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def get_cell_centre_coords(self, shape: tuple[int, int]) -> NDArray:
    """Return an array of (x, y) coordinates for the center of each cell.

    The coordinates will be in the coordinate system defined by the
    raster's transform.

    Args:
        shape: (rows, cols) of the raster array.

    Returns:
        (x, y) coordinates for each cell center, with shape (rows, cols, 2)
    """
    x_coords = self.get_cell_x_coords(shape[1])  # cols for x-coordinates
    y_coords = self.get_cell_y_coords(shape[0])  # rows for y-coordinates
    coords = np.stack(np.meshgrid(x_coords, y_coords), axis=-1)
    return coords

get_cell_x_coords(n_columns)

Return an array of x coordinates for the center of each cell.

The coordinates will be in the coordinate system defined by the raster's transform.

Parameters:

Name Type Description Default
n_columns int

Number of columns in the raster array.

required

Returns:

Type Description
NDArray

x_coordinates at cell centers, with shape (n_columns,)

Source code in src/rastr/meta.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def get_cell_x_coords(self, n_columns: int) -> NDArray:
    """Return an array of x coordinates for the center of each cell.

    The coordinates will be in the coordinate system defined by the
    raster's transform.

    Args:
        n_columns: Number of columns in the raster array.

    Returns:
        x_coordinates at cell centers, with shape (n_columns,)
    """
    x_idx = np.arange(n_columns) + 0.5
    y_idx = np.zeros_like(x_idx)  # Use y=0 for a single row
    x_coords, _ = self.transform * (x_idx, y_idx)  # type: ignore[reportAssignmentType] overloaded tuple size in affine
    return x_coords

get_cell_y_coords(n_rows)

Return an array of y coordinates for the center of each cell.

The coordinates will be in the coordinate system defined by the raster's transform.

Parameters:

Name Type Description Default
n_rows int

Number of rows in the raster array.

required

Returns:

Type Description
NDArray

y_coordinates at cell centers, with shape (n_rows,)

Source code in src/rastr/meta.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def get_cell_y_coords(self, n_rows: int) -> NDArray:
    """Return an array of y coordinates for the center of each cell.

    The coordinates will be in the coordinate system defined by the
    raster's transform.

    Args:
        n_rows: Number of rows in the raster array.

    Returns:
        y_coordinates at cell centers, with shape (n_rows,)
    """
    x_idx = np.zeros(n_rows)  # Use x=0 for a single column
    y_idx = np.arange(n_rows) + 0.5
    _, y_coords = self.transform * (x_idx, y_idx)  # type: ignore[reportAssignmentType] overloaded tuple size in affine
    return y_coords

infer(x, y, *, cell_size=None, crs) classmethod

Automatically get recommended raster metadata (and shape) using data points.

The cell size can be provided, or a heuristic will be used based on the spacing of the (x, y) points. Square cells are assumed unless a (cell_width, cell_height) pair is explicitly provided.

Source code in src/rastr/meta.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@classmethod
def infer(
    cls,
    x: np.ndarray,
    y: np.ndarray,
    *,
    cell_size: tuple[float, float] | float | None = None,
    crs: CRS,
) -> tuple[Self, tuple[int, int]]:
    """Automatically get recommended raster metadata (and shape) using data points.

    The cell size can be provided, or a heuristic will be used based on the spacing
    of the (x, y) points. Square cells are assumed unless a `(cell_width,
    cell_height)` pair is explicitly provided.
    """
    # Heuristic for cell size if not provided
    if cell_size is None:
        cell_size = infer_cell_size(x, y)

    cell_size = _ensure_pair(cell_size)

    shape = infer_shape(x, y, cell_size=cell_size)
    transform = infer_transform(x, y, cell_size=cell_size, crs=crs)

    raster_meta = cls(
        crs=crs,
        transform=transform,
    )
    return raster_meta, shape

infer_cell_size(x, y)

Infer a suitable cell size based on the spacing of (x, y) data points.

When points are distributed regularly, this corresponds to the distance between neighboring points.

When distributed irregularly, the size is more influenced by the densest clusters of points, i.e. the cell size will be small enough to capture the detail in these clusters.

This is based on a heuristic which has been found to work well in practice.

The inferred result uses square cells, i.e. (cell_size, cell_size).

Source code in src/rastr/meta.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def infer_cell_size(x: np.ndarray, y: np.ndarray) -> tuple[float, float]:
    """Infer a suitable cell size based on the spacing of (x, y) data points.

    When points are distributed regularly, this corresponds to the distance between
    neighboring points.

    When distributed irregularly, the size is more influenced by the densest clusters of
    points, i.e. the cell size will be small enough to capture the detail in these
    clusters.

    This is based on a heuristic which has been found to work well in practice.

    The inferred result uses square cells, i.e. `(cell_size, cell_size)`.
    """
    from scipy.spatial import KDTree

    # 5th percentile of nearest neighbor distances between the (x,y) points
    xy_points = np.column_stack((x, y))
    tree = KDTree(xy_points)
    distances, _ = tree.query(xy_points, k=2)
    distances: np.ndarray
    cell_size = float(np.percentile(distances[distances > 0], 5))

    return cell_size, cell_size

infer_origin(x, y, *, cell_size)

Infer a suitable raster origin based on the bounds of (x, y) data points.

Use equal values in cell_size for square cells, or distinct values for rectangular cells.

Source code in src/rastr/meta.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def infer_origin(
    x: np.ndarray, y: np.ndarray, *, cell_size: tuple[float, float]
) -> tuple[float, float]:
    """Infer a suitable raster origin based on the bounds of (x, y) data points.

    Use equal values in `cell_size` for square cells, or distinct values for
    rectangular cells.
    """
    cell_width, cell_height = cell_size

    # Compute bounds from data
    minx, _miny, _maxx, maxy = np.min(x), np.min(y), np.max(x), np.max(y)

    origin = (minx - cell_width / 2, maxy + cell_height / 2)
    return origin

infer_shape(x, y, *, cell_size=None)

Infer a suitable raster shape based on the bounds of (x, y) data points.

Square cells are assumed unless a (cell_width, cell_height) pair is explicitly provided.

Source code in src/rastr/meta.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def infer_shape(
    x: np.ndarray,
    y: np.ndarray,
    *,
    cell_size: tuple[float, float] | float | None = None,
) -> tuple[int, int]:
    """Infer a suitable raster shape based on the bounds of (x, y) data points.

    Square cells are assumed unless a `(cell_width, cell_height)` pair is explicitly
    provided.
    """
    if cell_size is None:
        cell_size = infer_cell_size(x, y)

    cell_width, cell_height = _ensure_pair(cell_size)

    # Compute bounds from data
    minx, miny, maxx, maxy = np.min(x), np.min(y), np.max(x), np.max(y)

    # Compute grid shape
    width = max(1, int(np.ceil((maxx - minx) / cell_width)) + 1)
    height = max(1, int(np.ceil((maxy - miny) / cell_height)) + 1)
    shape = (height, width)

    return shape

infer_transform(x, y, *, cell_size=None, crs)

Infer a suitable raster transform based on the bounds of (x, y) data points.

Square cells are assumed unless a (cell_width, cell_height) pair is explicitly provided.

Source code in src/rastr/meta.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def infer_transform(
    x: np.ndarray,
    y: np.ndarray,
    *,
    cell_size: tuple[float, float] | float | None = None,
    crs: CRS,
) -> Affine:
    """Infer a suitable raster transform based on the bounds of (x, y) data points.

    Square cells are assumed unless a `(cell_width, cell_height)` pair is explicitly
    provided.
    """
    if cell_size is None:
        cell_size = infer_cell_size(x, y)

    cell_width, cell_height = _ensure_pair(cell_size)

    (xs, ys) = get_affine_sign(crs)
    return Affine.translation(
        *infer_origin(x, y, cell_size=(cell_width, cell_height))
    ) * Affine.scale(xs * cell_width, ys * cell_height)