Skip to content

Commit

Permalink
FEAT: Load annulus from file
Browse files Browse the repository at this point in the history
and allow IMPORT DATA to load reg files.

Annulus support provided by glue-viz/glue#2403 and glue-viz/glue-astronomy#92

Add test for exception.

Bump minimum requirements for glue-core and glue-astronomy.
  • Loading branch information
pllim committed Jun 5, 2023
1 parent 7ffaa26 commit beb45e0
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 34 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Cubeviz
Imviz
^^^^^

- Added the ability to load DS9 region files (``.reg``) using the ``IMPORT DATA``
button. However, this only works after loading at least one image into Imviz. [#2201]

- Added support for new ``CircularAnnulusROI`` subset from glue. [#2201]

Mosviz
^^^^^^

Expand Down
3 changes: 3 additions & 0 deletions docs/imviz/import_data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ application. A notification will appear to let users know if the data import
was successful. Afterward, the new data set can be found in the :guilabel:`Data`
tab of each viewer's options menu as described in :ref:`cubeviz-selecting-data`.

Once data is loaded, you may use the :guilabel:`Import Data` button again
to load regions from a ``.reg`` file; also see :ref:`imviz-import-regions-api`.

.. _imviz-import-api:

Importing data via the API
Expand Down
5 changes: 5 additions & 0 deletions jdaviz/configs/imviz/plugins/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ def parse_data(app, file_obj, ext=None, data_label=None):
)
with rdd.open(file_obj) as pf:
_parse_image(app, pf, data_label, ext=ext)

elif file_obj_lower.endswith('.reg'):
# This will load DS9 regions as Subset but only if there is already data.
app._jdaviz_helper.load_regions_from_file(file_obj)

else: # Assume FITS
with fits.open(file_obj) as pf:
_parse_image(app, pf, data_label, ext=ext)
Expand Down
4 changes: 4 additions & 0 deletions jdaviz/configs/imviz/tests/data/ds9_annulus_01.reg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Region file format: DS9 version 4.1
global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1
icrs
annulus(197.8929,-1.36599,1.9820003",3.9640007",5.946001") # color=magenta font="helvetica 10 bold roman" text={Annulus}
55 changes: 32 additions & 23 deletions jdaviz/configs/imviz/tests/test_regions.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import glue_astronomy
import numpy as np
import pytest
from astropy import units as u
from astropy.coordinates import SkyCoord, Angle
from astropy.utils.data import get_pkg_data_filename
from packaging.version import Version
from photutils.aperture import CircularAperture, SkyCircularAperture
from regions import (PixCoord, CircleSkyRegion, RectanglePixelRegion, CirclePixelRegion,
EllipsePixelRegion, PointSkyRegion, PolygonPixelRegion,
CircleAnnulusPixelRegion, CircleAnnulusSkyRegion, Regions)

from jdaviz.configs.imviz.tests.utils import BaseImviz_WCS_NoWCS

GLUE_ASTRONOMY_LT_0_7_1 = not (Version(glue_astronomy.__version__) >= Version("0.7.1.dev"))


class BaseRegionHandler:
"""Test to see if region is loaded.
Expand Down Expand Up @@ -122,13 +119,15 @@ def test_regions_sky_has_wcs(self):
self.imviz._apply_interactive_region('bqplot:circle', (1.5, 2.5), (3.6, 4.6))

sky = SkyCoord(ra=337.5202808, dec=-20.833333059999998, unit='deg')
# This will become indistinguishable from normal Subset.
# These will become indistinguishable from normal Subset.
my_reg_sky_1 = CircleSkyRegion(sky, Angle(0.5, u.arcsec))
# Masked subset.
my_reg_sky_2 = CircleAnnulusSkyRegion(center=sky, inner_radius=0.0004 * u.deg,
outer_radius=0.0005 * u.deg)
# Add them both.
bad_regions = self.imviz.load_regions([my_reg_sky_1, my_reg_sky_2], return_bad_regions=True)
# Masked subset.
my_reg_sky_3 = PolygonPixelRegion(vertices=PixCoord(x=[1, 1, 3, 3, 1], y=[1, 3, 3, 1, 1]))
# Add them all.
bad_regions = self.imviz.load_regions([my_reg_sky_1, my_reg_sky_2, my_reg_sky_3],
return_bad_regions=True)
assert len(bad_regions) == 0

# Mimic interactive regions (after)
Expand All @@ -139,15 +138,28 @@ def test_regions_sky_has_wcs(self):
# that check hopefully is already done in glue-astronomy.
# Apparently, static region ate up one number...
subsets = self.imviz.get_interactive_regions()
assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 4', 'Subset 5'], subsets
assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 3', 'Subset 5', 'Subset 6'], subsets # noqa: E501
assert isinstance(subsets['Subset 1'], CirclePixelRegion)
assert isinstance(subsets['Subset 2'], CirclePixelRegion)
assert isinstance(subsets['Subset 4'], EllipsePixelRegion)
assert isinstance(subsets['Subset 5'], RectanglePixelRegion)
assert isinstance(subsets['Subset 3'], CircleAnnulusPixelRegion)
assert isinstance(subsets['Subset 5'], EllipsePixelRegion)
assert isinstance(subsets['Subset 6'], RectanglePixelRegion)

# Check static region
self.verify_region_loaded('MaskedSubset 1')

def test_regions_annulus_from_load_data(self):
# This file actually will load 2 annuli
regfile = get_pkg_data_filename('data/ds9_annulus_01.reg')
self.imviz.load_data(regfile)
assert len(self.imviz.app.data_collection) == 2 # Make sure not loaded as data

subsets = self.imviz.get_interactive_regions()
subset_names = list(subsets.keys())
assert subset_names == ['Subset 1', 'Subset 2']
for n in subset_names:
assert isinstance(subsets[n], CircleAnnulusPixelRegion)

def test_photutils_pixel(self):
my_aper = CircularAperture((5, 5), r=2)
bad_regions = self.imviz.load_regions([my_aper], return_bad_regions=True)
Expand All @@ -173,18 +185,21 @@ def setup_class(self):
self.raw_regions = Regions.read(self.region_file, format='ds9')

def test_ds9_load_all(self, imviz_helper):
with pytest.raises(ValueError, match="Cannot load regions without data"):
imviz_helper.load_data(self.region_file)

self.viewer = imviz_helper.default_viewer
imviz_helper.load_data(self.arr, data_label='my_image')
bad_regions = imviz_helper.load_regions_from_file(self.region_file, return_bad_regions=True)
assert len(bad_regions) == 1

# Will load 8/9 and 6 of that become ROIs.
# Will load 8/9 and 7 of that become ROIs.
subsets = imviz_helper.get_interactive_regions()
assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 3',
'Subset 4', 'Subset 5', 'Subset 6'], subsets
'Subset 4', 'Subset 5', 'Subset 6', 'Subset 7'], subsets

for i in (1, 2): # The other 2 are MaskedSubset
self.verify_region_loaded(f'MaskedSubset {i}', count=1)
# The other 1 is MaskedSubset
self.verify_region_loaded('MaskedSubset 1', count=1)

def test_ds9_load_two_good(self, imviz_helper):
self.viewer = imviz_helper.default_viewer
Expand Down Expand Up @@ -234,18 +249,12 @@ def test_annulus(self):
new_subset = subset_groups[0].subset_state & ~subset_groups[1].subset_state
self.viewer.apply_subset_state(new_subset)

# In older glue-astronomy, annulus is no longer accessible by API
# but also should not crash Imviz.
subsets = self.imviz.get_interactive_regions()
assert len(self.imviz.app.data_collection.subset_groups) == 3
if GLUE_ASTRONOMY_LT_0_7_1:
expected_subset_keys = ['Subset 1', 'Subset 2']
else:
expected_subset_keys = ['Subset 1', 'Subset 2', 'Subset 3']
assert isinstance(subsets['Subset 3'], CircleAnnulusPixelRegion)
assert list(subsets.keys()) == expected_subset_keys, subsets
assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 3'], subsets
assert isinstance(subsets['Subset 1'], CirclePixelRegion)
assert isinstance(subsets['Subset 2'], CirclePixelRegion)
assert isinstance(subsets['Subset 3'], CircleAnnulusPixelRegion)

# Clear the regions for next test.
self.imviz._delete_all_regions()
Expand Down
23 changes: 16 additions & 7 deletions jdaviz/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,12 +658,17 @@ def load_regions(self, regions, max_num_regions=None, refdata_label=None,
If not requested, return `None`.
"""
if len(self.app.data_collection) == 0:
raise ValueError('Cannot load regions without data.')

from photutils.aperture import (CircularAperture, SkyCircularAperture,
EllipticalAperture, SkyEllipticalAperture,
RectangularAperture, SkyRectangularAperture)
RectangularAperture, SkyRectangularAperture,
CircularAnnulus, SkyCircularAnnulus)
from regions import (Regions, CirclePixelRegion, CircleSkyRegion,
EllipsePixelRegion, EllipseSkyRegion,
RectanglePixelRegion, RectangleSkyRegion)
RectanglePixelRegion, RectangleSkyRegion,
CircleAnnulusPixelRegion, CircleAnnulusSkyRegion)
from jdaviz.core.region_translators import regions2roi, aperture2regions

# If user passes in one region obj instead of list, try to be smart.
Expand All @@ -688,23 +693,27 @@ def load_regions(self, regions, max_num_regions=None, refdata_label=None,
has_wcs = data_has_valid_wcs(data, ndim=2)

for region in regions:
if isinstance(region, (SkyCircularAperture, SkyEllipticalAperture,
SkyRectangularAperture, CircleSkyRegion,
EllipseSkyRegion, RectangleSkyRegion)) and not has_wcs:
if (isinstance(region, (SkyCircularAperture, SkyEllipticalAperture,
SkyRectangularAperture, SkyCircularAnnulus,
CircleSkyRegion, EllipseSkyRegion,
RectangleSkyRegion, CircleAnnulusSkyRegion))
and not has_wcs):
bad_regions.append((region, 'Sky region provided but data has no valid WCS'))
continue

# photutils: Convert to regions shape first
if isinstance(region, (CircularAperture, SkyCircularAperture,
EllipticalAperture, SkyEllipticalAperture,
RectangularAperture, SkyRectangularAperture)):
RectangularAperture, SkyRectangularAperture,
CircularAnnulus, SkyCircularAnnulus)):
region = aperture2regions(region)

# regions: Convert to ROI.
# NOTE: Out-of-bounds ROI will succeed; this is native glue behavior.
if isinstance(region, (CirclePixelRegion, CircleSkyRegion,
EllipsePixelRegion, EllipseSkyRegion,
RectanglePixelRegion, RectangleSkyRegion)):
RectanglePixelRegion, RectangleSkyRegion,
CircleAnnulusPixelRegion, CircleAnnulusSkyRegion)):
state = regions2roi(region, wcs=data.coords)

# TODO: Do we want user to specify viewer? Does it matter?
Expand Down
9 changes: 7 additions & 2 deletions jdaviz/core/region_translators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from astropy import units as u
from astropy.coordinates import Angle
from glue.core.roi import CircularROI, EllipticalROI, RectangularROI
from glue.core.roi import CircularROI, EllipticalROI, RectangularROI, CircularAnnulusROI
from photutils.aperture import (CircularAperture, SkyCircularAperture,
EllipticalAperture, SkyEllipticalAperture,
RectangularAperture, SkyRectangularAperture,
Expand Down Expand Up @@ -115,7 +115,8 @@ def regions2roi(region_shape, wcs=None):
<glue.core.roi.CircularROI object at ...>
"""
if isinstance(region_shape, (CircleSkyRegion, EllipseSkyRegion, RectangleSkyRegion)):
if isinstance(region_shape, (CircleSkyRegion, EllipseSkyRegion, RectangleSkyRegion,
CircleAnnulusSkyRegion)):
if wcs is None:
raise ValueError(f'WCS must be provided for {region_shape}')

Expand All @@ -140,6 +141,10 @@ def regions2roi(region_shape, wcs=None):
roi = RectangularROI(
xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
theta=region_shape.angle.to_value(u.radian))
elif isinstance(region_shape, CircleAnnulusPixelRegion):
roi = CircularAnnulusROI(
xc=region_shape.center.x, yc=region_shape.center.y,
inner_radius=region_shape.inner_radius, outer_radius=region_shape.outer_radius)
else:
raise NotImplementedError(f'{region_shape.__class__.__name__} is not supported')

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dependencies = [
"traitlets>=5.0.5",
"bqplot>=0.12.37",
"bqplot-image-gl>=1.4.11",
"glue-core>=1.6.0,!=1.9.0,!=1.10",
"glue-core>=1.11",
"glue-jupyter>=0.15.0",
"echo>=0.5.0",
"ipykernel>=6.19.4",
Expand All @@ -26,7 +26,7 @@ dependencies = [
"specutils>=1.9",
"specreduce>=1.3.0,<1.4.0",
"photutils>=1.4",
"glue-astronomy>=0.7",
"glue-astronomy>=0.9",
"asteval>=0.9.23",
"idna",
"vispy>=0.6.5",
Expand Down

0 comments on commit beb45e0

Please sign in to comment.