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