"""
Copyright (c) 2010-2023 CNRS / Centre de Recherche Astrophysique de Lyon
Copyright (c) 2012-2023 Laure Piqueras <laure.piqueras@univ-lyon1.fr>
Copyright (c) 2012-2017 Roland Bacon <roland.bacon@univ-lyon1.fr>
Copyright (c) 2014-2016 Johan Richard <jrichard@univ-lyon1.fr>
Copyright (c) 2014-2019 Simon Conseil <simon.conseil@univ-lyon1.fr>
Copyright (c) 2015 Floriane Leclercq <floriane.leclercq@univ-lyon1.fr>
Copyright (c) 2016 Martin Shepherd <martin.shepherd@univ-lyon1.fr>
Copyright (c) 2017 Ole Streicher <ole@aip.de>
Copyright (c) 2018-2019 David Carton <cartondj@gmail.com>
Copyright (c) 2018 Yannick Roehlly <yannick.roehlly@univ-lyon1.fr>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import numpy as np
from numpy import ma
import astropy.units as u
from astropy.io import fits
from astropy.stats import gaussian_sigma_to_fwhm, gaussian_fwhm_to_sigma
from scipy import interpolate, signal
from scipy import ndimage as ndi
from scipy.ndimage import affine_transform
from scipy.optimize import leastsq
import warnings
from datetime import datetime
from .arithmetic import ArithmeticMixin
from .coords import WCS
from .data import DataArray
from .fitting import Gauss2D, Moffat2D
from .objs import is_int, is_number, bounding_box, UnitMaskedArray, UnitArray
from .plot import FormatCoord, get_plot_norm
from ..tools import MpdafWarning
__all__ = ('Image', 'gauss_image', 'moffat_image', 'SpatialFrequencyLimits')
[docs]
class Image(ArithmeticMixin, DataArray):
"""Manage image, optionally including a variance and a bad pixel mask.
Parameters
----------
filename : str
Possible filename (.fits, .png or .bmp).
ext : int or (int,int) or string or (string,string)
Number/name of the data extension or numbers/names
of the data and variance extensions.
wcs : `mpdaf.obj.WCS`
World coordinates.
unit : str or `astropy.units.Unit`
The physical units of the data values. Defaults to
`astropy.units.dimensionless_unscaled`.
data : float array
Array containing the pixel values of the image. None by default.
var : float array
Array containing the variance. None by default.
copy : bool
If true (default), then the data and variance arrays are copied.
dtype : numpy.dtype
Type of the data (int, float)
Attributes
----------
filename : str
Possible FITS filename.
primary_header : `astropy.io.fits.Header`
FITS primary header instance.
data_header : `astropy.io.fits.Header`
FITS data header instance.
wcs : `mpdaf.obj.WCS`
World coordinates.
unit : `astropy.units.Unit`
Physical units of the data values.
dtype : numpy.dtype
Type of the data (int, float)
"""
_ndim_required = 2
_has_wcs = True
def __init__(self, filename=None, ext=None, wcs=None, data=None, var=None,
unit=u.dimensionless_unscaled, copy=True, dtype=None,
**kwargs):
self._spflims = None
super(Image, self).__init__(
filename=filename, ext=ext, wcs=wcs, unit=unit, data=data, var=var,
copy=copy, dtype=dtype, **kwargs)
[docs]
def copy(self):
"""Return a new copy of an Image object."""
obj = super(Image, self).copy()
# Make a deep copy of the spatial-frequency limits.
if self._spflims is not None:
obj._spflims = self._spflims.deepcopy()
return obj
[docs]
def get_step(self, unit=None):
"""Return the angular height and width of a pixel along the
Y and X axes of the image array.
In MPDAF, images are sampled on a regular grid of square
pixels that represent a flat projection of the celestial
sphere. The get_step() method returns the angular width and
height of these pixels on the sky.
See also get_axis_increments().
Parameters
----------
unit : `astropy.units.Unit`
The angular units of the returned values.
Returns
-------
out : numpy.ndarray
(dy,dx). These are the angular height and width of pixels
along the Y and X axes of the image. The returned values are
either in the unit specified by the 'unit' input parameter,
or in the unit specified by the self.unit property.
"""
if self.wcs is not None:
return self.wcs.get_step(unit)
[docs]
def write_mask_file(self, filename, invert=False):
"""
Save the image mask as a FITS file with a primary header containing WCS information.
The mask values are interpreted such that:
- a pixel value of 0 indicates that the pixel is not masked
- a pixel value of 1 indicates that the pixel is masked
Parameters
----------
filename : str
The name of the output FITS file.
invert : bool, optional
If True, invert the values of the mask (i.e. set masked pixels to 0 instead of 1).
"""
hdu = fits.PrimaryHDU()
hdu.header.update(self.wcs.to_header())
hdu.header['date'] = (str(datetime.now()), 'creation date')
hdu.header['author'] = ('MPDAF', 'origin of the file')
data = (self.mask ^ invert).astype(int)
hdu.data = data
hdu.writeto(filename, overwrite=True)
[docs]
def get_axis_increments(self, unit=None):
"""Return the displacements on the sky that result from
incrementing the array indexes of the image by one along the Y
and X axes, respectively.
In MPDAF, images are sampled on a regular grid of square
pixels that represent a flat projection of the celestial
sphere. The get_axis_increments() method returns the angular
width and height of these pixels on the sky, with signs that
indicate whether the angle increases or decreases as one
increments the array indexes. To keep plots consistent,
regardless of the rotation angle of the image on the sky, the
returned height is always positive, but the returned width is
negative if a plot of the image with pixel 0,0 at the bottom
left would place east anticlockwise of north, and positive
otherwise.
Parameters
----------
unit : `astropy.units.Unit`
The angular units of the returned values.
Returns
-------
out : numpy.ndarray
(dy,dx). These are the angular increments of pixels along
the Y and X axes of the image. The returned values are
either in the unit specified by the 'unit' input parameter,
or in the unit specified by the self.unit property.
"""
if self.wcs is not None:
return self.wcs.get_axis_increments(unit)
[docs]
def get_range(self, unit=None):
"""Return the minimum and maximum right-ascensions and declinations
in the image array.
Specifically a list is returned with the following contents:
[dec_min, ra_min, dec_max, ra_max]
Note that if the Y axis of the image is not parallel to the
declination axis, then the 4 returned values will all come
from different corners of the image. In particular, note that
this means that the coordinates [dec_min,ra_min] and
[dec_max,ra_max] will only coincide with pixels in the image
if the Y axis is aligned with the declination axis. Otherwise
they will be outside the bounds of the image.
Parameters
----------
unit : `astropy.units.Unit`
The units of the returned angles.
Returns
-------
out : numpy.ndarray
The range of right ascensions and declinations, arranged as
[dec_min, ra_min, dec_max, ra_max]. The returned values are
either in the units specified in the 'unit' input parameter,
or in the units stored in the self.unit property.
"""
if self.wcs is not None:
return self.wcs.get_range(unit)
[docs]
def get_start(self, unit=None):
"""Return [y,x] corresponding to pixel (0,0).
Parameters
----------
unit : `astropy.units.Unit`
type of the world coordinates
Returns
-------
out : float array
"""
if self.wcs is not None:
return self.wcs.get_start(unit)
[docs]
def get_end(self, unit=None):
"""Return [y,x] corresponding to pixel (-1,-1).
Parameters
----------
unit : `astropy.units.Unit`
type of the world coordinates
Returns
-------
out : float array
"""
if self.wcs is not None:
return self.wcs.get_end(unit)
[docs]
def get_rot(self, unit=u.deg):
"""Return the rotation angle of the image, defined such that a
rotation angle of zero aligns north along the positive Y axis,
and a positive rotation angle rotates north away from the Y
axis, in the sense of a rotation from north to east.
Note that the rotation angle is defined in a flat
map-projection of the sky. It is what would be seen if
the pixels of the image were drawn with their pixel
widths scaled by the angular pixel increments returned
by the get_axis_increments() method.
Parameters
----------
unit : `astropy.units.Unit`
The unit to give the returned angle (degrees by default).
Returns
-------
out : float
The angle between celestial north and the Y axis of
the image, in the sense of an eastward rotation of
celestial north from the Y-axis.
"""
if self.wcs is not None:
return self.wcs.get_rot(unit)
[docs]
def mask_region(self, center, radius, unit_center=u.deg,
unit_radius=u.arcsec, inside=True, posangle=0.0):
"""Mask values inside or outside a circular or rectangular region.
Parameters
----------
center : (float,float)
Center (y,x) of the region, where y,x are usually celestial
coordinates along the Y and X axes of the image, but are
interpretted as Y,X array-indexes if unit_center is changed
to None.
radius : float or (float,float)
The radius of a circular region, or the half-width and
half-height of a rectangular region, respectively.
unit_center : `astropy.units.Unit`
The units of the coordinates of the center argument
(degrees by default). If None, the units of the center
argument are assumed to be pixels.
unit_radius : `astropy.units.Unit`
The units of the radius argument (arcseconds by default).
If None, the units are assumed to be pixels.
inside : bool
If inside is True, pixels inside the region are masked.
If inside is False, pixels outside the region are masked.
posangle : float
When the region is rectangular, this is the counter-clockwise
rotation angle of the rectangle in degrees. When posangle is
0.0 (the default), the X and Y axes of the ellipse are along
the X and Y axes of the image.
"""
center = np.array(center)
# If the radius argument is a scalar value, this requests
# that a circular region be masked. Delegate this to mask_ellipse().
if np.isscalar(radius):
return self.mask_ellipse(center=center, radius=radius,
posangle=0.0, unit_center=unit_center,
unit_radius=unit_radius, inside=inside)
if unit_center is not None:
center = self.wcs.sky2pix(center, unit=unit_center)[0]
# Get the pixel sizes in the units of the radius argument.
if unit_radius is None:
step = np.array([1.0, 1.0]) # Pixel counts
else:
step = self.wcs.get_step(unit=unit_radius)
# Treat rotated rectangles as polygons.
if not np.isclose(posangle, 0.0):
cos = np.cos(np.radians(posangle))
sin = np.sin(np.radians(posangle))
hw, hh = radius
poly = np.array([[-hw * sin - hh * cos, -hw * cos + hh * sin],
[-hw * sin + hh * cos, -hw * cos - hh * sin],
[+hw * sin + hh * cos, +hw * cos - hh * sin],
[+hw * sin - hh * cos, +hw * cos + hh * sin]])
return self.mask_polygon(poly / step + center, unit=None,
inside=inside)
# Get Y-axis and X-axis slice objects that bound the rectangular area.
sy, sx = bounding_box(form="rectangle", center=center,
radii=radius, shape=self.shape, step=step)[0]
if inside:
self.data[sy, sx] = np.ma.masked
else:
self.data[0:sy.start, :] = np.ma.masked
self.data[sy.stop:, :] = np.ma.masked
self.data[sy, 0:sx.start] = np.ma.masked
self.data[sy, sx.stop:] = np.ma.masked
[docs]
def mask_ellipse(self, center, radius, posangle, unit_center=u.deg,
unit_radius=u.arcsec, inside=True):
"""Mask values inside or outside an elliptical region.
Parameters
----------
center : (float,float)
Center (y,x) of the region, where y,x are usually celestial
coordinates along the Y and X axes of the image, but are
interpretted as Y,X array-indexes if unit_center is changed
to None.
radius : (float,float)
The radii of the two orthogonal axes of the ellipse.
When posangle is zero, radius[0] is the radius along
the X axis of the image-array, and radius[1] is
the radius along the Y axis of the image-array.
posangle : float
The counter-clockwise rotation angle of the ellipse in
degrees. When posangle is zero, the X and Y axes of the
ellipse are along the X and Y axes of the image.
unit_center : `astropy.units.Unit`
The units of the center coordinates.
Degrees by default (use None for coordinates in pixels).
unit_radius : `astropy.units.Unit`
The units of the radius argument. Arcseconds by default.
(use None for radius in pixels)
inside : bool
If inside is True, pixels inside the described region are masked.
If inside is False, pixels outside the described region are masked.
"""
center = np.array(center)
if unit_center is not None:
center = self.wcs.sky2pix(center, unit=unit_center)[0]
# Get the pixel sizes in the units of the radius argument.
if unit_radius is None:
step = np.array([1.0, 1.0]) # Pixel counts
else:
step = self.wcs.get_step(unit=unit_radius)
# Get the two radii in the form of a numpy array.
if np.isscalar(radius):
radii = np.array([radius, radius])
else:
radii = np.asarray(radius)
# Obtain Y and X axis slice objects that select the rectangular
# region that just encloses the rotated ellipse.
[sy, sx], _, center = bounding_box(
form="ellipse", center=center, radii=radii,
shape=self.shape, posangle=posangle, step=step)
# Precompute the sine and cosine of the position angle.
cospa = np.cos(np.radians(posangle))
sinpa = np.sin(np.radians(posangle))
# When the position angle is zero, such that the
# xe and ye axes of the ellipse are along the X and Y axes
# of the image-array, the equation of the ellipse is:
#
# (xe / rx)**2 + (ye / ry)**2 = 1
#
# Before we can use this equation with the rotated ellipse, we
# have to rotate the pixel coordinates clockwise by the
# counterclockwise position angle of the ellipse to align the
# rotated axes of the ellipse along the image X and Y axes:
#
# xp = | cos(pa), sin(pa)| |x|
# yp |-sin(pa), cos(pa)| |y|
#
# The value of k returned by the following equation will then
# be < 1 for pixels inside the ellipse, == 1 for pixels on the
# ellipse and > 1 for pixels outside the ellipse.
#
# k = (xp / rx)**2 + (yp / ry)**2
x, y = np.meshgrid((np.arange(sx.start, sx.stop) - center[1]) * step[1],
(np.arange(sy.start, sy.stop) - center[0]) * step[0])
ksel = (((x * cospa + y * sinpa) / radii[0]) ** 2 +
((y * cospa - x * sinpa) / radii[1]) ** 2)
if inside:
self.data[sy, sx][ksel < 1] = np.ma.masked
else:
self.data[0:sy.start, :] = np.ma.masked
self.data[sy.stop:, :] = np.ma.masked
self.data[sy, 0:sx.start] = np.ma.masked
self.data[sy, sx.stop:] = np.ma.masked
self.data[sy, sx][ksel > 1] = np.ma.masked
[docs]
def mask_polygon(self, poly, unit=u.deg, inside=True):
"""Mask values inside or outside a polygonal region.
Parameters
----------
poly : (float, float)
An array of (float,float) containing a set of (p,q) or (dec,ra)
values for the polygon vertices.
unit : `astropy.units.Unit`
The units of the polygon coordinates (by default in degrees).
Use unit=None to have polygon coordinates in pixels.
inside : bool
If inside is True, pixels inside the polygonal region are masked.
If inside is False, pixels outside the polygonal region are masked.
"""
# Convert DEC,RA (deg) values coming from poly into Y,X value (pixels)
if unit is not None:
poly = np.array([
[self.wcs.sky2pix((val[0], val[1]), unit=unit)[0][0],
self.wcs.sky2pix((val[0], val[1]), unit=unit)[0][1]]
for val in poly])
b = np.mgrid[:self.shape[0], :self.shape[1]].reshape(2, -1).T
# Use a matplotlib method to create a polygon path and check if points
# are within the polygon. The ouput is a boolean table.
from matplotlib.path import Path
polymask = Path(poly)
c = polymask.contains_points(b)
if not inside:
c = ~c
self._mask |= c.reshape(self.shape)
return poly
[docs]
def truncate(self, y_min, y_max, x_min, x_max, mask=True, unit=u.deg,
inplace=False):
"""Return a sub-image that contains a specified area of the sky.
The ranges x_min to x_max and y_min to y_max, specify a rectangular
region of the sky in world coordinates. The truncate function returns
the sub-image that just encloses this region. Note that if the world
coordinate axes are not parallel to the array axes, the region will
appear to be a rotated rectangle within the sub-image. In such cases,
the corners of the sub-image will contain pixels that are outside the
region. By default these pixels are masked. However this can be
disabled by changing the optional mask argument to False.
Parameters
----------
y_min : float
The minimum Y-axis world-coordinate of the selected
region. The Y-axis is usually Declination, which may not
be parallel to the Y-axis of the image array.
y_max : float
The maximum Y-axis world coordinate of the selected region.
x_min : float
The minimum X-axis world-coordinate of the selected
region. The X-axis is usually Right Ascension, which may
not be parallel to the X-axis of the image array.
x_max : float
The maximum X-axis world coordinate of the selected region.
mask : bool
If True, any pixels in the sub-image that remain outside the
range x_min to x_max and y_min to y_max, will be masked.
unit : `astropy.units.Unit`
The units of the X and Y world-coordinates (degrees by default).
inplace : bool
If False, return a truncated copy of the image (the default).
If True, truncate the original image in-place, and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
# Get the sky and pixel coordinates of the corners of the rectangular
# region that is bounded by x_min..x_max and y_min..y_max.
skycrd = np.array([[y_min, x_min],
[y_min, x_max],
[y_max, x_min],
[y_max, x_max]])
if unit is not None:
pixcrd = self.wcs.sky2pix(skycrd, unit=unit)
else:
pixcrd = skycrd
# The sides of the selected region may not be parallel with the
# array axes. Determine the pixel bounds of a rectangular
# region of the array that contains the requested region.
imin = max(0, int(np.min(pixcrd[:, 0]) + 0.5))
imax = min(self.shape[0], int(np.max(pixcrd[:, 0]) + 0.5) + 1)
jmin = max(0, int(np.min(pixcrd[:, 1]) + 0.5))
jmax = min(self.shape[1], int(np.max(pixcrd[:, 1]) + 0.5) + 1)
# Extract the rectangular area that contains the requested region.
subima = self[imin:imax, jmin:jmax]
if inplace:
self._data = subima._data
if self._var is not None:
self._var = subima._var
self._mask = subima._mask
self.wcs = subima.wcs
out = self
else:
out = subima.copy()
# If the region is rotated relative to the image array axes
# then the rectangular sub-image that contains this will has
# some pixels outside this region. Should these be masked?
if mask:
# Get the indexes of all of the pixels in the "out" array,
# ordered like: [[0,0], [0,1], [1,0], [1,1], [2,0], [2,1]...]
pixcrd = np.mgrid[:out.shape[0], :out.shape[1]].reshape(2, -1).T
if unit is None:
skycrd = pixcrd
else:
skycrd = np.array(out.wcs.pix2sky(pixcrd, unit=unit))
# Reshape the array of coordinates to have the shape of
# the output array.
x = skycrd[:, 1].reshape(out.shape)
y = skycrd[:, 0].reshape(out.shape)
# Test the X and Y coordinates of each pixel against the
# requested range of X and Y coordinates, and mask pixels
# that are outside this range.
test_x = np.logical_or(x < x_min, x > x_max)
test_y = np.logical_or(y < y_min, y > y_max)
test = np.logical_or(test_x, test_y)
out._mask = np.logical_or(out._mask, test)
# Remove any array margins that are now completely masked.
out.crop()
return out
[docs]
def subimage(self, center, size, unit_center=u.deg, unit_size=u.arcsec,
minsize=2.0):
"""Return a view on a square or rectangular part.
This method returns a square or rectangular sub-image whose center and
size are specified in world coordinates. Note that this is a view on
the original map and that both will be modified at the same time. If
you need to modify only the sub-image, copy() the result of the
method.
Parameters
----------
center : (float,float)
The center (dec, ra) of the square region. If this position
is not within the parent image, None is returned.
size : float or (float,float)
The width of a square region, or the width and height of
a rectangular region.
unit_center : `astropy.units.Unit`
The units of the center coordinates.
Degrees are assumed by default. To specify the center
in pixels, assign None to unit_center.
unit_size : `astropy.units.Unit`
The units of the size and minsize arguments.
Arcseconds are assumed by default (use None to specify
sizes in pixels).
minsize : float
The minimum width of the output image along both the Y and
X axes. This function returns None if size is smaller than
minsize, or if the part of the square that lies within the
parent image is smaller than minsize along either axis.
Returns
-------
out : `~mpdaf.obj.Image`
"""
# If just one size is given, use it for both axes.
if np.isscalar(size):
size = np.array([size, size])
else:
size = np.asarray(size)
if size[0] <= 0 or size[1] <= 0:
raise ValueError('Size must be positive')
# # Require the center to be within the parent image.
# if not self.inside(center, unit_center):
# raise ValueError('The center must be within the image')
# Convert the center position from world-coordinates to pixel indexes.
center = np.asarray(center)
if unit_center is not None:
center = self.wcs.sky2pix(center, unit=unit_center)[0]
# Get the pixel sizes in the units of the size argument.
if unit_size is None:
step = np.array([1.0, 1.0]) # Pixel counts
else:
step = self.wcs.get_step(unit=unit_size)
# Convert the minimum size from world coordinates to pixel counts,
# taking account of the possibility that pixels can be rectangular.
minsize /= step
# Convert the width and height of the region to radii, and
# get Y-axis and X-axis slice objects that select this region.
radius = size / 2.
[sy, sx], [uy, ux], center = bounding_box(
form="rectangle", center=center, radii=radius,
shape=self.shape, step=step)
if (sx.start >= self.shape[1] or
sx.stop < 0 or
sx.start == sx.stop or
sy.start >= self.shape[0] or
sy.stop < 0 or
sy.start == sy.stop):
raise ValueError('Sub-image boundaries are outside the cube: '
'center: {}, shape: {}, size: {}'
.format(center, self.shape, size))
# Require that the image be at least minsize x minsize pixels.
if (sy.stop - sy.start + 1) < minsize[0] or \
(sx.stop - sx.start + 1) < minsize[1]:
# Should we raise an exception instead ?
self.logger.warning('extracted image is too small')
return
# Extract the requested part of the image.
res = self[sy, sx]
# If the image region was not clipped at the edges of the parent cube,
# then return the subcube.
if sy == uy and sx == ux:
return res
# Since the subimage is smaller than requested, due to clipping,
# create new data and variance arrays of the required size.
shape = (uy.stop - uy.start, ux.stop - ux.start)
data = np.zeros(shape, dtype=self.data.dtype)
if self._var is None:
var = None
else:
var = np.zeros(shape)
# If no mask is currently in use, start with every pixel of
# the new array filled with nans. Otherwise create a mask that
# initially flags all pixels.
if self._mask is ma.nomask:
mask = ma.nomask
data[:] = (np.nan if self.dtype.kind == 'f'
else self.data.fill_value)
if var is not None:
var[:] = np.nan
else:
mask = np.ones(shape, dtype=bool)
# Calculate the slices where the clipped subcube should go in
# the new arrays.
slices = (slice(sy.start - uy.start, sy.stop - uy.start),
slice(sx.start - ux.start, sx.stop - ux.start))
# Copy the clipped subcube into unclipped arrays.
data[slices] = res._data[:]
if var is not None:
var[slices] = res._var[:]
if (mask is not None) and (mask is not ma.nomask):
mask[slices] = res._mask[:]
# Create a new WCS object for the unclipped subcube.
wcs = res.wcs
wcs.set_crpix1(wcs.wcs.wcs.crpix[0] + slices[1].start)
wcs.set_crpix2(wcs.wcs.wcs.crpix[1] + slices[0].start)
wcs.naxis1 = shape[1]
wcs.naxis2 = shape[0]
# Create the new unclipped sub-cube.
return Image(wcs=wcs, unit=self.unit, copy=False,
data=data, var=var, mask=mask,
data_header=fits.Header(self.data_header),
primary_header=fits.Header(self.primary_header),
filename=self.filename)
def _rotate(self, theta=0.0, interp='no', reshape=False, order=1,
pivot=None, unit=u.deg, regrid=None, flux=False, cutoff=0.25):
# In general it isn't possible to both anchor a point in the
# image while reshaping the image so that it fits.
if reshape and pivot is not None:
raise ValueError("The pivot and reshape options can't be combined")
# Turn off the sampling filter when orders of less than 2 are selected.
prefilter = order > 1
# Convert the rotation angle to radians.
angle = (theta * unit).to(u.rad).value
# Get the current rotation angle of the image in radians.
oldrot = self.wcs.get_rot(unit=u.rad)
# Where should north end up after we have rotated the image?
newrot = oldrot + angle
# Get the current pixel size.
oldinc = self.wcs.get_axis_increments()
# If no value has been specified for the regrid option, regrid
# unless asked not to reshape the array.
if regrid is None:
regrid = reshape
# Have we been asked to adjust pixel dimensions to avoid undersampling
# and oversampling?
if regrid:
# Determine the spatial frequencies that need to be sampled
# along the rotated Y and X axes.
newfmax = self.get_spatial_fmax(np.rad2deg(newrot))
# Calculate the pixel increments along the X and Y axes
# that will be needed to adequately sample these
# frequencies.
newinc = 0.5 / newfmax * np.sign(oldinc)
else:
# Keep pixel sizes fixed?
newinc = oldinc
# Get the coordinate reference pixel of the input image,
# arranged as a column vector in python (Y,X) order. Note that
# crpix contains FITS pixel indexes which are 1 greater than
# the corresponding python pixel indexes.
oldcrpix = np.array([[self.wcs.get_crpix2() - 1],
[self.wcs.get_crpix1() - 1]])
# Create a similar matrix that would scale a column vector in
# (X,Y) axis order by the rotated X-axis and Y-axis pixel
# increments.
newscale = np.array([[newinc[1], 0.0],
[0.0, newinc[0]]])
# Get the current WCS coordinate transformation matrix (which
# transforms pixel coordinates to intermediate sky
# coordinates).
oldcd = self.wcs.get_cd()
# Create a rotation matrix that multiplies the sky by the
# above angle.
sinq = np.sin(newrot)
cosq = np.cos(newrot)
sky_mrot = np.array([[cosq, -sinq],
[sinq, cosq]])
# Compute the coordinate transformation matrix that will
# pertain to the output image. We can interpolate to any grid,
# so take the opportunity to zero any shear terms that were in
# the original CD matrix, and just create a CD matrix that
# rotates and scales the sky.
newcd = np.dot(sky_mrot, newscale)
# To fill the pixels of the output image we need a coordinate
# transformation matrix to transform pixel indexes of the
# rotated image back to pixel indexes of the input image. To
# do this, we apply the new CD matrix to convert the rotated
# indexes to intermediate sky coordinates, then apply the
# inverse of the old CD matrix, to convert these back to
# indexes of the original image.
wcs_remap = np.dot(np.linalg.inv(oldcd), newcd)
# The above matrix was computed from the WCS CD matrix, which
# is designed to multiply a column vector in FORTRAN (X,Y)
# axis order. Rearrange it to the equivalent matrix for
# multiplying a column vector in python (Y,X) axis order.
new2old = np.array([[wcs_remap[1, 1], wcs_remap[1, 0]],
[wcs_remap[0, 1], wcs_remap[0, 0]]])
# Also compute the inverse of this, so that we can convert
# from input image indexes to rotated image indexes.
old2new = np.linalg.inv(new2old)
# Have we been asked to reshape the image array to just encompass
# the rotated image?
if reshape:
# Determine where the corners of the input image end up in the
# output image with CRPIX set to [0,0].
corners = np.array(
[[0, 0, self.shape[0] - 1, self.shape[0] - 1], # Y indexes
[0, self.shape[1] - 1, 0, self.shape[1] - 1]], # X indexes
dtype=float)
pix = np.dot(old2new, (corners - oldcrpix))
# Get the ranges of indexes occupied by the input image in the
# rotated image.
ymin = min(pix[0, :])
ymax = max(pix[0, :])
xmin = min(pix[1, :])
xmax = max(pix[1, :])
# Calculate the indexes of the coordinate reference pixel of
# the rotated image, such that pixel [xmin,ymin] is moved to
# array index [0,0]. Use (Y,X) axis ordering.
newcrpix = np.array([[-ymin], [-xmin]])
# Calculate the dimensions of the output image in (Y,X) order.
# The dimensions are ymax-ymin+1 rounded up, and xmax-xmin+1
# rounded up.
newdims = np.array([int(ymax - ymin + 1.5),
int(xmax - xmin + 1.5)])
# If not asked to reshape the image array, keep the image
# dimensions the same, and choose the reference pixel such
# that the rotation appears to occur around a specified pixel,
# or the central pixel of the image.
else:
newdims = np.asarray(self.shape)
# If no pivot pixel has been specified, substitute the
# central pixel of the input image.
if pivot is None:
pivot = np.asarray(self.shape, dtype=float) / 2.0
else:
pivot = np.asarray(pivot, dtype=float)
# Convert the pivot indexes to a column vector.
pivot = pivot[np.newaxis, :].T
# If the new coordinate reference pixel were zero, where
# would the pivot pixel end up if we rotated the image
# around oldcrpix?
pix = np.dot(old2new, (pivot - oldcrpix))
# Calculate the indexes of the coordinate reference pixel of
# the rotated image, such that pixel pix is moved to
# pivot. Use (Y,X) axis ordering.
newcrpix = pivot - pix
# The affine_transform() function calculates the pixel index
# of the input image that corresponds to a given pixel index
# of the rotated image, as follows:
#
# oldpixel = new2old * newpixel + offset
#
# The coordinate reference pixels of the rotated and input
# images must refer to the same position on the sky, so:
#
# oldcrpix = new2old * newcrpix + offset
#
# Thus the value of 'offset' has to be:
#
# offset = oldcrpix - new2old * newcrpix
offset = oldcrpix - np.dot(new2old, newcrpix)
# Get a copy of the current image array with masked values filled.
newdata = self._prepare_data(interp)
# For each pixel of the rotated image, use the new2old affine
# transformation matrix to determine where that pixel
# originates in the input image, then interpolate a value from
# the pixels of the input image surrounding that point.
newdata = affine_transform(newdata, matrix=new2old,
offset=offset.flatten(), cval=0.0,
output_shape=newdims, output=float,
order=order, prefilter=prefilter)
# Zero the current data array and then fill its masked pixels
# with floating point 1.0s, so that we can rotate this in the
# the same way as the data to see where the masked areas end up.
self._data[:, :] = 0.0
newmask = np.ma.filled(self.data, 1.0)
# Rotate the array of 1s that represent masked pixels, and fill
# corners that weren't mapped from the input array with 1s, so
# that we end up flagging them too.
newmask = affine_transform(newmask, matrix=new2old,
offset=offset.flatten(), cval=1.0,
output_shape=newdims, output=float,
order=order, prefilter=prefilter)
# Create a new boolean mask in which all pixels that had an
# integrated contribution of more than 'cutoff' originally
# masked pixels are masked. Note that setting the cutoff to
# the "obvious" value of zero results in lots of pixels being
# masked that are far away from any masked pixels, due to
# precision errors in the affine_transform() function.
# Limit the minimum value of the cutoff to avoid this.
newmask = np.greater(newmask, max(cutoff, 1e-6))
# If the image has an associated array of variances, rotate it too.
if self._var is not None:
newvar = affine_transform(self._var, matrix=new2old,
offset=offset.flatten(), cval=0.0,
output_shape=newdims, output=float,
order=order, prefilter=prefilter)
else:
newvar = None
# Compute the number of old pixel areas per new pixel, if the
# pixel dimensions have been changed.
if regrid:
n = newinc.prod() / oldinc.prod()
# Scale the flux per pixel by the multiplicative increase in the
# area of a pixel?
if flux:
# Scale the pixel fluxes by the increase in the area.
newdata *= n
# Each output pixel is an interpolation between the
# nearest neighboring pixels, so the variance is unchanged
# by resampling. Scaling the pixel values by n, however,
# increases the variances by n**2.
if newvar is not None:
newvar *= n ** 2
# Install the rotated data array, mask and variances.
self._data = newdata
self._mask = newmask
self._var = newvar
# Install the new world-coordinate transformation matrix, along
# with the new reference pixel.
self.wcs.set_cd(newcd)
self.wcs.naxis1 = newdims[1]
self.wcs.naxis2 = newdims[0]
# Record the new value of the coordinate reference pixel,
# being careful to convert from python 0-relative pixel
# indexes to FITS 1-relative pixel indexes.
self.wcs.set_crpix1(newcrpix[1][0] + 1)
self.wcs.set_crpix2(newcrpix[0][0] + 1)
# If allowed to reshape the array, crop away any entirely
# masked margins.
if reshape:
self.crop()
[docs]
def rotate(self, theta=0.0, interp='no', reshape=False, order=1,
pivot=None, unit=u.deg, regrid=None, flux=False, cutoff=0.25,
inplace=False):
"""Rotate the sky within an image in the sense of a rotation from
north to east.
For example if the image rotation angle that is currently
returned by image.get_rot() is zero, image.rotate(10.0) will
rotate the northward direction of the image 10 degrees
eastward of where it was, and self.get_rot() will thereafter
return 10.0.
Uses `scipy.ndimage.affine_transform`.
Parameters
----------
theta : float
The angle to rotate the image (degrees). Positive
angles rotate features in the image in the sense of a
rotation from north to east.
interp : 'no' | 'linear' | 'spline'
If 'no', replace masked data with the median value of the
image. This is the default.
If 'linear', replace masked values using a linear
interpolation between neighboring values.
if 'spline', replace masked values using a spline
interpolation between neighboring values.
reshape : bool
If True, the size of the output image array is adjusted
so that the input image is contained completely in the
output. The default is False.
order : int
The order of the prefilter that is applied by the affine
transform function. Prefiltering is not really needed for
band-limited images, but this option is retained for
backwards compatibility with an older version of the
image.rotate method. In general orders > 1 tend to
generate ringing at sharp edges, such as those of CCD
saturation spikes, so this argument is best left with
its default value of 1.
pivot : float,float or None
When the reshape option is True, or the pivot argument is
None, the image is rotated around its center.
Alternatively, when the reshape option is False, the pivot
argument can be used to indicate which pixel index [y,x]
the image will be rotated around. Integer pixel indexes
specify the centers of pixels. Non-integer values can be
used to indicate positions between pixel centers.
On the sky, the rotation always occurs around the
coordinate reference position of the observation. However
the rotated sky is then mapped onto the pixel array of the
image in such a way as to keep the sky position of the
pivot pixel at the same place. This makes the image appear
to rotate around that pixel.
unit : `astropy.units.Unit`
The angular units of the rotation angle, theta.
regrid : bool
When this option is True, the pixel sizes along each axis
are adjusted to avoid undersampling or oversampling any
direction in the original image that would otherwise be
rotated onto a lower or higher resolution axis. This is
particularly important for images whose pixels have
different angular dimensions along the X and Y axes, but
it can also be important for images with square pixels,
because the diagonal of an image with square pixels has
higher resolution than the axes of that image.
If this option is left with its default value of None,
then it is given the value of the reshape option.
flux : bool
This tells the function whether the pixel units of the
image are flux densities (flux=True), such as
erg/s/cm2/Hz, or whether they are per-steradian brightness
units (flux=False), such as erg/s/cm2/Hz/steradian. It
needs to know this when it changes the pixel size, because
when pixel sizes change, resampled flux densities need to
be corrected for the change in the area per pixel, where
resampled brightnesses don't.
cutoff : float
Mask each output pixel where at least this fraction of the
pixel was interpolated from dummy values given to masked
input pixels.
inplace : bool
If False, return a rotated copy of the image (the default).
If True, rotate the original image in-place, and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
res = self if inplace else self.copy()
res._rotate(theta=theta, interp=interp, reshape=reshape, order=order,
pivot=pivot, unit=unit, regrid=regrid, flux=flux,
cutoff=cutoff)
return res
[docs]
def norm(self, typ='flux', value=1.0):
"""Normalize in place total flux to value (default 1).
Parameters
----------
type : 'flux' | 'sum' | 'max'
If 'flux',the flux is normalized and
the pixel area is taken into account.
If 'sum', the flux is normalized to the sum
of flux independently of pixel size.
If 'max', the flux is normalized so that
the maximum of intensity will be 'value'.
value : float
Normalized value (default 1).
"""
if typ == 'flux':
norm = value / (self.get_step().prod() * self.data.sum())
elif typ == 'sum':
norm = value / self.data.sum()
elif typ == 'max':
norm = value / self.data.max()
else:
raise ValueError('Error in type: only flux,sum,max permitted')
self._data *= norm
if self._var is not None:
self._var *= (norm * norm)
[docs]
def background(self, niter=3, sigma=3.0):
"""Compute the image background with sigma-clipping.
Returns the background value and its standard deviation.
Parameters
----------
niter : int
Number of iterations.
sigma : float
Number of sigma used for the clipping.
Returns
-------
out : 2-dim float array
"""
tab = self.data.compressed()
for n in range(niter + 1):
tab = tab[tab <= (tab.mean() + sigma * tab.std())]
return tab.mean(), tab.std()
[docs]
def peak_detection(self, nstruct, niter, threshold=None):
"""Return a list of peak locations.
Parameters
----------
nstruct : int
Size of the structuring element used for the erosion.
niter : int
Number of iterations used for the erosion and the dilatation.
threshold : float
Threshold value. If None, it is initialized with background value.
Returns
-------
out : np.array
"""
if threshold is None:
background, std = self.background()
threshold = background + 10 * std
def _struct(n):
struct = np.zeros([n, n])
for i in range(0, n):
dist = abs(i - (n // 2))
struct[i][dist: abs(n - dist)] = 1
return struct
selec = self.data > threshold
selec.fill_value = False
struct = _struct(nstruct)
selec = ndi.binary_erosion(selec, structure=struct, iterations=niter)
selec = ndi.binary_dilation(selec, structure=struct, iterations=niter)
selec = ndi.binary_fill_holes(selec)
structure = ndi.generate_binary_structure(2, 2)
label = ndi.label(selec, structure)
pos = ndi.center_of_mass(self.data, label[0], np.arange(label[1]) + 1)
return np.array(pos)
[docs]
def peak(self, center=None, radius=0, unit_center=u.deg,
unit_radius=u.arcsec, dpix=2, background=None, plot=False):
"""Find image peak location.
Used `scipy.ndimage.measurements.maximum_position` and
`scipy.ndimage.measurements.center_of_mass`.
Parameters
----------
center : (float,float)
Center (y,x) of the explored region.
If center is None, the full image is explored.
radius : float or (float,float)
Radius defined the explored region.
unit_center : `astropy.units.Unit`
Type of the center coordinates.
Degrees by default (use None for coordinates in pixels).
unit_radius : `astropy.units.Unit`
Radius unit.
Arcseconds by default (use None for radius in pixels)
dpix : int
Half size of the window (in pixels) to compute the center of
gravity.
background : float
Background value. If None, it is computed.
plot : bool
If True, the peak center is overplotted on the image.
Returns
-------
out : dict {'y', 'x', 'p', 'q', 'data'}
Containing the peak position and the peak intensity.
"""
if center is None or radius == 0:
d = self.data
imin = 0
jmin = 0
else:
if is_number(radius):
radius = (radius, radius)
if unit_center is not None:
center = self.wcs.sky2pix(center, unit=unit_center)[0]
if unit_radius is not None:
radius = radius / self.wcs.get_step(unit=unit_radius)
imin = max(0, int(center[0] - radius[0]))
imax = min(self.shape[0], int(center[0] + radius[0] + 1))
jmin = max(0, int(center[1] - radius[1]))
jmax = min(self.shape[1], int(center[1] + radius[1] + 1))
d = self.data[imin:imax, jmin:jmax]
if np.shape(d)[0] == 0 or np.shape(d)[1] == 0:
raise ValueError('Coord area outside image limits')
ic, jc = ndi.maximum_position(d)
if dpix == 0:
di = 0
dj = 0
else:
if background is None:
background = self.background()[0]
di, dj = ndi.center_of_mass(
d[max(0, ic - dpix):ic + dpix + 1,
max(0, jc - dpix):jc + dpix + 1] - background)
ic = imin + max(0, ic - dpix) + di
jc = jmin + max(0, jc - dpix) + dj
# WIP (to solve problem)
iic, jjc = int(round(ic)), int(round(jc))
if (iic < 0) or (jjc < 0) or (iic >= self.data.shape[0]) or (jjc >= self.data.shape[1]):
return None
[[dec, ra]] = self.wcs.pix2sky([[ic, jc]])
maxv = self.data[int(round(ic)), int(round(jc))]
if plot:
self._ax.plot(jc, ic, 'r+')
try:
_str = 'center (%g,%g) radius (%g,%g) dpix %i peak: %g %g' % \
(center[0], center[1], radius[0], radius[1], dpix, jc, ic)
except Exception:
_str = 'dpix %i peak: %g %g' % (dpix, ic, jc)
self._ax.title(_str)
return {'x': ra, 'y': dec, 'p': ic, 'q': jc, 'data': maxv}
[docs]
def fwhm(self, center=None, radius=0, unit_center=u.deg,
unit_radius=u.arcsec):
warnings.warn(
"fwhm method is deprecated. Use fwhm_gauss method instead.", MpdafWarning)
return self.fwhm_gauss(center, radius, unit_center, unit_radius)
[docs]
def fwhm_gauss(self, center=None, radius=0, unit_center=u.deg,
unit_radius=u.arcsec):
"""Compute the Gaussian fwhm.
Parameters
----------
center : (float,float)
Center of the explored region.
If center is None, the full image is explored.
radius : float or (float,float)
Radius defined the explored region.
unit_center : `astropy.units.Unit`
type of the center coordinates.
Degrees by default (use None for coordinates in pixels).
unit_radius : `astropy.units.Unit`
Radius unit. Arcseconds by default (use None for radius in pixels)
Returns
-------
out : array of float
[fwhm_y,fwhm_x], returned in unit_radius (arcseconds by default).
"""
if center is None or radius == 0:
img = self
else:
size = ((radius * 2, radius * 2) if is_number(radius)
else (radius[0] * 2, radius[1] * 2))
img = self.subimage(center, size, unit_center=unit_center,
unit_size=unit_radius)
width = img.moments(unit=unit_radius)
return width / 2 * gaussian_sigma_to_fwhm
[docs]
def ee(self, center=None, radius=0, unit_center=u.deg,
unit_radius=u.arcsec, frac=False, cont=0):
"""Compute ensquared/encircled energy.
Parameters
----------
center : (float,float)
Center of the explored region.
If center is None, the full image is explored.
radius : float or (float,float)
Radius defined the explored region.
If float, it defined a circular region (encircled energy).
If (float,float), it defined a rectangular region (ensquared
energy).
unit_center : `astropy.units.Unit`
Type of the center coordinates.
Degrees by default (use None for coordinates in pixels).
unit_radius : `astropy.units.Unit`
Radius unit. Arcseconds by default (use None for radius in pixels)
frac : bool
If frac is True, result is given relative to the total energy of
the full image.
cont : float
Continuum value.
Returns
-------
out : float
Ensquared/encircled flux.
"""
if center is None or radius == 0:
if frac:
return 1.
else:
return (self.data - cont).sum()
else:
if is_number(radius):
circular = True
radius2 = radius * radius
radius = (radius, radius)
else:
circular = False
if unit_center is not None:
center = self.wcs.sky2pix(center, unit=unit_center)[0]
if unit_radius is not None:
radius = radius / self.wcs.get_step(unit=unit_radius)
radius2 = radius[0] * radius[1]
imin = max(0, center[0] - radius[0])
imax = min(center[0] + radius[0] + 1, self.shape[0])
jmin = max(0, center[1] - radius[1])
jmax = min(center[1] + radius[1] + 1, self.shape[1])
ima = self[imin:imax, jmin:jmax]
if circular:
xaxis = np.arange(ima.shape[0], dtype=float) \
- ima.shape[0] / 2.
yaxis = np.arange(ima.shape[1], dtype=float) \
- ima.shape[1] / 2.
gridx = np.empty(ima.shape, dtype=float)
gridy = np.empty(ima.shape, dtype=float)
for j in range(ima.shape[1]):
gridx[:, j] = xaxis
for i in range(ima.shape[0]):
gridy[i, :] = yaxis
r2 = gridx * gridx + gridy * gridy
ksel = np.where(r2 < radius2)
if frac:
return (ima.data[ksel] - cont).sum() \
/ (self.data - cont).sum()
else:
return (ima.data[ksel] - cont).sum()
else:
if frac:
return (ima.data - cont).sum() / (self.data - cont).sum()
else:
return (ima.data - cont).sum()
[docs]
def eer_curve(self, center=None, unit_center=u.deg, unit_radius=u.arcsec,
etot=None, cont=0):
"""Return containing enclosed energy as function of radius.
The enclosed energy ratio (EER) shows how much light is concentrated
within a certain radius around the image-center.
Parameters
----------
center : (float,float)
Center of the explored region.
If center is None, center of the image is used.
unit_center : `astropy.units.Unit`
Type of the center coordinates.
Degrees by default (use None for coordinates in pixels).
unit_radius : `astropy.units.Unit`
Radius units (arcseconds by default)/
etot : float
Total energy used to comute the ratio.
If etot is not set, it is computed from the full image.
cont : float
Continuum value.
Returns
-------
out : (float array, float array)
Radius array, EER array
"""
if center is None:
i = self.shape[0] // 2
j = self.shape[1] // 2
elif unit_center is None:
i = center[0]
j = center[1]
else:
pixcrd = self.wcs.sky2pix([center[0], center[1]],
nearest=True, unit=unit_center)
i = pixcrd[0][0]
j = pixcrd[0][1]
nmax = min(self.shape[0] - i, self.shape[1] - j, i, j)
if etot is None:
etot = (self.data - cont).sum()
if nmax <= 1:
raise ValueError('Coord area outside image limits')
ee = np.empty(nmax)
for d in range(0, nmax):
ee[d] = (self.data[i - d:i + d + 1, j - d:j + d + 1] - cont).sum() / etot
radius = np.arange(0, nmax)
if unit_radius is not None:
step = np.mean(self.get_step(unit=unit_radius))
radius = radius * step
return radius, ee
[docs]
def ee_size(self, center=None, unit_center=u.deg, etot=None, frac=0.9,
cont=0, unit_size=u.arcsec):
"""Compute the size of the square centered on (y,x) containing the
fraction of the energy.
Parameters
----------
center : (float,float)
Center (y,x) of the explored region.
If center is None, center of the image is used.
unit : `astropy.units.Unit`
Type of the center coordinates.
Degrees by default (use None for coordinates in pixels).
etot : float
Total energy used to comute the ratio.
If etot is not set, it is computed from the full image.
frac : float in ]0,1]
Fraction of energy.
cont : float
continuum value
unit_center : `astropy.units.Unit`
Type of the center coordinates.
Degrees by default (use None for coordinates in pixels).
unit_size : `astropy.units.Unit`
Size unit. Arcseconds by default (use None for sier in pixels).
Returns
-------
out : float array
"""
if center is None:
i = self.shape[0] // 2
j = self.shape[1] // 2
elif unit_center is None:
i = center[0]
j = center[1]
else:
pixcrd = self.wcs.sky2pix([[center[0], center[1]]],
unit=unit_center)
i = int(pixcrd[0][0] + 0.5)
j = int(pixcrd[0][1] + 0.5)
nmax = min(self.shape[0] - i, self.shape[1] - j, i, j)
if etot is None:
etot = (self.data - cont).sum()
if nmax <= 1:
if unit_size is None:
return np.array([1, 1])
else:
return self.get_step(unit_size)
for d in range(1, nmax):
ee2 = (self.data[i - d:i + d + 1, j - d:j + d + 1] - cont).sum() / etot
if ee2 > frac:
break
d -= 1
ee1 = (self.data[i - d:i + d + 1, i - d:i + d + 1] - cont).sum() / etot
d += (frac - ee1) / (ee2 - ee1) # interpolate
d *= 2
if unit_size is None:
return np.array([d, d])
else:
step = self.get_step(unit_size)
return np.array([d * step[0], d * step[1]])
def _interp(self, grid, spline=False):
"""Return the interpolated values corresponding to the grid points.
Parameters
----------
grid :
pixel values
spline : bool
If False, linear interpolation (uses
`scipy.interpolate.griddata`), or if True: spline
interpolation (uses `scipy.interpolate.bisplrep` and
`scipy.interpolate.bisplev`).
"""
if self.mask is np.ma.nomask:
x, y = np.mgrid[:self.shape[0], :self.shape[1]].reshape(2, -1)
data = self._data
else:
x, y = np.where(~self._mask)
data = self._data[x, y]
grid = np.array(grid)
if spline:
if self.var is not None:
var = self.var.filled(np.inf)
weight = 1 / np.sqrt(np.abs(var[x, y]))
else:
weight = None
tck = interpolate.bisplrep(x, y, data, w=weight)
res = interpolate.bisplev(grid[0], grid[1], tck)
return res
else:
# FIXME - check if this is still needed :
# scipy 0.9 griddata - interpolate.interp2d segfaults when there
# are too many data points
# f = interpolate.interp2d(x, y, data)
res = interpolate.griddata((x, y), data, grid.T, method='linear')
return res
def _interp_data(self, spline=False):
"""Return data array with interpolated values for masked pixels.
Parameters
----------
spline : bool
False: bilinear interpolation (it uses
`scipy.interpolate.griddata`), True: spline interpolation (it
uses `scipy.interpolate.bisplrep` and
`scipy.interpolate.bisplev`).
"""
if not self._mask.any():
return self._data
else:
ksel = np.where(self._mask)
data = self._data.__copy__()
data[ksel] = self._interp(ksel, spline)
return data
def _prepare_data(self, interp='no'):
"""Return a copy of the data array in which masked values
have been filled, either with the median value of the image,
or by interpolating neighboring pixels.
Parameters
----------
interp : 'no' | 'linear' | 'spline'
If 'no', replace masked data with the median image value.
If 'linear', replace masked values using a linear
interpolation between neighboring values.
if 'spline', replace masked values using a spline
interpolation between neighboring values.
Returns
-------
out : numpy.ndarray
A patched copy of the data array.
"""
if interp == 'linear':
data = self._interp_data(spline=False)
elif interp == 'spline':
data = self._interp_data(spline=True)
else:
data = np.ma.filled(self.data, np.ma.median(self.data))
return data
[docs]
def moments(self, unit=u.arcsec):
"""Return [width_y, width_x] first moments of the 2D gaussian.
Parameters
----------
unit : `astropy.units.Unit`
Unit of the returned moments (arcseconds by default).
If None, moments will be in pixels.
Returns
-------
out : float array
"""
total = np.abs(self.data).sum()
P, Q = np.indices(self.data.shape)
# python convention: reverse x,y numpy.indices
p = np.argmax((Q * np.abs(self.data)).sum(axis=1) / total)
q = np.argmax((P * np.abs(self.data)).sum(axis=0) / total)
# FIXME: check from where does this formula comes. Should be equivalent
# to scipy.stats.moment(..., moment=2) ??
col = self.data[int(p), :]
width_q = np.sqrt(np.abs((np.arange(col.size) - p) * col).sum() /
np.abs(col).sum())
row = self.data[:, int(q)]
width_p = np.sqrt(np.abs((np.arange(row.size) - q) * row).sum() /
np.abs(row).sum())
mom = np.array([width_p, width_q])
if unit is not None:
mom *= self.wcs.get_step(unit=unit)
return mom
def _prepare_fit_parameters(self, pos_min, pos_max, weight=True,
center=None, unit_center=u.deg,
fwhm=None, unit_fwhm=u.arcsec):
pmin, qmin = 0, 0
pmax, qmax = self.shape
if unit_center is None:
if pos_min is not None:
pmin, qmin = pos_min
if pos_max is not None:
pmax, qmax = pos_max
else:
if pos_min is not None:
pmin, qmin = self.wcs.sky2pix(pos_min, unit=unit_center,
nearest=True)[0]
if pos_max is not None:
pmax, qmax = self.wcs.sky2pix(pos_max, unit=unit_center,
nearest=True)[0]
if pmin > pmax:
pmin, pmax = pmax, pmin
if qmin > qmax:
qmin, qmax = qmax, qmin
pmin = int(max(0, pmin))
qmin = int(max(0, qmin))
pmax = int(pmax)
qmax = int(qmax)
ima = self[pmin:pmax, qmin:qmax]
N = ima.data.count()
if N == 0:
raise ValueError('empty sub-image')
data = ima.data.compressed()
p, q = np.where(~ima._mask)
# weight
if ima.var is not None and weight:
wght = 1.0 / np.sqrt(np.abs(ima.var[p, q].filled(np.inf)))
else:
wght = np.ones(N)
# initial gaussian peak position
if center is None:
imax = data.argmax()
center = np.array([p[imax], q[imax]])
elif unit_center is not None:
center = ima.wcs.sky2pix(center, unit=unit_center)[0]
else:
center = np.array(center)
center[0] -= pmin
center[1] -= qmin
# initial moment value
if fwhm is None:
width = ima.moments(unit=None)
fwhm = width * gaussian_sigma_to_fwhm
else:
fwhm = np.asarray(fwhm) / self.wcs.get_step(unit=unit_fwhm)
return ima, pmin, pmax, qmin, qmax, data, wght, p, q, center, fwhm
[docs]
def gauss_fit(self, pos_min=None, pos_max=None, center=None, flux=None,
fwhm=None, circular=False, cont=0, fit_back=True, rot=0,
peak=False, factor=1, weight=True, plot=False,
unit_center=u.deg, unit_fwhm=u.arcsec, maxiter=0,
verbose=True, full_output=0):
"""Perform Gaussian fit on image.
Parameters
----------
pos_min : (float,float)
Minimum y and x values. Their unit is given by the unit_center
parameter (degrees by default).
pos_max : (float,float)
Maximum y and x values. Their unit is given by the unit_center
parameter (degrees by default).
center : (float,float)
Initial gaussian center (y_peak,x_peak) If None it is estimated.
The unit is given by the unit_center parameter (degrees by
default).
flux : float
Initial integrated gaussian flux or gaussian peak value if peak is
True. If None, peak value is estimated.
fwhm : (float,float)
Initial gaussian fwhm (fwhm_y,fwhm_x). If None, they are estimated.
The unit is given by ``unit_fwhm`` (arcseconds by default).
circular : bool
True: circular gaussian, False: elliptical gaussian
cont : float
continuum value, 0 by default.
fit_back : bool
False: continuum value is fixed,
True: continuum value is a fit parameter.
rot : float
Initial rotation in degree.
If None, rotation is fixed to 0.
peak : bool
If true, flux contains a gaussian peak value.
factor : int
If factor<=1, gaussian value is computed in the center of each
pixel. If factor>1, for each pixel, gaussian value is the sum of
the gaussian values on the factor*factor pixels divided by the
pixel area.
weight : bool
If weight is True, the weight is computed as the inverse of
variance.
unit_center : `astropy.units.Unit`
type of the center and position coordinates.
Degrees by default (use None for coordinates in pixels).
unit_fwhm : `astropy.units.Unit`
FWHM unit. Arcseconds by default (use None for radius in pixels)
maxiter : int
The maximum number of iterations during the sum of square
minimization.
plot : bool
If True, the gaussian is plotted.
verbose : bool
If True, the Gaussian parameters are printed at the end of the
method.
full_output : bool
True-zero to return a `mpdaf.obj.Gauss2D` object containing
the gauss image.
Returns
-------
out : `mpdaf.obj.Gauss2D`
"""
ima, pmin, pmax, qmin, qmax, data, wght, p, q, center, fwhm = \
self._prepare_fit_parameters(
pos_min, pos_max, weight=weight,
center=center, unit_center=unit_center,
fwhm=fwhm, unit_fwhm=unit_fwhm)
# initial gaussian integrated flux
if flux is None:
peak = ima._data[int(center[0]), int(center[1])] - cont
elif peak is True:
peak = flux - cont
N = len(p)
width = fwhm * gaussian_fwhm_to_sigma
flux = peak * np.sqrt(2 * np.pi * (width[0] ** 2)) \
* np.sqrt(2 * np.pi * (width[1] ** 2))
if circular:
rot = None
if not fit_back:
# 2d gaussian function
gaussfit = lambda v, p, q: \
cont + v[0] * (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-(p - v[1]) ** 2 / (2 * v[2] ** 2)) \
* (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-(q - v[3]) ** 2 / (2 * v[2] ** 2))
# inital guesses for Gaussian Fit
v0 = [flux, center[0], width[0], center[1]]
else:
# 2d gaussian function
gaussfit = lambda v, p, q: \
v[4] + v[0] * (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-(p - v[1]) ** 2 / (2 * v[2] ** 2)) \
* (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-(q - v[3]) ** 2 / (2 * v[2] ** 2))
# inital guesses for Gaussian Fit
v0 = [flux, center[0], width[0], center[1], cont]
else:
if not fit_back:
if rot is None:
# 2d gaussian function
gaussfit = lambda v, p, q: \
cont + v[0] * (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-(p - v[1]) ** 2 / (2 * v[2] ** 2)) \
* (1 / np.sqrt(2 * np.pi * (v[4] ** 2))) \
* np.exp(-(q - v[3]) ** 2 / (2 * v[4] ** 2))
# inital guesses for Gaussian Fit
v0 = [flux, center[0], width[0], center[1], width[1]]
else:
# rotation angle in rad
rot = np.pi * rot / 180.0
# 2d gaussian function
gaussfit = lambda v, p, q: \
cont + v[0] * (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-((p - v[1]) * np.cos(v[5])
- (q - v[3]) * np.sin(v[5])) ** 2
/ (2 * v[2] ** 2)) \
* (1 / np.sqrt(2 * np.pi * (v[4] ** 2))) \
* np.exp(-((p - v[1]) * np.sin(v[5])
+ (q - v[3]) * np.cos(v[5])) ** 2
/ (2 * v[4] ** 2))
# inital guesses for Gaussian Fit
v0 = [flux, center[0], width[0], center[1], width[1], rot]
else:
if rot is None:
# 2d gaussian function
gaussfit = lambda v, p, q: \
v[5] + v[0] * (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-(p - v[1]) ** 2 / (2 * v[2] ** 2)) \
* (1 / np.sqrt(2 * np.pi * (v[4] ** 2))) \
* np.exp(-(q - v[3]) ** 2 / (2 * v[4] ** 2))
# inital guesses for Gaussian Fit
v0 = [flux, center[0], width[0], center[1],
width[1], cont]
else:
# r otation angle in rad
rot = np.pi * rot / 180.0
# 2d gaussian function
gaussfit = lambda v, p, q: \
v[6] + v[0] * (1 / np.sqrt(2 * np.pi * (v[2] ** 2))) \
* np.exp(-((p - v[1]) * np.cos(v[5])
- (q - v[3]) * np.sin(v[5])) ** 2
/ (2 * v[2] ** 2)) \
* (1 / np.sqrt(2 * np.pi * (v[4] ** 2))) \
* np.exp(-((p - v[1]) * np.sin(v[5])
+ (q - v[3]) * np.cos(v[5])) ** 2
/ (2 * v[4] ** 2))
# inital guesses for Gaussian Fit
v0 = [flux, center[0], width[0], center[1],
width[1], rot, cont]
# Minimize the sum of squares
if factor > 1:
factor = int(factor)
deci = np.ones((factor, factor)) \
* np.arange(factor)[:, np.newaxis] \
/ float(factor) + 1. / float(factor * 2) - 0.5
fp = (p[:, np.newaxis] + deci.ravel()[np.newaxis, :]).ravel()
fq = (q[:, np.newaxis] + deci.T.ravel()[np.newaxis, :]).ravel()
pixcrd = np.array(list(zip(fp, fq)))
e_gauss_fit = lambda v, p, q, data, w: \
w * (((gaussfit(v, p, q)).reshape(N, factor * factor).sum(1)
/ factor / factor).T.ravel() - data)
v, covar, info, mesg, success = \
leastsq(e_gauss_fit, v0[:],
args=(pixcrd[:, 0], pixcrd[:, 1], data, wght),
maxfev=maxiter, full_output=1)
else:
e_gauss_fit = lambda v, p, q, data, w: \
w * (gaussfit(v, p, q) - data)
v, covar, info, mesg, success = \
leastsq(e_gauss_fit, v0[:], args=(p, q, data, wght),
maxfev=maxiter, full_output=1)
if success not in [1, 2, 3, 4]:
self._logger.info(mesg)
# calculate the errors from the estimated covariance matrix
chisq = sum(info["fvec"] * info["fvec"])
dof = len(info["fvec"]) - len(v)
if covar is not None:
err = np.array([np.sqrt(np.abs(covar[i, i]))
* np.sqrt(np.abs(chisq / dof))
for i in range(len(v))])
else:
err = None
# center in pixel in the input image
v[1] += int(pmin)
v[3] += int(qmin)
# plot
# ne fonctionne pas si colorbar
if plot:
pp = np.arange(pmin, pmax, float(pmax - pmin) / 100)
qq = np.arange(qmin, qmax, float(qmax - qmin) / 100)
ff = np.empty((np.shape(pp)[0], np.shape(qq)[0]))
for i in range(np.shape(pp)[0]):
ff[i, :] = gaussfit(v, pp[i], qq[:])
self._ax.contour(qq, pp, ff, 5)
# Gauss2D object in pixels
flux = v[0]
p_peak = v[1]
q_peak = v[3]
if circular:
if fit_back:
cont = v[4]
p_width = np.abs(v[2])
q_width = p_width
rot = 0
else:
if fit_back:
if rot is None:
cont = v[5]
else:
cont = v[6]
if rot is None:
p_width = np.abs(v[2])
q_width = np.abs(v[4])
rot = 0
else:
if np.abs(v[2]) > np.abs(v[4]):
p_width = np.abs(v[2])
q_width = np.abs(v[4])
rot = (v[5] * 180.0 / np.pi) % 180
else:
p_width = np.abs(v[4])
q_width = np.abs(v[2])
rot = (v[5] * 180.0 / np.pi + 90) % 180
p_fwhm = p_width * gaussian_sigma_to_fwhm
q_fwhm = q_width * gaussian_sigma_to_fwhm
peak = flux / np.sqrt(2 * np.pi * (p_width ** 2)) \
/ np.sqrt(2 * np.pi * (q_width ** 2))
# error
if err is not None:
err_flux = err[0]
err_p_peak = err[1]
err_q_peak = err[3]
if circular:
if fit_back:
err_cont = err[4]
else:
err_cont = 0
err_p_width = np.abs(err[2])
err_q_width = err_p_width
err_rot = 0
else:
if fit_back:
try:
err_cont = err[6]
except Exception:
err_cont = err[5]
else:
err_cont = 0
if np.abs(v[2]) > np.abs(v[4]) or rot == 0:
err_p_width = np.abs(err[2])
err_q_width = np.abs(err[4])
else:
err_p_width = np.abs(err[4])
err_q_width = np.abs(err[2])
try:
err_rot = err[4] * 180.0 / np.pi
except Exception:
err_rot = 0
err_p_fwhm = err_p_width * gaussian_sigma_to_fwhm
err_q_fwhm = err_q_width * gaussian_sigma_to_fwhm
err_peak = (err_flux * p_width * q_width - flux
* (err_p_width * q_width + err_q_width * p_width)) \
/ (2 * np.pi * p_width * p_width * q_width * q_width)
else:
err_flux = np.nan
err_p_peak = np.nan
err_p_width = np.nan
err_p_fwhm = np.nan
err_q_peak = np.nan
err_q_width = np.nan
err_q_fwhm = np.nan
err_rot = np.nan
err_peak = np.nan
err_cont = np.nan
if unit_center is not None:
# Gauss2D object in degrees/arcseconds
center = self.wcs.pix2sky([p_peak, q_peak], unit=unit_center)[0]
err_center = np.array([err_p_peak, err_q_peak]) * \
self.wcs.get_step(unit=unit_center)
else:
center = (p_peak, q_peak)
err_center = (err_p_peak, err_q_peak)
step = self.wcs.get_step(unit=unit_fwhm)
fwhm = np.array([p_fwhm, q_fwhm]) * step
err_fwhm = np.array([err_p_fwhm, err_q_fwhm]) * step
gauss = Gauss2D(center, flux, fwhm, cont, rot, peak, err_center,
err_flux, err_fwhm, err_cont, err_rot, err_peak)
if verbose:
gauss.print_param()
if full_output:
ima = gauss_image(shape=self.shape, wcs=self.wcs, gauss=gauss,
unit_center=unit_center, unit_fwhm=unit_fwhm)
gauss.ima = ima
return gauss
[docs]
def moffat_fit(self, pos_min=None, pos_max=None, center=None, fwhm=None,
flux=None, n=2.0, circular=False, cont=0, fit_back=True,
rot=0, peak=False, factor=1, weight=True, plot=False,
unit_center=u.deg, unit_fwhm=u.arcsec,
verbose=True, full_output=0, fit_n=True, maxiter=0):
"""Perform moffat fit on image.
Parameters
----------
pos_min : (float,float)
Minimum y and x values. Their unit is given by the unit_center
parameter (degrees by default).
pos_max : (float,float)
Maximum y and x values. Their unit is given by the unit_center
parameter (degrees by default).
center : (float,float)
Initial moffat center (y_peak,x_peak). If None it is estimated.
The unit is given by the unit_center parameter (degrees by
default).
flux : float
Initial integrated gaussian flux or gaussian peak value if peak is
True. If None, peak value is estimated.
fwhm : (float,float)
Initial gaussian fwhm (fwhm_y,fwhm_x). If None, they are estimated.
Their unit is given by the unit_fwhm parameter (arcseconds by
default).
n : int
Initial atmospheric scattering coefficient.
circular : bool
True: circular moffat, False: elliptical moffat
cont : float
continuum value, 0 by default.
fit_back : bool
False: continuum value is fixed,
True: continuum value is a fit parameter.
rot : float
Initial angle position in degree.
peak : bool
If true, flux contains a gaussian peak value.
factor : int
If factor<=1, gaussian is computed in the center of each pixel.
If factor>1, for each pixel, gaussian value is the sum of the
gaussian values on the factor*factor pixels divided by the pixel
area.
weight : bool
If weight is True, the weight is computed as the inverse of
variance.
plot : bool
If True, the gaussian is plotted.
unit_center : `astropy.units.Unit`
type of the center and position coordinates.
Degrees by default (use None for coordinates in pixels).
unit_fwhm : `astropy.units.Unit`
FWHM unit. Arcseconds by default (use None for radius in pixels)
full_output : bool
True to return a `mpdaf.obj.Moffat2D` object containing the
moffat image.
fit_n : bool
False: n value is fixed,
True: n value is a fit parameter.
maxiter : int
The maximum number of iterations during the sum of square
minimization.
Returns
-------
out : `mpdaf.obj.Moffat2D`
"""
ima, pmin, pmax, qmin, qmax, data, wght, p, q, center, fwhm = \
self._prepare_fit_parameters(
pos_min, pos_max, weight=weight,
center=center, unit_center=unit_center,
fwhm=fwhm, unit_fwhm=unit_fwhm)
N = len(p)
a = fwhm[0] / (2 * np.sqrt(2 ** (1.0 / n) - 1.0))
e = fwhm[0] / fwhm[1]
# initial gaussian integrated flux
if flux is None:
I = ima.data.data[int(center[0]), int(center[1])] - cont
elif peak is True:
I = flux - cont
else:
I = flux * (n - 1) / (np.pi * a * a * e)
def moffat(c, x, y, amplitude, x_0, y_0, alpha, beta, e):
"""Two dimensional Moffat model function"""
rr_gg = (((x - x_0) / alpha) ** 2 + ((y - y_0) / alpha / e) ** 2)
return c + amplitude * (1 + rr_gg) ** (-beta)
# def ellpt_moffat(c, x, y, amplitude, x_0, y_0, alpha, beta, e, theta):
# """Two dimensional elliptical Moffat model function"""
# cost = np.cos(theta)
# sint = np.sin(theta)
# xdiff = x - x_0
# ydiff = y - y_0
# rr_gg = (((xdiff * cost - ydiff * sint) / alpha) ** 2 +
# ((xdiff * sint + ydiff * cost) / alpha / e) ** 2)
# return c + amplitude * (1 + rr_gg) ** (-beta)
if circular:
rot = None
if not fit_back:
if fit_n:
moffatfit = lambda v, p, q: moffat(
cont, p, q, v[0], v[1], v[2], v[3], v[4], 1)
v0 = [I, center[0], center[1], a, n]
else:
moffatfit = lambda v, p, q: moffat(
cont, p, q, v[0], v[1], v[2], v[3], n, 1)
v0 = [I, center[0], center[1], a]
else:
# 2d moffat function
if fit_n:
moffatfit = lambda v, p, q: moffat(
v[5], p, q, v[0], v[1], v[2], v[3], v[4], 1)
v0 = [I, center[0], center[1], a, n, cont]
else:
moffatfit = lambda v, p, q: moffat(
v[4], p, q, v[0], v[1], v[2], v[3], n, 1)
v0 = [I, center[0], center[1], a, cont]
else:
if not fit_back:
if rot is None:
if fit_n:
moffatfit = lambda v, p, q: moffat(
cont, p, q, v[0], v[1], v[2], v[3], v[4], v[5])
v0 = [I, center[0], center[1], a, n, e]
else:
moffatfit = lambda v, p, q: moffat(
cont, p, q, v[0], v[1], v[2], v[3], n, v[5])
v0 = [I, center[0], center[1], a, e]
else:
# rotation angle in rad
rot = np.pi * rot / 180.0
if fit_n:
# 2d moffat function
moffatfit = lambda v, p, q: cont + v[0] \
* (1 + (((p - v[1]) * np.cos(v[6]) - (q - v[2])
* np.sin(v[6])) / v[3]) ** 2
+ (((p - v[1]) * np.sin(v[6]) + (q - v[2])
* np.cos(v[6])) / v[3] / v[5]) ** 2) ** (-v[4])
# inital guesses
v0 = [I, center[0], center[1], a, n, e, rot]
else:
# 2d moffat function
moffatfit = lambda v, p, q: cont + v[0] \
* (1 + (((p - v[1]) * np.cos(v[5]) - (q - v[2])
* np.sin(v[5])) / v[3]) ** 2
+ (((p - v[1]) * np.sin(v[5]) + (q - v[2])
* np.cos(v[5])) / v[3] / v[4]) ** 2) ** (-n)
# inital guesses
v0 = [I, center[0], center[1], a, e, rot]
else:
if rot is None:
if fit_n:
moffatfit = lambda v, p, q: moffat(
v[6], p, q, v[0], v[1], v[2], v[3], v[4], v[5])
v0 = [I, center[0], center[1], a, n, e, cont]
else:
moffatfit = lambda v, p, q: moffat(
v[5], p, q, v[0], v[1], v[2], v[3], n, v[4])
v0 = [I, center[0], center[1], a, e, cont]
else:
# rotation angle in rad
rot = np.pi * rot / 180.0
if fit_n:
# 2d moffat function
moffatfit = lambda v, p, q: v[7] + v[0] \
* (1 + (((p - v[1]) * np.cos(v[6])
- (q - v[2]) * np.sin(v[6])) / v[3]) ** 2
+ (((p - v[1]) * np.sin(v[6])
+ (q - v[2]) * np.cos(v[6])) / v[3] / v[5]) ** 2) ** (-v[4])
# inital guesses
v0 = [I, center[0], center[1], a, n, e, rot, cont]
else:
# 2d moffat function
moffatfit = lambda v, p, q: v[6] + v[0] \
* (1 + (((p - v[1]) * np.cos(v[5])
- (q - v[2]) * np.sin(v[5])) / v[3]) ** 2
+ (((p - v[1]) * np.sin(v[5])
+ (q - v[2]) * np.cos(v[5])) / v[3] / v[4]) ** 2) ** (-n)
# inital guesses
v0 = [I, center[0], center[1], a, e, rot, cont]
# Minimize the sum of squares
if factor > 1:
factor = int(factor)
deci = np.ones((factor, factor)) \
* np.arange(factor)[:, np.newaxis] / float(factor) \
+ 1 / float(factor * 2)
fp = (p[:, np.newaxis] + deci.ravel()[np.newaxis, :]).ravel()
fq = (q[:, np.newaxis] + deci.T.ravel()[np.newaxis, :]).ravel()
pixcrd = np.array(list(zip(fp, fq)))
e_moffat_fit = lambda v, p, q, data, w: \
w * (((moffatfit(v, p, q)).reshape(N, factor * factor).sum(1)
/ factor / factor).T.ravel() - data)
v, covar, info, mesg, success = \
leastsq(e_moffat_fit, v0[:], args=(pixcrd[:, 0], pixcrd[:, 1],
data, wght),
maxfev=maxiter, full_output=1)
else:
e_moffat_fit = lambda v, p, q, data, w: \
w * (moffatfit(v, p, q) - data)
v, covar, info, mesg, success = \
leastsq(e_moffat_fit, v0[:],
args=(p, q, data, wght),
maxfev=maxiter, full_output=1)
if success not in [1, 2, 3, 4]:
self._logger.warning(mesg)
# calculate the errors from the estimated covariance matrix
chisq = sum(info["fvec"] * info["fvec"])
dof = len(info["fvec"]) - len(v)
if covar is not None:
err = np.array([np.sqrt(np.abs(covar[i, i])) *
np.sqrt(np.abs(chisq / dof))
for i in range(len(v))])
else:
err = np.zeros_like(v)
err[:] = np.abs(v[:] - v0[:])
# center in pixel in the input image
v[1] += int(pmin)
v[2] += int(qmin)
if plot:
pp = np.arange(pmin, pmax, float(pmax - pmin) / 100)
qq = np.arange(qmin, qmax, float(qmax - qmin) / 100)
ff = np.empty((np.shape(pp)[0], np.shape(qq)[0]))
for i in range(np.shape(pp)[0]):
ff[i, :] = moffatfit(v, pp[i], qq[:])
self._ax.contour(qq, pp, ff, 5)
# Moffat2D object in pixels
I, p_peak, q_peak = v[:3]
a = np.abs(v[3])
v = list(v[4:])
# v0 = [I, center[0], center[1], a, n, e, rot, cont]
if fit_back:
# If present, cont is always the last parameter
cont = v.pop()
if fit_n:
n = v.pop(0)
_fwhm = a * (2 * np.sqrt(2 ** (1.0 / n) - 1.0))
if circular:
rot = 0
fwhm = (_fwhm, _fwhm)
else:
e = v.pop(0)
if e < 1:
fwhm = (_fwhm, _fwhm * e)
else:
fwhm = (_fwhm * e, _fwhm)
if rot is None:
rot = 0
else:
if e < 1:
rot = (v[0] * 180.0 / np.pi) % 180
else:
rot = (v[0] * 180.0 / np.pi + 90) % 180
flux = I / (n - 1) * (np.pi * a * a * e)
if err is not None:
err_I, err_p_peak, err_q_peak = err[:3]
err_a = err[3]
if fit_n:
err_n = err[4]
err_fwhm = err_a * n
if circular:
err_e = 0
err_rot = 0
err_fwhm = np.array([err_fwhm, err_fwhm])
if fit_back:
err_cont = err[5]
else:
err_cont = 0
err_flux = err_I * err_n * err_a * err_a
else:
err_e = err[5]
if err_e != 0:
err_fwhm = np.array([err_fwhm, err_fwhm / err_e])
else:
err_fwhm = np.array([err_fwhm, err_fwhm])
if rot is None:
err_rot = 0
if fit_back:
err_cont = err[6]
else:
err_cont = 0
else:
err_rot = err[6] * 180.0 / np.pi
if fit_back:
err_cont = err[7]
else:
err_cont = 0
err_flux = err_I * err_n * err_a * err_a * err_e
else:
err_n = 0
err_fwhm = err_a * n
if circular:
err_e = 0
err_rot = 0
err_fwhm = np.array([err_fwhm, err_fwhm])
if fit_back:
err_cont = err[4]
else:
err_cont = 0
err_flux = err_I * err_n * err_a * err_a
else:
err_e = err[4]
if err_e != 0:
err_fwhm = np.array([err_fwhm, err_fwhm / err_e])
else:
err_fwhm = np.array([err_fwhm, err_fwhm])
if rot is None:
err_rot = 0
if fit_back:
err_cont = err[5]
else:
err_cont = 0
else:
err_rot = err[5] * 180.0 / np.pi
if fit_back:
err_cont = err[6]
else:
err_cont = 0
err_flux = err_I * err_n * err_a * err_a * err_e
else:
err_I = np.nan
err_p_peak = np.nan
err_q_peak = np.nan
err_a = np.nan
err_n = np.nan
err_e = np.nan
err_rot = np.nan
err_cont = np.nan
err_fwhm = (np.nan, np.nan)
err_flux = np.nan
if unit_center is None:
center = (p_peak, q_peak)
err_center = (err_p_peak, err_q_peak)
else:
# Gauss2D object in degrees/arcseconds
center = self.wcs.pix2sky([p_peak, q_peak], unit=unit_center)[0]
err_center = np.array([err_p_peak, err_q_peak]) * \
self.wcs.get_step(unit=unit_center)
fwhm = np.array(fwhm)
if unit_fwhm is not None:
step0 = self.wcs.get_step(unit=unit_fwhm)[0]
a = a * step0
err_a = err_a * step0
fwhm = fwhm * step0
err_fwhm = err_fwhm * step0
result = Moffat2D(center, flux, fwhm, cont, n,
rot, I, err_center, err_flux, err_fwhm,
err_cont, err_n, err_rot, err_I)
if verbose:
result.print_param()
if full_output:
ima = moffat_image(shape=self.shape, wcs=self.wcs, moffat=result,
unit_center=unit_center, unit_fwhm=unit_fwhm)
result.ima = ima
return result
[docs]
def rebin(self, factor, margin='center', inplace=False):
"""Combine neighboring pixels to reduce the size of an image by
integer factors along each axis.
Each output pixel is the mean of n pixels, where n is the
product of the reduction factors in the factor argument.
Parameters
----------
factor : int or (int,int)
The integer reduction factor along the y and x array axes.
Note the conventional python ordering of the axes.
margin : 'center'|'right'|'left'|'origin'
When the dimensions of the input image are not integer
multiples of the reduction factor, the image is truncated
to remove just enough pixels that its dimensions are
multiples of the reduction factor. This subimage is then
rebinned in place of the original image. The margin
parameter determines which pixels of the input image are
truncated, and which remain.
The options are:
'origin' or 'center':
The starts of the axes of the output image are
coincident with the starts of the axes of the input
image.
'center':
The center of the output image is aligned with the
center of the input image, within one pixel along
each axis.
'right':
The ends of the axes of the output image are
coincident with the ends of the axes of the input
image.
inplace : bool
If False, return a rebinned copy of the image (the default).
If True, rebin the original image in-place, and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
# Delegate the rebinning to the generic DataArray function.
res = self._rebin(factor, margin, inplace)
# If the spatial frequency band-limits of the image have been
# reduced by the changes in the Y and X sampling intervals,
# record this.
res.update_spatial_fmax(0.5 / res.wcs.get_step())
return res
[docs]
def resample(self, newdim, newstart, newstep, flux=False,
order=1, interp='no', unit_start=u.deg, unit_step=u.arcsec,
antialias=True, inplace=False, window="blackman"):
"""Resample an image of the sky to select its angular resolution and
to specify which sky position appears at the center of pixel [0,0].
This function is a simplified interface to the `mpdaf.obj.Image.regrid`
function, which it calls with the following arguments::
regrid(newdim, newstart, [0.0, 0.0],
[abs(newstep[0]),-abs(newstep[1])]
flux=flux, order=order, interp=interp, unit_pos=unit_start,
unit_inc=unit_step, inplace=inplace)
When this function is used to resample an image to a lower
resolution, a low-pass anti-aliasing filter is applied to the
image before it is resampled, to remove all spatial frequencies
below half the new sampling rate. This is required to satisfy
the Nyquist sampling constraint. It prevents high
spatial-frequency noise and edges from being folded into lower
frequency artefacts in the resampled image. The removal of
this noise improves the signal to noise ratio of the resampled
image.
Parameters
----------
newdim : int or (int,int)
The desired new dimensions. Python notation: (ny,nx)
newstart : float or (float, float)
The sky position (dec,ra) that should appear at the center
of pixel [0,0].
If None, the value of self.get_start() is substituted,
so that the sky position that appears at the center of pixel
[0,0] is unchanged by the resampling operation.
newstep : float or (float, float)
The desired angular size of the image pixels on the sky.
The size is expressed as either one number to request
square pixels on the sky with that width and height, or
two numbers that specify the height and width of
rectangular pixels on the sky. In the latter case, the two
numbers are the size along the Y axis of the image array
followed by the size along the X axis.
flux : bool
This tells the function whether the pixel units of the
image are flux densities (flux=True), such as
erg/s/cm2/Hz, or whether they are per-steradian brightness
units (flux=False), such as erg/s/cm2/Hz/steradian. It
needs to know this when it changes the pixel size, because
when pixel sizes change, resampled flux densities need to
be corrected for the change in the area per pixel, where
resampled brightnesses don't.
order : int
The order of the spline interpolation. This can take any
value from 0-5. The default is 1 (linear interpolation).
When this function is used to lower the resolution of
an image, the low-pass anti-aliasing filter that is applied,
makes linear interpolation sufficient.
Conversely, when this function is used to increase the
image resolution, order=3 might be useful. Higher
orders than this will tend to introduce ringing artefacts.
interp : 'no' | 'linear' | 'spline'
If 'no', replace masked data with the median image value.
If 'linear', replace masked values using a linear
interpolation between neighboring values.
if 'spline', replace masked values using a spline
interpolation between neighboring values.
unit_start : `astropy.units.Unit`
The angular units of the newstart coordinates. Degrees by default.
unit_step : `astropy.units.Unit`
The angular units of the step argument. Arcseconds by default.
antialias : bool
By default, when the resolution of an image axis is about
to be reduced, a low pass filter is first applied to suppress
high spatial frequencies that can not be represented by the
reduced sampling interval. If this is not done, high-frequency
noise and sharp edges get folded back to lower frequencies,
where they increase the noise level of the image and introduce
ringing artefacts next to sharp edges, such as CCD saturation
spikes. This filtering can be disabled by passing False to
the antialias argument.
inplace : bool
If False, return a rotated copy of the image (the default).
If True, rotate the original image in-place, and return that.
window : str
The type of window function to use for antialiasing
in the Fourier plane. The following windows are supported:
blackman
This window suppresses ringing better than any other
window, at the expense of lowered image resolution. In
the image plane, the PSF of this window is
approximately gaussian, with a standard deviation of
around 0.96*newstep, and a FWHM of about 2.3*newstep.
gaussian
A truncated gaussian window. This has a smaller PSF
than the blackman window, however gaussians never fall
to zero, so either significant ringing will be seen due
to truncation of the gaussian, or low-level aliasing
will occur, depending on the spatial frequency coverage
of the image beyond the folding frequency. It can be a
good choice for images that only contain smoothly
varying features. It is equivalent to a convolution of
the image with both an airy profile and a gaussian of
standard deviation 0.724*newstep (FWHM 1.704*newstep).
rectangle
This window simply zeros all spatial frequencies above
the highest that can be correctly sampled by the new
pixel size. This gives the best resolution of any of
the windows, but this is marred by the strong sidelobes
of the resulting airy-profile, especially near bright
point sources and CCD saturation lines.
Returns
-------
out : `~mpdaf.obj.Image`
The resampled image.
"""
# Convert newstep to the newinc argument used by regrid(), being
# careful to preserve the signs of the existing coordinate increments.
step_signs = np.sign(self.get_axis_increments())
if is_number(newstep):
newinc = step_signs * abs(newstep)
else:
newinc = step_signs * abs(np.asarray(newstep))
# Convert newstart to the refpos,refpix arguments expected by regrid().
refpix = None if newstart is None else [0.0, 0.0]
return self.regrid(newdim, newstart, refpix, newinc, flux=flux,
order=order, interp=interp, unit_pos=unit_start,
unit_inc=unit_step, antialias=antialias,
inplace=inplace, window=window)
[docs]
def regrid(self, newdim, refpos, refpix, newinc, flux=False, order=1,
interp='no', unit_pos=u.deg, unit_inc=u.arcsec, antialias=True,
inplace=False, cutoff=0.25, window="blackman"):
"""Resample an image of the sky to select its angular resolution,
to specify the position of the sky in the image array, and
optionally to reflect one or more of its axes.
This function can be used to decrease or increase the
resolution of an image. It can also shift the contents of an
image to place a specific (dec,ra) position at a specific
fractional pixel position. Finally, it can be used to invert
the direction of one or both of the array axes on the sky.
When this function is used to resample an image to a lower
resolution, a low-pass anti-aliasing filter is applied to the
image before it is resampled, to remove all spatial
frequencies below half the new sampling rate. This is required
to satisfy the Nyquist sampling constraint. It prevents high
spatial-frequency noise and edges from being aliased to lower
frequency artefacts in the resampled image. The removal of
this noise improves the signal to noise ratio of the resampled
image.
Parameters
----------
newdim : int or (int,int)
The desired new dimensions. Python notation: (ny,nx)
refpos : (float, float)
The sky position (dec,ra) to place at the pixel specified
by the refpix argument.
If refpix and refpos are both None, the sky position at
the bottom corner of the input image is placed at the
bottom left corner of the output image. Note that refpix
and refpos must either both be given values, or both
be None.
refpix : (float, float)
The [Y, X] indexes of the output pixel where the sky
position, refpos, should be placed. Y and X are
interpreted as floating point indexes, where integer
values indicate pixel centers and integer values +/- 0.5
indicate the edges of pixels.
If refpix and refpos are both None, the sky position at
the bottom corner of the input image is placed at the
bottom left corner of the output image. Note that refpix
and refpos must either both be given values, or both
be None.
newinc : float or (float, float)
The signed increments of the angle on the sky from one
pixel to the next, given as either a single increment for
both image axes, or two numbers (dy,dx) for the Y and X
axes respectively.
The signs of these increments are interpreted as described
in the documentation of the Image.get_axis_increments()
function. In particular, note that dy is typically
positive and dx is usually negative, such that when the
image is plotted, east appears anticlockwise of north, and
east is towards the left of the plot when the image
rotation angle is zero.
If either of the signs of the two newinc numbers is
different from the sign of the increments of the original
image (queryable with image.get_axis_increments()), then
the image will be reflected about that axis. In this case
the value of the refpix argument should be chosen with
care, because otherwise the sampled part of the image may
end up being reflected outside the limits of the image
array, and the result will be a blank image.
If only one number is given for newinc then both axes
are given the same resolution, but the signs of the
increments are kept the same as the pixel increments
of the original image.
flux : bool
This tells the function whether the pixel units of the
image are flux densities (flux=True), such as
erg/s/cm2/Hz, or whether they are per-steradian brightness
units (flux=False), such as erg/s/cm2/Hz/steradian. It
needs to know this when it changes the pixel size, because
when pixel sizes change, resampled flux densities need to
be corrected for the change in the area per pixel, where
resampled brightnesses don't.
order : int
The order of the spline interpolation. This can take any
value from 0-5. The default is 1 (linear interpolation).
When this function is used to lower the resolution of
an image, the low-pass anti-aliasing filter that is applied,
makes linear interpolation sufficient.
Conversely, when this function is used to increase the
image resolution, order=3 might be useful. Higher
orders than this will tend to introduce ringing artefacts.
interp : 'no' | 'linear' | 'spline'
If 'no', replace masked data with the median image value.
If 'linear', replace masked values using a linear
interpolation between neighboring values.
if 'spline', replace masked values using a spline
interpolation between neighboring values.
unit_pos : `astropy.units.Unit`
The units of the refpos coordinates. Degrees by default.
unit_inc : `astropy.units.Unit`
The units of newinc. Arcseconds by default.
antialias : bool
By default, when the resolution of an image axis is about
to be reduced, a low pass filter is first applied to suppress
high spatial frequencies that can not be represented by the
reduced sampling interval. If this is not done, high-frequency
noise and sharp edges get folded back to lower frequencies,
where they increase the noise level of the image and introduce
ringing artefacts next to sharp edges, such as CCD saturation
spikes. This filtering can be disabled by passing False to
the antialias argument.
inplace : bool
If False, return a resampled copy of the image (the default).
If True, resample the original image in-place, and return that.
cutoff : float
Mask each output pixel where at least this fraction of the
pixel was interpolated from dummy values given to masked
input pixels.
window : str
The type of window function to use for antialiasing
in the Fourier plane. The following windows are supported:
blackman
This window suppresses ringing better than any other
window, at the expense of lowered image resolution. In
the image plane, the PSF of this window is
approximately gaussian, with a standard deviation of
around 0.96*newstep, and a FWHM of about 2.3*newstep.
gaussian
A truncated gaussian window. This has a smaller PSF
than the blackman window, however gaussians never fall
to zero, so either significant ringing will be seen due
to truncation of the gaussian, or low-level aliasing
will occur, depending on the spatial frequency coverage
of the image beyond the folding frequency. It can be a
good choice for images that only contain smoothly
varying features. It is equivalent to a convolution of
the image with both an airy profile and a gaussian of
standard deviation 0.724*newstep (FWHM 1.704*newstep).
rectangle
This window simply zeros all spatial frequencies above
the highest that can be correctly sampled by the new
pixel size. This gives the best resolution of any of
the windows, but this is marred by the strong sidelobes
of the resulting airy-profile, especially near bright
point sources and CCD saturation lines.
Returns
-------
out : `~mpdaf.obj.Image`
The resampled image is returned.
"""
if is_int(newdim):
newdim = (newdim, newdim)
newdim = np.asarray(newdim, dtype=int)
if refpos is None and refpix is None:
# If neither refpos nor refpix have values, substitute values
# that will place the current sky position of the bottom left
# corner of the image at the bottom left corner of the output
# image.
# Use the pixel index of the bottom left corner of the image.
refpix = np.array([-0.5, -0.5])
refpos = self.wcs.pix2sky(refpix)
elif refpos is not None and refpix is not None:
# Were refpos and refpix both given values?
# If necessary convert refpos to a numpy array and convert
# it's units to the current WCS units.
refpos = np.asarray(refpos, dtype=float)
if unit_pos is not None:
refpos = UnitArray(refpos, unit_pos, self.wcs.unit)
# If necessary convert refpix to a floating point numpy array.
refpix = np.asarray(refpix, dtype=float)
else:
# Complain if just one of refpos and refpix is None.
raise ValueError('The refpos and refpix arguments should both be '
'None or both have values.')
# Get the current index increments of the 2 axes.
oldinc = self.wcs.get_axis_increments()
# Use a common increment for both axes? If so, give them
# the same size, but with signs matching the current
# pixel increments.
if is_number(newinc):
size = abs(newinc)
newinc = (size * np.sign(oldinc[0]), size * np.sign(oldinc[1]))
# Ensure that newinc is an array of values that have the
# same units as the WCS object.
newinc = np.asarray(newinc, dtype=float)
if unit_inc is not None:
newinc = UnitArray(newinc, unit_inc, self.wcs.unit)
# Get a copy of the data array with masked values filled.
data = self._prepare_data(interp)
# If the angular pixel increments along either axis are being
# increased, then low-pass filter the data along that axis to
# prevent aliasing in the resampled data.
if antialias:
data, newfmax = _antialias_filter_image(
data, abs(oldinc), abs(newinc), self.get_spatial_fmax(),
window)
else:
newfmax = 0.5 / abs(newinc)
# For each pixel in the output image, the affine_transform
# function calculates the index of the equivalent pixel in the
# input image, and interpolates a value of the output pixel
# from the surrounding pixels of the input image. It calculates
# the input index from the output index as follows:
#
# oldpixel = new2old * newpixel + offset
#
# where new2old is a 2x2 affine transform matrix designed to
# multiply a column vector in axis order (Y,X). In our case
# the matrix is:
#
# new2old = |newinc[0]/oldinc[0], 0 |
# | 0 , newinc[1]/oldinc[0]|
#
# This scales an output index by newinc to calculate the
# corresponding angular offset of that pixel from the origin
# of the output array, then divides this by oldinc to compute
# the equivalent index offset in the input array.
new2old = np.array([[newinc[0] / oldinc[0], 0],
[0, newinc[1] / oldinc[1]]])
# Also work out the inverse, so that we can convert from
# pixels in the current image to the equivalent pixel of the
# resampled image.
old2new = np.linalg.inv(new2old)
# We have been asked to locate sky position 'refpos' at pixel
# 'refpix' of the resampled array. Reproducing the equation
# from above:
#
# oldpixel = new2old * newpixel + offset
#
# In this case oldpixel is the pixel index of the input array
# where the sky position 'refpos' is currently located, and
# newpixel is refpix.
#
# sky2pix(refpos) = new2old * refpix + offset
#
# Thus the appropriate value for the offset parameter of
# affine_transform() is:
#
# offset = sky2pix(refpos) - new2old * refpix
offset = (self.wcs.sky2pix(refpos).T[:, :1] -
np.dot(new2old, refpix[np.newaxis, :].T))
# For each pixel of the output image, map its index to the
# equivalent index of the input image and interpolate a value
# for the new pixel from there.
data = affine_transform(data, new2old, offset.flatten(),
output_shape=newdim, order=order,
prefilter=order >= 3)
# Create a floating point version of the mask in which masked
# elements are 1.0 and unmasked elements are 0.0.
mask = self._mask.astype(float)
# Resample the floating point version of the mask array.
mask = affine_transform(mask, new2old, offset.flatten(), cval=1.0,
output_shape=newdim, output=float)
# Create new boolean mask in which all pixels that had an
# integrated contribution of more than 'cutoff' originally
# masked pixels are masked. Note that setting the cutoff to
# the "obvious" value of zero results in lots of pixels being
# masked that are far away from any masked pixels, due to
# precision errors in the affine_transform() function.
# Limit the minimum value of the cutoff to avoid this.
mask = np.greater(mask, max(cutoff, 1.0e-6))
# Also repeat the procedure for the array of variances, if any.
if self._var is not None:
var = affine_transform(self._var, new2old, offset.flatten(),
output_shape=newdim, order=order,
prefilter=order >= 3)
else:
var = None
# Compute the absolute changes in the size of the pixels
# along the X and Y axes.
xs = abs(newinc[1] / oldinc[1])
ys = abs(newinc[0] / oldinc[0])
# Compute the number of input pixels per output pixel.
n = xs * ys
# Scale the flux per pixel by the multiplicative increase in the
# area of a pixel?
if flux:
# Scale the pixel fluxes by the increase in the area.
data *= n
# The variances of the output pixels depend on whether an
# anti-aliasing filter was applied, as follows.
#
# 1. An anti-aliasing filter is applied before resampling
# when increasing the pixel size. This filter
# effectively averages together n neighboring
# pixels. The affine_transform() samples these averages
# when it interpolates the output pixel values, so the
# output pixels are effectively the average of n
# independent pixels of the input image. Multiplying
# these pixel values by n, then turns each output pixel
# value into the sum of n pixels. The variance of a
# sum of n samples of variance v, is n*v.
# 2. No anti-aliasing filter is applied when decreasing
# the pixel size, so in this case affine_transform()
# samples raw pixel values. The variances of these
# output pixels are thus identical to those of the
# input pixels. If we then multiply this by n, then the
# variance of each output pixel is n**2 times the
# variance of the input pixels.
# 3. If the pixel sizes along one axis are increased,
# while those of the other axis are decreased, then we
# have a mix of the above two cases.
if var is not None:
# Scale the variance according to the prescription described
# above.
var *= ((xs if xs > 1.0 and antialias else xs ** 2) *
(ys if ys > 1.0 and antialias else ys ** 2))
# If we haven't been asked to scale the fluxes by the increase
# in the area of a pixel, the effect on the variances are as
# explained above, but without the flux scaling. If
# anti-aliasing was applied to both axes, then each output
# pixel is effectively the average of n input pixels, and the
# variance of a mean of n samples of variance v is v/n. If no
# anti-aliasing was applied, then there is no change to the
# variance.
else:
if var is not None and (xs > 1.0 or ys > 1.0):
var *= ((1 / xs if xs > 1.0 and antialias else 1.0) *
(1 / ys if ys > 1.0 and antialias else 1.0))
# Get the coordinate reference pixel of the input image,
# arranged as a column vector in python (Y,X) order. Note that
# crpix contains FITS pixel indexes which are 1 greater than
# the corresponding python pixel indexes.
oldcrpix = np.array([[self.wcs.get_crpix2() - 1],
[self.wcs.get_crpix1() - 1]])
# Compute the updated value of the coordinate reference pixel
# in (Y,X) axis order.
newcrpix = np.dot(old2new, (oldcrpix - offset))
# Make a copy of the WCS object of the image to use as a template
# for the WCS object of the resampled image.
wcs = self.wcs.copy()
# Install the new increments and image dimensions.
wcs.set_axis_increments(newinc)
wcs.naxis1 = newdim[1]
wcs.naxis2 = newdim[0]
# Record the new value of the coordinate reference pixel,
# being careful to convert from python 0-relative pixel
# indexes to FITS 1-relative pixel indexes.
wcs.set_crpix1(newcrpix[1][0] + 1)
wcs.set_crpix2(newcrpix[0][0] + 1)
# Install the resampled data, mask and variance arrays, either
# within self, or in a new Image object.
out = self if inplace else self.clone()
out._data = data
out._mask = mask
out._var = var
out.wcs = wcs
# If the spatial frequency band-limits of the image have been
# reduced by the changes in the Y and X sampling intervals,
# record this.
out.update_spatial_fmax(newfmax)
return out
[docs]
def align_with_image(self, other, flux=False, inplace=False, cutoff=0.25,
antialias=True, window="blackman"):
"""Resample the image to give it the same orientation, position,
resolution and size as a given image.
The image is first rotated to give it the same orientation on
the sky as the other image. The resampling process also
eliminates any shear terms from the original image, so that
its pixels can be correctly drawn on a rectangular grid.
Secondly the image is resampled. This changes its resolution,
shifts the image such that the same points on the sky appear
in the same pixels as in the other image, and changes the
dimensions of the image array to match that of the other
image.
The rotation and resampling processes are performed as
separate steps because the anti-aliasing filter that needs to
be applied in the resampling step reduces the resolution, is
difficult to implement before the axes have been rotated to
the final orientation.
Parameters
----------
other : `~mpdaf.obj.Image`
The image to be aligned with.
flux : bool
This tells the function whether the pixel units of the
image are flux densities (flux=True), such as
erg/s/cm2/Hz, or whether they are per-steradian brightness
units (flux=False), such as erg/s/cm2/Hz/steradian. It
needs to know this when it changes the pixel size, because
when pixel sizes change, resampled flux densities need to
be corrected for the change in the area per pixel, where
resampled brightnesses don't.
inplace : bool
If False, return an aligned copy of the image (the default).
If True, align the original image in-place, and return that.
cutoff : float
Mask each output pixel where at least this fraction of the
pixel was interpolated from dummy values given to masked
input pixels.
antialias : bool
By default, when the resolution of an image axis is about
to be reduced, a low pass filter is first applied to suppress
high spatial frequencies that can not be represented by the
reduced sampling interval. If this is not done, high-frequency
noise and sharp edges get folded back to lower frequencies,
where they increase the noise level of the image and introduce
ringing artefacts next to sharp edges, such as CCD saturation
spikes and bright unresolved stars. This filtering can be
disabled by passing False to the antialias argument.
window : str
The type of window function to use for antialiasing
in the Fourier plane. The following windows are supported:
blackman
This window suppresses ringing better than any other
window, at the expense of lowered image resolution. In
the image plane, the PSF of this window is
approximately gaussian, with a standard deviation of
around 0.96*newstep, and a FWHM of about 2.3*newstep.
gaussian
A truncated gaussian window. This has a smaller PSF
than the blackman window, however gaussians never fall
to zero, so either significant ringing will be seen due
to truncation of the gaussian, or low-level aliasing
will occur, depending on the spatial frequency coverage
of the image beyond the folding frequency. It can be a
good choice for images that only contain smoothly
varying features. It is equivalent to a convolution of
the image with both an airy profile and a gaussian of
standard deviation 0.724*newstep (FWHM 1.704*newstep).
rectangle
This window simply zeros all spatial frequencies above
the highest that can be correctly sampled by the new
pixel size. This gives the best resolution of any of
the windows, but this is marred by the strong sidelobes
of the resulting airy-profile, especially near bright
point sources and CCD saturation lines.
"""
# Do nothing if the images are already aligned.
if self.wcs.isEqual(other.wcs):
return self if inplace else self.copy()
# Determine the ranges of right-ascension and declination
# covered by the target image grid plus an extra pixel at
# each edge.
pixsky = other.wcs.pix2sky([[-1, -1],
[other.shape[0], -1],
[-1, other.shape[1]],
[other.shape[0], other.shape[1]]],
unit=u.deg)
dec_min, ra_min = pixsky.min(axis=0)
dec_max, ra_max = pixsky.max(axis=0)
# Truncate the input image to just enclose the above ranges of
# right-ascension and declination.
out = self.truncate(dec_min, dec_max, ra_min, ra_max, mask=False,
unit=u.deg, inplace=inplace)
# Rotate the image to have the same orientation as the other
# image. Note that the rotate function has a side effect of
# correcting the image for shear terms in the CD matrix, so we
# perform this step even if no rotation is otherwise needed.
out._rotate(other.wcs.get_rot() - out.wcs.get_rot(), reshape=True,
regrid=True, flux=flux, cutoff=cutoff)
# Get the pixel index and Dec,Ra coordinate at the center of
# the image that we are aligning with.
centerpix = np.asarray(other.shape) / 2.0
centersky = other.wcs.pix2sky(centerpix)[0]
# Re-sample the rotated image to have the same axis
# increments, offset and number of pixels as the image that we
# are aligning it with.
out.regrid(other.shape, centersky, centerpix,
other.wcs.get_axis_increments(unit=u.deg),
flux, unit_inc=u.deg, inplace=True, cutoff=cutoff,
antialias=antialias, window=window)
return out
[docs]
def estimate_coordinate_offset(self, ref, nsigma=1.0):
"""Given a reference image of the sky that is expected to
overlap with the current image, attempt to fit for any offset
between the sky coordinate system of the current image and
that of the reference image. The returned value is designed to
be added to the coordinate reference pixel values of self.wcs.
This function performs the following steps:
1. The align_with_image() method is called to resample the
reference image onto the same coordinate grid as the
current image.
2. The two images are then cross-correlated, after zeroing all
background values in the images below nsigma standard
deviations above the mean.
3. The peak in the auto-correlation image is found and its
sub-pixel position is estimated by a simple quadratic
interpolation. This position, relative to the center of the
auto-correlation image, gives the average position offset
between similar features in the two images.
Parameters
----------
ref : `~mpdaf.obj.Image`
The image of the sky that is to be used as the coordinate
reference. The sky coverage of this image should overlap
with that of self. Ideally the resolution of this image
should be at least as good as the resolution of self.
nsigma : float
Only values that exceed this many standard deviations
above the mean of each image will be used.
Returns
-------
out : float,float
The pixel offsets that would need to be added to the
coordinate reference pixel values, crpix2 and crpix1, of
self.wcs to make the features in self line up with those
in the reference image.
"""
# Resample the reference sky image onto the same coordinate
# grid as our image.
ref = ref.align_with_image(self)
# Before cross-correlating the images we need to make sure
# that any areas that are masked in one image are also masked
# in the other. Otherwise if one image has a very bright
# source in an area that is masked in the other, then this
# will produce false correlations.
#
# First get the union of the masked areas of the two images.
mask = np.ma.mask_or(self._mask, ref._mask)
# Place both image arrays into masked array containers that
# share the above mask.
sdata = np.ma.array(data=self._data, mask=mask)
rdata = np.ma.array(data=ref._data, mask=mask)
# Get copies of the above arrays with masked pixels filled
# with the median values of the images.
sdata = np.ma.filled(sdata, np.ma.median(sdata))
rdata = np.ma.filled(rdata, np.ma.median(rdata))
# When we cross-correlate the images, any constant or noisy
# background will bias the result towards the origin of the
# correlation, so remove most of the noisy background by
# zeroing all values that are less than nsigma standard
# deviations above the mean.
mask = sdata < sdata.mean() + nsigma * sdata.std()
sdata[mask] = 0
mask = rdata < rdata.mean() + nsigma * rdata.std()
rdata[mask] = 0
# Sometimes a bright artefact or a bright star with
# appreciable proper motion biases the correlation. To avoid
# this take the log of the thresholded data to prevent very
# bright features from dominating the correlation.
sdata = np.log(1.0 + sdata)
rdata = np.log(1.0 + rdata)
# Cross correlate our image with the reference image, by
# convolving our image with an axis-reversed version of the
# reference image. Use mode="same" to only keep the inner half
# of the array. We don't expect the peak to be outside this
# area, and this avoids edge effects where there is incomplete
# data.
cc = signal.fftconvolve(sdata, rdata[::-1, ::-1], mode="same")
# Find the position of the maximum value in the correlation image.
py, px = np.unravel_index(np.argmax(cc), cc.shape)
# Quadratically interpolate a more precise peak position from three
# points along the X and Y axes, centered on the position found above.
py2 = py - 1 + _find_quadratic_peak(cc[py - 1: py + 2, px])
px2 = px - 1 + _find_quadratic_peak(cc[py, px - 1: px + 2])
# Compute the offset of the peak relative to the central pixel
# of the correlation image. This yields the offset between the
# two images.
dy = py2 - float(cc.shape[0] // 2)
dx = px2 - float(cc.shape[1] // 2)
return dy, dx
[docs]
def adjust_coordinates(self, ref, nsigma=1.0, inplace=False):
"""Given a reference image of the sky that is expected to
overlap with the current image, attempt to fit for any offset
between the sky coordinate system of the current image and
that of the reference image. Apply this offset to the
coordinates of the current image, to bring it into line with
the reference image.
This function calls self.estimate_coordinate_offset() to
fit for the offset between the coordinate systems of the
two images, then adjusts the coordinate reference pixel of
the current image to bring its coordinates into line with
those of the reference image.
Parameters
----------
ref : `~mpdaf.obj.Image`
The image of the sky that is to be used as the coordinate
reference. The sky coverage of this image should overlap
with that of self. Ideally the resolution of this image
should be at least as good as the resolution of self.
nsigma : float
Only values that exceed this many standard deviations
above the mean of each image will be used.
inplace : bool
If False, return a shifted copy of the image (the default).
If True, shift the original image in-place, and return that.
Returns
-------
out : `~mpdaf.obj.Image`
A version of self in which the sky coordinates have been
shifted to match those of the reference image.
"""
out = self if inplace else self.copy()
# Determine the pixel offset of features in the current
# image relative to features in the reference image.
dy, dx = out.estimate_coordinate_offset(ref, nsigma)
# Offset the WCS of the current image by the pixel shift found above.
out.wcs.set_crpix1(out.wcs.get_crpix1() + dx)
out.wcs.set_crpix2(out.wcs.get_crpix2() + dy)
# Calculate the resulting shift in pixel coordinates, for display
# to the user.
units = u.arcsec if self.wcs.unit is u.deg else self.wcs.unit
offset = np.array([-dy, -dx]) * self.wcs.get_axis_increments(units)
self._logger.info("Shifted the coordinates by dy=%.3g dx=%.3g %s" %
(offset[0], offset[1], units))
return out
[docs]
def gaussian_filter(self, sigma=3, interp='no', inplace=False):
"""Return an image containing Gaussian filter applied to the current
image.
Uses `scipy.ndimage.gaussian_filter`.
Parameters
----------
sigma : float
Standard deviation for Gaussian kernel
interp : 'no' | 'linear' | 'spline'
if 'no', data median value replaced masked values.
if 'linear', linear interpolation of the masked values.
if 'spline', spline interpolation of the masked values.
inplace : bool
If False, return a filtered copy of the image (the default).
If True, filter the original image in-place, and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
out = self if inplace else self.copy()
# Get a copy of the data array with masked values filled.
data = out._prepare_data(interp)
out._data = ndi.gaussian_filter(data, sigma)
if out._var is not None:
out._var = ndi.gaussian_filter(out._var, sigma)
return out
[docs]
def segment(self, shape=(2, 2), minsize=20, minpts=None,
background=20, interp='no', median=None):
"""Segment the image in a number of smaller images.
Returns a list of images. Uses
`scipy.ndimage.generate_binary_structure`,
`scipy.ndimage.grey_dilation`, `scipy.ndimage.measurements.label`, and
`scipy.ndimage.measurements.find_objects`.
Parameters
----------
shape : (int,int)
Shape used for connectivity.
minsize : int
Minimmum size of the images.
minpts : int
Minimmum number of points in the object.
background : float
Under this value, flux is considered as background.
interp : 'no' | 'linear' | 'spline'
if 'no', data median value replaced masked values.
if 'linear', linear interpolation of the masked values.
if 'spline', spline interpolation of the masked values.
median : (int,int) or None
If not None (default), size of the window to apply a median filter
on the image.
Returns
-------
out : list of `Image`
"""
data = self._prepare_data(interp)
if median is not None:
data = np.ma.array(ndi.median_filter(data, median),
mask=self._mask)
expanded = ndi.grey_dilation(data, (minsize, minsize))
expanded[expanded < background] = 0
structure = ndi.generate_binary_structure(shape[0], shape[1])
labels, nlabels = ndi.label(expanded, structure)
slices = ndi.find_objects(labels)
return [self[slices[i]] for i in range(nlabels)
if minpts is None or len(data[labels == i + 1]) >= minpts]
[docs]
def add_gaussian_noise(self, sigma, interp='no'):
"""Add Gaussian noise to image in place.
Parameters
----------
sigma : float
Standard deviation.
interp : 'no' | 'linear' | 'spline'
if 'no', data median value replaced masked values.
if 'linear', linear interpolation of the masked values.
if 'spline', spline interpolation of the masked values.
"""
# Get a copy of the data array with masked values filled.
data = self._prepare_data(interp)
self._data = np.random.normal(data, sigma)
if self._var is None:
self._var = np.ones((self.shape)) * sigma * sigma
else:
self._var *= (sigma * sigma)
[docs]
def inside(self, coord, unit=u.deg):
"""Return True if coord is inside image.
Parameters
----------
coord : (float,float)
coordinates (y,x).
unit : `astropy.units.Unit`
Type of the coordinates (degrees by default)
Returns
-------
out : bool
"""
if unit is not None:
pixcrd = self.wcs.sky2pix([coord[0], coord[1]], unit=unit)[0]
else:
pixcrd = coord
if (pixcrd >= -self.wcs.get_step(unit=unit) / 100).all() and \
(pixcrd < self.shape + self.wcs.get_step(unit=unit) / 100).all():
return True
else:
return False
[docs]
def convolve(self, other, inplace=False):
"""Convolve an Image with a 2D array or another Image, using the
discrete convolution equation.
This function, which uses the discrete convolution equation, is
usually slower than Image.fftconvolve(). However it can be faster when
other.data.size is small, and it always uses much less memory, so it
is sometimes the only practical choice.
Masked values in self.data and self.var are replaced with zeros before
the convolution is performed, but they are masked again after the
convolution.
If self.var exists, the variances are propagated using the equation:
result.var = self.var (*) other**2
where (*) indicates convolution. This equation can be derived by
applying the usual rules of error-propagation to the discrete
convolution equation.
The speed of this function scales as O(Nd x No) where
Nd=self.data.size and No=other.data.size.
Uses `scipy.signal.convolve`.
Parameters
----------
other : Image or numpy.ndarray
The 2D array with which to convolve the image in self.data.
This array can be an image of the same size as self, or it
can be a smaller image, such as a small gaussian to use to
smooth the larger image.
When ``other`` contains a symmetric filtering function, such
as a two-dimensional gaussian, the center of the function
should be placed at the center of pixel:
``(other.shape - 1) // 2``
If other is an MPDAF Image object, note that only its data
array is used. Masked values in this array are treated
as zero. Any variances found in other.var are ignored.
inplace : bool
If False (the default), return the results in a new Image.
If True, record the result in self and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
# Delegate the task to DataArray._convolve()
return self._convolve(signal.convolve, other=other, inplace=inplace)
[docs]
def fftconvolve(self, other, inplace=False):
"""Convolve an Image with a 2D array or another Image, using the
Fourier convolution theorem.
This function, which performs the convolution by multiplying the
Fourier transforms of the two images, is usually much faster than
Image.convolve(), except when other.data.size is small. However it
uses much more memory, so Image.convolve() is sometimes a better
choice.
Masked values in self.data and self.var are replaced with zeros before
the convolution is performed, but they are masked again after the
convolution.
If self.var exists, the variances are propagated using the equation:
result.var = self.var (*) other**2
where (*) indicates convolution. This equation can be derived by
applying the usual rules of error-propagation to the discrete
convolution equation.
The speed of this function scales as O(Nd x log(Nd)) where
Nd=self.data.size. It temporarily allocates a pair of arrays that
have the sum of the shapes of self.shape and other.shape, rounded up
to a power of two along each axis. This can involve a lot of memory
being allocated. For this reason, when other.shape is small,
Image.convolve() may be more efficient than Image.fftconvolve().
Uses `scipy.signal.fftconvolve`.
Parameters
----------
other : Image or numpy.ndarray
The 2D array with which to convolve the image in self.data. This
array can be an image of the same size as self, or it can be a
smaller image, such as a small 2D gaussian to use to smooth the
larger image.
When ``other`` contains a symmetric filtering function, such as a
two-dimensional gaussian, the center of the function should be
placed at the center of pixel:
``(other.shape - 1) // 2``
If other is an MPDAF Image object, note that only its data array
is used. Masked values in this array are treated as zero. Any
variances found in other.var are ignored.
inplace : bool
If False (the default), return the results in a new Image.
If True, record the result in self and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
# Delegate the task to DataArray._convolve()
return self._convolve(signal.fftconvolve, other=other, inplace=inplace)
[docs]
def fftconvolve_gauss(self, center=None, flux=1., fwhm=(1., 1.),
peak=False, rot=0., factor=1, unit_center=u.deg,
unit_fwhm=u.arcsec, inplace=False):
"""Return the convolution of the image with a 2D gaussian.
Parameters
----------
center : (float,float)
Gaussian center (y_peak, x_peak). If None the center of the image
is used. The unit is given by the unit_center parameter (degrees
by default).
flux : float
Integrated gaussian flux or gaussian peak value if peak is True.
fwhm : (float,float)
Gaussian fwhm (fwhm_y,fwhm_x). The unit is given by the unit_fwhm
parameter (arcseconds by default).
peak : bool
If true, flux contains a gaussian peak value.
rot : float
Angle position in degree.
factor : int
If factor<=1, gaussian value is computed in the center of each
pixel. If factor>1, for each pixel, gaussian value is the sum of
the gaussian values on the factor*factor pixels divided by the
pixel area.
unit_center : `astropy.units.Unit`
type of the center and position coordinates.
Degrees by default (use None for coordinates in pixels).
unit_fwhm : `astropy.units.Unit`
FWHM unit. Arcseconds by default (use None for radius in pixels)
inplace : bool
If False, return a convolved copy of the image (default value).
If True, convolve the original image in-place, and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
ima = gauss_image(self.shape, wcs=self.wcs, center=center,
flux=flux, fwhm=fwhm, peak=peak, rot=rot,
factor=factor, gauss=None, unit_center=unit_center,
unit_fwhm=unit_fwhm, cont=0, unit=self.unit)
# Normalize the total flux of the Gaussian.
ima.norm(typ='sum')
return self.fftconvolve(ima, inplace=inplace)
[docs]
def fftconvolve_moffat(self, center=None, flux=1., a=1.0, q=1.0,
n=2, peak=False, rot=0., factor=1,
unit_center=u.deg, unit_a=u.arcsec, inplace=False):
"""Return the convolution of the image with a 2D moffat.
Parameters
----------
center : (float,float)
Gaussian center (y_peak, x_peak). If None the center of the image
is used. The unit is given by the unit_center parameter (degrees
by default).
flux : float
Integrated gaussian flux or gaussian peak value if peak is True.
a : float
Half width at half maximum of the image in the absence of
atmospheric scattering. 1 by default. The unit is given by the
unit_a parameter (arcseconds by default).
q : float
Axis ratio, 1 by default.
n : int
Atmospheric scattering coefficient. 2 by default.
rot : float
Angle position in degree.
factor : int
If factor<=1, moffat value is computed in the center of each pixel.
If factor>1, for each pixel, moffat value is the sum
of the moffat values on the factor*factor pixels
divided by the pixel area.
peak : bool
If true, flux contains a gaussian peak value.
unit_center : `astropy.units.Unit`
type of the center and position coordinates.
Degrees by default (use None for coordinates in pixels).
unit_a : `astropy.units.Unit`
a unit. Arcseconds by default (use None for radius in pixels)
inplace : bool
If False, return a convolved copy of the image (default value).
If True, convolve the original image in-place, and return that.
Returns
-------
out : `~mpdaf.obj.Image`
"""
fwhmy = a * (2 * np.sqrt(2 ** (1.0 / n) - 1.0))
fwhmx = fwhmy / q
ima = moffat_image(self.shape, wcs=self.wcs, factor=factor,
center=center, flux=flux, fwhm=(fwhmy, fwhmx), n=n,
rot=rot, peak=peak, unit_center=unit_center,
unit_fwhm=unit_a, unit=self.unit)
ima.norm(typ='sum')
return self.fftconvolve(ima, inplace=inplace)
[docs]
def correlate2d(self, other, interp='no'):
"""Return the cross-correlation of the image with an array/image
Uses `scipy.signal.correlate2d`.
Parameters
----------
other : 2d-array or Image
Second Image or 2d-array.
interp : 'no' | 'linear' | 'spline'
if 'no', data median value replaced masked values.
if 'linear', linear interpolation of the masked values.
if 'spline', spline interpolation of the masked values.
"""
if not isinstance(other, DataArray):
# Get a copy of the data array with masked values filled.
data = self._prepare_data(interp)
res = self.copy()
res._data = signal.correlate2d(data, other, mode='same',
boundary='symm')
if res._var is not None:
res._var = signal.correlate2d(res._var, other, mode='same',
boundary='symm')
return res
elif other.ndim == 2:
# Get copies of the data arrays with masked values filled.
data = self._prepare_data(interp)
other_data = other._prepare_data(interp)
other_data = UnitMaskedArray(other_data, other.unit, self.unit)
res = self.copy()
res._data = signal.correlate2d(data, other_data, mode='same')
if res._var is not None:
res._var = signal.correlate2d(res._var, other_data,
mode='same')
return res
else:
raise IOError('Operation forbidden')
[docs]
def plot(self, title=None, scale='linear', vmin=None, vmax=None,
zscale=False, colorbar=None, var=False, show_xlabel=False,
show_ylabel=False, ax=None, unit=u.deg, use_wcs=False, **kwargs):
"""Plot the image with axes labeled in pixels.
If either axis has just one pixel, plot a line instead of an image.
Colors are assigned to each pixel value as follows. First each
pixel value, ``pv``, is normalized over the range ``vmin`` to ``vmax``,
to have a value ``nv``, that goes from 0 to 1, as follows::
nv = (pv - vmin) / (vmax - vmin)
This value is then mapped to another number between 0 and 1 which
determines a position along the colorbar, and thus the color to give
the displayed pixel. The mapping from normalized values to colorbar
position, color, can be chosen using the scale argument, from the
following options:
- 'linear': ``color = nv``
- 'log': ``color = log(1000 * nv + 1) / log(1000 + 1)``
- 'sqrt': ``color = sqrt(nv)``
- 'arcsinh': ``color = arcsinh(10*nv) / arcsinh(10.0)``
A colorbar can optionally be drawn. If the colorbar argument is given
the value 'h', then a colorbar is drawn horizontally, above the plot.
If it is 'v', the colorbar is drawn vertically, to the right of the
plot.
By default the image is displayed in its own plot. Alternatively
to make it a subplot of a larger figure, a suitable
``matplotlib.axes.Axes`` object can be passed via the ``ax`` argument.
Note that unless matplotlib interative mode has previously been enabled
by calling ``matplotlib.pyplot.ion()``, the plot window will not appear
until the next time that ``matplotlib.pyplot.show()`` is called. So to
arrange that a new window appears as soon as ``Image.plot()`` is
called, do the following before the first call to ``Image.plot()``::
import matplotlib.pyplot as plt
plt.ion()
Parameters
----------
title : str
An optional title for the figure (None by default).
scale : 'linear' | 'log' | 'sqrt' | 'arcsinh'
The stretch function to use mapping pixel values to
colors (The default is 'linear'). The pixel values are
first normalized to range from 0 for values <= vmin,
to 1 for values >= vmax, then the stretch algorithm maps
these normalized values, nv, to a position p from 0 to 1
along the colorbar, as follows:
linear: p = nv
log: p = log(1000 * nv + 1) / log(1000 + 1)
sqrt: p = sqrt(nv)
arcsinh: p = arcsinh(10*nv) / arcsinh(10.0)
vmin : float
Pixels that have values <= vmin are given the color
at the dark end of the color bar. Pixel values between
vmin and vmax are given colors along the colorbar according
to the mapping algorithm specified by the scale argument.
vmax : float
Pixels that have values >= vmax are given the color
at the bright end of the color bar. If None, vmax is
set to the maximum pixel value in the image.
zscale : bool
If True, vmin and vmax are automatically computed
using the IRAF zscale algorithm.
colorbar : str
If 'h', a horizontal colorbar is drawn above the image.
If 'v', a vertical colorbar is drawn to the right of the image.
If None (the default), no colorbar is drawn.
var : bool
If true variance array is shown in place of data array
ax : matplotlib.axes.Axes
An optional Axes instance in which to draw the image,
or None to have one created using ``matplotlib.pyplot.gca()``.
unit : `astropy.units.Unit`
The units to use for displaying world coordinates
(degrees by default). In the interactive plot, when
the mouse pointer is over a pixel in the image the
coordinates of the pixel are shown using these units,
along with the pixel value.
use_wcs : bool
If True, use `astropy.visualization.wcsaxes` to get axes
with world coordinates.
kwargs : matplotlib.artist.Artist
Optional extra keyword/value arguments to be passed to
the ``ax.imshow()`` function.
Returns
-------
out : matplotlib AxesImage
"""
import matplotlib.pyplot as plt
cax = None
# Default X and Y axes are labeled in pixels.
xlabel = 'q (pixel)'
ylabel = 'p (pixel)'
if ax is None:
if use_wcs:
ax = plt.subplot(projection=self.wcs.wcs)
xlabel = 'ra'
ylabel = 'dec'
else:
ax = plt.gca()
elif use_wcs:
self._logger.warning(
'use_wcs does not work when giving also an axis (ax)')
if var:
data_plot = self.var
else:
data_plot = self.data
# If either axis has just one pixel, plot it as a line-graph.
if self.shape[1] == 1:
# Plot a column as a line-graph
yaxis = np.arange(self.shape[0], dtype=float)
ax.plot(yaxis, data_plot)
xlabel = 'p (pixel)'
ylabel = self.unit
elif self.shape[0] == 1:
# Plot a row as a line-graph
xaxis = np.arange(self.shape[1], dtype=float)
ax.plot(xaxis, data_plot.T)
xlabel = 'q (pixel)'
ylabel = self.unit
else:
# Plot a 2D image.
# get image normalization
norm = get_plot_norm(data_plot, vmin=vmin, vmax=vmax,
zscale=zscale, scale=scale)
# Display the image.
cax = ax.imshow(data_plot, interpolation='nearest',
origin='lower', norm=norm, **kwargs)
# Create a colorbar
import matplotlib.axes as maxes
from mpl_toolkits.axes_grid1 import make_axes_locatable
divider = make_axes_locatable(ax)
if colorbar == "h":
cax2 = divider.append_axes("top", size="5%", pad=0.2,
axes_class=maxes.Axes)
cbar = plt.colorbar(cax, cax=cax2, orientation='horizontal')
for t in cbar.ax.xaxis.get_major_ticks():
t.tick1On = True
t.tick2On = True
t.label1On = False
t.label2On = True
elif colorbar == "v":
cax2 = divider.append_axes("right", size="5%", pad=0.05,
axes_class=maxes.Axes)
plt.colorbar(cax, cax=cax2)
# Keep the axis to allow other functions to overplot
# the image with contours etc.
self._ax = ax
# Label the axes if requested.
if show_xlabel:
ax.set_xlabel(xlabel)
if show_ylabel:
ax.set_ylabel(ylabel)
if title is not None:
ax.set_title(title)
# Change the way that plt.show() displays coordinates when the pointer
# is over the image, such that world coordinates are displayed with the
# specified unit, and pixel values are displayed with their native
# units.
ax.format_coord = FormatCoord(self, data_plot)
self._unit = unit
return cax
[docs]
def get_spatial_fmax(self, rot=None):
"""Return the spatial-frequency band-limits of the image along
the Y and X axes.
See the documentation of set_spatial_fmax() for an explanation
of what the band-limits are used for.
If no band limits have been specified yet, this function has the
side-effect of setting them to the band-limits dictated by the
sampling interval of the image array. Specifically, an X axis
with a sampling interval of dx can sample spatial frequencies of
up to 0.5/dx cycles per unit of dx without aliasing.
Parameters
----------
rot : float or None
Either None, to request band-limits that pertain to the
Y and X axes of the current image without any rotation,
or, if the band-limits pertain to a rotated version of
the image, the rotation angle of its Y axis westward of north
(degrees). This is defined such that if image.wcs.get_rot()
is passed to this function, the band limits for the Y and
X axes of the current image axes will be returned.
Returns
-------
out : numpy.ndarray
The spatial-frequency band-limits of the image along
the Y and X axes of the image in cycles per self.wcs.unit.
"""
# If no image angle was provided, get the current rotation angle.
if rot is None:
rot = self.wcs.get_rot()
# If no band-limits have been specified, initialize them to the
# limits currently dictated by the sampling intervals of the image.
if self._spflims is None:
self.set_spatial_fmax(0.5 / self.get_step(), self.wcs.get_rot())
# Return the frequency limits that pertain to the specified
# rotation angle.
return self._spflims.get_fmax(rot)
[docs]
def update_spatial_fmax(self, newfmax, rot=None):
"""Update the spatial-frequency band-limits recorded for the
current image.
See the documentation of set_spatial_fmax() for an explanation
of what the band-limits are used for.
If either of the new limits is less than an existing
band-limit, and the rotation angle of the new limits is
the same as the angle of the recorded limits, then the smaller
limits replace the originals.
If either of the new limits is smaller than the existing
limits, but the rotation angle for the new limits differs from
the recorded limits, then both of the original limits are
discarded and replaced by the new ones at the specified angle.
Parameters
----------
newfmax : numpy.ndarray
The frequency limits along the Y and X axes, respectively,
specified in cycles per the angular unit in self.wcs.unit.
rot : float or None
Either None, to specify band-limits that pertain to the Y
and X axes of the current image without any rotation, or,
if the band-limits pertain to a rotated version of the
image, the rotation angle of its Y axis westward of north
(degrees). This is defined such that if
image.wcs.get_rot() is passed to this function, the
band-limit newfmax[0] will be along the Y axis of the
image and newfmax[1] will be along its X axis.
"""
# If no image rotation angle was specified, assume the
# current angle.
if rot is None:
rot = self.wcs.get_rot()
# If no band-limits have been set yet, record the new limits.
if self._spflims is None:
self.set_spatial_fmax(newfmax, rot)
else:
# Get the existing spatial-frequency band limits at the
# specified angle.
oldfmax = self._spflims.get_fmax(rot)
# Are either of the new limits smaller than the old ones?
if np.any(newfmax < oldfmax):
# If the rotation angle of the recorded limits is the
# same as the rotation angle of the new limits, keep
# existing axis limits that are smaller than the new
# limits.
if np.isclose(rot, self._spflims.rot):
newfmax = np.minimum(newfmax, oldfmax)
# Record the new limits.
self.set_spatial_fmax(newfmax, rot)
[docs]
def set_spatial_fmax(self, newfmax=None, rot=None):
"""Specify the spatial-frequency band-limits of the image along
the Y and X axis. This function completely replaces any existing
band-limits. See also update_spatial_fmax().
The recorded limits are used to avoid redundantly performing
anti-aliasing measures such as low-pass filtering an image
before resampling to a lower resolution, or decreasing pixel
sizes before rotating high resolution axes onto low resolution
axes.
Parameters
----------
newfmax : numpy.ndarray
The new frequency limits along the Y and X axes or a
band-limiting ellipse, specified in cycles per the angular
unit in self.wcs.unit.
rot : float or None
Either None, to specify band-limits that pertain to the Y
and X axes of the current image without any rotation, or,
if the band-limits pertain to a rotated version of the
image, the rotation angle of its Y axis westward of north
(degrees). This is defined such that if
image.wcs.get_rot() is passed to this function, the
band-limit newfmax[0] will be along the Y axis of the
image and newfmax[1] will be along its X axis.
"""
if rot is None:
rot = self.wcs.get_rot()
self._spflims = SpatialFrequencyLimits(newfmax, rot)
[docs]
def gauss_image(shape=(101, 101), wcs=None, factor=1, gauss=None,
center=None, flux=1., fwhm=(1., 1.), peak=False, rot=0.,
cont=0, unit_center=u.deg, unit_fwhm=u.arcsec,
unit=u.dimensionless_unscaled):
"""Create a new image from a 2D gaussian.
Parameters
----------
shape : int or (int,int)
Lengths of the image in Y and X with python notation: (ny,nx).
(101,101) by default. If wcs object contains dimensions, shape is
ignored and wcs dimensions are used.
wcs : `mpdaf.obj.WCS`
World coordinates.
factor : int
If factor<=1, gaussian value is computed in the center of each pixel.
If factor>1, for each pixel, gaussian value is the sum of the gaussian
values on the factor*factor pixels divided by the pixel area.
gauss : `mpdaf.obj.Gauss2D`
Object that contains all Gaussian parameters. If it is present, the
following parameters are not used.
center : (float,float)
Gaussian center (y_peak, x_peak). If None the center of the image is
used. The unit is given by the unit_center parameter (degrees by
default).
flux : float
Integrated gaussian flux or gaussian peak value if peak is True.
fwhm : (float,float)
Gaussian fwhm (fwhm_y,fwhm_x).
The unit is given by the unit_fwhm parameter (arcseconds by default).
peak : bool
If true, flux contains a gaussian peak value.
rot : float
Angle position in degree.
cont : float
Continuum value. 0 by default.
unit_center : `astropy.units.Unit`
type of the center and position coordinates.
Degrees by default (use None for coordinates in pixels).
unit_fwhm : `astropy.units.Unit`
FWHM unit. Arcseconds by default (use None for radius in pixels)
Returns
-------
out : `~mpdaf.obj.Image`
"""
if is_int(shape):
shape = (shape, shape)
shape = np.array(shape)
wcs = wcs or WCS()
if wcs.naxis1 == 1. and wcs.naxis2 == 1.:
wcs.naxis1 = shape[1]
wcs.naxis2 = shape[0]
else:
if wcs.naxis1 != 0. or wcs.naxis2 != 0.:
shape[1] = wcs.naxis1
shape[0] = wcs.naxis2
if gauss is not None:
center = gauss.center
flux = gauss.flux
fwhm = gauss.fwhm
peak = False
rot = gauss.rot
cont = gauss.cont
if center is None:
center = (np.array(shape) - 1) / 2.0
else:
if unit_center is not None:
center = wcs.sky2pix(center, unit=unit_center)[0]
if unit_fwhm is not None:
fwhm = np.array(fwhm) / wcs.get_step(unit=unit_fwhm)
# data = np.empty(shape=shape, dtype=float)
if fwhm[1] == 0 or fwhm[0] == 0:
raise ValueError('fwhm equal to 0')
p_width = fwhm[0] * gaussian_fwhm_to_sigma
q_width = fwhm[1] * gaussian_fwhm_to_sigma
# rotation angle in rad
theta = np.pi * rot / 180.0
if peak is True:
norm = flux * 2 * np.pi * p_width * q_width
else:
norm = flux
def gauss(p, q):
cost = np.cos(theta)
sint = np.sin(theta)
xdiff = p - center[0]
ydiff = q - center[1]
return (
norm / (2 * np.pi * p_width * q_width) *
np.exp(-(xdiff * cost - ydiff * sint) ** 2 / (2 * p_width ** 2)) *
np.exp(-(xdiff * sint + ydiff * cost) ** 2 / (2 * q_width ** 2))
)
if factor > 1:
if rot == 0:
from scipy import special
X, Y = np.meshgrid(range(shape[0]), range(shape[1]))
pixcrd_min = np.array(list(zip(X.ravel(), Y.ravel()))) - 0.5
# pixsky_min = wcs.pix2sky(pixcrd)
xmin = (pixcrd_min[:, 1] - center[1]) / np.sqrt(2.0) / q_width
ymin = (pixcrd_min[:, 0] - center[0]) / np.sqrt(2.0) / p_width
pixcrd_max = np.array(list(zip(X.ravel(), Y.ravel()))) + 0.5
# pixsky_max = wcs.pix2sky(pixcrd)
xmax = (pixcrd_max[:, 1] - center[1]) / np.sqrt(2.0) / q_width
ymax = (pixcrd_max[:, 0] - center[0]) / np.sqrt(2.0) / p_width
dx = pixcrd_max[:, 1] - pixcrd_min[:, 1]
dy = pixcrd_max[:, 0] - pixcrd_min[:, 0]
data = norm * 0.25 / dx / dy \
* (special.erf(xmax) - special.erf(xmin)) \
* (special.erf(ymax) - special.erf(ymin))
data = np.reshape(data, (shape[1], shape[0])).T
else:
yy, xx = np.mgrid[:shape[0] * factor, :shape[1] * factor] / factor
data = gauss(yy, xx)
data = data.reshape(shape[0], 2, shape[1], 2).sum(axis=(1, 3))
data /= factor ** 2
else:
yy, xx = np.mgrid[:shape[0], :shape[1]]
data = gauss(yy, xx)
return Image(data=data + cont, wcs=wcs, unit=unit, copy=False, dtype=None)
[docs]
def moffat_image(shape=(101, 101), wcs=None, factor=1, moffat=None,
center=None, flux=1., fwhm=(1., 1.), peak=False, n=2,
rot=0., cont=0, unit_center=u.deg, unit_fwhm=u.arcsec,
unit=u.dimensionless_unscaled):
"""Create a new image from a 2D Moffat function.
Parameters
----------
shape : int or (int,int)
Lengths of the image in Y and X with python notation: (ny,nx).
(101,101) by default. If wcs object contains dimensions, shape is
ignored and wcs dimensions are used.
wcs : `mpdaf.obj.WCS`
World coordinates.
factor : int
If factor<=1, moffat value is computed in the center of each pixel.
If factor>1, for each pixel, moffat value is the sum
of the moffat values on the factor*factor pixels divided
by the pixel area.
moffat : `mpdaf.obj.Moffat2D`
object that contains all moffat parameters.
If it is present, following parameters are not used.
center : (float,float)
Peak center (x_peak, y_peak). The unit is genven byt the parameter
unit_center (degrees by default). If None the center of the image is
used.
flux : float
Integrated gaussian flux or gaussian peak value
if peak is True.
fwhm : (float,float)
Gaussian fwhm (fwhm_y,fwhm_x).
The unit is given by the parameter unit_fwhm (arcseconds by default)
peak : bool
If true, flux contains a gaussian peak value.
n : int
Atmospheric scattering coefficient. 2 by default.
rot : float
Angle position in degree.
cont : float
Continuum value. 0 by default.
unit_center : `astropy.units.Unit`
type of the center and position coordinates.
Degrees by default (use None for coordinates in pixels).
unit_fwhm : `astropy.units.Unit`
FWHM unit. Arcseconds by default (use None for radius in pixels)
Returns
-------
out : `~mpdaf.obj.Image`
"""
n = float(n)
if is_int(shape):
shape = (shape, shape)
shape = np.array(shape)
wcs = wcs or WCS()
if wcs.naxis1 == 1. and wcs.naxis2 == 1.:
wcs.naxis1 = shape[1]
wcs.naxis2 = shape[0]
else:
if wcs.naxis1 != 0. or wcs.naxis2 != 0.:
shape[1] = wcs.naxis1
shape[0] = wcs.naxis2
if moffat is not None:
center = moffat.center
flux = moffat.flux
fwhm = moffat.fwhm
peak = False
n = moffat.n
rot = moffat.rot
cont = moffat.cont
fwhm = np.array(fwhm)
a = fwhm[0] / (2 * np.sqrt(2 ** (1.0 / n) - 1.0))
e = fwhm[1] / fwhm[0]
if unit_fwhm is not None:
a = a / wcs.get_step(unit=unit_fwhm)[0]
if peak:
norm = flux
else:
norm = flux * (n - 1) / (np.pi * a * a * e)
if center is None:
center = np.array([(shape[0] - 1) / 2.0, (shape[1] - 1) / 2.0])
else:
if unit_center is not None:
center = wcs.sky2pix(center, unit=unit_center)[0]
# rotation angle in rad
theta = np.pi * rot / 180.0
def moffat(p, q):
cost = np.cos(theta)
sint = np.sin(theta)
xdiff = p - center[0]
ydiff = q - center[1]
return (
norm * (1 +
((xdiff * cost - ydiff * sint) / a) ** 2 +
((xdiff * sint + ydiff * cost) / a / e) ** 2) ** (-n)
)
if factor > 1:
X, Y = np.meshgrid(range(shape[0] * factor),
range(shape[1] * factor))
factor = float(factor)
pixcrd = np.array(list(zip(X.ravel() / factor, Y.ravel() / factor)))
data = moffat(pixcrd[:, 0], pixcrd[:, 1])
data = (data.reshape(shape[1], factor, shape[0], factor)
.sum(1).sum(2) / factor / factor).T
else:
yy, xx = np.mgrid[:shape[0], :shape[1]]
data = moffat(yy, xx)
return Image(data=data + cont, wcs=wcs, unit=unit, copy=False, dtype=None)
def _antialias_filter_image(data, oldstep, newstep, oldfmax=None,
window="blackman"):
"""Apply an anti-aliasing prefilter to an image to prepare
it for subsampling.
Parameters
----------
data : np.ndimage
The 2D image to be filtered.
oldstep: float or (float, float)
The cell size of the input image. This can be a single
number for both the X and Y axes, or it can be two
numbers in an iterable, ordered like (ystep,xstep)
newstep: float or (float, float)
The cell size of the output image. This can be a single
number for both the X and Y axes, or it can be two
numbers in an iterable, ordered like (ystep,xstep)
oldfmax : float,float or None
When an image has previously been filtered, this
argument can be used to indicate the frequency cutoffs
that were applied at that time along the Y and X axes,
respectively, in units of cycles per the unit of oldstep
and newstep. Image axes that have already been sufficiently
filtered will then not be refiltered redundantly. If no
band-limits have previously been established, pass this
argument as None.
window : str
The type of window function to use to filter the
FFT, chosen from:
blackman
This window suppresses ringing better than any other
window, at the expense of lowered image resolution. In
the image plane, the PSF of this window is
approximately gaussian, with a standard deviation of
around 0.96*newstep, and a FWHM of about 2.3*newstep.
gaussian
A truncated gaussian window. This has a smaller PSF
than the blackman window, however gaussians never fall
to zero, so either significant ringing will be seen due
to truncation of the gaussian, or low-level aliasing
will occur, depending on the spatial frequency coverage
of the image beyond the folding frequency. It can be a
good choice for images that only contain smoothly
varying features. It is equivalent to a convolution of
the image with both an airy profile and a gaussian of
standard deviation 0.724*newstep (FWHM 1.704*newstep).
rectangle
This window simply zeros all spatial frequencies above
the highest that can be correctly sampled by the new
pixel size. This gives the best resolution of any of
the windows, but this is marred by the strong sidelobes
of the resulting airy-profile, especially near bright
point sources and CCD saturation lines.
Returns
-------
out : numpy.ndarray, numpy.ndarray
The filtered version of the 2D input image, followed by
a 2-element array that contains the new band-limits
along the Y and X axes, respectively.
"""
# Convert oldstep into a numpy array of two float elements.
if is_number(oldstep):
oldstep = (oldstep, oldstep)
oldstep = abs(np.asarray(oldstep, dtype=float))
# Convert newstep into a numpy array of two float elements.
if is_number(newstep):
newstep = (newstep, newstep)
newstep = abs(np.asarray(newstep, dtype=float))
# If no band-limits have been specified, substitute the
# band-limits dictated by the current sampling interval.
if oldfmax is None:
oldfmax = 0.5 / oldstep
else:
oldfmax = np.minimum(oldfmax, 0.5 / oldstep)
# Calculate the maximum frequencies that will be sampled by
# the new pixel sizes along the Y and X axes.
newfmax = 0.5 / newstep
# Which axes need to be filtered?
filter_axes = newfmax < oldfmax
# Return the original image if neither axis needs filtering.
if np.all(np.logical_not(filter_axes)):
return data, oldfmax
# Get the extent of the input image as a pair of slices.
image_slice = (slice(0, data.shape[0]), slice(0, data.shape[1]))
# FFT algorithms can be extremely slow for arrays whose
# dimensions are not powers of 2. The conventional way to avoid this
# is to copy the image into a new array whose dimensions
# are powers of 2, and fill the extra pixels with zeros.
shape = 2 ** (np.ceil(np.log(np.asarray(data.shape)) /
np.log(2.0))).astype(int)
if data.shape[0] != shape[0] or data.shape[1] != shape[1]:
tmp = np.zeros(shape)
tmp[image_slice] = data
data = tmp
# Get the new dimensions of the zero-padded image.
ny, nx = shape
# Obtain the FFT of the image.
fft = np.fft.rfft2(data)
del data
# The new pixel sizes along the X and Y axes can only correctly
# sample spatial frequencies up to the values in newfmax. Set the
# cutoff frequencies for the window functions along the x and y
# axes to those frequencies.
fycut, fxcut = newfmax
# Create an array which, for each pixel in the FFT image, holds
# the radial spatial-frequency of the pixel center, divided by
# the cutoff frequency. These values will later be used to index
# the 1D window-function.
wr = np.sqrt((np.fft.rfftfreq(nx, oldstep[1]) / fxcut) ** 2 +
(np.fft.fftfreq(ny, oldstep[0]) / fycut)[np.newaxis, :].T ** 2)
# Get the requested window function as a function of frequency
# divided by its cutoff frequency.
if window is None or window == "blackman":
winfn = lambda r: np.where(r <= 1.0,
0.42 + 0.5 * np.cos(np.pi * r) +
0.08 * np.cos(2 * np.pi * r),
0.0)
# For the gaussian window the standard deviation, sigma, is
# as a fraction of the normalized cutoff frequency. Note that
# in the image plane the corresponding gaussian standard
# deviation should be newstep/(pi*sigma).
elif window == "gaussian":
sigma = 0.44
winfn = lambda r: np.exp(-0.5 * (r / sigma) ** 2)
# For the rectangular window, just multiply all pixels below the
# cutoff frequency by one, and the rest by zero.
elif window == "rectangle":
winfn = lambda r: np.where(r <= 1.0, 1.0, 0.0)
# Apply the window function to the FFT to remove frequencies above the
# cutoff frequencies.
fft *= winfn(wr)
del wr
# Perform an inverse Fourier transform to get the filtered image
data = np.fft.irfft2(fft)
del fft
# Crop the antialiased image to remove the zero-padded pixels, and
# return this along with the new spatial-frequency limits.
return data[image_slice], np.where(filter_axes, newfmax, oldfmax)
def _find_quadratic_peak(y):
"""Given an array of 3 numbers in which the first and last numbers are
less than the central number, determine the array index at which a
quadratic curve through the 3 points reaches its peak value.
Parameters
----------
y : float,float,float
The values of the curve at x=0,1,2 respectively. Note that y[1]
must be greater than both y[0] and y[2]. Otherwise +/- infinity
will be returned.
Returns
-------
xpeak : float
The floating point array index of the peak of the quadratic. This
will always be in the range 0.0 to 2.0, provided that y[0]<y[1] and
y[2]<y[1].
"""
# Given the three equations:
#
# a * x0**2 + b * x0 + c = y0
# a * x1**2 + b * x1 + c = y1
# a * x2**2 + b * x2 + c = y2
#
# a, b, and c are given by:
#
# a = 0.5 * y0 - y1 + 0.5 * y2
# b = -1.5 * y0 + 2.0 * y1 - 0.5 * y2
# c = y0
a = 0.5 * y[0] - y[1] + 0.5 * y[2]
b = -1.5 * y[0] + 2.0 * y[1] - 0.5 * y[2]
# Quadratic curves peak at: x = -b / (2*a)
return -b / (2 * a)
[docs]
class SpatialFrequencyLimits:
"""Allow one to keep track of the spatial frequency limits of an image.
Such that before resampling an image it can see if anything needs to be
done to avoid undersampling and generating aliasing artefacts in the output
image.
The band-limits are recorded as an ellipse. Most telescopes have circularly
symmetric PSFs and thus circularly symmetric spatial-frequency band limits,
but this spatial-frequency profile may become elliptical if an image is
resampled to have a lower resolution along one axis.
The ellipse is defined in its own X,Y coordinate system as follows::
xe(t)=xs*cos(t)
ye(t)=ys*sin(t)
The ye axis of the ellipse is at self.rot degrees west of north in the
image. For the Y axis of a coordinate system where Y is rot degrees west of
north, the ellipse thus has to be rotated by ``psi = (rot - self.rot)``
degrees anticlockwise to calculate the X and Y values of the ellipse in
that coordinate system::
|x(t)| = |cos(psi), -sin(psi)| |xe(t)|
|y(t)| |sin(psi), cos(psi)| |ye(t)|
Parameters
----------
fmax : float, float
The frequency limits along the Y-axis and X-axis of an
elliptically shaped band-limit (eg. cycles per degree).
rot : float
The rotation angle of the Y axis of the ellipse westward of
north (degrees). This is defined such that if
image.wcs.get_rot() is passed to this function, the Y axis of
the ellipse will be aligned with the Y axis of the image.
"""
def __init__(self, fmax, rot):
# Store the Y and X axes of the band-limiting ellipse.
self.fmax = np.array(fmax, dtype=float, copy=True)
# Record the rotation angle in degrees of the ellipse, after
# wrapping the angle into the range -180 to 180, to make it
# easy to compare with angles returned by wcs.get_rot().
self.rot = float(rot - 360.0 * np.floor(rot / 360.0 + 0.5))
[docs]
def deepcopy(self):
return SpatialFrequencyLimits(self.fmax, self.rot)
[docs]
def get_fmax(self, rot):
"""Return the spatial-frequency band-limits along a Y axis that is
'rot' degrees west of north, and an X axis that is 90 degrees
away from this Y axis in the sense of a rotation from north to east.
Parameters
----------
rot : float
The angle of the target Y axis west of north (degrees).
Returns
-------
out : numpy.ndarray
The maximum spatial frequencies along the Y and X axes at
rotation angle rot, in the same units as were used to
initialize the object.
"""
# Extract the Y and X axis radii of the ellipse.
ys, xs = self.fmax
# Compute the rotation angle of the ellipse in radians.
psi = np.deg2rad(rot - self.rot)
# Precalculate sin and cos of the ellipse rotation angle.
cos_psi = np.cos(psi)
sin_psi = np.sin(psi)
# Calculate the ellipse phases where the X and Y coordinates
# of the ellipse locus are maximized. These equations come from
# calculating d[x(t)]/dt=0 and d[y(t)]/dt=0 using the definitions
# of x(t) and y(t) that are shown in the class documentation.
t_xmax = np.arctan2(-ys * sin_psi, xs * cos_psi)
t_ymax = np.arctan2(ys * cos_psi, xs * sin_psi)
# Get the maximum X and Y coordinates of the rotated ellipse.
xmax = xs * np.cos(t_xmax) * cos_psi - ys * np.sin(t_xmax) * sin_psi
ymax = xs * np.cos(t_ymax) * sin_psi + ys * np.sin(t_ymax) * cos_psi
return np.array([ymax, xmax], dtype=float)
[docs]
def ellipse_locus(self, t, rot):
"""Return the Y,X coordinates of the band-limiting ellipse
at ellipse phase t.
Parameters
----------
t : float
The elliptical phase at which the calculate the
coordinates (radians).
rot : float
The rotation angle of the Y axis of the ellipse west
of north (degrees).
Returns
-------
out : numpy.ndarray
The Y and X coordinates of the band-limiting ellipse.
"""
# Extract the Y and X axis radii of the ellipse.
ys, xs = self.fmax
# Compute the rotation angle of the ellipse in radians.
psi = np.deg2rad(rot - self.rot)
# Precalculate sin and cos of the ellipse rotation angle.
cos_psi = np.cos(psi)
sin_psi = np.sin(psi)
# Precalculate sin and cos of the phase of the ellipse.
cos_t = np.cos(t)
sin_t = np.sin(t)
# Calculate the locus of the ellipse at phase t, using
# the equations shown in the class documentation.
x = xs * cos_t * cos_psi - ys * sin_t * sin_psi
y = xs * cos_t * sin_psi + ys * sin_t * cos_psi
return np.array([y, x], dtype=float)