LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
_assembleImage.py
Go to the documentation of this file.
1 # This file is part of afw.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 __all__ = ['assembleAmplifierImage', 'assembleAmplifierRawImage',
23  'makeUpdatedDetector', 'AmplifierIsolator']
24 
25 # dict of doFlip: slice
26 _SliceDict = {
27  False: slice(None, None, 1),
28  True: slice(None, None, -1),
29 }
30 
31 
32 def _insertPixelChunk(outView, inView, amplifier, hasArrays):
33  # For the sake of simplicity and robustness, this code does not short-circuit the case flipX=flipY=False.
34  # However, it would save a bit of time, including the cost of making numpy array views.
35  # If short circuiting is wanted, do it here.
36 
37  xSlice = _SliceDict[amplifier.getRawFlipX()]
38  ySlice = _SliceDict[amplifier.getRawFlipY()]
39  if hasArrays:
40  # MaskedImage
41  inArrList = inView.getArrays()
42  outArrList = outView.getArrays()
43  else:
44  inArrList = [inView.getArray()]
45  outArrList = [outView.getArray()]
46 
47  for inArr, outArr in zip(inArrList, outArrList):
48  # y,x because numpy arrays are transposed w.r.t. afw Images
49  outArr[:] = inArr[ySlice, xSlice]
50 
51 
52 def assembleAmplifierImage(destImage, rawImage, amplifier):
53  """Assemble the amplifier region of an image from a raw image.
54 
55  Parameters
56  ----------
57  destImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
58  Assembled image; the region amplifier.getBBox() is overwritten with
59  the assembled amplifier image.
60  rawImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
61  Raw image (same type as destImage).
62  amplifier : `lsst.afw.cameraGeom.Amplifier`
63  Amplifier geometry, with raw amplifier info.
64 
65  Raises
66  ------
67  RuntimeError
68  Raised if image types do not match or amplifier has no raw amplifier info.
69  """
70  if type(destImage.Factory) != type(rawImage.Factory): # noqa: E721
71  raise RuntimeError(f"destImage type = {type(destImage.Factory).__name__} != "
72  f"{type(rawImage.Factory).__name__} = rawImage type")
73  inView = rawImage.Factory(rawImage, amplifier.getRawDataBBox())
74  outView = destImage.Factory(destImage, amplifier.getBBox())
75 
76  _insertPixelChunk(outView, inView, amplifier,
77  hasattr(rawImage, "getArrays"))
78 
79 
80 def assembleAmplifierRawImage(destImage, rawImage, amplifier):
81  """Assemble the amplifier region of a raw CCD image.
82 
83  For most cameras this is a no-op: the raw image already is an assembled
84  CCD image.
85  However, it is useful for camera such as LSST for which each amplifier
86  image is a separate image.
87 
88  Parameters
89  ----------
90  destImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
91  CCD Image; the region amplifier.getRawAmplifier().getBBox()
92  is overwritten with the raw amplifier image.
93  rawImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
94  Raw image (same type as destImage).
95  amplifier : `lsst.afw.cameraGeom.Amplifier`
96  Amplifier geometry with raw amplifier info
97 
98  Raises
99  ------
100  RuntimeError
101  Raised if image types do not match or amplifier has no raw amplifier info.
102  """
103  if type(destImage.Factory) != type(rawImage.Factory): # noqa: E721
104  raise RuntimeError(f"destImage type = {type(destImage.Factory).__name__} != "
105  f"{type(rawImage.Factory).__name__} = rawImage type")
106  inBBox = amplifier.getRawBBox()
107  inView = rawImage.Factory(rawImage, inBBox)
108  outBBox = amplifier.getRawBBox()
109  outBBox.shift(amplifier.getRawXYOffset())
110  outView = destImage.Factory(destImage, outBBox)
111 
112  _insertPixelChunk(outView, inView, amplifier,
113  hasattr(rawImage, "getArrays"))
114 
115 
117  """Return a Detector that has had the definitions of amplifier geometry
118  updated post assembly.
119 
120  Parameters
121  ----------
122  ccd : `lsst.afw.image.Detector`
123  The detector to copy and update.
124  """
125  builder = ccd.rebuild()
126  for amp in builder.getAmplifiers():
127  amp.transform()
128  return builder.finish()
129 
130 
132  """A class that can extracts single-amplifier subimages from trimmed or
133  untrimmed assembled images and transforms them to a particular orientation
134  and offset.
135 
136  Callers who have a in-memory assembled `lsst.afw.image.Exposure` should
137  generally just use the `apply` class method. Other methods can be used to
138  implement subimage loads of on on-disk images (e.g. formatter classes in
139  ``obs_base``) or obtain subsets from other image classes.
140 
141  Parameters
142  ----------
143  amplifier : `Amplifier`
144  Amplifier object that identifies the amplifier to load and sets the
145  orientation and offset of the returned subimage.
146  parent_bbox : `lsst.geom.Box2I`
147  Bounding box of the assembled parent image. This must be equal to
148  either ``parent_detector.getBBox()`` or
149  ``parent_detector.getRawBBox()``; which one is used to determine
150  whether the parent image (and hence the amplifier subimages) is
151  trimmed.
152  parent_detector : `Detector`
153  Detector object that describes the parent image.
154  """
155 
156  def __init__(self, amplifier, parent_bbox, parent_detector):
157  self._amplifier_amplifier = amplifier
158  self._parent_detector_parent_detector = parent_detector
159  self._parent_amplifier_parent_amplifier = self._parent_detector_parent_detector[self._amplifier_amplifier.getName()]
160  self._is_parent_trimmed_is_parent_trimmed = (parent_bbox == self._parent_detector_parent_detector.getBBox())
161  self._amplifier_comparison_amplifier_comparison = self._amplifier_amplifier.compareGeometry(self._parent_amplifier_parent_amplifier)
162  if self._is_parent_trimmed_is_parent_trimmed:
163  # We only care about the final bounding box; don't check e.g.
164  # overscan regions for consistency.
165  if self._parent_amplifier_parent_amplifier.getBBox() != self._amplifier_amplifier.getBBox():
166  raise ValueError(
167  f"The given amplifier's trimmed bounding box ({self._amplifier.getBBox()}) is not the "
168  "same as the trimmed bounding box of the same amplifier in the parent image "
169  f"({self._parent_amplifier.getBBox()})."
170  )
171  else:
172  # Parent is untrimmed, so we need all regions to be consistent
173  # between the amplifiers modulo flips and offsets.
174  if self._amplifier_comparison_amplifier_comparison & self._amplifier_comparison_amplifier_comparison.REGIONS_DIFFER:
175  raise ValueError(
176  "The given amplifier's subregions are fundamentally incompatible with those of the "
177  "parent image's amplifier."
178  )
179 
180  @property
181  def subimage_bbox(self):
182  """The bounding box of the target amplifier in the parent image
183  (`lsst.geom.Box2I`).
184  """
185  if self._is_parent_trimmed_is_parent_trimmed:
186  return self._parent_amplifier_parent_amplifier.getBBox()
187  else:
188  return self._parent_amplifier_parent_amplifier.getRawBBox()
189 
190  def transform_subimage(self, subimage):
191  """Transform an already-extracted subimage to match the orientation
192  and offset of the target amplifier.
193 
194  Parameters
195  ----------
196  subimage : image-like
197  The subimage to transform; may be any of `lsst.afw.image.Image`,
198  `lsst.afw.image.Mask`, `lsst.afw.image.MaskedImage`, and
199  `lsst.afw.image.Exposure`.
200 
201  Returns
202  -------
203  transformed : image-like
204  Transformed image of the same type as ``subimage``.
205  """
206  from lsst.afw.math import flipImage
207  if hasattr(subimage, "getMaskedImage"):
208  # flipImage doesn't support Exposure natively.
209  # And sadly, there's no way to write to an existing MaskedImage,
210  # so we need to make yet another copy.
211  result = subimage.clone()
212  result.setMaskedImage(
213  flipImage(
214  subimage.getMaskedImage(),
215  self._amplifier_comparison_amplifier_comparison & self._amplifier_comparison_amplifier_comparison.FLIPPED_X,
216  self._amplifier_comparison_amplifier_comparison & self._amplifier_comparison_amplifier_comparison.FLIPPED_Y,
217  )
218  )
219  else:
220  result = flipImage(
221  subimage,
222  self._amplifier_comparison_amplifier_comparison & self._amplifier_comparison_amplifier_comparison.FLIPPED_X,
223  self._amplifier_comparison_amplifier_comparison & self._amplifier_comparison_amplifier_comparison.FLIPPED_Y,
224  )
225  if self._is_parent_trimmed_is_parent_trimmed:
226  result.setXY0(self._amplifier_amplifier.getBBox().getMin())
227  else:
228  result.setXY0(self._amplifier_amplifier.getRawBBox().getMin() + self._amplifier_amplifier.getRawXYOffset())
229  return result
230 
231  def make_detector(self):
232  """Create a single-amplifier detector that describes the transformed
233  subimage.
234 
235  Returns
236  -------
237  detector : `Detector`
238  Detector object with a single amplifier, a trimmed bounding box
239  equal to the amplifier's trimmed bounding box, and no crosstalk.
240  """
241  detector = self._parent_detector_parent_detector.rebuild()
242  detector.clear()
243  detector.append(self._amplifier_amplifier.rebuild())
244  detector.setBBox(self._amplifier_amplifier.getBBox())
245  detector.unsetCrosstalk()
246  return detector.finish()
247 
248  @classmethod
249  def apply(cls, parent_exposure, amplifier):
250  """Obtain a single-amplifier `lsst.afw.image.Exposure` subimage that
251  masquerades as full-detector image for a single-amp detector.
252 
253  Parameters
254  ----------
255  parent_exposure : `lsst.afw.image.Exposure`
256  Parent image to obtain a subset from.
257  `~lsst.afw.image.Exposure.getDetector` must not return `None`.
258  amplifier : `Amplifier`
259  Target amplifier for the subimage. May differ from the amplifier
260  obtained by ``parent_exposure.getDetector()[amplifier.getName()]``
261  only by flips and differences in `~Amplifier.getRawXYOffset`.
262 
263  Returns
264  -------
265  subimage : `lsst.afw.image.Exposure`
266  Exposure subimage for the target amplifier, with the
267  orientation and XY0 described by that amplifier, and a single-amp
268  detector holding a copy of that amplifier.
269 
270  Notes
271  -----
272  Because we use the target amplifier's bounding box as the bounding box
273  of the detector attached to the returned exposure, other exposure
274  components that are passed through unmodified (e.g. the WCS) should
275  still be valid for the single-amp exposure after it is trimmed and
276  "assembled". Unlike most trimmed+assembled images, however, it will
277  have a nonzero XY0, and code that (incorrectly!) does not pay attention
278  to XY0 may break.
279  """
280  instance = cls(amplifier, parent_bbox=parent_exposure.getBBox(),
281  parent_detector=parent_exposure.getDetector())
282  result = instance.transform_subimage(parent_exposure[instance.subimage_bbox])
283  result.setDetector(instance.make_detector())
284  return result
table::Key< int > type
Definition: Detector.cc:163
def __init__(self, amplifier, parent_bbox, parent_detector)
def apply(cls, parent_exposure, amplifier)
def assembleAmplifierRawImage(destImage, rawImage, amplifier)
def assembleAmplifierImage(destImage, rawImage, amplifier)
std::string const & getName() const noexcept
Return a filter's name.
Definition: Filter.h:78
std::shared_ptr< ImageT > flipImage(ImageT const &inImage, bool flipLR, bool flipTB)
Flip an image left–right and/or top–bottom.
Definition: rotateImage.cc:92