Coverage for python/lsst/pipe/tasks/objectMasks.py: 15%

112 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-20 09:49 +0000

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 

31class ObjectMaskCatalog: 

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.")