lsst.meas.algorithms  22.0.1-18-g3db9cf4b+5fd1cb9ea0
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 import math
25 import numpy
26 
27 import lsst.log
28 import lsst.pex.exceptions as pexExcept
29 import lsst.daf.base as dafBase
30 import lsst.geom
31 import lsst.afw.geom as afwGeom
32 import lsst.afw.detection as afwDet
33 import lsst.afw.image as afwImage
34 import lsst.afw.math as afwMath
35 import lsst.afw.table as afwTable
36 import lsst.afw.display as afwDisplay
37 import lsst.afw.display.utils as displayUtils
38 import lsst.meas.base as measBase
39 from . import subtractPsf, fitKernelParamsToImage
40 
41 keptPlots = False # Have we arranged to keep spatial plots open?
42 
43 afwDisplay.setDefaultMaskTransparency(75)
44 
45 
46 def splitId(oid, asDict=True):
47 
48  objId = int((oid & 0xffff) - 1) # Should be the value set by apps code
49 
50  if asDict:
51  return dict(objId=objId)
52  else:
53  return [objId]
54 
55 
56 def 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"""
58 
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]
64 
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)
69 
70 #
71 # PSF display utilities
72 #
73 
74 
75 def 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.
78 
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  """
83 
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)
90 
91  if nMaxPerCell < 0:
92  nMaxPerCell = 0
93 
94  i = 0
95  goodies = ctypeBad is None
96  for cand in cell.begin(goodies):
97  if nMaxPerCell > 0:
98  i += 1
99 
100  xc, yc = cand.getXCenter() + origin[0], cand.getYCenter() + origin[1]
101 
102  if i > nMaxPerCell:
103  if not ctypeUnused:
104  continue
105 
106  color = ctypeBad if cand.isBad() else ctype
107 
108  if symb:
109  if i > nMaxPerCell:
110  ct = ctypeUnused
111  else:
112  ct = ctype
113 
114  display.dot(symb, xc, yc, ctype=ct, size=size)
115 
116  source = cand.getSource()
117 
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)
124 
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
129 
130 
131 def showPsfCandidates(exposure, psfCellSet, psf=None, display=None, normalize=True, showBadCandidates=True,
132  fitBasisComponents=False, variance=None, chi=None):
133  """Display the PSF candidates.
134 
135  If psf is provided include PSF model and residuals; if normalize is true normalize the PSFs
136  (and residuals)
137 
138  If chi is True, generate a plot of residuals/sqrt(variance), i.e. chi
139 
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()
145 
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
157 
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
163 
164  if not showBadCandidates and cand.isBad():
165  continue
166 
167  if psf:
168  im_resid = displayUtils.Mosaic(gutter=0, background=-5, mode="x")
169 
170  try:
171  im = cand.getMaskedImage() # copy of this object's image
172  xc, yc = cand.getXCenter(), cand.getYCenter()
173 
174  margin = 0 if True else 5
175  w, h = im.getDimensions()
176  bbox = lsst.geom.BoxI(lsst.geom.PointI(margin, margin), im.getDimensions())
177 
178  if margin > 0:
179  bim = im.Factory(w + 2*margin, h + 2*margin)
180 
181  stdev = numpy.sqrt(afwMath.makeStatistics(im.getVariance(), afwMath.MEAN).getValue())
182  afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
183  bim.getVariance().set(stdev**2)
184 
185  bim.assign(im, bbox)
186  im = bim
187  xc += margin
188  yc += margin
189 
190  im = im.Factory(im, True)
191  im.setXY0(cand.getMaskedImage().getXY0())
192  except Exception:
193  continue
194 
195  if not variance:
196  im_resid.append(im.Factory(im, True))
197 
198  if True: # tweak up centroids
199  mi = im
200  psfIm = mi.getImage()
201  config = measBase.SingleFrameMeasurementTask.ConfigClass()
202  config.slots.centroid = "base_SdssCentroid"
203 
204  schema = afwTable.SourceTable.makeMinimalSchema()
205  measureSources = measBase.SingleFrameMeasurementTask(schema, config=config)
206  catalog = afwTable.SourceCatalog(schema)
207 
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
214 
215  exp = afwImage.makeExposure(mi)
216  exp.setPsf(psf)
217 
218  footprintSet = afwDet.FootprintSet(mi,
219  afwDet.Threshold(0.5*numpy.max(psfIm.getArray())),
220  "DETECTED")
221  footprintSet.makeSources(catalog)
222 
223  if len(catalog) == 0:
224  raise RuntimeError("Failed to detect any objects")
225 
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()
236 
237  # residuals using spatial model
238  try:
239  subtractPsf(psf, im, xc, yc)
240  except Exception:
241  continue
242 
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
250 
251  im_resid.append(resid)
252 
253  # Fit the PSF components directly to the data (i.e. ignoring the spatial model)
254  if fitBasisComponents:
255  im = cand.getMaskedImage()
256 
257  im = im.Factory(im, True)
258  im.setXY0(cand.getMaskedImage().getXY0())
259 
260  try:
261  noSpatialKernel = psf.getKernel()
262  except Exception:
263  noSpatialKernel = None
264 
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)
271 
272  outImage = afwImage.ImageD(outputKernel.getDimensions())
273  outputKernel.computeImage(outImage, False)
274 
275  im -= outImage.convertF()
276  resid = im
277 
278  if margin > 0:
279  bim = im.Factory(w + 2*margin, h + 2*margin)
280  afwMath.randomGaussianImage(bim.getImage(), afwMath.Random())
281  bim *= stdev
282 
283  bim.assign(resid, bbox)
284  resid = bim
285 
286  if variance:
287  resid = resid.getImage()
288  resid /= var
289 
290  im_resid.append(resid)
291 
292  im = im_resid.makeMosaic()
293  else:
294  im = cand.getMaskedImage()
295 
296  if normalize:
297  im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
298 
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
306 
307  mos.append(im, lab, ctype)
308 
309  if False and numpy.isnan(rchi2):
310  display.mtv(cand.getMaskedImage().getImage(), title="showPsfCandidates: candidate")
311  print("amp", cand.getAmplitude())
312 
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)
320 
321  if variance:
322  title = "chi(Psf fit)"
323  else:
324  title = "Stars & residuals"
325  mosaicImage = mos.makeMosaic(display=display, title=title)
326 
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)
332 
333  return mosaicImage
334 
335 
336 def 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.
342 
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()
350 
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  """
380 
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
387 
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
393 
394  def myShow(fig):
395  fig.__show()
396  fig.canvas.draw()
397 
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  #
405 
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
412 
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()]))
418 
419  ax = axes[p][0]
420 
421  # this is the bbox that bounds all the bboxes, again in relative
422  # figure coords
423 
424  bbox = ax.bbox.union(bboxes)
425 
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)
441 
442  return False
443 
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
467 
468 
469 def plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, numSample=128,
470  matchKernelAmplitudes=False, keepPlots=True):
471  """Plot the PSF spatial model."""
472 
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
480 
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
497 
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()
504 
505  targetFits = badFits if cand.isBad() else candFits
506  targetPos = badPos if cand.isBad() else candPos
507  targetAmps = badAmps if cand.isBad() else candAmps
508 
509  targetFits.append([x / amp for x in params])
510  targetPos.append(candCenter)
511  targetAmps.append(amp)
512 
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)
516 
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)
521 
522  xRange = numpy.linspace(0, exposure.getWidth(), num=numSample)
523  yRange = numpy.linspace(0, exposure.getHeight(), num=numSample)
524 
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
534 
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)
546 
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)
558 
559  yMin = -0.01
560  yMax = 0.01
561 
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)
566 
567  ax = next(subplots)
568 
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)')
577 
578  ax = next(subplots)
579 
580  if matchKernelAmplitudes and k == 0:
581  vmin = 0.0
582  vmax = 1.1
583  else:
584  vmin = fRange.min()
585  vmax = fRange.max()
586 
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])
592 
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)
602 
603  ax = next(subplots)
604 
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)
609 
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+')
615 
616  ax.set_title('Flux variation')
617 
618  fig.show()
619 
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
633 
634 
635 def showPsf(psf, eigenValues=None, XY=None, normalize=True, display=None):
636  """Display a PSF's eigen images
637 
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  """
640 
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
647 
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()))
654 
655  if coeffs:
656  mos.append(im, "%g" % (coeffs[i]/coeffs[0]))
657  else:
658  mos.append(im)
659 
660  if not display:
661  display = afwDisplay.Display()
662  mos.makeMosaic(display=display, title="Kernel Basis Functions")
663 
664  return mos
665 
666 
667 def 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)
671 
672  If stampSize is > 0, the psf images will be trimmed to stampSize*stampSize
673  """
674 
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
679 
680  mos = displayUtils.Mosaic()
681 
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))
693 
694  if not ny:
695  ny = int(nx*float(height)/width + 0.5)
696  if not ny:
697  ny = 1
698 
699  centroidName = "SdssCentroid"
700  shapeName = "base_SdssShape"
701 
702  schema = afwTable.SourceTable.makeMinimalSchema()
703  schema.getAliasMap().set("slot_Centroid", centroidName)
704  schema.getAliasMap().set("slot_Centroid_flag", centroidName+"_flag")
705 
706  control = measBase.SdssCentroidControl()
707  centroider = measBase.SdssCentroidAlgorithm(control, centroidName, schema)
708 
709  sdssShape = measBase.SdssShapeControl()
710  shaper = measBase.SdssShapeAlgorithm(sdssShape, shapeName, schema)
711  table = afwTable.SourceTable.make(schema)
712 
713  table.defineCentroid(centroidName)
714  table.defineShape(shapeName)
715 
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))
722 
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
729 
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)
737 
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)
748 
749  try:
750  centroider.measure(src, exp)
751  centers.append((src.getX() - im.getX0(), src.getY() - im.getY0()))
752 
753  shaper.measure(src, exp)
754  shapes.append((src.getIxx(), src.getIxy(), src.getIyy()))
755  except Exception:
756  pass
757 
758  if not display:
759  display = afwDisplay.Display()
760  mos.makeMosaic(display=display, title=title if title else "Model Psf", mode=nx)
761 
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)
769 
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)
776 
777  return mos
778 
779 
780 def 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
783 
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)
790 
791  im = mimIn.Factory(w + psfWidth, h + psfHeight)
792 
793  cenPos = []
794  for s in sourceSet:
795  x, y = s.getX(), s.getY()
796 
797  sx, sy = int(x/scale + 0.5), int(y/scale + 0.5)
798 
799  smim = im.Factory(im, lsst.geom.BoxI(lsst.geom.PointI(sx, sy),
800  lsst.geom.ExtentI(psfWidth, psfHeight)))
801  sim = smim.getImage()
802 
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)
812 
813  subtractPsf(psf, mimIn, x, y, flux)
814  except Exception as e:
815  print(e)
816 
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
825 
826  cenPos.append([x - expIm.getX0() + sx, y - expIm.getY0() + sy])
827 
828  sim += expIm
829 
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)
836 
837  return im
838 
839 
840 def saveSpatialCellSet(psfCellSet, fileName="foo.fits", display=None):
841  """Write the contents of a SpatialCellSet to a many-MEF fits file"""
842 
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")
849 
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())
859 
860  im.writeFits(fileName, md, mode)
861  mode = "a"
862 
863  if display:
864  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:338
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:76
def showPsf(psf, eigenValues=None, XY=None, normalize=True, display=None)
Definition: utils.py:635
def plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, numSample=128, matchKernelAmplitudes=False, keepPlots=True)
Definition: utils.py:470
def splitId(oid, asDict=True)
Definition: utils.py:46
def showPsfResiduals(exposure, sourceSet, magType="psf", scale=10, display=None)
Definition: utils.py:780
def saveSpatialCellSet(psfCellSet, fileName="foo.fits", display=None)
Definition: utils.py:840
def showSourceSet(sSet, xy0=(0, 0), display=None, ctype=afwDisplay.GREEN, symb="+", size=2)
Definition: utils.py:56
def showPsfCandidates(exposure, psfCellSet, psf=None, display=None, normalize=True, showBadCandidates=True, fitBasisComponents=False, variance=None, chi=None)
Definition: utils.py:132
def showPsfMosaic(exposure, psf=None, nx=7, ny=None, showCenter=True, showEllipticity=False, showFwhm=False, stampSize=0, display=None, title=None)
Definition: utils.py:668
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())