Skip to content

Commit eace0ae

Browse files
committed
Refactored the image handling logic to further use the SRImage class, replacing the _ImageParser. Updated related tests and modules to accommodate SRImage-specific methods and streamlined code a bit.
1 parent b768d0c commit eace0ae

File tree

7 files changed

+96
-420
lines changed

7 files changed

+96
-420
lines changed

specreduce/background.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
from dataclasses import dataclass, field, InitVar
55

66
import numpy as np
7-
from astropy import units as u
8-
from astropy.nddata import NDData
97
from astropy.utils.decorators import deprecated_attribute
108
from specutils import Spectrum1D
119

@@ -302,7 +300,9 @@ def one_sided(cls, image, trace_object, separation, **kwargs):
302300
kwargs['traces'] = [trace_object+separation]
303301
return cls(image=image, **kwargs)
304302

305-
def bkg_image(self, image=None, disp_axis: int = 1, crossdisp_axis: int | None = None) -> SRImage:
303+
def bkg_image(self, image=None,
304+
disp_axis: int = 1,
305+
crossdisp_axis: int | None = None) -> SRImage:
306306
"""
307307
Expose the background tiled to the dimension of ``image``.
308308
@@ -319,7 +319,8 @@ def bkg_image(self, image=None, disp_axis: int = 1, crossdisp_axis: int | None =
319319
`~specutils.SRImage` object with same shape as ``image``.
320320
"""
321321
image = self.image if image is None else as_image(image, disp_axis, crossdisp_axis)
322-
return SRImage(np.tile(self._bkg_array,(image.shape[image.crossdisp_axis], 1)) * image.unit)
322+
return SRImage(np.tile(self._bkg_array,
323+
(image.shape[image.crossdisp_axis], 1)) * image.unit)
323324

324325
def bkg_spectrum(self, image=None) -> Spectrum1D:
325326
"""

specreduce/core.py

Lines changed: 1 addition & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -12,207 +12,8 @@
1212
__all__ = ['SpecreduceOperation']
1313

1414

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-
21415
@dataclass
215-
class SpecreduceOperation(_ImageParser):
16+
class SpecreduceOperation:
21617
"""
21718
An operation to perform as part of a spectroscopic reduction pipeline.
21819

0 commit comments

Comments
 (0)