1  """module for simple .fits image tasks (rotation, clipping out sections, making .pngs etc.) 
   2   
   3  (c) 2007-2014 Matt Hilton  
   4   
   5  U{http://astlib.sourceforge.net} 
   6   
   7  Some routines in this module will fail if, e.g., asked to clip a section from a .fits image at a 
   8  position not found within the image (as determined using the WCS). Where this occurs, the function 
   9  will return None. An error message will be printed to the console when this happens if 
  10  astImages.REPORT_ERRORS=True (the default). Testing if an astImages function returns None can be 
  11  used to handle errors in scripts.  
  12   
  13  """ 
  14   
  15  REPORT_ERRORS=True 
  16   
  17  import os 
  18  import sys 
  19  import math 
  20  from astLib import astWCS 
  21   
  22   
  23  try: 
  24      import pyfits 
  25  except: 
  26      try: 
  27          from astropy.io import fits as pyfits 
  28      except: 
  29          raise Exception, "couldn't import either pyfits or astropy.io.fits" 
  30       
  31  try: 
  32      from scipy import ndimage 
  33      from scipy import interpolate 
  34  except ImportError: 
  35      print("WARNING: astImages: failed to import scipy.ndimage - some functions will not work.") 
  36  import numpy 
  37  try: 
  38      import matplotlib 
  39      from matplotlib import pylab 
  40      matplotlib.interactive(False) 
  41  except ImportError: 
  42      print("WARNING: astImages: failed to import matplotlib - some functions will not work.") 
  43   
  44   
  46      """Clips a square or rectangular section from an image array at the given celestial coordinates.  
  47      An updated WCS for the clipped section is optionally returned, as well as the x, y pixel  
  48      coordinates in the original image corresponding to the clipped section. 
  49       
  50      Note that the clip size is specified in degrees on the sky. For projections that have varying 
  51      real pixel scale across the map (e.g. CEA), use L{clipUsingRADecCoords} instead. 
  52   
  53      @type imageData: numpy array 
  54      @param imageData: image data array 
  55      @type imageWCS: astWCS.WCS 
  56      @param imageWCS: astWCS.WCS object 
  57      @type RADeg: float 
  58      @param RADeg: coordinate in decimal degrees 
  59      @type decDeg: float 
  60      @param decDeg: coordinate in decimal degrees 
  61      @type clipSizeDeg: float or list in format [widthDeg, heightDeg] 
  62      @param clipSizeDeg: if float, size of square clipped section in decimal degrees; if list, 
  63      size of clipped section in degrees in x, y axes of image respectively 
  64      @type returnWCS: bool 
  65      @param returnWCS: if True, return an updated WCS for the clipped section 
  66      @rtype: dictionary 
  67      @return: clipped image section (numpy array), updated astWCS WCS object for 
  68      clipped image section, and coordinates of clipped section in imageData in format  
  69      {'data', 'wcs', 'clippedSection'}. 
  70           
  71      """  
  72       
  73      imHeight=imageData.shape[0] 
  74      imWidth=imageData.shape[1] 
  75      xImScale=imageWCS.getXPixelSizeDeg() 
  76      yImScale=imageWCS.getYPixelSizeDeg() 
  77       
  78      if type(clipSizeDeg) == float: 
  79          xHalfClipSizeDeg=clipSizeDeg/2.0 
  80          yHalfClipSizeDeg=xHalfClipSizeDeg 
  81      elif type(clipSizeDeg) == list or type(clipSizeDeg) == tuple: 
  82          xHalfClipSizeDeg=clipSizeDeg[0]/2.0 
  83          yHalfClipSizeDeg=clipSizeDeg[1]/2.0 
  84      else: 
  85          raise Exception("did not understand clipSizeDeg: should be float, or [widthDeg, heightDeg]") 
  86       
  87      xHalfSizePix=xHalfClipSizeDeg/xImScale 
  88      yHalfSizePix=yHalfClipSizeDeg/yImScale     
  89       
  90      cPixCoords=imageWCS.wcs2pix(RADeg, decDeg) 
  91       
  92      cTopLeft=[cPixCoords[0]+xHalfSizePix, cPixCoords[1]+yHalfSizePix] 
  93      cBottomRight=[cPixCoords[0]-xHalfSizePix, cPixCoords[1]-yHalfSizePix] 
  94           
  95      X=[int(round(cTopLeft[0])),int(round(cBottomRight[0]))] 
  96      Y=[int(round(cTopLeft[1])),int(round(cBottomRight[1]))] 
  97       
  98      X.sort() 
  99      Y.sort() 
 100       
 101      if X[0] < 0: 
 102          X[0]=0 
 103      if X[1] > imWidth: 
 104          X[1]=imWidth 
 105      if Y[0] < 0: 
 106          Y[0]=0 
 107      if Y[1] > imHeight: 
 108          Y[1]=imHeight 
 109       
 110      clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 
 111   
 112       
 113      if returnWCS == True: 
 114          try: 
 115              oldCRPIX1=imageWCS.header['CRPIX1'] 
 116              oldCRPIX2=imageWCS.header['CRPIX2'] 
 117              clippedWCS=imageWCS.copy() 
 118              clippedWCS.header['NAXIS1']=clippedData.shape[1] 
 119              clippedWCS.header['NAXIS2']=clippedData.shape[0] 
 120              clippedWCS.header['CRPIX1']=oldCRPIX1-X[0] 
 121              clippedWCS.header['CRPIX2']=oldCRPIX2-Y[0] 
 122              clippedWCS.updateFromHeader() 
 123               
 124          except KeyError: 
 125               
 126              if REPORT_ERRORS == True: 
 127                   
 128                  print("WARNING: astImages.clipImageSectionWCS() : no CRPIX1, CRPIX2 keywords found - not updating clipped image WCS.") 
 129                   
 130                  clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 
 131                  clippedWCS=imageWCS.copy() 
 132      else: 
 133          clippedWCS=None 
 134       
 135      return {'data': clippedData, 'wcs': clippedWCS, 'clippedSection': [X[0], X[1], Y[0], Y[1]]} 
  136       
 137   
 139      """Clips a square or rectangular section from an image array at the given pixel coordinates. 
 140       
 141      @type imageData: numpy array 
 142      @param imageData: image data array 
 143      @type XCoord: float 
 144      @param XCoord: coordinate in pixels 
 145      @type YCoord: float 
 146      @param YCoord: coordinate in pixels 
 147      @type clipSizePix: float or list in format [widthPix, heightPix] 
 148      @param clipSizePix: if float, size of square clipped section in pixels; if list, 
 149      size of clipped section in pixels in x, y axes of output image respectively 
 150      @rtype: numpy array 
 151      @return: clipped image section 
 152       
 153      """          
 154       
 155      imHeight=imageData.shape[0] 
 156      imWidth=imageData.shape[1] 
 157       
 158      if type(clipSizePix) == float or type(clipSizePix) == int: 
 159          xHalfClipSizePix=int(round(clipSizePix/2.0)) 
 160          yHalfClipSizePix=xHalfClipSizePix 
 161      elif type(clipSizePix) == list or type(clipSizePix) == tuple: 
 162          xHalfClipSizePix=int(round(clipSizePix[0]/2.0)) 
 163          yHalfClipSizePix=int(round(clipSizePix[1]/2.0)) 
 164      else: 
 165          raise Exception("did not understand clipSizePix: should be float, or [widthPix, heightPix]") 
 166          
 167      cTopLeft=[XCoord+xHalfClipSizePix, YCoord+yHalfClipSizePix] 
 168      cBottomRight=[XCoord-xHalfClipSizePix, YCoord-yHalfClipSizePix] 
 169       
 170      X=[int(round(cTopLeft[0])),int(round(cBottomRight[0]))] 
 171      Y=[int(round(cTopLeft[1])),int(round(cBottomRight[1]))] 
 172       
 173      X.sort() 
 174      Y.sort() 
 175       
 176      if X[0] < 0: 
 177          X[0]=0 
 178      if X[1] > imWidth: 
 179          X[1]=imWidth 
 180      if Y[0] < 0: 
 181          Y[0]=0 
 182      if Y[1] > imHeight: 
 183          Y[1]=imHeight            
 184           
 185      return imageData[Y[0]:Y[1],X[0]:X[1]] 
  186       
 187   
 189      """Clips a square or rectangular section from an image array at the given celestial coordinates.  
 190      The resulting clip is rotated and/or flipped such that North is at the top, and East appears at 
 191      the left. An updated WCS for the clipped section is also returned. Note that the alignment 
 192      of the rotated WCS is currently not perfect - however, it is probably good enough in most 
 193      cases for use with L{ImagePlot} for plotting purposes. 
 194       
 195      Note that the clip size is specified in degrees on the sky. For projections that have varying 
 196      real pixel scale across the map (e.g. CEA), use L{clipUsingRADecCoords} instead. 
 197       
 198      @type imageData: numpy array 
 199      @param imageData: image data array 
 200      @type imageWCS: astWCS.WCS 
 201      @param imageWCS: astWCS.WCS object 
 202      @type RADeg: float 
 203      @param RADeg: coordinate in decimal degrees 
 204      @type decDeg: float 
 205      @param decDeg: coordinate in decimal degrees 
 206      @type clipSizeDeg: float 
 207      @param clipSizeDeg: if float, size of square clipped section in decimal degrees; if list, 
 208      size of clipped section in degrees in RA, dec. axes of output rotated image respectively 
 209      @type returnWCS: bool 
 210      @param returnWCS: if True, return an updated WCS for the clipped section 
 211      @rtype: dictionary 
 212      @return: clipped image section (numpy array), updated astWCS WCS object for 
 213      clipped image section, in format {'data', 'wcs'}. 
 214       
 215      @note: Returns 'None' if the requested position is not found within the image. If the image 
 216      WCS does not have keywords of the form CD1_1 etc., the output WCS will not be rotated. 
 217       
 218      """ 
 219           
 220      halfImageSize=imageWCS.getHalfSizeDeg() 
 221      imageCentre=imageWCS.getCentreWCSCoords() 
 222      imScale=imageWCS.getPixelSizeDeg() 
 223   
 224      if type(clipSizeDeg) == float: 
 225          xHalfClipSizeDeg=clipSizeDeg/2.0 
 226          yHalfClipSizeDeg=xHalfClipSizeDeg 
 227      elif type(clipSizeDeg) == list or type(clipSizeDeg) == tuple: 
 228          xHalfClipSizeDeg=clipSizeDeg[0]/2.0 
 229          yHalfClipSizeDeg=clipSizeDeg[1]/2.0 
 230      else: 
 231          raise Exception("did not understand clipSizeDeg: should be float, or [widthDeg, heightDeg]") 
 232       
 233      diagonalHalfSizeDeg=math.sqrt((xHalfClipSizeDeg*xHalfClipSizeDeg) \ 
 234          +(yHalfClipSizeDeg*yHalfClipSizeDeg)) 
 235       
 236      diagonalHalfSizePix=diagonalHalfSizeDeg/imScale 
 237           
 238      if RADeg>imageCentre[0]-halfImageSize[0] and RADeg<imageCentre[0]+halfImageSize[0] \ 
 239          and decDeg>imageCentre[1]-halfImageSize[1] and decDeg<imageCentre[1]+halfImageSize[1]: 
 240           
 241          imageDiagonalClip=clipImageSectionWCS(imageData, imageWCS, RADeg, 
 242                          decDeg, diagonalHalfSizeDeg*2.0) 
 243          diagonalClip=imageDiagonalClip['data'] 
 244          diagonalWCS=imageDiagonalClip['wcs'] 
 245           
 246          rotDeg=diagonalWCS.getRotationDeg() 
 247          imageRotated=ndimage.rotate(diagonalClip, rotDeg) 
 248          if diagonalWCS.isFlipped() == 1: 
 249              imageRotated=pylab.fliplr(imageRotated) 
 250           
 251           
 252          rotatedWCS=diagonalWCS.copy() 
 253          rotRadians=math.radians(rotDeg) 
 254   
 255          if returnWCS == True: 
 256              try: 
 257                   
 258                  CD11=rotatedWCS.header['CD1_1'] 
 259                  CD21=rotatedWCS.header['CD2_1'] 
 260                  CD12=rotatedWCS.header['CD1_2'] 
 261                  CD22=rotatedWCS.header['CD2_2'] 
 262                  if rotatedWCS.isFlipped() == 1: 
 263                      CD11=CD11*-1 
 264                      CD12=CD12*-1 
 265                  CDMatrix=numpy.array([[CD11, CD12], [CD21, CD22]], dtype=numpy.float64) 
 266   
 267                  rotRadians=rotRadians 
 268                  rot11=math.cos(rotRadians) 
 269                  rot12=math.sin(rotRadians) 
 270                  rot21=-math.sin(rotRadians) 
 271                  rot22=math.cos(rotRadians) 
 272                  rotMatrix=numpy.array([[rot11, rot12], [rot21, rot22]], dtype=numpy.float64) 
 273                  newCDMatrix=numpy.dot(rotMatrix, CDMatrix) 
 274   
 275                  P1=diagonalWCS.header['CRPIX1'] 
 276                  P2=diagonalWCS.header['CRPIX2'] 
 277                  V1=diagonalWCS.header['CRVAL1'] 
 278                  V2=diagonalWCS.header['CRVAL2'] 
 279                   
 280                  PMatrix=numpy.zeros((2,), dtype = numpy.float64) 
 281                  PMatrix[0]=P1 
 282                  PMatrix[1]=P2 
 283                   
 284                   
 285                  CMatrix=numpy.array([imageRotated.shape[1]/2.0, imageRotated.shape[0]/2.0]) 
 286                  centreCoords=diagonalWCS.getCentreWCSCoords() 
 287                  alphaRad=math.radians(centreCoords[0]) 
 288                  deltaRad=math.radians(centreCoords[1]) 
 289                  thetaRad=math.asin(math.sin(deltaRad)*math.sin(math.radians(V2)) + \ 
 290                                  math.cos(deltaRad)*math.cos(math.radians(V2))*math.cos(alphaRad-math.radians(V1))) 
 291                  phiRad=math.atan2(-math.cos(deltaRad)*math.sin(alphaRad-math.radians(V1)), \ 
 292                                  math.sin(deltaRad)*math.cos(math.radians(V2)) - \ 
 293                                  math.cos(deltaRad)*math.sin(math.radians(V2))*math.cos(alphaRad-math.radians(V1))) + \ 
 294                                  math.pi 
 295                  RTheta=(180.0/math.pi)*(1.0/math.tan(thetaRad)) 
 296                   
 297                  xy=numpy.zeros((2,), dtype=numpy.float64) 
 298                  xy[0]=RTheta*math.sin(phiRad) 
 299                  xy[1]=-RTheta*math.cos(phiRad) 
 300                  newPMatrix=CMatrix - numpy.dot(numpy.linalg.inv(newCDMatrix), xy) 
 301                   
 302                   
 303                   
 304                   
 305                   
 306                   
 307                  rotatedWCS.header['NAXIS1']=imageRotated.shape[1] 
 308                  rotatedWCS.header['NAXIS2']=imageRotated.shape[0] 
 309                  rotatedWCS.header['CRPIX1']=newPMatrix[0] 
 310                  rotatedWCS.header['CRPIX2']=newPMatrix[1] 
 311                  rotatedWCS.header['CRVAL1']=V1 
 312                  rotatedWCS.header['CRVAL2']=V2 
 313                  rotatedWCS.header['CD1_1']=newCDMatrix[0][0] 
 314                  rotatedWCS.header['CD2_1']=newCDMatrix[1][0] 
 315                  rotatedWCS.header['CD1_2']=newCDMatrix[0][1] 
 316                  rotatedWCS.header['CD2_2']=newCDMatrix[1][1] 
 317                  rotatedWCS.updateFromHeader() 
 318                                   
 319              except KeyError: 
 320                   
 321                  if REPORT_ERRORS == True: 
 322                      print("WARNING: astImages.clipRotatedImageSectionWCS() : no CDi_j keywords found - not rotating WCS.") 
 323                       
 324                  imageRotated=diagonalClip 
 325                  rotatedWCS=diagonalWCS 
 326               
 327          imageRotatedClip=clipImageSectionWCS(imageRotated, rotatedWCS, RADeg, decDeg, clipSizeDeg) 
 328           
 329          if returnWCS == True: 
 330              return {'data': imageRotatedClip['data'], 'wcs': imageRotatedClip['wcs']} 
 331          else: 
 332              return {'data': imageRotatedClip['data'], 'wcs': None} 
 333           
 334      else: 
 335           
 336          if REPORT_ERRORS==True: 
 337              print("""ERROR: astImages.clipRotatedImageSectionWCS() :  
 338              RADeg, decDeg are not within imageData.""") 
 339           
 340          return None 
  341   
 342   
 344      """Clips a section from an image array at the pixel coordinates corresponding to the given 
 345      celestial coordinates. 
 346       
 347      @type imageData: numpy array 
 348      @param imageData: image data array 
 349      @type imageWCS: astWCS.WCS 
 350      @param imageWCS: astWCS.WCS object 
 351      @type RAMin: float 
 352      @param RAMin: minimum RA coordinate in decimal degrees 
 353      @type RAMax: float 
 354      @param RAMax: maximum RA coordinate in decimal degrees 
 355      @type decMin: float 
 356      @param decMin: minimum dec coordinate in decimal degrees 
 357      @type decMax: float 
 358      @param decMax: maximum dec coordinate in decimal degrees 
 359      @type returnWCS: bool 
 360      @param returnWCS: if True, return an updated WCS for the clipped section 
 361      @rtype: dictionary 
 362      @return: clipped image section (numpy array), updated astWCS WCS object for 
 363      clipped image section, and corresponding pixel coordinates in imageData in format  
 364      {'data', 'wcs', 'clippedSection'}. 
 365       
 366      @note: Returns 'None' if the requested position is not found within the image. 
 367       
 368      """ 
 369       
 370      imHeight=imageData.shape[0] 
 371      imWidth=imageData.shape[1] 
 372       
 373      xMin, yMin=imageWCS.wcs2pix(RAMin, decMin) 
 374      xMax, yMax=imageWCS.wcs2pix(RAMax, decMax) 
 375      xMin=int(round(xMin)) 
 376      xMax=int(round(xMax)) 
 377      yMin=int(round(yMin)) 
 378      yMax=int(round(yMax)) 
 379      X=[xMin, xMax] 
 380      X.sort() 
 381      Y=[yMin, yMax] 
 382      Y.sort() 
 383       
 384      if X[0] < 0: 
 385          X[0]=0 
 386      if X[1] > imWidth: 
 387          X[1]=imWidth 
 388      if Y[0] < 0: 
 389          Y[0]=0 
 390      if Y[1] > imHeight: 
 391          Y[1]=imHeight    
 392       
 393      clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 
 394   
 395       
 396      if returnWCS == True: 
 397          try: 
 398              oldCRPIX1=imageWCS.header['CRPIX1'] 
 399              oldCRPIX2=imageWCS.header['CRPIX2'] 
 400              clippedWCS=imageWCS.copy() 
 401              clippedWCS.header['NAXIS1']=clippedData.shape[1] 
 402              clippedWCS.header['NAXIS2']=clippedData.shape[0] 
 403              clippedWCS.header['CRPIX1']=oldCRPIX1-X[0] 
 404              clippedWCS.header['CRPIX2']=oldCRPIX2-Y[0] 
 405              clippedWCS.updateFromHeader() 
 406               
 407          except KeyError: 
 408               
 409              if REPORT_ERRORS == True: 
 410                   
 411                  print("WARNING: astImages.clipUsingRADecCoords() : no CRPIX1, CRPIX2 keywords found - not updating clipped image WCS.") 
 412                   
 413                  clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 
 414                  clippedWCS=imageWCS.copy() 
 415      else: 
 416          clippedWCS=None 
 417       
 418      return {'data': clippedData, 'wcs': clippedWCS, 'clippedSection': [X[0], X[1], Y[0], Y[1]]} 
  419       
 420   
 422      """Scales image array and WCS by the given scale factor. 
 423       
 424      @type imageData: numpy array 
 425      @param imageData: image data array 
 426      @type imageWCS: astWCS.WCS 
 427      @param imageWCS: astWCS.WCS object 
 428      @type scaleFactor: float or list or tuple 
 429      @param scaleFactor: factor to resize image by - if tuple or list, in format  
 430          [x scale factor, y scale factor] 
 431      @rtype: dictionary 
 432      @return: image data (numpy array), updated astWCS WCS object for image, in format {'data', 'wcs'}. 
 433       
 434      """ 
 435   
 436      if type(scaleFactor) == int or type(scaleFactor) == float: 
 437          scaleFactor=[float(scaleFactor), float(scaleFactor)]     
 438      scaledData=ndimage.zoom(imageData, scaleFactor) 
 439       
 440       
 441      properDimensions=numpy.array(imageData.shape)*scaleFactor 
 442      offset=properDimensions-numpy.array(scaledData.shape) 
 443       
 444       
 445      try: 
 446          oldCRPIX1=imageWCS.header['CRPIX1'] 
 447          oldCRPIX2=imageWCS.header['CRPIX2'] 
 448          CD11=imageWCS.header['CD1_1'] 
 449          CD21=imageWCS.header['CD2_1'] 
 450          CD12=imageWCS.header['CD1_2'] 
 451          CD22=imageWCS.header['CD2_2']  
 452      except KeyError: 
 453           
 454          try: 
 455              oldCRPIX1=imageWCS.header['CRPIX1'] 
 456              oldCRPIX2=imageWCS.header['CRPIX2'] 
 457              CD11=imageWCS.header['CDELT1'] 
 458              CD21=0 
 459              CD12=0 
 460              CD22=imageWCS.header['CDELT2'] 
 461          except KeyError: 
 462              if REPORT_ERRORS == True: 
 463                  print("WARNING: astImages.rescaleImage() : no CDij or CDELT keywords found - not updating WCS.") 
 464              scaledWCS=imageWCS.copy() 
 465              return {'data': scaledData, 'wcs': scaledWCS} 
 466   
 467      CDMatrix=numpy.array([[CD11, CD12], [CD21, CD22]], dtype=numpy.float64) 
 468      scaleFactorMatrix=numpy.array([[1.0/scaleFactor[0], 0], [0, 1.0/scaleFactor[1]]]) 
 469      scaledCDMatrix=numpy.dot(scaleFactorMatrix, CDMatrix) 
 470   
 471      scaledWCS=imageWCS.copy() 
 472      scaledWCS.header['NAXIS1']=scaledData.shape[1] 
 473      scaledWCS.header['NAXIS2']=scaledData.shape[0] 
 474      scaledWCS.header['CRPIX1']=oldCRPIX1*scaleFactor[0]+offset[1] 
 475      scaledWCS.header['CRPIX2']=oldCRPIX2*scaleFactor[1]+offset[0] 
 476      scaledWCS.header['CD1_1']=scaledCDMatrix[0][0] 
 477      scaledWCS.header['CD2_1']=scaledCDMatrix[1][0] 
 478      scaledWCS.header['CD1_2']=scaledCDMatrix[0][1] 
 479      scaledWCS.header['CD2_2']=scaledCDMatrix[1][1] 
 480      scaledWCS.updateFromHeader() 
 481       
 482      return {'data': scaledData, 'wcs': scaledWCS} 
  483       
 484   
 486      """Creates a matplotlib.pylab plot of an image array with the specified cuts in intensity 
 487      applied. This routine is used by L{saveBitmap} and L{saveContourOverlayBitmap}, which both 
 488      produce output as .png, .jpg, etc. images. 
 489       
 490      @type imageData: numpy array 
 491      @param imageData: image data array 
 492      @type cutLevels: list 
 493      @param cutLevels: sets the image scaling - available options: 
 494          - pixel values: cutLevels=[low value, high value]. 
 495          - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 
 496          - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 
 497          - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 
 498      ["smart", 99.5] seems to provide good scaling over a range of different images. 
 499      @rtype: dictionary 
 500      @return: image section (numpy.array), matplotlib image normalisation (matplotlib.colors.Normalize), in the format {'image', 'norm'}. 
 501       
 502      @note: If cutLevels[0] == "histEq", then only {'image'} is returned. 
 503       
 504      """ 
 505       
 506      oImWidth=imageData.shape[1] 
 507      oImHeight=imageData.shape[0] 
 508                       
 509       
 510      if cutLevels[0]=="histEq": 
 511           
 512          imageData=histEq(imageData, cutLevels[1]) 
 513          anorm=pylab.Normalize(imageData.min(), imageData.max()) 
 514           
 515      elif cutLevels[0]=="relative": 
 516           
 517           
 518          sorted=numpy.sort(numpy.ravel(imageData))        
 519          maxValue=sorted.max() 
 520          minValue=sorted.min() 
 521           
 522           
 523          topCutIndex=len(sorted-1) \ 
 524              -int(math.floor(float((100.0-cutLevels[1])/100.0)*len(sorted-1))) 
 525          bottomCutIndex=int(math.ceil(float((100.0-cutLevels[1])/100.0)*len(sorted-1))) 
 526          topCut=sorted[topCutIndex] 
 527          bottomCut=sorted[bottomCutIndex] 
 528          anorm=pylab.Normalize(bottomCut, topCut) 
 529           
 530      elif cutLevels[0]=="smart": 
 531           
 532           
 533          sorted=numpy.sort(numpy.ravel(imageData))        
 534          maxValue=sorted.max() 
 535          minValue=sorted.min() 
 536          numBins=10000            
 537          binWidth=(maxValue-minValue)/float(numBins) 
 538          histogram=ndimage.histogram(sorted, minValue, maxValue, numBins) 
 539           
 540           
 541           
 542           
 543           
 544           
 545           
 546          backgroundValue=histogram.max() 
 547          foundBackgroundBin=False 
 548          foundTopBin=False 
 549          lastBin=-10000                                   
 550          for i in range(len(histogram)): 
 551               
 552              if histogram[i]>=lastBin and foundBackgroundBin==True: 
 553                   
 554                   
 555                   
 556                  if (minValue+(binWidth*i))>bottomBinValue*1.1: 
 557                      topBinValue=minValue+(binWidth*i) 
 558                      foundTopBin=True 
 559                      break 
 560               
 561              if histogram[i]==backgroundValue and foundBackgroundBin==False: 
 562                  bottomBinValue=minValue+(binWidth*i) 
 563                  foundBackgroundBin=True 
 564   
 565              lastBin=histogram[i] 
 566           
 567          if foundTopBin==False: 
 568              topBinValue=maxValue 
 569            
 570           
 571          smartClipped=numpy.clip(sorted, bottomBinValue, topBinValue) 
 572          topCutIndex=len(smartClipped-1) \ 
 573              -int(math.floor(float((100.0-cutLevels[1])/100.0)*len(smartClipped-1))) 
 574          bottomCutIndex=int(math.ceil(float((100.0-cutLevels[1])/100.0)*len(smartClipped-1))) 
 575          topCut=smartClipped[topCutIndex] 
 576          bottomCut=smartClipped[bottomCutIndex] 
 577          anorm=pylab.Normalize(bottomCut, topCut) 
 578      else: 
 579           
 580           
 581          anorm=pylab.Normalize(cutLevels[0], cutLevels[1]) 
 582       
 583      if cutLevels[0]=="histEq": 
 584          return {'image': imageData.copy()} 
 585      else: 
 586          return {'image': imageData.copy(), 'norm': anorm} 
  587   
 588   
 590      """Resamples an image and WCS to a tangent plane projection. Purely for plotting purposes 
 591      (e.g., ensuring RA, dec. coordinate axes perpendicular). 
 592       
 593      @type imageData: numpy array 
 594      @param imageData: image data array 
 595      @type imageWCS: astWCS.WCS 
 596      @param imageWCS: astWCS.WCS object 
 597      @type outputPixDimensions: list 
 598      @param outputPixDimensions: [width, height] of output image in pixels 
 599      @rtype: dictionary 
 600      @return: image data (numpy array), updated astWCS WCS object for image, in format {'data', 'wcs'}. 
 601       
 602      """ 
 603       
 604      RADeg, decDeg=imageWCS.getCentreWCSCoords() 
 605      xPixelScale=imageWCS.getXPixelSizeDeg() 
 606      yPixelScale=imageWCS.getYPixelSizeDeg() 
 607      xSizeDeg, ySizeDeg=imageWCS.getFullSizeSkyDeg() 
 608      xSizePix=int(round(outputPixDimensions[0])) 
 609      ySizePix=int(round(outputPixDimensions[1])) 
 610      xRefPix=xSizePix/2.0 
 611      yRefPix=ySizePix/2.0 
 612      xOutPixScale=xSizeDeg/xSizePix 
 613      yOutPixScale=ySizeDeg/ySizePix 
 614      cardList=pyfits.CardList() 
 615      cardList.append(pyfits.Card('NAXIS', 2)) 
 616      cardList.append(pyfits.Card('NAXIS1', xSizePix)) 
 617      cardList.append(pyfits.Card('NAXIS2', ySizePix)) 
 618      cardList.append(pyfits.Card('CTYPE1', 'RA---TAN')) 
 619      cardList.append(pyfits.Card('CTYPE2', 'DEC--TAN')) 
 620      cardList.append(pyfits.Card('CRVAL1', RADeg)) 
 621      cardList.append(pyfits.Card('CRVAL2', decDeg)) 
 622      cardList.append(pyfits.Card('CRPIX1', xRefPix+1)) 
 623      cardList.append(pyfits.Card('CRPIX2', yRefPix+1)) 
 624      cardList.append(pyfits.Card('CDELT1', -xOutPixScale)) 
 625      cardList.append(pyfits.Card('CDELT2', xOutPixScale))     
 626      cardList.append(pyfits.Card('CUNIT1', 'DEG')) 
 627      cardList.append(pyfits.Card('CUNIT2', 'DEG')) 
 628      newHead=pyfits.Header(cards=cardList) 
 629      newWCS=astWCS.WCS(newHead, mode='pyfits') 
 630      newImage=numpy.zeros([ySizePix, xSizePix]) 
 631   
 632      tanImage=resampleToWCS(newImage, newWCS, imageData, imageWCS, highAccuracy=True,  
 633                              onlyOverlapping=False) 
 634       
 635      return tanImage  
  636       
 637   
 638 -def resampleToWCS(im1Data, im1WCS, im2Data, im2WCS, highAccuracy = False, onlyOverlapping = True): 
  639      """Resamples data corresponding to second image (with data im2Data, WCS im2WCS) onto the WCS  
 640      of the first image (im1Data, im1WCS). The output, resampled image is of the pixel same  
 641      dimensions of the first image. This routine is for assisting in plotting - performing  
 642      photometry on the output is not recommended.  
 643       
 644      Set highAccuracy == True to sample every corresponding pixel in each image; otherwise only 
 645      every nth pixel (where n is the ratio of the image scales) will be sampled, with values 
 646      in between being set using a linear interpolation (much faster). 
 647       
 648      Set onlyOverlapping == True to speed up resampling by only resampling the overlapping 
 649      area defined by both image WCSs. 
 650       
 651      @type im1Data: numpy array 
 652      @param im1Data: image data array for first image 
 653      @type im1WCS: astWCS.WCS 
 654      @param im1WCS: astWCS.WCS object corresponding to im1Data 
 655      @type im2Data: numpy array 
 656      @param im2Data: image data array for second image (to be resampled to match first image) 
 657      @type im2WCS: astWCS.WCS 
 658      @param im2WCS: astWCS.WCS object corresponding to im2Data 
 659      @type highAccuracy: bool 
 660      @param highAccuracy: if True, sample every corresponding pixel in each image; otherwise, sample 
 661          every nth pixel, where n = the ratio of the image scales. 
 662      @type onlyOverlapping: bool 
 663      @param onlyOverlapping: if True, only consider the overlapping area defined by both image WCSs 
 664          (speeds things up) 
 665      @rtype: dictionary 
 666      @return: numpy image data array and associated WCS in format {'data', 'wcs'} 
 667       
 668      """ 
 669       
 670      resampledData=numpy.zeros(im1Data.shape) 
 671       
 672       
 673       
 674       
 675      xPixRatio=(im2WCS.getXPixelSizeDeg()/im1WCS.getXPixelSizeDeg())/2.0 
 676      yPixRatio=(im2WCS.getYPixelSizeDeg()/im1WCS.getYPixelSizeDeg())/2.0 
 677      xBorder=xPixRatio*10.0 
 678      yBorder=yPixRatio*10.0 
 679      if highAccuracy == False: 
 680          if xPixRatio > 1: 
 681              xPixStep=int(math.ceil(xPixRatio)) 
 682          else: 
 683              xPixStep=1 
 684          if yPixRatio > 1: 
 685              yPixStep=int(math.ceil(yPixRatio)) 
 686          else: 
 687              yPixStep=1 
 688      else: 
 689          xPixStep=1 
 690          yPixStep=1 
 691       
 692      if onlyOverlapping == True: 
 693          overlap=astWCS.findWCSOverlap(im1WCS, im2WCS) 
 694          xOverlap=[overlap['wcs1Pix'][0], overlap['wcs1Pix'][1]] 
 695          yOverlap=[overlap['wcs1Pix'][2], overlap['wcs1Pix'][3]] 
 696          xOverlap.sort() 
 697          yOverlap.sort() 
 698          xMin=int(math.floor(xOverlap[0]-xBorder)) 
 699          xMax=int(math.ceil(xOverlap[1]+xBorder)) 
 700          yMin=int(math.floor(yOverlap[0]-yBorder)) 
 701          yMax=int(math.ceil(yOverlap[1]+yBorder)) 
 702          xRemainder=(xMax-xMin) % xPixStep 
 703          yRemainder=(yMax-yMin) % yPixStep 
 704          if xRemainder != 0: 
 705              xMax=xMax+xRemainder 
 706          if yRemainder != 0: 
 707              yMax=yMax+yRemainder 
 708           
 709          if xMin < 0: 
 710              xMin=0 
 711          if xMax > im1Data.shape[1]: 
 712              xMax=im1Data.shape[1] 
 713          if yMin < 0: 
 714              yMin=0 
 715          if yMax > im1Data.shape[0]: 
 716              yMax=im1Data.shape[0] 
 717      else: 
 718          xMin=0 
 719          xMax=im1Data.shape[1] 
 720          yMin=0 
 721          yMax=im1Data.shape[0] 
 722       
 723      for x in range(xMin, xMax, xPixStep): 
 724          for y in range(yMin, yMax, yPixStep): 
 725              RA, dec=im1WCS.pix2wcs(x, y) 
 726              x2, y2=im2WCS.wcs2pix(RA, dec) 
 727              x2=int(round(x2)) 
 728              y2=int(round(y2)) 
 729              if x2 >= 0 and x2 < im2Data.shape[1] and y2 >= 0 and y2 < im2Data.shape[0]: 
 730                  resampledData[y][x]=im2Data[y2][x2] 
 731   
 732       
 733      if highAccuracy == False: 
 734          for row in range(resampledData.shape[0]): 
 735              vals=resampledData[row, numpy.arange(xMin, xMax, xPixStep)] 
 736              index2data=interpolate.interp1d(numpy.arange(0, vals.shape[0], 1), vals) 
 737              interpedVals=index2data(numpy.arange(0, vals.shape[0]-1, 1.0/xPixStep)) 
 738              resampledData[row, xMin:xMin+interpedVals.shape[0]]=interpedVals 
 739          for col in range(resampledData.shape[1]): 
 740              vals=resampledData[numpy.arange(yMin, yMax, yPixStep), col] 
 741              index2data=interpolate.interp1d(numpy.arange(0, vals.shape[0], 1), vals) 
 742              interpedVals=index2data(numpy.arange(0, vals.shape[0]-1, 1.0/yPixStep)) 
 743              resampledData[yMin:yMin+interpedVals.shape[0], col]=interpedVals 
 744           
 745       
 746       
 747      return {'data': resampledData, 'wcs': im1WCS.copy()} 
  748       
 749   
 750 -def generateContourOverlay(backgroundImageData, backgroundImageWCS, contourImageData, contourImageWCS, \ 
 751                              contourLevels, contourSmoothFactor = 0, highAccuracy = False): 
  752      """Rescales an image array to be used as a contour overlay to have the same dimensions as the  
 753      background image, and generates a set of contour levels. The image array from which the contours  
 754      are to be generated will be resampled to the same dimensions as the background image data, and  
 755      can be optionally smoothed using a Gaussian filter. The sigma of the Gaussian filter  
 756      (contourSmoothFactor) is specified in arcsec. 
 757       
 758      @type backgroundImageData: numpy array 
 759      @param backgroundImageData: background image data array 
 760      @type backgroundImageWCS: astWCS.WCS 
 761      @param backgroundImageWCS: astWCS.WCS object of the background image data array 
 762      @type contourImageData: numpy array 
 763      @param contourImageData: image data array from which contours are to be generated 
 764      @type contourImageWCS: astWCS.WCS 
 765      @param contourImageWCS: astWCS.WCS object corresponding to contourImageData 
 766      @type contourLevels: list 
 767      @param contourLevels: sets the contour levels - available options: 
 768          - values: contourLevels=[list of values specifying each level] 
 769          - linear spacing: contourLevels=['linear', min level value, max level value, number 
 770          of levels] - can use "min", "max" to automatically set min, max levels from image data 
 771          - log spacing: contourLevels=['log', min level value, max level value, number of 
 772          levels] - can use "min", "max" to automatically set min, max levels from image data 
 773      @type contourSmoothFactor: float 
 774      @param contourSmoothFactor: standard deviation (in arcsec) of Gaussian filter for 
 775      pre-smoothing of contour image data (set to 0 for no smoothing) 
 776      @type highAccuracy: bool 
 777      @param highAccuracy: if True, sample every corresponding pixel in each image; otherwise, sample 
 778          every nth pixel, where n = the ratio of the image scales. 
 779       
 780      """  
 781       
 782       
 783       
 784       
 785      if ("CD1_1" in backgroundImageWCS.header) == True: 
 786          xScaleFactor=backgroundImageWCS.getXPixelSizeDeg()/(contourImageWCS.getXPixelSizeDeg()/5.0) 
 787          yScaleFactor=backgroundImageWCS.getYPixelSizeDeg()/(contourImageWCS.getYPixelSizeDeg()/5.0) 
 788          scaledBackground=scaleImage(backgroundImageData, backgroundImageWCS, (xScaleFactor, yScaleFactor)) 
 789          scaled=resampleToWCS(scaledBackground['data'], scaledBackground['wcs'],  
 790                                  contourImageData, contourImageWCS, highAccuracy = highAccuracy) 
 791          scaledContourData=scaled['data'] 
 792          scaledContourWCS=scaled['wcs'] 
 793          scaledBackground=True 
 794      else: 
 795          scaled=resampleToWCS(backgroundImageData, backgroundImageWCS,  
 796                                  contourImageData, contourImageWCS, highAccuracy = highAccuracy) 
 797          scaledContourData=scaled['data'] 
 798          scaledContourWCS=scaled['wcs'] 
 799          scaledBackground=False 
 800   
 801      if contourSmoothFactor > 0: 
 802          sigmaPix=(contourSmoothFactor/3600.0)/scaledContourWCS.getPixelSizeDeg() 
 803          scaledContourData=ndimage.gaussian_filter(scaledContourData, sigmaPix) 
 804       
 805       
 806       
 807      if contourLevels[0] == "linear": 
 808          if contourLevels[1] == "min": 
 809              xMin=contourImageData.flatten().min() 
 810          else: 
 811              xMin=float(contourLevels[1]) 
 812          if contourLevels[2] == "max": 
 813              xMax=contourImageData.flatten().max() 
 814          else: 
 815              xMax=float(contourLevels[2])         
 816          nLevels=contourLevels[3] 
 817          xStep=(xMax-xMin)/(nLevels-1) 
 818          cLevels=[] 
 819          for j in range(nLevels+1): 
 820              level=xMin+j*xStep 
 821              cLevels.append(level) 
 822       
 823      elif contourLevels[0] == "log": 
 824          if contourLevels[1] == "min": 
 825              xMin=contourImageData.flatten().min() 
 826          else: 
 827              xMin=float(contourLevels[1]) 
 828          if contourLevels[2] == "max": 
 829              xMax=contourImageData.flatten().max() 
 830          else: 
 831              xMax=float(contourLevels[2])      
 832          if xMin <= 0.0: 
 833              raise Exception("minimum contour level set to <= 0 and log scaling chosen.") 
 834          xLogMin=math.log10(xMin) 
 835          xLogMax=math.log10(xMax) 
 836          nLevels=contourLevels[3] 
 837          xLogStep=(xLogMax-xLogMin)/(nLevels-1) 
 838          cLevels=[] 
 839          prevLevel=0 
 840          for j in range(nLevels+1): 
 841              level=math.pow(10, xLogMin+j*xLogStep) 
 842              cLevels.append(level)                        
 843           
 844      else: 
 845          cLevels=contourLevels 
 846       
 847       
 848      if scaledBackground == True: 
 849          scaledBack=scaleImage(scaledContourData, scaledContourWCS, (1.0/xScaleFactor, 1.0/yScaleFactor))['data'] 
 850      else: 
 851          scaledBack=scaledContourData 
 852       
 853      return {'scaledImage': scaledBack, 'contourLevels': cLevels} 
  854       
 855   
 856 -def saveBitmap(outputFileName, imageData, cutLevels, size, colorMapName): 
  857      """Makes a bitmap image from an image array; the image format is specified by the 
 858      filename extension. (e.g. ".jpg" =JPEG, ".png"=PNG). 
 859       
 860      @type outputFileName: string 
 861      @param outputFileName: filename of output bitmap image 
 862      @type imageData: numpy array 
 863      @param imageData: image data array 
 864      @type cutLevels: list 
 865      @param cutLevels: sets the image scaling - available options: 
 866          - pixel values: cutLevels=[low value, high value]. 
 867          - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 
 868          - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 
 869          - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 
 870      ["smart", 99.5] seems to provide good scaling over a range of different images.  
 871      @type size: int 
 872      @param size: size of output image in pixels 
 873      @type colorMapName: string 
 874      @param colorMapName: name of a standard matplotlib colormap, e.g. "hot", "cool", "gray" 
 875      etc. (do "help(pylab.colormaps)" in the Python interpreter to see available options) 
 876       
 877      """          
 878       
 879      cut=intensityCutImage(imageData, cutLevels) 
 880       
 881       
 882      aspectR=float(cut['image'].shape[0])/float(cut['image'].shape[1]) 
 883      pylab.figure(figsize=(10,10*aspectR)) 
 884      pylab.axes([0,0,1,1]) 
 885           
 886      try: 
 887          colorMap=pylab.cm.get_cmap(colorMapName) 
 888      except AssertionError: 
 889          raise Exception(colorMapName+" is not a defined matplotlib colormap.") 
 890       
 891      if cutLevels[0]=="histEq": 
 892          pylab.imshow(cut['image'],  interpolation="bilinear", origin='lower', cmap=colorMap) 
 893       
 894      else: 
 895          pylab.imshow(cut['image'],  interpolation="bilinear",  norm=cut['norm'], origin='lower', 
 896              cmap=colorMap) 
 897   
 898      pylab.axis("off") 
 899       
 900      pylab.savefig("out_astImages.png")   
 901      pylab.close("all") 
 902       
 903      try: 
 904          from PIL import Image 
 905      except: 
 906          raise Exception("astImages.saveBitmap requires the Python Imaging Library to be installed.") 
 907      im=Image.open("out_astImages.png") 
 908      im.thumbnail((int(size),int(size))) 
 909      im.save(outputFileName) 
 910       
 911      os.remove("out_astImages.png") 
  912   
 913   
 914 -def saveContourOverlayBitmap(outputFileName, backgroundImageData, backgroundImageWCS, cutLevels, \ 
 915                                  size, colorMapName, contourImageData, contourImageWCS, \ 
 916                                  contourSmoothFactor, contourLevels, contourColor, contourWidth): 
  917      """Makes a bitmap image from an image array, with a set of contours generated from a 
 918      second image array overlaid. The image format is specified by the file extension 
 919      (e.g. ".jpg"=JPEG, ".png"=PNG). The image array from which the contours are to be generated 
 920      can optionally be pre-smoothed using a Gaussian filter.  
 921       
 922      @type outputFileName: string 
 923      @param outputFileName: filename of output bitmap image 
 924      @type backgroundImageData: numpy array 
 925      @param backgroundImageData: background image data array 
 926      @type backgroundImageWCS: astWCS.WCS 
 927      @param backgroundImageWCS: astWCS.WCS object of the background image data array 
 928      @type cutLevels: list 
 929      @param cutLevels: sets the image scaling - available options: 
 930          - pixel values: cutLevels=[low value, high value]. 
 931          - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 
 932          - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 
 933          - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 
 934      ["smart", 99.5] seems to provide good scaling over a range of different images.  
 935      @type size: int 
 936      @param size: size of output image in pixels 
 937      @type colorMapName: string 
 938      @param colorMapName: name of a standard matplotlib colormap, e.g. "hot", "cool", "gray" 
 939      etc. (do "help(pylab.colormaps)" in the Python interpreter to see available options) 
 940      @type contourImageData: numpy array 
 941      @param contourImageData: image data array from which contours are to be generated 
 942      @type contourImageWCS: astWCS.WCS 
 943      @param contourImageWCS: astWCS.WCS object corresponding to contourImageData 
 944      @type contourSmoothFactor: float 
 945      @param contourSmoothFactor: standard deviation (in pixels) of Gaussian filter for 
 946      pre-smoothing of contour image data (set to 0 for no smoothing) 
 947      @type contourLevels: list 
 948      @param contourLevels: sets the contour levels - available options: 
 949          - values: contourLevels=[list of values specifying each level] 
 950          - linear spacing: contourLevels=['linear', min level value, max level value, number 
 951          of levels] - can use "min", "max" to automatically set min, max levels from image data 
 952          - log spacing: contourLevels=['log', min level value, max level value, number of 
 953          levels] - can use "min", "max" to automatically set min, max levels from image data 
 954      @type contourColor: string 
 955      @param contourColor: color of the overlaid contours, specified by the name of a standard 
 956      matplotlib color, e.g., "black", "white", "cyan" 
 957      etc. (do "help(pylab.colors)" in the Python interpreter to see available options) 
 958      @type contourWidth: int 
 959      @param contourWidth: width of the overlaid contours 
 960       
 961      """  
 962       
 963      cut=intensityCutImage(backgroundImageData, cutLevels) 
 964       
 965       
 966      aspectR=float(cut['image'].shape[0])/float(cut['image'].shape[1]) 
 967      pylab.figure(figsize=(10,10*aspectR)) 
 968      pylab.axes([0,0,1,1]) 
 969           
 970      try: 
 971          colorMap=pylab.cm.get_cmap(colorMapName) 
 972      except AssertionError: 
 973          raise Exception(colorMapName+" is not a defined matplotlib colormap.") 
 974       
 975      if cutLevels[0]=="histEq": 
 976          pylab.imshow(cut['image'],  interpolation="bilinear", origin='lower', cmap=colorMap) 
 977       
 978      else: 
 979          pylab.imshow(cut['image'],  interpolation="bilinear",  norm=cut['norm'], origin='lower', 
 980              cmap=colorMap) 
 981   
 982      pylab.axis("off") 
 983   
 984       
 985      contourData=generateContourOverlay(backgroundImageData, backgroundImageWCS, contourImageData, \ 
 986                                          contourImageWCS, contourLevels, contourSmoothFactor) 
 987       
 988      pylab.contour(contourData['scaledImage'], contourData['contourLevels'], colors=contourColor, 
 989          linewidths=contourWidth)         
 990               
 991      pylab.savefig("out_astImages.png")   
 992      pylab.close("all") 
 993       
 994      try: 
 995          from PIL import Image 
 996      except: 
 997          raise Exception("astImages.saveContourOverlayBitmap requires the Python Imaging Library to be installed") 
 998       
 999      im=Image.open("out_astImages.png") 
1000      im.thumbnail((int(size),int(size))) 
1001      im.save(outputFileName) 
1002           
1003      os.remove("out_astImages.png") 
 1004       
1005   
1006 -def saveFITS(outputFileName, imageData, imageWCS = None): 
 1007      """Writes an image array to a new .fits file. 
1008       
1009      @type outputFileName: string 
1010      @param outputFileName: filename of output FITS image 
1011      @type imageData: numpy array 
1012      @param imageData: image data array 
1013      @type imageWCS: astWCS.WCS object 
1014      @param imageWCS: image WCS object 
1015       
1016      @note: If imageWCS=None, the FITS image will be written with a rudimentary header containing 
1017      no meta data. 
1018       
1019      """ 
1020       
1021      if os.path.exists(outputFileName): 
1022          os.remove(outputFileName) 
1023           
1024      newImg=pyfits.HDUList() 
1025       
1026      if imageWCS!=None: 
1027          hdu=pyfits.PrimaryHDU(None, imageWCS.header) 
1028      else: 
1029          hdu=pyfits.PrimaryHDU(None, None) 
1030       
1031      hdu.data=imageData 
1032      newImg.append(hdu) 
1033      newImg.writeto(outputFileName) 
1034      newImg.close() 
 1035       
1036   
1037 -def histEq(inputArray, numBins): 
 1038      """Performs histogram equalisation of the input numpy array. 
1039       
1040      @type inputArray: numpy array 
1041      @param inputArray: image data array 
1042      @type numBins: int 
1043      @param numBins: number of bins in which to perform the operation (e.g. 1024) 
1044      @rtype: numpy array 
1045      @return: image data array 
1046       
1047      """ 
1048       
1049      imageData=inputArray 
1050       
1051       
1052      sortedDataIntensities=numpy.sort(numpy.ravel(imageData))     
1053      median=numpy.median(sortedDataIntensities) 
1054       
1055       
1056      dataCumHist=numpy.zeros(numBins) 
1057      minIntensity=sortedDataIntensities.min()     
1058      maxIntensity=sortedDataIntensities.max() 
1059      histRange=maxIntensity-minIntensity 
1060      binWidth=histRange/float(numBins-1) 
1061      for i in range(len(sortedDataIntensities)): 
1062          binNumber=int(math.ceil((sortedDataIntensities[i]-minIntensity)/binWidth)) 
1063          addArray=numpy.zeros(numBins) 
1064          onesArray=numpy.ones(numBins-binNumber) 
1065          onesRange=list(range(binNumber, numBins)) 
1066          numpy.put(addArray, onesRange, onesArray) 
1067          dataCumHist=dataCumHist+addArray 
1068                   
1069       
1070      idealValue=dataCumHist.max()/float(numBins) 
1071      idealCumHist=numpy.arange(idealValue, dataCumHist.max()+idealValue, idealValue) 
1072       
1073       
1074      for y in range(imageData.shape[0]): 
1075          for x in range(imageData.shape[1]): 
1076               
1077              intensityBin=int(math.ceil((imageData[y][x]-minIntensity)/binWidth)) 
1078               
1079               
1080              if intensityBin<0: 
1081                  intensityBin=0 
1082              if intensityBin>len(dataCumHist)-1: 
1083                  intensityBin=len(dataCumHist)-1 
1084           
1085               
1086              dataCumFreq=dataCumHist[intensityBin] 
1087               
1088               
1089              idealBin=numpy.searchsorted(idealCumHist, dataCumFreq) 
1090              idealIntensity=(idealBin*binWidth)+minIntensity 
1091              imageData[y][x]=idealIntensity       
1092           
1093      return imageData 
 1094   
1095   
1097      """Clips the inputArray in intensity and normalises the array such that minimum and maximum 
1098      values are 0, 1. Clip in intensity is specified by clipMinMax, a list in the format  
1099      [clipMin, clipMax] 
1100       
1101      Used for normalising image arrays so that they can be turned into RGB arrays that matplotlib 
1102      can plot (see L{astPlots.ImagePlot}). 
1103       
1104      @type inputArray: numpy array 
1105      @param inputArray: image data array 
1106      @type clipMinMax: list 
1107      @param clipMinMax: [minimum value of clipped array, maximum value of clipped array] 
1108      @rtype: numpy array 
1109      @return: normalised array with minimum value 0, maximum value 1 
1110   
1111      """ 
1112      clipped=inputArray.clip(clipMinMax[0], clipMinMax[1]) 
1113      slope=1.0/(clipMinMax[1]-clipMinMax[0]) 
1114      intercept=-clipMinMax[0]*slope 
1115      clipped=clipped*slope+intercept 
1116       
1117      return clipped 
 1118