Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JP-2256: Apply median filter to IVM weight #7563

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
1.14.1 (unreleased)
===================

-
resample
--------

- Apply a median filter to IVM weight array to better handle saturated
pixels. [#7563]

Comment on lines +4 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
resample
--------
- Apply a median filter to IVM weight array to better handle saturated
pixels. [#7563]

now that #8671 is merged (switching change log handling to towncrier) this change log entry should be a file in changes/ instead:

echo "Apply a median filter to IVM weight array to better handle saturated pixels." > changes/7563.resample.rst

(new PRs will include instructions on how to do this)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ignore this if this PR is no longer active or needed)


1.14.0 (2024-03-29)
===================
Expand All @@ -17,7 +22,7 @@ ami
- Additional optional input arguments for greater user processing flexibility.
See documentation for details. [#7862]

- Bad pixel correction applied to data using new NRM reference file to calculate
- Bad pixel correction applied to data using new NRM reference file to calculate
complex visibility support (M. Ireland method implemented by J. Kammerer). [#7862]

- Make ``AmiAnalyze`` and ``AmiNormalize`` output conform to the OIFITS standard. [#7862]
Expand Down Expand Up @@ -53,7 +58,7 @@ charge_migration
as DO_NOT_USE. This group, and all subsequent groups, are then flagged as
CHARGELOSS and DO_NOT_USE. The four nearest pixel neighbor are then flagged
in the same group. [#8336]

- Added warning handler for expected NaN and inf clipping in the
``sigma_clip`` function. [#8320]

Expand Down Expand Up @@ -340,7 +345,7 @@ tweakreg

- Fixed a bug that caused failures instead of warnings when no GAIA sources
were found within the bounding box of the input image. [#8334]

- Suppress AstropyUserWarnings regarding NaNs in the input data. [#8320]

wfs_combine
Expand Down Expand Up @@ -903,6 +908,7 @@ resample
- Update the following exposure time keywords: XPOSURE (EFFEXPTM),
DURATION and TELAPSE. [#7793]


residual_fringe
---------------

Expand Down Expand Up @@ -1202,6 +1208,10 @@ tweakreg
- Added the 'GAIADR3' catalog to the available options for alignment;
this has been enabled as the default option [#7611].

- Apply median filtering to IVM weight array to better handle saturated
pixels. Added option of 'ivm-med', 'ivm-smed' (recommended default) to the
``weight_type`` argument of ``ResampleStep``. [#7563]


1.10.2 (2023-04-14)
===================
Expand Down
4 changes: 2 additions & 2 deletions jwst/resample/resample_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ResampleStep(Step):
pixfrac = float(default=1.0) # change back to None when drizpar reference files are updated
kernel = string(default='square') # change back to None when drizpar reference files are updated
fillval = string(default='INDEF' ) # change back to None when drizpar reference files are updated
weight_type = option('ivm', 'exptime', None, default='ivm') # change back to None when drizpar ref update
weight_type = option('ivm', 'ivm-med', 'ivm-smed', 'exptime', None, default='ivm-smed') # change back to None when drizpar ref update
output_shape = int_list(min=2, max=2, default=None) # [x, y] order
crpix = float_list(min=2, max=2, default=None)
crval = float_list(min=2, max=2, default=None)
Expand All @@ -58,7 +58,7 @@ class ResampleStep(Step):
blendheaders = boolean(default=True)
allowed_memory = float(default=None) # Fraction of memory to use for the combined image.
in_memory = boolean(default=True)
"""
""" # noqa: E501

reference_file_types = ['drizpars']

Expand Down
72 changes: 68 additions & 4 deletions jwst/resample/resample_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import logging
import warnings

import gwcs
import numpy as np
from scipy.ndimage import median_filter
from astropy import units as u
import gwcs

from stdatamodels.dqflags import interpret_bit_flags
from stdatamodels.jwst.datamodels.dqflags import pixel
Expand Down Expand Up @@ -170,23 +171,86 @@
"""
dqmask = build_mask(model.dq, good_bits)

if weight_type == 'ivm':
if weight_type and weight_type.startswith('ivm'):
weight_type = weight_type.strip()
selective_median = weight_type.startswith('ivm-smed')

bitvalue = interpret_bit_flags(good_bits, mnemonic_map=pixel)
if bitvalue is None:
bitvalue = 0
saturation = pixel['SATURATED']

if selective_median and not (bitvalue & saturation):
selective_median = False
weight_type = 'ivm'

Check warning on line 185 in jwst/resample/resample_utils.py

View check run for this annotation

Codecov / codecov/patch

jwst/resample/resample_utils.py#L184-L185

Added lines #L184 - L185 were not covered by tests

if (model.hasattr("var_rnoise") and model.var_rnoise is not None and
model.var_rnoise.shape == model.data.shape):
with np.errstate(divide="ignore", invalid="ignore"):
inv_variance = model.var_rnoise**-1

inv_variance[~np.isfinite(inv_variance)] = 1

if weight_type != 'ivm':
ny, nx = inv_variance.shape

# apply a median filter to smooth the weight at saturated
# (or high read-out noise) single pixels. keep kernel size
# small to still give lower weight to extended CRs, etc.
ksz = weight_type[8 if selective_median else 7 :]
if ksz:
kernel_size = int(ksz)
if not (kernel_size % 2):
raise ValueError(

Check warning on line 204 in jwst/resample/resample_utils.py

View check run for this annotation

Codecov / codecov/patch

jwst/resample/resample_utils.py#L204

Added line #L204 was not covered by tests
'Kernel size of the median filter in IVM weighting'
' must be an odd integer.'
)
else:
kernel_size = 3

ivm_copy = inv_variance.copy()

if selective_median:
# apply median filter selectively only at
# points of partially saturated sources:
jumps = np.where(model.dq & saturation)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. For the fully saturated case, do we set the saturation bit and the do_not_use bit, or only the do_not_use bit? If the former, we probably want this selection to be (model.dq & saturation) & ~(model.dq & do_not_use) to avoid treating fully saturated pixels as good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. That would be an oversight on my part. Thanks for catching this. Let me check

Copy link
Member Author

@mcara mcara Apr 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything is fine. On line 238 below, this is handled via dqmask itself computed on line 187:

# line 187:
dqmask = build_mask(model.dq, good_bits)
# line 238:
data = ivm_copy[y1:y2, x1:x2][dqmask[y1:y2, x1:x2]]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if user says that fully saturated pixels are "good" (i.e., should not be ignored), then it will be exactly the situation that you describe but that's what the user wants...

Copy link

@schlafly schlafly May 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay. I'm missing something, though. Say there's a fully saturated pixel that has SATURATED & DO_NOT_USE set. It gets an entry in jumps, and so we loop over it. As you point out, we don't include the fully bad pixels in the median we use to replace it (because dqmask doesn't include them), but it does look to me like we would replace the fully bad pixel with the median of its neighbors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you mean (initially I thought you were referring to using DO_NOT_USE pixels in the computation of the median). I still do not think this is an issue because as long as dqmask is 0, the associated weight for that pixel will be set to 0 on line 260 (result = inv_variance * dqmask). However, you are right and thanks for noticing this: it doesn't make sense to compute median if it is going to be ignored.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, I missed that. Thumbs up!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest commit should fix this. Please review

w2 = kernel_size // 2
for r, c in zip(*jumps):
x1 = max(0, c - w2)
x2 = min(nx, c + w2 + 1)
y1 = max(0, r - w2)
y2 = min(ny, r + w2 + 1)
data = ivm_copy[y1:y2, x1:x2][dqmask[y1:y2, x1:x2]]
if data.size:
inv_variance[r, c] = np.median(data)
# else: leave it as is

else:
# apply median to the entire inv-var array:
inv_variance = median_filter(
inv_variance,
size=kernel_size
)
bad_dqmask = np.logical_not(dqmask)
inv_variance[bad_dqmask] = ivm_copy[bad_dqmask]

else:
warnings.warn("var_rnoise array not available. Setting drizzle weight map to 1",
RuntimeWarning)
warnings.warn(
"var_rnoise array not available. "
"Setting drizzle weight map to 1",
RuntimeWarning
)
inv_variance = 1.0

result = inv_variance * dqmask

elif weight_type == 'exptime':
if _check_for_tmeasure(model):
exptime = model.meta.exposure.measurement_time
else:
exptime = model.meta.exposure.exposure_time
result = exptime * dqmask

else:
result = np.ones(model.data.shape, dtype=model.data.dtype) * dqmask

Expand Down
41 changes: 41 additions & 0 deletions jwst/resample/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


DO_NOT_USE = dqflags.pixel["DO_NOT_USE"]
SATURATED = dqflags.pixel["SATURATED"]
GOOD = dqflags.pixel["GOOD"]


Expand Down Expand Up @@ -127,6 +128,46 @@ def test_build_driz_weight(weight_type):
assert weight_map.dtype == np.float32


def test_build_driz_weight_median():
"""Check that correct weight map is returned of different weight types"""
model = ImageModel((10, 10))
model.dq[0] = DO_NOT_USE
model.meta.exposure.exposure_time = 10.0
model.var_rnoise += 0.1
model.var_rnoise[5, 5] = 1000

weight_map = build_driz_weight(
model,
weight_type="ivm-med3",
good_bits="SATURATED"
)
assert_array_equal(weight_map[0], 0)
assert_array_equal(weight_map[1:], 10.0)
assert weight_map.dtype == np.float32


def test_build_driz_weight_selective_median():
"""Check that correct weight map is returned of different weight types"""
model = ImageModel((10, 10))
model.dq[0] = DO_NOT_USE
model.meta.exposure.exposure_time = 10.0
model.var_rnoise += 0.1
model.var_rnoise[0, 5] = 1000
model.var_rnoise[5, 5] = 1000
model.dq[5, 5] = SATURATED
model.dq[0, 5] |= SATURATED

weight_map = build_driz_weight(
model,
weight_type="ivm-smed",
good_bits="SATURATED"
)

assert_array_equal(weight_map[0], 0)
assert_array_equal(weight_map[1:], 10.0)
assert weight_map.dtype == np.float32


@pytest.mark.parametrize("weight_type", ["ivm", None])
def test_build_driz_weight_zeros(weight_type):
"""Check that zero or not finite weight maps get set to 1"""
Expand Down
Loading