Source code for stimupy.utils.pad

import copy

import numpy as np

from .utils import resolution

__all__ = [
    "add_padding",
    "remove_padding",
    "pad_by_visual_size",
    "pad_to_visual_size",
    "pad_by_shape",
    "pad_to_shape",
    "pad_dict_by_visual_size",
    "pad_dict_to_visual_size",
    "pad_dict_by_shape",
    "pad_dict_to_shape",
]


[docs] def remove_padding(arr, c): """ Remove padding by c Parameters ---------- arr : numpy.ndarray input array c : int padding amount. Returns ------- arr : numpy.ndarray reduced array """ array_shape = arr.shape arr = arr[c : array_shape[0] - c, c : array_shape[1] - c] return arr
[docs] def add_padding(arr, c, val): """ Remove padding by c Parameters ---------- arr : numpy.ndarray input array c : int padding amount val : float background value Returns ------- arr : numpy.ndarray padded array """ h, w = arr.shape new_arr = np.ones([h + c * 2, w + c * 2]) * val new_arr[c : h + c, c : w + c] = arr return new_arr
[docs] def pad_by_visual_size(img, padding, ppd, pad_value=0.0): """Pad image by specified degrees of visual angle Can specify different amount (before, after) each axis. Parameters ---------- img : numpy.ndarray image-array to be padded padding : float, or Sequence[float, float], or Sequence[Sequence[float, float], ...] amount of padding, in degrees visual angle, in each direction: ((before_1, after_1), … (before_N, after_N)) unique pad widths for each axis (float,) or float is a shortcut for before = after = pad width for all axes. ppd : Sequence[Number] or Sequence[Number, Number] pixels per degree pad_value : Numeric, optional value to pad with, by default 0.0 Returns ------- numpy.ndarray img padded by the specified amount(s) See also --------- stimupy.utils.resolution """ # Broadcast to ((before_1, after_1),...(before_N, after_N)) padding_degs = np.broadcast_to(padding, (img.ndim, 2)) # ppd in canonical form ppd = resolution.validate_ppd(ppd) # Convert to shape in pixels padding_px = [] for axis in padding_degs: shape = [ resolution.length_from_visual_angle_ppd(i, ppd) if i > 0 else 0 for i, ppd in zip(axis, ppd) ] padding_px.append(shape) # Pad by shape in pixels return pad_by_shape(img=img, padding=padding_px, pad_value=pad_value)
[docs] def pad_to_visual_size(img, visual_size, ppd, pad_value=0): """Pad image to specified visual size in degrees visual angle Parameters ---------- img : numpy.ndarray image-array to be padded visual_size : Sequence[int, int, ...] desired visual size (in degrees visual angle) of img after padding ppd : Sequence[Number] or Sequence[Number, Number] pixels per degree pad_value : Numeric, optional value to pad with, by default 0.0 Returns ------- numpy.ndarray img padded by the specified amount(s) See also --------- stimupy.utils.resolution """ # visual_size to shape shape = resolution.shape_from_visual_size_ppd(visual_size=visual_size, ppd=ppd) # pad return pad_to_shape(img=img, shape=shape, pad_value=pad_value)
[docs] def pad_by_shape(img, padding, pad_value=0): """Pad image by specified amount(s) of pixels Can specify different amount (before, after) each axis. Parameters ---------- img : numpy.ndarray image-array to be padded padding : int, or Sequence[int, int], or Sequence[Sequence[int, int], ...] amount of padding, in pixels, in each direction: ((before_1, after_1), … (before_N, after_N)) unique pad widths for each axis (int,) or int is a shortcut for before = after = pad width for all axes. pad_val : float, optional value to pad with, by default 0.0 Returns ------- numpy.ndarray img padded by the specified amount(s) """ # Ensure padding is in integers padding = np.array(padding, dtype=np.int32) return np.pad(img, padding, mode="constant", constant_values=pad_value)
[docs] def pad_to_shape(img, shape, pad_value=0): """Pad image to a resulting specified shape in pixels Parameters ---------- img : numpy.ndarray image-array to be padded shape : Sequence[int, int, ...] desired shape of img after padding pad_value : float, optional value to pad with, by default 0.0 Returns ------- numpy.ndarray img padded to specified shape Raises ------ ValueError if img.shape already exceeds shape """ if np.any(img.shape > shape): raise ValueError("img is bigger than size after padding") padding_per_axis = np.array(shape) - np.array(img.shape) padding_before = padding_per_axis // 2 padding_after = padding_per_axis - padding_before padding = np.stack([padding_before, padding_after]).T return pad_by_shape( img, padding=padding, pad_value=pad_value, )
# %% ################################# # Dictionaries # #################################
[docs] def pad_dict_by_visual_size(dct, padding, ppd, pad_value=0.0, keys=("img", "*mask")): """Pad images in dictionary by specified degrees of visual angle Can specify different amount (before, after) each axis. Parameters ---------- dct : dict dict containing image-arrays to be padded padding : float, or Sequence[float, float], or Sequence[Sequence[float, float], ...] amount of padding, in degrees visual angle, in each direction: ((before_1, after_1), … (before_N, after_N)) unique pad widths for each axis (float,) or float is a shortcut for before = after = pad width for all axes. ppd : Sequence[Number] or Sequence[Number, Number] pixels per degree pad_value : Numeric, optional value to pad with, by default 0.0 keys : Sequence[String, String] or String keys in dict for images to be padded Returns ------- dict[str, Any] same as input dict but with larger key-arrays and updated keys for "visual_size" and "shape" """ # Create deepcopy to not override existing dict new_dict = copy.deepcopy(dct) if isinstance(keys, str): keys = (keys,) # Find relevant keys keys = [ dkey for key in keys for dkey in dct.keys() if ((dkey == key) or ((dkey.endswith(key[1::])) and (key.startswith("*")))) ] for key in dct.keys(): if key in keys: img = dct[key] if isinstance(img, np.ndarray): # Add mask which indicates padded region new_dict["pad_mask"] = pad_by_visual_size( np.zeros(img.shape), padding, ppd, 1 ).astype(int) if key.endswith("mask"): img = pad_by_visual_size(img, padding, ppd, 0) img = img.astype(int) else: img = pad_by_visual_size(img, padding, ppd, pad_value) new_dict[key] = img # Update resolution new_dict["visual_size"] = resolution.visual_size_from_shape_ppd(img.shape, ppd) new_dict["shape"] = resolution.validate_shape(img.shape) return new_dict
[docs] def pad_dict_to_visual_size(dct, visual_size, ppd, pad_value=0, keys=("img", "*mask")): """Pad images in dictionary to specified visual size in degrees visual angle Parameters ---------- dct : dict dict containing image-arrays to be padded visual_size : Sequence[int, int, ...] desired visual size (in degrees visual angle) of img after padding ppd : Sequence[Number] or Sequence[Number, Number] pixels per degree pad_value : Numeric, optional value to pad with, by default 0.0 keys : Sequence[String, String] or String keys in dict for images to be padded Returns ------- dict[str, Any] same as input dict but with larger key-arrays and updated keys for "visual_size" and "shape" """ # visual_size to shape shape = resolution.shape_from_visual_size_ppd(visual_size=visual_size, ppd=ppd) # pad padded_dict = pad_dict_to_shape(dct=dct, shape=shape, pad_value=pad_value, keys=keys) # update resolution padded_dict["ppd"] = ppd padded_dict["visual_size"] = visual_size return padded_dict
[docs] def pad_dict_by_shape(dct, padding, pad_value=0, keys=("img", "*mask")): """Pad images in dictionary by specified amount(s) of pixels Can specify different amount (before, after) each axis. Parameters ---------- dct : dict dict containing image-arrays to be padded padding : int, or Sequence[int, int], or Sequence[Sequence[int, int], ...] amount of padding, in pixels, in each direction: ((before_1, after_1), … (before_N, after_N)) unique pad widths for each axis (int,) or int is a shortcut for before = after = pad width for all axes. pad_val : float, optional value to pad with, by default 0.0 keys : Sequence[String, String] or String keys in dict for images to be padded Returns ------- dict[str, Any] same as input dict but with larger key-arrays and updated keys for "visual_size" and "shape" """ # Ensure padding is in integers padding = np.array(padding, dtype=np.int32) # Create deepcopy to not override existing dict new_dict = copy.deepcopy(dct) if isinstance(keys, str): keys = (keys,) # Find relevant keys keys = [ dkey for key in keys for dkey in dct.keys() if ((dkey == key) or ((dkey.endswith(key[1::])) and (key.startswith("*")))) ] for key in dct.keys(): if key in keys: img = dct[key] if isinstance(img, np.ndarray): # Add mask which indicates padded region new_dict["pad_mask"] = np.pad( np.zeros(img.shape), padding, mode="constant", constant_values=1 ).astype(int) if key.endswith("mask"): img = np.pad(img, padding, mode="constant", constant_values=0) img = img.astype(int) else: img = np.pad(img, padding, mode="constant", constant_values=pad_value) new_dict[key] = img # Update visual_size and shape-keys new_dict["shape"] = resolution.validate_shape(img.shape) if "ppd" in dct.keys(): new_dict["visual_size"] = resolution.visual_size_from_shape_ppd(img.shape, dct["ppd"]) return new_dict
[docs] def pad_dict_to_shape(dct, shape, pad_value=0, keys=("img", "*mask")): """Pad images in dictionary to a resulting specified shape in pixels Parameters ---------- dct : dict dict containing image-arrays to be padded shape : Sequence[int, int, ...] desired shape of img after padding pad_value : float, optional value to pad with, by default 0.0 keys : Sequence[String, String] or String keys in dict for images to be padded Returns ------- dict[str, Any] same as input dict but with larger key-arrays and updated keys for "visual_size" and "shape" Raises ------ ValueError if img.shape already exceeds shape """ # Create deepcopy to not override existing dict new_dict = copy.deepcopy(dct) if isinstance(keys, str): keys = (keys,) # Find relevant keys keys = [ dkey for key in keys for dkey in dct.keys() if ((dkey == key) or ((dkey.endswith(key[1::])) and (key.startswith("*")))) ] for key in dct.keys(): if key in keys: img = dct[key] if isinstance(img, np.ndarray): if np.any(img.shape > shape): raise ValueError("img is bigger than size after padding") padding_per_axis = np.array(shape) - np.array(img.shape) padding_before = padding_per_axis // 2 padding_after = padding_per_axis - padding_before padding = np.stack([padding_before, padding_after]).T # Add mask which indicates padded region new_dict["pad_mask"] = pad_by_shape( np.zeros(img.shape), padding=padding, pad_value=1 ).astype(int) if key.endswith("mask"): img = pad_by_shape(img, padding=padding, pad_value=0) img = img.astype(int) else: img = pad_by_shape(img, padding=padding, pad_value=pad_value) new_dict[key] = img # Update resolution new_dict["shape"] = resolution.validate_shape(shape) if "ppd" in dct.keys(): new_dict["visual_size"] = resolution.visual_size_from_shape_ppd(shape, dct["ppd"]) return new_dict