Ejemplo n.º 1
0
def test_slices_edges():
    """
    Test overlap_slices when extracting along edges.
    """

    slc_lg, slc_sm = overlap_slices((10, 10), (3, 3), (1, 1), mode='strict')
    assert slc_lg[0].start == slc_lg[1].start == 0
    assert slc_lg[0].stop == slc_lg[1].stop == 3
    assert slc_sm[0].start == slc_sm[1].start == 0
    assert slc_sm[0].stop == slc_sm[1].stop == 3

    slc_lg, slc_sm = overlap_slices((10, 10), (3, 3), (8, 8), mode='strict')
    assert slc_lg[0].start == slc_lg[1].start == 7
    assert slc_lg[0].stop == slc_lg[1].stop == 10
    assert slc_sm[0].start == slc_sm[1].start == 0
    assert slc_sm[0].stop == slc_sm[1].stop == 3

    # test (0, 0) shape
    slc_lg, slc_sm = overlap_slices((10, 10), (0, 0), (0, 0))
    assert slc_lg[0].start == slc_lg[0].stop == 0
    assert slc_lg[1].start == slc_lg[1].stop == 0
    assert slc_sm[0].start == slc_sm[0].stop == 0
    assert slc_sm[1].start == slc_sm[1].stop == 0

    slc_lg, slc_sm = overlap_slices((10, 10), (0, 0), (5, 5))
    assert slc_lg[0].start == slc_lg[0].stop == 5
    assert slc_lg[1].start == slc_lg[1].stop == 5
    assert slc_sm[0].start == slc_sm[0].stop == 0
    assert slc_sm[1].start == slc_sm[1].stop == 0
Ejemplo n.º 2
0
def test_slices_no_overlap(position):
    """
    A ValueError should be raised if position contains a non-finite
    value.
    """

    with pytest.raises(ValueError):
        overlap_slices((7, 7), (3, 3), position)
Ejemplo n.º 3
0
def test_slices_partial_overlap():
    '''Compute a slice for partially overlapping arrays.'''
    temp = overlap_slices((5, ), (3, ), (0, ))
    assert temp == ((slice(0, 2, None), ), (slice(1, 3, None), ))

    temp = overlap_slices((5, ), (3, ), (0, ), mode='partial')
    assert temp == ((slice(0, 2, None), ), (slice(1, 3, None), ))

    for pos in [0, 4]:
        with pytest.raises(PartialOverlapError) as e:
            temp = overlap_slices((5, ), (3, ), (pos, ), mode='strict')
        assert 'Arrays overlap only partially.' in str(e.value)
Ejemplo n.º 4
0
def test_slices_partial_overlap():
    '''Compute a slice for partially overlapping arrays.'''
    temp = overlap_slices((5,), (3,), (0,))
    assert temp == ((slice(0, 2, None),), (slice(1, 3, None),))

    temp = overlap_slices((5,), (3,), (0,), mode='partial')
    assert temp == ((slice(0, 2, None),), (slice(1, 3, None),))

    for pos in [0, 4]:
        with pytest.raises(PartialOverlapError) as e:
            temp = overlap_slices((5,), (3,), (pos,), mode='strict')
        assert 'Arrays overlap only partially.' in str(e.value)
Ejemplo n.º 5
0
    def __init__(self, nddata, position, shape):
        if isinstance(position, SkyCoord):
            if nddata.wcs is None:
                raise ValueError('nddata must contain WCS if the input '
                                 'position is a SkyCoord')

            x, y = skycoord_to_pixel(position, nddata.wcs, mode='all')
            position = (y, x)

        data = np.asanyarray(nddata.data)
        print(data.shape, shape, position)
        slices_large, slices_small = overlap_slices(data.shape, shape,
                                                    position)
        self.slices_large = slices_large
        self.slices_small = slices_small

        data = nddata.data[slices_large]
        mask = None
        uncertainty = None
        if nddata.mask is not None:
            mask = nddata.mask[slices_large]
        if nddata.uncertainty is not None:
            uncertainty = nddata.uncertainty[slices_large]

        self.nddata = NDData(data, mask=mask, uncertainty=uncertainty)
Ejemplo n.º 6
0
    def cutout_slices(self, geom, mode="partial"):
        """Compute cutout slices.

        Parameters
        ----------
        geom : `WcsGeom`
            Parent geometry
        mode : {"trim", "partial", "strict"}
            Cutout slices mode.

        Returns
        -------
        slices : dict
            Dictionary containing "parent-slices" and "cutout-slices".
        """
        position = geom.to_image().coord_to_pix(self.center_skydir)
        slices = overlap_slices(
            large_array_shape=geom.data_shape[-2:],
            small_array_shape=self.data_shape[-2:],
            position=position[::-1],
            mode=mode
        )
        return {
            "parent-slices": slices[0],
            "cutout-slices": slices[1],
        }
Ejemplo n.º 7
0
def insert_fake_source(image, source, x0, y0, flux, slicesout=False):
    '''Insert source with some scaling into image at position x0, y0

    ``image`` will be modified in place. Pass in a copy if the original
    should be preserved.
    '''
    slice_large, slice_small = overlap_slices(image.shape[:2], source.shape,
                                              (y0, x0), 'trim')
    image[slice_large] = image[slice_large] + flux / source.sum(
    ) * source[slice_small]
    if slicesout:
        return image, slice_large, slice_small
    else:
        return image
Ejemplo n.º 8
0
    def insert_in_shape(self, array, shape, fill_value=True, dtype=np.float):
        '''
        Insert the cut down mask into the given shape.
        '''

        full_size = np.ones(shape, dtype=dtype) * fill_value

        large_slices, small_slices = \
            overlap_slices(shape, array.shape,
                           self._center_coords, mode='partial')

        full_size[large_slices] = array[small_slices]

        return full_size
Ejemplo n.º 9
0
    def __init__(self, data, position, shape, wcs=None):
        if isinstance(position, SkyCoord):
            if wcs is None:
                raise ValueError('wcs must be input if position is a '
                                 'SkyCoord')

            x, y = skycoord_to_pixel(position, wcs, mode='all')
            position = (y, x)

        data = np.asanyarray(data)
        slices_large, slices_small = overlap_slices(data.shape, shape,
                                                    position)
        self.slices_large = slices_large
        self.slices_small = slices_small
        self.data = data[slices_large]
        self.requested_position = position
        self.requested_shape = shape
Ejemplo n.º 10
0
    def nstar(self, image, star_groups):
        """
        Fit, as appropriate, a compound or single model to the given
        ``star_groups``. Groups are fitted sequentially from the
        smallest to the biggest. In each iteration, ``image`` is
        subtracted by the previous fitted group.

        Parameters
        ----------
        image : numpy.ndarray
            Background-subtracted image.
        star_groups : `~astropy.table.Table`
            This table must contain the following columns: ``id``,
            ``group_id``, ``x_0``, ``y_0``, ``flux_0``.  ``x_0`` and
            ``y_0`` are initial estimates of the centroids and
            ``flux_0`` is an initial estimate of the flux. Additionally,
            columns named as ``<param_name>_0`` are required if any
            other parameter in the psf model is free (i.e., the
            ``fixed`` attribute of that parameter is ``False``).

        Returns
        -------
        result_tab : `~astropy.table.Table`
            Astropy table that contains photometry results.
        image : numpy.ndarray
            Residual image.
        """

        result_tab = Table()
        for param_tab_name in self._pars_to_output.keys():
            result_tab.add_column(Column(name=param_tab_name))

        unc_tab = Table()
        for param, isfixed in self.psf_model.fixed.items():
            if not isfixed:
                unc_tab.add_column(Column(name=param + "_unc"))

        y, x = np.indices(image.shape)

        star_groups = star_groups.group_by('group_id')
        for n in range(len(star_groups.groups)):
            group_psf = get_grouped_psf_model(self.psf_model,
                                              star_groups.groups[n],
                                              self._pars_to_set)
            usepixel = np.zeros_like(image, dtype=bool)

            for row in star_groups.groups[n]:
                usepixel[overlap_slices(large_array_shape=image.shape,
                                        small_array_shape=self.fitshape,
                                        position=(row['y_0'], row['x_0']),
                                        mode='trim')[0]] = True

            fit_model = self.fitter(group_psf, x[usepixel], y[usepixel],
                                    image[usepixel])
            param_table = self._model_params2table(fit_model,
                                                   len(star_groups.groups[n]))
            result_tab = vstack([result_tab, param_table])

            param_cov = self.fitter.fit_info.get('param_cov', None)
            if param_cov is not None:
                unc_tab = vstack([unc_tab,
                                  self._get_uncertainties(
                                      len(star_groups.groups[n]))])

            # do not subtract if the fitting did not go well
            try:
                image = subtract_psf(image, self.psf_model, param_table,
                                     subshape=self.fitshape)
            except NoOverlapError:
                pass

        if param_cov is not None:
            result_tab = hstack([result_tab, unc_tab])

        return result_tab, image
Ejemplo n.º 11
0
    def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info,
                  fit_boxsize):
        """
        Fit an ePSF model to a single star.

        The input ``epsf`` will usually be modified by the fitting
        routine in this function.  Make a copy before calling this
        function if the original is needed.
        """

        if fit_boxsize is not None:
            try:
                xcenter, ycenter = star.cutout_center
                large_slc, small_slc = overlap_slices(star.shape,
                                                      fit_boxsize,
                                                      (ycenter, xcenter),
                                                      mode='strict')
            except (PartialOverlapError, NoOverlapError):
                warnings.warn(
                    'The star at ({0}, {1}) cannot be fit because '
                    'its fitting region extends beyond the star '
                    'cutout image.'.format(star.center[0], star.center[1]),
                    AstropyUserWarning)

                star = copy.deepcopy(star)
                star._fit_error_status = 1

                return star

            data = star.data[large_slc]
            weights = star.weights[large_slc]

            # define the origin of the fitting region
            x0 = large_slc[1].start
            y0 = large_slc[0].start
        else:
            # use the entire cutout image
            data = star.data
            weights = star.weights

            # define the origin of the fitting region
            x0 = 0
            y0 = 0

        x_oversamp = star.pixel_scale[0] / epsf.pixel_scale[0]
        y_oversamp = star.pixel_scale[1] / epsf.pixel_scale[1]
        scaled_data = data / (x_oversamp * y_oversamp)

        # define positions in the ePSF oversampled grid
        yy, xx = np.indices(data.shape, dtype=np.float)
        xx = (xx - (star.cutout_center[0] - x0)) * x_oversamp
        yy = (yy - (star.cutout_center[1] - y0)) * y_oversamp

        # define the initial guesses for fitted flux and shifts
        epsf.flux = star.flux
        epsf.x_0 = 0.0
        epsf.y_0 = 0.0

        # The oversampling factor is used in the FittableImageModel
        # evaluate method (which is use when fitting).  We do not want
        # to use oversampling here because it has been set by the ratio
        # of the ePSF and EPSFStar pixel scales.  This allows for
        # oversampling factors that differ between stars and also for
        # the factor to be different along the x and y axes.
        epsf._oversampling = 1.

        try:
            fitted_epsf = fitter(model=epsf,
                                 x=xx,
                                 y=yy,
                                 z=scaled_data,
                                 weights=weights,
                                 **fitter_kwargs)
        except TypeError:
            # fitter doesn't support weights
            fitted_epsf = fitter(model=epsf,
                                 x=xx,
                                 y=yy,
                                 z=scaled_data,
                                 **fitter_kwargs)

        fit_error_status = 0
        if fitter_has_fit_info:
            fit_info = copy.copy(fitter.fit_info)

            if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]:
                fit_error_status = 2  # fit solution was not found
        else:
            fit_info = None

        # compute the star's fitted position
        x_center = (star.cutout_center[0] +
                    (fitted_epsf.x_0.value / x_oversamp))
        y_center = (star.cutout_center[1] +
                    (fitted_epsf.y_0.value / y_oversamp))

        star = copy.deepcopy(star)
        star.cutout_center = (x_center, y_center)

        # set the star's flux to the ePSF-fitted flux
        star.flux = fitted_epsf.flux.value

        star._fit_info = fit_info
        star._fit_error_status = fit_error_status

        return star
Ejemplo n.º 12
0
    def _recenter_epsf(self, epsf_data, epsf, centroid_func=centroid_com,
                       box_size=5, maxiters=20, center_accuracy=1.0e-4):
        """
        Calculate the center of the ePSF data and shift the data so the
        ePSF center is at the center of the ePSF data array.

        Parameters
        ----------
        epsf_data : 2D `~numpy.ndarray`
            A 2D array containing the ePSF image.

        epsf : `EPSFModel` object
            The ePSF model.

        centroid_func : callable, optional
            A callable object (e.g. function or class) that is used to
            calculate the centroid of a 2D array.  The callable must
            accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and
            optionally an ``error`` keyword.  The callable object must
            return a tuple of two 1D `~numpy.ndarray`\\s, representing
            the x and y centroids.  The default is
            `~photutils.centroids.centroid_com`.

        recentering_boxsize : float or tuple of two floats, optional
            The size (in pixels) of the box used to calculate the
            centroid of the ePSF during each build iteration.  If a
            single integer number is provided, then a square box will be
            used.  If two values are provided, then they should be in
            ``(ny, nx)`` order.  The default is 5.

        maxiters : int, optional
            The maximum number of recentering iterations to perform.
            The default is 20.

        center_accuracy : float, optional
            The desired accuracy for the centers of stars.  The building
            iterations will stop if the center of the ePSF changes by
            less than ``center_accuracy`` pixels between iterations.
            The default is 1.0e-4.

        Returns
        -------
        result : 2D `~numpy.ndarray`
            The recentered ePSF data.
        """

        # Define an EPSFModel for the input data.  This EPSFModel will be
        # used to evaluate the model on a shifted pixel grid to place the
        # centroid at the array center.
        epsf = EPSFModel(data=epsf_data, origin=epsf.origin, normalize=False,
                         oversampling=epsf.oversampling)

        epsf.fill_value = 0.0
        xcenter, ycenter = epsf.origin

        dx_total = 0
        dy_total = 0
        y, x = np.indices(epsf_data.shape, dtype=np.float)

        iter_num = 0
        center_accuracy_sq = center_accuracy ** 2
        center_dist_sq = center_accuracy_sq + 1.e6
        center_dist_sq_prev = center_dist_sq + 1
        while (iter_num < maxiters and
               center_dist_sq >= center_accuracy_sq):

            iter_num += 1

            # extract a cutout from the ePSF
            slices_large, slices_small = overlap_slices(epsf_data.shape,
                                                        box_size,
                                                        (ycenter, xcenter))
            epsf_cutout = epsf_data[slices_large]
            mask = ~np.isfinite(epsf_cutout)

            # find a new center position
            xcenter_new, ycenter_new = centroid_func(epsf_cutout, mask=mask)
            xcenter_new += slices_large[1].start
            ycenter_new += slices_large[0].start

            # calculate the shift
            dx = xcenter - xcenter_new
            dy = ycenter - ycenter_new
            center_dist_sq = dx**2 + dy**2
            if center_dist_sq >= center_dist_sq_prev:  # don't shift
                break
            center_dist_sq_prev = center_dist_sq

            # Resample the ePSF data to a shifted grid to place the
            # centroid in the center of the central pixel.  The shift is
            # always performed on the input epsf_data.
            dx_total += dx    # accumulated shifts for the input epsf_data
            dy_total += dy
            epsf_data = epsf.evaluate(x=x, y=y, flux=1.0,
                                      x_0=xcenter + dx_total,
                                      y_0=ycenter + dy_total,
                                      use_oversampling=False)

        return epsf_data
Ejemplo n.º 13
0
def _extract_stars(data, catalog, size=(11, 11), use_xy=True):
    """
    Extract cutout images from a single image centered on stars defined
    in the single input catalog.

    Parameters
    ----------
    data : `~astropy.nddata.NDData`
        A `~astropy.nddata.NDData` object containing the 2D image from
        which to extract the stars. If the input ``catalog`` contains
        only the sky coordinates (i.e., not the pixel coordinates) of
        the stars then the `~astropy.nddata.NDData` object must have a
        valid ``wcs`` attribute.

    catalogs : `~astropy.table.Table`
        A single catalog of sources to be extracted from the input
        ``data``.  The center of each source can be defined either in
        pixel coordinates (in ``x`` and ``y`` columns) or sky
        coordinates (in a ``skycoord`` column containing a
        `~astropy.coordinates.SkyCoord` object).  If both are specified,
        then the value of the ``use_xy`` keyword determines which
        coordinates will be used.

    size : int or array_like (int), optional
        The extraction box size along each axis.  If ``size`` is a
        scalar then a square box of size ``size`` will be used.  If
        ``size`` has two elements, they should be in ``(ny, nx)`` order.
        The size must be greater than or equal to 3 pixel for both axes.
        Size must be odd in both axes; if either is even, it is padded
        by one to force oddness.

    use_xy : bool, optional
        Whether to use the ``x`` and ``y`` pixel positions when both
        pixel and sky coordinates are present in the input catalog
        table. If `False` then sky coordinates are used instead of pixel
        coordinates (e.g., for linked stars). The default is `True`.

    Returns
    -------
    stars : list of `EPSFStar` objects
        A list of `EPSFStar` instances containing the extracted stars.
    """

    # Force size to odd numbers such that there is always a central pixel with
    # even spacing either side of the pixel.
    if np.isscalar(size):
        size = size + 1 if size % 2 == 0 else size
    else:
        size = tuple(_size + 1 if _size % 2 == 0 else _size for _size in size)

    colnames = catalog.colnames
    if ('x' not in colnames or 'y' not in colnames) or not use_xy:
        try:
            xcenters, ycenters = data.wcs.world_to_pixel(catalog['skycoord'])
        except AttributeError:
            # for Astropy < 3.1 WCS support
            xcenters, ycenters = skycoord_to_pixel(catalog['skycoord'],
                                                   data.wcs,
                                                   origin=0,
                                                   mode='all')
    else:
        xcenters = catalog['x'].data.astype(float)
        ycenters = catalog['y'].data.astype(float)

    if 'id' in colnames:
        ids = catalog['id']
    else:
        ids = np.arange(len(catalog), dtype=int) + 1

    if data.uncertainty is None:
        weights = np.ones_like(data.data)
    else:
        if data.uncertainty.uncertainty_type == 'weights':
            weights = np.asanyarray(data.uncertainty.array, dtype=float)
        else:
            warnings.warn(
                'The data uncertainty attribute has an unsupported '
                'type.  Only uncertainty_type="weights" can be '
                'used to set weights.  Weights will be set to 1.',
                AstropyUserWarning)
            weights = np.ones_like(data.data)

    if data.mask is not None:
        weights[data.mask] = 0.

    stars = []
    for xcenter, ycenter, obj_id in zip(xcenters, ycenters, ids):
        try:
            large_slc, _ = overlap_slices(data.data.shape,
                                          size, (ycenter, xcenter),
                                          mode='strict')
            data_cutout = data.data[large_slc]
            weights_cutout = weights[large_slc]
        except (PartialOverlapError, NoOverlapError):
            stars.append(None)
            continue

        origin = (large_slc[1].start, large_slc[0].start)
        cutout_center = (xcenter - origin[0], ycenter - origin[1])
        star = EPSFStar(data_cutout,
                        weights_cutout,
                        cutout_center=cutout_center,
                        origin=origin,
                        wcs_large=data.wcs,
                        id_label=obj_id)

        stars.append(star)

    return stars
Ejemplo n.º 14
0
    def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info,
                  fit_boxsize):
        """
        Fit an ePSF model to a single star.

        The input ``epsf`` will usually be modified by the fitting
        routine in this function.  Make a copy before calling this
        function if the original is needed.
        """

        if fit_boxsize is not None:
            try:
                xcenter, ycenter = star.cutout_center
                large_slc, _ = overlap_slices(star.shape,
                                              fit_boxsize, (ycenter, xcenter),
                                              mode='strict')
            except (PartialOverlapError, NoOverlapError):
                warnings.warn(
                    'The star at ({0}, {1}) cannot be fit because '
                    'its fitting region extends beyond the star '
                    'cutout image.'.format(star.center[0], star.center[1]),
                    AstropyUserWarning)

                star = copy.deepcopy(star)
                star._fit_error_status = 1

                return star

            data = star.data[large_slc]
            weights = star.weights[large_slc]

            # define the origin of the fitting region
            x0 = large_slc[1].start
            y0 = large_slc[0].start
        else:
            # use the entire cutout image
            data = star.data
            weights = star.weights

            # define the origin of the fitting region
            x0 = 0
            y0 = 0

        # Define positions in the undersampled grid. The fitter will
        # evaluate on the defined interpolation grid, currently in the
        # range [0, len(undersampled grid)].
        yy, xx = np.indices(data.shape, dtype=np.float)
        xx = xx + x0 - star.cutout_center[0]
        yy = yy + y0 - star.cutout_center[1]

        # define the initial guesses for fitted flux and shifts
        epsf.flux = star.flux
        epsf.x_0 = 0.0
        epsf.y_0 = 0.0

        try:
            fitted_epsf = fitter(model=epsf,
                                 x=xx,
                                 y=yy,
                                 z=data,
                                 weights=weights,
                                 **fitter_kwargs)
        except TypeError:
            # fitter doesn't support weights
            fitted_epsf = fitter(model=epsf,
                                 x=xx,
                                 y=yy,
                                 z=data,
                                 **fitter_kwargs)

        fit_error_status = 0
        if fitter_has_fit_info:
            fit_info = copy.copy(fitter.fit_info)

            if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]:
                fit_error_status = 2  # fit solution was not found
        else:
            fit_info = None

        # compute the star's fitted position
        x_center = star.cutout_center[0] + fitted_epsf.x_0.value
        y_center = star.cutout_center[1] + fitted_epsf.y_0.value

        star = copy.deepcopy(star)
        star.cutout_center = (x_center, y_center)

        # set the star's flux to the ePSF-fitted flux
        star.flux = fitted_epsf.flux.value

        star._fit_info = fit_info
        star._fit_error_status = fit_error_status

        return star
Ejemplo n.º 15
0
def cutout_footprint(data,
                     position,
                     box_size=3,
                     footprint=None,
                     mask=None,
                     error=None):
    """
    Cut out a region from data (and optional mask and error) centered at
    specified (x, y) position.

    The size of the region is specified via the ``box_size`` or
    ``footprint`` keywords.  The output mask for the cutout region
    represents the combination of the input mask and footprint mask.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    position : 2 tuple
        The ``(x, y)`` pixel coordinate of the center of the region.

    box_size : scalar or tuple, optional
        The size of the region to cutout from ``data``.  If ``box_size``
        is a scalar then a square box of size ``box_size`` will be used.
        If ``box_size`` has two elements, they should be in ``(ny, nx)``
        order.  Either ``box_size`` or ``footprint`` must be defined.
        If they are both defined, then ``footprint`` overrides
        ``box_size``.

    footprint : `~numpy.ndarray` of bools, optional
        A boolean array where `True` values describe the local footprint
        region.  ``box_size=(n, m)`` is equivalent to
        ``footprint=np.ones((n, m))``.  Either ``box_size`` or
        ``footprint`` must be defined.  If they are both defined, then
        ``footprint`` overrides ``box_size``.

    mask : array_like, bool, optional
        A boolean mask with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    Returns
    -------
    region_data : `~numpy.ndarray`
        The ``data`` cutout.

    region_mask : `~numpy.ndarray`
        The ``mask`` cutout.

    region_error : `~numpy.ndarray`
        The ``error`` cutout.

    slices : tuple of slices
        Slices in each dimension of the ``data`` array used to define
        the cutout region.
    """

    if len(position) != 2:
        raise ValueError('position must have a length of 2')

    if footprint is None:
        if box_size is None:
            raise ValueError('box_size or footprint must be defined.')
        if not isinstance(box_size, collections.Iterable):
            shape = (box_size, box_size)
        else:
            if len(box_size) != 2:
                raise ValueError('box_size must have a length of 2')
            shape = box_size
        footprint = np.ones(shape, dtype=bool)
    else:
        footprint = np.asanyarray(footprint, dtype=bool)

    slices_large, slices_small = overlap_slices(data.shape, footprint.shape,
                                                position[::-1])
    region_data = data[slices_large]

    if error is not None:
        region_error = error[slices_large]
    else:
        region_error = None

    if mask is not None:
        region_mask = mask[slices_large]
    else:
        region_mask = np.zeros_like(region_data, dtype=bool)
    footprint_mask = ~footprint
    footprint_mask = footprint_mask[slices_small]  # trim if necessary
    region_mask = np.logical_or(region_mask, footprint_mask)

    return region_data, region_mask, region_error, slices_large
Ejemplo n.º 16
0
def centroid_quadratic(data, xpeak=None, ypeak=None, fit_boxsize=5,
                       search_boxsize=None, mask=None):
    """
    Calculate the centroid of an n-dimensional array by fitting a 2D
    quadratic polynomial.

    A second degree 2D polynomial is fit within a small region of the
    data defined by ``fit_boxsize`` to calculate the centroid position.
    The initial center of the fitting box can specified using the
    ``xpeak`` and ``ypeak`` keywords. If both ``xpeak`` and ``ypeak``
    are `None`, then the box will be centered at the position of the
    maximum value in the input ``data``.

    If ``xmax`` and ``ymax`` are specified, the ``search_boxsize``
    optional keyword can be used to further refine the initial center of
    the fitting box by searching for the position of the maximum pixel
    within a box of size ``search_boxsize``.

    Parameters
    ----------
    data : numpy.ndarray
        Image data.

    xpeak, ypeak : float or `None`, optional
        The initial guess of the position of the centroid. When both
        ``xpeak`` and ``ypeak`` are `None` then the position of the
        maximum value in the input ``data`` will be used as the initial
        guess.

    fit_boxsize : int or tuple of int, optional
        The size (in pixels) of the box used to define the fitting
        region. If ``fit_boxsize`` has two elements, they should be in
        ``(ny, nx)`` order. If ``fit_boxsize`` is a scalar then a square
        box of size ``fit_boxsize`` will be used.

    search_boxsize : int or tuple of int, optional
        The size (in pixels) of the box used to search for the maximum
        pixel value if ``xpeak`` and ``ypeak`` are both `None`. If
        ``fit_boxsize`` has two elements, they should be in ``(ny, nx)``
        order. If ``fit_boxsize`` is a scalar then a square box of size
        ``fit_boxsize`` will be used.  This parameter is ignored when
        ``xmax`` and ``ymax`` are both `None`.  In that case, the entire
        array is search for the maximum value.

    mask : bool `~numpy.ndarray`, optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.
        Masked data are excluded from calculations.

    Returns
    -------
    centroid : `~numpy.ndarray`
        The ``x, y`` coordinates of the centroid.
    """
    if ((xpeak is None and ypeak is not None)
            or (xpeak is not None and ypeak is None)):
        raise ValueError('xpeak and ypeak must both be input or "None"')

    data = np.asanyarray(data, dtype=float)
    ny, nx = data.shape

    badmask = ~np.isfinite(data)
    if np.any(badmask):
        warnings.warn('Input data contains non-finite values (e.g., NaN or '
                      'inf) that were automatically masked.',
                      AstropyUserWarning)
        data[badmask] = np.nan

    if mask is not None:
        if data.shape != mask.shape:
            raise ValueError('data and mask must have the same shape.')
        data[mask] = np.nan

    fit_boxsize = _process_boxsize(fit_boxsize, data.shape)

    if np.product(fit_boxsize) < 6:
        raise ValueError('fit_boxsize is too small.  6 values are required '
                         'to fit a 2D quadratic polynomial.')

    if xpeak is None:  # and ypeak too
        yidx, xidx = np.unravel_index(np.nanargmax(data), data.shape)
    else:
        xidx = _py2intround(xpeak)
        yidx = _py2intround(ypeak)

        if search_boxsize is not None:
            search_boxsize = _process_boxsize(search_boxsize, data.shape)

            slc_data, slc_cutout = overlap_slices(data.shape, search_boxsize,
                                                  (yidx, xidx), mode='trim')
            cutout = data[slc_data]
            yidx, xidx = np.unravel_index(np.nanargmax(cutout), cutout.shape)
            xidx += slc_data[1].start
            yidx += slc_data[0].start

    # if peak is at the edge of the data, return the position of the maximum
    if xidx == 0 or xidx == nx - 1 or yidx == 0 or yidx == ny - 1:
        warnings.warn('maximum value is at the edge of the data and its '
                      'position was returned; no quadratic fit was '
                      'performed', AstropyUserWarning)
        return np.array((xidx, yidx), dtype=float)

    # extract the fitting region
    slc_data, slc_cutout = overlap_slices(data.shape, fit_boxsize,
                                          (yidx, xidx), mode='trim')
    xidx0, xidx1 = (slc_data[1].start, slc_data[1].stop)
    yidx0, yidx1 = (slc_data[0].start, slc_data[0].stop)

    # shift the fitting box if it was clipped by the data edge
    if (xidx1 - xidx0) < fit_boxsize[1]:
        if xidx0 == 0:
            xidx1 = min(nx, xidx0 + fit_boxsize[1])
        if xidx1 == nx:
            xidx0 = max(0, xidx1 - fit_boxsize[1])
    if (yidx1 - yidx0) < fit_boxsize[0]:
        if yidx0 == 0:
            yidx1 = min(ny, yidx0 + fit_boxsize[0])
        if yidx1 == ny:
            yidx0 = max(0, yidx1 - fit_boxsize[0])

    cutout = data[yidx0:yidx1, xidx0:xidx1].ravel()
    if np.count_nonzero(~np.isnan(cutout)) < 6:
        warnings.warn('at least 6 unmasked data points are required to '
                      'perform a 2D quadratic fit',
                      AstropyUserWarning)
        return np.array((np.nan, np.nan))

    # fit a 2D quadratic polynomial to the fitting region
    xi = np.arange(xidx0, xidx1)
    yi = np.arange(yidx0, yidx1)
    x, y = np.meshgrid(xi, yi)
    x = x.ravel()
    y = y.ravel()
    coeff_matrix = np.vstack((np.ones_like(x), x, y, x * y, x * x, y * y)).T

    try:
        c = np.linalg.lstsq(coeff_matrix, cutout, rcond=None)[0]
    except np.linalg.LinAlgError:
        warnings.warn('quadratic fit failed', AstropyUserWarning)
        return np.array((np.nan, np.nan))

    # analytically find the maximum of the polynomial
    _, c10, c01, c11, c20, c02 = c
    det = 4 * c20 * c02 - c11**2
    if det <= 0 or ((c20 > 0.0 and c02 >= 0.0) or (c20 >= 0.0 and c02 > 0.0)):
        warnings.warn('quadratic fit does not have a maximum',
                      AstropyUserWarning)
        return np.array((np.nan, np.nan))

    xm = (c01 * c11 - 2.0 * c02 * c10) / det
    ym = (c10 * c11 - 2.0 * c20 * c01) / det
    if xm > 0.0 and xm < (nx - 1.0) and ym > 0.0 and ym < (ny - 1.0):
        xycen = np.array((xm, ym), dtype=float)
    else:
        warnings.warn('quadratic polynomial maximum value falls outside '
                      'of the image', AstropyUserWarning)
        return np.array((np.nan, np.nan))

    return xycen
Ejemplo n.º 17
0
def listpixels(data, position, shape, subarray_indices=False, wcs=None):
    """
    Return a `~astropy.table.Table` listing the ``(row, col)``
    (``(y, x)``) positions and ``data`` values for a subarray.

    Given a position of the center of the subarray, with respect to the
    large array, the array indices and values are returned.  This
    function takes care of the correct behavior at the boundaries, where
    the small array is appropriately trimmed.

    Parameters
    ----------
    data : array-like
        The input data.

    position : tuple (int) or `~astropy.coordinates.SkyCoord`
        The position of the subarray center with respect to the data
        array.  The position can be specified either as an integer
        ``(row, col)`` (``(y, x)``) tuple or a
        `~astropy.coordinates.SkyCoord`, in which case ``wcs`` is a
        required input.

    shape : tuple (int)
        The integer shape (``(ny, nx)``) of the subarray.

    subarray_indices : bool, optional
        If `True` then the returned positions are relative to the small
        subarray.  If `False` (default) then the returned positions are
        relative to the ``data`` array.

    wcs : `~astropy.wcs.WCS`, optional
        The WCS transformation to use if ``position`` is a
        `~astropy.coordinates.SkyCoord`.

    Returns
    -------
    table : `~astropy.table.Table`
        A table containing the ``x`` and ``y`` positions and data
        values.

    Notes
    -----
    This function is decorated with `~astropy.nddata.support_nddata` and
    thus supports `~astropy.nddata.NDData` objects as input.

    See Also
    --------
    :func:`astropy.nddata.utils.overlap_slices`

    Examples
    --------
    >>> import numpy as np
    >>> from imutils import listpixels
    >>> data = np.arange(625).reshape(25, 25)
    >>> tbl = listpixels(data, (10, 12), (3, 3))
    >>> print(len(tbl))
    3

    >>> tbl.pprint(max_lines=-1)
     x   y  value
    --- --- -----
     11   9   236
     12   9   237
     13   9   238
     11  10   261
     12  10   262
     13  10   263
     11  11   286
     12  11   287
     13  11   288
    """

    if isinstance(position, SkyCoord):
        if wcs is None:
            raise ValueError('wcs must be input if positions is a SkyCoord')

        x, y = skycoord_to_pixel(position, wcs, mode='all')
        position = (y, x)

    data = np.asanyarray(data)
    slices_large, slices_small = overlap_slices(data.shape, shape, position)
    slices = slices_large
    yy, xx = np.mgrid[slices]
    values = data[yy, xx]

    if subarray_indices:
        slices = slices_small
        yy, xx = np.mgrid[slices]

    tbl = Table()
    tbl['x'] = xx.ravel()
    tbl['y'] = yy.ravel()
    tbl['value'] = values.ravel()

    return tbl
Ejemplo n.º 18
0
    def nstar(self, image, star_groups):
        """
        Fit, as appropriate, a compound or single model to the given
        ``star_groups``. Groups are fitted sequentially from the
        smallest to the biggest. In each iteration, ``image`` is
        subtracted by the previous fitted group.

        Parameters
        ----------
        image : numpy.ndarray
            Background-subtracted image.
        star_groups : `~astropy.table.Table`
            This table must contain the following columns: ``id``,
            ``group_id``, ``x_0``, ``y_0``, ``flux_0``.  ``x_0`` and
            ``y_0`` are initial estimates of the centroids and
            ``flux_0`` is an initial estimate of the flux. Additionally,
            columns named as ``<param_name>_0`` are required if any
            other parameter in the psf model is free (i.e., the
            ``fixed`` attribute of that parameter is ``False``).

        Returns
        -------
        result_tab : `~astropy.table.Table`
            Astropy table that contains photometry results.
        image : numpy.ndarray
            Residual image.
        """

        result_tab = Table()
        for param_tab_name in self._pars_to_output.keys():
            result_tab.add_column(Column(name=param_tab_name))

        unc_tab = Table()
        for param, isfixed in self.psf_model.fixed.items():
            if not isfixed:
                unc_tab.add_column(Column(name=param + "_unc"))

        y, x = np.indices(image.shape)

        star_groups = star_groups.group_by('group_id')
        for n in range(len(star_groups.groups)):
            group_psf = get_grouped_psf_model(self.psf_model,
                                              star_groups.groups[n],
                                              self._pars_to_set)
            usepixel = np.zeros_like(image, dtype=np.bool)

            for row in star_groups.groups[n]:
                usepixel[overlap_slices(large_array_shape=image.shape,
                                        small_array_shape=self.fitshape,
                                        position=(row['y_0'], row['x_0']),
                                        mode='trim')[0]] = True

            fit_model = self.fitter(group_psf, x[usepixel], y[usepixel],
                                    image[usepixel])
            param_table = self._model_params2table(fit_model,
                                                   len(star_groups.groups[n]))
            result_tab = vstack([result_tab, param_table])

            if 'param_cov' in self.fitter.fit_info.keys():
                unc_tab = vstack([unc_tab,
                                  self._get_uncertainties(
                                      len(star_groups.groups[n]))])
            try:
                from astropy.nddata.utils import NoOverlapError
            except ImportError:
                raise ImportError("astropy 1.1 or greater is required in "
                                  "order to use this class.")
            # do not subtract if the fitting did not go well
            try:
                image = subtract_psf(image, self.psf_model, param_table,
                                     subshape=self.fitshape)
            except NoOverlapError:
                pass

        if 'param_cov' in self.fitter.fit_info.keys():
            result_tab = hstack([result_tab, unc_tab])

        return result_tab, image
Ejemplo n.º 19
0
def test_slices_overlap_wrong_mode():
    '''Call overlap_slices with non-existing mode.'''
    with pytest.raises(ValueError) as e:
        overlap_slices((5,), (3,), (0,), mode='full')
    assert "Mode can be only" in str(e.value)
Ejemplo n.º 20
0
def centroid_sources(data, xpos, ypos, box_size=11, footprint=None,
                     error=None, mask=None, centroid_func=centroid_com):
    """
    Calculate the centroid of sources at the defined positions.

    A cutout image centered on each input position will be used to
    calculate the centroid position.  The cutout image is defined either
    using the ``box_size`` or ``footprint`` keyword.  The ``footprint``
    keyword can be used to create a non-rectangular cutout image.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    xpos, ypos : float or array-like of float
        The initial ``x`` and ``y`` pixel position(s) of the center
        position.  A cutout image centered on this position be used to
        calculate the centroid.

    box_size : int or array-like of int, optional
        The size of the cutout image along each axis.  If ``box_size``
        is a number, then a square cutout of ``box_size`` will be
        created.  If ``box_size`` has two elements, they should be in
        ``(ny, nx)`` order.

    footprint : `~numpy.ndarray` of bools, optional
        A 2D boolean array where `True` values describe the local
        footprint region to cutout.  ``footprint`` can be used to create
        a non-rectangular cutout image, in which case the input ``xpos``
        and ``ypos`` represent the center of the minimal bounding box
        for the input ``footprint``.  ``box_size=(n, m)`` is equivalent
        to ``footprint=np.ones((n, m))``.  Either ``box_size`` or
        ``footprint`` must be defined.  If they are both defined, then
        ``footprint`` overrides ``box_size``.

    mask : array_like, bool, optional
        A 2D boolean array with the same shape as ``data``, where a
        `True` value indicates the corresponding element of ``data`` is
        masked.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.
        ``error`` must have the same shape as ``data``.  ``error`` will
        be used only if supported by the input ``centroid_func``.

    centroid_func : callable, optional
        A callable object (e.g. function or class) that is used to
        calculate the centroid of a 2D array.  The ``centroid_func``
        must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and
        optionally an ``error`` keyword.  The callable object must
        return a tuple of two 1D `~numpy.ndarray`\s, representing the x
        and y centroids.  The default is
        `~photutils.centroids.centroid_com`.

    Returns
    -------
    xcentroid, ycentroid : `~numpy.ndarray`
        The ``x`` and ``y`` pixel position(s) of the centroids.
    """

    xpos = np.atleast_1d(xpos)
    ypos = np.atleast_1d(ypos)
    if xpos.ndim != 1:
        raise ValueError('xpos must be a 1D array.')
    if ypos.ndim != 1:
        raise ValueError('ypos must be a 1D array.')

    if footprint is None:
        if box_size is None:
            raise ValueError('box_size or footprint must be defined.')
        else:
            box_size = np.atleast_1d(box_size)
            if len(box_size) == 1:
                box_size = np.repeat(box_size, 2)
            if len(box_size) != 2:
                raise ValueError('box_size must have 1 or 2 elements.')

        footprint = np.ones(box_size, dtype=bool)
    else:
        footprint = np.asanyarray(footprint, dtype=bool)
        if footprint.ndim != 2:
            raise ValueError('footprint must be a 2D array.')

    use_error = False
    spec = inspect.getfullargspec(centroid_func)
    if 'mask' not in spec.args:
        raise ValueError('The input "centroid_func" must have a "mask" '
                         'keyword.')
    if 'error' in spec.args:
        use_error = True

    xcentroids = []
    ycentroids = []
    for xp, yp in zip(xpos, ypos):
        slices_large, slices_small = overlap_slices(data.shape,
                                                    footprint.shape, (yp, xp))
        data_cutout = data[slices_large]

        mask_cutout = None
        if mask is not None:
            mask_cutout = mask[slices_large]

        footprint_mask = ~footprint
        # trim footprint mask if partial overlap on the data
        footprint_mask = footprint_mask[slices_small]

        if mask_cutout is None:
            mask_cutout = footprint_mask
        else:
            # combine the input mask and footprint mask
            mask_cutout = np.logical_or(mask_cutout, footprint_mask)

        if error is not None and use_error:
            error_cutout = error[slices_large]
            xcen, ycen = centroid_func(data_cutout, mask=mask_cutout,
                                       error=error_cutout)
        else:
            xcen, ycen = centroid_func(data_cutout, mask=mask_cutout)

        xcentroids.append(xcen + slices_large[1].start)
        ycentroids.append(ycen + slices_large[0].start)

    return np.array(xcentroids), np.array(ycentroids)
Ejemplo n.º 21
0
def test_slices_no_overlap(pos):
    '''If there is no overlap between arrays, an error should be raised.'''
    with pytest.raises(NoOverlapError):
        overlap_slices((5, 5), (2, 2), pos)
Ejemplo n.º 22
0
def test_slices_pos_different_dim():
    '''Position must have same dim as arrays.'''
    with pytest.raises(ValueError) as e:
        overlap_slices((4, 5), (1, 2), (0, 0, 3))
    assert "the same number of dimensions" in str(e.value)
Ejemplo n.º 23
0
    def nstar(self, image, star_groups):
        """
        Fit, as appropriate, a compound or single model to the given
        ``star_groups``. Groups are fitted sequentially from the
        smallest to the biggest. In each iteration, ``image`` is
        subtracted by the previous fitted group.

        Parameters
        ----------
        image : numpy.ndarray
            Background-subtracted image.
        star_groups : `~astropy.table.Table`
            This table must contain the following columns: ``id``,
            ``group_id``, ``x_0``, ``y_0``, ``flux_0``.  ``x_0`` and
            ``y_0`` are initial estimates of the centroids and
            ``flux_0`` is an initial estimate of the flux.

        Returns
        -------
        result_tab : `~astropy.table.Table`
            Astropy table that contains photometry results.
        image : numpy.ndarray
            Residual image.
        """

        result_tab = Table([[], [], [], [], []],
                           names=('id', 'group_id', 'x_fit', 'y_fit',
                                  'flux_fit'),
                           dtype=('i4', 'i4', 'f8', 'f8', 'f8'))
        star_groups = star_groups.group_by('group_id')

        y, x = np.indices(image.shape)

        for n in range(len(star_groups.groups)):
            group_psf = get_grouped_psf_model(self.psf_model, star_groups.groups[n])
            usepixel = np.zeros_like(image, dtype=np.bool)

            for row in star_groups.groups[n]:
                usepixel[overlap_slices(large_array_shape=image.shape,
                                        small_array_shape=self.fitshape,
                                        position=(row['y_0'], row['x_0']),
                                        mode='trim')[0]] = True

            fit_model = self.fitter(group_psf, x[usepixel], y[usepixel],
                                    image[usepixel])
            param_table = self._model_params2table(fit_model,
                                                   star_groups.groups[n])
            result_tab = vstack([result_tab, param_table])

            try:
                from astropy.nddata.utils import NoOverlapError
            except ImportError:
                raise ImportError("astropy 1.1 or greater is required in "
                                  "order to use this class.")
            # do not subtract if the fitting did not go well
            try:
                image = subtract_psf(image, self.psf_model, param_table,
                                     subshape=self.fitshape)
            except NoOverlapError:
                pass

        return result_tab, image
Ejemplo n.º 24
0
    def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info,
                  fit_boxsize):
        """
        Fit an ePSF model to a single star.

        The input ``epsf`` will usually be modified by the fitting
        routine in this function.  Make a copy before calling this
        function if the original is needed.
        """

        if fit_boxsize is not None:
            try:
                xcenter, ycenter = star.cutout_center
                large_slc, small_slc = overlap_slices(star.shape,
                                                      fit_boxsize,
                                                      (ycenter, xcenter),
                                                      mode='strict')
            except (PartialOverlapError, NoOverlapError):
                warnings.warn(
                    'The star at ({0}, {1}) cannot be fit because '
                    'its fitting region extends beyond the star '
                    'cutout image.'.format(star.center[0], star.center[1]),
                    AstropyUserWarning)

                star = copy.deepcopy(star)
                star._fit_error_status = 1

                return star

            data = star.data[large_slc]
            weights = star.weights[large_slc]

            # define the origin of the fitting region
            x0 = large_slc[1].start
            y0 = large_slc[0].start
        else:
            # use the entire cutout image
            data = star.data
            weights = star.weights

            # define the origin of the fitting region
            x0 = 0
            y0 = 0

        scaled_data = data / np.prod(epsf._oversampling)

        # define positions in the ePSF oversampled grid
        yy, xx = np.indices(data.shape, dtype=np.float)
        xx = (xx - (star.cutout_center[0] - x0)) * epsf._oversampling[0]
        yy = (yy - (star.cutout_center[1] - y0)) * epsf._oversampling[1]

        # define the initial guesses for fitted flux and shifts
        epsf.flux = star.flux
        epsf.x_0 = 0.0
        epsf.y_0 = 0.0

        # create copy to avoid overwriting original oversampling factor
        _epsf = epsf.copy()
        _epsf._oversampling = np.array([1., 1.])

        try:
            fitted_epsf = fitter(model=_epsf,
                                 x=xx,
                                 y=yy,
                                 z=scaled_data,
                                 weights=weights,
                                 **fitter_kwargs)
        except TypeError:
            # fitter doesn't support weights
            fitted_epsf = fitter(model=_epsf,
                                 x=xx,
                                 y=yy,
                                 z=scaled_data,
                                 **fitter_kwargs)

        fit_error_status = 0
        if fitter_has_fit_info:
            fit_info = copy.copy(fitter.fit_info)

            if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]:
                fit_error_status = 2  # fit solution was not found
        else:
            fit_info = None

        # compute the star's fitted position
        x_center = (star.cutout_center[0] +
                    (fitted_epsf.x_0.value / epsf._oversampling[0]))
        y_center = (star.cutout_center[1] +
                    (fitted_epsf.y_0.value / epsf._oversampling[1]))

        star = copy.deepcopy(star)
        star.cutout_center = (x_center, y_center)

        # set the star's flux to the ePSF-fitted flux
        star.flux = fitted_epsf.flux.value

        star._fit_info = fit_info
        star._fit_error_status = fit_error_status

        return star
Ejemplo n.º 25
0
    def nstar(self, image, star_groups):
        """
        Fit, as appropriate, a compound or single model to the given
        ``star_groups``. Groups are fitted sequentially from the
        smallest to the biggest. In each iteration, ``image`` is
        subtracted by the previous fitted group.

        Parameters
        ----------
        image : numpy.ndarray
            Background-subtracted image.
        star_groups : `~astropy.table.Table`
            This table must contain the following columns: ``id``,
            ``group_id``, ``x_0``, ``y_0``, ``flux_0``.  ``x_0`` and
            ``y_0`` are initial estimates of the centroids and
            ``flux_0`` is an initial estimate of the flux.

        Returns
        -------
        result_tab : `~astropy.table.Table`
            Astropy table that contains photometry results.
        image : numpy.ndarray
            Residual image.
        """

        result_tab = Table([[], [], [], [], []],
                           names=('id', 'group_id', 'x_fit', 'y_fit',
                                  'flux_fit'),
                           dtype=('i4', 'i4', 'f8', 'f8', 'f8'))
        star_groups = star_groups.group_by('group_id')

        y, x = np.indices(image.shape)

        for n in range(len(star_groups.groups)):
            group_psf = get_grouped_psf_model(self.psf_model,
                                              star_groups.groups[n])
            usepixel = np.zeros_like(image, dtype=np.bool)

            for row in star_groups.groups[n]:
                usepixel[overlap_slices(large_array_shape=image.shape,
                                        small_array_shape=self.fitshape,
                                        position=(row['y_0'], row['x_0']),
                                        mode='trim')[0]] = True

            fit_model = self.fitter(group_psf, x[usepixel], y[usepixel],
                                    image[usepixel])
            param_table = self._model_params2table(fit_model,
                                                   star_groups.groups[n])
            result_tab = vstack([result_tab, param_table])

            try:
                from astropy.nddata.utils import NoOverlapError
            except ImportError:
                raise ImportError("astropy 1.1 or greater is required in "
                                  "order to use this class.")
            # do not subtract if the fitting did not go well
            try:
                image = subtract_psf(image,
                                     self.psf_model,
                                     param_table,
                                     subshape=self.fitshape)
            except NoOverlapError:
                pass

        return result_tab, image
Ejemplo n.º 26
0
def phot_sherpa(reduced_images621,
                reduced_images845,
                indexname,
                row,
                psf_621,
                psf_845,
                slice_size=11,
                debug=False,
                maxdxdy=1,
                **kwargs):
    '''Perform 2 band photometry

    This function performs two band photometry on a single source. Photometry
    is done by fitting a PSF model simultaneously to data in both bands. Only a
    single object is fit (so this does not take into account blended sources),
    but this function is still providing a benefit over simple aperture
    photometry:

    - Because the functional form of the PSF is fit, it does work
    in the presence of some masked pixels (e.g. if the center of the source is
    saturated).

    - This function couples the source position in both
    bands. Source positions are allowed to vary within a small range around the
    input source position. This allows for small errors in the WCS of the two
    images or for differences that arise because we extract the
    Cepheid-centered images to full-pixels, so that sub-pixel differences in
    the position can arise.
    '''
    i = indexname(row['TARGNAME'])

    # Set up data #
    # Note how x and y are reversed to match order of array
    slice_large, slice_small = overlap_slices(
        reduced_images621[:, :, 0].shape, (slice_size, slice_size),
        (row['ycentroid'], row['xcentroid']), 'trim')
    x0axis, x1axis = np.mgrid[slice_large]
    im621 = reduced_images621[:, :, i][slice_large]
    im845 = reduced_images845[:, :, i][slice_large]
    data621 = sherpa.data.Data2D('img621',
                                 x0axis.ravel(),
                                 x1axis.ravel(),
                                 im621.ravel(),
                                 shape=(slice_size, slice_size))
    data621.mask = ~im621.mask.ravel()
    data845 = sherpa.data.Data2D('img845',
                                 x0axis.ravel(),
                                 x1axis.ravel(),
                                 im845.ravel(),
                                 shape=(slice_size, slice_size))
    data845.mask = ~im845.mask.ravel()

    # Set up model #
    colnames = ['ycentroid', 'xcentroid']

    for psf in [psf_621, psf_845]:
        for j, par in enumerate([psf.xpos, psf.ypos]):
            # Set min/max to large numbers to make sure assignment works
            par.min = -1e10
            par.max = 1e10
            # set value
            par.val = row[colnames[j]]
            # Then restrict min/max to the range we want
            par.max = par.val + maxdxdy
            par.min = par.val - maxdxdy

    psf_621.ampl = data621.y.max()
    psf_845.ampl = data845.y.max()

    # Prepare and perform combined fit #
    # Code for combined fitting - but that turned out not to be necessary
    # databoth = sherpa.data.DataSimulFit('bothdata', (data621, data845))
    # modelboth = sherpa.models.SimulFitModel('bothmodel', (psf_621, psf_845))
    # fitboth = sherpa.fit.Fit(databoth, modelboth, **kwargs)
    # fitres = fitboth.fit()
    fit621 = sherpa.fit.Fit(data621, psf_621, **kwargs)
    fit845 = sherpa.fit.Fit(data845, psf_845, **kwargs)
    fit621.fit()
    fit845.fit()

    # Format output #
    # Get full images so that we can construct residual images of full size
    im621 = reduced_images621[:, :, i]
    x0axis, x1axis = np.mgrid[0:im621.shape[0], 0:im621.shape[1]]
    im845 = reduced_images845[:, :, i]
    resim621 = im621 - psf_621(x0axis.ravel(), x1axis.ravel()).reshape(
        im621.shape)
    resim845 = im845 - psf_845(x0axis.ravel(), x1axis.ravel()).reshape(
        im845.shape)

    outtab = Table({
        'TARGNAME': [row['TARGNAME']],
        'y_621': [psf_621.xpos.val],
        'x_621': [psf_621.ypos.val],
        'y_845': [psf_845.xpos.val],
        'x_845': [psf_845.ypos.val],
        'mag_621': [betamodel2mag(psf_621, 'F621M')],
        'mag_845': [betamodel2mag(psf_845, 'F845M')],
        'x_0': [row['xcentroid']],
        'y_0': [row['ycentroid']],
        'residual_image_621':
        resim621.reshape(1, resim621.shape[0], resim621.shape[1]),
        'residual_image_845':
        resim845.reshape(1, resim845.shape[0], resim845.shape[1]),
    })
    if debug:
        return data621, data845, fit621, fit845, outtab
    else:
        return outtab
Ejemplo n.º 27
0
def test_slices_overlap_wrong_mode():
    '''Call overlap_slices with non-existing mode.'''
    with pytest.raises(ValueError) as e:
        overlap_slices((5, ), (3, ), (0, ), mode='full')
    assert "Mode can be only" in str(e.value)
Ejemplo n.º 28
0
def fit_sources(image1d, psfbase, shape, normperim, medianim, mastermask,
                threshold=12, **kwargs):
    '''find and fit sources in the image

    perform PSF subtraction and then find and fit sources
    see comments in code for details

    Parameters
    ----------
    image1d : ndarray
        flattened, normalized image
    psfbase : ndarray
       2d array of psf templates (PSF library)
    threshold : float
        Detection threshold. Higher numbers find only the stronger sources.
        Experiment to find the right value.
    kwargs : dict or names arguments
        arguments for daofind (fwmh, min and max roundness, etc.)

    Returns
    -------
    fluxes_gaussian : astropy.table.Table
    imag :
        PSF subtracted image
    scaled_im :
        PSF subtracted image in daofind scaling

    '''
    psf_coeff = psf_from_projection(image1d, psfbase)
    im = image1d - np.dot(psfbase, psf_coeff)
    bkg_sigma = 1.48 * mad(im)

    # Do source detection on 2d, scaled image
    scaled_im = remove_normmask(im.reshape((-1, 1)), np.ones(1), np.ones_like(medianim), mastermask).reshape(shape)
    imag = remove_normmask(im.reshape((-1, 1)), normperim, medianim, mastermask).reshape(shape)
    sources = photutils.daofind(scaled_im, threshold=threshold * bkg_sigma, **kwargs)

    if len(sources) == 0:
        return None, imag, scaled_im
    else:
        # insert extra step here to find the brightest source, subtract it and
        # redo the PSF fit or add a PSF model to psfbase to improve the PSF fit
        # I think 1 level of that is enough, no infinite recursion.
        # Idea 1: mask out a region around the source, so that this does not
        #         influence the PSF fit.
        newmask = deepcopy(mastermask).reshape(shape)
        for source in sources:
            sl, temp = overlap_slices(shape, [9,9], [source['xcentroid'], source['ycentroid']])
            newmask[sl[0], sl[1]] = True
        newmask = newmask.flatten()

        psf_coeff = psf_from_projection(image1d[~(newmask[~mastermask])],
                                        psfbase[~(newmask[~mastermask]), :])
        im = image1d - np.dot(psfbase, psf_coeff)
        scaled_im = remove_normmask(im.reshape((-1, 1)), np.ones(1), np.ones_like(medianim), mastermask).reshape(shape)

        imag = remove_normmask(im.reshape((-1, 1)), normperim, medianim, mastermask).reshape(shape)
        # cosmics in the image lead to high points, which means that the
        # average area will be overcorrected
        imag = imag - np.ma.median(imag)
        # do photometry on image in real space

        psf_gaussian = photutils.psf.GaussianPSF(1.8)  # width measured by hand
        # default in photutils is to freeze this stuff, but I disagree
        # psf_gaussian.fixed['sigma'] = False
        # psf_gaussian.fixed['x_0'] = False
        # psf_gaussian.fixed['y_0'] = False
        fluxes_gaussian = photutils.psf.psf_photometry(imag, sources['xcentroid', 'ycentroid'], psf_gaussian)

        '''Estimate flux of Gaussian PSF from A and sigma.

        Should be part of photutils in a more clever (analytic) implementation.
        As long as it's missing there, but in this crutch here.
        '''
        x, y = np.mgrid[-3:3, -4:4]
        amp2flux = np.sum(psf_gaussian.evaluate(x, y, 1, 1, 0, 1.8))  # 1.8 hard-coded above
        fluxes_gaussian.add_column(MaskedColumn(name='flux_fit', data=amp2flux * fluxes_gaussian['amplitude_fit']))

        return fluxes_gaussian, imag, scaled_im
Ejemplo n.º 29
0
def centroid_sources(data,
                     xpos,
                     ypos,
                     box_size=11,
                     footprint=None,
                     error=None,
                     mask=None,
                     centroid_func=centroid_com):
    """
    Calculate the centroid of sources at the defined positions.

    A cutout image centered on each input position will be used to
    calculate the centroid position.  The cutout image is defined either
    using the ``box_size`` or ``footprint`` keyword.  The ``footprint``
    keyword can be used to create a non-rectangular cutout image.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    xpos, ypos : float or array-like of float
        The initial ``x`` and ``y`` pixel position(s) of the center
        position.  A cutout image centered on this position be used to
        calculate the centroid.

    box_size : int or array-like of int, optional
        The size of the cutout image along each axis.  If ``box_size``
        is a number, then a square cutout of ``box_size`` will be
        created.  If ``box_size`` has two elements, they should be in
        ``(ny, nx)`` order.

    footprint : `~numpy.ndarray` of bools, optional
        A 2D boolean array where `True` values describe the local
        footprint region to cutout.  ``footprint`` can be used to create
        a non-rectangular cutout image, in which case the input ``xpos``
        and ``ypos`` represent the center of the minimal bounding box
        for the input ``footprint``.  ``box_size=(n, m)`` is equivalent
        to ``footprint=np.ones((n, m))``.  Either ``box_size`` or
        ``footprint`` must be defined.  If they are both defined, then
        ``footprint`` overrides ``box_size``.

    mask : array_like, bool, optional
        A 2D boolean array with the same shape as ``data``, where a
        `True` value indicates the corresponding element of ``data`` is
        masked.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.
        ``error`` must have the same shape as ``data``.  ``error`` will
        be used only if supported by the input ``centroid_func``.

    centroid_func : callable, optional
        A callable object (e.g. function or class) that is used to
        calculate the centroid of a 2D array.  The ``centroid_func``
        must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and
        optionally an ``error`` keyword.  The callable object must
        return a tuple of two 1D `~numpy.ndarray`\\s, representing the x
        and y centroids.  The default is
        `~photutils.centroids.centroid_com`.

    Returns
    -------
    xcentroid, ycentroid : `~numpy.ndarray`
        The ``x`` and ``y`` pixel position(s) of the centroids.
    """

    xpos = np.atleast_1d(xpos)
    ypos = np.atleast_1d(ypos)
    if xpos.ndim != 1:
        raise ValueError('xpos must be a 1D array.')
    if ypos.ndim != 1:
        raise ValueError('ypos must be a 1D array.')

    if footprint is None:
        if box_size is None:
            raise ValueError('box_size or footprint must be defined.')

        box_size = np.atleast_1d(box_size)
        if len(box_size) == 1:
            box_size = np.repeat(box_size, 2)
        if len(box_size) != 2:
            raise ValueError('box_size must have 1 or 2 elements.')

        footprint = np.ones(box_size, dtype=bool)
    else:
        footprint = np.asanyarray(footprint, dtype=bool)
        if footprint.ndim != 2:
            raise ValueError('footprint must be a 2D array.')

    use_error = False
    spec = inspect.getfullargspec(centroid_func)
    if 'mask' not in spec.args:
        raise ValueError('The input "centroid_func" must have a "mask" '
                         'keyword.')
    if 'error' in spec.args:
        use_error = True

    xcentroids = []
    ycentroids = []
    for xp, yp in zip(xpos, ypos):
        slices_large, slices_small = overlap_slices(data.shape,
                                                    footprint.shape, (yp, xp))
        data_cutout = data[slices_large]

        mask_cutout = None
        if mask is not None:
            mask_cutout = mask[slices_large]

        footprint_mask = ~footprint
        # trim footprint mask if partial overlap on the data
        footprint_mask = footprint_mask[slices_small]

        if mask_cutout is None:
            mask_cutout = footprint_mask
        else:
            # combine the input mask and footprint mask
            mask_cutout = np.logical_or(mask_cutout, footprint_mask)

        if error is not None and use_error:
            error_cutout = error[slices_large]
            xcen, ycen = centroid_func(data_cutout,
                                       mask=mask_cutout,
                                       error=error_cutout)
        else:
            xcen, ycen = centroid_func(data_cutout, mask=mask_cutout)

        xcentroids.append(xcen + slices_large[1].start)
        ycentroids.append(ycen + slices_large[0].start)

    return np.array(xcentroids), np.array(ycentroids)
Ejemplo n.º 30
0
    def _recenter_epsf(self,
                       epsf,
                       centroid_func=centroid_epsf,
                       box_size=(5, 5),
                       maxiters=20,
                       center_accuracy=1.0e-4):
        """
        Calculate the center of the ePSF data and shift the data so the
        ePSF center is at the center of the ePSF data array.

        Parameters
        ----------
        epsf : `EPSFModel` object
            The ePSF model.

        centroid_func : callable, optional
            A callable object (e.g. function or class) that is used to
            calculate the centroid of a 2D array.  The callable must
            accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and
            optionally an ``error`` keyword.  The callable object must
            return a tuple of two 1D `~numpy.ndarray` variables, representing
            the x and y centroids.
        box_size : float or tuple of two floats, optional
            The size (in pixels) of the box used to calculate the
            centroid of the ePSF during each build iteration.  If a
            single integer number is provided, then a square box will be
            used.  If two values are provided, then they should be in
            ``(ny, nx)`` order.
        maxiters : int, optional
            The maximum number of recentering iterations to perform.
        center_accuracy : float, optional
            The desired accuracy for the centers of stars.  The building
            iterations will stop if the center of the ePSF changes by
            less than ``center_accuracy`` pixels between iterations.

        Returns
        -------
        result : 2D `~numpy.ndarray`
            The recentered ePSF data.
        """

        epsf_data = epsf._data

        epsf = EPSFModel(data=epsf._data,
                         origin=epsf.origin,
                         oversampling=epsf.oversampling,
                         norm_radius=epsf._norm_radius,
                         shift_val=epsf._shift_val,
                         normalize=False)

        xcenter, ycenter = epsf.origin

        y, x = np.indices(epsf._data.shape, dtype=np.float)
        x /= epsf.oversampling[0]
        y /= epsf.oversampling[1]

        dx_total, dy_total = 0, 0
        iter_num = 0
        center_accuracy_sq = center_accuracy**2
        center_dist_sq = center_accuracy_sq + 1.e6
        center_dist_sq_prev = center_dist_sq + 1
        while (iter_num < maxiters and center_dist_sq >= center_accuracy_sq):
            iter_num += 1

            # Anderson & King (2000) recentering function depends
            # on specific pixels, and thus does not need a cutout
            if self.recentering_func == centroid_epsf:
                epsf_cutout = epsf_data
            else:
                slices_large, _ = overlap_slices(
                    epsf_data.shape, box_size,
                    (ycenter * self.oversampling[1],
                     xcenter * self.oversampling[0]))
                epsf_cutout = epsf_data[slices_large]
            mask = ~np.isfinite(epsf_cutout)

            try:
                # find a new center position
                xcenter_new, ycenter_new = centroid_func(
                    epsf_cutout,
                    mask=mask,
                    oversampling=epsf.oversampling,
                    shift_val=epsf._shift_val)
            except TypeError:
                # centroid_func doesn't accept oversampling and/or shift_val
                # keywords - try oversampling alone
                try:
                    xcenter_new, ycenter_new = centroid_func(
                        epsf_cutout, mask=mask, oversampling=epsf.oversampling)
                except TypeError:
                    # centroid_func doesn't accept oversampling and
                    # shift_val
                    xcenter_new, ycenter_new = centroid_func(epsf_cutout,
                                                             mask=mask)

            if self.recentering_func != centroid_epsf:
                xcenter_new += slices_large[1].start / self.oversampling[0]
                ycenter_new += slices_large[0].start / self.oversampling[1]

            # Calculate the shift; dx = i - x_star so if dx was positively
            # incremented then x_star was negatively incremented for a given i.
            # We will therefore actually subsequently subtract dx from xcenter
            # (or x_star).
            dx = xcenter_new - xcenter
            dy = ycenter_new - ycenter

            center_dist_sq = dx**2 + dy**2

            if center_dist_sq >= center_dist_sq_prev:  # don't shift
                break
            center_dist_sq_prev = center_dist_sq

            dx_total += dx
            dy_total += dy

            epsf_data = epsf.evaluate(x=x,
                                      y=y,
                                      flux=1.0,
                                      x_0=xcenter - dx_total,
                                      y_0=ycenter - dy_total)

        return epsf_data
Ejemplo n.º 31
0
def _extract_stars(data, catalog, size=(11, 11), use_xy=True):
    """
    Extract cutout images from a single image centered on stars defined
    in the single input catalog.

    Parameters
    ----------
    data : `~astropy.nddata.NDData`
        A `~astropy.nddata.NDData` object containing the 2D image from
        which to extract the stars.  If the input ``catalog`` contains
        only the sky coordinates (i.e. not the pixel coordinates) of the
        stars then the `~astropy.nddata.NDData` object must have a valid
        ``wcs`` attribute.

    catalogs : `~astropy.table.Table`
        A single catalog of sources to be extracted from the input
        ``data``.  The center of each source can be defined either in
        pixel coordinates (in ``x`` and ``y`` columns) or sky
        coordinates (in a ``skycoord`` column containing a
        `~astropy.coordinates.SkyCoord` object).  If both are specified,
        then the value of the ``use_xy`` keyword determines which
        coordinates will be used.

    size : int or array_like (int), optional
        The extraction box size along each axis.  If ``size`` is a
        scalar then a square box of size ``size`` will be used.  If
        ``size`` has two elements, they should be in ``(ny, nx)`` order.
        The size must be greater than or equal to 3 pixel for both axes.

    use_xy : bool, optional
        Whether to use the ``x`` and ``y`` pixel positions when both
        pixel and sky coordinates are present in the input catalog
        table.  If `False` then sky coordinates are used instead of
        pixel coordinates (e.g. for linked stars).  The default is
        `True`.

    Returns
    -------
    stars : list of `EPSFStar` objects
        A list of `EPSFStar` instances containing the extracted stars.
    """

    colnames = catalog.colnames
    if ('x' not in colnames or 'y' not in colnames) or not use_xy:
        xcenters, ycenters = skycoord_to_pixel(catalog['skycoord'], data.wcs,
                                               origin=0, mode='all')
    else:
        xcenters = catalog['x'].data.astype(np.float)
        ycenters = catalog['y'].data.astype(np.float)

    if 'id' in colnames:
        ids = catalog['id']
    else:
        ids = np.arange(len(catalog), dtype=np.int) + 1

    if data.uncertainty is None:
        weights = np.ones_like(data.data)
    else:
        if data.uncertainty.uncertainty_type == 'weights':
            weights = np.asanyarray(data.uncertainty.array, dtype=np.float)
        else:
            warnings.warn('The data uncertainty attribute has an unsupported '
                          'type.  Only uncertainty_type="weights" can be '
                          'used to set weights.  Weights will be set to 1.',
                          AstropyUserWarning)
            weights = np.ones_like(data.data)

    if data.mask is not None:
        weights[data.mask] = 0.

    stars = []
    for xcenter, ycenter, obj_id in zip(xcenters, ycenters, ids):
        try:
            large_slc, small_slc = overlap_slices(data.data.shape, size,
                                                  (ycenter, xcenter),
                                                  mode='strict')
            data_cutout = data.data[large_slc]
            weights_cutout = weights[large_slc]
        except (PartialOverlapError, NoOverlapError):
            stars.append(None)
            continue

        origin = (large_slc[1].start, large_slc[0].start)
        cutout_center = (xcenter - origin[0], ycenter - origin[1])
        star = EPSFStar(data_cutout, weights_cutout,
                        cutout_center=cutout_center, origin=origin,
                        wcs_large=data.wcs, id_label=obj_id)

        stars.append(star)

    return stars
Ejemplo n.º 32
0
    def _fit_star(self, epsf, star, fitter, fitter_kwargs,
                  fitter_has_fit_info, fit_boxsize):
        """
        Fit an ePSF model to a single star.

        The input ``epsf`` will usually be modified by the fitting
        routine in this function.  Make a copy before calling this
        function if the original is needed.
        """

        if fit_boxsize is not None:
            try:
                xcenter, ycenter = star.cutout_center
                large_slc, small_slc = overlap_slices(star.shape,
                                                      fit_boxsize,
                                                      (ycenter, xcenter),
                                                      mode='strict')
            except (PartialOverlapError, NoOverlapError):
                warnings.warn('The star at ({0}, {1}) cannot be fit because '
                              'its fitting region extends beyond the star '
                              'cutout image.'.format(star.center[0],
                                                     star.center[1]),
                              AstropyUserWarning)

                star = copy.deepcopy(star)
                star._fit_error_status = 1

                return star

            data = star.data[large_slc]
            weights = star.weights[large_slc]

            # define the origin of the fitting region
            x0 = large_slc[1].start
            y0 = large_slc[0].start
        else:
            # use the entire cutout image
            data = star.data
            weights = star.weights

            # define the origin of the fitting region
            x0 = 0
            y0 = 0

        scaled_data = data / np.prod(epsf._oversampling)

        # define positions in the ePSF oversampled grid
        yy, xx = np.indices(data.shape, dtype=np.float)
        xx = (xx - (star.cutout_center[0] - x0)) * epsf._oversampling[0]
        yy = (yy - (star.cutout_center[1] - y0)) * epsf._oversampling[1]

        # define the initial guesses for fitted flux and shifts
        epsf.flux = star.flux
        epsf.x_0 = 0.0
        epsf.y_0 = 0.0

        # create copy to avoid overwriting original oversampling factor
        _epsf = epsf.copy()
        _epsf._oversampling = np.array([1., 1.])

        try:
            fitted_epsf = fitter(model=_epsf, x=xx, y=yy, z=scaled_data,
                                 weights=weights, **fitter_kwargs)
        except TypeError:
            # fitter doesn't support weights
            fitted_epsf = fitter(model=_epsf, x=xx, y=yy, z=scaled_data,
                                 **fitter_kwargs)

        fit_error_status = 0
        if fitter_has_fit_info:
            fit_info = copy.copy(fitter.fit_info)

            if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]:
                fit_error_status = 2    # fit solution was not found
        else:
            fit_info = None

        # compute the star's fitted position
        x_center = (star.cutout_center[0] +
                    (fitted_epsf.x_0.value / epsf._oversampling[0]))
        y_center = (star.cutout_center[1] +
                    (fitted_epsf.y_0.value / epsf._oversampling[1]))

        star = copy.deepcopy(star)
        star.cutout_center = (x_center, y_center)

        # set the star's flux to the ePSF-fitted flux
        star.flux = fitted_epsf.flux.value

        star._fit_info = fit_info
        star._fit_error_status = fit_error_status

        return star
Ejemplo n.º 33
0
def test_slices_pos_different_dim():
    '''Position must have same dim as arrays.'''
    with pytest.raises(ValueError) as e:
        overlap_slices((4, 5), (1, 2), (0, 0, 3))
    assert "the same number of dimensions" in str(e.value)
Ejemplo n.º 34
0
def listpixels(data, position, shape, subarray_indices=False, wcs=None):
    """
    Return a `~astropy.table.Table` listing the ``(y, x)`` positions and
    ``data`` values for a subarray.

    Given a position of the center of the subarray, with respect to the
    large array, the array indices and values are returned.  This
    function takes care of the correct behavior at the boundaries, where
    the small array is appropriately trimmed.

    Parameters
    ----------
    data : array-like
        The input data.

    position : tuple (int) or `~astropy.coordinates.SkyCoord`
        The position of the subarray center with respect to the data
        array.  The position can be specified either as an integer ``(y,
        x)`` tuple of pixel coordinates or a
        `~astropy.coordinates.SkyCoord`, in which case ``wcs`` is a
        required input.

    shape : tuple (int)
        The integer shape (``(ny, nx)``) of the subarray.

    subarray_indices : bool, optional
        If `True` then the returned positions are relative to the small
        subarray.  If `False` (default) then the returned positions are
        relative to the ``data`` array.

    wcs : `~astropy.wcs.WCS`, optional
        The WCS transformation to use if ``position`` is a
        `~astropy.coordinates.SkyCoord`.

    Returns
    -------
    table : `~astropy.table.Table`
        A table containing the ``x`` and ``y`` positions and data
        values.

    Notes
    -----
    This function is decorated with `~astropy.nddata.support_nddata` and
    thus supports `~astropy.nddata.NDData` objects as input.

    Examples
    --------
    >>> import numpy as np
    >>> from astroimtools import listpixels
    >>> np.random.seed(12345)
    >>> data = np.random.random((25, 25))
    >>> tbl = listpixels(data, (8, 11), (3, 3))
    >>> for col in tbl.colnames:
    ...     tbl[col].info.format = '%.8g'  # for consistent table output
    >>> tbl.pprint(max_lines=-1)
     x   y     value
    --- --- -----------
     10   7  0.75857204
     11   7 0.069529666
     12   7  0.70547344
     10   8   0.8406625
     11   8  0.46931469
     12   8  0.56264343
     10   9 0.034131584
     11   9  0.23049655
     12   9  0.22835371
    """

    if isinstance(position, SkyCoord):
        if wcs is None:
            raise ValueError('wcs must be input if positions is a SkyCoord')

        x, y = skycoord_to_pixel(position, wcs, mode='all')
        position = (y, x)

    data = np.asanyarray(data)
    slices_large, slices_small = overlap_slices(data.shape, shape, position)
    slices = slices_large
    yy, xx = np.mgrid[slices]
    values = data[yy, xx]

    if subarray_indices:
        slices = slices_small
        yy, xx = np.mgrid[slices]

    tbl = Table()
    tbl['x'] = xx.ravel()
    tbl['y'] = yy.ravel()
    tbl['value'] = values.ravel()
    return tbl
Ejemplo n.º 35
0
def test_slices_no_overlap(pos):
    '''If there is no overlap between arrays, an error should be raised.'''
    with pytest.raises(NoOverlapError):
        overlap_slices((5, 5), (2, 2), pos)
Ejemplo n.º 36
0
    def _recenter_epsf(self,
                       epsf_data,
                       epsf,
                       centroid_func=centroid_com,
                       box_size=5,
                       maxiters=20,
                       center_accuracy=1.0e-4):
        """
        Calculate the center of the ePSF data and shift the data so the
        ePSF center is at the center of the ePSF data array.

        Parameters
        ----------
        epsf_data : 2D `~numpy.ndarray`
            A 2D array containing the ePSF image.

        epsf : `EPSFModel` object
            The ePSF model.

        centroid_func : callable, optional
            A callable object (e.g. function or class) that is used to
            calculate the centroid of a 2D array.  The callable must
            accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and
            optionally an ``error`` keyword.  The callable object must
            return a tuple of two 1D `~numpy.ndarray`\\s, representing
            the x and y centroids.  The default is
            `~photutils.centroids.centroid_com`.

        recentering_boxsize : float or tuple of two floats, optional
            The size (in pixels) of the box used to calculate the
            centroid of the ePSF during each build iteration.  If a
            single integer number is provided, then a square box will be
            used.  If two values are provided, then they should be in
            ``(ny, nx)`` order.  The default is 5.

        maxiters : int, optional
            The maximum number of recentering iterations to perform.
            The default is 20.

        center_accuracy : float, optional
            The desired accuracy for the centers of stars.  The building
            iterations will stop if the center of the ePSF changes by
            less than ``center_accuracy`` pixels between iterations.
            The default is 1.0e-4.

        Returns
        -------
        result : 2D `~numpy.ndarray`
            The recentered ePSF data.
        """

        # Define an EPSFModel for the input data.  This EPSFModel will be
        # used to evaluate the model on a shifted pixel grid to place the
        # centroid at the array center.
        epsf = EPSFModel(data=epsf_data,
                         origin=epsf.origin,
                         normalize=False,
                         oversampling=epsf.oversampling,
                         pixel_scale=epsf.pixel_scale)
        epsf.fill_value = 0.0
        xcenter, ycenter = epsf.origin

        dx_total = 0
        dy_total = 0
        y, x = np.indices(epsf_data.shape, dtype=np.float)

        iter_num = 0
        center_accuracy_sq = center_accuracy**2
        center_dist_sq = center_accuracy_sq + 1.e6
        center_dist_sq_prev = center_dist_sq + 1
        while (iter_num < maxiters and center_dist_sq >= center_accuracy_sq):

            iter_num += 1

            # extract a cutout from the ePSF
            slices_large, slices_small = overlap_slices(
                epsf_data.shape, box_size, (ycenter, xcenter))
            epsf_cutout = epsf_data[slices_large]
            mask = ~np.isfinite(epsf_cutout)

            # find a new center position
            xcenter_new, ycenter_new = centroid_func(epsf_cutout, mask=mask)
            xcenter_new += slices_large[1].start
            ycenter_new += slices_large[0].start

            # calculate the shift
            dx = xcenter - xcenter_new
            dy = ycenter - ycenter_new
            center_dist_sq = dx**2 + dy**2
            if center_dist_sq >= center_dist_sq_prev:  # don't shift
                break
            center_dist_sq_prev = center_dist_sq

            # Resample the ePSF data to a shifted grid to place the
            # centroid in the center of the central pixel.  The shift is
            # always performed on the input epsf_data.
            dx_total += dx  # accumulated shifts for the input epsf_data
            dy_total += dy
            epsf_data = epsf.evaluate(x=x,
                                      y=y,
                                      flux=1.0,
                                      x_0=xcenter + dx_total,
                                      y_0=ycenter + dy_total,
                                      use_oversampling=False)

        return epsf_data
Ejemplo n.º 37
0
def centroid_sources(data,
                     xpos,
                     ypos,
                     box_size=11,
                     footprint=None,
                     mask=None,
                     centroid_func=centroid_com,
                     **kwargs):
    """
    Calculate the centroid of sources at the defined positions.

    A cutout image centered on each input position will be used to
    calculate the centroid position.  The cutout image is defined either
    using the ``box_size`` or ``footprint`` keyword.  The ``footprint``
    keyword can be used to create a non-rectangular cutout image.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    xpos, ypos : float or array-like of float
        The initial ``x`` and ``y`` pixel position(s) of the center
        position.  A cutout image centered on this position be used to
        calculate the centroid.

    box_size : int or array-like of int, optional
        The size of the cutout image along each axis. If ``box_size``
        is a number, then a square cutout of ``box_size`` will be
        created. If ``box_size`` has two elements, they should be in
        ``(ny, nx)`` order. Either ``box_size`` or ``footprint`` must be
        defined. If they are both defined, then ``footprint`` overrides
        ``box_size``.

    footprint : `~numpy.ndarray` of bools, optional
        A 2D boolean array where `True` values describe the local
        footprint region to cutout. ``footprint`` can be used to create
        a non-rectangular cutout image, in which case the input ``xpos``
        and ``ypos`` represent the center of the minimal bounding box
        for the input ``footprint``. ``box_size=(n, m)`` is equivalent
        to ``footprint=np.ones((n, m))``. Either ``box_size`` or
        ``footprint`` must be defined. If they are both defined, then
        ``footprint`` overrides ``box_size``.

    mask : array_like, bool, optional
        A 2D boolean array with the same shape as ``data``, where a
        `True` value indicates the corresponding element of ``data`` is
        masked.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.
        ``error`` must have the same shape as ``data``.  ``error`` will
        be used only if supported by the input ``centroid_func``.

    centroid_func : callable, optional
        A callable object (e.g., function or class) that is used to
        calculate the centroid of a 2D array. The ``centroid_func``
        must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and
        optionally an ``error`` keyword. The callable object must return
        a tuple of two 1D `~numpy.ndarray`, representing the x and y
        centroids. The default is `~photutils.centroids.centroid_com`.

    **kwargs : `dict`
        Any additional keyword arguments accepted by the
        ``centroid_func``.

    Returns
    -------
    xcentroid, ycentroid : `~numpy.ndarray`
        The ``x`` and ``y`` pixel position(s) of the centroids. NaNs
        will be returned where the centroid failed. This is usually due
        a ``box_size`` that is too small when using a fitting-based
        centroid function (e.g., `centroid_1dg`, `centroid_2dg`, or
        `centroid_quadratic`.
    """
    xpos = np.atleast_1d(xpos)
    ypos = np.atleast_1d(ypos)
    if xpos.ndim != 1:
        raise ValueError('xpos must be a 1D array.')
    if ypos.ndim != 1:
        raise ValueError('ypos must be a 1D array.')

    if (np.any(np.min(xpos) < 0) or np.any(np.min(ypos) < 0)
            or np.any(np.max(xpos) > data.shape[1] - 1)
            or np.any(np.max(ypos) > data.shape[0] - 1)):
        raise ValueError('xpos, ypos values contains point(s) outside of '
                         'input data')

    if footprint is None:
        if box_size is None:
            raise ValueError('box_size or footprint must be defined.')

        box_size = np.atleast_1d(box_size)
        if len(box_size) == 1:
            box_size = np.repeat(box_size, 2)
        if len(box_size) != 2:
            raise ValueError('box_size must have 1 or 2 elements.')

        footprint = np.ones(box_size, dtype=bool)
    else:
        footprint = np.asanyarray(footprint, dtype=bool)
        if footprint.ndim != 2:
            raise ValueError('footprint must be a 2D array.')

    spec = inspect.getfullargspec(centroid_func)
    if 'mask' not in spec.args:
        raise ValueError('The input "centroid_func" must have a "mask" '
                         'keyword.')

    # drop any **kwargs not supported by the centroid_func
    centroid_kwargs = {}
    for key, val in kwargs.items():
        if key in spec.args:
            centroid_kwargs[key] = val

    xcentroids = []
    ycentroids = []
    for xp, yp in zip(xpos, ypos):
        slices_large, slices_small = overlap_slices(data.shape,
                                                    footprint.shape, (yp, xp))
        data_cutout = data[slices_large]

        footprint_mask = np.logical_not(footprint)
        # trim footprint mask if it has only partial overlap on the data
        footprint_mask = footprint_mask[slices_small]

        if mask is not None:
            # combine the input mask cutout and footprint mask
            mask_cutout = np.logical_or(mask[slices_large], footprint_mask)
        else:
            mask_cutout = footprint_mask

        centroid_kwargs.update({'mask': mask_cutout})

        error = centroid_kwargs.get('error', None)
        if error is not None:
            centroid_kwargs['error'] = error[slices_large]

        # remove xpeak and ypeak from the dict and add back only if both
        # are specified and not None
        xpeak = centroid_kwargs.pop('xpeak', None)
        ypeak = centroid_kwargs.pop('ypeak', None)
        if xpeak is not None and ypeak is not None:
            centroid_kwargs['xpeak'] = xpeak - slices_large[1].start
            centroid_kwargs['ypeak'] = ypeak - slices_large[0].start

        try:
            xcen, ycen = centroid_func(data_cutout, **centroid_kwargs)
        except (ValueError, TypeError):
            xcen, ycen = np.nan, np.nan

        xcentroids.append(xcen + slices_large[1].start)
        ycentroids.append(ycen + slices_large[0].start)

    return np.array(xcentroids), np.array(ycentroids)
Ejemplo n.º 38
0
def cutout_footprint(data, position, box_size=3, footprint=None, mask=None,
                     error=None):
    """
    Cut out a region from data (and optional mask and error) centered at
    specified (x, y) position.

    The size of the region is specified via the ``box_size`` or
    ``footprint`` keywords.  The output mask for the cutout region
    represents the combination of the input mask and footprint mask.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    position : 2 tuple
        The ``(x, y)`` pixel coordinate of the center of the region.

    box_size : scalar or tuple, optional
        The size of the region to cutout from ``data``.  If ``box_size``
        is a scalar, then the region shape will be ``(box_size,
        box_size)``.  Either ``box_size`` or ``footprint`` must be
        defined.  If they are both defined, then ``footprint`` overrides
        ``box_size``.

    footprint : `~numpy.ndarray` of bools, optional
        A boolean array where `True` values describe the local footprint
        region.  ``box_size=(n, m)`` is equivalent to
        ``footprint=np.ones((n, m))``.  Either ``box_size`` or
        ``footprint`` must be defined.  If they are both defined, then
        ``footprint`` overrides ``box_size``.

    mask : array_like, bool, optional
        A boolean mask with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    Returns
    -------
    region_data : `~numpy.ndarray`
        The ``data`` cutout.

    region_mask : `~numpy.ndarray`
        The ``mask`` cutout.

    region_error : `~numpy.ndarray`
        The ``error`` cutout.

    slices : tuple of slices
        Slices in each dimension of the ``data`` array used to define
        the cutout region.
    """

    if len(position) != 2:
        raise ValueError('position must have a length of 2')

    if footprint is None:
        if box_size is None:
            raise ValueError('box_size or footprint must be defined.')
        if not isinstance(box_size, collections.Iterable):
            shape = (box_size, box_size)
        else:
            if len(box_size) != 2:
                raise ValueError('box_size must have a length of 2')
            shape = box_size
        footprint = np.ones(shape, dtype=bool)
    else:
        footprint = np.asanyarray(footprint, dtype=bool)

    slices_large, slices_small = overlap_slices(data.shape, footprint.shape,
                                                position[::-1])
    region_data = data[slices_large]

    if error is not None:
        region_error = error[slices_large]
    else:
        region_error = None

    if mask is not None:
        region_mask = mask[slices_large]
    else:
        region_mask = np.zeros_like(region_data, dtype=bool)
    footprint_mask = ~footprint
    footprint_mask = footprint_mask[slices_small]    # trim if necessary
    region_mask = np.logical_or(region_mask, footprint_mask)

    return region_data, region_mask, region_error, slices_large
Ejemplo n.º 39
0
def test_slices_different_dim():
    '''Overlap from arrays with different number of dim is undefined.'''
    with pytest.raises(ValueError) as e:
        overlap_slices((4, 5, 6), (1, 2), (0, 0))
    assert "the same number of dimensions" in str(e.value)
Ejemplo n.º 40
0
def listpixels(data, position, shape, subarray_indices=False, wcs=None):
    """
    Return a `~astropy.table.Table` listing the ``(y, x)`` positions and
    ``data`` values for a subarray.

    Given a position of the center of the subarray, with respect to the
    large array, the array indices and values are returned.  This
    function takes care of the correct behavior at the boundaries, where
    the small array is appropriately trimmed.

    Parameters
    ----------
    data : array-like
        The input data.

    position : tuple (int) or `~astropy.coordinates.SkyCoord`
        The position of the subarray center with respect to the data
        array.  The position can be specified either as an integer ``(y,
        x)`` tuple of pixel coordinates or a
        `~astropy.coordinates.SkyCoord`, in which case ``wcs`` is a
        required input.

    shape : tuple (int)
        The integer shape (``(ny, nx)``) of the subarray.

    subarray_indices : bool, optional
        If `True` then the returned positions are relative to the small
        subarray.  If `False` (default) then the returned positions are
        relative to the ``data`` array.

    wcs : `~astropy.wcs.WCS`, optional
        The WCS transformation to use if ``position`` is a
        `~astropy.coordinates.SkyCoord`.

    Returns
    -------
    table : `~astropy.table.Table`
        A table containing the ``x`` and ``y`` positions and data
        values.

    Notes
    -----
    This function is decorated with `~astropy.nddata.support_nddata` and
    thus supports `~astropy.nddata.NDData` objects as input.

    Examples
    --------
    >>> import numpy as np
    >>> from astroimtools import listpixels
    >>> np.random.seed(12345)
    >>> data = np.random.random((25, 25))
    >>> tbl = listpixels(data, (8, 11), (3, 3))
    >>> for col in tbl.colnames:
    ...     tbl[col].info.format = '%.8g'  # for consistent table output
    >>> tbl.pprint(max_lines=-1)
     x   y     value
    --- --- -----------
     10   7  0.75857204
     11   7 0.069529666
     12   7  0.70547344
     10   8   0.8406625
     11   8  0.46931469
     12   8  0.56264343
     10   9 0.034131584
     11   9  0.23049655
     12   9  0.22835371
    """

    if isinstance(position, SkyCoord):
        if wcs is None:
            raise ValueError('wcs must be input if positions is a SkyCoord')

        x, y = skycoord_to_pixel(position, wcs, mode='all')
        position = (y, x)

    data = np.asanyarray(data)
    slices_large, slices_small = overlap_slices(data.shape, shape, position)
    slices = slices_large
    yy, xx = np.mgrid[slices]
    values = data[yy, xx]

    if subarray_indices:
        slices = slices_small
        yy, xx = np.mgrid[slices]

    tbl = Table()
    tbl['x'] = xx.ravel()
    tbl['y'] = yy.ravel()
    tbl['value'] = values.ravel()
    return tbl
Ejemplo n.º 41
0
def test_slices_different_dim():
    '''Overlap from arrays with different number of dim is undefined.'''
    with pytest.raises(ValueError) as e:
        overlap_slices((4, 5, 6), (1, 2), (0, 0))
    assert "the same number of dimensions" in str(e.value)