23 import scipy.ndimage 
as ndImage
 
   37     """Config class for the QuickFrameMeasurementTask. 
   39     installPsf = pexConfig.ConfigurableField(
 
   40         target=InstallGaussianPsfTask,
 
   41         doc=
"Task for installing an initial PSF",
 
   43     maxNonRoundness = pexConfig.Field(
 
   45         doc=
"Ratio of xx to yy (or vice versa) above which to cut, in order to exclude spectra",
 
   48     maxExtendedness = pexConfig.Field(
 
   50         doc=
"Max absolute value of xx and yy above which to cut, in order to exclude large/things",
 
   53     doExtendednessCut = pexConfig.Field(
 
   55         doc=
"Apply the extendeness cut, as definted by maxExtendedness",
 
   58     centroidPixelPercentile = pexConfig.Field(
 
   60         doc=
"The image's percentile value which the centroid must be greater than to pass the final peak" 
   61             " check. Ignored if doCheckCentroidPixelValue is False",
 
   64     doCheckCentroidPixelValue = pexConfig.Field(
 
   66         doc=
"Check that the centroid found is actually in the centroidPixelPercentile percentile of the" 
   67             " image? Set to False for donut images.",
 
   70     initialPsfWidth = pexConfig.Field(
 
   72         doc=
"Guess at the initial PSF FWHM in pixels.",
 
   75     nSigmaDetection = pexConfig.Field(
 
   77         doc=
"Number of sigma for the detection limit.",
 
   80     nPixMinDetection = pexConfig.Field(
 
   82         doc=
"Minimum number of pixels in a detected source.",
 
   85     donutDiameter = pexConfig.Field(
 
   87         doc=
"The expected diameter of donuts in a donut image, in pixels.",
 
   97     """WARNING: An experimental new task with changable API! Do not rely on yet! 
   99     This task finds the centroid of the brightest source in a given CCD-image 
  100     and returns its centroid and a rough estimate of the seeing/PSF. 
  102     It is designed for speed, such that it can be used in observing scripts 
  103     to provide pointing offsets, allowing subsequent pointings to place 
  104     a source at an exact pixel position. 
  106     The approach taken here is deliberately sub-optimal in the detection and 
  107     measurement sense, with all optimisation being done for speed and robustness 
  110     A small set of unit tests exist for this task, which run automatically 
  111     if afwdata is setup. These, however, are stricky unit tests, and will not 
  112     catch algorithmic regressions. TODO: DM-29038 exists to merge a regression 
  113     real test which runs against 1,000 LATISS images, but is therefore slow 
  114     and requires access to the data. 
  118     config : `lsst.pipe.tasks.quickFrameMeasurement.QuickFrameMeasurementTaskConfig` 
  119         Configuration class for the QuickFrameMeasurementTask. 
  120     display : `lsst.afw.display.Display`, optional 
  121         The display to use for showing the images, detections and centroids. 
  125     result : `lsst.pipe.base.Struct` 
  126         Return strucure containing whether the task was successful, the main 
  127         source's centroid, its the aperture fluxes, the ixx and iyy of the 
  128         source, and the median ixx, iyy of the detections in the exposure. 
  129         See run() method for further details. 
  133     This task should *never* raise, as the run() method is enclosed in an 
  134     except Exception block, so that it will never fail during observing. 
  135     Failure modes should be limited to returning a return Struct() with the same 
  136     structure as the success case, with all value set to np.nan but with 
  137     result.success=False. 
  139     ConfigClass = QuickFrameMeasurementTaskConfig
 
  140     _DefaultName = 
'quickFrameMeasurementTask' 
  142     def __init__(self, config, *, display=None, **kwargs):
 
  143         super().
__init__(config=config, **kwargs)
 
  144         self.makeSubtask(
"installPsf")
 
  152         self.
schemaschema = afwTable.SourceTable.makeMinimalSchema()
 
  155         self.
controlcontrol = measBase.SdssCentroidControl()
 
  168         """Run a very basic but fast threshold-based object detection on an exposure 
  169         Return the footPrintSet for the objects in a postISR exposure. 
  173         exp : `lsst.afw.image.Exposure` 
  174             Image in which to detect objects. 
  176             nSigma above image's stddev at which to set the detection threshold. 
  178             Minimum number of pixels for detection. 
  180             Grow the detected footprint by this many pixels. 
  184         footPrintSet : `lsst.afw.detection.FootprintSet` 
  185             FootprintSet containing the detections. 
  196         """Perform a final check that centroid location is actually bright. 
  200         exp : `lsst.afw.image.Exposure` 
  201             The exposure on which to operate 
  202         centroid : `tuple` of `float` 
  203             Location of the centroid in pixel coordinates 
  205             Number of the source in the source catalog. Only used if the check 
  206             is failed, for debug purposes. 
  208             Image's percentile above which the pixel containing the centroid 
  209             must be in order to pass the check. 
  214             Raised if the centroid's pixel is not above the percentile threshold 
  216         threshold = np.percentile(exp.image.array, percentile)
 
  217         pixelValue = exp.image[centroid]
 
  218         if pixelValue < threshold:
 
  219             msg = (f
"Final centroid pixel value check failed: srcNum {srcNum} at {centroid}" 
  220                    f
" has central pixel = {pixelValue:3f} <" 
  221                    f
" {percentile} percentile of image = {threshold:3f}")
 
  222             raise ValueError(msg)
 
  226     def _calcMedianXxYy(objData):
 
  227         """Return the median ixx and iyy for object in the image. 
  229         medianXx = np.nanmedian([element[
'xx'] 
for element 
in objData.values()])
 
  230         medianYy = np.nanmedian([element[
'yy'] 
for element 
in objData.values()])
 
  231         return medianXx, medianYy
 
  234     def _getCenterOfMass(exp, nominalCentroid, boxSize):
 
  235         """Get the centre of mass around a point in the image. 
  239         exp : `lsst.afw.image.Exposure` 
  240             The exposure in question. 
  241         nominalCentroid : `tuple` of `float` 
  242             Nominal location of the centroid in pixel coordinates. 
  244             The size of the box around the nominalCentroid in which to measure 
  249         com : `tuple` of `float` 
  250             The locaiton of the centre of mass of the brightest source in pixel 
  256         bbox = bbox.dilatedBy(int(boxSize//2))
 
  257         bbox = bbox.clippedTo(exp.getBBox())
 
  258         data = exp[bbox].image.array
 
  259         xy0 = exp[bbox].getXY0()
 
  261         peak = ndImage.center_of_mass(data)
 
  262         peak = (peak[1], peak[0])  
 
  265         return (com[0], com[1])
 
  267     def _calcBrightestObjSrcNum(self, objData):
 
  268         """Find the brightest source which passes the cuts among the sources. 
  272         objData : `dict` of `dict` 
  273             Dictionary, keyed by source number, containing the measurements. 
  278             The source number of the brightest source which passes the cuts. 
  280         max70, max70srcNum = -1, -1
 
  281         max25, max25srcNum = -1, -1
 
  283         for srcNum 
in sorted(objData.keys()):  
 
  287             xx = objData[srcNum][
'xx']
 
  288             yy = objData[srcNum][
'yy']
 
  293             if self.config.doExtendednessCut:
 
  294                 if xx > self.config.maxExtendedness 
or yy > self.config.maxExtendedness:
 
  298             nonRoundness = 
max(nonRoundness, 1/nonRoundness)
 
  299             if nonRoundness > self.config.maxNonRoundness:
 
  303                 text = f
"src {srcNum}: {objData[srcNum]['xCentroid']:.0f}, {objData[srcNum]['yCentroid']:.0f}" 
  304                 text += f
" - xx={xx:.1f}, yy={yy:.1f}, nonRound={nonRoundness:.1f}" 
  305                 text += f
" - ap70={objData[srcNum]['apFlux70']:,.0f}" 
  306                 text += f
" - ap25={objData[srcNum]['apFlux25']:,.0f}" 
  307                 text += f
" - skip={skip}" 
  313             ap70 = objData[srcNum][
'apFlux70']
 
  314             ap25 = objData[srcNum][
'apFlux25']
 
  321         if max70srcNum != max25srcNum:
 
  322             self.log.
warning(
"WARNING! Max apFlux70 for different object than with max apFlux25")
 
  328     def _measureFp(self, fp, exp):
 
  329         """Run the measurements on a footprint. 
  333         fp : `lsst.afw.detection.Footprint` 
  334             The footprint to measure. 
  335         exp : `lsst.afw.image.Exposure` 
  336             The footprint's parent exposure. 
  340         src : `lsst.afw.table.SourceRecord` 
  341             The source record containing the measurements. 
  343         src = self.
tabletable.makeRecord()
 
  350     def _getDataFromSrcRecord(self, src):
 
  351         """Extract the shapes and centroids from a source record. 
  355         src : `lsst.afw.table.SourceRecord` 
  356             The source record from which to extract the measurements. 
  360         srcData : `lsst.pipe.base.Struct` 
  361             The struct containing the extracted measurements. 
  364         xx = np.sqrt(src[
'base_SdssShape_xx'])*2.355*pScale  
 
  365         yy = np.sqrt(src[
'base_SdssShape_yy'])*2.355*pScale
 
  366         xCentroid = src[
'base_SdssCentroid_x']
 
  367         yCentroid = src[
'base_SdssCentroid_y']
 
  369         apFlux70 = src[
'aperFlux_70_0_instFlux']
 
  370         apFlux25 = src[
'aperFlux_25_0_instFlux']
 
  371         return pipeBase.Struct(xx=xx,
 
  379     def _getDataFromFootprintOnly(fp, exp):
 
  380         """Get the shape, centroid and flux from a footprint. 
  384         fp : `lsst.afw.detection.Footprint` 
  385             The footprint to measure. 
  386         exp : `lsst.afw.image.Exposure` 
  387             The footprint's parent exposure. 
  391         srcData : `lsst.pipe.base.Struct` 
  392             The struct containing the extracted measurements. 
  394         xx = fp.getShape().getIxx()
 
  395         yy = fp.getShape().getIyy()
 
  396         xCentroid, yCentroid = fp.getCentroid()
 
  397         apFlux70 = np.sum(exp[fp.getBBox()].image.array)
 
  398         apFlux25 = np.sum(exp[fp.getBBox()].image.array)
 
  399         return pipeBase.Struct(xx=xx,
 
  407     def _measurementResultToDict(measurementResult):
 
  408         """Convenience function to repackage measurement results to a dict. 
  412         measurementResult : `lsst.afw.table.SourceRecord` 
  413             The source record to convert to a dict. 
  418             The dict containing the extracted data. 
  421         objData[
'xx'] = measurementResult.xx
 
  422         objData[
'yy'] = measurementResult.yy
 
  423         objData[
'xCentroid'] = measurementResult.xCentroid
 
  424         objData[
'yCentroid'] = measurementResult.yCentroid
 
  425         objData[
'apFlux70'] = measurementResult.apFlux70
 
  426         objData[
'apFlux25'] = measurementResult.apFlux25
 
  430     def _makeEmptyReturnStruct():
 
  431         """Make the default/template return struct, with defaults to False/nan. 
  435         objData : `lsst.pipe.base.Struct` 
  436             The default template return structure. 
  438         result = pipeBase.Struct()
 
  439         result.success = 
False 
  440         result.brightestObjCentroid = (np.nan, np.nan)
 
  441         result.brightestObjCentroidCofM = 
None 
  442         result.brightestObj_xXyY = (np.nan, np.nan)
 
  443         result.brightestObjApFlux70 = np.nan
 
  444         result.brightestObjApFlux25 = np.nan
 
  445         result.medianXxYy = (np.nan, np.nan)
 
  448     def run(self, exp, *, donutDiameter=None, doDisplay=False):
 
  449         """Calculate position, flux and shape of the brightest star in an image. 
  451         Given an an assembled (and at least minimally ISRed exposure), 
  452         quickly and robustly calculate the centroid of the 
  453         brightest star in the image. 
  457         exp : `lsst.afw.image.Exposure` 
  458             The exposure in which to find and measure the brightest star. 
  459         donutDiameter : `int` or `float`, optional 
  460             The expected diameter of donuts in pixels for use in the centre of 
  461             mass centroid measurement. If None is provided, the config option 
  464             Display the image and found sources. A diplay object must have 
  465             been passed to the task constructor. 
  469         result : `lsst.pipe.base.Struct` 
  471                 Whether the task ran successfully and found the object (bool) 
  472                 The object's centroid (float, float) 
  473                 The object's ixx, iyy (float, float) 
  474                 The object's 70 pixel aperture flux (float) 
  475                 The object's 25 pixel aperture flux (float) 
  476                 The images's median ixx, iyy (float, float) 
  477             If unsuccessful, the success field is False and all other results 
  478             are np.nan of the expected shape. 
  482         Because of this task's involvement in observing scripts, the run method 
  483         should *never* raise. Failure modes are noted by returning a Struct with 
  484         the same structure as the success case, with all value set to np.nan and 
  485         result.success=False. 
  488             result = self.
_run_run(exp=exp, donutDiameter=donutDiameter, doDisplay=doDisplay)
 
  490         except Exception 
as e:
 
  491             self.log.
warning(
"Failed to find main source centroid %s", e)
 
  495     def _run(self, exp, *, donutDiameter=None, doDisplay=False):
 
  496         """The actual run method, called by run() 
  498         Behaviour is documented in detail in the main run(). 
  500         if donutDiameter 
is None:
 
  501             donutDiameter = self.config.donutDiameter
 
  503         self.
plateScaleplateScale = exp.getWcs().getPixelScale().asArcseconds()
 
  504         median = np.nanmedian(exp.image.array)
 
  506         self.installPsf.
run(exp)
 
  507         sources = self.
detectObjectsInExpdetectObjectsInExp(exp, nSigma=self.config.nSigmaDetection,
 
  508                                           nPixMin=self.config.nPixMinDetection)
 
  511             if self.
displaydisplay 
is None:
 
  512                 raise RuntimeError(
"Display failed as no display provided during init()")
 
  515         fpSet = sources.getFootprints()
 
  516         self.log.
info(
"Found %d sources in exposure", len(fpSet))
 
  521         for srcNum, fp 
in enumerate(fpSet):
 
  525             except MeasurementError:
 
  529                 except MeasurementError 
as e:
 
  530                     self.log.
info(
"Skipped measuring source %s: %s", srcNum, e)
 
  535         self.log.
info(
"Measured %d of %d sources in exposure", nMeasured, len(fpSet))
 
  540         if brightestObjSrcNum 
is None:
 
  541             raise RuntimeError(
"No sources in image passed cuts")
 
  543         x = objData[brightestObjSrcNum][
'xCentroid']
 
  544         y = objData[brightestObjSrcNum][
'yCentroid']
 
  545         brightestObjCentroid = (x, y)
 
  546         xx = objData[brightestObjSrcNum][
'xx']
 
  547         yy = objData[brightestObjSrcNum][
'yy']
 
  548         brightestObjApFlux70 = objData[brightestObjSrcNum][
'apFlux70']
 
  549         brightestObjApFlux25 = objData[brightestObjSrcNum][
'apFlux25']
 
  552         if self.config.doCheckCentroidPixelValue:
 
  553             self.
checkResultcheckResult(exp, brightestObjCentroid, brightestObjSrcNum,
 
  554                              self.config.centroidPixelPercentile)
 
  556         boxSize = donutDiameter * 1.3  
 
  557         centreOfMass = self.
_getCenterOfMass_getCenterOfMass(exp, brightestObjCentroid, boxSize)
 
  560         result.success = 
True 
  561         result.brightestObjCentroid = brightestObjCentroid
 
  562         result.brightestObj_xXyY = (xx, yy)
 
  563         result.brightestObjApFlux70 = brightestObjApFlux70
 
  564         result.brightestObjApFlux25 = brightestObjApFlux25
 
  565         result.medianXxYy = medianXxYy
 
  566         result.brightestObjCentroidCofM = centreOfMass
 
A Threshold is used to pass a threshold value to detection algorithms.
Class for storing generic metadata.
An integer coordinate rectangle.
def _getDataFromSrcRecord(self, src)
def _calcBrightestObjSrcNum(self, objData)
def _measureFp(self, fp, exp)
def _getDataFromFootprintOnly(fp, exp)
def _calcMedianXxYy(objData)
def run(self, exp, *donutDiameter=None, doDisplay=False)
def _getCenterOfMass(exp, nominalCentroid, boxSize)
def checkResult(exp, centroid, srcNum, percentile)
def detectObjectsInExp(exp, nSigma, nPixMin, grow=0)
def _measurementResultToDict(measurementResult)
def _makeEmptyReturnStruct()
def _run(self, exp, *donutDiameter=None, doDisplay=False)
def __init__(self, config, *display=None, **kwargs)
daf::base::PropertySet * set
def mtv(data, frame=None, title="", wcs=None, *args, **kwargs)
def measure(mi, x, y, size, statistic, stats)
def isEnabledFor(loggername, level)