318 schema = SourceTable.makeMinimalSchema()
320 self.makeSubtask(
"background")
321 self.makeSubtask(
"installSimplePsf")
322 self.makeSubtask(
"repair")
323 if self.config.doMaskStreaks:
324 self.makeSubtask(
"maskStreaks")
325 self.makeSubtask(
"measurePsf", schema=self.
schema)
327 self.makeSubtask(
'detection', schema=self.
schema)
328 if self.config.doDeblend:
329 self.makeSubtask(
"deblend", schema=self.
schema)
330 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
331 if self.config.doApCorr:
332 self.makeSubtask(
'measureApCorr', schema=self.
schema)
333 self.makeSubtask(
'applyApCorr', schema=self.
schema)
334 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
337 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
338 afwTable.CoordKey.addErrorFields(self.
schema)
342 inputs = butlerQC.get(inputRefs)
343 if 'idGenerator' not in inputs.keys():
344 inputs[
'idGenerator'] = self.config.idGenerator.apply(butlerQC.quantum.dataId)
345 outputs = self.
run(**inputs)
346 butlerQC.put(outputs, outputRefs)
349 def run(self, exposure, background=None, idGenerator=None):
350 """Characterize a science image.
352 Peforms the following operations:
353 - Iterate the following config.psfIterations times, or once if config.doMeasurePsf false:
354 - detect and measure sources and estimate PSF (see detectMeasureAndEstimatePsf for details)
355 - interpolate over cosmic rays
356 - perform final measurement
360 exposure : `lsst.afw.image.ExposureF`
361 Exposure to characterize.
362 background : `lsst.afw.math.BackgroundList`, optional
363 Initial model of background already subtracted from exposure.
364 idGenerator : `lsst.meas.base.IdGenerator`, optional
365 Object that generates source IDs and provides RNG seeds.
369 result : `lsst.pipe.base.Struct`
370 Results as a struct with attributes:
373 Characterized exposure (`lsst.afw.image.ExposureF`).
375 Detected sources (`lsst.afw.table.SourceCatalog`).
377 Model of subtracted background (`lsst.afw.math.BackgroundList`).
379 Spatial cells of PSF candidates (`lsst.afw.math.SpatialCellSet`).
381 Another reference to ``exposure`` for compatibility.
383 Another reference to ``background`` for compatibility.
388 Raised if PSF sigma is NaN.
392 if not self.config.doMeasurePsf
and not exposure.hasPsf():
393 self.log.info(
"CharacterizeImageTask initialized with 'simple' PSF.")
394 self.installSimplePsf.run(exposure=exposure)
396 if idGenerator
is None:
397 idGenerator = IdGenerator()
400 background = self.background.run(exposure).background
402 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
403 for i
in range(psfIterations):
406 idGenerator=idGenerator,
407 background=background,
410 psf = dmeRes.exposure.getPsf()
412 psfAvgPos = psf.getAveragePosition()
413 psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
414 psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
415 medBackground = np.median(dmeRes.background.getImage().getArray())
416 self.log.info(
"iter %s; PSF sigma=%0.4f, dimensions=%s; median background=%0.2f",
417 i + 1, psfSigma, psfDimensions, medBackground)
418 if np.isnan(psfSigma):
419 raise RuntimeError(
"PSF sigma is NaN, cannot continue PSF determination.")
421 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
424 self.repair.run(exposure=dmeRes.exposure)
425 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
428 if self.config.doMaskStreaks:
429 _ = self.maskStreaks.run(dmeRes.exposure)
433 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
434 exposureId=idGenerator.catalog_id)
435 if self.config.doApCorr:
437 apCorrMap = self.measureApCorr.run(
438 exposure=dmeRes.exposure,
439 catalog=dmeRes.sourceCat,
441 except MeasureApCorrError:
445 dmeRes.exposure.info.setApCorrMap(
None)
447 dmeRes.exposure.info.setApCorrMap(apCorrMap)
448 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
450 self.catalogCalculation.run(dmeRes.sourceCat)
452 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
454 return pipeBase.Struct(
455 exposure=dmeRes.exposure,
456 sourceCat=dmeRes.sourceCat,
457 background=dmeRes.background,
458 psfCellSet=dmeRes.psfCellSet,
460 characterized=dmeRes.exposure,
461 backgroundModel=dmeRes.background
466 """Perform one iteration of detect, measure, and estimate PSF.
468 Performs the following operations:
470 - if config.doMeasurePsf or not exposure.hasPsf():
472 - install a simple PSF model (replacing the existing one, if need be)
474 - interpolate over cosmic rays with keepCRs=True
475 - estimate background and subtract it from the exposure
476 - detect, deblend and measure sources, and subtract a refined background model;
477 - if config.doMeasurePsf:
482 exposure : `lsst.afw.image.ExposureF`
483 Exposure to characterize.
484 idGenerator : `lsst.meas.base.IdGenerator`
485 Object that generates source IDs and provides RNG seeds.
486 background : `lsst.afw.math.BackgroundList`, optional
487 Initial model of background already subtracted from exposure.
491 result : `lsst.pipe.base.Struct`
492 Results as a struct with attributes:
495 Characterized exposure (`lsst.afw.image.ExposureF`).
497 Detected sources (`lsst.afw.table.SourceCatalog`).
499 Model of subtracted background (`lsst.afw.math.BackgroundList`).
501 Spatial cells of PSF candidates (`lsst.afw.math.SpatialCellSet`).
506 Raised if there are too many CR pixels.
509 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
510 self.log.info(
"PSF estimation initialized with 'simple' PSF")
511 self.installSimplePsf.run(exposure=exposure)
514 if self.config.requireCrForPsf:
515 self.repair.run(exposure=exposure, keepCRs=
True)
518 self.repair.run(exposure=exposure, keepCRs=
True)
520 self.log.warning(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)",
521 self.config.repair.cosmicray.nCrPixelMax)
523 self.
display(
"repair_iter", exposure=exposure)
525 if background
is None:
526 background = BackgroundList()
528 sourceIdFactory = idGenerator.make_table_id_factory()
529 table = SourceTable.make(self.
schema, sourceIdFactory)
532 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
533 sourceCat = detRes.sources
534 if detRes.background:
535 for bg
in detRes.background:
536 background.append(bg)
538 if self.config.doDeblend:
539 self.deblend.run(exposure=exposure, sources=sourceCat)
541 if not sourceCat.isContiguous():
542 sourceCat = sourceCat.copy(deep=
True)
544 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=idGenerator.catalog_id)
546 measPsfRes = pipeBase.Struct(cellSet=
None)
547 if self.config.doMeasurePsf:
548 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat,
549 expId=idGenerator.catalog_id)
550 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
552 return pipeBase.Struct(
555 background=background,
556 psfCellSet=measPsfRes.cellSet,
559 def display(self, itemName, exposure, sourceCat=None):
560 """Display exposure and sources on next frame (for debugging).
565 Name of item in ``debugInfo``.
566 exposure : `lsst.afw.image.ExposureF`
568 sourceCat : `lsst.afw.table.SourceCatalog`, optional
569 Catalog of sources detected on the exposure.
571 val = getDebugFrame(self._display, itemName)
575 displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self.
_frame, pause=
False)