|
12 | 12 | __all__ = ['SpecreduceOperation']
|
13 | 13 |
|
14 | 14 |
|
15 |
| -class _ImageParser: |
16 |
| - """ |
17 |
| - Coerces images from accepted formats to Spectrum1D objects for |
18 |
| - internal use in specreduce's operation classes. |
19 |
| -
|
20 |
| - Fills any and all of uncertainty, mask, units, and spectral axis |
21 |
| - that are missing in the provided image with generic values. |
22 |
| - Accepted image types are: |
23 |
| -
|
24 |
| - - `~specutils.spectra.spectrum1d.Spectrum1D` (preferred) |
25 |
| - - `~astropy.nddata.ccddata.CCDData` |
26 |
| - - `~astropy.nddata.ndddata.NDDData` |
27 |
| - - `~astropy.units.quantity.Quantity` |
28 |
| - - `~numpy.ndarray` |
29 |
| - """ |
30 |
| - |
31 |
| - # The '_valid_mask_treatment_methods' in the Background, Trace, and Extract |
32 |
| - # classes is a subset of implemented methods. |
33 |
| - implemented_mask_treatment_methods = 'filter', 'zero-fill', 'omit' |
34 |
| - |
35 |
| - def _parse_image(self, image, |
36 |
| - disp_axis: int = 1, |
37 |
| - mask_treatment: str = 'filter') -> Spectrum1D: |
38 |
| - """ |
39 |
| - Convert all accepted image types to a consistently formatted Spectrum1D object. |
40 |
| -
|
41 |
| - Parameters |
42 |
| - ---------- |
43 |
| - image : `~astropy.nddata.NDData`-like or array-like |
44 |
| - The image to be parsed. If None, defaults to class' own |
45 |
| - image attribute. |
46 |
| - disp_axis |
47 |
| - The index of the image's dispersion axis. Should not be |
48 |
| - changed until operations can handle variable image |
49 |
| - orientations. [default: 1] |
50 |
| - mask_treatment |
51 |
| - Treatment method for the mask. |
52 |
| -
|
53 |
| - Returns |
54 |
| - ------- |
55 |
| - Spectrum1D |
56 |
| - """ |
57 |
| - # would be nice to handle (cross)disp_axis consistently across |
58 |
| - # operations (public attribute? private attribute? argument only?) so |
59 |
| - # it can be called from self instead of via kwargs... |
60 |
| - |
61 |
| - if image is None: |
62 |
| - # useful for Background's instance methods |
63 |
| - return self.image |
64 |
| - |
65 |
| - return self._get_data_from_image(image, disp_axis=disp_axis, |
66 |
| - mask_treatment=mask_treatment) |
67 |
| - |
68 |
| - @staticmethod |
69 |
| - def _get_data_from_image(image, |
70 |
| - disp_axis: int = 1, |
71 |
| - mask_treatment: str = 'filter') -> Spectrum1D: |
72 |
| - """ |
73 |
| - Extract data array from various input types for `image`. |
74 |
| -
|
75 |
| - Parameters |
76 |
| - ---------- |
77 |
| - image : array-like or Quantity |
78 |
| - Input image from which data is extracted. This can be a 2D numpy |
79 |
| - array, Quantity, or an NDData object. |
80 |
| - disp_axis : int, optional |
81 |
| - The dispersion axis of the image. |
82 |
| - mask_treatment : str, optional |
83 |
| - Treatment method for the mask: |
84 |
| - - 'filter' (default): Return the unmodified input image and combined mask. |
85 |
| - - 'zero-fill': Set masked values in the image to zero. |
86 |
| - - 'omit': Mask all pixels along the cross dispersion axis if any value is masked. |
87 |
| -
|
88 |
| - Returns |
89 |
| - ------- |
90 |
| - Spectrum1D |
91 |
| - """ |
92 |
| - # This works only with 2D images. |
93 |
| - crossdisp_axis = (disp_axis + 1) % 2 |
94 |
| - |
95 |
| - if isinstance(image, u.quantity.Quantity): |
96 |
| - img = image.value |
97 |
| - elif isinstance(image, np.ndarray): |
98 |
| - img = image |
99 |
| - else: # NDData, including CCDData and Spectrum1D |
100 |
| - img = image.data |
101 |
| - |
102 |
| - mask = getattr(image, 'mask', None) |
103 |
| - |
104 |
| - # next, handle masked and nonfinite data in image. |
105 |
| - # A mask will be created from any nonfinite image data, and combined |
106 |
| - # with any additional 'mask' passed in. If image is being parsed within |
107 |
| - # a specreduce operation that has 'mask_treatment' options, this will be |
108 |
| - # handled as well. Note that input data may be modified if a fill value |
109 |
| - # is chosen to handle masked data. The returned image will always have |
110 |
| - # `image.mask` even if there are no nonfinte or masked values. |
111 |
| - img, mask = _ImageParser._mask_and_nonfinite_data_handling(image=img, |
112 |
| - mask=mask, |
113 |
| - mask_treatment=mask_treatment, |
114 |
| - crossdisp_axis=crossdisp_axis) |
115 |
| - |
116 |
| - # mask (handled above) and uncertainty are set as None when they aren't |
117 |
| - # specified upon creating a Spectrum1D object, so we must check whether |
118 |
| - # these attributes are absent *and* whether they are present but set as None |
119 |
| - if hasattr(image, 'uncertainty'): |
120 |
| - uncertainty = image.uncertainty |
121 |
| - else: |
122 |
| - uncertainty = VarianceUncertainty(np.ones(img.shape)) |
123 |
| - |
124 |
| - unit = getattr(image, 'unit', u.Unit('DN')) |
125 |
| - |
126 |
| - spectral_axis = getattr(image, 'spectral_axis', |
127 |
| - np.arange(img.shape[disp_axis]) * u.pix) |
128 |
| - |
129 |
| - img = Spectrum1D(img * unit, spectral_axis=spectral_axis, |
130 |
| - uncertainty=uncertainty, mask=mask) |
131 |
| - return img |
132 |
| - |
133 |
| - @staticmethod |
134 |
| - def _mask_and_nonfinite_data_handling(image, mask, |
135 |
| - mask_treatment: str = 'filter', |
136 |
| - crossdisp_axis: int = 0) -> tuple[np.ndarray, np.ndarray]: |
137 |
| - """ |
138 |
| - Handle the treatment of masked and nonfinite data. |
139 |
| -
|
140 |
| - All operations in Specreduce can take in a mask for the data as |
141 |
| - part of the input NDData. Additionally, any non-finite values in the |
142 |
| - data that aren't in the user-supplied mask will be combined bitwise |
143 |
| - with the input mask. |
144 |
| -
|
145 |
| - There are three options currently implemented for the treatment |
146 |
| - of masked and nonfinite data - filter, omit, and zero-fill. |
147 |
| - Depending on the step, all or a subset of these three options are valid. |
148 |
| -
|
149 |
| - Parameters |
150 |
| - ---------- |
151 |
| - image : array-like |
152 |
| - The input image data array that may contain nonfinite values. |
153 |
| - mask : array-like or None |
154 |
| - An optional mask array. Nonfinite values in the image will be added to this mask. |
155 |
| - mask_treatment : str |
156 |
| - Specifies how to handle masked data: |
157 |
| - - 'filter' (default): Returns the unmodified input image and combined mask. |
158 |
| - - 'zero-fill': Sets masked values in the image to zero. |
159 |
| - - 'omit': Masks entire columns or rows if any value is masked. |
160 |
| - crossdisp_axis : int |
161 |
| - Axis along which to collapse the 2D mask into a 1D mask for treatment 'omit'. |
162 |
| - """ |
163 |
| - if mask_treatment not in _ImageParser.implemented_mask_treatment_methods: |
164 |
| - raise ValueError("`mask_treatment` must be one of " |
165 |
| - f"{_ImageParser.implemented_mask_treatment_methods}") |
166 |
| - |
167 |
| - # make sure there is always a 'mask', even when all data is unmasked and finite. |
168 |
| - if mask is not None: |
169 |
| - # always mask any previously uncaught nonfinite values in image array |
170 |
| - # combining these with the (optional) user-provided mask on `image.mask` |
171 |
| - mask = np.logical_or(mask, ~np.isfinite(image)) |
172 |
| - else: |
173 |
| - mask = ~np.isfinite(image) |
174 |
| - |
175 |
| - # if mask option is the default 'filter' option, |
176 |
| - # nothing needs to be done. input mask (combined with nonfinite data) |
177 |
| - # remains with data as-is. |
178 |
| - |
179 |
| - if mask_treatment == 'zero-fill': |
180 |
| - # make a copy of the input image since we will be modifying it |
181 |
| - image = deepcopy(image) |
182 |
| - |
183 |
| - # if mask_treatment is 'zero_fill', set masked values to zero in |
184 |
| - # image data and drop image.mask. note that this is done after |
185 |
| - # _combine_mask_with_nonfinite_from_data, so non-finite values in |
186 |
| - # data (which are now in the mask) will also be set to zero. |
187 |
| - # set masked values to zero |
188 |
| - image[mask] = 0. |
189 |
| - |
190 |
| - # masked array with no masked values, so accessing image.mask works |
191 |
| - # but we don't need the actual mask anymore since data has been set to 0 |
192 |
| - mask = np.zeros(image.shape, dtype=bool) |
193 |
| - |
194 |
| - elif mask_treatment == 'omit': |
195 |
| - # collapse 2d mask (after accounting for addl non-finite values in |
196 |
| - # data) to a 1d mask, along dispersion axis, to fully mask columns |
197 |
| - # that have any masked values. |
198 |
| - |
199 |
| - # create a 1d mask along crossdisp axis - if any column has a single nan, |
200 |
| - # the entire column should be masked |
201 |
| - reduced_mask = np.logical_or.reduce(mask, axis=crossdisp_axis) |
202 |
| - |
203 |
| - # back to a 2D mask |
204 |
| - shape = (image.shape[0], 1) if crossdisp_axis == 0 else (1, image.shape[1]) |
205 |
| - mask = np.tile(reduced_mask, shape) |
206 |
| - |
207 |
| - # check for case where entire image is masked. |
208 |
| - if mask.all(): |
209 |
| - raise ValueError('Image is fully masked. Check for invalid values.') |
210 |
| - |
211 |
| - return image, mask |
212 |
| - |
213 |
| - |
214 | 15 | @dataclass
|
215 |
| -class SpecreduceOperation(_ImageParser): |
| 16 | +class SpecreduceOperation: |
216 | 17 | """
|
217 | 18 | An operation to perform as part of a spectroscopic reduction pipeline.
|
218 | 19 |
|
|
0 commit comments