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

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