LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
utils.py
Go to the documentation of this file.
1 # This file is part of afw.
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 # @file
23 # @brief Utilities to use with displaying images
24 
25 import lsst.geom
26 import lsst.afw.image as afwImage
27 
28 __all__ = (
29  "Mosaic",
30  "drawBBox", "drawFootprint", "drawCoaddInputs",
31 )
32 
33 
34 def _getDisplayFromDisplayOrFrame(display, frame=None):
35  """Return an `lsst.afw.display.Display` given either a display or a frame ID.
36 
37  Notes
38  -----
39  If the two arguments are consistent, return the desired display; if they are not,
40  raise a `RuntimeError` exception.
41 
42  If the desired display is `None`, return `None`;
43  if ``(display, frame) == ("deferToFrame", None)``, return the default display
44  """
45 
46  # import locally to allow this file to be imported by __init__
47  import lsst.afw.display as afwDisplay
48 
49  if display in ("deferToFrame", None):
50  if display is None and frame is None:
51  return None
52 
53  # "deferToFrame" is the default value, and means "obey frame"
54  display = None
55 
56  if display and not hasattr(display, "frame"):
57  raise RuntimeError(f"display == {display} doesn't support .frame")
58 
59  if frame and display and display.frame != frame:
60  raise RuntimeError("Please specify display *or* frame")
61 
62  if display:
63  frame = display.frame
64 
65  display = afwDisplay.getDisplay(frame, create=True)
66 
67  return display
68 
69 
70 class Mosaic:
71  """A class to handle mosaics of one or more identically-sized images
72  (or `~lsst.afw.image.Mask` or `~lsst.afw.image.MaskedImage`)
73 
74  Notes
75  -----
76  Note that this mosaic is a patchwork of the input images; if you want to
77  make a mosaic of a set images of the sky, you probably want to use the coadd code
78 
79  Examples
80  --------
81 
82  .. code-block:: py
83 
84  m = Mosaic()
85  m.setGutter(5)
86  m.setBackground(10)
87  m.setMode("square") # the default; other options are "x" or "y"
88 
89  mosaic = m.makeMosaic(im1, im2, im3) # build the mosaic
90  display = afwDisplay.getDisplay()
91  display.mtv(mosaic) # display it
92  m.drawLabels(["Label 1", "Label 2", "Label 3"], display) # label the panels
93 
94  # alternative way to build a mosaic
95  images = [im1, im2, im3]
96  labels = ["Label 1", "Label 2", "Label 3"]
97 
98  mosaic = m.makeMosaic(images)
99  display.mtv(mosaic)
100  m.drawLabels(labels, display)
101 
102  # Yet another way to build a mosaic (no need to build the images/labels lists)
103  for i in range(len(images)):
104  m.append(images[i], labels[i])
105  # You may optionally include a colour, e.g. afwDisplay.YELLOW, as a third argument
106 
107  mosaic = m.makeMosaic()
108  display.mtv(mosaic)
109  m.drawLabels(display=display)
110 
111  Or simply:
112 
113  .. code-block:: py
114 
115  mosaic = m.makeMosaic(display=display)
116 
117  You can return the (ix, iy)th (or nth) bounding box (in pixels) with `getBBox()`
118  """
119 
120  def __init__(self, gutter=3, background=0, mode="square"):
121  self.guttergutter = gutter # number of pixels between panels in a mosaic
122  self.backgroundbackground = background # value in gutters
123  self.setModesetMode(mode) # mosaicing mode
124  self.xsizexsize = 0 # column size of panels
125  self.ysizeysize = 0 # row size of panels
126 
127  self.resetreset()
128 
129  def reset(self):
130  """Reset the list of images to be mosaiced"""
131  self.imagesimages = [] # images to mosaic together
132  self.labelslabels = [] # labels for images
133 
134  def append(self, image, label=None, ctype=None):
135  """Add an image to the list of images to be mosaiced
136 
137  Returns
138  -------
139  index
140  the index of this image (may be passed to `getBBox()`)
141 
142  Notes
143  -----
144  Set may be cleared with ``Mosaic.reset()``
145  """
146  if not self.xsizexsize:
147  self.xsizexsize = image.getWidth()
148  self.ysizeysize = image.getHeight()
149 
150  self.imagesimages.append(image)
151  self.labelslabels.append((label, ctype))
152 
153  return len(self.imagesimages)
154 
155  def makeMosaic(self, images=None, display="deferToFrame", mode=None,
156  background=None, title=""):
157  """Return a mosaic of all the images provided.
158 
159  If none are specified, use the list accumulated with `Mosaic.append()`.
160 
161  If display is specified, display the mosaic
162  """
163 
164  if images:
165  if self.imagesimages:
166  raise RuntimeError(
167  f"You have already appended {len(self.images)} images to this Mosaic")
168 
169  try:
170  len(images) # check that it quacks like a list
171  except TypeError:
172  images = [images]
173 
174  self.imagesimages = images
175  else:
176  images = self.imagesimages
177 
178  if self.nImagenImagenImage == 0:
179  raise RuntimeError("You must provide at least one image")
180 
181  self.xsizexsize, self.ysizeysize = 0, 0
182  for im in images:
183  w, h = im.getWidth(), im.getHeight()
184  if w > self.xsizexsize:
185  self.xsizexsize = w
186  if h > self.ysizeysize:
187  self.ysizeysize = h
188 
189  if background is None:
190  background = self.backgroundbackground
191  if mode is None:
192  mode = self.modemode
193 
194  if mode == "square":
195  nx, ny = 1, self.nImagenImagenImage
196  while nx*im.getWidth() < ny*im.getHeight():
197  nx += 1
198  ny = self.nImagenImagenImage//nx
199 
200  if nx*ny < self.nImagenImagenImage:
201  ny += 1
202  if nx*ny < self.nImagenImagenImage:
203  nx += 1
204 
205  if nx > self.nImagenImagenImage:
206  nx = self.nImagenImagenImage
207 
208  assert(nx*ny >= self.nImagenImagenImage)
209  elif mode == "x":
210  nx, ny = self.nImagenImagenImage, 1
211  elif mode == "y":
212  nx, ny = 1, self.nImagenImagenImage
213  elif isinstance(mode, int):
214  nx = mode
215  ny = self.nImagenImagenImage//nx
216  if nx*ny < self.nImagenImagenImage:
217  ny += 1
218  else:
219  raise RuntimeError(f"Unknown mosaicing mode: {mode}")
220 
221  self.nx, self.nyny = nx, ny
222 
223  mosaic = images[0].Factory(
224  lsst.geom.Extent2I(nx*self.xsizexsize + (nx - 1)*self.guttergutter,
225  ny*self.ysizeysize + (ny - 1)*self.guttergutter)
226  )
227  try:
228  mosaic.set(self.backgroundbackground)
229  except AttributeError:
230  raise RuntimeError(f"Attempt to mosaic images of type {type(mosaic)} which don't support set")
231 
232  for i in range(len(images)):
233  smosaic = mosaic.Factory(
234  mosaic, self.getBBoxgetBBox(i%nx, i//nx), afwImage.LOCAL)
235  im = images[i]
236 
237  if smosaic.getDimensions() != im.getDimensions(): # im is smaller than smosaic
238  llc = lsst.geom.PointI((smosaic.getWidth() - im.getWidth())//2,
239  (smosaic.getHeight() - im.getHeight())//2)
240  smosaic = smosaic.Factory(smosaic, lsst.geom.Box2I(
241  llc, im.getDimensions()), afwImage.LOCAL)
242 
243  smosaic[:] = im
244 
245  display = _getDisplayFromDisplayOrFrame(display)
246  if display:
247  display.mtv(mosaic, title=title)
248 
249  if images == self.imagesimages:
250  self.drawLabelsdrawLabels(display=display)
251 
252  return mosaic
253 
254  def setGutter(self, gutter):
255  """Set the number of pixels between panels in a mosaic
256  """
257  self.guttergutter = gutter
258 
259  def setBackground(self, background):
260  """Set the value in the gutters
261  """
262  self.backgroundbackground = background
263 
264  def setMode(self, mode):
265  """Set mosaicing mode.
266 
267  Parameters
268  ----------
269  mode : {"square", "x", "y"}
270  Valid options:
271 
272  square
273  Make mosaic as square as possible
274  x
275  Make mosaic one image high
276  y
277  Make mosaic one image wide
278  """
279 
280  if mode not in ("square", "x", "y"):
281  raise RuntimeError(f"Unknown mosaicing mode: {mode}")
282 
283  self.modemode = mode
284 
285  def getBBox(self, ix, iy=None):
286  """Get the BBox for a panel
287 
288  Parameters
289  ----------
290  ix : `int`
291  If ``iy`` is not `None`, this is the x coordinate of the panel.
292  If ``iy`` is `None`, this is the number of the panel.
293  iy : `int`, optional
294  The y coordinate of the panel.
295  """
296 
297  if iy is None:
298  ix, iy = ix % self.nx, ix//self.nx
299 
300  return lsst.geom.Box2I(lsst.geom.PointI(ix*(self.xsizexsize + self.guttergutter), iy*(self.ysizeysize + self.guttergutter)),
301  lsst.geom.ExtentI(self.xsizexsize, self.ysizeysize))
302 
303  def drawLabels(self, labels=None, display="deferToFrame", frame=None):
304  """Draw the list labels at the corners of each panel.
305 
306  Notes
307  -----
308  If labels is None, use the ones specified by ``Mosaic.append()``
309  """
310 
311  if not labels:
312  labels = self.labelslabels
313 
314  if not labels:
315  return
316 
317  if len(labels) != self.nImagenImagenImage:
318  raise RuntimeError(f"You provided {len(labels)} labels for {self.nImage} panels")
319 
320  display = _getDisplayFromDisplayOrFrame(display, frame)
321  if not display:
322  return
323 
324  with display.Buffering():
325  for i in range(len(labels)):
326  if labels[i]:
327  label, ctype = labels[i], None
328  try:
329  label, ctype = label
330  except Exception:
331  pass
332 
333  if not label:
334  continue
335 
336  display.dot(str(label), self.getBBoxgetBBox(i).getMinX(),
337  self.getBBoxgetBBox(i).getMinY(), ctype=ctype)
338 
339  @property
340  def nImage(self):
341  """Number of images
342  """
343  return len(self.imagesimages)
344 
345 
346 def drawBBox(bbox, borderWidth=0.0, origin=None, display="deferToFrame", ctype=None, bin=1, frame=None):
347  """Draw a bounding box on a display frame with the specified ctype.
348 
349  Parameters
350  ----------
351  bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
352  The box to draw
353  borderWidth : `float`
354  Include this many pixels
355  origin
356  If specified, the box is shifted by ``origin``
357  display : `str`
358  ctype : `str`
359  The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11
360  bin : `int`
361  All BBox coordinates are divided by bin, as is right and proper for overlaying on a binned image
362  frame
363  """
364  x0, y0 = bbox.getMinX(), bbox.getMinY()
365  x1, y1 = bbox.getMaxX(), bbox.getMaxY()
366 
367  if origin:
368  x0 += origin[0]
369  x1 += origin[0]
370  y0 += origin[1]
371  y1 += origin[1]
372 
373  x0 /= bin
374  y0 /= bin
375  x1 /= bin
376  y1 /= bin
377  borderWidth /= bin
378 
379  display = _getDisplayFromDisplayOrFrame(display, frame)
380  display.line([(x0 - borderWidth, y0 - borderWidth),
381  (x0 - borderWidth, y1 + borderWidth),
382  (x1 + borderWidth, y1 + borderWidth),
383  (x1 + borderWidth, y0 - borderWidth),
384  (x0 - borderWidth, y0 - borderWidth),
385  ], ctype=ctype)
386 
387 
388 def drawFootprint(foot, borderWidth=0.5, origin=None, XY0=None, frame=None, ctype=None, bin=1,
389  peaks=False, symb="+", size=0.4, ctypePeak=None, display="deferToFrame"):
390  """Draw an `lsst.afw.detection.Footprint` on a display frame with the specified ctype.
391 
392  Parameters
393  ----------
394  foot : `lsst.afw.detection.Footprint`
395  borderWidth : `float`
396  Include an extra borderWidth pixels
397  origin
398  If ``origin`` is present, it's arithmetically added to the Footprint
399  XY0
400  if ``XY0`` is present is subtracted from the Footprint
401  frame
402  ctype : `str`
403  The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11
404  bin : `int`
405  All Footprint coordinates are divided by bin, as is right and proper
406  for overlaying on a binned image
407  peaks : `bool`
408  If peaks is `True`, also show the object's Peaks using the specified
409  ``symb`` and ``size`` and ``ctypePeak``
410  symb : `str`
411  size : `float`
412  ctypePeak : `str`
413  The desired color for peaks, either e.g. `lsst.afw.display.RED` or a color name known to X11
414  display : `str`
415  """
416 
417  if XY0:
418  if origin:
419  raise RuntimeError("You may not specify both origin and XY0")
420  origin = (-XY0[0], -XY0[1])
421 
422  display = _getDisplayFromDisplayOrFrame(display, frame)
423  with display.Buffering():
424  borderWidth /= bin
425  for s in foot.getSpans():
426  y, x0, x1 = s.getY(), s.getX0(), s.getX1()
427 
428  if origin:
429  x0 += origin[0]
430  x1 += origin[0]
431  y += origin[1]
432 
433  x0 /= bin
434  x1 /= bin
435  y /= bin
436 
437  display.line([(x0 - borderWidth, y - borderWidth),
438  (x0 - borderWidth, y + borderWidth),
439  (x1 + borderWidth, y + borderWidth),
440  (x1 + borderWidth, y - borderWidth),
441  (x0 - borderWidth, y - borderWidth),
442  ], ctype=ctype)
443 
444  if peaks:
445  for p in foot.getPeaks():
446  x, y = p.getIx(), p.getIy()
447 
448  if origin:
449  x += origin[0]
450  y += origin[1]
451 
452  x /= bin
453  y /= bin
454 
455  display.dot(symb, x, y, size=size, ctype=ctypePeak)
456 
457 
458 def drawCoaddInputs(exposure, frame=None, ctype=None, bin=1, display="deferToFrame"):
459  """Draw the bounding boxes of input exposures to a coadd on a display
460  frame with the specified ctype, assuming ``display.mtv()`` has already been
461  called on the given exposure on this frame.
462 
463  All coordinates are divided by ``bin``, as is right and proper for overlaying on a binned image
464  """
465  coaddWcs = exposure.getWcs()
466  catalog = exposure.getInfo().getCoaddInputs().ccds
467 
468  offset = lsst.geom.PointD() - lsst.geom.PointD(exposure.getXY0())
469 
470  display = _getDisplayFromDisplayOrFrame(display, frame)
471 
472  with display.Buffering():
473  for record in catalog:
474  ccdBox = lsst.geom.Box2D(record.getBBox())
475  ccdCorners = ccdBox.getCorners()
476  coaddCorners = [coaddWcs.skyToPixel(record.getWcs().pixelToSky(point)) + offset
477  for point in ccdCorners]
478  display.line([(coaddCorners[i].getX()/bin, coaddCorners[i].getY()/bin)
479  for i in range(-1, 4)], ctype=ctype)
def makeMosaic(self, images=None, display="deferToFrame", mode=None, background=None, title="")
Definition: utils.py:156
def setGutter(self, gutter)
Definition: utils.py:254
def setMode(self, mode)
Definition: utils.py:264
def getBBox(self, ix, iy=None)
Definition: utils.py:285
def append(self, image, label=None, ctype=None)
Definition: utils.py:134
def setBackground(self, background)
Definition: utils.py:259
def drawLabels(self, labels=None, display="deferToFrame", frame=None)
Definition: utils.py:303
def __init__(self, gutter=3, background=0, mode="square")
Definition: utils.py:120
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
An integer coordinate rectangle.
Definition: Box.h:55
def drawBBox(bbox, borderWidth=0.0, origin=None, display="deferToFrame", ctype=None, bin=1, frame=None)
Definition: utils.py:346
def drawFootprint(foot, borderWidth=0.5, origin=None, XY0=None, frame=None, ctype=None, bin=1, peaks=False, symb="+", size=0.4, ctypePeak=None, display="deferToFrame")
Definition: utils.py:389
def drawCoaddInputs(exposure, frame=None, ctype=None, bin=1, display="deferToFrame")
Definition: utils.py:458
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.