LSST Applications g06d8191974+b5247657d3,g180d380827+b23588344e,g2079a07aa2+86d27d4dc4,g2305ad1205+0130fb9023,g29320951ab+7714a6b20a,g2bbee38e9b+0e5473021a,g337abbeb29+0e5473021a,g33d1c0ed96+0e5473021a,g3a166c0a6a+0e5473021a,g3ddfee87b4+8783ab7716,g48712c4677+72a8b1060b,g487adcacf7+bbaada240a,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+ecccb6240b,g5a732f18d5+53520f316c,g5ea96fc03c+33ab2bc355,g64a986408d+b5247657d3,g858d7b2824+b5247657d3,g8a8a8dda67+585e252eca,g99cad8db69+1453026da9,g9ddcbc5298+9a081db1e4,ga1e77700b3+15fc3df1f7,gb0e22166c9+60f28cb32d,gba4ed39666+c2a2e4ac27,gbb8dafda3b+3751ca9c65,gc120e1dc64+c91d1388df,gc28159a63d+0e5473021a,gc3e9b769f7+241adb7c58,gcf0d15dbbd+8783ab7716,gdaeeff99f8+f9a426f77a,ge6526c86ff+acdbe9a537,ge79ae78c31+0e5473021a,gee10cc3b42+585e252eca,gff1a9f87cc+b5247657d3,w.2024.17
LSST Data Management Base Package
Loading...
Searching...
No Matches
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
25import lsst.geom
26import lsst.afw.image as afwImage
27
28__all__ = (
29 "Mosaic",
30 "drawBBox", "drawFootprint", "drawCoaddInputs",
31)
32
33
34def _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
70class 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.gutter = gutter # number of pixels between panels in a mosaic
122 self.background = background # value in gutters
123 self.setMode(mode) # mosaicing mode
124 self.xsize = 0 # column size of panels
125 self.ysize = 0 # row size of panels
126
127 self.reset()
128
129 def reset(self):
130 """Reset the list of images to be mosaiced"""
131 self.images = [] # images to mosaic together
132 self.labels = [] # 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.xsize:
147 self.xsize = image.getWidth()
148 self.ysize = image.getHeight()
149
150 self.images.append(image)
151 self.labels.append((label, ctype))
152
153 return len(self.images)
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.images:
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.images = images
175 else:
176 images = self.images
177
178 if self.nImagenImage == 0:
179 raise RuntimeError("You must provide at least one image")
180
181 self.xsize, self.ysize = 0, 0
182 for im in images:
183 w, h = im.getWidth(), im.getHeight()
184 if w > self.xsize:
185 self.xsize = w
186 if h > self.ysize:
187 self.ysize = h
188
189 if background is None:
190 background = self.background
191 if mode is None:
192 mode = self.mode
193
194 if mode == "square":
195 nx, ny = 1, self.nImagenImage
196 while nx*im.getWidth() < ny*im.getHeight():
197 nx += 1
198 ny = self.nImagenImage//nx
199
200 if nx*ny < self.nImagenImage:
201 ny += 1
202 if nx*ny < self.nImagenImage:
203 nx += 1
204
205 if nx > self.nImagenImage:
206 nx = self.nImagenImage
207
208 assert nx*ny >= self.nImagenImage
209 elif mode == "x":
210 nx, ny = self.nImagenImage, 1
211 elif mode == "y":
212 nx, ny = 1, self.nImagenImage
213 elif isinstance(mode, int):
214 nx = mode
215 ny = self.nImagenImage//nx
216 if nx*ny < self.nImagenImage:
217 ny += 1
218 else:
219 raise RuntimeError(f"Unknown mosaicing mode: {mode}")
220
221 self.nx, self.ny = nx, ny
222
223 mosaic = images[0].Factory(
224 lsst.geom.Extent2I(nx*self.xsize + (nx - 1)*self.gutter,
225 ny*self.ysize + (ny - 1)*self.gutter)
226 )
227 try:
228 mosaic.set(self.background)
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.getBBox(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.images:
250 self.drawLabels(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.gutter = gutter
258
259 def setBackground(self, background):
260 """Set the value in the gutters
261 """
262 self.background = 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.mode = 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.xsize + self.gutter), iy*(self.ysize + self.gutter)),
301 lsst.geom.ExtentI(self.xsize, self.ysize))
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.labels
313
314 if not labels:
315 return
316
317 if len(labels) != self.nImagenImage:
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.getBBox(i).getMinX(),
337 self.getBBox(i).getMinY(), ctype=ctype)
338
339 @property
340 def nImage(self):
341 """Number of images
342 """
343 return len(self.images)
344
345
346def 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
388def 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
458def 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)
__init__(self, gutter=3, background=0, mode="square")
Definition utils.py:120
getBBox(self, ix, iy=None)
Definition utils.py:285
makeMosaic(self, images=None, display="deferToFrame", mode=None, background=None, title="")
Definition utils.py:156
append(self, image, label=None, ctype=None)
Definition utils.py:134
setBackground(self, background)
Definition utils.py:259
drawLabels(self, labels=None, display="deferToFrame", frame=None)
Definition utils.py:303
A floating-point coordinate rectangle geometry.
Definition Box.h:413
An integer coordinate rectangle.
Definition Box.h:55
drawCoaddInputs(exposure, frame=None, ctype=None, bin=1, display="deferToFrame")
Definition utils.py:458
drawBBox(bbox, borderWidth=0.0, origin=None, display="deferToFrame", ctype=None, bin=1, frame=None)
Definition utils.py:346
_getDisplayFromDisplayOrFrame(display, frame=None)
Definition utils.py:34
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