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

113 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-27 03:35 -0700

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# (https://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 <https://www.gnu.org/licenses/>. 

21 

22__all__ = ["ObjectMaskCatalog", "RegionFileFormatter"] 

23 

24import re 

25import os.path 

26import logging 

27import lsst.daf.base as dafBase 

28import lsst.geom as geom 

29import lsst.afw.table as afwTable 

30from lsst.daf.butler.formatters.file import FileFormatter 

31 

32 

33class ObjectMaskCatalog: 

34 """Class to support bright object masks 

35 """ 

36 

37 def __init__(self): 

38 schema = afwTable.SimpleTable.makeMinimalSchema() 

39 schema.addField("type", str, "type of region (e.g. box, circle)", size=10) 

40 schema.addField("radius", "Angle", "radius of mask (if type == circle") 

41 schema.addField("height", "Angle", "height of mask (if type == box)") 

42 schema.addField("width", "Angle", "width of mask (if type == box)") 

43 schema.addField("angle", "Angle", "rotation of mask (if type == box)") 

44 schema.addField("mag", float, "object's magnitude") 

45 

46 self._catalog = afwTable.SimpleCatalog(schema) 

47 self._catalog.table.setMetadata(dafBase.PropertyList()) 

48 

49 self.table = self._catalog.table 

50 self.addNew = self._catalog.addNew 

51 

52 def __len__(self): 

53 return len(self._catalog) 

54 

55 def __iter__(self): 

56 return iter(self._catalog) 

57 

58 def __getitem__(self, i): 

59 return self._catalog.__getitem__(i) 

60 

61 def __setitem__(self, i, v): 

62 return self._catalog.__setitem__(i, v) 

63 

64 @classmethod 

65 def read(cls, fileName): 

66 """Read a ds9 region file, returning a ObjectMaskCatalog object 

67 

68 The files should be structured as follows: 

69 

70 # Description of catalogue as a comment 

71 # CATALOG: catalog-id-string 

72 # TRACT: 0 

73 # PATCH: 5,4 

74 # FILTER: HSC-I 

75 

76 wcs; fk5 

77 

78 circle(RA, DEC, RADIUS) # ID: 1, mag: 12.34 

79 box(RA, DEC, XSIZE, YSIZE, THETA) # ID: 2, mag: 23.45 

80 ... 

81 

82 The ", mag: XX.YY" is optional 

83 

84 The commented lines must be present, with the relevant fields such as 

85 tract patch and filter filled in. The coordinate system must be listed 

86 as above. Each patch is specified as a box or circle, with RA, DEC, 

87 and dimensions specified in decimal degrees (with or without an 

88 explicit "d"). 

89 

90 Only (axis-aligned) boxes and circles are currently supported as 

91 region definitions. 

92 """ 

93 

94 log = logging.getLogger("lsst.ObjectMaskCatalog") 

95 

96 brightObjects = cls() 

97 checkedWcsIsFk5 = False 

98 NaN = float("NaN")*geom.degrees 

99 

100 nFormatError = 0 # number of format errors seen 

101 with open(fileName) as fd: 

102 for lineNo, line in enumerate(fd.readlines(), 1): 

103 line = line.rstrip() 

104 

105 if re.search(r"^\s*#", line): 

106 # 

107 # Parse any line of the form "# key : value" and put them into the metadata. 

108 # 

109 # The medatdata values must be defined as outlined in the above docstring 

110 # 

111 # The value of these three keys will be checked, 

112 # so get them right! 

113 # 

114 mat = re.search(r"^\s*#\s*([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)", line) 

115 if mat: 

116 key, value = mat.group(1).lower(), mat.group(2) 

117 if key == "tract": 

118 value = int(value) 

119 

120 brightObjects.table.getMetadata().set(key, value) 

121 

122 line = re.sub(r"^\s*#.*", "", line) 

123 if not line: 

124 continue 

125 

126 if re.search(r"^\s*wcs\s*;\s*fk5\s*$", line, re.IGNORECASE): 

127 checkedWcsIsFk5 = True 

128 continue 

129 

130 # This regular expression parses the regions file for each region to be masked, 

131 # with the format as specified in the above docstring. 

132 mat = re.search(r"^\s*(box|circle)" 

133 r"(?:\s+|\s*\(\s*)" # open paren or space 

134 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # ra + units 

135 r"(?:\s+|\s*,\s*)" # sep 

136 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # dec + units 

137 r"(?:\s+|\s*,\s*)" # sep 

138 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param1 + units 

139 r"(?:" # start optional 1 

140 r"(?:\s+|\s*,\s*)" # sep 

141 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param2 + units 

142 r"(?:" # start optional 2 

143 r"(?:\s+|\s*,\s*)" # sep 

144 r"([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)([d]*)" # param3 + units 

145 ")?" # end optional 2 

146 ")?" # end optional 1 

147 r"(?:\s*|\s*\)\s*)" # close paren or space 

148 r"#\s*ID:[\w\s]*(\d+)" # start comment, ID 

149 r"(?:\s*,?\s*mag:\s*([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?))?" 

150 r"\s*$", line) 

151 if mat: 

152 _type, ra, raUnit, dec, decUnit, \ 

153 param1, param1Unit, param2, param2Unit, param3, param3Unit, \ 

154 _id, mag = mat.groups() 

155 

156 _id = int(_id) 

157 if mag is None: 

158 mag = NaN 

159 else: 

160 mag = float(mag) 

161 

162 ra = convertToAngle(ra, raUnit, "ra", fileName, lineNo) 

163 dec = convertToAngle(dec, decUnit, "dec", fileName, lineNo) 

164 

165 radius = NaN 

166 width = NaN 

167 height = NaN 

168 angle = 0.0*geom.degrees 

169 

170 if _type == "box": 

171 width = convertToAngle(param1, param1Unit, "width", fileName, lineNo) 

172 height = convertToAngle(param2, param2Unit, "height", fileName, lineNo) 

173 if param3 is not None: 

174 angle = convertToAngle(param3, param3Unit, "angle", fileName, lineNo) 

175 

176 if angle != 0.0: 

177 log.warning("Rotated boxes are not supported: \"%s\" at %s:%d", 

178 line, fileName, lineNo) 

179 nFormatError += 1 

180 elif _type == "circle": 

181 radius = convertToAngle(param1, param1Unit, "radius", fileName, lineNo) 

182 

183 if not (param2 is None and param3 is None): 

184 log.warning("Extra parameters for circle: \"%s\" at %s:%d", 

185 line, fileName, lineNo) 

186 nFormatError += 1 

187 

188 rec = brightObjects.addNew() 

189 # N.b. rec["coord"] = Coord is not supported, so we have to use the setter 

190 rec["type"] = _type 

191 rec["id"] = _id 

192 rec["mag"] = mag 

193 rec.setCoord(geom.SpherePoint(ra, dec)) 

194 

195 rec["angle"] = angle 

196 rec["height"] = height 

197 rec["width"] = width 

198 rec["radius"] = radius 

199 else: 

200 log.warning("Unexpected line \"%s\" at %s:%d", line, fileName, lineNo) 

201 nFormatError += 1 

202 

203 if nFormatError > 0: 

204 raise RuntimeError("Saw %d formatting errors in %s" % (nFormatError, fileName)) 

205 

206 if not checkedWcsIsFk5: 

207 raise RuntimeError("Expected to see a line specifying an fk5 wcs in %s" % fileName) 

208 

209 # This makes the deep copy contiguous in memory so that a ColumnView can be exposed to Numpy 

210 brightObjects._catalog = brightObjects._catalog.copy(True) 

211 

212 return brightObjects 

213 

214 

215def convertToAngle(var, varUnit, what, fileName, lineNo): 

216 """Given a variable and its units, return an geom.Angle 

217 

218 what, fileName, and lineNo are used to generate helpful error messages 

219 """ 

220 var = float(var) 

221 

222 if varUnit in ("d", "", None): 

223 pass 

224 elif varUnit == "'": 

225 var /= 60.0 

226 elif varUnit == '"': 

227 var /= 3600.0 

228 else: 

229 raise RuntimeError("unsupported unit \"%s\" for %s at %s:%d" % 

230 (varUnit, what, fileName, lineNo)) 

231 

232 return var*geom.degrees 

233 

234 

235class RegionFileFormatter(FileFormatter): 

236 """Plugin for reading DS9 region file catalogs with Gen3 Butler. 

237 """ 

238 extension = ".reg" 

239 

240 def _readFile(self, path, pytype): 

241 # Docstring inherited from FileFormatter._readFile 

242 if not os.path.exists(path): 

243 return None 

244 

245 return pytype.read(path) 

246 

247 def _writeFile(self, inMemoryDataset, fileDescriptor): 

248 # Docstring inherited from FileFormatter._writeFile 

249 raise NotImplementedError("Write not implemented.")