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

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
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
31__all__ = ["initSource", "morphToHeavy", "modelToHeavy"]
33logger = lsst.log.Log.getLogger("meas.deblender.deblend")
36def hasEdgeFlux(source, edgeDistance=1):
37 """hasEdgeFlux
39 Determine whether or not a source has flux within `edgeDistance`
40 of the edge.
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.
54 Returns
55 -------
56 isEdge: `bool`
57 Whether or not the source has flux on the edge.
58 """
59 assert edgeDistance > 0
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
78def initSource(frame, peak, observation, bbox,
79 symmetric=False, monotonic=True,
80 thresh=5, components=1, edgeDistance=1, shifting=False):
81 """Initialize a Source
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.
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.
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)
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
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
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
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
184 source.detectedPeak = peak
185 return source
188def morphToHeavy(source, peakSchema, xy0=Point2I()):
189 """Convert the morphology to a `HeavyFootprint`
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`.
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)
209 if len(ss) == 0:
210 return None
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
225def modelToHeavy(source, filters, xy0=Point2I(), observation=None, dtype=np.float32):
226 """Convert the model to a `MultibandFootprint`
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`.
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