Coverage for python/lsst/pipe/tasks/calexpCutout.py: 28%

51 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-11 10:41 +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# (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__ = ['CalexpCutoutTaskConfig', 'CalexpCutoutTask'] 

23 

24import lsst.pipe.base as pipeBase 

25import lsst.geom as geom 

26from lsst.pex.config import Field 

27from lsst.meas.algorithms import Stamp, Stamps 

28 

29DETECTOR_DIMENSIONS = ('instrument', 'visit', 'detector') 

30 

31 

32class CalexpCutoutTaskConnections(pipeBase.PipelineTaskConnections, 

33 dimensions=DETECTOR_DIMENSIONS, 

34 defaultTemplates={}): 

35 """Connections class for CalexpCutoutTask 

36 """ 

37 in_table = pipeBase.connectionTypes.Input( 

38 doc="Locations for cutouts", 

39 name="cutout_positions", 

40 storageClass="AstropyQTable", 

41 dimensions=DETECTOR_DIMENSIONS, 

42 ) 

43 calexp = pipeBase.connectionTypes.Input( 

44 doc="Calexp objects", 

45 name="calexp", 

46 storageClass="ExposureF", 

47 dimensions=DETECTOR_DIMENSIONS, 

48 ) 

49 cutouts = pipeBase.connectionTypes.Output( 

50 doc="Cutouts", 

51 name="calexp_cutouts", 

52 storageClass="Stamps", 

53 dimensions=DETECTOR_DIMENSIONS, 

54 ) 

55 

56 

57class CalexpCutoutTaskConfig(pipeBase.PipelineTaskConfig, 

58 pipelineConnections=CalexpCutoutTaskConnections): 

59 """Configuration for CalexpCutoutTask 

60 """ 

61 max_cutouts = Field(dtype=int, default=100, doc='Maximum number of entries to process. ' 

62 'The result will be the first N in the input table.') 

63 skip_bad = Field(dtype=bool, default=True, doc='Skip cutouts that do not fall completely within' 

64 ' the calexp bounding box? If set to False a ValueError' 

65 ' is raised instead.') 

66 

67 

68class CalexpCutoutTask(pipeBase.PipelineTask): 

69 """Task for computing cutouts on a specific calexp given 

70 positions, xspans, and yspans of the stamps. 

71 """ 

72 ConfigClass = CalexpCutoutTaskConfig 

73 _DefaultName = "calexpCutoutTask" 

74 

75 def run(self, in_table, calexp): 

76 """Compute and return the cutouts. 

77 

78 Parameters 

79 ---------- 

80 in_table : `astropy.QTable` 

81 A table containing at least the following columns: position, xspan 

82 and yspan. The position should be an `astropy.SkyCoord`. The 

83 xspan and yspan are the extent of the cutout in x and y in pixels. 

84 calexp : `lsst.afw.image.ExposureF` 

85 The calibrated exposure from which to extract cutouts 

86 

87 Returns 

88 ------- 

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

90 A struct containing: 

91 

92 * cutouts: an `lsst.meas.algorithms.Stamps` object 

93 that wraps a list of masked images of the cutouts and a 

94 `PropertyList` containing the metadata to be persisted 

95 with the cutouts. The exposure metadata is preserved and, 

96 in addition, arrays holding the RA and Dec of each stamp 

97 in degrees are added to the metadata. Note: the origin 

98 of the output stamps is `lsst.afw.image.PARENT`. 

99 * skipped_positions: a `list` of `lsst.geom.SpherePoint` objects for 

100 stamps that were skiped for being off the image 

101 or partially off the image 

102 

103 Raises 

104 ------ 

105 ValueError 

106 If the input catalog doesn't have the required columns, 

107 a ValueError is raised 

108 """ 

109 cols = in_table.colnames 

110 if 'position' not in cols or 'xspan' not in cols or 'yspan' not in cols: 

111 raise ValueError('Required column missing from the input table. ' 

112 'Required columns are "position", "xspan", and "yspan". ' 

113 f'The column names are: {in_table.colnames}') 

114 wcs = calexp.getWcs() 

115 if wcs is None: 

116 raise RuntimeError("Calexp has no WCS, so cannot compute sky positions.") 

117 max_idx = self.config.max_cutouts 

118 cutout_list = [] 

119 mim = calexp.getMaskedImage() 

120 ras = [] 

121 decs = [] 

122 skipped_positions = [] 

123 for rec in in_table[:max_idx]: 

124 ra = rec['position'].ra.degree 

125 dec = rec['position'].dec.degree 

126 ras.append(ra) 

127 decs.append(dec) 

128 pt = geom.SpherePoint(geom.Angle(ra, geom.degrees), 

129 geom.Angle(dec, geom.degrees)) 

130 pix = wcs.skyToPixel(pt) 

131 xspan = rec['xspan'].value 

132 yspan = rec['yspan'].value 

133 # Clamp to LL corner of the LL pixel and draw extent from there 

134 box = geom.Box2I(geom.Point2I(int(pix.x-xspan/2), int(pix.y-yspan/2)), 

135 geom.Extent2I(xspan, yspan)) 

136 if not mim.getBBox().contains(box): 

137 if not self.config.skip_bad: 

138 raise ValueError(f'Cutout bounding box is not completely contained in the image: {box}') 

139 else: 

140 skipped_positions.append(pt) 

141 continue 

142 sub = mim.Factory(mim, box) 

143 stamp = Stamp(stamp_im=sub, position=pt) 

144 cutout_list.append(stamp) 

145 metadata = calexp.getMetadata() 

146 metadata['RA_DEG'] = ras 

147 metadata['DEC_DEG'] = decs 

148 return pipeBase.Struct(cutouts=Stamps(cutout_list, metadata=metadata), 

149 skipped_positions=skipped_positions)