Coverage for python/lsst/pipe/tasks/visualizeVisit.py: 37%

69 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-19 05:09 -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__ = [ 

23 "VisualizeBinExpConfig", 

24 "VisualizeBinExpTask", 

25 "VisualizeMosaicExpConfig", 

26 "VisualizeMosaicExpTask", 

27] 

28 

29import lsst.afw.cameraGeom.utils as afwUtils 

30import lsst.afw.image as afwImage 

31import lsst.afw.math as afwMath 

32import lsst.pex.config as pexConfig 

33import lsst.pipe.base as pipeBase 

34import lsst.pipe.base.connectionTypes as cT 

35import numpy as np 

36 

37 

38class VisualizeBinExpConnections(pipeBase.PipelineTaskConnections, dimensions=("instrument", "detector")): 

39 inputExp = cT.Input( 

40 name="calexp", 

41 doc="Input exposure data to mosaic.", 

42 storageClass="ExposureF", 

43 dimensions=("instrument", "detector"), 

44 ) 

45 camera = cT.PrerequisiteInput( 

46 name="camera", 

47 doc="Input camera to use for mosaic geometry.", 

48 storageClass="Camera", 

49 dimensions=("instrument",), 

50 isCalibration=True, 

51 ) 

52 

53 outputExp = cT.Output( 

54 name="calexpBin", 

55 doc="Output binned image.", 

56 storageClass="ExposureF", 

57 dimensions=("instrument", "detector"), 

58 ) 

59 

60 

61class VisualizeBinExpConfig(pipeBase.PipelineTaskConfig, pipelineConnections=VisualizeBinExpConnections): 

62 """Configuration for focal plane visualization.""" 

63 

64 binning = pexConfig.Field( 

65 dtype=int, 

66 default=8, 

67 doc="Binning factor to apply to each input exposure's image data.", 

68 ) 

69 detectorKeyword = pexConfig.Field( 

70 dtype=str, 

71 default="DET-ID", 

72 doc="Metadata keyword to use to find detector if not available from input.", 

73 ) 

74 

75 

76class VisualizeBinExpTask(pipeBase.PipelineTask): 

77 """Bin the detectors of an exposure. 

78 

79 The outputs of this task should be passed to 

80 VisualizeMosaicExpTask to be mosaicked into a full focal plane 

81 visualization image. 

82 """ 

83 

84 ConfigClass = VisualizeBinExpConfig 

85 _DefaultName = "VisualizeBinExp" 

86 

87 def run(self, inputExp, camera): 

88 """Bin input image, attach associated detector. 

89 

90 Parameters 

91 ---------- 

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

93 Input exposure data to bin. 

94 camera : `lsst.afw.cameraGeom.Camera` 

95 Input camera to use for mosaic geometry. 

96 

97 Returns 

98 ------- 

99 output : `lsst.pipe.base.Struct` 

100 Results struct with attribute: 

101 

102 ``outputExp`` 

103 Binned version of input image (`lsst.afw.image.Exposure`). 

104 """ 

105 if inputExp.getDetector() is None: 

106 detectorId = inputExp.getMetadata().get(self.config.detectorKeyword) 

107 if detectorId is not None: 

108 inputExp.setDetector(camera[detectorId]) 

109 

110 binned = inputExp.getMaskedImage() 

111 binned = afwMath.binImage(binned, self.config.binning) 

112 outputExp = afwImage.makeExposure(binned) 

113 

114 outputExp.setInfo(inputExp.getInfo()) 

115 

116 return pipeBase.Struct(outputExp=outputExp) 

117 

118 

119class VisualizeMosaicExpConnections(pipeBase.PipelineTaskConnections, dimensions=("instrument",)): 

120 inputExps = cT.Input( 

121 name="calexpBin", 

122 doc="Input binned images mosaic.", 

123 storageClass="ExposureF", 

124 dimensions=("instrument", "detector"), 

125 multiple=True, 

126 ) 

127 camera = cT.PrerequisiteInput( 

128 name="camera", 

129 doc="Input camera to use for mosaic geometry.", 

130 storageClass="Camera", 

131 dimensions=("instrument",), 

132 isCalibration=True, 

133 ) 

134 

135 outputData = cT.Output( 

136 name="calexpFocalPlane", 

137 doc="Output binned mosaicked frame.", 

138 storageClass="ImageF", 

139 dimensions=("instrument",), 

140 ) 

141 

142 

143class VisualizeMosaicExpConfig( 

144 pipeBase.PipelineTaskConfig, pipelineConnections=VisualizeMosaicExpConnections 

145): 

146 """Configuration for focal plane visualization.""" 

147 

148 binning = pexConfig.Field( 

149 dtype=int, 

150 default=8, 

151 doc="Binning factor previously applied to input exposures.", 

152 ) 

153 

154 

155class VisualizeMosaicExpTask(pipeBase.PipelineTask): 

156 """Task to mosaic binned products. 

157 

158 The config.binning parameter must match that used in the 

159 VisualizeBinExpTask. Otherwise there will be a mismatch between 

160 the input image size and the expected size of that image in the 

161 full focal plane frame. 

162 """ 

163 

164 ConfigClass = VisualizeMosaicExpConfig 

165 _DefaultName = "VisualizeMosaicExp" 

166 

167 def makeCameraImage(self, inputExps, camera, binning): 

168 """Make an image of an entire focal plane. 

169 

170 Parameters 

171 ---------- 

172 exposures: `dict` [`int`, `lsst.afw.image.Exposure`] 

173 CCD exposures, binned by `binning`. The keys are the 

174 detectorIDs, with the values the binned image exposure. 

175 

176 Returns 

177 ------- 

178 image : `lsst.afw.image.Image` 

179 Image mosaicked from the individual binned images for each 

180 detector. 

181 """ 

182 image = afwUtils.makeImageFromCamera( 

183 camera, imageSource=ImageSource(inputExps), imageFactory=afwImage.ImageF, binSize=binning 

184 ) 

185 return image 

186 

187 def run(self, inputExps, camera, inputIds=None): 

188 """Mosaic inputs together to create focal plane image. 

189 

190 Parameters 

191 ---------- 

192 inputExps : `list` [`lsst.afw.image.Exposure`] 

193 Input exposure data to bin. 

194 camera : `lsst.afw.cameraGeom.Camera` 

195 Input camera to use for mosaic geometry. 

196 inputIds : `list` [`int`], optional 

197 Optional list providing exposure IDs corresponding to input 

198 exposures. Will be generated via the exposure data `getDetector` 

199 method if not provided. 

200 

201 Returns 

202 ------- 

203 output : `lsst.pipe.base.Struct` 

204 Results struct with attribute: 

205 

206 ``outputExp`` 

207 Binned version of input image (`lsst.afw.image.Exposure`). 

208 """ 

209 if not inputIds: 

210 inputIds = [exp.getDetector().getId() for exp in inputExps] 

211 expDict = {id: exp for id, exp in zip(inputIds, inputExps)} 

212 image = self.makeCameraImage(expDict, camera, self.config.binning) 

213 

214 return pipeBase.Struct(outputData=image) 

215 

216 

217class ImageSource: 

218 """Source of images for makeImageFromCamera""" 

219 

220 def __init__(self, exposures): 

221 self.exposures = exposures 

222 self.isTrimmed = True 

223 self.background = np.nan 

224 

225 def getCcdImage(self, detector, imageFactory, binSize): 

226 """Provide image of CCD to makeImageFromCamera 

227 

228 Parameters 

229 ---------- 

230 detector : `int` 

231 Detector ID to get image data for. 

232 imageFactory : `lsst.afw.image.Image` 

233 Type of image to construct. 

234 binSize : `int` 

235 Binsize to use to recompute dimensions. 

236 

237 Returns 

238 ------- 

239 image : `lsst.afw.image.Image` 

240 Appropriately rotated, binned, and transformed 

241 image to be mosaicked. 

242 detector : `lsst.afw.cameraGeom.Detector` 

243 Camera detector that the returned image data 

244 belongs to. 

245 """ 

246 detId = detector.getId() 

247 

248 if detId not in self.exposures: 

249 dims = detector.getBBox().getDimensions() / binSize 

250 image = imageFactory(*[int(xx) for xx in dims]) 

251 image.set(self.background) 

252 else: 

253 image = self.exposures[detector.getId()] 

254 if hasattr(image, "getMaskedImage"): 

255 image = image.getMaskedImage() 

256 if hasattr(image, "getMask"): 

257 mask = image.getMask() 

258 isBad = mask.getArray() & mask.getPlaneBitMask("NO_DATA") > 0 

259 image = image.clone() 

260 image.getImage().getArray()[isBad] = self.background 

261 if hasattr(image, "getImage"): 

262 image = image.getImage() 

263 

264 # afwMath.rotateImageBy90 checks NQuarter values, 

265 # so we don't need to here. 

266 image = afwMath.rotateImageBy90(image, detector.getOrientation().getNQuarter()) 

267 return image, detector