Coverage for python/lsst/meas/algorithms/utils.py : 4%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""
24import math
25import numpy
27import lsst.log
28import lsst.pex.exceptions as pexExcept
29import lsst.daf.base as dafBase
30import lsst.geom
31import lsst.afw.geom as afwGeom
32import lsst.afw.detection as afwDet
33import lsst.afw.image as afwImage
34import lsst.afw.math as afwMath
35import lsst.afw.table as afwTable
36import lsst.afw.display as afwDisplay
37import lsst.afw.display.utils as displayUtils
38import lsst.meas.base as measBase
39from . import subtractPsf, fitKernelParamsToImage
41keptPlots = False # Have we arranged to keep spatial plots open?
43afwDisplay.setDefaultMaskTransparency(75)
46def splitId(oid, asDict=True):
48 objId = int((oid & 0xffff) - 1) # Should be the value set by apps code
50 if asDict:
51 return dict(objId=objId)
52 else:
53 return [objId]
56def showSourceSet(sSet, xy0=(0, 0), display=None, ctype=afwDisplay.GREEN, symb="+", size=2):
57 """Draw the (XAstrom, YAstrom) positions of a set of Sources. Image has the given XY0"""
59 if not display:
60 display = afwDisplay.Display()
61 with display.Buffering():
62 for s in sSet:
63 xc, yc = s.getXAstrom() - xy0[0], s.getYAstrom() - xy0[1]
65 if symb == "id":
66 display.dot(str(splitId(s.getId(), True)["objId"]), xc, yc, ctype=ctype, size=size)
67 else:
68 display.dot(symb, xc, yc, ctype=ctype, size=size)
70#
71# PSF display utilities
72#
75def showPsfSpatialCells(exposure, psfCellSet, nMaxPerCell=-1, showChi2=False, showMoments=False,
76 symb=None, ctype=None, ctypeUnused=None, ctypeBad=None, size=2, display=None):
77 """Show the SpatialCells.
79 If symb is something that afwDisplay.Display.dot() understands (e.g. "o"),
80 the top nMaxPerCell candidates will be indicated with that symbol, using
81 ctype and size.
82 """
84 if not display:
85 display = afwDisplay.Display()
86 with display.Buffering():
87 origin = [-exposure.getMaskedImage().getX0(), -exposure.getMaskedImage().getY0()]
88 for cell in psfCellSet.getCellList():
89 displayUtils.drawBBox(cell.getBBox(), origin=origin, display=display)
91 if nMaxPerCell < 0:
92 nMaxPerCell = 0
94 i = 0
95 goodies = ctypeBad is None
96 for cand in cell.begin(goodies):
97 if nMaxPerCell > 0:
98 i += 1
100 xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1]
102 if i > nMaxPerCell:
103 if not ctypeUnused:
104 continue
106 color = ctypeBad if cand.isBad() else ctype
108 if symb:
109 if i > nMaxPerCell:
110 ct = ctypeUnused
111 else:
112 ct = ctype
114 display.dot(symb, xc, yc, ctype=ct, size=size)
116 source = cand.getSource()
118 if showChi2:
119 rchi2 = cand.getChi2()
120 if rchi2 > 1e100:
121 rchi2 = numpy.nan
122 display.dot("%d %.1f" % (splitId(source.getId(), True)["objId"], rchi2),
123 xc - size, yc - size - 4, ctype=color, size=2)
125 if showMoments:
126 display.dot("%.2f %.2f %.2f" % (source.getIxx(), source.getIxy(), source.getIyy()),
127 xc-size, yc + size + 4, ctype=color, size=size)
128 return display
131def showPsfCandidates(exposure, psfCellSet, psf=None, display=None, normalize=True, showBadCandidates=True,
132 fitBasisComponents=False, variance=None, chi=None):
133 """Display the PSF candidates.
135 If psf is provided include PSF model and residuals; if normalize is true normalize the PSFs
136 (and residuals)
138 If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi
140 If fitBasisComponents is true, also find the best linear combination of the PSF's components
141 (if they exist)
142 """
143 if not display:
144 display = afwDisplay.Display()
146 if chi is None:
147 if variance is not None: # old name for chi
148 chi = variance
149 #
150 # Show us the ccandidates
151 #
152 mos = displayUtils.Mosaic()
153 #
154 candidateCenters = []
155 candidateCentersBad = []
156 candidateIndex = 0
158 for cell in psfCellSet.getCellList():
159 for cand in cell.begin(False): # include bad candidates
160 rchi2 = cand.getChi2()
161 if rchi2 > 1e100:
162 rchi2 = numpy.nan
164 if not showBadCandidates and cand.isBad():
165 continue
167 if psf:
168 im_resid = displayUtils.Mosaic(gutter=0, background=-5, mode="x")
170 try:
171 im = cand.getMaskedImage() # copy of this object's image
172 xc, yc = cand.getXCenter(), cand.getYCenter()
174 margin = 0 if True else 5
175 w, h = im.getDimensions()
176 bbox = lsst.geom.BoxI(lsst.geom.PointI(margin, margin), im.getDimensions())
178 if margin > 0:
179 bim = im.Factory(w + 2*margin, h + 2*margin)
181 stdev = numpy.sqrt(afwMath.makeStatistics(im.getVariance(), afwMath.MEAN).getValue())
182 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
183 bim.getVariance().set(stdev**2)
185 bim.assign(im, bbox)
186 im = bim
187 xc += margin
188 yc += margin
190 im = im.Factory(im, True)
191 im.setXY0(cand.getMaskedImage().getXY0())
192 except Exception:
193 continue
195 if not variance:
196 im_resid.append(im.Factory(im, True))
198 if True: # tweak up centroids
199 mi = im
200 psfIm = mi.getImage()
201 config = measBase.SingleFrameMeasurementTask.ConfigClass()
202 config.slots.centroid = "base_SdssCentroid"
204 schema = afwTable.SourceTable.makeMinimalSchema()
205 measureSources = measBase.SingleFrameMeasurementTask(schema, config=config)
206 catalog = afwTable.SourceCatalog(schema)
208 extra = 10 # enough margin to run the sdss centroider
209 miBig = mi.Factory(im.getWidth() + 2*extra, im.getHeight() + 2*extra)
210 miBig[extra:-extra, extra:-extra, afwImage.LOCAL] = mi
211 miBig.setXY0(mi.getX0() - extra, mi.getY0() - extra)
212 mi = miBig
213 del miBig
215 exp = afwImage.makeExposure(mi)
216 exp.setPsf(psf)
218 footprintSet = afwDet.FootprintSet(mi,
219 afwDet.Threshold(0.5*numpy.max(psfIm.getArray())),
220 "DETECTED")
221 footprintSet.makeSources(catalog)
223 if len(catalog) == 0:
224 raise RuntimeError("Failed to detect any objects")
226 measureSources.run(catalog, exp)
227 if len(catalog) == 1:
228 source = catalog[0]
229 else: # more than one source; find the once closest to (xc, yc)
230 dmin = None # an invalid value to catch logic errors
231 for i, s in enumerate(catalog):
232 d = numpy.hypot(xc - s.getX(), yc - s.getY())
233 if i == 0 or d < dmin:
234 source, dmin = s, d
235 xc, yc = source.getCentroid()
237 # residuals using spatial model
238 try:
239 subtractPsf(psf, im, xc, yc)
240 except Exception:
241 continue
243 resid = im
244 if variance:
245 resid = resid.getImage()
246 var = im.getVariance()
247 var = var.Factory(var, True)
248 numpy.sqrt(var.getArray(), var.getArray()) # inplace sqrt
249 resid /= var
251 im_resid.append(resid)
253 # Fit the PSF components directly to the data (i.e. ignoring the spatial model)
254 if fitBasisComponents:
255 im = cand.getMaskedImage()
257 im = im.Factory(im, True)
258 im.setXY0(cand.getMaskedImage().getXY0())
260 try:
261 noSpatialKernel = psf.getKernel()
262 except Exception:
263 noSpatialKernel = None
265 if noSpatialKernel:
266 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter())
267 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter)
268 params = fit[0]
269 kernels = afwMath.KernelList(fit[1])
270 outputKernel = afwMath.LinearCombinationKernel(kernels, params)
272 outImage = afwImage.ImageD(outputKernel.getDimensions())
273 outputKernel.computeImage(outImage, False)
275 im -= outImage.convertF()
276 resid = im
278 if margin > 0:
279 bim = im.Factory(w + 2*margin, h + 2*margin)
280 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
281 bim *= stdev
283 bim.assign(resid, bbox)
284 resid = bim
286 if variance:
287 resid = resid.getImage()
288 resid /= var
290 im_resid.append(resid)
292 im = im_resid.makeMosaic()
293 else:
294 im = cand.getMaskedImage()
296 if normalize:
297 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
299 objId = splitId(cand.getSource().getId(), True)["objId"]
300 if psf:
301 lab = "%d chi^2 %.1f" % (objId, rchi2)
302 ctype = afwDisplay.RED if cand.isBad() else afwDisplay.GREEN
303 else:
304 lab = "%d flux %8.3g" % (objId, cand.getSource().getPsfInstFlux())
305 ctype = afwDisplay.GREEN
307 mos.append(im, lab, ctype)
309 if False and numpy.isnan(rchi2):
310 display.mtv(cand.getMaskedImage().getImage(), title="showPsfCandidates: candidate")
311 print("amp", cand.getAmplitude())
313 im = cand.getMaskedImage()
314 center = (candidateIndex, xc - im.getX0(), yc - im.getY0())
315 candidateIndex += 1
316 if cand.isBad():
317 candidateCentersBad.append(center)
318 else:
319 candidateCenters.append(center)
321 if variance:
322 title = "chi(Psf fit)"
323 else:
324 title = "Stars & residuals"
325 mosaicImage = mos.makeMosaic(display=display, title=title)
327 with display.Buffering():
328 for centers, color in ((candidateCenters, afwDisplay.GREEN), (candidateCentersBad, afwDisplay.RED)):
329 for cen in centers:
330 bbox = mos.getBBox(cen[0])
331 display.dot("+", cen[1] + bbox.getMinX(), cen[2] + bbox.getMinY(), ctype=color)
333 return mosaicImage
336def makeSubplots(fig, nx=2, ny=2, Nx=1, Ny=1, plottingArea=(0.1, 0.1, 0.85, 0.80),
337 pxgutter=0.05, pygutter=0.05, xgutter=0.04, ygutter=0.04,
338 headroom=0.0, panelBorderWeight=0, panelColor='black'):
339 """Return a generator of a set of subplots, a set of Nx*Ny panels of nx*ny plots. Each panel is fully
340 filled by row (starting in the bottom left) before the next panel is started. If panelBorderWidth is
341 greater than zero a border is drawn around each panel, adjusted to enclose the axis labels.
343 E.g.
344 subplots = makeSubplots(fig, 2, 2, Nx=1, Ny=1, panelColor='k')
345 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,0)')
346 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,0)')
347 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (0,1)')
348 ax = subplots.next(); ax.text(0.3, 0.5, '[0, 0] (1,1)')
349 fig.show()
351 Parameters
352 ----------
353 fig : `matplotlib.pyplot.figure`
354 The matplotlib figure to draw
355 nx : `int`
356 The number of plots in each row of each panel
357 ny : `int`
358 The number of plots in each column of each panel
359 Nx : `int`
360 The number of panels in each row of the figure
361 Ny : `int`
362 The number of panels in each column of the figure
363 plottingArea : `tuple`
364 (x0, y0, x1, y1) for the part of the figure containing all the panels
365 pxgutter : `float`
366 Spacing between columns of panels in units of (x1 - x0)
367 pygutter : `float`
368 Spacing between rows of panels in units of (y1 - y0)
369 xgutter : `float`
370 Spacing between columns of plots within a panel in units of (x1 - x0)
371 ygutter : `float`
372 Spacing between rows of plots within a panel in units of (y1 - y0)
373 headroom : `float`
374 Extra spacing above each plot for e.g. a title
375 panelBorderWeight : `int`
376 Width of border drawn around panels
377 panelColor : `str`
378 Colour of border around panels
379 """
381 log = lsst.log.Log.getLogger("utils.makeSubplots")
382 try:
383 import matplotlib.pyplot as plt
384 except ImportError as e:
385 log.warning("Unable to import matplotlib: %s", e)
386 return
388 # Make show() call canvas.draw() too so that we know how large the axis labels are. Sigh
389 try:
390 fig.__show
391 except AttributeError:
392 fig.__show = fig.show
394 def myShow(fig):
395 fig.__show()
396 fig.canvas.draw()
398 import types
399 fig.show = types.MethodType(myShow, fig)
400 #
401 # We can't get the axis sizes until after draw()'s been called, so use a callback Sigh^2
402 #
403 axes = {} # all axes in all the panels we're drawing: axes[panel][0] etc.
404 #
406 def on_draw(event):
407 """
408 Callback to draw the panel borders when the plots are drawn to the canvas
409 """
410 if panelBorderWeight <= 0:
411 return False
413 for p in axes.keys():
414 bboxes = []
415 for ax in axes[p]:
416 bboxes.append(ax.bbox.union([label.get_window_extent() for label in
417 ax.get_xticklabels() + ax.get_yticklabels()]))
419 ax = axes[p][0]
421 # this is the bbox that bounds all the bboxes, again in relative
422 # figure coords
424 bbox = ax.bbox.union(bboxes)
426 xy0, xy1 = ax.transData.inverted().transform(bbox)
427 x0, y0 = xy0
428 x1, y1 = xy1
429 w, h = x1 - x0, y1 - y0
430 # allow a little space around BBox
431 x0 -= 0.02*w
432 w += 0.04*w
433 y0 -= 0.02*h
434 h += 0.04*h
435 h += h*headroom
436 # draw BBox
437 ax.patches = [] # remove old ones
438 rec = ax.add_patch(plt.Rectangle((x0, y0), w, h, fill=False,
439 lw=panelBorderWeight, edgecolor=panelColor))
440 rec.set_clip_on(False)
442 return False
444 fig.canvas.mpl_connect('draw_event', on_draw)
445 #
446 # Choose the plotting areas for each subplot
447 #
448 x0, y0 = plottingArea[0:2]
449 W, H = plottingArea[2:4]
450 w = (W - (Nx - 1)*pxgutter - (nx*Nx - 1)*xgutter)/float(nx*Nx)
451 h = (H - (Ny - 1)*pygutter - (ny*Ny - 1)*ygutter)/float(ny*Ny)
452 #
453 # OK! Time to create the subplots
454 #
455 for panel in range(Nx*Ny):
456 axes[panel] = []
457 px = panel%Nx
458 py = panel//Nx
459 for window in range(nx*ny):
460 x = nx*px + window%nx
461 y = ny*py + window//nx
462 ax = fig.add_axes((x0 + xgutter + pxgutter + x*w + (px - 1)*pxgutter + (x - 1)*xgutter,
463 y0 + ygutter + pygutter + y*h + (py - 1)*pygutter + (y - 1)*ygutter,
464 w, h), frame_on=True, facecolor='w')
465 axes[panel].append(ax)
466 yield ax
469def plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, numSample=128,
470 matchKernelAmplitudes=False, keepPlots=True):
471 """Plot the PSF spatial model."""
473 log = lsst.log.Log.getLogger("utils.plotPsfSpatialModel")
474 try:
475 import matplotlib.pyplot as plt
476 import matplotlib as mpl
477 except ImportError as e:
478 log.warning("Unable to import matplotlib: %s", e)
479 return
481 noSpatialKernel = psf.getKernel()
482 candPos = list()
483 candFits = list()
484 badPos = list()
485 badFits = list()
486 candAmps = list()
487 badAmps = list()
488 for cell in psfCellSet.getCellList():
489 for cand in cell.begin(False):
490 if not showBadCandidates and cand.isBad():
491 continue
492 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter())
493 try:
494 im = cand.getMaskedImage()
495 except Exception:
496 continue
498 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter)
499 params = fit[0]
500 kernels = fit[1]
501 amp = 0.0
502 for p, k in zip(params, kernels):
503 amp += p * k.getSum()
505 targetFits = badFits if cand.isBad() else candFits
506 targetPos = badPos if cand.isBad() else candPos
507 targetAmps = badAmps if cand.isBad() else candAmps
509 targetFits.append([x / amp for x in params])
510 targetPos.append(candCenter)
511 targetAmps.append(amp)
513 xGood = numpy.array([pos.getX() for pos in candPos]) - exposure.getX0()
514 yGood = numpy.array([pos.getY() for pos in candPos]) - exposure.getY0()
515 zGood = numpy.array(candFits)
517 xBad = numpy.array([pos.getX() for pos in badPos]) - exposure.getX0()
518 yBad = numpy.array([pos.getY() for pos in badPos]) - exposure.getY0()
519 zBad = numpy.array(badFits)
520 numBad = len(badPos)
522 xRange = numpy.linspace(0, exposure.getWidth(), num=numSample)
523 yRange = numpy.linspace(0, exposure.getHeight(), num=numSample)
525 kernel = psf.getKernel()
526 nKernelComponents = kernel.getNKernelParameters()
527 #
528 # Figure out how many panels we'll need
529 #
530 nPanelX = int(math.sqrt(nKernelComponents))
531 nPanelY = nKernelComponents//nPanelX
532 while nPanelY*nPanelX < nKernelComponents:
533 nPanelX += 1
535 fig = plt.figure(1)
536 fig.clf()
537 try:
538 fig.canvas._tkcanvas._root().lift() # == Tk's raise, but raise is a python reserved word
539 except Exception: # protect against API changes
540 pass
541 #
542 # Generator for axes arranged in panels
543 #
544 mpl.rcParams["figure.titlesize"] = "x-small"
545 subplots = makeSubplots(fig, 2, 2, Nx=nPanelX, Ny=nPanelY, xgutter=0.06, ygutter=0.06, pygutter=0.04)
547 for k in range(nKernelComponents):
548 func = kernel.getSpatialFunction(k)
549 dfGood = zGood[:, k] - numpy.array([func(pos.getX(), pos.getY()) for pos in candPos])
550 yMin = dfGood.min()
551 yMax = dfGood.max()
552 if numBad > 0:
553 dfBad = zBad[:, k] - numpy.array([func(pos.getX(), pos.getY()) for pos in badPos])
554 yMin = min([yMin, dfBad.min()])
555 yMax = max([yMax, dfBad.max()])
556 yMin -= 0.05 * (yMax - yMin)
557 yMax += 0.05 * (yMax - yMin)
559 yMin = -0.01
560 yMax = 0.01
562 fRange = numpy.ndarray((len(xRange), len(yRange)))
563 for j, yVal in enumerate(yRange):
564 for i, xVal in enumerate(xRange):
565 fRange[j][i] = func(xVal, yVal)
567 ax = next(subplots)
569 ax.set_autoscale_on(False)
570 ax.set_xbound(lower=0, upper=exposure.getHeight())
571 ax.set_ybound(lower=yMin, upper=yMax)
572 ax.plot(yGood, dfGood, 'b+')
573 if numBad > 0:
574 ax.plot(yBad, dfBad, 'r+')
575 ax.axhline(0.0)
576 ax.set_title('Residuals(y)')
578 ax = next(subplots)
580 if matchKernelAmplitudes and k == 0:
581 vmin = 0.0
582 vmax = 1.1
583 else:
584 vmin = fRange.min()
585 vmax = fRange.max()
587 norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
588 im = ax.imshow(fRange, aspect='auto', origin="lower", norm=norm,
589 extent=[0, exposure.getWidth()-1, 0, exposure.getHeight()-1])
590 ax.set_title('Spatial poly')
591 plt.colorbar(im, orientation='horizontal', ticks=[vmin, vmax])
593 ax = next(subplots)
594 ax.set_autoscale_on(False)
595 ax.set_xbound(lower=0, upper=exposure.getWidth())
596 ax.set_ybound(lower=yMin, upper=yMax)
597 ax.plot(xGood, dfGood, 'b+')
598 if numBad > 0:
599 ax.plot(xBad, dfBad, 'r+')
600 ax.axhline(0.0)
601 ax.set_title('K%d Residuals(x)' % k)
603 ax = next(subplots)
605 photoCalib = exposure.getPhotoCalib()
606 # If there is no calibration factor, use 1.0.
607 if photoCalib.getCalibrationMean() <= 0:
608 photoCalib = afwImage.PhotoCalib(1.0)
610 ampMag = [photoCalib.instFluxToMagnitude(candAmp) for candAmp in candAmps]
611 ax.plot(ampMag, zGood[:, k], 'b+')
612 if numBad > 0:
613 badAmpMag = [photoCalib.instFluxToMagnitude(badAmp) for badAmp in badAmps]
614 ax.plot(badAmpMag, zBad[:, k], 'r+')
616 ax.set_title('Flux variation')
618 fig.show()
620 global keptPlots
621 if keepPlots and not keptPlots:
622 # Keep plots open when done
623 def show():
624 print("%s: Please close plots when done." % __name__)
625 try:
626 plt.show()
627 except Exception:
628 pass
629 print("Plots closed, exiting...")
630 import atexit
631 atexit.register(show)
632 keptPlots = True
635def showPsf(psf, eigenValues=None, XY=None, normalize=True, display=None):
636 """Display a PSF's eigen images
638 If normalize is True, set the largest absolute value of each eigenimage to 1.0 (n.b. sum == 0.0 for i > 0)
639 """
641 if eigenValues:
642 coeffs = eigenValues
643 elif XY is not None:
644 coeffs = psf.getLocalKernel(lsst.geom.PointD(XY[0], XY[1])).getKernelParameters()
645 else:
646 coeffs = None
648 mos = displayUtils.Mosaic(gutter=2, background=-0.1)
649 for i, k in enumerate(psf.getKernel().getKernelList()):
650 im = afwImage.ImageD(k.getDimensions())
651 k.computeImage(im, False)
652 if normalize:
653 im /= numpy.max(numpy.abs(im.getArray()))
655 if coeffs:
656 mos.append(im, "%g" % (coeffs[i]/coeffs[0]))
657 else:
658 mos.append(im)
660 if not display:
661 display = afwDisplay.Display()
662 mos.makeMosaic(display=display, title="Kernel Basis Functions")
664 return mos
667def showPsfMosaic(exposure, psf=None, nx=7, ny=None, showCenter=True, showEllipticity=False,
668 showFwhm=False, stampSize=0, display=None, title=None):
669 """Show a mosaic of Psf images. exposure may be an Exposure (optionally with PSF),
670 or a tuple (width, height)
672 If stampSize is > 0, the psf images will be trimmed to stampSize*stampSize
673 """
675 scale = 1.0
676 if showFwhm:
677 showEllipticity = True
678 scale = 2*math.log(2) # convert sigma^2 to HWHM^2 for a Gaussian
680 mos = displayUtils.Mosaic()
682 try: # maybe it's a real Exposure
683 width, height = exposure.getWidth(), exposure.getHeight()
684 x0, y0 = exposure.getXY0()
685 if not psf:
686 psf = exposure.getPsf()
687 except AttributeError:
688 try: # OK, maybe a list [width, height]
689 width, height = exposure[0], exposure[1]
690 x0, y0 = 0, 0
691 except TypeError: # I guess not
692 raise RuntimeError("Unable to extract width/height from object of type %s" % type(exposure))
694 if not ny:
695 ny = int(nx*float(height)/width + 0.5)
696 if not ny:
697 ny = 1
699 centroidName = "SdssCentroid"
700 shapeName = "base_SdssShape"
702 schema = afwTable.SourceTable.makeMinimalSchema()
703 schema.getAliasMap().set("slot_Centroid", centroidName)
704 schema.getAliasMap().set("slot_Centroid_flag", centroidName+"_flag")
706 control = measBase.SdssCentroidControl()
707 centroider = measBase.SdssCentroidAlgorithm(control, centroidName, schema)
709 sdssShape = measBase.SdssShapeControl()
710 shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema)
711 table = afwTable.SourceTable.make(schema)
713 table.defineCentroid(centroidName)
714 table.defineShape(shapeName)
716 bbox = None
717 if stampSize > 0:
718 w, h = psf.computeImage(lsst.geom.PointD(0, 0)).getDimensions()
719 if stampSize <= w and stampSize <= h:
720 bbox = lsst.geom.BoxI(lsst.geom.PointI((w - stampSize)//2, (h - stampSize)//2),
721 lsst.geom.ExtentI(stampSize, stampSize))
723 centers = []
724 shapes = []
725 for iy in range(ny):
726 for ix in range(nx):
727 x = int(ix*(width-1)/(nx-1)) + x0
728 y = int(iy*(height-1)/(ny-1)) + y0
730 im = psf.computeImage(lsst.geom.PointD(x, y)).convertF()
731 imPeak = psf.computePeak(lsst.geom.PointD(x, y))
732 im /= imPeak
733 if bbox:
734 im = im.Factory(im, bbox)
735 lab = "PSF(%d,%d)" % (x, y) if False else ""
736 mos.append(im, lab)
738 exp = afwImage.makeExposure(afwImage.makeMaskedImage(im))
739 exp.setPsf(psf)
740 w, h = im.getWidth(), im.getHeight()
741 centerX = im.getX0() + w//2
742 centerY = im.getY0() + h//2
743 src = table.makeRecord()
744 spans = afwGeom.SpanSet(exp.getBBox())
745 foot = afwDet.Footprint(spans)
746 foot.addPeak(centerX, centerY, 1)
747 src.setFootprint(foot)
749 try:
750 centroider.measure(src, exp)
751 centers.append((src.getX() - im.getX0(), src.getY() - im.getY0()))
753 shaper.measure(src, exp)
754 shapes.append((src.getIxx(), src.getIxy(), src.getIyy()))
755 except Exception:
756 pass
758 if not display:
759 display = afwDisplay.Display()
760 mos.makeMosaic(display=display, title=title if title else "Model Psf", mode=nx)
762 if centers and display:
763 with display.Buffering():
764 for i, (cen, shape) in enumerate(zip(centers, shapes)):
765 bbox = mos.getBBox(i)
766 xc, yc = cen[0] + bbox.getMinX(), cen[1] + bbox.getMinY()
767 if showCenter:
768 display.dot("+", xc, yc, ctype=afwDisplay.BLUE)
770 if showEllipticity:
771 ixx, ixy, iyy = shape
772 ixx *= scale
773 ixy *= scale
774 iyy *= scale
775 display.dot("@:%g,%g,%g" % (ixx, ixy, iyy), xc, yc, ctype=afwDisplay.RED)
777 return mos
780def showPsfResiduals(exposure, sourceSet, magType="psf", scale=10, display=None):
781 mimIn = exposure.getMaskedImage()
782 mimIn = mimIn.Factory(mimIn, True) # make a copy to subtract from
784 psf = exposure.getPsf()
785 psfWidth, psfHeight = psf.getLocalKernel().getDimensions()
786 #
787 # Make the image that we'll paste our residuals into. N.b. they can overlap the edges
788 #
789 w, h = int(mimIn.getWidth()/scale), int(mimIn.getHeight()/scale)
791 im = mimIn.Factory(w + psfWidth, h + psfHeight)
793 cenPos = []
794 for s in sourceSet:
795 x, y = s.getX(), s.getY()
797 sx, sy = int(x/scale + 0.5), int(y/scale + 0.5)
799 smim = im.Factory(im, lsst.geom.BoxI(lsst.geom.PointI(sx, sy),
800 lsst.geom.ExtentI(psfWidth, psfHeight)))
801 sim = smim.getImage()
803 try:
804 if magType == "ap":
805 flux = s.getApInstFlux()
806 elif magType == "model":
807 flux = s.getModelInstFlux()
808 elif magType == "psf":
809 flux = s.getPsfInstFlux()
810 else:
811 raise RuntimeError("Unknown flux type %s" % magType)
813 subtractPsf(psf, mimIn, x, y, flux)
814 except Exception as e:
815 print(e)
817 try:
818 expIm = mimIn.getImage().Factory(mimIn.getImage(),
819 lsst.geom.BoxI(lsst.geom.PointI(int(x) - psfWidth//2,
820 int(y) - psfHeight//2),
821 lsst.geom.ExtentI(psfWidth, psfHeight)),
822 )
823 except pexExcept.Exception:
824 continue
826 cenPos.append([x - expIm.getX0() + sx, y - expIm.getY0() + sy])
828 sim += expIm
830 if display:
831 display = afwDisplay.Display()
832 display.mtv(im, title="showPsfResiduals: image")
833 with display.Buffering():
834 for x, y in cenPos:
835 display.dot("+", x, y)
837 return im
840def saveSpatialCellSet(psfCellSet, fileName="foo.fits", display=None):
841 """Write the contents of a SpatialCellSet to a many-MEF fits file"""
843 mode = "w"
844 for cell in psfCellSet.getCellList():
845 for cand in cell.begin(False): # include bad candidates
846 dx = afwImage.positionToIndex(cand.getXCenter(), True)[1]
847 dy = afwImage.positionToIndex(cand.getYCenter(), True)[1]
848 im = afwMath.offsetImage(cand.getMaskedImage(), -dx, -dy, "lanczos5")
850 md = dafBase.PropertySet()
851 md.set("CELL", cell.getLabel())
852 md.set("ID", cand.getId())
853 md.set("XCENTER", cand.getXCenter())
854 md.set("YCENTER", cand.getYCenter())
855 md.set("BAD", cand.isBad())
856 md.set("AMPL", cand.getAmplitude())
857 md.set("FLUX", cand.getSource().getPsfInstFlux())
858 md.set("CHI2", cand.getSource().getChi2())
860 im.writeFits(fileName, md, mode)
861 mode = "a"
863 if display:
864 display.mtv(im, title="saveSpatialCellSet: image")