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

FEAT: Load annulus from file, load reg files from IMPORT DATA #2201

Merged
merged 1 commit into from
Jun 7, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if you try the .reg file before loading an image? Is the ValueError shown in the UI or as a traceback in the console?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It throws error and this is tested on L188 in test_regions.py in this PR:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error would bubble up in the same was as any other error in Imviz parsing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this look good to you?

Screenshot 2023-06-07 161022


- 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