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)
77 def showPsfSpatialCells(exposure, psfCellSet, nMaxPerCell=-1, showChi2=False, showMoments=False,
78 symb=
None, ctype=
None, ctypeUnused=
None, ctypeBad=
None, size=2, frame=
None):
79 """Show the SpatialCells. If symb is something that ds9.dot understands (e.g. "o"), the
80 top nMaxPerCell candidates will be indicated with that symbol, using ctype and size"""
83 origin = [-exposure.getMaskedImage().getX0(), -exposure.getMaskedImage().getY0()]
84 for cell
in psfCellSet.getCellList():
85 displayUtils.drawBBox(cell.getBBox(), origin=origin, frame=frame)
91 goodies = ctypeBad
is None
92 for cand
in cell.begin(goodies):
96 xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1]
102 color = ctypeBad
if cand.isBad()
else ctype
110 ds9.dot(symb, xc, yc, frame=frame, ctype=ct, size=size)
112 source = cand.getSource()
115 rchi2 = cand.getChi2()
118 ds9.dot(
"%d %.1f" % (
splitId(source.getId(),
True)[
"objId"], rchi2),
119 xc - size, yc - size - 4, frame=frame, ctype=color, size=2)
122 ds9.dot(
"%.2f %.2f %.2f" % (source.getIxx(), source.getIxy(), source.getIyy()),
123 xc-size, yc + size + 4, frame=frame, ctype=color, size=size)
126 def showPsfCandidates(exposure, psfCellSet, psf=None, frame=None, normalize=True, showBadCandidates=True,
127 fitBasisComponents=
False, variance=
None, chi=
None):
128 """Display the PSF candidates.
130 If psf is provided include PSF model and residuals; if normalize is true normalize the PSFs
133 If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi
135 If fitBasisComponents is true, also find the best linear combination of the PSF's components
139 if variance
is not None:
144 mos = displayUtils.Mosaic()
146 candidateCenters = []
147 candidateCentersBad = []
150 for cell
in psfCellSet.getCellList():
151 for cand
in cell.begin(
False):
152 rchi2 = cand.getChi2()
156 if not showBadCandidates
and cand.isBad():
160 im_resid = displayUtils.Mosaic(gutter=0, background=-5, mode=
"x")
163 im = cand.getMaskedImage()
164 xc, yc = cand.getXCenter(), cand.getYCenter()
166 margin = 0
if True else 5
167 w, h = im.getDimensions()
168 bbox = afwGeom.BoxI(afwGeom.PointI(margin, margin), im.getDimensions())
171 bim = im.Factory(w + 2*margin, h + 2*margin)
173 stdev = numpy.sqrt(afwMath.makeStatistics(im.getVariance(), afwMath.MEAN).getValue())
174 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
175 bim.getVariance().set(stdev**2)
182 im = im.Factory(im,
True)
183 im.setXY0(cand.getMaskedImage().getXY0())
188 im_resid.append(im.Factory(im,
True))
192 psfIm = mi.getImage()
193 config = measBase.SingleFrameMeasurementTask.ConfigClass()
194 config.slots.centroid =
"base_SdssCentroid"
196 schema = afwTable.SourceTable.makeMinimalSchema()
197 measureSources = measBase.SingleFrameMeasurementTask(schema, config=config)
198 catalog = afwTable.SourceCatalog(schema)
201 miBig = mi.Factory(im.getWidth() + 2*extra, im.getHeight() + 2*extra)
202 miBig[extra:-extra, extra:-extra] = mi
203 miBig.setXY0(mi.getX0() - extra, mi.getY0() - extra)
207 exp = afwImage.makeExposure(mi)
210 footprintSet = afwDet.FootprintSet(mi,
211 afwDet.Threshold(0.5*numpy.max(psfIm.getArray())),
213 footprintSet.makeSources(catalog)
215 if len(catalog) == 0:
216 raise RuntimeError(
"Failed to detect any objects")
218 measureSources.run(catalog, exp)
219 if len(catalog) == 1:
223 for i, s
in enumerate(catalog):
224 d = numpy.hypot(xc - s.getX(), yc - s.getY())
225 if i == 0
or d < dmin:
227 xc, yc = source.getCentroid()
237 resid = resid.getImage()
238 var = im.getVariance()
239 var = var.Factory(var,
True)
240 numpy.sqrt(var.getArray(), var.getArray())
243 im_resid.append(resid)
246 if fitBasisComponents:
247 im = cand.getMaskedImage()
249 im = im.Factory(im,
True)
250 im.setXY0(cand.getMaskedImage().getXY0())
253 noSpatialKernel = psf.getKernel()
255 noSpatialKernel =
None
258 candCenter = afwGeom.PointD(cand.getXCenter(), cand.getYCenter())
261 kernels = afwMath.KernelList(fit[1])
262 outputKernel = afwMath.LinearCombinationKernel(kernels, params)
264 outImage = afwImage.ImageD(outputKernel.getDimensions())
265 outputKernel.computeImage(outImage,
False)
267 im -= outImage.convertF()
271 bim = im.Factory(w + 2*margin, h + 2*margin)
272 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
275 bim.assign(resid, bbox)
279 resid = resid.getImage()
282 im_resid.append(resid)
284 im = im_resid.makeMosaic()
286 im = cand.getMaskedImage()
289 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
291 objId =
splitId(cand.getSource().getId(),
True)[
"objId"]
293 lab =
"%d chi^2 %.1f" % (objId, rchi2)
294 ctype = ds9.RED
if cand.isBad()
else ds9.GREEN
296 lab =
"%d flux %8.3g" % (objId, cand.getSource().getPsfFlux())
299 mos.append(im, lab, ctype)
301 if False and numpy.isnan(rchi2):
302 ds9.mtv(cand.getMaskedImage().getImage(), title=
"candidate", frame=1)
303 print(
"amp", cand.getAmplitude())
305 im = cand.getMaskedImage()
306 center = (candidateIndex, xc - im.getX0(), yc - im.getY0())
309 candidateCentersBad.append(center)
311 candidateCenters.append(center)
314 title =
"chi(Psf fit)"
316 title =
"Stars & residuals"
317 mosaicImage = mos.makeMosaic(frame=frame, title=title)
319 with ds9.Buffering():
320 for centers, color
in ((candidateCenters, ds9.GREEN), (candidateCentersBad, ds9.RED)):
322 bbox = mos.getBBox(cen[0])
323 ds9.dot(
"+", cen[1] + bbox.getMinX(), cen[2] + bbox.getMinY(), frame=frame, ctype=color)
328 def makeSubplots(fig, nx=2, ny=2, Nx=1, Ny=1, plottingArea=(0.1, 0.1, 0.85, 0.80),
329 pxgutter=0.05, pygutter=0.05, xgutter=0.04, ygutter=0.04,
330 headroom=0.0, panelBorderWeight=0, panelColor=
'black'):
331 """Return a generator of a set of subplots, a set of Nx*Ny panels of nx*ny plots. Each panel is fully
332 filled by row (starting in the bottom left) before the next panel is started. If panelBorderWidth is
333 greater than zero a border is drawn around each panel, adjusted to enclose the axis labels.
336 subplots = makeSubplots(fig, 2, 2, Nx=1, Ny=1, panelColor='k')
337 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,0)')
338 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,0)')
339 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,1)')
340 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,1)')
343 @param fig The matplotlib figure to draw
344 @param nx The number of plots in each row of each panel
345 @param ny The number of plots in each column of each panel
346 @param Nx The number of panels in each row of the figure
347 @param Ny The number of panels in each column of the figure
348 @param plottingArea (x0, y0, x1, y1) for the part of the figure containing all the panels
349 @param pxgutter Spacing between columns of panels in units of (x1 - x0)
350 @param pygutter Spacing between rows of panels in units of (y1 - y0)
351 @param xgutter Spacing between columns of plots within a panel in units of (x1 - x0)
352 @param ygutter Spacing between rows of plots within a panel in units of (y1 - y0)
353 @param headroom Extra spacing above each plot for e.g. a title
354 @param panelBorderWeight Width of border drawn around panels
355 @param panelColor Colour of border around panels
358 log = lsst.log.Log.getLogger(
"utils.makeSubplots")
360 import matplotlib.pyplot
as plt
361 except ImportError
as e:
362 log.warn(
"Unable to import matplotlib: %s", e)
368 except AttributeError:
369 fig.__show = fig.show
376 fig.show = types.MethodType(myShow, fig, fig.__class__)
385 Callback to draw the panel borders when the plots are drawn to the canvas
387 if panelBorderWeight <= 0:
390 for p
in axes.keys():
393 bboxes.append(ax.bbox.union([label.get_window_extent()
for label
in
394 ax.get_xticklabels() + ax.get_yticklabels()]))
401 bbox = ax.bbox.union(bboxes)
403 xy0, xy1 = ax.transData.inverted().transform(bbox)
406 w, h = x1 - x0, y1 - y0
415 rec = ax.add_patch(plt.Rectangle((x0, y0), w, h, fill=
False,
416 lw=panelBorderWeight, edgecolor=panelColor))
417 rec.set_clip_on(
False)
421 fig.canvas.mpl_connect(
'draw_event', on_draw)
425 x0, y0 = plottingArea[0:2]
426 W, H = plottingArea[2:4]
427 w = (W - (Nx - 1)*pxgutter - (nx*Nx - 1)*xgutter)/float(nx*Nx)
428 h = (H - (Ny - 1)*pygutter - (ny*Ny - 1)*ygutter)/float(ny*Ny)
432 for panel
in range(Nx*Ny):
436 for window
in range(nx*ny):
437 x = nx*px + window%nx
438 y = ny*py + window//nx
439 ax = fig.add_axes((x0 + xgutter + pxgutter + x*w + (px - 1)*pxgutter + (x - 1)*xgutter,
440 y0 + ygutter + pygutter + y*h + (py - 1)*pygutter + (y - 1)*ygutter,
441 w, h), frame_on=
True, axis_bgcolor=
'w')
442 axes[panel].append(ax)
447 matchKernelAmplitudes=
False, keepPlots=
True):
448 """Plot the PSF spatial model."""
450 log = lsst.log.Log.getLogger(
"utils.plotPsfSpatialModel")
452 import matplotlib.pyplot
as plt
453 import matplotlib.colors
454 except ImportError
as e:
455 log.warn(
"Unable to import matplotlib: %s", e)
458 noSpatialKernel = psf.getKernel()
465 for cell
in psfCellSet.getCellList():
466 for cand
in cell.begin(
False):
467 if not showBadCandidates
and cand.isBad():
469 candCenter = afwGeom.PointD(cand.getXCenter(), cand.getYCenter())
471 im = cand.getMaskedImage()
479 for p, k
in zip(params, kernels):
480 amp += p * k.getSum()
482 targetFits = badFits
if cand.isBad()
else candFits
483 targetPos = badPos
if cand.isBad()
else candPos
484 targetAmps = badAmps
if cand.isBad()
else candAmps
486 targetFits.append([x / amp
for x
in params])
487 targetPos.append(candCenter)
488 targetAmps.append(amp)
490 xGood = numpy.array([pos.getX()
for pos
in candPos]) - exposure.getX0()
491 yGood = numpy.array([pos.getY()
for pos
in candPos]) - exposure.getY0()
492 zGood = numpy.array(candFits)
494 xBad = numpy.array([pos.getX()
for pos
in badPos]) - exposure.getX0()
495 yBad = numpy.array([pos.getY()
for pos
in badPos]) - exposure.getY0()
496 zBad = numpy.array(badFits)
499 xRange = numpy.linspace(0, exposure.getWidth(), num=numSample)
500 yRange = numpy.linspace(0, exposure.getHeight(), num=numSample)
502 kernel = psf.getKernel()
503 nKernelComponents = kernel.getNKernelParameters()
507 nPanelX = int(math.sqrt(nKernelComponents))
508 nPanelY = nKernelComponents//nPanelX
509 while nPanelY*nPanelX < nKernelComponents:
515 fig.canvas._tkcanvas._root().lift()
521 subplots =
makeSubplots(fig, 2, 2, Nx=nPanelX, Ny=nPanelY, xgutter=0.06, ygutter=0.06, pygutter=0.04)
523 for k
in range(nKernelComponents):
524 func = kernel.getSpatialFunction(k)
525 dfGood = zGood[:, k] - numpy.array([func(pos.getX(), pos.getY())
for pos
in candPos])
529 dfBad = zBad[:, k] - numpy.array([func(pos.getX(), pos.getY())
for pos
in badPos])
530 yMin = min([yMin, dfBad.min()])
531 yMax = max([yMax, dfBad.max()])
532 yMin -= 0.05 * (yMax - yMin)
533 yMax += 0.05 * (yMax - yMin)
538 fRange = numpy.ndarray((len(xRange), len(yRange)))
539 for j, yVal
in enumerate(yRange):
540 for i, xVal
in enumerate(xRange):
541 fRange[j][i] = func(xVal, yVal)
545 ax.set_autoscale_on(
False)
546 ax.set_xbound(lower=0, upper=exposure.getHeight())
547 ax.set_ybound(lower=yMin, upper=yMax)
548 ax.plot(yGood, dfGood,
'b+')
550 ax.plot(yBad, dfBad,
'r+')
552 ax.set_title(
'Residuals(y)')
556 if matchKernelAmplitudes
and k == 0:
563 norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
564 im = ax.imshow(fRange, aspect=
'auto', origin=
"lower", norm=norm,
565 extent=[0, exposure.getWidth()-1, 0, exposure.getHeight()-1])
566 ax.set_title(
'Spatial poly')
567 plt.colorbar(im, orientation=
'horizontal', ticks=[vmin, vmax])
570 ax.set_autoscale_on(
False)
571 ax.set_xbound(lower=0, upper=exposure.getWidth())
572 ax.set_ybound(lower=yMin, upper=yMax)
573 ax.plot(xGood, dfGood,
'b+')
575 ax.plot(xBad, dfBad,
'r+')
577 ax.set_title(
'K%d Residuals(x)' % k)
582 ax.scatter(xGood, yGood, c=dfGood, marker=
'o')
583 ax.scatter(xBad, yBad, c=dfBad, marker=
'x')
584 ax.set_xbound(lower=0, upper=exposure.getWidth())
585 ax.set_ybound(lower=0, upper=exposure.getHeight())
586 ax.set_title(
'Spatial residuals')
587 plt.colorbar(im, orientation=
'horizontal')
589 calib = exposure.getCalib()
590 if calib.getFluxMag0()[0] <= 0:
591 calib = type(calib)()
592 calib.setFluxMag0(1.0)
595 ax.plot(calib.getMagnitude(candAmps), zGood[:, k],
'b+')
597 ax.plot(calib.getMagnitude(badAmps), zBad[:, k],
'r+')
599 ax.set_title(
'Flux variation')
604 if keepPlots
and not keptPlots:
607 print(
"%s: Please close plots when done." % __name__)
612 print(
"Plots closed, exiting...")
614 atexit.register(show)
618 def showPsf(psf, eigenValues=None, XY=None, normalize=True, frame=None):
619 """Display a PSF's eigen images
621 If normalize is True, set the largest absolute value of each eigenimage to 1.0 (n.b. sum == 0.0 for i > 0)
627 coeffs = psf.getLocalKernel(afwGeom.PointD(XY[0], XY[1])).getKernelParameters()
631 mos = displayUtils.Mosaic(gutter=2, background=-0.1)
632 for i, k
in enumerate(psf.getKernel().getKernelList()):
633 im = afwImage.ImageD(k.getDimensions())
634 k.computeImage(im,
False)
636 im /= numpy.max(numpy.abs(im.getArray()))
639 mos.append(im,
"%g" % (coeffs[i]/coeffs[0]))
643 mos.makeMosaic(frame=frame, title=
"Kernel Basis Functions")
649 showCenter=
True, showEllipticity=
False, showFwhm=
False,
650 stampSize=0, frame=
None, title=
None):
651 """Show a mosaic of Psf images. exposure may be an Exposure (optionally with PSF),
652 or a tuple (width, height)
654 If stampSize is > 0, the psf images will be trimmed to stampSize*stampSize
659 showEllipticity =
True
660 scale = 2*math.log(2)
662 mos = displayUtils.Mosaic()
665 width, height = exposure.getWidth(), exposure.getHeight()
666 x0, y0 = exposure.getXY0()
668 psf = exposure.getPsf()
669 except AttributeError:
671 width, height = exposure[0], exposure[1]
674 raise RuntimeError(
"Unable to extract width/height from object of type %s" % type(exposure))
677 ny = int(nx*float(height)/width + 0.5)
681 centroidName =
"base_GaussianCentroid"
682 shapeName =
"base_SdssShape"
684 schema = afwTable.SourceTable.makeMinimalSchema()
685 schema.getAliasMap().set(
"slot_Centroid", centroidName)
686 schema.getAliasMap().set(
"slot_Centroid_flag", centroidName+
"_flag")
688 control = measBase.GaussianCentroidControl()
689 centroider = measBase.GaussianCentroidAlgorithm(control, centroidName, schema)
691 sdssShape = measBase.SdssShapeControl()
692 shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema)
693 table = afwTable.SourceTable.make(schema)
695 table.defineCentroid(centroidName)
696 table.defineShape(shapeName)
700 w, h = psf.computeImage(afwGeom.PointD(0, 0)).getDimensions()
701 if stampSize <= w
and stampSize <= h:
702 bbox = afwGeom.BoxI(afwGeom.PointI((w - stampSize)//2, (h - stampSize)//2),
703 afwGeom.ExtentI(stampSize, stampSize))
709 x = int(ix*(width-1)/(nx-1)) + x0
710 y = int(iy*(height-1)/(ny-1)) + y0
712 im = psf.computeImage(afwGeom.PointD(x, y)).convertF()
713 imPeak = psf.computePeak(afwGeom.PointD(x, y))
716 im = im.Factory(im, bbox)
717 lab =
"PSF(%d,%d)" % (x, y)
if False else ""
720 exp = afwImage.makeExposure(afwImage.makeMaskedImage(im))
721 w, h = im.getWidth(), im.getHeight()
722 centerX = im.getX0() + w//2
723 centerY = im.getY0() + h//2
724 src = table.makeRecord()
725 foot = afwDet.Footprint(exp.getBBox())
726 foot.addPeak(centerX, centerY, 1)
727 src.setFootprint(foot)
729 centroider.measure(src, exp)
730 centers.append((src.getX() - im.getX0(), src.getY() - im.getY0()))
732 shaper.measure(src, exp)
733 shapes.append((src.getIxx(), src.getIxy(), src.getIyy()))
735 mos.makeMosaic(frame=frame, title=title
if title
else "Model Psf", mode=nx)
737 if centers
and frame
is not None:
738 with ds9.Buffering():
739 for i, (cen, shape)
in enumerate(zip(centers, shapes)):
740 bbox = mos.getBBox(i)
741 xc, yc = cen[0] + bbox.getMinX(), cen[1] + bbox.getMinY()
743 ds9.dot(
"+", xc, yc, ctype=ds9.BLUE, frame=frame)
746 ixx, ixy, iyy = shape
750 ds9.dot(
"@:%g,%g,%g" % (ixx, ixy, iyy), xc, yc, frame=frame, ctype=ds9.RED)
756 mimIn = exposure.getMaskedImage()
757 mimIn = mimIn.Factory(mimIn,
True)
759 psf = exposure.getPsf()
760 psfWidth, psfHeight = psf.getLocalKernel().getDimensions()
764 w, h = int(mimIn.getWidth()/scale), int(mimIn.getHeight()/scale)
766 im = mimIn.Factory(w + psfWidth, h + psfHeight)
770 x, y = s.getX(), s.getY()
772 sx, sy = int(x/scale + 0.5), int(y/scale + 0.5)
774 smim = im.Factory(im, afwGeom.BoxI(afwGeom.PointI(sx, sy), afwGeom.ExtentI(psfWidth, psfHeight)))
775 sim = smim.getImage()
780 elif magType ==
"model":
781 flux = s.getModelFlux()
782 elif magType ==
"psf":
783 flux = s.getPsfFlux()
785 raise RuntimeError(
"Unknown flux type %s" % magType)
788 except Exception
as e:
792 expIm = mimIn.getImage().Factory(mimIn.getImage(),
793 afwGeom.BoxI(afwGeom.PointI(int(x) - psfWidth//2,
794 int(y) - psfHeight//2),
795 afwGeom.ExtentI(psfWidth, psfHeight)),
797 except pexExcept.Exception:
800 cenPos.append([x - expIm.getX0() + sx, y - expIm.getY0() + sy])
804 if frame
is not None:
805 ds9.mtv(im, frame=frame)
806 with ds9.Buffering():
808 ds9.dot(
"+", x, y, frame=frame)
814 """Write the contents of a SpatialCellSet to a many-MEF fits file"""
817 for cell
in psfCellSet.getCellList():
818 for cand
in cell.begin(
False):
819 dx = afwImage.positionToIndex(cand.getXCenter(),
True)[1]
820 dy = afwImage.positionToIndex(cand.getYCenter(),
True)[1]
821 im = afwMath.offsetImage(cand.getMaskedImage(), -dx, -dy,
"lanczos5")
823 md = dafBase.PropertySet()
824 md.set(
"CELL", cell.getLabel())
825 md.set(
"ID", cand.getId())
826 md.set(
"XCENTER", cand.getXCenter())
827 md.set(
"YCENTER", cand.getYCenter())
828 md.set(
"BAD", cand.isBad())
829 md.set(
"AMPL", cand.getAmplitude())
830 md.set(
"FLUX", cand.getSource().getPsfFlux())
831 md.set(
"CHI2", cand.getSource().getChi2())
833 im.writeFits(fileName, md, mode)
836 if frame
is not None:
837 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)