astLib.astWCS

Package astLib :: Module astWCS |
[hide private]
[frames] | no frames]

# Source Code for Module astLib.astWCS

      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  # So far as I can tell in astropy 0.4 the API is the same as pyfits for what we need...
     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  # if True, -1 from pixel coords to be zero-indexed like numpy. If False, use
     40  # FITS convention.
     41  NUMPY_MODE = True
     42
     43  # Check for the locale bug when decimal separator isn't '.' (atof used in
     44  # libwcs)
     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  #-----------------------------------------------------------------------------
     53 -class WCS:
     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              # silentfix below won't deal with unprintable strings
    104              # so here we optionally remove problematic keywords
    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') # solves problems with non-standard headers
    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
    122 -    def copy(self):
    123          """Copies the WCS object to a new object.
    124
    125          @rtype: astWCS.WCS object
    126          @return: WCS object
    127
    128          """
    129
    130          # This only sets up a new WCS object, doesn't do a deep copy
    131          ret = WCS(self.headerSource, self.extensionName, self.mode)
    132
    133          # This fixes copy bug
    134          ret.header = self.header.copy()
    135          ret.updateFromHeader()
    136
    137          return ret
    138
    139
    140 -    def updateFromHeader(self):
    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          # Updated for pyfits 3.1+
    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          # Workaround for ZPN bug when PV2_3 == 0 (as in, e.g., ESO WFI images)
    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
    167 -    def getCentreWCSCoords(self):
    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
    183 -    def getFullSizeSkyDeg(self):
    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
    201 -    def getHalfSizeDeg(self):
    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
    218 -    def getImageMinMaxWCSCoords(self):
    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          # Get size of parent image this WCS is taken from
    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
    250 -    def wcs2pix(self, RADeg, decDeg):
    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                      # Below handles CEA wraparounds
    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              # Below handles CEA wraparounds
    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
    292 -    def pix2wcs(self, x, y):
    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
    318 -    def coordsAreInImage(self, RADeg, decDeg):
    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
    335 -    def getRotationDeg(self):
    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
    346 -    def isFlipped(self):
    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
    356 -    def getPixelSizeDeg(self):
    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
    370 -    def getXPixelSizeDeg(self):
    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
    383 -    def getYPixelSizeDeg(self):
    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
    396 -    def getEquinox(self):
    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
    406 -    def getEpoch(self):
    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  # Functions for comparing WCS objects
    418 -def findWCSOverlap(wcs1, wcs2):
    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      # Note order swapping below is essential
    436      # Min RA
    437      if mm1[0] - mm2[0] <= 0.0:
    438          overlapWCSCoords[0] = mm2[0]
    439      else:
    440          overlapWCSCoords[0] = mm1[0]
    441
    442      # Max RA
    443      if mm1[1] - mm2[1] <= 0.0:
    444          overlapWCSCoords[1] = mm1[1]
    445      else:
    446          overlapWCSCoords[1] = mm2[1]
    447
    448      # Min dec.
    449      if mm1[2] - mm2[2] <= 0.0:
    450          overlapWCSCoords[2] = mm2[2]
    451      else:
    452          overlapWCSCoords[2] = mm1[2]
    453
    454      # Max dec.
    455      if mm1[3] - mm2[3] <= 0.0:
    456          overlapWCSCoords[3] = mm1[3]
    457      else:
    458          overlapWCSCoords[3] = mm2[3]
    459
    460      # Get corresponding pixel coords
    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