Coverage for python/lsst/meas/algorithms/skyObjects.py: 25%

59 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-11 02:40 -0700

1# This file is part of meas_algorithms. 

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__ = ["SkyObjectsConfig", "SkyObjectsTask", "generateSkyObjects"] 

23 

24from scipy.stats import qmc 

25 

26from lsst.pex.config import Config, Field, ListField 

27from lsst.pipe.base import Task 

28 

29import lsst.afw.detection 

30import lsst.afw.geom 

31import lsst.afw.math 

32 

33 

34class SkyObjectsConfig(Config): 

35 """Configuration for generating sky objects""" 

36 avoidMask = ListField( 

37 dtype=str, 

38 default=["DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA"], 

39 doc="Avoid pixels masked with these mask planes." 

40 ) 

41 growMask = Field( 

42 dtype=int, 

43 default=0, 

44 doc="Number of pixels to grow the masked pixels when adding sky sources." 

45 ) 

46 sourceRadius = Field( 

47 dtype=float, 

48 default=8, 

49 doc="Radius, in pixels, of sky sources." 

50 ) 

51 nSources = Field( 

52 dtype=int, 

53 default=100, 

54 doc="Try to add this many sky sources." 

55 ) 

56 nTrialSources = Field( 

57 dtype=int, 

58 default=None, 

59 optional=True, 

60 doc="Maximum number of trial sky object positions " 

61 "(default: nSkySources*nTrialSkySourcesMultiplier)." 

62 ) 

63 nTrialSourcesMultiplier = Field( 

64 dtype=int, 

65 default=5, 

66 doc="Set nTrialSkySources to nSkySources*nTrialSkySourcesMultiplier " 

67 "if nTrialSkySources is None." 

68 ) 

69 

70 

71def generateSkyObjects(mask, seed, config): 

72 """Generate a list of Footprints of sky objects 

73 

74 Sky objects don't overlap with other objects. This is determined 

75 through the provided `mask` (in which objects are typically flagged 

76 as `DETECTED`). 

77 

78 Sky objects are positioned using a quasi-random Halton sequence number 

79 generator. This is a deterministic sequence that mimics a random trial and 

80 error approach whilst acting to minimize clustering of points for a given 

81 field of view. Up to `nTrialSources` points are generated, returning the 

82 first `nSources` that do not overlap with the mask. 

83 

84 Parameters 

85 ---------- 

86 mask : `lsst.afw.image.Mask` 

87 Input mask plane, which identifies pixels to avoid for the sky 

88 objects. 

89 seed : `int` 

90 Random number generator seed. 

91 config : `SkyObjectsConfig` 

92 Configuration for finding sky objects. 

93 

94 Returns 

95 ------- 

96 skyFootprints : `list` of `lsst.afw.detection.Footprint` 

97 Footprints of sky objects. Each will have a peak at the center 

98 of the sky object. 

99 """ 

100 if config.nSources <= 0: 

101 return [] 

102 

103 skySourceRadius = config.sourceRadius 

104 nSkySources = config.nSources 

105 nTrialSkySources = config.nTrialSources 

106 if nTrialSkySources is None: 

107 nTrialSkySources = config.nTrialSourcesMultiplier*nSkySources 

108 

109 box = mask.getBBox() 

110 box.grow(-(int(skySourceRadius) + 1)) # Avoid objects partially off the image 

111 xMin, yMin = box.getMin() 

112 xMax, yMax = box.getMax() 

113 

114 avoid = lsst.afw.geom.SpanSet.fromMask(mask, mask.getPlaneBitMask(config.avoidMask)) 

115 if config.growMask > 0: 

116 avoid = avoid.dilated(config.growMask) 

117 

118 sampler = qmc.Halton(d=2, seed=seed).random(nTrialSkySources) 

119 sample = qmc.scale(sampler, [xMin, yMin], [xMax, yMax]) 

120 

121 skyFootprints = [] 

122 for x, y in zip(sample[:, 0].astype(int), sample[:, 1].astype(int)): 

123 if len(skyFootprints) == nSkySources: 

124 break 

125 

126 spans = lsst.afw.geom.SpanSet.fromShape(int(skySourceRadius), offset=(x, y)) 

127 if spans.overlaps(avoid): 

128 continue 

129 

130 fp = lsst.afw.detection.Footprint(spans, mask.getBBox()) 

131 fp.addPeak(x, y, 0) 

132 skyFootprints.append(fp) 

133 

134 # Add doubled-in-size sky object spanSet to the avoid mask. 

135 avoid = avoid.union(spans.dilated(int(skySourceRadius))) 

136 

137 return skyFootprints 

138 

139 

140class SkyObjectsTask(Task): 

141 """Generate a list of Footprints of sky sources/objects (regions on the 

142 sky that do not otherwise have detections). 

143 

144 Parameters 

145 ---------- 

146 schema : `lsst.afw.table.Schema` 

147 Schema used to create the output `~lsst.afw.table.SourceCatalog`, 

148 updated with fields that will be written by this task. 

149 

150 """ 

151 ConfigClass = SkyObjectsConfig 

152 

153 def __init__(self, schema=None, **kwargs): 

154 super().__init__(**kwargs) 

155 if schema is not None: 

156 self.skySourceKey = schema.addField("sky_source", type="Flag", 

157 doc="Region on image with no detections.") 

158 else: 

159 self.skySourceKey = None 

160 

161 def run(self, mask, seed, catalog=None): 

162 """Generate a list of Footprints of sky sources/objects. 

163 

164 Sky objects don't overlap with other objects. This is determined 

165 through the provided `mask` (in which objects are typically flagged 

166 as `DETECTED`). 

167 

168 Sky objects are positioned using a quasi-random Halton sequence 

169 number generator. This is a deterministic sequence that mimics a random 

170 trial and error approach whilst acting to minimize clustering of points 

171 for a given field of view. Up to `nTrialSources` points are generated, 

172 returning the first `nSources` that do not overlap with the mask. 

173 

174 Parameters 

175 ---------- 

176 mask : `lsst.afw.image.Mask` 

177 Input mask plane, which identifies pixels to avoid for the sky 

178 objects. 

179 seed : `int` 

180 Random number generator seed. 

181 catalog : `lsst.afw.table.SourceCatalog`, optional 

182 Catalog to add detected footprints to; modified in-place if any 

183 sky source/object footprints are created. 

184 

185 Returns 

186 ------- 

187 skyFootprints : `list` of `lsst.afw.detection.Footprint` 

188 Footprints of sky objects. Each will have a peak at the center 

189 of the sky object. 

190 """ 

191 skyFootprints = generateSkyObjects(mask, seed, self.config) 

192 self.log.info("Added %d of %d requested sky sources (%.0f%%)", len(skyFootprints), 

193 self.config.nSources, 100*len(skyFootprints)/self.config.nSources) 

194 

195 if skyFootprints and self.skySourceKey is not None and catalog is not None: 

196 for footprint in skyFootprints: 

197 record = catalog.addNew() 

198 record.setFootprint(footprint) 

199 record.set(self.skySourceKey, True) 

200 

201 return skyFootprints