144 def __init__(self, config, *, display=None, **kwargs):
145 super().
__init__(config=config, **kwargs)
146 self.makeSubtask(
"installPsf")
154 self.
schema = afwTable.SourceTable.makeMinimalSchema()
158 self.
control.maxDistToPeak = -1
163 md = dafBase.PropertySet()
171 """Run a very basic but fast threshold-based object detection on an exposure
172 Return the footPrintSet for the objects in a postISR exposure.
176 exp : `lsst.afw.image.Exposure`
177 Image in which to detect objects.
179 nSigma above image's stddev at which to set the detection threshold.
181 Minimum number of pixels for detection.
183 Grow the detected footprint by this many pixels.
187 footPrintSet : `lsst.afw.detection.FootprintSet`
188 FootprintSet containing the detections.
190 threshold = afwDetect.Threshold(nSigma, afwDetect.Threshold.STDEV)
191 footPrintSet = afwDetect.FootprintSet(exp.getMaskedImage(), threshold,
"DETECTED", nPixMin)
194 footPrintSet = afwDetect.FootprintSet(footPrintSet, grow, isotropic)
199 """Perform a final check that centroid location is actually bright.
203 exp : `lsst.afw.image.Exposure`
204 The exposure on which to operate
205 centroid : `tuple` of `float`
206 Location of the centroid in pixel coordinates
208 Number of the source in the source catalog. Only used if the check
209 is failed, for debug purposes.
211 Image's percentile above which the pixel containing the centroid
212 must be in order to pass the check.
217 Raised if the centroid's pixel is not above the percentile threshold
219 threshold = np.percentile(exp.image.array, percentile)
220 pixelValue = exp.image[centroid]
221 if pixelValue < threshold:
222 msg = (f
"Final centroid pixel value check failed: srcNum {srcNum} at {centroid}"
223 f
" has central pixel = {pixelValue:3f} <"
224 f
" {percentile} percentile of image = {threshold:3f}")
225 raise ValueError(msg)
238 """Get the centre of mass around a point in the image.
242 exp : `lsst.afw.image.Exposure`
243 The exposure in question.
244 nominalCentroid : `tuple` of `float`
245 Nominal location of the centroid in pixel coordinates.
247 The size of the box around the nominalCentroid in which to measure
252 com : `tuple` of `float`
253 The locaiton of the centre of mass of the brightest source in pixel
259 bbox = bbox.dilatedBy(int(boxSize//2))
260 bbox = bbox.clippedTo(exp.getBBox())
261 data = exp[bbox].image.array
262 xy0 = exp[bbox].getXY0()
264 peak = ndImage.center_of_mass(data)
265 peak = (peak[1], peak[0])
268 return (com[0], com[1])
271 """Find the brightest source which passes the cuts among the sources.
275 objData : `dict` of `dict`
276 Dictionary, keyed by source number, containing the measurements.
281 The source number of the brightest source which passes the cuts.
283 max70, max70srcNum = -1, -1
284 max25, max25srcNum = -1, -1
286 for srcNum
in sorted(objData.keys()):
290 xx = objData[srcNum][
'xx']
291 yy = objData[srcNum][
'yy']
296 if self.config.doExtendednessCut:
297 if xx > self.config.maxExtendedness
or yy > self.config.maxExtendedness:
301 nonRoundness = max(nonRoundness, 1/nonRoundness)
302 if nonRoundness > self.config.maxNonRoundness:
305 if self.log.isEnabledFor(self.log.DEBUG):
306 text = f
"src {srcNum}: {objData[srcNum]['xCentroid']:.0f}, {objData[srcNum]['yCentroid']:.0f}"
307 text += f
" - xx={xx:.1f}, yy={yy:.1f}, nonRound={nonRoundness:.1f}"
308 text += f
" - ap70={objData[srcNum]['apFlux70']:,.0f}"
309 text += f
" - ap25={objData[srcNum]['apFlux25']:,.0f}"
310 text += f
" - skip={skip}"
316 ap70 = objData[srcNum][
'apFlux70']
317 ap25 = objData[srcNum][
'apFlux25']
324 if max70srcNum != max25srcNum:
325 self.log.warning(
"WARNING! Max apFlux70 for different object than with max apFlux25")
383 """Get the shape, centroid and flux from a footprint.
387 fp : `lsst.afw.detection.Footprint`
388 The footprint to measure.
389 exp : `lsst.afw.image.Exposure`
390 The footprint's parent exposure.
394 srcData : `lsst.pipe.base.Struct`
395 The struct containing the extracted measurements.
397 xx = fp.getShape().getIxx()
398 yy = fp.getShape().getIyy()
399 xCentroid, yCentroid = fp.getCentroid()
400 apFlux70 = np.sum(exp[fp.getBBox()].image.array)
401 apFlux25 = np.sum(exp[fp.getBBox()].image.array)
402 return pipeBase.Struct(xx=xx,
434 """Make the default/template return struct, with defaults to False/nan.
438 objData : `lsst.pipe.base.Struct`
439 The default template return structure.
441 result = pipeBase.Struct()
442 result.success =
False
443 result.brightestObjCentroid = (np.nan, np.nan)
444 result.brightestObjCentroidCofM =
None
445 result.brightestObj_xXyY = (np.nan, np.nan)
446 result.brightestObjApFlux70 = np.nan
447 result.brightestObjApFlux25 = np.nan
448 result.medianXxYy = (np.nan, np.nan)
451 def run(self, exp, *, donutDiameter=None, doDisplay=False):
452 """Calculate position, flux and shape of the brightest star in an image.
454 Given an an assembled (and at least minimally ISRed exposure),
455 quickly and robustly calculate the centroid of the
456 brightest star in the image.
460 exp : `lsst.afw.image.Exposure`
461 The exposure in which to find and measure the brightest star.
462 donutDiameter : `int` or `float`, optional
463 The expected diameter of donuts in pixels for use in the centre of
464 mass centroid measurement. If None is provided, the config option
467 Display the image and found sources. A diplay object must have
468 been passed to the task constructor.
472 result : `lsst.pipe.base.Struct`
474 Whether the task ran successfully and found the object (bool)
475 The object's centroid (float, float)
476 The object's ixx, iyy (float, float)
477 The object's 70 pixel aperture flux (float)
478 The object's 25 pixel aperture flux (float)
479 The images's median ixx, iyy (float, float)
480 If unsuccessful, the success field is False and all other results
481 are np.nan of the expected shape.
485 Because of this task's involvement in observing scripts, the run method
486 should *never* raise. Failure modes are noted by returning a Struct with
487 the same structure as the success case, with all value set to np.nan and
488 result.success=False.
491 result = self.
_run(exp=exp, donutDiameter=donutDiameter, doDisplay=doDisplay)
493 except Exception
as e:
494 self.log.warning(
"Failed to find main source centroid %s", e)
498 def _run(self, exp, *, donutDiameter=None, doDisplay=False):
499 """The actual run method, called by run()
501 Behaviour is documented in detail in the main run().
503 if donutDiameter
is None:
504 donutDiameter = self.config.donutDiameter
506 self.
plateScale = exp.getWcs().getPixelScale().asArcseconds()
507 median = np.nanmedian(exp.image.array)
509 self.installPsf.run(exp)
511 nPixMin=self.config.nPixMinDetection)
515 raise RuntimeError(
"Display failed as no display provided during init()")
518 fpSet = sources.getFootprints()
519 self.log.info(
"Found %d sources in exposure", len(fpSet))
524 for srcNum, fp
in enumerate(fpSet):
528 except MeasurementError:
532 except MeasurementError
as e:
533 self.log.info(
"Skipped measuring source %s: %s", srcNum, e)
538 self.log.info(
"Measured %d of %d sources in exposure", nMeasured, len(fpSet))
543 if brightestObjSrcNum
is None:
544 raise RuntimeError(
"No sources in image passed cuts")
546 x = objData[brightestObjSrcNum][
'xCentroid']
547 y = objData[brightestObjSrcNum][
'yCentroid']
548 brightestObjCentroid = (x, y)
549 xx = objData[brightestObjSrcNum][
'xx']
550 yy = objData[brightestObjSrcNum][
'yy']
551 brightestObjApFlux70 = objData[brightestObjSrcNum][
'apFlux70']
552 brightestObjApFlux25 = objData[brightestObjSrcNum][
'apFlux25']
555 if self.config.doCheckCentroidPixelValue:
556 self.
checkResult(exp, brightestObjCentroid, brightestObjSrcNum,
557 self.config.centroidPixelPercentile)
559 boxSize = donutDiameter * 1.3
563 result.success =
True
564 result.brightestObjCentroid = brightestObjCentroid
565 result.brightestObj_xXyY = (xx, yy)
566 result.brightestObjApFlux70 = brightestObjApFlux70
567 result.brightestObjApFlux25 = brightestObjApFlux25
568 result.medianXxYy = medianXxYy
569 result.brightestObjCentroidCofM = centreOfMass