Coverage for python/lsst/meas/extensions/scarlet/source.py : 9%

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/>.
22import numpy as np
23from scarlet.source import PointSource, ExtendedSource, MultiComponentSource
24from scarlet.bbox import Box
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
32__all__ = ["initSource", "morphToHeavy", "modelToHeavy"]
34logger = lsst.log.Log.getLogger("meas.deblender.deblend")
37def hasEdgeFlux(source, edgeDistance=1):
38 """hasEdgeFlux
40 Determine whether or not a source has flux within `edgeDistance`
41 of the edge.
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.
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
62 assert edgeDistance > 0
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
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
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.
91 See `~initSources` for a description of the parameters
93 Parameters
94 ----------
95 centers : list of tuples
96 `(y, x)` center location for each source
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
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
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.
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)
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
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
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)
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
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
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
261 return source
264def morphToHeavy(source, peakSchema, xy0=Point2I()):
265 """Convert the morphology to a `HeavyFootprint`
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`.
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)
285 if len(ss) == 0:
286 return None
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
301def modelToHeavy(source, filters, xy0=Point2I(), observation=None, dtype=np.float32):
302 """Convert the model to a `MultibandFootprint`
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`.
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.
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