@@ -32,9 +32,9 @@ class Background(_ImageParser):
32
32
----------
33
33
image : `~astropy.nddata.NDData`-like or array-like
34
34
image with 2-D spectral image data
35
- traces : trace, int, float (single or list)
35
+ traces : List, `tracing.Trace`, int, float
36
36
Individual or list of trace object(s) (or integers/floats to define
37
- FlatTraces) to extract the background. If None, a FlatTrace at the
37
+ FlatTraces) to extract the background. If None, a ` FlatTrace` at the
38
38
center of the image (according to `disp_axis`) will be used.
39
39
width : float
40
40
width of extraction aperture in pixels
@@ -44,8 +44,22 @@ class Background(_ImageParser):
44
44
pixels.
45
45
disp_axis : int
46
46
dispersion axis
47
+ [default: 1]
47
48
crossdisp_axis : int
48
49
cross-dispersion axis
50
+ [default: 0]
51
+ mask_treatment : string, optional
52
+ The method for handling masked or non-finite data. Choice of `filter`,
53
+ `omit`, or `zero-fill`. If `filter` is chosen, masked and non-finite
54
+ data will not contribute to the background statistic that is calculated
55
+ in each column along `disp_axis`. If `omit` is chosen, columns along
56
+ disp_axis with any masked/non-finite data values will be fully masked
57
+ (i.e, 2D mask is collapsed to 1D and applied). If `zero-fill` is chosen,
58
+ masked/non-finite data will be replaced with 0.0 in the input image,
59
+ and the mask will then be dropped. For all three options, the input mask
60
+ (optional on input NDData object) will be combined with a mask generated
61
+ from any non-finite values in the image data.
62
+ [default: ``filter``]
49
63
"""
50
64
# required so numpy won't call __rsub__ on individual elements
51
65
# https://stackoverflow.com/a/58409215
@@ -57,6 +71,8 @@ class Background(_ImageParser):
57
71
statistic : str = 'average'
58
72
disp_axis : int = 1
59
73
crossdisp_axis : int = 0
74
+ mask_treatment : str = 'filter'
75
+ _valid_mask_treatment_methods = ('filter' , 'omit' , 'zero-fill' )
60
76
61
77
# TO-DO: update bkg_array with Spectrum1D alternative (is bkg_image enough?)
62
78
bkg_array = deprecated_attribute ('bkg_array' , '1.3' )
@@ -82,9 +98,29 @@ def __post_init__(self):
82
98
dispersion axis
83
99
crossdisp_axis : int
84
100
cross-dispersion axis
101
+ mask_treatment : string
102
+ The method for handling masked or non-finite data. Choice of `filter`,
103
+ `omit`, or `zero-fill`. If `filter` is chosen, masked/non-finite data
104
+ will be filtered during the fit to each bin/column (along disp. axis) to
105
+ find the peak. If `omit` is chosen, columns along disp_axis with any
106
+ masked/non-finite data values will be fully masked (i.e, 2D mask is
107
+ collapsed to 1D and applied). If `zero-fill` is chosen, masked/non-finite
108
+ data will be replaced with 0.0 in the input image, and the mask will then
109
+ be dropped. For all three options, the input mask (optional on input
110
+ NDData object) will be combined with a mask generated from any non-finite
111
+ values in the image data.
85
112
"""
113
+
114
+ # Parse image, including masked/nonfinite data handling based on
115
+ # choice of `mask_treatment`. Any uncaught nonfinte data values will be
116
+ # masked as well. Returns a Spectrum1D.
86
117
self .image = self ._parse_image (self .image )
87
118
119
+ # always work with masked array, even if there is no masked
120
+ # or nonfinite data, in case padding is needed. if not, mask will be
121
+ # dropped at the end and a regular array will be returned.
122
+ img = np .ma .masked_array (self .image .data , self .image .mask )
123
+
88
124
if self .width < 0 :
89
125
raise ValueError ("width must be positive" )
90
126
if self .width == 0 :
@@ -95,9 +131,13 @@ def __post_init__(self):
95
131
96
132
bkg_wimage = np .zeros_like (self .image .data , dtype = np .float64 )
97
133
for trace in self .traces :
134
+ # note: ArrayTrace can have masked values, but if it does a MaskedArray
135
+ # will be returned so this should be reflected in the window size here
136
+ # (i.e, np.nanmax is not required.)
98
137
windows_max = trace .trace .data .max () + self .width / 2
99
138
windows_min = trace .trace .data .min () - self .width / 2
100
- if windows_max >= self .image .shape [self .crossdisp_axis ]:
139
+
140
+ if windows_max > self .image .shape [self .crossdisp_axis ]:
101
141
warnings .warn ("background window extends beyond image boundaries " +
102
142
f"({ windows_max } >= { self .image .shape [self .crossdisp_axis ]} )" )
103
143
if windows_min < 0 :
@@ -115,27 +155,26 @@ def __post_init__(self):
115
155
raise ValueError ("background regions overlapped" )
116
156
if np .any (np .sum (bkg_wimage , axis = self .crossdisp_axis ) == 0 ):
117
157
raise ValueError ("background window does not remain in bounds across entire dispersion axis" ) # noqa
158
+ # check if image contained within background window is fully-nonfinite and raise an error
159
+ if np .all (img .mask [bkg_wimage > 0 ]):
160
+ raise ValueError ("Image is fully masked within background window determined by `width`." )
118
161
119
162
if self .statistic == 'median' :
120
163
# make it clear in the expose image that partial pixels are fully-weighted
121
164
bkg_wimage [bkg_wimage > 0 ] = 1
122
165
123
166
self .bkg_wimage = bkg_wimage
124
167
125
- # mask user-highlighted and invalid values (if any) before taking stats
126
- or_mask = (np .logical_or (~ np .isfinite (self .image .data ), self .image .mask )
127
- if self .image .mask is not None
128
- else ~ np .isfinite (self .image .data ))
129
-
130
168
if self .statistic == 'average' :
131
- image_ma = np .ma .masked_array (self .image .data , mask = or_mask )
132
- self ._bkg_array = np .ma .average (image_ma ,
169
+ self ._bkg_array = np .ma .average (img ,
133
170
weights = self .bkg_wimage ,
134
- axis = self .crossdisp_axis ).data
171
+ axis = self .crossdisp_axis )
172
+
135
173
elif self .statistic == 'median' :
136
- med_mask = np .logical_or (self .bkg_wimage == 0 , or_mask )
137
- image_ma = np .ma .masked_array (self .image .data , mask = med_mask )
138
- self ._bkg_array = np .ma .median (image_ma , axis = self .crossdisp_axis ).data
174
+ # combine where background weight image is 0 with image masked (which already
175
+ # accounts for non-finite data that wasn't already masked)
176
+ img .mask = np .logical_or (self .bkg_wimage == 0 , self .image .mask )
177
+ self ._bkg_array = np .ma .median (img , axis = self .crossdisp_axis )
139
178
else :
140
179
raise ValueError ("statistic must be 'average' or 'median'" )
141
180
@@ -204,7 +243,19 @@ def two_sided(cls, image, trace_object, separation, **kwargs):
204
243
dispersion axis
205
244
crossdisp_axis : int
206
245
cross-dispersion axis
246
+ mask_treatment : string
247
+ The method for handling masked or non-finite data. Choice of `filter`,
248
+ `omit`, or `zero-fill`. If `filter` is chosen, masked/non-finite data
249
+ will be filtered during the fit to each bin/column (along disp. axis) to
250
+ find the peak. If `omit` is chosen, columns along disp_axis with any
251
+ masked/non-finite data values will be fully masked (i.e, 2D mask is
252
+ collapsed to 1D and applied). If `zero-fill` is chosen, masked/non-finite
253
+ data will be replaced with 0.0 in the input image, and the mask will then
254
+ be dropped. For all three options, the input mask (optional on input
255
+ NDData object) will be combined with a mask generated from any non-finite
256
+ values in the image data.
207
257
"""
258
+
208
259
image = _ImageParser ._get_data_from_image (image ) if image is not None else cls .image
209
260
kwargs ['traces' ] = [trace_object - separation , trace_object + separation ]
210
261
return cls (image = image , ** kwargs )
@@ -241,6 +292,17 @@ def one_sided(cls, image, trace_object, separation, **kwargs):
241
292
dispersion axis
242
293
crossdisp_axis : int
243
294
cross-dispersion axis
295
+ mask_treatment : string
296
+ The method for handling masked or non-finite data. Choice of `filter`,
297
+ `omit`, or `zero-fill`. If `filter` is chosen, masked/non-finite data
298
+ will be filtered during the fit to each bin/column (along disp. axis) to
299
+ find the peak. If `omit` is chosen, columns along disp_axis with any
300
+ masked/non-finite data values will be fully masked (i.e, 2D mask is
301
+ collapsed to 1D and applied). If `zero-fill` is chosen, masked/non-finite
302
+ data will be replaced with 0.0 in the input image, and the mask will then
303
+ be dropped. For all three options, the input mask (optional on input
304
+ NDData object) will be combined with a mask generated from any non-finite
305
+ values in the image data.
244
306
"""
245
307
image = _ImageParser ._get_data_from_image (image ) if image is not None else cls .image
246
308
kwargs ['traces' ] = [trace_object + separation ]
0 commit comments