1 from __future__
import absolute_import, division, print_function
2 from future
import standard_library
3 standard_library.install_aliases()
28 from future.utils
import with_metaclass
30 import lsst.afw.image
as afwImage
31 import lsst.afw.geom
as afwGeom
32 import lsst.afw.table
as afwTable
33 import lsst.meas.algorithms
as measAlg
34 import lsst.pex.config
as pexConfig
35 import lsst.pipe.base
as pipeBase
37 __all__ = (
"ImageMapReduceTask",
"ImageMapReduceConfig",
38 "ImageMapperSubtask",
"ImageMapperSubtaskConfig",
39 "ImageReducerSubtask",
"ImageReducerSubtaskConfig")
42 """Tasks for processing an exposure via processing on
43 multiple sub-exposures and then collecting the results
44 to either re-stitch the sub-exposures back into a new
45 exposure, or return summary results for each sub-exposure.
47 This essentially provides a framework for arbitrary mapper-reducer
48 operations on an exposure by implementing simple operations in
49 subTasks. It currently is not parallelized, although it could be in
50 the future. It does enable operations such as spatially-mapped
51 processing on a grid across an image, processing regions surrounding
52 centroids (such as for PSF processing), etc.
54 It is implemented as primary Task, `ImageMapReduceTask` which contains
55 two subtasks, `ImageMapperSubtask` and `ImageReducerSubtask`.
56 `ImageMapReduceTask` configures the centroids and sub-exposure
57 dimensions to be processed, and then calls the `run` methods of the
58 `ImageMapperSubtask` and `ImageReducerSubtask` on those sub-exposures.
59 `ImageMapReduceTask` may be configured with a list of sub-exposure
60 centroids (`config.cellCentroidsX` and `config.cellCentroidsY`) and a
61 single pair of bounding boxes defining their dimensions, or a set of
62 parameters defining a regular grid of centroids (`config.gridStepX`
63 and `config.gridStepY`).
65 `ImageMapperSubtask` is an abstract class and must be subclassed with
66 an implemented `run` method to provide the desired operation for
67 processing individual sub-exposures. It is called from
68 `ImageMapReduceTask.run`, and may return a new, processed sub-exposure
69 which is to be "stitched" back into a new resulting larger exposure
70 (depending on the configured `ImageMapReduceTask.mapperSubtask`);
71 otherwise if it does not return an lsst.afw.image.Exposure, then the results are
72 passed back directly to the caller.
74 `ImageReducerSubtask` will either stitch the `mapperResults` list of
75 results generated by the `ImageMapperSubtask` together into a new
76 Exposure (by default) or pass it through to the
77 caller. `ImageReducerSubtask` has an implemented `run` method for
78 basic reducing operations (`reduceOperation`) such as `average` (which
79 will average all overlapping pixels from sub-exposures produced by the
80 `ImageMapperSubtask` into the new exposure). Another notable
81 implemented `reduceOperation` is 'none', in which case the
82 `mapperResults` list is simply returned directly.
86 """Configuration parameters for ImageMapperSubtask
92 """Abstract base class for any task that is to be
93 used as `ImageMapReduceConfig.mapperSubtask`.
95 An `ImageMapperSubtask` is responsible for processing individual
96 sub-exposures in its `run` method, which is called from
97 `ImageMapReduceTask.run`. `run` may return a processed new
98 sub-exposure which can be be "stitched" back into a new resulting
99 larger exposure (depending on the configured
100 `ImageReducerSubtask`); otherwise if it does not return an
101 lsst.afw.image.Exposure, then the
102 `ImageReducerSubtask.config.reducerSubtask.reduceOperation`
103 should be set to 'none' and the result will be propagated
106 ConfigClass = ImageMapperSubtaskConfig
107 _DefaultName =
"ip_diffim_ImageMapperSubtask"
110 def run(self, subExposure, expandedSubExposure, fullBBox, **kwargs):
111 """Perform operation on `subExposure`.
113 To be implemented by subclasses. See class docstring for more
114 details. This method is given the `subExposure` which
115 is to be operated upon, and an `expandedSubExposure` which
116 will contain `subExposure` with additional surrounding
117 pixels. This allows for, for example, convolutions (which
118 should be performed on `expandedSubExposure`), to prevent the
119 returned sub-exposure from containing invalid pixels.
121 This method may return a new, processed sub-exposure which can
122 be be "stitched" back into a new resulting larger exposure
123 (depending on the paired, configured `ImageReducerSubtask`);
124 otherwise if it does not return an lsst.afw.image.Exposure, then the
125 `ImageReducerSubtask.config.mapperSubtask.reduceOperation`
126 should be set to 'none' and the result will be propagated
131 subExposure : lsst.afw.image.Exposure
132 the sub-exposure upon which to operate
133 expandedSubExposure : lsst.afw.image.Exposure
134 the expanded sub-exposure upon which to operate
135 fullBBox : afwGeom.BoundingBox
136 the bounding box of the original exposure
138 additional keyword arguments propagated from
139 `ImageMapReduceTask.run`.
143 A `pipeBase.Struct 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 the Struct
146 should be 'subExposure'. This is implemented here as a pass-through
149 return pipeBase.Struct(subExposure=subExposure)
153 """Configuration parameters for the ImageReducerSubtask
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""",
176 """Base class for any 'reduce' task that is to be
177 used as `ImageMapReduceConfig.reducerSubtask`.
179 Basic reduce operations are provided by the `run` method
180 of this class, to be selected by its config.
182 ConfigClass = ImageReducerSubtaskConfig
183 _DefaultName =
"ip_diffim_ImageReducerSubtask"
185 def run(self, mapperResults, exposure, **kwargs):
186 """Reduce a list of items produced by `ImageMapperSubtask`.
188 Either stitch the passed `mapperResults` list
189 together into a new Exposure (default) or pass it through
190 (if `self.config.reduceOperation` is 'none').
192 If `self.config.reduceOperation` is not 'none', then expect
193 that the `pipeBase.Struct`s in the `mapperResults` list
194 contain sub-exposures named 'subExposure', to be stitched back
195 into a single Exposure with the same dimensions, PSF, and mask
196 as the input `exposure`. Otherwise, the `mapperResults` list
197 is simply returned directly.
202 list of `pipeBase.Struct` returned by `ImageMapperSubtask.run`.
203 exposure : lsst.afw.image.Exposure
204 the original exposure which is cloned to use as the
205 basis for the resulting exposure (if
206 self.config.mapperSubtask.reduceOperation is not 'none')
208 additional keyword arguments propagated from
209 `ImageMapReduceTask.run`.
213 A `pipeBase.Struct` containing either an `lsst.afw.image.Exposure` (named 'exposure')
214 or a list (named 'result'), depending on `config.reduceOperation`.
218 1. This currently correctly handles overlapping sub-exposures.
219 For overlapping sub-exposures, use `config.reduceOperation='average'`.
220 2. This correctly handles varying PSFs, constructing the resulting
221 exposure's PSF via CoaddPsf (DM-9629).
225 1. To be done: correct handling of masks (nearly there)
226 2. This logic currently makes *two* copies of the original exposure
227 (one here and one in `mapperSubtask.run()`). Possibly of concern
228 for large images on memory-constrained systems.
231 if self.config.reduceOperation ==
'none':
232 return pipeBase.Struct(result=mapperResults)
234 if self.config.reduceOperation ==
'coaddPsf':
237 return pipeBase.Struct(result=coaddPsf)
239 newExp = exposure.clone()
240 newMI = newExp.getMaskedImage()
242 reduceOp = self.config.reduceOperation
243 if reduceOp ==
'copy':
245 newMI.getImage()[:, :] = np.nan
246 newMI.getVariance()[:, :] = np.nan
248 newMI.getImage()[:, :] = 0.
249 newMI.getVariance()[:, :] = 0.
250 if reduceOp ==
'average':
251 weights = afwImage.ImageI(newMI.getBBox())
253 for item
in mapperResults:
254 item = item.subExposure
255 if not (isinstance(item, afwImage.ExposureF)
or isinstance(item, afwImage.ExposureI)
or
256 isinstance(item, afwImage.ExposureU)
or isinstance(item, afwImage.ExposureD)):
257 raise TypeError(
"""Expecting an Exposure type, got %s.
258 Consider using `reduceOperation="none".""" % str(type(item)))
259 subExp = newExp.Factory(newExp, item.getBBox())
260 subMI = subExp.getMaskedImage()
261 patchMI = item.getMaskedImage()
262 isNotNan = ~(np.isnan(patchMI.getImage().getArray()) |
263 np.isnan(patchMI.getVariance().getArray()))
264 if reduceOp ==
'copy':
265 subMI.getImage().getArray()[isNotNan] = patchMI.getImage().getArray()[isNotNan]
266 subMI.getVariance().getArray()[isNotNan] = patchMI.getVariance().getArray()[isNotNan]
267 subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
269 if reduceOp ==
'sum' or reduceOp ==
'average':
270 subMI.getImage().getArray()[isNotNan] += patchMI.getImage().getArray()[isNotNan]
271 subMI.getVariance().getArray()[isNotNan] += patchMI.getVariance().getArray()[isNotNan]
272 subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
273 if reduceOp ==
'average':
275 wtsView = afwImage.ImageI(weights, item.getBBox())
276 wtsView.getArray()[isNotNan] += 1
278 if reduceOp ==
'average':
279 wts = weights.getArray().astype(np.float)
280 self.log.info(
'AVERAGE: Maximum overlap: %f', wts.max())
281 self.log.info(
'AVERAGE: Average overlap: %f', np.nanmean(wts))
282 newMI.getImage().getArray()[:, :] /= wts
283 newMI.getVariance().getArray()[:, :] /= wts
285 newMI.getImage().getArray()[wtsZero] = newMI.getVariance().getArray()[wtsZero] = np.nan
289 if reduceOp ==
'sum' or reduceOp ==
'average':
293 return pipeBase.Struct(exposure=newExp)
296 """Construct a CoaddPsf based on PSFs from individual subExposures
298 Currently uses (and returns) a CoaddPsf. TBD if we want to
299 create a custom subclass of CoaddPsf to differentiate it.
304 list of `pipeBase.Struct` returned by `ImageMapperSubtask.run`.
305 For this to work, each element of `mapperResults` must contain
306 a `subExposure` element, from which the component Psfs are
307 extracted (thus the reducerTask cannot have
308 `reduceOperation = 'none'`.
309 exposure : lsst.afw.image.Exposure
310 the original exposure which is used here solely for its
311 bounding-box and WCS.
315 A `measAlg.CoaddPsf` constructed from the PSFs of the individual
318 schema = afwTable.ExposureTable.makeMinimalSchema()
319 schema.addField(
"weight", type=
"D", doc=
"Coadd weight")
320 mycatalog = afwTable.ExposureCatalog(schema)
324 wcsref = exposure.getWcs()
325 for i, res
in enumerate(mapperResults):
326 record = mycatalog.getTable().makeRecord()
327 if 'subExposure' in res.getDict():
328 subExp = res.subExposure
329 if subExp.getWcs() != wcsref:
330 raise ValueError(
'Wcs of subExposure is different from exposure')
331 record.setPsf(subExp.getPsf())
332 record.setWcs(subExp.getWcs())
333 record.setBBox(subExp.getBBox())
334 elif 'psf' in res.getDict():
335 record.setPsf(res.psf)
336 record.setWcs(wcsref)
337 record.setBBox(res.bbox)
338 record[
'weight'] = 1.0
340 mycatalog.append(record)
343 psf = measAlg.CoaddPsf(mycatalog, wcsref,
'weight')
348 """Configuration parameters for the ImageMapReduceTask
350 mapperSubtask = pexConfig.ConfigurableField(
351 doc=
"Subtask to run on each subimage",
352 target=ImageMapperSubtask,
355 reducerSubtask = pexConfig.ConfigurableField(
356 doc=
"Subtask to combine results of mapperSubTask",
357 target=ImageReducerSubtask,
364 cellCentroidsX = pexConfig.ListField(
366 doc=
"""Input X centroids around which to place subimages.
367 If None, use grid config options below.""",
372 cellCentroidsY = pexConfig.ListField(
374 doc=
"""Input Y centroids around which to place subimages.
375 If None, use grid config options below.""",
380 cellSizeX = pexConfig.Field(
382 doc=
"""Dimensions of each grid cell in x direction""",
384 check=
lambda x: x > 0.
387 cellSizeY = pexConfig.Field(
389 doc=
"""Dimensions of each grid cell in y direction""",
391 check=
lambda x: x > 0.
394 gridStepX = pexConfig.Field(
396 doc=
"""Spacing between subsequent grid cells in x direction. If equal to
397 cellSizeX, then there is no overlap in the x direction.""",
399 check=
lambda x: x > 0.
402 gridStepY = pexConfig.Field(
404 doc=
"""Spacing between subsequent grid cells in y direction. If equal to
405 cellSizeY, then there is no overlap in the y direction.""",
407 check=
lambda x: x > 0.
410 borderSizeX = pexConfig.Field(
412 doc=
"""Dimensions of grid cell border in +/- x direction, to be used
413 for generating `expandedSubExposure`.""",
415 check=
lambda x: x > 0.
418 borderSizeY = pexConfig.Field(
420 doc=
"""Dimensions of grid cell border in +/- y direction, to be used
421 for generating `expandedSubExposure`.""",
423 check=
lambda x: x > 0.
426 adjustGridOption = pexConfig.ChoiceField(
428 doc=
"""Whether and how to adjust grid to fit evenly within, and cover entire
432 "spacing":
"adjust spacing between centers of grid cells (allowing overlaps)",
433 "size":
"adjust the sizes of the grid cells (disallowing overlaps)",
434 "none":
"do not adjust the grid sizes or spacing"
438 scaleByFwhm = pexConfig.Field(
440 doc=
"""Scale cellSize/gridStep/borderSize/overlapSize by PSF FWHM rather
445 returnSubImages = pexConfig.Field(
447 doc=
"""Return the input subExposures alongside the processed ones (for debugging)""",
451 ignoreMaskPlanes = pexConfig.ListField(
453 doc=
"""Mask planes to ignore for sigma-clipped statistics""",
454 default=(
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
467 """Split an Exposure into subExposures (optionally on a grid) and
468 perform the same operation on each.
470 Perform 'simple' operations on a gridded set of subExposures of a
471 larger Exposure, and then (by default) have those subExposures
472 stitched back together into a new, full-sized image.
474 Contrary to the expectation given by its name, this task does not
475 perform these operations in parallel, although it could be updatd
476 to provide such functionality.
478 The actual operations are performed by two subTasks passed to the
479 config. The exposure passed to this task's `run` method will be
480 divided, and those subExposures will be passed to the subTasks,
481 along with the original exposure. The reducing operation is
482 performed by the second subtask.
484 ConfigClass = ImageMapReduceConfig
485 _DefaultName =
"ip_diffim_imageMapReduce"
488 """Create the image map-reduce task
493 arguments to be passed to
494 `lsst.pipe.base.task.Task.__init__`
496 additional keyword arguments to be passed to
497 `lsst.pipe.base.task.Task.__init__`
499 pipeBase.Task.__init__(self, *args, **kwargs)
502 self.makeSubtask(
"mapperSubtask")
503 self.makeSubtask(
"reducerSubtask")
506 def run(self, exposure, **kwargs):
507 """Perform a map-reduce operation on the given exposure.
509 Split the exposure into sub-expposures on a grid (parameters
510 given by `ImageMapReduceConfig`) and perform
511 `config.mapperSubtask.run()` on each. Reduce the resulting
512 sub-exposures by running `config.reducerSubtask.run()`.
516 exposure : lsst.afw.image.Exposure
517 the full exposure to process
519 additional keyword arguments to be passed to
520 subtask `run` methods
524 output of `reducerSubtask.run()`
527 self.log.info(
"Mapper sub-task: %s", self.mapperSubtask._DefaultName)
528 mapperResults = self.
_runMapper(exposure, **kwargs)
529 self.log.info(
"Reducer sub-task: %s", self.reducerSubtask._DefaultName)
530 result = self.
_reduceImage(mapperResults, exposure, **kwargs)
534 """Perform `mapperSubtask.run` on each sub-exposure
536 Perform `mapperSubtask.run` on each sub-exposure across a
537 grid on `exposure` generated by `_generateGrid`. Also pass to
538 `mapperSubtask.run` an 'expanded sub-exposure' containing the
539 same region as the sub-exposure but with an expanded bounding box.
543 exposure : lsst.afw.image.Exposure
544 the original exposure which is used as the template
546 if True, clone the subimages before passing to subtask;
547 in that case, the sub-exps do not have to be considered as read-only
549 additional keyword arguments to be passed to
550 `mapperSubtask.run` and `self._generateGrid`, including `forceEvenSized`.
554 a list of `pipeBase.Struct`s as returned by `mapperSubtask.run`.
559 raise ValueError(
'Bounding boxes list and expanded bounding boxes list are of different lengths')
561 self.log.info(
"Processing %d sub-exposures", len(self.
boxes0))
564 subExp = exposure.Factory(exposure, box0)
565 expandedSubExp = exposure.Factory(exposure, box1)
567 subExp = subExp.clone()
568 expandedSubExp = expandedSubExp.clone()
569 result = self.mapperSubtask.run(subExp, expandedSubExp, exposure.getBBox(), **kwargs)
570 if self.config.returnSubImages:
571 toAdd = pipeBase.Struct(inputSubExposure=subExp,
572 inputExpandedSubExposure=expandedSubExp)
573 result.mergeItems(toAdd,
'inputSubExposure',
'inputExpandedSubExposure')
574 mapperResults.append(result)
579 """Reduce/merge a set of sub-exposures into a final result
581 Return an exposure of the same dimensions as `exposure`.
582 `mapperResults` is expected to have been produced by `runMapper`.
587 list of `pipeBase.Struct`, each of which was produced by
588 `config.mapperSubtask`
589 exposure : lsst.afw.image.Exposure
590 the original exposure
592 additional keyword arguments
596 Output of `reducerSubtask.run` which is a `pipeBase.Struct`.
598 result = self.reducerSubtask.run(mapperResults, exposure, **kwargs)
602 """Generate two lists of bounding boxes that evenly grid `exposure`
604 Unless the config was provided with `cellCentroidsX` and
605 `cellCentroidsY`, grid (subimage) centers are spaced evenly
606 by gridStepX/Y. Then the grid is adjusted as little as
607 possible to evenly cover the input exposure (if
608 adjustGridOption is not 'none'). Then the second set of
609 bounding boxes is expanded by borderSizeX/Y. The expanded
610 bounding boxes are adjusted to ensure that they intersect the
611 exposure's bounding box. The resulting lists of bounding boxes
612 and corresponding expanded bounding boxes are set to
613 `self.boxes0`, `self.boxes1`.
617 exposure : lsst.afw.image.Exposure
618 input exposure whose full bounding box is to be evenly gridded.
619 forceEvenSized : boolean
620 force grid elements to have even-valued x- and y- dimensions?
621 (Potentially useful if doing Fourier transform of subExposures.)
625 bbox = exposure.getBBox()
628 cellCentroidsX = self.config.cellCentroidsX
629 cellCentroidsY = self.config.cellCentroidsY
630 cellSizeX = self.config.cellSizeX
631 cellSizeY = self.config.cellSizeY
632 gridStepX = self.config.gridStepX
633 gridStepY = self.config.gridStepY
634 borderSizeX = self.config.borderSizeX
635 borderSizeY = self.config.borderSizeY
636 adjustGridOption = self.config.adjustGridOption
637 scaleByFwhm = self.config.scaleByFwhm
639 if cellCentroidsX
is None or len(cellCentroidsX) <= 0:
642 psfFwhm = (exposure.getPsf().computeShape().getDeterminantRadius() *
643 2.*np.sqrt(2.*np.log(2.)))
645 self.log.info(
"Scaling grid parameters by %f" % psfFwhm)
647 def rescaleValue(val):
649 return np.rint(val*psfFwhm).astype(int)
651 return np.rint(val).astype(int)
653 cellSizeX = rescaleValue(cellSizeX)
654 cellSizeY = rescaleValue(cellSizeY)
655 gridStepX = rescaleValue(gridStepX)
656 gridStepY = rescaleValue(gridStepY)
657 borderSizeX = rescaleValue(borderSizeX)
658 borderSizeY = rescaleValue(borderSizeY)
660 nGridX = bbox.getWidth()//gridStepX
661 nGridY = bbox.getHeight()//gridStepY
663 if adjustGridOption ==
'spacing':
665 nGridX = bbox.getWidth()//cellSizeX + 1
666 nGridY = bbox.getHeight()//cellSizeY + 1
667 xLinSpace = np.linspace(cellSizeX//2, bbox.getWidth() - cellSizeX//2, nGridX)
668 yLinSpace = np.linspace(cellSizeY//2, bbox.getHeight() - cellSizeY//2, nGridY)
670 elif adjustGridOption ==
'size':
671 cellSizeX = gridStepX
672 cellSizeY = gridStepY
673 xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, cellSizeX)
674 yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, cellSizeY)
679 xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, gridStepX)
680 yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, gridStepY)
682 cellCentroids = [(x, y)
for x
in xLinSpace
for y
in yLinSpace]
686 cellCentroids = [(cellCentroidsX[i], cellCentroidsY[i])
for i
in range(len(cellCentroidsX))]
689 bbox0 = afwGeom.Box2I(afwGeom.Point2I(bbox.getBegin()), afwGeom.Extent2I(cellSizeX, cellSizeY))
691 bbox1 = afwGeom.Box2I(bbox0)
692 bbox1.grow(afwGeom.Extent2I(borderSizeX, borderSizeY))
697 def _makeBoxEvenSized(bb):
698 """Force a bounding-box to have dimensions that are modulo 2."""
700 if bb.getWidth() % 2 == 1:
701 bb.include(afwGeom.Point2I(bb.getMaxX()+1, bb.getMaxY()))
703 if bb.getWidth() % 2 == 1:
704 bb.include(afwGeom.Point2I(bb.getMinX()-1, bb.getMaxY()))
706 if bb.getHeight() % 2 == 1:
707 bb.include(afwGeom.Point2I(bb.getMaxX(), bb.getMaxY()+1))
709 if bb.getHeight() % 2 == 1:
710 bb.include(afwGeom.Point2I(bb.getMaxX(), bb.getMinY()-1))
712 if bb.getWidth() % 2 == 1
or bb.getHeight() % 2 == 1:
713 raise RuntimeError(
'Cannot make bounding box even-sized. Probably too big.')
718 if cellCentroids
is not None and len(cellCentroids) > 0:
719 for x, y
in cellCentroids:
720 centroid = afwGeom.Point2D(x, y)
721 bb0 = afwGeom.Box2I(bbox0)
722 xoff = int(np.floor(centroid.getX())) - bb0.getWidth()//2
723 yoff = int(np.floor(centroid.getY())) - bb0.getHeight()//2
724 bb0.shift(afwGeom.Extent2I(xoff, yoff))
727 bb0 = _makeBoxEvenSized(bb0)
728 bb1 = afwGeom.Box2I(bbox1)
729 bb1.shift(afwGeom.Extent2I(xoff, yoff))
732 bb1 = _makeBoxEvenSized(bb1)
734 if bb0.getArea() > 1
and bb1.getArea() > 1:
735 self.boxes0.append(bb0)
736 self.boxes1.append(bb1)
741 """Plot both grids of boxes using matplotlib.
743 Will compute the grid via `_generateGrid` if
744 `self.boxes0` and `self.boxes1` have not already been set.
748 exposure : lsst.afw.image.Exposure
749 Exposure whose bounding box is gridded by this task.
751 Plot every skip-ped box (help make plots less confusing)
753 import matplotlib.pyplot
as plt
756 raise RuntimeError(
'Cannot plot boxes. Run _generateGrid first.')
760 plt.gca().set_prop_cycle(
None)
764 """Plot a grid of boxes using matplotlib.
769 a list of `afwGeom.BoundingBox`es
770 bbox : afwGeom.BoundingBox
771 an overall bounding box
773 additional keyword arguments for matplotlib
775 import matplotlib.pyplot
as plt
778 corners = np.array([np.array([pt.getX(), pt.getY()])
for pt
in box.getCorners()])
779 corners = np.vstack([corners, corners[0, :]])
780 plt.plot(corners[:, 0], corners[:, 1], **kwargs)
784 plt.xlim(bbox.getBeginX(), bbox.getEndX())
785 plt.ylim(bbox.getBeginY(), bbox.getEndY())