|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from typing import Iterable, Literal |
| 4 | + |
| 5 | +import numpy as np |
| 6 | +import rasterio as rio |
| 7 | + |
| 8 | +from geoutils._config import config |
| 9 | +from geoutils._typing import ArrayLike, NDArrayNum |
| 10 | + |
| 11 | + |
| 12 | +def _ij2xy( |
| 13 | + i: ArrayLike, |
| 14 | + j: ArrayLike, |
| 15 | + transform: rio.transform.Affine, |
| 16 | + area_or_point: Literal["Area", "Point"] | None, |
| 17 | + shift_area_or_point: bool | None = None, |
| 18 | + force_offset: str | None = None, |
| 19 | +) -> tuple[NDArrayNum, NDArrayNum]: |
| 20 | + """See description of Raster.ij2xy.""" |
| 21 | + |
| 22 | + # If undefined, default to the global system config |
| 23 | + if shift_area_or_point is None: |
| 24 | + shift_area_or_point = config["shift_area_or_point"] |
| 25 | + |
| 26 | + # Shift by half a pixel back for "Point" interpretation |
| 27 | + if shift_area_or_point and force_offset is None: |
| 28 | + if area_or_point is not None and area_or_point == "Point": |
| 29 | + i = np.asarray(i) - 0.5 |
| 30 | + j = np.asarray(j) - 0.5 |
| 31 | + |
| 32 | + # Default offset is upper-left for raster coordinates |
| 33 | + if force_offset is None: |
| 34 | + force_offset = "ul" |
| 35 | + |
| 36 | + x, y = rio.transform.xy(transform, i, j, offset=force_offset) |
| 37 | + |
| 38 | + return x, y |
| 39 | + |
| 40 | + |
| 41 | +def _xy2ij( |
| 42 | + x: ArrayLike, |
| 43 | + y: ArrayLike, |
| 44 | + transform: rio.transform.Affine, |
| 45 | + area_or_point: Literal["Area", "Point"] | None, |
| 46 | + op: type = np.float32, |
| 47 | + precision: float | None = None, |
| 48 | + shift_area_or_point: bool | None = None, |
| 49 | +) -> tuple[NDArrayNum, NDArrayNum]: |
| 50 | + """See description of Raster.xy2ij.""" |
| 51 | + |
| 52 | + # If undefined, default to the global system config |
| 53 | + if shift_area_or_point is None: |
| 54 | + shift_area_or_point = config["shift_area_or_point"] |
| 55 | + |
| 56 | + # Input checks |
| 57 | + if op not in [np.float32, np.float64, float]: |
| 58 | + raise UserWarning( |
| 59 | + "Operator is not of type float: rio.Dataset.index might " |
| 60 | + "return unreliable indexes due to rounding issues." |
| 61 | + ) |
| 62 | + |
| 63 | + i, j = rio.transform.rowcol(transform, x, y, op=op, precision=precision) |
| 64 | + |
| 65 | + # Necessary because rio.Dataset.index does not return abc.Iterable for a single point |
| 66 | + if not isinstance(i, Iterable): |
| 67 | + i, j = ( |
| 68 | + np.asarray( |
| 69 | + [ |
| 70 | + i, |
| 71 | + ] |
| 72 | + ), |
| 73 | + np.asarray( |
| 74 | + [ |
| 75 | + j, |
| 76 | + ] |
| 77 | + ), |
| 78 | + ) |
| 79 | + else: |
| 80 | + i, j = (np.asarray(i), np.asarray(j)) |
| 81 | + |
| 82 | + # AREA_OR_POINT GDAL attribute, i.e. does the value refer to the upper left corner "Area" or |
| 83 | + # the center of pixel "Point". This normally has no influence on georeferencing, it's only |
| 84 | + # about the interpretation of the raster values, and thus can affect sub-pixel interpolation, |
| 85 | + # for more details see: https://gdal.org/user/raster_data_model.html#metadata |
| 86 | + |
| 87 | + # If the user wants to shift according to the interpretation |
| 88 | + if shift_area_or_point: |
| 89 | + |
| 90 | + # Shift by half a pixel if the AREA_OR_POINT attribute is "Point", otherwise leave as is |
| 91 | + if area_or_point is not None and area_or_point == "Point": |
| 92 | + if not isinstance(i.flat[0], (np.floating, float)): |
| 93 | + raise ValueError("Operator must return np.floating values to perform pixel interpretation shifting.") |
| 94 | + |
| 95 | + i += 0.5 |
| 96 | + j += 0.5 |
| 97 | + |
| 98 | + # Convert output indexes to integer if they are all whole numbers |
| 99 | + if np.all(np.mod(i, 1) == 0) and np.all(np.mod(j, 1) == 0): |
| 100 | + i = i.astype(int) |
| 101 | + j = j.astype(int) |
| 102 | + |
| 103 | + return i, j |
| 104 | + |
| 105 | + |
| 106 | +def _coords( |
| 107 | + transform: rio.transform.Affine, |
| 108 | + shape: tuple[int, int], |
| 109 | + area_or_point: Literal["Area", "Point"] | None, |
| 110 | + grid: bool = True, |
| 111 | + shift_area_or_point: bool | None = None, |
| 112 | + force_offset: str | None = None, |
| 113 | +) -> tuple[NDArrayNum, NDArrayNum]: |
| 114 | + """See description of Raster.coords.""" |
| 115 | + |
| 116 | + # The coordinates are extracted from indexes 0 to shape |
| 117 | + _, yy = _ij2xy( |
| 118 | + i=np.arange(shape[0] - 1, -1, -1), |
| 119 | + j=0, |
| 120 | + transform=transform, |
| 121 | + area_or_point=area_or_point, |
| 122 | + shift_area_or_point=shift_area_or_point, |
| 123 | + force_offset=force_offset, |
| 124 | + ) |
| 125 | + xx, _ = _ij2xy( |
| 126 | + i=0, |
| 127 | + j=np.arange(shape[1]), |
| 128 | + transform=transform, |
| 129 | + area_or_point=area_or_point, |
| 130 | + shift_area_or_point=shift_area_or_point, |
| 131 | + force_offset=force_offset, |
| 132 | + ) |
| 133 | + |
| 134 | + # If grid is True, return coordinate grids |
| 135 | + if grid: |
| 136 | + meshgrid = tuple(np.meshgrid(xx, np.flip(yy))) |
| 137 | + return meshgrid # type: ignore |
| 138 | + else: |
| 139 | + return np.asarray(xx), np.asarray(yy) |
| 140 | + |
| 141 | + |
| 142 | +def _outside_image( |
| 143 | + xi: ArrayLike, |
| 144 | + yj: ArrayLike, |
| 145 | + transform: rio.transform.Affine, |
| 146 | + shape: tuple[int, int], |
| 147 | + area_or_point: Literal["Area", "Point"] | None, |
| 148 | + index: bool = True, |
| 149 | +) -> bool: |
| 150 | + """See description of Raster.outside_image.""" |
| 151 | + |
| 152 | + if not index: |
| 153 | + xi, xj = _xy2ij(xi, yj, transform=transform, area_or_point=area_or_point) |
| 154 | + |
| 155 | + if np.any(np.array((xi, yj)) < 0): |
| 156 | + return True |
| 157 | + elif np.asanyarray(xi) > shape[1] or np.asanyarray(yj) > shape[0]: |
| 158 | + return True |
| 159 | + else: |
| 160 | + return False |
| 161 | + |
| 162 | + |
| 163 | +def _res(transform: rio.transform.Affine) -> tuple[float, float]: |
| 164 | + """See description of Raster.res""" |
| 165 | + |
| 166 | + return transform[0], abs(transform[4]) |
| 167 | + |
| 168 | + |
| 169 | +def _bounds(transform: rio.transform.Affine, shape: tuple[int, int]) -> rio.coords.BoundingBox: |
| 170 | + """See description of Raster.bounds.""" |
| 171 | + |
| 172 | + return rio.coords.BoundingBox(*rio.transform.array_bounds(height=shape[0], width=shape[1], transform=transform)) |
0 commit comments