864 inputs = butlerQC.get(inputRefs)
865 exposures = inputs.pop(
"exposures")
867 exposure_record = inputRefs.exposures[0].dataId.records[
"exposure"]
868 if self.config.useButlerExposureRegion:
869 exposure_region = butlerQC.quantum.dataId.region
871 exposure_region =
None
873 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId)
875 astrometry_loader = lsst.meas.algorithms.ReferenceObjectLoader(
876 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.astrometry_ref_cat],
877 refCats=inputs.pop(
"astrometry_ref_cat"),
878 name=self.config.connections.astrometry_ref_cat,
879 config=self.config.astrometry_ref_loader, log=self.log)
880 self.astrometry.setRefObjLoader(astrometry_loader)
882 photometry_loader = lsst.meas.algorithms.ReferenceObjectLoader(
883 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.photometry_ref_cat],
884 refCats=inputs.pop(
"photometry_ref_cat"),
885 name=self.config.connections.photometry_ref_cat,
886 config=self.config.photometry_ref_loader, log=self.log)
887 self.photometry.match.setRefObjLoader(photometry_loader)
889 if self.config.doMaskDiffractionSpikes:
892 self.diffractionSpikeMask.setRefObjLoader(photometry_loader)
894 if self.config.do_illumination_correction:
895 background_flat = inputs.pop(
"background_flat")
896 illumination_correction = inputs.pop(
"illumination_correction")
898 background_flat =
None
899 illumination_correction =
None
902 if self.config.useButlerCamera:
903 if "camera_model" in inputs:
904 camera_model = inputs.pop(
"camera_model")
906 self.log.warning(
"useButlerCamera=True, but camera is not available for filter %s. The "
907 "astrometry fit will use the WCS already attached to the exposure.",
908 exposures[0].filter.bandLabel)
911 assert not inputs,
"runQuantum got more inputs than expected"
915 result = pipeBase.Struct(
917 stars_footprints=
None,
918 psf_stars_footprints=
None,
919 background_to_photometric_ratio=
None,
920 applied_photo_calib=
None,
926 id_generator=id_generator,
927 background_flat=background_flat,
928 illumination_correction=illumination_correction,
929 camera_model=camera_model,
930 exposure_record=exposure_record,
931 exposure_region=exposure_region,
933 except pipeBase.AlgorithmError
as e:
934 error = pipeBase.AnnotatedPartialOutputsError.annotate(
938 result.psf_stars_footprints,
939 result.stars_footprints,
942 if self.config.output_image_type ==
"future" and result.exposure
is not None:
948 "Cannot write a partial-outputs image in 'future' mode, so the image will not be "
949 "written; the other partial outputs still will be."
951 result.exposure =
None
952 butlerQC.put(result, outputRefs)
955 if self.config.output_image_type ==
"future":
958 butlerQC.put(result, outputRefs)
967 background_flat=None,
968 illumination_correction=None,
970 exposure_record=None,
971 exposure_region=None,
973 """Find stars and perform psf measurement, then do a deeper detection
974 and measurement and calibrate astrometry and photometry from that.
978 exposures : `lsst.afw.image.Exposure` or \
979 `list` [`lsst.afw.image.Exposure`]
980 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
981 Modified in-place during processing if only one is passed.
982 If two exposures are passed, treat them as snaps and combine
983 before doing further processing.
984 id_generator : `lsst.meas.base.IdGenerator`, optional
985 Object that generates source IDs and provides random seeds.
986 result : `lsst.pipe.base.Struct`, optional
987 Result struct that is modified to allow saving of partial outputs
988 for some failure conditions. If the task completes successfully,
989 this is also returned.
990 background_flat : `lsst.afw.image.Exposure`, optional
991 Background flat-field image.
992 illumination_correction : `lsst.afw.image.Exposure`, optional
993 Illumination correction image.
994 camera_model : `lsst.afw.cameraGeom.Camera`, optional
995 Camera to be used if constructing updated WCS.
996 exposure_record : `lsst.daf.butler.DimensionRecord`, optional
997 Butler metadata for the ``exposure`` dimension. Used if
998 constructing an updated WCS instead of the boresight and
999 orientation angle from the visit info.
1000 exposure_region : `lsst.sphgeom.Region`, optional
1001 The exposure region to use for the reference catalog filtering.
1002 If `None`, this region will be set as a padded bbox + current WCS
1007 result : `lsst.pipe.base.Struct`
1008 Results as a struct with attributes:
1011 Calibrated exposure, with pixels in nJy units if
1012 ``config.do_calibrate_pixels`` is `True`, and in the units of
1013 the input exposure otherwise.
1014 (`lsst.afw.image.Exposure`)
1016 Stars that were used to calibrate the exposure, with
1017 calibrated fluxes and magnitudes.
1018 (`astropy.table.Table`)
1019 ``stars_footprints``
1020 Footprints of stars that were used to calibrate the exposure.
1021 (`lsst.afw.table.SourceCatalog`)
1023 Stars that were used to determine the image PSF.
1024 (`astropy.table.Table`)
1025 ``psf_stars_footprints``
1026 Footprints of stars that were used to determine the image PSF.
1027 (`lsst.afw.table.SourceCatalog`)
1029 Background that was fit to the exposure when detecting
1030 ``stars``. (`lsst.afw.math.BackgroundList`)
1031 ``applied_photo_calib``
1032 Photometric calibration that was fit to the star catalog and
1033 applied to the exposure. (`lsst.afw.image.PhotoCalib`)
1034 This is `None` if ``config.do_calibrate_pixels`` is `False`.
1035 ``astrometry_matches``
1036 Reference catalog stars matches used in the astrometric fit.
1037 (`list` [`lsst.afw.table.ReferenceMatch`] or
1038 `lsst.afw.table.BaseCatalog`).
1039 ``photometry_matches``
1040 Reference catalog stars matches used in the photometric fit.
1041 (`list` [`lsst.afw.table.ReferenceMatch`] or
1042 `lsst.afw.table.BaseCatalog`).
1044 Copy of the mask plane of `exposure`.
1045 (`lsst.afw.image.Mask`)
1048 result = pipeBase.Struct()
1049 if id_generator
is None:
1050 id_generator = lsst.meas.base.IdGenerator()
1052 result.exposure = self.snap_combine.run(exposures).exposure
1053 self.log.info(
"Initial PhotoCalib: %s", result.exposure.getPhotoCalib())
1055 result.exposure.metadata[
"LSST CALIB ILLUMCORR APPLIED"] =
False
1058 if self.config.do_illumination_correction:
1059 if not result.exposure.metadata.get(
"LSST ISR FLAT APPLIED",
False):
1060 raise pipeBase.InvalidQuantumError(
1061 "Cannot use do_illumination_correction with an image that has not had a flat applied",
1070 result.exposure, camera_model, exposure_record=exposure_record
1072 elif exposure_record
is not None:
1076 ref_cat_source_density =
None
1077 if self.astrometry.refObjLoader
is not None:
1078 load_result = self.astrometry.load_reference_catalog(
1080 exposure_region=exposure_region
1082 ref_cat = load_result.refCat
1083 ref_cat_trimmed = ref_cat[(np.isfinite(ref_cat[
"coord_ra"])
1084 & np.isfinite(ref_cat[
"coord_dec"]))].copy(deep=
True)
1085 ref_cat_trimmed = ref_cat_trimmed.asAstropy()
1088 metadata_name =
"ref_cat_source_density"
1089 result.exposure.metadata[metadata_name.upper()] = ref_cat_source_density
1090 self.metadata[metadata_name] = ref_cat_source_density
1092 result.background =
None
1093 result.background_to_photometric_ratio =
None
1094 summary_stat_catalog =
None
1099 have_fit_psf =
False
1100 have_fit_astrometry =
False
1101 have_fit_photometry =
False
1106 illumination_correction,
1111 ref_cat_source_density=ref_cat_source_density,
1113 result.psf_stars_footprints = compute_psf_struct.detections_sources
1114 adaptive_det_res_struct = compute_psf_struct.adaptive_det_res_struct
1118 if self.config.do_adaptive_threshold_detection:
1119 metadata_name =
"psf_adaptive_threshold_value"
1120 result.exposure.metadata[metadata_name.upper()] = adaptive_det_res_struct.thresholdValue
1121 self.metadata[metadata_name] = adaptive_det_res_struct.thresholdValue
1122 metadata_name =
"psf_adaptive_include_threshold_multiplier"
1123 result.exposure.metadata[metadata_name.upper()] = \
1124 adaptive_det_res_struct.includeThresholdMultiplier
1125 self.metadata[metadata_name] = adaptive_det_res_struct.includeThresholdMultiplier
1130 if result.psf_stars_footprints[
"slot_Centroid_flag"].all():
1131 psf_shape = result.exposure.psf.computeShape(result.exposure.psf.getAveragePosition())
1133 shapelets_iq_score=result.exposure.info.getSummaryStats().shapeletsIqScore,
1134 n_sources=len(result.psf_stars_footprints),
1135 psf_shape_ixx=psf_shape.getIxx(),
1136 psf_shape_iyy=psf_shape.getIyy(),
1137 psf_shape_ixy=psf_shape.getIxy(),
1138 psf_size=psf_shape.getDeterminantRadius(),
1141 if result.background
is None:
1142 result.background = afwMath.BackgroundList()
1145 result.psf_stars = result.psf_stars_footprints.asAstropy()
1148 afwTable.updateSourceCoords(
1149 result.exposure.wcs,
1150 sourceList=result.psf_stars_footprints,
1151 include_covariance=self.config.do_include_astrometric_errors,
1154 result.exposure, result.psf_stars_footprints, exposure_region=exposure_region,
1155 load_result=load_result
1157 num_astrometry_matches = len(astrometry_matches)
1158 if "astrometry_matches" in self.config.optional_outputs:
1161 result.psf_stars = result.psf_stars_footprints.asAstropy()
1165 if self.config.doMaskDiffractionSpikes:
1166 self.diffractionSpikeMask.run(result.exposure)
1168 self.metadata[
'adaptive_threshold_value'] = float(
"nan")
1169 if self.config.do_remeasure_star_background
and self.config.do_adaptive_threshold_detection:
1172 background_to_photometric_ratio=result.background_to_photometric_ratio,
1180 background_to_photometric_ratio=result.background_to_photometric_ratio,
1181 adaptive_det_res_struct=adaptive_det_res_struct,
1182 num_astrometry_matches=num_astrometry_matches,
1184 psf = result.exposure.getPsf()
1185 psfSigma = psf.computeShape(result.exposure.getBBox().getCenter()).getDeterminantRadius()
1186 self.
_match_psf_stars(result.psf_stars_footprints, result.stars_footprints,
1190 afwTable.updateSourceCoords(
1191 result.exposure.wcs,
1192 sourceList=result.stars_footprints,
1193 include_covariance=self.config.do_include_astrometric_errors,
1196 summary_stat_catalog = result.stars_footprints
1197 result.stars = result.stars_footprints.asAstropy()
1198 self.metadata[
"star_count"] = np.sum(~result.stars[
"sky_source"])
1203 self.astrometry.check(result.exposure, result.stars_footprints, len(astrometry_matches))
1204 result.stars = result.stars_footprints.asAstropy()
1205 have_fit_astrometry =
True
1207 result.stars_footprints, photometry_matches, \
1208 photometry_meta, photo_calib = self.
_fit_photometry(result.exposure, result.stars_footprints)
1209 have_fit_photometry =
True
1210 self.metadata[
"photometry_matches_count"] = len(photometry_matches)
1213 result.stars = result.stars_footprints.asAstropy()
1217 summary_stat_catalog = result.stars_footprints
1218 if "photometry_matches" in self.config.optional_outputs:
1221 if "mask" in self.config.optional_outputs:
1222 result.mask = result.exposure.mask.clone()
1223 except pipeBase.AlgorithmError:
1224 if not have_fit_psf:
1225 result.exposure.setPsf(
None)
1226 if not have_fit_astrometry:
1227 result.exposure.setWcs(
None)
1228 if not have_fit_photometry:
1229 result.exposure.setPhotoCalib(
None)
1236 self.
_summarize(result.exposure, summary_stat_catalog, result.background,
1237 summary=result.exposure.info.getSummaryStats())
1240 self.
_summarize(result.exposure, summary_stat_catalog, result.background,
1241 summary=result.exposure.info.getSummaryStats())
1246 bgIgnoredPixelMasks = lsst.meas.algorithms.SubtractBackgroundConfig().ignoredPixelMask.list()
1247 for maskName
in self.config.background_stats_ignored_pixel_masks:
1248 if (maskName
in result.exposure.mask.getMaskPlaneDict().keys()
1249 and maskName
not in bgIgnoredPixelMasks):
1250 bgIgnoredPixelMasks += [maskName]
1252 statsCtrl = afwMath.StatisticsControl()
1253 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(bgIgnoredPixelMasks))
1255 statObj = afwMath.makeStatistics(result.exposure.getMaskedImage(), afwMath.MEDIAN, statsCtrl)
1256 median_bg, _ = statObj.getResult(afwMath.MEDIAN)
1257 self.metadata[
"bg_subtracted_skyPixel_instFlux_median"] = median_bg
1259 statObj = afwMath.makeStatistics(result.exposure.getMaskedImage(), afwMath.STDEV, statsCtrl)
1260 stdev_bg, _ = statObj.getResult(afwMath.STDEV)
1261 self.metadata[
"bg_subtracted_skyPixel_instFlux_stdev"] = stdev_bg
1263 self.metadata[
"bg_subtracted_skySource_flux_median"] = (
1264 np.median(result.stars[result.stars[
'sky_source']][self.config.background_stats_flux_column])
1266 self.metadata[
"bg_subtracted_skySource_flux_stdev"] = (
1267 np.std(result.stars[result.stars[
'sky_source']][self.config.background_stats_flux_column])
1270 if self.config.do_calibrate_pixels:
1274 background_to_photometric_ratio=result.background_to_photometric_ratio,
1276 result.applied_photo_calib = photo_calib
1278 result.applied_photo_calib =
None
1282 if self.config.run_sattle:
1286 populate_sattle_visit_cache(result.exposure.getInfo().getVisitInfo(),
1287 historical=self.config.sattle_historical)
1288 self.log.info(
'Successfully triggered load of sattle visit cache')
1289 except requests.exceptions.HTTPError:
1290 self.log.exception(
"Sattle visit cache update failed; continuing with image processing")
1340 """Find bright sources detected on an exposure and fit a PSF model to
1341 them, repairing likely cosmic rays before detection.
1343 Repair, detect, measure, and compute PSF twice, to ensure the PSF
1344 model does not include contributions from cosmic rays.
1348 result : `lsst.pipe.base.Struct`
1349 Result struct that is modified to allow saving of partial outputs
1350 for some failure conditions. Should contain at least the following
1353 - exposure : `lsst.afw.image.Exposure`
1354 Exposure to detect and measure bright stars on.
1355 - background : `lsst.afw.math.BackgroundList` | `None`
1356 Background that was fit to the exposure during detection.
1357 - background_to_photometric_ratio : `lsst.afw.image.Image` | `None`
1358 Image to convert photometric-flattened image to
1359 background-flattened image.
1360 id_generator : `lsst.meas.base.IdGenerator`
1361 Object that generates source IDs and provides random seeds.
1362 ref_cat_source_density : `float` or `None`, optional
1363 A rough estimate of the source density (in number per deg^2) in
1364 the region of the exposure as measured from the loaded reference
1366 TODO DM-54929: If not `None` use the source denstiy as an improved
1367 initial estimate for the adaptive thresholding with the goal of
1368 reducing the number of iterations required (namely, by increasing
1369 it for more crowded fields).
1373 sources : `lsst.afw.table.SourceCatalog`
1374 Catalog of detected bright sources.
1375 cell_set : `lsst.afw.math.SpatialCellSet`
1376 PSF candidates returned by the psf determiner.
1377 adaptive_det_res_struct : `lsst.pipe.base.Struct`
1378 Result struct from the adaptive threshold detection.
1382 This method modifies the exposure, background and
1383 background_to_photometric_ratio attributes of the result struct
1386 def log_psf(msg, addToMetadata=False):
1387 """Log the parameters of the psf and background, with a prepended
1388 message. There is also the option to add the PSF sigma to the task
1394 Message to prepend the log info with.
1395 addToMetadata : `bool`, optional
1396 Whether to add the final psf sigma value to the task
1397 metadata (the default is False).
1399 position = result.exposure.psf.getAveragePosition()
1400 sigma = result.exposure.psf.computeShape(position).getDeterminantRadius()
1401 dimensions = result.exposure.psf.computeImage(position).getDimensions()
1402 if result.background
is not None:
1403 median_background = np.median(result.background.getImage().array)
1405 median_background = 0.0
1406 self.log.info(
"%s sigma=%0.4f, dimensions=%s; median background=%0.2f",
1407 msg, sigma, dimensions, median_background)
1409 self.metadata[
"final_psf_sigma"] = sigma
1411 self.log.info(
"First pass detection with Gaussian PSF FWHM=%s pixels",
1412 self.config.install_simple_psf.fwhm)
1413 self.install_simple_psf.run(exposure=result.exposure)
1415 result.background = self.psf_subtract_background.run(
1416 exposure=result.exposure,
1417 backgroundToPhotometricRatio=result.background_to_photometric_ratio,
1419 log_psf(
"Initial PSF:")
1420 self.psf_repair.run(exposure=result.exposure, keepCRs=
True)
1422 table = afwTable.SourceTable.make(self.
psf_schema, id_generator.make_table_id_factory())
1423 if not self.config.do_adaptive_threshold_detection:
1424 adaptive_det_res_struct =
None
1427 detections = self.psf_detection.run(
1429 exposure=result.exposure,
1430 background=result.background,
1431 backgroundToPhotometricRatio=result.background_to_photometric_ratio,
1434 initialThreshold = self.config.psf_detection.thresholdValue
1435 initialThresholdMultiplier = self.config.psf_detection.includeThresholdMultiplier
1436 adaptive_det_res_struct = self.psf_adaptive_threshold_detection.run(
1437 table, result.exposure,
1438 initialThreshold=initialThreshold,
1439 initialThresholdMultiplier=initialThresholdMultiplier,
1441 detections = adaptive_det_res_struct.detections
1443 self.metadata[
"initial_psf_positive_footprint_count"] = detections.numPos
1444 self.metadata[
"initial_psf_negative_footprint_count"] = detections.numNeg
1445 self.metadata[
"initial_psf_positive_peak_count"] = detections.numPosPeaks
1446 self.metadata[
"initial_psf_negative_peak_count"] = detections.numNegPeaks
1448 if self.config.do_compute_shapelet_decomposition:
1451 self.log.info(
"Computing rough shapelet decomposition for PSF star candidates.")
1452 seed = id_generator.catalog_id & 0xFFFFFFFF
1453 shapelets_results = self.compute_shapelet_decomposition.run(
1454 masked_image=result.exposure.getMaskedImage(), catalog=detections.sources, seed=seed
1456 result.exposure.info.setSummaryStats(shapelets_results.shapelets_summary)
1457 detections.sources = shapelets_results.catalog
1459 self.psf_source_measurement.run(detections.sources, result.exposure)
1461 if self.config.do_compute_shapelet_decomposition:
1467 self.log.info(
"Computing shapelets_iq_score that includes power from the "
1468 "median centroid offset.")
1469 centroid_offset_scale = 0.5*np.sqrt(
1470 self.config.compute_shapelet_decomposition.max_footprint_area)
1471 shapelets_results = self.compute_shapelet_decomposition.compute_shapelets_iq_score(
1472 shapelets_results=shapelets_results,
1473 catalog=detections.sources,
1474 centroid_offset_scale=centroid_offset_scale,
1475 shapelets_base_name=self.compute_shapelet_decomposition.shapelets_base_name,
1478 result.exposure.info.setSummaryStats(shapelets_results.shapelets_summary)
1480 psf_result = self.psf_measure_psf.run(exposure=result.exposure, sources=detections.sources)
1487 if not self.config.do_adaptive_threshold_detection:
1491 self.install_simple_psf.run(exposure=result.exposure)
1493 log_psf(
"Rerunning with simple PSF:")
1501 self.psf_repair.run(exposure=result.exposure, keepCRs=
True)
1504 detections = self.psf_detection.run(
1506 exposure=result.exposure,
1507 background=result.background,
1508 backgroundToPhotometricRatio=result.background_to_photometric_ratio,
1510 self.psf_source_measurement.run(detections.sources, result.exposure)
1511 psf_result = self.psf_measure_psf.run(exposure=result.exposure, sources=detections.sources)
1512 self.metadata[
"simple_psf_positive_footprint_count"] = detections.numPos
1513 self.metadata[
"simple_psf_negative_footprint_count"] = detections.numNeg
1514 self.metadata[
"simple_psf_positive_peak_count"] = detections.numPosPeaks
1515 self.metadata[
"simple_psf_negative_peak_count"] = detections.numNegPeaks
1516 log_psf(
"Final PSF:", addToMetadata=
True)
1519 self.psf_repair.run(exposure=result.exposure)
1521 self.psf_source_measurement.run(detections.sources, result.exposure)
1525 return pipeBase.Struct(
1526 detections_sources=detections.sources,
1527 psf_result_cellSet=psf_result.cellSet,
1528 adaptive_det_res_struct=adaptive_det_res_struct,
1561 def _find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None,
1562 adaptive_det_res_struct=None, num_astrometry_matches=None):
1563 """Detect stars on an exposure that has a PSF model, and measure their
1564 PSF, circular aperture, compensated gaussian fluxes.
1568 exposure : `lsst.afw.image.Exposure`
1569 Exposure to detect and measure stars on.
1570 background : `lsst.afw.math.BackgroundList`
1571 Background that was fit to the exposure during detection;
1572 modified in-place during subsequent detection.
1573 id_generator : `lsst.meas.base.IdGenerator`
1574 Object that generates source IDs and provides random seeds.
1575 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1576 Image to convert photometric-flattened image to
1577 background-flattened image.
1581 stars : `SourceCatalog`
1582 Sources that are very likely to be stars, with a limited set of
1583 measurements performed on them.
1586 id_generator.make_table_id_factory())
1588 maxAdaptiveDetIter = 8
1590 if adaptive_det_res_struct
is not None:
1591 for adaptiveDetIter
in range(maxAdaptiveDetIter):
1592 adaptiveDetectionConfig = lsst.meas.algorithms.SourceDetectionConfig()
1593 if self.config.do_remeasure_star_background:
1594 adaptiveDetectionConfig.reEstimateBackground =
False
1596 adaptiveDetectionConfig.reEstimateBackground =
True
1597 adaptiveDetectionConfig.includeThresholdMultiplier = 2.0
1599 adaptive_det_res_struct.thresholdValue*adaptive_det_res_struct.includeThresholdMultiplier
1601 adaptiveDetectionConfig.thresholdValue = max(
1602 self.config.star_detection.thresholdValue,
1603 threshFactor*psfThreshold/adaptiveDetectionConfig.includeThresholdMultiplier
1605 self.log.info(
"Using adaptive threshold detection (nIter = %d) with "
1606 "thresholdValue = %.2f and multiplier = %.1f",
1607 adaptiveDetIter, adaptiveDetectionConfig.thresholdValue,
1608 adaptiveDetectionConfig.includeThresholdMultiplier)
1609 adaptiveDetectionTask = lsst.meas.algorithms.SourceDetectionTask(
1610 config=adaptiveDetectionConfig
1612 detections = adaptiveDetectionTask.run(
1615 background=background,
1616 backgroundToPhotometricRatio=background_to_photometric_ratio,
1618 nFootprint = len(detections.sources)
1621 for src
in detections.sources:
1622 nPeakSrc = len(src.getFootprint().getPeaks())
1626 minIsolated = min(400, max(3, 0.005*nPeak, 0.6*num_astrometry_matches))
1628 self.log.info(
"Adaptive threshold detection _find_stars nIter: %d; "
1629 "nPeak/nFootprint = %.2f (max is 800); nIsolated = %d (min is %.1f).",
1630 adaptiveDetIter, nPeak/nFootprint, nIsolated, minIsolated)
1631 if nPeak/nFootprint > 800
or nIsolated < minIsolated:
1632 threshFactor = max(0.01, 1.5*threshFactor)
1633 self.log.warning(
"nPeak/nFootprint = %.2f (max is 800); nIsolated = %d "
1634 "(min is %.1f).", nPeak/nFootprint, nIsolated, minIsolated)
1638 threshFactor *= 0.75
1639 self.log.warning(
"No footprints detected on image. Decreasing threshold "
1640 "factor to %.2f. and rerunning.", threshFactor)
1644 detections = self.star_detection.run(
1647 background=background,
1648 backgroundToPhotometricRatio=background_to_photometric_ratio,
1650 sources = detections.sources
1651 self.star_sky_sources.run(exposure.mask, id_generator.catalog_id, sources)
1653 n_sky_sources = np.sum(sources[
"sky_source"])
1654 if (self.config.do_downsample_footprints
1655 and (len(sources) - n_sky_sources) > self.config.downsample_max_footprints):
1656 if exposure.info.id
is None:
1657 self.log.warning(
"Exposure does not have a proper id; using 0 seed for downsample.")
1660 seed = exposure.info.id & 0xFFFFFFFF
1662 gen = np.random.RandomState(seed)
1665 indices = np.arange(len(sources))[~sources[
"sky_source"]]
1666 indices = gen.choice(
1668 size=self.config.downsample_max_footprints,
1671 skyIndices, = np.where(sources[
"sky_source"])
1672 indices = np.concatenate((indices, skyIndices))
1674 self.log.info(
"Downsampling from %d to %d non-sky-source footprints.", len(sources), len(indices))
1676 sel = np.zeros(len(sources), dtype=bool)
1678 sources = sources[sel]
1682 self.star_deblend.run(exposure=exposure, sources=sources)
1685 if not sources.isContiguous():
1686 sources = sources.copy(deep=
True)
1689 self.star_measurement.run(sources, exposure)
1690 self.metadata[
"post_deblend_source_count"] = np.sum(~sources[
"sky_source"])
1691 self.metadata[
"failed_deblend_source_count"] = np.sum(
1692 ~sources[
"sky_source"] & sources[
"deblend_failed"]
1694 self.metadata[
"saturated_source_count"] = np.sum(sources[
"base_PixelFlags_flag_saturated"])
1695 self.metadata[
"bad_source_count"] = np.sum(sources[
"base_PixelFlags_flag_bad"])
1700 self.star_normalized_calibration_flux.run(exposure=exposure, catalog=sources)
1701 self.star_apply_aperture_correction.run(sources, exposure.apCorrMap)
1702 self.star_catalog_calculation.run(sources)
1703 self.star_set_primary_flags.run(sources)
1705 result = self.star_selector.run(sources)
1707 if not result.sourceCat.isContiguous():
1708 return result.sourceCat.copy(deep=
True)
1710 return result.sourceCat
2093 """Remeasure the exposure's background with iterative adaptive
2094 threshold detection.
2098 result : `lsst.pipe.base.Struct`
2099 The current state of the result Strut from the run method of
2100 CalibrateImageTask. Will be modified in place.
2101 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
2102 Image to convert photometric-flattened image to
2103 background-flattened image.
2107 result : `lsst.pipe.base.Struct`
2108 The modified result Struct with the new background subtracted.
2113 backgroundOrig = result.background.clone()
2114 median_background_orig = np.median(backgroundOrig.getImage().array)
2115 self.log.info(
"Original median_background = %.2f", median_background_orig)
2116 result.exposure.image.array += result.background.getImage().array
2117 result.background = afwMath.BackgroundList()
2119 origMask = result.exposure.mask.clone()
2120 bad_mask_planes = [
"BAD",
"EDGE",
"NO_DATA"]
2121 detected_mask_planes = [
"DETECTED",
"DETECTED_NEGATIVE"]
2123 detected_mask_planes,
2125 minDetFracForFinalBg = 0.02
2126 maxDetFracForFinalBg = 0.93
2130 maxAdaptiveDetIter = 10
2131 for dilateIter
in range(maxAdaptiveDetIter):
2132 dilatedMask = origMask.clone()
2133 for maskName
in detected_mask_planes:
2135 detectedMaskBit = dilatedMask.getPlaneBitMask(maskName)
2136 detectedMaskSpanSet = SpanSet.fromMask(dilatedMask, detectedMaskBit)
2137 detectedMaskSpanSet = detectedMaskSpanSet.dilated(nPixToDilate)
2138 detectedMaskSpanSet = detectedMaskSpanSet.clippedTo(dilatedMask.getBBox())
2140 detectedMask = dilatedMask.getMaskPlane(maskName)
2141 dilatedMask.clearMaskPlane(detectedMask)
2143 detectedMaskSpanSet.setMask(dilatedMask, detectedMaskBit)
2146 detected_mask_planes,
2148 if detected_fraction_dilated < maxDetFracForFinalBg
or nPixToDilate == 1:
2153 result.exposure.mask = dilatedMask
2154 self.log.debug(
"detected_fraction_orig = %.3f; detected_fraction_dilated = %.3f",
2155 detected_fraction_orig, detected_fraction_dilated)
2156 n_above_max_per_amp = -99
2157 highest_detected_fraction_per_amp = float(
"nan")
2160 n_above_max_per_amp, highest_detected_fraction_per_amp, no_zero_det_amps = \
2162 detected_mask_planes, bad_mask_planes)
2163 self.log.debug(
"Dilated mask: n_above_max_per_amp = %d; "
2164 "highest_detected_fraction_per_amp = %.3f",
2165 n_above_max_per_amp, highest_detected_fraction_per_amp)
2167 bgIgnoreMasksToAdd = [
"SAT",
"SUSPECT",
"SPIKE"]
2168 detected_fraction = 1.0
2169 nFootprintTemp = 1e12
2170 starBackgroundDetectionConfig = lsst.meas.algorithms.SourceDetectionConfig()
2171 for maskName
in bgIgnoreMasksToAdd:
2172 if (maskName
in result.exposure.mask.getMaskPlaneDict().keys()
2173 and maskName
not in starBackgroundDetectionConfig.background.ignoredPixelMask):
2174 starBackgroundDetectionConfig.background.ignoredPixelMask += [maskName]
2175 starBackgroundDetectionConfig.doTempLocalBackground =
False
2176 starBackgroundDetectionConfig.nSigmaToGrow = 70.0
2177 starBackgroundDetectionConfig.reEstimateBackground =
False
2178 starBackgroundDetectionConfig.includeThresholdMultiplier = 1.0
2179 starBackgroundDetectionConfig.thresholdValue = max(2.0, 0.2*median_background_orig)
2180 starBackgroundDetectionConfig.thresholdType =
"pixel_stdev"
2182 n_above_max_per_amp = -99
2183 highest_detected_fraction_per_amp = float(
"nan")
2184 doCheckPerAmpDetFraction =
True
2186 minFootprints = self.config.star_background_min_footprints
2188 for nIter
in range(maxIter):
2189 currentThresh = starBackgroundDetectionConfig.thresholdValue
2190 nZeroEncountered = 0
2191 if nFootprintTemp == 0:
2192 zeroFactor = min(0.98, 0.9 + 0.01*nZeroEncountered)
2193 starBackgroundDetectionConfig.thresholdValue = zeroFactor*currentThresh
2194 self.log.warning(
"No footprints detected. Decreasing threshold to %.2f and rerunning.",
2195 starBackgroundDetectionConfig.thresholdValue)
2196 starBackgroundDetectionTask = lsst.meas.algorithms.SourceDetectionTask(
2197 config=starBackgroundDetectionConfig)
2199 tempDetections = starBackgroundDetectionTask.run(
2200 table=table, exposure=result.exposure, clearMask=
True)
2201 nFootprintTemp = len(tempDetections.sources)
2202 minFootprints = max(self.config.star_background_min_footprints,
2203 int(self.config.star_background_peak_fraction*tempDetections.numPosPeaks))
2204 minFootprints = min(200, minFootprints)
2205 nZeroEncountered += 1
2206 if nFootprintTemp >= minFootprints:
2208 detected_mask_planes,
2210 self.log.info(
"nIter = %d, thresh = %.2f: Fraction of pixels marked as DETECTED or "
2211 "DETECTED_NEGATIVE in star_background_detection = %.3f "
2212 "(max is %.3f; min is %.3f) nFootprint = %d (current min is %d)",
2213 nIter, starBackgroundDetectionConfig.thresholdValue,
2214 detected_fraction, maxDetFracForFinalBg, minDetFracForFinalBg,
2215 nFootprintTemp, minFootprints)
2216 self.metadata[
'adaptive_threshold_value'] = starBackgroundDetectionConfig.thresholdValue
2222 if nFootprintTemp > 0
and nFootprintTemp < minFootprints:
2225 if detected_fraction > maxDetFracForFinalBg
or nFootprintTemp <= minFootprints:
2226 starBackgroundDetectionConfig.thresholdValue = 1.07*currentThresh
2227 if nFootprintTemp < minFootprints
and detected_fraction > 0.9*maxDetFracForFinalBg:
2228 if nFootprintTemp == 1:
2229 starBackgroundDetectionConfig.thresholdValue = 1.4*currentThresh
2231 starBackgroundDetectionConfig.thresholdValue = 1.2*currentThresh
2233 if n_above_max_per_amp > 1:
2234 starBackgroundDetectionConfig.thresholdValue = 1.1*currentThresh
2235 if detected_fraction < minDetFracForFinalBg:
2236 starBackgroundDetectionConfig.thresholdValue = 0.8*currentThresh
2237 starBackgroundDetectionTask = lsst.meas.algorithms.SourceDetectionTask(
2238 config=starBackgroundDetectionConfig)
2240 tempDetections = starBackgroundDetectionTask.run(
2241 table=table, exposure=result.exposure, clearMask=
True)
2242 result.exposure.mask |= dilatedMask
2243 nFootprintTemp = len(tempDetections.sources)
2244 minFootprints = max(self.config.star_background_min_footprints,
2245 int(self.config.star_background_peak_fraction*tempDetections.numPosPeaks))
2246 minFootprints = min(200, minFootprints)
2249 self.log.info(
"nIter = %d, thresh = %.2f: Fraction of pixels marked as DETECTED or "
2250 "DETECTED_NEGATIVE in star_background_detection = %.3f "
2251 "(max is %.3f; min is %.3f); nFootprint = %d (current min is %d)",
2252 nIter, starBackgroundDetectionConfig.thresholdValue,
2253 detected_fraction, maxDetFracForFinalBg, minDetFracForFinalBg,
2254 nFootprintTemp, minFootprints)
2255 self.metadata[
'adaptive_threshold_value'] = starBackgroundDetectionConfig.thresholdValue
2257 n_amp = len(result.exposure.detector.getAmplifiers())
2258 if doCheckPerAmpDetFraction:
2259 n_above_max_per_amp, highest_detected_fraction_per_amp, no_zero_det_amps = \
2261 detected_mask_planes, bad_mask_planes)
2263 if not no_zero_det_amps:
2264 starBackgroundDetectionConfig.thresholdValue = 0.95*currentThresh
2266 if (detected_fraction < maxDetFracForFinalBg
and detected_fraction > minDetFracForFinalBg
2267 and n_above_max_per_amp < int(0.75*n_amp)
2268 and no_zero_det_amps
2269 and nFootprintTemp >= minFootprints):
2270 if (n_above_max_per_amp < max(1, int(0.15*n_amp))
2271 or detected_fraction < 0.85*maxDetFracForFinalBg):
2274 starBackgroundDetectionConfig.thresholdValue = 1.05*currentThresh
2275 self.log.debug(
"Number of amplifiers with detected fraction above the maximum "
2276 "(%.2f) excedes the maximum allowed (%d >= %d). Making a small "
2277 "tweak to the threshold (from %.2f to %.2f) and rerunning.",
2278 maxDetFracForFinalBg, n_above_max_per_amp, int(0.75*n_amp),
2279 currentThresh, starBackgroundDetectionConfig.thresholdValue)
2280 self.log.debug(
"n_above_max_per_amp = %d (abs max is %d)", n_above_max_per_amp, int(0.75*n_amp))
2284 self.log.info(
"Fraction of pixels marked as DETECTED or DETECTED_NEGATIVE is now %.5f "
2285 "(highest per amp section = %.5f)",
2286 detected_fraction, highest_detected_fraction_per_amp)
2288 if detected_fraction > maxDetFracForFinalBg:
2289 result.exposure.mask = dilatedMask
2290 self.log.warning(
"Final fraction of pixels marked as DETECTED or DETECTED_NEGATIVE "
2291 "was too large in star_background_detection = %.3f (max = %.3f). "
2292 "Reverting to dilated mask from PSF detection.",
2293 detected_fraction, maxDetFracForFinalBg)
2294 self.metadata[
'adaptive_threshold_value'] = float(
"nan")
2295 star_background = self.star_background.run(
2296 exposure=result.exposure,
2297 backgroundToPhotometricRatio=background_to_photometric_ratio,
2299 result.background.append(star_background[0])
2301 median_background = np.median(result.background.getImage().array)
2302 self.log.info(
"New initial median_background = %.2f", median_background)
2310 if detected_fraction < 0.5:
2311 dilatedMask = result.exposure.mask.clone()
2312 for maskName
in detected_mask_planes:
2314 detectedMaskBit = dilatedMask.getPlaneBitMask(maskName)
2315 detectedMaskSpanSet = SpanSet.fromMask(dilatedMask, detectedMaskBit)
2316 detectedMaskSpanSet = detectedMaskSpanSet.dilated(nPixToDilate)
2317 detectedMaskSpanSet = detectedMaskSpanSet.clippedTo(dilatedMask.getBBox())
2319 detectedMask = dilatedMask.getMaskPlane(maskName)
2320 dilatedMask.clearMaskPlane(detectedMask)
2322 detectedMaskSpanSet.setMask(dilatedMask, detectedMaskBit)
2325 detected_mask_planes,
2327 result.exposure.mask = dilatedMask
2328 self.log.debug(
"detected_fraction_orig = %.3f; detected_fraction_dilated = %.3f",
2329 detected_fraction_orig, detected_fraction_dilated)
2331 bbox = result.exposure.getBBox()
2342 pedestalBackgroundConfig = lsst.meas.algorithms.SubtractBackgroundConfig()
2343 for maskName
in bgIgnoreMasksToAdd:
2344 if (maskName
in result.exposure.mask.getMaskPlaneDict().keys()
2345 and maskName
not in pedestalBackgroundConfig.ignoredPixelMask):
2346 pedestalBackgroundConfig.ignoredPixelMask += [maskName]
2347 pedestalBackgroundConfig.statisticsProperty =
"MEDIAN"
2348 pedestalBackgroundConfig.approxOrderX = 0
2349 pedestalBackgroundConfig.binSize = min(32, int(math.ceil(max(bbox.width, bbox.height)/4.0)))
2350 self.log.info(
"Initial pedestal binSize = %d pixels", pedestalBackgroundConfig.binSize)
2351 cumulativePedestalLevel = 0.0
2354 relativeStoppingCriterion = 0.05
2358 absoluteStoppingCriterion = 0.5
2359 inPedestalIteration =
True
2360 while inPedestalIteration:
2361 cumulativePedestalPrev = cumulativePedestalLevel
2362 pedestalBackgroundTask = lsst.meas.algorithms.SubtractBackgroundTask(
2363 config=pedestalBackgroundConfig)
2364 pedestalBackgroundList = pedestalBackgroundTask.run(
2365 exposure=result.exposure,
2366 background=result.background,
2367 backgroundToPhotometricRatio=background_to_photometric_ratio,
2371 pedestalBackground = afwMath.BackgroundList()
2372 pedestalBackground.append(pedestalBackgroundList[-1])
2373 pedestalBackgroundLevel = pedestalBackground.getImage().array[0, 0]
2374 cumulativePedestalLevel += pedestalBackgroundLevel
2375 absoluteDelta = abs(cumulativePedestalLevel - cumulativePedestalPrev)
2376 if cumulativePedestalPrev != 0.0:
2377 relativeDelta = abs(absoluteDelta/cumulativePedestalPrev)
2380 self.log.info(
"Subtracted pedestalBackgroundLevel = %.4f (cumulative = %.4f; "
2381 "relativeDelta = %.4f; absoluteDelta = %.3f)",
2382 pedestalBackgroundLevel, cumulativePedestalLevel,
2383 relativeDelta, absoluteDelta)
2385 if (relativeDelta < relativeStoppingCriterion
2386 or absoluteDelta < absoluteStoppingCriterion):
2388 inPedestalIteration =
False
2391 if pedestalBackgroundConfig.binSize*2 < 2*max(bbox.width, bbox.height):
2392 pedestalBackgroundConfig.binSize = int(pedestalBackgroundConfig.binSize*2)
2393 self.log.info(
"Increasing pedestal binSize to %d pixels and remeasuring.",
2394 pedestalBackgroundConfig.binSize)
2397 self.log.warning(
"Reached maximum bin size. Exiting pedestal loop without meeting "
2398 "the convergence criteria (relativeDelta <= %.2f or "
2399 "absoluteDelta <= %.2f).",
2400 relativeStoppingCriterion, absoluteStoppingCriterion)
2401 inPedestalIteration =
False
2402 self.log.info(
"Final subtracted cumulativePedestalBackgroundLevel = %.4f", cumulativePedestalLevel)
2405 mask = result.exposure.mask
2406 for mp
in detected_mask_planes:
2407 if mp
not in mask.getMaskPlaneDict():
2408 mask.addMaskPlane(mp)
2409 mask &= ~mask.getPlaneBitMask(detected_mask_planes)