23 """Support for image defects"""
25 __all__ = (
"Defects",)
29 import collections.abc
49 log = logging.getLogger(__name__)
51 SCHEMA_NAME_KEY =
"DEFECTS_SCHEMA"
52 SCHEMA_VERSION_KEY =
"DEFECTS_SCHEMA_VERSION"
55 class Defects(collections.abc.MutableSequence):
56 """Collection of `lsst.meas.algorithms.Defect`.
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.
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
82 """The calibration type used for ingest."""
84 def __init__(self, defectList=None, metadata=None, *, normalize_on_init=True):
87 if defectList
is not None:
96 if metadata
is not None:
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.
112 new : `~lsst.meas.algorithms.Defect`
113 Either the supplied value or a new object derived from it.
118 Raised if the supplied value can not be converted to
119 `~lsst.meas.algorithms.Defect`
121 if isinstance(value, Defect):
124 value = Defect(value)
128 value = Defect(value.getBBox())
130 raise ValueError(f
"Defects must be of type Defect, BoxI, or PointI, not '{value!r}'")
140 """Can be given a `~lsst.meas.algorithms.Defect` or a `lsst.geom.BoxI`
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.
157 if not isinstance(other, self.__class__):
161 if len(self) != len(other):
165 for d1, d2
in zip(self, other):
166 if d1.getBBox() != d2.getBBox():
172 return "Defects(" +
",".join(str(d.getBBox())
for d
in self) +
")"
174 def _normalize(self):
175 """Recalculate defect bounding boxes for efficiency.
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.
189 minX, minY, maxX, maxY = float(
'inf'), float(
'inf'), float(
'-inf'), float(
'-inf')
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())
200 mi = lsst.afw.image.MaskedImageF(region)
202 self.
_defects = Defects.fromMask(mi,
"BAD")._defects
204 @contextlib.contextmanager
206 """Temporarily suspend normalization of the defect list.
220 """Retrieve metadata associated with these `Defects`.
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
232 """Store a copy of the supplied metadata with the defects.
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.
250 """Copy the defects to a new list, creating new defects from the
256 New list with new `Defect` entries.
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.
264 return self.__class__(d.getBBox()
for d
in self)
267 """Make a transposed copy of this defect list.
271 retDefectList : `Defects`
272 Transposed list of defects.
274 retDefectList = self.__class__()
276 bbox = defect.getBBox()
277 dimensions = bbox.getDimensions()
280 retDefectList.append(nbbox)
284 """Set mask plane based on these defects.
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.
294 mask = maskedImage.getMask()
295 bitmask = mask.getPlaneBitMask(maskName)
297 bbox = defect.getBBox()
301 """Convert defect list to `~lsst.afw.table.BaseCatalog` using the
302 FITS region standard.
306 table : `lsst.afw.table.BaseCatalog`
307 Defects in tabular form.
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.
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")
337 for i, defect
in enumerate(self.
_defects):
338 box = defect.getBBox()
339 center = box.getCenter()
341 xCol.append(center.getX() + 1.0)
342 yCol.append(center.getY() + 1.0)
347 if width == 1
and height == 1:
354 table[i][shape] = shapeType
356 rCol.append(np.array([width, height], dtype=np.float64))
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)
369 metadata[SCHEMA_NAME_KEY] =
"FITS Region"
370 metadata[SCHEMA_VERSION_KEY] = 1
371 table.setMetadata(metadata)
376 """Write defect list to FITS.
381 Arguments to be forwarded to
382 `lsst.afw.table.BaseCatalog.writeFits`.
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)
396 """Convert defects to a simple table form that we use to write
401 table : `lsst.afw.table.BaseCatalog`
402 Defects in simple tabular form.
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:
411 X coordinate of bottom left corner of box.
413 Y coordinate of bottom left corner of box.
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")
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)
455 metadata[SCHEMA_NAME_KEY] =
"Simple"
456 metadata[SCHEMA_VERSION_KEY] = 1
457 table.setMetadata(metadata)
462 """Write the defects out to a text file with the specified name.
467 Name of the file to write. The file extension ".ecsv" will
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
479 The file is written to ECSV format and will include any metadata
480 associated with the `Defects`.
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()
496 path, ext = os.path.splitext(filename)
497 filename = path +
".ecsv"
498 table.write(filename, format=
"ascii.ecsv")
502 def _get_values(values, n=1):
503 """Retrieve N values from the supplied values.
507 values : `numbers.Number` or `list` or `np.array`
510 Number of values to retrieve.
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.
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.
525 if isinstance(values, numbers.Number):
534 """Construct a `Defects` from the contents of a
535 `~lsst.afw.table.BaseCatalog`.
539 table : `lsst.afw.table.BaseCatalog`
540 Table with one row per defect.
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``
557 The FITS standard regions can only read BOX, POINT, or ROTBOX with
558 a zero degree rotation.
563 schema = table.getSchema()
566 if "X" in schema
and "Y" in schema
and "R" in schema
and "SHAPE" in schema:
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:
582 xKey = schema[
"x0"].asKey()
583 yKey = schema[
"y0"].asKey()
584 widthKey = schema[
"width"].asKey()
585 heightKey = schema[
"height"].asKey()
588 raise ValueError(
"Unsupported schema for defects extraction")
598 shape = record[shapeKey].upper()
603 elif shape ==
"POINT":
607 elif shape ==
"ROTBOX":
611 if math.isclose(rotang % 90.0, 0.0):
614 if math.isclose(rotang % 180.0, 0.0):
623 log.warning(
"Defect can not be defined using ROTBOX with non-aligned rotation angle")
626 log.warning(
"Defect lists can only be defined using BOX or POINT not %s", shape)
634 defectList.append(box)
636 defects =
cls(defectList)
637 defects.setMetadata(table.getMetadata())
640 metadata = defects.getMetadata()
641 for k
in (SCHEMA_NAME_KEY, SCHEMA_VERSION_KEY):
649 """Read defect list from FITS table.
654 Arguments to be forwarded to
655 `lsst.afw.table.BaseCatalog.writeFits`.
660 Defects read from a FITS table.
662 table = lsst.afw.table.BaseCatalog.readFits(*args)
667 """Read defect list from standard format text table file.
672 Name of the file containing the defects definitions.
677 Defects read from a FITS table.
679 with warnings.catch_warnings():
683 warnings.filterwarnings(
"ignore", category=ResourceWarning, module=
"astropy.io.ascii")
684 table = astropy.table.Table.read(filename)
688 for colName
in table.columns:
689 schema.addField(colName, units=str(table[colName].unit),
690 type=table[colName].dtype.type)
695 afwTable.resize(len(table))
696 for colName
in table.columns:
698 afwTable[colName] = table[colName]
702 for k, v
in table.meta.items():
704 afwTable.setMetadata(metadata)
711 """Read defects information from a legacy LSST format text file.
716 Name of text file containing the defect information.
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:
730 X coordinate of bottom left corner of box.
732 Y coordinate of bottom left corner of 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.
744 defect_array = np.loadtxt(filename,
745 dtype=[(
"x0",
"int"), (
"y0",
"int"),
746 (
"x_extent",
"int"), (
"y_extent",
"int")])
750 for row
in defect_array)
754 """Compute a defect list from a footprint list, optionally growing
759 fpList : `list` of `lsst.afw.detection.Footprint`
760 Footprint list to process.
770 for fp
in fpList), normalize_on_init=
False)
774 """Compute a defect list from a specified mask plane.
778 maskedImage : `lsst.afw.image.MaskedImage`
780 maskName : `str` or `list`
781 Mask plane name, or list of names to convert.
786 Defect list constructed from masked pixels.
788 mask = maskedImage.getMask()
790 lsst.afw.detection.Threshold.BITMASK)