Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 

2# LSST Data Management System 

3# Copyright 2008-2013 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23""" 

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

25""" 

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

27 

28import math 

29import numpy 

30 

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

32from lsst.pipe.base import Task, Struct 

33from lsst.meas.astrom.sip import makeCreateWcsWithSip 

34from lsst.afw.math import Warper 

35 

36import lsst.geom as geom 

37import lsst.afw.table as afwTable 

38 

39 

40class RegisterConfig(Config): 

41 """Configuration for RegisterTask""" 

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

51 Task to register (align) multiple images. 

52 

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

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

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

56 """ 

57 ConfigClass = RegisterConfig 

58 

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

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

61 

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

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

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

65 

66 @param inputSources: Sources from input exposure 

67 @param inputWcs: Wcs of input exposure 

68 @param inputBBox: Bounding box of input exposure 

69 @param templateSources: Sources from template exposure 

70 @return Struct(matches: Matches between sources, 

71 wcs: Wcs for input in frame of template, 

72 ) 

73 """ 

74 matches = self.matchSources(inputSources, templateSources) 

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

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

77 

78 def matchSources(self, inputSources, templateSources): 

79 """Match sources between the input and template 

80 

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

82 fitting assumes a particular order). 

83 

84 @param inputSources: Source catalog of the input frame 

85 @param templateSources: Source of the target frame 

86 @return Match list 

87 """ 

88 matches = afwTable.matchRaDec(templateSources, inputSources, 

89 self.config.matchRadius*geom.arcseconds) 

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

91 self.metadata.set("MATCH_NUM", len(matches)) 

92 if len(matches) == 0: 92 ↛ 93line 92 didn't jump to line 93, because the condition on line 92 was never true

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

94 return matches 

95 

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

97 """Fit Wcs to matches 

98 

99 The fitting includes iterative sigma-clipping. 

100 

101 @param matches: List of matches (first is target, second is input) 

102 @param inputWcs: Original input Wcs 

103 @param inputBBox: Bounding box of input image 

104 @return Wcs 

105 """ 

106 copyMatches = type(matches)(matches) 

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

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

109 for i in range(self.config.sipIter): 109 ↛ 128line 109 didn't jump to line 128, because the loop on line 109 didn't complete

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

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

112 i, sipFit.getScatterInPixels()) 

113 wcs = sipFit.getNewWcs() 

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

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

116 m in copyMatches] 

117 dr = numpy.array(dr) 

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

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

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

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

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

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

124 if numBad == 0: 

125 break 

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

127 

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

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

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

131 self.metadata.set("SIP_RMS", sipFit.getScatterInPixels()) 

132 self.metadata.set("SIP_GOOD", len(copyMatches)) 

133 self.metadata.set("SIP_REJECTED", len(matches) - len(copyMatches)) 

134 wcs = sipFit.getNewWcs() 

135 return wcs 

136 

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

138 """Warp input exposure to template frame 

139 

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

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

142 frame. 

143 

144 @param inputExp: Input exposure, to be warped 

145 @param newWcs: Revised Wcs for input exposure 

146 @param templateWcs: Target Wcs 

147 @param templateBBox: Target bounding box 

148 @return Warped exposure 

149 """ 

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

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

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

153 return alignedExp 

154 

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

156 """Warp sources to the new frame 

157 

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

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

160 pixel coordinates. 

161 

162 @param inputSources: Sources on input exposure, to be warped 

163 @param newWcs: Revised Wcs for input exposure 

164 @param templateWcs: Target Wcs 

165 @param templateBBox: Target bounding box 

166 @return Warped sources 

167 """ 

168 alignedSources = inputSources.copy(True) 

169 if not isinstance(templateBBox, geom.Box2D): 169 ↛ 172line 169 didn't jump to line 172, because the condition on line 169 was never false

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

171 templateBBox = geom.Box2D(templateBBox) 

172 table = alignedSources.getTable() 

173 coordKey = table.getCoordKey() 

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

175 deleteList = [] 

176 for i, s in enumerate(alignedSources): 

177 oldCentroid = s.get(centroidKey) 

178 newCoord = newWcs.pixelToSky(oldCentroid) 

179 newCentroid = templateWcs.skyToPixel(newCoord) 

180 if not templateBBox.contains(newCentroid): 180 ↛ 181line 180 didn't jump to line 181, because the condition on line 180 was never true

181 deleteList.append(i) 

182 continue 

183 s.set(coordKey, newCoord) 

184 s.set(centroidKey, newCentroid) 

185 

186 for i in reversed(deleteList): # Delete from back so we don't change indices 186 ↛ 187line 186 didn't jump to line 187, because the loop on line 186 never started

187 del alignedSources[i] 

188 

189 return alignedSources