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# This file is part of meas_extensions_scarlet. 

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 

22import numpy as np 

23from scarlet.source import PointSource, ExtendedSource, MultiComponentSource 

24 

25import lsst.afw.image as afwImage 

26from lsst.afw.geom import SpanSet 

27from lsst.geom import Point2I 

28import lsst.log 

29import lsst.afw.detection as afwDet 

30 

31__all__ = ["initSource", "morphToHeavy", "modelToHeavy"] 

32 

33logger = lsst.log.Log.getLogger("meas.deblender.deblend") 

34 

35 

36def hasEdgeFlux(source, edgeDistance=1): 

37 """hasEdgeFlux 

38 

39 Determine whether or not a source has flux within `edgeDistance` 

40 of the edge. 

41 

42 Parameters 

43 ---------- 

44 source : `scarlet.Component` 

45 The source to check for edge flux 

46 edgeDistance : int 

47 The distance from the edge of the image to consider 

48 a source an edge source. For example if `edgeDistance=3` 

49 then any source within 3 pixels of the edge will be 

50 considered to have edge flux. The minimum value of 

51 `edgeDistance` is one, meaning the rows and columns 

52 of pixels on the edge. 

53 

54 Returns 

55 ------- 

56 isEdge: `bool` 

57 Whether or not the source has flux on the edge. 

58 """ 

59 assert edgeDistance > 0 

60 

61 # Use the first band that has a non-zero SED 

62 if hasattr(source, "sed"): 

63 band = np.min(np.where(source.sed > 0)[0]) 

64 else: 

65 band = np.min(np.where(source.components[0].sed > 0)[0]) 

66 model = source.get_model()[band] 

67 for edge in range(edgeDistance): 

68 if ( 

69 np.any(model[edge-1] > 0) 

70 or np.any(model[-edge] > 0) 

71 or np.any(model[:, edge-1] > 0) 

72 or np.any(model[:, -edge] > 0) 

73 ): 

74 return True 

75 return False 

76 

77 

78def initSource(frame, peak, observation, bbox, 

79 symmetric=False, monotonic=True, 

80 thresh=5, components=1, edgeDistance=1, shifting=False): 

81 """Initialize a Source 

82 

83 The user can specify the number of desired components 

84 for the modeled source. If scarlet cannot initialize a 

85 model with the desired number of components it continues 

86 to attempt initialization of one fewer component until 

87 it finds a model that can be initialized. 

88 

89 It is possible that scarlet will be unable to initialize a 

90 source with the desired number of components, for example 

91 a two component source might have degenerate components, 

92 a single component source might not have enough signal in 

93 the joint coadd (all bands combined together into 

94 single signal-to-noise weighted image for initialization) 

95 to initialize, and a true spurious detection will not have 

96 enough signal to initialize as a point source. 

97 If all of the models fail, including a `PointSource` model, 

98 then this source is skipped. 

99 

100 Parameters 

101 ---------- 

102 frame : `LsstFrame` 

103 The model frame for the scene 

104 peak : `PeakRecord` 

105 Record for a peak in the parent `PeakCatalog` 

106 observation : `LsstObservation` 

107 The images, psfs, etc, of the observed data. 

108 bbox : `lsst.geom.Box2I` 

109 The bounding box of the parent footprint. 

110 symmetric : `bool` 

111 Whether or not the object is symmetric 

112 monotonic : `bool` 

113 Whether or not the object has flux monotonically 

114 decreasing from its center 

115 thresh : `float` 

116 Fraction of the background to use as a threshold for 

117 each pixel in the initialization 

118 components : int 

119 The number of components for the source. 

120 If `components=0` then a `PointSource` model is used. 

121 """ 

122 assert components <= 2 

123 xmin = bbox.getMinX() 

124 ymin = bbox.getMinY() 

125 center = np.array([peak.getIy()-ymin, peak.getIx()-xmin], dtype=int) 

126 

127 while components > 1: 

128 try: 

129 source = MultiComponentSource(frame, center, observation, symmetric=symmetric, 

130 monotonic=monotonic, thresh=thresh, shifting=shifting) 

131 if (np.any([np.any(np.isnan(c.sed)) for c in source.components]) 

132 or np.any([np.all(c.sed <= 0) for c in source.components]) 

133 or np.any([np.any(~np.isfinite(c.morph)) for c in source.components])): 

134 logger.warning("Could not initialize") 

135 raise ValueError("Could not initialize source") 

136 if hasEdgeFlux(source, edgeDistance): 

137 source.shifting = True 

138 break 

139 except Exception: 

140 # If the MultiComponentSource failed to initialize 

141 # try an ExtendedSource 

142 components -= 1 

143 

144 if components == 1: 

145 try: 

146 source = ExtendedSource(frame, center, observation, thresh=thresh, 

147 symmetric=symmetric, monotonic=monotonic, shifting=shifting) 

148 if np.any(np.isnan(source.sed)) or np.all(source.sed <= 0) or np.sum(source.morph) == 0: 

149 logger.warning("Could not initialize") 

150 raise ValueError("Could not initialize source") 

151 except Exception: 

152 # If the source is too faint for background detection, 

153 # initialize it as a PointSource 

154 components -= 1 

155 

156 if components == 0: 

157 try: 

158 source = PointSource(frame, center, observation) 

159 except Exception: 

160 # None of the models worked to initialize the source, 

161 # so skip this source 

162 return None 

163 

164 if hasEdgeFlux(source, edgeDistance): 

165 # The detection algorithm implemented in meas_algorithms 

166 # does not place sources within the edge mask 

167 # (roughly 5 pixels from the edge). This results in poor 

168 # deblending of the edge source, which for bright sources 

169 # may ruin an entire blend. 

170 # By turning on shifting we allow exxtended sources to be shifted 

171 # by a fraction of a pixel, which is computationally expensive and 

172 # not necessary for non-edge sources. 

173 # Due to the complexities of scarlet initialization it is easier 

174 # to reinitialize edge sources to allow for shifting than it is 

175 # to update this parameter on the fly. 

176 if not isinstance(source, PointSource) and not shifting: 

177 return initSource(frame, peak, observation, bbox, 

178 symmetric, monotonic, thresh, components, 

179 edgeDistance, shifting=True) 

180 source.isEdge = True 

181 else: 

182 source.isEdge = False 

183 

184 source.detectedPeak = peak 

185 return source 

186 

187 

188def morphToHeavy(source, peakSchema, xy0=Point2I()): 

189 """Convert the morphology to a `HeavyFootprint` 

190 

191 Parameters 

192 ---------- 

193 source : `scarlet.Component` 

194 The scarlet source with a morphology to convert to 

195 a `HeavyFootprint`. 

196 peakSchema : `lsst.daf.butler.Schema` 

197 The schema for the `PeakCatalog` of the `HeavyFootprint`. 

198 xy0 : `tuple` 

199 `(x,y)` coordinates of the bounding box containing the 

200 `HeavyFootprint`. 

201 

202 Returns 

203 ------- 

204 heavy : `lsst.afw.detection.HeavyFootprint` 

205 """ 

206 mask = afwImage.MaskX(np.array(source.morph > 0, dtype=np.int32), xy0=xy0) 

207 ss = SpanSet.fromMask(mask) 

208 

209 if len(ss) == 0: 

210 return None 

211 

212 tfoot = afwDet.Footprint(ss, peakSchema=peakSchema) 

213 cy, cx = source.pixel_center 

214 xmin, ymin = xy0 

215 # HeavyFootprints are not defined for 64 bit floats 

216 morph = source.morph.astype(np.float32) 

217 peakFlux = morph[cy, cx] 

218 tfoot.addPeak(cx+xmin, cy+ymin, peakFlux) 

219 timg = afwImage.ImageF(morph, xy0=xy0) 

220 timg = timg[tfoot.getBBox()] 

221 heavy = afwDet.makeHeavyFootprint(tfoot, afwImage.MaskedImageF(timg)) 

222 return heavy 

223 

224 

225def modelToHeavy(source, filters, xy0=Point2I(), observation=None, dtype=np.float32): 

226 """Convert the model to a `MultibandFootprint` 

227 

228 Parameters 

229 ---------- 

230 source : `scarlet.Component` 

231 The source to convert to a `HeavyFootprint`. 

232 filters : `iterable` 

233 A "list" of names for each filter. 

234 xy0 : `lsst.geom.Point2I` 

235 `(x,y)` coordinates of the bounding box containing the 

236 `HeavyFootprint`. 

237 observation : `scarlet.Observation` 

238 The scarlet observation, used to convolve the image with 

239 the origin PSF. If `observation`` is `None` then the 

240 `HeavyFootprint` will exist in the model frame. 

241 dtype : `numpy.dtype` 

242 The data type for the returned `HeavyFootprint`. 

243 

244 Returns 

245 ------- 

246 mHeavy : `lsst.detection.MultibandFootprint` 

247 The multi-band footprint containing the model for the source. 

248 """ 

249 if observation is not None: 

250 model = observation.render(source.get_model()).astype(dtype) 

251 else: 

252 model = source.get_model().astype(dtype) 

253 mHeavy = afwDet.MultibandFootprint.fromArrays(filters, model, xy0=xy0) 

254 peakCat = afwDet.PeakCatalog(source.detectedPeak.table) 

255 peakCat.append(source.detectedPeak) 

256 for footprint in mHeavy: 

257 footprint.setPeakCatalog(peakCat) 

258 return mHeavy