Coverage for python/lsst/meas/algorithms/defects.py : 15%

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#
2# LSST Data Management System
3#
4# Copyright 2008-2017 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
23"""Support for image defects"""
25__all__ = ("Defects",)
27import logging
28import itertools
29import collections.abc
30import contextlib
31import numpy as np
32import copy
33import datetime
34import math
35import numbers
36import os.path
37import warnings
38import astropy.table
40import lsst.geom
41import lsst.afw.table
42import lsst.afw.detection
43import lsst.afw.image
44import lsst.afw.geom
45from lsst.daf.base import PropertyList
47from . import Defect
49log = logging.getLogger(__name__)
51SCHEMA_NAME_KEY = "DEFECTS_SCHEMA"
52SCHEMA_VERSION_KEY = "DEFECTS_SCHEMA_VERSION"
55class Defects(collections.abc.MutableSequence):
56 """Collection of `lsst.meas.algorithms.Defect`.
58 Parameters
59 ----------
60 defectList : iterable of `lsst.meas.algorithms.Defect`
61 or `lsst.geom.BoxI`, optional
62 Collections of defects to apply to the image.
63 metadata : `lsst.daf.base.PropertyList`, optional
64 Metadata to associate with the defects. Will be copied and
65 overwrite existing metadata, if any. If not supplied the existing
66 metadata will be reset.
67 normalize_on_init : `bool`
68 If True, normalization is applied to the defects in ``defectList`` to
69 remove duplicates, eliminate overlaps, etc.
71 Notes
72 -----
73 Defects are stored within this collection in a "reduced" or "normalized"
74 form: rather than simply storing the bounding boxes which are added to the
75 collection, we eliminate overlaps and duplicates. This normalization
76 procedure may introduce overhead when adding many new defects; it may be
77 temporarily disabled using the `Defects.bulk_update` context manager if
78 necessary.
79 """
81 _OBSTYPE = "defects"
82 """The calibration type used for ingest."""
84 def __init__(self, defectList=None, metadata=None, *, normalize_on_init=True):
85 self._defects = []
87 if defectList is not None:
88 self._bulk_update = True
89 for d in defectList:
90 self.append(d)
91 self._bulk_update = False
93 if normalize_on_init:
94 self._normalize()
96 if metadata is not None:
97 self._metadata = metadata
98 else:
99 self.setMetadata()
101 def _check_value(self, value):
102 """Check that the supplied value is a `~lsst.meas.algorithms.Defect`
103 or can be converted to one.
105 Parameters
106 ----------
107 value : `object`
108 Value to check.
110 Returns
111 -------
112 new : `~lsst.meas.algorithms.Defect`
113 Either the supplied value or a new object derived from it.
115 Raises
116 ------
117 ValueError
118 Raised if the supplied value can not be converted to
119 `~lsst.meas.algorithms.Defect`
120 """
121 if isinstance(value, Defect):
122 pass
123 elif isinstance(value, lsst.geom.BoxI):
124 value = Defect(value)
125 elif isinstance(value, lsst.geom.PointI):
126 value = Defect(lsst.geom.Box2I(value, lsst.geom.Extent2I(1, 1)))
127 elif isinstance(value, lsst.afw.image.DefectBase):
128 value = Defect(value.getBBox())
129 else:
130 raise ValueError(f"Defects must be of type Defect, BoxI, or PointI, not '{value!r}'")
131 return value
133 def __len__(self):
134 return len(self._defects)
136 def __getitem__(self, index):
137 return self._defects[index]
139 def __setitem__(self, index, value):
140 """Can be given a `~lsst.meas.algorithms.Defect` or a `lsst.geom.BoxI`
141 """
142 self._defects[index] = self._check_value(value)
143 self._normalize()
145 def __iter__(self):
146 return iter(self._defects)
148 def __delitem__(self, index):
149 del self._defects[index]
151 def __eq__(self, other):
152 """Compare if two `Defects` are equal.
154 Two `Defects` are equal if their bounding boxes are equal and in
155 the same order. Metadata content is ignored.
156 """
157 if not isinstance(other, self.__class__):
158 return False
160 # checking the bboxes with zip() only works if same length
161 if len(self) != len(other):
162 return False
164 # Assume equal if bounding boxes are equal
165 for d1, d2 in zip(self, other):
166 if d1.getBBox() != d2.getBBox():
167 return False
169 return True
171 def __str__(self):
172 return "Defects(" + ",".join(str(d.getBBox()) for d in self) + ")"
174 def _normalize(self):
175 """Recalculate defect bounding boxes for efficiency.
177 Notes
178 -----
179 Ideally, this would generate the provably-minimal set of bounding
180 boxes necessary to represent the defects. At present, however, that
181 doesn't happen: see DM-24781. In the cases of substantial overlaps or
182 duplication, though, this will produce a much reduced set.
183 """
184 # In bulk-update mode, normalization is a no-op.
185 if self._bulk_update:
186 return
188 # work out the minimum and maximum bounds from all defect regions.
189 minX, minY, maxX, maxY = float('inf'), float('inf'), float('-inf'), float('-inf')
190 for defect in self:
191 bbox = defect.getBBox()
192 minX = min(minX, bbox.getMinX())
193 minY = min(minY, bbox.getMinY())
194 maxX = max(maxX, bbox.getMaxX())
195 maxY = max(maxY, bbox.getMaxY())
197 region = lsst.geom.Box2I(lsst.geom.Point2I(minX, minY),
198 lsst.geom.Point2I(maxX, maxY))
200 mi = lsst.afw.image.MaskedImageF(region)
201 self.maskPixels(mi, maskName="BAD")
202 self._defects = Defects.fromMask(mi, "BAD")._defects
204 @contextlib.contextmanager
205 def bulk_update(self):
206 """Temporarily suspend normalization of the defect list.
207 """
208 self._bulk_update = True
209 try:
210 yield
211 finally:
212 self._bulk_update = False
213 self._normalize()
215 def insert(self, index, value):
216 self._defects.insert(index, self._check_value(value))
217 self._normalize()
219 def getMetadata(self):
220 """Retrieve metadata associated with these `Defects`.
222 Returns
223 -------
224 meta : `lsst.daf.base.PropertyList`
225 Metadata. The returned `~lsst.daf.base.PropertyList` can be
226 modified by the caller and the changes will be written to
227 external files.
228 """
229 return self._metadata
231 def setMetadata(self, metadata=None):
232 """Store a copy of the supplied metadata with the defects.
234 Parameters
235 ----------
236 metadata : `lsst.daf.base.PropertyList`, optional
237 Metadata to associate with the defects. Will be copied and
238 overwrite existing metadata. If not supplied the existing
239 metadata will be reset.
240 """
241 if metadata is None:
242 self._metadata = PropertyList()
243 else:
244 self._metadata = copy.copy(metadata)
246 # Ensure that we have the obs type required by calibration ingest
247 self._metadata["OBSTYPE"] = self._OBSTYPE
249 def copy(self):
250 """Copy the defects to a new list, creating new defects from the
251 bounding boxes.
253 Returns
254 -------
255 new : `Defects`
256 New list with new `Defect` entries.
258 Notes
259 -----
260 This is not a shallow copy in that new `Defect` instances are
261 created from the original bounding boxes. It's also not a deep
262 copy since the bounding boxes are not recreated.
263 """
264 return self.__class__(d.getBBox() for d in self)
266 def transpose(self):
267 """Make a transposed copy of this defect list.
269 Returns
270 -------
271 retDefectList : `Defects`
272 Transposed list of defects.
273 """
274 retDefectList = self.__class__()
275 for defect in self:
276 bbox = defect.getBBox()
277 dimensions = bbox.getDimensions()
278 nbbox = lsst.geom.Box2I(lsst.geom.Point2I(bbox.getMinY(), bbox.getMinX()),
279 lsst.geom.Extent2I(dimensions[1], dimensions[0]))
280 retDefectList.append(nbbox)
281 return retDefectList
283 def maskPixels(self, maskedImage, maskName="BAD"):
284 """Set mask plane based on these defects.
286 Parameters
287 ----------
288 maskedImage : `lsst.afw.image.MaskedImage`
289 Image to process. Only the mask plane is updated.
290 maskName : str, optional
291 Mask plane name to use.
292 """
293 # mask bad pixels
294 mask = maskedImage.getMask()
295 bitmask = mask.getPlaneBitMask(maskName)
296 for defect in self:
297 bbox = defect.getBBox()
298 lsst.afw.geom.SpanSet(bbox).clippedTo(mask.getBBox()).setMask(mask, bitmask)
300 def toFitsRegionTable(self):
301 """Convert defect list to `~lsst.afw.table.BaseCatalog` using the
302 FITS region standard.
304 Returns
305 -------
306 table : `lsst.afw.table.BaseCatalog`
307 Defects in tabular form.
309 Notes
310 -----
311 The table created uses the
312 `FITS regions <https://fits.gsfc.nasa.gov/registry/region.html>`_
313 definition tabular format. The ``X`` and ``Y`` coordinates are
314 converted to FITS Physical coordinates that have origin pixel (1, 1)
315 rather than the (0, 0) used in LSST software.
316 """
318 nrows = len(self._defects)
320 schema = lsst.afw.table.Schema()
321 x = schema.addField("X", type="D", units="pix", doc="X coordinate of center of shape")
322 y = schema.addField("Y", type="D", units="pix", doc="Y coordinate of center of shape")
323 shape = schema.addField("SHAPE", type="String", size=16, doc="Shape defined by these values")
324 r = schema.addField("R", type="ArrayD", size=2, units="pix", doc="Extents")
325 rotang = schema.addField("ROTANG", type="D", units="deg", doc="Rotation angle")
326 component = schema.addField("COMPONENT", type="I", doc="Index of this region")
327 table = lsst.afw.table.BaseCatalog(schema)
328 table.resize(nrows)
330 if nrows:
331 # Adding entire columns is more efficient than adding
332 # each element separately
333 xCol = []
334 yCol = []
335 rCol = []
337 for i, defect in enumerate(self._defects):
338 box = defect.getBBox()
339 center = box.getCenter()
340 # Correct for the FITS 1-based offset
341 xCol.append(center.getX() + 1.0)
342 yCol.append(center.getY() + 1.0)
344 width = box.width
345 height = box.height
347 if width == 1 and height == 1:
348 # Call this a point
349 shapeType = "POINT"
350 else:
351 shapeType = "BOX"
353 # Strings have to be added per row
354 table[i][shape] = shapeType
356 rCol.append(np.array([width, height], dtype=np.float64))
358 # Assign the columns
359 table[x] = np.array(xCol, dtype=np.float64)
360 table[y] = np.array(yCol, dtype=np.float64)
362 table[r] = np.array(rCol)
363 table[rotang] = np.zeros(nrows, dtype=np.float64)
364 table[component] = np.arange(nrows)
366 # Set some metadata in the table (force OBSTYPE to exist)
367 metadata = copy.copy(self.getMetadata())
368 metadata["OBSTYPE"] = self._OBSTYPE
369 metadata[SCHEMA_NAME_KEY] = "FITS Region"
370 metadata[SCHEMA_VERSION_KEY] = 1
371 table.setMetadata(metadata)
373 return table
375 def writeFits(self, *args):
376 """Write defect list to FITS.
378 Parameters
379 ----------
380 *args
381 Arguments to be forwarded to
382 `lsst.afw.table.BaseCatalog.writeFits`.
383 """
384 table = self.toFitsRegionTable()
386 # Add some additional headers useful for tracking purposes
387 metadata = table.getMetadata()
388 now = datetime.datetime.utcnow()
389 metadata["DATE"] = now.isoformat()
390 metadata["CALIB_CREATION_DATE"] = now.strftime("%Y-%m-%d")
391 metadata["CALIB_CREATION_TIME"] = now.strftime("%T %Z").strip()
393 table.writeFits(*args)
395 def toSimpleTable(self):
396 """Convert defects to a simple table form that we use to write
397 to text files.
399 Returns
400 -------
401 table : `lsst.afw.table.BaseCatalog`
402 Defects in simple tabular form.
404 Notes
405 -----
406 These defect tables are used as the human readable definitions
407 of defects in calibration data definition repositories. The format
408 is to use four columns defined as follows:
410 x0 : `int`
411 X coordinate of bottom left corner of box.
412 y0 : `int`
413 Y coordinate of bottom left corner of box.
414 width : `int`
415 X extent of the box.
416 height : `int`
417 Y extent of the box.
418 """
419 schema = lsst.afw.table.Schema()
420 x = schema.addField("x0", type="I", units="pix",
421 doc="X coordinate of bottom left corner of box")
422 y = schema.addField("y0", type="I", units="pix",
423 doc="Y coordinate of bottom left corner of box")
424 width = schema.addField("width", type="I", units="pix",
425 doc="X extent of box")
426 height = schema.addField("height", type="I", units="pix",
427 doc="Y extent of box")
428 table = lsst.afw.table.BaseCatalog(schema)
430 nrows = len(self._defects)
431 table.resize(nrows)
433 if nrows:
435 xCol = []
436 yCol = []
437 widthCol = []
438 heightCol = []
440 for defect in self._defects:
441 box = defect.getBBox()
442 xCol.append(box.getBeginX())
443 yCol.append(box.getBeginY())
444 widthCol.append(box.getWidth())
445 heightCol.append(box.getHeight())
447 table[x] = np.array(xCol, dtype=np.int64)
448 table[y] = np.array(yCol, dtype=np.int64)
449 table[width] = np.array(widthCol, dtype=np.int64)
450 table[height] = np.array(heightCol, dtype=np.int64)
452 # Set some metadata in the table (force OBSTYPE to exist)
453 metadata = copy.copy(self.getMetadata())
454 metadata["OBSTYPE"] = self._OBSTYPE
455 metadata[SCHEMA_NAME_KEY] = "Simple"
456 metadata[SCHEMA_VERSION_KEY] = 1
457 table.setMetadata(metadata)
459 return table
461 def writeText(self, filename):
462 """Write the defects out to a text file with the specified name.
464 Parameters
465 ----------
466 filename : `str`
467 Name of the file to write. The file extension ".ecsv" will
468 always be used.
470 Returns
471 -------
472 used : `str`
473 The name of the file used to write the data (which may be
474 different from the supplied name given the change to file
475 extension).
477 Notes
478 -----
479 The file is written to ECSV format and will include any metadata
480 associated with the `Defects`.
481 """
483 # Using astropy table is the easiest way to serialize to ecsv
484 afwTable = self.toSimpleTable()
485 table = afwTable.asAstropy()
487 metadata = afwTable.getMetadata()
488 now = datetime.datetime.utcnow()
489 metadata["DATE"] = now.isoformat()
490 metadata["CALIB_CREATION_DATE"] = now.strftime("%Y-%m-%d")
491 metadata["CALIB_CREATION_TIME"] = now.strftime("%T %Z").strip()
493 table.meta = metadata.toDict()
495 # Force file extension to .ecsv
496 path, ext = os.path.splitext(filename)
497 filename = path + ".ecsv"
498 table.write(filename, format="ascii.ecsv")
499 return filename
501 @staticmethod
502 def _get_values(values, n=1):
503 """Retrieve N values from the supplied values.
505 Parameters
506 ----------
507 values : `numbers.Number` or `list` or `np.array`
508 Input values.
509 n : `int`
510 Number of values to retrieve.
512 Returns
513 -------
514 vals : `list` or `np.array` or `numbers.Number`
515 Single value from supplied list if ``n`` is 1, or `list`
516 containing first ``n`` values from supplied values.
518 Notes
519 -----
520 Some supplied tables have vectors in some columns that can also
521 be scalars. This method can be used to get the first number as
522 a scalar or the first N items from a vector as a vector.
523 """
524 if n == 1:
525 if isinstance(values, numbers.Number):
526 return values
527 else:
528 return values[0]
530 return values[:n]
532 @classmethod
533 def fromTable(cls, table):
534 """Construct a `Defects` from the contents of a
535 `~lsst.afw.table.BaseCatalog`.
537 Parameters
538 ----------
539 table : `lsst.afw.table.BaseCatalog`
540 Table with one row per defect.
542 Returns
543 -------
544 defects : `Defects`
545 A `Defects` list.
547 Notes
548 -----
549 Two table formats are recognized. The first is the
550 `FITS regions <https://fits.gsfc.nasa.gov/registry/region.html>`_
551 definition tabular format written by `toFitsRegionTable` where the
552 pixel origin is corrected from FITS 1-based to a 0-based origin.
553 The second is the legacy defects format using columns ``x0``, ``y0``
554 (bottom left hand pixel of box in 0-based coordinates), ``width``
555 and ``height``.
557 The FITS standard regions can only read BOX, POINT, or ROTBOX with
558 a zero degree rotation.
559 """
561 defectList = []
563 schema = table.getSchema()
565 # Check schema to see which definitions we have
566 if "X" in schema and "Y" in schema and "R" in schema and "SHAPE" in schema:
567 # This is a FITS region style table
568 isFitsRegion = True
570 # Preselect the keys
571 xKey = schema["X"].asKey()
572 yKey = schema["Y"].asKey()
573 shapeKey = schema["SHAPE"].asKey()
574 rKey = schema["R"].asKey()
575 rotangKey = schema["ROTANG"].asKey()
577 elif "x0" in schema and "y0" in schema and "width" in schema and "height" in schema:
578 # This is a classic LSST-style defect table
579 isFitsRegion = False
581 # Preselect the keys
582 xKey = schema["x0"].asKey()
583 yKey = schema["y0"].asKey()
584 widthKey = schema["width"].asKey()
585 heightKey = schema["height"].asKey()
587 else:
588 raise ValueError("Unsupported schema for defects extraction")
590 for record in table:
592 if isFitsRegion:
593 # Coordinates can be arrays (some shapes in the standard
594 # require this)
595 # Correct for FITS 1-based origin
596 xcen = cls._get_values(record[xKey]) - 1.0
597 ycen = cls._get_values(record[yKey]) - 1.0
598 shape = record[shapeKey].upper()
599 if shape == "BOX":
600 box = lsst.geom.Box2I.makeCenteredBox(lsst.geom.Point2D(xcen, ycen),
601 lsst.geom.Extent2I(cls._get_values(record[rKey],
602 n=2)))
603 elif shape == "POINT":
604 # Handle the case where we have an externally created
605 # FITS file.
606 box = lsst.geom.Point2I(xcen, ycen)
607 elif shape == "ROTBOX":
608 # Astropy regions always writes ROTBOX
609 rotang = cls._get_values(record[rotangKey])
610 # We can support 0 or 90 deg
611 if math.isclose(rotang % 90.0, 0.0):
612 # Two values required
613 r = cls._get_values(record[rKey], n=2)
614 if math.isclose(rotang % 180.0, 0.0):
615 width = r[0]
616 height = r[1]
617 else:
618 width = r[1]
619 height = r[0]
620 box = lsst.geom.Box2I.makeCenteredBox(lsst.geom.Point2D(xcen, ycen),
621 lsst.geom.Extent2I(width, height))
622 else:
623 log.warning("Defect can not be defined using ROTBOX with non-aligned rotation angle")
624 continue
625 else:
626 log.warning("Defect lists can only be defined using BOX or POINT not %s", shape)
627 continue
629 else:
630 # This is a classic LSST-style defect table
631 box = lsst.geom.Box2I(lsst.geom.Point2I(record[xKey], record[yKey]),
632 lsst.geom.Extent2I(record[widthKey], record[heightKey]))
634 defectList.append(box)
636 defects = cls(defectList)
637 defects.setMetadata(table.getMetadata())
639 # Once read, the schema headers are irrelevant
640 metadata = defects.getMetadata()
641 for k in (SCHEMA_NAME_KEY, SCHEMA_VERSION_KEY):
642 if k in metadata:
643 del metadata[k]
645 return defects
647 @classmethod
648 def readFits(cls, *args):
649 """Read defect list from FITS table.
651 Parameters
652 ----------
653 *args
654 Arguments to be forwarded to
655 `lsst.afw.table.BaseCatalog.writeFits`.
657 Returns
658 -------
659 defects : `Defects`
660 Defects read from a FITS table.
661 """
662 table = lsst.afw.table.BaseCatalog.readFits(*args)
663 return cls.fromTable(table)
665 @classmethod
666 def readText(cls, filename):
667 """Read defect list from standard format text table file.
669 Parameters
670 ----------
671 filename : `str`
672 Name of the file containing the defects definitions.
674 Returns
675 -------
676 defects : `Defects`
677 Defects read from a FITS table.
678 """
679 with warnings.catch_warnings():
680 # Squash warnings due to astropy failure to close files; we think
681 # this is a real problem, but the warnings are even worse.
682 # https://github.com/astropy/astropy/issues/8673
683 warnings.filterwarnings("ignore", category=ResourceWarning, module="astropy.io.ascii")
684 table = astropy.table.Table.read(filename)
686 # Need to convert the Astropy table to afw table
687 schema = lsst.afw.table.Schema()
688 for colName in table.columns:
689 schema.addField(colName, units=str(table[colName].unit),
690 type=table[colName].dtype.type)
692 # Create AFW table that is required by fromTable()
693 afwTable = lsst.afw.table.BaseCatalog(schema)
695 afwTable.resize(len(table))
696 for colName in table.columns:
697 # String columns will fail -- currently we do not expect any
698 afwTable[colName] = table[colName]
700 # Copy in the metadata from the astropy table
701 metadata = PropertyList()
702 for k, v in table.meta.items():
703 metadata[k] = v
704 afwTable.setMetadata(metadata)
706 # Extract defect information from the table itself
707 return cls.fromTable(afwTable)
709 @classmethod
710 def readLsstDefectsFile(cls, filename):
711 """Read defects information from a legacy LSST format text file.
713 Parameters
714 ----------
715 filename : `str`
716 Name of text file containing the defect information.
718 Returns
719 -------
720 defects : `Defects`
721 The defects.
723 Notes
724 -----
725 These defect text files are used as the human readable definitions
726 of defects in calibration data definition repositories. The format
727 is to use four columns defined as follows:
729 x0 : `int`
730 X coordinate of bottom left corner of box.
731 y0 : `int`
732 Y coordinate of bottom left corner of box.
733 width : `int`
734 X extent of the box.
735 height : `int`
736 Y extent of the box.
738 Files of this format were used historically to represent defects
739 in simple text form. Use `Defects.readText` and `Defects.writeText`
740 to use the more modern format.
741 """
742 # Use loadtxt so that ValueError is thrown if the file contains a
743 # non-integer value. genfromtxt converts bad values to -1.
744 defect_array = np.loadtxt(filename,
745 dtype=[("x0", "int"), ("y0", "int"),
746 ("x_extent", "int"), ("y_extent", "int")])
748 return cls(lsst.geom.Box2I(lsst.geom.Point2I(row["x0"], row["y0"]),
749 lsst.geom.Extent2I(row["x_extent"], row["y_extent"]))
750 for row in defect_array)
752 @classmethod
753 def fromFootprintList(cls, fpList):
754 """Compute a defect list from a footprint list, optionally growing
755 the footprints.
757 Parameters
758 ----------
759 fpList : `list` of `lsst.afw.detection.Footprint`
760 Footprint list to process.
762 Returns
763 -------
764 defects : `Defects`
765 List of defects.
766 """
767 # normalize_on_init is set to False to avoid recursively calling
768 # fromMask/fromFootprintList in Defects.__init__.
769 return cls(itertools.chain.from_iterable(lsst.afw.detection.footprintToBBoxList(fp)
770 for fp in fpList), normalize_on_init=False)
772 @classmethod
773 def fromMask(cls, maskedImage, maskName):
774 """Compute a defect list from a specified mask plane.
776 Parameters
777 ----------
778 maskedImage : `lsst.afw.image.MaskedImage`
779 Image to process.
780 maskName : `str` or `list`
781 Mask plane name, or list of names to convert.
783 Returns
784 -------
785 defects : `Defects`
786 Defect list constructed from masked pixels.
787 """
788 mask = maskedImage.getMask()
789 thresh = lsst.afw.detection.Threshold(mask.getPlaneBitMask(maskName),
790 lsst.afw.detection.Threshold.BITMASK)
791 fpList = lsst.afw.detection.FootprintSet(mask, thresh).getFootprints()
792 return cls.fromFootprintList(fpList)