23 __all__ = [
'FindDefectsTask',
24 'FindDefectsTaskConfig', ]
42 from .utils
import NonexistentDatasetTaskDataIdContainer, SingleVisitListTaskRunner, countMaskedPixels, \
47 """Config class for defect finding"""
49 isrForFlats = pexConfig.ConfigurableField(
51 doc=
"Task to perform instrumental signature removal",
53 isrForDarks = pexConfig.ConfigurableField(
55 doc=
"Task to perform instrumental signature removal",
57 isrMandatoryStepsFlats = pexConfig.ListField(
59 doc=(
"isr operations that must be performed for valid results when using flats."
60 " Raises if any of these are False"),
61 default=[
'doAssembleCcd',
'doFringe']
63 isrMandatoryStepsDarks = pexConfig.ListField(
65 doc=(
"isr operations that must be performed for valid results when using darks. "
66 "Raises if any of these are False"),
67 default=[
'doAssembleCcd',
'doFringe']
69 isrForbiddenStepsFlats = pexConfig.ListField(
71 doc=(
"isr operations that must NOT be performed for valid results when using flats."
72 " Raises if any of these are True"),
73 default=[
'doBrighterFatter',
'doUseOpticsTransmission',
74 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
76 isrForbiddenStepsDarks = pexConfig.ListField(
78 doc=(
"isr operations that must NOT be performed for valid results when using darks."
79 " Raises if any of these are True"),
80 default=[
'doBrighterFatter',
'doUseOpticsTransmission',
81 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
83 isrDesirableSteps = pexConfig.ListField(
85 doc=(
"isr operations that it is advisable to perform, but are not mission-critical."
86 " WARNs are logged for any of these found to be False."),
89 ccdKey = pexConfig.Field(
91 doc=
"The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'",
94 imageTypeKey = pexConfig.Field(
96 doc=
"The key for the butler to use by which to check whether images are darks or flats",
99 mode = pexConfig.ChoiceField(
100 doc=(
"Use single master calibs (flat and dark) for finding defects, or a list of raw visits?"
101 " If MASTER, a single visit number should be supplied, for which the corresponding master flat"
102 " and dark will be used. If VISITS, the list of visits will be used, treating the flats and "
103 " darks as appropriate, depending on their image types, as determined by their imageType from"
104 " config.imageTypeKey"),
108 "VISITS":
"Calculate defects from a list of raw visits",
109 "MASTER":
"Use the corresponding master calibs from the specified visit to measure defects",
112 nSigmaBright = pexConfig.Field(
114 doc=(
"Number of sigma above mean for bright pixel detection. The default value was found to be",
115 " appropriate for some LSST sensors in DM-17490."),
118 nSigmaDark = pexConfig.Field(
120 doc=(
"Number of sigma below mean for dark pixel detection. The default value was found to be",
121 " appropriate for some LSST sensors in DM-17490."),
124 nPixBorderUpDown = pexConfig.Field(
126 doc=
"Number of pixels to exclude from top & bottom of image when looking for defects.",
129 nPixBorderLeftRight = pexConfig.Field(
131 doc=
"Number of pixels to exclude from left & right of image when looking for defects.",
134 badOnAndOffPixelColumnThreshold = pexConfig.Field(
136 doc=(
"If BPC is the set of all the bad pixels in a given column (not necessarily consecutive) ",
137 "and the size of BPC is at least 'badOnAndOffPixelColumnThreshold', all the pixels between the ",
138 "pixels that satisfy minY (BPC) and maxY (BPC) will be marked as bad, with 'Y' being the long ",
139 "axis of the amplifier (and 'X' the other axis, which for a column is a constant for all ",
140 "pixels in the set BPC). If there are more than 'goodPixelColumnGapThreshold' consecutive ",
141 "non-bad pixels in BPC, an exception to the above is made and those consecutive ",
142 "'goodPixelColumnGapThreshold' are not marked as bad."),
145 goodPixelColumnGapThreshold = pexConfig.Field(
147 doc=(
"Size, in pixels, of usable consecutive pixels in a column with on and off bad pixels (see ",
148 "'badOnAndOffPixelColumnThreshold')."),
151 edgesAsDefects = pexConfig.Field(
153 doc=(
"Mark all edge pixels, as defined by nPixBorder[UpDown, LeftRight], as defects."
154 " Normal treatment is to simply exclude this region from the defect finding, such that no"
155 " defect will be located there."),
158 assertSameRun = pexConfig.Field(
160 doc=(
"Ensure that all visits are from the same run? Raises if this is not the case, or"
161 "if the run key isn't found."),
164 ignoreFilters = pexConfig.Field(
166 doc=(
"Set the filters used in the CALIB_ID to NONE regardless of the filters on the input"
167 " images. Allows mixing of filters in the input flats. Set to False if you think"
168 " your defects might be chromatic and want to have registry support for varying"
169 " defects with respect to filter."),
172 nullFilterName = pexConfig.Field(
174 doc=(
"The name of the null filter if ignoreFilters is True. Usually something like NONE or EMPTY"),
177 combinationMode = pexConfig.ChoiceField(
178 doc=
"Which types of defects to identify",
182 "AND":
"Logical AND the pixels found in each visit to form set ",
183 "OR":
"Logical OR the pixels found in each visit to form set ",
184 "FRACTION":
"Use pixels found in more than config.combinationFraction of visits ",
187 combinationFraction = pexConfig.RangeField(
189 doc=(
"The fraction (0..1) of visits in which a pixel was found to be defective across"
190 " the visit list in order to be marked as a defect. Note, upper bound is exclusive, so use"
191 " mode AND to require pixel to appear in all images."),
196 makePlots = pexConfig.Field(
198 doc=(
"Plot histograms for each visit for each amp (one plot per detector) and the final"
199 " defects overlaid on the sensor."),
202 writeAs = pexConfig.ChoiceField(
203 doc=
"Write the output file as ASCII or FITS table",
207 "ASCII":
"Write the output as an ASCII file",
208 "FITS":
"Write the output as an FITS table",
209 "BOTH":
"Write the output as both a FITS table and an ASCII file",
215 """Task for finding defects in sensors.
217 The task has two modes of operation, defect finding in raws and in
218 master calibrations, which work as follows.
220 Master calib defect finding
221 ----------------------------
223 A single visit number is supplied, for which the corresponding flat & dark
224 will be used. This is because, at present at least, there is no way to pass
225 a calibration exposure ID from the command line to a command line task.
227 The task retrieves the corresponding dark and flat exposures for the
228 supplied visit. If a flat is available the task will (be able to) look
229 for both bright and dark defects. If only a dark is found then only bright
230 defects will be sought.
232 All pixels above/below the specified nSigma which lie with the specified
233 borders for flats/darks are identified as defects.
235 Raw visit defect finding
236 ------------------------
238 A list of exposure IDs are supplied for defect finding. The task will
239 detect bright pixels in the dark frames, if supplied, and bright & dark
240 pixels in the flats, if supplied, i.e. if you only supply darks you will
241 only be given bright defects. This is done automatically from the imageType
242 of the exposure, so the input exposure list can be a mix.
244 As with the master calib detection, all pixels above/below the specified
245 nSigma which lie with the specified borders for flats/darks are identified
246 as defects. Then, a post-processing step is done to merge these detections,
247 with pixels appearing in a fraction [0..1] of the images are kept as defects
248 and those appearing below that occurrence-threshold are discarded.
251 RunnerClass = SingleVisitListTaskRunner
252 ConfigClass = FindDefectsTaskConfig
253 _DefaultName =
"findDefects"
256 pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
257 self.makeSubtask(
"isrForFlats")
258 self.makeSubtask(
"isrForDarks")
261 self.config.isrForbiddenStepsFlats, self.config.isrDesirableSteps)
263 self.config.isrForbiddenStepsDarks, self.config.isrDesirableSteps)
264 self.config.validate()
268 def _makeArgumentParser(cls):
269 """Augment argument parser for the FindDefectsTask."""
271 parser.add_argument(
"--visitList", dest=
"visitList", nargs=
"*",
272 help=(
"List of visits to use. Same for each detector."
273 " Uses the normal 0..10:3^234 syntax"))
274 parser.add_id_argument(
"--id", datasetType=
"newDefects",
275 ContainerClass=NonexistentDatasetTaskDataIdContainer,
276 help=
"The ccds to use, e.g. --id ccd=0..100")
281 """Run the defect finding task.
283 Find the defects, as described in the main task docstring, from a
284 dataRef and a list of visit(s).
288 dataRef : `lsst.daf.persistence.ButlerDataRef`
289 dataRef for the detector for the visits to be fit.
290 visitList : `list` [`int`]
291 List of visits to be processed. If config.mode == 'VISITS' then the
292 list of visits is used. If config.mode == 'MASTER' then the length
293 of visitList must be one, and the corresponding master calibrations
298 result : `lsst.pipe.base.Struct`
299 Result struct with Components:
301 - ``defects`` : `lsst.meas.algorithms.Defect`
302 The defects found by the task.
303 - ``exitStatus`` : `int`
307 detNum = dataRef.dataId[self.config.ccdKey]
308 self.log.info(
"Calculating defects using %s visits for detector %s" % (visitList, detNum))
310 defectLists = {
'dark': [],
'flat': []}
315 if self.config.mode ==
'MASTER':
316 if len(visitList) > 1:
317 raise RuntimeError(f
"Must only specify one visit when using mode MASTER, got {visitList}")
318 dataRef.dataId[
'expId'] = visitList[0]
320 for datasetType
in defectLists.keys():
321 exp = dataRef.get(datasetType)
323 filters.add(exp.getFilter().getName())
326 msg =
"Found %s defects containing %s pixels in master %s"
327 self.log.info(msg, len(defects), self.
_nPixFromDefects(defects), datasetType)
328 defectLists[datasetType].append(defects)
329 if self.config.makePlots:
331 defects, datasetType)
332 midTime /= len(defectLists.keys())
334 elif self.config.mode ==
'VISITS':
335 butler = dataRef.getButler()
337 if self.config.assertSameRun:
340 raise RuntimeError(f
"Got data from runs {runs} with assertSameRun==True")
342 for visit
in visitList:
343 imageType = butler.queryMetadata(
'raw', self.config.imageTypeKey, dataId={
'expId': visit})[0]
344 imageType = imageType.lower()
345 dataRef.dataId[
'expId'] = visit
347 if imageType ==
'flat':
348 exp = self.isrForFlats.
runDataRef(dataRef).exposure
350 defectLists[
'flat'].append(defects)
352 filters.add(exp.getFilter().getName())
354 elif imageType ==
'dark':
355 exp = self.isrForDarks.
runDataRef(dataRef).exposure
357 defectLists[
'dark'].append(defects)
359 filters.add(exp.getFilter().getName())
362 raise RuntimeError(f
"Failed on imageType {imageType}. Only flats and darks supported")
364 msg =
"Found %s defects containing %s pixels in visit %s"
367 if self.config.makePlots:
370 midTime /= len(visitList)
372 msg =
"Combining %s defect sets from darks for detector %s"
373 self.log.info(msg, len(defectLists[
'dark']), detNum)
375 self.config.combinationMode)
376 msg =
"Combining %s defect sets from flats for detector %s"
377 self.log.info(msg, len(defectLists[
'flat']), detNum)
379 self.config.combinationMode)
381 msg =
"Combining bright and dark defect sets for detector %s"
382 self.log.info(msg, detNum)
383 brightDarkPostMerge = [mergedDefectsFromDarks, mergedDefectsFromFlats]
386 self.
_writeData(dataRef, allDefects, midTime, filters)
388 self.log.info(
"Finished finding defects in detector %s" % detNum)
389 return pipeBase.Struct(defects=allDefects, exitStatus=0)
391 def _getNsigmaForPlot(self, imageType):
392 assert imageType
in [
'flat',
'dark']
393 nSig = self.config.nSigmaBright
if imageType ==
'flat' else self.config.nSigmaDark
397 def _nPixFromDefects(defect):
398 """Count the number of pixels in a defect object."""
401 nPix += d.getBBox().getArea()
404 def _writeData(self, dataRef, defects, midTime, filters):
405 """Write the data out to the defect file.
409 dataRef : `lsst.daf.persistence.ButlerDataRef`
410 dataRef for the detector for defects to be written.
411 defects : `lsst.meas.algorithms.Defect`
412 The defects to be written.
414 date = dafBase.DateTime(midTime, dafBase.DateTime.MJD).toPython().isoformat()
419 if not self.config.ignoreFilters:
422 filt = self.config.nullFilterName
424 CALIB_ID = f
"detectorName={detName} detector={detNum} calibDate={date} ccd={detNum} filter={filt}"
427 CALIB_ID += f
" raftName={raftName}"
431 now = dafBase.DateTime.now().toPython()
432 mdOriginal = defects.getMetadata()
433 mdSupplemental = {
"INSTRUME": instrumentName,
434 "DETECTOR": dataRef.dataId[
'detector'],
436 "CALIB_ID": CALIB_ID,
437 "CALIB_CREATION_DATE": now.date().isoformat(),
438 "CALIB_CREATION_TIME": now.time().isoformat()}
440 mdOriginal.update(mdSupplemental)
444 templateFilename = dataRef.getUri(write=
True)
445 baseDirName = os.path.dirname(templateFilename)
448 dirName = os.path.join(baseDirName, instrumentName,
"defects", detNameFull.lower())
449 if not os.path.exists(dirName):
453 filename = os.path.join(dirName, date)
455 msg =
"Writing defects to %s in format: %s"
456 self.log.info(msg, os.path.splitext(filename)[0], self.config.writeAs)
457 if self.config.writeAs
in [
'FITS',
'BOTH']:
458 defects.writeFits(filename)
459 if self.config.writeAs
in [
'ASCII',
'BOTH']:
460 wroteTo = defects.writeText(filename)
461 assert(os.path.splitext(wroteTo)[0] == os.path.splitext(filename)[0])
465 def _filterSetToFilterString(filters):
466 return "~".join([f
for f
in filters])
469 def _getDetectorNumber(dataRef):
470 """The detector's integer identifier."""
471 dataRefDetNum = dataRef.dataId[
'detector']
472 camera = dataRef.get(
'camera')
473 detectorDetNum = camera[dataRef.dataId[
'detector']].getId()
474 assert dataRefDetNum == detectorDetNum
478 def _getInstrumentName(dataRef):
479 camera = dataRef.get(
'camera')
480 return camera.getName()
483 def _getDetectorNameFull(dataRef):
484 """The detector's self-reported full name, e.g. R12_S01."""
485 camera = dataRef.get(
'camera')
486 return camera[dataRef.dataId[
'detector']].getName()
489 def _getDetectorNameShort(dataRef):
490 """The detectorName per the butler, e.g. slot name, e.g. S12."""
491 butler = dataRef.getButler()
492 detectorName = butler.queryMetadata(
'raw', [
'detectorName'], dataRef.dataId)[0]
496 def _getRaftName(dataRef):
497 """The detectorName per the butler, e.g. slot name, e.g. S12."""
498 butler = dataRef.getButler()
499 raftName = butler.queryMetadata(
'raw', [
'raftName'], dataRef.dataId)[0]
503 def _getMjd(exp, timescale=dafBase.DateTime.UTC):
504 vi = exp.getInfo().getVisitInfo()
505 dateObs = vi.getDate()
506 mjd = dateObs.get(dafBase.DateTime.MJD)
510 def _getRunListFromVisits(butler, visitList):
511 """Return the set of runs for the visits in visitList."""
513 for visit
in visitList:
514 runs.add(butler.queryMetadata(
'raw',
'run', dataId={
'expId': visit})[0])
517 def _postProcessDefectSets(self, defectList, imageDimensions, mode):
518 """Combine a list of defects to make a single defect object.
520 AND, OR or use percentage of visits in which defects appear
525 defectList : `list` [`lsst.meas.algorithms.Defect`]
526 The lList of defects to merge.
527 imageDimensions : `tuple` [`int`]
528 The size of the image.
530 The combination mode to use, either 'AND', 'OR' or 'FRACTION'
534 defects : `lsst.meas.algorithms.Defect`
535 The defect set resulting from the merge.
542 if len(defectList) == 1:
545 sumImage = afwImage.MaskedImageF(imageDimensions)
546 for defects
in defectList:
547 for defect
in defects:
548 sumImage.image[defect.getBBox()] += 1
549 sumImage /= len(defectList)
551 nDetected = len(np.where(sumImage.image.array > 0)[0])
552 self.log.info(
"Pre-merge %s pixels with non-zero detections" % nDetected)
555 indices = np.where(sumImage.image.array > 0)
559 elif mode ==
'FRACTION':
560 threshold = self.config.combinationFraction
562 raise RuntimeError(f
"Got unsupported combinationMode {mode}")
563 indices = np.where(sumImage.image.array >= threshold)
565 BADBIT = sumImage.mask.getPlaneBitMask(
'BAD')
566 sumImage.mask.array[indices] |= BADBIT
568 self.log.info(
"Post-merge %s pixels marked as defects" % len(indices[0]))
570 if self.config.edgesAsDefects:
571 self.log.info(
"Masking edge pixels as defects in addition to previously identified defects")
574 defects = measAlg.Defects.fromMask(sumImage,
'BAD')
578 def _getNumGoodPixels(maskedIm, badMaskString="NO_DATA"):
579 """Return the number of non-bad pixels in the image."""
580 nPixels = maskedIm.mask.array.size
582 return nPixels - nBad
585 """Find hot and cold pixels in an image.
587 Using config-defined thresholds on a per-amp basis, mask pixels
588 that are nSigma above threshold in dark frames (hot pixels),
589 or nSigma away from the clipped mean in flats (hot & cold pixels).
593 exp : `lsst.afw.image.exposure.Exposure`
594 The exposure in which to find defects.
596 The image type, either 'dark' or 'flat'.
598 If true, update exp with hot and cold pixels.
600 cold: DETECTED_NEGATIVE
604 defects : `lsst.meas.algorithms.Defect`
605 The defects found in the image.
607 assert imageType
in [
'flat',
'dark']
610 maskedIm = exp.maskedImage
615 polarities = {
'dark': [
True],
'flat': [
True,
False]}[imageType]
619 for amp
in exp.getDetector():
620 ampImg = maskedIm[amp.getBBox()].clone()
623 if self.config.nPixBorderLeftRight:
624 if ampImg.getX0() == 0:
625 ampImg = ampImg[self.config.nPixBorderLeftRight:, :, afwImage.LOCAL]
627 ampImg = ampImg[:-self.config.nPixBorderLeftRight, :, afwImage.LOCAL]
628 if self.config.nPixBorderUpDown:
629 if ampImg.getY0() == 0:
630 ampImg = ampImg[:, self.config.nPixBorderUpDown:, afwImage.LOCAL]
632 ampImg = ampImg[:, :-self.config.nPixBorderUpDown, afwImage.LOCAL]
637 ampImg -= afwMath.makeStatistics(ampImg, afwMath.MEANCLIP, ).getValue()
640 for polarity
in polarities:
641 nSig = self.config.nSigmaBright
if polarity
else self.config.nSigmaDark
642 threshold = afwDetection.createThreshold(nSig,
'stdev', polarity=polarity)
644 footprintSet = afwDetection.FootprintSet(ampImg, threshold)
646 footprintSet.setMask(maskedIm.mask, (
"DETECTED" if polarity
else "DETECTED_NEGATIVE"))
648 if mergedSet
is None:
649 mergedSet = footprintSet
651 mergedSet.merge(footprintSet)
653 footprintList += mergedSet.getFootprints()
655 defects = measAlg.Defects.fromFootprintList(footprintList)
661 """Mask blocks in a column if there are on-and-off bad pixels
663 If there's a column with on and off bad pixels, mask all the pixels in between,
664 except if there is a large enough gap of consecutive good pixels between two
665 bad pixels in the column.
669 defects: `lsst.meas.algorithms.Defect`
670 The defects found in the image so far
674 defects: `lsst.meas.algorithms.Defect`
675 If the number of bad pixels in a column is not larger or equal than
676 self.config.badPixelColumnThreshold, the iput list is returned. Otherwise,
677 the defects list returned will include boxes that mask blocks of on-and-of
682 for defect
in defects:
683 bbox = defect.getBBox()
684 x0, y0 = bbox.getMinX(), bbox.getMinY()
685 deltaX0, deltaY0 = bbox.getDimensions()
686 for j
in np.arange(y0, y0+deltaY0):
687 for i
in np.arange(x0, x0 + deltaX0):
688 coordinates.append((i, j))
691 for coordinatePair
in coordinates:
692 x.append(coordinatePair[0])
693 y.append(coordinatePair[1])
698 unique, counts = np.unique(x, return_counts=
True)
700 for (a, b)
in zip(unique, counts):
701 if b >= self.config.badOnAndOffPixelColumnThreshold:
703 if len(multipleX) != 0:
708 def _markBlocksInBadColumn(self, x, y, multipleX, defects):
709 """Mask blocks in a column if number of on-and-off bad pixels is above threshold.
711 This function is called if the number of on-and-off bad pixels in a column
712 is larger or equal than self.config.badOnAndOffPixelColumnThreshold.
717 Lower left x coordinate of defect box. x coordinate is along the short axis if amp.
720 Lower left y coordinate of defect box. x coordinate is along the long axis if amp.
723 List of x coordinates in amp. with multiple bad pixels (i.e., columns with defects).
725 defects: `lsst.meas.algorithms.Defect`
726 The defcts found in the image so far
730 defects: `lsst.meas.algorithms.Defect`
731 The defects list returned that will include boxes that mask blocks
734 with defects.bulk_update():
735 goodPixelColumnGapThreshold = self.config.goodPixelColumnGapThreshold
737 index = np.where(x == x0)
739 minY, maxY = np.min(multipleY), np.max(multipleY)
742 diffIndex = np.where(np.diff(multipleY) >= goodPixelColumnGapThreshold)[0]
743 if len(diffIndex) != 0:
745 for gapIndex
in diffIndex:
746 limits.append(multipleY[gapIndex])
747 limits.append(multipleY[gapIndex+1])
749 assert len(limits)%2 == 0,
'limits is even by design, but check anyways'
750 for i
in np.arange(0, len(limits)-1, 2):
758 def _setEdgeBits(self, exposureOrMaskedImage, maskplaneToSet='EDGE'):
759 """Set edge bits on an exposure or maskedImage.
764 Raised if parameter ``exposureOrMaskedImage`` is an invalid type.
766 if isinstance(exposureOrMaskedImage, afwImage.Exposure):
767 mi = exposureOrMaskedImage.maskedImage
768 elif isinstance(exposureOrMaskedImage, afwImage.MaskedImage):
769 mi = exposureOrMaskedImage
771 t = type(exposureOrMaskedImage)
772 raise TypeError(f
"Function supports exposure or maskedImage but not {t}")
774 MASKBIT = mi.mask.getPlaneBitMask(maskplaneToSet)
775 if self.config.nPixBorderLeftRight:
776 mi.mask[: self.config.nPixBorderLeftRight, :, afwImage.LOCAL] |= MASKBIT
777 mi.mask[-self.config.nPixBorderLeftRight:, :, afwImage.LOCAL] |= MASKBIT
778 if self.config.nPixBorderUpDown:
779 mi.mask[:, : self.config.nPixBorderUpDown, afwImage.LOCAL] |= MASKBIT
780 mi.mask[:, -self.config.nPixBorderUpDown:, afwImage.LOCAL] |= MASKBIT
782 def _plot(self, dataRef, exp, visit, nSig, defects, imageType):
783 """Plot the defects and pixel histograms.
787 dataRef : `lsst.daf.persistence.ButlerDataRef`
788 dataRef for the detector.
789 exp : `lsst.afw.image.exposure.Exposure`
790 The exposure in which the defects were found.
794 The number of sigma used for detection
795 defects : `lsst.meas.algorithms.Defect`
798 The type of image, either 'dark' or 'flat'.
800 Currently only for LSST sensors. Plots are written to the path
801 given by the butler for the ``cpPipePlotRoot`` dataset type.
803 import matplotlib.pyplot
as plt
804 from matplotlib.backends.backend_pdf
import PdfPages
806 afwDisplay.setDefaultBackend(
"matplotlib")
807 plt.interactive(
False)
809 dirname = dataRef.getUri(datasetType=
'cpPipePlotRoot', write=
True)
810 if not os.path.exists(dirname):
813 detNum = exp.getDetector().getId()
814 nAmps = len(exp.getDetector())
816 if self.config.mode ==
"MASTER":
817 filename = f
"defectPlot_det{detNum}_master-{imageType}_for-exp{visit}.pdf"
818 elif self.config.mode ==
"VISITS":
819 filename = f
"defectPlot_det{detNum}_{imageType}_exp{visit}.pdf"
821 filenameFull = os.path.join(dirname, filename)
823 with warnings.catch_warnings():
824 msg =
"Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure."
825 warnings.filterwarnings(
"ignore", message=msg)
826 with PdfPages(filenameFull)
as pdfPages:
833 self.log.info(
"Wrote plot(s) to %s" % filenameFull)
835 def _plotDefects(self, exp, visit, defects, imageType):
836 """Plot the defects found by the task.
840 exp : `lsst.afw.image.exposure.Exposure`
841 The exposure in which the defects were found.
844 defects : `lsst.meas.algorithms.Defect`
847 The type of image, either 'dark' or 'flat'.
849 expCopy = exp.clone()
851 maskedIm = expCopy.maskedImage
853 defects.maskPixels(expCopy.maskedImage,
"BAD")
854 detector = expCopy.getDetector()
856 disp = afwDisplay.Display(0, reopenPlot=
True, dpi=200)
858 if imageType ==
"flat":
860 ampIm = maskedIm.image[amp.getBBox()]
861 ampIm -= afwMath.makeStatistics(ampIm, afwMath.MEANCLIP).getValue() + 1
863 mpDict = maskedIm.mask.getMaskPlaneDict()
864 for plane
in mpDict.keys():
867 disp.setMaskPlaneColor(plane, afwDisplay.IGNORE)
869 disp.scale(
'asinh',
'zscale')
870 disp.setMaskTransparency(80)
871 disp.setMaskPlaneColor(
"BAD", afwDisplay.RED)
873 disp.setImageColormap(
'gray')
874 title = (f
"Detector: {detector.getName()[-3:]} {detector.getSerial()}"
875 f
", Type: {imageType}, visit: {visit}")
876 disp.mtv(maskedIm, title=title)
878 cameraGeom.utils.overlayCcdBoxes(detector, isTrimmed=
True, display=disp)
880 def _plotAmpHistogram(self, dataRef, exp, visit, nSigmaUsed):
882 Make a histogram of the distribution of pixel values for each amp.
884 The main image data histogram is plotted in blue. Edge pixels,
885 if masked, are in red. Note that masked edge pixels do not contribute
886 to the underflow and overflow numbers.
888 Note that this currently only supports the 16-amp LSST detectors.
892 dataRef : `lsst.daf.persistence.ButlerDataRef`
893 dataRef for the detector.
894 exp : `lsst.afw.image.exposure.Exposure`
895 The exposure in which the defects were found.
899 The number of sigma used for detection
901 import matplotlib.pyplot
as plt
903 detector = exp.getDetector()
905 if len(detector) != 16:
906 raise RuntimeError(
"Plotting currently only supported for 16 amp detectors")
907 fig, ax = plt.subplots(nrows=4, ncols=4, sharex=
'col', sharey=
'row', figsize=(13, 10))
909 expTime = exp.getInfo().getVisitInfo().getExposureTime()
911 for (amp, a)
in zip(reversed(detector), ax.flatten()):
912 mi = exp.maskedImage[amp.getBBox()]
915 mi.image.array /= expTime
916 stats = afwMath.makeStatistics(mi, afwMath.MEANCLIP | afwMath.STDEVCLIP)
917 mean, sigma = stats.getValue(afwMath.MEANCLIP), stats.getValue(afwMath.STDEVCLIP)
919 EDGEBIT = exp.maskedImage.mask.getPlaneBitMask(
"EDGE")
920 imgData = mi.image.array[(mi.mask.array & EDGEBIT) == 0].flatten()
921 edgeData = mi.image.array[(mi.mask.array & EDGEBIT) != 0].flatten()
923 thrUpper = mean + nSigmaUsed*sigma
924 thrLower = mean - nSigmaUsed*sigma
926 nRight = len(imgData[imgData > thrUpper])
927 nLeft = len(imgData[imgData < thrLower])
929 nsig = nSigmaUsed + 1.2
930 leftEdge = mean - nsig * nSigmaUsed*sigma
931 rightEdge = mean + nsig * nSigmaUsed*sigma
932 nbins = np.linspace(leftEdge, rightEdge, 1000)
933 ey, bin_borders, patches = a.hist(edgeData, histtype=
'step', bins=nbins, lw=1, edgecolor=
'red')
934 y, bin_borders, patches = a.hist(imgData, histtype=
'step', bins=nbins, lw=3, edgecolor=
'blue')
937 nOverflow = len(imgData[imgData > rightEdge])
938 nUnderflow = len(imgData[imgData < leftEdge])
941 a.axvline(thrUpper, c=
'k')
942 a.axvline(thrLower, c=
'k')
943 msg = f
"{amp.getName()}\nmean:{mean: .2f}\n$\\sigma$:{sigma: .2f}"
944 a.text(0.65, 0.6, msg, transform=a.transAxes, fontsize=11)
945 msg = f
"nLeft:{nLeft}\nnRight:{nRight}\nnOverflow:{nOverflow}\nnUnderflow:{nUnderflow}"
946 a.text(0.03, 0.6, msg, transform=a.transAxes, fontsize=11.5)
949 a.set_ylim([1., 1.7*np.max(y)])
950 lPlot, rPlot = a.get_xlim()
951 a.set_xlim(np.array([lPlot, rPlot]))
953 a.set_xlabel(
"ADU/s")