Coverage for python/lsst/pipe/tasks/registerImage.py: 29%

80 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-12 21:44 -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""" 

23This module contains a Task to register (align) multiple images. 

24""" 

25__all__ = ["RegisterTask", "RegisterConfig"] 

26 

27import math 

28import numpy 

29 

30from lsst.pex.config import Config, Field, ConfigField 

31from lsst.pipe.base import Task, Struct 

32from lsst.meas.astrom.sip import makeCreateWcsWithSip 

33from lsst.afw.math import Warper 

34 

35import lsst.geom as geom 

36import lsst.afw.table as afwTable 

37 

38 

39class RegisterConfig(Config): 

40 """Configuration for RegisterTask.""" 

41 

42 matchRadius = Field(dtype=float, default=1.0, doc="Matching radius (arcsec)", check=lambda x: x > 0) 

43 sipOrder = Field(dtype=int, default=4, doc="Order for SIP WCS", check=lambda x: x > 1) 

44 sipIter = Field(dtype=int, default=3, doc="Rejection iterations for SIP WCS", check=lambda x: x > 0) 

45 sipRej = Field(dtype=float, default=3.0, doc="Rejection threshold for SIP WCS", check=lambda x: x > 0) 

46 warper = ConfigField(dtype=Warper.ConfigClass, doc="Configuration for warping") 

47 

48 

49class RegisterTask(Task): 

50 """Task to register (align) multiple images. 

51 

52 The 'run' method provides a revised Wcs from matches and fitting sources. 

53 Additional methods are provided as a convenience to warp an exposure 

54 ('warpExposure') and sources ('warpSources') with the new Wcs. 

55 """ 

56 

57 ConfigClass = RegisterConfig 

58 

59 def run(self, inputSources, inputWcs, inputBBox, templateSources): 

60 """Register (align) an input exposure to the template 

61 The sources must have RA,Dec set, and accurate to within the 

62 'matchRadius' of the configuration in order to facilitate source 

63 matching. We fit a new Wcs, but do NOT set it in the input exposure. 

64 

65 Parameters 

66 ---------- 

67 inputSources : `lsst.afw.table.SourceCatalog` 

68 Sources from input exposure. 

69 inputWcs : `lsst.afw.geom.SkyWcs` 

70 Wcs of input exposure. 

71 inputBBox : `lsst.geom.Box` 

72 Bounding box of input exposure. 

73 templateSources : `lsst.afw.table.SourceCatalog` 

74 Sources from template exposure. 

75 

76 Returns 

77 ------- 

78 result : `lsst.pipe.base.Struct` 

79 Results as a struct with attributes: 

80 

81 ``matches`` 

82 Matches between sources (`list`). 

83 ``wcs`` 

84 Wcs for input in frame of template (`lsst.afw.geom.SkyWcs`). 

85 """ 

86 matches = self.matchSources(inputSources, templateSources) 

87 wcs = self.fitWcs(matches, inputWcs, inputBBox) 

88 return Struct(matches=matches, wcs=wcs) 

89 

90 def matchSources(self, inputSources, templateSources): 

91 """Match sources between the input and template. 

92 

93 The order of the input arguments matters (because the later Wcs 

94 fitting assumes a particular order). 

95 

96 Parameters 

97 ---------- 

98 inputSources : `lsst.afw.table.SourceCatalog` 

99 Source catalog of the input frame. 

100 templateSources : `lsst.afw.table.SourceCatalog` 

101 Source of the target frame. 

102 

103 Returns 

104 ------- 

105 matches: `list` 

106 Match list. 

107 """ 

108 matches = afwTable.matchRaDec(templateSources, inputSources, 

109 self.config.matchRadius*geom.arcseconds) 

110 self.log.info("Matching within %.1f arcsec: %d matches", self.config.matchRadius, len(matches)) 

111 self.metadata["MATCH_NUM"] = len(matches) 

112 if len(matches) == 0: 

113 raise RuntimeError("Unable to match source catalogs") 

114 return matches 

115 

116 def fitWcs(self, matches, inputWcs, inputBBox): 

117 """Fit Wcs to matches. 

118 

119 The fitting includes iterative sigma-clipping. 

120 

121 Parameters 

122 ---------- 

123 matches : `list` 

124 List of matches (first is target, second is input). 

125 inputWcs : `lsst.afw.geom.SkyWcs` 

126 Original input Wcs. 

127 inputBBox : `lsst.geom.Box` 

128 Bounding box of input exposure. 

129 

130 Returns 

131 ------- 

132 wcs: `lsst.afw.geom.SkyWcs` 

133 Wcs fitted to matches. 

134 """ 

135 copyMatches = type(matches)(matches) 

136 refCoordKey = copyMatches[0].first.getTable().getCoordKey() 

137 inCentroidKey = copyMatches[0].second.getTable().getCentroidSlot().getMeasKey() 

138 for i in range(self.config.sipIter): 

139 sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox) 

140 self.log.debug("Registration WCS RMS iteration %d: %f pixels", 

141 i, sipFit.getScatterInPixels()) 

142 wcs = sipFit.getNewWcs() 

143 dr = [m.first.get(refCoordKey).separation( 

144 wcs.pixelToSky(m.second.get(inCentroidKey))).asArcseconds() for 

145 m in copyMatches] 

146 dr = numpy.array(dr) 

147 rms = math.sqrt((dr*dr).mean()) # RMS from zero 

148 rms = max(rms, 1.0e-9) # Don't believe any RMS smaller than this 

149 self.log.debug("Registration iteration %d: rms=%f", i, rms) 

150 good = numpy.where(dr < self.config.sipRej*rms)[0] 

151 numBad = len(copyMatches) - len(good) 

152 self.log.debug("Registration iteration %d: rejected %d", i, numBad) 

153 if numBad == 0: 

154 break 

155 copyMatches = type(matches)(copyMatches[i] for i in good) 

156 

157 sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox) 

158 self.log.info("Registration WCS: final WCS RMS=%f pixels from %d matches", 

159 sipFit.getScatterInPixels(), len(copyMatches)) 

160 self.metadata["SIP_RMS"] = sipFit.getScatterInPixels() 

161 self.metadata["SIP_GOOD"] = len(copyMatches) 

162 self.metadata["SIP_REJECTED"] = len(matches) - len(copyMatches) 

163 wcs = sipFit.getNewWcs() 

164 return wcs 

165 

166 def warpExposure(self, inputExp, newWcs, templateWcs, templateBBox): 

167 """Warp input exposure to template frame. 

168 

169 There are a variety of data attached to the exposure (e.g., PSF, PhotoCalib 

170 and other metadata), but we do not attempt to warp these to the template 

171 frame. 

172 

173 Parameters 

174 ---------- 

175 inputExp : `lsst.afw.image.Exposure` 

176 Input exposure, to be warped. 

177 newWcs : `lsst.afw.geom.SkyWcs` 

178 Revised Wcs for input exposure. 

179 templateWcs : `lsst.afw.geom.SkyWcs` 

180 Target Wcs. 

181 templateBBox : `lsst.geom.Box` 

182 Target bounding box. 

183 

184 Returns 

185 ------- 

186 alignedExp : `lsst.afw.image.Exposure` 

187 Warped exposure. 

188 """ 

189 warper = Warper.fromConfig(self.config.warper) 

190 copyExp = inputExp.Factory(inputExp.getMaskedImage(), newWcs) 

191 alignedExp = warper.warpExposure(templateWcs, copyExp, destBBox=templateBBox) 

192 return alignedExp 

193 

194 def warpSources(self, inputSources, newWcs, templateWcs, templateBBox): 

195 """Warp sources to the new frame. 

196 

197 It would be difficult to transform all possible quantities of potential 

198 interest between the two frames. We therefore update only the sky and 

199 pixel coordinates. 

200 

201 Parameters 

202 ---------- 

203 inputSources : `lsst.afw.table.SourceCatalog` 

204 Sources on input exposure, to be warped. 

205 newWcs : `lsst.afw.geom.SkyWcs` 

206 Revised Wcs for input exposure. 

207 templateWcs : `lsst.afw.geom.SkyWcs` 

208 Target Wcs. 

209 templateBBox : `lsst.geom.Box` 

210 Target bounding box. 

211 

212 Returns 

213 ------- 

214 alignedSources : `lsst.afw.table.SourceCatalog` 

215 Warped sources. 

216 """ 

217 alignedSources = inputSources.copy(True) 

218 if not isinstance(templateBBox, geom.Box2D): 

219 # There is no method Box2I::contains(Point2D) 

220 templateBBox = geom.Box2D(templateBBox) 

221 table = alignedSources.getTable() 

222 coordKey = table.getCoordKey() 

223 centroidKey = table.getCentroidSlot().getMeasKey() 

224 deleteList = [] 

225 for i, s in enumerate(alignedSources): 

226 oldCentroid = s.get(centroidKey) 

227 newCoord = newWcs.pixelToSky(oldCentroid) 

228 newCentroid = templateWcs.skyToPixel(newCoord) 

229 if not templateBBox.contains(newCentroid): 

230 deleteList.append(i) 

231 continue 

232 s.set(coordKey, newCoord) 

233 s.set(centroidKey, newCentroid) 

234 

235 for i in reversed(deleteList): # Delete from back so we don't change indices 

236 del alignedSources[i] 

237 

238 return alignedSources