29import lsst.meas.algorithms
as measAlg
31import lsst.pipe.base
as pipeBase
33__all__ = (
"ImageMapReduceTask",
"ImageMapReduceConfig",
34 "ImageMapper",
"ImageMapperConfig",
35 "ImageReducer",
"ImageReducerConfig")
38"""Tasks for processing an exposure via processing on
39multiple sub-exposures and then collecting the results
40to either re-stitch the sub-exposures back into a new
41exposure, or return summary results for each sub-exposure.
43This provides a framework for arbitrary mapper-reducer
44operations on an exposure by implementing simple operations in
45subTasks. It currently is not parallelized, although it could be in
46the future. It does enable operations such as spatially-mapped
47processing on a grid across an image, processing regions surrounding
48centroids (such as for PSF processing), etc.
50It is implemented as primary Task, `ImageMapReduceTask` which contains
51two subtasks, `ImageMapper` and `ImageReducer`.
52`ImageMapReduceTask` configures the centroids and sub-exposure
53dimensions 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
56centroids (`config.cellCentroidsX` and `config.cellCentroidsY`) and a
57single pair of bounding boxes defining their dimensions, or a set of
58parameters defining a regular grid of centroids (`config.gridStepX`
59and `config.gridStepY`).
61`ImageMapper` is an abstract class and must be subclassed with
62an implemented `run` method to provide the desired operation for
63processing individual sub-exposures. It is called from
64`ImageMapReduceTask.run`, and may return a new, processed sub-exposure
65which is to be "stitched" back into a new resulting larger exposure
66(depending on the configured `ImageMapReduceTask.mapper`);
67otherwise if it does not return an lsst.afw.image.Exposure, then the results are
68passed back directly to the caller.
70`ImageReducer` will either stitch the `mapperResults` list of
71results generated by the `ImageMapper` together into a new
72Exposure (by default) or pass it through to the
73caller. `ImageReducer` has an implemented `run` method for
74basic reducing operations (`reduceOperation`) such as `average` (which
75will average all overlapping pixels from sub-exposures produced by the
76`ImageMapper` into the new exposure). Another notable
77implemented `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
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`);
124 `ImageReducer.config.mapper.reduceOperation`
125 should be set to
'none' and the result will be propagated
131 the sub-exposure upon which to operate
133 the expanded sub-exposure upon which to operate
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
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`.
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`.
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'`.
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")
349 mycatalog = afwTable.ExposureCatalog(schema)
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()`.
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.
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
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
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.
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.
790 a list of bounding boxes.
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())
def plotBoxes(self, fullBBox, skip=3)
def _runMapper(self, exposure, doClone=False, **kwargs)
def _generateGrid(self, exposure, forceEvenSized=False, **kwargs)
def run(self, exposure, **kwargs)
def _reduceImage(self, mapperResults, exposure, **kwargs)
def __init__(self, *args, **kwargs)
def _plotBoxGrid(self, boxes, bbox, **kwargs)
def run(self, subExposure, expandedSubExposure, fullBBox, **kwargs)
def _constructPsf(self, mapperResults, exposure)
def run(self, mapperResults, exposure, **kwargs)