lsst.ip.diffim  15.0-7-gbf600c9+2
imageMapReduce.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 import numpy as np
24 import abc
25 
26 import lsst.afw.image as afwImage
27 import lsst.afw.geom as afwGeom
28 import lsst.afw.table as afwTable
29 import lsst.meas.algorithms as measAlg
30 import lsst.pex.config as pexConfig
31 import lsst.pipe.base as pipeBase
32 
33 __all__ = ("ImageMapReduceTask", "ImageMapReduceConfig",
34  "ImageMapper", "ImageMapperConfig",
35  "ImageReducer", "ImageReducerConfig")
36 
37 
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.
42 
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.
49 
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`).
60 
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.
69 
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.
79 """
80 
81 
82 class ImageMapperConfig(pexConfig.Config):
83  """Configuration parameters for ImageMapper
84  """
85  pass
86 
87 
88 class ImageMapper(pipeBase.Task, metaclass=abc.ABCMeta):
89  """Abstract base class for any task that is to be
90  used as `ImageMapReduceConfig.mapper`.
91 
92  An `ImageMapper` is responsible for processing individual
93  sub-exposures in its `run` method, which is called from
94  `ImageMapReduceTask.run`. `run` may return a processed new
95  sub-exposure which can be be "stitched" back into a new resulting
96  larger exposure (depending on the configured
97  `ImageReducer`); otherwise if it does not return an
98  lsst.afw.image.Exposure, then the
99  `ImageReducer.config.reducer.reduceOperation`
100  should be set to 'none' and the result will be propagated
101  as-is.
102  """
103  ConfigClass = ImageMapperConfig
104  _DefaultName = "ip_diffim_ImageMapper"
105 
106  @abc.abstractmethod
107  def run(self, subExposure, expandedSubExposure, fullBBox, **kwargs):
108  """Perform operation on `subExposure`.
109 
110  To be implemented by subclasses. See class docstring for more
111  details. This method is given the `subExposure` which
112  is to be operated upon, and an `expandedSubExposure` which
113  will contain `subExposure` with additional surrounding
114  pixels. This allows for, for example, convolutions (which
115  should be performed on `expandedSubExposure`), to prevent the
116  returned sub-exposure from containing invalid pixels.
117 
118  This method may return a new, processed sub-exposure which can
119  be be "stitched" back into a new resulting larger exposure
120  (depending on the paired, configured `ImageReducer`);
121  otherwise if it does not return an lsst.afw.image.Exposure, then the
122  `ImageReducer.config.mapper.reduceOperation`
123  should be set to 'none' and the result will be propagated
124  as-is.
125 
126  Parameters
127  ----------
128  subExposure : lsst.afw.image.Exposure
129  the sub-exposure upon which to operate
130  expandedSubExposure : lsst.afw.image.Exposure
131  the expanded sub-exposure upon which to operate
132  fullBBox : lsst.afw.geom.BoundingBox
133  the bounding box of the original exposure
134  kwargs :
135  additional keyword arguments propagated from
136  `ImageMapReduceTask.run`.
137 
138  Returns
139  -------
140  A `pipeBase.Struct containing the result of the `subExposure` processing,
141  which may itself be of any type. See above for details. If it is an
142  lsst.afw.image.Exposure (processed sub-exposure), then the name in the Struct
143  should be 'subExposure'. This is implemented here as a pass-through
144  example only.
145  """
146  return pipeBase.Struct(subExposure=subExposure)
147 
148 
149 class ImageReducerConfig(pexConfig.Config):
150  """Configuration parameters for the ImageReducer
151  """
152  reduceOperation = pexConfig.ChoiceField(
153  dtype=str,
154  doc="""Operation to use for reducing subimages into new image.""",
155  default="average",
156  allowed={
157  "none": """simply return a list of values and don't re-map results into
158  a new image (noop operation)""",
159  "copy": """copy pixels directly from subimage into correct location in
160  new exposure (potentially non-deterministic for overlaps)""",
161  "sum": """add pixels from overlaps (probably never wanted; used for testing)
162  into correct location in new exposure""",
163  "average": """same as copy, but also average pixels from overlapped regions
164  (NaNs ignored)""",
165  "coaddPsf": """Instead of constructing an Exposure, take a list of returned
166  PSFs and use CoaddPsf to construct a single PSF that covers the
167  entire input exposure""",
168  }
169  )
170  badMaskPlanes = pexConfig.ListField(
171  dtype=str,
172  doc="""Mask planes to set for invalid pixels""",
173  default=('INVALID_MAPREDUCE', 'BAD', 'NO_DATA')
174  )
175 
176 
177 class ImageReducer(pipeBase.Task):
178  """Base class for any 'reduce' task that is to be
179  used as `ImageMapReduceConfig.reducer`.
180 
181  Basic reduce operations are provided by the `run` method
182  of this class, to be selected by its config.
183  """
184  ConfigClass = ImageReducerConfig
185  _DefaultName = "ip_diffim_ImageReducer"
186 
187  def run(self, mapperResults, exposure, **kwargs):
188  """Reduce a list of items produced by `ImageMapper`.
189 
190  Either stitch the passed `mapperResults` list
191  together into a new Exposure (default) or pass it through
192  (if `self.config.reduceOperation` is 'none').
193 
194  If `self.config.reduceOperation` is not 'none', then expect
195  that the `pipeBase.Struct`s in the `mapperResults` list
196  contain sub-exposures named 'subExposure', to be stitched back
197  into a single Exposure with the same dimensions, PSF, and mask
198  as the input `exposure`. Otherwise, the `mapperResults` list
199  is simply returned directly.
200 
201  Parameters
202  ----------
203  mapperResults : list
204  list of `pipeBase.Struct` returned by `ImageMapper.run`.
205  exposure : lsst.afw.image.Exposure
206  the original exposure which is cloned to use as the
207  basis for the resulting exposure (if
208  self.config.mapper.reduceOperation is not 'none')
209  kwargs :
210  additional keyword arguments propagated from
211  `ImageMapReduceTask.run`.
212 
213  Returns
214  -------
215  A `pipeBase.Struct` containing either an `lsst.afw.image.Exposure` (named 'exposure')
216  or a list (named 'result'), depending on `config.reduceOperation`.
217 
218  Notes
219  -----
220  1. This currently correctly handles overlapping sub-exposures.
221  For overlapping sub-exposures, use `config.reduceOperation='average'`.
222  2. This correctly handles varying PSFs, constructing the resulting
223  exposure's PSF via CoaddPsf (DM-9629).
224 
225  Known issues
226  ------------
227  1. To be done: correct handling of masks (nearly there)
228  2. This logic currently makes *two* copies of the original exposure
229  (one here and one in `mapper.run()`). Possibly of concern
230  for large images on memory-constrained systems.
231  """
232  # No-op; simply pass mapperResults directly to ImageMapReduceTask.run
233  if self.config.reduceOperation == 'none':
234  return pipeBase.Struct(result=mapperResults)
235 
236  if self.config.reduceOperation == 'coaddPsf':
237  # Each element of `mapperResults` should contain 'psf' and 'bbox'
238  coaddPsf = self._constructPsf(mapperResults, exposure)
239  return pipeBase.Struct(result=coaddPsf)
240 
241  newExp = exposure.clone()
242  newMI = newExp.getMaskedImage()
243 
244  reduceOp = self.config.reduceOperation
245  if reduceOp == 'copy':
246  weights = None
247  newMI.getImage()[:, :] = np.nan
248  newMI.getVariance()[:, :] = np.nan
249  else:
250  newMI.getImage()[:, :] = 0.
251  newMI.getVariance()[:, :] = 0.
252  if reduceOp == 'average': # make an array to keep track of weights
253  weights = afwImage.ImageI(newMI.getBBox())
254 
255  for item in mapperResults:
256  item = item.subExposure # Expected named value in the pipeBase.Struct
257  if not (isinstance(item, afwImage.ExposureF) or isinstance(item, afwImage.ExposureI) or
258  isinstance(item, afwImage.ExposureU) or isinstance(item, afwImage.ExposureD)):
259  raise TypeError("""Expecting an Exposure type, got %s.
260  Consider using `reduceOperation="none".""" % str(type(item)))
261  subExp = newExp.Factory(newExp, item.getBBox())
262  subMI = subExp.getMaskedImage()
263  patchMI = item.getMaskedImage()
264  isValid = ~np.isnan(patchMI.getImage().getArray() * patchMI.getVariance().getArray())
265 
266  if reduceOp == 'copy':
267  subMI.getImage().getArray()[isValid] = patchMI.getImage().getArray()[isValid]
268  subMI.getVariance().getArray()[isValid] = patchMI.getVariance().getArray()[isValid]
269  subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
270 
271  if reduceOp == 'sum' or reduceOp == 'average': # much of these two options is the same
272  subMI.getImage().getArray()[isValid] += patchMI.getImage().getArray()[isValid]
273  subMI.getVariance().getArray()[isValid] += patchMI.getVariance().getArray()[isValid]
274  subMI.getMask().getArray()[:, :] |= patchMI.getMask().getArray()
275  if reduceOp == 'average':
276  # wtsView is a view into the `weights` Image
277  wtsView = afwImage.ImageI(weights, item.getBBox())
278  wtsView.getArray()[isValid] += 1
279 
280  # New mask plane - for debugging map-reduced images
281  mask = newMI.getMask()
282  for m in self.config.badMaskPlanes:
283  mask.addMaskPlane(m)
284  bad = mask.getPlaneBitMask(self.config.badMaskPlanes)
285 
286  isNan = np.where(np.isnan(newMI.getImage().getArray() * newMI.getVariance().getArray()))
287  if len(isNan[0]) > 0:
288  # set mask to INVALID for pixels where produced exposure is NaN
289  mask.getArray()[isNan[0], isNan[1]] |= bad
290 
291  if reduceOp == 'average':
292  wts = weights.getArray().astype(np.float)
293  self.log.info('AVERAGE: Maximum overlap: %f', np.nanmax(wts))
294  self.log.info('AVERAGE: Average overlap: %f', np.nanmean(wts))
295  self.log.info('AVERAGE: Minimum overlap: %f', np.nanmin(wts))
296  wtsZero = np.equal(wts, 0.)
297  wtsZeroInds = np.where(wtsZero)
298  wtsZeroSum = len(wtsZeroInds[0])
299  self.log.info('AVERAGE: Number of zero pixels: %f (%f%%)', wtsZeroSum,
300  wtsZeroSum * 100. / wtsZero.size)
301  notWtsZero = ~wtsZero
302  tmp = newMI.getImage().getArray()
303  np.divide(tmp, wts, out=tmp, where=notWtsZero)
304  tmp = newMI.getVariance().getArray()
305  np.divide(tmp, wts, out=tmp, where=notWtsZero)
306  if len(wtsZeroInds[0]) > 0:
307  newMI.getImage().getArray()[wtsZeroInds] = np.nan
308  newMI.getVariance().getArray()[wtsZeroInds] = np.nan
309  # set mask to something for pixels where wts == 0.
310  # happens sometimes if operation failed on a certain subexposure
311  mask.getArray()[wtsZeroInds] |= bad
312 
313  # Not sure how to construct a PSF when reduceOp=='copy'...
314  if reduceOp == 'sum' or reduceOp == 'average':
315  psf = self._constructPsf(mapperResults, exposure)
316  newExp.setPsf(psf)
317 
318  return pipeBase.Struct(exposure=newExp)
319 
320  def _constructPsf(self, mapperResults, exposure):
321  """Construct a CoaddPsf based on PSFs from individual subExposures
322 
323  Currently uses (and returns) a CoaddPsf. TBD if we want to
324  create a custom subclass of CoaddPsf to differentiate it.
325 
326  Parameters
327  ----------
328  mapperResults : list
329  list of `pipeBase.Struct` returned by `ImageMapper.run`.
330  For this to work, each element of `mapperResults` must contain
331  a `subExposure` element, from which the component Psfs are
332  extracted (thus the reducerTask cannot have
333  `reduceOperation = 'none'`.
334  exposure : lsst.afw.image.Exposure
335  the original exposure which is used here solely for its
336  bounding-box and WCS.
337 
338  Returns
339  -------
340  A `measAlg.CoaddPsf` constructed from the PSFs of the individual
341  subExposures.
342  """
343  schema = afwTable.ExposureTable.makeMinimalSchema()
344  schema.addField("weight", type="D", doc="Coadd weight")
345  mycatalog = afwTable.ExposureCatalog(schema)
346 
347  # We're just using the exposure's WCS (assuming that the subExposures'
348  # WCSs are the same, which they better be!).
349  wcsref = exposure.getWcs()
350  for i, res in enumerate(mapperResults):
351  record = mycatalog.getTable().makeRecord()
352  if 'subExposure' in res.getDict():
353  subExp = res.subExposure
354  if subExp.getWcs() != wcsref:
355  raise ValueError('Wcs of subExposure is different from exposure')
356  record.setPsf(subExp.getPsf())
357  record.setWcs(subExp.getWcs())
358  record.setBBox(subExp.getBBox())
359  elif 'psf' in res.getDict():
360  record.setPsf(res.psf)
361  record.setWcs(wcsref)
362  record.setBBox(res.bbox)
363  record['weight'] = 1.0
364  record['id'] = i
365  mycatalog.append(record)
366 
367  # create the coaddpsf
368  psf = measAlg.CoaddPsf(mycatalog, wcsref, 'weight')
369  return psf
370 
371 
372 class ImageMapReduceConfig(pexConfig.Config):
373  """Configuration parameters for the ImageMapReduceTask
374  """
375  mapper = pexConfig.ConfigurableField(
376  doc="Task to run on each subimage",
377  target=ImageMapper,
378  )
379 
380  reducer = pexConfig.ConfigurableField(
381  doc="Task to combine results of mapper task",
382  target=ImageReducer,
383  )
384 
385  # Separate cellCentroidsX and cellCentroidsY since pexConfig.ListField accepts limited dtypes
386  # (i.e., no Point2D). The resulting set of centroids is the "vertical stack" of
387  # `cellCentroidsX` and `cellCentroidsY`, i.e. for (1,2), (3,4) respectively, the
388  # resulting centroids are ((1,3), (2,4)).
389  cellCentroidsX = pexConfig.ListField(
390  dtype=float,
391  doc="""Input X centroids around which to place subimages.
392  If None, use grid config options below.""",
393  optional=True,
394  default=None
395  )
396 
397  cellCentroidsY = pexConfig.ListField(
398  dtype=float,
399  doc="""Input Y centroids around which to place subimages.
400  If None, use grid config options below.""",
401  optional=True,
402  default=None
403  )
404 
405  cellSizeX = pexConfig.Field(
406  dtype=float,
407  doc="""Dimensions of each grid cell in x direction""",
408  default=10.,
409  check=lambda x: x > 0.
410  )
411 
412  cellSizeY = pexConfig.Field(
413  dtype=float,
414  doc="""Dimensions of each grid cell in y direction""",
415  default=10.,
416  check=lambda x: x > 0.
417  )
418 
419  gridStepX = pexConfig.Field(
420  dtype=float,
421  doc="""Spacing between subsequent grid cells in x direction. If equal to
422  cellSizeX, then there is no overlap in the x direction.""",
423  default=10.,
424  check=lambda x: x > 0.
425  )
426 
427  gridStepY = pexConfig.Field(
428  dtype=float,
429  doc="""Spacing between subsequent grid cells in y direction. If equal to
430  cellSizeY, then there is no overlap in the y direction.""",
431  default=10.,
432  check=lambda x: x > 0.
433  )
434 
435  borderSizeX = pexConfig.Field(
436  dtype=float,
437  doc="""Dimensions of grid cell border in +/- x direction, to be used
438  for generating `expandedSubExposure`.""",
439  default=5.,
440  check=lambda x: x > 0.
441  )
442 
443  borderSizeY = pexConfig.Field(
444  dtype=float,
445  doc="""Dimensions of grid cell border in +/- y direction, to be used
446  for generating `expandedSubExposure`.""",
447  default=5.,
448  check=lambda x: x > 0.
449  )
450 
451  adjustGridOption = pexConfig.ChoiceField(
452  dtype=str,
453  doc="""Whether and how to adjust grid to fit evenly within, and cover entire
454  image""",
455  default="spacing",
456  allowed={
457  "spacing": "adjust spacing between centers of grid cells (allowing overlaps)",
458  "size": "adjust the sizes of the grid cells (disallowing overlaps)",
459  "none": "do not adjust the grid sizes or spacing"
460  }
461  )
462 
463  scaleByFwhm = pexConfig.Field(
464  dtype=bool,
465  doc="""Scale cellSize/gridStep/borderSize/overlapSize by PSF FWHM rather
466  than pixels?""",
467  default=True
468  )
469 
470  returnSubImages = pexConfig.Field(
471  dtype=bool,
472  doc="""Return the input subExposures alongside the processed ones (for debugging)""",
473  default=False
474  )
475 
476  ignoreMaskPlanes = pexConfig.ListField(
477  dtype=str,
478  doc="""Mask planes to ignore for sigma-clipped statistics""",
479  default=("INTRP", "EDGE", "DETECTED", "SAT", "CR", "BAD", "NO_DATA", "DETECTED_NEGATIVE")
480  )
481 
482 
483 
489 
490 
491 class ImageMapReduceTask(pipeBase.Task):
492  """Split an Exposure into subExposures (optionally on a grid) and
493  perform the same operation on each.
494 
495  Perform 'simple' operations on a gridded set of subExposures of a
496  larger Exposure, and then (by default) have those subExposures
497  stitched back together into a new, full-sized image.
498 
499  Contrary to the expectation given by its name, this task does not
500  perform these operations in parallel, although it could be updatd
501  to provide such functionality.
502 
503  The actual operations are performed by two subTasks passed to the
504  config. The exposure passed to this task's `run` method will be
505  divided, and those subExposures will be passed to the subTasks,
506  along with the original exposure. The reducing operation is
507  performed by the second subtask.
508  """
509  ConfigClass = ImageMapReduceConfig
510  _DefaultName = "ip_diffim_imageMapReduce"
511 
512  def __init__(self, *args, **kwargs):
513  """Create the image map-reduce task
514 
515  Parameters
516  ----------
517  args :
518  arguments to be passed to
519  `lsst.pipe.base.task.Task.__init__`
520  kwargs :
521  additional keyword arguments to be passed to
522  `lsst.pipe.base.task.Task.__init__`
523  """
524  pipeBase.Task.__init__(self, *args, **kwargs)
525 
526  self.boxes0 = self.boxes1 = None
527  self.makeSubtask("mapper")
528  self.makeSubtask("reducer")
529 
530  @pipeBase.timeMethod
531  def run(self, exposure, **kwargs):
532  """Perform a map-reduce operation on the given exposure.
533 
534  Split the exposure into sub-expposures on a grid (parameters
535  given by `ImageMapReduceConfig`) and perform
536  `config.mapper.run()` on each. Reduce the resulting
537  sub-exposures by running `config.reducer.run()`.
538 
539  Parameters
540  ----------
541  exposure : lsst.afw.image.Exposure
542  the full exposure to process
543  kwargs :
544  additional keyword arguments to be passed to
545  subtask `run` methods
546 
547  Returns
548  -------
549  output of `reducer.run()`
550 
551  """
552  self.log.info("Mapper sub-task: %s", self.mapper._DefaultName)
553  mapperResults = self._runMapper(exposure, **kwargs)
554  self.log.info("Reducer sub-task: %s", self.reducer._DefaultName)
555  result = self._reduceImage(mapperResults, exposure, **kwargs)
556  return result
557 
558  def _runMapper(self, exposure, doClone=False, **kwargs):
559  """Perform `mapper.run` on each sub-exposure
560 
561  Perform `mapper.run` on each sub-exposure across a
562  grid on `exposure` generated by `_generateGrid`. Also pass to
563  `mapper.run` an 'expanded sub-exposure' containing the
564  same region as the sub-exposure but with an expanded bounding box.
565 
566  Parameters
567  ----------
568  exposure : lsst.afw.image.Exposure
569  the original exposure which is used as the template
570  doClone : boolean
571  if True, clone the subimages before passing to subtask;
572  in that case, the sub-exps do not have to be considered as read-only
573  kwargs :
574  additional keyword arguments to be passed to
575  `mapper.run` and `self._generateGrid`, including `forceEvenSized`.
576 
577  Returns
578  -------
579  a list of `pipeBase.Struct`s as returned by `mapper.run`.
580  """
581  if self.boxes0 is None:
582  self._generateGrid(exposure, **kwargs) # possibly pass `forceEvenSized`
583  if len(self.boxes0) != len(self.boxes1):
584  raise ValueError('Bounding boxes list and expanded bounding boxes list are of different lengths')
585 
586  self.log.info("Processing %d sub-exposures", len(self.boxes0))
587  mapperResults = []
588  for box0, box1 in zip(self.boxes0, self.boxes1):
589  subExp = exposure.Factory(exposure, box0)
590  expandedSubExp = exposure.Factory(exposure, box1)
591  if doClone:
592  subExp = subExp.clone()
593  expandedSubExp = expandedSubExp.clone()
594  result = self.mapper.run(subExp, expandedSubExp, exposure.getBBox(), **kwargs)
595  if self.config.returnSubImages:
596  toAdd = pipeBase.Struct(inputSubExposure=subExp,
597  inputExpandedSubExposure=expandedSubExp)
598  result.mergeItems(toAdd, 'inputSubExposure', 'inputExpandedSubExposure')
599  mapperResults.append(result)
600 
601  return mapperResults
602 
603  def _reduceImage(self, mapperResults, exposure, **kwargs):
604  """Reduce/merge a set of sub-exposures into a final result
605 
606  Return an exposure of the same dimensions as `exposure`.
607  `mapperResults` is expected to have been produced by `runMapper`.
608 
609  Parameters
610  ----------
611  mapperResults : list
612  list of `pipeBase.Struct`, each of which was produced by
613  `config.mapper`
614  exposure : lsst.afw.image.Exposure
615  the original exposure
616  **kwargs :
617  additional keyword arguments
618 
619  Returns
620  -------
621  Output of `reducer.run` which is a `pipeBase.Struct`.
622  """
623  result = self.reducer.run(mapperResults, exposure, **kwargs)
624  return result
625 
626  def _generateGrid(self, exposure, forceEvenSized=False, **kwargs):
627  """Generate two lists of bounding boxes that evenly grid `exposure`
628 
629  Unless the config was provided with `cellCentroidsX` and
630  `cellCentroidsY`, grid (subimage) centers are spaced evenly
631  by gridStepX/Y. Then the grid is adjusted as little as
632  possible to evenly cover the input exposure (if
633  adjustGridOption is not 'none'). Then the second set of
634  bounding boxes is expanded by borderSizeX/Y. The expanded
635  bounding boxes are adjusted to ensure that they intersect the
636  exposure's bounding box. The resulting lists of bounding boxes
637  and corresponding expanded bounding boxes are set to
638  `self.boxes0`, `self.boxes1`.
639 
640  Parameters
641  ----------
642  exposure : lsst.afw.image.Exposure
643  input exposure whose full bounding box is to be evenly gridded.
644  forceEvenSized : boolean
645  force grid elements to have even-valued x- and y- dimensions?
646  (Potentially useful if doing Fourier transform of subExposures.)
647  """
648  # kwargs are ignored, but necessary to enable optional passing of
649  # `forceEvenSized` from `_runMapper`.
650  bbox = exposure.getBBox()
651 
652  # Extract the config parameters for conciseness.
653  cellCentroidsX = self.config.cellCentroidsX
654  cellCentroidsY = self.config.cellCentroidsY
655  cellSizeX = self.config.cellSizeX
656  cellSizeY = self.config.cellSizeY
657  gridStepX = self.config.gridStepX
658  gridStepY = self.config.gridStepY
659  borderSizeX = self.config.borderSizeX
660  borderSizeY = self.config.borderSizeY
661  adjustGridOption = self.config.adjustGridOption
662  scaleByFwhm = self.config.scaleByFwhm
663 
664  if cellCentroidsX is None or len(cellCentroidsX) <= 0:
665  # Not given centroids; construct them from cellSize/gridStep
666 
667  psfFwhm = (exposure.getPsf().computeShape().getDeterminantRadius() *
668  2.*np.sqrt(2.*np.log(2.)))
669  if scaleByFwhm:
670  self.log.info("Scaling grid parameters by %f" % psfFwhm)
671 
672  def rescaleValue(val):
673  if scaleByFwhm:
674  return np.rint(val*psfFwhm).astype(int)
675  else:
676  return np.rint(val).astype(int)
677 
678  cellSizeX = rescaleValue(cellSizeX)
679  cellSizeY = rescaleValue(cellSizeY)
680  gridStepX = rescaleValue(gridStepX)
681  gridStepY = rescaleValue(gridStepY)
682  borderSizeX = rescaleValue(borderSizeX)
683  borderSizeY = rescaleValue(borderSizeY)
684 
685  nGridX = bbox.getWidth()//gridStepX
686  nGridY = bbox.getHeight()//gridStepY
687 
688  if adjustGridOption == 'spacing':
689  # Readjust spacings so that they fit perfectly in the image.
690  nGridX = bbox.getWidth()//cellSizeX + 1
691  nGridY = bbox.getHeight()//cellSizeY + 1
692  xLinSpace = np.linspace(cellSizeX//2, bbox.getWidth() - cellSizeX//2, nGridX)
693  yLinSpace = np.linspace(cellSizeY//2, bbox.getHeight() - cellSizeY//2, nGridY)
694 
695  elif adjustGridOption == 'size':
696  cellSizeX = gridStepX
697  cellSizeY = gridStepY
698  xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, cellSizeX)
699  yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, cellSizeY)
700  cellSizeX += 1 # add 1 to make sure there are no gaps
701  cellSizeY += 1
702 
703  else:
704  xLinSpace = np.arange(cellSizeX//2, bbox.getWidth() + cellSizeX//2, gridStepX)
705  yLinSpace = np.arange(cellSizeY//2, bbox.getHeight() + cellSizeY//2, gridStepY)
706 
707  cellCentroids = [(x, y) for x in xLinSpace for y in yLinSpace]
708 
709  else:
710  # in py3 zip returns an iterator, but want to test length below, so use this instead:
711  cellCentroids = [(cellCentroidsX[i], cellCentroidsY[i]) for i in range(len(cellCentroidsX))]
712 
713  # first "main" box at 0,0
714  bbox0 = afwGeom.Box2I(afwGeom.Point2I(bbox.getBegin()), afwGeom.Extent2I(cellSizeX, cellSizeY))
715  # first expanded box
716  bbox1 = afwGeom.Box2I(bbox0)
717  bbox1.grow(afwGeom.Extent2I(borderSizeX, borderSizeY))
718 
719  self.boxes0 = [] # "main" boxes; store in task so can be extracted if needed
720  self.boxes1 = [] # "expanded" boxes
721 
722  def _makeBoxEvenSized(bb):
723  """Force a bounding-box to have dimensions that are modulo 2."""
724 
725  if bb.getWidth() % 2 == 1: # grow to the right
726  bb.include(afwGeom.Point2I(bb.getMaxX()+1, bb.getMaxY())) # Expand by 1 pixel!
727  bb.clip(bbox)
728  if bb.getWidth() % 2 == 1: # clipped at right -- so grow to the left
729  bb.include(afwGeom.Point2I(bb.getMinX()-1, bb.getMaxY()))
730  bb.clip(bbox)
731  if bb.getHeight() % 2 == 1: # grow upwards
732  bb.include(afwGeom.Point2I(bb.getMaxX(), bb.getMaxY()+1)) # Expand by 1 pixel!
733  bb.clip(bbox)
734  if bb.getHeight() % 2 == 1: # clipped upwards -- so grow down
735  bb.include(afwGeom.Point2I(bb.getMaxX(), bb.getMinY()-1))
736  bb.clip(bbox)
737  if bb.getWidth() % 2 == 1 or bb.getHeight() % 2 == 1: # Box is probably too big
738  raise RuntimeError('Cannot make bounding box even-sized. Probably too big.')
739 
740  return bb
741 
742  # Use given or grid-parameterized centroids as centers for bounding boxes
743  if cellCentroids is not None and len(cellCentroids) > 0:
744  for x, y in cellCentroids:
745  centroid = afwGeom.Point2D(x, y)
746  bb0 = afwGeom.Box2I(bbox0)
747  xoff = int(np.floor(centroid.getX())) - bb0.getWidth()//2
748  yoff = int(np.floor(centroid.getY())) - bb0.getHeight()//2
749  bb0.shift(afwGeom.Extent2I(xoff, yoff))
750  bb0.clip(bbox)
751  if forceEvenSized:
752  bb0 = _makeBoxEvenSized(bb0)
753  bb1 = afwGeom.Box2I(bbox1)
754  bb1.shift(afwGeom.Extent2I(xoff, yoff))
755  bb1.clip(bbox)
756  if forceEvenSized:
757  bb1 = _makeBoxEvenSized(bb1)
758 
759  if bb0.getArea() > 1 and bb1.getArea() > 1:
760  self.boxes0.append(bb0)
761  self.boxes1.append(bb1)
762 
763  return self.boxes0, self.boxes1
764 
765  def plotBoxes(self, fullBBox, skip=3):
766  """Plot both grids of boxes using matplotlib.
767 
768  Will compute the grid via `_generateGrid` if
769  `self.boxes0` and `self.boxes1` have not already been set.
770 
771  Parameters
772  ----------
773  exposure : lsst.afw.image.Exposure
774  Exposure whose bounding box is gridded by this task.
775  skip : int
776  Plot every skip-ped box (help make plots less confusing)
777  """
778  import matplotlib.pyplot as plt
779 
780  if self.boxes0 is None:
781  raise RuntimeError('Cannot plot boxes. Run _generateGrid first.')
782  self._plotBoxGrid(self.boxes0[::skip], fullBBox, ls='--')
783  # reset the color cycle -- see
784  # http://stackoverflow.com/questions/24193174/reset-color-cycle-in-matplotlib
785  plt.gca().set_prop_cycle(None)
786  self._plotBoxGrid(self.boxes1[::skip], fullBBox, ls=':')
787 
788  def _plotBoxGrid(self, boxes, bbox, **kwargs):
789  """Plot a grid of boxes using matplotlib.
790 
791  Parameters
792  ----------
793  boxes : list
794  a list of `afwGeom.BoundingBox`es
795  bbox : afwGeom.BoundingBox
796  an overall bounding box
797  **kwargs :
798  additional keyword arguments for matplotlib
799  """
800  import matplotlib.pyplot as plt
801 
802  def plotBox(box):
803  corners = np.array([np.array([pt.getX(), pt.getY()]) for pt in box.getCorners()])
804  corners = np.vstack([corners, corners[0, :]])
805  plt.plot(corners[:, 0], corners[:, 1], **kwargs)
806 
807  for b in boxes:
808  plotBox(b)
809  plt.xlim(bbox.getBeginX(), bbox.getEndX())
810  plt.ylim(bbox.getBeginY(), bbox.getEndY())
def _generateGrid(self, exposure, forceEvenSized=False, kwargs)
def _reduceImage(self, mapperResults, exposure, kwargs)
def _constructPsf(self, mapperResults, exposure)
def _runMapper(self, exposure, doClone=False, kwargs)
def run(self, subExposure, expandedSubExposure, fullBBox, kwargs)
def run(self, mapperResults, exposure, kwargs)