23 """Support for image defects
26 __all__ = (
"Defects",)
30 import collections.abc
50 log = logging.getLogger(__name__)
52 SCHEMA_NAME_KEY =
"DEFECTS_SCHEMA"
53 SCHEMA_VERSION_KEY =
"DEFECTS_SCHEMA_VERSION"
56 class Defects(collections.abc.MutableSequence):
57 """Collection of `lsst.meas.algorithms.Defect`.
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.
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
83 """The calibration type used for ingest.
86 def __init__(self, defectList=None, metadata=None, *, normalize_on_init=True):
89 if defectList
is not None:
98 if metadata
is not None:
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.
114 new : `~lsst.meas.algorithms.Defect`
115 Either the supplied value or a new object derived from it.
120 Raised if the supplied value can not be converted to
121 `~lsst.meas.algorithms.Defect`
123 if isinstance(value, Defect):
126 value = Defect(value)
130 value = Defect(value.getBBox())
132 raise ValueError(f
"Defects must be of type Defect, BoxI, or PointI, not '{value!r}'")
142 """Can be given a `~lsst.meas.algorithms.Defect` or a `lsst.geom.BoxI`
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.
159 if not isinstance(other, self.__class__):
163 if len(self) != len(other):
167 for d1, d2
in zip(self, other):
168 if d1.getBBox() != d2.getBBox():
174 return "Defects(" +
",".join(str(d.getBBox())
for d
in self) +
")"
176 def _normalize(self):
177 """Recalculate defect bounding boxes for efficiency.
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.
191 minX, minY, maxX, maxY = float(
'inf'), float(
'inf'), float(
'-inf'), float(
'-inf')
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())
202 mi = lsst.afw.image.MaskedImageF(region)
204 self.
_defects = Defects.fromMask(mi,
"BAD")._defects
206 @contextlib.contextmanager
208 """Temporarily suspend normalization of the defect list.
222 """Retrieve metadata associated with these `Defects`.
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
234 """Store a copy of the supplied metadata with the defects.
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.
252 """Copy the defects to a new list, creating new defects from the
258 New list with new `Defect` entries.
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.
266 return self.__class__(d.getBBox()
for d
in self)
269 """Make a transposed copy of this defect list.
273 retDefectList : `Defects`
274 Transposed list of defects.
276 retDefectList = self.__class__()
278 bbox = defect.getBBox()
279 dimensions = bbox.getDimensions()
282 retDefectList.append(nbbox)
286 """Set mask plane based on these defects.
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.
296 mask = maskedImage.getMask()
297 bitmask = mask.getPlaneBitMask(maskName)
299 bbox = defect.getBBox()
303 """Convert defect list to `~lsst.afw.table.BaseCatalog` using the
304 FITS region standard.
308 table : `lsst.afw.table.BaseCatalog`
309 Defects in tabular form.
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.
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")
339 for i, defect
in enumerate(self.
_defects):
340 box = defect.getBBox()
341 center = box.getCenter()
343 xCol.append(center.getX() + 1.0)
344 yCol.append(center.getY() + 1.0)
349 if width == 1
and height == 1:
356 table[i][shape] = shapeType
358 rCol.append(np.array([width, height], dtype=np.float64))
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)
371 metadata[SCHEMA_NAME_KEY] =
"FITS Region"
372 metadata[SCHEMA_VERSION_KEY] = 1
373 table.setMetadata(metadata)
378 """Write defect list to FITS.
383 Arguments to be forwarded to
384 `lsst.afw.table.BaseCatalog.writeFits`.
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)
398 """Convert defects to a simple table form that we use to write
403 table : `lsst.afw.table.BaseCatalog`
404 Defects in simple tabular form.
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:
413 X coordinate of bottom left corner of box.
415 Y coordinate of bottom left corner of box.
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")
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)
457 metadata[SCHEMA_NAME_KEY] =
"Simple"
458 metadata[SCHEMA_VERSION_KEY] = 1
459 table.setMetadata(metadata)
464 """Write the defects out to a text file with the specified name.
469 Name of the file to write. The file extension ".ecsv" will
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
481 The file is written to ECSV format and will include any metadata
482 associated with the `Defects`.
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()
498 path, ext = os.path.splitext(filename)
499 filename = path +
".ecsv"
500 table.write(filename, format=
"ascii.ecsv")
504 def _get_values(values, n=1):
505 """Retrieve N values from the supplied values.
509 values : `numbers.Number` or `list` or `np.array`
512 Number of values to retrieve.
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.
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.
527 if isinstance(values, numbers.Number):
536 """Construct a `Defects` from the contents of a
537 `~lsst.afw.table.BaseCatalog`.
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
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``
564 The FITS standard regions can only read BOX, POINT, or ROTBOX with
565 a zero degree rotation.
570 schema = table.getSchema()
573 if "X" in schema
and "Y" in schema
and "R" in schema
and "SHAPE" in schema:
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:
589 xKey = schema[
"x0"].asKey()
590 yKey = schema[
"y0"].asKey()
591 widthKey = schema[
"width"].asKey()
592 heightKey = schema[
"height"].asKey()
595 raise ValueError(
"Unsupported schema for defects extraction")
605 shape = record[shapeKey].upper()
610 elif shape ==
"POINT":
614 elif shape ==
"ROTBOX":
618 if math.isclose(rotang % 90.0, 0.0):
621 if math.isclose(rotang % 180.0, 0.0):
630 log.warning(
"Defect can not be defined using ROTBOX with non-aligned rotation angle")
633 log.warning(
"Defect lists can only be defined using BOX or POINT not %s", shape)
641 defectList.append(box)
643 defects =
cls(defectList, normalize_on_init=normalize_on_init)
644 defects.setMetadata(table.getMetadata())
647 metadata = defects.getMetadata()
648 for k
in (SCHEMA_NAME_KEY, SCHEMA_VERSION_KEY):
656 """Read defect list from FITS table.
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
672 Defects read from a FITS table.
674 table = lsst.afw.table.BaseCatalog.readFits(*args)
675 return cls.
fromTable(table, normalize_on_init=normalize_on_init)
678 def readText(cls, filename, normalize_on_init=False):
679 """Read defect list from standard format text table file.
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
694 Defects read from a FITS table.
696 with warnings.catch_warnings():
700 warnings.filterwarnings(
"ignore", category=ResourceWarning, module=
"astropy.io.ascii")
701 table = astropy.table.Table.read(filename)
705 for colName
in table.columns:
706 schema.addField(colName, units=str(table[colName].unit),
707 type=table[colName].dtype.type)
712 afwTable.resize(len(table))
713 for colName
in table.columns:
715 afwTable[colName] = table[colName]
719 for k, v
in table.meta.items():
721 afwTable.setMetadata(metadata)
724 return cls.
fromTable(afwTable, normalize_on_init=normalize_on_init)
728 """Read defects information from a legacy LSST format text file.
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
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:
752 X coordinate of bottom left corner of box.
754 Y coordinate of bottom left corner of 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.
766 defect_array = np.loadtxt(filename,
767 dtype=[(
"x0",
"int"), (
"y0",
"int"),
768 (
"x_extent",
"int"), (
"y_extent",
"int")])
772 for row
in defect_array)
774 return cls(defects, normalize_on_init=normalize_on_init)
778 """Compute a defect list from a footprint list, optionally growing
783 fpList : `list` of `lsst.afw.detection.Footprint`
784 Footprint list to process.
794 for fp
in fpList), normalize_on_init=
False)
798 """Compute a defect list from a specified mask plane.
802 maskedImage : `lsst.afw.image.MaskedImage`
804 maskName : `str` or `list`
805 Mask plane name, or list of names to convert.
810 Defect list constructed from masked pixels.
812 mask = maskedImage.getMask()
814 lsst.afw.detection.Threshold.BITMASK)