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