22__all__ = [
"QuickFrameMeasurementTaskConfig",
"QuickFrameMeasurementTask"]
25import scipy.ndimage
as ndImage
39 """Config class for the QuickFrameMeasurementTask.
41 installPsf = pexConfig.ConfigurableField(
42 target=InstallGaussianPsfTask,
43 doc="Task for installing an initial PSF",
45 maxNonRoundness = pexConfig.Field(
47 doc=
"Ratio of xx to yy (or vice versa) above which to cut, in order to exclude spectra",
50 maxExtendedness = pexConfig.Field(
52 doc=
"Max absolute value of xx and yy above which to cut, in order to exclude large/things",
55 doExtendednessCut = pexConfig.Field(
57 doc=
"Apply the extendeness cut, as definted by maxExtendedness",
60 centroidPixelPercentile = pexConfig.Field(
62 doc=
"The image's percentile value which the centroid must be greater than to pass the final peak"
63 " check. Ignored if doCheckCentroidPixelValue is False",
66 doCheckCentroidPixelValue = pexConfig.Field(
68 doc=
"Check that the centroid found is actually in the centroidPixelPercentile percentile of the"
69 " image? Set to False for donut images.",
72 initialPsfWidth = pexConfig.Field(
74 doc=
"Guess at the initial PSF FWHM in pixels.",
77 nSigmaDetection = pexConfig.Field(
79 doc=
"Number of sigma for the detection limit.",
82 nPixMinDetection = pexConfig.Field(
84 doc=
"Minimum number of pixels in a detected source.",
87 donutDiameter = pexConfig.Field(
89 doc=
"The expected diameter of donuts in a donut image, in pixels.",
99 """WARNING: An experimental new task with changable API! Do not rely on yet!
101 This task finds the centroid of the brightest source in a given CCD-image
102 and returns its centroid
and a rough estimate of the seeing/PSF.
104 It
is designed
for speed, such that it can be used
in observing scripts
105 to provide pointing offsets, allowing subsequent pointings to place
106 a source at an exact pixel position.
108 The approach taken here
is deliberately sub-optimal
in the detection
and
109 measurement sense,
with all optimisation being done
for speed
and robustness
112 A small set of unit tests exist
for this task, which run automatically
113 if afwdata
is setup. These, however, are stricky unit tests,
and will
not
114 catch algorithmic regressions. TODO: DM-29038 exists to merge a regression
115 real test which runs against 1,000 LATISS images, but
is therefore slow
116 and requires access to the data.
123 The display to use
for showing the images, detections
and centroids.
127 result : `lsst.pipe.base.Struct`
128 Return strucure containing whether the task was successful, the main
129 source
's centroid, its the aperture fluxes, the ixx and iyy of the
130 source, and the median ixx, iyy of the detections
in the exposure.
131 See run() method
for further details.
135 This task should *never*
raise,
as the run() method
is enclosed
in an
136 except Exception block, so that it will never fail during observing.
137 Failure modes should be limited to returning a
return Struct()
with the same
138 structure
as the success case,
with all value set to np.nan but
with
139 result.success=
False.
141 ConfigClass = QuickFrameMeasurementTaskConfig
142 _DefaultName = 'quickFrameMeasurementTask'
144 def __init__(self, config, *, display=None, **kwargs):
145 super().
__init__(config=config, **kwargs)
146 self.makeSubtask(
"installPsf")
154 self.
schema = afwTable.SourceTable.makeMinimalSchema()
170 """Run a very basic but fast threshold-based object detection on an exposure
171 Return the footPrintSet for the objects
in a postISR exposure.
176 Image
in which to detect objects.
178 nSigma above image
's stddev at which to set the detection threshold.
180 Minimum number of pixels for detection.
182 Grow the detected footprint by this many pixels.
187 FootprintSet containing the detections.
198 """Perform a final check that centroid location is actually bright.
203 The exposure on which to operate
204 centroid : `tuple` of `float`
205 Location of the centroid in pixel coordinates
207 Number of the source
in the source catalog. Only used
if the check
208 is failed,
for debug purposes.
210 Image
's percentile above which the pixel containing the centroid
211 must be in order to
pass the check.
216 Raised
if the centroid
's pixel is not above the percentile threshold
218 threshold = np.percentile(exp.image.array, percentile)
219 pixelValue = exp.image[centroid]
220 if pixelValue < threshold:
221 msg = (f
"Final centroid pixel value check failed: srcNum {srcNum} at {centroid}"
222 f
" has central pixel = {pixelValue:3f} <"
223 f
" {percentile} percentile of image = {threshold:3f}")
224 raise ValueError(msg)
228 def _calcMedianXxYy(objData):
229 """Return the median ixx and iyy for object in the image.
231 medianXx = np.nanmedian([element['xx']
for element
in objData.values()])
232 medianYy = np.nanmedian([element[
'yy']
for element
in objData.values()])
233 return medianXx, medianYy
236 def _getCenterOfMass(exp, nominalCentroid, boxSize):
237 """Get the centre of mass around a point in the image.
242 The exposure in question.
243 nominalCentroid : `tuple` of `float`
244 Nominal location of the centroid
in pixel coordinates.
246 The size of the box around the nominalCentroid
in which to measure
251 com : `tuple` of `float`
252 The locaiton of the centre of mass of the brightest source
in pixel
258 bbox = bbox.dilatedBy(int(boxSize//2))
259 bbox = bbox.clippedTo(exp.getBBox())
260 data = exp[bbox].image.array
261 xy0 = exp[bbox].getXY0()
263 peak = ndImage.center_of_mass(data)
264 peak = (peak[1], peak[0])
267 return (com[0], com[1])
269 def _calcBrightestObjSrcNum(self, objData):
270 """Find the brightest source which passes the cuts among the sources.
274 objData : `dict` of `dict`
275 Dictionary, keyed by source number, containing the measurements.
280 The source number of the brightest source which passes the cuts.
282 max70, max70srcNum = -1, -1
283 max25, max25srcNum = -1, -1
285 for srcNum
in sorted(objData.keys()):
289 xx = objData[srcNum][
'xx']
290 yy = objData[srcNum][
'yy']
295 if self.config.doExtendednessCut:
296 if xx > self.config.maxExtendedness
or yy > self.config.maxExtendedness:
300 nonRoundness =
max(nonRoundness, 1/nonRoundness)
301 if nonRoundness > self.config.maxNonRoundness:
304 if self.log.isEnabledFor(self.log.DEBUG):
305 text = f
"src {srcNum}: {objData[srcNum]['xCentroid']:.0f}, {objData[srcNum]['yCentroid']:.0f}"
306 text += f
" - xx={xx:.1f}, yy={yy:.1f}, nonRound={nonRoundness:.1f}"
307 text += f
" - ap70={objData[srcNum]['apFlux70']:,.0f}"
308 text += f
" - ap25={objData[srcNum]['apFlux25']:,.0f}"
309 text += f
" - skip={skip}"
315 ap70 = objData[srcNum][
'apFlux70']
316 ap25 = objData[srcNum][
'apFlux25']
323 if max70srcNum != max25srcNum:
324 self.log.warning(
"WARNING! Max apFlux70 for different object than with max apFlux25")
330 def _measureFp(self, fp, exp):
331 """Run the measurements on a footprint.
336 The footprint to measure.
338 The footprint's parent exposure.
343 The source record containing the measurements.
345 src = self.table.makeRecord()
348 self.shaper.measure(src, exp)
352 def _getDataFromSrcRecord(self, src):
353 """Extract the shapes and centroids from a source record.
358 The source record from which to extract the measurements.
362 srcData : `lsst.pipe.base.Struct`
363 The struct containing the extracted measurements.
366 xx = np.sqrt(src['base_SdssShape_xx'])*2.355*pScale
367 yy = np.sqrt(src[
'base_SdssShape_yy'])*2.355*pScale
368 xCentroid = src[
'base_SdssCentroid_x']
369 yCentroid = src[
'base_SdssCentroid_y']
371 apFlux70 = src[
'aperFlux_70_0_instFlux']
372 apFlux25 = src[
'aperFlux_25_0_instFlux']
373 return pipeBase.Struct(xx=xx,
381 def _getDataFromFootprintOnly(fp, exp):
382 """Get the shape, centroid and flux from a footprint.
387 The footprint to measure.
389 The footprint's parent exposure.
393 srcData : `lsst.pipe.base.Struct`
394 The struct containing the extracted measurements.
396 xx = fp.getShape().getIxx()
397 yy = fp.getShape().getIyy()
398 xCentroid, yCentroid = fp.getCentroid()
399 apFlux70 = np.sum(exp[fp.getBBox()].image.array)
400 apFlux25 = np.sum(exp[fp.getBBox()].image.array)
401 return pipeBase.Struct(xx=xx,
409 def _measurementResultToDict(measurementResult):
410 """Convenience function to repackage measurement results to a dict.
415 The source record to convert to a dict.
420 The dict containing the extracted data.
423 objData['xx'] = measurementResult.xx
424 objData[
'yy'] = measurementResult.yy
425 objData[
'xCentroid'] = measurementResult.xCentroid
426 objData[
'yCentroid'] = measurementResult.yCentroid
427 objData[
'apFlux70'] = measurementResult.apFlux70
428 objData[
'apFlux25'] = measurementResult.apFlux25
432 def _makeEmptyReturnStruct():
433 """Make the default/template return struct, with defaults to False/nan.
437 objData : `lsst.pipe.base.Struct`
438 The default template return structure.
440 result = pipeBase.Struct()
441 result.success = False
442 result.brightestObjCentroid = (np.nan, np.nan)
443 result.brightestObjCentroidCofM =
None
444 result.brightestObj_xXyY = (np.nan, np.nan)
445 result.brightestObjApFlux70 = np.nan
446 result.brightestObjApFlux25 = np.nan
447 result.medianXxYy = (np.nan, np.nan)
450 def run(self, exp, *, donutDiameter=None, doDisplay=False):
451 """Calculate position, flux and shape of the brightest star in an image.
453 Given an an assembled (and at least minimally ISRed exposure),
454 quickly
and robustly calculate the centroid of the
455 brightest star
in the image.
460 The exposure
in which to find
and measure the brightest star.
461 donutDiameter : `int`
or `float`, optional
462 The expected diameter of donuts
in pixels
for use
in the centre of
463 mass centroid measurement. If
None is provided, the config option
466 Display the image
and found sources. A diplay object must have
467 been passed to the task constructor.
471 result : `lsst.pipe.base.Struct`
473 Whether the task ran successfully
and found the object (bool)
474 The object
's centroid (float, float)
475 The object's ixx, iyy (float, float)
476 The object's 70 pixel aperture flux (float)
477 The object's 25 pixel aperture flux (float)
478 The images's median ixx, iyy (float, float)
479 If unsuccessful, the success field is False and all other results
480 are np.nan of the expected shape.
484 Because of this task
's involvement in observing scripts, the run method
485 should *never* raise. Failure modes are noted by returning a Struct
with
486 the same structure
as the success case,
with all value set to np.nan
and
487 result.success=
False.
490 result = self.
_run(exp=exp, donutDiameter=donutDiameter, doDisplay=doDisplay)
492 except Exception
as e:
493 self.log.warning(
"Failed to find main source centroid %s", e)
497 def _run(self, exp, *, donutDiameter=None, doDisplay=False):
498 """The actual run method, called by run()
500 Behaviour is documented
in detail
in the main run().
502 if donutDiameter
is None:
503 donutDiameter = self.config.donutDiameter
505 self.
plateScale = exp.getWcs().getPixelScale().asArcseconds()
506 median = np.nanmedian(exp.image.array)
508 self.installPsf.run(exp)
510 nPixMin=self.config.nPixMinDetection)
514 raise RuntimeError(
"Display failed as no display provided during init()")
517 fpSet = sources.getFootprints()
518 self.log.info(
"Found %d sources in exposure", len(fpSet))
523 for srcNum, fp
in enumerate(fpSet):
527 except MeasurementError:
531 except MeasurementError
as e:
532 self.log.info(
"Skipped measuring source %s: %s", srcNum, e)
537 self.log.info(
"Measured %d of %d sources in exposure", nMeasured, len(fpSet))
542 if brightestObjSrcNum
is None:
543 raise RuntimeError(
"No sources in image passed cuts")
545 x = objData[brightestObjSrcNum][
'xCentroid']
546 y = objData[brightestObjSrcNum][
'yCentroid']
547 brightestObjCentroid = (x, y)
548 xx = objData[brightestObjSrcNum][
'xx']
549 yy = objData[brightestObjSrcNum][
'yy']
550 brightestObjApFlux70 = objData[brightestObjSrcNum][
'apFlux70']
551 brightestObjApFlux25 = objData[brightestObjSrcNum][
'apFlux25']
554 if self.config.doCheckCentroidPixelValue:
555 self.
checkResult(exp, brightestObjCentroid, brightestObjSrcNum,
556 self.config.centroidPixelPercentile)
558 boxSize = donutDiameter * 1.3
562 result.success =
True
563 result.brightestObjCentroid = brightestObjCentroid
564 result.brightestObj_xXyY = (xx, yy)
565 result.brightestObjApFlux70 = brightestObjApFlux70
566 result.brightestObjApFlux25 = brightestObjApFlux25
567 result.medianXxYy = medianXxYy
568 result.brightestObjCentroidCofM = centreOfMass
A Threshold is used to pass a threshold value to detection algorithms.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Record class that contains measurements made on a single exposure.
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 _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