Coverage for python/lsst/cp/pipe/defects.py: 15%
276 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-23 02:31 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-23 02:31 -0800
1# This file is part of cp_pipe.
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/>.
21#
22import numpy as np
24import lsst.pipe.base as pipeBase
25import lsst.pipe.base.connectionTypes as cT
27from lsstDebug import getDebugFrame
28import lsst.pex.config as pexConfig
30import lsst.afw.image as afwImage
31import lsst.afw.math as afwMath
32import lsst.afw.detection as afwDetection
33import lsst.afw.display as afwDisplay
34from lsst.afw import cameraGeom
35from lsst.geom import Box2I, Point2I
36from lsst.meas.algorithms import SourceDetectionTask
37from lsst.ip.isr import Defects, countMaskedPixels
38from lsst.pex.exceptions import InvalidParameterError
40from ._lookupStaticCalibration import lookupStaticCalibration
42__all__ = ['MeasureDefectsTaskConfig', 'MeasureDefectsTask',
43 'MergeDefectsTaskConfig', 'MergeDefectsTask', ]
46class MeasureDefectsConnections(pipeBase.PipelineTaskConnections,
47 dimensions=("instrument", "exposure", "detector")):
48 inputExp = cT.Input(
49 name="defectExps",
50 doc="Input ISR-processed exposures to measure.",
51 storageClass="Exposure",
52 dimensions=("instrument", "detector", "exposure"),
53 multiple=False
54 )
55 camera = cT.PrerequisiteInput(
56 name='camera',
57 doc="Camera associated with this exposure.",
58 storageClass="Camera",
59 dimensions=("instrument", ),
60 isCalibration=True,
61 lookupFunction=lookupStaticCalibration,
62 )
64 outputDefects = cT.Output(
65 name="singleExpDefects",
66 doc="Output measured defects.",
67 storageClass="Defects",
68 dimensions=("instrument", "detector", "exposure"),
69 )
72class MeasureDefectsTaskConfig(pipeBase.PipelineTaskConfig,
73 pipelineConnections=MeasureDefectsConnections):
74 """Configuration for measuring defects from a list of exposures
75 """
77 nSigmaBright = pexConfig.Field(
78 dtype=float,
79 doc=("Number of sigma above mean for bright pixel detection. The default value was found to be"
80 " appropriate for some LSST sensors in DM-17490."),
81 default=4.8,
82 )
83 nSigmaDark = pexConfig.Field(
84 dtype=float,
85 doc=("Number of sigma below mean for dark pixel detection. The default value was found to be"
86 " appropriate for some LSST sensors in DM-17490."),
87 default=-5.0,
88 )
89 nPixBorderUpDown = pexConfig.Field(
90 dtype=int,
91 doc="Number of pixels to exclude from top & bottom of image when looking for defects.",
92 default=7,
93 )
94 nPixBorderLeftRight = pexConfig.Field(
95 dtype=int,
96 doc="Number of pixels to exclude from left & right of image when looking for defects.",
97 default=7,
98 )
99 badOnAndOffPixelColumnThreshold = pexConfig.Field(
100 dtype=int,
101 doc=("If BPC is the set of all the bad pixels in a given column (not necessarily consecutive) "
102 "and the size of BPC is at least 'badOnAndOffPixelColumnThreshold', all the pixels between the "
103 "pixels that satisfy minY (BPC) and maxY (BPC) will be marked as bad, with 'Y' being the long "
104 "axis of the amplifier (and 'X' the other axis, which for a column is a constant for all "
105 "pixels in the set BPC). If there are more than 'goodPixelColumnGapThreshold' consecutive "
106 "non-bad pixels in BPC, an exception to the above is made and those consecutive "
107 "'goodPixelColumnGapThreshold' are not marked as bad."),
108 default=50,
109 )
110 goodPixelColumnGapThreshold = pexConfig.Field(
111 dtype=int,
112 doc=("Size, in pixels, of usable consecutive pixels in a column with on and off bad pixels (see "
113 "'badOnAndOffPixelColumnThreshold')."),
114 default=30,
115 )
117 def validate(self):
118 super().validate()
119 if self.nSigmaBright < 0.0:
120 raise ValueError("nSigmaBright must be above 0.0.")
121 if self.nSigmaDark > 0.0:
122 raise ValueError("nSigmaDark must be below 0.0.")
125class MeasureDefectsTask(pipeBase.PipelineTask):
126 """Measure the defects from one exposure.
127 """
129 ConfigClass = MeasureDefectsTaskConfig
130 _DefaultName = 'cpDefectMeasure'
132 def run(self, inputExp, camera):
133 """Measure one exposure for defects.
135 Parameters
136 ----------
137 inputExp : `lsst.afw.image.Exposure`
138 Exposure to examine.
139 camera : `lsst.afw.cameraGeom.Camera`
140 Camera to use for metadata.
142 Returns
143 -------
144 results : `lsst.pipe.base.Struct`
145 Results struct containing:
147 ``outputDefects``
148 The defects measured from this exposure
149 (`lsst.ip.isr.Defects`).
150 """
151 detector = inputExp.getDetector()
153 filterName = inputExp.getFilter().physicalLabel
154 datasetType = inputExp.getMetadata().get('IMGTYPE', 'UNKNOWN')
156 if datasetType.lower() == 'dark':
157 nSigmaList = [self.config.nSigmaBright]
158 else:
159 nSigmaList = [self.config.nSigmaBright, self.config.nSigmaDark]
160 defects = self.findHotAndColdPixels(inputExp, nSigmaList)
162 msg = "Found %s defects containing %s pixels in %s"
163 self.log.info(msg, len(defects), self._nPixFromDefects(defects), datasetType)
165 defects.updateMetadata(camera=camera, detector=detector, filterName=filterName,
166 setCalibId=True, setDate=True,
167 cpDefectGenImageType=datasetType)
169 return pipeBase.Struct(
170 outputDefects=defects,
171 )
173 @staticmethod
174 def _nPixFromDefects(defects):
175 """Count pixels in a defect.
177 Parameters
178 ----------
179 defects : `lsst.ip.isr.Defects`
180 Defects to measure.
182 Returns
183 -------
184 nPix : `int`
185 Number of defect pixels.
186 """
187 nPix = 0
188 for defect in defects:
189 nPix += defect.getBBox().getArea()
190 return nPix
192 def findHotAndColdPixels(self, exp, nSigma):
193 """Find hot and cold pixels in an image.
195 Using config-defined thresholds on a per-amp basis, mask
196 pixels that are nSigma above threshold in dark frames (hot
197 pixels), or nSigma away from the clipped mean in flats (hot &
198 cold pixels).
200 Parameters
201 ----------
202 exp : `lsst.afw.image.exposure.Exposure`
203 The exposure in which to find defects.
204 nSigma : `list` [`float`]
205 Detection threshold to use. Positive for DETECTED pixels,
206 negative for DETECTED_NEGATIVE pixels.
208 Returns
209 -------
210 defects : `lsst.ip.isr.Defects`
211 The defects found in the image.
212 """
213 self._setEdgeBits(exp)
214 maskedIm = exp.maskedImage
216 # the detection polarity for afwDetection, True for positive,
217 # False for negative, and therefore True for darks as they only have
218 # bright pixels, and both for flats, as they have bright and dark pix
219 footprintList = []
221 for amp in exp.getDetector():
222 ampImg = maskedIm[amp.getBBox()].clone()
224 # crop ampImage depending on where the amp lies in the image
225 if self.config.nPixBorderLeftRight:
226 if ampImg.getX0() == 0:
227 ampImg = ampImg[self.config.nPixBorderLeftRight:, :, afwImage.LOCAL]
228 else:
229 ampImg = ampImg[:-self.config.nPixBorderLeftRight, :, afwImage.LOCAL]
230 if self.config.nPixBorderUpDown:
231 if ampImg.getY0() == 0:
232 ampImg = ampImg[:, self.config.nPixBorderUpDown:, afwImage.LOCAL]
233 else:
234 ampImg = ampImg[:, :-self.config.nPixBorderUpDown, afwImage.LOCAL]
236 if self._getNumGoodPixels(ampImg) == 0: # amp contains no usable pixels
237 continue
239 # Remove a background estimate
240 ampImg -= afwMath.makeStatistics(ampImg, afwMath.MEANCLIP, ).getValue()
242 mergedSet = None
243 for sigma in nSigma:
244 nSig = np.abs(sigma)
245 self.debugHistogram('ampFlux', ampImg, nSig, exp)
246 polarity = {-1: False, 1: True}[np.sign(sigma)]
248 threshold = afwDetection.createThreshold(nSig, 'stdev', polarity=polarity)
250 try:
251 footprintSet = afwDetection.FootprintSet(ampImg, threshold)
252 except InvalidParameterError:
253 # This occurs if the image sigma value is 0.0.
254 # Let's mask the whole area.
255 minValue = np.nanmin(ampImg.image.array) - 1.0
256 threshold = afwDetection.createThreshold(minValue, 'value', polarity=True)
257 footprintSet = afwDetection.FootprintSet(ampImg, threshold)
259 footprintSet.setMask(maskedIm.mask, ("DETECTED" if polarity else "DETECTED_NEGATIVE"))
261 if mergedSet is None:
262 mergedSet = footprintSet
263 else:
264 mergedSet.merge(footprintSet)
266 footprintList += mergedSet.getFootprints()
268 self.debugView('defectMap', ampImg,
269 Defects.fromFootprintList(mergedSet.getFootprints()), exp.getDetector())
271 defects = Defects.fromFootprintList(footprintList)
272 defects = self.maskBlocksIfIntermitentBadPixelsInColumn(defects)
274 return defects
276 @staticmethod
277 def _getNumGoodPixels(maskedIm, badMaskString="NO_DATA"):
278 """Return the number of non-bad pixels in the image."""
279 nPixels = maskedIm.mask.array.size
280 nBad = countMaskedPixels(maskedIm, badMaskString)
281 return nPixels - nBad
283 def _setEdgeBits(self, exposureOrMaskedImage, maskplaneToSet='EDGE'):
284 """Set edge bits on an exposure or maskedImage.
286 Raises
287 ------
288 TypeError
289 Raised if parameter ``exposureOrMaskedImage`` is an invalid type.
290 """
291 if isinstance(exposureOrMaskedImage, afwImage.Exposure):
292 mi = exposureOrMaskedImage.maskedImage
293 elif isinstance(exposureOrMaskedImage, afwImage.MaskedImage):
294 mi = exposureOrMaskedImage
295 else:
296 t = type(exposureOrMaskedImage)
297 raise TypeError(f"Function supports exposure or maskedImage but not {t}")
299 MASKBIT = mi.mask.getPlaneBitMask(maskplaneToSet)
300 if self.config.nPixBorderLeftRight:
301 mi.mask[: self.config.nPixBorderLeftRight, :, afwImage.LOCAL] |= MASKBIT
302 mi.mask[-self.config.nPixBorderLeftRight:, :, afwImage.LOCAL] |= MASKBIT
303 if self.config.nPixBorderUpDown:
304 mi.mask[:, : self.config.nPixBorderUpDown, afwImage.LOCAL] |= MASKBIT
305 mi.mask[:, -self.config.nPixBorderUpDown:, afwImage.LOCAL] |= MASKBIT
307 def maskBlocksIfIntermitentBadPixelsInColumn(self, defects):
308 """Mask blocks in a column if there are on-and-off bad pixels
310 If there's a column with on and off bad pixels, mask all the
311 pixels in between, except if there is a large enough gap of
312 consecutive good pixels between two bad pixels in the column.
314 Parameters
315 ----------
316 defects : `lsst.ip.isr.Defects`
317 The defects found in the image so far
319 Returns
320 -------
321 defects : `lsst.ip.isr.Defects`
322 If the number of bad pixels in a column is not larger or
323 equal than self.config.badPixelColumnThreshold, the input
324 list is returned. Otherwise, the defects list returned
325 will include boxes that mask blocks of on-and-of pixels.
326 """
327 # Get the (x, y) values of each bad pixel in amp.
328 coordinates = []
329 for defect in defects:
330 bbox = defect.getBBox()
331 x0, y0 = bbox.getMinX(), bbox.getMinY()
332 deltaX0, deltaY0 = bbox.getDimensions()
333 for j in np.arange(y0, y0+deltaY0):
334 for i in np.arange(x0, x0 + deltaX0):
335 coordinates.append((i, j))
337 x, y = [], []
338 for coordinatePair in coordinates:
339 x.append(coordinatePair[0])
340 y.append(coordinatePair[1])
342 x = np.array(x)
343 y = np.array(y)
344 # Find the defects with same "x" (vertical) coordinate (column).
345 unique, counts = np.unique(x, return_counts=True)
346 multipleX = []
347 for (a, b) in zip(unique, counts):
348 if b >= self.config.badOnAndOffPixelColumnThreshold:
349 multipleX.append(a)
350 if len(multipleX) != 0:
351 defects = self._markBlocksInBadColumn(x, y, multipleX, defects)
353 return defects
355 def _markBlocksInBadColumn(self, x, y, multipleX, defects):
356 """Mask blocks in a column if number of on-and-off bad pixels is above
357 threshold.
359 This function is called if the number of on-and-off bad pixels
360 in a column is larger or equal than
361 self.config.badOnAndOffPixelColumnThreshold.
363 Parameters
364 ---------
365 x : `list`
366 Lower left x coordinate of defect box. x coordinate is
367 along the short axis if amp.
368 y : `list`
369 Lower left y coordinate of defect box. x coordinate is
370 along the long axis if amp.
371 multipleX : list
372 List of x coordinates in amp. with multiple bad pixels
373 (i.e., columns with defects).
374 defects : `lsst.ip.isr.Defects`
375 The defcts found in the image so far
377 Returns
378 -------
379 defects : `lsst.ip.isr.Defects`
380 The defects list returned that will include boxes that
381 mask blocks of on-and-of pixels.
382 """
383 with defects.bulk_update():
384 goodPixelColumnGapThreshold = self.config.goodPixelColumnGapThreshold
385 for x0 in multipleX:
386 index = np.where(x == x0)
387 multipleY = y[index] # multipleY and multipleX are in 1-1 correspondence.
388 minY, maxY = np.min(multipleY), np.max(multipleY)
389 # Next few lines: don't mask pixels in column if gap
390 # of good pixels between two consecutive bad pixels is
391 # larger or equal than 'goodPixelColumnGapThreshold'.
392 diffIndex = np.where(np.diff(multipleY) >= goodPixelColumnGapThreshold)[0]
393 if len(diffIndex) != 0:
394 limits = [minY] # put the minimum first
395 for gapIndex in diffIndex:
396 limits.append(multipleY[gapIndex])
397 limits.append(multipleY[gapIndex+1])
398 limits.append(maxY) # maximum last
399 assert len(limits)%2 == 0, 'limits is even by design, but check anyways'
400 for i in np.arange(0, len(limits)-1, 2):
401 s = Box2I(minimum=Point2I(x0, limits[i]), maximum=Point2I(x0, limits[i+1]))
402 defects.append(s)
403 else: # No gap is large enough
404 s = Box2I(minimum=Point2I(x0, minY), maximum=Point2I(x0, maxY))
405 defects.append(s)
406 return defects
408 def debugView(self, stepname, ampImage, defects, detector): # pragma: no cover
409 """Plot the defects found by the task.
411 Parameters
412 ----------
413 stepname : `str`
414 Debug frame to request.
415 ampImage : `lsst.afw.image.MaskedImage`
416 Amplifier image to display.
417 defects : `lsst.ip.isr.Defects`
418 The defects to plot.
419 detector : `lsst.afw.cameraGeom.Detector`
420 Detector holding camera geometry.
421 """
422 frame = getDebugFrame(self._display, stepname)
423 if frame:
424 disp = afwDisplay.Display(frame=frame)
425 disp.scale('asinh', 'zscale')
426 disp.setMaskTransparency(80)
427 disp.setMaskPlaneColor("BAD", afwDisplay.RED)
429 maskedIm = ampImage.clone()
430 defects.maskPixels(maskedIm, "BAD")
432 mpDict = maskedIm.mask.getMaskPlaneDict()
433 for plane in mpDict.keys():
434 if plane in ['BAD']:
435 continue
436 disp.setMaskPlaneColor(plane, afwDisplay.IGNORE)
438 disp.setImageColormap('gray')
439 disp.mtv(maskedIm)
440 cameraGeom.utils.overlayCcdBoxes(detector, isTrimmed=True, display=disp)
441 prompt = "Press Enter to continue [c]... "
442 while True:
443 ans = input(prompt).lower()
444 if ans in ('', 'c', ):
445 break
447 def debugHistogram(self, stepname, ampImage, nSigmaUsed, exp):
448 """Make a histogram of the distribution of pixel values for
449 each amp.
451 The main image data histogram is plotted in blue. Edge
452 pixels, if masked, are in red. Note that masked edge pixels
453 do not contribute to the underflow and overflow numbers.
455 Note that this currently only supports the 16-amp LSST
456 detectors.
458 Parameters
459 ----------
460 stepname : `str`
461 Debug frame to request.
462 ampImage : `lsst.afw.image.MaskedImage`
463 Amplifier image to display.
464 nSigmaUsed : `float`
465 The number of sigma used for detection
466 exp : `lsst.afw.image.exposure.Exposure`
467 The exposure in which the defects were found.
468 """
469 frame = getDebugFrame(self._display, stepname)
470 if frame:
471 import matplotlib.pyplot as plt
473 detector = exp.getDetector()
474 nX = np.floor(np.sqrt(len(detector)))
475 nY = len(detector) // nX
476 fig, ax = plt.subplots(nrows=int(nY), ncols=int(nX), sharex='col', sharey='row', figsize=(13, 10))
478 expTime = exp.getInfo().getVisitInfo().getExposureTime()
480 for (amp, a) in zip(reversed(detector), ax.flatten()):
481 mi = exp.maskedImage[amp.getBBox()]
483 # normalize by expTime as we plot in ADU/s and don't
484 # always work with master calibs
485 mi.image.array /= expTime
486 stats = afwMath.makeStatistics(mi, afwMath.MEANCLIP | afwMath.STDEVCLIP)
487 mean, sigma = stats.getValue(afwMath.MEANCLIP), stats.getValue(afwMath.STDEVCLIP)
488 # Get array of pixels
489 EDGEBIT = exp.maskedImage.mask.getPlaneBitMask("EDGE")
490 imgData = mi.image.array[(mi.mask.array & EDGEBIT) == 0].flatten()
491 edgeData = mi.image.array[(mi.mask.array & EDGEBIT) != 0].flatten()
493 thrUpper = mean + nSigmaUsed*sigma
494 thrLower = mean - nSigmaUsed*sigma
496 nRight = len(imgData[imgData > thrUpper])
497 nLeft = len(imgData[imgData < thrLower])
499 nsig = nSigmaUsed + 1.2 # add something small so the edge of the plot is out from level used
500 leftEdge = mean - nsig * nSigmaUsed*sigma
501 rightEdge = mean + nsig * nSigmaUsed*sigma
502 nbins = np.linspace(leftEdge, rightEdge, 1000)
503 ey, bin_borders, patches = a.hist(edgeData, histtype='step', bins=nbins,
504 lw=1, edgecolor='red')
505 y, bin_borders, patches = a.hist(imgData, histtype='step', bins=nbins,
506 lw=3, edgecolor='blue')
508 # Report number of entries in over- and under-flow
509 # bins, i.e. off the edges of the histogram
510 nOverflow = len(imgData[imgData > rightEdge])
511 nUnderflow = len(imgData[imgData < leftEdge])
513 # Put v-lines and textboxes in
514 a.axvline(thrUpper, c='k')
515 a.axvline(thrLower, c='k')
516 msg = f"{amp.getName()}\nmean:{mean: .2f}\n$\\sigma$:{sigma: .2f}"
517 a.text(0.65, 0.6, msg, transform=a.transAxes, fontsize=11)
518 msg = f"nLeft:{nLeft}\nnRight:{nRight}\nnOverflow:{nOverflow}\nnUnderflow:{nUnderflow}"
519 a.text(0.03, 0.6, msg, transform=a.transAxes, fontsize=11.5)
521 # set axis limits and scales
522 a.set_ylim([1., 1.7*np.max(y)])
523 lPlot, rPlot = a.get_xlim()
524 a.set_xlim(np.array([lPlot, rPlot]))
525 a.set_yscale('log')
526 a.set_xlabel("ADU/s")
527 fig.show()
528 prompt = "Press Enter or c to continue [chp]..."
529 while True:
530 ans = input(prompt).lower()
531 if ans in ("", " ", "c",):
532 break
533 elif ans in ("p", ):
534 import pdb
535 pdb.set_trace()
536 elif ans in ("h", ):
537 print("[h]elp [c]ontinue [p]db")
538 plt.close()
541class MergeDefectsConnections(pipeBase.PipelineTaskConnections,
542 dimensions=("instrument", "detector")):
543 inputDefects = cT.Input(
544 name="singleExpDefects",
545 doc="Measured defect lists.",
546 storageClass="Defects",
547 dimensions=("instrument", "detector", "exposure"),
548 multiple=True,
549 )
550 camera = cT.PrerequisiteInput(
551 name='camera',
552 doc="Camera associated with these defects.",
553 storageClass="Camera",
554 dimensions=("instrument", ),
555 isCalibration=True,
556 lookupFunction=lookupStaticCalibration,
557 )
559 mergedDefects = cT.Output(
560 name="defects",
561 doc="Final merged defects.",
562 storageClass="Defects",
563 dimensions=("instrument", "detector"),
564 multiple=False,
565 isCalibration=True,
566 )
569class MergeDefectsTaskConfig(pipeBase.PipelineTaskConfig,
570 pipelineConnections=MergeDefectsConnections):
571 """Configuration for merging single exposure defects.
572 """
574 assertSameRun = pexConfig.Field(
575 dtype=bool,
576 doc=("Ensure that all visits are from the same run? Raises if this is not the case, or "
577 "if the run key isn't found."),
578 default=False, # false because most obs_packages don't have runs. obs_lsst/ts8 overrides this.
579 )
580 ignoreFilters = pexConfig.Field(
581 dtype=bool,
582 doc=("Set the filters used in the CALIB_ID to NONE regardless of the filters on the input"
583 " images. Allows mixing of filters in the input flats. Set to False if you think"
584 " your defects might be chromatic and want to have registry support for varying"
585 " defects with respect to filter."),
586 default=True,
587 )
588 nullFilterName = pexConfig.Field(
589 dtype=str,
590 doc=("The name of the null filter if ignoreFilters is True. Usually something like NONE or EMPTY"),
591 default="NONE",
592 )
593 combinationMode = pexConfig.ChoiceField(
594 doc="Which types of defects to identify",
595 dtype=str,
596 default="FRACTION",
597 allowed={
598 "AND": "Logical AND the pixels found in each visit to form set ",
599 "OR": "Logical OR the pixels found in each visit to form set ",
600 "FRACTION": "Use pixels found in more than config.combinationFraction of visits ",
601 }
602 )
603 combinationFraction = pexConfig.RangeField(
604 dtype=float,
605 doc=("The fraction (0..1) of visits in which a pixel was found to be defective across"
606 " the visit list in order to be marked as a defect. Note, upper bound is exclusive, so use"
607 " mode AND to require pixel to appear in all images."),
608 default=0.7,
609 min=0,
610 max=1,
611 )
612 edgesAsDefects = pexConfig.Field(
613 dtype=bool,
614 doc=("Mark all edge pixels, as defined by nPixBorder[UpDown, LeftRight], as defects."
615 " Normal treatment is to simply exclude this region from the defect finding, such that no"
616 " defect will be located there."),
617 default=False,
618 )
621class MergeDefectsTask(pipeBase.PipelineTask):
622 """Merge the defects from multiple exposures.
623 """
625 ConfigClass = MergeDefectsTaskConfig
626 _DefaultName = 'cpDefectMerge'
628 def run(self, inputDefects, camera):
629 """Merge a list of single defects to find the common defect regions.
631 Parameters
632 ----------
633 inputDefects : `list` [`lsst.ip.isr.Defects`]
634 Partial defects from a single exposure.
635 camera : `lsst.afw.cameraGeom.Camera`
636 Camera to use for metadata.
638 Returns
639 -------
640 results : `lsst.pipe.base.Struct`
641 Results struct containing:
643 ``mergedDefects``
644 The defects merged from the input lists
645 (`lsst.ip.isr.Defects`).
646 """
647 detectorId = inputDefects[0].getMetadata().get('DETECTOR', None)
648 if detectorId is None:
649 raise RuntimeError("Cannot identify detector id.")
650 detector = camera[detectorId]
652 imageTypes = set()
653 for inDefect in inputDefects:
654 imageType = inDefect.getMetadata().get('cpDefectGenImageType', 'UNKNOWN')
655 imageTypes.add(imageType)
657 # Determine common defect pixels separately for each input image type.
658 splitDefects = list()
659 for imageType in imageTypes:
660 sumImage = afwImage.MaskedImageF(detector.getBBox())
661 count = 0
662 for inDefect in inputDefects:
663 if imageType == inDefect.getMetadata().get('cpDefectGenImageType', 'UNKNOWN'):
664 count += 1
665 for defect in inDefect:
666 sumImage.image[defect.getBBox()] += 1.0
667 sumImage /= count
668 nDetected = len(np.where(sumImage.getImage().getArray() > 0)[0])
669 self.log.info("Pre-merge %s pixels with non-zero detections for %s" % (nDetected, imageType))
671 if self.config.combinationMode == 'AND':
672 threshold = 1.0
673 elif self.config.combinationMode == 'OR':
674 threshold = 0.0
675 elif self.config.combinationMode == 'FRACTION':
676 threshold = self.config.combinationFraction
677 else:
678 raise RuntimeError(f"Got unsupported combinationMode {self.config.combinationMode}")
679 indices = np.where(sumImage.getImage().getArray() > threshold)
680 BADBIT = sumImage.getMask().getPlaneBitMask('BAD')
681 sumImage.getMask().getArray()[indices] |= BADBIT
682 self.log.info("Post-merge %s pixels marked as defects for %s" % (len(indices[0]), imageType))
683 partialDefect = Defects.fromMask(sumImage, 'BAD')
684 splitDefects.append(partialDefect)
686 # Do final combination of separate image types
687 finalImage = afwImage.MaskedImageF(detector.getBBox())
688 for inDefect in splitDefects:
689 for defect in inDefect:
690 finalImage.image[defect.getBBox()] += 1
691 finalImage /= len(splitDefects)
692 nDetected = len(np.where(finalImage.getImage().getArray() > 0)[0])
693 self.log.info("Pre-final merge %s pixels with non-zero detections" % (nDetected, ))
695 # This combination is the OR of all image types
696 threshold = 0.0
697 indices = np.where(finalImage.getImage().getArray() > threshold)
698 BADBIT = finalImage.getMask().getPlaneBitMask('BAD')
699 finalImage.getMask().getArray()[indices] |= BADBIT
700 self.log.info("Post-final merge %s pixels marked as defects" % (len(indices[0]), ))
702 if self.config.edgesAsDefects:
703 self.log.info("Masking edge pixels as defects.")
704 # Do the same as IsrTask.maskEdges()
705 box = detector.getBBox()
706 subImage = finalImage[box]
707 box.grow(-self.nPixBorder)
708 SourceDetectionTask.setEdgeBits(subImage, box, BADBIT)
710 merged = Defects.fromMask(finalImage, 'BAD')
711 merged.updateMetadata(camera=camera, detector=detector, filterName=None,
712 setCalibId=True, setDate=True)
714 return pipeBase.Struct(
715 mergedDefects=merged,
716 )