def run(display=False): """Subtract background, mask cosmic rays, then detect and measure """ # Create the tasks; if you use the default config then you don't have to construct the config, # as shown in constructing backgroundTask, but constructing the config makes it easier to modify repairConfig = RepairTask.ConfigClass() repairTask = RepairTask(config=repairConfig) backgroundTask = SubtractBackgroundTask() damConfig = DetectAndMeasureTask.ConfigClass() damConfig.detection.thresholdValue = 5.0 damConfig.detection.includeThresholdMultiplier = 1.0 damConfig.measurement.doApplyApCorr = "yes" detectAndMeasureTask = DetectAndMeasureTask(config=damConfig) # load the data # Exposure ID and the number of bits required for exposure IDs are usually obtained from a data repo, # but here we pick reasonable values (there are 64 bits to share between exposure IDs and source IDs). exposure = loadData() exposureIdInfo = ExposureIdInfo(expId=1, expBits=5) # repair cosmic rays repairTask.run(exposure=exposure) # subtract an initial estimate of background level backgroundTask.run(exposure=exposure) # detect and measure damRes = detectAndMeasureTask.run(exposure=exposure, exposureIdInfo=exposureIdInfo) if display: displayAstrometry(frame=2, exposure=damRes.exposure, sourceCat=damRes.sourceCat, pause=False)
def distort(self, sourceCat, exposure): """!Calculate distorted source positions CCD images are often affected by optical distortion that makes the astrometric solution higher order than linear. Unfortunately, most (all?) matching algorithms require that the distortion be small or zero, and so it must be removed. We do this by calculating (un-)distorted positions, based on a known optical distortion model in the Ccd. The distortion correction moves sources, so we return the distorted bounding box. \param[in] exposure Exposure to process \param[in,out] sourceCat SourceCatalog; getX() and getY() will be used as inputs, with distorted points in "centroid.distorted" field. \return bounding box of distorted exposure """ detector = exposure.getDetector() pixToTanXYTransform = None if detector is None: self.log.warn( "No detector associated with exposure; assuming null distortion" ) else: tanSys = detector.makeCameraSys(TAN_PIXELS) pixToTanXYTransform = detector.getTransformMap().get(tanSys) if pixToTanXYTransform is None: self.log.info("Null distortion correction") for s in sourceCat: s.set(self.centroidKey, s.getCentroid()) s.set(self.centroidErrKey, s.getCentroidErr()) s.set(self.centroidFlagKey, s.getCentroidFlag()) return exposure.getBBox() # Distort source positions self.log.info("Applying distortion correction") for s in sourceCat: centroid = pixToTanXYTransform.forwardTransform(s.getCentroid()) s.set(self.centroidKey, centroid) s.set(self.centroidErrKey, s.getCentroidErr()) s.set(self.centroidFlagKey, s.getCentroidFlag()) # Get distorted image size so that astrometry_net does not clip. bboxD = afwGeom.Box2D() for corner in detector.getCorners(TAN_PIXELS): bboxD.include(corner) if lsstDebug.Info(__name__).display: frame = lsstDebug.Info(__name__).frame pause = lsstDebug.Info(__name__).pause displayAstrometry(sourceCat=sourceCat, distortedCentroidKey=self.centroidKey, exposure=exposure, frame=frame, pause=pause) return afwGeom.Box2I(bboxD)
def display(self, itemName, exposure, sourceCat=None): """Display exposure and sources on next frame, if display of itemName has been requested @param[in] itemName name of item in debugInfo @param[in] exposure exposure to display @param[in] sourceCat source catalog to display """ val = getDebugFrame(self._display, itemName) if not val: return displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self._frame, pause=False) self._frame += 1
def loadAndMatch(self, exposure, sourceCat, bbox=None): """!Load reference objects overlapping an exposure and match to sources detected on that exposure @param[in] exposure exposure whose WCS is to be fit @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog) @param[in] bbox bounding box go use for finding reference objects; if None, use exposure's bbox @return an lsst.pipe.base.Struct with these fields: - refCat reference object catalog of objects that overlap the exposure (with some margin) (an lsst::afw::table::SimpleCatalog) - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch - matchMeta metadata about the field (an lsst.daf.base.PropertyList) @note ignores config.forceKnownWcs """ with self.distortionContext(sourceCat=sourceCat, exposure=exposure) as bbox: if not self.solver: self.makeSubtask("solver") astrom = self.solver.useKnownWcs( sourceCat=sourceCat, exposure=exposure, bbox=bbox, calculateSip=False, ) if astrom is None or astrom.getWcs() is None: raise RuntimeError("Unable to solve astrometry") matches = astrom.getMatches() matchMeta = astrom.getMatchMetadata() if matches is None or len(matches) == 0: raise RuntimeError("No astrometric matches") self.log.info("%d astrometric matches" % (len(matches))) if self._display: frame = lsstDebug.Info(__name__).frame displayAstrometry(exposure=exposure, sourceCat=sourceCat, matches=matches, frame=frame, pause=False) return pipeBase.Struct( refCat=astrom.refCat, matches=matches, matchMeta=matchMeta, )
def _astrometry(self, sourceCat, exposure, bbox=None): """!Solve astrometry to produce WCS \param[in] sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog \param[in,out] exposure Exposure to process, an lsst.afw.image.ExposureF or D; wcs is updated \param[in] bbox Bounding box, or None to use exposure \return a pipe.base.Struct with fields: - refCat reference object catalog of objects that overlap the exposure (with some margin) (an lsst::afw::table::SimpleCatalog) - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch - matchMeta metadata about the field (an lsst.daf.base.PropertyList) """ self.log.info("Solving astrometry") if bbox is None: bbox = exposure.getBBox() if not self.solver: self.makeSubtask("solver") astrom = self.solver.determineWcs(sourceCat=sourceCat, exposure=exposure, bbox=bbox) if astrom is None or astrom.getWcs() is None: raise RuntimeError("Unable to solve astrometry") matches = astrom.getMatches() matchMeta = astrom.getMatchMetadata() if matches is None or len(matches) == 0: raise RuntimeError("No astrometric matches") self.log.info("%d astrometric matches" % (len(matches))) # Note that this is the Wcs for the provided positions, which may be distorted exposure.setWcs(astrom.getWcs()) if self._display: frame = lsstDebug.Info(__name__).frame displayAstrometry(exposure=exposure, sourceCat=sourceCat, matches=matches, frame=frame, pause=False) return pipeBase.Struct( refCat=astrom.refCat, matches=matches, matchMeta=matchMeta, )
def run(display=False): """Subtract background, mask cosmic rays, then detect and measure """ # Create the tasks; note that background estimation is performed by a function, # not a task, though it has a config repairConfig = RepairTask.ConfigClass() repairTask = RepairTask(config=repairConfig) backgroundConfig = estimateBackground.ConfigClass() damConfig = DetectAndMeasureTask.ConfigClass() damConfig.detection.thresholdValue = 5.0 damConfig.detection.includeThresholdMultiplier = 1.0 damConfig.measurement.doApplyApCorr = "yes" detectAndMeasureTask = DetectAndMeasureTask(config=damConfig) # load the data # Exposure ID and the number of bits required for exposure IDs are usually obtained from a data repo, # but here we pick reasonable values (there are 64 bits to share between exposure IDs and source IDs). exposure = loadData() exposureIdInfo = ExposureIdInfo(expId=1, expBits=5) # repair cosmic rays repairTask.run(exposure=exposure) # subtract an initial estimate of background level estBg, exposure = estimateBackground( exposure=exposure, backgroundConfig=backgroundConfig, subtract=True, ) # detect and measure damRes = detectAndMeasureTask.run(exposure=exposure, exposureIdInfo=exposureIdInfo) if display: displayAstrometry(frame=2, exposure=damRes.exposure, sourceCat=damRes.sourceCat, pause=False)
def run(display=False): """Subtract background, mask cosmic rays, then detect and measure """ # Create the tasks; note that background estimation is performed by a function, # not a task, though it has a config repairConfig = RepairTask.ConfigClass() repairTask = RepairTask(config=repairConfig) backgroundConfig = estimateBackground.ConfigClass() damConfig = DetectAndMeasureTask.ConfigClass() damConfig.detection.thresholdValue = 5.0 damConfig.detection.includeThresholdMultiplier = 1.0 damConfig.measurement.doApplyApCorr = "yes" detectAndMeasureTask = DetectAndMeasureTask(config=damConfig) # load the data # Exposure ID and the number of bits required for exposure IDs are usually obtained from a data repo, # but here we pick reasonable values (there are 64 bits to share between exposure IDs and source IDs). exposure = loadData() exposureIdInfo = ExposureIdInfo(expId=1, expBits=5) # repair cosmic rays repairTask.run(exposure=exposure) # subtract an initial estimate of background level estBg, exposure = estimateBackground( exposure = exposure, backgroundConfig = backgroundConfig, subtract = True, ) # detect and measure damRes = detectAndMeasureTask.run(exposure=exposure, exposureIdInfo=exposureIdInfo) if display: displayAstrometry(frame=2, exposure=damRes.exposure, sourceCat=damRes.sourceCat, pause=False)
def refitWcs(self, sourceCat, exposure, matches): """!A final Wcs solution after matching and removing distortion Specifically, fitting the non-linear part, since the linear part has been provided by the matching engine. @param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog @param exposure Exposure of interest, an lsst.afw.image.ExposureF or D @param matches Astrometric matches, as a list of lsst.afw.table.ReferenceMatch @return the resolved-Wcs object, or None if config.solver.calculateSip is False. """ sip = None if self.config.solver.calculateSip: self.log.info("Refitting WCS") origMatches = matches wcs = exposure.getWcs() import lsstDebug display = lsstDebug.Info(__name__).display frame = lsstDebug.Info(__name__).frame pause = lsstDebug.Info(__name__).pause def fitWcs(initialWcs, title=None): """!Do the WCS fitting and display of the results""" sip = makeCreateWcsWithSip(matches, initialWcs, self.config.solver.sipOrder) resultWcs = sip.getNewWcs() if display: showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame, title=title, pause=pause) return resultWcs, sip.getScatterOnSky() numRejected = 0 try: for i in range(self.config.rejectIter): wcs, scatter = fitWcs(wcs, title="Iteration %d" % i) ref = np.array([wcs.skyToPixel(m.first.getCoord()) for m in matches]) src = np.array([m.second.getCentroid() for m in matches]) diff = ref - src rms = diff.std() trimmed = [] for d, m in zip(diff, matches): if np.all(np.abs(d) < self.config.rejectThresh*rms): trimmed.append(m) else: numRejected += 1 if len(matches) == len(trimmed): break matches = trimmed # Final fit after rejection iterations wcs, scatter = fitWcs(wcs, title="Final astrometry") except lsst.pex.exceptions.LengthError as e: self.log.warn("Unable to fit SIP: %s", e) self.log.info("Astrometric scatter: %f arcsec (%d matches, %d rejected)", scatter.asArcseconds(), len(matches), numRejected) exposure.setWcs(wcs) # Apply WCS to sources updateSourceCoords(wcs, sourceCat) else: self.log.warn("Not calculating a SIP solution; matches may be suspect") if self._display: frame = lsstDebug.Info(__name__).frame displayAstrometry(exposure=exposure, sourceCat=sourceCat, matches=matches, frame=frame, pause=False) return sip
def run(self, exposure, exposureIdInfo=None, background=None, icSourceCat=None): """!Calibrate an exposure (science image or coadd) @param[in,out] exposure exposure to calibrate (an lsst.afw.image.ExposureF or similar); in: - MaskedImage - Psf out: - MaskedImage has background subtracted - Wcs is replaced - PhotoCalib is replaced @param[in] exposureIdInfo ID info for exposure (an lsst.obs.base.ExposureIdInfo) If not provided, returned SourceCatalog IDs will not be globally unique. @param[in,out] background background model already subtracted from exposure (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted, though that is unusual for calibration. A refined background model is output. @param[in] icSourceCat A SourceCatalog from CharacterizeImageTask from which we can copy some fields. @return pipe_base Struct containing these fields: - exposure calibrate science exposure with refined WCS and PhotoCalib - background model of background subtracted from exposure (an lsst.afw.math.BackgroundList) - sourceCat catalog of measured sources - astromMatches list of source/refObj matches from the astrometry solver """ # detect, deblend and measure sources if exposureIdInfo is None: exposureIdInfo = ExposureIdInfo() if background is None: background = BackgroundList() sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits) table = SourceTable.make(self.schema, sourceIdFactory) table.setMetadata(self.algMetadata) detRes = self.detection.run(table=table, exposure=exposure, doSmooth=True) sourceCat = detRes.sources if detRes.fpSets.background: for bg in detRes.fpSets.background: background.append(bg) if self.config.doSkySources: skySourceFootprints = self.skySources.run(mask=exposure.mask, seed=exposureIdInfo.expId) if skySourceFootprints: for foot in skySourceFootprints: s = sourceCat.addNew() s.setFootprint(foot) s.set(self.skySourceKey, True) if self.config.doDeblend: self.deblend.run(exposure=exposure, sources=sourceCat) self.measurement.run( measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId ) if self.config.doApCorr: self.applyApCorr.run( catalog=sourceCat, apCorrMap=exposure.getInfo().getApCorrMap() ) self.catalogCalculation.run(sourceCat) self.setPrimaryFlags.run(sourceCat, includeDeblend=self.config.doDeblend) if icSourceCat is not None and \ len(self.config.icSourceFieldsToCopy) > 0: self.copyIcSourceFields(icSourceCat=icSourceCat, sourceCat=sourceCat) # TODO DM-11568: this contiguous check-and-copy could go away if we # reserve enough space during SourceDetection and/or SourceDeblend. # NOTE: sourceSelectors require contiguous catalogs, so ensure # contiguity now, so views are preserved from here on. if not sourceCat.isContiguous(): sourceCat = sourceCat.copy(deep=True) # perform astrometry calibration: # fit an improved WCS and update the exposure's WCS in place astromMatches = None matchMeta = None if self.config.doAstrometry: try: astromRes = self.astrometry.run( exposure=exposure, sourceCat=sourceCat, ) astromMatches = astromRes.matches matchMeta = astromRes.matchMeta except Exception as e: if self.config.requireAstrometry: raise self.log.warn("Unable to perform astrometric calibration " "(%s): attempting to proceed" % e) # compute photometric calibration if self.config.doPhotoCal: try: photoRes = self.photoCal.run(exposure, sourceCat=sourceCat, expId=exposureIdInfo.expId) exposure.setPhotoCalib(photoRes.photoCalib) # TODO: reword this to phrase it in terms of the calibration factor? self.log.info("Photometric zero-point: %f" % photoRes.photoCalib.instFluxToMagnitude(1.0)) self.setMetadata(exposure=exposure, photoRes=photoRes) except Exception as e: if self.config.requirePhotoCal: raise self.log.warn("Unable to perform photometric calibration " "(%s): attempting to proceed" % e) self.setMetadata(exposure=exposure, photoRes=None) if self.config.doInsertFakes: self.insertFakes.run(exposure, background=background) table = SourceTable.make(self.schema, sourceIdFactory) table.setMetadata(self.algMetadata) detRes = self.detection.run(table=table, exposure=exposure, doSmooth=True) sourceCat = detRes.sources if detRes.fpSets.background: for bg in detRes.fpSets.background: background.append(bg) if self.config.doDeblend: self.deblend.run(exposure=exposure, sources=sourceCat) self.measurement.run( measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId ) if self.config.doApCorr: self.applyApCorr.run( catalog=sourceCat, apCorrMap=exposure.getInfo().getApCorrMap() ) self.catalogCalculation.run(sourceCat) if icSourceCat is not None and len(self.config.icSourceFieldsToCopy) > 0: self.copyIcSourceFields(icSourceCat=icSourceCat, sourceCat=sourceCat) frame = getDebugFrame(self._display, "calibrate") if frame: displayAstrometry( sourceCat=sourceCat, exposure=exposure, matches=astromMatches, frame=frame, pause=False, ) return pipeBase.Struct( exposure=exposure, background=background, sourceCat=sourceCat, astromMatches=astromMatches, matchMeta=matchMeta, # These are duplicate entries with different names for use with # gen3 middleware outputExposure=exposure, outputCat=sourceCat, outputBackground=background, )
def calibrate(self, exposure, exposureIdInfo=None, background=None, icSourceCat=None): """!Calibrate an exposure (science image or coadd) @param[in,out] exposure exposure to calibrate (an lsst.afw.image.ExposureF or similar); in: - MaskedImage - Psf out: - MaskedImage has background subtracted - Wcs is replaced - Calib zero-point is set @param[in] exposureIdInfo ID info for exposure (an lsst.daf.butlerUtils.ExposureIdInfo) If not provided, returned SourceCatalog IDs will not be globally unique. @param[in,out] background background model already subtracted from exposure (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted, though that is unusual for calibration. A refined background model is output. @param[in] icSourceCat A SourceCatalog from CharacterizeImageTask from which we can copy some fields. @return pipe_base Struct containing these fields: - exposure calibrate science exposure with refined WCS and Calib - background model of background subtracted from exposure (an lsst.afw.math.BackgroundList) - sourceCat catalog of measured sources - astromMatches list of source/refObj matches from the astrometry solver """ # detect, deblend and measure sources if exposureIdInfo is None: exposureIdInfo = ExposureIdInfo() procRes = self.detectAndMeasure.run( exposure = exposure, exposureIdInfo = exposureIdInfo, background = background, ) background = procRes.background sourceCat = procRes.sourceCat if icSourceCat is not None and len(self.config.icSourceFieldsToCopy) > 0: self.copyIcSourceFields(icSourceCat=icSourceCat, sourceCat=sourceCat) # perform astrometry calibration: # fit an improved WCS and update the exposure's WCS in place astromMatches = None if self.config.doAstrometry: try: astromRes = self.astrometry.run( exposure = exposure, sourceCat = sourceCat, ) astromMatches = astromRes.matches except Exception as e: if self.config.requireAstrometry: raise self.log.warn("Unable to perform astrometric calibration (%s): attempting to proceed" % e) # compute photometric calibration if self.config.doPhotoCal: try: if astromMatches is None: astromRes = self.astrometry.loadAndMatch(exposure=exposure, sourceCat=sourceCat) photoRes = self.photoCal.run(exposure, astromMatches) exposure.getCalib().setFluxMag0(photoRes.calib.getFluxMag0()) self.log.info("Photometric zero-point: %f" % photoRes.calib.getMagnitude(1.0)) self.setMetadata(exposure=exposure, photoRes=photoRes) except Exception as e: if self.config.requirePhotoCal: raise self.log.warn("Unable to perform photometric calibration (%s): attempting to proceed" % e) self.setMetadata(exposure=exposure, photoRes=None) frame = getDebugFrame(self._display, "calibrate") if frame: displayAstrometry( sourceCat = sourceCat, exposure = exposure, matches = astromMatches, frame = frame, pause = False, ) return pipeBase.Struct( exposure = exposure, background = background, sourceCat = sourceCat, astromMatches = astromMatches, )
def calibrate(self, exposure, exposureIdInfo=None, background=None, icSourceCat=None): """!Calibrate an exposure (science image or coadd) @param[in,out] exposure exposure to calibrate (an lsst.afw.image.ExposureF or similar); in: - MaskedImage - Psf out: - MaskedImage has background subtracted - Wcs is replaced - Calib zero-point is set @param[in] exposureIdInfo ID info for exposure (an lsst.obs.base.ExposureIdInfo) If not provided, returned SourceCatalog IDs will not be globally unique. @param[in,out] background background model already subtracted from exposure (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted, though that is unusual for calibration. A refined background model is output. @param[in] icSourceCat A SourceCatalog from CharacterizeImageTask from which we can copy some fields. @return pipe_base Struct containing these fields: - exposure calibrate science exposure with refined WCS and Calib - background model of background subtracted from exposure (an lsst.afw.math.BackgroundList) - sourceCat catalog of measured sources - astromMatches list of source/refObj matches from the astrometry solver """ # detect, deblend and measure sources if exposureIdInfo is None: exposureIdInfo = ExposureIdInfo() if background is None: background = BackgroundList() sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits) table = SourceTable.make(self.schema, sourceIdFactory) table.setMetadata(self.algMetadata) if self.config.doInsertFakes: self.insertFakes.run(exposure, background=background) detRes = self.detection.run(table=table, exposure=exposure, doSmooth=True) sourceCat = detRes.sources if detRes.fpSets.background: background.append(detRes.fpSets.background) if self.config.doDeblend: self.deblend.run(exposure=exposure, sources=sourceCat) self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId) if self.config.doApCorr: self.applyApCorr.run(catalog=sourceCat, apCorrMap=exposure.getInfo().getApCorrMap()) self.catalogCalculation.run(sourceCat) if icSourceCat is not None and \ len(self.config.icSourceFieldsToCopy) > 0: self.copyIcSourceFields(icSourceCat=icSourceCat, sourceCat=sourceCat) # perform astrometry calibration: # fit an improved WCS and update the exposure's WCS in place astromMatches = None if self.config.doAstrometry: try: astromRes = self.astrometry.run( exposure=exposure, sourceCat=sourceCat, ) astromMatches = astromRes.matches except Exception as e: if self.config.requireAstrometry: raise self.log.warn("Unable to perform astrometric calibration " "(%s): attempting to proceed" % e) # compute photometric calibration if self.config.doPhotoCal: try: photoRes = self.photoCal.run(exposure, sourceCat=sourceCat) exposure.getCalib().setFluxMag0(photoRes.calib.getFluxMag0()) self.log.info("Photometric zero-point: %f" % photoRes.calib.getMagnitude(1.0)) self.setMetadata(exposure=exposure, photoRes=photoRes) except Exception as e: if self.config.requirePhotoCal: raise self.log.warn("Unable to perform photometric calibration " "(%s): attempting to proceed" % e) self.setMetadata(exposure=exposure, photoRes=None) frame = getDebugFrame(self._display, "calibrate") if frame: displayAstrometry( sourceCat=sourceCat, exposure=exposure, matches=astromMatches, frame=frame, pause=False, ) return pipeBase.Struct( exposure=exposure, background=background, sourceCat=sourceCat, astromMatches=astromMatches, )
def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter, searchRadius, parity, filterName=None): """ @param[in] parity True for flipped parity, False for normal parity, None to leave parity unchanged """ solver = self.refObjLoader._getSolver() imageSize = bbox.getDimensions() x0, y0 = bbox.getMin() # select sources with valid x, y, flux xybb = afwGeom.Box2D() goodsources = afwTable.SourceCatalog(sourceCat.table) badkeys = [ goodsources.getSchema().find(name).key for name in self.config.badFlags ] for s in sourceCat: if np.isfinite(s.getX()) and np.isfinite(s.getY()) and np.isfinite(s.getPsfFlux()) \ and self._isGoodSource(s, badkeys): goodsources.append(s) xybb.include(afwGeom.Point2D(s.getX() - x0, s.getY() - y0)) self.log.info("Number of selected sources for astrometry : %d" % (len(goodsources))) if len(goodsources) < len(sourceCat): self.log.debug( 'Keeping %i of %i sources with finite X,Y positions and PSF flux', len(goodsources), len(sourceCat)) self.log.debug( 'Feeding sources in range x=[%.1f, %.1f], y=[%.1f, %.1f] ' + '(after subtracting x0,y0 = %.1f,%.1f) to Astrometry.net', xybb.getMinX(), xybb.getMaxX(), xybb.getMinY(), xybb.getMaxY(), x0, y0) # setStars sorts them by PSF flux. solver.setStars(goodsources, x0, y0) solver.setMaxStars(self.config.maxStars) solver.setImageSize(*imageSize) solver.setMatchThreshold(self.config.matchThreshold) raDecRadius = None if radecCenter is not None: raDecRadius = (radecCenter.getLongitude().asDegrees(), radecCenter.getLatitude().asDegrees(), searchRadius.asDegrees()) solver.setRaDecRadius(*raDecRadius) self.log.debug( 'Searching for match around RA,Dec = (%g, %g) with radius %g deg' % raDecRadius) if pixelScale is not None: dscale = self.config.pixelScaleUncertainty scale = pixelScale.asArcseconds() lo = scale / dscale hi = scale * dscale solver.setPixelScaleRange(lo, hi) self.log.debug( 'Searching for matches with pixel scale = %g +- %g %% -> range [%g, %g] arcsec/pix', scale, 100. * (dscale - 1.), lo, hi) if parity is not None: solver.setParity(parity) self.log.debug('Searching for match with parity = %s', str(parity)) # Find and load index files within RA,Dec range and scale range. if radecCenter is not None: multiInds = self.refObjLoader._getMIndexesWithinRange( radecCenter, searchRadius) else: multiInds = self.refObjLoader.multiInds qlo, qhi = solver.getQuadSizeRangeArcsec() toload_multiInds = set() toload_inds = [] for mi in multiInds: for i in range(len(mi)): ind = mi[i] if not ind.overlapsScaleRange(qlo, qhi): continue toload_multiInds.add(mi) toload_inds.append(ind) import lsstDebug if lsstDebug.Info(__name__).display: # Use separate context for display, since astrometry.net can segfault if we don't... with LoadMultiIndexes(toload_multiInds): displayAstrometry(refCat=self.refObjLoader.loadPixelBox( bbox, wcs, filterName).refCat, frame=lsstDebug.Info(__name__).frame, pause=lsstDebug.Info(__name__).pause) with LoadMultiIndexes(toload_multiInds): solver.addIndices(toload_inds) self.memusage('Index files loaded: ') cpulimit = self.config.maxCpuTime solver.run(cpulimit) self.memusage('Solving finished: ') self.memusage('Index files unloaded: ') if solver.didSolve(): self.log.debug('Solved!') wcs = solver.getWcs() if x0 != 0 or y0 != 0: wcs = wcs.copyAtShiftedPixelOrigin(afwGeom.Extent2D(x0, y0)) else: self.log.warn( 'Did not get an astrometric solution from Astrometry.net') wcs = None # Gather debugging info... # -are there any reference stars in the proposed search area? # log the number found and discard the results if radecCenter is not None: self.refObjLoader.loadSkyCircle(radecCenter, searchRadius, filterName) qa = solver.getSolveStats() self.log.debug('qa: %s', qa.toString()) return wcs, qa
def calibrate(self, exposure, exposureIdInfo, background=None, icSourceCat=None): """!Calibrate an exposure (science image or coadd) @param[in,out] exposure exposure to calibrate (an lsst.afw.image.ExposureF or similar); in: - MaskedImage - Psf out: - MaskedImage has background subtracted - Wcs is replaced - Calib zero-point is set @param[in,out] background background model already subtracted from exposure (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted, though that is unusual for calibration. A refined background model is output. @return pipe_base Struct containing these fields: - exposure calibrate science exposure with refined WCS and Calib - background model of background subtracted from exposure (an lsst.afw.math.BackgroundList) - sourceCat catalog of measured sources - astromMatches list of source/refObj matches from the astrometry solver """ # detect, deblend and measure sources procRes = self.detectAndMeasure.run( exposure=exposure, exposureIdInfo=exposureIdInfo, background=background, ) background = procRes.background sourceCat = procRes.sourceCat if icSourceCat is not None and len( self.config.icSourceFieldsToCopy) > 0: self.copyIcSourceFields(icSourceCat=icSourceCat, sourceCat=sourceCat) # perform astrometry calibration: # fit an improved WCS and update the exposure's WCS in place astromMatches = None if self.config.doAstrometry: try: astromRes = self.astrometry.run( exposure=exposure, sourceCat=sourceCat, ) astromMatches = astromRes.matches except Exception as e: if self.config.requireAstrometry: raise self.log.warn( "Unable to perform astrometric calibration (%s): attempting to proceed" % e) # compute photometric calibration if self.config.doPhotoCal: try: if astromMatches is None: astromRes = self.astrometry.loadAndMatch( exposure=exposure, sourceCat=sourceCat) photoRes = self.photoCal.run(exposure, astromMatches) exposure.getCalib().setFluxMag0(photoRes.calib.getFluxMag0()) self.log.info("Photometric zero-point: %f" % photoRes.calib.getMagnitude(1.0)) self.setMetadata(exposure=exposure, photoRes=photoRes) except Exception as e: if self.config.requirePhotoCal: raise self.log.warn( "Unable to perform photometric calibration (%s): attempting to proceed" % e) self.setMetadata(exposure=exposure, photoRes=None) frame = getDebugFrame(self._display, "calibrate") if frame: displayAstrometry( sourceCat=sourceCat, exposure=exposure, astromMatches=astromMatches, frame=frame, pause=False, ) return pipeBase.Struct( exposure=exposure, background=background, sourceCat=sourceCat, astromMatches=astromMatches, )