Skip to content

Commit d0179b6

Browse files
authored
Disable bounding box in calc_pixmap (#164)
Allow disabling the bounding box on one or both input WCS
1 parent dab1009 commit d0179b6

File tree

4 files changed

+122
-23
lines changed

4 files changed

+122
-23
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,13 @@ jobs:
2222
envs: |
2323
- linux: check-style
2424
- linux: check-security
25-
2625
- linux: py310-xdist
2726
- linux: py311-xdist
2827
- linux: py313-xdist
2928
- macos: py312-xdist
3029
- windows: py312-xdist
3130
- linux: py312-xdist-cov
3231
coverage: codecov
33-
3432
- linux: py312-xdist-devdeps
3533
secrets:
3634
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

CHANGES.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
Release Notes
55
=============
66

7+
2.0.1 (Unreleased)
8+
==================
9+
10+
- Update ``utils.calc_pixmap`` code to be ready for upcoming changes in GWCS
11+
due to which inverse WCS transformations will respect bounding box by
12+
allowing the caller of ``utils.calc_pixmap`` to disable the bounding box(es)
13+
on both or one of the input WCS when computing the pixel map. [#164]
14+
15+
716
2.0.0 (2024-10-23)
817
==================
918

drizzle/tests/test_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,63 @@ def test_translated_map(wcs_type):
102102
assert_almost_equal(pixmap[2:, 2:], ok_pixmap[2:, 2:], decimal=5)
103103

104104

105+
def test_disable_gwcs_bbox():
106+
"""
107+
Map a pixel array to a translated version ofitself.
108+
"""
109+
first_wcs = wcs_from_file(
110+
"j8bt06nyq_sip_flt.fits",
111+
ext=1,
112+
wcs_type="gwcs"
113+
)
114+
second_wcs = wcs_from_file(
115+
"j8bt06nyq_sip_flt.fits",
116+
ext=1,
117+
crpix_shift=(-2, -2), # shift loaded WCS by adding this to CRPIX
118+
wcs_type="gwcs"
119+
)
120+
121+
ok_pixmap = np.indices(first_wcs.array_shape, dtype='float64') - 2.0
122+
ok_pixmap = ok_pixmap.transpose()
123+
124+
# Mapping an array to a translated array
125+
126+
# disable both bounding boxes:
127+
pixmap = calc_pixmap(first_wcs, second_wcs, disable_bbox="both")
128+
assert_almost_equal(pixmap[2:, 2:], ok_pixmap[2:, 2:], decimal=5)
129+
assert np.all(np.isfinite(pixmap[:2, :2]))
130+
assert np.all(np.isfinite(pixmap[-2:, -2:]))
131+
# check bbox was restored
132+
assert first_wcs.bounding_box is not None
133+
assert second_wcs.bounding_box is not None
134+
135+
# disable "from" bounding box:
136+
pixmap = calc_pixmap(second_wcs, first_wcs, disable_bbox="from")
137+
assert_almost_equal(pixmap[:-2, :-2], ok_pixmap[:-2, :-2] + 4.0, decimal=5)
138+
assert np.all(np.logical_not(np.isfinite(pixmap[-2:, -2:])))
139+
# check bbox was restored
140+
assert first_wcs.bounding_box is not None
141+
assert second_wcs.bounding_box is not None
142+
143+
# disable "to" bounding boxes:
144+
pixmap = calc_pixmap(first_wcs, second_wcs, disable_bbox="to")
145+
assert_almost_equal(pixmap[2:, 2:], ok_pixmap[2:, 2:], decimal=5)
146+
assert np.all(np.isfinite(pixmap[:2, :2]))
147+
assert np.all(pixmap[:2, :2] < 0.0)
148+
assert np.all(np.isfinite(pixmap[-2:, -2:]))
149+
# check bbox was restored
150+
assert first_wcs.bounding_box is not None
151+
assert second_wcs.bounding_box is not None
152+
153+
# enable all bounding boxes:
154+
pixmap = calc_pixmap(first_wcs, second_wcs, disable_bbox="none")
155+
assert_almost_equal(pixmap[2:, 2:], ok_pixmap[2:, 2:], decimal=5)
156+
assert np.all(np.logical_not(np.isfinite(pixmap[:2, :2])))
157+
# check bbox was restored
158+
assert first_wcs.bounding_box is not None
159+
assert second_wcs.bounding_box is not None
160+
161+
105162
def test_estimate_pixel_scale_ratio():
106163
w = wcs_from_file("j8bt06nyq_flt.fits", ext=1)
107164
pscale = estimate_pixel_scale_ratio(w, w, w.wcs.crpix, (0, 0))

drizzle/utils.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
_DEG2RAD = math.pi / 180.0
88

99

10-
def calc_pixmap(wcs_from, wcs_to, shape=None):
10+
def calc_pixmap(wcs_from, wcs_to, shape=None, disable_bbox="to"):
1111
"""
1212
Calculate a discretized on a grid mapping between the pixels of two images
1313
using provided WCS of the original ("from") image and the destination ("to")
@@ -35,6 +35,14 @@ def calc_pixmap(wcs_from, wcs_to, shape=None):
3535
``numpy.ndarray`` order. When provided, it takes precedence over the
3636
``wcs_from.array_shape`` property.
3737
38+
disable_bbox : {"to", "from", "both", "none"}, optional
39+
Indicates whether to use or not to use the bounding box of either
40+
(both) ``wcs_from`` or (and) ``wcs_to`` when computing pixel map. When
41+
``disable_bbox`` is "none", pixel coordinates outside of the bounding
42+
box are set to `NaN` only if ``wcs_from`` or (and) ``wcs_to`` sets
43+
world coordinates to NaN when input pixel coordinates are outside of
44+
the bounding box.
45+
3846
Returns
3947
-------
4048
pixmap : numpy.ndarray
@@ -57,35 +65,62 @@ def calc_pixmap(wcs_from, wcs_to, shape=None):
5765
If ``bounding_box`` is not available, a `ValueError` will be raised.
5866
5967
"""
68+
if (bbox_from := getattr(wcs_from, "bounding_box", None)) is not None:
69+
try:
70+
# to avoid dependency on astropy just to check whether
71+
# the bounding box is an instance of
72+
# modeling.bounding_box.ModelBoundingBox, we try to
73+
# directly use and bounding_box(order='F') and if it fails,
74+
# fall back to converting the bounding box to a tuple
75+
# (of intervals):
76+
bbox_from = bbox_from.bounding_box(order='F')
77+
except AttributeError:
78+
bbox_from = tuple(bbox_from)
79+
80+
if (bbox_to := getattr(wcs_to, "bounding_box", None)) is not None:
81+
try:
82+
# to avoid dependency on astropy just to check whether
83+
# the bounding box is an instance of
84+
# modeling.bounding_box.ModelBoundingBox, we try to
85+
# directly use and bounding_box(order='F') and if it fails,
86+
# fall back to converting the bounding box to a tuple
87+
# (of intervals):
88+
bbox_to = bbox_to.bounding_box(order='F')
89+
except AttributeError:
90+
bbox_to = tuple(bbox_to)
91+
6092
if shape is None:
6193
shape = wcs_from.array_shape
62-
if shape is None:
63-
if (bbox := getattr(wcs_from, "bounding_box", None)) is not None:
64-
try:
65-
# to avoid dependency on astropy just to check whether
66-
# the bounding box is an instance of
67-
# modeling.bounding_box.ModelBoundingBox, we try to
68-
# directly use and bounding_box(order='F') and if it fails,
69-
# fall back to converting the bounding box to a tuple
70-
# (of intervals):
71-
bbox = bbox.bounding_box(order='F')
72-
except AttributeError:
73-
bbox = tuple(bbox)
74-
75-
if (nd := np.ndim(bbox)) == 1:
76-
bbox = (bbox, )
77-
if nd > 1:
78-
shape = tuple(
79-
int(math.ceil(lim[1] + 0.5)) for lim in bbox[::-1]
80-
)
94+
if shape is None and bbox_from is not None:
95+
if (nd := np.ndim(bbox_from)) == 1:
96+
bbox_from = (bbox_from, )
97+
if nd > 1:
98+
shape = tuple(
99+
int(math.ceil(lim[1] + 0.5)) for lim in bbox_from[::-1]
100+
)
81101

82102
if shape is None:
83103
raise ValueError(
84104
'The "from" WCS must have pixel_shape property set.'
85105
)
86106

87107
y, x = np.indices(shape, dtype=np.float64)
88-
x, y = wcs_to.world_to_pixel_values(*wcs_from.pixel_to_world_values(x, y))
108+
109+
# temporarily disable the bounding box for the "from" WCS:
110+
if disable_bbox in ["from", "both"] and bbox_from is not None:
111+
wcs_from.bounding_box = None
112+
if disable_bbox in ["to", "both"] and bbox_to is not None:
113+
wcs_to.bounding_box = None
114+
try:
115+
x, y = wcs_to.world_to_pixel_values(
116+
*wcs_from.pixel_to_world_values(x, y)
117+
)
118+
finally:
119+
if bbox_from is not None:
120+
wcs_from.bounding_box = bbox_from
121+
if bbox_to is not None:
122+
wcs_to.bounding_box = bbox_to
123+
89124
pixmap = np.dstack([x, y])
90125
return pixmap
91126

0 commit comments

Comments
 (0)