144 def __init__(self, config, *, display=None, **kwargs):
145 super().
__init__(config=config, **kwargs)
146 self.makeSubtask(
"installPsf")
154 self.
schema = afwTable.SourceTable.makeMinimalSchema()
162 md = dafBase.PropertySet()
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.
175 exp : `lsst.afw.image.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.
186 footPrintSet : `lsst.afw.detection.FootprintSet`
187 FootprintSet containing the detections.
189 threshold = afwDetect.Threshold(nSigma, afwDetect.Threshold.STDEV)
190 footPrintSet = afwDetect.FootprintSet(exp.getMaskedImage(), threshold,
"DETECTED", nPixMin)
193 footPrintSet = afwDetect.FootprintSet(footPrintSet, grow, isotropic)
198 """Perform a final check that centroid location is actually bright.
202 exp : `lsst.afw.image.Exposure`
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)
237 """Get the centre of mass around a point in the image.
241 exp : `lsst.afw.image.Exposure`
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])
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")
382 """Get the shape, centroid and flux from a footprint.
386 fp : `lsst.afw.detection.Footprint`
387 The footprint to measure.
388 exp : `lsst.afw.image.Exposure`
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,
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.
459 exp : `lsst.afw.image.Exposure`
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