From 765388facf43838184d7c28e0def19eecfc4cbc6 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 15:11:51 -0500 Subject: [PATCH 1/8] Fix ApertureMask fill_value --- photutils/aperture/mask.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/photutils/aperture/mask.py b/photutils/aperture/mask.py index 5ce40cdf8..7c3174cb1 100644 --- a/photutils/aperture/mask.py +++ b/photutils/aperture/mask.py @@ -171,8 +171,7 @@ 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 From 50c5880f15885c8c3c34362d55c2d0eae3f676c0 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 15:13:31 -0500 Subject: [PATCH 2/8] Fix ApertureMask for non-finite fill_value with int array --- photutils/aperture/mask.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/photutils/aperture/mask.py b/photutils/aperture/mask.py index 7c3174cb1..deee96547 100644 --- a/photutils/aperture/mask.py +++ b/photutils/aperture/mask.py @@ -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] From bb917fb89d54616988fab6947bb154cd633d89b7 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 15:19:52 -0500 Subject: [PATCH 3/8] Add ApertureMask tests --- photutils/aperture/tests/test_mask.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/photutils/aperture/tests/test_mask.py b/photutils/aperture/tests/test_mask.py index e0ead1da7..874620adb 100644 --- a/photutils/aperture/tests/test_mask.py +++ b/photutils/aperture/tests/test_mask.py @@ -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: @@ -127,3 +127,20 @@ 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]) From 331e92fa0fa93072c37d8379c6b188c29f823b18 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 15:36:14 -0500 Subject: [PATCH 4/8] Add ApertureMask get_values method --- photutils/aperture/mask.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/photutils/aperture/mask.py b/photutils/aperture/mask.py index deee96547..fbc712668 100644 --- a/photutils/aperture/mask.py +++ b/photutils/aperture/mask.py @@ -179,3 +179,31 @@ def multiply(self, data, fill_value=0.): 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] From cf62a9e488a733e4ed006a87f1670a08357b6484 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 15:48:25 -0500 Subject: [PATCH 5/8] Add test for get_values --- photutils/aperture/tests/test_mask.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/photutils/aperture/tests/test_mask.py b/photutils/aperture/tests/test_mask.py index 874620adb..372682a95 100644 --- a/photutils/aperture/tests/test_mask.py +++ b/photutils/aperture/tests/test_mask.py @@ -144,3 +144,16 @@ def test_mask_multiply_fill_value(): 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) From 298f0edbb2f0f1b1b8ec39f69780156f932a031f Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 16:05:56 -0500 Subject: [PATCH 6/8] Add changelog entries --- CHANGES.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 194f43675..c431bdfd4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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``, @@ -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 From d92dc93b197e711dcd063de2216eeaa43b31d2f6 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 16:23:53 -0500 Subject: [PATCH 7/8] Use ApertureMask get_values in photometry --- photutils/aperture/core.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/photutils/aperture/core.py b/photutils/aperture/core.py index a34c1362d..5cef08db0 100644 --- a/photutils/aperture/core.py +++ b/photutils/aperture/core.py @@ -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: @@ -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) From 931b61ed9c0f4acecf92b56d442fafa4135ab01c Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 5 Feb 2021 16:27:03 -0500 Subject: [PATCH 8/8] Add test for no overlap --- photutils/aperture/tests/test_mask.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/photutils/aperture/tests/test_mask.py b/photutils/aperture/tests/test_mask.py index 372682a95..b1660aaf3 100644 --- a/photutils/aperture/tests/test_mask.py +++ b/photutils/aperture/tests/test_mask.py @@ -157,3 +157,11 @@ def test_mask_get_values(): 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])