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