33 __all__ = (
"ImageMapReduceTask",
"ImageMapReduceConfig",
34 "ImageMapper",
"ImageMapperConfig",
35 "ImageReducer",
"ImageReducerConfig")
38 """Tasks for processing an exposure via processing on
39 multiple sub-exposures and then collecting the results
40 to either re-stitch the sub-exposures back into a new
41 exposure, or return summary results for each sub-exposure.
43 This provides a framework for arbitrary mapper-reducer
44 operations on an exposure by implementing simple operations in
45 subTasks. It currently is not parallelized, although it could be in
46 the future. It does enable operations such as spatially-mapped
47 processing on a grid across an image, processing regions surrounding
48 centroids (such as for PSF processing), etc.
50 It is implemented as primary Task, `ImageMapReduceTask` which contains
51 two subtasks, `ImageMapper` and `ImageReducer`.
52 `ImageMapReduceTask` configures the centroids and sub-exposure
53 dimensions to be processed, and then calls the `run` methods of the
54 `ImageMapper` and `ImageReducer` on those sub-exposures.
55 `ImageMapReduceTask` may be configured with a list of sub-exposure
56 centroids (`config.cellCentroidsX` and `config.cellCentroidsY`) and a
57 single pair of bounding boxes defining their dimensions, or a set of
58 parameters defining a regular grid of centroids (`config.gridStepX`
59 and `config.gridStepY`).
61 `ImageMapper` is an abstract class and must be subclassed with
62 an implemented `run` method to provide the desired operation for
63 processing individual sub-exposures. It is called from
64 `ImageMapReduceTask.run`, and may return a new, processed sub-exposure
65 which is to be "stitched" back into a new resulting larger exposure
66 (depending on the configured `ImageMapReduceTask.mapper`);
67 otherwise if it does not return an lsst.afw.image.Exposure, then the results are
68 passed back directly to the caller.
70 `ImageReducer` will either stitch the `mapperResults` list of
71 results generated by the `ImageMapper` together into a new
72 Exposure (by default) or pass it through to the
73 caller. `ImageReducer` has an implemented `run` method for
74 basic reducing operations (`reduceOperation`) such as `average` (which
75 will average all overlapping pixels from sub-exposures produced by the
76 `ImageMapper` into the new exposure). Another notable
77 implemented `reduceOperation` is 'none', in which case the
78 `mapperResults` list is simply returned directly.
83 """Configuration parameters for ImageMapper
89 """Abstract base class for any task that is to be
90 used as `ImageMapReduceConfig.mapper`.
94 An `ImageMapper` is responsible for processing individual
95 sub-exposures in its `run` method, which is called from
96 `ImageMapReduceTask.run`. `run` may return a processed new
97 sub-exposure which can be be "stitched" back into a new resulting
98 larger exposure (depending on the configured
99 `ImageReducer`); otherwise if it does not return an
100 lsst.afw.image.Exposure, then the
101 `ImageReducer.config.reducer.reduceOperation`
102 should be set to 'none' and the result will be propagated
105 ConfigClass = ImageMapperConfig
106 _DefaultName =
"ip_diffim_ImageMapper"
109 def run(self, subExposure, expandedSubExposure, fullBBox, **kwargs):
110 """Perform operation on `subExposure`.
112 To be implemented by subclasses. See class docstring for more
113 details. This method is given the `subExposure` which
114 is to be operated upon, and an `expandedSubExposure` which
115 will contain `subExposure` with additional surrounding
116 pixels. This allows for, for example, convolutions (which
117 should be performed on `expandedSubExposure`), to prevent the
118 returned sub-exposure from containing invalid pixels.
120 This method may return a new, processed sub-exposure which can
121 be be "stitched" back into a new resulting larger exposure
122 (depending on the paired, configured `ImageReducer`);
123 otherwise if it does not return an lsst.afw.image.Exposure, then the
124 `ImageReducer.config.mapper.reduceOperation`
125 should be set to 'none' and the result will be propagated
130 subExposure : `lsst.afw.image.Exposure`
131 the sub-exposure upon which to operate
132 expandedSubExposure : `lsst.afw.image.Exposure`
133 the expanded sub-exposure upon which to operate
134 fullBBox : `lsst.geom.Box2I`
135 the bounding box of the original exposure
137 additional keyword arguments propagated from
138 `ImageMapReduceTask.run`.
142 result : `lsst.pipe.base.Struct`
143 A structure containing the result of the `subExposure` processing,
144 which may itself be of any type. See above for details. If it is an
145 `lsst.afw.image.Exposure` (processed sub-exposure), then the name in
146 the Struct should be 'subExposure'. This is implemented here as a
147 pass-through example only.
149 return pipeBase.Struct(subExposure=subExposure)
153 """Configuration parameters for the ImageReducer
155 reduceOperation = pexConfig.ChoiceField(
157 doc=
"""Operation to use for reducing subimages into new image.""",
160 "none":
"""simply return a list of values and don't re-map results into
161 a new image (noop operation)""",
162 "copy":
"""copy pixels directly from subimage into correct location in
163 new exposure (potentially non-deterministic for overlaps)""",
164 "sum":
"""add pixels from overlaps (probably never wanted; used for testing)
165 into correct location in new exposure""",
166 "average":
"""same as copy, but also average pixels from overlapped regions
168 "coaddPsf":
"""Instead of constructing an Exposure, take a list of returned
169 PSFs and use CoaddPsf to construct a single PSF that covers the
170 entire input exposure""",
173 badMaskPlanes = pexConfig.ListField(
175 doc=
"""Mask planes to set for invalid pixels""",
176 default=(
'INVALID_MAPREDUCE',
'BAD',
'NO_DATA')
181 """Base class for any 'reduce' task that is to be
182 used as `ImageMapReduceConfig.reducer`.
184 Basic reduce operations are provided by the `run` method
185 of this class, to be selected by its config.
187 ConfigClass = ImageReducerConfig
188 _DefaultName =
"ip_diffim_ImageReducer"
190 def run(self, mapperResults, exposure, **kwargs):
191 """Reduce a list of items produced by `ImageMapper`.
193 Either stitch the passed `mapperResults` list
194 together into a new Exposure (default) or pass it through
195 (if `self.config.reduceOperation` is 'none').
197 If `self.config.reduceOperation` is not 'none', then expect
198 that the `pipeBase.Struct`s in the `mapperResults` list
199 contain sub-exposures named 'subExposure', to be stitched back
200 into a single Exposure with the same dimensions, PSF, and mask
201 as the input `exposure`. Otherwise, the `mapperResults` list
202 is simply returned directly.
206 mapperResults : `list`
207 list of `lsst.pipe.base.Struct` returned by `ImageMapper.run`.
208 exposure : `lsst.afw.image.Exposure`
209 the original exposure which is cloned to use as the
210 basis for the resulting exposure (if
211 ``self.config.mapper.reduceOperation`` is not 'None')
213 additional keyword arguments propagated from
214 `ImageMapReduceTask.run`.
218 A `lsst.pipe.base.Struct` containing either an `lsst.afw.image.Exposure`
219 (named 'exposure') or a list (named 'result'),
220 depending on `config.reduceOperation`.
224 1. This currently correctly handles overlapping sub-exposures.
225 For overlapping sub-exposures, use `config.reduceOperation='average'`.
226 2. This correctly handles varying PSFs, constructing the resulting
227 exposure's PSF via CoaddPsf (DM-9629).
231 1. To be done: correct handling of masks (nearly there)
232 2. This logic currently makes *two* copies of the original exposure
233 (one here and one in `mapper.run()`). Possibly of concern
234 for large images on memory-constrained systems.
237 if self.config.reduceOperation ==
'none':
238 return pipeBase.Struct(result=mapperResults)
240 if self.config.reduceOperation ==
'coaddPsf':
243 return pipeBase.Struct(result=coaddPsf)
245 newExp = exposure.clone()
246 newMI = newExp.getMaskedImage()
248 reduceOp = self.config.reduceOperation
249 if reduceOp ==
'copy':
251 newMI.getImage()[:, :] = np.nan
252 newMI.getVariance()[:, :] = np.nan
254 newMI.getImage()[:, :] = 0.
255 newMI.getVariance()[:, :] = 0.
256 if reduceOp ==
'average':
257 weights = afwImage.ImageI(newMI.getBBox())
259 for item
in mapperResults:
260 item = item.subExposure
261 if not (isinstance(item, afwImage.ExposureF)
or isinstance(item, afwImage.ExposureI)
262 or isinstance(item, afwImage.ExposureU)
or isinstance(item, afwImage.ExposureD)):
263 raise TypeError(
"""Expecting an Exposure type, got %s.
264 Consider using `reduceOperation="none".""" % str(
type(item)))
265 subExp = newExp.Factory(newExp, item.getBBox())
266 subMI = subExp.getMaskedImage()
267 patchMI = item.getMaskedImage()
268 isValid = ~np.isnan(patchMI.getImage().getArray() * patchMI.getVariance().getArray())
270 if reduceOp ==
'copy':
271 subMI.getImage().getArray()[isValid] = patchMI.getImage().getArray()[isValid]
272 subMI.getVariance().getArray()[isValid] = patchMI.getVariance().getArray()[isValid]
273 subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
275 if reduceOp ==
'sum' or reduceOp ==
'average':
276 subMI.getImage().getArray()[isValid] += patchMI.getImage().getArray()[isValid]
277 subMI.getVariance().getArray()[isValid] += patchMI.getVariance().getArray()[isValid]
278 subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
279 if reduceOp ==
'average':
281 wtsView = afwImage.ImageI(weights, item.getBBox())
282 wtsView.getArray()[isValid] += 1
285 mask = newMI.getMask()
286 for m
in self.config.badMaskPlanes:
288 bad = mask.getPlaneBitMask(self.config.badMaskPlanes)
290 isNan = np.where(np.isnan(newMI.getImage().getArray() * newMI.getVariance().getArray()))
291 if len(isNan[0]) > 0:
293 mask.getArray()[isNan[0], isNan[1]] |= bad
295 if reduceOp ==
'average':
296 wts = weights.getArray().astype(np.float)
297 self.log.
info(
'AVERAGE: Maximum overlap: %f', np.nanmax(wts))
298 self.log.
info(
'AVERAGE: Average overlap: %f', np.nanmean(wts))
299 self.log.
info(
'AVERAGE: Minimum overlap: %f', np.nanmin(wts))
300 wtsZero = np.equal(wts, 0.)
301 wtsZeroInds = np.where(wtsZero)
302 wtsZeroSum = len(wtsZeroInds[0])
303 self.log.
info(
'AVERAGE: Number of zero pixels: %f (%f%%)', wtsZeroSum,
304 wtsZeroSum * 100. / wtsZero.size)
305 notWtsZero = ~wtsZero
306 tmp = newMI.getImage().getArray()
307 np.divide(tmp, wts, out=tmp, where=notWtsZero)
308 tmp = newMI.getVariance().getArray()
309 np.divide(tmp, wts, out=tmp, where=notWtsZero)
310 if len(wtsZeroInds[0]) > 0:
311 newMI.getImage().getArray()[wtsZeroInds] = np.nan
312 newMI.getVariance().getArray()[wtsZeroInds] = np.nan
315 mask.getArray()[wtsZeroInds] |= bad
318 if reduceOp ==
'sum' or reduceOp ==
'average':
322 return pipeBase.Struct(exposure=newExp)
324 def _constructPsf(self, mapperResults, exposure):
325 """Construct a CoaddPsf based on PSFs from individual subExposures
327 Currently uses (and returns) a CoaddPsf. TBD if we want to
328 create a custom subclass of CoaddPsf to differentiate it.
332 mapperResults : `list`
333 list of `pipeBase.Struct` returned by `ImageMapper.run`.
334 For this to work, each element of `mapperResults` must contain
335 a `subExposure` element, from which the component Psfs are
336 extracted (thus the reducerTask cannot have
337 `reduceOperation = 'none'`.
338 exposure : `lsst.afw.image.Exposure`
339 the original exposure which is used here solely for its
340 bounding-box and WCS.
344 psf : `lsst.meas.algorithms.CoaddPsf`
345 A psf constructed from the PSFs of the individual subExposures.
347 schema = afwTable.ExposureTable.makeMinimalSchema()
348 schema.addField(
"weight", type=
"D", doc=
"Coadd weight")
353 wcsref = exposure.getWcs()
354 for i, res
in enumerate(mapperResults):
355 record = mycatalog.getTable().makeRecord()
356 if 'subExposure' in res.getDict():
357 subExp = res.subExposure
358 if subExp.getWcs() != wcsref:
359 raise ValueError(
'Wcs of subExposure is different from exposure')
360 record.setPsf(subExp.getPsf())
361 record.setWcs(subExp.getWcs())
362 record.setBBox(subExp.getBBox())
363 elif 'psf' in res.getDict():
364 record.setPsf(res.psf)
365 record.setWcs(wcsref)
366 record.setBBox(res.bbox)
367 record[
'weight'] = 1.0
369 mycatalog.append(record)
372 psf = measAlg.CoaddPsf(mycatalog, wcsref,
'weight')
377 """Configuration parameters for the ImageMapReduceTask
379 mapper = pexConfig.ConfigurableField(
380 doc=
"Task to run on each subimage",
384 reducer = pexConfig.ConfigurableField(
385 doc=
"Task to combine results of mapper task",
393 cellCentroidsX = pexConfig.ListField(
395 doc=
"""Input X centroids around which to place subimages.
396 If None, use grid config options below.""",
401 cellCentroidsY = pexConfig.ListField(
403 doc=
"""Input Y centroids around which to place subimages.
404 If None, use grid config options below.""",
409 cellSizeX = pexConfig.Field(
411 doc=
"""Dimensions of each grid cell in x direction""",
413 check=
lambda x: x > 0.
416 cellSizeY = pexConfig.Field(
418 doc=
"""Dimensions of each grid cell in y direction""",
420 check=
lambda x: x > 0.
423 gridStepX = pexConfig.Field(
425 doc=
"""Spacing between subsequent grid cells in x direction. If equal to
426 cellSizeX, then there is no overlap in the x direction.""",
428 check=
lambda x: x > 0.
431 gridStepY = pexConfig.Field(
433 doc=
"""Spacing between subsequent grid cells in y direction. If equal to
434 cellSizeY, then there is no overlap in the y direction.""",
436 check=
lambda x: x > 0.
439 borderSizeX = pexConfig.Field(
441 doc=
"""Dimensions of grid cell border in +/- x direction, to be used
442 for generating `expandedSubExposure`.""",
444 check=
lambda x: x > 0.
447 borderSizeY = pexConfig.Field(
449 doc=
"""Dimensions of grid cell border in +/- y direction, to be used
450 for generating `expandedSubExposure`.""",
452 check=
lambda x: x > 0.
455 adjustGridOption = pexConfig.ChoiceField(
457 doc=
"""Whether and how to adjust grid to fit evenly within, and cover entire
461 "spacing":
"adjust spacing between centers of grid cells (allowing overlaps)",
462 "size":
"adjust the sizes of the grid cells (disallowing overlaps)",
463 "none":
"do not adjust the grid sizes or spacing"
467 scaleByFwhm = pexConfig.Field(
469 doc=
"""Scale cellSize/gridStep/borderSize/overlapSize by PSF FWHM rather
474 returnSubImages = pexConfig.Field(
476 doc=
"""Return the input subExposures alongside the processed ones (for debugging)""",
480 ignoreMaskPlanes = pexConfig.ListField(
482 doc=
"""Mask planes to ignore for sigma-clipped statistics""",
483 default=(
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
488 """Split an Exposure into subExposures (optionally on a grid) and
489 perform the same operation on each.
491 Perform 'simple' operations on a gridded set of subExposures of a
492 larger Exposure, and then (by default) have those subExposures
493 stitched back together into a new, full-sized image.
495 Contrary to the expectation given by its name, this task does not
496 perform these operations in parallel, although it could be updatd
497 to provide such functionality.
499 The actual operations are performed by two subTasks passed to the
500 config. The exposure passed to this task's `run` method will be
501 divided, and those subExposures will be passed to the subTasks,
502 along with the original exposure. The reducing operation is
503 performed by the second subtask.
505 ConfigClass = ImageMapReduceConfig
506 _DefaultName =
"ip_diffim_imageMapReduce"
509 """Create the image map-reduce task
514 arguments to be passed to
515 `lsst.pipe.base.task.Task.__init__`
517 additional keyword arguments to be passed to
518 `lsst.pipe.base.task.Task.__init__`
520 pipeBase.Task.__init__(self, *args, **kwargs)
523 self.makeSubtask(
"mapper")
524 self.makeSubtask(
"reducer")
527 def run(self, exposure, **kwargs):
528 """Perform a map-reduce operation on the given exposure.
530 Split the exposure into sub-expposures on a grid (parameters
531 given by `ImageMapReduceConfig`) and perform
532 `config.mapper.run()` on each. Reduce the resulting
533 sub-exposures by running `config.reducer.run()`.
537 exposure : `lsst.afw.image.Exposure`
538 the full exposure to process
540 additional keyword arguments to be passed to
541 subtask `run` methods
545 output of `reducer.run()`
548 self.log.
info(
"Mapper sub-task: %s", self.mapper._DefaultName)
549 mapperResults = self.
_runMapper(exposure, **kwargs)
550 self.log.
info(
"Reducer sub-task: %s", self.reducer._DefaultName)
551 result = self.
_reduceImage(mapperResults, exposure, **kwargs)
554 def _runMapper(self, exposure, doClone=False, **kwargs):
555 """Perform `mapper.run` on each sub-exposure
557 Perform `mapper.run` on each sub-exposure across a
558 grid on `exposure` generated by `_generateGrid`. Also pass to
559 `mapper.run` an 'expanded sub-exposure' containing the
560 same region as the sub-exposure but with an expanded bounding box.
564 exposure : `lsst.afw.image.Exposure`
565 the original exposure which is used as the template
567 if True, clone the subimages before passing to subtask;
568 in that case, the sub-exps do not have to be considered as read-only
570 additional keyword arguments to be passed to
571 `mapper.run` and `self._generateGrid`, including `forceEvenSized`.
575 a list of `pipeBase.Struct`s as returned by `mapper.run`.
580 raise ValueError(
'Bounding boxes list and expanded bounding boxes list are of different lengths')
582 self.log.
info(
"Processing %d sub-exposures", len(self.
boxes0))
585 subExp = exposure.Factory(exposure, box0)
586 expandedSubExp = exposure.Factory(exposure, box1)
588 subExp = subExp.clone()
589 expandedSubExp = expandedSubExp.clone()
590 result = self.mapper.
run(subExp, expandedSubExp, exposure.getBBox(), **kwargs)
591 if self.config.returnSubImages:
592 toAdd = pipeBase.Struct(inputSubExposure=subExp,
593 inputExpandedSubExposure=expandedSubExp)
594 result.mergeItems(toAdd,
'inputSubExposure',
'inputExpandedSubExposure')
595 mapperResults.append(result)
599 def _reduceImage(self, mapperResults, exposure, **kwargs):
600 """Reduce/merge a set of sub-exposures into a final result
602 Return an exposure of the same dimensions as `exposure`.
603 `mapperResults` is expected to have been produced by `runMapper`.
607 mapperResults : `list`
608 `list` of `lsst.pipe.base.Struct`, each of which was produced by
610 exposure : `lsst.afw.image.Exposure`
611 the original exposure
613 additional keyword arguments
617 Output of `reducer.run` which is a `pipeBase.Struct`.
619 result = self.reducer.
run(mapperResults, exposure, **kwargs)
622 def _generateGrid(self, exposure, forceEvenSized=False, **kwargs):
623 """Generate two lists of bounding boxes that evenly grid `exposure`
625 Unless the config was provided with `cellCentroidsX` and
626 `cellCentroidsY`, grid (subimage) centers are spaced evenly
627 by gridStepX/Y. Then the grid is adjusted as little as
628 possible to evenly cover the input exposure (if
629 adjustGridOption is not 'none'). Then the second set of
630 bounding boxes is expanded by borderSizeX/Y. The expanded
631 bounding boxes are adjusted to ensure that they intersect the
632 exposure's bounding box. The resulting lists of bounding boxes
633 and corresponding expanded bounding boxes are set to
634 `self.boxes0`, `self.boxes1`.
638 exposure : `lsst.afw.image.Exposure`
639 input exposure whose full bounding box is to be evenly gridded.
640 forceEvenSized : `bool`
641 force grid elements to have even-valued x- and y- dimensions?
642 (Potentially useful if doing Fourier transform of subExposures.)
646 bbox = exposure.getBBox()
649 cellCentroidsX = self.config.cellCentroidsX
650 cellCentroidsY = self.config.cellCentroidsY
651 cellSizeX = self.config.cellSizeX
652 cellSizeY = self.config.cellSizeY
653 gridStepX = self.config.gridStepX
654 gridStepY = self.config.gridStepY
655 borderSizeX = self.config.borderSizeX
656 borderSizeY = self.config.borderSizeY
657 adjustGridOption = self.config.adjustGridOption
658 scaleByFwhm = self.config.scaleByFwhm
660 if cellCentroidsX
is None or len(cellCentroidsX) <= 0:
663 psfFwhm = (exposure.getPsf().computeShape().getDeterminantRadius()
664 * 2.*np.sqrt(2.*np.log(2.)))
666 self.log.
info(
"Scaling grid parameters by %f" % psfFwhm)
668 def rescaleValue(val):
670 return np.rint(val*psfFwhm).astype(int)
672 return np.rint(val).astype(int)
674 cellSizeX = rescaleValue(cellSizeX)
675 cellSizeY = rescaleValue(cellSizeY)
676 gridStepX = rescaleValue(gridStepX)
677 gridStepY = rescaleValue(gridStepY)
678 borderSizeX = rescaleValue(borderSizeX)
679 borderSizeY = rescaleValue(borderSizeY)
681 nGridX = bbox.getWidth()//gridStepX
682 nGridY = bbox.getHeight()//gridStepY
684 if adjustGridOption ==
'spacing':
686 nGridX = bbox.getWidth()//cellSizeX + 1
687 nGridY = bbox.getHeight()//cellSizeY + 1
688 xLinSpace = np.linspace(cellSizeX//2, bbox.getWidth() - cellSizeX//2, nGridX)
689 yLinSpace = np.linspace(cellSizeY//2, bbox.getHeight() - cellSizeY//2, nGridY)
691 elif adjustGridOption ==
'size':
692 cellSizeX = gridStepX
693 cellSizeY = gridStepY
694 xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, cellSizeX)
695 yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, cellSizeY)
700 xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, gridStepX)
701 yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, gridStepY)
703 cellCentroids = [(x, y)
for x
in xLinSpace
for y
in yLinSpace]
707 cellCentroids = [(cellCentroidsX[i], cellCentroidsY[i])
for i
in range(len(cellCentroidsX))]
718 def _makeBoxEvenSized(bb):
719 """Force a bounding-box to have dimensions that are modulo 2."""
721 if bb.getWidth() % 2 == 1:
724 if bb.getWidth() % 2 == 1:
727 if bb.getHeight() % 2 == 1:
730 if bb.getHeight() % 2 == 1:
733 if bb.getWidth() % 2 == 1
or bb.getHeight() % 2 == 1:
734 raise RuntimeError(
'Cannot make bounding box even-sized. Probably too big.')
739 if cellCentroids
is not None and len(cellCentroids) > 0:
740 for x, y
in cellCentroids:
743 xoff = int(np.floor(centroid.getX())) - bb0.getWidth()//2
744 yoff = int(np.floor(centroid.getY())) - bb0.getHeight()//2
748 bb0 = _makeBoxEvenSized(bb0)
753 bb1 = _makeBoxEvenSized(bb1)
755 if bb0.getArea() > 1
and bb1.getArea() > 1:
762 """Plot both grids of boxes using matplotlib.
764 Will compute the grid via `_generateGrid` if
765 `self.boxes0` and `self.boxes1` have not already been set.
769 exposure : `lsst.afw.image.Exposure`
770 Exposure whose bounding box is gridded by this task.
772 Plot every skip-ped box (help make plots less confusing)
774 import matplotlib.pyplot
as plt
777 raise RuntimeError(
'Cannot plot boxes. Run _generateGrid first.')
781 plt.gca().set_prop_cycle(
None)
784 def _plotBoxGrid(self, boxes, bbox, **kwargs):
785 """Plot a grid of boxes using matplotlib.
789 boxes : `list` of `lsst.geom.Box2I`
790 a list of bounding boxes.
791 bbox : `lsst.geom.Box2I`
792 an overall bounding box
794 additional keyword arguments for matplotlib
796 import matplotlib.pyplot
as plt
799 corners = np.array([np.array([pt.getX(), pt.getY()])
for pt
in box.getCorners()])
800 corners = np.vstack([corners, corners[0, :]])
801 plt.plot(corners[:, 0], corners[:, 1], **kwargs)
805 plt.xlim(bbox.getBeginX(), bbox.getEndX())
806 plt.ylim(bbox.getBeginY(), bbox.getEndY())