Skip to content

Commit f257d71

Browse files
pllimrosteen
andauthored
No more centroiding in Simple Aperture Photometry plugin (#1841)
* No more centroiding in Simple Aperture Photometry plugin. Removed outdated known issue. Updated docs and tests. * DOC: Clarify how to recenter * DOC: Fix grammar Co-authored-by: Ricky O'Steen <39831871+rosteen@users.noreply.github.com>
1 parent 028a4a6 commit f257d71

8 files changed

Lines changed: 69 additions & 62 deletions

File tree

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ Cubeviz
5454
Imviz
5555
^^^^^
5656

57+
- Simple Aperture Photometry plugin no longer performs centroiding.
58+
For radial profile, curve of growth, and table reporting, the aperture
59+
center is used instead. For centroiding, use "Recenter" feature in
60+
the Subset Tools plugin. [#1841]
61+
5762
Mosviz
5863
^^^^^^
5964

docs/imviz/export_data.rst

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,8 @@ The columns are as follow:
5151

5252
* :attr:`~photutils.aperture.ApertureStats.id`: ID number assigned to the row,
5353
starting from 1.
54-
* :attr:`~photutils.aperture.ApertureStats.xcentroid`,
55-
:attr:`~photutils.aperture.ApertureStats.ycentroid`: Pixel centroids
56-
calculated using moments. This might differ from center of the aperture.
57-
* :attr:`~photutils.aperture.ApertureStats.sky_centroid`:
58-
`~astropy.coordinates.SkyCoord` associated with the centroid.
54+
* ``xcenter``, ``ycenter``: Center of the aperture (0-indexed).
55+
* ``sky_center``: `~astropy.coordinates.SkyCoord` associated with the center.
5956
If WCS is not available, this field is `None`.
6057
* ``background``: The value from :guilabel:`Background value`, with unit attached.
6158
* :attr:`~photutils.aperture.ApertureStats.sum`: Sum of flux in the aperture.
@@ -108,9 +105,8 @@ The columns are as follow:
108105
.. note::
109106

110107
Aperture sum and statistics are done on the originally drawn aperture only.
111-
Even though centroid is calculated, it is not used to move the aperture
112-
to the new center. However, radial profiles (including Gaussian fitting, if any)
113-
and curve of growth do use the centroid as zero-point on the X-axis.
108+
You can use the :ref:`imviz-subset-plugin` plugin to center it first on the
109+
object of interest, if you wish.
114110

115111
Once you have the results in a table, you can further manipulated them as
116112
documented in :ref:`astropy:astropy-table`.

docs/imviz/plugins.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ an interactively selected region. A typical workflow is as follows:
143143
2. Draw a region over the object of interest (see :ref:`imviz_defining_spatial_regions`).
144144
3. Select the desired image using the :guilabel:`Data` dropdown menu.
145145
4. Select the desired region using the :guilabel:`Subset` dropdown menu.
146+
You can use the :ref:`imviz-subset-plugin` plugin to center it first on the
147+
object of interest using its center of mass, if you wish.
148+
Depending on the object, it may take several iterations for re-centering
149+
to converge, or it may never converge at all.
146150
5. If you want to subtract background before performing photometry,
147151
you have the following 3 options. Otherwise if your image is already
148152
background subtracted, choose "Manual" and leave the background set at 0:
@@ -240,7 +244,7 @@ catalog dropdown menu.
240244
This plugin is still under active development. As a result, the search only uses the SDSS DR17 catalog
241245
and works best when you only have a single image loaded in a viewer.
242246

243-
To load a catalog from a supported `JWST ECSV catalog file <https://jwst-pipeline.readthedocs.io/en/latest/jwst/source_catalog/main.html#output-products>`_, choose "From File...".
247+
To load a catalog from a supported `JWST ECSV catalog file <https://jwst-pipeline.readthedocs.io/en/latest/jwst/source_catalog/main.html#output-products>`_, choose "From File...".
244248
The file must be able to be parsed by `astropy.table.Table.read` and contain a column labeled 'sky_centroid'.
245249
Clicking :guilabel:`SEARCH` will show markers for any entry within the filtered zoom window.
246250

docs/known_bugs.rst

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,6 @@ to show the markers, or not at all. This is a known bug reported in
143143
https://github.com/glue-viz/glue-jupyter/issues/243 . If you encounter this,
144144
try a different OS/browser combo.
145145

146-
Simple Aperture Photometry Plugin and dithered images
147-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
148-
149-
Due to a known bug reported in https://github.com/glue-viz/glue-astronomy/issues/52 ,
150-
aperture photometry and radial profile will report inaccurate results when you
151-
calculate them on dithered images linked by WCS *unless* you are on the reference image
152-
(this is usually the first loaded image).
153-
154146
.. _known_issues_specviz:
155147

156148
Specviz

jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ def vue_do_aper_phot(self, *args, **kwargs):
245245

246246
data = self._selected_data
247247
reg = self._selected_subset
248+
xcenter = reg.center.x
249+
ycenter = reg.center.y
248250

249251
# Reset last fitted model
250252
fit_model = None
@@ -257,6 +259,10 @@ def vue_do_aper_phot(self, *args, **kwargs):
257259
bg = float(self.background_value)
258260
except ValueError: # Clearer error message
259261
raise ValueError('Missing or invalid background value')
262+
if data.coords is not None:
263+
sky_center = data.coords.pixel_to_world(xcenter, ycenter)
264+
else:
265+
sky_center = None
260266
aperture = regions2aperture(reg)
261267
include_pixarea_fac = False
262268
include_counts_fac = False
@@ -289,12 +295,10 @@ def vue_do_aper_phot(self, *args, **kwargs):
289295
include_flux_scale = True
290296
phot_aperstats = ApertureStats(comp_data, aperture, wcs=data.coords, local_bkg=bg)
291297
phot_table = phot_aperstats.to_table(columns=(
292-
'id', 'xcentroid', 'ycentroid', 'sky_centroid', 'sum', 'sum_aper_area',
298+
'id', 'sum', 'sum_aper_area',
293299
'min', 'max', 'mean', 'median', 'mode', 'std', 'mad_std', 'var',
294300
'biweight_location', 'biweight_midvariance', 'fwhm', 'semimajor_sigma',
295301
'semiminor_sigma', 'orientation', 'eccentricity')) # Some cols excluded, add back as needed. # noqa
296-
phot_table['xcentroid'].unit = u.pix # photutils only assumes, we make it real
297-
phot_table['ycentroid'].unit = u.pix
298302
rawsum = phot_table['sum'][0]
299303

300304
if include_pixarea_fac:
@@ -323,12 +327,14 @@ def vue_do_aper_phot(self, *args, **kwargs):
323327

324328
# Extra info beyond photutils.
325329
phot_table.add_columns(
326-
[bg, pixarea_fac, sum_ct, sum_ct_err, ctfac, sum_mag, flux_scale, data.label,
330+
[xcenter * u.pix, ycenter * u.pix, sky_center,
331+
bg, pixarea_fac, sum_ct, sum_ct_err, ctfac, sum_mag, flux_scale, data.label,
327332
reg.meta.get('label', ''), Time(datetime.utcnow())],
328-
names=['background', 'pixarea_tot', 'aperture_sum_counts',
329-
'aperture_sum_counts_err', 'counts_fac', 'aperture_sum_mag', 'flux_scaling',
333+
names=['xcenter', 'ycenter', 'sky_center', 'background', 'pixarea_tot',
334+
'aperture_sum_counts', 'aperture_sum_counts_err', 'counts_fac',
335+
'aperture_sum_mag', 'flux_scaling',
330336
'data_label', 'subset_label', 'timestamp'],
331-
indexes=[4, 6, 6, 6, 6, 6, 6, 21, 21, 21])
337+
indexes=[1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 18, 18, 18])
332338

333339
# Attach to app for Python extraction.
334340
if (not hasattr(self.app, '_aper_phot_results') or
@@ -352,9 +358,9 @@ def vue_do_aper_phot(self, *args, **kwargs):
352358
line_y_sc = bqplot.LinearScale()
353359

354360
if self.current_plot_type == "Curve of Growth":
355-
self._fig.title = 'Curve of growth from source centroid'
361+
self._fig.title = 'Curve of growth from aperture center'
356362
x_arr, sum_arr, x_label, y_label = _curve_of_growth(
357-
comp_data, phot_aperstats.centroid, aperture, phot_table['sum'][0],
363+
comp_data, (xcenter, ycenter), aperture, phot_table['sum'][0],
358364
wcs=data.coords, background=bg, pixarea_fac=pixarea_fac)
359365
self._fig.axes = [bqplot.Axis(scale=line_x_sc, label=x_label),
360366
bqplot.Axis(scale=line_y_sc, orientation='vertical',
@@ -370,33 +376,33 @@ def vue_do_aper_phot(self, *args, **kwargs):
370376
label=comp.units or 'Value')]
371377

372378
if self.current_plot_type == "Radial Profile":
373-
self._fig.title = 'Radial profile from source centroid'
379+
self._fig.title = 'Radial profile from aperture center'
374380
x_data, y_data = _radial_profile(
375-
phot_aperstats.data_cutout, phot_aperstats.bbox, phot_aperstats.centroid,
381+
phot_aperstats.data_cutout, phot_aperstats.bbox, (xcenter, ycenter),
376382
raw=False)
377383
bqplot_line = bqplot.Lines(x=x_data, y=y_data, marker='circle',
378384
scales={'x': line_x_sc, 'y': line_y_sc},
379385
marker_size=32, colors='gray')
380386
else: # Radial Profile (Raw)
381-
self._fig.title = 'Raw radial profile from source centroid'
387+
self._fig.title = 'Raw radial profile from aperture center'
382388
x_data, y_data = _radial_profile(
383-
phot_aperstats.data_cutout, phot_aperstats.bbox, phot_aperstats.centroid,
389+
phot_aperstats.data_cutout, phot_aperstats.bbox, (xcenter, ycenter),
384390
raw=True)
385391
bqplot_line = bqplot.Scatter(x=x_data, y=y_data, marker='circle',
386392
scales={'x': line_x_sc, 'y': line_y_sc},
387393
default_size=1, colors='gray')
388394

389395
# Fit Gaussian1D to radial profile data.
390-
# mean is fixed at 0 because we recentered to centroid.
391396
if self.fit_radial_profile:
392397
fitter = LevMarLSQFitter()
393398
y_max = y_data.max()
399+
x_mean = x_data[np.where(y_data == y_max)].mean()
394400
std = 0.5 * (phot_table['semimajor_sigma'][0] +
395401
phot_table['semiminor_sigma'][0])
396402
if isinstance(std, u.Quantity):
397403
std = std.value
398-
gs = Gaussian1D(amplitude=y_max, mean=0, stddev=std,
399-
fixed={'mean': True, 'amplitude': True},
404+
gs = Gaussian1D(amplitude=y_max, mean=x_mean, stddev=std,
405+
fixed={'amplitude': True},
400406
bounds={'amplitude': (y_max * 0.5, y_max)})
401407
if Version(astropy.__version__) < Version('5.2'):
402408
fitter_kw = {}
@@ -433,13 +439,13 @@ def vue_do_aper_phot(self, *args, **kwargs):
433439
continue
434440
x = phot_table[key][0]
435441
if (isinstance(x, (int, float, u.Quantity)) and
436-
key not in ('xcentroid', 'ycentroid', 'sky_centroid', 'sum_aper_area',
442+
key not in ('xcenter', 'ycenter', 'sky_center', 'sum_aper_area',
437443
'aperture_sum_counts', 'aperture_sum_mag')):
438444
tmp.append({'function': key, 'result': f'{x:.4e}'})
439-
elif key == 'sky_centroid' and x is not None:
440-
tmp.append({'function': 'RA centroid', 'result': f'{x.ra.deg:.6f} deg'})
441-
tmp.append({'function': 'Dec centroid', 'result': f'{x.dec.deg:.6f} deg'})
442-
elif key in ('xcentroid', 'ycentroid', 'sum_aper_area'):
445+
elif key == 'sky_center' and x is not None:
446+
tmp.append({'function': 'RA center', 'result': f'{x.ra.deg:.6f} deg'})
447+
tmp.append({'function': 'Dec center', 'result': f'{x.dec.deg:.6f} deg'})
448+
elif key in ('xcenter', 'ycenter', 'sum_aper_area'):
443449
tmp.append({'function': key, 'result': f'{x:.1f}'})
444450
elif key == 'aperture_sum_counts' and x is not None:
445451
tmp.append({'function': key, 'result':
@@ -452,7 +458,7 @@ def vue_do_aper_phot(self, *args, **kwargs):
452458
# Also display fit results
453459
fit_tmp = []
454460
if fit_model is not None and isinstance(fit_model, Gaussian1D):
455-
for param in ('fwhm', 'amplitude'): # mean is fixed at 0
461+
for param in ('mean', 'fwhm', 'amplitude'):
456462
p_val = getattr(fit_model, param)
457463
if isinstance(p_val, Parameter):
458464
p_val = p_val.value

jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
<jupyter-widget :widget="radial_plot" style="width: 100%; height: 480px" />
142142
</v-row>
143143

144-
<div v-if="plot_available && fit_radial_profile">
144+
<div v-if="plot_available && fit_radial_profile && current_plot_type != 'Curve of Growth'">
145145
<j-plugin-section-header>Gaussian Fit Results</j-plugin-section-header>
146146
<v-row no-gutters>
147147
<v-col cols=6><U>Result</U></v-col>

jdaviz/configs/imviz/tests/test_parser.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,11 @@ def test_parse_jwst_nircam_level2(self, imviz_helper):
277277
assert_allclose(phot_plugin.flux_scaling, 0.003631)
278278
phot_plugin.vue_do_aper_phot()
279279
tbl = imviz_helper.get_aperture_photometry_results()
280-
assert_quantity_allclose(tbl[0]['xcentroid'], 970.935492 * u.pix)
281-
assert_quantity_allclose(tbl[0]['ycentroid'], 1116.967619 * u.pix)
282-
sky = tbl[0]['sky_centroid']
280+
assert_quantity_allclose(tbl[0]['xcenter'], 970.95 * u.pix)
281+
assert_quantity_allclose(tbl[0]['ycenter'], 1116.05 * u.pix)
282+
sky = tbl[0]['sky_center']
283283
assert_allclose(sky.ra.deg, 80.48419863)
284-
assert_allclose(sky.dec.deg, -69.494592)
284+
assert_allclose(sky.dec.deg, -69.494608)
285285
data_unit = u.MJy / u.sr
286286
assert_quantity_allclose(tbl[0]['background'], 0.1741226315498352 * data_unit)
287287
assert_quantity_allclose(tbl[0]['sum'], 4.486487e-11 * u.MJy, rtol=1e-6)
@@ -402,9 +402,9 @@ def test_parse_hst_drz(self, imviz_helper):
402402
assert_allclose(phot_plugin.pixel_area, 0.0025) # Not used but still auto-populated
403403
phot_plugin.vue_do_aper_phot()
404404
tbl = imviz_helper.get_aperture_photometry_results()
405-
assert_quantity_allclose(tbl[0]['xcentroid'], 1487.60825422 * u.pix, atol=2 * u.pix)
406-
assert_quantity_allclose(tbl[0]['ycentroid'], 2573.83983184 * u.pix, atol=2 * u.pix)
407-
sky = tbl[0]['sky_centroid']
405+
assert_quantity_allclose(tbl[0]['xcenter'], 1488.5 * u.pix, atol=2 * u.pix)
406+
assert_quantity_allclose(tbl[0]['ycenter'], 2576 * u.pix, atol=2 * u.pix)
407+
sky = tbl[0]['sky_center']
408408
assert_allclose(sky.ra.deg, 3.684062989070131, rtol=1e-3)
409409
assert_allclose(sky.dec.deg, 10.802045612042956, rtol=1e-3)
410410
data_unit = u.electron / u.s

jdaviz/configs/imviz/tests/test_simple_aper_phot.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def test_plugin_wcs_dithered(self):
6565

6666
# Check photometry results.
6767
assert tbl.colnames == [
68-
'id', 'xcentroid', 'ycentroid', 'sky_centroid', 'background', 'sum',
68+
'id', 'xcenter', 'ycenter', 'sky_center', 'background', 'sum',
6969
'sum_aper_area', 'pixarea_tot', 'aperture_sum_counts', 'aperture_sum_counts_err',
7070
'counts_fac', 'aperture_sum_mag', 'flux_scaling', 'min', 'max', 'mean', 'median',
7171
'mode', 'std', 'mad_std', 'var', 'biweight_location', 'biweight_midvariance',
@@ -99,11 +99,11 @@ def test_plugin_wcs_dithered(self):
9999
assert_array_equal(tbl['subset_label'], ['Subset 1', 'Subset 1'])
100100
assert tbl['timestamp'].scale == 'utc'
101101

102-
# Sky is the same but xcentroid different due to dithering.
102+
# Sky is the same but xcenter different due to dithering.
103103
# The aperture sum is different too because mask is a little off limit in second image.
104-
assert_quantity_allclose(tbl['xcentroid'], [4.5, 5.5] * u.pix)
105-
assert_quantity_allclose(tbl['ycentroid'], 4.5 * u.pix)
106-
sky = tbl['sky_centroid']
104+
assert_quantity_allclose(tbl['xcenter'], [4.5, 5.5] * u.pix)
105+
assert_quantity_allclose(tbl['ycenter'], 4.5 * u.pix)
106+
sky = tbl['sky_center']
107107
assert_allclose(sky.ra.deg, 337.518943)
108108
assert_allclose(sky.dec.deg, -20.832083)
109109
assert_allclose(tbl['sum'], [63.617251, 62.22684693104279])
@@ -117,9 +117,9 @@ def test_plugin_wcs_dithered(self):
117117
tbl = self.imviz.get_aperture_photometry_results()
118118
assert len(tbl) == 3 # New result is appended
119119
assert tbl[-1]['id'] == 3
120-
assert_quantity_allclose(tbl[-1]['xcentroid'], 4.5 * u.pix)
121-
assert_quantity_allclose(tbl[-1]['ycentroid'], 2 * u.pix)
122-
sky = tbl[-1]['sky_centroid']
120+
assert_quantity_allclose(tbl[-1]['xcenter'], 4.5 * u.pix)
121+
assert_quantity_allclose(tbl[-1]['ycenter'], 2 * u.pix)
122+
sky = tbl[-1]['sky_center']
123123
assert_allclose(sky.ra.deg, 337.51894336144454)
124124
assert_allclose(sky.dec.deg, -20.832777499255897)
125125
assert_quantity_allclose(tbl[-1]['sum_aper_area'], 28.274334 * (u.pix * u.pix))
@@ -139,11 +139,11 @@ def test_plugin_wcs_dithered(self):
139139
tbl = self.imviz.get_aperture_photometry_results()
140140
assert len(tbl) == 4 # New result is appended
141141
assert tbl[-1]['id'] == 4
142-
assert np.isnan(tbl[-1]['xcentroid'])
143-
assert np.isnan(tbl[-1]['ycentroid'])
144-
sky = tbl[-1]['sky_centroid']
145-
assert np.isnan(sky.ra.deg)
146-
assert np.isnan(sky.dec.deg)
142+
assert_quantity_allclose(tbl[-1]['xcenter'], 4.5 * u.pix)
143+
assert_quantity_allclose(tbl[-1]['ycenter'], 4.5 * u.pix)
144+
sky = tbl[-1]['sky_center']
145+
assert_allclose(sky.ra.deg, 337.51894336144454)
146+
assert_allclose(sky.dec.deg, -20.832083)
147147
assert_quantity_allclose(tbl[-1]['sum_aper_area'], 81 * (u.pix * u.pix))
148148
assert_allclose(tbl[-1]['sum'], 0)
149149
assert_allclose(tbl[-1]['mean'], 0)
@@ -168,7 +168,7 @@ def test_plugin_wcs_dithered(self):
168168
# Curve of growth
169169
phot_plugin.current_plot_type = 'Curve of Growth'
170170
phot_plugin.vue_do_aper_phot()
171-
assert phot_plugin._fig.title == 'Curve of growth from source centroid'
171+
assert phot_plugin._fig.title == 'Curve of growth from aperture center'
172172

173173

174174
class TestSimpleAperPhot_NoWCS(BaseImviz_WCS_NoWCS):
@@ -187,7 +187,7 @@ def test_plugin_no_wcs(self):
187187
phot_plugin.vue_do_aper_phot()
188188
tbl = self.imviz.get_aperture_photometry_results()
189189
assert len(tbl) == 1 # Old table discarded due to incompatible column
190-
assert_array_equal(tbl['sky_centroid'], None)
190+
assert_array_equal(tbl['sky_center'], None)
191191

192192

193193
def test_annulus_background(imviz_helper):
@@ -263,6 +263,8 @@ def test_annulus_background(imviz_helper):
263263

264264
# NOTE: Extracting the cutout for radial profile is aperture
265265
# shape agnostic, so we use ellipse as representative case.
266+
# NOTE: This test only tests the radial profile algorithm and does
267+
# not care if the actual plugin use centroid or not.
266268
class TestRadialProfile():
267269
def setup_class(self):
268270
data = np.ones((51, 51)) * u.nJy
@@ -286,6 +288,8 @@ def test_profile_imexam(self):
286288
assert_allclose(y_arr, 1)
287289

288290

291+
# NOTE: This test only tests the curve of growth algorithm and does
292+
# not care if the actual plugin use centroid or not.
289293
@pytest.mark.parametrize('with_unit', (False, True))
290294
def test_curve_of_growth(with_unit):
291295
data = np.ones((51, 51))

0 commit comments

Comments
 (0)