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/cutouts.py
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for cropping the images and creating image cutouts / postage stamps
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import datetime
|
|
7
|
+
|
|
8
|
+
from astropy.wcs import WCS
|
|
9
|
+
from astropy.io import fits
|
|
10
|
+
from astropy.time import Time
|
|
11
|
+
|
|
12
|
+
from scipy.stats import chi2
|
|
13
|
+
|
|
14
|
+
from scipy.optimize import minimize
|
|
15
|
+
from scipy.ndimage import shift
|
|
16
|
+
|
|
17
|
+
from . import utils
|
|
18
|
+
from . import astrometry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def crop_image_centered(data, x0, y0, r0, header=None):
|
|
22
|
+
"""Crop a square region centered on a given pixel position.
|
|
23
|
+
|
|
24
|
+
Output size is ``2*ceil(r0) + 1`` pixels. The original center falls
|
|
25
|
+
inside the pixel at ``x, y = ceil(r0), ceil(r0)``. If a FITS header is
|
|
26
|
+
provided, it is adjusted so that the WCS solution remains valid for the
|
|
27
|
+
cutout.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
data : ndarray
|
|
32
|
+
2D image array to crop.
|
|
33
|
+
x0 : float
|
|
34
|
+
X coordinate of the center (0-based).
|
|
35
|
+
y0 : float
|
|
36
|
+
Y coordinate of the center (0-based).
|
|
37
|
+
r0 : float
|
|
38
|
+
Cutout half-size in pixels.
|
|
39
|
+
header : astropy.io.fits.Header, optional
|
|
40
|
+
FITS header to adjust (``CRPIX`` shifted in-place on a copy).
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
ndarray or tuple
|
|
45
|
+
Cropped image, or ``(cropped_image, adjusted_header)`` if ``header`` is provided.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# x1,x2 = int(np.floor(x0 - r0)), int(np.ceil(x0 + r0))
|
|
49
|
+
# y1,y2 = int(np.floor(y0 - r0)), int(np.ceil(y0 + r0))
|
|
50
|
+
x1, x2 = int(np.round(x0) - np.ceil(r0)), int(np.round(x0) + np.ceil(r0))
|
|
51
|
+
y1, y2 = int(np.round(y0) - np.ceil(r0)), int(np.round(y0) + np.ceil(r0))
|
|
52
|
+
|
|
53
|
+
return crop_image(data, x1, y1, x2 - x1 + 1, y2 - y1 + 1, header=header)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def crop_image(data, x1, y1, width, height, header=None):
|
|
57
|
+
"""Crop a rectangular region from an image.
|
|
58
|
+
|
|
59
|
+
Pixels outside the original image boundary are filled with zeros (or NaN
|
|
60
|
+
for floating-point images). If a FITS header is provided, ``CRPIX`` is
|
|
61
|
+
adjusted so that the WCS solution remains valid for the cutout.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
data : ndarray
|
|
66
|
+
2D image array to crop.
|
|
67
|
+
x1 : int
|
|
68
|
+
Left edge of the crop region (0-based).
|
|
69
|
+
y1 : int
|
|
70
|
+
Bottom edge of the crop region (0-based).
|
|
71
|
+
width : int
|
|
72
|
+
Width of the crop region in pixels.
|
|
73
|
+
height : int
|
|
74
|
+
Height of the crop region in pixels.
|
|
75
|
+
header : astropy.io.fits.Header, optional
|
|
76
|
+
FITS header to adjust (copy is modified, original unchanged).
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
ndarray or tuple
|
|
81
|
+
Cropped image, or ``(cropped_image, adjusted_header)`` if ``header`` is provided.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
x2 = x1 + width
|
|
85
|
+
y2 = y1 + height
|
|
86
|
+
|
|
87
|
+
src = [
|
|
88
|
+
min(max(y1, 0), data.shape[0]),
|
|
89
|
+
max(min(y2, data.shape[0]), 0),
|
|
90
|
+
min(max(x1, 0), data.shape[1]),
|
|
91
|
+
max(min(x2, data.shape[1]), 0),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
dst = [src[0] - y1, src[1] - y1, src[2] - x1, src[3] - x1]
|
|
95
|
+
|
|
96
|
+
sub = np.zeros((y2 - y1, x2 - x1), data.dtype)
|
|
97
|
+
if isinstance(data[0][0], np.floating):
|
|
98
|
+
# For floating-point we may use NaN as a filler value
|
|
99
|
+
sub.fill(np.nan)
|
|
100
|
+
sub[dst[0] : dst[1], dst[2] : dst[3]] = data[src[0] : src[1], src[2] : src[3]]
|
|
101
|
+
|
|
102
|
+
if header is not None:
|
|
103
|
+
subheader = header.copy()
|
|
104
|
+
|
|
105
|
+
subheader['NAXIS1'] = sub.shape[1]
|
|
106
|
+
subheader['NAXIS2'] = sub.shape[0]
|
|
107
|
+
|
|
108
|
+
# Adjust the WCS keywords if present
|
|
109
|
+
if 'CRPIX1' in subheader and 'CRPIX2' in subheader:
|
|
110
|
+
subheader['CRPIX1'] -= x1
|
|
111
|
+
subheader['CRPIX2'] -= y1
|
|
112
|
+
|
|
113
|
+
# FIXME: should we use 0-based or 1-based coordinates here?..
|
|
114
|
+
|
|
115
|
+
# Crop position inside original frame
|
|
116
|
+
subheader['CROP_X1'] = x1
|
|
117
|
+
subheader['CROP_X2'] = x2
|
|
118
|
+
subheader['CROP_Y1'] = y1
|
|
119
|
+
subheader['CROP_Y2'] = y2
|
|
120
|
+
|
|
121
|
+
return sub, subheader
|
|
122
|
+
else:
|
|
123
|
+
return sub
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_cutout(
|
|
127
|
+
image, candidate, radius, header=None, wcs=None, time=None, filename=None, name=None, **kwargs
|
|
128
|
+
):
|
|
129
|
+
"""Create a cutout postage stamp from one or more image planes.
|
|
130
|
+
|
|
131
|
+
The candidate may be a row from :class:`astropy.table.Table` or a dict with
|
|
132
|
+
at least ``x`` and ``y`` keys. The cutout is centered so the object falls
|
|
133
|
+
inside the central pixel; size is ``2*ceil(radius) + 1`` pixels.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
image : ndarray
|
|
138
|
+
Primary (science) image plane.
|
|
139
|
+
candidate : table row or dict
|
|
140
|
+
Object record containing at least ``x`` and ``y`` pixel coordinates.
|
|
141
|
+
radius : float
|
|
142
|
+
Cutout half-size in pixels.
|
|
143
|
+
header : astropy.io.fits.Header, optional
|
|
144
|
+
Header of the original image. Copied and adjusted to represent the
|
|
145
|
+
cutout WCS; stored as ``cutout['header']``.
|
|
146
|
+
wcs : astropy.wcs.WCS, optional
|
|
147
|
+
WCS of the original image. Adjusted and stored as ``cutout['wcs']``.
|
|
148
|
+
Takes precedence over ``header`` for WCS.
|
|
149
|
+
time : astropy.time.Time, datetime, or str, optional
|
|
150
|
+
Observation timestamp; stored in ``cutout['meta']['time']``.
|
|
151
|
+
filename : str, optional
|
|
152
|
+
Source image filename; stored in ``cutout['meta']['filename']``.
|
|
153
|
+
name : str, optional
|
|
154
|
+
Object name; stored in ``cutout['meta']['name']``. If omitted and
|
|
155
|
+
``candidate`` has ``ra`` / ``dec``, a J2000 name is constructed.
|
|
156
|
+
**kwargs
|
|
157
|
+
Additional 2D arrays interpreted as extra image planes
|
|
158
|
+
(e.g. ``mask``, ``diff``, ``template``, ``convolved``, ``err``).
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
dict
|
|
163
|
+
Cutout dictionary with at least:
|
|
164
|
+
|
|
165
|
+
- ``image`` — primary image plane
|
|
166
|
+
- ``mask``, ``diff``, ``template``, etc. — extra planes if provided
|
|
167
|
+
- ``header`` — adjusted FITS header, if provided
|
|
168
|
+
- ``wcs`` — adjusted WCS, if provided
|
|
169
|
+
- ``meta`` — dict with all candidate fields plus ``time``, ``filename``,
|
|
170
|
+
and ``name``
|
|
171
|
+
"""
|
|
172
|
+
x0, y0 = candidate['x'], candidate['y']
|
|
173
|
+
|
|
174
|
+
_ = crop_image_centered(image, x0, y0, radius, header=header)
|
|
175
|
+
if header is not None:
|
|
176
|
+
crop, crophead = _
|
|
177
|
+
else:
|
|
178
|
+
crop, crophead = _, None
|
|
179
|
+
|
|
180
|
+
cutout = {'image': crop, 'meta': {}}
|
|
181
|
+
|
|
182
|
+
if wcs is not None:
|
|
183
|
+
cutout['wcs'] = wcs
|
|
184
|
+
|
|
185
|
+
# Let's update header info with this WCS
|
|
186
|
+
astrometry.clear_wcs(crophead, remove_underscored=True, remove_history=True)
|
|
187
|
+
crophead += wcs.to_header(relax=True)
|
|
188
|
+
|
|
189
|
+
if crophead is not None:
|
|
190
|
+
cutout['header'] = crophead
|
|
191
|
+
|
|
192
|
+
if wcs is None:
|
|
193
|
+
wcs = WCS(crophead)
|
|
194
|
+
cutout['wcs'] = wcs
|
|
195
|
+
|
|
196
|
+
# Image planes
|
|
197
|
+
for pname, plane in kwargs.items():
|
|
198
|
+
if plane is not None and np.ndim(plane) == 2:
|
|
199
|
+
cutout[pname] = crop_image_centered(plane, x0, y0, radius)
|
|
200
|
+
|
|
201
|
+
# Metadata
|
|
202
|
+
for _ in candidate.keys():
|
|
203
|
+
cutout['meta'][_] = candidate[_]
|
|
204
|
+
|
|
205
|
+
# Additional metadata to add or override
|
|
206
|
+
if time is not None:
|
|
207
|
+
cutout['meta']['time'] = Time(time)
|
|
208
|
+
|
|
209
|
+
if filename is not None:
|
|
210
|
+
cutout['meta']['filename'] = filename
|
|
211
|
+
|
|
212
|
+
if name is not None:
|
|
213
|
+
cutout['meta']['name'] = name
|
|
214
|
+
elif (
|
|
215
|
+
'name' not in cutout['meta']
|
|
216
|
+
and 'ra' in cutout['meta'].keys()
|
|
217
|
+
and 'dec' in cutout['meta'].keys()
|
|
218
|
+
):
|
|
219
|
+
cutout['meta']['name'] = utils.make_jname(candidate['ra'], candidate['dec'])
|
|
220
|
+
|
|
221
|
+
return cutout
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def write_cutout(cutout, filename):
|
|
225
|
+
"""Store a cutout as a multi-extension FITS file.
|
|
226
|
+
|
|
227
|
+
Each image plane becomes a named FITS extension; metadata is stored as
|
|
228
|
+
keywords in the primary header.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
cutout : dict
|
|
233
|
+
Cutout structure as returned by :func:`get_cutout`.
|
|
234
|
+
filename : str
|
|
235
|
+
Output FITS filename.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
hdus = []
|
|
239
|
+
|
|
240
|
+
# Store metadata to primary header
|
|
241
|
+
hdu = fits.PrimaryHDU()
|
|
242
|
+
|
|
243
|
+
for _ in cutout['meta']:
|
|
244
|
+
data = cutout['meta'][_]
|
|
245
|
+
# Special handling for unsupported FITS types
|
|
246
|
+
if data is None:
|
|
247
|
+
data = None
|
|
248
|
+
elif type(data) == Time or type(data) == datetime.datetime:
|
|
249
|
+
data = Time(data).to_value('fits')
|
|
250
|
+
elif not np.isscalar(data):
|
|
251
|
+
# NumPy arrays and like
|
|
252
|
+
data = repr(np.ma.filled(data))
|
|
253
|
+
elif np.isreal(data) and np.isnan(data):
|
|
254
|
+
data = 'NaN'
|
|
255
|
+
elif np.isreal(data) and not np.isfinite(data):
|
|
256
|
+
data = 'Inf'
|
|
257
|
+
|
|
258
|
+
hdu.header[_] = data
|
|
259
|
+
|
|
260
|
+
for _ in [
|
|
261
|
+
'x',
|
|
262
|
+
'y',
|
|
263
|
+
'ra',
|
|
264
|
+
'dec',
|
|
265
|
+
'mag',
|
|
266
|
+
'magerr',
|
|
267
|
+
'mag_calib',
|
|
268
|
+
'flags',
|
|
269
|
+
'id',
|
|
270
|
+
'time',
|
|
271
|
+
'filename',
|
|
272
|
+
]:
|
|
273
|
+
if _ in cutout:
|
|
274
|
+
data = cutout[_]
|
|
275
|
+
# Special handling for unsupported FITS types
|
|
276
|
+
if _ == 'time':
|
|
277
|
+
data = Time(data).to_value('fits')
|
|
278
|
+
|
|
279
|
+
hdu.header[_] = data
|
|
280
|
+
|
|
281
|
+
hdus.append(hdu)
|
|
282
|
+
|
|
283
|
+
# Store imaging data to named extensions
|
|
284
|
+
for _ in cutout.keys():
|
|
285
|
+
if _ not in ['meta', 'header', 'wcs'] and np.ndim(cutout[_]) == 2:
|
|
286
|
+
data = cutout[_]
|
|
287
|
+
|
|
288
|
+
if data.dtype == bool:
|
|
289
|
+
data = data.astype(np.uint16)
|
|
290
|
+
|
|
291
|
+
hdu = fits.ImageHDU(data, header=cutout.get('header'), name=_)
|
|
292
|
+
hdus.append(hdu)
|
|
293
|
+
|
|
294
|
+
fits.HDUList(hdus).writeto(filename, overwrite=True)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def load_cutout(filename):
|
|
298
|
+
"""Restore a cutout from a multi-extension FITS file.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
filename : str
|
|
303
|
+
Path to a FITS file written by :func:`write_cutout`.
|
|
304
|
+
|
|
305
|
+
Returns
|
|
306
|
+
-------
|
|
307
|
+
dict
|
|
308
|
+
Cutout structure as returned by :func:`get_cutout`.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
hdus = fits.open(filename)
|
|
312
|
+
|
|
313
|
+
cutout = {'meta': {}}
|
|
314
|
+
|
|
315
|
+
for _ in hdus[0].header[4:]:
|
|
316
|
+
name = _.lower()
|
|
317
|
+
data = hdus[0].header[_]
|
|
318
|
+
|
|
319
|
+
if name == 'time':
|
|
320
|
+
data = Time(data)
|
|
321
|
+
elif data == 'NaN':
|
|
322
|
+
data = np.nan
|
|
323
|
+
elif data == 'Inf':
|
|
324
|
+
data = np.inf
|
|
325
|
+
elif isinstance(data, str) and data.startswith('array('):
|
|
326
|
+
# FIXME: any safer way to convert it back to array?..
|
|
327
|
+
array = np.array
|
|
328
|
+
data = eval(data)
|
|
329
|
+
|
|
330
|
+
cutout['meta'][name] = data
|
|
331
|
+
|
|
332
|
+
for hdu in hdus[1:]:
|
|
333
|
+
if 'header' not in cutout:
|
|
334
|
+
cutout['header'] = hdu.header
|
|
335
|
+
|
|
336
|
+
name = hdu.name.lower()
|
|
337
|
+
|
|
338
|
+
cutout[name] = hdu.data
|
|
339
|
+
|
|
340
|
+
# Special handling of mask plane
|
|
341
|
+
if name == 'mask':
|
|
342
|
+
cutout[name] = cutout[name].astype(bool)
|
|
343
|
+
|
|
344
|
+
hdus.close()
|
|
345
|
+
|
|
346
|
+
if cutout['header']:
|
|
347
|
+
cutout['wcs'] = WCS(cutout['header'])
|
|
348
|
+
|
|
349
|
+
return cutout
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def adjust_cutout(
|
|
353
|
+
cutout,
|
|
354
|
+
max_shift=2,
|
|
355
|
+
max_scale=1.1,
|
|
356
|
+
inner=None,
|
|
357
|
+
normalize=False,
|
|
358
|
+
fit_bg=False,
|
|
359
|
+
verbose=False,
|
|
360
|
+
):
|
|
361
|
+
"""Fit a positional and flux-scaling adjustment to minimize the image–template residual.
|
|
362
|
+
|
|
363
|
+
Optimizes a shift (dx, dy), scale, and optionally background levels to
|
|
364
|
+
minimize the chi-squared residual between ``cutout['image']`` and
|
|
365
|
+
``cutout['convolved']``. On success, adds a ``cutout['adjusted']`` plane
|
|
366
|
+
with the optimized difference.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
cutout : dict
|
|
371
|
+
Cutout structure as returned by :func:`get_cutout`. Must contain
|
|
372
|
+
``image``, ``convolved``, and ``err`` planes.
|
|
373
|
+
max_shift : float, optional
|
|
374
|
+
Maximum allowed positional shift in pixels (symmetric bound).
|
|
375
|
+
max_scale : float, optional
|
|
376
|
+
Maximum allowed flux scale factor; scale is bounded to
|
|
377
|
+
``(1/max_scale, max_scale)``.
|
|
378
|
+
inner : int, optional
|
|
379
|
+
If set, only the central ``inner × inner`` pixel box is used for
|
|
380
|
+
optimization.
|
|
381
|
+
normalize : bool, optional
|
|
382
|
+
If True, divide the ``adjusted`` plane by the ``err`` plane.
|
|
383
|
+
fit_bg : bool, optional
|
|
384
|
+
If True, fit background levels as free parameters. If False, estimate
|
|
385
|
+
backgrounds from the cutout using SExtractor-mode
|
|
386
|
+
(``2.5*median − 1.5*mean``) and hold them fixed.
|
|
387
|
+
verbose : bool or callable, optional
|
|
388
|
+
Whether to show verbose messages. May be boolean or a ``print``-like callable.
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
bool
|
|
393
|
+
``True`` if optimization succeeded, ``False`` otherwise.
|
|
394
|
+
|
|
395
|
+
Notes
|
|
396
|
+
-----
|
|
397
|
+
On success the following keys are added to ``cutout['meta']``:
|
|
398
|
+
``adjust_chi2_0``, ``adjust_chi2``, ``adjust_df``, ``adjust_pval``,
|
|
399
|
+
``adjust_dx``, ``adjust_dy``, ``adjust_scale``, ``adjust_bg``, ``adjust_tbg``.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
# Simple wrapper around print for logging in verbose mode only
|
|
403
|
+
log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
|
|
404
|
+
|
|
405
|
+
mask = cutout['mask'] if 'mask' in cutout else ~np.isfinite(cutout['image'])
|
|
406
|
+
imask = np.zeros_like(mask)
|
|
407
|
+
|
|
408
|
+
# Rough estimation of backgrounds in the image and the template, using SExtractor-like mode estimation
|
|
409
|
+
# bg = np.nanmedian(cutout['image'][~mask])
|
|
410
|
+
bg = 2.5 * np.nanmedian(cutout['image'][~mask]) - 1.5 * np.nanmean(cutout['image'][~mask])
|
|
411
|
+
# tbg = np.nanmedian(cutout['convolved'][~mask])
|
|
412
|
+
tbg = 2.5 * np.nanmedian(cutout['convolved'][~mask]) - 1.5 * np.nanmean(
|
|
413
|
+
cutout['convolved'][~mask]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if inner is not None and inner > 0:
|
|
417
|
+
# Mask everything outside of a central box with given size
|
|
418
|
+
x, y = np.mgrid[0 : mask.shape[1], 0 : mask.shape[0]]
|
|
419
|
+
idx = np.abs(x - mask.shape[1] / 2 + 0.5) > inner / 2
|
|
420
|
+
idx |= np.abs(y - mask.shape[0] / 2 + 0.5) > inner / 2
|
|
421
|
+
imask[idx] = True
|
|
422
|
+
|
|
423
|
+
# Prepare and cleanup the arrays we will use for fitting (as 'shift' does not like nans etc)
|
|
424
|
+
image = cutout['image'].copy()
|
|
425
|
+
tmpl = cutout['convolved'].copy()
|
|
426
|
+
err = cutout['err'].copy()
|
|
427
|
+
|
|
428
|
+
image[~np.isfinite(image)] = 0
|
|
429
|
+
tmpl[~np.isfinite(tmpl)] = 0
|
|
430
|
+
err[~np.isfinite(err)] = 0
|
|
431
|
+
|
|
432
|
+
def _fn(dx, get_df=False, get_diff=False):
|
|
433
|
+
mask1 = mask | shift(mask, dx[:2], mode='reflect')
|
|
434
|
+
imask1 = mask1 | imask
|
|
435
|
+
diff = image - dx[3] - shift(tmpl - dx[4], dx[:2], mode='reflect') * dx[2]
|
|
436
|
+
|
|
437
|
+
chi2_value = np.sum((diff[~imask1] / err[~imask1]) ** 2) # Chi2
|
|
438
|
+
|
|
439
|
+
if get_diff:
|
|
440
|
+
return diff, mask1, imask1
|
|
441
|
+
elif get_df:
|
|
442
|
+
return chi2_value, np.sum(~imask1)
|
|
443
|
+
else:
|
|
444
|
+
return chi2_value
|
|
445
|
+
|
|
446
|
+
if fit_bg:
|
|
447
|
+
res = minimize(
|
|
448
|
+
_fn,
|
|
449
|
+
(0, 0, 1, bg, tbg),
|
|
450
|
+
bounds=(
|
|
451
|
+
(-max_shift, max_shift),
|
|
452
|
+
(-max_shift, max_shift),
|
|
453
|
+
(1 / max_scale, max_scale),
|
|
454
|
+
(None, None),
|
|
455
|
+
(None, None),
|
|
456
|
+
),
|
|
457
|
+
method='Powell',
|
|
458
|
+
options={'disp': False},
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
res = minimize(
|
|
462
|
+
_fn,
|
|
463
|
+
(0, 0, 1, bg, tbg),
|
|
464
|
+
bounds=(
|
|
465
|
+
(-max_shift, max_shift),
|
|
466
|
+
(-max_shift, max_shift),
|
|
467
|
+
(1 / max_scale, max_scale),
|
|
468
|
+
(bg, bg),
|
|
469
|
+
(tbg, tbg),
|
|
470
|
+
),
|
|
471
|
+
method='Powell',
|
|
472
|
+
options={'disp': False},
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
log(res.message)
|
|
476
|
+
|
|
477
|
+
if res.success:
|
|
478
|
+
log(
|
|
479
|
+
'Adjustment is: %.2f %.2f bg %.2g tbg %.2g scale %.2f'
|
|
480
|
+
% (res.x[0], res.x[1], res.x[3], res.x[4], res.x[2])
|
|
481
|
+
)
|
|
482
|
+
log('Chi2 improvement: %.2f -> %.2f' % (_fn([0, 0, 1, bg, tbg]), _fn(res.x)))
|
|
483
|
+
|
|
484
|
+
diff, mask1, _ = _fn(res.x, get_diff=True)
|
|
485
|
+
chi2_0 = _fn([0, 0, 1, bg, tbg])
|
|
486
|
+
chi2_1, df = _fn(res.x, get_df=True)
|
|
487
|
+
log('Final Chi2: %.2f df: %d p-value: %.2g' % (chi2_1, df, chi2.sf(chi2_1, df)))
|
|
488
|
+
|
|
489
|
+
# Keep the adjustment results in the metadata
|
|
490
|
+
cutout['meta']['adjust_chi2_0'] = chi2_0
|
|
491
|
+
cutout['meta']['adjust_chi2'] = chi2_1
|
|
492
|
+
cutout['meta']['adjust_df'] = df
|
|
493
|
+
cutout['meta']['adjust_pval'] = chi2.sf(chi2_1, df)
|
|
494
|
+
cutout['meta']['adjust_dx'] = res.x[0]
|
|
495
|
+
cutout['meta']['adjust_dy'] = res.x[1]
|
|
496
|
+
cutout['meta']['adjust_scale'] = res.x[2]
|
|
497
|
+
cutout['meta']['adjust_bg'] = res.x[3]
|
|
498
|
+
cutout['meta']['adjust_tbg'] = res.x[4]
|
|
499
|
+
|
|
500
|
+
if normalize:
|
|
501
|
+
diff[~mask1] /= err[~mask1]
|
|
502
|
+
diff[err > 1e-30] /= err[err > 1e-30]
|
|
503
|
+
|
|
504
|
+
# diff[mask1] = 1e-30
|
|
505
|
+
|
|
506
|
+
# Add result as a new cutout plane
|
|
507
|
+
cutout['adjusted'] = diff
|
|
508
|
+
|
|
509
|
+
return True
|
|
510
|
+
|
|
511
|
+
else:
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def downscale_image(image, scale=1, mode='sum', header=None):
|
|
516
|
+
"""Downscale an image by an integer factor.
|
|
517
|
+
|
|
518
|
+
If the image dimensions are not divisible by ``scale``, the image is
|
|
519
|
+
cropped to the largest divisible size first. If a FITS header is
|
|
520
|
+
provided, the WCS is adjusted for the downscaled pixel grid.
|
|
521
|
+
|
|
522
|
+
Parameters
|
|
523
|
+
----------
|
|
524
|
+
image : ndarray
|
|
525
|
+
2D image array to downscale.
|
|
526
|
+
scale : int, optional
|
|
527
|
+
Integer downscaling factor.
|
|
528
|
+
mode : str, optional
|
|
529
|
+
Pixel reduction mode: ``'sum'`` (default), ``'mean'``, ``'and'``, or ``'or'``.
|
|
530
|
+
header : astropy.io.fits.Header, optional
|
|
531
|
+
If provided, the WCS is adjusted and a new header is returned alongside
|
|
532
|
+
the downscaled image.
|
|
533
|
+
|
|
534
|
+
Returns
|
|
535
|
+
-------
|
|
536
|
+
ndarray or tuple
|
|
537
|
+
Downscaled image, or ``(downscaled_image, adjusted_header)`` if
|
|
538
|
+
``header`` is provided.
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
# Crop the image if necessary
|
|
542
|
+
maxx = scale * (image.shape[1] // scale)
|
|
543
|
+
maxy = scale * (image.shape[0] // scale)
|
|
544
|
+
image = image[:maxy, :maxx]
|
|
545
|
+
|
|
546
|
+
shape = (image.shape[0] // scale, scale, image.shape[1] // scale, scale)
|
|
547
|
+
|
|
548
|
+
image1 = image.reshape(shape)
|
|
549
|
+
|
|
550
|
+
if mode == 'mean':
|
|
551
|
+
image1 = image1.mean(-1).mean(1)
|
|
552
|
+
elif mode == 'and':
|
|
553
|
+
image1 = np.bitwise_and.reduce(image1, -1)
|
|
554
|
+
image1 = np.bitwise_and.reduce(image1, 1)
|
|
555
|
+
elif mode == 'or':
|
|
556
|
+
image1 = np.bitwise_or.reduce(image1, -1)
|
|
557
|
+
image1 = np.bitwise_or.reduce(image1, 1)
|
|
558
|
+
else:
|
|
559
|
+
# Fallback to mode='sum'
|
|
560
|
+
image1 = image1.sum(-1).sum(1)
|
|
561
|
+
|
|
562
|
+
if header is not None:
|
|
563
|
+
wcs = WCS(header)
|
|
564
|
+
wcs = wcs[::scale, ::scale]
|
|
565
|
+
header1 = astrometry.clear_wcs(header, copy=True)
|
|
566
|
+
header1 += wcs.to_header(relax=True)
|
|
567
|
+
|
|
568
|
+
return image1, header1
|
|
569
|
+
else:
|
|
570
|
+
return image1
|
|
Binary file
|