Skip to content

Commit adf54a3

Browse files
committed
Consolidate image parsing in _ImageParser mixin
1 parent b032d95 commit adf54a3

File tree

5 files changed

+95
-125
lines changed

5 files changed

+95
-125
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ New Features
77
API Changes
88
^^^^^^^^^^^
99

10+
- All operations now accept Spectrum1D and Quantity-type images. All accepted
11+
image types are now processed internally as Spectrum1D objects. [#146]
12+
-
13+
1014
Bug Fixes
1115
^^^^^^^^^
1216

specreduce/background.py

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44
from dataclasses import dataclass, field
55

66
import numpy as np
7-
from astropy.nddata import NDData, VarianceUncertainty
7+
from astropy.nddata import NDData
88
from astropy import units as u
99
from specutils import Spectrum1D
1010

11+
from specreduce.core import _ImageParser
1112
from specreduce.extract import _ap_weight_image, _to_spectrum1d_pixels
1213
from specreduce.tracing import Trace, FlatTrace
1314

1415
__all__ = ['Background']
1516

1617

1718
@dataclass
18-
class Background:
19+
class Background(_ImageParser):
1920
"""
2021
Determine the background from an image for subtraction.
2122
@@ -55,41 +56,6 @@ class Background:
5556
disp_axis: int = 1
5657
crossdisp_axis: int = 0
5758

58-
def _parse_image(self):
59-
"""
60-
Convert all accepted image types to a consistently formatted Spectrum1D.
61-
"""
62-
63-
if isinstance(self.image, np.ndarray):
64-
img = self.image
65-
elif isinstance(self.image, u.quantity.Quantity):
66-
img = self.image.value
67-
else: # NDData, including CCDData and Spectrum1D
68-
img = self.image.data
69-
70-
# mask and uncertainty are set as None when they aren't specified upon
71-
# creating a Spectrum1D object, so we must check whether these
72-
# attributes are absent *and* whether they are present but set as None
73-
if getattr(self.image, 'mask', None) is not None:
74-
mask = self.image.mask
75-
else:
76-
mask = np.ma.masked_invalid(img).mask
77-
78-
if getattr(self.image, 'uncertainty', None) is not None:
79-
uncertainty = self.image.uncertainty
80-
else:
81-
uncertainty = VarianceUncertainty(np.ones(img.shape))
82-
83-
unit = getattr(self.image, 'unit', u.Unit('DN')) # or u.Unit()?
84-
85-
spectral_axis = getattr(self.image, 'spectral_axis',
86-
(np.arange(img.shape[self.disp_axis])
87-
if hasattr(self, 'disp_axis')
88-
else np.arange(img.shape[1])) * u.pix)
89-
90-
self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis,
91-
uncertainty=uncertainty, mask=mask)
92-
9359
def __post_init__(self):
9460
"""
9561
Determine the background from an image for subtraction.
@@ -122,7 +88,7 @@ def _to_trace(trace):
12288
raise ValueError('trace_object.trace_pos must be >= 1')
12389
return trace
12490

125-
self._parse_image()
91+
self.image = self._parse_image(self.image)
12692

12793
if self.width < 0:
12894
raise ValueError("width must be positive")

specreduce/core.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,86 @@
11
# Licensed under a 3-clause BSD style license - see LICENSE.rst
22

33
import inspect
4+
import numpy as np
5+
6+
from astropy import units as u
7+
from astropy.nddata import VarianceUncertainty
48
from dataclasses import dataclass
9+
from specutils import Spectrum1D
510

611
__all__ = ['SpecreduceOperation']
712

813

14+
class _ImageParser:
15+
"""
16+
Coerces images from accepted formats to Spectrum1D objects for
17+
internal use in specreduce's operation classes.
18+
19+
Fills any and all of uncertainty, mask, units, and spectral axis
20+
that are missing in the provided image with generic values.
21+
Accepted image types are:
22+
23+
- `~specutils.spectra.spectrum1d.Spectrum1D` (preferred)
24+
- `~astropy.nddata.ccddata.CCDData`
25+
- `~astropy.nddata.ndddata.NDDData`
26+
- `~astropy.units.quantity.Quantity`
27+
- `~numpy.ndarray`
28+
"""
29+
def _parse_image(self, image, disp_axis=1):
30+
"""
31+
Convert all accepted image types to a consistently formatted
32+
Spectrum1D object.
33+
34+
Parameters
35+
----------
36+
image : `~astropy.nddata.NDData`-like or array-like, required
37+
The image to be parsed. If None, defaults to class' own
38+
image attribute.
39+
disp_axis : int, optional
40+
The index of the image's dispersion axis. Should not be
41+
changed until operations can handle variable image
42+
orientations. [default: 1]
43+
"""
44+
45+
# would be nice to handle (cross)disp_axis consistently across
46+
# operations (public attribute? private attribute? argument only?) so
47+
# it can be called from self instead of via kwargs...
48+
49+
if image is None:
50+
# useful for Background's instance methods
51+
return self.image
52+
53+
if isinstance(image, np.ndarray):
54+
img = image
55+
elif isinstance(image, u.quantity.Quantity):
56+
img = image.value
57+
else: # NDData, including CCDData and Spectrum1D
58+
img = image.data
59+
60+
# mask and uncertainty are set as None when they aren't specified upon
61+
# creating a Spectrum1D object, so we must check whether these
62+
# attributes are absent *and* whether they are present but set as None
63+
if getattr(image, 'mask', None) is not None:
64+
mask = image.mask
65+
else:
66+
mask = np.ma.masked_invalid(img).mask
67+
68+
if getattr(image, 'uncertainty', None) is not None:
69+
uncertainty = image.uncertainty
70+
else:
71+
uncertainty = VarianceUncertainty(np.ones(img.shape))
72+
73+
unit = getattr(image, 'unit', u.Unit('DN')) # or u.Unit()?
74+
75+
spectral_axis = getattr(image, 'spectral_axis',
76+
np.arange(img.shape[disp_axis]) * u.pix)
77+
78+
return Spectrum1D(img * unit, spectral_axis=spectral_axis,
79+
uncertainty=uncertainty, mask=mask)
80+
81+
982
@dataclass
10-
class SpecreduceOperation:
83+
class SpecreduceOperation(_ImageParser):
1184
"""
1285
An operation to perform as part of a spectroscopic reduction pipeline.
1386

specreduce/extract.py

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from astropy.modeling import Model, models, fitting
1010
from astropy.nddata import NDData, VarianceUncertainty
1111

12-
from specreduce.core import SpecreduceOperation
12+
from specreduce.core import _ImageParser, SpecreduceOperation
1313
from specreduce.tracing import Trace, FlatTrace
1414
from specutils import Spectrum1D
1515

@@ -164,41 +164,6 @@ class BoxcarExtract(SpecreduceOperation):
164164
def spectrum(self):
165165
return self.__call__()
166166

167-
def _parse_image(self):
168-
"""
169-
Convert all accepted image types to a consistently formatted Spectrum1D.
170-
"""
171-
172-
if isinstance(self.image, np.ndarray):
173-
img = self.image
174-
elif isinstance(self.image, u.quantity.Quantity):
175-
img = self.image.value
176-
else: # NDData, including CCDData and Spectrum1D
177-
img = self.image.data
178-
179-
# mask and uncertainty are set as None when they aren't specified upon
180-
# creating a Spectrum1D object, so we must check whether these
181-
# attributes are absent *and* whether they are present but set as None
182-
if getattr(self.image, 'mask', None) is not None:
183-
mask = self.image.mask
184-
else:
185-
mask = np.ma.masked_invalid(img).mask
186-
187-
if getattr(self.image, 'uncertainty', None) is not None:
188-
uncertainty = self.image.uncertainty
189-
else:
190-
uncertainty = VarianceUncertainty(np.ones(img.shape))
191-
192-
unit = getattr(self.image, 'unit', u.Unit('DN')) # or u.Unit()?
193-
194-
spectral_axis = getattr(self.image, 'spectral_axis',
195-
(np.arange(img.shape[self.disp_axis])
196-
if hasattr(self, 'disp_axis')
197-
else np.arange(img.shape[1])) * u.pix)
198-
199-
self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis,
200-
uncertainty=uncertainty, mask=mask)
201-
202167
def __call__(self, image=None, trace_object=None, width=None,
203168
disp_axis=None, crossdisp_axis=None):
204169
"""
@@ -231,12 +196,7 @@ def __call__(self, image=None, trace_object=None, width=None,
231196
crossdisp_axis = crossdisp_axis if crossdisp_axis is not None else self.crossdisp_axis
232197

233198
# handle image processing based on its type
234-
if isinstance(image, Spectrum1D):
235-
img = image.data
236-
unit = image.unit
237-
else:
238-
img = image
239-
unit = getattr(image, 'unit', u.DN)
199+
self.image = self._parse_image(image)
240200

241201
# TODO: this check can be removed if/when implemented as a check in FlatTrace
242202
if isinstance(trace_object, FlatTrace):
@@ -252,11 +212,11 @@ def __call__(self, image=None, trace_object=None, width=None,
252212
width,
253213
disp_axis,
254214
crossdisp_axis,
255-
img.shape)
215+
self.image.shape)
256216

257217
# extract
258-
ext1d = np.sum(img * wimg, axis=crossdisp_axis) * unit
259-
return _to_spectrum1d_pixels(ext1d)
218+
ext1d = np.sum(self.image.data * wimg, axis=crossdisp_axis)
219+
return _to_spectrum1d_pixels(ext1d * self.image.unit)
260220

261221

262222
@dataclass

specreduce/tracing.py

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from specutils import Spectrum1D
1313
import numpy as np
1414

15+
from specreduce.core import _ImageParser
16+
1517
__all__ = ['Trace', 'FlatTrace', 'ArrayTrace', 'KosmosTrace']
1618

1719

@@ -39,41 +41,6 @@ def __post_init__(self):
3941
def __getitem__(self, i):
4042
return self.trace[i]
4143

42-
def _parse_image(self):
43-
"""
44-
Convert all accepted image types to a consistently formatted Spectrum1D.
45-
"""
46-
47-
if isinstance(self.image, np.ndarray):
48-
img = self.image
49-
elif isinstance(self.image, u.quantity.Quantity):
50-
img = self.image.value
51-
else: # NDData, including CCDData and Spectrum1D
52-
img = self.image.data
53-
54-
# mask and uncertainty are set as None when they aren't specified upon
55-
# creating a Spectrum1D object, so we must check whether these
56-
# attributes are absent *and* whether they are present but set as None
57-
if getattr(self.image, 'mask', None) is not None:
58-
mask = self.image.mask
59-
else:
60-
mask = np.ma.masked_invalid(img).mask
61-
62-
if getattr(self.image, 'uncertainty', None) is not None:
63-
uncertainty = self.image.uncertainty
64-
else:
65-
uncertainty = VarianceUncertainty(np.ones(img.shape))
66-
67-
unit = getattr(self.image, 'unit', u.Unit('DN')) # or u.Unit()?
68-
69-
spectral_axis = getattr(self.image, 'spectral_axis',
70-
(np.arange(img.shape[self._disp_axis])
71-
if hasattr(self, '_disp_axis')
72-
else np.arange(img.shape[1])) * u.pix)
73-
74-
self.image = Spectrum1D(img * unit, spectral_axis=spectral_axis,
75-
uncertainty=uncertainty, mask=mask)
76-
7744
@property
7845
def shape(self):
7946
return self.trace.shape
@@ -116,7 +83,7 @@ def __sub__(self, delta):
11683

11784

11885
@dataclass
119-
class FlatTrace(Trace):
86+
class FlatTrace(Trace, _ImageParser):
12087
"""
12188
Trace that is constant along the axis being traced
12289
@@ -132,7 +99,7 @@ class FlatTrace(Trace):
13299
trace_pos: float
133100

134101
def __post_init__(self):
135-
super()._parse_image()
102+
self.image = self._parse_image(self.image)
136103

137104
self.set_position(self.trace_pos)
138105

@@ -151,7 +118,7 @@ def set_position(self, trace_pos):
151118

152119

153120
@dataclass
154-
class ArrayTrace(Trace):
121+
class ArrayTrace(Trace, _ImageParser):
155122
"""
156123
Define a trace given an array of trace positions
157124
@@ -163,7 +130,7 @@ class ArrayTrace(Trace):
163130
trace: np.ndarray
164131

165132
def __post_init__(self):
166-
super()._parse_image()
133+
self.image = self._parse_image(self.image)
167134

168135
nx = self.image.shape[1]
169136
nt = len(self.trace)
@@ -180,7 +147,7 @@ def __post_init__(self):
180147

181148

182149
@dataclass
183-
class KosmosTrace(Trace):
150+
class KosmosTrace(Trace, _ImageParser):
184151
"""
185152
Trace the spectrum aperture in an image.
186153
@@ -241,7 +208,7 @@ class KosmosTrace(Trace):
241208
_disp_axis = 1
242209

243210
def __post_init__(self):
244-
super()._parse_image()
211+
self.image = self._parse_image(self.image)
245212

246213
# mask any previously uncaught invalid values
247214
or_mask = np.logical_or(self.image.mask,

0 commit comments

Comments
 (0)