"""
Functions for image processing.
"""
import math
from typing import Optional, Tuple, Union
import numpy as np
from scipy.ndimage import fourier_shift, shift, rotate
from skimage.transform import rescale
from typeguard import typechecked
[docs]@typechecked
def center_pixel(image: np.ndarray) -> Tuple[int, int]:
"""
Function to get the pixel position of the image center. Note that this position
can not be unambiguously defined for an even-sized image. Python indexing starts
at 0 so the coordinates of the pixel in the bottom-left corner are (0, 0).
Parameters
----------
image : np.ndarray
Input image (2D or 3D).
Returns
-------
tuple(int, int)
Pixel position (y, x) of the image center.
"""
if image.shape[-2] % 2 == 0 and image.shape[-1] % 2 == 0:
center = (image.shape[-2] // 2 - 1, image.shape[-1] // 2 - 1)
elif image.shape[-2] % 2 == 0 and image.shape[-1] % 2 == 1:
center = (image.shape[-2] // 2 - 1, (image.shape[-1]-1) // 2)
elif image.shape[-2] % 2 == 1 and image.shape[-1] % 2 == 0:
center = ((image.shape[-2] - 1) // 2, image.shape[-1] // 2 - 1)
elif image.shape[-2] % 2 == 1 and image.shape[-1] % 2 == 1:
center = ((image.shape[-2] - 1) // 2, (image.shape[-1] - 1) // 2)
else:
raise RuntimeError('Unexpected image shape. This error should not occur.')
return center
[docs]@typechecked
def center_subpixel(image: np.ndarray) -> Tuple[float, float]:
"""
Function to get the precise position of the image center. The center of the pixel in the
bottom left corner of the image is defined as (0, 0), so the bottom left corner of the
image is located at (-0.5, -0.5).
Parameters
----------
image : np.ndarray
Input image (2D or 3D).
Returns
-------
tuple(float, float)
Subpixel position (y, x) of the image center.
"""
center_x = float(image.shape[-1]) / 2 - 0.5
center_y = float(image.shape[-2]) / 2 - 0.5
return center_y, center_x
[docs]@typechecked
def crop_image(image: np.ndarray,
center: Optional[tuple],
size: int,
copy: bool = True) -> np.ndarray:
"""
Function to crop square images around a specified position.
Parameters
----------
image : np.ndarray
Input image (2D or 3D).
center : tuple(int, int), None
The new image center (y, x). The center of the image is used if set to None.
size : int
Image size (pix) for both dimensions. Increased by 1 pixel if size is an even number.
copy : bool
Whether or not to return a copy (instead of a view) of the cropped image (default: True).
Returns
-------
np.ndarray
Cropped odd-sized image (2D or 3D).
"""
if center is None or (center[0] is None and center[1] is None):
center = center_pixel(image)
# if image.shape[-1] % 2 == 0:
# warnings.warn('The image is even-size so there is not a uniquely defined pixel in '
# 'the center of the image. The image center is determined (with pixel '
# 'precision) with the pynpoint.util.image.center_pixel function.')
if size % 2 == 0:
size += 1
x_start = center[1] - (size - 1) // 2
x_end = center[1] + (size - 1) // 2 + 1
y_start = center[0] - (size - 1) // 2
y_end = center[0] + (size - 1) // 2 + 1
if x_start < 0 or y_start < 0 or x_end > image.shape[-1] or y_end > image.shape[-2]:
raise ValueError('Target image resolution does not fit inside the input image resolution.')
return np.array(image[..., y_start:y_end, x_start:x_end], copy=copy)
[docs]@typechecked
def rotate_images(images: np.ndarray,
angles: np.ndarray) -> np.ndarray:
"""
Function to rotate all images in clockwise direction.
Parameters
----------
images : np.ndarray
Stack of images (3D).
angles : np.ndarray
Rotation angles (deg).
Returns
-------
np.ndarray
Rotated images.
"""
im_rot = np.zeros(images.shape)
for i, item in enumerate(angles):
im_rot[i, ] = rotate(input=images[i, ], angle=item, reshape=False)
return im_rot
[docs]@typechecked
def create_mask(im_shape: Tuple[int, int],
size: Union[Tuple[float, float],
Tuple[float, None],
Tuple[None, float],
Tuple[None, None]]) -> np.ndarray:
"""
Function to create a mask for the central and outer image regions.
Parameters
----------
im_shape : tuple(int, int)
Image size in both dimensions.
size : tuple(float, float)
Size (pix) of the inner and outer mask.
Returns
-------
np.ndarray
Image mask.
"""
mask = np.ones(im_shape)
npix = im_shape[0]
if size[0] is not None or size[1] is not None:
if npix % 2 == 0:
x_grid = y_grid = np.linspace(-npix / 2 + 0.5, npix / 2 - 0.5, npix)
else:
x_grid = y_grid = np.linspace(-(npix - 1) / 2, (npix - 1) / 2, npix)
xx_grid, yy_grid = np.meshgrid(x_grid, y_grid)
rr_grid = np.sqrt(xx_grid**2 + yy_grid**2)
if size[0] is not None:
mask[rr_grid < size[0]] = 0.
if size[1] is not None:
if size[1] > npix / 2:
size = (size[0], npix / 2)
mask[rr_grid > size[1]] = 0.
return mask
[docs]@typechecked
def shift_image(image: np.ndarray,
shift_yx: Union[Tuple[float, float], np.ndarray],
interpolation: str,
mode: str = 'constant') -> np.ndarray:
"""
Function to shift an image.
Parameters
----------
image : np.ndarray
Input image (2D or 3D). If 3D the image is not shifted along the 0th axis.
shift_yx : tuple(float, float), np.ndarray
Shift (y, x) to be applied (pix). An additional shift of zero pixels will be added
for the first dimension in case the input image is 3D.
interpolation : str
Interpolation type ('spline', 'bilinear', or 'fft').
mode : str
Interpolation mode.
Returns
-------
np.ndarray
Shifted image.
"""
if image.ndim == 2:
shift_val = (shift_yx[0], shift_yx[1])
elif image.ndim == 3:
shift_val = (0, shift_yx[0], shift_yx[1])
else:
raise ValueError('Invalid number of dimensions for image: must be 2 or 3')
if interpolation == 'spline':
im_center = shift(image, shift_val, order=5, mode=mode)
elif interpolation == 'bilinear':
im_center = shift(image, shift_val, order=1, mode=mode)
elif interpolation == 'fft':
fft_shift = fourier_shift(np.fft.fftn(image), shift_val)
im_center = np.fft.ifftn(fft_shift).real
else:
raise ValueError('interpolation must be one of the following: spline, bilinear, fft')
return im_center
[docs]@typechecked
def scale_image(image: np.ndarray,
scaling_y: Union[float, np.float32],
scaling_x: Union[float, np.float32]) -> np.ndarray:
"""
Function to spatially scale an image.
Parameters
----------
image : np.ndarray
Input image (2D).
scaling_y : float
Scaling factor y.
scaling_x : float
Scaling factor x.
Returns
-------
np.ndarray
Shifted image (2D).
"""
sum_before = np.sum(image)
im_scale = rescale(image,
(scaling_y, scaling_x),
order=5,
mode='reflect',
channel_axis=None,
anti_aliasing=True)
sum_after = np.sum(im_scale)
return im_scale * (sum_before / sum_after)
[docs]@typechecked
def cartesian_to_polar(center: Tuple[float, float],
y_pos: float,
x_pos: float) -> Tuple[float, float]:
"""
Function to convert pixel coordinates to polar coordinates.
Parameters
----------
center : tuple(float, float)
Image center (y, x) from :func:`~pynpoint.util.image.center_subpixel`.
y_pos : float
Pixel coordinate along the vertical axis. The bottom left corner of the image is
(-0.5, -0.5).
x_pos : float
Pixel coordinate along the horizontal axis. The bottom left corner of the image is
(-0.5, -0.5).
Returns
-------
tuple(float, float)
Separation (pix) and position angle (deg). The angle is measured counterclockwise with
respect to the positive y-axis.
"""
sep = math.sqrt((center[1] - x_pos)**2 + (center[0] - y_pos)**2)
ang = math.atan2(y_pos-center[1], x_pos-center[0])
ang = (math.degrees(ang) - 90) % 360
return sep, ang
[docs]@typechecked
def polar_to_cartesian(image: np.ndarray,
sep: float,
ang: float) -> Tuple[float, float]:
"""
Function to convert polar coordinates to pixel coordinates.
Parameters
----------
image : np.ndarray
Input image (2D or 3D).
sep : float
Separation (pixels).
ang : float
Position angle (deg), measured counterclockwise with respect to the positive y-axis.
Returns
-------
tuple(float, float)
Cartesian coordinates (y, x). The bottom left corner of the image is (-0.5, -0.5).
"""
center = center_subpixel(image) # (y, x)
x_pos = center[1] + sep * math.cos(math.radians(ang + 90))
y_pos = center[0] + sep * math.sin(math.radians(ang + 90))
return y_pos, x_pos
[docs]@typechecked
def pixel_distance(im_shape: Tuple[int, int],
position: Optional[Tuple[int, int]] = None) -> Tuple[
np.ndarray, np.ndarray, np.ndarray]:
"""
Function to calculate the distance of each pixel with respect to a given pixel position.
Supports both odd and even sized images.
Parameters
----------
im_shape : tuple(int, int)
Image shape (y, x).
position : tuple(int, int)
Pixel center (y, x) from which the distance is calculated. The image center is used if set
to None. Python indexing starts at zero so the center of the bottom left pixel is (0, 0).
Returns
-------
np.ndarray
2D array with the distances of each pixel from the provided pixel position.
np.ndarray
2D array with the x coordinates.
np.ndarray
2D array with the y coordinates.
"""
if im_shape[0] % 2 == 0:
y_grid = np.linspace(-im_shape[0] / 2 + 0.5, im_shape[0] / 2 - 0.5, im_shape[0])
else:
y_grid = np.linspace(-(im_shape[0] - 1) / 2, (im_shape[0] - 1) / 2, im_shape[0])
if im_shape[1] % 2 == 0:
x_grid = np.linspace(-im_shape[1] / 2 + 0.5, im_shape[1] / 2 - 0.5, im_shape[1])
else:
x_grid = np.linspace(-(im_shape[1] - 1) / 2, (im_shape[1] - 1) / 2, im_shape[1])
if position is not None:
y_shift = y_grid[position[0]]
x_shift = x_grid[position[1]]
y_grid -= y_shift
x_grid -= x_shift
xx_grid, yy_grid = np.meshgrid(x_grid, y_grid)
return np.sqrt(xx_grid**2 + yy_grid**2), xx_grid, yy_grid
[docs]@typechecked
def subpixel_distance(im_shape: Tuple[int, int],
position: Tuple[float, float],
shift_center: bool = True) -> np.ndarray:
"""
Function to calculate the distance of each pixel with respect to a given subpixel position.
Supports both odd and even sized images.
Parameters
----------
im_shape : tuple(int, int)
Image shape (y, x).
position : tuple(float, float)
Pixel center (y, x) from which the distance is calculated. Python indexing starts at zero
so the bottom left image corner is (-0.5, -0.5).
shift_center : bool
Apply the coordinate correction for the image center.
Returns
-------
np.ndarray
2D array with the distances of each pixel from the provided pixel position.
"""
# Get 2D x and y coordinates with respect to the image center
_, xx_grid, yy_grid = pixel_distance(im_shape, position=None)
if im_shape[0] % 2 == 0:
# Distance from the image center to the center of the outermost pixel
# Even sized images
y_size = im_shape[0] / 2 + 0.5
x_size = im_shape[1] / 2 + 0.5
else:
# Distance from the image center to the center of the outermost pixel
# Odd sized images
y_size = (im_shape[0] - 1) / 2
x_size = (im_shape[1] - 1) / 2
if shift_center:
# Shift the image center to the center of the bottom left pixel
yy_grid += y_size
xx_grid += x_size
# Apply a subpixel shift of the coordinate system to the requested position
yy_grid -= position[0]
xx_grid -= position[1]
return np.sqrt(xx_grid**2 + yy_grid**2)
[docs]@typechecked
def select_annulus(image_in: np.ndarray,
radius_in: float,
radius_out: float,
mask_position: Optional[Tuple[float, float]] = None,
mask_radius: Optional[float] = None) -> np.ndarray:
"""
image_in : np.ndarray
Input image.
radius_in : float
Inner radius of the annulus (pix).
radius_out : float
Outer radius of the annulus (pix).
mask_position : tuple(float, float), None
Center (pix) position (y, x) in of the circular region that is excluded. Not used
if set to None.
mask_radius : float, None
Radius (pix) of the circular region that is excluded. Not used if set to None.
"""
im_shape = image_in.shape
if im_shape[0] % 2 == 0:
y_grid = np.linspace(-im_shape[0] / 2 + 0.5, im_shape[0] / 2 - 0.5, im_shape[0])
else:
y_grid = np.linspace(-(im_shape[0] - 1) / 2, (im_shape[0] - 1) / 2, im_shape[0])
if im_shape[1] % 2 == 0:
x_grid = np.linspace(-im_shape[1] / 2 + 0.5, im_shape[1] / 2 - 0.5, im_shape[1])
else:
x_grid = np.linspace(-(im_shape[1] - 1) / 2, (im_shape[1] - 1) / 2, im_shape[1])
xx_grid, yy_grid = np.meshgrid(x_grid, y_grid)
rr_grid = np.sqrt(xx_grid**2 + yy_grid**2)
mask = np.ones(im_shape)
indices = np.where((rr_grid < radius_in) | (rr_grid > radius_out))
mask[indices[0], indices[1]] = 0.
if mask_position is not None and mask_radius is not None:
distance = subpixel_distance(im_shape=im_shape, position=mask_position)
indices = np.where(distance < mask_radius)
mask[indices[0], indices[1]] = 0.
indices = np.where(mask == 1.)
return image_in[indices[0], indices[1]]
[docs]@typechecked
def rotate_coordinates(center: Tuple[float, float],
position: Union[Tuple[float, float], np.ndarray],
angle: float) -> Tuple[float, float]:
"""
Function to rotate coordinates around the image center.
Parameters
----------
center : tuple(float, float)
Image center (y, x) with subpixel accuracy.
position : tuple(float, float)
Position (y, x) in the image, or a 2D numpy array of positions.
angle : float
Angle (deg) to rotate in counterclockwise direction.
Returns
-------
tuple(float, float)
New position (y, x).
"""
pos_y = (position[1] - center[1]) * math.sin(np.radians(angle)) + \
(position[0] - center[0]) * math.cos(np.radians(angle))
pos_x = (position[1] - center[1]) * math.cos(np.radians(angle)) - \
(position[0] - center[0]) * math.sin(np.radians(angle))
return center[0]+pos_y, center[1]+pos_x