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