Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ApertureMask get_values method #1158

Merged
merged 8 commits into from Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGES.rst
Expand Up @@ -20,6 +20,9 @@ New Features
- Added a ``get_overlap_slices`` method and a ``center`` attribute to
``BoundingBox``. [#1157]

- Added a ``get_values`` method to ``ApertureMask`` that returns a 1D
array of mask-weighted values. [#1158]

- ``photutils.background``

- The ``Background2D`` class now accepts astropy ``NDData``,
Expand All @@ -38,6 +41,14 @@ Bug Fixes
- Slicing a scalar ``Aperture`` object now raises an informative error
message. [#1154]

- Fixed an issue where ``ApertureMask.multiply`` ``fill_value`` was
not applied to pixels outside of the aperture mask, but within the
aperture bounding box. [#1158]

- Fixed an issue where ``ApertureMask.cutout`` would raise an error
if ``fill_value`` was non-finite and the input array was integer
type. [#1158]

- ``photutils.psf``

- Fixed a bug in ``EPSFBuild`` where a warning was raised if the input
Expand Down
17 changes: 4 additions & 13 deletions photutils/aperture/core.py
Expand Up @@ -332,7 +332,7 @@ def area_overlap(self, data, method='exact', subpixels=5):
if self.isscalar:
masks = (masks,)
data = np.ones_like(data)
areas = [mask.multiply(data).sum() for mask in masks]
areas = [mask.get_values(data).sum() for mask in masks]
if self.isscalar:
return areas[0]
else:
Expand All @@ -349,19 +349,10 @@ def _do_photometry(self, data, variance, method='exact', subpixels=5,
masks = (masks,)

for apermask in masks:
data_weighted = apermask.multiply(data)
if data_weighted is None:
aperture_sums.append(np.nan)
else:
aperture_sums.append(np.sum(data_weighted))

aperture_sums.append(apermask.get_values(data).sum())
if variance is not None:
variance_weighted = apermask.multiply(variance)
if variance_weighted is None:
aperture_sum_errs.append(np.nan)
else:
aperture_sum_errs.append(
np.sqrt(np.sum(variance_weighted)))
aperture_sum_errs.append(
np.sqrt(apermask.get_values(variance).sum()))

aperture_sums = np.array(aperture_sums)
aperture_sum_errs = np.array(aperture_sum_errs)
Expand Down
39 changes: 35 additions & 4 deletions photutils/aperture/mask.py
Expand Up @@ -130,7 +130,11 @@ def cutout(self, data, fill_value=0., copy=False):
return cutout

# cutout is always a copy for partial overlap
cutout = np.zeros(self.shape, dtype=data.dtype)
if ~np.isfinite(fill_value):
dtype = np.float
else:
dtype = data.dtype
cutout = np.zeros(self.shape, dtype=dtype)
cutout[:] = fill_value
cutout[slices_small] = data[slices_large]

Expand Down Expand Up @@ -171,8 +175,35 @@ def multiply(self, data, fill_value=0.):
else:
weighted_cutout = cutout * self.data

# needed to zero out non-finite data values outside of the
# mask but within the bounding box
weighted_cutout[self._mask] = 0.
# fill values outside of the mask but within the bounding box
weighted_cutout[self._mask] = fill_value

return weighted_cutout

def get_values(self, data):
"""
Get the mask-weighted pixel values from the data as a 1D array.

If the ``ApertureMask`` was created with ``method='center'``,
(where the mask weights are only 1 or 0), then the returned
values will simply be pixel values extracted from the data.

Parameters
----------
data : array_like or `~astropy.units.Quantity`
The 2D array from which to get mask-weighted values.

Returns
-------
result : `~numpy.ndarray`
A 1D array of mask-weighted pixel values from the input
``data``. If there is no overlap of the aperture with the
input ``data``, the result will be a 1-element array of
``numpy.nan``.
"""
slc_large, slc_small = self.bbox.get_overlap_slices(data.shape)
if slc_large is None:
return np.array([np.nan])
cutout = data[slc_large]
mask = self.data[slc_small]
return (cutout * mask)[mask > 0]
40 changes: 39 additions & 1 deletion photutils/aperture/tests/test_mask.py
Expand Up @@ -9,7 +9,7 @@
import pytest

from ..bounding_box import BoundingBox
from ..circle import CircularAperture
from ..circle import CircularAperture, CircularAnnulus
from ..mask import ApertureMask

try:
Expand Down Expand Up @@ -127,3 +127,41 @@ def test_mask_multiply_quantity():
# test that multiply() returns a copy
data[25, 25] = 100. * u.adu
assert data_weighted[10, 10].value == 1.


@pytest.mark.parametrize('value', (np.nan, np.inf))
def test_mask_nonfinite_fill_value(value):
aper = CircularAnnulus((0, 0), 10, 20)
data = np.ones((101, 101)).astype(int)
cutout = aper.to_mask().cutout(data, fill_value=value)
assert ~np.isfinite(cutout[0, 0])


def test_mask_multiply_fill_value():
aper = CircularAnnulus((0, 0), 10, 20)
data = np.ones((101, 101)).astype(int)
cutout = aper.to_mask().multiply(data, fill_value=np.nan)
xypos = ((20, 20), (5, 5), (5, 35), (35, 5), (35, 35))
for x, y in xypos:
assert np.isnan(cutout[y, x])


def test_mask_get_values():
aper = CircularAnnulus(((0, 0), (50, 50), (100, 100)), 10, 20)
data = np.ones((101, 101))
values = [mask.get_values(data) for mask in aper.to_mask()]
shapes = [val.shape for val in values]
sums = [np.sum(val) for val in values]
assert shapes[0] == (278,)
assert shapes[1] == (1068,)
assert shapes[2] == (278,)
sums_expected = (245.621534, 942.477796, 245.621534)
assert_allclose(sums, sums_expected)


def test_mask_get_values_no_overlap():
aper = CircularAperture((-100, -100), r=3)
data = np.ones((101, 101))
values = aper.to_mask().get_values(data)
assert values.size == 1
assert np.isnan(values[0])