832 inputs = butlerQC.get(inputRefs)
833 exposures = inputs.pop(
"exposures")
835 exposure_record = inputRefs.exposures[0].dataId.records[
"exposure"]
836 if self.config.useButlerExposureRegion:
837 exposure_region = butlerQC.quantum.dataId.region
839 exposure_region =
None
841 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId)
843 astrometry_loader = lsst.meas.algorithms.ReferenceObjectLoader(
844 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.astrometry_ref_cat],
845 refCats=inputs.pop(
"astrometry_ref_cat"),
846 name=self.config.connections.astrometry_ref_cat,
847 config=self.config.astrometry_ref_loader, log=self.log)
848 self.astrometry.setRefObjLoader(astrometry_loader)
850 photometry_loader = lsst.meas.algorithms.ReferenceObjectLoader(
851 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.photometry_ref_cat],
852 refCats=inputs.pop(
"photometry_ref_cat"),
853 name=self.config.connections.photometry_ref_cat,
854 config=self.config.photometry_ref_loader, log=self.log)
855 self.photometry.match.setRefObjLoader(photometry_loader)
857 if self.config.doMaskDiffractionSpikes:
860 self.diffractionSpikeMask.setRefObjLoader(photometry_loader)
862 if self.config.do_illumination_correction:
863 background_flat = inputs.pop(
"background_flat")
864 illumination_correction = inputs.pop(
"illumination_correction")
866 background_flat =
None
867 illumination_correction =
None
870 if self.config.useButlerCamera:
871 if "camera_model" in inputs:
872 camera_model = inputs.pop(
"camera_model")
874 self.log.warning(
"useButlerCamera=True, but camera is not available for filter %s. The "
875 "astrometry fit will use the WCS already attached to the exposure.",
876 exposures[0].filter.bandLabel)
879 assert not inputs,
"runQuantum got more inputs than expected"
883 result = pipeBase.Struct(
885 stars_footprints=
None,
886 psf_stars_footprints=
None,
887 background_to_photometric_ratio=
None,
893 id_generator=id_generator,
894 background_flat=background_flat,
895 illumination_correction=illumination_correction,
896 camera_model=camera_model,
897 exposure_record=exposure_record,
898 exposure_region=exposure_region,
900 except pipeBase.AlgorithmError
as e:
901 error = pipeBase.AnnotatedPartialOutputsError.annotate(
905 result.psf_stars_footprints,
906 result.stars_footprints,
909 butlerQC.put(result, outputRefs)
912 butlerQC.put(result, outputRefs)
921 background_flat=None,
922 illumination_correction=None,
924 exposure_record=None,
925 exposure_region=None,
927 """Find stars and perform psf measurement, then do a deeper detection
928 and measurement and calibrate astrometry and photometry from that.
932 exposures : `lsst.afw.image.Exposure` or \
933 `list` [`lsst.afw.image.Exposure`]
934 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
935 Modified in-place during processing if only one is passed.
936 If two exposures are passed, treat them as snaps and combine
937 before doing further processing.
938 id_generator : `lsst.meas.base.IdGenerator`, optional
939 Object that generates source IDs and provides random seeds.
940 result : `lsst.pipe.base.Struct`, optional
941 Result struct that is modified to allow saving of partial outputs
942 for some failure conditions. If the task completes successfully,
943 this is also returned.
944 background_flat : `lsst.afw.image.Exposure`, optional
945 Background flat-field image.
946 illumination_correction : `lsst.afw.image.Exposure`, optional
947 Illumination correction image.
948 camera_model : `lsst.afw.cameraGeom.Camera`, optional
949 Camera to be used if constructing updated WCS.
950 exposure_record : `lsst.daf.butler.DimensionRecord`, optional
951 Butler metadata for the ``exposure`` dimension. Used if
952 constructing an updated WCS instead of the boresight and
953 orientation angle from the visit info.
954 exposure_region : `lsst.sphgeom.Region`, optional
955 The exposure region to use for the reference catalog filtering.
956 If `None`, this region will be set as a padded bbox + current WCS
961 result : `lsst.pipe.base.Struct`
962 Results as a struct with attributes:
965 Calibrated exposure, with pixels in nJy units.
966 (`lsst.afw.image.Exposure`)
968 Stars that were used to calibrate the exposure, with
969 calibrated fluxes and magnitudes.
970 (`astropy.table.Table`)
972 Footprints of stars that were used to calibrate the exposure.
973 (`lsst.afw.table.SourceCatalog`)
975 Stars that were used to determine the image PSF.
976 (`astropy.table.Table`)
977 ``psf_stars_footprints``
978 Footprints of stars that were used to determine the image PSF.
979 (`lsst.afw.table.SourceCatalog`)
981 Background that was fit to the exposure when detecting
982 ``stars``. (`lsst.afw.math.BackgroundList`)
983 ``applied_photo_calib``
984 Photometric calibration that was fit to the star catalog and
985 applied to the exposure. (`lsst.afw.image.PhotoCalib`)
986 This is `None` if ``config.do_calibrate_pixels`` is `False`.
987 ``astrometry_matches``
988 Reference catalog stars matches used in the astrometric fit.
989 (`list` [`lsst.afw.table.ReferenceMatch`] or
990 `lsst.afw.table.BaseCatalog`).
991 ``photometry_matches``
992 Reference catalog stars matches used in the photometric fit.
993 (`list` [`lsst.afw.table.ReferenceMatch`] or
994 `lsst.afw.table.BaseCatalog`).
996 Copy of the mask plane of `exposure`.
997 (`lsst.afw.image.Mask`)
1000 result = pipeBase.Struct()
1001 if id_generator
is None:
1002 id_generator = lsst.meas.base.IdGenerator()
1004 result.exposure = self.snap_combine.run(exposures).exposure
1005 self.log.info(
"Initial PhotoCalib: %s", result.exposure.getPhotoCalib())
1007 result.exposure.metadata[
"LSST CALIB ILLUMCORR APPLIED"] =
False
1010 if self.config.do_illumination_correction:
1011 if not result.exposure.metadata.get(
"LSST ISR FLAT APPLIED",
False):
1012 raise pipeBase.InvalidQuantumError(
1013 "Cannot use do_illumination_correction with an image that has not had a flat applied",
1022 result.exposure, camera_model, exposure_record=exposure_record
1024 elif exposure_record
is not None:
1028 ref_cat_source_density =
None
1029 if self.astrometry.refObjLoader
is not None:
1030 load_result = self.astrometry.load_reference_catalog(
1032 exposure_region=exposure_region
1034 ref_cat = load_result.refCat
1035 ref_cat_trimmed = ref_cat[(np.isfinite(ref_cat[
"coord_ra"])
1036 & np.isfinite(ref_cat[
"coord_dec"]))].copy(deep=
True)
1037 ref_cat_trimmed = ref_cat_trimmed.asAstropy()
1040 metadata_name =
"ref_cat_source_density"
1041 result.exposure.metadata[metadata_name.upper()] = ref_cat_source_density
1042 self.metadata[metadata_name] = ref_cat_source_density
1044 result.background =
None
1045 result.background_to_photometric_ratio =
None
1046 summary_stat_catalog =
None
1051 have_fit_psf =
False
1052 have_fit_astrometry =
False
1053 have_fit_photometry =
False
1058 illumination_correction,
1063 ref_cat_source_density=ref_cat_source_density,
1065 result.psf_stars_footprints = compute_psf_struct.detections_sources
1066 adaptive_det_res_struct = compute_psf_struct.adaptive_det_res_struct
1070 if self.config.do_adaptive_threshold_detection:
1071 metadata_name =
"psf_adaptive_threshold_value"
1072 result.exposure.metadata[metadata_name.upper()] = adaptive_det_res_struct.thresholdValue
1073 self.metadata[metadata_name] = adaptive_det_res_struct.thresholdValue
1074 metadata_name =
"psf_adaptive_include_threshold_multiplier"
1075 result.exposure.metadata[metadata_name.upper()] = \
1076 adaptive_det_res_struct.includeThresholdMultiplier
1077 self.metadata[metadata_name] = adaptive_det_res_struct.includeThresholdMultiplier
1082 if result.psf_stars_footprints[
"slot_Centroid_flag"].all():
1083 psf_shape = result.exposure.psf.computeShape(result.exposure.psf.getAveragePosition())
1085 shapelets_iq_score=result.exposure.info.getSummaryStats().shapeletsIqScore,
1086 n_sources=len(result.psf_stars_footprints),
1087 psf_shape_ixx=psf_shape.getIxx(),
1088 psf_shape_iyy=psf_shape.getIyy(),
1089 psf_shape_ixy=psf_shape.getIxy(),
1090 psf_size=psf_shape.getDeterminantRadius(),
1093 if result.background
is None:
1094 result.background = afwMath.BackgroundList()
1097 result.psf_stars = result.psf_stars_footprints.asAstropy()
1100 afwTable.updateSourceCoords(
1101 result.exposure.wcs,
1102 sourceList=result.psf_stars_footprints,
1103 include_covariance=self.config.do_include_astrometric_errors,
1106 result.exposure, result.psf_stars_footprints, exposure_region=exposure_region,
1107 load_result=load_result
1109 num_astrometry_matches = len(astrometry_matches)
1110 if "astrometry_matches" in self.config.optional_outputs:
1113 result.psf_stars = result.psf_stars_footprints.asAstropy()
1117 if self.config.doMaskDiffractionSpikes:
1118 self.diffractionSpikeMask.run(result.exposure)
1120 self.metadata[
'adaptive_threshold_value'] = float(
"nan")
1121 if self.config.do_remeasure_star_background
and self.config.do_adaptive_threshold_detection:
1124 background_to_photometric_ratio=result.background_to_photometric_ratio,
1132 background_to_photometric_ratio=result.background_to_photometric_ratio,
1133 adaptive_det_res_struct=adaptive_det_res_struct,
1134 num_astrometry_matches=num_astrometry_matches,
1136 psf = result.exposure.getPsf()
1137 psfSigma = psf.computeShape(result.exposure.getBBox().getCenter()).getDeterminantRadius()
1138 self.
_match_psf_stars(result.psf_stars_footprints, result.stars_footprints,
1142 afwTable.updateSourceCoords(
1143 result.exposure.wcs,
1144 sourceList=result.stars_footprints,
1145 include_covariance=self.config.do_include_astrometric_errors,
1148 summary_stat_catalog = result.stars_footprints
1149 result.stars = result.stars_footprints.asAstropy()
1150 self.metadata[
"star_count"] = np.sum(~result.stars[
"sky_source"])
1155 self.astrometry.check(result.exposure, result.stars_footprints, len(astrometry_matches))
1156 result.stars = result.stars_footprints.asAstropy()
1157 have_fit_astrometry =
True
1159 result.stars_footprints, photometry_matches, \
1160 photometry_meta, photo_calib = self.
_fit_photometry(result.exposure, result.stars_footprints)
1161 have_fit_photometry =
True
1162 self.metadata[
"photometry_matches_count"] = len(photometry_matches)
1165 result.stars = result.stars_footprints.asAstropy()
1169 summary_stat_catalog = result.stars_footprints
1170 if "photometry_matches" in self.config.optional_outputs:
1173 if "mask" in self.config.optional_outputs:
1174 result.mask = result.exposure.mask.clone()
1175 except pipeBase.AlgorithmError:
1176 if not have_fit_psf:
1177 result.exposure.setPsf(
None)
1178 if not have_fit_astrometry:
1179 result.exposure.setWcs(
None)
1180 if not have_fit_photometry:
1181 result.exposure.setPhotoCalib(
None)
1188 self.
_summarize(result.exposure, summary_stat_catalog, result.background,
1189 summary=result.exposure.info.getSummaryStats())
1192 self.
_summarize(result.exposure, summary_stat_catalog, result.background,
1193 summary=result.exposure.info.getSummaryStats())
1198 bgIgnoredPixelMasks = lsst.meas.algorithms.SubtractBackgroundConfig().ignoredPixelMask.list()
1199 for maskName
in self.config.background_stats_ignored_pixel_masks:
1200 if (maskName
in result.exposure.mask.getMaskPlaneDict().keys()
1201 and maskName
not in bgIgnoredPixelMasks):
1202 bgIgnoredPixelMasks += [maskName]
1204 statsCtrl = afwMath.StatisticsControl()
1205 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(bgIgnoredPixelMasks))
1207 statObj = afwMath.makeStatistics(result.exposure.getMaskedImage(), afwMath.MEDIAN, statsCtrl)
1208 median_bg, _ = statObj.getResult(afwMath.MEDIAN)
1209 self.metadata[
"bg_subtracted_skyPixel_instFlux_median"] = median_bg
1211 statObj = afwMath.makeStatistics(result.exposure.getMaskedImage(), afwMath.STDEV, statsCtrl)
1212 stdev_bg, _ = statObj.getResult(afwMath.STDEV)
1213 self.metadata[
"bg_subtracted_skyPixel_instFlux_stdev"] = stdev_bg
1215 self.metadata[
"bg_subtracted_skySource_flux_median"] = (
1216 np.median(result.stars[result.stars[
'sky_source']][self.config.background_stats_flux_column])
1218 self.metadata[
"bg_subtracted_skySource_flux_stdev"] = (
1219 np.std(result.stars[result.stars[
'sky_source']][self.config.background_stats_flux_column])
1222 if self.config.do_calibrate_pixels:
1226 background_to_photometric_ratio=result.background_to_photometric_ratio,
1228 result.applied_photo_calib = photo_calib
1230 result.applied_photo_calib =
None
1234 if self.config.run_sattle:
1238 populate_sattle_visit_cache(result.exposure.getInfo().getVisitInfo(),
1239 historical=self.config.sattle_historical)
1240 self.log.info(
'Successfully triggered load of sattle visit cache')
1241 except requests.exceptions.HTTPError:
1242 self.log.exception(
"Sattle visit cache update failed; continuing with image processing")
1292 """Find bright sources detected on an exposure and fit a PSF model to
1293 them, repairing likely cosmic rays before detection.
1295 Repair, detect, measure, and compute PSF twice, to ensure the PSF
1296 model does not include contributions from cosmic rays.
1300 result : `lsst.pipe.base.Struct`
1301 Result struct that is modified to allow saving of partial outputs
1302 for some failure conditions. Should contain at least the following
1305 - exposure : `lsst.afw.image.Exposure`
1306 Exposure to detect and measure bright stars on.
1307 - background : `lsst.afw.math.BackgroundList` | `None`
1308 Background that was fit to the exposure during detection.
1309 - background_to_photometric_ratio : `lsst.afw.image.Image` | `None`
1310 Image to convert photometric-flattened image to
1311 background-flattened image.
1312 id_generator : `lsst.meas.base.IdGenerator`
1313 Object that generates source IDs and provides random seeds.
1314 ref_cat_source_density : `float` or `None`, optional
1315 A rough estimate of the source density (in number per deg^2) in
1316 the region of the exposure as measured from the loaded reference
1318 TODO DM-54929: If not `None` use the source denstiy as an improved
1319 initial estimate for the adaptive thresholding with the goal of
1320 reducing the number of iterations required (namely, by increasing
1321 it for more crowded fields).
1325 sources : `lsst.afw.table.SourceCatalog`
1326 Catalog of detected bright sources.
1327 cell_set : `lsst.afw.math.SpatialCellSet`
1328 PSF candidates returned by the psf determiner.
1329 adaptive_det_res_struct : `lsst.pipe.base.Struct`
1330 Result struct from the adaptive threshold detection.
1334 This method modifies the exposure, background and
1335 background_to_photometric_ratio attributes of the result struct
1338 def log_psf(msg, addToMetadata=False):
1339 """Log the parameters of the psf and background, with a prepended
1340 message. There is also the option to add the PSF sigma to the task
1346 Message to prepend the log info with.
1347 addToMetadata : `bool`, optional
1348 Whether to add the final psf sigma value to the task
1349 metadata (the default is False).
1351 position = result.exposure.psf.getAveragePosition()
1352 sigma = result.exposure.psf.computeShape(position).getDeterminantRadius()
1353 dimensions = result.exposure.psf.computeImage(position).getDimensions()
1354 if result.background
is not None:
1355 median_background = np.median(result.background.getImage().array)
1357 median_background = 0.0
1358 self.log.info(
"%s sigma=%0.4f, dimensions=%s; median background=%0.2f",
1359 msg, sigma, dimensions, median_background)
1361 self.metadata[
"final_psf_sigma"] = sigma
1363 self.log.info(
"First pass detection with Gaussian PSF FWHM=%s pixels",
1364 self.config.install_simple_psf.fwhm)
1365 self.install_simple_psf.run(exposure=result.exposure)
1367 result.background = self.psf_subtract_background.run(
1368 exposure=result.exposure,
1369 backgroundToPhotometricRatio=result.background_to_photometric_ratio,
1371 log_psf(
"Initial PSF:")
1372 self.psf_repair.run(exposure=result.exposure, keepCRs=
True)
1374 table = afwTable.SourceTable.make(self.
psf_schema, id_generator.make_table_id_factory())
1375 if not self.config.do_adaptive_threshold_detection:
1376 adaptive_det_res_struct =
None
1379 detections = self.psf_detection.run(
1381 exposure=result.exposure,
1382 background=result.background,
1383 backgroundToPhotometricRatio=result.background_to_photometric_ratio,
1386 initialThreshold = self.config.psf_detection.thresholdValue
1387 initialThresholdMultiplier = self.config.psf_detection.includeThresholdMultiplier
1388 adaptive_det_res_struct = self.psf_adaptive_threshold_detection.run(
1389 table, result.exposure,
1390 initialThreshold=initialThreshold,
1391 initialThresholdMultiplier=initialThresholdMultiplier,
1393 detections = adaptive_det_res_struct.detections
1395 self.metadata[
"initial_psf_positive_footprint_count"] = detections.numPos
1396 self.metadata[
"initial_psf_negative_footprint_count"] = detections.numNeg
1397 self.metadata[
"initial_psf_positive_peak_count"] = detections.numPosPeaks
1398 self.metadata[
"initial_psf_negative_peak_count"] = detections.numNegPeaks
1400 if self.config.do_compute_shapelet_decomposition:
1403 self.log.info(
"Computing rough shapelet decomposition for PSF star candidates.")
1404 seed = id_generator.catalog_id & 0xFFFFFFFF
1405 shapelets_results = self.compute_shapelet_decomposition.run(
1406 masked_image=result.exposure.getMaskedImage(), catalog=detections.sources, seed=seed
1408 result.exposure.info.setSummaryStats(shapelets_results.shapelets_summary)
1409 detections.sources = shapelets_results.catalog
1411 self.psf_source_measurement.run(detections.sources, result.exposure)
1413 if self.config.do_compute_shapelet_decomposition:
1419 self.log.info(
"Computing shapelets_iq_score that includes power from the "
1420 "median centroid offset.")
1421 centroid_offset_scale = 0.5*np.sqrt(
1422 self.config.compute_shapelet_decomposition.max_footprint_area)
1423 shapelets_results = self.compute_shapelet_decomposition.compute_shapelets_iq_score(
1424 shapelets_results=shapelets_results,
1425 catalog=detections.sources,
1426 centroid_offset_scale=centroid_offset_scale,
1427 shapelets_base_name=self.compute_shapelet_decomposition.shapelets_base_name,
1430 result.exposure.info.setSummaryStats(shapelets_results.shapelets_summary)
1432 psf_result = self.psf_measure_psf.run(exposure=result.exposure, sources=detections.sources)
1439 if not self.config.do_adaptive_threshold_detection:
1443 self.install_simple_psf.run(exposure=result.exposure)
1445 log_psf(
"Rerunning with simple PSF:")
1453 self.psf_repair.run(exposure=result.exposure, keepCRs=
True)
1456 detections = self.psf_detection.run(
1458 exposure=result.exposure,
1459 background=result.background,
1460 backgroundToPhotometricRatio=result.background_to_photometric_ratio,
1462 self.psf_source_measurement.run(detections.sources, result.exposure)
1463 psf_result = self.psf_measure_psf.run(exposure=result.exposure, sources=detections.sources)
1464 self.metadata[
"simple_psf_positive_footprint_count"] = detections.numPos
1465 self.metadata[
"simple_psf_negative_footprint_count"] = detections.numNeg
1466 self.metadata[
"simple_psf_positive_peak_count"] = detections.numPosPeaks
1467 self.metadata[
"simple_psf_negative_peak_count"] = detections.numNegPeaks
1468 log_psf(
"Final PSF:", addToMetadata=
True)
1471 self.psf_repair.run(exposure=result.exposure)
1473 self.psf_source_measurement.run(detections.sources, result.exposure)
1477 return pipeBase.Struct(
1478 detections_sources=detections.sources,
1479 psf_result_cellSet=psf_result.cellSet,
1480 adaptive_det_res_struct=adaptive_det_res_struct,
1513 def _find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None,
1514 adaptive_det_res_struct=None, num_astrometry_matches=None):
1515 """Detect stars on an exposure that has a PSF model, and measure their
1516 PSF, circular aperture, compensated gaussian fluxes.
1520 exposure : `lsst.afw.image.Exposure`
1521 Exposure to detect and measure stars on.
1522 background : `lsst.afw.math.BackgroundList`
1523 Background that was fit to the exposure during detection;
1524 modified in-place during subsequent detection.
1525 id_generator : `lsst.meas.base.IdGenerator`
1526 Object that generates source IDs and provides random seeds.
1527 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1528 Image to convert photometric-flattened image to
1529 background-flattened image.
1533 stars : `SourceCatalog`
1534 Sources that are very likely to be stars, with a limited set of
1535 measurements performed on them.
1538 id_generator.make_table_id_factory())
1540 maxAdaptiveDetIter = 8
1542 if adaptive_det_res_struct
is not None:
1543 for adaptiveDetIter
in range(maxAdaptiveDetIter):
1544 adaptiveDetectionConfig = lsst.meas.algorithms.SourceDetectionConfig()
1545 if self.config.do_remeasure_star_background:
1546 adaptiveDetectionConfig.reEstimateBackground =
False
1548 adaptiveDetectionConfig.reEstimateBackground =
True
1549 adaptiveDetectionConfig.includeThresholdMultiplier = 2.0
1551 adaptive_det_res_struct.thresholdValue*adaptive_det_res_struct.includeThresholdMultiplier
1553 adaptiveDetectionConfig.thresholdValue = max(
1554 self.config.star_detection.thresholdValue,
1555 threshFactor*psfThreshold/adaptiveDetectionConfig.includeThresholdMultiplier
1557 self.log.info(
"Using adaptive threshold detection (nIter = %d) with "
1558 "thresholdValue = %.2f and multiplier = %.1f",
1559 adaptiveDetIter, adaptiveDetectionConfig.thresholdValue,
1560 adaptiveDetectionConfig.includeThresholdMultiplier)
1561 adaptiveDetectionTask = lsst.meas.algorithms.SourceDetectionTask(
1562 config=adaptiveDetectionConfig
1564 detections = adaptiveDetectionTask.run(
1567 background=background,
1568 backgroundToPhotometricRatio=background_to_photometric_ratio,
1570 nFootprint = len(detections.sources)
1573 for src
in detections.sources:
1574 nPeakSrc = len(src.getFootprint().getPeaks())
1578 minIsolated = min(400, max(3, 0.005*nPeak, 0.6*num_astrometry_matches))
1580 self.log.info(
"Adaptive threshold detection _find_stars nIter: %d; "
1581 "nPeak/nFootprint = %.2f (max is 800); nIsolated = %d (min is %.1f).",
1582 adaptiveDetIter, nPeak/nFootprint, nIsolated, minIsolated)
1583 if nPeak/nFootprint > 800
or nIsolated < minIsolated:
1584 threshFactor = max(0.01, 1.5*threshFactor)
1585 self.log.warning(
"nPeak/nFootprint = %.2f (max is 800); nIsolated = %d "
1586 "(min is %.1f).", nPeak/nFootprint, nIsolated, minIsolated)
1590 threshFactor *= 0.75
1591 self.log.warning(
"No footprints detected on image. Decreasing threshold "
1592 "factor to %.2f. and rerunning.", threshFactor)
1596 detections = self.star_detection.run(
1599 background=background,
1600 backgroundToPhotometricRatio=background_to_photometric_ratio,
1602 sources = detections.sources
1603 self.star_sky_sources.run(exposure.mask, id_generator.catalog_id, sources)
1605 n_sky_sources = np.sum(sources[
"sky_source"])
1606 if (self.config.do_downsample_footprints
1607 and (len(sources) - n_sky_sources) > self.config.downsample_max_footprints):
1608 if exposure.info.id
is None:
1609 self.log.warning(
"Exposure does not have a proper id; using 0 seed for downsample.")
1612 seed = exposure.info.id & 0xFFFFFFFF
1614 gen = np.random.RandomState(seed)
1617 indices = np.arange(len(sources))[~sources[
"sky_source"]]
1618 indices = gen.choice(
1620 size=self.config.downsample_max_footprints,
1623 skyIndices, = np.where(sources[
"sky_source"])
1624 indices = np.concatenate((indices, skyIndices))
1626 self.log.info(
"Downsampling from %d to %d non-sky-source footprints.", len(sources), len(indices))
1628 sel = np.zeros(len(sources), dtype=bool)
1630 sources = sources[sel]
1634 self.star_deblend.run(exposure=exposure, sources=sources)
1637 if not sources.isContiguous():
1638 sources = sources.copy(deep=
True)
1641 self.star_measurement.run(sources, exposure)
1642 self.metadata[
"post_deblend_source_count"] = np.sum(~sources[
"sky_source"])
1643 self.metadata[
"failed_deblend_source_count"] = np.sum(
1644 ~sources[
"sky_source"] & sources[
"deblend_failed"]
1646 self.metadata[
"saturated_source_count"] = np.sum(sources[
"base_PixelFlags_flag_saturated"])
1647 self.metadata[
"bad_source_count"] = np.sum(sources[
"base_PixelFlags_flag_bad"])
1652 self.star_normalized_calibration_flux.run(exposure=exposure, catalog=sources)
1653 self.star_apply_aperture_correction.run(sources, exposure.apCorrMap)
1654 self.star_catalog_calculation.run(sources)
1655 self.star_set_primary_flags.run(sources)
1657 result = self.star_selector.run(sources)
1659 if not result.sourceCat.isContiguous():
1660 return result.sourceCat.copy(deep=
True)
1662 return result.sourceCat
2045 """Remeasure the exposure's background with iterative adaptive
2046 threshold detection.
2050 result : `lsst.pipe.base.Struct`
2051 The current state of the result Strut from the run method of
2052 CalibrateImageTask. Will be modified in place.
2053 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
2054 Image to convert photometric-flattened image to
2055 background-flattened image.
2059 result : `lsst.pipe.base.Struct`
2060 The modified result Struct with the new background subtracted.
2065 backgroundOrig = result.background.clone()
2066 median_background_orig = np.median(backgroundOrig.getImage().array)
2067 self.log.info(
"Original median_background = %.2f", median_background_orig)
2068 result.exposure.image.array += result.background.getImage().array
2069 result.background = afwMath.BackgroundList()
2071 origMask = result.exposure.mask.clone()
2072 bad_mask_planes = [
"BAD",
"EDGE",
"NO_DATA"]
2073 detected_mask_planes = [
"DETECTED",
"DETECTED_NEGATIVE"]
2075 detected_mask_planes,
2077 minDetFracForFinalBg = 0.02
2078 maxDetFracForFinalBg = 0.93
2082 maxAdaptiveDetIter = 10
2083 for dilateIter
in range(maxAdaptiveDetIter):
2084 dilatedMask = origMask.clone()
2085 for maskName
in detected_mask_planes:
2087 detectedMaskBit = dilatedMask.getPlaneBitMask(maskName)
2088 detectedMaskSpanSet = SpanSet.fromMask(dilatedMask, detectedMaskBit)
2089 detectedMaskSpanSet = detectedMaskSpanSet.dilated(nPixToDilate)
2090 detectedMaskSpanSet = detectedMaskSpanSet.clippedTo(dilatedMask.getBBox())
2092 detectedMask = dilatedMask.getMaskPlane(maskName)
2093 dilatedMask.clearMaskPlane(detectedMask)
2095 detectedMaskSpanSet.setMask(dilatedMask, detectedMaskBit)
2098 detected_mask_planes,
2100 if detected_fraction_dilated < maxDetFracForFinalBg
or nPixToDilate == 1:
2105 result.exposure.mask = dilatedMask
2106 self.log.debug(
"detected_fraction_orig = %.3f; detected_fraction_dilated = %.3f",
2107 detected_fraction_orig, detected_fraction_dilated)
2108 n_above_max_per_amp = -99
2109 highest_detected_fraction_per_amp = float(
"nan")
2112 n_above_max_per_amp, highest_detected_fraction_per_amp, no_zero_det_amps = \
2114 detected_mask_planes, bad_mask_planes)
2115 self.log.debug(
"Dilated mask: n_above_max_per_amp = %d; "
2116 "highest_detected_fraction_per_amp = %.3f",
2117 n_above_max_per_amp, highest_detected_fraction_per_amp)
2119 bgIgnoreMasksToAdd = [
"SAT",
"SUSPECT",
"SPIKE"]
2120 detected_fraction = 1.0
2121 nFootprintTemp = 1e12
2122 starBackgroundDetectionConfig = lsst.meas.algorithms.SourceDetectionConfig()
2123 for maskName
in bgIgnoreMasksToAdd:
2124 if (maskName
in result.exposure.mask.getMaskPlaneDict().keys()
2125 and maskName
not in starBackgroundDetectionConfig.background.ignoredPixelMask):
2126 starBackgroundDetectionConfig.background.ignoredPixelMask += [maskName]
2127 starBackgroundDetectionConfig.doTempLocalBackground =
False
2128 starBackgroundDetectionConfig.nSigmaToGrow = 70.0
2129 starBackgroundDetectionConfig.reEstimateBackground =
False
2130 starBackgroundDetectionConfig.includeThresholdMultiplier = 1.0
2131 starBackgroundDetectionConfig.thresholdValue = max(2.0, 0.2*median_background_orig)
2132 starBackgroundDetectionConfig.thresholdType =
"pixel_stdev"
2134 n_above_max_per_amp = -99
2135 highest_detected_fraction_per_amp = float(
"nan")
2136 doCheckPerAmpDetFraction =
True
2138 minFootprints = self.config.star_background_min_footprints
2140 for nIter
in range(maxIter):
2141 currentThresh = starBackgroundDetectionConfig.thresholdValue
2142 nZeroEncountered = 0
2143 if nFootprintTemp == 0:
2144 zeroFactor = min(0.98, 0.9 + 0.01*nZeroEncountered)
2145 starBackgroundDetectionConfig.thresholdValue = zeroFactor*currentThresh
2146 self.log.warning(
"No footprints detected. Decreasing threshold to %.2f and rerunning.",
2147 starBackgroundDetectionConfig.thresholdValue)
2148 starBackgroundDetectionTask = lsst.meas.algorithms.SourceDetectionTask(
2149 config=starBackgroundDetectionConfig)
2151 tempDetections = starBackgroundDetectionTask.run(
2152 table=table, exposure=result.exposure, clearMask=
True)
2153 nFootprintTemp = len(tempDetections.sources)
2154 minFootprints = max(self.config.star_background_min_footprints,
2155 int(self.config.star_background_peak_fraction*tempDetections.numPosPeaks))
2156 minFootprints = min(200, minFootprints)
2157 nZeroEncountered += 1
2158 if nFootprintTemp >= minFootprints:
2160 detected_mask_planes,
2162 self.log.info(
"nIter = %d, thresh = %.2f: Fraction of pixels marked as DETECTED or "
2163 "DETECTED_NEGATIVE in star_background_detection = %.3f "
2164 "(max is %.3f; min is %.3f) nFootprint = %d (current min is %d)",
2165 nIter, starBackgroundDetectionConfig.thresholdValue,
2166 detected_fraction, maxDetFracForFinalBg, minDetFracForFinalBg,
2167 nFootprintTemp, minFootprints)
2168 self.metadata[
'adaptive_threshold_value'] = starBackgroundDetectionConfig.thresholdValue
2174 if nFootprintTemp > 0
and nFootprintTemp < minFootprints:
2177 if detected_fraction > maxDetFracForFinalBg
or nFootprintTemp <= minFootprints:
2178 starBackgroundDetectionConfig.thresholdValue = 1.07*currentThresh
2179 if nFootprintTemp < minFootprints
and detected_fraction > 0.9*maxDetFracForFinalBg:
2180 if nFootprintTemp == 1:
2181 starBackgroundDetectionConfig.thresholdValue = 1.4*currentThresh
2183 starBackgroundDetectionConfig.thresholdValue = 1.2*currentThresh
2185 if n_above_max_per_amp > 1:
2186 starBackgroundDetectionConfig.thresholdValue = 1.1*currentThresh
2187 if detected_fraction < minDetFracForFinalBg:
2188 starBackgroundDetectionConfig.thresholdValue = 0.8*currentThresh
2189 starBackgroundDetectionTask = lsst.meas.algorithms.SourceDetectionTask(
2190 config=starBackgroundDetectionConfig)
2192 tempDetections = starBackgroundDetectionTask.run(
2193 table=table, exposure=result.exposure, clearMask=
True)
2194 result.exposure.mask |= dilatedMask
2195 nFootprintTemp = len(tempDetections.sources)
2196 minFootprints = max(self.config.star_background_min_footprints,
2197 int(self.config.star_background_peak_fraction*tempDetections.numPosPeaks))
2198 minFootprints = min(200, minFootprints)
2201 self.log.info(
"nIter = %d, thresh = %.2f: Fraction of pixels marked as DETECTED or "
2202 "DETECTED_NEGATIVE in star_background_detection = %.3f "
2203 "(max is %.3f; min is %.3f); nFootprint = %d (current min is %d)",
2204 nIter, starBackgroundDetectionConfig.thresholdValue,
2205 detected_fraction, maxDetFracForFinalBg, minDetFracForFinalBg,
2206 nFootprintTemp, minFootprints)
2207 self.metadata[
'adaptive_threshold_value'] = starBackgroundDetectionConfig.thresholdValue
2209 n_amp = len(result.exposure.detector.getAmplifiers())
2210 if doCheckPerAmpDetFraction:
2211 n_above_max_per_amp, highest_detected_fraction_per_amp, no_zero_det_amps = \
2213 detected_mask_planes, bad_mask_planes)
2215 if not no_zero_det_amps:
2216 starBackgroundDetectionConfig.thresholdValue = 0.95*currentThresh
2218 if (detected_fraction < maxDetFracForFinalBg
and detected_fraction > minDetFracForFinalBg
2219 and n_above_max_per_amp < int(0.75*n_amp)
2220 and no_zero_det_amps
2221 and nFootprintTemp >= minFootprints):
2222 if (n_above_max_per_amp < max(1, int(0.15*n_amp))
2223 or detected_fraction < 0.85*maxDetFracForFinalBg):
2226 starBackgroundDetectionConfig.thresholdValue = 1.05*currentThresh
2227 self.log.debug(
"Number of amplifiers with detected fraction above the maximum "
2228 "(%.2f) excedes the maximum allowed (%d >= %d). Making a small "
2229 "tweak to the threshold (from %.2f to %.2f) and rerunning.",
2230 maxDetFracForFinalBg, n_above_max_per_amp, int(0.75*n_amp),
2231 currentThresh, starBackgroundDetectionConfig.thresholdValue)
2232 self.log.debug(
"n_above_max_per_amp = %d (abs max is %d)", n_above_max_per_amp, int(0.75*n_amp))
2236 self.log.info(
"Fraction of pixels marked as DETECTED or DETECTED_NEGATIVE is now %.5f "
2237 "(highest per amp section = %.5f)",
2238 detected_fraction, highest_detected_fraction_per_amp)
2240 if detected_fraction > maxDetFracForFinalBg:
2241 result.exposure.mask = dilatedMask
2242 self.log.warning(
"Final fraction of pixels marked as DETECTED or DETECTED_NEGATIVE "
2243 "was too large in star_background_detection = %.3f (max = %.3f). "
2244 "Reverting to dilated mask from PSF detection.",
2245 detected_fraction, maxDetFracForFinalBg)
2246 self.metadata[
'adaptive_threshold_value'] = float(
"nan")
2247 star_background = self.star_background.run(
2248 exposure=result.exposure,
2249 backgroundToPhotometricRatio=background_to_photometric_ratio,
2251 result.background.append(star_background[0])
2253 median_background = np.median(result.background.getImage().array)
2254 self.log.info(
"New initial median_background = %.2f", median_background)
2262 if detected_fraction < 0.5:
2263 dilatedMask = result.exposure.mask.clone()
2264 for maskName
in detected_mask_planes:
2266 detectedMaskBit = dilatedMask.getPlaneBitMask(maskName)
2267 detectedMaskSpanSet = SpanSet.fromMask(dilatedMask, detectedMaskBit)
2268 detectedMaskSpanSet = detectedMaskSpanSet.dilated(nPixToDilate)
2269 detectedMaskSpanSet = detectedMaskSpanSet.clippedTo(dilatedMask.getBBox())
2271 detectedMask = dilatedMask.getMaskPlane(maskName)
2272 dilatedMask.clearMaskPlane(detectedMask)
2274 detectedMaskSpanSet.setMask(dilatedMask, detectedMaskBit)
2277 detected_mask_planes,
2279 result.exposure.mask = dilatedMask
2280 self.log.debug(
"detected_fraction_orig = %.3f; detected_fraction_dilated = %.3f",
2281 detected_fraction_orig, detected_fraction_dilated)
2283 bbox = result.exposure.getBBox()
2294 pedestalBackgroundConfig = lsst.meas.algorithms.SubtractBackgroundConfig()
2295 for maskName
in bgIgnoreMasksToAdd:
2296 if (maskName
in result.exposure.mask.getMaskPlaneDict().keys()
2297 and maskName
not in pedestalBackgroundConfig.ignoredPixelMask):
2298 pedestalBackgroundConfig.ignoredPixelMask += [maskName]
2299 pedestalBackgroundConfig.statisticsProperty =
"MEDIAN"
2300 pedestalBackgroundConfig.approxOrderX = 0
2301 pedestalBackgroundConfig.binSize = min(32, int(math.ceil(max(bbox.width, bbox.height)/4.0)))
2302 self.log.info(
"Initial pedestal binSize = %d pixels", pedestalBackgroundConfig.binSize)
2303 cumulativePedestalLevel = 0.0
2306 relativeStoppingCriterion = 0.05
2310 absoluteStoppingCriterion = 0.5
2311 inPedestalIteration =
True
2312 while inPedestalIteration:
2313 cumulativePedestalPrev = cumulativePedestalLevel
2314 pedestalBackgroundTask = lsst.meas.algorithms.SubtractBackgroundTask(
2315 config=pedestalBackgroundConfig)
2316 pedestalBackgroundList = pedestalBackgroundTask.run(
2317 exposure=result.exposure,
2318 background=result.background,
2319 backgroundToPhotometricRatio=background_to_photometric_ratio,
2323 pedestalBackground = afwMath.BackgroundList()
2324 pedestalBackground.append(pedestalBackgroundList[-1])
2325 pedestalBackgroundLevel = pedestalBackground.getImage().array[0, 0]
2326 cumulativePedestalLevel += pedestalBackgroundLevel
2327 absoluteDelta = abs(cumulativePedestalLevel - cumulativePedestalPrev)
2328 if cumulativePedestalPrev != 0.0:
2329 relativeDelta = abs(absoluteDelta/cumulativePedestalPrev)
2332 self.log.info(
"Subtracted pedestalBackgroundLevel = %.4f (cumulative = %.4f; "
2333 "relativeDelta = %.4f; absoluteDelta = %.3f)",
2334 pedestalBackgroundLevel, cumulativePedestalLevel,
2335 relativeDelta, absoluteDelta)
2337 if (relativeDelta < relativeStoppingCriterion
2338 or absoluteDelta < absoluteStoppingCriterion):
2340 inPedestalIteration =
False
2343 if pedestalBackgroundConfig.binSize*2 < 2*max(bbox.width, bbox.height):
2344 pedestalBackgroundConfig.binSize = int(pedestalBackgroundConfig.binSize*2)
2345 self.log.info(
"Increasing pedestal binSize to %d pixels and remeasuring.",
2346 pedestalBackgroundConfig.binSize)
2349 self.log.warning(
"Reached maximum bin size. Exiting pedestal loop without meeting "
2350 "the convergence criteria (relativeDelta <= %.2f or "
2351 "absoluteDelta <= %.2f).",
2352 relativeStoppingCriterion, absoluteStoppingCriterion)
2353 inPedestalIteration =
False
2354 self.log.info(
"Final subtracted cumulativePedestalBackgroundLevel = %.4f", cumulativePedestalLevel)
2357 mask = result.exposure.mask
2358 for mp
in detected_mask_planes:
2359 if mp
not in mask.getMaskPlaneDict():
2360 mask.addMaskPlane(mp)
2361 mask &= ~mask.getPlaneBitMask(detected_mask_planes)