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)