lsst.meas.algorithms g8d527e0710+5160d82a4f
utils.py
Go to the documentation of this file.
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/>.
21
22"""Support utilities for Measuring sources"""
23
24__all__ = ["splitId", "showSourceSet", "showPsfSpatialCells",
25 "showPsfCandidates", "makeSubplots", "plotPsfSpatialModel",
26 "showPsf", "showPsfMosaic", "showPsfResiduals", "saveSpatialCellSet"]
27
28import math
29import numpy
30
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
44
45keptPlots = False # Have we arranged to keep spatial plots open?
46
47afwDisplay.setDefaultMaskTransparency(75)
48
49
50def splitId(oid, asDict=True):
51
52 objId = int((oid & 0xffff) - 1) # Should be the value set by apps code
53
54 if asDict:
55 return dict(objId=objId)
56 else:
57 return [objId]
58
59
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"""
62
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]
68
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)
73
74#
75# PSF display utilities
76#
77
78
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.
82
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 """
87
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)
94
95 if nMaxPerCell < 0:
96 nMaxPerCell = 0
97
98 i = 0
99 goodies = ctypeBad is None
100 for cand in cell.begin(goodies):
101 if nMaxPerCell > 0:
102 i += 1
103
104 xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1]
105
106 if i > nMaxPerCell:
107 if not ctypeUnused:
108 continue
109
110 color = ctypeBad if cand.isBad() else ctype
111
112 if symb:
113 if i > nMaxPerCell:
114 ct = ctypeUnused
115 else:
116 ct = ctype
117
118 display.dot(symb, xc, yc, ctype=ct, size=size)
119
120 source = cand.getSource()
121
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)
128
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
133
134
135def showPsfCandidates(exposure, psfCellSet, psf=None, display=None, normalize=True, showBadCandidates=True,
136 fitBasisComponents=False, variance=None, chi=None):
137 """Display the PSF candidates.
138
139 If psf is provided include PSF model and residuals; if normalize is true normalize the PSFs
140 (and residuals)
141
142 If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi
143
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()
149
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
161
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
167
168 if not showBadCandidates and cand.isBad():
169 continue
170
171 if psf:
172 im_resid = displayUtils.Mosaic(gutter=0, background=-5, mode="x")
173
174 try:
175 im = cand.getMaskedImage() # copy of this object's image
176 xc, yc = cand.getXCenter(), cand.getYCenter()
177
178 margin = 0 if True else 5
179 w, h = im.getDimensions()
180 bbox = lsst.geom.BoxI(lsst.geom.PointI(margin, margin), im.getDimensions())
181
182 if margin > 0:
183 bim = im.Factory(w + 2*margin, h + 2*margin)
184
185 stdev = numpy.sqrt(afwMath.makeStatistics(im.getVariance(), afwMath.MEAN).getValue())
186 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
187 bim.getVariance().set(stdev**2)
188
189 bim.assign(im, bbox)
190 im = bim
191 xc += margin
192 yc += margin
193
194 im = im.Factory(im, True)
195 im.setXY0(cand.getMaskedImage().getXY0())
196 except Exception:
197 continue
198
199 if not variance:
200 im_resid.append(im.Factory(im, True))
201
202 if True: # tweak up centroids
203 mi = im
204 psfIm = mi.getImage()
205 config = measBase.SingleFrameMeasurementTask.ConfigClass()
206 config.slots.centroid = "base_SdssCentroid"
207
208 schema = afwTable.SourceTable.makeMinimalSchema()
209 measureSources = measBase.SingleFrameMeasurementTask(schema, config=config)
210 catalog = afwTable.SourceCatalog(schema)
211
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
218
219 exp = afwImage.makeExposure(mi)
220 exp.setPsf(psf)
221
222 footprintSet = afwDet.FootprintSet(mi,
223 afwDet.Threshold(0.5*numpy.max(psfIm.getArray())),
224 "DETECTED")
225 footprintSet.makeSources(catalog)
226
227 if len(catalog) == 0:
228 raise RuntimeError("Failed to detect any objects")
229
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()
240
241 # residuals using spatial model
242 try:
243 subtractPsf(psf, im, xc, yc)
244 except Exception:
245 continue
246
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
254
255 im_resid.append(resid)
256
257 # Fit the PSF components directly to the data (i.e. ignoring the spatial model)
258 if fitBasisComponents:
259 im = cand.getMaskedImage()
260
261 im = im.Factory(im, True)
262 im.setXY0(cand.getMaskedImage().getXY0())
263
264 try:
265 noSpatialKernel = psf.getKernel()
266 except Exception:
267 noSpatialKernel = None
268
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)
275
276 outImage = afwImage.ImageD(outputKernel.getDimensions())
277 outputKernel.computeImage(outImage, False)
278
279 im -= outImage.convertF()
280 resid = im
281
282 if margin > 0:
283 bim = im.Factory(w + 2*margin, h + 2*margin)
284 afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
285 bim *= stdev
286
287 bim.assign(resid, bbox)
288 resid = bim
289
290 if variance:
291 resid = resid.getImage()
292 resid /= var
293
294 im_resid.append(resid)
295
296 im = im_resid.makeMosaic()
297 else:
298 im = cand.getMaskedImage()
299
300 if normalize:
301 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
302
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
310
311 mos.append(im, lab, ctype)
312
313 if False and numpy.isnan(rchi2):
314 display.mtv(cand.getMaskedImage().getImage(), title="showPsfCandidates: candidate")
315 print("amp", cand.getAmplitude())
316
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)
324
325 if variance:
326 title = "chi(Psf fit)"
327 else:
328 title = "Stars & residuals"
329 mosaicImage = mos.makeMosaic(display=display, title=title)
330
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)
336
337 return mosaicImage
338
339
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.
346
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()
354
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 """
384
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
391
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
397
398 def myShow(fig):
399 fig.__show()
400 fig.canvas.draw()
401
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 #
409
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
416
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()]))
422
423 ax = axes[p][0]
424
425 # this is the bbox that bounds all the bboxes, again in relative
426 # figure coords
427
428 bbox = ax.bbox.union(bboxes)
429
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)
445
446 return False
447
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
471
472
473def plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, numSample=128,
474 matchKernelAmplitudes=False, keepPlots=True):
475 """Plot the PSF spatial model."""
476
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
484
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
501
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()
508
509 targetFits = badFits if cand.isBad() else candFits
510 targetPos = badPos if cand.isBad() else candPos
511 targetAmps = badAmps if cand.isBad() else candAmps
512
513 targetFits.append([x / amp for x in params])
514 targetPos.append(candCenter)
515 targetAmps.append(amp)
516
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)
520
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)
525
526 xRange = numpy.linspace(0, exposure.getWidth(), num=numSample)
527 yRange = numpy.linspace(0, exposure.getHeight(), num=numSample)
528
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
538
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)
550
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)
562
563 yMin = -0.01
564 yMax = 0.01
565
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)
570
571 ax = next(subplots)
572
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)')
581
582 ax = next(subplots)
583
584 if matchKernelAmplitudes and k == 0:
585 vmin = 0.0
586 vmax = 1.1
587 else:
588 vmin = fRange.min()
589 vmax = fRange.max()
590
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])
596
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)
606
607 ax = next(subplots)
608
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)
613
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+')
619
620 ax.set_title('Flux variation')
621
622 fig.show()
623
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
637
638
639def showPsf(psf, eigenValues=None, XY=None, normalize=True, display=None):
640 """Display a PSF's eigen images
641
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 """
644
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
651
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()))
658
659 if coeffs:
660 mos.append(im, "%g" % (coeffs[i]/coeffs[0]))
661 else:
662 mos.append(im)
663
664 if not display:
665 display = afwDisplay.Display()
666 mos.makeMosaic(display=display, title="Kernel Basis Functions")
667
668 return mos
669
670
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)
675
676 If stampSize is > 0, the psf images will be trimmed to stampSize*stampSize
677 """
678
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
683
684 mos = displayUtils.Mosaic()
685
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))
697
698 if not ny:
699 ny = int(nx*float(height)/width + 0.5)
700 if not ny:
701 ny = 1
702
703 centroidName = "SdssCentroid"
704 shapeName = "base_SdssShape"
705
706 schema = afwTable.SourceTable.makeMinimalSchema()
707 schema.getAliasMap().set("slot_Centroid", centroidName)
708 schema.getAliasMap().set("slot_Centroid_flag", centroidName+"_flag")
709
710 control = measBase.SdssCentroidControl()
711 centroider = measBase.SdssCentroidAlgorithm(control, centroidName, schema)
712
713 sdssShape = measBase.SdssShapeControl()
714 shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema)
715 table = afwTable.SourceTable.make(schema)
716
717 table.defineCentroid(centroidName)
718 table.defineShape(shapeName)
719
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))
726
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
733
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)
741
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)
752
753 try:
754 centroider.measure(src, exp)
755 centers.append((src.getX() - im.getX0(), src.getY() - im.getY0()))
756
757 shaper.measure(src, exp)
758 shapes.append((src.getIxx(), src.getIxy(), src.getIyy()))
759 except Exception:
760 pass
761
762 if not display:
763 display = afwDisplay.Display()
764 mos.makeMosaic(display=display, title=title if title else "Model Psf", mode=nx)
765
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)
773
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)
780
781 return mos
782
783
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
787
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)
794
795 im = mimIn.Factory(w + psfWidth, h + psfHeight)
796
797 cenPos = []
798 for s in sourceSet:
799 x, y = s.getX(), s.getY()
800
801 sx, sy = int(x/scale + 0.5), int(y/scale + 0.5)
802
803 smim = im.Factory(im, lsst.geom.BoxI(lsst.geom.PointI(sx, sy),
804 lsst.geom.ExtentI(psfWidth, psfHeight)))
805 sim = smim.getImage()
806
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)
816
817 subtractPsf(psf, mimIn, x, y, flux)
818 except Exception as e:
819 print(e)
820
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
829
830 cenPos.append([x - expIm.getX0() + sx, y - expIm.getY0() + sy])
831
832 sim += expIm
833
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)
840
841 return im
842
843
844def saveSpatialCellSet(psfCellSet, fileName="foo.fits", display=None):
845 """Write the contents of a SpatialCellSet to a many-MEF fits file"""
846
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")
853
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())
863
864 im.writeFits(fileName, md, mode)
865 mode = "a"
866
867 if display:
868 display.mtv(im, title="saveSpatialCellSet: image")
static Log getLogger(std::string const &loggername)
def makeSubplots(fig, nx=2, ny=2, Nx=1, Ny=1, plottingArea=(0.1, 0.1, 0.85, 0.80), pxgutter=0.05, pygutter=0.05, xgutter=0.04, ygutter=0.04, headroom=0.0, panelBorderWeight=0, panelColor='black')
Definition: utils.py:342
def showPsfSpatialCells(exposure, psfCellSet, nMaxPerCell=-1, showChi2=False, showMoments=False, symb=None, ctype=None, ctypeUnused=None, ctypeBad=None, size=2, display=None)
Definition: utils.py:80
def showPsf(psf, eigenValues=None, XY=None, normalize=True, display=None)
Definition: utils.py:639
def plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, numSample=128, matchKernelAmplitudes=False, keepPlots=True)
Definition: utils.py:474
def splitId(oid, asDict=True)
Definition: utils.py:50
def showPsfResiduals(exposure, sourceSet, magType="psf", scale=10, display=None)
Definition: utils.py:784
def saveSpatialCellSet(psfCellSet, fileName="foo.fits", display=None)
Definition: utils.py:844
def showSourceSet(sSet, xy0=(0, 0), display=None, ctype=afwDisplay.GREEN, symb="+", size=2)
Definition: utils.py:60
def showPsfCandidates(exposure, psfCellSet, psf=None, display=None, normalize=True, showBadCandidates=True, fitBasisComponents=False, variance=None, chi=None)
Definition: utils.py:136
def showPsfMosaic(exposure, psf=None, nx=7, ny=None, showCenter=True, showEllipticity=False, showFwhm=False, stampSize=0, display=None, title=None)
Definition: utils.py:672
std::pair< std::vector< double >, afw::math::KernelList > fitKernelParamsToImage(afw::math::LinearCombinationKernel const &kernel, Image const &image, geom::Point2D const &pos)
Fit a LinearCombinationKernel to an Image, allowing the coefficients of the components to vary.
double subtractPsf(afw::detection::Psf const &psf, ImageT *data, double x, double y, double psfFlux=std::numeric_limits< double >::quiet_NaN())