1 """module for handling World Coordinate Systems (WCS)
2
3 (c) 2007-2012 Matt Hilton
4
5 (c) 2013 Matt Hilton & Steven Boada
6
7 U{http://astlib.sourceforge.net}
8
9 This is a higher level interface to some of the routines in PyWCSTools
10 (distributed with astLib).
11 PyWCSTools is a simple SWIG wrapping of WCSTools by Doug Mink
12 (U{http://tdc-www.harvard.edu/software/wcstools/}). It is intended is to make
13 this interface complete enough such that direct use of PyWCSTools is
14 unnecessary.
15
16 @var NUMPY_MODE: If True (default), pixel coordinates accepted/returned by
17 routines such as L{astWCS.WCS.pix2wcs}, L{astWCS.WCS.wcs2pix} have (0, 0)
18 as the origin. Set to False to make these routines accept/return pixel
19 coords with (1, 1) as the origin (i.e. to match the FITS convention,
20 default behaviour prior to astLib version 0.3.0).
21 @type NUMPY_MODE: bool
22
23 """
24
25 import pyfits
26 from PyWCSTools import wcs
27 import numpy
28 import locale
29
30
31
32 NUMPY_MODE = True
33
34
35
36 lconv = locale.localeconv()
37 if lconv['decimal_point'] != '.':
38 print("WARNING: decimal point separator is not '.' - astWCS coordinate conversions will not work.")
39 print("Workaround: after importing any modules that set the locale (e.g. matplotlib) do the following:")
40 print(" import locale")
41 print(" locale.setlocale(locale.LC_NUMERIC, 'C')")
42
43
45 """This class provides methods for accessing information from the World
46 Coordinate System (WCS) contained in the header of a FITS image.
47 Conversions between pixel and WCS coordinates can also be performed.
48
49 To create a WCS object from a FITS file called "test.fits", simply:
50
51 WCS=astWCS.WCS("test.fits")
52
53 Likewise, to create a WCS object from the pyfits.header of "test.fits":
54
55 img=pyfits.open("test.fits")
56 header=img[0].header
57 WCS=astWCS.WCS(header, mode = "pyfits")
58
59 """
60
61 - def __init__(self, headerSource, extensionName = 0, mode = "image"):
62 """Creates a WCS object using either the information contained in the
63 header of the specified .fits image, or from a pyfits.header object.
64 Set mode = "pyfits" if the headerSource is a pyfits.header.
65
66 @type headerSource: string or pyfits.header
67 @param headerSource: filename of input .fits image, or a pyfits.header
68 object
69 @type extensionName: int or string
70 @param extensionName: name or number of .fits extension in which image
71 data is stored
72 @type mode: string
73 @param mode: set to "image" if headerSource is a .fits file name, or
74 set to "pyfits" if headerSource is a pyfits.header object
75
76 @note: The meta data provided by headerSource is stored in WCS.header
77 as a pyfits.header object.
78
79 """
80
81 self.mode = mode
82 self.headerSource = headerSource
83 self.extensionName = extensionName
84
85 if self.mode == "image":
86 img = pyfits.open(self.headerSource)
87 img.verify('silentfix')
88 self.header = img[self.extensionName].header
89 img.close()
90 elif self.mode == "pyfits":
91 self.header=headerSource
92
93 self.updateFromHeader()
94
95
97 """Copies the WCS object to a new object.
98
99 @rtype: astWCS.WCS object
100 @return: WCS object
101
102 """
103
104
105 ret = WCS(self.headerSource, self.extensionName, self.mode)
106
107
108 ret.header = self.header.copy()
109 ret.updateFromHeader()
110
111 return ret
112
113
115 """Updates the WCS object using information from WCS.header. This
116 routine should be called whenever changes are made to WCS keywords in
117 WCS.header.
118
119 """
120
121
122
123 cardList = pyfits.CardList()
124 for i in list(self.header.items()):
125 if len(str(i[1])) < 70:
126 if len(str(i[0])) <= 8:
127 cardList.append(pyfits.Card(i[0], i[1]))
128 else:
129 cardList.append(pyfits.Card('HIERARCH '+i[0], i[1]))
130 newHead = pyfits.Header(cards=cardList)
131
132
133 if "PV2_3" in list(newHead.keys()) and newHead['PV2_3'] == 0 and newHead['CTYPE1'] == 'RA---ZPN':
134 newHead.update("PV2_3", 1e-15)
135
136 cardlist = newHead.ascard
137 cardstring = ""
138 for card in cardlist:
139 cardstring = cardstring+str(card)
140
141 self.WCSStructure = wcs.wcsinit(cardstring)
142
143
145 """Returns the RA and dec coordinates (in decimal degrees) at the
146 centre of the WCS.
147
148 @rtype: list
149 @return: coordinates in decimal degrees in format [RADeg, decDeg]
150
151 """
152 full = wcs.wcsfull(self.WCSStructure)
153
154 RADeg = full[0]
155 decDeg = full[1]
156
157 return [RADeg, decDeg]
158
159
161 """Returns the width, height of the image according to the WCS in
162 decimal degrees on the sky (i.e., with the projection taken into
163 account).
164
165 @rtype: list
166 @return: width and height of image in decimal degrees on the sky in
167 format [width, height]
168
169 """
170 full = wcs.wcsfull(self.WCSStructure)
171
172 width = full[2]
173 height = full[3]
174
175 return [width, height]
176
177
179 """Returns the half-width, half-height of the image according to the
180 WCS in RA and dec degrees.
181
182 @rtype: list
183 @return: half-width and half-height of image in R.A., dec. decimal
184 degrees in format [half-width, half-height]
185
186 """
187 half = wcs.wcssize(self.WCSStructure)
188
189 width = half[2]
190 height = half[3]
191
192 return [width, height]
193
194
196 """Returns the minimum, maximum WCS coords defined by the size of the
197 parent image (as defined by the NAXIS keywords in the image header).
198
199 @rtype: list
200 @return: [minimum R.A., maximum R.A., minimum Dec., maximum Dec.]
201
202 """
203
204
205 maxX = self.header['NAXIS1']
206 maxY = self.header['NAXIS2']
207 minX = 1.0
208 minY = 1.0
209
210 if NUMPY_MODE == True:
211 maxX = maxX-1
212 maxY = maxY-1
213 minX = minX-1
214 minY = minY-1
215
216 bottomLeft = self.pix2wcs(minX, minY)
217 topRight = self.pix2wcs(maxX, maxY)
218
219 xCoords = [bottomLeft[0], topRight[0]]
220 yCoords = [bottomLeft[1], topRight[1]]
221 xCoords.sort()
222 yCoords.sort()
223
224 return [xCoords[0], xCoords[1], yCoords[0], yCoords[1]]
225
226
228 """Returns the pixel coordinates corresponding to the input WCS
229 coordinates (given in decimal degrees). RADeg, decDeg can be single
230 floats, or lists or numpy arrays.
231
232 @rtype: list
233 @return: pixel coordinates in format [x, y]
234
235 """
236
237 if type(RADeg) == numpy.ndarray or type(RADeg) == list:
238 if type(decDeg) == numpy.ndarray or type(decDeg) == list:
239 pixCoords = []
240 for ra, dec in zip(RADeg, decDeg):
241 pix = wcs.wcs2pix(self.WCSStructure, float(ra), float(dec))
242
243 if pix[0] < 1:
244 xTest = ((self.header['CRPIX1'])-(ra-360.0) /
245 self.getXPixelSizeDeg())
246 if xTest >= 1 and xTest < self.header['NAXIS1']:
247 pix[0] = xTest
248 if NUMPY_MODE == True:
249 pix[0] = pix[0]-1
250 pix[1] = pix[1]-1
251 pixCoords.append([pix[0], pix[1]])
252 else:
253 pixCoords = (wcs.wcs2pix(self.WCSStructure, float(RADeg),
254 float(decDeg)))
255
256 if pixCoords[0] < 1:
257 xTest = ((self.header['CRPIX1'])-(RADeg-360.0) /
258 self.getXPixelSizeDeg())
259 if xTest >= 1 and xTest < self.header['NAXIS1']:
260 pixCoords[0] = xTest
261 if NUMPY_MODE == True:
262 pixCoords[0] = pixCoords[0]-1
263 pixCoords[1] = pixCoords[1]-1
264 pixCoords = [pixCoords[0], pixCoords[1]]
265
266 return pixCoords
267
268
270 """Returns the WCS coordinates corresponding to the input pixel
271 coordinates.
272
273 @rtype: list
274 @return: WCS coordinates in format [RADeg, decDeg]
275
276 """
277 if type(x) == numpy.ndarray or type(x) == list:
278 if type(y) == numpy.ndarray or type(y) == list:
279 WCSCoords = []
280 for xc, yc in zip(x, y):
281 if NUMPY_MODE == True:
282 xc += 1
283 yc += 1
284 WCSCoords.append(wcs.pix2wcs(self.WCSStructure, float(xc),
285 float(yc)))
286 else:
287 if NUMPY_MODE == True:
288 x += 1
289 y += 1
290 WCSCoords = wcs.pix2wcs(self.WCSStructure, float(x), float(y))
291
292 return WCSCoords
293
294
296 """Returns True if the given RA, dec coordinate is within the image
297 boundaries.
298
299 @rtype: bool
300 @return: True if coordinate within image, False if not.
301
302 """
303
304 pixCoords = wcs.wcs2pix(self.WCSStructure, RADeg, decDeg)
305 if pixCoords[0] >= 0 and pixCoords[0] < self.header['NAXIS1'] and \
306 pixCoords[1] >= 0 and pixCoords[1] < self.header['NAXIS2']:
307 return True
308 else:
309 return False
310
311
313 """Returns the rotation angle in degrees around the axis, North through
314 East.
315
316 @rtype: float
317 @return: rotation angle in degrees
318
319 """
320 return self.WCSStructure.rot
321
322
324 """Returns 1 if image is reflected around axis, otherwise returns 0.
325
326 @rtype: int
327 @return: 1 if image is flipped, 0 otherwise
328
329 """
330 return self.WCSStructure.imflip
331
332
334 """Returns the pixel scale of the WCS. This is the average of the x, y
335 pixel scales.
336
337 @rtype: float
338 @return: pixel size in decimal degrees
339
340 """
341
342 avSize = (abs(self.WCSStructure.xinc)+abs(self.WCSStructure.yinc))/2.0
343
344 return avSize
345
346
348 """Returns the pixel scale along the x-axis of the WCS in degrees.
349
350 @rtype: float
351 @return: pixel size in decimal degrees
352
353 """
354
355 avSize = abs(self.WCSStructure.xinc)
356
357 return avSize
358
359
361 """Returns the pixel scale along the y-axis of the WCS in degrees.
362
363 @rtype: float
364 @return: pixel size in decimal degrees
365
366 """
367
368 avSize = abs(self.WCSStructure.yinc)
369
370 return avSize
371
372
374 """Returns the equinox of the WCS.
375
376 @rtype: float
377 @return: equinox of the WCS
378
379 """
380 return self.WCSStructure.equinox
381
382
384 """Returns the epoch of the WCS.
385
386 @rtype: float
387 @return: epoch of the WCS
388
389 """
390 return self.WCSStructure.epoch
391
392
393
394
396 """Finds the minimum, maximum WCS coords that overlap between wcs1 and
397 wcs2. Returns these coordinates, plus the corresponding pixel coordinates
398 for each wcs. Useful for clipping overlapping region between two images.
399
400 @rtype: dictionary
401 @return: dictionary with keys 'overlapWCS' (min, max RA, dec of overlap
402 between wcs1, wcs2) 'wcs1Pix', 'wcs2Pix' (pixel coords in each input
403 WCS that correspond to 'overlapWCS' coords)
404
405 """
406
407 mm1 = wcs1.getImageMinMaxWCSCoords()
408 mm2 = wcs2.getImageMinMaxWCSCoords()
409
410 overlapWCSCoords = [0.0, 0.0, 0.0, 0.0]
411
412
413
414 if mm1[0] - mm2[0] <= 0.0:
415 overlapWCSCoords[0] = mm2[0]
416 else:
417 overlapWCSCoords[0] = mm1[0]
418
419
420 if mm1[1] - mm2[1] <= 0.0:
421 overlapWCSCoords[1] = mm1[1]
422 else:
423 overlapWCSCoords[1] = mm2[1]
424
425
426 if mm1[2] - mm2[2] <= 0.0:
427 overlapWCSCoords[2] = mm2[2]
428 else:
429 overlapWCSCoords[2] = mm1[2]
430
431
432 if mm1[3] - mm2[3] <= 0.0:
433 overlapWCSCoords[3] = mm1[3]
434 else:
435 overlapWCSCoords[3] = mm2[3]
436
437
438 p1Low = wcs1.wcs2pix(overlapWCSCoords[0], overlapWCSCoords[2])
439 p1High = wcs1.wcs2pix(overlapWCSCoords[1], overlapWCSCoords[3])
440 p1 = [p1Low[0], p1High[0], p1Low[1], p1High[1]]
441
442 p2Low = wcs2.wcs2pix(overlapWCSCoords[0], overlapWCSCoords[2])
443 p2High = wcs2.wcs2pix(overlapWCSCoords[1], overlapWCSCoords[3])
444 p2 = [p2Low[0], p2High[0], p2Low[1], p2High[1]]
445
446 return {'overlapWCS': overlapWCSCoords, 'wcs1Pix': p1, 'wcs2Pix': p2}
447
448
449