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""",
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.reducerSubtask`.
184 Basic reduce operations are provided by the `run` method
185 of this class, to be selected by its config.
187 ConfigClass = ImageReducerSubtaskConfig
188 _DefaultName =
"ip_diffim_ImageReducerSubtask"
190 def run(self, mapperResults, exposure, **kwargs):
191 """Reduce a list of items produced by `ImageMapperSubtask`.
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.
207 list of `pipeBase.Struct` returned by `ImageMapperSubtask.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.mapperSubtask.reduceOperation is not 'none')
213 additional keyword arguments propagated from
214 `ImageMapReduceTask.run`.
218 A `pipeBase.Struct` containing either an `lsst.afw.image.Exposure` (named 'exposure')
219 or a list (named 'result'), depending on `config.reduceOperation`.
223 1. This currently correctly handles overlapping sub-exposures.
224 For overlapping sub-exposures, use `config.reduceOperation='average'`.
225 2. This correctly handles varying PSFs, constructing the resulting
226 exposure's PSF via CoaddPsf (DM-9629).
230 1. To be done: correct handling of masks (nearly there)
231 2. This logic currently makes *two* copies of the original exposure
232 (one here and one in `mapperSubtask.run()`). Possibly of concern
233 for large images on memory-constrained systems.
236 if self.config.reduceOperation ==
'none':
237 return pipeBase.Struct(result=mapperResults)
239 if self.config.reduceOperation ==
'coaddPsf':
242 return pipeBase.Struct(result=coaddPsf)
244 newExp = exposure.clone()
245 newMI = newExp.getMaskedImage()
247 reduceOp = self.config.reduceOperation
248 if reduceOp ==
'copy':
250 newMI.getImage()[:, :] = np.nan
251 newMI.getVariance()[:, :] = np.nan
253 newMI.getImage()[:, :] = 0.
254 newMI.getVariance()[:, :] = 0.
255 if reduceOp ==
'average':
256 weights = afwImage.ImageI(newMI.getBBox())
258 for item
in mapperResults:
259 item = item.subExposure
260 if not (isinstance(item, afwImage.ExposureF)
or isinstance(item, afwImage.ExposureI)
or
261 isinstance(item, afwImage.ExposureU)
or isinstance(item, afwImage.ExposureD)):
262 raise TypeError(
"""Expecting an Exposure type, got %s.
263 Consider using `reduceOperation="none".""" % str(type(item)))
264 subExp = newExp.Factory(newExp, item.getBBox())
265 subMI = subExp.getMaskedImage()
266 patchMI = item.getMaskedImage()
267 isValid = ~np.isnan(patchMI.getImage().getArray() * patchMI.getVariance().getArray())
269 if reduceOp ==
'copy':
270 subMI.getImage().getArray()[isValid] = patchMI.getImage().getArray()[isValid]
271 subMI.getVariance().getArray()[isValid] = patchMI.getVariance().getArray()[isValid]
272 subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
274 if reduceOp ==
'sum' or reduceOp ==
'average':
275 subMI.getImage().getArray()[isValid] += patchMI.getImage().getArray()[isValid]
276 subMI.getVariance().getArray()[isValid] += patchMI.getVariance().getArray()[isValid]
277 subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
278 if reduceOp ==
'average':
280 wtsView = afwImage.ImageI(weights, item.getBBox())
281 wtsView.getArray()[isValid] += 1
284 mask = newMI.getMask()
285 for m
in self.config.badMaskPlanes:
287 bad = mask.getPlaneBitMask(self.config.badMaskPlanes)
289 isNan = np.where(np.isnan(newMI.getImage().getArray() * newMI.getVariance().getArray()))
290 if len(isNan[0]) > 0:
292 mask.getArray()[isNan[0], isNan[1]] |= bad
294 if reduceOp ==
'average':
295 wts = weights.getArray().astype(np.float)
296 self.log.info(
'AVERAGE: Maximum overlap: %f', np.nanmax(wts))
297 self.log.info(
'AVERAGE: Average overlap: %f', np.nanmean(wts))
298 self.log.info(
'AVERAGE: Minimum overlap: %f', np.nanmin(wts))
299 wtsZero = np.equal(wts, 0.)
300 wtsZeroInds = np.where(wtsZero)
301 wtsZeroSum = len(wtsZeroInds[0])
302 self.log.info(
'AVERAGE: Number of zero pixels: %f (%f%%)', wtsZeroSum,
303 wtsZeroSum * 100. / wtsZero.size)
304 notWtsZero = ~wtsZero
305 tmp = newMI.getImage().getArray()
306 np.divide(tmp, wts, out=tmp, where=notWtsZero)
307 tmp = newMI.getVariance().getArray()
308 np.divide(tmp, wts, out=tmp, where=notWtsZero)
309 if len(wtsZeroInds[0]) > 0:
310 newMI.getImage().getArray()[wtsZeroInds] = np.nan
311 newMI.getVariance().getArray()[wtsZeroInds] = np.nan
314 mask.getArray()[wtsZeroInds] |= bad
317 if reduceOp ==
'sum' or reduceOp ==
'average':
321 return pipeBase.Struct(exposure=newExp)
324 """Construct a CoaddPsf based on PSFs from individual subExposures
326 Currently uses (and returns) a CoaddPsf. TBD if we want to
327 create a custom subclass of CoaddPsf to differentiate it.
332 list of `pipeBase.Struct` returned by `ImageMapperSubtask.run`.
333 For this to work, each element of `mapperResults` must contain
334 a `subExposure` element, from which the component Psfs are
335 extracted (thus the reducerTask cannot have
336 `reduceOperation = 'none'`.
337 exposure : lsst.afw.image.Exposure
338 the original exposure which is used here solely for its
339 bounding-box and WCS.
343 A `measAlg.CoaddPsf` constructed from the PSFs of the individual
346 schema = afwTable.ExposureTable.makeMinimalSchema()
347 schema.addField(
"weight", type=
"D", doc=
"Coadd weight")
348 mycatalog = afwTable.ExposureCatalog(schema)
352 wcsref = exposure.getWcs()
353 for i, res
in enumerate(mapperResults):
354 record = mycatalog.getTable().makeRecord()
355 if 'subExposure' in res.getDict():
356 subExp = res.subExposure
357 if subExp.getWcs() != wcsref:
358 raise ValueError(
'Wcs of subExposure is different from exposure')
359 record.setPsf(subExp.getPsf())
360 record.setWcs(subExp.getWcs())
361 record.setBBox(subExp.getBBox())
362 elif 'psf' in res.getDict():
363 record.setPsf(res.psf)
364 record.setWcs(wcsref)
365 record.setBBox(res.bbox)
366 record[
'weight'] = 1.0
368 mycatalog.append(record)
371 psf = measAlg.CoaddPsf(mycatalog, wcsref,
'weight')
376 """Configuration parameters for the ImageMapReduceTask
378 mapperSubtask = pexConfig.ConfigurableField(
379 doc=
"Subtask to run on each subimage",
380 target=ImageMapperSubtask,
383 reducerSubtask = pexConfig.ConfigurableField(
384 doc=
"Subtask to combine results of mapperSubTask",
385 target=ImageReducerSubtask,
392 cellCentroidsX = pexConfig.ListField(
394 doc=
"""Input X centroids around which to place subimages.
395 If None, use grid config options below.""",
400 cellCentroidsY = pexConfig.ListField(
402 doc=
"""Input Y centroids around which to place subimages.
403 If None, use grid config options below.""",
408 cellSizeX = pexConfig.Field(
410 doc=
"""Dimensions of each grid cell in x direction""",
412 check=
lambda x: x > 0.
415 cellSizeY = pexConfig.Field(
417 doc=
"""Dimensions of each grid cell in y direction""",
419 check=
lambda x: x > 0.
422 gridStepX = pexConfig.Field(
424 doc=
"""Spacing between subsequent grid cells in x direction. If equal to
425 cellSizeX, then there is no overlap in the x direction.""",
427 check=
lambda x: x > 0.
430 gridStepY = pexConfig.Field(
432 doc=
"""Spacing between subsequent grid cells in y direction. If equal to
433 cellSizeY, then there is no overlap in the y direction.""",
435 check=
lambda x: x > 0.
438 borderSizeX = pexConfig.Field(
440 doc=
"""Dimensions of grid cell border in +/- x direction, to be used
441 for generating `expandedSubExposure`.""",
443 check=
lambda x: x > 0.
446 borderSizeY = pexConfig.Field(
448 doc=
"""Dimensions of grid cell border in +/- y direction, to be used
449 for generating `expandedSubExposure`.""",
451 check=
lambda x: x > 0.
454 adjustGridOption = pexConfig.ChoiceField(
456 doc=
"""Whether and how to adjust grid to fit evenly within, and cover entire
460 "spacing":
"adjust spacing between centers of grid cells (allowing overlaps)",
461 "size":
"adjust the sizes of the grid cells (disallowing overlaps)",
462 "none":
"do not adjust the grid sizes or spacing"
466 scaleByFwhm = pexConfig.Field(
468 doc=
"""Scale cellSize/gridStep/borderSize/overlapSize by PSF FWHM rather
473 returnSubImages = pexConfig.Field(
475 doc=
"""Return the input subExposures alongside the processed ones (for debugging)""",
479 ignoreMaskPlanes = pexConfig.ListField(
481 doc=
"""Mask planes to ignore for sigma-clipped statistics""",
482 default=(
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
495 """Split an Exposure into subExposures (optionally on a grid) and
496 perform the same operation on each.
498 Perform 'simple' operations on a gridded set of subExposures of a
499 larger Exposure, and then (by default) have those subExposures
500 stitched back together into a new, full-sized image.
502 Contrary to the expectation given by its name, this task does not
503 perform these operations in parallel, although it could be updatd
504 to provide such functionality.
506 The actual operations are performed by two subTasks passed to the
507 config. The exposure passed to this task's `run` method will be
508 divided, and those subExposures will be passed to the subTasks,
509 along with the original exposure. The reducing operation is
510 performed by the second subtask.
512 ConfigClass = ImageMapReduceConfig
513 _DefaultName =
"ip_diffim_imageMapReduce"
516 """Create the image map-reduce task
521 arguments to be passed to
522 `lsst.pipe.base.task.Task.__init__`
524 additional keyword arguments to be passed to
525 `lsst.pipe.base.task.Task.__init__`
527 pipeBase.Task.__init__(self, *args, **kwargs)
530 self.makeSubtask(
"mapperSubtask")
531 self.makeSubtask(
"reducerSubtask")
534 def run(self, exposure, **kwargs):
535 """Perform a map-reduce operation on the given exposure.
537 Split the exposure into sub-expposures on a grid (parameters
538 given by `ImageMapReduceConfig`) and perform
539 `config.mapperSubtask.run()` on each. Reduce the resulting
540 sub-exposures by running `config.reducerSubtask.run()`.
544 exposure : lsst.afw.image.Exposure
545 the full exposure to process
547 additional keyword arguments to be passed to
548 subtask `run` methods
552 output of `reducerSubtask.run()`
555 self.log.info(
"Mapper sub-task: %s", self.mapperSubtask._DefaultName)
556 mapperResults = self.
_runMapper(exposure, **kwargs)
557 self.log.info(
"Reducer sub-task: %s", self.reducerSubtask._DefaultName)
558 result = self.
_reduceImage(mapperResults, exposure, **kwargs)
562 """Perform `mapperSubtask.run` on each sub-exposure
564 Perform `mapperSubtask.run` on each sub-exposure across a
565 grid on `exposure` generated by `_generateGrid`. Also pass to
566 `mapperSubtask.run` an 'expanded sub-exposure' containing the
567 same region as the sub-exposure but with an expanded bounding box.
571 exposure : lsst.afw.image.Exposure
572 the original exposure which is used as the template
574 if True, clone the subimages before passing to subtask;
575 in that case, the sub-exps do not have to be considered as read-only
577 additional keyword arguments to be passed to
578 `mapperSubtask.run` and `self._generateGrid`, including `forceEvenSized`.
582 a list of `pipeBase.Struct`s as returned by `mapperSubtask.run`.
587 raise ValueError(
'Bounding boxes list and expanded bounding boxes list are of different lengths')
589 self.log.info(
"Processing %d sub-exposures", len(self.
boxes0))
592 subExp = exposure.Factory(exposure, box0)
593 expandedSubExp = exposure.Factory(exposure, box1)
595 subExp = subExp.clone()
596 expandedSubExp = expandedSubExp.clone()
597 result = self.mapperSubtask.run(subExp, expandedSubExp, exposure.getBBox(), **kwargs)
598 if self.config.returnSubImages:
599 toAdd = pipeBase.Struct(inputSubExposure=subExp,
600 inputExpandedSubExposure=expandedSubExp)
601 result.mergeItems(toAdd,
'inputSubExposure',
'inputExpandedSubExposure')
602 mapperResults.append(result)
607 """Reduce/merge a set of sub-exposures into a final result
609 Return an exposure of the same dimensions as `exposure`.
610 `mapperResults` is expected to have been produced by `runMapper`.
615 list of `pipeBase.Struct`, each of which was produced by
616 `config.mapperSubtask`
617 exposure : lsst.afw.image.Exposure
618 the original exposure
620 additional keyword arguments
624 Output of `reducerSubtask.run` which is a `pipeBase.Struct`.
626 result = self.reducerSubtask.run(mapperResults, exposure, **kwargs)
630 """Generate two lists of bounding boxes that evenly grid `exposure`
632 Unless the config was provided with `cellCentroidsX` and
633 `cellCentroidsY`, grid (subimage) centers are spaced evenly
634 by gridStepX/Y. Then the grid is adjusted as little as
635 possible to evenly cover the input exposure (if
636 adjustGridOption is not 'none'). Then the second set of
637 bounding boxes is expanded by borderSizeX/Y. The expanded
638 bounding boxes are adjusted to ensure that they intersect the
639 exposure's bounding box. The resulting lists of bounding boxes
640 and corresponding expanded bounding boxes are set to
641 `self.boxes0`, `self.boxes1`.
645 exposure : lsst.afw.image.Exposure
646 input exposure whose full bounding box is to be evenly gridded.
647 forceEvenSized : boolean
648 force grid elements to have even-valued x- and y- dimensions?
649 (Potentially useful if doing Fourier transform of subExposures.)
653 bbox = exposure.getBBox()
656 cellCentroidsX = self.config.cellCentroidsX
657 cellCentroidsY = self.config.cellCentroidsY
658 cellSizeX = self.config.cellSizeX
659 cellSizeY = self.config.cellSizeY
660 gridStepX = self.config.gridStepX
661 gridStepY = self.config.gridStepY
662 borderSizeX = self.config.borderSizeX
663 borderSizeY = self.config.borderSizeY
664 adjustGridOption = self.config.adjustGridOption
665 scaleByFwhm = self.config.scaleByFwhm
667 if cellCentroidsX
is None or len(cellCentroidsX) <= 0:
670 psfFwhm = (exposure.getPsf().computeShape().getDeterminantRadius() *
671 2.*np.sqrt(2.*np.log(2.)))
673 self.log.info(
"Scaling grid parameters by %f" % psfFwhm)
675 def rescaleValue(val):
677 return np.rint(val*psfFwhm).astype(int)
679 return np.rint(val).astype(int)
681 cellSizeX = rescaleValue(cellSizeX)
682 cellSizeY = rescaleValue(cellSizeY)
683 gridStepX = rescaleValue(gridStepX)
684 gridStepY = rescaleValue(gridStepY)
685 borderSizeX = rescaleValue(borderSizeX)
686 borderSizeY = rescaleValue(borderSizeY)
688 nGridX = bbox.getWidth()//gridStepX
689 nGridY = bbox.getHeight()//gridStepY
691 if adjustGridOption ==
'spacing':
693 nGridX = bbox.getWidth()//cellSizeX + 1
694 nGridY = bbox.getHeight()//cellSizeY + 1
695 xLinSpace = np.linspace(cellSizeX//2, bbox.getWidth() - cellSizeX//2, nGridX)
696 yLinSpace = np.linspace(cellSizeY//2, bbox.getHeight() - cellSizeY//2, nGridY)
698 elif adjustGridOption ==
'size':
699 cellSizeX = gridStepX
700 cellSizeY = gridStepY
701 xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, cellSizeX)
702 yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, cellSizeY)
707 xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, gridStepX)
708 yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, gridStepY)
710 cellCentroids = [(x, y)
for x
in xLinSpace
for y
in yLinSpace]
714 cellCentroids = [(cellCentroidsX[i], cellCentroidsY[i])
for i
in range(len(cellCentroidsX))]
717 bbox0 = afwGeom.Box2I(afwGeom.Point2I(bbox.getBegin()), afwGeom.Extent2I(cellSizeX, cellSizeY))
719 bbox1 = afwGeom.Box2I(bbox0)
720 bbox1.grow(afwGeom.Extent2I(borderSizeX, borderSizeY))
725 def _makeBoxEvenSized(bb):
726 """Force a bounding-box to have dimensions that are modulo 2."""
728 if bb.getWidth() % 2 == 1:
729 bb.include(afwGeom.Point2I(bb.getMaxX()+1, bb.getMaxY()))
731 if bb.getWidth() % 2 == 1:
732 bb.include(afwGeom.Point2I(bb.getMinX()-1, bb.getMaxY()))
734 if bb.getHeight() % 2 == 1:
735 bb.include(afwGeom.Point2I(bb.getMaxX(), bb.getMaxY()+1))
737 if bb.getHeight() % 2 == 1:
738 bb.include(afwGeom.Point2I(bb.getMaxX(), bb.getMinY()-1))
740 if bb.getWidth() % 2 == 1
or bb.getHeight() % 2 == 1:
741 raise RuntimeError(
'Cannot make bounding box even-sized. Probably too big.')
746 if cellCentroids
is not None and len(cellCentroids) > 0:
747 for x, y
in cellCentroids:
748 centroid = afwGeom.Point2D(x, y)
749 bb0 = afwGeom.Box2I(bbox0)
750 xoff = int(np.floor(centroid.getX())) - bb0.getWidth()//2
751 yoff = int(np.floor(centroid.getY())) - bb0.getHeight()//2
752 bb0.shift(afwGeom.Extent2I(xoff, yoff))
755 bb0 = _makeBoxEvenSized(bb0)
756 bb1 = afwGeom.Box2I(bbox1)
757 bb1.shift(afwGeom.Extent2I(xoff, yoff))
760 bb1 = _makeBoxEvenSized(bb1)
762 if bb0.getArea() > 1
and bb1.getArea() > 1:
763 self.boxes0.append(bb0)
764 self.boxes1.append(bb1)
769 """Plot both grids of boxes using matplotlib.
771 Will compute the grid via `_generateGrid` if
772 `self.boxes0` and `self.boxes1` have not already been set.
776 exposure : lsst.afw.image.Exposure
777 Exposure whose bounding box is gridded by this task.
779 Plot every skip-ped box (help make plots less confusing)
781 import matplotlib.pyplot
as plt
784 raise RuntimeError(
'Cannot plot boxes. Run _generateGrid first.')
788 plt.gca().set_prop_cycle(
None)
792 """Plot a grid of boxes using matplotlib.
797 a list of `afwGeom.BoundingBox`es
798 bbox : afwGeom.BoundingBox
799 an overall bounding box
801 additional keyword arguments for matplotlib
803 import matplotlib.pyplot
as plt
806 corners = np.array([np.array([pt.getX(), pt.getY()])
for pt
in box.getCorners()])
807 corners = np.vstack([corners, corners[0, :]])
808 plt.plot(corners[:, 0], corners[:, 1], **kwargs)
812 plt.xlim(bbox.getBeginX(), bbox.getEndX())
813 plt.ylim(bbox.getBeginY(), bbox.getEndY())