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