23 """Support utilities for Measuring sources"""
24 from __future__
import print_function
25 from builtins
import next
26 from builtins
import zip
27 from builtins
import str
28 from builtins
import range
34 import lsst.pex.exceptions
as pexExcept
35 import lsst.daf.base
as dafBase
39 import lsst.afw.math
as afwMath
40 import lsst.afw.table
as afwTable
41 import lsst.afw.display.ds9
as ds9
42 import lsst.afw.display.utils
as displayUtils
43 import lsst.meas.base
as measBase
44 from .
import subtractPsf, fitKernelParamsToImage
45 from lsst.afw.image.utils
import CalibNoThrow
52 objId = int((oid & 0xffff) - 1)
55 return dict(objId=objId)
60 def showSourceSet(sSet, xy0=(0, 0), frame=0, ctype=ds9.GREEN, symb=
"+", size=2):
61 """Draw the (XAstrom, YAstrom) positions of a set of Sources. Image has the given XY0"""
65 xc, yc = s.getXAstrom() - xy0[0], s.getYAstrom() - xy0[1]
68 ds9.dot(str(
splitId(s.getId(),
True)[
"objId"]), xc, yc, frame=frame, ctype=ctype, size=size)
70 ds9.dot(symb, xc, yc, frame=frame, ctype=ctype, size=size)
78 def showPsfSpatialCells(exposure, psfCellSet, nMaxPerCell=-1, showChi2=False, showMoments=False,
79 symb=
None, ctype=
None, ctypeUnused=
None, ctypeBad=
None, size=2, frame=
None):
80 """Show the SpatialCells. If symb is something that ds9.dot understands (e.g. "o"), the
81 top nMaxPerCell candidates will be indicated with that symbol, using ctype and size"""
84 origin = [-exposure.getMaskedImage().getX0(), -exposure.getMaskedImage().getY0()]
85 for cell
in psfCellSet.getCellList():
86 displayUtils.drawBBox(cell.getBBox(), origin=origin, frame=frame)
92 goodies = ctypeBad
is None
93 for cand
in cell.begin(goodies):
97 xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1]
103 color = ctypeBad
if cand.isBad()
else ctype
111 ds9.dot(symb, xc, yc, frame=frame, ctype=ct, size=size)
113 source = cand.getSource()
116 rchi2 = cand.getChi2()
119 ds9.dot(
"%d %.1f" % (
splitId(source.getId(),
True)[
"objId"], rchi2),
120 xc - size, yc - size - 4, frame=frame, ctype=color, size=2)
123 ds9.dot(
"%.2f %.2f %.2f" % (source.getIxx(), source.getIxy(), source.getIyy()),
124 xc-size, yc + size + 4, frame=frame, ctype=color, size=size)
127 def showPsfCandidates(exposure, psfCellSet, psf=None, frame=None, normalize=True, showBadCandidates=True,
128 fitBasisComponents=
False, variance=
None, chi=
None):
129 """Display the PSF candidates.
131 If psf is provided include PSF model and residuals; if normalize is true normalize the PSFs
134 If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi
136 If fitBasisComponents is true, also find the best linear combination of the PSF's components
140 if variance
is not None:
145 mos = displayUtils.Mosaic()
147 candidateCenters = []
148 candidateCentersBad = []
151 for cell
in psfCellSet.getCellList():
152 for cand
in cell.begin(
False):
153 rchi2 = cand.getChi2()
157 if not showBadCandidates
and cand.isBad():
161 im_resid = displayUtils.Mosaic(gutter=0, background=-5, mode=
"x")
164 im = cand.getMaskedImage()
165 xc, yc = cand.getXCenter(), cand.getYCenter()
167 margin = 0
if True else 5
168 w, h = im.getDimensions()
169 bbox = afwGeom.BoxI(afwGeom.PointI(margin, margin), im.getDimensions())
172 bim = im.Factory(w + 2*margin, h + 2*margin)
174 stdev = numpy.sqrt(afwMath.makeStatistics(im.getVariance(), afwMath.MEAN).getValue())
175 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
176 bim.getVariance().set(stdev**2)
183 im = im.Factory(im,
True)
184 im.setXY0(cand.getMaskedImage().getXY0())
189 im_resid.append(im.Factory(im,
True))
193 psfIm = mi.getImage()
194 config = measBase.SingleFrameMeasurementTask.ConfigClass()
195 config.slots.centroid =
"base_SdssCentroid"
197 schema = afwTable.SourceTable.makeMinimalSchema()
198 measureSources = measBase.SingleFrameMeasurementTask(schema, config=config)
199 catalog = afwTable.SourceCatalog(schema)
202 miBig = mi.Factory(im.getWidth() + 2*extra, im.getHeight() + 2*extra)
203 miBig[extra:-extra, extra:-extra] = mi
204 miBig.setXY0(mi.getX0() - extra, mi.getY0() - extra)
208 exp = afwImage.makeExposure(mi)
211 footprintSet = afwDet.FootprintSet(mi,
212 afwDet.Threshold(0.5*numpy.max(psfIm.getArray())),
214 footprintSet.makeSources(catalog)
216 if len(catalog) == 0:
217 raise RuntimeError(
"Failed to detect any objects")
219 measureSources.run(catalog, exp)
220 if len(catalog) == 1:
224 for i, s
in enumerate(catalog):
225 d = numpy.hypot(xc - s.getX(), yc - s.getY())
226 if i == 0
or d < dmin:
228 xc, yc = source.getCentroid()
238 resid = resid.getImage()
239 var = im.getVariance()
240 var = var.Factory(var,
True)
241 numpy.sqrt(var.getArray(), var.getArray())
244 im_resid.append(resid)
247 if fitBasisComponents:
248 im = cand.getMaskedImage()
250 im = im.Factory(im,
True)
251 im.setXY0(cand.getMaskedImage().getXY0())
254 noSpatialKernel = psf.getKernel()
256 noSpatialKernel =
None
259 candCenter = afwGeom.PointD(cand.getXCenter(), cand.getYCenter())
262 kernels = afwMath.KernelList(fit[1])
263 outputKernel = afwMath.LinearCombinationKernel(kernels, params)
265 outImage = afwImage.ImageD(outputKernel.getDimensions())
266 outputKernel.computeImage(outImage,
False)
268 im -= outImage.convertF()
272 bim = im.Factory(w + 2*margin, h + 2*margin)
273 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
276 bim.assign(resid, bbox)
280 resid = resid.getImage()
283 im_resid.append(resid)
285 im = im_resid.makeMosaic()
287 im = cand.getMaskedImage()
290 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
292 objId =
splitId(cand.getSource().getId(),
True)[
"objId"]
294 lab =
"%d chi^2 %.1f" % (objId, rchi2)
295 ctype = ds9.RED
if cand.isBad()
else ds9.GREEN
297 lab =
"%d flux %8.3g" % (objId, cand.getSource().getPsfFlux())
300 mos.append(im, lab, ctype)
302 if False and numpy.isnan(rchi2):
303 ds9.mtv(cand.getMaskedImage().getImage(), title=
"candidate", frame=1)
304 print(
"amp", cand.getAmplitude())
306 im = cand.getMaskedImage()
307 center = (candidateIndex, xc - im.getX0(), yc - im.getY0())
310 candidateCentersBad.append(center)
312 candidateCenters.append(center)
315 title =
"chi(Psf fit)"
317 title =
"Stars & residuals"
318 mosaicImage = mos.makeMosaic(frame=frame, title=title)
320 with ds9.Buffering():
321 for centers, color
in ((candidateCenters, ds9.GREEN), (candidateCentersBad, ds9.RED)):
323 bbox = mos.getBBox(cen[0])
324 ds9.dot(
"+", cen[1] + bbox.getMinX(), cen[2] + bbox.getMinY(), frame=frame, ctype=color)
329 def makeSubplots(fig, nx=2, ny=2, Nx=1, Ny=1, plottingArea=(0.1, 0.1, 0.85, 0.80),
330 pxgutter=0.05, pygutter=0.05, xgutter=0.04, ygutter=0.04,
331 headroom=0.0, panelBorderWeight=0, panelColor=
'black'):
332 """Return a generator of a set of subplots, a set of Nx*Ny panels of nx*ny plots. Each panel is fully
333 filled by row (starting in the bottom left) before the next panel is started. If panelBorderWidth is
334 greater than zero a border is drawn around each panel, adjusted to enclose the axis labels.
337 subplots = makeSubplots(fig, 2, 2, Nx=1, Ny=1, panelColor='k')
338 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,0)')
339 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,0)')
340 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,1)')
341 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,1)')
344 @param fig The matplotlib figure to draw
345 @param nx The number of plots in each row of each panel
346 @param ny The number of plots in each column of each panel
347 @param Nx The number of panels in each row of the figure
348 @param Ny The number of panels in each column of the figure
349 @param plottingArea (x0, y0, x1, y1) for the part of the figure containing all the panels
350 @param pxgutter Spacing between columns of panels in units of (x1 - x0)
351 @param pygutter Spacing between rows of panels in units of (y1 - y0)
352 @param xgutter Spacing between columns of plots within a panel in units of (x1 - x0)
353 @param ygutter Spacing between rows of plots within a panel in units of (y1 - y0)
354 @param headroom Extra spacing above each plot for e.g. a title
355 @param panelBorderWeight Width of border drawn around panels
356 @param panelColor Colour of border around panels
359 log = lsst.log.Log.getLogger(
"utils.makeSubplots")
361 import matplotlib.pyplot
as plt
362 except ImportError
as e:
363 log.warn(
"Unable to import matplotlib: %s", e)
369 except AttributeError:
370 fig.__show = fig.show
377 fig.show = types.MethodType(myShow, fig, fig.__class__)
386 Callback to draw the panel borders when the plots are drawn to the canvas
388 if panelBorderWeight <= 0:
391 for p
in axes.keys():
394 bboxes.append(ax.bbox.union([label.get_window_extent()
for label
in
395 ax.get_xticklabels() + ax.get_yticklabels()]))
402 bbox = ax.bbox.union(bboxes)
404 xy0, xy1 = ax.transData.inverted().transform(bbox)
407 w, h = x1 - x0, y1 - y0
416 rec = ax.add_patch(plt.Rectangle((x0, y0), w, h, fill=
False,
417 lw=panelBorderWeight, edgecolor=panelColor))
418 rec.set_clip_on(
False)
422 fig.canvas.mpl_connect(
'draw_event', on_draw)
426 x0, y0 = plottingArea[0:2]
427 W, H = plottingArea[2:4]
428 w = (W - (Nx - 1)*pxgutter - (nx*Nx - 1)*xgutter)/float(nx*Nx)
429 h = (H - (Ny - 1)*pygutter - (ny*Ny - 1)*ygutter)/float(ny*Ny)
433 for panel
in range(Nx*Ny):
437 for window
in range(nx*ny):
438 x = nx*px + window%nx
439 y = ny*py + window//nx
440 ax = fig.add_axes((x0 + xgutter + pxgutter + x*w + (px - 1)*pxgutter + (x - 1)*xgutter,
441 y0 + ygutter + pygutter + y*h + (py - 1)*pygutter + (y - 1)*ygutter,
442 w, h), frame_on=
True, axis_bgcolor=
'w')
443 axes[panel].append(ax)
448 matchKernelAmplitudes=
False, keepPlots=
True):
449 """Plot the PSF spatial model."""
451 log = lsst.log.Log.getLogger(
"utils.plotPsfSpatialModel")
453 import matplotlib.pyplot
as plt
454 import matplotlib.colors
455 except ImportError
as e:
456 log.warn(
"Unable to import matplotlib: %s", e)
459 noSpatialKernel = psf.getKernel()
466 for cell
in psfCellSet.getCellList():
467 for cand
in cell.begin(
False):
468 if not showBadCandidates
and cand.isBad():
470 candCenter = afwGeom.PointD(cand.getXCenter(), cand.getYCenter())
472 im = cand.getMaskedImage()
480 for p, k
in zip(params, kernels):
481 amp += p * k.getSum()
483 targetFits = badFits
if cand.isBad()
else candFits
484 targetPos = badPos
if cand.isBad()
else candPos
485 targetAmps = badAmps
if cand.isBad()
else candAmps
487 targetFits.append([x / amp
for x
in params])
488 targetPos.append(candCenter)
489 targetAmps.append(amp)
491 xGood = numpy.array([pos.getX()
for pos
in candPos]) - exposure.getX0()
492 yGood = numpy.array([pos.getY()
for pos
in candPos]) - exposure.getY0()
493 zGood = numpy.array(candFits)
495 xBad = numpy.array([pos.getX()
for pos
in badPos]) - exposure.getX0()
496 yBad = numpy.array([pos.getY()
for pos
in badPos]) - exposure.getY0()
497 zBad = numpy.array(badFits)
500 xRange = numpy.linspace(0, exposure.getWidth(), num=numSample)
501 yRange = numpy.linspace(0, exposure.getHeight(), num=numSample)
503 kernel = psf.getKernel()
504 nKernelComponents = kernel.getNKernelParameters()
508 nPanelX = int(math.sqrt(nKernelComponents))
509 nPanelY = nKernelComponents//nPanelX
510 while nPanelY*nPanelX < nKernelComponents:
516 fig.canvas._tkcanvas._root().lift()
522 subplots =
makeSubplots(fig, 2, 2, Nx=nPanelX, Ny=nPanelY, xgutter=0.06, ygutter=0.06, pygutter=0.04)
524 for k
in range(nKernelComponents):
525 func = kernel.getSpatialFunction(k)
526 dfGood = zGood[:, k] - numpy.array([func(pos.getX(), pos.getY())
for pos
in candPos])
530 dfBad = zBad[:, k] - numpy.array([func(pos.getX(), pos.getY())
for pos
in badPos])
531 yMin = min([yMin, dfBad.min()])
532 yMax = max([yMax, dfBad.max()])
533 yMin -= 0.05 * (yMax - yMin)
534 yMax += 0.05 * (yMax - yMin)
539 fRange = numpy.ndarray((len(xRange), len(yRange)))
540 for j, yVal
in enumerate(yRange):
541 for i, xVal
in enumerate(xRange):
542 fRange[j][i] = func(xVal, yVal)
548 ax.set_autoscale_on(
False)
549 ax.set_xbound(lower=0, upper=exposure.getHeight())
550 ax.set_ybound(lower=yMin, upper=yMax)
551 ax.plot(yGood, dfGood,
'b+')
553 ax.plot(yBad, dfBad,
'r+')
555 ax.set_title(
'Residuals(y)')
561 if matchKernelAmplitudes
and k == 0:
568 norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
569 im = ax.imshow(fRange, aspect=
'auto', origin=
"lower", norm=norm,
570 extent=[0, exposure.getWidth()-1, 0, exposure.getHeight()-1])
571 ax.set_title(
'Spatial poly')
572 plt.colorbar(im, orientation=
'horizontal', ticks=[vmin, vmax])
577 ax.set_autoscale_on(
False)
578 ax.set_xbound(lower=0, upper=exposure.getWidth())
579 ax.set_ybound(lower=yMin, upper=yMax)
580 ax.plot(xGood, dfGood,
'b+')
582 ax.plot(xBad, dfBad,
'r+')
584 ax.set_title(
'K%d Residuals(x)' % k)
591 ax.scatter(xGood, yGood, c=dfGood, marker=
'o')
592 ax.scatter(xBad, yBad, c=dfBad, marker=
'x')
593 ax.set_xbound(lower=0, upper=exposure.getWidth())
594 ax.set_ybound(lower=0, upper=exposure.getHeight())
595 ax.set_title(
'Spatial residuals')
596 plt.colorbar(im, orientation=
'horizontal')
598 calib = exposure.getCalib()
599 if calib.getFluxMag0()[0] <= 0:
600 calib = type(calib)()
601 calib.setFluxMag0(1.0)
604 ax.plot(calib.getMagnitude(candAmps), zGood[:, k],
'b+')
606 ax.plot(calib.getMagnitude(badAmps), zBad[:, k],
'r+')
608 ax.set_title(
'Flux variation')
613 if keepPlots
and not keptPlots:
616 print(
"%s: Please close plots when done." % __name__)
621 print(
"Plots closed, exiting...")
623 atexit.register(show)
627 def showPsf(psf, eigenValues=None, XY=None, normalize=True, frame=None):
628 """Display a PSF's eigen images
630 If normalize is True, set the largest absolute value of each eigenimage to 1.0 (n.b. sum == 0.0 for i > 0)
636 coeffs = psf.getLocalKernel(afwGeom.PointD(XY[0], XY[1])).getKernelParameters()
640 mos = displayUtils.Mosaic(gutter=2, background=-0.1)
641 for i, k
in enumerate(psf.getKernel().getKernelList()):
642 im = afwImage.ImageD(k.getDimensions())
643 k.computeImage(im,
False)
645 im /= numpy.max(numpy.abs(im.getArray()))
648 mos.append(im,
"%g" % (coeffs[i]/coeffs[0]))
652 mos.makeMosaic(frame=frame, title=
"Kernel Basis Functions")
658 showCenter=
True, showEllipticity=
False, showFwhm=
False,
659 stampSize=0, frame=
None, title=
None):
660 """Show a mosaic of Psf images. exposure may be an Exposure (optionally with PSF),
661 or a tuple (width, height)
663 If stampSize is > 0, the psf images will be trimmed to stampSize*stampSize
668 showEllipticity =
True
669 scale = 2*math.log(2)
671 mos = displayUtils.Mosaic()
674 width, height = exposure.getWidth(), exposure.getHeight()
675 x0, y0 = exposure.getXY0()
677 psf = exposure.getPsf()
678 except AttributeError:
680 width, height = exposure[0], exposure[1]
683 raise RuntimeError(
"Unable to extract width/height from object of type %s" % type(exposure))
686 ny = int(nx*float(height)/width + 0.5)
690 centroidName =
"base_GaussianCentroid"
691 shapeName =
"base_SdssShape"
693 schema = afwTable.SourceTable.makeMinimalSchema()
694 schema.getAliasMap().set(
"slot_Centroid", centroidName)
695 schema.getAliasMap().set(
"slot_Centroid_flag", centroidName+
"_flag")
697 control = measBase.GaussianCentroidControl()
698 centroider = measBase.GaussianCentroidAlgorithm(control, centroidName, schema)
700 sdssShape = measBase.SdssShapeControl()
701 shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema)
702 table = afwTable.SourceTable.make(schema)
704 table.defineCentroid(centroidName)
705 table.defineShape(shapeName)
709 w, h = psf.computeImage(afwGeom.PointD(0, 0)).getDimensions()
710 if stampSize <= w
and stampSize <= h:
711 bbox = afwGeom.BoxI(afwGeom.PointI((w - stampSize)//2, (h - stampSize)//2),
712 afwGeom.ExtentI(stampSize, stampSize))
718 x = int(ix*(width-1)/(nx-1)) + x0
719 y = int(iy*(height-1)/(ny-1)) + y0
721 im = psf.computeImage(afwGeom.PointD(x, y)).convertF()
722 imPeak = psf.computePeak(afwGeom.PointD(x, y))
725 im = im.Factory(im, bbox)
726 lab =
"PSF(%d,%d)" % (x, y)
if False else ""
729 exp = afwImage.makeExposure(afwImage.makeMaskedImage(im))
730 w, h = im.getWidth(), im.getHeight()
731 centerX = im.getX0() + w//2
732 centerY = im.getY0() + h//2
733 src = table.makeRecord()
734 foot = afwDet.Footprint(exp.getBBox())
735 foot.addPeak(centerX, centerY, 1)
736 src.setFootprint(foot)
738 centroider.measure(src, exp)
739 centers.append((src.getX() - im.getX0(), src.getY() - im.getY0()))
741 shaper.measure(src, exp)
742 shapes.append((src.getIxx(), src.getIxy(), src.getIyy()))
744 mos.makeMosaic(frame=frame, title=title
if title
else "Model Psf", mode=nx)
746 if centers
and frame
is not None:
747 with ds9.Buffering():
748 for i, (cen, shape)
in enumerate(zip(centers, shapes)):
749 bbox = mos.getBBox(i)
750 xc, yc = cen[0] + bbox.getMinX(), cen[1] + bbox.getMinY()
752 ds9.dot(
"+", xc, yc, ctype=ds9.BLUE, frame=frame)
755 ixx, ixy, iyy = shape
759 ds9.dot(
"@:%g,%g,%g" % (ixx, ixy, iyy), xc, yc, frame=frame, ctype=ds9.RED)
765 mimIn = exposure.getMaskedImage()
766 mimIn = mimIn.Factory(mimIn,
True)
768 psf = exposure.getPsf()
769 psfWidth, psfHeight = psf.getLocalKernel().getDimensions()
773 w, h = int(mimIn.getWidth()/scale), int(mimIn.getHeight()/scale)
775 im = mimIn.Factory(w + psfWidth, h + psfHeight)
779 x, y = s.getX(), s.getY()
781 sx, sy = int(x/scale + 0.5), int(y/scale + 0.5)
783 smim = im.Factory(im, afwGeom.BoxI(afwGeom.PointI(sx, sy), afwGeom.ExtentI(psfWidth, psfHeight)))
784 sim = smim.getImage()
789 elif magType ==
"model":
790 flux = s.getModelFlux()
791 elif magType ==
"psf":
792 flux = s.getPsfFlux()
794 raise RuntimeError(
"Unknown flux type %s" % magType)
797 except Exception
as e:
801 expIm = mimIn.getImage().Factory(mimIn.getImage(),
802 afwGeom.BoxI(afwGeom.PointI(int(x) - psfWidth//2,
803 int(y) - psfHeight//2),
804 afwGeom.ExtentI(psfWidth, psfHeight)),
806 except pexExcept.Exception:
809 cenPos.append([x - expIm.getX0() + sx, y - expIm.getY0() + sy])
813 if frame
is not None:
814 ds9.mtv(im, frame=frame)
815 with ds9.Buffering():
817 ds9.dot(
"+", x, y, frame=frame)
825 """Write the contents of a SpatialCellSet to a many-MEF fits file"""
828 for cell
in psfCellSet.getCellList():
829 for cand
in cell.begin(
False):
830 dx = afwImage.positionToIndex(cand.getXCenter(),
True)[1]
831 dy = afwImage.positionToIndex(cand.getYCenter(),
True)[1]
832 im = afwMath.offsetImage(cand.getMaskedImage(), -dx, -dy,
"lanczos5")
834 md = dafBase.PropertySet()
835 md.set(
"CELL", cell.getLabel())
836 md.set(
"ID", cand.getId())
837 md.set(
"XCENTER", cand.getXCenter())
838 md.set(
"YCENTER", cand.getYCenter())
839 md.set(
"BAD", cand.isBad())
840 md.set(
"AMPL", cand.getAmplitude())
841 md.set(
"FLUX", cand.getSource().getPsfFlux())
842 md.set(
"CHI2", cand.getSource().getChi2())
844 im.writeFits(fileName, md, mode)
847 if frame
is not None:
848 ds9.mtv(im, frame=frame)
double subtractPsf(lsst::afw::detection::Psf const &psf, ImageT *data, double x, double y, double psfFlux=std::numeric_limits< double >::quiet_NaN())
std::pair< std::vector< double >, lsst::afw::math::KernelList > fitKernelParamsToImage(lsst::afw::math::LinearCombinationKernel const &kernel, Image const &image, lsst::afw::geom::Point2D const &pos)