"""
Useful utilities for working with raster data, typically `numpy` arrays.
Copyright (c) 2017- Sinergise and contributors
For the full list of contributors, see the CREDITS file in the root directory of this source tree.
This source code is licensed under the MIT license, see the LICENSE file in the root directory of this source tree.
"""
from __future__ import annotations
from typing import Literal
import numpy as np
[docs]def fast_nanpercentile(data: np.ndarray, percentile: float, *, method: str = "linear") -> np.ndarray:
"""This is an alternative implementation of `numpy.nanpercentile`. For cases where the size of the first dimension
is relatively small compared to the size of the entire array it works much faster than the original.
The algorithm divides pixel data over the first axis into groups by how many NaN values they have. In each group
NaN values are removed and `numpy.percentile` function is applied. If any series contains only NaN values also any
percentile of that series will be NaN.
This function differs from `numpy` implementations only in the following:
- In case the size of the first dimension of `data` is `0` this method will still return an output array with
all NaN values. This matches with `numpy.nanpercentile` while `numpy.percentile` raises an error.
- The output dtype of this method will be always the same as the input dtype while `numpy` implementations
in many cases use `float64` as the output dtype.
:param data: An array for which percentiles will be calculated along the first axis.
:param percentile: A percentile to compute, which must be between `0` and `100` inclusive.
:param method: A method for estimating the percentile. This parameter is propagated to `numpy.percentile`.
:return: An array of percentiles and a shape equal to the shape of `data` array without the first dimension.
"""
method_kwargs = {"method" if np.__version__ >= "1.22.0" else "interpolation": method}
combined_data = np.zeros(data.shape[1:], dtype=data.dtype)
no_data_counts = np.count_nonzero(np.isnan(data), axis=0)
for no_data_num in np.unique(no_data_counts):
mask = no_data_counts == no_data_num
chunk = data[..., mask]
time_size, sample_size = chunk.shape
if time_size == no_data_num:
result = np.full(sample_size, np.nan, dtype=chunk.dtype)
else:
chunk = chunk.flatten(order="F")
chunk = chunk[~np.isnan(chunk)]
chunk = chunk.reshape((time_size - no_data_num, sample_size), order="F")
result = np.percentile(chunk, q=percentile, axis=0, **method_kwargs) # type: ignore[call-overload]
combined_data[mask] = result
return combined_data
[docs]def constant_pad( # noqa: C901
array: np.ndarray,
multiple_of: tuple[int, int],
up_down_rule: Literal["even", "up", "down"] = "even",
left_right_rule: Literal["even", "left", "right"] = "even",
pad_value: float = 0,
) -> np.ndarray:
"""Function pads an image of shape (rows, columns, channels) with zeros.
It pads an image so that the shape becomes (rows + padded_rows, columns + padded_columns, channels), where
padded_rows = (int(rows/multiple_of[0]) + 1) * multiple_of[0] - rows
Same rule is applied to columns.
:param array: Array with shape `(rows, columns, ...)` to be padded.
:param multiple_of: make array' rows and columns multiple of this tuple
:param up_down_rule: Add padded rows evenly to the top/bottom of the image, or up (top) / down (bottom) only
:param left_right_rule: Add padded columns evenly to the left/right of the image, or left / right only
:param pad_value: Value to be assigned to padded rows and columns
"""
rows, columns = array.shape[:2]
row_padding, col_padding = 0, 0
if rows % multiple_of[0]:
row_padding = (int(rows / multiple_of[0]) + 1) * multiple_of[0] - rows
if columns % multiple_of[1]:
col_padding = (int(columns / multiple_of[1]) + 1) * multiple_of[1] - columns
row_padding_up, row_padding_down, col_padding_left, col_padding_right = 0, 0, 0, 0
if row_padding > 0:
if up_down_rule == "up":
row_padding_up = row_padding
elif up_down_rule == "down":
row_padding_down = row_padding
elif up_down_rule == "even":
row_padding_up = int(row_padding / 2)
row_padding_down = row_padding_up + (row_padding % 2)
else:
raise ValueError("Padding rule for rows not supported. Choose between even, down or up!")
if col_padding > 0:
if left_right_rule == "left":
col_padding_left = col_padding
elif left_right_rule == "right":
col_padding_right = col_padding
elif left_right_rule == "even":
col_padding_left = int(col_padding / 2)
col_padding_right = col_padding_left + (col_padding % 2)
else:
raise ValueError("Padding rule for columns not supported. Choose between even, left or right!")
return np.lib.pad(
array,
((row_padding_up, row_padding_down), (col_padding_left, col_padding_right)),
"constant",
constant_values=((pad_value, pad_value), (pad_value, pad_value)),
)