Source code for pyxel.util.image

#  Copyright (c) European Space Agency, 2020.
#
#  This file is subject to the terms and conditions defined in file 'LICENCE.txt', which
#  is part of this Pyxel package. No part of the package, including
#  this file, may be copied, modified, propagated, or distributed except according to
#  the terms contained in the file ‘LICENCE.txt’.


"""Utility functions for images."""

import sys
from enum import Enum
from functools import lru_cache
from pathlib import Path
from typing import Literal

import numpy as np


class Alignment(Enum):
    """Alignment class."""

    center = "center"
    top_left = "top_left"
    top_right = "top_right"
    bottom_left = "bottom_left"
    bottom_right = "bottom_right"


def _set_relative_position(
    array_x: int,
    array_y: int,
    output_x: int,
    output_y: int,
    alignment: Alignment,
) -> tuple[int, int]:
    """Calculate relative position of (0, 0) pixels for two different array shapes based on desired alignment.

    Parameters
    ----------
    output_y: int
    output_x: int
    array_y: int
    array_x: int
    alignment: Alignment

    Returns
    -------
    tuple
    """
    if alignment == Alignment.center:
        return int((output_y - array_y) / 2), int((output_x - array_x) / 2)
    elif alignment == Alignment.top_left:
        return output_y - array_y, 0
    elif alignment == Alignment.top_right:
        return output_y - array_y, output_x - array_x
    elif alignment == Alignment.bottom_left:
        return 0, 0
    elif alignment == Alignment.bottom_right:
        return 0, output_x - array_x
    else:
        raise NotImplementedError


[docs] def fit_into_array( array: np.ndarray, output_shape: tuple[int, ...], relative_position: tuple[int, int] = (0, 0), align: ( Literal["center", "top_left", "top_right", "bottom_left", "bottom_right"] | None ) = None, allow_smaller_array: bool = True, ) -> np.ndarray: """Fit input array into an output array of specified output shape. Input array can be either larger or smaller than output array. In the first case the input array will be cropped. The relative position between the arrays in the coordinate system is specified with argument relative_position. It is a tuple with coordinates (Y,X). User can use this argument to specify the position of input array values in the output array. If arrays are to be aligned in the center or one of the corners, it is also possible with argument align and passing a location keyword. User can turn on that smaller arrays than output shape are not allowed by setting allow_smaller_array to False. Parameters ---------- array: ndarray output_shape: tuple relative_position: tuple align: {'center', 'top_left', 'top_right', 'bottom_left', 'bottom_right'}, default: None allow_smaller_array: bool Returns ------- output: ndarray """ array_y, array_x = array.shape output = np.zeros(output_shape) output_y, output_x = output_shape if not allow_smaller_array and (array_y < output_y or array_x < output_x): raise ValueError("Input array too small to fit into the desired shape!.") if align: relative_position = _set_relative_position( array_x=array_x, array_y=array_y, output_x=output_x, output_y=output_y, alignment=Alignment(align), ) array_y_coordinates = np.array( range(relative_position[0], relative_position[0] + array_y) ) array_x_coordinates = np.array( range(relative_position[1], relative_position[1] + array_x) ) output_y_coordinates = np.array(range(output_y)) output_x_coordinates = np.array(range(output_x)) overlap_y = np.intersect1d(array_y_coordinates, output_y_coordinates) overlap_x = np.intersect1d(array_x_coordinates, output_x_coordinates) if overlap_y.size == overlap_x.size == 0: raise ValueError("No overlap of array and target in Y and X dimension.") elif overlap_y.size == 0: raise ValueError("No overlap of array and target in Y dimension.") elif overlap_x.size == 0: raise ValueError("No overlap of array and target in X dimension.") cropped_array = array[ slice( overlap_y[0] - relative_position[0], overlap_y[-1] + 1 - relative_position[0], ), slice( overlap_x[0] - relative_position[1], overlap_x[-1] + 1 - relative_position[1], ), ] output[ slice(overlap_y[0], overlap_y[-1] + 1), slice(overlap_x[0], overlap_x[-1] + 1) ] = cropped_array return output
@lru_cache(maxsize=128) # One must add parameter 'maxsize' for Python 3.7 def load_cropped_and_aligned_image( shape: tuple[int, ...], filename: str | Path, position_x: int = 0, position_y: int = 0, align: ( Literal["center", "top_left", "top_right", "bottom_left", "bottom_right"] | None ) = None, allow_smaller_array: bool = True, ) -> np.ndarray: """Load image from file and fit to detector shape. Parameters ---------- shape: tuple Detector shape. filename: str Path to image file. position_x: tuple Index of starting column, used when fitting image to detector. position_y: tuple Index of starting row, used when fitting image to detector. align: Literal Keyword to align the image to detector. Can be any from: ("center", "top_left", "top_right", "bottom_left", "bottom_right") Returns ------- cropped_and_aligned_image: ndarray """ # Load 2d image (which can be smaller or # larger in dimensions than detector imaging area) from pyxel.inputs import load_image try: image_2d: np.ndarray = load_image(filename) except OSError as exc: if sys.version_info >= (3, 11): exc.add_note(f"Cannot open filename: '{filename}'") raise else: raise OSError(f"Cannot open filename: '{filename}'") from exc cropped_and_aligned_image: np.ndarray = fit_into_array( array=image_2d, output_shape=shape, relative_position=(position_y, position_x), align=align, allow_smaller_array=allow_smaller_array, ) # Set this array as read-only. It avoids a lot of problems with 'lru_cache' cropped_and_aligned_image.setflags(write=False) return cropped_and_aligned_image