stdpipe 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stdpipe/__init__.py +9 -0
- stdpipe/artefacts.py +370 -0
- stdpipe/astrometry.py +2001 -0
- stdpipe/astrometry_quad.py +1643 -0
- stdpipe/astrometry_wcs.py +1141 -0
- stdpipe/catalogs.py +942 -0
- stdpipe/cutouts.py +570 -0
- stdpipe/data/legacysurvey_bricks.fits.gz +0 -0
- stdpipe/data/ps1skycells.txt +199708 -0
- stdpipe/db.py +130 -0
- stdpipe/fringe_removal.py +1086 -0
- stdpipe/lcs.py +440 -0
- stdpipe/photometry.py +2340 -0
- stdpipe/photometry_background.py +998 -0
- stdpipe/photometry_iraf.py +913 -0
- stdpipe/photometry_measure.py +2268 -0
- stdpipe/photometry_model.py +931 -0
- stdpipe/photometry_psf.py +1124 -0
- stdpipe/photometry_quality.py +185 -0
- stdpipe/pipeline.py +1303 -0
- stdpipe/plots.py +1582 -0
- stdpipe/psf.py +1542 -0
- stdpipe/realbogus.py +971 -0
- stdpipe/realbogus_features.py +1657 -0
- stdpipe/reproject.py +943 -0
- stdpipe/resolve.py +267 -0
- stdpipe/sfft.py +1077 -0
- stdpipe/simulation.py +2131 -0
- stdpipe/smoothing.py +488 -0
- stdpipe/subtraction.py +1324 -0
- stdpipe/templates.py +929 -0
- stdpipe/utils.py +365 -0
- stdpipe-0.4.0.dist-info/METADATA +171 -0
- stdpipe-0.4.0.dist-info/RECORD +37 -0
- stdpipe-0.4.0.dist-info/WHEEL +5 -0
- stdpipe-0.4.0.dist-info/licenses/LICENSE.md +21 -0
- stdpipe-0.4.0.dist-info/top_level.txt +1 -0
stdpipe/reproject.py
ADDED
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image reprojection routines.
|
|
3
|
+
|
|
4
|
+
Provides :func:`reproject_swarp` (SWarp wrapper) and :func:`reproject_lanczos`
|
|
5
|
+
(pure-Python Lanczos interpolation with SWarp-style oversampling and Jacobian
|
|
6
|
+
flux conservation).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
import shlex
|
|
14
|
+
import time
|
|
15
|
+
import shutil
|
|
16
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
17
|
+
|
|
18
|
+
from astropy.wcs import WCS
|
|
19
|
+
from astropy.wcs.utils import proj_plane_pixel_scales
|
|
20
|
+
from astropy.io import fits
|
|
21
|
+
|
|
22
|
+
from . import utils
|
|
23
|
+
from . import astrometry
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _pixel_to_pixel(wcs_from, wcs_to, x, y):
|
|
27
|
+
"""Map pixel coordinates from one WCS to another.
|
|
28
|
+
|
|
29
|
+
Like :func:`astropy.wcs.utils.pixel_to_pixel` but uses ``quiet=True``
|
|
30
|
+
for the inverse SIP transformation so that non-converging solutions do
|
|
31
|
+
not raise an exception. Sky positions outside the target projection
|
|
32
|
+
domain map to NaN. Note that pixels where the iterative SIP inversion
|
|
33
|
+
fails to converge get best-effort (possibly inaccurate) coordinates
|
|
34
|
+
rather than NaN; in practice such divergent solutions land far outside
|
|
35
|
+
the image and are rejected by the bounds checks downstream.
|
|
36
|
+
"""
|
|
37
|
+
sky = wcs_from.all_pix2world(x, y, 0)
|
|
38
|
+
try:
|
|
39
|
+
pix = wcs_to.all_world2pix(sky[0], sky[1], 0, quiet=True)
|
|
40
|
+
except TypeError:
|
|
41
|
+
# Very old astropy without quiet= support
|
|
42
|
+
pix = wcs_to.all_world2pix(sky[0], sky[1], 0)
|
|
43
|
+
return pix
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Lanczos helpers
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _lanczos_kernel(x, a):
|
|
52
|
+
"""Lanczos kernel of order *a*."""
|
|
53
|
+
x = np.asarray(x, dtype=np.float64)
|
|
54
|
+
result = np.zeros_like(x)
|
|
55
|
+
mask = np.abs(x) < a
|
|
56
|
+
zero = x == 0
|
|
57
|
+
result[zero] = 1.0
|
|
58
|
+
nonzero = mask & ~zero
|
|
59
|
+
xn = x[nonzero]
|
|
60
|
+
result[nonzero] = np.sin(np.pi * xn) * np.sin(np.pi * xn / a) / (np.pi * xn * np.pi * xn / a)
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _lanczos_map_coordinates(image, coords, a=3, cval=np.nan, weight=None, min_weight=0.5):
|
|
65
|
+
"""Interpolate *image* at fractional pixel coordinates using Lanczos kernel.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
image : 2D array
|
|
70
|
+
coords : (2, N) array of (row, col) coordinates
|
|
71
|
+
a : int
|
|
72
|
+
Lanczos kernel order (2, 3 or 4 typical).
|
|
73
|
+
cval : float
|
|
74
|
+
Fill value for out-of-bounds pixels.
|
|
75
|
+
weight : 2D array or None
|
|
76
|
+
Optional weight map (1.0 for valid, 0.0 for masked pixels). If
|
|
77
|
+
provided, ``image * weight`` and *weight* are interpolated with the
|
|
78
|
+
same kernel and their ratio is returned (normalized convolution), so
|
|
79
|
+
masked pixels are excluded from the interpolation. The image is
|
|
80
|
+
expected to have finite values (e.g. zero) at masked pixels.
|
|
81
|
+
min_weight : float
|
|
82
|
+
With *weight*: points receiving less than this fraction of their
|
|
83
|
+
kernel mass from valid pixels are set to *cval*.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
values : 1D array of interpolated values
|
|
88
|
+
|
|
89
|
+
Notes
|
|
90
|
+
-----
|
|
91
|
+
Kernel taps falling outside the image are replicated from the nearest
|
|
92
|
+
edge pixel (clamped indexing). Without *weight*, NaN pixels in the
|
|
93
|
+
input propagate to every output value whose ``2*a x 2*a`` kernel
|
|
94
|
+
support includes them.
|
|
95
|
+
"""
|
|
96
|
+
ny, nx = image.shape
|
|
97
|
+
n_pts = coords.shape[1]
|
|
98
|
+
result = np.full(n_pts, cval, dtype=np.float64)
|
|
99
|
+
|
|
100
|
+
yr, xr = coords[0], coords[1]
|
|
101
|
+
|
|
102
|
+
# Filter out-of-bounds
|
|
103
|
+
valid = (yr >= -0.5) & (yr < ny - 0.5) & (xr >= -0.5) & (xr < nx - 0.5)
|
|
104
|
+
|
|
105
|
+
yr_v = yr[valid]
|
|
106
|
+
xr_v = xr[valid]
|
|
107
|
+
|
|
108
|
+
if len(yr_v) == 0:
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
# Integer and fractional parts
|
|
112
|
+
iy = np.floor(yr_v).astype(int)
|
|
113
|
+
ix = np.floor(xr_v).astype(int)
|
|
114
|
+
fy = yr_v - iy
|
|
115
|
+
fx = xr_v - ix
|
|
116
|
+
|
|
117
|
+
# Kernel support: -a+1 to a
|
|
118
|
+
offsets = np.arange(-a + 1, a + 1)
|
|
119
|
+
|
|
120
|
+
# Precompute kernels: shape (n_valid, 2*a)
|
|
121
|
+
ky = _lanczos_kernel(fy[:, None] - offsets[None, :], a)
|
|
122
|
+
kx = _lanczos_kernel(fx[:, None] - offsets[None, :], a)
|
|
123
|
+
|
|
124
|
+
# Normalize
|
|
125
|
+
ky /= ky.sum(axis=1, keepdims=True)
|
|
126
|
+
kx /= kx.sum(axis=1, keepdims=True)
|
|
127
|
+
|
|
128
|
+
# Row and column indices
|
|
129
|
+
row_idx = np.clip(iy[:, None] + offsets[None, :], 0, ny - 1)
|
|
130
|
+
col_idx = np.clip(ix[:, None] + offsets[None, :], 0, nx - 1)
|
|
131
|
+
|
|
132
|
+
# Apply separable kernel (vectorized: 2a iters instead of 4a²)
|
|
133
|
+
vals = np.zeros(len(yr_v))
|
|
134
|
+
if weight is None:
|
|
135
|
+
for j in range(len(offsets)):
|
|
136
|
+
row_pixels = image[row_idx[:, j][:, None], col_idx] # (n_valid, 2a)
|
|
137
|
+
vals += ky[:, j] * np.sum(kx * row_pixels, axis=1)
|
|
138
|
+
|
|
139
|
+
result[valid] = vals
|
|
140
|
+
else:
|
|
141
|
+
# Normalized convolution: interpolate image*weight and weight with
|
|
142
|
+
# the same kernel, take the ratio. The denominator is the fraction
|
|
143
|
+
# of kernel mass on valid pixels (exactly 1 where all are valid).
|
|
144
|
+
dens = np.zeros(len(yr_v))
|
|
145
|
+
for j in range(len(offsets)):
|
|
146
|
+
rows = (row_idx[:, j][:, None], col_idx)
|
|
147
|
+
row_weight = weight[rows] # (n_valid, 2a)
|
|
148
|
+
vals += ky[:, j] * np.sum(kx * image[rows] * row_weight, axis=1)
|
|
149
|
+
dens += ky[:, j] * np.sum(kx * row_weight, axis=1)
|
|
150
|
+
|
|
151
|
+
out = np.full(len(yr_v), cval, dtype=np.float64)
|
|
152
|
+
good = dens >= min_weight
|
|
153
|
+
out[good] = vals[good] / dens[good]
|
|
154
|
+
result[valid] = out
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _local_area_ratio(x_in, y_in, fallback):
|
|
160
|
+
"""Per-pixel Jacobian area ratio of the output->input pixel mapping.
|
|
161
|
+
|
|
162
|
+
Computes ``|det d(x_in, y_in) / d(x_out, y_out)|`` by finite differences
|
|
163
|
+
of the coordinate grids, so it follows SIP distortion and
|
|
164
|
+
projection-induced pixel scale variation across the field.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
x_in, y_in : 2D arrays
|
|
169
|
+
Input pixel coordinates sampled on the (unit-spaced) output grid.
|
|
170
|
+
fallback : float
|
|
171
|
+
Value used where the determinant cannot be computed (grid smaller
|
|
172
|
+
than 2 pixels along an axis, or non-finite coordinates).
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
area : 2D array or float scalar
|
|
177
|
+
"""
|
|
178
|
+
if x_in.shape[0] < 2 or x_in.shape[1] < 2:
|
|
179
|
+
return fallback
|
|
180
|
+
|
|
181
|
+
dx_dc = np.gradient(x_in, axis=1)
|
|
182
|
+
dx_dr = np.gradient(x_in, axis=0)
|
|
183
|
+
dy_dc = np.gradient(y_in, axis=1)
|
|
184
|
+
dy_dr = np.gradient(y_in, axis=0)
|
|
185
|
+
|
|
186
|
+
area = np.abs(dx_dc * dy_dr - dx_dr * dy_dc)
|
|
187
|
+
area[~np.isfinite(area)] = fallback
|
|
188
|
+
return area
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _reproject_single_flags(image, wcs_in, wcs_out, shape_out, oversamp=None):
|
|
192
|
+
"""Reproject a single integer flag image using nearest-neighbor sampling.
|
|
193
|
+
|
|
194
|
+
When output pixels are larger than input ones (*oversamp* > 1, chosen
|
|
195
|
+
automatically by default), each output pixel is sampled on an
|
|
196
|
+
``oversamp x oversamp`` sub-pixel grid and the flags of all sampled
|
|
197
|
+
input pixels are combined with bitwise OR, so that isolated flagged
|
|
198
|
+
pixels are not lost in downscaling (like SWarp RESAMPLING_TYPE=FLAGS).
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
result : 2D integer array (0 where no data)
|
|
203
|
+
footprint : 2D float array (fractional coverage 0.0–1.0)
|
|
204
|
+
"""
|
|
205
|
+
ny_out, nx_out = shape_out
|
|
206
|
+
image = np.asarray(image)
|
|
207
|
+
dtype = image.dtype
|
|
208
|
+
ny_in, nx_in = image.shape
|
|
209
|
+
|
|
210
|
+
if oversamp is None:
|
|
211
|
+
area_ratio = np.prod(proj_plane_pixel_scales(wcs_out)) / np.prod(
|
|
212
|
+
proj_plane_pixel_scales(wcs_in)
|
|
213
|
+
)
|
|
214
|
+
oversamp = max(1, int(np.sqrt(area_ratio) + 0.5))
|
|
215
|
+
|
|
216
|
+
if np.issubdtype(dtype, np.floating):
|
|
217
|
+
# Float "flags" cannot be combined bitwise - single-sample nearest
|
|
218
|
+
oversamp = 1
|
|
219
|
+
|
|
220
|
+
step = 1.0 / oversamp
|
|
221
|
+
sub_offsets = np.arange(oversamp) * step + step / 2 - 0.5 if oversamp > 1 else [0.0]
|
|
222
|
+
n_sub = len(sub_offsets) ** 2
|
|
223
|
+
|
|
224
|
+
yy, xx = np.mgrid[0:ny_out, 0:nx_out]
|
|
225
|
+
|
|
226
|
+
if np.issubdtype(dtype, np.floating):
|
|
227
|
+
result = np.full(ny_out * nx_out, np.nan, dtype=dtype)
|
|
228
|
+
else:
|
|
229
|
+
result = np.zeros(ny_out * nx_out, dtype=dtype)
|
|
230
|
+
count = np.zeros(ny_out * nx_out, dtype=np.int32)
|
|
231
|
+
|
|
232
|
+
for dy_off in sub_offsets:
|
|
233
|
+
for dx_off in sub_offsets:
|
|
234
|
+
pixel_in = _pixel_to_pixel(
|
|
235
|
+
wcs_out,
|
|
236
|
+
wcs_in,
|
|
237
|
+
(xx + dx_off).ravel().astype(float),
|
|
238
|
+
(yy + dy_off).ravel().astype(float),
|
|
239
|
+
)
|
|
240
|
+
px = np.asarray(pixel_in[0])
|
|
241
|
+
py = np.asarray(pixel_in[1])
|
|
242
|
+
|
|
243
|
+
# NaN coordinates (outside projection domain) cannot be cast to int
|
|
244
|
+
finite = np.isfinite(px) & np.isfinite(py)
|
|
245
|
+
ix = np.full(px.shape, -1, dtype=int)
|
|
246
|
+
iy = np.full(py.shape, -1, dtype=int)
|
|
247
|
+
ix[finite] = np.round(px[finite]).astype(int)
|
|
248
|
+
iy[finite] = np.round(py[finite]).astype(int)
|
|
249
|
+
|
|
250
|
+
valid = (ix >= 0) & (ix < nx_in) & (iy >= 0) & (iy < ny_in)
|
|
251
|
+
|
|
252
|
+
if np.issubdtype(dtype, np.floating):
|
|
253
|
+
result[valid] = image[iy[valid], ix[valid]]
|
|
254
|
+
else:
|
|
255
|
+
result[valid] |= image[iy[valid], ix[valid]]
|
|
256
|
+
count[valid] += 1
|
|
257
|
+
|
|
258
|
+
footprint = (count.astype(np.float64) / n_sub).reshape(shape_out)
|
|
259
|
+
return result.reshape(shape_out), footprint
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _reproject_chunk(
|
|
263
|
+
image,
|
|
264
|
+
wcs_in,
|
|
265
|
+
wcs_out,
|
|
266
|
+
row_start,
|
|
267
|
+
row_end,
|
|
268
|
+
nx_out,
|
|
269
|
+
order,
|
|
270
|
+
oversamp,
|
|
271
|
+
conserve_flux,
|
|
272
|
+
area_fallback,
|
|
273
|
+
sub_offsets,
|
|
274
|
+
weight=None,
|
|
275
|
+
):
|
|
276
|
+
"""Reproject a horizontal chunk of rows. Used by :func:`_reproject_single`.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
conserve_flux : bool
|
|
281
|
+
If True, multiply by the local Jacobian area ratio (per pixel).
|
|
282
|
+
area_fallback : float
|
|
283
|
+
Global mean area ratio, used where the local Jacobian cannot be
|
|
284
|
+
computed.
|
|
285
|
+
weight : 2D array or None
|
|
286
|
+
Optional weight map for masked input pixels, passed to
|
|
287
|
+
:func:`_lanczos_map_coordinates`.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
row_start : int
|
|
292
|
+
result : 2D array (NaN where no data)
|
|
293
|
+
footprint : 2D float array (fractional coverage 0.0–1.0)
|
|
294
|
+
"""
|
|
295
|
+
ny_chunk = row_end - row_start
|
|
296
|
+
chunk_shape = (ny_chunk, nx_out)
|
|
297
|
+
|
|
298
|
+
yy, xx = np.mgrid[row_start:row_end, 0:nx_out]
|
|
299
|
+
|
|
300
|
+
if oversamp <= 1:
|
|
301
|
+
pixel_in = _pixel_to_pixel(
|
|
302
|
+
wcs_out, wcs_in, xx.ravel().astype(float), yy.ravel().astype(float)
|
|
303
|
+
)
|
|
304
|
+
x_in = np.asarray(pixel_in[0]).reshape(chunk_shape)
|
|
305
|
+
y_in = np.asarray(pixel_in[1]).reshape(chunk_shape)
|
|
306
|
+
coords = np.array([y_in.ravel(), x_in.ravel()])
|
|
307
|
+
values = _lanczos_map_coordinates(image, coords, a=order, weight=weight)
|
|
308
|
+
|
|
309
|
+
area = _local_area_ratio(x_in, y_in, area_fallback) if conserve_flux else 1.0
|
|
310
|
+
result = values.reshape(chunk_shape) * area
|
|
311
|
+
footprint = np.isfinite(result).astype(np.float64)
|
|
312
|
+
return row_start, result, footprint
|
|
313
|
+
|
|
314
|
+
accumulator = np.zeros(chunk_shape, dtype=np.float64)
|
|
315
|
+
count = np.zeros(chunk_shape, dtype=np.int32)
|
|
316
|
+
n_sub = len(sub_offsets) ** 2
|
|
317
|
+
area = None
|
|
318
|
+
|
|
319
|
+
for dy_off in sub_offsets:
|
|
320
|
+
for dx_off in sub_offsets:
|
|
321
|
+
pixel_out_x = (xx + dx_off).ravel().astype(float)
|
|
322
|
+
pixel_out_y = (yy + dy_off).ravel().astype(float)
|
|
323
|
+
|
|
324
|
+
pixel_in = _pixel_to_pixel(wcs_out, wcs_in, pixel_out_x, pixel_out_y)
|
|
325
|
+
x_in = np.asarray(pixel_in[0]).reshape(chunk_shape)
|
|
326
|
+
y_in = np.asarray(pixel_in[1]).reshape(chunk_shape)
|
|
327
|
+
|
|
328
|
+
if conserve_flux and area is None:
|
|
329
|
+
# The Jacobian varies negligibly over a sub-pixel offset,
|
|
330
|
+
# so one sub-grid is enough to estimate it
|
|
331
|
+
area = _local_area_ratio(x_in, y_in, area_fallback)
|
|
332
|
+
|
|
333
|
+
values = _lanczos_map_coordinates(
|
|
334
|
+
image, np.array([y_in.ravel(), x_in.ravel()]), a=order, weight=weight
|
|
335
|
+
)
|
|
336
|
+
vals_2d = values.reshape(chunk_shape)
|
|
337
|
+
|
|
338
|
+
valid = np.isfinite(vals_2d)
|
|
339
|
+
accumulator[valid] += vals_2d[valid]
|
|
340
|
+
count[valid] += 1
|
|
341
|
+
|
|
342
|
+
if area is None:
|
|
343
|
+
area = 1.0
|
|
344
|
+
|
|
345
|
+
result = np.full(chunk_shape, np.nan, dtype=np.float64)
|
|
346
|
+
good = count > 0
|
|
347
|
+
area_good = area[good] if isinstance(area, np.ndarray) else area
|
|
348
|
+
result[good] = (accumulator[good] / count[good]) * area_good
|
|
349
|
+
footprint = count.astype(np.float64) / n_sub
|
|
350
|
+
return row_start, result, footprint
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _reproject_single(
|
|
354
|
+
image,
|
|
355
|
+
wcs_in,
|
|
356
|
+
wcs_out,
|
|
357
|
+
shape_out,
|
|
358
|
+
order,
|
|
359
|
+
conserve_flux,
|
|
360
|
+
oversamp,
|
|
361
|
+
weight_nans=True,
|
|
362
|
+
parallel=False,
|
|
363
|
+
):
|
|
364
|
+
"""Reproject a single image with Lanczos interpolation.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
weight_nans : bool
|
|
369
|
+
If True, handle NaN input pixels through a SWarp-style weight map
|
|
370
|
+
(normalized convolution) instead of propagating them.
|
|
371
|
+
parallel : bool or int
|
|
372
|
+
If True, use threads (number chosen automatically).
|
|
373
|
+
If int > 1, use that many threads.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
result : 2D array (NaN where no data)
|
|
378
|
+
footprint : 2D float array (fractional coverage 0.0–1.0)
|
|
379
|
+
"""
|
|
380
|
+
ny_out, nx_out = shape_out
|
|
381
|
+
image = np.asarray(image, dtype=np.float64)
|
|
382
|
+
|
|
383
|
+
# SWarp-style weight map for masked (NaN) input pixels; skipped
|
|
384
|
+
# entirely for clean images where it would be a no-op
|
|
385
|
+
weight = None
|
|
386
|
+
if weight_nans and not np.all(np.isfinite(image)):
|
|
387
|
+
weight = np.isfinite(image).astype(np.float64)
|
|
388
|
+
image = np.where(weight > 0, image, 0.0)
|
|
389
|
+
|
|
390
|
+
# Global mean pixel area ratio (product of per-axis scales handles
|
|
391
|
+
# non-square pixels); >1 means output pixels cover more sky
|
|
392
|
+
area_fallback = np.prod(proj_plane_pixel_scales(wcs_out)) / np.prod(
|
|
393
|
+
proj_plane_pixel_scales(wcs_in)
|
|
394
|
+
)
|
|
395
|
+
scale_ratio = np.sqrt(area_fallback)
|
|
396
|
+
|
|
397
|
+
# Auto oversampling
|
|
398
|
+
if oversamp is None:
|
|
399
|
+
oversamp = max(1, int(scale_ratio + 0.5))
|
|
400
|
+
|
|
401
|
+
# Sub-pixel offsets for oversampling
|
|
402
|
+
if oversamp > 1:
|
|
403
|
+
step = 1.0 / oversamp
|
|
404
|
+
sub_offsets = np.arange(oversamp) * step + step / 2 - 0.5
|
|
405
|
+
else:
|
|
406
|
+
sub_offsets = None
|
|
407
|
+
|
|
408
|
+
# Determine number of workers
|
|
409
|
+
if parallel is True:
|
|
410
|
+
n_workers = min(os.cpu_count() or 1, ny_out)
|
|
411
|
+
elif isinstance(parallel, int) and parallel > 1:
|
|
412
|
+
n_workers = min(parallel, ny_out)
|
|
413
|
+
else:
|
|
414
|
+
n_workers = 1
|
|
415
|
+
|
|
416
|
+
if n_workers <= 1:
|
|
417
|
+
# Sequential path
|
|
418
|
+
_, result, footprint = _reproject_chunk(
|
|
419
|
+
image,
|
|
420
|
+
wcs_in,
|
|
421
|
+
wcs_out,
|
|
422
|
+
0,
|
|
423
|
+
ny_out,
|
|
424
|
+
nx_out,
|
|
425
|
+
order,
|
|
426
|
+
oversamp,
|
|
427
|
+
conserve_flux,
|
|
428
|
+
area_fallback,
|
|
429
|
+
sub_offsets,
|
|
430
|
+
weight,
|
|
431
|
+
)
|
|
432
|
+
return result, footprint
|
|
433
|
+
|
|
434
|
+
# Threaded path — split by row chunks
|
|
435
|
+
chunk_size = max(1, (ny_out + n_workers - 1) // n_workers)
|
|
436
|
+
row_ranges = []
|
|
437
|
+
for i in range(n_workers):
|
|
438
|
+
rs = i * chunk_size
|
|
439
|
+
re = min(rs + chunk_size, ny_out)
|
|
440
|
+
if rs < re:
|
|
441
|
+
row_ranges.append((rs, re))
|
|
442
|
+
|
|
443
|
+
result = np.full(shape_out, np.nan, dtype=np.float64)
|
|
444
|
+
footprint = np.zeros(shape_out, dtype=np.float64)
|
|
445
|
+
with ThreadPoolExecutor(max_workers=len(row_ranges)) as pool:
|
|
446
|
+
# WCS transformations are not guaranteed thread-safe when sharing
|
|
447
|
+
# a single object, so give each worker its own copies
|
|
448
|
+
futures = [
|
|
449
|
+
pool.submit(
|
|
450
|
+
_reproject_chunk,
|
|
451
|
+
image,
|
|
452
|
+
wcs_in.deepcopy(),
|
|
453
|
+
wcs_out.deepcopy(),
|
|
454
|
+
rs,
|
|
455
|
+
re,
|
|
456
|
+
nx_out,
|
|
457
|
+
order,
|
|
458
|
+
oversamp,
|
|
459
|
+
conserve_flux,
|
|
460
|
+
area_fallback,
|
|
461
|
+
sub_offsets,
|
|
462
|
+
weight,
|
|
463
|
+
)
|
|
464
|
+
for rs, re in row_ranges
|
|
465
|
+
]
|
|
466
|
+
for f in futures:
|
|
467
|
+
row_start, chunk, fp_chunk = f.result()
|
|
468
|
+
nrows = chunk.shape[0]
|
|
469
|
+
result[row_start : row_start + nrows] = chunk
|
|
470
|
+
footprint[row_start : row_start + nrows] = fp_chunk
|
|
471
|
+
|
|
472
|
+
return result, footprint
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# ---------------------------------------------------------------------------
|
|
476
|
+
# Public API
|
|
477
|
+
# ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def reproject_lanczos(
|
|
481
|
+
input=None,
|
|
482
|
+
wcs=None,
|
|
483
|
+
shape=None,
|
|
484
|
+
width=None,
|
|
485
|
+
height=None,
|
|
486
|
+
header=None,
|
|
487
|
+
order=3,
|
|
488
|
+
conserve_flux=True,
|
|
489
|
+
oversamp=None,
|
|
490
|
+
is_flags=False,
|
|
491
|
+
use_nans=True,
|
|
492
|
+
weight_nans=True,
|
|
493
|
+
parallel=False,
|
|
494
|
+
return_footprint=False,
|
|
495
|
+
verbose=False,
|
|
496
|
+
):
|
|
497
|
+
"""Reproject images using Lanczos interpolation with automatic oversampling.
|
|
498
|
+
|
|
499
|
+
Implements SWarp-style oversampling (sub-pixel averaging when output pixels
|
|
500
|
+
are larger than input pixels) and Jacobian area scaling for flux
|
|
501
|
+
conservation.
|
|
502
|
+
|
|
503
|
+
Accepts the same input format as :func:`reproject_swarp`: a list of
|
|
504
|
+
``(image, header/WCS)`` tuples or a list of FITS filenames. For multiple
|
|
505
|
+
inputs the reprojected frames are averaged (simple coadd).
|
|
506
|
+
|
|
507
|
+
Parameters
|
|
508
|
+
----------
|
|
509
|
+
input : list or tuple
|
|
510
|
+
List of ``(image, header_or_wcs)`` tuples or FITS filenames.
|
|
511
|
+
A single ``(image, header_or_wcs)`` tuple is also accepted
|
|
512
|
+
(wrapped into a list automatically for reproject compatibility).
|
|
513
|
+
wcs : `~astropy.wcs.WCS`, optional
|
|
514
|
+
Output WCS. Overrides any WCS already present in *header*.
|
|
515
|
+
shape : tuple, optional
|
|
516
|
+
Output ``(height, width)``.
|
|
517
|
+
width, height : int, optional
|
|
518
|
+
Output dimensions (alternative to *shape*).
|
|
519
|
+
header : `~astropy.io.fits.Header`, optional
|
|
520
|
+
Output FITS header providing the WCS (unless *wcs* is given) and
|
|
521
|
+
image dimensions (unless *shape*/*width*/*height* are given).
|
|
522
|
+
order : int
|
|
523
|
+
Lanczos kernel order (default 3).
|
|
524
|
+
conserve_flux : bool
|
|
525
|
+
If True (default), multiply by the local Jacobian area ratio of
|
|
526
|
+
the pixel mapping (computed per output pixel by finite
|
|
527
|
+
differences, so it follows SIP distortion and projection-induced
|
|
528
|
+
scale variation across the field) so that *total flux* is
|
|
529
|
+
conserved. If False, *surface brightness* is conserved instead.
|
|
530
|
+
oversamp : int or None
|
|
531
|
+
Sub-pixel oversampling factor per axis. ``None`` (default) selects
|
|
532
|
+
automatically: ``max(1, round(output_scale / input_scale))``.
|
|
533
|
+
is_flags : bool
|
|
534
|
+
If True, treat input as integer flag/mask images: use
|
|
535
|
+
nearest-neighbor resampling (no interpolation) and bitwise AND
|
|
536
|
+
for combining multiple inputs. Only frames actually covering a
|
|
537
|
+
pixel participate in the AND. When output pixels are larger
|
|
538
|
+
than input ones, the flags of all contributing input pixels are
|
|
539
|
+
combined with bitwise OR (controlled by *oversamp*, like SWarp
|
|
540
|
+
RESAMPLING_TYPE=FLAGS), so isolated flagged pixels survive
|
|
541
|
+
downscaling. Overrides *order* and *conserve_flux*.
|
|
542
|
+
use_nans : bool
|
|
543
|
+
If True (default), regions with no input coverage are set to NaN
|
|
544
|
+
for floating-point images, or have all flag bits set (``0xFFFF``
|
|
545
|
+
for 16-bit integers) for flag images. If False, they are set
|
|
546
|
+
to zero instead.
|
|
547
|
+
weight_nans : bool
|
|
548
|
+
If True (default), NaN input pixels are handled SWarp-style
|
|
549
|
+
through an internal weight map: the image (with masked pixels
|
|
550
|
+
zeroed) and the weight are resampled with the same kernel and
|
|
551
|
+
their ratio is taken (normalized convolution), so isolated masked
|
|
552
|
+
pixels do not poison their whole kernel neighbourhood. Output
|
|
553
|
+
pixels receiving less than half of their kernel weight from valid
|
|
554
|
+
pixels remain NaN. If False, any NaN within the kernel support
|
|
555
|
+
propagates to the output. Ignored for flag images.
|
|
556
|
+
parallel : bool or int
|
|
557
|
+
If True, use threads for parallel interpolation (number chosen
|
|
558
|
+
automatically). If int > 1, use that many threads. Gives
|
|
559
|
+
~3-4x speedup on multi-core machines.
|
|
560
|
+
return_footprint : bool
|
|
561
|
+
If True, return ``(coadd, footprint)`` where *footprint* is a
|
|
562
|
+
float array with values between 0.0 (no coverage) and 1.0 (full
|
|
563
|
+
coverage). When oversampling is active, fractional values
|
|
564
|
+
indicate partial sub-pixel coverage. Default is False for
|
|
565
|
+
backward compatibility.
|
|
566
|
+
verbose : bool or callable
|
|
567
|
+
Logging control.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
coadd : 2D `~numpy.ndarray` or None
|
|
572
|
+
Reprojected (and optionally coadded) image.
|
|
573
|
+
footprint : 2D `~numpy.ndarray`
|
|
574
|
+
Coverage map (only returned when ``return_footprint=True``).
|
|
575
|
+
|
|
576
|
+
Notes
|
|
577
|
+
-----
|
|
578
|
+
NaN pixels in the input images are treated as masked. With
|
|
579
|
+
``weight_nans=True`` (default) they are excluded from the
|
|
580
|
+
interpolation through a SWarp-style weight map, and only output
|
|
581
|
+
pixels dominated by masked input (less than half of the kernel
|
|
582
|
+
weight on valid pixels) become NaN. With ``weight_nans=False``
|
|
583
|
+
every output pixel whose Lanczos kernel support (``2*order x
|
|
584
|
+
2*order`` input pixels) contains a NaN becomes NaN. NaN output
|
|
585
|
+
pixels are excluded from the multi-frame average.
|
|
586
|
+
"""
|
|
587
|
+
if input is None:
|
|
588
|
+
input = []
|
|
589
|
+
|
|
590
|
+
# Accept a single (image, header/WCS) tuple for reproject compatibility
|
|
591
|
+
if isinstance(input, tuple) and len(input) == 2 and isinstance(input[0], np.ndarray):
|
|
592
|
+
input = [input]
|
|
593
|
+
|
|
594
|
+
log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
|
|
595
|
+
|
|
596
|
+
# Resolve output geometry
|
|
597
|
+
if header is not None:
|
|
598
|
+
header = header.copy()
|
|
599
|
+
else:
|
|
600
|
+
header = fits.Header({'NAXIS': 2, 'BITPIX': -64, 'EQUINOX': 2000.0})
|
|
601
|
+
|
|
602
|
+
if wcs is not None and wcs.is_celestial:
|
|
603
|
+
astrometry.clear_wcs(header)
|
|
604
|
+
header += wcs.to_header(relax=True)
|
|
605
|
+
|
|
606
|
+
if (width is None or height is None) and shape is not None:
|
|
607
|
+
height, width = shape
|
|
608
|
+
|
|
609
|
+
if width is not None:
|
|
610
|
+
header['NAXIS1'] = width
|
|
611
|
+
if height is not None:
|
|
612
|
+
header['NAXIS2'] = height
|
|
613
|
+
|
|
614
|
+
wcs_out = WCS(header)
|
|
615
|
+
if not wcs_out.is_celestial:
|
|
616
|
+
log("Can't reproject without target WCS")
|
|
617
|
+
return (None, None) if return_footprint else None
|
|
618
|
+
|
|
619
|
+
if 'NAXIS1' not in header or 'NAXIS2' not in header:
|
|
620
|
+
log("Can't reproject without output image dimensions")
|
|
621
|
+
return (None, None) if return_footprint else None
|
|
622
|
+
|
|
623
|
+
shape_out = (header['NAXIS2'], header['NAXIS1'])
|
|
624
|
+
|
|
625
|
+
if is_flags:
|
|
626
|
+
log('Input images will be handled as integer flags')
|
|
627
|
+
|
|
628
|
+
# Collect input frames
|
|
629
|
+
frames = []
|
|
630
|
+
for item in input:
|
|
631
|
+
if isinstance(item, str):
|
|
632
|
+
hdulist = fits.open(item)
|
|
633
|
+
img = hdulist[0].data
|
|
634
|
+
if not is_flags:
|
|
635
|
+
img = img.astype(np.float64)
|
|
636
|
+
wcs_in = WCS(hdulist[0].header)
|
|
637
|
+
hdulist.close()
|
|
638
|
+
else:
|
|
639
|
+
img, hdr_or_wcs = item
|
|
640
|
+
if not is_flags:
|
|
641
|
+
img = np.asarray(img, dtype=np.float64)
|
|
642
|
+
if isinstance(hdr_or_wcs, WCS):
|
|
643
|
+
wcs_in = hdr_or_wcs
|
|
644
|
+
else:
|
|
645
|
+
wcs_in = WCS(hdr_or_wcs)
|
|
646
|
+
frames.append((img, wcs_in))
|
|
647
|
+
|
|
648
|
+
if not frames:
|
|
649
|
+
log("No input frames")
|
|
650
|
+
if return_footprint:
|
|
651
|
+
return None, None
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
# Reproject and combine, frame by frame (flat memory)
|
|
655
|
+
if is_flags:
|
|
656
|
+
# Bitwise AND over covering frames (like SWarp COMBINE_TYPE=AND);
|
|
657
|
+
# frames not covering a pixel do not clear its flags
|
|
658
|
+
coadd = None
|
|
659
|
+
footprint = None
|
|
660
|
+
for i, (img, wcs_in) in enumerate(frames):
|
|
661
|
+
log('Reprojecting frame %d/%d' % (i + 1, len(frames)))
|
|
662
|
+
result, fp = _reproject_single_flags(
|
|
663
|
+
img, wcs_in, wcs_out, shape_out, oversamp=oversamp
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
if coadd is None:
|
|
667
|
+
if np.issubdtype(result.dtype, np.floating):
|
|
668
|
+
blank = np.nan
|
|
669
|
+
else:
|
|
670
|
+
# All bits set: neutral element for AND, and the
|
|
671
|
+
# "no coverage" sentinel (0xFFFF for 16-bit types)
|
|
672
|
+
blank = np.invert(result.dtype.type(0))
|
|
673
|
+
coadd = np.full(shape_out, blank, dtype=result.dtype)
|
|
674
|
+
footprint = np.zeros(shape_out, dtype=np.float64)
|
|
675
|
+
|
|
676
|
+
covered = fp > 0
|
|
677
|
+
if np.issubdtype(coadd.dtype, np.floating):
|
|
678
|
+
# Float "flags" cannot be combined bitwise; last frame wins
|
|
679
|
+
coadd[covered] = result[covered]
|
|
680
|
+
else:
|
|
681
|
+
coadd[covered] &= result[covered]
|
|
682
|
+
|
|
683
|
+
# Combined footprint: covered if any frame covers the pixel
|
|
684
|
+
footprint = np.maximum(footprint, fp)
|
|
685
|
+
|
|
686
|
+
if not use_nans:
|
|
687
|
+
coadd[footprint == 0] = 0
|
|
688
|
+
else:
|
|
689
|
+
sum_img = np.zeros(shape_out, dtype=np.float64)
|
|
690
|
+
count = np.zeros(shape_out, dtype=np.int32)
|
|
691
|
+
fp_sum = np.zeros(shape_out, dtype=np.float64)
|
|
692
|
+
for i, (img, wcs_in) in enumerate(frames):
|
|
693
|
+
log('Reprojecting frame %d/%d' % (i + 1, len(frames)))
|
|
694
|
+
result, fp = _reproject_single(
|
|
695
|
+
img,
|
|
696
|
+
wcs_in,
|
|
697
|
+
wcs_out,
|
|
698
|
+
shape_out,
|
|
699
|
+
order,
|
|
700
|
+
conserve_flux,
|
|
701
|
+
oversamp,
|
|
702
|
+
weight_nans=weight_nans,
|
|
703
|
+
parallel=parallel,
|
|
704
|
+
)
|
|
705
|
+
valid = np.isfinite(result)
|
|
706
|
+
sum_img[valid] += result[valid]
|
|
707
|
+
count[valid] += 1
|
|
708
|
+
fp_sum += fp
|
|
709
|
+
|
|
710
|
+
coadd = np.full(shape_out, np.nan, dtype=np.float64)
|
|
711
|
+
good = count > 0
|
|
712
|
+
coadd[good] = sum_img[good] / count[good]
|
|
713
|
+
# Average footprint across frames
|
|
714
|
+
footprint = fp_sum / len(frames)
|
|
715
|
+
|
|
716
|
+
if not use_nans:
|
|
717
|
+
coadd[~good] = 0.0
|
|
718
|
+
|
|
719
|
+
if return_footprint:
|
|
720
|
+
return coadd, footprint
|
|
721
|
+
return coadd
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def reproject_swarp(
|
|
725
|
+
input=None,
|
|
726
|
+
wcs=None,
|
|
727
|
+
shape=None,
|
|
728
|
+
width=None,
|
|
729
|
+
height=None,
|
|
730
|
+
header=None,
|
|
731
|
+
extra=None,
|
|
732
|
+
is_flags=False,
|
|
733
|
+
use_nans=True,
|
|
734
|
+
get_weights=False,
|
|
735
|
+
_workdir=None,
|
|
736
|
+
_tmpdir=None,
|
|
737
|
+
_exe=None,
|
|
738
|
+
verbose=False,
|
|
739
|
+
):
|
|
740
|
+
"""
|
|
741
|
+
Wrapper for running SWarp for re-projecting and mosaicking of images onto target WCS grid.
|
|
742
|
+
|
|
743
|
+
It accepts as input either list of filenames, or list of tuples where first
|
|
744
|
+
element is an image, and second one - either FITS header or WCS.
|
|
745
|
+
|
|
746
|
+
If the input images are integer flags, set `is_flags=True` so that it will be handled
|
|
747
|
+
by passing `RESAMPLING_TYPE=FLAGS` and `COMBINE_TYPE=AND`.
|
|
748
|
+
|
|
749
|
+
If `use_nans=True`, the regions with zero weights will be filled with NaNs (or 0xFFFF).
|
|
750
|
+
|
|
751
|
+
Any additional configuration parameter may be passed to SWarp through `extra` argument which
|
|
752
|
+
should be the dictionary with parameter names as keys.
|
|
753
|
+
|
|
754
|
+
"""
|
|
755
|
+
|
|
756
|
+
if input is None:
|
|
757
|
+
input = []
|
|
758
|
+
if extra is None:
|
|
759
|
+
extra = {}
|
|
760
|
+
|
|
761
|
+
# Simple wrapper around print for logging in verbose mode only
|
|
762
|
+
log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
|
|
763
|
+
|
|
764
|
+
# Find the binary
|
|
765
|
+
binname = None
|
|
766
|
+
|
|
767
|
+
if _exe is not None:
|
|
768
|
+
# Check user-provided binary path, and fail if not found
|
|
769
|
+
if os.path.isfile(_exe):
|
|
770
|
+
binname = _exe
|
|
771
|
+
else:
|
|
772
|
+
# Find SWarp binary in common paths
|
|
773
|
+
for exe in ['swarp']:
|
|
774
|
+
binname = shutil.which(exe)
|
|
775
|
+
if binname is not None:
|
|
776
|
+
break
|
|
777
|
+
|
|
778
|
+
if binname is None:
|
|
779
|
+
log("Can't find SWarp binary")
|
|
780
|
+
return None
|
|
781
|
+
# else:
|
|
782
|
+
# log("Using SWarp binary at", binname)
|
|
783
|
+
|
|
784
|
+
if (width is None or height is None) and shape is not None:
|
|
785
|
+
height, width = shape
|
|
786
|
+
|
|
787
|
+
if header is None:
|
|
788
|
+
# Construct minimal FITS header
|
|
789
|
+
header = fits.Header(
|
|
790
|
+
{
|
|
791
|
+
'NAXIS': 2,
|
|
792
|
+
'NAXIS1': width,
|
|
793
|
+
'NAXIS2': height,
|
|
794
|
+
'BITPIX': -64,
|
|
795
|
+
'EQUINOX': 2000.0,
|
|
796
|
+
}
|
|
797
|
+
)
|
|
798
|
+
else:
|
|
799
|
+
header = header.copy()
|
|
800
|
+
|
|
801
|
+
if wcs is not None and wcs.is_celestial:
|
|
802
|
+
# Add WCS information to the header
|
|
803
|
+
astrometry.clear_wcs(header)
|
|
804
|
+
whdr = wcs.to_header(relax=True)
|
|
805
|
+
|
|
806
|
+
if wcs.sip is not None:
|
|
807
|
+
whdr = astrometry.wcs_sip2pv(whdr)
|
|
808
|
+
|
|
809
|
+
# Here we will try to fix some common problems with WCS not supported by SWarp
|
|
810
|
+
# FIXME: handle SIP distortions!
|
|
811
|
+
if wcs.wcs.has_pc() and 'PC1_1' not in whdr:
|
|
812
|
+
pc = wcs.wcs.get_pc()
|
|
813
|
+
whdr['PC1_1'] = pc[0, 0]
|
|
814
|
+
whdr['PC1_2'] = pc[0, 1]
|
|
815
|
+
whdr['PC2_1'] = pc[1, 0]
|
|
816
|
+
whdr['PC2_2'] = pc[1, 1]
|
|
817
|
+
|
|
818
|
+
header += whdr
|
|
819
|
+
else:
|
|
820
|
+
wcs = WCS(header)
|
|
821
|
+
|
|
822
|
+
if wcs is None or not wcs.is_celestial:
|
|
823
|
+
log("Can't re-project without target WCS")
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
workdir = _workdir if _workdir is not None else tempfile.mkdtemp(prefix='swarp', dir=_tmpdir)
|
|
827
|
+
|
|
828
|
+
# Output coadd filename
|
|
829
|
+
coaddname = os.path.join(workdir, 'coadd.fits')
|
|
830
|
+
if os.path.exists(coaddname):
|
|
831
|
+
os.unlink(coaddname)
|
|
832
|
+
|
|
833
|
+
# Input header filename - the result will be re-projected to it
|
|
834
|
+
headername = os.path.join(workdir, 'coadd.head')
|
|
835
|
+
utils.file_write(headername, header.tostring(endcard=True, sep='\n'))
|
|
836
|
+
|
|
837
|
+
# Output weights filename
|
|
838
|
+
weightsname = os.path.join(workdir, 'coadd.weights.fits')
|
|
839
|
+
|
|
840
|
+
# Dummy config filename, to prevent loading from current dir
|
|
841
|
+
confname = os.path.join(workdir, 'empty.conf')
|
|
842
|
+
utils.file_write(confname)
|
|
843
|
+
|
|
844
|
+
xmlname = os.path.join(workdir, 'swarp.xml')
|
|
845
|
+
|
|
846
|
+
opts = {
|
|
847
|
+
'VERBOSE_TYPE': 'QUIET' if not verbose else 'NORMAL',
|
|
848
|
+
'IMAGEOUT_NAME': coaddname,
|
|
849
|
+
'WEIGHTOUT_NAME': weightsname,
|
|
850
|
+
'c': confname,
|
|
851
|
+
'XML_NAME': xmlname,
|
|
852
|
+
'VMEM_DIR': workdir,
|
|
853
|
+
'RESAMPLE_DIR': workdir,
|
|
854
|
+
#
|
|
855
|
+
'SUBTRACT_BACK': False, # Do not subtract the backgrounds
|
|
856
|
+
'FSCALASTRO_TYPE': 'VARIABLE', # and not re-scale the images by default
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if is_flags:
|
|
860
|
+
log('The images will be handled as integer flags')
|
|
861
|
+
opts['RESAMPLING_TYPE'] = 'FLAGS'
|
|
862
|
+
opts['COMBINE_TYPE'] = 'AND' # Use only common flags in overlapping masks
|
|
863
|
+
|
|
864
|
+
opts.update(extra)
|
|
865
|
+
|
|
866
|
+
# Handle input data
|
|
867
|
+
filenames = []
|
|
868
|
+
bzero = 0
|
|
869
|
+
for i, item in enumerate(input):
|
|
870
|
+
if isinstance(item, str):
|
|
871
|
+
# Item is filename already
|
|
872
|
+
filename = item
|
|
873
|
+
elif len(item) == 2:
|
|
874
|
+
# It should be a tuple of image plus header or WCS
|
|
875
|
+
image = item[0]
|
|
876
|
+
header = item[1]
|
|
877
|
+
|
|
878
|
+
if image.dtype.name == 'bool':
|
|
879
|
+
image = image.astype(np.int16)
|
|
880
|
+
|
|
881
|
+
if isinstance(header, WCS):
|
|
882
|
+
header = header.to_header(relax=True)
|
|
883
|
+
|
|
884
|
+
# Convert SIP headers to TPV
|
|
885
|
+
if WCS(header).sip is not None:
|
|
886
|
+
header = astrometry.wcs_sip2pv(header)
|
|
887
|
+
|
|
888
|
+
filename = os.path.join(workdir, 'image_%04d.fits' % i)
|
|
889
|
+
fits.writeto(filename, image, header, overwrite=True)
|
|
890
|
+
|
|
891
|
+
filenames.append(filename)
|
|
892
|
+
bzero = max(
|
|
893
|
+
bzero, fits.getheader(filename).get('BZERO', 0)
|
|
894
|
+
) # Keep the largest BZERO among input files
|
|
895
|
+
|
|
896
|
+
# Build the command line
|
|
897
|
+
command = (
|
|
898
|
+
binname
|
|
899
|
+
+ ' '
|
|
900
|
+
+ utils.format_astromatic_opts(opts)
|
|
901
|
+
+ ' '
|
|
902
|
+
+ ' '.join([shlex.quote(_) for _ in filenames])
|
|
903
|
+
)
|
|
904
|
+
if not verbose:
|
|
905
|
+
command += ' > /dev/null 2>/dev/null'
|
|
906
|
+
log('Will run SWarp like that:')
|
|
907
|
+
log(command)
|
|
908
|
+
|
|
909
|
+
# Run the command!
|
|
910
|
+
|
|
911
|
+
t0 = time.time()
|
|
912
|
+
res = os.system(command)
|
|
913
|
+
t1 = time.time()
|
|
914
|
+
|
|
915
|
+
if res == 0 and os.path.exists(coaddname) and os.path.exists(weightsname):
|
|
916
|
+
log('SWarp run successfully in %.2f seconds' % (t1 - t0))
|
|
917
|
+
|
|
918
|
+
coadd = fits.getdata(coaddname)
|
|
919
|
+
weights = fits.getdata(weightsname)
|
|
920
|
+
|
|
921
|
+
# it seems SWarp adds BZERO to the output if inputs had them (e.g. unsigned ints do)
|
|
922
|
+
# FIXME: this point needs further investigation!
|
|
923
|
+
if np.issubdtype(coadd.dtype.type, int):
|
|
924
|
+
coadd -= bzero
|
|
925
|
+
|
|
926
|
+
if use_nans:
|
|
927
|
+
if np.issubdtype(coadd.dtype, np.floating):
|
|
928
|
+
coadd[weights == 0] = np.nan
|
|
929
|
+
else:
|
|
930
|
+
coadd[weights == 0] = 0xFFFF
|
|
931
|
+
|
|
932
|
+
else:
|
|
933
|
+
log('Error', res, 'running SWarp')
|
|
934
|
+
coadd = None
|
|
935
|
+
weights = None
|
|
936
|
+
|
|
937
|
+
if _workdir is None:
|
|
938
|
+
shutil.rmtree(workdir)
|
|
939
|
+
|
|
940
|
+
if get_weights:
|
|
941
|
+
return coadd, weights
|
|
942
|
+
else:
|
|
943
|
+
return coadd
|