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 

24from scarlet.bbox import Box 

25 

26import lsst.afw.image as afwImage 

27from lsst.afw.geom import SpanSet 

28from lsst.geom import Point2I 

29import lsst.log 

30import lsst.afw.detection as afwDet 

31 

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

33 

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

35 

36 

37def hasEdgeFlux(source, edgeDistance=1): 

38 """hasEdgeFlux 

39 

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

41 of the edge. 

42 

43 Parameters 

44 ---------- 

45 source : `scarlet.Component` 

46 The source to check for edge flux 

47 edgeDistance : int 

48 The distance from the edge of the image to consider 

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

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

51 considered to have edge flux. 

52 If `edgeDistance` is `None` then the edge check is ignored. 

53 

54 Returns 

55 ------- 

56 isEdge: `bool` 

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

58 """ 

59 if edgeDistance is None: 

60 return False 

61 

62 assert edgeDistance > 0 

63 

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

65 if hasattr(source, "sed"): 

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

67 else: 

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

69 model = source.get_model()[band] 

70 for edge in range(edgeDistance): 

71 if ( 

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

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

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

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

76 ): 

77 return True 

78 return False 

79 

80 

81def initAllSources(frame, centers, observation, 

82 symmetric=False, monotonic=True, 

83 thresh=1, maxComponents=1, edgeDistance=1, shifting=False, 

84 downgrade=False, fallback=True): 

85 """Initialize all sources in a blend 

86 

87 Any sources which cannot be initialized are returned as a `skipped` 

88 index, the index needed to reinsert them into a catalog to preserve 

89 their index in the output catalog. 

90 

91 See `~initSources` for a description of the parameters 

92 

93 Parameters 

94 ---------- 

95 centers : list of tuples 

96 `(y, x)` center location for each source 

97 

98 Returns 

99 ------- 

100 sources: list 

101 List of intialized sources, where each source derives from the 

102 `~scarlet.Component` class. 

103 """ 

104 # Only deblend sources that can be initialized 

105 sources = [] 

106 skipped = [] 

107 for k, center in enumerate(centers): 

108 source = initSource( 

109 frame, center, observation, 

110 symmetric, monotonic, 

111 thresh, maxComponents, edgeDistance, shifting, 

112 downgrade, fallback) 

113 if source is not None: 

114 sources.append(source) 

115 else: 

116 skipped.append(k) 

117 return sources, skipped 

118 

119 

120def initSource(frame, center, observation, 

121 symmetric=False, monotonic=True, 

122 thresh=1, maxComponents=1, edgeDistance=1, shifting=False, 

123 downgrade=False, fallback=True): 

124 """Initialize a Source 

125 

126 The user can specify the number of desired components 

127 for the modeled source. If scarlet cannot initialize a 

128 model with the desired number of components it continues 

129 to attempt initialization of one fewer component until 

130 it finds a model that can be initialized. 

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

132 source with the desired number of components, for example 

133 a two component source might have degenerate components, 

134 a single component source might not have enough signal in 

135 the joint coadd (all bands combined together into 

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

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

138 enough signal to initialize as a point source. 

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

140 then this source is skipped. 

141 

142 Parameters 

143 ---------- 

144 frame : `LsstFrame` 

145 The model frame for the scene 

146 center : `tuple` of `float`` 

147 `(y, x)` location for the center of the source. 

148 observation : `~scarlet.Observation` 

149 The `Observation` that contains the images, weights, and PSF 

150 used to generate the model. 

151 symmetric : `bool` 

152 Whether or not the object is symmetric 

153 monotonic : `bool` 

154 Whether or not the object has flux monotonically 

155 decreasing from its center 

156 thresh : `float` 

157 Fraction of the background to use as a threshold for 

158 each pixel in the initialization 

159 maxComponents : int 

160 The maximum number of components in a source. 

161 If `fallback` is `True` then when 

162 a source fails to initialize with `maxComponents` it 

163 will continue to subtract one from the number of components 

164 until it reaches zero (which fits a point source). 

165 If a point source cannot be fit then the source is skipped. 

166 edgeDistance : int 

167 The distance from the edge of the image to consider 

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

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

170 considered to have edge flux. 

171 If `edgeDistance` is `None` then the edge check is ignored. 

172 shifting : bool 

173 Whether or not to fit the position of a source. 

174 This is an expensive operation and is typically only used when 

175 a source is on the edge of the detector. 

176 downgrade : bool 

177 Whether or not to decrease the number of components for sources 

178 with small bounding boxes. For example, a source with no flux 

179 outside of its 16x16 box is unlikely to be resolved enough 

180 for multiple components, so a single source can be used. 

181 fallback : bool 

182 Whether to reduce the number of components 

183 if the model cannot be initialized with `maxComponents`. 

184 This is unlikely to be used in production 

185 but can be useful for troubleshooting when an error can cause 

186 a particular source class to fail every time. 

187 """ 

188 while maxComponents > 1: 

189 try: 

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

191 monotonic=monotonic, thresh=thresh, shifting=shifting) 

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

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

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

195 msg = "Could not initialize source at {} with {} components".format(center, maxComponents) 

196 logger.warning(msg) 

197 raise ValueError(msg) 

198 

199 if downgrade and np.all([np.all(np.array(c.bbox.shape[1:]) <= 8) for c in source.components]): 

200 # the source is in a small box so it must be a point source 

201 maxComponents = 0 

202 elif downgrade and np.all([np.all(np.array(c.bbox.shape[1:]) <= 16) for c in source.components]): 

203 # if the source is in a slightly larger box 

204 # it is not big enough to model with 2 components 

205 maxComponents = 1 

206 elif hasEdgeFlux(source, edgeDistance): 

207 source.shifting = True 

208 

209 break 

210 except Exception as e: 

211 if not fallback: 

212 raise e 

213 # If the MultiComponentSource failed to initialize 

214 # try an ExtendedSource 

215 maxComponents -= 1 

216 

217 if maxComponents == 1: 

218 try: 

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

220 symmetric=symmetric, monotonic=monotonic, shifting=shifting) 

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

222 msg = "Could not initlialize source at {} with 1 component".format(center) 

223 logger.warning(msg) 

224 raise ValueError(msg) 

225 

226 if downgrade and np.all(np.array(source.bbox.shape[1:]) <= 16): 

227 # the source is in a small box so it must be a point source 

228 maxComponents = 0 

229 elif hasEdgeFlux(source, edgeDistance): 

230 source.shifting = True 

231 except Exception as e: 

232 if not fallback: 

233 raise e 

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

235 # initialize it as a PointSource 

236 maxComponents -= 1 

237 

238 if maxComponents == 0: 

239 try: 

240 source = PointSource(frame, center, observation) 

241 except Exception: 

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

243 # so skip this source 

244 return None 

245 

246 if hasEdgeFlux(source, edgeDistance): 

247 # The detection algorithm implemented in meas_algorithms 

248 # does not place sources within the edge mask 

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

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

251 # may ruin an entire blend. So we reinitialize edge sources 

252 # to allow for shifting and return the result. 

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

254 return initSource(frame, center, observation, 

255 symmetric, monotonic, thresh, maxComponents, 

256 edgeDistance, shifting=True) 

257 source.isEdge = True 

258 else: 

259 source.isEdge = False 

260 

261 return source 

262 

263 

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

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

266 

267 Parameters 

268 ---------- 

269 source : `scarlet.Component` 

270 The scarlet source with a morphology to convert to 

271 a `HeavyFootprint`. 

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

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

274 xy0 : `tuple` 

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

276 `HeavyFootprint`. 

277 

278 Returns 

279 ------- 

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

281 """ 

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

283 ss = SpanSet.fromMask(mask) 

284 

285 if len(ss) == 0: 

286 return None 

287 

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

289 cy, cx = source.pixel_center 

290 xmin, ymin = xy0 

291 # HeavyFootprints are not defined for 64 bit floats 

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

293 peakFlux = morph[cy, cx] 

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

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

296 timg = timg[tfoot.getBBox()] 

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

298 return heavy 

299 

300 

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

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

303 

304 Parameters 

305 ---------- 

306 source : `scarlet.Component` 

307 The source to convert to a `HeavyFootprint`. 

308 filters : `iterable` 

309 A "list" of names for each filter. 

310 xy0 : `lsst.geom.Point2I` 

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

312 `HeavyFootprint`. If `observation` is not `None` then 

313 this parameter is updated with the position of the new model 

314 observation : `scarlet.Observation` 

315 The scarlet observation, used to convolve the image with 

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

317 `HeavyFootprint` will exist in the model frame. 

318 dtype : `numpy.dtype` 

319 The data type for the returned `HeavyFootprint`. 

320 

321 Returns 

322 ------- 

323 mHeavy : `lsst.detection.MultibandFootprint` 

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

325 """ 

326 if observation is not None: 

327 # We want to convolve the model with the observed PSF, 

328 # which means we need to grow the model box by the PSF to 

329 # account for all of the flux after convolution. 

330 # FYI: The `scarlet.Box` class implements the `&` operator 

331 # to take the intersection of two boxes. 

332 

333 # Get the PSF size and radii to grow the box 

334 py, px = observation.frame.psf.shape[1:] 

335 dh = py // 2 

336 dw = px // 2 

337 shape = (source.bbox.shape[0], source.bbox.shape[1] + py, source.bbox.shape[2] + px) 

338 origin = (source.bbox.origin[0], source.bbox.origin[1] - dh, source.bbox.origin[2] - dw) 

339 # Create the larger box to fit the model + PSf 

340 bbox = Box(shape, origin=origin) 

341 # Only use the portion of the convolved model that fits in the image 

342 overlap = bbox & source.model_frame 

343 # Load the full multiband model in the larger box 

344 model = source.model_to_frame(overlap) 

345 # Convolve the model with the PSF in each band 

346 # Always use a real space convolution to limit artifacts 

347 model = observation.convolve(model, convolution_type="real").astype(dtype) 

348 # Update xy0 with the origin of the sources box 

349 xy0 = Point2I(overlap.origin[-1] + xy0.x, overlap.origin[-2] + xy0.y) 

350 else: 

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

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

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

354 peakCat.append(source.detectedPeak) 

355 for footprint in mHeavy: 

356 footprint.setPeakCatalog(peakCat) 

357 return mHeavy