30 import lsst.pex.config 
as pexConfig
 
   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())