Coverage for python/lsst/afw/display/utils.py : 7%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2008, 2009, 2010 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
23# @file
24# @brief Utilities to use with displaying images
26import lsst.geom
27import lsst.afw.image as afwImage
29__all__ = (
30 "Mosaic",
31 "drawBBox", "drawFootprint", "drawCoaddInputs",
32)
35def _getDisplayFromDisplayOrFrame(display, frame=None):
36 """Return an `lsst.afw.display.Display` given either a display or a frame ID.
38 Notes
39 -----
40 If the two arguments are consistent, return the desired display; if they are not,
41 raise a `RuntimeError` exception.
43 If the desired display is `None`, return `None`;
44 if ``(display, frame) == ("deferToFrame", None)``, return the default display
45 """
47 # import locally to allow this file to be imported by __init__
48 import lsst.afw.display as afwDisplay
50 if display in ("deferToFrame", None):
51 if display is None and frame is None:
52 return None
54 # "deferToFrame" is the default value, and means "obey frame"
55 display = None
57 if display and not hasattr(display, "frame"):
58 raise RuntimeError(f"display == {display} doesn't support .frame")
60 if frame and display and display.frame != frame:
61 raise RuntimeError("Please specify display *or* frame")
63 if display:
64 frame = display.frame
66 display = afwDisplay.getDisplay(frame, create=True)
68 return display
71class Mosaic:
72 """A class to handle mosaics of one or more identically-sized images
73 (or `~lsst.afw.image.Mask` or `~lsst.afw.image.MaskedImage`)
75 Notes
76 -----
77 Note that this mosaic is a patchwork of the input images; if you want to
78 make a mosaic of a set images of the sky, you probably want to use the coadd code
80 Examples
81 --------
83 .. code-block:: py
85 m = Mosaic()
86 m.setGutter(5)
87 m.setBackground(10)
88 m.setMode("square") # the default; other options are "x" or "y"
90 mosaic = m.makeMosaic(im1, im2, im3) # build the mosaic
91 display = afwDisplay.getDisplay()
92 display.mtv(mosaic) # display it
93 m.drawLabels(["Label 1", "Label 2", "Label 3"], display) # label the panels
95 # alternative way to build a mosaic
96 images = [im1, im2, im3]
97 labels = ["Label 1", "Label 2", "Label 3"]
99 mosaic = m.makeMosaic(images)
100 display.mtv(mosaic)
101 m.drawLabels(labels, display)
103 # Yet another way to build a mosaic (no need to build the images/labels lists)
104 for i in range(len(images)):
105 m.append(images[i], labels[i])
106 # You may optionally include a colour, e.g. afwDisplay.YELLOW, as a third argument
108 mosaic = m.makeMosaic()
109 display.mtv(mosaic)
110 m.drawLabels(display=display)
112 Or simply:
114 .. code-block:: py
116 mosaic = m.makeMosaic(display=display)
118 You can return the (ix, iy)th (or nth) bounding box (in pixels) with `getBBox()`
119 """
121 def __init__(self, gutter=3, background=0, mode="square"):
122 self.gutter = gutter # number of pixels between panels in a mosaic
123 self.background = background # value in gutters
124 self.setMode(mode) # mosaicing mode
125 self.xsize = 0 # column size of panels
126 self.ysize = 0 # row size of panels
128 self.reset()
130 def reset(self):
131 """Reset the list of images to be mosaiced"""
132 self.images = [] # images to mosaic together
133 self.labels = [] # labels for images
135 def append(self, image, label=None, ctype=None):
136 """Add an image to the list of images to be mosaiced
138 Returns
139 -------
140 index
141 the index of this image (may be passed to `getBBox()`)
143 Notes
144 -----
145 Set may be cleared with ``Mosaic.reset()``
146 """
147 if not self.xsize:
148 self.xsize = image.getWidth()
149 self.ysize = image.getHeight()
151 self.images.append(image)
152 self.labels.append((label, ctype))
154 return len(self.images)
156 def makeMosaic(self, images=None, display="deferToFrame", mode=None,
157 background=None, title=""):
158 """Return a mosaic of all the images provided.
160 If none are specified, use the list accumulated with `Mosaic.append()`.
162 If display is specified, display the mosaic
163 """
165 if images:
166 if self.images:
167 raise RuntimeError(
168 f"You have already appended {len(self.images)} images to this Mosaic")
170 try:
171 len(images) # check that it quacks like a list
172 except TypeError:
173 images = [images]
175 self.images = images
176 else:
177 images = self.images
179 if self.nImage == 0:
180 raise RuntimeError("You must provide at least one image")
182 self.xsize, self.ysize = 0, 0
183 for im in images:
184 w, h = im.getWidth(), im.getHeight()
185 if w > self.xsize:
186 self.xsize = w
187 if h > self.ysize:
188 self.ysize = h
190 if background is None:
191 background = self.background
192 if mode is None:
193 mode = self.mode
195 if mode == "square":
196 nx, ny = 1, self.nImage
197 while nx*im.getWidth() < ny*im.getHeight():
198 nx += 1
199 ny = self.nImage//nx
201 if nx*ny < self.nImage:
202 ny += 1
203 if nx*ny < self.nImage:
204 nx += 1
206 if nx > self.nImage:
207 nx = self.nImage
209 assert(nx*ny >= self.nImage)
210 elif mode == "x":
211 nx, ny = self.nImage, 1
212 elif mode == "y":
213 nx, ny = 1, self.nImage
214 elif isinstance(mode, int):
215 nx = mode
216 ny = self.nImage//nx
217 if nx*ny < self.nImage:
218 ny += 1
219 else:
220 raise RuntimeError(f"Unknown mosaicing mode: {mode}")
222 self.nx, self.ny = nx, ny
224 mosaic = images[0].Factory(
225 lsst.geom.Extent2I(nx*self.xsize + (nx - 1)*self.gutter,
226 ny*self.ysize + (ny - 1)*self.gutter)
227 )
228 try:
229 mosaic.set(self.background)
230 except AttributeError:
231 raise RuntimeError(f"Attempt to mosaic images of type {type(mosaic)} which don't support set")
233 for i in range(len(images)):
234 smosaic = mosaic.Factory(
235 mosaic, self.getBBox(i%nx, i//nx), afwImage.LOCAL)
236 im = images[i]
238 if smosaic.getDimensions() != im.getDimensions(): # im is smaller than smosaic
239 llc = lsst.geom.PointI((smosaic.getWidth() - im.getWidth())//2,
240 (smosaic.getHeight() - im.getHeight())//2)
241 smosaic = smosaic.Factory(smosaic, lsst.geom.Box2I(
242 llc, im.getDimensions()), afwImage.LOCAL)
244 smosaic[:] = im
246 display = _getDisplayFromDisplayOrFrame(display)
247 if display:
248 display.mtv(mosaic, title=title)
250 if images == self.images:
251 self.drawLabels(display=display)
253 return mosaic
255 def setGutter(self, gutter):
256 """Set the number of pixels between panels in a mosaic
257 """
258 self.gutter = gutter
260 def setBackground(self, background):
261 """Set the value in the gutters
262 """
263 self.background = background
265 def setMode(self, mode):
266 """Set mosaicing mode.
268 Parameters
269 ----------
270 mode : {"square", "x", "y"}
271 Valid options:
273 square
274 Make mosaic as square as possible
275 x
276 Make mosaic one image high
277 y
278 Make mosaic one image wide
279 """
281 if mode not in ("square", "x", "y"):
282 raise RuntimeError(f"Unknown mosaicing mode: {mode}")
284 self.mode = mode
286 def getBBox(self, ix, iy=None):
287 """Get the BBox for a panel
289 Parameters
290 ----------
291 ix : `int`
292 If ``iy`` is not `None`, this is the x coordinate of the panel.
293 If ``iy`` is `None`, this is the number of the panel.
294 iy : `int`, optional
295 The y coordinate of the panel.
296 """
298 if iy is None:
299 ix, iy = ix % self.nx, ix//self.nx
301 return lsst.geom.Box2I(lsst.geom.PointI(ix*(self.xsize + self.gutter), iy*(self.ysize + self.gutter)),
302 lsst.geom.ExtentI(self.xsize, self.ysize))
304 def drawLabels(self, labels=None, display="deferToFrame", frame=None):
305 """Draw the list labels at the corners of each panel.
307 Notes
308 -----
309 If labels is None, use the ones specified by ``Mosaic.append()``
310 """
312 if not labels:
313 labels = self.labels
315 if not labels:
316 return
318 if len(labels) != self.nImage:
319 raise RuntimeError(f"You provided {len(labels)} labels for {self.nImage} panels")
321 display = _getDisplayFromDisplayOrFrame(display, frame)
322 if not display:
323 return
325 with display.Buffering():
326 for i in range(len(labels)):
327 if labels[i]:
328 label, ctype = labels[i], None
329 try:
330 label, ctype = label
331 except Exception:
332 pass
334 if not label:
335 continue
337 display.dot(str(label), self.getBBox(i).getMinX(),
338 self.getBBox(i).getMinY(), ctype=ctype)
340 @property
341 def nImage(self):
342 """Number of images
343 """
344 return len(self.images)
347def drawBBox(bbox, borderWidth=0.0, origin=None, display="deferToFrame", ctype=None, bin=1, frame=None):
348 """Draw a bounding box on a display frame with the specified ctype.
350 Parameters
351 ----------
352 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
353 The box to draw
354 borderWidth : `float`
355 Include this many pixels
356 origin
357 If specified, the box is shifted by ``origin``
358 display : `str`
359 ctype : `str`
360 The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11
361 bin : `int`
362 All BBox coordinates are divided by bin, as is right and proper for overlaying on a binned image
363 frame
364 """
365 x0, y0 = bbox.getMinX(), bbox.getMinY()
366 x1, y1 = bbox.getMaxX(), bbox.getMaxY()
368 if origin:
369 x0 += origin[0]
370 x1 += origin[0]
371 y0 += origin[1]
372 y1 += origin[1]
374 x0 /= bin
375 y0 /= bin
376 x1 /= bin
377 y1 /= bin
378 borderWidth /= bin
380 display = _getDisplayFromDisplayOrFrame(display, frame)
381 display.line([(x0 - borderWidth, y0 - borderWidth),
382 (x0 - borderWidth, y1 + borderWidth),
383 (x1 + borderWidth, y1 + borderWidth),
384 (x1 + borderWidth, y0 - borderWidth),
385 (x0 - borderWidth, y0 - borderWidth),
386 ], ctype=ctype)
389def drawFootprint(foot, borderWidth=0.5, origin=None, XY0=None, frame=None, ctype=None, bin=1,
390 peaks=False, symb="+", size=0.4, ctypePeak=None, display="deferToFrame"):
391 """Draw an `lsst.afw.detection.Footprint` on a display frame with the specified ctype.
393 Parameters
394 ----------
395 foot : `lsst.afw.detection.Footprint`
396 borderWidth : `float`
397 Include an extra borderWidth pixels
398 origin
399 If ``origin`` is present, it's arithmetically added to the Footprint
400 XY0
401 if ``XY0`` is present is subtracted from the Footprint
402 frame
403 ctype : `str`
404 The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11
405 bin : `int`
406 All Footprint coordinates are divided by bin, as is right and proper
407 for overlaying on a binned image
408 peaks : `bool`
409 If peaks is `True`, also show the object's Peaks using the specified
410 ``symb`` and ``size`` and ``ctypePeak``
411 symb : `str`
412 size : `float`
413 ctypePeak : `str`
414 The desired color for peaks, either e.g. `lsst.afw.display.RED` or a color name known to X11
415 display : `str`
416 """
418 if XY0:
419 if origin:
420 raise RuntimeError("You may not specify both origin and XY0")
421 origin = (-XY0[0], -XY0[1])
423 display = _getDisplayFromDisplayOrFrame(display, frame)
424 with display.Buffering():
425 borderWidth /= bin
426 for s in foot.getSpans():
427 y, x0, x1 = s.getY(), s.getX0(), s.getX1()
429 if origin:
430 x0 += origin[0]
431 x1 += origin[0]
432 y += origin[1]
434 x0 /= bin
435 x1 /= bin
436 y /= bin
438 display.line([(x0 - borderWidth, y - borderWidth),
439 (x0 - borderWidth, y + borderWidth),
440 (x1 + borderWidth, y + borderWidth),
441 (x1 + borderWidth, y - borderWidth),
442 (x0 - borderWidth, y - borderWidth),
443 ], ctype=ctype)
445 if peaks:
446 for p in foot.getPeaks():
447 x, y = p.getIx(), p.getIy()
449 if origin:
450 x += origin[0]
451 y += origin[1]
453 x /= bin
454 y /= bin
456 display.dot(symb, x, y, size=size, ctype=ctypePeak)
459def drawCoaddInputs(exposure, frame=None, ctype=None, bin=1, display="deferToFrame"):
460 """Draw the bounding boxes of input exposures to a coadd on a display
461 frame with the specified ctype, assuming ``display.mtv()`` has already been
462 called on the given exposure on this frame.
464 All coordinates are divided by ``bin``, as is right and proper for overlaying on a binned image
465 """
466 coaddWcs = exposure.getWcs()
467 catalog = exposure.getInfo().getCoaddInputs().ccds
469 offset = lsst.geom.PointD() - lsst.geom.PointD(exposure.getXY0())
471 display = _getDisplayFromDisplayOrFrame(display, frame)
473 with display.Buffering():
474 for record in catalog:
475 ccdBox = lsst.geom.Box2D(record.getBBox())
476 ccdCorners = ccdBox.getCorners()
477 coaddCorners = [coaddWcs.skyToPixel(record.getWcs().pixelToSky(point)) + offset
478 for point in ccdCorners]
479 display.line([(coaddCorners[i].getX()/bin, coaddCorners[i].getY()/bin)
480 for i in range(-1, 4)], ctype=ctype)