lsst.pipe.tasks g6a99470703+f1f46aed25
objectMasks.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22import re
23import os.path
24import logging
25import lsst.daf.base as dafBase
26import lsst.geom as geom
27import lsst.afw.table as afwTable
28from lsst.daf.butler.formatters.file import FileFormatter
29
30
32 """Class to support bright object masks
33 """
34
35 def __init__(self):
36 schema = afwTable.SimpleTable.makeMinimalSchema()
37 schema.addField("type", str, "type of region (e.g. box, circle)", size=10)
38 schema.addField("radius", "Angle", "radius of mask (if type == circle")
39 schema.addField("height", "Angle", "height of mask (if type == box)")
40 schema.addField("width", "Angle", "width of mask (if type == box)")
41 schema.addField("angle", "Angle", "rotation of mask (if type == box)")
42 schema.addField("mag", float, "object's magnitude")
43
44 self._catalog = afwTable.SimpleCatalog(schema)
45 self._catalog.table.setMetadata(dafBase.PropertyList())
46
47 self.table = self._catalog.table
48 self.addNew = self._catalog.addNew
49
50 def __len__(self):
51 return len(self._catalog)
52
53 def __iter__(self):
54 return iter(self._catalog)
55
56 def __getitem__(self, i):
57 return self._catalog.__getitem__(i)
58
59 def __setitem__(self, i, v):
60 return self._catalog.__setitem__(i, v)
61
62 @classmethod
63 def read(cls, fileName):
64 """Read a ds9 region file, returning a ObjectMaskCatalog object
65
66 The files should be structured as follows:
67
68 # Description of catalogue as a comment
69 # CATALOG: catalog-id-string
70 # TRACT: 0
71 # PATCH: 5,4
72 # FILTER: HSC-I
73
74 wcs; fk5
75
76 circle(RA, DEC, RADIUS) # ID: 1, mag: 12.34
77 box(RA, DEC, XSIZE, YSIZE, THETA) # ID: 2, mag: 23.45
78 ...
79
80 The ", mag: XX.YY" is optional
81
82 The commented lines must be present, with the relevant fields such as
83 tract patch and filter filled in. The coordinate system must be listed
84 as above. Each patch is specified as a box or circle, with RA, DEC,
85 and dimensions specified in decimal degrees (with or without an
86 explicit "d").
87
88 Only (axis-aligned) boxes and circles are currently supported as
89 region definitions.
90 """
91
92 log = logging.getLogger("lsst.ObjectMaskCatalog")
93
94 brightObjects = cls()
95 checkedWcsIsFk5 = False
96 NaN = float("NaN")*geom.degrees
97
98 nFormatError = 0 # number of format errors seen
99 with open(fileName) as fd:
100 for lineNo, line in enumerate(fd.readlines(), 1):
101 line = line.rstrip()
102
103 if re.search(r"^\s*#", line):
104 #
105 # Parse any line of the form "# key : value" and put them into the metadata.
106 #
107 # The medatdata values must be defined as outlined in the above docstring
108 #
109 # The value of these three keys will be checked,
110 # so get them right!
111 #
112 mat = re.search(r"^\s*#\s*([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)", line)
113 if mat:
114 key, value = mat.group(1).lower(), mat.group(2)
115 if key == "tract":
116 value = int(value)
117
118 brightObjects.table.getMetadata().set(key, value)
119
120 line = re.sub(r"^\s*#.*", "", line)
121 if not line:
122 continue
123
124 if re.search(r"^\s*wcs\s*;\s*fk5\s*$", line, re.IGNORECASE):
125 checkedWcsIsFk5 = True
126 continue
127
128 # This regular expression parses the regions file for each region to be masked,
129 # with the format as specified in the above docstring.
130 mat = re.search(r"^\s*(box|circle)"
131 r"(?:\s+|\s*\‍(\s*)" # open paren or space
132 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # ra + units
133 r"(?:\s+|\s*,\s*)" # sep
134 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # dec + units
135 r"(?:\s+|\s*,\s*)" # sep
136 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param1 + units
137 r"(?:" # start optional 1
138 r"(?:\s+|\s*,\s*)" # sep
139 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param2 + units
140 r"(?:" # start optional 2
141 r"(?:\s+|\s*,\s*)" # sep
142 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param3 + units
143 ")?" # end optional 2
144 ")?" # end optional 1
145 r"(?:\s*|\s*\‍)\s*)" # close paren or space
146 r"#\s*ID:[\w\s]*(\d+)" # start comment, ID
147 r"(?:\s*,?\s*mag:\s*([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?))?"
148 r"\s*$", line)
149 if mat:
150 _type, ra, raUnit, dec, decUnit, \
151 param1, param1Unit, param2, param2Unit, param3, param3Unit, \
152 _id, mag = mat.groups()
153
154 _id = int(_id)
155 if mag is None:
156 mag = NaN
157 else:
158 mag = float(mag)
159
160 ra = convertToAngle(ra, raUnit, "ra", fileName, lineNo)
161 dec = convertToAngle(dec, decUnit, "dec", fileName, lineNo)
162
163 radius = NaN
164 width = NaN
165 height = NaN
166 angle = 0.0*geom.degrees
167
168 if _type == "box":
169 width = convertToAngle(param1, param1Unit, "width", fileName, lineNo)
170 height = convertToAngle(param2, param2Unit, "height", fileName, lineNo)
171 if param3 is not None:
172 angle = convertToAngle(param3, param3Unit, "angle", fileName, lineNo)
173
174 if angle != 0.0:
175 log.warning("Rotated boxes are not supported: \"%s\" at %s:%d",
176 line, fileName, lineNo)
177 nFormatError += 1
178 elif _type == "circle":
179 radius = convertToAngle(param1, param1Unit, "radius", fileName, lineNo)
180
181 if not (param2 is None and param3 is None):
182 log.warning("Extra parameters for circle: \"%s\" at %s:%d",
183 line, fileName, lineNo)
184 nFormatError += 1
185
186 rec = brightObjects.addNew()
187 # N.b. rec["coord"] = Coord is not supported, so we have to use the setter
188 rec["type"] = _type
189 rec["id"] = _id
190 rec["mag"] = mag
191 rec.setCoord(geom.SpherePoint(ra, dec))
192
193 rec["angle"] = angle
194 rec["height"] = height
195 rec["width"] = width
196 rec["radius"] = radius
197 else:
198 log.warning("Unexpected line \"%s\" at %s:%d", line, fileName, lineNo)
199 nFormatError += 1
200
201 if nFormatError > 0:
202 raise RuntimeError("Saw %d formatting errors in %s" % (nFormatError, fileName))
203
204 if not checkedWcsIsFk5:
205 raise RuntimeError("Expected to see a line specifying an fk5 wcs in %s" % fileName)
206
207 # This makes the deep copy contiguous in memory so that a ColumnView can be exposed to Numpy
208 brightObjects._catalog = brightObjects._catalog.copy(True)
209
210 return brightObjects
211
212
213def convertToAngle(var, varUnit, what, fileName, lineNo):
214 """Given a variable and its units, return an geom.Angle
215
216 what, fileName, and lineNo are used to generate helpful error messages
217 """
218 var = float(var)
219
220 if varUnit in ("d", "", None):
221 pass
222 elif varUnit == "'":
223 var /= 60.0
224 elif varUnit == '"':
225 var /= 3600.0
226 else:
227 raise RuntimeError("unsupported unit \"%s\" for %s at %s:%d" %
228 (varUnit, what, fileName, lineNo))
229
230 return var*geom.degrees
231
232
233class RegionFileFormatter(FileFormatter):
234 """Plugin for reading DS9 region file catalogs with Gen3 Butler.
235 """
236 extension = ".reg"
237
238 def _readFile(self, path, pytype):
239 # Docstring inherited from FileFormatter._readFile
240 if not os.path.exists(path):
241 return None
242
243 return pytype.read(path)
244
245 def _writeFile(self, inMemoryDataset, fileDescriptor):
246 # Docstring inherited from FileFormatter._writeFile
247 raise NotImplementedError("Write not implemented.")
def convertToAngle(var, varUnit, what, fileName, lineNo)
Definition: objectMasks.py:213