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