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 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 Doug 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  import pyfits 
 26  from PyWCSTools import wcs 
 27  import numpy 
 28  import locale 
 29   
 30  # if True, -1 from pixel coords to be zero-indexed like numpy. If False, use 
 31  # FITS convention. 
 32  NUMPY_MODE = True 
 33   
 34  # Check for the locale bug when decimal separator isn't '.' (atof used in 
 35  # libwcs) 
 36  lconv = locale.localeconv() 
 37  if lconv['decimal_point'] != '.': 
 38      print("WARNING: decimal point separator is not '.' - astWCS coordinate conversions will not work.") 
 39      print("Workaround: after importing any modules that set the locale (e.g. matplotlib) do the following:") 
 40      print("   import locale") 
 41      print("   locale.setlocale(locale.LC_NUMERIC, 'C')") 
 42   
 43  #----------------------------------------------------------------------------- 
44 -class WCS:
45 """This class provides methods for accessing information from the World 46 Coordinate System (WCS) contained in the header of a FITS image. 47 Conversions between pixel and WCS coordinates can also be performed. 48 49 To create a WCS object from a FITS file called "test.fits", simply: 50 51 WCS=astWCS.WCS("test.fits") 52 53 Likewise, to create a WCS object from the pyfits.header of "test.fits": 54 55 img=pyfits.open("test.fits") 56 header=img[0].header 57 WCS=astWCS.WCS(header, mode = "pyfits") 58 59 """ 60
61 - def __init__(self, headerSource, extensionName = 0, mode = "image"):
62 """Creates a WCS object using either the information contained in the 63 header of the specified .fits image, or from a pyfits.header object. 64 Set mode = "pyfits" if the headerSource is a pyfits.header. 65 66 @type headerSource: string or pyfits.header 67 @param headerSource: filename of input .fits image, or a pyfits.header 68 object 69 @type extensionName: int or string 70 @param extensionName: name or number of .fits extension in which image 71 data is stored 72 @type mode: string 73 @param mode: set to "image" if headerSource is a .fits file name, or 74 set to "pyfits" if headerSource is a pyfits.header object 75 76 @note: The meta data provided by headerSource is stored in WCS.header 77 as a pyfits.header object. 78 79 """ 80 81 self.mode = mode 82 self.headerSource = headerSource 83 self.extensionName = extensionName 84 85 if self.mode == "image": 86 img = pyfits.open(self.headerSource) 87 img.verify('silentfix') # solves problems with non-standard headers 88 self.header = img[self.extensionName].header 89 img.close() 90 elif self.mode == "pyfits": 91 self.header=headerSource 92 93 self.updateFromHeader()
94 95
96 - def copy(self):
97 """Copies the WCS object to a new object. 98 99 @rtype: astWCS.WCS object 100 @return: WCS object 101 102 """ 103 104 # This only sets up a new WCS object, doesn't do a deep copy 105 ret = WCS(self.headerSource, self.extensionName, self.mode) 106 107 # This fixes copy bug 108 ret.header = self.header.copy() 109 ret.updateFromHeader() 110 111 return ret
112 113
114 - def updateFromHeader(self):
115 """Updates the WCS object using information from WCS.header. This 116 routine should be called whenever changes are made to WCS keywords in 117 WCS.header. 118 119 """ 120 121 # Take out any problematic overly long header keyword values before 122 # creating the WCSStructure, as these cause problems for WCSTools 123 cardList = pyfits.CardList() 124 for i in list(self.header.items()): 125 if len(str(i[1])) < 70: 126 if len(str(i[0])) <= 8: 127 cardList.append(pyfits.Card(i[0], i[1])) 128 else: 129 cardList.append(pyfits.Card('HIERARCH '+i[0], i[1])) 130 newHead = pyfits.Header(cards=cardList) 131 132 # Workaround for ZPN bug when PV2_3 == 0 (as in, e.g., ESO WFI images) 133 if "PV2_3" in list(newHead.keys()) and newHead['PV2_3'] == 0 and newHead['CTYPE1'] == 'RA---ZPN': 134 newHead.update("PV2_3", 1e-15) 135 136 cardlist = newHead.ascard # Changed for pyfits 3.0+ 137 cardstring = "" 138 for card in cardlist: 139 cardstring = cardstring+str(card) 140 141 self.WCSStructure = wcs.wcsinit(cardstring)
142 143
144 - def getCentreWCSCoords(self):
145 """Returns the RA and dec coordinates (in decimal degrees) at the 146 centre of the WCS. 147 148 @rtype: list 149 @return: coordinates in decimal degrees in format [RADeg, decDeg] 150 151 """ 152 full = wcs.wcsfull(self.WCSStructure) 153 154 RADeg = full[0] 155 decDeg = full[1] 156 157 return [RADeg, decDeg]
158 159
160 - def getFullSizeSkyDeg(self):
161 """Returns the width, height of the image according to the WCS in 162 decimal degrees on the sky (i.e., with the projection taken into 163 account). 164 165 @rtype: list 166 @return: width and height of image in decimal degrees on the sky in 167 format [width, height] 168 169 """ 170 full = wcs.wcsfull(self.WCSStructure) 171 172 width = full[2] 173 height = full[3] 174 175 return [width, height]
176 177
178 - def getHalfSizeDeg(self):
179 """Returns the half-width, half-height of the image according to the 180 WCS in RA and dec degrees. 181 182 @rtype: list 183 @return: half-width and half-height of image in R.A., dec. decimal 184 degrees in format [half-width, half-height] 185 186 """ 187 half = wcs.wcssize(self.WCSStructure) 188 189 width = half[2] 190 height = half[3] 191 192 return [width, height]
193 194
195 - def getImageMinMaxWCSCoords(self):
196 """Returns the minimum, maximum WCS coords defined by the size of the 197 parent image (as defined by the NAXIS keywords in the image header). 198 199 @rtype: list 200 @return: [minimum R.A., maximum R.A., minimum Dec., maximum Dec.] 201 202 """ 203 204 # Get size of parent image this WCS is taken from 205 maxX = self.header['NAXIS1'] 206 maxY = self.header['NAXIS2'] 207 minX = 1.0 208 minY = 1.0 209 210 if NUMPY_MODE == True: 211 maxX = maxX-1 212 maxY = maxY-1 213 minX = minX-1 214 minY = minY-1 215 216 bottomLeft = self.pix2wcs(minX, minY) 217 topRight = self.pix2wcs(maxX, maxY) 218 219 xCoords = [bottomLeft[0], topRight[0]] 220 yCoords = [bottomLeft[1], topRight[1]] 221 xCoords.sort() 222 yCoords.sort() 223 224 return [xCoords[0], xCoords[1], yCoords[0], yCoords[1]]
225 226
227 - def wcs2pix(self, RADeg, decDeg):
228 """Returns the pixel coordinates corresponding to the input WCS 229 coordinates (given in decimal degrees). RADeg, decDeg can be single 230 floats, or lists or numpy arrays. 231 232 @rtype: list 233 @return: pixel coordinates in format [x, y] 234 235 """ 236 237 if type(RADeg) == numpy.ndarray or type(RADeg) == list: 238 if type(decDeg) == numpy.ndarray or type(decDeg) == list: 239 pixCoords = [] 240 for ra, dec in zip(RADeg, decDeg): 241 pix = wcs.wcs2pix(self.WCSStructure, float(ra), float(dec)) 242 # Below handles CEA wraparounds 243 if pix[0] < 1: 244 xTest = ((self.header['CRPIX1'])-(ra-360.0) / 245 self.getXPixelSizeDeg()) 246 if xTest >= 1 and xTest < self.header['NAXIS1']: 247 pix[0] = xTest 248 if NUMPY_MODE == True: 249 pix[0] = pix[0]-1 250 pix[1] = pix[1]-1 251 pixCoords.append([pix[0], pix[1]]) 252 else: 253 pixCoords = (wcs.wcs2pix(self.WCSStructure, float(RADeg), 254 float(decDeg))) 255 # Below handles CEA wraparounds 256 if pixCoords[0] < 1: 257 xTest = ((self.header['CRPIX1'])-(RADeg-360.0) / 258 self.getXPixelSizeDeg()) 259 if xTest >= 1 and xTest < self.header['NAXIS1']: 260 pixCoords[0] = xTest 261 if NUMPY_MODE == True: 262 pixCoords[0] = pixCoords[0]-1 263 pixCoords[1] = pixCoords[1]-1 264 pixCoords = [pixCoords[0], pixCoords[1]] 265 266 return pixCoords
267 268
269 - def pix2wcs(self, x, y):
270 """Returns the WCS coordinates corresponding to the input pixel 271 coordinates. 272 273 @rtype: list 274 @return: WCS coordinates in format [RADeg, decDeg] 275 276 """ 277 if type(x) == numpy.ndarray or type(x) == list: 278 if type(y) == numpy.ndarray or type(y) == list: 279 WCSCoords = [] 280 for xc, yc in zip(x, y): 281 if NUMPY_MODE == True: 282 xc += 1 283 yc += 1 284 WCSCoords.append(wcs.pix2wcs(self.WCSStructure, float(xc), 285 float(yc))) 286 else: 287 if NUMPY_MODE == True: 288 x += 1 289 y += 1 290 WCSCoords = wcs.pix2wcs(self.WCSStructure, float(x), float(y)) 291 292 return WCSCoords
293 294
295 - def coordsAreInImage(self, RADeg, decDeg):
296 """Returns True if the given RA, dec coordinate is within the image 297 boundaries. 298 299 @rtype: bool 300 @return: True if coordinate within image, False if not. 301 302 """ 303 304 pixCoords = wcs.wcs2pix(self.WCSStructure, RADeg, decDeg) 305 if pixCoords[0] >= 0 and pixCoords[0] < self.header['NAXIS1'] and \ 306 pixCoords[1] >= 0 and pixCoords[1] < self.header['NAXIS2']: 307 return True 308 else: 309 return False
310 311
312 - def getRotationDeg(self):
313 """Returns the rotation angle in degrees around the axis, North through 314 East. 315 316 @rtype: float 317 @return: rotation angle in degrees 318 319 """ 320 return self.WCSStructure.rot
321 322
323 - def isFlipped(self):
324 """Returns 1 if image is reflected around axis, otherwise returns 0. 325 326 @rtype: int 327 @return: 1 if image is flipped, 0 otherwise 328 329 """ 330 return self.WCSStructure.imflip
331 332
333 - def getPixelSizeDeg(self):
334 """Returns the pixel scale of the WCS. This is the average of the x, y 335 pixel scales. 336 337 @rtype: float 338 @return: pixel size in decimal degrees 339 340 """ 341 342 avSize = (abs(self.WCSStructure.xinc)+abs(self.WCSStructure.yinc))/2.0 343 344 return avSize
345 346
347 - def getXPixelSizeDeg(self):
348 """Returns the pixel scale along the x-axis of the WCS in degrees. 349 350 @rtype: float 351 @return: pixel size in decimal degrees 352 353 """ 354 355 avSize = abs(self.WCSStructure.xinc) 356 357 return avSize
358 359
360 - def getYPixelSizeDeg(self):
361 """Returns the pixel scale along the y-axis of the WCS in degrees. 362 363 @rtype: float 364 @return: pixel size in decimal degrees 365 366 """ 367 368 avSize = abs(self.WCSStructure.yinc) 369 370 return avSize
371 372
373 - def getEquinox(self):
374 """Returns the equinox of the WCS. 375 376 @rtype: float 377 @return: equinox of the WCS 378 379 """ 380 return self.WCSStructure.equinox
381 382
383 - def getEpoch(self):
384 """Returns the epoch of the WCS. 385 386 @rtype: float 387 @return: epoch of the WCS 388 389 """ 390 return self.WCSStructure.epoch
391 392 393 #----------------------------------------------------------------------------- 394 # Functions for comparing WCS objects
395 -def findWCSOverlap(wcs1, wcs2):
396 """Finds the minimum, maximum WCS coords that overlap between wcs1 and 397 wcs2. Returns these coordinates, plus the corresponding pixel coordinates 398 for each wcs. Useful for clipping overlapping region between two images. 399 400 @rtype: dictionary 401 @return: dictionary with keys 'overlapWCS' (min, max RA, dec of overlap 402 between wcs1, wcs2) 'wcs1Pix', 'wcs2Pix' (pixel coords in each input 403 WCS that correspond to 'overlapWCS' coords) 404 405 """ 406 407 mm1 = wcs1.getImageMinMaxWCSCoords() 408 mm2 = wcs2.getImageMinMaxWCSCoords() 409 410 overlapWCSCoords = [0.0, 0.0, 0.0, 0.0] 411 412 # Note order swapping below is essential 413 # Min RA 414 if mm1[0] - mm2[0] <= 0.0: 415 overlapWCSCoords[0] = mm2[0] 416 else: 417 overlapWCSCoords[0] = mm1[0] 418 419 # Max RA 420 if mm1[1] - mm2[1] <= 0.0: 421 overlapWCSCoords[1] = mm1[1] 422 else: 423 overlapWCSCoords[1] = mm2[1] 424 425 # Min dec. 426 if mm1[2] - mm2[2] <= 0.0: 427 overlapWCSCoords[2] = mm2[2] 428 else: 429 overlapWCSCoords[2] = mm1[2] 430 431 # Max dec. 432 if mm1[3] - mm2[3] <= 0.0: 433 overlapWCSCoords[3] = mm1[3] 434 else: 435 overlapWCSCoords[3] = mm2[3] 436 437 # Get corresponding pixel coords 438 p1Low = wcs1.wcs2pix(overlapWCSCoords[0], overlapWCSCoords[2]) 439 p1High = wcs1.wcs2pix(overlapWCSCoords[1], overlapWCSCoords[3]) 440 p1 = [p1Low[0], p1High[0], p1Low[1], p1High[1]] 441 442 p2Low = wcs2.wcs2pix(overlapWCSCoords[0], overlapWCSCoords[2]) 443 p2High = wcs2.wcs2pix(overlapWCSCoords[1], overlapWCSCoords[3]) 444 p2 = [p2Low[0], p2High[0], p2Low[1], p2High[1]] 445 446 return {'overlapWCS': overlapWCSCoords, 'wcs1Pix': p1, 'wcs2Pix': p2}
447 448 #----------------------------------------------------------------------------- 449