# This module lets one read circle, ellipse and box regions from a ds9
# region file. Beware that it assumes that the coordinate system of
# the central coordinates of each region is fk5.
import re
__all__ = ['Ds9Regions', 'Ds9Region', 'Ds9Circle', 'Ds9Ellipse', 'Ds9Box']
[docs]class Ds9Regions:
"""Parse circle, ellipse and box regions from a ds9 region file.
Only circle, ellipse, and box regions are recognized. Other types
of ds9 region and configuration parameters are simply ignored. The
center coordinates of each region are assumed to be in equatorial
coordinates and sizes are assumed to be angular sizes, not pixel
sizes.
The Ds9Regions object can be treated like an indexable tuple. For
example::
regions = ds9regions.Ds9Regions("myfile.reg")
# Indexing and len() are supported.
print("First region = ", regions[0])
print("Last region = ", regions[-1])
# Repeatable iteration like a tuple is supported:
for region in regions:
print(region)
Parameters
----------
regions : filename or iterable
Either a string that contains the name of a ds9 region file,
or an iterable that returns successive lines of a ds9 file.
"""
def __init__(self, regions):
# Start with an empty list of regions.
self._regions = []
# Have we been given the name of a ds9 region file, or an
# iterable.
if isinstance(regions, str):
lines = open(regions)
else:
lines = regions
# Parse the lines of the region file to build up the list of
# regions.
region = self._parse_regions(lines)
# Close the region file if we opened one.
if isinstance(regions, str):
lines.close()
def __str__(self):
s = ""
for region in self._regions:
s += "%s\n" % region.__str__()
return s
def __len__(self):
return len(self._regions)
def __getitem__(self, index):
return self._regions[index]
def _parse_regions(self, lines):
# Set the default coordinate system to match ds9's default.
system = "physical"
# Parse one line at a time.
for line_number, line in enumerate(lines):
try:
# No region has been parsed from this line yet.
region = None
# Each line can start with a sequence of configuration
# specifiers that are separated from each other by
# semicolons.
last = ""
for field in re.split(' *; *', line):
# Trim white-space and newlines.
field = field.strip()
# Check for a coordinate-system specifier.
if field.lower() in ["physical", "image", "fk4", "b1950", "fk5",
"j2000", "galactic", "ecliptic", "icrs",
"linear", "amplifier", "detector"]:
system = field.lower()
# Keep a record of the last non-empty field that we haven't
# already processed.
elif len(field) != 0:
last = field
# Discard all but the last unprocessed field.
line = last
# If the line is empty, then there is no region here.
if len(line) < 1:
continue
# Regions that start with a minus sign are to be excluded.
# If a minus sign is present, record this and remove it.
if line[0] == '-':
exclude = True
line = line[1:]
else:
exclude = False
# All lines that describe regions specify the shape followed by
# an open parenthesis
if re.match('[a-z]+\(', line) is None:
continue
# Split the line into its fields.
fields = re.split(' *[(),] *', line)
# All fields have a shape name, followed by the x and y axis
# coordinates of the center of the region.
shape = fields[0]
x = _parse_x(fields[1])
y = _parse_y(fields[2])
# Parse the distinct parameters of different shapes.
if shape == 'circle':
region = Ds9Circle(exclude, system, x, y,
radius=_parse_size(fields[3]))
elif shape == 'ellipse':
region = Ds9Ellipse(exclude, system, x, y,
width=_parse_size(fields[3]),
height=_parse_size(fields[4]),
pa=float(fields[5]))
elif shape == 'box':
region = Ds9Box(exclude, system, x, y,
width=_parse_size(fields[3]),
height=_parse_size(fields[4]),
pa=float(fields[5]))
if region is not None:
self._regions.append(region)
except ValueError as e:
raise ValueError("Error on line %d (%s)" % (line_number + 1, e.message))
[docs]class Ds9Region:
"""A generic Ds9Region.
Parameters
----------
shape : str
The name of the shape ("circle", "ellipse", or "box").
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x coordinate of the center of the shape (degrees).
y : float
The y coordinate of the center of the shape (degrees).
Attributes
----------
shape : str
The name of the shape ("circle", "ellipse", or "box").
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x coordinate of the center of the shape (degrees).
y : float
The y coordinate of the center of the shape (degrees).
"""
def __init__(self, shape, exclude, system, x, y):
self.shape = shape
self.exclude = exclude
self.system = system
self.x = x
self.y = y
def __str__(self):
return "%s %s %s x: %.6f y: %.6f" % ("exclude" if self.exclude else "include", self.system, self.shape, self.x, self.y)
[docs]class Ds9Circle(Ds9Region):
"""A circular ds9 region.
Parameters
----------
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x-axis coordinate of the center of the shape (degrees).
y : float
The declination of the center of the shape (degrees).
radius : float
The radius of the circle (degrees).
Attributes
----------
shape : str
The name of the shape ("circle", "ellipse", or "box").
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x coordinate of the center of the shape (degrees).
y : float
The y coordinate of the center of the shape (degrees).
radius : float
The radius of the circle (degrees).
"""
def __init__(self, exclude, system, x, y, radius):
Ds9Region.__init__(self, "circle", exclude, system, x, y)
self.radius = radius
def __str__(self):
return Ds9Region.__str__(self) + (" r: %g" % self.radius)
[docs]class Ds9Ellipse(Ds9Region):
"""An elliptical ds9 region.
Parameters
----------
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x-axis coordinate of the center of the shape (degrees).
y : float
The declination of the center of the shape (degrees).
width : float
The width (degrees) of the ellipse when the position angle is 0.
height : float
The height (degrees) of the ellipse when the position angle is 0.
pa : float
The position angle of the rectangle, east of celestial north for
sky coordinate systems.
Attributes
----------
shape : str
The name of the shape ("circle", "ellipse", or "box").
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x coordinate of the center of the shape (degrees).
y : float
The y coordinate of the center of the shape (degrees).
width : float
The width (degrees) of the ellipse when the position angle is 0.
height : float
The height (degrees) of the ellipse when the position angle is 0.
pa : float
The position angle of the rectangle, east of celestial north for
sky coordinate systems.
"""
def __init__(self, exclude, system, x, y, width, height, pa):
Ds9Region.__init__(self, "ellipse", exclude, system, x, y)
self.width = width
self.height = height
self.pa = pa
def __str__(self):
return Ds9Region.__str__(self) + (" w: %g h: %g PA: %g" % (self.width, self.height, self.pa))
[docs]class Ds9Box(Ds9Region):
"""A rectangular ds9 region.
Parameters
----------
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x-axis coordinate of the center of the shape (degrees).
y : float
The declination of the center of the shape (degrees).
width : float
The width (degrees) of the rectangle when the position angle is 0.
height : float
The height (degrees) of the rectangle when the position angle is 0.
pa : float
The position angle of the rectangle, east of celestial north for
sky coordinate systems.
Attributes
----------
shape : str
The name of the shape ("circle", "ellipse", or "box").
exclude : bool
True if the region is to be excluded.
system : str
The name of the coordinate system.
x : float
The x coordinate of the center of the shape (degrees).
y : float
The y coordinate of the center of the shape (degrees).
width : float
The width (degrees) of the rectangle when the position angle is 0.
height : float
The height (degrees) of the rectangle when the position angle is 0.
pa : float
The position angle of the rectangle, east of celestial north for
sky coordinate systems.
"""
def __init__(self, exclude, system, x, y, width, height, pa):
Ds9Region.__init__(self, "box", exclude, system, x, y)
self.width = width
self.height = height
self.pa = pa
def __str__(self):
return Ds9Region.__str__(self) + (" w: %g h: %g PA: %g" % (self.width, self.height, self.pa))
def _parse_x(s):
"""Parse an x-axis coordinate from a string.
This can be a simple number, or it can be a sexagesimal number of
hours, with up to three fields (hours, minutes, seconds) that are
separated by either colons or spaces. In the later case the
sexagesimal number is converted from sexagesimal hours to decimal
degrees.
"""
try:
return float(s)
except:
return _parse_sexagesimal(s) * 15.0
def _parse_y(s):
"""Parse a y-axis coordinate from a string.
This can be a simple number, or it can be a sexagesimal number of
degrees, with up to three fields (degrees, minutes, seconds) that
are separated by either colons or spaces. In the later case the
sexagesimal number is converted to decimal degrees.
"""
try:
return float(s)
except:
return _parse_sexagesimal(s)
def _parse_sexagesimal(s):
"""Parse a sexagesimal number from a string. This can have up
to numeric fields separated by either colons or spaces.
"""
# Get the sign of the number, and remove any sign character
# from the string.
if s[0] == '-' or s[0] == '+':
sign = -1.0 if s[0] == '-' else 1.0
s = s[1:]
else:
sign = 1.0
# Split the sexagesimal number into fields.
numbers = re.split(' *[ :] *', s)
# Complain if there are less than 1 or more than 3 numeric fields.
if len(numbers) < 1 or len(numbers) > 3:
raise ValueError("parse_sexagesimal expects from 1 to 3 numeric fields")
# Accumulate the angle in the units of the largest term.
angle = float(numbers[0])
scale = 1.0 / 60.0
for n in numbers[1:]:
angle += float(n) * scale
scale /= 60.0
# Scale the result by the sign.
return sign * angle
def _parse_size(s):
"""Parse a size from a string. This can be a simple number, or
an angle in arc-minutes followed by "'", or an angle in arc-seconds
followed by '"'. Arcseconds and arcminutes are converted to degrees.
"""
if s[-1] == "'": # Arcmin
return float(s[:-1]) / 60.0
elif s[-1] == '"': # Arcsec
return float(s[:-1]) / 3600.0
else:
return float(s)
# When this module is invoked as a standalone program, parse the
# contents of any region file that is specified on the command line
# and print out what is parsed from that file to stdout.
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
regs = Ds9Regions(sys.argv[1])
print(regs)
else:
print("Usage: %s <filename>" % sys.argv[0])
exit(1)