-
-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from joleroi/region_tests
Add tests for ds9 region parser
- Loading branch information
Showing
68 changed files
with
3,042 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
"""I/O | ||
""" | ||
from .read_ds9 import * | ||
from .write_ds9 import * | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
# Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
|
||
import string | ||
import itertools | ||
import re | ||
from astropy import units as u | ||
from astropy import coordinates | ||
from ..shapes import circle, rectangle, polygon, ellipse | ||
from ..core import PixCoord | ||
|
||
|
||
def read_ds9(filename): | ||
region_list = ds9_parser(filename) | ||
return region_list_to_objects(region_list) | ||
|
||
|
||
def coordinate(string_rep, unit): | ||
# Any ds9 coordinate representation (sexagesimal or degrees) | ||
if 'd' in string_rep or 'h' in string_rep: | ||
return coordinates.Angle(string_rep) | ||
elif unit is 'hour_or_deg': | ||
if ':' in string_rep: | ||
return coordinates.Angle(string_rep, unit=u.hour) | ||
else: | ||
return coordinates.Angle(string_rep, unit=u.deg) | ||
elif unit.is_equivalent(u.deg): | ||
return coordinates.Angle(string_rep, unit=unit) | ||
else: | ||
return u.Quantity(float(string_rep), unit) | ||
|
||
unit_mapping = {'"': u.arcsec, | ||
"'": u.arcmin, | ||
'r': u.rad, | ||
'i': u.dimensionless_unscaled, | ||
} | ||
|
||
|
||
def angular_length_quantity(string_rep): | ||
has_unit = string_rep[-1] not in string.digits | ||
if has_unit: | ||
unit = unit_mapping[string_rep[-1]] | ||
return u.Quantity(float(string_rep[:-1]), unit=unit) | ||
else: | ||
return u.Quantity(float(string_rep), unit=u.deg) | ||
|
||
# these are the same function, just different names | ||
radius = angular_length_quantity | ||
width = angular_length_quantity | ||
height = angular_length_quantity | ||
angle = angular_length_quantity | ||
|
||
language_spec = {'point': (coordinate, coordinate), | ||
'circle': (coordinate, coordinate, radius), | ||
# This is a special case to deal with n elliptical annuli | ||
'ellipse': itertools.chain((coordinate, coordinate), itertools.cycle((radius, ))), | ||
'box': (coordinate, coordinate, width, height, angle), | ||
'polygon': itertools.cycle((coordinate, )), | ||
} | ||
|
||
coordinate_systems = ['fk5', 'fk4', 'icrs', 'galactic', 'wcs', 'physical', 'image', 'ecliptic'] | ||
coordinate_systems += ['wcs{0}'.format(letter) for letter in string.ascii_lowercase] | ||
|
||
coordsys_name_mapping = dict(zip(coordinates.frame_transform_graph.get_names(), | ||
coordinates.frame_transform_graph.get_names())) | ||
coordsys_name_mapping['ecliptic'] = 'geocentrictrueecliptic' # needs expert attention TODO | ||
|
||
hour_or_deg = 'hour_or_deg' | ||
coordinate_units = {'fk5': (hour_or_deg, u.deg), | ||
'fk4': (hour_or_deg, u.deg), | ||
'icrs': (hour_or_deg, u.deg), | ||
'geocentrictrueecliptic': (u.deg, u.deg), | ||
'galactic': (u.deg, u.deg), | ||
'physical': (u.dimensionless_unscaled, u.dimensionless_unscaled), | ||
'image': (u.dimensionless_unscaled, u.dimensionless_unscaled), | ||
'wcs': (u.dimensionless_unscaled, u.dimensionless_unscaled), | ||
} | ||
for letter in string.ascii_lowercase: | ||
coordinate_units['wcs{0}'.format(letter)] = (u.dimensionless_unscaled, u.dimensionless_unscaled) | ||
|
||
region_type_or_coordsys_re = re.compile("^#? *(-?)([a-zA-Z0-9]+)") | ||
|
||
paren = re.compile("[()]") | ||
|
||
def strip_paren(string_rep): | ||
return paren.sub("", string_rep) | ||
|
||
|
||
def region_list_to_objects(region_list): | ||
viz_keywords = ['color', 'dashed', 'width', 'point', 'font', 'text'] | ||
|
||
output_list = [] | ||
for region_type, coord_list, meta in region_list: | ||
#print("region_type, region_type is 'circle', type(region_type), type('circle'), id(region_type), id('circle'), id(str(region_type))") | ||
#print(region_type, region_type is 'circle', type(region_type), type('circle'), id(region_type), id('circle'), id(str(region_type))) | ||
if region_type == 'circle': | ||
if isinstance(coord_list[0], coordinates.SkyCoord): | ||
reg = circle.CircleSkyRegion(coord_list[0], coord_list[1]) | ||
elif isinstance(coord_list[0], PixCoord): | ||
reg = circle.CirclePixelRegion(coord_list[0], coord_list[1]) | ||
else: | ||
raise ValueError("No central coordinate") | ||
elif region_type == 'ellipse': | ||
# Do not read elliptical annuli for now | ||
if len(coord_list) > 4: | ||
continue | ||
if isinstance(coord_list[0], coordinates.SkyCoord): | ||
reg = ellipse.EllipseSkyRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3]) | ||
elif isinstance(coord_list[0], PixCoord): | ||
reg = ellipse.EllipsePixelRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3]) | ||
else: | ||
raise ValueError("No central coordinate") | ||
elif region_type == 'polygon': | ||
if isinstance(coord_list[0], coordinates.SkyCoord): | ||
reg = polygon.PolygonSkyRegion(coord_list[0]) | ||
elif isinstance(coord_list[0], PixCoord): | ||
reg = polygon.PolygonPixelRegion(coord_list[0]) | ||
else: | ||
raise ValueError("No central coordinate") | ||
elif region_type == 'rectangle': | ||
if isinstance(coord_list[0], coordinates.SkyCoord): | ||
reg = rectangle.RectangleSkyRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3]) | ||
elif isinstance(coord_list[0], PixCoord): | ||
reg = rectangle.RectanglePixelRegion(coord_list[0], coord_list[1], coord_list[2], coord_list[3]) | ||
else: | ||
raise ValueError("No central coordinate") | ||
else: | ||
continue | ||
reg.vizmeta = {key: meta[key] for key in meta.keys() if key in viz_keywords} | ||
reg.meta = {key: meta[key] for key in meta.keys() if key not in viz_keywords} | ||
output_list.append(reg) | ||
return output_list | ||
|
||
|
||
def ds9_parser(filename): | ||
""" | ||
Parse a complete ds9 .reg file | ||
Returns | ||
------- | ||
list of (region type, coord_list, meta) tuples | ||
""" | ||
coordsys = None | ||
regions = [] | ||
composite_region = None | ||
|
||
with open(filename,'r') as fh: | ||
for line_ in fh: | ||
# ds9 regions can be split on \n or ; | ||
for line in line_.split(";"): | ||
parsed = line_parser(line, coordsys) | ||
if parsed in coordinate_systems: | ||
coordsys = parsed | ||
elif parsed: | ||
region_type, coordlist, meta, composite = parsed | ||
if composite and composite_region is None: | ||
composite_region = [(region_type, coordlist)] | ||
elif composite: | ||
composite_region.append((region_type, coordlist)) | ||
elif composite_region is not None: | ||
composite_region.append((region_type, coordlist)) | ||
regions.append(composite_region) | ||
composite_region = None | ||
else: | ||
regions.append((region_type, coordlist, meta)) | ||
|
||
return regions | ||
|
||
|
||
def line_parser(line, coordsys=None): | ||
region_type_search = region_type_or_coordsys_re.search(line) | ||
if region_type_search: | ||
include = region_type_search.groups()[0] | ||
region_type = region_type_search.groups()[1] | ||
else: | ||
return | ||
|
||
if region_type in coordinate_systems: | ||
return region_type # outer loop has to do something with the coordinate system information | ||
elif region_type in language_spec: | ||
if coordsys is None: | ||
raise ValueError("No coordinate system specified and a region has been found.") | ||
|
||
if "||" in line: | ||
composite = True | ||
else: | ||
composite = False | ||
|
||
# end_of_region_name is the coordinate of the end of the region's name, e.g.: | ||
# circle would be 6 because circle is 6 characters | ||
end_of_region_name = region_type_search.span()[1] | ||
# coordinate of the # symbol or end of the line (-1) if not found | ||
hash_or_end = line.find("#") | ||
coords_etc = strip_paren(line[end_of_region_name:hash_or_end].strip(" |")) | ||
meta_str = line[hash_or_end:] | ||
|
||
parsed_meta = meta_parser(meta_str) | ||
|
||
if coordsys in coordsys_name_mapping: | ||
parsed = type_parser(coords_etc, language_spec[region_type], | ||
coordsys_name_mapping[coordsys]) | ||
|
||
# Reset iterator for ellipse annulus | ||
if region_type == 'ellipse': | ||
language_spec[region_type] = itertools.chain((coordinate, coordinate), itertools.cycle((radius, ))) | ||
|
||
coords = coordinates.SkyCoord([(x, y) | ||
for x, y in zip(parsed[:-1:2], parsed[1::2]) | ||
if isinstance(x, coordinates.Angle) and | ||
isinstance(x, coordinates.Angle)], frame=coordsys_name_mapping[coordsys]) | ||
|
||
return region_type, [coords] + parsed[len(coords)*2:], parsed_meta, composite | ||
else: | ||
parsed = type_parser(coords_etc, language_spec[region_type], | ||
coordsys) | ||
if region_type == 'polygon': | ||
# have to special-case polygon in the phys coord case b/c can't typecheck when iterating as in sky coord case | ||
coord = PixCoord(parsed[0::2], parsed[1::2]) | ||
parsed_return = [coord] | ||
else: | ||
parsed = [_.value for _ in parsed] | ||
coord = PixCoord(parsed[0], parsed[1]) | ||
parsed_return = [coord]+parsed[2:] | ||
|
||
# Reset iterator for ellipse annulus | ||
if region_type == 'ellipse': | ||
language_spec[region_type] = itertools.chain((coordinate, coordinate), itertools.cycle((radius, ))) | ||
|
||
return region_type, parsed_return, parsed_meta, composite | ||
|
||
|
||
def type_parser(string_rep, specification, coordsys): | ||
coord_list = [] | ||
splitter = re.compile("[, ]") | ||
for ii, (element, element_parser) in enumerate(zip(splitter.split(string_rep), specification)): | ||
if element_parser is coordinate: | ||
unit = coordinate_units[coordsys][ii % 2] | ||
coord_list.append(element_parser(element, unit)) | ||
else: | ||
coord_list.append(element_parser(element)) | ||
|
||
return coord_list | ||
|
||
|
||
# match an x=y pair (where y can be any set of characters) that may or may not | ||
# be followed by another one | ||
meta_token = re.compile("([a-zA-Z]+)(=)([^= ]+) ?") | ||
|
||
#meta_spec = {'color': color, | ||
# } | ||
# 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 | ||
# ruler(+175:07:14.900,+50:56:21.236,+175:06:52.643,+50:56:11.190) ruler=physical physical color=white font="helvetica 12 normal roman" text={Ruler} | ||
|
||
|
||
|
||
def meta_parser(meta_str): | ||
meta_token_split = [x for x in meta_token.split(meta_str.strip()) if x] | ||
equals_inds = [i for i, x in enumerate(meta_token_split) if x is '='] | ||
result = {meta_token_split[ii-1]: | ||
" ".join(meta_token_split[ii+1:jj-1 if jj is not None else None]) | ||
for ii,jj in zip(equals_inds, equals_inds[1:]+[None])} | ||
|
||
return result | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
import os | ||
|
||
def get_package_data(): | ||
parser_test = ['data/*.reg'] | ||
return {'regions.io.tests': parser_test} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
""" | ||
This packages contains affiliated package tests. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# 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 | ||
fk5 | ||
circle(13:29:52.675,+47:11:45.02,1") # color=blue | ||
circle(13:29:52.675,+47:11:45.02,2") # color=#800 | ||
circle(13:29:52.675,+47:11:45.02,3") # color=#0a0 | ||
circle(13:29:52.675,+47:11:45.02,4") # color=#880000 | ||
circle(13:29:52.675,+47:11:45.02,5") # color=#00aa00 | ||
circle(13:29:52.675,+47:11:45.02,6") # color=#888800000000 | ||
circle(13:29:52.675,+47:11:45.02,7") # color=#0000aaaa0000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Region file format: DS9 version 4.0 | ||
# | ||
# foobar | ||
# foobar+ | ||
# foo+bar | ||
## foobar | ||
### foobar | ||
#. foobar | ||
##. foobar | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# 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 | ||
fk5 | ||
# composite(202.48165,47.1931,317.39831) || composite=1 | ||
circle(202.48705,47.208237,3.9640007") || # color=pink width=3 font="times 10 normal roman" text={Circle} tag={foo} tag={foo bar} This is a Comment | ||
-ellipse(202.48309,47.204492,7.9280014",3.9640007",2.3983109) || # color=#0ff font="helvetica 10 normal italic" text={Ellipse} background | ||
-box(202.47783,47.201057,15.856003",7.9280014",2.3983109) || # color=yellow font="helvetica 10 bold roman" text={Box} | ||
polygon(202.47309,47.198922,202.46877,47.199044,202.46859,47.196108,202.47291,47.195985) || # font="courier 10 normal roman" text={Polygon} edit=0 rotate=0 | ||
-line(202.4684,47.194116,202.46202,47.193946) || # line=1 1 color=cyan text={Line} | ||
# vector(202.46159,47.191504,7.9280014",2.3983109) || vector=1 color=red text={Vector} | ||
# text(202.45907,47.188886) || color=magenta font="helvetica 14 bold roman" text={Region} | ||
# ruler(202.48176,47.194165,202.47471,47.194171) || ruler=physical physical color=white font="helvetica 12 normal roman" text={Ruler} | ||
annulus(202.49319,47.203697,1.9820003",3.9640007",5.946001") || # color=magenta font="helvetica 10 bold roman" text={Annulus} | ||
ellipse(202.48834,47.200368,2.9730005",1.4865003",5.946001",2.9730005",2.3983109) || # color=red width=3 font="helvetica 10 bold roman" text={Ellipse Annulus} | ||
box(202.4832,47.197085,7.9280014",3.9640007",11.892002",5.946001",2.3983109) || # font="helvetica 10 bold roman" text={Box Annulus} | ||
point(202.49871,47.200189) || # point=circle text={Circle Point} | ||
point(202.49351,47.196791) || # point=box color=red width=3 text={Box Point} | ||
point(202.48857,47.193854) || # point=diamond text={Diamond Point} | ||
point(202.5005,47.197643) || # point=cross color=blue text={Cross Point} | ||
point(202.49591,47.194852) || # point=x text={X Point} | ||
point(202.49148,47.191862) || # point=arrow color=magenta text={Arrow Point} | ||
point(202.49864,47.19267) || # point=boxcircle text={BoxCircle Point} | ||
# projection(202.47611,47.189884,202.46694,47.189092,3.9640007") || text={Projection} | ||
panda(202.48266,47.190165,317.39831,587.39831,3,0",5.946001",2) || # text={Panda} | ||
panda(202.48753,47.186403,8.9802109,47.398311,1,0",2.9730005",1) || # panda=(8.9802109 47.398311 137.39831 227.39831)(0" 2.9730005" 5.946001") text={Panda 2} | ||
panda(202.48753,47.186403,8.9802109,47.398311,1,2.9730005",5.946001",1) || # panda=ignore | ||
panda(202.48753,47.186403,47.398311,137.39831,1,0",2.9730005",1) || # panda=ignore | ||
panda(202.48753,47.186403,47.398311,137.39831,1,2.9730005",5.946001",1) || # panda=ignore | ||
panda(202.48753,47.186403,137.39831,227.39831,1,0",2.9730005",1) || # panda=ignore | ||
panda(202.48753,47.186403,137.39831,227.39831,1,2.9730005",5.946001",1) || # panda=ignore | ||
# compass(202.46768,47.186188,7.9280014") || compass=physical {N} {E} 1 1 text={Compass} | ||
epanda(202.47821,47.186785,317.39831,587.39831,3,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # text={Epanda} | ||
epanda(202.48291,47.183066,2.3983109,47.398311,1,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # epanda=(2.3983109 47.398311 137.39831 227.39831)(2.9730005" 1.4865003" 5.946001" 2.9730005")(2.3983109) text={Epanda 2} | ||
epanda(202.48291,47.183066,47.398311,137.39831,1,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # epanda=ignore | ||
epanda(202.48291,47.183066,137.39831,227.39831,1,2.9730005",1.4865003",5.946001",2.9730005",1,2.3983109) || # epanda=ignore | ||
bpanda(202.47302,47.183543,317.39831,587.39831,3,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) || # text={Bpanda} | ||
bpanda(202.47809,47.180126,2.3983109,47.398311,1,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) # bpanda=(2.3983109 47.398311 137.39831 227.39831)(7.9280014" 3.9640007" 11.892002" 5.946001")(2.3983109) text={Bpanda 2} | ||
bpanda(202.47809,47.180126,47.398311,137.39831,1,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) # bpanda=ignore | ||
bpanda(202.47809,47.180126,137.39831,227.39831,1,7.9280014",3.9640007",11.892002",5.946001",1,2.3983109) # bpanda=ignore |
Oops, something went wrong.