Coverage for python/lsst/meas/algorithms/utils.py: 4%
545 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 18:11 -0800
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 18:11 -0800
1# This file is part of meas_algorithms.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22"""Support utilities for Measuring sources"""
24__all__ = ["splitId", "showSourceSet", "showPsfSpatialCells",
25 "showPsfCandidates", "makeSubplots", "plotPsfSpatialModel",
26 "showPsf", "showPsfMosaic", "showPsfResiduals", "saveSpatialCellSet"]
28import math
29import numpy
31import lsst.log
32import lsst.pex.exceptions as pexExcept
33import lsst.daf.base as dafBase
34import lsst.geom
35import lsst.afw.geom as afwGeom
36import lsst.afw.detection as afwDet
37import lsst.afw.image as afwImage
38import lsst.afw.math as afwMath
39import lsst.afw.table as afwTable
40import lsst.afw.display as afwDisplay
41import lsst.afw.display.utils as displayUtils
42import lsst.meas.base as measBase
43from . import subtractPsf, fitKernelParamsToImage
45keptPlots = False # Have we arranged to keep spatial plots open?
47afwDisplay.setDefaultMaskTransparency(75)
50def splitId(oid, asDict=True):
52 objId = int((oid & 0xffff) - 1) # Should be the value set by apps code
54 if asDict:
55 return dict(objId=objId)
56 else:
57 return [objId]
60def showSourceSet(sSet, xy0=(0, 0), display=None, ctype=afwDisplay.GREEN, symb="+", size=2):
61 """Draw the (XAstrom, YAstrom) positions of a set of Sources. Image has the given XY0"""
63 if not display:
64 display = afwDisplay.Display()
65 with display.Buffering():
66 for s in sSet:
67 xc, yc = s.getXAstrom() - xy0[0], s.getYAstrom() - xy0[1]
69 if symb == "id":
70 display.dot(str(splitId(s.getId(), True)["objId"]), xc, yc, ctype=ctype, size=size)
71 else:
72 display.dot(symb, xc, yc, ctype=ctype, size=size)
74#
75# PSF display utilities
76#
79def showPsfSpatialCells(exposure, psfCellSet, nMaxPerCell=-1, showChi2=False, showMoments=False,
80 symb=None, ctype=None, ctypeUnused=None, ctypeBad=None, size=2, display=None):
81 """Show the SpatialCells.
83 If symb is something that afwDisplay.Display.dot() understands (e.g. "o"),
84 the top nMaxPerCell candidates will be indicated with that symbol, using
85 ctype and size.
86 """
88 if not display:
89 display = afwDisplay.Display()
90 with display.Buffering():
91 origin = [-exposure.getMaskedImage().getX0(), -exposure.getMaskedImage().getY0()]
92 for cell in psfCellSet.getCellList():
93 displayUtils.drawBBox(cell.getBBox(), origin=origin, display=display)
95 if nMaxPerCell < 0:
96 nMaxPerCell = 0
98 i = 0
99 goodies = ctypeBad is None
100 for cand in cell.begin(goodies):
101 if nMaxPerCell > 0:
102 i += 1
104 xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1]
106 if i > nMaxPerCell:
107 if not ctypeUnused:
108 continue
110 color = ctypeBad if cand.isBad() else ctype
112 if symb:
113 if i > nMaxPerCell:
114 ct = ctypeUnused
115 else:
116 ct = ctype
118 display.dot(symb, xc, yc, ctype=ct, size=size)
120 source = cand.getSource()
122 if showChi2:
123 rchi2 = cand.getChi2()
124 if rchi2 > 1e100:
125 rchi2 = numpy.nan
126 display.dot("%d %.1f" % (splitId(source.getId(), True)["objId"], rchi2),
127 xc - size, yc - size - 4, ctype=color, size=2)
129 if showMoments:
130 display.dot("%.2f %.2f %.2f" % (source.getIxx(), source.getIxy(), source.getIyy()),
131 xc-size, yc + size + 4, ctype=color, size=size)
132 return display
135def showPsfCandidates(exposure, psfCellSet, psf=None, display=None, normalize=True, showBadCandidates=True,
136 fitBasisComponents=False, variance=None, chi=None):
137 """Display the PSF candidates.
139 If psf is provided include PSF model and residuals; if normalize is true normalize the PSFs
140 (and residuals)
142 If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi
144 If fitBasisComponents is true, also find the best linear combination of the PSF's components
145 (if they exist)
146 """
147 if not display:
148 display = afwDisplay.Display()
150 if chi is None:
151 if variance is not None: # old name for chi
152 chi = variance
153 #
154 # Show us the ccandidates
155 #
156 mos = displayUtils.Mosaic()
157 #
158 candidateCenters = []
159 candidateCentersBad = []
160 candidateIndex = 0
162 for cell in psfCellSet.getCellList():
163 for cand in cell.begin(False): # include bad candidates
164 rchi2 = cand.getChi2()
165 if rchi2 > 1e100:
166 rchi2 = numpy.nan
168 if not showBadCandidates and cand.isBad():
169 continue
171 if psf:
172 im_resid = displayUtils.Mosaic(gutter=0, background=-5, mode="x")
174 try:
175 im = cand.getMaskedImage() # copy of this object's image
176 xc, yc = cand.getXCenter(), cand.getYCenter()
178 margin = 0 if True else 5
179 w, h = im.getDimensions()
180 bbox = lsst.geom.BoxI(lsst.geom.PointI(margin, margin), im.getDimensions())
182 if margin > 0:
183 bim = im.Factory(w + 2*margin, h + 2*margin)
185 stdev = numpy.sqrt(afwMath.makeStatistics(im.getVariance(), afwMath.MEAN).getValue())
186 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
187 bim.getVariance().set(stdev**2)
189 bim.assign(im, bbox)
190 im = bim
191 xc += margin
192 yc += margin
194 im = im.Factory(im, True)
195 im.setXY0(cand.getMaskedImage().getXY0())
196 except Exception:
197 continue
199 if not variance:
200 im_resid.append(im.Factory(im, True))
202 if True: # tweak up centroids
203 mi = im
204 psfIm = mi.getImage()
205 config = measBase.SingleFrameMeasurementTask.ConfigClass()
206 config.slots.centroid = "base_SdssCentroid"
208 schema = afwTable.SourceTable.makeMinimalSchema()
209 measureSources = measBase.SingleFrameMeasurementTask(schema, config=config)
210 catalog = afwTable.SourceCatalog(schema)
212 extra = 10 # enough margin to run the sdss centroider
213 miBig = mi.Factory(im.getWidth() + 2*extra, im.getHeight() + 2*extra)
214 miBig[extra:-extra, extra:-extra, afwImage.LOCAL] = mi
215 miBig.setXY0(mi.getX0() - extra, mi.getY0() - extra)
216 mi = miBig
217 del miBig
219 exp = afwImage.makeExposure(mi)
220 exp.setPsf(psf)
222 footprintSet = afwDet.FootprintSet(mi,
223 afwDet.Threshold(0.5*numpy.max(psfIm.getArray())),
224 "DETECTED")
225 footprintSet.makeSources(catalog)
227 if len(catalog) == 0:
228 raise RuntimeError("Failed to detect any objects")
230 measureSources.run(catalog, exp)
231 if len(catalog) == 1:
232 source = catalog[0]
233 else: # more than one source; find the once closest to (xc, yc)
234 dmin = None # an invalid value to catch logic errors
235 for i, s in enumerate(catalog):
236 d = numpy.hypot(xc - s.getX(), yc - s.getY())
237 if i == 0 or d < dmin:
238 source, dmin = s, d
239 xc, yc = source.getCentroid()
241 # residuals using spatial model
242 try:
243 subtractPsf(psf, im, xc, yc)
244 except Exception:
245 continue
247 resid = im
248 if variance:
249 resid = resid.getImage()
250 var = im.getVariance()
251 var = var.Factory(var, True)
252 numpy.sqrt(var.getArray(), var.getArray()) # inplace sqrt
253 resid /= var
255 im_resid.append(resid)
257 # Fit the PSF components directly to the data (i.e. ignoring the spatial model)
258 if fitBasisComponents:
259 im = cand.getMaskedImage()
261 im = im.Factory(im, True)
262 im.setXY0(cand.getMaskedImage().getXY0())
264 try:
265 noSpatialKernel = psf.getKernel()
266 except Exception:
267 noSpatialKernel = None
269 if noSpatialKernel:
270 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter())
271 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter)
272 params = fit[0]
273 kernels = afwMath.KernelList(fit[1])
274 outputKernel = afwMath.LinearCombinationKernel(kernels, params)
276 outImage = afwImage.ImageD(outputKernel.getDimensions())
277 outputKernel.computeImage(outImage, False)
279 im -= outImage.convertF()
280 resid = im
282 if margin > 0:
283 bim = im.Factory(w + 2*margin, h + 2*margin)
284 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
285 bim *= stdev
287 bim.assign(resid, bbox)
288 resid = bim
290 if variance:
291 resid = resid.getImage()
292 resid /= var
294 im_resid.append(resid)
296 im = im_resid.makeMosaic()
297 else:
298 im = cand.getMaskedImage()
300 if normalize:
301 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
303 objId = splitId(cand.getSource().getId(), True)["objId"]
304 if psf:
305 lab = "%d chi^2 %.1f" % (objId, rchi2)
306 ctype = afwDisplay.RED if cand.isBad() else afwDisplay.GREEN
307 else:
308 lab = "%d flux %8.3g" % (objId, cand.getSource().getPsfInstFlux())
309 ctype = afwDisplay.GREEN
311 mos.append(im, lab, ctype)
313 if False and numpy.isnan(rchi2):
314 display.mtv(cand.getMaskedImage().getImage(), title="showPsfCandidates: candidate")
315 print("amp", cand.getAmplitude())
317 im = cand.getMaskedImage()
318 center = (candidateIndex, xc - im.getX0(), yc - im.getY0())
319 candidateIndex += 1
320 if cand.isBad():
321 candidateCentersBad.append(center)
322 else:
323 candidateCenters.append(center)
325 if variance:
326 title = "chi(Psf fit)"
327 else:
328 title = "Stars & residuals"
329 mosaicImage = mos.makeMosaic(display=display, title=title)
331 with display.Buffering():
332 for centers, color in ((candidateCenters, afwDisplay.GREEN), (candidateCentersBad, afwDisplay.RED)):
333 for cen in centers:
334 bbox = mos.getBBox(cen[0])
335 display.dot("+", cen[1] + bbox.getMinX(), cen[2] + bbox.getMinY(), ctype=color)
337 return mosaicImage
340def makeSubplots(fig, nx=2, ny=2, Nx=1, Ny=1, plottingArea=(0.1, 0.1, 0.85, 0.80),
341 pxgutter=0.05, pygutter=0.05, xgutter=0.04, ygutter=0.04,
342 headroom=0.0, panelBorderWeight=0, panelColor='black'):
343 """Return a generator of a set of subplots, a set of Nx*Ny panels of nx*ny plots. Each panel is fully
344 filled by row (starting in the bottom left) before the next panel is started. If panelBorderWidth is
345 greater than zero a border is drawn around each panel, adjusted to enclose the axis labels.
347 E.g.
348 subplots = makeSubplots(fig, 2, 2, Nx=1, Ny=1, panelColor='k')
349 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,0)')
350 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,0)')
351 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,1)')
352 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,1)')
353 fig.show()
355 Parameters
356 ----------
357 fig : `matplotlib.pyplot.figure`
358 The matplotlib figure to draw
359 nx : `int`
360 The number of plots in each row of each panel
361 ny : `int`
362 The number of plots in each column of each panel
363 Nx : `int`
364 The number of panels in each row of the figure
365 Ny : `int`
366 The number of panels in each column of the figure
367 plottingArea : `tuple`
368 (x0, y0, x1, y1) for the part of the figure containing all the panels
369 pxgutter : `float`
370 Spacing between columns of panels in units of (x1 - x0)
371 pygutter : `float`
372 Spacing between rows of panels in units of (y1 - y0)
373 xgutter : `float`
374 Spacing between columns of plots within a panel in units of (x1 - x0)
375 ygutter : `float`
376 Spacing between rows of plots within a panel in units of (y1 - y0)
377 headroom : `float`
378 Extra spacing above each plot for e.g. a title
379 panelBorderWeight : `int`
380 Width of border drawn around panels
381 panelColor : `str`
382 Colour of border around panels
383 """
385 log = lsst.log.Log.getLogger("utils.makeSubplots")
386 try:
387 import matplotlib.pyplot as plt
388 except ImportError as e:
389 log.warning("Unable to import matplotlib: %s", e)
390 return
392 # Make show() call canvas.draw() too so that we know how large the axis labels are. Sigh
393 try:
394 fig.__show
395 except AttributeError:
396 fig.__show = fig.show
398 def myShow(fig):
399 fig.__show()
400 fig.canvas.draw()
402 import types
403 fig.show = types.MethodType(myShow, fig)
404 #
405 # We can't get the axis sizes until after draw()'s been called, so use a callback Sigh^2
406 #
407 axes = {} # all axes in all the panels we're drawing: axes[panel][0] etc.
408 #
410 def on_draw(event):
411 """
412 Callback to draw the panel borders when the plots are drawn to the canvas
413 """
414 if panelBorderWeight <= 0:
415 return False
417 for p in axes.keys():
418 bboxes = []
419 for ax in axes[p]:
420 bboxes.append(ax.bbox.union([label.get_window_extent() for label in
421 ax.get_xticklabels() + ax.get_yticklabels()]))
423 ax = axes[p][0]
425 # this is the bbox that bounds all the bboxes, again in relative
426 # figure coords
428 bbox = ax.bbox.union(bboxes)
430 xy0, xy1 = ax.transData.inverted().transform(bbox)
431 x0, y0 = xy0
432 x1, y1 = xy1
433 w, h = x1 - x0, y1 - y0
434 # allow a little space around BBox
435 x0 -= 0.02*w
436 w += 0.04*w
437 y0 -= 0.02*h
438 h += 0.04*h
439 h += h*headroom
440 # draw BBox
441 ax.patches = [] # remove old ones
442 rec = ax.add_patch(plt.Rectangle((x0, y0), w, h, fill=False,
443 lw=panelBorderWeight, edgecolor=panelColor))
444 rec.set_clip_on(False)
446 return False
448 fig.canvas.mpl_connect('draw_event', on_draw)
449 #
450 # Choose the plotting areas for each subplot
451 #
452 x0, y0 = plottingArea[0:2]
453 W, H = plottingArea[2:4]
454 w = (W - (Nx - 1)*pxgutter - (nx*Nx - 1)*xgutter)/float(nx*Nx)
455 h = (H - (Ny - 1)*pygutter - (ny*Ny - 1)*ygutter)/float(ny*Ny)
456 #
457 # OK! Time to create the subplots
458 #
459 for panel in range(Nx*Ny):
460 axes[panel] = []
461 px = panel%Nx
462 py = panel//Nx
463 for window in range(nx*ny):
464 x = nx*px + window%nx
465 y = ny*py + window//nx
466 ax = fig.add_axes((x0 + xgutter + pxgutter + x*w + (px - 1)*pxgutter + (x - 1)*xgutter,
467 y0 + ygutter + pygutter + y*h + (py - 1)*pygutter + (y - 1)*ygutter,
468 w, h), frame_on=True, facecolor='w')
469 axes[panel].append(ax)
470 yield ax
473def plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, numSample=128,
474 matchKernelAmplitudes=False, keepPlots=True):
475 """Plot the PSF spatial model."""
477 log = lsst.log.Log.getLogger("utils.plotPsfSpatialModel")
478 try:
479 import matplotlib.pyplot as plt
480 import matplotlib as mpl
481 except ImportError as e:
482 log.warning("Unable to import matplotlib: %s", e)
483 return
485 noSpatialKernel = psf.getKernel()
486 candPos = list()
487 candFits = list()
488 badPos = list()
489 badFits = list()
490 candAmps = list()
491 badAmps = list()
492 for cell in psfCellSet.getCellList():
493 for cand in cell.begin(False):
494 if not showBadCandidates and cand.isBad():
495 continue
496 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter())
497 try:
498 im = cand.getMaskedImage()
499 except Exception:
500 continue
502 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter)
503 params = fit[0]
504 kernels = fit[1]
505 amp = 0.0
506 for p, k in zip(params, kernels):
507 amp += p * k.getSum()
509 targetFits = badFits if cand.isBad() else candFits
510 targetPos = badPos if cand.isBad() else candPos
511 targetAmps = badAmps if cand.isBad() else candAmps
513 targetFits.append([x / amp for x in params])
514 targetPos.append(candCenter)
515 targetAmps.append(amp)
517 xGood = numpy.array([pos.getX() for pos in candPos]) - exposure.getX0()
518 yGood = numpy.array([pos.getY() for pos in candPos]) - exposure.getY0()
519 zGood = numpy.array(candFits)
521 xBad = numpy.array([pos.getX() for pos in badPos]) - exposure.getX0()
522 yBad = numpy.array([pos.getY() for pos in badPos]) - exposure.getY0()
523 zBad = numpy.array(badFits)
524 numBad = len(badPos)
526 xRange = numpy.linspace(0, exposure.getWidth(), num=numSample)
527 yRange = numpy.linspace(0, exposure.getHeight(), num=numSample)
529 kernel = psf.getKernel()
530 nKernelComponents = kernel.getNKernelParameters()
531 #
532 # Figure out how many panels we'll need
533 #
534 nPanelX = int(math.sqrt(nKernelComponents))
535 nPanelY = nKernelComponents//nPanelX
536 while nPanelY*nPanelX < nKernelComponents:
537 nPanelX += 1
539 fig = plt.figure(1)
540 fig.clf()
541 try:
542 fig.canvas._tkcanvas._root().lift() # == Tk's raise, but raise is a python reserved word
543 except Exception: # protect against API changes
544 pass
545 #
546 # Generator for axes arranged in panels
547 #
548 mpl.rcParams["figure.titlesize"] = "x-small"
549 subplots = makeSubplots(fig, 2, 2, Nx=nPanelX, Ny=nPanelY, xgutter=0.06, ygutter=0.06, pygutter=0.04)
551 for k in range(nKernelComponents):
552 func = kernel.getSpatialFunction(k)
553 dfGood = zGood[:, k] - numpy.array([func(pos.getX(), pos.getY()) for pos in candPos])
554 yMin = dfGood.min()
555 yMax = dfGood.max()
556 if numBad > 0:
557 dfBad = zBad[:, k] - numpy.array([func(pos.getX(), pos.getY()) for pos in badPos])
558 yMin = min([yMin, dfBad.min()])
559 yMax = max([yMax, dfBad.max()])
560 yMin -= 0.05 * (yMax - yMin)
561 yMax += 0.05 * (yMax - yMin)
563 yMin = -0.01
564 yMax = 0.01
566 fRange = numpy.ndarray((len(xRange), len(yRange)))
567 for j, yVal in enumerate(yRange):
568 for i, xVal in enumerate(xRange):
569 fRange[j][i] = func(xVal, yVal)
571 ax = next(subplots)
573 ax.set_autoscale_on(False)
574 ax.set_xbound(lower=0, upper=exposure.getHeight())
575 ax.set_ybound(lower=yMin, upper=yMax)
576 ax.plot(yGood, dfGood, 'b+')
577 if numBad > 0:
578 ax.plot(yBad, dfBad, 'r+')
579 ax.axhline(0.0)
580 ax.set_title('Residuals(y)')
582 ax = next(subplots)
584 if matchKernelAmplitudes and k == 0:
585 vmin = 0.0
586 vmax = 1.1
587 else:
588 vmin = fRange.min()
589 vmax = fRange.max()
591 norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
592 im = ax.imshow(fRange, aspect='auto', origin="lower", norm=norm,
593 extent=[0, exposure.getWidth()-1, 0, exposure.getHeight()-1])
594 ax.set_title('Spatial poly')
595 plt.colorbar(im, orientation='horizontal', ticks=[vmin, vmax])
597 ax = next(subplots)
598 ax.set_autoscale_on(False)
599 ax.set_xbound(lower=0, upper=exposure.getWidth())
600 ax.set_ybound(lower=yMin, upper=yMax)
601 ax.plot(xGood, dfGood, 'b+')
602 if numBad > 0:
603 ax.plot(xBad, dfBad, 'r+')
604 ax.axhline(0.0)
605 ax.set_title('K%d Residuals(x)' % k)
607 ax = next(subplots)
609 photoCalib = exposure.getPhotoCalib()
610 # If there is no calibration factor, use 1.0.
611 if photoCalib.getCalibrationMean() <= 0:
612 photoCalib = afwImage.PhotoCalib(1.0)
614 ampMag = [photoCalib.instFluxToMagnitude(candAmp) for candAmp in candAmps]
615 ax.plot(ampMag, zGood[:, k], 'b+')
616 if numBad > 0:
617 badAmpMag = [photoCalib.instFluxToMagnitude(badAmp) for badAmp in badAmps]
618 ax.plot(badAmpMag, zBad[:, k], 'r+')
620 ax.set_title('Flux variation')
622 fig.show()
624 global keptPlots
625 if keepPlots and not keptPlots:
626 # Keep plots open when done
627 def show():
628 print("%s: Please close plots when done." % __name__)
629 try:
630 plt.show()
631 except Exception:
632 pass
633 print("Plots closed, exiting...")
634 import atexit
635 atexit.register(show)
636 keptPlots = True
639def showPsf(psf, eigenValues=None, XY=None, normalize=True, display=None):
640 """Display a PSF's eigen images
642 If normalize is True, set the largest absolute value of each eigenimage to 1.0 (n.b. sum == 0.0 for i > 0)
643 """
645 if eigenValues:
646 coeffs = eigenValues
647 elif XY is not None:
648 coeffs = psf.getLocalKernel(lsst.geom.PointD(XY[0], XY[1])).getKernelParameters()
649 else:
650 coeffs = None
652 mos = displayUtils.Mosaic(gutter=2, background=-0.1)
653 for i, k in enumerate(psf.getKernel().getKernelList()):
654 im = afwImage.ImageD(k.getDimensions())
655 k.computeImage(im, False)
656 if normalize:
657 im /= numpy.max(numpy.abs(im.getArray()))
659 if coeffs:
660 mos.append(im, "%g" % (coeffs[i]/coeffs[0]))
661 else:
662 mos.append(im)
664 if not display:
665 display = afwDisplay.Display()
666 mos.makeMosaic(display=display, title="Kernel Basis Functions")
668 return mos
671def showPsfMosaic(exposure, psf=None, nx=7, ny=None, showCenter=True, showEllipticity=False,
672 showFwhm=False, stampSize=0, display=None, title=None):
673 """Show a mosaic of Psf images. exposure may be an Exposure (optionally with PSF),
674 or a tuple (width, height)
676 If stampSize is > 0, the psf images will be trimmed to stampSize*stampSize
677 """
679 scale = 1.0
680 if showFwhm:
681 showEllipticity = True
682 scale = 2*math.log(2) # convert sigma^2 to HWHM^2 for a Gaussian
684 mos = displayUtils.Mosaic()
686 try: # maybe it's a real Exposure
687 width, height = exposure.getWidth(), exposure.getHeight()
688 x0, y0 = exposure.getXY0()
689 if not psf:
690 psf = exposure.getPsf()
691 except AttributeError:
692 try: # OK, maybe a list [width, height]
693 width, height = exposure[0], exposure[1]
694 x0, y0 = 0, 0
695 except TypeError: # I guess not
696 raise RuntimeError("Unable to extract width/height from object of type %s" % type(exposure))
698 if not ny:
699 ny = int(nx*float(height)/width + 0.5)
700 if not ny:
701 ny = 1
703 centroidName = "SdssCentroid"
704 shapeName = "base_SdssShape"
706 schema = afwTable.SourceTable.makeMinimalSchema()
707 schema.getAliasMap().set("slot_Centroid", centroidName)
708 schema.getAliasMap().set("slot_Centroid_flag", centroidName+"_flag")
710 control = measBase.SdssCentroidControl()
711 centroider = measBase.SdssCentroidAlgorithm(control, centroidName, schema)
713 sdssShape = measBase.SdssShapeControl()
714 shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema)
715 table = afwTable.SourceTable.make(schema)
717 table.defineCentroid(centroidName)
718 table.defineShape(shapeName)
720 bbox = None
721 if stampSize > 0:
722 w, h = psf.computeImage(lsst.geom.PointD(0, 0)).getDimensions()
723 if stampSize <= w and stampSize <= h:
724 bbox = lsst.geom.BoxI(lsst.geom.PointI((w - stampSize)//2, (h - stampSize)//2),
725 lsst.geom.ExtentI(stampSize, stampSize))
727 centers = []
728 shapes = []
729 for iy in range(ny):
730 for ix in range(nx):
731 x = int(ix*(width-1)/(nx-1)) + x0
732 y = int(iy*(height-1)/(ny-1)) + y0
734 im = psf.computeImage(lsst.geom.PointD(x, y)).convertF()
735 imPeak = psf.computePeak(lsst.geom.PointD(x, y))
736 im /= imPeak
737 if bbox:
738 im = im.Factory(im, bbox)
739 lab = "PSF(%d,%d)" % (x, y) if False else ""
740 mos.append(im, lab)
742 exp = afwImage.makeExposure(afwImage.makeMaskedImage(im))
743 exp.setPsf(psf)
744 w, h = im.getWidth(), im.getHeight()
745 centerX = im.getX0() + w//2
746 centerY = im.getY0() + h//2
747 src = table.makeRecord()
748 spans = afwGeom.SpanSet(exp.getBBox())
749 foot = afwDet.Footprint(spans)
750 foot.addPeak(centerX, centerY, 1)
751 src.setFootprint(foot)
753 try:
754 centroider.measure(src, exp)
755 centers.append((src.getX() - im.getX0(), src.getY() - im.getY0()))
757 shaper.measure(src, exp)
758 shapes.append((src.getIxx(), src.getIxy(), src.getIyy()))
759 except Exception:
760 pass
762 if not display:
763 display = afwDisplay.Display()
764 mos.makeMosaic(display=display, title=title if title else "Model Psf", mode=nx)
766 if centers and display:
767 with display.Buffering():
768 for i, (cen, shape) in enumerate(zip(centers, shapes)):
769 bbox = mos.getBBox(i)
770 xc, yc = cen[0] + bbox.getMinX(), cen[1] + bbox.getMinY()
771 if showCenter:
772 display.dot("+", xc, yc, ctype=afwDisplay.BLUE)
774 if showEllipticity:
775 ixx, ixy, iyy = shape
776 ixx *= scale
777 ixy *= scale
778 iyy *= scale
779 display.dot("@:%g,%g,%g" % (ixx, ixy, iyy), xc, yc, ctype=afwDisplay.RED)
781 return mos
784def showPsfResiduals(exposure, sourceSet, magType="psf", scale=10, display=None):
785 mimIn = exposure.getMaskedImage()
786 mimIn = mimIn.Factory(mimIn, True) # make a copy to subtract from
788 psf = exposure.getPsf()
789 psfWidth, psfHeight = psf.getLocalKernel().getDimensions()
790 #
791 # Make the image that we'll paste our residuals into. N.b. they can overlap the edges
792 #
793 w, h = int(mimIn.getWidth()/scale), int(mimIn.getHeight()/scale)
795 im = mimIn.Factory(w + psfWidth, h + psfHeight)
797 cenPos = []
798 for s in sourceSet:
799 x, y = s.getX(), s.getY()
801 sx, sy = int(x/scale + 0.5), int(y/scale + 0.5)
803 smim = im.Factory(im, lsst.geom.BoxI(lsst.geom.PointI(sx, sy),
804 lsst.geom.ExtentI(psfWidth, psfHeight)))
805 sim = smim.getImage()
807 try:
808 if magType == "ap":
809 flux = s.getApInstFlux()
810 elif magType == "model":
811 flux = s.getModelInstFlux()
812 elif magType == "psf":
813 flux = s.getPsfInstFlux()
814 else:
815 raise RuntimeError("Unknown flux type %s" % magType)
817 subtractPsf(psf, mimIn, x, y, flux)
818 except Exception as e:
819 print(e)
821 try:
822 expIm = mimIn.getImage().Factory(mimIn.getImage(),
823 lsst.geom.BoxI(lsst.geom.PointI(int(x) - psfWidth//2,
824 int(y) - psfHeight//2),
825 lsst.geom.ExtentI(psfWidth, psfHeight)),
826 )
827 except pexExcept.Exception:
828 continue
830 cenPos.append([x - expIm.getX0() + sx, y - expIm.getY0() + sy])
832 sim += expIm
834 if display:
835 display = afwDisplay.Display()
836 display.mtv(im, title="showPsfResiduals: image")
837 with display.Buffering():
838 for x, y in cenPos:
839 display.dot("+", x, y)
841 return im
844def saveSpatialCellSet(psfCellSet, fileName="foo.fits", display=None):
845 """Write the contents of a SpatialCellSet to a many-MEF fits file"""
847 mode = "w"
848 for cell in psfCellSet.getCellList():
849 for cand in cell.begin(False): # include bad candidates
850 dx = afwImage.positionToIndex(cand.getXCenter(), True)[1]
851 dy = afwImage.positionToIndex(cand.getYCenter(), True)[1]
852 im = afwMath.offsetImage(cand.getMaskedImage(), -dx, -dy, "lanczos5")
854 md = dafBase.PropertySet()
855 md.set("CELL", cell.getLabel())
856 md.set("ID", cand.getId())
857 md.set("XCENTER", cand.getXCenter())
858 md.set("YCENTER", cand.getYCenter())
859 md.set("BAD", cand.isBad())
860 md.set("AMPL", cand.getAmplitude())
861 md.set("FLUX", cand.getSource().getPsfInstFlux())
862 md.set("CHI2", cand.getSource().getChi2())
864 im.writeFits(fileName, md, mode)
865 mode = "a"
867 if display:
868 display.mtv(im, title="saveSpatialCellSet: image")