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